Skip to content
Merged
Show file tree
Hide file tree
Changes from 24 commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

118 changes: 102 additions & 16 deletions crates/vite_global_cli/src/commands/env/setup.rs
Original file line number Diff line number Diff line change
Expand Up @@ -540,6 +540,7 @@ async fn cleanup_legacy_completion_dir(vite_plus_home: &vite_path::AbsolutePath)
// Includes shell completion support
const ENV_TEMPLATE_POSIX: &str = r#"#!/bin/sh
# Vite+ environment setup (https://viteplus.dev)
export VP_HOME="__VP_HOME__"
__vp_bin="__VP_BIN__"
case ":${PATH}:" in
*":${__vp_bin}:"*)
Expand Down Expand Up @@ -587,6 +588,7 @@ fi
"#;

const ENV_TEMPLATE_FISH: &str = r#"# Vite+ environment setup (https://viteplus.dev)
set -gx VP_HOME "__VP_HOME__"
set -l __vp_idx (contains -i -- __VP_BIN__ $PATH)
and set -e PATH[$__vp_idx]
set -gx PATH __VP_BIN__ $PATH
Expand Down Expand Up @@ -622,6 +624,7 @@ complete -c vpr --keep-order --exclusive --arguments "(__vpr_complete)"
// 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__"
Comment thread
fengmk2 marked this conversation as resolved.
Outdated
$env.PATH = ($env.PATH | where { $in != "__VP_BIN__" } | prepend "__VP_BIN__")

