Skip to content

Commit fad6e8f

Browse files
Copilotbmiddha
andcommitted
fix: handle versioned selectors in globalOverrides for shrinkwrap comparison
When globalOverrides uses versioned selectors (e.g. "webpack@5": "5.103.0"), the shrinkwrap comparison now correctly resolves the override by matching the package name and checking version range intersection with semver.intersects. Previously, only exact package name matches were checked (this.overrides.get(name)), which caused versioned selector keys like "webpack@5" to never match lookups for "webpack", making rush update never stabilize. Co-authored-by: bmiddha <5100938+bmiddha@users.noreply.github.com>
1 parent 6132b7d commit fad6e8f

5 files changed

Lines changed: 197 additions & 3 deletions

File tree

libraries/rush-lib/src/logic/pnpm/PnpmShrinkwrapFile.ts

Lines changed: 47 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1110,7 +1110,9 @@ export class PnpmShrinkwrapFile extends BaseShrinkwrapFile {
11101110
if (!foundDependency) {
11111111
return true;
11121112
}
1113-
const resolvedVersion: string = this.overrides.get(importerPackageName) ?? foundDependency.version;
1113+
const resolvedVersion: string =
1114+
this._resolveOverrideVersion(importerPackageName, foundDependency.version) ??
1115+
foundDependency.version;
11141116
if (resolvedVersion !== importerVersionSpecifier) {
11151117
return true;
11161118
}
@@ -1167,7 +1169,7 @@ export class PnpmShrinkwrapFile extends BaseShrinkwrapFile {
11671169
if (this.shrinkwrapFileMajorVersion >= ShrinkwrapFileMajorVersion.V9) {
11681170
// TODO: Emit an error message when someone tries to override a version of something in one of their
11691171
// local repo packages.
1170-
let resolvedVersion: string = this.overrides.get(name) ?? version;
1172+
let resolvedVersion: string = this._resolveOverrideVersion(name, version) ?? version;
11711173
// convert path in posix style, otherwise pnpm install will fail in subspace case
11721174
resolvedVersion = Path.convertToSlashes(resolvedVersion);
11731175
const specifier: string = importer.specifiers[name];
@@ -1183,7 +1185,7 @@ export class PnpmShrinkwrapFile extends BaseShrinkwrapFile {
11831185
} else {
11841186
// TODO: Emit an error message when someone tries to override a version of something in one of their
11851187
// local repo packages.
1186-
let resolvedVersion: string = this.overrides.get(name) ?? version;
1188+
let resolvedVersion: string = this._resolveOverrideVersion(name, version) ?? version;
11871189
// convert path in posix style, otherwise pnpm install will fail in subspace case
11881190
resolvedVersion = Path.convertToSlashes(resolvedVersion);
11891191
if (
@@ -1218,6 +1220,48 @@ export class PnpmShrinkwrapFile extends BaseShrinkwrapFile {
12181220
return false;
12191221
}
12201222

1223+
/**
1224+
* Look up the override value for a given package name and version.
1225+
* Handles versioned selectors like "webpack@5" in addition to simple "webpack" keys.
1226+
*/
1227+
private _resolveOverrideVersion(name: string, version: string): string | undefined {
1228+
// First, try exact match by package name
1229+
const exactMatch: string | undefined = this.overrides.get(name);
1230+
if (exactMatch !== undefined) {
1231+
return exactMatch;
1232+
}
1233+
1234+
// Then try versioned selectors (e.g., "webpack@5" or "@scope/pkg@^2.0.0")
1235+
for (const [key, value] of this.overrides) {
1236+
// Skip nested dependency selectors (contain '>')
1237+
if (key.includes('>')) {
1238+
continue;
1239+
}
1240+
1241+
// Parse the package name and version range from the override key.
1242+
// Handle scoped packages (@scope/pkg@range) by finding the '@' after the scope prefix.
1243+
const atIndex: number = key.startsWith('@') ? key.indexOf('@', 1) : key.indexOf('@');
1244+
if (atIndex === -1) {
1245+
continue; // No version selector, already handled by exact match above
1246+
}
1247+
1248+
const packageName: string = key.substring(0, atIndex);
1249+
const rangeStr: string = key.substring(atIndex + 1);
1250+
1251+
if (packageName === name) {
1252+
try {
1253+
if (semver.intersects(version, rangeStr)) {
1254+
return value;
1255+
}
1256+
} catch {
1257+
// If semver parsing fails (e.g. for non-semver versions), skip this override
1258+
}
1259+
}
1260+
}
1261+
1262+
return undefined;
1263+
}
1264+
12211265
private _getIntegrityForPackage(specifier: string, optional: boolean): Map<string, string> {
12221266
const integrities: Map<string, Map<string, string>> = this._integrities;
12231267

libraries/rush-lib/src/logic/pnpm/test/PnpmShrinkwrapFile.test.ts

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -362,6 +362,21 @@ snapshots:
362362
)
363363
).resolves.toBe(false);
364364
});
365+
366+
it('can detect versioned overrides', async () => {
367+
const project = getMockRushProject();
368+
const pnpmShrinkwrapFile = getPnpmShrinkwrapFileFromFile(
369+
`${__dirname}/yamlFiles/pnpm-lock-v5/versioned-overrides-not-modified.yaml`,
370+
project.rushConfiguration.defaultSubspace
371+
);
372+
await expect(
373+
pnpmShrinkwrapFile.isWorkspaceProjectModifiedAsync(
374+
project,
375+
project.rushConfiguration.defaultSubspace,
376+
undefined
377+
)
378+
).resolves.toBe(false);
379+
});
365380
});
366381

