@vxrn/compiler 2.0.0-beta.33.1 → 2.0.0-beta.362.1

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 (115) hide show
  1. package/dist/cjs/cache.cjs +112 -109
  2. package/dist/cjs/configure.cjs +26 -28
  3. package/dist/cjs/constants.cjs +56 -60
  4. package/dist/cjs/index.cjs +428 -438
  5. package/dist/cjs/reactNativeCodegen.cjs +105 -114
  6. package/dist/cjs/reactNativeViewConfig.cjs +362 -381
  7. package/dist/cjs/refresh-runtime.cjs +390 -397
  8. package/dist/cjs/transformBabel.cjs +247 -221
  9. package/dist/cjs/transformHermesAsync.cjs +36 -47
  10. package/dist/cjs/transformHermesClasses.cjs +51 -0
  11. package/dist/cjs/transformHermesLoops.cjs +279 -281
  12. package/dist/cjs/transformSWC.cjs +183 -200
  13. package/dist/cjs/transformWorklets.cjs +107 -95
  14. package/dist/cjs/types.cjs +9 -11
  15. package/dist/cjs/workletUtils.cjs +30 -0
  16. package/dist/cjs/worklets/autoworklet.cjs +277 -222
  17. package/dist/cjs/worklets/globals.cjs +145 -60
  18. package/dist/cjs/worklets/hash.cjs +23 -27
  19. package/dist/cjs/worklets/scope.cjs +338 -359
  20. package/dist/cjs/worklets/serialize.cjs +73 -75
  21. package/dist/cjs/worklets/transform.cjs +217 -222
  22. package/dist/cjs/worklets/types.cjs +9 -11
  23. package/dist/esm/cache.mjs +96 -89
  24. package/dist/esm/cache.mjs.map +1 -1
  25. package/dist/esm/configure.mjs +11 -10
  26. package/dist/esm/configure.mjs.map +1 -1
  27. package/dist/esm/constants.mjs +41 -41
  28. package/dist/esm/constants.mjs.map +1 -1
  29. package/dist/esm/index.js +426 -420
  30. package/dist/esm/index.js.map +1 -1
  31. package/dist/esm/index.mjs +426 -420
  32. package/dist/esm/index.mjs.map +1 -1
  33. package/dist/esm/reactNativeCodegen.mjs +90 -88
  34. package/dist/esm/reactNativeCodegen.mjs.map +1 -1
  35. package/dist/esm/reactNativeViewConfig.mjs +347 -362
  36. package/dist/esm/reactNativeViewConfig.mjs.map +1 -1
  37. package/dist/esm/refresh-runtime.mjs +372 -376
  38. package/dist/esm/refresh-runtime.mjs.map +1 -1
  39. package/dist/esm/transformBabel.mjs +231 -198
  40. package/dist/esm/transformBabel.mjs.map +1 -1
  41. package/dist/esm/transformHermesAsync.mjs +20 -21
  42. package/dist/esm/transformHermesAsync.mjs.map +1 -1
  43. package/dist/esm/transformHermesClasses.mjs +25 -0
  44. package/dist/esm/transformHermesClasses.mjs.map +1 -0
  45. package/dist/esm/transformHermesLoops.mjs +264 -255
  46. package/dist/esm/transformHermesLoops.mjs.map +1 -1
  47. package/dist/esm/transformSWC.mjs +163 -171
  48. package/dist/esm/transformSWC.mjs.map +1 -1
  49. package/dist/esm/transformWorklets.mjs +89 -69
  50. package/dist/esm/transformWorklets.mjs.map +1 -1
  51. package/dist/esm/types.mjs +0 -2
  52. package/dist/esm/workletUtils.mjs +5 -0
  53. package/dist/esm/worklets/autoworklet.mjs +258 -200
  54. package/dist/esm/worklets/autoworklet.mjs.map +1 -1
  55. package/dist/esm/worklets/globals.mjs +130 -42
  56. package/dist/esm/worklets/globals.mjs.map +1 -1
  57. package/dist/esm/worklets/hash.mjs +10 -9
  58. package/dist/esm/worklets/hash.mjs.map +1 -1
  59. package/dist/esm/worklets/scope.mjs +325 -341
  60. package/dist/esm/worklets/scope.mjs.map +1 -1
  61. package/dist/esm/worklets/serialize.mjs +60 -58
  62. package/dist/esm/worklets/serialize.mjs.map +1 -1
  63. package/dist/esm/worklets/transform.mjs +201 -197
  64. package/dist/esm/worklets/transform.mjs.map +1 -1
  65. package/dist/esm/worklets/types.mjs +0 -2
  66. package/package.json +15 -6
  67. package/src/cache.ts +37 -7
  68. package/src/index.ts +44 -19
  69. package/src/reactNativeViewConfig.ts +6 -1
  70. package/src/transformBabel.test.ts +351 -17
  71. package/src/transformBabel.ts +137 -96
  72. package/src/transformHermesClasses.ts +34 -0
  73. package/src/transformSWC.ts +12 -1
  74. package/src/transformWorklets.test.ts +80 -5
  75. package/src/workletUtils.ts +4 -0
  76. package/src/worklets/autoworklet.ts +5 -2
  77. package/src/worklets/globals.ts +23 -0
  78. package/src/worklets/serialize.ts +1 -1
  79. package/types/cache.d.ts +2 -2
  80. package/types/cache.d.ts.map +1 -1
  81. package/types/index.d.ts +1 -0
  82. package/types/index.d.ts.map +1 -1
  83. package/types/reactNativeViewConfig.d.ts.map +1 -1
  84. package/types/transformBabel.d.ts +16 -1
  85. package/types/transformBabel.d.ts.map +1 -1
  86. package/types/transformHermesClasses.d.ts +5 -0
  87. package/types/transformHermesClasses.d.ts.map +1 -0
  88. package/types/transformSWC.d.ts +1 -4
  89. package/types/transformSWC.d.ts.map +1 -1
  90. package/types/workletUtils.d.ts +4 -0
  91. package/types/workletUtils.d.ts.map +1 -0
  92. package/types/worklets/autoworklet.d.ts.map +1 -1
  93. package/types/worklets/globals.d.ts.map +1 -1
  94. package/dist/cjs/transformBabel.test.cjs +0 -360
  95. package/dist/cjs/transformHermesLoops.test.cjs +0 -257
  96. package/dist/cjs/transformSWC.test.cjs +0 -117
  97. package/dist/cjs/transformWorklets.test.cjs +0 -604
  98. package/dist/esm/transformBabel.test.mjs +0 -337
  99. package/dist/esm/transformBabel.test.mjs.map +0 -1
  100. package/dist/esm/transformHermesLoops.test.mjs +0 -234
  101. package/dist/esm/transformHermesLoops.test.mjs.map +0 -1
  102. package/dist/esm/transformSWC.test.mjs +0 -118
  103. package/dist/esm/transformSWC.test.mjs.map +0 -1
  104. package/dist/esm/transformWorklets.test.mjs +0 -581
  105. package/dist/esm/transformWorklets.test.mjs.map +0 -1
  106. package/dist/esm/types.mjs.map +0 -1
  107. package/dist/esm/worklets/types.mjs.map +0 -1
  108. package/types/transformBabel.test.d.ts +0 -2
  109. package/types/transformBabel.test.d.ts.map +0 -1
  110. package/types/transformHermesLoops.test.d.ts +0 -2
  111. package/types/transformHermesLoops.test.d.ts.map +0 -1
  112. package/types/transformSWC.test.d.ts +0 -2
  113. package/types/transformSWC.test.d.ts.map +0 -1
  114. package/types/transformWorklets.test.d.ts +0 -2
  115. package/types/transformWorklets.test.d.ts.map +0 -1
