@vxrn/compiler 2.0.0-beta.98.1 → 2.6.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 (57) hide show
  1. package/dist/cjs/index.cjs +10 -1
  2. package/dist/cjs/reactNativeViewConfig.cjs +1 -1
  3. package/dist/cjs/transformBabel.cjs +15 -57
  4. package/dist/cjs/transformFlow.cjs +87 -0
  5. package/dist/cjs/transformHermesClasses.cjs +58 -0
  6. package/dist/cjs/transformSWC.cjs +3 -4
  7. package/dist/cjs/workletUtils.cjs +30 -0
  8. package/dist/cjs/worklets/autoworklet.cjs +2 -2
  9. package/dist/cjs/worklets/globals.cjs +22 -1
  10. package/dist/esm/index.js +12 -1
  11. package/dist/esm/index.js.map +1 -1
  12. package/dist/esm/index.mjs +12 -1
  13. package/dist/esm/index.mjs.map +1 -1
  14. package/dist/esm/reactNativeViewConfig.mjs +1 -1
  15. package/dist/esm/reactNativeViewConfig.mjs.map +1 -1
  16. package/dist/esm/transformBabel.mjs +17 -58
  17. package/dist/esm/transformBabel.mjs.map +1 -1
  18. package/dist/esm/transformFlow.mjs +62 -0
  19. package/dist/esm/transformFlow.mjs.map +1 -0
  20. package/dist/esm/transformHermesClasses.mjs +32 -0
  21. package/dist/esm/transformHermesClasses.mjs.map +1 -0
  22. package/dist/esm/transformSWC.mjs +3 -4
  23. package/dist/esm/transformSWC.mjs.map +1 -1
  24. package/dist/esm/workletUtils.mjs +5 -0
  25. package/dist/esm/worklets/autoworklet.mjs +2 -2
  26. package/dist/esm/worklets/autoworklet.mjs.map +1 -1
  27. package/dist/esm/worklets/globals.mjs +22 -1
  28. package/dist/esm/worklets/globals.mjs.map +1 -1
  29. package/package.json +12 -4
  30. package/src/index.ts +12 -1
  31. package/src/reactNativeViewConfig.ts +7 -2
  32. package/src/transformBabel.test.ts +61 -37
  33. package/src/transformBabel.ts +28 -92
  34. package/src/transformFlow.test.ts +134 -0
  35. package/src/transformFlow.ts +68 -0
  36. package/src/transformHermesClasses.ts +34 -0
  37. package/src/transformSWC.test.ts +33 -1
  38. package/src/transformSWC.ts +14 -4
  39. package/src/transformWorklets.test.ts +64 -0
  40. package/src/workletUtils.ts +4 -0
  41. package/src/worklets/autoworklet.ts +5 -2
  42. package/src/worklets/globals.ts +23 -0
  43. package/types/index.d.ts +2 -0
  44. package/types/index.d.ts.map +1 -1
  45. package/types/reactNativeViewConfig.d.ts +1 -1
  46. package/types/reactNativeViewConfig.d.ts.map +1 -1
  47. package/types/transformBabel.d.ts +0 -12
  48. package/types/transformBabel.d.ts.map +1 -1
  49. package/types/transformFlow.d.ts +5 -0
  50. package/types/transformFlow.d.ts.map +1 -0
  51. package/types/transformHermesClasses.d.ts +5 -0
  52. package/types/transformHermesClasses.d.ts.map +1 -0
  53. package/types/transformSWC.d.ts.map +1 -1
  54. package/types/workletUtils.d.ts +4 -0
  55. package/types/workletUtils.d.ts.map +1 -0
  56. package/types/worklets/autoworklet.d.ts.map +1 -1
  57. package/types/worklets/globals.d.ts.map +1 -1
@@ -1,5 +1,5 @@
1
1
  import { existsSync, readFileSync } from 'node:fs'
2
- import { extname, join, relative } from 'node:path'
2
+ import { extname, join } from 'node:path'
3
3
  // type-only, so that importing this module does not drag babel in. every metro
4
4
  // worker loads it through the package index, and on the native transform path
5
5
  // babel is never called at all: loading it there is pure startup cost.