# Shell function wrapper: intercepts `vp env use` to parse its stdout,
Expand Down Expand Up @@ -682,6 +685,7 @@ 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__"
$__vp_bin = "__VP_BIN_WIN__"
if ($env:Path -split ';' -notcontains $__vp_bin) {
$env:Path = "$__vp_bin;$env:Path"
Expand Down Expand Up @@ -742,37 +746,56 @@ Register-ArgumentCompleter -Native -CommandName vpr -ScriptBlock $__vpr_comp

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

/// Render the env-file content for `shell` against `vite_plus_home`.
fn render_env_content(shell: EnvShell, vite_plus_home: &vite_path::AbsolutePath) -> String {
let bin_path = vite_plus_home.join("bin");
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";

fn render_home_relative_path(path: &std::path::Path, home_dir: Option<&std::path::Path>) -> String {
// Use $HOME-relative path if install dir is under HOME (like rustup's ~/.cargo/env).
// This makes the env file portable across sessions where HOME may differ.
let home_dir = vite_shared::EnvConfig::get().user_home;
let bin_path_ref = home_dir
.as_ref()
.and_then(|h| bin_path.as_path().strip_prefix(h).ok())
home_dir
.and_then(|h| path.strip_prefix(h).ok())
.map(|s| {
// Normalize to forward slashes for $HOME/... paths (POSIX-style)
format!("$HOME/{}", s.display().to_string().replace('\\', "/"))
if s.as_os_str().is_empty() {
"$HOME".to_string()
} else {
// Normalize to forward slashes for $HOME/... paths (POSIX-style)
format!("$HOME/{}", s.display().to_string().replace('\\', "/"))
}
})
.unwrap_or_else(|| bin_path.as_path().display().to_string().replace('\\', "/"));
.unwrap_or_else(|| path.display().to_string().replace('\\', "/"))
}

/// Render the env-file content for `shell` against `vite_plus_home`.
fn render_env_content(shell: EnvShell, vite_plus_home: &vite_path::AbsolutePath) -> String {
let bin_path = vite_plus_home.join("bin");
let home_dir = vite_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 {
EnvShell::Posix => ENV_TEMPLATE_POSIX.replace("__VP_BIN__", &bin_path_ref),
EnvShell::Fish => ENV_TEMPLATE_FISH.replace("__VP_BIN__", &bin_path_ref),
EnvShell::Posix => ENV_TEMPLATE_POSIX
.replace("__VP_HOME__", &home_path_ref)
.replace("__VP_BIN__", &bin_path_ref),
EnvShell::Fish => ENV_TEMPLATE_FISH
.replace("__VP_HOME__", &home_path_ref)
.replace("__VP_BIN__", &bin_path_ref),
EnvShell::Nu => {
// Nushell requires `~` instead of `$HOME` in string literals — `$HOME` is not
// expanded at parse time, so PATH entries would contain a literal "$HOME/...".
let bin_path_ref_nu = bin_path_ref.replace("$HOME/", "~/");
ENV_TEMPLATE_NU.replace("__VP_BIN__", &bin_path_ref_nu)
let home_path_ref_nu =
vite_plus_home.as_path().display().to_string().replace('\\', "/");
Comment thread
liangmiQwQ marked this conversation as resolved.
Outdated
ENV_TEMPLATE_NU
.replace("__VP_HOME__", &home_path_ref_nu)
.replace("__VP_BIN__", &bin_path_ref_nu)
}
EnvShell::Powershell => {
// PowerShell uses the actual absolute path (not $HOME-relative)
let home_path_win = vite_plus_home.as_path().display().to_string();
let bin_path_win = bin_path.as_path().display().to_string();
ENV_TEMPLATE_PS1.replace("__VP_BIN_WIN__", &bin_path_win)
ENV_TEMPLATE_PS1
.replace("__VP_HOME_WIN__", &home_path_win)
.replace("__VP_BIN_WIN__", &bin_path_win)
}
}
}
Expand Down Expand Up @@ -940,6 +963,8 @@ mod tests {

let env_content = tokio::fs::read_to_string(home.join("env")).await.unwrap();
let fish_content = tokio::fs::read_to_string(home.join("env.fish")).await.unwrap();
let nu_content = tokio::fs::read_to_string(home.join("env.nu")).await.unwrap();
let ps1_content = tokio::fs::read_to_string(home.join("env.ps1")).await.unwrap();

// Placeholder should be fully replaced
assert!(
Expand All @@ -950,6 +975,14 @@ mod tests {
!fish_content.contains("__VP_BIN__"),
"env.fish file should not contain __VP_BIN__ placeholder"
);
assert!(
!env_content.contains("__VP_HOME__") && !fish_content.contains("__VP_HOME__"),
"env files should not contain __VP_HOME__ placeholder"
);
assert!(
!nu_content.contains("__VP_HOME__") && !ps1_content.contains("__VP_HOME_WIN__"),
"env files should not contain VP_HOME placeholders"
);

// Should use $HOME-relative path since install dir is under HOME
assert!(
Expand All @@ -960,6 +993,25 @@ mod tests {
fish_content.contains("$HOME/bin"),
"env.fish file should reference $HOME/bin, got: {fish_content}"
);
assert!(
env_content.contains("export VP_HOME=\"$HOME\""),
"env file should export VP_HOME, got: {env_content}"
);
assert!(
fish_content.contains("set -gx VP_HOME \"$HOME\""),
"env.fish file should export VP_HOME, got: {fish_content}"
);

let expected_home = home.as_path().display().to_string();
let expected_home_nu = expected_home.replace('\\', "/");
assert!(
nu_content.contains(&format!("$env.VP_HOME = \"{expected_home_nu}\"")),
"env.nu file should set VP_HOME, got: {nu_content}"
);
assert!(
ps1_content.contains(&format!("$env:VP_HOME = \"{expected_home}\"")),
"env.ps1 file should set VP_HOME, got: {ps1_content}"
);
}

#[tokio::test]
Expand All @@ -977,6 +1029,7 @@ mod tests {
// Should use absolute path since install dir is not under HOME
let expected_bin = home.join("bin");
let expected_str = expected_bin.as_path().display().to_string().replace('\\', "/");
let expected_home = home.as_path().display().to_string().replace('\\', "/");
assert!(
env_content.contains(&expected_str),
"env file should use absolute path {expected_str}, got: {env_content}"
Expand All @@ -985,6 +1038,14 @@ mod tests {
fish_content.contains(&expected_str),
"env.fish file should use absolute path {expected_str}, got: {fish_content}"
);
assert!(
env_content.contains(&format!("export VP_HOME=\"{expected_home}\"")),
"env file should export absolute VP_HOME {expected_home}, got: {env_content}"
);
assert!(
fish_content.contains(&format!("set -gx VP_HOME \"{expected_home}\"")),
"env.fish file should export absolute VP_HOME {expected_home}, got: {fish_content}"
);

// Should NOT use $HOME-relative path
assert!(!env_content.contains("$HOME/bin"), "env file should not reference $HOME/bin");
Expand Down Expand Up @@ -1134,6 +1195,31 @@ mod tests {
);
}

#[tokio::test]
async fn test_create_env_files_cmd_wrapper_sets_vp_home_before_env_use() {
let temp_dir = TempDir::new().unwrap();
let home = AbsolutePathBuf::new(temp_dir.path().to_path_buf()).unwrap();
let _guard = home_guard(temp_dir.path());
let bin_dir = home.join("bin");
tokio::fs::create_dir_all(&bin_dir).await.unwrap();

create_env_files(&home).await.unwrap();

let cmd_content = tokio::fs::read_to_string(bin_dir.join("vp-use.cmd")).await.unwrap();
let vp_home_index =
cmd_content.find("set VP_HOME=%~dp0..").expect("vp-use.cmd should set VP_HOME");
let env_use_index = cmd_content.find("for /f").expect("vp-use.cmd should run env use");

assert!(
vp_home_index < env_use_index,
"vp-use.cmd should set VP_HOME before invoking vp env use"
);
assert!(
cmd_content.contains("%~dp0..\\current\\bin\\vp.exe env use %*"),
"vp-use.cmd should invoke the install-local vp.exe"
);
}

#[tokio::test]
async fn test_execute_env_only_creates_home_dir_and_env_files() {
let temp_dir = TempDir::new().unwrap();
Expand Down
Loading
Loading