@@ -3,7 +3,7 @@ import fsPromises, { readFile, writeFile, readdir } from 'node:fs/promises'
33import { existsSync } from 'node:fs'
44import { fileURLToPath , pathToFileURL } from 'node:url'
55import { basename , dirname , extname , resolve , join , relative } from 'node:path'
6- import { createRequire } from 'node:module'
6+ import nodeModule , { createRequire } from 'node:module'
77import { randomUUID as uuid , randomBytes } from 'node:crypto'
88import * as esbuild from 'esbuild'
99
@@ -13,6 +13,46 @@ const cjsMockRegex = /\.exodus-test-mock\.cjs$/u
1313const cjsMockFallback = `throw new Error('Mocking loaded ESM modules in not possible in bundles')`
1414let resolveSrc , globLib
1515
16+ const packageJSONs = new Map ( )
17+ function findPackageJSON ( file ) {
18+ if ( packageJSONs . has ( file ) ) return packageJSONs . get ( file )
19+
20+ assert . equal ( resolve ( file ) , file ) // must be absolute and not end with a '/'
21+
22+ if ( nodeModule . findPackageJSON ) {
23+ const res = nodeModule . findPackageJSON ( pathToFileURL ( file ) ) ?? null
24+ packageJSONs . set ( file , res )
25+ return res
26+ }
27+
28+ // does not go into /package.json if file is a dir
29+ for ( let dir = dirname ( file ) ; dir ; ) {
30+ const res = join ( dir , 'package.json' )
31+ if ( existsSync ( res ) ) {
32+ packageJSONs . set ( file , res )
33+ return res
34+ }
35+
36+ const parent = dirname ( dir )
37+ if ( ! parent || parent === dir ) break
38+ dir = parent
39+ }
40+
41+ packageJSONs . set ( file , null )
42+ return null
43+ }
44+
45+ const reactNativeMaps = new Map ( )
46+ async function mapReactNative ( context ) {
47+ const pkg = findPackageJSON ( context )
48+ if ( ! pkg ) return [ null ]
49+ if ( reactNativeMaps . has ( pkg ) ) return reactNativeMaps . get ( pkg )
50+ const { browser, 'react-native' : rn } = JSON . parse ( await readFile ( pkg , 'utf8' ) )
51+ const res = [ ( rn ?? browser ) || null , dirname ( pkg ) ]
52+ reactNativeMaps . set ( pkg , res )
53+ return res
54+ }
55+
1656const emptyToUndefined = ( x ) => ( x . length > 0 ? x : undefined ) // optimize out define if there are none
1757const readSnapshots = async ( files , resolvers ) => {
1858 const snapshots = [ ]
@@ -435,14 +475,19 @@ export const build = async (...files) => {
435475 : JSON . stringify ( x , null , 1 ) . replaceAll ( / ^ * ( " .+ " ) ( , ? ) $ / gmu, ( _ , s , c ) => `${ wrap ( s ) } ${ c } ` )
436476 }
437477
438- const conditions = [ ]
478+ const conditions = [ ] // 'require' and 'import' are built-in
479+ const mainFields = [ 'module' , 'main' ]
439480 if ( process . env . EXODUS_TEST_PLATFORM === 'workerd' ) {
440481 conditions . push ( 'workerd' )
441482 } else if ( process . env . EXODUS_TEST_IS_BROWSER ) {
442483 // browsers, electron renderer, servo
443484 conditions . push ( 'browser' )
485+ mainFields . unshift ( 'browser' )
444486 } else if ( process . env . EXODUS_TEST_IS_BAREBONE ) {
445- conditions . push ( 'react-native' )
487+ conditions . push ( 'react-native' ) // does not set browser, see https://metrobundler.dev/docs/configuration/#unstable_conditionnames-experimental
488+ // FIXME: mainFields = react-native is unsupported by esbuild: https://github.com/evanw/esbuild/issues/4427
489+ // To not follow just browser here, we resolve that manually in onResolve plugin
490+ // mainFields.unshift('react-native', 'browser') // https://metrobundler.dev/docs/configuration/#resolvermainfields
446491 }
447492
448493 const config = {
@@ -456,7 +501,7 @@ export const build = async (...files) => {
456501 entryNames : filename ,
457502 platform : process . env . EXODUS_TEST_IS_BROWSER ? 'browser' : 'neutral' ,
458503 conditions,
459- mainFields : [ 'browser' , 'module' , 'main' ] , // FIXME: Removing 'browser' breaks some pkgs
504+ mainFields,
460505 define : {
461506 'process.browser' : stringify ( true ) ,
462507 'process.emitWarning' : 'undefined' ,
@@ -515,16 +560,56 @@ export const build = async (...files) => {
515560 plugins : [
516561 {
517562 name : 'exodus-test.bundle' ,
518- setup ( { onResolve, onLoad } ) {
519- onResolve ( { filter : / \. [ c m ] ? [ j t ] s x ? $ / } , ( args ) => {
520- if ( shouldInstallMocks && cjsMockRegex . test ( args . path ) ) {
521- return { path : args . path , namespace : 'file' }
563+ setup ( { onResolve, onLoad, resolve : esbuildResolve } ) {
564+ onResolve (
565+ { filter : process . env . EXODUS_TEST_IS_BAREBONE ? / ./ : cjsMockRegex , namespace : 'file' } ,
566+ async ( args ) => {
567+ let { path, ...opts } = args
568+ if ( shouldInstallMocks && cjsMockRegex . test ( path ) ) return { path, namespace : 'file' }
569+
570+ // This whole hack is needed because of https://github.com/evanw/esbuild/issues/4427
571+ if ( process . env . EXODUS_TEST_IS_BAREBONE ) {
572+ // Modules are mapped pre-resolve against importer
573+ if ( ! / ^ [ . / ] / u. test ( path ) ) {
574+ const [ map0 ] = await mapReactNative ( args . importer )
575+ if ( map0 && typeof map0 !== 'string' && Object . hasOwn ( map0 , path ) ) {
576+ if ( map0 [ path ] === false ) {
577+ // Unsupported, see https://github.com/evanw/esbuild/issues/4426
578+ // TODO
579+ } else {
580+ path = map0 [ path ]
581+ }
582+ }
583+ }
584+
585+ const r = await esbuildResolve ( path , { ...opts , namespace : 'exodus-test.bundle' } )
586+
587+ // Errors can only default to usual resolution to support e.g. optional dynamic require()
588+ if ( ! r . path || r . errors . length > 0 || r . warnings . length > 0 || r . external ) return
589+
590+ // Resolved files are mapped post-resolve against their package
591+ const [ map , dir ] = await mapReactNative ( r . path )
592+ if ( map ) {
593+ if ( typeof map === 'string' ) {
594+ // Only maps the entry point, check if we are using the entry point
595+ // TODO: also check for dir import to a package.json?
596+ if ( ! path . includes ( '/' ) ) r . path = resolve ( dir , map )
597+ } else {
598+ for ( const [ key , value ] of Object . entries ( map ) ) {
599+ if ( r . path !== resolve ( dir , key ) ) continue
600+ r . path = resolve ( dir , value )
601+ break
602+ }
603+ }
604+ }
605+
606+ return r
607+ }
522608 }
523- } )
609+ )
524610 onLoad ( { filter : / \. [ c m ] ? [ j t ] s x ? $ / , namespace : 'file' } , async ( args ) => {
525611 let filepath = args . path
526- // Resolve .native versions
527- // TODO: maybe follow package.json for this
612+ // Load .native versions where available (past onResolve)
528613 if ( process . env . EXODUS_TEST_IS_BAREBONE ) {
529614 const maybeNative = filepath . replace ( / ( \. [ c m ] ? [ j t ] s x ? ) $ / u, '.native$1' )
530615 if ( existsSync ( maybeNative ) ) filepath = maybeNative
0 commit comments