vxrn 1.25.16 → 1.26.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. package/dist/index.js +2 -0
  2. package/dist/index.js.map +1 -1
  3. package/dist/index.mjs +2 -0
  4. package/dist/index.mjs.map +1 -1
  5. package/dist/index.native.js +2 -0
  6. package/dist/index.native.js.map +1 -1
  7. package/dist/patches/builtInDepPatches.mjs +21 -0
  8. package/dist/patches/builtInDepPatches.mjs.map +1 -1
  9. package/dist/patches/builtInDepPatches.native.js +21 -0
  10. package/dist/patches/builtInDepPatches.native.js.map +1 -1
  11. package/dist/plugins/reactNativeDevServer.mjs +5 -0
  12. package/dist/plugins/reactNativeDevServer.mjs.map +1 -1
  13. package/dist/plugins/reactNativeDevServer.native.js +5 -0
  14. package/dist/plugins/reactNativeDevServer.native.js.map +1 -1
  15. package/dist/plugins/rnCodegenPlugin.mjs +16 -0
  16. package/dist/plugins/rnCodegenPlugin.mjs.map +1 -0
  17. package/dist/plugins/rnCodegenPlugin.native.js +16 -0
  18. package/dist/plugins/rnCodegenPlugin.native.js.map +1 -0
  19. package/dist/plugins/rnCodegenPlugin.test.mjs +239 -0
  20. package/dist/plugins/rnCodegenPlugin.test.mjs.map +1 -0
  21. package/dist/plugins/rnCodegenPlugin.test.native.js +242 -0
  22. package/dist/plugins/rnCodegenPlugin.test.native.js.map +1 -0
  23. package/dist/plugins/workletImportsPlugin.mjs +160 -0
  24. package/dist/plugins/workletImportsPlugin.mjs.map +1 -0
  25. package/dist/plugins/workletImportsPlugin.native.js +230 -0
  26. package/dist/plugins/workletImportsPlugin.native.js.map +1 -0
  27. package/dist/utils/createNativeDevEngine.mjs +198 -92
  28. package/dist/utils/createNativeDevEngine.mjs.map +1 -1
  29. package/dist/utils/createNativeDevEngine.native.js +252 -99
  30. package/dist/utils/createNativeDevEngine.native.js.map +1 -1
  31. package/dist/utils/createNativeDevEngine.test.mjs +775 -4
  32. package/dist/utils/createNativeDevEngine.test.mjs.map +1 -1
  33. package/dist/utils/createNativeDevEngine.test.native.js +836 -4
  34. package/dist/utils/createNativeDevEngine.test.native.js.map +1 -1
  35. package/dist/utils/patches.mjs +12 -5
  36. package/dist/utils/patches.mjs.map +1 -1
  37. package/dist/utils/patches.native.js +13 -5
  38. package/dist/utils/patches.native.js.map +1 -1
  39. package/dist/utils/patches.test.mjs +28 -0
  40. package/dist/utils/patches.test.mjs.map +1 -1
  41. package/dist/utils/patches.test.native.js +47 -0
  42. package/dist/utils/patches.test.native.js.map +1 -1
  43. package/dist/utils/scanDepsToOptimize.mjs +5 -1
  44. package/dist/utils/scanDepsToOptimize.mjs.map +1 -1
  45. package/dist/utils/scanDepsToOptimize.native.js +5 -1
  46. package/dist/utils/scanDepsToOptimize.native.js.map +1 -1
  47. package/package.json +19 -12
  48. package/src/index.ts +2 -0
  49. package/src/patches/builtInDepPatches.ts +25 -0
  50. package/src/plugins/reactNativeDevServer.ts +12 -0
  51. package/src/plugins/rnCodegenPlugin.test.ts +313 -0
  52. package/src/plugins/rnCodegenPlugin.ts +20 -0
  53. package/src/plugins/workletImportsPlugin.ts +226 -0
  54. package/src/utils/createNativeDevEngine.test.ts +877 -3
  55. package/src/utils/createNativeDevEngine.ts +259 -107
  56. package/src/utils/patches.test.ts +33 -0
  57. package/src/utils/patches.ts +17 -6
  58. package/src/utils/scanDepsToOptimize.ts +9 -0
  59. package/types/index.d.ts +2 -0
  60. package/types/index.d.ts.map +1 -1
  61. package/types/patches/builtInDepPatches.d.ts.map +1 -1
  62. package/types/plugins/reactNativeDevServer.d.ts.map +1 -1
  63. package/types/plugins/rnCodegenPlugin.d.ts +6 -0
  64. package/types/plugins/rnCodegenPlugin.d.ts.map +1 -0
  65. package/types/plugins/rnCodegenPlugin.test.d.ts +2 -0
  66. package/types/plugins/rnCodegenPlugin.test.d.ts.map +1 -0
  67. package/types/plugins/workletImportsPlugin.d.ts +8 -0
  68. package/types/plugins/workletImportsPlugin.d.ts.map +1 -0
  69. package/types/utils/createNativeDevEngine.d.ts +13 -0
  70. package/types/utils/createNativeDevEngine.d.ts.map +1 -1
  71. package/types/utils/patches.d.ts.map +1 -1
  72. package/types/utils/scanDepsToOptimize.d.ts.map +1 -1
@@ -2,14 +2,16 @@ import { existsSync } 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
- import { runInNewContext } from 'node:vm'
5
+ import { createContext, runInContext, runInNewContext } from 'node:vm'
6
6
  import { rolldown, type RolldownOutput } from 'rolldown'
7
7
  import { dev } from 'rolldown/experimental'
8
- import { describe, expect, it } from 'vitest'
8
+ import { describe, expect, it, vi } from 'vitest'
9
9
  import { getNativePrelude } from '../runtime/native-prelude'
