what-compiler 0.8.4 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/babel-plugin.js +314 -48
- package/dist/babel-plugin.js.map +2 -2
- package/dist/babel-plugin.min.js +1 -1
- package/dist/babel-plugin.min.js.map +3 -3
- package/dist/file-router.js +78 -2
- package/dist/file-router.js.map +2 -2
- package/dist/file-router.min.js +3 -2
- package/dist/file-router.min.js.map +3 -3
- package/dist/index.js +354 -52
- package/dist/index.js.map +2 -2
- package/dist/index.min.js +6 -6
- package/dist/index.min.js.map +3 -3
- package/dist/vite-plugin.js +354 -52
- package/dist/vite-plugin.js.map +2 -2
- package/dist/vite-plugin.min.js +6 -6
- package/dist/vite-plugin.min.js.map +3 -3
- package/package.json +2 -2
- package/src/babel-plugin.js +514 -65
- package/src/file-router.js +104 -2
- package/src/vite-plugin.js +28 -2
package/dist/index.js
CHANGED
|
@@ -61,9 +61,56 @@ var SIGNAL_CREATORS = /* @__PURE__ */ new Set([
|
|
|
61
61
|
"useQuery",
|
|
62
62
|
"useInfiniteQuery"
|
|
63
63
|
]);
|
|
64
|
+
function normalizeJsxText(value) {
|
|
65
|
+
if (!/[\r\n]/.test(value)) {
|
|
66
|
+
return value.replace(/\t/g, " ");
|
|
67
|
+
}
|
|
68
|
+
const lines = value.split(/\r\n|\n|\r/);
|
|
69
|
+
let lastNonEmpty = -1;
|
|
70
|
+
for (let i = 0; i < lines.length; i++) {
|
|
71
|
+
if (/[^ \t]/.test(lines[i])) lastNonEmpty = i;
|
|
72
|
+
}
|
|
73
|
+
if (lastNonEmpty === -1) return "";
|
|
74
|
+
let out = "";
|
|
75
|
+
for (let i = 0; i < lines.length; i++) {
|
|
76
|
+
let line = lines[i].replace(/\t/g, " ");
|
|
77
|
+
const isFirst = i === 0;
|
|
78
|
+
const isLast = i === lines.length - 1;
|
|
79
|
+
if (!isFirst) line = line.replace(/^ +/, "");
|
|
80
|
+
if (!isLast) line = line.replace(/ +$/, "");
|
|
81
|
+
if (!line) continue;
|
|
82
|
+
if (i !== lastNonEmpty) line += " ";
|
|
83
|
+
out += line;
|
|
84
|
+
}
|
|
85
|
+
return out;
|
|
86
|
+
}
|
|
64
87
|
function whatBabelPlugin({ types: t }) {
|
|
88
|
+
const _unknownModifierWarned = /* @__PURE__ */ new Set();
|
|
89
|
+
const _forInfoWarned = /* @__PURE__ */ new Set();
|
|
90
|
+
function hasEventModifiers(name, state) {
|
|
91
|
+
if (!name.includes("__")) return false;
|
|
92
|
+
if (!name.startsWith("on")) return false;
|
|
93
|
+
const parts = name.split("__");
|
|
94
|
+
const tail = parts.slice(1).filter((s) => s !== "");
|
|
95
|
+
if (tail.length === 0) return false;
|
|
96
|
+
if (true) {
|
|
97
|
+
const unknown = tail.filter((m) => !EVENT_MODIFIERS.has(m));
|
|
98
|
+
const filename = state && (state.filename || state.file && state.file.opts && state.file.opts.filename) || "<unknown>";
|
|
99
|
+
for (const m of unknown) {
|
|
100
|
+
const key = `${filename}::${m}`;
|
|
101
|
+
if (!_unknownModifierWarned.has(key)) {
|
|
102
|
+
_unknownModifierWarned.add(key);
|
|
103
|
+
console.warn(
|
|
104
|
+
`[what-compiler] Unknown event modifier "__${m}" in attribute "${name}" (${filename}). Known modifiers: ${[...EVENT_MODIFIERS].join(", ")}. Unknown segments are ignored.`
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return true;
|
|
110
|
+
}
|
|
65
111
|
function parseEventModifiers(name) {
|
|
66
|
-
const
|
|
112
|
+
const delimiter = name.includes("|") ? "|" : "__";
|
|
113
|
+
const parts = name.split(delimiter);
|
|
67
114
|
const eventName = parts[0];
|
|
68
115
|
const modifiers = parts.slice(1).filter((m) => EVENT_MODIFIERS.has(m));
|
|
69
116
|
return { eventName, modifiers };
|
|
@@ -193,22 +240,20 @@ function whatBabelPlugin({ types: t }) {
|
|
|
193
240
|
}
|
|
194
241
|
let scope = path3.scope;
|
|
195
242
|
while (scope) {
|
|
196
|
-
for (const
|
|
243
|
+
for (const binding of Object.values(scope.bindings)) {
|
|
197
244
|
if (binding.path.isVariableDeclarator()) {
|
|
198
245
|
extractFromDeclarator(binding.path.node);
|
|
199
246
|
}
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
}
|
|
211
|
-
}
|
|
247
|
+
}
|
|
248
|
+
const fnNode = scope.path && scope.path.node;
|
|
249
|
+
if (fnNode && fnNode.params) {
|
|
250
|
+
for (const param of fnNode.params) {
|
|
251
|
+
if (t.isObjectPattern(param)) {
|
|
252
|
+
for (const prop of param.properties) {
|
|
253
|
+
if (t.isObjectProperty(prop) && t.isIdentifier(prop.value)) {
|
|
254
|
+
signalNames.add(prop.value.name);
|
|
255
|
+
} else if (t.isRestElement(prop) && t.isIdentifier(prop.argument)) {
|
|
256
|
+
signalNames.add(prop.argument.name);
|
|
212
257
|
}
|
|
213
258
|
}
|
|
214
259
|
}
|
|
@@ -276,7 +321,7 @@ function whatBabelPlugin({ types: t }) {
|
|
|
276
321
|
return isPotentiallyReactive(expr.callee, signalNames, importedIds) || expr.arguments.some((arg) => isPotentiallyReactive(arg, signalNames, importedIds));
|
|
277
322
|
}
|
|
278
323
|
if (t.isIdentifier(expr)) {
|
|
279
|
-
return isSignalIdentifier(expr.name, signalNames);
|
|
324
|
+
return isSignalIdentifier(expr.name, signalNames) || importedIds && importedIds.has(expr.name);
|
|
280
325
|
}
|
|
281
326
|
if (t.isMemberExpression(expr)) {
|
|
282
327
|
return isPotentiallyReactive(expr.object, signalNames, importedIds);
|
|
@@ -306,6 +351,93 @@ function whatBabelPlugin({ types: t }) {
|
|
|
306
351
|
}
|
|
307
352
|
return false;
|
|
308
353
|
}
|
|
354
|
+
function tryLowerMapToMapArray(expr, state) {
|
|
355
|
+
let mapCall = expr;
|
|
356
|
+
let wrappedInArrow = false;
|
|
357
|
+
if (t.isArrowFunctionExpression(expr) && expr.params.length === 0) {
|
|
358
|
+
mapCall = expr.body;
|
|
359
|
+
wrappedInArrow = true;
|
|
360
|
+
}
|
|
361
|
+
if (t.isConditionalExpression(mapCall)) {
|
|
362
|
+
const loweredCon = tryLowerMapCall(mapCall.consequent, state);
|
|
363
|
+
const loweredAlt = tryLowerMapCall(mapCall.alternate, state);
|
|
364
|
+
if (loweredCon || loweredAlt) {
|
|
365
|
+
const result = t.conditionalExpression(
|
|
366
|
+
mapCall.test,
|
|
367
|
+
loweredCon || mapCall.consequent,
|
|
368
|
+
loweredAlt || mapCall.alternate
|
|
369
|
+
);
|
|
370
|
+
return wrappedInArrow ? t.arrowFunctionExpression([], result) : result;
|
|
371
|
+
}
|
|
372
|
+
return null;
|
|
373
|
+
}
|
|
374
|
+
if (t.isLogicalExpression(mapCall) && (mapCall.operator === "&&" || mapCall.operator === "||")) {
|
|
375
|
+
const loweredRight = tryLowerMapCall(mapCall.right, state);
|
|
376
|
+
if (loweredRight) {
|
|
377
|
+
const result = t.logicalExpression(mapCall.operator, mapCall.left, loweredRight);
|
|
378
|
+
return wrappedInArrow ? t.arrowFunctionExpression([], result) : result;
|
|
379
|
+
}
|
|
380
|
+
return null;
|
|
381
|
+
}
|
|
382
|
+
const lowered = tryLowerMapCall(mapCall, state);
|
|
383
|
+
return lowered;
|
|
384
|
+
}
|
|
385
|
+
function tryLowerMapCall(mapCall, state) {
|
|
386
|
+
if (!t.isCallExpression(mapCall)) return null;
|
|
387
|
+
if (!t.isMemberExpression(mapCall.callee)) return null;
|
|
388
|
+
if (!t.isIdentifier(mapCall.callee.property, { name: "map" })) return null;
|
|
389
|
+
if (mapCall.arguments.length < 1) return null;
|
|
390
|
+
const mapFn = mapCall.arguments[0];
|
|
391
|
+
if (!t.isArrowFunctionExpression(mapFn) && !t.isFunctionExpression(mapFn)) return null;
|
|
392
|
+
let returnExpr = null;
|
|
393
|
+
if (t.isArrowFunctionExpression(mapFn)) {
|
|
394
|
+
if (t.isExpression(mapFn.body)) {
|
|
395
|
+
returnExpr = mapFn.body;
|
|
396
|
+
} else if (t.isBlockStatement(mapFn.body)) {
|
|
397
|
+
const ret = mapFn.body.body.find((s) => t.isReturnStatement(s));
|
|
398
|
+
if (ret) returnExpr = ret.argument;
|
|
399
|
+
}
|
|
400
|
+
} else if (t.isFunctionExpression(mapFn)) {
|
|
401
|
+
const ret = mapFn.body.body.find((s) => t.isReturnStatement(s));
|
|
402
|
+
if (ret) returnExpr = ret.argument;
|
|
403
|
+
}
|
|
404
|
+
if (!returnExpr) return null;
|
|
405
|
+
if (!t.isJSXElement(returnExpr)) return null;
|
|
406
|
+
const attrs = returnExpr.openingElement.attributes;
|
|
407
|
+
let keyAttr = null;
|
|
408
|
+
for (const attr of attrs) {
|
|
409
|
+
if (t.isJSXAttribute(attr) && getAttrName(attr) === "key") {
|
|
410
|
+
keyAttr = attr;
|
|
411
|
+
break;
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
if (!keyAttr) {
|
|
415
|
+
if (true) {
|
|
416
|
+
const loc = returnExpr.loc;
|
|
417
|
+
const fileName = state.filename || state.file?.opts?.filename || "<unknown>";
|
|
418
|
+
const lineInfo = loc ? `:${loc.start.line}:${loc.start.column}` : "";
|
|
419
|
+
console.warn(
|
|
420
|
+
`[what-compiler] .map() returning JSX without a \`key\` prop at ${fileName}${lineInfo}. Without a key, the list cannot use keyed reconciliation \u2014 items are re-created on every update. Add key={...} to enable efficient updates.`
|
|
421
|
+
);
|
|
422
|
+
}
|
|
423
|
+
return null;
|
|
424
|
+
}
|
|
425
|
+
const keyValue = getAttributeValue(keyAttr.value);
|
|
426
|
+
if (!keyValue) return null;
|
|
427
|
+
returnExpr.openingElement.attributes = attrs.filter((a) => a !== keyAttr);
|
|
428
|
+
const sourceObj = mapCall.callee.object;
|
|
429
|
+
const source = t.arrowFunctionExpression([], sourceObj);
|
|
430
|
+
const itemParam = mapFn.params[0] ? t.cloneNode(mapFn.params[0], true) : t.identifier("_item");
|
|
431
|
+
const keyFn = t.arrowFunctionExpression([itemParam], t.cloneNode(keyValue, true));
|
|
432
|
+
return t.callExpression(t.identifier("_$mapArray"), [
|
|
433
|
+
source,
|
|
434
|
+
mapFn,
|
|
435
|
+
t.objectExpression([
|
|
436
|
+
t.objectProperty(t.identifier("key"), keyFn),
|
|
437
|
+
t.objectProperty(t.identifier("raw"), t.booleanLiteral(true))
|
|
438
|
+
])
|
|
439
|
+
]);
|
|
440
|
+
}
|
|
309
441
|
function isStaticChild(child) {
|
|
310
442
|
if (t.isJSXText(child)) return true;
|
|
311
443
|
if (t.isJSXExpressionContainer(child)) return false;
|
|
@@ -329,7 +461,7 @@ function whatBabelPlugin({ types: t }) {
|
|
|
329
461
|
}
|
|
330
462
|
function extractStaticHTML(node) {
|
|
331
463
|
if (t.isJSXText(node)) {
|
|
332
|
-
const text = node.value
|
|
464
|
+
const text = normalizeJsxText(node.value);
|
|
333
465
|
return text ? escapeHTML(text) : "";
|
|
334
466
|
}
|
|
335
467
|
if (t.isJSXExpressionContainer(node)) {
|
|
@@ -369,7 +501,7 @@ function whatBabelPlugin({ types: t }) {
|
|
|
369
501
|
html += ">";
|
|
370
502
|
for (const child of node.children) {
|
|
371
503
|
if (t.isJSXText(child)) {
|
|
372
|
-
const text = child.value
|
|
504
|
+
const text = normalizeJsxText(child.value);
|
|
373
505
|
if (text) html += escapeHTML(text);
|
|
374
506
|
} else if (t.isJSXExpressionContainer(child)) {
|
|
375
507
|
if (!t.isJSXEmptyExpression(child.expression)) {
|
|
@@ -396,15 +528,15 @@ function whatBabelPlugin({ types: t }) {
|
|
|
396
528
|
const { node } = path3;
|
|
397
529
|
const openingElement = node.openingElement;
|
|
398
530
|
const tagName = openingElement.name.name;
|
|
399
|
-
if (isComponent(tagName)) {
|
|
400
|
-
return transformComponentFineGrained(path3, state);
|
|
401
|
-
}
|
|
402
531
|
if (tagName === "For") {
|
|
403
532
|
return transformForFineGrained(path3, state);
|
|
404
533
|
}
|
|
405
534
|
if (tagName === "Show") {
|
|
406
535
|
return transformShowFineGrained(path3, state);
|
|
407
536
|
}
|
|
537
|
+
if (isComponent(tagName)) {
|
|
538
|
+
return transformComponentFineGrained(path3, state);
|
|
539
|
+
}
|
|
408
540
|
const attributes = openingElement.attributes;
|
|
409
541
|
const children = node.children;
|
|
410
542
|
const allChildrenStatic = children.every(isStaticChild);
|
|
@@ -469,7 +601,7 @@ function whatBabelPlugin({ types: t }) {
|
|
|
469
601
|
const transformedChildren = [];
|
|
470
602
|
for (const child of children) {
|
|
471
603
|
if (t.isJSXText(child)) {
|
|
472
|
-
const text = child.value
|
|
604
|
+
const text = normalizeJsxText(child.value);
|
|
473
605
|
if (text) transformedChildren.push(t.stringLiteral(text));
|
|
474
606
|
} else if (t.isJSXExpressionContainer(child)) {
|
|
475
607
|
if (!t.isJSXEmptyExpression(child.expression)) {
|
|
@@ -526,7 +658,7 @@ function whatBabelPlugin({ types: t }) {
|
|
|
526
658
|
);
|
|
527
659
|
continue;
|
|
528
660
|
}
|
|
529
|
-
if (attrName.startsWith("on") && !attrName.includes("|")) {
|
|
661
|
+
if (attrName.startsWith("on") && !attrName.includes("|") && !hasEventModifiers(attrName, state)) {
|
|
530
662
|
const event = attrName.slice(2).toLowerCase();
|
|
531
663
|
const handler = getAttributeValue(attr.value);
|
|
532
664
|
if (DELEGATED_EVENTS.has(event)) {
|
|
@@ -539,7 +671,7 @@ function whatBabelPlugin({ types: t }) {
|
|
|
539
671
|
"=",
|
|
540
672
|
t.memberExpression(
|
|
541
673
|
t.identifier(elId),
|
|
542
|
-
t.identifier(
|
|
674
|
+
t.identifier(`$$${event}`)
|
|
543
675
|
),
|
|
544
676
|
handler
|
|
545
677
|
)
|
|
@@ -557,7 +689,7 @@ function whatBabelPlugin({ types: t }) {
|
|
|
557
689
|
}
|
|
558
690
|
continue;
|
|
559
691
|
}
|
|
560
|
-
if (attrName.startsWith("on") && attrName.includes("|")) {
|
|
692
|
+
if (attrName.startsWith("on") && (attrName.includes("|") || hasEventModifiers(attrName, state))) {
|
|
561
693
|
const { eventName, modifiers } = parseEventModifiers(attrName);
|
|
562
694
|
const handler = getAttributeValue(attr.value);
|
|
563
695
|
const wrappedHandler = createEventHandler(handler, modifiers);
|
|
@@ -657,8 +789,9 @@ function whatBabelPlugin({ types: t }) {
|
|
|
657
789
|
const domName = normalizeAttrName(attrName);
|
|
658
790
|
if (isPotentiallyReactive(expr, state.signalNames, state.importedIdentifiers)) {
|
|
659
791
|
state.needsEffect = true;
|
|
792
|
+
const valueExpr = t.isIdentifier(expr) && (isSignalIdentifier(expr.name, state.signalNames) || state.importedIdentifiers && state.importedIdentifiers.has(expr.name)) ? t.callExpression(expr, []) : expr;
|
|
660
793
|
const effectCall = t.callExpression(t.identifier("_$effect"), [
|
|
661
|
-
t.arrowFunctionExpression([], buildSetPropCall(domName,
|
|
794
|
+
t.arrowFunctionExpression([], buildSetPropCall(domName, valueExpr))
|
|
662
795
|
]);
|
|
663
796
|
if (isUncertainReactive(expr, state.signalNames, state.importedIdentifiers)) {
|
|
664
797
|
t.addComment(
|
|
@@ -680,7 +813,7 @@ function whatBabelPlugin({ types: t }) {
|
|
|
680
813
|
let childIndex = 0;
|
|
681
814
|
for (const child of children) {
|
|
682
815
|
if (t.isJSXText(child)) {
|
|
683
|
-
const text = child.value
|
|
816
|
+
const text = normalizeJsxText(child.value);
|
|
684
817
|
if (text) childIndex++;
|
|
685
818
|
continue;
|
|
686
819
|
}
|
|
@@ -709,22 +842,31 @@ function whatBabelPlugin({ types: t }) {
|
|
|
709
842
|
const entriesNeedingRef = entries.filter(
|
|
710
843
|
(e) => e.type === "expression" || e.type === "component" || e.type === "static" && e.hasAnythingDynamic
|
|
711
844
|
);
|
|
712
|
-
const
|
|
713
|
-
const needsPreCapture = entriesNeedingRef.length >= 2 && hasDynamicInsert;
|
|
845
|
+
const needsPreCapture = entriesNeedingRef.length >= 2;
|
|
714
846
|
const markerVars = /* @__PURE__ */ new Map();
|
|
715
847
|
if (needsPreCapture) {
|
|
848
|
+
let prevVar = null;
|
|
849
|
+
let prevIndex = 0;
|
|
716
850
|
for (const entry of entriesNeedingRef) {
|
|
717
|
-
const
|
|
851
|
+
const idx = entry.childIndex;
|
|
718
852
|
const markerVar = state.nextVarId();
|
|
719
|
-
markerVars.set(
|
|
853
|
+
markerVars.set(idx, markerVar);
|
|
854
|
+
let init;
|
|
855
|
+
if (prevVar === null) {
|
|
856
|
+
init = buildChildAccess(elId, idx);
|
|
857
|
+
} else {
|
|
858
|
+
init = t.identifier(prevVar);
|
|
859
|
+
for (let i = prevIndex; i < idx; i++) {
|
|
860
|
+
init = t.memberExpression(init, t.identifier("nextSibling"));
|
|
861
|
+
}
|
|
862
|
+
}
|
|
720
863
|
statements.push(
|
|
721
864
|
t.variableDeclaration("const", [
|
|
722
|
-
t.variableDeclarator(
|
|
723
|
-
t.identifier(markerVar),
|
|
724
|
-
buildChildAccess(elId, entry.childIndex)
|
|
725
|
-
)
|
|
865
|
+
t.variableDeclarator(t.identifier(markerVar), init)
|
|
726
866
|
])
|
|
727
867
|
);
|
|
868
|
+
prevVar = markerVar;
|
|
869
|
+
prevIndex = idx;
|
|
728
870
|
}
|
|
729
871
|
}
|
|
730
872
|
function getMarker(idx) {
|
|
@@ -735,9 +877,41 @@ function whatBabelPlugin({ types: t }) {
|
|
|
735
877
|
}
|
|
736
878
|
for (const entry of entries) {
|
|
737
879
|
if (entry.type === "expression") {
|
|
738
|
-
|
|
880
|
+
let expr = entry.child.expression;
|
|
739
881
|
const marker = getMarker(entry.childIndex);
|
|
740
882
|
state.needsInsert = true;
|
|
883
|
+
const mapResult = tryLowerMapToMapArray(expr, state);
|
|
884
|
+
if (mapResult) {
|
|
885
|
+
state.needsMapArray = true;
|
|
886
|
+
const isBareMapArray = t.isCallExpression(mapResult) && t.isIdentifier(mapResult.callee) && (mapResult.callee.name === "_$mapArray" || mapResult.callee.name === "mapArray");
|
|
887
|
+
const isArrowAlready = t.isArrowFunctionExpression(mapResult);
|
|
888
|
+
const insertArg = isBareMapArray || isArrowAlready ? mapResult : t.arrowFunctionExpression([], mapResult);
|
|
889
|
+
statements.push(
|
|
890
|
+
t.expressionStatement(
|
|
891
|
+
t.callExpression(t.identifier("_$insert"), [
|
|
892
|
+
t.identifier(elId),
|
|
893
|
+
insertArg,
|
|
894
|
+
marker
|
|
895
|
+
])
|
|
896
|
+
)
|
|
897
|
+
);
|
|
898
|
+
continue;
|
|
899
|
+
}
|
|
900
|
+
const isMapArrayCall = t.isCallExpression(expr) && t.isIdentifier(expr.callee) && (expr.callee.name === "mapArray" || expr.callee.name === "_$mapArray");
|
|
901
|
+
if (isMapArrayCall) {
|
|
902
|
+
state.needsMapArray = true;
|
|
903
|
+
if (expr.callee.name === "mapArray") expr.callee.name = "_$mapArray";
|
|
904
|
+
statements.push(
|
|
905
|
+
t.expressionStatement(
|
|
906
|
+
t.callExpression(t.identifier("_$insert"), [
|
|
907
|
+
t.identifier(elId),
|
|
908
|
+
expr,
|
|
909
|
+
marker
|
|
910
|
+
])
|
|
911
|
+
)
|
|
912
|
+
);
|
|
913
|
+
continue;
|
|
914
|
+
}
|
|
741
915
|
if (isPotentiallyReactive(expr, state.signalNames, state.importedIdentifiers)) {
|
|
742
916
|
const insertCall = t.callExpression(t.identifier("_$insert"), [
|
|
743
917
|
t.identifier(elId),
|
|
@@ -946,7 +1120,7 @@ function whatBabelPlugin({ types: t }) {
|
|
|
946
1120
|
}
|
|
947
1121
|
continue;
|
|
948
1122
|
}
|
|
949
|
-
if (attrName.startsWith("on") && attrName.includes("|")) {
|
|
1123
|
+
if (attrName.startsWith("on") && (attrName.includes("|") || hasEventModifiers(attrName, state))) {
|
|
950
1124
|
const { eventName, modifiers } = parseEventModifiers(attrName);
|
|
951
1125
|
const handler = getAttributeValue(attr.value);
|
|
952
1126
|
const wrappedHandler = createEventHandler(handler, modifiers);
|
|
@@ -964,7 +1138,7 @@ function whatBabelPlugin({ types: t }) {
|
|
|
964
1138
|
const transformedChildren = [];
|
|
965
1139
|
for (const child of children) {
|
|
966
1140
|
if (t.isJSXText(child)) {
|
|
967
|
-
const text = child.value
|
|
1141
|
+
const text = normalizeJsxText(child.value);
|
|
968
1142
|
if (text) transformedChildren.push(t.stringLiteral(text));
|
|
969
1143
|
} else if (t.isJSXExpressionContainer(child)) {
|
|
970
1144
|
if (!t.isJSXEmptyExpression(child.expression)) {
|
|
@@ -998,10 +1172,24 @@ function whatBabelPlugin({ types: t }) {
|
|
|
998
1172
|
const { node } = path3;
|
|
999
1173
|
const attributes = node.openingElement.attributes;
|
|
1000
1174
|
const children = node.children;
|
|
1175
|
+
if (true) {
|
|
1176
|
+
const fileName = state.filename || state.file?.opts?.filename || "<unknown>";
|
|
1177
|
+
if (!_forInfoWarned.has(fileName)) {
|
|
1178
|
+
_forInfoWarned.add(fileName);
|
|
1179
|
+
const loc = node.loc;
|
|
1180
|
+
const lineInfo = loc ? `:${loc.start.line}:${loc.start.column}` : "";
|
|
1181
|
+
console.info(
|
|
1182
|
+
`[what-compiler] <For> at ${fileName}${lineInfo}: consider using .map() with a key prop instead. The compiler auto-lowers .map() to efficient keyed reconciliation. <For> is only needed for signal-wrapped item accessors (advanced).`
|
|
1183
|
+
);
|
|
1184
|
+
}
|
|
1185
|
+
}
|
|
1001
1186
|
let eachExpr = null;
|
|
1187
|
+
let keyExpr = null;
|
|
1002
1188
|
for (const attr of attributes) {
|
|
1003
|
-
if (t.isJSXAttribute(attr)
|
|
1004
|
-
|
|
1189
|
+
if (t.isJSXAttribute(attr)) {
|
|
1190
|
+
const name = getAttrName(attr);
|
|
1191
|
+
if (name === "each") eachExpr = getAttributeValue(attr.value);
|
|
1192
|
+
else if (name === "key") keyExpr = getAttributeValue(attr.value);
|
|
1005
1193
|
}
|
|
1006
1194
|
}
|
|
1007
1195
|
if (!eachExpr) {
|
|
@@ -1022,11 +1210,78 @@ function whatBabelPlugin({ types: t }) {
|
|
|
1022
1210
|
return transformElementAsH(path3, state);
|
|
1023
1211
|
}
|
|
1024
1212
|
state.needsMapArray = true;
|
|
1025
|
-
|
|
1213
|
+
const args = [eachExpr, renderFn];
|
|
1214
|
+
if (keyExpr) {
|
|
1215
|
+
args.push(t.objectExpression([
|
|
1216
|
+
t.objectProperty(t.identifier("key"), keyExpr)
|
|
1217
|
+
]));
|
|
1218
|
+
}
|
|
1219
|
+
return t.callExpression(t.identifier("_$mapArray"), args);
|
|
1026
1220
|
}
|
|
1027
1221
|
function transformShowFineGrained(path3, state) {
|
|
1028
|
-
|
|
1029
|
-
|
|
1222
|
+
const { node } = path3;
|
|
1223
|
+
const attributes = node.openingElement.attributes;
|
|
1224
|
+
const children = node.children;
|
|
1225
|
+
let whenExpr = null;
|
|
1226
|
+
let fallbackExpr = null;
|
|
1227
|
+
for (const attr of attributes) {
|
|
1228
|
+
if (t.isJSXAttribute(attr)) {
|
|
1229
|
+
const name = getAttrName(attr);
|
|
1230
|
+
if (name === "when") whenExpr = getAttributeValue(attr.value);
|
|
1231
|
+
else if (name === "fallback") fallbackExpr = getAttributeValue(attr.value);
|
|
1232
|
+
}
|
|
1233
|
+
}
|
|
1234
|
+
if (!whenExpr) {
|
|
1235
|
+
throw path3.buildCodeFrameError(
|
|
1236
|
+
'<Show> requires a "when" prop. Example: <Show when={isOpen} fallback={null}>...</Show>'
|
|
1237
|
+
);
|
|
1238
|
+
}
|
|
1239
|
+
let contentExpr = null;
|
|
1240
|
+
for (const child of children) {
|
|
1241
|
+
if (t.isJSXExpressionContainer(child) && !t.isJSXEmptyExpression(child.expression)) {
|
|
1242
|
+
contentExpr = child.expression;
|
|
1243
|
+
break;
|
|
1244
|
+
}
|
|
1245
|
+
}
|
|
1246
|
+
if (!contentExpr) {
|
|
1247
|
+
const transformedChildren = [];
|
|
1248
|
+
for (const child of children) {
|
|
1249
|
+
if (t.isJSXText(child)) {
|
|
1250
|
+
const text = normalizeJsxText(child.value);
|
|
1251
|
+
if (text) transformedChildren.push(t.stringLiteral(text));
|
|
1252
|
+
} else if (t.isJSXElement(child)) {
|
|
1253
|
+
transformedChildren.push(transformElementFineGrained({ node: child }, state));
|
|
1254
|
+
}
|
|
1255
|
+
}
|
|
1256
|
+
if (transformedChildren.length === 1) {
|
|
1257
|
+
contentExpr = transformedChildren[0];
|
|
1258
|
+
} else if (transformedChildren.length > 1) {
|
|
1259
|
+
contentExpr = t.arrayExpression(transformedChildren);
|
|
1260
|
+
} else {
|
|
1261
|
+
contentExpr = t.nullLiteral();
|
|
1262
|
+
}
|
|
1263
|
+
}
|
|
1264
|
+
let condition;
|
|
1265
|
+
if (t.isCallExpression(whenExpr)) {
|
|
1266
|
+
condition = whenExpr;
|
|
1267
|
+
} else if (t.isArrowFunctionExpression(whenExpr) && t.isExpression(whenExpr.body)) {
|
|
1268
|
+
condition = whenExpr.body;
|
|
1269
|
+
} else if (t.isIdentifier(whenExpr) && (state.signalNames && isSignalIdentifier(whenExpr.name, state.signalNames) || state.importedIdentifiers && state.importedIdentifiers.has(whenExpr.name))) {
|
|
1270
|
+
condition = t.callExpression(whenExpr, []);
|
|
1271
|
+
} else {
|
|
1272
|
+
condition = whenExpr;
|
|
1273
|
+
}
|
|
1274
|
+
const vId = path3.scope ? path3.scope.generateUidIdentifier("v") : t.identifier("_v");
|
|
1275
|
+
const consequent = t.isFunction(contentExpr) ? t.callExpression(contentExpr, [t.cloneNode(vId)]) : contentExpr;
|
|
1276
|
+
const alternate = fallbackExpr || t.nullLiteral();
|
|
1277
|
+
return t.arrowFunctionExpression([], t.blockStatement([
|
|
1278
|
+
t.variableDeclaration("const", [
|
|
1279
|
+
t.variableDeclarator(vId, condition)
|
|
1280
|
+
]),
|
|
1281
|
+
t.returnStatement(
|
|
1282
|
+
t.conditionalExpression(t.cloneNode(vId), consequent, alternate)
|
|
1283
|
+
)
|
|
1284
|
+
]));
|
|
1030
1285
|
}
|
|
1031
1286
|
function transformFragmentFineGrained(path3, state) {
|
|
1032
1287
|
const { node } = path3;
|
|
@@ -1034,7 +1289,7 @@ function whatBabelPlugin({ types: t }) {
|
|
|
1034
1289
|
const transformed = [];
|
|
1035
1290
|
for (const child of children) {
|
|
1036
1291
|
if (t.isJSXText(child)) {
|
|
1037
|
-
const text = child.value
|
|
1292
|
+
const text = normalizeJsxText(child.value);
|
|
1038
1293
|
if (text) transformed.push(t.stringLiteral(text));
|
|
1039
1294
|
} else if (t.isJSXExpressionContainer(child)) {
|
|
1040
1295
|
if (!t.isJSXEmptyExpression(child.expression)) {
|
|
@@ -1244,20 +1499,31 @@ function whatBabelPlugin({ types: t }) {
|
|
|
1244
1499
|
}
|
|
1245
1500
|
},
|
|
1246
1501
|
JSXElement(path3, state) {
|
|
1247
|
-
|
|
1502
|
+
const scope = path3.scope;
|
|
1503
|
+
let cache = state._signalNamesCache;
|
|
1504
|
+
if (!cache) cache = state._signalNamesCache = /* @__PURE__ */ new WeakMap();
|
|
1505
|
+
let names = cache.get(scope);
|
|
1506
|
+
if (!names) {
|
|
1507
|
+
names = collectSignalNamesFromScope(path3);
|
|
1508
|
+
cache.set(scope, names);
|
|
1509
|
+
}
|
|
1510
|
+
state.signalNames = names;
|
|
1248
1511
|
state._pendingSetup = [];
|
|
1249
1512
|
const transformed = transformElementFineGrained(path3, state);
|
|
1250
1513
|
const pending = state._pendingSetup;
|
|
1251
1514
|
state._pendingSetup = [];
|
|
1252
1515
|
if (pending.length > 0) {
|
|
1253
1516
|
let stmtPath = path3;
|
|
1517
|
+
let crossedFunctionBoundary = false;
|
|
1254
1518
|
while (stmtPath && !stmtPath.isStatement()) {
|
|
1519
|
+
if (stmtPath.isArrowFunctionExpression() || stmtPath.isFunctionExpression()) {
|
|
1520
|
+
crossedFunctionBoundary = true;
|
|
1521
|
+
}
|
|
1255
1522
|
stmtPath = stmtPath.parentPath;
|
|
1256
1523
|
}
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
}
|
|
1524
|
+
const inStatementList = stmtPath && stmtPath.isStatement() && (stmtPath.listKey === "body" || stmtPath.listKey === "consequent") && Array.isArray(stmtPath.container);
|
|
1525
|
+
if (inStatementList && !crossedFunctionBoundary) {
|
|
1526
|
+
stmtPath.insertBefore(pending);
|
|
1261
1527
|
path3.replaceWith(transformed);
|
|
1262
1528
|
} else {
|
|
1263
1529
|
pending.push(t.returnStatement(transformed));
|
|
@@ -1417,6 +1683,13 @@ function extractPageConfig(source) {
|
|
|
1417
1683
|
return { mode: "client" };
|
|
1418
1684
|
}
|
|
1419
1685
|
}
|
|
1686
|
+
function detectPageExports(source) {
|
|
1687
|
+
return {
|
|
1688
|
+
hasLoader: /export\s+(?:async\s+)?(?:const|let|var|function)\s+loader\b/.test(source),
|
|
1689
|
+
hasGetStaticPaths: /export\s+(?:async\s+)?(?:const|let|var|function)\s+getStaticPaths\b/.test(source),
|
|
1690
|
+
hasPageConfig: /export\s+const\s+page\b/.test(source)
|
|
1691
|
+
};
|
|
1692
|
+
}
|
|
1420
1693
|
function generateRoutesModule(pagesDir, rootDir) {
|
|
1421
1694
|
const { pages, layouts, apiRoutes } = scanPages(pagesDir);
|
|
1422
1695
|
const imports = [];
|
|
@@ -1433,9 +1706,11 @@ function generateRoutesModule(pagesDir, rootDir) {
|
|
|
1433
1706
|
const relPath = toImportPath(page.filePath, rootDir);
|
|
1434
1707
|
imports.push(`import ${varName} from '${relPath}';`);
|
|
1435
1708
|
let pageConfig = { mode: "client" };
|
|
1709
|
+
let detected = { hasLoader: false, hasGetStaticPaths: false, hasPageConfig: false };
|
|
1436
1710
|
try {
|
|
1437
1711
|
const source = fs.readFileSync(page.filePath, "utf-8");
|
|
1438
1712
|
pageConfig = extractPageConfig(source);
|
|
1713
|
+
detected = detectPageExports(source);
|
|
1439
1714
|
} catch {
|
|
1440
1715
|
}
|
|
1441
1716
|
const layoutVar = findLayout(page.routePath, layoutMap);
|
|
@@ -1443,7 +1718,8 @@ function generateRoutesModule(pagesDir, rootDir) {
|
|
|
1443
1718
|
path: page.routePath,
|
|
1444
1719
|
component: varName,
|
|
1445
1720
|
mode: pageConfig.mode || "client",
|
|
1446
|
-
layout: layoutVar || null
|
|
1721
|
+
layout: layoutVar || null,
|
|
1722
|
+
hasLoader: detected.hasLoader
|
|
1447
1723
|
};
|
|
1448
1724
|
routeEntries.push(entry);
|
|
1449
1725
|
});
|
|
@@ -1465,7 +1741,7 @@ function generateRoutesModule(pagesDir, rootDir) {
|
|
|
1465
1741
|
"",
|
|
1466
1742
|
"export const routes = [",
|
|
1467
1743
|
...routeEntries.map(
|
|
1468
|
-
(r) => ` { path: '${r.path}', component: ${r.component}, mode: '${r.mode}'${r.layout ? `, layout: ${r.layout}` : ""} },`
|
|
1744
|
+
(r) => ` { path: '${r.path}', component: ${r.component}, mode: '${r.mode}'${r.layout ? `, layout: ${r.layout}` : ""}${r.hasLoader ? ", hasLoader: true" : ""} },`
|
|
1469
1745
|
),
|
|
1470
1746
|
"];",
|
|
1471
1747
|
"",
|
|
@@ -2018,8 +2294,34 @@ function whatVitePlugin(options = {}) {
|
|
|
2018
2294
|
jsx: "preserve"
|
|
2019
2295
|
},
|
|
2020
2296
|
optimizeDeps: {
|
|
2021
|
-
//
|
|
2022
|
-
|
|
2297
|
+
// Exclude framework packages from Vite's dependency pre-bundling.
|
|
2298
|
+
//
|
|
2299
|
+
// Bug class this prevents — "dual module instance":
|
|
2300
|
+
// The compiler emits `import { ... } from 'what-framework/render'`
|
|
2301
|
+
// (a subpath resolved to the source file). Meanwhile user code
|
|
2302
|
+
// imports `'what-framework'` (the package entry). If Vite
|
|
2303
|
+
// pre-bundles `'what-framework'` into an esbuild chunk under
|
|
2304
|
+
// node_modules/.vite, those two import paths resolve to two
|
|
2305
|
+
// *different* module instances. Module-scoped state — the
|
|
2306
|
+
// `componentStack` used by createComponent, effect ownership,
|
|
2307
|
+
// the signal subscriber registry — is duplicated, so a signal
|
|
2308
|
+
// created in user code never notifies effects created via the
|
|
2309
|
+
// compiler-emitted path, and `getCurrentComponent()` returns
|
|
2310
|
+
// undefined inside components mounted through compiler output.
|
|
2311
|
+
//
|
|
2312
|
+
// Why `exclude` is the right knob:
|
|
2313
|
+
// `include` would force pre-bundling of the package entry, which
|
|
2314
|
+
// does not resolve the subpath import the compiler emits — so the
|
|
2315
|
+
// split persists. Using `exclude` tells Vite to skip the optimizer
|
|
2316
|
+
// for these packages and serve them via the normal module graph,
|
|
2317
|
+
// where both the package entry and the `/render` subpath share
|
|
2318
|
+
// a single ESM module record.
|
|
2319
|
+
//
|
|
2320
|
+
// Regression symptom if this is removed:
|
|
2321
|
+
// Components mount but lifecycle hooks (onMount, onCleanup) and
|
|
2322
|
+
// shared store state silently no-op; effects don't re-run on
|
|
2323
|
+
// signal writes from user code; SSR/CSR hydration mismatches.
|
|
2324
|
+
exclude: ["what-framework", "what-core", "what-compiler", "what-router"]
|
|
2023
2325
|
}
|
|
2024
2326
|
};
|
|
2025
2327
|
}
|