diff --git a/crates/vite_global_cli/src/commands/env/config.rs b/crates/vite_global_cli/src/commands/env/config.rs index 97a2e3f693..bc0f248f27 100644 --- a/crates/vite_global_cli/src/commands/env/config.rs +++ b/crates/vite_global_cli/src/commands/env/config.rs @@ -362,7 +362,7 @@ async fn resolve_version_string(version: &str, provider: &NodeProvider) -> Resul // Check for "latest" alias - resolves to absolute latest version (including non-LTS) if NodeProvider::is_latest_alias(version) { - let resolved = provider.resolve_version("*").await?; + let resolved = provider.resolve_absolute_latest_version().await?; return Ok(resolved.to_string()); } @@ -391,8 +391,7 @@ pub async fn resolve_version_alias( Ok(resolved.to_string()) } "latest" => { - // Resolve * to get the absolute latest version - let resolved = provider.resolve_version("*").await?; + let resolved = provider.resolve_absolute_latest_version().await?; Ok(resolved.to_string()) } _ => resolve_version_string(version, provider).await, diff --git a/crates/vite_global_cli/src/commands/env/default.rs b/crates/vite_global_cli/src/commands/env/default.rs index b6ccb87911..6f0c1bb79a 100644 --- a/crates/vite_global_cli/src/commands/env/default.rs +++ b/crates/vite_global_cli/src/commands/env/default.rs @@ -68,7 +68,7 @@ async fn set_default(version: &str) -> Result { } "latest" => { // Resolve to show current value, but store "latest" as alias - let current_latest = provider.resolve_version("*").await?; + let current_latest = provider.resolve_absolute_latest_version().await?; (format!("latest (currently {})", current_latest), "latest".to_string()) } _ => { @@ -102,7 +102,7 @@ async fn resolve_alias( ) -> Result { match alias { "lts" => Ok(provider.resolve_latest_version().await?.to_string()), - "latest" => Ok(provider.resolve_version("*").await?.to_string()), + "latest" => Ok(provider.resolve_absolute_latest_version().await?.to_string()), _ => Ok(alias.to_string()), } } diff --git a/crates/vite_global_cli/src/commands/env/exec.rs b/crates/vite_global_cli/src/commands/env/exec.rs index e3b26c2656..1de04d75b3 100644 --- a/crates/vite_global_cli/src/commands/env/exec.rs +++ b/crates/vite_global_cli/src/commands/env/exec.rs @@ -159,7 +159,7 @@ async fn resolve_version(version: &str, provider: &NodeProvider) -> Result { - let resolved = provider.resolve_version("*").await?; + let resolved = provider.resolve_absolute_latest_version().await?; Ok(resolved.to_string()) } _ => { diff --git a/crates/vite_global_cli/src/commands/env/pin.rs b/crates/vite_global_cli/src/commands/env/pin.rs index a05259fa50..5f891dd05c 100644 --- a/crates/vite_global_cli/src/commands/env/pin.rs +++ b/crates/vite_global_cli/src/commands/env/pin.rs @@ -181,7 +181,7 @@ async fn resolve_version_for_pin( Ok((resolved.to_string(), true)) } "latest" => { - let resolved = provider.resolve_version("*").await?; + let resolved = provider.resolve_absolute_latest_version().await?; Ok((resolved.to_string(), true)) } _ => { diff --git a/crates/vite_js_runtime/src/providers/node.rs b/crates/vite_js_runtime/src/providers/node.rs index 844a9172d0..5b1f2e764d 100644 --- a/crates/vite_js_runtime/src/providers/node.rs +++ b/crates/vite_js_runtime/src/providers/node.rs @@ -313,6 +313,16 @@ impl NodeProvider { find_latest_lts_version(&versions) } + /// Get the absolute latest version, including non-LTS. + /// + /// # Errors + /// + /// Returns an error if no version is found or the version index cannot be fetched. + pub async fn resolve_absolute_latest_version(&self) -> Result { + let versions = self.fetch_version_index().await?; + find_absolute_latest_version(&versions) + } + /// Check if a version string is an LTS alias (e.g., `lts/*`, `lts/iron`, `lts/-1`). /// /// Returns `true` for LTS alias formats: @@ -461,6 +471,21 @@ fn find_latest_lts_version(versions: &[NodeVersionEntry]) -> Result }) } +/// Find the absolute latest version, regardless of LTS status. +/// +/// The version index is sorted newest-first, so we take the first entry. +fn find_absolute_latest_version(versions: &[NodeVersionEntry]) -> Result { + versions + .first() + .map(|entry| { + let version_str = entry.version.strip_prefix('v').unwrap_or(&entry.version); + version_str.into() + }) + .ok_or_else(|| Error::VersionIndexParseFailed { + reason: "No version found in version index".into(), + }) +} + /// Resolve a version requirement to a matching version from a list. /// /// Prefers LTS versions over non-LTS versions. Returns the highest LTS version @@ -1183,6 +1208,49 @@ fedcba987654 node-v22.13.1-win-x64.zip"; assert_eq!(result, "20.19.0"); } + // ======================================================================== + // Absolute Latest Version Tests + // ======================================================================== + + #[test] + fn test_find_absolute_latest_version() { + use super::find_absolute_latest_version; + + let versions = vec![ + NodeVersionEntry { version: "v25.5.0".into(), lts: LtsInfo::NotLts }, + NodeVersionEntry { version: "v24.5.0".into(), lts: LtsInfo::Codename("Jod".into()) }, + NodeVersionEntry { version: "v22.15.0".into(), lts: LtsInfo::Codename("Jod".into()) }, + NodeVersionEntry { version: "v20.19.0".into(), lts: LtsInfo::Codename("Iron".into()) }, + ]; + + // Should return the absolute highest version, not LTS + let result = find_absolute_latest_version(&versions).unwrap(); + assert_eq!(result, "25.5.0"); + } + + #[test] + fn test_find_absolute_latest_version_all_lts() { + use super::find_absolute_latest_version; + + let versions = vec![ + NodeVersionEntry { version: "v24.5.0".into(), lts: LtsInfo::Codename("Jod".into()) }, + NodeVersionEntry { version: "v22.15.0".into(), lts: LtsInfo::Codename("Jod".into()) }, + ]; + + // When all versions are LTS, return the highest + let result = find_absolute_latest_version(&versions).unwrap(); + assert_eq!(result, "24.5.0"); + } + + #[test] + fn test_find_absolute_latest_version_empty() { + use super::find_absolute_latest_version; + + let versions: Vec = vec![]; + let result = find_absolute_latest_version(&versions); + assert!(result.is_err()); + } + // ======================================================================== // LTS Alias Tests // ======================================================================== diff --git a/crates/vite_js_runtime/src/runtime.rs b/crates/vite_js_runtime/src/runtime.rs index 443d14bf89..abc3d6e385 100644 --- a/crates/vite_js_runtime/src/runtime.rs +++ b/crates/vite_js_runtime/src/runtime.rs @@ -418,7 +418,7 @@ async fn resolve_version_for_project( // Handle "latest" alias - resolves to absolute latest version (including non-LTS) if NodeProvider::is_latest_alias(version_req) { tracing::debug!("Resolving 'latest' alias"); - return provider.resolve_version("*").await; + return provider.resolve_absolute_latest_version().await; } // Check if it's an exact version diff --git a/rfcs/env-command.md b/rfcs/env-command.md index 6aec2333ac..961c4440df 100644 --- a/rfcs/env-command.md +++ b/rfcs/env-command.md @@ -436,7 +436,8 @@ vite-plus supports the following version specification formats, compatible with | **LTS latest** | `lts/*` | Highest LTS version | time-based (1 hour) | | **LTS codename** | `lts/iron`, `lts/jod` | Highest version in LTS line | time-based (1 hour) | | **LTS offset** | `lts/-1`, `lts/-2` | nth-highest LTS line | time-based (1 hour) | -| **Wildcard** | `*` | Latest version | time-based (1 hour) | +| **Wildcard** | `*` | Highest matching (prefers LTS) | time-based (1 hour) | +| **Latest** | `latest` | Absolute latest version | time-based (1 hour) | ### Exact Versions diff --git a/rfcs/js-runtime.md b/rfcs/js-runtime.md index 08629511a2..81c6183e18 100644 --- a/rfcs/js-runtime.md +++ b/rfcs/js-runtime.md @@ -182,8 +182,11 @@ impl NodeProvider { /// Resolve version requirement (e.g., "^24.4.0") to exact version pub async fn resolve_version(&self, version_req: &str) -> Result; - /// Get latest version (first entry in index) + /// Get latest LTS version pub async fn resolve_latest_version(&self) -> Result; + + /// Get absolute latest version (including non-LTS) + pub async fn resolve_absolute_latest_version(&self) -> Result; } ```