From 23694a1943cef3cbd09a05730ffa3ba5e55cc325 Mon Sep 17 00:00:00 2001 From: Liang Mi Date: Sun, 21 Jun 2026 18:28:07 +0800 Subject: [PATCH 1/3] fix(upgrade): use LTS Node for dependency install --- Cargo.lock | 2 ++ crates/vite_setup/Cargo.toml | 2 ++ crates/vite_setup/src/install.rs | 44 ++++++++++++++++++++++++++++++-- 3 files changed, 46 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index ae6a2fb582..65a28d71b9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7976,7 +7976,9 @@ dependencies = [ "tokio", "tracing", "vite_install", + "vite_js_runtime", "vite_path", + "vite_shared", "vite_str", ] diff --git a/crates/vite_setup/Cargo.toml b/crates/vite_setup/Cargo.toml index 6a352afab0..0fd6e8b931 100644 --- a/crates/vite_setup/Cargo.toml +++ b/crates/vite_setup/Cargo.toml @@ -19,7 +19,9 @@ thiserror = { workspace = true } tokio = { workspace = true, features = ["full"] } tracing = { workspace = true } vite_install = { workspace = true } +vite_js_runtime = { workspace = true } vite_path = { workspace = true } +vite_shared = { workspace = true } vite_str = { workspace = true } [target.'cfg(windows)'.dependencies] diff --git a/crates/vite_setup/src/install.rs b/crates/vite_setup/src/install.rs index 9c89f5e670..4f56151fe8 100644 --- a/crates/vite_setup/src/install.rs +++ b/crates/vite_setup/src/install.rs @@ -269,7 +269,15 @@ pub async fn install_production_deps( args.push(registry_url); } - let output = run_vp_install(version_dir, &vp_binary, &args).await?; + // The child normally honors session and project runtime overrides. + // Pin an exact LTS version so incompatible overrides cannot skip optional native binaries. + let node_version = + vite_js_runtime::NodeProvider::new().resolve_latest_version().await.map_err(|error| { + Error::Setup( + format!("Failed to resolve the latest Node.js LTS version: {error}").into(), + ) + })?; + let output = run_vp_install(version_dir, &vp_binary, &args, &node_version).await?; if !output.status.success() { let log_path = write_upgrade_log(version_dir, &output.stdout, &output.stderr).await; @@ -301,7 +309,7 @@ pub async fn install_production_deps( // Only create the local override after explicit consent. This preserves // minimumReleaseAge protection for the default and non-interactive paths. write_release_age_overrides(version_dir).await?; - let retry_output = run_vp_install(version_dir, &vp_binary, &args).await?; + let retry_output = run_vp_install(version_dir, &vp_binary, &args, &node_version).await?; if !retry_output.status.success() { let retry_log_path = write_upgrade_log(version_dir, &retry_output.stdout, &retry_output.stderr).await; @@ -323,11 +331,13 @@ async fn run_vp_install( version_dir: &AbsolutePath, vp_binary: &AbsolutePath, args: &[&str], + node_version: &str, ) -> Result { let output = tokio::process::Command::new(vp_binary.as_path()) .args(args) .current_dir(version_dir) .env("CI", "true") + .env(vite_shared::env_vars::VP_NODE_VERSION, node_version) .output() .await?; @@ -823,6 +833,36 @@ mod tests { ); } + #[cfg(unix)] + #[tokio::test] + async fn run_vp_install_overrides_node_version() { + use std::os::unix::fs::PermissionsExt; + + let temp = tempfile::tempdir().unwrap(); + let version_dir = AbsolutePathBuf::new(temp.path().to_path_buf()).unwrap(); + let bin_dir = version_dir.join("bin"); + tokio::fs::create_dir_all(&bin_dir).await.unwrap(); + + let vp_binary = bin_dir.join(crate::VP_BINARY_NAME); + tokio::fs::write( + &vp_binary, + "#!/bin/sh\nprintf '%s' \"$VP_NODE_VERSION\" > node-version.txt\n", + ) + .await + .unwrap(); + tokio::fs::set_permissions(&vp_binary, std::fs::Permissions::from_mode(0o755)) + .await + .unwrap(); + + let output = + run_vp_install(&version_dir, &vp_binary, &["install"], "24.11.1").await.unwrap(); + assert!(output.status.success()); + + let node_version = + tokio::fs::read_to_string(version_dir.join("node-version.txt")).await.unwrap(); + assert_eq!(node_version, "24.11.1"); + } + #[test] fn test_is_release_age_error_detects_pnpm_no_mature_code() { assert!(is_release_age_error( From 6bb74359e7150d26d8cae408116e30ba42994c1b Mon Sep 17 00:00:00 2001 From: Liang Mi Date: Sun, 21 Jun 2026 19:05:54 +0800 Subject: [PATCH 2/3] fix(upgrade): bypass runtime overrides during bootstrap --- Cargo.lock | 2 - crates/vite_global_cli/src/commands/mod.rs | 65 ++++++++++++++++++---- crates/vite_setup/Cargo.toml | 2 - crates/vite_setup/src/install.rs | 28 +++------- crates/vite_setup/src/lib.rs | 3 + 5 files changed, 66 insertions(+), 34 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 65a28d71b9..ae6a2fb582 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7976,9 +7976,7 @@ dependencies = [ "tokio", "tracing", "vite_install", - "vite_js_runtime", "vite_path", - "vite_shared", "vite_str", ] diff --git a/crates/vite_global_cli/src/commands/mod.rs b/crates/vite_global_cli/src/commands/mod.rs index 18679f0de6..640ce68747 100644 --- a/crates/vite_global_cli/src/commands/mod.rs +++ b/crates/vite_global_cli/src/commands/mod.rs @@ -18,7 +18,8 @@ use std::{collections::HashMap, io::BufReader}; -use vite_path::AbsolutePath; +use vite_js_runtime::{JsRuntimeType, NodeProvider, download_runtime}; +use vite_path::{AbsolutePath, AbsolutePathBuf}; use vite_shared::{PrependOptions, prepend_to_path_env}; use crate::{error::Error, js_executor::JsExecutor}; @@ -65,17 +66,9 @@ pub fn has_vite_plus_dependency(cwd: &AbsolutePath) -> bool { /// If `project_path` contains a package.json, uses the project's runtime /// (based on devEngines.runtime). Otherwise, falls back to the CLI's runtime. pub async fn prepend_js_runtime_to_path_env(project_path: &AbsolutePath) -> Result<(), Error> { - let mut executor = JsExecutor::new(None); - - // Use project runtime if package.json exists, otherwise use CLI runtime - let package_json_path = project_path.join("package.json"); - let runtime = if package_json_path.as_path().exists() { - executor.ensure_project_runtime(project_path).await? - } else { - executor.ensure_cli_runtime().await? - }; + let force_lts = std::env::var_os(vite_setup::FORCE_LTS_NODE_ENV).is_some(); + let node_bin_prefix = resolve_package_manager_node_bin(project_path, force_lts).await?; - let node_bin_prefix = runtime.get_bin_prefix(); // Use dedupe_anywhere=true to check if node bin already exists anywhere in PATH let options = PrependOptions { dedupe_anywhere: true }; if prepend_to_path_env(&node_bin_prefix, options) { @@ -85,6 +78,26 @@ pub async fn prepend_js_runtime_to_path_env(project_path: &AbsolutePath) -> Resu Ok(()) } +async fn resolve_package_manager_node_bin( + project_path: &AbsolutePath, + force_lts: bool, +) -> Result { + if force_lts { + let version = NodeProvider::new().resolve_latest_version().await?; + return Ok(download_runtime(JsRuntimeType::Node, &version).await?.get_bin_prefix()); + } + + let mut executor = JsExecutor::new(None); + // Use project runtime if package.json exists, otherwise use CLI runtime + let package_json_path = project_path.join("package.json"); + let runtime = if package_json_path.as_path().exists() { + executor.ensure_project_runtime(project_path).await? + } else { + executor.ensure_cli_runtime().await? + }; + Ok(runtime.get_bin_prefix()) +} + // Global package management pub mod global; @@ -204,4 +217,34 @@ mod tests { // Should find the child's package.json first and return false assert!(!has_vite_plus_dependency(&child_path)); } + + #[tokio::test] + async fn forced_lts_runtime_ignores_project_and_system_first_config() { + use tempfile::TempDir; + use vite_shared::EnvConfig; + + let vp_home = TempDir::new().unwrap(); + let _guard = + EnvConfig::test_guard(EnvConfig::for_test_with_home(vp_home.path().to_path_buf())); + tokio::fs::write(vp_home.path().join("config.json"), r#"{"shimMode":"system_first"}"#) + .await + .unwrap(); + + let project = TempDir::new().unwrap(); + tokio::fs::write(project.path().join("package.json"), "{}").await.unwrap(); + tokio::fs::write(project.path().join(".node-version"), "20.0.0\n").await.unwrap(); + let project_path = AbsolutePathBuf::new(project.path().to_path_buf()).unwrap(); + + let node_bin = resolve_package_manager_node_bin(&project_path, true).await.unwrap(); + + assert!( + node_bin.as_path().starts_with(vp_home.path()), + "forced LTS runtime should be managed under VP_HOME, got {}", + node_bin.as_path().display() + ); + assert!( + !node_bin.as_path().to_string_lossy().contains("20.0.0"), + "forced LTS runtime should ignore the project Node pin" + ); + } } diff --git a/crates/vite_setup/Cargo.toml b/crates/vite_setup/Cargo.toml index 0fd6e8b931..6a352afab0 100644 --- a/crates/vite_setup/Cargo.toml +++ b/crates/vite_setup/Cargo.toml @@ -19,9 +19,7 @@ thiserror = { workspace = true } tokio = { workspace = true, features = ["full"] } tracing = { workspace = true } vite_install = { workspace = true } -vite_js_runtime = { workspace = true } vite_path = { workspace = true } -vite_shared = { workspace = true } vite_str = { workspace = true } [target.'cfg(windows)'.dependencies] diff --git a/crates/vite_setup/src/install.rs b/crates/vite_setup/src/install.rs index 4f56151fe8..6d2bea8928 100644 --- a/crates/vite_setup/src/install.rs +++ b/crates/vite_setup/src/install.rs @@ -269,15 +269,7 @@ pub async fn install_production_deps( args.push(registry_url); } - // The child normally honors session and project runtime overrides. - // Pin an exact LTS version so incompatible overrides cannot skip optional native binaries. - let node_version = - vite_js_runtime::NodeProvider::new().resolve_latest_version().await.map_err(|error| { - Error::Setup( - format!("Failed to resolve the latest Node.js LTS version: {error}").into(), - ) - })?; - let output = run_vp_install(version_dir, &vp_binary, &args, &node_version).await?; + let output = run_vp_install(version_dir, &vp_binary, &args).await?; if !output.status.success() { let log_path = write_upgrade_log(version_dir, &output.stdout, &output.stderr).await; @@ -309,7 +301,7 @@ pub async fn install_production_deps( // Only create the local override after explicit consent. This preserves // minimumReleaseAge protection for the default and non-interactive paths. write_release_age_overrides(version_dir).await?; - let retry_output = run_vp_install(version_dir, &vp_binary, &args, &node_version).await?; + let retry_output = run_vp_install(version_dir, &vp_binary, &args).await?; if !retry_output.status.success() { let retry_log_path = write_upgrade_log(version_dir, &retry_output.stdout, &retry_output.stderr).await; @@ -331,13 +323,12 @@ async fn run_vp_install( version_dir: &AbsolutePath, vp_binary: &AbsolutePath, args: &[&str], - node_version: &str, ) -> Result { let output = tokio::process::Command::new(vp_binary.as_path()) .args(args) .current_dir(version_dir) .env("CI", "true") - .env(vite_shared::env_vars::VP_NODE_VERSION, node_version) + .env(crate::FORCE_LTS_NODE_ENV, "1") .output() .await?; @@ -835,7 +826,7 @@ mod tests { #[cfg(unix)] #[tokio::test] - async fn run_vp_install_overrides_node_version() { + async fn run_vp_install_forces_lts_node() { use std::os::unix::fs::PermissionsExt; let temp = tempfile::tempdir().unwrap(); @@ -846,7 +837,7 @@ mod tests { let vp_binary = bin_dir.join(crate::VP_BINARY_NAME); tokio::fs::write( &vp_binary, - "#!/bin/sh\nprintf '%s' \"$VP_NODE_VERSION\" > node-version.txt\n", + "#!/bin/sh\nprintf '%s' \"$VP_FORCE_LTS_NODE\" > force-lts-node.txt\n", ) .await .unwrap(); @@ -854,13 +845,12 @@ mod tests { .await .unwrap(); - let output = - run_vp_install(&version_dir, &vp_binary, &["install"], "24.11.1").await.unwrap(); + let output = run_vp_install(&version_dir, &vp_binary, &["install"]).await.unwrap(); assert!(output.status.success()); - let node_version = - tokio::fs::read_to_string(version_dir.join("node-version.txt")).await.unwrap(); - assert_eq!(node_version, "24.11.1"); + let force_lts = + tokio::fs::read_to_string(version_dir.join("force-lts-node.txt")).await.unwrap(); + assert_eq!(force_lts, "1"); } #[test] diff --git a/crates/vite_setup/src/lib.rs b/crates/vite_setup/src/lib.rs index c2e5d4a494..9c2f2e9619 100644 --- a/crates/vite_setup/src/lib.rs +++ b/crates/vite_setup/src/lib.rs @@ -26,3 +26,6 @@ pub const MAX_VERSIONS_KEEP: usize = 3; /// Platform-specific binary name for the `vp` CLI. pub const VP_BINARY_NAME: &str = if cfg!(windows) { "vp.exe" } else { "vp" }; + +/// Force the package-manager bootstrap to use the managed latest LTS Node.js runtime. +pub const FORCE_LTS_NODE_ENV: &str = "VP_FORCE_LTS_NODE"; From d4dc75b4a2bfcd4276fdfaee203741864588a973 Mon Sep 17 00:00:00 2001 From: Liang Mi Date: Sun, 21 Jun 2026 19:09:53 +0800 Subject: [PATCH 3/3] fix(upgrade): run pinned pnpm with managed Node --- Cargo.lock | 1 + crates/vite_global_cli/src/commands/mod.rs | 65 +++---------- crates/vite_setup/Cargo.toml | 1 + crates/vite_setup/src/install.rs | 101 ++++++++++++++------- crates/vite_setup/src/lib.rs | 3 - 5 files changed, 83 insertions(+), 88 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index ae6a2fb582..040c3f8f5d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7976,6 +7976,7 @@ dependencies = [ "tokio", "tracing", "vite_install", + "vite_js_runtime", "vite_path", "vite_str", ] diff --git a/crates/vite_global_cli/src/commands/mod.rs b/crates/vite_global_cli/src/commands/mod.rs index 640ce68747..18679f0de6 100644 --- a/crates/vite_global_cli/src/commands/mod.rs +++ b/crates/vite_global_cli/src/commands/mod.rs @@ -18,8 +18,7 @@ use std::{collections::HashMap, io::BufReader}; -use vite_js_runtime::{JsRuntimeType, NodeProvider, download_runtime}; -use vite_path::{AbsolutePath, AbsolutePathBuf}; +use vite_path::AbsolutePath; use vite_shared::{PrependOptions, prepend_to_path_env}; use crate::{error::Error, js_executor::JsExecutor}; @@ -66,28 +65,8 @@ pub fn has_vite_plus_dependency(cwd: &AbsolutePath) -> bool { /// If `project_path` contains a package.json, uses the project's runtime /// (based on devEngines.runtime). Otherwise, falls back to the CLI's runtime. pub async fn prepend_js_runtime_to_path_env(project_path: &AbsolutePath) -> Result<(), Error> { - let force_lts = std::env::var_os(vite_setup::FORCE_LTS_NODE_ENV).is_some(); - let node_bin_prefix = resolve_package_manager_node_bin(project_path, force_lts).await?; - - // Use dedupe_anywhere=true to check if node bin already exists anywhere in PATH - let options = PrependOptions { dedupe_anywhere: true }; - if prepend_to_path_env(&node_bin_prefix, options) { - tracing::debug!("Set PATH to include {:?}", node_bin_prefix); - } - - Ok(()) -} - -async fn resolve_package_manager_node_bin( - project_path: &AbsolutePath, - force_lts: bool, -) -> Result { - if force_lts { - let version = NodeProvider::new().resolve_latest_version().await?; - return Ok(download_runtime(JsRuntimeType::Node, &version).await?.get_bin_prefix()); - } - let mut executor = JsExecutor::new(None); + // Use project runtime if package.json exists, otherwise use CLI runtime let package_json_path = project_path.join("package.json"); let runtime = if package_json_path.as_path().exists() { @@ -95,7 +74,15 @@ async fn resolve_package_manager_node_bin( } else { executor.ensure_cli_runtime().await? }; - Ok(runtime.get_bin_prefix()) + + let node_bin_prefix = runtime.get_bin_prefix(); + // Use dedupe_anywhere=true to check if node bin already exists anywhere in PATH + let options = PrependOptions { dedupe_anywhere: true }; + if prepend_to_path_env(&node_bin_prefix, options) { + tracing::debug!("Set PATH to include {:?}", node_bin_prefix); + } + + Ok(()) } // Global package management @@ -217,34 +204,4 @@ mod tests { // Should find the child's package.json first and return false assert!(!has_vite_plus_dependency(&child_path)); } - - #[tokio::test] - async fn forced_lts_runtime_ignores_project_and_system_first_config() { - use tempfile::TempDir; - use vite_shared::EnvConfig; - - let vp_home = TempDir::new().unwrap(); - let _guard = - EnvConfig::test_guard(EnvConfig::for_test_with_home(vp_home.path().to_path_buf())); - tokio::fs::write(vp_home.path().join("config.json"), r#"{"shimMode":"system_first"}"#) - .await - .unwrap(); - - let project = TempDir::new().unwrap(); - tokio::fs::write(project.path().join("package.json"), "{}").await.unwrap(); - tokio::fs::write(project.path().join(".node-version"), "20.0.0\n").await.unwrap(); - let project_path = AbsolutePathBuf::new(project.path().to_path_buf()).unwrap(); - - let node_bin = resolve_package_manager_node_bin(&project_path, true).await.unwrap(); - - assert!( - node_bin.as_path().starts_with(vp_home.path()), - "forced LTS runtime should be managed under VP_HOME, got {}", - node_bin.as_path().display() - ); - assert!( - !node_bin.as_path().to_string_lossy().contains("20.0.0"), - "forced LTS runtime should ignore the project Node pin" - ); - } } diff --git a/crates/vite_setup/Cargo.toml b/crates/vite_setup/Cargo.toml index 6a352afab0..741bd09c7a 100644 --- a/crates/vite_setup/Cargo.toml +++ b/crates/vite_setup/Cargo.toml @@ -19,6 +19,7 @@ thiserror = { workspace = true } tokio = { workspace = true, features = ["full"] } tracing = { workspace = true } vite_install = { workspace = true } +vite_js_runtime = { workspace = true } vite_path = { workspace = true } vite_str = { workspace = true } diff --git a/crates/vite_setup/src/install.rs b/crates/vite_setup/src/install.rs index 6d2bea8928..00824cd20e 100644 --- a/crates/vite_setup/src/install.rs +++ b/crates/vite_setup/src/install.rs @@ -4,6 +4,7 @@ //! and version cleanup. use std::{ + env, io::{Cursor, IsTerminal, Read as _, Write as _}, path::Path, process::{self, Output}, @@ -12,6 +13,8 @@ use std::{ use flate2::read::GzDecoder; use tar::Archive; +use vite_install::{PackageManagerType, download_package_manager}; +use vite_js_runtime::{JsRuntimeType, NodeProvider, download_runtime}; use vite_path::{AbsolutePath, AbsolutePathBuf}; use crate::error::Error; @@ -91,7 +94,7 @@ pub async fn extract_platform_package( /// The pnpm version pinned in the wrapper package.json for global installs. /// This ensures consistent install behavior regardless of the user's global pnpm version. -const PINNED_PNPM_VERSION: &str = "pnpm@10.33.0"; +const PINNED_PNPM_VERSION: &str = "10.33.0"; /// Generate a wrapper `package.json` that declares `vite-plus` as a dependency. /// @@ -106,7 +109,7 @@ pub async fn generate_wrapper_package_json( "name": "vp-global", "version": version, "private": true, - "packageManager": PINNED_PNPM_VERSION, + "packageManager": format!("pnpm@{PINNED_PNPM_VERSION}"), "dependencies": { "vite-plus": version } @@ -238,9 +241,9 @@ pub async fn write_upgrade_log( } } -/// Install production dependencies using the new version's binary. +/// Install production dependencies with managed Node.js LTS and pinned pnpm. /// -/// Spawns: `{version_dir}/bin/vp install [--registry ]` with `CI=true`. +/// Spawns: `node /bin/pnpm.cjs install [--registry ]` with `CI=true`. /// On failure, writes stdout+stderr to `{version_dir}/upgrade.log` for debugging. pub async fn install_production_deps( version_dir: &AbsolutePath, @@ -248,15 +251,7 @@ pub async fn install_production_deps( silent: bool, new_version: &str, ) -> Result<(), Error> { - let vp_binary = version_dir.join("bin").join(crate::VP_BINARY_NAME); - - if !tokio::fs::try_exists(&vp_binary).await.unwrap_or(false) { - return Err(Error::Setup( - format!("New binary not found at {}", vp_binary.as_path().display()).into(), - )); - } - - tracing::debug!("Running vp install in {}", version_dir.as_path().display()); + tracing::debug!("Running pnpm install in {}", version_dir.as_path().display()); // Do not pass `--silent` to the inner install: pnpm suppresses the // release-age error body in silent mode, which would leave upgrade.log @@ -264,12 +259,32 @@ pub async fn install_production_deps( // process captures the output and only surfaces it through the log. let mut args = vec!["install"]; if let Some(registry_url) = registry { - args.push("--"); args.push("--registry"); args.push(registry_url); } - let output = run_vp_install(version_dir, &vp_binary, &args).await?; + let node_version = NodeProvider::new().resolve_latest_version().await.map_err(|error| { + Error::Setup(format!("Failed to resolve the latest Node.js LTS version: {error}").into()) + })?; + let node_runtime = + download_runtime(JsRuntimeType::Node, &node_version).await.map_err(|error| { + Error::Setup(format!("Failed to install Node.js {node_version}: {error}").into()) + })?; + let (pnpm_dir, _, _) = + download_package_manager(PackageManagerType::Pnpm, PINNED_PNPM_VERSION, None) + .await + .map_err(|error| { + Error::Setup( + format!("Failed to install pnpm {PINNED_PNPM_VERSION}: {error}").into(), + ) + })?; + let pnpm_entry = pnpm_dir.join("bin").join("pnpm.cjs"); + if !tokio::fs::try_exists(&pnpm_entry).await.unwrap_or(false) { + return Err(Error::Setup( + format!("pnpm entry not found at {}", pnpm_entry.as_path().display()).into(), + )); + } + let output = run_pnpm_install(version_dir, &node_runtime, &pnpm_entry, &args).await?; if !output.status.success() { let log_path = write_upgrade_log(version_dir, &output.stdout, &output.stderr).await; @@ -301,7 +316,7 @@ pub async fn install_production_deps( // Only create the local override after explicit consent. This preserves // minimumReleaseAge protection for the default and non-interactive paths. write_release_age_overrides(version_dir).await?; - let retry_output = run_vp_install(version_dir, &vp_binary, &args).await?; + let retry_output = run_pnpm_install(version_dir, &node_runtime, &pnpm_entry, &args).await?; if !retry_output.status.success() { let retry_log_path = write_upgrade_log(version_dir, &retry_output.stdout, &retry_output.stderr).await; @@ -319,16 +334,28 @@ pub async fn install_production_deps( Ok(()) } -async fn run_vp_install( +async fn run_pnpm_install( version_dir: &AbsolutePath, - vp_binary: &AbsolutePath, + node_runtime: &vite_js_runtime::JsRuntime, + pnpm_entry: &AbsolutePath, args: &[&str], ) -> Result { - let output = tokio::process::Command::new(vp_binary.as_path()) + let node_bin = node_runtime.get_bin_prefix(); + let pnpm_bin = pnpm_entry.parent().ok_or_else(|| { + Error::Setup(format!("pnpm entry has no parent: {}", pnpm_entry.as_path().display()).into()) + })?; + let current_path = env::var_os("PATH").unwrap_or_default(); + let mut path_entries = vec![node_bin.as_path().to_path_buf(), pnpm_bin.as_path().to_path_buf()]; + path_entries.extend(env::split_paths(¤t_path)); + let path = env::join_paths(path_entries) + .map_err(|error| Error::Setup(format!("Failed to build PATH for pnpm: {error}").into()))?; + + let output = tokio::process::Command::new(node_runtime.get_binary_path().as_path()) + .arg(pnpm_entry.as_path()) .args(args) .current_dir(version_dir) .env("CI", "true") - .env(crate::FORCE_LTS_NODE_ENV, "1") + .env("PATH", path) .output() .await?; @@ -826,31 +853,43 @@ mod tests { #[cfg(unix)] #[tokio::test] - async fn run_vp_install_forces_lts_node() { + async fn run_pnpm_install_uses_managed_node_directly() { use std::os::unix::fs::PermissionsExt; let temp = tempfile::tempdir().unwrap(); let version_dir = AbsolutePathBuf::new(temp.path().to_path_buf()).unwrap(); - let bin_dir = version_dir.join("bin"); - tokio::fs::create_dir_all(&bin_dir).await.unwrap(); + let node_bin = version_dir.join("node").join("bin"); + let pnpm_bin = version_dir.join("pnpm").join("bin"); + tokio::fs::create_dir_all(&node_bin).await.unwrap(); + tokio::fs::create_dir_all(&pnpm_bin).await.unwrap(); - let vp_binary = bin_dir.join(crate::VP_BINARY_NAME); + let node_binary = node_bin.join("node"); tokio::fs::write( - &vp_binary, - "#!/bin/sh\nprintf '%s' \"$VP_FORCE_LTS_NODE\" > force-lts-node.txt\n", + &node_binary, + "#!/bin/sh\nprintf '%s\\n' \"$@\" > invocation.txt\nprintf '%s' \"$PATH\" > path.txt\n", ) .await .unwrap(); - tokio::fs::set_permissions(&vp_binary, std::fs::Permissions::from_mode(0o755)) + tokio::fs::set_permissions(&node_binary, std::fs::Permissions::from_mode(0o755)) .await .unwrap(); + let pnpm_entry = pnpm_bin.join("pnpm.cjs"); + tokio::fs::write(&pnpm_entry, "").await.unwrap(); + let node_runtime = + vite_js_runtime::JsRuntime::from_system(JsRuntimeType::Node, node_binary); - let output = run_vp_install(&version_dir, &vp_binary, &["install"]).await.unwrap(); + let output = + run_pnpm_install(&version_dir, &node_runtime, &pnpm_entry, &["install"]).await.unwrap(); assert!(output.status.success()); - let force_lts = - tokio::fs::read_to_string(version_dir.join("force-lts-node.txt")).await.unwrap(); - assert_eq!(force_lts, "1"); + let invocation = + tokio::fs::read_to_string(version_dir.join("invocation.txt")).await.unwrap(); + assert_eq!(invocation, format!("{}\ninstall\n", pnpm_entry.as_path().display())); + + let path = tokio::fs::read_to_string(version_dir.join("path.txt")).await.unwrap(); + let path_entries = env::split_paths(&path).collect::>(); + assert_eq!(path_entries[0], node_bin.as_path()); + assert_eq!(path_entries[1], pnpm_bin.as_path()); } #[test] diff --git a/crates/vite_setup/src/lib.rs b/crates/vite_setup/src/lib.rs index 9c2f2e9619..c2e5d4a494 100644 --- a/crates/vite_setup/src/lib.rs +++ b/crates/vite_setup/src/lib.rs @@ -26,6 +26,3 @@ pub const MAX_VERSIONS_KEEP: usize = 3; /// Platform-specific binary name for the `vp` CLI. pub const VP_BINARY_NAME: &str = if cfg!(windows) { "vp.exe" } else { "vp" }; - -/// Force the package-manager bootstrap to use the managed latest LTS Node.js runtime. -pub const FORCE_LTS_NODE_ENV: &str = "VP_FORCE_LTS_NODE";