From 06d2d3deb22d29de8f5ff182f011b4e04ebebc08 Mon Sep 17 00:00:00 2001 From: shiminshen Date: Sun, 17 May 2026 22:35:59 +0800 Subject: [PATCH] fix: write packageManagerDependencies to lockfile when devEngines.packageManager is set (#11681) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .../sync-env-lockfile-when-missing-11674.md | 5 ++++ pnpm/src/syncEnvLockfile.test.ts | 19 +++++++++++--- pnpm/src/syncEnvLockfile.ts | 25 ++++++++++--------- 3 files changed, 33 insertions(+), 16 deletions(-) create mode 100644 .changeset/sync-env-lockfile-when-missing-11674.md diff --git a/.changeset/sync-env-lockfile-when-missing-11674.md b/.changeset/sync-env-lockfile-when-missing-11674.md new file mode 100644 index 0000000000..0550a6f3c3 --- /dev/null +++ b/.changeset/sync-env-lockfile-when-missing-11674.md @@ -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). diff --git a/pnpm/src/syncEnvLockfile.test.ts b/pnpm/src/syncEnvLockfile.test.ts index 40eaeb86f4..e2c8705f27 100644 --- a/pnpm/src/syncEnvLockfile.test.ts +++ b/pnpm/src/syncEnvLockfile.test.ts @@ -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 () => { diff --git a/pnpm/src/syncEnvLockfile.ts b/pnpm/src/syncEnvLockfile.ts index 9373f9684d..f347be6e15 100644 --- a/pnpm/src/syncEnvLockfile.ts +++ b/pnpm/src/syncEnvLockfile.ts @@ -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 { 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,