what-compiler 0.8.4 → 0.11.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 +539 -87
- 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 +595 -93
- 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 +595 -93
- 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 +898 -105
- package/src/file-router.js +104 -2
- package/src/vite-plugin.js +60 -3
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
|
|
|
@@ -564,7 +773,7 @@ export default function whatBabelPlugin({ types: t }) {
|
|
|
564
773
|
];
|
|
565
774
|
|
|
566
775
|
// Apply dynamic attributes and events
|
|
567
|
-
applyDynamicAttrs(statements, elId, attributes, state);
|
|
776
|
+
applyDynamicAttrs(statements, elId, attributes, state, tagName);
|
|
568
777
|
|
|
569
778
|
// Handle dynamic children
|
|
570
779
|
applyDynamicChildren(statements, elId, children, node, state);
|
|
@@ -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)) {
|
|
@@ -620,8 +829,51 @@ export default function whatBabelPlugin({ types: t }) {
|
|
|
620
829
|
return t.callExpression(t.identifier('h'), [t.stringLiteral(tagName), propsExpr, ...transformedChildren]);
|
|
621
830
|
}
|
|
622
831
|
|
|
623
|
-
|
|
832
|
+
// Tags where `value` / `checked` are live DOM properties the user expects a
|
|
833
|
+
// dynamic binding to drive (input.value, select.value, input.checked, ...).
|
|
834
|
+
// Other tags keep the generic setProp path (e.g. <div value={x}> sets an
|
|
835
|
+
// attribute, <li value={n}> hits the `key in el` property branch).
|
|
836
|
+
const VALUE_PROP_TAGS = new Set(['input', 'textarea', 'select', 'option']);
|
|
837
|
+
|
|
838
|
+
function applyDynamicAttrs(statements, elId, attributes, state, tagName) {
|
|
839
|
+
// Specialized monomorphic setters for statically-known attribute names
|
|
840
|
+
// (SPRINT v0.11 C2). The generic _$setProp re-dispatches on the key string
|
|
841
|
+
// (ref/key/url/class/style/innerHTML/boolean/...) on EVERY reactive update;
|
|
842
|
+
// when the compiler knows the name it emits the direct helper instead.
|
|
843
|
+
// SECURITY: URL attributes (href/src/action/formaction) and innerHTML/
|
|
844
|
+
// dangerouslySetInnerHTML intentionally fall through to _$setProp — URL
|
|
845
|
+
// sanitization and the { __html } enforcement live there.
|
|
624
846
|
function buildSetPropCall(propName, valueExpr) {
|
|
847
|
+
if (propName === 'class') {
|
|
848
|
+
// normalizeAttrName already mapped className → class
|
|
849
|
+
state.needsSetClass = true;
|
|
850
|
+
return t.callExpression(t.identifier('_$setClass'), [t.identifier(elId), valueExpr]);
|
|
851
|
+
}
|
|
852
|
+
if (propName === 'style') {
|
|
853
|
+
state.needsSetStyle = true;
|
|
854
|
+
return t.callExpression(t.identifier('_$setStyle'), [t.identifier(elId), valueExpr]);
|
|
855
|
+
}
|
|
856
|
+
if (propName === 'value' && tagName && VALUE_PROP_TAGS.has(tagName)) {
|
|
857
|
+
state.needsSetValue = true;
|
|
858
|
+
return t.callExpression(t.identifier('_$setValue'), [t.identifier(elId), valueExpr]);
|
|
859
|
+
}
|
|
860
|
+
if (propName === 'checked' && tagName === 'input') {
|
|
861
|
+
// Live property write — matches bind:checked semantics. (The old
|
|
862
|
+
// setAttribute('checked') path only set the DEFAULT-checked state,
|
|
863
|
+
// which stops reflecting once the user has toggled the input.)
|
|
864
|
+
// A helper (not a raw `.checked =`) so function values still get
|
|
865
|
+
// reactive-accessor treatment, like every other setter.
|
|
866
|
+
state.needsSetChecked = true;
|
|
867
|
+
return t.callExpression(t.identifier('_$setChecked'), [t.identifier(elId), valueExpr]);
|
|
868
|
+
}
|
|
869
|
+
if (propName.startsWith('data-') || propName.startsWith('aria-')) {
|
|
870
|
+
state.needsSetAttr = true;
|
|
871
|
+
return t.callExpression(t.identifier('_$setAttr'), [
|
|
872
|
+
t.identifier(elId),
|
|
873
|
+
t.stringLiteral(propName),
|
|
874
|
+
valueExpr
|
|
875
|
+
]);
|
|
876
|
+
}
|
|
625
877
|
state.needsSetProp = true;
|
|
626
878
|
return t.callExpression(t.identifier('_$setProp'), [
|
|
627
879
|
t.identifier(elId),
|
|
@@ -630,6 +882,18 @@ export default function whatBabelPlugin({ types: t }) {
|
|
|
630
882
|
]);
|
|
631
883
|
}
|
|
632
884
|
|
|
885
|
+
// Lazy delegation init (C6): the first element of a module that assigns a
|
|
886
|
+
// delegated `$$event` handler also calls the once-guarded _$delegate$()
|
|
887
|
+
// helper at construction time. Emitted at most once per element.
|
|
888
|
+
let delegateInitEmitted = false;
|
|
889
|
+
function emitDelegateInit() {
|
|
890
|
+
if (delegateInitEmitted) return;
|
|
891
|
+
delegateInitEmitted = true;
|
|
892
|
+
statements.push(
|
|
893
|
+
t.expressionStatement(t.callExpression(t.identifier('_$delegate$'), []))
|
|
894
|
+
);
|
|
895
|
+
}
|
|
896
|
+
|
|
633
897
|
for (const attr of attributes) {
|
|
634
898
|
if (t.isJSXSpreadAttribute(attr)) {
|
|
635
899
|
state.needsSpread = true;
|
|
@@ -669,21 +933,22 @@ export default function whatBabelPlugin({ types: t }) {
|
|
|
669
933
|
}
|
|
670
934
|
|
|
671
935
|
// Event handlers
|
|
672
|
-
if (attrName.startsWith('on') && !attrName.includes('|')) {
|
|
936
|
+
if (attrName.startsWith('on') && !attrName.includes('|') && !hasEventModifiers(attrName, state)) {
|
|
673
937
|
const event = attrName.slice(2).toLowerCase();
|
|
674
938
|
const handler = getAttributeValue(attr.value);
|
|
675
939
|
|
|
676
940
|
if (DELEGATED_EVENTS.has(event)) {
|
|
677
|
-
// Use event delegation: el
|
|
941
|
+
// Use event delegation: el.$$click = handler (matches runtime lookup)
|
|
678
942
|
state.needsDelegation = true;
|
|
679
943
|
if (!state.delegatedEvents) state.delegatedEvents = new Set();
|
|
680
944
|
state.delegatedEvents.add(event);
|
|
945
|
+
emitDelegateInit();
|
|
681
946
|
statements.push(
|
|
682
947
|
t.expressionStatement(
|
|
683
948
|
t.assignmentExpression('=',
|
|
684
949
|
t.memberExpression(
|
|
685
950
|
t.identifier(elId),
|
|
686
|
-
t.identifier(
|
|
951
|
+
t.identifier(`$$${event}`)
|
|
687
952
|
),
|
|
688
953
|
handler
|
|
689
954
|
)
|
|
@@ -703,8 +968,8 @@ export default function whatBabelPlugin({ types: t }) {
|
|
|
703
968
|
continue;
|
|
704
969
|
}
|
|
705
970
|
|
|
706
|
-
// Event with modifiers
|
|
707
|
-
if (attrName.startsWith('on') && attrName.includes('|')) {
|
|
971
|
+
// Event with modifiers (pipe '|' or JSX-safe double underscore '__')
|
|
972
|
+
if (attrName.startsWith('on') && (attrName.includes('|') || hasEventModifiers(attrName, state))) {
|
|
708
973
|
const { eventName, modifiers } = parseEventModifiers(attrName);
|
|
709
974
|
const handler = getAttributeValue(attr.value);
|
|
710
975
|
const wrappedHandler = createEventHandler(handler, modifiers);
|
|
@@ -810,8 +1075,14 @@ export default function whatBabelPlugin({ types: t }) {
|
|
|
810
1075
|
|
|
811
1076
|
if (isPotentiallyReactive(expr, state.signalNames, state.importedIdentifiers)) {
|
|
812
1077
|
state.needsEffect = true;
|
|
1078
|
+
// Auto-invoke bare signal/imported identifiers: value={name} -> name()
|
|
1079
|
+
const valueExpr = t.isIdentifier(expr) &&
|
|
1080
|
+
(isSignalIdentifier(expr.name, state.signalNames) ||
|
|
1081
|
+
(state.importedIdentifiers && state.importedIdentifiers.has(expr.name)))
|
|
1082
|
+
? t.callExpression(expr, [])
|
|
1083
|
+
: expr;
|
|
813
1084
|
const effectCall = t.callExpression(t.identifier('_$effect'), [
|
|
814
|
-
t.arrowFunctionExpression([], buildSetPropCall(domName,
|
|
1085
|
+
t.arrowFunctionExpression([], buildSetPropCall(domName, valueExpr))
|
|
815
1086
|
]);
|
|
816
1087
|
// In dev mode, add a leading comment when the effect wrapping is uncertain
|
|
817
1088
|
// (non-signal function call whose args happen to contain signal reads)
|
|
@@ -830,6 +1101,99 @@ export default function whatBabelPlugin({ types: t }) {
|
|
|
830
1101
|
}
|
|
831
1102
|
}
|
|
832
1103
|
|
|
1104
|
+
// =====================================================
|
|
1105
|
+
// Branch Memoization (SPRINT v0.11 C1)
|
|
1106
|
+
// =====================================================
|
|
1107
|
+
// `_$insert(el, () => cond() ? <A/> : <B/>)` re-creates the taken branch's
|
|
1108
|
+
// DOM tree (and re-registers every effect inside it) on EVERY re-evaluation
|
|
1109
|
+
// of the insert effect — including writes to signals read by the condition
|
|
1110
|
+
// that do NOT flip which branch is taken (e.g. `count() > 5` while count
|
|
1111
|
+
// goes 6 → 7). Solid solves this by memoizing the condition: route the
|
|
1112
|
+
// condition through an eager, equality-gated memo so the insert effect
|
|
1113
|
+
// depends on the *memo* instead of the raw signals:
|
|
1114
|
+
//
|
|
1115
|
+
// const _c$0 = _$memo(() => !!(count() > 5));
|
|
1116
|
+
// _$insert(_el$, () => _c$0() ? <A/> : <B/>, marker);
|
|
1117
|
+
//
|
|
1118
|
+
// The memo re-evaluates on every count write, but only NOTIFIES when its
|
|
1119
|
+
// value changes — so branch DOM is recreated exactly on real flips.
|
|
1120
|
+
//
|
|
1121
|
+
// Semantics preserved:
|
|
1122
|
+
// - Ternary tests only matter for truthiness → memoize `!!test`.
|
|
1123
|
+
// - `a && b` / `a || b` render the LEFT operand's VALUE when it
|
|
1124
|
+
// short-circuits (`{0 && <div/>}` renders "0"), so the left side is
|
|
1125
|
+
// memoized by value (Object.is) — never coerced.
|
|
1126
|
+
// - Branch-internal reactivity (signals read inside <A/>) is fine-grained
|
|
1127
|
+
// and unaffected; plain-value branches read in the insert arrow are
|
|
1128
|
+
// still tracked by the insert effect directly.
|
|
1129
|
+
|
|
1130
|
+
// Does this expression produce DOM when evaluated? (raw JSX still present at
|
|
1131
|
+
// this stage, or an already-lowered _$mapArray list). Only then is branch
|
|
1132
|
+
// memoization worth the extra memo node.
|
|
1133
|
+
function buildsDOM(node) {
|
|
1134
|
+
if (!node || typeof node !== 'object') return false;
|
|
1135
|
+
if (Array.isArray(node)) return node.some(buildsDOM);
|
|
1136
|
+
if (node.type === 'JSXElement' || node.type === 'JSXFragment') return true;
|
|
1137
|
+
if (node.type === 'CallExpression' && node.callee &&
|
|
1138
|
+
node.callee.type === 'Identifier' &&
|
|
1139
|
+
(node.callee.name === '_$mapArray' || node.callee.name === 'mapArray')) {
|
|
1140
|
+
return true;
|
|
1141
|
+
}
|
|
1142
|
+
for (const key of Object.keys(node)) {
|
|
1143
|
+
if (key === 'loc' || key === 'start' || key === 'end' || key === 'leadingComments' ||
|
|
1144
|
+
key === 'trailingComments' || key === 'innerComments') continue;
|
|
1145
|
+
const v = node[key];
|
|
1146
|
+
if (v && typeof v === 'object' && buildsDOM(v)) return true;
|
|
1147
|
+
}
|
|
1148
|
+
return false;
|
|
1149
|
+
}
|
|
1150
|
+
|
|
1151
|
+
// If `expr` is a conditional (ternary / && / ||) with a reactive test and a
|
|
1152
|
+
// DOM-producing branch, hoist the test into `const _c$N = _$memo(...)` (pushed
|
|
1153
|
+
// onto `statements`) and return the expression rewritten to read the memo.
|
|
1154
|
+
// Otherwise returns `expr` unchanged.
|
|
1155
|
+
function memoizeBranchCondition(expr, statements, state) {
|
|
1156
|
+
let testExpr = null;
|
|
1157
|
+
let isTernary = false;
|
|
1158
|
+
if (t.isConditionalExpression(expr)) {
|
|
1159
|
+
testExpr = expr.test;
|
|
1160
|
+
isTernary = true;
|
|
1161
|
+
} else if (t.isLogicalExpression(expr) && (expr.operator === '&&' || expr.operator === '||')) {
|
|
1162
|
+
testExpr = expr.left;
|
|
1163
|
+
} else {
|
|
1164
|
+
return expr;
|
|
1165
|
+
}
|
|
1166
|
+
|
|
1167
|
+
if (!isPotentiallyReactive(testExpr, state.signalNames, state.importedIdentifiers)) return expr;
|
|
1168
|
+
|
|
1169
|
+
const branches = isTernary ? [expr.consequent, expr.alternate] : [expr.right];
|
|
1170
|
+
if (!branches.some(buildsDOM)) return expr;
|
|
1171
|
+
|
|
1172
|
+
const condId = state.nextMemoId();
|
|
1173
|
+
state.needsMemo = true;
|
|
1174
|
+
// Ternary: only truthiness matters in test position → gate on !!test so
|
|
1175
|
+
// value changes that don't flip truthiness (5 → 6) never notify.
|
|
1176
|
+
// Logical: the left VALUE is rendered on short-circuit → gate on the value.
|
|
1177
|
+
const memoBody = isTernary
|
|
1178
|
+
? t.unaryExpression('!', t.unaryExpression('!', testExpr))
|
|
1179
|
+
: testExpr;
|
|
1180
|
+
statements.push(
|
|
1181
|
+
t.variableDeclaration('const', [
|
|
1182
|
+
t.variableDeclarator(
|
|
1183
|
+
t.identifier(condId),
|
|
1184
|
+
t.callExpression(t.identifier('_$memo'), [
|
|
1185
|
+
t.arrowFunctionExpression([], memoBody)
|
|
1186
|
+
])
|
|
1187
|
+
)
|
|
1188
|
+
])
|
|
1189
|
+
);
|
|
1190
|
+
|
|
1191
|
+
const condRead = t.callExpression(t.identifier(condId), []);
|
|
1192
|
+
return isTernary
|
|
1193
|
+
? t.conditionalExpression(condRead, expr.consequent, expr.alternate)
|
|
1194
|
+
: t.logicalExpression(expr.operator, condRead, expr.right);
|
|
1195
|
+
}
|
|
1196
|
+
|
|
833
1197
|
function applyDynamicChildren(statements, elId, children, parentNode, state) {
|
|
834
1198
|
// Two-pass approach: first collect all children needing DOM references,
|
|
835
1199
|
// then pre-capture markers before any _$insert() calls shift indices.
|
|
@@ -841,7 +1205,7 @@ export default function whatBabelPlugin({ types: t }) {
|
|
|
841
1205
|
|
|
842
1206
|
for (const child of children) {
|
|
843
1207
|
if (t.isJSXText(child)) {
|
|
844
|
-
const text = child.value
|
|
1208
|
+
const text = normalizeJsxText(child.value);
|
|
845
1209
|
if (text) childIndex++;
|
|
846
1210
|
continue;
|
|
847
1211
|
}
|
|
@@ -881,24 +1245,43 @@ export default function whatBabelPlugin({ types: t }) {
|
|
|
881
1245
|
e.type === 'expression' || e.type === 'component' ||
|
|
882
1246
|
(e.type === 'static' && e.hasAnythingDynamic)
|
|
883
1247
|
);
|
|
884
|
-
|
|
885
|
-
|
|
1248
|
+
// Pre-capture whenever 2+ children need a DOM ref. Beyond preventing index
|
|
1249
|
+
// shift after insert() mutations, the shared O(n) cursor walk below replaces
|
|
1250
|
+
// per-child `el.firstChild.nextSibling…`-from-root access, which was O(n²) in
|
|
1251
|
+
// both compile time and emitted size for elements with many dynamic
|
|
1252
|
+
// children. (AUDIT-2026-06-06 H2)
|
|
1253
|
+
const needsPreCapture = entriesNeedingRef.length >= 2;
|
|
886
1254
|
|
|
887
1255
|
const markerVars = new Map(); // childIndex → variable name
|
|
888
1256
|
if (needsPreCapture) {
|
|
1257
|
+
// Chain each marker from the PREVIOUS captured cursor instead of
|
|
1258
|
+
// re-walking `el.firstChild.nextSibling…` from the root for every child.
|
|
1259
|
+
// entriesNeedingRef is in ascending childIndex order, so the per-marker
|
|
1260
|
+
// deltas sum to O(n) total instead of O(n²). This was the dominant
|
|
1261
|
+
// quadratic in compile time and emitted-bundle size for large elements.
|
|
1262
|
+
// (AUDIT-2026-06-06 H2)
|
|
1263
|
+
let prevVar = null;
|
|
1264
|
+
let prevIndex = 0;
|
|
889
1265
|
for (const entry of entriesNeedingRef) {
|
|
890
|
-
const
|
|
891
|
-
// Use a unique name to avoid collisions with element vars
|
|
1266
|
+
const idx = entry.childIndex;
|
|
892
1267
|
const markerVar = state.nextVarId();
|
|
893
|
-
markerVars.set(
|
|
1268
|
+
markerVars.set(idx, markerVar);
|
|
1269
|
+
let init;
|
|
1270
|
+
if (prevVar === null) {
|
|
1271
|
+
init = buildChildAccess(elId, idx);
|
|
1272
|
+
} else {
|
|
1273
|
+
init = t.identifier(prevVar);
|
|
1274
|
+
for (let i = prevIndex; i < idx; i++) {
|
|
1275
|
+
init = t.memberExpression(init, t.identifier('nextSibling'));
|
|
1276
|
+
}
|
|
1277
|
+
}
|
|
894
1278
|
statements.push(
|
|
895
1279
|
t.variableDeclaration('const', [
|
|
896
|
-
t.variableDeclarator(
|
|
897
|
-
t.identifier(markerVar),
|
|
898
|
-
buildChildAccess(elId, entry.childIndex)
|
|
899
|
-
)
|
|
1280
|
+
t.variableDeclarator(t.identifier(markerVar), init)
|
|
900
1281
|
])
|
|
901
1282
|
);
|
|
1283
|
+
prevVar = markerVar;
|
|
1284
|
+
prevIndex = idx;
|
|
902
1285
|
}
|
|
903
1286
|
}
|
|
904
1287
|
|
|
@@ -913,11 +1296,71 @@ export default function whatBabelPlugin({ types: t }) {
|
|
|
913
1296
|
// --- Pass 2: Generate code using stable references ---
|
|
914
1297
|
for (const entry of entries) {
|
|
915
1298
|
if (entry.type === 'expression') {
|
|
916
|
-
|
|
1299
|
+
let expr = entry.child.expression;
|
|
917
1300
|
const marker = getMarker(entry.childIndex);
|
|
918
1301
|
state.needsInsert = true;
|
|
919
1302
|
|
|
1303
|
+
// Auto-lower .map() to mapArray when the callback returns keyed JSX.
|
|
1304
|
+
// Pattern: source().map(item => <Comp key={...} />) or source().map((item, i) => ...)
|
|
1305
|
+
let mapResult = tryLowerMapToMapArray(expr, state);
|
|
1306
|
+
if (mapResult) {
|
|
1307
|
+
state.needsMapArray = true;
|
|
1308
|
+
// A bare _$mapArray(...) call is a self-managing inserter (it tracks
|
|
1309
|
+
// its source internally) and an arrow is already reactive — pass both
|
|
1310
|
+
// raw. But when lowering produced a ternary/logical wrapping the call
|
|
1311
|
+
// (e.g. cond ? _$mapArray(...) : fallback), the surrounding condition
|
|
1312
|
+
// must stay reactive, so wrap the whole expression in () => and let
|
|
1313
|
+
// _$insert re-evaluate it on change. Without this the condition is read
|
|
1314
|
+
// exactly once and never re-tracks. (AUDIT-2026-06-06 H1)
|
|
1315
|
+
const isBareMapArray = t.isCallExpression(mapResult) && t.isIdentifier(mapResult.callee) &&
|
|
1316
|
+
(mapResult.callee.name === '_$mapArray' || mapResult.callee.name === 'mapArray');
|
|
1317
|
+
const isArrowAlready = t.isArrowFunctionExpression(mapResult);
|
|
1318
|
+
// Branch memoization (C1): when the lowered result is a conditional
|
|
1319
|
+
// around the list (cond ? _$mapArray(...) : fallback), memoize the
|
|
1320
|
+
// condition so non-flip writes don't tear down and recreate the
|
|
1321
|
+
// entire list inserter.
|
|
1322
|
+
if (isArrowAlready && t.isExpression(mapResult.body)) {
|
|
1323
|
+
mapResult.body = memoizeBranchCondition(mapResult.body, statements, state);
|
|
1324
|
+
} else if (!isBareMapArray && !isArrowAlready) {
|
|
1325
|
+
mapResult = memoizeBranchCondition(mapResult, statements, state);
|
|
1326
|
+
}
|
|
1327
|
+
const insertArg = (isBareMapArray || isArrowAlready)
|
|
1328
|
+
? mapResult
|
|
1329
|
+
: t.arrowFunctionExpression([], mapResult);
|
|
1330
|
+
statements.push(
|
|
1331
|
+
t.expressionStatement(
|
|
1332
|
+
t.callExpression(t.identifier('_$insert'), [
|
|
1333
|
+
t.identifier(elId),
|
|
1334
|
+
insertArg,
|
|
1335
|
+
marker
|
|
1336
|
+
])
|
|
1337
|
+
)
|
|
1338
|
+
);
|
|
1339
|
+
continue;
|
|
1340
|
+
}
|
|
1341
|
+
|
|
1342
|
+
// mapArray() calls return self-managing inserters — pass directly, never wrap in () =>
|
|
1343
|
+
const isMapArrayCall = t.isCallExpression(expr) && t.isIdentifier(expr.callee) &&
|
|
1344
|
+
(expr.callee.name === 'mapArray' || expr.callee.name === '_$mapArray');
|
|
1345
|
+
if (isMapArrayCall) {
|
|
1346
|
+
state.needsMapArray = true;
|
|
1347
|
+
if (expr.callee.name === 'mapArray') expr.callee.name = '_$mapArray';
|
|
1348
|
+
statements.push(
|
|
1349
|
+
t.expressionStatement(
|
|
1350
|
+
t.callExpression(t.identifier('_$insert'), [
|
|
1351
|
+
t.identifier(elId),
|
|
1352
|
+
expr,
|
|
1353
|
+
marker
|
|
1354
|
+
])
|
|
1355
|
+
)
|
|
1356
|
+
);
|
|
1357
|
+
continue;
|
|
1358
|
+
}
|
|
1359
|
+
|
|
920
1360
|
if (isPotentiallyReactive(expr, state.signalNames, state.importedIdentifiers)) {
|
|
1361
|
+
// Branch memoization (C1): conditional children only rebuild branch
|
|
1362
|
+
// DOM when the condition actually flips.
|
|
1363
|
+
expr = memoizeBranchCondition(expr, statements, state);
|
|
921
1364
|
const insertCall = t.callExpression(t.identifier('_$insert'), [
|
|
922
1365
|
t.identifier(elId),
|
|
923
1366
|
t.arrowFunctionExpression([], expr),
|
|
@@ -976,7 +1419,7 @@ export default function whatBabelPlugin({ types: t }) {
|
|
|
976
1419
|
])
|
|
977
1420
|
);
|
|
978
1421
|
}
|
|
979
|
-
applyDynamicAttrs(statements, childElRef, entry.child.openingElement.attributes, state);
|
|
1422
|
+
applyDynamicAttrs(statements, childElRef, entry.child.openingElement.attributes, state, entry.child.openingElement.name.name);
|
|
980
1423
|
applyDynamicChildren(statements, childElRef, entry.child.children, entry.child, state);
|
|
981
1424
|
continue;
|
|
982
1425
|
}
|
|
@@ -985,8 +1428,9 @@ export default function whatBabelPlugin({ types: t }) {
|
|
|
985
1428
|
for (const fChild of entry.child.children) {
|
|
986
1429
|
if (t.isJSXExpressionContainer(fChild) && !t.isJSXEmptyExpression(fChild.expression)) {
|
|
987
1430
|
state.needsInsert = true;
|
|
988
|
-
|
|
1431
|
+
let expr = fChild.expression;
|
|
989
1432
|
if (isPotentiallyReactive(expr, state.signalNames, state.importedIdentifiers)) {
|
|
1433
|
+
expr = memoizeBranchCondition(expr, statements, state); // (C1)
|
|
990
1434
|
statements.push(
|
|
991
1435
|
t.expressionStatement(
|
|
992
1436
|
t.callExpression(t.identifier('_$insert'), [
|
|
@@ -1153,7 +1597,7 @@ export default function whatBabelPlugin({ types: t }) {
|
|
|
1153
1597
|
}
|
|
1154
1598
|
|
|
1155
1599
|
// Handle event modifiers on components
|
|
1156
|
-
if (attrName.startsWith('on') && attrName.includes('|')) {
|
|
1600
|
+
if (attrName.startsWith('on') && (attrName.includes('|') || hasEventModifiers(attrName, state))) {
|
|
1157
1601
|
const { eventName, modifiers } = parseEventModifiers(attrName);
|
|
1158
1602
|
const handler = getAttributeValue(attr.value);
|
|
1159
1603
|
const wrappedHandler = createEventHandler(handler, modifiers);
|
|
@@ -1177,7 +1621,7 @@ export default function whatBabelPlugin({ types: t }) {
|
|
|
1177
1621
|
const transformedChildren = [];
|
|
1178
1622
|
for (const child of children) {
|
|
1179
1623
|
if (t.isJSXText(child)) {
|
|
1180
|
-
const text = child.value
|
|
1624
|
+
const text = normalizeJsxText(child.value);
|
|
1181
1625
|
if (text) transformedChildren.push(t.stringLiteral(text));
|
|
1182
1626
|
} else if (t.isJSXExpressionContainer(child)) {
|
|
1183
1627
|
if (!t.isJSXEmptyExpression(child.expression)) {
|
|
@@ -1218,12 +1662,35 @@ export default function whatBabelPlugin({ types: t }) {
|
|
|
1218
1662
|
const attributes = node.openingElement.attributes;
|
|
1219
1663
|
const children = node.children;
|
|
1220
1664
|
|
|
1221
|
-
// <For each={data}>{(item) => <Row />}</For>
|
|
1222
|
-
// → mapArray(data, (item) =>
|
|
1665
|
+
// <For each={data} key={item => item.id}>{(item) => <Row />}</For>
|
|
1666
|
+
// → mapArray(data, (item) => ..., { key: item => item.id })
|
|
1667
|
+
//
|
|
1668
|
+
// NOTE: <For> is supported but .map() with a key prop is the preferred
|
|
1669
|
+
// pattern for list rendering. The compiler auto-lowers .map() to
|
|
1670
|
+
// _$mapArray with raw mode, which is simpler and matches JS idioms.
|
|
1671
|
+
// <For> is useful when you need signal-wrapped item accessors (keyed
|
|
1672
|
+
// mode without raw), so that item updates don't recreate DOM nodes.
|
|
1673
|
+
if (process.env.NODE_ENV !== 'production') {
|
|
1674
|
+
const fileName = state.filename || state.file?.opts?.filename || '<unknown>';
|
|
1675
|
+
if (!_forInfoWarned.has(fileName)) {
|
|
1676
|
+
_forInfoWarned.add(fileName);
|
|
1677
|
+
const loc = node.loc;
|
|
1678
|
+
const lineInfo = loc ? `:${loc.start.line}:${loc.start.column}` : '';
|
|
1679
|
+
console.info(
|
|
1680
|
+
`[what-compiler] <For> at ${fileName}${lineInfo}: consider using .map() with a key prop instead. ` +
|
|
1681
|
+
`The compiler auto-lowers .map() to efficient keyed reconciliation. ` +
|
|
1682
|
+
`<For> is only needed for signal-wrapped item accessors (advanced).`
|
|
1683
|
+
);
|
|
1684
|
+
}
|
|
1685
|
+
}
|
|
1686
|
+
|
|
1223
1687
|
let eachExpr = null;
|
|
1688
|
+
let keyExpr = null;
|
|
1224
1689
|
for (const attr of attributes) {
|
|
1225
|
-
if (t.isJSXAttribute(attr)
|
|
1226
|
-
|
|
1690
|
+
if (t.isJSXAttribute(attr)) {
|
|
1691
|
+
const name = getAttrName(attr);
|
|
1692
|
+
if (name === 'each') eachExpr = getAttributeValue(attr.value);
|
|
1693
|
+
else if (name === 'key') keyExpr = getAttributeValue(attr.value);
|
|
1227
1694
|
}
|
|
1228
1695
|
}
|
|
1229
1696
|
|
|
@@ -1248,14 +1715,209 @@ export default function whatBabelPlugin({ types: t }) {
|
|
|
1248
1715
|
}
|
|
1249
1716
|
|
|
1250
1717
|
state.needsMapArray = true;
|
|
1251
|
-
|
|
1718
|
+
const args = [eachExpr, renderFn];
|
|
1719
|
+
if (keyExpr) {
|
|
1720
|
+
args.push(t.objectExpression([
|
|
1721
|
+
t.objectProperty(t.identifier('key'), keyExpr)
|
|
1722
|
+
]));
|
|
1723
|
+
}
|
|
1724
|
+
return t.callExpression(t.identifier('_$mapArray'), args);
|
|
1252
1725
|
}
|
|
1253
1726
|
|
|
1254
1727
|
function transformShowFineGrained(path, state) {
|
|
1255
|
-
// <Show when={cond}>{content}</Show>
|
|
1256
|
-
//
|
|
1257
|
-
|
|
1258
|
-
|
|
1728
|
+
// <Show when={cond} fallback={alt}>{content}</Show>
|
|
1729
|
+
// → () => cond() ? content : (fallback || null)
|
|
1730
|
+
// This compiles to a reactive expression that insert() wraps in an effect.
|
|
1731
|
+
const { node } = path;
|
|
1732
|
+
const attributes = node.openingElement.attributes;
|
|
1733
|
+
const children = node.children;
|
|
1734
|
+
|
|
1735
|
+
let whenExpr = null;
|
|
1736
|
+
let fallbackExpr = null;
|
|
1737
|
+
for (const attr of attributes) {
|
|
1738
|
+
if (t.isJSXAttribute(attr)) {
|
|
1739
|
+
const name = getAttrName(attr);
|
|
1740
|
+
if (name === 'when') whenExpr = getAttributeValue(attr.value);
|
|
1741
|
+
else if (name === 'fallback') fallbackExpr = getAttributeValue(attr.value);
|
|
1742
|
+
}
|
|
1743
|
+
}
|
|
1744
|
+
|
|
1745
|
+
if (!whenExpr) {
|
|
1746
|
+
// <Show> without a when prop has no defined semantics — fail loudly at
|
|
1747
|
+
// build time so the user fixes their source instead of seeing runtime
|
|
1748
|
+
// confusion. buildCodeFrameError pins the error to the JSX location.
|
|
1749
|
+
throw path.buildCodeFrameError(
|
|
1750
|
+
'<Show> requires a "when" prop. Example: <Show when={isOpen} fallback={null}>...</Show>'
|
|
1751
|
+
);
|
|
1752
|
+
}
|
|
1753
|
+
|
|
1754
|
+
// Extract the content — either a render function child or static JSX children
|
|
1755
|
+
let contentExpr = null;
|
|
1756
|
+
for (const child of children) {
|
|
1757
|
+
if (t.isJSXExpressionContainer(child) && !t.isJSXEmptyExpression(child.expression)) {
|
|
1758
|
+
// Render function: {() => <div>...</div>} or {(value) => <div>{value}</div>}
|
|
1759
|
+
contentExpr = child.expression;
|
|
1760
|
+
break;
|
|
1761
|
+
}
|
|
1762
|
+
}
|
|
1763
|
+
|
|
1764
|
+
if (!contentExpr) {
|
|
1765
|
+
// Static children — collect and transform them
|
|
1766
|
+
const transformedChildren = [];
|
|
1767
|
+
for (const child of children) {
|
|
1768
|
+
if (t.isJSXText(child)) {
|
|
1769
|
+
const text = normalizeJsxText(child.value);
|
|
1770
|
+
if (text) transformedChildren.push(t.stringLiteral(text));
|
|
1771
|
+
} else if (t.isJSXElement(child)) {
|
|
1772
|
+
transformedChildren.push(transformElementFineGrained({ node: child }, state));
|
|
1773
|
+
}
|
|
1774
|
+
}
|
|
1775
|
+
if (transformedChildren.length === 1) {
|
|
1776
|
+
contentExpr = transformedChildren[0];
|
|
1777
|
+
} else if (transformedChildren.length > 1) {
|
|
1778
|
+
contentExpr = t.arrayExpression(transformedChildren);
|
|
1779
|
+
} else {
|
|
1780
|
+
contentExpr = t.nullLiteral();
|
|
1781
|
+
}
|
|
1782
|
+
}
|
|
1783
|
+
|
|
1784
|
+
// Build:
|
|
1785
|
+
// () => { const _v = <condition>; return _v ? <consequent> : <alternate>; }
|
|
1786
|
+
// Hoisting into a local prevents double-evaluation of the `when` signal
|
|
1787
|
+
// (the consequent's render callback also needs the resolved value).
|
|
1788
|
+
//
|
|
1789
|
+
// `whenExpr` shape determines how we form the condition:
|
|
1790
|
+
// - call expression → use as-is <Show when={cond()}>
|
|
1791
|
+
// - arrow w/ expression body → use the body <Show when={() => x > 5}>
|
|
1792
|
+
// - identifier that looks like a signal/import <Show when={isOpen}>
|
|
1793
|
+
// → invoke it as accessor: isOpen()
|
|
1794
|
+
// - anything else (member, literal, logical, etc.) <Show when={user.isAdmin}>
|
|
1795
|
+
// → use the raw expression. Do NOT invoke —
|
|
1796
|
+
// non-functions would throw at runtime.
|
|
1797
|
+
let condition;
|
|
1798
|
+
if (t.isCallExpression(whenExpr)) {
|
|
1799
|
+
condition = whenExpr;
|
|
1800
|
+
} else if (t.isArrowFunctionExpression(whenExpr) && t.isExpression(whenExpr.body)) {
|
|
1801
|
+
condition = whenExpr.body;
|
|
1802
|
+
} else if (
|
|
1803
|
+
t.isIdentifier(whenExpr) &&
|
|
1804
|
+
(
|
|
1805
|
+
(state.signalNames && isSignalIdentifier(whenExpr.name, state.signalNames)) ||
|
|
1806
|
+
(state.importedIdentifiers && state.importedIdentifiers.has(whenExpr.name))
|
|
1807
|
+
)
|
|
1808
|
+
) {
|
|
1809
|
+
condition = t.callExpression(whenExpr, []);
|
|
1810
|
+
} else {
|
|
1811
|
+
// Plain boolean expression — member access, literal, logical, etc.
|
|
1812
|
+
condition = whenExpr;
|
|
1813
|
+
}
|
|
1814
|
+
|
|
1815
|
+
const vId = path.scope
|
|
1816
|
+
? path.scope.generateUidIdentifier('v')
|
|
1817
|
+
: t.identifier('_v');
|
|
1818
|
+
|
|
1819
|
+
const contentIsFn = t.isFunction(contentExpr);
|
|
1820
|
+
const consequent = contentIsFn
|
|
1821
|
+
? t.callExpression(contentExpr, [t.cloneNode(vId)])
|
|
1822
|
+
: contentExpr;
|
|
1823
|
+
const alternate = fallbackExpr || t.nullLiteral();
|
|
1824
|
+
|
|
1825
|
+
// Branch memoization (SPRINT v0.11 C1): route a reactive `when` through an
|
|
1826
|
+
// equality-gated memo so the insert effect only re-fires (recreating the
|
|
1827
|
+
// taken branch's DOM) when the condition actually changes — not on every
|
|
1828
|
+
// write to a signal the condition happens to read.
|
|
1829
|
+
// - Render-function children receive the resolved value (`{v => ...}`),
|
|
1830
|
+
// so the memo is VALUE-gated (Object.is): identity changes re-render,
|
|
1831
|
+
// matching pre-memo semantics.
|
|
1832
|
+
// - Static children only use the value for truthiness → gate on !!cond
|
|
1833
|
+
// so e.g. `when={items().length}` doesn't re-render on 2 → 3.
|
|
1834
|
+
if (isPotentiallyReactive(condition, state.signalNames, state.importedIdentifiers)) {
|
|
1835
|
+
const condId = state.nextMemoId();
|
|
1836
|
+
state.needsMemo = true;
|
|
1837
|
+
const memoBody = contentIsFn
|
|
1838
|
+
? condition
|
|
1839
|
+
: t.unaryExpression('!', t.unaryExpression('!', condition));
|
|
1840
|
+
if (!state._pendingSetup) state._pendingSetup = [];
|
|
1841
|
+
state._pendingSetup.push(
|
|
1842
|
+
t.variableDeclaration('const', [
|
|
1843
|
+
t.variableDeclarator(
|
|
1844
|
+
t.identifier(condId),
|
|
1845
|
+
t.callExpression(t.identifier('_$memo'), [
|
|
1846
|
+
t.arrowFunctionExpression([], memoBody)
|
|
1847
|
+
])
|
|
1848
|
+
)
|
|
1849
|
+
])
|
|
1850
|
+
);
|
|
1851
|
+
condition = t.callExpression(t.identifier(condId), []);
|
|
1852
|
+
}
|
|
1853
|
+
|
|
1854
|
+
return t.arrowFunctionExpression([], t.blockStatement([
|
|
1855
|
+
t.variableDeclaration('const', [
|
|
1856
|
+
t.variableDeclarator(vId, condition)
|
|
1857
|
+
]),
|
|
1858
|
+
t.returnStatement(
|
|
1859
|
+
t.conditionalExpression(t.cloneNode(vId), consequent, alternate)
|
|
1860
|
+
)
|
|
1861
|
+
]));
|
|
1862
|
+
}
|
|
1863
|
+
|
|
1864
|
+
// A fragment-as-root returns an array (or single value) that the runtime
|
|
1865
|
+
// mounts element-by-element (createDOM / insert). Unlike the element-child
|
|
1866
|
+
// path there's no host element to _$insert into with a marker, so reactive
|
|
1867
|
+
// children must instead be emitted as `() => expr` arrows — the runtime's
|
|
1868
|
+
// createDOM/insert treat a function array-item as a reactive binding (it
|
|
1869
|
+
// wraps it in an effect with comment markers). Without this, a bare dynamic
|
|
1870
|
+
// expression like `{count()}` is evaluated exactly once and never updates.
|
|
1871
|
+
//
|
|
1872
|
+
// This applies the SAME lowering the element-child expression path uses:
|
|
1873
|
+
// - tryLowerMapToMapArray for keyed `items().map(...)` → _$mapArray
|
|
1874
|
+
// - memoizeBranchCondition for reactive ternary/&&/|| (node-identity stable
|
|
1875
|
+
// branches: the taken branch's DOM is only rebuilt when the condition
|
|
1876
|
+
// actually flips, not on every read of a signal the condition touches)
|
|
1877
|
+
// - reactive expressions wrapped in `() =>` so the runtime tracks them
|
|
1878
|
+
// Memo (_$memo) declarations are pushed into state._pendingSetup, which
|
|
1879
|
+
// transformJsxRoot drains into the enclosing scope. (SPRINT v0.11)
|
|
1880
|
+
function lowerFragmentExprChild(expr, state) {
|
|
1881
|
+
if (!state._pendingSetup) state._pendingSetup = [];
|
|
1882
|
+
const setup = state._pendingSetup;
|
|
1883
|
+
|
|
1884
|
+
// Auto-lower .map() to mapArray when the callback returns keyed JSX.
|
|
1885
|
+
const mapResult = tryLowerMapToMapArray(expr, state);
|
|
1886
|
+
if (mapResult) {
|
|
1887
|
+
state.needsMapArray = true;
|
|
1888
|
+
// A bare _$mapArray(...) is a self-managing inserter and an arrow is
|
|
1889
|
+
// already reactive — emit as-is. A ternary/logical wrapping the call
|
|
1890
|
+
// keeps its condition reactive via a () => wrapper (and memoization).
|
|
1891
|
+
const isBareMapArray = t.isCallExpression(mapResult) && t.isIdentifier(mapResult.callee) &&
|
|
1892
|
+
(mapResult.callee.name === '_$mapArray' || mapResult.callee.name === 'mapArray');
|
|
1893
|
+
const isArrowAlready = t.isArrowFunctionExpression(mapResult);
|
|
1894
|
+
if (isArrowAlready && t.isExpression(mapResult.body)) {
|
|
1895
|
+
mapResult.body = memoizeBranchCondition(mapResult.body, setup, state);
|
|
1896
|
+
return mapResult;
|
|
1897
|
+
}
|
|
1898
|
+
if (isBareMapArray) return mapResult;
|
|
1899
|
+
const memoized = memoizeBranchCondition(mapResult, setup, state);
|
|
1900
|
+
return t.arrowFunctionExpression([], memoized);
|
|
1901
|
+
}
|
|
1902
|
+
|
|
1903
|
+
// mapArray() calls are self-managing inserters — pass directly.
|
|
1904
|
+
const isMapArrayCall = t.isCallExpression(expr) && t.isIdentifier(expr.callee) &&
|
|
1905
|
+
(expr.callee.name === 'mapArray' || expr.callee.name === '_$mapArray');
|
|
1906
|
+
if (isMapArrayCall) {
|
|
1907
|
+
state.needsMapArray = true;
|
|
1908
|
+
if (expr.callee.name === 'mapArray') expr.callee.name = '_$mapArray';
|
|
1909
|
+
return expr;
|
|
1910
|
+
}
|
|
1911
|
+
|
|
1912
|
+
if (isPotentiallyReactive(expr, state.signalNames, state.importedIdentifiers)) {
|
|
1913
|
+
// Branch memoization (C1): conditional/logical children only rebuild the
|
|
1914
|
+
// taken branch's DOM when the condition actually flips.
|
|
1915
|
+
expr = memoizeBranchCondition(expr, setup, state);
|
|
1916
|
+
return t.arrowFunctionExpression([], expr);
|
|
1917
|
+
}
|
|
1918
|
+
|
|
1919
|
+
// Static — emit verbatim.
|
|
1920
|
+
return expr;
|
|
1259
1921
|
}
|
|
1260
1922
|
|
|
1261
1923
|
function transformFragmentFineGrained(path, state) {
|
|
@@ -1265,11 +1927,11 @@ export default function whatBabelPlugin({ types: t }) {
|
|
|
1265
1927
|
const transformed = [];
|
|
1266
1928
|
for (const child of children) {
|
|
1267
1929
|
if (t.isJSXText(child)) {
|
|
1268
|
-
const text = child.value
|
|
1930
|
+
const text = normalizeJsxText(child.value);
|
|
1269
1931
|
if (text) transformed.push(t.stringLiteral(text));
|
|
1270
1932
|
} else if (t.isJSXExpressionContainer(child)) {
|
|
1271
1933
|
if (!t.isJSXEmptyExpression(child.expression)) {
|
|
1272
|
-
transformed.push(child.expression);
|
|
1934
|
+
transformed.push(lowerFragmentExprChild(child.expression, state));
|
|
1273
1935
|
}
|
|
1274
1936
|
} else if (t.isJSXElement(child)) {
|
|
1275
1937
|
transformed.push(transformElementFineGrained({ node: child }, state));
|
|
@@ -1293,6 +1955,93 @@ export default function whatBabelPlugin({ types: t }) {
|
|
|
1293
1955
|
return id;
|
|
1294
1956
|
}
|
|
1295
1957
|
|
|
1958
|
+
// Shared driver for top-level JSX roots (elements AND fragments).
|
|
1959
|
+
//
|
|
1960
|
+
// transformElementFineGrained does NOT emit IIFEs: it pushes setup
|
|
1961
|
+
// statements (`const _el$N = _tmpl$X(); _el$N.$$click = ...; _$insert(...)`)
|
|
1962
|
+
// into state._pendingSetup and returns the bare `_el$N` identifier. Whoever
|
|
1963
|
+
// visits the JSX root is responsible for draining _pendingSetup and placing
|
|
1964
|
+
// those statements somewhere the returned reference can see them.
|
|
1965
|
+
//
|
|
1966
|
+
// The JSXElement visitor always did this; the JSXFragment visitor did not,
|
|
1967
|
+
// so fragments whose element children had dynamic parts (event handlers,
|
|
1968
|
+
// dynamic attrs/children) compiled to references to _el$N variables that
|
|
1969
|
+
// were never declared — a runtime ReferenceError. Both visitors now share
|
|
1970
|
+
// this driver. (SPRINT v0.11: composes with C1 branch memoization and C2
|
|
1971
|
+
// specialized setters — memo/setter statements ride in _pendingSetup too.)
|
|
1972
|
+
function transformJsxRoot(path, state, transform) {
|
|
1973
|
+
// FIX-1: Use scope-aware signal detection instead of file-global.
|
|
1974
|
+
// Memoize per Babel scope: every JSX root in the same scope yields the
|
|
1975
|
+
// same signal-name set, so without this the full scope-chain walk ran
|
|
1976
|
+
// once per element — O(n²) compile time for a large single component.
|
|
1977
|
+
// (AUDIT-2026-06-06 H2)
|
|
1978
|
+
const scope = path.scope;
|
|
1979
|
+
let cache = state._signalNamesCache;
|
|
1980
|
+
if (!cache) cache = state._signalNamesCache = new WeakMap();
|
|
1981
|
+
let names = cache.get(scope);
|
|
1982
|
+
if (!names) {
|
|
1983
|
+
names = collectSignalNamesFromScope(path);
|
|
1984
|
+
cache.set(scope, names);
|
|
1985
|
+
}
|
|
1986
|
+
state.signalNames = names;
|
|
1987
|
+
state._pendingSetup = [];
|
|
1988
|
+
const transformed = transform(path, state);
|
|
1989
|
+
const pending = state._pendingSetup;
|
|
1990
|
+
state._pendingSetup = [];
|
|
1991
|
+
|
|
1992
|
+
if (pending.length > 0) {
|
|
1993
|
+
// Find the enclosing statement to hoist setup before it,
|
|
1994
|
+
// but only if it's in the SAME function scope. Crossing into
|
|
1995
|
+
// an inner arrow/function (e.g., .map(item => <JSX/>)) would
|
|
1996
|
+
// hoist references to closure variables out of scope.
|
|
1997
|
+
let stmtPath = path;
|
|
1998
|
+
let crossedFunctionBoundary = false;
|
|
1999
|
+
while (stmtPath && !stmtPath.isStatement()) {
|
|
2000
|
+
if (stmtPath.isArrowFunctionExpression() || stmtPath.isFunctionExpression()) {
|
|
2001
|
+
crossedFunctionBoundary = true;
|
|
2002
|
+
}
|
|
2003
|
+
stmtPath = stmtPath.parentPath;
|
|
2004
|
+
}
|
|
2005
|
+
// We can safely hoist setup as siblings of `stmtPath` ONLY if
|
|
2006
|
+
// `stmtPath` lives inside a statement list (BlockStatement.body or
|
|
2007
|
+
// Program.body). For single-statement positions like
|
|
2008
|
+
// `if (cond) return <jsx/>;` or `while (x) return <jsx/>;`,
|
|
2009
|
+
// Babel's `insertBefore` wraps the parent into a block lazily and
|
|
2010
|
+
// multi-statement inserts end up split across scopes, leaving the
|
|
2011
|
+
// `_$insert(_el$N, ...)` call outside the block that declares
|
|
2012
|
+
// `const _el$N`. This is a TDZ/ReferenceError at runtime.
|
|
2013
|
+
//
|
|
2014
|
+
// To guarantee that ALL setup statements and the returned reference
|
|
2015
|
+
// share one lexical block, require that `stmtPath.listKey` points
|
|
2016
|
+
// at a statement list. Otherwise fall through to the IIFE path,
|
|
2017
|
+
// which is always safe.
|
|
2018
|
+
const inStatementList =
|
|
2019
|
+
stmtPath
|
|
2020
|
+
&& stmtPath.isStatement()
|
|
2021
|
+
&& (stmtPath.listKey === 'body' || stmtPath.listKey === 'consequent')
|
|
2022
|
+
&& Array.isArray(stmtPath.container);
|
|
2023
|
+
if (inStatementList && !crossedFunctionBoundary) {
|
|
2024
|
+
// Same function scope — safe to hoist setup before the enclosing
|
|
2025
|
+
// statement. Works for return statements too: `insertBefore`
|
|
2026
|
+
// places setup above `return <jsx/>` without wrapping in an IIFE.
|
|
2027
|
+
stmtPath.insertBefore(pending);
|
|
2028
|
+
path.replaceWith(transformed);
|
|
2029
|
+
} else {
|
|
2030
|
+
// Crossed a function boundary or no enclosing statement found —
|
|
2031
|
+
// fall back to IIFE so closure variables remain in scope.
|
|
2032
|
+
pending.push(t.returnStatement(transformed));
|
|
2033
|
+
path.replaceWith(
|
|
2034
|
+
t.callExpression(
|
|
2035
|
+
t.arrowFunctionExpression([], t.blockStatement(pending)),
|
|
2036
|
+
[]
|
|
2037
|
+
)
|
|
2038
|
+
);
|
|
2039
|
+
}
|
|
2040
|
+
} else {
|
|
2041
|
+
path.replaceWith(transformed);
|
|
2042
|
+
}
|
|
2043
|
+
}
|
|
2044
|
+
|
|
1296
2045
|
// =====================================================
|
|
1297
2046
|
// Plugin entry
|
|
1298
2047
|
// =====================================================
|
|
@@ -1310,6 +2059,12 @@ export default function whatBabelPlugin({ types: t }) {
|
|
|
1310
2059
|
state.needsMapArray = false;
|
|
1311
2060
|
state.needsSpread = false;
|
|
1312
2061
|
state.needsSetProp = false;
|
|
2062
|
+
state.needsMemo = false; // branch memoization (C1)
|
|
2063
|
+
state.needsSetClass = false; // specialized setters (C2)
|
|
2064
|
+
state.needsSetStyle = false;
|
|
2065
|
+
state.needsSetAttr = false;
|
|
2066
|
+
state.needsSetValue = false;
|
|
2067
|
+
state.needsSetChecked = false;
|
|
1313
2068
|
state.needsH = false;
|
|
1314
2069
|
state.needsCreateComponent = false;
|
|
1315
2070
|
state.needsFragment = false;
|
|
@@ -1320,8 +2075,10 @@ export default function whatBabelPlugin({ types: t }) {
|
|
|
1320
2075
|
state.templateMap = new Map(); // html → template id (deduplication)
|
|
1321
2076
|
state.templateCount = 0;
|
|
1322
2077
|
state._varCounter = 0;
|
|
2078
|
+
state._memoCounter = 0;
|
|
1323
2079
|
state._pendingSetup = [];
|
|
1324
2080
|
state.nextVarId = () => `_el$${state._varCounter++}`;
|
|
2081
|
+
state.nextMemoId = () => `_c$${state._memoCounter++}`;
|
|
1325
2082
|
|
|
1326
2083
|
// Collect signal names for smart reactivity detection
|
|
1327
2084
|
state.signalNames = new Set();
|
|
@@ -1400,12 +2157,14 @@ export default function whatBabelPlugin({ types: t }) {
|
|
|
1400
2157
|
exit(path, state) {
|
|
1401
2158
|
// Insert template declarations at top of program (hoisted to module scope)
|
|
1402
2159
|
for (const tmpl of state.templates.reverse()) {
|
|
2160
|
+
// /* @__PURE__ */ marks the hoisted call as side-effect-free so
|
|
2161
|
+
// bundlers (esbuild/rollup/terser) can drop templates whose
|
|
2162
|
+
// components are tree-shaken away. (SPRINT v0.11 C6)
|
|
2163
|
+
const tmplCall = t.callExpression(t.identifier('_$template'), [t.stringLiteral(tmpl.html)]);
|
|
2164
|
+
t.addComment(tmplCall, 'leading', ' @__PURE__ ');
|
|
1403
2165
|
path.unshiftContainer('body',
|
|
1404
2166
|
t.variableDeclaration('const', [
|
|
1405
|
-
t.variableDeclarator(
|
|
1406
|
-
t.identifier(tmpl.id),
|
|
1407
|
-
t.callExpression(t.identifier('_$template'), [t.stringLiteral(tmpl.html)])
|
|
1408
|
-
)
|
|
2167
|
+
t.variableDeclarator(t.identifier(tmpl.id), tmplCall)
|
|
1409
2168
|
])
|
|
1410
2169
|
);
|
|
1411
2170
|
}
|
|
@@ -1413,8 +2172,13 @@ export default function whatBabelPlugin({ types: t }) {
|
|
|
1413
2172
|
// Build fine-grained imports
|
|
1414
2173
|
const fgSpecifiers = [];
|
|
1415
2174
|
if (state.needsTemplate) {
|
|
2175
|
+
// Import the compiler-internal `_$template` export, NOT the public
|
|
2176
|
+
// `template` export. The public one warns in dev ("template() is a
|
|
2177
|
+
// compiler internal... XSS") — compiled output must never trip that
|
|
2178
|
+
// guard; the warning exists for *hand-written* template() calls
|
|
2179
|
+
// with dynamic strings. (SPRINT v0.11 C5)
|
|
1416
2180
|
fgSpecifiers.push(
|
|
1417
|
-
t.importSpecifier(t.identifier('_$template'), t.identifier('template'))
|
|
2181
|
+
t.importSpecifier(t.identifier('_$template'), t.identifier('_$template'))
|
|
1418
2182
|
);
|
|
1419
2183
|
}
|
|
1420
2184
|
if (state.needsInsert) {
|
|
@@ -1442,6 +2206,36 @@ export default function whatBabelPlugin({ types: t }) {
|
|
|
1442
2206
|
t.importSpecifier(t.identifier('_$setProp'), t.identifier('setProp'))
|
|
1443
2207
|
);
|
|
1444
2208
|
}
|
|
2209
|
+
if (state.needsMemo) {
|
|
2210
|
+
fgSpecifiers.push(
|
|
2211
|
+
t.importSpecifier(t.identifier('_$memo'), t.identifier('memo'))
|
|
2212
|
+
);
|
|
2213
|
+
}
|
|
2214
|
+
if (state.needsSetClass) {
|
|
2215
|
+
fgSpecifiers.push(
|
|
2216
|
+
t.importSpecifier(t.identifier('_$setClass'), t.identifier('setClass'))
|
|
2217
|
+
);
|
|
2218
|
+
}
|
|
2219
|
+
if (state.needsSetStyle) {
|
|
2220
|
+
fgSpecifiers.push(
|
|
2221
|
+
t.importSpecifier(t.identifier('_$setStyle'), t.identifier('setStyle'))
|
|
2222
|
+
);
|
|
2223
|
+
}
|
|
2224
|
+
if (state.needsSetAttr) {
|
|
2225
|
+
fgSpecifiers.push(
|
|
2226
|
+
t.importSpecifier(t.identifier('_$setAttr'), t.identifier('setAttr'))
|
|
2227
|
+
);
|
|
2228
|
+
}
|
|
2229
|
+
if (state.needsSetValue) {
|
|
2230
|
+
fgSpecifiers.push(
|
|
2231
|
+
t.importSpecifier(t.identifier('_$setValue'), t.identifier('setValue'))
|
|
2232
|
+
);
|
|
2233
|
+
}
|
|
2234
|
+
if (state.needsSetChecked) {
|
|
2235
|
+
fgSpecifiers.push(
|
|
2236
|
+
t.importSpecifier(t.identifier('_$setChecked'), t.identifier('setChecked'))
|
|
2237
|
+
);
|
|
2238
|
+
}
|
|
1445
2239
|
if (state.needsCreateComponent) {
|
|
1446
2240
|
fgSpecifiers.push(
|
|
1447
2241
|
t.importSpecifier(t.identifier('_$createComponent'), t.identifier('_$createComponent'))
|
|
@@ -1505,58 +2299,57 @@ export default function whatBabelPlugin({ types: t }) {
|
|
|
1505
2299
|
addCoreImports(path, t, coreSpecifiers);
|
|
1506
2300
|
}
|
|
1507
2301
|
|
|
1508
|
-
// Emit event delegation setup
|
|
2302
|
+
// Emit LAZY event delegation setup if any delegated events were used.
|
|
2303
|
+
// Previously this was a bare module-top-level `_$delegateEvents([...])`
|
|
2304
|
+
// call — a side effect that (a) prevented bundlers from tree-shaking
|
|
2305
|
+
// modules whose components are never used, and (b) attached document
|
|
2306
|
+
// listeners at import time even when no component ever mounted.
|
|
2307
|
+
// Instead we emit a once-guarded helper that each element setup calls
|
|
2308
|
+
// at construction time; unused modules carry only dead declarations
|
|
2309
|
+
// that DCE removes. (SPRINT v0.11 C6)
|
|
1509
2310
|
if (state.needsDelegation && state.delegatedEvents && state.delegatedEvents.size > 0) {
|
|
1510
2311
|
const eventArray = t.arrayExpression(
|
|
1511
2312
|
[...state.delegatedEvents].map(e => t.stringLiteral(e))
|
|
1512
2313
|
);
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
2314
|
+
// function _$delegate$() { if (_$delegated$) return; _$delegated$ = true; _$delegateEvents([...]); }
|
|
2315
|
+
const helperFn = t.functionDeclaration(
|
|
2316
|
+
t.identifier('_$delegate$'),
|
|
2317
|
+
[],
|
|
2318
|
+
t.blockStatement([
|
|
2319
|
+
t.ifStatement(
|
|
2320
|
+
t.identifier('_$delegated$'),
|
|
2321
|
+
t.returnStatement()
|
|
2322
|
+
),
|
|
2323
|
+
t.expressionStatement(
|
|
2324
|
+
t.assignmentExpression('=', t.identifier('_$delegated$'), t.booleanLiteral(true))
|
|
2325
|
+
),
|
|
2326
|
+
t.expressionStatement(
|
|
2327
|
+
t.callExpression(t.identifier('_$delegateEvents'), [eventArray])
|
|
2328
|
+
),
|
|
2329
|
+
])
|
|
1517
2330
|
);
|
|
2331
|
+
// Unshift so `let _$delegated$ = false` executes before any
|
|
2332
|
+
// top-level component construction (e.g. a same-module mount()).
|
|
2333
|
+
path.unshiftContainer('body', [
|
|
2334
|
+
t.variableDeclaration('let', [
|
|
2335
|
+
t.variableDeclarator(t.identifier('_$delegated$'), t.booleanLiteral(false))
|
|
2336
|
+
]),
|
|
2337
|
+
helperFn,
|
|
2338
|
+
]);
|
|
1518
2339
|
}
|
|
1519
2340
|
}
|
|
1520
2341
|
},
|
|
1521
2342
|
|
|
1522
2343
|
JSXElement(path, state) {
|
|
1523
|
-
|
|
1524
|
-
state.signalNames = collectSignalNamesFromScope(path);
|
|
1525
|
-
state._pendingSetup = [];
|
|
1526
|
-
const transformed = transformElementFineGrained(path, state);
|
|
1527
|
-
const pending = state._pendingSetup;
|
|
1528
|
-
state._pendingSetup = [];
|
|
1529
|
-
|
|
1530
|
-
if (pending.length > 0) {
|
|
1531
|
-
// Find the enclosing statement to hoist setup before it
|
|
1532
|
-
let stmtPath = path;
|
|
1533
|
-
while (stmtPath && !stmtPath.isStatement()) {
|
|
1534
|
-
stmtPath = stmtPath.parentPath;
|
|
1535
|
-
}
|
|
1536
|
-
if (stmtPath && stmtPath.isStatement()) {
|
|
1537
|
-
// Insert setup statements before the enclosing statement
|
|
1538
|
-
for (const stmt of pending) {
|
|
1539
|
-
stmtPath.insertBefore(stmt);
|
|
1540
|
-
}
|
|
1541
|
-
path.replaceWith(transformed);
|
|
1542
|
-
} else {
|
|
1543
|
-
// Fallback: if we can't find a statement parent, use IIFE
|
|
1544
|
-
pending.push(t.returnStatement(transformed));
|
|
1545
|
-
path.replaceWith(
|
|
1546
|
-
t.callExpression(
|
|
1547
|
-
t.arrowFunctionExpression([], t.blockStatement(pending)),
|
|
1548
|
-
[]
|
|
1549
|
-
)
|
|
1550
|
-
);
|
|
1551
|
-
}
|
|
1552
|
-
} else {
|
|
1553
|
-
path.replaceWith(transformed);
|
|
1554
|
-
}
|
|
2344
|
+
transformJsxRoot(path, state, transformElementFineGrained);
|
|
1555
2345
|
},
|
|
1556
2346
|
|
|
1557
2347
|
JSXFragment(path, state) {
|
|
1558
|
-
|
|
1559
|
-
|
|
2348
|
+
// Fragments share the element driver: their element children push
|
|
2349
|
+
// `const _el$N = ...` setup into _pendingSetup, which MUST be drained
|
|
2350
|
+
// here (hoisted or IIFE-wrapped) or the emitted `_el$N` references
|
|
2351
|
+
// are never declared (ReferenceError at runtime).
|
|
2352
|
+
transformJsxRoot(path, state, transformFragmentFineGrained);
|
|
1560
2353
|
}
|
|
1561
2354
|
}
|
|
1562
2355
|
};
|