Skip to content

Commit 72990ab

Browse files
liangmiQwQclaude
andcommitted
feat(js-executor): check Node.js version compatibility before running vp JS commands
When a user sets an incompatible Node.js version via `vp env use`, all JS-based vp commands fail with cryptic errors. This adds an explicit check in `ensure_project_runtime()` that compares the resolved version against vp's own engine requirements (read from the CLI's package.json). If the version is outside the required range an `Error::NodeVersionIncompatible` is returned with a clear message and a hint to run `vp env use 22`. System runtimes (version = "system") and unparseable versions are skipped silently so `vp env off` users are not affected. Fixes #1358 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 6a9eb3d commit 72990ab

2 files changed

Lines changed: 57 additions & 0 deletions

File tree

crates/vite_global_cli/src/error.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,4 +54,9 @@ pub enum Error {
5454

5555
#[error("{0}")]
5656
Setup(#[from] vite_setup::error::Error),
57+
58+
#[error(
59+
"Node.js {version} does not meet vite-plus requirements ({requirement}).\nRun `vp env use 22` to switch to a compatible version."
60+
)]
61+
NodeVersionIncompatible { version: String, requirement: String },
5762
}

crates/vite_global_cli/src/js_executor.rs

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,16 @@ impl JsExecutor {
112112
cmd
113113
}
114114

115+
/// Read the `engines.node` requirement from the CLI's own `package.json`.
116+
///
117+
/// Returns `None` when the file is missing, unreadable, or has no `engines.node`.
118+
async fn get_cli_engines_requirement(&self) -> Option<String> {
119+
let cli_dir = self.get_cli_package_dir().ok()?;
120+
let pkg_path = cli_dir.join("package.json");
121+
let pkg = read_package_json(&pkg_path).await.ok()??;
122+
pkg.engines?.node.map(|s| s.to_string())
123+
}
124+
115125
/// Get the CLI's package.json directory (parent of `scripts_dir`).
116126
///
117127
/// This is used for resolving the CLI's default Node.js version
@@ -178,6 +188,9 @@ impl JsExecutor {
178188
};
179189
if let Some(version) = session_version {
180190
let runtime = download_runtime(JsRuntimeType::Node, &version).await?;
191+
if let Some(req) = self.get_cli_engines_requirement().await {
192+
check_runtime_compatibility(&runtime, &req)?;
193+
}
181194
return Ok(self.project_runtime.insert(runtime));
182195
}
183196

@@ -198,6 +211,9 @@ impl JsExecutor {
198211
let resolution = config::resolve_version(project_path).await?;
199212
download_runtime(JsRuntimeType::Node, &resolution.version).await?
200213
};
214+
if let Some(req) = self.get_cli_engines_requirement().await {
215+
check_runtime_compatibility(&runtime, &req)?;
216+
}
201217
self.project_runtime = Some(runtime);
202218
}
203219
Ok(self.project_runtime.as_ref().unwrap())
@@ -406,6 +422,42 @@ async fn has_valid_version_source(
406422
Ok(engines_valid || dev_engines_valid)
407423
}
408424

425+
/// Check that a resolved runtime's version satisfies vp's engine requirements.
426+
///
427+
/// Skips silently when:
428+
/// - The runtime is a system install (version == `"system"`)
429+
/// - The version or requirement strings cannot be parsed as semver
430+
///
431+
/// Returns [`Error::NodeVersionIncompatible`] when the version is parseable but
432+
/// outside the required range.
433+
fn check_runtime_compatibility(runtime: &JsRuntime, requirement: &str) -> Result<(), Error> {
434+
use node_semver::{Range, Version};
435+
436+
let version_str = runtime.version();
437+
438+
// System runtimes report "system" — we cannot inspect the actual version cheaply,
439+
// and the user has explicitly opted in via `vp env off`.
440+
if version_str == "system" {
441+
return Ok(());
442+
}
443+
444+
let normalized = version_str.strip_prefix('v').unwrap_or(version_str);
445+
let Ok(version) = Version::parse(normalized) else {
446+
return Ok(()); // unparseable version — skip silently
447+
};
448+
let Ok(range) = Range::parse(requirement) else {
449+
return Ok(()); // invalid range in package.json — skip silently
450+
};
451+
452+
if !range.satisfies(&version) {
453+
return Err(Error::NodeVersionIncompatible {
454+
version: version_str.to_string(),
455+
requirement: requirement.to_string(),
456+
});
457+
}
458+
Ok(())
459+
}
460+
409461
/// Try to find system Node.js when in system-first mode (`vp env off`).
410462
///
411463
/// Returns `Some(JsRuntime)` when both conditions are met:

0 commit comments

Comments
 (0)