-
Notifications
You must be signed in to change notification settings - Fork 680
fix: Remove external filter in PnpmShrinkwrapFile.getIntegrityForImporter #5702
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 3 commits
d62a4b0
95d0931
d265189
b37088f
ee7bdb3
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,10 @@ | ||
| { | ||
| "changes": [ | ||
| { | ||
| "comment": "In PnpmShrinkwrapFile getIntegrityForImporter, remove the external filter and include workspace-local link: dependencies by recursing into their importer entries, so shrinkwrap-deps.json hashes cover the full dependency tree.", | ||
| "type": "patch", | ||
| "packageName": "@microsoft/rush" | ||
| } | ||
| ], | ||
| "packageName": "@microsoft/rush" | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -923,24 +923,46 @@ export class PnpmShrinkwrapFile extends BaseShrinkwrapFile { | |
|
|
||
| const { dependencies, devDependencies, optionalDependencies } = importer; | ||
|
|
||
| const externalFilter: (name: string, version: IPnpmVersionSpecifier) => boolean = ( | ||
| name: string, | ||
| versionSpecifier: IPnpmVersionSpecifier | ||
| ): boolean => { | ||
| const version: string = normalizePnpmVersionSpecifier(versionSpecifier); | ||
| return !version.includes('link:'); | ||
| const processCollection = ( | ||
| collection: Record<string, IPnpmVersionSpecifier>, | ||
| optional: boolean | ||
| ): void => { | ||
| const externalDeps: Record<string, IPnpmVersionSpecifier> = {}; | ||
| for (const [name, versionSpecifier] of Object.entries(collection)) { | ||
| const version: string = normalizePnpmVersionSpecifier(versionSpecifier); | ||
| if (version.startsWith('link:')) { | ||
| // This is a workspace-local dependency; resolve it to an importer key and recurse | ||
| const linkPath: string = version.slice('link:'.length); | ||
| const importerDir: string = path.dirname(importerKey); | ||
| const targetKey: string = Path.convertToSlashes( | ||
| path.normalize(path.join(importerDir, linkPath)) | ||
| ); | ||
| const linkedIntegrities: Map<string, string> | undefined = | ||
| this.getIntegrityForImporter(targetKey); | ||
| if (linkedIntegrities) { | ||
| for (const [dep, integrity] of linkedIntegrities) { | ||
| if (!integrityMap!.has(dep)) { | ||
|
||
| integrityMap!.set(dep, integrity); | ||
| } | ||
| } | ||
| } | ||
| } else { | ||
| externalDeps[name] = versionSpecifier; | ||
| } | ||
| } | ||
| this._addIntegrities(integrityMap!, externalDeps, optional); | ||
| }; | ||
|
|
||
| if (dependencies) { | ||
| this._addIntegrities(integrityMap, dependencies, false, externalFilter); | ||
| processCollection(dependencies, false); | ||
| } | ||
|
|
||
| if (devDependencies) { | ||
| this._addIntegrities(integrityMap, devDependencies, false, externalFilter); | ||
| processCollection(devDependencies, false); | ||
| } | ||
|
|
||
| if (optionalDependencies) { | ||
| this._addIntegrities(integrityMap, optionalDependencies, true, externalFilter); | ||
| processCollection(optionalDependencies, true); | ||
| } | ||
| } | ||
| } | ||
|
|
@@ -1269,14 +1291,9 @@ export class PnpmShrinkwrapFile extends BaseShrinkwrapFile { | |
| private _addIntegrities( | ||
| integrityMap: Map<string, string>, | ||
| collection: Record<string, IPnpmVersionSpecifier>, | ||
| optional: boolean, | ||
| filter?: (name: string, version: IPnpmVersionSpecifier) => boolean | ||
| optional: boolean | ||
| ): void { | ||
| for (const [name, version] of Object.entries(collection)) { | ||
| if (filter && !filter(name, version)) { | ||
| continue; | ||
| } | ||
|
|
||
| const packageId: string = this._getPackageId(name, version); | ||
| if (integrityMap.has(packageId)) { | ||
| // The entry could already have been added as a nested dependency | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -314,6 +314,114 @@ snapshots: | |
| // because the sub-dependency (bar) resolved to different versions | ||
| expect(fooIntegrity1).not.toEqual(fooIntegrity2); | ||
| }); | ||
|
|
||
| it('includes workspace-local link: dependencies by recursing into their importer entries', () => { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @copilot Be sure to cover the following scenarios with test cases:
Note that these may not necessarily be the same dependencies between examples.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Added both scenarios in b37088f:
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @copilot , for clarity, in Scenario 2,
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Updated in ee7bdb3. Scenario 2 now models the correct topology: Also added handling in |
||
| // This test verifies that link: (workspace-local) dependencies are no longer filtered out. | ||
| // The shrinkwrap-deps.json for an importer should include hashes from its workspace | ||
| // dependencies' importer sections, all the way down the tree. | ||
|
|
||
| // Shrinkwrap with a root importer that depends on a workspace-local package (link:) | ||
| // and that local package itself depends on an external package | ||
| const shrinkwrapContent: string = ` | ||
| lockfileVersion: '9.0' | ||
| settings: | ||
| autoInstallPeers: true | ||
| excludeLinksFromLockfile: false | ||
| importers: | ||
| .: | ||
| dependencies: | ||
| my-lib: | ||
| specifier: workspace:* | ||
| version: link:projects/my-lib | ||
| projects/my-lib: | ||
| dependencies: | ||
| lodash: | ||
| specifier: ^4.17.0 | ||
| version: 4.17.21 | ||
| packages: | ||
| lodash@4.17.21: | ||
| resolution: | ||
| integrity: sha512-lodash== | ||
| snapshots: | ||
| lodash@4.17.21: {} | ||
| `; | ||
|
|
||
| const shrinkwrapFile = PnpmShrinkwrapFile.loadFromString(shrinkwrapContent, { | ||
| subspaceHasNoProjects: false | ||
| }); | ||
|
|
||
| PnpmShrinkwrapFile.clearCache(); | ||
|
|
||
| const rootIntegrityMap = shrinkwrapFile.getIntegrityForImporter('.'); | ||
|
|
||
| expect(rootIntegrityMap).toBeDefined(); | ||
|
|
||
| // The root importer's integrity map should include the linked workspace package's importer entry | ||
| expect(rootIntegrityMap!.has('projects/my-lib')).toBe(true); | ||
|
|
||
| // It should also include the transitive external dependency of the linked package | ||
| expect(rootIntegrityMap!.has('lodash@4.17.21')).toBe(true); | ||
|
|
||
| // The integrity map for the workspace package importer itself should also be available | ||
| const libIntegrityMap = shrinkwrapFile.getIntegrityForImporter('projects/my-lib'); | ||
| expect(libIntegrityMap).toBeDefined(); | ||
| expect(libIntegrityMap!.has('projects/my-lib')).toBe(true); | ||
| expect(libIntegrityMap!.has('lodash@4.17.21')).toBe(true); | ||
| }); | ||
|
|
||
| it('produces different hashes when a workspace-local dependency changes', () => { | ||
| // This test verifies that changing the dependencies of a workspace-local package | ||
| // causes the root importer's integrity to differ. | ||
|
|
||
| const baseContent = (lodashVersion: string): string => ` | ||
| lockfileVersion: '9.0' | ||
| settings: | ||
| autoInstallPeers: true | ||
| excludeLinksFromLockfile: false | ||
| importers: | ||
| .: | ||
| dependencies: | ||
| my-lib: | ||
| specifier: workspace:* | ||
| version: link:projects/my-lib | ||
|
||
| projects/my-lib: | ||
| dependencies: | ||
| lodash: | ||
| specifier: ^4.17.0 | ||
| version: ${lodashVersion} | ||
| packages: | ||
| ${lodashVersion === '4.17.21' ? 'lodash@4.17.21' : 'lodash@4.17.20'}: | ||
| resolution: | ||
| integrity: sha512-lodash${lodashVersion === '4.17.21' ? '21' : '20'}== | ||
| snapshots: | ||
| ${lodashVersion === '4.17.21' ? 'lodash@4.17.21' : 'lodash@4.17.20'}: {} | ||
| `; | ||
|
|
||
| const shrinkwrapFile1 = PnpmShrinkwrapFile.loadFromString(baseContent('4.17.21'), { | ||
| subspaceHasNoProjects: false | ||
| }); | ||
| const shrinkwrapFile2 = PnpmShrinkwrapFile.loadFromString(baseContent('4.17.20'), { | ||
| subspaceHasNoProjects: false | ||
| }); | ||
|
|
||
| PnpmShrinkwrapFile.clearCache(); | ||
|
|
||
| const rootIntegrityMap1 = shrinkwrapFile1.getIntegrityForImporter('.'); | ||
| const rootIntegrityMap2 = shrinkwrapFile2.getIntegrityForImporter('.'); | ||
|
|
||
| expect(rootIntegrityMap1).toBeDefined(); | ||
| expect(rootIntegrityMap2).toBeDefined(); | ||
|
|
||
| // The self-hash of '.' does NOT change because the root importer object itself is identical | ||
| // in both cases (it still references link:projects/my-lib). However, the workspace lib's | ||
| // integrity hash should differ because its lodash dependency resolved to a different version. | ||
| const libIntegrity1 = rootIntegrityMap1!.get('projects/my-lib'); | ||
| const libIntegrity2 = rootIntegrityMap2!.get('projects/my-lib'); | ||
|
|
||
| expect(libIntegrity1).toBeDefined(); | ||
| expect(libIntegrity2).toBeDefined(); | ||
| expect(libIntegrity1).not.toEqual(libIntegrity2); | ||
| }); | ||
| }); | ||
|
|
||
| describe('Check is workspace project modified', () => { | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@copilot The lockfile paths will always be valid POSIX paths, so use the
path.posix.dirnameandpath.posix.joinhelpers here rather than normalizing and converting.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed in b37088f. Also corrected the join logic: the
link:path is relative to the project folder (the importer key itself), not its parent, so we now usepath.posix.join(importerKey, linkPath)rather thanpath.posix.join(path.posix.dirname(importerKey), linkPath). BothPath.convertToSlashesandpath.normalizeare removed.