Skip to content

feat(yarn): implement get cache directory path and remove npm --dry-run logic #4085

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Nov 5, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 16 additions & 2 deletions lib/base-package-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ export class BasePackageManager {
constructor(
protected $childProcess: IChildProcess,
private $hostInfo: IHostInfo,
private $pacoteService: IPacoteService,
private packageManager: string
) { }

Expand All @@ -17,10 +18,23 @@ export class BasePackageManager {
return npmExecutableName;
}

protected async processPackageManagerInstall(params: string[], opts: { cwd: string }) {
protected async processPackageManagerInstall(packageName: string, params: string[], opts: { cwd: string, isInstallingAllDependencies: boolean }): Promise<INpmInstallResultInfo> {
const npmExecutable = this.getPackageManagerExecutableName();
const stdioValue = isInteractive() ? "inherit" : "pipe";
return await this.$childProcess.spawnFromEvent(npmExecutable, params, "close", { cwd: opts.cwd, stdio: stdioValue });
await this.$childProcess.spawnFromEvent(npmExecutable, params, "close", { cwd: opts.cwd, stdio: stdioValue });

// Whenever calling "npm install" or "yarn add" without any arguments (hence installing all dependencies) no output is emitted on stdout
// Luckily, whenever you call "npm install" or "yarn add" to install all dependencies chances are you won't need the name/version of the package you're installing because there is none.
const { isInstallingAllDependencies } = opts;
if (isInstallingAllDependencies) {
return null;
}

const packageMetadata = await this.$pacoteService.manifest(packageName);
return {
name: packageMetadata.name,
version: packageMetadata.version
};
}

protected getFlagsString(config: any, asArray: boolean): any {
Expand Down
2 changes: 1 addition & 1 deletion lib/definitions/pacote-service.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ declare global {
* @param packageName The name of the package
* @param options The provided options can control which properties from package.json file will be returned. In case when fullMetadata option is provided, all data from package.json file will be returned.
*/
manifest(packageName: string, options: IPacoteManifestOptions): Promise<any>;
manifest(packageName: string, options?: IPacoteManifestOptions): Promise<any>;
/**
* Downloads the specified package and extracts it in specified destination directory
* @param packageName The name of the package
Expand Down
98 changes: 5 additions & 93 deletions lib/node-package-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,15 @@ import { exported, cache } from "./common/decorators";
import { CACACHE_DIRECTORY_NAME } from "./constants";

export class NodePackageManager extends BasePackageManager implements INodePackageManager {
private static SCOPED_DEPENDENCY_REGEXP = /^(@.+?)(?:@(.+?))?$/;
private static DEPENDENCY_REGEXP = /^(.+?)(?:@(.+?))?$/;

constructor(
$childProcess: IChildProcess,
private $errors: IErrors,
private $fs: IFileSystem,
$hostInfo: IHostInfo,
private $logger: ILogger,
private $httpClient: Server.IHttpClient) {
super($childProcess, $hostInfo, 'npm');
private $httpClient: Server.IHttpClient,
$pacoteService: IPacoteService) {
super($childProcess, $hostInfo, $pacoteService, 'npm');
}

@exported("npm")
Expand Down Expand Up @@ -56,21 +54,8 @@ export class NodePackageManager extends BasePackageManager implements INodePacka
}

try {
const spawnResult: ISpawnResult = await this.processPackageManagerInstall(params, { cwd });

// Whenever calling npm install without any arguments (hence installing all dependencies) no output is emitted on stdout
// Luckily, whenever you call npm install to install all dependencies chances are you won't need the name/version of the package you're installing because there is none.
if (isInstallingAllDependencies) {
return null;
}

params = params.concat(["--json", "--dry-run", "--prefix", cwd]);
// After the actual install runs successfully execute a dry-run in order to get information about the package.
// We cannot use the actual install with --json to get the information because of post-install scripts which may print on stdout
// dry-run install is quite fast when the dependencies are already installed even for many dependencies (e.g. angular) so we can live with this approach
// We need the --prefix here because without it no output is emitted on stdout because all the dependencies are already installed.
const spawnNpmDryRunResult = await this.$childProcess.spawnFromEvent(this.getPackageManagerExecutableName(), params, "close");
return this.parseNpmInstallResult(spawnNpmDryRunResult.stdout, spawnResult.stdout, packageName);
const result = await this.processPackageManagerInstall(packageName, params, { cwd, isInstallingAllDependencies });
return result;
} catch (err) {
if (err.message && err.message.indexOf("EPEERINVALID") !== -1) {
// Not installed peer dependencies are treated by npm 2 as errors, but npm 3 treats them as warnings.
Expand Down Expand Up @@ -138,79 +123,6 @@ export class NodePackageManager extends BasePackageManager implements INodePacka
const cachePath = await this.$childProcess.exec(`npm config get cache`);
return path.join(cachePath.trim(), CACACHE_DIRECTORY_NAME);
}

private parseNpmInstallResult(npmDryRunInstallOutput: string, npmInstallOutput: string, userSpecifiedPackageName: string): INpmInstallResultInfo {
// TODO: Add tests for this functionality
try {
const originalOutput: INpmInstallCLIResult | INpm5InstallCliResult = JSON.parse(npmDryRunInstallOutput);
const npm5Output = <INpm5InstallCliResult>originalOutput;
const npmOutput = <INpmInstallCLIResult>originalOutput;
let name: string;
_.forOwn(npmOutput.dependencies, (peerDependency: INpmPeerDependencyInfo, key: string) => {
if (!peerDependency.required && !peerDependency.peerMissing) {
name = key;
return false;
}
});

// Npm 5 return different object after performing `npm install --dry-run`.
// We find the correct dependency by searching for the `userSpecifiedPackageName` in the
// `npm5Output.updated` array and as a fallback, considering that the dependency is already installed,
// we find it as the first element.
if (!name && npm5Output.updated) {
const packageNameWithoutVersion = userSpecifiedPackageName.split('@')[0];
const updatedDependency = _.find(npm5Output.updated, ['name', packageNameWithoutVersion]) || npm5Output.updated[0];
return {
name: updatedDependency.name,
originalOutput,
version: updatedDependency.version
};
}
const dependency = _.pick<INpmDependencyInfo, INpmDependencyInfo | INpmPeerDependencyInfo>(npmOutput.dependencies, name);
return {
name,
originalOutput,
version: dependency[name].version
};
} catch (err) {
this.$logger.trace(`Unable to parse result of npm --dry-run operation. Output is: ${npmDryRunInstallOutput}.`);
this.$logger.trace("Now we'll try to parse the real output of npm install command.");

const npmOutputMatchRegExp = /^.--\s+(?!UNMET)(.*)@((?:\d+\.){2}\d+)/m;
const match = npmInstallOutput.match(npmOutputMatchRegExp);
if (match) {
return {
name: match[1],
version: match[2]
};
}
}

this.$logger.trace("Unable to get information from npm installation, trying to return value specified by user.");
return this.getDependencyInformation(userSpecifiedPackageName);
}

private getDependencyInformation(dependency: string): INpmInstallResultInfo {
const scopeDependencyMatch = dependency.match(NodePackageManager.SCOPED_DEPENDENCY_REGEXP);
let name: string = null;
let version: string = null;

if (scopeDependencyMatch) {
name = scopeDependencyMatch[1];
version = scopeDependencyMatch[2];
} else {
const matches = dependency.match(NodePackageManager.DEPENDENCY_REGEXP);
if (matches) {
name = matches[1];
version = matches[2];
}
}

return {
name,
version
};
}
}

$injector.register("npm", NodePackageManager);
15 changes: 11 additions & 4 deletions lib/services/pacote-service.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,19 @@
import * as pacote from "pacote";
import * as tar from "tar";
import * as path from "path";
import { cache } from "../common/decorators";

export class PacoteService implements IPacoteService {
constructor(private $fs: IFileSystem,
private $npm: INodePackageManager,
private $proxyService: IProxyService,
private $logger: ILogger) { }
private $injector: IInjector,
private $logger: ILogger,
private $proxyService: IProxyService) { }

@cache()
public get $packageManager(): INodePackageManager {
// need to be resolved here due to cyclic dependency
return this.$injector.resolve("packageManager");
}

public async manifest(packageName: string, options?: IPacoteManifestOptions): Promise<any> {
this.$logger.trace(`Calling pacoteService.manifest for packageName: '${packageName}' and options: ${options}`);
Expand Down Expand Up @@ -59,7 +66,7 @@ export class PacoteService implements IPacoteService {

private async getPacoteBaseOptions(): Promise<IPacoteBaseOptions> {
// In case `tns create myapp --template https://github.com/NativeScript/template-hello-world.git` command is executed, pacote module throws an error if cache option is not provided.
const cache = await this.$npm.getCachePath();
const cache = await this.$packageManager.getCachePath();
const pacoteOptions = { cache };
const proxySettings = await this.$proxyService.getCache();
if (proxySettings) {
Expand Down
24 changes: 7 additions & 17 deletions lib/yarn-package-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,9 @@ export class YarnPackageManager extends BasePackageManager implements INodePacka
$hostInfo: IHostInfo,
private $httpClient: Server.IHttpClient,
private $logger: ILogger,
private $pacoteService: IPacoteService
$pacoteService: IPacoteService
) {
super($childProcess, $hostInfo, 'yarn');
super($childProcess, $hostInfo, $pacoteService, 'yarn');
}

@exported("yarn")
Expand All @@ -39,18 +39,8 @@ export class YarnPackageManager extends BasePackageManager implements INodePacka
const cwd = pathToSave;

try {
await this.processPackageManagerInstall(params, { cwd });

if (isInstallingAllDependencies) {
return null;
}

const packageMetadata = await this.$pacoteService.manifest(packageName, {});
return {
name: packageMetadata.name,
version: packageMetadata.version
};

const result = await this.processPackageManagerInstall(packageName, params, { cwd, isInstallingAllDependencies });
return result;
} catch (e) {
this.$fs.writeJson(packageJsonPath, jsonContentBefore);
throw e;
Expand Down Expand Up @@ -102,9 +92,9 @@ export class YarnPackageManager extends BasePackageManager implements INodePacka
}

@exported("yarn")
getCachePath(): Promise<string> {
this.$errors.fail("Method not implemented");
return null;
public async getCachePath(): Promise<string> {
const result = await this.$childProcess.exec(`yarn cache dir`);
return result;
}
}

Expand Down
20 changes: 20 additions & 0 deletions test/plugins-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,26 @@ function createTestInjector() {
generateHashes: async (files: string[]): Promise<IStringDictionary> => ({})
});
testInjector.register("pacoteService", {
manifest: async (packageName: string) => {
const projectData = testInjector.resolve("projectData");
const fs = testInjector.resolve("fs");
let result = {};
let packageJsonPath = null;

const packageToInstall = packageName.split("@")[0];

if (fs.exists(packageToInstall)) {
packageJsonPath = path.join(packageName, "package.json");
} else {
packageJsonPath = path.join(projectData.projectDir, "node_modules", packageToInstall, "package.json");
}

if (fs.exists(packageJsonPath)) {
result = fs.readJson(packageJsonPath);
}

return result;
},
extractPackage: async (packageName: string, destinationDirectory: string, options?: IPacoteExtractOptions): Promise<void> => undefined
});
return testInjector;
Expand Down