dep-up-surgeon 2.2.2 → 2.2.4
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/README.md +122 -12
- package/dist/cli/overrideFlow.d.ts +50 -0
- package/dist/cli/overrideFlow.d.ts.map +1 -1
- package/dist/cli/overrideFlow.js +112 -69
- package/dist/cli/overrideFlow.js.map +1 -1
- package/dist/cli/summary.d.ts.map +1 -1
- package/dist/cli/summary.js +17 -4
- package/dist/cli/summary.js.map +1 -1
- package/dist/cli/undo.d.ts +105 -0
- package/dist/cli/undo.d.ts.map +1 -0
- package/dist/cli/undo.js +410 -0
- package/dist/cli/undo.js.map +1 -0
- package/dist/cli/undoCommand.d.ts +2 -0
- package/dist/cli/undoCommand.d.ts.map +1 -0
- package/dist/cli/undoCommand.js +101 -0
- package/dist/cli/undoCommand.js.map +1 -0
- package/dist/cli.js +56 -14
- package/dist/cli.js.map +1 -1
- package/dist/config/loadConfig.d.ts +64 -0
- package/dist/config/loadConfig.d.ts.map +1 -1
- package/dist/config/loadConfig.js +200 -1
- package/dist/config/loadConfig.js.map +1 -1
- package/dist/core/audit.d.ts +18 -0
- package/dist/core/audit.d.ts.map +1 -1
- package/dist/core/audit.js +29 -0
- package/dist/core/audit.js.map +1 -1
- package/dist/core/peerResolver.d.ts +64 -0
- package/dist/core/peerResolver.d.ts.map +1 -1
- package/dist/core/peerResolver.js +225 -2
- package/dist/core/peerResolver.js.map +1 -1
- package/dist/core/peerResolverAdHoc.d.ts +42 -0
- package/dist/core/peerResolverAdHoc.d.ts.map +1 -0
- package/dist/core/peerResolverAdHoc.js +226 -0
- package/dist/core/peerResolverAdHoc.js.map +1 -0
- package/dist/core/upgrader.d.ts +12 -1
- package/dist/core/upgrader.d.ts.map +1 -1
- package/dist/core/upgrader.js +171 -7
- package/dist/core/upgrader.js.map +1 -1
- package/dist/types.d.ts +24 -4
- package/dist/types.d.ts.map +1 -1
- package/dist/utils/overrides.d.ts +81 -7
- package/dist/utils/overrides.d.ts.map +1 -1
- package/dist/utils/overrides.js +344 -30
- package/dist/utils/overrides.js.map +1 -1
- package/package.json +3 -3
|
@@ -11,6 +11,27 @@ export interface OverrideEntry {
|
|
|
11
11
|
* version without speculating that a `>=` range would also be safe.
|
|
12
12
|
*/
|
|
13
13
|
range: string;
|
|
14
|
+
/**
|
|
15
|
+
* Parent chain from outermost package down to (but not including) `name`. When omitted or
|
|
16
|
+
* empty, the override is FLAT — written as a top-level key. When non-empty, the override is
|
|
17
|
+
* PARENT-SCOPED: we walk this chain to the matching nested object (npm) or encode it into
|
|
18
|
+
* the key with the manager-specific separator (`>` for pnpm, `/` for yarn).
|
|
19
|
+
*
|
|
20
|
+
* Example: `{ parentChain: ['some-dep'], name: 'foo', range: '1.2.3' }` →
|
|
21
|
+
* - npm: `{ "overrides": { "some-dep": { "foo": "1.2.3" } } }`
|
|
22
|
+
* - pnpm: `{ "pnpm": { "overrides": { "some-dep>foo": "1.2.3" } } }`
|
|
23
|
+
* - yarn: `{ "resolutions": { "some-dep/foo": "1.2.3" } }`
|
|
24
|
+
*/
|
|
25
|
+
parentChain?: string[];
|
|
26
|
+
}
|
|
27
|
+
/** Structured representation of an override entry after reading it back, chain included. */
|
|
28
|
+
export interface OverrideEntryRead extends OverrideEntry {
|
|
29
|
+
/**
|
|
30
|
+
* Full chain from outermost parent down to `name` (always length ≥ 1). Flat entries have
|
|
31
|
+
* `chain = [name]`; parent-scoped entries have `chain = [...parentChain, name]`. Provided so
|
|
32
|
+
* consumers don't need to re-assemble it from `parentChain + name` themselves.
|
|
33
|
+
*/
|
|
34
|
+
chain: string[];
|
|
14
35
|
}
|
|
15
36
|
export interface ReadOverridesResult {
|
|
16
37
|
/** True when the field was present (may still be empty `{}`). */
|
|
@@ -18,11 +39,16 @@ export interface ReadOverridesResult {
|
|
|
18
39
|
/** Field layout: `overrides` (npm), `pnpm.overrides` (pnpm), or `resolutions` (yarn). */
|
|
19
40
|
field: OverrideField;
|
|
20
41
|
/**
|
|
21
|
-
*
|
|
22
|
-
*
|
|
42
|
+
* All entries in the block, flat + parent-scoped mixed together. Each has a `chain` array
|
|
43
|
+
* describing where it lives; `chain.length === 1` is the flat case. npm nested objects,
|
|
44
|
+
* pnpm `>`-chains, and yarn `/`-chains are all surfaced uniformly here.
|
|
45
|
+
*/
|
|
46
|
+
entries: OverrideEntryRead[];
|
|
47
|
+
/**
|
|
48
|
+
* Anything that didn't fit the string|object pattern we parse (e.g. booleans, arrays, or
|
|
49
|
+
* unknown extensions pnpm added). Handed back raw so callers that REWRITE the block can
|
|
50
|
+
* preserve these entries without knowing what they mean.
|
|
23
51
|
*/
|
|
24
|
-
entries: OverrideEntry[];
|
|
25
|
-
/** Any non-string values we saw — handed back raw for round-trip preservation. */
|
|
26
52
|
nested: Record<string, unknown>;
|
|
27
53
|
}
|
|
28
54
|
export interface ApplyOverrideResult {
|
|
@@ -40,8 +66,21 @@ export interface ApplyOverrideResult {
|
|
|
40
66
|
}
|
|
41
67
|
/**
|
|
42
68
|
* Read the override block from a given `package.json` object (NOT from disk — caller owns the
|
|
43
|
-
* I/O). Returns a normalized `{ present, entries, nested }` shape that works across managers
|
|
44
|
-
*
|
|
69
|
+
* I/O). Returns a normalized `{ present, entries, nested }` shape that works across managers
|
|
70
|
+
* AND flattens all three parent-scoped encodings into a single `chain` array per entry:
|
|
71
|
+
*
|
|
72
|
+
* - npm nested: `{ "overrides": { "foo": { "bar": "1" } } }`
|
|
73
|
+
* → entry `{ chain: ['foo', 'bar'], name: 'bar', range: '1', parentChain: ['foo'] }`.
|
|
74
|
+
* The special key `"."` (npm's "the parent itself" selector) is folded into the chain
|
|
75
|
+
* too: `{"foo": {".": "2"}}` → chain `['foo']` (pins `foo` when nested under itself).
|
|
76
|
+
* - pnpm `>`-chain keys: `{ "pnpm": { "overrides": { "foo>bar": "1" } } }`
|
|
77
|
+
* → entry `{ chain: ['foo', 'bar'], ... }`.
|
|
78
|
+
* - yarn `/`-chain keys: `{ "resolutions": { "a/b": "1" } }`
|
|
79
|
+
* → entry `{ chain: ['a', 'b'], ... }`. Scoped names (`@org/foo`) complicate `/` parsing,
|
|
80
|
+
* so we treat `/` as a separator ONLY after a non-`@` segment. `"@org/foo"` stays one
|
|
81
|
+
* name; `"@org/foo/child"` splits into `["@org/foo", "child"]`.
|
|
82
|
+
*
|
|
83
|
+
* Accepts a `pkg` of `unknown` so callers can pass raw `JSON.parse` output.
|
|
45
84
|
*/
|
|
46
85
|
export declare function readOverrides(pkg: unknown, manager: PackageManager): ReadOverridesResult;
|
|
47
86
|
/**
|
|
@@ -72,8 +111,35 @@ export declare function decideOverride(existing: string | undefined, target: str
|
|
|
72
111
|
* In-memory write: return a new `package.json` object with the override applied. Does NOT
|
|
73
112
|
* touch disk — caller serializes + writes. Preserves ordering for existing keys and inserts
|
|
74
113
|
* the field at a stable position when it's new.
|
|
114
|
+
*
|
|
115
|
+
* When `entry.parentChain` is non-empty the write is **parent-scoped**:
|
|
116
|
+
* - npm writes a nested object (`{parent: {child: "X"}}`), creating intermediate levels as
|
|
117
|
+
* needed and preserving sibling keys at every level.
|
|
118
|
+
* - pnpm writes a flat `"parent>child"` key (pnpm's native chain encoding).
|
|
119
|
+
* - yarn writes a flat `"parent/child"` key. Scoped names are kept intact thanks to the
|
|
120
|
+
* chain being an array rather than a pre-encoded string.
|
|
75
121
|
*/
|
|
76
122
|
export declare function applyOverrideInMemory(pkg: Record<string, unknown>, manager: PackageManager, entry: OverrideEntry): Record<string, unknown>;
|
|
123
|
+
/**
|
|
124
|
+
* Encode a `chain: string[]` into the single-string key pnpm / yarn store in their flat
|
|
125
|
+
* overrides map. pnpm uses `>`; yarn uses `/`. Scoped names are preserved verbatim inside
|
|
126
|
+
* each segment since we only join WITH the separator — we never touch the segment contents.
|
|
127
|
+
*/
|
|
128
|
+
export declare function encodeChainKey(chain: string[], manager: 'pnpm' | 'yarn'): string;
|
|
129
|
+
/**
|
|
130
|
+
* Parse a user-facing selector string into a `chain + range` pair.
|
|
131
|
+
*
|
|
132
|
+
* Accepts BOTH the pnpm-style `parent>child@1.2.3` AND the yarn-style `parent/child@1.2.3`
|
|
133
|
+
* form (after accounting for scoped-name slashes). When neither separator is present the
|
|
134
|
+
* selector is treated as a flat override. The trailing `@<range>` is optional — callers that
|
|
135
|
+
* know the range separately (e.g. audit-derived pins) can pass just the chain.
|
|
136
|
+
*
|
|
137
|
+
* Returns `undefined` when the selector is malformed (empty, dangling `@`, trailing separator).
|
|
138
|
+
*/
|
|
139
|
+
export declare function parseOverrideSelector(spec: string): {
|
|
140
|
+
chain: string[];
|
|
141
|
+
range?: string;
|
|
142
|
+
} | undefined;
|
|
77
143
|
export interface ApplyOverrideToFileOptions {
|
|
78
144
|
packageJsonPath: string;
|
|
79
145
|
manager: PackageManager;
|
|
@@ -93,8 +159,16 @@ export declare function applyOverrideToFile(opts: ApplyOverrideToFileOptions): P
|
|
|
93
159
|
/**
|
|
94
160
|
* Inverse of `applyOverrideToFile` — delete a single override entry. Used by the rollback
|
|
95
161
|
* path when the install + validator fails AFTER we added a pin.
|
|
162
|
+
*
|
|
163
|
+
* `target` can be a bare package name (drops the flat top-level entry, legacy API) or a
|
|
164
|
+
* structured `{ chain: string[] }` selector (drops a parent-scoped entry matching the exact
|
|
165
|
+
* chain). When a parent-scoped write was adjacent to a sibling-writing flat pin, we leave the
|
|
166
|
+
* sibling intact; same-level empty objects are pruned recursively so the file shrinks back to
|
|
167
|
+
* what existed before the write.
|
|
96
168
|
*/
|
|
97
|
-
export declare function removeOverrideFromFile(packageJsonPath: string, manager: PackageManager,
|
|
169
|
+
export declare function removeOverrideFromFile(packageJsonPath: string, manager: PackageManager, target: string | {
|
|
170
|
+
chain: string[];
|
|
171
|
+
}): Promise<{
|
|
98
172
|
ok: boolean;
|
|
99
173
|
removed: boolean;
|
|
100
174
|
reason?: string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"overrides.d.ts","sourceRoot":"","sources":["../../src/utils/overrides.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"overrides.d.ts","sourceRoot":"","sources":["../../src/utils/overrides.ts"],"names":[],"mappings":"AA6CA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AAE5D,MAAM,MAAM,aAAa,GAAG,WAAW,GAAG,gBAAgB,GAAG,aAAa,CAAC;AAE3E,2DAA2D;AAC3D,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,cAAc,GAAG,aAAa,CAUvE;AAED,MAAM,WAAW,aAAa;IAC5B,gEAAgE;IAChE,IAAI,EAAE,MAAM,CAAC;IACb;;;;OAIG;IACH,KAAK,EAAE,MAAM,CAAC;IACd;;;;;;;;;;OAUG;IACH,WAAW,CAAC,EAAE,MAAM,EAAE,CAAC;CACxB;AAED,4FAA4F;AAC5F,MAAM,WAAW,iBAAkB,SAAQ,aAAa;IACtD;;;;OAIG;IACH,KAAK,EAAE,MAAM,EAAE,CAAC;CACjB;AAED,MAAM,WAAW,mBAAmB;IAClC,iEAAiE;IACjE,OAAO,EAAE,OAAO,CAAC;IACjB,yFAAyF;IACzF,KAAK,EAAE,aAAa,CAAC;IACrB;;;;OAIG;IACH,OAAO,EAAE,iBAAiB,EAAE,CAAC;IAC7B;;;;OAIG;IACH,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACjC;AAED,MAAM,WAAW,mBAAmB;IAClC,EAAE,EAAE,OAAO,CAAC;IACZ,4FAA4F;IAC5F,OAAO,EAAE,OAAO,CAAC;IACjB,0CAA0C;IAC1C,KAAK,EAAE,aAAa,CAAC;IACrB,4CAA4C;IAC5C,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,8DAA8D;IAC9D,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,sEAAsE;IACtE,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,aAAa,CAC3B,GAAG,EAAE,OAAO,EACZ,OAAO,EAAE,cAAc,GACtB,mBAAmB,CAoCrB;AA4ED;;;;;;;;;GASG;AACH,MAAM,MAAM,gBAAgB,GACxB;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,GACpD;IAAE,MAAM,EAAE,OAAO,CAAC;IAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,GACvD;IAAE,MAAM,EAAE,UAAU,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC;AAE7D,wBAAgB,cAAc,CAC5B,QAAQ,EAAE,MAAM,GAAG,SAAS,EAC5B,MAAM,EAAE,MAAM,GACb,gBAAgB,CA6BlB;AAwFD;;;;;;;;;;;GAWG;AACH,wBAAgB,qBAAqB,CACnC,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC5B,OAAO,EAAE,cAAc,EACvB,KAAK,EAAE,aAAa,GACnB,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAkEzB;AAED;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,KAAK,EAAE,MAAM,EAAE,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,GAAG,MAAM,CAGhF;AAED;;;;;;;;;GASG;AACH,wBAAgB,qBAAqB,CACnC,IAAI,EAAE,MAAM,GACX;IAAE,KAAK,EAAE,MAAM,EAAE,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,SAAS,CA6BjD;AAmBD,MAAM,WAAW,0BAA0B;IACzC,eAAe,EAAE,MAAM,CAAC;IACxB,OAAO,EAAE,cAAc,CAAC;IACxB,KAAK,EAAE,aAAa,CAAC;IACrB;;;;OAIG;IACH,kBAAkB,CAAC,EAAE,OAAO,CAAC;CAC9B;AAED;;;GAGG;AACH,wBAAsB,mBAAmB,CACvC,IAAI,EAAE,0BAA0B,GAC/B,OAAO,CAAC,mBAAmB,CAAC,CAmF9B;AAED;;;;;;;;;GASG;AACH,wBAAsB,sBAAsB,CAC1C,eAAe,EAAE,MAAM,EACvB,OAAO,EAAE,cAAc,EACvB,MAAM,EAAE,MAAM,GAAG;IAAE,KAAK,EAAE,MAAM,EAAE,CAAA;CAAE,GACnC,OAAO,CAAC;IAAE,EAAE,EAAE,OAAO,CAAC;IAAC,OAAO,EAAE,OAAO,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC,CA0F7D"}
|
package/dist/utils/overrides.js
CHANGED
|
@@ -1,23 +1,37 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Read / write the package-manager-specific "pin a transitive dep to version X" block inside
|
|
3
|
-
* `package.json`. Used by `--security-only --apply-overrides`
|
|
4
|
-
*
|
|
3
|
+
* `package.json`. Used by `--security-only --apply-overrides` (flat audit-driven pins) and by
|
|
4
|
+
* the user-facing `--override <selector>` CLI flag (parent-scoped pins).
|
|
5
5
|
*
|
|
6
|
-
* Manager-specific field layouts
|
|
6
|
+
* Manager-specific field layouts — we support BOTH the flat form and the parent-scoped /
|
|
7
|
+
* nested form each manager offers:
|
|
7
8
|
*
|
|
8
|
-
* - **npm (>=8.3.0)**:
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
9
|
+
* - **npm (>=8.3.0)**:
|
|
10
|
+
* Flat: `{ "overrides": { "<pkg>": "<range>" } }`
|
|
11
|
+
* Nested: `{ "overrides": { "<parent>": { "<pkg>": "<range>" } } }`
|
|
12
|
+
* Use `"."` to pin the parent itself.
|
|
13
|
+
* Ref: https://docs.npmjs.com/cli/v9/configuring-npm/package-json#overrides
|
|
14
|
+
*
|
|
15
|
+
* - **pnpm**:
|
|
16
|
+
* Flat: `{ "pnpm": { "overrides": { "<pkg>": "<range>" } } }`
|
|
17
|
+
* Chain: `{ "pnpm": { "overrides": { "<parent>>...>":"<range>", "<a>>b>c": "..." } } }`
|
|
18
|
+
* pnpm encodes the parent chain directly in the key using `>` as separator.
|
|
19
|
+
* Ref: https://pnpm.io/package_json#pnpmoverrides
|
|
20
|
+
*
|
|
21
|
+
* - **yarn (classic + berry)**:
|
|
22
|
+
* Flat: `{ "resolutions": { "<pkg>": "<range>" } }`
|
|
23
|
+
* Chain: `{ "resolutions": { "<parent>/<pkg>": "<range>" } }` — path-style, `/`-separated.
|
|
24
|
+
* Ref: https://classic.yarnpkg.com/lang/en/docs/selective-version-resolutions/
|
|
25
|
+
*
|
|
26
|
+
* Internal model: every override is a `{ chain: string[], range: string }` pair where `chain`
|
|
27
|
+
* is the parent chain from outermost package down to the pinned package. `chain.length === 1`
|
|
28
|
+
* is the flat case. On write, we translate the chain into the manager-specific encoding above;
|
|
29
|
+
* on read, we reverse the encoding so downstream consumers never worry about the format.
|
|
16
30
|
*
|
|
17
31
|
* Design notes:
|
|
18
32
|
* - Pure I/O helpers here — no engine integration. The higher-level "apply an override, run
|
|
19
|
-
* install, roll back on failure" loop lives in the caller (`cli.ts`
|
|
20
|
-
* - `
|
|
33
|
+
* install, roll back on failure" loop lives in the caller (`cli/overrideFlow.ts`).
|
|
34
|
+
* - `decideOverride` is conservative: it refuses to downgrade an existing override (so a user
|
|
21
35
|
* who manually pinned `foo@5.x` never gets auto-bumped to `5.1.x` because audit happens to
|
|
22
36
|
* suggest it). Upgrades and brand-new entries are always safe.
|
|
23
37
|
* - Writes preserve the original file's ordering for every untouched key, add the new block
|
|
@@ -43,8 +57,21 @@ export function overrideFieldFor(manager) {
|
|
|
43
57
|
}
|
|
44
58
|
/**
|
|
45
59
|
* Read the override block from a given `package.json` object (NOT from disk — caller owns the
|
|
46
|
-
* I/O). Returns a normalized `{ present, entries, nested }` shape that works across managers
|
|
47
|
-
*
|
|
60
|
+
* I/O). Returns a normalized `{ present, entries, nested }` shape that works across managers
|
|
61
|
+
* AND flattens all three parent-scoped encodings into a single `chain` array per entry:
|
|
62
|
+
*
|
|
63
|
+
* - npm nested: `{ "overrides": { "foo": { "bar": "1" } } }`
|
|
64
|
+
* → entry `{ chain: ['foo', 'bar'], name: 'bar', range: '1', parentChain: ['foo'] }`.
|
|
65
|
+
* The special key `"."` (npm's "the parent itself" selector) is folded into the chain
|
|
66
|
+
* too: `{"foo": {".": "2"}}` → chain `['foo']` (pins `foo` when nested under itself).
|
|
67
|
+
* - pnpm `>`-chain keys: `{ "pnpm": { "overrides": { "foo>bar": "1" } } }`
|
|
68
|
+
* → entry `{ chain: ['foo', 'bar'], ... }`.
|
|
69
|
+
* - yarn `/`-chain keys: `{ "resolutions": { "a/b": "1" } }`
|
|
70
|
+
* → entry `{ chain: ['a', 'b'], ... }`. Scoped names (`@org/foo`) complicate `/` parsing,
|
|
71
|
+
* so we treat `/` as a separator ONLY after a non-`@` segment. `"@org/foo"` stays one
|
|
72
|
+
* name; `"@org/foo/child"` splits into `["@org/foo", "child"]`.
|
|
73
|
+
*
|
|
74
|
+
* Accepts a `pkg` of `unknown` so callers can pass raw `JSON.parse` output.
|
|
48
75
|
*/
|
|
49
76
|
export function readOverrides(pkg, manager) {
|
|
50
77
|
const field = overrideFieldFor(manager);
|
|
@@ -68,14 +95,95 @@ export function readOverrides(pkg, manager) {
|
|
|
68
95
|
const nested = {};
|
|
69
96
|
for (const [k, v] of Object.entries(block)) {
|
|
70
97
|
if (typeof v === 'string') {
|
|
71
|
-
|
|
98
|
+
// Leaf: a direct `name` OR a chain key for pnpm/yarn.
|
|
99
|
+
const chain = parseChainKey(k, manager);
|
|
100
|
+
pushEntry(entries, chain, v);
|
|
101
|
+
}
|
|
102
|
+
else if (manager === 'npm' && v && typeof v === 'object' && !Array.isArray(v)) {
|
|
103
|
+
// npm nested object form. Walk it recursively so grandchildren (`foo>bar>baz` in npm
|
|
104
|
+
// parlance = `{ foo: { bar: { baz: "X" } } }`) are flattened into a single chain.
|
|
105
|
+
walkNpmNested([k], v, entries);
|
|
72
106
|
}
|
|
73
107
|
else {
|
|
108
|
+
// Anything we don't recognize — keep it so a rewrite doesn't lose data.
|
|
74
109
|
nested[k] = v;
|
|
75
110
|
}
|
|
76
111
|
}
|
|
77
112
|
return { present: true, field, entries, nested };
|
|
78
113
|
}
|
|
114
|
+
/**
|
|
115
|
+
* Split a pnpm / yarn chain key into its package-name segments. The separator is `>` for pnpm
|
|
116
|
+
* and `/` for yarn — except `/` is ALSO legal inside scoped package names (`@org/pkg`), so we
|
|
117
|
+
* treat `/` as a separator only when the preceding segment doesn't start with `@`.
|
|
118
|
+
*/
|
|
119
|
+
function parseChainKey(key, manager) {
|
|
120
|
+
const trimmed = key.trim();
|
|
121
|
+
if (!trimmed)
|
|
122
|
+
return [trimmed];
|
|
123
|
+
if (manager === 'pnpm') {
|
|
124
|
+
// pnpm supports an optional `@<version>` suffix on any segment to constrain WHICH version
|
|
125
|
+
// of the parent the override applies to. We strip it for identification purposes — the
|
|
126
|
+
// rest of the pipeline compares by NAME — but keep it verbatim when writing back.
|
|
127
|
+
return trimmed.split('>').map((s) => s.trim()).filter(Boolean);
|
|
128
|
+
}
|
|
129
|
+
if (manager === 'yarn') {
|
|
130
|
+
const parts = [];
|
|
131
|
+
let buf = '';
|
|
132
|
+
const raw = trimmed;
|
|
133
|
+
for (let i = 0; i < raw.length; i++) {
|
|
134
|
+
const c = raw[i];
|
|
135
|
+
if (c === '/' && !buf.startsWith('@')) {
|
|
136
|
+
if (buf)
|
|
137
|
+
parts.push(buf);
|
|
138
|
+
buf = '';
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
// Allow ONE `/` after a `@scope` so `@scope/pkg` is kept whole; any SECOND `/` splits.
|
|
142
|
+
if (c === '/' && buf.startsWith('@') && !buf.slice(1).includes('/')) {
|
|
143
|
+
buf += c;
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
if (c === '/' && buf.startsWith('@')) {
|
|
147
|
+
parts.push(buf);
|
|
148
|
+
buf = '';
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
buf += c;
|
|
152
|
+
}
|
|
153
|
+
if (buf)
|
|
154
|
+
parts.push(buf);
|
|
155
|
+
return parts;
|
|
156
|
+
}
|
|
157
|
+
// npm: its flat keys never contain chain markers (nesting uses object form handled above).
|
|
158
|
+
return [trimmed];
|
|
159
|
+
}
|
|
160
|
+
function pushEntry(entries, chain, range) {
|
|
161
|
+
if (chain.length === 0)
|
|
162
|
+
return;
|
|
163
|
+
const name = chain[chain.length - 1];
|
|
164
|
+
const parentChain = chain.slice(0, -1);
|
|
165
|
+
const entry = { chain: [...chain], name, range };
|
|
166
|
+
if (parentChain.length > 0)
|
|
167
|
+
entry.parentChain = parentChain;
|
|
168
|
+
entries.push(entry);
|
|
169
|
+
}
|
|
170
|
+
function walkNpmNested(chain, obj, out) {
|
|
171
|
+
for (const [k, v] of Object.entries(obj)) {
|
|
172
|
+
// npm's special `"."` selector means "pin the parent package itself" (the outermost key of
|
|
173
|
+
// THIS scope). It produces an entry whose name equals the current `chain` tail, not `.`.
|
|
174
|
+
if (k === '.' && typeof v === 'string') {
|
|
175
|
+
pushEntry(out, chain, v);
|
|
176
|
+
continue;
|
|
177
|
+
}
|
|
178
|
+
const nextChain = [...chain, k];
|
|
179
|
+
if (typeof v === 'string') {
|
|
180
|
+
pushEntry(out, nextChain, v);
|
|
181
|
+
}
|
|
182
|
+
else if (v && typeof v === 'object' && !Array.isArray(v)) {
|
|
183
|
+
walkNpmNested(nextChain, v, out);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
79
187
|
export function decideOverride(existing, target) {
|
|
80
188
|
const cleanTarget = target.trim();
|
|
81
189
|
if (!cleanTarget) {
|
|
@@ -134,14 +242,83 @@ function safeSatisfies(version, range) {
|
|
|
134
242
|
return false;
|
|
135
243
|
}
|
|
136
244
|
}
|
|
245
|
+
function chainsEqual(a, b) {
|
|
246
|
+
if (a.length !== b.length)
|
|
247
|
+
return false;
|
|
248
|
+
for (let i = 0; i < a.length; i++) {
|
|
249
|
+
if (a[i] !== b[i])
|
|
250
|
+
return false;
|
|
251
|
+
}
|
|
252
|
+
return true;
|
|
253
|
+
}
|
|
254
|
+
/**
|
|
255
|
+
* Walk `block` along `chain`, delete the leaf, and prune now-empty intermediate objects on
|
|
256
|
+
* the way back out. Returns `true` when something was actually removed. Works in-place: the
|
|
257
|
+
* caller should pass a shallow-cloned copy (we own the top level).
|
|
258
|
+
*/
|
|
259
|
+
function removeChainFromNpmNested(block, chain) {
|
|
260
|
+
if (chain.length === 0)
|
|
261
|
+
return false;
|
|
262
|
+
const [head, ...tail] = chain;
|
|
263
|
+
if (!(head in block))
|
|
264
|
+
return false;
|
|
265
|
+
const value = block[head];
|
|
266
|
+
// Leaf write: chain of length 1. The value MUST be a string for the flat match; if it's a
|
|
267
|
+
// nested object we instead look for a `"."` key inside (npm's self-pin) — that's the only
|
|
268
|
+
// way a length-1 chain can have created a nested shape.
|
|
269
|
+
if (tail.length === 0) {
|
|
270
|
+
if (typeof value === 'string') {
|
|
271
|
+
delete block[head];
|
|
272
|
+
return true;
|
|
273
|
+
}
|
|
274
|
+
if (value && typeof value === 'object' && !Array.isArray(value)) {
|
|
275
|
+
const inner = { ...value };
|
|
276
|
+
if ('.' in inner && typeof inner['.'] === 'string') {
|
|
277
|
+
delete inner['.'];
|
|
278
|
+
if (Object.keys(inner).length === 0) {
|
|
279
|
+
delete block[head];
|
|
280
|
+
}
|
|
281
|
+
else {
|
|
282
|
+
block[head] = inner;
|
|
283
|
+
}
|
|
284
|
+
return true;
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
return false;
|
|
288
|
+
}
|
|
289
|
+
// Internal node: descend. `value` must be a nested object for the chain to be meaningful.
|
|
290
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
291
|
+
return false;
|
|
292
|
+
const child = { ...value };
|
|
293
|
+
const ok = removeChainFromNpmNested(child, tail);
|
|
294
|
+
if (!ok)
|
|
295
|
+
return false;
|
|
296
|
+
if (Object.keys(child).length === 0) {
|
|
297
|
+
delete block[head];
|
|
298
|
+
}
|
|
299
|
+
else {
|
|
300
|
+
block[head] = child;
|
|
301
|
+
}
|
|
302
|
+
return true;
|
|
303
|
+
}
|
|
137
304
|
/**
|
|
138
305
|
* In-memory write: return a new `package.json` object with the override applied. Does NOT
|
|
139
306
|
* touch disk — caller serializes + writes. Preserves ordering for existing keys and inserts
|
|
140
307
|
* the field at a stable position when it's new.
|
|
308
|
+
*
|
|
309
|
+
* When `entry.parentChain` is non-empty the write is **parent-scoped**:
|
|
310
|
+
* - npm writes a nested object (`{parent: {child: "X"}}`), creating intermediate levels as
|
|
311
|
+
* needed and preserving sibling keys at every level.
|
|
312
|
+
* - pnpm writes a flat `"parent>child"` key (pnpm's native chain encoding).
|
|
313
|
+
* - yarn writes a flat `"parent/child"` key. Scoped names are kept intact thanks to the
|
|
314
|
+
* chain being an array rather than a pre-encoded string.
|
|
141
315
|
*/
|
|
142
316
|
export function applyOverrideInMemory(pkg, manager, entry) {
|
|
143
317
|
const field = overrideFieldFor(manager);
|
|
144
318
|
const next = { ...pkg };
|
|
319
|
+
const chain = [...(entry.parentChain ?? []), entry.name];
|
|
320
|
+
if (chain.length === 0)
|
|
321
|
+
return next; // degenerate — nothing to write
|
|
145
322
|
if (field === 'pnpm.overrides') {
|
|
146
323
|
const pnpmBlock = pkg.pnpm && typeof pkg.pnpm === 'object'
|
|
147
324
|
? { ...pkg.pnpm }
|
|
@@ -149,19 +326,123 @@ export function applyOverrideInMemory(pkg, manager, entry) {
|
|
|
149
326
|
const overrides = pnpmBlock.overrides && typeof pnpmBlock.overrides === 'object'
|
|
150
327
|
? { ...pnpmBlock.overrides }
|
|
151
328
|
: {};
|
|
152
|
-
overrides[
|
|
329
|
+
overrides[encodeChainKey(chain, 'pnpm')] = entry.range;
|
|
153
330
|
pnpmBlock.overrides = overrides;
|
|
154
331
|
next.pnpm = pnpmBlock;
|
|
155
332
|
return next;
|
|
156
333
|
}
|
|
334
|
+
if (field === 'resolutions') {
|
|
335
|
+
const current = pkg[field];
|
|
336
|
+
const block = current && typeof current === 'object' && !Array.isArray(current)
|
|
337
|
+
? { ...current }
|
|
338
|
+
: {};
|
|
339
|
+
block[encodeChainKey(chain, 'yarn')] = entry.range;
|
|
340
|
+
next[field] = block;
|
|
341
|
+
return next;
|
|
342
|
+
}
|
|
343
|
+
// npm nested-object form. For flat overrides (chain.length === 1) we still write to the top
|
|
344
|
+
// level; otherwise we descend, cloning every intermediate level so we don't mutate the
|
|
345
|
+
// input object.
|
|
157
346
|
const current = pkg[field];
|
|
158
|
-
const
|
|
347
|
+
const rootBlock = current && typeof current === 'object' && !Array.isArray(current)
|
|
159
348
|
? { ...current }
|
|
160
349
|
: {};
|
|
161
|
-
|
|
162
|
-
|
|
350
|
+
if (chain.length === 1) {
|
|
351
|
+
rootBlock[chain[0]] = entry.range;
|
|
352
|
+
next[field] = rootBlock;
|
|
353
|
+
return next;
|
|
354
|
+
}
|
|
355
|
+
// Descend into `rootBlock[parent[0]] = {...}`, merging with whatever's already there. If an
|
|
356
|
+
// existing entry at the parent is a STRING (flat pin), npm's spec promotes it under `"."`
|
|
357
|
+
// before we nest — that preserves the existing pin AND adds the scoped child.
|
|
358
|
+
let cursor = rootBlock;
|
|
359
|
+
for (let i = 0; i < chain.length - 1; i++) {
|
|
360
|
+
const seg = chain[i];
|
|
361
|
+
const existing = cursor[seg];
|
|
362
|
+
if (typeof existing === 'string') {
|
|
363
|
+
cursor[seg] = { '.': existing };
|
|
364
|
+
}
|
|
365
|
+
else if (!existing || typeof existing !== 'object' || Array.isArray(existing)) {
|
|
366
|
+
cursor[seg] = {};
|
|
367
|
+
}
|
|
368
|
+
else {
|
|
369
|
+
cursor[seg] = { ...existing };
|
|
370
|
+
}
|
|
371
|
+
cursor = cursor[seg];
|
|
372
|
+
}
|
|
373
|
+
cursor[chain[chain.length - 1]] = entry.range;
|
|
374
|
+
next[field] = rootBlock;
|
|
163
375
|
return next;
|
|
164
376
|
}
|
|
377
|
+
/**
|
|
378
|
+
* Encode a `chain: string[]` into the single-string key pnpm / yarn store in their flat
|
|
379
|
+
* overrides map. pnpm uses `>`; yarn uses `/`. Scoped names are preserved verbatim inside
|
|
380
|
+
* each segment since we only join WITH the separator — we never touch the segment contents.
|
|
381
|
+
*/
|
|
382
|
+
export function encodeChainKey(chain, manager) {
|
|
383
|
+
const sep = manager === 'pnpm' ? '>' : '/';
|
|
384
|
+
return chain.join(sep);
|
|
385
|
+
}
|
|
386
|
+
/**
|
|
387
|
+
* Parse a user-facing selector string into a `chain + range` pair.
|
|
388
|
+
*
|
|
389
|
+
* Accepts BOTH the pnpm-style `parent>child@1.2.3` AND the yarn-style `parent/child@1.2.3`
|
|
390
|
+
* form (after accounting for scoped-name slashes). When neither separator is present the
|
|
391
|
+
* selector is treated as a flat override. The trailing `@<range>` is optional — callers that
|
|
392
|
+
* know the range separately (e.g. audit-derived pins) can pass just the chain.
|
|
393
|
+
*
|
|
394
|
+
* Returns `undefined` when the selector is malformed (empty, dangling `@`, trailing separator).
|
|
395
|
+
*/
|
|
396
|
+
export function parseOverrideSelector(spec) {
|
|
397
|
+
const trimmed = spec.trim();
|
|
398
|
+
if (!trimmed)
|
|
399
|
+
return undefined;
|
|
400
|
+
// Split off the `@<range>` suffix. Scoped names start with `@`, so we look for the LAST `@`
|
|
401
|
+
// that has a separator-free tail (version / range) after it. Missing `@` is OK — caller
|
|
402
|
+
// supplies the range elsewhere.
|
|
403
|
+
let chainPart = trimmed;
|
|
404
|
+
let range;
|
|
405
|
+
const atIdx = findRangeAt(trimmed);
|
|
406
|
+
if (atIdx !== -1) {
|
|
407
|
+
chainPart = trimmed.slice(0, atIdx);
|
|
408
|
+
range = trimmed.slice(atIdx + 1).trim();
|
|
409
|
+
if (!range)
|
|
410
|
+
return undefined;
|
|
411
|
+
}
|
|
412
|
+
// Chain separator detection: `>` wins if present (pnpm style is unambiguous); else `/`
|
|
413
|
+
// (yarn style, scoped-name-aware). Plain names fall through as `[chainPart]`.
|
|
414
|
+
let chain;
|
|
415
|
+
if (chainPart.includes('>')) {
|
|
416
|
+
chain = chainPart.split('>').map((s) => s.trim()).filter(Boolean);
|
|
417
|
+
}
|
|
418
|
+
else {
|
|
419
|
+
chain = parseChainKey(chainPart, 'yarn');
|
|
420
|
+
}
|
|
421
|
+
if (chain.length === 0)
|
|
422
|
+
return undefined;
|
|
423
|
+
const result = { chain };
|
|
424
|
+
if (range)
|
|
425
|
+
result.range = range;
|
|
426
|
+
return result;
|
|
427
|
+
}
|
|
428
|
+
/**
|
|
429
|
+
* Locate the `@` that separates the chain from the version suffix. Scoped names contain `@`
|
|
430
|
+
* in front so we skip leading `@`s; the version `@` is always the LAST `@` in the string and
|
|
431
|
+
* must be followed by at least one non-space character.
|
|
432
|
+
*/
|
|
433
|
+
function findRangeAt(s) {
|
|
434
|
+
// Walk backwards. The range `@` is the first `@` we encounter that is NOT immediately after
|
|
435
|
+
// a chain separator (`/` or `>`) and is NOT at position 0 (scoped package prefix).
|
|
436
|
+
for (let i = s.length - 1; i > 0; i--) {
|
|
437
|
+
if (s[i] !== '@')
|
|
438
|
+
continue;
|
|
439
|
+
const prev = s[i - 1];
|
|
440
|
+
if (prev === '/' || prev === '>')
|
|
441
|
+
continue; // scoped name inside chain segment
|
|
442
|
+
return i;
|
|
443
|
+
}
|
|
444
|
+
return -1;
|
|
445
|
+
}
|
|
165
446
|
/**
|
|
166
447
|
* Disk-level convenience: read the package.json, compute the decision, write the new pin when
|
|
167
448
|
* appropriate, and return a structured result. The file is read + written with 2-space JSON.
|
|
@@ -196,9 +477,12 @@ export async function applyOverrideToFile(opts) {
|
|
|
196
477
|
reason: `parse failed: ${e instanceof Error ? e.message : String(e)}`,
|
|
197
478
|
};
|
|
198
479
|
}
|
|
199
|
-
// Read existing pin so we can decide skip / write / conflict.
|
|
480
|
+
// Read existing pin so we can decide skip / write / conflict. Match on the EXACT chain,
|
|
481
|
+
// not just the name — a flat `foo` pin and a parent-scoped `bar>foo` pin are separate
|
|
482
|
+
// entries and don't interact (both can coexist; overwriting one must never touch the other).
|
|
200
483
|
const read = readOverrides(pkg, opts.manager);
|
|
201
|
-
const
|
|
484
|
+
const wantChain = [...(opts.entry.parentChain ?? []), opts.entry.name];
|
|
485
|
+
const existing = read.entries.find((e) => chainsEqual(e.chain, wantChain));
|
|
202
486
|
const decision = decideOverride(existing?.range, opts.entry.range);
|
|
203
487
|
if (decision.action === 'skip') {
|
|
204
488
|
return {
|
|
@@ -250,8 +534,18 @@ export async function applyOverrideToFile(opts) {
|
|
|
250
534
|
/**
|
|
251
535
|
* Inverse of `applyOverrideToFile` — delete a single override entry. Used by the rollback
|
|
252
536
|
* path when the install + validator fails AFTER we added a pin.
|
|
537
|
+
*
|
|
538
|
+
* `target` can be a bare package name (drops the flat top-level entry, legacy API) or a
|
|
539
|
+
* structured `{ chain: string[] }` selector (drops a parent-scoped entry matching the exact
|
|
540
|
+
* chain). When a parent-scoped write was adjacent to a sibling-writing flat pin, we leave the
|
|
541
|
+
* sibling intact; same-level empty objects are pruned recursively so the file shrinks back to
|
|
542
|
+
* what existed before the write.
|
|
253
543
|
*/
|
|
254
|
-
export async function removeOverrideFromFile(packageJsonPath, manager,
|
|
544
|
+
export async function removeOverrideFromFile(packageJsonPath, manager, target) {
|
|
545
|
+
const chain = typeof target === 'string' ? [target] : [...target.chain];
|
|
546
|
+
if (chain.length === 0) {
|
|
547
|
+
return { ok: true, removed: false };
|
|
548
|
+
}
|
|
255
549
|
let raw;
|
|
256
550
|
try {
|
|
257
551
|
raw = await fs.readFile(packageJsonPath, 'utf8');
|
|
@@ -273,9 +567,13 @@ export async function removeOverrideFromFile(packageJsonPath, manager, name) {
|
|
|
273
567
|
if (pnpm && typeof pnpm === 'object') {
|
|
274
568
|
const pnpmObj = { ...pnpm };
|
|
275
569
|
const overrides = pnpmObj.overrides;
|
|
276
|
-
if (overrides && typeof overrides === 'object'
|
|
570
|
+
if (overrides && typeof overrides === 'object') {
|
|
277
571
|
const next = { ...overrides };
|
|
278
|
-
|
|
572
|
+
const key = encodeChainKey(chain, 'pnpm');
|
|
573
|
+
if (key in next) {
|
|
574
|
+
delete next[key];
|
|
575
|
+
removed = true;
|
|
576
|
+
}
|
|
279
577
|
if (Object.keys(next).length === 0) {
|
|
280
578
|
delete pnpmObj.overrides;
|
|
281
579
|
if (Object.keys(pnpmObj).length === 0) {
|
|
@@ -289,22 +587,38 @@ export async function removeOverrideFromFile(packageJsonPath, manager, name) {
|
|
|
289
587
|
pnpmObj.overrides = next;
|
|
290
588
|
pkg.pnpm = pnpmObj;
|
|
291
589
|
}
|
|
292
|
-
removed = true;
|
|
293
590
|
}
|
|
294
591
|
}
|
|
295
592
|
}
|
|
296
|
-
else {
|
|
593
|
+
else if (field === 'resolutions') {
|
|
297
594
|
const block = pkg[field];
|
|
298
|
-
if (block && typeof block === 'object'
|
|
595
|
+
if (block && typeof block === 'object') {
|
|
299
596
|
const next = { ...block };
|
|
300
|
-
|
|
597
|
+
const key = encodeChainKey(chain, 'yarn');
|
|
598
|
+
if (key in next) {
|
|
599
|
+
delete next[key];
|
|
600
|
+
removed = true;
|
|
601
|
+
}
|
|
301
602
|
if (Object.keys(next).length === 0) {
|
|
302
603
|
delete pkg[field];
|
|
303
604
|
}
|
|
304
605
|
else {
|
|
305
606
|
pkg[field] = next;
|
|
306
607
|
}
|
|
307
|
-
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
else {
|
|
611
|
+
// npm: flat OR nested object path.
|
|
612
|
+
const block = pkg[field];
|
|
613
|
+
if (block && typeof block === 'object' && !Array.isArray(block)) {
|
|
614
|
+
const nextRoot = { ...block };
|
|
615
|
+
removed = removeChainFromNpmNested(nextRoot, chain);
|
|
616
|
+
if (Object.keys(nextRoot).length === 0) {
|
|
617
|
+
delete pkg[field];
|
|
618
|
+
}
|
|
619
|
+
else {
|
|
620
|
+
pkg[field] = nextRoot;
|
|
621
|
+
}
|
|
308
622
|
}
|
|
309
623
|
}
|
|
310
624
|
if (!removed) {
|