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