package/src/index.ts CHANGED
@@ -35,6 +35,7 @@ export * from './transformWorklets'
35
35
  export * from './reactNativeCodegen'
36
36
  export * from './transformHermesLoops'
37
37
  export * from './transformHermesAsync'
38
+ export * from './transformHermesClasses'
38
39
  export { clearTransformCache } from './cache'
39
40
  export { JS_GLOBALS } from './worklets/globals'
40
41
  export { getClosureVariables } from './worklets/scope'
@@ -162,18 +163,26 @@ async function performBabelTransform({
162
163
  }
163
164
 
164
165
  if (userTransform !== 'swc' && userTransform !== 'oxc') {
165
- const babelOptions = getBabelOptions({
166
+ let babelOptions = getBabelOptions({
166
167
  ...transformProps,
167
168
  userSetting: userTransform,
168
169
  })
170
+ // worklets run without requesting a babel pass.
171
+ if (!babelOptions && shouldTransformWorklets({ id, code })) {
172
+ babelOptions = { plugins: [] }
173
+ }
169
174
 
170
175
  if (babelOptions) {
171
176
  const hasCompilerPlugin = babelOptions.plugins?.some(
172
177
  (x) => Array.isArray(x) && x[0] === 'babel-plugin-react-compiler'
173
178
  )
174
179
 
175
- // Check cache first
176
- const cached = getCachedTransform(id, code, environment)
180
+ // Check cache first. the user babel config feeds the transform output,
181
+ // so its identity joins the key: adding or editing babel.config.* must
182
+ // miss entries cached without that change.
183
+ const userBabelConfigPath =
184
+ typeof babelOptions.configFile === 'string' ? babelOptions.configFile : null
185
+ const cached = getCachedTransform(id, code, environment, userBabelConfigPath)
177
186
  if (cached) {
178
187
  perfStats.babel.byEnvironment[environment].transforms++
179
188
  if (
@@ -214,25 +223,32 @@ async function performBabelTransform({
214
223
  ) ?? -1
215
224
 
216
225
  if (compilerPluginIndex !== -1) {
217
- // mark callbacks before react compiler moves them into memoized bindings.
218
- if (useWorklets) {
219
- workletPreparation = prepareWorkletsForReactCompiler(
226
+ const compilerPluginConfig = babelOptions.plugins![compilerPluginIndex] as any[]
227
+ const compilerPluginOptions = compilerPluginConfig[1] || {}
228
+ const useBabelCompiler =
229
+ compilerPluginOptions.compiler === 'babel' ||
230
+ process.env.ONE_REACT_COMPILER === 'babel' ||
231
+ process.env.VXRN_REACT_COMPILER === 'babel'
232
+
233
+ if (!useBabelCompiler) {
234
+ // mark callbacks before react compiler moves them into memoized bindings.
235
+ if (useWorklets) {
236
+ workletPreparation = prepareWorkletsForReactCompiler(
237
+ id,
238
+ curCode,
239
+ shouldSourceMap()
240
+ )
241
+ if (workletPreparation) curCode = workletPreparation.code
242
+ }
243
+ compilerOut = await transformOxcReactCompiler(
220
244
  id,
221
245
  curCode,
246
+ compilerPluginOptions,
222
247
  shouldSourceMap()
223
248
  )
224
- if (workletPreparation) curCode = workletPreparation.code
249
+ if (compilerOut) curCode = compilerOut.code
250
+ babelOptions.plugins!.splice(compilerPluginIndex, 1)
225
251
  }
226
- const compilerTarget =
227
- (babelOptions.plugins![compilerPluginIndex] as any[])[1]?.target ?? '19'
228
- compilerOut = await transformOxcReactCompiler(
229
- id,
230
- curCode,
231
- compilerTarget,
232
- shouldSourceMap()
233
- )
234
- if (compilerOut) curCode = compilerOut.code
235
- babelOptions.plugins!.splice(compilerPluginIndex, 1)
236
252
  }
237
253
 
238
254
  let workletsOut: { code: string; map?: any } | null = null
@@ -251,7 +267,11 @@ async function performBabelTransform({
251
267
  const startTime = Date.now()
252
268
  let babelOut: { code?: string | null; map?: any } | null = null
253
269
 
254
- if (babelOptions.plugins?.length === 0) {
270
+ if (
271
+ babelOptions.plugins?.length === 0 &&
272
+ !babelOptions.configFile &&
273
+ !babelOptions.babelrc
274
+ ) {
255
275
  let finalMap: any = undefined
256
276
  if (shouldSourceMap()) {
257
277
  const intermediateMaps = [
@@ -311,7 +331,7 @@ async function performBabelTransform({
311
331
  const result = { code: outCode, map: babelOut.map }
312
332
 
313
333
  // Cache the result
314
- setCachedTransform(id, code, result, environment)
334
+ setCachedTransform(id, code, result, environment, userBabelConfigPath)
315
335
 
316
336
  return result
317
337
  }
@@ -628,6 +648,11 @@ ${rootJS.code}
628
648
  transform(code, _id) {
629
649
  if (this.environment.name !== 'client') return
630
650
  if (code.includes(runtimePublicPath)) return // already wrapped
651
+ // vite's special queries (the same pattern as its SPECIAL_QUERY_RE) make
652
+ // a string, url, or worker wrapper module, never the component source.
653
+ // wrapping one imports the refresh runtime, which writes to `window` and
654
+ // so throws when a worker imports `Foo.tsx?raw`.
655
+ if (/[?&](?:worker|sharedworker|raw|url)\b/.test(_id)) return
631
656
 
632
657
  const id = _id.split('?')[0]
633
658
  if (id.includes('node_modules')) return
@@ -389,7 +389,12 @@ function readCommands(call: any, decls: Declarations): Command[] {
389
389
  for (const m of commandMembers) {
390
390
  const name = m.key?.name ?? m.key?.value
391
391
  if (!name) continue
392
- const fn = m.typeAnnotation?.typeAnnotation ?? m.value ?? m.typeAnnotation
392
+ // `setPage(ref, index): void` carries its params on the member itself;
393
+ // `setPage: (ref, index) => void` and flow members carry a function type
394
+ const fn =
395
+ m.type === 'TSMethodSignature'
396
+ ? m
397
+ : (m.typeAnnotation?.typeAnnotation ?? m.value ?? m.typeAnnotation)
393
398
  const params = (fn?.params ?? fn?.parameters ?? []).slice(1)
394
399
  byName.set(
395
400
  name,
@@ -4,7 +4,9 @@ import path from 'node:path'
4
4
  import { afterEach, describe, expect, it } from 'vitest'
5
5
  import { configureVXRNCompilerPlugin } from './configure'
6
6
  import {
7
+ findUserBabelConfig,
7
8
  getBabelOptions,
9
+ stripFlowTypes,
8
10
  transformBabel,
9
11
  transformOxcReactCompiler,
10
12
  } from './transformBabel'
@@ -12,9 +14,8 @@ import {
12
14
  afterEach(() => {
13
15
  configureVXRNCompilerPlugin({ enableReanimated: false })
14
16
  })
15
-
16
- describe('getBabelOptions Worklets resolution', () => {
17
- it('uses the app-installed Worklets Babel plugin', () => {
17
+ describe('getBabelOptions worklets ownership', () => {
18
+ it('does not add an automatic Worklets Babel pass', () => {
18
19
  const projectRoot = fs.realpathSync(
19
20
  fs.mkdtempSync(path.join(os.tmpdir(), 'vxrn-worklets-'))
20
21
  )
@@ -40,7 +41,7 @@ describe('getBabelOptions Worklets resolution', () => {
40
41
  projectRoot,
41
42
  })
42
43
 
43
- expect(options?.plugins).toContain(pluginPath)
44
+ expect(options).toBeNull()
44
45
  } finally {
45
46
  fs.rmSync(projectRoot, { recursive: true, force: true })
46
47
  }
@@ -67,6 +68,27 @@ describe('transformBabel Flow parsing', () => {
67
68
  expect(result?.code).not.toContain('HostComponent')
68
69
  })
69
70
 
71
+ it('preserves React Native Flow enums as runtime values', async () => {
72
+ const result = await transformBabel(
73
+ '/project/VirtualView.js',
74
+ `
75
+ // @flow strict-local
76
+ export enum VirtualViewRenderState {
77
+ Unknown = 0,
78
+ Rendered = 1,
79
+ None = 2,
80
+ }
81
+
82
+ export const rendered = VirtualViewRenderState.Rendered
83
+ `,
84
+ { plugins: [] }
85
+ )
86
+
87
+ expect(result?.code).toContain('const VirtualViewRenderState =')
88
+ expect(result?.code).toContain('Rendered: 1')
89
+ expect(result?.code).toContain('VirtualViewRenderState.Rendered')
90
+ })
91
+
70
92
  it('rejects a required transform error instead of returning untransformed source', async () => {
71
93
  const negativeControl = () => ({
72
94
  visitor: {
@@ -130,6 +152,29 @@ describe('transformOxcReactCompiler', () => {
130
152
 
131
153
  expect(result.code).toContain('_c(')
132
154
  })
155
+
156
+ it('supports mutating useSharedValue in useEffect via Reanimated environment defaults', async () => {
157
+ const reanimatedCode = `
158
+ import { useEffect } from 'react'
159
+ import { useSharedValue } from 'react-native-reanimated'
160
+
161
+ export function ReanimatedComponent() {
162
+ const val = useSharedValue(0)
163
+ useEffect(() => {
164
+ val.value = 1
165
+ }, [val])
166
+ return <div>{val.value}</div>
167
+ }
168
+ `
169
+ const result = await transformOxcReactCompiler(
170
+ '/project/ReanimatedComponent.tsx',
171
+ reanimatedCode,
172
+ { target: '19' },
173
+ false
174
+ )
175
+
176
+ expect(result.code).toContain('_c(')
177
+ })
133
178
  })
134
179
 
135
180
  describe('compiler plugin multi-stage source map composition', () => {
@@ -203,7 +248,7 @@ export async function* generatorProbe() {
203
248
  })
204
249
 
205
250
  describe('shared compiler worklets backend selection', () => {
206
- it('retains Babel for worklets by default when enableNativeWorklets is false', async () => {
251
+ it('does not fall back to Babel when worklet compilation is disabled', async () => {
207
252
  const { vi } = await import('vitest')
208
253
  const { createVXRNCompilerPlugin } = await import('./index')
209
254
  const workletsModule = await import('./transformWorklets')
@@ -225,8 +270,8 @@ describe('shared compiler worklets backend selection', () => {
225
270
  const hook = plugin.transform.handler || plugin.transform
226
271
  const context = { environment: { name: 'ios' } }
227
272
  const result = await hook.call(context, inputCode, tempFile)
228
- expect(result).toBeDefined()
229
- // With enableNativeWorklets: false, transformWorklets (SWC) is NOT called
273
+ expect(result).toBeNull()
274
+ // disabling the transform does not select a second backend.
230
275
  expect(workletSpy).not.toHaveBeenCalled()
231
276
  } finally {
232
277
  workletSpy.mockRestore()
@@ -238,7 +283,7 @@ describe('shared compiler worklets backend selection', () => {
238
283
  }
239
284
  })
240
285
 
241
- it('uses native SWC for worklets when enableNativeWorklets is true', async () => {
286
+ it('compiles worklets with One when no Babel pass is requested', async () => {
242
287
  const compiler = await import('./index')
243
288
  const { vi } = await import('vitest')
244
289
  const workletsModule = await import('./transformWorklets')
@@ -261,7 +306,8 @@ describe('shared compiler worklets backend selection', () => {
261
306
  const context = { environment: { name: 'ios' } }
262
307
  const result = await hook.call(context, inputCode, tempFile)
263
308
  expect(result).toBeDefined()
264
- // With enableNativeWorklets: true, transformWorklets (SWC) IS called
309
+ expect(result.code).toContain('__workletHash')
310
+ // the worklet-only file still reaches the One transform.
265
311
  expect(workletSpy).toHaveBeenCalled()
266
312
  } finally {
267
313
  workletSpy.mockRestore()
@@ -284,7 +330,7 @@ describe('shared compiler worklets backend selection', () => {
284
330
  fs.writeFileSync(tempFile, inputCode)
285
331
 
286
332
  try {
287
- // Step 1: Run with Babel backend (enableNativeWorklets: false)
333
+ // begin with worklet compilation disabled.
288
334
  configureVXRNCompilerPlugin({
289
335
  enableReanimated: true,
290
336
  enableNativeWorklets: false,
@@ -297,10 +343,10 @@ describe('shared compiler worklets backend selection', () => {
297
343
  const context = { environment: { name: 'ios' } }
298
344
 
299
345
  const res1 = await hook1.call(context, inputCode, tempFile)
300
- expect(res1).toBeDefined()
346
+ expect(res1).toBeNull()
301
347
  expect(workletSpy).toHaveBeenCalledTimes(0)
302
348
 
303
- // Step 2: Toggle to native SWC backend on the SAME file without changes
349
+ // enable the transform on the same unchanged file.
304
350
  configureVXRNCompilerPlugin({
305
351
  enableReanimated: true,
306
352
  enableNativeWorklets: true,
@@ -313,15 +359,16 @@ describe('shared compiler worklets backend selection', () => {
313
359
 
314
360
  const res2 = await hook2.call(context, inputCode, tempFile)
315
361
  expect(res2).toBeDefined()
316
- // Cache must NOT serve the Babel entry; native transformWorklets must be invoked
362
+ expect(res2.code).toContain('__workletHash')
363
+ // the cache must not serve the untransformed entry.
317
364
  expect(workletSpy).toHaveBeenCalledTimes(1)
318
365
 
319
- // Step 3: Call again with native SWC to verify caching works for the native backend
366
+ // reuse the transformed cache entry.
320
367
  const res3 = await hook2.call(context, inputCode, tempFile)
321
368
  expect(res3).toBeDefined()
322
369
  expect(workletSpy).toHaveBeenCalledTimes(1)
323
370
 
324
- // Step 4: Toggle back to Babel backend; cache must not serve SWC entry
371
+ // disable the transform; the cache must not serve a transformed entry.
325
372
  configureVXRNCompilerPlugin({
326
373
  enableReanimated: true,
327
374
  enableNativeWorklets: false,
@@ -333,8 +380,8 @@ describe('shared compiler worklets backend selection', () => {
333
380
  const hook3 = plugin3.transform.handler || plugin3.transform
334
381
 
335
382
  const res4 = await hook3.call(context, inputCode, tempFile)
336
- expect(res4).toBeDefined()
337
- // workletSpy should still have only been called once
383
+ expect(res4).toBeNull()
384
+ // no second transform was requested.
338
385
  expect(workletSpy).toHaveBeenCalledTimes(1)
339
386
  } finally {
340
387
  workletSpy.mockRestore()
@@ -346,3 +393,290 @@ describe('shared compiler worklets backend selection', () => {
346
393
  }
347
394
  })
348
395
  })
396
+
397
+ describe('findUserBabelConfig and user Babel config respect', () => {
398
+ it('finds user babel config and ignores generated configs', () => {
399
+ const projectRoot = fs.realpathSync(
400
+ fs.mkdtempSync(path.join(os.tmpdir(), 'vxrn-babel-conf-'))
401
+ )
402
+ try {
403
+ expect(findUserBabelConfig(projectRoot)).toBeNull()
404
+
405
+ // written by `one patch` with the generated marker -> ignored
406
+ const generatedFile = path.join(projectRoot, 'babel.config.js')
407
+ fs.writeFileSync(
408
+ generatedFile,
409
+ '// @one/generated bundler-config\nmodule.exports = {}'
410
+ )
411
+ expect(findUserBabelConfig(projectRoot)).toBeNull()
412
+
413
+ // Overwritten with user config -> detected
414
+ fs.writeFileSync(generatedFile, 'module.exports = { plugins: [] }')
415
+ expect(findUserBabelConfig(projectRoot)).toBe(generatedFile)
416
+ } finally {
417
+ fs.rmSync(projectRoot, { recursive: true, force: true })
418
+ }
419
+ })
420
+
421
+ it('includes user configFile and babelrc in getBabelOptions for project files', () => {
422
+ const projectRoot = fs.realpathSync(
423
+ fs.mkdtempSync(path.join(os.tmpdir(), 'vxrn-babel-conf-'))
424
+ )
425
+ const userConfig = path.join(projectRoot, 'babel.config.json')
426
+ fs.writeFileSync(userConfig, '{"plugins": []}')
427
+ try {
428
+ // For project file:
429
+ const projectFileOptions = getBabelOptions({
430
+ id: path.join(projectRoot, 'src', 'index.tsx'),
431
+ code: `export const hello = () => 123`,
432
+ projectRoot,
433
+ development: true,
434
+ environment: 'client',
435
+ reactForRNVersion: '19',
436
+ })
437
+ expect(projectFileOptions?.configFile).toBe(userConfig)
438
+ expect(projectFileOptions?.babelrc).toBe(true)
439
+
440
+ // For node_modules file: should not attach user configFile
441
+ const nodeModulesOptions = getBabelOptions({
442
+ id: path.join(projectRoot, 'node_modules', 'foo', 'index.js'),
443
+ code: `export const foo = 1`,
444
+ projectRoot,
445
+ development: true,
446
+ environment: 'client',
447
+ reactForRNVersion: '19',
448
+ })
449
+ expect(nodeModulesOptions).toBeNull()
450
+ } finally {
451
+ fs.rmSync(projectRoot, { recursive: true, force: true })
452
+ }
453
+ })
454
+
455
+ it('runs user babel config transforms via transformBabel', async () => {
456
+ const projectRoot = fs.realpathSync(
457
+ fs.mkdtempSync(path.join(os.tmpdir(), 'vxrn-babel-conf-'))
458
+ )
459
+ const userConfig = path.join(projectRoot, 'babel.config.json')
460
+ // A babel config that injects a banner/plugin or standard babel syntax
461
+ fs.writeFileSync(
462
+ userConfig,
463
+ JSON.stringify({
464
+ comments: false,
465
+ })
466
+ )
467
+ try {
468
+ const code = '/* remove me */ export const x = 1'
469
+ const res = await transformBabel(path.join(projectRoot, 'src', 'index.ts'), code, {
470
+ configFile: userConfig,
471
+ babelrc: true,
472
+ })
473
+ expect(res.code).not.toContain('remove me')
474
+ expect(res.code).toContain('export const x = 1')
475
+ } finally {
476
+ fs.rmSync(projectRoot, { recursive: true, force: true })
477
+ }
478
+ })
479
+
480
+ it.each(['ios', 'android', 'client', 'ssr'] as const)(
481
+ 'tells user babel config the %s platform and that the bundler keeps static esm',
482
+ async (environment) => {
483
+ const projectRoot = fs.realpathSync(
484
+ fs.mkdtempSync(path.join(os.tmpdir(), 'vxrn-babel-conf-'))
485
+ )
486
+ const userConfig = path.join(projectRoot, 'babel.config.js')
487
+ // presets such as babel-preset-expo read this caller to decide whether to
488
+ // rewrite esm to commonjs and import.meta to a metro runtime global
489
+ fs.writeFileSync(
490
+ userConfig,
491
+ `module.exports = (api) => ({
492
+ comments: !api.caller((c) => c?.name === 'vxrn' && c?.supportsStaticESM === true && c?.platform === '${environment === 'ios' || environment === 'android' ? environment : 'web'}'),
493
+ })`
494
+ )
495
+ try {
496
+ const id = path.join(projectRoot, 'src', 'index.ts')
497
+ const code = '/* remove me */ export const x = 1'
498
+ const options = getBabelOptions({
499
+ id,
500
+ code,
501
+ projectRoot,
502
+ environment,
503
+ development: false,
504
+ reactForRNVersion: '19',
505
+ })
506
+ expect(options).not.toBeNull()
507
+ const res = await transformBabel(id, code, options!)
508
+ expect(res.code).not.toContain('remove me')
509
+ } finally {
510
+ fs.rmSync(projectRoot, { recursive: true, force: true })
511
+ }
512
+ }
513
+ )
514
+ })
515
+ describe('explicit swc/oxc per-file choice with a user babel config', () => {
516
+ it('returns null for swc/oxc string and object forms', () => {
517
+ const projectRoot = fs.realpathSync(
518
+ fs.mkdtempSync(path.join(os.tmpdir(), 'vxrn-babel-conf-'))
519
+ )
520
+ const userConfig = path.join(projectRoot, 'babel.config.js')
521
+ fs.writeFileSync(userConfig, 'module.exports = { plugins: [] }')
522
+ try {
523
+ const base = {
524
+ id: path.join(projectRoot, 'src', 'index.tsx'),
525
+ code: `export const x = 1`,
526
+ projectRoot,
527
+ development: true,
528
+ environment: 'client' as const,
529
+ reactForRNVersion: '19' as const,
530
+ }
531
+ // merely adding babel.config.js must not flip an explicit non-babel choice
532
+ expect(getBabelOptions({ ...base, userSetting: 'swc' })).toBeNull()
533
+ expect(getBabelOptions({ ...base, userSetting: 'oxc' })).toBeNull()
534
+ expect(getBabelOptions({ ...base, userSetting: { transform: 'swc' } })).toBeNull()
535
+ expect(getBabelOptions({ ...base, userSetting: { transform: 'oxc' } })).toBeNull()
536
+ // controls: babel choices still resolve through the user config
537
+ expect(getBabelOptions({ ...base, userSetting: 'babel' })?.configFile).toBe(
538
+ userConfig
539
+ )
540
+ expect(
541
+ getBabelOptions({ ...base, userSetting: { transform: 'babel' } })?.configFile
542
+ ).toBe(userConfig)
543
+ expect(getBabelOptions(base)?.configFile).toBe(userConfig)
544
+ } finally {
545
+ fs.rmSync(projectRoot, { recursive: true, force: true })
546
+ }
547
+ })
548
+
549
+ it('skips the transform end-to-end for object-form swc', async () => {
550
+ const { createVXRNCompilerPlugin } = await import('./index')
551
+ const projectRoot = fs.realpathSync(
552
+ fs.mkdtempSync(path.join(os.tmpdir(), 'vxrn-babel-conf-'))
553
+ )
554
+ const srcDir = path.join(projectRoot, 'src')
555
+ fs.mkdirSync(srcDir, { recursive: true })
556
+ const file = path.join(srcDir, 'index.ts')
557
+ const code = `export const x = 1`
558
+ fs.writeFileSync(file, code)
559
+ fs.writeFileSync(
560
+ path.join(projectRoot, 'babel.config.js'),
561
+ 'module.exports = { plugins: [] }'
562
+ )
563
+ configureVXRNCompilerPlugin({
564
+ enableCompiler: false,
565
+ enableReanimated: false,
566
+ })
567
+ try {
568
+ const plugins = await createVXRNCompilerPlugin({
569
+ transform: () => ({ transform: 'swc' }) as any,
570
+ })
571
+ const plugin = plugins.find((p: any) => p.name === 'one:compiler') as any
572
+ await plugin.configResolved({ root: projectRoot, build: {} })
573
+ const hook = plugin.transform.handler || plugin.transform
574
+ const result = await hook.call({ environment: { name: 'client' } }, code, file)
575
+ expect(result == null).toBe(true)
576
+ } finally {
577
+ configureVXRNCompilerPlugin({
578
+ enableCompiler: false,
579
+ enableReanimated: false,
580
+ })
581
+ fs.rmSync(projectRoot, { recursive: true, force: true })
582
+ }
583
+ })
584
+ })
585
+
586
+ describe('user babel config end-to-end through the compiler plugin', () => {
587
+ it('runs the user config when default plugins are empty', async () => {
588
+ const { createVXRNCompilerPlugin } = await import('./index')
589
+ const projectRoot = fs.realpathSync(
590
+ fs.mkdtempSync(path.join(os.tmpdir(), 'vxrn-babel-e2e-'))
591
+ )
592
+ const srcDir = path.join(projectRoot, 'src')
593
+ fs.mkdirSync(srcDir, { recursive: true })
594
+ const file = path.join(srcDir, 'index.ts')
595
+ const marker = 'babel-e2e-probe'
596
+ const code = `/* ${marker} */ export const x = 1`
597
+ fs.writeFileSync(file, code)
598
+ fs.writeFileSync(
599
+ path.join(projectRoot, 'babel.config.json'),
600
+ JSON.stringify({ comments: false })
601
+ )
602
+ configureVXRNCompilerPlugin({
603
+ enableCompiler: false,
604
+ enableReanimated: false,
605
+ })
606
+ try {
607
+ const plugins = await createVXRNCompilerPlugin()
608
+ const plugin = plugins.find((p: any) => p.name === 'one:compiler') as any
609
+ await plugin.configResolved({ root: projectRoot, build: {} })
610
+ const hook = plugin.transform.handler || plugin.transform
611
+ const result = await hook.call({ environment: { name: 'client' } }, code, file)
612
+ // default plugins are empty here, but the user configFile must still
613
+ // route through transformBabel instead of taking the skip fast-path
614
+ expect(result).toBeDefined()
615
+ expect(result.code).toContain('export const x = 1')
616
+ expect(result.code).not.toContain(marker)
617
+ } finally {
618
+ configureVXRNCompilerPlugin({
619
+ enableCompiler: false,
620
+ enableReanimated: false,
621
+ })
622
+ fs.rmSync(projectRoot, { recursive: true, force: true })
623
+ }
624
+ })
625
+
626
+ it('invalidates the cache when the user babel config is added or edited', async () => {
627
+ const { createVXRNCompilerPlugin } = await import('./index')
628
+ const projectRoot = fs.realpathSync(
629
+ fs.mkdtempSync(path.join(os.tmpdir(), 'vxrn-babel-cache-'))
630
+ )
631
+ const srcDir = path.join(projectRoot, 'src')
632
+ fs.mkdirSync(srcDir, { recursive: true })
633
+ const file = path.join(srcDir, 'index.ts')
634
+ const marker = 'babel-cache-probe'
635
+ const code = `/* ${marker} */ export const x = 1`
636
+ fs.writeFileSync(file, code)
637
+ const userConfig = path.join(projectRoot, 'babel.config.json')
638
+ configureVXRNCompilerPlugin({
639
+ enableCompiler: false,
640
+ enableReanimated: false,
641
+ })
642
+ try {
643
+ const plugins = await createVXRNCompilerPlugin({
644
+ transform: () => 'babel' as const,
645
+ })
646
+ const plugin = plugins.find((p: any) => p.name === 'one:compiler') as any
647
+ await plugin.configResolved({ root: projectRoot, build: {} })
648
+ const hook = plugin.transform.handler || plugin.transform
649
+ const context = { environment: { name: 'client' } }
650
+
651
+ const res1 = await hook.call(context, code, file)
652
+ expect(res1.code).toContain(marker)
653
+
654
+ // adding a config without touching the source must miss the cache
655
+ fs.writeFileSync(userConfig, JSON.stringify({ comments: false }))
656
+ const res2 = await hook.call(context, code, file)
657
+ expect(res2.code).not.toContain(marker)
658
+
659
+ // editing the config contents must miss again
660
+ fs.writeFileSync(userConfig, JSON.stringify({ comments: true }))
661
+ const res3 = await hook.call(context, code, file)
662
+ expect(res3.code).toContain(marker)
663
+ } finally {
664
+ configureVXRNCompilerPlugin({
665
+ enableCompiler: false,
666
+ enableReanimated: false,
667
+ })
668
+ fs.rmSync(projectRoot, { recursive: true, force: true })
669
+ }
670
+ })
671
+ })
672
+
673
+ describe('Flow and JSX parsing', () => {
674
+ it('strips Flow types while retaining JSX in published JavaScript', async () => {
675
+ const result = await stripFlowTypes(
676
+ '/app/VideoView.js',
677
+ `export function View(props: {value: string}) { return <NativeView {...props} /> }`
678
+ )
679
+ expect(result.code).not.toContain('value: string')
680
+ expect(result.code).toContain('<NativeView')
681
+ })
682
+ })