367382
describe('pnpm lockfile major version 6', () => {
@@ -410,6 +425,21 @@ snapshots:
410425
).resolves.toBe(false);
411426
});
412427

428+
it('can detect versioned overrides', async () => {
429+
const project = getMockRushProject();
430+
const pnpmShrinkwrapFile = getPnpmShrinkwrapFileFromFile(
431+
`${__dirname}/yamlFiles/pnpm-lock-v6/versioned-overrides-not-modified.yaml`,
432+
project.rushConfiguration.defaultSubspace
433+
);
434+
await expect(
435+
pnpmShrinkwrapFile.isWorkspaceProjectModifiedAsync(
436+
project,
437+
project.rushConfiguration.defaultSubspace,
438+
undefined
439+
)
440+
).resolves.toBe(false);
441+
});
442+
413443
it('can handle the inconsistent version of a package declared in dependencies and devDependencies', async () => {
414444
const project = getMockRushProject2();
415445
const pnpmShrinkwrapFile = getPnpmShrinkwrapFileFromFile(
@@ -472,6 +502,21 @@ snapshots:
472502
).resolves.toBe(false);
473503
});
474504

505+
it('can detect versioned overrides', async () => {
506+
const project = getMockRushProject();
507+
const pnpmShrinkwrapFile = getPnpmShrinkwrapFileFromFile(
508+
`${__dirname}/yamlFiles/pnpm-lock-v9/versioned-overrides-not-modified.yaml`,
509+
project.rushConfiguration.defaultSubspace
510+
);
511+
await expect(
512+
pnpmShrinkwrapFile.isWorkspaceProjectModifiedAsync(
513+
project,
514+
project.rushConfiguration.defaultSubspace,
515+
undefined
516+
)
517+
).resolves.toBe(false);
518+
});
519+
475520
it('can handle the inconsistent version of a package declared in dependencies and devDependencies', async () => {
476521
const project = getMockRushProject2();
477522
const pnpmShrinkwrapFile = getPnpmShrinkwrapFileFromFile(
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
lockfileVersion: 5.3
2+
3+
overrides:
4+
typescript@~5: 5.0.4
5+
6+
importers:
7+
.:
8+
specifiers: {}
9+
10+
../../apps/foo:
11+
specifiers:
12+
tslib: ~2.3.1
13+
typescript: 5.0.4
14+
dependencies:
15+
tslib: 2.3.1
16+
devDependencies:
17+
typescript: 5.0.4
18+
19+
packages:
20+
/typescript/5.0.4:
21+
resolution:
22+
{
23+
integrity: sha512-cW9T5W9xY37cc+jfEnaUvX91foxtHkza3Nw3wkoF4sSlKn0MONdkdEndig/qPBWXNkmplh3NzayQzCiHM4/hqw==
24+
}
25+
engines: { node: '>=12.20' }
26+
hasBin: true
27+
28+
/tslib/2.3.1:
29+
resolution:
30+
{
31+
integrity: sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==
32+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
lockfileVersion: '6.0'
2+
3+
overrides:
4+
typescript@~5: 5.0.4
5+
6+
importers:
7+
.: {}
8+
9+
../../apps/foo:
10+
dependencies:
11+
tslib:
12+
specifier: ~2.3.1
13+
version: 2.3.1
14+
devDependencies:
15+
typescript:
16+
specifier: 5.0.4
17+
version: 5.0.4
18+
19+
packages:
20+
/typescript/5.0.4:
21+
resolution:
22+
{
23+
integrity: sha512-cW9T5W9xY37cc+jfEnaUvX91foxtHkza3Nw3wkoF4sSlKn0MONdkdEndig/qPBWXNkmplh3NzayQzCiHM4/hqw==
24+
}
25+
engines: { node: '>=12.20' }
26+
hasBin: true
27+
28+
/tslib/2.3.1:
29+
resolution:
30+
{
31+
integrity: sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==
32+
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
lockfileVersion: '9.0'
2+
3+
settings:
4+
autoInstallPeers: true
5+
excludeLinksFromLockfile: false
6+
7+
overrides:
8+
typescript@~5: 5.0.4
9+
10+
importers:
11+
.: {}
12+
13+
../../apps/foo:
14+
dependencies:
15+
tslib:
16+
specifier: ~2.3.1
17+
version: 2.3.1
18+
devDependencies:
19+
typescript:
20+
specifier: 5.0.4
21+
version: 5.0.4
22+
23+
packages:
24+
tslib@2.3.1:
25+
resolution:
26+
{
27+
integrity: sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==
28+
}
29+
30+
typescript@5.0.4:
31+
resolution:
32+
{
33+
integrity: sha512-cW9T5W9xY37cc+jfEnaUvX91foxtHkza3Nw3wkoF4sSlKn0MONdkdEndig/qPBWXNkmplh3NzayQzCiHM4/hqw==
34+
}
35+
engines: { node: '>=12.20' }
36+
hasBin: true
37+
38+
snapshots:
39+
tslib@2.3.1: {}
40+
41+
typescript@5.0.4: {}

0 commit comments

Comments
 (0)