@barefootjs/jsx 0.4.0 → 0.5.1

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.
Files changed (67) hide show
  1. package/dist/adapters/interface.d.ts +20 -0
  2. package/dist/adapters/interface.d.ts.map +1 -1
  3. package/dist/adapters/test-adapter.d.ts.map +1 -1
  4. package/dist/expression-parser.d.ts +36 -19
  5. package/dist/expression-parser.d.ts.map +1 -1
  6. package/dist/import-map.d.ts +56 -0
  7. package/dist/import-map.d.ts.map +1 -0
  8. package/dist/import-map.js +18 -0
  9. package/dist/index.d.ts +3 -1
  10. package/dist/index.d.ts.map +1 -1
  11. package/dist/index.js +333 -199
  12. package/dist/ir-to-client-js/collect-elements.d.ts.map +1 -1
  13. package/dist/ir-to-client-js/control-flow/plan/build-loop.d.ts.map +1 -1
  14. package/dist/ir-to-client-js/control-flow/plan/loop.d.ts +14 -0
  15. package/dist/ir-to-client-js/control-flow/plan/loop.d.ts.map +1 -1
  16. package/dist/ir-to-client-js/control-flow/stringify/loop.d.ts.map +1 -1
  17. package/dist/ir-to-client-js/emit-reactive.d.ts.map +1 -1
  18. package/dist/ir-to-client-js/html-template.d.ts +0 -14
  19. package/dist/ir-to-client-js/html-template.d.ts.map +1 -1
  20. package/dist/ir-to-client-js/imports.d.ts +2 -2
  21. package/dist/ir-to-client-js/imports.d.ts.map +1 -1
  22. package/dist/ir-to-client-js/reactivity.d.ts.map +1 -1
  23. package/dist/ir-to-client-js/types.d.ts +7 -0
  24. package/dist/ir-to-client-js/types.d.ts.map +1 -1
  25. package/dist/ir-to-client-js/utils.d.ts +2 -2
  26. package/dist/ir-to-client-js/utils.d.ts.map +1 -1
  27. package/dist/scanner/js-scanner.d.ts +10 -0
  28. package/dist/scanner/js-scanner.d.ts.map +1 -1
  29. package/dist/scanner/js-scanner.js +5 -0
  30. package/dist/types.d.ts +11 -0
  31. package/dist/types.d.ts.map +1 -1
  32. package/package.json +7 -3
  33. package/src/__tests__/__snapshots__/doc-examples.test.ts.snap +438 -190
  34. package/src/__tests__/adapter-output.test.ts +49 -0
  35. package/src/__tests__/child-components-in-map.test.ts +76 -0
  36. package/src/__tests__/client-js-generation.test.ts +5 -2
  37. package/src/__tests__/import-map.test.ts +75 -0
  38. package/src/__tests__/inline-jsx-callback.test.ts +95 -0
  39. package/src/__tests__/ir-jsx-props.test.ts +5 -2
  40. package/src/__tests__/ir-sort-comparator.test.ts +212 -9
  41. package/src/__tests__/loop-item-conditional-codegen.test.ts +81 -0
  42. package/src/__tests__/map-logical-jsx-helper.test.ts +159 -0
  43. package/src/__tests__/missing-key-in-list.test.ts +49 -0
  44. package/src/__tests__/reactive-attrs-in-map.test.ts +41 -0
  45. package/src/__tests__/token-contains-ident.test.ts +27 -0
  46. package/src/__tests__/unsupported-expression.test.ts +42 -13
  47. package/src/adapters/interface.ts +20 -0
  48. package/src/adapters/test-adapter.ts +16 -1
  49. package/src/expression-parser.ts +265 -50
  50. package/src/import-map.ts +72 -0
  51. package/src/index.ts +5 -1
  52. package/src/ir-to-client-js/collect-elements.ts +3 -0
  53. package/src/ir-to-client-js/control-flow/plan/build-loop.ts +17 -0
  54. package/src/ir-to-client-js/control-flow/plan/loop.ts +14 -0
  55. package/src/ir-to-client-js/control-flow/stringify/insert.ts +7 -2
  56. package/src/ir-to-client-js/control-flow/stringify/loop.ts +60 -0
  57. package/src/ir-to-client-js/emit-reactive.ts +12 -3
  58. package/src/ir-to-client-js/html-template.ts +29 -3
  59. package/src/ir-to-client-js/imports.ts +2 -2
  60. package/src/ir-to-client-js/reactivity.ts +17 -1
  61. package/src/ir-to-client-js/stringify/static-array-child-init.ts +8 -4
  62. package/src/ir-to-client-js/types.ts +7 -0
  63. package/src/ir-to-client-js/utils.ts +31 -116
  64. package/src/jsx-to-ir.ts +161 -12
  65. package/src/preprocess-inline-jsx-callbacks.ts +28 -10
  66. package/src/scanner/js-scanner.ts +16 -1
  67. package/src/types.ts +12 -0
package/dist/index.js CHANGED
@@ -62,6 +62,9 @@ function canRegexStartHere(prev) {
62
62
  return true;
63
63
  }
64
64
  }
65
+ function isIdentifierLikeToken(kind) {
66
+ return kind === ts.SyntaxKind.Identifier || kind >= ts.SyntaxKind.FirstKeyword && kind <= ts.SyntaxKind.LastKeyword;
67
+ }
65
68
  function isOpaqueContentKind(kind) {
66
69
  return kind === ts.SyntaxKind.StringLiteral || kind === ts.SyntaxKind.NoSubstitutionTemplateLiteral || kind === ts.SyntaxKind.RegularExpressionLiteral || kind === ts.SyntaxKind.SingleLineCommentTrivia || kind === ts.SyntaxKind.MultiLineCommentTrivia;
67
70
  }
@@ -173,6 +176,22 @@ function findTopLevelTemplateLiterals(code) {
173
176
  return out;
174
177
  }
175
178
 
