what-compiler 0.11.7 → 0.11.8

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
 
@@ -1533,7 +1659,7 @@ export default function whatBabelPlugin({ types: t }) {
1533
1659
  state.needsIsland = true;
1534
1660
 
1535
1661
  const islandProps = [
1536
- t.objectProperty(t.identifier('component'), t.identifier(componentName)),
1662
+ t.objectProperty(t.identifier('component'), componentRef),
1537
1663
  t.objectProperty(t.identifier('mode'), t.stringLiteral(clientDirective.type)),
1538
1664
  ];
1539
1665
 
@@ -1643,19 +1769,34 @@ export default function whatBabelPlugin({ types: t }) {
1643
1769
  );
1644
1770
  }
1645
1771
 
1646
- // Transform children
1772
+ // Transform children.
1773
+ // Element children build their DOM at the call site, i.e. before the parent
1774
+ // component ever runs, so a <Provider> publishes its context value too late
1775
+ // for its own subtree. Collect the element setup and emit it behind a
1776
+ // zero-arg factory instead; the runtime calls the factory while the parent
1777
+ // is on the component stack. An expression child is deferred for the same
1778
+ // reason: it is the shape a wrapper forwards its own children through, and
1779
+ // reading it while the argument list is built happens before the component
1780
+ // it is being handed to has run. Text children have nothing to defer.
1781
+ const setupMark = state._pendingSetup ? state._pendingSetup.length : 0;
1647
1782
  const transformedChildren = [];
1783
+ let deferChildren = false;
1648
1784
  for (const child of children) {
1649
1785
  if (t.isJSXText(child)) {
1650
1786
  const text = normalizeJsxText(child.value);
1651
1787
  if (text) transformedChildren.push(t.stringLiteral(text));
1652
1788
  } else if (t.isJSXExpressionContainer(child)) {
1653
1789
  if (!t.isJSXEmptyExpression(child.expression)) {
1790
+ deferChildren = true;
1654
1791
  transformedChildren.push(child.expression);
1655
1792
  }
1656
1793
  } else if (t.isJSXElement(child)) {
1794
+ // <For>/<Show>/<Switch> lower to an inserter or a thunk (already lazy).
1795
+ const childTag = child.openingElement.name.name;
1796
+ if (childTag !== 'For' && childTag !== 'Show' && childTag !== 'Switch') deferChildren = true;
1657
1797
  transformedChildren.push(transformElementFineGrained({ node: child }, state));
1658
1798
  } else if (t.isJSXFragment(child)) {
1799
+ deferChildren = true;
1659
1800
  transformedChildren.push(transformFragmentFineGrained({ node: child }, state));
1660
1801
  }
1661
1802
  }
@@ -1676,11 +1817,20 @@ export default function whatBabelPlugin({ types: t }) {
1676
1817
  propsExpr = t.nullLiteral();
1677
1818
  }
1678
1819
 
1679
- const childrenArray = transformedChildren.length > 0
1680
- ? t.arrayExpression(transformedChildren)
1681
- : t.arrayExpression([]);
1820
+ let childrenArg = t.arrayExpression(transformedChildren);
1821
+ if (deferChildren) {
1822
+ // Hoisted element setup emitted while transforming these children belongs
1823
+ // inside the factory, not before the enclosing statement.
1824
+ const setup = state._pendingSetup ? state._pendingSetup.splice(setupMark) : [];
1825
+ childrenArg = t.arrowFunctionExpression(
1826
+ [],
1827
+ setup.length > 0
1828
+ ? t.blockStatement([...setup, t.returnStatement(childrenArg)])
1829
+ : childrenArg
1830
+ );
1831
+ }
1682
1832
 
1683
- return t.callExpression(t.identifier('_$createComponent'), [t.identifier(componentName), propsExpr, childrenArray]);
1833
+ return t.callExpression(t.identifier('_$createComponent'), [componentRef, propsExpr, childrenArg]);
1684
1834
  }
1685
1835
 
