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.
- package/dist/babel-plugin.min.js +1 -1
- package/dist/index.min.js +6 -6
- package/dist/vite-plugin.min.js +6 -6
- package/package.json +2 -2
- package/src/babel-plugin.js +375 -58
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
|
|
|
@@ -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'),
|
|
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
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
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'), [
|
|
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
|
|
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
|
},
|