vxrn 1.26.0 → 1.26.1
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/plugins/reactNativeDevServer.mjs +123 -42
- package/dist/plugins/reactNativeDevServer.mjs.map +1 -1
- package/dist/plugins/reactNativeDevServer.native.js +209 -44
- package/dist/plugins/reactNativeDevServer.native.js.map +1 -1
- package/dist/rn-commands/bundle/buildBundle.mjs +29 -10
- package/dist/rn-commands/bundle/buildBundle.mjs.map +1 -1
- package/dist/rn-commands/bundle/buildBundle.native.js +29 -10
- package/dist/rn-commands/bundle/buildBundle.native.js.map +1 -1
- package/dist/utils/createNativeDevEngine.mjs +220 -83
- package/dist/utils/createNativeDevEngine.mjs.map +1 -1
- package/dist/utils/createNativeDevEngine.native.js +257 -84
- package/dist/utils/createNativeDevEngine.native.js.map +1 -1
- package/dist/utils/createNativeDevEngine.test.mjs +285 -23
- package/dist/utils/createNativeDevEngine.test.mjs.map +1 -1
- package/dist/utils/createNativeDevEngine.test.native.js +319 -23
- package/dist/utils/createNativeDevEngine.test.native.js.map +1 -1
- package/dist/utils/patches.test.mjs.map +1 -1
- package/dist/utils/patches.test.native.js.map +1 -1
- package/dist/utils/symbolicateNativeStack.mjs +66 -0
- package/dist/utils/symbolicateNativeStack.mjs.map +1 -0
- package/dist/utils/symbolicateNativeStack.native.js +70 -0
- package/dist/utils/symbolicateNativeStack.native.js.map +1 -0
- package/dist/utils/symbolicateNativeStack.test.mjs +96 -0
- package/dist/utils/symbolicateNativeStack.test.mjs.map +1 -0
- package/dist/utils/symbolicateNativeStack.test.native.js +96 -0
- package/dist/utils/symbolicateNativeStack.test.native.js.map +1 -0
- package/package.json +12 -11
- package/src/plugins/reactNativeDevServer.ts +162 -50
- package/src/rn-commands/bundle/buildBundle.ts +28 -9
- package/src/utils/createNativeDevEngine.test.ts +581 -193
- package/src/utils/createNativeDevEngine.ts +434 -115
- package/src/utils/patches.test.ts +1 -4
- package/src/utils/symbolicateNativeStack.test.ts +78 -0
- package/src/utils/symbolicateNativeStack.ts +129 -0
- package/types/plugins/reactNativeDevServer.d.ts.map +1 -1
- package/types/rn-commands/bundle/buildBundle.d.ts.map +1 -1
- package/types/utils/createNativeDevEngine.d.ts +23 -4
- package/types/utils/createNativeDevEngine.d.ts.map +1 -1
- package/types/utils/symbolicateNativeStack.d.ts +46 -0
- package/types/utils/symbolicateNativeStack.d.ts.map +1 -0
- package/types/utils/symbolicateNativeStack.test.d.ts +2 -0
- package/types/utils/symbolicateNativeStack.test.d.ts.map +1 -0
|
@@ -1,10 +1,12 @@
|
|
|
1
|
-
import { existsSync } from 'node:fs'
|
|
1
|
+
import { existsSync, realpathSync } from 'node:fs'
|
|
2
2
|
import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises'
|
|
3
3
|
import { tmpdir } from 'node:os'
|
|
4
4
|
import { join } from 'node:path'
|
|
5
5
|
import { createContext, runInContext, runInNewContext } from 'node:vm'
|
|
6
|
-
import {
|
|
6
|
+
import { originalPositionFor, TraceMap } from '@jridgewell/trace-mapping'
|
|
7
|
+
import { rolldown, type Plugin, type RolldownOutput } from 'rolldown'
|
|
7
8
|
import { dev } from 'rolldown/experimental'
|
|
9
|
+
import { normalizePath } from 'vite'
|
|
8
10
|
import { describe, expect, it, vi } from 'vitest'
|
|
9
11
|
import { getNativePrelude } from '../runtime/native-prelude'
|
|
10
12
|
import { workletImportsPlugin } from '../plugins/workletImportsPlugin'
|
|
@@ -20,10 +22,104 @@ import {
|
|
|
20
22
|
hmrClientNoopPlugin,
|
|
21
23
|
nativeAnimatedGuardPlugin,
|
|
22
24
|
normalizeNativeCommonJSInterop,
|
|
25
|
+
postProcessNativeBundle,
|
|
23
26
|
vxrnCompilerPlugin,
|
|
24
27
|
wrapNativeBundleModuleScope,
|
|
28
|
+
type NativePluginContext,
|
|
25
29
|
} from './createNativeDevEngine'
|
|
26
30
|
|
|
31
|
+
describe.each(['ios', 'android'] as const)('native plugin adapters on %s', (platform) => {
|
|
32
|
+
it.each(['dev', 'build'] as const)(
|
|
33
|
+
'binds configured plugins to the %s bundle',
|
|
34
|
+
async (mode) => {
|
|
35
|
+
const root = await mkdtemp(join(tmpdir(), 'vxrn-native-plugin-adapter-'))
|
|
36
|
+
await writeFile(
|
|
37
|
+
join(root, 'entry.js'),
|
|
38
|
+
`globalThis.nativePluginResult = [__GLOBAL_PLUGIN__, __DIRECT_PLUGIN__, 'plain-plugin']`
|
|
39
|
+
)
|
|
40
|
+
const factories = ['global', 'direct'].map((name) =>
|
|
41
|
+
vi.fn(
|
|
42
|
+
(context: NativePluginContext): Plugin => ({
|
|
43
|
+
name: `${name}-native`,
|
|
44
|
+
transform(code, id) {
|
|
45
|
+
if (id.endsWith('/entry.js')) {
|
|
46
|
+
return code.replace(
|
|
47
|
+
`__${name.toUpperCase()}_PLUGIN__`,
|
|
48
|
+
JSON.stringify({ name, ...context })
|
|
49
|
+
)
|
|
50
|
+
}
|
|
51
|
+
},
|
|
52
|
+
})
|
|
53
|
+
)
|
|
54
|
+
)
|
|
55
|
+
const plugins = factories.map(
|
|
56
|
+
(vxrnNative, index): Plugin => ({
|
|
57
|
+
name: `adapter-${index}`,
|
|
58
|
+
api: { vxrnNative },
|
|
59
|
+
})
|
|
60
|
+
)
|
|
61
|
+
const previousPlugins = globalThis.__vxrnAddNativePlugins
|
|
62
|
+
globalThis.__vxrnAddNativePlugins = [plugins[0]]
|
|
63
|
+
const configuredPlugins: Plugin[] = [
|
|
64
|
+
plugins[1],
|
|
65
|
+
{
|
|
66
|
+
name: 'plain-native-plugin',
|
|
67
|
+
transform(code, id) {
|
|
68
|
+
if (id.endsWith('/__virtual-native-entry.tsx')) return `import './entry.js'`
|
|
69
|
+
if (id.endsWith('/entry.js')) return code.replace('plain-plugin', 'plain')
|
|
70
|
+
},
|
|
71
|
+
},
|
|
72
|
+
]
|
|
73
|
+
let native: Awaited<ReturnType<typeof createNativeDevEngine>> | undefined
|
|
74
|
+
try {
|
|
75
|
+
let code: string
|
|
76
|
+
if (mode === 'dev') {
|
|
77
|
+
native = await createNativeDevEngine({
|
|
78
|
+
root,
|
|
79
|
+
platform,
|
|
80
|
+
port: 0,
|
|
81
|
+
plugins: configuredPlugins,
|
|
82
|
+
})
|
|
83
|
+
code = (await native.getBundle()).code
|
|
84
|
+
} else {
|
|
85
|
+
code = (
|
|
86
|
+
await buildNativeBundle({
|
|
87
|
+
root,
|
|
88
|
+
platform,
|
|
89
|
+
entryFile: 'entry.js',
|
|
90
|
+
plugins: configuredPlugins,
|
|
91
|
+
})
|
|
92
|
+
).code
|
|
93
|
+
}
|
|
94
|
+
const context = {
|
|
95
|
+
console,
|
|
96
|
+
setTimeout,
|
|
97
|
+
clearTimeout,
|
|
98
|
+
__GLOBAL_PLUGIN__: 'unadapted',
|
|
99
|
+
__DIRECT_PLUGIN__: 'unadapted',
|
|
100
|
+
}
|
|
101
|
+
runInNewContext(code, context)
|
|
102
|
+
expect(Reflect.get(context, 'nativePluginResult')).toEqual([
|
|
103
|
+
{ name: 'global', root, platform, dev: mode === 'dev' },
|
|
104
|
+
{ name: 'direct', root, platform, dev: mode === 'dev' },
|
|
105
|
+
'plain',
|
|
106
|
+
])
|
|
107
|
+
for (const factory of factories) {
|
|
108
|
+
expect(factory).toHaveBeenCalledExactlyOnceWith({
|
|
109
|
+
root,
|
|
110
|
+
platform,
|
|
111
|
+
dev: mode === 'dev',
|
|
112
|
+
})
|
|
113
|
+
}
|
|
114
|
+
} finally {
|
|
115
|
+
await native?.close()
|
|
116
|
+
globalThis.__vxrnAddNativePlugins = previousPlugins
|
|
117
|
+
await rm(root, { recursive: true, force: true })
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
)
|
|
121
|
+
})
|
|
122
|
+
|
|
27
123
|
const nativeTransformProbe = `
|
|
28
124
|
export const transformProbe = () => {
|
|
29
125
|
'worklet'
|
|
@@ -128,120 +224,155 @@ globalThis.result = (function(parentArgument) {
|
|
|
128
224
|
}
|
|
129
225
|
)
|
|
130
226
|
|
|
131
|
-
describe.each(['shared', 'rolldown', 'metro'])(
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
await plugin.configResolved({ root: projectRoot, build: {} })
|
|
160
|
-
const hook = plugin.transform.handler || plugin.transform
|
|
161
|
-
result = await hook.call({ environment: { name: 'ios' } }, source, id)
|
|
162
|
-
} else if (pipeline === 'metro') {
|
|
163
|
-
const { transform } = await import('@vxrn/vite-plugin-metro/metroNativeWorker')
|
|
164
|
-
const output = await transform({}, projectRoot, id, Buffer.from(source), {
|
|
165
|
-
dev: false,
|
|
166
|
-
platform: 'ios',
|
|
167
|
-
type: 'module',
|
|
168
|
-
customTransformOptions: { reactCompiler: true, worklets: true },
|
|
227
|
+
describe.each(['shared', 'rolldown', 'metro'])(
|
|
228
|
+
'React Compiler worklets through %s',
|
|
229
|
+
(pipeline) => {
|
|
230
|
+
it.each([
|
|
231
|
+
['block', 'useDerivedValue(() => { return value + 8 }, [value])', 18],
|
|
232
|
+
['expression', 'useDerivedValue(() => value + 8, [value])', 18],
|
|
233
|
+
['object', 'useDerivedValue(() => ({ lift: value + 8 }), [value])', { lift: 18 }],
|
|
234
|
+
['nested', 'useDerivedValue(() => runOnUI(() => value + 8), [value])', 18],
|
|
235
|
+
[
|
|
236
|
+
'gesture',
|
|
237
|
+
'useDerivedValue(() => Gesture.Pan().onUpdate(() => value + 8), [value])',
|
|
238
|
+
18,
|
|
239
|
+
],
|
|
240
|
+
])(
|
|
241
|
+
'preserves %s callbacks on the UI runtime and memoizes stable inputs',
|
|
242
|
+
async (_name, callback, expected) => {
|
|
243
|
+
const compiler = await import('@vxrn/compiler')
|
|
244
|
+
const { parseSync } = await import('oxc-parser')
|
|
245
|
+
const { default: MagicString } = await import('magic-string')
|
|
246
|
+
const { TraceMap, originalPositionFor } =
|
|
247
|
+
await import('@jridgewell/trace-mapping')
|
|
248
|
+
const projectRoot = await createWorkletsProject(false)
|
|
249
|
+
const sourceMaps = process.env.VXRN_ENABLE_SOURCE_MAP
|
|
250
|
+
process.env.VXRN_ENABLE_SOURCE_MAP = '1'
|
|
251
|
+
compiler.configureVXRNCompilerPlugin({
|
|
252
|
+
enableCompiler: true,
|
|
253
|
+
enableReanimated: true,
|
|
254
|
+
enableNativeWorklets: true,
|
|
169
255
|
})
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
256
|
+
try {
|
|
257
|
+
const id = join(projectRoot, 'useProbe.ts')
|
|
258
|
+
const source = `export function useProbe(value) {\n return ${callback};\n}\n`
|
|
259
|
+
await writeFile(id, source)
|
|
260
|
+
let result: any
|
|
261
|
+
if (pipeline === 'shared') {
|
|
262
|
+
const plugins = await compiler.createVXRNCompilerPlugin()
|
|
263
|
+
const plugin = plugins.find((p: any) => p.name === 'one:compiler') as any
|
|
264
|
+
await plugin.configResolved({ root: projectRoot, build: {} })
|
|
265
|
+
const hook = plugin.transform.handler || plugin.transform
|
|
266
|
+
result = await hook.call({ environment: { name: 'ios' } }, source, id)
|
|
267
|
+
} else if (pipeline === 'metro') {
|
|
268
|
+
const { transform } =
|
|
269
|
+
await import('@vxrn/vite-plugin-metro/metroNativeWorker')
|
|
270
|
+
const output = await transform({}, projectRoot, id, Buffer.from(source), {
|
|
271
|
+
dev: false,
|
|
272
|
+
platform: 'ios',
|
|
273
|
+
type: 'module',
|
|
274
|
+
customTransformOptions: { reactCompiler: true, worklets: true },
|
|
275
|
+
})
|
|
276
|
+
const data = output.output[0].data
|
|
277
|
+
const msm = await import('metro-source-map')
|
|
278
|
+
result = {
|
|
279
|
+
code: data.code,
|
|
280
|
+
map: (msm.fromRawMappings as any)([
|
|
281
|
+
{ code: data.code, path: id, source, map: data.map },
|
|
282
|
+
]).toMap(),
|
|
283
|
+
}
|
|
284
|
+
} else {
|
|
285
|
+
const plugin = vxrnCompilerPlugin('ios', false, projectRoot, true)
|
|
286
|
+
result = await Reflect.apply(plugin.transform as Function, undefined, [
|
|
287
|
+
source,
|
|
288
|
+
id,
|
|
289
|
+
])
|
|
290
|
+
}
|
|
291
|
+
const runnable = new MagicString(result.code)
|
|
292
|
+
for (const node of parseSync('probe.js', result.code).program.body) {
|
|
293
|
+
if (node.type === 'ImportDeclaration') runnable.remove(node.start, node.end)
|
|
294
|
+
if (node.type === 'ExportNamedDeclaration' && node.declaration) {
|
|
295
|
+
runnable.remove(node.start, node.declaration.start)
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
let cache: any[] | undefined
|
|
299
|
+
let cacheCalls = 0
|
|
300
|
+
const memo = (size: number) => {
|
|
301
|
+
cacheCalls++
|
|
302
|
+
return (cache ??= Array(size).fill(Symbol.for('react.memo_cache_sentinel')))
|
|
303
|
+
}
|
|
304
|
+
const module = { exports: {} as any }
|
|
305
|
+
const probe = new Function(
|
|
306
|
+
'_c',
|
|
307
|
+
'useDerivedValue',
|
|
308
|
+
'runOnUI',
|
|
309
|
+
'Gesture',
|
|
310
|
+
'__d',
|
|
311
|
+
'module',
|
|
312
|
+
`${runnable}\nreturn ${pipeline === 'metro' ? 'module.exports.useProbe' : 'useProbe'}`
|
|
313
|
+
)(
|
|
314
|
+
memo,
|
|
315
|
+
(fn: any) => fn,
|
|
316
|
+
(fn: any) => fn,
|
|
317
|
+
{ Pan: () => ({ onUpdate: (fn: any) => fn }) },
|
|
318
|
+
(factory: any) =>
|
|
319
|
+
factory(
|
|
320
|
+
globalThis,
|
|
321
|
+
() => ({ c: memo }),
|
|
322
|
+
() => {},
|
|
323
|
+
() => {},
|
|
324
|
+
module,
|
|
325
|
+
module.exports,
|
|
326
|
+
[]
|
|
327
|
+
),
|
|
328
|
+
module
|
|
329
|
+
)
|
|
330
|
+
const first = probe(10)
|
|
331
|
+
expect(first.__initData?.code).toBeTypeOf('string')
|
|
332
|
+
for (const onUI of [false, true]) {
|
|
333
|
+
let fn = first
|
|
334
|
+
let value: any
|
|
335
|
+
for (let depth = 0; depth < 2; depth++) {
|
|
336
|
+
expect(fn.__initData?.code).toBeTypeOf('string')
|
|
337
|
+
value = onUI
|
|
338
|
+
? new Function('runOnUI', `return (${fn.__initData.code})`)(
|
|
339
|
+
(fn: any) => fn
|
|
340
|
+
).call({ __closure: fn.__closure })
|
|
341
|
+
: fn()
|
|
342
|
+
if (typeof value !== 'function') break
|
|
343
|
+
fn = value
|
|
344
|
+
}
|
|
345
|
+
expect(value).toEqual(expected)
|
|
346
|
+
}
|
|
347
|
+
expect(probe(10)).toBe(first)
|
|
348
|
+
expect(cacheCalls).toBe(2)
|
|
349
|
+
expect(probe(20)).not.toBe(first)
|
|
350
|
+
|
|
351
|
+
const lines = result.code.split('\n')
|
|
352
|
+
const line = lines.findIndex((text: string) =>
|
|
353
|
+
text.includes('function useProbe(')
|
|
354
|
+
)
|
|
355
|
+
expect(line).toBeGreaterThanOrEqual(0)
|
|
356
|
+
const position = originalPositionFor(new TraceMap(result.map), {
|
|
357
|
+
line: line + 1,
|
|
358
|
+
column: lines[line].indexOf('function'),
|
|
359
|
+
})
|
|
360
|
+
expect(position.source).toBe(id)
|
|
361
|
+
expect(position.line).toBe(1)
|
|
362
|
+
} finally {
|
|
363
|
+
if (sourceMaps === undefined) delete process.env.VXRN_ENABLE_SOURCE_MAP
|
|
364
|
+
else process.env.VXRN_ENABLE_SOURCE_MAP = sourceMaps
|
|
365
|
+
compiler.configureVXRNCompilerPlugin({
|
|
366
|
+
enableCompiler: false,
|
|
367
|
+
enableReanimated: false,
|
|
368
|
+
enableNativeWorklets: false,
|
|
369
|
+
})
|
|
370
|
+
await rm(projectRoot, { recursive: true, force: true })
|
|
217
371
|
}
|
|
218
|
-
expect(value).toEqual(expected)
|
|
219
372
|
}
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
const lines = result.code.split('\n')
|
|
225
|
-
const line = lines.findIndex((text: string) => text.includes('function useProbe('))
|
|
226
|
-
expect(line).toBeGreaterThanOrEqual(0)
|
|
227
|
-
const position = originalPositionFor(new TraceMap(result.map), {
|
|
228
|
-
line: line + 1,
|
|
229
|
-
column: lines[line].indexOf('function'),
|
|
230
|
-
})
|
|
231
|
-
expect(position.source).toBe(id)
|
|
232
|
-
expect(position.line).toBe(1)
|
|
233
|
-
} finally {
|
|
234
|
-
if (sourceMaps === undefined) delete process.env.VXRN_ENABLE_SOURCE_MAP
|
|
235
|
-
else process.env.VXRN_ENABLE_SOURCE_MAP = sourceMaps
|
|
236
|
-
compiler.configureVXRNCompilerPlugin({
|
|
237
|
-
enableCompiler: false,
|
|
238
|
-
enableReanimated: false,
|
|
239
|
-
enableNativeWorklets: false,
|
|
240
|
-
})
|
|
241
|
-
await rm(projectRoot, { recursive: true, force: true })
|
|
242
|
-
}
|
|
243
|
-
})
|
|
244
|
-
})
|
|
373
|
+
)
|
|
374
|
+
}
|
|
375
|
+
)
|
|
245
376
|
|
|
246
377
|
async function createWorkletsProject(throwOnTransform = true) {
|
|
247
378
|
const testRoot = await mkdtemp(join(tmpdir(), 'vxrn-native-transform-failure-'))
|
|
@@ -396,12 +527,17 @@ if (import.meta.hot) import.meta.hot.accept(() => {})
|
|
|
396
527
|
join(root, 'production.mjs'),
|
|
397
528
|
`import calculate, { reads } from 'pure-math'; globalThis.calculate = calculate; globalThis.reads = reads`
|
|
398
529
|
)
|
|
530
|
+
// a production bundle is minified by default, so this also covers worklet
|
|
531
|
+
// closure serialization under mangling: the worklet body is a string
|
|
532
|
+
// literal the minifier leaves alone, and __closure carries its captures
|
|
533
|
+
// by property name, which mangling does not rewrite.
|
|
399
534
|
const production = await buildNativeBundle({
|
|
400
535
|
root,
|
|
401
536
|
platform: 'android',
|
|
402
537
|
entryFile: 'production.mjs',
|
|
403
538
|
plugins: [workletImportsPlugin({ 'pure-math': ['default', 'reads'] })],
|
|
404
539
|
})
|
|
540
|
+
expect(production.code).not.toContain('__esmMin')
|
|
405
541
|
const productionRN: any = { console }
|
|
406
542
|
runInNewContext(production.code, productionRN)
|
|
407
543
|
expect(productionRN.calculate(10)).toBe(109)
|
|
@@ -409,6 +545,20 @@ if (import.meta.hot) import.meta.hot.accept(() => {})
|
|
|
409
545
|
expect(reconstruct(productionRN.reads)()).toBe(3)
|
|
410
546
|
expect(productionRN.reads()).toBe(1)
|
|
411
547
|
|
|
548
|
+
const unminifiedProduction = await buildNativeBundle({
|
|
549
|
+
root,
|
|
550
|
+
platform: 'android',
|
|
551
|
+
entryFile: 'production.mjs',
|
|
552
|
+
minify: false,
|
|
553
|
+
plugins: [workletImportsPlugin({ 'pure-math': ['default', 'reads'] })],
|
|
554
|
+
})
|
|
555
|
+
expect(unminifiedProduction.code).toContain('__esmMin')
|
|
556
|
+
const unminifiedRN: any = { console }
|
|
557
|
+
runInNewContext(unminifiedProduction.code, unminifiedRN)
|
|
558
|
+
expect(unminifiedRN.calculate(10)).toBe(109)
|
|
559
|
+
expect(reconstruct(unminifiedRN.calculate)(10)).toBe(109)
|
|
560
|
+
expect(reconstruct(unminifiedRN.reads)()).toBe(3)
|
|
561
|
+
|
|
412
562
|
const sharedPlugin = workletImportsPlugin({ 'pure-math': ['default', 'reads'] })
|
|
413
563
|
const parallel = await Promise.all(
|
|
414
564
|
(['ios', 'android'] as const).map(async (platform) => {
|
|
@@ -512,60 +662,70 @@ if (import.meta.hot) import.meta.hot.accept(() => {})
|
|
|
512
662
|
})
|
|
513
663
|
|
|
514
664
|
describe('native Rolldown HMR runtime', () => {
|
|
515
|
-
it.each([true, false])(
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
665
|
+
it.each([true, false])(
|
|
666
|
+
'keeps CommonJS default components callable through the native HMR wire path (Babel=%s)',
|
|
667
|
+
async (babel) => {
|
|
668
|
+
const root = await mkdtemp(join(tmpdir(), 'vxrn-native-hmr-interop-'))
|
|
669
|
+
const entry = join(root, 'entry.mjs')
|
|
670
|
+
const packageRoot = join(root, 'node_modules/component')
|
|
671
|
+
await mkdir(packageRoot, { recursive: true })
|
|
672
|
+
await writeFile(
|
|
673
|
+
join(packageRoot, 'package.json'),
|
|
674
|
+
JSON.stringify({ main: './component(view).js' })
|
|
675
|
+
)
|
|
676
|
+
await writeFile(
|
|
677
|
+
join(packageRoot, 'component(view).js'),
|
|
678
|
+
babel
|
|
679
|
+
? `Object.defineProperty(exports, '__esModule', { value: true }); exports.default = function Component() { return 42 }`
|
|
680
|
+
: `module.exports = function Component() { return 42 }`
|
|
681
|
+
)
|
|
682
|
+
const source = (version: number) => `
|
|
531
683
|
import Component from 'component'
|
|
532
684
|
globalThis.renderComponent = Component
|
|
533
685
|
export const version = ${version}
|
|
534
686
|
if (import.meta.hot) import.meta.hot.accept(() => {})
|
|
535
687
|
`
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
688
|
+
await writeFile(entry, source(1))
|
|
689
|
+
let resolveUpdate!: (update: any) => void
|
|
690
|
+
const updateReceived = new Promise<any>((resolve) => {
|
|
691
|
+
resolveUpdate = resolve
|
|
692
|
+
})
|
|
693
|
+
const native = await createNativeDevEngine({
|
|
694
|
+
root,
|
|
695
|
+
port: 0,
|
|
696
|
+
platform: 'ios',
|
|
697
|
+
plugins: [
|
|
698
|
+
{
|
|
699
|
+
name: 'native-hmr-fixture-entry',
|
|
700
|
+
transform(_code, id) {
|
|
701
|
+
if (id.endsWith('/__virtual-native-entry.tsx'))
|
|
702
|
+
return `import './entry.mjs'`
|
|
703
|
+
},
|
|
704
|
+
},
|
|
705
|
+
],
|
|
706
|
+
onHmrUpdate: resolveUpdate,
|
|
707
|
+
})
|
|
708
|
+
try {
|
|
709
|
+
const initial = await native.getBundle()
|
|
710
|
+
const context: any = { console, setTimeout, clearTimeout }
|
|
711
|
+
runInNewContext(initial.code, context)
|
|
712
|
+
expect(context.renderComponent()).toBe(42)
|
|
713
|
+
const runtime = context.__rolldown_runtime__
|
|
714
|
+
await native.engine.registerClient(runtime.clientId)
|
|
715
|
+
await writeFile(entry, source(2))
|
|
716
|
+
const update = await updateReceived
|
|
717
|
+
expect(update.type).toBe('hmr:update')
|
|
718
|
+
expect(update.clientId).toBe(runtime.clientId)
|
|
719
|
+
expect(runtime.applyHmrUpdate(update.code, update.changedIds, update.seq)).toBe(
|
|
720
|
+
true
|
|
721
|
+
)
|
|
722
|
+
expect(context.renderComponent()).toBe(42)
|
|
723
|
+
} finally {
|
|
724
|
+
await native.close()
|
|
725
|
+
await rm(root, { recursive: true, force: true })
|
|
726
|
+
}
|
|
567
727
|
}
|
|
568
|
-
|
|
728
|
+
)
|
|
569
729
|
|
|
570
730
|
it(
|
|
571
731
|
'registers a Rolldown 1.2 client and applies a self-accepted patch',
|
|
@@ -1594,6 +1754,102 @@ export function Box() {
|
|
|
1594
1754
|
})
|
|
1595
1755
|
})
|
|
1596
1756
|
|
|
1757
|
+
describe('native production minification', () => {
|
|
1758
|
+
// a cjs dependency reached through an esm module, a top-level export, and a
|
|
1759
|
+
// runtime result that depends on both. minification mangles every top-level
|
|
1760
|
+
// name here and drops every comment, which is what the bundle's
|
|
1761
|
+
// post-processing reads, so a pass that runs on minified output stops
|
|
1762
|
+
// applying — silently for the commonjs interop, and fatally for the `export`
|
|
1763
|
+
// statement, which hermes cannot parse.
|
|
1764
|
+
async function writeMinifyFixture() {
|
|
1765
|
+
const root = await mkdtemp(join(tmpdir(), 'vxrn-native-minify-'))
|
|
1766
|
+
const dep = join(root, 'node_modules', 'legacy-color')
|
|
1767
|
+
await mkdir(dep, { recursive: true })
|
|
1768
|
+
await writeFile(
|
|
1769
|
+
join(dep, 'package.json'),
|
|
1770
|
+
JSON.stringify({ name: 'legacy-color', main: 'index.js' })
|
|
1771
|
+
)
|
|
1772
|
+
await writeFile(
|
|
1773
|
+
join(dep, 'index.js'),
|
|
1774
|
+
`function LegacyColor(value) { this.value = value }
|
|
1775
|
+
LegacyColor.prototype.describe = function () { return 'legacy:' + this.value }
|
|
1776
|
+
module.exports = LegacyColor`
|
|
1777
|
+
)
|
|
1778
|
+
await writeFile(
|
|
1779
|
+
join(root, 'views.js'),
|
|
1780
|
+
`'use strict';
|
|
1781
|
+
import LegacyColor from 'legacy-color'
|
|
1782
|
+
import { level } from './entry.js'
|
|
1783
|
+
export const describeHeader = () => new LegacyColor(level()).describe()`
|
|
1784
|
+
)
|
|
1785
|
+
await writeFile(
|
|
1786
|
+
join(root, 'entry.js'),
|
|
1787
|
+
`import { describeHeader } from './views.js'
|
|
1788
|
+
export const level = () => 'header'
|
|
1789
|
+
globalThis.minifyProbe = describeHeader()`
|
|
1790
|
+
)
|
|
1791
|
+
return root
|
|
1792
|
+
}
|
|
1793
|
+
|
|
1794
|
+
function runFixture(code: string) {
|
|
1795
|
+
const context: Record<string, unknown> = { console }
|
|
1796
|
+
runInNewContext(code, context)
|
|
1797
|
+
return Reflect.get(context, 'minifyProbe')
|
|
1798
|
+
}
|
|
1799
|
+
|
|
1800
|
+
it('minifies when asked, leaves the bundle alone when not, and both behave the same', async () => {
|
|
1801
|
+
const root = await writeMinifyFixture()
|
|
1802
|
+
try {
|
|
1803
|
+
const [plain, minified] = await Promise.all(
|
|
1804
|
+
[false, true].map((minify) =>
|
|
1805
|
+
buildNativeBundle({ root, platform: 'ios', entryFile: 'entry.js', minify })
|
|
1806
|
+
)
|
|
1807
|
+
)
|
|
1808
|
+
|
|
1809
|
+
// the unminified bundle keeps rolldown's generated names and layout
|
|
1810
|
+
expect(plain.code).toContain('__toESM')
|
|
1811
|
+
expect(plain.code).toContain('describeHeader')
|
|
1812
|
+
expect(plain.code.split('\n').length).toBeGreaterThan(40)
|
|
1813
|
+
|
|
1814
|
+
// the minified one keeps neither
|
|
1815
|
+
expect(minified.code).not.toContain('describeHeader')
|
|
1816
|
+
expect(minified.code).not.toContain('//#region')
|
|
1817
|
+
expect(minified.code.length).toBeLessThan(plain.code.length * 0.7)
|
|
1818
|
+
|
|
1819
|
+
// and both still run, with the same result. before the post-processing
|
|
1820
|
+
// moved inside the bundle, the minified one threw on its surviving
|
|
1821
|
+
// `export {}` statement.
|
|
1822
|
+
expect(runFixture(minified.code)).toBe('legacy:header')
|
|
1823
|
+
expect(runFixture(plain.code)).toBe('legacy:header')
|
|
1824
|
+
} finally {
|
|
1825
|
+
await rm(root, { recursive: true, force: true })
|
|
1826
|
+
}
|
|
1827
|
+
})
|
|
1828
|
+
|
|
1829
|
+
it("defaults to react native's rule: minify a production bundle, never a dev one", async () => {
|
|
1830
|
+
const root = await writeMinifyFixture()
|
|
1831
|
+
try {
|
|
1832
|
+
const [prod, devBundle, explicitOff] = await Promise.all([
|
|
1833
|
+
buildNativeBundle({ root, platform: 'ios', entryFile: 'entry.js' }),
|
|
1834
|
+
buildNativeBundle({ root, platform: 'ios', entryFile: 'entry.js', dev: true }),
|
|
1835
|
+
buildNativeBundle({
|
|
1836
|
+
root,
|
|
1837
|
+
platform: 'ios',
|
|
1838
|
+
entryFile: 'entry.js',
|
|
1839
|
+
minify: false,
|
|
1840
|
+
}),
|
|
1841
|
+
])
|
|
1842
|
+
|
|
1843
|
+
expect(prod.code).not.toContain('describeHeader')
|
|
1844
|
+
expect(devBundle.code).toContain('describeHeader')
|
|
1845
|
+
expect(explicitOff.code).toContain('describeHeader')
|
|
1846
|
+
expect(prod.code.length).toBeLessThan(explicitOff.code.length)
|
|
1847
|
+
} finally {
|
|
1848
|
+
await rm(root, { recursive: true, force: true })
|
|
1849
|
+
}
|
|
1850
|
+
})
|
|
1851
|
+
})
|
|
1852
|
+
|
|
1597
1853
|
describe('native production assets', () => {
|
|
1598
1854
|
it('registers scale siblings and keeps monorepo assets inside assetsDest', async () => {
|
|
1599
1855
|
const testRoot = await mkdtemp(join(tmpdir(), 'vxrn-native-assets-'))
|
|
@@ -1650,7 +1906,23 @@ globalThis.__nativeAssetProbe = [icon, back]
|
|
|
1650
1906
|
assetsDest,
|
|
1651
1907
|
})
|
|
1652
1908
|
|
|
1653
|
-
|
|
1909
|
+
const registered: Array<{ name: string; scales: number[] }> = []
|
|
1910
|
+
runInNewContext(result.code, {
|
|
1911
|
+
console,
|
|
1912
|
+
require: (id: string) =>
|
|
1913
|
+
id === 'react-native/Libraries/Image/AssetRegistry'
|
|
1914
|
+
? {
|
|
1915
|
+
registerAsset: (asset: { name: string; scales: number[] }) => {
|
|
1916
|
+
registered.push(asset)
|
|
1917
|
+
return asset
|
|
1918
|
+
},
|
|
1919
|
+
}
|
|
1920
|
+
: {},
|
|
1921
|
+
})
|
|
1922
|
+
expect(registered.map((asset) => [asset.name, asset.scales])).toEqual([
|
|
1923
|
+
['icon', [1, 2, 3]],
|
|
1924
|
+
['back', [1, 2]],
|
|
1925
|
+
])
|
|
1654
1926
|
for (const file of ['icon.png', 'icon@2x.png', 'icon@3x.png']) {
|
|
1655
1927
|
expect(existsSync(join(assetsDest, 'assets/assets', file))).toBe(true)
|
|
1656
1928
|
}
|
|
@@ -1835,21 +2107,23 @@ globalThis.__vxrnConditionalExportProbe = helper()
|
|
|
1835
2107
|
})
|
|
1836
2108
|
|
|
1837
2109
|
describe('native Flow sources', () => {
|
|
1838
|
-
it.each(['', `/* ${'license text '.repeat(150)} */\n`])(
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
join(packageRoot, 'js
|
|
1852
|
-
|
|
2110
|
+
it.each(['', `/* ${'license text '.repeat(150)} */\n`])(
|
|
2111
|
+
'strips third-party Flow following a license header (%#)',
|
|
2112
|
+
async (license) => {
|
|
2113
|
+
const testRoot = await mkdtemp(join(tmpdir(), 'vxrn-native-flow-'))
|
|
2114
|
+
// mirrors @react-native-masked-view/masked-view: a Flow `.js` component
|
|
2115
|
+
// outside the react-native / @react-native scopes, which is what reached
|
|
2116
|
+
// rolldown unstripped when its patch was skipped.
|
|
2117
|
+
const packageRoot = join(testRoot, 'node_modules/@flowy-scope/flowy-lib')
|
|
2118
|
+
await mkdir(packageRoot, { recursive: true })
|
|
2119
|
+
await writeFile(
|
|
2120
|
+
join(packageRoot, 'package.json'),
|
|
2121
|
+
JSON.stringify({ name: '@flowy-scope/flowy-lib', main: './js/Flowy.js' })
|
|
2122
|
+
)
|
|
2123
|
+
await mkdir(join(packageRoot, 'js'), { recursive: true })
|
|
2124
|
+
await writeFile(
|
|
2125
|
+
join(packageRoot, 'js/Flowy.js'),
|
|
2126
|
+
`${license}/**
|
|
1853
2127
|
* @flow
|
|
1854
2128
|
* @format
|
|
1855
2129
|
*/
|
|
@@ -1868,25 +2142,139 @@ export default class Flowy {
|
|
|
1868
2142
|
}
|
|
1869
2143
|
}
|
|
1870
2144
|
`
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
|
|
2145
|
+
)
|
|
2146
|
+
await writeFile(
|
|
2147
|
+
join(testRoot, 'entry.js'),
|
|
2148
|
+
`import Flowy from '@flowy-scope/flowy-lib'
|
|
1875
2149
|
globalThis.__vxrnFlowProbe = new Flowy().measure({ value: 41 }) + 1
|
|
1876
2150
|
`
|
|
1877
|
-
|
|
2151
|
+
)
|
|
1878
2152
|
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
2153
|
+
try {
|
|
2154
|
+
const result = await buildNativeBundle({
|
|
2155
|
+
root: testRoot,
|
|
2156
|
+
platform: 'ios',
|
|
2157
|
+
entryFile: 'entry.js',
|
|
2158
|
+
})
|
|
2159
|
+
const context = { clearTimeout, console, process: { env: {} }, setTimeout }
|
|
2160
|
+
runInNewContext(result.code, context)
|
|
2161
|
+
expect(Reflect.get(context, '__vxrnFlowProbe')).toBe(42)
|
|
2162
|
+
} finally {
|
|
2163
|
+
await rm(testRoot, { recursive: true, force: true })
|
|
2164
|
+
}
|
|
1890
2165
|
}
|
|
2166
|
+
)
|
|
2167
|
+
})
|
|
2168
|
+
|
|
2169
|
+
describe('native bundle source maps', () => {
|
|
2170
|
+
// the generated position of a marker in the emitted bundle is exactly what a
|
|
2171
|
+
// device reports in a stack frame, so resolving it is the same question as
|
|
2172
|
+
// symbolicating a crash.
|
|
2173
|
+
const generatedPositionOf = (code: string, marker: string) => {
|
|
2174
|
+
const index = code.indexOf(marker)
|
|
2175
|
+
expect(index).toBeGreaterThan(-1)
|
|
2176
|
+
const before = code.slice(0, index)
|
|
2177
|
+
const lastNewline = before.lastIndexOf('\n')
|
|
2178
|
+
return {
|
|
2179
|
+
line: (before.match(/\n/g)?.length ?? 0) + 1,
|
|
2180
|
+
column: index - lastNewline - 1,
|
|
2181
|
+
}
|
|
2182
|
+
}
|
|
2183
|
+
|
|
2184
|
+
it(
|
|
2185
|
+
'resolves a frame to the authored file and line across an app file and a package file',
|
|
2186
|
+
{ timeout: 60_000 },
|
|
2187
|
+
async () => {
|
|
2188
|
+
// realpath: macOS hands out /var/folders temp dirs that resolve to
|
|
2189
|
+
// /private/var, and the map records the resolved path.
|
|
2190
|
+
const testRoot = realpathSync(
|
|
2191
|
+
await mkdtemp(join(tmpdir(), 'vxrn-native-sourcemap-'))
|
|
2192
|
+
)
|
|
2193
|
+
const packageRoot = join(testRoot, 'node_modules/probe-package')
|
|
2194
|
+
await mkdir(packageRoot, { recursive: true })
|
|
2195
|
+
await writeFile(
|
|
2196
|
+
join(packageRoot, 'package.json'),
|
|
2197
|
+
JSON.stringify({ name: 'probe-package', main: 'index.js', type: 'module' })
|
|
2198
|
+
)
|
|
2199
|
+
// a blank line directly above an `export default` is the case that used to
|
|
2200
|
+
// disappear during post-processing and shift every frame below it.
|
|
2201
|
+
await writeFile(
|
|
2202
|
+
join(packageRoot, 'index.js'),
|
|
2203
|
+
[
|
|
2204
|
+
`function packageThrow() {`, // 1
|
|
2205
|
+
` throw new Error('__probe_package__')`, // 2
|
|
2206
|
+
`}`, // 3
|
|
2207
|
+
``, // 4
|
|
2208
|
+
`export default packageThrow`, // 5
|
|
2209
|
+
].join('\n')
|
|
2210
|
+
)
|
|
2211
|
+
await writeFile(
|
|
2212
|
+
join(testRoot, 'entry.js'),
|
|
2213
|
+
[
|
|
2214
|
+
`import packageThrow from 'probe-package'`, // 1
|
|
2215
|
+
``, // 2
|
|
2216
|
+
`function appThrow() {`, // 3
|
|
2217
|
+
` throw new Error('__probe_app__')`, // 4
|
|
2218
|
+
`}`, // 5
|
|
2219
|
+
``, // 6
|
|
2220
|
+
`globalThis.__probe = [appThrow, packageThrow]`, // 7
|
|
2221
|
+
].join('\n')
|
|
2222
|
+
)
|
|
2223
|
+
|
|
2224
|
+
try {
|
|
2225
|
+
const result = await buildNativeBundle({
|
|
2226
|
+
root: testRoot,
|
|
2227
|
+
platform: 'ios',
|
|
2228
|
+
entryFile: 'entry.js',
|
|
2229
|
+
dev: true,
|
|
2230
|
+
sourcemap: true,
|
|
2231
|
+
})
|
|
2232
|
+
expect(result.map).toBeTruthy()
|
|
2233
|
+
const traced = new TraceMap(JSON.parse(result.map!))
|
|
2234
|
+
|
|
2235
|
+
const app = originalPositionFor(
|
|
2236
|
+
traced,
|
|
2237
|
+
generatedPositionOf(result.code, '__probe_app__')
|
|
2238
|
+
)
|
|
2239
|
+
const pkg = originalPositionFor(
|
|
2240
|
+
traced,
|
|
2241
|
+
generatedPositionOf(result.code, '__probe_package__')
|
|
2242
|
+
)
|
|
2243
|
+
|
|
2244
|
+
// the authored line, which a post-processing pass that drops a line
|
|
2245
|
+
// silently moves
|
|
2246
|
+
expect(app.source?.endsWith('/entry.js')).toBe(true)
|
|
2247
|
+
expect(app.line).toBe(4)
|
|
2248
|
+
expect(pkg.source?.endsWith('/index.js')).toBe(true)
|
|
2249
|
+
expect(pkg.line).toBe(2)
|
|
2250
|
+
|
|
2251
|
+
// and the absolute path, which is all a crash reporter has to go on
|
|
2252
|
+
expect(app.source).toBe(normalizePath(join(testRoot, 'entry.js')))
|
|
2253
|
+
expect(pkg.source).toBe(normalizePath(join(packageRoot, 'index.js')))
|
|
2254
|
+
} finally {
|
|
2255
|
+
await rm(testRoot, { recursive: true, force: true })
|
|
2256
|
+
}
|
|
2257
|
+
}
|
|
2258
|
+
)
|
|
2259
|
+
})
|
|
2260
|
+
|
|
2261
|
+
describe('postProcessNativeBundle', () => {
|
|
2262
|
+
// rolldown emits the map before these passes run, so a line they remove moves
|
|
2263
|
+
// every frame below it away from the source it maps to.
|
|
2264
|
+
const lines = (code: string) => code.split('\n').length
|
|
2265
|
+
|
|
2266
|
+
it('keeps the blank line above an export default', () => {
|
|
2267
|
+
const input = ['var a = 1;', '', 'export default a;', 'var b = 2;', ''].join('\n')
|
|
2268
|
+
const out = postProcessNativeBundle(input)
|
|
2269
|
+
expect(lines(out)).toBe(lines(input))
|
|
2270
|
+
expect(out.split('\n')[2]).toBe('a;')
|
|
2271
|
+
})
|
|
2272
|
+
|
|
2273
|
+
it('keeps the lines of a multi-line export statement', () => {
|
|
2274
|
+
const input = ['var a = 1;', 'export {', ' a', '};', 'var b = 2;', ''].join('\n')
|
|
2275
|
+
const out = postProcessNativeBundle(input)
|
|
2276
|
+
expect(lines(out)).toBe(lines(input))
|
|
2277
|
+
expect(out).not.toContain('export {')
|
|
2278
|
+
expect(out.split('\n')[4]).toBe('var b = 2;')
|
|
1891
2279
|
})
|
|
1892
2280
|
})
|