11use directories:: BaseDirs ;
22use vite_path:: { AbsolutePathBuf , current_dir} ;
3+ use which:: which;
34
45use crate :: EnvConfig ;
56
@@ -8,7 +9,9 @@ const VITE_PLUS_HOME_DIR: &str = ".vite-plus";
89
910/// Get the vite-plus home directory.
1011///
11- /// Uses `EnvConfig::get().vite_plus_home` if set, otherwise defaults to `~/.vite-plus`.
12+ /// Uses `EnvConfig::get().vite_plus_home` if set,
13+ /// or the `node` executable's grandparent directory if it ends with `.vite-plus`,
14+ /// otherwise defaults to `~/.vite-plus`.
1215/// Falls back to `$CWD/.vite-plus` if the home directory cannot be determined.
1316pub fn get_vite_plus_home ( ) -> std:: io:: Result < AbsolutePathBuf > {
1417 let config = EnvConfig :: get ( ) ;
@@ -18,6 +21,16 @@ pub fn get_vite_plus_home() -> std::io::Result<AbsolutePathBuf> {
1821 }
1922 }
2023
24+ // Get from `node` executable file's grandparent directory (~/.vite-plus/bin/node)
25+ // For the case where `$HOME` is overridden
26+ if let Ok ( path) = which ( "node" )
27+ && let Some ( parent) = path. parent ( )
28+ && let Some ( grandparent) = parent. parent ( )
29+ && grandparent. ends_with ( VITE_PLUS_HOME_DIR )
30+ {
31+ return Ok ( AbsolutePathBuf :: new ( grandparent. to_path_buf ( ) ) . unwrap ( ) ) ;
32+ }
33+
2134 // Default to ~/.vite-plus
2235 match BaseDirs :: new ( ) {
2336 Some ( dirs) => {
@@ -49,4 +62,63 @@ mod tests {
4962 assert_eq ! ( home. as_path( ) , temp_dir. as_path( ) ) ;
5063 } ) ;
5164 }
65+
66+ #[ test]
67+ fn test_get_vite_plus_without_home ( ) {
68+ use std:: path:: PathBuf ;
69+
70+ // Create a temp directory structure: /tmp/xxx/.vite-plus/bin/node
71+ let temp_dir = PathBuf :: from (
72+ std:: env:: temp_dir ( ) . join ( format ! ( "vp-test-node-path-{}" , std:: process:: id( ) ) ) ,
73+ ) ;
74+ let vite_plus_home = temp_dir. join ( ".vite-plus" ) ;
75+ let bin_dir = vite_plus_home. join ( "bin" ) ;
76+ std:: fs:: create_dir_all ( & bin_dir) . unwrap ( ) ;
77+
78+ // Create a fake node executable with platform-specific extension
79+ #[ cfg( windows) ]
80+ let node_path = bin_dir. join ( "node.exe" ) ;
81+ #[ cfg( not( windows) ) ]
82+ let node_path = bin_dir. join ( "node" ) ;
83+
84+ // Write minimal content - on Windows, the file just needs to exist with .exe extension
85+ // On Unix, we need a shebang and executable permissions
86+ #[ cfg( windows) ]
87+ std:: fs:: write ( & node_path, b"MZ" ) . unwrap ( ) ; // Minimal PE header for Windows
88+ #[ cfg( not( windows) ) ]
89+ {
90+ std:: fs:: write ( & node_path, "#!/bin/sh\n echo 'fake node'" ) . unwrap ( ) ;
91+ use std:: os:: unix:: fs:: PermissionsExt ;
92+ let mut perms = std:: fs:: metadata ( & node_path) . unwrap ( ) . permissions ( ) ;
93+ perms. set_mode ( 0o755 ) ;
94+ std:: fs:: set_permissions ( & node_path, perms) . unwrap ( ) ;
95+ }
96+
97+ // Set PATH to include the fake node directory FIRST (prepended)
98+ let original_path = std:: env:: var ( "PATH" ) . unwrap_or_default ( ) ;
99+ #[ cfg( windows) ]
100+ let path_separator = ';' ;
101+ #[ cfg( not( windows) ) ]
102+ let path_separator = ':' ;
103+ let new_path = format ! ( "{}{}{}" , bin_dir. display( ) , path_separator, original_path) ;
104+ // SAFETY: restore PATH after test
105+ unsafe {
106+ std:: env:: set_var ( "PATH" , & new_path) ;
107+ }
108+
109+ // Clear any existing VITE_PLUS_HOME env var by using a test config without it
110+ EnvConfig :: test_scope ( EnvConfig :: for_test ( ) , || {
111+ // Test: get_vite_plus_home should return /tmp/xxx/.vite-plus
112+ let home = get_vite_plus_home ( ) . unwrap ( ) ;
113+ assert_eq ! ( home. as_path( ) , vite_plus_home. as_path( ) ) ;
114+ } ) ;
115+
116+ // SAFETY: restore PATH after test
117+ unsafe {
118+ std:: env:: set_var ( "PATH" , original_path) ;
119+ }
120+
121+ // Cleanup
122+ let _ = std:: fs:: remove_dir_all ( & temp_dir) ;
123+ }
52124}
0 commit comments