what-compiler 0.8.3 → 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/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 parts = name.split("|");
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 [name, binding] of Object.entries(scope.bindings)) {
243
+ for (const binding of Object.values(scope.bindings)) {
197
244
  if (binding.path.isVariableDeclarator()) {
198
245
  extractFromDeclarator(binding.path.node);
199
246
  }
200
- if (binding.path.isIdentifier() || binding.kind === "param") {
201
- const fnPath = binding.scope.path;
202
- if (fnPath && fnPath.node && fnPath.node.params) {
203
- for (const param of fnPath.node.params) {
204
- if (t.isObjectPattern(param)) {
205
- for (const prop of param.properties) {
206
- if (t.isObjectProperty(prop) && t.isIdentifier(prop.value)) {
207
- signalNames.add(prop.value.name);
208
- } else if (t.isRestElement(prop) && t.isIdentifier(prop.argument)) {
209
- signalNames.add(prop.argument.name);
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.replace(/\n\s+/g, " ").trim();
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.replace(/\n\s+/g, " ").trim();
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.replace(/\n\s+/g, " ").trim();
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(`__${event}`)
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, expr))
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.replace(/\n\s+/g, " ").trim();
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 hasDynamicInsert = entries.some((e) => e.type === "expression" || e.type === "component");
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 varName = `_m$${entry.childIndex}`;
851
+ const idx = entry.childIndex;
718
852
  const markerVar = state.nextVarId();
719
- markerVars.set(entry.childIndex, markerVar);
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
- const expr = entry.child.expression;
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.replace(/\n\s+/g, " ").trim();
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) && getAttrName(attr) === "each") {
1004
- eachExpr = getAttributeValue(attr.value);
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
- return t.callExpression(t.identifier("_$mapArray"), [eachExpr, renderFn]);
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
- state.needsCreateComponent = true;
1029
- return transformComponentFineGrained(path3, state);
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.replace(/\n\s+/g, " ").trim();
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
- state.signalNames = collectSignalNamesFromScope(path3);
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
- if (stmtPath && stmtPath.isStatement()) {
1258
- for (const stmt of pending) {
1259
- stmtPath.insertBefore(stmt);
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
- // Pre-bundle the framework
2022
- include: ["what-framework"]
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
  }