Skip to content

Commit 0f66b4e

Browse files
committed
feat() - [Yarn Support - Part 1] Yarn and Package Manager implementation
1 parent b7de231 commit 0f66b4e

6 files changed

+290
-86
lines changed

lib/base-package-manager.ts

+94
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
import * as child_process from 'child_process';
2+
3+
export class BasePackageManager {
4+
constructor(private packageManager: string) { }
5+
6+
protected getNpmExecutableName(isWindows: boolean): string {
7+
let npmExecutableName = this.packageManager;
8+
9+
if (isWindows) {
10+
npmExecutableName += ".cmd";
11+
}
12+
13+
return npmExecutableName;
14+
}
15+
16+
protected async processPackageManagerInstall(
17+
childProcess: child_process.ChildProcess,
18+
isWindows: boolean,
19+
params: string[],
20+
): Promise<ISpawnResult> {
21+
return new Promise<ISpawnResult>((resolve, reject) => {
22+
let isFulfilled = false;
23+
let capturedOut = "";
24+
let capturedErr = "";
25+
26+
const npmExecutable = this.getNpmExecutableName(isWindows);
27+
28+
if (childProcess.stdout) {
29+
childProcess.stdout.on("data", (data: string) => {
30+
capturedOut += data;
31+
});
32+
}
33+
34+
if (childProcess.stderr) {
35+
childProcess.stderr.on("data", (data: string) => {
36+
capturedErr += data;
37+
});
38+
}
39+
40+
childProcess.on("close", (arg: any) => {
41+
const exitCode = typeof arg === "number" ? arg : arg && arg.code;
42+
43+
if (exitCode === 0) {
44+
isFulfilled = true;
45+
const result = {
46+
stdout: capturedOut,
47+
stderr: capturedErr,
48+
exitCode
49+
};
50+
51+
resolve(result);
52+
} else {
53+
let errorMessage = `Command ${npmExecutable} ${params && params.join(" ")} failed with exit code ${exitCode}`;
54+
if (capturedErr) {
55+
errorMessage += ` Error output: \n ${capturedErr}`;
56+
}
57+
58+
if (!isFulfilled) {
59+
isFulfilled = true;
60+
reject(new Error(errorMessage));
61+
}
62+
}
63+
});
64+
65+
childProcess.on("error", (err: Error) => {
66+
if (!isFulfilled) {
67+
isFulfilled = true;
68+
reject(err);
69+
}
70+
});
71+
});
72+
}
73+
74+
protected getFlagsString(config: any, asArray: boolean): any {
75+
const array: Array<string> = [];
76+
for (const flag in config) {
77+
if (flag === "global" && this.packageManager !== 'yarn') {
78+
array.push(`--${flag}`);
79+
array.push(`${config[flag]}`);
80+
} else if (config[flag]) {
81+
if (flag === "dist-tags" || flag === "versions") {
82+
array.push(` ${flag}`);
83+
continue;
84+
}
85+
array.push(`--${flag}`);
86+
}
87+
}
88+
if (asArray) {
89+
return array;
90+
}
91+
92+
return array.join(" ");
93+
}
94+
}

lib/declarations.d.ts

+1
Original file line numberDiff line numberDiff line change
@@ -506,6 +506,7 @@ interface IOptions extends IRelease, IDeviceIdentifier, IJustLaunch, IAvd, IAvai
506506
framework: string;
507507
frameworkName: string;
508508
frameworkVersion: string;
509+
yarn: string,
509510
ipa: string;
510511
tsc: boolean;
511512
ts: boolean;

lib/node-package-manager.ts

+15-86
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
import * as path from "path";
2+
import { BasePackageManager } from "./base-package-manager";
23
import { exported, cache } from "./common/decorators";
34
import { isInteractive } from "./common/helpers";
45
import { CACACHE_DIRECTORY_NAME } from "./constants";
56

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

@@ -12,7 +13,9 @@ export class NodePackageManager implements INodePackageManager {
1213
private $errors: IErrors,
1314
private $childProcess: IChildProcess,
1415
private $logger: ILogger,
15-
private $httpClient: Server.IHttpClient) { }
16+
private $httpClient: Server.IHttpClient) {
17+
super('npm');
18+
}
1619

