/
package-json.ts
499 lines (433 loc) · 14 KB
/
package-json.ts
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
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
import { execSync } from 'child_process';
import { Module } from 'module';
import type { Tree } from 'nx/src/generators/tree';
import type { GeneratorCallback } from 'nx/src/config/misc-interfaces';
import { clean, coerce, gt } from 'semver';
import { installPackagesTask } from '../tasks/install-packages-task';
import { requireNx } from '../../nx';
import { dirSync } from 'tmp';
import { join } from 'path';
const { readJson, updateJson, getPackageManagerCommand, workspaceRoot } =
requireNx();
const UNIDENTIFIED_VERSION = 'UNIDENTIFIED_VERSION';
const NON_SEMVER_TAGS = {
'*': 2,
[UNIDENTIFIED_VERSION]: 2,
next: 1,
latest: 0,
previous: -1,
legacy: -2,
};
function filterExistingDependencies(
dependencies: Record<string, string>,
existingAltDependencies: Record<string, string>
) {
if (!existingAltDependencies) {
return dependencies;
}
return Object.keys(dependencies ?? {})
.filter((d) => !existingAltDependencies[d])
.reduce((acc, d) => ({ ...acc, [d]: dependencies[d] }), {});
}
function cleanSemver(version: string) {
return clean(version) ?? coerce(version);
}
function isIncomingVersionGreater(
incomingVersion: string,
existingVersion: string
) {
// if version is in the format of "latest", "next" or similar - keep it, otherwise try to parse it
const incomingVersionCompareBy =
incomingVersion in NON_SEMVER_TAGS
? incomingVersion
: cleanSemver(incomingVersion)?.toString() ?? UNIDENTIFIED_VERSION;
const existingVersionCompareBy =
existingVersion in NON_SEMVER_TAGS
? existingVersion
: cleanSemver(existingVersion)?.toString() ?? UNIDENTIFIED_VERSION;
if (
incomingVersionCompareBy in NON_SEMVER_TAGS &&
existingVersionCompareBy in NON_SEMVER_TAGS
) {
return (
NON_SEMVER_TAGS[incomingVersionCompareBy] >
NON_SEMVER_TAGS[existingVersionCompareBy]
);
}
if (
incomingVersionCompareBy in NON_SEMVER_TAGS ||
existingVersionCompareBy in NON_SEMVER_TAGS
) {
return true;
}
return gt(cleanSemver(incomingVersion), cleanSemver(existingVersion));
}
function updateExistingAltDependenciesVersion(
dependencies: Record<string, string>,
existingAltDependencies: Record<string, string>
) {
return Object.keys(existingAltDependencies || {})
.filter((d) => {
if (!dependencies[d]) {
return false;
}
const incomingVersion = dependencies[d];
const existingVersion = existingAltDependencies[d];
return isIncomingVersionGreater(incomingVersion, existingVersion);
})
.reduce((acc, d) => ({ ...acc, [d]: dependencies[d] }), {});
}
function updateExistingDependenciesVersion(
dependencies: Record<string, string>,
existingDependencies: Record<string, string> = {}
) {
return Object.keys(dependencies)
.filter((d) => {
if (!existingDependencies[d]) {
return true;
}
const incomingVersion = dependencies[d];
const existingVersion = existingDependencies[d];
return isIncomingVersionGreater(incomingVersion, existingVersion);
})
.reduce((acc, d) => ({ ...acc, [d]: dependencies[d] }), {});
}
/**
* Add Dependencies and Dev Dependencies to package.json
*
* For example:
* ```typescript
* addDependenciesToPackageJson(tree, { react: 'latest' }, { jest: 'latest' })
* ```
* This will **add** `react` and `jest` to the dependencies and devDependencies sections of package.json respectively.
*
* @param tree Tree representing file system to modify
* @param dependencies Dependencies to be added to the dependencies section of package.json
* @param devDependencies Dependencies to be added to the devDependencies section of package.json
* @param packageJsonPath Path to package.json
* @returns Callback to install dependencies only if necessary, no-op otherwise
*/
export function addDependenciesToPackageJson(
tree: Tree,
dependencies: Record<string, string>,
devDependencies: Record<string, string>,
packageJsonPath: string = 'package.json'
): GeneratorCallback {
const currentPackageJson = readJson(tree, packageJsonPath);
/** Dependencies to install that are not met in dev dependencies */
let filteredDependencies = filterExistingDependencies(
dependencies,
currentPackageJson.devDependencies
);
/** Dev dependencies to install that are not met in dependencies */
let filteredDevDependencies = filterExistingDependencies(
devDependencies,
currentPackageJson.dependencies
);
// filtered dependencies should consist of:
// - dependencies of the same type that are not present
// - dependencies of the same type that have greater version
// - specified dependencies of the other type that have greater version and are already installed as current type
filteredDependencies = {
...updateExistingDependenciesVersion(
filteredDependencies,
currentPackageJson.dependencies
),
...updateExistingAltDependenciesVersion(
devDependencies,
currentPackageJson.dependencies
),
};
filteredDevDependencies = {
...updateExistingDependenciesVersion(
filteredDevDependencies,
currentPackageJson.devDependencies
),
...updateExistingAltDependenciesVersion(
dependencies,
currentPackageJson.devDependencies
),
};
filteredDependencies = removeLowerVersions(
filteredDependencies,
currentPackageJson.dependencies
);
filteredDevDependencies = removeLowerVersions(
filteredDevDependencies,
currentPackageJson.devDependencies
);
if (
requiresAddingOfPackages(
currentPackageJson,
filteredDependencies,
filteredDevDependencies
)
) {
updateJson(tree, packageJsonPath, (json) => {
json.dependencies = {
...(json.dependencies || {}),
...filteredDependencies,
};
json.devDependencies = {
...(json.devDependencies || {}),
...filteredDevDependencies,
};
json.dependencies = sortObjectByKeys(json.dependencies);
json.devDependencies = sortObjectByKeys(json.devDependencies);
return json;
});
return (): void => {
installPackagesTask(tree);
};
}
return () => {};
}
/**
* @returns The the incoming dependencies that are higher than the existing verions
**/
function removeLowerVersions(
incomingDeps: Record<string, string>,
existingDeps: Record<string, string>
) {
return Object.keys(incomingDeps).reduce((acc, d) => {
if (
existingDeps?.[d] &&
!isIncomingVersionGreater(incomingDeps[d], existingDeps[d])
) {
return acc;
}
return { ...acc, [d]: incomingDeps[d] };
}, {});
}
/**
* Remove Dependencies and Dev Dependencies from package.json
*
* For example:
* ```typescript
* removeDependenciesFromPackageJson(tree, ['react'], ['jest'])
* ```
* This will **remove** `react` and `jest` from the dependencies and devDependencies sections of package.json respectively.
*
* @param dependencies Dependencies to be removed from the dependencies section of package.json
* @param devDependencies Dependencies to be removed from the devDependencies section of package.json
* @returns Callback to uninstall dependencies only if necessary. undefined is returned if changes are not necessary.
*/
export function removeDependenciesFromPackageJson(
tree: Tree,
dependencies: string[],
devDependencies: string[],
packageJsonPath: string = 'package.json'
): GeneratorCallback {
const currentPackageJson = readJson(tree, packageJsonPath);
if (
requiresRemovingOfPackages(
currentPackageJson,
dependencies,
devDependencies
)
) {
updateJson(tree, packageJsonPath, (json) => {
for (const dep of dependencies) {
delete json.dependencies[dep];
}
for (const devDep of devDependencies) {
delete json.devDependencies[devDep];
}
json.dependencies = sortObjectByKeys(json.dependencies);
json.devDependencies = sortObjectByKeys(json.devDependencies);
return json;
});
}
return (): void => {
installPackagesTask(tree);
};
}
function sortObjectByKeys<T>(obj: T): T {
if (!obj || typeof obj !== 'object' || Array.isArray(obj)) {
return obj;
}
return Object.keys(obj)
.sort()
.reduce((result, key) => {
return {
...result,
[key]: obj[key],
};
}, {}) as T;
}
/**
* Verifies whether the given packageJson dependencies require an update
* given the deps & devDeps passed in
*/
function requiresAddingOfPackages(packageJsonFile, deps, devDeps): boolean {
let needsDepsUpdate = false;
let needsDevDepsUpdate = false;
packageJsonFile.dependencies = packageJsonFile.dependencies || {};
packageJsonFile.devDependencies = packageJsonFile.devDependencies || {};
if (Object.keys(deps).length > 0) {
needsDepsUpdate = Object.keys(deps).some((entry) => {
const incomingVersion = deps[entry];
if (packageJsonFile.dependencies[entry]) {
const existingVersion = packageJsonFile.dependencies[entry];
return isIncomingVersionGreater(incomingVersion, existingVersion);
}
if (packageJsonFile.devDependencies[entry]) {
const existingVersion = packageJsonFile.devDependencies[entry];
return isIncomingVersionGreater(incomingVersion, existingVersion);
}
return true;
});
}
if (Object.keys(devDeps).length > 0) {
needsDevDepsUpdate = Object.keys(devDeps).some((entry) => {
const incomingVersion = devDeps[entry];
if (packageJsonFile.devDependencies[entry]) {
const existingVersion = packageJsonFile.devDependencies[entry];
return isIncomingVersionGreater(incomingVersion, existingVersion);
}
if (packageJsonFile.dependencies[entry]) {
const existingVersion = packageJsonFile.dependencies[entry];
return isIncomingVersionGreater(incomingVersion, existingVersion);
}
return true;
});
}
return needsDepsUpdate || needsDevDepsUpdate;
}
/**
* Verifies whether the given packageJson dependencies require an update
* given the deps & devDeps passed in
*/
function requiresRemovingOfPackages(
packageJsonFile,
deps: string[],
devDeps: string[]
): boolean {
let needsDepsUpdate = false;
let needsDevDepsUpdate = false;
packageJsonFile.dependencies = packageJsonFile.dependencies || {};
packageJsonFile.devDependencies = packageJsonFile.devDependencies || {};
if (deps.length > 0) {
needsDepsUpdate = deps.some((entry) => packageJsonFile.dependencies[entry]);
}
if (devDeps.length > 0) {
needsDevDepsUpdate = devDeps.some(
(entry) => packageJsonFile.devDependencies[entry]
);
}
return needsDepsUpdate || needsDevDepsUpdate;
}
const packageMapCache = new Map<string, any>();
/**
* @typedef EnsurePackageOptions
* @type {object}
* @property {boolean} dev indicate if the package is a dev dependency
* @property {throwOnMissing} boolean throws an error when the package is missing
*/
/**
* @deprecated Use the other function signature without a Tree
*
* Use a package that has not been installed as a dependency.
*
* For example:
* ```typescript
* ensurePackage(tree, '@nrwl/jest', nxVersion)
* ```
* This install the @nrwl/jest@<nxVersion> and return the module
* When running with --dryRun, the function will throw when dependencies are missing.
*
* @param tree the file system tree
* @param pkg the package to check (e.g. @nrwl/jest)
* @param requiredVersion the version or semver range to check (e.g. ~1.0.0, >=1.0.0 <2.0.0)
* @param {EnsurePackageOptions} options?
*/
export function ensurePackage(
tree: Tree,
pkg: string,
requiredVersion: string,
options?: { dev?: boolean; throwOnMissing?: boolean }
): void;
/**
* Ensure that dependencies and devDependencies from package.json are installed at the required versions.
*
* For example:
* ```typescript
* ensurePackage(tree, '@nrwl/jest', nxVersion)
* ```
* @param pkg the package to install and require
* @param version the version to install if the package doesn't exist already
*/
export function ensurePackage<T extends any = any>(
pkg: string,
version: string
): T;
export function ensurePackage<T extends any = any>(
pkgOrTree: string | Tree,
requiredVersionOrPackage: string,
maybeRequiredVersion?: string,
_?: never
): T {
let pkg: string;
let requiredVersion: string;
if (typeof pkgOrTree === 'string') {
pkg = pkgOrTree;
requiredVersion = requiredVersionOrPackage;
} else {
// Old Signature
pkg = requiredVersionOrPackage;
requiredVersion = maybeRequiredVersion;
}
if (packageMapCache.has(pkg)) {
return packageMapCache.get(pkg) as T;
}
try {
return require(pkg);
} catch (e) {
if (e.code !== 'MODULE_NOT_FOUND') {
throw e;
}
}
if (process.env.NX_DRY_RUN && process.env.NX_DRY_RUN !== 'false') {
throw new Error(
'NOTE: This generator does not support --dry-run. If you are running this in Nx Console, it should execute fine once you hit the "Run" button.\n'
);
}
const tempDir = dirSync().name;
console.log(`Fetching ${pkg}...`);
execSync(`${getPackageManagerCommand().addDev} ${pkg}@${requiredVersion}`, {
cwd: tempDir,
stdio: 'ignore',
});
addToNodePath(join(workspaceRoot, 'node_modules'));
addToNodePath(join(tempDir, 'node_modules'));
// Re-initialize the added paths into require
(Module as any)._initPaths();
const result = require(require.resolve(pkg, {
paths: [tempDir],
}));
packageMapCache.set(pkg, result);
return result;
}
function addToNodePath(dir: string) {
// NODE_PATH is a delimited list of paths.
// The delimiter is different for windows.
const delimiter = require('os').platform() === 'win32' ? ';' : ':';
const paths = process.env.NODE_PATH
? process.env.NODE_PATH.split(delimiter)
: [];
// The path is already in the node path
if (paths.includes(dir)) {
return;
}
// Add the tmp path
paths.push(dir);
// Update the env variable.
process.env.NODE_PATH = paths.join(delimiter);
}
function getPackageVersion(pkg: string): string {
return require(join(pkg, 'package.json')).version;
}
/**
* @description The version of Nx used by the workspace. Returns null if no version is found.
*/
export const NX_VERSION = getPackageVersion('nx');