@vxrn/compiler 1.25.13 → 1.25.16-1788753284749

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (124) hide show
  1. package/dist/cjs/cache.cjs +3 -1
  2. package/dist/cjs/configure.cjs +9 -1
  3. package/dist/cjs/index.cjs +82 -4
  4. package/dist/cjs/reactNativeCodegen.cjs +140 -0
  5. package/dist/cjs/reactNativeViewConfig.cjs +489 -0
  6. package/dist/cjs/transformBabel.cjs +15 -20
  7. package/dist/cjs/transformBabel.test.cjs +260 -0
  8. package/dist/cjs/transformHermesLoops.cjs +317 -0
  9. package/dist/cjs/transformHermesLoops.test.cjs +220 -0
  10. package/dist/cjs/transformSWC.cjs +102 -137
  11. package/dist/cjs/transformSWC.test.cjs +74 -0
  12. package/dist/cjs/transformWorklets.cjs +121 -0
  13. package/dist/cjs/transformWorklets.test.cjs +604 -0
  14. package/dist/cjs/worklets/autoworklet.cjs +265 -0
  15. package/dist/cjs/worklets/globals.cjs +71 -0
  16. package/dist/cjs/worklets/hash.cjs +38 -0
  17. package/dist/cjs/worklets/scope.cjs +370 -0
  18. package/dist/cjs/worklets/serialize.cjs +101 -0
  19. package/dist/cjs/worklets/transform.cjs +232 -0
  20. package/dist/cjs/worklets/types.cjs +18 -0
  21. package/dist/esm/cache.mjs +4 -2
  22. package/dist/esm/cache.mjs.map +1 -1
  23. package/dist/esm/configure.mjs +8 -1
  24. package/dist/esm/configure.mjs.map +1 -1
  25. package/dist/esm/index.js +72 -5
  26. package/dist/esm/index.js.map +1 -1
  27. package/dist/esm/index.mjs +72 -5
  28. package/dist/esm/index.mjs.map +1 -1
  29. package/dist/esm/reactNativeCodegen.mjs +104 -0
  30. package/dist/esm/reactNativeCodegen.mjs.map +1 -0
  31. package/dist/esm/reactNativeViewConfig.mjs +460 -0
  32. package/dist/esm/reactNativeViewConfig.mjs.map +1 -0
  33. package/dist/esm/transformBabel.mjs +14 -19
  34. package/dist/esm/transformBabel.mjs.map +1 -1
  35. package/dist/esm/transformBabel.test.mjs +261 -1
  36. package/dist/esm/transformBabel.test.mjs.map +1 -1
  37. package/dist/esm/transformHermesLoops.mjs +281 -0
  38. package/dist/esm/transformHermesLoops.mjs.map +1 -0
  39. package/dist/esm/transformHermesLoops.test.mjs +197 -0
  40. package/dist/esm/transformHermesLoops.test.mjs.map +1 -0
  41. package/dist/esm/transformSWC.mjs +89 -138
  42. package/dist/esm/transformSWC.mjs.map +1 -1
  43. package/dist/esm/transformSWC.test.mjs +75 -0
  44. package/dist/esm/transformSWC.test.mjs.map +1 -0
  45. package/dist/esm/transformWorklets.mjs +81 -0
  46. package/dist/esm/transformWorklets.mjs.map +1 -0
  47. package/dist/esm/transformWorklets.test.mjs +581 -0
  48. package/dist/esm/transformWorklets.test.mjs.map +1 -0
  49. package/dist/esm/worklets/autoworklet.mjs +234 -0
  50. package/dist/esm/worklets/autoworklet.mjs.map +1 -0
  51. package/dist/esm/worklets/globals.mjs +45 -0
  52. package/dist/esm/worklets/globals.mjs.map +1 -0
  53. package/dist/esm/worklets/hash.mjs +13 -0
  54. package/dist/esm/worklets/hash.mjs.map +1 -0
  55. package/dist/esm/worklets/scope.mjs +345 -0
  56. package/dist/esm/worklets/scope.mjs.map +1 -0
  57. package/dist/esm/worklets/serialize.mjs +75 -0
  58. package/dist/esm/worklets/serialize.mjs.map +1 -0
  59. package/dist/esm/worklets/transform.mjs +196 -0
  60. package/dist/esm/worklets/transform.mjs.map +1 -0
  61. package/dist/esm/worklets/types.mjs +2 -0
  62. package/dist/esm/worklets/types.mjs.map +1 -0
  63. package/package.json +10 -6
  64. package/src/cache.ts +9 -2
  65. package/src/configure.ts +12 -0
  66. package/src/index.ts +93 -13
  67. package/src/reactNativeCodegen.ts +181 -0
  68. package/src/reactNativeViewConfig.ts +593 -0
  69. package/src/transformBabel.test.ts +268 -1
  70. package/src/transformBabel.ts +21 -31
  71. package/src/transformHermesLoops.test.ts +282 -0
  72. package/src/transformHermesLoops.ts +433 -0
  73. package/src/transformSWC.test.ts +88 -0
  74. package/src/transformSWC.ts +109 -160
  75. package/src/transformWorklets.test.ts +708 -0
  76. package/src/transformWorklets.ts +160 -0
  77. package/src/types.ts +4 -4
  78. package/src/worklets/autoworklet.ts +391 -0
  79. package/src/worklets/globals.ts +125 -0
  80. package/src/worklets/hash.ts +14 -0
  81. package/src/worklets/scope.ts +401 -0
  82. package/src/worklets/serialize.ts +114 -0
  83. package/src/worklets/transform.ts +282 -0
  84. package/src/worklets/types.ts +26 -0
  85. package/types/cache.d.ts +1 -0
  86. package/types/cache.d.ts.map +1 -1
  87. package/types/configure.d.ts +2 -0
  88. package/types/configure.d.ts.map +1 -1
  89. package/types/index.d.ts +3 -0
  90. package/types/index.d.ts.map +1 -1
  91. package/types/reactNativeCodegen.d.ts +5 -0
  92. package/types/reactNativeCodegen.d.ts.map +1 -0
  93. package/types/reactNativeViewConfig.d.ts +35 -0
  94. package/types/reactNativeViewConfig.d.ts.map +1 -0
  95. package/types/transformBabel.d.ts +3 -3
  96. package/types/transformBabel.d.ts.map +1 -1
  97. package/types/transformHermesLoops.d.ts +5 -0
  98. package/types/transformHermesLoops.d.ts.map +1 -0
  99. package/types/transformHermesLoops.test.d.ts +2 -0
  100. package/types/transformHermesLoops.test.d.ts.map +1 -0
  101. package/types/transformSWC.d.ts +12 -3
  102. package/types/transformSWC.d.ts.map +1 -1
  103. package/types/transformSWC.test.d.ts +2 -0
  104. package/types/transformSWC.test.d.ts.map +1 -0
  105. package/types/transformWorklets.d.ts +22 -0
  106. package/types/transformWorklets.d.ts.map +1 -0
  107. package/types/transformWorklets.test.d.ts +2 -0
  108. package/types/transformWorklets.test.d.ts.map +1 -0
  109. package/types/types.d.ts +4 -4
  110. package/types/types.d.ts.map +1 -1
  111. package/types/worklets/autoworklet.d.ts +14 -0
  112. package/types/worklets/autoworklet.d.ts.map +1 -0
  113. package/types/worklets/globals.d.ts +3 -0
  114. package/types/worklets/globals.d.ts.map +1 -0
  115. package/types/worklets/hash.d.ts +5 -0
  116. package/types/worklets/hash.d.ts.map +1 -0
  117. package/types/worklets/scope.d.ts +7 -0
  118. package/types/worklets/scope.d.ts.map +1 -0
  119. package/types/worklets/serialize.d.ts +12 -0
  120. package/types/worklets/serialize.d.ts.map +1 -0
  121. package/types/worklets/transform.d.ts +6 -0
  122. package/types/worklets/transform.d.ts.map +1 -0
  123. package/types/worklets/types.d.ts +21 -0
  124. package/types/worklets/types.d.ts.map +1 -0
@@ -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
+ })