10
+ import { workletImportsPlugin } from '../plugins/workletImportsPlugin'
10
11
  import {
11
12
  buildNativeBundle,
12
13
  createNativeDevAssetRegistry,
14
+ createNativeDevEngine,
13
15
  getNativeAssetData,
14
16
  getHermesSWCIncludes,
15
17
  getHmrRuntimeSource,
@@ -29,13 +31,225 @@ export const transformProbe = () => {
29
31
  }
30
32
  `
31
33
 
34
+ // node guards lowering semantics; the async-default regression itself needs
35
+ // hermes, where the original arrow resolves its await to undefined.
36
+ describe.each(['rolldown-dev', 'rolldown-build', 'metro-module', 'metro-script'])(
37
+ 'native async through %s',
38
+ (pipeline) => {
39
+ it('preserves defaults, lexical bindings, bigint, rejection, and iterator cleanup', async () => {
40
+ const id = '/project/native-async.js'
41
+ const source = `
42
+ globalThis.result = (function(parentArgument) {
43
+ const events = [];
44
+ const run = async(value = 2n, fail = false) => {
45
+ try {
46
+ const awaited = await (fail ? Promise.reject(new Error('rejected')) : Promise.resolve(3n));
47
+ events.push('after-await');
48
+ return [String(value + awaited), this.base, arguments[0]];
49
+ } catch (error) { return error.message; }
50
+ finally { events.push('finally'); }
51
+ };
52
+ const badDefault = async(value = (() => { throw new Error('default'); })()) => value;
53
+ let synchronousThrow = false;
54
+ let rejectedDefault;
55
+ try { rejectedDefault = badDefault().catch(error => error.message); }
56
+ catch { synchronousThrow = true; }
57
+ async function* values() {
58
+ try { yield 1n; yield 2n; }
59
+ finally { events.push('closed'); }
60
+ }
61
+ const first = async() => { for await (const value of values()) return String(value); };
62
+ return Promise.all([run(), run(4n), run(undefined, true), rejectedDefault, first()])
63
+ .then(values => ({ values, events, synchronousThrow }));
64
+ }).call({ base: 7 }, 'lexical');
65
+ `
66
+ let output: any
67
+ if (pipeline.startsWith('metro')) {
68
+ const { transform } = await import('@vxrn/vite-plugin-metro/metroNativeWorker')
69
+ const result = await transform({}, '/project', id, Buffer.from(source), {
70
+ dev: false,
71
+ platform: 'ios',
72
+ type: pipeline === 'metro-script' ? 'script' : 'module',
73
+ })
74
+ const data = result.output[0].data
75
+ const { fromRawMappings } = await import('metro-source-map')
76
+ output = {
77
+ code: data.code,
78
+ map: (fromRawMappings as any)([
79
+ { code: data.code, path: id, source, map: data.map },
80
+ ]).toMap(),
81
+ }
82
+ } else {
83
+ const plugin = hermesCompatSWCPlugin(pipeline === 'rolldown-dev', true)
84
+ output = await Reflect.apply(plugin.transform as Function, undefined, [
85
+ source,
86
+ id,
87
+ ])
88
+ }
89
+ const context: any = {}
90
+ context.__d = (factory: any) => {
91
+ const module = { exports: {} }
92
+ factory(
93
+ context,
94
+ (name: string) => {
95
+ throw new Error(`unexpected dependency ${name}`)
96
+ },
97
+ () => {},
98
+ () => {},
99
+ module,
100
+ module.exports,
101
+ []
102
+ )
103
+ }
104
+ runInNewContext(output.code, context)
105
+ const result = await context.result
106
+ expect(result.values).toEqual([
107
+ ['5', 7, 'lexical'],
108
+ ['7', 7, 'lexical'],
109
+ 'rejected',
110
+ 'default',
111
+ '1',
112
+ ])
113
+ expect(result.synchronousThrow).toBe(false)
114
+ expect(result.events.filter((value: string) => value === 'finally')).toHaveLength(3)
115
+ expect(result.events.filter((value: string) => value === 'closed')).toHaveLength(1)
116
+ const { originalPositionFor, TraceMap } = await import('@jridgewell/trace-mapping')
117
+ const lines = output.code.split('\n')
118
+ const line = lines.findIndex((value: string) => value.includes('after-await'))
119
+ const position = originalPositionFor(new TraceMap(output.map), {
120
+ line: line + 1,
121
+ column: lines[line].indexOf('events.push'),
122
+ })
123
+ expect(position.source).toBe(id)
124
+ expect(position.line).toBe(
125
+ source.split('\n').findIndex((value) => value.includes('after-await')) + 1
126
+ )
127
+ })
128
+ }
129
+ )
130
+
131
+ describe.each(['shared', 'rolldown', 'metro'])('React Compiler worklets through %s', (pipeline) => {
132
+ it.each([
133
+ ['block', 'useDerivedValue(() => { return value + 8 }, [value])', 18],
134
+ ['expression', 'useDerivedValue(() => value + 8, [value])', 18],
135
+ ['object', 'useDerivedValue(() => ({ lift: value + 8 }), [value])', { lift: 18 }],
136
+ ['nested', 'useDerivedValue(() => runOnUI(() => value + 8), [value])', 18],
137
+ ['gesture', 'useDerivedValue(() => Gesture.Pan().onUpdate(() => value + 8), [value])', 18],
138
+ ])('preserves %s callbacks on the UI runtime and memoizes stable inputs', async (_name, callback, expected) => {
139
+ const compiler = await import('@vxrn/compiler')
140
+ const { parseSync } = await import('oxc-parser')
141
+ const { default: MagicString } = await import('magic-string')
142
+ const { TraceMap, originalPositionFor } = await import('@jridgewell/trace-mapping')
143
+ const projectRoot = await createWorkletsProject(false)
144
+ const sourceMaps = process.env.VXRN_ENABLE_SOURCE_MAP
145
+ process.env.VXRN_ENABLE_SOURCE_MAP = '1'
146
+ compiler.configureVXRNCompilerPlugin({
147
+ enableCompiler: true,
148
+ enableReanimated: true,
149
+ enableNativeWorklets: true,
150
+ })
151
+ try {
152
+ const id = join(projectRoot, 'useProbe.ts')
153
+ const source = `export function useProbe(value) {\n return ${callback};\n}\n`
154
+ await writeFile(id, source)
155
+ let result: any
156
+ if (pipeline === 'shared') {
157
+ const plugins = await compiler.createVXRNCompilerPlugin()
158
+ const plugin = plugins.find((p: any) => p.name === 'one:compiler') as any
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 },
169
+ })
170
+ const data = output.output[0].data
171
+ const msm = await import('metro-source-map')
172
+ result = {
173
+ code: data.code,
174
+ map: (msm.fromRawMappings as any)([{ code: data.code, path: id, source, map: data.map }]).toMap(),
175
+ }
176
+ } else {
177
+ const plugin = vxrnCompilerPlugin('ios', false, projectRoot, true)
178
+ result = await Reflect.apply(plugin.transform as Function, undefined, [source, id])
179
+ }
180
+ const runnable = new MagicString(result.code)
181
+ for (const node of parseSync('probe.js', result.code).program.body) {
182
+ if (node.type === 'ImportDeclaration') runnable.remove(node.start, node.end)
183
+ if (node.type === 'ExportNamedDeclaration' && node.declaration) {
184
+ runnable.remove(node.start, node.declaration.start)
185
+ }
186
+ }
187
+ let cache: any[] | undefined
188
+ let cacheCalls = 0
189
+ const memo = (size: number) => {
190
+ cacheCalls++
191
+ return cache ??= Array(size).fill(Symbol.for('react.memo_cache_sentinel'))
192
+ }
193
+ const module = { exports: {} as any }
194
+ const probe = new Function('_c', 'useDerivedValue', 'runOnUI', 'Gesture', '__d', 'module',
195
+ `${runnable}\nreturn ${pipeline === 'metro' ? 'module.exports.useProbe' : 'useProbe'}`
196
+ )(
197
+ memo,
198
+ (fn: any) => fn,
199
+ (fn: any) => fn,
200
+ { Pan: () => ({ onUpdate: (fn: any) => fn }) },
201
+ (factory: any) => factory(globalThis, () => ({ c: memo }), () => {}, () => {}, module, module.exports, []),
202
+ module
203
+ )
204
+ const first = probe(10)
205
+ expect(first.__initData?.code).toBeTypeOf('string')
206
+ for (const onUI of [false, true]) {
207
+ let fn = first
208
+ let value: any
209
+ for (let depth = 0; depth < 2; depth++) {
210
+ expect(fn.__initData?.code).toBeTypeOf('string')
211
+ value = onUI
212
+ ? new Function('runOnUI', `return (${fn.__initData.code})`)((fn: any) => fn)
213
+ .call({ __closure: fn.__closure })
214
+ : fn()
215
+ if (typeof value !== 'function') break
216
+ fn = value
217
+ }
218
+ expect(value).toEqual(expected)
219
+ }
220
+ expect(probe(10)).toBe(first)
221
+ expect(cacheCalls).toBe(2)
222
+ expect(probe(20)).not.toBe(first)
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
+ })
245
+
32
246
  async function createWorkletsProject(throwOnTransform = true) {
33
247
  const testRoot = await mkdtemp(join(tmpdir(), 'vxrn-native-transform-failure-'))
34
248
  const packageRoot = join(testRoot, 'node_modules/react-native-worklets')
35
249
  await mkdir(packageRoot, { recursive: true })
36
250
  await writeFile(
37
251
  join(packageRoot, 'package.json'),
38
- JSON.stringify({ name: 'react-native-worklets' })
252
+ JSON.stringify({ name: 'react-native-worklets', version: '0.10.1' })
39
253
  )
40
254
  await writeFile(
41
255
  join(packageRoot, 'plugin.js'),
@@ -76,7 +290,283 @@ describe('native prelude', () => {
76
290
  })
77
291
  })
78
292
 
293
+ describe('native pure-function worklet imports', () => {
294
+ it('keeps live module versions isolated across a transitive HMR update', async () => {
295
+ const root = await createWorkletsProject(false)
296
+ const packageRoot = join(root, 'node_modules/pure-math')
297
+ await mkdir(packageRoot, { recursive: true })
298
+ await writeFile(
299
+ join(packageRoot, 'package.json'),
300
+ JSON.stringify({ type: 'module', exports: './index.js' })
301
+ )
302
+ const dependency = join(packageRoot, 'amount.ios.js')
303
+ await writeFile(dependency, 'export const amount = 2')
304
+ await writeFile(join(packageRoot, 'amount.android.js'), 'export const amount = 99')
305
+ await writeFile(
306
+ join(packageRoot, 'index.js'),
307
+ `
308
+ import { amount } from './amount'
309
+ const instance = globalThis.moduleInitializations = (globalThis.moduleInitializations || 0) + 1
310
+ export default function calculate(value) { return value + amount }
311
+ export function reads() { return instance }
312
+ `
313
+ )
314
+ await writeFile(
315
+ join(root, 'entry.mjs'),
316
+ `
317
+ import calculate, { reads } from 'pure-math'
318
+ globalThis.calculate = calculate
319
+ globalThis.reads = reads
320
+ if (import.meta.hot) import.meta.hot.accept(() => {})
321
+ `
322
+ )
323
+ const compiler = await import('@vxrn/compiler')
324
+ compiler.configureVXRNCompilerPlugin({
325
+ enableReanimated: true,
326
+ enableNativeWorklets: true,
327
+ })
328
+ let resolveUpdate!: (value: any) => void
329
+ const updated = new Promise<any>((resolve) => {
330
+ resolveUpdate = resolve
331
+ })
332
+ const native = await createNativeDevEngine({
333
+ root,
334
+ port: 0,
335
+ platform: 'ios',
336
+ plugins: [
337
+ workletImportsPlugin({ 'pure-math': ['default', 'reads'] }),
338
+ {
339
+ name: 'worklet-import-fixture',
340
+ transform(_code, id) {
341
+ if (id.endsWith('/__virtual-native-entry.tsx')) return `import './entry.mjs'`
342
+ },
343
+ },
344
+ ],
345
+ onHmrUpdate: resolveUpdate,
346
+ })
347
+ try {
348
+ const initial = await native.getBundle()
349
+ const rn: any = { console, setTimeout, clearTimeout }
350
+ runInNewContext(initial.code, rn)
351
+ expect(rn.calculate(10)).toBe(12)
352
+ expect(rn.reads()).toBe(1)
353
+ const ui: any = createContext({})
354
+ const compiled = new Map<number, Function>()
355
+ const reconstruct = (worklet: any): Function => {
356
+ if (!worklet.__initData || !worklet.__workletHash)
357
+ throw new Error('remote function')
358
+ let fn = compiled.get(worklet.__workletHash)
359
+ if (!fn) {
360
+ fn = runInContext(`(${worklet.__initData.code})`, ui)
361
+ compiled.set(worklet.__workletHash, fn!)
362
+ }
363
+ const closure = Object.fromEntries(
364
+ Object.entries(worklet.__closure).map(([name, value]) => [
365
+ name,
366
+ typeof value === 'function' ? reconstruct(value) : value,
367
+ ])
368
+ )
369
+ return fn!.bind({ __closure: closure })
370
+ }
371
+ expect(() => reconstruct((value: number) => value + 2)).toThrow('remote function')
372
+ const old = reconstruct(rn.calculate)
373
+ const oldReads = reconstruct(rn.reads)
374
+ expect(old(10)).toBe(12)
375
+ expect(reconstruct(rn.calculate)(11)).toBe(13)
376
+ expect(oldReads()).toBe(1)
377
+ expect(rn.reads()).toBe(1)
378
+
379
+ const runtime = rn.__rolldown_runtime__
380
+ await native.engine.registerClient(runtime.clientId)
381
+ await writeFile(dependency, 'export const amount = 3')
382
+ const update = await updated
383
+ expect(update.type).toBe('hmr:update')
384
+ expect(runtime.applyHmrUpdate(update.code, update.changedIds, update.seq)).toBe(
385
+ true
386
+ )
387
+ expect(rn.calculate(10)).toBe(13)
388
+ const next = reconstruct(rn.calculate)
389
+ expect([old(10), next(10), old(10), next(10)]).toEqual([12, 13, 12, 13])
390
+ const nextReads = reconstruct(rn.reads)
391
+ expect([oldReads(), nextReads(), oldReads(), nextReads()]).toEqual([1, 2, 1, 2])
392
+ expect(ui.moduleInitializations).toBe(2)
393
+ expect(rn.reads()).toBe(2)
394
+
395
+ await writeFile(
396
+ join(root, 'production.mjs'),
397
+ `import calculate, { reads } from 'pure-math'; globalThis.calculate = calculate; globalThis.reads = reads`
398
+ )
399
+ const production = await buildNativeBundle({
400
+ root,
401
+ platform: 'android',
402
+ entryFile: 'production.mjs',
403
+ plugins: [workletImportsPlugin({ 'pure-math': ['default', 'reads'] })],
404
+ })
405
+ const productionRN: any = { console }
406
+ runInNewContext(production.code, productionRN)
407
+ expect(productionRN.calculate(10)).toBe(109)
408
+ expect(reconstruct(productionRN.calculate)(10)).toBe(109)
409
+ expect(reconstruct(productionRN.reads)()).toBe(3)
410
+ expect(productionRN.reads()).toBe(1)
411
+
412
+ const sharedPlugin = workletImportsPlugin({ 'pure-math': ['default', 'reads'] })
413
+ const parallel = await Promise.all(
414
+ (['ios', 'android'] as const).map(async (platform) => {
415
+ const output = await buildNativeBundle({
416
+ root,
417
+ platform,
418
+ entryFile: 'production.mjs',
419
+ plugins: [sharedPlugin],
420
+ })
421
+ const context: any = { console }
422
+ runInNewContext(output.code, context)
423
+ return context.calculate(10)
424
+ })
425
+ )
426
+ expect(parallel).toEqual([13, 109])
427
+ } finally {
428
+ await native.close()
429
+ compiler.configureVXRNCompilerPlugin({
430
+ enableReanimated: false,
431
+ enableNativeWorklets: false,
432
+ })
433
+ await rm(root, { recursive: true, force: true })
434
+ }
435
+ })
436
+
437
+ it.each([
438
+ [
439
+ 'unselected',
440
+ 'export default function value() { return 1 }; export const other = 2',
441
+ `import { other } from 'pure-math'; globalThis.value = other`,
442
+ /unselected export other/,
443
+ ],
444
+ [
445
+ 'missing',
446
+ 'export const other = 2',
447
+ `import value from 'pure-math'; globalThis.value = value`,
448
+ /default.*not exported|MISSING_EXPORT/,
449
+ ],
450
+ [
451
+ 'builtin',
452
+ `import fs from 'node:fs'; export default function value() { return fs.readFileSync('x') }`,
453
+ `import value from 'pure-math'; globalThis.value = value`,
454
+ /runtime dependency node:fs/,
455
+ ],
456
+ [
457
+ 'global',
458
+ 'export default function value() { return process.pid }',
459
+ `import value from 'pure-math'; globalThis.value = value`,
460
+ /unsupported runtime globals: process/,
461
+ ],
462
+ [
463
+ 'dynamic',
464
+ `export default function value() { return import('./other.js') }`,
465
+ `import value from 'pure-math'; globalThis.value = value`,
466
+ /dynamic imports are unsupported/,
467
+ ],
468
+ ] as const)(
469
+ 'rejects %s through the native engine',
470
+ async (_name, source, entry, expected) => {
471
+ const root = await createWorkletsProject(false)
472
+ const packageRoot = join(root, 'node_modules/pure-math')
473
+ await mkdir(packageRoot, { recursive: true })
474
+ await writeFile(
475
+ join(packageRoot, 'package.json'),
476
+ JSON.stringify({ type: 'module', exports: './index.js' })
477
+ )
478
+ await writeFile(join(packageRoot, 'index.js'), source)
479
+ await writeFile(join(root, 'entry.mjs'), entry)
480
+ const compiler = await import('@vxrn/compiler')
481
+ compiler.configureVXRNCompilerPlugin({
482
+ enableReanimated: true,
483
+ enableNativeWorklets: true,
484
+ })
485
+ const native = await createNativeDevEngine({
486
+ root,
487
+ port: 0,
488
+ platform: 'ios',
489
+ plugins: [
490
+ workletImportsPlugin({ 'pure-math': ['default'] }),
491
+ {
492
+ name: 'worklet-import-negative-fixture',
493
+ transform(_code, id) {
494
+ if (id.endsWith('/__virtual-native-entry.tsx'))
495
+ return `import './entry.mjs'`
496
+ },
497
+ },
498
+ ],
499
+ })
500
+ try {
501
+ await expect(native.getBundle()).rejects.toThrow(expected)
502
+ } finally {
503
+ await native.close()
504
+ compiler.configureVXRNCompilerPlugin({
505
+ enableReanimated: false,
506
+ enableNativeWorklets: false,
507
+ })
508
+ await rm(root, { recursive: true, force: true })
509
+ }
510
+ }
511
+ )
512
+ })
513
+
79
514
  describe('native Rolldown HMR runtime', () => {
515
+ it.each([true, false])('keeps CommonJS default components callable through the native HMR wire path (Babel=%s)', async (babel) => {
516
+ const root = await mkdtemp(join(tmpdir(), 'vxrn-native-hmr-interop-'))
517
+ const entry = join(root, 'entry.mjs')
518
+ const packageRoot = join(root, 'node_modules/component')
519
+ await mkdir(packageRoot, { recursive: true })
520
+ await writeFile(
521
+ join(packageRoot, 'package.json'),
522
+ JSON.stringify({ main: './component(view).js' })
523
+ )
524
+ await writeFile(
525
+ join(packageRoot, 'component(view).js'),
526
+ babel
527
+ ? `Object.defineProperty(exports, '__esModule', { value: true }); exports.default = function Component() { return 42 }`
528
+ : `module.exports = function Component() { return 42 }`
529
+ )
530
+ const source = (version: number) => `
531
+ import Component from 'component'
532
+ globalThis.renderComponent = Component
533
+ export const version = ${version}
534
+ if (import.meta.hot) import.meta.hot.accept(() => {})
535
+ `
536
+ await writeFile(entry, source(1))
537
+ let resolveUpdate!: (update: any) => void
538
+ const updateReceived = new Promise<any>((resolve) => { resolveUpdate = resolve })
539
+ const native = await createNativeDevEngine({
540
+ root,
541
+ port: 0,
542
+ platform: 'ios',
543
+ plugins: [{
544
+ name: 'native-hmr-fixture-entry',
545
+ transform(_code, id) {
546
+ if (id.endsWith('/__virtual-native-entry.tsx')) return `import './entry.mjs'`
547
+ },
548
+ }],
549
+ onHmrUpdate: resolveUpdate,
550
+ })
551
+ try {
552
+ const initial = await native.getBundle()
553
+ const context: any = { console, setTimeout, clearTimeout }
554
+ runInNewContext(initial.code, context)
555
+ expect(context.renderComponent()).toBe(42)
556
+ const runtime = context.__rolldown_runtime__
557
+ await native.engine.registerClient(runtime.clientId)
558
+ await writeFile(entry, source(2))
559
+ const update = await updateReceived
560
+ expect(update.type).toBe('hmr:update')
561
+ expect(update.clientId).toBe(runtime.clientId)
562
+ expect(runtime.applyHmrUpdate(update.code, update.changedIds, update.seq)).toBe(true)
563
+ expect(context.renderComponent()).toBe(42)
564
+ } finally {
565
+ await native.close()
566
+ await rm(root, { recursive: true, force: true })
567
+ }
568
+ })
569
+
80
570
  it(
81
571
  'registers a Rolldown 1.2 client and applies a self-accepted patch',
82
572
  { timeout: 30_000 },
@@ -493,6 +983,35 @@ describe('getHermesSWCIncludes', () => {
493
983
  expect(result.code).not.toContain('async function *')
494
984
  })
495
985
 
986
+ it('downlevels private fields, public class fields, and static blocks for Hermes', async () => {
987
+ const plugin = hermesCompatSWCPlugin(true)
988
+ if (typeof plugin.transform !== 'function') {
989
+ throw new Error('Hermes compatibility transform hook is not callable')
990
+ }
991
+
992
+ const result = await Reflect.apply(plugin.transform, undefined, [
993
+ `
994
+ class TransformProbe {
995
+ #privateField = 1
996
+ publicField = 2
997
+ static {
998
+ TransformProbe.initialized = true
999
+ }
1000
+
1001
+ getSecret() {
1002
+ return this.#privateField
1003
+ }
1004
+ }
1005
+ `,
1006
+ '/project/TransformProbe.ts',
1007
+ ])
1008
+
1009
+ expect(result.code).not.toContain('#privateField')
1010
+ expect(result.code).not.toMatch(/^\s*publicField\s*=/m)
1011
+ expect(result.code).not.toContain('static {')
1012
+ expect(result.code).toContain('this.publicField = 2')
1013
+ })
1014
+
496
1015
  it('preserves per-iteration bindings used by lazy method getters', async () => {
497
1016
  const plugin = hermesCompatSWCPlugin(true)
498
1017
  if (typeof plugin.transform !== 'function') {
@@ -661,6 +1180,9 @@ describe('native required transform failures', () => {
661
1180
  async (dev) => {
662
1181
  const testRoot = await createWorkletsProject()
663
1182
  const compiler = await import('@vxrn/compiler')
1183
+ const spy = vi
1184
+ .spyOn(compiler, 'transformWorklets')
1185
+ .mockRejectedValue(new Error('NATIVE_TRANSFORM_NEGATIVE_CONTROL'))
664
1186
  compiler.configureVXRNCompilerPlugin({ enableReanimated: true })
665
1187
 
666
1188
  try {
@@ -676,6 +1198,7 @@ describe('native required transform failures', () => {
676
1198
  ])
677
1199
  ).rejects.toThrow('NATIVE_TRANSFORM_NEGATIVE_CONTROL')
678
1200
  } finally {
1201
+ spy.mockRestore()
679
1202
  compiler.configureVXRNCompilerPlugin({ enableReanimated: false })
680
1203
  await rm(testRoot, { recursive: true, force: true })
681
1204
  }
@@ -685,6 +1208,9 @@ describe('native required transform failures', () => {
685
1208
  it('returns a dev build error and no output when the required compiler transform fails', async () => {
686
1209
  const testRoot = await createWorkletsProject()
687
1210
  const compiler = await import('@vxrn/compiler')
1211
+ const spy = vi
1212
+ .spyOn(compiler, 'transformWorklets')
1213
+ .mockRejectedValue(new Error('NATIVE_TRANSFORM_NEGATIVE_CONTROL'))
688
1214
  compiler.configureVXRNCompilerPlugin({ enableReanimated: true })
689
1215
  let resolveOutput!: (output: unknown) => void
690
1216
  const output = new Promise<unknown>((resolve) => {
@@ -706,6 +1232,7 @@ describe('native required transform failures', () => {
706
1232
  expect(result).toBeInstanceOf(Error)
707
1233
  expect(String(result)).toContain('NATIVE_TRANSFORM_NEGATIVE_CONTROL')
708
1234
  } finally {
1235
+ spy.mockRestore()
709
1236
  await engine.close()
710
1237
  compiler.configureVXRNCompilerPlugin({ enableReanimated: false })
711
1238
  await rm(testRoot, { recursive: true, force: true })
@@ -715,6 +1242,9 @@ describe('native required transform failures', () => {
715
1242
  it('emits no production bundle when the required compiler transform fails', async () => {
716
1243
  const testRoot = await createWorkletsProject()
717
1244
  const compiler = await import('@vxrn/compiler')
1245
+ const spy = vi
1246
+ .spyOn(compiler, 'transformWorklets')
1247
+ .mockRejectedValue(new Error('NATIVE_TRANSFORM_NEGATIVE_CONTROL'))
718
1248
  compiler.configureVXRNCompilerPlugin({ enableReanimated: true })
719
1249
 
720
1250
  try {
@@ -726,6 +1256,7 @@ describe('native required transform failures', () => {
726
1256
  })
727
1257
  ).rejects.toThrow('NATIVE_TRANSFORM_NEGATIVE_CONTROL')
728
1258
  } finally {
1259
+ spy.mockRestore()
729
1260
  compiler.configureVXRNCompilerPlugin({ enableReanimated: false })
730
1261
  await rm(testRoot, { recursive: true, force: true })
731
1262
  }
@@ -775,6 +1306,292 @@ describe('native required transform failures', () => {
775
1306
  await rm(testRoot, { recursive: true, force: true })
776
1307
  }
777
1308
  })
1309
+
1310
+ it('composes source maps across React Compiler, Fast Refresh, and module wrapper back to original source lines', async () => {
1311
+ const { TraceMap, originalPositionFor } = await import('@jridgewell/trace-mapping')
1312
+ const compiler = await import('@vxrn/compiler')
1313
+ compiler.configureVXRNCompilerPlugin({ enableCompiler: true })
1314
+
1315
+ try {
1316
+ const plugin = vxrnCompilerPlugin('ios', true, '/project', true)
1317
+ if (typeof plugin.transform !== 'function') {
1318
+ throw new Error('transform hook not callable')
1319
+ }
1320
+
1321
+ const inputCode = `import { useState } from 'react'
1322
+
1323
+ export function Counter() {
1324
+ const [count, setCount] = useState(0)
1325
+ return <button onClick={() => setCount(count + 1)}>{count}</button>
1326
+ }
1327
+ `
1328
+ const id = '/project/Counter.tsx'
1329
+ const result = await Reflect.apply(plugin.transform, undefined, [inputCode, id])
1330
+
1331
+ expect(result).toBeDefined()
1332
+ expect(result.map).toBeDefined()
1333
+
1334
+ const tracer = new TraceMap(result.map)
1335
+ const lines = result.code.split('\n')
1336
+ const counterLineIndex = lines.findIndex((l: string) =>
1337
+ l.includes('function Counter')
1338
+ )
1339
+ expect(counterLineIndex).toBeGreaterThan(0)
1340
+
1341
+ const pos = originalPositionFor(tracer, {
1342
+ line: counterLineIndex + 1,
1343
+ column: 16,
1344
+ })
1345
+
1346
+ expect(pos.source).toBe(id)
1347
+ expect(pos.line).toBe(3)
1348
+ } finally {
1349
+ compiler.configureVXRNCompilerPlugin({ enableCompiler: false })
1350
+ }
1351
+ })
1352
+
1353
+ it('composes source maps accurately across mixed path (React Compiler + Babel + Fast Refresh + module wrapper)', async () => {
1354
+ const { TraceMap, originalPositionFor } = await import('@jridgewell/trace-mapping')
1355
+ const compiler = await import('@vxrn/compiler')
1356
+ const origEnv = process.env.VXRN_USE_BABEL_FOR_GENERATORS
1357
+ process.env.VXRN_USE_BABEL_FOR_GENERATORS = '1'
1358
+ compiler.configureVXRNCompilerPlugin({
1359
+ enableCompiler: true,
1360
+ enableReanimated: false,
1361
+ })
1362
+
1363
+ try {
1364
+ const plugin = vxrnCompilerPlugin('ios', true, '/project', true)
1365
+ if (typeof plugin.transform !== 'function') {
1366
+ throw new Error('transform hook not callable')
1367
+ }
1368
+
1369
+ const inputCode = `export function Counter() { return <View />; }
1370
+
1371
+
1372
+
1373
+
1374
+
1375
+
1376
+
1377
+ export async function* probe() { throw new Error("MARKER"); }
1378
+ `
1379
+ const id = '/project/Counter.tsx'
1380
+ const result = await Reflect.apply(plugin.transform, undefined, [inputCode, id])
1381
+
1382
+ expect(result).toBeDefined()
1383
+ expect(result.map).toBeDefined()
1384
+ // Assert that Babel's generator downleveling actually executed
1385
+ expect(
1386
+ result.code.includes('_wrapAsyncGenerator') ||
1387
+ result.code.includes('regeneratorRuntime')
1388
+ ).toBe(true)
1389
+
1390
+ const tracer = new TraceMap(result.map)
1391
+ const lines = result.code.split('\n')
1392
+ const markerLineIndex = lines.findIndex((l: string) => l.includes('MARKER'))
1393
+ expect(markerLineIndex).toBeGreaterThan(0)
1394
+ const markerCol = lines[markerLineIndex].indexOf('throw')
1395
+
1396
+ const pos = originalPositionFor(tracer, {
1397
+ line: markerLineIndex + 1,
1398
+ column: markerCol,
1399
+ })
1400
+
1401
+ expect(pos.source).toBe(id)
1402
+ expect(pos.line).toBe(9)
1403
+ expect(pos.column).toBe(33)
1404
+
1405
+ // Negative control: verify that remapping through compilerOut.map a second time
1406
+ // (the prior double-composition bug) breaks the trace and returns null source/line.
1407
+ const compilerOut = await compiler.transformOxcReactCompiler(
1408
+ id,
1409
+ inputCode,
1410
+ '19',
1411
+ true
1412
+ )
1413
+ const remapping = (await import('@jridgewell/remapping')).default
1414
+ const doubleComposedMap = remapping(
1415
+ [result.map as any, compilerOut.map as any],
1416
+ () => null
1417
+ )
1418
+ const badTracer = new TraceMap(doubleComposedMap as any)
1419
+ const badPos = originalPositionFor(badTracer, {
1420
+ line: markerLineIndex + 1,
1421
+ column: markerCol,
1422
+ })
1423
+ expect(badPos.source).toBeNull()
1424
+ expect(badPos.line).toBeNull()
1425
+ } finally {
1426
+ compiler.configureVXRNCompilerPlugin({ enableCompiler: false })
1427
+ if (origEnv !== undefined) {
1428
+ process.env.VXRN_USE_BABEL_FOR_GENERATORS = origEnv
1429
+ } else {
1430
+ delete process.env.VXRN_USE_BABEL_FOR_GENERATORS
1431
+ }
1432
+ }
1433
+ })
1434
+
1435
+ it('transforms worklets via native Rust/Wasm SWC with zero Babel execution and accurate sourcemaps', async () => {
1436
+ const { TraceMap, originalPositionFor } = await import('@jridgewell/trace-mapping')
1437
+ const compiler = await import('@vxrn/compiler')
1438
+ const { vi } = await import('vitest')
1439
+ const babelSpy = vi.spyOn(compiler, 'transformBabel')
1440
+
1441
+ compiler.configureVXRNCompilerPlugin({
1442
+ enableReanimated: true,
1443
+ enableNativeWorklets: true,
1444
+ enableCompiler: false,
1445
+ })
1446
+
1447
+ try {
1448
+ const projectRoot = process.cwd()
1449
+ const plugin = vxrnCompilerPlugin('ios', true, projectRoot, true)
1450
+ if (typeof plugin.transform !== 'function') {
1451
+ throw new Error('transform hook not callable')
1452
+ }
1453
+
1454
+ const inputCode = `import { useAnimatedStyle } from 'react-native-reanimated'
1455
+
1456
+ export function Box() {
1457
+ const animatedStyle = useAnimatedStyle(() => {
1458
+ return { opacity: 1 }
1459
+ })
1460
+ return <div style={animatedStyle} />
1461
+ }
1462
+ `
1463
+ const id = join(projectRoot, 'Box.tsx')
1464
+ const result = await Reflect.apply(plugin.transform, undefined, [inputCode, id])
1465
+
1466
+ expect(result).toBeDefined()
1467
+ expect(result.code).toContain('__workletHash')
1468
+ expect(result.code).toContain('__closure')
1469
+ // Zero Babel calls!
1470
+ expect(babelSpy).not.toHaveBeenCalled()
1471
+
1472
+ // Source map tracing
1473
+ expect(result.map).toBeDefined()
1474
+ const tracer = new TraceMap(result.map)
1475
+ const lines = result.code.split('\n')
1476
+ const boxLineIndex = lines.findIndex((l: string) =>
1477
+ l.includes('export function Box')
1478
+ )
1479
+ expect(boxLineIndex).toBeGreaterThan(0)
1480
+
1481
+ const pos = originalPositionFor(tracer, {
1482
+ line: boxLineIndex + 1,
1483
+ column: 16,
1484
+ })
1485
+
1486
+ expect(pos.source).toBe(id)
1487
+ expect(pos.line).toBe(3)
1488
+ } finally {
1489
+ babelSpy.mockRestore()
1490
+ compiler.configureVXRNCompilerPlugin({
1491
+ enableReanimated: false,
1492
+ enableNativeWorklets: false,
1493
+ })
1494
+ }
1495
+ })
1496
+
1497
+ it('retains the existing Babel backend by default when enableNativeWorklets is false for auto-detected Reanimated and ordinary configured worklets plugin', async () => {
1498
+ const compiler = await import('@vxrn/compiler')
1499
+ const { vi } = await import('vitest')
1500
+ const babelSpy = vi.spyOn(compiler, 'transformBabel').mockResolvedValue({
1501
+ code: '/* babel transformed */',
1502
+ } as any)
1503
+ const workletSpy = vi.spyOn(compiler, 'transformWorklets')
1504
+
1505
+ // enableReanimated is auto-detected, but enableNativeWorklets is NOT explicitly enabled
1506
+ compiler.configureVXRNCompilerPlugin({
1507
+ enableReanimated: true,
1508
+ enableNativeWorklets: false,
1509
+ })
1510
+
1511
+ const getBabelOptionsSpy = vi.spyOn(compiler, 'getBabelOptions').mockReturnValue({
1512
+ plugins: ['react-native-reanimated/plugin'],
1513
+ } as any)
1514
+
1515
+ try {
1516
+ const projectRoot = process.cwd()
1517
+ const plugin = vxrnCompilerPlugin('ios', true, projectRoot, false)
1518
+ if (typeof plugin.transform !== 'function') {
1519
+ throw new Error('transform hook not callable')
1520
+ }
1521
+
1522
+ const inputCode = `export function fn() { 'worklet'; return 1 }`
1523
+ const id = join(projectRoot, 'DefaultWorklet.tsx')
1524
+ const result = await Reflect.apply(plugin.transform, undefined, [inputCode, id])
1525
+
1526
+ expect(result).toBeDefined()
1527
+ // Retained existing Babel backend: Babel was called with the plugin
1528
+ expect(babelSpy).toHaveBeenCalledWith(
1529
+ id,
1530
+ inputCode,
1531
+ expect.objectContaining({
1532
+ plugins: expect.arrayContaining(['react-native-reanimated/plugin']),
1533
+ })
1534
+ )
1535
+ // Native SWC was NOT called
1536
+ expect(workletSpy).not.toHaveBeenCalled()
1537
+ } finally {
1538
+ babelSpy.mockRestore()
1539
+ workletSpy.mockRestore()
1540
+ getBabelOptionsSpy.mockRestore()
1541
+ compiler.configureVXRNCompilerPlugin({
1542
+ enableReanimated: false,
1543
+ enableNativeWorklets: false,
1544
+ })
1545
+ }
1546
+ })
1547
+
1548
+ it('intercepts tuple worklet plugins with options, executes native SWC with options, and skips Babel when enableNativeWorklets is true', async () => {
1549
+ const compiler = await import('@vxrn/compiler')
1550
+ const { vi } = await import('vitest')
1551
+ const babelSpy = vi.spyOn(compiler, 'transformBabel')
1552
+ const workletSpy = vi.spyOn(compiler, 'transformWorklets')
1553
+
1554
+ compiler.configureVXRNCompilerPlugin({
1555
+ enableReanimated: true,
1556
+ enableNativeWorklets: true,
1557
+ })
1558
+
1559
+ const getBabelOptionsSpy = vi.spyOn(compiler, 'getBabelOptions').mockReturnValue({
1560
+ plugins: [['react-native-reanimated/plugin', { globals: ['customGlobal'] }]],
1561
+ } as any)
1562
+
1563
+ try {
1564
+ const projectRoot = process.cwd()
1565
+ const plugin = vxrnCompilerPlugin('ios', true, projectRoot, false)
1566
+ if (typeof plugin.transform !== 'function') {
1567
+ throw new Error('transform hook not callable')
1568
+ }
1569
+
1570
+ const inputCode = `export function fn() { 'worklet'; return customGlobal * 2 }`
1571
+ const id = join(projectRoot, 'TupleWorklet.tsx')
1572
+ const result = await Reflect.apply(plugin.transform, undefined, [inputCode, id])
1573
+
1574
+ expect(result).toBeDefined()
1575
+ expect(result.code).toContain('__workletHash')
1576
+ // Forwarded options
1577
+ expect(workletSpy).toHaveBeenCalledWith(
1578
+ id,
1579
+ expect.any(String),
1580
+ false,
1581
+ expect.objectContaining({ globals: ['customGlobal'], projectRoot })
1582
+ )
1583
+ // Babel was completely skipped
1584
+ expect(babelSpy).not.toHaveBeenCalled()
1585
+ } finally {
1586
+ babelSpy.mockRestore()
1587
+ workletSpy.mockRestore()
1588
+ getBabelOptionsSpy.mockRestore()
1589
+ compiler.configureVXRNCompilerPlugin({
1590
+ enableReanimated: false,
1591
+ enableNativeWorklets: false,
1592
+ })
1593
+ }
1594
+ })
778
1595
  })
779
1596
 
780
1597
  describe('native production assets', () => {
@@ -1016,3 +1833,60 @@ globalThis.__vxrnConditionalExportProbe = helper()
1016
1833
  }
1017
1834
  })
1018
1835
  })
1836
+
1837
+ describe('native Flow sources', () => {
1838
+ it.each(['', `/* ${'license text '.repeat(150)} */\n`])('strips third-party Flow following a license header (%#)', async (license) => {
1839
+ const testRoot = await mkdtemp(join(tmpdir(), 'vxrn-native-flow-'))
1840
+ // mirrors @react-native-masked-view/masked-view: a Flow `.js` component
1841
+ // outside the react-native / @react-native scopes, which is what reached
1842
+ // rolldown unstripped when its patch was skipped.
1843
+ const packageRoot = join(testRoot, 'node_modules/@flowy-scope/flowy-lib')
1844
+ await mkdir(packageRoot, { recursive: true })
1845
+ await writeFile(
1846
+ join(packageRoot, 'package.json'),
1847
+ JSON.stringify({ name: '@flowy-scope/flowy-lib', main: './js/Flowy.js' })
1848
+ )
1849
+ await mkdir(join(packageRoot, 'js'), { recursive: true })
1850
+ await writeFile(
1851
+ join(packageRoot, 'js/Flowy.js'),
1852
+ `${license}/**
1853
+ * @flow
1854
+ * @format
1855
+ */
1856
+
1857
+ type FlowyProps = {|
1858
+ +value: number,
1859
+ |}
1860
+
1861
+ export default class Flowy {
1862
+ _seen: boolean = false
1863
+
1864
+ measure(props: FlowyProps): number {
1865
+ const { value, ...rest }: FlowyProps = props
1866
+ this._seen = true
1867
+ return ((value: any): number) + Object.keys(rest).length
1868
+ }
1869
+ }
1870
+ `
1871
+ )
1872
+ await writeFile(
1873
+ join(testRoot, 'entry.js'),
1874
+ `import Flowy from '@flowy-scope/flowy-lib'
1875
+ globalThis.__vxrnFlowProbe = new Flowy().measure({ value: 41 }) + 1
1876
+ `
1877
+ )
1878
+
1879
+ try {
1880
+ const result = await buildNativeBundle({
1881
+ root: testRoot,
1882
+ platform: 'ios',
1883
+ entryFile: 'entry.js',
1884
+ })
1885
+ const context = { clearTimeout, console, process: { env: {} }, setTimeout }
1886
+ runInNewContext(result.code, context)
1887
+ expect(Reflect.get(context, '__vxrnFlowProbe')).toBe(42)
1888
+ } finally {
1889
+ await rm(testRoot, { recursive: true, force: true })
1890
+ }
1891
+ })
1892
+ })