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,