diff --git a/common/changes/@microsoft/rush/fix-rush-update-stability_2026-02-09-23-11.json b/common/changes/@microsoft/rush/fix-rush-update-stability_2026-02-09-23-11.json new file mode 100644 index 00000000000..cb03b28a05a --- /dev/null +++ b/common/changes/@microsoft/rush/fix-rush-update-stability_2026-02-09-23-11.json @@ -0,0 +1,10 @@ +{ + "changes": [ + { + "packageName": "@microsoft/rush", + "comment": "Handle versioned selectors in `globalOverrides` during pnpm shrinkwrap comparison.", + "type": "patch" + } + ], + "packageName": "@microsoft/rush" +} diff --git a/libraries/rush-lib/src/logic/pnpm/PnpmShrinkwrapFile.ts b/libraries/rush-lib/src/logic/pnpm/PnpmShrinkwrapFile.ts index 97068b557ec..e673e6b0857 100644 --- a/libraries/rush-lib/src/logic/pnpm/PnpmShrinkwrapFile.ts +++ b/libraries/rush-lib/src/logic/pnpm/PnpmShrinkwrapFile.ts @@ -1110,7 +1110,9 @@ export class PnpmShrinkwrapFile extends BaseShrinkwrapFile { if (!foundDependency) { return true; } - const resolvedVersion: string = this.overrides.get(importerPackageName) ?? foundDependency.version; + const resolvedVersion: string = + this._resolveOverrideVersion(importerPackageName, foundDependency.version) ?? + foundDependency.version; if (resolvedVersion !== importerVersionSpecifier) { return true; } @@ -1167,7 +1169,7 @@ export class PnpmShrinkwrapFile extends BaseShrinkwrapFile { if (this.shrinkwrapFileMajorVersion >= ShrinkwrapFileMajorVersion.V9) { // TODO: Emit an error message when someone tries to override a version of something in one of their // local repo packages. - let resolvedVersion: string = this.overrides.get(name) ?? version; + let resolvedVersion: string = this._resolveOverrideVersion(name, version) ?? version; // convert path in posix style, otherwise pnpm install will fail in subspace case resolvedVersion = Path.convertToSlashes(resolvedVersion); const specifier: string = importer.specifiers[name]; @@ -1183,7 +1185,7 @@ export class PnpmShrinkwrapFile extends BaseShrinkwrapFile { } else { // TODO: Emit an error message when someone tries to override a version of something in one of their // local repo packages. - let resolvedVersion: string = this.overrides.get(name) ?? version; + let resolvedVersion: string = this._resolveOverrideVersion(name, version) ?? version; // convert path in posix style, otherwise pnpm install will fail in subspace case resolvedVersion = Path.convertToSlashes(resolvedVersion); if ( @@ -1218,6 +1220,57 @@ export class PnpmShrinkwrapFile extends BaseShrinkwrapFile { return false; } + /** + * Look up the override value for a given package name and version. + * Handles versioned selectors like "webpack@5" and nested dependency selectors like + * "consumer>webpack" or "consumer@1>webpack@5" in addition to simple "webpack" keys. + */ + private _resolveOverrideVersion(name: string, version: string): string | undefined { + // First, try exact match by package name + const exactMatch: string | undefined = this.overrides.get(name); + if (exactMatch !== undefined) { + return exactMatch; + } + + // Then try versioned selectors (e.g., "webpack@5" or "@scope/pkg@^2.0.0") + // and nested dependency selectors (e.g., "consumer>webpack" or "consumer@1>webpack@5") + for (const [key, value] of this.overrides) { + // For nested dependency selectors (contain '>'), extract the dependency portion after '>'. + // For example: "bar@1>foo@2" -> dependency selector is "foo@2" + const isNested: boolean = key.includes('>'); + const depSelector: string = isNested ? key.substring(key.indexOf('>') + 1) : key; + + // Parse the package name and version range from the selector. + // Handle scoped packages (@scope/pkg@range) by finding the '@' after the scope prefix. + const atIndex: number = depSelector.startsWith('@') + ? depSelector.indexOf('@', 1) + : depSelector.indexOf('@'); + if (atIndex === -1) { + // No version selector; for non-nested keys this was already handled by exact match above. + // For nested keys (e.g. "consumer>webpack"), check if the dependency name matches. + if (isNested && depSelector === name) { + return value; + } + continue; + } + + const packageName: string = depSelector.substring(0, atIndex); + const rangeStr: string = depSelector.substring(atIndex + 1); + + if (packageName === name) { + try { + if (semver.intersects(version, rangeStr)) { + return value; + } + } catch { + // If semver parsing fails (e.g. for non-semver versions), skip this override + } + } + } + + return undefined; + } + private _getIntegrityForPackage(specifier: string, optional: boolean): Map { const integrities: Map> = this._integrities; diff --git a/libraries/rush-lib/src/logic/pnpm/test/PnpmShrinkwrapFile.test.ts b/libraries/rush-lib/src/logic/pnpm/test/PnpmShrinkwrapFile.test.ts index b0a090ed9fc..e00bfea64e9 100644 --- a/libraries/rush-lib/src/logic/pnpm/test/PnpmShrinkwrapFile.test.ts +++ b/libraries/rush-lib/src/logic/pnpm/test/PnpmShrinkwrapFile.test.ts @@ -362,6 +362,36 @@ snapshots: ) ).resolves.toBe(false); }); + + it('can detect versioned overrides', async () => { + const project = getMockRushProject(); + const pnpmShrinkwrapFile = getPnpmShrinkwrapFileFromFile( + `${__dirname}/yamlFiles/pnpm-lock-v5/versioned-overrides-not-modified.yaml`, + project.rushConfiguration.defaultSubspace + ); + await expect( + pnpmShrinkwrapFile.isWorkspaceProjectModifiedAsync( + project, + project.rushConfiguration.defaultSubspace, + undefined + ) + ).resolves.toBe(false); + }); + + it('can detect nested dependency overrides', async () => { + const project = getMockRushProject(); + const pnpmShrinkwrapFile = getPnpmShrinkwrapFileFromFile( + `${__dirname}/yamlFiles/pnpm-lock-v5/nested-overrides-not-modified.yaml`, + project.rushConfiguration.defaultSubspace + ); + await expect( + pnpmShrinkwrapFile.isWorkspaceProjectModifiedAsync( + project, + project.rushConfiguration.defaultSubspace, + undefined + ) + ).resolves.toBe(false); + }); }); describe('pnpm lockfile major version 6', () => { @@ -410,6 +440,36 @@ snapshots: ).resolves.toBe(false); }); + it('can detect versioned overrides', async () => { + const project = getMockRushProject(); + const pnpmShrinkwrapFile = getPnpmShrinkwrapFileFromFile( + `${__dirname}/yamlFiles/pnpm-lock-v6/versioned-overrides-not-modified.yaml`, + project.rushConfiguration.defaultSubspace + ); + await expect( + pnpmShrinkwrapFile.isWorkspaceProjectModifiedAsync( + project, + project.rushConfiguration.defaultSubspace, + undefined + ) + ).resolves.toBe(false); + }); + + it('can detect nested dependency overrides', async () => { + const project = getMockRushProject(); + const pnpmShrinkwrapFile = getPnpmShrinkwrapFileFromFile( + `${__dirname}/yamlFiles/pnpm-lock-v6/nested-overrides-not-modified.yaml`, + project.rushConfiguration.defaultSubspace + ); + await expect( + pnpmShrinkwrapFile.isWorkspaceProjectModifiedAsync( + project, + project.rushConfiguration.defaultSubspace, + undefined + ) + ).resolves.toBe(false); + }); + it('can handle the inconsistent version of a package declared in dependencies and devDependencies', async () => { const project = getMockRushProject2(); const pnpmShrinkwrapFile = getPnpmShrinkwrapFileFromFile( @@ -472,6 +532,36 @@ snapshots: ).resolves.toBe(false); }); + it('can detect versioned overrides', async () => { + const project = getMockRushProject(); + const pnpmShrinkwrapFile = getPnpmShrinkwrapFileFromFile( + `${__dirname}/yamlFiles/pnpm-lock-v9/versioned-overrides-not-modified.yaml`, + project.rushConfiguration.defaultSubspace + ); + await expect( + pnpmShrinkwrapFile.isWorkspaceProjectModifiedAsync( + project, + project.rushConfiguration.defaultSubspace, + undefined + ) + ).resolves.toBe(false); + }); + + it('can detect nested dependency overrides', async () => { + const project = getMockRushProject(); + const pnpmShrinkwrapFile = getPnpmShrinkwrapFileFromFile( + `${__dirname}/yamlFiles/pnpm-lock-v9/nested-overrides-not-modified.yaml`, + project.rushConfiguration.defaultSubspace + ); + await expect( + pnpmShrinkwrapFile.isWorkspaceProjectModifiedAsync( + project, + project.rushConfiguration.defaultSubspace, + undefined + ) + ).resolves.toBe(false); + }); + it('can handle the inconsistent version of a package declared in dependencies and devDependencies', async () => { const project = getMockRushProject2(); const pnpmShrinkwrapFile = getPnpmShrinkwrapFileFromFile( diff --git a/libraries/rush-lib/src/logic/pnpm/test/yamlFiles/pnpm-lock-v5/nested-overrides-not-modified.yaml b/libraries/rush-lib/src/logic/pnpm/test/yamlFiles/pnpm-lock-v5/nested-overrides-not-modified.yaml new file mode 100644 index 00000000000..8d2160a9ffc --- /dev/null +++ b/libraries/rush-lib/src/logic/pnpm/test/yamlFiles/pnpm-lock-v5/nested-overrides-not-modified.yaml @@ -0,0 +1,32 @@ +lockfileVersion: 5.3 + +overrides: + foo>typescript: 5.0.4 + +importers: + .: + specifiers: {} + + ../../apps/foo: + specifiers: + tslib: ~2.3.1 + typescript: 5.0.4 + dependencies: + tslib: 2.3.1 + devDependencies: + typescript: 5.0.4 + +packages: + /typescript/5.0.4: + resolution: + { + integrity: sha512-cW9T5W9xY37cc+jfEnaUvX91foxtHkza3Nw3wkoF4sSlKn0MONdkdEndig/qPBWXNkmplh3NzayQzCiHM4/hqw== + } + engines: { node: '>=12.20' } + hasBin: true + + /tslib/2.3.1: + resolution: + { + integrity: sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw== + } diff --git a/libraries/rush-lib/src/logic/pnpm/test/yamlFiles/pnpm-lock-v5/versioned-overrides-not-modified.yaml b/libraries/rush-lib/src/logic/pnpm/test/yamlFiles/pnpm-lock-v5/versioned-overrides-not-modified.yaml new file mode 100644 index 00000000000..e178078ab17 --- /dev/null +++ b/libraries/rush-lib/src/logic/pnpm/test/yamlFiles/pnpm-lock-v5/versioned-overrides-not-modified.yaml @@ -0,0 +1,32 @@ +lockfileVersion: 5.3 + +overrides: + typescript@~5: 5.0.4 + +importers: + .: + specifiers: {} + + ../../apps/foo: + specifiers: + tslib: ~2.3.1 + typescript: 5.0.4 + dependencies: + tslib: 2.3.1 + devDependencies: + typescript: 5.0.4 + +packages: + /typescript/5.0.4: + resolution: + { + integrity: sha512-cW9T5W9xY37cc+jfEnaUvX91foxtHkza3Nw3wkoF4sSlKn0MONdkdEndig/qPBWXNkmplh3NzayQzCiHM4/hqw== + } + engines: { node: '>=12.20' } + hasBin: true + + /tslib/2.3.1: + resolution: + { + integrity: sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw== + } diff --git a/libraries/rush-lib/src/logic/pnpm/test/yamlFiles/pnpm-lock-v6/nested-overrides-not-modified.yaml b/libraries/rush-lib/src/logic/pnpm/test/yamlFiles/pnpm-lock-v6/nested-overrides-not-modified.yaml new file mode 100644 index 00000000000..e72742903ef --- /dev/null +++ b/libraries/rush-lib/src/logic/pnpm/test/yamlFiles/pnpm-lock-v6/nested-overrides-not-modified.yaml @@ -0,0 +1,32 @@ +lockfileVersion: '6.0' + +overrides: + foo>typescript: 5.0.4 + +importers: + .: {} + + ../../apps/foo: + dependencies: + tslib: + specifier: ~2.3.1 + version: 2.3.1 + devDependencies: + typescript: + specifier: 5.0.4 + version: 5.0.4 + +packages: + /typescript/5.0.4: + resolution: + { + integrity: sha512-cW9T5W9xY37cc+jfEnaUvX91foxtHkza3Nw3wkoF4sSlKn0MONdkdEndig/qPBWXNkmplh3NzayQzCiHM4/hqw== + } + engines: { node: '>=12.20' } + hasBin: true + + /tslib/2.3.1: + resolution: + { + integrity: sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw== + } diff --git a/libraries/rush-lib/src/logic/pnpm/test/yamlFiles/pnpm-lock-v6/versioned-overrides-not-modified.yaml b/libraries/rush-lib/src/logic/pnpm/test/yamlFiles/pnpm-lock-v6/versioned-overrides-not-modified.yaml new file mode 100644 index 00000000000..98d524112d3 --- /dev/null +++ b/libraries/rush-lib/src/logic/pnpm/test/yamlFiles/pnpm-lock-v6/versioned-overrides-not-modified.yaml @@ -0,0 +1,32 @@ +lockfileVersion: '6.0' + +overrides: + typescript@~5: 5.0.4 + +importers: + .: {} + + ../../apps/foo: + dependencies: + tslib: + specifier: ~2.3.1 + version: 2.3.1 + devDependencies: + typescript: + specifier: 5.0.4 + version: 5.0.4 + +packages: + /typescript/5.0.4: + resolution: + { + integrity: sha512-cW9T5W9xY37cc+jfEnaUvX91foxtHkza3Nw3wkoF4sSlKn0MONdkdEndig/qPBWXNkmplh3NzayQzCiHM4/hqw== + } + engines: { node: '>=12.20' } + hasBin: true + + /tslib/2.3.1: + resolution: + { + integrity: sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw== + } diff --git a/libraries/rush-lib/src/logic/pnpm/test/yamlFiles/pnpm-lock-v9/nested-overrides-not-modified.yaml b/libraries/rush-lib/src/logic/pnpm/test/yamlFiles/pnpm-lock-v9/nested-overrides-not-modified.yaml new file mode 100644 index 00000000000..f72f4940c95 --- /dev/null +++ b/libraries/rush-lib/src/logic/pnpm/test/yamlFiles/pnpm-lock-v9/nested-overrides-not-modified.yaml @@ -0,0 +1,41 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +overrides: + foo>typescript: 5.0.4 + +importers: + .: {} + + ../../apps/foo: + dependencies: + tslib: + specifier: ~2.3.1 + version: 2.3.1 + devDependencies: + typescript: + specifier: 5.0.4 + version: 5.0.4 + +packages: + tslib@2.3.1: + resolution: + { + integrity: sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw== + } + + typescript@5.0.4: + resolution: + { + integrity: sha512-cW9T5W9xY37cc+jfEnaUvX91foxtHkza3Nw3wkoF4sSlKn0MONdkdEndig/qPBWXNkmplh3NzayQzCiHM4/hqw== + } + engines: { node: '>=12.20' } + hasBin: true + +snapshots: + tslib@2.3.1: {} + + typescript@5.0.4: {} diff --git a/libraries/rush-lib/src/logic/pnpm/test/yamlFiles/pnpm-lock-v9/versioned-overrides-not-modified.yaml b/libraries/rush-lib/src/logic/pnpm/test/yamlFiles/pnpm-lock-v9/versioned-overrides-not-modified.yaml new file mode 100644 index 00000000000..4d19edffa07 --- /dev/null +++ b/libraries/rush-lib/src/logic/pnpm/test/yamlFiles/pnpm-lock-v9/versioned-overrides-not-modified.yaml @@ -0,0 +1,41 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +overrides: + typescript@~5: 5.0.4 + +importers: + .: {} + + ../../apps/foo: + dependencies: + tslib: + specifier: ~2.3.1 + version: 2.3.1 + devDependencies: + typescript: + specifier: 5.0.4 + version: 5.0.4 + +packages: + tslib@2.3.1: + resolution: + { + integrity: sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw== + } + + typescript@5.0.4: + resolution: + { + integrity: sha512-cW9T5W9xY37cc+jfEnaUvX91foxtHkza3Nw3wkoF4sSlKn0MONdkdEndig/qPBWXNkmplh3NzayQzCiHM4/hqw== + } + engines: { node: '>=12.20' } + hasBin: true + +snapshots: + tslib@2.3.1: {} + + typescript@5.0.4: {}