@vxrn/compiler 1.25.13 → 1.25.16-1788753284749

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 (124) hide show
  1. package/dist/cjs/cache.cjs +3 -1
  2. package/dist/cjs/configure.cjs +9 -1
  3. package/dist/cjs/index.cjs +82 -4
  4. package/dist/cjs/reactNativeCodegen.cjs +140 -0
  5. package/dist/cjs/reactNativeViewConfig.cjs +489 -0
  6. package/dist/cjs/transformBabel.cjs +15 -20
  7. package/dist/cjs/transformBabel.test.cjs +260 -0
  8. package/dist/cjs/transformHermesLoops.cjs +317 -0
  9. package/dist/cjs/transformHermesLoops.test.cjs +220 -0
  10. package/dist/cjs/transformSWC.cjs +102 -137
  11. package/dist/cjs/transformSWC.test.cjs +74 -0
  12. package/dist/cjs/transformWorklets.cjs +121 -0
  13. package/dist/cjs/transformWorklets.test.cjs +604 -0
  14. package/dist/cjs/worklets/autoworklet.cjs +265 -0
  15. package/dist/cjs/worklets/globals.cjs +71 -0
  16. package/dist/cjs/worklets/hash.cjs +38 -0
  17. package/dist/cjs/worklets/scope.cjs +370 -0
  18. package/dist/cjs/worklets/serialize.cjs +101 -0
  19. package/dist/cjs/worklets/transform.cjs +232 -0
  20. package/dist/cjs/worklets/types.cjs +18 -0
  21. package/dist/esm/cache.mjs +4 -2
  22. package/dist/esm/cache.mjs.map +1 -1
  23. package/dist/esm/configure.mjs +8 -1
  24. package/dist/esm/configure.mjs.map +1 -1
  25. package/dist/esm/index.js +72 -5
  26. package/dist/esm/index.js.map +1 -1
  27. package/dist/esm/index.mjs +72 -5
  28. package/dist/esm/index.mjs.map +1 -1
  29. package/dist/esm/reactNativeCodegen.mjs +104 -0
  30. package/dist/esm/reactNativeCodegen.mjs.map +1 -0
  31. package/dist/esm/reactNativeViewConfig.mjs +460 -0
  32. package/dist/esm/reactNativeViewConfig.mjs.map +1 -0
  33. package/dist/esm/transformBabel.mjs +14 -19
  34. package/dist/esm/transformBabel.mjs.map +1 -1
  35. package/dist/esm/transformBabel.test.mjs +261 -1
  36. package/dist/esm/transformBabel.test.mjs.map +1 -1
  37. package/dist/esm/transformHermesLoops.mjs +281 -0
  38. package/dist/esm/transformHermesLoops.mjs.map +1 -0
  39. package/dist/esm/transformHermesLoops.test.mjs +197 -0
  40. package/dist/esm/transformHermesLoops.test.mjs.map +1 -0
  41. package/dist/esm/transformSWC.mjs +89 -138
  42. package/dist/esm/transformSWC.mjs.map +1 -1
  43. package/dist/esm/transformSWC.test.mjs +75 -0
  44. package/dist/esm/transformSWC.test.mjs.map +1 -0
  45. package/dist/esm/transformWorklets.mjs +81 -0
  46. package/dist/esm/transformWorklets.mjs.map +1 -0
  47. package/dist/esm/transformWorklets.test.mjs +581 -0
  48. package/dist/esm/transformWorklets.test.mjs.map +1 -0
  49. package/dist/esm/worklets/autoworklet.mjs +234 -0
  50. package/dist/esm/worklets/autoworklet.mjs.map +1 -0
  51. package/dist/esm/worklets/globals.mjs +45 -0
  52. package/dist/esm/worklets/globals.mjs.map +1 -0
  53. package/dist/esm/worklets/hash.mjs +13 -0
  54. package/dist/esm/worklets/hash.mjs.map +1 -0
  55. package/dist/esm/worklets/scope.mjs +345 -0
  56. package/dist/esm/worklets/scope.mjs.map +1 -0
  57. package/dist/esm/worklets/serialize.mjs +75 -0
  58. package/dist/esm/worklets/serialize.mjs.map +1 -0
  59. package/dist/esm/worklets/transform.mjs +196 -0
  60. package/dist/esm/worklets/transform.mjs.map +1 -0
  61. package/dist/esm/worklets/types.mjs +2 -0
  62. package/dist/esm/worklets/types.mjs.map +1 -0
  63. package/package.json +10 -6
  64. package/src/cache.ts +9 -2
  65. package/src/configure.ts +12 -0
  66. package/src/index.ts +93 -13
  67. package/src/reactNativeCodegen.ts +181 -0
  68. package/src/reactNativeViewConfig.ts +593 -0
  69. package/src/transformBabel.test.ts +268 -1
  70. package/src/transformBabel.ts +21 -31
  71. package/src/transformHermesLoops.test.ts +282 -0
  72. package/src/transformHermesLoops.ts +433 -0
  73. package/src/transformSWC.test.ts +88 -0
  74. package/src/transformSWC.ts +109 -160
  75. package/src/transformWorklets.test.ts +708 -0
  76. package/src/transformWorklets.ts +160 -0
  77. package/src/types.ts +4 -4
  78. package/src/worklets/autoworklet.ts +391 -0
  79. package/src/worklets/globals.ts +125 -0
  80. package/src/worklets/hash.ts +14 -0
  81. package/src/worklets/scope.ts +401 -0
  82. package/src/worklets/serialize.ts +114 -0
  83. package/src/worklets/transform.ts +282 -0
  84. package/src/worklets/types.ts +26 -0
  85. package/types/cache.d.ts +1 -0
  86. package/types/cache.d.ts.map +1 -1
  87. package/types/configure.d.ts +2 -0
  88. package/types/configure.d.ts.map +1 -1
  89. package/types/index.d.ts +3 -0
  90. package/types/index.d.ts.map +1 -1
  91. package/types/reactNativeCodegen.d.ts +5 -0
  92. package/types/reactNativeCodegen.d.ts.map +1 -0
  93. package/types/reactNativeViewConfig.d.ts +35 -0
  94. package/types/reactNativeViewConfig.d.ts.map +1 -0
  95. package/types/transformBabel.d.ts +3 -3
  96. package/types/transformBabel.d.ts.map +1 -1
  97. package/types/transformHermesLoops.d.ts +5 -0
  98. package/types/transformHermesLoops.d.ts.map +1 -0
  99. package/types/transformHermesLoops.test.d.ts +2 -0
  100. package/types/transformHermesLoops.test.d.ts.map +1 -0
  101. package/types/transformSWC.d.ts +12 -3
  102. package/types/transformSWC.d.ts.map +1 -1
  103. package/types/transformSWC.test.d.ts +2 -0
  104. package/types/transformSWC.test.d.ts.map +1 -0
  105. package/types/transformWorklets.d.ts +22 -0
  106. package/types/transformWorklets.d.ts.map +1 -0
  107. package/types/transformWorklets.test.d.ts +2 -0
  108. package/types/transformWorklets.test.d.ts.map +1 -0
  109. package/types/types.d.ts +4 -4
  110. package/types/types.d.ts.map +1 -1
  111. package/types/worklets/autoworklet.d.ts +14 -0
  112. package/types/worklets/autoworklet.d.ts.map +1 -0
  113. package/types/worklets/globals.d.ts +3 -0
  114. package/types/worklets/globals.d.ts.map +1 -0
  115. package/types/worklets/hash.d.ts +5 -0
  116. package/types/worklets/hash.d.ts.map +1 -0
  117. package/types/worklets/scope.d.ts +7 -0
  118. package/types/worklets/scope.d.ts.map +1 -0
  119. package/types/worklets/serialize.d.ts +12 -0
  120. package/types/worklets/serialize.d.ts.map +1 -0
  121. package/types/worklets/transform.d.ts +6 -0
  122. package/types/worklets/transform.d.ts.map +1 -0
  123. package/types/worklets/types.d.ts +21 -0
  124. package/types/worklets/types.d.ts.map +1 -0
