From 1b4d8a07666db785dc9e2fd4fe8202e77ba250b8 Mon Sep 17 00:00:00 2001 From: JongKyung Lee Date: Thu, 12 Nov 2026 15:33:31 +0900 Subject: [PATCH] fix(env): accept lts/* and lts/ in env exec --node and env pin Both commands resolved versions with their own alias handling, which only knew lts and latest. They now share config::resolve_version_alias with vp env use. env pin still validates that an exact version exists. --- crates/vp_global_cli/src/commands/env/exec.rs | 74 +------------------ crates/vp_global_cli/src/commands/env/pin.rs | 46 ++++++------ 2 files changed, 24 insertions(+), 96 deletions(-) diff --git a/crates/vp_global_cli/src/commands/env/exec.rs b/crates/vp_global_cli/src/commands/env/exec.rs index 236702a5e5..96eead9ae6 100644 --- a/crates/vp_global_cli/src/commands/env/exec.rs +++ b/crates/vp_global_cli/src/commands/env/exec.rs @@ -148,7 +148,7 @@ async fn execute_with_version( let mut path_prefixes = Vec::new(); let modes = config::load_config().await?; let (resolved_node, system_node_bin) = if let Some(node_version) = node_version { - (resolve_version(node_version, &NodeProvider::new()).await?, None) + (config::resolve_version_alias(node_version, &NodeProvider::new()).await?, None) } else if modes.node_shim_mode == config::ShimMode::SystemFirst && let Some(path) = crate::shim::dispatch::find_system_tool("node") { @@ -272,47 +272,6 @@ async fn read_tool_version(path: &AbsolutePath) -> Option { .then(|| String::from_utf8_lossy(&output.stdout).trim().trim_start_matches('v').to_string()) } -/// Resolve version to an exact version. -/// -/// Handles aliases (lts, latest) and version ranges. -async fn resolve_version(version: &str, provider: &NodeProvider) -> Result { - match classify_version(version) { - VersionSelector::LatestLts => { - let resolved = provider.resolve_latest_version().await?; - Ok(resolved.to_string()) - } - VersionSelector::AbsoluteLatest => { - let resolved = provider.resolve_absolute_latest_version().await?; - Ok(resolved.to_string()) - } - VersionSelector::Exact(version) => Ok(version.to_string()), - VersionSelector::Range(version) => { - let resolved = provider.resolve_version(version).await?; - Ok(resolved.to_string()) - } - } -} - -#[derive(Debug, PartialEq, Eq)] -enum VersionSelector<'a> { - LatestLts, - AbsoluteLatest, - Exact(&'a str), - Range(&'a str), -} - -fn classify_version(version: &str) -> VersionSelector<'_> { - if version.eq_ignore_ascii_case("lts") { - VersionSelector::LatestLts - } else if version.eq_ignore_ascii_case("latest") { - VersionSelector::AbsoluteLatest - } else if NodeProvider::is_exact_version(version) { - VersionSelector::Exact(version.strip_prefix('v').unwrap_or(version)) - } else { - VersionSelector::Range(version) - } -} - #[cfg(test)] mod tests { use super::*; @@ -360,37 +319,6 @@ mod tests { .await; } - #[tokio::test] - async fn test_resolve_version_exact() { - let provider = NodeProvider::new(); - let version = resolve_version("20.18.0", &provider).await.unwrap(); - assert_eq!(version, "20.18.0"); - } - - #[tokio::test] - async fn test_resolve_version_with_v_prefix() { - let provider = NodeProvider::new(); - let version = resolve_version("v20.18.0", &provider).await.unwrap(); - assert_eq!(version, "20.18.0"); - } - - #[test] - fn test_classify_version_partial() { - assert_eq!(classify_version("20"), VersionSelector::Range("20")); - } - - #[test] - fn test_classify_version_range() { - assert_eq!(classify_version("^20.0.0"), VersionSelector::Range("^20.0.0")); - } - - #[test] - fn test_classify_version_aliases() { - assert_eq!(classify_version("lts"), VersionSelector::LatestLts); - assert_eq!(classify_version("LTS"), VersionSelector::LatestLts); - assert_eq!(classify_version("latest"), VersionSelector::AbsoluteLatest); - } - #[test] fn test_normalize_wrapper_command_strips_only_wrapper_separator() { let command = vec!["node".to_string(), "--".to_string(), "--version".to_string()]; diff --git a/crates/vp_global_cli/src/commands/env/pin.rs b/crates/vp_global_cli/src/commands/env/pin.rs index 2ef60b7fb9..6ef493d469 100644 --- a/crates/vp_global_cli/src/commands/env/pin.rs +++ b/crates/vp_global_cli/src/commands/env/pin.rs @@ -18,7 +18,7 @@ use vp_shared::output; use vt_path::AbsolutePathBuf; use super::{ - config::{get_config_path, load_config}, + config::{get_config_path, load_config, resolve_version_alias}, package_manager, spec::{EnvScope, EnvSpecs}, }; @@ -689,34 +689,18 @@ async fn remove_dev_engines_runtime_node(cwd: &AbsolutePathBuf) -> Result Result<(String, bool), Error> { - match version.to_lowercase().as_str() { - "lts" => { - let resolved = provider.resolve_latest_version().await?; - Ok((resolved.to_string(), true)) - } - "latest" => { - let resolved = provider.resolve_absolute_latest_version().await?; - Ok((resolved.to_string(), true)) - } - _ => { - // For exact versions, validate they exist - if NodeProvider::is_exact_version(version) { - // Validate the version exists by trying to resolve it - provider.resolve_version(version).await?; - Ok((version.to_string(), false)) - } else { - // For ranges/partial versions, resolve to exact version - let resolved = provider.resolve_version(version).await?; - Ok((resolved.to_string(), true)) - } - } + if NodeProvider::is_exact_version(version) { + // Validate the version exists by trying to resolve it + provider.resolve_version(version).await?; + return Ok((version.to_string(), false)); } + Ok((resolve_version_alias(version, provider).await?, true)) } /// Remove the Node.js pin from the current directory. @@ -1830,6 +1814,22 @@ mod tests { .await; } + #[tokio::test] + async fn test_resolve_version_for_pin_lts_aliases() { + let vp_home = TempDir::new().unwrap(); + with_mock_node_index([(env_vars::VP_HOME, vp_home.path().as_os_str())], || async { + let provider = NodeProvider::new(); + + for (alias, expected) in [("lts/*", "24.11.0"), ("lts/iron", "20.19.0")] { + let (resolved, was_alias) = + resolve_version_for_pin(alias, &provider).await.unwrap(); + assert!(was_alias, "{alias} should be treated as alias"); + assert_eq!(resolved, expected); + } + }) + .await; + } + #[tokio::test] async fn test_resolve_version_for_pin_exact_version() { let vp_home = TempDir::new().unwrap();