Skip to content

Commit 62f9f0e

Browse files
authored
fix(env): clarify the use case of vp-use.cmd (#2128)
`vp env setup` currently creates `vp-use.cmd` on every platform even though the wrapper is only usable from Windows Command Prompt. Its writer also runs before `VP_HOME/bin` is created, so setup skips the wrapper when starting from a fresh home. The CMD-specific version-switching command is also missing from the public environment guide. This PR creates `vp-use.cmd` alongside the Windows shims after the bin directory is initialized, adds regression coverage for fresh Windows setup and Unix exclusion, and documents how to use `vp-use` from Command Prompt. 🤖 Generated with Codex
1 parent 9247583 commit 62f9f0e

2 files changed

Lines changed: 76 additions & 13 deletions

File tree

crates/vite_global_cli/src/commands/env/setup.rs

Lines changed: 66 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,9 @@ pub async fn execute(refresh: bool, env_only: bool) -> Result<ExitStatus, Error>
8080
// Ensure bin directory exists
8181
tokio::fs::create_dir_all(&bin_dir).await?;
8282

83+
#[cfg(windows)]
84+
tokio::fs::write(bin_dir.join("vp-use.cmd"), VP_USE_CMD_CONTENT).await?;
85+
8386
// Get the current executable path (for shims)
8487
let current_exe = std::env::current_exe()?;
8588

@@ -732,6 +735,7 @@ Register-ArgumentCompleter -Native -CommandName vpr -ScriptBlock $__vpr_comp
732735

733736
// cmd.exe wrapper for `vp env use` (cmd.exe cannot define shell functions).
734737
// Users run `vp-use 24` in cmd.exe instead of `vp env use 24`.
738+
#[cfg(windows)]
735739
const VP_USE_CMD_CONTENT: &str = "@echo off\r\nset VP_ENV_USE_EVAL_ENABLE=1\r\nset VP_HOME=%~dp0..\r\nfor /f \"delims=\" %%i in ('%~dp0..\\current\\bin\\vp.exe env use %*') do %%i\r\nset VP_ENV_USE_EVAL_ENABLE=\r\n";
736740

737741
fn render_home_relative_path(path: &std::path::Path, home_dir: Option<&std::path::Path>) -> String {
@@ -800,19 +804,12 @@ fn render_env_content(shell: EnvShell, vite_plus_home: &vite_path::AbsolutePath)
800804
/// - `~/.vite-plus/env.fish` (fish shell) with `vp` wrapper function
801805
/// - `~/.vite-plus/env.nu` (Nushell) with `vp env use` wrapper function
802806
/// - `~/.vite-plus/env.ps1` (PowerShell) with PATH setup + `vp` function
803-
/// - `~/.vite-plus/bin/vp-use.cmd` (cmd.exe wrapper for `vp env use`)
804807
async fn create_env_files(vite_plus_home: &vite_path::AbsolutePath) -> Result<(), Error> {
805808
for shell in [EnvShell::Posix, EnvShell::Fish, EnvShell::Nu, EnvShell::Powershell] {
806809
let content = render_env_content(shell, vite_plus_home);
807810
tokio::fs::write(vite_plus_home.join(shell.env_file_name()), content).await?;
808811
}
809812

810-
// Only write the cmd wrapper if bin directory exists (it may not during --env-only)
811-
let bin_path = vite_plus_home.join("bin");
812-
if tokio::fs::try_exists(&bin_path).await.unwrap_or(false) {
813-
tokio::fs::write(bin_path.join("vp-use.cmd"), VP_USE_CMD_CONTENT).await?;
814-
}
815-
816813
Ok(())
817814
}
818815

@@ -900,6 +897,37 @@ mod tests {
900897
})
901898
}
902899

