what-compiler 0.8.3 → 0.10.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/babel-plugin.js +314 -48
- package/dist/babel-plugin.js.map +2 -2
- package/dist/babel-plugin.min.js +1 -1
- package/dist/babel-plugin.min.js.map +3 -3
- package/dist/file-router.js +78 -2
- package/dist/file-router.js.map +2 -2
- package/dist/file-router.min.js +3 -2
- package/dist/file-router.min.js.map +3 -3
- package/dist/index.js +354 -52
- package/dist/index.js.map +2 -2
- package/dist/index.min.js +6 -6
- package/dist/index.min.js.map +3 -3
- package/dist/vite-plugin.js +354 -52
- package/dist/vite-plugin.js.map +2 -2
- package/dist/vite-plugin.min.js +6 -6
- package/dist/vite-plugin.min.js.map +3 -3
- package/package.json +2 -2
- package/src/babel-plugin.js +514 -65
- package/src/file-router.js +104 -2
- package/src/vite-plugin.js +28 -2
package/src/babel-plugin.js
CHANGED
|
@@ -25,7 +25,7 @@ const VOID_HTML_ELEMENTS = new Set([
|
|
|
25
25
|
]);
|
|
26
26
|
|
|
27
27
|
// Events that use document-level delegation for performance.
|
|
28
|
-
// The compiler emits `el
|
|
28
|
+
// The compiler emits `el.$$click = handler` instead of addEventListener.
|
|
29
29
|
// A one-time document listener walks event.target upward to find the handler.
|
|
30
30
|
const DELEGATED_EVENTS = new Set([
|
|
31
31
|
'click', 'input', 'change', 'keydown', 'keyup', 'submit',
|
|
@@ -47,13 +47,90 @@ const SIGNAL_CREATORS = new Set([
|
|
|
47
47
|
'createResource', 'useSWR', 'useQuery', 'useInfiniteQuery',
|
|
48
48
|
]);
|
|
49
49
|
|
|
50
|
+
// Normalize JSX text per React/Babel rules:
|
|
51
|
+
// - Split on newlines, treat tabs as spaces.
|
|
52
|
+
// - For interior lines: trim leading and trailing horizontal whitespace.
|
|
53
|
+
// - For the first line: only trim trailing whitespace.
|
|
54
|
+
// - For the last line: only trim leading whitespace.
|
|
55
|
+
// - Skip lines that are entirely whitespace (don't add a separator space).
|
|
56
|
+
// - Join the remaining non-empty lines with single spaces.
|
|
57
|
+
//
|
|
58
|
+
// This preserves leading/trailing single-line whitespace that sits next to
|
|
59
|
+
// an expression like `{count} items` — without this, the space is eaten and
|
|
60
|
+
// the rendered output reads `5items`.
|
|
61
|
+
function normalizeJsxText(value) {
|
|
62
|
+
// Single-line text (no newlines): preserve the original (just tabs->spaces).
|
|
63
|
+
// This keeps the space in cases like `{a} {b}` where the JSXText is " ".
|
|
64
|
+
if (!/[\r\n]/.test(value)) {
|
|
65
|
+
return value.replace(/\t/g, ' ');
|
|
66
|
+
}
|
|
67
|
+
const lines = value.split(/\r\n|\n|\r/);
|
|
68
|
+
let lastNonEmpty = -1;
|
|
69
|
+
for (let i = 0; i < lines.length; i++) {
|
|
70
|
+
if (/[^ \t]/.test(lines[i])) lastNonEmpty = i;
|
|
71
|
+
}
|
|
72
|
+
if (lastNonEmpty === -1) return '';
|
|
73
|
+
let out = '';
|
|
74
|
+
for (let i = 0; i < lines.length; i++) {
|
|
75
|
+
let line = lines[i].replace(/\t/g, ' ');
|
|
76
|
+
const isFirst = i === 0;
|
|
77
|
+
const isLast = i === lines.length - 1;
|
|
78
|
+
if (!isFirst) line = line.replace(/^ +/, '');
|
|
79
|
+
if (!isLast) line = line.replace(/ +$/, '');
|
|
80
|
+
if (!line) continue;
|
|
81
|
+
if (i !== lastNonEmpty) line += ' ';
|
|
82
|
+
out += line;
|
|
83
|
+
}
|
|
84
|
+
return out;
|
|
85
|
+
}
|
|
86
|
+
|
|
50
87
|
export default function whatBabelPlugin({ types: t }) {
|
|
51
88
|
// =====================================================
|
|
52
89
|
// Shared utilities
|
|
53
90
|
// =====================================================
|
|
54
91
|
|
|
92
|
+
// Warn-once tracking for unknown event modifier segments. Keyed by
|
|
93
|
+
// `${filename}::${segment}` so each typo is reported at most once per file
|
|
94
|
+
// per compile process. Without the filename in the key, the same typo in
|
|
95
|
+
// two different files would silently warn for the first file only —
|
|
96
|
+
// problematic in long-running Vite dev servers.
|
|
97
|
+
const _unknownModifierWarned = new Set();
|
|
98
|
+
const _forInfoWarned = new Set();
|
|
99
|
+
|
|
100
|
+
function hasEventModifiers(name, state) {
|
|
101
|
+
// Any `__` in an `on*` attribute is intended as modifier syntax — even
|
|
102
|
+
// if every segment is unknown. Returning false there would emit the
|
|
103
|
+
// attribute as a plain delegated-event property (e.g.
|
|
104
|
+
// `el.$$onclick__totalyWrong = handler`), which never fires. Instead,
|
|
105
|
+
// always route through the modifier-handling branch so the parser can
|
|
106
|
+
// warn about the typo and drop the unknown segments.
|
|
107
|
+
if (!name.includes('__')) return false;
|
|
108
|
+
if (!name.startsWith('on')) return false;
|
|
109
|
+
const parts = name.split('__');
|
|
110
|
+
const tail = parts.slice(1).filter(s => s !== '');
|
|
111
|
+
if (tail.length === 0) return false;
|
|
112
|
+
if (process.env.NODE_ENV !== 'production') {
|
|
113
|
+
const unknown = tail.filter(m => !EVENT_MODIFIERS.has(m));
|
|
114
|
+
const filename = (state && (state.filename || (state.file && state.file.opts && state.file.opts.filename))) || '<unknown>';
|
|
115
|
+
for (const m of unknown) {
|
|
116
|
+
const key = `${filename}::${m}`;
|
|
117
|
+
if (!_unknownModifierWarned.has(key)) {
|
|
118
|
+
_unknownModifierWarned.add(key);
|
|
119
|
+
console.warn(
|
|
120
|
+
`[what-compiler] Unknown event modifier "__${m}" in attribute "${name}" (${filename}). ` +
|
|
121
|
+
`Known modifiers: ${[...EVENT_MODIFIERS].join(', ')}. ` +
|
|
122
|
+
`Unknown segments are ignored.`
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
return true;
|
|
128
|
+
}
|
|
129
|
+
|
|
55
130
|
function parseEventModifiers(name) {
|
|
56
|
-
|
|
131
|
+
// Support both '|' (template strings) and '__' (JSX-safe) as modifier delimiters
|
|
132
|
+
const delimiter = name.includes('|') ? '|' : '__';
|
|
133
|
+
const parts = name.split(delimiter);
|
|
57
134
|
const eventName = parts[0];
|
|
58
135
|
const modifiers = parts.slice(1).filter(m => EVENT_MODIFIERS.has(m));
|
|
59
136
|
return { eventName, modifiers };
|
|
@@ -211,27 +288,27 @@ export default function whatBabelPlugin({ types: t }) {
|
|
|
211
288
|
}
|
|
212
289
|
}
|
|
213
290
|
|
|
214
|
-
// Walk up the scope chain using Babel's scope API
|
|
291
|
+
// Walk up the scope chain using Babel's scope API.
|
|
215
292
|
let scope = path.scope;
|
|
216
293
|
while (scope) {
|
|
217
|
-
// Check all bindings in this scope
|
|
218
|
-
for (const
|
|
294
|
+
// Check all variable bindings in this scope.
|
|
295
|
+
for (const binding of Object.values(scope.bindings)) {
|
|
219
296
|
if (binding.path.isVariableDeclarator()) {
|
|
220
297
|
extractFromDeclarator(binding.path.node);
|
|
221
298
|
}
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
299
|
+
}
|
|
300
|
+
// Scan this scope's own function params (destructured props) ONCE per
|
|
301
|
+
// scope — not once per binding. The old per-binding rescan made this
|
|
302
|
+
// O(params × bindings) per scope per JSXElement. (AUDIT-2026-06-06 H2)
|
|
303
|
+
const fnNode = scope.path && scope.path.node;
|
|
304
|
+
if (fnNode && fnNode.params) {
|
|
305
|
+
for (const param of fnNode.params) {
|
|
306
|
+
if (t.isObjectPattern(param)) {
|
|
307
|
+
for (const prop of param.properties) {
|
|
308
|
+
if (t.isObjectProperty(prop) && t.isIdentifier(prop.value)) {
|
|
309
|
+
signalNames.add(prop.value.name);
|
|
310
|
+
} else if (t.isRestElement(prop) && t.isIdentifier(prop.argument)) {
|
|
311
|
+
signalNames.add(prop.argument.name);
|
|
235
312
|
}
|
|
236
313
|
}
|
|
237
314
|
}
|
|
@@ -339,7 +416,8 @@ export default function whatBabelPlugin({ types: t }) {
|
|
|
339
416
|
}
|
|
340
417
|
|
|
341
418
|
if (t.isIdentifier(expr)) {
|
|
342
|
-
return isSignalIdentifier(expr.name, signalNames)
|
|
419
|
+
return isSignalIdentifier(expr.name, signalNames) ||
|
|
420
|
+
(importedIds && importedIds.has(expr.name));
|
|
343
421
|
}
|
|
344
422
|
|
|
345
423
|
if (t.isMemberExpression(expr)) {
|
|
@@ -383,6 +461,137 @@ export default function whatBabelPlugin({ types: t }) {
|
|
|
383
461
|
return false;
|
|
384
462
|
}
|
|
385
463
|
|
|
464
|
+
// --- Auto-lower .map() to mapArray ---
|
|
465
|
+
// Detects: source().map((item) => <Comp key={expr} .../>)
|
|
466
|
+
// or wrapped in an arrow: () => source().map(...)
|
|
467
|
+
// Also walks into ternary (cond ? a.map(...) : fallback) and
|
|
468
|
+
// logical (cond && a.map(...)) expressions so React-style
|
|
469
|
+
// conditional list patterns get keyed reconciliation.
|
|
470
|
+
// Produces: _$mapArray(source, (item) => <Comp .../>, { key: item => expr })
|
|
471
|
+
function tryLowerMapToMapArray(expr, state) {
|
|
472
|
+
// Unwrap arrow function: () => source().map(...)
|
|
473
|
+
let mapCall = expr;
|
|
474
|
+
let wrappedInArrow = false;
|
|
475
|
+
if (t.isArrowFunctionExpression(expr) && expr.params.length === 0) {
|
|
476
|
+
mapCall = expr.body;
|
|
477
|
+
wrappedInArrow = true;
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
// Walk into ternary: cond ? arr().map(...) : fallback
|
|
481
|
+
if (t.isConditionalExpression(mapCall)) {
|
|
482
|
+
const loweredCon = tryLowerMapCall(mapCall.consequent, state);
|
|
483
|
+
const loweredAlt = tryLowerMapCall(mapCall.alternate, state);
|
|
484
|
+
if (loweredCon || loweredAlt) {
|
|
485
|
+
const result = t.conditionalExpression(
|
|
486
|
+
mapCall.test,
|
|
487
|
+
loweredCon || mapCall.consequent,
|
|
488
|
+
loweredAlt || mapCall.alternate
|
|
489
|
+
);
|
|
490
|
+
return wrappedInArrow ? t.arrowFunctionExpression([], result) : result;
|
|
491
|
+
}
|
|
492
|
+
return null;
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
// Walk into logical: cond && arr().map(...)
|
|
496
|
+
if (t.isLogicalExpression(mapCall) && (mapCall.operator === '&&' || mapCall.operator === '||')) {
|
|
497
|
+
const loweredRight = tryLowerMapCall(mapCall.right, state);
|
|
498
|
+
if (loweredRight) {
|
|
499
|
+
const result = t.logicalExpression(mapCall.operator, mapCall.left, loweredRight);
|
|
500
|
+
return wrappedInArrow ? t.arrowFunctionExpression([], result) : result;
|
|
501
|
+
}
|
|
502
|
+
return null;
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
// Direct .map() call
|
|
506
|
+
const lowered = tryLowerMapCall(mapCall, state);
|
|
507
|
+
return lowered;
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
// Core .map() lowering — extracted so it can be called per-branch
|
|
511
|
+
function tryLowerMapCall(mapCall, state) {
|
|
512
|
+
// Check: something.map(fn)
|
|
513
|
+
if (!t.isCallExpression(mapCall)) return null;
|
|
514
|
+
if (!t.isMemberExpression(mapCall.callee)) return null;
|
|
515
|
+
if (!t.isIdentifier(mapCall.callee.property, { name: 'map' })) return null;
|
|
516
|
+
if (mapCall.arguments.length < 1) return null;
|
|
517
|
+
|
|
518
|
+
const mapFn = mapCall.arguments[0];
|
|
519
|
+
if (!t.isArrowFunctionExpression(mapFn) && !t.isFunctionExpression(mapFn)) return null;
|
|
520
|
+
|
|
521
|
+
// Get the map callback's return expression
|
|
522
|
+
let returnExpr = null;
|
|
523
|
+
if (t.isArrowFunctionExpression(mapFn)) {
|
|
524
|
+
if (t.isExpression(mapFn.body)) {
|
|
525
|
+
returnExpr = mapFn.body;
|
|
526
|
+
} else if (t.isBlockStatement(mapFn.body)) {
|
|
527
|
+
const ret = mapFn.body.body.find(s => t.isReturnStatement(s));
|
|
528
|
+
if (ret) returnExpr = ret.argument;
|
|
529
|
+
}
|
|
530
|
+
} else if (t.isFunctionExpression(mapFn)) {
|
|
531
|
+
const ret = mapFn.body.body.find(s => t.isReturnStatement(s));
|
|
532
|
+
if (ret) returnExpr = ret.argument;
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
if (!returnExpr) return null;
|
|
536
|
+
|
|
537
|
+
// Check if the return is JSX with a `key` prop
|
|
538
|
+
if (!t.isJSXElement(returnExpr)) return null;
|
|
539
|
+
const attrs = returnExpr.openingElement.attributes;
|
|
540
|
+
let keyAttr = null;
|
|
541
|
+
for (const attr of attrs) {
|
|
542
|
+
if (t.isJSXAttribute(attr) && getAttrName(attr) === 'key') {
|
|
543
|
+
keyAttr = attr;
|
|
544
|
+
break;
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
if (!keyAttr) {
|
|
548
|
+
// JSX returned without a key — bail out, but warn at compile time so
|
|
549
|
+
// users notice they're missing keyed reconciliation. Only warn in dev
|
|
550
|
+
// (production builds are noiseless).
|
|
551
|
+
if (process.env.NODE_ENV !== 'production') {
|
|
552
|
+
const loc = returnExpr.loc;
|
|
553
|
+
const fileName = state.filename || state.file?.opts?.filename || '<unknown>';
|
|
554
|
+
const lineInfo = loc ? `:${loc.start.line}:${loc.start.column}` : '';
|
|
555
|
+
console.warn(
|
|
556
|
+
`[what-compiler] .map() returning JSX without a \`key\` prop at ${fileName}${lineInfo}. ` +
|
|
557
|
+
`Without a key, the list cannot use keyed reconciliation — items are re-created on every update. ` +
|
|
558
|
+
`Add key={...} to enable efficient updates.`
|
|
559
|
+
);
|
|
560
|
+
}
|
|
561
|
+
return null;
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
// Extract the key expression
|
|
565
|
+
const keyValue = getAttributeValue(keyAttr.value);
|
|
566
|
+
if (!keyValue) return null;
|
|
567
|
+
|
|
568
|
+
// Remove the key prop from the JSX element (mapArray handles keying, not the DOM)
|
|
569
|
+
returnExpr.openingElement.attributes = attrs.filter(a => a !== keyAttr);
|
|
570
|
+
|
|
571
|
+
// Build the source: the object before .map() — wrap in an arrow for reactive access
|
|
572
|
+
const sourceObj = mapCall.callee.object;
|
|
573
|
+
const source = t.arrowFunctionExpression([], sourceObj);
|
|
574
|
+
|
|
575
|
+
// Build the key function: (item) => keyExpr.
|
|
576
|
+
// Clone both the parameter and the key expression — the parameter is shared
|
|
577
|
+
// with the user's map callback AST and keyValue may be referenced elsewhere
|
|
578
|
+
// in the tree. Cloning insulates this new arrow from later mutations.
|
|
579
|
+
const itemParam = mapFn.params[0] ? t.cloneNode(mapFn.params[0], true) : t.identifier('_item');
|
|
580
|
+
const keyFn = t.arrowFunctionExpression([itemParam], t.cloneNode(keyValue, true));
|
|
581
|
+
|
|
582
|
+
// Build: _$mapArray(source, mapFn, { key: keyFn, raw: true })
|
|
583
|
+
// raw: true means mapFn receives the raw item value (not a signal accessor),
|
|
584
|
+
// matching user-authored .map() semantics where `item.prop` accesses values directly.
|
|
585
|
+
return t.callExpression(t.identifier('_$mapArray'), [
|
|
586
|
+
source,
|
|
587
|
+
mapFn,
|
|
588
|
+
t.objectExpression([
|
|
589
|
+
t.objectProperty(t.identifier('key'), keyFn),
|
|
590
|
+
t.objectProperty(t.identifier('raw'), t.booleanLiteral(true))
|
|
591
|
+
])
|
|
592
|
+
]);
|
|
593
|
+
}
|
|
594
|
+
|
|
386
595
|
// =====================================================
|
|
387
596
|
// Fine-Grained Mode (template + insert + effect)
|
|
388
597
|
// =====================================================
|
|
@@ -415,7 +624,7 @@ export default function whatBabelPlugin({ types: t }) {
|
|
|
415
624
|
// Extract static HTML from JSX element for template()
|
|
416
625
|
function extractStaticHTML(node) {
|
|
417
626
|
if (t.isJSXText(node)) {
|
|
418
|
-
const text = node.value
|
|
627
|
+
const text = normalizeJsxText(node.value);
|
|
419
628
|
return text ? escapeHTML(text) : '';
|
|
420
629
|
}
|
|
421
630
|
|
|
@@ -467,7 +676,7 @@ export default function whatBabelPlugin({ types: t }) {
|
|
|
467
676
|
|
|
468
677
|
for (const child of node.children) {
|
|
469
678
|
if (t.isJSXText(child)) {
|
|
470
|
-
const text = child.value
|
|
679
|
+
const text = normalizeJsxText(child.value);
|
|
471
680
|
if (text) html += escapeHTML(text);
|
|
472
681
|
} else if (t.isJSXExpressionContainer(child)) {
|
|
473
682
|
if (!t.isJSXEmptyExpression(child.expression)) {
|
|
@@ -500,11 +709,7 @@ export default function whatBabelPlugin({ types: t }) {
|
|
|
500
709
|
const openingElement = node.openingElement;
|
|
501
710
|
const tagName = openingElement.name.name;
|
|
502
711
|
|
|
503
|
-
|
|
504
|
-
return transformComponentFineGrained(path, state);
|
|
505
|
-
}
|
|
506
|
-
|
|
507
|
-
// Control flow components (lowercase but special)
|
|
712
|
+
// Control flow components — check before generic isComponent since they start uppercase
|
|
508
713
|
if (tagName === 'For') {
|
|
509
714
|
return transformForFineGrained(path, state);
|
|
510
715
|
}
|
|
@@ -512,6 +717,10 @@ export default function whatBabelPlugin({ types: t }) {
|
|
|
512
717
|
return transformShowFineGrained(path, state);
|
|
513
718
|
}
|
|
514
719
|
|
|
720
|
+
if (isComponent(tagName)) {
|
|
721
|
+
return transformComponentFineGrained(path, state);
|
|
722
|
+
}
|
|
723
|
+
|
|
515
724
|
const attributes = openingElement.attributes;
|
|
516
725
|
const children = node.children;
|
|
517
726
|
|
|
@@ -603,7 +812,7 @@ export default function whatBabelPlugin({ types: t }) {
|
|
|
603
812
|
const transformedChildren = [];
|
|
604
813
|
for (const child of children) {
|
|
605
814
|
if (t.isJSXText(child)) {
|
|
606
|
-
const text = child.value
|
|
815
|
+
const text = normalizeJsxText(child.value);
|
|
607
816
|
if (text) transformedChildren.push(t.stringLiteral(text));
|
|
608
817
|
} else if (t.isJSXExpressionContainer(child)) {
|
|
609
818
|
if (!t.isJSXEmptyExpression(child.expression)) {
|
|
@@ -669,12 +878,12 @@ export default function whatBabelPlugin({ types: t }) {
|
|
|
669
878
|
}
|
|
670
879
|
|
|
671
880
|
// Event handlers
|
|
672
|
-
if (attrName.startsWith('on') && !attrName.includes('|')) {
|
|
881
|
+
if (attrName.startsWith('on') && !attrName.includes('|') && !hasEventModifiers(attrName, state)) {
|
|
673
882
|
const event = attrName.slice(2).toLowerCase();
|
|
674
883
|
const handler = getAttributeValue(attr.value);
|
|
675
884
|
|
|
676
885
|
if (DELEGATED_EVENTS.has(event)) {
|
|
677
|
-
// Use event delegation: el
|
|
886
|
+
// Use event delegation: el.$$click = handler (matches runtime lookup)
|
|
678
887
|
state.needsDelegation = true;
|
|
679
888
|
if (!state.delegatedEvents) state.delegatedEvents = new Set();
|
|
680
889
|
state.delegatedEvents.add(event);
|
|
@@ -683,7 +892,7 @@ export default function whatBabelPlugin({ types: t }) {
|
|
|
683
892
|
t.assignmentExpression('=',
|
|
684
893
|
t.memberExpression(
|
|
685
894
|
t.identifier(elId),
|
|
686
|
-
t.identifier(
|
|
895
|
+
t.identifier(`$$${event}`)
|
|
687
896
|
),
|
|
688
897
|
handler
|
|
689
898
|
)
|
|
@@ -703,8 +912,8 @@ export default function whatBabelPlugin({ types: t }) {
|
|
|
703
912
|
continue;
|
|
704
913
|
}
|
|
705
914
|
|
|
706
|
-
// Event with modifiers
|
|
707
|
-
if (attrName.startsWith('on') && attrName.includes('|')) {
|
|
915
|
+
// Event with modifiers (pipe '|' or JSX-safe double underscore '__')
|
|
916
|
+
if (attrName.startsWith('on') && (attrName.includes('|') || hasEventModifiers(attrName, state))) {
|
|
708
917
|
const { eventName, modifiers } = parseEventModifiers(attrName);
|
|
709
918
|
const handler = getAttributeValue(attr.value);
|
|
710
919
|
const wrappedHandler = createEventHandler(handler, modifiers);
|
|
@@ -810,8 +1019,14 @@ export default function whatBabelPlugin({ types: t }) {
|
|
|
810
1019
|
|
|
811
1020
|
if (isPotentiallyReactive(expr, state.signalNames, state.importedIdentifiers)) {
|
|
812
1021
|
state.needsEffect = true;
|
|
1022
|
+
// Auto-invoke bare signal/imported identifiers: value={name} -> name()
|
|
1023
|
+
const valueExpr = t.isIdentifier(expr) &&
|
|
1024
|
+
(isSignalIdentifier(expr.name, state.signalNames) ||
|
|
1025
|
+
(state.importedIdentifiers && state.importedIdentifiers.has(expr.name)))
|
|
1026
|
+
? t.callExpression(expr, [])
|
|
1027
|
+
: expr;
|
|
813
1028
|
const effectCall = t.callExpression(t.identifier('_$effect'), [
|
|
814
|
-
t.arrowFunctionExpression([], buildSetPropCall(domName,
|
|
1029
|
+
t.arrowFunctionExpression([], buildSetPropCall(domName, valueExpr))
|
|
815
1030
|
]);
|
|
816
1031
|
// In dev mode, add a leading comment when the effect wrapping is uncertain
|
|
817
1032
|
// (non-signal function call whose args happen to contain signal reads)
|
|
@@ -841,7 +1056,7 @@ export default function whatBabelPlugin({ types: t }) {
|
|
|
841
1056
|
|
|
842
1057
|
for (const child of children) {
|
|
843
1058
|
if (t.isJSXText(child)) {
|
|
844
|
-
const text = child.value
|
|
1059
|
+
const text = normalizeJsxText(child.value);
|
|
845
1060
|
if (text) childIndex++;
|
|
846
1061
|
continue;
|
|
847
1062
|
}
|
|
@@ -881,24 +1096,43 @@ export default function whatBabelPlugin({ types: t }) {
|
|
|
881
1096
|
e.type === 'expression' || e.type === 'component' ||
|
|
882
1097
|
(e.type === 'static' && e.hasAnythingDynamic)
|
|
883
1098
|
);
|
|
884
|
-
|
|
885
|
-
|
|
1099
|
+
// Pre-capture whenever 2+ children need a DOM ref. Beyond preventing index
|
|
1100
|
+
// shift after insert() mutations, the shared O(n) cursor walk below replaces
|
|
1101
|
+
// per-child `el.firstChild.nextSibling…`-from-root access, which was O(n²) in
|
|
1102
|
+
// both compile time and emitted size for elements with many dynamic
|
|
1103
|
+
// children. (AUDIT-2026-06-06 H2)
|
|
1104
|
+
const needsPreCapture = entriesNeedingRef.length >= 2;
|
|
886
1105
|
|
|
887
1106
|
const markerVars = new Map(); // childIndex → variable name
|
|
888
1107
|
if (needsPreCapture) {
|
|
1108
|
+
// Chain each marker from the PREVIOUS captured cursor instead of
|
|
1109
|
+
// re-walking `el.firstChild.nextSibling…` from the root for every child.
|
|
1110
|
+
// entriesNeedingRef is in ascending childIndex order, so the per-marker
|
|
1111
|
+
// deltas sum to O(n) total instead of O(n²). This was the dominant
|
|
1112
|
+
// quadratic in compile time and emitted-bundle size for large elements.
|
|
1113
|
+
// (AUDIT-2026-06-06 H2)
|
|
1114
|
+
let prevVar = null;
|
|
1115
|
+
let prevIndex = 0;
|
|
889
1116
|
for (const entry of entriesNeedingRef) {
|
|
890
|
-
const
|
|
891
|
-
// Use a unique name to avoid collisions with element vars
|
|
1117
|
+
const idx = entry.childIndex;
|
|
892
1118
|
const markerVar = state.nextVarId();
|
|
893
|
-
markerVars.set(
|
|
1119
|
+
markerVars.set(idx, markerVar);
|
|
1120
|
+
let init;
|
|
1121
|
+
if (prevVar === null) {
|
|
1122
|
+
init = buildChildAccess(elId, idx);
|
|
1123
|
+
} else {
|
|
1124
|
+
init = t.identifier(prevVar);
|
|
1125
|
+
for (let i = prevIndex; i < idx; i++) {
|
|
1126
|
+
init = t.memberExpression(init, t.identifier('nextSibling'));
|
|
1127
|
+
}
|
|
1128
|
+
}
|
|
894
1129
|
statements.push(
|
|
895
1130
|
t.variableDeclaration('const', [
|
|
896
|
-
t.variableDeclarator(
|
|
897
|
-
t.identifier(markerVar),
|
|
898
|
-
buildChildAccess(elId, entry.childIndex)
|
|
899
|
-
)
|
|
1131
|
+
t.variableDeclarator(t.identifier(markerVar), init)
|
|
900
1132
|
])
|
|
901
1133
|
);
|
|
1134
|
+
prevVar = markerVar;
|
|
1135
|
+
prevIndex = idx;
|
|
902
1136
|
}
|
|
903
1137
|
}
|
|
904
1138
|
|
|
@@ -913,10 +1147,58 @@ export default function whatBabelPlugin({ types: t }) {
|
|
|
913
1147
|
// --- Pass 2: Generate code using stable references ---
|
|
914
1148
|
for (const entry of entries) {
|
|
915
1149
|
if (entry.type === 'expression') {
|
|
916
|
-
|
|
1150
|
+
let expr = entry.child.expression;
|
|
917
1151
|
const marker = getMarker(entry.childIndex);
|
|
918
1152
|
state.needsInsert = true;
|
|
919
1153
|
|
|
1154
|
+
// Auto-lower .map() to mapArray when the callback returns keyed JSX.
|
|
1155
|
+
// Pattern: source().map(item => <Comp key={...} />) or source().map((item, i) => ...)
|
|
1156
|
+
const mapResult = tryLowerMapToMapArray(expr, state);
|
|
1157
|
+
if (mapResult) {
|
|
1158
|
+
state.needsMapArray = true;
|
|
1159
|
+
// A bare _$mapArray(...) call is a self-managing inserter (it tracks
|
|
1160
|
+
// its source internally) and an arrow is already reactive — pass both
|
|
1161
|
+
// raw. But when lowering produced a ternary/logical wrapping the call
|
|
1162
|
+
// (e.g. cond ? _$mapArray(...) : fallback), the surrounding condition
|
|
1163
|
+
// must stay reactive, so wrap the whole expression in () => and let
|
|
1164
|
+
// _$insert re-evaluate it on change. Without this the condition is read
|
|
1165
|
+
// exactly once and never re-tracks. (AUDIT-2026-06-06 H1)
|
|
1166
|
+
const isBareMapArray = t.isCallExpression(mapResult) && t.isIdentifier(mapResult.callee) &&
|
|
1167
|
+
(mapResult.callee.name === '_$mapArray' || mapResult.callee.name === 'mapArray');
|
|
1168
|
+
const isArrowAlready = t.isArrowFunctionExpression(mapResult);
|
|
1169
|
+
const insertArg = (isBareMapArray || isArrowAlready)
|
|
1170
|
+
? mapResult
|
|
1171
|
+
: t.arrowFunctionExpression([], mapResult);
|
|
1172
|
+
statements.push(
|
|
1173
|
+
t.expressionStatement(
|
|
1174
|
+
t.callExpression(t.identifier('_$insert'), [
|
|
1175
|
+
t.identifier(elId),
|
|
1176
|
+
insertArg,
|
|
1177
|
+
marker
|
|
1178
|
+
])
|
|
1179
|
+
)
|
|
1180
|
+
);
|
|
1181
|
+
continue;
|
|
1182
|
+
}
|
|
1183
|
+
|
|
1184
|
+
// mapArray() calls return self-managing inserters — pass directly, never wrap in () =>
|
|
1185
|
+
const isMapArrayCall = t.isCallExpression(expr) && t.isIdentifier(expr.callee) &&
|
|
1186
|
+
(expr.callee.name === 'mapArray' || expr.callee.name === '_$mapArray');
|
|
1187
|
+
if (isMapArrayCall) {
|
|
1188
|
+
state.needsMapArray = true;
|
|
1189
|
+
if (expr.callee.name === 'mapArray') expr.callee.name = '_$mapArray';
|
|
1190
|
+
statements.push(
|
|
1191
|
+
t.expressionStatement(
|
|
1192
|
+
t.callExpression(t.identifier('_$insert'), [
|
|
1193
|
+
t.identifier(elId),
|
|
1194
|
+
expr,
|
|
1195
|
+
marker
|
|
1196
|
+
])
|
|
1197
|
+
)
|
|
1198
|
+
);
|
|
1199
|
+
continue;
|
|
1200
|
+
}
|
|
1201
|
+
|
|
920
1202
|
if (isPotentiallyReactive(expr, state.signalNames, state.importedIdentifiers)) {
|
|
921
1203
|
const insertCall = t.callExpression(t.identifier('_$insert'), [
|
|
922
1204
|
t.identifier(elId),
|
|
@@ -1153,7 +1435,7 @@ export default function whatBabelPlugin({ types: t }) {
|
|
|
1153
1435
|
}
|
|
1154
1436
|
|
|
1155
1437
|
// Handle event modifiers on components
|
|
1156
|
-
if (attrName.startsWith('on') && attrName.includes('|')) {
|
|
1438
|
+
if (attrName.startsWith('on') && (attrName.includes('|') || hasEventModifiers(attrName, state))) {
|
|
1157
1439
|
const { eventName, modifiers } = parseEventModifiers(attrName);
|
|
1158
1440
|
const handler = getAttributeValue(attr.value);
|
|
1159
1441
|
const wrappedHandler = createEventHandler(handler, modifiers);
|
|
@@ -1177,7 +1459,7 @@ export default function whatBabelPlugin({ types: t }) {
|
|
|
1177
1459
|
const transformedChildren = [];
|
|
1178
1460
|
for (const child of children) {
|
|
1179
1461
|
if (t.isJSXText(child)) {
|
|
1180
|
-
const text = child.value
|
|
1462
|
+
const text = normalizeJsxText(child.value);
|
|
1181
1463
|
if (text) transformedChildren.push(t.stringLiteral(text));
|
|
1182
1464
|
} else if (t.isJSXExpressionContainer(child)) {
|
|
1183
1465
|
if (!t.isJSXEmptyExpression(child.expression)) {
|
|
@@ -1218,12 +1500,35 @@ export default function whatBabelPlugin({ types: t }) {
|
|
|
1218
1500
|
const attributes = node.openingElement.attributes;
|
|
1219
1501
|
const children = node.children;
|
|
1220
1502
|
|
|
1221
|
-
// <For each={data}>{(item) => <Row />}</For>
|
|
1222
|
-
// → mapArray(data, (item) =>
|
|
1503
|
+
// <For each={data} key={item => item.id}>{(item) => <Row />}</For>
|
|
1504
|
+
// → mapArray(data, (item) => ..., { key: item => item.id })
|
|
1505
|
+
//
|
|
1506
|
+
// NOTE: <For> is supported but .map() with a key prop is the preferred
|
|
1507
|
+
// pattern for list rendering. The compiler auto-lowers .map() to
|
|
1508
|
+
// _$mapArray with raw mode, which is simpler and matches JS idioms.
|
|
1509
|
+
// <For> is useful when you need signal-wrapped item accessors (keyed
|
|
1510
|
+
// mode without raw), so that item updates don't recreate DOM nodes.
|
|
1511
|
+
if (process.env.NODE_ENV !== 'production') {
|
|
1512
|
+
const fileName = state.filename || state.file?.opts?.filename || '<unknown>';
|
|
1513
|
+
if (!_forInfoWarned.has(fileName)) {
|
|
1514
|
+
_forInfoWarned.add(fileName);
|
|
1515
|
+
const loc = node.loc;
|
|
1516
|
+
const lineInfo = loc ? `:${loc.start.line}:${loc.start.column}` : '';
|
|
1517
|
+
console.info(
|
|
1518
|
+
`[what-compiler] <For> at ${fileName}${lineInfo}: consider using .map() with a key prop instead. ` +
|
|
1519
|
+
`The compiler auto-lowers .map() to efficient keyed reconciliation. ` +
|
|
1520
|
+
`<For> is only needed for signal-wrapped item accessors (advanced).`
|
|
1521
|
+
);
|
|
1522
|
+
}
|
|
1523
|
+
}
|
|
1524
|
+
|
|
1223
1525
|
let eachExpr = null;
|
|
1526
|
+
let keyExpr = null;
|
|
1224
1527
|
for (const attr of attributes) {
|
|
1225
|
-
if (t.isJSXAttribute(attr)
|
|
1226
|
-
|
|
1528
|
+
if (t.isJSXAttribute(attr)) {
|
|
1529
|
+
const name = getAttrName(attr);
|
|
1530
|
+
if (name === 'each') eachExpr = getAttributeValue(attr.value);
|
|
1531
|
+
else if (name === 'key') keyExpr = getAttributeValue(attr.value);
|
|
1227
1532
|
}
|
|
1228
1533
|
}
|
|
1229
1534
|
|
|
@@ -1248,14 +1553,120 @@ export default function whatBabelPlugin({ types: t }) {
|
|
|
1248
1553
|
}
|
|
1249
1554
|
|
|
1250
1555
|
state.needsMapArray = true;
|
|
1251
|
-
|
|
1556
|
+
const args = [eachExpr, renderFn];
|
|
1557
|
+
if (keyExpr) {
|
|
1558
|
+
args.push(t.objectExpression([
|
|
1559
|
+
t.objectProperty(t.identifier('key'), keyExpr)
|
|
1560
|
+
]));
|
|
1561
|
+
}
|
|
1562
|
+
return t.callExpression(t.identifier('_$mapArray'), args);
|
|
1252
1563
|
}
|
|
1253
1564
|
|
|
1254
1565
|
function transformShowFineGrained(path, state) {
|
|
1255
|
-
// <Show when={cond}>{content}</Show>
|
|
1256
|
-
//
|
|
1257
|
-
|
|
1258
|
-
|
|
1566
|
+
// <Show when={cond} fallback={alt}>{content}</Show>
|
|
1567
|
+
// → () => cond() ? content : (fallback || null)
|
|
1568
|
+
// This compiles to a reactive expression that insert() wraps in an effect.
|
|
1569
|
+
const { node } = path;
|
|
1570
|
+
const attributes = node.openingElement.attributes;
|
|
1571
|
+
const children = node.children;
|
|
1572
|
+
|
|
1573
|
+
let whenExpr = null;
|
|
1574
|
+
let fallbackExpr = null;
|
|
1575
|
+
for (const attr of attributes) {
|
|
1576
|
+
if (t.isJSXAttribute(attr)) {
|
|
1577
|
+
const name = getAttrName(attr);
|
|
1578
|
+
if (name === 'when') whenExpr = getAttributeValue(attr.value);
|
|
1579
|
+
else if (name === 'fallback') fallbackExpr = getAttributeValue(attr.value);
|
|
1580
|
+
}
|
|
1581
|
+
}
|
|
1582
|
+
|
|
1583
|
+
if (!whenExpr) {
|
|
1584
|
+
// <Show> without a when prop has no defined semantics — fail loudly at
|
|
1585
|
+
// build time so the user fixes their source instead of seeing runtime
|
|
1586
|
+
// confusion. buildCodeFrameError pins the error to the JSX location.
|
|
1587
|
+
throw path.buildCodeFrameError(
|
|
1588
|
+
'<Show> requires a "when" prop. Example: <Show when={isOpen} fallback={null}>...</Show>'
|
|
1589
|
+
);
|
|
1590
|
+
}
|
|
1591
|
+
|
|
1592
|
+
// Extract the content — either a render function child or static JSX children
|
|
1593
|
+
let contentExpr = null;
|
|
1594
|
+
for (const child of children) {
|
|
1595
|
+
if (t.isJSXExpressionContainer(child) && !t.isJSXEmptyExpression(child.expression)) {
|
|
1596
|
+
// Render function: {() => <div>...</div>} or {(value) => <div>{value}</div>}
|
|
1597
|
+
contentExpr = child.expression;
|
|
1598
|
+
break;
|
|
1599
|
+
}
|
|
1600
|
+
}
|
|
1601
|
+
|
|
1602
|
+
if (!contentExpr) {
|
|
1603
|
+
// Static children — collect and transform them
|
|
1604
|
+
const transformedChildren = [];
|
|
1605
|
+
for (const child of children) {
|
|
1606
|
+
if (t.isJSXText(child)) {
|
|
1607
|
+
const text = normalizeJsxText(child.value);
|
|
1608
|
+
if (text) transformedChildren.push(t.stringLiteral(text));
|
|
1609
|
+
} else if (t.isJSXElement(child)) {
|
|
1610
|
+
transformedChildren.push(transformElementFineGrained({ node: child }, state));
|
|
1611
|
+
}
|
|
1612
|
+
}
|
|
1613
|
+
if (transformedChildren.length === 1) {
|
|
1614
|
+
contentExpr = transformedChildren[0];
|
|
1615
|
+
} else if (transformedChildren.length > 1) {
|
|
1616
|
+
contentExpr = t.arrayExpression(transformedChildren);
|
|
1617
|
+
} else {
|
|
1618
|
+
contentExpr = t.nullLiteral();
|
|
1619
|
+
}
|
|
1620
|
+
}
|
|
1621
|
+
|
|
1622
|
+
// Build:
|
|
1623
|
+
// () => { const _v = <condition>; return _v ? <consequent> : <alternate>; }
|
|
1624
|
+
// Hoisting into a local prevents double-evaluation of the `when` signal
|
|
1625
|
+
// (the consequent's render callback also needs the resolved value).
|
|
1626
|
+
//
|
|
1627
|
+
// `whenExpr` shape determines how we form the condition:
|
|
1628
|
+
// - call expression → use as-is <Show when={cond()}>
|
|
1629
|
+
// - arrow w/ expression body → use the body <Show when={() => x > 5}>
|
|
1630
|
+
// - identifier that looks like a signal/import <Show when={isOpen}>
|
|
1631
|
+
// → invoke it as accessor: isOpen()
|
|
1632
|
+
// - anything else (member, literal, logical, etc.) <Show when={user.isAdmin}>
|
|
1633
|
+
// → use the raw expression. Do NOT invoke —
|
|
1634
|
+
// non-functions would throw at runtime.
|
|
1635
|
+
let condition;
|
|
1636
|
+
if (t.isCallExpression(whenExpr)) {
|
|
1637
|
+
condition = whenExpr;
|
|
1638
|
+
} else if (t.isArrowFunctionExpression(whenExpr) && t.isExpression(whenExpr.body)) {
|
|
1639
|
+
condition = whenExpr.body;
|
|
1640
|
+
} else if (
|
|
1641
|
+
t.isIdentifier(whenExpr) &&
|
|
1642
|
+
(
|
|
1643
|
+
(state.signalNames && isSignalIdentifier(whenExpr.name, state.signalNames)) ||
|
|
1644
|
+
(state.importedIdentifiers && state.importedIdentifiers.has(whenExpr.name))
|
|
1645
|
+
)
|
|
1646
|
+
) {
|
|
1647
|
+
condition = t.callExpression(whenExpr, []);
|
|
1648
|
+
} else {
|
|
1649
|
+
// Plain boolean expression — member access, literal, logical, etc.
|
|
1650
|
+
condition = whenExpr;
|
|
1651
|
+
}
|
|
1652
|
+
|
|
1653
|
+
const vId = path.scope
|
|
1654
|
+
? path.scope.generateUidIdentifier('v')
|
|
1655
|
+
: t.identifier('_v');
|
|
1656
|
+
|
|
1657
|
+
const consequent = t.isFunction(contentExpr)
|
|
1658
|
+
? t.callExpression(contentExpr, [t.cloneNode(vId)])
|
|
1659
|
+
: contentExpr;
|
|
1660
|
+
const alternate = fallbackExpr || t.nullLiteral();
|
|
1661
|
+
|
|
1662
|
+
return t.arrowFunctionExpression([], t.blockStatement([
|
|
1663
|
+
t.variableDeclaration('const', [
|
|
1664
|
+
t.variableDeclarator(vId, condition)
|
|
1665
|
+
]),
|
|
1666
|
+
t.returnStatement(
|
|
1667
|
+
t.conditionalExpression(t.cloneNode(vId), consequent, alternate)
|
|
1668
|
+
)
|
|
1669
|
+
]));
|
|
1259
1670
|
}
|
|
1260
1671
|
|
|
1261
1672
|
function transformFragmentFineGrained(path, state) {
|
|
@@ -1265,7 +1676,7 @@ export default function whatBabelPlugin({ types: t }) {
|
|
|
1265
1676
|
const transformed = [];
|
|
1266
1677
|
for (const child of children) {
|
|
1267
1678
|
if (t.isJSXText(child)) {
|
|
1268
|
-
const text = child.value
|
|
1679
|
+
const text = normalizeJsxText(child.value);
|
|
1269
1680
|
if (text) transformed.push(t.stringLiteral(text));
|
|
1270
1681
|
} else if (t.isJSXExpressionContainer(child)) {
|
|
1271
1682
|
if (!t.isJSXEmptyExpression(child.expression)) {
|
|
@@ -1520,27 +1931,65 @@ export default function whatBabelPlugin({ types: t }) {
|
|
|
1520
1931
|
},
|
|
1521
1932
|
|
|
1522
1933
|
JSXElement(path, state) {
|
|
1523
|
-
// FIX-1: Use scope-aware signal detection instead of file-global
|
|
1524
|
-
|
|
1934
|
+
// FIX-1: Use scope-aware signal detection instead of file-global.
|
|
1935
|
+
// Memoize per Babel scope: every JSXElement in the same scope yields the
|
|
1936
|
+
// same signal-name set, so without this the full scope-chain walk ran
|
|
1937
|
+
// once per element — O(n²) compile time for a large single component.
|
|
1938
|
+
// (AUDIT-2026-06-06 H2)
|
|
1939
|
+
const scope = path.scope;
|
|
1940
|
+
let cache = state._signalNamesCache;
|
|
1941
|
+
if (!cache) cache = state._signalNamesCache = new WeakMap();
|
|
1942
|
+
let names = cache.get(scope);
|
|
1943
|
+
if (!names) {
|
|
1944
|
+
names = collectSignalNamesFromScope(path);
|
|
1945
|
+
cache.set(scope, names);
|
|
1946
|
+
}
|
|
1947
|
+
state.signalNames = names;
|
|
1525
1948
|
state._pendingSetup = [];
|
|
1526
1949
|
const transformed = transformElementFineGrained(path, state);
|
|
1527
1950
|
const pending = state._pendingSetup;
|
|
1528
1951
|
state._pendingSetup = [];
|
|
1529
1952
|
|
|
1530
1953
|
if (pending.length > 0) {
|
|
1531
|
-
// Find the enclosing statement to hoist setup before it
|
|
1954
|
+
// Find the enclosing statement to hoist setup before it,
|
|
1955
|
+
// but only if it's in the SAME function scope. Crossing into
|
|
1956
|
+
// an inner arrow/function (e.g., .map(item => <JSX/>)) would
|
|
1957
|
+
// hoist references to closure variables out of scope.
|
|
1532
1958
|
let stmtPath = path;
|
|
1959
|
+
let crossedFunctionBoundary = false;
|
|
1533
1960
|
while (stmtPath && !stmtPath.isStatement()) {
|
|
1961
|
+
if (stmtPath.isArrowFunctionExpression() || stmtPath.isFunctionExpression()) {
|
|
1962
|
+
crossedFunctionBoundary = true;
|
|
1963
|
+
}
|
|
1534
1964
|
stmtPath = stmtPath.parentPath;
|
|
1535
1965
|
}
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1966
|
+
// We can safely hoist setup as siblings of `stmtPath` ONLY if
|
|
1967
|
+
// `stmtPath` lives inside a statement list (BlockStatement.body or
|
|
1968
|
+
// Program.body). For single-statement positions like
|
|
1969
|
+
// `if (cond) return <jsx/>;` or `while (x) return <jsx/>;`,
|
|
1970
|
+
// Babel's `insertBefore` wraps the parent into a block lazily and
|
|
1971
|
+
// multi-statement inserts end up split across scopes, leaving the
|
|
1972
|
+
// `_$insert(_el$N, ...)` call outside the block that declares
|
|
1973
|
+
// `const _el$N`. This is a TDZ/ReferenceError at runtime.
|
|
1974
|
+
//
|
|
1975
|
+
// To guarantee that ALL setup statements and the returned reference
|
|
1976
|
+
// share one lexical block, require that `stmtPath.listKey` points
|
|
1977
|
+
// at a statement list. Otherwise fall through to the IIFE path,
|
|
1978
|
+
// which is always safe.
|
|
1979
|
+
const inStatementList =
|
|
1980
|
+
stmtPath
|
|
1981
|
+
&& stmtPath.isStatement()
|
|
1982
|
+
&& (stmtPath.listKey === 'body' || stmtPath.listKey === 'consequent')
|
|
1983
|
+
&& Array.isArray(stmtPath.container);
|
|
1984
|
+
if (inStatementList && !crossedFunctionBoundary) {
|
|
1985
|
+
// Same function scope — safe to hoist setup before the enclosing
|
|
1986
|
+
// statement. Works for return statements too: `insertBefore`
|
|
1987
|
+
// places setup above `return <jsx/>` without wrapping in an IIFE.
|
|
1988
|
+
stmtPath.insertBefore(pending);
|
|
1541
1989
|
path.replaceWith(transformed);
|
|
1542
1990
|
} else {
|
|
1543
|
-
//
|
|
1991
|
+
// Crossed a function boundary or no enclosing statement found —
|
|
1992
|
+
// fall back to IIFE so closure variables remain in scope.
|
|
1544
1993
|
pending.push(t.returnStatement(transformed));
|
|
1545
1994
|
path.replaceWith(
|
|
1546
1995
|
t.callExpression(
|