fix(engine-pm): record a satisfied range pin in the lockfile (#13932)

* fix(engine-pm): record a satisfied range pin in the lockfile

The comment in pre_command says a pin the running pnpm already satisfies
still has to reach the lockfile — the switch would have written it on the
way to the wanted version — but package_manager_to_sync could only produce
a version for an exact pin (or a source checkout), so a range pin like
`^12.0.0-rc.6` written by hand, or by any tool other than `pnpm add` /
`pnpm self-update`, never landed in packageManagerDependencies and the
project went without the shared resolution the pin exists to provide.

When the running pnpm satisfies the range, its version is the one the
project actually uses, so that is what gets recorded. An exact pin still
records itself, and a source checkout still answers first.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* docs: keep the range-pin comments to what the code cannot say

The comment recorded how the code used to behave and the test's doc
repeated its own name. What is left is the part neither the name nor the
body carries: why the range is built from PNPM_VERSION.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Zoltan KochanandClaude Opus 5 authored and GitHub committed 2026-08-27 18:58:13 +02:00
1 parent d278429ba9
commit 12ea6ca0bb
3 files changed
+54 -4

No files matched your search

+5
View File
@@ -0,0 +1,5 @@
---
"pacquet": patch
---
A `devEngines.packageManager` range pin on pnpm is now recorded in `pnpm-lock.yaml`'s `packageManagerDependencies` when the running pnpm already satisfies it, using the running version. Previously only an exact pin — or a range resolved on the way through a version switch — reached the lockfile, so a range pin written by hand (or by any tool other than `pnpm add` / `pnpm self-update`) left the project without the shared resolution the pin exists to provide.
@@ -1,5 +1,5 @@
use miette::IntoDiagnostic;
use pnpm_config::PmOnFail;
use pnpm_config::{PNPM_VERSION, PmOnFail};
use pnpm_env_installer::is_package_manager_resolved;
use pnpm_lockfile::EnvLockfile;
use pnpm_package_manifest::{
@@ -57,9 +57,17 @@ pub(crate) fn package_manager_to_sync(
{
return Some(PackageManagerToSync { specifier: wanted_version.to_string(), version });
}
exact_version(wanted_version)
.filter(|version| version_satisfies(version, wanted_version))
.map(|version| PackageManagerToSync { specifier: wanted_version.to_string(), version })
if let Some(version) =
exact_version(wanted_version).filter(|version| version_satisfies(version, wanted_version))
{
return Some(PackageManagerToSync { specifier: wanted_version.to_string(), version });
}
// A range pin names no exact version, so the running pnpm's version is
// the one the project actually uses.
version_satisfies(PNPM_VERSION, wanted_version).then(|| PackageManagerToSync {
specifier: wanted_version.to_string(),
version: PNPM_VERSION.to_string(),
})
}
/// Whether the pnpm version the manifest at `root_dir` pins still has to be
+37
View File
@@ -725,6 +725,43 @@ fn package_manager_to_sync_preserves_dev_engine_specifier() {
);
}
/// The range is built from `PNPM_VERSION` so the source checkout's version
/// (a different major) can never satisfy it and answer first.
#[test]
fn package_manager_to_sync_records_the_running_version_for_a_satisfied_range_pin() {
let root = TempDir::new().expect("tmp dir");
let manifest_path = root.path().join("package.json");
let range = format!("^{}", pnpm_config::PNPM_VERSION);
std::fs::write(
&manifest_path,
format!(
r#"{{"devEngines":{{"packageManager":{{"name":"pnpm","version":"{range}","onFail":"download"}}}}}}"#,
),
)
.expect("write manifest");
let manifest = read_manifest_json(&manifest_path).expect("read manifest").expect("manifest");
let package_manager =
package_manager_to_sync(&manifest, root.path(), None).expect("sync package manager");
assert_eq!(package_manager.specifier, range);
assert_eq!(package_manager.version, pnpm_config::PNPM_VERSION);
}
#[test]
fn package_manager_to_sync_records_nothing_for_a_pin_nothing_satisfies() {
let root = TempDir::new().expect("tmp dir");
let manifest_path = root.path().join("package.json");
std::fs::write(
&manifest_path,
r#"{"devEngines":{"packageManager":{"name":"pnpm","version":"^999.0.0","onFail":"download"}}}"#,
)
.expect("write manifest");
let manifest = read_manifest_json(&manifest_path).expect("read manifest").expect("manifest");
assert_eq!(package_manager_to_sync(&manifest, root.path(), None), None);
}
#[test]
fn resolve_bool_override_tri_state() {
// force_on wins, force_off wins over a config `true`, and an unset