@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
|
@@ -0,0 +1,433 @@
|
|
|
1
|
+
import MagicString from 'magic-string'
|
|
2
|
+
import { parseSync } from 'oxc-parser'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Hermes does not give a loop a per-iteration binding, for its head OR for
|
|
6
|
+
* anything its body declares. It emits one CreateFunctionEnvironment before the
|
|
7
|
+
* loop, so every closure created in the body captures the same variable and
|
|
8
|
+
* observes its final value:
|
|
9
|
+
*
|
|
10
|
+
* for (let i = 0; i < 2; i++) fns.push(() => i)
|
|
11
|
+
* fns.map(f => f()) // [2, 2] on Hermes, [0, 1] everywhere else
|
|
12
|
+
*
|
|
13
|
+
* The body form bites just as hard, and is what breaks react-native's own
|
|
14
|
+
* NativeAnimatedHelper, where every native operation wrapper ends up resolving
|
|
15
|
+
* the last method name in the list:
|
|
16
|
+
*
|
|
17
|
+
* for (var i = 0; i < 2; i++) { const j = i; fns.push(() => j) }
|
|
18
|
+
*
|
|
19
|
+
* Verified with hermesc -dump-bytecode and on device. Nothing else in the
|
|
20
|
+
* pipeline can lower it: oxc's target floor is es2015 and no target it accepts
|
|
21
|
+
* rewrites loop bindings, and esbuild refuses ("Transforming let to the
|
|
22
|
+
* configured target environment is not supported yet") at every target.
|
|
23
|
+
*
|
|
24
|
+
* A function call is the only construct that gets a fresh environment per
|
|
25
|
+
* invocation, so a loop that captures its binding becomes:
|
|
26
|
+
*
|
|
27
|
+
* var _loop = (i) => { ...body... }
|
|
28
|
+
* for (var i = 0; i < 2; i++) _loop(i)
|
|
29
|
+
*
|
|
30
|
+
* Only loops that actually capture their binding in a closure are touched, so
|
|
31
|
+
* this is a no-op for the overwhelming majority of loops.
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
// only a loop can share a binding this way, and only a lexical declaration can
|
|
35
|
+
// be shared, so a module missing either cannot be affected and is never parsed.
|
|
36
|
+
const LOOP_RE = /\b(?:for|while)\s*(?:await\s+)?\(/
|
|
37
|
+
const LEXICAL_RE = /\b(?:let|const|class)\b/
|
|
38
|
+
|
|
39
|
+
type Loop = {
|
|
40
|
+
node: any
|
|
41
|
+
names: string[]
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function langFor(filename: string) {
|
|
45
|
+
const ext = filename.split('?')[0].split('.').pop() || 'js'
|
|
46
|
+
// a .ts file is not tsx (`const f = <T>(x: T) => x` is a type parameter
|
|
47
|
+
// there, an unclosed element under tsx), but everything else is parsed as
|
|
48
|
+
// jsx: react-native ships jsx inside plain .js, and oxc's `js` rejects it.
|
|
49
|
+
if (ext === 'ts' || ext === 'cts' || ext === 'mts') return 'ts' as const
|
|
50
|
+
if (ext === 'tsx') return 'tsx' as const
|
|
51
|
+
return 'jsx' as const
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function collectPatternNames(node: any, out: string[]) {
|
|
55
|
+
if (!node || typeof node !== 'object') return
|
|
56
|
+
switch (node.type) {
|
|
57
|
+
case 'Identifier':
|
|
58
|
+
out.push(node.name)
|
|
59
|
+
return
|
|
60
|
+
case 'ObjectPattern':
|
|
61
|
+
for (const p of node.properties || []) {
|
|
62
|
+
collectPatternNames(p.value ?? p.argument, out)
|
|
63
|
+
}
|
|
64
|
+
return
|
|
65
|
+
case 'ArrayPattern':
|
|
66
|
+
for (const el of node.elements || []) collectPatternNames(el, out)
|
|
67
|
+
return
|
|
68
|
+
case 'AssignmentPattern':
|
|
69
|
+
collectPatternNames(node.left, out)
|
|
70
|
+
return
|
|
71
|
+
case 'RestElement':
|
|
72
|
+
collectPatternNames(node.argument, out)
|
|
73
|
+
return
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const FUNCTION_TYPES = new Set([
|
|
78
|
+
'FunctionDeclaration',
|
|
79
|
+
'FunctionExpression',
|
|
80
|
+
'ArrowFunctionExpression',
|
|
81
|
+
])
|
|
82
|
+
|
|
83
|
+
function eachChild(node: any, fn: (child: any) => void) {
|
|
84
|
+
for (const key in node) {
|
|
85
|
+
if (key === 'type' || key === 'start' || key === 'end' || key === 'parent') continue
|
|
86
|
+
const value = node[key]
|
|
87
|
+
if (Array.isArray(value)) {
|
|
88
|
+
for (const item of value) {
|
|
89
|
+
if (item && typeof item.type === 'string') fn(item)
|
|
90
|
+
}
|
|
91
|
+
} else if (value && typeof value === 'object' && typeof value.type === 'string') {
|
|
92
|
+
fn(value)
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** does any closure inside `body` read one of `names`? */
|
|
98
|
+
function capturesInClosure(body: any, names: Set<string>): boolean {
|
|
99
|
+
let found = false
|
|
100
|
+
|
|
101
|
+
function scanInsideFunction(node: any, shadowed: Set<string>) {
|
|
102
|
+
if (found || !node || typeof node !== 'object') return
|
|
103
|
+
if (node.type === 'Identifier' && names.has(node.name) && !shadowed.has(node.name)) {
|
|
104
|
+
found = true
|
|
105
|
+
return
|
|
106
|
+
}
|
|
107
|
+
if (FUNCTION_TYPES.has(node.type)) {
|
|
108
|
+
const inner = new Set(shadowed)
|
|
109
|
+
const params: string[] = []
|
|
110
|
+
for (const p of node.params || []) collectPatternNames(p, params)
|
|
111
|
+
for (const p of params) inner.add(p)
|
|
112
|
+
if (node.id?.name) inner.add(node.id.name)
|
|
113
|
+
eachChild(node, (c) => scanInsideFunction(c, inner))
|
|
114
|
+
return
|
|
115
|
+
}
|
|
116
|
+
// a member expression's property is not a reference to the binding
|
|
117
|
+
if (node.type === 'MemberExpression' && !node.computed) {
|
|
118
|
+
scanInsideFunction(node.object, shadowed)
|
|
119
|
+
return
|
|
120
|
+
}
|
|
121
|
+
if (node.type === 'Property' && !node.computed && node.key === node.value) {
|
|
122
|
+
// shorthand `{ i }` still reads i
|
|
123
|
+
scanInsideFunction(node.value, shadowed)
|
|
124
|
+
return
|
|
125
|
+
}
|
|
126
|
+
eachChild(node, (c) => scanInsideFunction(c, shadowed))
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function walk(node: any) {
|
|
130
|
+
if (found || !node || typeof node !== 'object') return
|
|
131
|
+
if (FUNCTION_TYPES.has(node.type)) {
|
|
132
|
+
const shadowed = new Set<string>()
|
|
133
|
+
const params: string[] = []
|
|
134
|
+
for (const p of node.params || []) collectPatternNames(p, params)
|
|
135
|
+
for (const p of params) shadowed.add(p)
|
|
136
|
+
eachChild(node, (c) => scanInsideFunction(c, shadowed))
|
|
137
|
+
return
|
|
138
|
+
}
|
|
139
|
+
eachChild(node, walk)
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
walk(body)
|
|
143
|
+
return found
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
type BodyFacts = {
|
|
147
|
+
hasBreak: boolean
|
|
148
|
+
hasContinue: boolean
|
|
149
|
+
hasReturn: boolean
|
|
150
|
+
hasAwait: boolean
|
|
151
|
+
hasYield: boolean
|
|
152
|
+
assignsBinding: boolean
|
|
153
|
+
hasLabeledJump: boolean
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Control flow that has to survive being moved into a function body. `break`
|
|
158
|
+
* and `continue` are only relevant when they belong to THIS loop, so nested
|
|
159
|
+
* loops and switches are not descended into for those.
|
|
160
|
+
*/
|
|
161
|
+
function analyzeBody(body: any, names: Set<string>): BodyFacts {
|
|
162
|
+
const facts: BodyFacts = {
|
|
163
|
+
hasBreak: false,
|
|
164
|
+
hasContinue: false,
|
|
165
|
+
hasReturn: false,
|
|
166
|
+
hasAwait: false,
|
|
167
|
+
hasYield: false,
|
|
168
|
+
assignsBinding: false,
|
|
169
|
+
hasLabeledJump: false,
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function walk(node: any, inNestedBreakable: boolean, inNestedLoop: boolean, inFunction: boolean) {
|
|
173
|
+
if (!node || typeof node !== 'object') return
|
|
174
|
+
|
|
175
|
+
if (FUNCTION_TYPES.has(node.type)) {
|
|
176
|
+
eachChild(node, (c) => walk(c, inNestedBreakable, inNestedLoop, true))
|
|
177
|
+
return
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
switch (node.type) {
|
|
181
|
+
case 'BreakStatement':
|
|
182
|
+
if (node.label) facts.hasLabeledJump = true
|
|
183
|
+
else if (!inNestedBreakable) facts.hasBreak = true
|
|
184
|
+
break
|
|
185
|
+
case 'ContinueStatement':
|
|
186
|
+
if (node.label) facts.hasLabeledJump = true
|
|
187
|
+
else if (!inNestedLoop) facts.hasContinue = true
|
|
188
|
+
break
|
|
189
|
+
case 'ReturnStatement':
|
|
190
|
+
if (!inFunction) facts.hasReturn = true
|
|
191
|
+
break
|
|
192
|
+
case 'AwaitExpression':
|
|
193
|
+
if (!inFunction) facts.hasAwait = true
|
|
194
|
+
break
|
|
195
|
+
case 'YieldExpression':
|
|
196
|
+
if (!inFunction) facts.hasYield = true
|
|
197
|
+
break
|
|
198
|
+
case 'AssignmentExpression':
|
|
199
|
+
if (node.left?.type === 'Identifier' && names.has(node.left.name)) {
|
|
200
|
+
facts.assignsBinding = true
|
|
201
|
+
}
|
|
202
|
+
break
|
|
203
|
+
case 'UpdateExpression':
|
|
204
|
+
if (node.argument?.type === 'Identifier' && names.has(node.argument.name)) {
|
|
205
|
+
facts.assignsBinding = true
|
|
206
|
+
}
|
|
207
|
+
break
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const isLoop =
|
|
211
|
+
node.type === 'ForStatement' ||
|
|
212
|
+
node.type === 'ForOfStatement' ||
|
|
213
|
+
node.type === 'ForInStatement' ||
|
|
214
|
+
node.type === 'WhileStatement' ||
|
|
215
|
+
node.type === 'DoWhileStatement'
|
|
216
|
+
const isBreakable = isLoop || node.type === 'SwitchStatement'
|
|
217
|
+
|
|
218
|
+
eachChild(node, (c) =>
|
|
219
|
+
walk(c, inNestedBreakable || isBreakable, inNestedLoop || isLoop, inFunction)
|
|
220
|
+
)
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
walk(body, false, false, false)
|
|
224
|
+
return facts
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Lexical names the body itself declares. Nested functions are skipped, since
|
|
229
|
+
* their declarations already get a fresh environment per call, but nested
|
|
230
|
+
* blocks and nested loop heads are not: Hermes gives the whole loop one
|
|
231
|
+
* environment, so anything declared anywhere under it is shared too.
|
|
232
|
+
*/
|
|
233
|
+
function collectBodyLexicalNames(node: any, out: string[]) {
|
|
234
|
+
if (!node || typeof node !== 'object' || typeof node.type !== 'string') return
|
|
235
|
+
if (FUNCTION_TYPES.has(node.type) || node.type === 'ClassBody') return
|
|
236
|
+
if (node.type === 'VariableDeclaration') {
|
|
237
|
+
if (node.kind === 'let' || node.kind === 'const') {
|
|
238
|
+
for (const d of node.declarations || []) collectPatternNames(d.id, out)
|
|
239
|
+
}
|
|
240
|
+
return
|
|
241
|
+
}
|
|
242
|
+
if (node.type === 'ClassDeclaration') {
|
|
243
|
+
if (node.id?.name) out.push(node.id.name)
|
|
244
|
+
return
|
|
245
|
+
}
|
|
246
|
+
eachChild(node, (c) => collectBodyLexicalNames(c, out))
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
const LOOP_TYPES = new Set([
|
|
250
|
+
'ForStatement',
|
|
251
|
+
'ForOfStatement',
|
|
252
|
+
'ForInStatement',
|
|
253
|
+
'WhileStatement',
|
|
254
|
+
'DoWhileStatement',
|
|
255
|
+
])
|
|
256
|
+
|
|
257
|
+
function headDeclaration(node: any): any {
|
|
258
|
+
if (node.type === 'ForStatement') return node.init
|
|
259
|
+
if (node.type === 'ForOfStatement' || node.type === 'ForInStatement') return node.left
|
|
260
|
+
return null
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* One pass. Only the outermost qualifying loop of a nested chain is rewritten,
|
|
265
|
+
* because rewriting an inner loop whose text is about to be moved would produce
|
|
266
|
+
* overlapping edits. The caller re-runs until the code stops changing, which
|
|
267
|
+
* picks up inner loops on later passes.
|
|
268
|
+
*/
|
|
269
|
+
function transformOnce(
|
|
270
|
+
code: string,
|
|
271
|
+
filename: string
|
|
272
|
+
): { code: string; map: any } | null {
|
|
273
|
+
const parsed = parseSync(filename, code, { lang: langFor(filename) })
|
|
274
|
+
if (!parsed?.program || parsed.errors?.length) return null
|
|
275
|
+
|
|
276
|
+
const candidates: Loop[] = []
|
|
277
|
+
|
|
278
|
+
function findLoops(node: any, insideCandidate: boolean) {
|
|
279
|
+
if (!node || typeof node !== 'object') return
|
|
280
|
+
|
|
281
|
+
let isCandidate = false
|
|
282
|
+
if (LOOP_TYPES.has(node.type) && node.body) {
|
|
283
|
+
// only head names become parameters of the lifted function; body
|
|
284
|
+
// declarations are made fresh just by moving into a function body.
|
|
285
|
+
const names: string[] = []
|
|
286
|
+
const decl = headDeclaration(node)
|
|
287
|
+
if (decl?.type === 'VariableDeclaration' && (decl.kind === 'let' || decl.kind === 'const')) {
|
|
288
|
+
for (const d of decl.declarations || []) collectPatternNames(d.id, names)
|
|
289
|
+
}
|
|
290
|
+
const shared = [...names]
|
|
291
|
+
collectBodyLexicalNames(node.body, shared)
|
|
292
|
+
if (shared.length && capturesInClosure(node.body, new Set(shared))) {
|
|
293
|
+
isCandidate = true
|
|
294
|
+
if (!insideCandidate) candidates.push({ node, names })
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
eachChild(node, (c) => findLoops(c, insideCandidate || isCandidate))
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
findLoops(parsed.program, false)
|
|
302
|
+
if (!candidates.length) return null
|
|
303
|
+
|
|
304
|
+
const s = new MagicString(code)
|
|
305
|
+
let counter = 0
|
|
306
|
+
let changed = false
|
|
307
|
+
|
|
308
|
+
for (const { node, names } of candidates) {
|
|
309
|
+
const nameSet = new Set(names)
|
|
310
|
+
const facts = analyzeBody(node.body, nameSet)
|
|
311
|
+
|
|
312
|
+
// a generator body cannot be lifted into a plain function, and reassigning
|
|
313
|
+
// the loop binding inside the body would strand the update in the callee.
|
|
314
|
+
// both are rare and left alone rather than rewritten incorrectly.
|
|
315
|
+
if (facts.hasYield || facts.assignsBinding || facts.hasLabeledJump) continue
|
|
316
|
+
|
|
317
|
+
// the head declaration is left exactly as written. lowering it to `var`
|
|
318
|
+
// hoists it out of the loop, where it collides with any sibling `const` of
|
|
319
|
+
// the same name (react-native-gesture-handler's Pressable declares `const
|
|
320
|
+
// gesture` right after a `for (const gesture of gestures)`).
|
|
321
|
+
const fnName = `_hermesLoop${counter++}`
|
|
322
|
+
const params = names.join(', ')
|
|
323
|
+
|
|
324
|
+
let bodyText = code.slice(node.body.start, node.body.end)
|
|
325
|
+
if (node.body.type !== 'BlockStatement') {
|
|
326
|
+
bodyText = `{ ${bodyText} }`
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
if (facts.hasBreak || facts.hasReturn || facts.hasContinue) {
|
|
330
|
+
bodyText = rewriteJumps(bodyText, node.body, code, nameSet)
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
const asyncKw = facts.hasAwait ? 'async ' : ''
|
|
334
|
+
const awaitKw = facts.hasAwait ? 'await ' : ''
|
|
335
|
+
|
|
336
|
+
let call = `${awaitKw}${fnName}(${params})`
|
|
337
|
+
if (facts.hasBreak || facts.hasReturn) {
|
|
338
|
+
const checks: string[] = []
|
|
339
|
+
if (facts.hasBreak) checks.push('if (_r === 1) break;')
|
|
340
|
+
if (facts.hasReturn) checks.push('if (_r) return _r.v;')
|
|
341
|
+
call = `{ var _r = ${call}; ${checks.join(' ')} }`
|
|
342
|
+
} else {
|
|
343
|
+
call = `{ ${call}; }`
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
s.overwrite(node.body.start, node.body.end, call)
|
|
347
|
+
// an arrow, so `this`, `arguments`, `new.target` and `super` inside the body
|
|
348
|
+
// still mean what they meant in the function that contained the loop.
|
|
349
|
+
s.appendLeft(node.start, `var ${fnName} = ${asyncKw}(${params}) => ${bodyText};\n`)
|
|
350
|
+
changed = true
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
if (!changed) return null
|
|
354
|
+
return {
|
|
355
|
+
code: s.toString(),
|
|
356
|
+
map: s.generateMap({ hires: true, source: filename, includeContent: false }),
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/**
|
|
361
|
+
* `break` leaves the loop and `return` leaves the enclosing function, but once
|
|
362
|
+
* the body is a function neither reaches its original target. They become
|
|
363
|
+
* return values the call site acts on: 1 for break, {v} for return. A normal
|
|
364
|
+
* completion and a `continue` both return undefined, which is exactly right,
|
|
365
|
+
* since falling out of _loop continues the loop.
|
|
366
|
+
*/
|
|
367
|
+
function rewriteJumps(bodyText: string, bodyNode: any, code: string, names: Set<string>): string {
|
|
368
|
+
const offset = bodyNode.start
|
|
369
|
+
const s = new MagicString(bodyText)
|
|
370
|
+
const pad = bodyNode.type === 'BlockStatement' ? 0 : 2
|
|
371
|
+
|
|
372
|
+
function walk(node: any, inNestedBreakable: boolean, inNestedLoop: boolean, inFunction: boolean) {
|
|
373
|
+
if (!node || typeof node !== 'object') return
|
|
374
|
+
|
|
375
|
+
if (FUNCTION_TYPES.has(node.type)) {
|
|
376
|
+
eachChild(node, (c) => walk(c, inNestedBreakable, inNestedLoop, true))
|
|
377
|
+
return
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
if (node.type === 'BreakStatement' && !node.label && !inNestedBreakable) {
|
|
381
|
+
s.overwrite(node.start - offset + pad, node.end - offset + pad, 'return 1;')
|
|
382
|
+
return
|
|
383
|
+
}
|
|
384
|
+
if (node.type === 'ContinueStatement' && !node.label && !inNestedLoop) {
|
|
385
|
+
s.overwrite(node.start - offset + pad, node.end - offset + pad, 'return;')
|
|
386
|
+
return
|
|
387
|
+
}
|
|
388
|
+
if (node.type === 'ReturnStatement' && !inFunction) {
|
|
389
|
+
const arg = node.argument
|
|
390
|
+
const replacement = arg
|
|
391
|
+
? `return { v: ${code.slice(arg.start, arg.end)} };`
|
|
392
|
+
: 'return { v: undefined };'
|
|
393
|
+
s.overwrite(node.start - offset + pad, node.end - offset + pad, replacement)
|
|
394
|
+
return
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
const isLoop =
|
|
398
|
+
node.type === 'ForStatement' ||
|
|
399
|
+
node.type === 'ForOfStatement' ||
|
|
400
|
+
node.type === 'ForInStatement' ||
|
|
401
|
+
node.type === 'WhileStatement' ||
|
|
402
|
+
node.type === 'DoWhileStatement'
|
|
403
|
+
const isBreakable = isLoop || node.type === 'SwitchStatement'
|
|
404
|
+
|
|
405
|
+
eachChild(node, (c) =>
|
|
406
|
+
walk(c, inNestedBreakable || isBreakable, inNestedLoop || isLoop, inFunction)
|
|
407
|
+
)
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
walk(bodyNode, false, false, false)
|
|
411
|
+
return s.toString()
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
export function transformHermesLoops(
|
|
415
|
+
code: string,
|
|
416
|
+
filename: string
|
|
417
|
+
): { code: string; maps: any[] } | null {
|
|
418
|
+
if (!LOOP_RE.test(code) || !LEXICAL_RE.test(code)) return null
|
|
419
|
+
|
|
420
|
+
let current = code
|
|
421
|
+
const maps: any[] = []
|
|
422
|
+
// each pass rewrites the outermost qualifying loops; nested ones surface on
|
|
423
|
+
// the next pass once the enclosing body has been lifted. every pass emits its
|
|
424
|
+
// own map so the caller can chain them and keep symbolication accurate.
|
|
425
|
+
for (let i = 0; i < 10; i++) {
|
|
426
|
+
const next = transformOnce(current, filename)
|
|
427
|
+
if (!next) break
|
|
428
|
+
current = next.code
|
|
429
|
+
maps.push(next.map)
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
return maps.length ? { code: current, maps } : null
|
|
433
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import { transformOxc, transformSWC, transformSWCStripJSX } from './transformSWC'
|
|
3
|
+
|
|
4
|
+
describe('transformSWC / transformOxc with oxc-transform for Hermes lowering', () => {
|
|
5
|
+
it('lowers class fields, private fields, and static blocks with Hermes target and assumptions', async () => {
|
|
6
|
+
const input = `
|
|
7
|
+
class Sample {
|
|
8
|
+
#secret = 42
|
|
9
|
+
publicField = 100
|
|
10
|
+
static {
|
|
11
|
+
Sample.initialized = true
|
|
12
|
+
}
|
|
13
|
+
getSecret() {
|
|
14
|
+
return this.#secret
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
`
|
|
18
|
+
const res = await transformSWC('Sample.ts', input, {
|
|
19
|
+
environment: 'ios',
|
|
20
|
+
mode: 'build',
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
expect(res).toBeDefined()
|
|
24
|
+
expect(res!.code).not.toContain('#secret')
|
|
25
|
+
expect(res!.code).not.toMatch(/^\s*publicField\s*=/m)
|
|
26
|
+
expect(res!.code).not.toContain('static {')
|
|
27
|
+
expect(res!.code).toContain('this.publicField = 100')
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
it('lowers JSX to react automatic runtime calls', async () => {
|
|
31
|
+
const input = `
|
|
32
|
+
export function MyComponent() {
|
|
33
|
+
return <div className="test"><span>Hello</span></div>
|
|
34
|
+
}
|
|
35
|
+
`
|
|
36
|
+
const res = await transformSWC('MyComponent.tsx', input, {
|
|
37
|
+
environment: 'client',
|
|
38
|
+
mode: 'build',
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
expect(res).toBeDefined()
|
|
42
|
+
expect(res!.code).not.toContain('<div')
|
|
43
|
+
expect(res!.code).not.toContain('<span>')
|
|
44
|
+
expect(res!.code).toContain('_jsx')
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
it('respects sourcemaps option', async () => {
|
|
48
|
+
const input = `
|
|
49
|
+
class Sample {
|
|
50
|
+
val = 1
|
|
51
|
+
}
|
|
52
|
+
`
|
|
53
|
+
const resWithoutMap = await transformSWC(
|
|
54
|
+
'Sample.ts',
|
|
55
|
+
input,
|
|
56
|
+
{
|
|
57
|
+
environment: 'ios',
|
|
58
|
+
mode: 'build',
|
|
59
|
+
},
|
|
60
|
+
{ sourceMaps: false }
|
|
61
|
+
)
|
|
62
|
+
expect(resWithoutMap?.map).toBeUndefined()
|
|
63
|
+
|
|
64
|
+
const resWithMap = await transformSWC(
|
|
65
|
+
'Sample.ts',
|
|
66
|
+
input,
|
|
67
|
+
{
|
|
68
|
+
environment: 'ios',
|
|
69
|
+
mode: 'build',
|
|
70
|
+
},
|
|
71
|
+
{ sourceMaps: true }
|
|
72
|
+
)
|
|
73
|
+
expect(resWithMap?.map).toBeDefined()
|
|
74
|
+
expect(resWithMap?.map.mappings).toBeDefined()
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
it('provides transformOxc alias', () => {
|
|
78
|
+
expect(transformOxc).toBe(transformSWC)
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
it('strips JSX using transformSWCStripJSX', async () => {
|
|
82
|
+
const input = `export const el = <div>Hello</div>`
|
|
83
|
+
const res = await transformSWCStripJSX('el.tsx', input)
|
|
84
|
+
expect(res).toBeDefined()
|
|
85
|
+
expect(res!.code).not.toContain('<div>')
|
|
86
|
+
expect(res!.code).toContain('_jsx')
|
|
87
|
+
})
|
|
88
|
+
})
|