From 155af875855ef9e1fad92c5170757cfd69bcbf92 Mon Sep 17 00:00:00 2001 From: Zoltan Kochan Date: Sun, 24 May 2026 01:49:33 +0200 Subject: [PATCH] fix(env-installer): prune env lockfile when updating a config dep (#11892) `pnpm add --config ` (via `resolveConfigDeps`) wrote the env lockfile without pruning, so optional subdependencies from the previously resolved version remained as orphans. Mirror the prune call from `resolveAndInstallConfigDeps`. --- ...prune-env-lockfile-on-config-dep-update.md | 6 +++ .../env-installer/src/resolveConfigDeps.ts | 3 ++ .../test/resolveConfigDeps.test.ts | 42 ++++++++++++++++++- 3 files changed, 50 insertions(+), 1 deletion(-) create mode 100644 .changeset/prune-env-lockfile-on-config-dep-update.md diff --git a/.changeset/prune-env-lockfile-on-config-dep-update.md b/.changeset/prune-env-lockfile-on-config-dep-update.md new file mode 100644 index 0000000000..5044629b5f --- /dev/null +++ b/.changeset/prune-env-lockfile-on-config-dep-update.md @@ -0,0 +1,6 @@ +--- +"@pnpm/installing.env-installer": patch +"pnpm": patch +--- + +Fixed `pnpm add --config` leaving orphan entries in `pnpm-lock.env.yaml` (the optional subdependencies of the previously resolved version of the updated config dependency). diff --git a/installing/env-installer/src/resolveConfigDeps.ts b/installing/env-installer/src/resolveConfigDeps.ts index 12df8111c6..5421dadef9 100644 --- a/installing/env-installer/src/resolveConfigDeps.ts +++ b/installing/env-installer/src/resolveConfigDeps.ts @@ -15,6 +15,7 @@ import { parseWantedDependency } from '@pnpm/resolving.parse-wanted-dependency' import type { ConfigDependencies, ConfigDependencySpecifiers, RegistryConfig } from '@pnpm/types' import { installConfigDeps, type InstallConfigDepsOpts } from './installConfigDeps.js' +import { pruneEnvLockfile } from './pruneEnvLockfile.js' import { resolveOptionalSubdeps } from './resolveOptionalSubdeps.js' export type ResolveConfigDepsOpts = CreateFetchFromRegistryOptions & ResolverFactoryOptions & InstallConfigDepsOpts & { @@ -78,6 +79,8 @@ export async function resolveConfigDeps (configDeps: string[], opts: ResolveConf envLockfile.snapshots[pkgKey] = optionalSubdeps ? { optionalDependencies: optionalSubdeps } : {} })) + pruneEnvLockfile(envLockfile) + await Promise.all([ writeSettings({ ...opts, diff --git a/installing/env-installer/test/resolveConfigDeps.test.ts b/installing/env-installer/test/resolveConfigDeps.test.ts index 44f33c8958..9165398e62 100644 --- a/installing/env-installer/test/resolveConfigDeps.test.ts +++ b/installing/env-installer/test/resolveConfigDeps.test.ts @@ -2,7 +2,7 @@ import path from 'node:path' import { expect, test } from '@jest/globals' import { resolveConfigDeps } from '@pnpm/installing.env-installer' -import { readEnvLockfile } from '@pnpm/lockfile.fs' +import { readEnvLockfile, writeEnvLockfile } from '@pnpm/lockfile.fs' import { prepareEmpty } from '@pnpm/prepare' import { getIntegrity, REGISTRY_MOCK_PORT } from '@pnpm/registry-mock' import { createTempStore } from '@pnpm/testing.temp-store' @@ -140,6 +140,46 @@ test('rejects an optionalDependency declared with a non-exact version', async () })).rejects.toThrow(/only exact versions are supported/) }) +test('orphan optional subdeps from a previous resolution are pruned', async () => { + prepareEmpty() + const { storeController, storeDir } = createTempStore() + + // Simulate a prior resolution that left optional subdeps for a now-removed + // version of a config dependency. The stale `foo@99.0.0` and its optional + // subdep `bar@1.0.0` are not referenced from any current configDependency. + await writeEnvLockfile(process.cwd(), { + lockfileVersion: '9.0', + importers: { + '.': { configDependencies: {} }, + }, + packages: { + '@pnpm.e2e/foo@99.0.0': { resolution: { integrity: 'sha512-stale==' } }, + '@pnpm.e2e/bar@1.0.0': { resolution: { integrity: 'sha512-stale==' } }, + }, + snapshots: { + '@pnpm.e2e/foo@99.0.0': { optionalDependencies: { '@pnpm.e2e/bar': '1.0.0' } }, + '@pnpm.e2e/bar@1.0.0': { optional: true }, + }, + }) + + await resolveConfigDeps(['@pnpm.e2e/foo@100.0.0'], { + registries: { + default: registry, + }, + rootDir: process.cwd(), + cacheDir: path.resolve('cache'), + store: storeController, + storeDir, + }) + + const envLockfile = await readEnvLockfile(process.cwd()) + expect(envLockfile!.packages['@pnpm.e2e/foo@99.0.0']).toBeUndefined() + expect(envLockfile!.packages['@pnpm.e2e/bar@1.0.0']).toBeUndefined() + expect(envLockfile!.snapshots['@pnpm.e2e/foo@99.0.0']).toBeUndefined() + expect(envLockfile!.snapshots['@pnpm.e2e/bar@1.0.0']).toBeUndefined() + expect(envLockfile!.packages['@pnpm.e2e/foo@100.0.0']).toBeDefined() +}) + test('fails with frozenLockfile', async () => { prepareEmpty() const { storeController, storeDir } = createTempStore()