1686
1836
  function transformForFineGrained(path, state) {
@@ -1811,32 +1961,7 @@ export default function whatBabelPlugin({ types: t }) {
1811
1961
  // () => { const _v = <condition>; return _v ? <consequent> : <alternate>; }
1812
1962
  // Hoisting into a local prevents double-evaluation of the `when` signal
1813
1963
  // (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
- }
1964
+ let condition = buildWhenCondition(whenExpr, state);
1840
1965
 
1841
1966
  const vId = path.scope
1842
1967
  ? path.scope.generateUidIdentifier('v')
@@ -1887,6 +2012,183 @@ export default function whatBabelPlugin({ types: t }) {
1887
2012
  ]));
1888
2013
  }
1889
2014
 
2015
+ // A `when` prop's shape determines how the condition is formed:
2016
+ // - call expression → use as-is when={cond()}
2017
+ // - arrow w/ expression body → use the body when={() => x > 5}
2018
+ // - identifier that looks like a signal/import when={isOpen}
2019
+ // → invoke it as accessor: isOpen()
2020
+ // - anything else (member, literal, logical, etc.) when={user.isAdmin}
2021
+ // → use the raw expression. Do NOT invoke it:
2022
+ // non-functions would throw at runtime.
2023
+ function buildWhenCondition(whenExpr, state) {
2024
+ if (t.isCallExpression(whenExpr)) return whenExpr;
2025
+ if (t.isArrowFunctionExpression(whenExpr) && t.isExpression(whenExpr.body)) return whenExpr.body;
2026
+ if (
2027
+ t.isIdentifier(whenExpr) &&
2028
+ (
2029
+ (state.signalNames && isSignalIdentifier(whenExpr.name, state.signalNames)) ||
2030
+ (state.importedIdentifiers && state.importedIdentifiers.has(whenExpr.name))
2031
+ )
2032
+ ) {
2033
+ return t.callExpression(whenExpr, []);
2034
+ }
2035
+ return whenExpr;
2036
+ }
2037
+
2038
+ function collectControlFlowContent(children, state) {
2039
+ const parts = [];
2040
+ for (const child of children) {
2041
+ if (t.isJSXText(child)) {
2042
+ const text = normalizeJsxText(child.value);
2043
+ if (text) parts.push(t.stringLiteral(text));
2044
+ } else if (t.isJSXExpressionContainer(child)) {
2045
+ if (!t.isJSXEmptyExpression(child.expression)) parts.push(child.expression);
2046
+ } else if (t.isJSXElement(child)) {
2047
+ parts.push(transformElementFineGrained({ node: child }, state));
2048
+ } else if (t.isJSXFragment(child)) {
2049
+ parts.push(transformFragmentFineGrained({ node: child }, state));
2050
+ }
2051
+ }
2052
+ if (parts.length === 0) return t.nullLiteral();
2053
+ if (parts.length === 1) return parts[0];
2054
+ return t.arrayExpression(parts);
2055
+ }
2056
+
2057
+ function transformSwitchFineGrained(path, state) {
2058
+ // <Switch fallback={alt}><Match when={a}>A</Match><Match when={b}>B</Match></Switch>
2059
+ // → () => a ? A : b ? B : alt
2060
+ //
2061
+ // Same reactive-thunk shape <Show> lowers to, for the same reason: the
2062
+ // component runs once, so the arm has to be picked inside a thunk that
2063
+ // insert() re-evaluates. The runtime Switch matches on Match marker vnodes,
2064
+ // which _$createComponent never produces (calling Match instead recurses),
2065
+ // so compiled JSX has no working runtime path.
2066
+ //
2067
+ // A <Switch> is provably What's when either its binding was imported from
2068
+ // What or it has literal <Match> arms; anything else with the same tag name
2069
+ // is somebody else's component and goes to the component path untouched.
2070
+ // Once it is provably ours, a shape whose arms cannot be read statically has
2071
+ // nowhere to go, since the runtime component would render its fallback and
2072
+ // nothing else forever. Fail that build rather than emit a silent blank.
2073
+ const { node } = path;
2074
+
2075
+ // A <Switch> that got here on its arms alone came from the same foreign
2076
+ // specifier its arms did, so its <Match> is ours by the same reasoning.
2077
+ const armsProveIt = isForeignTag(node.openingElement.name.name, state);
2078
+ const isMatch = (child) =>
2079
+ t.isJSXElement(child) && t.isJSXIdentifier(child.openingElement.name, { name: 'Match' }) &&
2080
+ (armsProveIt || !isForeignTag('Match', state));
2081
+ if (!node.children.some(isMatch) && !isWhatTag(node.openingElement.name.name, state)) {
2082
+ return transformComponentFineGrained(path, state);
2083
+ }
2084
+
2085
+ const unsupported = (why) => {
2086
+ const message =
2087
+ `<Switch> ${why}. The compiler needs to read its arms statically. ` +
2088
+ 'Write them out: <Switch fallback={...}><Match when={a}>…</Match><Match when={b}>…</Match></Switch>';
2089
+ throw path.buildCodeFrameError
2090
+ ? path.buildCodeFrameError(message)
2091
+ : new Error(`[what-compiler] ${message}`);
2092
+ };
2093
+
2094
+ let fallbackExpr = null;
2095
+ for (const attr of node.openingElement.attributes) {
2096
+ if (t.isJSXSpreadAttribute(attr)) unsupported('cannot take a spread attribute');
2097
+ if (getAttrName(attr) === 'fallback') fallbackExpr = getAttributeValue(attr.value);
2098
+ }
2099
+
2100
+ const arms = [];
2101
+ for (const child of node.children) {
2102
+ if (t.isJSXText(child)) {
2103
+ if (normalizeJsxText(child.value)) unsupported('cannot mix text with its <Match> arms');
2104
+ continue;
2105
+ }
2106
+ if (t.isJSXExpressionContainer(child)) {
2107
+ if (t.isJSXEmptyExpression(child.expression)) continue;
2108
+ unsupported('cannot read its arms from an expression child');
2109
+ }
2110
+ if (!isMatch(child)) {
2111
+ unsupported('cannot mix other elements with its <Match> arms');
2112
+ }
2113
+ arms.push(child);
2114
+ }
2115
+
2116
+ if (arms.length === 0) unsupported('has no <Match> arms');
2117
+
2118
+ if (!state._pendingSetup) state._pendingSetup = [];
2119
+ const branches = [];
2120
+
2121
+ for (const arm of arms) {
2122
+ let whenExpr = null;
2123
+ for (const attr of arm.openingElement.attributes) {
2124
+ if (t.isJSXAttribute(attr) && getAttrName(attr) === 'when') whenExpr = getAttributeValue(attr.value);
2125
+ }
2126
+ if (!whenExpr) {
2127
+ unsupported('has a <Match> with no "when" prop');
2128
+ }
2129
+
2130
+ // An arm's element setup belongs inside the arm. Hoisting it next to the
2131
+ // thunk, the way a lone element does, would build every arm's DOM on the
2132
+ // way to picking one.
2133
+ const setupMark = state._pendingSetup.length;
2134
+ let content = collectControlFlowContent(arm.children, state);
2135
+ const setup = state._pendingSetup.splice(setupMark);
2136
+ if (setup.length > 0) {
2137
+ content = t.callExpression(
2138
+ t.arrowFunctionExpression([], t.blockStatement([...setup, t.returnStatement(content)])),
2139
+ []
2140
+ );
2141
+ }
2142
+
2143
+ branches.push([buildWhenCondition(whenExpr, state), content]);
2144
+ }
2145
+
2146
+ const anyReactive = branches.some(([condition]) =>
2147
+ isPotentiallyReactive(condition, state.signalNames, state.importedIdentifiers));
2148
+
2149
+ const fallback = fallbackExpr || t.nullLiteral();
2150
+
2151
+ if (!anyReactive) {
2152
+ let expr = fallback;
2153
+ for (let i = branches.length - 1; i >= 0; i--) {
2154
+ expr = t.conditionalExpression(branches[i][0], branches[i][1], expr);
2155
+ }
2156
+ return t.arrowFunctionExpression([], expr);
2157
+ }
2158
+
2159
+ // ONE memo over the whole chain, not one per arm. _$memo evaluates its body
2160
+ // immediately, so a memo per arm would evaluate every arm's `when` up front,
2161
+ // while the runtime Switch stops at the first match. A single memo that
2162
+ // resolves to the winning arm's index keeps that short-circuit (`when` props
2163
+ // after the first truthy one are never read) and still gates re-rendering on
2164
+ // the selected arm actually changing.
2165
+ let selector = t.numericLiteral(-1);
2166
+ for (let i = branches.length - 1; i >= 0; i--) {
2167
+ selector = t.conditionalExpression(branches[i][0], t.numericLiteral(i), selector);
2168
+ }
2169
+
2170
+ const armId = state.nextMemoId();
2171
+ state.needsMemo = true;
2172
+ state._pendingSetup.push(
2173
+ t.variableDeclaration('const', [
2174
+ t.variableDeclarator(
2175
+ t.identifier(armId),
2176
+ t.callExpression(t.identifier('_$memo'), [t.arrowFunctionExpression([], selector)])
2177
+ )
2178
+ ])
2179
+ );
2180
+
2181
+ let expr = fallback;
2182
+ for (let i = branches.length - 1; i >= 0; i--) {
2183
+ expr = t.conditionalExpression(
2184
+ t.binaryExpression('===', t.callExpression(t.identifier(armId), []), t.numericLiteral(i)),
2185
+ branches[i][1],
2186
+ expr
2187
+ );
2188
+ }
2189
+ return t.arrowFunctionExpression([], expr);
2190
+ }
2191
+
1890
2192
  // A fragment-as-root returns an array (or single value) that the runtime
