fix: write packageManagerDependencies to lockfile when devEngines.packageManager is set (#11681)

When `devEngines.packageManager.pnpm` is set without `onFail: "download"`,
`pnpm install` ran `syncEnvLockfile` instead of `switchCliVersion`. That sync
returned early whenever the env lockfile did not already record a
`packageManagerDependencies.pnpm` entry, so the resolved pnpm version was
never recorded on first install — contradicting the documented behavior
("The resolved version is stored in pnpm-lock.yaml") and forcing users to
add `onFail: "download"` purely to trigger the lockfile write.

Drop the two early-returns that only fired when the env lockfile was
missing or empty. The resolution proceeds whenever (a) the project pins a
pnpm version via `devEngines.packageManager` (or a v12+ `packageManager`
field) and (b) the running pnpm satisfies that pin. The existing
"already-resolved" no-op path still skips work when the lockfile already
records a satisfying version, so steady-state installs don't churn.

Closes #11674 (part 1). Part 3 (pruning `@pnpm/exe` platform entries when
`onFail: "download"` is removed) is left for a follow-up — it needs a
state-transition signal the codebase doesn't yet track.

Co-authored-by: Damon <damon@deeplearning.ai>
This commit is contained in:
shiminshen
2026-05-17 22:35:59 +08:00
committed by GitHub
parent 963861cac1
commit 06d2d3deb2
3 changed files with 33 additions and 16 deletions

View File

@@ -0,0 +1,5 @@
---
"pnpm": patch
---
Fix `devEngines.packageManager` not writing `packageManagerDependencies` to `pnpm-lock.yaml` when the lockfile lacks an env-doc entry. Previously the lockfile sync skipped resolution unless an existing `packageManagerDependencies.pnpm` entry needed refreshing, so a fresh install without `onFail: "download"` left the resolved pnpm version unrecorded — contradicting the documented behavior that the resolved version is stored in `pnpm-lock.yaml` [#11674](https://github.com/pnpm/pnpm/issues/11674).

View File

@@ -84,21 +84,32 @@ test('no-op when running pnpm does not satisfy wanted range', async () => {
expect(resolvePackageManagerIntegrities).not.toHaveBeenCalled()
})
test('no-op when no env lockfile exists', async () => {
test('writes packageManagerDependencies when no env lockfile exists yet (#11674)', async () => {
const dir = tempDir()
await syncEnvLockfile(baseConfig, makeContext(dir, {
wantedPackageManager: { name: 'pnpm', version: packageManager.version, fromDevEngines: true },
}))
expect(resolvePackageManagerIntegrities).not.toHaveBeenCalled()
expect(resolvePackageManagerIntegrities).toHaveBeenCalledTimes(1)
const updated = await readEnvLockfile(dir)
expect(updated).not.toBeNull()
expect(updated!.importers['.'].packageManagerDependencies?.['pnpm']).toEqual({
specifier: packageManager.version,
version: packageManager.version,
})
})
test('no-op when lockfile has no packageManagerDependencies for pnpm', async () => {
test('writes packageManagerDependencies when env lockfile exists but lacks pnpm entry (#11674)', async () => {
const dir = tempDir()
writeEnvLockfileWithoutPmDeps(dir)
await syncEnvLockfile(baseConfig, makeContext(dir, {
wantedPackageManager: { name: 'pnpm', version: packageManager.version, fromDevEngines: true },
}))
expect(resolvePackageManagerIntegrities).not.toHaveBeenCalled()
expect(resolvePackageManagerIntegrities).toHaveBeenCalledTimes(1)
const updated = await readEnvLockfile(dir)
expect(updated!.importers['.'].packageManagerDependencies?.['pnpm']).toEqual({
specifier: packageManager.version,
version: packageManager.version,
})
})
test('no-op when lockfile already records a satisfying version', async () => {

View File

@@ -8,14 +8,17 @@ import semver from 'semver'
import { shouldPersistLockfile } from './shouldPersistLockfile.js'
/**
* Refreshes the env lockfile's `packageManagerDependencies` entry when it
* records a pnpm version that no longer satisfies the wanted
* `devEngines.packageManager` range. The currently running pnpm version
* (already verified to satisfy the wanted range by checkPackageManager) is
* recorded as the new resolution.
* Records the currently running pnpm version in the env lockfile's
* `packageManagerDependencies` entry when the project opts in to
* lockfile-pinned versioning (via `devEngines.packageManager`, or a v12+
* `packageManager` pin) and the lockfile doesn't already record a version
* that satisfies the wanted range.
*
* No-op when the project does not pin a pnpm version, when no env lockfile
* exists yet, or when the recorded version still satisfies the wanted range.
* The currently running pnpm version has already been verified by
* checkPackageManager to satisfy the wanted range, so recording it is safe.
*
* No-op when the project does not pin a pnpm version or when the recorded
* version still satisfies the wanted range.
*/
export async function syncEnvLockfile (config: Config, context: ConfigContext): Promise<void> {
const pm = context.wantedPackageManager
@@ -27,15 +30,13 @@ export async function syncEnvLockfile (config: Config, context: ConfigContext):
if (!semver.satisfies(packageManager.version, pm.version, { includePrerelease: true })) return
const envLockfile = await readEnvLockfile(context.rootProjectManifestDir)
if (envLockfile == null) return
const lockedVersion = envLockfile.importers['.'].packageManagerDependencies?.['pnpm']?.version
if (lockedVersion == null) return
if (semver.satisfies(lockedVersion, pm.version, { includePrerelease: true })) return
const lockedVersion = envLockfile?.importers['.'].packageManagerDependencies?.['pnpm']?.version
if (lockedVersion != null && semver.satisfies(lockedVersion, pm.version, { includePrerelease: true })) return
const store = await createStoreController({ ...config, ...context })
try {
await resolvePackageManagerIntegrities(packageManager.version, {
envLockfile,
envLockfile: envLockfile ?? undefined,
registries: config.registries,
rootDir: context.rootProjectManifestDir,
storeController: store.ctrl,