@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.
- package/dist/cjs/cache.cjs +3 -1
- package/dist/cjs/configure.cjs +9 -1
- package/dist/cjs/index.cjs +82 -4
- package/dist/cjs/reactNativeCodegen.cjs +140 -0
- package/dist/cjs/reactNativeViewConfig.cjs +489 -0
- package/dist/cjs/transformBabel.cjs +15 -20
- package/dist/cjs/transformBabel.test.cjs +260 -0
- package/dist/cjs/transformHermesLoops.cjs +317 -0
- package/dist/cjs/transformHermesLoops.test.cjs +220 -0
- package/dist/cjs/transformSWC.cjs +102 -137
- package/dist/cjs/transformSWC.test.cjs +74 -0
- package/dist/cjs/transformWorklets.cjs +121 -0
- package/dist/cjs/transformWorklets.test.cjs +604 -0
- package/dist/cjs/worklets/autoworklet.cjs +265 -0
- package/dist/cjs/worklets/globals.cjs +71 -0
- package/dist/cjs/worklets/hash.cjs +38 -0
- package/dist/cjs/worklets/scope.cjs +370 -0
- package/dist/cjs/worklets/serialize.cjs +101 -0
- package/dist/cjs/worklets/transform.cjs +232 -0
- package/dist/cjs/worklets/types.cjs +18 -0
- package/dist/esm/cache.mjs +4 -2
- package/dist/esm/cache.mjs.map +1 -1
- package/dist/esm/configure.mjs +8 -1
- package/dist/esm/configure.mjs.map +1 -1
- package/dist/esm/index.js +72 -5
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/index.mjs +72 -5
- package/dist/esm/index.mjs.map +1 -1
- package/dist/esm/reactNativeCodegen.mjs +104 -0
- package/dist/esm/reactNativeCodegen.mjs.map +1 -0
- package/dist/esm/reactNativeViewConfig.mjs +460 -0
- package/dist/esm/reactNativeViewConfig.mjs.map +1 -0
- package/dist/esm/transformBabel.mjs +14 -19
- package/dist/esm/transformBabel.mjs.map +1 -1
- package/dist/esm/transformBabel.test.mjs +261 -1
- package/dist/esm/transformBabel.test.mjs.map +1 -1
- package/dist/esm/transformHermesLoops.mjs +281 -0
- package/dist/esm/transformHermesLoops.mjs.map +1 -0
- package/dist/esm/transformHermesLoops.test.mjs +197 -0
- package/dist/esm/transformHermesLoops.test.mjs.map +1 -0
- package/dist/esm/transformSWC.mjs +89 -138
- package/dist/esm/transformSWC.mjs.map +1 -1
- package/dist/esm/transformSWC.test.mjs +75 -0
- package/dist/esm/transformSWC.test.mjs.map +1 -0
- package/dist/esm/transformWorklets.mjs +81 -0
- package/dist/esm/transformWorklets.mjs.map +1 -0
- package/dist/esm/transformWorklets.test.mjs +581 -0
- package/dist/esm/transformWorklets.test.mjs.map +1 -0
- package/dist/esm/worklets/autoworklet.mjs +234 -0
- package/dist/esm/worklets/autoworklet.mjs.map +1 -0
- package/dist/esm/worklets/globals.mjs +45 -0
- package/dist/esm/worklets/globals.mjs.map +1 -0
- package/dist/esm/worklets/hash.mjs +13 -0
- package/dist/esm/worklets/hash.mjs.map +1 -0
- package/dist/esm/worklets/scope.mjs +345 -0
- package/dist/esm/worklets/scope.mjs.map +1 -0
- package/dist/esm/worklets/serialize.mjs +75 -0
- package/dist/esm/worklets/serialize.mjs.map +1 -0
- package/dist/esm/worklets/transform.mjs +196 -0
- package/dist/esm/worklets/transform.mjs.map +1 -0
- package/dist/esm/worklets/types.mjs +2 -0
- package/dist/esm/worklets/types.mjs.map +1 -0
- package/package.json +10 -6
- package/src/cache.ts +9 -2
- package/src/configure.ts +12 -0
- package/src/index.ts +93 -13
- package/src/reactNativeCodegen.ts +181 -0
- package/src/reactNativeViewConfig.ts +593 -0
- package/src/transformBabel.test.ts +268 -1
- package/src/transformBabel.ts +21 -31
- package/src/transformHermesLoops.test.ts +282 -0
- package/src/transformHermesLoops.ts +433 -0
- package/src/transformSWC.test.ts +88 -0
- package/src/transformSWC.ts +109 -160
- package/src/transformWorklets.test.ts +708 -0
- package/src/transformWorklets.ts +160 -0
- package/src/types.ts +4 -4
- package/src/worklets/autoworklet.ts +391 -0
- package/src/worklets/globals.ts +125 -0
- package/src/worklets/hash.ts +14 -0
- package/src/worklets/scope.ts +401 -0
- package/src/worklets/serialize.ts +114 -0
- package/src/worklets/transform.ts +282 -0
- package/src/worklets/types.ts +26 -0
- package/types/cache.d.ts +1 -0
- package/types/cache.d.ts.map +1 -1
- package/types/configure.d.ts +2 -0
- package/types/configure.d.ts.map +1 -1
- package/types/index.d.ts +3 -0
- package/types/index.d.ts.map +1 -1
- package/types/reactNativeCodegen.d.ts +5 -0
- package/types/reactNativeCodegen.d.ts.map +1 -0
- package/types/reactNativeViewConfig.d.ts +35 -0
- package/types/reactNativeViewConfig.d.ts.map +1 -0
- package/types/transformBabel.d.ts +3 -3
- package/types/transformBabel.d.ts.map +1 -1
- package/types/transformHermesLoops.d.ts +5 -0
- package/types/transformHermesLoops.d.ts.map +1 -0
- package/types/transformHermesLoops.test.d.ts +2 -0
- package/types/transformHermesLoops.test.d.ts.map +1 -0
- package/types/transformSWC.d.ts +12 -3
- package/types/transformSWC.d.ts.map +1 -1
- package/types/transformSWC.test.d.ts +2 -0
- package/types/transformSWC.test.d.ts.map +1 -0
- package/types/transformWorklets.d.ts +22 -0
- package/types/transformWorklets.d.ts.map +1 -0
- package/types/transformWorklets.test.d.ts +2 -0
- package/types/transformWorklets.test.d.ts.map +1 -0
- package/types/types.d.ts +4 -4
- package/types/types.d.ts.map +1 -1
- package/types/worklets/autoworklet.d.ts +14 -0
- package/types/worklets/autoworklet.d.ts.map +1 -0
- package/types/worklets/globals.d.ts +3 -0
- package/types/worklets/globals.d.ts.map +1 -0
- package/types/worklets/hash.d.ts +5 -0
- package/types/worklets/hash.d.ts.map +1 -0
- package/types/worklets/scope.d.ts +7 -0
- package/types/worklets/scope.d.ts.map +1 -0
- package/types/worklets/serialize.d.ts +12 -0
- package/types/worklets/serialize.d.ts.map +1 -0
- package/types/worklets/transform.d.ts +6 -0
- package/types/worklets/transform.d.ts.map +1 -0
- package/types/worklets/types.d.ts +21 -0
- 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 {
|
|
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
|
+
})
|
package/src/transformBabel.ts
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { extname, relative } from 'node:path'
|
|
2
|
-
|
|
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
|
-
|
|
59
|
-
|
|
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
|
-
|
|
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
|
+
})
|