what-compiler 0.13.4 → 0.13.6
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 -1
- package/dist/index.min.js +21 -15
- package/dist/vite-plugin.min.js +21 -15
- package/package.json +2 -2
- package/src/babel-plugin.js +376 -147
- package/src/vite-plugin.js +111 -10
package/src/babel-plugin.js
CHANGED
|
@@ -41,6 +41,20 @@ const SAFE_GLOBAL_CALLS = new Set([
|
|
|
41
41
|
'console', 'RegExp',
|
|
42
42
|
]);
|
|
43
43
|
|
|
44
|
+
// Built-in prototype methods that convert or copy a value. A zero-argument call
|
|
45
|
+
// is otherwise assumed to be a signal read (see isPotentiallyReactive); these
|
|
46
|
+
// names are the ones JavaScript itself defines, so they never are, and they are
|
|
47
|
+
// common enough in JSX that wrapping them would be pure cost.
|
|
48
|
+
const PURE_BUILTIN_METHODS = new Set([
|
|
49
|
+
'toString', 'toLocaleString', 'valueOf', 'toJSON',
|
|
50
|
+
'toUpperCase', 'toLowerCase', 'toLocaleUpperCase', 'toLocaleLowerCase',
|
|
51
|
+
'trim', 'trimStart', 'trimEnd',
|
|
52
|
+
'toFixed', 'toPrecision', 'toExponential',
|
|
53
|
+
'toISOString', 'toUTCString', 'toDateString', 'toTimeString',
|
|
54
|
+
'toLocaleDateString', 'toLocaleTimeString',
|
|
55
|
+
'slice', 'reverse', 'sort', 'flat', 'concat', 'join',
|
|
56
|
+
]);
|
|
57
|
+
|
|
44
58
|
// Known signal-creating functions
|
|
45
59
|
const SIGNAL_CREATORS = new Set([
|
|
46
60
|
'useSignal', 'signal', 'computed', 'useComputed', 'useState', 'useReducer',
|
|
@@ -57,6 +71,17 @@ const SERVER_ACTION_SOURCES = new Set([
|
|
|
57
71
|
'what-server/actions',
|
|
58
72
|
]);
|
|
59
73
|
|
|
74
|
+
// A bare specifier that resolves to this framework, entry point included:
|
|
75
|
+
// 'what-framework', 'what-framework/render', 'what-core', 'what-core/render'.
|
|
76
|
+
// Used to decide whether an imported binding is provably ours before the plugin
|
|
77
|
+
// changes how a call to it is compiled.
|
|
78
|
+
function isWhatModuleSource(source) {
|
|
79
|
+
return source === 'what-framework'
|
|
80
|
+
|| source.startsWith('what-framework/')
|
|
81
|
+
|| source === 'what-core'
|
|
82
|
+
|| source.startsWith('what-core/');
|
|
83
|
+
}
|
|
84
|
+
|
|
60
85
|
function stableActionHash(value) {
|
|
61
86
|
let hash = 0xcbf29ce484222325n;
|
|
62
87
|
for (let i = 0; i < value.length; i++) {
|
|
@@ -242,6 +267,67 @@ export default function whatBabelPlugin({ types: t }) {
|
|
|
242
267
|
t.isJSXElement(child) && t.isJSXIdentifier(child.openingElement.name, { name: 'Match' }));
|
|
243
268
|
}
|
|
244
269
|
|
|
270
|
+
// --- The JSX handed straight to hydrate() ---
|
|
271
|
+
//
|
|
272
|
+
// hydrate() can only adopt the server's markup from an UNBUILT tree: a node
|
|
273
|
+
// that has already been built has its bindings wired to itself, so hydrateNode
|
|
274
|
+
// inserts it and lets the trim delete everything the server sent. The
|
|
275
|
+
// documented client entry, `hydrate(<App />, el)`, lowered to
|
|
276
|
+
// `_$createComponent(App, ...)`, which runs App and builds its DOM before
|
|
277
|
+
// hydrate() is even called — so every compiled app that hydrated threw its
|
|
278
|
+
// server render away and re-rendered from scratch, silently in production
|
|
279
|
+
// where the warning is stripped.
|
|
280
|
+
//
|
|
281
|
+
// Only the first argument is the tree; the second is the container. And only a
|
|
282
|
+
// binding that provably resolves to OUR hydrate qualifies, because rewriting a
|
|
283
|
+
// call to somebody else's `hydrate` would change what their code means.
|
|
284
|
+
function isHydrateRootArgument(path) {
|
|
285
|
+
// transformElementFineGrained is also called with a bare `{ node }` stand-in
|
|
286
|
+
// when it recurses into children, and a child is never the hydrate root.
|
|
287
|
+
const parent = path.parentPath;
|
|
288
|
+
if (!parent || typeof parent.isCallExpression !== 'function' || !parent.isCallExpression()) {
|
|
289
|
+
return false;
|
|
290
|
+
}
|
|
291
|
+
if (parent.node.arguments[0] !== path.node) return false;
|
|
292
|
+
return isWhatHydrateCallee(parent.node.callee, parent.scope);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// Resolve the callee through Babel's scope rather than by name, so a local
|
|
296
|
+
// `function hydrate()` or `const hydrate = ...` that shadows the import is left
|
|
297
|
+
// alone. getBinding returns the NEAREST binding, which is exactly the shadowing
|
|
298
|
+
// rule the language itself applies. An unresolved (global) `hydrate` is not
|
|
299
|
+
// ours either, so it stays untouched too.
|
|
300
|
+
function isWhatHydrateCallee(callee, scope) {
|
|
301
|
+
// `ns.hydrate(...)` after `import * as ns from 'what-framework'`.
|
|
302
|
+
if (t.isMemberExpression(callee) && !callee.computed
|
|
303
|
+
&& t.isIdentifier(callee.property, { name: 'hydrate' })
|
|
304
|
+
&& t.isIdentifier(callee.object)) {
|
|
305
|
+
return importedFrom(scope.getBinding(callee.object.name), t.isImportNamespaceSpecifier, null);
|
|
306
|
+
}
|
|
307
|
+
if (!t.isIdentifier(callee)) return false;
|
|
308
|
+
// `import { hydrate }` and `import { hydrate as boot }` alike: the binding
|
|
309
|
+
// remembers the name that was IMPORTED, which is the one that matters.
|
|
310
|
+
return importedFrom(scope.getBinding(callee.name), t.isImportSpecifier, 'hydrate');
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// A binding is ours when it came from an import declaration of a What module
|
|
314
|
+
// through the expected specifier kind, optionally carrying a required imported
|
|
315
|
+
// name (`hydrate` for a named import, none for a namespace).
|
|
316
|
+
function importedFrom(binding, isSpecifier, importedName) {
|
|
317
|
+
if (!binding || binding.kind !== 'module' || !binding.path) return false;
|
|
318
|
+
const spec = binding.path.node;
|
|
319
|
+
if (!isSpecifier(spec)) return false;
|
|
320
|
+
if (importedName !== null) {
|
|
321
|
+
const imported = spec.imported;
|
|
322
|
+
const name = t.isIdentifier(imported) ? imported.name
|
|
323
|
+
: t.isStringLiteral(imported) ? imported.value
|
|
324
|
+
: null;
|
|
325
|
+
if (name !== importedName) return false;
|
|
326
|
+
}
|
|
327
|
+
const decl = binding.path.parent;
|
|
328
|
+
return t.isImportDeclaration(decl) && isWhatModuleSource(decl.source.value);
|
|
329
|
+
}
|
|
330
|
+
|
|
245
331
|
// JSXMemberExpression -> MemberExpression callee for _$createComponent.
|
|
246
332
|
function componentCallee(name) {
|
|
247
333
|
if (t.isJSXMemberExpression(name)) {
|
|
@@ -331,6 +417,27 @@ export default function whatBabelPlugin({ types: t }) {
|
|
|
331
417
|
return attrName;
|
|
332
418
|
}
|
|
333
419
|
|
|
420
|
+
// Is this attribute overwritten by a later one naming the same DOM property?
|
|
421
|
+
//
|
|
422
|
+
// `<div id={a} id="b"/>` is legal JSX and means id="b": the last writer of a
|
|
423
|
+
// name wins, exactly as it does in the object literal the h() spelling
|
|
424
|
+
// builds. The template could not express that, because a repeated attribute
|
|
425
|
+
// in an HTML string keeps the FIRST value and a dynamic attribute is applied
|
|
426
|
+
// after the template regardless of where it was written. Dropping the
|
|
427
|
+
// shadowed one leaves a single writer per name, so written order decides.
|
|
428
|
+
//
|
|
429
|
+
// A later SPREAD never shadows: it overrides only the names it actually
|
|
430
|
+
// carries, which is not knowable here and is handled by ordering instead.
|
|
431
|
+
function isShadowedAttr(attributes, index) {
|
|
432
|
+
const name = normalizeAttrName(getAttrName(attributes[index]));
|
|
433
|
+
for (let i = index + 1; i < attributes.length; i += 1) {
|
|
434
|
+
const later = attributes[i];
|
|
435
|
+
if (t.isJSXSpreadAttribute(later)) continue;
|
|
436
|
+
if (normalizeAttrName(getAttrName(later)) === name) return true;
|
|
437
|
+
}
|
|
438
|
+
return false;
|
|
439
|
+
}
|
|
440
|
+
|
|
334
441
|
// Safely extract attribute name, handling JSXNamespacedName (e.g., client:idle, bind:value)
|
|
335
442
|
function getAttrName(attr) {
|
|
336
443
|
if (t.isJSXNamespacedName(attr.name)) {
|
|
@@ -339,6 +446,20 @@ export default function whatBabelPlugin({ types: t }) {
|
|
|
339
446
|
return typeof attr.name.name === 'string' ? attr.name.name : String(attr.name.name);
|
|
340
447
|
}
|
|
341
448
|
|
|
449
|
+
// The key half of a prop in an emitted object literal. A JSX attribute name is
|
|
450
|
+
// frequently NOT a valid JavaScript identifier — `data-x`, `aria-label` and
|
|
451
|
+
// every namespaced name contain characters an identifier cannot — so anything
|
|
452
|
+
// that does not match has to be quoted.
|
|
453
|
+
//
|
|
454
|
+
// Shared rather than inlined because babel's builders do not validate: handing
|
|
455
|
+
// t.identifier() a name with a dash produces `{data-x: "1"}` in the output and
|
|
456
|
+
// no error anywhere, so the mistake surfaces as a parse failure in generated
|
|
457
|
+
// code. Two call sites had their own copy of this test and one of them was
|
|
458
|
+
// missing it entirely.
|
|
459
|
+
function propKey(name) {
|
|
460
|
+
return /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(name) ? t.identifier(name) : t.stringLiteral(name);
|
|
461
|
+
}
|
|
462
|
+
|
|
342
463
|
// Handler-shaped attribute name, case-insensitive. Mirrors _isEventProp in
|
|
343
464
|
// what-core's dom.js.
|
|
344
465
|
function isEventAttrName(name) {
|
|
@@ -520,6 +641,16 @@ export default function whatBabelPlugin({ types: t }) {
|
|
|
520
641
|
// backward compat (used in collectSignalNames calls)". Nothing called it;
|
|
521
642
|
// every call site uses collectSignalNamesFromScope directly.
|
|
522
643
|
|
|
644
|
+
// `x.toUpperCase()` and friends, listed in PURE_BUILTIN_METHODS. Only the method
|
|
645
|
+
// name is checked: whether the RECEIVER is reactive is decided separately, so
|
|
646
|
+
// `user().name.trim()` stays reactive through its object.
|
|
647
|
+
function isPureBuiltinMethodCall(expr) {
|
|
648
|
+
return t.isMemberExpression(expr.callee)
|
|
649
|
+
&& !expr.callee.computed
|
|
650
|
+
&& t.isIdentifier(expr.callee.property)
|
|
651
|
+
&& PURE_BUILTIN_METHODS.has(expr.callee.property.name);
|
|
652
|
+
}
|
|
653
|
+
|
|
523
654
|
// Check if a call expression is a safe (non-reactive) global call
|
|
524
655
|
function isSafeGlobalCall(expr) {
|
|
525
656
|
if (!t.isCallExpression(expr)) return false;
|
|
@@ -599,6 +730,17 @@ export default function whatBabelPlugin({ types: t }) {
|
|
|
599
730
|
if (isSafeGlobalCall(expr)) {
|
|
600
731
|
return expr.arguments.some(arg => isPotentiallyReactive(arg, signalNames, importedIds));
|
|
601
732
|
}
|
|
733
|
+
// Any other zero-argument call has the shape of an accessor read, so it
|
|
734
|
+
// is assumed to be one. `props.count()`, a plain parameter `count()` and
|
|
735
|
+
// `s()` from `const s = useThing()` are all opaque to the compiler: it
|
|
736
|
+
// knows only the names it watched being created. Assuming the narrow way
|
|
737
|
+
// round meant those three froze silently while the docs promised
|
|
738
|
+
// `{count()}` was reactive. Assuming this way round costs, when the call
|
|
739
|
+
// turns out to be constant, one effect that runs once and never again.
|
|
740
|
+
if (expr.arguments.length === 0 && !isPureBuiltinMethodCall(expr)) {
|
|
741
|
+
return true;
|
|
742
|
+
}
|
|
743
|
+
|
|
602
744
|
// Unknown call — check if callee or args contain signal reads
|
|
603
745
|
if (t.isIdentifier(expr.callee)) {
|
|
604
746
|
// Could be a function that reads signals internally
|
|
@@ -819,6 +961,7 @@ export default function whatBabelPlugin({ types: t }) {
|
|
|
819
961
|
if (!t.isArrowFunctionExpression(mapFn) && !t.isFunctionExpression(mapFn)) return null;
|
|
820
962
|
|
|
821
963
|
// Get the map callback's return expression
|
|
964
|
+
/** @type {any} */
|
|
822
965
|
let returnExpr = null;
|
|
823
966
|
if (t.isArrowFunctionExpression(mapFn)) {
|
|
824
967
|
if (t.isExpression(mapFn.body)) {
|
|
@@ -837,6 +980,7 @@ export default function whatBabelPlugin({ types: t }) {
|
|
|
837
980
|
// Check if the return is JSX with a `key` prop
|
|
838
981
|
if (!t.isJSXElement(returnExpr)) return null;
|
|
839
982
|
const attrs = returnExpr.openingElement.attributes;
|
|
983
|
+
/** @type {any} */
|
|
840
984
|
let keyAttr = null;
|
|
841
985
|
for (const attr of attrs) {
|
|
842
986
|
if (t.isJSXAttribute(attr) && getAttrName(attr) === 'key') {
|
|
@@ -983,6 +1127,37 @@ export default function whatBabelPlugin({ types: t }) {
|
|
|
983
1127
|
return t.isJSXExpressionContainer(attr.value);
|
|
984
1128
|
}
|
|
985
1129
|
|
|
1130
|
+
// The child slots a fragment contributes to its parent, flattened.
|
|
1131
|
+
//
|
|
1132
|
+
// A fragment renders no node of its own, so each of its children takes a
|
|
1133
|
+
// sibling position in the parent exactly as if the fragment were not written.
|
|
1134
|
+
// extractStaticHTML() and applyDynamicChildren() both read this list, so the
|
|
1135
|
+
// markers baked into the template and the inserts emitted against them can
|
|
1136
|
+
// never disagree about how many positions the fragment occupies.
|
|
1137
|
+
//
|
|
1138
|
+
// Nested fragments flatten too: `<><>a</>b</>` is two slots in the parent.
|
|
1139
|
+
// Whitespace-only text and `{/* comment */}` contribute nothing, matching how
|
|
1140
|
+
// the same children are treated directly under an element.
|
|
1141
|
+
function fragmentSlots(fragmentNode, out = []) {
|
|
1142
|
+
for (const child of fragmentNode.children) {
|
|
1143
|
+
if (t.isJSXText(child)) {
|
|
1144
|
+
const text = normalizeJsxText(child.value);
|
|
1145
|
+
if (text) out.push({ kind: 'text', text });
|
|
1146
|
+
continue;
|
|
1147
|
+
}
|
|
1148
|
+
if (t.isJSXFragment(child)) {
|
|
1149
|
+
fragmentSlots(child, out);
|
|
1150
|
+
continue;
|
|
1151
|
+
}
|
|
1152
|
+
if (t.isJSXExpressionContainer(child)) {
|
|
1153
|
+
if (!t.isJSXEmptyExpression(child.expression)) out.push({ kind: 'expression', child });
|
|
1154
|
+
continue;
|
|
1155
|
+
}
|
|
1156
|
+
if (t.isJSXElement(child)) out.push({ kind: 'element', child });
|
|
1157
|
+
}
|
|
1158
|
+
return out;
|
|
1159
|
+
}
|
|
1160
|
+
|
|
986
1161
|
// Extract static HTML from JSX element for template()
|
|
987
1162
|
function extractStaticHTML(node) {
|
|
988
1163
|
if (t.isJSXText(node)) {
|
|
@@ -1004,8 +1179,19 @@ export default function whatBabelPlugin({ types: t }) {
|
|
|
1004
1179
|
|
|
1005
1180
|
let html = `<${tagName}`;
|
|
1006
1181
|
|
|
1007
|
-
|
|
1182
|
+
// A static attribute written AFTER a spread cannot be baked into the
|
|
1183
|
+
// template. The template is applied first and the spread runs against the
|
|
1184
|
+
// finished element, so baking would let the spread overwrite an attribute
|
|
1185
|
+
// the author pinned after it: `<div {...{id:'a'}} id="b"/>` rendered id="a"
|
|
1186
|
+
// while the h() spelling of the same tree renders id="b". Everything from
|
|
1187
|
+
// the first spread onward is emitted as an ordered runtime set instead, in
|
|
1188
|
+
// applyDynamicAttrs.
|
|
1189
|
+
const firstSpread = el.attributes.findIndex(a => t.isJSXSpreadAttribute(a));
|
|
1190
|
+
|
|
1191
|
+
for (const [attrIndex, attr] of el.attributes.entries()) {
|
|
1008
1192
|
if (t.isJSXSpreadAttribute(attr)) continue;
|
|
1193
|
+
if (firstSpread !== -1 && attrIndex > firstSpread) continue;
|
|
1194
|
+
if (isShadowedAttr(el.attributes, attrIndex)) continue;
|
|
1009
1195
|
const name = getAttrName(attr);
|
|
1010
1196
|
if (name === 'key') continue;
|
|
1011
1197
|
// Case-insensitive: a static template is applied via innerHTML, so an
|
|
@@ -1055,11 +1241,14 @@ export default function whatBabelPlugin({ types: t }) {
|
|
|
1055
1241
|
html += extractStaticHTML(child);
|
|
1056
1242
|
}
|
|
1057
1243
|
} else if (t.isJSXFragment(child)) {
|
|
1058
|
-
// One marker
|
|
1059
|
-
//
|
|
1060
|
-
//
|
|
1061
|
-
//
|
|
1062
|
-
|
|
1244
|
+
// One marker PER fragment slot, not one for the whole fragment. A
|
|
1245
|
+
// fragment is not a DOM boundary, so its children are siblings of the
|
|
1246
|
+
// fragment's siblings and each needs its own anchor to keep its place.
|
|
1247
|
+
// With a single shared anchor the order held at mount and then broke on
|
|
1248
|
+
// the first re-run: reconcileInsert re-places a reactive region before
|
|
1249
|
+
// that anchor, which puts it after every sibling that had been inserted
|
|
1250
|
+
// before the same anchor earlier.
|
|
1251
|
+
html += '<!--$-->'.repeat(fragmentSlots(child).length);
|
|
1063
1252
|
}
|
|
1064
1253
|
}
|
|
1065
1254
|
|
|
@@ -1274,8 +1463,15 @@ export default function whatBabelPlugin({ types: t }) {
|
|
|
1274
1463
|
);
|
|
1275
1464
|
}
|
|
1276
1465
|
|
|
1277
|
-
|
|
1466
|
+
// Static attributes before the first spread are already in the template.
|
|
1467
|
+
// Ones after it are not, because the template is applied before _$spread
|
|
1468
|
+
// runs and would lose to it; they are set here instead, in source order, so
|
|
1469
|
+
// the last write for a key wins exactly as it does in the h() call.
|
|
1470
|
+
let seenSpread = false;
|
|
1471
|
+
|
|
1472
|
+
for (const [attrIndex, attr] of attributes.entries()) {
|
|
1278
1473
|
if (t.isJSXSpreadAttribute(attr)) {
|
|
1474
|
+
seenSpread = true;
|
|
1279
1475
|
state.needsSpread = true;
|
|
1280
1476
|
statements.push(
|
|
1281
1477
|
t.expressionStatement(
|
|
@@ -1285,6 +1481,10 @@ export default function whatBabelPlugin({ types: t }) {
|
|
|
1285
1481
|
continue;
|
|
1286
1482
|
}
|
|
1287
1483
|
|
|
1484
|
+
// A later attribute of the same name is the only writer that matters.
|
|
1485
|
+
// Emitting this one too would apply it after the template and let it win.
|
|
1486
|
+
if (isShadowedAttr(attributes, attrIndex)) continue;
|
|
1487
|
+
|
|
1288
1488
|
const attrName = getAttrName(attr);
|
|
1289
1489
|
|
|
1290
1490
|
// Strip key prop — WhatFW has no virtual DOM, so key is meaningless (issue #6)
|
|
@@ -1484,6 +1684,19 @@ export default function whatBabelPlugin({ types: t }) {
|
|
|
1484
1684
|
// Static expression (no signal calls) — set once
|
|
1485
1685
|
statements.push(t.expressionStatement(buildSetPropCall(domName, expr)));
|
|
1486
1686
|
}
|
|
1687
|
+
continue;
|
|
1688
|
+
}
|
|
1689
|
+
|
|
1690
|
+
// A literal attribute that follows a spread. Everything before the first
|
|
1691
|
+
// spread is in the template; this one is not, so re-apply it here to
|
|
1692
|
+
// restore the order the author wrote. A valueless attribute is `true` in
|
|
1693
|
+
// JSX, which is what the h() spelling passes.
|
|
1694
|
+
if (seenSpread) {
|
|
1695
|
+
statements.push(
|
|
1696
|
+
t.expressionStatement(
|
|
1697
|
+
buildSetPropCall(normalizeAttrName(attrName), getAttributeValue(attr.value))
|
|
1698
|
+
)
|
|
1699
|
+
);
|
|
1487
1700
|
}
|
|
1488
1701
|
}
|
|
1489
1702
|
}
|
|
@@ -1646,8 +1859,24 @@ export default function whatBabelPlugin({ types: t }) {
|
|
|
1646
1859
|
}
|
|
1647
1860
|
|
|
1648
1861
|
if (t.isJSXFragment(child)) {
|
|
1649
|
-
|
|
1650
|
-
|
|
1862
|
+
// Flattened: every slot the fragment contributes gets its own marker and
|
|
1863
|
+
// its own entry, so it is handled exactly like a child written directly
|
|
1864
|
+
// under this element. That is what keeps document order stable across
|
|
1865
|
+
// re-runs, and it also means fragment children now get the treatments
|
|
1866
|
+
// the old single-anchor branch reimplemented without: keyed .map()
|
|
1867
|
+
// lowering, branch memoization and per-element dynamic attributes.
|
|
1868
|
+
for (const slot of fragmentSlots(child)) {
|
|
1869
|
+
if (slot.kind === 'text') {
|
|
1870
|
+
entries.push({ type: 'fragmentText', text: slot.text, childIndex });
|
|
1871
|
+
} else if (slot.kind === 'expression') {
|
|
1872
|
+
entries.push({ type: 'expression', child: slot.child, childIndex });
|
|
1873
|
+
} else {
|
|
1874
|
+
// The template holds only a marker here, so the element is built at
|
|
1875
|
+
// runtime and inserted before it, which is the 'component' slot shape.
|
|
1876
|
+
entries.push({ type: 'component', child: slot.child, childIndex });
|
|
1877
|
+
}
|
|
1878
|
+
childIndex++;
|
|
1879
|
+
}
|
|
1651
1880
|
}
|
|
1652
1881
|
}
|
|
1653
1882
|
|
|
@@ -1655,7 +1884,7 @@ export default function whatBabelPlugin({ types: t }) {
|
|
|
1655
1884
|
// When there are multiple entries needing DOM refs and at least one _$insert(),
|
|
1656
1885
|
// capture all markers upfront to avoid index shifting after DOM mutations.
|
|
1657
1886
|
const entriesNeedingRef = entries.filter(e =>
|
|
1658
|
-
e.type === 'expression' || e.type === 'component' || e.type === '
|
|
1887
|
+
e.type === 'expression' || e.type === 'component' || e.type === 'fragmentText' ||
|
|
1659
1888
|
(e.type === 'static' && e.hasAnythingDynamic)
|
|
1660
1889
|
);
|
|
1661
1890
|
// Pre-capture whenever 2+ children need a DOM ref. Beyond preventing index
|
|
@@ -1837,96 +2066,18 @@ export default function whatBabelPlugin({ types: t }) {
|
|
|
1837
2066
|
continue;
|
|
1838
2067
|
}
|
|
1839
2068
|
|
|
1840
|
-
if (entry.type === '
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
// Every child now inserts before the fragment's own marker. Repeated
|
|
1849
|
-
// insertBefore against one anchor appends in call order, so the
|
|
1850
|
-
// fragment's children keep their order and the fragment keeps its place
|
|
1851
|
-
// among its siblings.
|
|
1852
|
-
//
|
|
1853
|
-
// The marker is captured into a variable first. Each insert mutates the
|
|
1854
|
-
// parent, so an inline `el.firstChild.nextSibling…` walk would resolve
|
|
1855
|
-
// against a tree that the previous insert already shifted.
|
|
1856
|
-
const idx = /** @type {number} */ (entry.childIndex);
|
|
1857
|
-
let anchor = getMarker(idx);
|
|
1858
|
-
if (!t.isIdentifier(anchor)) {
|
|
1859
|
-
const anchorVar = state.nextVarId();
|
|
1860
|
-
statements.push(
|
|
1861
|
-
t.variableDeclaration('const', [
|
|
1862
|
-
t.variableDeclarator(t.identifier(anchorVar), anchor)
|
|
2069
|
+
if (entry.type === 'fragmentText') {
|
|
2070
|
+
state.needsInsert = true;
|
|
2071
|
+
statements.push(
|
|
2072
|
+
t.expressionStatement(
|
|
2073
|
+
t.callExpression(t.identifier('_$insert'), [
|
|
2074
|
+
t.identifier(elId),
|
|
2075
|
+
t.stringLiteral(entry.text),
|
|
2076
|
+
getMarker(entry.childIndex),
|
|
1863
2077
|
])
|
|
1864
|
-
)
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
emitFragmentInserts(statements, elId, entry.child, anchor, state);
|
|
1868
|
-
}
|
|
1869
|
-
}
|
|
1870
|
-
|
|
1871
|
-
// Insert one fragment's children into `elId`, each before `anchor`.
|
|
1872
|
-
// Recursive, because `<><>a</>b</>` is a fragment whose child is a fragment
|
|
1873
|
-
// and both belong at the same position in the parent.
|
|
1874
|
-
function emitFragmentInserts(statements, elId, fragmentNode, anchor, state) {
|
|
1875
|
-
for (const fChild of fragmentNode.children) {
|
|
1876
|
-
if (t.isJSXText(fChild)) {
|
|
1877
|
-
const text = normalizeJsxText(fChild.value);
|
|
1878
|
-
if (!text) continue;
|
|
1879
|
-
state.needsInsert = true;
|
|
1880
|
-
statements.push(
|
|
1881
|
-
t.expressionStatement(
|
|
1882
|
-
t.callExpression(t.identifier('_$insert'), [
|
|
1883
|
-
t.identifier(elId),
|
|
1884
|
-
t.stringLiteral(text),
|
|
1885
|
-
anchor,
|
|
1886
|
-
])
|
|
1887
|
-
)
|
|
1888
|
-
);
|
|
1889
|
-
continue;
|
|
1890
|
-
}
|
|
1891
|
-
|
|
1892
|
-
if (t.isJSXFragment(fChild)) {
|
|
1893
|
-
emitFragmentInserts(statements, elId, fChild, anchor, state);
|
|
1894
|
-
continue;
|
|
1895
|
-
}
|
|
1896
|
-
|
|
1897
|
-
if (t.isJSXElement(fChild)) {
|
|
1898
|
-
state.needsInsert = true;
|
|
1899
|
-
statements.push(
|
|
1900
|
-
t.expressionStatement(
|
|
1901
|
-
t.callExpression(t.identifier('_$insert'), [
|
|
1902
|
-
t.identifier(elId),
|
|
1903
|
-
transformElementFineGrained({ node: fChild }, state),
|
|
1904
|
-
anchor,
|
|
1905
|
-
])
|
|
1906
|
-
)
|
|
1907
|
-
);
|
|
1908
|
-
continue;
|
|
1909
|
-
}
|
|
1910
|
-
|
|
1911
|
-
if (t.isJSXExpressionContainer(fChild) && !t.isJSXEmptyExpression(fChild.expression)) {
|
|
1912
|
-
state.needsInsert = true;
|
|
1913
|
-
let expr = fChild.expression;
|
|
1914
|
-
// Unchanged from before this fix: a potentially reactive expression
|
|
1915
|
-
// inside a fragment is wrapped in a thunk so insert() re-runs it.
|
|
1916
|
-
if (isPotentiallyReactive(expr, state.signalNames, state.importedIdentifiers)) {
|
|
1917
|
-
expr = memoizeBranchCondition(expr, statements, state); // (C1)
|
|
1918
|
-
expr = t.arrowFunctionExpression([], expr);
|
|
1919
|
-
}
|
|
1920
|
-
statements.push(
|
|
1921
|
-
t.expressionStatement(
|
|
1922
|
-
t.callExpression(t.identifier('_$insert'), [
|
|
1923
|
-
t.identifier(elId),
|
|
1924
|
-
expr,
|
|
1925
|
-
anchor,
|
|
1926
|
-
])
|
|
1927
|
-
)
|
|
1928
|
-
);
|
|
1929
|
-
}
|
|
2078
|
+
)
|
|
2079
|
+
);
|
|
2080
|
+
continue;
|
|
1930
2081
|
}
|
|
1931
2082
|
}
|
|
1932
2083
|
}
|
|
@@ -1953,6 +2104,7 @@ export default function whatBabelPlugin({ types: t }) {
|
|
|
1953
2104
|
const children = node.children;
|
|
1954
2105
|
|
|
1955
2106
|
// Check for client: directive (islands)
|
|
2107
|
+
/** @type {{ type: string, value?: any } | null} */
|
|
1956
2108
|
let clientDirective = null;
|
|
1957
2109
|
const filteredAttrs = [];
|
|
1958
2110
|
|
|
@@ -2002,7 +2154,17 @@ export default function whatBabelPlugin({ types: t }) {
|
|
|
2002
2154
|
} else if (t.isJSXExpressionContainer(child)) {
|
|
2003
2155
|
if (!t.isJSXEmptyExpression(child.expression)) {
|
|
2004
2156
|
deferChildren = true;
|
|
2005
|
-
|
|
2157
|
+
// The same lowering an element child and a fragment child get.
|
|
2158
|
+
// Without it `<Box>{count()}</Box>` was evaluated once and never
|
|
2159
|
+
// again, while `<div>{count()}</div>` right beside it stayed live —
|
|
2160
|
+
// the auto-thunk docs/GOTCHAS.md section 2 promises for "any call
|
|
2161
|
+
// with no arguments" reached every JSX position EXCEPT this one.
|
|
2162
|
+
//
|
|
2163
|
+
// Nothing about it is component-specific: a component's children
|
|
2164
|
+
// are built into an array with no host element to _$insert into,
|
|
2165
|
+
// which is exactly the situation lowerFragmentExprChild exists for,
|
|
2166
|
+
// and it is where `.map()` picks up keyed reconciliation too.
|
|
2167
|
+
transformedChildren.push(lowerFragmentExprChild(child.expression, state));
|
|
2006
2168
|
}
|
|
2007
2169
|
} else if (t.isJSXElement(child)) {
|
|
2008
2170
|
// <For>/<Show>/<Switch> lower to an inserter or a thunk (already lazy).
|
|
@@ -2034,39 +2196,71 @@ export default function whatBabelPlugin({ types: t }) {
|
|
|
2034
2196
|
state.needsCreateComponent = true;
|
|
2035
2197
|
state.needsIsland = true;
|
|
2036
2198
|
|
|
2037
|
-
|
|
2199
|
+
// The directive's own machinery, kept apart from user data because it is
|
|
2200
|
+
// always applied LAST: `component` comes from the tag and `mode` from the
|
|
2201
|
+
// directive, so nothing a caller spreads in can reach either one.
|
|
2202
|
+
const directiveProps = [
|
|
2038
2203
|
t.objectProperty(t.identifier('component'), componentRef),
|
|
2039
2204
|
t.objectProperty(t.identifier('mode'), t.stringLiteral(clientDirective.type)),
|
|
2040
2205
|
];
|
|
2041
2206
|
|
|
2042
2207
|
if (clientDirective.value) {
|
|
2043
|
-
|
|
2208
|
+
directiveProps.push(
|
|
2044
2209
|
t.objectProperty(t.identifier('mediaQuery'), t.stringLiteral(clientDirective.value))
|
|
2045
2210
|
);
|
|
2046
2211
|
}
|
|
2047
2212
|
|
|
2048
|
-
|
|
2213
|
+
// User props in source order — same rule, and the same reasons, as the
|
|
2214
|
+
// regular component path below. An island IS a component, so
|
|
2215
|
+
// `<Chart client:load {...a} {...b} />` has to mean `{ ...a, ...b }` for
|
|
2216
|
+
// exactly the reason `<Chart {...a} {...b} />` does.
|
|
2217
|
+
const islandParts = [];
|
|
2218
|
+
let islandUserProps = [];
|
|
2219
|
+
const flushIslandUserProps = () => {
|
|
2220
|
+
if (islandUserProps.length > 0) {
|
|
2221
|
+
islandParts.push(t.objectExpression(islandUserProps));
|
|
2222
|
+
islandUserProps = [];
|
|
2223
|
+
}
|
|
2224
|
+
};
|
|
2225
|
+
|
|
2049
2226
|
for (const attr of filteredAttrs) {
|
|
2050
2227
|
if (t.isJSXSpreadAttribute(attr)) {
|
|
2051
2228
|
// A spread used to be dropped on the floor here, so
|
|
2052
2229
|
// `<Chart client:visible {...config} />` lost every prop in `config`.
|
|
2053
|
-
|
|
2230
|
+
// Then only the LAST spread was kept, which lost every key an earlier
|
|
2231
|
+
// spread carried and the last one did not.
|
|
2232
|
+
flushIslandUserProps();
|
|
2233
|
+
islandParts.push(attr.argument);
|
|
2054
2234
|
continue;
|
|
2055
2235
|
}
|
|
2056
2236
|
const attrName = getAttrName(attr);
|
|
2057
2237
|
if (attrName === 'key') continue;
|
|
2058
2238
|
const value = getAttributeValue(attr.value);
|
|
2059
|
-
|
|
2239
|
+
// `data-x` and `aria-label` are not identifiers. Emitting one as a bare
|
|
2240
|
+
// key produced `{data-x: "1"}`, which is a SYNTAX ERROR: babel does not
|
|
2241
|
+
// validate what its builders are handed, so the compiler exited 0 and
|
|
2242
|
+
// wrote a file that no parser would accept. `<Chart client:load
|
|
2243
|
+
// aria-label="..." />` is an ordinary thing to write, and it took the
|
|
2244
|
+
// whole build down with an error pointing at generated output.
|
|
2245
|
+
//
|
|
2246
|
+
// The regular component branch below has always tested the name first;
|
|
2247
|
+
// this branch is the copy that did not.
|
|
2248
|
+
islandUserProps.push(t.objectProperty(propKey(attrName), value));
|
|
2060
2249
|
}
|
|
2061
2250
|
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
|
|
2251
|
+
let islandPropsExpr;
|
|
2252
|
+
if (islandParts.length === 0) {
|
|
2253
|
+
// No spread anywhere: one object literal, directive keys written last so
|
|
2254
|
+
// they still win. This is the shape nearly every island has, and keeping
|
|
2255
|
+
// it a literal keeps the emitted output byte-identical to before.
|
|
2256
|
+
islandPropsExpr = t.objectExpression([...islandUserProps, ...directiveProps]);
|
|
2257
|
+
} else {
|
|
2258
|
+
flushIslandUserProps();
|
|
2259
|
+
islandPropsExpr = t.callExpression(
|
|
2260
|
+
t.memberExpression(t.identifier('Object'), t.identifier('assign')),
|
|
2261
|
+
[t.objectExpression([]), ...islandParts, t.objectExpression(directiveProps)]
|
|
2262
|
+
);
|
|
2263
|
+
}
|
|
2070
2264
|
|
|
2071
2265
|
return t.callExpression(
|
|
2072
2266
|
t.identifier('_$createComponent'),
|
|
@@ -2074,17 +2268,51 @@ export default function whatBabelPlugin({ types: t }) {
|
|
|
2074
2268
|
);
|
|
2075
2269
|
}
|
|
2076
2270
|
|
|
2077
|
-
// Regular component — use _$createComponent to instantiate, component runs once
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
|
|
2081
|
-
|
|
2082
|
-
|
|
2271
|
+
// Regular component — use _$createComponent to instantiate, component runs once.
|
|
2272
|
+
// Unless this element IS the tree handed to hydrate(), in which case it has
|
|
2273
|
+
// to arrive unbuilt: see isHydrateRootArgument.
|
|
2274
|
+
//
|
|
2275
|
+
// The root, and only the root. A host element lowers to a template clone, so
|
|
2276
|
+
// "unbuilt" is not something the children of an arbitrary tree can inherit —
|
|
2277
|
+
// <App><p>hi</p></App> would still build the <p>, and pushing the choice down
|
|
2278
|
+
// through component children only would produce a tree that hydrates in
|
|
2279
|
+
// patches. The root is the one position where the answer is unambiguous and
|
|
2280
|
+
// it is where the documented entry point lives.
|
|
2281
|
+
const isHydrateRoot = isHydrateRootArgument(path);
|
|
2282
|
+
if (isHydrateRoot) state.needsComponentVNode = true;
|
|
2283
|
+
else state.needsCreateComponent = true;
|
|
2284
|
+
|
|
2285
|
+
// Props resolve in SOURCE order. `<Box a {...s} b />` is one spelling of
|
|
2286
|
+
// `{ a, ...s, b }`, which is what h(), the JSX runtime, every other JSX
|
|
2287
|
+
// toolchain, and (since #65) this compiler's own ELEMENT path all produce.
|
|
2288
|
+
// `parts` is that argument list: runs of explicit attributes, separated by
|
|
2289
|
+
// the spreads written between them.
|
|
2290
|
+
//
|
|
2291
|
+
// Keeping a single `spreadExpr` and appending every explicit prop last, the
|
|
2292
|
+
// way this used to, got both halves wrong. `<Box {...a} {...b} />` kept only
|
|
2293
|
+
// `b`, so every key `a` carried and `b` did not vanished completely — data
|
|
2294
|
+
// loss, not merely precedence — and an attribute written BEFORE a spread
|
|
2295
|
+
// beat it, which no other JSX implementation does.
|
|
2296
|
+
//
|
|
2297
|
+
// The merge is a shallow copy, deliberately: a prop is reactive here when
|
|
2298
|
+
// its VALUE is an accessor, and Object.assign copies that function through
|
|
2299
|
+
// untouched. A lone non-literal spread is copied too, the same
|
|
2300
|
+
// Object.assign path a multi-prop merge already used, so two
|
|
2301
|
+
// `<Box {...reused}>` sites do not share one identity. createComponent
|
|
2302
|
+
// (dom.js) copies again before the component sees the props.
|
|
2303
|
+
const parts = [];
|
|
2304
|
+
let props = [];
|
|
2305
|
+
const flushProps = () => {
|
|
2306
|
+
if (props.length > 0) {
|
|
2307
|
+
parts.push(t.objectExpression(props));
|
|
2308
|
+
props = [];
|
|
2309
|
+
}
|
|
2310
|
+
};
|
|
2083
2311
|
|
|
2084
2312
|
for (const attr of filteredAttrs) {
|
|
2085
2313
|
if (t.isJSXSpreadAttribute(attr)) {
|
|
2086
|
-
|
|
2087
|
-
|
|
2314
|
+
flushProps();
|
|
2315
|
+
parts.push(attr.argument);
|
|
2088
2316
|
continue;
|
|
2089
2317
|
}
|
|
2090
2318
|
|
|
@@ -2151,35 +2379,32 @@ export default function whatBabelPlugin({ types: t }) {
|
|
|
2151
2379
|
|
|
2152
2380
|
const value = getAttributeValue(attr.value);
|
|
2153
2381
|
|
|
2154
|
-
props.push(
|
|
2155
|
-
t.objectProperty(
|
|
2156
|
-
/^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(attrName)
|
|
2157
|
-
? t.identifier(attrName)
|
|
2158
|
-
: t.stringLiteral(attrName),
|
|
2159
|
-
value
|
|
2160
|
-
)
|
|
2161
|
-
);
|
|
2382
|
+
props.push(t.objectProperty(propKey(attrName), value));
|
|
2162
2383
|
}
|
|
2163
2384
|
|
|
2385
|
+
flushProps();
|
|
2386
|
+
|
|
2164
2387
|
let propsExpr;
|
|
2165
|
-
if (
|
|
2166
|
-
if (props.length > 0) {
|
|
2167
|
-
propsExpr = t.callExpression(
|
|
2168
|
-
t.memberExpression(t.identifier('Object'), t.identifier('assign')),
|
|
2169
|
-
[t.objectExpression([]), spreadExpr, t.objectExpression(props)]
|
|
2170
|
-
);
|
|
2171
|
-
} else {
|
|
2172
|
-
propsExpr = spreadExpr;
|
|
2173
|
-
}
|
|
2174
|
-
} else if (props.length > 0) {
|
|
2175
|
-
propsExpr = t.objectExpression(props);
|
|
2176
|
-
} else {
|
|
2388
|
+
if (parts.length === 0) {
|
|
2177
2389
|
propsExpr = t.nullLiteral();
|
|
2390
|
+
} else if (parts.length === 1 && t.isObjectExpression(parts[0])) {
|
|
2391
|
+
// A lone object literal (no spread, or an inline `{...}`) is already a
|
|
2392
|
+
// fresh props object. A lone identifier/member/call spread is the
|
|
2393
|
+
// CALLER'S object and must go through the copy below.
|
|
2394
|
+
propsExpr = parts[0];
|
|
2395
|
+
} else {
|
|
2396
|
+
propsExpr = t.callExpression(
|
|
2397
|
+
t.memberExpression(t.identifier('Object'), t.identifier('assign')),
|
|
2398
|
+
[t.objectExpression([]), ...parts]
|
|
2399
|
+
);
|
|
2178
2400
|
}
|
|
2179
2401
|
|
|
2180
2402
|
const childrenArg = transformComponentChildren();
|
|
2181
2403
|
|
|
2182
|
-
return t.callExpression(
|
|
2404
|
+
return t.callExpression(
|
|
2405
|
+
t.identifier(isHydrateRoot ? '_$componentVNode' : '_$createComponent'),
|
|
2406
|
+
[componentRef, propsExpr, childrenArg]
|
|
2407
|
+
);
|
|
2183
2408
|
}
|
|
2184
2409
|
|
|
2185
2410
|
function transformForFineGrained(path, state) {
|
|
@@ -3066,6 +3291,7 @@ export default function whatBabelPlugin({ types: t }) {
|
|
|
3066
3291
|
state.needsSetChecked = false;
|
|
3067
3292
|
state.needsH = false;
|
|
3068
3293
|
state.needsCreateComponent = false;
|
|
3294
|
+
state.needsComponentVNode = false;
|
|
3069
3295
|
state.needsFragment = false;
|
|
3070
3296
|
state.needsIsland = false;
|
|
3071
3297
|
state.needsDelegation = false;
|
|
@@ -3112,11 +3338,7 @@ export default function whatBabelPlugin({ types: t }) {
|
|
|
3112
3338
|
for (const node of path.node.body) {
|
|
3113
3339
|
if (t.isImportDeclaration(node)) {
|
|
3114
3340
|
const source = node.source.value;
|
|
3115
|
-
const isWhatSource =
|
|
3116
|
-
source === 'what-framework' ||
|
|
3117
|
-
source.startsWith('what-framework/') ||
|
|
3118
|
-
source === 'what-core' ||
|
|
3119
|
-
source.startsWith('what-core/');
|
|
3341
|
+
const isWhatSource = isWhatModuleSource(source);
|
|
3120
3342
|
const isReactiveSource =
|
|
3121
3343
|
isWhatSource ||
|
|
3122
3344
|
source.startsWith('./') ||
|
|
@@ -3285,6 +3507,11 @@ export default function whatBabelPlugin({ types: t }) {
|
|
|
3285
3507
|
t.importSpecifier(t.identifier('_$createComponent'), t.identifier('_$createComponent'))
|
|
3286
3508
|
);
|
|
3287
3509
|
}
|
|
3510
|
+
if (state.needsComponentVNode) {
|
|
3511
|
+
fgSpecifiers.push(
|
|
3512
|
+
t.importSpecifier(t.identifier('_$componentVNode'), t.identifier('_$componentVNode'))
|
|
3513
|
+
);
|
|
3514
|
+
}
|
|
3288
3515
|
if (state.needsDelegation) {
|
|
3289
3516
|
fgSpecifiers.push(
|
|
3290
3517
|
t.importSpecifier(t.identifier('_$delegateEvents'), t.identifier('delegateEvents'))
|
|
@@ -3310,6 +3537,7 @@ export default function whatBabelPlugin({ types: t }) {
|
|
|
3310
3537
|
}
|
|
3311
3538
|
|
|
3312
3539
|
if (fgSpecifiers.length > 0) {
|
|
3540
|
+
/** @type {any} */
|
|
3313
3541
|
let existingRenderImport = null;
|
|
3314
3542
|
for (const node of path.node.body) {
|
|
3315
3543
|
if (t.isImportDeclaration(node) && (
|
|
@@ -3400,6 +3628,7 @@ export default function whatBabelPlugin({ types: t }) {
|
|
|
3400
3628
|
}
|
|
3401
3629
|
|
|
3402
3630
|
function addCoreImports(path, t, coreSpecifiers) {
|
|
3631
|
+
/** @type {any} */
|
|
3403
3632
|
let existingImport = null;
|
|
3404
3633
|
for (const node of path.node.body) {
|
|
3405
3634
|
if (t.isImportDeclaration(node) && (
|