From 7491b48d015ff47f6a7c48206b64f0c0cd192be4 Mon Sep 17 00:00:00 2001 From: Thijs Koerselman Date: Sat, 29 Aug 2026 13:46:16 +0200 Subject: [PATCH 1/4] Prune unused pnpm env lockfile entries --- ...generate-pnpm-lockfile.integration.test.ts | 4 + .../helpers/generate-pnpm-lockfile.test.ts | 94 ++++++++++++ .../helpers/generate-pnpm-lockfile.ts | 140 ++++++++++++++---- 3 files changed, 210 insertions(+), 28 deletions(-) diff --git a/src/lib/lockfile/helpers/generate-pnpm-lockfile.integration.test.ts b/src/lib/lockfile/helpers/generate-pnpm-lockfile.integration.test.ts index 3969784..eddba34 100644 --- a/src/lib/lockfile/helpers/generate-pnpm-lockfile.integration.test.ts +++ b/src/lib/lockfile/helpers/generate-pnpm-lockfile.integration.test.ts @@ -278,6 +278,10 @@ describe("generatePnpmLockfile integration", () => { "my-config": { specifier: "1.0.0", version: "1.0.0" }, }); expect(rootImporter).not.toHaveProperty("packageManagerDependencies"); + expect(documents[0]?.packages).toEqual({ + "my-config@1.0.0": expect.any(Object), + }); + expect(documents[0]?.snapshots).toEqual({}); }); /** diff --git a/src/lib/lockfile/helpers/generate-pnpm-lockfile.test.ts b/src/lib/lockfile/helpers/generate-pnpm-lockfile.test.ts index 3f456b2..c847ec2 100644 --- a/src/lib/lockfile/helpers/generate-pnpm-lockfile.test.ts +++ b/src/lib/lockfile/helpers/generate-pnpm-lockfile.test.ts @@ -605,6 +605,36 @@ describe("generatePnpmLockfile", () => { snapshots: {}, } satisfies EnvLockfile; + const envLockfileWithConfigDependency = { + lockfileVersion: "9.0", + importers: { + ".": { + configDependencies: { + "my-config": { specifier: "1.0.0", version: "1.0.0" }, + }, + packageManagerDependencies: { + pnpm: { specifier: "12.0.0", version: "12.0.0" }, + }, + }, + }, + packages: { + "my-config@1.0.0": { resolution: { integrity: "config" } }, + "pnpm@12.0.0": { resolution: { integrity: "pnpm" } }, + "@pnpm/exe.linux-x64@12.0.0": { + resolution: { integrity: "executable" }, + }, + }, + snapshots: { + "my-config@1.0.0": {}, + "pnpm@12.0.0": { + optionalDependencies: { + "@pnpm/exe.linux-x64": "12.0.0", + }, + }, + "@pnpm/exe.linux-x64@12.0.0": {}, + }, + } satisfies EnvLockfile; + beforeEach(() => { const lockfile = createMockLockfile(); readWantedLockfile_v9.mockResolvedValue(lockfile as never); @@ -668,6 +698,70 @@ describe("generatePnpmLockfile", () => { expect(writeEnvLockfile_v9).not.toHaveBeenCalled(); }); + it("should prune package manager packages when the output omits packageManager", async () => { + readEnvLockfile_v9.mockResolvedValue(envLockfileWithConfigDependency); + + await generate({ name: "my-app", version: "1.0.0" }); + + expect(writeEnvLockfile_v9).toHaveBeenCalledWith( + "/workspace/apps/my-app/isolate", + { + lockfileVersion: "9.0", + importers: { + ".": { + configDependencies: { + "my-config": { specifier: "1.0.0", version: "1.0.0" }, + }, + }, + }, + packages: { + "my-config@1.0.0": { resolution: { integrity: "config" } }, + }, + snapshots: { "my-config@1.0.0": {} }, + }, + ); + }); + + it("should omit config dependencies that a generated Rush workspace does not declare", async () => { + isRushWorkspace.mockReturnValue(true); + readEnvLockfile_v9.mockResolvedValue(envLockfileWithConfigDependency); + + await generate({ + name: "my-app", + version: "1.0.0", + packageManager: "pnpm@12.0.0", + }); + + expect(writeEnvLockfile_v9).toHaveBeenCalledWith( + "/workspace/apps/my-app/isolate", + { + lockfileVersion: "9.0", + importers: { + ".": { + configDependencies: {}, + packageManagerDependencies: { + pnpm: { specifier: "12.0.0", version: "12.0.0" }, + }, + }, + }, + packages: { + "pnpm@12.0.0": { resolution: { integrity: "pnpm" } }, + "@pnpm/exe.linux-x64@12.0.0": { + resolution: { integrity: "executable" }, + }, + }, + snapshots: { + "pnpm@12.0.0": { + optionalDependencies: { + "@pnpm/exe.linux-x64": "12.0.0", + }, + }, + "@pnpm/exe.linux-x64@12.0.0": {}, + }, + }, + ); + }); + it("should skip the env document when the workspace lockfile has none", async () => { readEnvLockfile_v9.mockResolvedValue(null); diff --git a/src/lib/lockfile/helpers/generate-pnpm-lockfile.ts b/src/lib/lockfile/helpers/generate-pnpm-lockfile.ts index 80eb537..e97d2f7 100644 --- a/src/lib/lockfile/helpers/generate-pnpm-lockfile.ts +++ b/src/lib/lockfile/helpers/generate-pnpm-lockfile.ts @@ -14,7 +14,7 @@ import { } from "pnpm_lockfile_file_v9"; import { pruneLockfile as pruneLockfile_v8 } from "pnpm_prune_lockfile_v8"; import { pruneLockfile as pruneLockfile_v9 } from "pnpm_prune_lockfile_v9"; -import { omit, pick } from "remeda"; +import { pick } from "remeda"; import { useLogger } from "#/lib/logger"; import type { PackageManifest, PackagesRegistry, PatchFile } from "#/lib/types"; import { @@ -246,6 +246,7 @@ export async function generatePnpmLockfile({ lockfileRootDir, isolateDir, targetPackageManifest, + isRush, }); } else { await writeWantedLockfile_v8(isolateDir, { @@ -284,10 +285,12 @@ async function copyEnvLockfile({ lockfileRootDir, isolateDir, targetPackageManifest, + isRush, }: { lockfileRootDir: string; isolateDir: string; targetPackageManifest: PackageManifest; + isRush: boolean; }) { const log = useLogger(); @@ -327,58 +330,139 @@ async function copyEnvLockfile({ return; } - if (targetPackageManifest.packageManager) { + if (targetPackageManifest.packageManager && !isRush) { log.debug("Copying the lockfile env document"); await writeEnvLockfile_v9(isolateDir, envLockfile); return; } - const withoutPackageManager = omitEnvPackageManagerDependencies(envLockfile); + const filteredEnvLockfile = filterEnvLockfile(envLockfile, { + keepConfigDependencies: !isRush, + keepPackageManagerDependencies: Boolean( + targetPackageManifest.packageManager, + ), + }); - if (!pinsConfigDependencies(withoutPackageManager)) { + if (!pinsEnvDependencies(filteredEnvLockfile)) { log.debug( - "Skipping the lockfile env document because the output manifest has no packageManager field and the document pins no config dependencies", + "Skipping the lockfile env document because the isolated output declares none of its dependencies", ); return; } - log.debug( - "Copying the lockfile env document without its packageManagerDependencies", - ); + log.debug("Copying the filtered lockfile env document"); - await writeEnvLockfile_v9(isolateDir, withoutPackageManager); + await writeEnvLockfile_v9(isolateDir, filteredEnvLockfile); } /** - * Strip the `packageManagerDependencies` pin from every importer of an env - * document, leaving the rest of it untouched. The `packages` and `snapshots` - * entries the pin resolved to are left in place, because nothing reads them: - * verified by running `pnpm install --frozen-lockfile` (pnpm 12) against - * exactly this output — the lockfile is accepted as up to date, and env - * entries never enter the isolate's virtual store whether an importer - * references them or not. Pruning them would be a reachability walk from the - * remaining `configDependencies` rather than a re-resolution, so it is - * affordable; it is left undone because the residue is some unread YAML. + * Keep only the env dependencies declared by the isolated output, then retain + * their reachable package and snapshot entries. Rush isolates use a generated + * workspace file and therefore declare no config dependencies; ordinary + * isolates keep the copied workspace declarations. The output manifest + * independently determines whether the package manager pin remains. */ -function omitEnvPackageManagerDependencies( +function filterEnvLockfile( envLockfile: EnvLockfile, + { + keepConfigDependencies, + keepPackageManagerDependencies, + }: { + keepConfigDependencies: boolean; + keepPackageManagerDependencies: boolean; + }, ): EnvLockfile { + const importers = Object.fromEntries( + Object.entries(envLockfile.importers).map(([importerId, importer]) => [ + importerId, + { + configDependencies: keepConfigDependencies + ? importer.configDependencies + : {}, + ...(keepPackageManagerDependencies && + importer.packageManagerDependencies + ? { + packageManagerDependencies: importer.packageManagerDependencies, + } + : {}), + }, + ]), + ) as EnvLockfile["importers"]; + + const reachablePackageKeys = collectReachableEnvPackageKeys({ + importers, + snapshots: envLockfile.snapshots, + }); + return { ...envLockfile, - importers: Object.fromEntries( - Object.entries(envLockfile.importers).map(([importerId, importer]) => [ - importerId, - omit(importer, ["packageManagerDependencies"]), - ]), - ) as EnvLockfile["importers"], + importers, + packages: pick(envLockfile.packages, [...reachablePackageKeys]), + snapshots: pick(envLockfile.snapshots, [...reachablePackageKeys]), }; } -/** Whether an env document pins any config dependency */ -function pinsConfigDependencies(envLockfile: EnvLockfile) { +/** Find every env package reachable from the retained importer dependencies. */ +function collectReachableEnvPackageKeys({ + importers, + snapshots, +}: Pick) { + const pending = Object.values(importers).flatMap((importer) => + Object.entries({ + ...importer.configDependencies, + ...importer.packageManagerDependencies, + }).map(([packageName, dependency]) => + envDependencyToPackageKey(packageName, dependency.version), + ), + ); + const reachable = new Set(); + + for (const packageKey of pending) { + if (!packageKey || reachable.has(packageKey)) continue; + + reachable.add(packageKey); + + const snapshot = snapshots[packageKey]; + if (!snapshot) continue; + + for (const [dependencyName, reference] of Object.entries({ + ...snapshot.dependencies, + ...snapshot.optionalDependencies, + })) { + pending.push(envDependencyToPackageKey(dependencyName, reference)); + } + } + + return reachable; +} + +/** Convert a lockfile dependency reference to its package/snapshot key. */ +function envDependencyToPackageKey(packageName: string, reference: string) { + if (reference.startsWith("link:")) return null; + if (reference.startsWith("@")) return reference; + + const atIndex = reference.indexOf("@"); + const colonIndex = reference.indexOf(":"); + const peerIndex = reference.indexOf("("); + + if ( + atIndex !== -1 && + (colonIndex === -1 || atIndex < colonIndex) && + (peerIndex === -1 || atIndex < peerIndex) + ) { + return reference; + } + + return `${packageName}@${reference}`; +} + +/** Whether an env document still pins any dependency. */ +function pinsEnvDependencies(envLockfile: EnvLockfile) { return Object.values(envLockfile.importers).some( - (importer) => Object.keys(importer.configDependencies ?? {}).length > 0, + (importer) => + Object.keys(importer.configDependencies).length > 0 || + Object.keys(importer.packageManagerDependencies ?? {}).length > 0, ); } From 41a372cb2c6f97b05436861efc9036b9399ca75c Mon Sep 17 00:00:00 2001 From: Thijs Koerselman Date: Sat, 29 Aug 2026 13:57:55 +0200 Subject: [PATCH 2/4] Address code-review findings (env graph pruning and coverage) --- package.json | 1 + pnpm-lock.yaml | 29 ++++++++-- .../helpers/generate-pnpm-lockfile.test.ts | 31 +++++++++- .../helpers/generate-pnpm-lockfile.ts | 58 +++++-------------- 4 files changed, 70 insertions(+), 49 deletions(-) diff --git a/package.json b/package.json index 131c5a4..24952b6 100644 --- a/package.json +++ b/package.json @@ -56,6 +56,7 @@ "dependencies": { "@npmcli/arborist": "^9.4.2", "@npmcli/config": "^10.8.1", + "@pnpm/deps.path": "^1101.0.0", "@pnpm/logger": "^1100.0.0", "@pnpm/types": "^1001.3.0", "consola": "^3.4.2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7675334..2f41c33 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -17,6 +17,9 @@ importers: '@npmcli/config': specifier: ^10.8.1 version: 10.9.0 + '@pnpm/deps.path': + specifier: ^1101.0.0 + version: 1101.0.0 '@pnpm/logger': specifier: ^1100.0.0 version: 1100.0.0 @@ -830,6 +833,10 @@ packages: resolution: {integrity: sha512-1KbfwG1D5mKg0g1XacgGlw9NsBpO7HspQHdYbMhN6XvQ+10V3v4G9PQCo5Vlems6qolZKNYC0wv/HKOETuAsQA==} engines: {node: '>=22.13'} + '@pnpm/deps.path@1101.0.0': + resolution: {integrity: sha512-awgi3rf0/Kes/QTVLx/E34rGrg72hNASNDQVp3dE0sQkNv71D3RAQI3K4kaey2VPSvTPx4O/Kn8nMpEUnxVmvA==} + engines: {node: '>=22.13'} + '@pnpm/error@1100.1.2': resolution: {integrity: sha512-HzeZOy38srT5glZqrlBcdfHd13tec3wrLiVRB5vQnj+ScOswQ2DzZzMJpqMh39gss6zhiTl/MskqKzoY/qT9ew==} engines: {node: '>=22.13'} @@ -941,6 +948,10 @@ packages: resolution: {integrity: sha512-lB2aSB2h950UX1GX3JW4AZ86xjXD92YJbV92IBSQqHXXZdiemoVKADLA2cDQ4/wKeS7djY8fkITM6X+6iYF62A==} engines: {node: '>=22.13'} + '@pnpm/types@1102.0.0': + resolution: {integrity: sha512-mgR291P+TGABUzGifQyZyU+EE0lBSTGJ87C5J95Ez2XRFlfcH/9GIN5n2saIgzXQAiyoeFSlEVEiXeEIJy+B3A==} + engines: {node: '>=22.13'} + '@pnpm/types@9.4.2': resolution: {integrity: sha512-g1hcF8Nv4gd76POilz9gD4LITAPXOe5nX4ijgr8ixCbLQZfcpYiMfJ+C1RlMNRUDo8vhlNB4O3bUlxmT6EAQXA==} engines: {node: '>=16.14'} @@ -3301,7 +3312,7 @@ snapshots: lru-cache: 11.3.6 npm-pick-manifest: 11.0.3 proc-log: 6.1.0 - semver: 7.7.4 + semver: 7.8.5 which: 6.0.1 '@npmcli/installed-package-contents@4.0.0': @@ -3523,13 +3534,13 @@ snapshots: '@pnpm/crypto.base32-hash': 2.0.0 '@pnpm/types': 9.4.2 encode-registry: 3.0.1 - semver: 7.7.4 + semver: 7.8.5 '@pnpm/dependency-path@3.0.0': dependencies: '@pnpm/crypto.base32-hash': 3.0.0 '@pnpm/types': 10.0.0 - semver: 7.7.4 + semver: 7.8.5 '@pnpm/deps.path@1100.1.0': dependencies: @@ -3537,6 +3548,12 @@ snapshots: '@pnpm/types': 1101.9.0 semver: 7.8.5 + '@pnpm/deps.path@1101.0.0': + dependencies: + '@pnpm/crypto.hash': 1100.0.2 + '@pnpm/types': 1102.0.0 + semver: 7.8.5 + '@pnpm/error@1100.1.2': dependencies: '@pnpm/constants': 1101.0.0 @@ -3654,7 +3671,7 @@ snapshots: '@pnpm/lockfile-types': 5.1.5 comver-to-semver: 1.0.0 ramda: '@pnpm/ramda@0.28.1' - semver: 7.7.4 + semver: 7.8.5 '@pnpm/network.git-utils@1100.0.3': dependencies: @@ -3699,6 +3716,8 @@ snapshots: '@pnpm/types@1101.9.0': {} + '@pnpm/types@1102.0.0': {} + '@pnpm/types@9.4.2': {} '@pnpm/util.lex-comparator@1.0.0': {} @@ -4832,7 +4851,7 @@ snapshots: graceful-fs: 4.2.11 nopt: 9.0.0 proc-log: 6.1.0 - semver: 7.7.4 + semver: 7.8.5 tar: 7.5.15 tinyglobby: 0.2.16 undici: 6.25.0 diff --git a/src/lib/lockfile/helpers/generate-pnpm-lockfile.test.ts b/src/lib/lockfile/helpers/generate-pnpm-lockfile.test.ts index c847ec2..c59886c 100644 --- a/src/lib/lockfile/helpers/generate-pnpm-lockfile.test.ts +++ b/src/lib/lockfile/helpers/generate-pnpm-lockfile.test.ts @@ -619,19 +619,27 @@ describe("generatePnpmLockfile", () => { }, packages: { "my-config@1.0.0": { resolution: { integrity: "config" } }, + "config-runtime@1.0.0": { resolution: { integrity: "runtime" } }, "pnpm@12.0.0": { resolution: { integrity: "pnpm" } }, "@pnpm/exe.linux-x64@12.0.0": { resolution: { integrity: "executable" }, }, + "unrelated@1.0.0": { resolution: { integrity: "unrelated" } }, }, snapshots: { - "my-config@1.0.0": {}, + "my-config@1.0.0": { + dependencies: { + "config-runtime": "1.0.0(peer@2.0.0)", + }, + }, + "config-runtime@1.0.0(peer@2.0.0)": {}, "pnpm@12.0.0": { optionalDependencies: { "@pnpm/exe.linux-x64": "12.0.0", }, }, "@pnpm/exe.linux-x64@12.0.0": {}, + "unrelated@1.0.0": {}, }, } satisfies EnvLockfile; @@ -716,8 +724,18 @@ describe("generatePnpmLockfile", () => { }, packages: { "my-config@1.0.0": { resolution: { integrity: "config" } }, + "config-runtime@1.0.0": { + resolution: { integrity: "runtime" }, + }, + }, + snapshots: { + "my-config@1.0.0": { + dependencies: { + "config-runtime": "1.0.0(peer@2.0.0)", + }, + }, + "config-runtime@1.0.0(peer@2.0.0)": {}, }, - snapshots: { "my-config@1.0.0": {} }, }, ); }); @@ -762,6 +780,15 @@ describe("generatePnpmLockfile", () => { ); }); + it("should skip the env document for a Rush output without packageManager", async () => { + isRushWorkspace.mockReturnValue(true); + readEnvLockfile_v9.mockResolvedValue(envLockfileWithConfigDependency); + + await generate({ name: "my-app", version: "1.0.0" }); + + expect(writeEnvLockfile_v9).not.toHaveBeenCalled(); + }); + it("should skip the env document when the workspace lockfile has none", async () => { readEnvLockfile_v9.mockResolvedValue(null); diff --git a/src/lib/lockfile/helpers/generate-pnpm-lockfile.ts b/src/lib/lockfile/helpers/generate-pnpm-lockfile.ts index e97d2f7..849247f 100644 --- a/src/lib/lockfile/helpers/generate-pnpm-lockfile.ts +++ b/src/lib/lockfile/helpers/generate-pnpm-lockfile.ts @@ -5,6 +5,7 @@ import { getLockfileImporterId as getLockfileImporterId_v8, writeWantedLockfile as writeWantedLockfile_v8, } from "pnpm_lockfile_file_v8"; +import { refToRelative, removeSuffix } from "@pnpm/deps.path"; import type { EnvLockfile } from "pnpm_lockfile_file_v9"; import { getLockfileImporterId as getLockfileImporterId_v9, @@ -263,23 +264,13 @@ export async function generatePnpmLockfile({ } /** - * Since pnpm 12 the lockfile can be a stream of two YAML documents: an "env" - * document, followed by the project document. The env document pins two - * independent things — the package manager itself - * (`packageManagerDependencies`) and the workspace's config dependencies - * (`configDependencies`) — recording the resolutions and integrity hashes for - * specifiers that live in `pnpm-workspace.yaml`. An isolated output that keeps - * either of those but drops the env document is rejected by - * `pnpm install --frozen-lockfile` with - * ERR_PNPM_FROZEN_LOCKFILE_WITH_OUTDATED_LOCKFILE. - * - * The env document describes the environment rather than the workspace graph, - * so there is nothing to prune from it and it is copied without pruning. The - * one thing that can need dropping is `packageManagerDependencies`: when - * `omitPackageManager` strips the field from the output manifest, pnpm would - * consider that pin stale for the opposite reason. The config dependencies - * still have to survive that, because `pnpm-workspace.yaml` is copied to the - * isolate verbatim and keeps declaring them. + * Since pnpm 12 the lockfile can be a stream of two YAML documents. The leading + * env document pins the package manager and workspace config dependencies, + * including their resolution graphs. An isolated output keeps only the pins + * it still declares: `omitPackageManager` removes the package manager pin, and + * Rush output removes config pins because its workspace file is generated + * without config dependency declarations. The corresponding package and + * snapshot graphs are pruned by reachability before the document is written. */ async function copyEnvLockfile({ lockfileRootDir, @@ -390,21 +381,24 @@ function filterEnvLockfile( ]), ) as EnvLockfile["importers"]; - const reachablePackageKeys = collectReachableEnvPackageKeys({ + const reachableSnapshotKeys = collectReachableEnvSnapshotKeys({ importers, snapshots: envLockfile.snapshots, }); + const reachablePackageKeys = new Set( + [...reachableSnapshotKeys].map((packageKey) => removeSuffix(packageKey)), + ); return { ...envLockfile, importers, packages: pick(envLockfile.packages, [...reachablePackageKeys]), - snapshots: pick(envLockfile.snapshots, [...reachablePackageKeys]), + snapshots: pick(envLockfile.snapshots, [...reachableSnapshotKeys]), }; } /** Find every env package reachable from the retained importer dependencies. */ -function collectReachableEnvPackageKeys({ +function collectReachableEnvSnapshotKeys({ importers, snapshots, }: Pick) { @@ -413,7 +407,7 @@ function collectReachableEnvPackageKeys({ ...importer.configDependencies, ...importer.packageManagerDependencies, }).map(([packageName, dependency]) => - envDependencyToPackageKey(packageName, dependency.version), + refToRelative(dependency.version, packageName), ), ); const reachable = new Set(); @@ -430,33 +424,13 @@ function collectReachableEnvPackageKeys({ ...snapshot.dependencies, ...snapshot.optionalDependencies, })) { - pending.push(envDependencyToPackageKey(dependencyName, reference)); + pending.push(refToRelative(reference, dependencyName)); } } return reachable; } -/** Convert a lockfile dependency reference to its package/snapshot key. */ -function envDependencyToPackageKey(packageName: string, reference: string) { - if (reference.startsWith("link:")) return null; - if (reference.startsWith("@")) return reference; - - const atIndex = reference.indexOf("@"); - const colonIndex = reference.indexOf(":"); - const peerIndex = reference.indexOf("("); - - if ( - atIndex !== -1 && - (colonIndex === -1 || atIndex < colonIndex) && - (peerIndex === -1 || atIndex < peerIndex) - ) { - return reference; - } - - return `${packageName}@${reference}`; -} - /** Whether an env document still pins any dependency. */ function pinsEnvDependencies(envLockfile: EnvLockfile) { return Object.values(envLockfile.importers).some( From 410e8f0659ee204389dce82f7587b40607ad5b7e Mon Sep 17 00:00:00 2001 From: Thijs Koerselman Date: Sat, 29 Aug 2026 14:05:52 +0200 Subject: [PATCH 3/4] Address GLM review findings in env pruning --- .../helpers/generate-pnpm-lockfile.test.ts | 10 +++++ .../helpers/generate-pnpm-lockfile.ts | 45 +++++++++++-------- 2 files changed, 37 insertions(+), 18 deletions(-) diff --git a/src/lib/lockfile/helpers/generate-pnpm-lockfile.test.ts b/src/lib/lockfile/helpers/generate-pnpm-lockfile.test.ts index c59886c..f0bec62 100644 --- a/src/lib/lockfile/helpers/generate-pnpm-lockfile.test.ts +++ b/src/lib/lockfile/helpers/generate-pnpm-lockfile.test.ts @@ -620,6 +620,9 @@ describe("generatePnpmLockfile", () => { packages: { "my-config@1.0.0": { resolution: { integrity: "config" } }, "config-runtime@1.0.0": { resolution: { integrity: "runtime" } }, + "patched-runtime@1.0.0(patch_hash=abc)": { + resolution: { integrity: "patched" }, + }, "pnpm@12.0.0": { resolution: { integrity: "pnpm" } }, "@pnpm/exe.linux-x64@12.0.0": { resolution: { integrity: "executable" }, @@ -630,9 +633,11 @@ describe("generatePnpmLockfile", () => { "my-config@1.0.0": { dependencies: { "config-runtime": "1.0.0(peer@2.0.0)", + "patched-runtime": "1.0.0(patch_hash=abc)", }, }, "config-runtime@1.0.0(peer@2.0.0)": {}, + "patched-runtime@1.0.0(patch_hash=abc)": {}, "pnpm@12.0.0": { optionalDependencies: { "@pnpm/exe.linux-x64": "12.0.0", @@ -727,14 +732,19 @@ describe("generatePnpmLockfile", () => { "config-runtime@1.0.0": { resolution: { integrity: "runtime" }, }, + "patched-runtime@1.0.0(patch_hash=abc)": { + resolution: { integrity: "patched" }, + }, }, snapshots: { "my-config@1.0.0": { dependencies: { "config-runtime": "1.0.0(peer@2.0.0)", + "patched-runtime": "1.0.0(patch_hash=abc)", }, }, "config-runtime@1.0.0(peer@2.0.0)": {}, + "patched-runtime@1.0.0(patch_hash=abc)": {}, }, }, ); diff --git a/src/lib/lockfile/helpers/generate-pnpm-lockfile.ts b/src/lib/lockfile/helpers/generate-pnpm-lockfile.ts index 849247f..5061172 100644 --- a/src/lib/lockfile/helpers/generate-pnpm-lockfile.ts +++ b/src/lib/lockfile/helpers/generate-pnpm-lockfile.ts @@ -5,7 +5,7 @@ import { getLockfileImporterId as getLockfileImporterId_v8, writeWantedLockfile as writeWantedLockfile_v8, } from "pnpm_lockfile_file_v8"; -import { refToRelative, removeSuffix } from "@pnpm/deps.path"; +import { refToRelative, removePeersSuffix } from "@pnpm/deps.path"; import type { EnvLockfile } from "pnpm_lockfile_file_v9"; import { getLockfileImporterId as getLockfileImporterId_v9, @@ -270,7 +270,8 @@ export async function generatePnpmLockfile({ * it still declares: `omitPackageManager` removes the package manager pin, and * Rush output removes config pins because its workspace file is generated * without config dependency declarations. The corresponding package and - * snapshot graphs are pruned by reachability before the document is written. + * snapshot graphs are pruned by reachability when either kind of pin is + * removed. When every pin remains, the original document is copied verbatim. */ async function copyEnvLockfile({ lockfileRootDir, @@ -365,20 +366,26 @@ function filterEnvLockfile( }, ): EnvLockfile { const importers = Object.fromEntries( - Object.entries(envLockfile.importers).map(([importerId, importer]) => [ - importerId, - { - configDependencies: keepConfigDependencies - ? importer.configDependencies - : {}, - ...(keepPackageManagerDependencies && - importer.packageManagerDependencies - ? { - packageManagerDependencies: importer.packageManagerDependencies, - } - : {}), - }, - ]), + Object.entries(envLockfile.importers).map(([importerId, importer]) => { + const { + configDependencies, + packageManagerDependencies, + ...otherImporterFields + } = importer; + + return [ + importerId, + { + ...otherImporterFields, + configDependencies: keepConfigDependencies + ? (configDependencies ?? {}) + : {}, + ...(keepPackageManagerDependencies && packageManagerDependencies + ? { packageManagerDependencies } + : {}), + }, + ]; + }), ) as EnvLockfile["importers"]; const reachableSnapshotKeys = collectReachableEnvSnapshotKeys({ @@ -386,7 +393,9 @@ function filterEnvLockfile( snapshots: envLockfile.snapshots, }); const reachablePackageKeys = new Set( - [...reachableSnapshotKeys].map((packageKey) => removeSuffix(packageKey)), + [...reachableSnapshotKeys].map((packageKey) => + removePeersSuffix(packageKey), + ), ); return { @@ -435,7 +444,7 @@ function collectReachableEnvSnapshotKeys({ function pinsEnvDependencies(envLockfile: EnvLockfile) { return Object.values(envLockfile.importers).some( (importer) => - Object.keys(importer.configDependencies).length > 0 || + Object.keys(importer.configDependencies ?? {}).length > 0 || Object.keys(importer.packageManagerDependencies ?? {}).length > 0, ); } From d9667d5fb5329355b9dae9016ad3194b18c74805 Mon Sep 17 00:00:00 2001 From: Thijs Koerselman Date: Sat, 29 Aug 2026 14:15:09 +0200 Subject: [PATCH 4/4] Use supply-chain eligible pnpm path helpers --- package.json | 2 +- pnpm-lock.yaml | 20 ++------------------ 2 files changed, 3 insertions(+), 19 deletions(-) diff --git a/package.json b/package.json index 24952b6..1fe2ff5 100644 --- a/package.json +++ b/package.json @@ -56,7 +56,7 @@ "dependencies": { "@npmcli/arborist": "^9.4.2", "@npmcli/config": "^10.8.1", - "@pnpm/deps.path": "^1101.0.0", + "@pnpm/deps.path": "^1100.1.0", "@pnpm/logger": "^1100.0.0", "@pnpm/types": "^1001.3.0", "consola": "^3.4.2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2f41c33..15c6b54 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -18,8 +18,8 @@ importers: specifier: ^10.8.1 version: 10.9.0 '@pnpm/deps.path': - specifier: ^1101.0.0 - version: 1101.0.0 + specifier: ^1100.1.0 + version: 1100.1.0 '@pnpm/logger': specifier: ^1100.0.0 version: 1100.0.0 @@ -833,10 +833,6 @@ packages: resolution: {integrity: sha512-1KbfwG1D5mKg0g1XacgGlw9NsBpO7HspQHdYbMhN6XvQ+10V3v4G9PQCo5Vlems6qolZKNYC0wv/HKOETuAsQA==} engines: {node: '>=22.13'} - '@pnpm/deps.path@1101.0.0': - resolution: {integrity: sha512-awgi3rf0/Kes/QTVLx/E34rGrg72hNASNDQVp3dE0sQkNv71D3RAQI3K4kaey2VPSvTPx4O/Kn8nMpEUnxVmvA==} - engines: {node: '>=22.13'} - '@pnpm/error@1100.1.2': resolution: {integrity: sha512-HzeZOy38srT5glZqrlBcdfHd13tec3wrLiVRB5vQnj+ScOswQ2DzZzMJpqMh39gss6zhiTl/MskqKzoY/qT9ew==} engines: {node: '>=22.13'} @@ -948,10 +944,6 @@ packages: resolution: {integrity: sha512-lB2aSB2h950UX1GX3JW4AZ86xjXD92YJbV92IBSQqHXXZdiemoVKADLA2cDQ4/wKeS7djY8fkITM6X+6iYF62A==} engines: {node: '>=22.13'} - '@pnpm/types@1102.0.0': - resolution: {integrity: sha512-mgR291P+TGABUzGifQyZyU+EE0lBSTGJ87C5J95Ez2XRFlfcH/9GIN5n2saIgzXQAiyoeFSlEVEiXeEIJy+B3A==} - engines: {node: '>=22.13'} - '@pnpm/types@9.4.2': resolution: {integrity: sha512-g1hcF8Nv4gd76POilz9gD4LITAPXOe5nX4ijgr8ixCbLQZfcpYiMfJ+C1RlMNRUDo8vhlNB4O3bUlxmT6EAQXA==} engines: {node: '>=16.14'} @@ -3548,12 +3540,6 @@ snapshots: '@pnpm/types': 1101.9.0 semver: 7.8.5 - '@pnpm/deps.path@1101.0.0': - dependencies: - '@pnpm/crypto.hash': 1100.0.2 - '@pnpm/types': 1102.0.0 - semver: 7.8.5 - '@pnpm/error@1100.1.2': dependencies: '@pnpm/constants': 1101.0.0 @@ -3716,8 +3702,6 @@ snapshots: '@pnpm/types@1101.9.0': {} - '@pnpm/types@1102.0.0': {} - '@pnpm/types@9.4.2': {} '@pnpm/util.lex-comparator@1.0.0': {}