Skip to content

Commit c285766

Browse files
committed
fix(ci): merge nested shrinkwrap override pins
1 parent 8ee767b commit c285766

2 files changed

Lines changed: 127 additions & 0 deletions

File tree

scripts/generate-npm-shrinkwrap.mjs

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -305,11 +305,60 @@ function mergeOverrideEntry(merged, name, spec) {
305305
}
306306
return;
307307
}
308+
if (
309+
typeof current === "string" &&
310+
isPlainObject(spec) &&
311+
typeof spec["."] === "string" &&
312+
exactOverrideVersionsMatch(current, spec["."])
313+
) {
314+
merged[name] = { ".": preferredExactOverrideRootSpec(current, spec["."]) };
315+
for (const [nestedName, nestedSpec] of Object.entries(spec)) {
316+
if (nestedName === ".") {
317+
continue;
318+
}
319+
mergeOverrideEntry(merged[name], nestedName, nestedSpec);
320+
}
321+
return;
322+
}
323+
if (
324+
isPlainObject(current) &&
325+
typeof spec === "string" &&
326+
typeof current["."] === "string" &&
327+
exactOverrideVersionsMatch(current["."], spec)
328+
) {
329+
current["."] = preferredExactOverrideRootSpec(current["."], spec);
330+
return;
331+
}
308332
if (JSON.stringify(current) !== JSON.stringify(spec)) {
309333
throw new Error(`package.json overrides.${name} conflicts with pnpm lock policy for ${name}`);
310334
}
311335
}
312336

337+
function preferredExactOverrideRootSpec(current, incoming) {
338+
return incoming.startsWith("npm:") ? incoming : current;
339+
}
340+
341+
function exactOverrideVersionsMatch(left, right) {
342+
const leftVersion = exactVersionFromOverrideSpec(left);
343+
if (leftVersion === null || leftVersion !== exactVersionFromOverrideSpec(right)) {
344+
return false;
345+
}
346+
const leftAlias = parseNpmAliasOverrideSpec(left);
347+
const rightAlias = parseNpmAliasOverrideSpec(right);
348+
return !leftAlias || !rightAlias || leftAlias.name === rightAlias.name;
349+
}
350+
351+
function parseNpmAliasOverrideSpec(spec) {
352+
if (!spec.startsWith("npm:")) {
353+
return null;
354+
}
355+
const versionIndex = spec.lastIndexOf("@");
356+
if (versionIndex <= "npm:".length) {
357+
return null;
358+
}
359+
return { name: spec.slice("npm:".length, versionIndex) };
360+
}
361+
313362
function mergeOverrides(packageOverrides, workspaceOverrides, pnpmLockOverrides) {
314363
const merged = normalizeOverrides(packageOverrides);
315364
for (const [name, spec] of [
@@ -1085,6 +1134,7 @@ export {
10851134
disableShrinkwrappedOverrideConflictSources,
10861135
exactOverrideRulesFromOverrides,
10871136
exactVersionFromOverrideSpec,
1137+
mergeOverrides,
10881138
applyPackageExtensionPeerMetadata,
10891139
normalizeNpmVersionDrift,
10901140
packageJsonForShrinkwrap,

test/package-manager-config.test.ts

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { parse } from "yaml";
44
import {
55
collectCurrentShrinkwrapOverrides,
66
collectPnpmLockViolations,
7+
mergeOverrides,
78
parsePnpmPackageKey,
89
readShrinkwrapOverrides,
910
} from "../scripts/generate-npm-shrinkwrap.mjs";
@@ -132,6 +133,82 @@ describe("package manager build policy", () => {
132133
});
133134
});
134135

136+
it("merges exact current shrinkwrap pins with nested lock-derived pins", () => {
137+
expect(
138+
mergeOverrides(
139+
{ "@mistralai/mistralai": "2.2.1" },
140+
{ "@mistralai/mistralai": { ".": "2.2.1", zod: "4.4.3" } },
141+
{},
142+
),
143+
).toEqual({
144+
"@mistralai/mistralai": { ".": "2.2.1", zod: "4.4.3" },
145+
});
146+
});
147+
148+
it("preserves npm alias pins when merging nested lock-derived pins", () => {
149+
expect(
150+
mergeOverrides(
151+
{ "node-domexception": "npm:@nolyfill/domexception@1.0.28" },
152+
{ "node-domexception": { ".": "1.0.28", child: "2.0.0" } },
153+
{},
154+
),
155+
).toEqual({
156+
"node-domexception": {
157+
".": "npm:@nolyfill/domexception@1.0.28",
158+
child: "2.0.0",
159+
},
160+
});
161+
});
162+
163+
it("preserves later npm alias pins when nested pins are already merged", () => {
164+
expect(
165+
mergeOverrides(
166+
{ "node-domexception": { ".": "1.0.28", child: "2.0.0" } },
167+
{ "node-domexception": "npm:@nolyfill/domexception@1.0.28" },
168+
{},
169+
),
170+
).toEqual({
171+
"node-domexception": {
172+
".": "npm:@nolyfill/domexception@1.0.28",
173+
child: "2.0.0",
174+
},
175+
});
176+
});
177+
178+
it("rejects non-exact root pins when merging nested pins", () => {
179+
expect(() =>
180+
mergeOverrides(
181+
{ "floating-package": "^1.0.0" },
182+
{ "floating-package": { ".": "~1.0.0", child: "2.0.0" } },
183+
{},
184+
),
185+
).toThrow(/conflicts with pnpm lock policy/u);
186+
expect(() =>
187+
mergeOverrides(
188+
{ "floating-package": { ".": "^1.0.0", child: "2.0.0" } },
189+
{ "floating-package": "~1.0.0" },
190+
{},
191+
),
192+
).toThrow(/conflicts with pnpm lock policy/u);
193+
});
194+
195+
it("rejects distinct npm alias targets with matching versions", () => {
196+
expect(() =>
197+
mergeOverrides(
198+
{ "aliased-package": "npm:@safe/foo@1.0.0" },
199+
{ "aliased-package": { ".": "npm:@other/foo@1.0.0", child: "2.0.0" } },
200+
{},
201+
),
202+
).toThrow(/conflicts with pnpm lock policy/u);
203+
expect(() =>
204+
mergeOverrides(
205+
{ "aliased-package": { ".": "npm:@safe/foo@1.0.0", child: "2.0.0" } },
206+
{ "aliased-package": "npm:@other/foo@1.0.0" },
207+
{},
208+
),
209+
).toThrow(/conflicts with pnpm lock policy/u);
210+
});
211+
135212
it("keeps npm shrinkwrap package versions inside the pnpm lock graph", () => {
136213
const pnpmLockPackages = collectPnpmLockPackages();
137214
const shrinkwrapPaths = [

0 commit comments

Comments
 (0)