@@ -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,34 @@ async fn remove_package_shim(
486475mod tests {
487476 use super :: * ;
488477
478+ /// On Windows, create a fake trampoline binary and set the env var so
479+ /// `get_trampoline_path()` finds it instead of looking next to the test harness.
480+ #[ cfg( windows) ]
481+ fn setup_fake_trampoline ( dir : & std:: path:: Path ) {
482+ let trampoline = dir. join ( "vp-shim.exe" ) ;
483+ std:: fs:: write ( & trampoline, b"fake-trampoline" ) . unwrap ( ) ;
484+ unsafe {
485+ std:: env:: set_var ( "VITE_PLUS_TRAMPOLINE_PATH" , & trampoline) ;
486+ }
487+ }
488+
489+ /// Clean up the trampoline env var after a Windows test.
490+ #[ cfg( windows) ]
491+ fn cleanup_fake_trampoline ( ) {
492+ unsafe {
493+ std:: env:: remove_var ( "VITE_PLUS_TRAMPOLINE_PATH" ) ;
494+ }
495+ }
496+
489497 #[ tokio:: test]
490498 async fn test_create_package_shim_creates_bin_dir ( ) {
491499 use tempfile:: TempDir ;
492500 use vite_path:: AbsolutePathBuf ;
493501
494502 // Create a temp directory but don't create the bin subdirectory
495503 let temp_dir = TempDir :: new ( ) . unwrap ( ) ;
504+ #[ cfg( windows) ]
505+ setup_fake_trampoline ( temp_dir. path ( ) ) ;
496506 let bin_dir = temp_dir. path ( ) . join ( "bin" ) ;
497507 let bin_dir = AbsolutePathBuf :: new ( bin_dir) . unwrap ( ) ;
498508
@@ -501,11 +511,13 @@ mod tests {
501511
502512 // Create a shim - this should create the bin directory
503513 create_package_shim ( & bin_dir, "test-shim" , "test-package" ) . await . unwrap ( ) ;
514+ #[ cfg( windows) ]
515+ cleanup_fake_trampoline ( ) ;
504516
505517 // Verify bin directory was created
506518 assert ! ( bin_dir. as_path( ) . exists( ) ) ;
507519
508- // Verify shim file was created (on Windows, shims have .cmd extension)
520+ // Verify shim file was created (on Windows, shims have .exe extension)
509521 // On Unix, symlinks may be broken (target doesn't exist), so use symlink_metadata
510522 #[ cfg( unix) ]
511523 {
@@ -517,7 +529,7 @@ mod tests {
517529 }
518530 #[ cfg( windows) ]
519531 {
520- let shim_path = bin_dir. join ( "test-shim.cmd " ) ;
532+ let shim_path = bin_dir. join ( "test-shim.exe " ) ;
521533 assert ! ( shim_path. as_path( ) . exists( ) ) ;
522534 }
523535 }
@@ -537,7 +549,7 @@ mod tests {
537549 #[ cfg( unix) ]
538550 let shim_path = bin_dir. join ( "node" ) ;
539551 #[ cfg( windows) ]
540- let shim_path = bin_dir. join ( "node.cmd " ) ;
552+ let shim_path = bin_dir. join ( "node.exe " ) ;
541553 assert ! ( !shim_path. as_path( ) . exists( ) ) ;
542554 }
543555
@@ -547,6 +559,8 @@ mod tests {
547559 use vite_path:: AbsolutePathBuf ;
548560
549561 let temp_dir = TempDir :: new ( ) . unwrap ( ) ;
562+ #[ cfg( windows) ]
563+ setup_fake_trampoline ( temp_dir. path ( ) ) ;
550564 let bin_dir = AbsolutePathBuf :: new ( temp_dir. path ( ) . to_path_buf ( ) ) . unwrap ( ) ;
551565
552566 // Create a shim
@@ -573,7 +587,7 @@ mod tests {
573587 }
574588 #[ cfg( windows) ]
575589 {
576- let shim_path = bin_dir. join ( "tsc.cmd " ) ;
590+ let shim_path = bin_dir. join ( "tsc.exe " ) ;
577591 assert ! ( shim_path. as_path( ) . exists( ) , "Shim should exist after creation" ) ;
578592
579593 // Remove the shim
@@ -603,6 +617,8 @@ mod tests {
603617
604618 let temp_dir = TempDir :: new ( ) . unwrap ( ) ;
605619 let temp_path = temp_dir. path ( ) . to_path_buf ( ) ;
620+ #[ cfg( windows) ]
621+ setup_fake_trampoline ( & temp_path) ;
606622 let _guard = vite_shared:: EnvConfig :: test_guard (
607623 vite_shared:: EnvConfig :: for_test_with_home ( & temp_path) ,
608624 ) ;
@@ -630,10 +646,10 @@ mod tests {
630646 }
631647 #[ cfg( windows) ]
632648 {
633- assert ! ( bin_dir. join( "tsc.cmd " ) . as_path( ) . exists( ) , "tsc.cmd shim should exist" ) ;
649+ assert ! ( bin_dir. join( "tsc.exe " ) . as_path( ) . exists( ) , "tsc.exe shim should exist" ) ;
634650 assert ! (
635- bin_dir. join( "tsserver.cmd " ) . as_path( ) . exists( ) ,
636- "tsserver.cmd shim should exist"
651+ bin_dir. join( "tsserver.exe " ) . as_path( ) . exists( ) ,
652+ "tsserver.exe shim should exist"
637653 ) ;
638654 }
639655
@@ -674,10 +690,10 @@ mod tests {
674690 }
675691 #[ cfg( windows) ]
676692 {
677- assert ! ( !bin_dir. join( "tsc.cmd " ) . as_path( ) . exists( ) , "tsc.cmd shim should be removed" ) ;
693+ assert ! ( !bin_dir. join( "tsc.exe " ) . as_path( ) . exists( ) , "tsc.exe shim should be removed" ) ;
678694 assert ! (
679- !bin_dir. join( "tsserver.cmd " ) . as_path( ) . exists( ) ,
680- "tsserver.cmd shim should be removed"
695+ !bin_dir. join( "tsserver.exe " ) . as_path( ) . exists( ) ,
696+ "tsserver.exe shim should be removed"
681697 ) ;
682698 }
683699 }
0 commit comments