diff --git a/.changeset/stale-bars-tan.md b/.changeset/stale-bars-tan.md new file mode 100644 index 0000000000..5a8afca6ce --- /dev/null +++ b/.changeset/stale-bars-tan.md @@ -0,0 +1,6 @@ +--- +"@pnpm/dependency-path": patch +"@pnpm/calc-dep-state": patch +--- + +Fix dependency graph hash calculation for runtime dependencies (like Node.js, Deno). diff --git a/packages/calc-dep-state/src/index.ts b/packages/calc-dep-state/src/index.ts index d9c790331e..f2b247a908 100644 --- a/packages/calc-dep-state/src/index.ts +++ b/packages/calc-dep-state/src/index.ts @@ -48,7 +48,15 @@ function calcDepGraphHash ( if (cache[depPath]) return cache[depPath] const node = depsGraph[depPath] if (!node) return '' - node.fullPkgId ??= createFullPkgId(node.pkgIdWithPatchHash!, node.resolution!) + if (!node.fullPkgId) { + if (!node.pkgIdWithPatchHash) { + throw new Error(`pkgIdWithPatchHash is not defined for ${depPath} in depsGraph`) + } + if (!node.resolution) { + throw new Error(`resolution is not defined for ${depPath} in depsGraph`) + } + node.fullPkgId = createFullPkgId(node.pkgIdWithPatchHash, node.resolution) + } const deps: Record = {} if (Object.keys(node.children).length && !parents.has(node.fullPkgId)) { const nextParents = new Set([...Array.from(parents), node.fullPkgId]) @@ -135,6 +143,6 @@ function lockfileDepsToGraphChildren (deps: Record): Record import { depPathToFilename, + getPkgIdWithPatchHash, isAbsolute, parse, refToRelative, @@ -109,3 +110,32 @@ test('tryGetPackageId', () => { expect(tryGetPackageId('/@(-.-)/foo@1.0.0(@types/babel__core@7.1.14)' as DepPath)).toEqual('/@(-.-)/foo@1.0.0') expect(tryGetPackageId('foo@1.0.0(patch_hash=xxxx)(@types/babel__core@7.1.14)' as DepPath)).toEqual('foo@1.0.0') }) + +test('getPkgIdWithPatchHash', () => { + // Runtime dependency + expect(getPkgIdWithPatchHash('node@runtime:24.11.1' as DepPath)).toBe('node@runtime:24.11.1') + + // Regular packages + expect(getPkgIdWithPatchHash('foo@1.0.0' as DepPath)).toBe('foo@1.0.0') + + // Packages with patch hash + expect(getPkgIdWithPatchHash('foo@1.0.0(patch_hash=xxxx)' as DepPath)).toBe('foo@1.0.0(patch_hash=xxxx)') + + // Packages with peer dependencies (should remove peer dependencies) + expect(getPkgIdWithPatchHash('foo@1.0.0(@types/babel__core@7.1.14)' as DepPath)).toBe('foo@1.0.0') + + // Packages with both patch hash and peer dependencies (should keep patch hash, remove peer dependencies) + expect(getPkgIdWithPatchHash('foo@1.0.0(patch_hash=xxxx)(@types/babel__core@7.1.14)' as DepPath)).toBe('foo@1.0.0(patch_hash=xxxx)') + + // Scoped packages + expect(getPkgIdWithPatchHash('@foo/bar@1.0.0' as DepPath)).toBe('@foo/bar@1.0.0') + + // Scoped packages with patch hash + expect(getPkgIdWithPatchHash('@foo/bar@1.0.0(patch_hash=yyyy)' as DepPath)).toBe('@foo/bar@1.0.0(patch_hash=yyyy)') + + // Scoped packages with peer dependencies + expect(getPkgIdWithPatchHash('@foo/bar@1.0.0(@types/node@18.0.0)' as DepPath)).toBe('@foo/bar@1.0.0') + + // Scoped packages with both patch hash and peer dependencies + expect(getPkgIdWithPatchHash('@foo/bar@1.0.0(patch_hash=zzzz)(@types/node@18.0.0)' as DepPath)).toBe('@foo/bar@1.0.0(patch_hash=zzzz)') +})