Skip to content

Commit 4bf3ec1

Browse files
committed
fix(env): resolve latest to absolute latest Node.js version (#1253)
Previously, `vp env install latest` resolved to the highest LTS version because it called `resolve_version("*")` which prefers LTS. Now the explicit `latest` keyword uses a dedicated `resolve_absolute_latest_version()` method that returns the highest version regardless of LTS status. The `*` wildcard and other semver ranges retain their LTS-preference behavior. Closes #1188
1 parent 7db2260 commit 4bf3ec1

File tree

8 files changed

+81
-10
lines changed

8 files changed

+81
-10
lines changed

crates/vite_global_cli/src/commands/env/config.rs

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -362,7 +362,7 @@ async fn resolve_version_string(version: &str, provider: &NodeProvider) -> Resul
362362

363363
// Check for "latest" alias - resolves to absolute latest version (including non-LTS)
364364
if NodeProvider::is_latest_alias(version) {
365-
let resolved = provider.resolve_version("*").await?;
365+
let resolved = provider.resolve_absolute_latest_version().await?;
366366
return Ok(resolved.to_string());
367367
}
368368

@@ -391,8 +391,7 @@ pub async fn resolve_version_alias(
391391
Ok(resolved.to_string())
392392
}
393393
"latest" => {
394-
// Resolve * to get the absolute latest version
395-
let resolved = provider.resolve_version("*").await?;
394+
let resolved = provider.resolve_absolute_latest_version().await?;
396395
Ok(resolved.to_string())
397396
}
398397
_ => resolve_version_string(version, provider).await,

crates/vite_global_cli/src/commands/env/default.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ async fn set_default(version: &str) -> Result<ExitStatus, Error> {
6868
}
6969
"latest" => {
7070
// Resolve to show current value, but store "latest" as alias
71-
let current_latest = provider.resolve_version("*").await?;
71+
let current_latest = provider.resolve_absolute_latest_version().await?;
7272
(format!("latest (currently {})", current_latest), "latest".to_string())
7373
}
7474
_ => {
@@ -102,7 +102,7 @@ async fn resolve_alias(
102102
) -> Result<String, Error> {
103103
match alias {
104104
"lts" => Ok(provider.resolve_latest_version().await?.to_string()),
105-
"latest" => Ok(provider.resolve_version("*").await?.to_string()),
105+
"latest" => Ok(provider.resolve_absolute_latest_version().await?.to_string()),
106106
_ => Ok(alias.to_string()),
107107
}
108108
}

crates/vite_global_cli/src/commands/env/exec.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -159,7 +159,7 @@ async fn resolve_version(version: &str, provider: &NodeProvider) -> Result<Strin
159159
Ok(resolved.to_string())
160160
}
161161
"latest" => {
162-
let resolved = provider.resolve_version("*").await?;
162+
let resolved = provider.resolve_absolute_latest_version().await?;
163163
Ok(resolved.to_string())
164164
}
165165
_ => {

crates/vite_global_cli/src/commands/env/pin.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -181,7 +181,7 @@ async fn resolve_version_for_pin(
181181
Ok((resolved.to_string(), true))
182182
}
183183
"latest" => {
184-
let resolved = provider.resolve_version("*").await?;
184+
let resolved = provider.resolve_absolute_latest_version().await?;
185185
Ok((resolved.to_string(), true))
186186
}
187187
_ => {

crates/vite_js_runtime/src/providers/node.rs

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -313,6 +313,16 @@ impl NodeProvider {
313313
find_latest_lts_version(&versions)
314314
}
315315

316+
/// Get the absolute latest version, including non-LTS.
317+
///
318+
/// # Errors
319+
///
320+
/// Returns an error if no version is found or the version index cannot be fetched.
321+
pub async fn resolve_absolute_latest_version(&self) -> Result<Str, Error> {
322+
let versions = self.fetch_version_index().await?;
323+
find_absolute_latest_version(&versions)
324+
}
325+
316326
/// Check if a version string is an LTS alias (e.g., `lts/*`, `lts/iron`, `lts/-1`).
317327
///
318328
/// Returns `true` for LTS alias formats:
@@ -461,6 +471,21 @@ fn find_latest_lts_version(versions: &[NodeVersionEntry]) -> Result<Str, Error>
461471
})
462472
}
463473

