From d79eeee085faa64d148307e1e5af0d900258d5bb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ma=C3=ABl=20Nison?= Date: Tue, 19 May 2026 21:27:49 +0200 Subject: [PATCH 1/5] Implements "switchVersionRequirement" --- Cargo.lock | 1 + packages/zpm-config/schema.json | 4 ++ packages/zpm-config/src/lib.rs | 3 + packages/zpm-switch/Cargo.toml | 1 + .../src/commands/switch/explicit.rs | 6 +- packages/zpm-switch/src/config.rs | 53 +++++++++++++++ packages/zpm-switch/src/errors.rs | 15 ++++ packages/zpm-switch/src/lib.rs | 1 + packages/zpm-switch/src/main.rs | 1 + .../sources/features/yarnSwitch.test.ts | 68 +++++++++++++++++++ yarn.lock | 16 ++--- 11 files changed, 160 insertions(+), 9 deletions(-) create mode 100644 packages/zpm-switch/src/config.rs create mode 100644 tests/acceptance-tests/pkg-tests-specs/sources/features/yarnSwitch.test.ts diff --git a/Cargo.lock b/Cargo.lock index 98cf28e5..5029798c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5005,6 +5005,7 @@ dependencies = [ "serde_json", "serde_plain", "serde_with", + "serde_yaml", "thiserror 2.0.18", "tokio", "tokio-tungstenite", diff --git a/packages/zpm-config/schema.json b/packages/zpm-config/schema.json index 8a7d3585..3da8d200 100644 --- a/packages/zpm-config/schema.json +++ b/packages/zpm-config/schema.json @@ -553,6 +553,10 @@ } } }, + "switchVersionRequirement": { + "type": ["zpm_semver::Range", "null"], + "description": "When set, validates that the Yarn Switch binary version satisfies this semver range before executing any command." + }, "catalog": { "type": "object", "description": "Default catalog containing package name to version range mappings. This is an alias for catalogs.default.", diff --git a/packages/zpm-config/src/lib.rs b/packages/zpm-config/src/lib.rs index d233b335..d226c838 100644 --- a/packages/zpm-config/src/lib.rs +++ b/packages/zpm-config/src/lib.rs @@ -1166,6 +1166,9 @@ merge_optional_settings!(zpm_primitives::PeerRange); merge_optional_settings!(zpm_primitives::Range); merge_optional_settings!(zpm_primitives::Reference); +merge_settings!(zpm_semver::Range, |s: &str| FromFileString::from_file_string(s).unwrap()); +merge_optional_settings!(zpm_semver::Range); + merge_settings!(zpm_semver::RangeKind, |s: &str| FromFileString::from_file_string(s).unwrap()); merge_optional_settings!(zpm_semver::RangeKind); diff --git a/packages/zpm-switch/Cargo.toml b/packages/zpm-switch/Cargo.toml index de4503d3..16b70b2c 100644 --- a/packages/zpm-switch/Cargo.toml +++ b/packages/zpm-switch/Cargo.toml @@ -24,6 +24,7 @@ futures = { workspace = true } open = { workspace = true } uuid = { version = "1.21.0", features = ["v4"] } zpm-allocator = { workspace = true } +serde_yaml = { workspace = true } zpm-formats = { workspace = true } zpm-macro-enum = { workspace = true } zpm-parsers = { workspace = true } diff --git a/packages/zpm-switch/src/commands/switch/explicit.rs b/packages/zpm-switch/src/commands/switch/explicit.rs index 08bcea72..a33d7adb 100644 --- a/packages/zpm-switch/src/commands/switch/explicit.rs +++ b/packages/zpm-switch/src/commands/switch/explicit.rs @@ -3,7 +3,7 @@ use std::{process::{Command, ExitStatus, Stdio}, sync::Arc}; use clipanion::cli; use zpm_utils::ToFileString; -use crate::{cwd::{get_fake_cwd, get_final_cwd}, errors::Error, install::install_package_manager, ipc::YARNSW_PATH_ENV, manifest::{find_closest_package_manager, PackageManagerReference, VersionPackageManagerReference}, yarn::resolve_selector, yarn_enums::Selector}; +use crate::{config::validate_yarn_version, cwd::{get_fake_cwd, get_final_cwd}, errors::Error, install::install_package_manager, ipc::YARNSW_PATH_ENV, manifest::{find_closest_package_manager, PackageManagerReference, VersionPackageManagerReference}, yarn::resolve_selector, yarn_enums::Selector}; /// Call a custom Yarn binary for the current project #[cli::command(proxy)] @@ -17,6 +17,10 @@ pub struct ExplicitCommand { impl ExplicitCommand { pub async fn run(reference: &PackageManagerReference, args: &[String]) -> Result { + if let PackageManagerReference::Version(params) = reference { + validate_yarn_version(¶ms.version)?; + } + let mut binary = match reference { PackageManagerReference::Version(params) => install_package_manager(params).await?, diff --git a/packages/zpm-switch/src/config.rs b/packages/zpm-switch/src/config.rs new file mode 100644 index 00000000..7bea3fdd --- /dev/null +++ b/packages/zpm-switch/src/config.rs @@ -0,0 +1,53 @@ +use serde::Deserialize; +use zpm_semver::{Range, Version}; +use zpm_utils::{IoResultExt, Path}; + +use crate::errors::Error; + +#[derive(Deserialize, Default)] +#[serde(rename_all = "camelCase")] +struct PartialRcSettings { + #[serde(default)] + switch_version_requirement: Option, +} + +fn load_switch_version_requirement() -> Result, Error> { + let home_dir + = Path::home_dir()? + .ok_or(Error::MissingHomeFolder)?; + + let rc_filename + = std::env::var("YARN_RC_FILENAME") + .unwrap_or_else(|_| ".yarnrc.yml".to_string()); + + let rc_path + = home_dir.with_join_str(&rc_filename); + + let text + = rc_path.fs_read_text() + .ok_missing()?; + + let Some(text) = text else { + return Ok(None); + }; + + let partial: PartialRcSettings + = serde_yaml::from_str(&text)?; + + Ok(partial.switch_version_requirement) +} + +pub fn validate_yarn_version(version: &Version) -> Result<(), Error> { + let Some(requirement) = load_switch_version_requirement()? else { + return Ok(()); + }; + + if !requirement.check_ignore_rc(version) { + return Err(Error::SwitchVersionMismatch { + requirement, + actual: version.clone(), + }); + } + + Ok(()) +} diff --git a/packages/zpm-switch/src/errors.rs b/packages/zpm-switch/src/errors.rs index 122eff3e..059b0cd4 100644 --- a/packages/zpm-switch/src/errors.rs +++ b/packages/zpm-switch/src/errors.rs @@ -27,6 +27,9 @@ pub enum Error { #[error(transparent)] JsonError(#[from] zpm_parsers::Error), + #[error(transparent)] + YamlError(#[from] Arc), + #[error("Internal error: Join failed ({0})")] JoinFailed(#[from] Arc), @@ -93,6 +96,12 @@ pub enum Error { #[error("Failed to start daemon: {0}")] FailedToStartDaemon(Arc), + #[error("The resolved Yarn version ({actual}) does not satisfy the required range ({requirement})", actual = .actual.to_print_string(), requirement = .requirement.to_print_string())] + SwitchVersionMismatch { + requirement: zpm_semver::Range, + actual: zpm_semver::Version, + }, + #[error("No daemon is running for this project")] DaemonNotRunning, @@ -141,3 +150,9 @@ impl From for Error { Error::from(Arc::new(value)) } } + +impl From for Error { + fn from(value: serde_yaml::Error) -> Self { + Error::from(Arc::new(value)) + } +} diff --git a/packages/zpm-switch/src/lib.rs b/packages/zpm-switch/src/lib.rs index ffd2b0f6..528c425a 100644 --- a/packages/zpm-switch/src/lib.rs +++ b/packages/zpm-switch/src/lib.rs @@ -1,3 +1,4 @@ +pub mod config; pub mod daemons; mod errors; mod http; diff --git a/packages/zpm-switch/src/main.rs b/packages/zpm-switch/src/main.rs index 11caf954..bd9c5fdb 100644 --- a/packages/zpm-switch/src/main.rs +++ b/packages/zpm-switch/src/main.rs @@ -4,6 +4,7 @@ use std::process::ExitCode; mod cache; mod commands; +mod config; mod cwd; mod daemons; mod errors; diff --git a/tests/acceptance-tests/pkg-tests-specs/sources/features/yarnSwitch.test.ts b/tests/acceptance-tests/pkg-tests-specs/sources/features/yarnSwitch.test.ts new file mode 100644 index 00000000..07bb6371 --- /dev/null +++ b/tests/acceptance-tests/pkg-tests-specs/sources/features/yarnSwitch.test.ts @@ -0,0 +1,68 @@ +import {Filename, ppath, xfs} from '@yarnpkg/fslib'; + +describe(`Features`, () => { + describe(`Yarn Switch`, () => { + describe(`switchVersionRequirement`, () => { + test( + `it should succeed when no config file exists`, + makeTemporaryEnv({ + packageManager: `yarn@6.0.0`, + }, async ({path, runSwitch}) => { + await expect(runSwitch(`--version`)).resolves.toMatchObject({ + code: 0, + }); + }), + ); + + test( + `it should succeed when the config file is empty`, + makeTemporaryEnv({ + packageManager: `yarn@6.0.0`, + }, async ({path, runSwitch}) => { + const homePath = ppath.dirname(path); + + await xfs.writeFilePromise(ppath.join(homePath, Filename.rc), ``); + + await expect(runSwitch(`--version`)).resolves.toMatchObject({ + code: 0, + }); + }), + ); + + test( + `it should succeed when the version matches the requirement`, + makeTemporaryEnv({ + packageManager: `yarn@6.0.0`, + }, async ({path, runSwitch}) => { + const homePath = ppath.dirname(path); + + await xfs.writeJsonPromise(ppath.join(homePath, Filename.rc), { + switchVersionRequirement: `>=6.0.0`, + }); + + await expect(runSwitch(`--version`)).resolves.toMatchObject({ + code: 0, + }); + }), + ); + + test( + `it should fail when the version does not match the requirement`, + makeTemporaryEnv({ + packageManager: `yarn@6.0.0`, + }, async ({path, runSwitch}) => { + const homePath = ppath.dirname(path); + + await xfs.writeJsonPromise(ppath.join(homePath, Filename.rc), { + switchVersionRequirement: `>=99.0.0`, + }); + + await expect(runSwitch(`--version`)).rejects.toMatchObject({ + code: 1, + stdout: expect.stringContaining(`does not satisfy the required range`), + }); + }), + ); + }); + }); +}); diff --git a/yarn.lock b/yarn.lock index 3a26e038..b1b0ba11 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3,14 +3,14 @@ "version": 9 }, "workspaces": { - "@yarnpkg/documentation": "3a36e375c2f880ca4b3727f2db3a67968d85b7112518ce2cf2fdc9ce2cde237ab1010ab1320a074b1cece29e0c3b79b415b1b48bca4518c0f9157f0a3818e55d", - "@yarnpkg/monorepo": "f5e81432184d8250cce7f479f2ebb436a9b0900c22d0393bf1502e06cdad038193a88a7c6d3d6341963c6426eef186f390b1b44eff3242dc606b9f4a5cdb5846", - "@yarnpkg/zpm-constraints": "a602e3cc3ea1dd931ef50091753a9f1de3a06ff9ee0efb29c8222fb3491132ccbac1581220770e74dcf06e2c30189f636b1d7eb691bfdeff36012d3169630bc6", - "@yarnpkg/zpm-daemon-ui": "eeab3988f3f683099d2ac293259ca85accca0b08345680953771cb8aea311d570c309aa1dd19294742d23e95a728c70c96b985684fe454ac4a0415ae333e16ee", - "acceptance-tests": "e3fd8d082d2569f2db5632be90586713161177de887b3baef196e920aa6eb7a15447fb097a93339f72786bb1de1dab65e30bc9954296037e4fc72a9835d3e178", - "pkg-tests-core": "27dc1794f148dca7d11639e6ea0877781dcf88daf2e38af45bae8a0f9f259d4f49cb3f960e27849a3ee1e9d702da717d0060409c37fe6b26b23e509382d945e9", - "pkg-tests-fixtures": "634e2d39424349e30ef9ace3cbe374d6c2b404ab29491084908f538e4f9823567caa6d900e0c15e78c1515ed8b1418294c0e3063027fb7475982d0ddd62c2b64", - "pkg-tests-specs": "5cdd5861797fecf735eae28ef1480c96e7cbf11168c7459eaef247e8e9c8ab19a9940ba6436d7a982863a08286a2ca01d1a76cc926501a860822dda6f627ff4a" + "@yarnpkg/documentation": "91807b34e6dea592435ccae123ff1de2e4eb5915b5bf6f69e9b6108bb1fc47e614b3e6c095e90fddbc48070749ad63c372a7990a826f9c9c5da55a01875290a4", + "@yarnpkg/monorepo": "bb4a782d3d434fcd650e79b1367560cde2ac80cebaba1f906373cf370aa66aef412bac5d990d807b0e2c87734ac966a221b6442932a96bc0d774438e3fcafbd5", + "@yarnpkg/zpm-constraints": "60af699f2119810820520bb8e3eb436c2b62987eae269d932eb44f73f96cb2274059fcbe7ec29bfb8c3c3022719b5b27829653a4c00162d7a4979963f63d6055", + "@yarnpkg/zpm-daemon-ui": "fa7a9a93084cb9be77a2ecc957130a65d371e4755518d7ca9ad0882d86965132a9629bb843f688f22c61cccecc5e746e572cbbfaad253e0cfca99f381fc16e00", + "acceptance-tests": "9d8edc097d9e560116c52345d9a48c0066f7b6f25e95554a612e01de95b19c720af054e6f98c4d5a3e00696ea566f8910687a0149f591c0911cc757bd5f2bad0", + "pkg-tests-core": "5a2339ba2265d216312aa34bbf0b7e0fd3a8724e6fe285562c70f2b27a76c4370832597316e2d20246c8cdc54f96df0eb083aeec824f489a8f68c9bf2efc9a55", + "pkg-tests-fixtures": "6f988cb36fe126bc5d41ce56ce3fd0ad485e201dfe8aaa5c70bbf8a3b64a0c0e4ca7f13215026f93dd8cb6c5c43be41574f171e84a345f67a47898fb8e1204cd", + "pkg-tests-specs": "12b6677dde1f354be6117229b4fd4c8714ec52269a28175843170cbed4cf0942b2634c01016de5df3f1e942126a0b0141b01180201e26aca444b51637714cfa0" }, "entries": { "@ai-sdk/gateway@npm:1.0.33": { From 8f09762593772b53b0ed49f20f926fc07c9c584c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ma=C3=ABl=20Nison?= Date: Thu, 21 May 2026 21:28:05 +0200 Subject: [PATCH 2/5] Updates lockfile --- yarn.lock | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/yarn.lock b/yarn.lock index b1b0ba11..3a26e038 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3,14 +3,14 @@ "version": 9 }, "workspaces": { - "@yarnpkg/documentation": "91807b34e6dea592435ccae123ff1de2e4eb5915b5bf6f69e9b6108bb1fc47e614b3e6c095e90fddbc48070749ad63c372a7990a826f9c9c5da55a01875290a4", - "@yarnpkg/monorepo": "bb4a782d3d434fcd650e79b1367560cde2ac80cebaba1f906373cf370aa66aef412bac5d990d807b0e2c87734ac966a221b6442932a96bc0d774438e3fcafbd5", - "@yarnpkg/zpm-constraints": "60af699f2119810820520bb8e3eb436c2b62987eae269d932eb44f73f96cb2274059fcbe7ec29bfb8c3c3022719b5b27829653a4c00162d7a4979963f63d6055", - "@yarnpkg/zpm-daemon-ui": "fa7a9a93084cb9be77a2ecc957130a65d371e4755518d7ca9ad0882d86965132a9629bb843f688f22c61cccecc5e746e572cbbfaad253e0cfca99f381fc16e00", - "acceptance-tests": "9d8edc097d9e560116c52345d9a48c0066f7b6f25e95554a612e01de95b19c720af054e6f98c4d5a3e00696ea566f8910687a0149f591c0911cc757bd5f2bad0", - "pkg-tests-core": "5a2339ba2265d216312aa34bbf0b7e0fd3a8724e6fe285562c70f2b27a76c4370832597316e2d20246c8cdc54f96df0eb083aeec824f489a8f68c9bf2efc9a55", - "pkg-tests-fixtures": "6f988cb36fe126bc5d41ce56ce3fd0ad485e201dfe8aaa5c70bbf8a3b64a0c0e4ca7f13215026f93dd8cb6c5c43be41574f171e84a345f67a47898fb8e1204cd", - "pkg-tests-specs": "12b6677dde1f354be6117229b4fd4c8714ec52269a28175843170cbed4cf0942b2634c01016de5df3f1e942126a0b0141b01180201e26aca444b51637714cfa0" + "@yarnpkg/documentation": "3a36e375c2f880ca4b3727f2db3a67968d85b7112518ce2cf2fdc9ce2cde237ab1010ab1320a074b1cece29e0c3b79b415b1b48bca4518c0f9157f0a3818e55d", + "@yarnpkg/monorepo": "f5e81432184d8250cce7f479f2ebb436a9b0900c22d0393bf1502e06cdad038193a88a7c6d3d6341963c6426eef186f390b1b44eff3242dc606b9f4a5cdb5846", + "@yarnpkg/zpm-constraints": "a602e3cc3ea1dd931ef50091753a9f1de3a06ff9ee0efb29c8222fb3491132ccbac1581220770e74dcf06e2c30189f636b1d7eb691bfdeff36012d3169630bc6", + "@yarnpkg/zpm-daemon-ui": "eeab3988f3f683099d2ac293259ca85accca0b08345680953771cb8aea311d570c309aa1dd19294742d23e95a728c70c96b985684fe454ac4a0415ae333e16ee", + "acceptance-tests": "e3fd8d082d2569f2db5632be90586713161177de887b3baef196e920aa6eb7a15447fb097a93339f72786bb1de1dab65e30bc9954296037e4fc72a9835d3e178", + "pkg-tests-core": "27dc1794f148dca7d11639e6ea0877781dcf88daf2e38af45bae8a0f9f259d4f49cb3f960e27849a3ee1e9d702da717d0060409c37fe6b26b23e509382d945e9", + "pkg-tests-fixtures": "634e2d39424349e30ef9ace3cbe374d6c2b404ab29491084908f538e4f9823567caa6d900e0c15e78c1515ed8b1418294c0e3063027fb7475982d0ddd62c2b64", + "pkg-tests-specs": "5cdd5861797fecf735eae28ef1480c96e7cbf11168c7459eaef247e8e9c8ab19a9940ba6436d7a982863a08286a2ca01d1a76cc926501a860822dda6f627ff4a" }, "entries": { "@ai-sdk/gateway@npm:1.0.33": { From c69c36764b5f84f2a6ca6ce262f98322fa53660e Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Thu, 21 May 2026 19:56:07 +0000 Subject: [PATCH 3/5] Handle empty switch config files Applied via @cursor push command --- packages/zpm-switch/src/config.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/zpm-switch/src/config.rs b/packages/zpm-switch/src/config.rs index 7bea3fdd..69bd67b9 100644 --- a/packages/zpm-switch/src/config.rs +++ b/packages/zpm-switch/src/config.rs @@ -31,6 +31,10 @@ fn load_switch_version_requirement() -> Result, Error> { return Ok(None); }; + if text.is_empty() { + return Ok(None); + } + let partial: PartialRcSettings = serde_yaml::from_str(&text)?; From dd6fa44c98fa4b85a783d689a951b1a19e91b521 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Thu, 21 May 2026 20:14:14 +0000 Subject: [PATCH 4/5] Validate daemon Yarn versions Applied via @cursor push command --- .../src/commands/switch/daemon_open.rs | 3 +++ .../sources/features/yarnSwitch.test.ts | 18 ++++++++++++++++++ 2 files changed, 21 insertions(+) diff --git a/packages/zpm-switch/src/commands/switch/daemon_open.rs b/packages/zpm-switch/src/commands/switch/daemon_open.rs index b301c22f..3b44d5d0 100644 --- a/packages/zpm-switch/src/commands/switch/daemon_open.rs +++ b/packages/zpm-switch/src/commands/switch/daemon_open.rs @@ -5,6 +5,7 @@ use zpm_semver::Version; use zpm_utils::{Path, ToFileString}; use crate::{ + config::validate_yarn_version, cwd::get_final_cwd, daemons::{self, DaemonEntry}, errors::Error, @@ -77,6 +78,8 @@ impl DaemonOpenCommand { match &reference { PackageManagerReference::Version(version_ref) => { + validate_yarn_version(&version_ref.version)?; + let mut binary = install_package_manager(version_ref).await?; diff --git a/tests/acceptance-tests/pkg-tests-specs/sources/features/yarnSwitch.test.ts b/tests/acceptance-tests/pkg-tests-specs/sources/features/yarnSwitch.test.ts index 07bb6371..ce3648e2 100644 --- a/tests/acceptance-tests/pkg-tests-specs/sources/features/yarnSwitch.test.ts +++ b/tests/acceptance-tests/pkg-tests-specs/sources/features/yarnSwitch.test.ts @@ -63,6 +63,24 @@ describe(`Features`, () => { }); }), ); + + test( + `it should fail to start a daemon when the version does not match the requirement`, + makeTemporaryEnv({ + packageManager: `yarn@6.0.0`, + }, async ({path, runSwitch}) => { + const homePath = ppath.dirname(path); + + await xfs.writeJsonPromise(ppath.join(homePath, Filename.rc), { + switchVersionRequirement: `>=99.0.0`, + }); + + await expect(runSwitch(`switch`, `daemon`, `--start`)).rejects.toMatchObject({ + code: 1, + stdout: expect.stringContaining(`does not satisfy the required range`), + }); + }), + ); }); }); }); From 54cf9ae3ef7ab109ccf030ee7c6642e4847ca068 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Thu, 21 May 2026 20:34:41 +0000 Subject: [PATCH 5/5] Validate Yarn version during proxy completions Applied via @cursor push command --- packages/zpm-switch/src/commands/proxy.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/zpm-switch/src/commands/proxy.rs b/packages/zpm-switch/src/commands/proxy.rs index 2d91ed1d..64b86c59 100644 --- a/packages/zpm-switch/src/commands/proxy.rs +++ b/packages/zpm-switch/src/commands/proxy.rs @@ -4,7 +4,7 @@ use clipanion::cli; use clipanion::core::{Completion, CompletionContext}; use zpm_utils::{DataType, Note, ToFileString}; -use crate::{cwd::{get_fake_cwd, get_final_cwd}, errors::Error, install::install_package_manager, ipc::YARNSW_PATH_ENV, links::{LinkTarget, get_link, unset_link}, manifest::{LocalPackageManagerReference, PackageManagerField, PackageManagerReference, find_closest_package_manager}, yarn::get_default_yarn_version, yarn_enums::ReleaseLine}; +use crate::{config::validate_yarn_version, cwd::{get_fake_cwd, get_final_cwd}, errors::Error, install::install_package_manager, ipc::YARNSW_PATH_ENV, links::{LinkTarget, get_link, unset_link}, manifest::{LocalPackageManagerReference, PackageManagerField, PackageManagerReference, find_closest_package_manager}, yarn::get_default_yarn_version, yarn_enums::ReleaseLine}; use super::switch::explicit::ExplicitCommand; @@ -74,6 +74,10 @@ async fn proxy_completer_async(ctx: &CompletionContext<'_>) -> Vec { let mut binary = match &reference { PackageManagerReference::Version(params) => { + let Ok(()) = validate_yarn_version(¶ms.version) else { + return vec![]; + }; + let Ok(cmd) = install_package_manager(params).await else { return vec![]; };