diff --git a/packages/nx/src/plugins/js/lock-file/pnpm-parser.spec.ts b/packages/nx/src/plugins/js/lock-file/pnpm-parser.spec.ts index 0aade0d299bc06..fe6ea9ae5eb774 100644 --- a/packages/nx/src/plugins/js/lock-file/pnpm-parser.spec.ts +++ b/packages/nx/src/plugins/js/lock-file/pnpm-parser.spec.ts @@ -1827,4 +1827,160 @@ snapshots: ).not.toThrow(); }); }); + + describe('patched dependencies', () => { + beforeEach(() => { + const fileSys = { + 'node_modules/.modules.yaml': `hoistedDependencies: {}`, + 'node_modules/vitest/package.json': '{"version": "3.2.4"}', + 'node_modules/lodash/package.json': '{"version": "4.17.21"}', + }; + vol.fromJSON(fileSys, '/root'); + }); + + it('should include patch hash in external node hash (v9)', () => { + const lockFile = `lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +patchedDependencies: + vitest: + hash: dde3c7a634692aefe0877c763192102a73d3451e6d12dae61df88fd1e6e9368e + path: patches/vitest.patch + +importers: + + .: + dependencies: + lodash: + specifier: ^4.17.21 + version: 4.17.21 + vitest: + specifier: 3.2.4 + version: 3.2.4 + +packages: + + lodash@4.17.21: + resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} + + vitest@3.2.4: + resolution: {integrity: sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==} + engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} + hasBin: true + +snapshots: + + lodash@4.17.21: {} + + vitest@3.2.4: {}`; + + const lockFileHash = 'test-lockfile-hash'; + + const { nodes: externalNodes } = getPnpmLockfileNodes( + lockFile, + lockFileHash + ); + + // Lodash should have only the integrity hash + expect(externalNodes['npm:lodash']).toMatchObject({ + type: 'npm', + name: 'npm:lodash', + data: { + version: '4.17.21', + packageName: 'lodash', + hash: 'sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==', + }, + }); + + // Vitest should have integrity + patch hash combined + expect(externalNodes['npm:vitest']).toMatchObject({ + type: 'npm', + name: 'npm:vitest', + data: { + version: '3.2.4', + packageName: 'vitest', + // This is hashArray([integrity, patchHash]) + hash: 'sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==|dde3c7a634692aefe0877c763192102a73d3451e6d12dae61df88fd1e6e9368e', + }, + }); + }); + + it('should detect patch hash changes', () => { + const lockFileWithPatch = `lockfileVersion: '9.0' + +patchedDependencies: + vitest: + hash: patch123 + path: patches/vitest.patch + +importers: + + .: + dependencies: + vitest: + specifier: 3.2.4 + version: 3.2.4 + +packages: + + vitest@3.2.4: + resolution: {integrity: sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==} + +snapshots: + + vitest@3.2.4: {}`; + + const lockFileWithModifiedPatch = `lockfileVersion: '9.0' + +patchedDependencies: + vitest: + hash: patch456 + path: patches/vitest.patch + +importers: + + .: + dependencies: + vitest: + specifier: 3.2.4 + version: 3.2.4 + +packages: + + vitest@3.2.4: + resolution: {integrity: sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==} + +snapshots: + + vitest@3.2.4: {}`; + + const { nodes: externalNodes1 } = getPnpmLockfileNodes( + lockFileWithPatch, + 'test-lockfile-hash-1' + ); + + const { nodes: externalNodes2 } = getPnpmLockfileNodes( + lockFileWithModifiedPatch, + 'test-lockfile-hash-2' + ); + + // Hashes should be different when patch changes + expect(externalNodes1['npm:vitest'].data.hash).not.toBe( + externalNodes2['npm:vitest'].data.hash + ); + + // First has integrity + patch123 + expect(externalNodes1['npm:vitest'].data.hash).toBe( + 'sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==|patch123' + ); + + // Second has integrity + patch456 + expect(externalNodes2['npm:vitest'].data.hash).toBe( + 'sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==|patch456' + ); + }); + }); }); diff --git a/packages/nx/src/plugins/js/lock-file/pnpm-parser.ts b/packages/nx/src/plugins/js/lock-file/pnpm-parser.ts index b4155eeecbce49..760c92f9ee46c8 100644 --- a/packages/nx/src/plugins/js/lock-file/pnpm-parser.ts +++ b/packages/nx/src/plugins/js/lock-file/pnpm-parser.ts @@ -114,13 +114,19 @@ function matchedDependencyName( ); } -function createHashFromSnapshot(snapshot: PackageSnapshot) { - return ( +function createHashFromSnapshot(snapshot: PackageSnapshot, patchHash?: string) { + const baseHash = snapshot.resolution?.['integrity'] || (snapshot.resolution?.['tarball'] ? hashArray([snapshot.resolution['tarball']]) - : undefined) - ); + : undefined); + + // If there's a patch hash, combine it with the base hash + if (patchHash && baseHash) { + return hashArray([baseHash, patchHash]); + } + + return baseHash; } function isAliasVersion(depVersion: string) { @@ -137,6 +143,18 @@ function getNodes( const keyMap = new Map>(); const nodes: Map> = new Map(); + // Extract patch hashes from patchedDependencies section + const patchHashes = new Map(); + if (data.patchedDependencies) { + for (const [pkgName, patchInfo] of Object.entries( + data.patchedDependencies + )) { + if (patchInfo && typeof patchInfo === 'object' && 'hash' in patchInfo) { + patchHashes.set(pkgName, patchInfo.hash); + } + } + } + const maybeAliasedPackageVersions = new Map(); // if (data.importers['.'].optionalDependencies) { @@ -179,7 +197,11 @@ function getNodes( if (!originalPackageName) { continue; } - const hash = createHashFromSnapshot(snapshot); + + // Compute hash once, including patch hash if available + const patchHash = patchHashes.get(originalPackageName); + const hash = createHashFromSnapshot(snapshot, patchHash); + // snapshot already has a name if (snapshot.name) { packageNameObj = { @@ -210,7 +232,7 @@ function getNodes( packageNameObj = { key, packageName: rootDependencyName, - hash: createHashFromSnapshot(snapshot), + hash, }; } @@ -218,7 +240,7 @@ function getNodes( packageNameObj = { key, packageName: originalPackageName, - hash: createHashFromSnapshot(snapshot), + hash, }; }