import fs from 'node:fs'
import os from 'node:os'
import { createRequire } from 'node:module'
import path from 'node:path'
import { runInNewContext } from 'node:vm'
import { afterEach, describe, expect, it } from 'vitest'
import { configureVXRNCompilerPlugin } from './configure'
import { shouldTransformWorklets, transformWorklets } from './transformWorklets'
afterEach(() => {
configureVXRNCompilerPlugin({ enableReanimated: false, enableNativeWorklets: false })
})
describe('shouldTransformWorklets', () => {
it('returns false when enableReanimated is false', () => {
configureVXRNCompilerPlugin({ enableReanimated: false, enableNativeWorklets: true })
expect(
shouldTransformWorklets({
id: '/app/index.tsx',
code: `export const foo = () => { 'worklet'; return 1; }`,
})
).toBe(false)
})
it('returns false by default when enableNativeWorklets is false even if enableReanimated is true', () => {
configureVXRNCompilerPlugin({ enableReanimated: true, enableNativeWorklets: false })
expect(
shouldTransformWorklets({
id: '/app/index.tsx',
code: `export const foo = () => { 'worklet'; return 1; }`,
})
).toBe(false)
})
it('returns true when both enableReanimated and enableNativeWorklets are true and worklet keyword is present', () => {
configureVXRNCompilerPlugin({ enableReanimated: true, enableNativeWorklets: true })
expect(
shouldTransformWorklets({
id: '/app/index.tsx',
code: `export const foo = () => { 'worklet'; return 1; }`,
})
).toBe(true)
})
it('returns false for ignored paths such as react-native internals', () => {
configureVXRNCompilerPlugin({ enableReanimated: true, enableNativeWorklets: true })
expect(
shouldTransformWorklets({
id: '/project/node_modules/react-native/Libraries/StyleSheet/index.js',
code: `export const foo = () => { 'worklet'; return 1; }`,
})
).toBe(false)
})
})
describe('transformWorklets', () => {
it.each([
{ body: 'value + 1', closure: [], expected: 11 },
{ body: 'value * multiplier + offset', closure: [5, 3], expected: 53 },
])(
'interoperates with upstream closure serialization for $body',
async ({ body, closure, expected }) => {
const code = `
const multiplier = 5
const offset = 3
export function compute(value) {
'worklet'
return ${body}
}
`
const require = createRequire(import.meta.url)
const upstream = require('@babel/core').transformSync(code, {
filename: '/app/compute.js',
babelrc: false,
configFile: false,
plugins: [
[require.resolve('react-native-worklets/plugin'), { disableSourceMaps: true }],
],
})
const ours = await transformWorklets('/app/compute.js', code)
const evaluate = (output: string) =>
new Function('global', `${output.replace(/\bexport /g, '')}; return compute`)(
globalThis
)
const upstreamFn = evaluate(upstream.code)
const ourFn = evaluate(ours.code)
expect(ourFn.__closure).toEqual(closure)
expect(ourFn.__closure).toEqual(upstreamFn.__closure ?? [])
expect(ourFn(10)).toBe(expected)
const upstreamUI = eval(upstreamFn.__initData.code)
const ourUI = eval(ourFn.__initData.code)
expect(upstreamUI.call({ __closure: ourFn.__closure }, 10)).toBe(expected)
expect(ourUI.call({ __closure: upstreamFn.__closure }, 10)).toBe(expected)
}
)
it('transforms functions with worklet directive', async () => {
const code = `
export function animate() {
'worklet'
return 42
}
`
const result = await transformWorklets('/app/animate.ts', code, false)
expect(result.code).toContain('__workletHash')
expect(result.code).toContain('__initData')
expect(result.code).toContain('__closure')
expect(result.map).toBeUndefined()
const serialized = result.code.match(/code: ("(?:\\.|[^"\\])*")/)?.[1]
expect(serialized, 'serialized worklet code').toBeTruthy()
const worklet = (0, eval)(JSON.parse(serialized!))
expect(worklet).toBeTypeOf('function')
expect(worklet()).toBe(42)
})
it('ignores calls whose callee name collides with Object.prototype', async () => {
// reanimated calls .constructor(), .toString() and similar. these must not
// resolve against the auto-worklet argument map's prototype chain.
const code = `
const out = value.constructor(1)
const s = thing.toString()
const h = obj.hasOwnProperty('x')
const v = obj.valueOf()
`
const result = await transformWorklets('/app/collide.ts', code, false)
expect(result.code).not.toContain('__workletHash')
})
it('workletizes gesture callbacks only when the receiver is a Gesture chain', async () => {
// `onChange`/`onStart`/`onEnd` are ordinary identifiers in unrelated code.
// matching them by name alone ships app callbacks to the UI runtime.
const gesture = await transformWorklets(
'/app/gesture.ts',
`const g = Gesture.Pan().onStart((e) => { scale.value = e.x }).onChange((e) => { scale.value = e.y })`,
false
)
expect((gesture.code.match(/__workletHash/g) ?? []).length).toBe(2)
const notAGesture = await transformWorklets(
'/app/plain.ts',
`onChange((d) => d + scrollDiff); emitter.onStart(function () { return count })`,
false
)
expect(notAGesture.code).not.toContain('__workletHash')
})
it('workletizes withCallback only on a layout animation chain', async () => {
const layout = await transformWorklets(
'/app/layout.ts',
`const a = FadeIn.duration(300).withCallback((finished) => { done.value = finished })`,
false
)
expect(layout.code).toContain('__workletHash')
const plain = await transformWorklets(
'/app/plain-callback.ts',
`queue.withCallback((finished) => { done.value = finished })`,
false
)
expect(plain.code).not.toContain('__workletHash')
})
it('transforms closure variables into worklet parameters', async () => {
const code = `
const factor = 2
export function multiply(x: number) {
'worklet'
return x * factor
}
`
const result = await transformWorklets('/app/multiply.ts', code, false)
expect(result.code).toContain('__closure')
expect(result.code).toContain('factor')
})
it('transforms an anonymous function expression worklet', async () => {
// passed inline, so there is no variable to take a name from.
const code = `
register(function (e) {
'worklet'
return e.x
})
`
const result = await transformWorklets('/app/handler.ts', code, false)
expect(result.code).toContain('__workletHash')
expect(result.code).toMatch(/code: "\(function _worklet\(e\)/)
expect(result.code).toContain('var _worklet = function _worklet(e)')
})
it('does not capture hoisted declarations used before their declaration', async () => {
// `flushQueue` and `later` are declared below their use. hoisting makes that
// legal, so neither is a captured variable; treating them as captured puts
// undefined in the closure and the worklet throws on the UI thread.
const code = `
export function setupLoop() {
'worklet'
function nativeFlush(t: number) {
flushQueue(t)
}
globalThis.__native(nativeFlush)
function flushQueue(t: number) {
globalThis.__stamp = t
}
const start = () => later()
const later = () => start
return start
}
`
const result = await transformWorklets('/app/setupLoop.ts', code, false)
expect(result.code).toMatch(/setupLoop\.__closure = \[\s*\]/)
const init = result.code.match(/code: "\(function setupLoop[^"]*"/)
expect(init, 'serialized worklet code').toBeTruthy()
expect(init![0]).not.toContain('this.__closure')
expect(init![0]).toContain('flushQueue')
})
it('keeps Reanimated UI-runtime globals out of the JavaScript closure', async () => {
const result = await transformWorklets(
'/app/valueUnpacker.ts',
`function valueUnpacker(value) { 'worklet'; return _toString(value) }
globalThis.__unpacker = valueUnpacker`,
false,
{ pluginVersion: '3.19.1' }
)
const mainRuntime: any = {}
runInNewContext(result.code, mainRuntime)
expect(Object.keys(mainRuntime.__unpacker.__closure)).toEqual([])
const uiRuntime: any = { _toString: String }
const unpack = runInNewContext(mainRuntime.__unpacker.__initData.code, uiRuntime)
expect(unpack(42)).toBe('42')
})
it('keeps strict global configuration explicit for UI-runtime helpers', async () => {
const result = await transformWorklets(
'/app/strictGlobals.ts',
`function unpack(value) { 'worklet'; return _toString(value) }`,
false,
{ pluginVersion: '3.19.1', strictGlobal: true }
)
expect(result.code).toMatch(/__closure = \[\s*_toString/)
})
it('honors no-worklet-closure and limit-init-data-hoisting the way the worklets runtime needs', async () => {
// this is the shape react-native-worklets uses for its unpacker installers.
// the outer worklet is serialized to a string and evaluated on a worklet
// runtime with no closure bound, so it must not emit an unpacker line, and
// the inner worklet's init data must live inside it rather than at module
// scope, which does not exist over there.
const code = `
const outerModuleValue = 1
export function installUnpacker() {
'worklet'
'no-worklet-closure'
const proxy = globalThis.__proxy
proxy.register(outerModuleValue)
proxy.install(() => {
'worklet'
'limit-init-data-hoisting'
return 42
})
}
`
const result = await transformWorklets('/app/installUnpacker.ts', code, false)
const outerInit = result.code.match(/code: "\(function installUnpacker[^"]*"/)
expect(outerInit, 'outer worklet serialized code').toBeTruthy()
// no unpacker line: nothing is read off `this` on the worklet runtime.
expect(outerInit![0]).not.toContain('this.__closure')
expect(result.code).toMatch(/installUnpacker\.__closure = \[\s*\]/)
// the directives themselves are not part of the serialized body.
expect(outerInit![0]).not.toContain('no-worklet-closure')
// the inner worklet's init data is declared inside the outer function body,
// not hoisted to module scope where the worklet runtime cannot see it.
const innerVar = result.code.match(
/var (_worklet_\d+_init_data) = \{\s*code: "\(function _worklet/
)
expect(innerVar, 'inner worklet init data').toBeTruthy()
// only look past the serialized string, which mentions the same name inside
// its escaped body.
const emitted = result.code.slice(result.code.indexOf('export var installUnpacker'))
expect(emitted).toContain(`var ${innerVar![1]} = {`)
expect(
result.code.slice(0, result.code.indexOf('export var installUnpacker'))
).not.toContain(`\nvar ${innerVar![1]} = {`)
})
it('autoworkletizes hooks like useAnimatedStyle and withTiming', async () => {
const code = `
export function Card() {
const style = useAnimatedStyle(() => {
return {
opacity: withTiming(1),
}
})
return
}
`
const result = await transformWorklets('/app/Card.tsx', code, false)
expect(result.code).toContain('__workletHash')
expect(result.code).toContain('useAnimatedStyle')
expect(result.code).toContain('')
})
it('generates sourcemaps when requested', async () => {
const code = `
export const workletFn = () => {
'worklet'
return 'ok'
}
`
const result = await transformWorklets('/app/worklet.ts', code, true)
expect(result.map).toBeDefined()
expect(result.map?.mappings).toBeDefined()
})
it('workletizes published Flow packages before the pipeline strips Flow', async () => {
const code = `/**
* @flow strict-local
*/
import type { Node } from 'react'
export function animate(value: number): number {
'worklet'
return value * 2
}
export const Picker = (props: { onChange: (v: string) => void }): Node => null
`
const result = await transformWorklets(
'/app/node_modules/flow-picker/js/Picker.ios.js',
code,
true
)
expect(result.code).toContain('__workletHash')
expect(result.code).not.toContain('import type')
expect(result.map?.sources).toContain('/app/node_modules/flow-picker/js/Picker.ios.js')
const serialized = result.code.match(/code: ("(?:\\.|[^"\\])*")/)?.[1]
expect((0, eval)(JSON.parse(serialized!))(21)).toBe(42)
})
it('leaves a Flow codegen spec typed for the codegen pass', async () => {
const code = `// @flow
import type { ViewProps } from 'react-native'
import codegenNativeComponent from 'react-native/Libraries/Utilities/codegenNativeComponent'
type NativeProps = $ReadOnly<{| ...ViewProps, onChange?: ?() => void |}>
export default (codegenNativeComponent('FlowPicker'): HostComponent)
`
const result = await transformWorklets(
'/app/node_modules/flow-picker/js/FlowPickerNativeComponent.js',
code,
false
)
expect(result.code).toBe(code)
})
it('rejects on transform errors instead of returning untransformed source', async () => {
await expect(
transformWorklets(
'/app/broken.ts',
`export const fn = () => { 'worklet'; return ;;; const = }`,
false
)
).rejects.toThrow()
})
it('stamps the package runtime version into __pluginVersion to avoid runtime mismatch', async () => {
const testRoot = fs.realpathSync(
fs.mkdtempSync(path.join(os.tmpdir(), 'vxrn-worklets-ver-'))
)
const packageRoot = path.join(testRoot, 'node_modules', 'react-native-worklets')
fs.mkdirSync(packageRoot, { recursive: true })
fs.writeFileSync(
path.join(packageRoot, 'package.json'),
JSON.stringify({ name: 'react-native-worklets', version: '0.10.1' })
)
try {
const result = await transformWorklets(
path.join(testRoot, 'entry.ts'),
`export const fn = () => { 'worklet'; return 1; }`,
false,
{ projectRoot: testRoot }
)
expect(result.code).toContain('__pluginVersion = "0.10.1"')
} finally {
fs.rmSync(testRoot, { recursive: true, force: true })
}
})
it('correctly resolves hoisted react-native-worklets in a monorepo when child has reanimated', async () => {
const rootDir = fs.realpathSync(
fs.mkdtempSync(path.join(os.tmpdir(), 'vxrn-worklets-hoist-'))
)
const appDir = path.join(rootDir, 'apps', 'mobile')
const hoistedWorkletsDir = path.join(rootDir, 'node_modules', 'react-native-worklets')
const localReanimatedDir = path.join(
appDir,
'node_modules',
'react-native-reanimated'
)
fs.mkdirSync(hoistedWorkletsDir, { recursive: true })
fs.writeFileSync(
path.join(hoistedWorkletsDir, 'package.json'),
JSON.stringify({ name: 'react-native-worklets', version: '0.10.1' })
)
fs.mkdirSync(localReanimatedDir, { recursive: true })
fs.writeFileSync(
path.join(localReanimatedDir, 'package.json'),
JSON.stringify({ name: 'react-native-reanimated', version: '4.5.1' })
)
fs.writeFileSync(
path.join(appDir, 'package.json'),
JSON.stringify({ name: 'mobile' })
)
try {
const result = await transformWorklets(
path.join(appDir, 'entry.ts'),
`export function test() { 'worklet'; return 1; }`,
false,
{ projectRoot: appDir }
)
// Must stamp the worklets runtime version (0.10.1), NOT reanimated version (4.5.1)!
expect(result.code).toContain('__pluginVersion = "0.10.1"')
} finally {
fs.rmSync(rootDir, { recursive: true, force: true })
}
})
it('throws an actionable error when neither worklets nor reanimated can be resolved', async () => {
const emptyRoot = fs.realpathSync(
fs.mkdtempSync(path.join(os.tmpdir(), 'vxrn-worklets-empty-'))
)
fs.writeFileSync(
path.join(emptyRoot, 'package.json'),
JSON.stringify({ name: 'empty-test' })
)
try {
await expect(
transformWorklets(
path.join(emptyRoot, 'entry.ts'),
`export function test() { 'worklet'; return 1; }`,
false,
{ projectRoot: emptyRoot }
)
).rejects.toThrow(
/Unable to resolve "react-native-worklets" or "react-native-reanimated"/
)
} finally {
fs.rmSync(emptyRoot, { recursive: true, force: true })
}
})
it('forwards plugin options such as bundleMode and globals', async () => {
const code = `
export function testCustom() {
'worklet'
return customGlobalValue * 2
}
`
const result = await transformWorklets('/app/custom.ts', code, false, {
globals: ['customGlobalValue'],
bundleMode: false,
})
expect(result.code).toContain('__workletHash')
expect(result.code).toContain('__initData')
})
it('satisfies runtime worklet serialization, local execution, and UI-thread reconstruction', async () => {
const code = `
const multiplier = 5
export function scale(val: number) {
'worklet'
return val * multiplier
}
`
const result = await transformWorklets('/app/scale.ts', code, false)
// Evaluate the transformed code in a JS runtime context
const sandbox = { global: globalThis, exports: {} as any }
const runner = new Function(
'exports',
'global',
result.code.replace('export var scale', 'exports.scale')
)
runner(sandbox.exports, sandbox.global)
const scaleFn = sandbox.exports.scale
expect(typeof scaleFn).toBe('function')
expect(scaleFn.__workletHash).toBeDefined()
expect(typeof scaleFn.__workletHash).toBe('number')
// /app has no package.json, so the stamp comes from the worklets runtime
// this workspace actually installs
const installedWorklets = createRequire(path.resolve(process.cwd(), 'package.json'))(
'react-native-worklets/package.json'
).version
expect(scaleFn.__pluginVersion).toBe(installedWorklets)
expect(scaleFn.__closure).toEqual([5])
expect(scaleFn.__initData).toBeDefined()
expect(scaleFn.__initData.code).toContain('__closure')
// Local JS execution
expect(scaleFn(10)).toBe(50)
// UI-thread reconstruction from __initData.code + __closure
const reconstructedUIFn = eval(`(${scaleFn.__initData.code})`)
const uiResult = reconstructedUIFn.call({ __closure: scaleFn.__closure }, 10)
expect(uiResult).toBe(50)
})
it('autoworkletizes hook callbacks with closure capture and UI-thread execution parity', async () => {
const code = `
import { useAnimatedStyle } from 'react-native-reanimated'
const baseOpacity = 0.8
export function getCardStyle() {
return useAnimatedStyle(() => {
return { opacity: baseOpacity }
})
}
`
const result = await transformWorklets('/app/CardStyle.ts', code, false)
let capturedCallback: any = null
const sandbox = {
global: globalThis,
exports: {} as any,
useAnimatedStyle: (cb: any) => {
capturedCallback = cb
return cb
},
}
const runner = new Function(
'exports',
'global',
'useAnimatedStyle',
result.code
.replace(/import\s+{[^}]+}\s+from\s+['"][^'"]+['"];?/, '')
.replace(
'export function getCardStyle',
'exports.getCardStyle = function getCardStyle'
)
)
runner(sandbox.exports, sandbox.global, sandbox.useAnimatedStyle)
sandbox.exports.getCardStyle()
expect(capturedCallback).toBeDefined()
expect(capturedCallback.__workletHash).toBeDefined()
expect(capturedCallback.__closure).toEqual([0.8])
// UI thread execution of the animated style worklet
const reconstructedStyleFn = eval(`(${capturedCallback.__initData.code})`)
const styleResult = reconstructedStyleFn.call({
__closure: capturedCallback.__closure,
})
expect(styleResult).toEqual({ opacity: 0.8 })
})
it('compiles UI-thread worklet function to valid Hermes Bytecode (HBC)', async () => {
const code = `
const factor = 10
export function calculate(val: number) {
'worklet'
return val * factor
}
`
const result = await transformWorklets('/app/calculate.ts', code, false)
const match = result.code.match(/code:\s*"([^"]+)"/)
expect(match).toBeDefined()
const uiCode = JSON.parse(`"${match![1]}"`)
// Verify Hermes bytecode compilation via hermesc if available
const { execFileSync } = await import('node:child_process')
// hermes-compiler ships all three platforms' binaries, so the osx one exists
// on linux CI too and running it gets a shell syntax error rather than a
// skip. pick the one that can actually execute here.
const hermescDir =
process.platform === 'darwin'
? 'osx-bin'
: process.platform === 'win32'
? 'win64-bin'
: 'linux64-bin'
const hermescPath = path.resolve(
__dirname,
`../../../node_modules/hermes-compiler/hermesc/${hermescDir}/hermesc${
process.platform === 'win32' ? '.exe' : ''
}`
)
if (fs.existsSync(hermescPath)) {
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'hermes-worklet-test-'))
const jsPath = path.join(tempDir, 'worklet.js')
const hbcPath = path.join(tempDir, 'worklet.hbc')
try {
fs.writeFileSync(jsPath, `(${uiCode})`)
execFileSync(hermescPath, ['-emit-binary', '-out', hbcPath, jsPath])
expect(fs.existsSync(hbcPath)).toBe(true)
expect(fs.statSync(hbcPath).size).toBeGreaterThan(0)
} finally {
fs.rmSync(tempDir, { recursive: true, force: true })
}
}
})
it('transforms multiple worklets in the same file with independent closures', async () => {
const code = `
const a = 10
const b = 20
export function first() {
'worklet'
return a
}
export function second() {
'worklet'
return b
}
`
const result = await transformWorklets('/app/multiple.ts', code, false)
expect(result.code).toContain('first')
expect(result.code).toContain('second')
expect(result.code).toContain('__closure = [a]')
expect(result.code).toContain('__closure = [b]')
})
it('transforms concise arrow functions with useDerivedValue', async () => {
const code = `
import { useDerivedValue } from 'react-native-reanimated'
const multiplier = 3
export const derived = useDerivedValue(() => multiplier * 10)
`
const result = await transformWorklets('/app/derived.ts', code, false)
expect(result.code).toContain('__workletHash')
expect(result.code).toContain('multiplier')
})
it('transforms scroll handler with object methods', async () => {
const code = `
import { useAnimatedScrollHandler } from 'react-native-reanimated'
const offset = 5
export const handler = useAnimatedScrollHandler({
onScroll(event) {
'worklet'
return event.contentOffset.y + offset
}
})
`
const result = await transformWorklets('/app/handler.ts', code, false)
expect(result.code).toContain('onScroll:')
expect(result.code).toContain('__closure = [offset]')
})
it('correctly handles default and destructuring parameters in closure capture', async () => {
const code = `
const factor = 4
export function compute({ x, y = 1 }: { x: number; y?: number }, z = 0) {
'worklet'
return (x + y + z) * factor
}
`
const result = await transformWorklets('/app/destructure.ts', code, false)
expect(result.code).toContain('__closure = [factor]')
const sandbox = { global: globalThis, exports: {} as any }
const runner = new Function(
'exports',
'global',
result.code.replace('export var compute', 'exports.compute')
)
runner(sandbox.exports, sandbox.global)
const computeFn = sandbox.exports.compute
expect(computeFn({ x: 2 })).toBe(12) // (2 + 1 + 0) * 4 = 12
const reconstructed = eval(`(${computeFn.__initData.code})`)
const uiRes = reconstructed.call(
{ __closure: computeFn.__closure },
{ x: 2, y: 3 },
5
)
expect(uiRes).toBe(40) // (2 + 3 + 5) * 4 = 40
})
it('operates with 100% elimination of SWC (zero swc dependencies in transformWorklets)', async () => {
const fs = await import('node:fs')
const transformWorkletsSource = fs.readFileSync(
path.resolve(__dirname, 'transformWorklets.ts'),
'utf-8'
)
expect(transformWorkletsSource).not.toContain('@swc/core')
expect(transformWorkletsSource).not.toContain('@react-native-swc/worklets-plugin')
const result = await transformWorklets(
'/app/zeroSwc.ts',
`export function zeroSwc() { 'worklet'; return 999; }`,
false
)
expect(result.code).toContain('__workletHash')
expect(result.code).toContain('__pluginVersion')
expect(result.code).toContain('999')
})
it('correctly handles lexical block scope shadowing without losing outer bindings', async () => {
const code = `
const x = 7
export function f() {
'worklet'
{
const x = 1
}
return x
}
`
const result = await transformWorklets('/app/scope-shadow.ts', code, false)
expect(result.code).toContain('__closure = [x]')
const sandbox = { global: globalThis, exports: {} as any }
const runner = new Function(
'exports',
'global',
result.code.replace('export var f', 'exports.f')
)
runner(sandbox.exports, sandbox.global)
const f = sandbox.exports.f
expect(f()).toBe(7)
const reconstructed = eval(`(${f.__initData.code})`)
const uiRes = reconstructed.call({ __closure: f.__closure })
expect(uiRes).toBe(7)
})
it('captures the outer binding a property worklet shares its key with', async () => {
// react-native-gesture-handler's NativeProxy: the property worklet calls the
// module-level host function of the same name. naming the worklet after its
// key made it capture itself, so serializing its closure never ended.
const code = `
const updateConfig = (tag) => tag * 2
export const proxy = {
updateConfig: (tag) => {
'worklet'
return updateConfig(tag)
},
flush: function (tag) {
'worklet'
return updateConfig(tag) + 1
},
}
`
const result = await transformWorklets('/app/proxy.ts', code, false)
const sandbox = { global: globalThis, exports: {} as any }
const runner = new Function(
'exports',
'global',
result.code.replace('export const proxy', 'exports.proxy')
)
runner(sandbox.exports, sandbox.global)
const { proxy } = sandbox.exports
for (const worklet of [proxy.updateConfig, proxy.flush]) {
expect(worklet.__closure[0]).not.toBe(worklet)
const reconstructed = eval(`(${worklet.__initData.code})`)
expect(reconstructed.call({ __closure: worklet.__closure }, 3)).toBe(worklet(3))
}
expect(proxy.updateConfig(3)).toBe(6)
expect(proxy.flush(3)).toBe(7)
})
it('transforms nested worklets with bottom-up composition', async () => {
const code = `
export function outer() {
'worklet'
function inner() {
'worklet'
return 1
}
return inner()
}
`
const result = await transformWorklets('/app/nested.ts', code, false)
expect(result.code).toContain('export var outer')
expect(result.code).toContain('inner.__workletHash')
expect(result.code).toContain('outer.__workletHash')
const sandbox = { global: globalThis, exports: {} as any }
const runner = new Function(
'exports',
'global',
result.code.replace('export var outer', 'exports.outer')
)
runner(sandbox.exports, sandbox.global)
const outerFn = sandbox.exports.outer
expect(outerFn()).toBe(1)
})
it('transforms modules with >50 sibling worklets without truncation', async () => {
const fnCount = 60
const code = Array.from(
{ length: fnCount },
(_, i) => `export function f${i}() { 'worklet'; return ${i}; }`
).join('\n')
const result = await transformWorklets('/app/many.ts', code, false)
for (let i = 0; i < fnCount; i++) {
expect(result.code).toContain(`export var f${i} =`)
expect(result.code).toContain(`f${i}.__workletHash`)
}
// Verify last worklet (f59) is transformed and executable
const sandbox = { global: globalThis, exports: {} as any }
const runner = new Function(
'exports',
'global',
result.code.replace(/export var /g, 'exports.')
)
runner(sandbox.exports, sandbox.global)
expect(sandbox.exports.f0()).toBe(0)
expect(sandbox.exports.f50()).toBe(50)
expect(sandbox.exports.f59()).toBe(59)
})
it('transforms deeply nested worklets (3 levels) bottom-up', async () => {
const code = `
export function level1() {
'worklet';
function level2() {
'worklet';
function level3() {
'worklet';
return 42;
}
return level3();
}
return level2();
}
`
const result = await transformWorklets('/app/deep.ts', code, false)
expect(result.code).toContain('level3.__workletHash')
expect(result.code).toContain('level2.__workletHash')
expect(result.code).toContain('level1.__workletHash')
const sandbox = { global: globalThis, exports: {} as any }
const runner = new Function(
'exports',
'global',
result.code.replace('export var level1', 'exports.level1')
)
runner(sandbox.exports, sandbox.global)
expect(sandbox.exports.level1()).toBe(42)
})
})
describe('worklet detection gate', () => {
it('accepts files whose only worklets come from gesture callbacks', () => {
// .onBegin/.onEnd/.onTouchesMove are auto-workletized by the transform, so
// a gate that misses them ships those callbacks untransformed.
configureVXRNCompilerPlugin({ enableReanimated: true, enableNativeWorklets: true })
const code = `
const tap = Gesture.Tap().onBegin((e) => { setState(e) }).onTouchesMove((e) => {}).onEnd(() => {})
`
expect(shouldTransformWorklets({ id: '/app/gesture.native.js', code })).toBe(true)
})
})