@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,1200 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
|
2
|
+
import * as babel from '@babel/core'
|
|
3
|
+
import * as vm from 'node:vm'
|
|
4
|
+
import { parseSync } from 'oxc-parser'
|
|
5
|
+
import * as fs from 'node:fs'
|
|
6
|
+
import * as path from 'node:path'
|
|
7
|
+
import { createRequire } from 'node:module'
|
|
8
|
+
import {
|
|
9
|
+
transform,
|
|
10
|
+
getCacheKey,
|
|
11
|
+
wrapModule,
|
|
12
|
+
wrapJson,
|
|
13
|
+
extractDependencies,
|
|
14
|
+
rewriteDependencyCalls,
|
|
15
|
+
countLinesAndTerminateMap,
|
|
16
|
+
applyInlineEnvVars,
|
|
17
|
+
assertNoUnportedBabelPlugins,
|
|
18
|
+
applyEnvironmentGuard,
|
|
19
|
+
getRemoveServerCodeRouterRoot,
|
|
20
|
+
} from './metroNativeWorker'
|
|
21
|
+
import { buildMetroConfigInputFromViteConfig } from '../metro-config/getMetroConfigFromViteConfig'
|
|
22
|
+
|
|
23
|
+
const babelCalls = {
|
|
24
|
+
transform: 0,
|
|
25
|
+
transformSync: 0,
|
|
26
|
+
transformAsync: 0,
|
|
27
|
+
transformFromAstSync: 0,
|
|
28
|
+
parse: 0,
|
|
29
|
+
parseSync: 0,
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
vi.mock('@babel/core', async (importOriginal) => {
|
|
33
|
+
const actual = await importOriginal<typeof import('@babel/core')>()
|
|
34
|
+
return {
|
|
35
|
+
...actual,
|
|
36
|
+
transform: vi.fn((...args: any[]) => {
|
|
37
|
+
babelCalls.transform++
|
|
38
|
+
return (actual.transform as any)(...args)
|
|
39
|
+
}),
|
|
40
|
+
transformSync: vi.fn((...args: any[]) => {
|
|
41
|
+
babelCalls.transformSync++
|
|
42
|
+
return (actual.transformSync as any)(...args)
|
|
43
|
+
}),
|
|
44
|
+
transformAsync: vi.fn((...args: any[]) => {
|
|
45
|
+
babelCalls.transformAsync++
|
|
46
|
+
return (actual.transformAsync as any)(...args)
|
|
47
|
+
}),
|
|
48
|
+
transformFromAstSync: vi.fn((...args: any[]) => {
|
|
49
|
+
babelCalls.transformFromAstSync++
|
|
50
|
+
return (actual.transformFromAstSync as any)(...args)
|
|
51
|
+
}),
|
|
52
|
+
parse: vi.fn((...args: any[]) => {
|
|
53
|
+
babelCalls.parse++
|
|
54
|
+
return (actual.parse as any)(...args)
|
|
55
|
+
}),
|
|
56
|
+
parseSync: vi.fn((...args: any[]) => {
|
|
57
|
+
babelCalls.parseSync++
|
|
58
|
+
return (actual.parseSync as any)(...args)
|
|
59
|
+
}),
|
|
60
|
+
}
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
// module scope so every describe in this file can assert it, not just the first
|
|
64
|
+
beforeEach(() => {
|
|
65
|
+
babelCalls.transform = 0
|
|
66
|
+
babelCalls.transformSync = 0
|
|
67
|
+
babelCalls.transformAsync = 0
|
|
68
|
+
babelCalls.transformFromAstSync = 0
|
|
69
|
+
babelCalls.parse = 0
|
|
70
|
+
babelCalls.parseSync = 0
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
function assertZeroBabelCalls() {
|
|
74
|
+
expect(babelCalls.transform).toBe(0)
|
|
75
|
+
expect(babelCalls.transformSync).toBe(0)
|
|
76
|
+
expect(babelCalls.transformAsync).toBe(0)
|
|
77
|
+
expect(babelCalls.transformFromAstSync).toBe(0)
|
|
78
|
+
expect(babelCalls.parse).toBe(0)
|
|
79
|
+
expect(babelCalls.parseSync).toBe(0)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
describe('metroNativeWorker', () => {
|
|
83
|
+
it.each([['data.json', true], ['data.json', false], ['data.js', false]] as const)('minifies %s with source maps enabled=%s without stale mappings', async (filename, sourceMap) => {
|
|
84
|
+
const result = await transform(
|
|
85
|
+
{
|
|
86
|
+
minifierPath: createRequire(import.meta.url).resolve('metro-minify-terser'),
|
|
87
|
+
minifierConfig: { sourceMap },
|
|
88
|
+
},
|
|
89
|
+
'/project',
|
|
90
|
+
filename,
|
|
91
|
+
Buffer.from(filename.endsWith('.json') ? '{"answer":42}' : 'module.exports = { answer: 42 }'),
|
|
92
|
+
{ dev: false, minify: true, platform: 'ios', type: 'module' }
|
|
93
|
+
)
|
|
94
|
+
const module = { exports: {} as any }
|
|
95
|
+
const { code, map } = result.output[0].data
|
|
96
|
+
vm.runInNewContext(code, {
|
|
97
|
+
__d(factory: any) {
|
|
98
|
+
factory({}, () => {}, () => {}, () => {}, module, module.exports, [])
|
|
99
|
+
},
|
|
100
|
+
})
|
|
101
|
+
expect(module.exports.answer).toBe(42)
|
|
102
|
+
expect(map.every((entry) => entry.length === 2)).toBe(true)
|
|
103
|
+
assertZeroBabelCalls()
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
it('minifies production modules with executable output and original source locations', async () => {
|
|
107
|
+
const source = 'exports.answer = function answer(value) {\n return value + 42\n}\n'
|
|
108
|
+
const result = await transform(
|
|
109
|
+
{
|
|
110
|
+
minifierPath: createRequire(import.meta.url).resolve('metro-minify-terser'),
|
|
111
|
+
minifierConfig: { mangle: true },
|
|
112
|
+
},
|
|
113
|
+
'/project',
|
|
114
|
+
'answer.js',
|
|
115
|
+
Buffer.from(source),
|
|
116
|
+
{ dev: false, minify: true, platform: 'ios', type: 'module' }
|
|
117
|
+
)
|
|
118
|
+
const { code, map } = result.output[0].data
|
|
119
|
+
const module = { exports: {} as any }
|
|
120
|
+
vm.runInNewContext(code, {
|
|
121
|
+
__d(factory: any) {
|
|
122
|
+
factory({}, () => {}, () => {}, () => {}, module, module.exports, [])
|
|
123
|
+
},
|
|
124
|
+
})
|
|
125
|
+
expect(module.exports.answer(8)).toBe(50)
|
|
126
|
+
|
|
127
|
+
const msm = await import('metro-source-map')
|
|
128
|
+
const fullMap = (msm.fromRawMappings as any)([
|
|
129
|
+
{ code, path: 'answer.js', source, map },
|
|
130
|
+
]).toMap()
|
|
131
|
+
const offset = code.indexOf('42')
|
|
132
|
+
expect(offset).toBeGreaterThan(-1)
|
|
133
|
+
const prefix = code.slice(0, offset).split('\n')
|
|
134
|
+
const position = new msm.Consumer(fullMap).originalPositionFor({
|
|
135
|
+
line: prefix.length as any,
|
|
136
|
+
column: prefix.at(-1)!.length as any,
|
|
137
|
+
})
|
|
138
|
+
expect(position.source).toBe('answer.js')
|
|
139
|
+
expect(position.line).toBe(2)
|
|
140
|
+
expect(position.column).toBe(17)
|
|
141
|
+
assertZeroBabelCalls()
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
it('extracts all dependencies with oxc-parser and zero Babel', () => {
|
|
145
|
+
const code = `
|
|
146
|
+
import React, { useState } from 'react'
|
|
147
|
+
import * as RN from 'react-native'
|
|
148
|
+
import './global.css'
|
|
149
|
+
import type { UserType } from './user-types'
|
|
150
|
+
export { Button } from './Button'
|
|
151
|
+
export * from './icons'
|
|
152
|
+
export type { ThemeType } from './theme-types'
|
|
153
|
+
const helper = require('./helper')
|
|
154
|
+
async function loadAsync() {
|
|
155
|
+
const dyn = await import('./dynamic-module')
|
|
156
|
+
}
|
|
157
|
+
`
|
|
158
|
+
|
|
159
|
+
const deps = extractDependencies(code, 'TestComponent.tsx')
|
|
160
|
+
assertZeroBabelCalls()
|
|
161
|
+
|
|
162
|
+
const names = deps.map((d) => d.name)
|
|
163
|
+
expect(names).toContain('react')
|
|
164
|
+
expect(names).toContain('react-native')
|
|
165
|
+
expect(names).toContain('./global.css')
|
|
166
|
+
expect(names).toContain('./Button')
|
|
167
|
+
expect(names).toContain('./icons')
|
|
168
|
+
expect(names).toContain('./helper')
|
|
169
|
+
expect(names).toContain('./dynamic-module')
|
|
170
|
+
|
|
171
|
+
// Type-only imports and exports should NOT be extracted as runtime dependencies
|
|
172
|
+
expect(names).not.toContain('./user-types')
|
|
173
|
+
expect(names).not.toContain('./theme-types')
|
|
174
|
+
|
|
175
|
+
// ESM import flags
|
|
176
|
+
const reactDep = deps.find((d) => d.name === 'react')!
|
|
177
|
+
expect(reactDep.data.isESMImportAtSource).toBe(true)
|
|
178
|
+
expect(reactDep.data.asyncType).toBeNull()
|
|
179
|
+
expect(reactDep.data.locs.length).toBeGreaterThan(0)
|
|
180
|
+
// metro's unable-to-resolve error path reads loc.start.line and loc.end.line,
|
|
181
|
+
// so a flat {line, column} makes that error throw and hide the real failure.
|
|
182
|
+
const loc = reactDep.data.locs[0]
|
|
183
|
+
expect(loc.start.line).toBeGreaterThan(0)
|
|
184
|
+
expect(loc.start.column).toBeGreaterThanOrEqual(0)
|
|
185
|
+
expect(loc.end.line).toBeGreaterThanOrEqual(loc.start.line)
|
|
186
|
+
expect(loc.end.column).toBeGreaterThanOrEqual(0)
|
|
187
|
+
|
|
188
|
+
// CJS require flag
|
|
189
|
+
const helperDep = deps.find((d) => d.name === './helper')!
|
|
190
|
+
expect(helperDep.data.isESMImportAtSource).toBe(false)
|
|
191
|
+
expect(helperDep.data.asyncType).toBeNull()
|
|
192
|
+
|
|
193
|
+
// Dynamic import flag
|
|
194
|
+
const dynamicDep = deps.find((d) => d.name === './dynamic-module')!
|
|
195
|
+
expect(dynamicDep.data.asyncType).toBe('async')
|
|
196
|
+
})
|
|
197
|
+
|
|
198
|
+
it('marks requires inside a try block optional', () => {
|
|
199
|
+
// optional native modules are required exactly this way. without
|
|
200
|
+
// data.isOptional metro fails the whole build when one is absent.
|
|
201
|
+
const code = `
|
|
202
|
+
let worklets
|
|
203
|
+
try {
|
|
204
|
+
worklets = require('react-native-worklets-core')
|
|
205
|
+
} catch {}
|
|
206
|
+
const always = require('./always-there')
|
|
207
|
+
`
|
|
208
|
+
|
|
209
|
+
const off = extractDependencies(code, 'setup.js')
|
|
210
|
+
expect(off.find((d) => d.name === 'react-native-worklets-core')!.data.isOptional).toBeUndefined()
|
|
211
|
+
|
|
212
|
+
const on = extractDependencies(code, 'setup.js', { allowOptionalDependencies: true })
|
|
213
|
+
expect(on.find((d) => d.name === 'react-native-worklets-core')!.data.isOptional).toBe(true)
|
|
214
|
+
expect(on.find((d) => d.name === './always-there')!.data.isOptional).toBeUndefined()
|
|
215
|
+
|
|
216
|
+
const excluded = extractDependencies(code, 'setup.js', {
|
|
217
|
+
allowOptionalDependencies: { exclude: ['react-native-worklets-core'] },
|
|
218
|
+
})
|
|
219
|
+
expect(
|
|
220
|
+
excluded.find((d) => d.name === 'react-native-worklets-core')!.data.isOptional
|
|
221
|
+
).toBeUndefined()
|
|
222
|
+
})
|
|
223
|
+
|
|
224
|
+
it('wraps modules in Metro CommonJS format with and without moduleId', () => {
|
|
225
|
+
const code = 'exports.val = 123;'
|
|
226
|
+
|
|
227
|
+
const wrappedWithoutId = wrapModule(code)
|
|
228
|
+
expect(wrappedWithoutId).toContain(
|
|
229
|
+
'__d(function (global, _$$_REQUIRE, _$$_IMPORT_DEFAULT, _$$_IMPORT_ALL, module, exports, _dependencyMap) {'
|
|
230
|
+
)
|
|
231
|
+
expect(wrappedWithoutId).toContain('var require = _$$_REQUIRE;')
|
|
232
|
+
expect(wrappedWithoutId).toContain('exports.val = 123;')
|
|
233
|
+
expect(wrappedWithoutId.endsWith(');')).toBe(true)
|
|
234
|
+
|
|
235
|
+
const wrappedWithId = wrapModule(code, {
|
|
236
|
+
moduleId: 99,
|
|
237
|
+
dependencyIds: [1, 2, 3],
|
|
238
|
+
})
|
|
239
|
+
expect(wrappedWithId).toContain(', 99, [1,2,3]);')
|
|
240
|
+
})
|
|
241
|
+
|
|
242
|
+
it('transforms TSX with React JSX, Hermes lowering, and ZERO Babel', async () => {
|
|
243
|
+
const sourceCode = `
|
|
244
|
+
import React from 'react'
|
|
245
|
+
import { View, Text } from 'react-native'
|
|
246
|
+
const helper = require('./helper')
|
|
247
|
+
|
|
248
|
+
export class Greeter extends React.Component {
|
|
249
|
+
greeting = 'Hello from Native'
|
|
250
|
+
|
|
251
|
+
async fetchGreeting() {
|
|
252
|
+
return this.greeting
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
render() {
|
|
256
|
+
return (
|
|
257
|
+
<View testID="greeter">
|
|
258
|
+
<Text>{this.greeting}</Text>
|
|
259
|
+
</View>
|
|
260
|
+
)
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
`
|
|
264
|
+
|
|
265
|
+
const result = await transform(
|
|
266
|
+
{},
|
|
267
|
+
'/project',
|
|
268
|
+
'Greeter.tsx',
|
|
269
|
+
Buffer.from(sourceCode, 'utf8'),
|
|
270
|
+
{ dev: true, platform: 'ios', type: 'module' }
|
|
271
|
+
)
|
|
272
|
+
|
|
273
|
+
// Verify ZERO Babel calls
|
|
274
|
+
assertZeroBabelCalls()
|
|
275
|
+
|
|
276
|
+
expect(result.output).toHaveLength(1)
|
|
277
|
+
const output = result.output[0]
|
|
278
|
+
expect(output.type).toBe('js/module')
|
|
279
|
+
expect(output.data.lineCount).toBeGreaterThan(0)
|
|
280
|
+
expect(output.data.map.length).toBeGreaterThan(0)
|
|
281
|
+
|
|
282
|
+
const code = output.data.code
|
|
283
|
+
|
|
284
|
+
// Module wrapping check
|
|
285
|
+
expect(code).toContain(
|
|
286
|
+
'__d(function (global, _$$_REQUIRE, _$$_IMPORT_DEFAULT, _$$_IMPORT_ALL, module, exports, _dependencyMap) {'
|
|
287
|
+
)
|
|
288
|
+
expect(code).toContain('var require = _$$_REQUIRE;')
|
|
289
|
+
|
|
290
|
+
// JSX transformed (no raw JSX tags remain)
|
|
291
|
+
expect(code).not.toContain('<View')
|
|
292
|
+
expect(code).not.toContain('<Text')
|
|
293
|
+
|
|
294
|
+
// Hermes lowering applied (class field property lowered to constructor)
|
|
295
|
+
expect(code).toContain('this.greeting = "Hello from Native"')
|
|
296
|
+
|
|
297
|
+
// Dependencies extracted
|
|
298
|
+
const depNames = result.dependencies.map((d) => d.name)
|
|
299
|
+
expect(depNames).toContain('react')
|
|
300
|
+
expect(depNames).toContain('react-native')
|
|
301
|
+
expect(depNames).toContain('./helper')
|
|
302
|
+
expect(depNames).toContain('react/jsx-dev-runtime')
|
|
303
|
+
})
|
|
304
|
+
|
|
305
|
+
it('lowers async generators, including the method shorthand Hermes also rejects', async () => {
|
|
306
|
+
// each form goes through on its own, so a detector that misses one cannot be
|
|
307
|
+
// covered for by another form sharing the module.
|
|
308
|
+
const sources = {
|
|
309
|
+
// the shorthand the device caught: `async *name()` in a class body.
|
|
310
|
+
'cursor.ts': `
|
|
311
|
+
export class Cursor {
|
|
312
|
+
async *rows() {
|
|
313
|
+
yield await Promise.resolve(1)
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
`,
|
|
317
|
+
'stream.ts': `
|
|
318
|
+
export async function* stream() {
|
|
319
|
+
yield 1
|
|
320
|
+
}
|
|
321
|
+
`,
|
|
322
|
+
'drain.ts': `
|
|
323
|
+
export async function drain(src) {
|
|
324
|
+
for await (const x of src) {
|
|
325
|
+
console.log(x)
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
`,
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
for (const [filename, sourceCode] of Object.entries(sources)) {
|
|
332
|
+
const result = await transform(
|
|
333
|
+
{},
|
|
334
|
+
'/project',
|
|
335
|
+
filename,
|
|
336
|
+
Buffer.from(sourceCode, 'utf8'),
|
|
337
|
+
{ dev: true, platform: 'ios', type: 'module' }
|
|
338
|
+
)
|
|
339
|
+
|
|
340
|
+
assertZeroBabelCalls()
|
|
341
|
+
|
|
342
|
+
const code = result.output[0].data.code
|
|
343
|
+
// hermes rejects all three of these at parse time, so none may survive.
|
|
344
|
+
expect(code, filename).not.toMatch(/\basync\s+function\s*\*/)
|
|
345
|
+
expect(code, filename).not.toMatch(/\basync\s*\*\s*[\w$[]/)
|
|
346
|
+
expect(code, filename).not.toMatch(/\bfor\s+await\s*\(/)
|
|
347
|
+
}
|
|
348
|
+
})
|
|
349
|
+
|
|
350
|
+
it('handles JSON files without Babel', async () => {
|
|
351
|
+
const jsonSource = JSON.stringify({ name: 'my-package', version: '1.0.0' })
|
|
352
|
+
const result = await transform(
|
|
353
|
+
{},
|
|
354
|
+
'/project',
|
|
355
|
+
'package.json',
|
|
356
|
+
Buffer.from(jsonSource, 'utf8'),
|
|
357
|
+
{ dev: true, platform: 'ios', type: 'module' }
|
|
358
|
+
)
|
|
359
|
+
|
|
360
|
+
assertZeroBabelCalls()
|
|
361
|
+
expect(result.dependencies).toEqual([])
|
|
362
|
+
expect(result.output).toHaveLength(1)
|
|
363
|
+
expect(result.output[0].type).toBe('js/module')
|
|
364
|
+
expect(result.output[0].data.code).toContain('module.exports = {"name":"my-package","version":"1.0.0"}')
|
|
365
|
+
expect(result.output[0].data.code).toContain('__d(function (global, require')
|
|
366
|
+
})
|
|
367
|
+
|
|
368
|
+
it('handles Asset files without Babel', async () => {
|
|
369
|
+
const result = await transform(
|
|
370
|
+
{
|
|
371
|
+
assetRegistryPath: 'react-native/Libraries/Image/AssetRegistry',
|
|
372
|
+
publicPath: '/assets',
|
|
373
|
+
},
|
|
374
|
+
'/project',
|
|
375
|
+
'logo.png',
|
|
376
|
+
Buffer.from('fake-png-binary-data', 'utf8'),
|
|
377
|
+
{ dev: true, platform: 'ios', type: 'asset' }
|
|
378
|
+
)
|
|
379
|
+
|
|
380
|
+
assertZeroBabelCalls()
|
|
381
|
+
expect(result.output[0].type).toBe('js/module/asset')
|
|
382
|
+
expect(result.output[0].data.code).toContain('registerAsset(')
|
|
383
|
+
expect(result.output[0].data.code).toContain('__d(function')
|
|
384
|
+
|
|
385
|
+
const depNames = result.dependencies.map((d) => d.name)
|
|
386
|
+
expect(depNames).toContain('react-native/Libraries/Image/AssetRegistry')
|
|
387
|
+
})
|
|
388
|
+
|
|
389
|
+
it('handles Flow files by stripping types without Babel', async () => {
|
|
390
|
+
const flowSource = `
|
|
391
|
+
// @flow
|
|
392
|
+
function add(a: number, b: number): number {
|
|
393
|
+
return a + b;
|
|
394
|
+
}
|
|
395
|
+
export default add;
|
|
396
|
+
`
|
|
397
|
+
|
|
398
|
+
const result = await transform(
|
|
399
|
+
{},
|
|
400
|
+
'/project',
|
|
401
|
+
'math.js',
|
|
402
|
+
Buffer.from(flowSource, 'utf8'),
|
|
403
|
+
{ dev: false, platform: 'ios', type: 'module' }
|
|
404
|
+
)
|
|
405
|
+
|
|
406
|
+
assertZeroBabelCalls()
|
|
407
|
+
expect(result.output[0].data.code).not.toContain(': number')
|
|
408
|
+
expect(result.output[0].data.code).toContain('function add(a, b)')
|
|
409
|
+
})
|
|
410
|
+
|
|
411
|
+
it('generates a deterministic cache key without Babel', () => {
|
|
412
|
+
const key1 = getCacheKey({ globalPrefix: '__one_', minifierPath: 'terser' }, { projectRoot: '/app' })
|
|
413
|
+
const key2 = getCacheKey({ globalPrefix: '__one_', minifierPath: 'terser' }, { projectRoot: '/app' })
|
|
414
|
+
const key3 = getCacheKey({ globalPrefix: '__other_' }, { projectRoot: '/app' })
|
|
415
|
+
|
|
416
|
+
assertZeroBabelCalls()
|
|
417
|
+
expect(key1).toBe(key2)
|
|
418
|
+
expect(key1).not.toBe(key3)
|
|
419
|
+
expect(typeof key1).toBe('string')
|
|
420
|
+
expect(key1.length).toBe(64) // SHA-256 hex
|
|
421
|
+
})
|
|
422
|
+
|
|
423
|
+
it('correctly sets transformerPath in buildMetroConfigInputFromViteConfig when active', async () => {
|
|
424
|
+
const mockViteConfig = { root: process.cwd() } as any
|
|
425
|
+
|
|
426
|
+
// transformerPath is read by metro at the top level of the config. asserting
|
|
427
|
+
// it under `transformer` passes while metro silently keeps its own worker,
|
|
428
|
+
// so both the presence and the nesting are checked here.
|
|
429
|
+
|
|
430
|
+
// Default: the no-babel worker, with nothing configured.
|
|
431
|
+
delete process.env.ONE_METRO_NATIVE_TRANSFORMS
|
|
432
|
+
const configDefault = await buildMetroConfigInputFromViteConfig(mockViteConfig, {})
|
|
433
|
+
expect(configDefault.defaultConfig.transformerPath).toContain('metroNativeWorker')
|
|
434
|
+
expect(configDefault.defaultConfig.transformer.transformerPath).toBeUndefined()
|
|
435
|
+
|
|
436
|
+
// Opting back into babel via metroPluginOptions leaves expo's default
|
|
437
|
+
// worker in the key, which is itself proof that top level is where metro
|
|
438
|
+
// reads it from.
|
|
439
|
+
const configOptOff = await buildMetroConfigInputFromViteConfig(mockViteConfig, {
|
|
440
|
+
nativeTransforms: false,
|
|
441
|
+
})
|
|
442
|
+
expect(configOptOff.defaultConfig.transformerPath).not.toContain('metroNativeWorker')
|
|
443
|
+
|
|
444
|
+
// Opting back into babel for one run via the environment variable
|
|
445
|
+
process.env.ONE_METRO_NATIVE_TRANSFORMS = '0'
|
|
446
|
+
const configEnvOff = await buildMetroConfigInputFromViteConfig(mockViteConfig, {})
|
|
447
|
+
expect(configEnvOff.defaultConfig.transformerPath).not.toContain('metroNativeWorker')
|
|
448
|
+
|
|
449
|
+
// and the env var wins over an option that asked for babel
|
|
450
|
+
process.env.ONE_METRO_NATIVE_TRANSFORMS = '1'
|
|
451
|
+
const configEnvOn = await buildMetroConfigInputFromViteConfig(mockViteConfig, {
|
|
452
|
+
nativeTransforms: false,
|
|
453
|
+
})
|
|
454
|
+
expect(configEnvOn.defaultConfig.transformerPath).toContain('metroNativeWorker')
|
|
455
|
+
delete process.env.ONE_METRO_NATIVE_TRANSFORMS
|
|
456
|
+
})
|
|
457
|
+
|
|
458
|
+
it('transforms and executes a real 2-module Metro bundle through metro-runtime require polyfill', async () => {
|
|
459
|
+
// Probe 1 verification:
|
|
460
|
+
// Module 2 (dep.js): exports a value
|
|
461
|
+
// Module 1 (a.js): requires dep.js using Metro dependency ABI and exports computed result
|
|
462
|
+
const depSource = 'exports.value = 42;'
|
|
463
|
+
const aSource = 'const dep = require("./dep"); module.exports = { doubled: dep.value * 2 };'
|
|
464
|
+
|
|
465
|
+
// Transform dep.js (Module 2)
|
|
466
|
+
const depRes = await transform(
|
|
467
|
+
{},
|
|
468
|
+
'/project',
|
|
469
|
+
'dep.js',
|
|
470
|
+
Buffer.from(depSource, 'utf8'),
|
|
471
|
+
{
|
|
472
|
+
dev: false,
|
|
473
|
+
platform: 'ios',
|
|
474
|
+
type: 'module',
|
|
475
|
+
moduleId: 2,
|
|
476
|
+
dependencyIds: [],
|
|
477
|
+
} as any
|
|
478
|
+
)
|
|
479
|
+
|
|
480
|
+
// Transform a.js (Module 1) which depends on dep.js (dependency slot 0 -> moduleId 2)
|
|
481
|
+
const aRes = await transform(
|
|
482
|
+
{},
|
|
483
|
+
'/project',
|
|
484
|
+
'a.js',
|
|
485
|
+
Buffer.from(aSource, 'utf8'),
|
|
486
|
+
{
|
|
487
|
+
dev: false,
|
|
488
|
+
platform: 'ios',
|
|
489
|
+
type: 'module',
|
|
490
|
+
moduleId: 1,
|
|
491
|
+
dependencyIds: [2],
|
|
492
|
+
} as any
|
|
493
|
+
)
|
|
494
|
+
|
|
495
|
+
assertZeroBabelCalls()
|
|
496
|
+
|
|
497
|
+
// Verify dependency extraction
|
|
498
|
+
expect(aRes.dependencies).toHaveLength(1)
|
|
499
|
+
expect(aRes.dependencies[0].name).toBe('./dep')
|
|
500
|
+
|
|
501
|
+
// Verify dependency call rewriting to Metro ABI
|
|
502
|
+
const aEmittedCode = aRes.output[0].data.code
|
|
503
|
+
expect(aEmittedCode).toContain('require(_dependencyMap[0], "./dep")')
|
|
504
|
+
|
|
505
|
+
// Execute through REAL Metro runtime polyfill
|
|
506
|
+
const polyfillPath = require.resolve('metro-runtime/src/polyfills/require.js')
|
|
507
|
+
const polyfillCode = fs.readFileSync(polyfillPath, 'utf8')
|
|
508
|
+
|
|
509
|
+
const contextObj = {
|
|
510
|
+
__DEV__: false,
|
|
511
|
+
__METRO_GLOBAL_PREFIX__: '',
|
|
512
|
+
console: console,
|
|
513
|
+
} as any
|
|
514
|
+
contextObj.global = contextObj
|
|
515
|
+
vm.createContext(contextObj)
|
|
516
|
+
vm.runInContext(polyfillCode, contextObj)
|
|
517
|
+
|
|
518
|
+
// Register module 2 and module 1 in the Metro runtime
|
|
519
|
+
vm.runInContext(depRes.output[0].data.code, contextObj)
|
|
520
|
+
vm.runInContext(aEmittedCode, contextObj)
|
|
521
|
+
|
|
522
|
+
// Execute Module 1 via Metro's require entrypoint
|
|
523
|
+
const executedExports = vm.runInContext('global.__r(1)', contextObj)
|
|
524
|
+
expect(executedExports).toEqual({ doubled: 84 })
|
|
525
|
+
})
|
|
526
|
+
|
|
527
|
+
it('transforms and executes a 2-module Metro bundle with ESM imports in dev mode', async () => {
|
|
528
|
+
const depSource = 'export const greeting = "Hello Metro Native";'
|
|
529
|
+
const aSource = 'import { greeting } from "./dep"; export const message = greeting.toUpperCase();'
|
|
530
|
+
|
|
531
|
+
// Transform dep.js (Module 2)
|
|
532
|
+
const depRes = await transform(
|
|
533
|
+
{},
|
|
534
|
+
'/project',
|
|
535
|
+
'dep.js',
|
|
536
|
+
Buffer.from(depSource, 'utf8'),
|
|
537
|
+
{
|
|
538
|
+
dev: true,
|
|
539
|
+
platform: 'ios',
|
|
540
|
+
type: 'module',
|
|
541
|
+
moduleId: 2,
|
|
542
|
+
dependencyIds: [],
|
|
543
|
+
} as any
|
|
544
|
+
)
|
|
545
|
+
|
|
546
|
+
// Transform a.js (Module 1) depending on dep.js (dependency slot 0 -> moduleId 2)
|
|
547
|
+
const aRes = await transform(
|
|
548
|
+
{},
|
|
549
|
+
'/project',
|
|
550
|
+
'a.js',
|
|
551
|
+
Buffer.from(aSource, 'utf8'),
|
|
552
|
+
{
|
|
553
|
+
dev: true,
|
|
554
|
+
platform: 'ios',
|
|
555
|
+
type: 'module',
|
|
556
|
+
moduleId: 1,
|
|
557
|
+
dependencyIds: [2],
|
|
558
|
+
} as any
|
|
559
|
+
)
|
|
560
|
+
|
|
561
|
+
assertZeroBabelCalls()
|
|
562
|
+
|
|
563
|
+
const polyfillPath = require.resolve('metro-runtime/src/polyfills/require.js')
|
|
564
|
+
const polyfillCode = fs.readFileSync(polyfillPath, 'utf8')
|
|
565
|
+
|
|
566
|
+
const contextObj = {
|
|
567
|
+
__DEV__: true,
|
|
568
|
+
__METRO_GLOBAL_PREFIX__: '',
|
|
569
|
+
console: console,
|
|
570
|
+
} as any
|
|
571
|
+
contextObj.global = contextObj
|
|
572
|
+
vm.createContext(contextObj)
|
|
573
|
+
vm.runInContext(polyfillCode, contextObj)
|
|
574
|
+
|
|
575
|
+
vm.runInContext(depRes.output[0].data.code, contextObj)
|
|
576
|
+
vm.runInContext(aRes.output[0].data.code, contextObj)
|
|
577
|
+
|
|
578
|
+
const executedExports = vm.runInContext('global.__r(1)', contextObj)
|
|
579
|
+
expect(executedExports.message).toBe('HELLO METRO NATIVE')
|
|
580
|
+
})
|
|
581
|
+
|
|
582
|
+
it('respects lexical scope shadowing for require calls (Astra probe)', async () => {
|
|
583
|
+
// Exact probe from Astra:
|
|
584
|
+
// function f(require){return require("./local")}; module.exports=f(x=>x);
|
|
585
|
+
// require is shadowed by parameter: must NOT extract ./local, and must NOT rewrite require
|
|
586
|
+
const input = 'function f(require){return require("./local")}; module.exports=f(x=>x);'
|
|
587
|
+
|
|
588
|
+
const res = await transform(
|
|
589
|
+
{},
|
|
590
|
+
'/project',
|
|
591
|
+
'shadowed.js',
|
|
592
|
+
Buffer.from(input, 'utf8'),
|
|
593
|
+
{
|
|
594
|
+
dev: false,
|
|
595
|
+
platform: 'ios',
|
|
596
|
+
type: 'module',
|
|
597
|
+
moduleId: 1,
|
|
598
|
+
dependencyIds: [],
|
|
599
|
+
} as any
|
|
600
|
+
)
|
|
601
|
+
|
|
602
|
+
assertZeroBabelCalls()
|
|
603
|
+
|
|
604
|
+
// 1. Dependency must NOT be extracted
|
|
605
|
+
expect(res.dependencies).toHaveLength(0)
|
|
606
|
+
|
|
607
|
+
// 2. Emitted code must NOT rewrite local require to _dependencyMap
|
|
608
|
+
const emitted = res.output[0].data.code
|
|
609
|
+
expect(emitted).not.toContain('_dependencyMap[0]')
|
|
610
|
+
expect(emitted).toMatch(/require\d*\("\.\/local"\)/)
|
|
611
|
+
|
|
612
|
+
// 3. Bundled execution through Metro runtime returns the string without missing-module error
|
|
613
|
+
const polyfillPath = require.resolve('metro-runtime/src/polyfills/require.js')
|
|
614
|
+
const polyfillCode = fs.readFileSync(polyfillPath, 'utf8')
|
|
615
|
+
|
|
616
|
+
const contextObj = {
|
|
617
|
+
__DEV__: false,
|
|
618
|
+
__METRO_GLOBAL_PREFIX__: '',
|
|
619
|
+
console: console,
|
|
620
|
+
} as any
|
|
621
|
+
contextObj.global = contextObj
|
|
622
|
+
vm.createContext(contextObj)
|
|
623
|
+
vm.runInContext(polyfillCode, contextObj)
|
|
624
|
+
|
|
625
|
+
vm.runInContext(emitted, contextObj)
|
|
626
|
+
const result = vm.runInContext('global.__r(1)', contextObj)
|
|
627
|
+
expect(result).toBe('./local')
|
|
628
|
+
})
|
|
629
|
+
|
|
630
|
+
it('preserves distinct indexed identity for duplicate and mixed sync/async dependencies', async () => {
|
|
631
|
+
const code = `
|
|
632
|
+
const a = require('./dep');
|
|
633
|
+
const b = require('./dep');
|
|
634
|
+
async function load() {
|
|
635
|
+
const c = await import('./dep');
|
|
636
|
+
}
|
|
637
|
+
`
|
|
638
|
+
const res = await transform(
|
|
639
|
+
{},
|
|
640
|
+
'/project',
|
|
641
|
+
'index.js',
|
|
642
|
+
Buffer.from(code, 'utf8'),
|
|
643
|
+
{ dev: false, platform: 'ios', type: 'module' }
|
|
644
|
+
)
|
|
645
|
+
|
|
646
|
+
assertZeroBabelCalls()
|
|
647
|
+
|
|
648
|
+
// Key format separates sync require from async import
|
|
649
|
+
const depNames = res.dependencies.map((d) => d.name)
|
|
650
|
+
expect(depNames).toContain('./dep')
|
|
651
|
+
|
|
652
|
+
const syncDep = res.dependencies.find((d) => d.name === './dep' && d.data.asyncType === null)!
|
|
653
|
+
const asyncDep = res.dependencies.find((d) => d.name === './dep' && d.data.asyncType === 'async')!
|
|
654
|
+
expect(syncDep).toBeDefined()
|
|
655
|
+
expect(asyncDep).toBeDefined()
|
|
656
|
+
expect(syncDep.data.index).not.toEqual(asyncDep.data.index)
|
|
657
|
+
|
|
658
|
+
// Rewritten code points both sync require calls to the sync dependency slot index
|
|
659
|
+
const emitted = res.output[0].data.code
|
|
660
|
+
const syncIndex = syncDep.data.index
|
|
661
|
+
expect(emitted).toContain(`require(_dependencyMap[${syncIndex}], "./dep")`)
|
|
662
|
+
})
|
|
663
|
+
|
|
664
|
+
it('conforms to Metro map ABI with raw mapping segment tuples and accurate symbolication', async () => {
|
|
665
|
+
const sourceCode = `
|
|
666
|
+
export function add(a: number, b: number): number {
|
|
667
|
+
return a + b;
|
|
668
|
+
}
|
|
669
|
+
`
|
|
670
|
+
|
|
671
|
+
const res = await transform(
|
|
672
|
+
{},
|
|
673
|
+
'/project',
|
|
674
|
+
'math.ts',
|
|
675
|
+
Buffer.from(sourceCode, 'utf8'),
|
|
676
|
+
{ dev: false, platform: 'ios', type: 'module' }
|
|
677
|
+
)
|
|
678
|
+
|
|
679
|
+
assertZeroBabelCalls()
|
|
680
|
+
|
|
681
|
+
const map = res.output[0].data.map
|
|
682
|
+
expect(Array.isArray(map)).toBe(true)
|
|
683
|
+
expect(map.length).toBeGreaterThan(0)
|
|
684
|
+
|
|
685
|
+
// Verify Metro Raw Mapping Segment Tuples:
|
|
686
|
+
// Every element must be an array tuple [line, column, origLine?, origCol?, name?]
|
|
687
|
+
// No SourceMapV3 objects embedded
|
|
688
|
+
for (const tuple of map) {
|
|
689
|
+
expect(Array.isArray(tuple)).toBe(true)
|
|
690
|
+
expect(typeof tuple[0]).toBe('number') // generated line (1-based)
|
|
691
|
+
expect(typeof tuple[1]).toBe('number') // generated column (0-based)
|
|
692
|
+
expect(tuple).not.toHaveProperty('version')
|
|
693
|
+
expect(tuple).not.toHaveProperty('mappings')
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
// Verify with Metro's own fromRawMappings and Consumer
|
|
697
|
+
const msm = await import('metro-source-map')
|
|
698
|
+
const fullMap = (msm.fromRawMappings as any)([
|
|
699
|
+
{
|
|
700
|
+
code: res.output[0].data.code,
|
|
701
|
+
source: 'math.ts',
|
|
702
|
+
map,
|
|
703
|
+
},
|
|
704
|
+
]).toMap()
|
|
705
|
+
|
|
706
|
+
const consumer = new msm.Consumer(fullMap)
|
|
707
|
+
// Find generated line for function add
|
|
708
|
+
const lines = res.output[0].data.code.split('\n')
|
|
709
|
+
const addLineIndex = lines.findIndex((l) => l.includes('function add'))
|
|
710
|
+
expect(addLineIndex).toBeGreaterThan(0)
|
|
711
|
+
|
|
712
|
+
const pos = consumer.originalPositionFor({
|
|
713
|
+
line: (addLineIndex + 1) as any,
|
|
714
|
+
column: 16 as any,
|
|
715
|
+
})
|
|
716
|
+
|
|
717
|
+
expect(pos.line).toBe(2)
|
|
718
|
+
})
|
|
719
|
+
|
|
720
|
+
it('models JS hoisting in ScopeTracker for functions and vars (Astra probe)', async () => {
|
|
721
|
+
// Exact probe from Astra:
|
|
722
|
+
// function f(){const x=require("./local"); function require(x){return x}; return x} module.exports=f();
|
|
723
|
+
// function require is hoisted to f's function scope, so require("./local") is a local call and needs no module.
|
|
724
|
+
const input = 'function f(){const x=require("./local"); function require(x){return x}; return x} module.exports=f();'
|
|
725
|
+
|
|
726
|
+
const res = await transform(
|
|
727
|
+
{},
|
|
728
|
+
'/project',
|
|
729
|
+
'hoisted.js',
|
|
730
|
+
Buffer.from(input, 'utf8'),
|
|
731
|
+
{
|
|
732
|
+
dev: false,
|
|
733
|
+
platform: 'ios',
|
|
734
|
+
type: 'module',
|
|
735
|
+
moduleId: 1,
|
|
736
|
+
dependencyIds: [],
|
|
737
|
+
} as any
|
|
738
|
+
)
|
|
739
|
+
|
|
740
|
+
assertZeroBabelCalls()
|
|
741
|
+
|
|
742
|
+
// 1. Dependency must NOT be extracted
|
|
743
|
+
expect(res.dependencies).toHaveLength(0)
|
|
744
|
+
|
|
745
|
+
// 2. Emitted code must NOT rewrite require("./local") to _dependencyMap
|
|
746
|
+
const emitted = res.output[0].data.code
|
|
747
|
+
expect(emitted).not.toContain('_dependencyMap[0]')
|
|
748
|
+
expect(emitted).toMatch(/require\d*\("\.\/local"\)/)
|
|
749
|
+
|
|
750
|
+
// 3. Bundled execution through Metro runtime returns "./local" without missing-module error
|
|
751
|
+
const polyfillPath = require.resolve('metro-runtime/src/polyfills/require.js')
|
|
752
|
+
const polyfillCode = fs.readFileSync(polyfillPath, 'utf8')
|
|
753
|
+
|
|
754
|
+
const contextObj = {
|
|
755
|
+
__DEV__: false,
|
|
756
|
+
__METRO_GLOBAL_PREFIX__: '',
|
|
757
|
+
console: console,
|
|
758
|
+
} as any
|
|
759
|
+
contextObj.global = contextObj
|
|
760
|
+
vm.createContext(contextObj)
|
|
761
|
+
vm.runInContext(polyfillCode, contextObj)
|
|
762
|
+
|
|
763
|
+
vm.runInContext(emitted, contextObj)
|
|
764
|
+
const result = vm.runInContext('global.__r(1)', contextObj)
|
|
765
|
+
expect(result).toBe('./local')
|
|
766
|
+
})
|
|
767
|
+
|
|
768
|
+
it('rewrites dynamic import to Metro asyncRequire and executes promise resolution in dev and release', async () => {
|
|
769
|
+
// Exact probe from Astra:
|
|
770
|
+
// transform of module.exports = () => import("./dep");
|
|
771
|
+
// Emitted __d wrapper must rewrite to require(asyncRequire)(_dependencyMap[dep], _dependencyMap.paths, "./dep")
|
|
772
|
+
// and resolve promise through real Metro runtime.
|
|
773
|
+
const depSource = 'exports.message = "hello dynamic";'
|
|
774
|
+
const mainSource = 'module.exports = () => import("./dep");'
|
|
775
|
+
|
|
776
|
+
const depRes = await transform(
|
|
777
|
+
{},
|
|
778
|
+
'/project',
|
|
779
|
+
'dep.js',
|
|
780
|
+
Buffer.from(depSource, 'utf8'),
|
|
781
|
+
{
|
|
782
|
+
dev: true,
|
|
783
|
+
platform: 'ios',
|
|
784
|
+
type: 'module',
|
|
785
|
+
moduleId: 2,
|
|
786
|
+
dependencyIds: [],
|
|
787
|
+
} as any
|
|
788
|
+
)
|
|
789
|
+
|
|
790
|
+
const mainRes = await transform(
|
|
791
|
+
{},
|
|
792
|
+
'/project',
|
|
793
|
+
'main.js',
|
|
794
|
+
Buffer.from(mainSource, 'utf8'),
|
|
795
|
+
{
|
|
796
|
+
dev: true,
|
|
797
|
+
platform: 'ios',
|
|
798
|
+
type: 'module',
|
|
799
|
+
moduleId: 0,
|
|
800
|
+
dependencyIds: [2, 1], // slot 0 -> module 2 (dep), slot 1 -> module 1 (asyncRequire)
|
|
801
|
+
} as any
|
|
802
|
+
)
|
|
803
|
+
|
|
804
|
+
assertZeroBabelCalls()
|
|
805
|
+
|
|
806
|
+
// 1. Dependencies include ./dep (async) and asyncRequire (sync)
|
|
807
|
+
const depNames = mainRes.dependencies.map((d) => d.name)
|
|
808
|
+
expect(depNames).toContain('./dep')
|
|
809
|
+
expect(depNames).toContain('metro-runtime/src/modules/asyncRequire')
|
|
810
|
+
|
|
811
|
+
const dynDep = mainRes.dependencies.find((d) => d.name === './dep')!
|
|
812
|
+
expect(dynDep.data.asyncType).toBe('async')
|
|
813
|
+
|
|
814
|
+
// 2. Emitted code must rewrite import("./dep") to asyncRequire call
|
|
815
|
+
const emittedMain = mainRes.output[0].data.code
|
|
816
|
+
expect(emittedMain).not.toContain('import("./dep")')
|
|
817
|
+
expect(emittedMain).toContain(
|
|
818
|
+
'require(_dependencyMap[1], "metro-runtime/src/modules/asyncRequire")(_dependencyMap[0], _dependencyMap.paths, "./dep")'
|
|
819
|
+
)
|
|
820
|
+
|
|
821
|
+
// 3. Test Metro VM runtime execution in DEV and RELEASE modes
|
|
822
|
+
const polyfillPath = require.resolve('metro-runtime/src/polyfills/require.js')
|
|
823
|
+
const polyfillCode = fs.readFileSync(polyfillPath, 'utf8')
|
|
824
|
+
const asyncRequirePath = require.resolve('metro-runtime/src/modules/asyncRequire.js')
|
|
825
|
+
const asyncRequireSource = fs.readFileSync(asyncRequirePath, 'utf8')
|
|
826
|
+
const wrappedAsyncRequire = wrapModule(asyncRequireSource, { moduleId: 1, dependencyIds: [] })
|
|
827
|
+
|
|
828
|
+
for (const isDev of [true, false]) {
|
|
829
|
+
const contextObj = {
|
|
830
|
+
__DEV__: isDev,
|
|
831
|
+
__METRO_GLOBAL_PREFIX__: '',
|
|
832
|
+
console: console,
|
|
833
|
+
} as any
|
|
834
|
+
contextObj.global = contextObj
|
|
835
|
+
vm.createContext(contextObj)
|
|
836
|
+
vm.runInContext(polyfillCode, contextObj)
|
|
837
|
+
|
|
838
|
+
// Register asyncRequire (1), dep (2), and main (0)
|
|
839
|
+
vm.runInContext(wrappedAsyncRequire, contextObj)
|
|
840
|
+
vm.runInContext(depRes.output[0].data.code, contextObj)
|
|
841
|
+
vm.runInContext(emittedMain, contextObj)
|
|
842
|
+
|
|
843
|
+
const mainFn = vm.runInContext('global.__r(0)', contextObj)
|
|
844
|
+
expect(typeof mainFn).toBe('function')
|
|
845
|
+
|
|
846
|
+
const promise = mainFn()
|
|
847
|
+
expect(promise instanceof Promise || (promise && typeof promise.then === 'function')).toBe(true)
|
|
848
|
+
|
|
849
|
+
const resolved = await promise
|
|
850
|
+
expect(resolved.message).toBe('hello dynamic')
|
|
851
|
+
expect(resolved.default.message).toBe('hello dynamic')
|
|
852
|
+
}
|
|
853
|
+
})
|
|
854
|
+
|
|
855
|
+
it('preserves accurate sourcemap for tokens after rewritten require calls on the same line', async () => {
|
|
856
|
+
// Exact probe from Astra:
|
|
857
|
+
// rewriteDependencyCalls returns { code, map }
|
|
858
|
+
// Trace a source location AFTER a rewritten require on the same generated line
|
|
859
|
+
const sourceCode = 'const a = require("./dep"), b = 42;'
|
|
860
|
+
|
|
861
|
+
// Transform with module wrapping (standard mode)
|
|
862
|
+
const resWrapped = await transform(
|
|
863
|
+
{},
|
|
864
|
+
'/project',
|
|
865
|
+
'app.js',
|
|
866
|
+
Buffer.from(sourceCode, 'utf8'),
|
|
867
|
+
{
|
|
868
|
+
dev: false,
|
|
869
|
+
platform: 'ios',
|
|
870
|
+
type: 'module',
|
|
871
|
+
} as any
|
|
872
|
+
)
|
|
873
|
+
|
|
874
|
+
assertZeroBabelCalls()
|
|
875
|
+
|
|
876
|
+
const msm = await import('metro-source-map')
|
|
877
|
+
const fullMapWrapped = (msm.fromRawMappings as any)([
|
|
878
|
+
{
|
|
879
|
+
code: resWrapped.output[0].data.code,
|
|
880
|
+
source: 'app.js',
|
|
881
|
+
map: resWrapped.output[0].data.map,
|
|
882
|
+
},
|
|
883
|
+
]).toMap()
|
|
884
|
+
|
|
885
|
+
const consumerWrapped = new msm.Consumer(fullMapWrapped)
|
|
886
|
+
const linesWrapped = resWrapped.output[0].data.code.split('\n')
|
|
887
|
+
const lineIndexWrapped = linesWrapped.findIndex((l) => l.includes('b = 42'))
|
|
888
|
+
expect(lineIndexWrapped).toBeGreaterThanOrEqual(0)
|
|
889
|
+
|
|
890
|
+
const generatedColWrapped = linesWrapped[lineIndexWrapped].indexOf('b = 42')
|
|
891
|
+
expect(generatedColWrapped).toBeGreaterThan(0)
|
|
892
|
+
|
|
893
|
+
const posWrapped = consumerWrapped.originalPositionFor({
|
|
894
|
+
line: (lineIndexWrapped + 1) as any,
|
|
895
|
+
column: generatedColWrapped as any,
|
|
896
|
+
})
|
|
897
|
+
|
|
898
|
+
// Original position of 'b = 42' in sourceCode: line 1, column 28
|
|
899
|
+
expect(posWrapped.line).toBe(1)
|
|
900
|
+
expect(posWrapped.column).toBe(28)
|
|
901
|
+
expect(sourceCode.slice(Number(posWrapped.column))).toContain('b = 42;')
|
|
902
|
+
|
|
903
|
+
// Also test unwrapped mode: unstable_disableModuleWrapping: true
|
|
904
|
+
const resUnwrapped = await transform(
|
|
905
|
+
{ unstable_disableModuleWrapping: true },
|
|
906
|
+
'/project',
|
|
907
|
+
'app.js',
|
|
908
|
+
Buffer.from(sourceCode, 'utf8'),
|
|
909
|
+
{
|
|
910
|
+
dev: false,
|
|
911
|
+
platform: 'ios',
|
|
912
|
+
type: 'module',
|
|
913
|
+
} as any
|
|
914
|
+
)
|
|
915
|
+
|
|
916
|
+
const fullMapUnwrapped = (msm.fromRawMappings as any)([
|
|
917
|
+
{
|
|
918
|
+
code: resUnwrapped.output[0].data.code,
|
|
919
|
+
source: 'app.js',
|
|
920
|
+
map: resUnwrapped.output[0].data.map,
|
|
921
|
+
},
|
|
922
|
+
]).toMap()
|
|
923
|
+
|
|
924
|
+
const consumerUnwrapped = new msm.Consumer(fullMapUnwrapped)
|
|
925
|
+
const linesUnwrapped = resUnwrapped.output[0].data.code.split('\n')
|
|
926
|
+
const lineIndexUnwrapped = linesUnwrapped.findIndex((l) => l.includes('b = 42'))
|
|
927
|
+
expect(lineIndexUnwrapped).toBeGreaterThanOrEqual(0)
|
|
928
|
+
|
|
929
|
+
const generatedColUnwrapped = linesUnwrapped[lineIndexUnwrapped].indexOf('b = 42')
|
|
930
|
+
const posUnwrapped = consumerUnwrapped.originalPositionFor({
|
|
931
|
+
line: (lineIndexUnwrapped + 1) as any,
|
|
932
|
+
column: generatedColUnwrapped as any,
|
|
933
|
+
})
|
|
934
|
+
|
|
935
|
+
expect(posUnwrapped.line).toBe(1)
|
|
936
|
+
expect(posUnwrapped.column).toBe(28)
|
|
937
|
+
})
|
|
938
|
+
|
|
939
|
+
it('routes a require.context whose directory is held in a variable through the dependency map', async () => {
|
|
940
|
+
// one's router entry reads its route root into a binding and passes that to
|
|
941
|
+
// require.context. if the call is not rewritten it survives into the bundle
|
|
942
|
+
// and reaches metro's fallbackRequireContext, which throws at startup.
|
|
943
|
+
const sourceCode = `
|
|
944
|
+
const routeRoot = './app'
|
|
945
|
+
export const ctx = require.context(routeRoot, true, /\\.tsx$/)
|
|
946
|
+
`
|
|
947
|
+
|
|
948
|
+
const result = await transform({}, '/project', 'entry.js', Buffer.from(sourceCode, 'utf8'), {
|
|
949
|
+
dev: true,
|
|
950
|
+
platform: 'ios',
|
|
951
|
+
type: 'module',
|
|
952
|
+
})
|
|
953
|
+
|
|
954
|
+
const contextDep = result.dependencies.find((d) => d.data.contextParams != null)
|
|
955
|
+
expect(contextDep?.name).toBe('./app')
|
|
956
|
+
|
|
957
|
+
const calls: any[] = []
|
|
958
|
+
let ctx: any
|
|
959
|
+
const sandbox: any = {
|
|
960
|
+
__d: (factory: any) => {
|
|
961
|
+
const module = { exports: {} as any }
|
|
962
|
+
const req: any = (...args: any[]) => {
|
|
963
|
+
calls.push(args)
|
|
964
|
+
return { keys: () => [] }
|
|
965
|
+
}
|
|
966
|
+
// reaching this means the call was left as a runtime require.context
|
|
967
|
+
req.context = () => {
|
|
968
|
+
throw new Error('fallbackRequireContext reached')
|
|
969
|
+
}
|
|
970
|
+
factory({}, req, () => ({}), () => ({}), module, module.exports, ['./app'])
|
|
971
|
+
ctx = module.exports.ctx
|
|
972
|
+
},
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
vm.runInNewContext(result.output[0].data.code, sandbox)
|
|
976
|
+
|
|
977
|
+
expect(ctx).toBeDefined()
|
|
978
|
+
expect(calls[0][1]).toBe('./app')
|
|
979
|
+
})
|
|
980
|
+
|
|
981
|
+
it('gives loop bodies per-iteration bindings that survive a runtime without them', async () => {
|
|
982
|
+
const sourceCode = `
|
|
983
|
+
export function collect() {
|
|
984
|
+
const out = []
|
|
985
|
+
for (let i = 0; i < 2; i++) out.push(() => i)
|
|
986
|
+
for (const label of ['a', 'b']) out.push(() => label)
|
|
987
|
+
for (const key in { x: 1, y: 2 }) out.push(() => key)
|
|
988
|
+
return out.map((f) => f())
|
|
989
|
+
}
|
|
990
|
+
`
|
|
991
|
+
|
|
992
|
+
const result = await transform({}, '/project', 'loops.ts', Buffer.from(sourceCode, 'utf8'), {
|
|
993
|
+
dev: true,
|
|
994
|
+
platform: 'ios',
|
|
995
|
+
type: 'module',
|
|
996
|
+
})
|
|
997
|
+
|
|
998
|
+
const code = result.output[0].data.code
|
|
999
|
+
|
|
1000
|
+
// Hermes emits one CreateFunctionEnvironment before a loop rather than one
|
|
1001
|
+
// per iteration, so a `let` loop head behaves exactly like `var` there.
|
|
1002
|
+
// Rewriting the heads that way reproduces Hermes on a runtime that is
|
|
1003
|
+
// otherwise spec-correct, which is what makes this assertion meaningful:
|
|
1004
|
+
// the untransformed source returns 2,2,b,b,y,y under it.
|
|
1005
|
+
const asHermesWouldSeeIt = code.replace(
|
|
1006
|
+
/\bfor\s*\(\s*(let|const)\b/g,
|
|
1007
|
+
'for (var'
|
|
1008
|
+
)
|
|
1009
|
+
|
|
1010
|
+
let collect: any
|
|
1011
|
+
const sandbox: any = {
|
|
1012
|
+
__d: (factory: any) => {
|
|
1013
|
+
const module = { exports: {} as any }
|
|
1014
|
+
factory({}, () => ({}), () => ({}), () => ({}), module, module.exports, [])
|
|
1015
|
+
collect = module.exports.collect
|
|
1016
|
+
},
|
|
1017
|
+
}
|
|
1018
|
+
vm.runInNewContext(asHermesWouldSeeIt, sandbox)
|
|
1019
|
+
|
|
1020
|
+
expect(collect()).toEqual([0, 1, 'a', 'b', 'x', 'y'])
|
|
1021
|
+
})
|
|
1022
|
+
})
|
|
1023
|
+
|
|
1024
|
+
describe('one native transform ports', () => {
|
|
1025
|
+
it('inlines process.env.ONE_SERVER_URL so a native bundle can find its loaders', () => {
|
|
1026
|
+
// without this a native prod bundle keeps a `process.env` read that has
|
|
1027
|
+
// nothing behind it at runtime, and every loader fetch goes to undefined.
|
|
1028
|
+
process.env.ONE_SERVER_URL = 'https://example.test'
|
|
1029
|
+
try {
|
|
1030
|
+
const out = applyInlineEnvVars(
|
|
1031
|
+
'export const url = process.env.ONE_SERVER_URL;',
|
|
1032
|
+
'loader.ts',
|
|
1033
|
+
false
|
|
1034
|
+
)
|
|
1035
|
+
expect(out).toBe('export const url = "https://example.test";')
|
|
1036
|
+
} finally {
|
|
1037
|
+
process.env.ONE_SERVER_URL = undefined
|
|
1038
|
+
}
|
|
1039
|
+
})
|
|
1040
|
+
|
|
1041
|
+
it('leaves ONE_SERVER_URL alone when it is only a string', () => {
|
|
1042
|
+
const src = 'export const key = "process.env.ONE_SERVER_URL";'
|
|
1043
|
+
expect(applyInlineEnvVars(src, 'a.ts', false)).toBe(src)
|
|
1044
|
+
})
|
|
1045
|
+
|
|
1046
|
+
it('inlines import.meta.env reads, which oxc otherwise lowers to an empty object', () => {
|
|
1047
|
+
// oxc's CJS lowering emits `var import_meta = {}`, so an untouched
|
|
1048
|
+
// `import.meta.env.X` silently reads undefined in every native bundle.
|
|
1049
|
+
const env = { DEV: false, VITE_POSTHOG_API_KEY: 'pk_live', TAMAGUI_TARGET: 'native' }
|
|
1050
|
+
const out = applyInlineEnvVars(
|
|
1051
|
+
`export const key = import.meta.env.VITE_POSTHOG_API_KEY;
|
|
1052
|
+
export const target = import.meta.env?.TAMAGUI_TARGET;
|
|
1053
|
+
export const missing = import.meta.env.NOT_SET;
|
|
1054
|
+
export const all = { ...import.meta.env };`,
|
|
1055
|
+
'env.ts',
|
|
1056
|
+
true,
|
|
1057
|
+
env
|
|
1058
|
+
)
|
|
1059
|
+
expect(out).toContain('export const key = "pk_live";')
|
|
1060
|
+
expect(out).toContain('export const target = "native";')
|
|
1061
|
+
expect(out).toContain('export const missing = undefined;')
|
|
1062
|
+
expect(out).toContain('"VITE_POSTHOG_API_KEY":"pk_live"')
|
|
1063
|
+
expect(out).not.toContain('import.meta')
|
|
1064
|
+
})
|
|
1065
|
+
|
|
1066
|
+
it('inlines process.env keys the vite env map defines', () => {
|
|
1067
|
+
const out = applyInlineEnvVars(
|
|
1068
|
+
'export const t = process.env.TAMAGUI_TARGET; export const u = process.env.UNKNOWN_THING;',
|
|
1069
|
+
'env.ts',
|
|
1070
|
+
true,
|
|
1071
|
+
{ TAMAGUI_TARGET: 'native' }
|
|
1072
|
+
)
|
|
1073
|
+
expect(out).toContain('export const t = "native";')
|
|
1074
|
+
// a key the map does not define is left for the runtime, not guessed at.
|
|
1075
|
+
expect(out).toContain('process.env.UNKNOWN_THING')
|
|
1076
|
+
})
|
|
1077
|
+
|
|
1078
|
+
it('never rewrites an env read in an assignment target', () => {
|
|
1079
|
+
const src = 'process.env.VITE_ENVIRONMENT = "ssr";'
|
|
1080
|
+
expect(applyInlineEnvVars(src, 'a.ts', true, { VITE_ENVIRONMENT: 'ios' })).toBe(src)
|
|
1081
|
+
})
|
|
1082
|
+
|
|
1083
|
+
it('folds branches the inlining made constant so metro never resolves dead imports', () => {
|
|
1084
|
+
const out = applyInlineEnvVars(
|
|
1085
|
+
`if (import.meta.env.SSR) { require('./server-only-thing') } else { load(import.meta.env.API) }`,
|
|
1086
|
+
'branch.ts',
|
|
1087
|
+
true,
|
|
1088
|
+
{ SSR: false, API: 'https://api.test' }
|
|
1089
|
+
)
|
|
1090
|
+
expect(out).not.toContain('server-only-thing')
|
|
1091
|
+
// the surviving branch still gets its own inlining, which a fold that
|
|
1092
|
+
// copied original source would have thrown away.
|
|
1093
|
+
expect(out).toContain('load("https://api.test")')
|
|
1094
|
+
})
|
|
1095
|
+
|
|
1096
|
+
it('folds a ternary and keeps a branch it cannot resolve', () => {
|
|
1097
|
+
expect(
|
|
1098
|
+
applyInlineEnvVars('const a = import.meta.env.DEV ? x : y;', 'a.ts', true, { DEV: false })
|
|
1099
|
+
).toContain('const a = y;')
|
|
1100
|
+
const dynamic = 'const a = flag ? x : y;'
|
|
1101
|
+
expect(applyInlineEnvVars(dynamic, 'a.ts', true, { DEV: false })).toBe(dynamic)
|
|
1102
|
+
})
|
|
1103
|
+
|
|
1104
|
+
it('runs the user native transforms that stand in for their babel plugins', async () => {
|
|
1105
|
+
// takeout's OTA plugin rewrites a bare `__HOT_UPDATER_BUNDLE_ID` that is
|
|
1106
|
+
// otherwise undeclared, so without a way to run it the bundle throws a
|
|
1107
|
+
// ReferenceError at module load. this is that way.
|
|
1108
|
+
const result = await transform(
|
|
1109
|
+
{},
|
|
1110
|
+
import.meta.dirname,
|
|
1111
|
+
'ota.ts',
|
|
1112
|
+
Buffer.from(
|
|
1113
|
+
'export const id = __EXAMPLE_BUILD_ID; export const from = __EXAMPLE_FILENAME;',
|
|
1114
|
+
'utf8'
|
|
1115
|
+
),
|
|
1116
|
+
{
|
|
1117
|
+
dev: true,
|
|
1118
|
+
platform: 'ios',
|
|
1119
|
+
type: 'module',
|
|
1120
|
+
customTransformOptions: {
|
|
1121
|
+
vite: {
|
|
1122
|
+
nativeTransformModules: ['./__fixtures__/exampleNativeTransform.cjs'],
|
|
1123
|
+
},
|
|
1124
|
+
},
|
|
1125
|
+
} as any
|
|
1126
|
+
)
|
|
1127
|
+
|
|
1128
|
+
assertZeroBabelCalls()
|
|
1129
|
+
expect(result.output[0].data.code).toContain('"ios-build"')
|
|
1130
|
+
expect(result.output[0].data.code).not.toContain('__EXAMPLE_BUILD_ID')
|
|
1131
|
+
// the transform is handed an absolute path, not metro's relative one
|
|
1132
|
+
expect(result.output[0].data.code).toContain(
|
|
1133
|
+
JSON.stringify(path.join(import.meta.dirname, 'ota.ts'))
|
|
1134
|
+
)
|
|
1135
|
+
})
|
|
1136
|
+
|
|
1137
|
+
it('refuses to build when a user babel plugin has no native port', () => {
|
|
1138
|
+
// this worker runs no babel, so an unrecognized plugin does nothing at all.
|
|
1139
|
+
// takeout adds `hot-updater/babel-plugin` for OTA; dropping it silently
|
|
1140
|
+
// would ship a build whose updates never apply.
|
|
1141
|
+
const withPlugins = (plugins: any[]) =>
|
|
1142
|
+
({ customTransformOptions: { vite: { babelConfig: { plugins } } } }) as any
|
|
1143
|
+
|
|
1144
|
+
expect(() =>
|
|
1145
|
+
assertNoUnportedBabelPlugins(
|
|
1146
|
+
withPlugins([
|
|
1147
|
+
['@vxrn/vite-plugin-metro/babel-plugins/import-meta-env-plugin', { env: {} }],
|
|
1148
|
+
'one/babel-plugin-environment-guard',
|
|
1149
|
+
'hot-updater/babel-plugin',
|
|
1150
|
+
])
|
|
1151
|
+
)
|
|
1152
|
+
).toThrow(/hot-updater\/babel-plugin/)
|
|
1153
|
+
|
|
1154
|
+
expect(() =>
|
|
1155
|
+
assertNoUnportedBabelPlugins(
|
|
1156
|
+
withPlugins([
|
|
1157
|
+
['@vxrn/vite-plugin-metro/babel-plugins/import-meta-env-plugin', { env: {} }],
|
|
1158
|
+
'one/babel-plugin-environment-guard',
|
|
1159
|
+
['one/babel-plugin-remove-server-code', { routerRoot: 'app' }],
|
|
1160
|
+
['babel-plugin-module-resolver', { alias: {} }],
|
|
1161
|
+
['one/babel-plugin-one-router-metro', {}],
|
|
1162
|
+
'one/babel-plugin-inline-one-server-url',
|
|
1163
|
+
'babel-plugin-react-compiler',
|
|
1164
|
+
'react-native-reanimated/plugin',
|
|
1165
|
+
])
|
|
1166
|
+
)
|
|
1167
|
+
).not.toThrow()
|
|
1168
|
+
})
|
|
1169
|
+
|
|
1170
|
+
it('turns a server-only import into a throw and drops native-only', () => {
|
|
1171
|
+
const out = applyEnvironmentGuard(
|
|
1172
|
+
`import 'native-only'\nimport 'server-only'\nexport const a = 1`,
|
|
1173
|
+
'route.ts'
|
|
1174
|
+
)
|
|
1175
|
+
expect(out).not.toContain('native-only')
|
|
1176
|
+
expect(out).toContain('server-only cannot be imported in a native environment')
|
|
1177
|
+
// the throw has to replace the import as a STATEMENT. an expression left in
|
|
1178
|
+
// its place is a syntax error, which would fail the whole bundle to parse.
|
|
1179
|
+
const parsed = parseSync('route.ts', out, { lang: 'ts' })
|
|
1180
|
+
expect(parsed.errors).toEqual([])
|
|
1181
|
+
expect(parsed.program.body.map((n: any) => n.type)).toContain('ThrowStatement')
|
|
1182
|
+
})
|
|
1183
|
+
|
|
1184
|
+
it('reads the router root off one\'s remove-server-code plugin entry', () => {
|
|
1185
|
+
const options: any = {
|
|
1186
|
+
customTransformOptions: {
|
|
1187
|
+
vite: {
|
|
1188
|
+
babelConfig: {
|
|
1189
|
+
plugins: [['one/babel-plugin-remove-server-code', { routerRoot: 'src/app' }]],
|
|
1190
|
+
},
|
|
1191
|
+
},
|
|
1192
|
+
},
|
|
1193
|
+
}
|
|
1194
|
+
expect(getRemoveServerCodeRouterRoot(options)).toBe('src/app')
|
|
1195
|
+
// absent means one did not ask for it, which must not be read as 'app'
|
|
1196
|
+
expect(getRemoveServerCodeRouterRoot({ customTransformOptions: {} } as any)).toBe(
|
|
1197
|
+
undefined
|
|
1198
|
+
)
|
|
1199
|
+
})
|
|
1200
|
+
})
|