Skip to content

feat() - [Yarn Support - Part 1 and 2] Yarn and Package Manager implementation #4050

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 10 commits into from
Nov 1, 2018
Merged
Show file tree
Hide file tree
Changes from 9 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
46 changes: 46 additions & 0 deletions lib/base-package-manager.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { isInteractive } from "./common/helpers";

export class BasePackageManager {
constructor(
protected $childProcess: IChildProcess,
private $hostInfo: IHostInfo,
private packageManager: string
) { }

protected getPackageManagerExecutableName(): string {
let npmExecutableName = this.packageManager;

if (this.$hostInfo.isWindows) {
npmExecutableName += ".cmd";
}

return npmExecutableName;
}

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

protected getFlagsString(config: any, asArray: boolean): any {
const array: Array<string> = [];
for (const flag in config) {
if (flag === "global" && this.packageManager !== 'yarn') {
array.push(`--${flag}`);
array.push(`${config[flag]}`);
} else if (config[flag]) {
if (flag === "dist-tags" || flag === "versions") {
array.push(` ${flag}`);
continue;
}
array.push(`--${flag}`);
}
}
if (asArray) {
return array;
}

return array.join(" ");
}
}
4 changes: 4 additions & 0 deletions lib/bootstrap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,11 @@ $injector.require("itmsTransporterService", "./services/itmstransporter-service"
$injector.requireCommand("setup|*", "./commands/setup");
$injector.requireCommand(["setup|cloud", "cloud|setup"], "./commands/setup");

$injector.requirePublic("packageManager", "./package-manager");
$injector.requirePublic("npm", "./node-package-manager");
$injector.requirePublic("yarn", "./yarn-package-manager");
$injector.requireCommand("package-manager|set", "./commands/package-manager-set");

$injector.require("npmInstallationManager", "./npm-installation-manager");
$injector.require("dynamicHelpProvider", "./dynamic-help-provider");
$injector.require("mobilePlatformsCapabilities", "./mobile-platforms-capabilities");
Expand Down
4 changes: 2 additions & 2 deletions lib/commands/install.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ export class InstallCommand implements ICommand {
private $logger: ILogger,
private $fs: IFileSystem,
private $stringParameter: ICommandParameter,
private $npm: INodePackageManager) {
private $packageManager: INodePackageManager) {
this.$projectData.initializeProjectData();
}

Expand Down Expand Up @@ -54,7 +54,7 @@ export class InstallCommand implements ICommand {
moduleName = devPrefix + moduleName;
}

await this.$npm.install(moduleName, projectDir, {
await this.$packageManager.install(moduleName, projectDir, {
'save-dev': true,
disableNpmInstall: this.$options.disableNpmInstall,
frameworkPath: this.$options.frameworkPath,
Expand Down
4 changes: 2 additions & 2 deletions lib/commands/plugin/create-plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ export class CreatePluginCommand implements ICommand {
private $fs: IFileSystem,
private $childProcess: IChildProcess,
private $prompter: IPrompter,
private $npm: INodePackageManager) { }
private $packageManager: INodePackageManager) { }

public async execute(args: string[]): Promise<void> {
const pluginRepoName = args[0];
Expand Down Expand Up @@ -45,7 +45,7 @@ export class CreatePluginCommand implements ICommand {
try {
spinner.start();
const npmOptions: any = { silent: true };
await this.$npm.install(cwd, cwd, npmOptions);
await this.$packageManager.install(cwd, cwd, npmOptions);
} finally {
spinner.stop();
}
Expand Down
6 changes: 3 additions & 3 deletions lib/commands/test-init.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ class TestInitCommand implements ICommand {
mocha: ['chai']
};

constructor(private $npm: INodePackageManager,
constructor(private $packageManager: INodePackageManager,
private $projectData: IProjectData,
private $errors: IErrors,
private $options: IOptions,
Expand Down Expand Up @@ -55,7 +55,7 @@ class TestInitCommand implements ICommand {
if (mod.version) {
moduleToInstall += `@${mod.version}`;
}
await this.$npm.install(moduleToInstall, projectDir, {
await this.$packageManager.install(moduleToInstall, projectDir, {
'save-dev': true,
'save-exact': true,
optional: false,
Expand All @@ -76,7 +76,7 @@ class TestInitCommand implements ICommand {
// catch errors when a peerDependency is already installed
// e.g karma is installed; karma-jasmine depends on karma and will try to install it again
try {
await this.$npm.install(`${peerDependency}@${dependencyVersion}`, projectDir, {
await this.$packageManager.install(`${peerDependency}@${dependencyVersion}`, projectDir, {
'save-dev': true,
'save-exact': true,
disableNpmInstall: false,
Expand Down
20 changes: 20 additions & 0 deletions lib/common/commands/package-manager-set.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@

export class PackageManagerCommand implements ICommand {

constructor(private $userSettingsService: IUserSettingsService,
private $errors: IErrors,
private $stringParameter: ICommandParameter) { }

public allowedParameters: ICommandParameter[] = [this.$stringParameter];

public execute(args: string[]): Promise<void> {
if (args[0] === 'yarn' ) {
return this.$userSettingsService.saveSetting("packageManager", "yarn");
} else if ( args[0] === 'npm') {
return this.$userSettingsService.saveSetting("packageManager", "npm");
}
return this.$errors.fail(`${args[0]} is not a valid package manager. Only yarn or npm are supported.`);
}
}

$injector.registerCommand("package-manager|set", PackageManagerCommand);
1 change: 1 addition & 0 deletions lib/declarations.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -506,6 +506,7 @@ interface IOptions extends IRelease, IDeviceIdentifier, IJustLaunch, IAvd, IAvai
framework: string;
frameworkName: string;
frameworkVersion: string;
yarn: string,
ipa: string;
tsc: boolean;
ts: boolean;
Expand Down
112 changes: 13 additions & 99 deletions lib/node-package-manager.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,21 @@
import * as path from "path";
import { BasePackageManager } from "./base-package-manager";
import { exported, cache } from "./common/decorators";
import { isInteractive } from "./common/helpers";
import { CACACHE_DIRECTORY_NAME } from "./constants";

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

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

@exported("npm")
public async install(packageName: string, pathToSave: string, config: INodePackageManagerInstallOptions): Promise<INpmInstallResultInfo> {
Expand Down Expand Up @@ -53,7 +56,7 @@ export class NodePackageManager implements INodePackageManager {
}

try {
const spawnResult: ISpawnResult = await this.getNpmInstallResult(params, cwd);
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.
Expand All @@ -66,7 +69,7 @@ export class NodePackageManager implements INodePackageManager {
// 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.getNpmExecutableName(), params, "close");
const spawnNpmDryRunResult = await this.$childProcess.spawnFromEvent(this.getPackageManagerExecutableName(), params, "close");
return this.parseNpmInstallResult(spawnNpmDryRunResult.stdout, spawnResult.stdout, packageName);
} catch (err) {
if (err.message && err.message.indexOf("EPEERINVALID") !== -1) {
Expand Down Expand Up @@ -136,43 +139,12 @@ export class NodePackageManager implements INodePackageManager {
return path.join(cachePath.trim(), CACACHE_DIRECTORY_NAME);
}

private getNpmExecutableName(): string {
let npmExecutableName = "npm";

if (this.$hostInfo.isWindows) {
npmExecutableName += ".cmd";
}

return npmExecutableName;
}

private getFlagsString(config: any, asArray: boolean): any {
const array: Array<string> = [];
for (const flag in config) {
if (flag === "global") {
array.push(`--${flag}`);
array.push(`${config[flag]}`);
} else if (config[flag]) {
if (flag === "dist-tags" || flag === "versions") {
array.push(` ${flag}`);
continue;
}
array.push(`--${flag}`);
}
}
if (asArray) {
return array;
}

return array.join(" ");
}

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;
const npm5Output = <INpm5InstallCliResult>originalOutput;
const npmOutput = <INpmInstallCLIResult>originalOutput;
let name: string;
_.forOwn(npmOutput.dependencies, (peerDependency: INpmPeerDependencyInfo, key: string) => {
if (!peerDependency.required && !peerDependency.peerMissing) {
Expand Down Expand Up @@ -239,64 +211,6 @@ export class NodePackageManager implements INodePackageManager {
version
};
}

private async getNpmInstallResult(params: string[], cwd: string): Promise<ISpawnResult> {
return new Promise<ISpawnResult>((resolve, reject) => {
const npmExecutable = this.getNpmExecutableName();
const stdioValue = isInteractive() ? "inherit" : "pipe";

const childProcess = this.$childProcess.spawn(npmExecutable, params, { cwd, stdio: stdioValue });

let isFulfilled = false;
let capturedOut = "";
let capturedErr = "";

if (childProcess.stdout) {
childProcess.stdout.on("data", (data: string) => {
this.$logger.write(data.toString());
capturedOut += data;
});
}

if (childProcess.stderr) {
childProcess.stderr.on("data", (data: string) => {
capturedErr += data;
});
}

childProcess.on("close", (arg: any) => {
const exitCode = typeof arg === "number" ? arg : arg && arg.code;

if (exitCode === 0) {
isFulfilled = true;
const result = {
stdout: capturedOut,
stderr: capturedErr,
exitCode
};

resolve(result);
} else {
let errorMessage = `Command ${npmExecutable} ${params && params.join(" ")} failed with exit code ${exitCode}`;
if (capturedErr) {
errorMessage += ` Error output: \n ${capturedErr}`;
}

if (!isFulfilled) {
isFulfilled = true;
reject(new Error(errorMessage));
}
}
});

childProcess.on("error", (err: Error) => {
if (!isFulfilled) {
isFulfilled = true;
reject(err);
}
});
});
}
}

$injector.register("npm", NodePackageManager);
1 change: 1 addition & 0 deletions lib/options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ export class Options {
tsc: { type: OptionType.Boolean },
ts: { type: OptionType.Boolean },
typescript: { type: OptionType.Boolean },
yarn: { type: OptionType.Boolean },
androidTypings: { type: OptionType.Boolean },
bundle: { type: OptionType.String },
all: { type: OptionType.Boolean },
Expand Down
71 changes: 71 additions & 0 deletions lib/package-manager.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@

import { cache, exported, invokeInit } from './common/decorators';
export class PackageManager implements INodePackageManager {
private packageManager: INodePackageManager;

constructor(
private $errors: IErrors,
private $npm: INodePackageManager,
private $options: IOptions,
private $yarn: INodePackageManager,
private $userSettingsService: IUserSettingsService
) {}

@cache()
protected async init(): Promise<void> {
this.packageManager = await this._determinePackageManager();
}

@exported("packageManager")
@invokeInit()
public install(packageName: string, pathToSave: string, config: INodePackageManagerInstallOptions): Promise<INpmInstallResultInfo> {
return this.packageManager.install(packageName, pathToSave, config);
}
@exported("packageManager")
@invokeInit()
public uninstall(packageName: string, config?: IDictionary<string | boolean>, path?: string): Promise<string> {
return this.packageManager.uninstall(packageName, config, path);
}
@exported("packageManager")
@invokeInit()
public view(packageName: string, config: Object): Promise<any> {
return this.packageManager.view(packageName, config);
}
@exported("packageManager")
@invokeInit()
public search(filter: string[], config: IDictionary<string | boolean>): Promise<string> {
return this.packageManager.search(filter, config);
}

@invokeInit()
public searchNpms(keyword: string): Promise<INpmsResult> {
return this.packageManager.searchNpms(keyword);
}

@invokeInit()
public getRegistryPackageData(packageName: string): Promise<any> {
return this.packageManager.getRegistryPackageData(packageName);
}

@invokeInit()
public getCachePath(): Promise<string> {
return this.packageManager.getCachePath();
}

private async _determinePackageManager(): Promise<INodePackageManager> {
let pm = null;
try {
pm = await this.$userSettingsService.getSettingValue('packageManager');
} catch (err) {
this.$errors.fail(`Unable to read package manager config from user settings ${err}`);
}

if (pm === 'yarn' || this.$options.yarn) {
return this.$yarn;
} else {
return this.$npm;
}
}
}

$injector.register('packageManager', PackageManager);
Loading