900+
#[cfg(windows)]
901+
struct FakeTrampolineGuard(Option<std::ffi::OsString>);
902+
903+
#[cfg(windows)]
904+
impl FakeTrampolineGuard {
905+
fn new(dir: &std::path::Path) -> Self {
906+
let trampoline = dir.join("vp-shim.exe");
907+
std::fs::write(&trampoline, b"fake-trampoline").unwrap();
908+
let previous = std::env::var_os(vite_shared::env_vars::VP_TRAMPOLINE_PATH);
909+
// SAFETY: This Windows-only test is serialized and the guard restores the variable.
910+
unsafe {
911+
std::env::set_var(vite_shared::env_vars::VP_TRAMPOLINE_PATH, &trampoline);
912+
}
913+
Self(previous)
914+
}
915+
}
916+
917+
#[cfg(windows)]
918+
impl Drop for FakeTrampolineGuard {
919+
fn drop(&mut self) {
920+
// SAFETY: This Windows-only test is serialized and restores the previous value.
921+
unsafe {
922+
if let Some(previous) = self.0.take() {
923+
std::env::set_var(vite_shared::env_vars::VP_TRAMPOLINE_PATH, previous);
924+
} else {
925+
std::env::remove_var(vite_shared::env_vars::VP_TRAMPOLINE_PATH);
926+
}
927+
}
928+
}
929+
}
930+
903931
#[tokio::test]
904932
async fn test_create_env_files_creates_all_files() {
905933
let temp_dir = TempDir::new().unwrap();
@@ -1193,15 +1221,23 @@ mod tests {
11931221
}
11941222

11951223
#[tokio::test]
1196-
async fn test_create_env_files_cmd_wrapper_sets_vp_home_before_env_use() {
1224+
#[cfg(windows)]
1225+
#[serial_test::serial]
1226+
async fn test_execute_creates_cmd_wrapper_in_fresh_home() {
11971227
let temp_dir = TempDir::new().unwrap();
1198-
let home = AbsolutePathBuf::new(temp_dir.path().to_path_buf()).unwrap();
1199-
let _guard = home_guard(temp_dir.path());
1200-
let bin_dir = home.join("bin");
1201-
tokio::fs::create_dir_all(&bin_dir).await.unwrap();
1228+
let fresh_home = temp_dir.path().join("new-vite-plus");
1229+
let _trampoline_guard = FakeTrampolineGuard::new(temp_dir.path());
1230+
let _env_guard = vite_shared::EnvConfig::test_guard(vite_shared::EnvConfig {
1231+
vite_plus_home: Some(fresh_home.clone()),
1232+
user_home: Some(temp_dir.path().to_path_buf()),
1233+
..vite_shared::EnvConfig::for_test()
1234+
});
12021235

1203-
create_env_files(&home).await.unwrap();
1236+
assert!(!fresh_home.exists(), "VP_HOME should not exist before initial setup");
1237+
let status = execute(false, false).await.unwrap();
12041238

1239+
assert!(status.success(), "initial vp env setup should succeed");
1240+
let bin_dir = AbsolutePathBuf::new(fresh_home.join("bin")).unwrap();
12051241
let cmd_content = tokio::fs::read_to_string(bin_dir.join("vp-use.cmd")).await.unwrap();
12061242
assert!(
12071243
cmd_content.contains("set VP_HOME=%~dp0..\r\nfor /f"),
@@ -1213,6 +1249,23 @@ mod tests {
12131249
);
12141250
}
12151251

1252+
#[tokio::test]
1253+
#[cfg(unix)]
1254+
async fn test_create_env_files_does_not_create_cmd_wrapper_on_unix() {
1255+
let temp_dir = TempDir::new().unwrap();
1256+
let home = AbsolutePathBuf::new(temp_dir.path().to_path_buf()).unwrap();
1257+
let _guard = home_guard(temp_dir.path());
1258+
let bin_dir = home.join("bin");
1259+
tokio::fs::create_dir_all(&bin_dir).await.unwrap();
1260+
1261+
create_env_files(&home).await.unwrap();
1262+
1263+
assert!(
1264+
!bin_dir.join("vp-use.cmd").as_path().exists(),
1265+
"vp-use.cmd should only be created on Windows"
1266+
);
1267+
}
1268+
12161269
#[tokio::test]
12171270
async fn test_execute_env_only_creates_home_dir_and_env_files() {
12181271
let temp_dir = TempDir::new().unwrap();

docs/guide/env.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,16 @@ Open the profile file for editing:
6464
Invoke-Item $PROFILE
6565
```
6666

67+
Windows Command Prompt (`cmd.exe`) cannot define the wrapper function needed for `vp env use` to update the current shell session. Use the generated `vp-use.cmd` command instead:
68+
69+
```batch
70+
vp-use 20
71+
node --version
72+
vp-use --unset
73+
```
74+
75+
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.
76+
6777
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.
6878

6979
### Manage

0 commit comments

Comments
 (0)