@vxrn/vite-plugin-metro 1.25.16 → 1.26.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/dist/cjs/metro-config/getMetroConfigFromViteConfig.cjs +20 -0
  2. package/dist/cjs/metro-config/getMetroConfigFromViteConfig.native.js +20 -0
  3. package/dist/cjs/metro-config/getMetroConfigFromViteConfig.native.js.map +1 -1
  4. package/dist/cjs/metro-config/patchMetroServerWithViteConfigAndMetroPluginOptions.cjs +2 -1
  5. package/dist/cjs/metro-config/patchMetroServerWithViteConfigAndMetroPluginOptions.native.js +2 -1
  6. package/dist/cjs/metro-config/patchMetroServerWithViteConfigAndMetroPluginOptions.native.js.map +1 -1
  7. package/dist/cjs/plugins/metroPlugin.native.js.map +1 -1
  8. package/dist/cjs/transformer/metroNativeWorker.cjs +1590 -0
  9. package/dist/cjs/transformer/metroNativeWorker.native.js +2674 -0
  10. package/dist/cjs/transformer/metroNativeWorker.native.js.map +1 -0
  11. package/dist/cjs/transformer/metroNativeWorker.test.cjs +886 -0
  12. package/dist/cjs/transformer/metroNativeWorker.test.native.js +995 -0
  13. package/dist/cjs/transformer/metroNativeWorker.test.native.js.map +1 -0
  14. package/dist/esm/metro-config/getMetroConfigFromViteConfig.mjs +20 -0
  15. package/dist/esm/metro-config/getMetroConfigFromViteConfig.mjs.map +1 -1
  16. package/dist/esm/metro-config/getMetroConfigFromViteConfig.native.js +20 -0
  17. package/dist/esm/metro-config/getMetroConfigFromViteConfig.native.js.map +1 -1
  18. package/dist/esm/metro-config/patchMetroServerWithViteConfigAndMetroPluginOptions.mjs +2 -1
  19. package/dist/esm/metro-config/patchMetroServerWithViteConfigAndMetroPluginOptions.mjs.map +1 -1
  20. package/dist/esm/metro-config/patchMetroServerWithViteConfigAndMetroPluginOptions.native.js +2 -1
  21. package/dist/esm/metro-config/patchMetroServerWithViteConfigAndMetroPluginOptions.native.js.map +1 -1
  22. package/dist/esm/plugins/metroPlugin.mjs.map +1 -1
  23. package/dist/esm/plugins/metroPlugin.native.js.map +1 -1
  24. package/dist/esm/transformer/metroNativeWorker.mjs +1531 -0
  25. package/dist/esm/transformer/metroNativeWorker.mjs.map +1 -0
  26. package/dist/esm/transformer/metroNativeWorker.native.js +2612 -0
  27. package/dist/esm/transformer/metroNativeWorker.native.js.map +1 -0
  28. package/dist/esm/transformer/metroNativeWorker.test.mjs +850 -0
  29. package/dist/esm/transformer/metroNativeWorker.test.mjs.map +1 -0
  30. package/dist/esm/transformer/metroNativeWorker.test.native.js +957 -0
  31. package/dist/esm/transformer/metroNativeWorker.test.native.js.map +1 -0
  32. package/package.json +22 -3
  33. package/src/metro-config/getMetroConfigFromViteConfig.ts +42 -0
  34. package/src/metro-config/patchMetroServerWithViteConfigAndMetroPluginOptions.ts +1 -0
  35. package/src/plugins/metroPlugin.ts +22 -0
  36. package/src/transformer/__fixtures__/exampleNativeTransform.cjs +17 -0
  37. package/src/transformer/metroNativeWorker.test.ts +1200 -0
  38. package/src/transformer/metroNativeWorker.ts +2379 -0
  39. package/src/transformer/types.ts +21 -0
  40. package/types/metro-config/getMetroConfigFromViteConfig.d.ts.map +1 -1
  41. package/types/metro-config/patchMetroServerWithViteConfigAndMetroPluginOptions.d.ts.map +1 -1
  42. package/types/plugins/metroPlugin.d.ts +22 -0
  43. package/types/plugins/metroPlugin.d.ts.map +1 -1
  44. package/types/transformer/metroNativeWorker.d.ts +251 -0
  45. package/types/transformer/metroNativeWorker.d.ts.map +1 -0
  46. package/types/transformer/metroNativeWorker.test.d.ts +2 -0
  47. package/types/transformer/metroNativeWorker.test.d.ts.map +1 -0
  48. package/types/transformer/types.d.ts +17 -0
  49. package/types/transformer/types.d.ts.map +1 -1