@@ -0,0 +1,708 @@
1
+ import fs from 'node:fs'
2
+ import os from 'node:os'
3
+ import path from 'node:path'
4
+ import { afterEach, describe, expect, it } from 'vitest'
5
+ import { configureVXRNCompilerPlugin } from './configure'
6
+ import { shouldTransformWorklets, transformWorklets } from './transformWorklets'
7
+
8
+ afterEach(() => {
9
+ configureVXRNCompilerPlugin({ enableReanimated: false, enableNativeWorklets: false })
10
+ })
11
+
12
+ describe('shouldTransformWorklets', () => {
13
+ it('returns false when enableReanimated is false', () => {
14
+ configureVXRNCompilerPlugin({ enableReanimated: false, enableNativeWorklets: true })
15
+ expect(
16
+ shouldTransformWorklets({
17
+ id: '/app/index.tsx',
18
+ code: `export const foo = () => { 'worklet'; return 1; }`,
19
+ })
20
+ ).toBe(false)
21
+ })
22
+
23
+ it('returns false by default when enableNativeWorklets is false even if enableReanimated is true', () => {
24
+ configureVXRNCompilerPlugin({ enableReanimated: true, enableNativeWorklets: false })
25
+ expect(
26
+ shouldTransformWorklets({
27
+ id: '/app/index.tsx',
28
+ code: `export const foo = () => { 'worklet'; return 1; }`,
29
+ })
30
+ ).toBe(false)
31
+ })
32
+
33
+ it('returns true when both enableReanimated and enableNativeWorklets are true and worklet keyword is present', () => {
34
+ configureVXRNCompilerPlugin({ enableReanimated: true, enableNativeWorklets: true })
35
+ expect(
36
+ shouldTransformWorklets({
37
+ id: '/app/index.tsx',
38
+ code: `export const foo = () => { 'worklet'; return 1; }`,
39
+ })
40
+ ).toBe(true)
41
+ })
42
+
43
+ it('returns false for ignored paths such as react-native internals', () => {
44
+ configureVXRNCompilerPlugin({ enableReanimated: true, enableNativeWorklets: true })
45
+ expect(
46
+ shouldTransformWorklets({
47
+ id: '/project/node_modules/react-native/Libraries/StyleSheet/index.js',
48
+ code: `export const foo = () => { 'worklet'; return 1; }`,
49
+ })
50
+ ).toBe(false)
51
+ })
52
+ })
53
+
54
+ describe('transformWorklets', () => {
55
+ it('transforms functions with worklet directive', async () => {
56
+ const code = `
57
+ export function animate() {
58
+ 'worklet'
59
+ return 42
60
+ }
61
+ `
62
+ const result = await transformWorklets('/app/animate.ts', code, false)
63
+ expect(result.code).toContain('__workletHash')
64
+ expect(result.code).toContain('__initData')
65
+ expect(result.code).toContain('__closure')
66
+ expect(result.map).toBeUndefined()
67
+ })
68
+
69
+ it('ignores calls whose callee name collides with Object.prototype', async () => {
70
+ // reanimated calls .constructor(), .toString() and similar. these must not
71
+ // resolve against the auto-worklet argument map's prototype chain.
72
+ const code = `
73
+ const out = value.constructor(1)
74
+ const s = thing.toString()
75
+ const h = obj.hasOwnProperty('x')
76
+ const v = obj.valueOf()
77
+ `
78
+ const result = await transformWorklets('/app/collide.ts', code, false)
79
+ expect(result.code).not.toContain('__workletHash')
80
+ })
81
+
82
+ it('workletizes gesture callbacks only when the receiver is a Gesture chain', async () => {
83
+ // `onChange`/`onStart`/`onEnd` are ordinary identifiers in unrelated code.
84
+ // matching them by name alone ships app callbacks to the UI runtime.
85
+ const gesture = await transformWorklets(
86
+ '/app/gesture.ts',
87
+ `const g = Gesture.Pan().onStart((e) => { scale.value = e.x }).onChange((e) => { scale.value = e.y })`,
88
+ false
89
+ )
90
+ expect((gesture.code.match(/__workletHash/g) ?? []).length).toBe(2)
91
+
92
+ const notAGesture = await transformWorklets(
93
+ '/app/plain.ts',
94
+ `onChange((d) => d + scrollDiff); emitter.onStart(function () { return count })`,
95
+ false
96
+ )
97
+ expect(notAGesture.code).not.toContain('__workletHash')
98
+ })
99
+
100
+ it('workletizes withCallback only on a layout animation chain', async () => {
101
+ const layout = await transformWorklets(
102
+ '/app/layout.ts',
103
+ `const a = FadeIn.duration(300).withCallback((finished) => { done.value = finished })`,
104
+ false
105
+ )
106
+ expect(layout.code).toContain('__workletHash')
107
+
108
+ const plain = await transformWorklets(
109
+ '/app/plain-callback.ts',
110
+ `queue.withCallback((finished) => { done.value = finished })`,
111
+ false
112
+ )
113
+ expect(plain.code).not.toContain('__workletHash')
114
+ })
115
+
116
+ it('transforms closure variables into worklet parameters', async () => {
117
+ const code = `
118
+ const factor = 2
119
+ export function multiply(x: number) {
120
+ 'worklet'
121
+ return x * factor
122
+ }
123
+ `
124
+ const result = await transformWorklets('/app/multiply.ts', code, false)
125
+ expect(result.code).toContain('__closure')
126
+ expect(result.code).toContain('factor')
127
+ })
128
+
129
+ it('transforms an anonymous function expression worklet', async () => {
130
+ // passed inline, so there is no variable to take a name from.
131
+ const code = `
132
+ register(function (e) {
133
+ 'worklet'
134
+ return e.x
135
+ })
136
+ `
137
+ const result = await transformWorklets('/app/handler.ts', code, false)
138
+ expect(result.code).toContain('__workletHash')
139
+ expect(result.code).toMatch(/code: "function _worklet\(e\)/)
140
+ expect(result.code).toContain('var _worklet = function _worklet(e)')
141
+ })
142
+
143
+ it('does not capture hoisted declarations used before their declaration', async () => {
144
+ // `flushQueue` and `later` are declared below their use. hoisting makes that
145
+ // legal, so neither is a captured variable; treating them as captured puts
146
+ // undefined in the closure and the worklet throws on the UI thread.
147
+ const code = `
148
+ export function setupLoop() {
149
+ 'worklet'
150
+ function nativeFlush(t: number) {
151
+ flushQueue(t)
152
+ }
153
+ globalThis.__native(nativeFlush)
154
+ function flushQueue(t: number) {
155
+ globalThis.__stamp = t
156
+ }
157
+ const start = () => later()
158
+ const later = () => start
159
+ return start
160
+ }
161
+ `
162
+ const result = await transformWorklets('/app/setupLoop.ts', code, false)
163
+
164
+ expect(result.code).toMatch(/setupLoop\.__closure = \{\s*\}/)
165
+ const init = result.code.match(/code: "function setupLoop[^"]*"/)
166
+ expect(init, 'serialized worklet code').toBeTruthy()
167
+ expect(init![0]).not.toContain('this.__closure')
168
+ expect(init![0]).toContain('flushQueue')
169
+ })
170
+
171
+ it('honors no-worklet-closure and limit-init-data-hoisting the way the worklets runtime needs', async () => {
172
+ // this is the shape react-native-worklets uses for its unpacker installers.
173
+ // the outer worklet is serialized to a string and evaluated on a worklet
174
+ // runtime with no closure bound, so it must not emit an unpacker line, and
175
+ // the inner worklet's init data must live inside it rather than at module
176
+ // scope, which does not exist over there.
177
+ const code = `
178
+ const outerModuleValue = 1
179
+ export function installUnpacker() {
180
+ 'worklet'
181
+ 'no-worklet-closure'
182
+ const proxy = globalThis.__proxy
183
+ proxy.register(outerModuleValue)
184
+ proxy.install(() => {
185
+ 'worklet'
186
+ 'limit-init-data-hoisting'
187
+ return 42
188
+ })
189
+ }
190
+ `
191
+ const result = await transformWorklets('/app/installUnpacker.ts', code, false)
192
+
193
+ const outerInit = result.code.match(/code: "function installUnpacker[^"]*"/)
194
+ expect(outerInit, 'outer worklet serialized code').toBeTruthy()
195
+ // no unpacker line: nothing is read off `this` on the worklet runtime.
196
+ expect(outerInit![0]).not.toContain('this.__closure')
197
+ expect(result.code).toMatch(/installUnpacker\.__closure = \{\s*\}/)
198
+ // the directives themselves are not part of the serialized body.
199
+ expect(outerInit![0]).not.toContain('no-worklet-closure')
200
+
201
+ // the inner worklet's init data is declared inside the outer function body,
202
+ // not hoisted to module scope where the worklet runtime cannot see it.
203
+ const innerVar = result.code.match(/var (_worklet_\d+_init_data) = \{\s*code: "function _worklet/)
204
+ expect(innerVar, 'inner worklet init data').toBeTruthy()
205
+ // only look past the serialized string, which mentions the same name inside
206
+ // its escaped body.
207
+ const emitted = result.code.slice(result.code.indexOf('export var installUnpacker'))
208
+ expect(emitted).toContain(`var ${innerVar![1]} = {`)
209
+ expect(result.code.slice(0, result.code.indexOf('export var installUnpacker'))).not.toContain(
210
+ `\nvar ${innerVar![1]} = {`
211
+ )
212
+ })
213
+
214
+ it('autoworkletizes hooks like useAnimatedStyle and withTiming', async () => {
215
+ const code = `
216
+ export function Card() {
217
+ const style = useAnimatedStyle(() => {
218
+ return {
219
+ opacity: withTiming(1),
220
+ }
221
+ })
222
+ return <div style={style} />
223
+ }
224
+ `
225
+ const result = await transformWorklets('/app/Card.tsx', code, false)
226
+ expect(result.code).toContain('__workletHash')
227
+ expect(result.code).toContain('useAnimatedStyle')
228
+ expect(result.code).toContain('<div style={style} />')
229
+ })
230
+
231
+ it('generates sourcemaps when requested', async () => {
232
+ const code = `
233
+ export const workletFn = () => {
234
+ 'worklet'
235
+ return 'ok'
236
+ }
237
+ `
238
+ const result = await transformWorklets('/app/worklet.ts', code, true)
239
+ expect(result.map).toBeDefined()
240
+ expect(result.map?.mappings).toBeDefined()
241
+ })
242
+
243
+ it('rejects on transform errors instead of returning untransformed source', async () => {
244
+ await expect(
245
+ transformWorklets(
246
+ '/app/broken.ts',
247
+ `export const fn = () => { 'worklet'; return ;;; const = }`,
248
+ false
249
+ )
250
+ ).rejects.toThrow()
251
+ })
252
+
253
+ it('stamps the package runtime version into __pluginVersion to avoid runtime mismatch', async () => {
254
+ const testRoot = fs.realpathSync(
255
+ fs.mkdtempSync(path.join(os.tmpdir(), 'vxrn-worklets-ver-'))
256
+ )
257
+ const packageRoot = path.join(testRoot, 'node_modules', 'react-native-worklets')
258
+ fs.mkdirSync(packageRoot, { recursive: true })
259
+ fs.writeFileSync(
260
+ path.join(packageRoot, 'package.json'),
261
+ JSON.stringify({ name: 'react-native-worklets', version: '0.10.1' })
262
+ )
263
+
264
+ try {
265
+ const result = await transformWorklets(
266
+ path.join(testRoot, 'entry.ts'),
267
+ `export const fn = () => { 'worklet'; return 1; }`,
268
+ false,
269
+ { projectRoot: testRoot }
270
+ )
271
+ expect(result.code).toContain('__pluginVersion = "0.10.1"')
272
+ } finally {
273
+ fs.rmSync(testRoot, { recursive: true, force: true })
274
+ }
275
+ })
276
+
277
+ it('correctly resolves hoisted react-native-worklets in a monorepo when child has reanimated', async () => {
278
+ const rootDir = fs.realpathSync(
279
+ fs.mkdtempSync(path.join(os.tmpdir(), 'vxrn-worklets-hoist-'))
280
+ )
281
+ const appDir = path.join(rootDir, 'apps', 'mobile')
282
+ const hoistedWorkletsDir = path.join(rootDir, 'node_modules', 'react-native-worklets')
283
+ const localReanimatedDir = path.join(
284
+ appDir,
285
+ 'node_modules',
286
+ 'react-native-reanimated'
287
+ )
288
+
289
+ fs.mkdirSync(hoistedWorkletsDir, { recursive: true })
290
+ fs.writeFileSync(
291
+ path.join(hoistedWorkletsDir, 'package.json'),
292
+ JSON.stringify({ name: 'react-native-worklets', version: '0.10.1' })
293
+ )
294
+
295
+ fs.mkdirSync(localReanimatedDir, { recursive: true })
296
+ fs.writeFileSync(
297
+ path.join(localReanimatedDir, 'package.json'),
298
+ JSON.stringify({ name: 'react-native-reanimated', version: '4.5.1' })
299
+ )
300
+ fs.writeFileSync(
301
+ path.join(appDir, 'package.json'),
302
+ JSON.stringify({ name: 'mobile' })
303
+ )
304
+
305
+ try {
306
+ const result = await transformWorklets(
307
+ path.join(appDir, 'entry.ts'),
308
+ `export function test() { 'worklet'; return 1; }`,
309
+ false,
310
+ { projectRoot: appDir }
311
+ )
312
+ // Must stamp the worklets runtime version (0.10.1), NOT reanimated version (4.5.1)!
313
+ expect(result.code).toContain('__pluginVersion = "0.10.1"')
314
+ } finally {
315
+ fs.rmSync(rootDir, { recursive: true, force: true })
316
+ }
317
+ })
318
+
319
+ it('throws an actionable error when neither worklets nor reanimated can be resolved', async () => {
320
+ const emptyRoot = fs.realpathSync(
321
+ fs.mkdtempSync(path.join(os.tmpdir(), 'vxrn-worklets-empty-'))
322
+ )
323
+ fs.writeFileSync(
324
+ path.join(emptyRoot, 'package.json'),
325
+ JSON.stringify({ name: 'empty-test' })
326
+ )
327
+
328
+ try {
329
+ await expect(
330
+ transformWorklets(
331
+ path.join(emptyRoot, 'entry.ts'),
332
+ `export function test() { 'worklet'; return 1; }`,
333
+ false,
334
+ { projectRoot: emptyRoot }
335
+ )
336
+ ).rejects.toThrow(
337
+ /Unable to resolve "react-native-worklets" or "react-native-reanimated"/
338
+ )
339
+ } finally {
340
+ fs.rmSync(emptyRoot, { recursive: true, force: true })
341
+ }
342
+ })
343
+
344
+ it('forwards plugin options such as bundleMode and globals', async () => {
345
+ const code = `
346
+ export function testCustom() {
347
+ 'worklet'
348
+ return customGlobalValue * 2
349
+ }
350
+ `
351
+ const result = await transformWorklets('/app/custom.ts', code, false, {
352
+ globals: ['customGlobalValue'],
353
+ bundleMode: false,
354
+ })
355
+ expect(result.code).toContain('__workletHash')
356
+ expect(result.code).toContain('__initData')
357
+ })
358
+
359
+ it('satisfies runtime worklet serialization, local execution, and UI-thread reconstruction', async () => {
360
+ const code = `
361
+ const multiplier = 5
362
+ export function scale(val: number) {
363
+ 'worklet'
364
+ return val * multiplier
365
+ }
366
+ `
367
+ const result = await transformWorklets('/app/scale.ts', code, false)
368
+
369
+ // Evaluate the transformed code in a JS runtime context
370
+ const sandbox = { global: globalThis, exports: {} as any }
371
+ const runner = new Function(
372
+ 'exports',
373
+ 'global',
374
+ result.code.replace('export var scale', 'exports.scale')
375
+ )
376
+ runner(sandbox.exports, sandbox.global)
377
+
378
+ const scaleFn = sandbox.exports.scale
379
+ expect(typeof scaleFn).toBe('function')
380
+ expect(scaleFn.__workletHash).toBeDefined()
381
+ expect(typeof scaleFn.__workletHash).toBe('number')
382
+ expect(scaleFn.__pluginVersion).toBe('0.10.1')
383
+ expect(scaleFn.__closure).toEqual({ multiplier: 5 })
384
+ expect(scaleFn.__initData).toBeDefined()
385
+ expect(scaleFn.__initData.code).toContain('__closure')
386
+
387
+ // Local JS execution
388
+ expect(scaleFn(10)).toBe(50)
389
+
390
+ // UI-thread reconstruction from __initData.code + __closure
391
+ const reconstructedUIFn = eval(`(${scaleFn.__initData.code})`)
392
+ const uiResult = reconstructedUIFn.call({ __closure: scaleFn.__closure }, 10)
393
+ expect(uiResult).toBe(50)
394
+ })
395
+
396
+ it('autoworkletizes hook callbacks with closure capture and UI-thread execution parity', async () => {
397
+ const code = `
398
+ import { useAnimatedStyle } from 'react-native-reanimated'
399
+ const baseOpacity = 0.8
400
+ export function getCardStyle() {
401
+ return useAnimatedStyle(() => {
402
+ return { opacity: baseOpacity }
403
+ })
404
+ }
405
+ `
406
+ const result = await transformWorklets('/app/CardStyle.ts', code, false)
407
+
408
+ let capturedCallback: any = null
409
+ const sandbox = {
410
+ global: globalThis,
411
+ exports: {} as any,
412
+ useAnimatedStyle: (cb: any) => {
413
+ capturedCallback = cb
414
+ return cb
415
+ },
416
+ }
417
+ const runner = new Function(
418
+ 'exports',
419
+ 'global',
420
+ 'useAnimatedStyle',
421
+ result.code
422
+ .replace(/import\s+{[^}]+}\s+from\s+['"][^'"]+['"];?/, '')
423
+ .replace(
424
+ 'export function getCardStyle',
425
+ 'exports.getCardStyle = function getCardStyle'
426
+ )
427
+ )
428
+ runner(sandbox.exports, sandbox.global, sandbox.useAnimatedStyle)
429
+
430
+ sandbox.exports.getCardStyle()
431
+ expect(capturedCallback).toBeDefined()
432
+ expect(capturedCallback.__workletHash).toBeDefined()
433
+ expect(capturedCallback.__closure).toEqual({ baseOpacity: 0.8 })
434
+
435
+ // UI thread execution of the animated style worklet
436
+ const reconstructedStyleFn = eval(`(${capturedCallback.__initData.code})`)
437
+ const styleResult = reconstructedStyleFn.call({
438
+ __closure: capturedCallback.__closure,
439
+ })
440
+ expect(styleResult).toEqual({ opacity: 0.8 })
441
+ })
442
+
443
+ it('compiles UI-thread worklet function to valid Hermes Bytecode (HBC)', async () => {
444
+ const code = `
445
+ const factor = 10
446
+ export function calculate(val: number) {
447
+ 'worklet'
448
+ return val * factor
449
+ }
450
+ `
451
+ const result = await transformWorklets('/app/calculate.ts', code, false)
452
+ const match = result.code.match(/code:\s*"([^"]+)"/)
453
+ expect(match).toBeDefined()
454
+ const uiCode = JSON.parse(`"${match![1]}"`)
455
+
456
+ // Verify Hermes bytecode compilation via hermesc if available
457
+ const { execFileSync } = await import('node:child_process')
458
+ // hermes-compiler ships all three platforms' binaries, so the osx one exists
459
+ // on linux CI too and running it gets a shell syntax error rather than a
460
+ // skip. pick the one that can actually execute here.
461
+ const hermescDir =
462
+ process.platform === 'darwin'
463
+ ? 'osx-bin'
464
+ : process.platform === 'win32'
465
+ ? 'win64-bin'
466
+ : 'linux64-bin'
467
+ const hermescPath = path.resolve(
468
+ __dirname,
469
+ `../../../node_modules/hermes-compiler/hermesc/${hermescDir}/hermesc${
470
+ process.platform === 'win32' ? '.exe' : ''
471
+ }`
472
+ )
473
+ if (fs.existsSync(hermescPath)) {
474
+ const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'hermes-worklet-test-'))
475
+ const jsPath = path.join(tempDir, 'worklet.js')
476
+ const hbcPath = path.join(tempDir, 'worklet.hbc')
477
+ try {
478
+ fs.writeFileSync(jsPath, `(${uiCode})`)
479
+ execFileSync(hermescPath, ['-emit-binary', '-out', hbcPath, jsPath])
480
+ expect(fs.existsSync(hbcPath)).toBe(true)
481
+ expect(fs.statSync(hbcPath).size).toBeGreaterThan(0)
482
+ } finally {
483
+ fs.rmSync(tempDir, { recursive: true, force: true })
484
+ }
485
+ }
486
+ })
487
+
488
+ it('transforms multiple worklets in the same file with independent closures', async () => {
489
+ const code = `
490
+ const a = 10
491
+ const b = 20
492
+ export function first() {
493
+ 'worklet'
494
+ return a
495
+ }
496
+ export function second() {
497
+ 'worklet'
498
+ return b
499
+ }
500
+ `
501
+ const result = await transformWorklets('/app/multiple.ts', code, false)
502
+ expect(result.code).toContain('first')
503
+ expect(result.code).toContain('second')
504
+ expect(result.code).toContain('__closure = { a: a }')
505
+ expect(result.code).toContain('__closure = { b: b }')
506
+ })
507
+
508
+ it('transforms concise arrow functions with useDerivedValue', async () => {
509
+ const code = `
510
+ import { useDerivedValue } from 'react-native-reanimated'
511
+ const multiplier = 3
512
+ export const derived = useDerivedValue(() => multiplier * 10)
513
+ `
514
+ const result = await transformWorklets('/app/derived.ts', code, false)
515
+ expect(result.code).toContain('__workletHash')
516
+ expect(result.code).toContain('multiplier')
517
+ })
518
+
519
+ it('transforms scroll handler with object methods', async () => {
520
+ const code = `
521
+ import { useAnimatedScrollHandler } from 'react-native-reanimated'
522
+ const offset = 5
523
+ export const handler = useAnimatedScrollHandler({
524
+ onScroll(event) {
525
+ 'worklet'
526
+ return event.contentOffset.y + offset
527
+ }
528
+ })
529
+ `
530
+ const result = await transformWorklets('/app/handler.ts', code, false)
531
+ expect(result.code).toContain('onScroll:')
532
+ expect(result.code).toContain('__closure = { offset: offset }')
533
+ })
534
+
535
+ it('correctly handles default and destructuring parameters in closure capture', async () => {
536
+ const code = `
537
+ const factor = 4
538
+ export function compute({ x, y = 1 }: { x: number; y?: number }, z = 0) {
539
+ 'worklet'
540
+ return (x + y + z) * factor
541
+ }
542
+ `
543
+ const result = await transformWorklets('/app/destructure.ts', code, false)
544
+ expect(result.code).toContain('__closure = { factor: factor }')
545
+
546
+ const sandbox = { global: globalThis, exports: {} as any }
547
+ const runner = new Function(
548
+ 'exports',
549
+ 'global',
550
+ result.code.replace('export var compute', 'exports.compute')
551
+ )
552
+ runner(sandbox.exports, sandbox.global)
553
+
554
+ const computeFn = sandbox.exports.compute
555
+ expect(computeFn({ x: 2 })).toBe(12) // (2 + 1 + 0) * 4 = 12
556
+
557
+ const reconstructed = eval(`(${computeFn.__initData.code})`)
558
+ const uiRes = reconstructed.call({ __closure: computeFn.__closure }, { x: 2, y: 3 }, 5)
559
+ expect(uiRes).toBe(40) // (2 + 3 + 5) * 4 = 40
560
+ })
561
+
562
+ it('operates with 100% elimination of SWC (zero swc dependencies in transformWorklets)', async () => {
563
+ const fs = await import('node:fs')
564
+ const transformWorkletsSource = fs.readFileSync(
565
+ path.resolve(__dirname, 'transformWorklets.ts'),
566
+ 'utf-8'
567
+ )
568
+ expect(transformWorkletsSource).not.toContain('@swc/core')
569
+ expect(transformWorkletsSource).not.toContain('@react-native-swc/worklets-plugin')
570
+
571
+ const result = await transformWorklets(
572
+ '/app/zeroSwc.ts',
573
+ `export function zeroSwc() { 'worklet'; return 999; }`,
574
+ false
575
+ )
576
+ expect(result.code).toContain('__workletHash')
577
+ expect(result.code).toContain('__pluginVersion')
578
+ expect(result.code).toContain('999')
579
+ })
580
+
581
+ it('correctly handles lexical block scope shadowing without losing outer bindings', async () => {
582
+ const code = `
583
+ const x = 7
584
+ export function f() {
585
+ 'worklet'
586
+ {
587
+ const x = 1
588
+ }
589
+ return x
590
+ }
591
+ `
592
+ const result = await transformWorklets('/app/scope-shadow.ts', code, false)
593
+ expect(result.code).toContain('__closure = { x: x }')
594
+
595
+ const sandbox = { global: globalThis, exports: {} as any }
596
+ const runner = new Function(
597
+ 'exports',
598
+ 'global',
599
+ result.code.replace('export var f', 'exports.f')
600
+ )
601
+ runner(sandbox.exports, sandbox.global)
602
+ const f = sandbox.exports.f
603
+ expect(f()).toBe(7)
604
+
605
+ const reconstructed = eval(`(${f.__initData.code})`)
606
+ const uiRes = reconstructed.call({ __closure: f.__closure })
607
+ expect(uiRes).toBe(7)
608
+ })
609
+
610
+ it('transforms nested worklets with bottom-up composition', async () => {
611
+ const code = `
612
+ export function outer() {
613
+ 'worklet'
614
+ function inner() {
615
+ 'worklet'
616
+ return 1
617
+ }
618
+ return inner()
619
+ }
620
+ `
621
+ const result = await transformWorklets('/app/nested.ts', code, false)
622
+ expect(result.code).toContain('export var outer')
623
+ expect(result.code).toContain('inner.__workletHash')
624
+ expect(result.code).toContain('outer.__workletHash')
625
+
626
+ const sandbox = { global: globalThis, exports: {} as any }
627
+ const runner = new Function(
628
+ 'exports',
629
+ 'global',
630
+ result.code.replace('export var outer', 'exports.outer')
631
+ )
632
+ runner(sandbox.exports, sandbox.global)
633
+ const outerFn = sandbox.exports.outer
634
+ expect(outerFn()).toBe(1)
635
+ })
636
+
637
+ it('transforms modules with >50 sibling worklets without truncation', async () => {
638
+ const fnCount = 60
639
+ const code = Array.from(
640
+ { length: fnCount },
641
+ (_, i) => `export function f${i}() { 'worklet'; return ${i}; }`
642
+ ).join('\n')
643
+
644
+ const result = await transformWorklets('/app/many.ts', code, false)
645
+
646
+ for (let i = 0; i < fnCount; i++) {
647
+ expect(result.code).toContain(`export var f${i} =`)
648
+ expect(result.code).toContain(`f${i}.__workletHash`)
649
+ }
650
+
651
+ // Verify last worklet (f59) is transformed and executable
652
+ const sandbox = { global: globalThis, exports: {} as any }
653
+ const runner = new Function(
654
+ 'exports',
655
+ 'global',
656
+ result.code.replace(/export var /g, 'exports.')
657
+ )
658
+ runner(sandbox.exports, sandbox.global)
659
+ expect(sandbox.exports.f0()).toBe(0)
660
+ expect(sandbox.exports.f50()).toBe(50)
661
+ expect(sandbox.exports.f59()).toBe(59)
662
+ })
663
+
664
+ it('transforms deeply nested worklets (3 levels) bottom-up', async () => {
665
+ const code = `
666
+ export function level1() {
667
+ 'worklet';
668
+ function level2() {
669
+ 'worklet';
670
+ function level3() {
671
+ 'worklet';
672
+ return 42;
673
+ }
674
+ return level3();
675
+ }
676
+ return level2();
677
+ }
678
+ `
679
+ const result = await transformWorklets('/app/deep.ts', code, false)
680
+ expect(result.code).toContain('level3.__workletHash')
681
+ expect(result.code).toContain('level2.__workletHash')
682
+ expect(result.code).toContain('level1.__workletHash')
683
+
684
+ const sandbox = { global: globalThis, exports: {} as any }
685
+ const runner = new Function(
686
+ 'exports',
687
+ 'global',
688
+ result.code.replace('export var level1', 'exports.level1')
689
+ )
690
+ runner(sandbox.exports, sandbox.global)
691
+ expect(sandbox.exports.level1()).toBe(42)
692
+ })
693
+ })
694
+
695
+
696
+
697
+
698
+ describe('worklet detection gate', () => {
699
+ it('accepts files whose only worklets come from gesture callbacks', () => {
700
+ // .onBegin/.onEnd/.onTouchesMove are auto-workletized by the transform, so
701
+ // a gate that misses them ships those callbacks untransformed.
702
+ configureVXRNCompilerPlugin({ enableReanimated: true, enableNativeWorklets: true })
703
+ const code = `
704
+ const tap = Gesture.Tap().onBegin((e) => { setState(e) }).onTouchesMove((e) => {}).onEnd(() => {})
705
+ `
706
+ expect(shouldTransformWorklets({ id: '/app/gesture.native.js', code })).toBe(true)
707
+ })
708
+ })