@vxrn/compiler 1.25.13 → 1.25.16-1788753284749

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (124) hide show
  1. package/dist/cjs/cache.cjs +3 -1
  2. package/dist/cjs/configure.cjs +9 -1
  3. package/dist/cjs/index.cjs +82 -4
  4. package/dist/cjs/reactNativeCodegen.cjs +140 -0
  5. package/dist/cjs/reactNativeViewConfig.cjs +489 -0
  6. package/dist/cjs/transformBabel.cjs +15 -20
  7. package/dist/cjs/transformBabel.test.cjs +260 -0
  8. package/dist/cjs/transformHermesLoops.cjs +317 -0
  9. package/dist/cjs/transformHermesLoops.test.cjs +220 -0
  10. package/dist/cjs/transformSWC.cjs +102 -137
  11. package/dist/cjs/transformSWC.test.cjs +74 -0
  12. package/dist/cjs/transformWorklets.cjs +121 -0
  13. package/dist/cjs/transformWorklets.test.cjs +604 -0
  14. package/dist/cjs/worklets/autoworklet.cjs +265 -0
  15. package/dist/cjs/worklets/globals.cjs +71 -0
  16. package/dist/cjs/worklets/hash.cjs +38 -0
  17. package/dist/cjs/worklets/scope.cjs +370 -0
  18. package/dist/cjs/worklets/serialize.cjs +101 -0
  19. package/dist/cjs/worklets/transform.cjs +232 -0
  20. package/dist/cjs/worklets/types.cjs +18 -0
  21. package/dist/esm/cache.mjs +4 -2
  22. package/dist/esm/cache.mjs.map +1 -1
  23. package/dist/esm/configure.mjs +8 -1
  24. package/dist/esm/configure.mjs.map +1 -1
  25. package/dist/esm/index.js +72 -5
  26. package/dist/esm/index.js.map +1 -1
  27. package/dist/esm/index.mjs +72 -5
  28. package/dist/esm/index.mjs.map +1 -1
  29. package/dist/esm/reactNativeCodegen.mjs +104 -0
  30. package/dist/esm/reactNativeCodegen.mjs.map +1 -0
  31. package/dist/esm/reactNativeViewConfig.mjs +460 -0
  32. package/dist/esm/reactNativeViewConfig.mjs.map +1 -0
  33. package/dist/esm/transformBabel.mjs +14 -19
  34. package/dist/esm/transformBabel.mjs.map +1 -1
  35. package/dist/esm/transformBabel.test.mjs +261 -1
  36. package/dist/esm/transformBabel.test.mjs.map +1 -1
  37. package/dist/esm/transformHermesLoops.mjs +281 -0
  38. package/dist/esm/transformHermesLoops.mjs.map +1 -0
  39. package/dist/esm/transformHermesLoops.test.mjs +197 -0
  40. package/dist/esm/transformHermesLoops.test.mjs.map +1 -0
  41. package/dist/esm/transformSWC.mjs +89 -138
  42. package/dist/esm/transformSWC.mjs.map +1 -1
  43. package/dist/esm/transformSWC.test.mjs +75 -0
  44. package/dist/esm/transformSWC.test.mjs.map +1 -0
  45. package/dist/esm/transformWorklets.mjs +81 -0
  46. package/dist/esm/transformWorklets.mjs.map +1 -0
  47. package/dist/esm/transformWorklets.test.mjs +581 -0
  48. package/dist/esm/transformWorklets.test.mjs.map +1 -0
  49. package/dist/esm/worklets/autoworklet.mjs +234 -0
  50. package/dist/esm/worklets/autoworklet.mjs.map +1 -0
  51. package/dist/esm/worklets/globals.mjs +45 -0
  52. package/dist/esm/worklets/globals.mjs.map +1 -0
  53. package/dist/esm/worklets/hash.mjs +13 -0
  54. package/dist/esm/worklets/hash.mjs.map +1 -0
  55. package/dist/esm/worklets/scope.mjs +345 -0
  56. package/dist/esm/worklets/scope.mjs.map +1 -0
  57. package/dist/esm/worklets/serialize.mjs +75 -0
  58. package/dist/esm/worklets/serialize.mjs.map +1 -0
  59. package/dist/esm/worklets/transform.mjs +196 -0
  60. package/dist/esm/worklets/transform.mjs.map +1 -0
  61. package/dist/esm/worklets/types.mjs +2 -0
  62. package/dist/esm/worklets/types.mjs.map +1 -0
  63. package/package.json +10 -6
  64. package/src/cache.ts +9 -2
  65. package/src/configure.ts +12 -0
  66. package/src/index.ts +93 -13
  67. package/src/reactNativeCodegen.ts +181 -0
  68. package/src/reactNativeViewConfig.ts +593 -0
  69. package/src/transformBabel.test.ts +268 -1
  70. package/src/transformBabel.ts +21 -31
  71. package/src/transformHermesLoops.test.ts +282 -0
  72. package/src/transformHermesLoops.ts +433 -0
  73. package/src/transformSWC.test.ts +88 -0
  74. package/src/transformSWC.ts +109 -160
  75. package/src/transformWorklets.test.ts +708 -0
  76. package/src/transformWorklets.ts +160 -0
  77. package/src/types.ts +4 -4
  78. package/src/worklets/autoworklet.ts +391 -0
  79. package/src/worklets/globals.ts +125 -0
  80. package/src/worklets/hash.ts +14 -0
  81. package/src/worklets/scope.ts +401 -0
  82. package/src/worklets/serialize.ts +114 -0
  83. package/src/worklets/transform.ts +282 -0
  84. package/src/worklets/types.ts +26 -0
  85. package/types/cache.d.ts +1 -0
  86. package/types/cache.d.ts.map +1 -1
  87. package/types/configure.d.ts +2 -0
  88. package/types/configure.d.ts.map +1 -1
  89. package/types/index.d.ts +3 -0
  90. package/types/index.d.ts.map +1 -1
  91. package/types/reactNativeCodegen.d.ts +5 -0
  92. package/types/reactNativeCodegen.d.ts.map +1 -0
  93. package/types/reactNativeViewConfig.d.ts +35 -0
  94. package/types/reactNativeViewConfig.d.ts.map +1 -0
  95. package/types/transformBabel.d.ts +3 -3
  96. package/types/transformBabel.d.ts.map +1 -1
  97. package/types/transformHermesLoops.d.ts +5 -0
  98. package/types/transformHermesLoops.d.ts.map +1 -0
  99. package/types/transformHermesLoops.test.d.ts +2 -0
  100. package/types/transformHermesLoops.test.d.ts.map +1 -0
  101. package/types/transformSWC.d.ts +12 -3
  102. package/types/transformSWC.d.ts.map +1 -1
  103. package/types/transformSWC.test.d.ts +2 -0
  104. package/types/transformSWC.test.d.ts.map +1 -0
  105. package/types/transformWorklets.d.ts +22 -0
  106. package/types/transformWorklets.d.ts.map +1 -0
  107. package/types/transformWorklets.test.d.ts +2 -0
  108. package/types/transformWorklets.test.d.ts.map +1 -0
  109. package/types/types.d.ts +4 -4
  110. package/types/types.d.ts.map +1 -1
  111. package/types/worklets/autoworklet.d.ts +14 -0
  112. package/types/worklets/autoworklet.d.ts.map +1 -0
  113. package/types/worklets/globals.d.ts +3 -0
  114. package/types/worklets/globals.d.ts.map +1 -0
  115. package/types/worklets/hash.d.ts +5 -0
  116. package/types/worklets/hash.d.ts.map +1 -0
  117. package/types/worklets/scope.d.ts +7 -0
  118. package/types/worklets/scope.d.ts.map +1 -0
  119. package/types/worklets/serialize.d.ts +12 -0
  120. package/types/worklets/serialize.d.ts.map +1 -0
  121. package/types/worklets/transform.d.ts +6 -0
  122. package/types/worklets/transform.d.ts.map +1 -0
  123. package/types/worklets/types.d.ts +21 -0
  124. package/types/worklets/types.d.ts.map +1 -0
