@@ -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 ?;
426-
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 ?;
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 ?;
421+
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,16 +451,15 @@ async fn remove_package_shim(
466451
467452 #[ cfg( windows) ]
468453 {
469- // Remove .cmd wrapper
470- let cmd_path = bin_dir. join ( format ! ( "{}.cmd" , bin_name) ) ;
471- if tokio:: fs:: try_exists ( & cmd_path) . await . unwrap_or ( false ) {
472- tokio:: fs:: remove_file ( & cmd_path) . await ?;
473- }
474-
475- // Also remove shell script (for Git Bash)
476- let sh_path = bin_dir. join ( bin_name) ;
477- if tokio:: fs:: try_exists ( & sh_path) . await . unwrap_or ( false ) {
478- tokio:: fs:: remove_file ( & sh_path) . await ?;
454+ // Remove trampoline .exe shim and legacy .cmd / shell script wrappers.
455+ // Best-effort: ignore NotFound errors for files that don't exist.
456+ for suffix in & [ ".exe" , ".cmd" , "" ] {
457+ let path = if suffix. is_empty ( ) {
458+ bin_dir. join ( bin_name)
459+ } else {
460+ bin_dir. join ( format ! ( "{bin_name}{suffix}" ) )
461+ } ;
462+ let _ = tokio:: fs:: remove_file ( & path) . await ;
479463 }
480464 }
481465
@@ -486,13 +470,42 @@ async fn remove_package_shim(
486470mod tests {
487471 use super :: * ;
488472
473+ /// RAII guard that sets `VITE_PLUS_TRAMPOLINE_PATH` to a fake binary on creation
474+ /// and clears it on drop. Ensures cleanup even on test panics.
475+ #[ cfg( windows) ]
476+ struct FakeTrampolineGuard ;
477+
478+ #[ cfg( windows) ]
479+ impl FakeTrampolineGuard {
480+ fn new ( dir : & std:: path:: Path ) -> Self {
481+ let trampoline = dir. join ( "vp-shim.exe" ) ;
482+ std:: fs:: write ( & trampoline, b"fake-trampoline" ) . unwrap ( ) ;
483+ unsafe {
484+ std:: env:: set_var ( vite_shared:: env_vars:: VITE_PLUS_TRAMPOLINE_PATH , & trampoline) ;
485+ }
486+ Self
487+ }
488+ }
489+
490+ #[ cfg( windows) ]
491+ impl Drop for FakeTrampolineGuard {
492+ fn drop ( & mut self ) {
493+ unsafe {
494+ std:: env:: remove_var ( vite_shared:: env_vars:: VITE_PLUS_TRAMPOLINE_PATH ) ;
495+ }
496+ }
497+ }
498+
489499 #[ tokio:: test]
500+ #[ cfg_attr( windows, serial_test:: serial) ]
490501 async fn test_create_package_shim_creates_bin_dir ( ) {
491502 use tempfile:: TempDir ;
492503 use vite_path:: AbsolutePathBuf ;
493504
494505 // Create a temp directory but don't create the bin subdirectory
495506 let temp_dir = TempDir :: new ( ) . unwrap ( ) ;
507+ #[ cfg( windows) ]
508+ let _guard = FakeTrampolineGuard :: new ( temp_dir. path ( ) ) ;
496509 let bin_dir = temp_dir. path ( ) . join ( "bin" ) ;
497510 let bin_dir = AbsolutePathBuf :: new ( bin_dir) . unwrap ( ) ;
498511
@@ -505,7 +518,7 @@ mod tests {
505518 // Verify bin directory was created
506519 assert ! ( bin_dir. as_path( ) . exists( ) ) ;
507520
508- // Verify shim file was created (on Windows, shims have .cmd extension)
521+ // Verify shim file was created (on Windows, shims have .exe extension)
509522 // On Unix, symlinks may be broken (target doesn't exist), so use symlink_metadata
510523 #[ cfg( unix) ]
511524 {
@@ -517,7 +530,7 @@ mod tests {
517530 }
518531 #[ cfg( windows) ]
519532 {
520- let shim_path = bin_dir. join ( "test-shim.cmd " ) ;
533+ let shim_path = bin_dir. join ( "test-shim.exe " ) ;
521534 assert ! ( shim_path. as_path( ) . exists( ) ) ;
522535 }
523536 }
@@ -537,16 +550,19 @@ mod tests {
537550 #[ cfg( unix) ]
538551 let shim_path = bin_dir. join ( "node" ) ;
539552 #[ cfg( windows) ]
540- let shim_path = bin_dir. join ( "node.cmd " ) ;
553+ let shim_path = bin_dir. join ( "node.exe " ) ;
541554 assert ! ( !shim_path. as_path( ) . exists( ) ) ;
542555 }
543556
544557 #[ tokio:: test]
558+ #[ cfg_attr( windows, serial_test:: serial) ]
545559 async fn test_remove_package_shim_removes_shim ( ) {
546560 use tempfile:: TempDir ;
547561 use vite_path:: AbsolutePathBuf ;
548562
549563 let temp_dir = TempDir :: new ( ) . unwrap ( ) ;
564+ #[ cfg( windows) ]
565+ let _guard = FakeTrampolineGuard :: new ( temp_dir. path ( ) ) ;
550566 let bin_dir = AbsolutePathBuf :: new ( temp_dir. path ( ) . to_path_buf ( ) ) . unwrap ( ) ;
551567
552568 // Create a shim
@@ -573,7 +589,7 @@ mod tests {
573589 }
574590 #[ cfg( windows) ]
575591 {
576- let shim_path = bin_dir. join ( "tsc.cmd " ) ;
592+ let shim_path = bin_dir. join ( "tsc.exe " ) ;
577593 assert ! ( shim_path. as_path( ) . exists( ) , "Shim should exist after creation" ) ;
578594
579595 // Remove the shim
@@ -597,12 +613,15 @@ mod tests {
597613 }
598614
599615 #[ tokio:: test]
616+ #[ cfg_attr( windows, serial_test:: serial) ]
600617 async fn test_uninstall_removes_shims_from_metadata ( ) {
601618 use tempfile:: TempDir ;
602619 use vite_path:: AbsolutePathBuf ;
603620
604621 let temp_dir = TempDir :: new ( ) . unwrap ( ) ;
605622 let temp_path = temp_dir. path ( ) . to_path_buf ( ) ;
623+ #[ cfg( windows) ]
624+ let _guard = FakeTrampolineGuard :: new ( & temp_path) ;
606625 let _guard = vite_shared:: EnvConfig :: test_guard (
607626 vite_shared:: EnvConfig :: for_test_with_home ( & temp_path) ,
608627 ) ;
@@ -630,10 +649,10 @@ mod tests {
630649 }
631650 #[ cfg( windows) ]
632651 {
633- assert ! ( bin_dir. join( "tsc.cmd " ) . as_path( ) . exists( ) , "tsc.cmd shim should exist" ) ;
652+ assert ! ( bin_dir. join( "tsc.exe " ) . as_path( ) . exists( ) , "tsc.exe shim should exist" ) ;
634653 assert ! (
635- bin_dir. join( "tsserver.cmd " ) . as_path( ) . exists( ) ,
636- "tsserver.cmd shim should exist"
654+ bin_dir. join( "tsserver.exe " ) . as_path( ) . exists( ) ,
655+ "tsserver.exe shim should exist"
637656 ) ;
638657 }
639658
@@ -674,10 +693,10 @@ mod tests {
674693 }
675694 #[ cfg( windows) ]
676695 {
677- assert ! ( !bin_dir. join( "tsc.cmd " ) . as_path( ) . exists( ) , "tsc.cmd shim should be removed" ) ;
696+ assert ! ( !bin_dir. join( "tsc.exe " ) . as_path( ) . exists( ) , "tsc.exe shim should be removed" ) ;
678697 assert ! (
679- !bin_dir. join( "tsserver.cmd " ) . as_path( ) . exists( ) ,
680- "tsserver.cmd shim should be removed"
698+ !bin_dir. join( "tsserver.exe " ) . as_path( ) . exists( ) ,
699+ "tsserver.exe shim should be removed"
681700 ) ;
682701 }
683702 }
0 commit comments