1720
@exported("npm")
1821
public async install(packageName: string, pathToSave: string, config: INodePackageManagerInstallOptions): Promise<INpmInstallResultInfo> {
@@ -66,7 +69,7 @@ export class NodePackageManager implements INodePackageManager {
6669
// We cannot use the actual install with --json to get the information because of post-install scripts which may print on stdout
6770
// 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
6871
// We need the --prefix here because without it no output is emitted on stdout because all the dependencies are already installed.
69-
const spawnNpmDryRunResult = await this.$childProcess.spawnFromEvent(this.getNpmExecutableName(), params, "close");
72+
const spawnNpmDryRunResult = await this.$childProcess.spawnFromEvent(this.getNpmExecutableName(this.$hostInfo.isWindows), params, "close");
7073
return this.parseNpmInstallResult(spawnNpmDryRunResult.stdout, spawnResult.stdout, packageName);
7174
} catch (err) {
7275
if (err.message && err.message.indexOf("EPEERINVALID") !== -1) {
@@ -136,43 +139,12 @@ export class NodePackageManager implements INodePackageManager {
136139
return path.join(cachePath.trim(), CACACHE_DIRECTORY_NAME);
137140
}
138141

139-
private getNpmExecutableName(): string {
140-
let npmExecutableName = "npm";
141-
142-
if (this.$hostInfo.isWindows) {
143-
npmExecutableName += ".cmd";
144-
}
145-
146-
return npmExecutableName;
147-
}
148-
149-
private getFlagsString(config: any, asArray: boolean): any {
150-
const array: Array<string> = [];
151-
for (const flag in config) {
152-
if (flag === "global") {
153-
array.push(`--${flag}`);
154-
array.push(`${config[flag]}`);
155-
} else if (config[flag]) {
156-
if (flag === "dist-tags" || flag === "versions") {
157-
array.push(` ${flag}`);
158-
continue;
159-
}
160-
array.push(`--${flag}`);
161-
}
162-
}
163-
if (asArray) {
164-
return array;
165-
}
166-
167-
return array.join(" ");
168-
}
169-
170142
private parseNpmInstallResult(npmDryRunInstallOutput: string, npmInstallOutput: string, userSpecifiedPackageName: string): INpmInstallResultInfo {
171143
// TODO: Add tests for this functionality
172144
try {
173145
const originalOutput: INpmInstallCLIResult | INpm5InstallCliResult = JSON.parse(npmDryRunInstallOutput);
174-
const npm5Output = <INpm5InstallCliResult> originalOutput;
175-
const npmOutput = <INpmInstallCLIResult> originalOutput;
146+
const npm5Output = <INpm5InstallCliResult>originalOutput;
147+
const npmOutput = <INpmInstallCLIResult>originalOutput;
176148
let name: string;
177149
_.forOwn(npmOutput.dependencies, (peerDependency: INpmPeerDependencyInfo, key: string) => {
178150
if (!peerDependency.required && !peerDependency.peerMissing) {
@@ -241,60 +213,17 @@ export class NodePackageManager implements INodePackageManager {
241213
}
242214

243215
private async getNpmInstallResult(params: string[], cwd: string): Promise<ISpawnResult> {
244-
return new Promise<ISpawnResult>((resolve, reject) => {
245-
const npmExecutable = this.getNpmExecutableName();
216+
return new Promise<ISpawnResult>(async (resolve, reject) => {
217+
const npmExecutable = this.getNpmExecutableName(this.$hostInfo.isWindows);
246218
const stdioValue = isInteractive() ? "inherit" : "pipe";
247-
248219
const childProcess = this.$childProcess.spawn(npmExecutable, params, { cwd, stdio: stdioValue });
249220

250-
let isFulfilled = false;
251-
let capturedOut = "";
252-
let capturedErr = "";
253-
254-
if (childProcess.stdout) {
255-
childProcess.stdout.on("data", (data: string) => {
256-
this.$logger.write(data.toString());
257-
capturedOut += data;
258-
});
259-
}
260-
261-
if (childProcess.stderr) {
262-
childProcess.stderr.on("data", (data: string) => {
263-
capturedErr += data;
264-
});
221+
try {
222+
const result = await this.processPackageManagerInstall(childProcess, this.$hostInfo.isWindows, params);
223+
resolve(result);
224+
} catch (e) {
225+
reject(e);
265226
}
266-
267-
childProcess.on("close", (arg: any) => {
268-
const exitCode = typeof arg === "number" ? arg : arg && arg.code;
269-
270-
if (exitCode === 0) {
271-
isFulfilled = true;
272-
const result = {
273-
stdout: capturedOut,
274-
stderr: capturedErr,
275-
exitCode
276-
};
277-
278-
resolve(result);
279-
} else {
280-
let errorMessage = `Command ${npmExecutable} ${params && params.join(" ")} failed with exit code ${exitCode}`;
281-
if (capturedErr) {
282-
errorMessage += ` Error output: \n ${capturedErr}`;
283-
}
284-
285-
if (!isFulfilled) {
286-
isFulfilled = true;
287-
reject(new Error(errorMessage));
288-
}
289-
}
290-
});
291-
292-
childProcess.on("error", (err: Error) => {
293-
if (!isFulfilled) {
294-
isFulfilled = true;
295-
reject(err);
296-
}
297-
});
298227
});
299228
}
300229
}

lib/options.ts

+1
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ export class Options {
7171
tsc: { type: OptionType.Boolean },
7272
ts: { type: OptionType.Boolean },
7373
typescript: { type: OptionType.Boolean },
74+
yarn: { type: OptionType.Boolean },
7475
androidTypings: { type: OptionType.Boolean },
7576
bundle: { type: OptionType.String },
7677
all: { type: OptionType.Boolean },

lib/package-manager.ts

+54
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
2+
import { exported } from './common/decorators';
3+
export class PackageManager implements INodePackageManager {
4+
private packageManager: INodePackageManager;
5+
6+
constructor(
7+
private $errors: IErrors,
8+
private $npm: INodePackageManager,
9+
private $options: IOptions,
10+
private $yarn: INodePackageManager,
11+
private $userSettingsService: IUserSettingsService
12+
) {
13+
this._determinePackageManager();
14+
}
15+
@exported("packageManager")
16+
public install(packageName: string, pathToSave: string, config: INodePackageManagerInstallOptions): Promise<INpmInstallResultInfo> {
17+
return this.packageManager.install(packageName, pathToSave, config);
18+
}
19+
@exported("packageManager")
20+
public uninstall(packageName: string, config?: IDictionary<string | boolean>, path?: string): Promise<string> {
21+
return this.packageManager.uninstall(packageName, config, path);
22+
}
23+
@exported("packageManager")
24+
public view(packageName: string, config: Object): Promise<any> {
25+
return this.packageManager.view(packageName, config);
26+
}
27+
@exported("packageManager")
28+
public search(filter: string[], config: IDictionary<string | boolean>): Promise<string> {
29+
return this.packageManager.search(filter, config);
30+
}
31+
public searchNpms(keyword: string): Promise<INpmsResult> {
32+
return this.packageManager.searchNpms(keyword);
33+
}
34+
public getRegistryPackageData(packageName: string): Promise<any> {
35+
return this.packageManager.getRegistryPackageData(packageName);
36+
}
37+
public getCachePath(): Promise<string> {
38+
return this.packageManager.getCachePath();
39+
}
40+
41+
private _determinePackageManager(): void {
42+
this.$userSettingsService.getSettingValue('packageManager').then ( (pm: string) => {
43+
if (pm === 'yarn' || this.$options.yarn) {
44+
this.packageManager = this.$yarn;
45+
} else {
46+
this.packageManager = this.$npm;
47+
}
48+
}, (err) => {
49+
this.$errors.fail(`Unable to read package manager config from user settings ${err}`);
50+
});
51+
}
52+
}
53+
54+
$injector.register('packageManager', PackageManager);

0 commit comments

Comments
 (0)