Skip to content
Merged
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
5 changes: 2 additions & 3 deletions crates/vite_global_cli/src/commands/env/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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());
}

Expand Down Expand Up @@ -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,
Expand Down
4 changes: 2 additions & 2 deletions crates/vite_global_cli/src/commands/env/default.rs
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ async fn set_default(version: &str) -> Result<ExitStatus, Error> {
}
"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())
}
_ => {
Expand Down Expand Up @@ -102,7 +102,7 @@ async fn resolve_alias(
) -> Result<String, Error> {
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()),
}
}
2 changes: 1 addition & 1 deletion crates/vite_global_cli/src/commands/env/exec.rs
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,7 @@ async fn resolve_version(version: &str, provider: &NodeProvider) -> Result<Strin
Ok(resolved.to_string())
}
"latest" => {
let resolved = provider.resolve_version("*").await?;
let resolved = provider.resolve_absolute_latest_version().await?;
Ok(resolved.to_string())
}
_ => {
Expand Down
2 changes: 1 addition & 1 deletion crates/vite_global_cli/src/commands/env/pin.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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))
}
_ => {
Expand Down
68 changes: 68 additions & 0 deletions crates/vite_js_runtime/src/providers/node.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Str, Error> {
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:
Expand Down Expand Up @@ -461,6 +471,21 @@ fn find_latest_lts_version(versions: &[NodeVersionEntry]) -> Result<Str, Error>
})
}

/// 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<Str, Error> {
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
Expand Down Expand Up @@ -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<NodeVersionEntry> = vec![];
let result = find_absolute_latest_version(&versions);
assert!(result.is_err());
}

// ========================================================================
// LTS Alias Tests
// ========================================================================
Expand Down
2 changes: 1 addition & 1 deletion crates/vite_js_runtime/src/runtime.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 2 additions & 1 deletion rfcs/env-command.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
5 changes: 4 additions & 1 deletion rfcs/js-runtime.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<Str, Error>;

/// Get latest version (first entry in index)
/// Get latest LTS version
pub async fn resolve_latest_version(&self) -> Result<Str, Error>;

/// Get absolute latest version (including non-LTS)
pub async fn resolve_absolute_latest_version(&self) -> Result<Str, Error>;
}
```

Expand Down
Loading