what-compiler 0.12.3 → 0.12.4
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/file-router.min.js +13 -3
- package/dist/index.min.js +16 -6
- package/dist/vite-plugin.min.js +16 -6
- package/package.json +2 -2
- package/src/babel-plugin.js +470 -19
- package/src/file-router.js +252 -19
package/src/babel-plugin.js
CHANGED
|
@@ -178,6 +178,43 @@ export default function whatBabelPlugin({ types: t }) {
|
|
|
178
178
|
return /^[A-Z]/.test(name);
|
|
179
179
|
}
|
|
180
180
|
|
|
181
|
+
// Build the Error for a JSX shape the compiler refuses to lower.
|
|
182
|
+
//
|
|
183
|
+
// Every fine-grained transform is reachable two ways: from the JSXElement
|
|
184
|
+
// visitor, where `path` is a real NodePath, and recursively from a parent
|
|
185
|
+
// transform, which passes a SYNTHETIC `{ node: child }` (see the
|
|
186
|
+
// transformElementFineGrained / transformFragmentFineGrained calls further
|
|
187
|
+
// down). Only the visitor's path carries buildCodeFrameError, so calling it
|
|
188
|
+
// unguarded threw "path.buildCodeFrameError is not a function" — an internal
|
|
189
|
+
// crash in place of the diagnostic the author was meant to act on. That hit
|
|
190
|
+
// the ordinary shapes, not the exotic ones: `<div><Show>…</Show></div>` is
|
|
191
|
+
// already synthetic, so only a control-flow tag that was the ENTIRE return
|
|
192
|
+
// value ever reported properly.
|
|
193
|
+
//
|
|
194
|
+
// Guarding the call is not enough either. `throw new Error(message)` reaches
|
|
195
|
+
// the developer, but with no file and no line, which for a build error in a
|
|
196
|
+
// 300-file app is barely better than the crash. state.file is Babel's File
|
|
197
|
+
// for the module being compiled and its buildCodeFrameError(node, msg) pins
|
|
198
|
+
// the frame from the node's own loc, so a synthetic path still gets the same
|
|
199
|
+
// pointed-at source the visitor path does.
|
|
200
|
+
function compileError(path, state, message) {
|
|
201
|
+
if (typeof path?.buildCodeFrameError === 'function') {
|
|
202
|
+
return path.buildCodeFrameError(message);
|
|
203
|
+
}
|
|
204
|
+
const node = path?.node;
|
|
205
|
+
if (node && typeof state?.file?.buildCodeFrameError === 'function') {
|
|
206
|
+
return state.file.buildCodeFrameError(node, message);
|
|
207
|
+
}
|
|
208
|
+
// No NodePath and no File (a direct unit-test call, or a node the parser
|
|
209
|
+
// never produced). Name the file and line by hand so the message still
|
|
210
|
+
// says where to look.
|
|
211
|
+
const loc = node?.loc?.start;
|
|
212
|
+
const filename = state?.filename || state?.file?.opts?.filename || '<unknown>';
|
|
213
|
+
return new Error(
|
|
214
|
+
`[what-compiler] ${message} (${loc ? `${filename}:${loc.line}:${loc.column + 1}` : filename})`
|
|
215
|
+
);
|
|
216
|
+
}
|
|
217
|
+
|
|
181
218
|
// Dotted tags (<Ctx.Provider>, <Foo.Bar.Baz>) are always components.
|
|
182
219
|
function isComponentElement(el) {
|
|
183
220
|
const name = el.openingElement ? el.openingElement.name : el.name;
|
|
@@ -620,6 +657,111 @@ export default function whatBabelPlugin({ types: t }) {
|
|
|
620
657
|
return false;
|
|
621
658
|
}
|
|
622
659
|
|
|
660
|
+
// Names bound by a parameter pattern. Destructuring counts, so
|
|
661
|
+
// `({ id }, i) => <li key={id}>` binds `id` and a key of `id` resolves fine.
|
|
662
|
+
function collectPatternNames(node, out) {
|
|
663
|
+
if (!node || typeof node !== 'object') return out;
|
|
664
|
+
switch (node.type) {
|
|
665
|
+
case 'Identifier': out.add(node.name); break;
|
|
666
|
+
case 'AssignmentPattern': collectPatternNames(node.left, out); break;
|
|
667
|
+
case 'RestElement': collectPatternNames(node.argument, out); break;
|
|
668
|
+
case 'ArrayPattern':
|
|
669
|
+
for (const el of node.elements) collectPatternNames(el, out);
|
|
670
|
+
break;
|
|
671
|
+
case 'ObjectPattern':
|
|
672
|
+
for (const p of node.properties) {
|
|
673
|
+
collectPatternNames(p.type === 'RestElement' ? p.argument : p.value, out);
|
|
674
|
+
}
|
|
675
|
+
break;
|
|
676
|
+
default: break;
|
|
677
|
+
}
|
|
678
|
+
return out;
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
// The key function is extracted OUT of the map callback and rebuilt as
|
|
682
|
+
// `(item) => keyExpr`, taking only the first parameter. So any name the key
|
|
683
|
+
// expression gets from somewhere else inside the callback becomes a free
|
|
684
|
+
// variable in the emitted code. This collects exactly those names: the
|
|
685
|
+
// parameters after the first, and everything declared in the callback body
|
|
686
|
+
// that is in scope at the return.
|
|
687
|
+
//
|
|
688
|
+
// Nested function bodies are deliberately not descended into. Their bindings
|
|
689
|
+
// cannot reach the return expression, so counting them would reject keys that
|
|
690
|
+
// are perfectly fine. A nested function's own name is hoisted, so that one
|
|
691
|
+
// is collected.
|
|
692
|
+
function namesInvisibleToKeyFn(mapFn) {
|
|
693
|
+
const out = new Set();
|
|
694
|
+
for (let i = 1; i < mapFn.params.length; i++) collectPatternNames(mapFn.params[i], out);
|
|
695
|
+
|
|
696
|
+
const visit = (node) => {
|
|
697
|
+
if (!node || typeof node !== 'object') return;
|
|
698
|
+
if (Array.isArray(node)) { node.forEach(visit); return; }
|
|
699
|
+
switch (node.type) {
|
|
700
|
+
case 'VariableDeclaration':
|
|
701
|
+
for (const d of node.declarations) collectPatternNames(d.id, out);
|
|
702
|
+
return;
|
|
703
|
+
case 'FunctionDeclaration':
|
|
704
|
+
case 'ClassDeclaration':
|
|
705
|
+
if (node.id) out.add(node.id.name);
|
|
706
|
+
return; // body is its own scope
|
|
707
|
+
case 'FunctionExpression':
|
|
708
|
+
case 'ArrowFunctionExpression':
|
|
709
|
+
case 'ClassExpression':
|
|
710
|
+
return; // own scope
|
|
711
|
+
case 'CatchClause':
|
|
712
|
+
collectPatternNames(node.param, out);
|
|
713
|
+
visit(node.body);
|
|
714
|
+
return;
|
|
715
|
+
case 'ForStatement':
|
|
716
|
+
case 'ForInStatement':
|
|
717
|
+
case 'ForOfStatement':
|
|
718
|
+
visit(node.init); visit(node.left); visit(node.body);
|
|
719
|
+
return;
|
|
720
|
+
default: break;
|
|
721
|
+
}
|
|
722
|
+
for (const k of Object.keys(node)) {
|
|
723
|
+
if (k === 'loc' || k === 'start' || k === 'end' || k === 'type' ||
|
|
724
|
+
k === 'leadingComments' || k === 'trailingComments' || k === 'innerComments') continue;
|
|
725
|
+
const v = node[k];
|
|
726
|
+
if (v && typeof v === 'object') visit(v);
|
|
727
|
+
}
|
|
728
|
+
};
|
|
729
|
+
|
|
730
|
+
if (t.isBlockStatement(mapFn.body)) visit(mapFn.body.body);
|
|
731
|
+
|
|
732
|
+
// The first parameter IS passed to the key function, so anything it binds
|
|
733
|
+
// stays visible even if a nested scope happens to reuse the name.
|
|
734
|
+
for (const name of collectPatternNames(mapFn.params[0], new Set())) out.delete(name);
|
|
735
|
+
return out;
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
// Does `node` reference any of `names` as a value? Property positions are
|
|
739
|
+
// skipped so `item.i` does not look like a use of `i`, but the check is
|
|
740
|
+
// otherwise deliberately generous: a false positive costs keyed
|
|
741
|
+
// reconciliation, a false negative ships a ReferenceError.
|
|
742
|
+
function referencesAny(node, names) {
|
|
743
|
+
if (names.size === 0) return null;
|
|
744
|
+
let found = null;
|
|
745
|
+
const visit = (n) => {
|
|
746
|
+
if (found || !n || typeof n !== 'object') return;
|
|
747
|
+
if (Array.isArray(n)) { n.forEach(visit); return; }
|
|
748
|
+
if (n.type === 'Identifier') {
|
|
749
|
+
if (names.has(n.name)) found = n.name;
|
|
750
|
+
return;
|
|
751
|
+
}
|
|
752
|
+
for (const k of Object.keys(n)) {
|
|
753
|
+
if (k === 'loc' || k === 'start' || k === 'end' || k === 'type' ||
|
|
754
|
+
k === 'leadingComments' || k === 'trailingComments' || k === 'innerComments') continue;
|
|
755
|
+
if (n.type === 'MemberExpression' && k === 'property' && !n.computed) continue;
|
|
756
|
+
if ((n.type === 'ObjectProperty' || n.type === 'ObjectMethod') && k === 'key' && !n.computed) continue;
|
|
757
|
+
const v = n[k];
|
|
758
|
+
if (v && typeof v === 'object') visit(v);
|
|
759
|
+
}
|
|
760
|
+
};
|
|
761
|
+
visit(node);
|
|
762
|
+
return found;
|
|
763
|
+
}
|
|
764
|
+
|
|
623
765
|
// --- Auto-lower .map() to mapArray ---
|
|
624
766
|
// Detects: source().map((item) => <Comp key={expr} .../>)
|
|
625
767
|
// or wrapped in an arrow: () => source().map(...)
|
|
@@ -707,14 +849,23 @@ export default function whatBabelPlugin({ types: t }) {
|
|
|
707
849
|
// JSX returned without a key — bail out, but warn at compile time so
|
|
708
850
|
// users notice they're missing keyed reconciliation. Only warn in dev
|
|
709
851
|
// (production builds are noiseless).
|
|
852
|
+
//
|
|
853
|
+
// The message names ERR_MISSING_KEY because that is the code the docs and
|
|
854
|
+
// the MCP `what_fix` tool already publish for this exact mistake. Without
|
|
855
|
+
// the code in the text there was nothing anywhere in the toolchain that
|
|
856
|
+
// ever emitted it, so an agent reading the warning had no way to reach
|
|
857
|
+
// the diagnosis and the worked example filed under that name. This is a
|
|
858
|
+
// BUILD-time report, not a runtime one: whether a list has keys is
|
|
859
|
+
// settled by the source, and the compiler is the only place that sees it.
|
|
710
860
|
if (process.env.NODE_ENV !== 'production') {
|
|
711
861
|
const loc = returnExpr.loc;
|
|
712
862
|
const fileName = state.filename || state.file?.opts?.filename || '<unknown>';
|
|
713
863
|
const lineInfo = loc ? `:${loc.start.line}:${loc.start.column}` : '';
|
|
714
864
|
console.warn(
|
|
715
|
-
`[what-compiler] .map() returning JSX without a \`key\` prop at ${fileName}${lineInfo}. ` +
|
|
865
|
+
`[what-compiler] ERR_MISSING_KEY: .map() returning JSX without a \`key\` prop at ${fileName}${lineInfo}. ` +
|
|
716
866
|
`Without a key, the list cannot use keyed reconciliation — items are re-created on every update. ` +
|
|
717
|
-
`Add key={...} to enable efficient updates
|
|
867
|
+
`Add key={...} to enable efficient updates. ` +
|
|
868
|
+
`Run what_fix({ error: 'ERR_MISSING_KEY' }) for the worked example.`
|
|
718
869
|
);
|
|
719
870
|
}
|
|
720
871
|
return null;
|
|
@@ -724,6 +875,60 @@ export default function whatBabelPlugin({ types: t }) {
|
|
|
724
875
|
const keyValue = getAttributeValue(keyAttr.value);
|
|
725
876
|
if (!keyValue) return null;
|
|
726
877
|
|
|
878
|
+
// The key function is rebuilt as `(item) => keyExpr` and hoisted out of the
|
|
879
|
+
// callback, so it only sees the first parameter. A key built from the index
|
|
880
|
+
// (`key={i}`, the pattern the tutorial itself taught) compiled to
|
|
881
|
+
// `key: t => i` with `i` free, and the failure was about as bad as it gets:
|
|
882
|
+
// the list rendered correctly the first time, then the reconciler threw
|
|
883
|
+
// `ReferenceError: i is not defined` inside its effect, the effect error
|
|
884
|
+
// handler swallowed it to a single console.error, and the list stayed
|
|
885
|
+
// frozen on its first render forever. Nothing visibly crashed. A key built
|
|
886
|
+
// from a variable declared in the callback body failed the same way.
|
|
887
|
+
//
|
|
888
|
+
// Falling back is also the semantically right answer, not just the safe
|
|
889
|
+
// one. A key derived from the index IS the position, so it carries no
|
|
890
|
+
// identity across an update: an item that moves gets a different key and
|
|
891
|
+
// reads as a different item. That is precisely what the unkeyed path
|
|
892
|
+
// already does, correctly and more cheaply, so hand it that.
|
|
893
|
+
const invisible = namesInvisibleToKeyFn(mapFn);
|
|
894
|
+
const unreachable = referencesAny(keyValue, invisible);
|
|
895
|
+
if (unreachable) {
|
|
896
|
+
// A BARE index key (`key={i}`, exactly the second parameter and nothing
|
|
897
|
+
// else) is deliberate and now behaves correctly, so it gets no warning.
|
|
898
|
+
// The author wrote "position is identity", positional reconciliation is
|
|
899
|
+
// precisely that, and there is no edit that would improve the output. The
|
|
900
|
+
// framework's own tutorial keys a fixed nine-square board this way, where
|
|
901
|
+
// it is not merely acceptable but the right answer, and a build-time
|
|
902
|
+
// warning on step two of a beginner tutorial reads as something being
|
|
903
|
+
// broken when nothing is.
|
|
904
|
+
//
|
|
905
|
+
// Everything else still warns, because everything else looks stable and is
|
|
906
|
+
// not: `key={`${t.type}-${i}`}` reads like a composite identity but changes
|
|
907
|
+
// the moment a row moves, and a key built from a variable declared in the
|
|
908
|
+
// callback body would have been a ReferenceError before this fallback
|
|
909
|
+
// existed. Those are worth interrupting someone over. A bare index is not.
|
|
910
|
+
const isBareIndex = t.isIdentifier(keyValue)
|
|
911
|
+
&& mapFn.params[1]
|
|
912
|
+
&& t.isIdentifier(mapFn.params[1], { name: keyValue.name });
|
|
913
|
+
|
|
914
|
+
if (!isBareIndex && process.env.NODE_ENV !== 'production') {
|
|
915
|
+
const loc = returnExpr.loc;
|
|
916
|
+
const fileName = state.filename || state.file?.opts?.filename || '<unknown>';
|
|
917
|
+
const lineInfo = loc ? `:${loc.start.line}:${loc.start.column}` : '';
|
|
918
|
+
const usesIndex = mapFn.params[1] && t.isIdentifier(mapFn.params[1], { name: unreachable });
|
|
919
|
+
console.warn(
|
|
920
|
+
`[what-compiler] key={...} at ${fileName}${lineInfo} reads \`${unreachable}\`, which is not available ` +
|
|
921
|
+
`to the key function (it only receives the item). ` +
|
|
922
|
+
(usesIndex
|
|
923
|
+
? `A key built from the index is the item's position, not its identity, so it cannot survive a reorder. `
|
|
924
|
+
: `\`${unreachable}\` is declared inside the map callback. `) +
|
|
925
|
+
`Falling back to positional reconciliation. Key by something stable on the item (key={item.id}) ` +
|
|
926
|
+
`to get keyed reconciliation.`
|
|
927
|
+
);
|
|
928
|
+
}
|
|
929
|
+
return null;
|
|
930
|
+
}
|
|
931
|
+
|
|
727
932
|
// Remove the key prop from the JSX element (mapArray handles keying, not the DOM)
|
|
728
933
|
returnExpr.openingElement.attributes = attrs.filter(a => a !== keyAttr);
|
|
729
934
|
|
|
@@ -1325,6 +1530,31 @@ export default function whatBabelPlugin({ types: t }) {
|
|
|
1325
1530
|
return false;
|
|
1326
1531
|
}
|
|
1327
1532
|
|
|
1533
|
+
// Does `path` sit in a slot its parent only evaluates SOMETIMES?
|
|
1534
|
+
//
|
|
1535
|
+
// Used to decide whether a JSX root's setup statements may be hoisted out to
|
|
1536
|
+
// the enclosing statement. Anything in one of these slots must not be: the
|
|
1537
|
+
// source deliberately said "only build this when the guard opens", and the
|
|
1538
|
+
// hoist turns that into "always build this".
|
|
1539
|
+
//
|
|
1540
|
+
// Only the slots that are genuinely skipped count. A logical expression's
|
|
1541
|
+
// LEFT operand and a ternary's TEST always run, so JSX there is not guarded
|
|
1542
|
+
// and keeps the cheaper hoist.
|
|
1543
|
+
function isConditionallyEvaluated(path) {
|
|
1544
|
+
const parent = path.parent;
|
|
1545
|
+
if (!parent) return false;
|
|
1546
|
+
if (t.isLogicalExpression(parent)) return path.key === 'right';
|
|
1547
|
+
if (t.isConditionalExpression(parent)) {
|
|
1548
|
+
return path.key === 'consequent' || path.key === 'alternate';
|
|
1549
|
+
}
|
|
1550
|
+
// `maybe?.render(<jsx/>)` skips its whole argument list when the chain
|
|
1551
|
+
// short-circuits, so an argument is guarded; the callee/object is not.
|
|
1552
|
+
if (t.isOptionalCallExpression(parent) || t.isOptionalMemberExpression(parent)) {
|
|
1553
|
+
return path.key !== 'callee' && path.key !== 'object';
|
|
1554
|
+
}
|
|
1555
|
+
return false;
|
|
1556
|
+
}
|
|
1557
|
+
|
|
1328
1558
|
// If `expr` is a conditional (ternary / && / ||) with a reactive test and a
|
|
1329
1559
|
// DOM-producing branch, hoist the test into `const _c$N = _$memo(...)` (pushed
|
|
1330
1560
|
// onto `statements`) and return the expression rewritten to read the memo.
|
|
@@ -1912,11 +2142,13 @@ export default function whatBabelPlugin({ types: t }) {
|
|
|
1912
2142
|
|
|
1913
2143
|
let eachExpr = null;
|
|
1914
2144
|
let keyExpr = null;
|
|
2145
|
+
let fallbackExpr = null;
|
|
1915
2146
|
for (const attr of attributes) {
|
|
1916
2147
|
if (t.isJSXAttribute(attr)) {
|
|
1917
2148
|
const name = getAttrName(attr);
|
|
1918
2149
|
if (name === 'each') eachExpr = getAttributeValue(attr.value);
|
|
1919
2150
|
else if (name === 'key') keyExpr = getAttributeValue(attr.value);
|
|
2151
|
+
else if (name === 'fallback') fallbackExpr = getAttributeValue(attr.value);
|
|
1920
2152
|
}
|
|
1921
2153
|
}
|
|
1922
2154
|
|
|
@@ -1941,13 +2173,184 @@ export default function whatBabelPlugin({ types: t }) {
|
|
|
1941
2173
|
}
|
|
1942
2174
|
|
|
1943
2175
|
state.needsMapArray = true;
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
|
|
2176
|
+
|
|
2177
|
+
// `_$mapArray(source, renderFn[, { key }])` — built from whichever source
|
|
2178
|
+
// expression the caller wants, because the fallback lowering below has to
|
|
2179
|
+
// read the source twice and therefore binds it to a const first.
|
|
2180
|
+
const buildMapArrayCall = (sourceExpr) => {
|
|
2181
|
+
const args = [sourceExpr, renderFn];
|
|
2182
|
+
if (keyExpr) {
|
|
2183
|
+
args.push(t.objectExpression([
|
|
2184
|
+
t.objectProperty(t.identifier('key'), keyExpr)
|
|
2185
|
+
]));
|
|
2186
|
+
}
|
|
2187
|
+
return t.callExpression(t.identifier('_$mapArray'), args);
|
|
2188
|
+
};
|
|
2189
|
+
|
|
2190
|
+
if (!fallbackExpr) {
|
|
2191
|
+
return buildMapArrayCall(eachExpr);
|
|
1949
2192
|
}
|
|
1950
|
-
|
|
2193
|
+
|
|
2194
|
+
// `fallback` used to be read by nobody: the loop above only looked at
|
|
2195
|
+
// `each` and `key`, so the attribute was silently discarded. The RUNTIME
|
|
2196
|
+
// For (packages/core/src/components.js) does implement it, so an app that
|
|
2197
|
+
// moved from a buildless setup to the Vite compiler lost its empty state
|
|
2198
|
+
// with nothing on the console.
|
|
2199
|
+
//
|
|
2200
|
+
// mapArray has no fallback of its own, and the obvious lowering — gate the
|
|
2201
|
+
// WHOLE thing on emptiness, `() => empty() ? _$mapArray(...) : fallback` —
|
|
2202
|
+
// is a trap. `insert()` has a fast path for a mapArray inserter: it
|
|
2203
|
+
// recognises `child._mapArray` and installs the list ONCE for the life of
|
|
2204
|
+
// the component. A thunk is not that, so the gated form falls through to
|
|
2205
|
+
// the generic reactive branch, whose effect calls the thunk again on every
|
|
2206
|
+
// flip and gets a BRAND NEW inserter each time. Nothing disposes the old
|
|
2207
|
+
// one, so it stays subscribed to the source forever: every empty -> fill
|
|
2208
|
+
// cycle permanently adds one more live list. Measured row-render-fn calls
|
|
2209
|
+
// per two writes, over successive cycles, went 2 -> 9 -> 16 -> 23 while
|
|
2210
|
+
// logging "NotFoundError: The child can not be found in the parent" from
|
|
2211
|
+
// the orphans reconciling against the detached fragment they captured. The
|
|
2212
|
+
// DOM stayed correct, which is why it looked fine.
|
|
2213
|
+
//
|
|
2214
|
+
// So keep the list permanent and give the fallback its OWN inserter beside
|
|
2215
|
+
// it, both anchored to the same marker:
|
|
2216
|
+
// _$insert(el, _$mapArray(each, fn), marker) // permanent
|
|
2217
|
+
// _$insert(el, () => empty() ? null : fallback, marker)
|
|
2218
|
+
// Only one of the two ever has content, and each reconciles independently.
|
|
2219
|
+
//
|
|
2220
|
+
// This function has to return a single expression, and a <For> can land in
|
|
2221
|
+
// a component's return, an element child, a component prop or a fragment.
|
|
2222
|
+
// Rather than teach all of those to emit two inserts, wrap the pair in one
|
|
2223
|
+
// inserter that performs both when it is mounted, and copy the list's own
|
|
2224
|
+
// marker properties onto it with Object.assign so it IS a mapArray inserter
|
|
2225
|
+
// to every consumer: `insert`, `createDOM`, `hydrateNode` and the server's
|
|
2226
|
+
// `_mapArrayToArray` all key off `_mapArray` and the `_mapArraySource` /
|
|
2227
|
+
// `_mapArrayFn` / `_mapArrayKeyed` trio. Object.assign copies exactly the
|
|
2228
|
+
// own enumerable properties mapArray sets, so this keeps working if the
|
|
2229
|
+
// runtime adds another one.
|
|
2230
|
+
//
|
|
2231
|
+
// Two more things this has to get right:
|
|
2232
|
+
// - The source expression is evaluated ONCE and shared, so the list and
|
|
2233
|
+
// the emptiness test can never disagree about which list they see.
|
|
2234
|
+
// - The emptiness test is MEMOIZED, so a write that leaves the list
|
|
2235
|
+
// non-empty does not re-run the fallback thunk and rebuild its DOM.
|
|
2236
|
+
if (!state._pendingSetup) state._pendingSetup = [];
|
|
2237
|
+
const forIndex = state.nextForIndex();
|
|
2238
|
+
const eachId = `_each$${forIndex}`;
|
|
2239
|
+
const listId = `_for$${forIndex}`;
|
|
2240
|
+
const fallbackId = `_fb$${forIndex}`;
|
|
2241
|
+
const emptyId = state.nextMemoId();
|
|
2242
|
+
state.needsMemo = true;
|
|
2243
|
+
state.needsInsert = true;
|
|
2244
|
+
|
|
2245
|
+
// `each` has to be a signal, or some other accessor. The typeof guard below
|
|
2246
|
+
// is NOT plain-array support and must not be read as advertising any: it
|
|
2247
|
+
// only keeps this memo total. `_$mapArray` calls its source, so
|
|
2248
|
+
// `each={["a","b"]}` throws "source is not a function" the moment the list
|
|
2249
|
+
// mounts — identically to the no-fallback lowering above, which hands the
|
|
2250
|
+
// same value straight to `_$mapArray`. The guard is here so that failure
|
|
2251
|
+
// arrives from the list, with that message, instead of from the emptiness
|
|
2252
|
+
// test first with a less obvious one.
|
|
2253
|
+
const resolvedId = path.scope
|
|
2254
|
+
? path.scope.generateUidIdentifier('list')
|
|
2255
|
+
: t.identifier('_list');
|
|
2256
|
+
const resolveList = t.conditionalExpression(
|
|
2257
|
+
t.binaryExpression('===',
|
|
2258
|
+
t.unaryExpression('typeof', t.identifier(eachId)),
|
|
2259
|
+
t.stringLiteral('function')
|
|
2260
|
+
),
|
|
2261
|
+
t.callExpression(t.identifier(eachId), []),
|
|
2262
|
+
t.identifier(eachId)
|
|
2263
|
+
);
|
|
2264
|
+
|
|
2265
|
+
state._pendingSetup.push(
|
|
2266
|
+
t.variableDeclaration('const', [
|
|
2267
|
+
t.variableDeclarator(t.identifier(eachId), eachExpr)
|
|
2268
|
+
]),
|
|
2269
|
+
t.variableDeclaration('const', [
|
|
2270
|
+
t.variableDeclarator(
|
|
2271
|
+
t.identifier(emptyId),
|
|
2272
|
+
t.callExpression(t.identifier('_$memo'), [
|
|
2273
|
+
t.arrowFunctionExpression([], t.blockStatement([
|
|
2274
|
+
t.variableDeclaration('const', [
|
|
2275
|
+
t.variableDeclarator(resolvedId, resolveList)
|
|
2276
|
+
]),
|
|
2277
|
+
t.returnStatement(
|
|
2278
|
+
t.unaryExpression('!', t.unaryExpression('!',
|
|
2279
|
+
t.logicalExpression('&&',
|
|
2280
|
+
t.cloneNode(resolvedId),
|
|
2281
|
+
t.memberExpression(t.cloneNode(resolvedId), t.identifier('length'))
|
|
2282
|
+
)
|
|
2283
|
+
))
|
|
2284
|
+
)
|
|
2285
|
+
]))
|
|
2286
|
+
])
|
|
2287
|
+
)
|
|
2288
|
+
]),
|
|
2289
|
+
t.variableDeclaration('const', [
|
|
2290
|
+
t.variableDeclarator(t.identifier(listId), buildMapArrayCall(t.identifier(eachId)))
|
|
2291
|
+
]),
|
|
2292
|
+
// The fallback thunk is hoisted rather than written inline in the
|
|
2293
|
+
// inserter below so that the inserter's body mentions no user code at
|
|
2294
|
+
// all. Its parameters can then be given fixed names without any risk of
|
|
2295
|
+
// capturing an identifier the fallback expression wanted from an outer
|
|
2296
|
+
// scope — this function is also called with a synthetic `{ node }` path
|
|
2297
|
+
// that carries no scope to generate a unique name from.
|
|
2298
|
+
t.variableDeclaration('const', [
|
|
2299
|
+
t.variableDeclarator(
|
|
2300
|
+
t.identifier(fallbackId),
|
|
2301
|
+
t.arrowFunctionExpression([], t.conditionalExpression(
|
|
2302
|
+
t.callExpression(t.identifier(emptyId), []),
|
|
2303
|
+
t.nullLiteral(),
|
|
2304
|
+
fallbackExpr
|
|
2305
|
+
))
|
|
2306
|
+
)
|
|
2307
|
+
])
|
|
2308
|
+
);
|
|
2309
|
+
|
|
2310
|
+
const parentParam = t.identifier('_parent');
|
|
2311
|
+
const markerParam = t.identifier('_marker');
|
|
2312
|
+
const endId = t.identifier('_end');
|
|
2313
|
+
return t.callExpression(
|
|
2314
|
+
t.memberExpression(t.identifier('Object'), t.identifier('assign')),
|
|
2315
|
+
[
|
|
2316
|
+
t.arrowFunctionExpression(
|
|
2317
|
+
[parentParam, markerParam],
|
|
2318
|
+
t.blockStatement([
|
|
2319
|
+
// The list goes in first so its `<!--/list-->` end marker sits
|
|
2320
|
+
// BEFORE the fallback: rows render above the empty state, and the
|
|
2321
|
+
// return value stays the end marker every other mapArray inserter
|
|
2322
|
+
// hands back.
|
|
2323
|
+
t.variableDeclaration('const', [
|
|
2324
|
+
t.variableDeclarator(endId, t.callExpression(t.identifier(listId), [
|
|
2325
|
+
t.cloneNode(parentParam),
|
|
2326
|
+
t.cloneNode(markerParam)
|
|
2327
|
+
]))
|
|
2328
|
+
]),
|
|
2329
|
+
// The thunk is wrapped in an ARRAY on purpose. `insert()` has two
|
|
2330
|
+
// ways to mount a function and only one of them survives here:
|
|
2331
|
+
// - passed bare, it takes insert's own reactive branch, whose
|
|
2332
|
+
// effect closes over the `parent` it was handed. When this
|
|
2333
|
+
// inserter is mounted through createDOM (a <For> as a component's
|
|
2334
|
+
// return value, at a fragment root, or inside a <Show> arm)
|
|
2335
|
+
// that parent is a throwaway DocumentFragment which is emptied
|
|
2336
|
+
// into the real container straight after. Every later run then
|
|
2337
|
+
// reconciles against a detached node, so the fallback was never
|
|
2338
|
+
// removed once rows arrived and stayed on screen underneath them.
|
|
2339
|
+
// - wrapped in an array, it goes through valuesToNodes -> createDOM,
|
|
2340
|
+
// whose reactive branch re-reads `endMarker.parentNode` on every
|
|
2341
|
+
// run. That is the same live-parent resolution mapArray does for
|
|
2342
|
+
// exactly this reason, so both halves survive being moved.
|
|
2343
|
+
t.expressionStatement(t.callExpression(t.identifier('_$insert'), [
|
|
2344
|
+
t.cloneNode(parentParam),
|
|
2345
|
+
t.arrayExpression([t.identifier(fallbackId)]),
|
|
2346
|
+
t.cloneNode(markerParam)
|
|
2347
|
+
])),
|
|
2348
|
+
t.returnStatement(t.cloneNode(endId))
|
|
2349
|
+
])
|
|
2350
|
+
),
|
|
2351
|
+
t.identifier(listId)
|
|
2352
|
+
]
|
|
2353
|
+
);
|
|
1951
2354
|
}
|
|
1952
2355
|
|
|
1953
2356
|
function transformShowFineGrained(path, state) {
|
|
@@ -1971,8 +2374,13 @@ export default function whatBabelPlugin({ types: t }) {
|
|
|
1971
2374
|
if (!whenExpr) {
|
|
1972
2375
|
// <Show> without a when prop has no defined semantics — fail loudly at
|
|
1973
2376
|
// build time so the user fixes their source instead of seeing runtime
|
|
1974
|
-
// confusion.
|
|
1975
|
-
|
|
2377
|
+
// confusion. compileError pins the message to the JSX location from
|
|
2378
|
+
// either a real NodePath or the synthetic `{ node }` a parent transform
|
|
2379
|
+
// passes; calling path.buildCodeFrameError directly crashed on the
|
|
2380
|
+
// second, which is the shape almost every real <Show> arrives as.
|
|
2381
|
+
throw compileError(
|
|
2382
|
+
path,
|
|
2383
|
+
state,
|
|
1976
2384
|
'<Show> requires a "when" prop. Example: <Show when={isOpen} fallback={null}>...</Show>'
|
|
1977
2385
|
);
|
|
1978
2386
|
}
|
|
@@ -1988,7 +2396,22 @@ export default function whatBabelPlugin({ types: t }) {
|
|
|
1988
2396
|
}
|
|
1989
2397
|
|
|
1990
2398
|
if (!contentExpr) {
|
|
1991
|
-
// Static children — collect and transform them
|
|
2399
|
+
// Static children — collect and transform them.
|
|
2400
|
+
//
|
|
2401
|
+
// The arm's element setup belongs INSIDE the arm, exactly as it does for
|
|
2402
|
+
// a <Match> arm (see the identical splice in transformSwitchFineGrained).
|
|
2403
|
+
// transformElementFineGrained pushes `const _el$N = _tmpl$X()` and every
|
|
2404
|
+
// binding it needs into state._pendingSetup, and _pendingSetup is drained
|
|
2405
|
+
// next to the thunk this function returns. So without the splice the arm
|
|
2406
|
+
// was built, and its bindings ran, alongside the enclosing component:
|
|
2407
|
+
// <Show when={user}><p>{() => user().name}</p></Show>
|
|
2408
|
+
// threw at first render with a null user, and logged an uncaught effect
|
|
2409
|
+
// error on every transition back to falsy because the binding stayed
|
|
2410
|
+
// subscribed. Uncompiled, the same JSX is lazy — children of the runtime
|
|
2411
|
+
// Show really are deferred — so this was a compiled-vs-runtime
|
|
2412
|
+
// divergence, not just a bug.
|
|
2413
|
+
if (!state._pendingSetup) state._pendingSetup = [];
|
|
2414
|
+
const setupMark = state._pendingSetup.length;
|
|
1992
2415
|
const transformedChildren = [];
|
|
1993
2416
|
for (const child of children) {
|
|
1994
2417
|
if (t.isJSXText(child)) {
|
|
@@ -2005,6 +2428,13 @@ export default function whatBabelPlugin({ types: t }) {
|
|
|
2005
2428
|
} else {
|
|
2006
2429
|
contentExpr = t.nullLiteral();
|
|
2007
2430
|
}
|
|
2431
|
+
const setup = state._pendingSetup.splice(setupMark);
|
|
2432
|
+
if (setup.length > 0) {
|
|
2433
|
+
contentExpr = t.callExpression(
|
|
2434
|
+
t.arrowFunctionExpression([], t.blockStatement([...setup, t.returnStatement(contentExpr)])),
|
|
2435
|
+
[]
|
|
2436
|
+
);
|
|
2437
|
+
}
|
|
2008
2438
|
}
|
|
2009
2439
|
|
|
2010
2440
|
// Build:
|
|
@@ -2133,12 +2563,16 @@ export default function whatBabelPlugin({ types: t }) {
|
|
|
2133
2563
|
}
|
|
2134
2564
|
|
|
2135
2565
|
const unsupported = (why) => {
|
|
2136
|
-
|
|
2566
|
+
// The guard this used to carry (`path.buildCodeFrameError ? … : new
|
|
2567
|
+
// Error(…)`) did keep the message reaching the developer from a
|
|
2568
|
+
// synthetic path, but dropped the file and the line with it. compileError
|
|
2569
|
+
// keeps both.
|
|
2570
|
+
throw compileError(
|
|
2571
|
+
path,
|
|
2572
|
+
state,
|
|
2137
2573
|
`<Switch> ${why}. The compiler needs to read its arms statically. ` +
|
|
2138
|
-
'Write them out: <Switch fallback={...}><Match when={a}>…</Match><Match when={b}>…</Match></Switch>'
|
|
2139
|
-
|
|
2140
|
-
? path.buildCodeFrameError(message)
|
|
2141
|
-
: new Error(`[what-compiler] ${message}`);
|
|
2574
|
+
'Write them out: <Switch fallback={...}><Match when={a}>…</Match><Match when={b}>…</Match></Switch>'
|
|
2575
|
+
);
|
|
2142
2576
|
};
|
|
2143
2577
|
|
|
2144
2578
|
let fallbackExpr = null;
|
|
@@ -2374,10 +2808,19 @@ export default function whatBabelPlugin({ types: t }) {
|
|
|
2374
2808
|
// hoist references to closure variables out of scope.
|
|
2375
2809
|
let stmtPath = path;
|
|
2376
2810
|
let crossedFunctionBoundary = false;
|
|
2811
|
+
// Setup hoisted out of a guard runs whether the guard opens or not.
|
|
2812
|
+
// `return user() && <p>{user().name}</p>` is the React shape everybody
|
|
2813
|
+
// writes, and hoisting built the <p> and ran `() => user().name` at
|
|
2814
|
+
// mount, so the very first render threw on a null user. Same for a
|
|
2815
|
+
// ternary arm and for a guarded arm sitting in a component prop.
|
|
2816
|
+
// A guarded arm has to be CONSTRUCTED INSIDE its guard, which is exactly
|
|
2817
|
+
// what the IIFE path below does.
|
|
2818
|
+
let crossedGuard = false;
|
|
2377
2819
|
while (stmtPath && !stmtPath.isStatement()) {
|
|
2378
2820
|
if (stmtPath.isArrowFunctionExpression() || stmtPath.isFunctionExpression()) {
|
|
2379
2821
|
crossedFunctionBoundary = true;
|
|
2380
2822
|
}
|
|
2823
|
+
if (isConditionallyEvaluated(stmtPath)) crossedGuard = true;
|
|
2381
2824
|
stmtPath = stmtPath.parentPath;
|
|
2382
2825
|
}
|
|
2383
2826
|
// We can safely hoist setup as siblings of `stmtPath` ONLY if
|
|
@@ -2398,15 +2841,16 @@ export default function whatBabelPlugin({ types: t }) {
|
|
|
2398
2841
|
&& stmtPath.isStatement()
|
|
2399
2842
|
&& (stmtPath.listKey === 'body' || stmtPath.listKey === 'consequent')
|
|
2400
2843
|
&& Array.isArray(stmtPath.container);
|
|
2401
|
-
if (inStatementList && !crossedFunctionBoundary) {
|
|
2844
|
+
if (inStatementList && !crossedFunctionBoundary && !crossedGuard) {
|
|
2402
2845
|
// Same function scope — safe to hoist setup before the enclosing
|
|
2403
2846
|
// statement. Works for return statements too: `insertBefore`
|
|
2404
2847
|
// places setup above `return <jsx/>` without wrapping in an IIFE.
|
|
2405
2848
|
stmtPath.insertBefore(pending);
|
|
2406
2849
|
path.replaceWith(transformed);
|
|
2407
2850
|
} else {
|
|
2408
|
-
// Crossed a function boundary or no enclosing statement
|
|
2409
|
-
// fall back to IIFE
|
|
2851
|
+
// Crossed a function boundary or a guard, or no enclosing statement
|
|
2852
|
+
// was found — fall back to IIFE, which keeps closure variables in
|
|
2853
|
+
// scope AND keeps the construction where the source put it.
|
|
2410
2854
|
pending.push(t.returnStatement(transformed));
|
|
2411
2855
|
path.replaceWith(
|
|
2412
2856
|
t.callExpression(
|
|
@@ -2562,9 +3006,16 @@ export default function whatBabelPlugin({ types: t }) {
|
|
|
2562
3006
|
state.templateCount = 0;
|
|
2563
3007
|
state._varCounter = 0;
|
|
2564
3008
|
state._memoCounter = 0;
|
|
3009
|
+
// Separate counter from nextVarId: a <For each> source is not an
|
|
3010
|
+
// element, and calling it _el$N would make the emitted code lie.
|
|
3011
|
+
// One index names the whole group a <For fallback> lowers to, so the
|
|
3012
|
+
// source (_each$N), the list inserter (_for$N) it feeds and the
|
|
3013
|
+
// fallback thunk (_fb$N) beside it are visibly the same <For>.
|
|
3014
|
+
state._listCounter = 0;
|
|
2565
3015
|
state._pendingSetup = [];
|
|
2566
3016
|
state.nextVarId = () => `_el$${state._varCounter++}`;
|
|
2567
3017
|
state.nextMemoId = () => `_c$${state._memoCounter++}`;
|
|
3018
|
+
state.nextForIndex = () => state._listCounter++;
|
|
2568
3019
|
|
|
2569
3020
|
state.serverActionBindings = new Set();
|
|
2570
3021
|
state.serverActionNamespaces = new Set();
|