fix(env-installer): prune env lockfile when updating a config dep (#11892)

`pnpm add --config <pkg>` (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`.
This commit is contained in:
Zoltan Kochan
2026-05-24 01:49:33 +02:00
committed by GitHub
parent 3209c2510c
commit 155af87585
3 changed files with 50 additions and 1 deletions

View File

@@ -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).

View File

@@ -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,

View File

@@ -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()