11import { exists , log , retry , TmpDir } from "builder-util"
2- import { spawn as spawnProcess } from "child_process"
2+ import * as childProcess from "child_process"
33import { CancellationToken } from "builder-util-runtime"
44import * as fs from "fs-extra"
55import { createWriteStream , readJson } from "fs-extra"
@@ -9,6 +9,7 @@ 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"
1213
1314export abstract class NodeModulesCollector < ProdDepType extends Dependency < ProdDepType , OptionalDepType > , OptionalDepType > {
1415 private nodeModules : NodeModuleInfo [ ] = [ ]
@@ -17,21 +18,33 @@ export abstract class NodeModulesCollector<ProdDepType extends Dependency<ProdDe
1718 protected pkgJsonCache : Map < string , string > = new Map ( )
1819 protected memoResolvedModules = new Map < string , Promise < string | null > > ( )
1920
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+ } )
31+
2032 // Unified cache for all file system and module operations
2133 protected cache : ModuleCache = createModuleCache ( )
2234
2335 protected isHoisted = new Lazy < boolean > ( async ( ) => {
24- const command = getPackageManagerCommand ( this . installOptions . manager )
36+ const { manager } = this . installOptions
37+ const command = getPackageManagerCommand ( manager )
2538
2639 const config = ( await this . asyncExec ( command , [ "config" , "list" ] ) ) . stdout
2740 if ( config == null ) {
28- log . debug ( { manager : this . installOptions . manager } , "unable to determine if node_modules are hoisted: no config output. falling back to hoisted mode" )
41+ log . debug ( { manager } , "unable to determine if node_modules are hoisted: no config output. falling back to hoisted mode" )
2942 return false
3043 }
3144 const lines = Object . fromEntries ( config . split ( "\n" ) . map ( line => line . split ( "=" ) . map ( s => s . trim ( ) ) ) )
3245
3346 if ( lines [ "node-linker" ] === "hoisted" ) {
34- log . debug ( { manager : this . installOptions . manager } , "node_modules are hoisted" )
47+ log . debug ( { manager } , "node_modules are hoisted" )
3548 return true
3649 }
3750
@@ -57,7 +70,7 @@ export abstract class NodeModulesCollector<ProdDepType extends Dependency<ProdDe
5770
5871 await this . collectAllDependencies ( tree , packageName )
5972
60- const realTree : ProdDepType = await this . getTreeFromWorkspaces ( tree )
73+ const realTree : ProdDepType = await this . getTreeFromWorkspaces ( tree , packageName )
6174 await this . extractProductionDependencyGraph ( realTree , packageName )
6275
6376 if ( cancellationToken . cancelled ) {
@@ -184,27 +197,57 @@ export abstract class NodeModulesCollector<ProdDepType extends Dependency<ProdDe
184197 * Resolves a package to its filesystem location using Node.js module resolution.
185198 * Returns the directory containing the package, not the package.json path.
186199 */
187- protected resolvePackageDir = ( packageName : string , fromDir : string ) : string | null = > {
200+ protected async resolvePackageDir ( packageName : string , fromDir : string ) : Promise < string | null > {
188201 const cacheKey = `${ packageName } ::${ fromDir } `
189-
190- // Check memoization cache
191202 if ( this . cache . requireResolve . has ( cacheKey ) ) {
192203 return this . cache . requireResolve . get ( cacheKey ) !
193204 }
194205
195- try {
196- // require.resolve finds the main entry point, so we look for package.json instead
197- const packageJsonPath = require . resolve ( `${ packageName } /package.json` , {
198- paths : [ fromDir , this . rootDir ] ,
199- } )
200- const result = path . dirname ( packageJsonPath )
201- this . cache . requireResolve . set ( cacheKey , result )
202- return result
203- } catch ( error : any ) {
204- log . warn ( { packageName, fromDir, error : error . message } , "could not resolve package" )
206+ const resolveEntry = async ( ) => {
207+ // 1) Try Node ESM resolver (handles exports reliably)
208+ if ( await this . importMetaResolve . value ) {
209+ try {
210+ const url = ( await this . importMetaResolve . value ) ( packageName , pathToFileURL ( fromDir ) . href )
211+ return url . startsWith ( "file://" ) ? fileURLToPath ( url ) : null
212+ } catch {
213+ // ignore
214+ }
215+ }
216+
217+ // 2) Fallback: require.resolve (CJS, old packages)
218+ try {
219+ return require . resolve ( packageName , { paths : [ fromDir , this . rootDir ] } )
220+ } catch {
221+ // ignore
222+ }
223+
224+ return null
225+ }
226+
227+ const entry = await resolveEntry ( )
228+ if ( ! entry ) {
205229 this . cache . requireResolve . set ( cacheKey , null )
206230 return null
207231 }
232+
233+ // 3) Walk upward until you find a package.json
234+ let dir = path . dirname ( entry )
235+ while ( true ) {
236+ const pkgFile = path . join ( dir , "package.json" )
237+ if ( await exists ( pkgFile ) ) {
238+ this . cache . requireResolve . set ( cacheKey , dir )
239+ return dir
240+ }
241+
242+ const parent = path . dirname ( dir )
243+ if ( parent === dir ) {
244+ break // root reached
245+ }
246+ dir = parent
247+ }
248+
249+ this . cache . requireResolve . set ( cacheKey , null )
250+ return null
208251 }
209252
210253 protected requireMemoized ( pkgPath : string ) : PackageJson {
@@ -244,20 +287,20 @@ export abstract class NodeModulesCollector<ProdDepType extends Dependency<ProdDe
244287 return { name, version }
245288 }
246289
247- protected async getTreeFromWorkspaces ( tree : ProdDepType ) : Promise < ProdDepType > {
248- if ( tree . workspaces && tree . dependencies ) {
249- const packageJson = await this . appPkgJson . value
250- const dependencyName = packageJson . name
290+ protected async getTreeFromWorkspaces ( tree : ProdDepType , packageName : string ) : Promise < ProdDepType > {
291+ if ( ! ( tree . workspaces && tree . dependencies ) ) {
292+ return tree
293+ }
251294
252- for ( const [ key , value ] of Object . entries ( tree . dependencies ) ) {
253- if ( key === dependencyName ) {
254- log . debug ( { key , path : value . path } , "returning workspace tree for root dependency " )
255- return value
256- }
295+ if ( tree . dependencies ?. [ packageName ] ) {
296+ const { name , path , dependencies } = tree . dependencies [ packageName ]
297+ log . debug ( { name , path, dependencies : JSON . stringify ( dependencies ) } , "pruning root app/self package from workspace tree " )
298+ for ( const [ name , pkg ] of Object . entries ( dependencies ?? { } ) ) {
299+ tree . dependencies [ name ] = pkg
257300 }
301+ delete tree . dependencies [ packageName ]
258302 }
259-
260- return tree
303+ return Promise . resolve ( tree )
261304 }
262305
263306 private transformToHoisterTree ( obj : DependencyGraph , key : string , nodes : Map < string , HoisterTree > = new Map ( ) ) : HoisterTree {
@@ -352,7 +395,7 @@ export abstract class NodeModulesCollector<ProdDepType extends Dependency<ProdDe
352395 await new Promise < void > ( ( resolve , reject ) => {
353396 const outStream = createWriteStream ( tempOutputFile )
354397
355- const child = spawnProcess ( command , args , {
398+ const child = childProcess . spawn ( command , args , {
356399 cwd,
357400 shell : false , // required to prevent console logs polution from shell profile loading when `true`
358401 } )
0 commit comments