@@ -3,7 +3,11 @@ import os from 'node:os'
3
3
  import path from 'node:path'
4
4
  import { afterEach, describe, expect, it } from 'vitest'
5
5
  import { configureVXRNCompilerPlugin } from './configure'
6
- import { getBabelOptions, transformBabel } from './transformBabel'
6
+ import {
7
+ getBabelOptions,
8
+ transformBabel,
9
+ transformOxcReactCompiler,
10
+ } from './transformBabel'
7
11
 
8
12
  afterEach(() => {
9
13
  configureVXRNCompilerPlugin({ enableReanimated: false })
@@ -79,3 +83,266 @@ describe('transformBabel Flow parsing', () => {
79
83
  ).rejects.toThrow('NATIVE_TRANSFORM_NEGATIVE_CONTROL')
80
84
  })
81
85
  })
86
+
87
+ describe('transformOxcReactCompiler', () => {
88
+ const componentCode = `
89
+ import { useState } from 'react'
90
+
91
+ export function Counter() {
92
+ const [count, setCount] = useState(0)
93
+ return <button onClick={() => setCount(count + 1)}>{count}</button>
94
+ }
95
+ `
96
+
97
+ it('compiles and memoizes React components with Rust React Compiler', async () => {
98
+ const result = await transformOxcReactCompiler(
99
+ '/project/Counter.tsx',
100
+ componentCode,
101
+ '19',
102
+ false
103
+ )
104
+
105
+ expect(result.code).toContain('_c(')
106
+ expect(result.code).toContain('react/compiler-runtime')
107
+ expect(result.map).toBeUndefined()
108
+ })
109
+
110
+ it('generates source maps when requested', async () => {
111
+ const result = await transformOxcReactCompiler(
112
+ '/project/Counter.tsx',
113
+ componentCode,
114
+ '19',
115
+ true
116
+ )
117
+
118
+ expect(result.code).toContain('_c(')
119
+ expect(result.map).toBeDefined()
120
+ expect(result.map?.mappings).toBeDefined()
121
+ })
122
+
123
+ it('supports target 18', async () => {
124
+ const result = await transformOxcReactCompiler(
125
+ '/project/Counter.tsx',
126
+ componentCode,
127
+ '18',
128
+ false
129
+ )
130
+
131
+ expect(result.code).toContain('_c(')
132
+ })
133
+ })
134
+
135
+ describe('compiler plugin multi-stage source map composition', () => {
136
+ it('composes earlier compiler maps when Babel also runs', async () => {
137
+ const { createVXRNCompilerPlugin } = await import('./index')
138
+ const { TraceMap, originalPositionFor } = await import('@jridgewell/trace-mapping')
139
+
140
+ const origSourceMap = process.env.VXRN_ENABLE_SOURCE_MAP
141
+ const origBabelGen = process.env.VXRN_USE_BABEL_FOR_GENERATORS
142
+ process.env.VXRN_ENABLE_SOURCE_MAP = '1'
143
+ process.env.VXRN_USE_BABEL_FOR_GENERATORS = '1'
144
+ configureVXRNCompilerPlugin({ enableCompiler: true })
145
+
146
+ const tempFile = path.join(os.tmpdir(), `Counter-${Date.now()}.tsx`)
147
+ fs.writeFileSync(
148
+ tempFile,
149
+ `import { useState } from 'react'
150
+
151
+ export function Counter() {
152
+ const [count, setCount] = useState(0)
153
+ return <button onClick={() => setCount(count + 1)}>{count}</button>
154
+ }
155
+
156
+ export async function* generatorProbe() {
157
+ throw new Error("MARKER")
158
+ }
159
+ `
160
+ )
161
+
162
+ try {
163
+ const plugins = await createVXRNCompilerPlugin()
164
+ const plugin = plugins.find((p: any) => p.name === 'one:compiler') as any
165
+ await plugin.configResolved({ root: os.tmpdir(), build: {} })
166
+ const hook = plugin.transform.handler || plugin.transform
167
+
168
+ const code = fs.readFileSync(tempFile, 'utf8')
169
+ const context = { environment: { name: 'client' } }
170
+ const result = await hook.call(context, code, tempFile)
171
+
172
+ expect(result).toBeDefined()
173
+ expect(result.map).toBeDefined()
174
+
175
+ const tracer = new TraceMap(result.map)
176
+ const lines = result.code.split('\n')
177
+ const markerLineIndex = lines.findIndex((l: string) => l.includes('MARKER'))
178
+ expect(markerLineIndex).toBeGreaterThan(0)
179
+ const markerCol = lines[markerLineIndex].indexOf('throw')
180
+
181
+ const pos = originalPositionFor(tracer, {
182
+ line: markerLineIndex + 1,
183
+ column: markerCol,
184
+ })
185
+
186
+ expect(pos.source).toBe(tempFile)
187
+ expect(pos.line).toBe(9)
188
+ } finally {
189
+ if (origSourceMap !== undefined) {
190
+ process.env.VXRN_ENABLE_SOURCE_MAP = origSourceMap
191
+ } else {
192
+ delete process.env.VXRN_ENABLE_SOURCE_MAP
193
+ }
194
+ if (origBabelGen !== undefined) {
195
+ process.env.VXRN_USE_BABEL_FOR_GENERATORS = origBabelGen
196
+ } else {
197
+ delete process.env.VXRN_USE_BABEL_FOR_GENERATORS
198
+ }
199
+ configureVXRNCompilerPlugin({ enableCompiler: false })
200
+ fs.rmSync(tempFile, { force: true })
201
+ }
202
+ })
203
+ })
204
+
205
+ describe('shared compiler worklets backend selection', () => {
206
+ it('retains Babel for worklets by default when enableNativeWorklets is false', async () => {
207
+ const { vi } = await import('vitest')
208
+ const { createVXRNCompilerPlugin } = await import('./index')
209
+ const workletsModule = await import('./transformWorklets')
210
+ const workletSpy = vi.spyOn(workletsModule, 'transformWorklets')
211
+
212
+ configureVXRNCompilerPlugin({
213
+ enableReanimated: true,
214
+ enableNativeWorklets: false,
215
+ })
216
+
217
+ const tempFile = path.join(process.cwd(), `test-default-worklet-${Date.now()}.tsx`)
218
+ const inputCode = `export function fn() { 'worklet'; return 1 }`
219
+ fs.writeFileSync(tempFile, inputCode)
220
+
221
+ try {
222
+ const plugins = await createVXRNCompilerPlugin()
223
+ const plugin = plugins.find((p: any) => p.name === 'one:compiler') as any
224
+ await plugin.configResolved({ root: process.cwd(), build: {} })
225
+ const hook = plugin.transform.handler || plugin.transform
226
+ const context = { environment: { name: 'ios' } }
227
+ const result = await hook.call(context, inputCode, tempFile)
228
+ expect(result).toBeDefined()
229
+ // With enableNativeWorklets: false, transformWorklets (SWC) is NOT called
230
+ expect(workletSpy).not.toHaveBeenCalled()
231
+ } finally {
232
+ workletSpy.mockRestore()
233
+ configureVXRNCompilerPlugin({
234
+ enableReanimated: false,
235
+ enableNativeWorklets: false,
236
+ })
237
+ fs.rmSync(tempFile, { force: true })
238
+ }
239
+ })
240
+
241
+ it('uses native SWC for worklets when enableNativeWorklets is true', async () => {
242
+ const compiler = await import('./index')
243
+ const { vi } = await import('vitest')
244
+ const workletsModule = await import('./transformWorklets')
245
+ const workletSpy = vi.spyOn(workletsModule, 'transformWorklets')
246
+
247
+ configureVXRNCompilerPlugin({
248
+ enableReanimated: true,
249
+ enableNativeWorklets: true,
250
+ })
251
+
252
+ const tempFile = path.join(process.cwd(), `test-native-worklet-${Date.now()}.tsx`)
253
+ const inputCode = `export function fn() { 'worklet'; return 1 }`
254
+ fs.writeFileSync(tempFile, inputCode)
255
+
256
+ try {
257
+ const plugins = await compiler.createVXRNCompilerPlugin()
258
+ const plugin = plugins.find((p: any) => p.name === 'one:compiler') as any
259
+ await plugin.configResolved({ root: process.cwd(), build: {} })
260
+ const hook = plugin.transform.handler || plugin.transform
261
+ const context = { environment: { name: 'ios' } }
262
+ const result = await hook.call(context, inputCode, tempFile)
263
+ expect(result).toBeDefined()
264
+ // With enableNativeWorklets: true, transformWorklets (SWC) IS called
265
+ expect(workletSpy).toHaveBeenCalled()
266
+ } finally {
267
+ workletSpy.mockRestore()
268
+ configureVXRNCompilerPlugin({
269
+ enableReanimated: false,
270
+ enableNativeWorklets: false,
271
+ })
272
+ fs.rmSync(tempFile, { force: true })
273
+ }
274
+ })
275
+
276
+ it('invalidates cache when toggling worklets backend on the same unchanged file', async () => {
277
+ const compiler = await import('./index')
278
+ const { vi } = await import('vitest')
279
+ const workletsModule = await import('./transformWorklets')
280
+ const workletSpy = vi.spyOn(workletsModule, 'transformWorklets')
281
+
282
+ const tempFile = path.join(process.cwd(), `test-cache-toggle-${Date.now()}.tsx`)
283
+ const inputCode = `export function fn() { 'worklet'; return 1 }`
284
+ fs.writeFileSync(tempFile, inputCode)
285
+
286
+ try {
287
+ // Step 1: Run with Babel backend (enableNativeWorklets: false)
288
+ configureVXRNCompilerPlugin({
289
+ enableReanimated: true,
290
+ enableNativeWorklets: false,
291
+ })
292
+
293
+ const plugins1 = await compiler.createVXRNCompilerPlugin()
294
+ const plugin1 = plugins1.find((p: any) => p.name === 'one:compiler') as any
295
+ await plugin1.configResolved({ root: process.cwd(), build: {} })
296
+ const hook1 = plugin1.transform.handler || plugin1.transform
297
+ const context = { environment: { name: 'ios' } }
298
+
299
+ const res1 = await hook1.call(context, inputCode, tempFile)
300
+ expect(res1).toBeDefined()
301
+ expect(workletSpy).toHaveBeenCalledTimes(0)
302
+
303
+ // Step 2: Toggle to native SWC backend on the SAME file without changes
304
+ configureVXRNCompilerPlugin({
305
+ enableReanimated: true,
306
+ enableNativeWorklets: true,
307
+ })
308
+
309
+ const plugins2 = await compiler.createVXRNCompilerPlugin()
310
+ const plugin2 = plugins2.find((p: any) => p.name === 'one:compiler') as any
311
+ await plugin2.configResolved({ root: process.cwd(), build: {} })
312
+ const hook2 = plugin2.transform.handler || plugin2.transform
313
+
314
+ const res2 = await hook2.call(context, inputCode, tempFile)
315
+ expect(res2).toBeDefined()
316
+ // Cache must NOT serve the Babel entry; native transformWorklets must be invoked
317
+ expect(workletSpy).toHaveBeenCalledTimes(1)
318
+
319
+ // Step 3: Call again with native SWC to verify caching works for the native backend
320
+ const res3 = await hook2.call(context, inputCode, tempFile)
321
+ expect(res3).toBeDefined()
322
+ expect(workletSpy).toHaveBeenCalledTimes(1)
323
+
324
+ // Step 4: Toggle back to Babel backend; cache must not serve SWC entry
325
+ configureVXRNCompilerPlugin({
326
+ enableReanimated: true,
327
+ enableNativeWorklets: false,
328
+ })
329
+
330
+ const plugins3 = await compiler.createVXRNCompilerPlugin()
331
+ const plugin3 = plugins3.find((p: any) => p.name === 'one:compiler') as any
332
+ await plugin3.configResolved({ root: process.cwd(), build: {} })
333
+ const hook3 = plugin3.transform.handler || plugin3.transform
334
+
335
+ const res4 = await hook3.call(context, inputCode, tempFile)
336
+ expect(res4).toBeDefined()
337
+ // workletSpy should still have only been called once
338
+ expect(workletSpy).toHaveBeenCalledTimes(1)
339
+ } finally {
340
+ workletSpy.mockRestore()
341
+ configureVXRNCompilerPlugin({
342
+ enableReanimated: false,
343
+ enableNativeWorklets: false,
344
+ })
345
+ fs.rmSync(tempFile, { force: true })
346
+ }
347
+ })
348
+ })
@@ -1,7 +1,9 @@
1
1
  import { extname, relative } from 'node:path'
