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.
@@ -65,9 +65,56 @@ var SIGNAL_CREATORS = /* @__PURE__ */ new Set([
65
65
  "useQuery",
66
66
  "useInfiniteQuery"
67
67
  ]);
68
+ function normalizeJsxText(value) {
69
+ if (!/[\r\n]/.test(value)) {
70
+ return value.replace(/\t/g, " ");
71
+ }
72
+ const lines = value.split(/\r\n|\n|\r/);
73
+ let lastNonEmpty = -1;
74
+ for (let i = 0; i < lines.length; i++) {
75
+ if (/[^ \t]/.test(lines[i])) lastNonEmpty = i;
76
+ }
77
+ if (lastNonEmpty === -1) return "";
78
+ let out = "";
79
+ for (let i = 0; i < lines.length; i++) {
80
+ let line = lines[i].replace(/\t/g, " ");
81
+ const isFirst = i === 0;
82
+ const isLast = i === lines.length - 1;
83
+ if (!isFirst) line = line.replace(/^ +/, "");
84
+ if (!isLast) line = line.replace(/ +$/, "");
85
+ if (!line) continue;
86
+ if (i !== lastNonEmpty) line += " ";
87
+ out += line;
88
+ }
89
+ return out;
90
+ }
68
91
  function whatBabelPlugin({ types: t }) {
92
+ const _unknownModifierWarned = /* @__PURE__ */ new Set();
93
+ const _forInfoWarned = /* @__PURE__ */ new Set();
94
+ function hasEventModifiers(name, state) {
95
+ if (!name.includes("__")) return false;
96
+ if (!name.startsWith("on")) return false;
97
+ const parts = name.split("__");
98
+ const tail = parts.slice(1).filter((s) => s !== "");
99
+ if (tail.length === 0) return false;
100
+ if (true) {
101
+ const unknown = tail.filter((m) => !EVENT_MODIFIERS.has(m));
102
+ const filename = state && (state.filename || state.file && state.file.opts && state.file.opts.filename) || "<unknown>";
103
+ for (const m of unknown) {
104
+ const key = `${filename}::${m}`;
105
+ if (!_unknownModifierWarned.has(key)) {
106
+ _unknownModifierWarned.add(key);
107
+ console.warn(
108
+ `[what-compiler] Unknown event modifier "__${m}" in attribute "${name}" (${filename}). Known modifiers: ${[...EVENT_MODIFIERS].join(", ")}. Unknown segments are ignored.`
109
+ );
110
+ }
111
+ }
112
+ }
113
+ return true;
114
+ }
69
115
  function parseEventModifiers(name) {
70
- const parts = name.split("|");
116
+ const delimiter = name.includes("|") ? "|" : "__";
117
+ const parts = name.split(delimiter);
71
118
  const eventName = parts[0];
72
119
  const modifiers = parts.slice(1).filter((m) => EVENT_MODIFIERS.has(m));
73
120
  return { eventName, modifiers };
@@ -197,22 +244,20 @@ function whatBabelPlugin({ types: t }) {
197
244
  }
198
245
  let scope = path3.scope;
199
246
  while (scope) {
200
- for (const [name, binding] of Object.entries(scope.bindings)) {
247
+ for (const binding of Object.values(scope.bindings)) {
201
248
  if (binding.path.isVariableDeclarator()) {
202
249
  extractFromDeclarator(binding.path.node);
203
250
  }
204
- if (binding.path.isIdentifier() || binding.kind === "param") {
205
- const fnPath = binding.scope.path;
206
- if (fnPath && fnPath.node && fnPath.node.params) {
207
- for (const param of fnPath.node.params) {
208
- if (t.isObjectPattern(param)) {
209
- for (const prop of param.properties) {
210
- if (t.isObjectProperty(prop) && t.isIdentifier(prop.value)) {
211
- signalNames.add(prop.value.name);
212
- } else if (t.isRestElement(prop) && t.isIdentifier(prop.argument)) {
213
- signalNames.add(prop.argument.name);
214
- }
215
- }
251
+ }
252
+ const fnNode = scope.path && scope.path.node;
253
+ if (fnNode && fnNode.params) {
254
+ for (const param of fnNode.params) {
255
+ if (t.isObjectPattern(param)) {
256
+ for (const prop of param.properties) {
257
+ if (t.isObjectProperty(prop) && t.isIdentifier(prop.value)) {
258
+ signalNames.add(prop.value.name);
259
+ } else if (t.isRestElement(prop) && t.isIdentifier(prop.argument)) {
260
+ signalNames.add(prop.argument.name);
216
261
  }
217
262
  }
218
263
  }
@@ -280,7 +325,7 @@ function whatBabelPlugin({ types: t }) {
280
325
  return isPotentiallyReactive(expr.callee, signalNames, importedIds) || expr.arguments.some((arg) => isPotentiallyReactive(arg, signalNames, importedIds));
281
326
  }
282
327
  if (t.isIdentifier(expr)) {
283
- return isSignalIdentifier(expr.name, signalNames);
328
+ return isSignalIdentifier(expr.name, signalNames) || importedIds && importedIds.has(expr.name);
284
329
  }
285
330
  if (t.isMemberExpression(expr)) {
286
331
  return isPotentiallyReactive(expr.object, signalNames, importedIds);
@@ -310,6 +355,93 @@ function whatBabelPlugin({ types: t }) {
310
355
  }
311
356
  return false;
312
357
  }
358
+ function tryLowerMapToMapArray(expr, state) {
359
+ let mapCall = expr;
360
+ let wrappedInArrow = false;
361
+ if (t.isArrowFunctionExpression(expr) && expr.params.length === 0) {
362
+ mapCall = expr.body;
363
+ wrappedInArrow = true;
364
+ }
365
+ if (t.isConditionalExpression(mapCall)) {
366
+ const loweredCon = tryLowerMapCall(mapCall.consequent, state);
367
+ const loweredAlt = tryLowerMapCall(mapCall.alternate, state);
368
+ if (loweredCon || loweredAlt) {
369
+ const result = t.conditionalExpression(
370
+ mapCall.test,
371
+ loweredCon || mapCall.consequent,
372
+ loweredAlt || mapCall.alternate
373
+ );
374
+ return wrappedInArrow ? t.arrowFunctionExpression([], result) : result;
375
+ }
376
+ return null;
377
+ }
378
+ if (t.isLogicalExpression(mapCall) && (mapCall.operator === "&&" || mapCall.operator === "||")) {
379
+ const loweredRight = tryLowerMapCall(mapCall.right, state);
380
+ if (loweredRight) {
381
+ const result = t.logicalExpression(mapCall.operator, mapCall.left, loweredRight);
382
+ return wrappedInArrow ? t.arrowFunctionExpression([], result) : result;
383
+ }
384
+ return null;
385
+ }
386
+ const lowered = tryLowerMapCall(mapCall, state);
387
+ return lowered;
388
+ }
389
+ function tryLowerMapCall(mapCall, state) {
390
+ if (!t.isCallExpression(mapCall)) return null;
391
+ if (!t.isMemberExpression(mapCall.callee)) return null;
392
+ if (!t.isIdentifier(mapCall.callee.property, { name: "map" })) return null;
393
+ if (mapCall.arguments.length < 1) return null;
394
+ const mapFn = mapCall.arguments[0];
395
+ if (!t.isArrowFunctionExpression(mapFn) && !t.isFunctionExpression(mapFn)) return null;
396
+ let returnExpr = null;
397
+ if (t.isArrowFunctionExpression(mapFn)) {
398
+ if (t.isExpression(mapFn.body)) {
399
+ returnExpr = mapFn.body;
400
+ } else if (t.isBlockStatement(mapFn.body)) {
401
+ const ret = mapFn.body.body.find((s) => t.isReturnStatement(s));
402
+ if (ret) returnExpr = ret.argument;
403
+ }
404
+ } else if (t.isFunctionExpression(mapFn)) {
405
+ const ret = mapFn.body.body.find((s) => t.isReturnStatement(s));
406
+ if (ret) returnExpr = ret.argument;
407
+ }
408
+ if (!returnExpr) return null;
409
+ if (!t.isJSXElement(returnExpr)) return null;
410
+ const attrs = returnExpr.openingElement.attributes;
411
+ let keyAttr = null;
412
+ for (const attr of attrs) {
413
+ if (t.isJSXAttribute(attr) && getAttrName(attr) === "key") {
414
+ keyAttr = attr;
415
+ break;
416
+ }
417
+ }
418
+ if (!keyAttr) {
419
+ if (true) {
420
+ const loc = returnExpr.loc;
421
+ const fileName = state.filename || state.file?.opts?.filename || "<unknown>";
422
+ const lineInfo = loc ? `:${loc.start.line}:${loc.start.column}` : "";
423
+ console.warn(
424
+ `[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.`
425
+ );
426
+ }
427
+ return null;
428
+ }
429
+ const keyValue = getAttributeValue(keyAttr.value);
430
+ if (!keyValue) return null;
431
+ returnExpr.openingElement.attributes = attrs.filter((a) => a !== keyAttr);
432
+ const sourceObj = mapCall.callee.object;
433
+ const source = t.arrowFunctionExpression([], sourceObj);
434
+ const itemParam = mapFn.params[0] ? t.cloneNode(mapFn.params[0], true) : t.identifier("_item");
435
+ const keyFn = t.arrowFunctionExpression([itemParam], t.cloneNode(keyValue, true));
436
+ return t.callExpression(t.identifier("_$mapArray"), [
437
+ source,
438
+ mapFn,
439
+ t.objectExpression([
440
+ t.objectProperty(t.identifier("key"), keyFn),
441
+ t.objectProperty(t.identifier("raw"), t.booleanLiteral(true))
442
+ ])
443
+ ]);
444
+ }
313
445
  function isStaticChild(child) {
314
446
  if (t.isJSXText(child)) return true;
315
447
  if (t.isJSXExpressionContainer(child)) return false;
@@ -333,7 +465,7 @@ function whatBabelPlugin({ types: t }) {
333
465
  }
334
466
  function extractStaticHTML(node) {
335
467
  if (t.isJSXText(node)) {
336
- const text = node.value.replace(/\n\s+/g, " ").trim();
468
+ const text = normalizeJsxText(node.value);
337
469
  return text ? escapeHTML(text) : "";
338
470
  }
339
471
  if (t.isJSXExpressionContainer(node)) {
@@ -373,7 +505,7 @@ function whatBabelPlugin({ types: t }) {
373
505
  html += ">";
374
506
  for (const child of node.children) {
375
507
  if (t.isJSXText(child)) {
376
- const text = child.value.replace(/\n\s+/g, " ").trim();
508
+ const text = normalizeJsxText(child.value);
377
509
  if (text) html += escapeHTML(text);
378
510
  } else if (t.isJSXExpressionContainer(child)) {
379
511
  if (!t.isJSXEmptyExpression(child.expression)) {
@@ -400,15 +532,15 @@ function whatBabelPlugin({ types: t }) {
400
532
  const { node } = path3;
401
533
  const openingElement = node.openingElement;
402
534
  const tagName = openingElement.name.name;
403
- if (isComponent(tagName)) {
404
- return transformComponentFineGrained(path3, state);
405
- }
406
535
  if (tagName === "For") {
407
536
  return transformForFineGrained(path3, state);
408
537
  }
409
538
  if (tagName === "Show") {
410
539
  return transformShowFineGrained(path3, state);
411
540
  }
541
+ if (isComponent(tagName)) {
542
+ return transformComponentFineGrained(path3, state);
543
+ }
412
544
  const attributes = openingElement.attributes;
413
545
  const children = node.children;
414
546
  const allChildrenStatic = children.every(isStaticChild);
@@ -473,7 +605,7 @@ function whatBabelPlugin({ types: t }) {
473
605
  const transformedChildren = [];
474
606
  for (const child of children) {
475
607
  if (t.isJSXText(child)) {
476
- const text = child.value.replace(/\n\s+/g, " ").trim();
608
+ const text = normalizeJsxText(child.value);
477
609
  if (text) transformedChildren.push(t.stringLiteral(text));
478
610
  } else if (t.isJSXExpressionContainer(child)) {
479
611
  if (!t.isJSXEmptyExpression(child.expression)) {
@@ -530,7 +662,7 @@ function whatBabelPlugin({ types: t }) {
530
662
  );
531
663
  continue;
532
664
  }
533
- if (attrName.startsWith("on") && !attrName.includes("|")) {
665
+ if (attrName.startsWith("on") && !attrName.includes("|") && !hasEventModifiers(attrName, state)) {
534
666
  const event = attrName.slice(2).toLowerCase();
535
667
  const handler = getAttributeValue(attr.value);
536
668
  if (DELEGATED_EVENTS.has(event)) {
@@ -543,7 +675,7 @@ function whatBabelPlugin({ types: t }) {
543
675
  "=",
544
676
  t.memberExpression(
545
677
  t.identifier(elId),
546
- t.identifier(`__${event}`)
678
+ t.identifier(`$$${event}`)
547
679
  ),
548
680
  handler
549
681
  )
@@ -561,7 +693,7 @@ function whatBabelPlugin({ types: t }) {
561
693
  }
562
694
  continue;
563
695
  }
564
- if (attrName.startsWith("on") && attrName.includes("|")) {
696
+ if (attrName.startsWith("on") && (attrName.includes("|") || hasEventModifiers(attrName, state))) {
565
697
  const { eventName, modifiers } = parseEventModifiers(attrName);
566
698
  const handler = getAttributeValue(attr.value);
567
699
  const wrappedHandler = createEventHandler(handler, modifiers);
@@ -661,8 +793,9 @@ function whatBabelPlugin({ types: t }) {
661
793
  const domName = normalizeAttrName(attrName);
662
794
  if (isPotentiallyReactive(expr, state.signalNames, state.importedIdentifiers)) {
663
795
  state.needsEffect = true;
796
+ const valueExpr = t.isIdentifier(expr) && (isSignalIdentifier(expr.name, state.signalNames) || state.importedIdentifiers && state.importedIdentifiers.has(expr.name)) ? t.callExpression(expr, []) : expr;
664
797
  const effectCall = t.callExpression(t.identifier("_$effect"), [
665
- t.arrowFunctionExpression([], buildSetPropCall(domName, expr))
798
+ t.arrowFunctionExpression([], buildSetPropCall(domName, valueExpr))
666
799
  ]);
667
800
  if (isUncertainReactive(expr, state.signalNames, state.importedIdentifiers)) {
668
801
  t.addComment(
@@ -684,7 +817,7 @@ function whatBabelPlugin({ types: t }) {
684
817
  let childIndex = 0;
685
818
  for (const child of children) {
686
819
  if (t.isJSXText(child)) {
687
- const text = child.value.replace(/\n\s+/g, " ").trim();
820
+ const text = normalizeJsxText(child.value);
688
821
  if (text) childIndex++;
689
822
  continue;
690
823
  }
@@ -713,22 +846,31 @@ function whatBabelPlugin({ types: t }) {
713
846
  const entriesNeedingRef = entries.filter(
714
847
  (e) => e.type === "expression" || e.type === "component" || e.type === "static" && e.hasAnythingDynamic
715
848
  );
716
- const hasDynamicInsert = entries.some((e) => e.type === "expression" || e.type === "component");
717
- const needsPreCapture = entriesNeedingRef.length >= 2 && hasDynamicInsert;
849
+ const needsPreCapture = entriesNeedingRef.length >= 2;
718
850
  const markerVars = /* @__PURE__ */ new Map();
719
851
  if (needsPreCapture) {
852
+ let prevVar = null;
853
+ let prevIndex = 0;
720
854
  for (const entry of entriesNeedingRef) {
721
- const varName = `_m$${entry.childIndex}`;
855
+ const idx = entry.childIndex;
722
856
  const markerVar = state.nextVarId();
723
- markerVars.set(entry.childIndex, markerVar);
857
+ markerVars.set(idx, markerVar);
858
+ let init;
859
+ if (prevVar === null) {
860
+ init = buildChildAccess(elId, idx);
861
+ } else {
862
+ init = t.identifier(prevVar);
863
+ for (let i = prevIndex; i < idx; i++) {
864
+ init = t.memberExpression(init, t.identifier("nextSibling"));
865
+ }
866
+ }
724
867
  statements.push(
725
868
  t.variableDeclaration("const", [
726
- t.variableDeclarator(
727
- t.identifier(markerVar),
728
- buildChildAccess(elId, entry.childIndex)
729
- )
869
+ t.variableDeclarator(t.identifier(markerVar), init)
730
870
  ])
731
871
  );
872
+ prevVar = markerVar;
873
+ prevIndex = idx;
732
874
  }
733
875
  }
734
876
  function getMarker(idx) {
@@ -739,9 +881,41 @@ function whatBabelPlugin({ types: t }) {
739
881
  }
740
882
  for (const entry of entries) {
741
883
  if (entry.type === "expression") {
742
- const expr = entry.child.expression;
884
+ let expr = entry.child.expression;
743
885
  const marker = getMarker(entry.childIndex);
744
886
  state.needsInsert = true;
887
+ const mapResult = tryLowerMapToMapArray(expr, state);
888
+ if (mapResult) {
889
+ state.needsMapArray = true;
890
+ const isBareMapArray = t.isCallExpression(mapResult) && t.isIdentifier(mapResult.callee) && (mapResult.callee.name === "_$mapArray" || mapResult.callee.name === "mapArray");
891
+ const isArrowAlready = t.isArrowFunctionExpression(mapResult);
892
+ const insertArg = isBareMapArray || isArrowAlready ? mapResult : t.arrowFunctionExpression([], mapResult);
893
+ statements.push(
894
+ t.expressionStatement(
895
+ t.callExpression(t.identifier("_$insert"), [
896
+ t.identifier(elId),
897
+ insertArg,
898
+ marker
899
+ ])
900
+ )
901
+ );
902
+ continue;
903
+ }
904
+ const isMapArrayCall = t.isCallExpression(expr) && t.isIdentifier(expr.callee) && (expr.callee.name === "mapArray" || expr.callee.name === "_$mapArray");
905
+ if (isMapArrayCall) {
906
+ state.needsMapArray = true;
907
+ if (expr.callee.name === "mapArray") expr.callee.name = "_$mapArray";
908
+ statements.push(
909
+ t.expressionStatement(
910
+ t.callExpression(t.identifier("_$insert"), [
911
+ t.identifier(elId),
912
+ expr,
913
+ marker
914
+ ])
915
+ )
916
+ );
917
+ continue;
918
+ }
745
919
  if (isPotentiallyReactive(expr, state.signalNames, state.importedIdentifiers)) {
746
920
  const insertCall = t.callExpression(t.identifier("_$insert"), [
747
921
  t.identifier(elId),
@@ -950,7 +1124,7 @@ function whatBabelPlugin({ types: t }) {
950
1124
  }
951
1125
  continue;
952
1126
  }
953
- if (attrName.startsWith("on") && attrName.includes("|")) {
1127
+ if (attrName.startsWith("on") && (attrName.includes("|") || hasEventModifiers(attrName, state))) {
954
1128
  const { eventName, modifiers } = parseEventModifiers(attrName);
955
1129
  const handler = getAttributeValue(attr.value);
956
1130
  const wrappedHandler = createEventHandler(handler, modifiers);
@@ -968,7 +1142,7 @@ function whatBabelPlugin({ types: t }) {
968
1142
  const transformedChildren = [];
969
1143
  for (const child of children) {
970
1144
  if (t.isJSXText(child)) {
971
- const text = child.value.replace(/\n\s+/g, " ").trim();
1145
+ const text = normalizeJsxText(child.value);
972
1146
  if (text) transformedChildren.push(t.stringLiteral(text));
973
1147
  } else if (t.isJSXExpressionContainer(child)) {
974
1148
  if (!t.isJSXEmptyExpression(child.expression)) {
@@ -1002,10 +1176,24 @@ function whatBabelPlugin({ types: t }) {
1002
1176
  const { node } = path3;
1003
1177
  const attributes = node.openingElement.attributes;
1004
1178
  const children = node.children;
1179
+ if (true) {
1180
+ const fileName = state.filename || state.file?.opts?.filename || "<unknown>";
1181
+ if (!_forInfoWarned.has(fileName)) {
1182
+ _forInfoWarned.add(fileName);
1183
+ const loc = node.loc;
1184
+ const lineInfo = loc ? `:${loc.start.line}:${loc.start.column}` : "";
1185
+ console.info(
1186
+ `[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).`
1187
+ );
1188
+ }
1189
+ }
1005
1190
  let eachExpr = null;
1191
+ let keyExpr = null;
1006
1192
  for (const attr of attributes) {
1007
- if (t.isJSXAttribute(attr) && getAttrName(attr) === "each") {
1008
- eachExpr = getAttributeValue(attr.value);
1193
+ if (t.isJSXAttribute(attr)) {
1194
+ const name = getAttrName(attr);
1195
+ if (name === "each") eachExpr = getAttributeValue(attr.value);
1196
+ else if (name === "key") keyExpr = getAttributeValue(attr.value);
1009
1197
  }
1010
1198
  }
1011
1199
  if (!eachExpr) {
@@ -1026,11 +1214,78 @@ function whatBabelPlugin({ types: t }) {
1026
1214
  return transformElementAsH(path3, state);
1027
1215
  }
1028
1216
  state.needsMapArray = true;
1029
- return t.callExpression(t.identifier("_$mapArray"), [eachExpr, renderFn]);
1217
+ const args = [eachExpr, renderFn];
1218
+ if (keyExpr) {
1219
+ args.push(t.objectExpression([
1220
+ t.objectProperty(t.identifier("key"), keyExpr)
1221
+ ]));
1222
+ }
1223
+ return t.callExpression(t.identifier("_$mapArray"), args);
1030
1224
  }
1031
1225
  function transformShowFineGrained(path3, state) {
1032
- state.needsCreateComponent = true;
1033
- return transformComponentFineGrained(path3, state);
1226
+ const { node } = path3;
1227
+ const attributes = node.openingElement.attributes;
1228
+ const children = node.children;
1229
+ let whenExpr = null;
1230
+ let fallbackExpr = null;
1231
+ for (const attr of attributes) {
1232
+ if (t.isJSXAttribute(attr)) {
1233
+ const name = getAttrName(attr);
1234
+ if (name === "when") whenExpr = getAttributeValue(attr.value);
1235
+ else if (name === "fallback") fallbackExpr = getAttributeValue(attr.value);
1236
+ }
1237
+ }
1238
+ if (!whenExpr) {
1239
+ throw path3.buildCodeFrameError(
1240
+ '<Show> requires a "when" prop. Example: <Show when={isOpen} fallback={null}>...</Show>'
1241
+ );
1242
+ }
1243
+ let contentExpr = null;
1244
+ for (const child of children) {
1245
+ if (t.isJSXExpressionContainer(child) && !t.isJSXEmptyExpression(child.expression)) {
1246
+ contentExpr = child.expression;
1247
+ break;
1248
+ }
1249
+ }
1250
+ if (!contentExpr) {
1251
+ const transformedChildren = [];
1252
+ for (const child of children) {
1253
+ if (t.isJSXText(child)) {
1254
+ const text = normalizeJsxText(child.value);
1255
+ if (text) transformedChildren.push(t.stringLiteral(text));
1256
+ } else if (t.isJSXElement(child)) {
1257
+ transformedChildren.push(transformElementFineGrained({ node: child }, state));
1258
+ }
1259
+ }
1260
+ if (transformedChildren.length === 1) {
1261
+ contentExpr = transformedChildren[0];
1262
+ } else if (transformedChildren.length > 1) {
1263
+ contentExpr = t.arrayExpression(transformedChildren);
1264
+ } else {
1265
+ contentExpr = t.nullLiteral();
1266
+ }
1267
+ }
1268
+ let condition;
1269
+ if (t.isCallExpression(whenExpr)) {
1270
+ condition = whenExpr;
1271
+ } else if (t.isArrowFunctionExpression(whenExpr) && t.isExpression(whenExpr.body)) {
1272
+ condition = whenExpr.body;
1273
+ } else if (t.isIdentifier(whenExpr) && (state.signalNames && isSignalIdentifier(whenExpr.name, state.signalNames) || state.importedIdentifiers && state.importedIdentifiers.has(whenExpr.name))) {
1274
+ condition = t.callExpression(whenExpr, []);
1275
+ } else {
1276
+ condition = whenExpr;
1277
+ }
1278
+ const vId = path3.scope ? path3.scope.generateUidIdentifier("v") : t.identifier("_v");
1279
+ const consequent = t.isFunction(contentExpr) ? t.callExpression(contentExpr, [t.cloneNode(vId)]) : contentExpr;
1280
+ const alternate = fallbackExpr || t.nullLiteral();
1281
+ return t.arrowFunctionExpression([], t.blockStatement([
1282
+ t.variableDeclaration("const", [
1283
+ t.variableDeclarator(vId, condition)
1284
+ ]),
1285
+ t.returnStatement(
1286
+ t.conditionalExpression(t.cloneNode(vId), consequent, alternate)
1287
+ )
1288
+ ]));
1034
1289
  }
1035
1290
  function transformFragmentFineGrained(path3, state) {
1036
1291
  const { node } = path3;
@@ -1038,7 +1293,7 @@ function whatBabelPlugin({ types: t }) {
1038
1293
  const transformed = [];
1039
1294
  for (const child of children) {
1040
1295
  if (t.isJSXText(child)) {
1041
- const text = child.value.replace(/\n\s+/g, " ").trim();
1296
+ const text = normalizeJsxText(child.value);
1042
1297
  if (text) transformed.push(t.stringLiteral(text));
1043
1298
  } else if (t.isJSXExpressionContainer(child)) {
1044
1299
  if (!t.isJSXEmptyExpression(child.expression)) {
@@ -1248,20 +1503,31 @@ function whatBabelPlugin({ types: t }) {
1248
1503
  }
1249
1504
  },
1250
1505
  JSXElement(path3, state) {
1251
- state.signalNames = collectSignalNamesFromScope(path3);
1506
+ const scope = path3.scope;
1507
+ let cache = state._signalNamesCache;
1508
+ if (!cache) cache = state._signalNamesCache = /* @__PURE__ */ new WeakMap();
1509
+ let names = cache.get(scope);
1510
+ if (!names) {
1511
+ names = collectSignalNamesFromScope(path3);
1512
+ cache.set(scope, names);
1513
+ }
1514
+ state.signalNames = names;
1252
1515
  state._pendingSetup = [];
1253
1516
  const transformed = transformElementFineGrained(path3, state);
1254
1517
  const pending = state._pendingSetup;
1255
1518
  state._pendingSetup = [];
1256
1519
  if (pending.length > 0) {
1257
1520
  let stmtPath = path3;
1521
+ let crossedFunctionBoundary = false;
1258
1522
  while (stmtPath && !stmtPath.isStatement()) {
1523
+ if (stmtPath.isArrowFunctionExpression() || stmtPath.isFunctionExpression()) {
1524
+ crossedFunctionBoundary = true;
1525
+ }
1259
1526
  stmtPath = stmtPath.parentPath;
1260
1527
  }
1261
- if (stmtPath && stmtPath.isStatement()) {
1262
- for (const stmt of pending) {
1263
- stmtPath.insertBefore(stmt);
1264
- }
1528
+ const inStatementList = stmtPath && stmtPath.isStatement() && (stmtPath.listKey === "body" || stmtPath.listKey === "consequent") && Array.isArray(stmtPath.container);
1529
+ if (inStatementList && !crossedFunctionBoundary) {
1530
+ stmtPath.insertBefore(pending);
1265
1531
  path3.replaceWith(transformed);
1266
1532
  } else {
1267
1533
  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
  }