From 0b3ad59855a4051153382e035d9586cf41a76a49 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EC=A2=85=EA=B2=BD?= Date: Wed, 27 May 2026 14:49:49 +0900 Subject: [PATCH 1/6] fix(cli): expose configured package manager to direct commands Prepend the managed package manager bin from an explicit packageManager field to direct built-in command environments so tools spawned by vp test can find the configured package manager without relying on the user's shell PATH. Keep the behavior limited to explicit packageManager metadata and avoid lockfile inference or package.json mutation. --- packages/cli/binding/src/cli/mod.rs | 124 +++++++++++++++++++++++++++- 1 file changed, 123 insertions(+), 1 deletion(-) diff --git a/packages/cli/binding/src/cli/mod.rs b/packages/cli/binding/src/cli/mod.rs index 872e0802e3..c21316a2f2 100644 --- a/packages/cli/binding/src/cli/mod.rs +++ b/packages/cli/binding/src/cli/mod.rs @@ -60,6 +60,7 @@ async fn execute_direct_subcommand( .map(|(k, v)| (Arc::from(k.as_os_str()), Arc::from(v.as_os_str()))) .collect(), ); + let envs = envs_with_explicit_package_manager_path(cwd, envs).await?; let status = match subcommand { SynthesizableSubcommand::Check { @@ -113,6 +114,57 @@ async fn execute_direct_subcommand( Ok(status) } +fn is_path_env_key(key: &OsStr) -> bool { + if cfg!(windows) { key.eq_ignore_ascii_case("PATH") } else { key == "PATH" } +} + +fn prepend_to_env_path( + envs: &Arc, Arc>>, + bin_prefix: &AbsolutePath, +) -> Result, Arc>>, Error> { + let path_key = envs + .keys() + .find(|key| is_path_env_key(key.as_ref())) + .cloned() + .unwrap_or_else(|| Arc::from(OsStr::new("PATH"))); + let current_path = + envs.get(&path_key).map_or_else(Default::default, |path| path.to_os_string()); + let paths = env::split_paths(¤t_path).collect::>(); + + if paths.first().is_some_and(|path| path == bin_prefix.as_path()) { + return Ok(Arc::clone(envs)); + } + + let new_path = env::join_paths( + std::iter::once(bin_prefix.as_path().to_path_buf()).chain(paths.into_iter()), + ) + .map_err(|error| Error::Anyhow(anyhow::Error::new(error)))?; + + let mut envs = FxHashMap::clone(envs); + envs.insert(path_key, Arc::from(new_path.as_os_str())); + Ok(Arc::new(envs)) +} + +async fn envs_with_explicit_package_manager_path( + cwd: &AbsolutePath, + envs: Arc, Arc>>, +) -> Result, Arc>>, Error> { + let Some(resolution) = + vite_install::package_manager::resolve_package_manager_from_package_json(cwd)? + else { + return Ok(envs); + }; + + let (install_dir, _, _) = vite_install::download_package_manager( + resolution.package_manager_type, + &resolution.version, + resolution.hash.as_deref(), + ) + .await?; + + prepend_to_env_path(&envs, &install_dir.join("bin")) +} + /// Execute a vite-task command (run, cache) through Session. async fn execute_vite_task_command( command: vite_task::Command, @@ -224,10 +276,80 @@ async fn execute_pm_command( #[cfg(test)] mod tests { - use std::path::PathBuf; + use std::{ + ffi::OsStr, + fs, + path::PathBuf, + sync::Arc, + time::{SystemTime, UNIX_EPOCH}, + }; + use rustc_hash::FxHashMap; + use vite_path::AbsolutePathBuf; use vite_task::config::UserRunConfig; + use super::{envs_with_explicit_package_manager_path, prepend_to_env_path}; + + fn envs_with_path(path: &std::ffi::OsStr) -> Arc, Arc>> { + Arc::new(FxHashMap::from_iter([(Arc::from(OsStr::new("PATH")), Arc::from(path))])) + } + + #[test] + fn prepends_package_manager_bin_to_env_path() { + let cwd = std::env::current_dir().expect("current_dir should exist"); + let old_bin = cwd.join("old-bin"); + let pm_bin = AbsolutePathBuf::new(cwd.join("pm-bin")).expect("pm bin should be absolute"); + let original_path = std::env::join_paths([old_bin.as_path()]).expect("valid PATH"); + let envs = envs_with_path(original_path.as_os_str()); + + let updated = prepend_to_env_path(&envs, &pm_bin).expect("PATH should update"); + let path_value = updated.get(OsStr::new("PATH")).expect("PATH should exist"); + let paths = std::env::split_paths(path_value).collect::>(); + + assert_eq!(paths.first().map(std::path::PathBuf::as_path), Some(pm_bin.as_path())); + assert_eq!(paths.get(1).map(std::path::PathBuf::as_path), Some(old_bin.as_path())); + } + + #[test] + fn does_not_duplicate_package_manager_bin_when_already_first() { + let cwd = std::env::current_dir().expect("current_dir should exist"); + let pm_bin = AbsolutePathBuf::new(cwd.join("pm-bin")).expect("pm bin should be absolute"); + let original_path = std::env::join_paths([pm_bin.as_path()]).expect("valid PATH"); + let envs = envs_with_path(original_path.as_os_str()); + + let updated = prepend_to_env_path(&envs, &pm_bin).expect("PATH should update"); + let path_value = updated.get(OsStr::new("PATH")).expect("PATH should exist"); + let paths = std::env::split_paths(path_value).collect::>(); + + assert_eq!(paths, vec![pm_bin.as_path().to_path_buf()]); + } + + #[tokio::test] + async fn ignores_lockfile_without_explicit_package_manager() { + let suffix = + SystemTime::now().duration_since(UNIX_EPOCH).expect("time should be valid").as_nanos(); + let temp_dir = std::env::temp_dir().join(format!("vite-plus-no-pm-{suffix}")); + fs::create_dir_all(&temp_dir).expect("temp dir should be created"); + fs::write(temp_dir.join("package.json"), r#"{"name":"fixture"}"#) + .expect("package.json should be written"); + fs::write(temp_dir.join("pnpm-lock.yaml"), "lockfileVersion: '9.0'\n") + .expect("lockfile should be written"); + let cwd = AbsolutePathBuf::new(temp_dir.clone()).expect("temp dir should be absolute"); + let original_path = std::env::join_paths([temp_dir.join("old-bin")]).expect("valid PATH"); + let envs = envs_with_path(original_path.as_os_str()); + + let updated = envs_with_explicit_package_manager_path(&cwd, Arc::clone(&envs)) + .await + .expect("missing packageManager should not error"); + + assert_eq!(updated.get(OsStr::new("PATH")), envs.get(OsStr::new("PATH"))); + assert_eq!( + fs::read_to_string(temp_dir.join("package.json")).expect("package.json should exist"), + r#"{"name":"fixture"}"# + ); + fs::remove_dir_all(temp_dir).expect("temp dir should be removed"); + } + #[test] fn run_config_types_in_sync() { // Remove \r for cross-platform consistency From efad700e0addac8e8fcf842027c46a04963b1131 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EC=A2=85=EA=B2=BD?= Date: Wed, 27 May 2026 14:56:59 +0900 Subject: [PATCH 2/6] test(cli): cover direct package manager PATH --- .../direct-test-path-env-include-bun/package.json | 6 ++++++ .../direct-test-path-env-include-bun/snap.txt | 10 ++++++++++ .../src/package-manager.test.ts | 8 ++++++++ .../direct-test-path-env-include-bun/steps.json | 6 ++++++ .../direct-test-path-env-include-bun/vite.config.ts | 3 +++ .../direct-test-path-env-include-npm/package.json | 6 ++++++ .../direct-test-path-env-include-npm/snap.txt | 10 ++++++++++ .../src/package-manager.test.ts | 8 ++++++++ .../direct-test-path-env-include-npm/steps.json | 6 ++++++ .../direct-test-path-env-include-npm/vite.config.ts | 3 +++ .../direct-test-path-env-include-pnpm/package.json | 6 ++++++ .../direct-test-path-env-include-pnpm/snap.txt | 11 +++++++++++ .../src/package-manager.test.ts | 8 ++++++++ .../direct-test-path-env-include-pnpm/steps.json | 6 ++++++ .../direct-test-path-env-include-pnpm/vite.config.ts | 3 +++ .../direct-test-path-env-include-yarn/package.json | 6 ++++++ .../direct-test-path-env-include-yarn/snap.txt | 11 +++++++++++ .../src/package-manager.test.ts | 8 ++++++++ .../direct-test-path-env-include-yarn/steps.json | 6 ++++++ .../direct-test-path-env-include-yarn/vite.config.ts | 3 +++ 20 files changed, 134 insertions(+) create mode 100644 packages/cli/snap-tests/direct-test-path-env-include-bun/package.json create mode 100644 packages/cli/snap-tests/direct-test-path-env-include-bun/snap.txt create mode 100644 packages/cli/snap-tests/direct-test-path-env-include-bun/src/package-manager.test.ts create mode 100644 packages/cli/snap-tests/direct-test-path-env-include-bun/steps.json create mode 100644 packages/cli/snap-tests/direct-test-path-env-include-bun/vite.config.ts create mode 100644 packages/cli/snap-tests/direct-test-path-env-include-npm/package.json create mode 100644 packages/cli/snap-tests/direct-test-path-env-include-npm/snap.txt create mode 100644 packages/cli/snap-tests/direct-test-path-env-include-npm/src/package-manager.test.ts create mode 100644 packages/cli/snap-tests/direct-test-path-env-include-npm/steps.json create mode 100644 packages/cli/snap-tests/direct-test-path-env-include-npm/vite.config.ts create mode 100644 packages/cli/snap-tests/direct-test-path-env-include-pnpm/package.json create mode 100644 packages/cli/snap-tests/direct-test-path-env-include-pnpm/snap.txt create mode 100644 packages/cli/snap-tests/direct-test-path-env-include-pnpm/src/package-manager.test.ts create mode 100644 packages/cli/snap-tests/direct-test-path-env-include-pnpm/steps.json create mode 100644 packages/cli/snap-tests/direct-test-path-env-include-pnpm/vite.config.ts create mode 100644 packages/cli/snap-tests/direct-test-path-env-include-yarn/package.json create mode 100644 packages/cli/snap-tests/direct-test-path-env-include-yarn/snap.txt create mode 100644 packages/cli/snap-tests/direct-test-path-env-include-yarn/src/package-manager.test.ts create mode 100644 packages/cli/snap-tests/direct-test-path-env-include-yarn/steps.json create mode 100644 packages/cli/snap-tests/direct-test-path-env-include-yarn/vite.config.ts diff --git a/packages/cli/snap-tests/direct-test-path-env-include-bun/package.json b/packages/cli/snap-tests/direct-test-path-env-include-bun/package.json new file mode 100644 index 0000000000..8522b7d9b7 --- /dev/null +++ b/packages/cli/snap-tests/direct-test-path-env-include-bun/package.json @@ -0,0 +1,6 @@ +{ + "name": "direct-test-path-env-include-bun", + "private": true, + "type": "module", + "packageManager": "bun@1.3.11" +} diff --git a/packages/cli/snap-tests/direct-test-path-env-include-bun/snap.txt b/packages/cli/snap-tests/direct-test-path-env-include-bun/snap.txt new file mode 100644 index 0000000000..4081ffbb32 --- /dev/null +++ b/packages/cli/snap-tests/direct-test-path-env-include-bun/snap.txt @@ -0,0 +1,10 @@ +> fake_bin=$(mktemp -d) && printf '#!/bin/sh\necho 0.0.0\n' > "$fake_bin/bun" && chmod +x "$fake_bin/bun" && PATH="$fake_bin:$PATH" vp test + RUN + + ✓ src/package-manager.test.ts (1 test) ms + + Test Files 1 passed (1) + Tests 1 passed (1) + Start at + Duration ms (transform ms, setup ms, import ms, tests ms, environment ms) + diff --git a/packages/cli/snap-tests/direct-test-path-env-include-bun/src/package-manager.test.ts b/packages/cli/snap-tests/direct-test-path-env-include-bun/src/package-manager.test.ts new file mode 100644 index 0000000000..4a977f439e --- /dev/null +++ b/packages/cli/snap-tests/direct-test-path-env-include-bun/src/package-manager.test.ts @@ -0,0 +1,8 @@ +import { execFileSync } from 'node:child_process'; + +import { expect, test } from '@voidzero-dev/vite-plus-test'; + +test('direct test command exposes the configured package manager on PATH', () => { + const version = execFileSync('bun', ['--version'], { encoding: 'utf8' }).trim(); + expect(version).toBe('1.3.11'); +}); diff --git a/packages/cli/snap-tests/direct-test-path-env-include-bun/steps.json b/packages/cli/snap-tests/direct-test-path-env-include-bun/steps.json new file mode 100644 index 0000000000..d4f4364965 --- /dev/null +++ b/packages/cli/snap-tests/direct-test-path-env-include-bun/steps.json @@ -0,0 +1,6 @@ +{ + "ignoredPlatforms": ["win32"], + "commands": [ + "fake_bin=$(mktemp -d) && printf '#!/bin/sh\\necho 0.0.0\\n' > \"$fake_bin/bun\" && chmod +x \"$fake_bin/bun\" && PATH=\"$fake_bin:$PATH\" vp test" + ] +} diff --git a/packages/cli/snap-tests/direct-test-path-env-include-bun/vite.config.ts b/packages/cli/snap-tests/direct-test-path-env-include-bun/vite.config.ts new file mode 100644 index 0000000000..3210accbf9 --- /dev/null +++ b/packages/cli/snap-tests/direct-test-path-env-include-bun/vite.config.ts @@ -0,0 +1,3 @@ +import { defineConfig } from 'vite-plus'; + +export default defineConfig({}); diff --git a/packages/cli/snap-tests/direct-test-path-env-include-npm/package.json b/packages/cli/snap-tests/direct-test-path-env-include-npm/package.json new file mode 100644 index 0000000000..7c61580e9b --- /dev/null +++ b/packages/cli/snap-tests/direct-test-path-env-include-npm/package.json @@ -0,0 +1,6 @@ +{ + "name": "direct-test-path-env-include-npm", + "private": true, + "type": "module", + "packageManager": "npm@10.9.4" +} diff --git a/packages/cli/snap-tests/direct-test-path-env-include-npm/snap.txt b/packages/cli/snap-tests/direct-test-path-env-include-npm/snap.txt new file mode 100644 index 0000000000..7c84317c85 --- /dev/null +++ b/packages/cli/snap-tests/direct-test-path-env-include-npm/snap.txt @@ -0,0 +1,10 @@ +> fake_bin=$(mktemp -d) && printf '#!/bin/sh\necho 0.0.0\n' > "$fake_bin/npm" && chmod +x "$fake_bin/npm" && PATH="$fake_bin:$PATH" vp test + RUN + + ✓ src/package-manager.test.ts (1 test) ms + + Test Files 1 passed (1) + Tests 1 passed (1) + Start at + Duration ms (transform ms, setup ms, import ms, tests ms, environment ms) + diff --git a/packages/cli/snap-tests/direct-test-path-env-include-npm/src/package-manager.test.ts b/packages/cli/snap-tests/direct-test-path-env-include-npm/src/package-manager.test.ts new file mode 100644 index 0000000000..0649bc6bc0 --- /dev/null +++ b/packages/cli/snap-tests/direct-test-path-env-include-npm/src/package-manager.test.ts @@ -0,0 +1,8 @@ +import { execFileSync } from 'node:child_process'; + +import { expect, test } from '@voidzero-dev/vite-plus-test'; + +test('direct test command exposes the configured package manager on PATH', () => { + const version = execFileSync('npm', ['--version'], { encoding: 'utf8' }).trim(); + expect(version).toBe('10.9.4'); +}); diff --git a/packages/cli/snap-tests/direct-test-path-env-include-npm/steps.json b/packages/cli/snap-tests/direct-test-path-env-include-npm/steps.json new file mode 100644 index 0000000000..9eedc0090a --- /dev/null +++ b/packages/cli/snap-tests/direct-test-path-env-include-npm/steps.json @@ -0,0 +1,6 @@ +{ + "ignoredPlatforms": ["win32"], + "commands": [ + "fake_bin=$(mktemp -d) && printf '#!/bin/sh\\necho 0.0.0\\n' > \"$fake_bin/npm\" && chmod +x \"$fake_bin/npm\" && PATH=\"$fake_bin:$PATH\" vp test" + ] +} diff --git a/packages/cli/snap-tests/direct-test-path-env-include-npm/vite.config.ts b/packages/cli/snap-tests/direct-test-path-env-include-npm/vite.config.ts new file mode 100644 index 0000000000..3210accbf9 --- /dev/null +++ b/packages/cli/snap-tests/direct-test-path-env-include-npm/vite.config.ts @@ -0,0 +1,3 @@ +import { defineConfig } from 'vite-plus'; + +export default defineConfig({}); diff --git a/packages/cli/snap-tests/direct-test-path-env-include-pnpm/package.json b/packages/cli/snap-tests/direct-test-path-env-include-pnpm/package.json new file mode 100644 index 0000000000..53e470ac1a --- /dev/null +++ b/packages/cli/snap-tests/direct-test-path-env-include-pnpm/package.json @@ -0,0 +1,6 @@ +{ + "name": "direct-test-path-env-include-pnpm", + "private": true, + "type": "module", + "packageManager": "pnpm@11.2.2" +} diff --git a/packages/cli/snap-tests/direct-test-path-env-include-pnpm/snap.txt b/packages/cli/snap-tests/direct-test-path-env-include-pnpm/snap.txt new file mode 100644 index 0000000000..1f135d5f4b --- /dev/null +++ b/packages/cli/snap-tests/direct-test-path-env-include-pnpm/snap.txt @@ -0,0 +1,11 @@ +> fake_bin=$(mktemp -d) && printf '#!/bin/sh\necho 0.0.0\n' > "$fake_bin/pnpm" && chmod +x "$fake_bin/pnpm" && PATH="$fake_bin:$PATH" vp test + RUN + + ✓ src/package-manager.test.ts (1 test) ms + ✓ direct test command exposes the configured package manager on PATH ms + + Test Files 1 passed (1) + Tests 1 passed (1) + Start at + Duration ms (transform ms, setup ms, import ms, tests ms, environment ms) + diff --git a/packages/cli/snap-tests/direct-test-path-env-include-pnpm/src/package-manager.test.ts b/packages/cli/snap-tests/direct-test-path-env-include-pnpm/src/package-manager.test.ts new file mode 100644 index 0000000000..1e9c1ffb06 --- /dev/null +++ b/packages/cli/snap-tests/direct-test-path-env-include-pnpm/src/package-manager.test.ts @@ -0,0 +1,8 @@ +import { execFileSync } from 'node:child_process'; + +import { expect, test } from '@voidzero-dev/vite-plus-test'; + +test('direct test command exposes the configured package manager on PATH', () => { + const version = execFileSync('pnpm', ['--version'], { encoding: 'utf8' }).trim(); + expect(version).toBe('11.2.2'); +}); diff --git a/packages/cli/snap-tests/direct-test-path-env-include-pnpm/steps.json b/packages/cli/snap-tests/direct-test-path-env-include-pnpm/steps.json new file mode 100644 index 0000000000..24fd9c0058 --- /dev/null +++ b/packages/cli/snap-tests/direct-test-path-env-include-pnpm/steps.json @@ -0,0 +1,6 @@ +{ + "ignoredPlatforms": ["win32"], + "commands": [ + "fake_bin=$(mktemp -d) && printf '#!/bin/sh\\necho 0.0.0\\n' > \"$fake_bin/pnpm\" && chmod +x \"$fake_bin/pnpm\" && PATH=\"$fake_bin:$PATH\" vp test" + ] +} diff --git a/packages/cli/snap-tests/direct-test-path-env-include-pnpm/vite.config.ts b/packages/cli/snap-tests/direct-test-path-env-include-pnpm/vite.config.ts new file mode 100644 index 0000000000..3210accbf9 --- /dev/null +++ b/packages/cli/snap-tests/direct-test-path-env-include-pnpm/vite.config.ts @@ -0,0 +1,3 @@ +import { defineConfig } from 'vite-plus'; + +export default defineConfig({}); diff --git a/packages/cli/snap-tests/direct-test-path-env-include-yarn/package.json b/packages/cli/snap-tests/direct-test-path-env-include-yarn/package.json new file mode 100644 index 0000000000..ea727e5217 --- /dev/null +++ b/packages/cli/snap-tests/direct-test-path-env-include-yarn/package.json @@ -0,0 +1,6 @@ +{ + "name": "direct-test-path-env-include-yarn", + "private": true, + "type": "module", + "packageManager": "yarn@4.10.3" +} diff --git a/packages/cli/snap-tests/direct-test-path-env-include-yarn/snap.txt b/packages/cli/snap-tests/direct-test-path-env-include-yarn/snap.txt new file mode 100644 index 0000000000..e313b8f7b9 --- /dev/null +++ b/packages/cli/snap-tests/direct-test-path-env-include-yarn/snap.txt @@ -0,0 +1,11 @@ +> fake_bin=$(mktemp -d) && printf '#!/bin/sh\necho 0.0.0\n' > "$fake_bin/yarn" && chmod +x "$fake_bin/yarn" && PATH="$fake_bin:$PATH" vp test + RUN + + ✓ src/package-manager.test.ts (1 test) ms + ✓ direct test command exposes the configured package manager on PATH ms + + Test Files 1 passed (1) + Tests 1 passed (1) + Start at + Duration ms (transform ms, setup ms, import ms, tests ms, environment ms) + diff --git a/packages/cli/snap-tests/direct-test-path-env-include-yarn/src/package-manager.test.ts b/packages/cli/snap-tests/direct-test-path-env-include-yarn/src/package-manager.test.ts new file mode 100644 index 0000000000..c3519a6b81 --- /dev/null +++ b/packages/cli/snap-tests/direct-test-path-env-include-yarn/src/package-manager.test.ts @@ -0,0 +1,8 @@ +import { execFileSync } from 'node:child_process'; + +import { expect, test } from '@voidzero-dev/vite-plus-test'; + +test('direct test command exposes the configured package manager on PATH', () => { + const version = execFileSync('yarn', ['--version'], { encoding: 'utf8' }).trim(); + expect(version).toBe('4.10.3'); +}); diff --git a/packages/cli/snap-tests/direct-test-path-env-include-yarn/steps.json b/packages/cli/snap-tests/direct-test-path-env-include-yarn/steps.json new file mode 100644 index 0000000000..79fb7d6b5f --- /dev/null +++ b/packages/cli/snap-tests/direct-test-path-env-include-yarn/steps.json @@ -0,0 +1,6 @@ +{ + "ignoredPlatforms": ["win32"], + "commands": [ + "fake_bin=$(mktemp -d) && printf '#!/bin/sh\\necho 0.0.0\\n' > \"$fake_bin/yarn\" && chmod +x \"$fake_bin/yarn\" && PATH=\"$fake_bin:$PATH\" vp test" + ] +} diff --git a/packages/cli/snap-tests/direct-test-path-env-include-yarn/vite.config.ts b/packages/cli/snap-tests/direct-test-path-env-include-yarn/vite.config.ts new file mode 100644 index 0000000000..3210accbf9 --- /dev/null +++ b/packages/cli/snap-tests/direct-test-path-env-include-yarn/vite.config.ts @@ -0,0 +1,3 @@ +import { defineConfig } from 'vite-plus'; + +export default defineConfig({}); From d05654897e020447a6f1931c8576f8677a1a587d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EC=A2=85=EA=B2=BD?= Date: Wed, 27 May 2026 17:38:42 +0900 Subject: [PATCH 3/6] test(cli): stabilize direct package manager PATH snaps --- .../direct-test-path-env-include-bun/snap.txt | 11 +---------- .../direct-test-path-env-include-bun/steps.json | 5 ++++- .../direct-test-path-env-include-npm/snap.txt | 11 +---------- .../direct-test-path-env-include-npm/steps.json | 5 ++++- .../direct-test-path-env-include-pnpm/snap.txt | 12 +----------- .../direct-test-path-env-include-pnpm/steps.json | 5 ++++- .../direct-test-path-env-include-yarn/snap.txt | 12 +----------- .../direct-test-path-env-include-yarn/steps.json | 5 ++++- 8 files changed, 20 insertions(+), 46 deletions(-) diff --git a/packages/cli/snap-tests/direct-test-path-env-include-bun/snap.txt b/packages/cli/snap-tests/direct-test-path-env-include-bun/snap.txt index 4081ffbb32..ae59c5ef34 100644 --- a/packages/cli/snap-tests/direct-test-path-env-include-bun/snap.txt +++ b/packages/cli/snap-tests/direct-test-path-env-include-bun/snap.txt @@ -1,10 +1 @@ -> fake_bin=$(mktemp -d) && printf '#!/bin/sh\necho 0.0.0\n' > "$fake_bin/bun" && chmod +x "$fake_bin/bun" && PATH="$fake_bin:$PATH" vp test - RUN - - ✓ src/package-manager.test.ts (1 test) ms - - Test Files 1 passed (1) - Tests 1 passed (1) - Start at - Duration ms (transform ms, setup ms, import ms, tests ms, environment ms) - +> fake_bin=$(mktemp -d) && printf '#!/bin/sh\necho 0.0.0\n' > "$fake_bin/bun" && chmod +x "$fake_bin/bun" && PATH="$fake_bin:$PATH" vp test \ No newline at end of file diff --git a/packages/cli/snap-tests/direct-test-path-env-include-bun/steps.json b/packages/cli/snap-tests/direct-test-path-env-include-bun/steps.json index d4f4364965..dc311bd972 100644 --- a/packages/cli/snap-tests/direct-test-path-env-include-bun/steps.json +++ b/packages/cli/snap-tests/direct-test-path-env-include-bun/steps.json @@ -1,6 +1,9 @@ { "ignoredPlatforms": ["win32"], "commands": [ - "fake_bin=$(mktemp -d) && printf '#!/bin/sh\\necho 0.0.0\\n' > \"$fake_bin/bun\" && chmod +x \"$fake_bin/bun\" && PATH=\"$fake_bin:$PATH\" vp test" + { + "command": "fake_bin=$(mktemp -d) && printf '#!/bin/sh\\necho 0.0.0\\n' > \"$fake_bin/bun\" && chmod +x \"$fake_bin/bun\" && PATH=\"$fake_bin:$PATH\" vp test", + "ignoreOutput": true + } ] } diff --git a/packages/cli/snap-tests/direct-test-path-env-include-npm/snap.txt b/packages/cli/snap-tests/direct-test-path-env-include-npm/snap.txt index 7c84317c85..b2978f607a 100644 --- a/packages/cli/snap-tests/direct-test-path-env-include-npm/snap.txt +++ b/packages/cli/snap-tests/direct-test-path-env-include-npm/snap.txt @@ -1,10 +1 @@ -> fake_bin=$(mktemp -d) && printf '#!/bin/sh\necho 0.0.0\n' > "$fake_bin/npm" && chmod +x "$fake_bin/npm" && PATH="$fake_bin:$PATH" vp test - RUN - - ✓ src/package-manager.test.ts (1 test) ms - - Test Files 1 passed (1) - Tests 1 passed (1) - Start at - Duration ms (transform ms, setup ms, import ms, tests ms, environment ms) - +> fake_bin=$(mktemp -d) && printf '#!/bin/sh\necho 0.0.0\n' > "$fake_bin/npm" && chmod +x "$fake_bin/npm" && PATH="$fake_bin:$PATH" vp test \ No newline at end of file diff --git a/packages/cli/snap-tests/direct-test-path-env-include-npm/steps.json b/packages/cli/snap-tests/direct-test-path-env-include-npm/steps.json index 9eedc0090a..edeb78e97d 100644 --- a/packages/cli/snap-tests/direct-test-path-env-include-npm/steps.json +++ b/packages/cli/snap-tests/direct-test-path-env-include-npm/steps.json @@ -1,6 +1,9 @@ { "ignoredPlatforms": ["win32"], "commands": [ - "fake_bin=$(mktemp -d) && printf '#!/bin/sh\\necho 0.0.0\\n' > \"$fake_bin/npm\" && chmod +x \"$fake_bin/npm\" && PATH=\"$fake_bin:$PATH\" vp test" + { + "command": "fake_bin=$(mktemp -d) && printf '#!/bin/sh\\necho 0.0.0\\n' > \"$fake_bin/npm\" && chmod +x \"$fake_bin/npm\" && PATH=\"$fake_bin:$PATH\" vp test", + "ignoreOutput": true + } ] } diff --git a/packages/cli/snap-tests/direct-test-path-env-include-pnpm/snap.txt b/packages/cli/snap-tests/direct-test-path-env-include-pnpm/snap.txt index 1f135d5f4b..162b4142c8 100644 --- a/packages/cli/snap-tests/direct-test-path-env-include-pnpm/snap.txt +++ b/packages/cli/snap-tests/direct-test-path-env-include-pnpm/snap.txt @@ -1,11 +1 @@ -> fake_bin=$(mktemp -d) && printf '#!/bin/sh\necho 0.0.0\n' > "$fake_bin/pnpm" && chmod +x "$fake_bin/pnpm" && PATH="$fake_bin:$PATH" vp test - RUN - - ✓ src/package-manager.test.ts (1 test) ms - ✓ direct test command exposes the configured package manager on PATH ms - - Test Files 1 passed (1) - Tests 1 passed (1) - Start at - Duration ms (transform ms, setup ms, import ms, tests ms, environment ms) - +> fake_bin=$(mktemp -d) && printf '#!/bin/sh\necho 0.0.0\n' > "$fake_bin/pnpm" && chmod +x "$fake_bin/pnpm" && PATH="$fake_bin:$PATH" vp test \ No newline at end of file diff --git a/packages/cli/snap-tests/direct-test-path-env-include-pnpm/steps.json b/packages/cli/snap-tests/direct-test-path-env-include-pnpm/steps.json index 24fd9c0058..2182499521 100644 --- a/packages/cli/snap-tests/direct-test-path-env-include-pnpm/steps.json +++ b/packages/cli/snap-tests/direct-test-path-env-include-pnpm/steps.json @@ -1,6 +1,9 @@ { "ignoredPlatforms": ["win32"], "commands": [ - "fake_bin=$(mktemp -d) && printf '#!/bin/sh\\necho 0.0.0\\n' > \"$fake_bin/pnpm\" && chmod +x \"$fake_bin/pnpm\" && PATH=\"$fake_bin:$PATH\" vp test" + { + "command": "fake_bin=$(mktemp -d) && printf '#!/bin/sh\\necho 0.0.0\\n' > \"$fake_bin/pnpm\" && chmod +x \"$fake_bin/pnpm\" && PATH=\"$fake_bin:$PATH\" vp test", + "ignoreOutput": true + } ] } diff --git a/packages/cli/snap-tests/direct-test-path-env-include-yarn/snap.txt b/packages/cli/snap-tests/direct-test-path-env-include-yarn/snap.txt index e313b8f7b9..23d790518c 100644 --- a/packages/cli/snap-tests/direct-test-path-env-include-yarn/snap.txt +++ b/packages/cli/snap-tests/direct-test-path-env-include-yarn/snap.txt @@ -1,11 +1 @@ -> fake_bin=$(mktemp -d) && printf '#!/bin/sh\necho 0.0.0\n' > "$fake_bin/yarn" && chmod +x "$fake_bin/yarn" && PATH="$fake_bin:$PATH" vp test - RUN - - ✓ src/package-manager.test.ts (1 test) ms - ✓ direct test command exposes the configured package manager on PATH ms - - Test Files 1 passed (1) - Tests 1 passed (1) - Start at - Duration ms (transform ms, setup ms, import ms, tests ms, environment ms) - +> fake_bin=$(mktemp -d) && printf '#!/bin/sh\necho 0.0.0\n' > "$fake_bin/yarn" && chmod +x "$fake_bin/yarn" && PATH="$fake_bin:$PATH" vp test \ No newline at end of file diff --git a/packages/cli/snap-tests/direct-test-path-env-include-yarn/steps.json b/packages/cli/snap-tests/direct-test-path-env-include-yarn/steps.json index 79fb7d6b5f..f4fd839439 100644 --- a/packages/cli/snap-tests/direct-test-path-env-include-yarn/steps.json +++ b/packages/cli/snap-tests/direct-test-path-env-include-yarn/steps.json @@ -1,6 +1,9 @@ { "ignoredPlatforms": ["win32"], "commands": [ - "fake_bin=$(mktemp -d) && printf '#!/bin/sh\\necho 0.0.0\\n' > \"$fake_bin/yarn\" && chmod +x \"$fake_bin/yarn\" && PATH=\"$fake_bin:$PATH\" vp test" + { + "command": "fake_bin=$(mktemp -d) && printf '#!/bin/sh\\necho 0.0.0\\n' > \"$fake_bin/yarn\" && chmod +x \"$fake_bin/yarn\" && PATH=\"$fake_bin:$PATH\" vp test", + "ignoreOutput": true + } ] } From 6ae7679bcfd81f28e80cd90acbc0044723bbb123 Mon Sep 17 00:00:00 2001 From: JongKyung Lee Date: Wed, 27 May 2026 20:02:44 +0900 Subject: [PATCH 4/6] fix(cli): keep direct PM PATH preflight best-effort Avoid failing direct built-ins when explicit package-manager resolution or ensure fails before the underlying tool starts. Align regression coverage with the global CLI path using a sanitized-PATH pnpm fixture. --- packages/cli/binding/src/cli/mod.rs | 87 ++++++++++++++++++- .../package.json | 2 +- .../snap.txt | 10 +++ .../src/managed-pm-path.test.ts} | 0 .../steps.json | 6 ++ .../package.json | 6 -- .../direct-test-path-env-include-bun/snap.txt | 1 - .../src/package-manager.test.ts | 8 -- .../steps.json | 9 -- .../vite.config.ts | 3 - .../package.json | 6 -- .../direct-test-path-env-include-npm/snap.txt | 1 - .../src/package-manager.test.ts | 8 -- .../steps.json | 9 -- .../vite.config.ts | 3 - .../snap.txt | 1 - .../steps.json | 9 -- .../vite.config.ts | 3 - .../package.json | 6 -- .../snap.txt | 1 - .../src/package-manager.test.ts | 8 -- .../steps.json | 9 -- .../vite.config.ts | 3 - 23 files changed, 100 insertions(+), 99 deletions(-) rename packages/cli/{snap-tests/direct-test-path-env-include-pnpm => snap-tests-global/test-managed-package-manager-path}/package.json (62%) create mode 100644 packages/cli/snap-tests-global/test-managed-package-manager-path/snap.txt rename packages/cli/{snap-tests/direct-test-path-env-include-pnpm/src/package-manager.test.ts => snap-tests-global/test-managed-package-manager-path/src/managed-pm-path.test.ts} (100%) create mode 100644 packages/cli/snap-tests-global/test-managed-package-manager-path/steps.json delete mode 100644 packages/cli/snap-tests/direct-test-path-env-include-bun/package.json delete mode 100644 packages/cli/snap-tests/direct-test-path-env-include-bun/snap.txt delete mode 100644 packages/cli/snap-tests/direct-test-path-env-include-bun/src/package-manager.test.ts delete mode 100644 packages/cli/snap-tests/direct-test-path-env-include-bun/steps.json delete mode 100644 packages/cli/snap-tests/direct-test-path-env-include-bun/vite.config.ts delete mode 100644 packages/cli/snap-tests/direct-test-path-env-include-npm/package.json delete mode 100644 packages/cli/snap-tests/direct-test-path-env-include-npm/snap.txt delete mode 100644 packages/cli/snap-tests/direct-test-path-env-include-npm/src/package-manager.test.ts delete mode 100644 packages/cli/snap-tests/direct-test-path-env-include-npm/steps.json delete mode 100644 packages/cli/snap-tests/direct-test-path-env-include-npm/vite.config.ts delete mode 100644 packages/cli/snap-tests/direct-test-path-env-include-pnpm/snap.txt delete mode 100644 packages/cli/snap-tests/direct-test-path-env-include-pnpm/steps.json delete mode 100644 packages/cli/snap-tests/direct-test-path-env-include-pnpm/vite.config.ts delete mode 100644 packages/cli/snap-tests/direct-test-path-env-include-yarn/package.json delete mode 100644 packages/cli/snap-tests/direct-test-path-env-include-yarn/snap.txt delete mode 100644 packages/cli/snap-tests/direct-test-path-env-include-yarn/src/package-manager.test.ts delete mode 100644 packages/cli/snap-tests/direct-test-path-env-include-yarn/steps.json delete mode 100644 packages/cli/snap-tests/direct-test-path-env-include-yarn/vite.config.ts diff --git a/packages/cli/binding/src/cli/mod.rs b/packages/cli/binding/src/cli/mod.rs index c21316a2f2..8bd7773ffb 100644 --- a/packages/cli/binding/src/cli/mod.rs +++ b/packages/cli/binding/src/cli/mod.rs @@ -129,7 +129,11 @@ fn prepend_to_env_path( .unwrap_or_else(|| Arc::from(OsStr::new("PATH"))); let current_path = envs.get(&path_key).map_or_else(Default::default, |path| path.to_os_string()); - let paths = env::split_paths(¤t_path).collect::>(); + let paths = if current_path.is_empty() { + Vec::new() + } else { + env::split_paths(¤t_path).collect::>() + }; if paths.first().is_some_and(|path| path == bin_prefix.as_path()) { return Ok(Arc::clone(envs)); @@ -150,17 +154,36 @@ async fn envs_with_explicit_package_manager_path( envs: Arc, Arc>>, ) -> Result, Arc>>, Error> { let Some(resolution) = - vite_install::package_manager::resolve_package_manager_from_package_json(cwd)? + (match vite_install::package_manager::resolve_package_manager_from_package_json(cwd) { + Ok(resolution) => resolution, + Err(error) => { + tracing::debug!( + ?error, + "failed to resolve explicit packageManager for direct command PATH setup" + ); + return Ok(envs); + } + }) else { return Ok(envs); }; - let (install_dir, _, _) = vite_install::download_package_manager( + let (install_dir, _, _) = match vite_install::download_package_manager( resolution.package_manager_type, &resolution.version, resolution.hash.as_deref(), ) - .await?; + .await + { + Ok(result) => result, + Err(error) => { + tracing::debug!( + ?error, + "failed to ensure managed package manager for direct command PATH setup" + ); + return Ok(envs); + } + }; prepend_to_env_path(&envs, &install_dir.join("bin")) } @@ -324,6 +347,62 @@ mod tests { assert_eq!(paths, vec![pm_bin.as_path().to_path_buf()]); } + #[test] + fn creates_path_when_env_map_has_no_path() { + let cwd = std::env::current_dir().expect("current_dir should exist"); + let pm_bin = AbsolutePathBuf::new(cwd.join("pm-bin")).expect("pm bin should be absolute"); + let envs = Arc::new(FxHashMap::default()); + + let updated = prepend_to_env_path(&envs, &pm_bin).expect("PATH should update"); + let path_value = updated.get(OsStr::new("PATH")).expect("PATH should be created"); + let paths = std::env::split_paths(path_value).collect::>(); + + assert_eq!(paths, vec![pm_bin.as_path().to_path_buf()]); + } + + #[test] + fn preserves_path_key_casing_on_windows() { + let cwd = std::env::current_dir().expect("current_dir should exist"); + let old_bin = cwd.join("old-bin"); + let pm_bin = AbsolutePathBuf::new(cwd.join("pm-bin")).expect("pm bin should be absolute"); + let original_path = std::env::join_paths([old_bin.as_path()]).expect("valid PATH"); + let key = if cfg!(windows) { "Path" } else { "PATH" }; + let envs = Arc::new(FxHashMap::from_iter([( + Arc::from(OsStr::new(key)), + Arc::from(original_path.as_os_str()), + )])); + + let updated = prepend_to_env_path(&envs, &pm_bin).expect("PATH should update"); + let path_value = updated.get(OsStr::new(key)).expect("existing PATH key should be updated"); + let paths = std::env::split_paths(path_value).collect::>(); + + assert_eq!(paths.first().map(std::path::PathBuf::as_path), Some(pm_bin.as_path())); + assert_eq!(paths.get(1).map(std::path::PathBuf::as_path), Some(old_bin.as_path())); + } + + #[tokio::test] + async fn ignores_invalid_explicit_package_manager() { + let suffix = + SystemTime::now().duration_since(UNIX_EPOCH).expect("time should be valid").as_nanos(); + let temp_dir = std::env::temp_dir().join(format!("vite-plus-invalid-pm-{suffix}")); + fs::create_dir_all(&temp_dir).expect("temp dir should be created"); + fs::write( + temp_dir.join("package.json"), + r#"{"name":"fixture","packageManager":"unknown@1.0.0"}"#, + ) + .expect("package.json should be written"); + let cwd = AbsolutePathBuf::new(temp_dir.clone()).expect("temp dir should be absolute"); + let original_path = std::env::join_paths([temp_dir.join("old-bin")]).expect("valid PATH"); + let envs = envs_with_path(original_path.as_os_str()); + + let updated = envs_with_explicit_package_manager_path(&cwd, Arc::clone(&envs)) + .await + .expect("package manager preflight errors should not fail direct commands"); + + assert_eq!(updated.get(OsStr::new("PATH")), envs.get(OsStr::new("PATH"))); + fs::remove_dir_all(temp_dir).expect("temp dir should be removed"); + } + #[tokio::test] async fn ignores_lockfile_without_explicit_package_manager() { let suffix = diff --git a/packages/cli/snap-tests/direct-test-path-env-include-pnpm/package.json b/packages/cli/snap-tests-global/test-managed-package-manager-path/package.json similarity index 62% rename from packages/cli/snap-tests/direct-test-path-env-include-pnpm/package.json rename to packages/cli/snap-tests-global/test-managed-package-manager-path/package.json index 53e470ac1a..c54481aefb 100644 --- a/packages/cli/snap-tests/direct-test-path-env-include-pnpm/package.json +++ b/packages/cli/snap-tests-global/test-managed-package-manager-path/package.json @@ -1,5 +1,5 @@ { - "name": "direct-test-path-env-include-pnpm", + "name": "test-managed-package-manager-path", "private": true, "type": "module", "packageManager": "pnpm@11.2.2" diff --git a/packages/cli/snap-tests-global/test-managed-package-manager-path/snap.txt b/packages/cli/snap-tests-global/test-managed-package-manager-path/snap.txt new file mode 100644 index 0000000000..e5ec7c881f --- /dev/null +++ b/packages/cli/snap-tests-global/test-managed-package-manager-path/snap.txt @@ -0,0 +1,10 @@ +> sh -c 'vp_bin=$(command -v vp) && node_path=$(command -v node) && node_bin=$(mktemp -d) && ln -s "$node_path" "$node_bin/node" && sanitized_path="$node_bin:/bin:/usr/bin" && PATH="$sanitized_path" node --version >/dev/null && if PATH="$sanitized_path" command -v pnpm >/dev/null; then echo "pnpm unexpectedly available on sanitized PATH"; exit 1; fi && PATH="$sanitized_path" "$vp_bin" test' + RUN + + ✓ src/managed-pm-path.test.ts (1 test) ms + + Test Files 1 passed (1) + Tests 1 passed (1) + Start at + Duration ms (transform ms, setup ms, import ms, tests ms, environment ms) + diff --git a/packages/cli/snap-tests/direct-test-path-env-include-pnpm/src/package-manager.test.ts b/packages/cli/snap-tests-global/test-managed-package-manager-path/src/managed-pm-path.test.ts similarity index 100% rename from packages/cli/snap-tests/direct-test-path-env-include-pnpm/src/package-manager.test.ts rename to packages/cli/snap-tests-global/test-managed-package-manager-path/src/managed-pm-path.test.ts diff --git a/packages/cli/snap-tests-global/test-managed-package-manager-path/steps.json b/packages/cli/snap-tests-global/test-managed-package-manager-path/steps.json new file mode 100644 index 0000000000..2b312eb996 --- /dev/null +++ b/packages/cli/snap-tests-global/test-managed-package-manager-path/steps.json @@ -0,0 +1,6 @@ +{ + "ignoredPlatforms": ["win32"], + "commands": [ + "sh -c 'vp_bin=$(command -v vp) && node_path=$(command -v node) && node_bin=$(mktemp -d) && ln -s \"$node_path\" \"$node_bin/node\" && sanitized_path=\"$node_bin:/bin:/usr/bin\" && PATH=\"$sanitized_path\" node --version >/dev/null && if PATH=\"$sanitized_path\" command -v pnpm >/dev/null; then echo \"pnpm unexpectedly available on sanitized PATH\"; exit 1; fi && PATH=\"$sanitized_path\" \"$vp_bin\" test'" + ] +} diff --git a/packages/cli/snap-tests/direct-test-path-env-include-bun/package.json b/packages/cli/snap-tests/direct-test-path-env-include-bun/package.json deleted file mode 100644 index 8522b7d9b7..0000000000 --- a/packages/cli/snap-tests/direct-test-path-env-include-bun/package.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "name": "direct-test-path-env-include-bun", - "private": true, - "type": "module", - "packageManager": "bun@1.3.11" -} diff --git a/packages/cli/snap-tests/direct-test-path-env-include-bun/snap.txt b/packages/cli/snap-tests/direct-test-path-env-include-bun/snap.txt deleted file mode 100644 index ae59c5ef34..0000000000 --- a/packages/cli/snap-tests/direct-test-path-env-include-bun/snap.txt +++ /dev/null @@ -1 +0,0 @@ -> fake_bin=$(mktemp -d) && printf '#!/bin/sh\necho 0.0.0\n' > "$fake_bin/bun" && chmod +x "$fake_bin/bun" && PATH="$fake_bin:$PATH" vp test \ No newline at end of file diff --git a/packages/cli/snap-tests/direct-test-path-env-include-bun/src/package-manager.test.ts b/packages/cli/snap-tests/direct-test-path-env-include-bun/src/package-manager.test.ts deleted file mode 100644 index 4a977f439e..0000000000 --- a/packages/cli/snap-tests/direct-test-path-env-include-bun/src/package-manager.test.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { execFileSync } from 'node:child_process'; - -import { expect, test } from '@voidzero-dev/vite-plus-test'; - -test('direct test command exposes the configured package manager on PATH', () => { - const version = execFileSync('bun', ['--version'], { encoding: 'utf8' }).trim(); - expect(version).toBe('1.3.11'); -}); diff --git a/packages/cli/snap-tests/direct-test-path-env-include-bun/steps.json b/packages/cli/snap-tests/direct-test-path-env-include-bun/steps.json deleted file mode 100644 index dc311bd972..0000000000 --- a/packages/cli/snap-tests/direct-test-path-env-include-bun/steps.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "ignoredPlatforms": ["win32"], - "commands": [ - { - "command": "fake_bin=$(mktemp -d) && printf '#!/bin/sh\\necho 0.0.0\\n' > \"$fake_bin/bun\" && chmod +x \"$fake_bin/bun\" && PATH=\"$fake_bin:$PATH\" vp test", - "ignoreOutput": true - } - ] -} diff --git a/packages/cli/snap-tests/direct-test-path-env-include-bun/vite.config.ts b/packages/cli/snap-tests/direct-test-path-env-include-bun/vite.config.ts deleted file mode 100644 index 3210accbf9..0000000000 --- a/packages/cli/snap-tests/direct-test-path-env-include-bun/vite.config.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { defineConfig } from 'vite-plus'; - -export default defineConfig({}); diff --git a/packages/cli/snap-tests/direct-test-path-env-include-npm/package.json b/packages/cli/snap-tests/direct-test-path-env-include-npm/package.json deleted file mode 100644 index 7c61580e9b..0000000000 --- a/packages/cli/snap-tests/direct-test-path-env-include-npm/package.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "name": "direct-test-path-env-include-npm", - "private": true, - "type": "module", - "packageManager": "npm@10.9.4" -} diff --git a/packages/cli/snap-tests/direct-test-path-env-include-npm/snap.txt b/packages/cli/snap-tests/direct-test-path-env-include-npm/snap.txt deleted file mode 100644 index b2978f607a..0000000000 --- a/packages/cli/snap-tests/direct-test-path-env-include-npm/snap.txt +++ /dev/null @@ -1 +0,0 @@ -> fake_bin=$(mktemp -d) && printf '#!/bin/sh\necho 0.0.0\n' > "$fake_bin/npm" && chmod +x "$fake_bin/npm" && PATH="$fake_bin:$PATH" vp test \ No newline at end of file diff --git a/packages/cli/snap-tests/direct-test-path-env-include-npm/src/package-manager.test.ts b/packages/cli/snap-tests/direct-test-path-env-include-npm/src/package-manager.test.ts deleted file mode 100644 index 0649bc6bc0..0000000000 --- a/packages/cli/snap-tests/direct-test-path-env-include-npm/src/package-manager.test.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { execFileSync } from 'node:child_process'; - -import { expect, test } from '@voidzero-dev/vite-plus-test'; - -test('direct test command exposes the configured package manager on PATH', () => { - const version = execFileSync('npm', ['--version'], { encoding: 'utf8' }).trim(); - expect(version).toBe('10.9.4'); -}); diff --git a/packages/cli/snap-tests/direct-test-path-env-include-npm/steps.json b/packages/cli/snap-tests/direct-test-path-env-include-npm/steps.json deleted file mode 100644 index edeb78e97d..0000000000 --- a/packages/cli/snap-tests/direct-test-path-env-include-npm/steps.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "ignoredPlatforms": ["win32"], - "commands": [ - { - "command": "fake_bin=$(mktemp -d) && printf '#!/bin/sh\\necho 0.0.0\\n' > \"$fake_bin/npm\" && chmod +x \"$fake_bin/npm\" && PATH=\"$fake_bin:$PATH\" vp test", - "ignoreOutput": true - } - ] -} diff --git a/packages/cli/snap-tests/direct-test-path-env-include-npm/vite.config.ts b/packages/cli/snap-tests/direct-test-path-env-include-npm/vite.config.ts deleted file mode 100644 index 3210accbf9..0000000000 --- a/packages/cli/snap-tests/direct-test-path-env-include-npm/vite.config.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { defineConfig } from 'vite-plus'; - -export default defineConfig({}); diff --git a/packages/cli/snap-tests/direct-test-path-env-include-pnpm/snap.txt b/packages/cli/snap-tests/direct-test-path-env-include-pnpm/snap.txt deleted file mode 100644 index 162b4142c8..0000000000 --- a/packages/cli/snap-tests/direct-test-path-env-include-pnpm/snap.txt +++ /dev/null @@ -1 +0,0 @@ -> fake_bin=$(mktemp -d) && printf '#!/bin/sh\necho 0.0.0\n' > "$fake_bin/pnpm" && chmod +x "$fake_bin/pnpm" && PATH="$fake_bin:$PATH" vp test \ No newline at end of file diff --git a/packages/cli/snap-tests/direct-test-path-env-include-pnpm/steps.json b/packages/cli/snap-tests/direct-test-path-env-include-pnpm/steps.json deleted file mode 100644 index 2182499521..0000000000 --- a/packages/cli/snap-tests/direct-test-path-env-include-pnpm/steps.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "ignoredPlatforms": ["win32"], - "commands": [ - { - "command": "fake_bin=$(mktemp -d) && printf '#!/bin/sh\\necho 0.0.0\\n' > \"$fake_bin/pnpm\" && chmod +x \"$fake_bin/pnpm\" && PATH=\"$fake_bin:$PATH\" vp test", - "ignoreOutput": true - } - ] -} diff --git a/packages/cli/snap-tests/direct-test-path-env-include-pnpm/vite.config.ts b/packages/cli/snap-tests/direct-test-path-env-include-pnpm/vite.config.ts deleted file mode 100644 index 3210accbf9..0000000000 --- a/packages/cli/snap-tests/direct-test-path-env-include-pnpm/vite.config.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { defineConfig } from 'vite-plus'; - -export default defineConfig({}); diff --git a/packages/cli/snap-tests/direct-test-path-env-include-yarn/package.json b/packages/cli/snap-tests/direct-test-path-env-include-yarn/package.json deleted file mode 100644 index ea727e5217..0000000000 --- a/packages/cli/snap-tests/direct-test-path-env-include-yarn/package.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "name": "direct-test-path-env-include-yarn", - "private": true, - "type": "module", - "packageManager": "yarn@4.10.3" -} diff --git a/packages/cli/snap-tests/direct-test-path-env-include-yarn/snap.txt b/packages/cli/snap-tests/direct-test-path-env-include-yarn/snap.txt deleted file mode 100644 index 23d790518c..0000000000 --- a/packages/cli/snap-tests/direct-test-path-env-include-yarn/snap.txt +++ /dev/null @@ -1 +0,0 @@ -> fake_bin=$(mktemp -d) && printf '#!/bin/sh\necho 0.0.0\n' > "$fake_bin/yarn" && chmod +x "$fake_bin/yarn" && PATH="$fake_bin:$PATH" vp test \ No newline at end of file diff --git a/packages/cli/snap-tests/direct-test-path-env-include-yarn/src/package-manager.test.ts b/packages/cli/snap-tests/direct-test-path-env-include-yarn/src/package-manager.test.ts deleted file mode 100644 index c3519a6b81..0000000000 --- a/packages/cli/snap-tests/direct-test-path-env-include-yarn/src/package-manager.test.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { execFileSync } from 'node:child_process'; - -import { expect, test } from '@voidzero-dev/vite-plus-test'; - -test('direct test command exposes the configured package manager on PATH', () => { - const version = execFileSync('yarn', ['--version'], { encoding: 'utf8' }).trim(); - expect(version).toBe('4.10.3'); -}); diff --git a/packages/cli/snap-tests/direct-test-path-env-include-yarn/steps.json b/packages/cli/snap-tests/direct-test-path-env-include-yarn/steps.json deleted file mode 100644 index f4fd839439..0000000000 --- a/packages/cli/snap-tests/direct-test-path-env-include-yarn/steps.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "ignoredPlatforms": ["win32"], - "commands": [ - { - "command": "fake_bin=$(mktemp -d) && printf '#!/bin/sh\\necho 0.0.0\\n' > \"$fake_bin/yarn\" && chmod +x \"$fake_bin/yarn\" && PATH=\"$fake_bin:$PATH\" vp test", - "ignoreOutput": true - } - ] -} diff --git a/packages/cli/snap-tests/direct-test-path-env-include-yarn/vite.config.ts b/packages/cli/snap-tests/direct-test-path-env-include-yarn/vite.config.ts deleted file mode 100644 index 3210accbf9..0000000000 --- a/packages/cli/snap-tests/direct-test-path-env-include-yarn/vite.config.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { defineConfig } from 'vite-plus'; - -export default defineConfig({}); From 9d01e8827394b9afc407be755b806153eb14d86d Mon Sep 17 00:00:00 2001 From: JongKyung Lee Date: Wed, 27 May 2026 21:22:56 +0900 Subject: [PATCH 5/6] fix(cli): keep PATH prepend fallback non-fatal Treat direct-command PATH augmentation as fully best-effort, including PATH join/prepend failures. Exclude global snap fixtures from the root unit-test discovery so they only run through the snap harness. --- packages/cli/binding/src/cli/mod.rs | 43 +++++++++++++++++++++++++---- vite.config.ts | 1 + 2 files changed, 38 insertions(+), 6 deletions(-) diff --git a/packages/cli/binding/src/cli/mod.rs b/packages/cli/binding/src/cli/mod.rs index 8bd7773ffb..bfc6c9ab42 100644 --- a/packages/cli/binding/src/cli/mod.rs +++ b/packages/cli/binding/src/cli/mod.rs @@ -118,7 +118,7 @@ fn is_path_env_key(key: &OsStr) -> bool { if cfg!(windows) { key.eq_ignore_ascii_case("PATH") } else { key == "PATH" } } -fn prepend_to_env_path( +fn try_prepend_to_env_path( envs: &Arc, Arc>>, bin_prefix: &AbsolutePath, ) -> Result, Arc>>, Error> { @@ -149,6 +149,22 @@ fn prepend_to_env_path( Ok(Arc::new(envs)) } +fn prepend_to_env_path( + envs: &Arc, Arc>>, + bin_prefix: &AbsolutePath, +) -> Arc, Arc>> { + match try_prepend_to_env_path(envs, bin_prefix) { + Ok(updated_envs) => updated_envs, + Err(error) => { + tracing::debug!( + ?error, + "failed to prepend managed package manager bin to direct command PATH" + ); + Arc::clone(envs) + } + } +} + async fn envs_with_explicit_package_manager_path( cwd: &AbsolutePath, envs: Arc, Arc>>, @@ -185,7 +201,7 @@ async fn envs_with_explicit_package_manager_path( } }; - prepend_to_env_path(&envs, &install_dir.join("bin")) + Ok(prepend_to_env_path(&envs, &install_dir.join("bin"))) } /// Execute a vite-task command (run, cache) through Session. @@ -325,7 +341,7 @@ mod tests { let original_path = std::env::join_paths([old_bin.as_path()]).expect("valid PATH"); let envs = envs_with_path(original_path.as_os_str()); - let updated = prepend_to_env_path(&envs, &pm_bin).expect("PATH should update"); + let updated = prepend_to_env_path(&envs, &pm_bin); let path_value = updated.get(OsStr::new("PATH")).expect("PATH should exist"); let paths = std::env::split_paths(path_value).collect::>(); @@ -340,7 +356,7 @@ mod tests { let original_path = std::env::join_paths([pm_bin.as_path()]).expect("valid PATH"); let envs = envs_with_path(original_path.as_os_str()); - let updated = prepend_to_env_path(&envs, &pm_bin).expect("PATH should update"); + let updated = prepend_to_env_path(&envs, &pm_bin); let path_value = updated.get(OsStr::new("PATH")).expect("PATH should exist"); let paths = std::env::split_paths(path_value).collect::>(); @@ -353,7 +369,7 @@ mod tests { let pm_bin = AbsolutePathBuf::new(cwd.join("pm-bin")).expect("pm bin should be absolute"); let envs = Arc::new(FxHashMap::default()); - let updated = prepend_to_env_path(&envs, &pm_bin).expect("PATH should update"); + let updated = prepend_to_env_path(&envs, &pm_bin); let path_value = updated.get(OsStr::new("PATH")).expect("PATH should be created"); let paths = std::env::split_paths(path_value).collect::>(); @@ -372,7 +388,7 @@ mod tests { Arc::from(original_path.as_os_str()), )])); - let updated = prepend_to_env_path(&envs, &pm_bin).expect("PATH should update"); + let updated = prepend_to_env_path(&envs, &pm_bin); let path_value = updated.get(OsStr::new(key)).expect("existing PATH key should be updated"); let paths = std::env::split_paths(path_value).collect::>(); @@ -380,6 +396,21 @@ mod tests { assert_eq!(paths.get(1).map(std::path::PathBuf::as_path), Some(old_bin.as_path())); } + #[test] + fn keeps_original_env_when_path_prepend_fails() { + let cwd = std::env::current_dir().expect("current_dir should exist"); + let old_bin = cwd.join("old-bin"); + let invalid_dir_name = if cfg!(windows) { "pm;bin" } else { "pm:bin" }; + let pm_bin = + AbsolutePathBuf::new(cwd.join(invalid_dir_name)).expect("pm bin should be absolute"); + let original_path = std::env::join_paths([old_bin.as_path()]).expect("valid PATH"); + let envs = envs_with_path(original_path.as_os_str()); + + let updated = prepend_to_env_path(&envs, &pm_bin); + + assert_eq!(updated.get(OsStr::new("PATH")), envs.get(OsStr::new("PATH"))); + } + #[tokio::test] async fn ignores_invalid_explicit_package_manager() { let suffix = diff --git a/vite.config.ts b/vite.config.ts index 18e9c63990..4ddb9187f3 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -55,6 +55,7 @@ export default defineConfig({ './rolldown/**', '**/node_modules/**', '**/snap-tests/**', + '**/snap-tests-global/**', // FIXME: Error: failed to prepare the command for injection: Invalid argument (os error 22) 'packages/*/binding/__tests__/', ], From fc5082020660e1fbfcbad1751a98b4c330ab3cf5 Mon Sep 17 00:00:00 2001 From: JongKyung Lee Date: Wed, 27 May 2026 21:52:12 +0900 Subject: [PATCH 6/6] test(cli): stabilize managed package manager PATH snap Drop the invalid PATH fallback unit test with platform-specific assumptions and raise the Vitest slow-test reporting threshold for the focused global snapshot. --- packages/cli/binding/src/cli/mod.rs | 15 --------------- .../test-managed-package-manager-path/snap.txt | 2 +- .../test-managed-package-manager-path/steps.json | 2 +- 3 files changed, 2 insertions(+), 17 deletions(-) diff --git a/packages/cli/binding/src/cli/mod.rs b/packages/cli/binding/src/cli/mod.rs index bfc6c9ab42..5033a24339 100644 --- a/packages/cli/binding/src/cli/mod.rs +++ b/packages/cli/binding/src/cli/mod.rs @@ -396,21 +396,6 @@ mod tests { assert_eq!(paths.get(1).map(std::path::PathBuf::as_path), Some(old_bin.as_path())); } - #[test] - fn keeps_original_env_when_path_prepend_fails() { - let cwd = std::env::current_dir().expect("current_dir should exist"); - let old_bin = cwd.join("old-bin"); - let invalid_dir_name = if cfg!(windows) { "pm;bin" } else { "pm:bin" }; - let pm_bin = - AbsolutePathBuf::new(cwd.join(invalid_dir_name)).expect("pm bin should be absolute"); - let original_path = std::env::join_paths([old_bin.as_path()]).expect("valid PATH"); - let envs = envs_with_path(original_path.as_os_str()); - - let updated = prepend_to_env_path(&envs, &pm_bin); - - assert_eq!(updated.get(OsStr::new("PATH")), envs.get(OsStr::new("PATH"))); - } - #[tokio::test] async fn ignores_invalid_explicit_package_manager() { let suffix = diff --git a/packages/cli/snap-tests-global/test-managed-package-manager-path/snap.txt b/packages/cli/snap-tests-global/test-managed-package-manager-path/snap.txt index e5ec7c881f..3d83ee3c9e 100644 --- a/packages/cli/snap-tests-global/test-managed-package-manager-path/snap.txt +++ b/packages/cli/snap-tests-global/test-managed-package-manager-path/snap.txt @@ -1,4 +1,4 @@ -> sh -c 'vp_bin=$(command -v vp) && node_path=$(command -v node) && node_bin=$(mktemp -d) && ln -s "$node_path" "$node_bin/node" && sanitized_path="$node_bin:/bin:/usr/bin" && PATH="$sanitized_path" node --version >/dev/null && if PATH="$sanitized_path" command -v pnpm >/dev/null; then echo "pnpm unexpectedly available on sanitized PATH"; exit 1; fi && PATH="$sanitized_path" "$vp_bin" test' +> sh -c 'vp_bin=$(command -v vp) && node_path=$(command -v node) && node_bin=$(mktemp -d) && ln -s "$node_path" "$node_bin/node" && sanitized_path="$node_bin:/bin:/usr/bin" && PATH="$sanitized_path" node --version >/dev/null && if PATH="$sanitized_path" command -v pnpm >/dev/null; then echo "pnpm unexpectedly available on sanitized PATH"; exit 1; fi && PATH="$sanitized_path" "$vp_bin" test --slowTestThreshold 10000' RUN ✓ src/managed-pm-path.test.ts (1 test) ms diff --git a/packages/cli/snap-tests-global/test-managed-package-manager-path/steps.json b/packages/cli/snap-tests-global/test-managed-package-manager-path/steps.json index 2b312eb996..e119af84ef 100644 --- a/packages/cli/snap-tests-global/test-managed-package-manager-path/steps.json +++ b/packages/cli/snap-tests-global/test-managed-package-manager-path/steps.json @@ -1,6 +1,6 @@ { "ignoredPlatforms": ["win32"], "commands": [ - "sh -c 'vp_bin=$(command -v vp) && node_path=$(command -v node) && node_bin=$(mktemp -d) && ln -s \"$node_path\" \"$node_bin/node\" && sanitized_path=\"$node_bin:/bin:/usr/bin\" && PATH=\"$sanitized_path\" node --version >/dev/null && if PATH=\"$sanitized_path\" command -v pnpm >/dev/null; then echo \"pnpm unexpectedly available on sanitized PATH\"; exit 1; fi && PATH=\"$sanitized_path\" \"$vp_bin\" test'" + "sh -c 'vp_bin=$(command -v vp) && node_path=$(command -v node) && node_bin=$(mktemp -d) && ln -s \"$node_path\" \"$node_bin/node\" && sanitized_path=\"$node_bin:/bin:/usr/bin\" && PATH=\"$sanitized_path\" node --version >/dev/null && if PATH=\"$sanitized_path\" command -v pnpm >/dev/null; then echo \"pnpm unexpectedly available on sanitized PATH\"; exit 1; fi && PATH=\"$sanitized_path\" \"$vp_bin\" test --slowTestThreshold 10000'" ] }