2
- import babel from '@babel/core'
2
+ // type-only, so that importing this module does not drag babel in. every metro
3
+ // worker loads it through the package index, and on the native transform path
4
+ // babel is never called at all: loading it there is pure startup cost.
5
+ import type * as babel from '@babel/core'
3
6
  import { resolvePath } from '@vxrn/utils'
4
- import hermesParserPlugin from 'babel-plugin-syntax-hermes-parser'
5
7
  import { normalizePath } from 'vite'
6
8
  import { configuration } from './configure'
7
9
  import { asyncGeneratorRegex, debug } from './constants'
@@ -55,8 +57,14 @@ const getOptions = (props: Props, force = false): babel.TransformOptions | null
55
57
  }
56
58
 
57
59
  if (enableNativewind || shouldBabelReanimated(props)) {
58
- debug?.(`Using babel worklets on file ${props.id}`)
59
- plugins.push(resolvePath('react-native-worklets/plugin', props.projectRoot))
60
+ try {
61
+ const workletsPlugin = resolvePath(
62
+ 'react-native-worklets/plugin',
63
+ props.projectRoot
64
+ )
65
+ debug?.(`Using babel worklets on file ${props.id}`)
66
+ plugins.push(workletsPlugin)
67
+ } catch {}
60
68
  }
