1- import { exists , log , retry , TmpDir } from "builder-util"
1+ import { exists , isEmptyOrSpaces , log , retry , TmpDir } from "builder-util"
22import * as childProcess from "child_process"
33import { CancellationToken } from "builder-util-runtime"
44import * as fs from "fs-extra"
@@ -9,35 +9,21 @@ import { hoist, type HoisterResult, type HoisterTree } from "./hoist"
99import { createModuleCache , type ModuleCache } from "./moduleCache"
1010import { getPackageManagerCommand , PM } from "./packageManager"
1111import type { Dependency , DependencyGraph , NodeModuleInfo , PackageJson } from "./types"
12- import { fileURLToPath , pathToFileURL } from "node:url"
1312
1413export abstract class NodeModulesCollector < ProdDepType extends Dependency < ProdDepType , OptionalDepType > , OptionalDepType > {
15- private nodeModules : NodeModuleInfo [ ] = [ ]
16- protected allDependencies : Map < string , ProdDepType > = new Map ( )
17- protected productionGraph : DependencyGraph = { }
18- protected pkgJsonCache : Map < string , string > = new Map ( )
19- protected memoResolvedModules = new Map < string , Promise < string | null > > ( )
20-
21- private importMetaResolve = new Lazy ( async ( ) => {
22- try {
23- // Node >= 16 builtin ESM-aware resolver
24- const packageName = "import-meta-resolve"
25- const imported = await import ( packageName )
26- return imported . resolve
27- } catch {
28- return null
29- }
30- } )
14+ private readonly nodeModules : NodeModuleInfo [ ] = [ ]
15+ protected readonly allDependencies : Map < string , ProdDepType > = new Map ( )
16+ protected readonly productionGraph : DependencyGraph = { }
3117
3218 // Unified cache for all file system and module operations
33- protected cache : ModuleCache = createModuleCache ( )
19+ private readonly cache : ModuleCache = createModuleCache ( )
3420
35- protected isHoisted = new Lazy < boolean > ( async ( ) => {
21+ protected readonly isHoisted = new Lazy < boolean > ( async ( ) => {
3622 const { manager } = this . installOptions
3723 const command = getPackageManagerCommand ( manager )
3824
39- const config = ( await this . asyncExec ( command , [ "config" , "list" ] ) ) . stdout
40- if ( config == null ) {
25+ const config = ( await this . asyncExec ( command , [ "config" , "list" ] ) ) . stdout ?. trim ( )
26+ if ( isEmptyOrSpaces ( config ) ) {
4127 log . debug ( { manager } , "unable to determine if node_modules are hoisted: no config output. falling back to hoisted mode" )
4228 return false
4329 }
@@ -69,7 +55,6 @@ export abstract class NodeModulesCollector<ProdDepType extends Dependency<ProdDe
6955 }
7056
7157 await this . collectAllDependencies ( tree , packageName )
72-
7358 const realTree : ProdDepType = await this . getTreeFromWorkspaces ( tree , packageName )
7459 await this . extractProductionDependencyGraph ( realTree , packageName )
7560
@@ -80,10 +65,8 @@ export abstract class NodeModulesCollector<ProdDepType extends Dependency<ProdDe
8065 const hoisterResult : HoisterResult = hoist ( this . transformToHoisterTree ( this . productionGraph , packageName ) , {
8166 check : log . isDebugEnabled ,
8267 } )
83-
8468 await this . _getNodeModules ( hoisterResult . dependencies , this . nodeModules )
8569 log . debug ( { packageName, depCount : this . nodeModules . length } , "node modules collection complete" )
86-
8770 return this . nodeModules
8871 }
8972
@@ -210,60 +193,52 @@ export abstract class NodeModulesCollector<ProdDepType extends Dependency<ProdDe
210193 }
211194
212195 /**
213- * Resolves a package to its filesystem location using Node.js module resolution.
214- * Returns the directory containing the package, not the package.json path.
196+ * Resolve a package directory purely from the filesystem.
197+ * Does NOT attempt to load the module or resolve an "exports" entrypoint.
198+ * Good for Yarn 4 because a package may not be resolvable as a module,
199+ * but still exists on disk.
215200 */
216- protected async resolvePackageDir ( packageName : string , fromDir : string ) : Promise < string | null > {
201+ protected async resolvePackage ( packageName : string , fromDir : string ) : Promise < { entry : string ; packageDir : string } | null > {
217202 const cacheKey = `${ packageName } ::${ fromDir } `
218203 if ( this . cache . requireResolve . has ( cacheKey ) ) {
219204 return this . cache . requireResolve . get ( cacheKey ) !
220205 }
221206
222- const resolveEntry = async ( ) => {
223- // 1) Try Node ESM resolver (handles exports reliably)
224- if ( await this . importMetaResolve . value ) {
225- try {
226- const url = ( await this . importMetaResolve . value ) ( packageName , pathToFileURL ( fromDir ) . href )
227- return url . startsWith ( "file://" ) ? fileURLToPath ( url ) : null
228- } catch ( error : any ) {
229- log . debug ( { error : error . message } , "import-meta-resolve failed" )
230- // ignore
231- }
232- }
233-
234- // 2) Fallback: require.resolve (CJS, old packages)
235- try {
236- return require . resolve ( packageName , { paths : [ fromDir , this . rootDir ] } )
237- } catch ( error : any ) {
238- log . debug ( { error : error . message } , "require.resolve failed" )
239- // ignore
240- }
241-
242- return null
207+ // 1. NESTED under fromDir/node_modules/<name>
208+ let candidate = path . join ( fromDir , "node_modules" , packageName )
209+ let pkgJson = path . join ( candidate , "package.json" )
210+ if ( await this . existsMemoized ( pkgJson ) ) {
211+ this . cache . requireResolve . set ( cacheKey , { entry : pkgJson , packageDir : candidate } )
212+ return { entry : pkgJson , packageDir : candidate }
243213 }
244214
245- const entry = await resolveEntry ( )
246- if ( ! entry ) {
247- this . cache . requireResolve . set ( cacheKey , null )
248- return null
215+ // 2. HOISTED under rootDir/node_modules/<name>
216+ candidate = path . join ( this . rootDir , "node_modules" , packageName )
217+ pkgJson = path . join ( candidate , "package.json" )
218+ if ( await this . existsMemoized ( pkgJson ) ) {
219+ this . cache . requireResolve . set ( cacheKey , { entry : pkgJson , packageDir : candidate } )
220+ return { entry : pkgJson , packageDir : candidate }
249221 }
250222
251- // 3) Walk upward until you find a package.json
252- let dir = path . dirname ( entry )
223+ // 3. FALLBACK: try parent directories BFS (classic Node-style search)
224+ let current = fromDir
253225 while ( true ) {
254- const pkgFile = path . join ( dir , "package.json" )
255- if ( await this . existsMemoized ( pkgFile ) ) {
256- this . cache . requireResolve . set ( cacheKey , dir )
257- return dir
226+ const nm = path . join ( current , "node_modules" , packageName )
227+ const pkg = path . join ( nm , "package.json" )
228+
229+ if ( await this . existsMemoized ( pkg ) ) {
230+ this . cache . requireResolve . set ( cacheKey , { entry : pkg , packageDir : nm } )
231+ return { entry : pkg , packageDir : nm }
258232 }
259233
260- const parent = path . dirname ( dir )
261- if ( parent === dir ) {
262- break // root reached
234+ const parent = path . dirname ( current )
235+ if ( parent === current ) {
236+ break
263237 }
264- dir = parent
238+ current = parent
265239 }
266240
241+ // 4. LAST RESORT: DO NOT throw — just return null
267242 this . cache . requireResolve . set ( cacheKey , null )
268243 return null
269244 }
@@ -298,9 +273,10 @@ export abstract class NodeModulesCollector<ProdDepType extends Dependency<ProdDe
298273
299274 if ( tree . dependencies ?. [ packageName ] ) {
300275 const { name, path, dependencies } = tree . dependencies [ packageName ]
301- log . debug ( { name, path, dependencies : JSON . stringify ( dependencies ) } , "pruning root app/self reference from workspace tree" )
276+ log . debug ( { name, path, dependencies : JSON . stringify ( dependencies ) } , "pruning root app/self reference from workspace tree, merging dependencies uptree " )
302277 for ( const [ name , pkg ] of Object . entries ( dependencies ?? { } ) ) {
303278 tree . dependencies [ name ] = pkg
279+ this . allDependencies . set ( this . packageVersionString ( pkg ) , pkg )
304280 }
305281 delete tree . dependencies [ packageName ]
306282 }
@@ -396,6 +372,8 @@ export abstract class NodeModulesCollector<ProdDepType extends Dependency<ProdDe
396372 args = [ "/c" , tempBatFile , ...args ]
397373 }
398374
375+ log . debug ( { command, args, cwd, tempOutputFile } , "spawning node module collector process" )
376+
399377 await new Promise < void > ( ( resolve , reject ) => {
400378 const outStream = createWriteStream ( tempOutputFile )
401379
0 commit comments