what-compiler 0.8.4 → 0.11.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.
@@ -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 = path.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 } = path;
397
529
  const openingElement = node.openingElement;
398
530
  const tagName = openingElement.name.name;
399
- if (isComponent(tagName)) {
400
- return transformComponentFineGrained(path, state);
401
- }
402
531
  if (tagName === "For") {
403
532
  return transformForFineGrained(path, state);
404
533
  }
405
534
  if (tagName === "Show") {
406
535
  return transformShowFineGrained(path, state);
407
536
  }
537
+ if (isComponent(tagName)) {
538
+ return transformComponentFineGrained(path, state);
539
+ }
408
540
  const attributes = openingElement.attributes;
409
541
  const children = node.children;
410
542
  const allChildrenStatic = children.every(isStaticChild);
@@ -441,7 +573,7 @@ function whatBabelPlugin({ types: t }) {
441
573
  t.variableDeclarator(t.identifier(elId), t.callExpression(t.identifier(tmplId), []))
442
574
  ])
443
575
  ];
444
- applyDynamicAttrs(statements, elId, attributes, state);
576
+ applyDynamicAttrs(statements, elId, attributes, state, tagName);
445
577
  applyDynamicChildren(statements, elId, children, node, state);
446
578
  if (!state._pendingSetup) state._pendingSetup = [];
447
579
  state._pendingSetup.push(...statements);