61
69
 
62
70
  if (shouldBabelReactCompiler(props)) {
@@ -64,11 +72,6 @@ const getOptions = (props: Props, force = false): babel.TransformOptions | null
64
72
  plugins.push(getBabelReactCompilerPlugin(props))
65
73
  }
66
74
 
67
- if (shouldBabelReactNativeCodegen(props)) {
68
- debug?.(`Using babel @react-native/babel-plugin-codegen on file`)
69
- plugins.push('@react-native/babel-plugin-codegen')
70
- }
71
-
72
75
  if (plugins.length) {
73
76
  return { plugins }
74
77
  }
@@ -88,11 +91,13 @@ const getOptions = (props: Props, force = false): babel.TransformOptions | null
88
91
  export async function transformOxcReactCompiler(
89
92
  id: string,
90
93
  code: string,
91
- target: '18' | '19'
94
+ target: '18' | '19',
95
+ sourceMap = false
92
96
  ) {
93
97
  const { transform } = await import('oxc-transform-react')
94
98
  const result = await transform(id, code, {
95
99
  jsx: 'preserve',
100
+ sourcemap: sourceMap,
96
101
  reactCompiler: { target },
97
102
  })
98
103
 
@@ -114,7 +119,7 @@ export async function transformOxcReactCompiler(
114
119
  )
115
120
  }
