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,18 +2,234 @@ 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 } 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.mjs";
10
- import { buildNativeBundle, createNativeDevAssetRegistry, getHermesSWCIncludes, getHmrRuntimeSource, getNativeAssetData, getNativeTransformConfig, hermesCompatSWCPlugin, hmrClientNoopPlugin, nativeAnimatedGuardPlugin, normalizeNativeCommonJSInterop, vxrnCompilerPlugin, wrapNativeBundleModuleScope } from "./createNativeDevEngine.mjs";
10
+ import { workletImportsPlugin } from "../plugins/workletImportsPlugin.mjs";
11
+ import { buildNativeBundle, createNativeDevAssetRegistry, createNativeDevEngine, getHermesSWCIncludes, getHmrRuntimeSource, getNativeAssetData, getNativeTransformConfig, hermesCompatSWCPlugin, hmrClientNoopPlugin, nativeAnimatedGuardPlugin, normalizeNativeCommonJSInterop, vxrnCompilerPlugin, wrapNativeBundleModuleScope } from "./createNativeDevEngine.mjs";
11
12
  const nativeTransformProbe = `
12
13
  export const transformProbe = () => {
13
14
  'worklet'
14
15
  return 'transformed'
15
16
  }
16
17
  `;
18
+ describe.each(["rolldown-dev", "rolldown-build", "metro-module", "metro-script"])("native async through %s", pipeline => {
19
+ it("preserves defaults, lexical bindings, bigint, rejection, and iterator cleanup", async () => {
20
+ const id = "/project/native-async.js";
21
+ const source = `
22
+ globalThis.result = (function(parentArgument) {
23
+ const events = [];
24
+ const run = async(value = 2n, fail = false) => {
25
+ try {
26
+ const awaited = await (fail ? Promise.reject(new Error('rejected')) : Promise.resolve(3n));
27
+ events.push('after-await');
28
+ return [String(value + awaited), this.base, arguments[0]];
29
+ } catch (error) { return error.message; }
30
+ finally { events.push('finally'); }
31
+ };
32
+ const badDefault = async(value = (() => { throw new Error('default'); })()) => value;
33
+ let synchronousThrow = false;
34
+ let rejectedDefault;
35
+ try { rejectedDefault = badDefault().catch(error => error.message); }
36
+ catch { synchronousThrow = true; }
37
+ async function* values() {
38
+ try { yield 1n; yield 2n; }
39
+ finally { events.push('closed'); }
40
+ }
41
+ const first = async() => { for await (const value of values()) return String(value); };
42
+ return Promise.all([run(), run(4n), run(undefined, true), rejectedDefault, first()])
43
+ .then(values => ({ values, events, synchronousThrow }));
44
+ }).call({ base: 7 }, 'lexical');
45
+ `;
46
+ let output;
47
+ if (pipeline.startsWith("metro")) {
48
+ const {
49
+ transform
50
+ } = await import("@vxrn/vite-plugin-metro/metroNativeWorker");
51
+ const data = (await transform({}, "/project", id, Buffer.from(source), {
52
+ dev: false,
53
+ platform: "ios",
54
+ type: pipeline === "metro-script" ? "script" : "module"
55
+ })).output[0].data;
56
+ const {
57
+ fromRawMappings
58
+ } = await import("metro-source-map");
59
+ output = {
60
+ code: data.code,
61
+ map: fromRawMappings([{
62
+ code: data.code,
63
+ path: id,
64
+ source,
65
+ map: data.map
66
+ }]).toMap()
67
+ };
68
+ } else {
69
+ const plugin = hermesCompatSWCPlugin(pipeline === "rolldown-dev", true);
70
+ output = await Reflect.apply(plugin.transform, void 0, [source, id]);
71
+ }
72
+ const context = {};
73
+ context.__d = factory => {
74
+ const module = {
75
+ exports: {}
76
+ };
77
+ factory(context, name => {
78
+ throw new Error(`unexpected dependency ${name}`);
79
+ }, () => {}, () => {}, module, module.exports, []);
80
+ };
81
+ runInNewContext(output.code, context);
82
+ const result = await context.result;
83
+ expect(result.values).toEqual([["5", 7, "lexical"], ["7", 7, "lexical"], "rejected", "default", "1"]);
84
+ expect(result.synchronousThrow).toBe(false);
85
+ expect(result.events.filter(value => value === "finally")).toHaveLength(3);
86
+ expect(result.events.filter(value => value === "closed")).toHaveLength(1);
87
+ const {
88
+ originalPositionFor,
89
+ TraceMap
90
+ } = await import("@jridgewell/trace-mapping");
91
+ const lines = output.code.split("\n");
92
+ const line = lines.findIndex(value => value.includes("after-await"));
93
+ const position = originalPositionFor(new TraceMap(output.map), {
94
+ line: line + 1,
95
+ column: lines[line].indexOf("events.push")
96
+ });
97
+ expect(position.source).toBe(id);
98
+ expect(position.line).toBe(source.split("\n").findIndex(value => value.includes("after-await")) + 1);
99
+ });
100
+ });
101
+ describe.each(["shared", "rolldown", "metro"])("React Compiler worklets through %s", pipeline => {
102
+ it.each([["block", "useDerivedValue(() => { return value + 8 }, [value])", 18], ["expression", "useDerivedValue(() => value + 8, [value])", 18], ["object", "useDerivedValue(() => ({ lift: value + 8 }), [value])", {
103
+ lift: 18
104
+ }], ["nested", "useDerivedValue(() => runOnUI(() => value + 8), [value])", 18], ["gesture", "useDerivedValue(() => Gesture.Pan().onUpdate(() => value + 8), [value])", 18]])("preserves %s callbacks on the UI runtime and memoizes stable inputs", async (_name, callback, expected) => {
105
+ const compiler = await import("@vxrn/compiler");
106
+ const {
107
+ parseSync
108
+ } = await import("oxc-parser");
109
+ const {
110
+ default: MagicString
111
+ } = await import("magic-string");
112
+ const {
113
+ TraceMap,
114
+ originalPositionFor
115
+ } = await import("@jridgewell/trace-mapping");
116
+ const projectRoot = await createWorkletsProject(false);
117
+ const sourceMaps = process.env.VXRN_ENABLE_SOURCE_MAP;
118
+ process.env.VXRN_ENABLE_SOURCE_MAP = "1";
119
+ compiler.configureVXRNCompilerPlugin({
120
+ enableCompiler: true,
121
+ enableReanimated: true,
122
+ enableNativeWorklets: true
123
+ });
124
+ try {
125
+ const id = join(projectRoot, "useProbe.ts");
126
+ const source = `export function useProbe(value) {
127
+ return ${callback};
128
+ }
129
+ `;
130
+ await writeFile(id, source);
131
+ let result;
132
+ if (pipeline === "shared") {
133
+ const plugin = (await compiler.createVXRNCompilerPlugin()).find(p => p.name === "one:compiler");
134
+ await plugin.configResolved({
135
+ root: projectRoot,
136
+ build: {}
137
+ });
138
+ result = await (plugin.transform.handler || plugin.transform).call({
139
+ environment: {
140
+ name: "ios"
141
+ }
142
+ }, source, id);
143
+ } else if (pipeline === "metro") {
144
+ const {
145
+ transform
146
+ } = await import("@vxrn/vite-plugin-metro/metroNativeWorker");
147
+ const data = (await transform({}, projectRoot, id, Buffer.from(source), {
148
+ dev: false,
149
+ platform: "ios",
150
+ type: "module",
151
+ customTransformOptions: {
152
+ reactCompiler: true,
153
+ worklets: true
154
+ }
155
+ })).output[0].data;
156
+ const msm = await import("metro-source-map");
157
+ result = {
158
+ code: data.code,
159
+ map: msm.fromRawMappings([{
160
+ code: data.code,
161
+ path: id,
162
+ source,
163
+ map: data.map
164
+ }]).toMap()
165
+ };
166
+ } else {
167
+ const plugin = vxrnCompilerPlugin("ios", false, projectRoot, true);
168
+ result = await Reflect.apply(plugin.transform, void 0, [source, id]);
169
+ }
170
+ const runnable = new MagicString(result.code);
171
+ for (const node of parseSync("probe.js", result.code).program.body) {
172
+ if (node.type === "ImportDeclaration") runnable.remove(node.start, node.end);
173
+ if (node.type === "ExportNamedDeclaration" && node.declaration) runnable.remove(node.start, node.declaration.start);
174
+ }
175
+ let cache;
176
+ let cacheCalls = 0;
177
+ const memo = size => {
178
+ cacheCalls++;
179
+ return cache ??= Array(size).fill(/* @__PURE__ */Symbol.for("react.memo_cache_sentinel"));
180
+ };
181
+ const module = {
182
+ exports: {}
183
+ };
184
+ const probe = new Function("_c", "useDerivedValue", "runOnUI", "Gesture", "__d", "module", `${runnable}
185
+ return ${pipeline === "metro" ? "module.exports.useProbe" : "useProbe"}`)(memo, fn => fn, fn => fn, {
186
+ Pan: () => ({
187
+ onUpdate: fn => fn
188
+ })
189
+ }, factory => factory(globalThis, () => ({
190
+ c: memo
191
+ }), () => {}, () => {}, module, module.exports, []), module);
192
+ const first = probe(10);
193
+ expect(first.__initData?.code).toBeTypeOf("string");
194
+ for (const onUI of [false, true]) {
195
+ let fn = first;
196
+ let value;
197
+ for (let depth = 0; depth < 2; depth++) {
198
+ expect(fn.__initData?.code).toBeTypeOf("string");
199
+ value = onUI ? new Function("runOnUI", `return (${fn.__initData.code})`)(fn2 => fn2).call({
200
+ __closure: fn.__closure
201
+ }) : fn();
202
+ if (typeof value !== "function") break;
203
+ fn = value;
204
+ }
205
+ expect(value).toEqual(expected);
206
+ }
207
+ expect(probe(10)).toBe(first);
208
+ expect(cacheCalls).toBe(2);
209
+ expect(probe(20)).not.toBe(first);
210
+ const lines = result.code.split("\n");
211
+ const line = lines.findIndex(text => text.includes("function useProbe("));
212
+ expect(line).toBeGreaterThanOrEqual(0);
213
+ const position = originalPositionFor(new TraceMap(result.map), {
214
+ line: line + 1,
215
+ column: lines[line].indexOf("function")
216
+ });
217
+ expect(position.source).toBe(id);
218
+ expect(position.line).toBe(1);
219
+ } finally {
220
+ if (sourceMaps === void 0) delete process.env.VXRN_ENABLE_SOURCE_MAP;else process.env.VXRN_ENABLE_SOURCE_MAP = sourceMaps;
221
+ compiler.configureVXRNCompilerPlugin({
222
+ enableCompiler: false,
223
+ enableReanimated: false,
224
+ enableNativeWorklets: false
225
+ });
226
+ await rm(projectRoot, {
227
+ recursive: true,
228
+ force: true
229
+ });
230
+ }
231
+ });
232
+ });
17
233
  async function createWorkletsProject(throwOnTransform = true) {
18
234
  const testRoot = await mkdtemp(join(tmpdir(), "vxrn-native-transform-failure-"));
19
235
  const packageRoot = join(testRoot, "node_modules/react-native-worklets");
@@ -21,7 +237,8 @@ async function createWorkletsProject(throwOnTransform = true) {
21
237
  recursive: true
22
238
  });
