Skip to content

Commit 1659a70

Browse files
committed
feat: support react-native/browser mappings on barebone engines
1 parent a27abf8 commit 1659a70

1 file changed

Lines changed: 96 additions & 11 deletions

File tree

bundler/bundle.js

Lines changed: 96 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import fsPromises, { readFile, writeFile, readdir } from 'node:fs/promises'
33
import { existsSync } from 'node:fs'
44
import { fileURLToPath, pathToFileURL } from 'node:url'
55
import { basename, dirname, extname, resolve, join, relative } from 'node:path'
6-
import { createRequire } from 'node:module'
6+
import nodeModule, { createRequire } from 'node:module'
77
import { randomUUID as uuid, randomBytes } from 'node:crypto'
88
import * as esbuild from 'esbuild'
99

@@ -13,6 +13,46 @@ const cjsMockRegex = /\.exodus-test-mock\.cjs$/u
1313
const cjsMockFallback = `throw new Error('Mocking loaded ESM modules in not possible in bundles')`
1414
let 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+
1656
const emptyToUndefined = (x) => (x.length > 0 ? x : undefined) // optimize out define if there are none
1757
const 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: /\.[cm]?[jt]sx?$/ }, (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: /\.[cm]?[jt]sx?$/, 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(/(\.[cm]?[jt]sx?)$/u, '.native$1')
530615
if (existsSync(maybeNative)) filepath = maybeNative

0 commit comments

Comments
 (0)