import fs from 'node:fs' import { createRequire } from 'node:module' import path from 'node:path' import { configuration, isNativeWorkletsEnabled } from './configure' import { AUTOWORKLET_FUNCTION_ARGS, GESTURE_BUILDER_METHODS, } from './worklets/autoworklet' // every callee the transform auto-workletizes has to be in this gate, or files // whose only worklets come from gesture callbacks (`.onBegin`, `.onEnd`, // `.onTouchesMove`) are skipped before the transform ever sees them and ship // untransformed. export const REANIMATED_AUTOWORKLETIZATION_KEYWORDS = [ 'worklet', 'useAnimatedGestureHandler', 'useAnimatedScrollHandler', 'useFrameCallback', 'useAnimatedStyle', 'useAnimatedProps', 'createAnimatedPropAdapter', 'useDerivedValue', 'useAnimatedReaction', 'useWorkletCallback', 'withTiming', 'withSpring', 'withDecay', 'withRepeat', 'runOnUI', 'executeOnUIRuntimeSync', ] const REANIMATED_REGEX = new RegExp( [ ...new Set([ ...REANIMATED_AUTOWORKLETIZATION_KEYWORDS, ...Object.keys(AUTOWORKLET_FUNCTION_ARGS), // gesture callbacks are only workletized on a `Gesture.()` chain, // but this file gate has to be permissive or a file whose only worklets // are gesture callbacks never reaches the transform at all. ...GESTURE_BUILDER_METHODS, 'withCallback', ]), ] .map((name) => `\\b${name}\\b`) .join('|') ) const REANIMATED_IGNORED_PATHS = [ 'react-native-prebuilt', 'node_modules/.vxrn/react-native', 'node_modules/react/', 'node_modules/react-dom/', 'node_modules/react-native/', // react-native's own scoped packages are flow, and the worklets transform // runs before flow stripping in the vite pipeline. they never hold app // worklets, exactly like node_modules/react-native/ above. 'node_modules/@react-native/', 'node_modules/react-native-web/', ] const REANIMATED_IGNORED_PATHS_REGEX = new RegExp(REANIMATED_IGNORED_PATHS.join('|')) export function shouldTransformWorklets({ id, code }: { id: string; code: string }) { if (!isNativeWorkletsEnabled() || !configuration.enableReanimated) { return false } const normalizedId = id.replace(/\\/g, '/') if (REANIMATED_IGNORED_PATHS_REGEX.test(normalizedId)) { return false } return REANIMATED_REGEX.test(code) } export function getWorkletsVersion(projectRoot?: string, filename?: string): string { if (projectRoot) { try { const req = createRequire(path.resolve(projectRoot, 'package.json')) // Look for react-native-worklets first (preferred in modern Worklets / Reanimated 4) try { const ver = req('react-native-worklets/package.json')?.version if (ver) return ver } catch {} // Fall back to react-native-reanimated (Reanimated 3 where worklets is bundled inside) try { const ver = req('react-native-reanimated/package.json')?.version if (ver) return ver } catch {} } catch {} throw new Error( `[@vxrn/compiler] Unable to resolve "react-native-worklets" or "react-native-reanimated" version from project root "${projectRoot}". ` + `Please ensure react-native-worklets is installed in your project.` ) } // If no projectRoot, try resolving from filename if it exists on disk if (filename && !filename.startsWith('\0') && fs.existsSync(filename)) { try { const req = createRequire(filename) try { const ver = req('react-native-worklets/package.json')?.version if (ver) return ver } catch {} try { const ver = req('react-native-reanimated/package.json')?.version if (ver) return ver } catch {} } catch {} } // Fall back to process.cwd() or import.meta.url for synthetic / dev environments const fallbacks = [ () => createRequire(path.resolve(process.cwd(), 'package.json')), () => createRequire(import.meta.url), ] for (const getReq of fallbacks) { try { const req = getReq() try { const ver = req('react-native-worklets/package.json')?.version if (ver) return ver } catch {} try { const ver = req('react-native-reanimated/package.json')?.version if (ver) return ver } catch {} } catch {} } const target = filename || process.cwd() throw new Error( `[@vxrn/compiler] Unable to resolve "react-native-worklets" or "react-native-reanimated" version from "${target}". ` + `Please ensure react-native-worklets is installed in your project.` ) } export type TransformWorkletsOptions = { projectRoot?: string bundleMode?: boolean disableInlineStylesWarning?: boolean disableSourceMaps?: boolean disableWorkletClasses?: boolean globals?: string[] relativeSourceLocation?: boolean strictGlobal?: boolean pluginVersion?: string } import remapping from '@jridgewell/remapping' import { isReactNativeCodegenSpec } from './reactNativeCodegen' import { stripFlowTypes } from './transformFlow' import { shouldStripFlow } from './transformSWC' import { executeWorkletTransform } from './worklets/transform' export { prepareWorkletsForReactCompiler } from './worklets/transform' export async function transformWorklets( id: string, code: string, sourceMaps = false, options?: TransformWorkletsOptions ): Promise<{ code: string; map?: any }> { // published Flow packages reach this before the pipeline's Flow strip, and the // worklet parser has no Flow support. a codegen spec keeps its types for the // codegen pass after this one and never holds worklets. if (shouldStripFlow(id, code)) { if (isReactNativeCodegenSpec(id, code)) return { code } const stripped = await stripFlowTypes(id, code, sourceMaps) const result = executeWorkletTransform( id, stripped.code, sourceMaps, options, getWorkletsVersion ) if (!sourceMaps || !stripped.map) return result return { code: result.code, map: result.map ? remapping([result.map, stripped.map], () => null) : stripped.map, } } return executeWorkletTransform(id, code, sourceMaps, options, getWorkletsVersion) }