23
239
  await writeFile(join(packageRoot, "package.json"), JSON.stringify({
24
- name: "react-native-worklets"
240
+ name: "react-native-worklets",
241
+ version: "0.10.1"
25
242
  }));
26
243
  await writeFile(join(packageRoot, "plugin.js"), throwOnTransform ? `module.exports = () => ({
27
244
  visitor: {
@@ -59,7 +276,249 @@ describe("native prelude", () => {
59
276
  expect(context.removeEventListener).toBe(removeEventListener);
60
277
  });
61
278
  });
279
+ describe("native pure-function worklet imports", () => {
280
+ it("keeps live module versions isolated across a transitive HMR update", async () => {
281
+ const root2 = await createWorkletsProject(false);
282
+ const packageRoot = join(root2, "node_modules/pure-math");
283
+ await mkdir(packageRoot, {
284
+ recursive: true
285
+ });
286
+ await writeFile(join(packageRoot, "package.json"), JSON.stringify({
287
+ type: "module",
288
+ exports: "./index.js"
289
+ }));
290
+ const dependency = join(packageRoot, "amount.ios.js");
291
+ await writeFile(dependency, "export const amount = 2");
292
+ await writeFile(join(packageRoot, "amount.android.js"), "export const amount = 99");
293
+ await writeFile(join(packageRoot, "index.js"), `
294
+ import { amount } from './amount'
295
+ const instance = globalThis.moduleInitializations = (globalThis.moduleInitializations || 0) + 1
296
+ export default function calculate(value) { return value + amount }
297
+ export function reads() { return instance }
298
+ `);
299
+ await writeFile(join(root2, "entry.mjs"), `
300
+ import calculate, { reads } from 'pure-math'
301
+ globalThis.calculate = calculate
302
+ globalThis.reads = reads
303
+ if (import.meta.hot) import.meta.hot.accept(() => {})
304
+ `);
305
+ const compiler = await import("@vxrn/compiler");
306
+ compiler.configureVXRNCompilerPlugin({
307
+ enableReanimated: true,
308
+ enableNativeWorklets: true
309
+ });
310
+ let resolveUpdate;
311
+ const updated = new Promise(resolve => {
312
+ resolveUpdate = resolve;
313
+ });
314
+ const native = await createNativeDevEngine({
315
+ root: root2,
316
+ port: 0,
317
+ platform: "ios",
318
+ plugins: [workletImportsPlugin({
319
+ "pure-math": ["default", "reads"]
320
+ }), {
321
+ name: "worklet-import-fixture",
322
+ transform(_code, id) {
323
+ if (id.endsWith("/__virtual-native-entry.tsx")) return `import './entry.mjs'`;
324
+ }
325
+ }],
326
+ onHmrUpdate: resolveUpdate
327
+ });
328
+ try {
329
+ const initial = await native.getBundle();
330
+ const rn = {
331
+ console,
332
+ setTimeout,
333
+ clearTimeout
334
+ };
335
+ runInNewContext(initial.code, rn);
336
+ expect(rn.calculate(10)).toBe(12);
337
+ expect(rn.reads()).toBe(1);
338
+ const ui = createContext({});
339
+ const compiled = /* @__PURE__ */new Map();
340
+ const reconstruct = worklet => {
341
+ if (!worklet.__initData || !worklet.__workletHash) throw new Error("remote function");
342
+ let fn = compiled.get(worklet.__workletHash);
343
+ if (!fn) {
344
+ fn = runInContext(`(${worklet.__initData.code})`, ui);
345
+ compiled.set(worklet.__workletHash, fn);
346
+ }
347
+ const closure = Object.fromEntries(Object.entries(worklet.__closure).map(([name, value]) => [name, typeof value === "function" ? reconstruct(value) : value]));
348
+ return fn.bind({
349
+ __closure: closure
350
+ });
351
+ };
352
+ expect(() => reconstruct(value => value + 2)).toThrow("remote function");
353
+ const old = reconstruct(rn.calculate);
354
+ const oldReads = reconstruct(rn.reads);
355
+ expect(old(10)).toBe(12);
356
+ expect(reconstruct(rn.calculate)(11)).toBe(13);
357
+ expect(oldReads()).toBe(1);
358
+ expect(rn.reads()).toBe(1);
359
+ const runtime = rn.__rolldown_runtime__;
360
+ await native.engine.registerClient(runtime.clientId);
361
+ await writeFile(dependency, "export const amount = 3");
362
+ const update = await updated;
363
+ expect(update.type).toBe("hmr:update");
364
+ expect(runtime.applyHmrUpdate(update.code, update.changedIds, update.seq)).toBe(true);
365
+ expect(rn.calculate(10)).toBe(13);
366
+ const next = reconstruct(rn.calculate);
367
+ expect([old(10), next(10), old(10), next(10)]).toEqual([12, 13, 12, 13]);
368
+ const nextReads = reconstruct(rn.reads);
369
+ expect([oldReads(), nextReads(), oldReads(), nextReads()]).toEqual([1, 2, 1, 2]);
370
+ expect(ui.moduleInitializations).toBe(2);
371
+ expect(rn.reads()).toBe(2);
372
+ await writeFile(join(root2, "production.mjs"), `import calculate, { reads } from 'pure-math'; globalThis.calculate = calculate; globalThis.reads = reads`);
373
+ const production = await buildNativeBundle({
374
+ root: root2,
375
+ platform: "android",
376
+ entryFile: "production.mjs",
377
+ plugins: [workletImportsPlugin({
378
+ "pure-math": ["default", "reads"]
379
+ })]
380
+ });
381
+ const productionRN = {
382
+ console
383
+ };
384
+ runInNewContext(production.code, productionRN);
385
+ expect(productionRN.calculate(10)).toBe(109);
386
+ expect(reconstruct(productionRN.calculate)(10)).toBe(109);
387
+ expect(reconstruct(productionRN.reads)()).toBe(3);
388
+ expect(productionRN.reads()).toBe(1);
389
+ const sharedPlugin = workletImportsPlugin({
390
+ "pure-math": ["default", "reads"]
391
+ });
392
+ expect(await Promise.all(["ios", "android"].map(async platform => {
393
+ const output = await buildNativeBundle({
394
+ root: root2,
395
+ platform,
396
+ entryFile: "production.mjs",
397
+ plugins: [sharedPlugin]
398
+ });
399
+ const context = {
400
+ console
401
+ };
402
+ runInNewContext(output.code, context);
403
+ return context.calculate(10);
404
+ }))).toEqual([13, 109]);
405
+ } finally {
406
+ await native.close();
407
+ compiler.configureVXRNCompilerPlugin({
408
+ enableReanimated: false,
409
+ enableNativeWorklets: false
410
+ });
411
+ await rm(root2, {
412
+ recursive: true,
413
+ force: true
414
+ });
415
+ }
416
+ });
417
+ it.each([["unselected", "export default function value() { return 1 }; export const other = 2", `import { other } from 'pure-math'; globalThis.value = other`, /unselected export other/], ["missing", "export const other = 2", `import value from 'pure-math'; globalThis.value = value`, /default.*not exported|MISSING_EXPORT/], ["builtin", `import fs from 'node:fs'; export default function value() { return fs.readFileSync('x') }`, `import value from 'pure-math'; globalThis.value = value`, /runtime dependency node:fs/], ["global", "export default function value() { return process.pid }", `import value from 'pure-math'; globalThis.value = value`, /unsupported runtime globals: process/], ["dynamic", `export default function value() { return import('./other.js') }`, `import value from 'pure-math'; globalThis.value = value`, /dynamic imports are unsupported/]])("rejects %s through the native engine", async (_name, source, entry, expected) => {
418
+ const root2 = await createWorkletsProject(false);
419
+ const packageRoot = join(root2, "node_modules/pure-math");
420
+ await mkdir(packageRoot, {
421
+ recursive: true
422
+ });
423
+ await writeFile(join(packageRoot, "package.json"), JSON.stringify({
424
+ type: "module",
425
+ exports: "./index.js"
426
+ }));
427
+ await writeFile(join(packageRoot, "index.js"), source);
428
+ await writeFile(join(root2, "entry.mjs"), entry);
429
+ const compiler = await import("@vxrn/compiler");
430
+ compiler.configureVXRNCompilerPlugin({
431
+ enableReanimated: true,
432
+ enableNativeWorklets: true
433
+ });
434
+ const native = await createNativeDevEngine({
435
+ root: root2,
436
+ port: 0,
437
+ platform: "ios",
438
+ plugins: [workletImportsPlugin({
439
+ "pure-math": ["default"]
440
+ }), {
441
+ name: "worklet-import-negative-fixture",
442
+ transform(_code, id) {
443
+ if (id.endsWith("/__virtual-native-entry.tsx")) return `import './entry.mjs'`;
444
+ }
445
+ }]
446
+ });
447
+ try {
448
+ await expect(native.getBundle()).rejects.toThrow(expected);
449
+ } finally {
450
+ await native.close();
451
+ compiler.configureVXRNCompilerPlugin({
452
+ enableReanimated: false,
453
+ enableNativeWorklets: false
454
+ });
455
+ await rm(root2, {
456
+ recursive: true,
457
+ force: true
458
+ });
459
+ }
460
+ });
461
+ });
62
462
  describe("native Rolldown HMR runtime", () => {
463
+ it.each([true, false])("keeps CommonJS default components callable through the native HMR wire path (Babel=%s)", async babel => {
464
+ const root2 = await mkdtemp(join(tmpdir(), "vxrn-native-hmr-interop-"));
465
+ const entry = join(root2, "entry.mjs");
466
+ const packageRoot = join(root2, "node_modules/component");
467
+ await mkdir(packageRoot, {
468
+ recursive: true
469
+ });
470
+ await writeFile(join(packageRoot, "package.json"), JSON.stringify({
471
+ main: "./component(view).js"
472
+ }));
473
+ await writeFile(join(packageRoot, "component(view).js"), babel ? `Object.defineProperty(exports, '__esModule', { value: true }); exports.default = function Component() { return 42 }` : `module.exports = function Component() { return 42 }`);
474
+ const source = version => `
475
+ import Component from 'component'
476
+ globalThis.renderComponent = Component
477
+ export const version = ${version}
478
+ if (import.meta.hot) import.meta.hot.accept(() => {})
479
+ `;
480
+ await writeFile(entry, source(1));
481
+ let resolveUpdate;
482
+ const updateReceived = new Promise(resolve => {
483
+ resolveUpdate = resolve;
484
+ });
485
+ const native = await createNativeDevEngine({
486
+ root: root2,
487
+ port: 0,
488
+ platform: "ios",
489
+ plugins: [{
490
+ name: "native-hmr-fixture-entry",
491
+ transform(_code, id) {
492
+ if (id.endsWith("/__virtual-native-entry.tsx")) return `import './entry.mjs'`;
493
+ }
494
+ }],
495
+ onHmrUpdate: resolveUpdate
496
+ });
497
+ try {
498
+ const initial = await native.getBundle();
499
+ const context = {
500
+ console,
501
+ setTimeout,
502
+ clearTimeout
503
+ };
504
+ runInNewContext(initial.code, context);
505
+ expect(context.renderComponent()).toBe(42);
506
+ const runtime = context.__rolldown_runtime__;
507
+ await native.engine.registerClient(runtime.clientId);
508
+ await writeFile(entry, source(2));
509
+ const update = await updateReceived;
510
+ expect(update.type).toBe("hmr:update");
511
+ expect(update.clientId).toBe(runtime.clientId);
512
+ expect(runtime.applyHmrUpdate(update.code, update.changedIds, update.seq)).toBe(true);
513
+ expect(context.renderComponent()).toBe(42);
514
+ } finally {
515
+ await native.close();
516
+ await rm(root2, {
517
+ recursive: true,
518
+ force: true
519
+ });
520
+ }
521
+ });
63
522
  it("registers a Rolldown 1.2 client and applies a self-accepted patch", {
64
523
  timeout: 3e4
65
524
  }, async () => {
@@ -347,6 +806,27 @@ describe("getHermesSWCIncludes", () => {
347
806
  expect(result.code).not.toContain("async function*");
348
807
  expect(result.code).not.toContain("async function *");
349
808
  });
809
+ it("downlevels private fields, public class fields, and static blocks for Hermes", async () => {
810
+ const plugin = hermesCompatSWCPlugin(true);
811
+ if (typeof plugin.transform !== "function") throw new Error("Hermes compatibility transform hook is not callable");
812
+ const result = await Reflect.apply(plugin.transform, void 0, [`
813
+ class TransformProbe {
814
+ #privateField = 1
815
+ publicField = 2
816
+ static {
817
+ TransformProbe.initialized = true
818
+ }
819
+
820
+ getSecret() {
821
+ return this.#privateField
822
+ }
823
+ }
824
+ `, "/project/TransformProbe.ts"]);
825
+ expect(result.code).not.toContain("#privateField");
826
+ expect(result.code).not.toMatch(/^\s*publicField\s*=/m);
827
+ expect(result.code).not.toContain("static {");
828
+ expect(result.code).toContain("this.publicField = 2");
829
+ });
350
830
  it("preserves per-iteration bindings used by lazy method getters", async () => {
351
831
  const plugin = hermesCompatSWCPlugin(true);
352
832
  if (typeof plugin.transform !== "function") throw new Error("Hermes compatibility transform hook is not callable");
@@ -457,6 +937,7 @@ describe("native required transform failures", () => {
457
937
  it.each([true, false])("rejects valid worklet source when its required compiler transform fails (dev=%s)", async dev2 => {
458
938
  const testRoot = await createWorkletsProject();
459
939
  const compiler = await import("@vxrn/compiler");
940
+ const spy = vi.spyOn(compiler, "transformWorklets").mockRejectedValue(/* @__PURE__ */new Error("NATIVE_TRANSFORM_NEGATIVE_CONTROL"));
460
941
  compiler.configureVXRNCompilerPlugin({
461
942
  enableReanimated: true
462
943
  });
@@ -465,6 +946,7 @@ describe("native required transform failures", () => {
465
946
  if (typeof plugin.transform !== "function") throw new Error("vxrn compiler transform hook is not callable");
466
947
  await expect(Reflect.apply(plugin.transform, void 0, [nativeTransformProbe, join(testRoot, "entry.ts")])).rejects.toThrow("NATIVE_TRANSFORM_NEGATIVE_CONTROL");
467
948
  } finally {
949
+ spy.mockRestore();
468
950
  compiler.configureVXRNCompilerPlugin({
469
951
  enableReanimated: false
470
952
  });
@@ -477,6 +959,7 @@ describe("native required transform failures", () => {
477
959
  it("returns a dev build error and no output when the required compiler transform fails", async () => {
478
960
  const testRoot = await createWorkletsProject();
479
961
  const compiler = await import("@vxrn/compiler");
962
+ const spy = vi.spyOn(compiler, "transformWorklets").mockRejectedValue(/* @__PURE__ */new Error("NATIVE_TRANSFORM_NEGATIVE_CONTROL"));
480
963
  compiler.configureVXRNCompilerPlugin({
481
964
  enableReanimated: true
482
965
  });
@@ -499,6 +982,7 @@ describe("native required transform failures", () => {
499
982
  expect(result).toBeInstanceOf(Error);
500
983
  expect(String(result)).toContain("NATIVE_TRANSFORM_NEGATIVE_CONTROL");
501
984
  } finally {
985
+ spy.mockRestore();
502
986
  await engine.close();
503
987
  compiler.configureVXRNCompilerPlugin({
504
988
  enableReanimated: false
@@ -512,6 +996,7 @@ describe("native required transform failures", () => {
512
996
  it("emits no production bundle when the required compiler transform fails", async () => {
513
997
  const testRoot = await createWorkletsProject();
514
998
  const compiler = await import("@vxrn/compiler");
999
+ const spy = vi.spyOn(compiler, "transformWorklets").mockRejectedValue(/* @__PURE__ */new Error("NATIVE_TRANSFORM_NEGATIVE_CONTROL"));
515
1000
  compiler.configureVXRNCompilerPlugin({
516
1001
  enableReanimated: true
517
1002
  });
@@ -522,6 +1007,7 @@ describe("native required transform failures", () => {
522
1007
  entryFile: "entry.ts"
523
1008
  })).rejects.toThrow("NATIVE_TRANSFORM_NEGATIVE_CONTROL");
524
1009
  } finally {
1010
+ spy.mockRestore();
525
1011
  compiler.configureVXRNCompilerPlugin({
526
1012
  enableReanimated: false
527
1013
  });
@@ -559,6 +1045,230 @@ describe("native required transform failures", () => {
559
1045
  });
560
1046
  }
561
1047
  });
1048
+ it("composes source maps across React Compiler, Fast Refresh, and module wrapper back to original source lines", async () => {
1049
+ const {
1050
+ TraceMap,
1051
+ originalPositionFor
1052
+ } = await import("@jridgewell/trace-mapping");
1053
+ const compiler = await import("@vxrn/compiler");
1054
+ compiler.configureVXRNCompilerPlugin({
1055
+ enableCompiler: true
1056
+ });
1057
+ try {
1058
+ const plugin = vxrnCompilerPlugin("ios", true, "/project", true);
1059
+ if (typeof plugin.transform !== "function") throw new Error("transform hook not callable");
1060
+ const inputCode = `import { useState } from 'react'
1061
+
1062
+ export function Counter() {
1063
+ const [count, setCount] = useState(0)
1064
+ return <button onClick={() => setCount(count + 1)}>{count}</button>
1065
+ }
1066
+ `;
1067
+ const id = "/project/Counter.tsx";
1068
+ const result = await Reflect.apply(plugin.transform, void 0, [inputCode, id]);
1069
+ expect(result).toBeDefined();
1070
+ expect(result.map).toBeDefined();
1071
+ const tracer = new TraceMap(result.map);
1072
+ const counterLineIndex = result.code.split("\n").findIndex(l => l.includes("function Counter"));
1073
+ expect(counterLineIndex).toBeGreaterThan(0);
1074
+ const pos = originalPositionFor(tracer, {
1075
+ line: counterLineIndex + 1,
1076
+ column: 16
1077
+ });
1078
+ expect(pos.source).toBe(id);
1079
+ expect(pos.line).toBe(3);
1080
+ } finally {
1081
+ compiler.configureVXRNCompilerPlugin({
1082
+ enableCompiler: false
1083
+ });
1084
+ }
1085
+ });
1086
+ it("composes source maps accurately across mixed path (React Compiler + Babel + Fast Refresh + module wrapper)", async () => {
1087
+ const {
1088
+ TraceMap,
1089
+ originalPositionFor
1090
+ } = await import("@jridgewell/trace-mapping");
1091
+ const compiler = await import("@vxrn/compiler");
1092
+ const origEnv = process.env.VXRN_USE_BABEL_FOR_GENERATORS;
1093
+ process.env.VXRN_USE_BABEL_FOR_GENERATORS = "1";
1094
+ compiler.configureVXRNCompilerPlugin({
1095
+ enableCompiler: true,
1096
+ enableReanimated: false
1097
+ });
1098
+ try {
1099
+ const plugin = vxrnCompilerPlugin("ios", true, "/project", true);
1100
+ if (typeof plugin.transform !== "function") throw new Error("transform hook not callable");
1101
+ const inputCode = `export function Counter() { return <View />; }
1102
+
1103
+
1104
+
1105
+
1106
+
1107
+
1108
+
1109
+ export async function* probe() { throw new Error("MARKER"); }
1110
+ `;
1111
+ const id = "/project/Counter.tsx";
1112
+ const result = await Reflect.apply(plugin.transform, void 0, [inputCode, id]);
1113
+ expect(result).toBeDefined();
1114
+ expect(result.map).toBeDefined();
1115
+ expect(result.code.includes("_wrapAsyncGenerator") || result.code.includes("regeneratorRuntime")).toBe(true);
1116
+ const tracer = new TraceMap(result.map);
1117
+ const lines = result.code.split("\n");
1118
+ const markerLineIndex = lines.findIndex(l => l.includes("MARKER"));
1119
+ expect(markerLineIndex).toBeGreaterThan(0);
1120
+ const markerCol = lines[markerLineIndex].indexOf("throw");
1121
+ const pos = originalPositionFor(tracer, {
1122
+ line: markerLineIndex + 1,
1123
+ column: markerCol
1124
+ });
1125
+ expect(pos.source).toBe(id);
1126
+ expect(pos.line).toBe(9);
1127
+ expect(pos.column).toBe(33);
1128
+ const compilerOut = await compiler.transformOxcReactCompiler(id, inputCode, "19", true);
1129
+ const remapping = (await import("@jridgewell/remapping")).default;
1130
+ const badPos = originalPositionFor(new TraceMap(remapping([result.map, compilerOut.map], () => null)), {
1131
+ line: markerLineIndex + 1,
1132
+ column: markerCol
1133
+ });
1134
+ expect(badPos.source).toBeNull();
1135
+ expect(badPos.line).toBeNull();
1136
+ } finally {
1137
+ compiler.configureVXRNCompilerPlugin({
1138
+ enableCompiler: false
1139
+ });
1140
+ if (origEnv !== void 0) process.env.VXRN_USE_BABEL_FOR_GENERATORS = origEnv;else delete process.env.VXRN_USE_BABEL_FOR_GENERATORS;
1141
+ }
1142
+ });
1143
+ it("transforms worklets via native Rust/Wasm SWC with zero Babel execution and accurate sourcemaps", async () => {
1144
+ const {
1145
+ TraceMap,
1146
+ originalPositionFor
1147
+ } = await import("@jridgewell/trace-mapping");
1148
+ const compiler = await import("@vxrn/compiler");
1149
+ const {
1150
+ vi: vi2
1151
+ } = await import("vitest");
1152
+ const babelSpy = vi2.spyOn(compiler, "transformBabel");
1153
+ compiler.configureVXRNCompilerPlugin({
1154
+ enableReanimated: true,
1155
+ enableNativeWorklets: true,
1156
+ enableCompiler: false
1157
+ });
1158
+ try {
1159
+ const projectRoot = process.cwd();
1160
+ const plugin = vxrnCompilerPlugin("ios", true, projectRoot, true);
1161
+ if (typeof plugin.transform !== "function") throw new Error("transform hook not callable");
1162
+ const inputCode = `import { useAnimatedStyle } from 'react-native-reanimated'
1163
+
1164
+ export function Box() {
1165
+ const animatedStyle = useAnimatedStyle(() => {
1166
+ return { opacity: 1 }
1167
+ })
1168
+ return <div style={animatedStyle} />
1169
+ }
1170
+ `;
1171
+ const id = join(projectRoot, "Box.tsx");
1172
+ const result = await Reflect.apply(plugin.transform, void 0, [inputCode, id]);
1173
+ expect(result).toBeDefined();
1174
+ expect(result.code).toContain("__workletHash");
1175
+ expect(result.code).toContain("__closure");
1176
+ expect(babelSpy).not.toHaveBeenCalled();
1177
+ expect(result.map).toBeDefined();
1178
+ const tracer = new TraceMap(result.map);
1179
+ const boxLineIndex = result.code.split("\n").findIndex(l => l.includes("export function Box"));
1180
+ expect(boxLineIndex).toBeGreaterThan(0);
1181
+ const pos = originalPositionFor(tracer, {
1182
+ line: boxLineIndex + 1,
1183
+ column: 16
1184
+ });
1185
+ expect(pos.source).toBe(id);
1186
+ expect(pos.line).toBe(3);
1187
+ } finally {
1188
+ babelSpy.mockRestore();
1189
+ compiler.configureVXRNCompilerPlugin({
1190
+ enableReanimated: false,
1191
+ enableNativeWorklets: false
1192
+ });
1193
+ }
1194
+ });
1195
+ it("retains the existing Babel backend by default when enableNativeWorklets is false for auto-detected Reanimated and ordinary configured worklets plugin", async () => {
1196
+ const compiler = await import("@vxrn/compiler");
1197
+ const {
1198
+ vi: vi2
1199
+ } = await import("vitest");
1200
+ const babelSpy = vi2.spyOn(compiler, "transformBabel").mockResolvedValue({
1201
+ code: "/* babel transformed */"
1202
+ });
1203
+ const workletSpy = vi2.spyOn(compiler, "transformWorklets");
1204
+ compiler.configureVXRNCompilerPlugin({
1205
+ enableReanimated: true,
1206
+ enableNativeWorklets: false
1207
+ });
1208
+ const getBabelOptionsSpy = vi2.spyOn(compiler, "getBabelOptions").mockReturnValue({
1209
+ plugins: ["react-native-reanimated/plugin"]
1210
+ });
1211
+ try {
1212
+ const projectRoot = process.cwd();
1213
+ const plugin = vxrnCompilerPlugin("ios", true, projectRoot, false);
1214
+ if (typeof plugin.transform !== "function") throw new Error("transform hook not callable");
1215
+ const inputCode = `export function fn() { 'worklet'; return 1 }`;
1216
+ const id = join(projectRoot, "DefaultWorklet.tsx");
1217
+ expect(await Reflect.apply(plugin.transform, void 0, [inputCode, id])).toBeDefined();
1218
+ expect(babelSpy).toHaveBeenCalledWith(id, inputCode, expect.objectContaining({
1219
+ plugins: expect.arrayContaining(["react-native-reanimated/plugin"])
1220
+ }));
1221
+ expect(workletSpy).not.toHaveBeenCalled();
1222
+ } finally {
1223
+ babelSpy.mockRestore();
1224
+ workletSpy.mockRestore();
1225
+ getBabelOptionsSpy.mockRestore();
1226
+ compiler.configureVXRNCompilerPlugin({
1227
+ enableReanimated: false,
1228
+ enableNativeWorklets: false
1229
+ });
1230
+ }
1231
+ });
1232
+ it("intercepts tuple worklet plugins with options, executes native SWC with options, and skips Babel when enableNativeWorklets is true", async () => {
1233
+ const compiler = await import("@vxrn/compiler");
1234
+ const {
1235
+ vi: vi2
1236
+ } = await import("vitest");
1237
+ const babelSpy = vi2.spyOn(compiler, "transformBabel");
1238
+ const workletSpy = vi2.spyOn(compiler, "transformWorklets");
1239
+ compiler.configureVXRNCompilerPlugin({
1240
+ enableReanimated: true,
1241
+ enableNativeWorklets: true
1242
+ });
1243
+ const getBabelOptionsSpy = vi2.spyOn(compiler, "getBabelOptions").mockReturnValue({
1244
+ plugins: [["react-native-reanimated/plugin", {
1245
+ globals: ["customGlobal"]
1246
+ }]]
1247
+ });
1248
+ try {
1249
+ const projectRoot = process.cwd();
1250
+ const plugin = vxrnCompilerPlugin("ios", true, projectRoot, false);
1251
+ if (typeof plugin.transform !== "function") throw new Error("transform hook not callable");
1252
+ const inputCode = `export function fn() { 'worklet'; return customGlobal * 2 }`;
1253
+ const id = join(projectRoot, "TupleWorklet.tsx");
1254
+ const result = await Reflect.apply(plugin.transform, void 0, [inputCode, id]);
1255
+ expect(result).toBeDefined();
1256
+ expect(result.code).toContain("__workletHash");
1257
+ expect(workletSpy).toHaveBeenCalledWith(id, expect.any(String), false, expect.objectContaining({
1258
+ globals: ["customGlobal"],
1259
+ projectRoot
1260
+ }));
1261
+ expect(babelSpy).not.toHaveBeenCalled();
1262
+ } finally {
1263
+ babelSpy.mockRestore();
1264
+ workletSpy.mockRestore();
1265
+ getBabelOptionsSpy.mockRestore();
1266
+ compiler.configureVXRNCompilerPlugin({
1267
+ enableReanimated: false,
1268
+ enableNativeWorklets: false
1269
+ });
1270
+ }
1271
+ });
562
1272
  });
563
1273
  describe("native production assets", () => {
564
1274
  it("registers scale siblings and keeps monorepo assets inside assetsDest", async () => {
@@ -752,4 +1462,65 @@ globalThis.__vxrnConditionalExportProbe = helper()
752
1462
  }
753
1463
  });
754
1464
  });
1465
+ describe("native Flow sources", () => {
1466
+ it.each(["", `/* ${"license text ".repeat(150)} */
1467
+ `])("strips third-party Flow following a license header (%#)", async license => {
1468
+ const testRoot = await mkdtemp(join(tmpdir(), "vxrn-native-flow-"));
1469
+ const packageRoot = join(testRoot, "node_modules/@flowy-scope/flowy-lib");
1470
+ await mkdir(packageRoot, {
1471
+ recursive: true
1472
+ });
1473
+ await writeFile(join(packageRoot, "package.json"), JSON.stringify({
1474
+ name: "@flowy-scope/flowy-lib",
1475
+ main: "./js/Flowy.js"
1476
+ }));
1477
+ await mkdir(join(packageRoot, "js"), {
1478
+ recursive: true
1479
+ });
1480
+ await writeFile(join(packageRoot, "js/Flowy.js"), `${license}/**
1481
+ * @flow
1482
+ * @format
1483
+ */
1484
+
1485
+ type FlowyProps = {|
1486
+ +value: number,
1487
+ |}
1488
+
1489
+ export default class Flowy {
1490
+ _seen: boolean = false
1491
+
1492
+ measure(props: FlowyProps): number {
1493
+ const { value, ...rest }: FlowyProps = props
1494
+ this._seen = true
1495
+ return ((value: any): number) + Object.keys(rest).length
1496
+ }
1497
+ }
1498
+ `);
1499
+ await writeFile(join(testRoot, "entry.js"), `import Flowy from '@flowy-scope/flowy-lib'
1500
+ globalThis.__vxrnFlowProbe = new Flowy().measure({ value: 41 }) + 1
1501
+ `);
1502
+ try {
1503
+ const result = await buildNativeBundle({
1504
+ root: testRoot,
1505
+ platform: "ios",
1506
+ entryFile: "entry.js"
1507
+ });
1508
+ const context = {
1509
+ clearTimeout,
1510
+ console,
1511
+ process: {
1512
+ env: {}
1513
+ },
1514
+ setTimeout
1515
+ };
1516
+ runInNewContext(result.code, context);
1517
+ expect(Reflect.get(context, "__vxrnFlowProbe")).toBe(42);
1518
+ } finally {
1519
+ await rm(testRoot, {
1520
+ recursive: true,
1521
+ force: true
1522
+ });
1523
+ }
1524
+ });
1525
+ });
755
1526
  //# sourceMappingURL=createNativeDevEngine.test.mjs.map