/
npm-client.js
384 lines (352 loc) · 12.4 KB
/
npm-client.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
// @flow
import execa from 'execa';
import pMapSeries from 'p-map-series';
import semver from 'semver';
import R, { isNil, merge, toPairs, map, join, is } from 'ramda';
import chalk from 'chalk';
import fs from 'fs-extra';
import path from 'path';
import logger from '../logger/logger';
import { DEFAULT_PACKAGE_MANAGER, BASE_DOCS_DOMAIN } from '../constants';
import type { PathOsBased } from '../utils/path';
import GeneralError from '../error/general-error';
import { Analytics } from '../analytics/analytics';
type PackageManagerResults = { stdout: string, stderr: string };
const objectToArray = obj => map(join('@'), toPairs(obj));
const rejectNils = R.reject(isNil);
const defaultNpmArgs = [];
const defaultYarnArgs = [];
const defaultPackageManagerArgs = {
npm: defaultNpmArgs,
yarn: defaultYarnArgs
};
const defaultPackageManagerProcessOptions = {
cwd: process.cwd
};
const warningPrefix = (packageManager: string): string => {
return packageManager === 'npm' ? 'npm WARN' : 'warning';
};
const errorPrefix = (packageManager: string): string => {
return packageManager === 'npm' ? 'npm ERR!' : 'error';
};
const peerDependenciesMissing = (packageManager: string): string => {
return packageManager === 'npm' ? 'requires a peer' : 'unmet peer';
};
const stripNonNpmErrors = (errors: string, packageManager: string) => {
// a workaround to remove all 'npm warn' and 'npm notice'.
// NPM itself returns them even when --loglevel = error or when --silent/--quiet flags are set
const prefix = errorPrefix(packageManager);
return errors
.split('\n')
.filter(error => error.startsWith(prefix))
.join('\n');
};
const stripNonPeerDependenciesWarnings = (errors: string, packageManager: string) => {
const prefix = warningPrefix(packageManager);
const peer = peerDependenciesMissing(packageManager);
return errors
.split('\n')
.filter(error => error.startsWith(prefix) && error.includes(peer))
.join('\n');
};
/**
* Pick only allowed to be overridden options
* @param {Object} userOptions
*/
const getAllowdPackageManagerProcessOptions = (userOptions) => {
const allowdOptions = ['shell', 'env', 'extendEnv', 'uid', 'gid', 'preferLocal', 'localDir', 'timeout'];
return R.pick(allowdOptions, userOptions);
};
type installArgs = {
modules?: string[] | { [string]: number | string },
packageManager: 'npm' | 'yarn',
packageManagerArgs: string[],
packageManagerProcessOptions: Object,
useWorkspaces: boolean,
dirs: string[],
rootDir: ?string, // Used for yarn workspace
installRootPackageJson: boolean,
installPeerDependencies: boolean,
verbose: boolean
};
/**
* Install packages in specific directory
*/
const _installInOneDirectory = ({
modules = [],
packageManager = DEFAULT_PACKAGE_MANAGER,
packageManagerArgs = [],
packageManagerProcessOptions = {},
dir,
verbose = false
}): Promise<PackageManagerResults> => {
// Handle process options
const allowedPackageManagerProcessOptions = getAllowdPackageManagerProcessOptions(packageManagerProcessOptions);
const concretePackageManagerProcessOptions = merge(
defaultPackageManagerProcessOptions,
allowedPackageManagerProcessOptions
);
concretePackageManagerProcessOptions.cwd = dir || concretePackageManagerProcessOptions.cwd;
const cwd = concretePackageManagerProcessOptions.cwd;
// taking care of object case
const processedModules = is(Object, modules) && !Array.isArray(modules) ? objectToArray(modules) : modules;
// Handle process args
const concretePackageManagerDefaultArgs = [
'install',
...processedModules,
...defaultPackageManagerArgs[packageManager]
];
const concretePackageManagerArgs = rejectNils(R.concat(concretePackageManagerDefaultArgs, packageManagerArgs));
// Add npm verbose flag
if (verbose && packageManager === 'npm') {
// we may want to use it later. For now, it print too much information
// concretePackageManagerArgs.push('--verbose');
}
fs.ensureDirSync(path.join(cwd, 'node_modules'));
logger.debug(
`installing npm packages using ${packageManager} at ${cwd} with options:`,
concretePackageManagerProcessOptions,
`and args: ${concretePackageManagerArgs}`
);
// Set the shell to true to prevent problems with post install scripts when running as root
const packageManagerClientName = packageManager;
const childProcess = execa(
packageManagerClientName,
concretePackageManagerArgs,
concretePackageManagerProcessOptions
);
// Remove the install from args since it's always there
const printArgs = concretePackageManagerArgs.filter(arg => arg !== 'install');
const argsString = printArgs && printArgs.length > 0 ? `with args: ${printArgs}` : '';
return childProcess
.then(({ stdout, stderr }) => {
const successMessage = `\nsuccessfully ran ${packageManager} install at ${cwd} ${argsString}`;
const peerWarnings = stripNonPeerDependenciesWarnings(stderr, packageManager);
stdout = verbose ? stdout + successMessage : chalk.white(peerWarnings) + successMessage;
stderr = verbose ? stderr : '';
return { stdout, stderr };
})
.catch((err) => {
let stderr = `failed running ${packageManager} install at ${cwd} ${argsString} \n`;
stderr += verbose ? err.stderr : stripNonNpmErrors(err.stderr, packageManager);
return Promise.reject(
new GeneralError(
`${stderr}\n\n${chalk.yellow(
`see troubleshooting at https://${BASE_DOCS_DOMAIN}/docs/install-components.html`
)}`
)
);
});
};
/**
* Get peer dependencies for directory
* you should run this after you run npm install
* internally it uses npm list -j
*/
const _getPeerDeps = async (dir: PathOsBased): Promise<string[]> => {
const packageManager = DEFAULT_PACKAGE_MANAGER;
let npmList;
try {
npmList = await execa(packageManager, ['list', '-j'], { cwd: dir });
} catch (err) {
if (err.stdout && err.stdout.startsWith('{')) {
// it's probably a valid json with errors, that's fine, parse it.
npmList = err;
} else {
logger.error(err);
throw new Error(`failed running ${err.cmd} to find the peer dependencies due to an error: ${err.message}`);
}
}
const peerDepsObject = await getPeerDepsFromNpmList(npmList.stdout, packageManager);
return objectToArray(peerDepsObject);
};
async function getPeerDepsFromNpmList(npmList: string, packageManager: string): Promise<Object> {
const parsePeers = (deps: Object): Object => {
const result = {};
R.forEachObjIndexed((dep) => {
if (dep.peerMissing) {
const name = dep.required.name;
const version = dep.required.version;
result[name] = version;
}
}, deps);
return result;
};
const npmListObject = await parseNpmListJsonGracefully(npmList, packageManager);
return parsePeers(npmListObject.dependencies);
}
async function parseNpmListJsonGracefully(str: string, packageManager: string): Object {
try {
const json = JSON.parse(str);
return json;
} catch (err) {
logger.error(err);
if (packageManager === 'npm') {
const version = await getNpmVersion();
Analytics.setExtraData('npmVersion', version);
if (version && semver.gte(version, '5.0.0') && semver.lt(version, '5.1.0')) {
// see here for more info about this issue with npm 5.0.0
// https://github.com/npm/npm/issues/17331
throw new Error(
`error: your npm version "${version}" has issues returning json, please upgrade to 5.1.0 or above (npm install -g npm@5.1.0)`
);
}
}
throw new Error(`failed parsing the output of npm list due to an error: ${err.message}`);
}
}
/**
* A wrapper function to call the install
* then get the peers
* then install the peers
*/
const _installInOneDirectoryWithPeerOption = async ({
modules = [],
packageManager = DEFAULT_PACKAGE_MANAGER,
packageManagerArgs = [],
packageManagerProcessOptions = {},
dir,
installPeerDependencies = false,
verbose = false
}): Promise<PackageManagerResults | PackageManagerResults[]> => {
const rootDirResults = await _installInOneDirectory({
modules,
packageManager,
packageManagerArgs,
packageManagerProcessOptions,
dir,
installPeerDependencies,
verbose
});
if (installPeerDependencies) {
const peers = await _getPeerDeps(dir);
const peerResults = await _installInOneDirectory({
modules: peers,
packageManager,
packageManagerArgs,
packageManagerProcessOptions,
dir,
verbose
});
return [rootDirResults, peerResults];
}
return rootDirResults;
};
/**
* when modules is empty, it runs 'npm install' without any package, which installs according to package.json file
*/
const installAction = async ({
modules,
packageManager = DEFAULT_PACKAGE_MANAGER,
packageManagerArgs = [],
packageManagerProcessOptions = {},
useWorkspaces = false,
dirs = [],
rootDir,
installRootPackageJson = false,
installPeerDependencies = false,
verbose = false
}: installArgs): Promise<PackageManagerResults | PackageManagerResults[]> => {
if (useWorkspaces && packageManager === 'yarn') {
await _installInOneDirectoryWithPeerOption({
modules,
packageManager,
packageManagerArgs,
packageManagerProcessOptions,
dir: rootDir,
installPeerDependencies,
verbose
});
}
const results = [];
if (installRootPackageJson) {
// installation of the root package.json has to be completed before installing the sub-directories package.json.
const rootDirResults = await _installInOneDirectoryWithPeerOption({
modules,
packageManager,
packageManagerArgs,
packageManagerProcessOptions,
dir: rootDir,
installPeerDependencies,
verbose
});
if (Array.isArray(rootDirResults)) {
results.concat(rootDirResults);
} else {
results.push(rootDirResults);
}
}
const installInDir = dir =>
_installInOneDirectoryWithPeerOption({
modules,
packageManager,
packageManagerArgs,
packageManagerProcessOptions,
dir,
installPeerDependencies,
verbose
});
// run npm install for each one of the directories serially, not in parallel. Don’t use Promise.all() here.
// running them in parallel result in race condition and random NPM errors. (see https://github.com/teambit/bit/issues/1617)
const promisesResults = await pMapSeries(dirs, installInDir);
return results.concat(R.flatten(promisesResults));
};
const printResults = ({ stdout, stderr }: { stdout: string, stderr: string }) => {
console.log(chalk.yellow(stdout)); // eslint-disable-line
console.log(chalk.yellow(stderr)); // eslint-disable-line
};
async function getNpmVersion(): Promise<?string> {
try {
const { stdout, stderr } = await execa('npm', ['--version']);
if (stdout && !stderr) return stdout;
} catch (err) {
logger.debugAndAddBreadCrumb('npm-client', `got an error when executing "npm --version". ${err.message}`);
}
return null;
}
async function getYarnVersion(): Promise<?string> {
try {
const { stdout } = await execa('yarn', ['-v']);
return stdout;
} catch (e) {
logger.debugAndAddBreadCrumb('npm-client', `can't find yarn version by running yarn -v. ${e.message}`);
}
return null;
}
/**
* a situation where rootDir and subDir have package.json, some of the packages may be shared
* and some may be conflicted. And the "npm/yarn install" is done from the root dir.
* package managers install the shared packages only once in the rootDir.
* however, as to the conflicted packages, only npm@5 and above install it in the subDir.
* others, install it in the root, which, result in an incorrect package resolution for the subDir.
*/
async function isSupportedInstallationOfSubDirFromRoot(packageManager: string): Promise<boolean> {
if (packageManager === 'npm') {
const version = await getNpmVersion();
if (version && semver.gte(version, '5.0.0')) {
return true;
}
}
return false;
}
async function getPackageLatestVersion(packageName: string): Promise<?string> {
try {
const { stdout } = await execa('npm', ['show', packageName, 'version']);
return stdout;
} catch (e) {
logger.debugAndAddBreadCrumb(
'npm-client',
`can't find ${packageName} version by running npm show ${packageName} version. ${e.message}`
);
}
return null;
}
export default {
install: installAction,
printResults,
isSupportedInstallationOfSubDirFromRoot,
getNpmVersion,
getYarnVersion,
getPeerDepsFromNpmList,
getPackageLatestVersion
};