116
121
 
117
- return { code: result.code, map: undefined }
122
+ return { code: result.code, map: sourceMap ? (result.map as any) : undefined }
118
123
  }
119
124
 
120
125
  /**
@@ -125,6 +130,10 @@ export async function transformBabel(
125
130
  code: string,
126
131
  options: babel.TransformOptions
127
132
  ) {
133
+ const [{ default: babelCore }, { default: hermesParserPlugin }] = await Promise.all([
134
+ import('@babel/core'),
135
+ import('babel-plugin-syntax-hermes-parser'),
136
+ ])
128
137
  const extension = extname(id)
129
138
  const isTSX = extension === '.tsx'
130
139
  const isTS = isTSX || extension === '.ts'
@@ -166,7 +175,7 @@ export async function transformBabel(
166
175
  }
167
176
 
168
177
  return await new Promise<babel.BabelFileResult>((res, rej) => {
169
- babel.transform(code, babelOptions, (err: unknown, result) => {
178
+ babelCore.transform(code, babelOptions, (err: unknown, result) => {
170
179
  if (!result || err) {
171
180
  return rej(err || new Error(`[vxrn:compiler] babel returned no result for ${id}`))
172
181
  }
@@ -191,25 +200,6 @@ const getBasePlugins = ({ development }: Props) =>
191
200
  ],
192
201
  ] satisfies babel.PluginItem[]
193
202
 
194
- /**
195
- * ----- react native codegen ----
196
- */
197
-
198
- // Codegen specification files need to go through the react-native codegen babel plugin.
199
- // See:
200
- // * https://reactnative.dev/docs/fabric-native-components-introduction#1-define-specification-for-codegen
201
- // * https://reactnative.dev/docs/turbo-native-modules-introduction#1-declare-typed-specification
202
-
203
- const NATIVE_COMPONENT_RE = /NativeComponent\.[jt]sx?$/
204
- const SPEC_FILE_RE = /[/\\]specs?[/\\]/
205
-
206
- const shouldBabelReactNativeCodegen = ({ id, environment }: Props) => {
207
- return (
208
- (environment === 'ios' || environment === 'android') &&
209
- (NATIVE_COMPONENT_RE.test(id) || SPEC_FILE_RE.test(id))
210
- )
211
- }
212
-
213
203
  /**
214
204
  * ----- react compiler -----
215
205
  */
