From 88ef1b2aa925bc70f773fa584b2b0f0c47e1059f Mon Sep 17 00:00:00 2001 From: Tarik Ermis Date: Sun, 9 Aug 2026 08:19:05 +0200 Subject: [PATCH 1/4] fix(cli): stamp package-manager lifecycle env for vp run scripts --- crates/vp_pm_cli/src/lib.rs | 2 + crates/vp_pm_cli/src/lifecycle_env.rs | 378 ++++++++++++++++++ packages/cli/binding/index.d.cts | 10 + packages/cli/binding/src/cli/lifecycle_env.rs | 50 +++ packages/cli/binding/src/cli/mod.rs | 20 + packages/cli/binding/src/cli/types.rs | 6 + packages/cli/binding/src/lib.rs | 10 + packages/cli/src/bin.ts | 2 + 8 files changed, 478 insertions(+) create mode 100644 crates/vp_pm_cli/src/lifecycle_env.rs create mode 100644 packages/cli/binding/src/cli/lifecycle_env.rs diff --git a/crates/vp_pm_cli/src/lib.rs b/crates/vp_pm_cli/src/lib.rs index aaace5b2e1..e1c539bbc0 100644 --- a/crates/vp_pm_cli/src/lib.rs +++ b/crates/vp_pm_cli/src/lib.rs @@ -12,6 +12,7 @@ mod config; mod dispatch; mod error; mod helpers; +mod lifecycle_env; mod package_manager; mod request; pub(crate) mod resolution; @@ -21,6 +22,7 @@ pub use cli::{ManagedGlobalCommand, PackageManagerCommand, PmCommand}; pub use config::npm_registry; pub use dispatch::dispatch; pub use error::Error; +pub use lifecycle_env::LifecycleEnvContext; pub use package_manager::{ PackageManager, PackageManagerBuilder, PackageManagerResolution, PackageManagerSource, PackageManagerType, download_package_manager, get_package_manager_type_and_version, diff --git a/crates/vp_pm_cli/src/lifecycle_env.rs b/crates/vp_pm_cli/src/lifecycle_env.rs new file mode 100644 index 0000000000..1a3f42dba2 --- /dev/null +++ b/crates/vp_pm_cli/src/lifecycle_env.rs @@ -0,0 +1,378 @@ +//! Package-manager lifecycle environment for script execution. +//! +//! When pnpm, npm, or yarn run a `package.json` script, they stamp environment +//! variables (`npm_execpath`, `npm_config_user_agent`, …) that let child +//! tooling — npm-run-all, `ni`, package-manager detectors — identify which +//! package manager owns the script run. `vp run` executes scripts itself, so +//! without stamping those variables child runners fall back to npm even in +//! pnpm projects (voidzero-dev/vite-plus#2317). +//! +//! Only the session-constant subset is computed here; stamping it into the +//! process env is the caller's job. Per-script variables +//! (`npm_lifecycle_event`, `npm_lifecycle_script`, `npm_package_*`, +//! `PNPM_SCRIPT_SRC_DIR`) name the script being run or the package that owns +//! it, so they belong to the task engine, which knows each script's name and +//! package. + +use std::{env, ffi::OsString, path::PathBuf}; + +use vt_path::AbsolutePathBuf; + +use crate::package_manager::{PackageManager, PackageManagerType, package_manager_bin_path}; + +/// Everything [`PackageManager::lifecycle_env_vars`] needs beyond the package +/// manager itself. +#[derive(Debug)] +pub struct LifecycleEnvContext { + /// Directory `vp run` was invoked in (`INIT_CWD`). + pub init_cwd: AbsolutePathBuf, + /// Node.js version (i.e. `process.version`) for the user-agent string. + pub node_version: Option, + /// Path to the running Node.js binary (`npm_node_execpath`/`NODE`), i.e. + /// `process.execPath`. + pub node_execpath: Option, +} + +impl PackageManager { + /// The path the package manager identifies itself with in `npm_execpath` + /// when it runs lifecycle scripts: the JS CLI entry for JS distributions + /// (`pnpm.cjs`, `npm-cli.js`, `yarn.js`), the native binary for pnpm >= 12, + /// with the bin shim as fallback. + /// + /// Child runners (e.g. npm-run-all) execute `.js`/`.cjs` values through the + /// current Node.js binary, which works on every platform — unlike + /// extensionless shims on Windows. + #[must_use] + pub fn lifecycle_exec_path(&self) -> AbsolutePathBuf { + let bin_dir = self.install_dir.join("bin"); + let js_entry_name = match self.client { + PackageManagerType::Pnpm => Some("pnpm.cjs"), + PackageManagerType::Npm => Some("npm-cli.js"), + PackageManagerType::Yarn => Some("yarn.js"), + // bun is a native binary; it has no JS CLI entry. + PackageManagerType::Bun => None, + }; + if let Some(name) = js_entry_name { + let entry = bin_dir.join(name); + if entry.as_path().is_file() { + return entry; + } + } + // pnpm >= 12 ships a native binary (see `download_pnpm_native_package_manager`) + // and identifies with the running executable itself. + if matches!(self.client, PackageManagerType::Pnpm) { + let native = if cfg!(windows) { + bin_dir.join("pnpm.native.exe") + } else { + bin_dir.join("pnpm.native") + }; + if native.as_path().is_file() { + return native; + } + } + let shim = package_manager_bin_path(&self.install_dir, &self.client.to_string()); + // The shim breaks child runners on Windows (see above), so if this + // shows up in a log the on-disk layout probably changed. (bun never + // has a JS CLI entry, so the message would be misleading there.) + if js_entry_name.is_some() { + tracing::debug!( + "No JS CLI entry under {bin_dir:?}, using package-manager shim {shim:?} for npm_execpath" + ); + } + shim + } + + /// Environment variables the package manager would stamp when running a + /// `package.json` script, limited to the subset that is constant across a + /// `vp run` session. Empty for bun: what `bun run` stamps is unverified, + /// so its environment is left untouched rather than guessed at. + #[must_use] + pub fn lifecycle_env_vars( + &self, + context: &LifecycleEnvContext, + ) -> Vec<(&'static str, OsString)> { + if matches!(self.client, PackageManagerType::Bun) { + return Vec::new(); + } + + let mut vars = vec![ + ("npm_execpath", self.lifecycle_exec_path().as_path().as_os_str().to_os_string()), + ( + "npm_config_user_agent", + OsString::from(user_agent( + self.client, + &self.version, + context.node_version.as_deref(), + )), + ), + ("INIT_CWD", context.init_cwd.as_path().as_os_str().to_os_string()), + ]; + + if let Some(node_execpath) = &context.node_execpath { + vars.push(("npm_node_execpath", node_execpath.as_os_str().to_os_string())); + vars.push(("NODE", node_execpath.as_os_str().to_os_string())); + } + + vars + } +} + +/// `npm_config_user_agent`, formatted the way the package manager itself does: +/// `pnpm/11.20.0 npm/? node/v22.23.1 linux x64` (pnpm, yarn) or +/// `npm/10.9.8 node/v22.23.1 linux x64 workspaces/false` (npm). +fn user_agent( + package_manager_type: PackageManagerType, + version: &str, + node_version: Option<&str>, +) -> String { + let node = node_version.map_or_else(String::new, |v| vt_str::format!(" node/{v}").to_string()); + let platform = node_platform(env::consts::OS); + let arch = node_arch(env::consts::ARCH); + match package_manager_type { + PackageManagerType::Pnpm | PackageManagerType::Yarn => { + vt_str::format!("{package_manager_type}/{version} npm/?{node} {platform} {arch}") + .to_string() + } + // npm's `workspaces/` flag reflects the `--workspaces` command flag, + // which `vp run` has no analogue of, so it stays `false` (verified + // against npm 10.9.8, including inside a workspace root). + PackageManagerType::Npm => { + vt_str::format!("npm/{version}{node} {platform} {arch} workspaces/false").to_string() + } + // Callers skip bun before building a user agent. + PackageManagerType::Bun => String::new(), + } +} + +/// Map Rust's `env::consts::OS` to Node.js `process.platform` spellings. +fn node_platform(os: &'static str) -> &'static str { + match os { + "macos" => "darwin", + "windows" => "win32", + "solaris" | "illumos" => "sunos", + other => other, + } +} + +/// Map Rust's `env::consts::ARCH` to Node.js `process.arch` spellings. +fn node_arch(arch: &'static str) -> &'static str { + match arch { + "x86" => "ia32", + "x86_64" => "x64", + "aarch64" => "arm64", + "powerpc" => "ppc", + "powerpc64" => "ppc64", + "loongarch64" => "loong64", + other => other, + } +} + +#[cfg(test)] +mod tests { + use std::ffi::OsStr; + + use super::*; + + fn package_manager( + package_manager_type: PackageManagerType, + version: &str, + install_dir: &std::path::Path, + ) -> PackageManager { + PackageManager { + client: package_manager_type, + version: version.into(), + install_dir: AbsolutePathBuf::new(install_dir.to_path_buf()).unwrap(), + } + } + + fn project_dir() -> AbsolutePathBuf { + let path = if cfg!(windows) { "C:\\project" } else { "/project" }; + AbsolutePathBuf::new(path.into()).unwrap() + } + + fn node_path() -> PathBuf { + PathBuf::from(if cfg!(windows) { "C:\\node\\node.exe" } else { "/node/bin/node" }) + } + + fn context(node_version: Option<&str>) -> LifecycleEnvContext { + LifecycleEnvContext { + init_cwd: project_dir(), + node_version: node_version.map(str::to_string), + node_execpath: Some(node_path()), + } + } + + fn vars_map<'a>( + vars: &'a [(&'static str, OsString)], + ) -> std::collections::HashMap<&'a str, &'a OsStr> { + vars.iter().map(|(k, v)| (*k, v.as_os_str())).collect() + } + + fn write_file(path: &std::path::Path) { + std::fs::create_dir_all(path.parent().unwrap()).unwrap(); + std::fs::write(path, "").unwrap(); + } + + #[test] + fn exec_path_prefers_js_cli_entry() { + let cases = [ + (PackageManagerType::Pnpm, "pnpm.cjs"), + (PackageManagerType::Npm, "npm-cli.js"), + (PackageManagerType::Yarn, "yarn.js"), + ]; + for (pm_type, js_entry) in cases { + let dir = tempfile::tempdir().unwrap(); + let install_dir = dir.path().join("pm"); + write_file(&install_dir.join("bin").join(js_entry)); + let pm = package_manager(pm_type, "1.0.0", &install_dir); + assert_eq!( + pm.lifecycle_exec_path().as_path(), + install_dir.join("bin").join(js_entry).as_path() + ); + } + } + + #[test] + fn exec_path_uses_native_binary_for_native_pnpm() { + let dir = tempfile::tempdir().unwrap(); + let install_dir = dir.path().join("pm"); + let native_name = if cfg!(windows) { "pnpm.native.exe" } else { "pnpm.native" }; + write_file(&install_dir.join("bin").join(native_name)); + let pm = package_manager(PackageManagerType::Pnpm, "12.0.0", &install_dir); + assert_eq!( + pm.lifecycle_exec_path().as_path(), + install_dir.join("bin").join(native_name).as_path() + ); + } + + #[test] + fn exec_path_falls_back_to_bin_shim() { + let dir = tempfile::tempdir().unwrap(); + let install_dir = dir.path().join("pm"); + std::fs::create_dir_all(install_dir.join("bin")).unwrap(); + let pm = package_manager(PackageManagerType::Pnpm, "1.0.0", &install_dir); + let expected = if cfg!(windows) { + install_dir.join("bin").join("pnpm.cmd") + } else { + install_dir.join("bin").join("pnpm") + }; + assert_eq!(pm.lifecycle_exec_path().as_path(), expected.as_path()); + } + + #[test] + fn pnpm_vars_match_pnpm_stamps() { + let dir = tempfile::tempdir().unwrap(); + let install_dir = dir.path().join("pm"); + write_file(&install_dir.join("bin").join("pnpm.cjs")); + let pm = package_manager(PackageManagerType::Pnpm, "11.20.0", &install_dir); + + let vars = pm.lifecycle_env_vars(&context(Some("v22.23.1"))); + let map = vars_map(&vars); + + assert_eq!( + map["npm_execpath"], + install_dir.join("bin").join("pnpm.cjs").as_path().as_os_str() + ); + assert_eq!( + map["npm_config_user_agent"], + OsStr::new(&vt_str::format!( + "pnpm/11.20.0 npm/? node/v22.23.1 {} {}", + node_platform(env::consts::OS), + node_arch(env::consts::ARCH) + )) + ); + assert_eq!(map["INIT_CWD"], project_dir().as_path().as_os_str()); + assert_eq!(map["npm_node_execpath"], node_path().as_os_str()); + assert_eq!(map["NODE"], node_path().as_os_str()); + // Per-script; belongs to the task engine, not the session stamp. + assert!(!map.contains_key("PNPM_SCRIPT_SRC_DIR")); + } + + #[test] + fn npm_vars_match_npm_stamps() { + let dir = tempfile::tempdir().unwrap(); + let install_dir = dir.path().join("pm"); + write_file(&install_dir.join("bin").join("npm-cli.js")); + let pm = package_manager(PackageManagerType::Npm, "10.9.8", &install_dir); + + let vars = pm.lifecycle_env_vars(&context(Some("v22.23.1"))); + let map = vars_map(&vars); + + assert_eq!( + map["npm_execpath"], + install_dir.join("bin").join("npm-cli.js").as_path().as_os_str() + ); + assert_eq!( + map["npm_config_user_agent"], + OsStr::new(&vt_str::format!( + "npm/10.9.8 node/v22.23.1 {} {} workspaces/false", + node_platform(env::consts::OS), + node_arch(env::consts::ARCH) + )) + ); + } + + #[test] + fn yarn_user_agent_matches_yarn_stamps() { + let ua = user_agent(PackageManagerType::Yarn, "1.22.22", Some("v22.23.1")); + assert_eq!( + ua, + vt_str::format!( + "yarn/1.22.22 npm/? node/v22.23.1 {} {}", + node_platform(env::consts::OS), + node_arch(env::consts::ARCH) + ) + .to_string() + ); + } + + #[test] + fn user_agent_omits_node_segment_without_version() { + let ua = user_agent(PackageManagerType::Pnpm, "11.20.0", None); + assert_eq!( + ua, + vt_str::format!( + "pnpm/11.20.0 npm/? {} {}", + node_platform(env::consts::OS), + node_arch(env::consts::ARCH) + ) + .to_string() + ); + assert!(!ua.contains("node/")); + } + + #[test] + fn node_execpath_is_optional() { + let dir = tempfile::tempdir().unwrap(); + let install_dir = dir.path().join("pm"); + let pm = package_manager(PackageManagerType::Pnpm, "11.20.0", &install_dir); + let mut context = context(None); + context.node_execpath = None; + + let vars = pm.lifecycle_env_vars(&context); + let map = vars_map(&vars); + + assert!(!map.contains_key("npm_node_execpath")); + assert!(!map.contains_key("NODE")); + assert!(map.contains_key("npm_execpath")); + } + + #[test] + fn bun_stamps_no_lifecycle_vars() { + let dir = tempfile::tempdir().unwrap(); + let install_dir = dir.path().join("pm"); + let pm = package_manager(PackageManagerType::Bun, "1.3.0", &install_dir); + assert!(pm.lifecycle_env_vars(&context(Some("v22.23.1"))).is_empty()); + } + + #[test] + fn node_platform_and_arch_match_node_spellings() { + assert_eq!(node_platform("macos"), "darwin"); + assert_eq!(node_platform("windows"), "win32"); + assert_eq!(node_platform("linux"), "linux"); + assert_eq!(node_arch("x86_64"), "x64"); + assert_eq!(node_arch("x86"), "ia32"); + assert_eq!(node_arch("aarch64"), "arm64"); + assert_eq!(node_arch("powerpc"), "ppc"); + } +} diff --git a/packages/cli/binding/index.d.cts b/packages/cli/binding/index.d.cts index 00bee183d8..6ddae4cf21 100644 --- a/packages/cli/binding/index.d.cts +++ b/packages/cli/binding/index.d.cts @@ -3379,6 +3379,16 @@ export interface CliOptions { cwd?: string; /** CLI arguments (should be process.argv.slice(2) from JavaScript) */ args?: Array; + /** + * Host Node.js version (`process.version`), used for the package-manager + * lifecycle env user agent. + */ + nodeVersion?: string; + /** + * Host Node.js executable path (`process.execPath`), used for the + * package-manager lifecycle env (`npm_node_execpath`/`NODE`). + */ + nodeExecPath?: string; /** Read the vite.config.ts in the Node.js side and return the `lint` and `fmt` config JSON string back to the Rust side */ resolveUniversalViteConfig: (err: Error | null, arg: string) => Promise; } diff --git a/packages/cli/binding/src/cli/lifecycle_env.rs b/packages/cli/binding/src/cli/lifecycle_env.rs new file mode 100644 index 0000000000..158f517bbb --- /dev/null +++ b/packages/cli/binding/src/cli/lifecycle_env.rs @@ -0,0 +1,50 @@ +//! Package-manager lifecycle environment for `vp run`/`vpr` script execution. +//! +//! pnpm, npm, and yarn stamp `npm_execpath`, `npm_config_user_agent`, and +//! friends when running `package.json` scripts so child tooling (npm-run-all, +//! `ni`, package-manager detectors) can tell which package manager owns the +//! run. vite-task spawns scripts with the session env snapshot only, so under +//! `vp run` those variables are missing and child runners fall back to npm +//! even in pnpm projects (#2317). Stamping happens here, before +//! `Session::init` snapshots the process env. + +use vp_pm_cli::{LifecycleEnvContext, PackageManager}; +use vt_path::{AbsolutePath, AbsolutePathBuf}; + +/// Stamp the package-manager lifecycle env into the process environment. +/// +/// `node_version` and `node_execpath` are the host Node.js `process.version` +/// and `process.execPath` when the JS side provides them, keeping the user +/// agent and `npm_node_execpath`/`NODE` in the shape the package managers +/// produce. +pub(super) fn stamp_package_manager_lifecycle_env( + pm: &PackageManager, + cwd: &AbsolutePath, + node_version: Option<&str>, + node_execpath: Option<&str>, +) { + if node_version.is_none() || node_execpath.is_none() { + tracing::debug!( + "Host Node.js version/exec path not provided; stamping a partial package-manager lifecycle env" + ); + } + // Like pnpm's INIT_CWD this is the directory the command was invoked from, + // which stays the process cwd even when `--cwd` redirects task resolution. + let init_cwd = std::env::current_dir() + .ok() + .and_then(AbsolutePathBuf::new) + .unwrap_or_else(|| cwd.to_absolute_path_buf()); + let context = LifecycleEnvContext { + init_cwd, + node_version: node_version.map(str::to_string), + node_execpath: node_execpath.map(std::path::PathBuf::from), + }; + for (name, value) in pm.lifecycle_env_vars(&context) { + // SAFETY: `set_var` is unsound while another thread may read the + // environment. This runs in the same startup window as the PATH + // prepend right above it in `execute_vite_task_command` (before + // `Session::init` spawns task threads), so it adds no exposure + // beyond that existing call. + unsafe { std::env::set_var(name, value) }; + } +} diff --git a/packages/cli/binding/src/cli/mod.rs b/packages/cli/binding/src/cli/mod.rs index 713cc7b275..3bb4c7e5a3 100644 --- a/packages/cli/binding/src/cli/mod.rs +++ b/packages/cli/binding/src/cli/mod.rs @@ -7,6 +7,7 @@ mod app_target; mod execution; mod handler; mod help; +mod lifecycle_env; mod resolver; mod script_note; mod types; @@ -238,6 +239,9 @@ async fn execute_vite_task_command( let (workspace_root, _) = vt_workspace::find_workspace_root(&cwd)?; let workspace_path: Arc = workspace_root.path.into(); + let node_version = options.as_ref().and_then(|o| o.node_version.clone()); + let node_exec_path = options.as_ref().and_then(|o| o.node_exec_path.clone()); + let resolve_vite_config_fn = options .as_ref() .map(|o| Arc::clone(&o.resolve_universal_vite_config)) @@ -260,6 +264,22 @@ async fn execute_vite_task_command( if let Ok(pm) = vp_pm_cli::PackageManager::builder(&cwd).build().await { let bin_prefix = pm.get_bin_prefix(); let _ = prepend_to_path_env(&bin_prefix, PrependOptions::default()); + + // Stamp the package-manager lifecycle env (`npm_execpath`, + // `npm_config_user_agent`, …) like pnpm/npm/yarn would, so child + // runners inside scripts can detect the package manager (#2317). + // Session::init snapshots the process env, so this must also happen + // before it. + lifecycle_env::stamp_package_manager_lifecycle_env( + &pm, + &cwd, + node_version.as_deref(), + node_exec_path.as_deref(), + ); + } else { + tracing::debug!( + "Package manager resolution failed; skipping lifecycle env stamping for {cwd:?}" + ); } let session = Session::init(SessionConfig { diff --git a/packages/cli/binding/src/cli/types.rs b/packages/cli/binding/src/cli/types.rs index d9193b94b6..fc099b4c8e 100644 --- a/packages/cli/binding/src/cli/types.rs +++ b/packages/cli/binding/src/cli/types.rs @@ -137,6 +137,12 @@ pub struct CliOptions { pub pack: BoxedResolverFn, pub doc: BoxedResolverFn, pub resolve_universal_vite_config: ViteConfigResolverFn, + /// Host Node.js version (`process.version`), used for the package-manager + /// lifecycle env user agent. + pub node_version: Option, + /// Host Node.js executable path (`process.execPath`), used for the + /// package-manager lifecycle env (`npm_node_execpath`/`NODE`). + pub node_exec_path: Option, } /// A resolved subcommand ready for execution. diff --git a/packages/cli/binding/src/lib.rs b/packages/cli/binding/src/lib.rs index 0c48c33e7f..546124b795 100644 --- a/packages/cli/binding/src/lib.rs +++ b/packages/cli/binding/src/lib.rs @@ -74,6 +74,12 @@ pub struct CliOptions { pub cwd: Option, /// CLI arguments (should be process.argv.slice(2) from JavaScript) pub args: Option>, + /// Host Node.js version (`process.version`), used for the package-manager + /// lifecycle env user agent. + pub node_version: Option, + /// Host Node.js executable path (`process.execPath`), used for the + /// package-manager lifecycle env (`npm_node_execpath`/`NODE`). + pub node_exec_path: Option, /// Read the vite.config.ts in the Node.js side and return the `lint` and `fmt` config JSON string back to the Rust side pub resolve_universal_vite_config: Arc>>, } @@ -173,6 +179,8 @@ pub async fn run(options: CliOptions) -> Result { let doc_tsf = options.doc; let resolve_universal_vite_config_tsf = options.resolve_universal_vite_config; let args = options.args; + let node_version = options.node_version; + let node_exec_path = options.node_exec_path; // Create a channel to receive the result from the worker thread let (tx, rx) = tokio::sync::oneshot::channel(); @@ -192,6 +200,8 @@ pub async fn run(options: CliOptions) -> Result { resolve_universal_vite_config: create_vite_config_resolver( resolve_universal_vite_config_tsf, ), + node_version, + node_exec_path, }; // Create a new single-threaded runtime for non-Send futures diff --git a/packages/cli/src/bin.ts b/packages/cli/src/bin.ts index 60fc0b7ae8..1adf90680e 100644 --- a/packages/cli/src/bin.ts +++ b/packages/cli/src/bin.ts @@ -138,6 +138,8 @@ if (maybePrintCommandHelp(args)) { test, doc, resolveUniversalViteConfig, + nodeVersion: process.version, + nodeExecPath: process.execPath, args: rustCliArgs, }); From abd0cd3e52b44882f6770ffcfa1d9e429a7295d1 Mon Sep 17 00:00:00 2001 From: Tarik Ermis Date: Mon, 10 Aug 2026 12:35:04 +0200 Subject: [PATCH 2/4] test(pm): add snapshot coverage for package-manager lifecycle env stamp Covers voidzero-dev/vite-plus#2317: snapshot the session-constant lifecycle env computed for fixture package-manager install layouts (pnpm/npm/yarn JS CLI entries, native pnpm binary, shim fallback, and bun's empty stamp) so a regression that drops the stamp or changes exec-path resolution fails the test. --- Cargo.lock | 1 + crates/vp_pm_cli/Cargo.toml | 1 + crates/vp_pm_cli/src/lifecycle_env.rs | 131 ++++++++++++++++++ ...snapshot_bun_stamps_no_lifecycle_vars.snap | 5 + ...le_env__tests__snapshot_npm_js_layout.snap | 10 ++ ...t_pnpm_js_entry_preferred_over_native.snap | 10 ++ ...e_env__tests__snapshot_pnpm_js_layout.snap | 10 ++ ...v__tests__snapshot_pnpm_native_layout.snap | 10 ++ ...v__tests__snapshot_pnpm_shim_fallback.snap | 10 ++ ...e_env__tests__snapshot_yarn_js_layout.snap | 10 ++ 10 files changed, 198 insertions(+) create mode 100644 crates/vp_pm_cli/src/snapshots/vp_pm_cli__lifecycle_env__tests__snapshot_bun_stamps_no_lifecycle_vars.snap create mode 100644 crates/vp_pm_cli/src/snapshots/vp_pm_cli__lifecycle_env__tests__snapshot_npm_js_layout.snap create mode 100644 crates/vp_pm_cli/src/snapshots/vp_pm_cli__lifecycle_env__tests__snapshot_pnpm_js_entry_preferred_over_native.snap create mode 100644 crates/vp_pm_cli/src/snapshots/vp_pm_cli__lifecycle_env__tests__snapshot_pnpm_js_layout.snap create mode 100644 crates/vp_pm_cli/src/snapshots/vp_pm_cli__lifecycle_env__tests__snapshot_pnpm_native_layout.snap create mode 100644 crates/vp_pm_cli/src/snapshots/vp_pm_cli__lifecycle_env__tests__snapshot_pnpm_shim_fallback.snap create mode 100644 crates/vp_pm_cli/src/snapshots/vp_pm_cli__lifecycle_env__tests__snapshot_yarn_js_layout.snap diff --git a/Cargo.lock b/Cargo.lock index 1801b20276..d642003e70 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8539,6 +8539,7 @@ dependencies = [ "httpmock", "indicatif", "indoc", + "insta", "node-semver", "pathdiff", "reqwest", diff --git a/crates/vp_pm_cli/Cargo.toml b/crates/vp_pm_cli/Cargo.toml index c0e5c7b181..c48d86a454 100644 --- a/crates/vp_pm_cli/Cargo.toml +++ b/crates/vp_pm_cli/Cargo.toml @@ -49,6 +49,7 @@ doctest = false [dev-dependencies] httpmock = { workspace = true } +insta = { workspace = true } test-log = { workspace = true } [lints] diff --git a/crates/vp_pm_cli/src/lifecycle_env.rs b/crates/vp_pm_cli/src/lifecycle_env.rs index 1a3f42dba2..68c2b9bdad 100644 --- a/crates/vp_pm_cli/src/lifecycle_env.rs +++ b/crates/vp_pm_cli/src/lifecycle_env.rs @@ -375,4 +375,135 @@ mod tests { assert_eq!(node_arch("aarch64"), "arm64"); assert_eq!(node_arch("powerpc"), "ppc"); } + + fn native_pnpm_bin_name() -> &'static str { + if cfg!(windows) { "pnpm.native.exe" } else { "pnpm.native" } + } + + /// Render the session lifecycle stamp for a fixture install layout as + /// snapshot-stable text: machine- and platform-dependent values (tempdir, + /// path separators, `.exe`/`.cmd` suffixes, the user-agent platform/arch + /// tail) become placeholders. A regression of voidzero-dev/vite-plus#2317 — + /// scripts spawned without the package-manager lifecycle env — empties or + /// changes the stamp and fails the snapshot. + fn render_lifecycle_stamp( + package_manager_type: PackageManagerType, + version: &str, + bin_entries: &[&str], + ) -> String { + let dir = tempfile::tempdir().unwrap(); + let install_dir = dir.path().join("pm"); + for entry in bin_entries { + write_file(&install_dir.join("bin").join(entry)); + } + let pm = package_manager(package_manager_type, version, &install_dir); + + let vars = pm.lifecycle_env_vars(&context(Some("v22.23.1"))); + let mut rendered = vt_str::format!("{package_manager_type} {version}").to_string(); + for (key, value) in &vars { + rendered.push_str(&vt_str::format!("\n{key}={}", value.to_string_lossy())); + } + + use cow_utils::CowUtils; + let install_dir = install_dir.as_os_str().to_string_lossy(); + let init_cwd = project_dir(); + let init_cwd = init_cwd.as_path().as_os_str().to_string_lossy(); + let node = node_path(); + let node = node.as_os_str().to_string_lossy(); + let platform_arch = + vt_str::format!("{} {}", node_platform(env::consts::OS), node_arch(env::consts::ARCH)) + .to_string(); + rendered + .cow_replace(install_dir.as_ref(), "[INSTALL_DIR]") + .cow_replace(init_cwd.as_ref(), "[INIT_CWD]") + .cow_replace(node.as_ref(), "[NODE]") + .cow_replace("pnpm.native.exe", "pnpm.native") + .cow_replace("pnpm.cmd", "pnpm") + .cow_replace('\\', "/") + .cow_replace(&platform_arch, "[platform] [arch]") + .into_owned() + } + + #[test] + #[expect( + clippy::disallowed_macros, + reason = "insta::assert_snapshot! expands to std format! for its failure message" + )] + fn snapshot_pnpm_js_layout() { + insta::assert_snapshot!(render_lifecycle_stamp( + PackageManagerType::Pnpm, + "11.20.0", + &["pnpm.cjs"] + )); + } + + #[test] + #[expect( + clippy::disallowed_macros, + reason = "insta::assert_snapshot! expands to std format! for its failure message" + )] + fn snapshot_pnpm_native_layout() { + insta::assert_snapshot!(render_lifecycle_stamp( + PackageManagerType::Pnpm, + "12.0.0", + &[native_pnpm_bin_name()] + )); + } + + #[test] + #[expect( + clippy::disallowed_macros, + reason = "insta::assert_snapshot! expands to std format! for its failure message" + )] + fn snapshot_pnpm_js_entry_preferred_over_native() { + insta::assert_snapshot!(render_lifecycle_stamp( + PackageManagerType::Pnpm, + "12.0.0", + &["pnpm.cjs", native_pnpm_bin_name()] + )); + } + + #[test] + #[expect( + clippy::disallowed_macros, + reason = "insta::assert_snapshot! expands to std format! for its failure message" + )] + fn snapshot_pnpm_shim_fallback() { + insta::assert_snapshot!(render_lifecycle_stamp(PackageManagerType::Pnpm, "11.20.0", &[])); + } + + #[test] + #[expect( + clippy::disallowed_macros, + reason = "insta::assert_snapshot! expands to std format! for its failure message" + )] + fn snapshot_npm_js_layout() { + insta::assert_snapshot!(render_lifecycle_stamp( + PackageManagerType::Npm, + "10.9.8", + &["npm-cli.js"] + )); + } + + #[test] + #[expect( + clippy::disallowed_macros, + reason = "insta::assert_snapshot! expands to std format! for its failure message" + )] + fn snapshot_yarn_js_layout() { + insta::assert_snapshot!(render_lifecycle_stamp( + PackageManagerType::Yarn, + "1.22.22", + &["yarn.js"] + )); + } + + #[test] + #[expect( + clippy::disallowed_macros, + reason = "insta::assert_snapshot! expands to std format! for its failure message" + )] + fn snapshot_bun_stamps_no_lifecycle_vars() { + insta::assert_snapshot!(render_lifecycle_stamp(PackageManagerType::Bun, "1.3.0", &[])); + } } diff --git a/crates/vp_pm_cli/src/snapshots/vp_pm_cli__lifecycle_env__tests__snapshot_bun_stamps_no_lifecycle_vars.snap b/crates/vp_pm_cli/src/snapshots/vp_pm_cli__lifecycle_env__tests__snapshot_bun_stamps_no_lifecycle_vars.snap new file mode 100644 index 0000000000..7d0c03a5a2 --- /dev/null +++ b/crates/vp_pm_cli/src/snapshots/vp_pm_cli__lifecycle_env__tests__snapshot_bun_stamps_no_lifecycle_vars.snap @@ -0,0 +1,5 @@ +--- +source: crates/vp_pm_cli/src/lifecycle_env.rs +expression: "render_lifecycle_stamp(PackageManagerType::Bun, \"1.3.0\", &[])" +--- +bun 1.3.0 diff --git a/crates/vp_pm_cli/src/snapshots/vp_pm_cli__lifecycle_env__tests__snapshot_npm_js_layout.snap b/crates/vp_pm_cli/src/snapshots/vp_pm_cli__lifecycle_env__tests__snapshot_npm_js_layout.snap new file mode 100644 index 0000000000..bb5d8f2636 --- /dev/null +++ b/crates/vp_pm_cli/src/snapshots/vp_pm_cli__lifecycle_env__tests__snapshot_npm_js_layout.snap @@ -0,0 +1,10 @@ +--- +source: crates/vp_pm_cli/src/lifecycle_env.rs +expression: "render_lifecycle_stamp(PackageManagerType::Npm, \"10.9.8\", &[\"npm-cli.js\"])" +--- +npm 10.9.8 +npm_execpath=[INSTALL_DIR]/bin/npm-cli.js +npm_config_user_agent=npm/10.9.8 node/v22.23.1 [platform] [arch] workspaces/false +INIT_CWD=[INIT_CWD] +npm_node_execpath=[NODE] +NODE=[NODE] diff --git a/crates/vp_pm_cli/src/snapshots/vp_pm_cli__lifecycle_env__tests__snapshot_pnpm_js_entry_preferred_over_native.snap b/crates/vp_pm_cli/src/snapshots/vp_pm_cli__lifecycle_env__tests__snapshot_pnpm_js_entry_preferred_over_native.snap new file mode 100644 index 0000000000..a4e26b8a86 --- /dev/null +++ b/crates/vp_pm_cli/src/snapshots/vp_pm_cli__lifecycle_env__tests__snapshot_pnpm_js_entry_preferred_over_native.snap @@ -0,0 +1,10 @@ +--- +source: crates/vp_pm_cli/src/lifecycle_env.rs +expression: "render_lifecycle_stamp(PackageManagerType::Pnpm, \"12.0.0\",\n&[\"pnpm.cjs\", native_pnpm_bin_name()])" +--- +pnpm 12.0.0 +npm_execpath=[INSTALL_DIR]/bin/pnpm.cjs +npm_config_user_agent=pnpm/12.0.0 npm/? node/v22.23.1 [platform] [arch] +INIT_CWD=[INIT_CWD] +npm_node_execpath=[NODE] +NODE=[NODE] diff --git a/crates/vp_pm_cli/src/snapshots/vp_pm_cli__lifecycle_env__tests__snapshot_pnpm_js_layout.snap b/crates/vp_pm_cli/src/snapshots/vp_pm_cli__lifecycle_env__tests__snapshot_pnpm_js_layout.snap new file mode 100644 index 0000000000..e713a4651b --- /dev/null +++ b/crates/vp_pm_cli/src/snapshots/vp_pm_cli__lifecycle_env__tests__snapshot_pnpm_js_layout.snap @@ -0,0 +1,10 @@ +--- +source: crates/vp_pm_cli/src/lifecycle_env.rs +expression: "render_lifecycle_stamp(PackageManagerType::Pnpm, \"11.20.0\", &[\"pnpm.cjs\"])" +--- +pnpm 11.20.0 +npm_execpath=[INSTALL_DIR]/bin/pnpm.cjs +npm_config_user_agent=pnpm/11.20.0 npm/? node/v22.23.1 [platform] [arch] +INIT_CWD=[INIT_CWD] +npm_node_execpath=[NODE] +NODE=[NODE] diff --git a/crates/vp_pm_cli/src/snapshots/vp_pm_cli__lifecycle_env__tests__snapshot_pnpm_native_layout.snap b/crates/vp_pm_cli/src/snapshots/vp_pm_cli__lifecycle_env__tests__snapshot_pnpm_native_layout.snap new file mode 100644 index 0000000000..920cc26691 --- /dev/null +++ b/crates/vp_pm_cli/src/snapshots/vp_pm_cli__lifecycle_env__tests__snapshot_pnpm_native_layout.snap @@ -0,0 +1,10 @@ +--- +source: crates/vp_pm_cli/src/lifecycle_env.rs +expression: "render_lifecycle_stamp(PackageManagerType::Pnpm, \"12.0.0\",\n&[native_pnpm_bin_name()])" +--- +pnpm 12.0.0 +npm_execpath=[INSTALL_DIR]/bin/pnpm.native +npm_config_user_agent=pnpm/12.0.0 npm/? node/v22.23.1 [platform] [arch] +INIT_CWD=[INIT_CWD] +npm_node_execpath=[NODE] +NODE=[NODE] diff --git a/crates/vp_pm_cli/src/snapshots/vp_pm_cli__lifecycle_env__tests__snapshot_pnpm_shim_fallback.snap b/crates/vp_pm_cli/src/snapshots/vp_pm_cli__lifecycle_env__tests__snapshot_pnpm_shim_fallback.snap new file mode 100644 index 0000000000..1c80975fa4 --- /dev/null +++ b/crates/vp_pm_cli/src/snapshots/vp_pm_cli__lifecycle_env__tests__snapshot_pnpm_shim_fallback.snap @@ -0,0 +1,10 @@ +--- +source: crates/vp_pm_cli/src/lifecycle_env.rs +expression: "render_lifecycle_stamp(PackageManagerType::Pnpm, \"11.20.0\", &[])" +--- +pnpm 11.20.0 +npm_execpath=[INSTALL_DIR]/bin/pnpm +npm_config_user_agent=pnpm/11.20.0 npm/? node/v22.23.1 [platform] [arch] +INIT_CWD=[INIT_CWD] +npm_node_execpath=[NODE] +NODE=[NODE] diff --git a/crates/vp_pm_cli/src/snapshots/vp_pm_cli__lifecycle_env__tests__snapshot_yarn_js_layout.snap b/crates/vp_pm_cli/src/snapshots/vp_pm_cli__lifecycle_env__tests__snapshot_yarn_js_layout.snap new file mode 100644 index 0000000000..b5069bd132 --- /dev/null +++ b/crates/vp_pm_cli/src/snapshots/vp_pm_cli__lifecycle_env__tests__snapshot_yarn_js_layout.snap @@ -0,0 +1,10 @@ +--- +source: crates/vp_pm_cli/src/lifecycle_env.rs +expression: "render_lifecycle_stamp(PackageManagerType::Yarn, \"1.22.22\", &[\"yarn.js\"])" +--- +yarn 1.22.22 +npm_execpath=[INSTALL_DIR]/bin/yarn.js +npm_config_user_agent=yarn/1.22.22 npm/? node/v22.23.1 [platform] [arch] +INIT_CWD=[INIT_CWD] +npm_node_execpath=[NODE] +NODE=[NODE] From 59a8c835298f15bbe2dd7adffee7a14711ea719e Mon Sep 17 00:00:00 2001 From: Tarik Ermis Date: Mon, 10 Aug 2026 15:02:26 +0200 Subject: [PATCH 3/4] test(cli): add PTY snapshot coverage for vp run lifecycle env stamp CLI-level regression test for #2317: a fixture pnpm project runs a package.json script via vp run that surfaces npm_execpath, npm_config_user_agent, and INIT_CWD. Pre-fix all three were undefined in the script process, so child tooling like npm-run-all fell back to npm. A fake managed pnpm install under VP_HOME keeps the case offline. --- .../vite_task_lifecycle_env/check-env.js | 14 ++++++++++ .../vite_task_lifecycle_env/package.json | 8 ++++++ .../scripts/setup-fake-pnpm.cjs | 28 +++++++++++++++++++ .../vite_task_lifecycle_env/snapshots.toml | 15 ++++++++++ .../snapshots/vite_task_lifecycle_env.md | 20 +++++++++++++ 5 files changed, 85 insertions(+) create mode 100644 crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/vite_task_lifecycle_env/check-env.js create mode 100644 crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/vite_task_lifecycle_env/package.json create mode 100644 crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/vite_task_lifecycle_env/scripts/setup-fake-pnpm.cjs create mode 100644 crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/vite_task_lifecycle_env/snapshots.toml create mode 100644 crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/vite_task_lifecycle_env/snapshots/vite_task_lifecycle_env.md diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/vite_task_lifecycle_env/check-env.js b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/vite_task_lifecycle_env/check-env.js new file mode 100644 index 0000000000..99d28e0d5e --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/vite_task_lifecycle_env/check-env.js @@ -0,0 +1,14 @@ +// Surfaces the package-manager lifecycle env that `vp run` stamps for +// package.json scripts (#2317): before the fix every variable below printed +// `(undefined)`, so child tooling (npm-run-all, `ni`) could not detect pnpm +// and fell back to npm. The user-agent platform/arch tail (`linux x64`) is +// the one machine-dependent value the suite redaction does not mask, so it +// is normalized here from the runtime's own platform/arch. +const vars = ['npm_execpath', 'npm_config_user_agent', 'INIT_CWD']; +for (const name of vars) { + let value = process.env[name] ?? '(undefined)'; + if (name === 'npm_config_user_agent') { + value = value.replace(`${process.platform} ${process.arch}`, ' '); + } + console.log(`${name}=${value}`); +} diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/vite_task_lifecycle_env/package.json b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/vite_task_lifecycle_env/package.json new file mode 100644 index 0000000000..ac4d5d83bf --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/vite_task_lifecycle_env/package.json @@ -0,0 +1,8 @@ +{ + "name": "vite-task-lifecycle-env", + "private": true, + "scripts": { + "check-env": "node check-env.js" + }, + "packageManager": "pnpm@11.0.0" +} diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/vite_task_lifecycle_env/scripts/setup-fake-pnpm.cjs b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/vite_task_lifecycle_env/scripts/setup-fake-pnpm.cjs new file mode 100644 index 0000000000..3958a21289 --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/vite_task_lifecycle_env/scripts/setup-fake-pnpm.cjs @@ -0,0 +1,28 @@ +const { chmodSync, mkdirSync, writeFileSync } = require('node:fs'); +const { join } = require('node:path'); + +const vpHome = process.env.VP_HOME; +if (!vpHome) { + throw new Error('VP_HOME is required'); +} + +// The layout of a managed pnpm install: the JS CLI entry `pnpm.cjs` plus the +// platform shims, so package-manager resolution finds pnpm@11.0.0 without a +// download and `npm_execpath` resolves to the real JS CLI entry. +const binDir = join(vpHome, 'package_manager', 'pnpm', '11.0.0', 'pnpm', 'bin'); +mkdirSync(binDir, { recursive: true }); + +writeFileSync( + join(binDir, 'pnpm.cjs'), + "console.log('pnpm ' + process.argv.slice(2).join(' '));\n", +); + +const unixShim = join(binDir, 'pnpm'); +writeFileSync(unixShim, "#!/usr/bin/env node\nrequire('./pnpm.cjs');\n"); +chmodSync(unixShim, 0o755); + +writeFileSync(join(binDir, 'pnpm.cmd'), '@echo off\r\nnode "%~dp0pnpm.cjs" %*\r\n'); +writeFileSync( + join(binDir, 'pnpm.ps1'), + 'node "$PSScriptRoot/pnpm.cjs" @args\nexit $LASTEXITCODE\n', +); diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/vite_task_lifecycle_env/snapshots.toml b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/vite_task_lifecycle_env/snapshots.toml new file mode 100644 index 0000000000..33d0470f03 --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/vite_task_lifecycle_env/snapshots.toml @@ -0,0 +1,15 @@ +[[case]] +name = "vite_task_lifecycle_env" +vp = "local" +comment = """ +Regression test for #2317: `vp run` stamps the package-manager lifecycle env +(npm_execpath, npm_config_user_agent, INIT_CWD) for package.json scripts, so +child tooling like npm-run-all detects pnpm instead of falling back to npm. +Pre-fix every variable printed `(undefined)`. The fake managed pnpm install +under VP_HOME keeps the case offline; the script normalizes the user-agent +platform/arch tail that suite redaction does not mask. +""" +steps = [ + { argv = ["node", "scripts/setup-fake-pnpm.cjs"], snapshot = false }, + { argv = ["vp", "run", "check-env"] }, +] diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/vite_task_lifecycle_env/snapshots/vite_task_lifecycle_env.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/vite_task_lifecycle_env/snapshots/vite_task_lifecycle_env.md new file mode 100644 index 0000000000..002b4179e2 --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/vite_task_lifecycle_env/snapshots/vite_task_lifecycle_env.md @@ -0,0 +1,20 @@ +# vite_task_lifecycle_env + +Regression test for #2317: `vp run` stamps the package-manager lifecycle env +(npm_execpath, npm_config_user_agent, INIT_CWD) for package.json scripts, so +child tooling like npm-run-all detects pnpm instead of falling back to npm. +Pre-fix every variable printed `(undefined)`. The fake managed pnpm install +under VP_HOME keeps the case offline; the script normalizes the user-agent +platform/arch tail that suite redaction does not mask. + +## `node scripts/setup-fake-pnpm.cjs` + + +## `vp run check-env` + +``` +$ node check-env.js ⊘ cache disabled +npm_execpath=/.vite-plus/package_manager/pnpm//pnpm/bin/pnpm.cjs +npm_config_user_agent=pnpm/ npm/? node/ +INIT_CWD= +``` From 9cbc024464c4345721be8785422b126f2fa0900c Mon Sep 17 00:00:00 2001 From: Tarik Ermis Date: Mon, 10 Aug 2026 19:48:15 +0200 Subject: [PATCH 4/4] test(pm): revert insta snapshots; document lifecycle env var origins Revert the crate-level insta snapshot tests (the CLI-level vp_cli_snapshots case covers the bug fix end to end), and document where each lifecycle env var name and format comes from: npm's set-envs.js and user-agent definition, pnpm's @pnpm/npm-lifecycle and config userAgent, verified against pnpm 11.21.0 and npm 10.9.8. --- Cargo.lock | 1 - crates/vp_pm_cli/Cargo.toml | 1 - crates/vp_pm_cli/src/lifecycle_env.rs | 156 +++--------------- ...snapshot_bun_stamps_no_lifecycle_vars.snap | 5 - ...le_env__tests__snapshot_npm_js_layout.snap | 10 -- ...t_pnpm_js_entry_preferred_over_native.snap | 10 -- ...e_env__tests__snapshot_pnpm_js_layout.snap | 10 -- ...v__tests__snapshot_pnpm_native_layout.snap | 10 -- ...v__tests__snapshot_pnpm_shim_fallback.snap | 10 -- ...e_env__tests__snapshot_yarn_js_layout.snap | 10 -- 10 files changed, 25 insertions(+), 198 deletions(-) delete mode 100644 crates/vp_pm_cli/src/snapshots/vp_pm_cli__lifecycle_env__tests__snapshot_bun_stamps_no_lifecycle_vars.snap delete mode 100644 crates/vp_pm_cli/src/snapshots/vp_pm_cli__lifecycle_env__tests__snapshot_npm_js_layout.snap delete mode 100644 crates/vp_pm_cli/src/snapshots/vp_pm_cli__lifecycle_env__tests__snapshot_pnpm_js_entry_preferred_over_native.snap delete mode 100644 crates/vp_pm_cli/src/snapshots/vp_pm_cli__lifecycle_env__tests__snapshot_pnpm_js_layout.snap delete mode 100644 crates/vp_pm_cli/src/snapshots/vp_pm_cli__lifecycle_env__tests__snapshot_pnpm_native_layout.snap delete mode 100644 crates/vp_pm_cli/src/snapshots/vp_pm_cli__lifecycle_env__tests__snapshot_pnpm_shim_fallback.snap delete mode 100644 crates/vp_pm_cli/src/snapshots/vp_pm_cli__lifecycle_env__tests__snapshot_yarn_js_layout.snap diff --git a/Cargo.lock b/Cargo.lock index d642003e70..1801b20276 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8539,7 +8539,6 @@ dependencies = [ "httpmock", "indicatif", "indoc", - "insta", "node-semver", "pathdiff", "reqwest", diff --git a/crates/vp_pm_cli/Cargo.toml b/crates/vp_pm_cli/Cargo.toml index c48d86a454..c0e5c7b181 100644 --- a/crates/vp_pm_cli/Cargo.toml +++ b/crates/vp_pm_cli/Cargo.toml @@ -49,7 +49,6 @@ doctest = false [dev-dependencies] httpmock = { workspace = true } -insta = { workspace = true } test-log = { workspace = true } [lints] diff --git a/crates/vp_pm_cli/src/lifecycle_env.rs b/crates/vp_pm_cli/src/lifecycle_env.rs index 68c2b9bdad..74abab91d9 100644 --- a/crates/vp_pm_cli/src/lifecycle_env.rs +++ b/crates/vp_pm_cli/src/lifecycle_env.rs @@ -39,6 +39,13 @@ impl PackageManager { /// (`pnpm.cjs`, `npm-cli.js`, `yarn.js`), the native binary for pnpm >= 12, /// with the bin shim as fallback. /// + /// Mirrors the package managers' own stamping code: npm sets + /// `npm_execpath = config.npmBin` (its `bin/npm-cli.js`), and pnpm sets it + /// to the CLI's `process.argv[1]` — or `process.execPath` when bundled as + /// a binary, the case the native pnpm >= 12 layout mirrors + /// (https://github.com/npm/cli/blob/latest/workspaces/config/lib/set-envs.js, + /// https://github.com/pnpm/npm-lifecycle/blob/main/index.js). + /// /// Child runners (e.g. npm-run-all) execute `.js`/`.cjs` values through the /// current Node.js binary, which works on every platform — unlike /// extensionless shims on Windows. @@ -86,6 +93,17 @@ impl PackageManager { /// `package.json` script, limited to the subset that is constant across a /// `vp run` session. Empty for bun: what `bun run` stamps is unverified, /// so its environment is left untouched rather than guessed at. + /// + /// Names follow npm's lifecycle script environment + /// (https://docs.npmjs.com/cli/v10/using-npm/scripts#environment), which + /// pnpm reproduces by routing `pnpm run` scripts through + /// `@pnpm/npm-lifecycle` + /// (https://github.com/pnpm/pnpm/blob/main/pnpm11/exec/lifecycle/src/runLifecycleHook.ts): + /// `INIT_CWD` is the cwd the command was invoked in, `npm_node_execpath` + /// and `NODE` the running Node.js binary + /// (https://github.com/npm/cli/blob/latest/workspaces/config/lib/set-envs.js, + /// https://github.com/pnpm/npm-lifecycle/blob/main/index.js). + /// Verified against pnpm 11.21.0 and npm 10.9.8. #[must_use] pub fn lifecycle_env_vars( &self, @@ -120,6 +138,13 @@ impl PackageManager { /// `npm_config_user_agent`, formatted the way the package manager itself does: /// `pnpm/11.20.0 npm/? node/v22.23.1 linux x64` (pnpm, yarn) or /// `npm/10.9.8 node/v22.23.1 linux x64 workspaces/false` (npm). +/// +/// Formats follow pnpm's resolved `userAgent` config +/// (`{name}/{version} npm/? node/{version} {platform} {arch}`, +/// https://github.com/pnpm/pnpm/blob/main/pnpm11/config/reader/src/index.ts) +/// and npm's `user-agent` definition (`npm/{npm-version} node/{node-version} +/// {platform} {arch} workspaces/{workspaces}`, +/// https://github.com/npm/cli/blob/latest/workspaces/config/lib/definitions/definitions.js). fn user_agent( package_manager_type: PackageManagerType, version: &str, @@ -375,135 +400,4 @@ mod tests { assert_eq!(node_arch("aarch64"), "arm64"); assert_eq!(node_arch("powerpc"), "ppc"); } - - fn native_pnpm_bin_name() -> &'static str { - if cfg!(windows) { "pnpm.native.exe" } else { "pnpm.native" } - } - - /// Render the session lifecycle stamp for a fixture install layout as - /// snapshot-stable text: machine- and platform-dependent values (tempdir, - /// path separators, `.exe`/`.cmd` suffixes, the user-agent platform/arch - /// tail) become placeholders. A regression of voidzero-dev/vite-plus#2317 — - /// scripts spawned without the package-manager lifecycle env — empties or - /// changes the stamp and fails the snapshot. - fn render_lifecycle_stamp( - package_manager_type: PackageManagerType, - version: &str, - bin_entries: &[&str], - ) -> String { - let dir = tempfile::tempdir().unwrap(); - let install_dir = dir.path().join("pm"); - for entry in bin_entries { - write_file(&install_dir.join("bin").join(entry)); - } - let pm = package_manager(package_manager_type, version, &install_dir); - - let vars = pm.lifecycle_env_vars(&context(Some("v22.23.1"))); - let mut rendered = vt_str::format!("{package_manager_type} {version}").to_string(); - for (key, value) in &vars { - rendered.push_str(&vt_str::format!("\n{key}={}", value.to_string_lossy())); - } - - use cow_utils::CowUtils; - let install_dir = install_dir.as_os_str().to_string_lossy(); - let init_cwd = project_dir(); - let init_cwd = init_cwd.as_path().as_os_str().to_string_lossy(); - let node = node_path(); - let node = node.as_os_str().to_string_lossy(); - let platform_arch = - vt_str::format!("{} {}", node_platform(env::consts::OS), node_arch(env::consts::ARCH)) - .to_string(); - rendered - .cow_replace(install_dir.as_ref(), "[INSTALL_DIR]") - .cow_replace(init_cwd.as_ref(), "[INIT_CWD]") - .cow_replace(node.as_ref(), "[NODE]") - .cow_replace("pnpm.native.exe", "pnpm.native") - .cow_replace("pnpm.cmd", "pnpm") - .cow_replace('\\', "/") - .cow_replace(&platform_arch, "[platform] [arch]") - .into_owned() - } - - #[test] - #[expect( - clippy::disallowed_macros, - reason = "insta::assert_snapshot! expands to std format! for its failure message" - )] - fn snapshot_pnpm_js_layout() { - insta::assert_snapshot!(render_lifecycle_stamp( - PackageManagerType::Pnpm, - "11.20.0", - &["pnpm.cjs"] - )); - } - - #[test] - #[expect( - clippy::disallowed_macros, - reason = "insta::assert_snapshot! expands to std format! for its failure message" - )] - fn snapshot_pnpm_native_layout() { - insta::assert_snapshot!(render_lifecycle_stamp( - PackageManagerType::Pnpm, - "12.0.0", - &[native_pnpm_bin_name()] - )); - } - - #[test] - #[expect( - clippy::disallowed_macros, - reason = "insta::assert_snapshot! expands to std format! for its failure message" - )] - fn snapshot_pnpm_js_entry_preferred_over_native() { - insta::assert_snapshot!(render_lifecycle_stamp( - PackageManagerType::Pnpm, - "12.0.0", - &["pnpm.cjs", native_pnpm_bin_name()] - )); - } - - #[test] - #[expect( - clippy::disallowed_macros, - reason = "insta::assert_snapshot! expands to std format! for its failure message" - )] - fn snapshot_pnpm_shim_fallback() { - insta::assert_snapshot!(render_lifecycle_stamp(PackageManagerType::Pnpm, "11.20.0", &[])); - } - - #[test] - #[expect( - clippy::disallowed_macros, - reason = "insta::assert_snapshot! expands to std format! for its failure message" - )] - fn snapshot_npm_js_layout() { - insta::assert_snapshot!(render_lifecycle_stamp( - PackageManagerType::Npm, - "10.9.8", - &["npm-cli.js"] - )); - } - - #[test] - #[expect( - clippy::disallowed_macros, - reason = "insta::assert_snapshot! expands to std format! for its failure message" - )] - fn snapshot_yarn_js_layout() { - insta::assert_snapshot!(render_lifecycle_stamp( - PackageManagerType::Yarn, - "1.22.22", - &["yarn.js"] - )); - } - - #[test] - #[expect( - clippy::disallowed_macros, - reason = "insta::assert_snapshot! expands to std format! for its failure message" - )] - fn snapshot_bun_stamps_no_lifecycle_vars() { - insta::assert_snapshot!(render_lifecycle_stamp(PackageManagerType::Bun, "1.3.0", &[])); - } } diff --git a/crates/vp_pm_cli/src/snapshots/vp_pm_cli__lifecycle_env__tests__snapshot_bun_stamps_no_lifecycle_vars.snap b/crates/vp_pm_cli/src/snapshots/vp_pm_cli__lifecycle_env__tests__snapshot_bun_stamps_no_lifecycle_vars.snap deleted file mode 100644 index 7d0c03a5a2..0000000000 --- a/crates/vp_pm_cli/src/snapshots/vp_pm_cli__lifecycle_env__tests__snapshot_bun_stamps_no_lifecycle_vars.snap +++ /dev/null @@ -1,5 +0,0 @@ ---- -source: crates/vp_pm_cli/src/lifecycle_env.rs -expression: "render_lifecycle_stamp(PackageManagerType::Bun, \"1.3.0\", &[])" ---- -bun 1.3.0 diff --git a/crates/vp_pm_cli/src/snapshots/vp_pm_cli__lifecycle_env__tests__snapshot_npm_js_layout.snap b/crates/vp_pm_cli/src/snapshots/vp_pm_cli__lifecycle_env__tests__snapshot_npm_js_layout.snap deleted file mode 100644 index bb5d8f2636..0000000000 --- a/crates/vp_pm_cli/src/snapshots/vp_pm_cli__lifecycle_env__tests__snapshot_npm_js_layout.snap +++ /dev/null @@ -1,10 +0,0 @@ ---- -source: crates/vp_pm_cli/src/lifecycle_env.rs -expression: "render_lifecycle_stamp(PackageManagerType::Npm, \"10.9.8\", &[\"npm-cli.js\"])" ---- -npm 10.9.8 -npm_execpath=[INSTALL_DIR]/bin/npm-cli.js -npm_config_user_agent=npm/10.9.8 node/v22.23.1 [platform] [arch] workspaces/false -INIT_CWD=[INIT_CWD] -npm_node_execpath=[NODE] -NODE=[NODE] diff --git a/crates/vp_pm_cli/src/snapshots/vp_pm_cli__lifecycle_env__tests__snapshot_pnpm_js_entry_preferred_over_native.snap b/crates/vp_pm_cli/src/snapshots/vp_pm_cli__lifecycle_env__tests__snapshot_pnpm_js_entry_preferred_over_native.snap deleted file mode 100644 index a4e26b8a86..0000000000 --- a/crates/vp_pm_cli/src/snapshots/vp_pm_cli__lifecycle_env__tests__snapshot_pnpm_js_entry_preferred_over_native.snap +++ /dev/null @@ -1,10 +0,0 @@ ---- -source: crates/vp_pm_cli/src/lifecycle_env.rs -expression: "render_lifecycle_stamp(PackageManagerType::Pnpm, \"12.0.0\",\n&[\"pnpm.cjs\", native_pnpm_bin_name()])" ---- -pnpm 12.0.0 -npm_execpath=[INSTALL_DIR]/bin/pnpm.cjs -npm_config_user_agent=pnpm/12.0.0 npm/? node/v22.23.1 [platform] [arch] -INIT_CWD=[INIT_CWD] -npm_node_execpath=[NODE] -NODE=[NODE] diff --git a/crates/vp_pm_cli/src/snapshots/vp_pm_cli__lifecycle_env__tests__snapshot_pnpm_js_layout.snap b/crates/vp_pm_cli/src/snapshots/vp_pm_cli__lifecycle_env__tests__snapshot_pnpm_js_layout.snap deleted file mode 100644 index e713a4651b..0000000000 --- a/crates/vp_pm_cli/src/snapshots/vp_pm_cli__lifecycle_env__tests__snapshot_pnpm_js_layout.snap +++ /dev/null @@ -1,10 +0,0 @@ ---- -source: crates/vp_pm_cli/src/lifecycle_env.rs -expression: "render_lifecycle_stamp(PackageManagerType::Pnpm, \"11.20.0\", &[\"pnpm.cjs\"])" ---- -pnpm 11.20.0 -npm_execpath=[INSTALL_DIR]/bin/pnpm.cjs -npm_config_user_agent=pnpm/11.20.0 npm/? node/v22.23.1 [platform] [arch] -INIT_CWD=[INIT_CWD] -npm_node_execpath=[NODE] -NODE=[NODE] diff --git a/crates/vp_pm_cli/src/snapshots/vp_pm_cli__lifecycle_env__tests__snapshot_pnpm_native_layout.snap b/crates/vp_pm_cli/src/snapshots/vp_pm_cli__lifecycle_env__tests__snapshot_pnpm_native_layout.snap deleted file mode 100644 index 920cc26691..0000000000 --- a/crates/vp_pm_cli/src/snapshots/vp_pm_cli__lifecycle_env__tests__snapshot_pnpm_native_layout.snap +++ /dev/null @@ -1,10 +0,0 @@ ---- -source: crates/vp_pm_cli/src/lifecycle_env.rs -expression: "render_lifecycle_stamp(PackageManagerType::Pnpm, \"12.0.0\",\n&[native_pnpm_bin_name()])" ---- -pnpm 12.0.0 -npm_execpath=[INSTALL_DIR]/bin/pnpm.native -npm_config_user_agent=pnpm/12.0.0 npm/? node/v22.23.1 [platform] [arch] -INIT_CWD=[INIT_CWD] -npm_node_execpath=[NODE] -NODE=[NODE] diff --git a/crates/vp_pm_cli/src/snapshots/vp_pm_cli__lifecycle_env__tests__snapshot_pnpm_shim_fallback.snap b/crates/vp_pm_cli/src/snapshots/vp_pm_cli__lifecycle_env__tests__snapshot_pnpm_shim_fallback.snap deleted file mode 100644 index 1c80975fa4..0000000000 --- a/crates/vp_pm_cli/src/snapshots/vp_pm_cli__lifecycle_env__tests__snapshot_pnpm_shim_fallback.snap +++ /dev/null @@ -1,10 +0,0 @@ ---- -source: crates/vp_pm_cli/src/lifecycle_env.rs -expression: "render_lifecycle_stamp(PackageManagerType::Pnpm, \"11.20.0\", &[])" ---- -pnpm 11.20.0 -npm_execpath=[INSTALL_DIR]/bin/pnpm -npm_config_user_agent=pnpm/11.20.0 npm/? node/v22.23.1 [platform] [arch] -INIT_CWD=[INIT_CWD] -npm_node_execpath=[NODE] -NODE=[NODE] diff --git a/crates/vp_pm_cli/src/snapshots/vp_pm_cli__lifecycle_env__tests__snapshot_yarn_js_layout.snap b/crates/vp_pm_cli/src/snapshots/vp_pm_cli__lifecycle_env__tests__snapshot_yarn_js_layout.snap deleted file mode 100644 index b5069bd132..0000000000 --- a/crates/vp_pm_cli/src/snapshots/vp_pm_cli__lifecycle_env__tests__snapshot_yarn_js_layout.snap +++ /dev/null @@ -1,10 +0,0 @@ ---- -source: crates/vp_pm_cli/src/lifecycle_env.rs -expression: "render_lifecycle_stamp(PackageManagerType::Yarn, \"1.22.22\", &[\"yarn.js\"])" ---- -yarn 1.22.22 -npm_execpath=[INSTALL_DIR]/bin/yarn.js -npm_config_user_agent=yarn/1.22.22 npm/? node/v22.23.1 [platform] [arch] -INIT_CWD=[INIT_CWD] -npm_node_execpath=[NODE] -NODE=[NODE]