diff --git a/.changeset/sync-satisfied-range-pin.md b/.changeset/sync-satisfied-range-pin.md new file mode 100644 index 0000000000..baa11fdda1 --- /dev/null +++ b/.changeset/sync-satisfied-range-pin.md @@ -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. diff --git a/pnpm/crates/cli/src/cli_args/package_manager.rs b/pnpm/crates/cli/src/cli_args/package_manager.rs index bdaf980c39..30073c170f 100644 --- a/pnpm/crates/cli/src/cli_args/package_manager.rs +++ b/pnpm/crates/cli/src/cli_args/package_manager.rs @@ -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 diff --git a/pnpm/crates/cli/src/cli_args/tests.rs b/pnpm/crates/cli/src/cli_args/tests.rs index c1945a83f4..fd4c22e325 100644 --- a/pnpm/crates/cli/src/cli_args/tests.rs +++ b/pnpm/crates/cli/src/cli_args/tests.rs @@ -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