@@ -368,7 +368,7 @@ pub(crate) const CORE_SHIMS: &[&str] = &["node", "npm", "npx", "vp"];
368368/// Create a shim for a package binary.
369369///
370370/// On Unix: Creates a symlink to ../current/bin/vp
371- /// On Windows: Creates a .cmd wrapper that calls `vp env exec <bin_name>`
371+ /// On Windows: Creates a trampoline .exe that forwards to vp.exe
372372async fn create_package_shim (
373373 bin_dir : & vite_path:: AbsolutePath ,
374374 bin_name : & str ,
@@ -406,40 +406,25 @@ async fn create_package_shim(
406406
407407 #[ cfg( windows) ]
408408 {
409- let cmd_path = bin_dir. join ( format ! ( "{}.cmd " , bin_name) ) ;
409+ let shim_path = bin_dir. join ( format ! ( "{}.exe " , bin_name) ) ;
410410
411411 // Skip if already exists (e.g., re-installing the same package)
412- if tokio:: fs:: try_exists ( & cmd_path ) . await . unwrap_or ( false ) {
412+ if tokio:: fs:: try_exists ( & shim_path ) . await . unwrap_or ( false ) {
413413 return Ok ( ( ) ) ;
414414 }
415415
416- // Create .cmd wrapper that calls vp env exec <bin_name>.
417- // Use `--` so args like `--help` are forwarded to the package binary,
418- // not consumed by clap while parsing `vp env exec`.
419- // Set VITE_PLUS_HOME using %~dp0.. which resolves to the parent of bin/
420- // This ensures the vp binary knows its home directory
421- let wrapper_content = format ! (
422- "@echo off\r \n set VITE_PLUS_HOME=%~dp0..\r \n set VITE_PLUS_SHIM_WRAPPER=1\r \n \" %VITE_PLUS_HOME%\\ current\\ bin\\ vp.exe\" env exec {} -- %*\r \n exit /b %ERRORLEVEL%\r \n " ,
423- bin_name
424- ) ;
425- tokio:: fs:: write ( & cmd_path, wrapper_content) . await ?;
416+ // Copy the trampoline binary as <bin_name>.exe.
417+ // The trampoline detects the tool name from its own filename and sets
418+ // VITE_PLUS_SHIM_TOOL env var before spawning vp.exe.
419+ let trampoline_src = super :: setup:: get_trampoline_path ( ) ?;
420+ tokio:: fs:: copy ( trampoline_src. as_path ( ) , & shim_path) . await ?;
426421
427- // Also create shell script for Git Bash (bin_name without extension)
428- // Uses explicit "vp env exec <bin_name>" instead of symlink+argv[0] because
429- // Windows symlinks require admin privileges
430- let sh_path = bin_dir. join ( bin_name) ;
431- let sh_content = format ! (
432- r#"#!/bin/sh
433- VITE_PLUS_HOME="$(dirname "$(dirname "$(readlink -f "$0" 2>/dev/null || echo "$0")")")"
434- export VITE_PLUS_HOME
435- export VITE_PLUS_SHIM_WRAPPER=1
436- exec "$VITE_PLUS_HOME/current/bin/vp.exe" env exec {} -- "$@"
437- "# ,
438- bin_name
439- ) ;
440- tokio:: fs:: write ( & sh_path, sh_content) . await ?;
422+ // Remove legacy .cmd and shell script wrappers from previous versions.
423+ // In Git Bash/MSYS, the extensionless script takes precedence over .exe,
424+ // so leftover wrappers would bypass the trampoline.
425+ super :: setup:: cleanup_legacy_windows_shim ( bin_dir, bin_name) . await ;
441426
442- tracing:: debug!( "Created package shim wrappers for {} (.cmd and shell script) " , bin_name ) ;
427+ tracing:: debug!( "Created package trampoline shim {:?} " , shim_path ) ;
443428 }
444429
445430 Ok ( ( ) )
@@ -466,13 +451,17 @@ async fn remove_package_shim(
466451
467452 #[ cfg( windows) ]
468453 {
469- // Remove .cmd wrapper
454+ // Remove trampoline .exe shim
455+ let exe_path = bin_dir. join ( format ! ( "{}.exe" , bin_name) ) ;
456+ if tokio:: fs:: try_exists ( & exe_path) . await . unwrap_or ( false ) {
457+ tokio:: fs:: remove_file ( & exe_path) . await ?;
458+ }
459+
460+ // Also remove legacy .cmd wrapper and shell script from previous versions
470461 let cmd_path = bin_dir. join ( format ! ( "{}.cmd" , bin_name) ) ;
471462 if tokio:: fs:: try_exists ( & cmd_path) . await . unwrap_or ( false ) {
472463 tokio:: fs:: remove_file ( & cmd_path) . await ?;
473464 }
474-
475- // Also remove shell script (for Git Bash)
476465 let sh_path = bin_dir. join ( bin_name) ;
477466 if tokio:: fs:: try_exists ( & sh_path) . await . unwrap_or ( false ) {
478467 tokio:: fs:: remove_file ( & sh_path) . await ?;
@@ -486,13 +475,42 @@ async fn remove_package_shim(
486475mod tests {
487476 use super :: * ;
488477
478+ /// RAII guard that sets `VITE_PLUS_TRAMPOLINE_PATH` to a fake binary on creation
479+ /// and clears it on drop. Ensures cleanup even on test panics.
480+ #[ cfg( windows) ]
481+ struct FakeTrampolineGuard ;
482+
483+ #[ cfg( windows) ]
484+ impl FakeTrampolineGuard {
485+ fn new ( dir : & std:: path:: Path ) -> Self {
486+ let trampoline = dir. join ( "vp-shim.exe" ) ;
487+ std:: fs:: write ( & trampoline, b"fake-trampoline" ) . unwrap ( ) ;
488+ unsafe {
489+ std:: env:: set_var ( "VITE_PLUS_TRAMPOLINE_PATH" , & trampoline) ;
490+ }
491+ Self
492+ }
493+ }
494+
495+ #[ cfg( windows) ]
496+ impl Drop for FakeTrampolineGuard {
497+ fn drop ( & mut self ) {
498+ unsafe {
499+ std:: env:: remove_var ( "VITE_PLUS_TRAMPOLINE_PATH" ) ;
500+ }
501+ }
502+ }
503+
489504 #[ tokio:: test]
505+ #[ cfg_attr( windows, serial_test:: serial) ]
490506 async fn test_create_package_shim_creates_bin_dir ( ) {
491507 use tempfile:: TempDir ;
492508 use vite_path:: AbsolutePathBuf ;
493509
494510 // Create a temp directory but don't create the bin subdirectory
495511 let temp_dir = TempDir :: new ( ) . unwrap ( ) ;
512+ #[ cfg( windows) ]
513+ let _guard = FakeTrampolineGuard :: new ( temp_dir. path ( ) ) ;
496514 let bin_dir = temp_dir. path ( ) . join ( "bin" ) ;
497515 let bin_dir = AbsolutePathBuf :: new ( bin_dir) . unwrap ( ) ;
498516
@@ -505,7 +523,7 @@ mod tests {
505523 // Verify bin directory was created
506524 assert ! ( bin_dir. as_path( ) . exists( ) ) ;
507525
508- // Verify shim file was created (on Windows, shims have .cmd extension)
526+ // Verify shim file was created (on Windows, shims have .exe extension)
509527 // On Unix, symlinks may be broken (target doesn't exist), so use symlink_metadata
510528 #[ cfg( unix) ]
511529 {
@@ -517,7 +535,7 @@ mod tests {
517535 }
518536 #[ cfg( windows) ]
519537 {
520- let shim_path = bin_dir. join ( "test-shim.cmd " ) ;
538+ let shim_path = bin_dir. join ( "test-shim.exe " ) ;
521539 assert ! ( shim_path. as_path( ) . exists( ) ) ;
522540 }
523541 }
@@ -537,16 +555,19 @@ mod tests {
537555 #[ cfg( unix) ]
538556 let shim_path = bin_dir. join ( "node" ) ;
539557 #[ cfg( windows) ]
540- let shim_path = bin_dir. join ( "node.cmd " ) ;
558+ let shim_path = bin_dir. join ( "node.exe " ) ;
541559 assert ! ( !shim_path. as_path( ) . exists( ) ) ;
542560 }
543561
544562 #[ tokio:: test]
563+ #[ cfg_attr( windows, serial_test:: serial) ]
545564 async fn test_remove_package_shim_removes_shim ( ) {
546565 use tempfile:: TempDir ;
547566 use vite_path:: AbsolutePathBuf ;
548567
549568 let temp_dir = TempDir :: new ( ) . unwrap ( ) ;
569+ #[ cfg( windows) ]
570+ let _guard = FakeTrampolineGuard :: new ( temp_dir. path ( ) ) ;
550571 let bin_dir = AbsolutePathBuf :: new ( temp_dir. path ( ) . to_path_buf ( ) ) . unwrap ( ) ;
551572
552573 // Create a shim
@@ -573,7 +594,7 @@ mod tests {
573594 }
574595 #[ cfg( windows) ]
575596 {
576- let shim_path = bin_dir. join ( "tsc.cmd " ) ;
597+ let shim_path = bin_dir. join ( "tsc.exe " ) ;
577598 assert ! ( shim_path. as_path( ) . exists( ) , "Shim should exist after creation" ) ;
578599
579600 // Remove the shim
@@ -597,12 +618,15 @@ mod tests {
597618 }
598619
599620 #[ tokio:: test]
621+ #[ cfg_attr( windows, serial_test:: serial) ]
600622 async fn test_uninstall_removes_shims_from_metadata ( ) {
601623 use tempfile:: TempDir ;
602624 use vite_path:: AbsolutePathBuf ;
603625
604626 let temp_dir = TempDir :: new ( ) . unwrap ( ) ;
605627 let temp_path = temp_dir. path ( ) . to_path_buf ( ) ;
628+ #[ cfg( windows) ]
629+ let _guard = FakeTrampolineGuard :: new ( & temp_path) ;
606630 let _guard = vite_shared:: EnvConfig :: test_guard (
607631 vite_shared:: EnvConfig :: for_test_with_home ( & temp_path) ,
608632 ) ;
@@ -630,10 +654,10 @@ mod tests {
630654 }
631655 #[ cfg( windows) ]
632656 {
633- assert ! ( bin_dir. join( "tsc.cmd " ) . as_path( ) . exists( ) , "tsc.cmd shim should exist" ) ;
657+ assert ! ( bin_dir. join( "tsc.exe " ) . as_path( ) . exists( ) , "tsc.exe shim should exist" ) ;
634658 assert ! (
635- bin_dir. join( "tsserver.cmd " ) . as_path( ) . exists( ) ,
636- "tsserver.cmd shim should exist"
659+ bin_dir. join( "tsserver.exe " ) . as_path( ) . exists( ) ,
660+ "tsserver.exe shim should exist"
637661 ) ;
638662 }
639663
@@ -674,10 +698,10 @@ mod tests {
674698 }
675699 #[ cfg( windows) ]
676700 {
677- assert ! ( !bin_dir. join( "tsc.cmd " ) . as_path( ) . exists( ) , "tsc.cmd shim should be removed" ) ;
701+ assert ! ( !bin_dir. join( "tsc.exe " ) . as_path( ) . exists( ) , "tsc.exe shim should be removed" ) ;
678702 assert ! (
679- !bin_dir. join( "tsserver.cmd " ) . as_path( ) . exists( ) ,
680- "tsserver.cmd shim should be removed"
703+ !bin_dir. join( "tsserver.exe " ) . as_path( ) . exists( ) ,
704+ "tsserver.exe shim should be removed"
681705 ) ;
682706 }
683707 }
0 commit comments