@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.
- package/dist/cjs/metro-config/getMetroConfigFromViteConfig.cjs +20 -0
- package/dist/cjs/metro-config/getMetroConfigFromViteConfig.native.js +20 -0
- package/dist/cjs/metro-config/getMetroConfigFromViteConfig.native.js.map +1 -1
- package/dist/cjs/metro-config/patchMetroServerWithViteConfigAndMetroPluginOptions.cjs +2 -1
- package/dist/cjs/metro-config/patchMetroServerWithViteConfigAndMetroPluginOptions.native.js +2 -1
- package/dist/cjs/metro-config/patchMetroServerWithViteConfigAndMetroPluginOptions.native.js.map +1 -1
- package/dist/cjs/plugins/metroPlugin.native.js.map +1 -1
- package/dist/cjs/transformer/metroNativeWorker.cjs +1590 -0
- package/dist/cjs/transformer/metroNativeWorker.native.js +2674 -0
- package/dist/cjs/transformer/metroNativeWorker.native.js.map +1 -0
- package/dist/cjs/transformer/metroNativeWorker.test.cjs +886 -0
- package/dist/cjs/transformer/metroNativeWorker.test.native.js +995 -0
- package/dist/cjs/transformer/metroNativeWorker.test.native.js.map +1 -0
- package/dist/esm/metro-config/getMetroConfigFromViteConfig.mjs +20 -0
- package/dist/esm/metro-config/getMetroConfigFromViteConfig.mjs.map +1 -1
- package/dist/esm/metro-config/getMetroConfigFromViteConfig.native.js +20 -0
- package/dist/esm/metro-config/getMetroConfigFromViteConfig.native.js.map +1 -1
- package/dist/esm/metro-config/patchMetroServerWithViteConfigAndMetroPluginOptions.mjs +2 -1
- package/dist/esm/metro-config/patchMetroServerWithViteConfigAndMetroPluginOptions.mjs.map +1 -1
- package/dist/esm/metro-config/patchMetroServerWithViteConfigAndMetroPluginOptions.native.js +2 -1
- package/dist/esm/metro-config/patchMetroServerWithViteConfigAndMetroPluginOptions.native.js.map +1 -1
- package/dist/esm/plugins/metroPlugin.mjs.map +1 -1
- package/dist/esm/plugins/metroPlugin.native.js.map +1 -1
- package/dist/esm/transformer/metroNativeWorker.mjs +1531 -0
- package/dist/esm/transformer/metroNativeWorker.mjs.map +1 -0
- package/dist/esm/transformer/metroNativeWorker.native.js +2612 -0
- package/dist/esm/transformer/metroNativeWorker.native.js.map +1 -0
- package/dist/esm/transformer/metroNativeWorker.test.mjs +850 -0
- package/dist/esm/transformer/metroNativeWorker.test.mjs.map +1 -0
- package/dist/esm/transformer/metroNativeWorker.test.native.js +957 -0
- package/dist/esm/transformer/metroNativeWorker.test.native.js.map +1 -0
- package/package.json +22 -3
- package/src/metro-config/getMetroConfigFromViteConfig.ts +42 -0
- package/src/metro-config/patchMetroServerWithViteConfigAndMetroPluginOptions.ts +1 -0
- package/src/plugins/metroPlugin.ts +22 -0
- package/src/transformer/__fixtures__/exampleNativeTransform.cjs +17 -0
- package/src/transformer/metroNativeWorker.test.ts +1200 -0
- package/src/transformer/metroNativeWorker.ts +2379 -0
- package/src/transformer/types.ts +21 -0
- package/types/metro-config/getMetroConfigFromViteConfig.d.ts.map +1 -1
- package/types/metro-config/patchMetroServerWithViteConfigAndMetroPluginOptions.d.ts.map +1 -1
- package/types/plugins/metroPlugin.d.ts +22 -0
- package/types/plugins/metroPlugin.d.ts.map +1 -1
- package/types/transformer/metroNativeWorker.d.ts +251 -0
- package/types/transformer/metroNativeWorker.d.ts.map +1 -0
- package/types/transformer/metroNativeWorker.test.d.ts +2 -0
- package/types/transformer/metroNativeWorker.test.d.ts.map +1 -0
- package/types/transformer/types.d.ts +17 -0
- 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
|
+
}
|