474+
/// Find the absolute latest version, regardless of LTS status.
475+
///
476+
/// The version index is sorted newest-first, so we take the first entry.
477+
fn find_absolute_latest_version(versions: &[NodeVersionEntry]) -> Result<Str, Error> {
478+
versions
479+
.first()
480+
.map(|entry| {
481+
let version_str = entry.version.strip_prefix('v').unwrap_or(&entry.version);
482+
version_str.into()
483+
})
484+
.ok_or_else(|| Error::VersionIndexParseFailed {
485+
reason: "No version found in version index".into(),
486+
})
487+
}
488+
464489
/// Resolve a version requirement to a matching version from a list.
465490
///
466491
/// Prefers LTS versions over non-LTS versions. Returns the highest LTS version
@@ -1183,6 +1208,49 @@ fedcba987654 node-v22.13.1-win-x64.zip";
11831208
assert_eq!(result, "20.19.0");
11841209
}
11851210

1211+
// ========================================================================
1212+
// Absolute Latest Version Tests
1213+
// ========================================================================
1214+
1215+
#[test]
1216+
fn test_find_absolute_latest_version() {
1217+
use super::find_absolute_latest_version;
1218+
1219+
let versions = vec![
1220+
NodeVersionEntry { version: "v25.5.0".into(), lts: LtsInfo::NotLts },
1221+
NodeVersionEntry { version: "v24.5.0".into(), lts: LtsInfo::Codename("Jod".into()) },
1222+
NodeVersionEntry { version: "v22.15.0".into(), lts: LtsInfo::Codename("Jod".into()) },
1223+
NodeVersionEntry { version: "v20.19.0".into(), lts: LtsInfo::Codename("Iron".into()) },
1224+
];
1225+
1226+
// Should return the absolute highest version, not LTS
1227+
let result = find_absolute_latest_version(&versions).unwrap();
1228+
assert_eq!(result, "25.5.0");
1229+
}
1230+
1231+
#[test]
1232+
fn test_find_absolute_latest_version_all_lts() {
1233+
use super::find_absolute_latest_version;
1234+
1235+
let versions = vec![
1236+
NodeVersionEntry { version: "v24.5.0".into(), lts: LtsInfo::Codename("Jod".into()) },
1237+
NodeVersionEntry { version: "v22.15.0".into(), lts: LtsInfo::Codename("Jod".into()) },
1238+
];
1239+
1240+
// When all versions are LTS, return the highest
1241+
let result = find_absolute_latest_version(&versions).unwrap();
1242+
assert_eq!(result, "24.5.0");
1243+
}
1244+
1245+
#[test]
1246+
fn test_find_absolute_latest_version_empty() {
1247+
use super::find_absolute_latest_version;
1248+
1249+
let versions: Vec<NodeVersionEntry> = vec![];
1250+
let result = find_absolute_latest_version(&versions);
1251+
assert!(result.is_err());
1252+
}
1253+
11861254
// ========================================================================
11871255
// LTS Alias Tests
11881256
// ========================================================================

crates/vite_js_runtime/src/runtime.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -418,7 +418,7 @@ async fn resolve_version_for_project(
418418
// Handle "latest" alias - resolves to absolute latest version (including non-LTS)
419419
if NodeProvider::is_latest_alias(version_req) {
420420
tracing::debug!("Resolving 'latest' alias");
421-
return provider.resolve_version("*").await;
421+
return provider.resolve_absolute_latest_version().await;
422422
}
423423

424424
// Check if it's an exact version

rfcs/env-command.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -436,7 +436,8 @@ vite-plus supports the following version specification formats, compatible with
436436
| **LTS latest** | `lts/*` | Highest LTS version | time-based (1 hour) |
437437
| **LTS codename** | `lts/iron`, `lts/jod` | Highest version in LTS line | time-based (1 hour) |
438438
| **LTS offset** | `lts/-1`, `lts/-2` | nth-highest LTS line | time-based (1 hour) |
439-
| **Wildcard** | `*` | Latest version | time-based (1 hour) |
439+
| **Wildcard** | `*` | Highest matching (prefers LTS) | time-based (1 hour) |
440+
| **Latest** | `latest` | Absolute latest version | time-based (1 hour) |
440441

441442
### Exact Versions
442443

rfcs/js-runtime.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -182,8 +182,11 @@ impl NodeProvider {
182182
/// Resolve version requirement (e.g., "^24.4.0") to exact version
183183
pub async fn resolve_version(&self, version_req: &str) -> Result<Str, Error>;
184184

185-
/// Get latest version (first entry in index)
185+
/// Get latest LTS version
186186
pub async fn resolve_latest_version(&self) -> Result<Str, Error>;
187+
188+
/// Get absolute latest version (including non-LTS)
189+
pub async fn resolve_absolute_latest_version(&self) -> Result<Str, Error>;
187190
}
188191
```
189192

0 commit comments

Comments
 (0)