@@ -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)) {
@@ -484,8 +616,33 @@ function whatBabelPlugin({ types: t }) {
484
616
  const propsExpr = props.length > 0 ? t.objectExpression(props) : t.nullLiteral();
485
617
  return t.callExpression(t.identifier("h"), [t.stringLiteral(tagName), propsExpr, ...transformedChildren]);
486
618
  }
487
- function applyDynamicAttrs(statements, elId, attributes, state) {
619
+ const VALUE_PROP_TAGS = /* @__PURE__ */ new Set(["input", "textarea", "select", "option"]);
620
+ function applyDynamicAttrs(statements, elId, attributes, state, tagName) {
488
621
  function buildSetPropCall(propName, valueExpr) {
622
+ if (propName === "class") {
623
+ state.needsSetClass = true;
624
+ return t.callExpression(t.identifier("_$setClass"), [t.identifier(elId), valueExpr]);
625
+ }
626
+ if (propName === "style") {
627
+ state.needsSetStyle = true;
628
+ return t.callExpression(t.identifier("_$setStyle"), [t.identifier(elId), valueExpr]);
629
+ }
630
+ if (propName === "value" && tagName && VALUE_PROP_TAGS.has(tagName)) {
631
+ state.needsSetValue = true;
632
+ return t.callExpression(t.identifier("_$setValue"), [t.identifier(elId), valueExpr]);
633
+ }
634
+ if (propName === "checked" && tagName === "input") {
635
+ state.needsSetChecked = true;
636
+ return t.callExpression(t.identifier("_$setChecked"), [t.identifier(elId), valueExpr]);
637
+ }
638
+ if (propName.startsWith("data-") || propName.startsWith("aria-")) {
639
+ state.needsSetAttr = true;
640
+ return t.callExpression(t.identifier("_$setAttr"), [
641
+ t.identifier(elId),
642
+ t.stringLiteral(propName),
643
+ valueExpr
644
+ ]);
645
+ }
489
646
  state.needsSetProp = true;
490
647
  return t.callExpression(t.identifier("_$setProp"), [
491
648
  t.identifier(elId),
@@ -493,6 +650,14 @@ function whatBabelPlugin({ types: t }) {
493
650
  valueExpr
494
651
  ]);
495
652
  }
653
+ let delegateInitEmitted = false;
654
+ function emitDelegateInit() {
655
+ if (delegateInitEmitted) return;
656
+ delegateInitEmitted = true;
657
+ statements.push(
658
+ t.expressionStatement(t.callExpression(t.identifier("_$delegate$"), []))
659
+ );
660
+ }
496
661
  for (const attr of attributes) {
497
662
  if (t.isJSXSpreadAttribute(attr)) {
498
663
  state.needsSpread = true;
@@ -526,20 +691,21 @@ function whatBabelPlugin({ types: t }) {
526
691
  );
527
692
  continue;
528
693
  }
529
- if (attrName.startsWith("on") && !attrName.includes("|")) {
694
+ if (attrName.startsWith("on") && !attrName.includes("|") && !hasEventModifiers(attrName, state)) {
530
695
  const event = attrName.slice(2).toLowerCase();
531
696
  const handler = getAttributeValue(attr.value);
532
697
  if (DELEGATED_EVENTS.has(event)) {
533
698
  state.needsDelegation = true;
534
699
  if (!state.delegatedEvents) state.delegatedEvents = /* @__PURE__ */ new Set();
535
700
  state.delegatedEvents.add(event);
701
+ emitDelegateInit();
536
702
  statements.push(
537
703
  t.expressionStatement(
538
704
  t.assignmentExpression(
539
705
  "=",
540
706
  t.memberExpression(
541
707
  t.identifier(elId),
542
- t.identifier(`__${event}`)
708
+ t.identifier(`$$${event}`)
543
709
  ),
544
710
  handler
545
711
  )
@@ -557,7 +723,7 @@ function whatBabelPlugin({ types: t }) {
557
723
  }
558
724
  continue;
559
725
  }
560
- if (attrName.startsWith("on") && attrName.includes("|")) {
726
+ if (attrName.startsWith("on") && (attrName.includes("|") || hasEventModifiers(attrName, state))) {
561
727
  const { eventName, modifiers } = parseEventModifiers(attrName);
562
728
  const handler = getAttributeValue(attr.value);
563
729
  const wrappedHandler = createEventHandler(handler, modifiers);
@@ -657,8 +823,9 @@ function whatBabelPlugin({ types: t }) {
657
823
  const domName = normalizeAttrName(attrName);
658
824
  if (isPotentiallyReactive(expr, state.signalNames, state.importedIdentifiers)) {
659
825
  state.needsEffect = true;
826
+ const valueExpr = t.isIdentifier(expr) && (isSignalIdentifier(expr.name, state.signalNames) || state.importedIdentifiers && state.importedIdentifiers.has(expr.name)) ? t.callExpression(expr, []) : expr;
660
827
  const effectCall = t.callExpression(t.identifier("_$effect"), [
661
- t.arrowFunctionExpression([], buildSetPropCall(domName, expr))
828
+ t.arrowFunctionExpression([], buildSetPropCall(domName, valueExpr))
662
829
  ]);
663
830
  if (isUncertainReactive(expr, state.signalNames, state.importedIdentifiers)) {
664
831
  t.addComment(
@@ -675,12 +842,56 @@ function whatBabelPlugin({ types: t }) {
675
842
  }
676
843
  }
677
844
  }
845
+ function buildsDOM(node) {
846
+ if (!node || typeof node !== "object") return false;
847
+ if (Array.isArray(node)) return node.some(buildsDOM);
848
+ if (node.type === "JSXElement" || node.type === "JSXFragment") return true;
849
+ if (node.type === "CallExpression" && node.callee && node.callee.type === "Identifier" && (node.callee.name === "_$mapArray" || node.callee.name === "mapArray")) {
850
+ return true;
851
+ }
852
+ for (const key of Object.keys(node)) {
853
+ if (key === "loc" || key === "start" || key === "end" || key === "leadingComments" || key === "trailingComments" || key === "innerComments") continue;
854
+ const v = node[key];
855
+ if (v && typeof v === "object" && buildsDOM(v)) return true;
856
+ }
857
+ return false;
858
+ }
859
+ function memoizeBranchCondition(expr, statements, state) {
860
+ let testExpr = null;
861
+ let isTernary = false;
862
+ if (t.isConditionalExpression(expr)) {
863
+ testExpr = expr.test;
864
+ isTernary = true;
865
+ } else if (t.isLogicalExpression(expr) && (expr.operator === "&&" || expr.operator === "||")) {
866
+ testExpr = expr.left;
867
+ } else {
868
+ return expr;
869
+ }
870
+ if (!isPotentiallyReactive(testExpr, state.signalNames, state.importedIdentifiers)) return expr;
871
+ const branches = isTernary ? [expr.consequent, expr.alternate] : [expr.right];
872
+ if (!branches.some(buildsDOM)) return expr;
873
+ const condId = state.nextMemoId();
874
+ state.needsMemo = true;
875
+ const memoBody = isTernary ? t.unaryExpression("!", t.unaryExpression("!", testExpr)) : testExpr;
876
+ statements.push(
877
+ t.variableDeclaration("const", [
878
+ t.variableDeclarator(
879
+ t.identifier(condId),
880
+ t.callExpression(t.identifier("_$memo"), [
881
+ t.arrowFunctionExpression([], memoBody)
882
+ ])
883
+ )
884
+ ])
885
+ );
886
+ const condRead = t.callExpression(t.identifier(condId), []);
887
+ return isTernary ? t.conditionalExpression(condRead, expr.consequent, expr.alternate) : t.logicalExpression(expr.operator, condRead, expr.right);
888
+ }
678
889
  function applyDynamicChildren(statements, elId, children, parentNode, state) {
679
890
  const entries = [];
680
891
  let childIndex = 0;
681
892
  for (const child of children) {
682
893
  if (t.isJSXText(child)) {
683
- const text = child.value.replace(/\n\s+/g, " ").trim();
894
+ const text = normalizeJsxText(child.value);
684
895
  if (text) childIndex++;
685
896
  continue;
686
897
  }
@@ -709,22 +920,31 @@ function whatBabelPlugin({ types: t }) {
709
920
  const entriesNeedingRef = entries.filter(
710
921
  (e) => e.type === "expression" || e.type === "component" || e.type === "static" && e.hasAnythingDynamic
711
922
  );
712
- const hasDynamicInsert = entries.some((e) => e.type === "expression" || e.type === "component");
713
- const needsPreCapture = entriesNeedingRef.length >= 2 && hasDynamicInsert;
923
+ const needsPreCapture = entriesNeedingRef.length >= 2;
714
924
  const markerVars = /* @__PURE__ */ new Map();
715
925
  if (needsPreCapture) {
926
+ let prevVar = null;
927
+ let prevIndex = 0;
716
928
  for (const entry of entriesNeedingRef) {
717
- const varName = `_m$${entry.childIndex}`;
929
+ const idx = entry.childIndex;
718
930
  const markerVar = state.nextVarId();
719
- markerVars.set(entry.childIndex, markerVar);
931
+ markerVars.set(idx, markerVar);
932
+ let init;
933
+ if (prevVar === null) {
934
+ init = buildChildAccess(elId, idx);
935
+ } else {
936
+ init = t.identifier(prevVar);
937
+ for (let i = prevIndex; i < idx; i++) {
938
+ init = t.memberExpression(init, t.identifier("nextSibling"));
939
+ }
940
+ }
720
941
  statements.push(
721
942
  t.variableDeclaration("const", [
722
- t.variableDeclarator(
723
- t.identifier(markerVar),
724
- buildChildAccess(elId, entry.childIndex)
725
- )
943
+ t.variableDeclarator(t.identifier(markerVar), init)
726
944
  ])
727
945
  );
946
+ prevVar = markerVar;
947
+ prevIndex = idx;
728
948
  }
729
949
  }
730
950
  function getMarker(idx) {
@@ -735,10 +955,48 @@ function whatBabelPlugin({ types: t }) {
735
955
  }
736
956
  for (const entry of entries) {
737
957
  if (entry.type === "expression") {
738
- const expr = entry.child.expression;
958
+ let expr = entry.child.expression;
739
959
  const marker = getMarker(entry.childIndex);
740
960
  state.needsInsert = true;
961
+ let mapResult = tryLowerMapToMapArray(expr, state);
962
+ if (mapResult) {
963
+ state.needsMapArray = true;
964
+ const isBareMapArray = t.isCallExpression(mapResult) && t.isIdentifier(mapResult.callee) && (mapResult.callee.name === "_$mapArray" || mapResult.callee.name === "mapArray");
965
+ const isArrowAlready = t.isArrowFunctionExpression(mapResult);
966
+ if (isArrowAlready && t.isExpression(mapResult.body)) {
967
+ mapResult.body = memoizeBranchCondition(mapResult.body, statements, state);
968
+ } else if (!isBareMapArray && !isArrowAlready) {
969
+ mapResult = memoizeBranchCondition(mapResult, statements, state);
970
+ }
971
+ const insertArg = isBareMapArray || isArrowAlready ? mapResult : t.arrowFunctionExpression([], mapResult);
972
+ statements.push(
973
+ t.expressionStatement(
974
+ t.callExpression(t.identifier("_$insert"), [
975
+ t.identifier(elId),
976
+ insertArg,
977
+ marker
978
+ ])
979
+ )
980
+ );
981
+ continue;
982
+ }
983
+ const isMapArrayCall = t.isCallExpression(expr) && t.isIdentifier(expr.callee) && (expr.callee.name === "mapArray" || expr.callee.name === "_$mapArray");
984
+ if (isMapArrayCall) {
985
+ state.needsMapArray = true;
986
+ if (expr.callee.name === "mapArray") expr.callee.name = "_$mapArray";
987
+ statements.push(
988
+ t.expressionStatement(
989
+ t.callExpression(t.identifier("_$insert"), [
990
+ t.identifier(elId),
991
+ expr,
992
+ marker
993
+ ])
994
+ )
995
+ );
996
+ continue;
997
+ }
741
998
  if (isPotentiallyReactive(expr, state.signalNames, state.importedIdentifiers)) {
999
+ expr = memoizeBranchCondition(expr, statements, state);
742
1000
  const insertCall = t.callExpression(t.identifier("_$insert"), [
743
1001
  t.identifier(elId),
744
1002
  t.arrowFunctionExpression([], expr),
@@ -796,7 +1054,7 @@ function whatBabelPlugin({ types: t }) {
796
1054
  ])
797
1055
  );
798
1056
  }
799
- applyDynamicAttrs(statements, childElRef, entry.child.openingElement.attributes, state);
1057
+ applyDynamicAttrs(statements, childElRef, entry.child.openingElement.attributes, state, entry.child.openingElement.name.name);
800
1058
  applyDynamicChildren(statements, childElRef, entry.child.children, entry.child, state);
801
1059
  continue;
802
1060
  }
@@ -804,8 +1062,9 @@ function whatBabelPlugin({ types: t }) {
804
1062
  for (const fChild of entry.child.children) {
805
1063
  if (t.isJSXExpressionContainer(fChild) && !t.isJSXEmptyExpression(fChild.expression)) {
806
1064
  state.needsInsert = true;
807
- const expr = fChild.expression;
1065
+ let expr = fChild.expression;
808
1066
  if (isPotentiallyReactive(expr, state.signalNames, state.importedIdentifiers)) {
1067
+ expr = memoizeBranchCondition(expr, statements, state);
809
1068
  statements.push(
810
1069
  t.expressionStatement(
811
1070
  t.callExpression(t.identifier("_$insert"), [
@@ -946,7 +1205,7 @@ function whatBabelPlugin({ types: t }) {
946
1205
  }
947
1206
  continue;
948
1207
  }
949
- if (attrName.startsWith("on") && attrName.includes("|")) {
1208
+ if (attrName.startsWith("on") && (attrName.includes("|") || hasEventModifiers(attrName, state))) {
950
1209
  const { eventName, modifiers } = parseEventModifiers(attrName);
951
1210
  const handler = getAttributeValue(attr.value);
952
1211
  const wrappedHandler = createEventHandler(handler, modifiers);
@@ -964,7 +1223,7 @@ function whatBabelPlugin({ types: t }) {
964
1223
  const transformedChildren = [];
965
1224
  for (const child of children) {
966
1225
  if (t.isJSXText(child)) {
967
- const text = child.value.replace(/\n\s+/g, " ").trim();
1226
+ const text = normalizeJsxText(child.value);
968
1227
  if (text) transformedChildren.push(t.stringLiteral(text));
969
1228
  } else if (t.isJSXExpressionContainer(child)) {
970
1229
  if (!t.isJSXEmptyExpression(child.expression)) {
@@ -998,10 +1257,24 @@ function whatBabelPlugin({ types: t }) {
998
1257
  const { node } = path;
999
1258
  const attributes = node.openingElement.attributes;
1000
1259
  const children = node.children;
1260
+ if (true) {
1261
+ const fileName = state.filename || state.file?.opts?.filename || "<unknown>";
1262
+ if (!_forInfoWarned.has(fileName)) {
1263
+ _forInfoWarned.add(fileName);
1264
+ const loc = node.loc;
1265
+ const lineInfo = loc ? `:${loc.start.line}:${loc.start.column}` : "";
1266
+ console.info(
1267
+ `[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).`
1268
+ );
1269
+ }
1270
+ }
1001
1271
  let eachExpr = null;
1272
+ let keyExpr = null;
1002
1273
  for (const attr of attributes) {
1003
- if (t.isJSXAttribute(attr) && getAttrName(attr) === "each") {
1004
- eachExpr = getAttributeValue(attr.value);
1274
+ if (t.isJSXAttribute(attr)) {
1275
+ const name = getAttrName(attr);
1276
+ if (name === "each") eachExpr = getAttributeValue(attr.value);
1277
+ else if (name === "key") keyExpr = getAttributeValue(attr.value);
1005
1278
  }
1006
1279
  }
1007
1280
  if (!eachExpr) {
@@ -1022,11 +1295,124 @@ function whatBabelPlugin({ types: t }) {
1022
1295
  return transformElementAsH(path, state);
1023
1296
  }
1024
1297
  state.needsMapArray = true;
1025
- return t.callExpression(t.identifier("_$mapArray"), [eachExpr, renderFn]);
1298
+ const args = [eachExpr, renderFn];
1299
+ if (keyExpr) {
1300
+ args.push(t.objectExpression([
1301
+ t.objectProperty(t.identifier("key"), keyExpr)
1302
+ ]));
1303
+ }
1304
+ return t.callExpression(t.identifier("_$mapArray"), args);
1026
1305
  }
1027
1306
  function transformShowFineGrained(path, state) {
1028
- state.needsCreateComponent = true;
1029
- return transformComponentFineGrained(path, state);
1307
+ const { node } = path;
1308
+ const attributes = node.openingElement.attributes;
1309
+ const children = node.children;
1310
+ let whenExpr = null;
1311
+ let fallbackExpr = null;
1312
+ for (const attr of attributes) {
1313
+ if (t.isJSXAttribute(attr)) {
1314
+ const name = getAttrName(attr);
1315
+ if (name === "when") whenExpr = getAttributeValue(attr.value);
1316
+ else if (name === "fallback") fallbackExpr = getAttributeValue(attr.value);
1317
+ }
1318
+ }
1319
+ if (!whenExpr) {
1320
+ throw path.buildCodeFrameError(
1321
+ '<Show> requires a "when" prop. Example: <Show when={isOpen} fallback={null}>...</Show>'
1322
+ );
1323
+ }
1324
+ let contentExpr = null;
1325
+ for (const child of children) {
1326
+ if (t.isJSXExpressionContainer(child) && !t.isJSXEmptyExpression(child.expression)) {
1327
+ contentExpr = child.expression;
1328
+ break;
1329
+ }
1330
+ }
1331
+ if (!contentExpr) {
1332
+ const transformedChildren = [];
1333
+ for (const child of children) {
1334
+ if (t.isJSXText(child)) {
1335
+ const text = normalizeJsxText(child.value);
1336
+ if (text) transformedChildren.push(t.stringLiteral(text));
1337
+ } else if (t.isJSXElement(child)) {
1338
+ transformedChildren.push(transformElementFineGrained({ node: child }, state));
1339
+ }
1340
+ }
1341
+ if (transformedChildren.length === 1) {
1342
+ contentExpr = transformedChildren[0];
1343
+ } else if (transformedChildren.length > 1) {
1344
+ contentExpr = t.arrayExpression(transformedChildren);
1345
+ } else {
1346
+ contentExpr = t.nullLiteral();
1347
+ }
1348
+ }
1349
+ let condition;
1350
+ if (t.isCallExpression(whenExpr)) {
1351
+ condition = whenExpr;
1352
+ } else if (t.isArrowFunctionExpression(whenExpr) && t.isExpression(whenExpr.body)) {
1353
+ condition = whenExpr.body;
1354
+ } else if (t.isIdentifier(whenExpr) && (state.signalNames && isSignalIdentifier(whenExpr.name, state.signalNames) || state.importedIdentifiers && state.importedIdentifiers.has(whenExpr.name))) {
1355
+ condition = t.callExpression(whenExpr, []);
1356
+ } else {
1357
+ condition = whenExpr;
1358
+ }
1359
+ const vId = path.scope ? path.scope.generateUidIdentifier("v") : t.identifier("_v");
1360
+ const contentIsFn = t.isFunction(contentExpr);
1361
+ const consequent = contentIsFn ? t.callExpression(contentExpr, [t.cloneNode(vId)]) : contentExpr;
1362
+ const alternate = fallbackExpr || t.nullLiteral();
1363
+ if (isPotentiallyReactive(condition, state.signalNames, state.importedIdentifiers)) {
1364
+ const condId = state.nextMemoId();
1365
+ state.needsMemo = true;
1366
+ const memoBody = contentIsFn ? condition : t.unaryExpression("!", t.unaryExpression("!", condition));
1367
+ if (!state._pendingSetup) state._pendingSetup = [];
1368
+ state._pendingSetup.push(
1369
+ t.variableDeclaration("const", [
1370
+ t.variableDeclarator(
1371
+ t.identifier(condId),
1372
+ t.callExpression(t.identifier("_$memo"), [
1373
+ t.arrowFunctionExpression([], memoBody)
1374
+ ])
1375
+ )
1376
+ ])
1377
+ );
1378
+ condition = t.callExpression(t.identifier(condId), []);
1379
+ }
1380
+ return t.arrowFunctionExpression([], t.blockStatement([
1381
+ t.variableDeclaration("const", [
1382
+ t.variableDeclarator(vId, condition)
1383
+ ]),
1384
+ t.returnStatement(
1385
+ t.conditionalExpression(t.cloneNode(vId), consequent, alternate)
1386
+ )
1387
+ ]));
1388
+ }
1389
+ function lowerFragmentExprChild(expr, state) {
1390
+ if (!state._pendingSetup) state._pendingSetup = [];
1391
+ const setup = state._pendingSetup;
1392
+ const mapResult = tryLowerMapToMapArray(expr, state);
1393
+ if (mapResult) {
1394
+ state.needsMapArray = true;
1395
+ const isBareMapArray = t.isCallExpression(mapResult) && t.isIdentifier(mapResult.callee) && (mapResult.callee.name === "_$mapArray" || mapResult.callee.name === "mapArray");
1396
+ const isArrowAlready = t.isArrowFunctionExpression(mapResult);
1397
+ if (isArrowAlready && t.isExpression(mapResult.body)) {
1398
+ mapResult.body = memoizeBranchCondition(mapResult.body, setup, state);
1399
+ return mapResult;
1400
+ }
1401
+ if (isBareMapArray) return mapResult;
1402
+ const memoized = memoizeBranchCondition(mapResult, setup, state);
1403
+ return t.arrowFunctionExpression([], memoized);
1404
+ }
1405
+ const isMapArrayCall = t.isCallExpression(expr) && t.isIdentifier(expr.callee) && (expr.callee.name === "mapArray" || expr.callee.name === "_$mapArray");
1406
+ if (isMapArrayCall) {
1407
+ state.needsMapArray = true;
1408
+ if (expr.callee.name === "mapArray") expr.callee.name = "_$mapArray";
1409
+ return expr;
1410
+ }
1411
+ if (isPotentiallyReactive(expr, state.signalNames, state.importedIdentifiers)) {
1412
+ expr = memoizeBranchCondition(expr, setup, state);
1413
+ return t.arrowFunctionExpression([], expr);
1414
+ }
1415
+ return expr;
1030
1416
  }
1031
1417
  function transformFragmentFineGrained(path, state) {
1032
1418
  const { node } = path;
@@ -1034,11 +1420,11 @@ function whatBabelPlugin({ types: t }) {
1034
1420
  const transformed = [];
1035
1421
  for (const child of children) {
1036
1422
  if (t.isJSXText(child)) {
1037
- const text = child.value.replace(/\n\s+/g, " ").trim();
1423
+ const text = normalizeJsxText(child.value);
1038
1424
  if (text) transformed.push(t.stringLiteral(text));
1039
1425
  } else if (t.isJSXExpressionContainer(child)) {
1040
1426
  if (!t.isJSXEmptyExpression(child.expression)) {
1041
- transformed.push(child.expression);
1427
+ transformed.push(lowerFragmentExprChild(child.expression, state));
1042
1428
  }
1043
1429
  } else if (t.isJSXElement(child)) {
1044
1430
  transformed.push(transformElementFineGrained({ node: child }, state));
@@ -1058,6 +1444,46 @@ function whatBabelPlugin({ types: t }) {
1058
1444
  state.templates.push({ id, html });
1059
1445
  return id;
1060
1446
  }
1447
+ function transformJsxRoot(path, state, transform) {
1448
+ const scope = path.scope;
1449
+ let cache = state._signalNamesCache;
1450
+ if (!cache) cache = state._signalNamesCache = /* @__PURE__ */ new WeakMap();
1451
+ let names = cache.get(scope);
1452
+ if (!names) {
1453
+ names = collectSignalNamesFromScope(path);
1454
+ cache.set(scope, names);
1455
+ }
1456
+ state.signalNames = names;
1457
+ state._pendingSetup = [];
1458
+ const transformed = transform(path, state);
1459
+ const pending = state._pendingSetup;
1460
+ state._pendingSetup = [];
1461
+ if (pending.length > 0) {
1462
+ let stmtPath = path;
1463
+ let crossedFunctionBoundary = false;
1464
+ while (stmtPath && !stmtPath.isStatement()) {
1465
+ if (stmtPath.isArrowFunctionExpression() || stmtPath.isFunctionExpression()) {
1466
+ crossedFunctionBoundary = true;
1467
+ }
1468
+ stmtPath = stmtPath.parentPath;
1469
+ }
1470
+ const inStatementList = stmtPath && stmtPath.isStatement() && (stmtPath.listKey === "body" || stmtPath.listKey === "consequent") && Array.isArray(stmtPath.container);
1471
+ if (inStatementList && !crossedFunctionBoundary) {
1472
+ stmtPath.insertBefore(pending);
1473
+ path.replaceWith(transformed);
1474
+ } else {
1475
+ pending.push(t.returnStatement(transformed));
1476
+ path.replaceWith(
1477
+ t.callExpression(
1478
+ t.arrowFunctionExpression([], t.blockStatement(pending)),
1479
+ []
1480
+ )
1481
+ );
1482
+ }
1483
+ } else {
1484
+ path.replaceWith(transformed);
1485
+ }
1486
+ }
1061
1487
  return {
1062
1488
  name: "what-jsx-transform",
1063
1489
  visitor: {
@@ -1069,6 +1495,12 @@ function whatBabelPlugin({ types: t }) {
1069
1495
  state.needsMapArray = false;
1070
1496
  state.needsSpread = false;
1071
1497
  state.needsSetProp = false;
1498
+ state.needsMemo = false;
1499
+ state.needsSetClass = false;
1500
+ state.needsSetStyle = false;
1501
+ state.needsSetAttr = false;
1502
+ state.needsSetValue = false;
1503
+ state.needsSetChecked = false;
1072
1504
  state.needsH = false;
1073
1505
  state.needsCreateComponent = false;
1074
1506
  state.needsFragment = false;
@@ -1079,8 +1511,10 @@ function whatBabelPlugin({ types: t }) {
1079
1511
  state.templateMap = /* @__PURE__ */ new Map();
1080
1512
  state.templateCount = 0;
1081
1513
  state._varCounter = 0;
1514
+ state._memoCounter = 0;
1082
1515
  state._pendingSetup = [];
1083
1516
  state.nextVarId = () => `_el$${state._varCounter++}`;
1517
+ state.nextMemoId = () => `_c$${state._memoCounter++}`;
1084
1518
  state.signalNames = /* @__PURE__ */ new Set();
1085
1519
  state.importedIdentifiers = /* @__PURE__ */ new Set();
1086
1520
  for (const node of path.node.body) {
@@ -1136,20 +1570,19 @@ function whatBabelPlugin({ types: t }) {
1136
1570
  },
1137
1571
  exit(path, state) {
1138
1572
  for (const tmpl of state.templates.reverse()) {
1573
+ const tmplCall = t.callExpression(t.identifier("_$template"), [t.stringLiteral(tmpl.html)]);
1574
+ t.addComment(tmplCall, "leading", " @__PURE__ ");
1139
1575
  path.unshiftContainer(
1140
1576
  "body",
1141
1577
  t.variableDeclaration("const", [
1142
- t.variableDeclarator(
1143
- t.identifier(tmpl.id),
1144
- t.callExpression(t.identifier("_$template"), [t.stringLiteral(tmpl.html)])
1145
- )
1578
+ t.variableDeclarator(t.identifier(tmpl.id), tmplCall)
1146
1579
  ])
1147
1580
  );
1148
1581
  }
1149
1582
  const fgSpecifiers = [];
1150
1583
  if (state.needsTemplate) {
1151
1584
  fgSpecifiers.push(
1152
- t.importSpecifier(t.identifier("_$template"), t.identifier("template"))
1585
+ t.importSpecifier(t.identifier("_$template"), t.identifier("_$template"))
1153
1586
  );
1154
1587
  }
1155
1588
  if (state.needsInsert) {
@@ -1177,6 +1610,36 @@ function whatBabelPlugin({ types: t }) {
1177
1610
  t.importSpecifier(t.identifier("_$setProp"), t.identifier("setProp"))
1178
1611
  );
1179
1612
  }
1613
+ if (state.needsMemo) {
1614
+ fgSpecifiers.push(
1615
+ t.importSpecifier(t.identifier("_$memo"), t.identifier("memo"))
1616
+ );
1617
+ }
1618
+ if (state.needsSetClass) {
1619
+ fgSpecifiers.push(
1620
+ t.importSpecifier(t.identifier("_$setClass"), t.identifier("setClass"))
1621
+ );
1622
+ }
1623
+ if (state.needsSetStyle) {
1624
+ fgSpecifiers.push(
1625
+ t.importSpecifier(t.identifier("_$setStyle"), t.identifier("setStyle"))
1626
+ );
1627
+ }
1628
+ if (state.needsSetAttr) {
1629
+ fgSpecifiers.push(
1630
+ t.importSpecifier(t.identifier("_$setAttr"), t.identifier("setAttr"))
1631
+ );
1632
+ }
1633
+ if (state.needsSetValue) {
1634
+ fgSpecifiers.push(
1635
+ t.importSpecifier(t.identifier("_$setValue"), t.identifier("setValue"))
1636
+ );
1637
+ }
1638
+ if (state.needsSetChecked) {
1639
+ fgSpecifiers.push(
1640
+ t.importSpecifier(t.identifier("_$setChecked"), t.identifier("setChecked"))
1641
+ );
1642
+ }
1180
1643
  if (state.needsCreateComponent) {
1181
1644
  fgSpecifiers.push(
1182
1645
  t.importSpecifier(t.identifier("_$createComponent"), t.identifier("_$createComponent"))
@@ -1234,47 +1697,36 @@ function whatBabelPlugin({ types: t }) {
1234
1697
  const eventArray = t.arrayExpression(
1235
1698
  [...state.delegatedEvents].map((e) => t.stringLiteral(e))
1236
1699
  );
1237
- path.pushContainer(
1238
- "body",
1239
- t.expressionStatement(
1240
- t.callExpression(t.identifier("_$delegateEvents"), [eventArray])
1241
- )
1700
+ const helperFn = t.functionDeclaration(
1701
+ t.identifier("_$delegate$"),
1702
+ [],
1703
+ t.blockStatement([
1704
+ t.ifStatement(
1705
+ t.identifier("_$delegated$"),
1706
+ t.returnStatement()
1707
+ ),
1708
+ t.expressionStatement(
1709
+ t.assignmentExpression("=", t.identifier("_$delegated$"), t.booleanLiteral(true))
1710
+ ),
1711
+ t.expressionStatement(
1712
+ t.callExpression(t.identifier("_$delegateEvents"), [eventArray])
1713
+ )
1714
+ ])
1242
1715
  );
1716
+ path.unshiftContainer("body", [
1717
+ t.variableDeclaration("let", [
1718
+ t.variableDeclarator(t.identifier("_$delegated$"), t.booleanLiteral(false))
1719
+ ]),
1720
+ helperFn
1721
+ ]);
1243
1722
  }
1244
1723
  }
1245
1724
  },
1246
1725
  JSXElement(path, state) {
1247
- state.signalNames = collectSignalNamesFromScope(path);
1248
- state._pendingSetup = [];
1249
- const transformed = transformElementFineGrained(path, state);
1250
- const pending = state._pendingSetup;
1251
- state._pendingSetup = [];
1252
- if (pending.length > 0) {
1253
- let stmtPath = path;
1254
- while (stmtPath && !stmtPath.isStatement()) {
1255
- stmtPath = stmtPath.parentPath;
1256
- }
1257
- if (stmtPath && stmtPath.isStatement()) {
1258
- for (const stmt of pending) {
1259
- stmtPath.insertBefore(stmt);
1260
- }
1261
- path.replaceWith(transformed);
1262
- } else {
1263
- pending.push(t.returnStatement(transformed));
1264
- path.replaceWith(
1265
- t.callExpression(
1266
- t.arrowFunctionExpression([], t.blockStatement(pending)),
1267
- []
1268
- )
1269
- );
1270
- }
1271
- } else {
1272
- path.replaceWith(transformed);
1273
- }
1726
+ transformJsxRoot(path, state, transformElementFineGrained);
1274
1727
  },
1275
1728
  JSXFragment(path, state) {
1276
- const transformed = transformFragmentFineGrained(path, state);
1277
- path.replaceWith(transformed);
1729
+ transformJsxRoot(path, state, transformFragmentFineGrained);
1278
1730
  }
1279
1731
  }
1280
1732
  };