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.
@@ -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 && fnNode.params) {
331
- for (const param of fnNode.params) {
332
- if (t.isObjectPattern(param)) {
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
- const tagName = el.name.name;
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 (isComponent(tagName)) return '';
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
- if (name.startsWith('on') || name.startsWith('bind:') || name.includes('|')) continue;
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 (isComponent(child.openingElement.name.name)) {
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 — check before generic isComponent since they start uppercase
739
- if (tagName === 'For') {
740
- return transformForFineGrained(path, state);
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 === 'Show') {
743
- return transformShowFineGrained(path, state);
868
+ if (tagName === 'Switch' && (!isForeignTag(tagName, state) || hasLiteralMatchArms(node))) {
869
+ return transformSwitchFineGrained(path, state);
744
870
  }
745
871
 
746
- if (isComponent(tagName)) {
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 (isComponent(childTag) || childTag === 'For' || childTag === 'Show') {
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 componentName = openingElement.name.name;
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'), t.identifier(componentName)),
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)) continue;
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'), t.objectExpression(islandProps), t.arrayExpression([])]
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 childrenArray = transformedChildren.length > 0
1680
- ? t.arrayExpression(transformedChildren)
1681
- : t.arrayExpression([]);
1856
+ const childrenArg = transformComponentChildren();
1682
1857
 
1683
- return t.callExpression(t.identifier('_$createComponent'), [t.identifier(componentName), propsExpr, childrenArray]);
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 isReactiveSource =
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
  },