Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
74 changes: 1 addition & 73 deletions crates/vp_global_cli/src/commands/env/exec.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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")
{
Expand Down Expand Up @@ -272,47 +272,6 @@ async fn read_tool_version(path: &AbsolutePath) -> Option<String> {
.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<String, Error> {
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::*;
Expand Down Expand Up @@ -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()];
Expand Down
46 changes: 23 additions & 23 deletions crates/vp_global_cli/src/commands/env/pin.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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},
};
Expand Down Expand Up @@ -689,34 +689,18 @@ async fn remove_dev_engines_runtime_node(cwd: &AbsolutePathBuf) -> Result<bool,

/// Resolve version for pinning.
///
/// Aliases (lts, latest) are resolved to exact versions.
/// Aliases (lts, latest, lts/*, lts/iron) and ranges are resolved to exact versions.
/// Returns (resolved_version, was_alias).
async fn resolve_version_for_pin(
version: &str,
provider: &NodeProvider,
) -> 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.
Expand Down Expand Up @@ -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();
Expand Down
Loading