179
+ // src/import-map.ts
180
+ function escapeHtmlAttr(value) {
181
+ return value.replace(/&/g, "&amp;").replace(/"/g, "&quot;").replace(/</g, "&lt;");
182
+ }
183
+ function renderImportMapHtml(manifest) {
184
+ const imports = manifest.importmap?.imports ?? {};
185
+ const json = JSON.stringify({ imports }).replace(/</g, "\\u003c");
186
+ const lines = [`<script type="importmap">${json}</script>`];
187
+ for (const href of manifest.preloads ?? []) {
188
+ lines.push(`<link rel="modulepreload" href="${escapeHtmlAttr(href)}" crossorigin>`);
189
+ }
190
+ return lines.join(`
191
+ `) + `
192
+ `;
193
+ }
194
+
176
195
  // src/analyzer.ts
177
196
  import ts7 from "typescript";
178
197
 
@@ -203,6 +222,7 @@ import {
203
222
  BF_LOOP_END,
204
223
  loopStartMarker,
205
224
  loopEndMarker,
225
+ loopItemMarker,
206
226
  toHTMLAttrName as toHtmlAttrName
207
227
  } from "@barefootjs/shared";
208
228
  var PROPS_PARAM = "_p";
@@ -398,150 +418,17 @@ function setIntersects(a, b) {
398
418
  function tokenContainsIdent(expr, ident) {
399
419
  return scanForIdentifiers(expr, (token) => token === ident);
400
420
  }
401
- var IDENT_START_RE = /[A-Za-z_$]/;
402
- var IDENT_PART_RE = /[A-Za-z0-9_$]/;
403
421
  function scanForIdentifiers(expr, predicate) {
404
- const n = expr.length;
405
- let i = 0;
406
- let state = 0;
407
- const tmplExprStack = [];
408
- let braceDepth = 0;
409
- while (i < n) {
410
- const ch = expr[i];
411
- switch (state) {
412
- case 0:
413
- case 4: {
414
- if (ch === "'") {
415
- state = 1;
416
- i++;
417
- continue;
418
- }
419
- if (ch === '"') {
420
- state = 2;
421
- i++;
422
- continue;
423
- }
424
- if (ch === "`") {
425
- state = 3;
426
- i++;
427
- continue;
428
- }
429
- if (ch === "/" && i + 1 < n) {
430
- const next = expr[i + 1];
431
- if (next === "/") {
432
- state = 5;
433
- i += 2;
434
- continue;
435
- }
436
- if (next === "*") {
437
- state = 6;
438
- i += 2;
439
- continue;
440
- }
441
- }
442
- if (state === 4) {
443
- if (ch === "{") {
444
- braceDepth++;
445
- i++;
446
- continue;
447
- }
448
- if (ch === "}") {
449
- if (braceDepth === 0) {
450
- const restored = tmplExprStack.pop();
451
- braceDepth = restored ?? 0;
452
- state = 3;
453
- i++;
454
- continue;
455
- }
456
- braceDepth--;
457
- i++;
458
- continue;
459
- }
460
- }
461
- if (IDENT_START_RE.test(ch)) {
462
- let j = i + 1;
463
- while (j < n && IDENT_PART_RE.test(expr[j]))
464
- j++;
465
- const token = expr.slice(i, j);
466
- let prev = i - 1;
467
- while (prev >= 0 && (expr[prev] === " " || expr[prev] === "\t" || expr[prev] === `
468
- ` || expr[prev] === "\r"))
469
- prev--;
470
- const isMemberTail = prev >= 0 && expr[prev] === "." && (prev === 0 || expr[prev - 1] !== ".");
471
- if (!isMemberTail && predicate(token))
472
- return true;
473
- i = j;
474
- continue;
475
- }
476
- i++;
477
- continue;
478
- }
479
- case 1: {
480
- if (ch === "\\" && i + 1 < n) {
481
- i += 2;
482
- continue;
483
- }
484
- if (ch === "'") {
485
- state = 0;
486
- i++;
487
- continue;
488
- }
489
- i++;
490
- continue;
491
- }
492
- case 2: {
493
- if (ch === "\\" && i + 1 < n) {
494
- i += 2;
495
- continue;
496
- }
497
- if (ch === '"') {
498
- state = 0;
499
- i++;
500
- continue;
501
- }
502
- i++;
503
- continue;
504
- }
505
- case 3: {
506
- if (ch === "\\" && i + 1 < n) {
507
- i += 2;
508
- continue;
509
- }
510
- if (ch === "`") {
511
- state = tmplExprStack.length > 0 ? 4 : 0;
512
- i++;
513
- continue;
514
- }
515
- if (ch === "$" && i + 1 < n && expr[i + 1] === "{") {
516
- tmplExprStack.push(braceDepth);
517
- braceDepth = 0;
518
- state = 4;
519
- i += 2;
520
- continue;
521
- }
522
- i++;
523
- continue;
524
- }
525
- case 5: {
526
- if (ch === `
527
- ` || ch === "\r") {
528
- state = 0;
529
- i++;
530
- continue;
531
- }
532
- i++;
533
- continue;
534
- }
535
- case 6: {
536
- if (ch === "*" && i + 1 < n && expr[i + 1] === "/") {
537
- state = 0;
538
- i += 2;
539
- continue;
540
- }
541
- i++;
542
- continue;
543
- }
422
+ let prevSignificant;
423
+ for (const tok of iterateJsTokens(expr)) {
424
+ if (isTriviaKind(tok.kind))
425
+ continue;
426
+ if (isIdentifierLikeToken(tok.kind)) {
427
+ const isMemberTail = prevSignificant === ts2.SyntaxKind.DotToken || prevSignificant === ts2.SyntaxKind.QuestionDotToken;
428
+ if (!isMemberTail && predicate(expr.slice(tok.pos, tok.end)))
429
+ return true;
544
430
  }
431
+ prevSignificant = tok.kind;
545
432
  }
546
433
  return false;
547
434
  }
@@ -1268,6 +1155,9 @@ function buildSpreadAttrsMergeCall(args) {
1268
1155
  }
1269
1156
  return `\${spreadAttrs({${objMembers.join(", ")}})}`;
1270
1157
  }
1158
+ function itemAnchorTemplate(keyExpr) {
1159
+ return `<!--${loopItemMarker("${" + keyExpr + "}")}-->`;
1160
+ }
1271
1161
  function irToHtmlTemplate(node, restSpreadNames, loopDepth = 0, loopParams, branchSlotsVar, insideLoop = false, inHoistedChildren = false) {
1272
1162
  const recurse = (n) => irToHtmlTemplate(n, restSpreadNames, loopDepth, loopParams, branchSlotsVar, insideLoop, inHoistedChildren);
1273
1163
  const wrapExpr = (expr) => wrapExprWithLoopParams(expr, loopParams);
@@ -1363,7 +1253,10 @@ function irToHtmlTemplate(node, restSpreadNames, loopDepth = 0, loopParams, bran
1363
1253
  }
1364
1254
  case "loop": {
1365
1255
  const innerRecurse = (n) => irToHtmlTemplate(n, restSpreadNames, loopDepth + 1, loopParams, branchSlotsVar, insideLoop);
1366
- const childTemplate = node.children.map(innerRecurse).join("");
1256
+ let childTemplate = node.children.map(innerRecurse).join("");
1257
+ if (node.bodyIsItemConditional && node.key) {
1258
+ childTemplate = `${itemAnchorTemplate(node.key)}${childTemplate}`;
1259
+ }
1367
1260
  const indexParam = node.index ? `, ${node.index}` : "";
1368
1261
  const rawChainedArray = applyLoopChain(node);
1369
1262
  const { array: iterArray, callbackParam } = applyIterationShape(node, rawChainedArray, indexParam);
@@ -1942,7 +1835,10 @@ function generateCsrTemplateWithOpts(node, opts) {
1942
1835
  return `\${renderChild('${nameForRegistryRef(node.name)}', ${propsExpr}${keyArg || (slotArg ? ", undefined" : "")}${slotArg})}`;
1943
1836
  }
1944
1837
  case "loop": {
1945
- const childTemplate = node.children.map(recurseInLoop).join("");
1838
+ let childTemplate = node.children.map(recurseInLoop).join("");
1839
+ if (node.bodyIsItemConditional && node.key) {
1840
+ childTemplate = `${itemAnchorTemplate(node.key)}${childTemplate}`;
1841
+ }
1946
1842
  const indexParam = node.index ? `, ${node.index}` : "";
1947
1843
  const chainedTemplateArray = node.sortComparator || node.filterPredicate ? applyLoopChain(node, node.templateArray) : node.templateArray;
1948
1844
  const rawArrayExpr = transformExpr(node.array, chainedTemplateArray);
@@ -5418,6 +5314,8 @@ function convertNode(node, raw) {
5418
5314
  ` + ` (a, b) => a.field - b.field
5419
5315
  ` + ` (a, b) => a.localeCompare(b)
5420
5316
  ` + ` (a, b) => a.field.localeCompare(b.field)
5317
+ ` + ` (a, b) => a.field > b.field ? 1 : -1 (relational ternary)
5318
+ ` + ` any of the above ||-chained for multi-key tie-breaks
5421
5319
  ` + `(reverse the operands for descending order). ` + `Wrap the call in /* @client */ to evaluate at hydration.`
5422
5320
  };
5423
5321
  }
@@ -5577,26 +5475,54 @@ function extractSortComparatorFromTS(node, method) {
5577
5475
  const paramA = pA.name.text;
5578
5476
  const paramB = pB.name.text;
5579
5477
  let body;
5580
- if (ts8.isArrowFunction(node)) {
5581
- if (ts8.isBlock(node.body))
5582
- return null;
5478
+ if (ts8.isArrowFunction(node) && !ts8.isBlock(node.body)) {
5583
5479
  body = node.body;
5584
5480
  } else {
5585
- const stmts = node.body.statements;
5481
+ const block = node.body;
5482
+ const stmts = block.statements;
5586
5483
  if (stmts.length !== 1 || !ts8.isReturnStatement(stmts[0]) || !stmts[0].expression)
5587
5484
  return null;
5588
5485
  body = stmts[0].expression;
5589
5486
  }
5590
5487
  const raw = body.getText();
5488
+ const keys = [];
5489
+ for (const operand of flattenLogicalOr(body)) {
5490
+ const key = classifyLeafComparator(operand, paramA, paramB);
5491
+ if (!key)
5492
+ return null;
5493
+ keys.push(key);
5494
+ }
5495
+ if (keys.length === 0)
5496
+ return null;
5497
+ return { keys, raw, paramA, paramB, method };
5498
+ }
5499
+ function unwrapParens(expr) {
5500
+ let e = expr;
5501
+ while (ts8.isParenthesizedExpression(e))
5502
+ e = e.expression;
5503
+ return e;
5504
+ }
5505
+ function flattenLogicalOr(expr) {
5506
+ const inner = unwrapParens(expr);
5507
+ if (ts8.isBinaryExpression(inner) && inner.operatorToken.kind === ts8.SyntaxKind.BarBarToken) {
5508
+ return [...flattenLogicalOr(inner.left), ...flattenLogicalOr(inner.right)];
5509
+ }
5510
+ return [inner];
5511
+ }
5512
+ function classifyLeafComparator(expr, paramA, paramB) {
5513
+ const body = unwrapParens(expr);
5591
5514
  if (ts8.isBinaryExpression(body) && body.operatorToken.kind === ts8.SyntaxKind.MinusToken) {
5592
- return classifyComparatorOperands(body.left, body.right, paramA, paramB, "numeric", method, raw);
5515
+ return classifyComparatorOperands(body.left, body.right, paramA, paramB, "numeric");
5593
5516
  }
5594
5517
  if (ts8.isCallExpression(body) && ts8.isPropertyAccessExpression(body.expression) && body.expression.name.text === "localeCompare" && body.arguments.length === 1) {
5595
- return classifyComparatorOperands(body.expression.expression, body.arguments[0], paramA, paramB, "string", method, raw);
5518
+ return classifyComparatorOperands(body.expression.expression, body.arguments[0], paramA, paramB, "string");
5519
+ }
5520
+ if (ts8.isConditionalExpression(body)) {
5521
+ return classifyTernaryComparator(body, paramA, paramB);
5596
5522
  }
5597
5523
  return null;
5598
5524
  }
5599
- function classifyComparatorOperands(left, right, paramA, paramB, type, method, raw) {
5525
+ function classifyComparatorOperands(left, right, paramA, paramB, type) {
5600
5526
  const leftRef = classifySortOperand(left, paramA, paramB);
5601
5527
  const rightRef = classifySortOperand(right, paramA, paramB);
5602
5528
  if (!leftRef || !rightRef)
@@ -5609,15 +5535,89 @@ function classifyComparatorOperands(left, right, paramA, paramB, type, method, r
5609
5535
  return null;
5610
5536
  }
5611
5537
  const direction = leftRef.param === "A" ? "asc" : "desc";
5612
- return {
5613
- key: leftRef.key,
5614
- type,
5615
- direction,
5616
- raw,
5617
- paramA,
5618
- paramB,
5619
- method
5620
- };
5538
+ return { key: leftRef.key, type, direction };
5539
+ }
5540
+ function classifyTernaryComparator(node, paramA, paramB) {
5541
+ const cond = unwrapParens(node.condition);
5542
+ if (ts8.isBinaryExpression(cond) && (cond.operatorToken.kind === ts8.SyntaxKind.EqualsEqualsEqualsToken || cond.operatorToken.kind === ts8.SyntaxKind.EqualsEqualsToken) && sameKeyOperands(cond.left, cond.right, paramA, paramB) && numericSign(node.whenTrue) === 0) {
5543
+ const elseBranch = unwrapParens(node.whenFalse);
5544
+ if (ts8.isConditionalExpression(elseBranch)) {
5545
+ return classifyTernaryComparator(elseBranch, paramA, paramB);
5546
+ }
5547
+ return null;
5548
+ }
5549
+ if (!ts8.isBinaryExpression(cond))
5550
+ return null;
5551
+ const op = cond.operatorToken.kind;
5552
+ const isGreater = op === ts8.SyntaxKind.GreaterThanToken || op === ts8.SyntaxKind.GreaterThanEqualsToken;
5553
+ const isLess = op === ts8.SyntaxKind.LessThanToken || op === ts8.SyntaxKind.LessThanEqualsToken;
5554
+ if (!isGreater && !isLess)
5555
+ return null;
5556
+ const leftRef = classifySortOperand(cond.left, paramA, paramB);
5557
+ const rightRef = classifySortOperand(cond.right, paramA, paramB);
5558
+ if (!leftRef || !rightRef)
5559
+ return null;
5560
+ if (leftRef.param === rightRef.param)
5561
+ return null;
5562
+ if (leftRef.key.kind !== rightRef.key.kind)
5563
+ return null;
5564
+ if (leftRef.key.kind === "field" && rightRef.key.kind === "field" && leftRef.key.field !== rightRef.key.field) {
5565
+ return null;
5566
+ }
5567
+ const trueSign = numericSign(node.whenTrue);
5568
+ if (trueSign === null || trueSign === 0)
5569
+ return null;
5570
+ if (!isBoundedTernaryElse(node.whenFalse, leftRef.key, paramA, paramB))
5571
+ return null;
5572
+ const greaterForA = leftRef.param === "A" ? isGreater : !isGreater;
5573
+ const asc = greaterForA ? trueSign > 0 : trueSign < 0;
5574
+ return { key: leftRef.key, type: "auto", direction: asc ? "asc" : "desc" };
5575
+ }
5576
+ function sameKeyOperands(left, right, paramA, paramB) {
5577
+ const l = classifySortOperand(left, paramA, paramB);
5578
+ const r = classifySortOperand(right, paramA, paramB);
5579
+ if (!l || !r)
5580
+ return false;
5581
+ if (l.param === r.param)
5582
+ return false;
5583
+ if (l.key.kind !== r.key.kind)
5584
+ return false;
5585
+ if (l.key.kind === "field" && r.key.kind === "field" && l.key.field !== r.key.field)
5586
+ return false;
5587
+ return true;
5588
+ }
5589
+ function numericSign(expr) {
5590
+ const e = unwrapParens(expr);
5591
+ if (ts8.isPrefixUnaryExpression(e) && e.operator === ts8.SyntaxKind.MinusToken) {
5592
+ const inner = numericSign(e.operand);
5593
+ return inner === null ? null : -inner;
5594
+ }
5595
+ if (ts8.isNumericLiteral(e)) {
5596
+ const n = Number(e.text);
5597
+ if (Number.isNaN(n))
5598
+ return null;
5599
+ if (n === 0)
5600
+ return 0;
5601
+ return n > 0 ? 1 : -1;
5602
+ }
5603
+ return null;
5604
+ }
5605
+ function isBoundedTernaryElse(expr, key, paramA, paramB) {
5606
+ const e = unwrapParens(expr);
5607
+ if (numericSign(e) !== null)
5608
+ return true;
5609
+ if (ts8.isConditionalExpression(e)) {
5610
+ const nested = classifyTernaryComparator(e, paramA, paramB);
5611
+ return nested !== null && sortKeyEquals(nested.key, key);
5612
+ }
5613
+ return false;
5614
+ }
5615
+ function sortKeyEquals(a, b) {
5616
+ if (a.kind !== b.kind)
5617
+ return false;
5618
+ if (a.kind === "field" && b.kind === "field")
5619
+ return a.field === b.field;
5620
+ return true;
5621
5621
  }
5622
5622
  function classifySortOperand(expr, paramA, paramB) {
5623
5623
  if (ts8.isIdentifier(expr)) {
@@ -7543,6 +7543,23 @@ function containsJsxInExpression(node) {
7543
7543
  }
7544
7544
  return ts11.forEachChild(node, containsJsxInExpression) ?? false;
7545
7545
  }
7546
+ function callsJsxHelper(node, ctx) {
7547
+ let found = false;
7548
+ const visit2 = (n) => {
7549
+ if (found)
7550
+ return;
7551
+ if (ts11.isCallExpression(n) && ts11.isIdentifier(n.expression)) {
7552
+ const name = n.expression.text;
7553
+ if (ctx.analyzer.jsxFunctions.has(name) || ctx.analyzer.jsxMultiReturnFunctions.has(name)) {
7554
+ found = true;
7555
+ return;
7556
+ }
7557
+ }
7558
+ ts11.forEachChild(n, visit2);
7559
+ };
7560
+ visit2(node);
7561
+ return found;
7562
+ }
7546
7563
  function containsAwaitExpression(node) {
7547
7564
  if (ts11.isAwaitExpression(node))
7548
7565
  return true;
@@ -7622,7 +7639,7 @@ function transformJsxExpression(expr, ctx, isClientOnly = false) {
7622
7639
  if (node.operatorToken.kind === ts11.SyntaxKind.AmpersandAmpersandToken) {
7623
7640
  return transformLogicalAnd(node, ctx);
7624
7641
  }
7625
- if ((node.operatorToken.kind === ts11.SyntaxKind.QuestionQuestionToken || node.operatorToken.kind === ts11.SyntaxKind.BarBarToken) && containsJsxInExpression(node.right)) {
7642
+ if ((node.operatorToken.kind === ts11.SyntaxKind.QuestionQuestionToken || node.operatorToken.kind === ts11.SyntaxKind.BarBarToken) && (containsJsxInExpression(node.right) || callsJsxHelper(node.right, ctx))) {
7626
7643
  return transformNullishCoalescing(node, ctx);
7627
7644
  }
7628
7645
  return null;
@@ -8079,23 +8096,23 @@ function checkLoopKey(callback, ctx, isNested) {
8079
8096
  }
8080
8097
  while (ts11.isParenthesizedExpression(body))
8081
8098
  body = body.expression;
8099
+ function checkJsxOperand(node) {
8100
+ let n = node;
8101
+ while (ts11.isParenthesizedExpression(n))
8102
+ n = n.expression;
8103
+ if (ts11.isJsxElement(n))
8104
+ checkOpening(n.openingElement);
8105
+ else if (ts11.isJsxSelfClosingElement(n))
8106
+ checkOpening(n);
8107
+ }
8082
8108
  if (ts11.isConditionalExpression(body)) {
8083
- const whenTrue = body.whenTrue;
8084
- const whenFalse = body.whenFalse;
8085
- let wt = whenTrue;
8086
- let wf = whenFalse;
8087
- while (ts11.isParenthesizedExpression(wt))
8088
- wt = wt.expression;
8089
- while (ts11.isParenthesizedExpression(wf))
8090
- wf = wf.expression;
8091
- if (ts11.isJsxElement(wt))
8092
- checkOpening(wt.openingElement);
8093
- else if (ts11.isJsxSelfClosingElement(wt))
8094
- checkOpening(wt);
8095
- if (ts11.isJsxElement(wf))
8096
- checkOpening(wf.openingElement);
8097
- else if (ts11.isJsxSelfClosingElement(wf))
8098
- checkOpening(wf);
8109
+ checkJsxOperand(body.whenTrue);
8110
+ checkJsxOperand(body.whenFalse);
8111
+ return;
8112
+ }
8113
+ if (ts11.isBinaryExpression(body) && (body.operatorToken.kind === ts11.SyntaxKind.AmpersandAmpersandToken || body.operatorToken.kind === ts11.SyntaxKind.BarBarToken || body.operatorToken.kind === ts11.SyntaxKind.QuestionQuestionToken)) {
8114
+ checkJsxOperand(body.left);
8115
+ checkJsxOperand(body.right);
8099
8116
  return;
8100
8117
  }
8101
8118
  if (ts11.isJsxElement(body)) {
@@ -8118,6 +8135,38 @@ function loopBodyIsMultiRoot(children) {
8118
8135
  return false;
8119
8136
  return loopBodyIsMultiRoot(only.children);
8120
8137
  }
8138
+ function branchHasNoElement(node) {
8139
+ if (node.type === "element" || node.type === "component")
8140
+ return false;
8141
+ if (node.type === "conditional") {
8142
+ return branchHasNoElement(node.whenTrue) || branchHasNoElement(node.whenFalse);
8143
+ }
8144
+ if (node.type === "fragment") {
8145
+ const real = node.children.filter((c) => !(c.type === "text" && typeof c.value === "string" && !c.value.trim()));
8146
+ return real.length !== 1 || branchHasNoElement(real[0]);
8147
+ }
8148
+ return true;
8149
+ }
8150
+ function loopBodyItemConditional(children) {
8151
+ const real = children.filter((c) => !(c.type === "text" && typeof c.value === "string" && !c.value.trim()));
8152
+ if (real.length !== 1)
8153
+ return null;
8154
+ const only = real[0];
8155
+ if (only.type !== "conditional")
8156
+ return null;
8157
+ if (branchHasNoElement(only.whenTrue) || branchHasNoElement(only.whenFalse)) {
8158
+ return only;
8159
+ }
8160
+ return null;
8161
+ }
8162
+ function extractItemConditionalKey(cond) {
8163
+ const a = branchHasNoElement(cond.whenTrue) ? null : extractLoopKey(cond.whenTrue);
8164
+ const b = branchHasNoElement(cond.whenFalse) ? null : extractLoopKey(cond.whenFalse);
8165
+ if (a !== null && b !== null) {
8166
+ return normalizeKeyExpr(a) === normalizeKeyExpr(b) ? a : null;
8167
+ }
8168
+ return a ?? b;
8169
+ }
8121
8170
  function transformMapCall(node, ctx, isClientOnly = false, method = "map") {
8122
8171
  const isNested = ctx.loopParams.size > 0;
8123
8172
  const propAccess = node.expression;
@@ -8280,6 +8329,19 @@ function transformMapCall(node, ctx, isClientOnly = false, method = "map") {
8280
8329
  }
8281
8330
  if (index)
8282
8331
  ctx.loopParams.add(index);
8332
+ const tryTransformRenderableBody = (expr) => {
8333
+ if (!ts11.isBinaryExpression(expr))
8334
+ return;
8335
+ const op = expr.operatorToken.kind;
8336
+ if (op !== ts11.SyntaxKind.AmpersandAmpersandToken && op !== ts11.SyntaxKind.BarBarToken && op !== ts11.SyntaxKind.QuestionQuestionToken) {
8337
+ return;
8338
+ }
8339
+ if (!containsJsxInExpression(expr) && !callsJsxHelper(expr, ctx))
8340
+ return;
8341
+ const transformed = transformJsxExpression(expr, ctx, isClientOnly);
8342
+ if (transformed)
8343
+ children = [transformed];
8344
+ };
8283
8345
  const body = callback.body;
8284
8346
  if (ts11.isJsxElement(body) || ts11.isJsxSelfClosingElement(body) || ts11.isJsxFragment(body)) {
8285
8347
  const transformed = transformNode(body, ctx);
@@ -8302,6 +8364,8 @@ function transformMapCall(node, ctx, isClientOnly = false, method = "map") {
8302
8364
  children = [transformConditional(inner, ctx)];
8303
8365
  } else if (method === "flatMap" && ts11.isArrayLiteralExpression(inner)) {
8304
8366
  children = transformArrayLiteralChildren(inner, ctx);
8367
+ } else {
8368
+ tryTransformRenderableBody(inner);
8305
8369
  }
8306
8370
  } else if (method === "flatMap" && ts11.isArrayLiteralExpression(body)) {
8307
8371
  children = transformArrayLiteralChildren(body, ctx);
@@ -8350,6 +8414,8 @@ function transformMapCall(node, ctx, isClientOnly = false, method = "map") {
8350
8414
  if (method === "flatMap" && children.length === 0) {
8351
8415
  flatMapCallback = buildFlatMapCallback(callback, body, ctx);
8352
8416
  }
8417
+ } else {
8418
+ tryTransformRenderableBody(body);
8353
8419
  }
8354
8420
  if (paramBindings) {
8355
8421
  for (const b of paramBindings)
@@ -8366,7 +8432,9 @@ function transformMapCall(node, ctx, isClientOnly = false, method = "map") {
8366
8432
  if (ts11.isArrowFunction(node.arguments[0]) && children.length > 0) {
8367
8433
  checkLoopKey(node.arguments[0], ctx, isNested);
8368
8434
  }
8369
- const key = children.length > 0 ? extractLoopKey(children[0]) : null;
8435
+ const itemConditional = children.length > 0 ? loopBodyItemConditional(children) : null;
8436
+ const bodyIsItemConditional = itemConditional !== null;
8437
+ const key = bodyIsItemConditional ? extractItemConditionalKey(itemConditional) : children.length > 0 ? extractLoopKey(children[0]) : null;
8370
8438
  let childComponent;
8371
8439
  if (children.length === 1 && children[0].type === "component") {
8372
8440
  const comp = children[0];
@@ -8405,6 +8473,7 @@ function transformMapCall(node, ctx, isClientOnly = false, method = "map") {
8405
8473
  callsReactiveGetters: callsReactive || undefined,
8406
8474
  hasFunctionCalls: hasCalls || undefined,
8407
8475
  bodyIsMultiRoot: bodyIsMultiRoot || undefined,
8476
+ bodyIsItemConditional: bodyIsItemConditional || undefined,
8408
8477
  childComponent,
8409
8478
  nestedComponents,
8410
8479
  filterPredicate,
@@ -9583,7 +9652,8 @@ function collectLoopChildReactiveAttrs(node, ctx, loopParam, loopParamBindings)
9583
9652
  if (!valueStr)
9584
9653
  continue;
9585
9654
  const expanded = expandConstantForReactivity(valueStr, ctx, attr.freeIdentifiers);
9586
- if (!attr.clientOnly && classifyReactivity(expanded.expr, ctx, loopParam, loopParamBindings, expanded.freeIds).kind === "none")
9655
+ const reactive = classifyReactivity(expanded.expr, ctx, loopParam, loopParamBindings, expanded.freeIds).kind !== "none" || attr.callsReactiveGetters || attr.hasFunctionCalls;
9656
+ if (!attr.clientOnly && !reactive)
9587
9657
  continue;
9588
9658
  attrs.push({
9589
9659
  childSlotId: el.slotId,
@@ -9697,6 +9767,7 @@ function collectInnerLoops(nodes, siblingOffsets, outerLoopParam, ctx, options)
9697
9767
  key: n.key,
9698
9768
  markerId: n.markerId,
9699
9769
  bodyIsMultiRoot: n.bodyIsMultiRoot,
9770
+ bodyIsItemConditional: n.bodyIsItemConditional,
9700
9771
  iterationShape: n.iterationShape,
9701
9772
  containerSlotId: scope.parentSlotId,
9702
9773
  template,
@@ -9911,6 +9982,7 @@ function collectElements(node, ctx, siblingOffsets, insideConditional = false) {
9911
9982
  key: l.key,
9912
9983
  markerId: l.markerId,
9913
9984
  bodyIsMultiRoot: l.bodyIsMultiRoot,
9985
+ bodyIsItemConditional: l.bodyIsItemConditional,
9914
9986
  iterationShape: l.iterationShape,
9915
9987
  template,
9916
9988
  staticItemTemplate,
@@ -10106,6 +10178,7 @@ function collectBranchLoops(node, ctx, siblingOffsets) {
10106
10178
  key: n.key,
10107
10179
  markerId: n.markerId,
10108
10180
  bodyIsMultiRoot: n.bodyIsMultiRoot,
10181
+ bodyIsItemConditional: n.bodyIsItemConditional,
10109
10182
  iterationShape: n.iterationShape,
10110
10183
  template: childTemplate,
10111
10184
  containerSlotId: containerSlot,
@@ -10672,6 +10745,7 @@ var RUNTIME_IMPORT_CANDIDATES = [
10672
10745
  "getLoopChildren",
10673
10746
  "getLoopNodes",
10674
10747
  "mapArray",
10748
+ "mapArrayAnchored",
10675
10749
  "createDisposableEffect",
10676
10750
  "createComponent",
10677
10751
  "renderChild",
@@ -10695,7 +10769,8 @@ var RUNTIME_IMPORT_CANDIDATES = [
10695
10769
  "qsaChildScopes",
10696
10770
  "upsertChildItem",
10697
10771
  "__slot",
10698
- "__bfSlot"
10772
+ "__bfSlot",
10773
+ "__bfText"
10699
10774
  ];
10700
10775
  var RUNTIME_MODULE = "@barefootjs/client/runtime";
10701
10776
  var IMPORT_PLACEHOLDER = "/* __BAREFOOTJS_DOM_IMPORTS__ */";
@@ -12691,10 +12766,11 @@ function emitInnerLoopNested(lines, plan) {
12691
12766
  for (const stmt of innerPreludeStatements) {
12692
12767
  lines.push(` ${stmt}`);
12693
12768
  }
12694
- for (const comp of comps) {
12695
- lines.push(` const __compEl = qsaChildScope(__innerEl, ${comp.selector})`);
12696
- lines.push(` if (__compEl) initChild('${nameForRegistryRef(comp.componentName)}', __compEl, ${comp.propsExpr})`);
12697
- }
12769
+ comps.forEach((comp, i) => {
12770
+ const compElVar = comps.length > 1 ? `__compEl${i}` : "__compEl";
12771
+ lines.push(` const ${compElVar} = qsaChildScope(__innerEl, ${comp.selector})`);
12772
+ lines.push(` if (${compElVar}) initChild('${nameForRegistryRef(comp.componentName)}', ${compElVar}, ${comp.propsExpr})`);
12773
+ });
12698
12774
  lines.push(` })`);
12699
12775
  lines.push(` })`);
12700
12776
  lines.push(` }`);
@@ -13516,17 +13592,21 @@ function emitDynamicTextUpdates(lines, ctx) {
13516
13592
  const conditionalElems = elems.filter((e) => e.insideConditional);
13517
13593
  const normalElems = elems.filter((e) => !e.insideConditional);
13518
13594
  if (normalElems.length > 0 || conditionalElems.length > 0) {
13595
+ for (const elem of normalElems) {
13596
+ const v = varSlotId(elem.slotId);
13597
+ lines.push(` let __anchor_${v} = _${v}`);
13598
+ }
13519
13599
  lines.push(` createEffect(() => {`);
13520
13600
  if (normalElems.length > 0) {
13521
13601
  lines.push(` const __val = ${expr}`);
13522
13602
  for (const elem of normalElems) {
13523
13603
  const v = varSlotId(elem.slotId);
13524
- lines.push(` if (_${v} && !__val?.__isSlot) _${v}.nodeValue = String(__val ?? '')`);
13604
+ lines.push(` __anchor_${v} = __bfText(__anchor_${v}, __val)`);
13525
13605
  }
13526
13606
  for (const elem of conditionalElems) {
13527
13607
  const v = varSlotId(elem.slotId);
13528
13608
  lines.push(` const [__el_${v}] = $t(__scope, '${elem.slotId}')`);
13529
- lines.push(` if (__el_${v} && !__val?.__isSlot) __el_${v}.nodeValue = String(__val ?? '')`);
13609
+ lines.push(` __bfText(__el_${v}, __val)`);
13530
13610
  }
13531
13611
  } else {
13532
13612
  lines.push(` let __val`);
@@ -13534,7 +13614,7 @@ function emitDynamicTextUpdates(lines, ctx) {
13534
13614
  for (const elem of conditionalElems) {
13535
13615
  const v = varSlotId(elem.slotId);
13536
13616
  lines.push(` const [__el_${v}] = $t(__scope, '${elem.slotId}')`);
13537
- lines.push(` if (__el_${v} && !__val?.__isSlot) __el_${v}.nodeValue = String(__val ?? '')`);
13617
+ lines.push(` __bfText(__el_${v}, __val)`);
13538
13618
  }
13539
13619
  }
13540
13620
  lines.push(` })`);
@@ -14030,8 +14110,14 @@ function stringifyPlainLoop(lines, plan, topIndent = " ") {
14030
14110
  template,
14031
14111
  reactiveEffects,
14032
14112
  childRefs,
14033
- bodyIsMultiRoot
14113
+ bodyIsMultiRoot,
14114
+ anchored,
14115
+ anchorKeyExpr
14034
14116
  } = plan;
14117
+ if (anchored) {
14118
+ stringifyAnchoredLoop(lines, plan, topIndent, anchorKeyExpr);
14119
+ return;
14120
+ }
14035
14121
  if (reactiveEffects === null && !bodyIsMultiRoot && childRefs.length === 0) {
14036
14122
  const unwrapInline = paramUnwrap ? `${paramUnwrap} ` : "";
14037
14123
  const preamble = mapPreambleWrapped ? `${mapPreambleWrapped}; ` : "";
@@ -14058,6 +14144,41 @@ function stringifyPlainLoop(lines, plan, topIndent = " ") {
14058
14144
  lines.push(`${bodyIndent}return __el`);
14059
14145
  lines.push(`${topIndent}}, '${markerId}')`);
14060
14146
  }
14147
+ function stringifyAnchoredLoop(lines, plan, topIndent, anchorKeyExpr) {
14148
+ const {
14149
+ containerVar,
14150
+ markerId,
14151
+ arrayExpr,
14152
+ keyFn,
14153
+ paramHead,
14154
+ paramUnwrap,
14155
+ indexParam,
14156
+ mapPreambleWrapped,
14157
+ reactiveEffects
14158
+ } = plan;
14159
+ const condSlot = reactiveEffects?.conditionals[0]?.slotId ?? null;
14160
+ lines.push(`${topIndent}mapArrayAnchored(() => ${arrayExpr}, ${containerVar}, ${keyFn}, (${paramHead}, ${indexParam}, __existing) => {`);
14161
+ const bodyIndent = topIndent + " ";
14162
+ if (paramUnwrap)
14163
+ lines.push(`${bodyIndent}${paramUnwrap}`);
14164
+ if (mapPreambleWrapped)
14165
+ lines.push(`${bodyIndent}${mapPreambleWrapped}`);
14166
+ lines.push(`${bodyIndent}const __anchor = __existing ?? document.createComment(\`bf-loop-i:\${${anchorKeyExpr}}\`)`);
14167
+ lines.push(`${bodyIndent}let __frag = null`);
14168
+ lines.push(`${bodyIndent}if (!__existing) {`);
14169
+ lines.push(`${bodyIndent} __frag = document.createDocumentFragment()`);
14170
+ lines.push(`${bodyIndent} __frag.appendChild(__anchor)`);
14171
+ if (condSlot) {
14172
+ lines.push(`${bodyIndent} __frag.appendChild(document.createComment('bf-cond-start:${condSlot}'))`);
14173
+ lines.push(`${bodyIndent} __frag.appendChild(document.createComment('bf-cond-end:${condSlot}'))`);
14174
+ }
14175
+ lines.push(`${bodyIndent}}`);
14176
+ if (reactiveEffects !== null) {
14177
+ stringifyReactiveEffects(lines, reactiveEffects, { indent: bodyIndent, elVar: "__anchor", bodyIsMultiRoot: false });
14178
+ }
14179
+ lines.push(`${bodyIndent}return __frag ?? __anchor`);
14180
+ lines.push(`${topIndent}}, '${markerId}')`);
14181
+ }
14061
14182
  function stringifyStaticLoop(lines, plan) {
14062
14183
  const { containerVar, arrayExpr, param, indexParam, childIndexExpr, attrsBySlot, texts, childRefs, csrMaterialize } = plan;
14063
14184
  const hasAttrs = attrsBySlot.length > 0;
@@ -14577,10 +14698,10 @@ function emitArmBody2(lines, body, mode, indent) {
14577
14698
  }
14578
14699
  for (const te of body.textEffects) {
14579
14700
  const v = varSlotId(te.slotId);
14580
- lines.push(`${indent}const [__el_${v}] = $t(__branchScope, '${te.slotId}')`);
14701
+ lines.push(`${indent}let __anchor_${v} = $t(__branchScope, '${te.slotId}')[0]`);
14581
14702
  lines.push(`${indent}__disposers.push(createDisposableEffect(() => {`);
14582
14703
  lines.push(`${indent} const __val = ${te.expression}`);
14583
- lines.push(`${indent} if (__el_${v} && !__val?.__isSlot) __el_${v}.nodeValue = String(__val ?? '')`);
14704
+ lines.push(`${indent} __anchor_${v} = __bfText(__anchor_${v}, __val)`);
14584
14705
  lines.push(`${indent}}))`);
14585
14706
  }
14586
14707
  if (body.loops.length > 0) {
@@ -14653,6 +14774,9 @@ function buildComponentLoopPlan(elem) {
14653
14774
 
14654
14775
  // src/ir-to-client-js/control-flow/plan/build-loop.ts
14655
14776
  function buildLoopPlan(elem, opts) {
14777
+ if (elem.bodyIsItemConditional) {
14778
+ return buildPlainLoopPlan(elem);
14779
+ }
14656
14780
  if (elem.isStaticArray) {
14657
14781
  return buildStaticLoopPlan(elem, opts.unsafeLocalNames);
14658
14782
  }
@@ -14682,7 +14806,9 @@ function buildPlainLoopPlan(elem) {
14682
14806
  template: elem.template,
14683
14807
  reactiveEffects: hasReactive2 ? buildLoopReactiveEffectsPlan(elem) : null,
14684
14808
  childRefs: buildChildRefBindings(elem.bindings.refs, elem.param, elem.paramBindings),
14685
- bodyIsMultiRoot: elem.bodyIsMultiRoot ?? false
14809
+ bodyIsMultiRoot: elem.bodyIsMultiRoot ?? false,
14810
+ anchored: elem.bodyIsItemConditional ?? false,
14811
+ anchorKeyExpr: elem.key ? wrap(elem.key) : elem.index || "__idx"
14686
14812
  };
14687
14813
  }
14688
14814
  function buildStaticLoopPlan(elem, unsafeLocalNames) {
@@ -15672,18 +15798,25 @@ function runSinglePass(source, filePath, startingCounter) {
15672
15798
  }
15673
15799
  function visit3(node) {
15674
15800
  if (ts15.isJsxAttribute(node) && node.initializer && ts15.isJsxExpression(node.initializer) && node.initializer.expression) {
15675
- let expr = node.initializer.expression;
15676
- while (ts15.isParenthesizedExpression(expr))
15677
- expr = expr.expression;
15678
- if (ts15.isArrowFunction(expr) && arrowBodyContainsJsx(expr)) {
15679
- const handled = handleInlineArrow(expr);
15680
- if (handled) {
15681
- return;
15682
- }
15801
+ if (tryHandleArrowValue(node.initializer.expression)) {
15802
+ return;
15683
15803
  }
15684
15804
  }
15805
+ if (ts15.isPropertyAssignment(node) && node.initializer) {
15806
+ if (tryHandleArrowValue(node.initializer))
15807
+ return;
15808
+ }
15685
15809
  ts15.forEachChild(node, visit3);
15686
15810
  }
15811
+ function tryHandleArrowValue(initializer) {
15812
+ let expr = initializer;
15813
+ while (ts15.isParenthesizedExpression(expr))
15814
+ expr = expr.expression;
15815
+ if (ts15.isArrowFunction(expr) && arrowBodyContainsJsx(expr)) {
15816
+ return handleInlineArrow(expr);
15817
+ }
15818
+ return false;
15819
+ }
15687
15820
  function handleInlineArrow(arrow) {
15688
15821
  const paramNames = collectArrowParamNames(arrow);
15689
15822
  const free = collectFreeIdentifiers(arrow);
@@ -18365,6 +18498,7 @@ export {
18365
18498
  rewriteImportsForTemplate,
18366
18499
  resolveSetters,
18367
18500
  resetCompilerCounters,
18501
+ renderImportMapHtml,
18368
18502
  parseExpression,
18369
18503
  parseBlockBody,
18370
18504
  needsTypeBasedDetection,