@@ -0,0 +1,282 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import * as vm from 'node:vm'
3
+ import { transformHermesLoops } from './transformHermesLoops'
4
+
5
+ /**
6
+ * Hermes emits one CreateFunctionEnvironment before a loop rather than one per
7
+ * iteration, and that environment covers the loop's head AND everything its
8
+ * body declares, so every lexical binding under a loop behaves there exactly
9
+ * like `var`. Rewriting them all that way reproduces Hermes on a spec-correct
10
+ * runtime, which is what makes these assertions meaningful: every one of them
11
+ * returns the wrong answer against the untransformed source.
12
+ *
13
+ * Verified against `hermesc -dump-bytecode`, which emits a single
14
+ * CreateFunctionEnvironment before the loop and a StoreToEnvironment into the
15
+ * same slot on each iteration.
16
+ */
17
+ function runAsHermes(code: string) {
18
+ const asHermesWouldSeeIt = code.replace(/\b(?:let|const)\b/g, 'var')
19
+ const sandbox: any = { result: undefined, console }
20
+ vm.runInNewContext(asHermesWouldSeeIt, sandbox)
21
+ return sandbox.result
22
+ }
23
+
24
+ function run(source: string) {
25
+ const out = transformHermesLoops(source, 'test.js')
26
+ return runAsHermes(out ? out.code : source)
27
+ }
28
+
29
+ describe('transformHermesLoops', () => {
30
+ it('gives a C-style loop a per-iteration binding', () => {
31
+ expect(
32
+ run(`
33
+ var fns = []
34
+ for (let i = 0; i < 3; i++) fns.push(() => i)
35
+ result = fns.map((f) => f())
36
+ `)
37
+ ).toEqual([0, 1, 2])
38
+ })
39
+
40
+ it('gives for-of and for-in per-iteration bindings', () => {
41
+ expect(
42
+ run(`
43
+ var fns = []
44
+ for (const label of ['a', 'b']) fns.push(() => label)
45
+ for (const key in { x: 1, y: 2 }) fns.push(() => key)
46
+ result = fns.map((f) => f())
47
+ `)
48
+ ).toEqual(['a', 'b', 'x', 'y'])
49
+ })
50
+
51
+ it('preserves break', () => {
52
+ expect(
53
+ run(`
54
+ var fns = []
55
+ for (let i = 0; i < 10; i++) {
56
+ if (i === 3) break
57
+ fns.push(() => i)
58
+ }
59
+ result = fns.map((f) => f())
60
+ `)
61
+ ).toEqual([0, 1, 2])
62
+ })
63
+
64
+ it('preserves continue', () => {
65
+ expect(
66
+ run(`
67
+ var fns = []
68
+ for (let i = 0; i < 5; i++) {
69
+ if (i % 2 === 0) continue
70
+ fns.push(() => i)
71
+ }
72
+ result = fns.map((f) => f())
73
+ `)
74
+ ).toEqual([1, 3])
75
+ })
76
+
77
+ it('preserves return out of the enclosing function', () => {
78
+ expect(
79
+ run(`
80
+ function find() {
81
+ for (const n of [1, 2, 3]) {
82
+ const get = () => n
83
+ if (n === 2) return get()
84
+ }
85
+ return 'none'
86
+ }
87
+ result = find()
88
+ `)
89
+ ).toBe(2)
90
+ })
91
+
92
+ it('preserves a bare return', () => {
93
+ expect(
94
+ run(`
95
+ var fns = []
96
+ function go() {
97
+ for (let i = 0; i < 5; i++) {
98
+ if (i === 2) return
99
+ fns.push(() => i)
100
+ }
101
+ }
102
+ go()
103
+ result = fns.map((f) => f())
104
+ `)
105
+ ).toEqual([0, 1])
106
+ })
107
+
108
+ it('leaves break and continue belonging to a nested loop alone', () => {
109
+ expect(
110
+ run(`
111
+ var fns = []
112
+ for (let i = 0; i < 2; i++) {
113
+ for (var j = 0; j < 5; j++) {
114
+ if (j > 0) break
115
+ }
116
+ fns.push(() => i)
117
+ }
118
+ result = fns.map((f) => f())
119
+ `)
120
+ ).toEqual([0, 1])
121
+ })
122
+
123
+ it('handles nested capturing loops', () => {
124
+ expect(
125
+ run(`
126
+ var fns = []
127
+ for (let i = 0; i < 2; i++) {
128
+ for (let j = 0; j < 2; j++) {
129
+ fns.push(() => i + ':' + j)
130
+ }
131
+ }
132
+ result = fns.map((f) => f())
133
+ `)
134
+ ).toEqual(['0:0', '0:1', '1:0', '1:1'])
135
+ })
136
+
137
+ it('handles await in the loop body', async () => {
138
+ const out = transformHermesLoops(
139
+ `
140
+ var fns = []
141
+ async function go() {
142
+ for (const n of [1, 2]) {
143
+ await Promise.resolve()
144
+ fns.push(() => n)
145
+ }
146
+ return fns.map((f) => f())
147
+ }
148
+ result = go()
149
+ `,
150
+ 'test.js'
151
+ )
152
+ expect(out).not.toBeNull()
153
+ await expect(runAsHermes(out!.code)).resolves.toEqual([1, 2])
154
+ })
155
+
156
+ it('destructures a for-of binding per iteration', () => {
157
+ expect(
158
+ run(`
159
+ var fns = []
160
+ for (const { id } of [{ id: 'a' }, { id: 'b' }]) fns.push(() => id)
161
+ result = fns.map((f) => f())
162
+ `)
163
+ ).toEqual(['a', 'b'])
164
+ })
165
+
166
+ it('rewrites the esbuild interop helper that broke every multi-export module', () => {
167
+ // this exact shape is what esbuild inlines into every commonjs-interop
168
+ // module, and under Hermes it made each named export resolve to the last one
169
+ expect(
170
+ run(`
171
+ var __defProp = Object.defineProperty
172
+ var __getOwnPropNames = Object.getOwnPropertyNames
173
+ var from = { a: 1, b: 2 }
174
+ var to = {}
175
+ for (let key of __getOwnPropNames(from)) {
176
+ __defProp(to, key, { get: () => from[key], enumerable: true })
177
+ }
178
+ result = [to.a, to.b]
179
+ `)
180
+ ).toEqual([1, 2])
181
+ })
182
+
183
+ it('leaves loops that never capture their binding untouched', () => {
184
+ expect(
185
+ transformHermesLoops('for (let i = 0; i < 3; i++) sum += i', 'test.js')
186
+ ).toBeNull()
187
+ })
188
+
189
+ it('gives a binding declared in the loop body a per-iteration copy', () => {
190
+ // react-native's NativeAnimatedHelper builds one wrapper per method this
191
+ // way; sharing `methodName` made every wrapper resolve the last method.
192
+ expect(
193
+ run(`
194
+ var names = ['createAnimatedNode', 'addListener', 'removeListener']
195
+ var wrappers = {}
196
+ for (var ii = 0; ii < names.length; ii++) {
197
+ const methodName = names[ii]
198
+ wrappers[methodName] = () => methodName
199
+ }
200
+ result = names.map((n) => wrappers[n]())
201
+ `)
202
+ ).toEqual(['createAnimatedNode', 'addListener', 'removeListener'])
203
+ })
204
+
205
+ it('gives a binding declared in a nested block of the loop body a per-iteration copy', () => {
206
+ expect(
207
+ run(`
208
+ var fns = []
209
+ for (var i = 0; i < 3; i++) {
210
+ if (i >= 0) {
211
+ const doubled = i * 2
212
+ fns.push(() => doubled)
213
+ }
214
+ }
215
+ result = fns.map((f) => f())
216
+ `)
217
+ ).toEqual([0, 2, 4])
218
+ })
219
+
220
+ it('keeps `this` and `arguments` meaning what they meant outside the loop', () => {
221
+ // reanimated's PropsFilter reads `this._map` from a closure inside a
222
+ // `for (const key in props)` body, so a lifted body that rebound `this`
223
+ // crashed every animated component.
224
+ expect(
225
+ run(`
226
+ class PropsFilter {
227
+ constructor() { this._map = { a: 'A', b: 'B' } }
228
+ filter(props) {
229
+ var out = []
230
+ for (const key in props) {
231
+ const label = key
232
+ out.push(() => this._map[label] + arguments.length)
233
+ }
234
+ return out
235
+ }
236
+ }
237
+ result = new PropsFilter().filter({ a: 1, b: 2 }).map((f) => f())
238
+ `)
239
+ ).toEqual(['A1', 'B1'])
240
+ })
241
+
242
+ it('leaves a loop with no lexical binding untouched', () => {
243
+ expect(transformHermesLoops('for (var i = 0; i < 3; i++) fns.push(() => i)', 'test.js')).toBeNull()
244
+ })
245
+
246
+ it('leaves the head declaration alone so it cannot collide with a sibling', () => {
247
+ // react-native-gesture-handler's Pressable declares `const gesture` in the
248
+ // same block as `for (const gesture of gestures)`. hoisting the head to
249
+ // `var` made that a redeclaration and failed the whole bundle to parse.
250
+ const out = transformHermesLoops(
251
+ `function build(gestures) {
252
+ var handlers = []
253
+ for (const gesture of gestures) { handlers.push(() => gesture) }
254
+ const gesture = gestures[0]
255
+ return gesture
256
+ }`,
257
+ 'Pressable.js'
258
+ )
259
+ // a redeclaration is a SyntaxError, so compiling is the whole assertion
260
+ expect(() => new vm.Script(out!.code)).not.toThrow()
261
+ })
262
+
263
+ it('rewrites a loop in a .js file that still contains jsx', () => {
264
+ // react-native and its dependencies ship jsx inside plain `.js`, and the
265
+ // rolldown native pipeline keeps jsx unlowered until after this transform.
266
+ const out = transformHermesLoops(
267
+ 'for (const c of items) rows.push(() => <Row key={c} />)',
268
+ 'Rows.js'
269
+ )
270
+ expect(out?.code).toContain('_hermesLoop0')
271
+ })
272
+
273
+ it('does not rewrite a loop that reassigns its own binding', () => {
274
+ // the update would be stranded inside the lifted function
275
+ expect(
276
+ transformHermesLoops(
277
+ 'for (let i = 0; i < 3; i++) { i += 1; fns.push(() => i) }',
278
+ 'test.js'
279
+ )
280
+ ).toBeNull()
281
+ })
282
+ })