@@ -66,6 +66,7 @@ export function getBabelOptions(props: Props): babel.TransformOptions | null {
66
66
  ) {
67
67
  if (props.userSetting?.excludeDefaultPlugins) {
68
68
  return {
69
+ caller: getBabelCaller(props),
69
70
  ...props.userSetting,
70
71
  ...(userBabelConfig ? { configFile: userBabelConfig, babelrc: true } : {}),
71
72
  }
@@ -116,17 +117,6 @@ const getOptions = (
116
117
  }
117
118
  }
118
119
 
119
- if (enableNativewind || shouldBabelReanimated(props)) {
120
- try {
121
- const workletsPlugin = resolvePath(
122
- 'react-native-worklets/plugin',
123
- props.projectRoot
124
- )
125
- debug?.(`Using babel worklets on file ${props.id}`)
126
- plugins.push(workletsPlugin)
127
- } catch {}
128
- }
129
-
130
120
  if (shouldBabelReactCompiler(props)) {
131
121
  debug?.(`Using babel react compiler on file`)
132
122
  plugins.push(getBabelReactCompilerPlugin(props))
@@ -135,6 +125,7 @@ const getOptions = (
135
125
  if (plugins.length || userBabelConfig) {
136
126
  return {
137
127
  plugins,
128
+ caller: getBabelCaller(props),
138
129
  ...(userBabelConfig ? { configFile: userBabelConfig, babelrc: true } : {}),
139
130
  }
140
131
  }
@@ -142,6 +133,21 @@ const getOptions = (
142
133
  return null
143
134
  }
144
135
 
136
+ function getBabelCaller(props: Props): babel.TransformOptions['caller'] {
137
+ // babel hands every caller field to presets (babel-preset-expo reads platform
138
+ // and isDev), but its types list only the esm support flags, so the extra
139
+ // fields go through a named value rather than a checked literal.
140
+ const caller = {
141
+ name: 'vxrn',
142
+ platform:
143
+ props.environment === 'ios' || props.environment === 'android'
144
+ ? props.environment
145
+ : 'web',
146
+ isDev: props.development,
147
+ }
148
+ return caller
149
+ }
150
+
145
151
  /**
146
152
  * Run the react compiler through oxc's rust port instead of babel.
147
153
  *
@@ -216,7 +222,7 @@ export async function transformBabel(
216
222
  const extension = extname(id)
217
223
  const isTSX = extension === '.tsx'
218
224
  const isTS = isTSX || extension === '.ts'
219
- const babelOptions = {
225
+ const babelOptions: babel.TransformOptions = {
220
226
  filename: id,
221
227
  compact: false,
222
228
  babelrc: options.babelrc ?? false,
@@ -224,10 +230,19 @@ export async function transformBabel(
224
230
  sourceMaps: false,
225
231
  minified: false,
226
232
  ...options,
233
+ ...(!isTS
234
+ ? {
235
+ parserOpts: {
236
+ ...options.parserOpts,
237
+ plugins: [...(options.parserOpts?.plugins || []), 'jsx'],
238
+ },
239
+ }
240
+ : {}),
227
241
  // vite and rolldown own module syntax and import.meta, so presets written for
228
242
  // metro (babel-preset-expo) must keep esm instead of rewriting it for metro's runtime
229
243
  caller: {
230
244
  name: 'vxrn',
245
+ ...options.caller,
231
246
  supportsStaticESM: true,
232
247
  supportsDynamicImport: true,
233
248
  },
@@ -273,14 +288,6 @@ export async function transformBabel(
273
288
  })
274
289
  }
275
290
 
276
- export async function stripFlowTypes(id: string, code: string, sourceMaps = true) {
277
- const result = await transformBabel(id, code, { sourceMaps })
278
- return {
279
- code: result.code!,
280
- map: result.map,
281
- }
282
- }
283
-
284
291
  const getBasePlugins = ({ development }: Props) =>
285
292
  [
286
293
  ['@babel/plugin-transform-destructuring'],
@@ -391,74 +398,3 @@ function shouldBabelGenerators({ code }: Props) {
391
398
  return asyncGeneratorRegex.test(code)
392
399
  }
393
400
  }
394
-
395
- /**
396
- * ------- reanimated --------
397
- */
398
-
399
- /**
400
- * Taken from https://github.com/software-mansion/react-native-reanimated/blob/3.15.1/packages/react-native-reanimated/plugin/src/autoworkletization.ts#L19-L59, need to check if this is up-to-date when supporting newer versions of react-native-reanimated.
401
- */
402
- const REANIMATED_AUTOWORKLETIZATION_KEYWORDS = [
403
- 'worklet',
404
- 'useAnimatedGestureHandler',
405
- 'useAnimatedScrollHandler',
406
- 'useFrameCallback',
407
- 'useAnimatedStyle',
408
- 'useAnimatedProps',
409
- 'createAnimatedPropAdapter',
410
- 'useDerivedValue',
411
- 'useAnimatedReaction',
412
- 'useWorkletCallback',
413
- 'withTiming',
414
- 'withSpring',
415
- 'withDecay',
416
- 'withRepeat',
417
- 'runOnUI',
418
- 'executeOnUIRuntimeSync',
419
- ]
420
-
421
- /**
422
- * Regex to test if a piece of code should be processed by react-native-reanimated's Babel plugin.
423
- */
424
- const REANIMATED_REGEX = new RegExp(REANIMATED_AUTOWORKLETIZATION_KEYWORDS.join('|'))
425
-
426
- // Packages to skip for reanimated babel transform
427
- // These either have false positives (mention keywords but don't use worklets)
428
- // or cause issues when transformed
429
- const REANIMATED_IGNORED_PATHS = [
430
- // Prebuilt/vendored react-native that shouldn't be transformed
431
- 'react-native-prebuilt',
432
- 'node_modules/.vxrn/react-native',
433
- // Known false positives - they mention worklet keywords in comments/strings but don't use them
434
- 'node_modules/react/',
435
- 'node_modules/react-dom/',
436
- 'node_modules/react-native/',
437
- 'node_modules/react-native-web/',
438
- ]
439
-
440
- // `id`s are normalized to forward slashes at getBabelOptions' entry, so these
441
- // plain forward-slash paths match on every OS. before that normalization the
442
- // backslash `id`s Windows hands us slipped past this list, pushing react-native's
443
- // own files through the reanimated babel pass, which has no JSX/TS parser.
444
- const REANIMATED_IGNORED_PATHS_REGEX = new RegExp(REANIMATED_IGNORED_PATHS.join('|'))
445
-
446
- function shouldBabelReanimated({ code, id }: Props) {
447
- if (!configuration.enableReanimated) {
448
- return false
449
- }
450
-
451
- // Check if path should be ignored
452
- if (REANIMATED_IGNORED_PATHS_REGEX.test(id)) {
453
- return false
454
- }
455
-
456
- // Check regex for all files (both node_modules and user code)
457
- if (REANIMATED_REGEX.test(code)) {
458
- const location = id.includes('node_modules') ? 'node_modules' : 'user-code'
459
- debug?.(` 🪄 [reanimated/${location}] ${relative(process.cwd(), id)}`)
460
- return true
461
- }
462
-
463
- return false
464
- }
@@ -0,0 +1,134 @@
1
+ import { createRequire } from 'node:module'
2
+ import { runInNewContext } from 'node:vm'
3
+ import { originalPositionFor, TraceMap } from '@jridgewell/trace-mapping'
4
+ import { parseSync } from 'oxc-parser'
5
+ import { transformSync } from 'oxc-transform'
6
+ import { describe, expect, it, vi } from 'vitest'
7
+ import { transformBabel } from './transformBabel'
8
+ import { stripFlowTypes } from './transformFlow'
9
+ import { transformHermesClasses } from './transformHermesClasses'
10
+
11
+ vi.mock('@babel/core', () => ({
12
+ default: {
13
+ transform() {
14
+ throw new Error('babel reached native transform')
15
+ },
16
+ transformAsync() {
17
+ throw new Error('babel reached native transform')
18
+ },
19
+ },
20
+ }))
21
+
22
+ const require = createRequire(import.meta.url)
23
+
24
+ describe('native transforms preserve Flow runtime behavior without Babel', () => {
25
+ it('rejects the old Babel path as its negative control', async () => {
26
+ await expect(transformBabel('control.js', 'const value = 1', {})).rejects.toThrow(
27
+ 'babel reached native transform'
28
+ )
29
+ })
30
+
31
+ it.each([
32
+ 'enum State { Ready = 1, Busy = 2 }',
33
+ 'enum State { Ready, Busy }',
34
+ 'enum State of symbol { Ready, Busy }',
35
+ 'enum State of boolean { Ready = true, Busy = false }',
36
+ ])(
37
+ 'preserves enum methods, readonly types, defaults and mappings: %s',
38
+ async (enumSource) => {
39
+ const source = [
40
+ '// @flow',
41
+ 'type Input = { readonly value: number };',
42
+ enumSource,
43
+ 'function value(input: Input = {value: 42}): number {',
44
+ ' return input.value;',
45
+ '}',
46
+ 'globalThis.result = { value: value(), State };',
47
+ ].join('\n')
48
+ const result = await stripFlowTypes('flow.js', source)
49
+ expect(parseSync('flow.js', result.code).errors).toEqual([])
50
+ const context: any = { require }
51
+ runInNewContext(result.code, context)
52
+ expect(context.result.value).toBe(42)
53
+ const { State } = context.result
54
+ expect(State.isValid(State.Ready)).toBe(true)
55
+ expect(State.isValid('invalid')).toBe(false)
56
+ expect(State.cast(State.Busy)).toBe(State.Busy)
57
+ expect(State.getName(State.Ready)).toBe('Ready')
58
+ expect(Array.from(State.members())).toEqual([State.Ready, State.Busy])
59
+ expect(Object.keys(State)).toEqual([])
60
+ expect(Object.isFrozen(State)).toBe(true)
61
+ const lines = result.code.split('\n')
62
+ const line = lines.findIndex((text) => text.includes('input.value'))
63
+ const position = originalPositionFor(new TraceMap(result.map), {
64
+ line: line + 1,
65
+ column: lines[line].indexOf('input.value'),
66
+ })
67
+ expect(position.source).toBe('flow.js')
68
+ expect(position.line).toBe(5)
69
+ expect(result.map.sourcesContent).toEqual([source])
70
+ }
71
+ )
72
+
73
+ it('retains named and default enum exports and JSX for the native bundler', async () => {
74
+ for (const prefix of ['export', 'export default']) {
75
+ const result = await stripFlowTypes(
76
+ 'View.js',
77
+ `${prefix} enum State {Ready, Busy}
78
+ export function View(props: {value: string}) { return <NativeView {...props} /> }`
79
+ )
80
+ expect(parseSync('View.jsx', result.code, { lang: 'jsx' }).errors).toEqual([])
81
+ }
82
+ })
83
+
84
+ it('erases Flow field declarations before Hermes class lowering to preserve prototype constants', async () => {
85
+ const source = `class Event {
86
+ static +NONE: 0;
87
+ +NONE: 0;
88
+ value: number = 42;
89
+ }
90
+ Object.defineProperty(Event, 'NONE', {value: 0});
91
+ Object.defineProperty(Event.prototype, 'NONE', {value: 0});
92
+ globalThis.result = new Event();
93
+ globalThis.Event = Event;`
94
+ const stripped = await stripFlowTypes('Event.js', source)
95
+ const fields = transformSync('Event.js', stripped.code, {
96
+ target: 'es2020',
97
+ assumptions: { setPublicClassFields: true },
98
+ })
99
+ expect(fields.errors).toEqual([])
100
+ const lowered = await transformHermesClasses(fields.code, 'Event.js', true)
101
+ expect(lowered).toBeDefined()
102
+ const context: any = {}
103
+ runInNewContext(lowered!.code, context)
104
+ expect(context.result.NONE).toBe(0)
105
+ expect(context.Event.NONE).toBe(0)
106
+ expect(Object.hasOwn(context.result, 'NONE')).toBe(false)
107
+ expect(context.result.value).toBe(42)
108
+ })
109
+
110
+ it('lowers class hierarchies and parameter shadowing without Babel', async () => {
111
+ const result = await transformHermesClasses(
112
+ `class Base { value() { return 2 } }
113
+ class Derived extends Base {
114
+ constructor(value = 'default', ...rest) { super(); this.params = [value, ...rest] }
115
+ value() { return super.value() + 3 }
116
+ }
117
+ function read(config) { if (true) { const config = 7; globalThis.inner = config } return config.id }
118
+ globalThis.result = [new Derived().value(), read({id: 'device'})];
119
+ globalThis.params = new Derived(undefined, 'rest-a', 'rest-b').params;
120
+ globalThis.run = async (value = 2n) => await Promise.resolve(value);
121
+ globalThis.pending = globalThis.run();`,
122
+ 'classes.js',
123
+ true
124
+ )
125
+ expect(result).toBeDefined()
126
+ const context: any = {}
127
+ runInNewContext(result!.code, context)
128
+ expect(Array.from(context.result)).toEqual([5, 'device'])
129
+ expect(context.inner).toBe(7)
130
+ expect(Array.from(context.params)).toEqual(['default', 'rest-a', 'rest-b'])
131
+ expect(await context.pending).toBe(2n)
132
+ expect(context.run.constructor.name).toBe('AsyncFunction')
133
+ })
134
+ })
@@ -0,0 +1,68 @@
1
+ import remapping from '@jridgewell/remapping'
2
+ import { generate } from 'astring'
3
+ import { parse, SimpleTraverser, Transforms } from 'hermes-parser'
4
+ import MagicString from 'magic-string'
5
+
6
+ export async function stripFlowTypes(id: string, code: string, sourceMaps = true) {
7
+ // swc represents flow enums as typescript enums. hermes owns their distinct
8
+ // runtime semantics; lower only those declarations before native stripping.
9
+ const edited = new MagicString(code)
10
+ if (/\benum\b/.test(code)) {
11
+ const program = parse(code, { flow: 'all', sourceType: 'module' })
12
+ SimpleTraverser.traverse(program, {
13
+ enter(node, parent) {
14
+ if (node.type !== 'EnumDeclaration') return
15
+ const declaration = parent?.type === 'ExportDefaultDeclaration' ? parent : node
16
+ const [start, end] = declaration.range
17
+ const lowered = Transforms.transformEnumSyntax(
18
+ { ...program, body: [declaration] },
19
+ {}
20
+ )
21
+ edited.overwrite(start, end, generate(lowered).trimEnd())
22
+ },
23
+ leave() {},
24
+ })
25
+ }
26
+ const { transform } = await import('@swc/core')
27
+ const result = await transform(edited.toString(), {
28
+ filename: id,
29
+ swcrc: false,
30
+ configFile: false,
31
+ inputSourceMap: false,
32
+ sourceMaps,
33
+ jsc: {
34
+ target: 'esnext',
35
+ parser: {
36
+ syntax: 'flow',
37
+ all: true,
38
+ jsx: true,
39
+ enums: true,
40
+ components: true,
41
+ patternMatching: true,
42
+ },
43
+ // match Flow's declaration erasure before the native class-field pass.
44
+ transform: {
45
+ react: { runtime: 'preserve' },
46
+ useDefineForClassFields: false,
47
+ },
48
+ },
49
+ })
50
+ const map = result.map ? JSON.parse(result.map) : undefined
51
+ return {
52
+ code: result.code,
53
+ map:
54
+ map && edited.hasChanged()
55
+ ? remapping(
56
+ [
57
+ map,
58
+ edited.generateMap({
59
+ source: id,
60
+ includeContent: true,
61
+ hires: true,
62
+ }),
63
+ ],
64
+ () => null
65
+ )
66
+ : map,
67
+ }
68
+ }
@@ -0,0 +1,34 @@
1
+ // hermes before v1 cannot parse classes. swc lowers the complete hierarchy
2
+ // together with lexical bindings after oxc's field and syntax transforms.
3
+ export async function transformHermesClasses(
4
+ code: string,
5
+ filename: string,
6
+ sourceMaps = false
7
+ ): Promise<{ code: string; map?: any } | undefined> {
8
+ if (!/\b(?:class(?:\s|\{)|(?:let|const)\s)/.test(code)) return
9
+ const { transform } = await import('@swc/core')
10
+ const result = await transform(code, {
11
+ filename,
12
+ swcrc: false,
13
+ configFile: false,
14
+ sourceMaps,
15
+ // the caller composes stage maps itself; a file's own sourceMappingURL
16
+ // composed here would give the stage map several sources
17
+ inputSourceMap: false,
18
+ isModule: 'unknown',
19
+ jsc: {
20
+ parser: { syntax: 'ecmascript', jsx: true },
21
+ transform: { react: { runtime: 'preserve' } },
22
+ },
23
+ // class lowering emits strict constructors, so their defaults and rest
24
+ // parameters must be lowered with them; async lowering belongs to its stage.
25
+ env: {
26
+ include: ['transform-classes', 'transform-block-scoping', 'transform-parameters'],
27
+ },
28
+ })
29
+ if (!result?.code) return
30
+ return {
31
+ code: result.code,
32
+ map: result.map ? JSON.parse(result.map) : undefined,
33
+ }
34
+ }
@@ -1,7 +1,39 @@
1
1
  import { describe, expect, it } from 'vitest'
2
2
  import { runInNewContext } from 'node:vm'
3
3
  import { rolldown } from 'rolldown'
4
- import { transformOxc, transformSWC, transformSWCStripJSX } from './transformSWC'
4
+ import {
5
+ shouldStripFlow,
6
+ transformOxc,
7
+ transformSWC,
8
+ transformSWCStripJSX,
9
+ } from './transformSWC'
10
+
11
+ describe('Flow stripping keeps generated JavaScript on the native parser path', () => {
12
+ it.each([
13
+ [
14
+ '/app/node_modules/react-native/Libraries/Renderer/generated.js',
15
+ 'module.exports = { value: 42 }',
16
+ ],
17
+ [
18
+ '/app/node_modules/react-devtools-core/backend.js',
19
+ 'const label = "function value(x: string)"; module.exports = label',
20
+ ],
21
+ [
22
+ '/app/NativeView.js',
23
+ '/* @flow */ export function View() { return <NativeView /> }',
24
+ ],
25
+ ])('preserves JavaScript accepted by the native parser: %s', (id, code) => {
26
+ expect(shouldStripFlow(id!, code!)).toBe(false)
27
+ })
28
+
29
+ it.each([
30
+ 'export function value(x: number): number { return x + 1 }',
31
+ 'export type Value = { value: number }; export const value: Value = { value: 1 }',
32
+ 'enum Status { Ready, Waiting }; export const ready = Status.Ready',
33
+ ])('retains stripping for actual Flow syntax: %s', (code) => {
34
+ expect(shouldStripFlow('/app/node_modules/react-native/Flow.js', code)).toBe(true)
35
+ })
36
+ })
5
37
 
6
38
  describe('transformSWC / transformOxc with oxc-transform for Hermes lowering', () => {
7
39
  it('preserves unbraced for-await bodies and early returns', async () => {
@@ -1,4 +1,5 @@
1
1
  import { extname } from 'node:path'
2
+ import { parseSync } from 'oxc-parser'
2
3
  import { normalizePath } from 'vite'
3
4
  import { configuration } from './configure'
4
5
  import { asyncGeneratorRegex, debug, runtimePublicPath } from './constants'
@@ -15,14 +16,23 @@ const ignoreId = /node_modules\/(\.vite|vite)\//
15
16
  export function shouldStripFlow(id: string, code: string): boolean {
16
17
  const filename = id.split('?')[0]
17
18
  if (!/\.jsx?$/.test(filename)) return false
18
- if (/node_modules[\\/](?:react-native|@react-native)[\\/].*\.js$/.test(filename)) {
19
- return true
20
- }
21
19
  // scan the complete comment preamble, including license blocks before @flow.
22
20
  const header =
23
21
  code.match(/^(?:#![^\r\n]*(?:\r?\n|$))?(?:\s|\/\*[\s\S]*?\*\/|\/\/[^\r\n]*)*/)?.[0] ||
24
22
  ''
25
- return /@flow\b/.test(header) || /\b(?:import|export)\s+type\b/.test(code)
23
+ const candidate =
24
+ /node_modules[\\/](?:react-native|@react-native)[\\/].*\.js$/.test(filename) ||
25
+ /@flow\b/.test(header) ||
26
+ /\b(?:import|export)\s+type\b/.test(code) ||
27
+ // Some published Flow sources omit the pragma and use ordinary imports.
28
+ // Typed function returns, declarations, and class fields still need stripping.
29
+ /\(\s*(?:\.\.\.)?[\w$]+\??\s*:\s*/.test(code) ||
30
+ /\)\s*:\s*(?:[\w$]+(?:<[^;{}]*>)?(?:\[\])?\s*)(?:=>|\{)/.test(code) ||
31
+ /^\s*[\w$]+\??\s*:\s*(?:string|number|boolean|any|mixed|void|[A-Z][\w$]*)(?:[^;\r\n=]*)(?:=[^;\r\n]*)?;/m.test(
32
+ code
33
+ ) ||
34
+ /\b(?:opaque\s+)?type\s+[\w$]+(?:\s*<[^;{}]*>)?\s*=/.test(code)
35
+ return candidate && parseSync(filename, code, { lang: 'jsx' }).errors.length > 0
26
36
  }
27
37
 
28
38
  export async function transformSWC(
@@ -2,6 +2,7 @@ import fs from 'node:fs'
2
2
  import os from 'node:os'
3
3
  import { createRequire } from 'node:module'
4
4
  import path from 'node:path'
5
+ import { runInNewContext } from 'node:vm'
5
6
  import { afterEach, describe, expect, it } from 'vitest'
6
7
  import { configureVXRNCompilerPlugin } from './configure'
7
8
  import { shouldTransformWorklets, transformWorklets } from './transformWorklets'
@@ -174,6 +175,32 @@ describe('transformWorklets', () => {
174
175
  expect(init![0]).toContain('flushQueue')
175
176
  })
176
177
 
178
+ it('keeps Reanimated UI-runtime globals out of the JavaScript closure', async () => {
179
+ const result = await transformWorklets(
180
+ '/app/valueUnpacker.ts',
181
+ `function valueUnpacker(value) { 'worklet'; return _toString(value) }
182
+ globalThis.__unpacker = valueUnpacker`,
183
+ false,
184
+ { pluginVersion: '3.19.1' }
185
+ )
186
+ const mainRuntime: any = {}
187
+ runInNewContext(result.code, mainRuntime)
188
+ expect(Object.keys(mainRuntime.__unpacker.__closure)).toEqual([])
189
+ const uiRuntime: any = { _toString: String }
190
+ const unpack = runInNewContext(mainRuntime.__unpacker.__initData.code, uiRuntime)
191
+ expect(unpack(42)).toBe('42')
192
+ })
193
+
194
+ it('keeps strict global configuration explicit for UI-runtime helpers', async () => {
195
+ const result = await transformWorklets(
196
+ '/app/strictGlobals.ts',
197
+ `function unpack(value) { 'worklet'; return _toString(value) }`,
198
+ false,
199
+ { pluginVersion: '3.19.1', strictGlobal: true }
200
+ )
201
+ expect(result.code).toMatch(/__closure = \{\s*_toString/)
202
+ })
203
+
177
204
  it('honors no-worklet-closure and limit-init-data-hoisting the way the worklets runtime needs', async () => {
178
205
  // this is the shape react-native-worklets uses for its unpacker installers.
179
206
  // the outer worklet is serialized to a string and evaluated on a worklet
@@ -624,6 +651,43 @@ describe('transformWorklets', () => {
624
651
  expect(uiRes).toBe(7)
625
652
  })
626
653
 
654
+ it('captures the outer binding a property worklet shares its key with', async () => {
655
+ // react-native-gesture-handler's NativeProxy: the property worklet calls the
656
+ // module-level host function of the same name. naming the worklet after its
657
+ // key made it capture itself, so serializing its closure never ended.
658
+ const code = `
659
+ const updateConfig = (tag) => tag * 2
660
+ export const proxy = {
661
+ updateConfig: (tag) => {
662
+ 'worklet'
663
+ return updateConfig(tag)
664
+ },
665
+ flush: function (tag) {
666
+ 'worklet'
667
+ return updateConfig(tag) + 1
668
+ },
669
+ }
670
+ `
671
+ const result = await transformWorklets('/app/proxy.ts', code, false)
672
+
673
+ const sandbox = { global: globalThis, exports: {} as any }
674
+ const runner = new Function(
675
+ 'exports',
676
+ 'global',
677
+ result.code.replace('export const proxy', 'exports.proxy')
678
+ )
679
+ runner(sandbox.exports, sandbox.global)
680
+ const { proxy } = sandbox.exports
681
+
682
+ for (const worklet of [proxy.updateConfig, proxy.flush]) {
683
+ expect(worklet.__closure.updateConfig).not.toBe(worklet)
684
+ const reconstructed = eval(`(${worklet.__initData.code})`)
685
+ expect(reconstructed.call({ __closure: worklet.__closure }, 3)).toBe(worklet(3))
686
+ }
687
+ expect(proxy.updateConfig(3)).toBe(6)
688
+ expect(proxy.flush(3)).toBe(7)
689
+ })
690
+
627
691
  it('transforms nested worklets with bottom-up composition', async () => {
628
692
  const code = `
629
693
  export function outer() {
@@ -0,0 +1,4 @@
1
+ // compiler hosts can load capture analysis without importing Vite.
2
+ export { getClosureVariables } from './worklets/scope'
3
+ export { JS_GLOBALS } from './worklets/globals'
4
+ export { transformHermesLoops } from './transformHermesLoops'
@@ -306,7 +306,10 @@ export function findWorkletCandidates(program: any): WorkletCandidate[] {
306
306
  : fn.type === 'ArrowFunctionExpression'
307
307
  ? 'arrow_function'
308
308
  : 'function_expression',
309
- name: node.key?.name,
309
+ // only a method is named by its key, as upstream's plugin does: a
310
+ // property's function has no binding of that name, so naming it would
311
+ // shadow an outer binding it closes over with the worklet itself.
312
+ name: node.method ? node.key?.name : fn.id?.name,
310
313
  parent,
311
314
  isAutoWorklet: false,
312
315
  })
@@ -368,7 +371,7 @@ export function findWorkletCandidates(program: any): WorkletCandidate[] {
368
371
  prop.value.type === 'ArrowFunctionExpression'
369
372
  ? 'arrow_function'
370
373
  : 'function_expression',
371
- name: prop.key?.name,
374
+ name: prop.value.id?.name,
372
375
  parent: prop,
373
376
  isAutoWorklet: true,
374
377
  })
@@ -108,6 +108,29 @@ export const DEFAULT_GLOBALS = new Set([
108
108
  'clearInterval',
109
109
  'HermesInternal',
110
110
  '_WORKLET',
111
+ // Reanimated installs these in the UI runtime. Capturing them on the JS
112
+ // thread fails before its value unpacker can initialize (Reanimated 3).
113
+ 'ReanimatedError',
114
+ '__reanimatedLoggerConfig',
115
+ '_IS_FABRIC',
116
+ '_log',
117
+ '_toString',
118
+ '_scheduleHostFunctionOnJS',
119
+ '_scheduleRemoteFunctionOnJS',
120
+ '_scheduleOnRuntime',
121
+ '_makeShareableClone',
122
+ '_updatePropsPaper',
123
+ '_updatePropsFabric',
124
+ '_measurePaper',
125
+ '_measureFabric',
126
+ '_scrollToPaper',
127
+ '_dispatchCommandPaper',
128
+ '_dispatchCommandFabric',
129
+ '_setGestureState',
130
+ '_notifyAboutProgress',
131
+ '_notifyAboutEnd',
132
+ '_runOnUIQueue',
133
+ '_getAnimationTimestamp',
111
134
  ])
112
135
 
113
136
  export function createGlobalsSet(
package/types/index.d.ts CHANGED
@@ -7,11 +7,13 @@ import type { PluginOption } from 'vite';
7
7
  import type { Options } from './types';
8
8
  export * from './configure';
9
9
  export * from './transformBabel';
10
+ export * from './transformFlow';
10
11
  export * from './transformSWC';
11
12
  export * from './transformWorklets';
12
13
  export * from './reactNativeCodegen';
13
14
  export * from './transformHermesLoops';
14
15
  export * from './transformHermesAsync';
16
+ export * from './transformHermesClasses';
15
17
  export { clearTransformCache } from './cache';
16
18
  export { JS_GLOBALS } from './worklets/globals';
17
19
  export { getClosureVariables } from './worklets/scope';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AASH,OAAO,KAAK,EAAE,YAAY,EAA8B,MAAM,MAAM,CAAA;AAcpE,OAAO,KAAK,EAAkC,OAAO,EAAE,MAAM,SAAS,CAAA;AAGtE,cAAc,aAAa,CAAA;AAC3B,cAAc,kBAAkB,CAAA;AAChC,cAAc,gBAAgB,CAAA;AAC9B,cAAc,qBAAqB,CAAA;AACnC,cAAc,sBAAsB,CAAA;AACpC,cAAc,wBAAwB,CAAA;AACtC,cAAc,wBAAwB,CAAA;AACtC,OAAO,EAAE,mBAAmB,EAAE,MAAM,SAAS,CAAA;AAC7C,OAAO,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAA;AAC/C,OAAO,EAAE,mBAAmB,EAAE,MAAM,kBAAkB,CAAA;AACtD,YAAY,EAAE,YAAY,EAAE,MAAM,SAAS,CAAA;AA0S3C,wBAAsB,wBAAwB,CAC5C,SAAS,CAAC,EAAE,OAAO,CAAC,OAAO,CAAC,GAC3B,OAAO,CAAC,YAAY,EAAE,CAAC,CA6VzB"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AASH,OAAO,KAAK,EAAE,YAAY,EAA8B,MAAM,MAAM,CAAA;AAcpE,OAAO,KAAK,EAAkC,OAAO,EAAE,MAAM,SAAS,CAAA;AAGtE,cAAc,aAAa,CAAA;AAC3B,cAAc,kBAAkB,CAAA;AAChC,cAAc,iBAAiB,CAAA;AAC/B,cAAc,gBAAgB,CAAA;AAC9B,cAAc,qBAAqB,CAAA;AACnC,cAAc,sBAAsB,CAAA;AACpC,cAAc,wBAAwB,CAAA;AACtC,cAAc,wBAAwB,CAAA;AACtC,cAAc,0BAA0B,CAAA;AACxC,OAAO,EAAE,mBAAmB,EAAE,MAAM,SAAS,CAAA;AAC7C,OAAO,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAA;AAC/C,OAAO,EAAE,mBAAmB,EAAE,MAAM,kBAAkB,CAAA;AACtD,YAAY,EAAE,YAAY,EAAE,MAAM,SAAS,CAAA;AA8S3C,wBAAsB,wBAAwB,CAC5C,SAAS,CAAC,EAAE,OAAO,CAAC,OAAO,CAAC,GAC3B,OAAO,CAAC,YAAY,EAAE,CAAC,CAkWzB"}
@@ -3,7 +3,7 @@
3
3
  *
4
4
  * `@react-native/codegen` builds a full CodegenSchema with @babel/parser and
5
5
  * emits the module with @babel/generator, which is the last thing in the native
6
- * pipeline that runs babel at all: 11,774 babel calls per bundle of the soot
6
+ * pipeline that runs babel at all: 11,774 babel calls per bundle of the contrast
7
7
  * app, every one of them from codegen. The view config itself is a plain object
8
8
  * literal, so it can be read off the spec's own AST and printed as text.
9
9
  *
@@ -1 +1 @@
1
- {"version":3,"file":"reactNativeViewConfig.d.ts","sourceRoot":"","sources":["../src/reactNativeViewConfig.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AA4dH,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,WAAW,CAAC,EAAE,MAAM,GAAG,GAAG,CAuBnF;AAED;;;;GAIG;AACH,wBAAgB,kBAAkB,CAChC,OAAO,EAAE,GAAG,GACX;IAAE,OAAO,EAAE,MAAM,EAAE,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CAsE5C;AAED,kFAAkF;AAClF,wBAAgB,sBAAsB,CAAC,MAAM,EAAE;IAAE,OAAO,EAAE,MAAM,EAAE,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,UAEjF;AAED,wBAAgB,cAAc,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAIvD"}
1
+ {"version":3,"file":"reactNativeViewConfig.d.ts","sourceRoot":"","sources":["../src/reactNativeViewConfig.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAieH,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,WAAW,CAAC,EAAE,MAAM,GAAG,GAAG,CAuBnF;AAED;;;;GAIG;AACH,wBAAgB,kBAAkB,CAChC,OAAO,EAAE,GAAG,GACX;IAAE,OAAO,EAAE,MAAM,EAAE,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CAsE5C;AAED,kFAAkF;AAClF,wBAAgB,sBAAsB,CAAC,MAAM,EAAE;IAAE,OAAO,EAAE,MAAM,EAAE,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,UAEjF;AAED,wBAAgB,cAAc,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAIvD"}