what-compiler 0.10.0 → 0.11.1
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.min.js +1 -2
- package/dist/file-router.min.js +0 -1
- package/dist/index.min.js +6 -7
- package/dist/runtime.min.js +0 -1
- package/dist/vite-plugin.min.js +6 -7
- package/package.json +3 -3
- package/src/babel-plugin.js +433 -89
- package/src/vite-plugin.js +75 -5
- package/dist/babel-plugin.js +0 -1576
- package/dist/babel-plugin.js.map +0 -7
- package/dist/babel-plugin.min.js.map +0 -7
- package/dist/file-router.js +0 -271
- package/dist/file-router.js.map +0 -7
- package/dist/file-router.min.js.map +0 -7
- package/dist/index.js +0 -2370
- package/dist/index.js.map +0 -7
- package/dist/index.min.js.map +0 -7
- package/dist/runtime.js +0 -9
- package/dist/runtime.js.map +0 -7
- package/dist/runtime.min.js.map +0 -7
- package/dist/vite-plugin.js +0 -2359
- package/dist/vite-plugin.js.map +0 -7
- package/dist/vite-plugin.min.js.map +0 -7
package/src/babel-plugin.js
CHANGED
|
@@ -773,7 +773,7 @@ export default function whatBabelPlugin({ types: t }) {
|
|
|
773
773
|
];
|
|
774
774
|
|
|
775
775
|
// Apply dynamic attributes and events
|
|
776
|
-
applyDynamicAttrs(statements, elId, attributes, state);
|
|
776
|
+
applyDynamicAttrs(statements, elId, attributes, state, tagName);
|
|
777
777
|
|
|
778
778
|
// Handle dynamic children
|
|
779
779
|
applyDynamicChildren(statements, elId, children, node, state);
|
|
@@ -829,8 +829,51 @@ export default function whatBabelPlugin({ types: t }) {
|
|
|
829
829
|
return t.callExpression(t.identifier('h'), [t.stringLiteral(tagName), propsExpr, ...transformedChildren]);
|
|
830
830
|
}
|
|
831
831
|
|
|
832
|
-
|
|
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.
|
|
833
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
|
+
}
|
|
834
877
|
state.needsSetProp = true;
|
|
835
878
|
return t.callExpression(t.identifier('_$setProp'), [
|
|
836
879
|
t.identifier(elId),
|
|
@@ -839,6 +882,18 @@ export default function whatBabelPlugin({ types: t }) {
|
|
|
839
882
|
]);
|
|
840
883
|
}
|
|
841
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
|
+
|
|
842
897
|
for (const attr of attributes) {
|
|
843
898
|
if (t.isJSXSpreadAttribute(attr)) {
|
|
844
899
|
state.needsSpread = true;
|
|
@@ -887,6 +942,7 @@ export default function whatBabelPlugin({ types: t }) {
|
|
|
887
942
|
state.needsDelegation = true;
|
|
888
943
|
if (!state.delegatedEvents) state.delegatedEvents = new Set();
|
|
889
944
|
state.delegatedEvents.add(event);
|
|
945
|
+
emitDelegateInit();
|
|
890
946
|
statements.push(
|
|
891
947
|
t.expressionStatement(
|
|
892
948
|
t.assignmentExpression('=',
|
|
@@ -1045,6 +1101,99 @@ export default function whatBabelPlugin({ types: t }) {
|
|
|
1045
1101
|
}
|
|
1046
1102
|
}
|
|
1047
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
|
+
|
|
1048
1197
|
function applyDynamicChildren(statements, elId, children, parentNode, state) {
|
|
1049
1198
|
// Two-pass approach: first collect all children needing DOM references,
|
|
1050
1199
|
// then pre-capture markers before any _$insert() calls shift indices.
|
|
@@ -1153,7 +1302,7 @@ export default function whatBabelPlugin({ types: t }) {
|
|
|
1153
1302
|
|
|
1154
1303
|
// Auto-lower .map() to mapArray when the callback returns keyed JSX.
|
|
1155
1304
|
// Pattern: source().map(item => <Comp key={...} />) or source().map((item, i) => ...)
|
|
1156
|
-
|
|
1305
|
+
let mapResult = tryLowerMapToMapArray(expr, state);
|
|
1157
1306
|
if (mapResult) {
|
|
1158
1307
|
state.needsMapArray = true;
|
|
1159
1308
|
// A bare _$mapArray(...) call is a self-managing inserter (it tracks
|
|
@@ -1166,6 +1315,15 @@ export default function whatBabelPlugin({ types: t }) {
|
|
|
1166
1315
|
const isBareMapArray = t.isCallExpression(mapResult) && t.isIdentifier(mapResult.callee) &&
|
|
1167
1316
|
(mapResult.callee.name === '_$mapArray' || mapResult.callee.name === 'mapArray');
|
|
1168
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
|
+
}
|
|
1169
1327
|
const insertArg = (isBareMapArray || isArrowAlready)
|
|
1170
1328
|
? mapResult
|
|
1171
1329
|
: t.arrowFunctionExpression([], mapResult);
|
|
@@ -1200,6 +1358,9 @@ export default function whatBabelPlugin({ types: t }) {
|
|
|
1200
1358
|
}
|
|
1201
1359
|
|
|
1202
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);
|
|
1203
1364
|
const insertCall = t.callExpression(t.identifier('_$insert'), [
|
|
1204
1365
|
t.identifier(elId),
|
|
1205
1366
|
t.arrowFunctionExpression([], expr),
|
|
@@ -1258,7 +1419,7 @@ export default function whatBabelPlugin({ types: t }) {
|
|
|
1258
1419
|
])
|
|
1259
1420
|
);
|
|
1260
1421
|
}
|
|
1261
|
-
applyDynamicAttrs(statements, childElRef, entry.child.openingElement.attributes, state);
|
|
1422
|
+
applyDynamicAttrs(statements, childElRef, entry.child.openingElement.attributes, state, entry.child.openingElement.name.name);
|
|
1262
1423
|
applyDynamicChildren(statements, childElRef, entry.child.children, entry.child, state);
|
|
1263
1424
|
continue;
|
|
1264
1425
|
}
|
|
@@ -1267,8 +1428,9 @@ export default function whatBabelPlugin({ types: t }) {
|
|
|
1267
1428
|
for (const fChild of entry.child.children) {
|
|
1268
1429
|
if (t.isJSXExpressionContainer(fChild) && !t.isJSXEmptyExpression(fChild.expression)) {
|
|
1269
1430
|
state.needsInsert = true;
|
|
1270
|
-
|
|
1431
|
+
let expr = fChild.expression;
|
|
1271
1432
|
if (isPotentiallyReactive(expr, state.signalNames, state.importedIdentifiers)) {
|
|
1433
|
+
expr = memoizeBranchCondition(expr, statements, state); // (C1)
|
|
1272
1434
|
statements.push(
|
|
1273
1435
|
t.expressionStatement(
|
|
1274
1436
|
t.callExpression(t.identifier('_$insert'), [
|
|
@@ -1654,11 +1816,41 @@ export default function whatBabelPlugin({ types: t }) {
|
|
|
1654
1816
|
? path.scope.generateUidIdentifier('v')
|
|
1655
1817
|
: t.identifier('_v');
|
|
1656
1818
|
|
|
1657
|
-
const
|
|
1819
|
+
const contentIsFn = t.isFunction(contentExpr);
|
|
1820
|
+
const consequent = contentIsFn
|
|
1658
1821
|
? t.callExpression(contentExpr, [t.cloneNode(vId)])
|
|
1659
1822
|
: contentExpr;
|
|
1660
1823
|
const alternate = fallbackExpr || t.nullLiteral();
|
|
1661
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
|
+
|
|
1662
1854
|
return t.arrowFunctionExpression([], t.blockStatement([
|
|
1663
1855
|
t.variableDeclaration('const', [
|
|
1664
1856
|
t.variableDeclarator(vId, condition)
|
|
@@ -1669,6 +1861,65 @@ export default function whatBabelPlugin({ types: t }) {
|
|
|
1669
1861
|
]));
|
|
1670
1862
|
}
|
|
1671
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;
|
|
1921
|
+
}
|
|
1922
|
+
|
|
1672
1923
|
function transformFragmentFineGrained(path, state) {
|
|
1673
1924
|
const { node } = path;
|
|
1674
1925
|
const children = node.children;
|
|
@@ -1680,7 +1931,7 @@ export default function whatBabelPlugin({ types: t }) {
|
|
|
1680
1931
|
if (text) transformed.push(t.stringLiteral(text));
|
|
1681
1932
|
} else if (t.isJSXExpressionContainer(child)) {
|
|
1682
1933
|
if (!t.isJSXEmptyExpression(child.expression)) {
|
|
1683
|
-
transformed.push(child.expression);
|
|
1934
|
+
transformed.push(lowerFragmentExprChild(child.expression, state));
|
|
1684
1935
|
}
|
|
1685
1936
|
} else if (t.isJSXElement(child)) {
|
|
1686
1937
|
transformed.push(transformElementFineGrained({ node: child }, state));
|
|
@@ -1704,6 +1955,93 @@ export default function whatBabelPlugin({ types: t }) {
|
|
|
1704
1955
|
return id;
|
|
1705
1956
|
}
|
|
1706
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
|
+
|
|
1707
2045
|
// =====================================================
|
|
1708
2046
|
// Plugin entry
|
|
1709
2047
|
// =====================================================
|
|
@@ -1721,6 +2059,12 @@ export default function whatBabelPlugin({ types: t }) {
|
|
|
1721
2059
|
state.needsMapArray = false;
|
|
1722
2060
|
state.needsSpread = false;
|
|
1723
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;
|
|
1724
2068
|
state.needsH = false;
|
|
1725
2069
|
state.needsCreateComponent = false;
|
|
1726
2070
|
state.needsFragment = false;
|
|
@@ -1731,8 +2075,10 @@ export default function whatBabelPlugin({ types: t }) {
|
|
|
1731
2075
|
state.templateMap = new Map(); // html → template id (deduplication)
|
|
1732
2076
|
state.templateCount = 0;
|
|
1733
2077
|
state._varCounter = 0;
|
|
2078
|
+
state._memoCounter = 0;
|
|
1734
2079
|
state._pendingSetup = [];
|
|
1735
2080
|
state.nextVarId = () => `_el$${state._varCounter++}`;
|
|
2081
|
+
state.nextMemoId = () => `_c$${state._memoCounter++}`;
|
|
1736
2082
|
|
|
1737
2083
|
// Collect signal names for smart reactivity detection
|
|
1738
2084
|
state.signalNames = new Set();
|
|
@@ -1811,12 +2157,14 @@ export default function whatBabelPlugin({ types: t }) {
|
|
|
1811
2157
|
exit(path, state) {
|
|
1812
2158
|
// Insert template declarations at top of program (hoisted to module scope)
|
|
1813
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__ ');
|
|
1814
2165
|
path.unshiftContainer('body',
|
|
1815
2166
|
t.variableDeclaration('const', [
|
|
1816
|
-
t.variableDeclarator(
|
|
1817
|
-
t.identifier(tmpl.id),
|
|
1818
|
-
t.callExpression(t.identifier('_$template'), [t.stringLiteral(tmpl.html)])
|
|
1819
|
-
)
|
|
2167
|
+
t.variableDeclarator(t.identifier(tmpl.id), tmplCall)
|
|
1820
2168
|
])
|
|
1821
2169
|
);
|
|
1822
2170
|
}
|
|
@@ -1824,8 +2172,13 @@ export default function whatBabelPlugin({ types: t }) {
|
|
|
1824
2172
|
// Build fine-grained imports
|
|
1825
2173
|
const fgSpecifiers = [];
|
|
1826
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)
|
|
1827
2180
|
fgSpecifiers.push(
|
|
1828
|
-
t.importSpecifier(t.identifier('_$template'), t.identifier('template'))
|
|
2181
|
+
t.importSpecifier(t.identifier('_$template'), t.identifier('_$template'))
|
|
1829
2182
|
);
|
|
1830
2183
|
}
|
|
1831
2184
|
if (state.needsInsert) {
|
|
@@ -1853,6 +2206,36 @@ export default function whatBabelPlugin({ types: t }) {
|
|
|
1853
2206
|
t.importSpecifier(t.identifier('_$setProp'), t.identifier('setProp'))
|
|
1854
2207
|
);
|
|
1855
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
|
+
}
|
|
1856
2239
|
if (state.needsCreateComponent) {
|
|
1857
2240
|
fgSpecifiers.push(
|
|
1858
2241
|
t.importSpecifier(t.identifier('_$createComponent'), t.identifier('_$createComponent'))
|
|
@@ -1916,96 +2299,57 @@ export default function whatBabelPlugin({ types: t }) {
|
|
|
1916
2299
|
addCoreImports(path, t, coreSpecifiers);
|
|
1917
2300
|
}
|
|
1918
2301
|
|
|
1919
|
-
// 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)
|
|
1920
2310
|
if (state.needsDelegation && state.delegatedEvents && state.delegatedEvents.size > 0) {
|
|
1921
2311
|
const eventArray = t.arrayExpression(
|
|
1922
2312
|
[...state.delegatedEvents].map(e => t.stringLiteral(e))
|
|
1923
2313
|
);
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
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
|
+
])
|
|
1928
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
|
+
]);
|
|
1929
2339
|
}
|
|
1930
2340
|
}
|
|
1931
2341
|
},
|
|
1932
2342
|
|
|
1933
2343
|
JSXElement(path, state) {
|
|
1934
|
-
|
|
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;
|
|
1948
|
-
state._pendingSetup = [];
|
|
1949
|
-
const transformed = transformElementFineGrained(path, state);
|
|
1950
|
-
const pending = state._pendingSetup;
|
|
1951
|
-
state._pendingSetup = [];
|
|
1952
|
-
|
|
1953
|
-
if (pending.length > 0) {
|
|
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.
|
|
1958
|
-
let stmtPath = path;
|
|
1959
|
-
let crossedFunctionBoundary = false;
|
|
1960
|
-
while (stmtPath && !stmtPath.isStatement()) {
|
|
1961
|
-
if (stmtPath.isArrowFunctionExpression() || stmtPath.isFunctionExpression()) {
|
|
1962
|
-
crossedFunctionBoundary = true;
|
|
1963
|
-
}
|
|
1964
|
-
stmtPath = stmtPath.parentPath;
|
|
1965
|
-
}
|
|
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);
|
|
1989
|
-
path.replaceWith(transformed);
|
|
1990
|
-
} else {
|
|
1991
|
-
// Crossed a function boundary or no enclosing statement found —
|
|
1992
|
-
// fall back to IIFE so closure variables remain in scope.
|
|
1993
|
-
pending.push(t.returnStatement(transformed));
|
|
1994
|
-
path.replaceWith(
|
|
1995
|
-
t.callExpression(
|
|
1996
|
-
t.arrowFunctionExpression([], t.blockStatement(pending)),
|
|
1997
|
-
[]
|
|
1998
|
-
)
|
|
1999
|
-
);
|
|
2000
|
-
}
|
|
2001
|
-
} else {
|
|
2002
|
-
path.replaceWith(transformed);
|
|
2003
|
-
}
|
|
2344
|
+
transformJsxRoot(path, state, transformElementFineGrained);
|
|
2004
2345
|
},
|
|
2005
2346
|
|
|
2006
2347
|
JSXFragment(path, state) {
|
|
2007
|
-
|
|
2008
|
-
|
|
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);
|
|
2009
2353
|
}
|
|
2010
2354
|
}
|
|
2011
2355
|
};
|