what-compiler 0.11.7 → 0.12.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.min.js +1 -1
- package/dist/index.min.js +6 -6
- package/dist/vite-plugin.min.js +6 -6
- package/index.d.ts +7 -7
- package/package.json +2 -2
- package/src/babel-plugin.js +418 -76
package/src/babel-plugin.js
CHANGED
|
@@ -47,6 +47,10 @@ const SIGNAL_CREATORS = new Set([
|
|
|
47
47
|
'createResource', 'useSWR', 'useQuery', 'useInfiniteQuery',
|
|
48
48
|
]);
|
|
49
49
|
|
|
50
|
+
// Control-flow tags the plugin lowers itself instead of emitting
|
|
51
|
+
// _$createComponent, so their children never travel through a factory.
|
|
52
|
+
const LOWERED_TAGS = new Set(['For', 'Show', 'Switch', 'Match']);
|
|
53
|
+
|
|
50
54
|
const SERVER_ACTION_SOURCES = new Set([
|
|
51
55
|
'what-framework/server',
|
|
52
56
|
'what-server',
|
|
@@ -174,6 +178,105 @@ export default function whatBabelPlugin({ types: t }) {
|
|
|
174
178
|
return /^[A-Z]/.test(name);
|
|
175
179
|
}
|
|
176
180
|
|
|
181
|
+
// Dotted tags (<Ctx.Provider>, <Foo.Bar.Baz>) are always components.
|
|
182
|
+
function isComponentElement(el) {
|
|
183
|
+
const name = el.openingElement ? el.openingElement.name : el.name;
|
|
184
|
+
if (t.isJSXMemberExpression(name)) return true;
|
|
185
|
+
return t.isJSXIdentifier(name) && isComponent(name.name);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// A tag whose binding came from a package other than What. isWhatTag is the
|
|
189
|
+
// stronger claim: the binding came from What, so the tag is provably ours.
|
|
190
|
+
function isForeignTag(name, state) {
|
|
191
|
+
return !!(state.foreignImports && state.foreignImports.has(name));
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function isWhatTag(name, state) {
|
|
195
|
+
return !!(state.whatImports && state.whatImports.has(name));
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// Literal <Match> arms are proof enough that a <Switch> is What's, whoever it
|
|
199
|
+
// was imported from: a bare specifier can be a re-export of What's control
|
|
200
|
+
// flow just as a relative one can, and nobody writes <Match> arms against a
|
|
201
|
+
// third-party <Switch>. Unlike <Show> and <For>, <Switch> has no working
|
|
202
|
+
// runtime path, so guessing wrong here renders the fallback forever.
|
|
203
|
+
function hasLiteralMatchArms(node) {
|
|
204
|
+
return node.children.some((child) =>
|
|
205
|
+
t.isJSXElement(child) && t.isJSXIdentifier(child.openingElement.name, { name: 'Match' }));
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// JSXMemberExpression -> MemberExpression callee for _$createComponent.
|
|
209
|
+
function componentCallee(name) {
|
|
210
|
+
if (t.isJSXMemberExpression(name)) {
|
|
211
|
+
return t.memberExpression(componentCallee(name.object), t.identifier(name.property.name));
|
|
212
|
+
}
|
|
213
|
+
return t.identifier(name.name);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// --- Forwarded children on a destructured parameter ---
|
|
217
|
+
// `function Wrap({ children }) { return <ErrorBoundary>{children}</ErrorBoundary>; }`
|
|
218
|
+
// realizes the subtree while the parameter list is destructured, which is
|
|
219
|
+
// before ErrorBoundary is entered, so the boundary never catches and a
|
|
220
|
+
// provider publishes its value too late for its own subtree. Rewriting the
|
|
221
|
+
// pattern to a props identifier moves the read into the deferred children
|
|
222
|
+
// factory, where the enclosing component is already on the stack. Only a
|
|
223
|
+
// binding whose every reference is a component-child forward qualifies: a
|
|
224
|
+
// component that inspects its children still needs them realized.
|
|
225
|
+
function deferForwardedChildren(fnPath) {
|
|
226
|
+
const params = fnPath.node.params;
|
|
227
|
+
if (params.length !== 1 || !t.isObjectPattern(params[0])) return;
|
|
228
|
+
const pattern = params[0];
|
|
229
|
+
let index = -1;
|
|
230
|
+
for (let i = 0; i < pattern.properties.length; i++) {
|
|
231
|
+
const prop = pattern.properties[i];
|
|
232
|
+
if (t.isRestElement(prop)) return;
|
|
233
|
+
if (t.isObjectProperty(prop) && !prop.computed &&
|
|
234
|
+
t.isIdentifier(prop.key, { name: 'children' })) {
|
|
235
|
+
// A duplicate key would leave the other one destructured in the body,
|
|
236
|
+
// which reads the getter and defeats the deferral.
|
|
237
|
+
if (index !== -1 || !t.isIdentifier(prop.value)) return;
|
|
238
|
+
index = i;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
if (index === -1) return;
|
|
242
|
+
|
|
243
|
+
const binding = fnPath.scope.getBinding(pattern.properties[index].value.name);
|
|
244
|
+
if (!binding || binding.constantViolations.length > 0 || binding.referencePaths.length === 0) return;
|
|
245
|
+
const forwarded = binding.referencePaths.every((ref) => {
|
|
246
|
+
const container = ref.parentPath;
|
|
247
|
+
if (!container || !container.isJSXExpressionContainer()) return false;
|
|
248
|
+
const owner = container.parentPath;
|
|
249
|
+
if (!owner || !owner.isJSXElement() || !isComponentElement(owner.node)) return false;
|
|
250
|
+
const tag = owner.node.openingElement.name;
|
|
251
|
+
return !(t.isJSXIdentifier(tag) && LOWERED_TAGS.has(tag.name));
|
|
252
|
+
});
|
|
253
|
+
if (!forwarded) return;
|
|
254
|
+
|
|
255
|
+
const propsId = fnPath.scope.generateUidIdentifier('props');
|
|
256
|
+
if (pattern.typeAnnotation) propsId.typeAnnotation = pattern.typeAnnotation;
|
|
257
|
+
params[0] = propsId;
|
|
258
|
+
const remaining = pattern.properties.filter((_, i) => i !== index);
|
|
259
|
+
if (remaining.length > 0) {
|
|
260
|
+
if (!t.isBlockStatement(fnPath.node.body)) {
|
|
261
|
+
fnPath.node.body = t.blockStatement([t.returnStatement(fnPath.node.body)]);
|
|
262
|
+
}
|
|
263
|
+
const moved = t.objectPattern(remaining);
|
|
264
|
+
fnPath.node.body.body.unshift(
|
|
265
|
+
t.variableDeclaration('const', [
|
|
266
|
+
t.variableDeclarator(moved, t.cloneNode(propsId)),
|
|
267
|
+
])
|
|
268
|
+
);
|
|
269
|
+
// collectSignalNamesFromScope classifies a component's props by reading
|
|
270
|
+
// the parameter list, so the moved pattern has to stay visible to it or
|
|
271
|
+
// every sibling prop silently stops being treated as reactive.
|
|
272
|
+
fnPath.node._whatMovedProps = moved;
|
|
273
|
+
}
|
|
274
|
+
for (const ref of binding.referencePaths) {
|
|
275
|
+
ref.replaceWith(t.memberExpression(t.cloneNode(propsId), t.identifier('children')));
|
|
276
|
+
}
|
|
277
|
+
fnPath.scope.crawl();
|
|
278
|
+
}
|
|
279
|
+
|
|
177
280
|
function isVoidHtmlElement(name) {
|
|
178
281
|
return VOID_HTML_ELEMENTS.has(String(name).toLowerCase());
|
|
179
282
|
}
|
|
@@ -199,6 +302,12 @@ export default function whatBabelPlugin({ types: t }) {
|
|
|
199
302
|
return typeof attr.name.name === 'string' ? attr.name.name : String(attr.name.name);
|
|
200
303
|
}
|
|
201
304
|
|
|
305
|
+
// Handler-shaped attribute name, case-insensitive. Mirrors _isEventProp in
|
|
306
|
+
// what-core's dom.js.
|
|
307
|
+
function isEventAttrName(name) {
|
|
308
|
+
return !!name && name.length > 2 && /^on/i.test(name);
|
|
309
|
+
}
|
|
310
|
+
|
|
202
311
|
function createEventHandler(handler, modifiers) {
|
|
203
312
|
if (modifiers.length === 0) return handler;
|
|
204
313
|
|
|
@@ -314,6 +423,17 @@ export default function whatBabelPlugin({ types: t }) {
|
|
|
314
423
|
}
|
|
315
424
|
}
|
|
316
425
|
|
|
426
|
+
function extractFromPattern(param) {
|
|
427
|
+
if (!t.isObjectPattern(param)) return;
|
|
428
|
+
for (const prop of param.properties) {
|
|
429
|
+
if (t.isObjectProperty(prop) && t.isIdentifier(prop.value)) {
|
|
430
|
+
signalNames.add(prop.value.name);
|
|
431
|
+
} else if (t.isRestElement(prop) && t.isIdentifier(prop.argument)) {
|
|
432
|
+
signalNames.add(prop.argument.name);
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
|
|
317
437
|
// Walk up the scope chain using Babel's scope API.
|
|
318
438
|
let scope = path.scope;
|
|
319
439
|
while (scope) {
|
|
@@ -327,18 +447,13 @@ export default function whatBabelPlugin({ types: t }) {
|
|
|
327
447
|
// scope — not once per binding. The old per-binding rescan made this
|
|
328
448
|
// O(params × bindings) per scope per JSXElement. (AUDIT-2026-06-06 H2)
|
|
329
449
|
const fnNode = scope.path && scope.path.node;
|
|
330
|
-
if (fnNode
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
for (const prop of param.properties) {
|
|
334
|
-
if (t.isObjectProperty(prop) && t.isIdentifier(prop.value)) {
|
|
335
|
-
signalNames.add(prop.value.name);
|
|
336
|
-
} else if (t.isRestElement(prop) && t.isIdentifier(prop.argument)) {
|
|
337
|
-
signalNames.add(prop.argument.name);
|
|
338
|
-
}
|
|
339
|
-
}
|
|
340
|
-
}
|
|
450
|
+
if (fnNode) {
|
|
451
|
+
if (fnNode.params) {
|
|
452
|
+
for (const param of fnNode.params) extractFromPattern(param);
|
|
341
453
|
}
|
|
454
|
+
// deferForwardedChildren moved this pattern out of the parameter list;
|
|
455
|
+
// it is still the same props destructuring and classifies the same way.
|
|
456
|
+
if (fnNode._whatMovedProps) extractFromPattern(fnNode._whatMovedProps);
|
|
342
457
|
}
|
|
343
458
|
scope = scope.parent;
|
|
344
459
|
}
|
|
@@ -628,8 +743,7 @@ export default function whatBabelPlugin({ types: t }) {
|
|
|
628
743
|
if (t.isJSXExpressionContainer(child)) return false;
|
|
629
744
|
if (t.isJSXElement(child)) {
|
|
630
745
|
const el = child.openingElement;
|
|
631
|
-
|
|
632
|
-
if (isComponent(tagName)) return false;
|
|
746
|
+
if (isComponentElement(child)) return false;
|
|
633
747
|
for (const attr of el.attributes) {
|
|
634
748
|
if (t.isJSXSpreadAttribute(attr)) return false;
|
|
635
749
|
const value = attr.value;
|
|
@@ -664,7 +778,7 @@ export default function whatBabelPlugin({ types: t }) {
|
|
|
664
778
|
const el = node.openingElement;
|
|
665
779
|
const tagName = el.name.name;
|
|
666
780
|
|
|
667
|
-
if (
|
|
781
|
+
if (isComponentElement(node)) return '';
|
|
668
782
|
|
|
669
783
|
let html = `<${tagName}`;
|
|
670
784
|
|
|
@@ -672,7 +786,11 @@ export default function whatBabelPlugin({ types: t }) {
|
|
|
672
786
|
if (t.isJSXSpreadAttribute(attr)) continue;
|
|
673
787
|
const name = getAttrName(attr);
|
|
674
788
|
if (name === 'key') continue;
|
|
675
|
-
|
|
789
|
+
// Case-insensitive: a static template is applied via innerHTML, so an
|
|
790
|
+
// `ONCLICK="alert(1)"` baked into the string becomes a live handler.
|
|
791
|
+
// Lowercase handlers are compiled separately; uppercase ones are dropped,
|
|
792
|
+
// matching how the runtime and SSR paths treat them.
|
|
793
|
+
if (isEventAttrName(name) || name.startsWith('bind:') || name.includes('|')) continue;
|
|
676
794
|
|
|
677
795
|
let domName = name;
|
|
678
796
|
if (name === 'className') domName = 'class';
|
|
@@ -709,7 +827,7 @@ export default function whatBabelPlugin({ types: t }) {
|
|
|
709
827
|
html += '<!--$-->';
|
|
710
828
|
}
|
|
711
829
|
} else if (t.isJSXElement(child)) {
|
|
712
|
-
if (
|
|
830
|
+
if (isComponentElement(child)) {
|
|
713
831
|
html += '<!--$-->';
|
|
714
832
|
} else {
|
|
715
833
|
html += extractStaticHTML(child);
|
|
@@ -735,15 +853,23 @@ export default function whatBabelPlugin({ types: t }) {
|
|
|
735
853
|
const openingElement = node.openingElement;
|
|
736
854
|
const tagName = openingElement.name.name;
|
|
737
855
|
|
|
738
|
-
// Control flow components
|
|
739
|
-
|
|
740
|
-
|
|
856
|
+
// Control flow components, checked before generic isComponent since they start uppercase.
|
|
857
|
+
// A tag imported from somewhere other than What is that package's component,
|
|
858
|
+
// whatever it is called, so it goes to the component path untouched, unless
|
|
859
|
+
// its own arms prove otherwise.
|
|
860
|
+
if (!isForeignTag(tagName, state)) {
|
|
861
|
+
if (tagName === 'For') {
|
|
862
|
+
return transformForFineGrained(path, state);
|
|
863
|
+
}
|
|
864
|
+
if (tagName === 'Show') {
|
|
865
|
+
return transformShowFineGrained(path, state);
|
|
866
|
+
}
|
|
741
867
|
}
|
|
742
|
-
if (tagName === '
|
|
743
|
-
return
|
|
868
|
+
if (tagName === 'Switch' && (!isForeignTag(tagName, state) || hasLiteralMatchArms(node))) {
|
|
869
|
+
return transformSwitchFineGrained(path, state);
|
|
744
870
|
}
|
|
745
871
|
|
|
746
|
-
if (
|
|
872
|
+
if (isComponentElement(node)) {
|
|
747
873
|
return transformComponentFineGrained(path, state);
|
|
748
874
|
}
|
|
749
875
|
|
|
@@ -1245,7 +1371,7 @@ export default function whatBabelPlugin({ types: t }) {
|
|
|
1245
1371
|
|
|
1246
1372
|
if (t.isJSXElement(child)) {
|
|
1247
1373
|
const childTag = child.openingElement.name.name;
|
|
1248
|
-
if (
|
|
1374
|
+
if (isComponentElement(child) || childTag === 'For' || childTag === 'Show') {
|
|
1249
1375
|
entries.push({ type: 'component', child, childIndex });
|
|
1250
1376
|
childIndex++;
|
|
1251
1377
|
} else {
|
|
@@ -1498,7 +1624,7 @@ export default function whatBabelPlugin({ types: t }) {
|
|
|
1498
1624
|
function transformComponentFineGrained(path, state) {
|
|
1499
1625
|
const { node } = path;
|
|
1500
1626
|
const openingElement = node.openingElement;
|
|
1501
|
-
const
|
|
1627
|
+
const componentRef = componentCallee(openingElement.name);
|
|
1502
1628
|
const attributes = openingElement.attributes;
|
|
1503
1629
|
const children = node.children;
|
|
1504
1630
|
|
|
@@ -1528,12 +1654,64 @@ export default function whatBabelPlugin({ types: t }) {
|
|
|
1528
1654
|
filteredAttrs.push(attr);
|
|
1529
1655
|
}
|
|
1530
1656
|
|
|
1657
|
+
// Transform children.
|
|
1658
|
+
// Element children build their DOM at the call site, i.e. before the parent
|
|
1659
|
+
// component ever runs, so a <Provider> publishes its context value too late
|
|
1660
|
+
// for its own subtree. Collect the element setup and emit it behind a
|
|
1661
|
+
// zero-arg factory instead; the runtime calls the factory while the parent
|
|
1662
|
+
// is on the component stack. An expression child is deferred for the same
|
|
1663
|
+
// reason: it is the shape a wrapper forwards its own children through, and
|
|
1664
|
+
// reading it while the argument list is built happens before the component
|
|
1665
|
+
// it is being handed to has run. Text children have nothing to defer.
|
|
1666
|
+
//
|
|
1667
|
+
// Shared by the island branch and the regular component branch: an island is
|
|
1668
|
+
// still a component, and `<Panel client:visible>...</Panel>` must keep its
|
|
1669
|
+
// children rather than silently dropping them.
|
|
1670
|
+
function transformComponentChildren() {
|
|
1671
|
+
const setupMark = state._pendingSetup ? state._pendingSetup.length : 0;
|
|
1672
|
+
const transformedChildren = [];
|
|
1673
|
+
let deferChildren = false;
|
|
1674
|
+
for (const child of children) {
|
|
1675
|
+
if (t.isJSXText(child)) {
|
|
1676
|
+
const text = normalizeJsxText(child.value);
|
|
1677
|
+
if (text) transformedChildren.push(t.stringLiteral(text));
|
|
1678
|
+
} else if (t.isJSXExpressionContainer(child)) {
|
|
1679
|
+
if (!t.isJSXEmptyExpression(child.expression)) {
|
|
1680
|
+
deferChildren = true;
|
|
1681
|
+
transformedChildren.push(child.expression);
|
|
1682
|
+
}
|
|
1683
|
+
} else if (t.isJSXElement(child)) {
|
|
1684
|
+
// <For>/<Show>/<Switch> lower to an inserter or a thunk (already lazy).
|
|
1685
|
+
const childTag = child.openingElement.name.name;
|
|
1686
|
+
if (childTag !== 'For' && childTag !== 'Show' && childTag !== 'Switch') deferChildren = true;
|
|
1687
|
+
transformedChildren.push(transformElementFineGrained({ node: child }, state));
|
|
1688
|
+
} else if (t.isJSXFragment(child)) {
|
|
1689
|
+
deferChildren = true;
|
|
1690
|
+
transformedChildren.push(transformFragmentFineGrained({ node: child }, state));
|
|
1691
|
+
}
|
|
1692
|
+
}
|
|
1693
|
+
|
|
1694
|
+
let childrenArg = t.arrayExpression(transformedChildren);
|
|
1695
|
+
if (deferChildren) {
|
|
1696
|
+
// Hoisted element setup emitted while transforming these children belongs
|
|
1697
|
+
// inside the factory, not before the enclosing statement.
|
|
1698
|
+
const setup = state._pendingSetup ? state._pendingSetup.splice(setupMark) : [];
|
|
1699
|
+
childrenArg = t.arrowFunctionExpression(
|
|
1700
|
+
[],
|
|
1701
|
+
setup.length > 0
|
|
1702
|
+
? t.blockStatement([...setup, t.returnStatement(childrenArg)])
|
|
1703
|
+
: childrenArg
|
|
1704
|
+
);
|
|
1705
|
+
}
|
|
1706
|
+
return childrenArg;
|
|
1707
|
+
}
|
|
1708
|
+
|
|
1531
1709
|
if (clientDirective) {
|
|
1532
1710
|
state.needsCreateComponent = true;
|
|
1533
1711
|
state.needsIsland = true;
|
|
1534
1712
|
|
|
1535
1713
|
const islandProps = [
|
|
1536
|
-
t.objectProperty(t.identifier('component'),
|
|
1714
|
+
t.objectProperty(t.identifier('component'), componentRef),
|
|
1537
1715
|
t.objectProperty(t.identifier('mode'), t.stringLiteral(clientDirective.type)),
|
|
1538
1716
|
];
|
|
1539
1717
|
|
|
@@ -1543,16 +1721,32 @@ export default function whatBabelPlugin({ types: t }) {
|
|
|
1543
1721
|
);
|
|
1544
1722
|
}
|
|
1545
1723
|
|
|
1724
|
+
let islandSpread = null;
|
|
1546
1725
|
for (const attr of filteredAttrs) {
|
|
1547
|
-
if (t.isJSXSpreadAttribute(attr))
|
|
1726
|
+
if (t.isJSXSpreadAttribute(attr)) {
|
|
1727
|
+
// A spread used to be dropped on the floor here, so
|
|
1728
|
+
// `<Chart client:visible {...config} />` lost every prop in `config`.
|
|
1729
|
+
islandSpread = attr.argument;
|
|
1730
|
+
continue;
|
|
1731
|
+
}
|
|
1548
1732
|
const attrName = getAttrName(attr);
|
|
1733
|
+
if (attrName === 'key') continue;
|
|
1549
1734
|
const value = getAttributeValue(attr.value);
|
|
1550
1735
|
islandProps.push(t.objectProperty(t.identifier(attrName), value));
|
|
1551
1736
|
}
|
|
1552
1737
|
|
|
1738
|
+
// Spread first, explicit attributes second, so a written-out prop wins over
|
|
1739
|
+
// the same key coming from the spread (JSX evaluation order).
|
|
1740
|
+
const islandPropsExpr = islandSpread
|
|
1741
|
+
? t.callExpression(
|
|
1742
|
+
t.memberExpression(t.identifier('Object'), t.identifier('assign')),
|
|
1743
|
+
[t.objectExpression([]), islandSpread, t.objectExpression(islandProps)]
|
|
1744
|
+
)
|
|
1745
|
+
: t.objectExpression(islandProps);
|
|
1746
|
+
|
|
1553
1747
|
return t.callExpression(
|
|
1554
1748
|
t.identifier('_$createComponent'),
|
|
1555
|
-
[t.identifier('Island'),
|
|
1749
|
+
[t.identifier('Island'), islandPropsExpr, transformComponentChildren()]
|
|
1556
1750
|
);
|
|
1557
1751
|
}
|
|
1558
1752
|
|
|
@@ -1643,23 +1837,6 @@ export default function whatBabelPlugin({ types: t }) {
|
|
|
1643
1837
|
);
|
|
1644
1838
|
}
|
|
1645
1839
|
|
|
1646
|
-
// Transform children
|
|
1647
|
-
const transformedChildren = [];
|
|
1648
|
-
for (const child of children) {
|
|
1649
|
-
if (t.isJSXText(child)) {
|
|
1650
|
-
const text = normalizeJsxText(child.value);
|
|
1651
|
-
if (text) transformedChildren.push(t.stringLiteral(text));
|
|
1652
|
-
} else if (t.isJSXExpressionContainer(child)) {
|
|
1653
|
-
if (!t.isJSXEmptyExpression(child.expression)) {
|
|
1654
|
-
transformedChildren.push(child.expression);
|
|
1655
|
-
}
|
|
1656
|
-
} else if (t.isJSXElement(child)) {
|
|
1657
|
-
transformedChildren.push(transformElementFineGrained({ node: child }, state));
|
|
1658
|
-
} else if (t.isJSXFragment(child)) {
|
|
1659
|
-
transformedChildren.push(transformFragmentFineGrained({ node: child }, state));
|
|
1660
|
-
}
|
|
1661
|
-
}
|
|
1662
|
-
|
|
1663
1840
|
let propsExpr;
|
|
1664
1841
|
if (hasSpread) {
|
|
1665
1842
|
if (props.length > 0) {
|
|
@@ -1676,11 +1853,9 @@ export default function whatBabelPlugin({ types: t }) {
|
|
|
1676
1853
|
propsExpr = t.nullLiteral();
|
|
1677
1854
|
}
|
|
1678
1855
|
|
|
1679
|
-
const
|
|
1680
|
-
? t.arrayExpression(transformedChildren)
|
|
1681
|
-
: t.arrayExpression([]);
|
|
1856
|
+
const childrenArg = transformComponentChildren();
|
|
1682
1857
|
|
|
1683
|
-
return t.callExpression(t.identifier('_$createComponent'), [
|
|
1858
|
+
return t.callExpression(t.identifier('_$createComponent'), [componentRef, propsExpr, childrenArg]);
|
|
1684
1859
|
}
|
|
1685
1860
|
|
|
1686
1861
|
function transformForFineGrained(path, state) {
|
|
@@ -1811,32 +1986,7 @@ export default function whatBabelPlugin({ types: t }) {
|
|
|
1811
1986
|
// () => { const _v = <condition>; return _v ? <consequent> : <alternate>; }
|
|
1812
1987
|
// Hoisting into a local prevents double-evaluation of the `when` signal
|
|
1813
1988
|
// (the consequent's render callback also needs the resolved value).
|
|
1814
|
-
|
|
1815
|
-
// `whenExpr` shape determines how we form the condition:
|
|
1816
|
-
// - call expression → use as-is <Show when={cond()}>
|
|
1817
|
-
// - arrow w/ expression body → use the body <Show when={() => x > 5}>
|
|
1818
|
-
// - identifier that looks like a signal/import <Show when={isOpen}>
|
|
1819
|
-
// → invoke it as accessor: isOpen()
|
|
1820
|
-
// - anything else (member, literal, logical, etc.) <Show when={user.isAdmin}>
|
|
1821
|
-
// → use the raw expression. Do NOT invoke —
|
|
1822
|
-
// non-functions would throw at runtime.
|
|
1823
|
-
let condition;
|
|
1824
|
-
if (t.isCallExpression(whenExpr)) {
|
|
1825
|
-
condition = whenExpr;
|
|
1826
|
-
} else if (t.isArrowFunctionExpression(whenExpr) && t.isExpression(whenExpr.body)) {
|
|
1827
|
-
condition = whenExpr.body;
|
|
1828
|
-
} else if (
|
|
1829
|
-
t.isIdentifier(whenExpr) &&
|
|
1830
|
-
(
|
|
1831
|
-
(state.signalNames && isSignalIdentifier(whenExpr.name, state.signalNames)) ||
|
|
1832
|
-
(state.importedIdentifiers && state.importedIdentifiers.has(whenExpr.name))
|
|
1833
|
-
)
|
|
1834
|
-
) {
|
|
1835
|
-
condition = t.callExpression(whenExpr, []);
|
|
1836
|
-
} else {
|
|
1837
|
-
// Plain boolean expression — member access, literal, logical, etc.
|
|
1838
|
-
condition = whenExpr;
|
|
1839
|
-
}
|
|
1989
|
+
let condition = buildWhenCondition(whenExpr, state);
|
|
1840
1990
|
|
|
1841
1991
|
const vId = path.scope
|
|
1842
1992
|
? path.scope.generateUidIdentifier('v')
|
|
@@ -1887,6 +2037,183 @@ export default function whatBabelPlugin({ types: t }) {
|
|
|
1887
2037
|
]));
|
|
1888
2038
|
}
|
|
1889
2039
|
|
|
2040
|
+
// A `when` prop's shape determines how the condition is formed:
|
|
2041
|
+
// - call expression → use as-is when={cond()}
|
|
2042
|
+
// - arrow w/ expression body → use the body when={() => x > 5}
|
|
2043
|
+
// - identifier that looks like a signal/import when={isOpen}
|
|
2044
|
+
// → invoke it as accessor: isOpen()
|
|
2045
|
+
// - anything else (member, literal, logical, etc.) when={user.isAdmin}
|
|
2046
|
+
// → use the raw expression. Do NOT invoke it:
|
|
2047
|
+
// non-functions would throw at runtime.
|
|
2048
|
+
function buildWhenCondition(whenExpr, state) {
|
|
2049
|
+
if (t.isCallExpression(whenExpr)) return whenExpr;
|
|
2050
|
+
if (t.isArrowFunctionExpression(whenExpr) && t.isExpression(whenExpr.body)) return whenExpr.body;
|
|
2051
|
+
if (
|
|
2052
|
+
t.isIdentifier(whenExpr) &&
|
|
2053
|
+
(
|
|
2054
|
+
(state.signalNames && isSignalIdentifier(whenExpr.name, state.signalNames)) ||
|
|
2055
|
+
(state.importedIdentifiers && state.importedIdentifiers.has(whenExpr.name))
|
|
2056
|
+
)
|
|
2057
|
+
) {
|
|
2058
|
+
return t.callExpression(whenExpr, []);
|
|
2059
|
+
}
|
|
2060
|
+
return whenExpr;
|
|
2061
|
+
}
|
|
2062
|
+
|
|
2063
|
+
function collectControlFlowContent(children, state) {
|
|
2064
|
+
const parts = [];
|
|
2065
|
+
for (const child of children) {
|
|
2066
|
+
if (t.isJSXText(child)) {
|
|
2067
|
+
const text = normalizeJsxText(child.value);
|
|
2068
|
+
if (text) parts.push(t.stringLiteral(text));
|
|
2069
|
+
} else if (t.isJSXExpressionContainer(child)) {
|
|
2070
|
+
if (!t.isJSXEmptyExpression(child.expression)) parts.push(child.expression);
|
|
2071
|
+
} else if (t.isJSXElement(child)) {
|
|
2072
|
+
parts.push(transformElementFineGrained({ node: child }, state));
|
|
2073
|
+
} else if (t.isJSXFragment(child)) {
|
|
2074
|
+
parts.push(transformFragmentFineGrained({ node: child }, state));
|
|
2075
|
+
}
|
|
2076
|
+
}
|
|
2077
|
+
if (parts.length === 0) return t.nullLiteral();
|
|
2078
|
+
if (parts.length === 1) return parts[0];
|
|
2079
|
+
return t.arrayExpression(parts);
|
|
2080
|
+
}
|
|
2081
|
+
|
|
2082
|
+
function transformSwitchFineGrained(path, state) {
|
|
2083
|
+
// <Switch fallback={alt}><Match when={a}>A</Match><Match when={b}>B</Match></Switch>
|
|
2084
|
+
// → () => a ? A : b ? B : alt
|
|
2085
|
+
//
|
|
2086
|
+
// Same reactive-thunk shape <Show> lowers to, for the same reason: the
|
|
2087
|
+
// component runs once, so the arm has to be picked inside a thunk that
|
|
2088
|
+
// insert() re-evaluates. The runtime Switch matches on Match marker vnodes,
|
|
2089
|
+
// which _$createComponent never produces (calling Match instead recurses),
|
|
2090
|
+
// so compiled JSX has no working runtime path.
|
|
2091
|
+
//
|
|
2092
|
+
// A <Switch> is provably What's when either its binding was imported from
|
|
2093
|
+
// What or it has literal <Match> arms; anything else with the same tag name
|
|
2094
|
+
// is somebody else's component and goes to the component path untouched.
|
|
2095
|
+
// Once it is provably ours, a shape whose arms cannot be read statically has
|
|
2096
|
+
// nowhere to go, since the runtime component would render its fallback and
|
|
2097
|
+
// nothing else forever. Fail that build rather than emit a silent blank.
|
|
2098
|
+
const { node } = path;
|
|
2099
|
+
|
|
2100
|
+
// A <Switch> that got here on its arms alone came from the same foreign
|
|
2101
|
+
// specifier its arms did, so its <Match> is ours by the same reasoning.
|
|
2102
|
+
const armsProveIt = isForeignTag(node.openingElement.name.name, state);
|
|
2103
|
+
const isMatch = (child) =>
|
|
2104
|
+
t.isJSXElement(child) && t.isJSXIdentifier(child.openingElement.name, { name: 'Match' }) &&
|
|
2105
|
+
(armsProveIt || !isForeignTag('Match', state));
|
|
2106
|
+
if (!node.children.some(isMatch) && !isWhatTag(node.openingElement.name.name, state)) {
|
|
2107
|
+
return transformComponentFineGrained(path, state);
|
|
2108
|
+
}
|
|
2109
|
+
|
|
2110
|
+
const unsupported = (why) => {
|
|
2111
|
+
const message =
|
|
2112
|
+
`<Switch> ${why}. The compiler needs to read its arms statically. ` +
|
|
2113
|
+
'Write them out: <Switch fallback={...}><Match when={a}>…</Match><Match when={b}>…</Match></Switch>';
|
|
2114
|
+
throw path.buildCodeFrameError
|
|
2115
|
+
? path.buildCodeFrameError(message)
|
|
2116
|
+
: new Error(`[what-compiler] ${message}`);
|
|
2117
|
+
};
|
|
2118
|
+
|
|
2119
|
+
let fallbackExpr = null;
|
|
2120
|
+
for (const attr of node.openingElement.attributes) {
|
|
2121
|
+
if (t.isJSXSpreadAttribute(attr)) unsupported('cannot take a spread attribute');
|
|
2122
|
+
if (getAttrName(attr) === 'fallback') fallbackExpr = getAttributeValue(attr.value);
|
|
2123
|
+
}
|
|
2124
|
+
|
|
2125
|
+
const arms = [];
|
|
2126
|
+
for (const child of node.children) {
|
|
2127
|
+
if (t.isJSXText(child)) {
|
|
2128
|
+
if (normalizeJsxText(child.value)) unsupported('cannot mix text with its <Match> arms');
|
|
2129
|
+
continue;
|
|
2130
|
+
}
|
|
2131
|
+
if (t.isJSXExpressionContainer(child)) {
|
|
2132
|
+
if (t.isJSXEmptyExpression(child.expression)) continue;
|
|
2133
|
+
unsupported('cannot read its arms from an expression child');
|
|
2134
|
+
}
|
|
2135
|
+
if (!isMatch(child)) {
|
|
2136
|
+
unsupported('cannot mix other elements with its <Match> arms');
|
|
2137
|
+
}
|
|
2138
|
+
arms.push(child);
|
|
2139
|
+
}
|
|
2140
|
+
|
|
2141
|
+
if (arms.length === 0) unsupported('has no <Match> arms');
|
|
2142
|
+
|
|
2143
|
+
if (!state._pendingSetup) state._pendingSetup = [];
|
|
2144
|
+
const branches = [];
|
|
2145
|
+
|
|
2146
|
+
for (const arm of arms) {
|
|
2147
|
+
let whenExpr = null;
|
|
2148
|
+
for (const attr of arm.openingElement.attributes) {
|
|
2149
|
+
if (t.isJSXAttribute(attr) && getAttrName(attr) === 'when') whenExpr = getAttributeValue(attr.value);
|
|
2150
|
+
}
|
|
2151
|
+
if (!whenExpr) {
|
|
2152
|
+
unsupported('has a <Match> with no "when" prop');
|
|
2153
|
+
}
|
|
2154
|
+
|
|
2155
|
+
// An arm's element setup belongs inside the arm. Hoisting it next to the
|
|
2156
|
+
// thunk, the way a lone element does, would build every arm's DOM on the
|
|
2157
|
+
// way to picking one.
|
|
2158
|
+
const setupMark = state._pendingSetup.length;
|
|
2159
|
+
let content = collectControlFlowContent(arm.children, state);
|
|
2160
|
+
const setup = state._pendingSetup.splice(setupMark);
|
|
2161
|
+
if (setup.length > 0) {
|
|
2162
|
+
content = t.callExpression(
|
|
2163
|
+
t.arrowFunctionExpression([], t.blockStatement([...setup, t.returnStatement(content)])),
|
|
2164
|
+
[]
|
|
2165
|
+
);
|
|
2166
|
+
}
|
|
2167
|
+
|
|
2168
|
+
branches.push([buildWhenCondition(whenExpr, state), content]);
|
|
2169
|
+
}
|
|
2170
|
+
|
|
2171
|
+
const anyReactive = branches.some(([condition]) =>
|
|
2172
|
+
isPotentiallyReactive(condition, state.signalNames, state.importedIdentifiers));
|
|
2173
|
+
|
|
2174
|
+
const fallback = fallbackExpr || t.nullLiteral();
|
|
2175
|
+
|
|
2176
|
+
if (!anyReactive) {
|
|
2177
|
+
let expr = fallback;
|
|
2178
|
+
for (let i = branches.length - 1; i >= 0; i--) {
|
|
2179
|
+
expr = t.conditionalExpression(branches[i][0], branches[i][1], expr);
|
|
2180
|
+
}
|
|
2181
|
+
return t.arrowFunctionExpression([], expr);
|
|
2182
|
+
}
|
|
2183
|
+
|
|
2184
|
+
// ONE memo over the whole chain, not one per arm. _$memo evaluates its body
|
|
2185
|
+
// immediately, so a memo per arm would evaluate every arm's `when` up front,
|
|
2186
|
+
// while the runtime Switch stops at the first match. A single memo that
|
|
2187
|
+
// resolves to the winning arm's index keeps that short-circuit (`when` props
|
|
2188
|
+
// after the first truthy one are never read) and still gates re-rendering on
|
|
2189
|
+
// the selected arm actually changing.
|
|
2190
|
+
let selector = t.numericLiteral(-1);
|
|
2191
|
+
for (let i = branches.length - 1; i >= 0; i--) {
|
|
2192
|
+
selector = t.conditionalExpression(branches[i][0], t.numericLiteral(i), selector);
|
|
2193
|
+
}
|
|
2194
|
+
|
|
2195
|
+
const armId = state.nextMemoId();
|
|
2196
|
+
state.needsMemo = true;
|
|
2197
|
+
state._pendingSetup.push(
|
|
2198
|
+
t.variableDeclaration('const', [
|
|
2199
|
+
t.variableDeclarator(
|
|
2200
|
+
t.identifier(armId),
|
|
2201
|
+
t.callExpression(t.identifier('_$memo'), [t.arrowFunctionExpression([], selector)])
|
|
2202
|
+
)
|
|
2203
|
+
])
|
|
2204
|
+
);
|
|
2205
|
+
|
|
2206
|
+
let expr = fallback;
|
|
2207
|
+
for (let i = branches.length - 1; i >= 0; i--) {
|
|
2208
|
+
expr = t.conditionalExpression(
|
|
2209
|
+
t.binaryExpression('===', t.callExpression(t.identifier(armId), []), t.numericLiteral(i)),
|
|
2210
|
+
branches[i][1],
|
|
2211
|
+
expr
|
|
2212
|
+
);
|
|
2213
|
+
}
|
|
2214
|
+
return t.arrowFunctionExpression([], expr);
|
|
2215
|
+
}
|
|
2216
|
+
|
|
1890
2217
|
// A fragment-as-root returns an array (or single value) that the runtime
|
|
1891
2218
|
// mounts element-by-element (createDOM / insert). Unlike the element-child
|
|
1892
2219
|
// path there's no host element to _$insert into with a marker, so reactive
|
|
@@ -2231,14 +2558,22 @@ export default function whatBabelPlugin({ types: t }) {
|
|
|
2231
2558
|
// (user stores), or functions matching use*/create* naming conventions.
|
|
2232
2559
|
// This prevents over-wrapping of utility imports (lodash, etc.).
|
|
2233
2560
|
state.importedIdentifiers = new Set();
|
|
2561
|
+
// <Switch>, <Show>, <For> and <Match> are dispatched on the bare tag
|
|
2562
|
+
// name, and Ant Design, Headless UI and MUI all ship a <Switch>.
|
|
2563
|
+
// Recording which package a tag was imported from tells What's own
|
|
2564
|
+
// control flow apart from a third party's by provenance instead.
|
|
2565
|
+
state.whatImports = new Set();
|
|
2566
|
+
state.foreignImports = new Set();
|
|
2234
2567
|
for (const node of path.node.body) {
|
|
2235
2568
|
if (t.isImportDeclaration(node)) {
|
|
2236
2569
|
const source = node.source.value;
|
|
2237
|
-
const
|
|
2570
|
+
const isWhatSource =
|
|
2238
2571
|
source === 'what-framework' ||
|
|
2239
2572
|
source.startsWith('what-framework/') ||
|
|
2240
2573
|
source === 'what-core' ||
|
|
2241
|
-
source.startsWith('what-core/')
|
|
2574
|
+
source.startsWith('what-core/');
|
|
2575
|
+
const isReactiveSource =
|
|
2576
|
+
isWhatSource ||
|
|
2242
2577
|
source.startsWith('./') ||
|
|
2243
2578
|
source.startsWith('../');
|
|
2244
2579
|
|
|
@@ -2258,6 +2593,10 @@ export default function whatBabelPlugin({ types: t }) {
|
|
|
2258
2593
|
if (isReactiveSource || /^(use|create)[A-Z]/.test(localName)) {
|
|
2259
2594
|
state.importedIdentifiers.add(localName);
|
|
2260
2595
|
}
|
|
2596
|
+
// A relative import can be a re-export of What's own control
|
|
2597
|
+
// flow, so only a bare package specifier counts as foreign.
|
|
2598
|
+
if (isWhatSource) state.whatImports.add(localName);
|
|
2599
|
+
else if (!source.startsWith('.')) state.foreignImports.add(localName);
|
|
2261
2600
|
}
|
|
2262
2601
|
|
|
2263
2602
|
if (SERVER_ACTION_SOURCES.has(source)) {
|
|
@@ -2276,6 +2615,9 @@ export default function whatBabelPlugin({ types: t }) {
|
|
|
2276
2615
|
}
|
|
2277
2616
|
|
|
2278
2617
|
path.traverse({
|
|
2618
|
+
Function(fnPath) {
|
|
2619
|
+
deferForwardedChildren(fnPath);
|
|
2620
|
+
},
|
|
2279
2621
|
CallExpression(callPath) {
|
|
2280
2622
|
transformServerAction(callPath, state);
|
|
2281
2623
|
},
|