@@ -0,0 +1,2379 @@
1
+ import crypto from 'node:crypto'
2
+ import { createRequire } from 'node:module'
3
+ import fs from 'node:fs'
4
+ import path from 'node:path'
5
+ import { parseSync } from 'oxc-parser'
6
+ import MagicString from 'magic-string'
7
+ import remapping from '@jridgewell/remapping'
8
+ import { TraceMap, eachMapping } from '@jridgewell/trace-mapping'
9
+ import {
10
+ shouldStripFlow,
11
+ transformHermesAsync,
12
+ transformHermesLoops,
13
+ transformReactNativeCodegen,
14
+ } from '@vxrn/compiler'
15
+ import { getPlatformEnv, metroPlatformToViteEnvironment } from '../env/platformEnv'
16
+
17
+ /**
18
+ * Module-scope bindings initialized to a literal and never reassigned, so a
19
+ * `require.context` argument held in a variable resolves to its value. Metro
20
+ * evaluates those arguments rather than demanding literals, and one's router
21
+ * entry keeps its route root in exactly such a binding.
22
+ *
23
+ * The declaration kind is deliberately ignored. The Hermes loop-binding pass
24
+ * rewrites every `const` to `var`, so matching on the keyword would stop
25
+ * resolving the router root and drop every route from the bundle. Not being
26
+ * reassigned is the property that actually matters.
27
+ */
28
+ function collectModuleLiteralBindings(program: any): Map<string, any> {
29
+ const reassigned = new Set<string>()
30
+ const declCount = new Map<string, number>()
31
+
32
+ ;(function findReassignments(node: any) {
33
+ if (!node || typeof node !== 'object') return
34
+ if (Array.isArray(node)) {
35
+ for (const child of node) findReassignments(child)
36
+ return
37
+ }
38
+ if (node.type === 'AssignmentExpression' && node.left?.type === 'Identifier') {
39
+ reassigned.add(node.left.name)
40
+ } else if (node.type === 'UpdateExpression' && node.argument?.type === 'Identifier') {
41
+ reassigned.add(node.argument.name)
42
+ }
43
+ for (const key in node) {
44
+ if (key === 'type' || key === 'start' || key === 'end') continue
45
+ findReassignments(node[key])
46
+ }
47
+ })(program)
48
+
49
+ for (const stmt of program.body || []) {
50
+ if (stmt.type !== 'VariableDeclaration') continue
51
+ for (const decl of stmt.declarations || []) {
52
+ if (decl.id?.type === 'Identifier') {
53
+ declCount.set(decl.id.name, (declCount.get(decl.id.name) || 0) + 1)
54
+ }
55
+ }
56
+ }
57
+
58
+ const bindings = new Map<string, any>()
59
+ for (const stmt of program.body || []) {
60
+ if (stmt.type !== 'VariableDeclaration') continue
61
+ for (const decl of stmt.declarations || []) {
62
+ if (
63
+ decl.id?.type === 'Identifier' &&
64
+ decl.init?.type === 'Literal' &&
65
+ !reassigned.has(decl.id.name) &&
66
+ declCount.get(decl.id.name) === 1
67
+ ) {
68
+ bindings.set(decl.id.name, decl.init)
69
+ }
70
+ }
71
+ }
72
+ return bindings
73
+ }
74
+
75
+ export type MetroContextParams = {
76
+ recursive: boolean
77
+ filter: { pattern: string; flags: string }
78
+ mode: 'sync' | 'eager' | 'lazy' | 'lazy-once'
79
+ }
80
+
81
+ export type MetroDependencyData = {
82
+ key?: string
83
+ asyncType?: 'async' | 'weak' | 'maybeSync' | null
84
+ contextParams?: MetroContextParams
85
+ locs: Array<{ start: { line: number; column: number }; end: { line: number; column: number } }>
86
+ isOptional?: boolean
87
+ isESMImportAtSource?: boolean
88
+ isESMImport?: boolean
89
+ index?: number
90
+ }
91
+
92
+ export type MetroDependency = {
93
+ name: string
94
+ data: MetroDependencyData
95
+ }
96
+
97
+ export type MetroOutputData = {
98
+ code: string
99
+ lineCount: number
100
+ map: any[]
101
+ functionMap?: any
102
+ }
103
+
104
+ export type MetroOutput = {
105
+ data: MetroOutputData
106
+ type: 'js/module' | 'js/script' | 'js/module/asset'
107
+ }
108
+
109
+ export type MetroWorkerResult = {
110
+ dependencies: MetroDependency[]
111
+ output: MetroOutput[]
112
+ }
113
+
114
+ export type MetroWorkerConfig = {
115
+ assetRegistryPath?: string
116
+ assetPlugins?: string[]
117
+ asyncRequireModulePath?: string
118
+ globalPrefix?: string
119
+ unstable_dependencyMapReservedName?: string
120
+ unstable_disableModuleWrapping?: boolean
121
+ minifierPath?: string
122
+ minifierConfig?: any
123
+ publicPath?: string
124
+ allowOptionalDependencies?: any
125
+ dynamicDepsInPackages?: string
126
+ }
127
+
128
+ export type MetroWorkerOptions = {
129
+ dev?: boolean
130
+ minify?: boolean
131
+ platform?: 'ios' | 'android' | 'web' | string
132
+ type?: 'module' | 'script' | 'asset'
133
+ customTransformOptions?: Record<string, any>
134
+ unstable_transformProfile?: string
135
+ moduleId?: number | string
136
+ dependencyIds?: Array<number | string>
137
+ }
138
+
139
+ export type WrapModuleOptions = {
140
+ globalPrefix?: string
141
+ moduleId?: number | string
142
+ dependencyIds?: Array<number | string>
143
+ dependencyMapName?: string
144
+ requireAlias?: boolean
145
+ }
146
+
147
+ // bump whenever this worker's output changes, or metro serves cached modules
148
+ // transformed by the previous version.
149
+ const WORKER_CACHE_KEY_VERSION = '5'
150
+
151
+ /**
152
+ * react-native ships jsx inside plain .js files, and oxc disables jsx for .js
153
+ * unless told otherwise. parsing those as 'js' fails on the first JSX element,
154
+ * so every non-typescript file is parsed as jsx.
155
+ */
156
+ function langForFilename(filename: string): 'ts' | 'tsx' | 'jsx' {
157
+ if (filename.endsWith('.tsx')) return 'tsx'
158
+ if (filename.endsWith('.ts')) return 'ts'
159
+ return 'jsx'
160
+ }
161
+
162
+ export type OneRouterMetroOptions = {
163
+ ONE_ROUTER_APP_ROOT_RELATIVE_TO_ENTRY?: string
164
+ ONE_ROUTER_LINKING_CONFIG?: unknown
165
+ ONE_ROUTER_ROOT_FOLDER_NAME?: string
166
+ ONE_ROUTER_REQUIRE_CONTEXT_REGEX_STRING?: string
167
+ ONE_SETUP_FILE_NATIVE?: string
168
+ }
169
+
170
+ /**
171
+ * one's router options reach the transformer on the same channel the babel
172
+ * transformer reads them from, as the options of its `one-router-metro` plugin
173
+ * entry.
174
+ */
175
+ export function getOneRouterMetroOptions(options: MetroWorkerOptions): OneRouterMetroOptions | undefined {
176
+ const plugins = (options.customTransformOptions as any)?.vite?.babelConfig?.plugins
177
+ if (!Array.isArray(plugins)) return undefined
178
+ for (const plugin of plugins) {
179
+ if (Array.isArray(plugin) && typeof plugin[0] === 'string' && plugin[0].includes('one-router-metro')) {
180
+ return plugin[1] as OneRouterMetroOptions
181
+ }
182
+ }
183
+ return undefined
184
+ }
185
+
186
+ /**
187
+ * Reads the router root one's babel preset hands to
188
+ * `babel-plugin-remove-server-code`. Its absence means one did not ask for
189
+ * server-code removal, so the step is skipped rather than guessed at.
190
+ */
191
+ export function getRemoveServerCodeRouterRoot(
192
+ options: MetroWorkerOptions
193
+ ): string | undefined {
194
+ const plugins = (options.customTransformOptions as any)?.vite?.babelConfig?.plugins
195
+ if (!Array.isArray(plugins)) return undefined
196
+ for (const plugin of plugins) {
197
+ if (
198
+ Array.isArray(plugin) &&
199
+ typeof plugin[0] === 'string' &&
200
+ plugin[0].includes('remove-server-code')
201
+ ) {
202
+ return plugin[1]?.routerRoot ?? 'app'
203
+ }
204
+ }
205
+ return undefined
206
+ }
207
+
208
+ // the merged map is the same for every file in a bundle, so it is built once
209
+ // per (plugins array, platform) rather than per file.
210
+ const importMetaEnvCache = new WeakMap<
211
+ object,
212
+ Map<string, Record<string, string | boolean | undefined>>
213
+ >()
214
+
215
+ /**
216
+ * Reads the env map one hands to its `import-meta-env-plugin`, with the
217
+ * platform's own values layered on top exactly as that plugin does. Without it
218
+ * every `import.meta.env.X` read compiles to a property of the empty
219
+ * `var import_meta = {}` oxc emits when it lowers ESM to CJS, so the whole map
220
+ * silently reads `undefined` on native.
221
+ */
222
+ export function getImportMetaEnv(
223
+ options: MetroWorkerOptions
224
+ ): Record<string, string | boolean | undefined> {
225
+ const plugins = (options.customTransformOptions as any)?.vite?.babelConfig?.plugins
226
+ const platform = options.platform ?? ''
227
+
228
+ let byPlatform = Array.isArray(plugins) ? importMetaEnvCache.get(plugins) : undefined
229
+ const cached = byPlatform?.get(platform)
230
+ if (cached) return cached
231
+
232
+ let env: Record<string, string | boolean | undefined> = {}
233
+ if (Array.isArray(plugins)) {
234
+ for (const plugin of plugins) {
235
+ if (
236
+ Array.isArray(plugin) &&
237
+ typeof plugin[0] === 'string' &&
238
+ plugin[0].includes('import-meta-env-plugin')
239
+ ) {
240
+ const found = plugin[1]?.env
241
+ if (found && typeof found === 'object') env = found
242
+ }
243
+ }
244
+ }
245
+
246
+ const merged = {
247
+ ...env,
248
+ ...getPlatformEnv(metroPlatformToViteEnvironment(options.platform)),
249
+ }
250
+
251
+ if (Array.isArray(plugins)) {
252
+ if (!byPlatform) {
253
+ byPlatform = new Map()
254
+ importMetaEnvCache.set(plugins, byPlatform)
255
+ }
256
+ byPlatform.set(platform, merged)
257
+ }
258
+ return merged
259
+ }
260
+
261
+ // every babel plugin id this worker has a native port of. the worker runs no
262
+ // babel at all, so a plugin outside this list has no effect whatsoever.
263
+ const PORTED_BABEL_PLUGINS = [
264
+ 'import-meta-env-plugin',
265
+ 'one-router-metro',
266
+ 'remove-server-code',
267
+ 'module-resolver',
268
+ 'environment-guard',
269
+ 'inline-one-server-url',
270
+ // the worklets transform is a native oxc port, so both plugin names it can
271
+ // arrive under are covered.
272
+ 'react-native-worklets/plugin',
273
+ 'react-native-reanimated/plugin',
274
+ 'babel-plugin-react-compiler',
275
+ ]
276
+
277
+ // keyed on the plugins array so the scan happens once per bundle rather than
278
+ // once per file, while every file still fails.
279
+ const unportedPluginCache = new WeakMap<object, string[]>()
280
+
281
+ const loadedNativeTransforms = new Map<string, (code: string, ctx: any) => unknown>()
282
+
283
+ /**
284
+ * Loads the user's own native transforms — the thing a babel plugin becomes
285
+ * when there is no babel. Each module default-exports `(code, ctx) => string`.
286
+ * Resolved from the project root, since they are the app's dependencies rather
287
+ * than this package's.
288
+ */
289
+ function getNativeTransforms(
290
+ options: MetroWorkerOptions,
291
+ projectRoot: string
292
+ ): ((code: string, ctx: any) => unknown)[] {
293
+ const ids = (options.customTransformOptions as any)?.vite?.nativeTransformModules
294
+ if (!Array.isArray(ids) || !ids.length) return []
295
+
296
+ const req = createRequire(path.join(projectRoot, 'package.json'))
297
+ return ids.map((id: string) => {
298
+ const cached = loadedNativeTransforms.get(id)
299
+ if (cached) return cached
300
+
301
+ let mod: any
302
+ try {
303
+ mod = req(id)
304
+ } catch (err: any) {
305
+ throw new Error(
306
+ `[vxrn/metro] Could not load the native transform "${id}" from ${projectRoot}: ${err.message}`
307
+ )
308
+ }
309
+ const fn = mod?.default ?? mod
310
+ if (typeof fn !== 'function') {
311
+ throw new Error(
312
+ `[vxrn/metro] The native transform "${id}" must default-export a function (code, ctx) => string.`
313
+ )
314
+ }
315
+ loadedNativeTransforms.set(id, fn)
316
+ return fn
317
+ })
318
+ }
319
+
320
+ /**
321
+ * The same files @vxrn/compiler's `applyDefaultFilters` runs the react compiler
322
+ * on for the vite path: app typescript only. node_modules is excluded because
323
+ * react-native's own sources are flow, which the oxc react compiler rejects
324
+ * outright, and the app never asked for its dependencies to be memoized.
325
+ */
326
+ function isReactCompilerCandidate(filename: string): boolean {
327
+ if (!/\.tsx?$/.test(filename)) return false
328
+ if (filename.includes('node_modules')) return false
329
+ if (filename.includes('+api.')) return false
330
+ return true
331
+ }
332
+
333
+ /**
334
+ * Whether the babel plugin list one handed metro names `id`. The list is the
335
+ * only channel one has to say "the app turned this on", so the ported
336
+ * transforms read their own gate out of it.
337
+ */
338
+ function hasBabelPlugin(options: MetroWorkerOptions, id: string): boolean {
339
+ const plugins = (options.customTransformOptions as any)?.vite?.babelConfig?.plugins
340
+ if (!Array.isArray(plugins)) return false
341
+ return plugins.some((plugin) => (Array.isArray(plugin) ? plugin[0] : plugin) === id)
342
+ }
343
+
344
+ /**
345
+ * A babel plugin the user added through `bundlerOptions.babelConfigOverrides`
346
+ * would silently do nothing here, because this worker replaced babel outright.
347
+ * Silently dropping someone's OTA or instrumentation plugin is worse than
348
+ * refusing to build, so it is named and thrown.
349
+ */
350
+ export function assertNoUnportedBabelPlugins(options: MetroWorkerOptions): void {
351
+ const plugins = (options.customTransformOptions as any)?.vite?.babelConfig?.plugins
352
+ if (!Array.isArray(plugins)) return
353
+
354
+ let unported = unportedPluginCache.get(plugins)
355
+ if (!unported) {
356
+ unported = []
357
+ for (const plugin of plugins) {
358
+ const id = Array.isArray(plugin) ? plugin[0] : plugin
359
+ if (typeof id !== 'string') {
360
+ unported.push('(a function or object plugin, which cannot be ported)')
361
+ continue
362
+ }
363
+ if (!PORTED_BABEL_PLUGINS.some((ported) => id.includes(ported))) {
364
+ unported.push(id)
365
+ }
366
+ }
367
+ unportedPluginCache.set(plugins, unported)
368
+ }
369
+
370
+ if (unported.length) {
371
+ throw new Error(
372
+ `[vxrn/metro] the native transform path runs no babel, so these babel plugins would be silently ignored:\n` +
373
+ unported.map((id) => ` - ${id}`).join('\n') +
374
+ `\n\nPort each one to a native transform and list it in bundlerOptions.nativeTransformModules, ` +
375
+ `remove it from bundlerOptions.babelConfigOverrides, or set ` +
376
+ `bundlerOptions.nativeTransforms: false (or ONE_METRO_NATIVE_TRANSFORMS=0) ` +
377
+ `to go back to the babel transformer.`
378
+ )
379
+ }
380
+ }
381
+
382
+ /**
383
+ * Reads the alias map one's babel preset hands to `babel-plugin-module-resolver`
384
+ * (its "vite-tsconfig-paths for Metro"). Keys ending in `$` are exact matches,
385
+ * the rest are prefixes.
386
+ */
387
+ export function getModuleResolverAliases(
388
+ options: MetroWorkerOptions
389
+ ): Record<string, string> | undefined {
390
+ const plugins = (options.customTransformOptions as any)?.vite?.babelConfig?.plugins
391
+ if (!Array.isArray(plugins)) return undefined
392
+ for (const plugin of plugins) {
393
+ if (
394
+ Array.isArray(plugin) &&
395
+ typeof plugin[0] === 'string' &&
396
+ plugin[0].includes('module-resolver')
397
+ ) {
398
+ const alias = plugin[1]?.alias
399
+ if (alias && typeof alias === 'object') return alias
400
+ }
401
+ }
402
+ return undefined
403
+ }
404
+
405
+ /**
406
+ * Resolves one tsconfig-path alias to a specifier relative to the importing
407
+ * file. The native worker replaces the babel transformer, so without this every
408
+ * aliased import fails to resolve.
409
+ */
410
+ export function resolveAliasSpecifier(
411
+ specifier: string,
412
+ filename: string,
413
+ projectRoot: string,
414
+ aliases: Record<string, string>
415
+ ): string | undefined {
416
+ for (const [rawKey, value] of Object.entries(aliases)) {
417
+ let target: string | undefined
418
+ if (rawKey.endsWith('$')) {
419
+ if (specifier === rawKey.slice(0, -1)) target = value
420
+ } else if (specifier === rawKey || specifier.startsWith(`${rawKey}/`)) {
421
+ target = value + specifier.slice(rawKey.length)
422
+ }
423
+ if (target === undefined) continue
424
+
425
+ const abs = path.resolve(projectRoot, target)
426
+ let rel = path.relative(path.dirname(filename), abs).split(path.sep).join('/')
427
+ if (!rel.startsWith('.')) rel = `./${rel}`
428
+ return rel
429
+ }
430
+ return undefined
431
+ }
432
+
433
+ /**
434
+ * Rewrites aliased import/export/require specifiers in place.
435
+ */
436
+ export function applyModuleResolverAliases(
437
+ code: string,
438
+ filename: string,
439
+ projectRoot: string,
440
+ aliases: Record<string, string>
441
+ ): string {
442
+ const parsed = parseSync(filename, code, { lang: langForFilename(filename) })
443
+ if (parsed?.errors?.length) {
444
+ throw new Error(
445
+ `[vxrn/metro] Failed to parse ${filename} for alias resolution: ${parsed.errors[0].message}`
446
+ )
447
+ }
448
+ if (!parsed?.program) return code
449
+
450
+ const ms = new MagicString(code)
451
+ let changed = false
452
+
453
+ function rewrite(sourceNode: any) {
454
+ if (sourceNode?.type !== 'Literal' || typeof sourceNode.value !== 'string') return
455
+ const resolved = resolveAliasSpecifier(sourceNode.value, filename, projectRoot, aliases)
456
+ if (resolved === undefined) return
457
+ ms.overwrite(sourceNode.start, sourceNode.end, JSON.stringify(resolved))
458
+ changed = true
459
+ }
460
+
461
+ function walk(node: any) {
462
+ if (!node || typeof node !== 'object') return
463
+
464
+ if (
465
+ node.type === 'ImportDeclaration' ||
466
+ node.type === 'ExportNamedDeclaration' ||
467
+ node.type === 'ExportAllDeclaration' ||
468
+ node.type === 'ImportExpression'
469
+ ) {
470
+ rewrite(node.source)
471
+ } else if (
472
+ node.type === 'CallExpression' &&
473
+ node.callee?.type === 'Identifier' &&
474
+ node.callee.name === 'require'
475
+ ) {
476
+ rewrite(node.arguments?.[0])
477
+ }
478
+
479
+ for (const k of Object.keys(node)) {
480
+ if (k === 'start' || k === 'end' || k === 'loc' || k === 'range' || k === 'parent' || k === 'comments') continue
481
+ const child = node[k]
482
+ if (Array.isArray(child)) {
483
+ for (const item of child) walk(item)
484
+ } else if (child && typeof child === 'object') {
485
+ walk(child)
486
+ }
487
+ }
488
+ }
489
+
490
+ walk(parsed.program)
491
+ return changed ? ms.toString() : code
492
+ }
493
+
494
+ /**
495
+ * Native port of babel-preset-expo's `expo-inline-or-reference-env-vars` and
496
+ * one's `babel-plugin-inline-one-server-url`. In production every
497
+ * `process.env.EXPO_PUBLIC_*` read is inlined as a literal; in development each
498
+ * one is routed through the `expo/virtual/env` module so edits to .env take
499
+ * effect without a full rebuild. Without this the reads survive into the bundle
500
+ * and every EXPO_PUBLIC_ value is undefined at runtime.
501
+ *
502
+ * `process.env.ONE_SERVER_URL` is inlined in both modes, matching one's plugin:
503
+ * it is how a native bundle knows where to fetch loader data from, and a native
504
+ * runtime has no `process.env` to read it back out of.
505
+ *
506
+ * Both live in one pass because they are the same rewrite over the same walk,
507
+ * and a second parse of every file is the cost this transformer exists to avoid.
508
+ */
509
+ export function applyInlineEnvVars(
510
+ code: string,
511
+ filename: string,
512
+ isProduction: boolean,
513
+ env: Record<string, string | boolean | undefined> = {}
514
+ ): string {
515
+ const parsed = parseSync(filename, code, { lang: langForFilename(filename) })
516
+ if (parsed?.errors?.length) {
517
+ throw new Error(
518
+ `[vxrn/metro] Failed to parse ${filename} for env inlining: ${parsed.errors[0].message}`
519
+ )
520
+ }
521
+ if (!parsed?.program) return code
522
+
523
+ const ms = new MagicString(code)
524
+ let needsEnvImport = false
525
+
526
+ // edits are collected rather than written straight through, because a folded
527
+ // dead branch swallows the range an inner edit sits in and MagicString throws
528
+ // on overlapping overwrites.
529
+ const edits: { start: number; end: number; text: string }[] = []
530
+ // nodes whose value is known after inlining, so `if (import.meta.env.SSR)`
531
+ // can be resolved instead of left for metro to resolve imports inside.
532
+ const known = new Map<any, unknown>()
533
+
534
+ function replace(node: any, value: unknown) {
535
+ const text = value === undefined ? 'undefined' : JSON.stringify(value)
536
+ edits.push({ start: node.start, end: node.end, text })
537
+ known.set(node, value)
538
+ }
539
+
540
+ function keyOf(prop: any, computed: boolean): string | undefined {
541
+ if (!computed && prop?.type === 'Identifier') return prop.name
542
+ if (prop?.type === 'Literal' && typeof prop.value === 'string') return prop.value
543
+ return undefined
544
+ }
545
+
546
+ function isImportMeta(node: any): boolean {
547
+ return node?.type === 'MetaProperty' && node.meta?.name === 'import' && node.property?.name === 'meta'
548
+ }
549
+
550
+ function walk(node: any, parent: any) {
551
+ if (!node || typeof node !== 'object') return
552
+
553
+ if (node.type === 'MemberExpression' || node.type === 'OptionalMemberExpression') {
554
+ const obj = node.object
555
+ const isMember = obj?.type === 'MemberExpression' || obj?.type === 'OptionalMemberExpression'
556
+ const isProcessEnv =
557
+ isMember &&
558
+ obj.object?.type === 'Identifier' &&
559
+ obj.object.name === 'process' &&
560
+ keyOf(obj.property, obj.computed) === 'env'
561
+ // an assignment target is left alone, matching the babel plugin: rewriting
562
+ // it would produce a write to the virtual module.
563
+ const isAssignmentTarget =
564
+ parent?.type === 'AssignmentExpression' && parent.left === node
565
+ const key = keyOf(node.property, node.computed)
566
+
567
+ if (isProcessEnv && !isAssignmentTarget && key === 'ONE_SERVER_URL') {
568
+ replace(node, process.env.ONE_SERVER_URL || '')
569
+ return
570
+ }
571
+
572
+ if (isProcessEnv && !isAssignmentTarget && key?.startsWith('EXPO_PUBLIC_')) {
573
+ if (isProduction) {
574
+ replace(node, process.env[key] ?? undefined)
575
+ } else {
576
+ edits.push({ start: node.start, end: node.end, text: `_$$_EXPO_ENV.${key}` })
577
+ needsEnvImport = true
578
+ }
579
+ return
580
+ }
581
+
582
+ // `import.meta.env.X`. the walk is top-down, so this outer node is reached
583
+ // before the `import.meta.env` inside it and the shorter form never fires.
584
+ if (
585
+ !isAssignmentTarget &&
586
+ isMember &&
587
+ isImportMeta(obj.object) &&
588
+ keyOf(obj.property, obj.computed) === 'env' &&
589
+ key !== undefined
590
+ ) {
591
+ replace(node, env[key])
592
+ return
593
+ }
594
+
595
+ // bare `import.meta.env`, spread or passed around whole.
596
+ if (!isAssignmentTarget && isImportMeta(obj) && key === 'env') {
597
+ edits.push({ start: node.start, end: node.end, text: JSON.stringify(env) })
598
+ return
599
+ }
600
+
601
+ // `process.env.X` for anything the vite env map defines. runs after the
602
+ // two branches above so ONE_SERVER_URL and EXPO_PUBLIC_ keep their own
603
+ // handling.
604
+ if (isProcessEnv && !isAssignmentTarget && key !== undefined && key in env) {
605
+ replace(node, env[key])
606
+ return
607
+ }
608
+ }
609
+
610
+ for (const k of Object.keys(node)) {
611
+ if (k === 'start' || k === 'end' || k === 'loc' || k === 'range' || k === 'parent' || k === 'comments') continue
612
+ const child = node[k]
613
+ if (Array.isArray(child)) {
614
+ for (const item of child) walk(item, node)
615
+ } else if (child && typeof child === 'object') {
616
+ walk(child, node)
617
+ }
618
+ }
619
+ }
620
+
621
+ walk(parsed.program, null)
622
+
623
+ // resolves a test expression to a boolean when inlining made it constant.
624
+ function testValue(node: any): boolean | undefined {
625
+ if (known.has(node)) return !!known.get(node)
626
+ if (node?.type === 'UnaryExpression' && node.operator === '!') {
627
+ const inner = testValue(node.argument)
628
+ return inner === undefined ? undefined : !inner
629
+ }
630
+ if (node?.type === 'BinaryExpression' && (node.operator === '===' || node.operator === '!==')) {
631
+ const left = known.has(node.left) ? known.get(node.left) : undefined
632
+ const right = node.right?.type === 'Literal' ? node.right.value : undefined
633
+ if (!known.has(node.left) || node.right?.type !== 'Literal') return undefined
634
+ return node.operator === '===' ? left === right : left !== right
635
+ }
636
+ return undefined
637
+ }
638
+
639
+ // fold constant branches so metro never tries to resolve the imports inside a
640
+ // dead one. only the dead ranges are cut, never the kept branch, so env reads
641
+ // inside the surviving branch still get their own inlining.
642
+ const dead: { start: number; end: number; text: string }[] = []
643
+ ;(function foldWalk(node: any) {
644
+ if (!node || typeof node !== 'object') return
645
+ if (Array.isArray(node)) {
646
+ for (const child of node) foldWalk(child)
647
+ return
648
+ }
649
+ if (node.type === 'IfStatement' || node.type === 'ConditionalExpression') {
650
+ const value = testValue(node.test)
651
+ if (value !== undefined) {
652
+ const kept = value ? node.consequent : node.alternate
653
+ if (!kept) {
654
+ // `if (false) {...}` with no else: the whole statement is dead.
655
+ dead.push({ start: node.start, end: node.end, text: ';' })
656
+ return
657
+ }
658
+ // cutting `if (test)` off leaves the consequent as a bare statement or
659
+ // block, and cutting `test ?` leaves the branch as a bare expression.
660
+ dead.push({ start: node.start, end: kept.start, text: '' })
661
+ if (kept.end < node.end) {
662
+ dead.push({ start: kept.end, end: node.end, text: '' })
663
+ }
664
+ foldWalk(kept)
665
+ return
666
+ }
667
+ }
668
+ for (const k of Object.keys(node)) {
669
+ if (k === 'start' || k === 'end' || k === 'loc' || k === 'range' || k === 'parent' || k === 'comments') continue
670
+ foldWalk(node[k])
671
+ }
672
+ })(parsed.program)
673
+
674
+ for (const range of dead) {
675
+ ms.overwrite(range.start, range.end, range.text)
676
+ }
677
+ for (const edit of edits) {
678
+ if (dead.some((d) => edit.start >= d.start && edit.end <= d.end)) continue
679
+ ms.overwrite(edit.start, edit.end, edit.text)
680
+ }
681
+
682
+ if (needsEnvImport) {
683
+ ms.prepend(`import { env as _$$_EXPO_ENV } from "expo/virtual/env";\n`)
684
+ }
685
+ return ms.hasChanged() ? ms.toString() : code
686
+ }
687
+
688
+ // `native-only` is a side-effect guard that is satisfied simply by being in a
689
+ // native build, so the import is dropped. the other three assert the file is
690
+ // NOT in one, and a native bundle that keeps them silently ships web or server
691
+ // code, so the import becomes the throw it was standing in for.
692
+ const GUARD_SPECIFIERS = new Set(['server-only', 'client-only', 'web-only'])
693
+
694
+ /**
695
+ * Native port of one's `babel-plugin-environment-guard`.
696
+ */
697
+ export function applyEnvironmentGuard(code: string, filename: string): string {
698
+ const parsed = parseSync(filename, code, { lang: langForFilename(filename) })
699
+ if (parsed?.errors?.length) {
700
+ throw new Error(
701
+ `[vxrn/metro] Failed to parse ${filename} for environment guards: ${parsed.errors[0].message}`
702
+ )
703
+ }
704
+ const body = parsed?.program?.body
705
+ if (!body) return code
706
+
707
+ const ms = new MagicString(code)
708
+ let changed = false
709
+
710
+ for (const node of body) {
711
+ if (node.type !== 'ImportDeclaration') continue
712
+ const source = node.source?.value
713
+ if (source === 'native-only') {
714
+ ms.remove(node.start, node.end)
715
+ changed = true
716
+ } else if (GUARD_SPECIFIERS.has(source)) {
717
+ ms.overwrite(
718
+ node.start,
719
+ node.end,
720
+ `throw new Error(${JSON.stringify(`${source} cannot be imported in a native environment`)});`
721
+ )
722
+ changed = true
723
+ }
724
+ }
725
+
726
+ return changed ? ms.toString() : code
727
+ }
728
+
729
+ /**
730
+ * Native port of one's `babel-plugin-one-router-metro`. The native worker
731
+ * replaces the babel transformer wholesale, so without this the router entry
732
+ * keeps `process.env.ONE_ROUTER_*` reads that never resolve: `require.context`
733
+ * gets a non-literal regex and metro drops the entire route tree, and the
734
+ * configured setup file is never imported.
735
+ */
736
+ export function applyOneRouterMetro(
737
+ code: string,
738
+ filename: string,
739
+ options: OneRouterMetroOptions
740
+ ): string {
741
+ const isMetroEntry = filename.endsWith('metro-entry.js')
742
+ const isEntryCtx = filename.endsWith('metro-entry-ctx.js')
743
+ if (!isMetroEntry && !isEntryCtx) return code
744
+
745
+ const parsed = parseSync(filename, code, { lang: langForFilename(filename) })
746
+ if (parsed?.errors?.length) {
747
+ throw new Error(
748
+ `[vxrn/metro] Failed to parse ${filename} for one-router-metro: ${parsed.errors[0].message}`
749
+ )
750
+ }
751
+ if (!parsed?.program) return code
752
+
753
+ const ms = new MagicString(code)
754
+ let lastImportEnd = -1
755
+
756
+ function replacementFor(key: string): string | undefined {
757
+ if (key.startsWith('ONE_ROUTER_APP_ROOT_RELATIVE_TO_ENTRY')) {
758
+ return JSON.stringify(options.ONE_ROUTER_APP_ROOT_RELATIVE_TO_ENTRY)
759
+ }
760
+ if (key.startsWith('ONE_ROUTER_ROOT_FOLDER_NAME')) {
761
+ return JSON.stringify(options.ONE_ROUTER_ROOT_FOLDER_NAME)
762
+ }
763
+ if (key.startsWith('ONE_ROUTER_REQUIRE_CONTEXT_REGEX')) {
764
+ // must become a real regex literal: metro rejects any other node type as
765
+ // the third argument of require.context.
766
+ return `/${options.ONE_ROUTER_REQUIRE_CONTEXT_REGEX_STRING}/`
767
+ }
768
+ if (key === 'ONE_ROUTER_LINKING_CONFIG') {
769
+ // gated to the entry so a user module reading this name is left alone
770
+ return isMetroEntry ? JSON.stringify(options.ONE_ROUTER_LINKING_CONFIG ?? null) : undefined
771
+ }
772
+ if (key === 'ONE_SETUP_FILE_NATIVE') {
773
+ return options.ONE_SETUP_FILE_NATIVE
774
+ ? JSON.stringify(options.ONE_SETUP_FILE_NATIVE)
775
+ : 'undefined'
776
+ }
777
+ return undefined
778
+ }
779
+
780
+ function walk(node: any, parent: any) {
781
+ if (!node || typeof node !== 'object') return
782
+
783
+ if (node.type === 'ImportDeclaration' && parent === parsed.program) {
784
+ lastImportEnd = Math.max(lastImportEnd, node.end)
785
+ }
786
+
787
+ if (
788
+ node.type === 'MemberExpression' &&
789
+ node.object?.type === 'MemberExpression' &&
790
+ !node.object.computed &&
791
+ node.object.object?.type === 'Identifier' &&
792
+ node.object.object.name === 'process' &&
793
+ node.object.property?.type === 'Identifier' &&
794
+ node.object.property.name === 'env'
795
+ ) {
796
+ const key = node.computed
797
+ ? node.property?.type === 'Literal' && typeof node.property.value === 'string'
798
+ ? node.property.value
799
+ : undefined
800
+ : node.property?.type === 'Identifier'
801
+ ? node.property.name
802
+ : undefined
803
+ const isAssignTarget =
804
+ parent?.type === 'AssignmentExpression' && parent.left === node
805
+ if (key && !isAssignTarget) {
806
+ const replacement = replacementFor(key)
807
+ if (replacement !== undefined) {
808
+ ms.overwrite(node.start, node.end, replacement)
809
+ }
810
+ }
811
+ }
812
+
813
+ for (const k of Object.keys(node)) {
814
+ if (k === 'start' || k === 'end' || k === 'loc' || k === 'range' || k === 'parent' || k === 'comments') continue
815
+ const child = node[k]
816
+ if (Array.isArray(child)) {
817
+ for (const item of child) walk(item, node)
818
+ } else if (child && typeof child === 'object') {
819
+ walk(child, node)
820
+ }
821
+ }
822
+ }
823
+
824
+ walk(parsed.program, parsed.program)
825
+
826
+ // the setup file goes after the existing imports so react-native is
827
+ // initialized before it runs, matching the babel plugin's ordering.
828
+ if (isMetroEntry && options.ONE_SETUP_FILE_NATIVE) {
829
+ const stmt = `\nimport ${JSON.stringify(options.ONE_SETUP_FILE_NATIVE)};`
830
+ if (lastImportEnd >= 0) {
831
+ ms.appendRight(lastImportEnd, stmt)
832
+ } else {
833
+ ms.prepend(`${stmt}\n`)
834
+ }
835
+ }
836
+
837
+ return ms.toString()
838
+ }
839
+
840
+ /**
841
+ * Recursively collects identifier names from patterns (bindings).
842
+ */
843
+ export function collectPatternNames(pattern: any, names: Set<string>) {
844
+ if (!pattern) return
845
+ if (pattern.type === 'Identifier') {
846
+ names.add(pattern.name)
847
+ } else if (pattern.type === 'AssignmentPattern') {
848
+ collectPatternNames(pattern.left, names)
849
+ } else if (pattern.type === 'ObjectPattern') {
850
+ for (const prop of pattern.properties || []) {
851
+ if (prop.type === 'Property') collectPatternNames(prop.value, names)
852
+ else if (prop.type === 'RestElement') collectPatternNames(prop.argument, names)
853
+ }
854
+ } else if (pattern.type === 'ArrayPattern') {
855
+ for (const elem of pattern.elements || []) {
856
+ if (elem) collectPatternNames(elem, names)
857
+ }
858
+ } else if (pattern.type === 'RestElement') {
859
+ collectPatternNames(pattern.argument, names)
860
+ }
861
+ }
862
+
863
+ /**
864
+ * Recursively collects all hoisted `var` declarations and `FunctionDeclaration` names
865
+ * within a function body or program, stopping traversal at nested function boundaries.
866
+ */
867
+ export function collectHoistedBindings(node: any, names: Set<string>) {
868
+ function traverse(n: any) {
869
+ if (!n || typeof n !== 'object') return
870
+ if (n === node) {
871
+ // Root of traversal
872
+ } else if (
873
+ n.type === 'FunctionDeclaration' ||
874
+ n.type === 'FunctionExpression' ||
875
+ n.type === 'ArrowFunctionExpression'
876
+ ) {
877
+ if (n.type === 'FunctionDeclaration' && n.id?.name) {
878
+ names.add(n.id.name)
879
+ }
880
+ return
881
+ }
882
+
883
+ if (n.type === 'VariableDeclaration' && n.kind === 'var') {
884
+ for (const decl of n.declarations || []) {
885
+ collectPatternNames(decl.id, names)
886
+ }
887
+ }
888
+
889
+ for (const key of Object.keys(n)) {
890
+ if (key === 'start' || key === 'end' || key === 'loc' || key === 'range' || key === 'parent' || key === 'comments') continue
891
+ const child = n[key]
892
+ if (Array.isArray(child)) {
893
+ for (const item of child) traverse(item)
894
+ } else if (child && typeof child === 'object') {
895
+ traverse(child)
896
+ }
897
+ }
898
+ }
899
+
900
+ traverse(node)
901
+ }
902
+
903
+ /**
904
+ * Collects block-scoped declarations (let, const, class, and block-level function declarations)
905
+ * directly declared in this block (not inside nested blocks).
906
+ */
907
+ export function collectBlockBindings(statements: any[], names: Set<string>) {
908
+ if (!Array.isArray(statements)) return
909
+ for (const stmt of statements) {
910
+ if (!stmt) continue
911
+ if (stmt.type === 'VariableDeclaration' && stmt.kind !== 'var') {
912
+ for (const decl of stmt.declarations || []) {
913
+ collectPatternNames(decl.id, names)
914
+ }
915
+ } else if (stmt.type === 'ClassDeclaration' && stmt.id?.name) {
916
+ names.add(stmt.id.name)
917
+ } else if (stmt.type === 'FunctionDeclaration' && stmt.id?.name) {
918
+ names.add(stmt.id.name)
919
+ }
920
+ }
921
+ }
922
+
923
+ /**
924
+ * Lexical scope tracker for identifier shadowing analysis.
925
+ */
926
+ export class ScopeTracker {
927
+ private stack: Array<Set<string>> = [new Set()]
928
+
929
+ enter(initialBindings?: Iterable<string>) {
930
+ this.stack.push(new Set(initialBindings))
931
+ }
932
+
933
+ exit() {
934
+ if (this.stack.length > 1) {
935
+ this.stack.pop()
936
+ }
937
+ }
938
+
939
+ add(name: string) {
940
+ this.stack[this.stack.length - 1].add(name)
941
+ }
942
+
943
+ currentScope(): Set<string> {
944
+ return this.stack[this.stack.length - 1]
945
+ }
946
+
947
+ addPattern(pattern: any) {
948
+ collectPatternNames(pattern, this.currentScope())
949
+ }
950
+
951
+ isShadowed(name: string): boolean {
952
+ for (let i = this.stack.length - 1; i >= 0; i--) {
953
+ if (this.stack[i].has(name)) return true
954
+ }
955
+ return false
956
+ }
957
+ }
958
+
959
+ /**
960
+ * Cache key generation without any Babel references.
961
+ */
962
+ export function getCacheKey(config: MetroWorkerConfig = {}, opts?: any): string {
963
+ const hash = crypto.createHash('sha256')
964
+ hash.update('metroNativeWorker')
965
+ hash.update(WORKER_CACHE_KEY_VERSION)
966
+ if (config.globalPrefix) {
967
+ hash.update(config.globalPrefix)
968
+ }
969
+ if (config.unstable_dependencyMapReservedName) {
970
+ hash.update(config.unstable_dependencyMapReservedName)
971
+ }
972
+ if (config.minifierPath) {
973
+ hash.update(config.minifierPath)
974
+ }
975
+ if (opts?.projectRoot) {
976
+ hash.update(opts.projectRoot)
977
+ }
978
+ return hash.digest('hex')
979
+ }
980
+
981
+ /**
982
+ * Converts a standard SourceMap V3 object into Metro's raw mapping segment tuples.
983
+ * Metro expects an array of tuples: [line, column] or [line, column, origLine, origCol, name?]
984
+ * Mappings must be strictly sorted by generatedLine ascending, then generatedColumn ascending.
985
+ */
986
+ export function convertSourceMapToMetroRawMappings(
987
+ composedMap: any
988
+ ): Array<[number, number] | [number, number, number, number] | [number, number, number, number, string]> {
989
+ if (!composedMap) return []
990
+ const tracer = new TraceMap(composedMap)
991
+ const rawMappings: Array<[number, number] | [number, number, number, number] | [number, number, number, number, string]> = []
992
+
993
+ eachMapping(tracer, (m) => {
994
+ if (m.originalLine == null) {
995
+ rawMappings.push([m.generatedLine, m.generatedColumn])
996
+ } else if (m.name == null) {
997
+ rawMappings.push([m.generatedLine, m.generatedColumn, m.originalLine, m.originalColumn])
998
+ } else {
999
+ rawMappings.push([m.generatedLine, m.generatedColumn, m.originalLine, m.originalColumn, m.name])
1000
+ }
1001
+ })
1002
+
1003
+ // Mappings MUST be strictly sorted by generatedLine ascending, then generatedColumn ascending
1004
+ rawMappings.sort((a, b) => {
1005
+ if (a[0] !== b[0]) return a[0] - b[0]
1006
+ return a[1] - b[1]
1007
+ })
1008
+
1009
+ // Deduplicate consecutive identical (generatedLine, generatedColumn) tuples
1010
+ const deduped: Array<[number, number] | [number, number, number, number] | [number, number, number, number, string]> = []
1011
+ for (const mapping of rawMappings) {
1012
+ const prev = deduped[deduped.length - 1]
1013
+ if (prev && prev[0] === mapping[0] && prev[1] === mapping[1]) {
1014
+ continue
1015
+ }
1016
+ deduped.push(mapping)
1017
+ }
1018
+
1019
+ return deduped
1020
+ }
1021
+
1022
+ /**
1023
+ * Counts lines and ensures map terminates per Metro contract.
1024
+ */
1025
+ export function countLinesAndTerminateMap(
1026
+ code: string,
1027
+ map: Array<[number, number] | [number, number, number, number] | [number, number, number, number, string]> = []
1028
+ ): { lineCount: number; map: Array<[number, number] | [number, number, number, number] | [number, number, number, number, string]> } {
1029
+ const NEWLINE = /\r\n?|\n|\u2028|\u2029/g
1030
+ let lineCount = 1
1031
+ let lastLineStart = 0
1032
+ for (const match of code.matchAll(NEWLINE)) {
1033
+ lineCount++
1034
+ lastLineStart = match.index! + match[0].length
1035
+ }
1036
+ const lastLineLength = code.length - lastLineStart
1037
+ const lastLineIndex1Based = lineCount
1038
+ const lastLineNextColumn0Based = lastLineLength
1039
+ const lastMapping = map[map.length - 1]
1040
+ const terminatingMapping: [number, number] = [lastLineIndex1Based, lastLineNextColumn0Based]
1041
+ if (
1042
+ !lastMapping ||
1043
+ lastMapping[0] !== terminatingMapping[0] ||
1044
+ lastMapping[1] !== terminatingMapping[1]
1045
+ ) {
1046
+ return {
1047
+ lineCount,
1048
+ map: map.concat([terminatingMapping]),
1049
+ }
1050
+ }
1051
+ return {
1052
+ lineCount,
1053
+ map: [...map],
1054
+ }
1055
+ }
1056
+
1057
+ /**
1058
+ * Wraps JSON in CommonJS via module.exports and Metro's module wrapper.
1059
+ */
1060
+ export function wrapJson(
1061
+ source: string,
1062
+ options?: {
1063
+ globalPrefix?: string
1064
+ moduleId?: number | string
1065
+ dependencyIds?: Array<number | string>
1066
+ }
1067
+ ): string {
1068
+ const globalPrefix = options?.globalPrefix || ''
1069
+ const factory = [
1070
+ 'function (global, require, _importDefaultUnused, _importAllUnused, module, exports, _dependencyMapUnused) {',
1071
+ ` module.exports = ${source};`,
1072
+ '}',
1073
+ ].join('\n')
1074
+
1075
+ if (options?.moduleId !== undefined) {
1076
+ const depIds = options.dependencyIds ? JSON.stringify(options.dependencyIds) : '[]'
1077
+ return `${globalPrefix}__d(${factory}, ${JSON.stringify(options.moduleId)}, ${depIds});`
1078
+ }
1079
+
1080
+ return `${globalPrefix}__d(${factory});`
1081
+ }
1082
+
1083
+ /**
1084
+ * Wraps CommonJS code in Metro's standard define wrapper:
1085
+ * __d(function (global, _$$_REQUIRE, _$$_IMPORT_DEFAULT, _$$_IMPORT_ALL, module, exports, _dependencyMap) { ... }, moduleId, [dependencyIds])
1086
+ */
1087
+ export function wrapModule(
1088
+ code: string,
1089
+ options?: WrapModuleOptions
1090
+ ): string {
1091
+ const globalPrefix = options?.globalPrefix || ''
1092
+ const depMap = options?.dependencyMapName || '_dependencyMap'
1093
+ const header = `function (global, _$$_REQUIRE, _$$_IMPORT_DEFAULT, _$$_IMPORT_ALL, module, exports, ${depMap}) {`
1094
+ const requireAlias = options?.requireAlias !== false ? 'var require = _$$_REQUIRE;\n' : ''
1095
+ const footer = `}`
1096
+
1097
+ if (options?.moduleId !== undefined) {
1098
+ const depIds = options.dependencyIds ? JSON.stringify(options.dependencyIds) : '[]'
1099
+ return `${globalPrefix}__d(${header}\n${requireAlias}${code}\n${footer}, ${JSON.stringify(options.moduleId)}, ${depIds});`
1100
+ }
1101
+
1102
+ return `${globalPrefix}__d(${header}\n${requireAlias}${code}\n${footer});`
1103
+ }
1104
+
1105
+ /**
1106
+ * Metro canonical dependency qualifier key format.
1107
+ */
1108
+ export function getDependencyKey(
1109
+ name: string,
1110
+ isESM: boolean,
1111
+ asyncType: 'async' | 'weak' | 'maybeSync' | null = null,
1112
+ contextParams?: MetroContextParams
1113
+ ): string {
1114
+ let key = `${name}\0${isESM ? 'import' : 'require'}`
1115
+ if (asyncType != null) {
1116
+ key += `\0${asyncType}`
1117
+ }
1118
+ // mirrors metro's getKeyForDependency so a require.context dependency keys the
1119
+ // same way metro's own collectDependencies would key it.
1120
+ if (contextParams) {
1121
+ key += [
1122
+ '',
1123
+ 'context',
1124
+ String(contextParams.recursive),
1125
+ String(contextParams.filter.pattern),
1126
+ String(contextParams.filter.flags),
1127
+ contextParams.mode,
1128
+ ].join('\0')
1129
+ }
1130
+ return key
1131
+ }
1132
+
1133
+ /**
1134
+ * Rewrites require("dep") calls to Metro's dependency ABI:
1135
+ * require(_dependencyMap[index], "dep")
1136
+ * Respects lexical scope shadowing (does NOT rewrite local parameters or variables named require).
1137
+ */
1138
+ export type RewriteDependencyCallsResult = {
1139
+ code: string
1140
+ map: any
1141
+ toString(): string
1142
+ }
1143
+
1144
+ /**
1145
+ * Rewrites require("dep") calls and import("dep") expressions to Metro's dependency ABI:
1146
+ * require(_dependencyMap[index], "dep")
1147
+ * require(_dependencyMap[asyncIndex], "metro-runtime/src/modules/asyncRequire")(_dependencyMap[depIndex], _dependencyMap.paths, "dep")
1148
+ * Respects lexical scope shadowing and JS hoisting rules.
1149
+ * Returns { code, map, toString() } preserving source maps for the rewrite stage.
1150
+ */
1151
+ export function rewriteDependencyCalls(
1152
+ code: string,
1153
+ dependencies: MetroDependency[],
1154
+ depMapName = '_dependencyMap',
1155
+ filename = 'module.js',
1156
+ asyncRequirePath = 'metro-runtime/src/modules/asyncRequire'
1157
+ ): RewriteDependencyCallsResult {
1158
+ const ms = new MagicString(code)
1159
+
1160
+ const emptyResult: RewriteDependencyCallsResult = {
1161
+ code,
1162
+ map: ms.generateMap({ source: filename, hires: true, includeContent: true }),
1163
+ toString() {
1164
+ return code
1165
+ },
1166
+ }
1167
+
1168
+ if (dependencies.length === 0) return emptyResult
1169
+
1170
+ let parsed: any
1171
+ try {
1172
+ parsed = parseSync(filename, code, { lang: langForFilename(filename) })
1173
+ } catch (err: any) {
1174
+ throw new Error(
1175
+ `[vxrn/metro] Failed to parse ${filename} for dependency rewriting: ${err.message || err}`
1176
+ )
1177
+ }
1178
+
1179
+ // a silent bail here leaves raw require("./dep") calls that metro never
1180
+ // registered, so the module is absent from the bundle and throws at runtime.
1181
+ if (parsed?.errors?.length) {
1182
+ throw new Error(
1183
+ `[vxrn/metro] Failed to parse ${filename} for dependency rewriting: ${parsed.errors[0].message}`
1184
+ )
1185
+ }
1186
+
1187
+ if (!parsed?.program) return emptyResult
1188
+
1189
+ const contextLiteralBindings = collectModuleLiteralBindings(parsed.program)
1190
+
1191
+ const scopeTracker = new ScopeTracker()
1192
+ collectHoistedBindings(parsed.program, scopeTracker.currentScope())
1193
+ collectBlockBindings(parsed.program.body, scopeTracker.currentScope())
1194
+ for (const stmt of parsed.program.body || []) {
1195
+ if (stmt.type === 'ImportDeclaration') {
1196
+ for (const spec of stmt.specifiers || []) {
1197
+ if (spec.local?.name) {
1198
+ scopeTracker.add(spec.local.name)
1199
+ }
1200
+ }
1201
+ }
1202
+ }
1203
+
1204
+ function walk(node: any) {
1205
+ if (!node || typeof node !== 'object') return
1206
+
1207
+ let pushedScope = false
1208
+
1209
+ if (
1210
+ node.type === 'FunctionDeclaration' ||
1211
+ node.type === 'FunctionExpression' ||
1212
+ node.type === 'ArrowFunctionExpression'
1213
+ ) {
1214
+ scopeTracker.enter()
1215
+ pushedScope = true
1216
+ for (const param of node.params || []) {
1217
+ scopeTracker.addPattern(param)
1218
+ }
1219
+ if (node.type === 'FunctionExpression' && node.id?.name) {
1220
+ scopeTracker.add(node.id.name)
1221
+ }
1222
+ if (node.body) {
1223
+ collectHoistedBindings(node.body, scopeTracker.currentScope())
1224
+ }
1225
+ } else if (node.type === 'BlockStatement') {
1226
+ scopeTracker.enter()
1227
+ pushedScope = true
1228
+ collectBlockBindings(node.body, scopeTracker.currentScope())
1229
+ } else if (node.type === 'CatchClause') {
1230
+ scopeTracker.enter()
1231
+ pushedScope = true
1232
+ if (node.param) {
1233
+ scopeTracker.addPattern(node.param)
1234
+ }
1235
+ } else if (
1236
+ node.type === 'ForStatement' ||
1237
+ node.type === 'ForInStatement' ||
1238
+ node.type === 'ForOfStatement'
1239
+ ) {
1240
+ const decl = node.type === 'ForStatement' ? node.init : node.left
1241
+ if (decl?.type === 'VariableDeclaration' && decl.kind !== 'var') {
1242
+ scopeTracker.enter()
1243
+ pushedScope = true
1244
+ for (const d of decl.declarations || []) {
1245
+ scopeTracker.addPattern(d.id)
1246
+ }
1247
+ }
1248
+ }
1249
+
1250
+ if (
1251
+ node.type === 'ImportExpression' &&
1252
+ node.source?.type === 'Literal' &&
1253
+ typeof node.source.value === 'string'
1254
+ ) {
1255
+ const depName = node.source.value
1256
+ const depKey = getDependencyKey(depName, true, 'async')
1257
+ const dep =
1258
+ dependencies.find((d) => d.data.key === depKey) ||
1259
+ dependencies.find((d) => d.name === depName && d.data.asyncType === 'async')
1260
+
1261
+ const asyncKey = getDependencyKey(asyncRequirePath, false, null)
1262
+ const asyncDep =
1263
+ dependencies.find((d) => d.data.key === asyncKey) ||
1264
+ dependencies.find((d) => d.name === asyncRequirePath && d.data.asyncType == null)
1265
+
1266
+ if (dep && asyncDep) {
1267
+ const depIndex = dep.data.index ?? dependencies.indexOf(dep)
1268
+ const asyncIndex = asyncDep.data.index ?? dependencies.indexOf(asyncDep)
1269
+ ms.overwrite(
1270
+ node.start,
1271
+ node.end,
1272
+ `require(${depMapName}[${asyncIndex}], ${JSON.stringify(asyncRequirePath)})(${depMapName}[${depIndex}], ${depMapName}.paths, ${JSON.stringify(depName)})`
1273
+ )
1274
+ }
1275
+ } else if (
1276
+ node.type === 'CallExpression' &&
1277
+ node.callee?.type === 'Identifier' &&
1278
+ node.callee.name === 'require' &&
1279
+ !scopeTracker.isShadowed('require') &&
1280
+ node.arguments?.length >= 1
1281
+ ) {
1282
+ const arg = node.arguments[0]
1283
+ if (arg.type === 'Literal' && typeof arg.value === 'string') {
1284
+ const depKey = getDependencyKey(arg.value, false, null)
1285
+ const dep =
1286
+ dependencies.find((d) => d.data.key === depKey) ||
1287
+ dependencies.find((d) => d.name === arg.value && d.data.asyncType == null)
1288
+ if (dep) {
1289
+ const index = dep.data.index ?? dependencies.indexOf(dep)
1290
+ ms.overwrite(arg.start, arg.end, `${depMapName}[${index}], ${JSON.stringify(arg.value)}`)
1291
+ }
1292
+ }
1293
+ } else if (
1294
+ // metro rewrites require.context(dir, ...) down to a plain require of the
1295
+ // single context module it registered for that directory.
1296
+ node.type === 'CallExpression' &&
1297
+ node.callee?.type === 'MemberExpression' &&
1298
+ !node.callee.computed &&
1299
+ node.callee.object?.type === 'Identifier' &&
1300
+ node.callee.object.name === 'require' &&
1301
+ node.callee.property?.type === 'Identifier' &&
1302
+ node.callee.property.name === 'context' &&
1303
+ !scopeTracker.isShadowed('require') &&
1304
+ node.arguments?.length >= 1
1305
+ ) {
1306
+ // resolved the same way dependency extraction resolved it. matching only
1307
+ // a direct Literal here left a variable-held directory registered as a
1308
+ // dependency but never rewritten, so the call reached metro's
1309
+ // fallbackRequireContext at runtime and threw.
1310
+ const arg =
1311
+ node.arguments[0]?.type === 'Identifier'
1312
+ ? contextLiteralBindings.get(node.arguments[0].name)
1313
+ : node.arguments[0]
1314
+ if (arg?.type === 'Literal' && typeof arg.value === 'string') {
1315
+ const dep = dependencies.find(
1316
+ (d) => d.name === arg.value && d.data.contextParams != null
1317
+ )
1318
+ if (dep) {
1319
+ const index = dep.data.index ?? dependencies.indexOf(dep)
1320
+ ms.overwrite(
1321
+ node.start,
1322
+ node.end,
1323
+ `require(${depMapName}[${index}], ${JSON.stringify(arg.value)})`
1324
+ )
1325
+ }
1326
+ }
1327
+ }
1328
+
1329
+ for (const key of Object.keys(node)) {
1330
+ if (key === 'start' || key === 'end' || key === 'loc' || key === 'range' || key === 'parent' || key === 'comments') continue
1331
+ const child = node[key]
1332
+ if (Array.isArray(child)) {
1333
+ for (const item of child) walk(item)
1334
+ } else if (child && typeof child === 'object') {
1335
+ walk(child)
1336
+ }
1337
+ }
1338
+
1339
+ if (pushedScope) {
1340
+ scopeTracker.exit()
1341
+ }
1342
+ }
1343
+
1344
+ walk(parsed.program)
1345
+ const resultString = ms.toString()
1346
+ const map = ms.generateMap({ source: filename, hires: true, includeContent: true })
1347
+ return {
1348
+ code: resultString,
1349
+ map,
1350
+ toString() {
1351
+ return resultString
1352
+ },
1353
+ }
1354
+ }
1355
+
1356
+ /**
1357
+ * Extracts dependencies using oxc-parser (zero Babel).
1358
+ * Respects lexical scope shadowing and JS hoisting rules.
1359
+ */
1360
+ export function extractDependencies(
1361
+ code: string,
1362
+ filename: string,
1363
+ options?: {
1364
+ asyncRequireModulePath?: string
1365
+ allowOptionalDependencies?: any
1366
+ }
1367
+ ): MetroDependency[] {
1368
+ const asyncRequirePath =
1369
+ options?.asyncRequireModulePath || 'metro-runtime/src/modules/asyncRequire'
1370
+ const allowOptionalDependencies = options?.allowOptionalDependencies
1371
+ const lineOffsets = [0]
1372
+ for (let i = 0; i < code.length; i++) {
1373
+ if (code[i] === '\n') {
1374
+ lineOffsets.push(i + 1)
1375
+ }
1376
+ }
1377
+
1378
+ function offsetToLoc(pos: number): { line: number; column: number } {
1379
+ let low = 0
1380
+ let high = lineOffsets.length - 1
1381
+ while (low <= high) {
1382
+ const mid = (low + high) >> 1
1383
+ if (lineOffsets[mid] <= pos) {
1384
+ low = mid + 1
1385
+ } else {
1386
+ high = mid - 1
1387
+ }
1388
+ }
1389
+ const line = high + 1
1390
+ const column = pos - lineOffsets[high]
1391
+ return { line, column }
1392
+ }
1393
+
1394
+ let parseResult: any
1395
+ try {
1396
+ parseResult = parseSync(filename, code, { lang: langForFilename(filename) })
1397
+ } catch (err: any) {
1398
+ throw new Error(`[vxrn/metro] Failed to parse ${filename} for dependency extraction: ${err.message || err}`)
1399
+ }
1400
+
1401
+ // oxc reports syntax errors on the result rather than throwing. ignoring them
1402
+ // yields zero dependencies, which metro turns into a bundle that is missing
1403
+ // modules and only fails at runtime, so surface it here instead.
1404
+ if (parseResult?.errors?.length) {
1405
+ throw new Error(
1406
+ `[vxrn/metro] Failed to parse ${filename} for dependency extraction: ${parseResult.errors[0].message}`
1407
+ )
1408
+ }
1409
+
1410
+ if (!parseResult?.program) {
1411
+ return []
1412
+ }
1413
+
1414
+ const depMap = new Map<string, MetroDependency>()
1415
+
1416
+ function addDep(
1417
+ name: string,
1418
+ isESM: boolean,
1419
+ asyncType: 'async' | 'weak' | 'maybeSync' | null,
1420
+ start: number,
1421
+ end: number,
1422
+ contextParams?: MetroContextParams,
1423
+ isOptional?: boolean
1424
+ ) {
1425
+ const key = getDependencyKey(name, isESM, asyncType, contextParams)
1426
+ let entry = depMap.get(key)
1427
+ // metro reads loc.start.line / loc.end.line when it formats an
1428
+ // unable-to-resolve error. a flat {line, column} makes that error path throw
1429
+ // instead, which hides the real resolution failure behind a crash.
1430
+ const loc = { start: offsetToLoc(start), end: offsetToLoc(end) }
1431
+ if (!entry) {
1432
+ entry = {
1433
+ name,
1434
+ data: {
1435
+ key,
1436
+ asyncType,
1437
+ locs: [loc],
1438
+ isESMImportAtSource: isESM,
1439
+ isESMImport: isESM,
1440
+ index: depMap.size,
1441
+ ...(contextParams ? { contextParams } : {}),
1442
+ ...(isOptional ? { isOptional: true } : {}),
1443
+ },
1444
+ }
1445
+ depMap.set(key, entry)
1446
+ } else {
1447
+ entry.data.locs.push(loc)
1448
+ }
1449
+ }
1450
+
1451
+ // metro evaluates require.context arguments instead of demanding literals, so
1452
+ // a module-scope const holding the value works there. this covers that one
1453
+ // case rather than reimplementing babel's full constant evaluation.
1454
+ const moduleConsts = collectModuleLiteralBindings(parseResult.program)
1455
+
1456
+ function resolveConst(node: any): any {
1457
+ if (node?.type === 'Identifier') return moduleConsts.get(node.name)
1458
+ return node
1459
+ }
1460
+
1461
+ // mirrors metro's isOptionalDependency: a require within three statement
1462
+ // levels of a try block is allowed to fail resolution. optional native modules
1463
+ // like react-native-worklets-core are required exactly this way, and treating
1464
+ // them as required fails the whole build.
1465
+ const ancestors: any[] = []
1466
+ function isOptionalHere(name: string): boolean {
1467
+ if (!allowOptionalDependencies) return false
1468
+ if (Array.isArray(allowOptionalDependencies.exclude)) {
1469
+ if (allowOptionalDependencies.exclude.includes(name)) return false
1470
+ }
1471
+ let sCount = 0
1472
+ for (let i = ancestors.length - 1; i >= 0 && sCount < 3; i--) {
1473
+ const n = ancestors[i]
1474
+ const isStatement = /(?:Statement|Declaration)$/.test(n.type)
1475
+ if (!isStatement) continue
1476
+ if (n.type === 'BlockStatement') {
1477
+ const parent = ancestors[i - 1]
1478
+ return parent?.type === 'TryStatement' && parent.block === n
1479
+ }
1480
+ sCount += 1
1481
+ }
1482
+ return false
1483
+ }
1484
+
1485
+ const scopeTracker = new ScopeTracker()
1486
+ collectHoistedBindings(parseResult.program, scopeTracker.currentScope())
1487
+ collectBlockBindings(parseResult.program.body, scopeTracker.currentScope())
1488
+ for (const stmt of parseResult.program.body || []) {
1489
+ if (stmt.type === 'ImportDeclaration') {
1490
+ for (const spec of stmt.specifiers || []) {
1491
+ if (spec.local?.name) {
1492
+ scopeTracker.add(spec.local.name)
1493
+ }
1494
+ }
1495
+ }
1496
+ }
1497
+
1498
+ function walk(node: any) {
1499
+ if (!node || typeof node !== 'object') return
1500
+
1501
+ let pushedScope = false
1502
+ ancestors.push(node)
1503
+
1504
+ if (
1505
+ node.type === 'FunctionDeclaration' ||
1506
+ node.type === 'FunctionExpression' ||
1507
+ node.type === 'ArrowFunctionExpression'
1508
+ ) {
1509
+ scopeTracker.enter()
1510
+ pushedScope = true
1511
+ for (const param of node.params || []) {
1512
+ scopeTracker.addPattern(param)
1513
+ }
1514
+ if (node.type === 'FunctionExpression' && node.id?.name) {
1515
+ scopeTracker.add(node.id.name)
1516
+ }
1517
+ if (node.body) {
1518
+ collectHoistedBindings(node.body, scopeTracker.currentScope())
1519
+ }
1520
+ } else if (node.type === 'BlockStatement') {
1521
+ scopeTracker.enter()
1522
+ pushedScope = true
1523
+ collectBlockBindings(node.body, scopeTracker.currentScope())
1524
+ } else if (node.type === 'CatchClause') {
1525
+ scopeTracker.enter()
1526
+ pushedScope = true
1527
+ if (node.param) {
1528
+ scopeTracker.addPattern(node.param)
1529
+ }
1530
+ } else if (
1531
+ node.type === 'ForStatement' ||
1532
+ node.type === 'ForInStatement' ||
1533
+ node.type === 'ForOfStatement'
1534
+ ) {
1535
+ const decl = node.type === 'ForStatement' ? node.init : node.left
1536
+ if (decl?.type === 'VariableDeclaration' && decl.kind !== 'var') {
1537
+ scopeTracker.enter()
1538
+ pushedScope = true
1539
+ for (const d of decl.declarations || []) {
1540
+ scopeTracker.addPattern(d.id)
1541
+ }
1542
+ }
1543
+ }
1544
+
1545
+ if (node.type === 'ImportDeclaration' && node.source?.value) {
1546
+ if (node.importKind !== 'type') {
1547
+ const hasValueSpecifier =
1548
+ !node.specifiers?.length ||
1549
+ node.specifiers.some((s: any) => s.importKind !== 'type')
1550
+ if (hasValueSpecifier) {
1551
+ addDep(node.source.value, true, null, node.start, node.end)
1552
+ }
1553
+ }
1554
+ } else if (
1555
+ (node.type === 'ExportNamedDeclaration' || node.type === 'ExportAllDeclaration') &&
1556
+ node.source?.value
1557
+ ) {
1558
+ if (node.exportKind !== 'type') {
1559
+ addDep(node.source.value, true, null, node.start, node.end)
1560
+ }
1561
+ } else if (node.type === 'ImportExpression') {
1562
+ if (node.source?.type === 'Literal' && typeof node.source.value === 'string') {
1563
+ addDep(node.source.value, true, 'async', node.start, node.end)
1564
+ addDep(asyncRequirePath, false, null, node.start, node.end)
1565
+ } else {
1566
+ throw new Error(`[vxrn/metro] Dynamic import with non-string literal is not supported by Metro in ${filename}`)
1567
+ }
1568
+ } else if (
1569
+ node.type === 'CallExpression' &&
1570
+ node.callee?.type === 'Identifier' &&
1571
+ node.callee.name === 'require' &&
1572
+ !scopeTracker.isShadowed('require')
1573
+ ) {
1574
+ if (
1575
+ node.arguments?.[0]?.type === 'Literal' &&
1576
+ typeof node.arguments[0].value === 'string'
1577
+ ) {
1578
+ addDep(
1579
+ node.arguments[0].value,
1580
+ false,
1581
+ null,
1582
+ node.start,
1583
+ node.end,
1584
+ undefined,
1585
+ node.arguments[0].value !== asyncRequirePath && isOptionalHere(node.arguments[0].value)
1586
+ )
1587
+ }
1588
+ } else if (
1589
+ // require.context(dir, recursive?, filter?, mode?) registers a whole
1590
+ // directory as one dependency. one's router entry uses it for the route
1591
+ // tree, so dropping it silently omits every route from the bundle.
1592
+ node.type === 'CallExpression' &&
1593
+ node.callee?.type === 'MemberExpression' &&
1594
+ !node.callee.computed &&
1595
+ node.callee.object?.type === 'Identifier' &&
1596
+ node.callee.object.name === 'require' &&
1597
+ node.callee.property?.type === 'Identifier' &&
1598
+ node.callee.property.name === 'context' &&
1599
+ !scopeTracker.isShadowed('require')
1600
+ ) {
1601
+ const args = node.arguments || []
1602
+ // metro evaluates these arguments rather than requiring literals, so a
1603
+ // module-scope const holding the string is valid. one's router entry reads
1604
+ // its route root through exactly such a const.
1605
+ const dir = resolveConst(args[0])
1606
+ if (dir?.type !== 'Literal' || typeof dir.value !== 'string') {
1607
+ throw new Error(
1608
+ `[vxrn/metro] First argument of require.context must be a string literal in ${filename}`
1609
+ )
1610
+ }
1611
+ if (args.length > 4) {
1612
+ throw new Error(
1613
+ `[vxrn/metro] Too many arguments provided to require.context in ${filename}. Expected 4, got: ${args.length}`
1614
+ )
1615
+ }
1616
+
1617
+ let recursive = true
1618
+ const recursiveArg = resolveConst(args[1])
1619
+ if (recursiveArg) {
1620
+ if (recursiveArg.type !== 'Literal' || typeof recursiveArg.value !== 'boolean') {
1621
+ throw new Error(
1622
+ `[vxrn/metro] Second argument of require.context must be an optional boolean literal in ${filename}`
1623
+ )
1624
+ }
1625
+ recursive = recursiveArg.value
1626
+ }
1627
+
1628
+ let filter = { pattern: '.*', flags: '' }
1629
+ const filterArg = resolveConst(args[2])
1630
+ if (filterArg) {
1631
+ // oxc reports regex literals as Literal nodes carrying a `regex` field.
1632
+ if (filterArg.type !== 'Literal' || !filterArg.regex) {
1633
+ throw new Error(
1634
+ `[vxrn/metro] Third argument of require.context must be an optional RegExp literal in ${filename}, instead found node of type: ${filterArg.type}`
1635
+ )
1636
+ }
1637
+ filter = { pattern: filterArg.regex.pattern, flags: filterArg.regex.flags || '' }
1638
+ }
1639
+
1640
+ let mode: MetroContextParams['mode'] = 'sync'
1641
+ const modeArg = resolveConst(args[3])
1642
+ if (modeArg) {
1643
+ const m = modeArg.type === 'Literal' ? modeArg.value : undefined
1644
+ if (m !== 'sync' && m !== 'eager' && m !== 'lazy' && m !== 'lazy-once') {
1645
+ throw new Error(
1646
+ `[vxrn/metro] require.context mode must be one of sync, eager, lazy, lazy-once in ${filename}`
1647
+ )
1648
+ }
1649
+ mode = m
1650
+ }
1651
+
1652
+ addDep(dir.value, false, null, node.start, node.end, { recursive, filter, mode })
1653
+ }
1654
+
1655
+ for (const key of Object.keys(node)) {
1656
+ if (key === 'start' || key === 'end' || key === 'loc' || key === 'range' || key === 'parent' || key === 'comments') continue
1657
+ const child = node[key]
1658
+ if (Array.isArray(child)) {
1659
+ for (const item of child) walk(item)
1660
+ } else if (child && typeof child === 'object') {
1661
+ walk(child)
1662
+ }
1663
+ }
1664
+
1665
+ ancestors.pop()
1666
+ if (pushedScope) {
1667
+ scopeTracker.exit()
1668
+ }
1669
+ }
1670
+
1671
+ walk(parseResult.program)
1672
+ return Array.from(depMap.values())
1673
+ }
1674
+
1675
+ function checkReservedStrings(sourceCode: string, config: MetroWorkerConfig, options: MetroWorkerOptions) {
1676
+ const reservedStrings: string[] = []
1677
+ if (
1678
+ options.customTransformOptions?.unstable_staticHermesOptimizedRequire === true
1679
+ ) {
1680
+ reservedStrings.push('_$$_METRO_MODULE_ID')
1681
+ }
1682
+ if (config.unstable_dependencyMapReservedName != null) {
1683
+ reservedStrings.push(config.unstable_dependencyMapReservedName)
1684
+ }
1685
+ for (const reservedString of reservedStrings) {
1686
+ const position = sourceCode.indexOf(reservedString)
1687
+ if (position > -1) {
1688
+ throw new SyntaxError(
1689
+ 'Source code contains the reserved string `' +
1690
+ reservedString +
1691
+ '` at character offset ' +
1692
+ position
1693
+ )
1694
+ }
1695
+ }
1696
+ }
1697
+
1698
+ /**
1699
+ * Main transform entry point conforming to Metro's worker contract with ZERO Babel.
1700
+ */
1701
+ let workletsConfigured = false
1702
+
1703
+ /**
1704
+ * Turns on @vxrn/compiler's reanimated transform inside this metro worker
1705
+ * process when the project actually depends on reanimated.
1706
+ */
1707
+ type OneNativeTransforms = typeof import('one/native-transforms')
1708
+
1709
+ let oneNativeTransforms: OneNativeTransforms | null | undefined
1710
+
1711
+ /**
1712
+ * Resolves `one/native-transforms` from the user's project. Returns null once
1713
+ * and stays null when one isn't installed, so a bare vite-plugin-metro app
1714
+ * doesn't pay a failed resolve per file.
1715
+ */
1716
+ function loadOneNativeTransforms(projectRoot: string) {
1717
+ if (oneNativeTransforms !== undefined) return oneNativeTransforms
1718
+ try {
1719
+ // require, not import: this worker runs as cjs, so `resolve` lands on one's
1720
+ // cjs build, whose named exports an `import()` namespace would not carry.
1721
+ const req = createRequire(path.resolve(projectRoot, 'package.json'))
1722
+ oneNativeTransforms = req('one/native-transforms') as OneNativeTransforms
1723
+ } catch {
1724
+ oneNativeTransforms = null
1725
+ }
1726
+ return oneNativeTransforms
1727
+ }
1728
+
1729
+ async function configureWorkletsForWorker(projectRoot: string | undefined) {
1730
+ if (workletsConfigured) return
1731
+ workletsConfigured = true
1732
+ if (!projectRoot) return
1733
+
1734
+ const req = createRequire(path.resolve(projectRoot, 'package.json'))
1735
+ let hasReanimated = false
1736
+ try {
1737
+ req.resolve('react-native-reanimated/package.json')
1738
+ hasReanimated = true
1739
+ } catch {
1740
+ try {
1741
+ req.resolve('react-native-worklets/package.json')
1742
+ hasReanimated = true
1743
+ } catch {}
1744
+ }
1745
+
1746
+ const { configureVXRNCompilerPlugin } = await import('@vxrn/compiler')
1747
+ configureVXRNCompilerPlugin({ enableReanimated: hasReanimated })
1748
+ }
1749
+
1750
+ export async function transform(
1751
+ config: MetroWorkerConfig,
1752
+ projectRoot: string,
1753
+ filename: string,
1754
+ data: Buffer | string,
1755
+ options: MetroWorkerOptions
1756
+ ): Promise<MetroWorkerResult> {
1757
+ assertNoUnportedBabelPlugins(options)
1758
+
1759
+ let sourceCode = typeof data === 'string' ? data : data.toString('utf8')
1760
+
1761
+ // expo's own transform worker substitutes the source of two virtual files
1762
+ // before transforming them. this worker replaces that worker outright, so
1763
+ // without the same substitution `expo/virtual/env` stays a bare
1764
+ // `process.env` re-export and no .env file ever reaches the bundle.
1765
+ const environment = options.customTransformOptions?.environment
1766
+ const isClientEnvironment = environment !== 'node' && environment !== 'react-server'
1767
+
1768
+ if (isClientEnvironment && /[\\/]expo[\\/]virtual[\\/]env\.js$/.test(filename)) {
1769
+ if (options.dev) {
1770
+ const rel = path.relative(path.dirname(filename), projectRoot).split(path.sep).join('/')
1771
+ sourceCode = `const dotEnvModules = require.context(${JSON.stringify(rel)},false,/^\\.\\/\\.env/);
1772
+ export const env = !dotEnvModules.keys().length ? process.env : { ...process.env, ...['.env', '.env.development', '.env.local', '.env.development.local'].reduce((acc, file) => {
1773
+ return { ...acc, ...(dotEnvModules(file)?.default ?? {}) };
1774
+ }, {}) };`
1775
+ } else {
1776
+ // production inlines every value at its use site, so reaching this module
1777
+ // at all is a bug worth naming rather than silently returning undefined.
1778
+ sourceCode = `export const env = new Proxy({}, {
1779
+ get(target, key) {
1780
+ throw new Error(\`Attempting to access internal environment variable "\${String(key)}" is not supported in production bundles.\`);
1781
+ },
1782
+ });`
1783
+ }
1784
+ } else if (/(^|[\\/])\.env(\.(local|(development|production)(\.local)?))?$/.test(filename)) {
1785
+ const { parseEnvFile } = await import(
1786
+ '@expo/metro-config/build/transform-worker/dot-env-development'
1787
+ )
1788
+ sourceCode = `export default ${JSON.stringify(parseEnvFile(sourceCode, isClientEnvironment))};`
1789
+ }
1790
+
1791
+ checkReservedStrings(sourceCode, config, options)
1792
+
1793
+ // 1. JSON files
1794
+ if (filename.endsWith('.json')) {
1795
+ try {
1796
+ JSON.parse(sourceCode)
1797
+ } catch (err: any) {
1798
+ throw new Error(`[vxrn/metro] JSON parse error in ${filename}: ${err.message || err}`)
1799
+ }
1800
+
1801
+ let code =
1802
+ config.unstable_disableModuleWrapping === true
1803
+ ? `module.exports = ${sourceCode};`
1804
+ : wrapJson(sourceCode, {
1805
+ globalPrefix: config.globalPrefix,
1806
+ moduleId: options.moduleId,
1807
+ dependencyIds: options.dependencyIds,
1808
+ })
1809
+
1810
+ if (
1811
+ options.minify &&
1812
+ options.unstable_transformProfile !== 'hermes-canary' &&
1813
+ options.unstable_transformProfile !== 'hermes-stable' &&
1814
+ config.minifierPath
1815
+ ) {
1816
+ try {
1817
+ const minifier = require(config.minifierPath)
1818
+ const minified = await minifier({
1819
+ code,
1820
+ config: config.minifierConfig,
1821
+ filename,
1822
+ })
1823
+ code = minified.code
1824
+ } catch (err: any) {
1825
+ throw new Error(`[vxrn/metro] Minifier failed for ${filename}: ${err.message || err}`)
1826
+ }
1827
+ }
1828
+
1829
+ const { lineCount, map } = countLinesAndTerminateMap(code, [])
1830
+ const outputType: MetroOutput['type'] =
1831
+ options.type === 'asset'
1832
+ ? 'js/module/asset'
1833
+ : options.type === 'script'
1834
+ ? 'js/script'
1835
+ : 'js/module'
1836
+
1837
+ return {
1838
+ dependencies: [],
1839
+ output: [
1840
+ {
1841
+ data: {
1842
+ code,
1843
+ lineCount,
1844
+ map,
1845
+ functionMap: null,
1846
+ },
1847
+ type: outputType,
1848
+ },
1849
+ ],
1850
+ }
1851
+ }
1852
+
1853
+ // 2. Asset files
1854
+ if (options.type === 'asset') {
1855
+ const assetRegistryPath =
1856
+ config.assetRegistryPath || 'react-native/Libraries/Image/AssetRegistry'
1857
+ let properDescriptor: any = null
1858
+
1859
+ const absolutePath = path.resolve(projectRoot, filename)
1860
+ if (fs.existsSync(absolutePath)) {
1861
+ try {
1862
+ let getAssetData: any
1863
+ try {
1864
+ getAssetData = require('metro/private/Assets').getAssetData
1865
+ } catch {
1866
+ getAssetData = require('metro/src/Assets').getAssetData
1867
+ }
1868
+
1869
+ if (typeof getAssetData === 'function') {
1870
+ const rawData = await getAssetData(
1871
+ absolutePath,
1872
+ filename,
1873
+ config.assetPlugins || [],
1874
+ options.platform || '',
1875
+ config.publicPath || '/assets'
1876
+ )
1877
+ const copied = { ...rawData }
1878
+ delete copied.files
1879
+ delete copied.fileSystemLocation
1880
+ delete copied.path
1881
+ properDescriptor = copied
1882
+ }
1883
+ } catch (err: any) {
1884
+ throw new Error(`[vxrn/metro] Asset resolution failed for ${filename}: ${err.message || err}`)
1885
+ }
1886
+ }
1887
+
1888
+ if (!properDescriptor) {
1889
+ const ext = path.extname(filename).slice(1)
1890
+ const name = path.basename(filename, '.' + ext)
1891
+ properDescriptor = {
1892
+ __packager_asset: true,
1893
+ httpServerLocation: config.publicPath || '/assets',
1894
+ width: undefined,
1895
+ height: undefined,
1896
+ scales: [1],
1897
+ hash: crypto.createHash('md5').update(sourceCode).digest('hex'),
1898
+ name,
1899
+ type: ext,
1900
+ }
1901
+ }
1902
+
1903
+ // the registry is dependency 0 below, and metro's runtime require takes a
1904
+ // module id from the dependency map. emitting the bare specifier here leaves
1905
+ // a require metro cannot resolve, so every asset throws when it renders.
1906
+ const assetDepMapName = config.unstable_dependencyMapReservedName || '_dependencyMap'
1907
+ const assetCode = `module.exports = require(${assetDepMapName}[0], ${JSON.stringify(assetRegistryPath)}).registerAsset(${JSON.stringify(properDescriptor)});`
1908
+ let code =
1909
+ config.unstable_disableModuleWrapping === true
1910
+ ? assetCode
1911
+ : wrapModule(assetCode, {
1912
+ globalPrefix: config.globalPrefix,
1913
+ moduleId: options.moduleId,
1914
+ dependencyIds: options.dependencyIds,
1915
+ dependencyMapName: config.unstable_dependencyMapReservedName,
1916
+ })
1917
+
1918
+ const dependencies: MetroDependency[] = [
1919
+ {
1920
+ name: assetRegistryPath,
1921
+ data: {
1922
+ key: getDependencyKey(assetRegistryPath, false, null),
1923
+ asyncType: null,
1924
+ locs: [],
1925
+ isESMImportAtSource: false,
1926
+ isESMImport: false,
1927
+ index: 0,
1928
+ },
1929
+ },
1930
+ ]
1931
+
1932
+ const { lineCount, map } = countLinesAndTerminateMap(code, [])
1933
+ return {
1934
+ dependencies,
1935
+ output: [
1936
+ {
1937
+ data: {
1938
+ code,
1939
+ lineCount,
1940
+ map,
1941
+ functionMap: null,
1942
+ },
1943
+ type: 'js/module/asset',
1944
+ },
1945
+ ],
1946
+ }
1947
+ }
1948
+
1949
+ // 3. JS / TS files
1950
+ let code = sourceCode
1951
+ const intermediateMaps: any[] = []
1952
+
1953
+ // Step A00: the user's own native transforms, each standing in for a babel
1954
+ // plugin. they run on the file exactly as it came off disk, because in the
1955
+ // babel path every plugin visits one AST parsed from that source, so a plugin
1956
+ // that reads positions (jump-to-source stamps file:line:column) sees original
1957
+ // line numbers. one's ports below are sequential string rewrites that shift
1958
+ // lines, so running these after them would report a location the editor
1959
+ // cannot jump to.
1960
+ for (const nativeTransform of getNativeTransforms(options, projectRoot)) {
1961
+ const out = nativeTransform(code, {
1962
+ // absolute, unlike metro's own project-relative name. a transform that
1963
+ // matches on paths (include/exclude lists, node_modules skips) or stamps
1964
+ // a location into the output would otherwise get a different answer here
1965
+ // than the babel plugin it replaced, which is handed an absolute path.
1966
+ filename: path.isAbsolute(filename) ? filename : path.resolve(projectRoot, filename),
1967
+ platform: options.platform,
1968
+ dev: options.dev,
1969
+ projectRoot,
1970
+ })
1971
+ if (typeof out === 'string') code = out
1972
+ }
1973
+
1974
+ // Step A0: one's router entry rewrites. must run before dependency extraction
1975
+ // so the injected setup import and the inlined require.context regex are both
1976
+ // visible to it.
1977
+ const oneRouterOptions = getOneRouterMetroOptions(options)
1978
+ if (oneRouterOptions) {
1979
+ code = applyOneRouterMetro(code, filename, oneRouterOptions)
1980
+ }
1981
+
1982
+
1983
+ // Step A0b: React Native codegen. `codegenNativeComponent('RNSScreen')` is a
1984
+ // spec that has to become a real view config registration; without it the
1985
+ // component has no view config and the screen never mounts. Metro's default
1986
+ // pipeline gets this from @react-native/babel-plugin-codegen inside
1987
+ // @react-native/babel-preset, which this transformer replaces.
1988
+ //
1989
+ // This runs before Flow stripping because codegen reads the component's props
1990
+ // out of the type argument to codegenNativeComponent. Once Step A removes the
1991
+ // types there is nothing left to generate from, and react-native's own parser
1992
+ // rejects the file with "Could not find component config".
1993
+ try {
1994
+ const codegenRes = transformReactNativeCodegen(code, filename, projectRoot)
1995
+ if (codegenRes?.code) {
1996
+ code = codegenRes.code
1997
+ if (codegenRes.map) {
1998
+ intermediateMaps.push(codegenRes.map)
1999
+ }
2000
+ }
2001
+ } catch (err: any) {
2002
+ throw new Error(
2003
+ `[vxrn/metro] React Native codegen failed for ${filename}: ${err.message || err}`
2004
+ )
2005
+ }
2006
+
2007
+ // Step A: Flow stripping
2008
+ if (shouldStripFlow(filename, code)) {
2009
+ try {
2010
+ const fft = await import('fast-flow-transform')
2011
+ const flowTransform = fft.default || fft
2012
+ const flowResult = await flowTransform({
2013
+ filename,
2014
+ source: code,
2015
+ sourcemap: true,
2016
+ dialect: 'flow',
2017
+ format: 'pretty',
2018
+ })
2019
+ if (flowResult?.code) {
2020
+ code = flowResult.code
2021
+ if (flowResult.map) {
2022
+ intermediateMaps.push(
2023
+ typeof flowResult.map === 'string' ? JSON.parse(flowResult.map) : flowResult.map
2024
+ )
2025
+ }
2026
+ }
2027
+ } catch (err: any) {
2028
+ throw new Error(`[vxrn/metro] Flow transform failed for ${filename}: ${err.message || err}`)
2029
+ }
2030
+ }
2031
+
2032
+ const asyncRequirePath =
2033
+ config.asyncRequireModulePath || 'metro-runtime/src/modules/asyncRequire'
2034
+
2035
+ // Step A1: tsconfig path aliases. runs after flow stripping so the parse can
2036
+ // succeed, and before dependency extraction so metro only ever sees
2037
+ // specifiers it can resolve. the substring guard keeps the extra parse off
2038
+ // the files that have no aliased import at all.
2039
+ const aliases = getModuleResolverAliases(options)
2040
+ if (aliases && projectRoot) {
2041
+ const aliasKeys = Object.keys(aliases)
2042
+ if (aliasKeys.some((k) => code.includes(k.endsWith('$') ? k.slice(0, -1) : k))) {
2043
+ code = applyModuleResolverAliases(code, filename, projectRoot, aliases)
2044
+ }
2045
+ }
2046
+
2047
+ // Step A2: expo public env vars, one's server url, and `import.meta.env`.
2048
+ // after flow stripping so the parse succeeds, before extraction so both the
2049
+ // injected `expo/virtual/env` import and any folded-away dead branch are seen.
2050
+ // the substring guards keep the extra parse off files with no such read at all.
2051
+ if (code.includes('process.env') || code.includes('import.meta')) {
2052
+ code = applyInlineEnvVars(code, filename, !options.dev, getImportMetaEnv(options))
2053
+ }
2054
+
2055
+ // Step A3: one's environment guards. `import 'server-only'` in a native
2056
+ // bundle has to become a throw, and it has to happen before extraction or
2057
+ // metro tries to resolve a module that only exists to be rejected.
2058
+ if (code.includes('-only')) {
2059
+ code = applyEnvironmentGuard(code, filename)
2060
+ }
2061
+
2062
+ // Step A4: one's server-only route exports. a route's `loader` runs on the
2063
+ // server, and leaving it in drags everything it imports — database clients,
2064
+ // secrets — into the app bundle. this reuses one's own oxc tree-shaker rather
2065
+ // than reimplementing its dead-code elimination here; it is resolved out of
2066
+ // the user's `one` install because one depends on this package, not the
2067
+ // reverse.
2068
+ const serverCodeRouterRoot = getRemoveServerCodeRouterRoot(options)
2069
+ if (serverCodeRouterRoot && projectRoot && !filename.includes('node_modules')) {
2070
+ const oneNativeTransforms = loadOneNativeTransforms(projectRoot)
2071
+ if (oneNativeTransforms) {
2072
+ const shaken = await oneNativeTransforms.transformTreeShakeClient(
2073
+ code,
2074
+ filename,
2075
+ projectRoot,
2076
+ serverCodeRouterRoot
2077
+ )
2078
+ if (shaken?.code) {
2079
+ code = shaken.code
2080
+ }
2081
+ }
2082
+ }
2083
+
2084
+ // Step B: Extract dependencies using oxc-parser (zero Babel, lexical scope aware)
2085
+ const dependencies = extractDependencies(code, filename, {
2086
+ asyncRequireModulePath: asyncRequirePath,
2087
+ allowOptionalDependencies: config.allowOptionalDependencies,
2088
+ })
2089
+
2090
+ // determine worklet candidates before react compiler can hoist them.
2091
+ //
2092
+ // @vxrn/compiler's reanimated gate is process-global state set by one's vite
2093
+ // plugin. metro runs its transformer in separate worker processes that never
2094
+ // see that call, so the gate reads false there and every worklet in the app
2095
+ // silently ships untransformed. configure it once per worker from the same
2096
+ // fact the vite side derives it from: does the project have reanimated.
2097
+ await configureWorkletsForWorker(projectRoot)
2098
+
2099
+ let shouldRunWorklets = Boolean(options.customTransformOptions?.worklets)
2100
+ if (!shouldRunWorklets) {
2101
+ const { shouldTransformWorklets } = await import('@vxrn/compiler')
2102
+ shouldRunWorklets = shouldTransformWorklets({ id: filename, code })
2103
+ }
2104
+
2105
+ // Step C: React Compiler (via @vxrn/compiler / oxc-transform-react)
2106
+ //
2107
+ // `react: { compiler: true }` in one's config reaches metro as
2108
+ // babel-plugin-react-compiler in the babel plugin list, not as expo's
2109
+ // customTransformOptions.reactCompiler (which only ever comes from a
2110
+ // `transform.reactCompiler` query param on the bundle url). the babel
2111
+ // transformer read the plugin list, so this one has to as well or the app
2112
+ // asked for the compiler and silently got nothing.
2113
+ const shouldRunReactCompiler =
2114
+ Boolean(options.customTransformOptions?.reactCompiler) ||
2115
+ (hasBabelPlugin(options, 'babel-plugin-react-compiler') && isReactCompilerCandidate(filename)) ||
2116
+ code.includes('"use memo"') ||
2117
+ code.includes("'use memo'")
2118
+
2119
+ if (shouldRunReactCompiler) {
2120
+ try {
2121
+ const { prepareWorkletsForReactCompiler, transformOxcReactCompiler } =
2122
+ await import('@vxrn/compiler')
2123
+ if (shouldRunWorklets) {
2124
+ const prepared = prepareWorkletsForReactCompiler(filename, code, true)
2125
+ if (prepared) {
2126
+ code = prepared.code
2127
+ if (prepared.map) intermediateMaps.push(prepared.map)
2128
+ }
2129
+ }
2130
+ const compilerRes = await transformOxcReactCompiler(filename, code, '19', true)
2131
+ if (compilerRes?.code) {
2132
+ code = compilerRes.code
2133
+ if (compilerRes.map) {
2134
+ intermediateMaps.push(
2135
+ typeof compilerRes.map === 'string' ? JSON.parse(compilerRes.map) : compilerRes.map
2136
+ )
2137
+ }
2138
+ }
2139
+ } catch (err: any) {
2140
+ throw new Error(`[vxrn/metro] React Compiler failed for ${filename}: ${err.message || err}`)
2141
+ }
2142
+ }
2143
+
2144
+ // Step D: Native Worklets (via @vxrn/compiler)
2145
+ if (shouldRunWorklets) {
2146
+ try {
2147
+ const { transformWorklets } = await import('@vxrn/compiler')
2148
+ // no pluginVersion here: the compiler reads it from the worklets package
2149
+ // the app actually installed, and worklets throws at runtime when the
2150
+ // stamped version is not exactly its own.
2151
+ const workletRes = await transformWorklets(filename, code, true, {
2152
+ projectRoot,
2153
+ })
2154
+ if (workletRes?.code) {
2155
+ code = workletRes.code
2156
+ if (workletRes.map) {
2157
+ intermediateMaps.push(
2158
+ typeof workletRes.map === 'string' ? JSON.parse(workletRes.map) : workletRes.map
2159
+ )
2160
+ }
2161
+ }
2162
+ } catch (err: any) {
2163
+ throw new Error(`[vxrn/metro] Worklets transform failed for ${filename}: ${err.message || err}`)
2164
+ }
2165
+ }
2166
+
2167
+ // Step E: Hermes lowering + CommonJS + TS/JSX via oxc + esbuild (zero Babel, zero SWC)
2168
+ const lang = langForFilename(filename)
2169
+
2170
+ const { transformSync: oxcTransform } = await import('oxc-transform')
2171
+ let oxcRes: any
2172
+ try {
2173
+ oxcRes = oxcTransform(filename, code, {
2174
+ lang,
2175
+ target: 'es2020',
2176
+ assumptions: {
2177
+ setPublicClassFields: true,
2178
+ },
2179
+ jsx: {
2180
+ runtime: 'automatic',
2181
+ development: Boolean(options.dev),
2182
+ },
2183
+ sourcemap: true,
2184
+ })
2185
+ } catch (err: any) {
2186
+ throw new Error(`[vxrn/metro] Oxc transform failed for ${filename}: ${err.message || err}`)
2187
+ }
2188
+
2189
+ // oxc reports errors on the result rather than throwing. falling back to the
2190
+ // untransformed source here leaves jsx and typescript in place, which only
2191
+ // fails later in esbuild with a misleading message about the wrong step.
2192
+ if (oxcRes.errors?.length) {
2193
+ throw new Error(
2194
+ `[vxrn/metro] Oxc transform failed for ${filename}: ${oxcRes.errors[0].message || oxcRes.errors[0]}`
2195
+ )
2196
+ }
2197
+
2198
+ code = oxcRes.code || code
2199
+ if (oxcRes.map) {
2200
+ intermediateMaps.push(
2201
+ typeof oxcRes.map === 'string' ? JSON.parse(oxcRes.map) : oxcRes.map
2202
+ )
2203
+ }
2204
+
2205
+ // run after worklets, including scripts; only module wrapping is conditional.
2206
+ try {
2207
+ const esbuildRes = await transformHermesAsync(
2208
+ code, filename, true, options.type === 'script' ? undefined : 'cjs'
2209
+ )
2210
+ if (esbuildRes) {
2211
+ code = esbuildRes.code
2212
+ if (esbuildRes.map) {
2213
+ intermediateMaps.push(esbuildRes.map)
2214
+ }
2215
+ }
2216
+ } catch (err: any) {
2217
+ throw new Error(`[vxrn/metro] native async/CJS lowering failed for ${filename}: ${err.message || err}`)
2218
+ }
2219
+
2220
+ // Step E2: per-iteration loop bindings for Hermes.
2221
+ //
2222
+ // Runs after esbuild because esbuild's own __copyProps interop helper is built
2223
+ // on `for (let key of ...)` with a closure in the body, which under Hermes
2224
+ // makes every named export of a module resolve to its last one. See
2225
+ // transformHermesLoops for the engine detail and the bytecode evidence.
2226
+ try {
2227
+ const loopsRes = transformHermesLoops(code, filename)
2228
+ if (loopsRes) {
2229
+ code = loopsRes.code
2230
+ for (const map of loopsRes.maps) {
2231
+ intermediateMaps.push(map)
2232
+ }
2233
+ }
2234
+ } catch (err: any) {
2235
+ throw new Error(
2236
+ `[vxrn/metro] Hermes loop-binding transform failed for ${filename}: ${err.message || err}`
2237
+ )
2238
+ }
2239
+
2240
+ // step B read the original source, but steps C through E both add and remove
2241
+ // imports: they inject the jsx runtime, react/compiler-runtime, oxc's
2242
+ // @oxc-project/runtime helpers and the asset registry, and oxc elides
2243
+ // type-only imports typescript never emits. metro resolves and bundles exactly
2244
+ // this list, so an extra entry is an unresolvable module that fails the build
2245
+ // and a missing one is a module absent from the bundle. reconcile against what
2246
+ // the final code actually requires.
2247
+ const finalDeps = extractDependencies(code, filename, {
2248
+ asyncRequireModulePath: asyncRequirePath,
2249
+ allowOptionalDependencies: config.allowOptionalDependencies,
2250
+ })
2251
+ const finalNames = new Set(finalDeps.map((d) => d.name))
2252
+ // keep the original entries, which carry the source's ESM metadata that the
2253
+ // cjs-lowered code no longer shows, then append whatever was injected.
2254
+ const reconciled = dependencies.filter((d) => finalNames.has(d.name))
2255
+ for (const injected of finalDeps) {
2256
+ if (reconciled.some((d) => d.name === injected.name)) continue
2257
+ reconciled.push(injected)
2258
+ }
2259
+ dependencies.length = 0
2260
+ for (const dep of reconciled) {
2261
+ dependencies.push({ ...dep, data: { ...dep.data, index: dependencies.length } })
2262
+ }
2263
+
2264
+ // Step F: Module Wrapping & Dependency ABI Rewriting with full sourcemap tracking
2265
+ const depMapName = config.unstable_dependencyMapReservedName || '_dependencyMap'
2266
+
2267
+ if (options.type === 'script') {
2268
+ const msScript = new MagicString(code)
2269
+ msScript.prepend(`(function (global) {\n`)
2270
+ msScript.append(
2271
+ `\n})(typeof globalThis !== 'undefined' ? globalThis : typeof global !== 'undefined' ? global : typeof window !== 'undefined' ? window : this);`
2272
+ )
2273
+ code = msScript.toString()
2274
+ intermediateMaps.push(msScript.generateMap({ source: filename, hires: true, includeContent: true }))
2275
+ } else if (config.unstable_disableModuleWrapping === true) {
2276
+ const rewriteRes = rewriteDependencyCalls(code, dependencies, depMapName, filename, asyncRequirePath)
2277
+ code = rewriteRes.code
2278
+ if (rewriteRes.map) {
2279
+ intermediateMaps.push(rewriteRes.map)
2280
+ }
2281
+ } else {
2282
+ const rewriteRes = rewriteDependencyCalls(code, dependencies, depMapName, filename, asyncRequirePath)
2283
+ if (rewriteRes.map) {
2284
+ intermediateMaps.push(rewriteRes.map)
2285
+ }
2286
+ const msWrap = new MagicString(rewriteRes.code)
2287
+ const globalPrefix = config.globalPrefix || ''
2288
+ const header = `function (global, _$$_REQUIRE, _$$_IMPORT_DEFAULT, _$$_IMPORT_ALL, module, exports, ${depMapName}) {\nvar require = _$$_REQUIRE;\n`
2289
+ const footer = `\n}`
2290
+
2291
+ msWrap.prepend(`${globalPrefix}__d(${header}`)
2292
+ if (options.moduleId !== undefined) {
2293
+ const depIds = options.dependencyIds ? JSON.stringify(options.dependencyIds) : '[]'
2294
+ msWrap.append(`${footer}, ${JSON.stringify(options.moduleId)}, ${depIds});`)
2295
+ } else {
2296
+ msWrap.append(`${footer});`)
2297
+ }
2298
+
2299
+ code = msWrap.toString()
2300
+ intermediateMaps.push(msWrap.generateMap({ source: filename, hires: true, includeContent: true }))
2301
+ }
2302
+
2303
+ // Step G: Minification
2304
+ if (
2305
+ options.minify &&
2306
+ options.unstable_transformProfile !== 'hermes-canary' &&
2307
+ options.unstable_transformProfile !== 'hermes-stable' &&
2308
+ config.minifierPath
2309
+ ) {
2310
+ try {
2311
+ const minifier = require(config.minifierPath)
2312
+ const minified = await minifier({
2313
+ code,
2314
+ config: config.minifierConfig,
2315
+ filename,
2316
+ map: new MagicString(code).generateMap({
2317
+ source: filename,
2318
+ hires: true,
2319
+ includeContent: true,
2320
+ }),
2321
+ })
2322
+ code = minified.code
2323
+ if (minified.map) {
2324
+ intermediateMaps.push(
2325
+ typeof minified.map === 'string' ? JSON.parse(minified.map) : minified.map
2326
+ )
2327
+ } else {
2328
+ // earlier maps describe the unminified code and cannot be used here.
2329
+ intermediateMaps.length = 0
2330
+ }
2331
+ } catch (err: any) {
2332
+ throw new Error(`[vxrn/metro] Minifier failed for ${filename}: ${err.message || err}`)
2333
+ }
2334
+ }
2335
+
2336
+ // Step H: Compose all pipeline maps and convert to Metro raw mapping segment tuples
2337
+ let rawMappings: Array<[number, number] | [number, number, number, number] | [number, number, number, number, string]> = []
2338
+
2339
+ if (intermediateMaps.length > 0) {
2340
+ const cleanMaps = intermediateMaps
2341
+ .map((m) => (typeof m === 'string' ? JSON.parse(m) : m))
2342
+ .filter(Boolean)
2343
+
2344
+ if (cleanMaps.length > 0) {
2345
+ const composed =
2346
+ cleanMaps.length === 1
2347
+ ? cleanMaps[0]
2348
+ : remapping(cleanMaps.slice().reverse(), () => null)
2349
+ rawMappings = convertSourceMapToMetroRawMappings(composed)
2350
+ }
2351
+ }
2352
+
2353
+ const { lineCount, map } = countLinesAndTerminateMap(code, rawMappings)
2354
+
2355
+ return {
2356
+ dependencies,
2357
+ output: [
2358
+ {
2359
+ data: {
2360
+ code,
2361
+ lineCount,
2362
+ map,
2363
+ functionMap: null,
2364
+ },
2365
+ type: options.type === 'script' ? 'js/script' : 'js/module',
2366
+ },
2367
+ ],
2368
+ }
2369
+ }
2370
+
2371
+ export default {
2372
+ transform,
2373
+ getCacheKey,
2374
+ wrapModule,
2375
+ wrapJson,
2376
+ extractDependencies,
2377
+ rewriteDependencyCalls,
2378
+ convertSourceMapToMetroRawMappings,
2379
+ }