diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_env_install_no_arg/snapshots/command_env_install_standalone_npm_fallback.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_env_install_no_arg/snapshots/command_env_install_standalone_npm_fallback.md index 0f15d9154e..3f1e3d792c 100644 --- a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_env_install_no_arg/snapshots/command_env_install_standalone_npm_fallback.md +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_env_install_no_arg/snapshots/command_env_install_standalone_npm_fallback.md @@ -7,7 +7,7 @@ Explicit npm family scopes use standalone registry npm; only the directly invoke an explicit npm scope exports the standalone npm fallback ``` -export VP_PACKAGE_MANAGER=npm@12.0.2 +export VP_NPM_VERSION=12.0.2 Using npm (resolved from registry fallback) ``` diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_env_package_manager_diagnostics/snapshots.toml b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_env_package_manager_diagnostics/snapshots.toml index 3db9ca3129..e3f389ad03 100644 --- a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_env_package_manager_diagnostics/snapshots.toml +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_env_package_manager_diagnostics/snapshots.toml @@ -17,7 +17,7 @@ steps = [ { argv = ["vp", "env", "use", "npm@10.9.4", "--no-install"], snapshot = false }, { argv = ["vpt", "write-file", "$VP_HOME/package_manager/npm/10.9.4/npm/bin/npm", "#!/bin/sh\n"], snapshot = false }, { argv = ["vpt", "chmod", "+x", "$VP_HOME/package_manager/npm/10.9.4/npm/bin/npm"], snapshot = false }, - { argv = ["vp", "env", "current", "pm", "--json"], comment = "current reports the package-manager session file path" }, + { argv = ["vp", "env", "current", "npm", "--json"], comment = "current reports the package-manager session file path" }, { argv = ["vp", "env", "which", "npm"], comment = "which reports the package-manager session file as its source" }, ] @@ -43,5 +43,5 @@ skip-platforms = ["windows"] steps = [ { argv = ["vp", "env", "use", "pnpm@10.18.0", "--no-install"], snapshot = false }, { argv = ["vp", "env", "use", "--unset", "pnpm"], envs = [["VP_PACKAGE_MANAGER", "yarn@4.12.0"]], snapshot = false }, - { argv = ["vpt", "stat-file", "$VP_HOME/.session-package-manager", "--assert", "missing"], comment = "a different environment override does not hide the matching session file from scoped cleanup" }, + { argv = ["vpt", "stat-file", "$VP_HOME/.session-pnpm-version", "--assert", "missing"], comment = "a different environment override does not hide the matching session file from scoped cleanup" }, ] diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_env_package_manager_diagnostics/snapshots/command_env_package_manager_session_provenance.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_env_package_manager_diagnostics/snapshots/command_env_package_manager_session_provenance.md index 534ce55cd2..2c0a4b398d 100644 --- a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_env_package_manager_diagnostics/snapshots/command_env_package_manager_session_provenance.md +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_env_package_manager_diagnostics/snapshots/command_env_package_manager_session_provenance.md @@ -10,7 +10,7 @@ ## `vpt chmod +x $VP_HOME/package_manager/npm/10.9.4/npm/bin/npm` -## `vp env current pm --json` +## `vp env current npm --json` current reports the package-manager session file path @@ -19,8 +19,8 @@ current reports the package-manager session file path "package_manager": { "name": "npm", "version": "", - "source": ".session-package-manager", - "source_path": "/.vite-plus/.session-package-manager", + "source": ".session-npm-version", + "source_path": "/.vite-plus/.session-npm-version", "bin_paths": { "npm": "/.vite-plus/package_manager/npm//npm/bin/npm", "npx": "/.vite-plus/package_manager/npm//npm/bin/npx" @@ -40,5 +40,5 @@ VITE+ - The Unified Toolchain for the Web /.vite-plus/package_manager/npm//npm/bin/npm Package: npm@10.9.4 - Source: /.vite-plus/.session-package-manager + Source: /.vite-plus/.session-npm-version ``` diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_env_package_manager_diagnostics/snapshots/command_env_unset_session_independently_of_override.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_env_package_manager_diagnostics/snapshots/command_env_unset_session_independently_of_override.md index cc8ad9f29d..6351eb4479 100644 --- a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_env_package_manager_diagnostics/snapshots/command_env_unset_session_independently_of_override.md +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_env_package_manager_diagnostics/snapshots/command_env_unset_session_independently_of_override.md @@ -8,10 +8,10 @@ Scoped unset must inspect and clear the session file independently of any differ ## `VP_PACKAGE_MANAGER=yarn@4.12.0 vp env use --unset pnpm` -## `vpt stat-file $VP_HOME/.session-package-manager --assert missing` +## `vpt stat-file $VP_HOME/.session-pnpm-version --assert missing` a different environment override does not hide the matching session file from scoped cleanup ``` -/.vite-plus/.session-package-manager: missing +/.vite-plus/.session-pnpm-version: missing ``` diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_env_package_manager_mismatch/snapshots/env_use_does_not_warn_for_different_default.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_env_package_manager_mismatch/snapshots/env_use_does_not_warn_for_different_default.md index c1ff7e9dbf..da3664aee6 100644 --- a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_env_package_manager_mismatch/snapshots/env_use_does_not_warn_for_different_default.md +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_env_package_manager_mismatch/snapshots/env_use_does_not_warn_for_different_default.md @@ -12,6 +12,6 @@ a different fallback manager does not warn ``` -export VP_PACKAGE_MANAGER=yarn@4.12.0 +export VP_YARN_VERSION=4.12.0 Using yarn (resolved from 4.12.0) ``` diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_env_package_manager_mismatch/snapshots/env_use_warns_when_package_manager_differs.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_env_package_manager_mismatch/snapshots/env_use_warns_when_package_manager_differs.md index 5608d80719..1b2b6dbb14 100644 --- a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_env_package_manager_mismatch/snapshots/env_use_warns_when_package_manager_differs.md +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_env_package_manager_mismatch/snapshots/env_use_warns_when_package_manager_differs.md @@ -6,6 +6,6 @@ an explicit project manager warns before a different session manager is used ``` warn: Current environment resolves to pnpm from packageManager, but yarn was requested. -export VP_PACKAGE_MANAGER=yarn@4.12.0 +export VP_YARN_VERSION=4.12.0 Using yarn (resolved from 4.12.0) ``` diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_env_package_manager_modes/snapshots.toml b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_env_package_manager_modes/snapshots.toml index 7ad74cc8b8..7babda2948 100644 --- a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_env_package_manager_modes/snapshots.toml +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_env_package_manager_modes/snapshots.toml @@ -4,10 +4,10 @@ vp = "global" seed-runtime = false steps = [ { argv = ["vp", "env", "off", "pnpm"], comment = "switch only pnpm to system-first mode" }, - { argv = ["vp", "env", "current", "pnpm", "--json"], envs = [["VP_PACKAGE_MANAGER", "pnpm@10.18.0"], ["VP_BYPASS", "${PATH}"]], comment = "pnpm uses its individual mode" }, - { argv = ["vp", "env", "current", "bun", "--json"], envs = [["VP_PACKAGE_MANAGER", "bun@1.2.3"]], comment = "bun keeps the shared managed mode" }, + { argv = ["vp", "env", "current", "pnpm", "--json"], envs = [["VP_PNPM_VERSION", "10.18.0"], ["VP_BYPASS", "${PATH}"]], comment = "pnpm uses its individual mode" }, + { argv = ["vp", "env", "current", "bun", "--json"], envs = [["VP_BUN_VERSION", "1.2.3"]], comment = "bun keeps the shared managed mode" }, { argv = ["vp", "env", "on", "pnpm"], comment = "restore only pnpm to managed mode" }, - { argv = ["vp", "env", "current", "pnpm", "--json"], envs = [["VP_PACKAGE_MANAGER", "pnpm@10.18.0"]], comment = "pnpm returns to the shared managed mode" }, + { argv = ["vp", "env", "current", "pnpm", "--json"], envs = [["VP_PNPM_VERSION", "10.18.0"]], comment = "pnpm returns to the shared managed mode" }, ] [[case]] diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_env_package_manager_modes/snapshots/command_env_package_manager_modes.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_env_package_manager_modes/snapshots/command_env_package_manager_modes.md index c3fb82526c..396f5b3b7c 100644 --- a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_env_package_manager_modes/snapshots/command_env_package_manager_modes.md +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_env_package_manager_modes/snapshots/command_env_package_manager_modes.md @@ -14,7 +14,7 @@ Selected commands and shims will now prefer system tools, falling back to manage Run `vp env on` to always use Vite+ managed tools. ``` -## `VP_PACKAGE_MANAGER=pnpm@10.18.0 VP_BYPASS=${PATH} vp env current pnpm --json` +## `VP_PNPM_VERSION=10.18.0 VP_BYPASS=${PATH} vp env current pnpm --json` pnpm uses its individual mode @@ -23,7 +23,7 @@ pnpm uses its individual mode "package_manager": { "name": "pnpm", "version": "", - "source": "VP_PACKAGE_MANAGER", + "source": "VP_PNPM_VERSION", "bin_paths": { "pnpm": "/.vite-plus/package_manager/pnpm//pnpm/bin/pnpm", "pnpx": "/.vite-plus/package_manager/pnpm//pnpm/bin/pnpx" @@ -34,7 +34,7 @@ pnpm uses its individual mode } ``` -## `VP_PACKAGE_MANAGER=bun@1.2.3 vp env current bun --json` +## `VP_BUN_VERSION=1.2.3 vp env current bun --json` bun keeps the shared managed mode @@ -43,7 +43,7 @@ bun keeps the shared managed mode "package_manager": { "name": "bun", "version": "", - "source": "VP_PACKAGE_MANAGER", + "source": "VP_BUN_VERSION", "bin_paths": { "bun": "/.vite-plus/package_manager/bun//bun/bin/bun", "bunx": "/.vite-plus/package_manager/bun//bun/bin/bunx" @@ -68,7 +68,7 @@ Selected commands and shims will now use Vite+ managed tools. Run `vp env off` to prefer system tools instead. ``` -## `VP_PACKAGE_MANAGER=pnpm@10.18.0 vp env current pnpm --json` +## `VP_PNPM_VERSION=10.18.0 vp env current pnpm --json` pnpm returns to the shared managed mode @@ -77,7 +77,7 @@ pnpm returns to the shared managed mode "package_manager": { "name": "pnpm", "version": "", - "source": "VP_PACKAGE_MANAGER", + "source": "VP_PNPM_VERSION", "bin_paths": { "pnpm": "/.vite-plus/package_manager/pnpm//pnpm/bin/pnpm", "pnpx": "/.vite-plus/package_manager/pnpm//pnpm/bin/pnpx" diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_env_use/snapshots.toml b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_env_use/snapshots.toml index f550341122..64f86fdba1 100644 --- a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_env_use/snapshots.toml +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_env_use/snapshots.toml @@ -9,7 +9,7 @@ steps = [ { argv = ["vp", "env", "use", "--unset"], comment = "should output unset command to stdout", continue-on-failure = true }, { argv = ["vp", "env", "use", "d"], comment = "should show friendly error for invalid version", continue-on-failure = true }, { argv = ["vp", "env", "use", "abc"], comment = "should show friendly error for invalid version", continue-on-failure = true }, - { argv = ["vp", "env", "use", "--silent-if-unchanged", "--no-install"], comment = "an unchanged project environment emits no shell mutations", envs = [["VP_NODE_VERSION", "20.18.0"], ["VP_PACKAGE_MANAGER", "npm@10.9.4"]] }, + { argv = ["vp", "env", "use", "--silent-if-unchanged", "--no-install"], comment = "an unchanged project environment emits no shell mutations", envs = [["VP_NODE_VERSION", "20.18.0"], ["VP_NPM_VERSION", "10.9.4"]] }, ] [[case]] @@ -19,6 +19,6 @@ comment = "The unchanged guard must return before installation so --silent-if-un seed-runtime = false steps = [ { argv = ["vpt", "write-file", "package.json", "{\"name\":\"command-env-use\",\"private\":true,\"packageManager\":\"pnpm@10.18.0\"}\n"], snapshot = false }, - { argv = ["vp", "env", "use", "pm", "--silent-if-unchanged"], envs = [["VP_PACKAGE_MANAGER", "pnpm@10.18.0"]], snapshot = false }, + { argv = ["vp", "env", "use", "pm", "--silent-if-unchanged"], envs = [["VP_PNPM_VERSION", "10.18.0"]], snapshot = false }, { argv = ["vpt", "stat-file", "$VP_HOME/package_manager/pnpm/10.18.0/pnpm/bin/pnpm", "--assert", "missing"], comment = "silent unchanged mode preserves the legacy no-op behavior" }, ] diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_env_use/snapshots/command_env_use.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_env_use/snapshots/command_env_use.md index 4933ff95cd..5c98767c1e 100644 --- a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_env_use/snapshots/command_env_use.md +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_env_use/snapshots/command_env_use.md @@ -43,7 +43,10 @@ should output unset command to stdout ``` unset VP_NODE_VERSION -unset VP_PACKAGE_MANAGER +unset VP_NPM_VERSION +unset VP_PNPM_VERSION +unset VP_YARN_VERSION +unset VP_BUN_VERSION Reverted selected components to project environment resolution ``` @@ -79,7 +82,7 @@ Valid examples: vp env use latest # Latest version ``` -## `VP_NODE_VERSION=20.18.0 VP_PACKAGE_MANAGER=npm@10.9.4 vp env use --silent-if-unchanged --no-install` +## `VP_NODE_VERSION=20.18.0 VP_NPM_VERSION=10.9.4 vp env use --silent-if-unchanged --no-install` an unchanged project environment emits no shell mutations diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_env_use/snapshots/command_env_use_silent_unchanged_stays_noop.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_env_use/snapshots/command_env_use_silent_unchanged_stays_noop.md index ab71a2a075..9452d04d94 100644 --- a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_env_use/snapshots/command_env_use_silent_unchanged_stays_noop.md +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_env_use/snapshots/command_env_use_silent_unchanged_stays_noop.md @@ -6,7 +6,7 @@ The unchanged guard must return before installation so --silent-if-unchanged rem '` -## `VP_PACKAGE_MANAGER=pnpm@10.18.0 vp env use pm --silent-if-unchanged` +## `VP_PNPM_VERSION=10.18.0 vp env use pm --silent-if-unchanged` ## `vpt stat-file $VP_HOME/package_manager/pnpm/10.18.0/pnpm/bin/pnpm --assert missing` diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_env_use_shells/snapshots.toml b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_env_use_shells/snapshots.toml index 42d1d16309..aa84c8fae0 100644 --- a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_env_use_shells/snapshots.toml +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_env_use_shells/snapshots.toml @@ -19,7 +19,7 @@ steps = [ name = "env_use_clears_stale_package_manager_override" vp = "global" skip-platforms = ["windows"] -env = { VP_ENV_USE_EVAL_ENABLE = "1", VP_PACKAGE_MANAGER = "pnpm@10.18.0" } +env = { VP_ENV_USE_EVAL_ENABLE = "1", VP_PNPM_VERSION = "10.18.0", VP_PACKAGE_MANAGER = "yarn@4.12.0" } steps = [ { argv = ["vp", "env", "use", "--no-install"], comment = "activating a project with no package-manager selection clears the previous override", envs = [["VP_SHELL", "bash"]] }, ] diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_env_use_shells/snapshots/command_env_use_shells.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_env_use_shells/snapshots/command_env_use_shells.md index f96a1999f0..ec9c747f94 100644 --- a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_env_use_shells/snapshots/command_env_use_shells.md +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_env_use_shells/snapshots/command_env_use_shells.md @@ -6,7 +6,7 @@ should detect bash and output both posix exports ``` export VP_NODE_VERSION=20.18.0 -export VP_PACKAGE_MANAGER=pnpm@10.18.0 +export VP_PNPM_VERSION=10.18.0 Using Node.js (resolved from 20.18.0) Using pnpm (resolved from 10.18.0) ``` @@ -17,7 +17,7 @@ should detect zsh and output both posix exports ``` export VP_NODE_VERSION=20.18.0 -export VP_PACKAGE_MANAGER=pnpm@10.18.0 +export VP_PNPM_VERSION=10.18.0 Using Node.js (resolved from 20.18.0) Using pnpm (resolved from 10.18.0) ``` @@ -28,7 +28,7 @@ should detect fish and output both fish exports ``` set -gx VP_NODE_VERSION 20.18.0 -set -gx VP_PACKAGE_MANAGER pnpm@10.18.0 +set -gx VP_PNPM_VERSION 10.18.0 Using Node.js (resolved from 20.18.0) Using pnpm (resolved from 10.18.0) ``` @@ -39,7 +39,7 @@ should detect nushell and output both nushell exports ``` $env.VP_NODE_VERSION = "20.18.0" -$env.VP_PACKAGE_MANAGER = "pnpm@10.18.0" +$env.VP_PNPM_VERSION = "10.18.0" Using Node.js (resolved from 20.18.0) Using pnpm (resolved from 10.18.0) ``` @@ -50,7 +50,7 @@ should detect powershell and output both powershell exports ``` $env:VP_NODE_VERSION = "20.18.0" -$env:VP_PACKAGE_MANAGER = "pnpm@10.18.0" +$env:VP_PNPM_VERSION = "10.18.0" Using Node.js (resolved from 20.18.0) Using pnpm (resolved from 10.18.0) ``` @@ -61,7 +61,7 @@ should detect cmd and output both cmd exports ``` set VP_NODE_VERSION=20.18.0 -set VP_PACKAGE_MANAGER=pnpm@10.18.0 +set VP_PNPM_VERSION=10.18.0 Using Node.js (resolved from 20.18.0) Using pnpm (resolved from 10.18.0) ``` @@ -72,7 +72,7 @@ should detect case-insensitive bash ``` export VP_NODE_VERSION=20.18.0 -export VP_PACKAGE_MANAGER=pnpm@10.18.0 +export VP_PNPM_VERSION=10.18.0 Using Node.js (resolved from 20.18.0) Using pnpm (resolved from 10.18.0) ``` @@ -83,7 +83,7 @@ should detect case-insensitive fish ``` set -gx VP_NODE_VERSION 20.18.0 -set -gx VP_PACKAGE_MANAGER pnpm@10.18.0 +set -gx VP_PNPM_VERSION 10.18.0 Using Node.js (resolved from 20.18.0) Using pnpm (resolved from 10.18.0) ``` @@ -94,7 +94,7 @@ should detect case-insensitive powershell ``` $env:VP_NODE_VERSION = "20.18.0" -$env:VP_PACKAGE_MANAGER = "pnpm@10.18.0" +$env:VP_PNPM_VERSION = "10.18.0" Using Node.js (resolved from 20.18.0) Using pnpm (resolved from 10.18.0) ``` diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_env_use_shells/snapshots/env_use_clears_stale_package_manager_override.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_env_use_shells/snapshots/env_use_clears_stale_package_manager_override.md index 7ddcc11086..d7fbae9e81 100644 --- a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_env_use_shells/snapshots/env_use_clears_stale_package_manager_override.md +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_env_use_shells/snapshots/env_use_clears_stale_package_manager_override.md @@ -6,6 +6,9 @@ activating a project with no package-manager selection clears the previous overr ``` export VP_NODE_VERSION=20.18.0 -unset VP_PACKAGE_MANAGER +unset VP_NPM_VERSION +unset VP_PNPM_VERSION +unset VP_YARN_VERSION +unset VP_BUN_VERSION Using Node.js (resolved from .node-version) ``` diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/shim_injected_tool_contracts/snapshots.toml b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/shim_injected_tool_contracts/snapshots.toml index e27fd6c4a8..ecff410570 100644 --- a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/shim_injected_tool_contracts/snapshots.toml +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/shim_injected_tool_contracts/snapshots.toml @@ -160,3 +160,12 @@ steps = [ { argv = ["vp", "env", "on", "npm"], snapshot = false }, { argv = ["vp", "env", "exec", "--node", "22.18.0", "node", "assert-unix-shim-path.cjs", "partial", "10.5.0"], comment = "A project npm pin still takes priority over bundled npm after a PATH reset" }, ] + +[[case]] +name = "explicit_npm_after_partial_path_reset" +vp = "global" +skip-platforms = ["windows"] +env = { VP_NPM_VERSION = "10.9.4" } +steps = [ + { argv = ["vp", "env", "exec", "--node", "22.18.0", "--npm", "10.5.0", "node", "assert-unix-shim-path.cjs", "partial", "10.5.0"], comment = "An explicit npm version overrides the inherited version and survives a PATH reset" }, +] diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/shim_injected_tool_contracts/snapshots/explicit_npm_after_partial_path_reset.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/shim_injected_tool_contracts/snapshots/explicit_npm_after_partial_path_reset.md new file mode 100644 index 0000000000..57cd4f464c --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/shim_injected_tool_contracts/snapshots/explicit_npm_after_partial_path_reset.md @@ -0,0 +1,9 @@ +# explicit_npm_after_partial_path_reset + +## `vp env exec --node 22.18.0 --npm 10.5.0 node assert-unix-shim-path.cjs partial 10.5.0` + +An explicit npm version overrides the inherited version and survives a PATH reset + +``` +Node and its tools survive partial PATH +``` diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/shim_package_manager_version_overrides/check-session.sh b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/shim_package_manager_version_overrides/check-session.sh new file mode 100644 index 0000000000..1f0a0941ac --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/shim_package_manager_version_overrides/check-session.sh @@ -0,0 +1,19 @@ +set -eu +. "$VP_HOME/env" + +export VP_PACKAGE_MANAGER=pnpm@10.19.0 +vp env use pnpm@10.20.0 --no-install +vp env use yarn@1.22.22 --no-install +test "$VP_PNPM_VERSION" = 10.20.0 +test "$VP_YARN_VERSION" = 1.22.22 +test "$VP_PACKAGE_MANAGER" = pnpm@10.19.0 + +vp env use pnpm --unset +test "${VP_PNPM_VERSION-unset}" = unset +test "$VP_YARN_VERSION" = 1.22.22 +test "$VP_PACKAGE_MANAGER" = pnpm@10.19.0 + +vp env use pm --unset +test "${VP_YARN_VERSION-unset}" = unset +test "$VP_PACKAGE_MANAGER" = pnpm@10.19.0 +echo 'env use keeps shim versions independent of the selected manager' diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/shim_package_manager_version_overrides/check.cjs b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/shim_package_manager_version_overrides/check.cjs new file mode 100644 index 0000000000..03054f0883 --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/shim_package_manager_version_overrides/check.cjs @@ -0,0 +1,15 @@ +const assert = require('node:assert/strict'); +const { execFileSync } = require('node:child_process'); +const { delimiter } = require('node:path'); + +// Start each child at the shim even though the parent Node process has injected tool paths. +const env = { + ...process.env, + PATH: [process.env.VP_HOME + '/bin', '/usr/bin', '/bin'].join(delimiter), + VP_PATH_INJECTED_TOOLS: '', +}; +const [tool, expected] = process.argv.slice(2); +const args = tool === 'vp' ? ['install', '--', '--version'] : ['--version']; +const actual = execFileSync(tool, args, { env, encoding: 'utf8' }).trim(); +assert.equal(actual, expected); +console.log(`${tool} uses the expected version`); diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/shim_package_manager_version_overrides/package.json b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/shim_package_manager_version_overrides/package.json new file mode 100644 index 0000000000..9447434d78 --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/shim_package_manager_version_overrides/package.json @@ -0,0 +1,5 @@ +{ + "name": "shim-package-manager-version-overrides", + "private": true, + "packageManager": "pnpm@10.18.0" +} diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/shim_package_manager_version_overrides/snapshots.toml b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/shim_package_manager_version_overrides/snapshots.toml new file mode 100644 index 0000000000..01ebecfeb4 --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/shim_package_manager_version_overrides/snapshots.toml @@ -0,0 +1,44 @@ +[[case]] +name = "shim_versions_are_independent" +vp = "global" +skip-platforms = ["windows"] +env = { VP_PACKAGE_MANAGER = "pnpm@10.19.0", VP_NPM_VERSION = "10.9.4", VP_PNPM_VERSION = "10.20.0", VP_YARN_VERSION = "1.22.22", VP_BUN_VERSION = "1.2.0", npm_config_manage_package_manager_versions = "false" } +steps = [ + ["node", "check.cjs", "npm", "10.9.4"], + ["node", "check.cjs", "npx", "10.9.4"], + ["node", "check.cjs", "pnpm", "10.20.0"], + ["node", "check.cjs", "yarn", "1.22.22"], + ["node", "check.cjs", "yarnpkg", "1.22.22"], + ["node", "check.cjs", "bun", "1.2.0"], + { argv = ["node", "check.cjs", "vp", "10.19.0"], comment = "Shim overrides do not override vp install's selected version" }, +] + +[[case]] +name = "env_use_keeps_independent_session_files" +vp = "global" +skip-platforms = ["windows"] +env = { VP_PACKAGE_MANAGER = "pnpm@10.19.0", npm_config_manage_package_manager_versions = "false" } +steps = [ + { argv = ["vp", "env", "use", "pnpm@10.20.0"], snapshot = false }, + { argv = ["vp", "env", "use", "yarn@1.22.22"], snapshot = false }, + ["node", "check.cjs", "pnpm", "10.20.0"], + ["node", "check.cjs", "yarn", "1.22.22"], + ["node", "check.cjs", "vp", "10.19.0"], + { argv = ["vp", "env", "current", "pnpm", "--json"], comment = "current identifies the per-family session source" }, + { argv = ["node", "check.cjs", "pnpm", "10.21.0"], envs = [["VP_PNPM_VERSION", "10.21.0"]], comment = "An environment version takes priority over the session file" }, + { argv = ["vp", "env", "use", "--unset", "pnpm"], snapshot = false }, + ["node", "check.cjs", "pnpm", "10.18.0"], + ["node", "check.cjs", "yarn", "1.22.22"], + { argv = ["vp", "env", "use", "--unset", "pm"], snapshot = false }, + ["vpt", "stat-file", "$VP_HOME/.session-yarn-version", "--assert", "missing"], + ["node", "check.cjs", "vp", "10.19.0"], +] + +[[case]] +name = "env_use_keeps_independent_shell_versions" +vp = "global" +skip-platforms = ["windows"] +requires = ["sh"] +steps = [ + ["sh", "check-session.sh"], +] diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/shim_package_manager_version_overrides/snapshots/env_use_keeps_independent_session_files.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/shim_package_manager_version_overrides/snapshots/env_use_keeps_independent_session_files.md new file mode 100644 index 0000000000..f89ea949d5 --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/shim_package_manager_version_overrides/snapshots/env_use_keeps_independent_session_files.md @@ -0,0 +1,84 @@ +# env_use_keeps_independent_session_files + +## `vp env use pnpm@10.20.0` + + +## `vp env use yarn@1.22.22` + + +## `node check.cjs pnpm 10.20.0` + +``` +pnpm uses the expected version +``` + +## `node check.cjs yarn 1.22.22` + +``` +yarn uses the expected version +``` + +## `node check.cjs vp 10.19.0` + +``` +vp uses the expected version +``` + +## `vp env current pnpm --json` + +current identifies the per-family session source + +``` +{ + "package_manager": { + "name": "pnpm", + "version": "", + "source": ".session-pnpm-version", + "source_path": "/.vite-plus/.session-pnpm-version", + "bin_paths": { + "pnpm": "/.vite-plus/package_manager/pnpm//pnpm/bin/pnpm", + "pnpx": "/.vite-plus/package_manager/pnpm//pnpm/bin/pnpx" + }, + "installed": true, + "mode": "managed" + } +} +``` + +## `VP_PNPM_VERSION=10.21.0 node check.cjs pnpm 10.21.0` + +An environment version takes priority over the session file + +``` +pnpm uses the expected version +``` + +## `vp env use --unset pnpm` + + +## `node check.cjs pnpm 10.18.0` + +``` +pnpm uses the expected version +``` + +## `node check.cjs yarn 1.22.22` + +``` +yarn uses the expected version +``` + +## `vp env use --unset pm` + + +## `vpt stat-file $VP_HOME/.session-yarn-version --assert missing` + +``` +/.vite-plus/.session-yarn-version: missing +``` + +## `node check.cjs vp 10.19.0` + +``` +vp uses the expected version +``` diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/shim_package_manager_version_overrides/snapshots/env_use_keeps_independent_shell_versions.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/shim_package_manager_version_overrides/snapshots/env_use_keeps_independent_shell_versions.md new file mode 100644 index 0000000000..5dc412c3ae --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/shim_package_manager_version_overrides/snapshots/env_use_keeps_independent_shell_versions.md @@ -0,0 +1,12 @@ +# env_use_keeps_independent_shell_versions + +## `sh check-session.sh` + +``` +Using pnpm (resolved from 10.20.0) +warn: Current environment resolves to pnpm from VP_PACKAGE_MANAGER, but yarn was requested. +Using yarn (resolved from 1.22.22) +Reverted selected components to project environment resolution +Reverted selected components to project environment resolution +env use keeps shim versions independent of the selected manager +``` diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/shim_package_manager_version_overrides/snapshots/shim_versions_are_independent.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/shim_package_manager_version_overrides/snapshots/shim_versions_are_independent.md new file mode 100644 index 0000000000..d3648675a9 --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/shim_package_manager_version_overrides/snapshots/shim_versions_are_independent.md @@ -0,0 +1,45 @@ +# shim_versions_are_independent + +## `node check.cjs npm 10.9.4` + +``` +npm uses the expected version +``` + +## `node check.cjs npx 10.9.4` + +``` +npx uses the expected version +``` + +## `node check.cjs pnpm 10.20.0` + +``` +pnpm uses the expected version +``` + +## `node check.cjs yarn 1.22.22` + +``` +yarn uses the expected version +``` + +## `node check.cjs yarnpkg 1.22.22` + +``` +yarnpkg uses the expected version +``` + +## `node check.cjs bun 1.2.0` + +``` +bun uses the expected version +``` + +## `node check.cjs vp 10.19.0` + +Shim overrides do not override vp install's selected version + +``` +vp uses the expected version +``` diff --git a/crates/vp_global_cli/src/commands/env/clean.rs b/crates/vp_global_cli/src/commands/env/clean.rs index bb79fdc6db..dda5fe65c1 100644 --- a/crates/vp_global_cli/src/commands/env/clean.rs +++ b/crates/vp_global_cli/src/commands/env/clean.rs @@ -56,6 +56,10 @@ async fn protected_package_manager( ) -> Result, Error> { let current = package_manager::resolve_current_or_fallback_for(cwd, kind).await?; let mut protected = vec![current.version.to_string()]; + // vp commands can select a different version from the family's direct shims. + if let Some(selected) = package_manager::resolve_current_for(cwd, Some(kind)).await? { + push_unique_version(&mut protected, selected.version.to_string()); + } let config = config::load_config().await?; if let Some((_, selector, _)) = package_manager::configured_default_for(&config, kind)? { let version = resolve_package_manager_version(kind, &selector).await?.to_string(); diff --git a/crates/vp_global_cli/src/commands/env/config.rs b/crates/vp_global_cli/src/commands/env/config.rs index 7df905e760..cdb370b428 100644 --- a/crates/vp_global_cli/src/commands/env/config.rs +++ b/crates/vp_global_cli/src/commands/env/config.rs @@ -240,22 +240,21 @@ pub async fn save_config(config: &Config) -> Result<(), Error> { /// Set by `vp env use` command. pub const VERSION_ENV_VAR: &str = vp_shared::env_vars::VP_NODE_VERSION; -/// Environment variable for the per-shell package-manager override. +/// Environment variable selecting the package manager for vp commands. pub const PACKAGE_MANAGER_ENV_VAR: &str = vp_shared::env_vars::VP_PACKAGE_MANAGER; /// Session version file name, written by `vp env use` so shims work without the shell eval wrapper. pub const SESSION_VERSION_FILE: &str = ".session-node-version"; -/// Package-manager session override file name. -pub const SESSION_PACKAGE_MANAGER_FILE: &str = ".session-package-manager"; - /// Get the path to the session version file (`/.session-node-version`). pub fn get_session_version_path() -> Result { Ok(vp_shared::EnvConfig::get().dirs.state.join(SESSION_VERSION_FILE)) } -pub fn get_session_package_manager_path() -> Result { - Ok(vp_shared::EnvConfig::get().dirs.state.join(SESSION_PACKAGE_MANAGER_FILE)) +pub fn get_session_package_manager_path( + kind: PackageManagerType, +) -> Result { + Ok(vp_shared::EnvConfig::get().dirs.state.join(format!(".session-{kind}-version"))) } /// Read the session version file. Returns `None` if the file is missing or empty. @@ -266,8 +265,8 @@ pub async fn read_session_version() -> Option { if trimmed.is_empty() { None } else { Some(trimmed) } } -pub async fn read_session_package_manager() -> Option { - let path = get_session_package_manager_path().ok()?; +pub async fn read_session_package_manager(kind: PackageManagerType) -> Option { + let path = get_session_package_manager_path(kind).ok()?; let content = tokio::fs::read_to_string(path).await.ok()?; let trimmed = content.trim().to_string(); if trimmed.is_empty() { None } else { Some(trimmed) } @@ -292,12 +291,15 @@ pub async fn write_session_version(version: &str) -> Result<(), Error> { Ok(()) } -pub async fn write_session_package_manager(spec: &str) -> Result<(), Error> { - let path = get_session_package_manager_path()?; +pub async fn write_session_package_manager( + kind: PackageManagerType, + version: &str, +) -> Result<(), Error> { + let path = get_session_package_manager_path(kind)?; if let Some(parent) = path.parent() { tokio::fs::create_dir_all(parent).await?; } - tokio::fs::write(path, spec).await?; + tokio::fs::write(path, version).await?; Ok(()) } @@ -311,8 +313,8 @@ pub async fn delete_session_version() -> Result<(), Error> { } } -pub async fn delete_session_package_manager() -> Result<(), Error> { - let path = get_session_package_manager_path()?; +pub async fn delete_session_package_manager(kind: PackageManagerType) -> Result<(), Error> { + let path = get_session_package_manager_path(kind)?; match tokio::fs::remove_file(path).await { Ok(()) => Ok(()), Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(()), @@ -1608,10 +1610,11 @@ mod tests { vp_shared::EnvConfig::with_vars_async( [(vp_shared::env_vars::VP_HOME, temp_dir.path())], |_| async { - write_session_package_manager("pnpm@10.18.0").await.unwrap(); - assert_eq!(read_session_package_manager().await.as_deref(), Some("pnpm@10.18.0")); - delete_session_package_manager().await.unwrap(); - assert!(read_session_package_manager().await.is_none()); + let kind = PackageManagerType::Pnpm; + write_session_package_manager(kind, "10.18.0").await.unwrap(); + assert_eq!(read_session_package_manager(kind).await.as_deref(), Some("10.18.0")); + delete_session_package_manager(kind).await.unwrap(); + assert!(read_session_package_manager(kind).await.is_none()); }, ) .await; diff --git a/crates/vp_global_cli/src/commands/env/current.rs b/crates/vp_global_cli/src/commands/env/current.rs index 10fc9875a5..a8e99d0237 100644 --- a/crates/vp_global_cli/src/commands/env/current.rs +++ b/crates/vp_global_cli/src/commands/env/current.rs @@ -161,9 +161,12 @@ async fn resolve_package_manager_info( scope: EnvScope, config: &config::Config, ) -> Result, Error> { - let selected = package_manager::resolve_current_spec(cwd).await?.filter(|resolution| { - scope.package_manager().is_none_or(|expected| expected == resolution.package_manager_type) - }); + // A concrete family resolves through its shim overrides before project metadata. + let selected = if scope.package_manager().is_some() { + None + } else { + package_manager::resolve_current_spec(cwd).await? + }; let selected_type = selected .as_ref() .map(|resolution| resolution.package_manager_type) @@ -184,6 +187,15 @@ async fn resolve_package_manager_info( if let Some(primary) = bin_paths.get(selected_type.to_string().as_str()) && let Some(primary) = AbsolutePathBuf::new(primary.into()) { + let selected = if scope.package_manager().is_some() { + // Project provenance is optional when the executable comes from PATH. + vp_pm_cli::resolve_environment_package_manager_spec(cwd, None, None) + .ok() + .flatten() + .filter(|resolution| resolution.package_manager_type == selected_type) + } else { + selected + }; return Ok(Some(PackageManagerInfo { name: selected_type.to_string(), version: read_tool_version(&primary).await.unwrap_or_else(|| "unknown".into()), diff --git a/crates/vp_global_cli/src/commands/env/doctor.rs b/crates/vp_global_cli/src/commands/env/doctor.rs index a332b60058..70673d5893 100644 --- a/crates/vp_global_cli/src/commands/env/doctor.rs +++ b/crates/vp_global_cli/src/commands/env/doctor.rs @@ -323,10 +323,12 @@ async fn check_shim_mode(scope: EnvScope) -> (config::Config, Option bool { - let selected = match package_manager::resolve_current_spec(cwd).await { - Ok(selected) => selected.filter(|resolution| { - scope - .package_manager() - .is_none_or(|expected| expected == resolution.package_manager_type) - }), - Err(error) => { - print_check(&output::CROSS.red().to_string(), "Package manager", &error.to_string()); - return false; + let selected_type = if let Some(kind) = scope.package_manager() { + Some(kind) + } else { + match package_manager::resolve_current_spec(cwd).await { + Ok(selected) => selected.map(|resolution| resolution.package_manager_type), + Err(error) => { + print_check( + &output::CROSS.red().to_string(), + "Package manager", + &error.to_string(), + ); + return false; + } } }; - let Some(selected) = selected else { + let Some(selected_type) = selected_type else { print_check(" ", "Package manager", "not selected"); return true; }; - if config.package_manager_shim_mode_for(selected.package_manager_type) == ShimMode::SystemFirst - && let Some(system_binary) = - shim::find_system_tool(&selected.package_manager_type.to_string()) + if config.package_manager_shim_mode_for(selected_type) == ShimMode::SystemFirst + && let Some(system_binary) = shim::find_system_tool(&selected_type.to_string()) { let Some(version) = try_get_tool_version(&system_binary).await else { print_check(" ", "Source", "system PATH"); @@ -370,7 +375,7 @@ async fn check_package_manager_resolution( print_check( " ", "Version", - &format!("{}@{version}", selected.package_manager_type).bright_green().to_string(), + &format!("{selected_type}@{version}").bright_green().to_string(), ); print_check( &output::CHECK.green().to_string(), @@ -380,7 +385,11 @@ async fn check_package_manager_resolution( return true; } - match package_manager::resolve_current_for(cwd, scope.package_manager()).await { + let resolution = match scope.package_manager() { + Some(kind) => package_manager::resolve_shim_for(cwd, kind).await, + None => package_manager::resolve_current(cwd).await, + }; + match resolution { Ok(Some(resolution)) => { print_check(" ", "Source", &resolution.source); print_check( diff --git a/crates/vp_global_cli/src/commands/env/exec.rs b/crates/vp_global_cli/src/commands/env/exec.rs index b35dbad72c..38c32f774c 100644 --- a/crates/vp_global_cli/src/commands/env/exec.rs +++ b/crates/vp_global_cli/src/commands/env/exec.rs @@ -226,6 +226,11 @@ async fn execute_with_version( let mut child = tokio::process::Command::new(cmd); child.args(args).envs(child_env.into_envs()).env(env_vars::VP_NODE_VERSION, &resolved_node); if let Some(package_manager) = resolved_package_manager { + if explicit_package_manager { + // Preserve explicit versions when a child removes the injected tool directory from PATH. + let (kind, version, _) = parse_package_manager_spec_with_hash(&package_manager)?; + child.env(package_manager_resolution::version_env_var(kind), version); + } child.env(env_vars::VP_PACKAGE_MANAGER, package_manager); } // The child runs in the inherited cwd, which a leading `-C ` changes diff --git a/crates/vp_global_cli/src/commands/env/list_remote.rs b/crates/vp_global_cli/src/commands/env/list_remote.rs index 2f0f0e693a..9758d953c2 100644 --- a/crates/vp_global_cli/src/commands/env/list_remote.rs +++ b/crates/vp_global_cli/src/commands/env/list_remote.rs @@ -90,7 +90,10 @@ pub async fn execute( None }; let current_pm = if scope.includes_package_managers() { - package_manager::resolve_current_for(&cwd, scope.package_manager()).await? + match scope.package_manager() { + Some(kind) => package_manager::resolve_shim_for(&cwd, kind).await?, + None => package_manager::resolve_current(&cwd).await?, + } } else { None }; diff --git a/crates/vp_global_cli/src/commands/env/package_manager.rs b/crates/vp_global_cli/src/commands/env/package_manager.rs index dfd823ef1a..f7bfa7251b 100644 --- a/crates/vp_global_cli/src/commands/env/package_manager.rs +++ b/crates/vp_global_cli/src/commands/env/package_manager.rs @@ -2,7 +2,7 @@ use vp_pm_cli::{ EnvironmentPackageManagerResolution, PackageManagerType, resolve_environment_package_manager, resolve_environment_package_manager_spec, resolve_package_manager_version, }; -use vt_path::{AbsolutePath, AbsolutePathBuf}; +use vt_path::AbsolutePath; use super::{config, spec::parse_package_manager_spec_with_hash}; use crate::error::Error; @@ -13,22 +13,63 @@ pub(crate) async fn resolve_current( resolve_current_for(cwd, None).await } -/// Selecting a manager for vp commands must not change direct shim versions. +/// Direct shims have independent overrides; selecting a manager for vp commands must not change them. pub(crate) async fn resolve_shim_for( cwd: &AbsolutePath, expected: PackageManagerType, ) -> Result, Error> { - let session = config::read_session_package_manager().await; - let session = session.as_deref().map(parse_package_manager_spec_with_hash).transpose()?; + let (version, source, source_path) = if let Some(version) = environment_version(expected) { + (Some(version), version_env_var(expected).to_string(), None) + } else { + ( + config::read_session_package_manager(expected).await, + format!(".session-{expected}-version"), + config::get_session_package_manager_path(expected).ok(), + ) + }; + let override_spec = version + .map(|version| parse_package_manager_spec_with_hash(&format!("{expected}@{version}"))) + .transpose()?; let default = configured_default_for(&config::load_config().await?, expected)?; - resolve_environment_package_manager( + let mut resolution = resolve_environment_package_manager( cwd, - session.as_ref().map(|(kind, version, hash)| (*kind, version.as_str(), hash.as_deref())), + override_spec + .as_ref() + .map(|(kind, version, hash)| (*kind, version.as_str(), hash.as_deref())), default.as_ref().map(|(kind, version, hash)| (*kind, version.as_str(), hash.as_deref())), Some(expected), ) - .await - .map_err(Error::from) + .await?; + if override_spec.is_some() + && let Some(resolution) = &mut resolution + { + resolution.source = source.into(); + resolution.source_path = source_path; + } + Ok(resolution) +} + +pub(crate) fn version_env_var(kind: PackageManagerType) -> &'static str { + use vp_shared::env_vars; + match kind { + PackageManagerType::Npm => env_vars::VP_NPM_VERSION, + PackageManagerType::Pnpm => env_vars::VP_PNPM_VERSION, + PackageManagerType::Yarn => env_vars::VP_YARN_VERSION, + PackageManagerType::Bun => env_vars::VP_BUN_VERSION, + } +} + +pub(crate) fn environment_version(kind: PackageManagerType) -> Option { + let env = vp_shared::EnvConfig::get(); + match kind { + PackageManagerType::Npm => env.npm_version.as_deref(), + PackageManagerType::Pnpm => env.pnpm_version.as_deref(), + PackageManagerType::Yarn => env.yarn_version.as_deref(), + PackageManagerType::Bun => env.bun_version.as_deref(), + } + .map(str::trim) + .filter(|version| !version.is_empty()) + .map(str::to_string) } pub(crate) async fn resolve_current_for( @@ -38,12 +79,12 @@ pub(crate) async fn resolve_current_for( let specs = current_specs(expected).await?; let mut resolution = resolve_environment_package_manager( cwd, - specs.session_spec(), + specs.override_spec(), specs.default_spec(), expected, ) .await?; - specs.apply_session_source(&mut resolution); + specs.apply_override_source(&mut resolution); Ok(resolution) } @@ -51,7 +92,7 @@ pub(crate) async fn resolve_current_or_fallback_for( cwd: &AbsolutePath, package_manager: PackageManagerType, ) -> Result { - if let Some(resolution) = resolve_current_for(cwd, Some(package_manager)).await? { + if let Some(resolution) = resolve_shim_for(cwd, package_manager).await? { return Ok(resolution); } @@ -64,24 +105,22 @@ pub(crate) async fn resolve_current_spec( let specs = current_specs(None).await?; let mut resolution = - resolve_environment_package_manager_spec(cwd, specs.session_spec(), specs.default_spec()) + resolve_environment_package_manager_spec(cwd, specs.override_spec(), specs.default_spec()) .map_err(Error::from)?; - specs.apply_session_source(&mut resolution); + specs.apply_override_source(&mut resolution); Ok(resolution) } pub(crate) type PackageManagerSpec = (PackageManagerType, String, Option); struct CurrentSpecs { - session: Option, - session_source: Option<&'static str>, - session_source_path: Option, + selected: Option, default: Option, } impl CurrentSpecs { - fn session_spec(&self) -> Option<(PackageManagerType, &str, Option<&str>)> { - self.session + fn override_spec(&self) -> Option<(PackageManagerType, &str, Option<&str>)> { + self.selected .as_ref() .map(|(kind, version, hash)| (*kind, version.as_str(), hash.as_deref())) } @@ -92,39 +131,31 @@ impl CurrentSpecs { .map(|(kind, version, hash)| (*kind, version.as_str(), hash.as_deref())) } - fn apply_session_source(&self, resolution: &mut Option) { - if let (Some(resolution), Some(source)) = (resolution, self.session_source) { - resolution.source = source.into(); - resolution.source_path.clone_from(&self.session_source_path); + fn apply_override_source(&self, resolution: &mut Option) { + if self.selected.is_some() + && let Some(resolution) = resolution + { + resolution.source = config::PACKAGE_MANAGER_ENV_VAR.into(); + resolution.source_path = None; } } } async fn current_specs(expected: Option) -> Result { - let config = vp_shared::EnvConfig::get(); - let (session, session_source, session_source_path) = if let Some(spec) = - config.package_manager.as_deref().map(str::trim).filter(|spec| !spec.is_empty()) - { - ( - Some(parse_package_manager_spec_with_hash(spec)?), - Some(config::PACKAGE_MANAGER_ENV_VAR), - None, - ) - } else if let Some(spec) = config::read_session_package_manager().await { - ( - Some(parse_package_manager_spec_with_hash(spec.trim())?), - Some(config::SESSION_PACKAGE_MANAGER_FILE), - config::get_session_package_manager_path().ok(), - ) - } else { - (None, None, None) - }; + let env = vp_shared::EnvConfig::get(); + let selected = env + .package_manager + .as_deref() + .map(str::trim) + .filter(|spec| !spec.is_empty()) + .map(parse_package_manager_spec_with_hash) + .transpose()?; let config = config::load_config().await?; let default = expected .map(|package_manager| configured_default_for(&config, package_manager)) .transpose()? .flatten(); - Ok(CurrentSpecs { session, session_source, session_source_path, default }) + Ok(CurrentSpecs { selected, default }) } pub(crate) fn configured_default_for( diff --git a/crates/vp_global_cli/src/commands/env/spec.rs b/crates/vp_global_cli/src/commands/env/spec.rs index 6fe97f67f7..bf5c679634 100644 --- a/crates/vp_global_cli/src/commands/env/spec.rs +++ b/crates/vp_global_cli/src/commands/env/spec.rs @@ -92,13 +92,6 @@ impl EnvSpecs { } } -pub(crate) fn parse_package_manager_spec( - value: &str, -) -> Result<(PackageManagerType, String), Error> { - let (package_manager, version, _) = parse_package_manager_spec_with_hash(value)?; - Ok((package_manager, version)) -} - pub(crate) fn parse_package_manager_spec_with_hash( value: &str, ) -> Result<(PackageManagerType, String, Option), Error> { diff --git a/crates/vp_global_cli/src/commands/env/use.rs b/crates/vp_global_cli/src/commands/env/use.rs index bf6d0aa565..bb7160c7bb 100644 --- a/crates/vp_global_cli/src/commands/env/use.rs +++ b/crates/vp_global_cli/src/commands/env/use.rs @@ -14,7 +14,7 @@ use vp_pm_cli::{PackageManagerType, download_package_manager, resolve_package_ma use vt_path::AbsolutePathBuf; use super::{ - config::{self, PACKAGE_MANAGER_ENV_VAR, VERSION_ENV_VAR}, + config::{self, VERSION_ENV_VAR}, exit_status, package_manager, spec::{EnvScope, EnvSpecs}, }; @@ -71,12 +71,8 @@ fn print_windows_eval_wrapper_required() { eprintln!("Then dot-source it now (or open a new PowerShell session) to load the wrapper."); } -fn package_manager_spec( - package_manager: PackageManagerType, - version: &str, - hash: Option<&str>, -) -> Result { - let mut spec = format!("{package_manager}@{version}"); +fn package_manager_spec(version: &str, hash: Option<&str>) -> Result { + let mut spec = version.to_string(); if let Some(hash) = hash { if hash.is_empty() || !hash.bytes().all(|byte| { @@ -110,27 +106,18 @@ pub async fn execute( // Always delete the session file: on Windows it lives under VP_HOME and can // leak across shell windows, so even eval mode must clean it up. if unset { - let session_package_manager = config::read_session_package_manager().await; - let environment_package_manager = vp_shared::EnvConfig::get().package_manager.clone(); - let (delete_session_package_manager, unset_environment_package_manager) = match scope { - EnvScope::PackageManager(expected) => ( - package_manager_matches(session_package_manager.as_deref(), expected), - package_manager_matches(environment_package_manager.as_deref(), expected), - ), - _ => (scope.includes_package_managers(), scope.includes_package_managers()), - }; if scope.includes_node() { config::delete_session_version().await?; } - if delete_session_package_manager { - config::delete_session_package_manager().await?; + for kind in package_manager::selected(scope) { + config::delete_session_package_manager(kind).await?; } if has_eval_wrapper() { if scope.includes_node() { println!("{}", format_unset(&shell, VERSION_ENV_VAR)); } - if unset_environment_package_manager { - println!("{}", format_unset(&shell, PACKAGE_MANAGER_ENV_VAR)); + for kind in package_manager::selected(scope) { + println!("{}", format_unset(&shell, package_manager::version_env_var(kind))); } } else if !can_use_session_file() { print_windows_eval_wrapper_required(); @@ -198,20 +185,24 @@ pub async fn execute( }; let package_manager_unchanged = match &package_manager { Some((kind, version, _, hash)) => { - let spec = package_manager_spec(*kind, version, hash.as_deref())?; + let spec = package_manager_spec(version, hash.as_deref())?; current_override( - config::read_session_package_manager().await, - vp_shared::EnvConfig::get().package_manager.clone(), + config::read_session_package_manager(*kind).await, + package_manager::environment_version(*kind), ) .as_deref() == Some(spec.as_str()) } None if uses_project_environment && scope.includes_package_managers() => { - current_override( - config::read_session_package_manager().await, - vp_shared::EnvConfig::get().package_manager.clone(), - ) - .is_none() + let mut unchanged = true; + for kind in package_manager::selected(scope) { + unchanged &= current_override( + config::read_session_package_manager(kind).await, + package_manager::environment_version(kind), + ) + .is_none(); + } + unchanged } None => true, }; @@ -227,8 +218,8 @@ pub async fn execute( if scope.includes_node() { config::delete_session_version().await?; } - if scope.includes_package_managers() { - config::delete_session_package_manager().await?; + for kind in package_manager::selected(scope) { + config::delete_session_package_manager(kind).await?; } eprintln!("Reverted selected components to project environment resolution"); print_windows_eval_wrapper_required(); @@ -245,18 +236,20 @@ pub async fn execute( println!("{}", format_export(&shell, VERSION_ENV_VAR, version)); } if let Some((kind, version, _, hash)) = &package_manager { - config::delete_session_package_manager().await?; + config::delete_session_package_manager(*kind).await?; println!( "{}", format_export( &shell, - PACKAGE_MANAGER_ENV_VAR, - &package_manager_spec(*kind, version, hash.as_deref())? + package_manager::version_env_var(*kind), + &package_manager_spec(version, hash.as_deref())? ) ); } else if uses_project_environment && scope.includes_package_managers() { - config::delete_session_package_manager().await?; - println!("{}", format_unset(&shell, PACKAGE_MANAGER_ENV_VAR)); + for kind in package_manager::selected(scope) { + config::delete_session_package_manager(kind).await?; + println!("{}", format_unset(&shell, package_manager::version_env_var(kind))); + } } } else if !can_use_session_file() { print_windows_eval_wrapper_required(); @@ -267,14 +260,15 @@ pub async fn execute( config::write_session_version(version).await?; } if let Some((kind, version, _, hash)) = &package_manager { - config::write_session_package_manager(&package_manager_spec( + config::write_session_package_manager( *kind, - version, - hash.as_deref(), - )?) + &package_manager_spec(version, hash.as_deref())?, + ) .await?; } else if uses_project_environment && scope.includes_package_managers() { - config::delete_session_package_manager().await?; + for kind in package_manager::selected(scope) { + config::delete_session_package_manager(kind).await?; + } } } @@ -288,14 +282,6 @@ pub async fn execute( Ok(ExitStatus::default()) } -fn package_manager_matches(value: Option<&str>, expected: PackageManagerType) -> bool { - value - .map(str::trim) - .filter(|value| !value.is_empty()) - .and_then(|value| super::spec::parse_package_manager_spec(value).ok()) - .is_some_and(|(kind, _)| kind == expected) -} - async fn ensure_components_installed( node: &Option<(String, String)>, package_manager: &Option<(PackageManagerType, String, String, Option)>, @@ -457,12 +443,8 @@ mod tests { #[test] fn package_manager_spec_rejects_shell_metacharacters() { - let error = package_manager_spec( - PackageManagerType::Pnpm, - "10.18.0", - Some("sha512.valid; touch injected"), - ) - .unwrap_err(); + let error = + package_manager_spec("10.18.0", Some("sha512.valid; touch injected")).unwrap_err(); assert!(error.to_string().contains("invalid package-manager integrity suffix")); } diff --git a/crates/vp_global_cli/src/commands/env/which.rs b/crates/vp_global_cli/src/commands/env/which.rs index 9aa3c894dc..444764ebf1 100644 --- a/crates/vp_global_cli/src/commands/env/which.rs +++ b/crates/vp_global_cli/src/commands/env/which.rs @@ -151,7 +151,7 @@ async fn execute_package_manager_tool( let Some(expected_type) = PackageManagerType::from_tool(tool) else { return Ok(None); }; - let resolution = package_manager::resolve_current_for(cwd, Some(expected_type)).await?; + let resolution = package_manager::resolve_shim_for(cwd, expected_type).await?; let (version, source) = match &resolution { Some(resolution) => ( resolution.version.to_string(), diff --git a/crates/vp_shared/src/env_config.rs b/crates/vp_shared/src/env_config.rs index af2f23d434..103b26c041 100644 --- a/crates/vp_shared/src/env_config.rs +++ b/crates/vp_shared/src/env_config.rs @@ -162,6 +162,12 @@ pub struct EnvConfig { /// Env: `VP_PACKAGE_MANAGER` pub package_manager: Option, + /// Direct shim version overrides, independent of the selected package manager. + pub npm_version: Option, + pub pnpm_version: Option, + pub yarn_version: Option, + pub bun_version: Option, + /// User home directory. /// /// Resolved once from `HOME` or `USERPROFILE` in platform order. See @@ -208,6 +214,10 @@ impl EnvConfig { env_use_eval_enable: std::env::var(env_vars::VP_ENV_USE_EVAL_ENABLE).is_ok(), node_version: std::env::var(env_vars::VP_NODE_VERSION).ok(), package_manager: std::env::var(env_vars::VP_PACKAGE_MANAGER).ok(), + npm_version: std::env::var(env_vars::VP_NPM_VERSION).ok(), + pnpm_version: std::env::var(env_vars::VP_PNPM_VERSION).ok(), + yarn_version: std::env::var(env_vars::VP_YARN_VERSION).ok(), + bun_version: std::env::var(env_vars::VP_BUN_VERSION).ok(), user_home, vp_shell: std::env::var(env_vars::VP_SHELL).ok(), }) diff --git a/crates/vp_shared/src/env_vars.rs b/crates/vp_shared/src/env_vars.rs index 8c11c3c065..b21971e23a 100644 --- a/crates/vp_shared/src/env_vars.rs +++ b/crates/vp_shared/src/env_vars.rs @@ -85,9 +85,22 @@ pub const VP_NODE_SKIP_SIGNATURE_VERIFY: &str = "VP_NODE_SKIP_SIGNATURE_VERIFY"; /// Override Node.js version (takes highest priority in version resolution). pub const VP_NODE_VERSION: &str = "VP_NODE_VERSION"; -/// Override package manager and version (for example, `pnpm@10.18.0`). +/// Override package manager and version for vp commands (for example, `pnpm@10.18.0`). +/// Direct package-manager shims use their own version overrides instead. pub const VP_PACKAGE_MANAGER: &str = "VP_PACKAGE_MANAGER"; +/// Override the npm and npx shim version. +pub const VP_NPM_VERSION: &str = "VP_NPM_VERSION"; + +/// Override the pnpm and pnpx shim version. +pub const VP_PNPM_VERSION: &str = "VP_PNPM_VERSION"; + +/// Override the yarn and yarnpkg shim version. +pub const VP_YARN_VERSION: &str = "VP_YARN_VERSION"; + +/// Override the bun and bunx shim version. +pub const VP_BUN_VERSION: &str = "VP_BUN_VERSION"; + /// Enable debug output for shim dispatch. pub const VP_DEBUG_SHIM: &str = "VP_DEBUG_SHIM"; diff --git a/docs/guide/env.md b/docs/guide/env.md index f027b30204..e2190b6e86 100644 --- a/docs/guide/env.md +++ b/docs/guide/env.md @@ -47,16 +47,36 @@ This setting also disables pnpm's automatic management of other declared runtime Package-manager selection uses this priority: 1. Explicit command override -2. `VP_PACKAGE_MANAGER` or the shell-session override +2. `VP_PACKAGE_MANAGER` 3. Top-level `packageManager` 4. `devEngines.packageManager` 5. Lockfile or manager-specific configuration 6. The named package manager's global default version 7. The named shim's latest release -`VP_PACKAGE_MANAGER` selects the manager and version for commands such as `vp install`. Direct package-manager shims ignore this variable and continue to resolve their versions from the session file, project configuration, and family default. +`VP_PACKAGE_MANAGER` selects the manager and version for commands such as `vp install`. Direct package-manager shims ignore this variable and use independent version overrides: -A project selection controls only its named shims. For example, pnpm controls `pnpm` and `pnpx`; invoking `npm` still resolves npm independently. Alias pairs are `npm`/`npx`, `pnpm`/`pnpx`, `yarn`/`yarnpkg`, and `bun`/`bunx`. Without a matching project selection, a named shim uses its configured default version and otherwise uses the latest release without prompting. The resolved version is cached for one hour and an expired cache remains available when the registry cannot be reached. The directly invoked npm shim keeps its Node-bundled fallback, while an explicit `vp env ... npm` family scope uses standalone npm's latest release. +| Variable | Shims | +| ----------------- | ----------------- | +| `VP_NPM_VERSION` | `npm`, `npx` | +| `VP_PNPM_VERSION` | `pnpm`, `pnpx` | +| `VP_YARN_VERSION` | `yarn`, `yarnpkg` | +| `VP_BUN_VERSION` | `bun`, `bunx` | + +These variables accept a version or range, such as `10.18.0`, `10`, or `latest`, and override the matching shim's project and default versions. They do not change the manager or version selected by `vp install`. + +`vp env use pnpm@10.20.0` sets `VP_PNPM_VERSION` for the current shell, just as `vp env use node@22` sets `VP_NODE_VERSION`. Each package manager has its own override, so switching Yarn does not clear a pnpm override. `vp env use` does not set or clear `VP_PACKAGE_MANAGER`. + +Direct shims resolve their version from the matching environment variable, then the matching session file when no shell wrapper is available, then project configuration and the family default. `vp env current pnpm` and `vp env which pnpm` inspect this shim selection; `vp env current pm` reports the manager selected for vp commands. + +```bash +VP_PACKAGE_MANAGER=pnpm@10.18.0 vp install +VP_PNPM_VERSION=10.20.0 pnpm --version +``` + +The overrides apply in managed mode. A package manager can also perform its own version switching after Vite+ launches it; for example, pnpm's `managePackageManagerVersions` setting may switch back to the version in `package.json`. + +A project selection applies only to its matching shims. For example, pnpm controls `pnpm` and `pnpx`; invoking `npm` still resolves npm independently. Without a matching project selection, a named shim uses its configured default version and otherwise uses the latest release without prompting. The resolved version is cached for one hour and an expired cache remains available when the registry cannot be reached. The directly invoked npm shim keeps its Node-bundled fallback, while an explicit `vp env ... npm` family scope uses standalone npm's latest release. A fresh install uses the split platform layout by default. On Unix, Vite+ stores managed runtimes and related files in `~/.local/share/vite-plus`. It @@ -132,7 +152,8 @@ vp-use --unset Only `vp env use` needs this alternate command. Other `vp env` commands work normally in Command Prompt. `vp env setup` creates `vp-use.cmd` in the bin directory on Windows. In CI, `vp env use` can run without shell initialization. It writes a temporary -Node.js or package-manager session file in the resolved state directory. Later +session file per runtime or package manager in the resolved state directory, +such as `.session-node-version` or `.session-pnpm-version`. Later shim calls in the same job use these files to resolve the same environment. ### Manage @@ -177,7 +198,8 @@ vp env install # Install the complete resolved environment vp env default node@24 # Set the global Node.js default vp env default pnpm@10 # Set pnpm's global default version vp env use 20 pnpm@10 # Override both components for this shell -vp env use --unset pm # Remove only the PM session override +vp env use --unset pnpm # Remove only the pnpm session version +vp env use --unset pm # Remove all package-manager session versions vp env clean # Remove unused managed Node.js and package manager versions # Inspect