From 77bedde16f3f58b3ad081591c7f5d62800f0f958 Mon Sep 17 00:00:00 2001 From: Liang Date: Sun, 16 Aug 2026 21:12:04 +0800 Subject: [PATCH 1/5] feat(env): disable pnpm runtime management in managed mode --- .../assert-env-files.mjs | 29 +++++++ .../assert-posix-wrapper.mjs | 16 ++++ .../command_env_pnpm_runtime/snapshots.toml | 20 +++++ .../snapshots/command_env_pnpm_runtime.md | 47 +++++++++++ .../command_env_pnpm_runtime_current_shell.md | 9 ++ .../vp_global_cli/src/commands/env/doctor.rs | 48 ++++++++++- crates/vp_global_cli/src/commands/env/mod.rs | 2 + crates/vp_global_cli/src/commands/env/off.rs | 7 +- crates/vp_global_cli/src/commands/env/on.rs | 7 +- .../vp_global_cli/src/commands/env/setup.rs | 82 +++++++++++++++---- docs/guide/env.md | 6 +- 11 files changed, 250 insertions(+), 23 deletions(-) create mode 100644 crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_env_pnpm_runtime/assert-env-files.mjs create mode 100644 crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_env_pnpm_runtime/assert-posix-wrapper.mjs create mode 100644 crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_env_pnpm_runtime/snapshots.toml create mode 100644 crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_env_pnpm_runtime/snapshots/command_env_pnpm_runtime.md create mode 100644 crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_env_pnpm_runtime/snapshots/command_env_pnpm_runtime_current_shell.md diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_env_pnpm_runtime/assert-env-files.mjs b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_env_pnpm_runtime/assert-env-files.mjs new file mode 100644 index 0000000000..fb0b27b87d --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_env_pnpm_runtime/assert-env-files.mjs @@ -0,0 +1,29 @@ +import { readFileSync } from 'node:fs' +import { join } from 'node:path' + +const mode = process.argv[2] +const expected = { + managed: { + env: 'export PNPM_CONFIG_RUNTIME=false', + 'env.fish': 'set -gx PNPM_CONFIG_RUNTIME false', + 'env.nu': '$env.PNPM_CONFIG_RUNTIME = "false"', + 'env.ps1': '$env:PNPM_CONFIG_RUNTIME = "false"', + }, + 'system-first': { + env: 'unset PNPM_CONFIG_RUNTIME', + 'env.fish': 'set -e PNPM_CONFIG_RUNTIME', + 'env.nu': 'if ("PNPM_CONFIG_RUNTIME" in $env) { hide-env PNPM_CONFIG_RUNTIME }', + 'env.ps1': 'Remove-Item Env:\\PNPM_CONFIG_RUNTIME -ErrorAction SilentlyContinue', + }, +}[mode] + +if (!expected) + throw new Error(`Unknown mode: ${mode}`) + +for (const [file, line] of Object.entries(expected)) { + const setup = readFileSync(join(process.env.VP_HOME, file), 'utf8').split('\n').slice(0, 6) + if (!setup.includes(line)) + throw new Error(`${file} does not configure pnpm for ${mode} mode`) +} + +console.log(`All shell environments match ${mode} mode`) diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_env_pnpm_runtime/assert-posix-wrapper.mjs b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_env_pnpm_runtime/assert-posix-wrapper.mjs new file mode 100644 index 0000000000..aa976a2bdd --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_env_pnpm_runtime/assert-posix-wrapper.mjs @@ -0,0 +1,16 @@ +import { spawnSync } from 'node:child_process' + +const script = [ + '. "$VP_HOME/env"', + '[ "$PNPM_CONFIG_RUNTIME" = false ] || exit 1', + 'vp env off >/dev/null || exit 1', + '[ -z "${PNPM_CONFIG_RUNTIME+x}" ] || exit 1', + 'vp env on >/dev/null || exit 1', + '[ "$PNPM_CONFIG_RUNTIME" = false ] || exit 1', +].join('\n') +const result = spawnSync('/bin/sh', ['-c', script], { env: process.env, encoding: 'utf8' }) + +if (result.status !== 0) + throw new Error(result.stderr || `shell exited with ${result.status}`) + +console.log('Current shell follows Vite+ environment mode') diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_env_pnpm_runtime/snapshots.toml b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_env_pnpm_runtime/snapshots.toml new file mode 100644 index 0000000000..3950fadc79 --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_env_pnpm_runtime/snapshots.toml @@ -0,0 +1,20 @@ +[[case]] +name = "command_env_pnpm_runtime" +vp = "global" +steps = [ + { argv = ["node", "assert-env-files.mjs", "managed"], comment = "managed mode disables pnpm runtime management in every generated shell environment" }, + { argv = ["node", "-e", "const {execFileSync}=require('node:child_process');const text=execFileSync('vp',['env','doctor'],{encoding:'utf8',env:{...process.env,PNPM_CONFIG_RUNTIME:'false'}}).replace(/\\u001b\\[[0-9;]*m/g,'');const line=text.split('\\n').find(line=>line.includes('pnpm runtime'));if(!line)process.exit(1);console.log(line.trim());"], comment = "doctor reports the managed-mode pnpm runtime setting" }, + { argv = ["vp", "env", "off"], snapshot = false }, + { argv = ["node", "assert-env-files.mjs", "system-first"], comment = "system-first mode removes the setting from every generated shell environment" }, + { argv = ["node", "-e", "const {execFileSync}=require('node:child_process');const text=execFileSync('vp',['env','doctor'],{encoding:'utf8'}).replace(/\\u001b\\[[0-9;]*m/g,'');const line=text.split('\\n').find(line=>line.includes('pnpm runtime'));if(!line)process.exit(1);console.log(line.trim());"], comment = "doctor reports the system-first pnpm runtime setting" }, + { argv = ["vp", "env", "on"], snapshot = false }, + { argv = ["node", "assert-env-files.mjs", "managed"], comment = "managed mode restores the setting" }, +] + +[[case]] +name = "command_env_pnpm_runtime_current_shell" +vp = "global" +skip-platforms = ["windows"] +steps = [ + { argv = ["node", "assert-posix-wrapper.mjs"], comment = "vp env on and off update the current shell" }, +] diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_env_pnpm_runtime/snapshots/command_env_pnpm_runtime.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_env_pnpm_runtime/snapshots/command_env_pnpm_runtime.md new file mode 100644 index 0000000000..563dfd6d77 --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_env_pnpm_runtime/snapshots/command_env_pnpm_runtime.md @@ -0,0 +1,47 @@ +# command_env_pnpm_runtime + +## `node assert-env-files.mjs managed` + +managed mode disables pnpm runtime management in every generated shell environment + +``` +All shell environments match managed mode +``` + +## `node -e 'const {execFileSync}=require('\''node:child_process'\'');const text=execFileSync('\''vp'\'',['\''env'\'','\''doctor'\''],{encoding:'\''utf8'\'',env:{...process.env,PNPM_CONFIG_RUNTIME:'\''false'\''}}).replace(/\u001b\[[0-9;]*m/g,'\'''\'');const line=text.split('\''\n'\'').find(line=>line.includes('\''pnpm runtime'\''));if('\!'line)process.exit(1);console.log(line.trim());'` + +doctor reports the managed-mode pnpm runtime setting + +``` +✓ pnpm runtime PNPM_CONFIG_RUNTIME=false +``` + +## `vp env off` + + +## `node assert-env-files.mjs system-first` + +system-first mode removes the setting from every generated shell environment + +``` +All shell environments match system-first mode +``` + +## `node -e 'const {execFileSync}=require('\''node:child_process'\'');const text=execFileSync('\''vp'\'',['\''env'\'','\''doctor'\''],{encoding:'\''utf8'\''}).replace(/\u001b\[[0-9;]*m/g,'\'''\'');const line=text.split('\''\n'\'').find(line=>line.includes('\''pnpm runtime'\''));if('\!'line)process.exit(1);console.log(line.trim());'` + +doctor reports the system-first pnpm runtime setting + +``` +✓ pnpm runtime PNPM_CONFIG_RUNTIME unset +``` + +## `vp env on` + + +## `node assert-env-files.mjs managed` + +managed mode restores the setting + +``` +All shell environments match managed mode +``` diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_env_pnpm_runtime/snapshots/command_env_pnpm_runtime_current_shell.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_env_pnpm_runtime/snapshots/command_env_pnpm_runtime_current_shell.md new file mode 100644 index 0000000000..adb3177ce1 --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_env_pnpm_runtime/snapshots/command_env_pnpm_runtime_current_shell.md @@ -0,0 +1,9 @@ +# command_env_pnpm_runtime_current_shell + +## `node assert-posix-wrapper.mjs` + +vp env on and off update the current shell + +``` +Current shell follows Vite+ environment mode +``` diff --git a/crates/vp_global_cli/src/commands/env/doctor.rs b/crates/vp_global_cli/src/commands/env/doctor.rs index 6ce79f0473..004e8e9d3b 100644 --- a/crates/vp_global_cli/src/commands/env/doctor.rs +++ b/crates/vp_global_cli/src/commands/env/doctor.rs @@ -6,7 +6,11 @@ use owo_colors::OwoColorize; use vp_shared::{env_vars, output}; use vt_path::{AbsolutePathBuf, current_dir}; -use super::config::{self, ShimMode, get_bin_dir, get_vp_home, load_config, resolve_version}; +use super::{ + PNPM_CONFIG_RUNTIME, + config::{self, ShimMode, get_bin_dir, get_vp_home, load_config, resolve_version}, + setup::SHIM_TOOLS, +}; use crate::{ commands::shell::{ALL_SHELL_PROFILES, IDE_SHELL_PROFILES, ShellProfile, resolve_profile_path}, error::Error, @@ -33,8 +37,6 @@ const KNOWN_VERSION_MANAGERS: &[(&str, &str)] = &[ ("n", "N_PREFIX"), ]; -use super::setup::SHIM_TOOLS; - /// Column width for left-side keys in aligned output const KEY_WIDTH: usize = 18; @@ -85,6 +87,7 @@ pub async fn execute(cwd: AbsolutePathBuf) -> Result { // Section: Configuration print_section("Configuration"); let (shim_mode, system_node_path) = check_shim_mode().await; + check_pnpm_runtime(shim_mode); // Check env sourcing: IDE-relevant profiles first, then all shell profiles let env_status = cfg!(not(windows)).then(check_env_sourcing); @@ -260,6 +263,45 @@ async fn check_shim_mode() -> (ShimMode, Option) { (config.shim_mode, system_node_path) } +fn check_pnpm_runtime(shim_mode: ShimMode) { + let value = + std::env::var_os(PNPM_CONFIG_RUNTIME).map(|value| value.to_string_lossy().into_owned()); + + match (shim_mode, value.as_deref()) { + (ShimMode::Managed, Some(value)) if value.eq_ignore_ascii_case("false") => { + print_check( + &output::CHECK.green().to_string(), + "pnpm runtime", + &format!("{PNPM_CONFIG_RUNTIME}={value}"), + ); + } + (ShimMode::SystemFirst, None) => { + print_check( + &output::CHECK.green().to_string(), + "pnpm runtime", + &format!("{PNPM_CONFIG_RUNTIME} unset"), + ); + } + (ShimMode::Managed, value) => { + let value = value.unwrap_or("unset"); + print_check( + &output::WARN_SIGN.yellow().to_string(), + "pnpm runtime", + &format!("{PNPM_CONFIG_RUNTIME}={value} (expected false)").yellow().to_string(), + ); + print_hint("Re-source the Vite+ environment file or restart your shell."); + } + (ShimMode::SystemFirst, Some(value)) => { + print_check( + &output::WARN_SIGN.yellow().to_string(), + "pnpm runtime", + &format!("{PNPM_CONFIG_RUNTIME}={value} (expected unset)").yellow().to_string(), + ); + print_hint("Re-source the Vite+ environment file or restart your shell."); + } + } +} + /// Check profile files for env sourcing and classify where it was found. /// /// Tries IDE-relevant profiles first, then falls back to all shell profiles. diff --git a/crates/vp_global_cli/src/commands/env/mod.rs b/crates/vp_global_cli/src/commands/env/mod.rs index bae8bccd8c..057e7c960e 100644 --- a/crates/vp_global_cli/src/commands/env/mod.rs +++ b/crates/vp_global_cli/src/commands/env/mod.rs @@ -33,6 +33,8 @@ use crate::{ error::Error, }; +const PNPM_CONFIG_RUNTIME: &str = "PNPM_CONFIG_RUNTIME"; + fn print_env_header() { vp_shared::header::print_header(); } diff --git a/crates/vp_global_cli/src/commands/env/off.rs b/crates/vp_global_cli/src/commands/env/off.rs index 461fdef0bf..b237a65e45 100644 --- a/crates/vp_global_cli/src/commands/env/off.rs +++ b/crates/vp_global_cli/src/commands/env/off.rs @@ -5,7 +5,10 @@ use std::process::ExitStatus; -use super::config::{ShimMode, load_config, save_config}; +use super::{ + config::{ShimMode, load_config, save_config}, + setup, +}; use crate::{error::Error, help}; /// Execute the `vp env off` command. @@ -13,6 +16,7 @@ pub async fn execute() -> Result { let mut config = load_config().await?; if config.shim_mode == ShimMode::SystemFirst { + setup::refresh_env_files().await?; println!("Node.js management is already set to system-first."); println!( "All vp commands and shims will prefer system Node.js, falling back to managed if not found." @@ -22,6 +26,7 @@ pub async fn execute() -> Result { config.shim_mode = ShimMode::SystemFirst; save_config(&config).await?; + setup::refresh_env_files().await?; println!("\u{2713} Node.js management set to system-first."); println!(); diff --git a/crates/vp_global_cli/src/commands/env/on.rs b/crates/vp_global_cli/src/commands/env/on.rs index 6ded635215..22f29a1f70 100644 --- a/crates/vp_global_cli/src/commands/env/on.rs +++ b/crates/vp_global_cli/src/commands/env/on.rs @@ -4,7 +4,10 @@ use std::process::ExitStatus; -use super::config::{ShimMode, load_config, save_config}; +use super::{ + config::{ShimMode, load_config, save_config}, + setup, +}; use crate::{error::Error, help}; /// Execute the `vp env on` command. @@ -12,6 +15,7 @@ pub async fn execute() -> Result { let mut config = load_config().await?; if config.shim_mode == ShimMode::Managed { + setup::refresh_env_files().await?; println!("Node.js management is already set to managed."); println!("All vp commands and shims will always use Vite+ managed Node.js."); return Ok(ExitStatus::default()); @@ -19,6 +23,7 @@ pub async fn execute() -> Result { config.shim_mode = ShimMode::Managed; save_config(&config).await?; + setup::refresh_env_files().await?; println!("\u{2713} Node.js management set to managed."); println!(); diff --git a/crates/vp_global_cli/src/commands/env/setup.rs b/crates/vp_global_cli/src/commands/env/setup.rs index b99249b124..1f803ff706 100644 --- a/crates/vp_global_cli/src/commands/env/setup.rs +++ b/crates/vp_global_cli/src/commands/env/setup.rs @@ -17,7 +17,7 @@ use std::process::ExitStatus; -use super::config::{get_bin_dir, get_vp_home}; +use super::config::{ShimMode, get_bin_dir, get_vp_home, load_config}; use crate::{error::Error, help}; /// Shells that get a generated `~/.vite-plus/env.*` setup script. @@ -515,11 +515,12 @@ pub(crate) async fn cleanup_legacy_windows_shim(bin_dir: &vt_path::AbsolutePath, // POSIX env file (bash/zsh) // When sourced multiple times, removes existing entry and re-prepends to front // Uses parameter expansion to split PATH around the bin entry in O(1) operations -// Includes vp() shell function wrapper for `vp env use` (evals stdout) +// Includes a vp() shell function wrapper for session-mutating env commands // Includes shell completion support const ENV_TEMPLATE_POSIX: &str = r#"#!/bin/sh # Vite+ environment setup (https://viteplus.dev) export VP_HOME="__VP_HOME__" +__PNPM_RUNTIME_ENV__ __vp_bin="__VP_BIN__" case ":${PATH}:" in *":${__vp_bin}:"*) @@ -537,13 +538,16 @@ case ":${PATH}:" in esac unset __vp_bin -# Shell function wrapper: intercepts `vp env use` to eval its stdout, -# which sets/unsets VP_NODE_VERSION in the current shell session. +# Shell function wrapper: applies `vp env use`, `on`, and `off` changes to this session. vp() { if [ "$1" = "env" ] && [ "$2" = "use" ]; then case " $* " in *" -h "*|*" --help "*) command vp "$@"; return; esac __vp_out="$(VP_ENV_USE_EVAL_ENABLE=1 VP_SHELL=sh command vp "$@")" || return $? eval "$__vp_out" + elif [ "$#" -eq 2 ] && [ "$1" = "env" ] && [ "$2" = "on" ]; then + command vp "$@" && export PNPM_CONFIG_RUNTIME=false + elif [ "$#" -eq 2 ] && [ "$1" = "env" ] && [ "$2" = "off" ]; then + command vp "$@" && unset PNPM_CONFIG_RUNTIME else command vp "$@" fi @@ -568,12 +572,12 @@ fi const ENV_TEMPLATE_FISH: &str = r#"# Vite+ environment setup (https://viteplus.dev) set -gx VP_HOME "__VP_HOME__" +__PNPM_RUNTIME_ENV__ set -l __vp_idx (contains -i -- __VP_BIN__ $PATH) and set -e PATH[$__vp_idx] set -gx PATH __VP_BIN__ $PATH -# Shell function wrapper: intercepts `vp env use` to eval its stdout, -# which sets/unsets VP_NODE_VERSION in the current shell session. +# Shell function wrapper: applies `vp env use`, `on`, and `off` changes to this session. function vp if test (count $argv) -ge 2; and test "$argv[1]" = "env"; and test "$argv[2]" = "use" if contains -- -h $argv; or contains -- --help $argv @@ -583,6 +587,10 @@ function vp set -lx VP_SHELL fish set -l __vp_out (command vp $argv); or return $status eval (string join ';' $__vp_out) + else if test (count $argv) -eq 2; and test "$argv[1]" = "env"; and test "$argv[2]" = "on" + command vp $argv; and set -gx PNPM_CONFIG_RUNTIME false + else if test (count $argv) -eq 2; and test "$argv[1]" = "env"; and test "$argv[2]" = "off" + command vp $argv; and set -e PNPM_CONFIG_RUNTIME else command vp $argv end @@ -599,15 +607,15 @@ end complete -c vpr --keep-order --exclusive --arguments "(__vpr_complete)" "#; -// Nushell env file with vp wrapper function. +// Nushell env file with a wrapper for session-mutating env commands. // Completions delegate to Fish dynamically (VP_COMPLETE=fish) because clap_complete_nushell // generates multiple rest params (e.g. for `vp install`), which Nushell does not support. const ENV_TEMPLATE_NU: &str = r#"# Vite+ environment setup (https://viteplus.dev) $env.VP_HOME = ("__VP_HOME__" | path expand --no-symlink) +__PNPM_RUNTIME_ENV__ $env.PATH = ($env.PATH | where { $in != "__VP_BIN__" } | prepend "__VP_BIN__") -# Shell function wrapper: intercepts `vp env use` to parse its stdout, -# which sets/unsets VP_NODE_VERSION in the current shell session. +# Shell function wrapper: applies `vp env use`, `on`, and `off` changes to this session. def --env --wrapped vp [...args: string@"nu-complete vp"] { if ($args | length) >= 2 and $args.0 == "env" and $args.1 == "use" { if ("-h" in $args) or ("--help" in $args) { @@ -630,6 +638,14 @@ def --env --wrapped vp [...args: string@"nu-complete vp"] { for key in $unsets { if ($key in $env) { hide-env $key } } + } else if ($args | length) == 2 and $args.0 == "env" and $args.1 == "on" { + ^vp ...$args + if $env.LAST_EXIT_CODE == 0 { $env.PNPM_CONFIG_RUNTIME = "false" } + } else if ($args | length) == 2 and $args.0 == "env" and $args.1 == "off" { + ^vp ...$args + if $env.LAST_EXIT_CODE == 0 and ("PNPM_CONFIG_RUNTIME" in $env) { + hide-env PNPM_CONFIG_RUNTIME + } } else { ^vp ...$args } @@ -665,13 +681,13 @@ export extern "vpr" [...args: string@"nu-complete vpr"] const ENV_TEMPLATE_PS1: &str = r#"# Vite+ environment setup (https://viteplus.dev) $env:VP_HOME = "__VP_HOME_WIN__" +__PNPM_RUNTIME_ENV__ $__vp_bin = "__VP_BIN_WIN__" if ($env:Path -split ';' -notcontains $__vp_bin) { $env:Path = "$__vp_bin;$env:Path" } -# Shell function wrapper: intercepts `vp env use` to eval its stdout, -# which sets/unsets VP_NODE_VERSION in the current shell session. +# Shell function wrapper: applies `vp env use`, `on`, and `off` changes to this session. function vp { if ($args.Count -ge 2 -and $args[0] -eq "env" -and $args[1] -eq "use") { if ($args -contains "-h" -or $args -contains "--help") { @@ -691,6 +707,14 @@ function vp { if ($LASTEXITCODE -eq 0 -and $output) { Invoke-Expression ($output -join "`n") } + } elseif ($args.Count -eq 2 -and $args[0] -eq "env" -and $args[1] -eq "on") { + & (Join-Path $__vp_bin "vp") @args + if ($LASTEXITCODE -eq 0) { $env:PNPM_CONFIG_RUNTIME = "false" } + } elseif ($args.Count -eq 2 -and $args[0] -eq "env" -and $args[1] -eq "off") { + & (Join-Path $__vp_bin "vp") @args + if ($LASTEXITCODE -eq 0) { + Remove-Item Env:\PNPM_CONFIG_RUNTIME -ErrorAction SilentlyContinue + } } else { & (Join-Path $__vp_bin "vp") @args } @@ -769,14 +793,18 @@ fn escape_nu_double_quoted_string(value: &str) -> String { } /// Render the env-file content for `shell` against `vite_plus_home`. -fn render_env_content(shell: EnvShell, vite_plus_home: &vt_path::AbsolutePath) -> String { +fn render_env_content( + shell: EnvShell, + vite_plus_home: &vt_path::AbsolutePath, + shim_mode: ShimMode, +) -> String { let bin_path = vite_plus_home.join("bin"); let home_dir = vp_shared::EnvConfig::get().user_home; let home_dir = home_dir.as_deref(); let home_path_ref = render_home_relative_path(vite_plus_home.as_path(), home_dir); let bin_path_ref = render_home_relative_path(bin_path.as_path(), home_dir); - match shell { + let content = match shell { EnvShell::Posix => ENV_TEMPLATE_POSIX .replace("__VP_HOME__", &home_path_ref) .replace("__VP_BIN__", &bin_path_ref), @@ -802,7 +830,24 @@ fn render_env_content(shell: EnvShell, vite_plus_home: &vt_path::AbsolutePath) - .replace("__VP_HOME_WIN__", &home_path_win) .replace("__VP_BIN_WIN__", &bin_path_win) } - } + }; + + let pnpm_runtime_env = match (shell, shim_mode) { + (EnvShell::Posix, ShimMode::Managed) => "export PNPM_CONFIG_RUNTIME=false", + (EnvShell::Posix, ShimMode::SystemFirst) => "unset PNPM_CONFIG_RUNTIME", + (EnvShell::Fish, ShimMode::Managed) => "set -gx PNPM_CONFIG_RUNTIME false", + (EnvShell::Fish, ShimMode::SystemFirst) => "set -e PNPM_CONFIG_RUNTIME", + (EnvShell::Nu, ShimMode::Managed) => "$env.PNPM_CONFIG_RUNTIME = \"false\"", + (EnvShell::Nu, ShimMode::SystemFirst) => { + "if (\"PNPM_CONFIG_RUNTIME\" in $env) { hide-env PNPM_CONFIG_RUNTIME }" + } + (EnvShell::Powershell, ShimMode::Managed) => "$env:PNPM_CONFIG_RUNTIME = \"false\"", + (EnvShell::Powershell, ShimMode::SystemFirst) => { + "Remove-Item Env:\\PNPM_CONFIG_RUNTIME -ErrorAction SilentlyContinue" + } + }; + + content.replace("__PNPM_RUNTIME_ENV__", pnpm_runtime_env) } /// Create env files with PATH guard (prevents duplicate PATH entries). @@ -813,14 +858,19 @@ fn render_env_content(shell: EnvShell, vite_plus_home: &vt_path::AbsolutePath) - /// - `~/.vite-plus/env.nu` (Nushell) with `vp env use` wrapper function /// - `~/.vite-plus/env.ps1` (PowerShell) with PATH setup + `vp` function async fn create_env_files(vite_plus_home: &vt_path::AbsolutePath) -> Result<(), Error> { + let shim_mode = load_config().await?.shim_mode; for shell in [EnvShell::Posix, EnvShell::Fish, EnvShell::Nu, EnvShell::Powershell] { - let content = render_env_content(shell, vite_plus_home); + let content = render_env_content(shell, vite_plus_home, shim_mode); tokio::fs::write(vite_plus_home.join(shell.env_file_name()), content).await?; } Ok(()) } +pub(crate) async fn refresh_env_files() -> Result<(), Error> { + create_env_files(&get_vp_home()?).await +} + /// Print instructions for adding bin directory to PATH. fn print_path_instructions(bin_dir: &vt_path::AbsolutePath) { // Derive vite_plus_home from bin_dir (parent), using $HOME prefix for readability @@ -969,7 +1019,7 @@ mod tests { let home = AbsolutePathBuf::new(std::path::PathBuf::from(r#"/tmp/vp "home\with spaces""#)) .unwrap(); - let content = render_env_content(EnvShell::Nu, &home); + let content = render_env_content(EnvShell::Nu, &home, ShimMode::Managed); assert!( content.contains( diff --git a/docs/guide/env.md b/docs/guide/env.md index a1580348d5..1a00226198 100644 --- a/docs/guide/env.md +++ b/docs/guide/env.md @@ -39,6 +39,8 @@ vp env off This switches to system-first mode, where the shims prefer your system Node.js and only fall back to the Vite+-managed runtime when needed. +In managed mode, the generated shell environment sets `PNPM_CONFIG_RUNTIME=false` so pnpm does not download a second copy of Node.js. `vp env off` removes the setting, and `vp env on` restores it. Run `vp env doctor` to check that the current shell matches the selected mode. + ## Commands ### Setup @@ -48,7 +50,7 @@ This switches to system-first mode, where the shims prefer your system Node.js a - `vp env off` enables system-first mode so shims prefer system Node.js first - `vp env print` prints the shell snippet for the current session -PowerShell needs to dot-source the generated setup script in the current shell before `vp env use` can affect only that shell session: +PowerShell needs to dot-source the generated setup script in the current shell before `vp env use`, `vp env on`, and `vp env off` can update that shell session: ```powershell . "$env:USERPROFILE\.vite-plus\env.ps1" @@ -76,7 +78,7 @@ node --version 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` under `VP_HOME/bin` on Windows. +Only `vp env use` needs this alternate command. Other `vp env` commands work normally in Command Prompt. Because Command Prompt cannot update its parent environment, restart it after `vp env on` or `vp env off` to apply the corresponding `PNPM_CONFIG_RUNTIME` change. `vp env setup` creates `vp-use.cmd` under `VP_HOME/bin` on Windows. In CI, `vp env use` can still run without shell initialization. It writes a temporary session file under `VP_HOME` so later shim calls in the same job can resolve the selected Node.js version. From d4d2c61e62b62bd4a734ca9bc22edd1570b41165 Mon Sep 17 00:00:00 2001 From: Liang Date: Sun, 16 Aug 2026 21:45:37 +0800 Subject: [PATCH 2/5] fix(env): keep pnpm runtime state in sync --- .../assert-posix-wrapper.mjs | 8 ++ .../command_env_pnpm_runtime/snapshots.toml | 2 +- .../command_env_pnpm_runtime_current_shell.md | 2 +- .../vp_global_cli/src/commands/env/setup.rs | 78 ++++++++++++++++--- crates/vp_setup/src/install.rs | 10 +-- 5 files changed, 83 insertions(+), 17 deletions(-) diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_env_pnpm_runtime/assert-posix-wrapper.mjs b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_env_pnpm_runtime/assert-posix-wrapper.mjs index aa976a2bdd..b358bdacf5 100644 --- a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_env_pnpm_runtime/assert-posix-wrapper.mjs +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_env_pnpm_runtime/assert-posix-wrapper.mjs @@ -7,6 +7,14 @@ const script = [ '[ -z "${PNPM_CONFIG_RUNTIME+x}" ] || exit 1', 'vp env on >/dev/null || exit 1', '[ "$PNPM_CONFIG_RUNTIME" = false ] || exit 1', + 'vp -C "$PWD" env off >/dev/null || exit 1', + '[ -z "${PNPM_CONFIG_RUNTIME+x}" ] || exit 1', + 'vp "-C$PWD" env on >/dev/null || exit 1', + '[ "$PNPM_CONFIG_RUNTIME" = false ] || exit 1', + 'vp "-C=$PWD" env off >/dev/null || exit 1', + '[ -z "${PNPM_CONFIG_RUNTIME+x}" ] || exit 1', + 'vp env on >/dev/null || exit 1', + '[ "$PNPM_CONFIG_RUNTIME" = false ] || exit 1', ].join('\n') const result = spawnSync('/bin/sh', ['-c', script], { env: process.env, encoding: 'utf8' }) diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_env_pnpm_runtime/snapshots.toml b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_env_pnpm_runtime/snapshots.toml index 3950fadc79..51e22f07ca 100644 --- a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_env_pnpm_runtime/snapshots.toml +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_env_pnpm_runtime/snapshots.toml @@ -16,5 +16,5 @@ name = "command_env_pnpm_runtime_current_shell" vp = "global" skip-platforms = ["windows"] steps = [ - { argv = ["node", "assert-posix-wrapper.mjs"], comment = "vp env on and off update the current shell" }, + { argv = ["node", "assert-posix-wrapper.mjs"], comment = "vp env on and off update the current shell, including through the global -C flag" }, ] diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_env_pnpm_runtime/snapshots/command_env_pnpm_runtime_current_shell.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_env_pnpm_runtime/snapshots/command_env_pnpm_runtime_current_shell.md index adb3177ce1..d4755532be 100644 --- a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_env_pnpm_runtime/snapshots/command_env_pnpm_runtime_current_shell.md +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_env_pnpm_runtime/snapshots/command_env_pnpm_runtime_current_shell.md @@ -2,7 +2,7 @@ ## `node assert-posix-wrapper.mjs` -vp env on and off update the current shell +vp env on and off update the current shell, including through the global -C flag ``` Current shell follows Vite+ environment mode diff --git a/crates/vp_global_cli/src/commands/env/setup.rs b/crates/vp_global_cli/src/commands/env/setup.rs index 1f803ff706..d2bebbe0d9 100644 --- a/crates/vp_global_cli/src/commands/env/setup.rs +++ b/crates/vp_global_cli/src/commands/env/setup.rs @@ -538,15 +538,30 @@ case ":${PATH}:" in esac unset __vp_bin +# Match `vp env on|off` with an optional leading `-C` global flag. +__vp_is_env_mode() { + case "$#" in + 3) [ "$2" = "env" ] && [ "$3" = "$1" ] ;; + 4) + case "$2" in + -C?*) [ "$3" = "env" ] && [ "$4" = "$1" ] ;; + *) return 1 ;; + esac + ;; + 5) [ "$2" = "-C" ] && [ "$4" = "env" ] && [ "$5" = "$1" ] ;; + *) return 1 ;; + esac +} + # Shell function wrapper: applies `vp env use`, `on`, and `off` changes to this session. vp() { if [ "$1" = "env" ] && [ "$2" = "use" ]; then case " $* " in *" -h "*|*" --help "*) command vp "$@"; return; esac __vp_out="$(VP_ENV_USE_EVAL_ENABLE=1 VP_SHELL=sh command vp "$@")" || return $? eval "$__vp_out" - elif [ "$#" -eq 2 ] && [ "$1" = "env" ] && [ "$2" = "on" ]; then + elif __vp_is_env_mode on "$@"; then command vp "$@" && export PNPM_CONFIG_RUNTIME=false - elif [ "$#" -eq 2 ] && [ "$1" = "env" ] && [ "$2" = "off" ]; then + elif __vp_is_env_mode off "$@"; then command vp "$@" && unset PNPM_CONFIG_RUNTIME else command vp "$@" @@ -579,6 +594,15 @@ set -gx PATH __VP_BIN__ $PATH # Shell function wrapper: applies `vp env use`, `on`, and `off` changes to this session. function vp + set -l __vp_env_mode + if test (count $argv) -eq 2; and test "$argv[1]" = "env" + set __vp_env_mode $argv[2] + else if test (count $argv) -eq 3; and string match -qr '^-C.+' -- "$argv[1]"; and test "$argv[2]" = "env" + set __vp_env_mode $argv[3] + else if test (count $argv) -eq 4; and test "$argv[1]" = "-C"; and test "$argv[3]" = "env" + set __vp_env_mode $argv[4] + end + if test (count $argv) -ge 2; and test "$argv[1]" = "env"; and test "$argv[2]" = "use" if contains -- -h $argv; or contains -- --help $argv command vp $argv; return @@ -587,9 +611,9 @@ function vp set -lx VP_SHELL fish set -l __vp_out (command vp $argv); or return $status eval (string join ';' $__vp_out) - else if test (count $argv) -eq 2; and test "$argv[1]" = "env"; and test "$argv[2]" = "on" + else if test "$__vp_env_mode" = "on" command vp $argv; and set -gx PNPM_CONFIG_RUNTIME false - else if test (count $argv) -eq 2; and test "$argv[1]" = "env"; and test "$argv[2]" = "off" + else if test "$__vp_env_mode" = "off" command vp $argv; and set -e PNPM_CONFIG_RUNTIME else command vp $argv @@ -617,6 +641,16 @@ $env.PATH = ($env.PATH | where { $in != "__VP_BIN__" } | prepend "__VP_BIN__") # Shell function wrapper: applies `vp env use`, `on`, and `off` changes to this session. def --env --wrapped vp [...args: string@"nu-complete vp"] { + let env_mode = if ($args | length) == 2 and $args.0 == "env" { + $args.1 + } else if ($args | length) == 3 and $args.0 =~ '^-C.+' and $args.1 == "env" { + $args.2 + } else if ($args | length) == 4 and $args.0 == "-C" and $args.2 == "env" { + $args.3 + } else { + null + } + if ($args | length) >= 2 and $args.0 == "env" and $args.1 == "use" { if ("-h" in $args) or ("--help" in $args) { ^vp ...$args @@ -638,10 +672,10 @@ def --env --wrapped vp [...args: string@"nu-complete vp"] { for key in $unsets { if ($key in $env) { hide-env $key } } - } else if ($args | length) == 2 and $args.0 == "env" and $args.1 == "on" { + } else if $env_mode == "on" { ^vp ...$args if $env.LAST_EXIT_CODE == 0 { $env.PNPM_CONFIG_RUNTIME = "false" } - } else if ($args | length) == 2 and $args.0 == "env" and $args.1 == "off" { + } else if $env_mode == "off" { ^vp ...$args if $env.LAST_EXIT_CODE == 0 and ("PNPM_CONFIG_RUNTIME" in $env) { hide-env PNPM_CONFIG_RUNTIME @@ -689,6 +723,15 @@ if ($env:Path -split ';' -notcontains $__vp_bin) { # Shell function wrapper: applies `vp env use`, `on`, and `off` changes to this session. function vp { + $envMode = $null + if ($args.Count -eq 2 -and $args[0] -eq "env") { + $envMode = $args[1] + } elseif ($args.Count -eq 3 -and $args[0] -match '^-C.+' -and $args[1] -eq "env") { + $envMode = $args[2] + } elseif ($args.Count -eq 4 -and $args[0] -eq "-C" -and $args[2] -eq "env") { + $envMode = $args[3] + } + if ($args.Count -ge 2 -and $args[0] -eq "env" -and $args[1] -eq "use") { if ($args -contains "-h" -or $args -contains "--help") { & (Join-Path $__vp_bin "vp") @args; return @@ -707,10 +750,10 @@ function vp { if ($LASTEXITCODE -eq 0 -and $output) { Invoke-Expression ($output -join "`n") } - } elseif ($args.Count -eq 2 -and $args[0] -eq "env" -and $args[1] -eq "on") { + } elseif ($envMode -eq "on") { & (Join-Path $__vp_bin "vp") @args if ($LASTEXITCODE -eq 0) { $env:PNPM_CONFIG_RUNTIME = "false" } - } elseif ($args.Count -eq 2 -and $args[0] -eq "env" -and $args[1] -eq "off") { + } elseif ($envMode -eq "off") { & (Join-Path $__vp_bin "vp") @args if ($LASTEXITCODE -eq 0) { Remove-Item Env:\PNPM_CONFIG_RUNTIME -ErrorAction SilentlyContinue @@ -868,7 +911,9 @@ async fn create_env_files(vite_plus_home: &vt_path::AbsolutePath) -> Result<(), } pub(crate) async fn refresh_env_files() -> Result<(), Error> { - create_env_files(&get_vp_home()?).await + let vite_plus_home = get_vp_home()?; + tokio::fs::create_dir_all(&vite_plus_home).await?; + create_env_files(&vite_plus_home).await } /// Print instructions for adding bin directory to PATH. @@ -1004,6 +1049,21 @@ mod tests { assert!(env_ps1_path.as_path().exists(), "env.ps1 file should be created"); } + #[tokio::test] + async fn test_refresh_env_files_creates_missing_home() { + let temp_dir = TempDir::new().unwrap(); + let home = AbsolutePathBuf::new(temp_dir.path().join(".vite-plus")).unwrap(); + let _guard = vp_shared::EnvConfig::test_guard(vp_shared::EnvConfig { + vite_plus_home: Some(home.as_path().to_path_buf()), + user_home: Some(temp_dir.path().to_path_buf()), + ..vp_shared::EnvConfig::for_test() + }); + + refresh_env_files().await.unwrap(); + + assert!(home.join("env").as_path().exists(), "env file should be created"); + } + #[test] fn test_escape_nu_double_quoted_string() { assert_eq!( diff --git a/crates/vp_setup/src/install.rs b/crates/vp_setup/src/install.rs index af5aa48aa8..80f1613b69 100644 --- a/crates/vp_setup/src/install.rs +++ b/crates/vp_setup/src/install.rs @@ -592,7 +592,7 @@ pub async fn read_current_version(install_dir: &AbsolutePath) -> Option target.file_name().and_then(|n| n.to_str()).map(String::from) } -/// Create shell env files by running `vp env setup --env-only`. +/// Create system-first shell env files by running `vp env off`. /// /// Used when the Node.js manager is disabled — ensures env files exist /// even without a full shim refresh. @@ -603,15 +603,13 @@ pub async fn create_env_files(install_dir: &AbsolutePath) -> Result<(), Error> { return Ok(()); } - let output = tokio::process::Command::new(vp_binary.as_path()) - .args(["env", "setup", "--env-only"]) - .output() - .await?; + let output = + tokio::process::Command::new(vp_binary.as_path()).args(["env", "off"]).output().await?; if !output.status.success() { let stderr = String::from_utf8_lossy(&output.stderr); tracing::warn!( - "env setup --env-only exited with code {}, continuing anyway\n{}", + "env off exited with code {}, continuing anyway\n{}", vp_shared::exit_code_from_status(output.status), stderr.trim() ); From 611e885506f42272fb3df8f5095536e885f926e7 Mon Sep 17 00:00:00 2001 From: Liang Date: Sun, 16 Aug 2026 21:52:14 +0800 Subject: [PATCH 3/5] fix(install): preserve pnpm runtime opt-out --- packages/cli/install.sh | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/cli/install.sh b/packages/cli/install.sh index 5aa2244fab..33061f5e0f 100644 --- a/packages/cli/install.sh +++ b/packages/cli/install.sh @@ -1198,6 +1198,9 @@ WRAPPER_EOF # Setup Node.js version manager (shims) - separate component setup_node_manager "$BIN_DIR" + if [ "$NODE_MANAGER_ENABLED" = "false" ]; then + "$vp_bin" env off > /dev/null + fi prompt_remove_previous_install_dir "$previous_install_dir" From c99a498b04b121e89bddb117b9aa169ceadefca0 Mon Sep 17 00:00:00 2001 From: Liang Date: Sun, 16 Aug 2026 22:00:25 +0800 Subject: [PATCH 4/5] docs(env): clarify command prompt limitation --- docs/guide/env.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/guide/env.md b/docs/guide/env.md index 1a00226198..fb645bc96a 100644 --- a/docs/guide/env.md +++ b/docs/guide/env.md @@ -78,7 +78,7 @@ node --version vp-use --unset ``` -Only `vp env use` needs this alternate command. Other `vp env` commands work normally in Command Prompt. Because Command Prompt cannot update its parent environment, restart it after `vp env on` or `vp env off` to apply the corresponding `PNPM_CONFIG_RUNTIME` change. `vp env setup` creates `vp-use.cmd` under `VP_HOME/bin` on Windows. +Only `vp env use` needs this alternate command. Other `vp env` commands still update Vite+'s configuration, but Command Prompt does not load the generated shell environment files, so `PNPM_CONFIG_RUNTIME` is not applied there. `vp env setup` creates `vp-use.cmd` under `VP_HOME/bin` on Windows. In CI, `vp env use` can still run without shell initialization. It writes a temporary session file under `VP_HOME` so later shim calls in the same job can resolve the selected Node.js version. From d940f82707ab993e294f64bda3d00975aab0c319 Mon Sep 17 00:00:00 2001 From: Liang Date: Sun, 16 Aug 2026 22:05:54 +0800 Subject: [PATCH 5/5] fix(install): preserve Windows runtime opt-out --- packages/cli/install.ps1 | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/packages/cli/install.ps1 b/packages/cli/install.ps1 index c37507a314..ba4f9dd84f 100644 --- a/packages/cli/install.ps1 +++ b/packages/cli/install.ps1 @@ -964,6 +964,13 @@ exec "`$VP_HOME/current/bin/vp.exe" "`$@" # Setup Node.js version manager (shims) - separate component $nodeManagerResult = Setup-NodeManager -BinDir $BinDir + if ($nodeManagerResult -eq "false") { + $envOffOutput = & "$BinDir\vp.exe" env off 2>&1 + if ($LASTEXITCODE -ne 0) { + Write-Warn "Failed to configure system-first Node.js mode:" + Write-Host "$envOffOutput" + } + } Prompt-RemovePreviousInstallDir -PreviousInstallDir $previousInstallDir