@@ -526,6 +526,7 @@ pub(crate) async fn cleanup_legacy_windows_shim(bin_dir: &vite_path::AbsolutePat
526526// Includes shell completion support
527527const ENV_TEMPLATE_POSIX : & str = r#"#!/bin/sh
528528# Vite+ environment setup (https://viteplus.dev)
529+ export VP_HOME="__VP_HOME__"
529530__vp_bin="__VP_BIN__"
530531case ":${PATH}:" in
531532 *":${__vp_bin}:"*)
573574"# ;
574575
575576const ENV_TEMPLATE_FISH : & str = r#"# Vite+ environment setup (https://viteplus.dev)
577+ set -gx VP_HOME "__VP_HOME__"
576578set -l __vp_idx (contains -i -- __VP_BIN__ $PATH)
577579and set -e PATH[$__vp_idx]
578580set -gx PATH __VP_BIN__ $PATH
@@ -608,6 +610,7 @@ complete -c vpr --keep-order --exclusive --arguments "(__vpr_complete)"
608610// Completions delegate to Fish dynamically (VP_COMPLETE=fish) because clap_complete_nushell
609611// generates multiple rest params (e.g. for `vp install`), which Nushell does not support.
610612const ENV_TEMPLATE_NU : & str = r#"# Vite+ environment setup (https://viteplus.dev)
613+ $env.VP_HOME = ("__VP_HOME__" | path expand --no-symlink)
611614$env.PATH = ($env.PATH | where { $in != "__VP_BIN__" } | prepend "__VP_BIN__")
612615
613616# Shell function wrapper: intercepts `vp env use` to parse its stdout,
@@ -668,6 +671,7 @@ export extern "vpr" [...args: string@"nu-complete vpr"]
668671"# ;
669672
670673const ENV_TEMPLATE_PS1 : & str = r#"# Vite+ environment setup (https://viteplus.dev)
674+ $env:VP_HOME = "__VP_HOME_WIN__"
671675$__vp_bin = "__VP_BIN_WIN__"
672676if ($env:Path -split ';' -notcontains $__vp_bin) {
673677 $env:Path = "$__vp_bin;$env:Path"
@@ -728,37 +732,63 @@ Register-ArgumentCompleter -Native -CommandName vpr -ScriptBlock $__vpr_comp
728732
729733// cmd.exe wrapper for `vp env use` (cmd.exe cannot define shell functions).
730734// Users run `vp-use 24` in cmd.exe instead of `vp env use 24`.
731- const VP_USE_CMD_CONTENT : & str = "@echo off\r \n set VP_ENV_USE_EVAL_ENABLE=1\r \n for /f \" delims=\" %%i in ('%~dp0..\\ current\\ bin\\ vp.exe env use %*') do %%i\r \n set VP_ENV_USE_EVAL_ENABLE=\r \n " ;
732-
733- /// Render the env-file content for `shell` against `vite_plus_home`.
734- fn render_env_content ( shell : EnvShell , vite_plus_home : & vite_path:: AbsolutePath ) -> String {
735- let bin_path = vite_plus_home. join ( "bin" ) ;
735+ const VP_USE_CMD_CONTENT : & str = "@echo off\r \n set VP_ENV_USE_EVAL_ENABLE=1\r \n set VP_HOME=%~dp0..\r \n for /f \" delims=\" %%i in ('%~dp0..\\ current\\ bin\\ vp.exe env use %*') do %%i\r \n set VP_ENV_USE_EVAL_ENABLE=\r \n " ;
736736
737+ fn render_home_relative_path ( path : & std:: path:: Path , home_dir : Option < & std:: path:: Path > ) -> String {
737738 // Use $HOME-relative path if install dir is under HOME (like rustup's ~/.cargo/env).
738739 // This makes the env file portable across sessions where HOME may differ.
739- let home_dir = vite_shared:: EnvConfig :: get ( ) . user_home ;
740- let bin_path_ref = home_dir
741- . as_ref ( )
742- . and_then ( |h| bin_path. as_path ( ) . strip_prefix ( h) . ok ( ) )
740+ home_dir
741+ . and_then ( |h| path. strip_prefix ( h) . ok ( ) )
743742 . map ( |s| {
744- // Normalize to forward slashes for $HOME/... paths (POSIX-style)
745- format ! ( "$HOME/{}" , s. display( ) . to_string( ) . replace( '\\' , "/" ) )
743+ if s. as_os_str ( ) . is_empty ( ) {
744+ "$HOME" . to_string ( )
745+ } else {
746+ // Normalize to forward slashes for $HOME/... paths (POSIX-style)
747+ format ! ( "$HOME/{}" , s. display( ) . to_string( ) . replace( '\\' , "/" ) )
748+ }
746749 } )
747- . unwrap_or_else ( || bin_path. as_path ( ) . display ( ) . to_string ( ) . replace ( '\\' , "/" ) ) ;
750+ . unwrap_or_else ( || path. display ( ) . to_string ( ) . replace ( '\\' , "/" ) )
751+ }
752+
753+ fn render_nu_path_ref ( path_ref : & str ) -> String {
754+ match path_ref. strip_prefix ( "$HOME" ) {
755+ Some ( "" ) => "~" . to_string ( ) ,
756+ Some ( suffix) if suffix. starts_with ( '/' ) => format ! ( "~{suffix}" ) ,
757+ _ => path_ref. to_string ( ) ,
758+ }
759+ }
760+
761+ /// Render the env-file content for `shell` against `vite_plus_home`.
762+ fn render_env_content ( shell : EnvShell , vite_plus_home : & vite_path:: AbsolutePath ) -> String {
763+ let bin_path = vite_plus_home. join ( "bin" ) ;
764+ let home_dir = vite_shared:: EnvConfig :: get ( ) . user_home ;
765+ let home_dir = home_dir. as_deref ( ) ;
766+ let home_path_ref = render_home_relative_path ( vite_plus_home. as_path ( ) , home_dir) ;
767+ let bin_path_ref = render_home_relative_path ( bin_path. as_path ( ) , home_dir) ;
748768
749769 match shell {
750- EnvShell :: Posix => ENV_TEMPLATE_POSIX . replace ( "__VP_BIN__" , & bin_path_ref) ,
751- EnvShell :: Fish => ENV_TEMPLATE_FISH . replace ( "__VP_BIN__" , & bin_path_ref) ,
770+ EnvShell :: Posix => ENV_TEMPLATE_POSIX
771+ . replace ( "__VP_HOME__" , & home_path_ref)
772+ . replace ( "__VP_BIN__" , & bin_path_ref) ,
773+ EnvShell :: Fish => ENV_TEMPLATE_FISH
774+ . replace ( "__VP_HOME__" , & home_path_ref)
775+ . replace ( "__VP_BIN__" , & bin_path_ref) ,
752776 EnvShell :: Nu => {
753777 // Nushell requires `~` instead of `$HOME` in string literals — `$HOME` is not
754778 // expanded at parse time, so PATH entries would contain a literal "$HOME/...".
755- let bin_path_ref_nu = bin_path_ref. replace ( "$HOME/" , "~/" ) ;
756- ENV_TEMPLATE_NU . replace ( "__VP_BIN__" , & bin_path_ref_nu)
779+ let home_path_ref_nu = render_nu_path_ref ( & home_path_ref) ;
780+ let bin_path_ref_nu = render_nu_path_ref ( & bin_path_ref) ;
781+ ENV_TEMPLATE_NU
782+ . replace ( "__VP_HOME__" , & home_path_ref_nu)
783+ . replace ( "__VP_BIN__" , & bin_path_ref_nu)
757784 }
758785 EnvShell :: Powershell => {
759786 // PowerShell uses the actual absolute path (not $HOME-relative)
787+ let home_path_win = vite_plus_home. as_path ( ) . display ( ) . to_string ( ) ;
760788 let bin_path_win = bin_path. as_path ( ) . display ( ) . to_string ( ) ;
761- ENV_TEMPLATE_PS1 . replace ( "__VP_BIN_WIN__" , & bin_path_win)
789+ ENV_TEMPLATE_PS1
790+ . replace ( "__VP_HOME_WIN__" , & home_path_win)
791+ . replace ( "__VP_BIN_WIN__" , & bin_path_win)
762792 }
763793 }
764794}
@@ -919,13 +949,16 @@ mod tests {
919949 #[ tokio:: test]
920950 async fn test_create_env_files_replaces_placeholder_with_home_relative_path ( ) {
921951 let temp_dir = TempDir :: new ( ) . unwrap ( ) ;
922- let home = AbsolutePathBuf :: new ( temp_dir. path ( ) . to_path_buf ( ) ) . unwrap ( ) ;
952+ let home = AbsolutePathBuf :: new ( temp_dir. path ( ) . join ( "vp_home" ) ) . unwrap ( ) ;
923953 let _guard = home_guard ( temp_dir. path ( ) ) ;
954+ tokio:: fs:: create_dir_all ( & home) . await . unwrap ( ) ;
924955
925956 create_env_files ( & home) . await . unwrap ( ) ;
926957
927958 let env_content = tokio:: fs:: read_to_string ( home. join ( "env" ) ) . await . unwrap ( ) ;
928959 let fish_content = tokio:: fs:: read_to_string ( home. join ( "env.fish" ) ) . await . unwrap ( ) ;
960+ let nu_content = tokio:: fs:: read_to_string ( home. join ( "env.nu" ) ) . await . unwrap ( ) ;
961+ let ps1_content = tokio:: fs:: read_to_string ( home. join ( "env.ps1" ) ) . await . unwrap ( ) ;
929962
930963 // Placeholder should be fully replaced
931964 assert ! (
@@ -936,15 +969,45 @@ mod tests {
936969 !fish_content. contains( "__VP_BIN__" ) ,
937970 "env.fish file should not contain __VP_BIN__ placeholder"
938971 ) ;
972+ assert ! (
973+ !env_content. contains( "__VP_HOME__" ) && !fish_content. contains( "__VP_HOME__" ) ,
974+ "env files should not contain __VP_HOME__ placeholder"
975+ ) ;
976+ assert ! (
977+ !nu_content. contains( "__VP_HOME__" ) && !ps1_content. contains( "__VP_HOME_WIN__" ) ,
978+ "env files should not contain VP_HOME placeholders"
979+ ) ;
939980
940981 // Should use $HOME-relative path since install dir is under HOME
941982 assert ! (
942- env_content. contains( "$HOME/bin" ) ,
943- "env file should reference $HOME/bin, got: {env_content}"
983+ env_content. contains( "$HOME/vp_home/bin" ) ,
984+ "env file should reference $HOME/vp_home/bin, got: {env_content}"
985+ ) ;
986+ assert ! (
987+ fish_content. contains( "$HOME/vp_home/bin" ) ,
988+ "env.fish file should reference $HOME/vp_home/bin, got: {fish_content}"
989+ ) ;
990+ assert ! (
991+ env_content. contains( "export VP_HOME=\" $HOME/vp_home\" " ) ,
992+ "env file should export VP_HOME, got: {env_content}"
944993 ) ;
945994 assert ! (
946- fish_content. contains( "$HOME/bin" ) ,
947- "env.fish file should reference $HOME/bin, got: {fish_content}"
995+ fish_content. contains( "set -gx VP_HOME \" $HOME/vp_home\" " ) ,
996+ "env.fish file should export VP_HOME, got: {fish_content}"
997+ ) ;
998+ assert ! (
999+ nu_content. contains( "$env.VP_HOME = (\" ~/vp_home\" | path expand --no-symlink)" ) ,
1000+ "env.nu file should set home-relative VP_HOME, got: {nu_content}"
1001+ ) ;
1002+ assert ! (
1003+ nu_content. contains( "~/vp_home/bin" ) ,
1004+ "env.nu file should reference ~/vp_home/bin, got: {nu_content}"
1005+ ) ;
1006+
1007+ let expected_home = home. as_path ( ) . display ( ) . to_string ( ) ;
1008+ assert ! (
1009+ ps1_content. contains( & format!( "$env:VP_HOME = \" {expected_home}\" " ) ) ,
1010+ "env.ps1 file should set VP_HOME, got: {ps1_content}"
9481011 ) ;
9491012 }
9501013
@@ -963,6 +1026,7 @@ mod tests {
9631026 // Should use absolute path since install dir is not under HOME
9641027 let expected_bin = home. join ( "bin" ) ;
9651028 let expected_str = expected_bin. as_path ( ) . display ( ) . to_string ( ) . replace ( '\\' , "/" ) ;
1029+ let expected_home = home. as_path ( ) . display ( ) . to_string ( ) . replace ( '\\' , "/" ) ;
9661030 assert ! (
9671031 env_content. contains( & expected_str) ,
9681032 "env file should use absolute path {expected_str}, got: {env_content}"
@@ -971,6 +1035,14 @@ mod tests {
9711035 fish_content. contains( & expected_str) ,
9721036 "env.fish file should use absolute path {expected_str}, got: {fish_content}"
9731037 ) ;
1038+ assert ! (
1039+ env_content. contains( & format!( "export VP_HOME=\" {expected_home}\" " ) ) ,
1040+ "env file should export absolute VP_HOME {expected_home}, got: {env_content}"
1041+ ) ;
1042+ assert ! (
1043+ fish_content. contains( & format!( "set -gx VP_HOME \" {expected_home}\" " ) ) ,
1044+ "env.fish file should export absolute VP_HOME {expected_home}, got: {fish_content}"
1045+ ) ;
9741046
9751047 // Should NOT use $HOME-relative path
9761048 assert ! ( !env_content. contains( "$HOME/bin" ) , "env file should not reference $HOME/bin" ) ;
@@ -1120,6 +1192,27 @@ mod tests {
11201192 ) ;
11211193 }
11221194
1195+ #[ tokio:: test]
1196+ async fn test_create_env_files_cmd_wrapper_sets_vp_home_before_env_use ( ) {
1197+ 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 ( ) ;
1202+
1203+ create_env_files ( & home) . await . unwrap ( ) ;
1204+
1205+ let cmd_content = tokio:: fs:: read_to_string ( bin_dir. join ( "vp-use.cmd" ) ) . await . unwrap ( ) ;
1206+ assert ! (
1207+ cmd_content. contains( "set VP_HOME=%~dp0..\r \n for /f" ) ,
1208+ "vp-use.cmd should set VP_HOME before invoking vp env use, got: {cmd_content}"
1209+ ) ;
1210+ assert ! (
1211+ cmd_content. contains( "%~dp0..\\ current\\ bin\\ vp.exe env use %*" ) ,
1212+ "vp-use.cmd should invoke the install-local vp.exe"
1213+ ) ;
1214+ }
1215+
11231216 #[ tokio:: test]
11241217 async fn test_execute_env_only_creates_home_dir_and_env_files ( ) {
11251218 let temp_dir = TempDir :: new ( ) . unwrap ( ) ;
0 commit comments