Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"changes": [
{
"packageName": "@microsoft/rush",
"comment": "Handle versioned selectors in `globalOverrides` during pnpm shrinkwrap comparison.",
"type": "patch"
}
],
"packageName": "@microsoft/rush"
}
59 changes: 56 additions & 3 deletions libraries/rush-lib/src/logic/pnpm/PnpmShrinkwrapFile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down Expand Up @@ -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];
Expand All @@ -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 (
Expand Down Expand Up @@ -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<string, string> {
const integrities: Map<string, Map<string, string>> = this._integrities;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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(
Expand Down
Original file line number Diff line number Diff line change
@@ -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==
}
Original file line number Diff line number Diff line change
@@ -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==
}
Original file line number Diff line number Diff line change
@@ -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==
}
Original file line number Diff line number Diff line change
@@ -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==
}
Original file line number Diff line number Diff line change
@@ -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:
[email protected]:
resolution:
{
integrity: sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==
}

[email protected]:
resolution:
{
integrity: sha512-cW9T5W9xY37cc+jfEnaUvX91foxtHkza3Nw3wkoF4sSlKn0MONdkdEndig/qPBWXNkmplh3NzayQzCiHM4/hqw==
}
engines: { node: '>=12.20' }
hasBin: true

snapshots:
[email protected]: {}

[email protected]: {}
Loading