@@ -13,6 +13,14 @@ import { readJsonSync } from './fs'
1313import { isPath , normalizePath } from './paths/normalize'
1414import { spawn } from './spawn'
1515
16+ // Cache for binary path resolutions to avoid repeated PATH searches.
17+ // Cache is validated with existsSync() which is much cheaper than PATH search.
18+ const binPathCache = new Map < string , string > ( )
19+ // Separate cache for 'all: true' results (array of paths).
20+ const binPathAllCache = new Map < string , string [ ] > ( )
21+ // Cache for Volta binary path resolutions (keyed by volta path + binary name).
22+ const voltaBinCache = new Map < string , string > ( )
23+
1624let _fs : typeof import ( 'node:fs' ) | undefined
1725/**
1826 * Lazily load the fs module to avoid Webpack errors.
@@ -75,10 +83,30 @@ export async function execBin(
7583 args ?: string [ ] ,
7684 options ?: import ( './spawn' ) . SpawnOptions ,
7785) {
78- // Resolve the binary path.
79- const resolvedPath = isPath ( binPath )
80- ? resolveRealBinSync ( binPath )
81- : await whichReal ( binPath )
86+ // Resolve the binary path, using cache for binary names (not paths).
87+ let resolvedPath : string | string [ ] | undefined
88+ if ( isPath ( binPath ) ) {
89+ resolvedPath = resolveRealBinSync ( binPath )
90+ } else {
91+ // Check cache first for binary names.
92+ // Validate with existsSync() - cheaper than full PATH search.
93+ const cached = binPathCache . get ( binPath )
94+ if ( cached ) {
95+ if ( getFs ( ) . existsSync ( cached ) ) {
96+ resolvedPath = cached
97+ } else {
98+ // Cached path no longer exists, remove stale entry.
99+ binPathCache . delete ( binPath )
100+ }
101+ }
102+ if ( ! resolvedPath ) {
103+ resolvedPath = await whichReal ( binPath )
104+ // Cache the result if found.
105+ if ( typeof resolvedPath === 'string' ) {
106+ binPathCache . set ( binPath , resolvedPath )
107+ }
108+ }
109+ }
82110
83111 if ( ! resolvedPath ) {
84112 const error = new Error (
@@ -127,34 +155,30 @@ export function findRealBin(
127155 }
128156
129157 // Fall back to whichModule.sync if no direct path found.
158+ // Use all: true to get all paths in a single call (avoids double PATH search on Windows).
130159 /* c8 ignore next - External which call */
131- const binPath = whichModule . sync ( binName , { nothrow : true } )
132- if ( binPath ) {
133- const binDir = path . dirname ( binPath )
160+ const allPaths = whichModule . sync ( binName , { all : true , nothrow : true } ) || [ ]
161+ // Ensure allPaths is an array.
162+ const pathsArray = Array . isArray ( allPaths )
163+ ? allPaths
164+ : typeof allPaths === 'string'
165+ ? [ allPaths ]
166+ : [ ]
167+
168+ if ( pathsArray . length === 0 ) {
169+ return undefined
170+ }
134171
135- if ( isShadowBinPath ( binDir ) ) {
136- // This is likely a shadowed binary, try to find the real one.
137- /* c8 ignore next 2 - External which call */
138- const allPaths =
139- whichModule . sync ( binName , { all : true , nothrow : true } ) || [ ]
140- // Ensure allPaths is an array.
141- const pathsArray = Array . isArray ( allPaths )
142- ? allPaths
143- : typeof allPaths === 'string'
144- ? [ allPaths ]
145- : [ ]
146-
147- for ( const altPath of pathsArray ) {
148- const altDir = path . dirname ( altPath )
149- if ( ! isShadowBinPath ( altDir ) ) {
150- return altPath
151- }
152- }
172+ // First, try to find a non-shadow bin path.
173+ for ( const binPath of pathsArray ) {
174+ const binDir = path . dirname ( binPath )
175+ if ( ! isShadowBinPath ( binDir ) ) {
176+ return binPath
153177 }
154- return binPath
155178 }
156- // If all else fails, return undefined to indicate binary not found.
157- return undefined
179+
180+ // If all paths are shadow bins, return the first one.
181+ return pathsArray [ 0 ]
158182}
159183
160184/**
@@ -289,6 +313,17 @@ export function resolveRealBinSync(binPath: string): string {
289313 basename === 'node' ? - 1 : ( / (?< = \/ ) \. v o l t a \/ / i. exec ( binPath ) ?. index ?? - 1 )
290314 if ( voltaIndex !== - 1 ) {
291315 const voltaPath = binPath . slice ( 0 , voltaIndex )
316+ // Check Volta cache first - keyed by volta path + binary name.
317+ const voltaCacheKey = `${ voltaPath } :${ basename } `
318+ const cachedVolta = voltaBinCache . get ( voltaCacheKey )
319+ if ( cachedVolta ) {
320+ if ( fs . existsSync ( cachedVolta ) ) {
321+ return cachedVolta
322+ }
323+ // Cached Volta path no longer exists, remove stale entry.
324+ voltaBinCache . delete ( voltaCacheKey )
325+ }
326+
292327 const voltaToolsPath = path . join ( voltaPath , 'tools' )
293328 const voltaImagePath = path . join ( voltaToolsPath , 'image' )
294329 const voltaUserPath = path . join ( voltaToolsPath , 'user' )
@@ -337,10 +372,13 @@ export function resolveRealBinSync(binPath: string): string {
337372 }
338373 }
339374 if ( voltaBinPath ) {
375+ let resolvedVoltaPath = voltaBinPath
340376 try {
341- return normalizePath ( fs . realpathSync . native ( voltaBinPath ) )
377+ resolvedVoltaPath = normalizePath ( fs . realpathSync . native ( voltaBinPath ) )
342378 } catch { }
343- return voltaBinPath
379+ // Cache the resolved Volta path.
380+ voltaBinCache . set ( voltaCacheKey , resolvedVoltaPath )
381+ return resolvedVoltaPath
344382 }
345383 }
346384 if ( WIN32 ) {
@@ -689,9 +727,33 @@ export async function whichReal(
689727 binName : string ,
690728 options ?: WhichOptions ,
691729) : Promise < string | string [ ] | undefined > {
692- // whichModule is imported at the top
730+ const fs = getFs ( )
693731 // Default to nothrow: true if not specified to return undefined instead of throwing
694732 const opts = { nothrow : true , ...options }
733+
734+ // Use cache - validate with existsSync() which is cheaper than full PATH search.
735+ if ( opts . all ) {
736+ // Check array cache for 'all: true' lookups.
737+ // Only validate first path for performance - if primary binary exists, assume others do too.
738+ const cachedAll = binPathAllCache . get ( binName )
739+ if ( cachedAll && cachedAll . length > 0 ) {
740+ if ( fs . existsSync ( cachedAll [ 0 ] ! ) ) {
741+ return cachedAll
742+ }
743+ // Primary cached path no longer exists, remove stale entry.
744+ binPathAllCache . delete ( binName )
745+ }
746+ } else {
747+ const cached = binPathCache . get ( binName )
748+ if ( cached ) {
749+ if ( fs . existsSync ( cached ) ) {
750+ return cached
751+ }
752+ // Cached path no longer exists, remove stale entry.
753+ binPathCache . delete ( binName )
754+ }
755+ }
756+
695757 // Depending on options `whichModule` may throw if `binName` is not found.
696758 // With nothrow: true, it returns null when `binName` is not found.
697759 /* c8 ignore next - External which call */
@@ -705,15 +767,24 @@ export async function whichReal(
705767 ? [ result ]
706768 : undefined
707769 // If all is true and we have paths, resolve each one.
708- return paths ?. length ? paths . map ( p => resolveRealBinSync ( p ) ) : paths
770+ if ( paths ?. length ) {
771+ const resolved = paths . map ( p => resolveRealBinSync ( p ) )
772+ // Cache the resolved paths.
773+ binPathAllCache . set ( binName , resolved )
774+ return resolved
775+ }
776+ return paths
709777 }
710778
711779 // If result is undefined (binary not found), return undefined
712780 if ( ! result ) {
713781 return undefined
714782 }
715783
716- return resolveRealBinSync ( result )
784+ const resolved = resolveRealBinSync ( result )
785+ // Cache the resolved path.
786+ binPathCache . set ( binName , resolved )
787+ return resolved
717788}
718789
719790/**
@@ -725,8 +796,33 @@ export function whichRealSync(
725796 binName : string ,
726797 options ?: WhichOptions ,
727798) : string | string [ ] | undefined {
799+ const fs = getFs ( )
728800 // Default to nothrow: true if not specified to return undefined instead of throwing
729801 const opts = { nothrow : true , ...options }
802+
803+ // Use cache - validate with existsSync() which is cheaper than full PATH search.
804+ if ( opts . all ) {
805+ // Check array cache for 'all: true' lookups.
806+ // Only validate first path for performance - if primary binary exists, assume others do too.
807+ const cachedAll = binPathAllCache . get ( binName )
808+ if ( cachedAll && cachedAll . length > 0 ) {
809+ if ( fs . existsSync ( cachedAll [ 0 ] ! ) ) {
810+ return cachedAll
811+ }
812+ // Primary cached path no longer exists, remove stale entry.
813+ binPathAllCache . delete ( binName )
814+ }
815+ } else {
816+ const cached = binPathCache . get ( binName )
817+ if ( cached ) {
818+ if ( fs . existsSync ( cached ) ) {
819+ return cached
820+ }
821+ // Cached path no longer exists, remove stale entry.
822+ binPathCache . delete ( binName )
823+ }
824+ }
825+
730826 // Depending on options `which` may throw if `binName` is not found.
731827 // With nothrow: true, it returns null when `binName` is not found.
732828 const result = whichSync ( binName , opts )
@@ -739,15 +835,24 @@ export function whichRealSync(
739835 ? [ result ]
740836 : undefined
741837 // If all is true and we have paths, resolve each one.
742- return paths ?. length ? paths . map ( p => resolveRealBinSync ( p ) ) : paths
838+ if ( paths ?. length ) {
839+ const resolved = paths . map ( p => resolveRealBinSync ( p ) )
840+ // Cache the resolved paths.
841+ binPathAllCache . set ( binName , resolved )
842+ return resolved
843+ }
844+ return paths
743845 }
744846
745847 // If result is undefined (binary not found), return undefined
746848 if ( ! result ) {
747849 return undefined
748850 }
749851
750- return resolveRealBinSync ( result as string )
852+ const resolved = resolveRealBinSync ( result as string )
853+ // Cache the resolved path.
854+ binPathCache . set ( binName , resolved )
855+ return resolved
751856}
752857
753858/**
0 commit comments