@openrewrite/recipes-nodejs 0.37.0-20260106-083133 → 0.37.0-20260106-170728
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/security/dependency-vulnerability-check.d.ts +8 -54
- package/dist/security/dependency-vulnerability-check.d.ts.map +1 -1
- package/dist/security/dependency-vulnerability-check.js +176 -287
- package/dist/security/dependency-vulnerability-check.js.map +1 -1
- package/dist/security/index.d.ts +3 -0
- package/dist/security/index.d.ts.map +1 -1
- package/dist/security/index.js +3 -0
- package/dist/security/index.js.map +1 -1
- package/dist/security/npm-utils.d.ts +8 -2
- package/dist/security/npm-utils.d.ts.map +1 -1
- package/dist/security/npm-utils.js +114 -14
- package/dist/security/npm-utils.js.map +1 -1
- package/dist/security/override-utils.d.ts +23 -0
- package/dist/security/override-utils.d.ts.map +1 -0
- package/dist/security/override-utils.js +169 -0
- package/dist/security/override-utils.js.map +1 -0
- package/dist/security/remove-redundant-overrides.d.ts +1 -10
- package/dist/security/remove-redundant-overrides.d.ts.map +1 -1
- package/dist/security/remove-redundant-overrides.js +4 -152
- package/dist/security/remove-redundant-overrides.js.map +1 -1
- package/dist/security/types.d.ts +42 -0
- package/dist/security/types.d.ts.map +1 -0
- package/dist/security/types.js +7 -0
- package/dist/security/types.js.map +1 -0
- package/dist/security/version-utils.d.ts +13 -0
- package/dist/security/version-utils.d.ts.map +1 -0
- package/dist/security/version-utils.js +173 -0
- package/dist/security/version-utils.js.map +1 -0
- package/package.json +1 -1
- package/src/security/dependency-vulnerability-check.ts +300 -525
- package/src/security/index.ts +3 -0
- package/src/security/npm-utils.ts +172 -37
- package/src/security/override-utils.ts +253 -0
- package/src/security/remove-redundant-overrides.ts +9 -211
- package/src/security/types.ts +115 -0
- package/src/security/version-utils.ts +198 -0
package/src/security/index.ts
CHANGED
|
@@ -107,7 +107,7 @@ export async function fetchNpmPackageInfo(
|
|
|
107
107
|
|
|
108
108
|
if (Array.isArray(versions)) {
|
|
109
109
|
for (const v of versions) {
|
|
110
|
-
versionMap[v] = {
|
|
110
|
+
versionMap[v] = {version: v};
|
|
111
111
|
}
|
|
112
112
|
}
|
|
113
113
|
|
|
@@ -251,42 +251,41 @@ export interface DirectUpgradeCheckResult {
|
|
|
251
251
|
export type DependencyScope = 'dependencies' | 'devDependencies' | 'peerDependencies' | 'optionalDependencies';
|
|
252
252
|
|
|
253
253
|
/**
|
|
254
|
-
* Checks if
|
|
255
|
-
*
|
|
254
|
+
* Checks if a direct dependency at a given version, when installed alone (in isolation),
|
|
255
|
+
* brings in a safe version of a transitive dependency. This is useful when multiple
|
|
256
|
+
* direct dependencies bring in the same vulnerable transitive - we need to check each
|
|
257
|
+
* in isolation to find upgrade candidates.
|
|
256
258
|
*
|
|
257
259
|
* @param pm The package manager to use
|
|
258
|
-
* @param directDepName The direct dependency
|
|
259
|
-
* @param directDepVersion The version to
|
|
260
|
-
* @param transitiveDepName The transitive dependency
|
|
261
|
-
* @param isVulnerable Function that checks if a version is vulnerable
|
|
262
|
-
* @param
|
|
263
|
-
* @param scope The dependency scope of the direct dependency
|
|
260
|
+
* @param directDepName The direct dependency name
|
|
261
|
+
* @param directDepVersion The version to test
|
|
262
|
+
* @param transitiveDepName The transitive dependency to check
|
|
263
|
+
* @param isVulnerable Function that checks if a transitive version is vulnerable
|
|
264
|
+
* @param scope The dependency scope
|
|
264
265
|
* @param configFiles Optional config files (e.g., .npmrc)
|
|
265
|
-
* @returns Result indicating if the upgrade
|
|
266
|
+
* @returns Result indicating if the upgrade is safe and what transitive version it brings
|
|
266
267
|
*/
|
|
267
|
-
export async function
|
|
268
|
+
export async function checkDirectDepBringsSafeTransitiveInIsolation(
|
|
268
269
|
pm: PackageManager,
|
|
269
270
|
directDepName: string,
|
|
270
271
|
directDepVersion: string,
|
|
271
272
|
transitiveDepName: string,
|
|
272
273
|
isVulnerable: (version: string) => boolean,
|
|
273
|
-
originalPackageJson: string,
|
|
274
274
|
scope: DependencyScope,
|
|
275
275
|
configFiles?: Record<string, string>
|
|
276
276
|
): Promise<DirectUpgradeCheckResult> {
|
|
277
277
|
try {
|
|
278
|
-
//
|
|
279
|
-
const
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
const modifiedPackageJson = JSON.stringify(pkgJson, null, 2);
|
|
278
|
+
// Create a minimal package.json with ONLY this direct dependency
|
|
279
|
+
const minimalPackageJson = JSON.stringify({
|
|
280
|
+
name: 'upgrade-check-isolated',
|
|
281
|
+
version: '1.0.0',
|
|
282
|
+
[scope]: {
|
|
283
|
+
[directDepName]: directDepVersion
|
|
284
|
+
}
|
|
285
|
+
}, null, 2);
|
|
287
286
|
|
|
288
287
|
// Run install in temp directory
|
|
289
|
-
const result = await runInstallInTempDir(pm,
|
|
288
|
+
const result = await runInstallInTempDir(pm, minimalPackageJson, {
|
|
290
289
|
configFiles,
|
|
291
290
|
lockOnly: true
|
|
292
291
|
});
|
|
@@ -321,8 +320,9 @@ export async function checkDirectUpgradeFixesTransitive(
|
|
|
321
320
|
}
|
|
322
321
|
|
|
323
322
|
/**
|
|
324
|
-
* Finds a direct dependency upgrade that
|
|
325
|
-
*
|
|
323
|
+
* Finds a direct dependency upgrade that brings in a safe transitive, checking in isolation.
|
|
324
|
+
* This differs from findDirectUpgradeThatFixesTransitive by testing the direct dep alone,
|
|
325
|
+
* not with other dependencies that might also bring in the vulnerable transitive.
|
|
326
326
|
*
|
|
327
327
|
* @param pm The package manager to use
|
|
328
328
|
* @param directDepName The direct dependency to upgrade
|
|
@@ -330,19 +330,17 @@ export async function checkDirectUpgradeFixesTransitive(
|
|
|
330
330
|
* @param transitiveDepName The transitive dependency that has a vulnerability
|
|
331
331
|
* @param isVulnerable Function that checks if a version is vulnerable
|
|
332
332
|
* @param isWithinDelta Function that checks if a version is within the allowed upgrade delta
|
|
333
|
-
* @param
|
|
334
|
-
* @param scope The dependency scope of the direct dependency
|
|
333
|
+
* @param scope The dependency scope
|
|
335
334
|
* @param configFiles Optional config files (e.g., .npmrc)
|
|
336
|
-
* @returns The direct dependency version that
|
|
335
|
+
* @returns The direct dependency version that brings in a safe transitive, or undefined
|
|
337
336
|
*/
|
|
338
|
-
export async function
|
|
337
|
+
export async function findDirectUpgradeWithSafeTransitiveInIsolation(
|
|
339
338
|
pm: PackageManager,
|
|
340
339
|
directDepName: string,
|
|
341
340
|
currentDirectVersion: string,
|
|
342
341
|
transitiveDepName: string,
|
|
343
342
|
isVulnerable: (version: string) => boolean,
|
|
344
343
|
isWithinDelta: (fromVersion: string, toVersion: string) => boolean,
|
|
345
|
-
originalPackageJson: string,
|
|
346
344
|
scope: DependencyScope,
|
|
347
345
|
configFiles?: Record<string, string>
|
|
348
346
|
): Promise<string | undefined> {
|
|
@@ -355,7 +353,6 @@ export async function findDirectUpgradeThatFixesTransitive(
|
|
|
355
353
|
const currentMajor = semver.major(currentDirectVersion);
|
|
356
354
|
|
|
357
355
|
// Filter to versions newer than current and within delta
|
|
358
|
-
// Note: getAvailableVersions already filters out prereleases
|
|
359
356
|
const candidateVersions = availableVersions.filter(v => {
|
|
360
357
|
try {
|
|
361
358
|
return semver.gt(v, currentDirectVersion) && isWithinDelta(currentDirectVersion, v);
|
|
@@ -369,30 +366,26 @@ export async function findDirectUpgradeThatFixesTransitive(
|
|
|
369
366
|
}
|
|
370
367
|
|
|
371
368
|
// Sort candidates: same major version first (descending), then other majors (descending)
|
|
372
|
-
// This prioritizes peer-compatible versions while still allowing major upgrades if needed
|
|
373
369
|
candidateVersions.sort((a, b) => {
|
|
374
370
|
const aMajor = semver.major(a);
|
|
375
371
|
const bMajor = semver.major(b);
|
|
376
372
|
const aIsSameMajor = aMajor === currentMajor;
|
|
377
373
|
const bIsSameMajor = bMajor === currentMajor;
|
|
378
374
|
|
|
379
|
-
// Same major versions come first
|
|
380
375
|
if (aIsSameMajor && !bIsSameMajor) return -1;
|
|
381
376
|
if (!aIsSameMajor && bIsSameMajor) return 1;
|
|
382
377
|
|
|
383
|
-
// Within the same priority group, sort by version descending
|
|
384
378
|
return semver.rcompare(a, b);
|
|
385
379
|
});
|
|
386
380
|
|
|
387
381
|
// Try candidates in order (same major first, then others)
|
|
388
382
|
for (const candidate of candidateVersions) {
|
|
389
|
-
const result = await
|
|
383
|
+
const result = await checkDirectDepBringsSafeTransitiveInIsolation(
|
|
390
384
|
pm,
|
|
391
385
|
directDepName,
|
|
392
386
|
candidate,
|
|
393
387
|
transitiveDepName,
|
|
394
388
|
isVulnerable,
|
|
395
|
-
originalPackageJson,
|
|
396
389
|
scope,
|
|
397
390
|
configFiles
|
|
398
391
|
);
|
|
@@ -402,13 +395,155 @@ export async function findDirectUpgradeThatFixesTransitive(
|
|
|
402
395
|
}
|
|
403
396
|
|
|
404
397
|
// Only try a few candidates to avoid excessive npm installs
|
|
405
|
-
// After trying the newest same-major and newest other-major, give up
|
|
406
398
|
const candidateMajor = semver.major(candidate);
|
|
407
399
|
if (candidateMajor !== currentMajor) {
|
|
408
|
-
// We've tried a different major version and it didn't work, give up
|
|
409
400
|
break;
|
|
410
401
|
}
|
|
411
402
|
}
|
|
412
403
|
|
|
413
404
|
return undefined;
|
|
414
405
|
}
|
|
406
|
+
|
|
407
|
+
/**
|
|
408
|
+
* Verifies that applying all proposed direct dependency upgrades fixes a transitive vulnerability.
|
|
409
|
+
* This is used after finding individual upgrade candidates to confirm they work together.
|
|
410
|
+
*
|
|
411
|
+
* @param pm The package manager to use
|
|
412
|
+
* @param upgrades Array of direct dependency upgrades to apply
|
|
413
|
+
* @param transitiveDepName The transitive dependency that should be fixed
|
|
414
|
+
* @param isVulnerable Function that checks if a transitive version is vulnerable
|
|
415
|
+
* @param originalPackageJson The original package.json content
|
|
416
|
+
* @param configFiles Optional config files (e.g., .npmrc)
|
|
417
|
+
* @returns True if all upgrades together fix the transitive vulnerability
|
|
418
|
+
*/
|
|
419
|
+
export async function verifyAllUpgradesFixTransitive(
|
|
420
|
+
pm: PackageManager,
|
|
421
|
+
upgrades: Array<{ name: string; version: string; scope: DependencyScope }>,
|
|
422
|
+
transitiveDepName: string,
|
|
423
|
+
isVulnerable: (version: string) => boolean,
|
|
424
|
+
originalPackageJson: string,
|
|
425
|
+
configFiles?: Record<string, string>
|
|
426
|
+
): Promise<boolean> {
|
|
427
|
+
try {
|
|
428
|
+
// Parse and modify package.json with all upgraded dependencies
|
|
429
|
+
const pkgJson = JSON.parse(originalPackageJson);
|
|
430
|
+
|
|
431
|
+
for (const upgrade of upgrades) {
|
|
432
|
+
if (pkgJson[upgrade.scope]?.[upgrade.name]) {
|
|
433
|
+
pkgJson[upgrade.scope][upgrade.name] = upgrade.version;
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
const modifiedPackageJson = JSON.stringify(pkgJson, null, 2);
|
|
438
|
+
|
|
439
|
+
// Run install in temp directory
|
|
440
|
+
const result = await runInstallInTempDir(pm, modifiedPackageJson, {
|
|
441
|
+
configFiles,
|
|
442
|
+
lockOnly: true
|
|
443
|
+
});
|
|
444
|
+
|
|
445
|
+
if (!result.success || !result.lockFileContent) {
|
|
446
|
+
return false;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
// Check all copies of the transitive in the lock file
|
|
450
|
+
const allVersions = extractAllVersionsFromLockFile(
|
|
451
|
+
result.lockFileContent,
|
|
452
|
+
transitiveDepName,
|
|
453
|
+
pm
|
|
454
|
+
);
|
|
455
|
+
|
|
456
|
+
if (allVersions.length === 0) {
|
|
457
|
+
// Transitive not found - considered fixed
|
|
458
|
+
return true;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
// ALL copies must be safe
|
|
462
|
+
return allVersions.every(v => !isVulnerable(v));
|
|
463
|
+
} catch {
|
|
464
|
+
return false;
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
/**
|
|
469
|
+
* Extracts ALL versions of a package from a lock file (handles multiple copies).
|
|
470
|
+
* This is important when the same package appears multiple times at different versions
|
|
471
|
+
* due to incompatible peer dependency ranges.
|
|
472
|
+
*/
|
|
473
|
+
export function extractAllVersionsFromLockFile(
|
|
474
|
+
lockFileContent: string,
|
|
475
|
+
packageName: string,
|
|
476
|
+
pm: PackageManager
|
|
477
|
+
): string[] {
|
|
478
|
+
const versions: string[] = [];
|
|
479
|
+
try {
|
|
480
|
+
switch (pm) {
|
|
481
|
+
case PackageManager.Npm: {
|
|
482
|
+
const lockJson = JSON.parse(lockFileContent);
|
|
483
|
+
if (lockJson.packages) {
|
|
484
|
+
for (const [key, value] of Object.entries(lockJson.packages)) {
|
|
485
|
+
// Match both root and nested copies: node_modules/pkg or .../node_modules/pkg
|
|
486
|
+
if (key.endsWith(`node_modules/${packageName}`)) {
|
|
487
|
+
const version = (value as any).version;
|
|
488
|
+
if (version && !versions.includes(version)) {
|
|
489
|
+
versions.push(version);
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
// Also check v1 format
|
|
495
|
+
if (lockJson.dependencies?.[packageName]) {
|
|
496
|
+
const version = lockJson.dependencies[packageName].version;
|
|
497
|
+
if (version && !versions.includes(version)) {
|
|
498
|
+
versions.push(version);
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
break;
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
case PackageManager.Pnpm: {
|
|
505
|
+
const regex = new RegExp(`['"]?${escapeRegExp(packageName)}@([\\d.]+)['"]?:`, 'g');
|
|
506
|
+
let match;
|
|
507
|
+
while ((match = regex.exec(lockFileContent)) !== null) {
|
|
508
|
+
if (!versions.includes(match[1])) {
|
|
509
|
+
versions.push(match[1]);
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
break;
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
case PackageManager.YarnClassic:
|
|
516
|
+
case PackageManager.YarnBerry: {
|
|
517
|
+
const regex = new RegExp(`"?${escapeRegExp(packageName)}@[^":\\n]+[":]*\\s*\\n\\s*version:?\\s*"?([^"\\n]+)"?`, 'g');
|
|
518
|
+
let match;
|
|
519
|
+
while ((match = regex.exec(lockFileContent)) !== null) {
|
|
520
|
+
const version = match[1]?.trim();
|
|
521
|
+
if (version && !versions.includes(version)) {
|
|
522
|
+
versions.push(version);
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
break;
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
case PackageManager.Bun: {
|
|
529
|
+
const lockJson = JSON.parse(lockFileContent);
|
|
530
|
+
if (lockJson.packages) {
|
|
531
|
+
for (const [key, value] of Object.entries(lockJson.packages)) {
|
|
532
|
+
if (key === packageName || key.endsWith(`/${packageName}`)) {
|
|
533
|
+
if (Array.isArray(value) && value[0]) {
|
|
534
|
+
const version = String(value[0]);
|
|
535
|
+
if (!versions.includes(version)) {
|
|
536
|
+
versions.push(version);
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
break;
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
} catch {
|
|
546
|
+
// Return empty array on parse errors
|
|
547
|
+
}
|
|
548
|
+
return versions;
|
|
549
|
+
}
|
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright 2025 the original author or authors.
|
|
3
|
+
*
|
|
4
|
+
* Moderne Proprietary. Only for use by Moderne customers under the terms of a commercial contract.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import {DependencyScope, PackageManager} from "@openrewrite/rewrite/javascript";
|
|
8
|
+
import {ALL_DEPENDENCY_SCOPES} from "./types";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Finds the dependency scope where a package is declared as a direct dependency.
|
|
12
|
+
* Returns the scope name (dependencies, devDependencies, etc.) or undefined if not found.
|
|
13
|
+
*/
|
|
14
|
+
export function findDirectDependencyScope(
|
|
15
|
+
packageJson: Record<string, any>,
|
|
16
|
+
packageName: string
|
|
17
|
+
): DependencyScope | undefined {
|
|
18
|
+
for (const scope of ALL_DEPENDENCY_SCOPES) {
|
|
19
|
+
if (packageJson[scope]?.[packageName]) {
|
|
20
|
+
return scope;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
return undefined;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Extracts the overrides section from a package.json based on the package manager.
|
|
28
|
+
* Returns the overrides object or undefined if not present.
|
|
29
|
+
*/
|
|
30
|
+
export function getOverridesFromPackageJson(
|
|
31
|
+
packageJson: Record<string, any>,
|
|
32
|
+
packageManager: PackageManager
|
|
33
|
+
): Record<string, string> | undefined {
|
|
34
|
+
switch (packageManager) {
|
|
35
|
+
case PackageManager.Npm:
|
|
36
|
+
case PackageManager.Bun:
|
|
37
|
+
return packageJson.overrides;
|
|
38
|
+
case PackageManager.Pnpm:
|
|
39
|
+
return packageJson.pnpm?.overrides;
|
|
40
|
+
case PackageManager.YarnClassic:
|
|
41
|
+
case PackageManager.YarnBerry:
|
|
42
|
+
return packageJson.resolutions;
|
|
43
|
+
default:
|
|
44
|
+
return undefined;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Gets the field names for overrides and comments based on package manager.
|
|
50
|
+
*/
|
|
51
|
+
export function getOverrideFieldNames(packageManager: PackageManager): {
|
|
52
|
+
overrideField: string;
|
|
53
|
+
commentField: string;
|
|
54
|
+
} {
|
|
55
|
+
switch (packageManager) {
|
|
56
|
+
case PackageManager.Npm:
|
|
57
|
+
case PackageManager.Bun:
|
|
58
|
+
return {overrideField: 'overrides', commentField: '//overrides'};
|
|
59
|
+
case PackageManager.Pnpm:
|
|
60
|
+
return {overrideField: 'pnpm', commentField: '//pnpm.overrides'};
|
|
61
|
+
case PackageManager.YarnClassic:
|
|
62
|
+
case PackageManager.YarnBerry:
|
|
63
|
+
return {overrideField: 'resolutions', commentField: '//resolutions'};
|
|
64
|
+
default:
|
|
65
|
+
return {overrideField: 'overrides', commentField: '//overrides'};
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Information about an override entry.
|
|
71
|
+
*/
|
|
72
|
+
export interface OverrideInfo {
|
|
73
|
+
/** The full key (e.g., "package" or "package@^1") */
|
|
74
|
+
key: string;
|
|
75
|
+
/** The base package name (without version specifier) */
|
|
76
|
+
packageName: string;
|
|
77
|
+
/** The version the override pins to */
|
|
78
|
+
version: string;
|
|
79
|
+
/** Whether this is a version-specific override (e.g., "package@^1") */
|
|
80
|
+
isVersionSpecific: boolean;
|
|
81
|
+
/** The version range specifier if version-specific (e.g., "^1") */
|
|
82
|
+
versionRange?: string;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Parses an override key to extract package name and version range.
|
|
87
|
+
* Handles both regular packages (lodash) and scoped packages (@scope/package).
|
|
88
|
+
*/
|
|
89
|
+
export function parseOverrideKey(key: string): {
|
|
90
|
+
packageName: string;
|
|
91
|
+
versionRange?: string;
|
|
92
|
+
isVersionSpecific: boolean;
|
|
93
|
+
} {
|
|
94
|
+
const atIndex = key.lastIndexOf('@');
|
|
95
|
+
let packageName: string;
|
|
96
|
+
let versionRange: string | undefined;
|
|
97
|
+
let isVersionSpecific = false;
|
|
98
|
+
|
|
99
|
+
// Check if this is a version-specific override like "package@^1"
|
|
100
|
+
// But be careful with scoped packages like "@scope/package"
|
|
101
|
+
if (atIndex > 0 && !key.startsWith('@')) {
|
|
102
|
+
// Unscoped package with version specifier
|
|
103
|
+
packageName = key.substring(0, atIndex);
|
|
104
|
+
versionRange = key.substring(atIndex + 1);
|
|
105
|
+
isVersionSpecific = true;
|
|
106
|
+
} else if (atIndex > 0 && key.startsWith('@')) {
|
|
107
|
+
// Scoped package - check if there's another @ after the scope
|
|
108
|
+
const secondAtIndex = key.indexOf('@', 1);
|
|
109
|
+
if (secondAtIndex > 0 && secondAtIndex !== atIndex) {
|
|
110
|
+
// Has version specifier: @scope/package@^1
|
|
111
|
+
packageName = key.substring(0, secondAtIndex);
|
|
112
|
+
versionRange = key.substring(secondAtIndex + 1);
|
|
113
|
+
isVersionSpecific = true;
|
|
114
|
+
} else {
|
|
115
|
+
// Just @scope/package
|
|
116
|
+
packageName = key;
|
|
117
|
+
}
|
|
118
|
+
} else {
|
|
119
|
+
packageName = key;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return {packageName, versionRange, isVersionSpecific};
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Extracts all override entries from a package.json.
|
|
127
|
+
*/
|
|
128
|
+
export function extractOverrides(
|
|
129
|
+
packageJson: Record<string, any>,
|
|
130
|
+
packageManager: PackageManager
|
|
131
|
+
): OverrideInfo[] {
|
|
132
|
+
const overrides: OverrideInfo[] = [];
|
|
133
|
+
const overrideObj = getOverridesFromPackageJson(packageJson, packageManager);
|
|
134
|
+
|
|
135
|
+
if (!overrideObj) {
|
|
136
|
+
return overrides;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
for (const [key, value] of Object.entries(overrideObj)) {
|
|
140
|
+
// Skip nested overrides (npm supports objects as values)
|
|
141
|
+
if (typeof value !== 'string') {
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const {packageName, versionRange, isVersionSpecific} = parseOverrideKey(key);
|
|
146
|
+
|
|
147
|
+
overrides.push({
|
|
148
|
+
key,
|
|
149
|
+
packageName,
|
|
150
|
+
version: value,
|
|
151
|
+
isVersionSpecific,
|
|
152
|
+
versionRange
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return overrides;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Removes a single override from the package.json object.
|
|
161
|
+
* Mutates the object in place.
|
|
162
|
+
*/
|
|
163
|
+
export function removeOverrideFromObject(
|
|
164
|
+
packageJson: Record<string, any>,
|
|
165
|
+
packageManager: PackageManager,
|
|
166
|
+
key: string
|
|
167
|
+
): void {
|
|
168
|
+
switch (packageManager) {
|
|
169
|
+
case PackageManager.Npm:
|
|
170
|
+
case PackageManager.Bun:
|
|
171
|
+
if (packageJson.overrides) {
|
|
172
|
+
delete packageJson.overrides[key];
|
|
173
|
+
if (Object.keys(packageJson.overrides).length === 0) {
|
|
174
|
+
delete packageJson.overrides;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
break;
|
|
178
|
+
case PackageManager.Pnpm:
|
|
179
|
+
if (packageJson.pnpm?.overrides) {
|
|
180
|
+
delete packageJson.pnpm.overrides[key];
|
|
181
|
+
if (Object.keys(packageJson.pnpm.overrides).length === 0) {
|
|
182
|
+
delete packageJson.pnpm.overrides;
|
|
183
|
+
}
|
|
184
|
+
if (Object.keys(packageJson.pnpm).length === 0) {
|
|
185
|
+
delete packageJson.pnpm;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
break;
|
|
189
|
+
case PackageManager.YarnClassic:
|
|
190
|
+
case PackageManager.YarnBerry:
|
|
191
|
+
if (packageJson.resolutions) {
|
|
192
|
+
delete packageJson.resolutions[key];
|
|
193
|
+
if (Object.keys(packageJson.resolutions).length === 0) {
|
|
194
|
+
delete packageJson.resolutions;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
break;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Removes multiple overrides from package.json content.
|
|
203
|
+
* Also removes associated comments.
|
|
204
|
+
* Returns the modified JSON string.
|
|
205
|
+
*/
|
|
206
|
+
export function removeOverridesFromContent(
|
|
207
|
+
originalContent: string,
|
|
208
|
+
packageManager: PackageManager,
|
|
209
|
+
keysToRemove: Set<string>
|
|
210
|
+
): string {
|
|
211
|
+
const packageJson = JSON.parse(originalContent);
|
|
212
|
+
const {overrideField, commentField} = getOverrideFieldNames(packageManager);
|
|
213
|
+
|
|
214
|
+
// Remove overrides
|
|
215
|
+
if (packageManager === PackageManager.Pnpm) {
|
|
216
|
+
if (packageJson.pnpm?.overrides) {
|
|
217
|
+
for (const key of keysToRemove) {
|
|
218
|
+
delete packageJson.pnpm.overrides[key];
|
|
219
|
+
}
|
|
220
|
+
if (Object.keys(packageJson.pnpm.overrides).length === 0) {
|
|
221
|
+
delete packageJson.pnpm.overrides;
|
|
222
|
+
}
|
|
223
|
+
if (Object.keys(packageJson.pnpm).length === 0) {
|
|
224
|
+
delete packageJson.pnpm;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
} else {
|
|
228
|
+
if (packageJson[overrideField]) {
|
|
229
|
+
for (const key of keysToRemove) {
|
|
230
|
+
delete packageJson[overrideField][key];
|
|
231
|
+
}
|
|
232
|
+
if (Object.keys(packageJson[overrideField]).length === 0) {
|
|
233
|
+
delete packageJson[overrideField];
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// Remove associated comments
|
|
239
|
+
if (packageJson[commentField]) {
|
|
240
|
+
for (const key of keysToRemove) {
|
|
241
|
+
delete packageJson[commentField][key];
|
|
242
|
+
}
|
|
243
|
+
if (Object.keys(packageJson[commentField]).length === 0) {
|
|
244
|
+
delete packageJson[commentField];
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// Preserve original indentation
|
|
249
|
+
const indentMatch = originalContent.match(/^(\s+)"/m);
|
|
250
|
+
const indent = indentMatch ? indentMatch[1].length : 2;
|
|
251
|
+
|
|
252
|
+
return JSON.stringify(packageJson, null, indent);
|
|
253
|
+
}
|