From 3a95df99165ea24d4280fd114b6155f06adbef8c Mon Sep 17 00:00:00 2001 From: leihaohao Date: Wed, 24 Jun 2026 17:39:34 +0800 Subject: [PATCH] feat(passthrough): add low-Node passthrough for eligible commands Closes #1909 Closes #1916 When the project's resolved Node version is below the supported minimum (20.19.0), eligible commands (vp run/vpr + package-manager family) bypass the Vite+ JS CLI and run the project's package manager directly. - Add shared passthrough module (vite_shared) with version comparison - Add passthrough command routing (vite_global_cli) with PATH handling - Add dispatch_with_pm for externally-provided PackageManager - Add detect_only on PackageManagerBuilder (no devEngines pin) - Defer is_low_node check in vpx until actually needed - Use vite_shared path utilities for safe non-UTF-8 PATH handling --- Cargo.lock | 2 + crates/vite_global_cli/src/cli.rs | 13 + crates/vite_global_cli/src/commands/mod.rs | 3 + .../src/commands/passthrough.rs | 336 ++++++++++++++++++ crates/vite_global_cli/src/commands/vpr.rs | 46 +++ crates/vite_global_cli/src/commands/vpx.rs | 106 +++++- .../vite_global_cli/tests/passthrough_e2e.rs | 65 ++++ crates/vite_install/src/package_manager.rs | 140 ++++++++ crates/vite_pm_cli/Cargo.toml | 3 + crates/vite_pm_cli/src/dispatch.rs | 51 ++- crates/vite_pm_cli/src/handlers.rs | 229 +++++++++++- crates/vite_shared/Cargo.toml | 1 + crates/vite_shared/src/lib.rs | 2 + crates/vite_shared/src/passthrough.rs | 120 +++++++ .../passthrough-notice/.node-version | 1 + .../passthrough-notice/package.json | 7 + .../passthrough-notice/snap.txt | 4 + .../passthrough-notice/steps.json | 5 + 18 files changed, 1113 insertions(+), 21 deletions(-) create mode 100644 crates/vite_global_cli/src/commands/passthrough.rs create mode 100644 crates/vite_global_cli/tests/passthrough_e2e.rs create mode 100644 crates/vite_shared/src/passthrough.rs create mode 100644 packages/cli/snap-tests-global/passthrough-notice/.node-version create mode 100644 packages/cli/snap-tests-global/passthrough-notice/package.json create mode 100644 packages/cli/snap-tests-global/passthrough-notice/snap.txt create mode 100644 packages/cli/snap-tests-global/passthrough-notice/steps.json diff --git a/Cargo.lock b/Cargo.lock index d1e34dc7d6..a3cd4d9d38 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8504,6 +8504,7 @@ version = "0.0.0" dependencies = [ "clap", "serde_json", + "tempfile", "thiserror 2.0.18", "tokio", "tracing", @@ -8563,6 +8564,7 @@ version = "0.0.0" dependencies = [ "directories", "nix 0.30.1", + "node-semver", "owo-colors", "reqwest", "rustls", diff --git a/crates/vite_global_cli/src/cli.rs b/crates/vite_global_cli/src/cli.rs index 93d363ff82..a4d1159e43 100644 --- a/crates/vite_global_cli/src/cli.rs +++ b/crates/vite_global_cli/src/cli.rs @@ -901,6 +901,19 @@ pub async fn run_command_with_options( return Ok(std::process::ExitStatus::default()); }; + // Low-Node passthrough precheck: when the project's resolved Node is below + // the supported minimum AND the command is eligible (run / package manager), + // bypass the Vite+ JS CLI and run the project's own package manager directly. + if commands::passthrough::is_eligible(&command) { + if let Some(node_version) = commands::passthrough::resolve_project_node_version(&cwd).await { + if commands::passthrough::should_passthrough(&command, &node_version) { + let mut executor = crate::js_executor::JsExecutor::new(None); + let runtime = executor.ensure_project_runtime(&cwd).await?; + return commands::passthrough::execute(&cwd, &command, runtime).await; + } + } + } + match command { // Category A: Package Manager Commands // Print the runtime header for `vp install` (when not silent). diff --git a/crates/vite_global_cli/src/commands/mod.rs b/crates/vite_global_cli/src/commands/mod.rs index 18679f0de6..09c2f701de 100644 --- a/crates/vite_global_cli/src/commands/mod.rs +++ b/crates/vite_global_cli/src/commands/mod.rs @@ -110,6 +110,9 @@ pub mod upgrade; // Category C: Local CLI Delegation pub mod delegate; +// Low-Node passthrough (degrades eligible commands to the project's package manager) +pub mod passthrough; + #[cfg(test)] mod tests { use vite_path::AbsolutePathBuf; diff --git a/crates/vite_global_cli/src/commands/passthrough.rs b/crates/vite_global_cli/src/commands/passthrough.rs new file mode 100644 index 0000000000..f52f7eff50 --- /dev/null +++ b/crates/vite_global_cli/src/commands/passthrough.rs @@ -0,0 +1,336 @@ +//! Low-Node passthrough: when the project's Node is below the supported +//! minimum, eligible commands (`vpr`/`vp run` + the package-manager family) +//! bypass the Vite+ JS CLI and run the project's own package manager directly, +//! skipping `devEngines` pinning. + +use std::{collections::HashMap, process::ExitStatus}; + +use vite_command::run_command; +use vite_install::PackageManager; +use vite_js_runtime::JsRuntime; +use vite_path::AbsolutePath; +use vite_shared::{PrependOptions, is_node_below_min, prepend_to_path_env}; + +use crate::{cli::Commands, error::Error}; + +/// Commands that degrade to passthrough on low Node. +/// +/// Add new commands here ONLY if they are pure script-run / package-manager +/// operations. Dev/build/test/lint/fmt/check/pack depend on bundled tools and +/// are NEVER eligible. +/// +/// Global PM operations (`-g`/`--global`) are excluded because they should use +/// VP's managed global install system, which has its own Node runtime. +#[allow(dead_code)] // called by run_command_with_options and execute_vpr +#[must_use] +pub fn is_eligible(command: &Commands) -> bool { + match command { + Commands::Run { .. } => true, + Commands::PackageManager(pm_cmd) => !is_global_pm_command(pm_cmd), + _ => false, + } +} + +/// Returns true if the PM command is a global operation that should bypass +/// passthrough and use VP's managed install system instead. +/// +/// Mirrors [`PackageManagerCommand::is_managed_global`] — keep in sync. +fn is_global_pm_command(command: &vite_pm_cli::PackageManagerCommand) -> bool { + command.is_managed_global() +} + +/// Print the one-line passthrough notice. Pure I/O, no version check. +pub(crate) fn print_passthrough_notice(node_version: &str, min: &str) { + // Reuse shared output style; keep to a single concise line. + vite_shared::output::warn(&format!( + "Node {node_version} is below the Vite+ minimum ({min}); using passthrough mode — \ + running the project's package manager directly without loading the Vite+ CLI. \ + Upgrade Node to restore full Vite+ functionality." + )); +} + +/// Returns true when passthrough should activate: eligible command AND the +/// resolved Node version is below the supported minimum. +#[allow(dead_code)] // called by run_command_with_options and execute_vpr +#[must_use] +pub fn should_passthrough(command: &Commands, node_version: &str) -> bool { + is_eligible(command) && is_node_below_min(node_version) +} + +/// Run an eligible command in passthrough mode. +/// +/// `runtime` is the resolved project Node (already ensured/downloaded by the +/// precheck caller in `run_command_with_options`). The package manager is +/// resolved via `PackageManager::detect_only` (no `devEngines` pin): +/// - `Commands::PackageManager` delegates to `vite_pm_cli::dispatch_with_pm`, +/// reusing the existing `resolve_*` parameter generation (zero drift). +/// - `Commands::Run` (`vpr`/`vp run