1891
2193
  // mounts element-by-element (createDOM / insert). Unlike the element-child
1892
2194
  // path there's no host element to _$insert into with a marker, so reactive
@@ -2231,14 +2533,22 @@ export default function whatBabelPlugin({ types: t }) {
2231
2533
  // (user stores), or functions matching use*/create* naming conventions.
2232
2534
  // This prevents over-wrapping of utility imports (lodash, etc.).
2233
2535
  state.importedIdentifiers = new Set();
2536
+ // <Switch>, <Show>, <For> and <Match> are dispatched on the bare tag
2537
+ // name, and Ant Design, Headless UI and MUI all ship a <Switch>.
2538
+ // Recording which package a tag was imported from tells What's own
2539
+ // control flow apart from a third party's by provenance instead.
2540
+ state.whatImports = new Set();
2541
+ state.foreignImports = new Set();
2234
2542
  for (const node of path.node.body) {
2235
2543
  if (t.isImportDeclaration(node)) {
2236
2544
  const source = node.source.value;
2237
- const isReactiveSource =
2545
+ const isWhatSource =
2238
2546
  source === 'what-framework' ||
2239
2547
  source.startsWith('what-framework/') ||
2240
2548
  source === 'what-core' ||
2241
- source.startsWith('what-core/') ||
2549
+ source.startsWith('what-core/');
2550
+ const isReactiveSource =
2551
+ isWhatSource ||
2242
2552
  source.startsWith('./') ||
2243
2553
  source.startsWith('../');
2244
2554
 
@@ -2258,6 +2568,10 @@ export default function whatBabelPlugin({ types: t }) {
2258
2568
  if (isReactiveSource || /^(use|create)[A-Z]/.test(localName)) {
2259
2569
  state.importedIdentifiers.add(localName);
2260
2570
  }
2571
+ // A relative import can be a re-export of What's own control
2572
+ // flow, so only a bare package specifier counts as foreign.
2573
+ if (isWhatSource) state.whatImports.add(localName);
2574
+ else if (!source.startsWith('.')) state.foreignImports.add(localName);
2261
2575
  }
2262
2576
 
2263
2577
  if (SERVER_ACTION_SOURCES.has(source)) {
@@ -2276,6 +2590,9 @@ export default function whatBabelPlugin({ types: t }) {
2276
2590
  }
2277
2591
 
2278
2592
  path.traverse({
2593
+ Function(fnPath) {
2594
+ deferForwardedChildren(fnPath);
2595
+ },
2279
2596
  CallExpression(callPath) {
2280
2597
  transformServerAction(callPath, state);
2281
2598
  },