55//! 2. Node.js installation (if needed)
66//! 3. Tool execution (core tools and package binaries)
77
8+ use vite_install:: package_manager:: {
9+ PackageManagerType , download_package_manager, package_manager_bin_path,
10+ resolve_package_manager_from_package_json,
11+ } ;
812use vite_path:: { AbsolutePath , AbsolutePathBuf , current_dir} ;
913use vite_shared:: { PrependOptions , env_vars, output, prepend_to_path_env} ;
1014
1115use super :: {
1216 cache:: { self , ResolveCache , ResolveCacheEntry } ,
1317 exec, is_core_shim_tool,
1418} ;
15- use crate :: commands:: env:: {
16- bin_config:: { BinConfig , BinSource } ,
17- config:: { self , ShimMode } ,
18- global_install:: CORE_SHIMS ,
19- package_metadata:: PackageMetadata ,
19+ use crate :: {
20+ commands:: env:: {
21+ bin_config:: { BinConfig , BinSource } ,
22+ config:: { self , ShimMode } ,
23+ global_install:: CORE_SHIMS ,
24+ package_metadata:: PackageMetadata ,
25+ } ,
26+ error:: Error ,
2027} ;
2128
2229/// Environment variable used to prevent infinite recursion in shim dispatch.
@@ -33,6 +40,25 @@ fn is_package_manager_tool(tool: &str) -> bool {
3340 PACKAGE_MANAGER_TOOLS . contains ( & tool)
3441}
3542
43+ fn package_manager_type_for_tool ( tool : & str ) -> Option < PackageManagerType > {
44+ match tool {
45+ "npm" => Some ( PackageManagerType :: Npm ) ,
46+ "pnpm" => Some ( PackageManagerType :: Pnpm ) ,
47+ "yarn" => Some ( PackageManagerType :: Yarn ) ,
48+ "bun" => Some ( PackageManagerType :: Bun ) ,
49+ _ => None ,
50+ }
51+ }
52+
53+ fn package_manager_bin_name ( tool : & str , package_manager_type : PackageManagerType ) -> & str {
54+ match ( tool, package_manager_type) {
55+ ( _, PackageManagerType :: Npm ) => "npm" ,
56+ ( _, PackageManagerType :: Pnpm ) => "pnpm" ,
57+ ( _, PackageManagerType :: Yarn ) => "yarn" ,
58+ ( _, PackageManagerType :: Bun ) => "bun" ,
59+ }
60+ }
61+
3662/// Parsed npm global command (install or uninstall).
3763struct NpmGlobalCommand {
3864 /// Package names/specs extracted from args (e.g., ["codex", "typescript@5"])
@@ -654,6 +680,37 @@ fn resolve_npm_prefix(
654680 get_npm_global_prefix ( npm_path, node_dir)
655681}
656682
683+ /// Resolve a matching package-manager binary from the current project's explicit
684+ /// `packageManager` field.
685+ ///
686+ /// The match is intentionally strict to avoid translating commands: `npm` only uses
687+ /// `npm@...`, `pnpm` only uses `pnpm@...`, etc.
688+ async fn resolve_matching_package_manager_tool (
689+ cwd : & AbsolutePath ,
690+ tool : & str ,
691+ ) -> Result < Option < AbsolutePathBuf > , Error > {
692+ let Some ( expected_type) = package_manager_type_for_tool ( tool) else {
693+ return Ok ( None ) ;
694+ } ;
695+
696+ let Some ( resolution) = resolve_package_manager_from_package_json ( cwd) ? else {
697+ return Ok ( None ) ;
698+ } ;
699+
700+ if resolution. package_manager_type != expected_type {
701+ return Ok ( None ) ;
702+ }
703+
704+ let ( install_dir, _, _) = download_package_manager (
705+ resolution. package_manager_type ,
706+ & resolution. version ,
707+ resolution. hash . as_deref ( ) ,
708+ )
709+ . await ?;
710+ let bin_name = package_manager_bin_name ( tool, resolution. package_manager_type ) ;
711+ Ok ( Some ( package_manager_bin_path ( & install_dir, bin_name) ) )
712+ }
713+
657714/// Main shim dispatch entry point.
658715///
659716/// Called when the binary is invoked as node, npm, npx, or a package binary.
@@ -757,24 +814,58 @@ pub async fn dispatch(tool: &str, args: &[String]) -> i32 {
757814 return 1 ;
758815 }
759816
760- // Locate tool binary
761- let tool_path = match locate_tool ( & resolution. version , tool) {
817+ // Locate the Node binary for PATH preparation. Package-manager shims can use
818+ // their own declared version, but JS-based package managers still need the
819+ // project-resolved Node.js runtime to execute.
820+ let node_path = match locate_tool ( & resolution. version , "node" ) {
762821 Ok ( p) => p,
763822 Err ( e) => {
764- eprintln ! ( "vp: Tool '{tool}' not found: {e}" ) ;
823+ eprintln ! ( "vp: Node not found: {e}" ) ;
765824 return 1 ;
766825 }
767826 } ;
768827
769- // Save original PATH before we modify it — needed for npm global install check.
828+ // Locate tool binary. If the current project explicitly pins the invoked
829+ // package manager in `packageManager`, prefer that managed package-manager
830+ // binary over the tool bundled with Node.js.
831+ let package_manager_tool_path = match resolve_matching_package_manager_tool ( & cwd, tool) . await {
832+ Ok ( path) => path,
833+ Err ( e) => {
834+ eprintln ! ( "vp: Failed to resolve package manager for '{tool}': {e}" ) ;
835+ return 1 ;
836+ }
837+ } ;
838+ let tool_path = match package_manager_tool_path {
839+ Some ( path) => path,
840+ None => match locate_tool ( & resolution. version , tool) {
841+ Ok ( p) => p,
842+ Err ( e) => {
843+ eprintln ! ( "vp: Tool '{tool}' not found: {e}" ) ;
844+ return 1 ;
845+ }
846+ } ,
847+ } ;
848+
849+ if !tool_path. as_path ( ) . exists ( ) {
850+ eprintln ! ( "vp: Tool '{tool}' not found: {}" , tool_path. as_path( ) . display( ) ) ;
851+ return 1 ;
852+ }
853+
854+ // Save original PATH before we modify it - needed for npm global install check.
770855 // Only captured for npm to avoid unnecessary work on node/npx hot path.
771856 let original_path = if tool == "npm" { std:: env:: var_os ( "PATH" ) } else { None } ;
772857
773- // Prepare environment for recursive invocations
774- // Prepend real node bin dir to PATH so child processes use the correct version
775- let node_bin_dir = tool_path. parent ( ) . expect ( "Tool has no parent directory" ) ;
776- // Use dedupe_anywhere=false to only check if it's first in PATH (original behavior)
858+ // Prepare environment for recursive invocations. Keep the project Node.js
859+ // bin dir available for JS package-manager shims, and when a package-manager
860+ // version was selected from `packageManager`, put that PM bin dir first so
861+ // nested invocations see the same PM version while recursion prevention is set.
862+ let node_bin_dir = node_path. parent ( ) . expect ( "Node has no parent directory" ) ;
777863 let _ = prepend_to_path_env ( node_bin_dir, PrependOptions :: default ( ) ) ;
864+ if let Some ( pm_bin_dir) = tool_path. parent ( )
865+ && pm_bin_dir != node_bin_dir
866+ {
867+ let _ = prepend_to_path_env ( pm_bin_dir, PrependOptions :: default ( ) ) ;
868+ }
778869
779870 // Optional debug env vars
780871 if std:: env:: var ( env_vars:: VP_DEBUG_SHIM ) . is_ok ( ) {
@@ -842,6 +933,55 @@ pub async fn dispatch(tool: &str, args: &[String]) -> i32 {
842933/// Finds the package that provides this binary and executes it with the
843934/// Node.js version that was used to install the package.
844935async fn dispatch_package_binary ( tool : & str , args : & [ String ] ) -> i32 {
936+ if package_manager_type_for_tool ( tool) . is_some ( ) {
937+ let cwd = match current_dir ( ) {
938+ Ok ( path) => path,
939+ Err ( e) => {
940+ eprintln ! ( "vp: Failed to get current directory: {e}" ) ;
941+ return 1 ;
942+ }
943+ } ;
944+
945+ match resolve_matching_package_manager_tool ( & cwd, tool) . await {
946+ Ok ( Some ( tool_path) ) => {
947+ let node_version = match resolve_with_cache ( & cwd) . await {
948+ Ok ( resolution) => resolution. version ,
949+ Err ( _) => match find_package_for_binary ( tool) . await {
950+ Ok ( Some ( metadata) ) => metadata. platform . node ,
951+ _ => String :: new ( ) ,
952+ } ,
953+ } ;
954+
955+ if !node_version. is_empty ( ) {
956+ if let Err ( e) = ensure_installed ( & node_version) . await {
957+ eprintln ! ( "vp: Failed to install Node {}: {e}" , node_version) ;
958+ return 1 ;
959+ }
960+ if let Ok ( node_path) = locate_tool ( & node_version, "node" )
961+ && let Some ( node_bin_dir) = node_path. parent ( )
962+ {
963+ let _ = prepend_to_path_env ( node_bin_dir, PrependOptions :: default ( ) ) ;
964+ }
965+ }
966+
967+ if let Some ( pm_bin_dir) = tool_path. parent ( ) {
968+ let _ = prepend_to_path_env ( pm_bin_dir, PrependOptions :: default ( ) ) ;
969+ }
970+
971+ // SAFETY: Setting env vars at this point before exec is safe
972+ unsafe {
973+ std:: env:: set_var ( RECURSION_ENV_VAR , "1" ) ;
974+ }
975+ return exec:: exec_tool ( & tool_path, args) ;
976+ }
977+ Ok ( None ) => { }
978+ Err ( e) => {
979+ eprintln ! ( "vp: Failed to resolve package manager for '{tool}': {e}" ) ;
980+ return 1 ;
981+ }
982+ }
983+ }
984+
845985 // Find which package provides this binary
846986 let package_metadata = match find_package_for_binary ( tool) . await {
847987 Ok ( Some ( metadata) ) => metadata,
0 commit comments