@@ -60,6 +60,7 @@ async fn execute_direct_subcommand(
6060 . map ( |( k, v) | ( Arc :: from ( k. as_os_str ( ) ) , Arc :: from ( v. as_os_str ( ) ) ) )
6161 . collect ( ) ,
6262 ) ;
63+ let envs = envs_with_explicit_package_manager_path ( cwd, envs) . await ?;
6364
6465 let status = match subcommand {
6566 SynthesizableSubcommand :: Check {
@@ -113,6 +114,57 @@ async fn execute_direct_subcommand(
113114 Ok ( status)
114115}
115116
117+ fn is_path_env_key ( key : & OsStr ) -> bool {
118+ if cfg ! ( windows) { key. eq_ignore_ascii_case ( "PATH" ) } else { key == "PATH" }
119+ }
120+
121+ fn prepend_to_env_path (
122+ envs : & Arc < FxHashMap < Arc < OsStr > , Arc < OsStr > > > ,
123+ bin_prefix : & AbsolutePath ,
124+ ) -> Result < Arc < FxHashMap < Arc < OsStr > , Arc < OsStr > > > , Error > {
125+ let path_key = envs
126+ . keys ( )
127+ . find ( |key| is_path_env_key ( key. as_ref ( ) ) )
128+ . cloned ( )
129+ . unwrap_or_else ( || Arc :: from ( OsStr :: new ( "PATH" ) ) ) ;
130+ let current_path =
131+ envs. get ( & path_key) . map_or_else ( Default :: default, |path| path. to_os_string ( ) ) ;
132+ let paths = env:: split_paths ( & current_path) . collect :: < Vec < _ > > ( ) ;
133+
134+ if paths. first ( ) . is_some_and ( |path| path == bin_prefix. as_path ( ) ) {
135+ return Ok ( Arc :: clone ( envs) ) ;
136+ }
137+
138+ let new_path = env:: join_paths (
139+ std:: iter:: once ( bin_prefix. as_path ( ) . to_path_buf ( ) ) . chain ( paths. into_iter ( ) ) ,
140+ )
141+ . map_err ( |error| Error :: Anyhow ( anyhow:: Error :: new ( error) ) ) ?;
142+
143+ let mut envs = FxHashMap :: clone ( envs) ;
144+ envs. insert ( path_key, Arc :: from ( new_path. as_os_str ( ) ) ) ;
145+ Ok ( Arc :: new ( envs) )
146+ }
147+
148+ async fn envs_with_explicit_package_manager_path (
149+ cwd : & AbsolutePath ,
150+ envs : Arc < FxHashMap < Arc < OsStr > , Arc < OsStr > > > ,
151+ ) -> Result < Arc < FxHashMap < Arc < OsStr > , Arc < OsStr > > > , Error > {
152+ let Some ( resolution) =
153+ vite_install:: package_manager:: resolve_package_manager_from_package_json ( cwd) ?
154+ else {
155+ return Ok ( envs) ;
156+ } ;
157+
158+ let ( install_dir, _, _) = vite_install:: download_package_manager (
159+ resolution. package_manager_type ,
160+ & resolution. version ,
161+ resolution. hash . as_deref ( ) ,
162+ )
163+ . await ?;
164+
165+ prepend_to_env_path ( & envs, & install_dir. join ( "bin" ) )
166+ }
167+
116168/// Execute a vite-task command (run, cache) through Session.
117169async fn execute_vite_task_command (
118170 command : vite_task:: Command ,
@@ -224,10 +276,80 @@ async fn execute_pm_command(
224276
225277#[ cfg( test) ]
226278mod tests {
227- use std:: path:: PathBuf ;
279+ use std:: {
280+ ffi:: OsStr ,
281+ fs,
282+ path:: PathBuf ,
283+ sync:: Arc ,
284+ time:: { SystemTime , UNIX_EPOCH } ,
285+ } ;
228286
287+ use rustc_hash:: FxHashMap ;
288+ use vite_path:: AbsolutePathBuf ;
229289 use vite_task:: config:: UserRunConfig ;
230290
291+ use super :: { envs_with_explicit_package_manager_path, prepend_to_env_path} ;
292+
293+ fn envs_with_path ( path : & std:: ffi:: OsStr ) -> Arc < FxHashMap < Arc < OsStr > , Arc < OsStr > > > {
294+ Arc :: new ( FxHashMap :: from_iter ( [ ( Arc :: from ( OsStr :: new ( "PATH" ) ) , Arc :: from ( path) ) ] ) )
295+ }
296+
297+ #[ test]
298+ fn prepends_package_manager_bin_to_env_path ( ) {
299+ let cwd = std:: env:: current_dir ( ) . expect ( "current_dir should exist" ) ;
300+ let old_bin = cwd. join ( "old-bin" ) ;
301+ let pm_bin = AbsolutePathBuf :: new ( cwd. join ( "pm-bin" ) ) . expect ( "pm bin should be absolute" ) ;
302+ let original_path = std:: env:: join_paths ( [ old_bin. as_path ( ) ] ) . expect ( "valid PATH" ) ;
303+ let envs = envs_with_path ( original_path. as_os_str ( ) ) ;
304+
305+ let updated = prepend_to_env_path ( & envs, & pm_bin) . expect ( "PATH should update" ) ;
306+ let path_value = updated. get ( OsStr :: new ( "PATH" ) ) . expect ( "PATH should exist" ) ;
307+ let paths = std:: env:: split_paths ( path_value) . collect :: < Vec < _ > > ( ) ;
308+
309+ assert_eq ! ( paths. first( ) . map( std:: path:: PathBuf :: as_path) , Some ( pm_bin. as_path( ) ) ) ;
310+ assert_eq ! ( paths. get( 1 ) . map( std:: path:: PathBuf :: as_path) , Some ( old_bin. as_path( ) ) ) ;
311+ }
312+
313+ #[ test]
314+ fn does_not_duplicate_package_manager_bin_when_already_first ( ) {
315+ let cwd = std:: env:: current_dir ( ) . expect ( "current_dir should exist" ) ;
316+ let pm_bin = AbsolutePathBuf :: new ( cwd. join ( "pm-bin" ) ) . expect ( "pm bin should be absolute" ) ;
317+ let original_path = std:: env:: join_paths ( [ pm_bin. as_path ( ) ] ) . expect ( "valid PATH" ) ;
318+ let envs = envs_with_path ( original_path. as_os_str ( ) ) ;
319+
320+ let updated = prepend_to_env_path ( & envs, & pm_bin) . expect ( "PATH should update" ) ;
321+ let path_value = updated. get ( OsStr :: new ( "PATH" ) ) . expect ( "PATH should exist" ) ;
322+ let paths = std:: env:: split_paths ( path_value) . collect :: < Vec < _ > > ( ) ;
323+
324+ assert_eq ! ( paths, vec![ pm_bin. as_path( ) . to_path_buf( ) ] ) ;
325+ }
326+
327+ #[ tokio:: test]
328+ async fn ignores_lockfile_without_explicit_package_manager ( ) {
329+ let suffix =
330+ SystemTime :: now ( ) . duration_since ( UNIX_EPOCH ) . expect ( "time should be valid" ) . as_nanos ( ) ;
331+ let temp_dir = std:: env:: temp_dir ( ) . join ( format ! ( "vite-plus-no-pm-{suffix}" ) ) ;
332+ fs:: create_dir_all ( & temp_dir) . expect ( "temp dir should be created" ) ;
333+ fs:: write ( temp_dir. join ( "package.json" ) , r#"{"name":"fixture"}"# )
334+ . expect ( "package.json should be written" ) ;
335+ fs:: write ( temp_dir. join ( "pnpm-lock.yaml" ) , "lockfileVersion: '9.0'\n " )
336+ . expect ( "lockfile should be written" ) ;
337+ let cwd = AbsolutePathBuf :: new ( temp_dir. clone ( ) ) . expect ( "temp dir should be absolute" ) ;
338+ let original_path = std:: env:: join_paths ( [ temp_dir. join ( "old-bin" ) ] ) . expect ( "valid PATH" ) ;
339+ let envs = envs_with_path ( original_path. as_os_str ( ) ) ;
340+
341+ let updated = envs_with_explicit_package_manager_path ( & cwd, Arc :: clone ( & envs) )
342+ . await
343+ . expect ( "missing packageManager should not error" ) ;
344+
345+ assert_eq ! ( updated. get( OsStr :: new( "PATH" ) ) , envs. get( OsStr :: new( "PATH" ) ) ) ;
346+ assert_eq ! (
347+ fs:: read_to_string( temp_dir. join( "package.json" ) ) . expect( "package.json should exist" ) ,
348+ r#"{"name":"fixture"}"#
349+ ) ;
350+ fs:: remove_dir_all ( temp_dir) . expect ( "temp dir should be removed" ) ;
351+ }
352+
231353 #[ test]
232354 fn run_config_types_in_sync ( ) {
233355 // Remove \r for cross-platform consistency
0 commit comments