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+ package_manager_install_dir, 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.
@@ -25,12 +32,14 @@ use crate::commands::env::{
2532/// directly using the current PATH (passthrough mode).
2633const RECURSION_ENV_VAR : & str = env_vars:: VP_TOOL_RECURSION ;
2734
28- /// Package manager tools that should resolve Node.js version from the project context
29- /// rather than using the install-time version.
30- const PACKAGE_MANAGER_TOOLS : & [ & str ] = & [ "pnpm" , "yarn" , "bun" ] ;
31-
35+ /// Package-manager tools whose Node.js runtime should be resolved from the
36+ /// project context rather than the install-time version.
37+ ///
38+ /// Intentionally excludes `npm`/`npx`: those are core shims (see
39+ /// `is_core_shim_tool`) and never reach `dispatch_package_binary`, so they are
40+ /// handled by the main `dispatch` path instead.
3241fn is_package_manager_tool ( tool : & str ) -> bool {
33- PACKAGE_MANAGER_TOOLS . contains ( & tool)
42+ matches ! ( PackageManagerType :: from_tool ( tool) , Some ( t ) if t != PackageManagerType :: Npm )
3443}
3544
3645/// Parsed npm global command (install or uninstall).
@@ -654,6 +663,48 @@ fn resolve_npm_prefix(
654663 get_npm_global_prefix ( npm_path, node_dir)
655664}
656665
666+ /// Resolve a matching package-manager binary from the current project's explicit
667+ /// `packageManager` field.
668+ ///
669+ /// The match is intentionally strict to avoid translating commands: `npm` only uses
670+ /// `npm@...`, `pnpm` only uses `pnpm@...`, etc.
671+ async fn resolve_matching_package_manager_tool (
672+ cwd : & AbsolutePath ,
673+ tool : & str ,
674+ ) -> Result < Option < AbsolutePathBuf > , Error > {
675+ let Some ( expected_type) = PackageManagerType :: from_tool ( tool) else {
676+ return Ok ( None ) ;
677+ } ;
678+
679+ let Some ( resolution) = resolve_package_manager_from_package_json ( cwd) ? else {
680+ return Ok ( None ) ;
681+ } ;
682+
683+ if resolution. package_manager_type != expected_type {
684+ return Ok ( None ) ;
685+ }
686+
687+ let bin_name = expected_type. bin_name_for_tool ( tool) ;
688+
689+ // Fast path: if the managed install already exists, skip download_package_manager
690+ // entirely. The slow path stats three files (`bin`, `.cmd`, `.ps1`) on every
691+ // invocation, which adds up on the shim hot path.
692+ if let Some ( install_dir) = package_manager_install_dir ( expected_type, & resolution. version ) {
693+ let bin_path = package_manager_bin_path ( & install_dir, bin_name) ;
694+ if bin_path. as_path ( ) . exists ( ) {
695+ return Ok ( Some ( bin_path) ) ;
696+ }
697+ }
698+
699+ let ( install_dir, _, _) = download_package_manager (
700+ resolution. package_manager_type ,
701+ & resolution. version ,
702+ resolution. hash . as_deref ( ) ,
703+ )
704+ . await ?;
705+ Ok ( Some ( package_manager_bin_path ( & install_dir, bin_name) ) )
706+ }
707+
657708/// Main shim dispatch entry point.
658709///
659710/// Called when the binary is invoked as node, npm, npx, or a package binary.
@@ -757,24 +808,53 @@ pub async fn dispatch(tool: &str, args: &[String]) -> i32 {
757808 return 1 ;
758809 }
759810
760- // Locate tool binary
761- let tool_path = match locate_tool ( & resolution. version , tool) {
811+ // Locate the Node binary for PATH preparation. Package-manager shims can use
812+ // their own declared version, but JS-based package managers still need the
813+ // project-resolved Node.js runtime to execute.
814+ let node_path = match locate_tool ( & resolution. version , "node" ) {
762815 Ok ( p) => p,
763816 Err ( e) => {
764- eprintln ! ( "vp: Tool '{tool}' not found: {e}" ) ;
817+ eprintln ! ( "vp: Node not found: {e}" ) ;
818+ return 1 ;
819+ }
820+ } ;
821+
822+ // Locate tool binary. If the current project explicitly pins the invoked
823+ // package manager in `packageManager`, prefer that managed package-manager
824+ // binary over the tool bundled with Node.js.
825+ let package_manager_tool_path = match resolve_matching_package_manager_tool ( & cwd, tool) . await {
826+ Ok ( path) => path,
827+ Err ( e) => {
828+ eprintln ! ( "vp: Failed to resolve package manager for '{tool}': {e}" ) ;
765829 return 1 ;
766830 }
767831 } ;
832+ let tool_path = match package_manager_tool_path {
833+ Some ( path) => path,
834+ None => match locate_tool ( & resolution. version , tool) {
835+ Ok ( p) => p,
836+ Err ( e) => {
837+ eprintln ! ( "vp: Tool '{tool}' not found: {e}" ) ;
838+ return 1 ;
839+ }
840+ } ,
841+ } ;
768842
769- // Save original PATH before we modify it — needed for npm global install check.
843+ // Save original PATH before we modify it - needed for npm global install check.
770844 // Only captured for npm to avoid unnecessary work on node/npx hot path.
771845 let original_path = if tool == "npm" { std:: env:: var_os ( "PATH" ) } else { None } ;
772846
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)
847+ // Prepare environment for recursive invocations. Keep the project Node.js
848+ // bin dir available for JS package-manager shims, and when a package-manager
849+ // version was selected from `packageManager`, put that PM bin dir first so
850+ // nested invocations see the same PM version while recursion prevention is set.
851+ let node_bin_dir = node_path. parent ( ) . expect ( "Node has no parent directory" ) ;
777852 let _ = prepend_to_path_env ( node_bin_dir, PrependOptions :: default ( ) ) ;
853+ if let Some ( pm_bin_dir) = tool_path. parent ( )
854+ && pm_bin_dir != node_bin_dir
855+ {
856+ let _ = prepend_to_path_env ( pm_bin_dir, PrependOptions :: default ( ) ) ;
857+ }
778858
779859 // Optional debug env vars
780860 if std:: env:: var ( env_vars:: VP_DEBUG_SHIM ) . is_ok ( ) {
@@ -842,6 +922,59 @@ pub async fn dispatch(tool: &str, args: &[String]) -> i32 {
842922/// Finds the package that provides this binary and executes it with the
843923/// Node.js version that was used to install the package.
844924async fn dispatch_package_binary ( tool : & str , args : & [ String ] ) -> i32 {
925+ if let Some ( pm_family) = PackageManagerType :: from_tool ( tool) {
926+ let cwd = match current_dir ( ) {
927+ Ok ( path) => path,
928+ Err ( e) => {
929+ eprintln ! ( "vp: Failed to get current directory: {e}" ) ;
930+ return 1 ;
931+ }
932+ } ;
933+
934+ match resolve_matching_package_manager_tool ( & cwd, tool) . await {
935+ Ok ( Some ( tool_path) ) => {
936+ // Bun is a native binary and does not need a Node.js runtime on PATH;
937+ // JS-based PMs (npm/pnpm/yarn) do.
938+ if pm_family != PackageManagerType :: Bun {
939+ let node_version = match resolve_with_cache ( & cwd) . await {
940+ Ok ( resolution) => resolution. version ,
941+ Err ( _) => match find_package_for_binary ( tool) . await {
942+ Ok ( Some ( metadata) ) => metadata. platform . node ,
943+ _ => String :: new ( ) ,
944+ } ,
945+ } ;
946+
947+ if !node_version. is_empty ( ) {
948+ if let Err ( e) = ensure_installed ( & node_version) . await {
949+ eprintln ! ( "vp: Failed to install Node {}: {e}" , node_version) ;
950+ return 1 ;
951+ }
952+ if let Ok ( node_path) = locate_tool ( & node_version, "node" )
953+ && let Some ( node_bin_dir) = node_path. parent ( )
954+ {
955+ let _ = prepend_to_path_env ( node_bin_dir, PrependOptions :: default ( ) ) ;
956+ }
957+ }
958+ }
959+
960+ if let Some ( pm_bin_dir) = tool_path. parent ( ) {
961+ let _ = prepend_to_path_env ( pm_bin_dir, PrependOptions :: default ( ) ) ;
962+ }
963+
964+ // SAFETY: Setting env vars at this point before exec is safe
965+ unsafe {
966+ std:: env:: set_var ( RECURSION_ENV_VAR , "1" ) ;
967+ }
968+ return exec:: exec_tool ( & tool_path, args) ;
969+ }
970+ Ok ( None ) => { }
971+ Err ( e) => {
972+ eprintln ! ( "vp: Failed to resolve package manager for '{tool}': {e}" ) ;
973+ return 1 ;
974+ }
975+ }
976+ }
977+
845978 // Find which package provides this binary
846979 let package_metadata = match find_package_for_binary ( tool) . await {
847980 Ok ( Some ( metadata) ) => metadata,
0 commit comments