From 1f7b1f6305dc958b7710614f5582e14c9ee68146 Mon Sep 17 00:00:00 2001 From: Liang Date: Tue, 1 Sep 2026 22:08:16 +0800 Subject: [PATCH 1/5] feat(env): disable pnpm runtime for managed node --- .../pnpm_runtime_management/snapshots.toml | 56 ++++++++++++++ .../pnpm_runtime_disabled_for_managed_node.md | 77 +++++++++++++++++++ ...pm_runtime_preserved_for_mixed_runtimes.md | 41 ++++++++++ ...runtime_preserved_for_system_first_node.md | 37 +++++++++ .../pnpm_runtime_management/system-bin/node | 2 + .../pnpm_runtime_management/system-bin/pnpm | 6 ++ crates/vp_global_cli/src/cli.rs | 39 ++++++++-- crates/vp_global_cli/src/commands/mod.rs | 1 + .../src/commands/pnpm_runtime.rs | 33 ++++++++ crates/vp_global_cli/src/shim/dispatch.rs | 36 ++++++++- crates/vp_global_cli/src/shim/exec.rs | 23 ++++-- crates/vp_pm_cli/src/dispatch.rs | 54 ++++++++++--- crates/vp_pm_cli/src/lib.rs | 3 +- crates/vp_pm_cli/src/resolution/resolve.rs | 10 +++ docs/guide/env.md | 2 + 15 files changed, 393 insertions(+), 27 deletions(-) create mode 100644 crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/pnpm_runtime_management/snapshots.toml create mode 100644 crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/pnpm_runtime_management/snapshots/pnpm_runtime_disabled_for_managed_node.md create mode 100644 crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/pnpm_runtime_management/snapshots/pnpm_runtime_preserved_for_mixed_runtimes.md create mode 100644 crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/pnpm_runtime_management/snapshots/pnpm_runtime_preserved_for_system_first_node.md create mode 100644 crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/pnpm_runtime_management/system-bin/node create mode 100644 crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/pnpm_runtime_management/system-bin/pnpm create mode 100644 crates/vp_global_cli/src/commands/pnpm_runtime.rs diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/pnpm_runtime_management/snapshots.toml b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/pnpm_runtime_management/snapshots.toml new file mode 100644 index 0000000000..adf71fe82a --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/pnpm_runtime_management/snapshots.toml @@ -0,0 +1,56 @@ +[[case]] +name = "pnpm_runtime_disabled_for_managed_node" +vp = "global" +seed-runtime = false +skip-platforms = ["windows"] +comment = "Vite+ owns Node.js runtime management independently from the selected pnpm binary." +steps = [ + { argv = ["vpt", "write-file", "package.json", "{\"name\":\"pnpm-runtime-management\",\"private\":true,\"packageManager\":\"pnpm@10.18.0\",\"devEngines\":{\"runtime\":{\"name\":\"node\",\"version\":\"20.18.0\",\"onFail\":\"download\"}}}\n"], snapshot = false }, + { argv = ["vpt", "write-file", "$VP_HOME/js_runtime/node/20.18.0/bin/node", "#!/bin/sh\n"], snapshot = false }, + { argv = ["vpt", "chmod", "+x", "$VP_HOME/js_runtime/node/20.18.0/bin/node"], snapshot = false }, + { argv = ["vpt", "chmod", "+x", "system-bin/pnpm"], snapshot = false }, + { argv = ["vp", "env", "on", "node"], snapshot = false }, + { argv = ["vp", "env", "off", "pnpm"], snapshot = false }, + { argv = ["pnpm", "install"], envs = [["PATH", "${VP_HOME}/bin${PATH_SEPARATOR}${workspace}/system-bin${PATH_SEPARATOR}${PATH}"], ["PNPM_CONFIG_RUNTIME", "from-user"]], comment = "direct system-first pnpm disables its duplicate Node.js runtime" }, + { argv = ["vp", "install"], envs = [["PATH", "${VP_HOME}/bin${PATH_SEPARATOR}${workspace}/system-bin${PATH_SEPARATOR}${PATH}"], ["PNPM_CONFIG_RUNTIME", "from-user"]], comment = "pnpm-backed vp install applies the same system-first policy" }, + { argv = ["vpt", "write-file", "$VP_HOME/package_manager/pnpm/10.18.0/pnpm/bin/pnpm", "#!/bin/sh\nif [ \"$1\" = \"--version\" ]; then printf '10.18.0\\n'; else printf 'PNPM_CONFIG_RUNTIME=%s\\n' \"${PNPM_CONFIG_RUNTIME-unset}\"; fi\n"], snapshot = false }, + { argv = ["vpt", "write-file", "$VP_HOME/package_manager/pnpm/10.18.0/pnpm/bin/pnpx", "#!/bin/sh\n"], snapshot = false }, + { argv = ["vpt", "chmod", "+x", "$VP_HOME/package_manager/pnpm/10.18.0/pnpm/bin/pnpm"], snapshot = false }, + { argv = ["vpt", "chmod", "+x", "$VP_HOME/package_manager/pnpm/10.18.0/pnpm/bin/pnpx"], snapshot = false }, + { argv = ["vp", "env", "on", "pnpm"], snapshot = false }, + { argv = ["pnpm", "install"], envs = [["PNPM_CONFIG_RUNTIME", "from-user"]], comment = "direct managed pnpm disables its duplicate Node.js runtime" }, + { argv = ["vp", "install"], envs = [["PNPM_CONFIG_RUNTIME", "from-user"]], comment = "pnpm-backed vp install applies the same managed-pnpm policy" }, +] + +[[case]] +name = "pnpm_runtime_preserved_for_mixed_runtimes" +vp = "global" +seed-runtime = false +skip-platforms = ["windows"] +comment = "Vite+ must not disable pnpm runtimes that it cannot manage." +steps = [ + { argv = ["vpt", "write-file", "package.json", "{\"name\":\"pnpm-runtime-management\",\"private\":true,\"packageManager\":\"pnpm@10.18.0\",\"devEngines\":{\"runtime\":[{\"name\":\"node\",\"version\":\"20.18.0\",\"onFail\":\"download\"},{\"name\":\"deno\",\"version\":\"2.0.0\",\"onFail\":\"download\"}]}}\n"], snapshot = false }, + { argv = ["vpt", "write-file", "$VP_HOME/js_runtime/node/20.18.0/bin/node", "#!/bin/sh\n"], snapshot = false }, + { argv = ["vpt", "chmod", "+x", "$VP_HOME/js_runtime/node/20.18.0/bin/node"], snapshot = false }, + { argv = ["vpt", "chmod", "+x", "system-bin/pnpm"], snapshot = false }, + { argv = ["vp", "env", "on", "node"], snapshot = false }, + { argv = ["vp", "env", "off", "pnpm"], snapshot = false }, + { argv = ["pnpm", "install"], envs = [["PATH", "${VP_HOME}/bin${PATH_SEPARATOR}${workspace}/system-bin${PATH_SEPARATOR}${PATH}"], ["PNPM_CONFIG_RUNTIME", "from-user"]], comment = "direct pnpm preserves runtime management for a mixed declaration" }, + { argv = ["vp", "install"], envs = [["PATH", "${VP_HOME}/bin${PATH_SEPARATOR}${workspace}/system-bin${PATH_SEPARATOR}${PATH}"], ["PNPM_CONFIG_RUNTIME", "from-user"]], comment = "vp install preserves the same mixed-runtime setting" }, +] + +[[case]] +name = "pnpm_runtime_preserved_for_system_first_node" +vp = "global" +seed-runtime = false +skip-platforms = ["windows"] +comment = "pnpm keeps runtime ownership when Vite+ does not manage Node.js." +steps = [ + { argv = ["vpt", "write-file", "package.json", "{\"name\":\"pnpm-runtime-management\",\"private\":true,\"packageManager\":\"pnpm@10.18.0\",\"devEngines\":{\"runtime\":{\"name\":\"node\",\"version\":\"20.18.0\",\"onFail\":\"download\"}}}\n"], snapshot = false }, + { argv = ["vpt", "chmod", "+x", "system-bin/node"], snapshot = false }, + { argv = ["vpt", "chmod", "+x", "system-bin/pnpm"], snapshot = false }, + { argv = ["vp", "env", "off", "node"], snapshot = false }, + { argv = ["vp", "env", "off", "pnpm"], snapshot = false }, + { argv = ["pnpm", "install"], envs = [["PATH", "${VP_HOME}/bin${PATH_SEPARATOR}${workspace}/system-bin${PATH_SEPARATOR}${PATH}"], ["PNPM_CONFIG_RUNTIME", "from-user"]], comment = "direct pnpm preserves runtime management with system-first Node.js" }, + { argv = ["vp", "install"], envs = [["PATH", "${VP_HOME}/bin${PATH_SEPARATOR}${workspace}/system-bin${PATH_SEPARATOR}${PATH}"], ["PNPM_CONFIG_RUNTIME", "from-user"]], comment = "vp install preserves the same system-first Node.js policy" }, +] diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/pnpm_runtime_management/snapshots/pnpm_runtime_disabled_for_managed_node.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/pnpm_runtime_management/snapshots/pnpm_runtime_disabled_for_managed_node.md new file mode 100644 index 0000000000..3356583016 --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/pnpm_runtime_management/snapshots/pnpm_runtime_disabled_for_managed_node.md @@ -0,0 +1,77 @@ +# pnpm_runtime_disabled_for_managed_node + +Vite+ owns Node.js runtime management independently from the selected pnpm binary. + +## `vpt write-file package.json '{"name":"pnpm-runtime-management","private":true,"packageManager":"pnpm@10.18.0","devEngines":{"runtime":{"name":"node","version":"20.18.0","onFail":"download"}}} +'` + + +## `vpt write-file $VP_HOME/js_runtime/node/20.18.0/bin/node '#'\!'/bin/sh +'` + + +## `vpt chmod +x $VP_HOME/js_runtime/node/20.18.0/bin/node` + + +## `vpt chmod +x system-bin/pnpm` + + +## `vp env on node` + + +## `vp env off pnpm` + + +## `PATH=${VP_HOME}/bin${PATH_SEPARATOR}${workspace}/system-bin${PATH_SEPARATOR}${PATH} PNPM_CONFIG_RUNTIME=from-user pnpm install` + +direct system-first pnpm disables its duplicate Node.js runtime + +``` +PNPM_CONFIG_RUNTIME=false +``` + +## `PATH=${VP_HOME}/bin${PATH_SEPARATOR}${workspace}/system-bin${PATH_SEPARATOR}${PATH} PNPM_CONFIG_RUNTIME=from-user vp install` + +pnpm-backed vp install applies the same system-first policy + +``` +VITE+ - The Unified Toolchain for the Web + +PNPM_CONFIG_RUNTIME=false +``` + +## `vpt write-file $VP_HOME/package_manager/pnpm/10.18.0/pnpm/bin/pnpm '#'\!'/bin/sh +if [ "$1" = "--version" ]; then printf '\''10.18.0\n'\''; else printf '\''PNPM_CONFIG_RUNTIME=%s\n'\'' "${PNPM_CONFIG_RUNTIME-unset}"; fi +'` + + +## `vpt write-file $VP_HOME/package_manager/pnpm/10.18.0/pnpm/bin/pnpx '#'\!'/bin/sh +'` + + +## `vpt chmod +x $VP_HOME/package_manager/pnpm/10.18.0/pnpm/bin/pnpm` + + +## `vpt chmod +x $VP_HOME/package_manager/pnpm/10.18.0/pnpm/bin/pnpx` + + +## `vp env on pnpm` + + +## `PNPM_CONFIG_RUNTIME=from-user pnpm install` + +direct managed pnpm disables its duplicate Node.js runtime + +``` +PNPM_CONFIG_RUNTIME=false +``` + +## `PNPM_CONFIG_RUNTIME=from-user vp install` + +pnpm-backed vp install applies the same managed-pnpm policy + +``` +VITE+ - The Unified Toolchain for the Web + +PNPM_CONFIG_RUNTIME=false +``` diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/pnpm_runtime_management/snapshots/pnpm_runtime_preserved_for_mixed_runtimes.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/pnpm_runtime_management/snapshots/pnpm_runtime_preserved_for_mixed_runtimes.md new file mode 100644 index 0000000000..76b5a51f48 --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/pnpm_runtime_management/snapshots/pnpm_runtime_preserved_for_mixed_runtimes.md @@ -0,0 +1,41 @@ +# pnpm_runtime_preserved_for_mixed_runtimes + +Vite+ must not disable pnpm runtimes that it cannot manage. + +## `vpt write-file package.json '{"name":"pnpm-runtime-management","private":true,"packageManager":"pnpm@10.18.0","devEngines":{"runtime":[{"name":"node","version":"20.18.0","onFail":"download"},{"name":"deno","version":"2.0.0","onFail":"download"}]}} +'` + + +## `vpt write-file $VP_HOME/js_runtime/node/20.18.0/bin/node '#'\!'/bin/sh +'` + + +## `vpt chmod +x $VP_HOME/js_runtime/node/20.18.0/bin/node` + + +## `vpt chmod +x system-bin/pnpm` + + +## `vp env on node` + + +## `vp env off pnpm` + + +## `PATH=${VP_HOME}/bin${PATH_SEPARATOR}${workspace}/system-bin${PATH_SEPARATOR}${PATH} PNPM_CONFIG_RUNTIME=from-user pnpm install` + +direct pnpm preserves runtime management for a mixed declaration + +``` +PNPM_CONFIG_RUNTIME=from-user +``` + +## `PATH=${VP_HOME}/bin${PATH_SEPARATOR}${workspace}/system-bin${PATH_SEPARATOR}${PATH} PNPM_CONFIG_RUNTIME=from-user vp install` + +vp install preserves the same mixed-runtime setting + +``` +VITE+ - The Unified Toolchain for the Web + +PNPM_CONFIG_RUNTIME=from-user +``` diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/pnpm_runtime_management/snapshots/pnpm_runtime_preserved_for_system_first_node.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/pnpm_runtime_management/snapshots/pnpm_runtime_preserved_for_system_first_node.md new file mode 100644 index 0000000000..e981818abe --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/pnpm_runtime_management/snapshots/pnpm_runtime_preserved_for_system_first_node.md @@ -0,0 +1,37 @@ +# pnpm_runtime_preserved_for_system_first_node + +pnpm keeps runtime ownership when Vite+ does not manage Node.js. + +## `vpt write-file package.json '{"name":"pnpm-runtime-management","private":true,"packageManager":"pnpm@10.18.0","devEngines":{"runtime":{"name":"node","version":"20.18.0","onFail":"download"}}} +'` + + +## `vpt chmod +x system-bin/node` + + +## `vpt chmod +x system-bin/pnpm` + + +## `vp env off node` + + +## `vp env off pnpm` + + +## `PATH=${VP_HOME}/bin${PATH_SEPARATOR}${workspace}/system-bin${PATH_SEPARATOR}${PATH} PNPM_CONFIG_RUNTIME=from-user pnpm install` + +direct pnpm preserves runtime management with system-first Node.js + +``` +PNPM_CONFIG_RUNTIME=from-user +``` + +## `PATH=${VP_HOME}/bin${PATH_SEPARATOR}${workspace}/system-bin${PATH_SEPARATOR}${PATH} PNPM_CONFIG_RUNTIME=from-user vp install` + +vp install preserves the same system-first Node.js policy + +``` +VITE+ - The Unified Toolchain for the Web + +PNPM_CONFIG_RUNTIME=from-user +``` diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/pnpm_runtime_management/system-bin/node b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/pnpm_runtime_management/system-bin/node new file mode 100644 index 0000000000..c743bd241e --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/pnpm_runtime_management/system-bin/node @@ -0,0 +1,2 @@ +#!/bin/sh +printf 'v20.18.0\n' diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/pnpm_runtime_management/system-bin/pnpm b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/pnpm_runtime_management/system-bin/pnpm new file mode 100644 index 0000000000..4ccecfce59 --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/pnpm_runtime_management/system-bin/pnpm @@ -0,0 +1,6 @@ +#!/bin/sh +if [ "$1" = "--version" ]; then + printf '10.18.0\n' +else + printf 'PNPM_CONFIG_RUNTIME=%s\n' "${PNPM_CONFIG_RUNTIME-unset}" +fi diff --git a/crates/vp_global_cli/src/cli.rs b/crates/vp_global_cli/src/cli.rs index cf61829a7e..05af145040 100644 --- a/crates/vp_global_cli/src/cli.rs +++ b/crates/vp_global_cli/src/cli.rs @@ -3,7 +3,11 @@ //! This module defines the CLI structure using clap and routes commands //! to their appropriate handlers. -use std::{collections::HashSet, ffi::OsStr, process::ExitStatus}; +use std::{ + collections::{BTreeMap, HashSet}, + ffi::OsStr, + process::ExitStatus, +}; use clap::{CommandFactory, FromArgMatches, Parser, Subcommand}; use clap_complete::ArgValueCompleter; @@ -675,22 +679,45 @@ async fn run_package_manager_command( commands::prepend_js_runtime_to_path_env(&cwd).await?; let selected = commands::env::package_manager::resolve_current_spec(&cwd).await?; + let config = commands::env::config::load_config().await?; + let mut package_manager_env = BTreeMap::new(); + if matches!(&command, PackageManagerCommand::Install(_)) + && selected.as_ref().is_some_and(|selected| { + selected.package_manager_type == vp_pm_cli::PackageManagerType::Pnpm + }) + && commands::pnpm_runtime::should_disable(&cwd, config.node_shim_mode).await? + { + package_manager_env.insert( + commands::pnpm_runtime::PNPM_CONFIG_RUNTIME.to_string(), + commands::pnpm_runtime::PNPM_CONFIG_RUNTIME_DISABLED.to_string(), + ); + } let result = if let Some(selected) = selected.as_ref() - && commands::env::config::load_config() - .await? - .package_manager_shim_mode_for(selected.package_manager_type) + && config.package_manager_shim_mode_for(selected.package_manager_type) == commands::env::config::ShimMode::SystemFirst && let Some(system_path) = crate::shim::dispatch::find_system_tool(&selected.package_manager_type.to_string()) && let Some(manager) = system_package_manager(selected.package_manager_type, &system_path).await { - vp_pm_cli::dispatch_with_resolved_package_manager(&cwd, command, manager).await? + vp_pm_cli::dispatch_with_resolved_package_manager_and_env( + &cwd, + command, + manager, + &package_manager_env, + ) + .await? } else { let selected = commands::env::package_manager::resolve_current(&cwd).await?; match selected { Some(selected) => { - vp_pm_cli::dispatch_with_package_manager(&cwd, command, &selected).await? + vp_pm_cli::dispatch_with_package_manager_and_env( + &cwd, + command, + &selected, + &package_manager_env, + ) + .await? } None => vp_pm_cli::dispatch_with_metadata(&cwd, command).await?, } diff --git a/crates/vp_global_cli/src/commands/mod.rs b/crates/vp_global_cli/src/commands/mod.rs index 007d351332..7d03b41a8d 100644 --- a/crates/vp_global_cli/src/commands/mod.rs +++ b/crates/vp_global_cli/src/commands/mod.rs @@ -176,6 +176,7 @@ pub mod config; pub mod create; pub mod hooks; pub mod migrate; +pub(crate) mod pnpm_runtime; pub mod staged; pub mod toolchain; pub mod version; diff --git a/crates/vp_global_cli/src/commands/pnpm_runtime.rs b/crates/vp_global_cli/src/commands/pnpm_runtime.rs new file mode 100644 index 0000000000..cab98f89f3 --- /dev/null +++ b/crates/vp_global_cli/src/commands/pnpm_runtime.rs @@ -0,0 +1,33 @@ +use vp_shared::PackageJson; +use vt_path::AbsolutePath; + +use super::env::config::ShimMode; +use crate::error::Error; + +pub(crate) const PNPM_CONFIG_RUNTIME: &str = "PNPM_CONFIG_RUNTIME"; +pub(crate) const PNPM_CONFIG_RUNTIME_DISABLED: &str = "false"; + +pub(crate) async fn should_disable( + cwd: &AbsolutePath, + node_shim_mode: ShimMode, +) -> Result { + if node_shim_mode != ShimMode::Managed { + return Ok(false); + } + + let workspace = match vt_workspace::find_workspace_root(cwd) { + Ok((workspace, _)) => workspace, + Err(vt_workspace::Error::PackageJsonNotFound(_)) => return Ok(false), + Err(error) => return Err(error.into()), + }; + let content = tokio::fs::read_to_string(workspace.path.join("package.json")).await?; + let package_json: PackageJson = serde_json::from_str(&content)?; + let Some(runtime) = package_json.dev_engines.and_then(|engines| engines.runtime) else { + return Ok(false); + }; + let entries = runtime.entries(); + + // pnpm's runtime opt-out covers Node.js, Bun, and Deno together, so Vite+ + // can use it only when every declared runtime is one Vite+ manages. + Ok(!entries.is_empty() && entries.iter().all(|entry| entry.name == "node")) +} diff --git a/crates/vp_global_cli/src/shim/dispatch.rs b/crates/vp_global_cli/src/shim/dispatch.rs index 0c67e85c1b..c0d31ac407 100644 --- a/crates/vp_global_cli/src/shim/dispatch.rs +++ b/crates/vp_global_cli/src/shim/dispatch.rs @@ -23,6 +23,7 @@ use crate::{ package_metadata::PackageMetadata, }, global::install::is_protected_shim, + pnpm_runtime, }, error::Error, }; @@ -737,6 +738,37 @@ pub async fn dispatch(tool: &str, args: &[String]) -> i32 { // Check shim mode from config let shim_mode = load_shim_mode(tool).await; + let disable_pnpm_runtime = + if PackageManagerType::from_tool(tool) == Some(PackageManagerType::Pnpm) { + let cwd = match current_dir() { + Ok(path) => path, + Err(error) => { + eprintln!("vp: Failed to get current directory: {error}"); + return 1; + } + }; + let node_shim_mode = match config::load_config().await { + Ok(config) => config.node_shim_mode, + Err(error) => { + eprintln!("vp: Failed to load Node.js shim mode: {error}"); + return 1; + } + }; + match pnpm_runtime::should_disable(&cwd, node_shim_mode).await { + Ok(disable) => disable, + Err(error) => { + eprintln!("vp: Failed to resolve pnpm runtime management: {error}"); + return 1; + } + } + } else { + false + }; + let pnpm_runtime_env = if disable_pnpm_runtime { + &[(pnpm_runtime::PNPM_CONFIG_RUNTIME, pnpm_runtime::PNPM_CONFIG_RUNTIME_DISABLED)][..] + } else { + &[] + }; if shim_mode == ShimMode::SystemFirst { tracing::debug!("system-first mode enabled"); // In system-first mode, try to find system tool first @@ -764,7 +796,7 @@ pub async fn dispatch(tool: &str, args: &[String]) -> i32 { std::env::set_var(env_vars::VP_BYPASS, bypass_val); } } - return exec::exec_tool(&system_path, args); + return exec::exec_tool_with_env(&system_path, args, pnpm_runtime_env); } // Fall through to managed if system not found } @@ -930,7 +962,7 @@ pub async fn dispatch(tool: &str, args: &[String]) -> i32 { } // Execute the tool (normal path — exec replaces process on Unix) - exec::exec_tool(&tool_path, args) + exec::exec_tool_with_env(&tool_path, args, pnpm_runtime_env) } fn node_prefix_from_binary(node_path: &AbsolutePath) -> AbsolutePathBuf { diff --git a/crates/vp_global_cli/src/shim/exec.rs b/crates/vp_global_cli/src/shim/exec.rs index 2fe5d0b7f9..fdca5e7b2e 100644 --- a/crates/vp_global_cli/src/shim/exec.rs +++ b/crates/vp_global_cli/src/shim/exec.rs @@ -24,8 +24,12 @@ fn sync_child_pwd(cmd: &mut std::process::Command) { /// Unlike `exec_tool()`, this does NOT replace the current process on Unix, /// allowing the caller to run code after the tool exits. pub fn spawn_tool(path: &AbsolutePath, args: &[String]) -> i32 { + spawn_tool_with_env(path, args, &[]) +} + +fn spawn_tool_with_env(path: &AbsolutePath, args: &[String], env: &[(&str, &str)]) -> i32 { let mut cmd = std::process::Command::new(path.as_path()); - cmd.args(args); + cmd.args(args).envs(env.iter().copied()); sync_child_pwd(&mut cmd); match cmd.status() { Ok(status) => exit_code_from_status(status), @@ -40,24 +44,29 @@ pub fn spawn_tool(path: &AbsolutePath, args: &[String]) -> i32 { /// /// Returns an exit code on Windows or if exec fails on Unix. pub fn exec_tool(path: &AbsolutePath, args: &[String]) -> i32 { + exec_tool_with_env(path, args, &[]) +} + +/// Execute a tool with child-only environment overrides. +pub fn exec_tool_with_env(path: &AbsolutePath, args: &[String], env: &[(&str, &str)]) -> i32 { #[cfg(unix)] { - exec_unix(path, args) + exec_unix(path, args, env) } #[cfg(windows)] { - exec_windows(path, args) + exec_windows(path, args, env) } } /// Unix: Use exec to replace the current process. #[cfg(unix)] -fn exec_unix(path: &AbsolutePath, args: &[String]) -> i32 { +fn exec_unix(path: &AbsolutePath, args: &[String], env: &[(&str, &str)]) -> i32 { use std::os::unix::process::CommandExt; let mut cmd = std::process::Command::new(path.as_path()); - cmd.args(args); + cmd.args(args).envs(env.iter().copied()); sync_child_pwd(&mut cmd); // exec replaces the current process - this only returns on error @@ -68,6 +77,6 @@ fn exec_unix(path: &AbsolutePath, args: &[String]) -> i32 { /// Windows: Spawn the process and wait for completion. #[cfg(windows)] -fn exec_windows(path: &AbsolutePath, args: &[String]) -> i32 { - spawn_tool(path, args) +fn exec_windows(path: &AbsolutePath, args: &[String], env: &[(&str, &str)]) -> i32 { + spawn_tool_with_env(path, args, env) } diff --git a/crates/vp_pm_cli/src/dispatch.rs b/crates/vp_pm_cli/src/dispatch.rs index be89973c4e..e9e6d7176c 100644 --- a/crates/vp_pm_cli/src/dispatch.rs +++ b/crates/vp_pm_cli/src/dispatch.rs @@ -3,7 +3,7 @@ //! Callers must perform any environment setup (PATH adjustments, runtime //! download) before invoking [`dispatch`]. -use std::process::ExitStatus; +use std::{collections::BTreeMap, process::ExitStatus}; use vt_path::AbsolutePath; @@ -13,7 +13,7 @@ use crate::{ download_package_manager, error::Error, helpers::build_package_manager_or_npm_default, - resolution::{DlxArgs, run_resolution}, + resolution::{DlxArgs, Resolution, run_resolution}, }; #[derive(Debug)] @@ -39,7 +39,7 @@ pub async fn dispatch_with_metadata( cwd: &AbsolutePath, command: PackageManagerCommand, ) -> Result { - dispatch_with_manager(cwd, command, ManagerSource::Detect).await + dispatch_with_manager(cwd, command, ManagerSource::Detect, None).await } pub async fn dispatch_with_package_manager( @@ -47,7 +47,17 @@ pub async fn dispatch_with_package_manager( command: PackageManagerCommand, package_manager: &EnvironmentPackageManagerResolution, ) -> Result { - dispatch_with_manager(cwd, command, ManagerSource::Environment(package_manager)).await + dispatch_with_manager(cwd, command, ManagerSource::Environment(package_manager), None).await +} + +pub async fn dispatch_with_package_manager_and_env( + cwd: &AbsolutePath, + command: PackageManagerCommand, + package_manager: &EnvironmentPackageManagerResolution, + env: &BTreeMap, +) -> Result { + dispatch_with_manager(cwd, command, ManagerSource::Environment(package_manager), Some(env)) + .await } pub async fn dispatch_with_resolved_package_manager( @@ -55,25 +65,38 @@ pub async fn dispatch_with_resolved_package_manager( command: PackageManagerCommand, manager: PackageManager, ) -> Result { - dispatch_with_manager(cwd, command, ManagerSource::Resolved(manager)).await + dispatch_with_manager(cwd, command, ManagerSource::Resolved(manager), None).await +} + +pub async fn dispatch_with_resolved_package_manager_and_env( + cwd: &AbsolutePath, + command: PackageManagerCommand, + manager: PackageManager, + env: &BTreeMap, +) -> Result { + dispatch_with_manager(cwd, command, ManagerSource::Resolved(manager), Some(env)).await } async fn dispatch_with_manager( cwd: &AbsolutePath, command: PackageManagerCommand, source: ManagerSource<'_>, + env: Option<&BTreeMap>, ) -> Result { let render_diagnostics = command.should_render_diagnostics(); let command = match command { PackageManagerCommand::Dlx(args) => { let manager = match source { - ManagerSource::Detect => return dispatch_dlx(cwd, args, render_diagnostics).await, + ManagerSource::Detect => { + return dispatch_dlx(cwd, args, render_diagnostics, env).await; + } ManagerSource::Environment(package_manager) => { build_selected_package_manager(package_manager).await? } ManagerSource::Resolved(manager) => manager, }; - let resolution = PackageManagerCommand::Dlx(args).resolve_for_manager(&manager)?; + let resolution = + with_env(PackageManagerCommand::Dlx(args).resolve_for_manager(&manager)?, env); let status = run_resolution(cwd, resolution, render_diagnostics).await?; return Ok(DispatchResult { status, why_hint_packages: None }); } @@ -89,7 +112,7 @@ async fn dispatch_with_manager( }; let package_manager = manager.client; let why_hint_packages = command.why_hint_packages(package_manager).map(<[String]>::to_vec); - let resolution = command.resolve_for_manager(&manager)?; + let resolution = with_env(command.resolve_for_manager(&manager)?, env); let status = run_resolution(cwd, resolution, render_diagnostics).await?; Ok(DispatchResult { status, why_hint_packages }) } @@ -107,20 +130,29 @@ async fn build_selected_package_manager( Ok(PackageManager::from_install_dir(package_manager.package_manager_type, version, install_dir)) } +fn with_env(mut resolution: Resolution, env: Option<&BTreeMap>) -> Resolution { + if let Some(env) = env { + resolution.extend_env(env); + } + resolution +} + async fn dispatch_dlx( cwd: &AbsolutePath, args: DlxArgs, render_diagnostics: bool, + env: Option<&BTreeMap>, ) -> Result { match PackageManager::builder(cwd).build_with_default().await { Ok(manager) => { - let resolution = PackageManagerCommand::Dlx(args).resolve_for_manager(&manager)?; + let resolution = + with_env(PackageManagerCommand::Dlx(args).resolve_for_manager(&manager)?, env); let status = run_resolution(cwd, resolution, render_diagnostics).await?; Ok(DispatchResult { status, why_hint_packages: None }) } Err(vp_error::Error::WorkspaceError(vt_workspace::Error::PackageJsonNotFound(_))) => { - let status = - run_resolution(cwd, args.resolve_npx_fallback(), render_diagnostics).await?; + let resolution = with_env(args.resolve_npx_fallback(), env); + let status = run_resolution(cwd, resolution, render_diagnostics).await?; Ok(DispatchResult { status, why_hint_packages: None }) } Err(error) => Err(Error::Install(error)), diff --git a/crates/vp_pm_cli/src/lib.rs b/crates/vp_pm_cli/src/lib.rs index fe0ec60df7..9fe6707b11 100644 --- a/crates/vp_pm_cli/src/lib.rs +++ b/crates/vp_pm_cli/src/lib.rs @@ -21,7 +21,8 @@ pub use cli::{ManagedGlobalCommand, PackageManagerCommand, PmCommand}; pub use config::npm_registry; pub use dispatch::{ DispatchResult, dispatch, dispatch_with_metadata, dispatch_with_package_manager, - dispatch_with_resolved_package_manager, + dispatch_with_package_manager_and_env, dispatch_with_resolved_package_manager, + dispatch_with_resolved_package_manager_and_env, }; pub use error::Error; pub use package_manager::{ diff --git a/crates/vp_pm_cli/src/resolution/resolve.rs b/crates/vp_pm_cli/src/resolution/resolve.rs index 896d1a461f..6c342f416c 100644 --- a/crates/vp_pm_cli/src/resolution/resolve.rs +++ b/crates/vp_pm_cli/src/resolution/resolve.rs @@ -1,3 +1,5 @@ +use std::collections::BTreeMap; + use semver::Version; use crate::{ @@ -13,6 +15,14 @@ pub(crate) struct Resolution { pub(crate) diagnostics: Diagnostics, } +impl Resolution { + pub(crate) fn extend_env(&mut self, env: &BTreeMap) { + if let CommandResolution::Run(command) = &mut self.outcome { + command.env.extend(env.clone()); + } + } +} + pub(crate) trait Resolve: PackageManagerDialect { fn resolve(&self, args: &A, diag: &mut Diagnostics) -> CommandResolution; } diff --git a/docs/guide/env.md b/docs/guide/env.md index 4a7e5d6142..32950f1c61 100644 --- a/docs/guide/env.md +++ b/docs/guide/env.md @@ -74,6 +74,8 @@ vp env off This switches both components to system-first mode. Vite+ prefers system tools and falls back to managed installations. Mixed configurations compose: a system package-manager launcher receives the Node.js selected by the Node mode. +When Node.js is managed and a pnpm project declares only Node.js in `devEngines.runtime`, Vite+ passes `PNPM_CONFIG_RUNTIME=false` to pnpm so it does not install a duplicate runtime. This also applies when pnpm is system-first because the Node.js and package-manager modes are independent. Vite+ leaves pnpm runtime management unchanged for Bun, Deno, and mixed runtime declarations. + Using `pm` records the selected mode for all currently supported package managers and replaces their individual choices. An unscoped `on` or `off` does the same while also changing Node.js. A family without a recorded mode remains undecided until its shim is first used or an `on` / `off` command configures it. ## Commands From 53dee88f3a389bc125e7e0ed0fbc7c15a41da9de Mon Sep 17 00:00:00 2001 From: Liang Date: Tue, 1 Sep 2026 22:43:50 +0800 Subject: [PATCH 2/5] fix(env): preserve pnpm runtime boundaries --- .../pnpm_runtime_management/snapshots.toml | 70 +++++++++++++++++-- .../pnpm_runtime_disabled_for_managed_node.md | 21 ++++-- ...m_runtime_preserved_for_engines_runtime.md | 41 +++++++++++ ...pm_runtime_preserved_for_mixed_runtimes.md | 2 +- ...runtime_preserved_for_system_first_node.md | 2 +- ..._runtime_preserved_for_workspace_member.md | 22 ++++++ ...runtime_uses_explicit_working_directory.md | 38 ++++++++++ .../src/commands/pnpm_runtime.rs | 69 +++++++++++++++--- crates/vp_global_cli/src/shim/dispatch.rs | 2 +- 9 files changed, 242 insertions(+), 25 deletions(-) create mode 100644 crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/pnpm_runtime_management/snapshots/pnpm_runtime_preserved_for_engines_runtime.md create mode 100644 crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/pnpm_runtime_management/snapshots/pnpm_runtime_preserved_for_workspace_member.md create mode 100644 crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/pnpm_runtime_management/snapshots/pnpm_runtime_uses_explicit_working_directory.md diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/pnpm_runtime_management/snapshots.toml b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/pnpm_runtime_management/snapshots.toml index adf71fe82a..0105b853cd 100644 --- a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/pnpm_runtime_management/snapshots.toml +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/pnpm_runtime_management/snapshots.toml @@ -5,7 +5,7 @@ seed-runtime = false skip-platforms = ["windows"] comment = "Vite+ owns Node.js runtime management independently from the selected pnpm binary." steps = [ - { argv = ["vpt", "write-file", "package.json", "{\"name\":\"pnpm-runtime-management\",\"private\":true,\"packageManager\":\"pnpm@10.18.0\",\"devEngines\":{\"runtime\":{\"name\":\"node\",\"version\":\"20.18.0\",\"onFail\":\"download\"}}}\n"], snapshot = false }, + { argv = ["vpt", "write-file", "package.json", "{\"name\":\"pnpm-runtime-management\",\"private\":true,\"packageManager\":\"pnpm@11.1.0\",\"devEngines\":{\"runtime\":{\"name\":\"node\",\"version\":\"20.18.0\",\"onFail\":\"download\"}}}\n"], snapshot = false }, { argv = ["vpt", "write-file", "$VP_HOME/js_runtime/node/20.18.0/bin/node", "#!/bin/sh\n"], snapshot = false }, { argv = ["vpt", "chmod", "+x", "$VP_HOME/js_runtime/node/20.18.0/bin/node"], snapshot = false }, { argv = ["vpt", "chmod", "+x", "system-bin/pnpm"], snapshot = false }, @@ -13,12 +13,13 @@ steps = [ { argv = ["vp", "env", "off", "pnpm"], snapshot = false }, { argv = ["pnpm", "install"], envs = [["PATH", "${VP_HOME}/bin${PATH_SEPARATOR}${workspace}/system-bin${PATH_SEPARATOR}${PATH}"], ["PNPM_CONFIG_RUNTIME", "from-user"]], comment = "direct system-first pnpm disables its duplicate Node.js runtime" }, { argv = ["vp", "install"], envs = [["PATH", "${VP_HOME}/bin${PATH_SEPARATOR}${workspace}/system-bin${PATH_SEPARATOR}${PATH}"], ["PNPM_CONFIG_RUNTIME", "from-user"]], comment = "pnpm-backed vp install applies the same system-first policy" }, - { argv = ["vpt", "write-file", "$VP_HOME/package_manager/pnpm/10.18.0/pnpm/bin/pnpm", "#!/bin/sh\nif [ \"$1\" = \"--version\" ]; then printf '10.18.0\\n'; else printf 'PNPM_CONFIG_RUNTIME=%s\\n' \"${PNPM_CONFIG_RUNTIME-unset}\"; fi\n"], snapshot = false }, - { argv = ["vpt", "write-file", "$VP_HOME/package_manager/pnpm/10.18.0/pnpm/bin/pnpx", "#!/bin/sh\n"], snapshot = false }, - { argv = ["vpt", "chmod", "+x", "$VP_HOME/package_manager/pnpm/10.18.0/pnpm/bin/pnpm"], snapshot = false }, - { argv = ["vpt", "chmod", "+x", "$VP_HOME/package_manager/pnpm/10.18.0/pnpm/bin/pnpx"], snapshot = false }, + { argv = ["vpt", "write-file", "$VP_HOME/package_manager/pnpm/11.1.0/pnpm/bin/pnpm", "#!/bin/sh\nif [ \"$1\" = \"--version\" ]; then printf '11.1.0\\n'; else printf 'PNPM_CONFIG_RUNTIME=%s\\n' \"${PNPM_CONFIG_RUNTIME-unset}\"; fi\n"], snapshot = false }, + { argv = ["vpt", "write-file", "$VP_HOME/package_manager/pnpm/11.1.0/pnpm/bin/pnpx", "#!/bin/sh\nprintf 'PNPM_CONFIG_RUNTIME=%s\\n' \"${PNPM_CONFIG_RUNTIME-unset}\"\n"], snapshot = false }, + { argv = ["vpt", "chmod", "+x", "$VP_HOME/package_manager/pnpm/11.1.0/pnpm/bin/pnpm"], snapshot = false }, + { argv = ["vpt", "chmod", "+x", "$VP_HOME/package_manager/pnpm/11.1.0/pnpm/bin/pnpx"], snapshot = false }, { argv = ["vp", "env", "on", "pnpm"], snapshot = false }, { argv = ["pnpm", "install"], envs = [["PNPM_CONFIG_RUNTIME", "from-user"]], comment = "direct managed pnpm disables its duplicate Node.js runtime" }, + { argv = ["pnpx", "package"], envs = [["PNPM_CONFIG_RUNTIME", "from-user"]], comment = "the pnpx alias receives the same child-only override" }, { argv = ["vp", "install"], envs = [["PNPM_CONFIG_RUNTIME", "from-user"]], comment = "pnpm-backed vp install applies the same managed-pnpm policy" }, ] @@ -29,7 +30,7 @@ seed-runtime = false skip-platforms = ["windows"] comment = "Vite+ must not disable pnpm runtimes that it cannot manage." steps = [ - { argv = ["vpt", "write-file", "package.json", "{\"name\":\"pnpm-runtime-management\",\"private\":true,\"packageManager\":\"pnpm@10.18.0\",\"devEngines\":{\"runtime\":[{\"name\":\"node\",\"version\":\"20.18.0\",\"onFail\":\"download\"},{\"name\":\"deno\",\"version\":\"2.0.0\",\"onFail\":\"download\"}]}}\n"], snapshot = false }, + { argv = ["vpt", "write-file", "package.json", "{\"name\":\"pnpm-runtime-management\",\"private\":true,\"packageManager\":\"pnpm@11.1.0\",\"devEngines\":{\"runtime\":[{\"name\":\"node\",\"version\":\"20.18.0\",\"onFail\":\"download\"},{\"name\":\"deno\",\"version\":\"2.0.0\",\"onFail\":\"download\"}]}}\n"], snapshot = false }, { argv = ["vpt", "write-file", "$VP_HOME/js_runtime/node/20.18.0/bin/node", "#!/bin/sh\n"], snapshot = false }, { argv = ["vpt", "chmod", "+x", "$VP_HOME/js_runtime/node/20.18.0/bin/node"], snapshot = false }, { argv = ["vpt", "chmod", "+x", "system-bin/pnpm"], snapshot = false }, @@ -46,7 +47,7 @@ seed-runtime = false skip-platforms = ["windows"] comment = "pnpm keeps runtime ownership when Vite+ does not manage Node.js." steps = [ - { argv = ["vpt", "write-file", "package.json", "{\"name\":\"pnpm-runtime-management\",\"private\":true,\"packageManager\":\"pnpm@10.18.0\",\"devEngines\":{\"runtime\":{\"name\":\"node\",\"version\":\"20.18.0\",\"onFail\":\"download\"}}}\n"], snapshot = false }, + { argv = ["vpt", "write-file", "package.json", "{\"name\":\"pnpm-runtime-management\",\"private\":true,\"packageManager\":\"pnpm@11.1.0\",\"devEngines\":{\"runtime\":{\"name\":\"node\",\"version\":\"20.18.0\",\"onFail\":\"download\"}}}\n"], snapshot = false }, { argv = ["vpt", "chmod", "+x", "system-bin/node"], snapshot = false }, { argv = ["vpt", "chmod", "+x", "system-bin/pnpm"], snapshot = false }, { argv = ["vp", "env", "off", "node"], snapshot = false }, @@ -54,3 +55,58 @@ steps = [ { argv = ["pnpm", "install"], envs = [["PATH", "${VP_HOME}/bin${PATH_SEPARATOR}${workspace}/system-bin${PATH_SEPARATOR}${PATH}"], ["PNPM_CONFIG_RUNTIME", "from-user"]], comment = "direct pnpm preserves runtime management with system-first Node.js" }, { argv = ["vp", "install"], envs = [["PATH", "${VP_HOME}/bin${PATH_SEPARATOR}${workspace}/system-bin${PATH_SEPARATOR}${PATH}"], ["PNPM_CONFIG_RUNTIME", "from-user"]], comment = "vp install preserves the same system-first Node.js policy" }, ] + +[[case]] +name = "pnpm_runtime_preserved_for_engines_runtime" +vp = "global" +seed-runtime = false +skip-platforms = ["windows"] +comment = "pnpm runtime declarations in engines are part of the same process-wide policy." +steps = [ + { argv = ["vpt", "write-file", "package.json", "{\"name\":\"pnpm-runtime-management\",\"private\":true,\"packageManager\":\"pnpm@11.1.0\",\"devEngines\":{\"runtime\":{\"name\":\"node\",\"version\":\"20.18.0\",\"onFail\":\"download\"}},\"engines\":{\"runtime\":{\"name\":\"deno\",\"version\":\"2.0.0\",\"onFail\":\"download\"}}}\n"], snapshot = false }, + { argv = ["vpt", "write-file", "$VP_HOME/js_runtime/node/20.18.0/bin/node", "#!/bin/sh\n"], snapshot = false }, + { argv = ["vpt", "chmod", "+x", "$VP_HOME/js_runtime/node/20.18.0/bin/node"], snapshot = false }, + { argv = ["vpt", "chmod", "+x", "system-bin/pnpm"], snapshot = false }, + { argv = ["vp", "env", "on", "node"], snapshot = false }, + { argv = ["vp", "env", "off", "pnpm"], snapshot = false }, + { argv = ["pnpm", "install"], envs = [["PATH", "${VP_HOME}/bin${PATH_SEPARATOR}${workspace}/system-bin${PATH_SEPARATOR}${PATH}"], ["PNPM_CONFIG_RUNTIME", "from-user"]], comment = "direct pnpm preserves the Deno runtime declared in engines.runtime" }, + { argv = ["vp", "install"], envs = [["PATH", "${VP_HOME}/bin${PATH_SEPARATOR}${workspace}/system-bin${PATH_SEPARATOR}${PATH}"], ["PNPM_CONFIG_RUNTIME", "from-user"]], comment = "vp install preserves the same engines.runtime setting" }, +] + +[[case]] +name = "pnpm_runtime_preserved_for_workspace_member" +vp = "global" +seed-runtime = false +skip-platforms = ["windows"] +comment = "A runtime required by any workspace member keeps pnpm runtime management enabled." +steps = [ + { argv = ["vpt", "write-file", "package.json", "{\"name\":\"pnpm-runtime-management\",\"private\":true,\"packageManager\":\"pnpm@11.1.0\",\"devEngines\":{\"runtime\":{\"name\":\"node\",\"version\":\"20.18.0\",\"onFail\":\"download\"}}}\n"], snapshot = false }, + { argv = ["vpt", "write-file", "pnpm-workspace.yaml", "packages:\n - packages/*\n"], snapshot = false }, + { argv = ["vpt", "mkdir", "packages/member"], snapshot = false }, + { argv = ["vpt", "write-file", "packages/member/package.json", "{\"name\":\"member\",\"version\":\"1.0.0\",\"devEngines\":{\"runtime\":{\"name\":\"bun\",\"version\":\"1.2.0\",\"onFail\":\"download\"}}}\n"], snapshot = false }, + { argv = ["vpt", "write-file", "$VP_HOME/js_runtime/node/20.18.0/bin/node", "#!/bin/sh\n"], snapshot = false }, + { argv = ["vpt", "chmod", "+x", "$VP_HOME/js_runtime/node/20.18.0/bin/node"], snapshot = false }, + { argv = ["vpt", "chmod", "+x", "system-bin/pnpm"], snapshot = false }, + { argv = ["vp", "env", "on", "node"], snapshot = false }, + { argv = ["vp", "env", "off", "pnpm"], snapshot = false }, + { argv = ["pnpm", "install"], envs = [["PATH", "${VP_HOME}/bin${PATH_SEPARATOR}${workspace}/system-bin${PATH_SEPARATOR}${PATH}"], ["PNPM_CONFIG_RUNTIME", "from-user"]], comment = "direct pnpm preserves the member's Bun runtime" }, + { argv = ["vp", "install"], envs = [["PATH", "${VP_HOME}/bin${PATH_SEPARATOR}${workspace}/system-bin${PATH_SEPARATOR}${PATH}"], ["PNPM_CONFIG_RUNTIME", "from-user"]], comment = "vp install preserves the same workspace-wide setting" }, +] + +[[case]] +name = "pnpm_runtime_uses_explicit_working_directory" +vp = "global" +seed-runtime = false +skip-platforms = ["windows"] +comment = "Direct pnpm evaluates runtime ownership in the project selected by -C." +steps = [ + { argv = ["vpt", "write-file", "package.json", "{\"name\":\"pnpm-runtime-management\",\"private\":true,\"packageManager\":\"pnpm@11.1.0\",\"devEngines\":{\"runtime\":{\"name\":\"node\",\"version\":\"20.18.0\",\"onFail\":\"download\"}}}\n"], snapshot = false }, + { argv = ["vpt", "mkdir", "other"], snapshot = false }, + { argv = ["vpt", "write-file", "other/package.json", "{\"name\":\"other\",\"private\":true,\"devEngines\":{\"runtime\":{\"name\":\"deno\",\"version\":\"2.0.0\",\"onFail\":\"download\"}}}\n"], snapshot = false }, + { argv = ["vpt", "write-file", "$VP_HOME/js_runtime/node/20.18.0/bin/node", "#!/bin/sh\n"], snapshot = false }, + { argv = ["vpt", "chmod", "+x", "$VP_HOME/js_runtime/node/20.18.0/bin/node"], snapshot = false }, + { argv = ["vpt", "chmod", "+x", "system-bin/pnpm"], snapshot = false }, + { argv = ["vp", "env", "on", "node"], snapshot = false }, + { argv = ["vp", "env", "off", "pnpm"], snapshot = false }, + { argv = ["pnpm", "-C", "other", "install"], envs = [["PATH", "${VP_HOME}/bin${PATH_SEPARATOR}${workspace}/system-bin${PATH_SEPARATOR}${PATH}"], ["PNPM_CONFIG_RUNTIME", "from-user"]], comment = "the target project's Deno runtime remains managed by pnpm" }, +] diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/pnpm_runtime_management/snapshots/pnpm_runtime_disabled_for_managed_node.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/pnpm_runtime_management/snapshots/pnpm_runtime_disabled_for_managed_node.md index 3356583016..bf710504a5 100644 --- a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/pnpm_runtime_management/snapshots/pnpm_runtime_disabled_for_managed_node.md +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/pnpm_runtime_management/snapshots/pnpm_runtime_disabled_for_managed_node.md @@ -2,7 +2,7 @@ Vite+ owns Node.js runtime management independently from the selected pnpm binary. -## `vpt write-file package.json '{"name":"pnpm-runtime-management","private":true,"packageManager":"pnpm@10.18.0","devEngines":{"runtime":{"name":"node","version":"20.18.0","onFail":"download"}}} +## `vpt write-file package.json '{"name":"pnpm-runtime-management","private":true,"packageManager":"pnpm@11.1.0","devEngines":{"runtime":{"name":"node","version":"20.18.0","onFail":"download"}}} '` @@ -40,19 +40,20 @@ VITE+ - The Unified Toolchain for the Web PNPM_CONFIG_RUNTIME=false ``` -## `vpt write-file $VP_HOME/package_manager/pnpm/10.18.0/pnpm/bin/pnpm '#'\!'/bin/sh -if [ "$1" = "--version" ]; then printf '\''10.18.0\n'\''; else printf '\''PNPM_CONFIG_RUNTIME=%s\n'\'' "${PNPM_CONFIG_RUNTIME-unset}"; fi +## `vpt write-file $VP_HOME/package_manager/pnpm/11.1.0/pnpm/bin/pnpm '#'\!'/bin/sh +if [ "$1" = "--version" ]; then printf '\''11.1.0\n'\''; else printf '\''PNPM_CONFIG_RUNTIME=%s\n'\'' "${PNPM_CONFIG_RUNTIME-unset}"; fi '` -## `vpt write-file $VP_HOME/package_manager/pnpm/10.18.0/pnpm/bin/pnpx '#'\!'/bin/sh +## `vpt write-file $VP_HOME/package_manager/pnpm/11.1.0/pnpm/bin/pnpx '#'\!'/bin/sh +printf '\''PNPM_CONFIG_RUNTIME=%s\n'\'' "${PNPM_CONFIG_RUNTIME-unset}" '` -## `vpt chmod +x $VP_HOME/package_manager/pnpm/10.18.0/pnpm/bin/pnpm` +## `vpt chmod +x $VP_HOME/package_manager/pnpm/11.1.0/pnpm/bin/pnpm` -## `vpt chmod +x $VP_HOME/package_manager/pnpm/10.18.0/pnpm/bin/pnpx` +## `vpt chmod +x $VP_HOME/package_manager/pnpm/11.1.0/pnpm/bin/pnpx` ## `vp env on pnpm` @@ -66,6 +67,14 @@ direct managed pnpm disables its duplicate Node.js runtime PNPM_CONFIG_RUNTIME=false ``` +## `PNPM_CONFIG_RUNTIME=from-user pnpx package` + +the pnpx alias receives the same child-only override + +``` +PNPM_CONFIG_RUNTIME=false +``` + ## `PNPM_CONFIG_RUNTIME=from-user vp install` pnpm-backed vp install applies the same managed-pnpm policy diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/pnpm_runtime_management/snapshots/pnpm_runtime_preserved_for_engines_runtime.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/pnpm_runtime_management/snapshots/pnpm_runtime_preserved_for_engines_runtime.md new file mode 100644 index 0000000000..4d8540bfd7 --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/pnpm_runtime_management/snapshots/pnpm_runtime_preserved_for_engines_runtime.md @@ -0,0 +1,41 @@ +# pnpm_runtime_preserved_for_engines_runtime + +pnpm runtime declarations in engines are part of the same process-wide policy. + +## `vpt write-file package.json '{"name":"pnpm-runtime-management","private":true,"packageManager":"pnpm@11.1.0","devEngines":{"runtime":{"name":"node","version":"20.18.0","onFail":"download"}},"engines":{"runtime":{"name":"deno","version":"2.0.0","onFail":"download"}}} +'` + + +## `vpt write-file $VP_HOME/js_runtime/node/20.18.0/bin/node '#'\!'/bin/sh +'` + + +## `vpt chmod +x $VP_HOME/js_runtime/node/20.18.0/bin/node` + + +## `vpt chmod +x system-bin/pnpm` + + +## `vp env on node` + + +## `vp env off pnpm` + + +## `PATH=${VP_HOME}/bin${PATH_SEPARATOR}${workspace}/system-bin${PATH_SEPARATOR}${PATH} PNPM_CONFIG_RUNTIME=from-user pnpm install` + +direct pnpm preserves the Deno runtime declared in engines.runtime + +``` +PNPM_CONFIG_RUNTIME=from-user +``` + +## `PATH=${VP_HOME}/bin${PATH_SEPARATOR}${workspace}/system-bin${PATH_SEPARATOR}${PATH} PNPM_CONFIG_RUNTIME=from-user vp install` + +vp install preserves the same engines.runtime setting + +``` +VITE+ - The Unified Toolchain for the Web + +PNPM_CONFIG_RUNTIME=from-user +``` diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/pnpm_runtime_management/snapshots/pnpm_runtime_preserved_for_mixed_runtimes.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/pnpm_runtime_management/snapshots/pnpm_runtime_preserved_for_mixed_runtimes.md index 76b5a51f48..467ea77646 100644 --- a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/pnpm_runtime_management/snapshots/pnpm_runtime_preserved_for_mixed_runtimes.md +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/pnpm_runtime_management/snapshots/pnpm_runtime_preserved_for_mixed_runtimes.md @@ -2,7 +2,7 @@ Vite+ must not disable pnpm runtimes that it cannot manage. -## `vpt write-file package.json '{"name":"pnpm-runtime-management","private":true,"packageManager":"pnpm@10.18.0","devEngines":{"runtime":[{"name":"node","version":"20.18.0","onFail":"download"},{"name":"deno","version":"2.0.0","onFail":"download"}]}} +## `vpt write-file package.json '{"name":"pnpm-runtime-management","private":true,"packageManager":"pnpm@11.1.0","devEngines":{"runtime":[{"name":"node","version":"20.18.0","onFail":"download"},{"name":"deno","version":"2.0.0","onFail":"download"}]}} '` diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/pnpm_runtime_management/snapshots/pnpm_runtime_preserved_for_system_first_node.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/pnpm_runtime_management/snapshots/pnpm_runtime_preserved_for_system_first_node.md index e981818abe..bda7460ffa 100644 --- a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/pnpm_runtime_management/snapshots/pnpm_runtime_preserved_for_system_first_node.md +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/pnpm_runtime_management/snapshots/pnpm_runtime_preserved_for_system_first_node.md @@ -2,7 +2,7 @@ pnpm keeps runtime ownership when Vite+ does not manage Node.js. -## `vpt write-file package.json '{"name":"pnpm-runtime-management","private":true,"packageManager":"pnpm@10.18.0","devEngines":{"runtime":{"name":"node","version":"20.18.0","onFail":"download"}}} +## `vpt write-file package.json '{"name":"pnpm-runtime-management","private":true,"packageManager":"pnpm@11.1.0","devEngines":{"runtime":{"name":"node","version":"20.18.0","onFail":"download"}}} '` diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/pnpm_runtime_management/snapshots/pnpm_runtime_preserved_for_workspace_member.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/pnpm_runtime_management/snapshots/pnpm_runtime_preserved_for_workspace_member.md new file mode 100644 index 0000000000..e09562bc51 --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/pnpm_runtime_management/snapshots/pnpm_runtime_preserved_for_workspace_member.md @@ -0,0 +1,22 @@ +# pnpm_runtime_preserved_for_workspace_member + +A runtime required by any workspace member keeps pnpm runtime management enabled. + +## `vpt write-file package.json '{"name":"pnpm-runtime-management","private":true,"packageManager":"pnpm@11.1.0","devEngines":{"runtime":{"name":"node","version":"20.18.0","onFail":"download"}}} +'` + + +## `vpt write-file pnpm-workspace.yaml 'packages: + - packages/* +'` + + +## `vpt mkdir packages/member` + +**Exit code:** 1 + +``` +No such file or directory (os error 2) +``` + +*(remaining steps skipped: step failed)* diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/pnpm_runtime_management/snapshots/pnpm_runtime_uses_explicit_working_directory.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/pnpm_runtime_management/snapshots/pnpm_runtime_uses_explicit_working_directory.md new file mode 100644 index 0000000000..d663630c37 --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/pnpm_runtime_management/snapshots/pnpm_runtime_uses_explicit_working_directory.md @@ -0,0 +1,38 @@ +# pnpm_runtime_uses_explicit_working_directory + +Direct pnpm evaluates runtime ownership in the project selected by -C. + +## `vpt write-file package.json '{"name":"pnpm-runtime-management","private":true,"packageManager":"pnpm@11.1.0","devEngines":{"runtime":{"name":"node","version":"20.18.0","onFail":"download"}}} +'` + + +## `vpt mkdir other` + + +## `vpt write-file other/package.json '{"name":"other","private":true,"devEngines":{"runtime":{"name":"deno","version":"2.0.0","onFail":"download"}}} +'` + + +## `vpt write-file $VP_HOME/js_runtime/node/20.18.0/bin/node '#'\!'/bin/sh +'` + + +## `vpt chmod +x $VP_HOME/js_runtime/node/20.18.0/bin/node` + + +## `vpt chmod +x system-bin/pnpm` + + +## `vp env on node` + + +## `vp env off pnpm` + + +## `PATH=${VP_HOME}/bin${PATH_SEPARATOR}${workspace}/system-bin${PATH_SEPARATOR}${PATH} PNPM_CONFIG_RUNTIME=from-user pnpm -C other install` + +the target project's Deno runtime remains managed by pnpm + +``` +PNPM_CONFIG_RUNTIME=from-user +``` diff --git a/crates/vp_global_cli/src/commands/pnpm_runtime.rs b/crates/vp_global_cli/src/commands/pnpm_runtime.rs index cab98f89f3..0337eac5aa 100644 --- a/crates/vp_global_cli/src/commands/pnpm_runtime.rs +++ b/crates/vp_global_cli/src/commands/pnpm_runtime.rs @@ -1,5 +1,6 @@ -use vp_shared::PackageJson; -use vt_path::AbsolutePath; +use std::path::Path; + +use vt_path::{AbsolutePath, AbsolutePathBuf}; use super::env::config::ShimMode; use crate::error::Error; @@ -7,6 +8,37 @@ use crate::error::Error; pub(crate) const PNPM_CONFIG_RUNTIME: &str = "PNPM_CONFIG_RUNTIME"; pub(crate) const PNPM_CONFIG_RUNTIME_DISABLED: &str = "false"; +fn command_cwd(cwd: &AbsolutePath, args: &[String]) -> Option { + let mut dir = None; + let mut args = args.iter(); + while let Some(arg) = args.next() { + if arg == "--" { + break; + } + if arg == "-C" || arg == "--dir" { + dir = Some(args.next()?.as_str()); + } else if let Some(value) = arg.strip_prefix("--dir=") { + dir = Some(value); + } + } + + let Some(dir) = dir else { return Some(cwd.to_absolute_path_buf()) }; + if dir.is_empty() { + return None; + } + let path = Path::new(dir); + if path.is_absolute() { AbsolutePathBuf::new(path.to_path_buf()) } else { Some(cwd.join(path)) } +} + +pub(crate) async fn should_disable_for_command( + cwd: &AbsolutePath, + args: &[String], + node_shim_mode: ShimMode, +) -> Result { + let Some(cwd) = command_cwd(cwd, args) else { return Ok(false) }; + should_disable(&cwd, node_shim_mode).await +} + pub(crate) async fn should_disable( cwd: &AbsolutePath, node_shim_mode: ShimMode, @@ -20,14 +52,33 @@ pub(crate) async fn should_disable( Err(vt_workspace::Error::PackageJsonNotFound(_)) => return Ok(false), Err(error) => return Err(error.into()), }; - let content = tokio::fs::read_to_string(workspace.path.join("package.json")).await?; - let package_json: PackageJson = serde_json::from_str(&content)?; - let Some(runtime) = package_json.dev_engines.and_then(|engines| engines.runtime) else { - return Ok(false); - }; - let entries = runtime.entries(); + let packages = vt_workspace::load_package_graph(&workspace)?; + let mut has_managed_node = false; + + // pnpm converts both fields from every workspace manifest into runtime dependencies. + for package in packages.node_weights() { + let content = tokio::fs::read_to_string(package.absolute_path.join("package.json")).await?; + let package_json: serde_json::Value = serde_json::from_str(&content)?; + for engines in ["devEngines", "engines"] { + let Some(runtime) = package_json.get(engines).and_then(|value| value.get("runtime")) + else { + continue; + }; + let entries = + runtime.as_array().map_or_else(|| std::slice::from_ref(runtime), Vec::as_slice); + for entry in entries { + let Some(name) = entry.get("name").and_then(serde_json::Value::as_str) else { + continue; + }; + if name != "node" { + return Ok(false); + } + has_managed_node = has_managed_node || engines == "devEngines"; + } + } + } // pnpm's runtime opt-out covers Node.js, Bun, and Deno together, so Vite+ // can use it only when every declared runtime is one Vite+ manages. - Ok(!entries.is_empty() && entries.iter().all(|entry| entry.name == "node")) + Ok(has_managed_node) } diff --git a/crates/vp_global_cli/src/shim/dispatch.rs b/crates/vp_global_cli/src/shim/dispatch.rs index c0d31ac407..01dec18697 100644 --- a/crates/vp_global_cli/src/shim/dispatch.rs +++ b/crates/vp_global_cli/src/shim/dispatch.rs @@ -754,7 +754,7 @@ pub async fn dispatch(tool: &str, args: &[String]) -> i32 { return 1; } }; - match pnpm_runtime::should_disable(&cwd, node_shim_mode).await { + match pnpm_runtime::should_disable_for_command(&cwd, args, node_shim_mode).await { Ok(disable) => disable, Err(error) => { eprintln!("vp: Failed to resolve pnpm runtime management: {error}"); From c7f6bc29b779698e63a6d63b64dac7f94829710f Mon Sep 17 00:00:00 2001 From: Liang Date: Wed, 2 Sep 2026 02:27:26 +0800 Subject: [PATCH 3/5] fix(env): verify managed node runtime --- .../pnpm_runtime_management/snapshots.toml | 18 ++++++++ ...preserved_for_conflicting_node_versions.md | 46 +++++++++++++++++++ .../src/commands/pnpm_runtime.rs | 21 +++++++-- 3 files changed, 81 insertions(+), 4 deletions(-) create mode 100644 crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/pnpm_runtime_management/snapshots/pnpm_runtime_preserved_for_conflicting_node_versions.md diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/pnpm_runtime_management/snapshots.toml b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/pnpm_runtime_management/snapshots.toml index 0105b853cd..72c17dcf68 100644 --- a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/pnpm_runtime_management/snapshots.toml +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/pnpm_runtime_management/snapshots.toml @@ -110,3 +110,21 @@ steps = [ { argv = ["vp", "env", "off", "pnpm"], snapshot = false }, { argv = ["pnpm", "-C", "other", "install"], envs = [["PATH", "${VP_HOME}/bin${PATH_SEPARATOR}${workspace}/system-bin${PATH_SEPARATOR}${PATH}"], ["PNPM_CONFIG_RUNTIME", "from-user"]], comment = "the target project's Deno runtime remains managed by pnpm" }, ] + +[[case]] +name = "pnpm_runtime_preserved_for_conflicting_node_versions" +vp = "global" +seed-runtime = false +skip-platforms = ["windows"] +comment = "pnpm keeps runtime ownership when Vite+ selects a different Node.js version." +steps = [ + { argv = ["vpt", "write-file", "package.json", "{\"name\":\"pnpm-runtime-management\",\"private\":true,\"packageManager\":\"pnpm@11.1.0\",\"devEngines\":{\"runtime\":{\"name\":\"node\",\"version\":\"22.11.0\",\"onFail\":\"download\"}}}\n"], snapshot = false }, + { argv = ["vpt", "write-file", ".node-version", "20.18.0\n"], snapshot = false }, + { argv = ["vpt", "write-file", "$VP_HOME/js_runtime/node/20.18.0/bin/node", "#!/bin/sh\n"], snapshot = false }, + { argv = ["vpt", "chmod", "+x", "$VP_HOME/js_runtime/node/20.18.0/bin/node"], snapshot = false }, + { argv = ["vpt", "chmod", "+x", "system-bin/pnpm"], snapshot = false }, + { argv = ["vp", "env", "on", "node"], snapshot = false }, + { argv = ["vp", "env", "off", "pnpm"], snapshot = false }, + { argv = ["pnpm", "install"], envs = [["PATH", "${VP_HOME}/bin${PATH_SEPARATOR}${workspace}/system-bin${PATH_SEPARATOR}${PATH}"], ["PNPM_CONFIG_RUNTIME", "from-user"]], comment = "direct pnpm preserves the conflicting Node.js runtime" }, + { argv = ["vp", "install"], envs = [["PATH", "${VP_HOME}/bin${PATH_SEPARATOR}${workspace}/system-bin${PATH_SEPARATOR}${PATH}"], ["PNPM_CONFIG_RUNTIME", "from-user"]], comment = "vp install preserves the same conflicting runtime" }, +] diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/pnpm_runtime_management/snapshots/pnpm_runtime_preserved_for_conflicting_node_versions.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/pnpm_runtime_management/snapshots/pnpm_runtime_preserved_for_conflicting_node_versions.md new file mode 100644 index 0000000000..302c0f46fa --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/pnpm_runtime_management/snapshots/pnpm_runtime_preserved_for_conflicting_node_versions.md @@ -0,0 +1,46 @@ +# pnpm_runtime_preserved_for_conflicting_node_versions + +pnpm keeps runtime ownership when Vite+ selects a different Node.js version. + +## `vpt write-file package.json '{"name":"pnpm-runtime-management","private":true,"packageManager":"pnpm@11.1.0","devEngines":{"runtime":{"name":"node","version":"22.11.0","onFail":"download"}}} +'` + + +## `vpt write-file .node-version '20.18.0 +'` + + +## `vpt write-file $VP_HOME/js_runtime/node/20.18.0/bin/node '#'\!'/bin/sh +'` + + +## `vpt chmod +x $VP_HOME/js_runtime/node/20.18.0/bin/node` + + +## `vpt chmod +x system-bin/pnpm` + + +## `vp env on node` + + +## `vp env off pnpm` + + +## `PATH=${VP_HOME}/bin${PATH_SEPARATOR}${workspace}/system-bin${PATH_SEPARATOR}${PATH} PNPM_CONFIG_RUNTIME=from-user pnpm install` + +direct pnpm preserves the conflicting Node.js runtime + +``` +PNPM_CONFIG_RUNTIME=from-user +``` + +## `PATH=${VP_HOME}/bin${PATH_SEPARATOR}${workspace}/system-bin${PATH_SEPARATOR}${PATH} PNPM_CONFIG_RUNTIME=from-user vp install` + +vp install preserves the same conflicting runtime + +``` +VITE+ - The Unified Toolchain for the Web + +warning: Node.js version 20.18.0 (from .node-version) does not satisfy devEngines.runtime constraint '22.11.0' +PNPM_CONFIG_RUNTIME=from-user +``` diff --git a/crates/vp_global_cli/src/commands/pnpm_runtime.rs b/crates/vp_global_cli/src/commands/pnpm_runtime.rs index 0337eac5aa..85d2e279ff 100644 --- a/crates/vp_global_cli/src/commands/pnpm_runtime.rs +++ b/crates/vp_global_cli/src/commands/pnpm_runtime.rs @@ -2,7 +2,7 @@ use std::path::Path; use vt_path::{AbsolutePath, AbsolutePathBuf}; -use super::env::config::ShimMode; +use super::env::config::{self, ShimMode}; use crate::error::Error; pub(crate) const PNPM_CONFIG_RUNTIME: &str = "PNPM_CONFIG_RUNTIME"; @@ -54,6 +54,7 @@ pub(crate) async fn should_disable( }; let packages = vt_workspace::load_package_graph(&workspace)?; let mut has_managed_node = false; + let mut node_requirements = Vec::new(); // pnpm converts both fields from every workspace manifest into runtime dependencies. for package in packages.node_weights() { @@ -74,11 +75,23 @@ pub(crate) async fn should_disable( return Ok(false); } has_managed_node = has_managed_node || engines == "devEngines"; + if let Some(version) = entry.get("version").and_then(serde_json::Value::as_str) { + node_requirements.push(version.to_string()); + } } } } - // pnpm's runtime opt-out covers Node.js, Bun, and Deno together, so Vite+ - // can use it only when every declared runtime is one Vite+ manages. - Ok(has_managed_node) + // The opt-out covers every runtime, so the selected Node.js must satisfy + // every declaration that pnpm would otherwise install. + if !has_managed_node { + return Ok(false); + } + let selected = config::resolve_version(cwd).await?; + let Ok(selected) = node_semver::Version::parse(&selected.version) else { + return Ok(false); + }; + Ok(node_requirements.iter().all(|requirement| { + node_semver::Range::parse(requirement).is_ok_and(|range| range.satisfies(&selected)) + })) } From d2b6d48ad75bc73533ee7a1fd4af8cfe9ab33739 Mon Sep 17 00:00:00 2001 From: Liang Date: Wed, 2 Sep 2026 05:57:42 +0800 Subject: [PATCH 4/5] fix(env): align pnpm runtime target --- .../pnpm_runtime_management/snapshots.toml | 25 +++++++++ .../pnpm_runtime_disabled_for_managed_node.md | 15 +++++ .../pnpm_runtime_uses_target_node_version.md | 55 +++++++++++++++++++ .../src/commands/pnpm_runtime.rs | 24 ++++---- crates/vp_global_cli/src/shim/dispatch.rs | 29 +++++++--- 5 files changed, 128 insertions(+), 20 deletions(-) create mode 100644 crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/pnpm_runtime_management/snapshots/pnpm_runtime_uses_target_node_version.md diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/pnpm_runtime_management/snapshots.toml b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/pnpm_runtime_management/snapshots.toml index 72c17dcf68..061d198df6 100644 --- a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/pnpm_runtime_management/snapshots.toml +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/pnpm_runtime_management/snapshots.toml @@ -17,9 +17,12 @@ steps = [ { argv = ["vpt", "write-file", "$VP_HOME/package_manager/pnpm/11.1.0/pnpm/bin/pnpx", "#!/bin/sh\nprintf 'PNPM_CONFIG_RUNTIME=%s\\n' \"${PNPM_CONFIG_RUNTIME-unset}\"\n"], snapshot = false }, { argv = ["vpt", "chmod", "+x", "$VP_HOME/package_manager/pnpm/11.1.0/pnpm/bin/pnpm"], snapshot = false }, { argv = ["vpt", "chmod", "+x", "$VP_HOME/package_manager/pnpm/11.1.0/pnpm/bin/pnpx"], snapshot = false }, + { argv = ["vpt", "mkdir", "other"], snapshot = false }, + { argv = ["vpt", "write-file", "other/package.json", "{\"name\":\"other\",\"private\":true,\"devEngines\":{\"runtime\":{\"name\":\"deno\",\"version\":\"2.0.0\",\"onFail\":\"download\"}}}\n"], snapshot = false }, { argv = ["vp", "env", "on", "pnpm"], snapshot = false }, { argv = ["pnpm", "install"], envs = [["PNPM_CONFIG_RUNTIME", "from-user"]], comment = "direct managed pnpm disables its duplicate Node.js runtime" }, { argv = ["pnpx", "package"], envs = [["PNPM_CONFIG_RUNTIME", "from-user"]], comment = "the pnpx alias receives the same child-only override" }, + { argv = ["pnpx", "package", "--dir", "other"], envs = [["PNPM_CONFIG_RUNTIME", "from-user"]], comment = "pnpx forwards --dir to the downloaded command without changing runtime ownership" }, { argv = ["vp", "install"], envs = [["PNPM_CONFIG_RUNTIME", "from-user"]], comment = "pnpm-backed vp install applies the same managed-pnpm policy" }, ] @@ -128,3 +131,25 @@ steps = [ { argv = ["pnpm", "install"], envs = [["PATH", "${VP_HOME}/bin${PATH_SEPARATOR}${workspace}/system-bin${PATH_SEPARATOR}${PATH}"], ["PNPM_CONFIG_RUNTIME", "from-user"]], comment = "direct pnpm preserves the conflicting Node.js runtime" }, { argv = ["vp", "install"], envs = [["PATH", "${VP_HOME}/bin${PATH_SEPARATOR}${workspace}/system-bin${PATH_SEPARATOR}${PATH}"], ["PNPM_CONFIG_RUNTIME", "from-user"]], comment = "vp install preserves the same conflicting runtime" }, ] + +[[case]] +name = "pnpm_runtime_uses_target_node_version" +vp = "global" +seed-runtime = false +skip-platforms = ["windows"] +comment = "The managed Node.js selected for direct pnpm matches the project inspected for runtime ownership." +steps = [ + { argv = ["vpt", "write-file", "package.json", "{\"name\":\"pnpm-runtime-management\",\"private\":true,\"packageManager\":\"pnpm@11.1.0\"}\n"], snapshot = false }, + { argv = ["vpt", "write-file", ".node-version", "20.18.0\n"], snapshot = false }, + { argv = ["vpt", "mkdir", "other"], snapshot = false }, + { argv = ["vpt", "write-file", "other/package.json", "{\"name\":\"other\",\"private\":true,\"devEngines\":{\"runtime\":{\"name\":\"node\",\"version\":\"22.11.0\",\"onFail\":\"download\"}}}\n"], snapshot = false }, + { argv = ["vpt", "write-file", "$VP_HOME/js_runtime/node/20.18.0/bin/node", "#!/bin/sh\n"], snapshot = false }, + { argv = ["vpt", "write-file", "$VP_HOME/js_runtime/node/22.11.0/bin/node", "#!/bin/sh\n"], snapshot = false }, + { argv = ["vpt", "chmod", "+x", "$VP_HOME/js_runtime/node/20.18.0/bin/node"], snapshot = false }, + { argv = ["vpt", "chmod", "+x", "$VP_HOME/js_runtime/node/22.11.0/bin/node"], snapshot = false }, + { argv = ["vpt", "write-file", "$VP_HOME/package_manager/pnpm/11.1.0/pnpm/bin/pnpm", "#!/bin/sh\nif [ \"$1\" = \"--version\" ]; then printf '11.1.0\\n'; else printf 'VP_ACTIVE_NODE=%s\\nPNPM_CONFIG_RUNTIME=%s\\n' \"${VP_ACTIVE_NODE-unset}\" \"${PNPM_CONFIG_RUNTIME-unset}\"; fi\n"], snapshot = false }, + { argv = ["vpt", "chmod", "+x", "$VP_HOME/package_manager/pnpm/11.1.0/pnpm/bin/pnpm"], snapshot = false }, + { argv = ["vp", "env", "on", "node"], snapshot = false }, + { argv = ["vp", "env", "on", "pnpm"], snapshot = false }, + { argv = ["pnpm", "-C", "other", "install"], envs = [["VP_DEBUG_SHIM", "1"], ["PNPM_CONFIG_RUNTIME", "from-user"]], comment = "the target Node.js version and runtime opt-out are applied together" }, +] diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/pnpm_runtime_management/snapshots/pnpm_runtime_disabled_for_managed_node.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/pnpm_runtime_management/snapshots/pnpm_runtime_disabled_for_managed_node.md index bf710504a5..ffd25500bf 100644 --- a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/pnpm_runtime_management/snapshots/pnpm_runtime_disabled_for_managed_node.md +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/pnpm_runtime_management/snapshots/pnpm_runtime_disabled_for_managed_node.md @@ -56,6 +56,13 @@ printf '\''PNPM_CONFIG_RUNTIME=%s\n'\'' "${PNPM_CONFIG_RUNTIME-unset}" ## `vpt chmod +x $VP_HOME/package_manager/pnpm/11.1.0/pnpm/bin/pnpx` +## `vpt mkdir other` + + +## `vpt write-file other/package.json '{"name":"other","private":true,"devEngines":{"runtime":{"name":"deno","version":"2.0.0","onFail":"download"}}} +'` + + ## `vp env on pnpm` @@ -75,6 +82,14 @@ the pnpx alias receives the same child-only override PNPM_CONFIG_RUNTIME=false ``` +## `PNPM_CONFIG_RUNTIME=from-user pnpx package --dir other` + +pnpx forwards --dir to the downloaded command without changing runtime ownership + +``` +PNPM_CONFIG_RUNTIME=false +``` + ## `PNPM_CONFIG_RUNTIME=from-user vp install` pnpm-backed vp install applies the same managed-pnpm policy diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/pnpm_runtime_management/snapshots/pnpm_runtime_uses_target_node_version.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/pnpm_runtime_management/snapshots/pnpm_runtime_uses_target_node_version.md new file mode 100644 index 0000000000..0426534731 --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/pnpm_runtime_management/snapshots/pnpm_runtime_uses_target_node_version.md @@ -0,0 +1,55 @@ +# pnpm_runtime_uses_target_node_version + +The managed Node.js selected for direct pnpm matches the project inspected for runtime ownership. + +## `vpt write-file package.json '{"name":"pnpm-runtime-management","private":true,"packageManager":"pnpm@11.1.0"} +'` + + +## `vpt write-file .node-version '20.18.0 +'` + + +## `vpt mkdir other` + + +## `vpt write-file other/package.json '{"name":"other","private":true,"devEngines":{"runtime":{"name":"node","version":"22.11.0","onFail":"download"}}} +'` + + +## `vpt write-file $VP_HOME/js_runtime/node/20.18.0/bin/node '#'\!'/bin/sh +'` + + +## `vpt write-file $VP_HOME/js_runtime/node/22.11.0/bin/node '#'\!'/bin/sh +'` + + +## `vpt chmod +x $VP_HOME/js_runtime/node/20.18.0/bin/node` + + +## `vpt chmod +x $VP_HOME/js_runtime/node/22.11.0/bin/node` + + +## `vpt write-file $VP_HOME/package_manager/pnpm/11.1.0/pnpm/bin/pnpm '#'\!'/bin/sh +if [ "$1" = "--version" ]; then printf '\''11.1.0\n'\''; else printf '\''VP_ACTIVE_NODE=%s\nPNPM_CONFIG_RUNTIME=%s\n'\'' "${VP_ACTIVE_NODE-unset}" "${PNPM_CONFIG_RUNTIME-unset}"; fi +'` + + +## `vpt chmod +x $VP_HOME/package_manager/pnpm/11.1.0/pnpm/bin/pnpm` + + +## `vp env on node` + + +## `vp env on pnpm` + + +## `VP_DEBUG_SHIM=1 PNPM_CONFIG_RUNTIME=from-user pnpm -C other install` + +the target Node.js version and runtime opt-out are applied together + +``` +VP_ACTIVE_NODE=22.11.0 +PNPM_CONFIG_RUNTIME=false +``` diff --git a/crates/vp_global_cli/src/commands/pnpm_runtime.rs b/crates/vp_global_cli/src/commands/pnpm_runtime.rs index 85d2e279ff..8976708a3b 100644 --- a/crates/vp_global_cli/src/commands/pnpm_runtime.rs +++ b/crates/vp_global_cli/src/commands/pnpm_runtime.rs @@ -8,7 +8,16 @@ use crate::error::Error; pub(crate) const PNPM_CONFIG_RUNTIME: &str = "PNPM_CONFIG_RUNTIME"; pub(crate) const PNPM_CONFIG_RUNTIME_DISABLED: &str = "false"; -fn command_cwd(cwd: &AbsolutePath, args: &[String]) -> Option { +pub(crate) fn command_cwd( + tool: &str, + cwd: &AbsolutePath, + args: &[String], +) -> Option { + // pnpx forwards every argument to the downloaded command. + if tool != "pnpm" { + return Some(cwd.to_absolute_path_buf()); + } + let mut dir = None; let mut args = args.iter(); while let Some(arg) = args.next() { @@ -30,15 +39,6 @@ fn command_cwd(cwd: &AbsolutePath, args: &[String]) -> Option { if path.is_absolute() { AbsolutePathBuf::new(path.to_path_buf()) } else { Some(cwd.join(path)) } } -pub(crate) async fn should_disable_for_command( - cwd: &AbsolutePath, - args: &[String], - node_shim_mode: ShimMode, -) -> Result { - let Some(cwd) = command_cwd(cwd, args) else { return Ok(false) }; - should_disable(&cwd, node_shim_mode).await -} - pub(crate) async fn should_disable( cwd: &AbsolutePath, node_shim_mode: ShimMode, @@ -82,8 +82,8 @@ pub(crate) async fn should_disable( } } - // The opt-out covers every runtime, so the selected Node.js must satisfy - // every declaration that pnpm would otherwise install. + // The opt-out filters runtime entries for every workspace importer, so the + // selected Node.js must satisfy each workspace declaration that it replaces. if !has_managed_node { return Ok(false); } diff --git a/crates/vp_global_cli/src/shim/dispatch.rs b/crates/vp_global_cli/src/shim/dispatch.rs index 01dec18697..1665a0b010 100644 --- a/crates/vp_global_cli/src/shim/dispatch.rs +++ b/crates/vp_global_cli/src/shim/dispatch.rs @@ -738,7 +738,7 @@ pub async fn dispatch(tool: &str, args: &[String]) -> i32 { // Check shim mode from config let shim_mode = load_shim_mode(tool).await; - let disable_pnpm_runtime = + let (disable_pnpm_runtime, pnpm_cwd) = if PackageManagerType::from_tool(tool) == Some(PackageManagerType::Pnpm) { let cwd = match current_dir() { Ok(path) => path, @@ -754,16 +754,22 @@ pub async fn dispatch(tool: &str, args: &[String]) -> i32 { return 1; } }; - match pnpm_runtime::should_disable_for_command(&cwd, args, node_shim_mode).await { - Ok(disable) => disable, + let command_cwd = pnpm_runtime::command_cwd(tool, &cwd, args); + let disable = match command_cwd.as_deref() { + Some(cwd) => pnpm_runtime::should_disable(cwd, node_shim_mode).await, + None => Ok(false), + }; + match disable { + Ok(disable) => (disable, command_cwd), Err(error) => { eprintln!("vp: Failed to resolve pnpm runtime management: {error}"); return 1; } } } else { - false + (false, None) }; + let pnpm_node_cwd = pnpm_cwd.as_deref().filter(|_| disable_pnpm_runtime); let pnpm_runtime_env = if disable_pnpm_runtime { &[(pnpm_runtime::PNPM_CONFIG_RUNTIME, pnpm_runtime::PNPM_CONFIG_RUNTIME_DISABLED)][..] } else { @@ -774,7 +780,8 @@ pub async fn dispatch(tool: &str, args: &[String]) -> i32 { // In system-first mode, try to find system tool first if let Some(system_path) = find_system_tool(tool) { if PackageManagerType::from_tool(tool).is_some() - && let Err(error) = prepare_node_path_for_system_package_manager().await + && let Err(error) = + prepare_node_path_for_system_package_manager(pnpm_node_cwd).await { eprintln!("vp: Failed to prepare Node.js for system package manager: {error}"); return 1; @@ -815,6 +822,7 @@ pub async fn dispatch(tool: &str, args: &[String]) -> i32 { return 1; } }; + let node_cwd = pnpm_node_cwd.map_or_else(|| cwd.clone(), AbsolutePath::to_absolute_path_buf); // Ensure Node.js is installed and locate its binary for PATH preparation. // Package-manager shims can use their own declared version, but JS-based @@ -834,7 +842,7 @@ pub async fn dispatch(tool: &str, args: &[String]) -> i32 { None }; let resolution = if system_node.is_none() { - match resolve_with_cache(&cwd).await { + match resolve_with_cache(&node_cwd).await { Ok(resolution) => Some(resolution), Err(error) => { eprintln!("vp: Failed to resolve Node version: {error}"); @@ -981,7 +989,9 @@ fn read_node_version(node_path: &AbsolutePath) -> Option { .then(|| String::from_utf8_lossy(&output.stdout).trim().trim_start_matches('v').to_string()) } -async fn prepare_node_path_for_system_package_manager() -> Result<(), Error> { +async fn prepare_node_path_for_system_package_manager( + pnpm_cwd: Option<&AbsolutePath>, +) -> Result<(), Error> { let config = config::load_config().await?; if config.node_shim_mode == ShimMode::SystemFirst && let Some(node) = find_system_tool("node") @@ -991,7 +1001,10 @@ async fn prepare_node_path_for_system_package_manager() -> Result<(), Error> { return Ok(()); } - let cwd = current_dir()?; + let cwd = match pnpm_cwd { + Some(cwd) => cwd.to_absolute_path_buf(), + None => current_dir()?, + }; let resolution = resolve_with_cache(&cwd).await.map_err(|error| Error::Other(error.into()))?; let node = ensure_installed(&resolution.version).await.map_err(|error| Error::Other(error.into()))?; From c853694233bbc680fce5eccf65bfbc917ddbfbfe Mon Sep 17 00:00:00 2001 From: Liang Date: Wed, 2 Sep 2026 06:10:06 +0800 Subject: [PATCH 5/5] fix(env): preserve pnpm runtime intent --- .../pnpm_runtime_management/snapshots.toml | 6 +++ .../pnpm_runtime_disabled_for_managed_node.md | 37 +++++++++++++++++++ ...runtime_uses_explicit_working_directory.md | 8 ++++ crates/vp_global_cli/src/commands/mod.rs | 16 ++++---- .../src/commands/pnpm_runtime.rs | 13 +++++++ crates/vp_global_cli/src/shim/dispatch.rs | 9 +++-- 6 files changed, 78 insertions(+), 11 deletions(-) diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/pnpm_runtime_management/snapshots.toml b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/pnpm_runtime_management/snapshots.toml index 061d198df6..22cad4aa8c 100644 --- a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/pnpm_runtime_management/snapshots.toml +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/pnpm_runtime_management/snapshots.toml @@ -19,11 +19,16 @@ steps = [ { argv = ["vpt", "chmod", "+x", "$VP_HOME/package_manager/pnpm/11.1.0/pnpm/bin/pnpx"], snapshot = false }, { argv = ["vpt", "mkdir", "other"], snapshot = false }, { argv = ["vpt", "write-file", "other/package.json", "{\"name\":\"other\",\"private\":true,\"devEngines\":{\"runtime\":{\"name\":\"deno\",\"version\":\"2.0.0\",\"onFail\":\"download\"}}}\n"], snapshot = false }, + { argv = ["vpt", "mkdir", "child"], snapshot = false }, { argv = ["vp", "env", "on", "pnpm"], snapshot = false }, { argv = ["pnpm", "install"], envs = [["PNPM_CONFIG_RUNTIME", "from-user"]], comment = "direct managed pnpm disables its duplicate Node.js runtime" }, { argv = ["pnpx", "package"], envs = [["PNPM_CONFIG_RUNTIME", "from-user"]], comment = "the pnpx alias receives the same child-only override" }, { argv = ["pnpx", "package", "--dir", "other"], envs = [["PNPM_CONFIG_RUNTIME", "from-user"]], comment = "pnpx forwards --dir to the downloaded command without changing runtime ownership" }, + { argv = ["pnpm", "exec", "package", "--dir", "other"], envs = [["PNPM_CONFIG_RUNTIME", "from-user"]], comment = "pnpm exec forwards --dir after the command without changing runtime ownership" }, + { argv = ["pnpm", "runtime", "set", "deno", "2"], envs = [["PNPM_CONFIG_RUNTIME", "from-user"]], comment = "an explicit runtime command keeps pnpm runtime management enabled" }, + { argv = ["pnpm", "add", "bun@runtime:1.2.0"], envs = [["PNPM_CONFIG_RUNTIME", "from-user"]], comment = "an explicit runtime dependency keeps pnpm runtime management enabled" }, { argv = ["vp", "install"], envs = [["PNPM_CONFIG_RUNTIME", "from-user"]], comment = "pnpm-backed vp install applies the same managed-pnpm policy" }, + { argv = ["vp", "install"], cwd = "child", envs = [["PNPM_CONFIG_RUNTIME", "from-user"]], comment = "vp install from a child directory uses the parent package's managed Node.js" }, ] [[case]] @@ -112,6 +117,7 @@ steps = [ { argv = ["vp", "env", "on", "node"], snapshot = false }, { argv = ["vp", "env", "off", "pnpm"], snapshot = false }, { argv = ["pnpm", "-C", "other", "install"], envs = [["PATH", "${VP_HOME}/bin${PATH_SEPARATOR}${workspace}/system-bin${PATH_SEPARATOR}${PATH}"], ["PNPM_CONFIG_RUNTIME", "from-user"]], comment = "the target project's Deno runtime remains managed by pnpm" }, + { argv = ["pnpm", "-C=other", "install"], envs = [["PATH", "${VP_HOME}/bin${PATH_SEPARATOR}${workspace}/system-bin${PATH_SEPARATOR}${PATH}"], ["PNPM_CONFIG_RUNTIME", "from-user"]], comment = "the short equals form resolves the same target project" }, ] [[case]] diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/pnpm_runtime_management/snapshots/pnpm_runtime_disabled_for_managed_node.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/pnpm_runtime_management/snapshots/pnpm_runtime_disabled_for_managed_node.md index ffd25500bf..adaca1b2e6 100644 --- a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/pnpm_runtime_management/snapshots/pnpm_runtime_disabled_for_managed_node.md +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/pnpm_runtime_management/snapshots/pnpm_runtime_disabled_for_managed_node.md @@ -63,6 +63,9 @@ printf '\''PNPM_CONFIG_RUNTIME=%s\n'\'' "${PNPM_CONFIG_RUNTIME-unset}" '` +## `vpt mkdir child` + + ## `vp env on pnpm` @@ -90,6 +93,30 @@ pnpx forwards --dir to the downloaded command without changing runtime ownership PNPM_CONFIG_RUNTIME=false ``` +## `PNPM_CONFIG_RUNTIME=from-user pnpm exec package --dir other` + +pnpm exec forwards --dir after the command without changing runtime ownership + +``` +PNPM_CONFIG_RUNTIME=false +``` + +## `PNPM_CONFIG_RUNTIME=from-user pnpm runtime set deno 2` + +an explicit runtime command keeps pnpm runtime management enabled + +``` +PNPM_CONFIG_RUNTIME=from-user +``` + +## `PNPM_CONFIG_RUNTIME=from-user pnpm add bun@runtime:1.2.0` + +an explicit runtime dependency keeps pnpm runtime management enabled + +``` +PNPM_CONFIG_RUNTIME=from-user +``` + ## `PNPM_CONFIG_RUNTIME=from-user vp install` pnpm-backed vp install applies the same managed-pnpm policy @@ -99,3 +126,13 @@ VITE+ - The Unified Toolchain for the Web PNPM_CONFIG_RUNTIME=false ``` + +## `cd child && PNPM_CONFIG_RUNTIME=from-user vp install` + +vp install from a child directory uses the parent package's managed Node.js + +``` +VITE+ - The Unified Toolchain for the Web + +PNPM_CONFIG_RUNTIME=false +``` diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/pnpm_runtime_management/snapshots/pnpm_runtime_uses_explicit_working_directory.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/pnpm_runtime_management/snapshots/pnpm_runtime_uses_explicit_working_directory.md index d663630c37..355be2cbe6 100644 --- a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/pnpm_runtime_management/snapshots/pnpm_runtime_uses_explicit_working_directory.md +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/pnpm_runtime_management/snapshots/pnpm_runtime_uses_explicit_working_directory.md @@ -36,3 +36,11 @@ the target project's Deno runtime remains managed by pnpm ``` PNPM_CONFIG_RUNTIME=from-user ``` + +## `PATH=${VP_HOME}/bin${PATH_SEPARATOR}${workspace}/system-bin${PATH_SEPARATOR}${PATH} PNPM_CONFIG_RUNTIME=from-user pnpm -C=other install` + +the short equals form resolves the same target project + +``` +PNPM_CONFIG_RUNTIME=from-user +``` diff --git a/crates/vp_global_cli/src/commands/mod.rs b/crates/vp_global_cli/src/commands/mod.rs index 7d03b41a8d..717b82eaa3 100644 --- a/crates/vp_global_cli/src/commands/mod.rs +++ b/crates/vp_global_cli/src/commands/mod.rs @@ -111,8 +111,8 @@ pub(crate) fn warn_missing_local_cli_if_project(cwd: &AbsolutePath) { /// Select the configured JS runtime and prepend its bin directory to PATH. /// This should be called before executing any package manager command. /// -/// If `project_path` contains a package.json, uses the project's runtime -/// (based on devEngines.runtime). Otherwise, falls back to the CLI's runtime. +/// If `project_path` is inside a package, uses the nearest package's runtime. +/// Otherwise, falls back to the CLI's runtime. pub async fn prepend_js_runtime_to_path_env(project_path: &AbsolutePath) -> Result<(), Error> { let config = env::config::load_config().await?; let mut executor = JsExecutor::new(None); @@ -123,12 +123,12 @@ pub async fn prepend_js_runtime_to_path_env(project_path: &AbsolutePath) -> Resu { bin_dir.to_absolute_path_buf() } else { - // 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 runtime = match vt_workspace::find_package_root(project_path) { + Ok(package) => executor.ensure_project_runtime(package.path).await?, + Err(vt_workspace::Error::PackageJsonNotFound(_)) => { + executor.ensure_cli_runtime().await? + } + Err(error) => return Err(error.into()), }; runtime.get_bin_prefix() }; diff --git a/crates/vp_global_cli/src/commands/pnpm_runtime.rs b/crates/vp_global_cli/src/commands/pnpm_runtime.rs index 8976708a3b..f4738d3f9e 100644 --- a/crates/vp_global_cli/src/commands/pnpm_runtime.rs +++ b/crates/vp_global_cli/src/commands/pnpm_runtime.rs @@ -8,6 +8,9 @@ use crate::error::Error; pub(crate) const PNPM_CONFIG_RUNTIME: &str = "PNPM_CONFIG_RUNTIME"; pub(crate) const PNPM_CONFIG_RUNTIME_DISABLED: &str = "false"; +const FORWARDED_ARGUMENT_COMMANDS: &[&str] = + &["create", "dlx", "exec", "restart", "run", "start", "stop", "test"]; + pub(crate) fn command_cwd( tool: &str, cwd: &AbsolutePath, @@ -24,8 +27,13 @@ pub(crate) fn command_cwd( if arg == "--" { break; } + if FORWARDED_ARGUMENT_COMMANDS.contains(&arg.as_str()) { + break; + } if arg == "-C" || arg == "--dir" { dir = Some(args.next()?.as_str()); + } else if let Some(value) = arg.strip_prefix("-C=") { + dir = Some(value); } else if let Some(value) = arg.strip_prefix("--dir=") { dir = Some(value); } @@ -39,6 +47,11 @@ pub(crate) fn command_cwd( if path.is_absolute() { AbsolutePathBuf::new(path.to_path_buf()) } else { Some(cwd.join(path)) } } +pub(crate) fn explicitly_manages_runtime(args: &[String]) -> bool { + args.windows(2).any(|args| args[0] == "runtime" && args[1] == "set") + || args.iter().any(|arg| arg.contains("@runtime:")) +} + pub(crate) async fn should_disable( cwd: &AbsolutePath, node_shim_mode: ShimMode, diff --git a/crates/vp_global_cli/src/shim/dispatch.rs b/crates/vp_global_cli/src/shim/dispatch.rs index 1665a0b010..5802d9a664 100644 --- a/crates/vp_global_cli/src/shim/dispatch.rs +++ b/crates/vp_global_cli/src/shim/dispatch.rs @@ -755,9 +755,12 @@ pub async fn dispatch(tool: &str, args: &[String]) -> i32 { } }; let command_cwd = pnpm_runtime::command_cwd(tool, &cwd, args); - let disable = match command_cwd.as_deref() { - Some(cwd) => pnpm_runtime::should_disable(cwd, node_shim_mode).await, - None => Ok(false), + let disable = if pnpm_runtime::explicitly_manages_runtime(args) { + Ok(false) + } else if let Some(cwd) = command_cwd.as_deref() { + pnpm_runtime::should_disable(cwd, node_shim_mode).await + } else { + Ok(false) }; match disable { Ok(disable) => (disable, command_cwd),