@barefootjs/cli 0.17.1 → 0.18.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -408,6 +408,7 @@ function convertNode(node, raw) {
408
408
  if (callee.property === "flat") {
409
409
  const depthNode = node.arguments[0];
410
410
  let flatDepth;
411
+ let depthExpr;
411
412
  if (depthNode === void 0) {
412
413
  flatDepth = 1;
413
414
  } else if (ts.isIdentifier(depthNode) && depthNode.text === "Infinity") {
@@ -420,16 +421,23 @@ function convertNode(node, raw) {
420
421
  n = -Number(depthNode.operand.text);
421
422
  }
422
423
  if (n === void 0 || Number.isNaN(n)) {
423
- return {
424
- kind: "unsupported",
425
- raw,
426
- reason: `\`.flat(depth)\` needs a literal integer or \`Infinity\` depth \u2014 a computed depth can't be resolved at template time. Use a literal depth, or pre-compute the value before the template.`
427
- };
424
+ const parsedDepth = convertNode(depthNode, raw);
425
+ if (checkSupport(parsedDepth).supported) {
426
+ depthExpr = parsedDepth;
427
+ flatDepth = 1;
428
+ } else {
429
+ return {
430
+ kind: "unsupported",
431
+ raw,
432
+ reason: `\`.flat(depth)\` needs a literal integer, \`Infinity\`, or a supported dynamic depth expression \u2014 this depth can't be resolved. Use a literal depth, a supported expression (prop/signal/arithmetic), or pre-compute the value before the template.`
433
+ };
434
+ }
435
+ } else {
436
+ const truncated = Math.trunc(n);
437
+ flatDepth = truncated < 0 ? 0 : truncated;
428
438
  }
429
- const truncated = Math.trunc(n);
430
- flatDepth = truncated < 0 ? 0 : truncated;
431
439
  }
432
- return { kind: "array-method", method: "flat", object: callee.object, args: [], flatDepth };
440
+ return depthExpr !== void 0 ? { kind: "array-method", method: "flat", object: callee.object, args: [], flatDepth, depthExpr } : { kind: "array-method", method: "flat", object: callee.object, args: [], flatDepth };
433
441
  }
434
442
  if (callee.property === "toLowerCase") {
435
443
  return { kind: "array-method", method: "toLowerCase", object: callee.object, args: args2 };
@@ -941,6 +949,7 @@ function validateRestUsage(expr, restName, excludedTopKeys) {
941
949
  case "array-method":
942
950
  walk(e.object);
943
951
  for (const a of e.args) walk(a);
952
+ if (e.method === "flat" && e.depthExpr) walk(e.depthExpr);
944
953
  return;
945
954
  case "literal":
946
955
  case "unsupported":
@@ -1023,6 +1032,7 @@ function collectIdentifiers(expr, out) {
1023
1032
  case "array-method":
1024
1033
  collectIdentifiers(expr.object, out);
1025
1034
  expr.args.forEach((e) => collectIdentifiers(e, out));
1035
+ if (expr.method === "flat" && expr.depthExpr) collectIdentifiers(expr.depthExpr, out);
1026
1036
  return;
1027
1037
  case "literal":
1028
1038
  case "regex":
@@ -1083,7 +1093,14 @@ function substituteDestructuredFields(expr, fieldMap, syntheticParam, restName)
1083
1093
  return { kind: "array-literal", elements: e.elements.map(walk) };
1084
1094
  case "array-method":
1085
1095
  if (e.method === "flat") {
1086
- return { kind: "array-method", method: "flat", object: walk(e.object), args: [], flatDepth: e.flatDepth };
1096
+ return {
1097
+ kind: "array-method",
1098
+ method: "flat",
1099
+ object: walk(e.object),
1100
+ args: [],
1101
+ flatDepth: e.flatDepth,
1102
+ ...e.depthExpr ? { depthExpr: walk(e.depthExpr) } : {}
1103
+ };
1087
1104
  }
1088
1105
  return { kind: "array-method", method: e.method, object: walk(e.object), args: e.args.map(walk) };
1089
1106
  case "literal":
@@ -1187,6 +1204,10 @@ function checkSupport(expr) {
1187
1204
  const argSupport = checkSupport(arg);
1188
1205
  if (!argSupport.supported) return argSupport;
1189
1206
  }
1207
+ if (expr.method === "flat" && expr.depthExpr) {
1208
+ const depthSupport = checkSupport(expr.depthExpr);
1209
+ if (!depthSupport.supported) return depthSupport;
1210
+ }
1190
1211
  return { supported: true, level: "L2" };
1191
1212
  }
1192
1213
  case "call": {
@@ -1278,6 +1299,9 @@ function checkSupport(expr) {
1278
1299
  case "logical": {
1279
1300
  const leftSupport = checkSupport(expr.left);
1280
1301
  if (!leftSupport.supported) return leftSupport;
1302
+ if (expr.op === "??" && expr.right.kind === "object-literal" && expr.right.properties.length === 0) {
1303
+ return { supported: true, level: "L4" };
1304
+ }
1281
1305
  const rightSupport = checkSupport(expr.right);
1282
1306
  if (!rightSupport.supported) return rightSupport;
1283
1307
  return { supported: true, level: "L4" };
@@ -1326,7 +1350,7 @@ function containsHigherOrder(expr) {
1326
1350
  case "array-literal":
1327
1351
  return expr.elements.some(containsHigherOrder);
1328
1352
  case "array-method":
1329
- return containsHigherOrder(expr.object) || expr.args.some(containsHigherOrder);
1353
+ return containsHigherOrder(expr.object) || expr.args.some(containsHigherOrder) || expr.method === "flat" && expr.depthExpr !== void 0 && containsHigherOrder(expr.depthExpr);
1330
1354
  default:
1331
1355
  return false;
1332
1356
  }
@@ -1492,7 +1516,10 @@ function usesPerPath(name2, expr) {
1492
1516
  case "array-literal":
1493
1517
  return sum(e.elements);
1494
1518
  case "array-method":
1495
- return add(walk(e.object), e.method === "flat" ? { min: 0, max: 0 } : sum(e.args));
1519
+ if (e.method === "flat") {
1520
+ return add(walk(e.object), e.depthExpr ? walk(e.depthExpr) : { min: 0, max: 0 });
1521
+ }
1522
+ return add(walk(e.object), sum(e.args));
1496
1523
  case "object-literal":
1497
1524
  return sum(e.properties.map((p) => p.value));
1498
1525
  case "arrow":
@@ -1547,7 +1574,14 @@ function inlineBinding(expr, name2, value2) {
1547
1574
  return { kind: "array-literal", elements: e.elements.map((el) => walk(el, enclosing)) };
1548
1575
  case "array-method":
1549
1576
  if (e.method === "flat") {
1550
- return { kind: "array-method", method: "flat", object: walk(e.object, enclosing), args: [], flatDepth: e.flatDepth };
1577
+ return {
1578
+ kind: "array-method",
1579
+ method: "flat",
1580
+ object: walk(e.object, enclosing),
1581
+ args: [],
1582
+ flatDepth: e.flatDepth,
1583
+ ...e.depthExpr ? { depthExpr: walk(e.depthExpr, enclosing) } : {}
1584
+ };
1551
1585
  }
1552
1586
  return { kind: "array-method", method: e.method, object: walk(e.object, enclosing), args: e.args.map((a) => walk(a, enclosing)) };
1553
1587
  case "object-literal":
@@ -1669,6 +1703,9 @@ function exprToString(expr) {
1669
1703
  return `[${expr.elements.map(exprToString).join(", ")}]`;
1670
1704
  case "array-method":
1671
1705
  if (expr.method === "flat") {
1706
+ if (expr.depthExpr) {
1707
+ return `${exprToString(expr.object)}.flat(${exprToString(expr.depthExpr)})`;
1708
+ }
1672
1709
  const d = expr.flatDepth;
1673
1710
  const depthSrc = d === "infinity" ? "Infinity" : String(d);
1674
1711
  return `${exprToString(expr.object)}.flat(${d === 1 ? "" : depthSrc})`;
@@ -1721,6 +1758,9 @@ function stringifyParsedExpr(expr) {
1721
1758
  return `[${expr.elements.map(stringifyParsedExpr).join(", ")}]`;
1722
1759
  case "array-method":
1723
1760
  if (expr.method === "flat") {
1761
+ if (expr.depthExpr) {
1762
+ return `${stringifyParsedExpr(expr.object)}.flat(${stringifyParsedExpr(expr.depthExpr)})`;
1763
+ }
1724
1764
  const d = expr.flatDepth;
1725
1765
  const depthSrc = d === "infinity" ? "Infinity" : String(d);
1726
1766
  return `${stringifyParsedExpr(expr.object)}.flat(${d === 1 ? "" : depthSrc})`;
@@ -1766,7 +1806,9 @@ function materializeGetterCalls(expr, names) {
1766
1806
  case "array-literal":
1767
1807
  return { kind: "array-literal", elements: expr.elements.map(rw) };
1768
1808
  case "array-method":
1769
- if (expr.method === "flat") return { ...expr, object: rw(expr.object) };
1809
+ if (expr.method === "flat") {
1810
+ return { ...expr, object: rw(expr.object), ...expr.depthExpr ? { depthExpr: rw(expr.depthExpr) } : {} };
1811
+ }
1770
1812
  return { ...expr, object: rw(expr.object), args: expr.args.map(rw) };
1771
1813
  case "object-literal":
1772
1814
  return {
@@ -1790,60 +1832,66 @@ function serializeParsedExpr(expr) {
1790
1832
  }
1791
1833
  function freeVarsInBody(body2, params) {
1792
1834
  const found = /* @__PURE__ */ new Set();
1793
- const visit3 = (e) => {
1835
+ const visit3 = (e, bound) => {
1794
1836
  switch (e.kind) {
1795
1837
  case "identifier":
1796
- if (!params.has(e.name)) found.add(e.name);
1838
+ if (!bound.has(e.name)) found.add(e.name);
1797
1839
  return;
1798
1840
  case "binary":
1799
1841
  case "logical":
1800
- visit3(e.left);
1801
- visit3(e.right);
1842
+ visit3(e.left, bound);
1843
+ visit3(e.right, bound);
1802
1844
  return;
1803
1845
  case "unary":
1804
- visit3(e.argument);
1846
+ visit3(e.argument, bound);
1805
1847
  return;
1806
1848
  case "conditional":
1807
- visit3(e.test);
1808
- visit3(e.consequent);
1809
- visit3(e.alternate);
1849
+ visit3(e.test, bound);
1850
+ visit3(e.consequent, bound);
1851
+ visit3(e.alternate, bound);
1810
1852
  return;
1811
1853
  case "member":
1812
- visit3(e.object);
1854
+ visit3(e.object, bound);
1813
1855
  return;
1814
1856
  case "index-access":
1815
- visit3(e.object);
1816
- visit3(e.index);
1857
+ visit3(e.object, bound);
1858
+ visit3(e.index, bound);
1817
1859
  return;
1818
1860
  case "call":
1819
- if (evalBuiltinCalleeName(e.callee) === null) visit3(e.callee);
1820
- e.args.forEach(visit3);
1861
+ if (evalBuiltinCalleeName(e.callee) === null) visit3(e.callee, bound);
1862
+ e.args.forEach((a) => visit3(a, bound));
1821
1863
  return;
1822
1864
  case "template-literal":
1823
- for (const p of e.parts) if (p.type === "expression") visit3(p.expr);
1865
+ for (const p of e.parts) if (p.type === "expression") visit3(p.expr, bound);
1824
1866
  return;
1825
1867
  case "array-literal":
1826
- e.elements.forEach(visit3);
1868
+ e.elements.forEach((el) => visit3(el, bound));
1827
1869
  return;
1828
1870
  case "object-literal":
1829
- for (const p of e.properties) visit3(p.value);
1871
+ for (const p of e.properties) visit3(p.value, bound);
1830
1872
  return;
1831
1873
  case "array-method":
1832
- if (e.method === "includes") {
1833
- visit3(e.object);
1834
- e.args.forEach(visit3);
1874
+ if (e.method === "includes" || e.method === "join") {
1875
+ visit3(e.object, bound);
1876
+ e.args.forEach((a) => visit3(a, bound));
1835
1877
  }
1836
1878
  return;
1879
+ // A nested callback arrow (the `.map`/`.filter` callback argument,
1880
+ // #2094): its own params shadow the outer bound set for its body only.
1881
+ case "arrow": {
1882
+ const inner = e.params.length === 0 ? bound : /* @__PURE__ */ new Set([...bound, ...e.params]);
1883
+ visit3(e.body, inner);
1884
+ return;
1885
+ }
1837
1886
  // Non-serializable kinds don't occur in a serializable body
1838
1887
  // (serializeParsedExpr returns null for them); nothing to collect.
1839
1888
  case "literal":
1840
- case "arrow":
1841
1889
  case "regex":
1842
1890
  case "unsupported":
1843
1891
  return;
1844
1892
  }
1845
1893
  };
1846
- visit3(body2);
1894
+ visit3(body2, params);
1847
1895
  return [...found].sort();
1848
1896
  }
1849
1897
  function freeIdentifiers(expr) {
@@ -1884,6 +1932,7 @@ function freeIdentifiers(expr) {
1884
1932
  case "array-method":
1885
1933
  if (!visit3(e.object, bound)) return false;
1886
1934
  for (const a of e.args) if (!visit3(a, bound)) return false;
1935
+ if (e.method === "flat" && e.depthExpr && !visit3(e.depthExpr, bound)) return false;
1887
1936
  return true;
1888
1937
  case "object-literal":
1889
1938
  for (const p of e.properties) if (!visit3(p.value, bound)) return false;
@@ -1949,6 +1998,18 @@ function toEvalNode(e) {
1949
1998
  return object && index ? { kind: "index-access", object, index } : null;
1950
1999
  }
1951
2000
  case "call": {
2001
+ const cb = asCallbackMethodCall(e);
2002
+ if (cb && (cb.method === "map" || cb.method === "filter")) {
2003
+ const object = toEvalNode(cb.object);
2004
+ if (!object) return null;
2005
+ const body2 = toEvalNode(cb.arrow.body);
2006
+ if (!body2) return null;
2007
+ return {
2008
+ kind: "call",
2009
+ callee: { kind: "member", object, property: cb.method, computed: false },
2010
+ args: [{ kind: "arrow", params: cb.arrow.params, body: body2 }]
2011
+ };
2012
+ }
1952
2013
  if (evalBuiltinCalleeName(e.callee) === null) return null;
1953
2014
  const callee = toEvalNode(e.callee);
1954
2015
  if (!callee) return null;
@@ -1997,6 +2058,13 @@ function toEvalNode(e) {
1997
2058
  const arg = toEvalNode(e.args[0]);
1998
2059
  return object && arg ? { kind: "array-method", method: "includes", object, args: [arg] } : null;
1999
2060
  }
2061
+ if (e.method === "join" && e.args.length <= 1) {
2062
+ const object = toEvalNode(e.object);
2063
+ if (!object) return null;
2064
+ if (e.args.length === 0) return { kind: "array-method", method: "join", object, args: [] };
2065
+ const sep = toEvalNode(e.args[0]);
2066
+ return sep ? { kind: "array-method", method: "join", object, args: [sep] } : null;
2067
+ }
2000
2068
  return null;
2001
2069
  }
2002
2070
  // Outside the evaluator's pure-expression surface — refuse so the caller
@@ -3568,6 +3636,84 @@ function irToHtmlTemplate(node, restSpreadNames, loopDepth = 0, loopParams, bran
3568
3636
  return assertNever(node);
3569
3637
  }
3570
3638
  }
3639
+ function buildLoopSkeletonTemplate(node, safe) {
3640
+ switch (node.type) {
3641
+ case "element": {
3642
+ const attrParts = [];
3643
+ for (const a of node.attrs) {
3644
+ if (a.name === "...") return null;
3645
+ if (a.name === "dangerouslySetInnerHTML") return null;
3646
+ if (a.name === "key") {
3647
+ attrParts.push(`${keyAttrName(0)}=""`);
3648
+ continue;
3649
+ }
3650
+ const v = a.value;
3651
+ switch (v.kind) {
3652
+ case "literal":
3653
+ attrParts.push(`${toHTMLAttrName(a.name)}="${v.value}"`);
3654
+ break;
3655
+ case "boolean-attr":
3656
+ attrParts.push(toHTMLAttrName(a.name));
3657
+ break;
3658
+ case "boolean-shorthand":
3659
+ case "jsx-children":
3660
+ break;
3661
+ case "expression":
3662
+ case "template": {
3663
+ const attrKey = node.slotId ? `${node.slotId}::${a.name}` : null;
3664
+ if (!attrKey || !safe.reactiveAttrKeys.has(attrKey)) return null;
3665
+ break;
3666
+ }
3667
+ case "spread":
3668
+ return null;
3669
+ }
3670
+ }
3671
+ if (node.slotId) attrParts.push(`bf="${node.slotId}"`);
3672
+ const attrs = attrParts.join(" ");
3673
+ let children2 = "";
3674
+ for (const child of node.children) {
3675
+ const rendered = buildLoopSkeletonTemplate(child, safe);
3676
+ if (rendered === null) return null;
3677
+ children2 += rendered;
3678
+ }
3679
+ if (children2 || !VOID_ELEMENTS.has(node.tag)) {
3680
+ return `<${node.tag}${attrs ? " " + attrs : ""}>${children2}</${node.tag}>`;
3681
+ }
3682
+ return `<${node.tag}${attrs ? " " + attrs : ""} />`;
3683
+ }
3684
+ case "text":
3685
+ return node.value;
3686
+ case "expression":
3687
+ if (node.expr === "null" || node.expr === "undefined") return "";
3688
+ if (!node.slotId) {
3689
+ return null;
3690
+ }
3691
+ if (!safe.reactiveTextSlotIds.has(node.slotId)) return null;
3692
+ return `<!--bf:${node.slotId}--><!--/-->`;
3693
+ case "fragment": {
3694
+ let out = "";
3695
+ for (const child of node.children) {
3696
+ const rendered = buildLoopSkeletonTemplate(child, safe);
3697
+ if (rendered === null) return null;
3698
+ out += rendered;
3699
+ }
3700
+ return out;
3701
+ }
3702
+ // Conditionals, child components, nested loops, and provider/async/
3703
+ // if-statement/slot boundaries are all out of scope for the hoisted
3704
+ // fast path — the caller falls back to `irToHtmlTemplate`.
3705
+ case "conditional":
3706
+ case "component":
3707
+ case "loop":
3708
+ case "if-statement":
3709
+ case "provider":
3710
+ case "async":
3711
+ case "slot":
3712
+ return null;
3713
+ default:
3714
+ return assertNever(node);
3715
+ }
3716
+ }
3571
3717
  function irToPlaceholderTemplate(node, restSpreadNames, loopDepth = 0, loopParams) {
3572
3718
  const recurse = (n) => irToPlaceholderTemplate(n, restSpreadNames, loopDepth, loopParams);
3573
3719
  const wrapExpr = (expr) => wrapExprWithLoopParams(expr, loopParams);
@@ -3961,7 +4107,20 @@ function generateCsrTemplate(node, inlinableConstants, ctx2, insideLoop, restSpr
3961
4107
  }
3962
4108
  }
3963
4109
  }
3964
- return generateCsrTemplateWithOpts(node, { inlinableConstants, restSpreadNames, propsObjectName, csrEnv, insideLoop, unsafeLocalNames, deferredChildSlots, loopDepth: -1 });
4110
+ const effectiveUnsafeLocalNames = mergeCsrNullUnsafe(ctx2, unsafeLocalNames);
4111
+ return generateCsrTemplateWithOpts(node, { inlinableConstants, restSpreadNames, propsObjectName, csrEnv, insideLoop, unsafeLocalNames: effectiveUnsafeLocalNames, deferredChildSlots, loopDepth: -1 });
4112
+ }
4113
+ function mergeCsrNullUnsafe(ctx2, unsafeLocalNames) {
4114
+ let merged = null;
4115
+ let exemptNames = null;
4116
+ for (const [name2, entry] of ctx2.csrInlinable) {
4117
+ if (entry !== null || unsafeLocalNames?.has(name2)) continue;
4118
+ exemptNames ??= new Set(ctx2.localConstants.filter((c) => c.isJsx || c.systemConstructKind).map((c) => c.name));
4119
+ if (exemptNames.has(name2)) continue;
4120
+ if (!merged) merged = new Set(unsafeLocalNames ?? []);
4121
+ merged.add(name2);
4122
+ }
4123
+ return merged ?? unsafeLocalNames;
3965
4124
  }
3966
4125
  function buildCsrEnvForCtx(ctx2, inlinableConstants, propsObjectName) {
3967
4126
  const base = buildSignalMemoEnv(ctx2.signals, ctx2.memos, propsObjectName ?? null);
@@ -4561,6 +4720,15 @@ function createAnalyzerContext(sourceFile, filePath) {
4561
4720
  checker: null,
4562
4721
  componentBodyBlock: null,
4563
4722
  getJS(node) {
4723
+ let ownSourceFile;
4724
+ try {
4725
+ ownSourceFile = node.getSourceFile();
4726
+ } catch {
4727
+ ownSourceFile = void 0;
4728
+ }
4729
+ if (ownSourceFile && ownSourceFile !== sourceFile) {
4730
+ return node.getText(ownSourceFile);
4731
+ }
4564
4732
  return reconstructWithoutTypes(node, sourceFile, this.typeExcludeRanges);
4565
4733
  }
4566
4734
  };
@@ -6499,6 +6667,7 @@ function extractProps(param, ctx2) {
6499
6667
  loc: getSourceLocation(param, ctx2.sourceFile, ctx2.filePath),
6500
6668
  hasIgnoreDirective: ignored
6501
6669
  };
6670
+ const memberTypes = param.type ? collectMemberTypes(param.type, ctx2) : null;
6502
6671
  for (const element of param.name.elements) {
6503
6672
  if (ts8.isBindingElement(element) && ts8.isIdentifier(element.name)) {
6504
6673
  const localName2 = element.name.text;
@@ -6507,10 +6676,12 @@ function extractProps(param, ctx2) {
6507
6676
  ctx2.restPropsName = localName2;
6508
6677
  continue;
6509
6678
  }
6679
+ const sourcePropName = element.propertyName && ts8.isIdentifier(element.propertyName) ? element.propertyName.text : localName2;
6680
+ const resolvedType = memberTypes?.get(sourcePropName) ?? { kind: "unknown", raw: "unknown" };
6510
6681
  const defaultContainsArrow = element.initializer ? nodeContainsArrow(element.initializer) : false;
6511
6682
  ctx2.propsParams.push({
6512
6683
  name: localName2,
6513
- type: { kind: "unknown", raw: "unknown" },
6684
+ type: resolvedType,
6514
6685
  optional: !!element.initializer,
6515
6686
  defaultValue: defaultValue2,
6516
6687
  defaultContainsArrow: defaultContainsArrow || void 0
@@ -6568,6 +6739,36 @@ function collectKeysFromMembers(members, ctx2) {
6568
6739
  }
6569
6740
  return keys;
6570
6741
  }
6742
+ function collectMemberTypes(typeNode, ctx2) {
6743
+ const isResolvablePrimitive = (info) => info.kind === "primitive" && (info.primitive === "string" || info.primitive === "number" || info.primitive === "boolean");
6744
+ const fromMembers = (members) => {
6745
+ const map = /* @__PURE__ */ new Map();
6746
+ for (const member of members) {
6747
+ if (ts8.isPropertySignature(member) && member.name && member.type && !member.questionToken) {
6748
+ const info = typeNodeToTypeInfo(member.type, ctx2.sourceFile);
6749
+ if (info && isResolvablePrimitive(info)) {
6750
+ map.set(member.name.getText(ctx2.sourceFile), info);
6751
+ }
6752
+ }
6753
+ }
6754
+ return map;
6755
+ };
6756
+ if (ts8.isTypeLiteralNode(typeNode)) {
6757
+ return fromMembers(typeNode.members);
6758
+ }
6759
+ if (ts8.isTypeReferenceNode(typeNode)) {
6760
+ const typeName = typeNode.typeName.getText(ctx2.sourceFile);
6761
+ const typeDecl = findTypeDeclaration(typeName, ctx2.sourceFile);
6762
+ if (!typeDecl) return null;
6763
+ if (ts8.isInterfaceDeclaration(typeDecl)) {
6764
+ return fromMembers(typeDecl.members);
6765
+ }
6766
+ if (ts8.isTypeAliasDeclaration(typeDecl) && ts8.isTypeLiteralNode(typeDecl.type)) {
6767
+ return fromMembers(typeDecl.type.members);
6768
+ }
6769
+ }
6770
+ return null;
6771
+ }
6571
6772
  function extractPropsFromType(typeNode, ctx2) {
6572
6773
  if (ts8.isTypeLiteralNode(typeNode)) {
6573
6774
  extractPropsFromTypeMembers(typeNode.members, ctx2);
@@ -7300,6 +7501,115 @@ var init_types = __esm({
7300
7501
  }
7301
7502
  });
7302
7503
 
7504
+ // ../jsx/src/module-exports.ts
7505
+ function generateModuleExports(ir, extraInlineExported = /* @__PURE__ */ new Set(), rewriteRelativeImport) {
7506
+ const lines = [];
7507
+ for (const constant of ir.metadata.localConstants) {
7508
+ if (!constant.isExported) continue;
7509
+ const keyword = constant.declarationKind ?? "const";
7510
+ if (!constant.value) {
7511
+ lines.push(`export ${keyword} ${constant.name}`);
7512
+ continue;
7513
+ }
7514
+ const value2 = constant.value.trim();
7515
+ if (/^createContext\b/.test(value2) || /^new WeakMap\b/.test(value2)) continue;
7516
+ lines.push(`export ${keyword} ${constant.name} = ${constant.value}`);
7517
+ }
7518
+ for (const func of ir.metadata.localFunctions) {
7519
+ if (!func.isExported) continue;
7520
+ const params = func.typedParams !== void 0 ? func.typedParams : func.params.map(formatParamWithType).join(", ");
7521
+ const returnAnnotation = func.typedReturnType ? `: ${func.typedReturnType}` : "";
7522
+ const body2 = func.typedBody ?? func.body;
7523
+ const asyncKw = func.isAsync ? "async " : "";
7524
+ lines.push(`export ${asyncKw}function ${func.name}(${params})${returnAnnotation} ${body2}`);
7525
+ }
7526
+ const inlineExported = collectInlineExportedNames(ir);
7527
+ for (const name2 of extraInlineExported) inlineExported.add(name2);
7528
+ for (const block of ir.metadata.namedExports) {
7529
+ const isReexportFrom = block.source !== null;
7530
+ const survivingSpecs = block.specifiers.filter((spec) => {
7531
+ if (isReexportFrom) return true;
7532
+ return !(inlineExported.has(spec.name) && spec.alias == null);
7533
+ });
7534
+ if (survivingSpecs.length === 0) continue;
7535
+ const specText = survivingSpecs.map((s) => {
7536
+ const prefix2 = s.isTypeOnly ? "type " : "";
7537
+ return s.alias ? `${prefix2}${s.name} as ${s.alias}` : `${prefix2}${s.name}`;
7538
+ }).join(", ");
7539
+ const typeKw = block.isTypeOnly ? "type " : "";
7540
+ if (isReexportFrom) {
7541
+ const source = rewriteRelativeImport && block.source.startsWith(".") ? rewriteRelativeImport(block.source) : block.source;
7542
+ lines.push(`export ${typeKw}{ ${specText} } from '${source}'`);
7543
+ } else {
7544
+ lines.push(`export ${typeKw}{ ${specText} }`);
7545
+ }
7546
+ }
7547
+ return lines.length > 0 ? lines.join("\n") : null;
7548
+ }
7549
+ function collectInlineExportedNames(ir) {
7550
+ const names = /* @__PURE__ */ new Set();
7551
+ for (const c of ir.metadata.localConstants) {
7552
+ if (c.isExported) names.add(c.name);
7553
+ }
7554
+ for (const f of ir.metadata.localFunctions) {
7555
+ if (f.isExported) names.add(f.name);
7556
+ }
7557
+ if (ir.metadata.isExported && ir.metadata.componentName) {
7558
+ names.add(ir.metadata.componentName);
7559
+ }
7560
+ return names;
7561
+ }
7562
+ function formatParamWithType(p) {
7563
+ const rest2 = p.isRest ? "..." : "";
7564
+ const optional = p.optional ? "?" : "";
7565
+ const typeAnnotation = p.type?.raw && p.type.raw !== "unknown" ? `: ${p.type.raw}` : "";
7566
+ const defaultPart = p.defaultValue !== void 0 ? ` = ${p.defaultValue}` : "";
7567
+ return `${rest2}${p.name}${optional}${typeAnnotation}${defaultPart}`;
7568
+ }
7569
+ function findReachableNames(primaryRefs, declarations) {
7570
+ const allNames = new Set(declarations.map((d) => d.name));
7571
+ const bodyMap = new Map(declarations.map((d) => [d.name, d.body]));
7572
+ const reachable = /* @__PURE__ */ new Set();
7573
+ const queue = [];
7574
+ for (const name2 of allNames) {
7575
+ if (new RegExp(`\\b${name2}\\b`).test(primaryRefs)) {
7576
+ reachable.add(name2);
7577
+ queue.push(name2);
7578
+ }
7579
+ }
7580
+ while (queue.length > 0) {
7581
+ const current = queue.shift();
7582
+ const body2 = bodyMap.get(current) || "";
7583
+ for (const name2 of allNames) {
7584
+ if (!reachable.has(name2) && new RegExp(`\\b${name2}\\b`).test(body2)) {
7585
+ reachable.add(name2);
7586
+ queue.push(name2);
7587
+ }
7588
+ }
7589
+ }
7590
+ return reachable;
7591
+ }
7592
+ function extractFunctionParams(value2) {
7593
+ const arrowMatch = value2.match(/^(?:async\s*)?\(([^)]*)\)\s*(?::\s*[^=]+)?\s*=>/);
7594
+ if (arrowMatch) {
7595
+ return arrowMatch[1].split(",").map((p) => p.trim().split(":")[0].split("=")[0].trim()).filter(Boolean).join(", ");
7596
+ }
7597
+ const singleMatch = value2.match(/^(?:async\s*)?(\w+)\s*=>/);
7598
+ if (singleMatch) {
7599
+ return singleMatch[1];
7600
+ }
7601
+ const funcMatch = value2.match(/^(?:async\s*)?function\s*\w*\s*\(([^)]*)\)/);
7602
+ if (funcMatch) {
7603
+ return funcMatch[1].split(",").map((p) => p.trim().split(":")[0].split("=")[0].trim()).filter(Boolean).join(", ");
7604
+ }
7605
+ return "";
7606
+ }
7607
+ var init_module_exports = __esm({
7608
+ "../jsx/src/module-exports.ts"() {
7609
+ "use strict";
7610
+ }
7611
+ });
7612
+
7303
7613
  // ../jsx/src/builtins.ts
7304
7614
  function isClientBuiltinName(name2) {
7305
7615
  return name2 === "Async" || name2 === "Region";
@@ -8408,6 +8718,7 @@ function transformExpression(node, ctx2) {
8408
8718
  return transformExpressionInner(expr, ctx2, node, isClientOnly);
8409
8719
  }
8410
8720
  function transformExpressionInner(expr, ctx2, node, isClientOnly) {
8721
+ expr = tryDesugarInterleaveTaggedTemplate(expr, ctx2);
8411
8722
  checkBareSignalOrMemoIdentifier(expr, ctx2);
8412
8723
  if (ts11.isIdentifier(expr)) {
8413
8724
  const jsxNode = ctx2.analyzer.jsxConstants.get(expr.text);
@@ -8957,11 +9268,10 @@ function isIteratorShapeCall(node) {
8957
9268
  return { array: node.expression.expression, shape: name2 };
8958
9269
  }
8959
9270
  function extractSortComparator(callback, _method, ctx2) {
8960
- const unsupported = () => {
8961
- const raw = ctx2.getJS(callback);
8962
- return {
8963
- result: null,
8964
- unsupportedReason: `Sort comparator '${raw}' is not a supported shape. Accepted:
9271
+ const outerRaw = ctx2.getJS(callback);
9272
+ const unsupported = () => ({
9273
+ result: null,
9274
+ unsupportedReason: `Sort comparator '${outerRaw}' is not a supported shape. Accepted:
8965
9275
  (a, b) => a - b
8966
9276
  (a, b) => a.field - b.field
8967
9277
  (a, b) => a.localeCompare(b)
@@ -8969,15 +9279,25 @@ function extractSortComparator(callback, _method, ctx2) {
8969
9279
  (a, b) => a.field > b.field ? 1 : a.field < b.field ? -1 : 0
8970
9280
  any of the above '||'-chained for multi-key tie-breaks
8971
9281
  (reverse the operands for descending order).`
8972
- };
8973
- };
8974
- if (!ts11.isArrowFunction(callback) && !ts11.isFunctionExpression(callback)) {
9282
+ });
9283
+ let resolvedNode = callback;
9284
+ if (ts11.isIdentifier(callback)) {
9285
+ const resolved = resolveSortComparatorIdentifier(callback.text, ctx2);
9286
+ if (!resolved) {
9287
+ return {
9288
+ result: null,
9289
+ unsupportedReason: `Sort comparator '${outerRaw}' could not be resolved to a local function \u2014 declare it in the same file or inline it.`
9290
+ };
9291
+ }
9292
+ resolvedNode = resolved;
9293
+ }
9294
+ if (!ts11.isArrowFunction(resolvedNode) && !ts11.isFunctionExpression(resolvedNode)) {
8975
9295
  return {
8976
9296
  result: null,
8977
9297
  unsupportedReason: "Sort comparator must be an arrow function or function expression"
8978
9298
  };
8979
9299
  }
8980
- const arrow = tsNodeToParsedExpr(callback);
9300
+ const arrow = tsNodeToParsedExpr(resolvedNode);
8981
9301
  if (arrow.kind !== "arrow" || arrow.params.length !== 2) return unsupported();
8982
9302
  if (sortComparatorFromArrow(arrow) === null) return unsupported();
8983
9303
  return {
@@ -8989,6 +9309,20 @@ function extractSortComparator(callback, _method, ctx2) {
8989
9309
  }
8990
9310
  };
8991
9311
  }
9312
+ function resolveSortComparatorIdentifier(name2, ctx2) {
9313
+ const constInfo = findLocalConst(name2, ctx2);
9314
+ const fnInfo = findLocalFunction(name2, ctx2);
9315
+ if (constInfo && fnInfo) return null;
9316
+ if (constInfo) {
9317
+ const ast = parseConstInitializer(constInfo);
9318
+ return ast && (ts11.isArrowFunction(ast) || ts11.isFunctionExpression(ast)) ? ast : null;
9319
+ }
9320
+ if (fnInfo) {
9321
+ const ast = parseFunctionInfoAsExpr(fnInfo);
9322
+ return ast && (ts11.isArrowFunction(ast) || ts11.isFunctionExpression(ast)) ? ast : null;
9323
+ }
9324
+ return null;
9325
+ }
8992
9326
  function extractFilterPredicate(callback, ctx2) {
8993
9327
  if (!ts11.isArrowFunction(callback)) return { result: null };
8994
9328
  if (callback.parameters.length < 1) return { result: null };
@@ -9056,7 +9390,7 @@ function extractLoopParamBindings(pattern) {
9056
9390
  const appendDotAccess = (prefix2, key) => {
9057
9391
  return isIdent(key) ? `${prefix2}.${key}` : `${prefix2}[${JSON.stringify(key)}]`;
9058
9392
  };
9059
- const walk = (p, prefix2) => {
9393
+ const walk = (p, prefix2, segments) => {
9060
9394
  if (unsupported) return;
9061
9395
  if (ts11.isArrayBindingPattern(p)) {
9062
9396
  const elements3 = p.elements;
@@ -9076,15 +9410,17 @@ function extractLoopParamBindings(pattern) {
9076
9410
  bindings.push({
9077
9411
  name: el.name.text,
9078
9412
  path: prefix2,
9079
- rest: { kind: "array", from: index }
9413
+ rest: { kind: "array", from: index },
9414
+ segments
9080
9415
  });
9081
9416
  return;
9082
9417
  }
9083
9418
  const path25 = `${prefix2}[${index}]`;
9419
+ const nextSegments = [...segments, { kind: "index", index }];
9084
9420
  if (ts11.isIdentifier(el.name)) {
9085
- bindings.push({ name: el.name.text, path: path25 });
9421
+ bindings.push({ name: el.name.text, path: path25, segments: nextSegments });
9086
9422
  } else {
9087
- walk(el.name, path25);
9423
+ walk(el.name, path25, nextSegments);
9088
9424
  }
9089
9425
  }
9090
9426
  return;
@@ -9106,7 +9442,8 @@ function extractLoopParamBindings(pattern) {
9106
9442
  bindings.push({
9107
9443
  name: el.name.text,
9108
9444
  path: prefix2,
9109
- rest: { kind: "object", exclude: collectedKeys }
9445
+ rest: { kind: "object", exclude: collectedKeys },
9446
+ segments
9110
9447
  });
9111
9448
  return;
9112
9449
  }
@@ -9126,17 +9463,19 @@ function extractLoopParamBindings(pattern) {
9126
9463
  unsupported = true;
9127
9464
  return;
9128
9465
  }
9129
- collectedKeys.push({ key: keyText2, isIdent: isIdent(keyText2) });
9466
+ const keyIsIdent = isIdent(keyText2);
9467
+ collectedKeys.push({ key: keyText2, isIdent: keyIsIdent });
9130
9468
  const path25 = appendDotAccess(prefix2, keyText2);
9469
+ const nextSegments = [...segments, { kind: "field", key: keyText2, isIdent: keyIsIdent }];
9131
9470
  if (ts11.isIdentifier(el.name)) {
9132
- bindings.push({ name: el.name.text, path: path25 });
9471
+ bindings.push({ name: el.name.text, path: path25, segments: nextSegments });
9133
9472
  } else {
9134
- walk(el.name, path25);
9473
+ walk(el.name, path25, nextSegments);
9135
9474
  }
9136
9475
  }
9137
9476
  };
9138
9477
  if (ts11.isArrayBindingPattern(pattern) || ts11.isObjectBindingPattern(pattern)) {
9139
- walk(pattern, "");
9478
+ walk(pattern, "", []);
9140
9479
  if (unsupported) return { unsupported: true };
9141
9480
  return bindings;
9142
9481
  }
@@ -9585,11 +9924,11 @@ function transformMapCall(node, ctx2, isClientOnly = false, method2 = "map") {
9585
9924
  if (stmt === returnStmt) break;
9586
9925
  const js = ctx2.getJS(stmt);
9587
9926
  const tjs = ctx2.getTemplateJS(stmt);
9588
- const ts25 = stmt.getText(ctx2.sourceFile);
9927
+ const ts26 = stmt.getText(ctx2.sourceFile);
9589
9928
  preambleStmts.push(js.endsWith(";") ? js : js + ";");
9590
9929
  templatePreambleStmts.push(tjs.endsWith(";") ? tjs : tjs + ";");
9591
- typedPreambleStmts.push(ts25.endsWith(";") ? ts25 : ts25 + ";");
9592
- if (js !== ts25) hasTypeDiff = true;
9930
+ typedPreambleStmts.push(ts26.endsWith(";") ? ts26 : ts26 + ";");
9931
+ if (js !== ts26) hasTypeDiff = true;
9593
9932
  if (js !== tjs) hasTemplateDiff = true;
9594
9933
  }
9595
9934
  if (preambleStmts.length > 0) {
@@ -9924,6 +10263,7 @@ function getAttributeValue(attr, ctx2) {
9924
10263
  expr = branchInit;
9925
10264
  }
9926
10265
  }
10266
+ expr = tryDesugarInterleaveTaggedTemplate(expr, ctx2);
9927
10267
  if (ts11.isAwaitExpression(expr)) {
9928
10268
  ctx2.analyzer.errors.push(
9929
10269
  createError(
@@ -10053,6 +10393,13 @@ function findLocalConst(name2, ctx2) {
10053
10393
  const pool = fnScoped.length > 0 ? fnScoped : matches;
10054
10394
  return pool[pool.length - 1];
10055
10395
  }
10396
+ function findLocalFunction(name2, ctx2) {
10397
+ const matches = ctx2.analyzer.localFunctions.filter((f) => f.name === name2);
10398
+ if (matches.length === 0) return void 0;
10399
+ const fnScoped = matches.filter((f) => !f.isModule);
10400
+ const pool = fnScoped.length > 0 ? fnScoped : matches;
10401
+ return pool[pool.length - 1];
10402
+ }
10056
10403
  function isDynamicTagLocal(name2, ctx2) {
10057
10404
  if (!hasDynamicTagBinding(name2, ctx2.sourceFile)) return false;
10058
10405
  const a = ctx2.analyzer;
@@ -10138,6 +10485,130 @@ function parseConstInitializerImpl(c) {
10138
10485
  function astText(node) {
10139
10486
  return node.getText(node.getSourceFile());
10140
10487
  }
10488
+ function parseFunctionInfoAsExpr(fn) {
10489
+ const cached = functionInfoExprCache.get(fn);
10490
+ if (cached !== void 0) return cached;
10491
+ const result2 = parseFunctionInfoAsExprImpl(fn);
10492
+ functionInfoExprCache.set(fn, result2);
10493
+ return result2;
10494
+ }
10495
+ function parseFunctionInfoAsExprImpl(fn) {
10496
+ if (!fn.body) return null;
10497
+ const params = fn.typedParams !== void 0 ? fn.typedParams : fn.params.map(formatParamWithType).join(", ");
10498
+ const body2 = fn.typedBody ?? fn.body;
10499
+ const wrapped = `const __bf_resolve_fn__ = function(${params}) ${body2}`;
10500
+ const sf = ts11.createSourceFile(
10501
+ "__bf_resolve_fn.ts",
10502
+ wrapped,
10503
+ ts11.ScriptTarget.Latest,
10504
+ /* setParentNodes */
10505
+ true,
10506
+ ts11.ScriptKind.TS
10507
+ );
10508
+ const stmt = sf.statements[0];
10509
+ if (!stmt || !ts11.isVariableStatement(stmt)) return null;
10510
+ const decl = stmt.declarationList.declarations[0];
10511
+ if (!decl?.initializer) return null;
10512
+ return decl.initializer;
10513
+ }
10514
+ function tryDesugarInterleaveTaggedTemplate(expr, ctx2) {
10515
+ if (!ts11.isTaggedTemplateExpression(expr)) return expr;
10516
+ if (!ts11.isIdentifier(expr.tag)) return expr;
10517
+ const resolvedTag = resolveInterleaveTagIdentifier(expr.tag.text, ctx2);
10518
+ if (!resolvedTag) return expr;
10519
+ if (!isInterleaveTagFunction(resolvedTag)) return expr;
10520
+ const rewritten = buildUntaggedTemplateLiteral(expr, ctx2);
10521
+ return rewritten ?? expr;
10522
+ }
10523
+ function resolveInterleaveTagIdentifier(name2, ctx2) {
10524
+ const constInfo = findLocalConst(name2, ctx2);
10525
+ const fnInfo = findLocalFunction(name2, ctx2);
10526
+ if (constInfo && fnInfo) return null;
10527
+ if (constInfo) {
10528
+ const ast = parseConstInitializer(constInfo);
10529
+ return ast && (ts11.isArrowFunction(ast) || ts11.isFunctionExpression(ast)) ? ast : null;
10530
+ }
10531
+ if (fnInfo) {
10532
+ const ast = parseFunctionInfoAsExpr(fnInfo);
10533
+ return ast && (ts11.isArrowFunction(ast) || ts11.isFunctionExpression(ast)) ? ast : null;
10534
+ }
10535
+ return null;
10536
+ }
10537
+ function isInterleaveTagFunction(fn) {
10538
+ if (!ts11.isArrowFunction(fn) && !ts11.isFunctionExpression(fn)) return false;
10539
+ if (fn.parameters.length !== 2) return false;
10540
+ const [partsParam, argsParam] = fn.parameters;
10541
+ if (!ts11.isIdentifier(partsParam.name) || partsParam.dotDotDotToken) return false;
10542
+ if (!ts11.isIdentifier(argsParam.name) || !argsParam.dotDotDotToken) return false;
10543
+ const parsed = tsNodeToParsedExpr(fn);
10544
+ if (parsed.kind !== "arrow") return false;
10545
+ return isInterleaveReduceCall(parsed.body, partsParam.name.text, argsParam.name.text);
10546
+ }
10547
+ function isInterleaveReduceCall(body2, partsName, argsName) {
10548
+ if (body2.kind !== "call" || body2.args.length !== 2) return false;
10549
+ const { callee, args: args2 } = body2;
10550
+ if (callee.kind !== "member" || callee.computed || callee.property !== "reduce") return false;
10551
+ if (callee.object.kind !== "identifier" || callee.object.name !== partsName) return false;
10552
+ const [callback, init] = args2;
10553
+ if (init.kind !== "literal" || init.literalType !== "string" || init.value !== "") return false;
10554
+ if (callback.kind !== "arrow" || callback.params.length !== 3) return false;
10555
+ const [acc, p, i] = callback.params;
10556
+ return isInterleaveReduceCallbackBody(callback.body, acc, p, i, argsName);
10557
+ }
10558
+ function isInterleaveReduceCallbackBody(body2, acc, p, i, argsName) {
10559
+ if (body2.kind !== "binary" || body2.op !== "+") return false;
10560
+ const { left, right } = body2;
10561
+ if (left.kind !== "binary" || left.op !== "+") return false;
10562
+ if (left.left.kind !== "identifier" || left.left.name !== acc) return false;
10563
+ if (left.right.kind !== "identifier" || left.right.name !== p) return false;
10564
+ return isInterleaveSpanExpr(right, i, argsName);
10565
+ }
10566
+ function isInterleaveSpanExpr(expr, i, argsName) {
10567
+ let inner = expr;
10568
+ if (inner.kind === "call" && inner.args.length === 1 && inner.callee.kind === "identifier" && inner.callee.name === "String") {
10569
+ inner = inner.args[0];
10570
+ }
10571
+ if (inner.kind !== "logical" || inner.op !== "??") return false;
10572
+ if (inner.right.kind !== "literal" || inner.right.literalType !== "string" || inner.right.value !== "") {
10573
+ return false;
10574
+ }
10575
+ const idx = inner.left;
10576
+ if (idx.kind !== "index-access") return false;
10577
+ if (idx.object.kind !== "identifier" || idx.object.name !== argsName) return false;
10578
+ if (idx.index.kind !== "identifier" || idx.index.name !== i) return false;
10579
+ return true;
10580
+ }
10581
+ function buildUntaggedTemplateLiteral(node, ctx2) {
10582
+ const template = node.template;
10583
+ let text;
10584
+ if (ts11.isNoSubstitutionTemplateLiteral(template)) {
10585
+ text = "`" + (template.rawText ?? template.text) + "`";
10586
+ } else {
10587
+ let body2 = template.head.rawText ?? template.head.text;
10588
+ for (const span of template.templateSpans) {
10589
+ const spanText = ctx2.getJS(span.expression);
10590
+ body2 += "${(" + spanText + ") ?? ''}";
10591
+ body2 += span.literal.rawText ?? span.literal.text;
10592
+ }
10593
+ text = "`" + body2 + "`";
10594
+ }
10595
+ const wrapped = `const __bf_resolve_tagged__ = (${text})`;
10596
+ const sf = ts11.createSourceFile(
10597
+ "__bf_resolve_tagged.tsx",
10598
+ wrapped,
10599
+ ts11.ScriptTarget.Latest,
10600
+ /* setParentNodes */
10601
+ true,
10602
+ ts11.ScriptKind.TSX
10603
+ );
10604
+ const stmt = sf.statements[0];
10605
+ if (!stmt || !ts11.isVariableStatement(stmt)) return null;
10606
+ const decl = stmt.declarationList.declarations[0];
10607
+ if (!decl?.initializer) return null;
10608
+ const result2 = ts11.isParenthesizedExpression(decl.initializer) ? decl.initializer.expression : decl.initializer;
10609
+ if (!ts11.isTemplateExpression(result2) && !ts11.isNoSubstitutionTemplateLiteral(result2)) return null;
10610
+ return result2;
10611
+ }
10141
10612
  function parseTernary(expr, ctx2) {
10142
10613
  const whenTrueValue = getStringValue(expr.whenTrue);
10143
10614
  const whenFalseValue = getStringValue(expr.whenFalse);
@@ -10550,13 +11021,14 @@ function buildIfStatementChain(analyzer, ctx2) {
10550
11021
  }
10551
11022
  return alternate;
10552
11023
  }
10553
- var CLIENT_DIRECTIVE_INTERIOR_RE2, BLOCK_COMMENT_RE2, constInitializerCache;
11024
+ var CLIENT_DIRECTIVE_INTERIOR_RE2, BLOCK_COMMENT_RE2, constInitializerCache, functionInfoExprCache;
10554
11025
  var init_jsx_to_ir = __esm({
10555
11026
  "../jsx/src/jsx-to-ir.ts"() {
10556
11027
  "use strict";
10557
11028
  init_types();
10558
11029
  init_analyzer_context();
10559
11030
  init_expression_parser();
11031
+ init_module_exports();
10560
11032
  init_errors();
10561
11033
  init_builtins();
10562
11034
  init_reactivity_checker();
@@ -10568,6 +11040,7 @@ var init_jsx_to_ir = __esm({
10568
11040
  CLIENT_DIRECTIVE_INTERIOR_RE2 = /^\s*@client\s*$/;
10569
11041
  BLOCK_COMMENT_RE2 = /\/\*([\s\S]*?)\*\//g;
10570
11042
  constInitializerCache = /* @__PURE__ */ new WeakMap();
11043
+ functionInfoExprCache = /* @__PURE__ */ new WeakMap();
10571
11044
  }
10572
11045
  });
10573
11046
 
@@ -11242,6 +11715,7 @@ function collectElements(node, ctx2, siblingOffsets, insideConditional = false)
11242
11715
  const { useElementReconciliation, innerLoops } = decideLoopRendering(l, siblingOffsets, ctx2);
11243
11716
  let template = "";
11244
11717
  let staticItemTemplate;
11718
+ let skeletonTemplate;
11245
11719
  if (l.childComponent) {
11246
11720
  template = "";
11247
11721
  if (l.isStaticArray && l.children[0]) {
@@ -11260,6 +11734,11 @@ function collectElements(node, ctx2, siblingOffsets, insideConditional = false)
11260
11734
  template = useElementReconciliation ? irToPlaceholderTemplate(l.children[0], buildRestSpreadNames(ctx2), 0, loopParamSpec) : irToHtmlTemplate(l.children[0], buildRestSpreadNames(ctx2), 0, loopParamSpec);
11261
11735
  if (l.isStaticArray) {
11262
11736
  staticItemTemplate = useElementReconciliation ? irToPlaceholderTemplate(l.children[0], buildRestSpreadNames(ctx2), 0) : irToHtmlTemplate(l.children[0], buildRestSpreadNames(ctx2), 0);
11737
+ } else if (!useElementReconciliation && !l.bodyIsMultiRoot && !l.bodyIsItemConditional) {
11738
+ skeletonTemplate = buildLoopSkeletonTemplate(l.children[0], {
11739
+ reactiveAttrKeys: new Set(bindings.reactiveAttrs.map((a) => `${a.childSlotId}::${a.attrName}`)),
11740
+ reactiveTextSlotIds: new Set(bindings.reactiveTexts.map((t) => t.slotId))
11741
+ }) ?? void 0;
11263
11742
  }
11264
11743
  }
11265
11744
  ctx2.loopElements.push({
@@ -11277,6 +11756,7 @@ function collectElements(node, ctx2, siblingOffsets, insideConditional = false)
11277
11756
  iterationShape: l.iterationShape,
11278
11757
  template,
11279
11758
  staticItemTemplate,
11759
+ skeletonTemplate,
11280
11760
  childEventHandlers: childHandlers,
11281
11761
  bindings,
11282
11762
  childComponent: l.childComponent,
@@ -12180,6 +12660,45 @@ var init_imports = __esm({
12180
12660
  }
12181
12661
  });
12182
12662
 
12663
+ // ../jsx/src/lowering-registry.ts
12664
+ function registerLoweringPlugin(plugin) {
12665
+ const existing = plugins.findIndex((p) => p.name === plugin.name);
12666
+ if (existing >= 0) plugins[existing] = plugin;
12667
+ else plugins.push(plugin);
12668
+ }
12669
+ function getLoweringPlugins() {
12670
+ return [...plugins];
12671
+ }
12672
+ function prepareLoweringMatchers(metadata) {
12673
+ const matchers = [];
12674
+ for (const plugin of plugins) {
12675
+ const matcher = plugin.prepare(metadata);
12676
+ if (matcher) matchers.push(matcher);
12677
+ }
12678
+ return matchers;
12679
+ }
12680
+ function matchLoweringCall(callee, args2, metadata) {
12681
+ for (const matcher of prepareLoweringMatchers(metadata)) {
12682
+ const node = matcher(callee, args2);
12683
+ if (node) return node;
12684
+ }
12685
+ return null;
12686
+ }
12687
+ function isValidHelperId(helper) {
12688
+ return /^[A-Za-z_][A-Za-z0-9_]*$/.test(helper);
12689
+ }
12690
+ function __resetLoweringPluginsForTest(next = []) {
12691
+ plugins.length = 0;
12692
+ plugins.push(...next);
12693
+ }
12694
+ var plugins;
12695
+ var init_lowering_registry = __esm({
12696
+ "../jsx/src/lowering-registry.ts"() {
12697
+ "use strict";
12698
+ plugins = [];
12699
+ }
12700
+ });
12701
+
12183
12702
  // ../jsx/src/relocate.ts
12184
12703
  import ts13 from "typescript";
12185
12704
  function classify(name2, env) {
@@ -12213,6 +12732,14 @@ function decideAction(kind2, toScope, env, name2) {
12213
12732
  return { action: "lift-to-prop", rewrittenAs: `${PROPS_PARAM}.${name2}` };
12214
12733
  }
12215
12734
  if ((kind2 === "init-local" || kind2 === "sub-init-local") && toScope === "template") {
12735
+ const aliasTarget = env.aliasTargets?.get(name2);
12736
+ if (aliasTarget !== void 0) {
12737
+ const targetLeftmost = aliasTarget.includes(".") ? aliasTarget.split(".")[0] : aliasTarget;
12738
+ const targetKind = classify(targetLeftmost, env);
12739
+ if (isVisibleIn(toScope, targetKind)) {
12740
+ return { action: "inline", rewrittenAs: aliasTarget };
12741
+ }
12742
+ }
12216
12743
  const inlineForm = env.inlinable.get(name2);
12217
12744
  if (inlineForm !== void 0) {
12218
12745
  return { action: "inline", rewrittenAs: inlineForm };
@@ -12290,6 +12817,7 @@ function isInlinableInTemplate(value2, env) {
12290
12817
  return { ok: true, rewrittenValue: r2.text, decisions: r2.decisions };
12291
12818
  }
12292
12819
  function getCalleeIdentifierPath(callee) {
12820
+ if (ts13.isParenthesizedExpression(callee)) return getCalleeIdentifierPath(callee.expression);
12293
12821
  if (ts13.isIdentifier(callee)) return callee.text;
12294
12822
  if (ts13.isPropertyAccessExpression(callee)) {
12295
12823
  const left = getCalleeIdentifierPath(callee.expression);
@@ -12299,6 +12827,7 @@ function getCalleeIdentifierPath(callee) {
12299
12827
  return null;
12300
12828
  }
12301
12829
  function getCalleeLeftmostIdentifier(callee) {
12830
+ if (ts13.isParenthesizedExpression(callee)) return getCalleeLeftmostIdentifier(callee.expression);
12302
12831
  if (ts13.isIdentifier(callee)) return callee.text;
12303
12832
  if (ts13.isPropertyAccessExpression(callee)) {
12304
12833
  return getCalleeLeftmostIdentifier(callee.expression);
@@ -12306,17 +12835,35 @@ function getCalleeLeftmostIdentifier(callee) {
12306
12835
  return null;
12307
12836
  }
12308
12837
  function isCallAcceptedByAdapter(call, env) {
12309
- const name2 = getCalleeIdentifierPath(call.expression);
12310
- if (name2 === null) return false;
12838
+ const originalPath = getCalleeIdentifierPath(call.expression);
12839
+ if (originalPath === null) return false;
12311
12840
  const leftmost = getCalleeLeftmostIdentifier(call.expression);
12841
+ let resolvedPath = originalPath;
12842
+ let resolvedLeftmost = leftmost;
12312
12843
  if (leftmost !== null) {
12313
- const kind2 = env.bindings.get(leftmost);
12844
+ const aliasTarget = env.aliasTargets?.get(leftmost);
12845
+ if (aliasTarget !== void 0) {
12846
+ resolvedPath = originalPath === leftmost ? aliasTarget : `${aliasTarget}${originalPath.slice(leftmost.length)}`;
12847
+ resolvedLeftmost = aliasTarget.includes(".") ? aliasTarget.split(".")[0] : aliasTarget;
12848
+ }
12849
+ }
12850
+ if (resolvedLeftmost !== null) {
12851
+ const kind2 = env.bindings.get(resolvedLeftmost);
12314
12852
  if (kind2 !== void 0 && !REGISTRY_SAFE_BINDING_KINDS.has(kind2)) {
12315
12853
  return false;
12316
12854
  }
12317
12855
  }
12318
- if (env.templatePrimitives && env.templatePrimitives[name2]) return true;
12319
- if (env.acceptsTemplateCall && env.acceptsTemplateCall(name2)) return true;
12856
+ if (env.templatePrimitives && env.templatePrimitives[resolvedPath]) return true;
12857
+ if (env.acceptsTemplateCall && env.acceptsTemplateCall(resolvedPath)) return true;
12858
+ if (env.loweringMatchers && env.loweringMatchers.length > 0) {
12859
+ const parsed = tsNodeToParsedExpr(call);
12860
+ if (parsed.kind === "call") {
12861
+ const calleeForMatch = resolvedPath !== originalPath && !resolvedPath.includes(".") && parsed.callee.kind === "identifier" ? { kind: "identifier", name: resolvedPath } : parsed.callee;
12862
+ for (const matcher of env.loweringMatchers) {
12863
+ if (matcher(calleeForMatch, parsed.args)) return true;
12864
+ }
12865
+ }
12866
+ }
12320
12867
  return false;
12321
12868
  }
12322
12869
  function parseExpressionNode(text) {
@@ -12422,6 +12969,7 @@ function buildRelocateEnvFromIR(metadata, options2) {
12422
12969
  const env = buildRelocateEnvFromFields(metadata);
12423
12970
  if (options2?.templatePrimitives) env.templatePrimitives = options2.templatePrimitives;
12424
12971
  if (options2?.acceptsTemplateCall) env.acceptsTemplateCall = options2.acceptsTemplateCall;
12972
+ env.loweringMatchers = prepareLoweringMatchers(metadata);
12425
12973
  return env;
12426
12974
  }
12427
12975
  function buildRelocateEnvFromFields(src) {
@@ -12464,21 +13012,40 @@ function buildRelocateEnvFromFields(src) {
12464
13012
  for (const [name2, kind2] of bindings) {
12465
13013
  if (kind2 === "prop") propsForLift.add(name2);
12466
13014
  }
13015
+ const aliasTargets = /* @__PURE__ */ new Map();
13016
+ for (const c of src.localConstants) {
13017
+ const kind2 = bindings.get(c.name);
13018
+ if (kind2 !== "init-local" && kind2 !== "module-local") continue;
13019
+ const target2 = identifierPathFromParsed(c.parsed);
13020
+ if (target2 !== null) aliasTargets.set(c.name, target2);
13021
+ }
12467
13022
  return {
12468
13023
  bindings,
12469
13024
  inlinable: /* @__PURE__ */ new Map(),
12470
13025
  // populated by compute-inlinability after analyzer runs
12471
13026
  propsForLift,
12472
13027
  propsObjectName,
12473
- allowFallback: true
13028
+ allowFallback: true,
13029
+ aliasTargets
12474
13030
  };
12475
13031
  }
13032
+ function identifierPathFromParsed(expr) {
13033
+ if (!expr) return null;
13034
+ if (expr.kind === "identifier") return expr.name;
13035
+ if (expr.kind === "member" && !expr.computed) {
13036
+ const object = identifierPathFromParsed(expr.object);
13037
+ return object === null ? null : `${object}.${expr.property}`;
13038
+ }
13039
+ return null;
13040
+ }
12476
13041
  var REGISTRY_SAFE_BINDING_KINDS, RESERVED_WORDS;
12477
13042
  var init_relocate = __esm({
12478
13043
  "../jsx/src/relocate.ts"() {
12479
13044
  "use strict";
12480
13045
  init_types();
12481
13046
  init_utils();
13047
+ init_expression_parser();
13048
+ init_lowering_registry();
12482
13049
  REGISTRY_SAFE_BINDING_KINDS = /* @__PURE__ */ new Set([
12483
13050
  "global",
12484
13051
  "module-import",
@@ -12543,7 +13110,12 @@ function buildEnvFromCtx(ctx2) {
12543
13110
  effects: ctx2.effects,
12544
13111
  onMounts: ctx2.onMounts,
12545
13112
  initStatements: ctx2.initStatements,
12546
- imports: [],
13113
+ // Real component imports (#2069) — `buildRelocateEnvFromIR` calls
13114
+ // `prepareLoweringMatchers(metadata)` on this reconstructed object,
13115
+ // and plugin `prepare()` resolves local import names from
13116
+ // `metadata.imports`. `[]` here would silently disable every
13117
+ // import-aware LoweringPlugin for the client-JS inline-safety gate.
13118
+ imports: ctx2.imports,
12547
13119
  templateImports: [],
12548
13120
  namedExports: [],
12549
13121
  localFunctions: ctx2.localFunctions,
@@ -15431,6 +16003,15 @@ function emitTemplateCloneInline(template) {
15431
16003
  }
15432
16004
  return `const __tpl = document.createElement('template'); __tpl.innerHTML = \`${template}\`; return __tpl.content.firstElementChild.cloneNode(true)`;
15433
16005
  }
16006
+ function emitHoistedTemplateDecl(lines, indent, tplVar, skeletonTemplate) {
16007
+ const isSvg = templateRootIsSvg(skeletonTemplate);
16008
+ const html = isSvg ? `<svg>${skeletonTemplate}</svg>` : skeletonTemplate;
16009
+ lines.push(`${indent}const ${tplVar} = document.createElement('template')`);
16010
+ lines.push(`${indent}${tplVar}.innerHTML = \`${html}\``);
16011
+ }
16012
+ function hoistedCloneExpr(tplVar, skeletonTemplate) {
16013
+ return templateRootIsSvg(skeletonTemplate) ? `${tplVar}.content.firstElementChild.firstElementChild.cloneNode(true)` : `${tplVar}.content.firstElementChild.cloneNode(true)`;
16014
+ }
15434
16015
  function emitTemplateCloneLines(template, indent) {
15435
16016
  if (templateRootIsSvg(template)) {
15436
16017
  return [
@@ -15805,6 +16386,7 @@ function stringifyPlainLoop(lines, plan, topIndent = " ") {
15805
16386
  indexParam,
15806
16387
  mapPreambleWrapped,
15807
16388
  template,
16389
+ skeletonTemplate,
15808
16390
  reactiveEffects,
15809
16391
  childRefs,
15810
16392
  bodyIsMultiRoot,
@@ -15815,11 +16397,16 @@ function stringifyPlainLoop(lines, plan, topIndent = " ") {
15815
16397
  stringifyAnchoredLoop(lines, plan, topIndent, anchorKeyExpr);
15816
16398
  return;
15817
16399
  }
16400
+ const hoistedTpl = !bodyIsMultiRoot && skeletonTemplate ? skeletonTemplate : null;
16401
+ const tplVar = `__tpl_${markerId.replace(/[^A-Za-z0-9_$]/g, "_")}`;
16402
+ if (hoistedTpl) {
16403
+ emitHoistedTemplateDecl(lines, topIndent, tplVar, hoistedTpl);
16404
+ }
15818
16405
  const loopBfId = plan.profileLoopId ? `, ${JSON.stringify(plan.profileLoopId)}` : "";
15819
16406
  if (reactiveEffects === null && !bodyIsMultiRoot && childRefs.length === 0) {
15820
16407
  const unwrapInline = paramUnwrap ? `${paramUnwrap} ` : "";
15821
16408
  const preamble = mapPreambleWrapped ? `${mapPreambleWrapped}; ` : "";
15822
- const cloneExpr = emitTemplateCloneInline(template);
16409
+ const cloneExpr = hoistedTpl ? `return ${hoistedCloneExpr(tplVar, hoistedTpl)}` : emitTemplateCloneInline(template);
15823
16410
  lines.push(
15824
16411
  `${topIndent}mapArray(() => ${arrayExpr}, ${containerVar}, ${keyFn}, (${paramHead}, ${indexParam}, __existing) => { ${unwrapInline}${preamble}if (__existing) return __existing; ${cloneExpr} }, '${markerId}'${loopBfId})`
15825
16412
  );
@@ -15829,12 +16416,16 @@ function stringifyPlainLoop(lines, plan, topIndent = " ") {
15829
16416
  const bodyIndent = topIndent + " ";
15830
16417
  if (paramUnwrap) lines.push(`${bodyIndent}${paramUnwrap}`);
15831
16418
  if (mapPreambleWrapped) lines.push(`${bodyIndent}${mapPreambleWrapped}`);
15832
- emitLoopItemElementSetup(lines, {
15833
- template,
15834
- bodyIsMultiRoot,
15835
- indent: bodyIndent,
15836
- singleRootLayout: "inline"
15837
- });
16419
+ if (hoistedTpl) {
16420
+ lines.push(`${bodyIndent}const __el = __existing ?? ${hoistedCloneExpr(tplVar, hoistedTpl)}`);
16421
+ } else {
16422
+ emitLoopItemElementSetup(lines, {
16423
+ template,
16424
+ bodyIsMultiRoot,
16425
+ indent: bodyIndent,
16426
+ singleRootLayout: "inline"
16427
+ });
16428
+ }
15838
16429
  if (reactiveEffects !== null) {
15839
16430
  stringifyReactiveEffects(lines, reactiveEffects, { indent: bodyIndent, elVar: "__el", bodyIsMultiRoot });
15840
16431
  }
@@ -16582,6 +17173,7 @@ function buildPlainLoopPlan(elem, profileComponentName) {
16582
17173
  indexParam: elem.index || "__idx",
16583
17174
  mapPreambleWrapped: elem.mapPreamble ? wrap(elem.mapPreamble) : "",
16584
17175
  template: elem.template,
17176
+ skeletonTemplate: elem.skeletonTemplate,
16585
17177
  reactiveEffects: hasReactive2 ? buildLoopReactiveEffectsPlan(elem, profileComponentName) : null,
16586
17178
  childRefs: buildChildRefBindings(elem.bindings.refs, elem.param, elem.paramBindings),
16587
17179
  bodyIsMultiRoot: elem.bodyIsMultiRoot ?? false,
@@ -17292,6 +17884,7 @@ function createContext2(ir, scope, adapterCapabilities, profile) {
17292
17884
  initStatements: ir.metadata.initStatements ?? [],
17293
17885
  localFunctions: ir.metadata.localFunctions,
17294
17886
  localConstants: ir.metadata.localConstants,
17887
+ imports: ir.metadata.imports,
17295
17888
  propsParams: ir.metadata.propsParams,
17296
17889
  propsObjectName: ir.metadata.propsObjectName,
17297
17890
  restPropsName: ir.metadata.restPropsName,
@@ -17391,115 +17984,6 @@ var init_ir_to_client_js = __esm({
17391
17984
  }
17392
17985
  });
17393
17986
 
17394
- // ../jsx/src/module-exports.ts
17395
- function generateModuleExports(ir, extraInlineExported = /* @__PURE__ */ new Set(), rewriteRelativeImport) {
17396
- const lines = [];
17397
- for (const constant of ir.metadata.localConstants) {
17398
- if (!constant.isExported) continue;
17399
- const keyword = constant.declarationKind ?? "const";
17400
- if (!constant.value) {
17401
- lines.push(`export ${keyword} ${constant.name}`);
17402
- continue;
17403
- }
17404
- const value2 = constant.value.trim();
17405
- if (/^createContext\b/.test(value2) || /^new WeakMap\b/.test(value2)) continue;
17406
- lines.push(`export ${keyword} ${constant.name} = ${constant.value}`);
17407
- }
17408
- for (const func of ir.metadata.localFunctions) {
17409
- if (!func.isExported) continue;
17410
- const params = func.typedParams !== void 0 ? func.typedParams : func.params.map(formatParamWithType).join(", ");
17411
- const returnAnnotation = func.typedReturnType ? `: ${func.typedReturnType}` : "";
17412
- const body2 = func.typedBody ?? func.body;
17413
- const asyncKw = func.isAsync ? "async " : "";
17414
- lines.push(`export ${asyncKw}function ${func.name}(${params})${returnAnnotation} ${body2}`);
17415
- }
17416
- const inlineExported = collectInlineExportedNames(ir);
17417
- for (const name2 of extraInlineExported) inlineExported.add(name2);
17418
- for (const block of ir.metadata.namedExports) {
17419
- const isReexportFrom = block.source !== null;
17420
- const survivingSpecs = block.specifiers.filter((spec) => {
17421
- if (isReexportFrom) return true;
17422
- return !(inlineExported.has(spec.name) && spec.alias == null);
17423
- });
17424
- if (survivingSpecs.length === 0) continue;
17425
- const specText = survivingSpecs.map((s) => {
17426
- const prefix2 = s.isTypeOnly ? "type " : "";
17427
- return s.alias ? `${prefix2}${s.name} as ${s.alias}` : `${prefix2}${s.name}`;
17428
- }).join(", ");
17429
- const typeKw = block.isTypeOnly ? "type " : "";
17430
- if (isReexportFrom) {
17431
- const source = rewriteRelativeImport && block.source.startsWith(".") ? rewriteRelativeImport(block.source) : block.source;
17432
- lines.push(`export ${typeKw}{ ${specText} } from '${source}'`);
17433
- } else {
17434
- lines.push(`export ${typeKw}{ ${specText} }`);
17435
- }
17436
- }
17437
- return lines.length > 0 ? lines.join("\n") : null;
17438
- }
17439
- function collectInlineExportedNames(ir) {
17440
- const names = /* @__PURE__ */ new Set();
17441
- for (const c of ir.metadata.localConstants) {
17442
- if (c.isExported) names.add(c.name);
17443
- }
17444
- for (const f of ir.metadata.localFunctions) {
17445
- if (f.isExported) names.add(f.name);
17446
- }
17447
- if (ir.metadata.isExported && ir.metadata.componentName) {
17448
- names.add(ir.metadata.componentName);
17449
- }
17450
- return names;
17451
- }
17452
- function formatParamWithType(p) {
17453
- const rest2 = p.isRest ? "..." : "";
17454
- const optional = p.optional ? "?" : "";
17455
- const typeAnnotation = p.type?.raw && p.type.raw !== "unknown" ? `: ${p.type.raw}` : "";
17456
- const defaultPart = p.defaultValue !== void 0 ? ` = ${p.defaultValue}` : "";
17457
- return `${rest2}${p.name}${optional}${typeAnnotation}${defaultPart}`;
17458
- }
17459
- function findReachableNames(primaryRefs, declarations) {
17460
- const allNames = new Set(declarations.map((d) => d.name));
17461
- const bodyMap = new Map(declarations.map((d) => [d.name, d.body]));
17462
- const reachable = /* @__PURE__ */ new Set();
17463
- const queue = [];
17464
- for (const name2 of allNames) {
17465
- if (new RegExp(`\\b${name2}\\b`).test(primaryRefs)) {
17466
- reachable.add(name2);
17467
- queue.push(name2);
17468
- }
17469
- }
17470
- while (queue.length > 0) {
17471
- const current = queue.shift();
17472
- const body2 = bodyMap.get(current) || "";
17473
- for (const name2 of allNames) {
17474
- if (!reachable.has(name2) && new RegExp(`\\b${name2}\\b`).test(body2)) {
17475
- reachable.add(name2);
17476
- queue.push(name2);
17477
- }
17478
- }
17479
- }
17480
- return reachable;
17481
- }
17482
- function extractFunctionParams(value2) {
17483
- const arrowMatch = value2.match(/^(?:async\s*)?\(([^)]*)\)\s*(?::\s*[^=]+)?\s*=>/);
17484
- if (arrowMatch) {
17485
- return arrowMatch[1].split(",").map((p) => p.trim().split(":")[0].split("=")[0].trim()).filter(Boolean).join(", ");
17486
- }
17487
- const singleMatch = value2.match(/^(?:async\s*)?(\w+)\s*=>/);
17488
- if (singleMatch) {
17489
- return singleMatch[1];
17490
- }
17491
- const funcMatch = value2.match(/^(?:async\s*)?function\s*\w*\s*\(([^)]*)\)/);
17492
- if (funcMatch) {
17493
- return funcMatch[1].split(",").map((p) => p.trim().split(":")[0].split("=")[0].trim()).filter(Boolean).join(", ");
17494
- }
17495
- return "";
17496
- }
17497
- var init_module_exports = __esm({
17498
- "../jsx/src/module-exports.ts"() {
17499
- "use strict";
17500
- }
17501
- });
17502
-
17503
17987
  // ../jsx/src/css-layer-prefixer.ts
17504
17988
  function prefixClass(cls, layerName) {
17505
17989
  if (!cls || cls.startsWith("layer-")) return cls;
@@ -17962,15 +18446,13 @@ function extractSsrDefaults(metadata) {
17962
18446
  const propsLike = /* @__PURE__ */ new Set();
17963
18447
  if (metadata.propsObjectName) propsLike.add(metadata.propsObjectName);
17964
18448
  for (const p of metadata.propsParams) propsLike.add(p.name);
17965
- if (metadata.propsObjectName === null) {
17966
- for (const p of metadata.propsParams) {
17967
- if (p.isRest) continue;
17968
- if (p.defaultValue !== void 0) {
17969
- const value2 = tryStaticEval(p.defaultValue, { bindings: {}, propsLike });
17970
- out[p.name] = { propName: p.name, value: resultToJsonable(value2) };
17971
- } else {
17972
- out[p.name] = { propName: p.name, value: null };
17973
- }
18449
+ for (const p of metadata.propsParams) {
18450
+ if (p.isRest) continue;
18451
+ if (metadata.propsObjectName === null && p.defaultValue !== void 0) {
18452
+ const value2 = tryStaticEval(p.defaultValue, { bindings: {}, propsLike });
18453
+ out[p.name] = { propName: p.name, value: resultToJsonable(value2) };
18454
+ } else {
18455
+ out[p.name] = { propName: p.name, value: null };
17974
18456
  }
17975
18457
  }
17976
18458
  if (metadata.restPropsName) {
@@ -18252,9 +18734,11 @@ import ts17 from "typescript";
18252
18734
  function collectContextConsumers(metadata) {
18253
18735
  const constants = metadata.localConstants ?? [];
18254
18736
  const contextDefaults = /* @__PURE__ */ new Map();
18737
+ const contextDefaultKinds = /* @__PURE__ */ new Map();
18255
18738
  for (const c of constants) {
18256
18739
  if (c.systemConstructKind !== "createContext" || c.value === void 0) continue;
18257
18740
  contextDefaults.set(c.name, parseCreateContextDefault(c.value));
18741
+ if (isObjectLiteralCreateContextDefault(c.value)) contextDefaultKinds.set(c.name, "object");
18258
18742
  }
18259
18743
  if (contextDefaults.size === 0) return [];
18260
18744
  const consumers = [];
@@ -18265,7 +18749,8 @@ function collectContextConsumers(metadata) {
18265
18749
  consumers.push({
18266
18750
  localName: c.name,
18267
18751
  contextName: ctxName,
18268
- defaultValue: contextDefaults.get(ctxName) ?? null
18752
+ defaultValue: contextDefaults.get(ctxName) ?? null,
18753
+ defaultKind: contextDefaultKinds.get(ctxName)
18269
18754
  });
18270
18755
  }
18271
18756
  return consumers;
@@ -18289,6 +18774,12 @@ function parseCreateContextDefault(source) {
18289
18774
  if (arg.kind === ts17.SyntaxKind.FalseKeyword) return false;
18290
18775
  return null;
18291
18776
  }
18777
+ function isObjectLiteralCreateContextDefault(source) {
18778
+ const expr = parseSingleExpression(source);
18779
+ if (!expr || !ts17.isCallExpression(expr)) return false;
18780
+ if (expr.arguments.length === 0) return false;
18781
+ return ts17.isObjectLiteralExpression(expr.arguments[0]);
18782
+ }
18292
18783
  function parseSingleExpression(source) {
18293
18784
  const sf = ts17.createSourceFile("__ctx.ts", `(${source})`, ts17.ScriptTarget.Latest, false);
18294
18785
  const stmt = sf.statements[0];
@@ -18305,21 +18796,77 @@ function augmentInheritedPropAccesses(ir) {
18305
18796
  const booleanAttrProps = /* @__PURE__ */ new Set();
18306
18797
  const accessed = /* @__PURE__ */ new Set();
18307
18798
  const accessRe = new RegExp(`(?:^|[^\\w$.])${propsObj}\\.([A-Za-z_$][\\w$]*)`, "g");
18308
- const scan = (s) => {
18799
+ const scan = (s, also) => {
18309
18800
  if (!s) return;
18310
- for (const m of s.matchAll(accessRe)) accessed.add(m[1]);
18801
+ for (const m of s.matchAll(accessRe)) {
18802
+ accessed.add(m[1]);
18803
+ also?.add(m[1]);
18804
+ }
18805
+ };
18806
+ const coalesceLiteralTypes = /* @__PURE__ */ new Map();
18807
+ const pinCoalesceLiterals = (s) => {
18808
+ if (!s || !s.includes(propsObj)) return;
18809
+ const sf = ts17.createSourceFile("__aug.ts", `(${s})`, ts17.ScriptTarget.Latest, false);
18810
+ const visit3 = (n) => {
18811
+ if (ts17.isBinaryExpression(n) && (n.operatorToken.kind === ts17.SyntaxKind.QuestionQuestionToken || n.operatorToken.kind === ts17.SyntaxKind.BarBarToken)) {
18812
+ let left = n.left;
18813
+ while (ts17.isParenthesizedExpression(left)) left = left.expression;
18814
+ if (ts17.isPropertyAccessExpression(left) && ts17.isIdentifier(left.expression) && left.expression.text === propsObj) {
18815
+ const name2 = left.name.text;
18816
+ let right = n.right;
18817
+ while (ts17.isParenthesizedExpression(right)) right = right.expression;
18818
+ if (ts17.isPrefixUnaryExpression(right)) right = right.operand;
18819
+ const kind2 = ts17.isNumericLiteral(right) ? "number" : right.kind === ts17.SyntaxKind.TrueKeyword || right.kind === ts17.SyntaxKind.FalseKeyword ? "boolean" : ts17.isStringLiteralLike(right) ? "string" : null;
18820
+ if (kind2 && !coalesceLiteralTypes.has(name2)) coalesceLiteralTypes.set(name2, kind2);
18821
+ }
18822
+ }
18823
+ ts17.forEachChild(n, visit3);
18824
+ };
18825
+ visit3(sf);
18311
18826
  };
18312
- for (const memo of ir.metadata.memos) scan(memo.computation);
18313
- for (const signal2 of ir.metadata.signals) scan(signal2.initialValue);
18827
+ for (const memo of ir.metadata.memos) {
18828
+ scan(memo.computation);
18829
+ pinCoalesceLiterals(memo.computation);
18830
+ }
18831
+ for (const signal2 of ir.metadata.signals) {
18832
+ scan(signal2.initialValue);
18833
+ pinCoalesceLiterals(signal2.initialValue);
18834
+ }
18314
18835
  for (const stmt of ir.metadata.initStatements ?? []) scan(stmt.body);
18315
18836
  for (const eff of ir.metadata.effects ?? []) scan(eff.body);
18316
18837
  for (const c of ir.metadata.localConstants ?? []) {
18317
18838
  if (c.isModule) continue;
18318
18839
  scan(c.value);
18840
+ pinCoalesceLiterals(c.value);
18319
18841
  }
18320
18842
  const walk = (node) => {
18321
18843
  if (!node) return;
18844
+ const carrier = node;
18845
+ if (carrier.type === "expression") {
18846
+ scan(carrier.expr);
18847
+ pinCoalesceLiterals(carrier.expr);
18848
+ }
18849
+ scan(carrier.condition, bareRefProps);
18850
+ pinCoalesceLiterals(carrier.condition);
18851
+ scan(carrier.array, bareRefProps);
18322
18852
  const el = node;
18853
+ for (const prop of node.props ?? []) {
18854
+ const v = prop.value;
18855
+ if (v?.parts) {
18856
+ for (const part of v.parts) {
18857
+ if (part.type === "string") scan(part.value);
18858
+ else if (part.type === "ternary") {
18859
+ scan(part.condition);
18860
+ scan(part.whenTrue);
18861
+ scan(part.whenFalse);
18862
+ } else if (part.type === "lookup") scan(part.key);
18863
+ }
18864
+ }
18865
+ if (v?.kind === "expression" && typeof v.expr === "string") {
18866
+ scan(v.expr, bareRefProps);
18867
+ pinCoalesceLiterals(v.expr);
18868
+ }
18869
+ }
18323
18870
  for (const attr of el.attrs ?? []) {
18324
18871
  const v = attr.value;
18325
18872
  if (v?.parts) {
@@ -18360,9 +18907,10 @@ function augmentInheritedPropAccesses(ir) {
18360
18907
  if (existing.has(name2)) continue;
18361
18908
  let raw;
18362
18909
  if (booleanAttrProps.has(name2)) raw = "boolean";
18910
+ else if (coalesceLiteralTypes.has(name2)) raw = coalesceLiteralTypes.get(name2);
18363
18911
  else if (bareRefProps.has(name2)) raw = "unknown";
18364
18912
  else raw = "string";
18365
- const type2 = raw === "boolean" ? { kind: "primitive", raw: "boolean", primitive: "boolean" } : raw === "string" ? { kind: "primitive", raw: "string", primitive: "string" } : { kind: "unknown", raw: "unknown" };
18913
+ const type2 = raw === "unknown" ? { kind: "unknown", raw: "unknown" } : { kind: "primitive", raw, primitive: raw };
18366
18914
  ir.metadata.propsParams.push({ name: name2, type: type2, optional: true });
18367
18915
  existing.add(name2);
18368
18916
  }
@@ -18751,7 +19299,8 @@ function compileMultipleComponents(source, filePath, componentNames, options2) {
18751
19299
  files2.push({
18752
19300
  path: dir + output.componentName + adapter.extension,
18753
19301
  content: output.rawTemplate,
18754
- type: "markedTemplate"
19302
+ type: "markedTemplate",
19303
+ componentName: output.componentName
18755
19304
  });
18756
19305
  const ir = entries2.find((e) => e.componentIR.metadata.componentName === output.componentName);
18757
19306
  const ssrDefaults = ir ? extractSsrDefaults(ir.componentIR.metadata) : void 0;
@@ -18759,7 +19308,8 @@ function compileMultipleComponents(source, filePath, componentNames, options2) {
18759
19308
  files2.push({
18760
19309
  path: dir + output.componentName + ".ssr-defaults.json",
18761
19310
  content: JSON.stringify(ssrDefaults),
18762
- type: "ssrDefaults"
19311
+ type: "ssrDefaults",
19312
+ componentName: output.componentName
18763
19313
  });
18764
19314
  }
18765
19315
  }
@@ -19038,7 +19588,8 @@ function compileJSX(source, filePath, options2) {
19038
19588
  files2.push({
19039
19589
  path: filePath.replace(/\.tsx?$/, adapter.extension),
19040
19590
  content: content2,
19041
- type: "markedTemplate"
19591
+ type: "markedTemplate",
19592
+ componentName: componentIR.metadata.componentName
19042
19593
  });
19043
19594
  {
19044
19595
  const ssrDefaults = extractSsrDefaults(componentIR.metadata);
@@ -19046,7 +19597,8 @@ function compileJSX(source, filePath, options2) {
19046
19597
  files2.push({
19047
19598
  path: filePath.replace(/\.tsx?$/, ".ssr-defaults.json"),
19048
19599
  content: JSON.stringify(ssrDefaults),
19049
- type: "ssrDefaults"
19600
+ type: "ssrDefaults",
19601
+ componentName: componentIR.metadata.componentName
19050
19602
  });
19051
19603
  }
19052
19604
  }
@@ -19690,7 +20242,11 @@ function emitParsedExpr(expr, emitter) {
19690
20242
  return emitter.objectLiteral(expr.properties, expr.raw, emit);
19691
20243
  case "array-method":
19692
20244
  if (expr.method === "flat") {
19693
- return emitter.flatMethod(expr.object, expr.flatDepth, emit);
20245
+ return emitter.flatMethod(
20246
+ expr.object,
20247
+ expr.depthExpr ? { expr: expr.depthExpr } : expr.flatDepth,
20248
+ emit
20249
+ );
19694
20250
  }
19695
20251
  return emitter.arrayMethod(expr.method, expr.object, expr.args, emit);
19696
20252
  case "unsupported":
@@ -19775,42 +20331,6 @@ var init_query_href_lowering = __esm({
19775
20331
  }
19776
20332
  });
19777
20333
 
19778
- // ../jsx/src/lowering-registry.ts
19779
- function registerLoweringPlugin(plugin) {
19780
- const existing = plugins.findIndex((p) => p.name === plugin.name);
19781
- if (existing >= 0) plugins[existing] = plugin;
19782
- else plugins.push(plugin);
19783
- }
19784
- function getLoweringPlugins() {
19785
- return [...plugins];
19786
- }
19787
- function prepareLoweringMatchers(metadata) {
19788
- const matchers = [];
19789
- for (const plugin of plugins) {
19790
- const matcher = plugin.prepare(metadata);
19791
- if (matcher) matchers.push(matcher);
19792
- }
19793
- return matchers;
19794
- }
19795
- function matchLoweringCall(callee, args2, metadata) {
19796
- for (const matcher of prepareLoweringMatchers(metadata)) {
19797
- const node = matcher(callee, args2);
19798
- if (node) return node;
19799
- }
19800
- return null;
19801
- }
19802
- function __resetLoweringPluginsForTest(next = []) {
19803
- plugins.length = 0;
19804
- plugins.push(...next);
19805
- }
19806
- var plugins;
19807
- var init_lowering_registry = __esm({
19808
- "../jsx/src/lowering-registry.ts"() {
19809
- "use strict";
19810
- plugins = [];
19811
- }
19812
- });
19813
-
19814
20334
  // ../jsx/src/builtin-lowering-plugins.ts
19815
20335
  function registerBuiltinLoweringPlugins() {
19816
20336
  for (const plugin of BUILTIN_LOWERING_PLUGINS) registerLoweringPlugin(plugin);
@@ -20040,7 +20560,7 @@ var init_import_map = __esm({
20040
20560
  });
20041
20561
 
20042
20562
  // ../jsx/src/loop-destructure.ts
20043
- function isLowerableObjectRestDestructure(loop) {
20563
+ function isLowerableLoopDestructure(loop) {
20044
20564
  const bindings = loop.paramBindings;
20045
20565
  if (!bindings || bindings.length === 0) return false;
20046
20566
  if (loop.filterPredicate) return false;
@@ -20049,16 +20569,17 @@ function isLowerableObjectRestDestructure(loop) {
20049
20569
  }
20050
20570
  for (const b of bindings) {
20051
20571
  if (b.rest) {
20052
- if (b.rest.kind !== "object") return false;
20053
- } else if (!SIMPLE_FIELD.test(b.path)) {
20572
+ if (!b.segments) return false;
20573
+ } else if (!b.segments || b.segments.length === 0) {
20054
20574
  return false;
20055
20575
  }
20056
20576
  }
20057
- const restNames = bindings.filter((b) => b.rest).map((b) => b.name);
20058
- if (restNames.length === 0) return true;
20059
- return !restNamesMisused(loop, restNames);
20577
+ const objectRestNames = bindings.filter((b) => b.rest?.kind === "object").map((b) => b.name);
20578
+ if (objectRestNames.length === 0) return true;
20579
+ return !restNamesMisused(loop, objectRestNames);
20060
20580
  }
20061
20581
  function restNamesMisused(loop, names) {
20582
+ const nameSet = new Set(names);
20062
20583
  const valueUse = names.map(
20063
20584
  (n) => new RegExp(`(?<![\\w.$])${escapeRe(n)}(?!\\s*\\??\\.)(?![\\w$])`)
20064
20585
  );
@@ -20072,7 +20593,10 @@ function restNamesMisused(loop, names) {
20072
20593
  }
20073
20594
  }
20074
20595
  };
20075
- const attr = (v) => {
20596
+ const attr = (v, isIntrinsicElementAttrs) => {
20597
+ if (v.kind === "spread" && isIntrinsicElementAttrs && nameSet.has(v.expr.trim())) {
20598
+ return;
20599
+ }
20076
20600
  if (v.kind === "expression" || v.kind === "spread") {
20077
20601
  check(v.expr);
20078
20602
  check(v.templateExpr);
@@ -20129,16 +20653,16 @@ function restNamesMisused(loop, names) {
20129
20653
  if (node.alternate) visit3(node.alternate);
20130
20654
  break;
20131
20655
  case "element":
20132
- node.attrs.forEach((a) => attr(a.value));
20656
+ node.attrs.forEach((a) => attr(a.value, true));
20133
20657
  node.events.forEach((e) => check(e.handler));
20134
20658
  node.children.forEach(visit3);
20135
20659
  break;
20136
20660
  case "component":
20137
- node.props.forEach((p) => attr(p.value));
20661
+ node.props.forEach((p) => attr(p.value, false));
20138
20662
  node.children.forEach(visit3);
20139
20663
  break;
20140
20664
  case "provider":
20141
- attr(node.valueProp.value);
20665
+ attr(node.valueProp.value, false);
20142
20666
  node.children.forEach(visit3);
20143
20667
  break;
20144
20668
  case "fragment":
@@ -20162,11 +20686,11 @@ function restNamesMisused(loop, names) {
20162
20686
  function escapeRe(s) {
20163
20687
  return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
20164
20688
  }
20165
- var SIMPLE_FIELD;
20689
+ var isLowerableObjectRestDestructure;
20166
20690
  var init_loop_destructure = __esm({
20167
20691
  "../jsx/src/loop-destructure.ts"() {
20168
20692
  "use strict";
20169
- SIMPLE_FIELD = /^\.[A-Za-z_$][\w$]*$/;
20693
+ isLowerableObjectRestDestructure = isLowerableLoopDestructure;
20170
20694
  }
20171
20695
  });
20172
20696
 
@@ -22779,8 +23303,10 @@ __export(src_exports, {
22779
23303
  identifierPath: () => identifierPath,
22780
23304
  importsSearchParams: () => importsSearchParams,
22781
23305
  isBooleanAttr: () => isBooleanAttr,
23306
+ isLowerableLoopDestructure: () => isLowerableLoopDestructure,
22782
23307
  isLowerableObjectRestDestructure: () => isLowerableObjectRestDestructure,
22783
23308
  isSupported: () => isSupported,
23309
+ isValidHelperId: () => isValidHelperId,
22784
23310
  joinProfilerEvents: () => joinProfilerEvents,
22785
23311
  jsxToIR: () => jsxToIR,
22786
23312
  listComponentFunctions: () => listComponentFunctions,
@@ -23721,12 +24247,143 @@ var init_assets_ignore = __esm({
23721
24247
  }
23722
24248
  });
23723
24249
 
23724
- // src/lib/build.ts
24250
+ // src/lib/runtime-treeshake.ts
23725
24251
  import ts23 from "typescript";
24252
+ import { basename, dirname as dirname3 } from "node:path";
24253
+ import { build as esbuildBuild } from "esbuild";
24254
+ function isBarefootClientSpecifier(spec) {
24255
+ return spec === "@barefootjs/client" || spec.startsWith("@barefootjs/client/");
24256
+ }
24257
+ function emptyCollection() {
24258
+ return { names: /* @__PURE__ */ new Set(), unsafe: false, reasons: [] };
24259
+ }
24260
+ function collectUsedRuntimeExports(code, sourceLabel = "<input>") {
24261
+ const result2 = emptyCollection();
24262
+ if (!code.includes("@barefootjs/client")) return result2;
24263
+ let sourceFile;
24264
+ try {
24265
+ sourceFile = ts23.createSourceFile(
24266
+ sourceLabel,
24267
+ code,
24268
+ ts23.ScriptTarget.Latest,
24269
+ /*setParentNodes*/
24270
+ false,
24271
+ ts23.ScriptKind.JS
24272
+ );
24273
+ } catch (err) {
24274
+ result2.unsafe = true;
24275
+ result2.reasons.push(`failed to parse ${sourceLabel}: ${err.message}`);
24276
+ return result2;
24277
+ }
24278
+ const visit3 = (node) => {
24279
+ if (ts23.isImportDeclaration(node)) {
24280
+ const spec = node.moduleSpecifier;
24281
+ if (ts23.isStringLiteral(spec) && isBarefootClientSpecifier(spec.text)) {
24282
+ const clause = node.importClause;
24283
+ if (!clause) {
24284
+ } else if (clause.isTypeOnly) {
24285
+ } else if (clause.namedBindings && ts23.isNamedImports(clause.namedBindings)) {
24286
+ for (const el of clause.namedBindings.elements) {
24287
+ if (el.isTypeOnly) continue;
24288
+ const imported = (el.propertyName ?? el.name).text;
24289
+ result2.names.add(imported);
24290
+ }
24291
+ if (clause.name) {
24292
+ result2.unsafe = true;
24293
+ result2.reasons.push(`default import of "${spec.text}" in ${sourceLabel}`);
24294
+ }
24295
+ } else if (clause.namedBindings && ts23.isNamespaceImport(clause.namedBindings)) {
24296
+ result2.unsafe = true;
24297
+ result2.reasons.push(`namespace import (* as ${clause.namedBindings.name.text}) of "${spec.text}" in ${sourceLabel}`);
24298
+ } else if (clause.name) {
24299
+ result2.unsafe = true;
24300
+ result2.reasons.push(`default import of "${spec.text}" in ${sourceLabel}`);
24301
+ }
24302
+ }
24303
+ } else if (ts23.isCallExpression(node) && node.expression.kind === ts23.SyntaxKind.ImportKeyword) {
24304
+ const arg = node.arguments[0];
24305
+ if (arg && ts23.isStringLiteral(arg) && isBarefootClientSpecifier(arg.text)) {
24306
+ result2.unsafe = true;
24307
+ result2.reasons.push(`dynamic import("${arg.text}") in ${sourceLabel}`);
24308
+ }
24309
+ }
24310
+ ts23.forEachChild(node, visit3);
24311
+ };
24312
+ visit3(sourceFile);
24313
+ return result2;
24314
+ }
24315
+ function mergeRuntimeImportCollections(collections) {
24316
+ const merged = emptyCollection();
24317
+ for (const c of collections) {
24318
+ for (const n of c.names) merged.names.add(n);
24319
+ if (c.unsafe) {
24320
+ merged.unsafe = true;
24321
+ merged.reasons.push(...c.reasons);
24322
+ }
24323
+ }
24324
+ return merged;
24325
+ }
24326
+ async function buildRuntimeBundle(opts) {
24327
+ const { entrySource, keepNames, minify } = opts;
24328
+ const sorted = [...new Set(keepNames)].sort();
24329
+ if (sorted.length === 0) {
24330
+ throw new Error("buildRuntimeBundle: keepNames is empty");
24331
+ }
24332
+ const entryContents = `export { ${sorted.join(", ")} } from ${JSON.stringify(`./${basename(entrySource)}`)}
24333
+ `;
24334
+ const result2 = await esbuildBuild({
24335
+ stdin: {
24336
+ contents: entryContents,
24337
+ resolveDir: dirname3(entrySource),
24338
+ sourcefile: "bf-runtime-entry.mjs",
24339
+ loader: "js"
24340
+ },
24341
+ format: "esm",
24342
+ bundle: true,
24343
+ // Unlike `transpile()`'s policy for per-component client JS
24344
+ // (packages/cli/src/lib/runtime.ts — identifiers preserved there so
24345
+ // e.g. `hydrate('ComponentName', ...)` call-site names and combine.ts's
24346
+ // cross-file lookups stay intact), `barefoot.js` is a self-contained
24347
+ // leaf artifact loaded only via the importmap: nothing parses its
24348
+ // source for internal identifier names, and esbuild always keeps
24349
+ // *exported* binding names stable regardless of `minifyIdentifiers`
24350
+ // (verified: a re-export entry's `export { keepMe } from '...'` still
24351
+ // exports as `keepMe` even when the internal implementation is renamed).
24352
+ // So full minification is safe here and meaningfully smaller.
24353
+ minify,
24354
+ treeShaking: true,
24355
+ platform: "browser",
24356
+ write: false
24357
+ });
24358
+ const out = result2.outputFiles?.[0];
24359
+ if (!out) {
24360
+ throw new Error("esbuild produced no output for the runtime bundle");
24361
+ }
24362
+ return out.text;
24363
+ }
24364
+ var ALWAYS_KEEP_RUNTIME_EXPORTS;
24365
+ var init_runtime_treeshake = __esm({
24366
+ "src/lib/runtime-treeshake.ts"() {
24367
+ "use strict";
24368
+ ALWAYS_KEEP_RUNTIME_EXPORTS = [
24369
+ "render",
24370
+ "hydrate",
24371
+ "flushHydration",
24372
+ "rehydrateAll",
24373
+ "rehydrateScope",
24374
+ "disposeScope",
24375
+ "setupStreaming",
24376
+ "createSearchParams"
24377
+ ];
24378
+ }
24379
+ });
24380
+
24381
+ // src/lib/build.ts
24382
+ import ts24 from "typescript";
23726
24383
  import { mkdir, readdir, stat, unlink } from "node:fs/promises";
23727
- import { resolve as resolve6, basename, relative as relative2, dirname as dirname3, isAbsolute as isAbsolute2 } from "node:path";
24384
+ import { resolve as resolve6, basename as basename2, relative as relative2, dirname as dirname4, isAbsolute as isAbsolute2 } from "node:path";
23728
24385
  import { fileURLToPath as fileURLToPath2 } from "node:url";
23729
- import { build as esbuildBuild } from "esbuild";
24386
+ import { build as esbuildBuild2 } from "esbuild";
23730
24387
  function detectMissingUseClient(content2) {
23731
24388
  const importRe = /import\s*(?:type\s+)?\{([^}]+)\}\s*from\s*['"]@barefootjs\/client['"]/g;
23732
24389
  const hits = /* @__PURE__ */ new Set();
@@ -23802,11 +24459,13 @@ function resolveBuildConfigFromTs(projectDir, tsConfig, overrides) {
23802
24459
  outfile: e.outfile,
23803
24460
  externals: e.externals
23804
24461
  })),
23805
- localImportPrefixes: tsConfig.localImportPrefixes
24462
+ localImportPrefixes: tsConfig.localImportPrefixes,
24463
+ runtimeBundle: tsConfig.runtimeBundle,
24464
+ runtimeKeep: tsConfig.runtimeKeep
23806
24465
  };
23807
24466
  }
23808
24467
  async function findCliPackageJson() {
23809
- const here = dirname3(fileURLToPath2(import.meta.url));
24468
+ const here = dirname4(fileURLToPath2(import.meta.url));
23810
24469
  const candidates = [
23811
24470
  resolve6(here, "../package.json"),
23812
24471
  // bundled dist/index.js
@@ -23825,7 +24484,7 @@ async function findNearestLockfile(projectDir) {
23825
24484
  const candidate = resolve6(dir, name2);
23826
24485
  if (await fileExists(candidate)) return candidate;
23827
24486
  }
23828
- const parent2 = dirname3(dir);
24487
+ const parent2 = dirname4(dir);
23829
24488
  if (parent2 === dir) return null;
23830
24489
  dir = parent2;
23831
24490
  }
@@ -23856,7 +24515,7 @@ async function computeGlobalHash(config) {
23856
24515
  const lockfile = await findNearestLockfile(config.projectDir);
23857
24516
  if (lockfile) {
23858
24517
  const bytes = await readBytes(lockfile);
23859
- parts.push(`lockfile:${basename(lockfile)}:${hashBytes(bytes)}`);
24518
+ parts.push(`lockfile:${basename2(lockfile)}:${hashBytes(bytes)}`);
23860
24519
  }
23861
24520
  return hashContent(parts.join("\0"));
23862
24521
  }
@@ -23900,27 +24559,29 @@ async function build(config, options2 = {}) {
23900
24559
  } catch {
23901
24560
  }
23902
24561
  }
23903
- if (domDistFile) {
23904
- const runtimeOutPath = resolve6(runtimeOutDir, "barefoot.js");
23905
- let runtimeContent;
23906
- if (config.minify) {
23907
- runtimeContent = transpile(await readText(domDistFile), { loader: "js", minify: true });
24562
+ const runtimeMode = config.runtimeBundle ?? "treeshake";
24563
+ if (runtimeMode === "full") {
24564
+ if (domDistFile) {
24565
+ const runtimeOutPath = resolve6(runtimeOutDir, "barefoot.js");
24566
+ let runtimeContent;
24567
+ if (config.minify) {
24568
+ runtimeContent = transpile(await readText(domDistFile), { loader: "js", minify: true });
24569
+ } else {
24570
+ runtimeContent = await readBytes(domDistFile);
24571
+ }
24572
+ const wrote = await writeIfChanged(runtimeOutPath, runtimeContent);
24573
+ if (wrote) {
24574
+ anyOutputChanged = true;
24575
+ console.log(`Generated: ${runtimeSubdir}/barefoot.js`);
24576
+ }
23908
24577
  } else {
23909
- runtimeContent = await readBytes(domDistFile);
24578
+ console.warn("Warning: @barefootjs/client dist not found. Skipping barefoot.js copy.");
23910
24579
  }
23911
- const wrote = await writeIfChanged(runtimeOutPath, runtimeContent);
23912
- if (wrote) {
23913
- anyOutputChanged = true;
23914
- console.log(`Generated: ${runtimeSubdir}/barefoot.js`);
23915
- }
23916
- } else {
23917
- console.warn("Warning: @barefootjs/client dist not found. Skipping barefoot.js copy.");
23918
24580
  }
23919
- const { changed: externalsChanged, allExternals } = await processExternals(config, runtimeSubdir, runtimeOutDir);
24581
+ const { changed: externalsChanged, allExternals, outfiles: externalOutfiles } = await processExternals(config, runtimeSubdir, runtimeOutDir);
23920
24582
  if (externalsChanged) anyOutputChanged = true;
23921
- if (await processBundleEntries(config, clientJsOutDir, clientJsSubdir, allExternals, cache2, nextEntries, force)) {
23922
- anyOutputChanged = true;
23923
- }
24583
+ const { changed: bundleEntriesChanged } = await processBundleEntries(config, clientJsOutDir, clientJsSubdir, allExternals, cache2, nextEntries, force);
24584
+ if (bundleEntriesChanged) anyOutputChanged = true;
23924
24585
  const allFiles = [];
23925
24586
  for (const dir of config.componentDirs) {
23926
24587
  allFiles.push(...await discoverComponentFiles(dir));
@@ -23977,6 +24638,12 @@ async function build(config, options2 = {}) {
23977
24638
  }
23978
24639
  return sharedProgram;
23979
24640
  };
24641
+ let childShapesRegistered = false;
24642
+ const ensureChildShapesRegistered = () => {
24643
+ if (childShapesRegistered) return;
24644
+ childShapesRegistered = true;
24645
+ registerAdapterChildShapes(config.adapter, allFiles, sourceContents, getSharedProgram());
24646
+ };
23980
24647
  for (const entryPath of allFiles) {
23981
24648
  const sourceContent = sourceContents.get(entryPath);
23982
24649
  if (!hasUseClientDirective2(sourceContent)) {
@@ -24007,6 +24674,7 @@ async function build(config, options2 = {}) {
24007
24674
  cachedCount++;
24008
24675
  continue;
24009
24676
  }
24677
+ ensureChildShapesRegistered();
24010
24678
  const result2 = await compileEntry({
24011
24679
  entryPath,
24012
24680
  sourceContent,
@@ -24129,7 +24797,7 @@ async function build(config, options2 = {}) {
24129
24797
  const sourceDirsByManifestKey = {};
24130
24798
  for (const [sourcePath, entry] of Object.entries(nextEntries)) {
24131
24799
  if (entry.manifestKey && !sourcePath.startsWith(BUNDLE_KEY_PREFIX)) {
24132
- sourceDirsByManifestKey[entry.manifestKey] = [dirname3(sourcePath)];
24800
+ sourceDirsByManifestKey[entry.manifestKey] = [dirname4(sourcePath)];
24133
24801
  }
24134
24802
  }
24135
24803
  const { errors: resolveErrors, stubDepsByManifestKey } = await resolveRelativeImports({
@@ -24171,12 +24839,90 @@ async function build(config, options2 = {}) {
24171
24839
  if (cacheEntry?.manifestEntry) delete cacheEntry.manifestEntry.stubDeps;
24172
24840
  }
24173
24841
  }
24842
+ let runtimeKeepHash = cache2.runtimeKeepHash;
24843
+ if (runtimeMode === "treeshake") {
24844
+ if (!domDistFile) {
24845
+ console.warn("Warning: @barefootjs/client dist not found. Skipping barefoot.js generation.");
24846
+ runtimeKeepHash = void 0;
24847
+ } else {
24848
+ const scanTargets = [...externalOutfiles];
24849
+ for (const entry of Object.values(nextEntries)) {
24850
+ for (const rel of entry.outputs) {
24851
+ if (rel.endsWith(".js")) scanTargets.push(resolve6(config.outDir, rel));
24852
+ }
24853
+ }
24854
+ const collections = [];
24855
+ for (const filePath of scanTargets) {
24856
+ try {
24857
+ collections.push(collectUsedRuntimeExports(await readText(filePath), relative2(config.outDir, filePath)));
24858
+ } catch {
24859
+ }
24860
+ }
24861
+ const merged = mergeRuntimeImportCollections(collections);
24862
+ const runtimeOutPath = resolve6(runtimeOutDir, "barefoot.js");
24863
+ if (merged.unsafe) {
24864
+ for (const reason2 of merged.reasons) {
24865
+ console.warn(`Warning: runtime tree-shake \u2014 falling back to full runtime copy (${reason2})`);
24866
+ }
24867
+ let runtimeContent;
24868
+ if (config.minify) {
24869
+ runtimeContent = transpile(await readText(domDistFile), { loader: "js", minify: true });
24870
+ } else {
24871
+ runtimeContent = await readBytes(domDistFile);
24872
+ }
24873
+ if (await writeIfChanged(runtimeOutPath, runtimeContent)) {
24874
+ anyOutputChanged = true;
24875
+ console.log(`Generated: ${runtimeSubdir}/barefoot.js (full copy \u2014 see warning above)`);
24876
+ }
24877
+ runtimeKeepHash = void 0;
24878
+ } else {
24879
+ const keepNames = /* @__PURE__ */ new Set([
24880
+ ...ALWAYS_KEEP_RUNTIME_EXPORTS,
24881
+ ...config.runtimeKeep ?? [],
24882
+ ...merged.names
24883
+ ]);
24884
+ const distBytes = await readBytes(domDistFile);
24885
+ const nextKeepHash = hashContent(JSON.stringify({
24886
+ mode: runtimeMode,
24887
+ minify: config.minify,
24888
+ distHash: hashBytes(distBytes),
24889
+ keep: [...keepNames].sort()
24890
+ }));
24891
+ if (nextKeepHash === cache2.runtimeKeepHash && await fileExists(runtimeOutPath)) {
24892
+ runtimeKeepHash = nextKeepHash;
24893
+ } else {
24894
+ try {
24895
+ const bundled = await buildRuntimeBundle({
24896
+ entrySource: domDistFile,
24897
+ keepNames,
24898
+ minify: config.minify
24899
+ });
24900
+ if (await writeIfChanged(runtimeOutPath, bundled)) {
24901
+ anyOutputChanged = true;
24902
+ console.log(`Generated: ${runtimeSubdir}/barefoot.js (tree-shaken: ${keepNames.size} exports kept)`);
24903
+ }
24904
+ runtimeKeepHash = nextKeepHash;
24905
+ } catch (err) {
24906
+ console.warn(
24907
+ `Warning: runtime tree-shake bundling failed (${err.message}); falling back to full runtime copy.`
24908
+ );
24909
+ const runtimeContent = config.minify ? transpile(new TextDecoder().decode(distBytes), { loader: "js", minify: true }) : distBytes;
24910
+ if (await writeIfChanged(runtimeOutPath, runtimeContent)) {
24911
+ anyOutputChanged = true;
24912
+ console.log(`Generated: ${runtimeSubdir}/barefoot.js (full copy \u2014 see warning above)`);
24913
+ }
24914
+ runtimeKeepHash = void 0;
24915
+ }
24916
+ }
24917
+ }
24918
+ }
24919
+ }
24174
24920
  {
24175
24921
  const runtimeAbs = resolve6(config.outDir, runtimeSubdir, "barefoot.js");
24176
24922
  for (const [name2, entry] of Object.entries(manifest)) {
24177
24923
  if (!entry.clientJs || name2 === "__barefoot__") continue;
24178
24924
  const filePath = resolve6(config.outDir, entry.clientJs);
24179
- let rel = relative2(dirname3(filePath), runtimeAbs);
24925
+ let rel = relative2(dirname4(filePath), runtimeAbs);
24180
24926
  if (!rel.startsWith(".")) rel = "./" + rel;
24181
24927
  try {
24182
24928
  let content2 = await readText(filePath);
@@ -24228,7 +24974,8 @@ async function build(config, options2 = {}) {
24228
24974
  }
24229
24975
  const nextCache = {
24230
24976
  globalHash,
24231
- entries: nextEntries
24977
+ entries: nextEntries,
24978
+ runtimeKeepHash
24232
24979
  };
24233
24980
  const nextLedger = emptyLedger();
24234
24981
  for (const [key, entry] of Object.entries(nextEntries)) {
@@ -24270,7 +25017,7 @@ async function build(config, options2 = {}) {
24270
25017
  };
24271
25018
  }
24272
25019
  function extractBareImports(code) {
24273
- const { importedFiles } = ts23.preProcessFile(code, true, true);
25020
+ const { importedFiles } = ts24.preProcessFile(code, true, true);
24274
25021
  const specifiers = /* @__PURE__ */ new Set();
24275
25022
  for (const { fileName } of importedFiles) {
24276
25023
  if (!fileName.startsWith(".") && !fileName.startsWith("/") && !fileName.includes("://")) {
@@ -24296,7 +25043,7 @@ function vendorChunkFilename(pkgName) {
24296
25043
  return `${base}.js`;
24297
25044
  }
24298
25045
  function effectiveNamesFor(entryPath, componentDirs) {
24299
- const bn = basename(entryPath);
25046
+ const bn = basename2(entryPath);
24300
25047
  if (componentDirs && componentDirs.length > 0) {
24301
25048
  for (const dir of componentDirs) {
24302
25049
  const root2 = resolve6(dir);
@@ -24311,8 +25058,8 @@ function effectiveNamesFor(entryPath, componentDirs) {
24311
25058
  return { baseFileName: bn, baseNameNoExt: noExt };
24312
25059
  }
24313
25060
  function buildRelativeImportRewriter(sourcePath, outputPath, componentDirs, templatesOutDir) {
24314
- const sourceDir = dirname3(sourcePath);
24315
- const outputDir = dirname3(outputPath);
25061
+ const sourceDir = dirname4(sourcePath);
25062
+ const outputDir = dirname4(outputPath);
24316
25063
  const resolvedComponentDirs = componentDirs.map((d) => resolve6(d));
24317
25064
  return (importPath) => {
24318
25065
  const srcAbs = resolve6(sourceDir, importPath);
@@ -24331,22 +25078,22 @@ function buildRelativeImportRewriter(sourcePath, outputPath, componentDirs, temp
24331
25078
  };
24332
25079
  }
24333
25080
  function effectiveOutName(tplPath, entryBaseNoExt) {
24334
- const bn = basename(tplPath);
25081
+ const bn = basename2(tplPath);
24335
25082
  const entryDir = entryBaseNoExt.includes("/") ? entryBaseNoExt.slice(0, entryBaseNoExt.lastIndexOf("/")) : "";
24336
25083
  return entryDir ? `${entryDir}/${bn}` : bn;
24337
25084
  }
24338
25085
  function topLevelImportLines(content2) {
24339
25086
  const lines = /* @__PURE__ */ new Set();
24340
- const sourceFile = ts23.createSourceFile(
25087
+ const sourceFile = ts24.createSourceFile(
24341
25088
  "merge.js",
24342
25089
  content2,
24343
- ts23.ScriptTarget.Latest,
25090
+ ts24.ScriptTarget.Latest,
24344
25091
  /*setParentNodes*/
24345
25092
  true,
24346
- ts23.ScriptKind.JS
25093
+ ts24.ScriptKind.JS
24347
25094
  );
24348
25095
  for (const stmt of sourceFile.statements) {
24349
- if (ts23.isImportDeclaration(stmt)) {
25096
+ if (ts24.isImportDeclaration(stmt)) {
24350
25097
  const { line } = sourceFile.getLineAndCharacterOfPosition(stmt.getStart(sourceFile));
24351
25098
  lines.add(line);
24352
25099
  }
@@ -24355,29 +25102,29 @@ function topLevelImportLines(content2) {
24355
25102
  }
24356
25103
  function rewriteBarefootClientSpecifiers(content2, rel) {
24357
25104
  if (!content2.includes("@barefootjs/client")) return content2;
24358
- const sourceFile = ts23.createSourceFile(
25105
+ const sourceFile = ts24.createSourceFile(
24359
25106
  "client.js",
24360
25107
  content2,
24361
- ts23.ScriptTarget.Latest,
25108
+ ts24.ScriptTarget.Latest,
24362
25109
  /*setParentNodes*/
24363
25110
  true,
24364
- ts23.ScriptKind.JS
25111
+ ts24.ScriptKind.JS
24365
25112
  );
24366
25113
  const isBarefootClient = (s) => s === "@barefootjs/client" || s.startsWith("@barefootjs/client/");
24367
25114
  const spans = [];
24368
25115
  const visit3 = (node) => {
24369
- if (ts23.isImportDeclaration(node) || ts23.isExportDeclaration(node)) {
25116
+ if (ts24.isImportDeclaration(node) || ts24.isExportDeclaration(node)) {
24370
25117
  const ms = node.moduleSpecifier;
24371
- if (ms && ts23.isStringLiteral(ms) && isBarefootClient(ms.text)) {
25118
+ if (ms && ts24.isStringLiteral(ms) && isBarefootClient(ms.text)) {
24372
25119
  spans.push([ms.getStart(sourceFile), ms.getEnd()]);
24373
25120
  }
24374
- } else if (ts23.isCallExpression(node) && node.expression.kind === ts23.SyntaxKind.ImportKeyword) {
25121
+ } else if (ts24.isCallExpression(node) && node.expression.kind === ts24.SyntaxKind.ImportKeyword) {
24375
25122
  const arg = node.arguments[0];
24376
- if (arg && ts23.isStringLiteral(arg) && isBarefootClient(arg.text)) {
25123
+ if (arg && ts24.isStringLiteral(arg) && isBarefootClient(arg.text)) {
24377
25124
  spans.push([arg.getStart(sourceFile), arg.getEnd()]);
24378
25125
  }
24379
25126
  }
24380
- ts23.forEachChild(node, visit3);
25127
+ ts24.forEachChild(node, visit3);
24381
25128
  };
24382
25129
  visit3(sourceFile);
24383
25130
  if (spans.length === 0) return content2;
@@ -24465,11 +25212,14 @@ async function resolvePkgBrowserEntry(pkgDir) {
24465
25212
  return null;
24466
25213
  }
24467
25214
  async function processExternals(config, runtimeSubdir, runtimeOutDir) {
24468
- if (!config.externals || Object.keys(config.externals).length === 0) return { changed: false, allExternals: [] };
25215
+ if (!config.externals || Object.keys(config.externals).length === 0) {
25216
+ return { changed: false, allExternals: [], outfiles: [] };
25217
+ }
24469
25218
  const basePath = config.externalsBasePath ?? `/${runtimeSubdir}/`;
24470
25219
  const base = basePath.endsWith("/") ? basePath : basePath + "/";
24471
25220
  const imports = {};
24472
25221
  const preloads2 = [];
25222
+ const outfiles = [];
24473
25223
  let anyChanged = false;
24474
25224
  for (const [pkgName, spec] of Object.entries(config.externals)) {
24475
25225
  const isChunk = spec === true || typeof spec === "object" && !("url" in spec);
@@ -24492,8 +25242,9 @@ async function processExternals(config, runtimeSubdir, runtimeOutDir) {
24492
25242
  const srcFile = entry.path;
24493
25243
  const filename = vendorChunkFilename(pkgName);
24494
25244
  const destPath = resolve6(runtimeOutDir, filename);
25245
+ outfiles.push(destPath);
24495
25246
  if (wantRebundle) {
24496
- await esbuildBuild({
25247
+ await esbuildBuild2({
24497
25248
  entryPoints: [srcFile],
24498
25249
  outfile: destPath,
24499
25250
  format: "esm",
@@ -24553,16 +25304,18 @@ async function processExternals(config, runtimeSubdir, runtimeOutDir) {
24553
25304
  console.log("Generated: barefoot-importmap.html");
24554
25305
  }
24555
25306
  }
24556
- return { changed: anyChanged, allExternals };
25307
+ return { changed: anyChanged, allExternals, outfiles };
24557
25308
  }
24558
25309
  async function processBundleEntries(config, clientJsOutDir, clientJsSubdir, allExternals, cache2, nextEntries, force) {
24559
- if (!config.bundleEntries || config.bundleEntries.length === 0) return false;
25310
+ if (!config.bundleEntries || config.bundleEntries.length === 0) return { changed: false, outfiles: [] };
24560
25311
  let anyChanged = false;
25312
+ const outfiles = [];
24561
25313
  for (const entry of config.bundleEntries) {
24562
25314
  const entryExternals = [
24563
25315
  .../* @__PURE__ */ new Set([...BF_CLIENT_DEDUP_KEYS, ...allExternals, ...entry.externals ?? []])
24564
25316
  ];
24565
25317
  const outfilePath = resolve6(clientJsOutDir, entry.outfile);
25318
+ outfiles.push(outfilePath);
24566
25319
  const absEntry = resolve6(entry.entry);
24567
25320
  const cacheKey = `${BUNDLE_KEY_PREFIX}${absEntry}`;
24568
25321
  const sourceContent = await readText(absEntry);
@@ -24585,7 +25338,7 @@ async function processBundleEntries(config, clientJsOutDir, clientJsSubdir, allE
24585
25338
  }
24586
25339
  }
24587
25340
  const absWorkingDir = process.cwd();
24588
- const result2 = await esbuildBuild({
25341
+ const result2 = await esbuildBuild2({
24589
25342
  entryPoints: [entry.entry],
24590
25343
  outfile: outfilePath,
24591
25344
  format: "esm",
@@ -24618,10 +25371,26 @@ async function processBundleEntries(config, clientJsOutDir, clientJsSubdir, allE
24618
25371
  manifestKey: null
24619
25372
  };
24620
25373
  }
24621
- return anyChanged;
25374
+ return { changed: anyChanged, outfiles };
25375
+ }
25376
+ function registerAdapterChildShapes(adapter, allFiles, sourceContents, program) {
25377
+ const hook = adapter.registerChildComponentShape;
25378
+ if (typeof hook !== "function") return;
25379
+ for (const filePath of allFiles) {
25380
+ const source = sourceContents.get(filePath);
25381
+ if (!source) continue;
25382
+ try {
25383
+ for (const componentName of listComponentFunctions(source, filePath)) {
25384
+ const ctx2 = analyzeComponent(source, filePath, componentName, program);
25385
+ if (!ctx2.jsxReturn) continue;
25386
+ hook.call(adapter, { metadata: buildMetadata(ctx2) });
25387
+ }
25388
+ } catch {
25389
+ }
25390
+ }
24622
25391
  }
24623
25392
  async function collectRelativeImportDeps(entryPath, sourceContent) {
24624
- const baseDir = dirname3(entryPath);
25393
+ const baseDir = dirname4(entryPath);
24625
25394
  const seen = /* @__PURE__ */ new Set();
24626
25395
  const results = [];
24627
25396
  const EXT_CANDIDATES = [".tsx", ".ts", "/index.tsx", "/index.ts"];
@@ -24729,7 +25498,7 @@ async function compileEntry(args2) {
24729
25498
  const rel = `${clientJsSubdir}/${clientJsFilename}`;
24730
25499
  outputs.push(rel);
24731
25500
  const target2 = resolve6(clientJsOutDir, clientJsFilename);
24732
- await mkdir(dirname3(target2), { recursive: true });
25501
+ await mkdir(dirname4(target2), { recursive: true });
24733
25502
  if (await writeIfChanged(target2, clientJsContent)) {
24734
25503
  wroteAny = true;
24735
25504
  console.log(`Generated: ${rel}`);
@@ -24746,7 +25515,7 @@ async function compileEntry(args2) {
24746
25515
  const rel = `${templatesSubdir}/${outName}`;
24747
25516
  outputs.push(rel);
24748
25517
  const target2 = resolve6(templatesOutDir, outName);
24749
- await mkdir(dirname3(target2), { recursive: true });
25518
+ await mkdir(dirname4(target2), { recursive: true });
24750
25519
  if (await writeIfChanged(target2, outputContent)) {
24751
25520
  wroteAny = true;
24752
25521
  console.log(`Generated: ${rel}`);
@@ -24758,22 +25527,35 @@ async function compileEntry(args2) {
24758
25527
  if (!config.clientOnly && markedTemplates.length > 0) {
24759
25528
  const primaryTpl = markedTemplates.find((t) => effectiveOutName(t.path, baseNameNoExt).startsWith(baseNameNoExt + ".")) ?? markedTemplates[0];
24760
25529
  manifestKey = baseNameNoExt;
24761
- const primaryBase = primaryTpl.path.replace(/\.[^.]+$/, "").replace(/\.html$/, "");
24762
- const ssrDefaultsFile = result2.files.find(
24763
- (f) => f.type === "ssrDefaults" && f.path === primaryBase + ".ssr-defaults.json"
24764
- ) ?? result2.files.find((f) => f.type === "ssrDefaults");
24765
- let ssrDefaults;
24766
- if (ssrDefaultsFile) {
25530
+ const ssrDefaultsForTemplate = (tplPath) => {
25531
+ const base = tplPath.replace(/\.[^.]+$/, "").replace(/\.html$/, "");
25532
+ const file = result2.files.find((f) => f.type === "ssrDefaults" && f.path === base + ".ssr-defaults.json") ?? (tplPath === primaryTpl.path ? result2.files.find((f) => f.type === "ssrDefaults") : void 0);
25533
+ if (!file) return void 0;
24767
25534
  try {
24768
- ssrDefaults = JSON.parse(ssrDefaultsFile.content);
25535
+ return JSON.parse(file.content);
24769
25536
  } catch {
24770
- ssrDefaults = void 0;
25537
+ return void 0;
25538
+ }
25539
+ };
25540
+ const ssrDefaults = ssrDefaultsForTemplate(primaryTpl.path);
25541
+ let components;
25542
+ if (config.adapter.templatesPerComponent) {
25543
+ components = {};
25544
+ for (const tpl of markedTemplates) {
25545
+ if (!tpl.componentName) continue;
25546
+ const componentDefaults = ssrDefaultsForTemplate(tpl.path);
25547
+ components[tpl.componentName] = {
25548
+ markedTemplate: `${templatesSubdir}/${effectiveOutName(tpl.path, baseNameNoExt)}`,
25549
+ ...componentDefaults ? { ssrDefaults: componentDefaults } : {}
25550
+ };
24771
25551
  }
25552
+ if (Object.keys(components).length === 0) components = void 0;
24772
25553
  }
24773
25554
  manifestEntry = {
24774
25555
  markedTemplate: `${templatesSubdir}/${effectiveOutName(primaryTpl.path, baseNameNoExt)}`,
24775
25556
  clientJs: hasClientJs ? `${clientJsSubdir}/${clientJsFilename}` : void 0,
24776
- ...ssrDefaults ? { ssrDefaults } : {}
25557
+ ...ssrDefaults ? { ssrDefaults } : {},
25558
+ ...components ? { components } : {}
24777
25559
  };
24778
25560
  }
24779
25561
  return {
@@ -24937,6 +25719,7 @@ var init_build = __esm({
24937
25719
  init_fs_utils();
24938
25720
  init_assets_ignore();
24939
25721
  init_runtime();
25722
+ init_runtime_treeshake();
24940
25723
  init_resolve_imports();
24941
25724
  BF001_TRIPWIRE_IMPORTS = /* @__PURE__ */ new Set([
24942
25725
  ...REACTIVE_PRIMITIVES,
@@ -25892,8 +26675,17 @@ __export(add_exports, {
25892
26675
  run: () => run3,
25893
26676
  toRegistryName: () => toRegistryName
25894
26677
  });
25895
- import { existsSync as existsSync7, mkdirSync as mkdirSync2, copyFileSync, writeFileSync as writeFileSync2, readFileSync as readFileSync4, readdirSync as readdirSync2 } from "node:fs";
26678
+ import { existsSync as existsSync7, mkdirSync as mkdirSync2, copyFileSync, writeFileSync as writeFileSync2, readFileSync as readFileSync4, readdirSync as readdirSync2, utimesSync } from "node:fs";
25896
26679
  import path9 from "node:path";
26680
+ function kickCssWatcher(projectDir) {
26681
+ const unoConfig = path9.resolve(projectDir, "uno.config.ts");
26682
+ if (!existsSync7(unoConfig)) return;
26683
+ const now = /* @__PURE__ */ new Date();
26684
+ try {
26685
+ utimesSync(unoConfig, now, now);
26686
+ } catch {
26687
+ }
26688
+ }
25897
26689
  async function run3(args2, ctx2) {
25898
26690
  const force = args2.includes("--force");
25899
26691
  let registryUrl;
@@ -26038,6 +26830,9 @@ async function addFromRegistry(componentNames, registryUrl, projectDir, config,
26038
26830
  if (existsSync7(destMetaDir)) {
26039
26831
  rebuildMetaIndex(destMetaDir);
26040
26832
  }
26833
+ if (added.length > 0) {
26834
+ kickCssWatcher(projectDir);
26835
+ }
26041
26836
  if (silent) return;
26042
26837
  if (added.length > 0) {
26043
26838
  console.log(`
@@ -26103,6 +26898,9 @@ function addFromLocal(componentNames, ctx2, projectDir, config, force) {
26103
26898
  added.push(name2);
26104
26899
  }
26105
26900
  rebuildMetaIndex(destMetaDir);
26901
+ if (added.length > 0) {
26902
+ kickCssWatcher(projectDir);
26903
+ }
26106
26904
  if (added.length > 0) {
26107
26905
  console.log(`
26108
26906
  Added: ${added.join(", ")}`);
@@ -26154,6 +26952,9 @@ var init_add = __esm({
26154
26952
  });
26155
26953
 
26156
26954
  // src/lib/adapters/shared.ts
26955
+ function faviconLinkTag(href2) {
26956
+ return FAVICON_LINK_TAG.replace("__FAVICON_HREF__", href2);
26957
+ }
26157
26958
  function unoConfigTs(scanGlobs) {
26158
26959
  const formatted = scanGlobs.map((g) => `'${g}'`).join(", ");
26159
26960
  return `import { defineConfig, presetWind4 } from 'unocss'
@@ -26229,7 +27030,7 @@ function buildGitignore(sections) {
26229
27030
  lines.push(...SHARED_GITIGNORE_LINES);
26230
27031
  return lines.join("\n") + "\n";
26231
27032
  }
26232
- var CSS_LINKS_BEGIN, CSS_LINKS_END, SHARED_COUNTER_TSX, SHARED_COUNTER_TEST_TSX, SHARED_COUNTER_BARE_TSX, SHARED_COUNTER_BARE_TEST_TSX, TOKENS_CSS, STYLES_CSS, UNO_CSS_PLACEHOLDER, COMPONENTS_MANIFEST_SEED, UNOCSS_DEV_DEPENDENCIES, SHARED_GITIGNORE_LINES;
27033
+ var CSS_LINKS_BEGIN, CSS_LINKS_END, SHARED_COUNTER_TSX, SHARED_COUNTER_TEST_TSX, SHARED_COUNTER_BARE_TSX, SHARED_COUNTER_BARE_TEST_TSX, TOKENS_CSS, STYLES_CSS, UNO_CSS_PLACEHOLDER, FAVICON_SVG, FAVICON_LINK_TAG, COMPONENTS_MANIFEST_SEED, UNOCSS_DEV_DEPENDENCIES, SHARED_GITIGNORE_LINES;
26233
27034
  var init_shared2 = __esm({
26234
27035
  "src/lib/adapters/shared.ts"() {
26235
27036
  "use strict";
@@ -26485,6 +27286,16 @@ main {
26485
27286
  `;
26486
27287
  UNO_CSS_PLACEHOLDER = `/* generated by unocss --watch */
26487
27288
  `;
27289
+ FAVICON_SVG = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
27290
+ <rect width="32" height="32" rx="7" fill="#1c1917"/>
27291
+ <ellipse cx="16" cy="20.5" rx="6" ry="7" fill="#fafaf9"/>
27292
+ <circle cx="9" cy="10" r="2.6" fill="#fafaf9"/>
27293
+ <circle cx="14.5" cy="7.2" r="2.8" fill="#fafaf9"/>
27294
+ <circle cx="20.5" cy="7.2" r="2.8" fill="#fafaf9"/>
27295
+ <circle cx="25" cy="10.5" r="2.4" fill="#fafaf9"/>
27296
+ </svg>
27297
+ `;
27298
+ FAVICON_LINK_TAG = '<link rel="icon" type="image/svg+xml" href="__FAVICON_HREF__" />';
26488
27299
  COMPONENTS_MANIFEST_SEED = "{}\n";
26489
27300
  UNOCSS_DEV_DEPENDENCIES = {
26490
27301
  "@unocss/cli": "^66.0.0",
@@ -26514,11 +27325,12 @@ main {
26514
27325
  });
26515
27326
 
26516
27327
  // src/lib/adapters/runtimes.generated.ts
26517
- var bfGoSource, streamingGoSource, bfdevGoSource;
27328
+ var bfGoSource, evalGoSource, streamingGoSource, bfdevGoSource;
26518
27329
  var init_runtimes_generated = __esm({
26519
27330
  "src/lib/adapters/runtimes.generated.ts"() {
26520
27331
  "use strict";
26521
- bfGoSource = '// Package bf provides runtime helper functions for BarefootJS Go templates.\n// These functions mirror JavaScript behavior for consistent SSR output.\npackage bf\n\nimport (\n "bytes"\n "encoding/json"\n "fmt"\n "html/template"\n "math"\n "math/rand"\n "net/url"\n "os"\n "reflect"\n "sort"\n "strconv"\n "strings"\n "unicode/utf8"\n)\n\n// FuncMap returns a template.FuncMap with all BarefootJS helper functions.\n// Usage:\n//\n// tmpl := template.New("").Funcs(bf.FuncMap())\nfunc FuncMap() template.FuncMap {\n return template.FuncMap{\n // Arithmetic\n "bf_add": Add,\n "bf_sub": Sub,\n "bf_mul": Mul,\n "bf_div": Div,\n "bf_mod": Mod,\n "bf_neg": Neg,\n "bf_min": Min,\n "bf_max": Max,\n\n // String\n "bf_lower": Lower,\n "bf_upper": Upper,\n "bf_trim": Trim,\n "bf_contains": Contains,\n "bf_join": Join,\n "bf_split": Split,\n "bf_starts_with": StartsWith,\n "bf_ends_with": EndsWith,\n "bf_replace": Replace,\n "bf_repeat": Repeat,\n "bf_pad_start": PadStart,\n "bf_pad_end": PadEnd,\n "bf_string": String,\n\n // URL query builder (#1897 PostList href helpers): conditional\n // (include, key, value) triples \u2192 "base?k=v&\u2026", mirroring a\n // URLSearchParams builder with guarded `.set()` calls.\n "bf_query": Query,\n\n // JSON / numeric primitives \u2014 JS-compat callees registered on\n // the Go adapter\'s `templatePrimitives` map (#1188).\n "bf_json": JSON,\n "bf_number": Number,\n "bf_floor": Floor,\n "bf_ceil": Ceil,\n "bf_round": Round,\n "bf_to_fixed": ToFixed,\n\n // Array/Slice\n "bf_len": Len,\n "bf_at": At,\n "bf_includes": Includes,\n "bf_index_of": IndexOf,\n "bf_last_index_of": LastIndexOf,\n "bf_concat": Concat,\n "bf_slice": Slice,\n "bf_reverse": Reverse,\n "bf_flat": Flat,\n "bf_flat_map": FlatMap,\n "bf_flat_map_tuple": FlatMapTuple,\n "bf_first": First,\n "bf_last": Last,\n "bf_arr": Arr,\n "bf_filter_truthy": FilterTruthy,\n\n // Higher-order Array Methods\n "bf_every": Every,\n "bf_some": Some,\n "bf_filter": Filter,\n "bf_find": Find,\n "bf_find_index": FindIndex,\n "bf_find_last": FindLast,\n "bf_find_last_index": FindLastIndex,\n "bf_sort": Sort,\n "bf_reduce": Reduce,\n\n // Evaluator-driven higher-order folds (#2018): the comparator / reducer\n // body travels as a serialized ParsedExpr (JSON) evaluated per element,\n // generalizing bf_sort / bf_reduce beyond their fixed catalogues. The\n // adapter falls back to bf_sort for a comparator the evaluator can\'t\n // model (e.g. localeCompare). `bf_env` builds the captured-free-var\n // environment passed as the trailing base_env argument.\n "bf_sort_eval": SortEval,\n "bf_reduce_eval": FoldEval,\n "bf_env": Env,\n\n // Evaluator-driven higher-order predicates (#2018, P2): the predicate\n // body travels as a serialized ParsedExpr (JSON) evaluated per element,\n // generalizing bf_filter / bf_find / bf_find_index / bf_every / bf_some\n // beyond their field-equality / truthiness catalogues. `bf_find_eval` /\n // `bf_find_index_eval` take a `forward` bool (false \u2192 findLast variants).\n "bf_filter_eval": FilterEval,\n "bf_every_eval": EveryEval,\n "bf_some_eval": SomeEval,\n "bf_find_eval": FindEval,\n "bf_find_index_eval": FindIndexEval,\n // `.flatMap(proj)`: project each element through the serialized\n // projection body, then flatten one level.\n "bf_flat_map_eval": FlatMapEval,\n // Value-producing `.map(cb)` (#2073): project each element, one\n // result per element (no flatten).\n "bf_map_eval": MapEval,\n\n // Comment marker (for hydration)\n "bfComment": Comment,\n "bfTextStart": TextStart,\n "bfTextEnd": TextEnd,\n\n // Script collection\n "bfScripts": BfScripts,\n\n // Scope attribute value (#1249: bare scope id, no `~` prefix)\n "bfScopeAttr": ScopeAttr,\n\n // Slot-identity markers (#1249): bf-h, bf-m, bf-r\n "bfHydrationAttrs": HydrationAttrs,\n\n // Child component marker (kept for backward compatibility)\n "bfIsChild": IsChild,\n\n // Props attribute for hydration\n "bfPropsAttr": BfPropsAttr,\n\n // Portal HTML rendering (parses and executes template string)\n "bfPortalHTML": PortalHTML,\n\n // JSX children passed to an imported child component (#1896):\n // the parent renders the children fragment via a companion\n // define (executed through bf_tmpl from TemplateFuncMap) and\n // injects the result into the child\'s Children field.\n "bf_with_children": WithChildren,\n\n // Scope comment for fragment roots\n "bfScopeComment": ScopeComment,\n\n // JSX intrinsic-element spread lowering (#1407)\n "bf_spread_attrs": SpreadAttrs,\n }\n}\n\n// Query builds a URL from a base path plus a query string assembled from\n// (include, key, value) triples, in order. A pair is considered only when its\n// `include` flag is true \u2014 mirroring a JS URLSearchParams builder whose\n// `.set(key, value)` calls are each guarded by an `if`. The compiler lowers a\n// conditional `cond ? v : undefined` to the `include` bool and a plain `key: v`\n// to a `true` include; the emptiness check is applied HERE (an included but\n// empty value is dropped), matching the client `queryHref` and the Perl `query`\n// helper. Keys and values use formEscape (application/x-www-form-urlencoded),\n// so the rendered query is byte-for-byte identical to the browser\'s\n// URLSearchParams. An empty query yields the bare base.\n//\n// A value may be a string slice ([]string or []any), which APPENDS one pair per\n// non-empty member (URLSearchParams.append) \u2014 `{tag: [a, b]}` \u2192 `tag=a&tag=b`.\n// A scalar value follows URLSearchParams.set() semantics: repeating a key\n// overwrites the value at the key\'s first position rather than duplicating it\n// (object literals have unique keys, so this is defensive). Trailing args that\n// don\'t complete a triple are ignored.\n//\n// formEscape differs from url.QueryEscape only on `~` (kept by QueryEscape,\n// `%7E` here) and `*` (`%2A` by QueryEscape, kept here).\nfunc Query(base string, triples ...any) string {\n type kv struct{ key, val string }\n pairs := make([]kv, 0, len(triples)/3)\n pos := make(map[string]int)\n for i := 0; i+2 < len(triples); i += 3 {\n include, _ := triples[i].(bool)\n if !include {\n continue\n }\n k := String(triples[i+1])\n if members, ok := asStringSlice(triples[i+2]); ok {\n // Array value \u2192 append each non-empty member; appended pairs never\n // overwrite, so they don\'t participate in the set()-position map.\n for _, m := range members {\n if m == "" {\n continue\n }\n pairs = append(pairs, kv{k, m})\n }\n continue\n }\n v := String(triples[i+2])\n if v == "" {\n continue // omit an included-but-empty value (client / Perl parity)\n }\n if at, ok := pos[k]; ok {\n pairs[at].val = v // set(): overwrite the first occurrence\'s value\n } else {\n pos[k] = len(pairs)\n pairs = append(pairs, kv{k, v})\n }\n }\n var b strings.Builder\n for _, p := range pairs {\n if b.Len() == 0 {\n b.WriteByte(\'?\')\n } else {\n b.WriteByte(\'&\')\n }\n b.WriteString(formEscape(p.key))\n b.WriteByte(\'=\')\n b.WriteString(formEscape(p.val))\n }\n return base + b.String()\n}\n\n// asStringSlice reports whether v is a query *array* value and, if so, returns\n// its members stringified. A compiled template passes a `[]string` field; the\n// golden conformance vectors decode JSON arrays to `[]any`. Anything else is a\n// scalar (false), handled by the set() path.\nfunc asStringSlice(v any) ([]string, bool) {\n switch s := v.(type) {\n case []string:\n return s, true\n case []any:\n out := make([]string, len(s))\n for i, m := range s {\n out[i] = String(m)\n }\n return out, true\n default:\n return nil, false\n }\n}\n\nconst hexUpper = "0123456789ABCDEF"\n\n// formEscape percent-encodes s with the application/x-www-form-urlencoded byte\n// set, matching the browser\'s URLSearchParams serialization (and the Perl\n// `query` helper) so SSR query strings render byte-for-byte identically across\n// adapters. The unreserved set kept verbatim is A-Z a-z 0-9 and `* - . _`; a\n// space becomes `+`; every other byte is `%XX` with uppercase hex. Encoding is\n// byte-wise, so multi-byte UTF-8 is percent-encoded per byte (`\xE9` \u2192 `%C3%A9`).\n//\n// This differs from url.QueryEscape only for `~` (kept by QueryEscape, encoded\n// to `%7E` here) and `*` (encoded to `%2A` by QueryEscape, kept here).\nfunc formEscape(s string) string {\n var b strings.Builder\n for i := 0; i < len(s); i++ {\n c := s[i]\n switch {\n case c >= \'A\' && c <= \'Z\', c >= \'a\' && c <= \'z\', c >= \'0\' && c <= \'9\',\n c == \'*\', c == \'-\', c == \'.\', c == \'_\':\n b.WriteByte(c)\n case c == \' \':\n b.WriteByte(\'+\')\n default:\n b.WriteByte(\'%\')\n b.WriteByte(hexUpper[c>>4])\n b.WriteByte(hexUpper[c&0x0F])\n }\n }\n return b.String()\n}\n\n// ScopeAttr returns the bare bf-s scope id (#1249).\nfunc ScopeAttr(props interface{}) string {\n return getStringField(props, "ScopeID")\n}\n\n// HydrationAttrs emits `bf-h="<host>" bf-m="<slot>" bf-r=""` conditionally.\n// See spec/compiler.md "Slot identity".\nfunc HydrationAttrs(props interface{}) template.HTMLAttr {\n parts := []string{}\n if host := getStringField(props, "BfParent"); host != "" {\n parts = append(parts, fmt.Sprintf(`bf-h="%s"`, template.HTMLEscapeString(host)))\n }\n if mount := getStringField(props, "BfMount"); mount != "" {\n parts = append(parts, fmt.Sprintf(`bf-m="%s"`, template.HTMLEscapeString(mount)))\n }\n if !getBoolField(props, "BfIsChild") {\n parts = append(parts, `bf-r=""`)\n }\n if len(parts) == 0 {\n return ""\n }\n return template.HTMLAttr(strings.Join(parts, " "))\n}\n\n// IsChild is a deprecated no-op stub. Child status is signalled by bf-h\n// presence (#1249); use HydrationAttrs instead.\nfunc IsChild(props interface{}) template.HTMLAttr {\n return ""\n}\n\n// svgCamelCaseAttrs mirrors SVG_CAMEL_CASE_ATTRS from\n// packages/client/src/runtime/spread-attrs.ts. SVG XML attribute\n// names are case-sensitive; the default camelCase \u2192 kebab-case\n// rewrite must NOT apply to these or the SVG stops rendering\n// (#1407). Coordinates with the compile-time SVG_CAMEL_TO_KEBAB\n// table in packages/jsx/src/ir-to-client-js/utils.ts: presentation\n// attrs (clipPath, strokeWidth, \u2026) live there and must NOT appear\n// here, or the same JSX prop would lower to clip-path via the\n// explicit-attr path and stay clipPath via the spread path.\nvar svgCamelCaseAttrs = map[string]struct{}{\n "allowReorder": {}, "attributeName": {}, "attributeType": {}, "autoReverse": {},\n "baseFrequency": {}, "baseProfile": {}, "calcMode": {}, "clipPathUnits": {},\n "contentScriptType": {}, "contentStyleType": {}, "diffuseConstant": {}, "edgeMode": {},\n "externalResourcesRequired": {}, "filterRes": {}, "filterUnits": {}, "glyphRef": {},\n "gradientTransform": {}, "gradientUnits": {}, "kernelMatrix": {}, "kernelUnitLength": {},\n "keyPoints": {}, "keySplines": {}, "keyTimes": {}, "lengthAdjust": {}, "limitingConeAngle": {},\n "markerHeight": {}, "markerUnits": {}, "markerWidth": {}, "maskContentUnits": {},\n "maskUnits": {}, "numOctaves": {}, "pathLength": {}, "patternContentUnits": {},\n "patternTransform": {}, "patternUnits": {}, "pointsAtX": {}, "pointsAtY": {}, "pointsAtZ": {},\n "preserveAlpha": {}, "preserveAspectRatio": {}, "primitiveUnits": {}, "refX": {}, "refY": {},\n "repeatCount": {}, "repeatDur": {}, "requiredExtensions": {}, "requiredFeatures": {},\n "specularConstant": {}, "specularExponent": {}, "spreadMethod": {}, "startOffset": {},\n "stdDeviation": {}, "stitchTiles": {}, "surfaceScale": {}, "systemLanguage": {},\n "tableValues": {}, "targetX": {}, "targetY": {}, "textLength": {}, "viewBox": {}, "viewTarget": {},\n "xChannelSelector": {}, "yChannelSelector": {}, "zoomAndPan": {},\n}\n\n// toAttrName mirrors the JSX\u2192HTML attribute-name rewrite from\n// packages/client/src/runtime/spread-attrs.ts. className \u2192 class,\n// htmlFor \u2192 for, SVG camelCase attrs preserved, other camelCase\n// keys lowered to kebab-case.\nfunc toAttrName(key string) string {\n if key == "className" {\n return "class"\n }\n if key == "htmlFor" {\n return "for"\n }\n if _, ok := svgCamelCaseAttrs[key]; ok {\n return key\n }\n // camelCase \u2192 kebab-case: mirror the JS reference exactly\n // (`key.replace(/([A-Z])/g, \'-$1\').toLowerCase()`). The JS shape\n // produces a leading `-` for an initial uppercase letter\n // (`XData` \u2192 `-x-data`); both this Go path and the matching JS\n // runtime are wrong-by-construction for that case (the resulting\n // HTML attribute name is invalid), but keeping them byte-equal\n // avoids silent SSR/CSR divergence (#1411 review).\n var b strings.Builder\n for _, r := range key {\n if r >= \'A\' && r <= \'Z\' {\n b.WriteByte(\'-\')\n b.WriteRune(r + 32)\n } else {\n b.WriteRune(r)\n }\n }\n return b.String()\n}\n\n// StyleToCss mirrors styleToCss from\n// packages/client/src/runtime/style.ts. Accepts a string passthrough,\n// or a map (JSON-deserialized object) whose camelCase keys are\n// lowered to kebab-case and joined with `;`. Returns ("", false) for\n// nullish/empty input so callers can omit the attribute entirely.\nfunc StyleToCss(v any) (string, bool) {\n if v == nil {\n return "", false\n }\n rv := reflect.ValueOf(v)\n for rv.Kind() == reflect.Interface || rv.Kind() == reflect.Pointer {\n if rv.IsNil() {\n return "", false\n }\n rv = rv.Elem()\n }\n if rv.Kind() != reflect.Map {\n // Non-object: stringify and return as-is, matching the JS\n // `typeof value !== \'object\'` branch.\n s := fmt.Sprint(v)\n if s == "" {\n return "", false\n }\n return s, true\n }\n keys := rv.MapKeys()\n sorted := make([]string, 0, len(keys))\n for _, k := range keys {\n if k.Kind() == reflect.String {\n sorted = append(sorted, k.String())\n }\n }\n sort.Strings(sorted)\n parts := make([]string, 0, len(sorted))\n for _, k := range sorted {\n val := rv.MapIndex(reflect.ValueOf(k))\n // Skip nil entries (matches the JS `if (v == null) continue`).\n if !val.IsValid() {\n continue\n }\n if val.Kind() == reflect.Interface || val.Kind() == reflect.Pointer {\n if val.IsNil() {\n continue\n }\n val = val.Elem()\n }\n prop := toAttrName(k)\n parts = append(parts, fmt.Sprintf("%s:%v", prop, val.Interface()))\n }\n if len(parts) == 0 {\n return "", false\n }\n return strings.Join(parts, ";"), true\n}\n\n// SpreadAttrs lowers a JSX intrinsic-element spread bag (#1407) to\n// an HTML attribute string. Mirrors spreadAttrs from\n// packages/client/src/runtime/spread-attrs.ts so SSR output matches\n// what CSR\'s `applyRestAttrs` writes at hydration.\n//\n// Skip rules: nil/false values, event handlers (`on[A-Z]*`),\n// `children`, `ref`.\n//\n// Key remap: className \u2192 class, htmlFor \u2192 for, SVG camelCase\n// preserved, other camelCase \u2192 kebab-case.\n//\n// `style` is routed through StyleToCss so object literals serialize\n// to a real CSS string instead of Go\'s default `map[k:v]` form.\n//\n// Booleans: true \u2192 bare attribute name, false \u2192 omitted.\n// Other scalar values are HTML-escaped via template.HTMLEscapeString.\n// Returns a `template.HTMLAttr` so html/template emits the result\n// verbatim (the function does its own escaping).\n//\n// Keys are sorted alphabetically before emission for deterministic\n// output. SSR/CSR attribute-order divergence is acceptable per the\n// rest-destructure-object-spread-in-map fixture\'s documented policy\n// \u2014 browsers honor the LAST value when a key is duplicated, so\n// pairing with static attrs (`<div class="x" {...rest}>`) is\n// last-wins regardless of order.\nfunc SpreadAttrs(bag any) template.HTMLAttr {\n if bag == nil {\n return ""\n }\n rv := reflect.ValueOf(bag)\n for rv.Kind() == reflect.Interface || rv.Kind() == reflect.Pointer {\n if rv.IsNil() {\n return ""\n }\n rv = rv.Elem()\n }\n if rv.Kind() != reflect.Map {\n return ""\n }\n keys := rv.MapKeys()\n sortedKeys := make([]string, 0, len(keys))\n for _, k := range keys {\n if k.Kind() == reflect.String {\n sortedKeys = append(sortedKeys, k.String())\n }\n }\n sort.Strings(sortedKeys)\n parts := make([]string, 0, len(sortedKeys))\n for _, key := range sortedKeys {\n // Event handlers \u2014 skip at SSR the same way\n // packages/client/src/runtime/spread-attrs.ts does at\n // hydration. The JS predicate is\n // `key.startsWith(\'on\') && key.length > 2 && key[2] === key[2].toUpperCase()`,\n // which is true for any character whose uppercase form is\n // itself: ASCII A-Z, digits, underscore, and non-letter\n // symbols. Mirror that here by skipping when key[2] is NOT\n // a lowercase ASCII letter \u2014 so `onClick`, `on_custom`, and\n // `on0` all match (#1411 review).\n if len(key) > 2 && key[0] == \'o\' && key[1] == \'n\' && !(key[2] >= \'a\' && key[2] <= \'z\') {\n continue\n }\n // `children` is a JSX construct rendered inside the element,\n // never a DOM attribute. `ref` is intentionally NOT filtered\n // here so output stays byte-equal with the JS reference\n // `spreadAttrs` in packages/client/src/runtime/spread-attrs.ts\n // (which only filters null/false, event handlers, and\n // children) \u2014 aligning Go\'s filter set diverges from JS in\n // the opposite direction. Filtering `ref` consistently across\n // both SSR runtimes is a separate concern tracked alongside\n // the JS `applyRestAttrs` vs `spreadAttrs` mismatch (#1411\n // review).\n if key == "children" {\n continue\n }\n val := rv.MapIndex(reflect.ValueOf(key))\n if !val.IsValid() {\n continue\n }\n // Unwrap interface wrappers (json.Unmarshal produces\n // interface{}-wrapped values for map[string]any).\n v := val\n for v.Kind() == reflect.Interface || v.Kind() == reflect.Pointer {\n if v.IsNil() {\n // Skip null entries.\n v = reflect.Value{}\n break\n }\n v = v.Elem()\n }\n if !v.IsValid() {\n continue\n }\n // Boolean values: true \u2192 bare attribute, false \u2192 omitted.\n if v.Kind() == reflect.Bool {\n if !v.Bool() {\n continue\n }\n parts = append(parts, toAttrName(key))\n continue\n }\n // `style` routes through StyleToCss so object literals get a\n // real CSS string. The JS side does the same.\n if key == "style" {\n css, ok := StyleToCss(v.Interface())\n if !ok {\n continue\n }\n parts = append(parts, fmt.Sprintf(`style="%s"`, template.HTMLEscapeString(css)))\n continue\n }\n // Stringify and escape. fmt.Sprint handles numbers, bools-as-\n // strings, and arbitrary stringer types the same way the JS\n // `String(value)` coercion does for the analogous cases.\n s := fmt.Sprint(v.Interface())\n parts = append(parts, fmt.Sprintf(`%s="%s"`, toAttrName(key), template.HTMLEscapeString(s)))\n }\n if len(parts) == 0 {\n return ""\n }\n return template.HTMLAttr(strings.Join(parts, " "))\n}\n\n// BfPropsAttr returns the bf-p attribute with the JSON-serialized\n// props in flat format. Output format: `bf-p=\'{"propName":value,...}\'`.\n// Only emits the attribute for root components (BfIsRoot == true);\n// child components receive props from their parent via initChild().\n//\n// Returns the marshal error so a `template.Execute` call fails\n// loudly on cycles / unsupported props rather than silently\n// dropping the bf-p attribute and breaking client-side hydration.\n// Same loud-failure policy as `JSON` \u2014 user data going through\n// `encoding/json` shouldn\'t fail invisibly.\nfunc BfPropsAttr(props interface{}) (template.HTMLAttr, error) {\n // Only root components should emit bf-p\n if !getBoolField(props, "BfIsRoot") {\n return "", nil\n }\n\n propsJSON, err := json.Marshal(props)\n if err != nil {\n return "", err\n }\n\n escaped := template.HTMLEscapeString(string(propsJSON))\n return template.HTMLAttr(`bf-p="` + escaped + `"`), nil\n}\n\n// =============================================================================\n// Arithmetic Operations\n// =============================================================================\n\n// Add returns a + b. Supports int and float64.\nfunc Add(a, b any) any {\n av, bv := toFloat64(a), toFloat64(b)\n result := av + bv\n // Return int if both inputs were int-like\n if isIntLike(a) && isIntLike(b) && result == float64(int(result)) {\n return int(result)\n }\n return result\n}\n\n// Sub returns a - b. Supports int and float64.\nfunc Sub(a, b any) any {\n av, bv := toFloat64(a), toFloat64(b)\n result := av - bv\n if isIntLike(a) && isIntLike(b) && result == float64(int(result)) {\n return int(result)\n }\n return result\n}\n\n// Mul returns a * b. Supports int and float64.\nfunc Mul(a, b any) any {\n av, bv := toFloat64(a), toFloat64(b)\n result := av * bv\n if isIntLike(a) && isIntLike(b) && result == float64(int(result)) {\n return int(result)\n }\n return result\n}\n\n// Div returns a / b. Returns float64 to match JavaScript behavior.\n// Returns 0 if b is 0 (instead of panicking).\nfunc Div(a, b any) any {\n av, bv := toFloat64(a), toFloat64(b)\n if bv == 0 {\n return 0\n }\n return av / bv\n}\n\n// Min returns the smaller of a and b (two-arg `Math.min`). Like Mul, it keeps\n// an integer result when both operands are int-like so a CSS value such as\n// `bf_min 100 x` stays `100` rather than `100.000000`.\nfunc Min(a, b any) any {\n av, bv := toFloat64(a), toFloat64(b)\n r := av\n if bv < av {\n r = bv\n }\n if isIntLike(a) && isIntLike(b) && r == float64(int(r)) {\n return int(r)\n }\n return r\n}\n\n// Max returns the larger of a and b (two-arg `Math.max`), with the same\n// int-preserving rule as Min.\nfunc Max(a, b any) any {\n av, bv := toFloat64(a), toFloat64(b)\n r := av\n if bv > av {\n r = bv\n }\n if isIntLike(a) && isIntLike(b) && r == float64(int(r)) {\n return int(r)\n }\n return r\n}\n\n// Mod returns a % b (modulo). Supports int only.\nfunc Mod(a, b any) int {\n av, bv := toInt(a), toInt(b)\n if bv == 0 {\n return 0\n }\n return av % bv\n}\n\n// Neg returns -a (negation).\nfunc Neg(a any) any {\n if v, ok := a.(int); ok {\n return -v\n }\n return -toFloat64(a)\n}\n\n// =============================================================================\n// String Operations\n// =============================================================================\n\n// Lower returns the lowercase version of s.\nfunc Lower(s string) string {\n return strings.ToLower(s)\n}\n\n// Upper returns the uppercase version of s.\nfunc Upper(s string) string {\n return strings.ToUpper(s)\n}\n\n// Trim returns s with leading and trailing whitespace removed.\nfunc Trim(s string) string {\n return strings.TrimSpace(s)\n}\n\n// Contains returns true if s contains substr.\nfunc Contains(s, substr string) bool {\n return strings.Contains(s, substr)\n}\n\n// Split lowers `String.prototype.split(sep, limit?)` (#1448 Tier B). It\n// wraps `strings.Split` and normalises the result to `[]any` so the\n// slice composes with the array-method surface downstream (`bf_join`,\n// range loops, `bf_len`, \u2026) the same way `bf_slice` / `bf_reverse`\n// results do. Like JS, an empty separator splits into individual UTF-8\n// characters and trailing empty fields are preserved (`"a,".split(",")`\n// \u2192 `["a", ""]`). An optional `limit` caps the number of returned\n// pieces (`"a,b,c".split(",", 2)` \u2192 `["a", "b"]`); a negative limit is\n// ignored (JS would also return every piece \u2014 its ToUint32 wrap makes\n// the limit effectively unbounded). The no-separator form is handled by\n// the adapter (it emits `bf_arr` for the whole-string single element).\nfunc Split(s, sep string, limit ...int) []any {\n parts := strings.Split(s, sep)\n if len(limit) > 0 && limit[0] >= 0 && limit[0] < len(parts) {\n parts = parts[:limit[0]]\n }\n out := make([]any, len(parts))\n for i, p := range parts {\n out[i] = p\n }\n return out\n}\n\n// StartsWith lowers `String.prototype.startsWith(prefix, position?)`\n// (#1448 Tier B). Wraps `strings.HasPrefix`; an empty prefix is always\n// true (JS parity). The optional `position` re-anchors the test to start\n// at that index (clamped to `[0, len]` so it never panics), matching JS\n// `"abc".startsWith("b", 1) === true`.\nfunc StartsWith(s, prefix string, position ...int) bool {\n if len(position) > 0 {\n p := position[0]\n if p < 0 {\n p = 0\n }\n if p > len(s) {\n p = len(s)\n }\n s = s[p:]\n }\n return strings.HasPrefix(s, prefix)\n}\n\n// EndsWith lowers `String.prototype.endsWith(suffix, endPosition?)`\n// (#1448 Tier B). Wraps `strings.HasSuffix`; an empty suffix is always\n// true (JS parity). The optional `endPosition` treats the string as if\n// it were only that many bytes long (clamped to `[0, len]`), matching JS\n// `"abc".endsWith("b", 2) === true`.\nfunc EndsWith(s, suffix string, endPosition ...int) bool {\n if len(endPosition) > 0 {\n e := endPosition[0]\n if e < 0 {\n e = 0\n }\n if e > len(s) {\n e = len(s)\n }\n s = s[:e]\n }\n return strings.HasSuffix(s, suffix)\n}\n\n// Replace lowers the string-pattern form of `String.prototype.replace`\n// (#1448 Tier B). JS replaces only the FIRST occurrence for a string\n// pattern, so the count is 1 (`strings.Replace` with n=1; n<0 would\n// replace all \u2014 that\'s `.replaceAll`, still refused). The replacement\n// is treated literally: unlike JS, special replacement patterns like\n// `$&` / `$1` are NOT interpreted (Go and Perl agree on literal\n// replacement, keeping the two template adapters byte-equal; this\n// diverges from the Hono/CSR JS path only for replacement strings that\n// contain `$`-patterns, which are rare in template position).\nfunc Replace(s, old, new string) string {\n return strings.Replace(s, old, new, 1)\n}\n\n// Repeat lowers `String.prototype.repeat(n)` (#1448 Tier B): the\n// receiver concatenated n times. JS throws RangeError for a negative\n// count and `strings.Repeat` panics, so a negative count clamps to the\n// empty string \u2014 SSR templates degrade rather than crash the render.\n// A zero count is the empty string (JS parity).\nfunc Repeat(s string, n int) string {\n if n <= 0 {\n return ""\n }\n return strings.Repeat(s, n)\n}\n\n// padTo lowers the shared body of `String.prototype.padStart` /\n// `padEnd` (#1448 Tier B): pad `s` to `target` code points using `pad`\n// repeated and truncated to fill, prepended (atStart) or appended.\n// Length is measured in runes (not bytes) so the result matches the\n// Perl `bf->pad_*` helpers \u2014 this diverges from JS\'s UTF-16-unit length\n// only for astral-plane input. An empty pad, or a receiver already at\n// least `target` long, returns `s` unchanged (JS parity).\nfunc padTo(s string, target int, pad string, atStart bool) string {\n if pad == "" {\n return s\n }\n sLen := utf8.RuneCountInString(s)\n if sLen >= target {\n return s\n }\n need := target - sLen\n padRunes := []rune(pad)\n fill := make([]rune, 0, need)\n for len(fill) < need {\n for _, r := range padRunes {\n if len(fill) >= need {\n break\n }\n fill = append(fill, r)\n }\n }\n if atStart {\n return string(fill) + s\n }\n return s + string(fill)\n}\n\n// PadStart lowers `String.prototype.padStart(target, pad?)` (#1448 Tier\n// B). The pad string defaults to a single space when omitted.\nfunc PadStart(s string, target int, pad ...string) string {\n p := " "\n if len(pad) > 0 {\n p = pad[0]\n }\n return padTo(s, target, p, true)\n}\n\n// PadEnd lowers `String.prototype.padEnd(target, pad?)` (#1448 Tier B).\nfunc PadEnd(s string, target int, pad ...string) string {\n p := " "\n if len(pad) > 0 {\n p = pad[0]\n }\n return padTo(s, target, p, false)\n}\n\n// Join concatenates elements of a slice with sep. Accepts both\n// reflect.Slice (the common case \u2014 `bf_arr` and `bf_filter_truthy`\n// both return `[]any`) AND reflect.Array (fixed-size Go arrays like\n// `[3]string{...}`), mirroring JS `Array.prototype.join` which\n// doesn\'t distinguish between the two. Pre-fix this returned "" for\n// fixed-size arrays passed through template data (Copilot review on\n// #1445).\nfunc Join(items any, sep string) string {\n v := reflect.ValueOf(items)\n if v.Kind() != reflect.Slice && v.Kind() != reflect.Array {\n return ""\n }\n\n parts := make([]string, v.Len())\n for i := 0; i < v.Len(); i++ {\n parts[i] = toString(v.Index(i).Interface())\n }\n return strings.Join(parts, sep)\n}\n\n// String returns the string form of v. Mirrors JS `String(v)` for\n// non-nil values via `fmt.Sprintf("%v", ...)`. Diverges from JS on\n// nil: JS `String(null)` is "null", but the template path renders\n// `nil` as the empty string here so an unset prop doesn\'t surface\n// as a literal "null"/"undefined" in user-facing HTML. Document the\n// divergence explicitly so callers don\'t rely on JS-exact parity.\nfunc String(v any) string {\n if v == nil {\n return ""\n }\n return fmt.Sprintf("%v", v)\n}\n\n// JSON returns the JSON encoding of v as a string. Mirrors\n// JS `JSON.stringify(v)` for the V1 single-arg shape (no `replacer`\n// or `space`). Object key order is determined by Go\'s `encoding/json`\n// (alphabetical for maps, declaration order for structs) \u2014 the\n// #1187 contract requires value-compat, not order-compat.\n//\n// Top-level NaN / \xB1Inf are pre-handled to match JS \u2014 JS\'s\n// `JSON.stringify(NaN)` and `JSON.stringify(Infinity)` both produce\n// `"null"`, but Go\'s `encoding/json` rejects them with\n// `UnsupportedValueError`. Without this carve-out the common\n// composition `JSON.stringify(Number("garbage"))` would error\n// instead of emitting `"null"` like JS does. Nested NaN/Inf inside\n// a struct/map still surfaces an error \u2014 covering that needs a\n// custom marshaller; out of V1 scope.\n//\n// Returns the marshal error so a `template.Execute` call fails\n// loudly on cycles / unsupported values rather than silently\n// producing `""` and reintroducing the SSR data-loss class\n// #1187 was filed against. Go\'s text/template treats a non-nil\n// error return from a func as an execution failure.\nfunc JSON(v any) (string, error) {\n if f, ok := v.(float64); ok && (math.IsNaN(f) || math.IsInf(f, 0)) {\n return "null", nil\n }\n b, err := json.Marshal(v)\n if err != nil {\n return "", err\n }\n return string(b), nil\n}\n\n// Number coerces v to a float64. Mirrors JS `Number(v)` semantics:\n// numeric / boolean inputs convert as expected; non-numeric strings\n// and other unsupported shapes return `NaN` (matching JS rather\n// than silently substituting 0, which would mis-shape downstream\n// arithmetic and template-side comparisons). Templates that need\n// a deterministic fallback should compose with the user-side\n// default (e.g. `Number(props.x ?? 0)` in JSX).\nfunc Number(v any) float64 {\n if v == nil {\n return math.NaN()\n }\n switch x := v.(type) {\n case float64:\n return x\n case float32:\n return float64(x)\n case int:\n return float64(x)\n case int32:\n return float64(x)\n case int64:\n return float64(x)\n case bool:\n if x {\n return 1\n }\n return 0\n case string:\n f, err := strconv.ParseFloat(x, 64)\n if err != nil {\n return math.NaN()\n }\n return f\n }\n return math.NaN()\n}\n\n// Floor returns the largest integer \u2264 v as a float64. Mirrors JS\n// `Math.floor`. The return type stays float64 so chained primitives\n// (`bf_floor` then `bf_string`) line up with JS\'s number type.\nfunc Floor(v any) float64 {\n return math.Floor(Number(v))\n}\n\n// ToFixed formats v with exactly `digits` decimal places, mirroring JS\n// `Number.prototype.toFixed` (zero-padding + half-toward-+Infinity\n// rounding). JS rounds the scaled integer half up (`(2.5).toFixed(0)`\n// is "3"); bare `fmt.Sprintf("%.*f")` rounds half-to-even ("2"), so we\n// scale, round with `Floor(x + 0.5)` (matching `Round`), then format\n// the exact multiple. #1897.\nfunc ToFixed(v any, digits int) string {\n if digits < 0 {\n digits = 0\n }\n n := Number(v)\n // JS toFixed returns the strings "NaN" / "Infinity" / "-Infinity" for\n // non-finite inputs; fmt would render "NaN"/"+Inf"/"-Inf".\n if math.IsNaN(n) {\n return "NaN"\n }\n if math.IsInf(n, 1) {\n return "Infinity"\n }\n if math.IsInf(n, -1) {\n return "-Infinity"\n }\n factor := math.Pow(10, float64(digits))\n rounded := math.Floor(n*factor + 0.5)\n return fmt.Sprintf("%.*f", digits, rounded/factor)\n}\n\n// Ceil returns the smallest integer \u2265 v as a float64. Mirrors JS\n// `Math.ceil`.\nfunc Ceil(v any) float64 {\n return math.Ceil(Number(v))\n}\n\n// Round returns v rounded to the nearest integer as a float64.\n// Mirrors JS `Math.round` \u2014 half-away-from-zero (Go\'s `math.Round`\n// matches; JS rounds half toward +Infinity which differs at .5\n// negatives; we accept that minor divergence since the conformance\n// contract is value-compat for the common positive case).\nfunc Round(v any) float64 {\n return math.Round(Number(v))\n}\n\n// =============================================================================\n// Array/Slice Operations\n// =============================================================================\n\n// Len returns the length of a slice, array, map, string, or channel.\n// Returns 0 for nil or unsupported types.\nfunc Len(v any) int {\n if v == nil {\n return 0\n }\n rv := reflect.ValueOf(v)\n switch rv.Kind() {\n case reflect.Slice, reflect.Array, reflect.Map, reflect.String, reflect.Chan:\n return rv.Len()\n default:\n return 0\n }\n}\n\n// At returns the element at index i from a slice.\n// Supports negative indices (e.g., -1 for last element).\n// Returns nil if index is out of bounds.\nfunc At(items any, index int) any {\n v := reflect.ValueOf(items)\n if v.Kind() != reflect.Slice && v.Kind() != reflect.Array {\n return nil\n }\n\n length := v.Len()\n if length == 0 {\n return nil\n }\n\n // Handle negative indices\n if index < 0 {\n index = length + index\n }\n\n if index < 0 || index >= length {\n return nil\n }\n\n return v.Index(index).Interface()\n}\n\n// Includes returns true if items contains elem. Lowers both\n// `Array.prototype.includes` and `String.prototype.includes` \u2014\n// the adapter can\'t disambiguate the receiver at compile time,\n// so this helper dispatches at runtime on `reflect.Kind()`:\n//\n// - slice/array receiver: SameValueZero element search (matches\n// the evaluator\'s `evalSameValueZero`/`evalIncludes` in eval.go,\n// which back the serialized-callback path) \u2014 numeric types compare\n// by value across int/float64 the way JS\'s single "number" type\n// does, and NaN matches NaN (unlike `===`). This used to be\n// `reflect.DeepEqual`, which is type-strict (`int(2)` != `float64(2)`)\n// and never matches NaN to NaN; that diverged from the evaluator\'s\n// `.includes` and from JS itself, so it was unified here.\n// - string receiver: strings.Contains substring search\n//\n// Anything else returns false (matches the JS semantic where\n// `.includes` is only defined on Array / TypedArray / String).\nfunc Includes(recv any, elem any) bool {\n v := reflect.ValueOf(recv)\n if v.Kind() == reflect.String {\n // JS `String.prototype.includes` accepts only string args;\n // non-string `elem` would TypeError in real JS but our\n // callers have lowered through `convertExpressionToGo`\n // where the arg type is whatever the template binds. Stringify\n // via fmt to keep the helper total.\n needle, ok := elem.(string)\n if !ok {\n needle = fmt.Sprintf("%v", elem)\n }\n return strings.Contains(v.String(), needle)\n }\n if v.Kind() != reflect.Slice && v.Kind() != reflect.Array {\n return false\n }\n for i := 0; i < v.Len(); i++ {\n if evalSameValueZero(v.Index(i).Interface(), elem) {\n return true\n }\n }\n return false\n}\n\n// IndexOf returns the 0-based position of the first item that\n// DeepEquals `elem`, or -1 if not found. Lowers\n// `Array.prototype.indexOf(x)` (#1448 Tier A). The existing\n// `FindIndex` helper does struct-field equality (used by the\n// higher-order `.find` lowering); this one does value equality\n// against scalar / struct items so callers don\'t have to compose\n// a synthetic predicate.\n//\n// Non-array / non-slice receivers return -1 (matches the JS\n// semantic that `.indexOf` is only defined on Array / TypedArray).\nfunc IndexOf(items any, elem any) int {\n v := reflect.ValueOf(items)\n if v.Kind() != reflect.Slice && v.Kind() != reflect.Array {\n return -1\n }\n for i := 0; i < v.Len(); i++ {\n if reflect.DeepEqual(v.Index(i).Interface(), elem) {\n return i\n }\n }\n return -1\n}\n\n// LastIndexOf returns the 0-based position of the last item that\n// DeepEquals `elem`, or -1 if not found. Mirrors\n// `Array.prototype.lastIndexOf(x)`. The reverse traversal is the\n// only behavioural difference vs `IndexOf` \u2014 disambiguating a\n// duplicated value\'s first vs last position is the canonical\n// reason a JS author reaches for `lastIndexOf`.\nfunc LastIndexOf(items any, elem any) int {\n v := reflect.ValueOf(items)\n if v.Kind() != reflect.Slice && v.Kind() != reflect.Array {\n return -1\n }\n for i := v.Len() - 1; i >= 0; i-- {\n if reflect.DeepEqual(v.Index(i).Interface(), elem) {\n return i\n }\n }\n return -1\n}\n\n// Concat merges two arrays (or slices) into a single `[]any`,\n// preserving order: receiver elements first, then `other`\'s.\n// Lowers `Array.prototype.concat(other)` (#1448 Tier A). Non-array\n// operands collapse to an empty source \u2014 matches the JS semantic\n// where `.concat` on a non-Array reads it as a single element only\n// if its `Symbol.isConcatSpreadable` is true; the template-language\n// path doesn\'t have user objects with that flag, so treating\n// non-arrays as empty is the conservative lowering. Variadic\n// `.concat(a, b, c)` is out of scope here (parser gates to a single\n// arg); the helper itself stays binary so a future variadic IR can\n// fold via repeated calls without changing this signature.\nfunc Concat(a, b any) []any {\n flatten := func(v reflect.Value) []any {\n if !v.IsValid() {\n return nil\n }\n if v.Kind() != reflect.Slice && v.Kind() != reflect.Array {\n return nil\n }\n out := make([]any, v.Len())\n for i := 0; i < v.Len(); i++ {\n out[i] = v.Index(i).Interface()\n }\n return out\n }\n left := flatten(reflect.ValueOf(a))\n right := flatten(reflect.ValueOf(b))\n return append(left, right...)\n}\n\n// Slice carves out a sub-range from `items`. Lowers\n// `Array.prototype.slice(start, end?)` (#1448 Tier A). The variadic\n// `end` arg lets Go template\'s call dispatcher pass either 2 or 3\n// arguments; an absent end means "to length".\n//\n// JS-compat clamping:\n// - start < 0 \u2192 length + start (e.g. -1 = last index)\n// - end < 0 \u2192 length + end\n// - start < 0 after clamp \u2192 0\n// - end > length \u2192 length\n// - start >= end \u2192 empty slice (no panic)\n//\n// Non-array receivers return an empty `[]any`.\nfunc Slice(items any, start int, end ...int) []any {\n v := reflect.ValueOf(items)\n if v.Kind() != reflect.Slice && v.Kind() != reflect.Array {\n return []any{}\n }\n length := v.Len()\n\n // Normalise start (negative = from end).\n if start < 0 {\n start = length + start\n }\n if start < 0 {\n start = 0\n }\n if start > length {\n start = length\n }\n\n // Normalise end (optional; absent = length).\n stop := length\n if len(end) > 0 {\n stop = end[0]\n if stop < 0 {\n stop = length + stop\n }\n if stop < 0 {\n stop = 0\n }\n if stop > length {\n stop = length\n }\n }\n\n if start >= stop {\n return []any{}\n }\n\n out := make([]any, 0, stop-start)\n for i := start; i < stop; i++ {\n out = append(out, v.Index(i).Interface())\n }\n return out\n}\n\n// Reverse returns a new slice with `items`\'s elements in reverse\n// order. Lowers both `Array.prototype.reverse()` and\n// `Array.prototype.toReversed()` (#1448 Tier A) \u2014 SSR templates\n// render a snapshot, so JS\'s mutate-receiver vs return-new-array\n// distinction has no template-level meaning, and the safer\n// non-mutating shape is used uniformly.\n//\n// Non-array receivers return an empty `[]any`.\nfunc Reverse(items any) []any {\n v := reflect.ValueOf(items)\n if v.Kind() != reflect.Slice && v.Kind() != reflect.Array {\n return []any{}\n }\n length := v.Len()\n out := make([]any, length)\n for i := 0; i < length; i++ {\n out[length-1-i] = v.Index(i).Interface()\n }\n return out\n}\n\n// Flat flattens nested slices/arrays `depth` levels deep. Lowers\n// `Array.prototype.flat(depth?)` (#1448 Tier C). A `depth` of `-1` is the\n// `Infinity` sentinel (flatten fully); `0` (or negative-from-JS, already\n// normalised to 0 at compile time) returns a shallow copy. Non-array\n// elements are kept as-is (JS only flattens nested arrays). A non-array\n// receiver returns an empty `[]any`.\nfunc Flat(items any, depth int) []any {\n v := reflect.ValueOf(items)\n if v.Kind() != reflect.Slice && v.Kind() != reflect.Array {\n return []any{}\n }\n out := make([]any, 0, v.Len())\n for i := 0; i < v.Len(); i++ {\n el := v.Index(i).Interface()\n ev := reflect.ValueOf(el)\n if depth != 0 && (ev.Kind() == reflect.Slice || ev.Kind() == reflect.Array) {\n // `-1` (Infinity) recurses unbounded; a finite depth spends one level.\n next := depth\n if depth > 0 {\n next = depth - 1\n }\n out = append(out, Flat(el, next)...)\n } else {\n out = append(out, el)\n }\n }\n return out\n}\n\n// FlatMap projects each element through a `self` / `field` projection and\n// flattens the result one level. Lowers value-returning\n// `Array.prototype.flatMap(fn)` for the field-projection catalogue\n// (#1448 Tier C): `items.flatMap(i => i)` (self) and\n// `items.flatMap(i => i.field)` (field). A projected non-array value is\n// kept as-is (flatMap = map + flat(1)). Non-array receiver \u2192 empty.\nfunc FlatMap(items any, keyKind, keyName string) []any {\n v := reflect.ValueOf(items)\n if v.Kind() != reflect.Slice && v.Kind() != reflect.Array {\n return []any{}\n }\n projected := make([]any, 0, v.Len())\n for i := 0; i < v.Len(); i++ {\n el := v.Index(i).Interface()\n if keyKind == "field" {\n projected = append(projected, getFieldValue(el, keyName))\n } else {\n projected = append(projected, el)\n }\n }\n return Flat(projected, 1)\n}\n\n// FlatMapTuple lowers an array-literal flatMap projection\n// `items.flatMap(i => [i.a, i.b])` (#1448 Tier C). `specs` is a flat list\n// of (kind, name) pairs, one per array-literal leaf: ("self", "") for the\n// item itself, ("field", "<Name>") for a struct field. For each item it\n// appends every leaf\'s value in order. Unlike the scalar `FlatMap`, the\n// per-item array is flattened only one level (flat(1) removes the literal\n// wrapper), so an array-valued leaf is appended verbatim rather than\n// spread \u2014 which is exactly "append each leaf". Non-array receiver \u2192 empty.\nfunc FlatMapTuple(items any, specs ...string) []any {\n v := reflect.ValueOf(items)\n if v.Kind() != reflect.Slice && v.Kind() != reflect.Array {\n return []any{}\n }\n out := make([]any, 0, v.Len())\n for i := 0; i < v.Len(); i++ {\n el := v.Index(i).Interface()\n for j := 0; j+1 < len(specs); j += 2 {\n if specs[j] == "field" {\n out = append(out, getFieldValue(el, specs[j+1]))\n } else {\n out = append(out, el)\n }\n }\n }\n return out\n}\n\n// First returns the first element of a slice, or nil if empty.\nfunc First(items any) any {\n return At(items, 0)\n}\n\n// Last returns the last element of a slice, or nil if empty.\nfunc Last(items any) any {\n return At(items, -1)\n}\n\n// Arr builds an []any from variadic args. Used to lower JS array\n// literals like `[a, b]` for the registry Slot\'s\n// `[className, childClass].filter(Boolean).join(\' \')` shape (#1443) \u2014\n// Go templates have no array-literal syntax, so the codegen routes\n// array-literal IR nodes through this helper.\nfunc Arr(items ...any) []any {\n return items\n}\n\n// FilterTruthy returns a new slice containing only truthy items.\n// Mirrors `arr.filter(Boolean)` semantics: drop nil, false, 0, "" \u2014 the\n// same falsy set JavaScript\'s `Boolean(x)` recognises. Used to lower\n// the registry Slot\'s class-merge pattern (#1443); generalising to\n// arbitrary callable predicates would need the callee-resolution path\n// blocked by #1389, so this stays Boolean-specific.\nfunc FilterTruthy(items any) []any {\n v := reflect.ValueOf(items)\n if !v.IsValid() || (v.Kind() != reflect.Slice && v.Kind() != reflect.Array) {\n return nil\n }\n result := make([]any, 0, v.Len())\n for i := 0; i < v.Len(); i++ {\n raw := v.Index(i).Interface()\n if isTruthy(raw) {\n result = append(result, raw)\n }\n }\n return result\n}\n\n// Truthy is the exported form of isTruthy \u2014 JavaScript\'s `Boolean(x)`\n// semantics \u2014 for generated `NewXxxProps` code lowering a conditional\n// inline-object spread condition on an `interface{}` prop (whose runtime\n// value may be a string, number, bool, \u2026). Keeps the spread bag\'s\n// inclusion test faithful to JS rather than string-biased (#1752).\nfunc Truthy(v any) bool { return isTruthy(v) }\n\n// isTruthy mirrors JavaScript\'s `Boolean(x)` for the value shapes the\n// template path actually receives \u2014 nil / false / 0 / "" are falsy.\n// Other shapes (non-empty maps, slices, structs, true) are truthy, in\n// line with JS\'s "objects are truthy" rule.\nfunc isTruthy(v any) bool {\n if v == nil {\n return false\n }\n switch x := v.(type) {\n case bool:\n return x\n case string:\n return x != ""\n case int:\n return x != 0\n case int8, int16, int32, int64:\n return reflect.ValueOf(v).Int() != 0\n case uint, uint8, uint16, uint32, uint64:\n return reflect.ValueOf(v).Uint() != 0\n case float32:\n // JS `Boolean(NaN)` is false regardless of float width \u2014 the\n // float64 arm below was the only one checking IsNaN, which\n // diverged from JS for `float32` NaN inputs (Copilot review on\n // #1445). Widening to float64 for the IsNaN check keeps the\n // two branches in lock-step.\n return x != 0 && !math.IsNaN(float64(x))\n case float64:\n return x != 0 && !math.IsNaN(x)\n }\n return true\n}\n\n// =============================================================================\n// Higher-order Array Methods\n// =============================================================================\n\n// fieldValue projects item.field for the higher-order predicate\n// helpers. The field name arrives in JS casing; structs resolve via\n// the capitalized Go convention (FieldByName inside getFieldValue),\n// maps via getFieldValue\'s case-variant lookup \u2014 the same dual\n// support Sort/Reduce gained in #1487, extended here so JSON-decoded\n// data (map items) participates instead of being silently skipped.\n// nil-safe: missing fields and nil items project to nil.\nfunc fieldValue(item any, field string) any {\n return getFieldValue(item, capitalize(field))\n}\n\n// Every returns true if every item\'s field is truthy under JS\n// `Boolean(item.field)` semantics. Mirrors JavaScript\'s\n// Array.prototype.every(item => item.field) \u2014 including being\n// vacuously true for an empty receiver.\nfunc Every(items any, field string) bool {\n v := reflect.ValueOf(items)\n if v.Kind() != reflect.Slice && v.Kind() != reflect.Array {\n return false\n }\n for i := 0; i < v.Len(); i++ {\n if !isTruthy(fieldValue(v.Index(i).Interface(), field)) {\n return false\n }\n }\n return true\n}\n\n// Some returns true if at least one item\'s field is truthy. Mirrors\n// JavaScript\'s Array.prototype.some(item => item.field).\nfunc Some(items any, field string) bool {\n v := reflect.ValueOf(items)\n if v.Kind() != reflect.Slice && v.Kind() != reflect.Array {\n return false\n }\n for i := 0; i < v.Len(); i++ {\n if isTruthy(fieldValue(v.Index(i).Interface(), field)) {\n return true\n }\n }\n return false\n}\n\n// Filter returns items where item.field == value.\n// Mirrors JavaScript\'s Array.prototype.filter(item => item.field === value).\n// Returns []any to allow chaining with other bf_* functions.\nfunc Filter(items any, field string, value any) []any {\n v := reflect.ValueOf(items)\n if v.Kind() != reflect.Slice && v.Kind() != reflect.Array {\n return nil\n }\n var result []any\n for i := 0; i < v.Len(); i++ {\n item := v.Index(i).Interface()\n if reflect.DeepEqual(fieldValue(item, field), value) {\n result = append(result, item)\n }\n }\n return result\n}\n\n// Find returns the first item where item.field == value, or nil if not found.\n// Mirrors JavaScript\'s Array.prototype.find(item => item.field === value).\nfunc Find(items any, field string, value any) any {\n v := reflect.ValueOf(items)\n if v.Kind() != reflect.Slice && v.Kind() != reflect.Array {\n return nil\n }\n for i := 0; i < v.Len(); i++ {\n item := v.Index(i).Interface()\n if reflect.DeepEqual(fieldValue(item, field), value) {\n return item\n }\n }\n return nil\n}\n\n// FindIndex returns the index of the first item where item.field == value, or -1.\n// Mirrors JavaScript\'s Array.prototype.findIndex(item => item.field === value).\nfunc FindIndex(items any, field string, value any) int {\n v := reflect.ValueOf(items)\n if v.Kind() != reflect.Slice && v.Kind() != reflect.Array {\n return -1\n }\n for i := 0; i < v.Len(); i++ {\n if reflect.DeepEqual(fieldValue(v.Index(i).Interface(), field), value) {\n return i\n }\n }\n return -1\n}\n\n// FindLast returns the last item where item.field == value, or nil if not found.\n// Mirrors JavaScript\'s Array.prototype.findLast(item => item.field === value).\nfunc FindLast(items any, field string, value any) any {\n v := reflect.ValueOf(items)\n if v.Kind() != reflect.Slice && v.Kind() != reflect.Array {\n return nil\n }\n for i := v.Len() - 1; i >= 0; i-- {\n item := v.Index(i).Interface()\n if reflect.DeepEqual(fieldValue(item, field), value) {\n return item\n }\n }\n return nil\n}\n\n// FindLastIndex returns the index of the last item where item.field == value, or -1.\n// Mirrors JavaScript\'s Array.prototype.findLastIndex(item => item.field === value).\nfunc FindLastIndex(items any, field string, value any) int {\n v := reflect.ValueOf(items)\n if v.Kind() != reflect.Slice && v.Kind() != reflect.Array {\n return -1\n }\n for i := v.Len() - 1; i >= 0; i-- {\n if reflect.DeepEqual(fieldValue(v.Index(i).Interface(), field), value) {\n return i\n }\n }\n return -1\n}\n\n// sortKeySpec is one parsed comparison key. A simple comparator has\n// one; a `||`-chained multi-key comparator has several, applied in\n// order as tie-breakers.\ntype sortKeySpec struct {\n kind string // "self" | "field"\n name string // capitalised field name, or "" for "self"\n compareType string // "numeric" | "string" | "auto"\n direction string // "asc" | "desc"\n}\n\n// Sort returns a new stable-sorted slice. Lowers\n// `Array.prototype.sort` / `Array.prototype.toSorted` (#1448 Tier B).\n// Non-mutating \u2014 JS\'s mutate-vs-new distinction is moot in SSR\n// template context (templates render a snapshot).\n//\n// Call shape (the compiler emits one 4-string group per key):\n//\n// bf_sort <items> (<keyKind> <keyName> <compareType> <direction>)+\n//\n// keyKind: "self" | "field"\n// keyName: "" when keyKind == "self"; capitalised struct field\n// name (e.g. "Price") otherwise\n// compareType: "numeric" | "string" | "auto"\n// direction: "asc" | "desc"\n//\n// The groups cover the accepted comparator catalogue: `a.f - b.f`,\n// `a - b`, `a[.f].localeCompare(b[.f])`, and relational-ternary keys\n// (`a.f > b.f ? 1 : -1` \u2192 "auto"), each `||`-chainable for multi-key\n// tie-breaks. Anything outside refuses at compile time (BF101 from the\n// JSX compiler) and never reaches this helper.\n//\n// "auto" compares numerically when both projected keys parse as\n// numbers, else lexically \u2014 mirroring the Perl `bf->sort` helper\'s\n// `looks_like_number` rule so the two template adapters stay\n// byte-equal. This diverges from JS `<`/`>` only for numeric strings.\n//\n// A future `nulls` knob can extend the per-key group without rewriting\n// existing call sites \u2014 each key already projects before comparing.\nfunc Sort(items any, spec ...string) []any {\n v := reflect.ValueOf(items)\n if v.Kind() != reflect.Slice && v.Kind() != reflect.Array {\n return nil\n }\n\n length := v.Len()\n if length == 0 {\n return []any{}\n }\n\n // Copy into a fresh []any so the sort is non-mutating regardless\n // of whether the receiver is `[]T` or `[]any`.\n result := make([]any, length)\n for i := 0; i < length; i++ {\n result[i] = v.Index(i).Interface()\n }\n\n keys := parseSortSpec(spec)\n sort.SliceStable(result, func(i, j int) bool {\n for _, k := range keys {\n ki := projectSortKey(result[i], k.kind, k.name)\n kj := projectSortKey(result[j], k.kind, k.name)\n c := compareSortKey(ki, kj, k.compareType)\n if c == 0 {\n continue // tie on this key \u2014 fall through to the next\n }\n if k.direction == "desc" {\n return c > 0\n }\n return c < 0\n }\n return false\n })\n\n return result\n}\n\n// parseSortSpec chunks the variadic operand list into 4-string key\n// groups. A trailing partial group (malformed emit) is ignored rather\n// than panicking \u2014 defensive, mirroring the helper\'s nil-safe stance.\nfunc parseSortSpec(spec []string) []sortKeySpec {\n var keys []sortKeySpec\n for i := 0; i+3 < len(spec); i += 4 {\n keys = append(keys, sortKeySpec{\n kind: spec[i],\n name: spec[i+1],\n compareType: spec[i+2],\n direction: spec[i+3],\n })\n }\n return keys\n}\n\n// compareSortKey returns -1 / 0 / 1 for two projected keys under the\n// given compare type (ascending orientation; the caller flips for\n// "desc"). "string" stringifies both (nil \u2192 "", matching the\n// documented `bf->string(undef) === ""` divergence). "auto" compares\n// numerically when both parse as numbers, else lexically.\nfunc compareSortKey(ki, kj any, compareType string) int {\n switch compareType {\n case "string":\n return strings.Compare(toString(ki), toString(kj))\n case "auto":\n ni, okI := toFloat64WithOK(ki)\n nj, okJ := toFloat64WithOK(kj)\n if okI && okJ {\n return cmpFloat(ni, nj)\n }\n return strings.Compare(toString(ki), toString(kj))\n default: // numeric\n return cmpFloat(toFloat64(ki), toFloat64(kj))\n }\n}\n\nfunc cmpFloat(a, b float64) int {\n if a < b {\n return -1\n }\n if a > b {\n return 1\n }\n return 0\n}\n\n// toFloat64WithOK reports a value\'s numeric float and whether it is\n// number-like. Genuine numeric kinds always qualify; strings qualify\n// when they parse as a float (so the "auto" compare path matches the\n// Perl `looks_like_number` rule). Everything else is non-numeric.\nfunc toFloat64WithOK(v any) (float64, bool) {\n switch n := v.(type) {\n case int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64, float32, float64:\n return toFloat64(v), true\n case string:\n f, err := strconv.ParseFloat(strings.TrimSpace(n), 64)\n if err != nil {\n return 0, false\n }\n return f, true\n default:\n return 0, false\n }\n}\n\n// projectSortKey reduces an item to the value the comparator\n// actually compares. For `keyKind == "field"` it reads the named\n// struct field; for `keyKind == "self"` (primitive arrays) it\n// returns the item unchanged.\nfunc projectSortKey(item any, keyKind, keyName string) any {\n if keyKind == "field" {\n return getFieldValue(item, keyName)\n }\n return item\n}\n\n// getFieldValue extracts a struct field value using reflection. For\n// map receivers it falls back to case-variant lookup so JSON-decoded\n// user data (`map[string]any{"price": 30}`) and PascalCase-emitted\n// test data both resolve under a single key name. (#1487)\nfunc getFieldValue(item any, field string) any {\n v := reflect.ValueOf(item)\n // Defensive IsNil guards mirror `SpreadAttrs` \u2014 keeps the helper\n // safe against typed-nil pointer / nil-interface items inside a\n // `[]any` so a single bad row doesn\'t crash the whole sort.\n if v.Kind() == reflect.Interface {\n if v.IsNil() {\n return nil\n }\n v = v.Elem()\n }\n if v.Kind() == reflect.Ptr {\n if v.IsNil() {\n return nil\n }\n v = v.Elem()\n }\n\n if v.Kind() == reflect.Map {\n keyType := v.Type().Key()\n if keyType.Kind() != reflect.String {\n return nil\n }\n // Convert the lookup string to the map\'s actual key type so\n // maps keyed by a named string type (`type Key string`) don\'t\n // panic with `value of type string is not assignable to type X`.\n lookup := func(s string) (any, bool) {\n k := reflect.ValueOf(s).Convert(keyType)\n if mv := v.MapIndex(k); mv.IsValid() {\n return mv.Interface(), true\n }\n return nil, false\n }\n if r, ok := lookup(field); ok {\n return r\n }\n if cap := capitalize(field); cap != field {\n if r, ok := lookup(cap); ok {\n return r\n }\n }\n if low := decapitalize(field); low != field {\n if r, ok := lookup(low); ok {\n return r\n }\n }\n // All-lowercase fallback: a Go-initialism field projects as an\n // all-caps key (`id` \u2192 `ID`), and `decapitalize("ID")` only\n // lowers the first char (`iD`), so the JS-keyed map ("id") still\n // misses. Try the fully-lowered key last to resolve it.\n if lower := strings.ToLower(field); lower != field && lower != decapitalize(field) {\n if r, ok := lookup(lower); ok {\n return r\n }\n }\n return nil\n }\n\n if v.Kind() != reflect.Struct {\n return nil\n }\n\n fieldVal := v.FieldByName(field)\n if !fieldVal.IsValid() {\n // Case-variant fallback: the evaluator carries the JS field name\n // (`id` / `url`) against a Go-capitalised struct field (`ID` / `URL`),\n // which exact `FieldByName` misses and the initialism rules can\'t be\n // reproduced char-for-char here. Match case-insensitively instead \u2014\n // `FieldByNameFunc` returns the zero Value (\u2192 nil) for an ambiguous\n // match, so it stays safe. The legacy bf_sort/bf_reduce pass an\n // already-capitalised name, so they hit the exact match above and never\n // reach this fallback.\n fieldVal = v.FieldByNameFunc(func(n string) bool { return strings.EqualFold(n, field) })\n if !fieldVal.IsValid() {\n return nil\n }\n }\n return fieldVal.Interface()\n}\n\n// capitalize uppercases the first character of a string.\nfunc capitalize(s string) string {\n if s == "" {\n return s\n }\n return strings.ToUpper(s[:1]) + s[1:]\n}\n\n// decapitalize lowercases the first character of a string. Used by\n// `getFieldValue`\'s map-receiver fallback when the projected key\n// name is PascalCase but the receiver carries lowercase JS-style\n// keys (the inverse of the `capitalize` lookup).\nfunc decapitalize(s string) string {\n if s == "" {\n return s\n }\n return strings.ToLower(s[:1]) + s[1:]\n}\n\n// Reduce folds an array into a scalar via the arithmetic-fold\n// catalogue (#1448 Tier C). It lowers `Array.prototype.reduce(fn, init)`\n// and `Array.prototype.reduceRight(fn, init)` for the shapes\n// `(acc, x) => acc <op> x` and `(acc, x) => acc <op> x.field`:\n//\n// bf_reduce <items> "<op>" "<keyKind>" "<keyName>" "<type>" "<init>" "<direction>"\n//\n// direction: "left" (reduce) | "right" (reduceRight). Only changes the\n// result for string concatenation; numeric folds commute.\n//\n// op: "+" | "*"\n// keyKind: "self" | "field"\n// keyName: "" when keyKind == "self"; capitalised struct field name\n// (e.g. "Duration") otherwise\n// type: "numeric" | "string"\n// init: the fold\'s start value \u2014 the compiler emits the *decoded*\n// seed, so numeric inits arrive as canonical decimal\n// (`1_000`/`0x10` already normalised to `1000`/`16`) that\n// ParseFloat accepts, and string inits arrive as escape-free\n// contents\n//\n// Numeric folds accumulate as float64; each projected key is read via\n// `toFloat64WithOK`, so numeric *strings* ("5" \u2192 5) parse and\n// non-numeric values fold as 0 \u2014 matching Perl\'s\n// `looks_like_number ? $n : 0` so the two template adapters stay\n// byte-equal. String folds concatenate (toString per projected key,\n// matching the documented `bf->string(undef) === ""` convention). The\n// init seeds the accumulator, so an empty receiver returns the init\n// unchanged \u2014 exactly like JS `reduce(fn, init)`. Anything outside the\n// catalogue refuses at compile time (BF101 from the JSX compiler) and\n// never reaches here.\n//\n// Two documented divergences from the JS / Hono path, both rare and\n// mirroring the `bf_sort` "auto" caveat:\n// - float64 stringification differs for sums whose binary expansion\n// isn\'t exact (e.g. 0.1 + 0.2);\n// - numeric-*string* keys fold numerically here, but JS `+`\n// string-concatenates once an operand is a string, so\n// numeric-string data can render differently under CSR.\n//\n// Genuine numbers \u2014 the common SSR case \u2014 agree across all three.\nfunc Reduce(items any, op, keyKind, keyName, typ, init, direction string) any {\n v := reflect.ValueOf(items)\n isSlice := v.Kind() == reflect.Slice || v.Kind() == reflect.Array\n\n // `direction == "right"` (reduceRight) folds right-to-left. Only\n // observable for string concatenation \u2014 numeric sum / product are\n // commutative, so the order doesn\'t change the result there. Build a\n // start/stop/step triple so both folds share one loop shape.\n start, stop, step := 0, 0, 1\n if isSlice {\n stop = v.Len()\n if direction == "right" {\n start, stop, step = v.Len()-1, -1, -1\n }\n }\n\n if typ == "string" {\n acc := init\n if isSlice {\n for i := start; i != stop; i += step {\n key := projectSortKey(v.Index(i).Interface(), keyKind, keyName)\n acc += toString(key)\n }\n }\n return acc\n }\n\n // numeric fold\n acc, _ := strconv.ParseFloat(strings.TrimSpace(init), 64)\n if isSlice {\n for i := start; i != stop; i += step {\n key := projectSortKey(v.Index(i).Interface(), keyKind, keyName)\n // `toFloat64WithOK` parses numeric *strings* ("5" \u2192 5) and\n // returns 0 for non-numeric values \u2014 mirroring Perl\'s\n // `looks_like_number ? $n : 0` so numeric-string data folds\n // byte-equal across adapters (the same rule `bf_sort`\'s\n // "auto" compare uses). Plain `toFloat64` would zero "5".\n n, _ := toFloat64WithOK(key)\n if op == "*" {\n acc *= n\n } else {\n acc += n\n }\n }\n }\n return acc\n}\n\n// =============================================================================\n// HTML/Template Helpers\n// =============================================================================\n\n// Comment returns an HTML comment string for hydration markers.\n// The "bf-" prefix is automatically added.\nfunc Comment(content string) template.HTML {\n return template.HTML("<!--bf-" + content + "-->")\n}\n\n// TextStart returns an HTML comment start marker for reactive text expressions.\n// Format: <!--bf:slotId-->\nfunc TextStart(slotId string) template.HTML {\n return template.HTML("<!--bf:" + slotId + "-->")\n}\n\n// TextEnd returns an HTML comment end marker for reactive text expressions.\n// Format: <!--/-->\nfunc TextEnd() template.HTML {\n return "<!--/-->"\n}\n\n// ScopeComment emits a fragment-rooted scope marker. See spec/compiler.md\n// "Slot identity" for the wire format. Loud-fails on marshal errors\n// (same policy as JSON / BfPropsAttr).\nfunc ScopeComment(props interface{}) (template.HTML, error) {\n scopeID := getStringField(props, "ScopeID")\n hostSegment := ""\n if host := getStringField(props, "BfParent"); host != "" {\n mount := getStringField(props, "BfMount")\n hostSegment = "|h=" + host + "|m=" + mount\n }\n propsJSON := ""\n if getBoolField(props, "BfIsRoot") {\n pJSON, err := json.Marshal(props)\n if err != nil {\n return "", err\n }\n propsJSON = "|" + string(pJSON)\n }\n return template.HTML("<!--bf-scope:" + scopeID + hostSegment + propsJSON + "-->"), nil\n}\n\n// TemplateFuncMap returns the helpers that need access to the executing\n// template set itself, closed over the *template.Template the component\n// defines are parsed into. Register it alongside FuncMap BEFORE parsing:\n//\n// t := template.New("")\n// t.Funcs(bf.FuncMap()).Funcs(bf.TemplateFuncMap(t))\n// template.Must(t.Parse(src))\n//\n// bf_tmpl executes a named define from the same set and returns its\n// output \u2014 used for the per-call-site children defines the Go adapter\n// emits when JSX children passed to an imported component contain\n// template actions (nested components, dynamic text) and therefore\n// cannot be baked to a static HTML string (#1896). Reentrant execution\n// of an html/template set from inside a FuncMap function is safe: the\n// escape analysis over every define completes before the outer\n// Execute begins evaluating.\nfunc TemplateFuncMap(t *template.Template) template.FuncMap {\n return template.FuncMap{\n "bf_tmpl": func(name string, data interface{}) (template.HTML, error) {\n var buf bytes.Buffer\n if err := t.ExecuteTemplate(&buf, name, data); err != nil {\n return "", err\n }\n return template.HTML(buf.String()), nil\n },\n }\n}\n\n// WithChildren returns a shallow copy of a component Props struct with its\n// Children field replaced by the given pre-rendered fragment (#1896). The\n// props value stays by-value semantics: callers\' originals are untouched.\n// A props type without a Children field passes through unchanged \u2014 the\n// child template then simply has no children to render, matching the\n// pre-#1896 behaviour.\nfunc WithChildren(props interface{}, children template.HTML) (interface{}, error) {\n v := reflect.ValueOf(props)\n if v.Kind() == reflect.Ptr {\n v = v.Elem()\n }\n if v.Kind() != reflect.Struct {\n return props, nil\n }\n field := v.FieldByName("Children")\n if !field.IsValid() {\n return props, nil\n }\n copyPtr := reflect.New(v.Type())\n copyPtr.Elem().Set(v)\n target := copyPtr.Elem().FieldByName("Children")\n switch {\n case target.Kind() == reflect.Interface:\n target.Set(reflect.ValueOf(children))\n case target.Kind() == reflect.String:\n // Covers both `string` and `template.HTML`-typed fields.\n target.SetString(string(children))\n default:\n return props, fmt.Errorf("bf_with_children: unsupported Children field type %s", target.Type())\n }\n return copyPtr.Elem().Interface(), nil\n}\n\n// PortalHTML parses and executes a template string with the provided data.\n// Used for rendering dynamic portal content where the template string\n// contains Go template expressions (e.g., {{if .Open}}open{{end}}).\n//\n// The template string is parsed fresh each time to support dynamic content.\n// Standard Go template functions (if, range, eq, etc.) are available.\nfunc PortalHTML(data interface{}, tmplStr string) template.HTML {\n // Create a new template with the FuncMap for custom functions\n t, err := template.New("portal").Funcs(FuncMap()).Parse(tmplStr)\n if err != nil {\n // Return error message as HTML comment for debugging\n return template.HTML("<!-- bfPortalHTML error: " + err.Error() + " -->")\n }\n\n var buf bytes.Buffer\n if err := t.Execute(&buf, data); err != nil {\n return template.HTML("<!-- bfPortalHTML exec error: " + err.Error() + " -->")\n }\n\n return template.HTML(buf.String())\n}\n\n// =============================================================================\n// Portal Collection\n// =============================================================================\n\n// PortalContent represents a single portal\'s content to be rendered at body end.\ntype PortalContent struct {\n ID string // Unique portal ID for hydration matching\n OwnerID string // Owner scope ID for find() support\n Content template.HTML // Portal HTML content\n}\n\n// PortalCollector collects portal content during template rendering.\n// Portal content is rendered at </body> to avoid z-index issues.\ntype PortalCollector struct {\n portals []PortalContent\n counter int\n}\n\n// NewPortalCollector creates a new PortalCollector.\nfunc NewPortalCollector() *PortalCollector {\n return &PortalCollector{\n portals: []PortalContent{},\n counter: 0,\n }\n}\n\n// Add registers portal content to be rendered at body end.\nfunc (pc *PortalCollector) Add(ownerID string, content template.HTML) string {\n pc.counter++\n id := "bf-portal-" + strconv.Itoa(pc.counter)\n pc.portals = append(pc.portals, PortalContent{\n ID: id,\n OwnerID: ownerID,\n Content: content,\n })\n return "" // Return empty string for template use\n}\n\n// Render outputs all collected portals as HTML.\n// Each portal is wrapped in a div with bf-pi (portal ID) and bf-po (portal owner).\nfunc (pc *PortalCollector) Render() template.HTML {\n if pc == nil || len(pc.portals) == 0 {\n return ""\n }\n var buf strings.Builder\n for _, p := range pc.portals {\n buf.WriteString(`<div bf-pi="`)\n buf.WriteString(p.ID)\n buf.WriteString(`" bf-po="`)\n buf.WriteString(p.OwnerID)\n buf.WriteString(`">`)\n buf.WriteString(string(p.Content))\n buf.WriteString("</div>\\n")\n }\n return template.HTML(buf.String())\n}\n\n// =============================================================================\n// Script Collection\n// =============================================================================\n\n// ScriptCollector collects client scripts with deduplication.\n// It preserves insertion order for deterministic output.\ntype ScriptCollector struct {\n scripts map[string]bool\n order []string\n}\n\n// NewScriptCollector creates a new ScriptCollector.\nfunc NewScriptCollector() *ScriptCollector {\n return &ScriptCollector{\n scripts: make(map[string]bool),\n order: []string{},\n }\n}\n\n// Register adds a script source to the collection.\n// Duplicate scripts are ignored (only first registration counts).\nfunc (sc *ScriptCollector) Register(src string) string {\n if sc.scripts[src] {\n return "" // Already registered\n }\n sc.scripts[src] = true\n sc.order = append(sc.order, src)\n return "" // Return empty string for template use\n}\n\n// Scripts returns all registered scripts in insertion order.\nfunc (sc *ScriptCollector) Scripts() []string {\n return sc.order\n}\n\n// BfScripts generates script tags for all registered scripts.\n// Returns HTML safe for embedding in templates.\nfunc BfScripts(collector *ScriptCollector) template.HTML {\n if collector == nil {\n return ""\n }\n var result strings.Builder\n for _, src := range collector.Scripts() {\n result.WriteString(`<script type="module" src="`)\n result.WriteString(src)\n result.WriteString(`"></script>`)\n result.WriteString("\\n")\n }\n return template.HTML(result.String())\n}\n\n// =============================================================================\n// Component Renderer\n// =============================================================================\n\n// RenderContext contains all data needed to render a component page.\n// The layout function receives this context to build the final HTML.\ntype RenderContext struct {\n // ComponentName is the template name being rendered\n ComponentName string\n\n // Props is the component props (for layout to access if needed)\n Props interface{}\n\n // ComponentHTML is the rendered component template output\n ComponentHTML template.HTML\n\n // Portals contains collected portal content to render at body end\n Portals template.HTML\n\n // Scripts contains the collected JS script tags\n Scripts template.HTML\n\n // Title is the page title (defaults to "{ComponentName} - BarefootJS")\n Title string\n\n // Heading is the page heading. Empty string means no heading.\n Heading string\n\n // Extra holds additional user-defined data for the layout\n Extra map[string]interface{}\n}\n\n// LayoutFunc renders the final HTML page given the render context.\ntype LayoutFunc func(ctx *RenderContext) string\n\n// Renderer renders BarefootJS components with a customizable layout.\ntype Renderer struct {\n templates *template.Template\n layout LayoutFunc\n}\n\n// NewRenderer creates a Renderer with the given templates and layout function.\n//\n// Example usage:\n//\n// renderer := bf.NewRenderer(templates, func(ctx *bf.RenderContext) string {\n// return fmt.Sprintf(`<!DOCTYPE html>\n// <html>\n// <head><title>%s</title></head>\n// <body>%s%s</body>\n// </html>`, ctx.Title, ctx.ComponentHTML, ctx.Scripts)\n// })\nfunc NewRenderer(tmpl *template.Template, layout LayoutFunc) *Renderer {\n return &Renderer{\n templates: tmpl,\n layout: layout,\n }\n}\n\n// RenderOptions configures a single render call.\ntype RenderOptions struct {\n // ComponentName is the template name to render (required)\n ComponentName string\n\n // Props is the component props (must be a pointer to struct with Scripts field)\n Props interface{}\n\n // Title is the page title. If empty, defaults to "{ComponentName} - BarefootJS"\n Title string\n\n // Heading is the page heading. If empty, no heading is shown.\n Heading string\n\n // Extra holds additional data to pass to the layout\n Extra map[string]interface{}\n}\n\n// Render renders a component to a full HTML page using the configured layout.\n// Child component props are automatically detected (any slice field with ScopeID/Scripts).\n// renderTemplateErrorPanel formats a Go template execution error into a\n// fragment of HTML that\'s visible in the browser. The panel is\n// HTML-escaped so a faulty template name (anything from `template:\n// "..."`) can\'t smuggle markup back into the page. Keep the styling\n// inline so the panel surfaces even when the project\'s CSS hasn\'t\n// loaded yet (e.g. the failure aborted before the stylesheet links\n// emitted).\n//\n// Surfaced for the #1442 echo repro: a template referencing\n// `.Todo.Done` (instead of the range dot\'s `.Done`) used to fail\n// silently \u2014 Go\'s html/template aborted mid-stream, the partial body\n// flushed as a 200, and the user saw a truncated list with no console\n// signal. With this panel they get the template name, the error\n// message, and a "what to look at" hint inline.\nfunc renderTemplateErrorPanel(componentName string, err error) string {\n return `<div style="margin:1em 0;padding:1em;border:2px solid #d33;background:#fff5f5;color:#900;font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;font-size:13px;line-height:1.5"><strong style="display:block;margin-bottom:.5em">Template error in <code>` +\n template.HTMLEscapeString(componentName) +\n `</code></strong><pre style="margin:0;white-space:pre-wrap;word-break:break-word">` +\n template.HTMLEscapeString(err.Error()) +\n `</pre><div style="margin-top:.75em;font-size:12px;opacity:.7">Common cause: a JSX expression referenced a name the adapter could not resolve to a struct field. Open the matching <code>dist/templates/*.tmpl</code> for the unresolved reference, then fix the source component.</div></div>`\n}\n\n// renderComponentInto wires a component\'s props (script/portal collectors,\n// child-slot scope ids + hydration, root marking) against the PROVIDED\n// collectors and returns just the component\'s HTML \u2014 no layout. Both Render\n// (one fresh collector pair per page) and RenderFragment (a shared pair across\n// several islands) funnel through here so their wiring stays identical.\nfunc (r *Renderer) renderComponentInto(opts RenderOptions, scriptCollector *ScriptCollector, portalCollector *PortalCollector) template.HTML {\n // Inject the shared collectors into the props.\n setScriptsField(opts.Props, scriptCollector)\n setPortalsField(opts.Props, portalCollector)\n\n // Auto-detect and process child component props (slices)\n childSlices := findChildComponentSlices(opts.Props)\n for _, slice := range childSlices {\n setScopeIDsOnSlice(slice)\n setScriptsOnSlice(slice, scriptCollector)\n setPortalsOnSlice(slice, portalCollector)\n setBoolOnSlice(slice, "BfIsChild", true)\n }\n\n // Auto-detect and process single child component props\n singleChildren := findSingleChildComponents(opts.Props)\n for _, child := range singleChildren {\n setScopeIDOnSingle(child)\n setScriptsOnSingle(child, scriptCollector)\n setPortalsOnSingle(child, portalCollector)\n setBoolField(child, "BfIsChild", true)\n }\n\n // Mark the root component so BfPropsAttr emits bf-p only for it\n setBoolField(opts.Props, "BfIsRoot", true)\n\n // Render the component template.\n //\n // Errors here are NOT silently dropped. The original implementation\n // ignored the return value of `ExecuteTemplate`, which masked a real\n // onboarding failure mode: a template referencing a non-existent\n // field (`.Todo.Done` instead of the range dot\'s `.Done`) caused\n // html/template to abort mid-stream, the partial output got\n // returned, and the HTTP server happily flushed a 200 with a\n // truncated body. No error log, no signal \u2014 the user just saw a\n // blank list (#1442 echo TodoApp repro).\n //\n // Now we capture the error and replace the partial output with a\n // visible inline panel (dev mode) or a fenced error comment\n // (production), so the cause is on-screen and grep-able in logs.\n // Either way the renderer also writes to stderr so structured log\n // aggregators see it.\n var componentBuf strings.Builder\n if err := r.templates.ExecuteTemplate(&componentBuf, opts.ComponentName, opts.Props); err != nil {\n fmt.Fprintf(os.Stderr, "barefoot: template %q failed to render: %v\\n", opts.ComponentName, err)\n // Preserve whatever the template did manage to emit before\n // failing (Go\'s text/template flushes incrementally), but\n // follow it with a clearly-marked error block so the user\n // notices something is wrong instead of seeing a silent\n // truncation.\n componentBuf.WriteString(renderTemplateErrorPanel(opts.ComponentName, err))\n }\n\n return template.HTML(componentBuf.String())\n}\n\nfunc (r *Renderer) Render(opts RenderOptions) string {\n // One script + portal collector pair for the whole page.\n scriptCollector := NewScriptCollector()\n portalCollector := NewPortalCollector()\n\n componentHTML := r.renderComponentInto(opts, scriptCollector, portalCollector)\n\n // Determine title (default: "{ComponentName} - BarefootJS")\n title := opts.Title\n if title == "" {\n title = opts.ComponentName + " - BarefootJS"\n }\n\n // Heading (empty means no heading)\n heading := opts.Heading\n\n // Build render context\n ctx := &RenderContext{\n ComponentName: opts.ComponentName,\n Props: opts.Props,\n ComponentHTML: componentHTML,\n Portals: portalCollector.Render(),\n Scripts: BfScripts(scriptCollector),\n Title: title,\n Heading: heading,\n Extra: opts.Extra,\n }\n\n return r.layout(ctx)\n}\n\n// RenderFragment renders a single island subtree into the caller-provided\n// script and portal collectors and returns just its HTML \u2014 no page layout.\n//\n// It exists for hand-authored "region shell" pages (the `@barefootjs/router`\n// showcase): a layout that places several independent islands \u2014 e.g. a header\n// ThemeToggle, an `<aside bf-region>` Sidebar, and a `<PageShell>` wrapping the\n// route content \u2014 must collect ALL their scripts and portals into ONE place so\n// the runtime (`barefoot.js`) and each island\'s client JS are emitted exactly\n// once and share a single reactive instance. Render each island with the same\n// collectors, splice the returned HTML into the shell, then emit\n// `BfScripts(sc)` and `pc.Render()` once at the end of the document.\n//\n// Each fragment is treated as its own root (it emits `bf-p` like any top-level\n// island); nested children declared in its props are wired as children, exactly\n// as in Render.\nfunc (r *Renderer) RenderFragment(opts RenderOptions, scriptCollector *ScriptCollector, portalCollector *PortalCollector) template.HTML {\n return r.renderComponentInto(opts, scriptCollector, portalCollector)\n}\n\n// setScriptsField sets the Scripts field on a struct using reflection.\nfunc setScriptsField(v interface{}, collector *ScriptCollector) {\n val := reflect.ValueOf(v)\n if val.Kind() == reflect.Ptr {\n val = val.Elem()\n }\n if val.Kind() != reflect.Struct {\n return\n }\n field := val.FieldByName("Scripts")\n if field.IsValid() && field.CanSet() {\n field.Set(reflect.ValueOf(collector))\n }\n}\n\n// setPortalsField sets the Portals field on a struct using reflection.\nfunc setPortalsField(v interface{}, collector *PortalCollector) {\n val := reflect.ValueOf(v)\n if val.Kind() == reflect.Ptr {\n val = val.Elem()\n }\n if val.Kind() != reflect.Struct {\n return\n }\n field := val.FieldByName("Portals")\n if field.IsValid() && field.CanSet() {\n field.Set(reflect.ValueOf(collector))\n }\n}\n\n// getStringField extracts a string field from a struct using reflection.\nfunc setBoolField(v interface{}, fieldName string, val bool) {\n rv := reflect.ValueOf(v)\n if rv.Kind() == reflect.Ptr {\n rv = rv.Elem()\n }\n if rv.Kind() != reflect.Struct {\n return\n }\n field := rv.FieldByName(fieldName)\n if field.IsValid() && field.CanSet() && field.Kind() == reflect.Bool {\n field.SetBool(val)\n }\n}\n\nfunc getBoolField(v interface{}, fieldName string) bool {\n val := reflect.ValueOf(v)\n if val.Kind() == reflect.Ptr {\n val = val.Elem()\n }\n if val.Kind() != reflect.Struct {\n return false\n }\n field := val.FieldByName(fieldName)\n if !field.IsValid() || field.Kind() != reflect.Bool {\n return false\n }\n return field.Bool()\n}\n\nfunc getStringField(v interface{}, fieldName string) string {\n val := reflect.ValueOf(v)\n if val.Kind() == reflect.Ptr {\n val = val.Elem()\n }\n if val.Kind() != reflect.Struct {\n return ""\n }\n field := val.FieldByName(fieldName)\n if !field.IsValid() || field.Kind() != reflect.String {\n return ""\n }\n return field.String()\n}\n\n// scopeIDChars is the alphabet for auto-generated ScopeID suffixes. It\n// mirrors the `randomID` helper the go-template adapter emits into the\n// generated New<Component>Props constructors so runtime-assigned and\n// constructor-assigned ids are indistinguishable.\nconst scopeIDChars = "abcdefghijklmnopqrstuvwxyz0123456789"\n\n// randomScopeSuffix returns a random lowercase-alphanumeric string of\n// length n. math/rand (auto-seeded since Go 1.20) is sufficient here: the\n// suffix only needs to be unique enough to keep a page\'s bf-s scope ids\n// from colliding, not cryptographically unpredictable.\nfunc randomScopeSuffix(n int) string {\n b := make([]byte, n)\n for i := range b {\n b[i] = scopeIDChars[rand.Intn(len(scopeIDChars))]\n }\n return string(b)\n}\n\n// scopeIDPrefix derives the human-readable ScopeID prefix from a child\n// component\'s type, e.g. `TodoItemProps` \u2192 `TodoItem`. Matches the\n// `"<Component>_" + randomID(6)` shape the generated constructors use.\nfunc scopeIDPrefix(t reflect.Type) string {\n for t.Kind() == reflect.Ptr {\n t = t.Elem()\n }\n return strings.TrimSuffix(t.Name(), "Props")\n}\n\n// assignScopeID fills a child component\'s ScopeID with a generated id when\n// the caller left it empty, so application code doesn\'t have to mint scope\n// ids by hand (the parent\'s New<Component>Props constructor does the same\n// for components built through it). A non-empty ScopeID is left untouched,\n// so callers can still pin a stable id when they need one.\nfunc assignScopeID(structVal reflect.Value, prefix string) {\n field := structVal.FieldByName("ScopeID")\n if !field.IsValid() || !field.CanSet() || field.Kind() != reflect.String {\n return\n }\n if field.String() != "" {\n return\n }\n id := randomScopeSuffix(6)\n if prefix != "" {\n id = prefix + "_" + id\n }\n field.SetString(id)\n}\n\n// setScopeIDsOnSlice assigns a generated ScopeID to every child in a slice\n// whose ScopeID is empty.\nfunc setScopeIDsOnSlice(slice interface{}) {\n v := reflect.ValueOf(slice)\n if v.Kind() != reflect.Slice {\n return\n }\n prefix := scopeIDPrefix(v.Type().Elem())\n for i := 0; i < v.Len(); i++ {\n item := v.Index(i)\n if item.Kind() == reflect.Ptr {\n if item.IsNil() {\n continue\n }\n item = item.Elem()\n }\n if item.Kind() == reflect.Struct {\n assignScopeID(item, prefix)\n }\n }\n}\n\n// setScopeIDOnSingle assigns a generated ScopeID to a single child\n// component when its ScopeID is empty.\nfunc setScopeIDOnSingle(child interface{}) {\n v := reflect.ValueOf(child)\n if v.Kind() == reflect.Ptr {\n v = v.Elem()\n }\n if v.Kind() != reflect.Struct {\n return\n }\n assignScopeID(v, scopeIDPrefix(v.Type()))\n}\n\n// findChildComponentSlices finds slice fields containing child component props.\n// Child props are identified by having ScopeID and Scripts fields.\nfunc findChildComponentSlices(props interface{}) []interface{} {\n var result []interface{}\n\n val := reflect.ValueOf(props)\n if val.Kind() == reflect.Ptr {\n val = val.Elem()\n }\n if val.Kind() != reflect.Struct {\n return result\n }\n\n for i := 0; i < val.NumField(); i++ {\n field := val.Field(i)\n if field.Kind() != reflect.Slice || field.Len() == 0 {\n continue\n }\n\n elem := field.Index(0)\n if elem.Kind() == reflect.Ptr {\n elem = elem.Elem()\n }\n if elem.Kind() != reflect.Struct {\n continue\n }\n\n hasScopeID := elem.FieldByName("ScopeID").IsValid()\n hasScripts := elem.FieldByName("Scripts").IsValid()\n\n if hasScopeID && hasScripts {\n result = append(result, field.Interface())\n }\n }\n\n return result\n}\n\n// setScriptsOnSlice sets Scripts on all items in a slice.\nfunc setScriptsOnSlice(slice interface{}, collector *ScriptCollector) {\n val := reflect.ValueOf(slice)\n if val.Kind() != reflect.Slice {\n return\n }\n for i := 0; i < val.Len(); i++ {\n item := val.Index(i)\n if item.Kind() == reflect.Ptr {\n item = item.Elem()\n }\n if item.Kind() == reflect.Struct {\n field := item.FieldByName("Scripts")\n if field.IsValid() && field.CanSet() {\n field.Set(reflect.ValueOf(collector))\n }\n }\n }\n}\n\n// setBoolOnSlice sets a bool field on all items in a slice.\nfunc setBoolOnSlice(slice interface{}, fieldName string, val bool) {\n v := reflect.ValueOf(slice)\n if v.Kind() != reflect.Slice {\n return\n }\n for i := 0; i < v.Len(); i++ {\n item := v.Index(i)\n if item.Kind() == reflect.Ptr {\n item = item.Elem()\n }\n if item.Kind() == reflect.Struct {\n field := item.FieldByName(fieldName)\n if field.IsValid() && field.CanSet() && field.Kind() == reflect.Bool {\n field.SetBool(val)\n }\n }\n }\n}\n\n// setPortalsOnSlice sets Portals on all items in a slice.\nfunc setPortalsOnSlice(slice interface{}, collector *PortalCollector) {\n val := reflect.ValueOf(slice)\n if val.Kind() != reflect.Slice {\n return\n }\n for i := 0; i < val.Len(); i++ {\n item := val.Index(i)\n if item.Kind() == reflect.Ptr {\n item = item.Elem()\n }\n if item.Kind() == reflect.Struct {\n field := item.FieldByName("Portals")\n if field.IsValid() && field.CanSet() {\n field.Set(reflect.ValueOf(collector))\n }\n }\n }\n}\n\n// findSingleChildComponents finds single struct fields containing child component props.\n// Child props are identified by having ScopeID and Scripts fields.\nfunc findSingleChildComponents(props interface{}) []interface{} {\n var result []interface{}\n\n val := reflect.ValueOf(props)\n if val.Kind() == reflect.Ptr {\n val = val.Elem()\n }\n if val.Kind() != reflect.Struct {\n return result\n }\n\n for i := 0; i < val.NumField(); i++ {\n field := val.Field(i)\n\n // Handle pointer to struct\n if field.Kind() == reflect.Ptr {\n if field.IsNil() {\n continue\n }\n field = field.Elem()\n }\n\n // Skip non-struct fields (slices handled by findChildComponentSlices)\n if field.Kind() != reflect.Struct {\n continue\n }\n\n hasScopeID := field.FieldByName("ScopeID").IsValid()\n hasScripts := field.FieldByName("Scripts").IsValid()\n\n if hasScopeID && hasScripts {\n result = append(result, field.Addr().Interface())\n }\n }\n\n return result\n}\n\n// setScriptsOnSingle sets Scripts on a single struct child component.\nfunc setScriptsOnSingle(child interface{}, collector *ScriptCollector) {\n val := reflect.ValueOf(child)\n if val.Kind() == reflect.Ptr {\n val = val.Elem()\n }\n if val.Kind() == reflect.Struct {\n field := val.FieldByName("Scripts")\n if field.IsValid() && field.CanSet() {\n field.Set(reflect.ValueOf(collector))\n }\n }\n}\n\n// setPortalsOnSingle sets Portals on a single struct child component.\nfunc setPortalsOnSingle(child interface{}, collector *PortalCollector) {\n val := reflect.ValueOf(child)\n if val.Kind() == reflect.Ptr {\n val = val.Elem()\n }\n if val.Kind() == reflect.Struct {\n field := val.FieldByName("Portals")\n if field.IsValid() && field.CanSet() {\n field.Set(reflect.ValueOf(collector))\n }\n }\n}\n\n// =============================================================================\n// Internal Helpers\n// =============================================================================\n\nfunc toFloat64(v any) float64 {\n switch n := v.(type) {\n case int:\n return float64(n)\n case int8:\n return float64(n)\n case int16:\n return float64(n)\n case int32:\n return float64(n)\n case int64:\n return float64(n)\n case uint:\n return float64(n)\n case uint8:\n return float64(n)\n case uint16:\n return float64(n)\n case uint32:\n return float64(n)\n case uint64:\n return float64(n)\n case float32:\n return float64(n)\n case float64:\n return n\n default:\n return 0\n }\n}\n\nfunc toInt(v any) int {\n switch n := v.(type) {\n case int:\n return n\n case int8:\n return int(n)\n case int16:\n return int(n)\n case int32:\n return int(n)\n case int64:\n return int(n)\n case uint:\n return int(n)\n case uint8:\n return int(n)\n case uint16:\n return int(n)\n case uint32:\n return int(n)\n case uint64:\n return int(n)\n case float32:\n return int(n)\n case float64:\n return int(n)\n default:\n return 0\n }\n}\n\nfunc isIntLike(v any) bool {\n switch v.(type) {\n case int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64:\n return true\n default:\n return false\n }\n}\n\nfunc toString(v any) string {\n switch s := v.(type) {\n case string:\n return s\n case int:\n return strconv.Itoa(s)\n case int64:\n return strconv.FormatInt(s, 10)\n case float64:\n return strconv.FormatFloat(s, \'f\', -1, 64)\n case bool:\n return strconv.FormatBool(s)\n default:\n return ""\n }\n}\n\n// =============================================================================\n// searchParams() \u2014 request-scoped environment signal (router v0.5, #1922)\n// =============================================================================\n\n// SearchParams is the SSR view of the request query string behind the\n// reactive searchParams() environment signal. The route handler builds it\n// from the request URL and assigns it to the component\'s SearchParams input\n// field; the generated template reads it via `.SearchParams.Get "key"`.\n//\n// The zero value is an empty query (url.Values.Get tolerates a nil map), so a\n// render with no request query \u2014 e.g. the adapter conformance harness, which\n// issues no query string \u2014 resolves every key to "", which the template\'s\n// `or`/`??` fallback turns into the author\'s default.\ntype SearchParams struct {\n values url.Values\n}\n\n// NewSearchParams parses a raw query string (with or without a leading "?")\n// into a SearchParams. A malformed query yields an empty set rather than an\n// error, mirroring the browser\'s URLSearchParams, which never throws on junk.\n//\n// Typical handler use (net/http):\n//\n// in := MyComponentInput{SearchParams: bf.NewSearchParams(r.URL.RawQuery)}\nfunc NewSearchParams(raw string) SearchParams {\n raw = strings.TrimPrefix(raw, "?")\n values, err := url.ParseQuery(raw)\n if err != nil {\n values = url.Values{}\n }\n return SearchParams{values: values}\n}\n\n// Get returns the first value associated with key, or "" when the key is\n// absent. This mirrors url.Values.Get, which also returns "" for a\n// present-but-empty value (`?sort=`). Safe on the zero value (nil map).\n//\n// This is not byte-for-byte URLSearchParams.get under the template\'s `??`\n// lowering. JS distinguishes absent (`null`) from present-but-empty (`""`):\n// `null ?? d` yields the default, but `"" ?? d` keeps the empty string. The\n// Go adapter lowers `??` to the `or` builtin \u2014 Go templates have no\n// null-coalescing operator \u2014 so here BOTH an absent key and a present-but-\n// empty value fall back to the author\'s default. The conformance fixture only\n// exercises the absent-key default, where the two runtimes agree; the\n// empty-string divergence is the same general `?? \u2192 or` limitation that\n// applies to any `x ?? default` the Go adapter lowers.\nfunc (s SearchParams) Get(key string) string {\n return s.values.Get(key)\n}\n';
27332
+ bfGoSource = '// Package bf provides runtime helper functions for BarefootJS Go templates.\n// These functions mirror JavaScript behavior for consistent SSR output.\npackage bf\n\nimport (\n "bytes"\n "encoding/json"\n "fmt"\n "html/template"\n "math"\n "math/rand"\n "net/url"\n "os"\n "reflect"\n "sort"\n "strconv"\n "strings"\n "unicode/utf8"\n)\n\n// FuncMap returns a template.FuncMap with all BarefootJS helper functions.\n// Usage:\n//\n// tmpl := template.New("").Funcs(bf.FuncMap())\nfunc FuncMap() template.FuncMap {\n return template.FuncMap{\n // Arithmetic\n "bf_add": Add,\n "bf_sub": Sub,\n "bf_mul": Mul,\n "bf_div": Div,\n "bf_mod": Mod,\n "bf_neg": Neg,\n "bf_min": Min,\n "bf_max": Max,\n\n // String\n "bf_lower": Lower,\n "bf_upper": Upper,\n "bf_trim": Trim,\n "bf_contains": Contains,\n "bf_join": Join,\n "bf_split": Split,\n "bf_starts_with": StartsWith,\n "bf_ends_with": EndsWith,\n "bf_replace": Replace,\n "bf_repeat": Repeat,\n "bf_pad_start": PadStart,\n "bf_pad_end": PadEnd,\n "bf_string": String,\n\n // URL query builder (#1897 PostList href helpers): conditional\n // (include, key, value) triples \u2192 "base?k=v&\u2026", mirroring a\n // URLSearchParams builder with guarded `.set()` calls.\n "bf_query": Query,\n\n // JSON / numeric primitives \u2014 JS-compat callees registered on\n // the Go adapter\'s `templatePrimitives` map (#1188).\n "bf_json": JSON,\n "bf_number": Number,\n "bf_floor": Floor,\n "bf_ceil": Ceil,\n "bf_round": Round,\n "bf_to_fixed": ToFixed,\n\n // Array/Slice\n "bf_len": Len,\n "bf_at": At,\n "bf_includes": Includes,\n "bf_index_of": IndexOf,\n "bf_last_index_of": LastIndexOf,\n "bf_concat": Concat,\n "bf_slice": Slice,\n "bf_reverse": Reverse,\n "bf_flat": Flat,\n "bf_flat_dynamic": FlatDynamicDepth,\n "bf_flat_map": FlatMap,\n "bf_flat_map_tuple": FlatMapTuple,\n "bf_first": First,\n "bf_last": Last,\n "bf_arr": Arr,\n "bf_filter_truthy": FilterTruthy,\n\n // Higher-order Array Methods\n "bf_every": Every,\n "bf_some": Some,\n "bf_filter": Filter,\n "bf_find": Find,\n "bf_find_index": FindIndex,\n "bf_find_last": FindLast,\n "bf_find_last_index": FindLastIndex,\n "bf_sort": Sort,\n "bf_reduce": Reduce,\n\n // Evaluator-driven higher-order folds (#2018): the comparator / reducer\n // body travels as a serialized ParsedExpr (JSON) evaluated per element,\n // generalizing bf_sort / bf_reduce beyond their fixed catalogues. The\n // adapter falls back to bf_sort for a comparator the evaluator can\'t\n // model (e.g. localeCompare). `bf_env` builds the captured-free-var\n // environment passed as the trailing base_env argument.\n "bf_sort_eval": SortEval,\n "bf_reduce_eval": FoldEval,\n "bf_env": Env,\n\n // Evaluator-driven higher-order predicates (#2018, P2): the predicate\n // body travels as a serialized ParsedExpr (JSON) evaluated per element,\n // generalizing bf_filter / bf_find / bf_find_index / bf_every / bf_some\n // beyond their field-equality / truthiness catalogues. `bf_find_eval` /\n // `bf_find_index_eval` take a `forward` bool (false \u2192 findLast variants).\n "bf_filter_eval": FilterEval,\n "bf_every_eval": EveryEval,\n "bf_some_eval": SomeEval,\n "bf_find_eval": FindEval,\n "bf_find_index_eval": FindIndexEval,\n // `.flatMap(proj)`: project each element through the serialized\n // projection body, then flatten one level.\n "bf_flat_map_eval": FlatMapEval,\n // Value-producing `.map(cb)` (#2073): project each element, one\n // result per element (no flatten).\n "bf_map_eval": MapEval,\n\n // Comment marker (for hydration)\n "bfComment": Comment,\n "bfTextStart": TextStart,\n "bfTextEnd": TextEnd,\n\n // Script collection\n "bfScripts": BfScripts,\n\n // Scope attribute value (#1249: bare scope id, no `~` prefix)\n "bfScopeAttr": ScopeAttr,\n\n // Slot-identity markers (#1249): bf-h, bf-m, bf-r\n "bfHydrationAttrs": HydrationAttrs,\n\n // Child component marker (kept for backward compatibility)\n "bfIsChild": IsChild,\n\n // Props attribute for hydration\n "bfPropsAttr": BfPropsAttr,\n\n // Portal HTML rendering (parses and executes template string)\n "bfPortalHTML": PortalHTML,\n\n // JSX children passed to an imported child component (#1896):\n // the parent renders the children fragment via a companion\n // define (executed through bf_tmpl from TemplateFuncMap) and\n // injects the result into the child\'s Children field.\n "bf_with_children": WithChildren,\n\n // Scope comment for fragment roots\n "bfScopeComment": ScopeComment,\n\n // JSX intrinsic-element spread lowering (#1407)\n "bf_spread_attrs": SpreadAttrs,\n\n // Destructure object-rest spread-onto-element residual (#2087):\n // "every field except these" for a struct/map, keyed by json tag.\n "bf_omit": Omit,\n\n // Case-tolerant single-field read off a map or struct (#2087): a\n // `useContext` local whose `createContext` default is object-shaped\n // (e.g. `createContext<{ config: X }>({ config: {} })`) is typed\n // `map[string]interface{}`, and its keys are the SOURCE (JS-cased)\n // property names a `<Ctx.Provider value={{ \u2026 }}>` bakes\n // (`providerObjectValueToGoMap`, go-template-adapter.ts) \u2014 plain\n // `text/template` dot access does an exact-string `MapIndex`, so\n // `ctx.config.label` lowers to nested `bf_get` calls instead of\n // `.Ctx.Config.Label`. Reuses the same `getFieldValue` the\n // project/sort helpers already use for a dynamic field-name lookup;\n // safe on a nil map/interface (returns nil) so a missing Provider or\n // an absent key falls through to `??`\'s fallback.\n "bf_get": getFieldValue,\n }\n}\n\n// Query builds a URL from a base path plus a query string assembled from\n// (include, key, value) triples, in order. A pair is considered only when its\n// `include` flag is true \u2014 mirroring a JS URLSearchParams builder whose\n// `.set(key, value)` calls are each guarded by an `if`. The compiler lowers a\n// conditional `cond ? v : undefined` to the `include` bool and a plain `key: v`\n// to a `true` include; the emptiness check is applied HERE (an included but\n// empty value is dropped), matching the client `queryHref` and the Perl `query`\n// helper. Keys and values use formEscape (application/x-www-form-urlencoded),\n// so the rendered query is byte-for-byte identical to the browser\'s\n// URLSearchParams. An empty query yields the bare base.\n//\n// A value may be a string slice ([]string or []any), which APPENDS one pair per\n// non-empty member (URLSearchParams.append) \u2014 `{tag: [a, b]}` \u2192 `tag=a&tag=b`.\n// A scalar value follows URLSearchParams.set() semantics: repeating a key\n// overwrites the value at the key\'s first position rather than duplicating it\n// (object literals have unique keys, so this is defensive). Trailing args that\n// don\'t complete a triple are ignored.\n//\n// formEscape differs from url.QueryEscape only on `~` (kept by QueryEscape,\n// `%7E` here) and `*` (`%2A` by QueryEscape, kept here).\nfunc Query(base string, triples ...any) string {\n type kv struct{ key, val string }\n pairs := make([]kv, 0, len(triples)/3)\n pos := make(map[string]int)\n for i := 0; i+2 < len(triples); i += 3 {\n include, _ := triples[i].(bool)\n if !include {\n continue\n }\n k := String(triples[i+1])\n if members, ok := asStringSlice(triples[i+2]); ok {\n // Array value \u2192 append each non-empty member; appended pairs never\n // overwrite, so they don\'t participate in the set()-position map.\n for _, m := range members {\n if m == "" {\n continue\n }\n pairs = append(pairs, kv{k, m})\n }\n continue\n }\n v := String(triples[i+2])\n if v == "" {\n continue // omit an included-but-empty value (client / Perl parity)\n }\n if at, ok := pos[k]; ok {\n pairs[at].val = v // set(): overwrite the first occurrence\'s value\n } else {\n pos[k] = len(pairs)\n pairs = append(pairs, kv{k, v})\n }\n }\n var b strings.Builder\n for _, p := range pairs {\n if b.Len() == 0 {\n b.WriteByte(\'?\')\n } else {\n b.WriteByte(\'&\')\n }\n b.WriteString(formEscape(p.key))\n b.WriteByte(\'=\')\n b.WriteString(formEscape(p.val))\n }\n return base + b.String()\n}\n\n// asStringSlice reports whether v is a query *array* value and, if so, returns\n// its members stringified. A compiled template passes a `[]string` field; the\n// golden conformance vectors decode JSON arrays to `[]any`. Anything else is a\n// scalar (false), handled by the set() path.\nfunc asStringSlice(v any) ([]string, bool) {\n switch s := v.(type) {\n case []string:\n return s, true\n case []any:\n out := make([]string, len(s))\n for i, m := range s {\n out[i] = String(m)\n }\n return out, true\n default:\n return nil, false\n }\n}\n\nconst hexUpper = "0123456789ABCDEF"\n\n// formEscape percent-encodes s with the application/x-www-form-urlencoded byte\n// set, matching the browser\'s URLSearchParams serialization (and the Perl\n// `query` helper) so SSR query strings render byte-for-byte identically across\n// adapters. The unreserved set kept verbatim is A-Z a-z 0-9 and `* - . _`; a\n// space becomes `+`; every other byte is `%XX` with uppercase hex. Encoding is\n// byte-wise, so multi-byte UTF-8 is percent-encoded per byte (`\xE9` \u2192 `%C3%A9`).\n//\n// This differs from url.QueryEscape only for `~` (kept by QueryEscape, encoded\n// to `%7E` here) and `*` (encoded to `%2A` by QueryEscape, kept here).\nfunc formEscape(s string) string {\n var b strings.Builder\n for i := 0; i < len(s); i++ {\n c := s[i]\n switch {\n case c >= \'A\' && c <= \'Z\', c >= \'a\' && c <= \'z\', c >= \'0\' && c <= \'9\',\n c == \'*\', c == \'-\', c == \'.\', c == \'_\':\n b.WriteByte(c)\n case c == \' \':\n b.WriteByte(\'+\')\n default:\n b.WriteByte(\'%\')\n b.WriteByte(hexUpper[c>>4])\n b.WriteByte(hexUpper[c&0x0F])\n }\n }\n return b.String()\n}\n\n// ScopeAttr returns the bare bf-s scope id (#1249).\nfunc ScopeAttr(props interface{}) string {\n return getStringField(props, "ScopeID")\n}\n\n// HydrationAttrs emits `bf-h="<host>" bf-m="<slot>" bf-r=""` conditionally.\n// See spec/compiler.md "Slot identity".\nfunc HydrationAttrs(props interface{}) template.HTMLAttr {\n parts := []string{}\n if host := getStringField(props, "BfParent"); host != "" {\n parts = append(parts, fmt.Sprintf(`bf-h="%s"`, template.HTMLEscapeString(host)))\n }\n if mount := getStringField(props, "BfMount"); mount != "" {\n parts = append(parts, fmt.Sprintf(`bf-m="%s"`, template.HTMLEscapeString(mount)))\n }\n if !getBoolField(props, "BfIsChild") {\n parts = append(parts, `bf-r=""`)\n }\n if len(parts) == 0 {\n return ""\n }\n return template.HTMLAttr(strings.Join(parts, " "))\n}\n\n// IsChild is a deprecated no-op stub. Child status is signalled by bf-h\n// presence (#1249); use HydrationAttrs instead.\nfunc IsChild(props interface{}) template.HTMLAttr {\n return ""\n}\n\n// svgCamelCaseAttrs mirrors SVG_CAMEL_CASE_ATTRS from\n// packages/client/src/runtime/spread-attrs.ts. SVG XML attribute\n// names are case-sensitive; the default camelCase \u2192 kebab-case\n// rewrite must NOT apply to these or the SVG stops rendering\n// (#1407). Coordinates with the compile-time SVG_CAMEL_TO_KEBAB\n// table in packages/jsx/src/ir-to-client-js/utils.ts: presentation\n// attrs (clipPath, strokeWidth, \u2026) live there and must NOT appear\n// here, or the same JSX prop would lower to clip-path via the\n// explicit-attr path and stay clipPath via the spread path.\nvar svgCamelCaseAttrs = map[string]struct{}{\n "allowReorder": {}, "attributeName": {}, "attributeType": {}, "autoReverse": {},\n "baseFrequency": {}, "baseProfile": {}, "calcMode": {}, "clipPathUnits": {},\n "contentScriptType": {}, "contentStyleType": {}, "diffuseConstant": {}, "edgeMode": {},\n "externalResourcesRequired": {}, "filterRes": {}, "filterUnits": {}, "glyphRef": {},\n "gradientTransform": {}, "gradientUnits": {}, "kernelMatrix": {}, "kernelUnitLength": {},\n "keyPoints": {}, "keySplines": {}, "keyTimes": {}, "lengthAdjust": {}, "limitingConeAngle": {},\n "markerHeight": {}, "markerUnits": {}, "markerWidth": {}, "maskContentUnits": {},\n "maskUnits": {}, "numOctaves": {}, "pathLength": {}, "patternContentUnits": {},\n "patternTransform": {}, "patternUnits": {}, "pointsAtX": {}, "pointsAtY": {}, "pointsAtZ": {},\n "preserveAlpha": {}, "preserveAspectRatio": {}, "primitiveUnits": {}, "refX": {}, "refY": {},\n "repeatCount": {}, "repeatDur": {}, "requiredExtensions": {}, "requiredFeatures": {},\n "specularConstant": {}, "specularExponent": {}, "spreadMethod": {}, "startOffset": {},\n "stdDeviation": {}, "stitchTiles": {}, "surfaceScale": {}, "systemLanguage": {},\n "tableValues": {}, "targetX": {}, "targetY": {}, "textLength": {}, "viewBox": {}, "viewTarget": {},\n "xChannelSelector": {}, "yChannelSelector": {}, "zoomAndPan": {},\n}\n\n// toAttrName mirrors the JSX\u2192HTML attribute-name rewrite from\n// packages/client/src/runtime/spread-attrs.ts. className \u2192 class,\n// htmlFor \u2192 for, SVG camelCase attrs preserved, other camelCase\n// keys lowered to kebab-case.\nfunc toAttrName(key string) string {\n if key == "className" {\n return "class"\n }\n if key == "htmlFor" {\n return "for"\n }\n if _, ok := svgCamelCaseAttrs[key]; ok {\n return key\n }\n // camelCase \u2192 kebab-case: mirror the JS reference exactly\n // (`key.replace(/([A-Z])/g, \'-$1\').toLowerCase()`). The JS shape\n // produces a leading `-` for an initial uppercase letter\n // (`XData` \u2192 `-x-data`); both this Go path and the matching JS\n // runtime are wrong-by-construction for that case (the resulting\n // HTML attribute name is invalid), but keeping them byte-equal\n // avoids silent SSR/CSR divergence (#1411 review).\n var b strings.Builder\n for _, r := range key {\n if r >= \'A\' && r <= \'Z\' {\n b.WriteByte(\'-\')\n b.WriteRune(r + 32)\n } else {\n b.WriteRune(r)\n }\n }\n return b.String()\n}\n\n// StyleToCss mirrors styleToCss from\n// packages/client/src/runtime/style.ts. Accepts a string passthrough,\n// or a map (JSON-deserialized object) whose camelCase keys are\n// lowered to kebab-case and joined with `;`. Returns ("", false) for\n// nullish/empty input so callers can omit the attribute entirely.\nfunc StyleToCss(v any) (string, bool) {\n if v == nil {\n return "", false\n }\n rv := reflect.ValueOf(v)\n for rv.Kind() == reflect.Interface || rv.Kind() == reflect.Pointer {\n if rv.IsNil() {\n return "", false\n }\n rv = rv.Elem()\n }\n if rv.Kind() != reflect.Map {\n // Non-object: stringify and return as-is, matching the JS\n // `typeof value !== \'object\'` branch.\n s := fmt.Sprint(v)\n if s == "" {\n return "", false\n }\n return s, true\n }\n keys := rv.MapKeys()\n sorted := make([]string, 0, len(keys))\n for _, k := range keys {\n if k.Kind() == reflect.String {\n sorted = append(sorted, k.String())\n }\n }\n sort.Strings(sorted)\n parts := make([]string, 0, len(sorted))\n for _, k := range sorted {\n val := rv.MapIndex(reflect.ValueOf(k))\n // Skip nil entries (matches the JS `if (v == null) continue`).\n if !val.IsValid() {\n continue\n }\n if val.Kind() == reflect.Interface || val.Kind() == reflect.Pointer {\n if val.IsNil() {\n continue\n }\n val = val.Elem()\n }\n prop := toAttrName(k)\n parts = append(parts, fmt.Sprintf("%s:%v", prop, val.Interface()))\n }\n if len(parts) == 0 {\n return "", false\n }\n return strings.Join(parts, ";"), true\n}\n\n// SpreadAttrs lowers a JSX intrinsic-element spread bag (#1407) to\n// an HTML attribute string. Mirrors spreadAttrs from\n// packages/client/src/runtime/spread-attrs.ts so SSR output matches\n// what CSR\'s `applyRestAttrs` writes at hydration.\n//\n// Skip rules: nil/false values, event handlers (`on[A-Z]*`),\n// `children`, `ref`.\n//\n// Key remap: className \u2192 class, htmlFor \u2192 for, SVG camelCase\n// preserved, other camelCase \u2192 kebab-case.\n//\n// `style` is routed through StyleToCss so object literals serialize\n// to a real CSS string instead of Go\'s default `map[k:v]` form.\n//\n// Booleans: true \u2192 bare attribute name, false \u2192 omitted.\n// Other scalar values are HTML-escaped via template.HTMLEscapeString.\n// Returns a `template.HTMLAttr` so html/template emits the result\n// verbatim (the function does its own escaping).\n//\n// Keys are sorted alphabetically before emission for deterministic\n// output. SSR/CSR attribute-order divergence is acceptable per the\n// rest-destructure-object-spread-in-map fixture\'s documented policy\n// \u2014 browsers honor the LAST value when a key is duplicated, so\n// pairing with static attrs (`<div class="x" {...rest}>`) is\n// last-wins regardless of order.\nfunc SpreadAttrs(bag any) template.HTMLAttr {\n if bag == nil {\n return ""\n }\n rv := reflect.ValueOf(bag)\n for rv.Kind() == reflect.Interface || rv.Kind() == reflect.Pointer {\n if rv.IsNil() {\n return ""\n }\n rv = rv.Elem()\n }\n if rv.Kind() != reflect.Map {\n return ""\n }\n keys := rv.MapKeys()\n sortedKeys := make([]string, 0, len(keys))\n for _, k := range keys {\n if k.Kind() == reflect.String {\n sortedKeys = append(sortedKeys, k.String())\n }\n }\n sort.Strings(sortedKeys)\n parts := make([]string, 0, len(sortedKeys))\n for _, key := range sortedKeys {\n // Event handlers \u2014 skip at SSR the same way\n // packages/client/src/runtime/spread-attrs.ts does at\n // hydration. The JS predicate is\n // `key.startsWith(\'on\') && key.length > 2 && key[2] === key[2].toUpperCase()`,\n // which is true for any character whose uppercase form is\n // itself: ASCII A-Z, digits, underscore, and non-letter\n // symbols. Mirror that here by skipping when key[2] is NOT\n // a lowercase ASCII letter \u2014 so `onClick`, `on_custom`, and\n // `on0` all match (#1411 review).\n if len(key) > 2 && key[0] == \'o\' && key[1] == \'n\' && !(key[2] >= \'a\' && key[2] <= \'z\') {\n continue\n }\n // `children` is a JSX construct rendered inside the element,\n // never a DOM attribute. `ref` is intentionally NOT filtered\n // here so output stays byte-equal with the JS reference\n // `spreadAttrs` in packages/client/src/runtime/spread-attrs.ts\n // (which only filters null/false, event handlers, and\n // children) \u2014 aligning Go\'s filter set diverges from JS in\n // the opposite direction. Filtering `ref` consistently across\n // both SSR runtimes is a separate concern tracked alongside\n // the JS `applyRestAttrs` vs `spreadAttrs` mismatch (#1411\n // review).\n if key == "children" {\n continue\n }\n val := rv.MapIndex(reflect.ValueOf(key))\n if !val.IsValid() {\n continue\n }\n // Unwrap interface wrappers (json.Unmarshal produces\n // interface{}-wrapped values for map[string]any).\n v := val\n for v.Kind() == reflect.Interface || v.Kind() == reflect.Pointer {\n if v.IsNil() {\n // Skip null entries.\n v = reflect.Value{}\n break\n }\n v = v.Elem()\n }\n if !v.IsValid() {\n continue\n }\n // Boolean values: true \u2192 bare attribute, false \u2192 omitted.\n if v.Kind() == reflect.Bool {\n if !v.Bool() {\n continue\n }\n parts = append(parts, toAttrName(key))\n continue\n }\n // `style` routes through StyleToCss so object literals get a\n // real CSS string. The JS side does the same.\n if key == "style" {\n css, ok := StyleToCss(v.Interface())\n if !ok {\n continue\n }\n parts = append(parts, fmt.Sprintf(`style="%s"`, template.HTMLEscapeString(css)))\n continue\n }\n // Stringify and escape. fmt.Sprint handles numbers, bools-as-\n // strings, and arbitrary stringer types the same way the JS\n // `String(value)` coercion does for the analogous cases.\n s := fmt.Sprint(v.Interface())\n parts = append(parts, fmt.Sprintf(`%s="%s"`, toAttrName(key), template.HTMLEscapeString(s)))\n }\n if len(parts) == 0 {\n return ""\n }\n return template.HTMLAttr(strings.Join(parts, " "))\n}\n\n// Omit builds a `map[string]any` residual bag from a struct or map value,\n// excluding the given keys \u2014 powers the `{...rest}` spread-onto-element\n// lowering for a destructured `.map()` loop-item\'s object-rest binding\n// (#2087): `.map(({ id, title, ...rest }) => <li {...rest}>)` needs "every\n// field EXCEPT the ones the pattern already destructured out", and a static\n// Go struct type has no way to express "minus a field" \u2014 the exclude set is\n// known at COMPILE TIME (the sibling keys the destructure pattern names), so\n// the compiler passes them here and this does the per-item field-vs-key\n// matching a static type can\'t. The result feeds `bf_spread_attrs`\n// (`SpreadAttrs`), same as a top-level `{...attrs()}` bag.\n//\n// Struct receiver: iterates exported fields via reflection, keyed by each\n// field\'s `json` struct tag (falling back to the Go field name when absent)\n// \u2014 the generated struct\'s json tag is always the ORIGINAL source property\n// name (see `structFieldsFor` / `typeDefinitionToGo` in the Go adapter), so\n// this reproduces the exact JS key `SpreadAttrs`\'s `toAttrName` expects\n// (`"data-priority"`, not a re-derived `"DataPriority"`). A tag of `"-"`\n// (opt-out) is skipped like `encoding/json` does.\n//\n// Map receiver: copies string keys through directly, same exclude/skip\n// rules.\n//\n// Anything else (nil, a non-struct/non-map interface) returns an empty map.\nfunc Omit(item any, excludeKeys ...string) map[string]any {\n exclude := make(map[string]struct{}, len(excludeKeys))\n for _, k := range excludeKeys {\n exclude[k] = struct{}{}\n }\n out := map[string]any{}\n rv := reflect.ValueOf(item)\n for rv.Kind() == reflect.Interface || rv.Kind() == reflect.Pointer {\n if rv.IsNil() {\n return out\n }\n rv = rv.Elem()\n }\n switch rv.Kind() {\n case reflect.Struct:\n rt := rv.Type()\n for i := 0; i < rt.NumField(); i++ {\n field := rt.Field(i)\n if !field.IsExported() {\n continue\n }\n key := field.Name\n if tag, ok := field.Tag.Lookup("json"); ok {\n if comma := strings.Index(tag, ","); comma >= 0 {\n tag = tag[:comma]\n }\n if tag == "-" {\n continue\n }\n if tag != "" {\n key = tag\n }\n }\n if _, skip := exclude[key]; skip {\n continue\n }\n out[key] = rv.Field(i).Interface()\n }\n case reflect.Map:\n for _, k := range rv.MapKeys() {\n if k.Kind() != reflect.String {\n continue\n }\n key := k.String()\n if _, skip := exclude[key]; skip {\n continue\n }\n val := rv.MapIndex(k)\n if val.IsValid() {\n out[key] = val.Interface()\n }\n }\n }\n return out\n}\n\n// BfPropsAttr returns the bf-p attribute with the JSON-serialized\n// props in flat format. Output format: `bf-p=\'{"propName":value,...}\'`.\n// Only emits the attribute for root components (BfIsRoot == true);\n// child components receive props from their parent via initChild().\n//\n// Returns the marshal error so a `template.Execute` call fails\n// loudly on cycles / unsupported props rather than silently\n// dropping the bf-p attribute and breaking client-side hydration.\n// Same loud-failure policy as `JSON` \u2014 user data going through\n// `encoding/json` shouldn\'t fail invisibly.\nfunc BfPropsAttr(props interface{}) (template.HTMLAttr, error) {\n // Only root components should emit bf-p\n if !getBoolField(props, "BfIsRoot") {\n return "", nil\n }\n\n propsJSON, err := json.Marshal(props)\n if err != nil {\n return "", err\n }\n\n escaped := template.HTMLEscapeString(string(propsJSON))\n return template.HTMLAttr(`bf-p="` + escaped + `"`), nil\n}\n\n// =============================================================================\n// Arithmetic Operations\n// =============================================================================\n\n// Add returns a + b. Supports int and float64.\nfunc Add(a, b any) any {\n av, bv := toFloat64(a), toFloat64(b)\n result := av + bv\n // Return int if both inputs were int-like\n if isIntLike(a) && isIntLike(b) && result == float64(int(result)) {\n return int(result)\n }\n return result\n}\n\n// Sub returns a - b. Supports int and float64.\nfunc Sub(a, b any) any {\n av, bv := toFloat64(a), toFloat64(b)\n result := av - bv\n if isIntLike(a) && isIntLike(b) && result == float64(int(result)) {\n return int(result)\n }\n return result\n}\n\n// Mul returns a * b. Supports int and float64.\nfunc Mul(a, b any) any {\n av, bv := toFloat64(a), toFloat64(b)\n result := av * bv\n if isIntLike(a) && isIntLike(b) && result == float64(int(result)) {\n return int(result)\n }\n return result\n}\n\n// Div returns a / b. Returns float64 to match JavaScript behavior.\n// Returns 0 if b is 0 (instead of panicking).\nfunc Div(a, b any) any {\n av, bv := toFloat64(a), toFloat64(b)\n if bv == 0 {\n return 0\n }\n return av / bv\n}\n\n// Min returns the smaller of a and b (two-arg `Math.min`). Like Mul, it keeps\n// an integer result when both operands are int-like so a CSS value such as\n// `bf_min 100 x` stays `100` rather than `100.000000`.\nfunc Min(a, b any) any {\n av, bv := toFloat64(a), toFloat64(b)\n r := av\n if bv < av {\n r = bv\n }\n if isIntLike(a) && isIntLike(b) && r == float64(int(r)) {\n return int(r)\n }\n return r\n}\n\n// Max returns the larger of a and b (two-arg `Math.max`), with the same\n// int-preserving rule as Min.\nfunc Max(a, b any) any {\n av, bv := toFloat64(a), toFloat64(b)\n r := av\n if bv > av {\n r = bv\n }\n if isIntLike(a) && isIntLike(b) && r == float64(int(r)) {\n return int(r)\n }\n return r\n}\n\n// Mod returns a % b (modulo). Supports int only.\nfunc Mod(a, b any) int {\n av, bv := toInt(a), toInt(b)\n if bv == 0 {\n return 0\n }\n return av % bv\n}\n\n// Neg returns -a (negation).\nfunc Neg(a any) any {\n if v, ok := a.(int); ok {\n return -v\n }\n return -toFloat64(a)\n}\n\n// =============================================================================\n// String Operations\n// =============================================================================\n\n// Lower returns the lowercase version of s.\nfunc Lower(s string) string {\n return strings.ToLower(s)\n}\n\n// Upper returns the uppercase version of s.\nfunc Upper(s string) string {\n return strings.ToUpper(s)\n}\n\n// Trim returns s with leading and trailing whitespace removed.\nfunc Trim(s string) string {\n return strings.TrimSpace(s)\n}\n\n// Contains returns true if s contains substr.\nfunc Contains(s, substr string) bool {\n return strings.Contains(s, substr)\n}\n\n// Split lowers `String.prototype.split(sep, limit?)` (#1448 Tier B). It\n// wraps `strings.Split` and normalises the result to `[]any` so the\n// slice composes with the array-method surface downstream (`bf_join`,\n// range loops, `bf_len`, \u2026) the same way `bf_slice` / `bf_reverse`\n// results do. Like JS, an empty separator splits into individual UTF-8\n// characters and trailing empty fields are preserved (`"a,".split(",")`\n// \u2192 `["a", ""]`). An optional `limit` caps the number of returned\n// pieces (`"a,b,c".split(",", 2)` \u2192 `["a", "b"]`); a negative limit is\n// ignored (JS would also return every piece \u2014 its ToUint32 wrap makes\n// the limit effectively unbounded). The no-separator form is handled by\n// the adapter (it emits `bf_arr` for the whole-string single element).\nfunc Split(s, sep string, limit ...int) []any {\n parts := strings.Split(s, sep)\n if len(limit) > 0 && limit[0] >= 0 && limit[0] < len(parts) {\n parts = parts[:limit[0]]\n }\n out := make([]any, len(parts))\n for i, p := range parts {\n out[i] = p\n }\n return out\n}\n\n// StartsWith lowers `String.prototype.startsWith(prefix, position?)`\n// (#1448 Tier B). Wraps `strings.HasPrefix`; an empty prefix is always\n// true (JS parity). The optional `position` re-anchors the test to start\n// at that index (clamped to `[0, len]` so it never panics), matching JS\n// `"abc".startsWith("b", 1) === true`.\nfunc StartsWith(s, prefix string, position ...int) bool {\n if len(position) > 0 {\n p := position[0]\n if p < 0 {\n p = 0\n }\n if p > len(s) {\n p = len(s)\n }\n s = s[p:]\n }\n return strings.HasPrefix(s, prefix)\n}\n\n// EndsWith lowers `String.prototype.endsWith(suffix, endPosition?)`\n// (#1448 Tier B). Wraps `strings.HasSuffix`; an empty suffix is always\n// true (JS parity). The optional `endPosition` treats the string as if\n// it were only that many bytes long (clamped to `[0, len]`), matching JS\n// `"abc".endsWith("b", 2) === true`.\nfunc EndsWith(s, suffix string, endPosition ...int) bool {\n if len(endPosition) > 0 {\n e := endPosition[0]\n if e < 0 {\n e = 0\n }\n if e > len(s) {\n e = len(s)\n }\n s = s[:e]\n }\n return strings.HasSuffix(s, suffix)\n}\n\n// Replace lowers the string-pattern form of `String.prototype.replace`\n// (#1448 Tier B). JS replaces only the FIRST occurrence for a string\n// pattern, so the count is 1 (`strings.Replace` with n=1; n<0 would\n// replace all \u2014 that\'s `.replaceAll`, still refused). The replacement\n// is treated literally: unlike JS, special replacement patterns like\n// `$&` / `$1` are NOT interpreted (Go and Perl agree on literal\n// replacement, keeping the two template adapters byte-equal; this\n// diverges from the Hono/CSR JS path only for replacement strings that\n// contain `$`-patterns, which are rare in template position).\nfunc Replace(s, old, new string) string {\n return strings.Replace(s, old, new, 1)\n}\n\n// Repeat lowers `String.prototype.repeat(n)` (#1448 Tier B): the\n// receiver concatenated n times. JS throws RangeError for a negative\n// count and `strings.Repeat` panics, so a negative count clamps to the\n// empty string \u2014 SSR templates degrade rather than crash the render.\n// A zero count is the empty string (JS parity).\nfunc Repeat(s string, n int) string {\n if n <= 0 {\n return ""\n }\n return strings.Repeat(s, n)\n}\n\n// padTo lowers the shared body of `String.prototype.padStart` /\n// `padEnd` (#1448 Tier B): pad `s` to `target` code points using `pad`\n// repeated and truncated to fill, prepended (atStart) or appended.\n// Length is measured in runes (not bytes) so the result matches the\n// Perl `bf->pad_*` helpers \u2014 this diverges from JS\'s UTF-16-unit length\n// only for astral-plane input. An empty pad, or a receiver already at\n// least `target` long, returns `s` unchanged (JS parity).\nfunc padTo(s string, target int, pad string, atStart bool) string {\n if pad == "" {\n return s\n }\n sLen := utf8.RuneCountInString(s)\n if sLen >= target {\n return s\n }\n need := target - sLen\n padRunes := []rune(pad)\n fill := make([]rune, 0, need)\n for len(fill) < need {\n for _, r := range padRunes {\n if len(fill) >= need {\n break\n }\n fill = append(fill, r)\n }\n }\n if atStart {\n return string(fill) + s\n }\n return s + string(fill)\n}\n\n// PadStart lowers `String.prototype.padStart(target, pad?)` (#1448 Tier\n// B). The pad string defaults to a single space when omitted.\nfunc PadStart(s string, target int, pad ...string) string {\n p := " "\n if len(pad) > 0 {\n p = pad[0]\n }\n return padTo(s, target, p, true)\n}\n\n// PadEnd lowers `String.prototype.padEnd(target, pad?)` (#1448 Tier B).\nfunc PadEnd(s string, target int, pad ...string) string {\n p := " "\n if len(pad) > 0 {\n p = pad[0]\n }\n return padTo(s, target, p, false)\n}\n\n// Join concatenates elements of a slice with sep. Accepts both\n// reflect.Slice (the common case \u2014 `bf_arr` and `bf_filter_truthy`\n// both return `[]any`) AND reflect.Array (fixed-size Go arrays like\n// `[3]string{...}`), mirroring JS `Array.prototype.join` which\n// doesn\'t distinguish between the two. Pre-fix this returned "" for\n// fixed-size arrays passed through template data (Copilot review on\n// #1445).\nfunc Join(items any, sep string) string {\n v := reflect.ValueOf(items)\n if v.Kind() != reflect.Slice && v.Kind() != reflect.Array {\n return ""\n }\n\n parts := make([]string, v.Len())\n for i := 0; i < v.Len(); i++ {\n parts[i] = toString(v.Index(i).Interface())\n }\n return strings.Join(parts, sep)\n}\n\n// String returns the string form of v. Mirrors JS `String(v)` for\n// non-nil values via `fmt.Sprintf("%v", ...)`. Diverges from JS on\n// nil: JS `String(null)` is "null", but the template path renders\n// `nil` as the empty string here so an unset prop doesn\'t surface\n// as a literal "null"/"undefined" in user-facing HTML. Document the\n// divergence explicitly so callers don\'t rely on JS-exact parity.\nfunc String(v any) string {\n if v == nil {\n return ""\n }\n return fmt.Sprintf("%v", v)\n}\n\n// JSON returns the JSON encoding of v as a string. Mirrors\n// JS `JSON.stringify(v)` for the V1 single-arg shape (no `replacer`\n// or `space`). Object key order is determined by Go\'s `encoding/json`\n// (alphabetical for maps, declaration order for structs) \u2014 the\n// #1187 contract requires value-compat, not order-compat.\n//\n// Top-level NaN / \xB1Inf are pre-handled to match JS \u2014 JS\'s\n// `JSON.stringify(NaN)` and `JSON.stringify(Infinity)` both produce\n// `"null"`, but Go\'s `encoding/json` rejects them with\n// `UnsupportedValueError`. Without this carve-out the common\n// composition `JSON.stringify(Number("garbage"))` would error\n// instead of emitting `"null"` like JS does. Nested NaN/Inf inside\n// a struct/map still surfaces an error \u2014 covering that needs a\n// custom marshaller; out of V1 scope.\n//\n// Returns the marshal error so a `template.Execute` call fails\n// loudly on cycles / unsupported values rather than silently\n// producing `""` and reintroducing the SSR data-loss class\n// #1187 was filed against. Go\'s text/template treats a non-nil\n// error return from a func as an execution failure.\nfunc JSON(v any) (string, error) {\n if f, ok := v.(float64); ok && (math.IsNaN(f) || math.IsInf(f, 0)) {\n return "null", nil\n }\n b, err := json.Marshal(v)\n if err != nil {\n return "", err\n }\n return string(b), nil\n}\n\n// Number coerces v to a float64. Mirrors JS `Number(v)` semantics:\n// numeric / boolean inputs convert as expected; non-numeric strings\n// and other unsupported shapes return `NaN` (matching JS rather\n// than silently substituting 0, which would mis-shape downstream\n// arithmetic and template-side comparisons). Templates that need\n// a deterministic fallback should compose with the user-side\n// default (e.g. `Number(props.x ?? 0)` in JSX).\nfunc Number(v any) float64 {\n if v == nil {\n return math.NaN()\n }\n switch x := v.(type) {\n case float64:\n return x\n case float32:\n return float64(x)\n case int:\n return float64(x)\n case int32:\n return float64(x)\n case int64:\n return float64(x)\n case bool:\n if x {\n return 1\n }\n return 0\n case string:\n f, err := strconv.ParseFloat(x, 64)\n if err != nil {\n return math.NaN()\n }\n return f\n }\n return math.NaN()\n}\n\n// Floor returns the largest integer \u2264 v as a float64. Mirrors JS\n// `Math.floor`. The return type stays float64 so chained primitives\n// (`bf_floor` then `bf_string`) line up with JS\'s number type.\nfunc Floor(v any) float64 {\n return math.Floor(Number(v))\n}\n\n// ToFixed formats v with exactly `digits` decimal places, mirroring JS\n// `Number.prototype.toFixed` (zero-padding + half-toward-+Infinity\n// rounding). JS rounds the scaled integer half up (`(2.5).toFixed(0)`\n// is "3"); bare `fmt.Sprintf("%.*f")` rounds half-to-even ("2"), so we\n// scale, round with `Floor(x + 0.5)` (matching `Round`), then format\n// the exact multiple. #1897.\nfunc ToFixed(v any, digits int) string {\n if digits < 0 {\n digits = 0\n }\n n := Number(v)\n // JS toFixed returns the strings "NaN" / "Infinity" / "-Infinity" for\n // non-finite inputs; fmt would render "NaN"/"+Inf"/"-Inf".\n if math.IsNaN(n) {\n return "NaN"\n }\n if math.IsInf(n, 1) {\n return "Infinity"\n }\n if math.IsInf(n, -1) {\n return "-Infinity"\n }\n factor := math.Pow(10, float64(digits))\n rounded := math.Floor(n*factor + 0.5)\n return fmt.Sprintf("%.*f", digits, rounded/factor)\n}\n\n// Ceil returns the smallest integer \u2265 v as a float64. Mirrors JS\n// `Math.ceil`.\nfunc Ceil(v any) float64 {\n return math.Ceil(Number(v))\n}\n\n// Round returns v rounded to the nearest integer as a float64.\n// Mirrors JS `Math.round` \u2014 half-away-from-zero (Go\'s `math.Round`\n// matches; JS rounds half toward +Infinity which differs at .5\n// negatives; we accept that minor divergence since the conformance\n// contract is value-compat for the common positive case).\nfunc Round(v any) float64 {\n return math.Round(Number(v))\n}\n\n// =============================================================================\n// Array/Slice Operations\n// =============================================================================\n\n// Len returns the length of a slice, array, map, string, or channel.\n// Returns 0 for nil or unsupported types.\nfunc Len(v any) int {\n if v == nil {\n return 0\n }\n rv := reflect.ValueOf(v)\n switch rv.Kind() {\n case reflect.Slice, reflect.Array, reflect.Map, reflect.String, reflect.Chan:\n return rv.Len()\n default:\n return 0\n }\n}\n\n// At returns the element at index i from a slice.\n// Supports negative indices (e.g., -1 for last element).\n// Returns nil if index is out of bounds.\nfunc At(items any, index int) any {\n v := reflect.ValueOf(items)\n if v.Kind() != reflect.Slice && v.Kind() != reflect.Array {\n return nil\n }\n\n length := v.Len()\n if length == 0 {\n return nil\n }\n\n // Handle negative indices\n if index < 0 {\n index = length + index\n }\n\n if index < 0 || index >= length {\n return nil\n }\n\n return v.Index(index).Interface()\n}\n\n// Includes returns true if items contains elem. Lowers both\n// `Array.prototype.includes` and `String.prototype.includes` \u2014\n// the adapter can\'t disambiguate the receiver at compile time,\n// so this helper dispatches at runtime on `reflect.Kind()`:\n//\n// - slice/array receiver: SameValueZero element search (matches\n// the evaluator\'s `evalSameValueZero`/`evalIncludes` in eval.go,\n// which back the serialized-callback path) \u2014 numeric types compare\n// by value across int/float64 the way JS\'s single "number" type\n// does, and NaN matches NaN (unlike `===`). This used to be\n// `reflect.DeepEqual`, which is type-strict (`int(2)` != `float64(2)`)\n// and never matches NaN to NaN; that diverged from the evaluator\'s\n// `.includes` and from JS itself, so it was unified here.\n// - string receiver: strings.Contains substring search\n//\n// Anything else returns false (matches the JS semantic where\n// `.includes` is only defined on Array / TypedArray / String).\nfunc Includes(recv any, elem any) bool {\n v := reflect.ValueOf(recv)\n if v.Kind() == reflect.String {\n // JS `String.prototype.includes` accepts only string args;\n // non-string `elem` would TypeError in real JS but our\n // callers have lowered through `convertExpressionToGo`\n // where the arg type is whatever the template binds. Stringify\n // via fmt to keep the helper total.\n needle, ok := elem.(string)\n if !ok {\n needle = fmt.Sprintf("%v", elem)\n }\n return strings.Contains(v.String(), needle)\n }\n if v.Kind() != reflect.Slice && v.Kind() != reflect.Array {\n return false\n }\n for i := 0; i < v.Len(); i++ {\n if evalSameValueZero(v.Index(i).Interface(), elem) {\n return true\n }\n }\n return false\n}\n\n// IndexOf returns the 0-based position of the first item that\n// DeepEquals `elem`, or -1 if not found. Lowers\n// `Array.prototype.indexOf(x)` (#1448 Tier A). The existing\n// `FindIndex` helper does struct-field equality (used by the\n// higher-order `.find` lowering); this one does value equality\n// against scalar / struct items so callers don\'t have to compose\n// a synthetic predicate.\n//\n// Non-array / non-slice receivers return -1 (matches the JS\n// semantic that `.indexOf` is only defined on Array / TypedArray).\nfunc IndexOf(items any, elem any) int {\n v := reflect.ValueOf(items)\n if v.Kind() != reflect.Slice && v.Kind() != reflect.Array {\n return -1\n }\n for i := 0; i < v.Len(); i++ {\n if reflect.DeepEqual(v.Index(i).Interface(), elem) {\n return i\n }\n }\n return -1\n}\n\n// LastIndexOf returns the 0-based position of the last item that\n// DeepEquals `elem`, or -1 if not found. Mirrors\n// `Array.prototype.lastIndexOf(x)`. The reverse traversal is the\n// only behavioural difference vs `IndexOf` \u2014 disambiguating a\n// duplicated value\'s first vs last position is the canonical\n// reason a JS author reaches for `lastIndexOf`.\nfunc LastIndexOf(items any, elem any) int {\n v := reflect.ValueOf(items)\n if v.Kind() != reflect.Slice && v.Kind() != reflect.Array {\n return -1\n }\n for i := v.Len() - 1; i >= 0; i-- {\n if reflect.DeepEqual(v.Index(i).Interface(), elem) {\n return i\n }\n }\n return -1\n}\n\n// Concat merges two arrays (or slices) into a single `[]any`,\n// preserving order: receiver elements first, then `other`\'s.\n// Lowers `Array.prototype.concat(other)` (#1448 Tier A). Non-array\n// operands collapse to an empty source \u2014 matches the JS semantic\n// where `.concat` on a non-Array reads it as a single element only\n// if its `Symbol.isConcatSpreadable` is true; the template-language\n// path doesn\'t have user objects with that flag, so treating\n// non-arrays as empty is the conservative lowering. Variadic\n// `.concat(a, b, c)` is out of scope here (parser gates to a single\n// arg); the helper itself stays binary so a future variadic IR can\n// fold via repeated calls without changing this signature.\nfunc Concat(a, b any) []any {\n flatten := func(v reflect.Value) []any {\n if !v.IsValid() {\n return nil\n }\n if v.Kind() != reflect.Slice && v.Kind() != reflect.Array {\n return nil\n }\n out := make([]any, v.Len())\n for i := 0; i < v.Len(); i++ {\n out[i] = v.Index(i).Interface()\n }\n return out\n }\n left := flatten(reflect.ValueOf(a))\n right := flatten(reflect.ValueOf(b))\n return append(left, right...)\n}\n\n// Slice carves out a sub-range from `items`. Lowers\n// `Array.prototype.slice(start, end?)` (#1448 Tier A). The variadic\n// `end` arg lets Go template\'s call dispatcher pass either 2 or 3\n// arguments; an absent end means "to length".\n//\n// JS-compat clamping:\n// - start < 0 \u2192 length + start (e.g. -1 = last index)\n// - end < 0 \u2192 length + end\n// - start < 0 after clamp \u2192 0\n// - end > length \u2192 length\n// - start >= end \u2192 empty slice (no panic)\n//\n// Non-array receivers return an empty `[]any`.\nfunc Slice(items any, start int, end ...int) []any {\n v := reflect.ValueOf(items)\n if v.Kind() != reflect.Slice && v.Kind() != reflect.Array {\n return []any{}\n }\n length := v.Len()\n\n // Normalise start (negative = from end).\n if start < 0 {\n start = length + start\n }\n if start < 0 {\n start = 0\n }\n if start > length {\n start = length\n }\n\n // Normalise end (optional; absent = length).\n stop := length\n if len(end) > 0 {\n stop = end[0]\n if stop < 0 {\n stop = length + stop\n }\n if stop < 0 {\n stop = 0\n }\n if stop > length {\n stop = length\n }\n }\n\n if start >= stop {\n return []any{}\n }\n\n out := make([]any, 0, stop-start)\n for i := start; i < stop; i++ {\n out = append(out, v.Index(i).Interface())\n }\n return out\n}\n\n// Reverse returns a new slice with `items`\'s elements in reverse\n// order. Lowers both `Array.prototype.reverse()` and\n// `Array.prototype.toReversed()` (#1448 Tier A) \u2014 SSR templates\n// render a snapshot, so JS\'s mutate-receiver vs return-new-array\n// distinction has no template-level meaning, and the safer\n// non-mutating shape is used uniformly.\n//\n// Non-array receivers return an empty `[]any`.\nfunc Reverse(items any) []any {\n v := reflect.ValueOf(items)\n if v.Kind() != reflect.Slice && v.Kind() != reflect.Array {\n return []any{}\n }\n length := v.Len()\n out := make([]any, length)\n for i := 0; i < length; i++ {\n out[length-1-i] = v.Index(i).Interface()\n }\n return out\n}\n\n// Flat flattens nested slices/arrays `depth` levels deep. Lowers\n// `Array.prototype.flat(depth?)` (#1448 Tier C). A `depth` of `-1` is the\n// `Infinity` sentinel (flatten fully); `0` (or negative-from-JS, already\n// normalised to 0 at compile time) returns a shallow copy. Non-array\n// elements are kept as-is (JS only flattens nested arrays). A non-array\n// receiver returns an empty `[]any`.\nfunc Flat(items any, depth int) []any {\n v := reflect.ValueOf(items)\n if v.Kind() != reflect.Slice && v.Kind() != reflect.Array {\n return []any{}\n }\n out := make([]any, 0, v.Len())\n for i := 0; i < v.Len(); i++ {\n el := v.Index(i).Interface()\n ev := reflect.ValueOf(el)\n if depth != 0 && (ev.Kind() == reflect.Slice || ev.Kind() == reflect.Array) {\n // `-1` (Infinity) recurses unbounded; a finite depth spends one level.\n next := depth\n if depth > 0 {\n next = depth - 1\n }\n out = append(out, Flat(el, next)...)\n } else {\n out = append(out, el)\n }\n }\n return out\n}\n\n// FlatDynamicDepth coerces `depth` via JS\'s `ToIntegerOrInfinity` and\n// flattens `items` that many levels. Lowers a DYNAMIC `.flat(depth)`\n// (#2094) \u2014 one whose depth isn\'t a compile-time literal, so (unlike\n// `Flat` above) the coercion happens here at render time instead of in the\n// parser.\n//\n// This is a SEPARATE helper from `Flat`/`bf_flat` \u2014 NOT a drop-in\n// replacement \u2014 because `Flat`\'s `depth int` parameter treats `-1` as a\n// compile-time SENTINEL meaning "flatten fully" (the parser\'s own\n// normalisation of a literal `Infinity`). A genuinely dynamic depth value\n// of `-1` means the JS-correct OPPOSITE: `Array.prototype.flat(-1)` never\n// recurses (same as `.flat(0)`, a shallow copy), because\n// `FlattenIntoArray` only recurses when `depth > 0`. Reusing `Flat`\'s int\n// contract for a raw dynamic value would silently invert that case, so\n// this function coerces FIRST \u2014 mapping a real `+Infinity` / huge finite\n// value to `Flat`\'s own `-1` sentinel, and a real negative value to `0` \u2014\n// and only then delegates to `Flat`\'s recursion.\n//\n// Coercion rules (JS `ToIntegerOrInfinity`, mirrored exactly; pinned by the\n// `flat_dynamic` golden-vector cases in\n// packages/adapter-tests/vectors/cases.ts):\n// - the value converts via `ToNumber` first (numeric string / bool /\n// number all coerce; see `flatDepthToFloat`);\n// - a NaN result (including a non-numeric string) \u2192 `0`;\n// - truncates toward zero (`2.7` \u2192 `2`);\n// - negative \u2192 `0`;\n// - `+Infinity` / a huge finite value \u2192 flattens fully.\nfunc FlatDynamicDepth(items any, depth any) []any {\n return Flat(items, coerceFlatDepth(depth))\n}\n\n// coerceFlatDepth implements JS\'s `ToIntegerOrInfinity` for a dynamic\n// `.flat(depth)` argument, returning an int in `Flat`\'s own contract (`-1`\n// = unbounded, `>= 0` = that many levels).\nfunc coerceFlatDepth(depth any) int {\n f, ok := flatDepthToFloat(depth)\n if !ok || math.IsNaN(f) {\n return 0\n }\n if math.IsInf(f, 1) {\n return -1 // Flat\'s "flatten fully" sentinel\n }\n if math.IsInf(f, -1) {\n return 0\n }\n trunc := math.Trunc(f)\n if trunc < 0 {\n return 0\n }\n // A huge finite depth behaves identically to "flatten fully" in\n // practice \u2014 real data bottoms out at its actual nesting depth long\n // before a counter this large would ever reach zero. Capping it here\n // avoids an absurd countdown without needing a second sentinel.\n if trunc > 1_000_000 {\n return -1\n }\n return int(trunc)\n}\n\n// flatDepthToFloat converts a dynamic `.flat(depth)` argument to a float64,\n// mirroring JS\'s `ToNumber` across the value shapes a Go template data\n// model can carry (every numeric kind, bool, numeric string). `ok` is\n// false for a shape `ToNumber` can\'t coerce meaningfully (`nil`, or a\n// non-numeric string) \u2014 `coerceFlatDepth` treats that the same as NaN\n// (\u2192 depth `0`), matching JS.\nfunc flatDepthToFloat(v any) (float64, bool) {\n switch n := v.(type) {\n case nil:\n return 0, false\n case float64:\n return n, true\n case float32:\n return float64(n), true\n case int:\n return float64(n), true\n case int8:\n return float64(n), true\n case int16:\n return float64(n), true\n case int32:\n return float64(n), true\n case int64:\n return float64(n), true\n case uint:\n return float64(n), true\n case uint8:\n return float64(n), true\n case uint16:\n return float64(n), true\n case uint32:\n return float64(n), true\n case uint64:\n return float64(n), true\n case bool:\n if n {\n return 1, true\n }\n return 0, true\n case string:\n s := strings.TrimSpace(n)\n if s == "" {\n return 0, true // JS: Number("") is 0\n }\n f, err := strconv.ParseFloat(s, 64)\n if err != nil {\n return 0, false // not numeric \u2192 NaN path\n }\n return f, true\n default:\n return 0, false\n }\n}\n\n// FlatMap projects each element through a `self` / `field` projection and\n// flattens the result one level. Lowers value-returning\n// `Array.prototype.flatMap(fn)` for the field-projection catalogue\n// (#1448 Tier C): `items.flatMap(i => i)` (self) and\n// `items.flatMap(i => i.field)` (field). A projected non-array value is\n// kept as-is (flatMap = map + flat(1)). Non-array receiver \u2192 empty.\nfunc FlatMap(items any, keyKind, keyName string) []any {\n v := reflect.ValueOf(items)\n if v.Kind() != reflect.Slice && v.Kind() != reflect.Array {\n return []any{}\n }\n projected := make([]any, 0, v.Len())\n for i := 0; i < v.Len(); i++ {\n el := v.Index(i).Interface()\n if keyKind == "field" {\n projected = append(projected, getFieldValue(el, keyName))\n } else {\n projected = append(projected, el)\n }\n }\n return Flat(projected, 1)\n}\n\n// FlatMapTuple lowers an array-literal flatMap projection\n// `items.flatMap(i => [i.a, i.b])` (#1448 Tier C). `specs` is a flat list\n// of (kind, name) pairs, one per array-literal leaf: ("self", "") for the\n// item itself, ("field", "<Name>") for a struct field. For each item it\n// appends every leaf\'s value in order. Unlike the scalar `FlatMap`, the\n// per-item array is flattened only one level (flat(1) removes the literal\n// wrapper), so an array-valued leaf is appended verbatim rather than\n// spread \u2014 which is exactly "append each leaf". Non-array receiver \u2192 empty.\nfunc FlatMapTuple(items any, specs ...string) []any {\n v := reflect.ValueOf(items)\n if v.Kind() != reflect.Slice && v.Kind() != reflect.Array {\n return []any{}\n }\n out := make([]any, 0, v.Len())\n for i := 0; i < v.Len(); i++ {\n el := v.Index(i).Interface()\n for j := 0; j+1 < len(specs); j += 2 {\n if specs[j] == "field" {\n out = append(out, getFieldValue(el, specs[j+1]))\n } else {\n out = append(out, el)\n }\n }\n }\n return out\n}\n\n// First returns the first element of a slice, or nil if empty.\nfunc First(items any) any {\n return At(items, 0)\n}\n\n// Last returns the last element of a slice, or nil if empty.\nfunc Last(items any) any {\n return At(items, -1)\n}\n\n// Arr builds an []any from variadic args. Used to lower JS array\n// literals like `[a, b]` for the registry Slot\'s\n// `[className, childClass].filter(Boolean).join(\' \')` shape (#1443) \u2014\n// Go templates have no array-literal syntax, so the codegen routes\n// array-literal IR nodes through this helper.\nfunc Arr(items ...any) []any {\n return items\n}\n\n// FilterTruthy returns a new slice containing only truthy items.\n// Mirrors `arr.filter(Boolean)` semantics: drop nil, false, 0, "" \u2014 the\n// same falsy set JavaScript\'s `Boolean(x)` recognises. Used to lower\n// the registry Slot\'s class-merge pattern (#1443); generalising to\n// arbitrary callable predicates would need the callee-resolution path\n// blocked by #1389, so this stays Boolean-specific.\nfunc FilterTruthy(items any) []any {\n v := reflect.ValueOf(items)\n if !v.IsValid() || (v.Kind() != reflect.Slice && v.Kind() != reflect.Array) {\n return nil\n }\n result := make([]any, 0, v.Len())\n for i := 0; i < v.Len(); i++ {\n raw := v.Index(i).Interface()\n if isTruthy(raw) {\n result = append(result, raw)\n }\n }\n return result\n}\n\n// Truthy is the exported form of isTruthy \u2014 JavaScript\'s `Boolean(x)`\n// semantics \u2014 for generated `NewXxxProps` code lowering a conditional\n// inline-object spread condition on an `interface{}` prop (whose runtime\n// value may be a string, number, bool, \u2026). Keeps the spread bag\'s\n// inclusion test faithful to JS rather than string-biased (#1752).\nfunc Truthy(v any) bool { return isTruthy(v) }\n\n// isTruthy mirrors JavaScript\'s `Boolean(x)` for the value shapes the\n// template path actually receives \u2014 nil / false / 0 / "" are falsy.\n// Other shapes (non-empty maps, slices, structs, true) are truthy, in\n// line with JS\'s "objects are truthy" rule.\nfunc isTruthy(v any) bool {\n if v == nil {\n return false\n }\n switch x := v.(type) {\n case bool:\n return x\n case string:\n return x != ""\n case int:\n return x != 0\n case int8, int16, int32, int64:\n return reflect.ValueOf(v).Int() != 0\n case uint, uint8, uint16, uint32, uint64:\n return reflect.ValueOf(v).Uint() != 0\n case float32:\n // JS `Boolean(NaN)` is false regardless of float width \u2014 the\n // float64 arm below was the only one checking IsNaN, which\n // diverged from JS for `float32` NaN inputs (Copilot review on\n // #1445). Widening to float64 for the IsNaN check keeps the\n // two branches in lock-step.\n return x != 0 && !math.IsNaN(float64(x))\n case float64:\n return x != 0 && !math.IsNaN(x)\n }\n return true\n}\n\n// =============================================================================\n// Higher-order Array Methods\n// =============================================================================\n\n// fieldValue projects item.field for the higher-order predicate\n// helpers. The field name arrives in JS casing; structs resolve via\n// the capitalized Go convention (FieldByName inside getFieldValue),\n// maps via getFieldValue\'s case-variant lookup \u2014 the same dual\n// support Sort/Reduce gained in #1487, extended here so JSON-decoded\n// data (map items) participates instead of being silently skipped.\n// nil-safe: missing fields and nil items project to nil.\nfunc fieldValue(item any, field string) any {\n return getFieldValue(item, capitalize(field))\n}\n\n// Every returns true if every item\'s field is truthy under JS\n// `Boolean(item.field)` semantics. Mirrors JavaScript\'s\n// Array.prototype.every(item => item.field) \u2014 including being\n// vacuously true for an empty receiver.\nfunc Every(items any, field string) bool {\n v := reflect.ValueOf(items)\n if v.Kind() != reflect.Slice && v.Kind() != reflect.Array {\n return false\n }\n for i := 0; i < v.Len(); i++ {\n if !isTruthy(fieldValue(v.Index(i).Interface(), field)) {\n return false\n }\n }\n return true\n}\n\n// Some returns true if at least one item\'s field is truthy. Mirrors\n// JavaScript\'s Array.prototype.some(item => item.field).\nfunc Some(items any, field string) bool {\n v := reflect.ValueOf(items)\n if v.Kind() != reflect.Slice && v.Kind() != reflect.Array {\n return false\n }\n for i := 0; i < v.Len(); i++ {\n if isTruthy(fieldValue(v.Index(i).Interface(), field)) {\n return true\n }\n }\n return false\n}\n\n// Filter returns items where item.field == value.\n// Mirrors JavaScript\'s Array.prototype.filter(item => item.field === value).\n// Returns []any to allow chaining with other bf_* functions.\nfunc Filter(items any, field string, value any) []any {\n v := reflect.ValueOf(items)\n if v.Kind() != reflect.Slice && v.Kind() != reflect.Array {\n return nil\n }\n var result []any\n for i := 0; i < v.Len(); i++ {\n item := v.Index(i).Interface()\n if reflect.DeepEqual(fieldValue(item, field), value) {\n result = append(result, item)\n }\n }\n return result\n}\n\n// Find returns the first item where item.field == value, or nil if not found.\n// Mirrors JavaScript\'s Array.prototype.find(item => item.field === value).\nfunc Find(items any, field string, value any) any {\n v := reflect.ValueOf(items)\n if v.Kind() != reflect.Slice && v.Kind() != reflect.Array {\n return nil\n }\n for i := 0; i < v.Len(); i++ {\n item := v.Index(i).Interface()\n if reflect.DeepEqual(fieldValue(item, field), value) {\n return item\n }\n }\n return nil\n}\n\n// FindIndex returns the index of the first item where item.field == value, or -1.\n// Mirrors JavaScript\'s Array.prototype.findIndex(item => item.field === value).\nfunc FindIndex(items any, field string, value any) int {\n v := reflect.ValueOf(items)\n if v.Kind() != reflect.Slice && v.Kind() != reflect.Array {\n return -1\n }\n for i := 0; i < v.Len(); i++ {\n if reflect.DeepEqual(fieldValue(v.Index(i).Interface(), field), value) {\n return i\n }\n }\n return -1\n}\n\n// FindLast returns the last item where item.field == value, or nil if not found.\n// Mirrors JavaScript\'s Array.prototype.findLast(item => item.field === value).\nfunc FindLast(items any, field string, value any) any {\n v := reflect.ValueOf(items)\n if v.Kind() != reflect.Slice && v.Kind() != reflect.Array {\n return nil\n }\n for i := v.Len() - 1; i >= 0; i-- {\n item := v.Index(i).Interface()\n if reflect.DeepEqual(fieldValue(item, field), value) {\n return item\n }\n }\n return nil\n}\n\n// FindLastIndex returns the index of the last item where item.field == value, or -1.\n// Mirrors JavaScript\'s Array.prototype.findLastIndex(item => item.field === value).\nfunc FindLastIndex(items any, field string, value any) int {\n v := reflect.ValueOf(items)\n if v.Kind() != reflect.Slice && v.Kind() != reflect.Array {\n return -1\n }\n for i := v.Len() - 1; i >= 0; i-- {\n if reflect.DeepEqual(fieldValue(v.Index(i).Interface(), field), value) {\n return i\n }\n }\n return -1\n}\n\n// sortKeySpec is one parsed comparison key. A simple comparator has\n// one; a `||`-chained multi-key comparator has several, applied in\n// order as tie-breakers.\ntype sortKeySpec struct {\n kind string // "self" | "field"\n name string // capitalised field name, or "" for "self"\n compareType string // "numeric" | "string" | "auto"\n direction string // "asc" | "desc"\n}\n\n// Sort returns a new stable-sorted slice. Lowers\n// `Array.prototype.sort` / `Array.prototype.toSorted` (#1448 Tier B).\n// Non-mutating \u2014 JS\'s mutate-vs-new distinction is moot in SSR\n// template context (templates render a snapshot).\n//\n// Call shape (the compiler emits one 4-string group per key):\n//\n// bf_sort <items> (<keyKind> <keyName> <compareType> <direction>)+\n//\n// keyKind: "self" | "field"\n// keyName: "" when keyKind == "self"; capitalised struct field\n// name (e.g. "Price") otherwise\n// compareType: "numeric" | "string" | "auto"\n// direction: "asc" | "desc"\n//\n// The groups cover the accepted comparator catalogue: `a.f - b.f`,\n// `a - b`, `a[.f].localeCompare(b[.f])`, and relational-ternary keys\n// (`a.f > b.f ? 1 : -1` \u2192 "auto"), each `||`-chainable for multi-key\n// tie-breaks. Anything outside refuses at compile time (BF101 from the\n// JSX compiler) and never reaches this helper.\n//\n// "auto" compares numerically when both projected keys parse as\n// numbers, else lexically \u2014 mirroring the Perl `bf->sort` helper\'s\n// `looks_like_number` rule so the two template adapters stay\n// byte-equal. This diverges from JS `<`/`>` only for numeric strings.\n//\n// A future `nulls` knob can extend the per-key group without rewriting\n// existing call sites \u2014 each key already projects before comparing.\nfunc Sort(items any, spec ...string) []any {\n v := reflect.ValueOf(items)\n if v.Kind() != reflect.Slice && v.Kind() != reflect.Array {\n return nil\n }\n\n length := v.Len()\n if length == 0 {\n return []any{}\n }\n\n // Copy into a fresh []any so the sort is non-mutating regardless\n // of whether the receiver is `[]T` or `[]any`.\n result := make([]any, length)\n for i := 0; i < length; i++ {\n result[i] = v.Index(i).Interface()\n }\n\n keys := parseSortSpec(spec)\n sort.SliceStable(result, func(i, j int) bool {\n for _, k := range keys {\n ki := projectSortKey(result[i], k.kind, k.name)\n kj := projectSortKey(result[j], k.kind, k.name)\n c := compareSortKey(ki, kj, k.compareType)\n if c == 0 {\n continue // tie on this key \u2014 fall through to the next\n }\n if k.direction == "desc" {\n return c > 0\n }\n return c < 0\n }\n return false\n })\n\n return result\n}\n\n// parseSortSpec chunks the variadic operand list into 4-string key\n// groups. A trailing partial group (malformed emit) is ignored rather\n// than panicking \u2014 defensive, mirroring the helper\'s nil-safe stance.\nfunc parseSortSpec(spec []string) []sortKeySpec {\n var keys []sortKeySpec\n for i := 0; i+3 < len(spec); i += 4 {\n keys = append(keys, sortKeySpec{\n kind: spec[i],\n name: spec[i+1],\n compareType: spec[i+2],\n direction: spec[i+3],\n })\n }\n return keys\n}\n\n// compareSortKey returns -1 / 0 / 1 for two projected keys under the\n// given compare type (ascending orientation; the caller flips for\n// "desc"). "string" stringifies both (nil \u2192 "", matching the\n// documented `bf->string(undef) === ""` divergence). "auto" compares\n// numerically when both parse as numbers, else lexically.\nfunc compareSortKey(ki, kj any, compareType string) int {\n switch compareType {\n case "string":\n return strings.Compare(toString(ki), toString(kj))\n case "auto":\n ni, okI := toFloat64WithOK(ki)\n nj, okJ := toFloat64WithOK(kj)\n if okI && okJ {\n return cmpFloat(ni, nj)\n }\n return strings.Compare(toString(ki), toString(kj))\n default: // numeric\n return cmpFloat(toFloat64(ki), toFloat64(kj))\n }\n}\n\nfunc cmpFloat(a, b float64) int {\n if a < b {\n return -1\n }\n if a > b {\n return 1\n }\n return 0\n}\n\n// toFloat64WithOK reports a value\'s numeric float and whether it is\n// number-like. Genuine numeric kinds always qualify; strings qualify\n// when they parse as a float (so the "auto" compare path matches the\n// Perl `looks_like_number` rule). Everything else is non-numeric.\nfunc toFloat64WithOK(v any) (float64, bool) {\n switch n := v.(type) {\n case int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64, float32, float64:\n return toFloat64(v), true\n case string:\n f, err := strconv.ParseFloat(strings.TrimSpace(n), 64)\n if err != nil {\n return 0, false\n }\n return f, true\n default:\n return 0, false\n }\n}\n\n// projectSortKey reduces an item to the value the comparator\n// actually compares. For `keyKind == "field"` it reads the named\n// struct field; for `keyKind == "self"` (primitive arrays) it\n// returns the item unchanged.\nfunc projectSortKey(item any, keyKind, keyName string) any {\n if keyKind == "field" {\n return getFieldValue(item, keyName)\n }\n return item\n}\n\n// getFieldValue extracts a struct field value using reflection. For\n// map receivers it falls back to case-variant lookup so JSON-decoded\n// user data (`map[string]any{"price": 30}`) and PascalCase-emitted\n// test data both resolve under a single key name. (#1487)\nfunc getFieldValue(item any, field string) any {\n v := reflect.ValueOf(item)\n // Defensive IsNil guards mirror `SpreadAttrs` \u2014 keeps the helper\n // safe against typed-nil pointer / nil-interface items inside a\n // `[]any` so a single bad row doesn\'t crash the whole sort.\n if v.Kind() == reflect.Interface {\n if v.IsNil() {\n return nil\n }\n v = v.Elem()\n }\n if v.Kind() == reflect.Ptr {\n if v.IsNil() {\n return nil\n }\n v = v.Elem()\n }\n\n if v.Kind() == reflect.Map {\n keyType := v.Type().Key()\n if keyType.Kind() != reflect.String {\n return nil\n }\n // Convert the lookup string to the map\'s actual key type so\n // maps keyed by a named string type (`type Key string`) don\'t\n // panic with `value of type string is not assignable to type X`.\n lookup := func(s string) (any, bool) {\n k := reflect.ValueOf(s).Convert(keyType)\n if mv := v.MapIndex(k); mv.IsValid() {\n return mv.Interface(), true\n }\n return nil, false\n }\n if r, ok := lookup(field); ok {\n return r\n }\n if cap := capitalize(field); cap != field {\n if r, ok := lookup(cap); ok {\n return r\n }\n }\n if low := decapitalize(field); low != field {\n if r, ok := lookup(low); ok {\n return r\n }\n }\n // All-lowercase fallback: a Go-initialism field projects as an\n // all-caps key (`id` \u2192 `ID`), and `decapitalize("ID")` only\n // lowers the first char (`iD`), so the JS-keyed map ("id") still\n // misses. Try the fully-lowered key last to resolve it.\n if lower := strings.ToLower(field); lower != field && lower != decapitalize(field) {\n if r, ok := lookup(lower); ok {\n return r\n }\n }\n return nil\n }\n\n if v.Kind() != reflect.Struct {\n return nil\n }\n\n fieldVal := v.FieldByName(field)\n if !fieldVal.IsValid() {\n // Case-variant fallback: the evaluator carries the JS field name\n // (`id` / `url`) against a Go-capitalised struct field (`ID` / `URL`),\n // which exact `FieldByName` misses and the initialism rules can\'t be\n // reproduced char-for-char here. Match case-insensitively instead \u2014\n // `FieldByNameFunc` returns the zero Value (\u2192 nil) for an ambiguous\n // match, so it stays safe. The legacy bf_sort/bf_reduce pass an\n // already-capitalised name, so they hit the exact match above and never\n // reach this fallback.\n fieldVal = v.FieldByNameFunc(func(n string) bool { return strings.EqualFold(n, field) })\n if !fieldVal.IsValid() {\n return nil\n }\n }\n return fieldVal.Interface()\n}\n\n// AsMap normalizes a dynamically-typed prop value into a\n// map[string]interface{} for object-valued context bindings\n// (`lowerProviderMapMemberValue`, go-template-adapter.ts). A caller-side\n// `interface{}` field can legally hold ANY string-keyed map kind \u2014 a Go\n// handler modelling `Record<string, string>` naturally passes\n// map[string]string \u2014 so a bare `.(map[string]interface{})` type assertion\n// would silently drop provided values (#2111 review). Returns nil (never an\n// empty map) when the value is absent \u2014 nil interface, typed-nil map or\n// pointer, or any non-map / non-string-keyed value \u2014 so the generated\n// `?? {}` fallback can distinguish "missing" (fall back) from "present but\n// empty" (use as-is). map[string]interface{} passes through without copying.\nfunc AsMap(v any) map[string]interface{} {\n if v == nil {\n return nil\n }\n if m, ok := v.(map[string]interface{}); ok {\n if m == nil {\n return nil\n }\n return m\n }\n rv := reflect.ValueOf(v)\n if rv.Kind() == reflect.Ptr {\n if rv.IsNil() {\n return nil\n }\n rv = rv.Elem()\n }\n if rv.Kind() != reflect.Map || rv.Type().Key().Kind() != reflect.String || rv.IsNil() {\n return nil\n }\n out := make(map[string]interface{}, rv.Len())\n iter := rv.MapRange()\n for iter.Next() {\n out[iter.Key().String()] = iter.Value().Interface()\n }\n return out\n}\n\n// capitalize uppercases the first character of a string.\nfunc capitalize(s string) string {\n if s == "" {\n return s\n }\n return strings.ToUpper(s[:1]) + s[1:]\n}\n\n// decapitalize lowercases the first character of a string. Used by\n// `getFieldValue`\'s map-receiver fallback when the projected key\n// name is PascalCase but the receiver carries lowercase JS-style\n// keys (the inverse of the `capitalize` lookup).\nfunc decapitalize(s string) string {\n if s == "" {\n return s\n }\n return strings.ToLower(s[:1]) + s[1:]\n}\n\n// Reduce folds an array into a scalar via the arithmetic-fold\n// catalogue (#1448 Tier C). It lowers `Array.prototype.reduce(fn, init)`\n// and `Array.prototype.reduceRight(fn, init)` for the shapes\n// `(acc, x) => acc <op> x` and `(acc, x) => acc <op> x.field`:\n//\n// bf_reduce <items> "<op>" "<keyKind>" "<keyName>" "<type>" "<init>" "<direction>"\n//\n// direction: "left" (reduce) | "right" (reduceRight). Only changes the\n// result for string concatenation; numeric folds commute.\n//\n// op: "+" | "*"\n// keyKind: "self" | "field"\n// keyName: "" when keyKind == "self"; capitalised struct field name\n// (e.g. "Duration") otherwise\n// type: "numeric" | "string"\n// init: the fold\'s start value \u2014 the compiler emits the *decoded*\n// seed, so numeric inits arrive as canonical decimal\n// (`1_000`/`0x10` already normalised to `1000`/`16`) that\n// ParseFloat accepts, and string inits arrive as escape-free\n// contents\n//\n// Numeric folds accumulate as float64; each projected key is read via\n// `toFloat64WithOK`, so numeric *strings* ("5" \u2192 5) parse and\n// non-numeric values fold as 0 \u2014 matching Perl\'s\n// `looks_like_number ? $n : 0` so the two template adapters stay\n// byte-equal. String folds concatenate (toString per projected key,\n// matching the documented `bf->string(undef) === ""` convention). The\n// init seeds the accumulator, so an empty receiver returns the init\n// unchanged \u2014 exactly like JS `reduce(fn, init)`. Anything outside the\n// catalogue refuses at compile time (BF101 from the JSX compiler) and\n// never reaches here.\n//\n// Two documented divergences from the JS / Hono path, both rare and\n// mirroring the `bf_sort` "auto" caveat:\n// - float64 stringification differs for sums whose binary expansion\n// isn\'t exact (e.g. 0.1 + 0.2);\n// - numeric-*string* keys fold numerically here, but JS `+`\n// string-concatenates once an operand is a string, so\n// numeric-string data can render differently under CSR.\n//\n// Genuine numbers \u2014 the common SSR case \u2014 agree across all three.\nfunc Reduce(items any, op, keyKind, keyName, typ, init, direction string) any {\n v := reflect.ValueOf(items)\n isSlice := v.Kind() == reflect.Slice || v.Kind() == reflect.Array\n\n // `direction == "right"` (reduceRight) folds right-to-left. Only\n // observable for string concatenation \u2014 numeric sum / product are\n // commutative, so the order doesn\'t change the result there. Build a\n // start/stop/step triple so both folds share one loop shape.\n start, stop, step := 0, 0, 1\n if isSlice {\n stop = v.Len()\n if direction == "right" {\n start, stop, step = v.Len()-1, -1, -1\n }\n }\n\n if typ == "string" {\n acc := init\n if isSlice {\n for i := start; i != stop; i += step {\n key := projectSortKey(v.Index(i).Interface(), keyKind, keyName)\n acc += toString(key)\n }\n }\n return acc\n }\n\n // numeric fold\n acc, _ := strconv.ParseFloat(strings.TrimSpace(init), 64)\n if isSlice {\n for i := start; i != stop; i += step {\n key := projectSortKey(v.Index(i).Interface(), keyKind, keyName)\n // `toFloat64WithOK` parses numeric *strings* ("5" \u2192 5) and\n // returns 0 for non-numeric values \u2014 mirroring Perl\'s\n // `looks_like_number ? $n : 0` so numeric-string data folds\n // byte-equal across adapters (the same rule `bf_sort`\'s\n // "auto" compare uses). Plain `toFloat64` would zero "5".\n n, _ := toFloat64WithOK(key)\n if op == "*" {\n acc *= n\n } else {\n acc += n\n }\n }\n }\n return acc\n}\n\n// =============================================================================\n// HTML/Template Helpers\n// =============================================================================\n\n// Comment returns an HTML comment string for hydration markers.\n// The "bf-" prefix is automatically added.\nfunc Comment(content string) template.HTML {\n return template.HTML("<!--bf-" + content + "-->")\n}\n\n// TextStart returns an HTML comment start marker for reactive text expressions.\n// Format: <!--bf:slotId-->\nfunc TextStart(slotId string) template.HTML {\n return template.HTML("<!--bf:" + slotId + "-->")\n}\n\n// TextEnd returns an HTML comment end marker for reactive text expressions.\n// Format: <!--/-->\nfunc TextEnd() template.HTML {\n return "<!--/-->"\n}\n\n// ScopeComment emits a fragment-rooted scope marker. See spec/compiler.md\n// "Slot identity" for the wire format. Loud-fails on marshal errors\n// (same policy as JSON / BfPropsAttr).\nfunc ScopeComment(props interface{}) (template.HTML, error) {\n scopeID := getStringField(props, "ScopeID")\n hostSegment := ""\n if host := getStringField(props, "BfParent"); host != "" {\n mount := getStringField(props, "BfMount")\n hostSegment = "|h=" + host + "|m=" + mount\n }\n propsJSON := ""\n if getBoolField(props, "BfIsRoot") {\n pJSON, err := json.Marshal(props)\n if err != nil {\n return "", err\n }\n propsJSON = "|" + string(pJSON)\n }\n return template.HTML("<!--bf-scope:" + scopeID + hostSegment + propsJSON + "-->"), nil\n}\n\n// TemplateFuncMap returns the helpers that need access to the executing\n// template set itself, closed over the *template.Template the component\n// defines are parsed into. Register it alongside FuncMap BEFORE parsing:\n//\n// t := template.New("")\n// t.Funcs(bf.FuncMap()).Funcs(bf.TemplateFuncMap(t))\n// template.Must(t.Parse(src))\n//\n// bf_tmpl executes a named define from the same set and returns its\n// output \u2014 used for the per-call-site children defines the Go adapter\n// emits when JSX children passed to an imported component contain\n// template actions (nested components, dynamic text) and therefore\n// cannot be baked to a static HTML string (#1896). Reentrant execution\n// of an html/template set from inside a FuncMap function is safe: the\n// escape analysis over every define completes before the outer\n// Execute begins evaluating.\nfunc TemplateFuncMap(t *template.Template) template.FuncMap {\n return template.FuncMap{\n "bf_tmpl": func(name string, data interface{}) (template.HTML, error) {\n var buf bytes.Buffer\n if err := t.ExecuteTemplate(&buf, name, data); err != nil {\n return "", err\n }\n return template.HTML(buf.String()), nil\n },\n }\n}\n\n// WithChildren returns a shallow copy of a component Props struct with its\n// Children field replaced by the given pre-rendered fragment (#1896). The\n// props value stays by-value semantics: callers\' originals are untouched.\n// A props type without a Children field passes through unchanged \u2014 the\n// child template then simply has no children to render, matching the\n// pre-#1896 behaviour.\nfunc WithChildren(props interface{}, children template.HTML) (interface{}, error) {\n v := reflect.ValueOf(props)\n if v.Kind() == reflect.Ptr {\n v = v.Elem()\n }\n if v.Kind() != reflect.Struct {\n return props, nil\n }\n field := v.FieldByName("Children")\n if !field.IsValid() {\n return props, nil\n }\n copyPtr := reflect.New(v.Type())\n copyPtr.Elem().Set(v)\n target := copyPtr.Elem().FieldByName("Children")\n switch {\n case target.Kind() == reflect.Interface:\n target.Set(reflect.ValueOf(children))\n case target.Kind() == reflect.String:\n // Covers both `string` and `template.HTML`-typed fields.\n target.SetString(string(children))\n default:\n return props, fmt.Errorf("bf_with_children: unsupported Children field type %s", target.Type())\n }\n return copyPtr.Elem().Interface(), nil\n}\n\n// PortalHTML parses and executes a template string with the provided data.\n// Used for rendering dynamic portal content where the template string\n// contains Go template expressions (e.g., {{if .Open}}open{{end}}).\n//\n// The template string is parsed fresh each time to support dynamic content.\n// Standard Go template functions (if, range, eq, etc.) are available.\nfunc PortalHTML(data interface{}, tmplStr string) template.HTML {\n // Create a new template with the FuncMap for custom functions\n t, err := template.New("portal").Funcs(FuncMap()).Parse(tmplStr)\n if err != nil {\n // Return error message as HTML comment for debugging\n return template.HTML("<!-- bfPortalHTML error: " + err.Error() + " -->")\n }\n\n var buf bytes.Buffer\n if err := t.Execute(&buf, data); err != nil {\n return template.HTML("<!-- bfPortalHTML exec error: " + err.Error() + " -->")\n }\n\n return template.HTML(buf.String())\n}\n\n// =============================================================================\n// Portal Collection\n// =============================================================================\n\n// PortalContent represents a single portal\'s content to be rendered at body end.\ntype PortalContent struct {\n ID string // Unique portal ID for hydration matching\n OwnerID string // Owner scope ID for find() support\n Content template.HTML // Portal HTML content\n}\n\n// PortalCollector collects portal content during template rendering.\n// Portal content is rendered at </body> to avoid z-index issues.\ntype PortalCollector struct {\n portals []PortalContent\n counter int\n}\n\n// NewPortalCollector creates a new PortalCollector.\nfunc NewPortalCollector() *PortalCollector {\n return &PortalCollector{\n portals: []PortalContent{},\n counter: 0,\n }\n}\n\n// Add registers portal content to be rendered at body end.\nfunc (pc *PortalCollector) Add(ownerID string, content template.HTML) string {\n pc.counter++\n id := "bf-portal-" + strconv.Itoa(pc.counter)\n pc.portals = append(pc.portals, PortalContent{\n ID: id,\n OwnerID: ownerID,\n Content: content,\n })\n return "" // Return empty string for template use\n}\n\n// Render outputs all collected portals as HTML.\n// Each portal is wrapped in a div with bf-pi (portal ID) and bf-po (portal owner).\nfunc (pc *PortalCollector) Render() template.HTML {\n if pc == nil || len(pc.portals) == 0 {\n return ""\n }\n var buf strings.Builder\n for _, p := range pc.portals {\n buf.WriteString(`<div bf-pi="`)\n buf.WriteString(p.ID)\n buf.WriteString(`" bf-po="`)\n buf.WriteString(p.OwnerID)\n buf.WriteString(`">`)\n buf.WriteString(string(p.Content))\n buf.WriteString("</div>\\n")\n }\n return template.HTML(buf.String())\n}\n\n// =============================================================================\n// Script Collection\n// =============================================================================\n\n// ScriptCollector collects client scripts with deduplication.\n// It preserves insertion order for deterministic output.\ntype ScriptCollector struct {\n scripts map[string]bool\n order []string\n}\n\n// NewScriptCollector creates a new ScriptCollector.\nfunc NewScriptCollector() *ScriptCollector {\n return &ScriptCollector{\n scripts: make(map[string]bool),\n order: []string{},\n }\n}\n\n// Register adds a script source to the collection.\n// Duplicate scripts are ignored (only first registration counts).\nfunc (sc *ScriptCollector) Register(src string) string {\n if sc.scripts[src] {\n return "" // Already registered\n }\n sc.scripts[src] = true\n sc.order = append(sc.order, src)\n return "" // Return empty string for template use\n}\n\n// Scripts returns all registered scripts in insertion order.\nfunc (sc *ScriptCollector) Scripts() []string {\n return sc.order\n}\n\n// BfScripts generates script tags for all registered scripts.\n// Returns HTML safe for embedding in templates.\nfunc BfScripts(collector *ScriptCollector) template.HTML {\n if collector == nil {\n return ""\n }\n var result strings.Builder\n for _, src := range collector.Scripts() {\n result.WriteString(`<script type="module" src="`)\n result.WriteString(src)\n result.WriteString(`"></script>`)\n result.WriteString("\\n")\n }\n return template.HTML(result.String())\n}\n\n// =============================================================================\n// Component Renderer\n// =============================================================================\n\n// RenderContext contains all data needed to render a component page.\n// The layout function receives this context to build the final HTML.\ntype RenderContext struct {\n // ComponentName is the template name being rendered\n ComponentName string\n\n // Props is the component props (for layout to access if needed)\n Props interface{}\n\n // ComponentHTML is the rendered component template output\n ComponentHTML template.HTML\n\n // Portals contains collected portal content to render at body end\n Portals template.HTML\n\n // Scripts contains the collected JS script tags\n Scripts template.HTML\n\n // Title is the page title (defaults to "{ComponentName} - BarefootJS")\n Title string\n\n // Heading is the page heading. Empty string means no heading.\n Heading string\n\n // Extra holds additional user-defined data for the layout\n Extra map[string]interface{}\n}\n\n// LayoutFunc renders the final HTML page given the render context.\ntype LayoutFunc func(ctx *RenderContext) string\n\n// Renderer renders BarefootJS components with a customizable layout.\ntype Renderer struct {\n templates *template.Template\n layout LayoutFunc\n}\n\n// NewRenderer creates a Renderer with the given templates and layout function.\n//\n// Example usage:\n//\n// renderer := bf.NewRenderer(templates, func(ctx *bf.RenderContext) string {\n// return fmt.Sprintf(`<!DOCTYPE html>\n// <html>\n// <head><title>%s</title></head>\n// <body>%s%s</body>\n// </html>`, ctx.Title, ctx.ComponentHTML, ctx.Scripts)\n// })\nfunc NewRenderer(tmpl *template.Template, layout LayoutFunc) *Renderer {\n return &Renderer{\n templates: tmpl,\n layout: layout,\n }\n}\n\n// RenderOptions configures a single render call.\ntype RenderOptions struct {\n // ComponentName is the template name to render (required)\n ComponentName string\n\n // Props is the component props (must be a pointer to struct with Scripts field)\n Props interface{}\n\n // Title is the page title. If empty, defaults to "{ComponentName} - BarefootJS"\n Title string\n\n // Heading is the page heading. If empty, no heading is shown.\n Heading string\n\n // Extra holds additional data to pass to the layout\n Extra map[string]interface{}\n}\n\n// Render renders a component to a full HTML page using the configured layout.\n// Child component props are automatically detected (any slice field with ScopeID/Scripts).\n// renderTemplateErrorPanel formats a Go template execution error into a\n// fragment of HTML that\'s visible in the browser. The panel is\n// HTML-escaped so a faulty template name (anything from `template:\n// "..."`) can\'t smuggle markup back into the page. Keep the styling\n// inline so the panel surfaces even when the project\'s CSS hasn\'t\n// loaded yet (e.g. the failure aborted before the stylesheet links\n// emitted).\n//\n// Surfaced for the #1442 echo repro: a template referencing\n// `.Todo.Done` (instead of the range dot\'s `.Done`) used to fail\n// silently \u2014 Go\'s html/template aborted mid-stream, the partial body\n// flushed as a 200, and the user saw a truncated list with no console\n// signal. With this panel they get the template name, the error\n// message, and a "what to look at" hint inline.\nfunc renderTemplateErrorPanel(componentName string, err error) string {\n return `<div style="margin:1em 0;padding:1em;border:2px solid #d33;background:#fff5f5;color:#900;font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;font-size:13px;line-height:1.5"><strong style="display:block;margin-bottom:.5em">Template error in <code>` +\n template.HTMLEscapeString(componentName) +\n `</code></strong><pre style="margin:0;white-space:pre-wrap;word-break:break-word">` +\n template.HTMLEscapeString(err.Error()) +\n `</pre><div style="margin-top:.75em;font-size:12px;opacity:.7">Common cause: a JSX expression referenced a name the adapter could not resolve to a struct field. Open the matching <code>dist/templates/*.tmpl</code> for the unresolved reference, then fix the source component.</div></div>`\n}\n\n// renderComponentInto wires a component\'s props (script/portal collectors,\n// child-slot scope ids + hydration, root marking) against the PROVIDED\n// collectors and returns just the component\'s HTML \u2014 no layout. Both Render\n// (one fresh collector pair per page) and RenderFragment (a shared pair across\n// several islands) funnel through here so their wiring stays identical.\nfunc (r *Renderer) renderComponentInto(opts RenderOptions, scriptCollector *ScriptCollector, portalCollector *PortalCollector) template.HTML {\n // Inject the shared collectors into the props.\n setScriptsField(opts.Props, scriptCollector)\n setPortalsField(opts.Props, portalCollector)\n\n // Auto-detect and process child component props (slices)\n childSlices := findChildComponentSlices(opts.Props)\n for _, slice := range childSlices {\n setScopeIDsOnSlice(slice)\n setScriptsOnSlice(slice, scriptCollector)\n setPortalsOnSlice(slice, portalCollector)\n setBoolOnSlice(slice, "BfIsChild", true)\n }\n\n // Auto-detect and process single child component props\n singleChildren := findSingleChildComponents(opts.Props)\n for _, child := range singleChildren {\n setScopeIDOnSingle(child)\n setScriptsOnSingle(child, scriptCollector)\n setPortalsOnSingle(child, portalCollector)\n setBoolField(child, "BfIsChild", true)\n }\n\n // Mark the root component so BfPropsAttr emits bf-p only for it\n setBoolField(opts.Props, "BfIsRoot", true)\n\n // Render the component template.\n //\n // Errors here are NOT silently dropped. The original implementation\n // ignored the return value of `ExecuteTemplate`, which masked a real\n // onboarding failure mode: a template referencing a non-existent\n // field (`.Todo.Done` instead of the range dot\'s `.Done`) caused\n // html/template to abort mid-stream, the partial output got\n // returned, and the HTTP server happily flushed a 200 with a\n // truncated body. No error log, no signal \u2014 the user just saw a\n // blank list (#1442 echo TodoApp repro).\n //\n // Now we capture the error and replace the partial output with a\n // visible inline panel (dev mode) or a fenced error comment\n // (production), so the cause is on-screen and grep-able in logs.\n // Either way the renderer also writes to stderr so structured log\n // aggregators see it.\n var componentBuf strings.Builder\n if err := r.templates.ExecuteTemplate(&componentBuf, opts.ComponentName, opts.Props); err != nil {\n fmt.Fprintf(os.Stderr, "barefoot: template %q failed to render: %v\\n", opts.ComponentName, err)\n // Preserve whatever the template did manage to emit before\n // failing (Go\'s text/template flushes incrementally), but\n // follow it with a clearly-marked error block so the user\n // notices something is wrong instead of seeing a silent\n // truncation.\n componentBuf.WriteString(renderTemplateErrorPanel(opts.ComponentName, err))\n }\n\n return template.HTML(componentBuf.String())\n}\n\nfunc (r *Renderer) Render(opts RenderOptions) string {\n // One script + portal collector pair for the whole page.\n scriptCollector := NewScriptCollector()\n portalCollector := NewPortalCollector()\n\n componentHTML := r.renderComponentInto(opts, scriptCollector, portalCollector)\n\n // Determine title (default: "{ComponentName} - BarefootJS")\n title := opts.Title\n if title == "" {\n title = opts.ComponentName + " - BarefootJS"\n }\n\n // Heading (empty means no heading)\n heading := opts.Heading\n\n // Build render context\n ctx := &RenderContext{\n ComponentName: opts.ComponentName,\n Props: opts.Props,\n ComponentHTML: componentHTML,\n Portals: portalCollector.Render(),\n Scripts: BfScripts(scriptCollector),\n Title: title,\n Heading: heading,\n Extra: opts.Extra,\n }\n\n return r.layout(ctx)\n}\n\n// RenderFragment renders a single island subtree into the caller-provided\n// script and portal collectors and returns just its HTML \u2014 no page layout.\n//\n// It exists for hand-authored "region shell" pages (the `@barefootjs/router`\n// showcase): a layout that places several independent islands \u2014 e.g. a header\n// ThemeToggle, an `<aside bf-region>` Sidebar, and a `<PageShell>` wrapping the\n// route content \u2014 must collect ALL their scripts and portals into ONE place so\n// the runtime (`barefoot.js`) and each island\'s client JS are emitted exactly\n// once and share a single reactive instance. Render each island with the same\n// collectors, splice the returned HTML into the shell, then emit\n// `BfScripts(sc)` and `pc.Render()` once at the end of the document.\n//\n// Each fragment is treated as its own root (it emits `bf-p` like any top-level\n// island); nested children declared in its props are wired as children, exactly\n// as in Render.\nfunc (r *Renderer) RenderFragment(opts RenderOptions, scriptCollector *ScriptCollector, portalCollector *PortalCollector) template.HTML {\n return r.renderComponentInto(opts, scriptCollector, portalCollector)\n}\n\n// setScriptsField sets the Scripts field on a struct using reflection.\nfunc setScriptsField(v interface{}, collector *ScriptCollector) {\n val := reflect.ValueOf(v)\n if val.Kind() == reflect.Ptr {\n val = val.Elem()\n }\n if val.Kind() != reflect.Struct {\n return\n }\n field := val.FieldByName("Scripts")\n if field.IsValid() && field.CanSet() {\n field.Set(reflect.ValueOf(collector))\n }\n}\n\n// setPortalsField sets the Portals field on a struct using reflection.\nfunc setPortalsField(v interface{}, collector *PortalCollector) {\n val := reflect.ValueOf(v)\n if val.Kind() == reflect.Ptr {\n val = val.Elem()\n }\n if val.Kind() != reflect.Struct {\n return\n }\n field := val.FieldByName("Portals")\n if field.IsValid() && field.CanSet() {\n field.Set(reflect.ValueOf(collector))\n }\n}\n\n// getStringField extracts a string field from a struct using reflection.\nfunc setBoolField(v interface{}, fieldName string, val bool) {\n rv := reflect.ValueOf(v)\n if rv.Kind() == reflect.Ptr {\n rv = rv.Elem()\n }\n if rv.Kind() != reflect.Struct {\n return\n }\n field := rv.FieldByName(fieldName)\n if field.IsValid() && field.CanSet() && field.Kind() == reflect.Bool {\n field.SetBool(val)\n }\n}\n\nfunc getBoolField(v interface{}, fieldName string) bool {\n val := reflect.ValueOf(v)\n if val.Kind() == reflect.Ptr {\n val = val.Elem()\n }\n if val.Kind() != reflect.Struct {\n return false\n }\n field := val.FieldByName(fieldName)\n if !field.IsValid() || field.Kind() != reflect.Bool {\n return false\n }\n return field.Bool()\n}\n\nfunc getStringField(v interface{}, fieldName string) string {\n val := reflect.ValueOf(v)\n if val.Kind() == reflect.Ptr {\n val = val.Elem()\n }\n if val.Kind() != reflect.Struct {\n return ""\n }\n field := val.FieldByName(fieldName)\n if !field.IsValid() || field.Kind() != reflect.String {\n return ""\n }\n return field.String()\n}\n\n// scopeIDChars is the alphabet for auto-generated ScopeID suffixes. It\n// mirrors the `randomID` helper the go-template adapter emits into the\n// generated New<Component>Props constructors so runtime-assigned and\n// constructor-assigned ids are indistinguishable.\nconst scopeIDChars = "abcdefghijklmnopqrstuvwxyz0123456789"\n\n// randomScopeSuffix returns a random lowercase-alphanumeric string of\n// length n. math/rand (auto-seeded since Go 1.20) is sufficient here: the\n// suffix only needs to be unique enough to keep a page\'s bf-s scope ids\n// from colliding, not cryptographically unpredictable.\nfunc randomScopeSuffix(n int) string {\n b := make([]byte, n)\n for i := range b {\n b[i] = scopeIDChars[rand.Intn(len(scopeIDChars))]\n }\n return string(b)\n}\n\n// scopeIDPrefix derives the human-readable ScopeID prefix from a child\n// component\'s type, e.g. `TodoItemProps` \u2192 `TodoItem`. Matches the\n// `"<Component>_" + randomID(6)` shape the generated constructors use.\nfunc scopeIDPrefix(t reflect.Type) string {\n for t.Kind() == reflect.Ptr {\n t = t.Elem()\n }\n return strings.TrimSuffix(t.Name(), "Props")\n}\n\n// assignScopeID fills a child component\'s ScopeID with a generated id when\n// the caller left it empty, so application code doesn\'t have to mint scope\n// ids by hand (the parent\'s New<Component>Props constructor does the same\n// for components built through it). A non-empty ScopeID is left untouched,\n// so callers can still pin a stable id when they need one.\nfunc assignScopeID(structVal reflect.Value, prefix string) {\n field := structVal.FieldByName("ScopeID")\n if !field.IsValid() || !field.CanSet() || field.Kind() != reflect.String {\n return\n }\n if field.String() != "" {\n return\n }\n id := randomScopeSuffix(6)\n if prefix != "" {\n id = prefix + "_" + id\n }\n field.SetString(id)\n}\n\n// setScopeIDsOnSlice assigns a generated ScopeID to every child in a slice\n// whose ScopeID is empty.\nfunc setScopeIDsOnSlice(slice interface{}) {\n v := reflect.ValueOf(slice)\n if v.Kind() != reflect.Slice {\n return\n }\n prefix := scopeIDPrefix(v.Type().Elem())\n for i := 0; i < v.Len(); i++ {\n item := v.Index(i)\n if item.Kind() == reflect.Ptr {\n if item.IsNil() {\n continue\n }\n item = item.Elem()\n }\n if item.Kind() == reflect.Struct {\n assignScopeID(item, prefix)\n }\n }\n}\n\n// setScopeIDOnSingle assigns a generated ScopeID to a single child\n// component when its ScopeID is empty.\nfunc setScopeIDOnSingle(child interface{}) {\n v := reflect.ValueOf(child)\n if v.Kind() == reflect.Ptr {\n v = v.Elem()\n }\n if v.Kind() != reflect.Struct {\n return\n }\n assignScopeID(v, scopeIDPrefix(v.Type()))\n}\n\n// findChildComponentSlices finds slice fields containing child component props.\n// Child props are identified by having ScopeID and Scripts fields.\nfunc findChildComponentSlices(props interface{}) []interface{} {\n var result []interface{}\n\n val := reflect.ValueOf(props)\n if val.Kind() == reflect.Ptr {\n val = val.Elem()\n }\n if val.Kind() != reflect.Struct {\n return result\n }\n\n for i := 0; i < val.NumField(); i++ {\n field := val.Field(i)\n if field.Kind() != reflect.Slice || field.Len() == 0 {\n continue\n }\n\n elem := field.Index(0)\n if elem.Kind() == reflect.Ptr {\n elem = elem.Elem()\n }\n if elem.Kind() != reflect.Struct {\n continue\n }\n\n hasScopeID := elem.FieldByName("ScopeID").IsValid()\n hasScripts := elem.FieldByName("Scripts").IsValid()\n\n if hasScopeID && hasScripts {\n result = append(result, field.Interface())\n }\n }\n\n return result\n}\n\n// setScriptsOnSlice sets Scripts on all items in a slice.\nfunc setScriptsOnSlice(slice interface{}, collector *ScriptCollector) {\n val := reflect.ValueOf(slice)\n if val.Kind() != reflect.Slice {\n return\n }\n for i := 0; i < val.Len(); i++ {\n item := val.Index(i)\n if item.Kind() == reflect.Ptr {\n item = item.Elem()\n }\n if item.Kind() == reflect.Struct {\n field := item.FieldByName("Scripts")\n if field.IsValid() && field.CanSet() {\n field.Set(reflect.ValueOf(collector))\n }\n }\n }\n}\n\n// setBoolOnSlice sets a bool field on all items in a slice.\nfunc setBoolOnSlice(slice interface{}, fieldName string, val bool) {\n v := reflect.ValueOf(slice)\n if v.Kind() != reflect.Slice {\n return\n }\n for i := 0; i < v.Len(); i++ {\n item := v.Index(i)\n if item.Kind() == reflect.Ptr {\n item = item.Elem()\n }\n if item.Kind() == reflect.Struct {\n field := item.FieldByName(fieldName)\n if field.IsValid() && field.CanSet() && field.Kind() == reflect.Bool {\n field.SetBool(val)\n }\n }\n }\n}\n\n// setPortalsOnSlice sets Portals on all items in a slice.\nfunc setPortalsOnSlice(slice interface{}, collector *PortalCollector) {\n val := reflect.ValueOf(slice)\n if val.Kind() != reflect.Slice {\n return\n }\n for i := 0; i < val.Len(); i++ {\n item := val.Index(i)\n if item.Kind() == reflect.Ptr {\n item = item.Elem()\n }\n if item.Kind() == reflect.Struct {\n field := item.FieldByName("Portals")\n if field.IsValid() && field.CanSet() {\n field.Set(reflect.ValueOf(collector))\n }\n }\n }\n}\n\n// findSingleChildComponents finds single struct fields containing child component props.\n// Child props are identified by having ScopeID and Scripts fields.\nfunc findSingleChildComponents(props interface{}) []interface{} {\n var result []interface{}\n\n val := reflect.ValueOf(props)\n if val.Kind() == reflect.Ptr {\n val = val.Elem()\n }\n if val.Kind() != reflect.Struct {\n return result\n }\n\n for i := 0; i < val.NumField(); i++ {\n field := val.Field(i)\n\n // Handle pointer to struct\n if field.Kind() == reflect.Ptr {\n if field.IsNil() {\n continue\n }\n field = field.Elem()\n }\n\n // Skip non-struct fields (slices handled by findChildComponentSlices)\n if field.Kind() != reflect.Struct {\n continue\n }\n\n hasScopeID := field.FieldByName("ScopeID").IsValid()\n hasScripts := field.FieldByName("Scripts").IsValid()\n\n if hasScopeID && hasScripts {\n result = append(result, field.Addr().Interface())\n }\n }\n\n return result\n}\n\n// setScriptsOnSingle sets Scripts on a single struct child component.\nfunc setScriptsOnSingle(child interface{}, collector *ScriptCollector) {\n val := reflect.ValueOf(child)\n if val.Kind() == reflect.Ptr {\n val = val.Elem()\n }\n if val.Kind() == reflect.Struct {\n field := val.FieldByName("Scripts")\n if field.IsValid() && field.CanSet() {\n field.Set(reflect.ValueOf(collector))\n }\n }\n}\n\n// setPortalsOnSingle sets Portals on a single struct child component.\nfunc setPortalsOnSingle(child interface{}, collector *PortalCollector) {\n val := reflect.ValueOf(child)\n if val.Kind() == reflect.Ptr {\n val = val.Elem()\n }\n if val.Kind() == reflect.Struct {\n field := val.FieldByName("Portals")\n if field.IsValid() && field.CanSet() {\n field.Set(reflect.ValueOf(collector))\n }\n }\n}\n\n// =============================================================================\n// Internal Helpers\n// =============================================================================\n\nfunc toFloat64(v any) float64 {\n switch n := v.(type) {\n case int:\n return float64(n)\n case int8:\n return float64(n)\n case int16:\n return float64(n)\n case int32:\n return float64(n)\n case int64:\n return float64(n)\n case uint:\n return float64(n)\n case uint8:\n return float64(n)\n case uint16:\n return float64(n)\n case uint32:\n return float64(n)\n case uint64:\n return float64(n)\n case float32:\n return float64(n)\n case float64:\n return n\n default:\n return 0\n }\n}\n\nfunc toInt(v any) int {\n switch n := v.(type) {\n case int:\n return n\n case int8:\n return int(n)\n case int16:\n return int(n)\n case int32:\n return int(n)\n case int64:\n return int(n)\n case uint:\n return int(n)\n case uint8:\n return int(n)\n case uint16:\n return int(n)\n case uint32:\n return int(n)\n case uint64:\n return int(n)\n case float32:\n return int(n)\n case float64:\n return int(n)\n default:\n return 0\n }\n}\n\nfunc isIntLike(v any) bool {\n switch v.(type) {\n case int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64:\n return true\n default:\n return false\n }\n}\n\nfunc toString(v any) string {\n switch s := v.(type) {\n case string:\n return s\n case int:\n return strconv.Itoa(s)\n case int64:\n return strconv.FormatInt(s, 10)\n case float64:\n return strconv.FormatFloat(s, \'f\', -1, 64)\n case bool:\n return strconv.FormatBool(s)\n default:\n return ""\n }\n}\n\n// =============================================================================\n// searchParams() \u2014 request-scoped environment signal (router v0.5, #1922)\n// =============================================================================\n\n// SearchParams is the SSR view of the request query string behind the\n// reactive searchParams() environment signal. The route handler builds it\n// from the request URL and assigns it to the component\'s SearchParams input\n// field; the generated template reads it via `.SearchParams.Get "key"`.\n//\n// The zero value is an empty query (url.Values.Get tolerates a nil map), so a\n// render with no request query \u2014 e.g. the adapter conformance harness, which\n// issues no query string \u2014 resolves every key to "", which the template\'s\n// `or`/`??` fallback turns into the author\'s default.\ntype SearchParams struct {\n values url.Values\n}\n\n// NewSearchParams parses a raw query string (with or without a leading "?")\n// into a SearchParams. A malformed query yields an empty set rather than an\n// error, mirroring the browser\'s URLSearchParams, which never throws on junk.\n//\n// Typical handler use (net/http):\n//\n// in := MyComponentInput{SearchParams: bf.NewSearchParams(r.URL.RawQuery)}\nfunc NewSearchParams(raw string) SearchParams {\n raw = strings.TrimPrefix(raw, "?")\n values, err := url.ParseQuery(raw)\n if err != nil {\n values = url.Values{}\n }\n return SearchParams{values: values}\n}\n\n// Get returns the first value associated with key, or "" when the key is\n// absent. This mirrors url.Values.Get, which also returns "" for a\n// present-but-empty value (`?sort=`). Safe on the zero value (nil map).\n//\n// This is not byte-for-byte URLSearchParams.get under the template\'s `??`\n// lowering. JS distinguishes absent (`null`) from present-but-empty (`""`):\n// `null ?? d` yields the default, but `"" ?? d` keeps the empty string. The\n// Go adapter lowers `??` to the `or` builtin \u2014 Go templates have no\n// null-coalescing operator \u2014 so here BOTH an absent key and a present-but-\n// empty value fall back to the author\'s default. The conformance fixture only\n// exercises the absent-key default, where the two runtimes agree; the\n// empty-string divergence is the same general `?? \u2192 or` limitation that\n// applies to any `x ?? default` the Go adapter lowers.\nfunc (s SearchParams) Get(key string) string {\n return s.values.Get(key)\n}\n';
27333
+ evalGoSource = 'package bf\n\nimport (\n "encoding/json"\n "math"\n "reflect"\n "sort"\n "strconv"\n "strings"\n "unicode/utf8"\n)\n\n// =============================================================================\n// Lightweight ParsedExpr evaluator (issue #2018)\n// =============================================================================\n//\n// Templates cannot carry a lambda in expression position, which is why the\n// adapter historically special-cased higher-order callbacks (reduce / sort /\n// map / filter / find) into fixed shapes (bf_sort\'s comparator catalogue,\n// bf_reduce\'s +/* fold). This evaluator replaces that ad-hoc list: a callback\n// BODY is carried as a pure `ParsedExpr` subtree (the same structured IR the\n// compiler already produces) and evaluated here against an environment\n// (`{acc, item, \u2026captured free vars}`).\n//\n// Scope: higher-order callback bodies only. Ordinary expressions stay lowered\n// to template-native syntax \u2014 this is NOT a general expression engine.\n//\n// The accepted pure subset and its semantics (evaluation order, coercion,\n// equality, allowed operators / builtins) are documented in spec/compiler.md\n// ("ParsedExpr Evaluator Semantics") and pinned isomorphically by the golden\n// vectors in packages/adapter-tests/vectors/eval-vectors.json (shared\n// with the Perl evaluator). The coercion rules below are the literal JS rules\n// (ToNumber / ToString / ToBoolean) \u2014 deliberately NOT the divergent\n// bf->string / bf_reduce helper conventions \u2014 so the contract is unambiguous\n// and the backends stay byte-isomorphic.\n//\n// String semantics operate on Unicode code points / UTF-8 bytes: `.length`\n// counts code points and relational `<`/`>` compares byte order. This equals\n// the JS reference (UTF-16 code units) across the BMP \u2014 the range template\n// data uses \u2014 and matches the Perl evaluator exactly (the primary "same input\n// \u2192 same output" contract is between the two SSR backends). Astral-plane\n// characters (where JS counts a surrogate pair as length 2 and orders by\n// surrogate code units) are a documented divergence region, alongside the\n// already-documented non-ASCII relational / localeCompare carve-outs\n// (spec/compiler.md). Both backends stay equal; only the JS reference differs,\n// and no corpus vector exercises the astral range.\n\n// EvalExpr evaluates a pure ParsedExpr (carried as its JSON encoding) against\n// env. The result is a value in the JSON domain: a number (Go int or float64 \u2014\n// both are the single JS number type; e.g. `.length` returns int, arithmetic\n// returns float64), string, bool, nil, []any, or map[string]any. A malformed\n// tree yields nil.\nfunc EvalExpr(exprJSON string, env map[string]any) any {\n var node any\n if err := json.Unmarshal([]byte(exprJSON), &node); err != nil {\n return nil\n }\n return EvalNode(node, env)\n}\n\n// EvalNode evaluates an already-decoded ParsedExpr node (a map[string]any with\n// a "kind" discriminator) against env. Exported so callers that already hold a\n// decoded tree (e.g. the golden-vector harness) skip a re-marshal.\nfunc EvalNode(node any, env map[string]any) any {\n n, ok := node.(map[string]any)\n if !ok {\n return nil\n }\n kind, _ := n["kind"].(string)\n switch kind {\n case "literal":\n return n["value"]\n\n case "identifier":\n name, _ := n["name"].(string)\n return env[name]\n\n case "binary":\n op, _ := n["op"].(string)\n return evalBinary(op, EvalNode(n["left"], env), EvalNode(n["right"], env))\n\n case "unary":\n op, _ := n["op"].(string)\n return evalUnary(op, EvalNode(n["argument"], env))\n\n case "logical":\n op, _ := n["op"].(string)\n left := EvalNode(n["left"], env)\n switch op {\n case "&&":\n if !evalTruthy(left) {\n return left\n }\n return EvalNode(n["right"], env)\n case "||":\n if evalTruthy(left) {\n return left\n }\n return EvalNode(n["right"], env)\n case "??":\n if left == nil {\n return EvalNode(n["right"], env)\n }\n return left\n }\n return nil\n\n case "conditional":\n if evalTruthy(EvalNode(n["test"], env)) {\n return EvalNode(n["consequent"], env)\n }\n return EvalNode(n["alternate"], env)\n\n case "member":\n prop, _ := n["property"].(string)\n return evalReadProperty(EvalNode(n["object"], env), prop)\n\n case "index-access":\n return evalReadIndex(EvalNode(n["object"], env), EvalNode(n["index"], env))\n\n case "call":\n // A nested `.map(cb)` / `.filter(cb)` callback call (#2094): syntactically\n // a `call` whose callee is `<recv>.map`/`<recv>.filter` and whose first\n // argument is an `arrow` \u2014 the SAME shape `asCallbackMethodCall`\n // recognizes at compile time, and the shape the `eval-vectors.json`\n // golden corpus itself carries (it stores the genuine `ParsedExpr`, not\n // a bespoke wrapper). Checked BEFORE the builtin-callee gate below,\n // since `<recv>.map` would otherwise resolve to a non-builtin member\n // callee and refuse.\n if method, objNode, arrowNode, ok := evalArrayCallbackCall(n); ok {\n return evalArrayCallback(method, objNode, arrowNode, env)\n }\n name := evalBuiltinName(n["callee"])\n if name == "" {\n return nil\n }\n rawArgs, _ := n["args"].([]any)\n args := make([]any, len(rawArgs))\n for i, a := range rawArgs {\n args[i] = EvalNode(a, env)\n }\n return evalCallBuiltin(name, args)\n\n case "template-literal":\n parts, _ := n["parts"].([]any)\n var sb strings.Builder\n for _, p := range parts {\n pm, _ := p.(map[string]any)\n if t, _ := pm["type"].(string); t == "string" {\n s, _ := pm["value"].(string)\n sb.WriteString(s)\n } else {\n sb.WriteString(evalToString(EvalNode(pm["expr"], env)))\n }\n }\n return sb.String()\n\n case "array-literal":\n elems, _ := n["elements"].([]any)\n out := make([]any, len(elems))\n for i, e := range elems {\n out[i] = EvalNode(e, env)\n }\n return out\n\n case "object-literal":\n props, _ := n["properties"].([]any)\n out := make(map[string]any, len(props))\n for _, p := range props {\n pm, _ := p.(map[string]any)\n key, _ := pm["key"].(string)\n out[key] = EvalNode(pm["value"], env)\n }\n return out\n\n case "array-method":\n // `.includes(x)` / `.join(sep?)` are the `array-method` shapes the\n // evaluator executes (the JS reference\'s `evaluate` "array-method" arm,\n // eval-reference.ts). A nested `.map`/`.filter` is NOT an\n // `array-method` node \u2014 it reaches the `call` case above (it carries\n // an `arrow` callback, not a plain `args` list). Every other\n // array/string method (`slice`, `flat`, \u2026) is refused upstream\n // (BF101) and never reaches here.\n method, _ := n["method"].(string)\n rawArgs, _ := n["args"].([]any)\n if method == "includes" && len(rawArgs) == 1 {\n return evalIncludes(EvalNode(n["object"], env), EvalNode(rawArgs[0], env))\n }\n if method == "join" && len(rawArgs) <= 1 {\n sep := ","\n if len(rawArgs) == 1 {\n sep = evalToString(EvalNode(rawArgs[0], env))\n }\n return evalJoin(EvalNode(n["object"], env), sep)\n }\n return nil\n }\n // arrow-fn / higher-order / unsupported: a callback body containing these\n // is refused upstream (BF101); never reached here.\n return nil\n}\n\n// evalArrayCallbackCall reports whether the decoded `call` node `n` is a\n// nested `.map(cb)` / `.filter(cb)` callback call (#2094): its callee is a\n// non-computed member `<recv>.map`/`<recv>.filter` and its first argument is\n// an `arrow` node. Returns the method name, the (still-encoded) receiver\n// object node, and the (still-encoded) arrow node.\nfunc evalArrayCallbackCall(n map[string]any) (method string, object any, arrow map[string]any, ok bool) {\n callee, _ := n["callee"].(map[string]any)\n if callee == nil || callee["kind"] != "member" {\n return "", nil, nil, false\n }\n if computed, _ := callee["computed"].(bool); computed {\n return "", nil, nil, false\n }\n prop, _ := callee["property"].(string)\n if prop != "map" && prop != "filter" {\n return "", nil, nil, false\n }\n rawArgs, _ := n["args"].([]any)\n if len(rawArgs) == 0 {\n return "", nil, nil, false\n }\n arrowNode, _ := rawArgs[0].(map[string]any)\n if arrowNode == nil || arrowNode["kind"] != "arrow" {\n return "", nil, nil, false\n }\n return prop, callee["object"], arrowNode, true\n}\n\n// evalArrayCallback executes a nested `.map`/`.filter` callback call: evaluates\n// the receiver, then evaluates the arrow body per element in a CHILD env that\n// binds the arrow\'s first param to the element and (when the arrow declares a\n// second param) the second to the integer index \u2014 both 1- and 2-param arrows\n// are supported. `map` keeps one result per element (order-preserving);\n// `filter` keeps the elements whose body evaluates truthy. A non-array\n// receiver degrades to nil (unreachable for a body the compiler validated,\n// since the receiver of a nested `.map`/`.filter` is itself gated upstream).\nfunc evalArrayCallback(method string, objectNode any, arrowNode map[string]any, env map[string]any) any {\n arr := toAnySlice(EvalNode(objectNode, env))\n if arr == nil {\n return nil\n }\n rawParams, _ := arrowNode["params"].([]any)\n params := make([]string, len(rawParams))\n for i, p := range rawParams {\n params[i], _ = p.(string)\n }\n body := arrowNode["body"]\n callCb := func(item any, index int) any {\n inner := make(map[string]any, len(env)+2)\n for k, v := range env {\n inner[k] = v\n }\n if len(params) > 0 {\n inner[params[0]] = item\n }\n if len(params) > 1 {\n inner[params[1]] = index\n }\n return EvalNode(body, inner)\n }\n if method == "map" {\n out := make([]any, len(arr))\n for i, item := range arr {\n out[i] = callCb(item, i)\n }\n return out\n }\n out := []any{}\n for i, item := range arr {\n if evalTruthy(callCb(item, i)) {\n out = append(out, item)\n }\n }\n return out\n}\n\n// evalJoin implements `.join(sep)`: elements ToString\'d and joined; a\n// null/undefined element ToStrings to the empty string (matching JS\n// `Array.prototype.join`, which skips null/undefined rather than rendering\n// the literal string "null"/"undefined"). A non-array receiver degrades to\n// the empty string (unreachable for a validated body).\nfunc evalJoin(obj any, sep string) string {\n arr := toAnySlice(obj)\n if arr == nil {\n return ""\n }\n parts := make([]string, len(arr))\n for i, el := range arr {\n if el == nil {\n parts[i] = ""\n continue\n }\n parts[i] = evalToString(el)\n }\n return strings.Join(parts, sep)\n}\n\n// ---------------------------------------------------------------------------\n// JS coercion primitives (ToNumber / ToString / ToBoolean), pinned so the\n// evaluator matches the JS reference. These are JS-faithful and intentionally\n// distinct from the bf->string / Number helpers, which diverge (null \u2192 "",\n// null/"" \u2192 NaN) for SSR-survival reasons that do not apply to the evaluator.\n// ---------------------------------------------------------------------------\n\nfunc evalToNumber(v any) float64 {\n switch x := v.(type) {\n case nil:\n return 0\n case bool:\n if x {\n return 1\n }\n return 0\n case string:\n t := strings.TrimSpace(x)\n if t == "" {\n return 0\n }\n // Decimal / exponent / "Infinity" numeric strings parse JS-faithfully\n // (ParseFloat handles these, matching the Perl evaluator). The\n // radix-prefixed forms JS Number() also accepts ("0x10" / "0o17" /\n // "0b101") are a documented divergence region: they yield NaN here, as\n // they do in the Perl evaluator (looks_like_number is false for them),\n // so Go==Perl while differing from the JS reference. Template data\n // carries JSON numbers, not radix-string literals, so this never\n // arises in practice.\n f, err := strconv.ParseFloat(t, 64)\n if err != nil {\n return math.NaN()\n }\n return f\n default:\n if evalIsNumeric(v) {\n return toFloat64(v)\n }\n return math.NaN()\n }\n}\n\n// evalIsNumeric reports whether v is one of the Go numeric types (all of\n// which are the single JS "number" type to the evaluator).\nfunc evalIsNumeric(v any) bool {\n switch v.(type) {\n case int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64, float32, float64:\n return true\n }\n return false\n}\n\nfunc evalToString(v any) string {\n if v == nil {\n return "null"\n }\n // JS spells the non-finite doubles "Infinity" / "-Infinity" / "NaN"; the\n // runtime String() helper (fmt %v) would render "+Inf" / "-Inf" / "NaN",\n // so the non-finite cases are pinned here to stay JS-faithful (and match\n // the Perl evaluator\'s _to_string). Strings, bools and nil are JS-faithful\n // through String() (nil is already handled above as "null").\n //\n // Finite-number formatting is a documented divergence region. Go\'s fmt is\n // shortest-round-trip (it matches JS\'s *digits*, e.g. 0.1+0.2 \u2192\n // "0.30000000000000004"), but its exponent threshold/padding differ from\n // JS Number::toString for very large / very small magnitudes (Go renders\n // 1e6 as "1e+06" where JS keeps "1000000", and "1e-07" vs JS "1e-7").\n // Perl\'s `%.15g` instead diverges on *precision* (the pinned helper-vector\n // "0.3" case). A fully JS-faithful Number::toString is not reimplemented\n // here because Perl has no shortest-round-trip formatter, so the three\n // could never all agree; the common integer / short-decimal range \u2014 what\n // arithmetic over template data produces \u2014 renders identically across all\n // three. Only the \xB10 sign is normalised below, since it is cheap and the\n // realisticly-reachable case.\n if f, ok := v.(float64); ok {\n if math.IsNaN(f) {\n return "NaN"\n }\n if math.IsInf(f, 1) {\n return "Infinity"\n }\n if math.IsInf(f, -1) {\n return "-Infinity"\n }\n // JS `String(-0)` is "0", but fmt %v renders Go\'s negative zero as\n // "-0". `f == 0` matches both \xB10, normalising to JS\'s spelling. (A\n // unary `-` on a zero operand is the way the evaluator can produce -0.)\n if f == 0 {\n return "0"\n }\n }\n // Non-primitive operands (arrays / objects) in ToString position are\n // outside the evaluator subset \u2014 the JS reference refuses them, and the\n // compiler gates such a callback body with BF101 before it ever reaches\n // the runtime. This evaluator runs already-validated bodies, so it does\n // not re-reject here; String() (fmt %v) is a best-effort fallback for that\n // unreachable path, never exercised by the in-subset corpus.\n return String(v)\n}\n\nfunc evalTruthy(v any) bool {\n return isTruthy(v)\n}\n\n// ---------------------------------------------------------------------------\n// Operators\n// ---------------------------------------------------------------------------\n\nfunc evalBinary(op string, l, r any) any {\n switch op {\n case "+":\n // JS `+`: string concatenation once either operand is a string,\n // numeric addition otherwise.\n if _, lok := l.(string); lok {\n return evalToString(l) + evalToString(r)\n }\n if _, rok := r.(string); rok {\n return evalToString(l) + evalToString(r)\n }\n return evalToNumber(l) + evalToNumber(r)\n case "-":\n return evalToNumber(l) - evalToNumber(r)\n case "*":\n return evalToNumber(l) * evalToNumber(r)\n case "/":\n return evalToNumber(l) / evalToNumber(r)\n case "%":\n return math.Mod(evalToNumber(l), evalToNumber(r))\n case "<", "<=", ">", ">=":\n return evalRelational(op, l, r)\n case "===":\n return evalStrictEq(l, r)\n case "!==":\n return !evalStrictEq(l, r)\n }\n // Loose equality / bitwise / shift are out of the subset.\n return nil\n}\n\nfunc evalRelational(op string, l, r any) bool {\n // JS Abstract Relational Comparison: both strings \u2192 compare by code\n // unit; otherwise coerce both to numbers (a NaN operand makes every\n // comparison false).\n var c int\n ls, lok := l.(string)\n rs, rok := r.(string)\n if lok && rok {\n if ls < rs {\n c = -1\n } else if ls > rs {\n c = 1\n }\n } else {\n ln := evalToNumber(l)\n rn := evalToNumber(r)\n if math.IsNaN(ln) || math.IsNaN(rn) {\n return false\n }\n if ln < rn {\n c = -1\n } else if ln > rn {\n c = 1\n }\n }\n switch op {\n case "<":\n return c < 0\n case "<=":\n return c <= 0\n case ">":\n return c > 0\n case ">=":\n return c >= 0\n }\n return false\n}\n\nfunc evalStrictEq(l, r any) bool {\n // Strict `===`: equal JS type and value, no coercion. All numeric Go\n // types are the single JS "number" type, so int 2 === float64 2.\n lnum := evalIsNumeric(l)\n rnum := evalIsNumeric(r)\n if lnum && rnum {\n lf, rf := toFloat64(l), toFloat64(r)\n if math.IsNaN(lf) || math.IsNaN(rf) {\n return false\n }\n return lf == rf\n }\n if lnum != rnum {\n return false\n }\n switch lv := l.(type) {\n case nil:\n return r == nil\n case string:\n rv, ok := r.(string)\n return ok && rv == lv\n case bool:\n rv, ok := r.(bool)\n return ok && rv == lv\n }\n // Non-primitive operands (arrays / objects) are outside the subset: the JS\n // reference refuses `===` on them and the compiler gates such a body with\n // BF101 upstream, so this is unreachable for an in-subset corpus. The\n // runtime trusts that gate rather than re-validating, returning false\n // here (it does not attempt JS reference identity, which templates can\'t\n // model anyway).\n return false\n}\n\n// evalSameValueZero implements `Array.prototype.includes`\'s membership\n// comparison: `===` except `NaN` equals itself (JS\'s SameValueZero). Reuses\n// evalStrictEq \u2014 the one divergence, both operands NaN, is checked first;\n// every other pair (including "one NaN, one not") falls through to the same\n// equality `evalBinary`\'s `===` uses, so the two operators stay in lockstep.\nfunc evalSameValueZero(a, b any) bool {\n if evalIsNumeric(a) && evalIsNumeric(b) {\n af, bf := toFloat64(a), toFloat64(b)\n if math.IsNaN(af) && math.IsNaN(bf) {\n return true\n }\n }\n return evalStrictEq(a, b)\n}\n\n// evalIncludes implements `.includes(needle)`, shared between\n// `Array.prototype.includes` (SameValueZero membership over a slice/array\n// receiver) and `String.prototype.includes` (substring search), mirroring\n// the receiver-type dispatch the SSR template lowering already does\n// (`bf_includes`). Any other receiver type is not a JS `.includes` target;\n// this degrades to false rather than panicking (there is no receiver here\n// for which JS itself would throw), matching the JS reference\n// (eval-reference.ts `includes`).\nfunc evalIncludes(obj, needle any) bool {\n if arr := toAnySlice(obj); arr != nil {\n for _, el := range arr {\n if evalSameValueZero(el, needle) {\n return true\n }\n }\n return false\n }\n if s, ok := obj.(string); ok {\n return strings.Contains(s, evalToString(needle))\n }\n return false\n}\n\nfunc evalUnary(op string, v any) any {\n switch op {\n case "!":\n return !evalTruthy(v)\n case "-":\n return -evalToNumber(v)\n case "+":\n return evalToNumber(v)\n }\n return nil\n}\n\n// ---------------------------------------------------------------------------\n// Built-in calls (the deterministic allowlist). Locale-sensitive builtins\n// (localeCompare) are deliberately excluded to keep the backends isomorphic.\n// ---------------------------------------------------------------------------\n\n// evalBuiltinName resolves a `call` callee to its builtin name (e.g.\n// "Math.max"), or "" if the callee is not an allowlisted builtin reference.\nfunc evalBuiltinName(callee any) string {\n cm, ok := callee.(map[string]any)\n if !ok {\n return ""\n }\n switch cm["kind"] {\n case "identifier":\n name, _ := cm["name"].(string)\n return name\n case "member":\n if computed, _ := cm["computed"].(bool); computed {\n return ""\n }\n obj, _ := cm["object"].(map[string]any)\n if obj == nil || obj["kind"] != "identifier" {\n return ""\n }\n objName, _ := obj["name"].(string)\n prop, _ := cm["property"].(string)\n return objName + "." + prop\n }\n return ""\n}\n\n// evalMathRound rounds a half toward +Infinity (JS Math.round: 2.5\u21923,\n// -2.5\u2192-2), matching the existing `round` helper rather than Go\'s math.Round.\nfunc evalMathRound(n float64) float64 {\n // floor(n+0.5) yields +0 for x in [-0.5, -0], where JS Math.round returns\n // -0. That sign is only observable through a subsequent division\n // (`1 / Math.round(-0.5)` is -Infinity in JS, +Infinity here) \u2014 through\n // ToString both \xB10 render "0". It is left as +0 deliberately: the two SSR\n // backends must stay equal, and Perl can\'t reproduce the -0 divisor sign\n // without fragile, version-dependent zero handling (its native `/` even\n // dies on a zero divisor). So Math.round\'s -0 is a JS-reference-only\n // divergence region, like the astral-plane / radix-string carve-outs.\n return math.Floor(n + 0.5)\n}\n\nfunc evalCallBuiltin(name string, args []any) any {\n switch name {\n case "Math.max":\n if len(args) == 0 {\n return math.Inf(-1)\n }\n m := evalToNumber(args[0])\n for _, a := range args[1:] {\n m = math.Max(m, evalToNumber(a))\n }\n return m\n case "Math.min":\n if len(args) == 0 {\n return math.Inf(1)\n }\n m := evalToNumber(args[0])\n for _, a := range args[1:] {\n m = math.Min(m, evalToNumber(a))\n }\n return m\n case "Math.abs":\n return math.Abs(evalToNumber(arg0(args)))\n case "Math.floor":\n return math.Floor(evalToNumber(arg0(args)))\n case "Math.ceil":\n return math.Ceil(evalToNumber(arg0(args)))\n case "Math.round":\n return evalMathRound(evalToNumber(arg0(args)))\n case "String":\n return evalToString(arg0(args))\n case "Number":\n return evalToNumber(arg0(args))\n case "Boolean":\n return evalTruthy(arg0(args))\n }\n // Any other callee is outside the subset (refused upstream).\n return nil\n}\n\nfunc arg0(args []any) any {\n if len(args) == 0 {\n return nil\n }\n return args[0]\n}\n\n// ---------------------------------------------------------------------------\n// Member / index access\n// ---------------------------------------------------------------------------\n\nfunc evalReadProperty(obj any, key string) any {\n switch o := obj.(type) {\n case string:\n if key == "length" {\n // Code-point count (== JS UTF-16 length across the BMP, == Perl\n // `length`). RuneCountInString avoids the []rune allocation since\n // this can run inside comparator / reducer evaluation.\n return utf8.RuneCountInString(o)\n }\n return nil\n case []any:\n if key == "length" {\n return len(o)\n }\n return nil\n case map[string]any:\n // Case-variant lookup: a callback body reads the raw JS property\n // (`t.duration`), but test data / Go-keyed maps may carry PascalCase\n // keys (`{"Duration": \u2026}`). Reuse the field reader the sort / reduce\n // helpers use \u2014 it resolves `duration` \u2192 `Duration` and reads a\n // genuinely missing key as null (the backends\' single absent value).\n return getFieldValue(o, key)\n case nil:\n return nil\n default:\n // Real template data (Go structs): reuse the field reader the sort /\n // reduce helpers use, which handles case-variant keys.\n return getFieldValue(obj, key)\n }\n}\n\n// ---------------------------------------------------------------------------\n// Evaluator-driven higher-order folds (the generalization of bf_reduce /\n// bf_sort onto the evaluator)\n//\n// These prove the evaluator subsumes the special-cased callback catalogue:\n// the callback BODY is carried as a pure ParsedExpr (JSON) and evaluated per\n// element against an environment, so the op restriction (bf_reduce\'s +/*),\n// the acc-canonical form, and the comparator pattern restriction (bf_sort)\n// all disappear \u2014 any pure reducer / comparator body works. They are the\n// runtime half of the integration; the compiler-side emit migration (carrying\n// callback bodies as ParsedExpr) and the byte-equal divergence decision for\n// the string-`localeCompare` sort path (won\'t-fix for byte-equal SSR, see\n// spec/compiler.md Known limitations) are the remaining follow-up.\n// ---------------------------------------------------------------------------\n\n// toAnySlice copies a reflect-iterable receiver into a fresh []any, returning\n// nil for a non-slice/array (matching the bf_sort / bf_reduce nil-tolerance).\nfunc toAnySlice(items any) []any {\n v := reflect.ValueOf(items)\n // A nil interface yields an invalid Value (Kind() == Invalid, not a\n // panic), so the Slice/Array guard below already tolerates nil; the\n // explicit IsValid check just documents the nil-tolerance intent.\n if !v.IsValid() || (v.Kind() != reflect.Slice && v.Kind() != reflect.Array) {\n return nil\n }\n out := make([]any, v.Len())\n for i := range out {\n out[i] = v.Index(i).Interface()\n }\n return out\n}\n\n// FoldEval folds items into a value via the ParsedExpr evaluator. The reducer\n// body is a pure ParsedExpr (JSON) evaluated against `{accName: acc, itemName:\n// item}` plus the captured free vars in `baseEnv` for each element; `init`\n// seeds the accumulator and `direction` is "left" (reduce) or "right"\n// (reduceRight). This is the evaluator-based generalization of bf_reduce \u2014 any\n// reducer body, not just the `+`/`*` arithmetic catalogue, and `acc` may\n// appear anywhere in the body. `baseEnv` may be nil when the body captures no\n// outer references; the accName / itemName keys shadow any same-named base key.\nfunc FoldEval(items any, bodyJSON, accName, itemName string, init any, direction string, baseEnv map[string]any) any {\n var body any\n if err := json.Unmarshal([]byte(bodyJSON), &body); err != nil {\n return init\n }\n arr := toAnySlice(items)\n if direction == "right" {\n for i, j := 0, len(arr)-1; i < j; i, j = i+1, j-1 {\n arr[i], arr[j] = arr[j], arr[i]\n }\n }\n acc := init\n // Seed the env from the captured free vars once; acc / item are\n // overwritten each iteration (constant base keys carry through).\n env := make(map[string]any, len(baseEnv)+2)\n for k, v := range baseEnv {\n env[k] = v\n }\n for _, item := range arr {\n env[accName] = acc\n env[itemName] = item\n acc = EvalNode(body, env)\n }\n return acc\n}\n\n// SortEval returns a new stable-sorted slice ordered by a ParsedExpr\n// comparator body (JSON) evaluated against `{paramA: a, paramB: b}` plus the\n// captured free vars in `baseEnv` to a number (negative / zero / positive,\n// like a JS comparator). This is the evaluator-based generalization of bf_sort\n// \u2014 any comparator body, not just the subtraction / relational-ternary\n// catalogue. `baseEnv` may be nil. Non-mutating.\nfunc SortEval(items any, cmpJSON, paramA, paramB string, baseEnv map[string]any) []any {\n arr := toAnySlice(items)\n if arr == nil {\n return nil\n }\n var cmp any\n if err := json.Unmarshal([]byte(cmpJSON), &cmp); err != nil {\n return arr\n }\n // One env seeded from the captured free vars; the two operand keys are\n // overwritten per comparison (the comparator runs synchronously).\n env := make(map[string]any, len(baseEnv)+2)\n for k, v := range baseEnv {\n env[k] = v\n }\n sort.SliceStable(arr, func(i, j int) bool {\n env[paramA] = arr[i]\n env[paramB] = arr[j]\n return evalToNumber(EvalNode(cmp, env)) < 0\n })\n return arr\n}\n\n// ---------------------------------------------------------------------------\n// Evaluator-driven higher-order predicates (#2018, P2) \u2014 the generalization\n// of bf_filter / bf_find / bf_find_index / bf_every / bf_some onto the\n// evaluator. The predicate BODY travels as a pure ParsedExpr (JSON) and is\n// evaluated per element against `{param: item}` plus the captured free vars in\n// `baseEnv`, lifting the field-equality / truthiness restriction of the\n// special-cased helpers to any pure predicate. `baseEnv` may be nil.\n// ---------------------------------------------------------------------------\n\n// decodeEvalBody unmarshals a serialized ParsedExpr body; ok is false on bad\n// JSON (only reachable by a corrupt emit \u2014 the adapter always emits valid JSON).\nfunc decodeEvalBody(bodyJSON string) (any, bool) {\n var body any\n if err := json.Unmarshal([]byte(bodyJSON), &body); err != nil {\n return nil, false\n }\n return body, true\n}\n\n// seedPredEnv copies the captured free vars into a fresh env with room for the\n// single predicate param, which the callers overwrite per element.\nfunc seedPredEnv(baseEnv map[string]any) map[string]any {\n env := make(map[string]any, len(baseEnv)+1)\n for k, v := range baseEnv {\n env[k] = v\n }\n return env\n}\n\n// FilterEval returns a new slice of the elements for which the predicate body\n// evaluates truthy \u2014 the evaluator generalization of bf_filter. Returns a\n// non-nil empty slice when nothing matches (so a downstream `range` / `bf_join`\n// sees a real slice); returns nil only on a bad body.\nfunc FilterEval(items any, predJSON, param string, baseEnv map[string]any) []any {\n pred, ok := decodeEvalBody(predJSON)\n if !ok {\n return nil\n }\n env := seedPredEnv(baseEnv)\n out := []any{}\n for _, item := range toAnySlice(items) {\n env[param] = item\n if evalTruthy(EvalNode(pred, env)) {\n out = append(out, item)\n }\n }\n return out\n}\n\n// EveryEval reports whether every element satisfies the predicate (vacuously\n// true for an empty receiver, like JS) \u2014 the generalization of bf_every.\nfunc EveryEval(items any, predJSON, param string, baseEnv map[string]any) bool {\n pred, ok := decodeEvalBody(predJSON)\n if !ok {\n return false\n }\n env := seedPredEnv(baseEnv)\n for _, item := range toAnySlice(items) {\n env[param] = item\n if !evalTruthy(EvalNode(pred, env)) {\n return false\n }\n }\n return true\n}\n\n// SomeEval reports whether any element satisfies the predicate (false for an\n// empty receiver, like JS) \u2014 the generalization of bf_some.\nfunc SomeEval(items any, predJSON, param string, baseEnv map[string]any) bool {\n pred, ok := decodeEvalBody(predJSON)\n if !ok {\n return false\n }\n env := seedPredEnv(baseEnv)\n for _, item := range toAnySlice(items) {\n env[param] = item\n if evalTruthy(EvalNode(pred, env)) {\n return true\n }\n }\n return false\n}\n\n// FindEval returns the first element satisfying the predicate, or nil when none\n// does \u2014 the generalization of bf_find. `forward` false searches from the end\n// (findLast).\nfunc FindEval(items any, predJSON, param string, forward bool, baseEnv map[string]any) any {\n pred, ok := decodeEvalBody(predJSON)\n if !ok {\n return nil\n }\n env := seedPredEnv(baseEnv)\n arr := toAnySlice(items)\n for n := range arr {\n i := n\n if !forward {\n i = len(arr) - 1 - n\n }\n env[param] = arr[i]\n if evalTruthy(EvalNode(pred, env)) {\n return arr[i]\n }\n }\n return nil\n}\n\n// FindIndexEval returns the index of the first element satisfying the predicate,\n// or -1 when none does \u2014 the generalization of bf_find_index. `forward` false\n// searches from the end (findLastIndex).\nfunc FindIndexEval(items any, predJSON, param string, forward bool, baseEnv map[string]any) int {\n pred, ok := decodeEvalBody(predJSON)\n if !ok {\n return -1\n }\n env := seedPredEnv(baseEnv)\n arr := toAnySlice(items)\n for n := range arr {\n i := n\n if !forward {\n i = len(arr) - 1 - n\n }\n env[param] = arr[i]\n if evalTruthy(EvalNode(pred, env)) {\n return i\n }\n }\n return -1\n}\n\n// FlatMapEval projects each element through the projection body (a pure\n// ParsedExpr JSON evaluated against `{param: item}` + baseEnv) and flattens the\n// results one level \u2014 the evaluator generalization of bf_flat_map /\n// bf_flat_map_tuple. A projection that yields a slice contributes its elements;\n// any other value contributes itself (matching JS `.flatMap`, where a non-array\n// return is kept as a single element). Returns a non-nil empty slice on a bad\n// body so a downstream `range` / `bf_join` sees a real slice.\nfunc FlatMapEval(items any, projJSON, param string, baseEnv map[string]any) []any {\n proj, ok := decodeEvalBody(projJSON)\n if !ok {\n return []any{}\n }\n env := seedPredEnv(baseEnv)\n out := []any{}\n for _, item := range toAnySlice(items) {\n env[param] = item\n v := EvalNode(proj, env)\n // Flatten any slice/array kind one level, not just []any: real Go\n // template data projects a field like `i.tags` to a typed slice\n // (`[]string` / `[]int`), which `toAnySlice` normalizes to []any. A\n // non-slice value (string / number / struct / nil) contributes itself,\n // matching JS `.flatMap` (a non-array return is kept as one element).\n if sub := toAnySlice(v); sub != nil {\n out = append(out, sub...)\n } else {\n out = append(out, v)\n }\n }\n return out\n}\n\n// MapEval projects each element through the projection body (a pure ParsedExpr\n// JSON evaluated against `{param: item}` + baseEnv), keeping one result per\n// element \u2014 the value-producing `.map(cb)` lowering (#2073). Unlike\n// FlatMapEval there is no flatten: a projection yielding a slice contributes\n// that slice as a single element, matching JS `.map`. Returns a non-nil empty\n// slice on a bad body so a downstream `range` / `bf_join` sees a real slice.\nfunc MapEval(items any, projJSON, param string, baseEnv map[string]any) []any {\n proj, ok := decodeEvalBody(projJSON)\n if !ok {\n return []any{}\n }\n env := seedPredEnv(baseEnv)\n out := []any{}\n for _, item := range toAnySlice(items) {\n env[param] = item\n out = append(out, EvalNode(proj, env))\n }\n return out\n}\n\n// Env builds the captured-free-var environment for FoldEval / SortEval from a\n// flat key, value, key, value, \u2026 argument list \u2014 the adapter emits\n// `bf_env "k1" v1 "k2" v2 \u2026` for the free variables a callback body references\n// beyond its own params. An odd trailing key with no value is ignored; with no\n// pairs it returns an empty (non-nil) map, the no-capture case. A non-string\n// key (only reachable by a malformed template call, never by the adapter\'s\n// quoted-literal emit) is skipped rather than collapsed into an `env[""]` slot.\nfunc Env(pairs ...any) map[string]any {\n env := make(map[string]any, len(pairs)/2)\n for i := 0; i+1 < len(pairs); i += 2 {\n key, ok := pairs[i].(string)\n if !ok {\n continue\n }\n env[key] = pairs[i+1]\n }\n return env\n}\n\nfunc evalReadIndex(obj any, index any) any {\n switch o := obj.(type) {\n case []any:\n f := evalToNumber(index)\n i := int(f)\n if float64(i) != f || i < 0 || i >= len(o) {\n return nil\n }\n return o[i]\n case map[string]any:\n return o[evalToString(index)]\n case nil:\n return nil\n default:\n return getFieldValue(obj, evalToString(index))\n }\n}\n';
26522
27334
  streamingGoSource = `// Package bf \u2014 Out-of-Order Streaming SSR helpers
26523
27335
  //
26524
27336
  // Provides StreamRenderer for progressive page rendering using HTTP
@@ -26736,6 +27548,7 @@ function goCommonFiles() {
26736
27548
  "renderer.go": GO_RENDERER_GO,
26737
27549
  "env.go": GO_ENV_GO,
26738
27550
  "bf-runtime/bf.go": bfGoSource,
27551
+ "bf-runtime/eval.go": evalGoSource,
26739
27552
  "bf-runtime/streaming.go": streamingGoSource,
26740
27553
  "bf-runtime/bfdev/bfdev.go": bfdevGoSource,
26741
27554
  "bf-runtime/go.mod": GO_BF_RUNTIME_GO_MOD,
@@ -26750,13 +27563,16 @@ function goCommonFiles() {
26750
27563
  "public/styles.css": STYLES_CSS,
26751
27564
  "public/tokens.css": TOKENS_CSS,
26752
27565
  "public/uno.css": UNO_CSS_PLACEHOLDER,
27566
+ "public/favicon.svg": FAVICON_SVG,
26753
27567
  ".gitignore": GO_GITIGNORE
26754
27568
  };
26755
27569
  }
26756
27570
  function goScripts() {
26757
27571
  return {
26758
27572
  dev: 'go mod tidy && bf build && unocss && concurrently -k -n build,uno,server -c blue,magenta,green "bf build --watch" "unocss --watch" "go run ."',
26759
- build: "go mod tidy && bf build && unocss",
27573
+ // `--minify` (not on `dev`): matches the site's "~14 kB min+gzip"
27574
+ // runtime-size claim, which is measured on a minified build.
27575
+ build: "go mod tidy && bf build --minify && unocss",
26760
27576
  start: "go run ."
26761
27577
  };
26762
27578
  }
@@ -26851,6 +27667,7 @@ func defaultLayout(ctx *bf.RenderContext) string {
26851
27667
  <meta charset="utf-8" />
26852
27668
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
26853
27669
  <title>%s</title>
27670
+ ${faviconLinkTag("/static/favicon.svg")}
26854
27671
  ${CSS_LINKS_BEGIN}
26855
27672
  <!-- Link all three sheets so the browser fetches them in parallel \u2014
26856
27673
  chaining via styles.css @import would defer tokens/uno to a
@@ -26934,6 +27751,7 @@ func IsDev() bool {
26934
27751
  // inside Slot that's only reachable when AsChild=true.
26935
27752
  func loadTemplates() (*template.Template, error) {
26936
27753
  root := template.New("").Funcs(bf.FuncMap())
27754
+ root = root.Funcs(bf.TemplateFuncMap(root))
26937
27755
  if _, err := root.New("Tag").Parse(""); err != nil {
26938
27756
  return nil, err
26939
27757
  }
@@ -27243,6 +28061,7 @@ server.listen(port, () => {
27243
28061
  <script type="importmap">
27244
28062
  { "imports": { "@barefootjs/client/runtime": "/static/components/barefoot.js" } }
27245
28063
  </script>
28064
+ ${faviconLinkTag("/static/favicon.svg")}
27246
28065
  ${CSS_LINKS_BEGIN}
27247
28066
  <!-- Link all three sheets so the browser fetches them in parallel \u2014
27248
28067
  chaining via styles.css @import would defer tokens/uno to a
@@ -27305,11 +28124,14 @@ server.listen(port, () => {
27305
28124
  "public/styles.css": STYLES_CSS,
27306
28125
  "public/tokens.css": TOKENS_CSS,
27307
28126
  "public/uno.css": UNO_CSS_PLACEHOLDER,
28127
+ "public/favicon.svg": FAVICON_SVG,
27308
28128
  ".gitignore": CSR_GITIGNORE
27309
28129
  },
27310
28130
  scripts: {
27311
28131
  dev: 'bf build && unocss && concurrently -k -n build,uno,server -c blue,magenta,green "bf build --watch" "unocss --watch" "tsx watch server.ts"',
27312
- build: "bf build && unocss",
28132
+ // `--minify` (not on `dev`): matches the site's "~14 kB min+gzip"
28133
+ // runtime-size claim, which is measured on a minified build.
28134
+ build: "bf build --minify && unocss",
27313
28135
  start: "tsx server.ts"
27314
28136
  },
27315
28137
  dependencies: {
@@ -27444,6 +28266,7 @@ func defaultLayout(ctx *bf.RenderContext) string {
27444
28266
  <meta charset="utf-8" />
27445
28267
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
27446
28268
  <title>%s</title>
28269
+ ${faviconLinkTag("/static/favicon.svg")}
27447
28270
  ${CSS_LINKS_BEGIN}
27448
28271
  <!-- Link all three sheets so the browser fetches them in parallel \u2014
27449
28272
  chaining via styles.css @import would defer tokens/uno to a
@@ -27514,6 +28337,7 @@ import (
27514
28337
  // updated boilerplate).
27515
28338
  func loadTemplates() (*template.Template, error) {
27516
28339
  root := template.New("").Funcs(bf.FuncMap())
28340
+ root = root.Funcs(bf.TemplateFuncMap(root))
27517
28341
  if _, err := root.New("Tag").Parse(""); err != nil {
27518
28342
  return nil, err
27519
28343
  }
@@ -27803,6 +28627,7 @@ replace github.com/barefootjs/runtime/bf => ./bf-runtime
27803
28627
  "env.go": ECHO_ENV_GO,
27804
28628
  "go.mod": ECHO_GO_MOD,
27805
28629
  "bf-runtime/bf.go": bfGoSource,
28630
+ "bf-runtime/eval.go": evalGoSource,
27806
28631
  "bf-runtime/streaming.go": streamingGoSource,
27807
28632
  "bf-runtime/go.mod": ECHO_BF_RUNTIME_GO_MOD,
27808
28633
  "barefoot.config.ts": ECHO_BAREFOOT_CONFIG_TS,
@@ -27816,11 +28641,14 @@ replace github.com/barefootjs/runtime/bf => ./bf-runtime
27816
28641
  "public/styles.css": STYLES_CSS,
27817
28642
  "public/tokens.css": TOKENS_CSS,
27818
28643
  "public/uno.css": UNO_CSS_PLACEHOLDER,
28644
+ "public/favicon.svg": FAVICON_SVG,
27819
28645
  ".gitignore": ECHO_GITIGNORE
27820
28646
  },
27821
28647
  scripts: {
27822
28648
  dev: 'go mod tidy && bf build && unocss && concurrently -k -n build,uno,server -c blue,magenta,green "bf build --watch" "unocss --watch" "go run ."',
27823
- build: "go mod tidy && bf build && unocss",
28649
+ // `--minify` (not on `dev`): matches the site's "~14 kB min+gzip"
28650
+ // runtime-size claim, which is measured on a minified build.
28651
+ build: "go mod tidy && bf build --minify && unocss",
27824
28652
  start: "go run ."
27825
28653
  },
27826
28654
  dependencies: {
@@ -27969,7 +28797,6 @@ var HONO_SERVER_TSX, HONO_RENDERER_TSX, HONO_BAREFOOT_CONFIG_TS, HONO_TSCONFIG,
27969
28797
  var init_hono = __esm({
27970
28798
  "src/lib/adapters/hono.ts"() {
27971
28799
  "use strict";
27972
- init_pm();
27973
28800
  init_shared2();
27974
28801
  HONO_SERVER_TSX = `import { Hono } from 'hono'
27975
28802
  import { renderer } from './renderer'
@@ -28009,6 +28836,7 @@ export const renderer = jsxRenderer(({ children, title }) => (
28009
28836
  <meta charset="UTF-8" />
28010
28837
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
28011
28838
  <title>{title ?? 'BarefootJS app'}</title>
28839
+ ${faviconLinkTag("/favicon.svg")}
28012
28840
  ${CSS_LINKS_BEGIN}
28013
28841
  {/* Link all three sheets so the browser fetches them in
28014
28842
  parallel \u2014 chaining via styles.css @import would defer
@@ -28125,15 +28953,22 @@ export default createConfig({
28125
28953
  "public/styles.css": STYLES_CSS,
28126
28954
  "public/tokens.css": TOKENS_CSS,
28127
28955
  "public/uno.css": UNO_CSS_PLACEHOLDER,
28956
+ "public/favicon.svg": FAVICON_SVG,
28128
28957
  "public/components/manifest.json": COMPONENTS_MANIFEST_SEED,
28129
28958
  ".gitignore": HONO_GITIGNORE
28130
28959
  },
28131
28960
  scripts: {
28132
- dev: (pm) => `concurrently -k -n build,uno,server -c blue,magenta,green "bf build --watch" "unocss --watch" "${commandsFor(pm).exec(
28133
- "wrangler dev --live-reload"
28134
- )}"`,
28135
- build: "bf build && unocss",
28136
- deploy: (pm) => `bf build && unocss && ${commandsFor(pm).exec("wrangler deploy")}`
28961
+ // `wrangler` is a devDependency below, so package.json scripts
28962
+ // resolve it straight from `node_modules/.bin` — no `npx`/`bunx`/
28963
+ // `pnpm dlx` wrapper needed (and no unpinned download on first
28964
+ // `<pm> run dev`, since the version is pinned in devDependencies).
28965
+ dev: 'concurrently -k -n build,uno,server -c blue,magenta,green "bf build --watch" "unocss --watch" "wrangler dev --live-reload"',
28966
+ // `--minify` here (not on `dev`/`watch`): the site's landing-page
28967
+ // runtime-size claim ("~14 kB min+gzip", site/core/build.ts) is
28968
+ // measured on a minified build, and the dev pipeline intentionally
28969
+ // serves the unminified runtime for readable stack traces.
28970
+ build: "bf build --minify && unocss",
28971
+ deploy: "bf build --minify && unocss && wrangler deploy"
28137
28972
  },
28138
28973
  deploy: {
28139
28974
  target: "Cloudflare Workers",
@@ -28162,7 +28997,12 @@ export default createConfig({
28162
28997
  // doesn't ship vitest, and an npm project doesn't ship a bun-only
28163
28998
  // type package.
28164
28999
  concurrently: "^9.0.0",
28165
- typescript: "^5.6.0"
29000
+ typescript: "^5.6.0",
29001
+ // Pinned so `<pm> run dev` / `<pm> run deploy` resolve a known
29002
+ // `wrangler` from `node_modules/.bin` instead of pausing on an
29003
+ // unpinned download the first time they run (see the `scripts`
29004
+ // comment above — this is what makes the bare invocation safe).
29005
+ wrangler: "^4.0.0"
28166
29006
  },
28167
29007
  prereqWarnings: () => []
28168
29008
  };
@@ -28304,6 +29144,7 @@ export function createRenderer({ componentsBase }: CreateRendererOptions) {
28304
29144
  <meta charset="UTF-8" />
28305
29145
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
28306
29146
  <title>{title ?? 'BarefootJS app'}</title>
29147
+ ${faviconLinkTag("/static/favicon.svg")}
28307
29148
  ${CSS_LINKS_BEGIN}
28308
29149
  {/* Link all three sheets so the browser fetches them in
28309
29150
  parallel \u2014 chaining via styles.css @import would defer
@@ -28401,12 +29242,15 @@ export default createConfig({
28401
29242
  "public/styles.css": STYLES_CSS,
28402
29243
  "public/tokens.css": TOKENS_CSS,
28403
29244
  "public/uno.css": UNO_CSS_PLACEHOLDER,
29245
+ "public/favicon.svg": FAVICON_SVG,
28404
29246
  "dist/components/manifest.json": COMPONENTS_MANIFEST_SEED,
28405
29247
  ".gitignore": HONO_NODE_GITIGNORE
28406
29248
  },
28407
29249
  scripts: {
28408
29250
  dev: 'bf build && unocss && concurrently -k -n build,uno,server -c blue,magenta,green "bf build --watch" "unocss --watch" "tsx watch server.tsx"',
28409
- build: "bf build && unocss",
29251
+ // `--minify` (not on `dev`): matches the site's "~14 kB min+gzip"
29252
+ // runtime-size claim, which is measured on a minified build.
29253
+ build: "bf build --minify && unocss",
28410
29254
  start: "tsx server.tsx"
28411
29255
  },
28412
29256
  dependencies: {
@@ -28512,8 +29356,12 @@ get '/static/*asset' => sub ($c) {
28512
29356
  $c->reply->static($c->stash('asset') // '') or $c->reply->not_found;
28513
29357
  };
28514
29358
 
29359
+ # Component props are ordinary stash values. The plugin seeds every
29360
+ # template variable's static default from the build manifest, so
29361
+ # passing \`initial\` here is optional \u2014 but it's how you hand real
29362
+ # data to a component, so the starter route shows the shape.
28515
29363
  get '/' => sub ($c) {
28516
- $c->render(template => 'Counter', layout => 'default');
29364
+ $c->render(template => 'Counter', layout => 'default', initial => 0);
28517
29365
  };
28518
29366
 
28519
29367
  app->start;
@@ -28527,6 +29375,7 @@ __DATA__
28527
29375
  <meta charset="utf-8">
28528
29376
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
28529
29377
  <title>BarefootJS app</title>
29378
+ ${faviconLinkTag("/static/favicon.svg")}
28530
29379
  ${CSS_LINKS_BEGIN}
28531
29380
  <!-- Link all three sheets so the browser fetches them in parallel \u2014
28532
29381
  chaining via styles.css @import would defer tokens/uno to a
@@ -28606,6 +29455,7 @@ requires 'Mojolicious', '9.0';
28606
29455
  "public/styles.css": STYLES_CSS,
28607
29456
  "public/tokens.css": TOKENS_CSS,
28608
29457
  "public/uno.css": UNO_CSS_PLACEHOLDER,
29458
+ "public/favicon.svg": FAVICON_SVG,
28609
29459
  "dist/components/manifest.json": COMPONENTS_MANIFEST_SEED,
28610
29460
  ".gitignore": MOJO_GITIGNORE
28611
29461
  },
@@ -28620,7 +29470,9 @@ requires 'Mojolicious', '9.0';
28620
29470
  // a concern now that the adapter lowers that expression
28621
29471
  // natively.)
28622
29472
  dev: 'concurrently -k -n build,uno,server -c blue,magenta,green "bf build --watch" "unocss --watch" "morbo app.pl -l http://*:3002"',
28623
- build: "bf build && unocss",
29473
+ // `--minify` (not on `dev`): matches the site's "~14 kB min+gzip"
29474
+ // runtime-size claim, which is measured on a minified build.
29475
+ build: "bf build --minify && unocss",
28624
29476
  start: "perl app.pl daemon -l http://*:3002"
28625
29477
  },
28626
29478
  dependencies: {
@@ -28922,6 +29774,7 @@ sub layout (%a) {
28922
29774
  <meta charset="utf-8">
28923
29775
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
28924
29776
  <title>BarefootJS app</title>
29777
+ ${faviconLinkTag("/static/favicon.svg")}
28925
29778
  ${CSS_LINKS_BEGIN}
28926
29779
  <!-- Link all three sheets so the browser fetches them in parallel.
28927
29780
  tokens first so its CSS variables exist before any rule uses them. -->
@@ -29109,6 +29962,7 @@ describe('Counter', () => {
29109
29962
  "public/styles.css": STYLES_CSS,
29110
29963
  "public/tokens.css": TOKENS_CSS,
29111
29964
  "public/uno.css": UNO_CSS_PLACEHOLDER,
29965
+ "public/favicon.svg": FAVICON_SVG,
29112
29966
  "dist/components/manifest.json": COMPONENTS_MANIFEST_SEED,
29113
29967
  ".gitignore": XSLATE_GITIGNORE
29114
29968
  },
@@ -29118,7 +29972,9 @@ describe('Counter', () => {
29118
29972
  // Starman (not plackup's default single-process server) so the
29119
29973
  // dev-reload SSE endpoint can stream while requests are served.
29120
29974
  dev: `concurrently -k -n build,uno,server -c blue,magenta,green "bf build --watch" "unocss --watch" "plackup -s Starman --workers 5 -p ${XSLATE_PORT} app.psgi"`,
29121
- build: "bf build && unocss",
29975
+ // `--minify` (not on `dev`): matches the site's "~14 kB min+gzip"
29976
+ // runtime-size claim, which is measured on a minified build.
29977
+ build: "bf build --minify && unocss",
29122
29978
  start: `PLACK_ENV=production plackup -s Starman --workers 5 -p ${XSLATE_PORT} app.psgi`
29123
29979
  },
29124
29980
  dependencies: {
@@ -29189,6 +30045,9 @@ var init_templates = __esm({
29189
30045
 
29190
30046
  // src/lib/select.ts
29191
30047
  import readline from "node:readline";
30048
+ function confirmationLabel(opt) {
30049
+ return opt.shortLabel ?? opt.label.replace(/\s*\(.*\)$/, "");
30050
+ }
29192
30051
  async function select(args2) {
29193
30052
  const input = args2.input ?? process.stdin;
29194
30053
  const output = args2.output ?? process.stdout;
@@ -29251,7 +30110,7 @@ async function select(args2) {
29251
30110
  if (key.name === "return") {
29252
30111
  cleanup();
29253
30112
  const picked = args2.options[cursor];
29254
- const shortLabel = picked.shortLabel ?? picked.label.replace(/\s*\(.*\)$/, "");
30113
+ const shortLabel = confirmationLabel(picked);
29255
30114
  const totalLines = args2.options.length + 1;
29256
30115
  output.write(`\x1B[${totalLines}A`);
29257
30116
  for (let i = 0; i < totalLines; i++) {
@@ -29372,13 +30231,91 @@ var init_css = __esm({
29372
30231
  }
29373
30232
  });
29374
30233
 
30234
+ // src/lib/readme.ts
30235
+ function generateReadmeMd(pkgName, adapter, pm) {
30236
+ const cmd = commandsFor(pm);
30237
+ const lines = [];
30238
+ lines.push(`# ${pkgName}`, "");
30239
+ lines.push(
30240
+ `A [BarefootJS](https://barefootjs.dev) app scaffolded with the **${adapter.label}** adapter.`,
30241
+ ""
30242
+ );
30243
+ lines.push("## Getting started", "");
30244
+ lines.push("```sh");
30245
+ lines.push(cmd.install);
30246
+ if (adapter.extraSetupSteps) {
30247
+ for (const step of adapter.extraSetupSteps) {
30248
+ if (step.label) lines.push(`# ${step.label}`);
30249
+ lines.push(step.command);
30250
+ }
30251
+ }
30252
+ lines.push(cmd.run("dev"));
30253
+ lines.push("```", "");
30254
+ if (adapter.scripts.build || adapter.deploy) {
30255
+ lines.push("## Build & deploy", "");
30256
+ lines.push("```sh");
30257
+ if (adapter.scripts.build) lines.push(cmd.run("build"));
30258
+ if (adapter.deploy) {
30259
+ lines.push(`${cmd.run(adapter.deploy.script)} # deploy to ${adapter.deploy.target}`);
30260
+ }
30261
+ lines.push("```", "");
30262
+ }
30263
+ lines.push("## `bf` CLI cheat sheet", "");
30264
+ lines.push(
30265
+ "The `bf` CLI is the first reference for component APIs and framework docs \u2014 run `bf --help` for the full command list.",
30266
+ ""
30267
+ );
30268
+ lines.push("| Command | What it does |");
30269
+ lines.push("| --- | --- |");
30270
+ lines.push("| `bf search <term>` | Search the component registry |");
30271
+ lines.push("| `bf add <component>` | Add a component from the registry |");
30272
+ lines.push("| `bf docs <component>` | Show a component's API surface |");
30273
+ lines.push(
30274
+ '| `bf debug graph <component>` | Inspect a `"use client"` component\'s reactive signal graph |'
30275
+ );
30276
+ lines.push("| `bf guide` | Open the framework guide |");
30277
+ lines.push("");
30278
+ lines.push("## Generated output", "");
30279
+ lines.push(
30280
+ "The compiled output directory (produced by `bf build`) is regenerated on every build \u2014 don't edit it by hand.",
30281
+ ""
30282
+ );
30283
+ return lines.join("\n");
30284
+ }
30285
+ var init_readme = __esm({
30286
+ "src/lib/readme.ts"() {
30287
+ "use strict";
30288
+ init_pm();
30289
+ }
30290
+ });
30291
+
29375
30292
  // src/commands/init.ts
29376
30293
  var init_exports = {};
29377
30294
  __export(init_exports, {
29378
30295
  run: () => run4
29379
30296
  });
29380
- import { existsSync as existsSync8, mkdirSync as mkdirSync3, writeFileSync as writeFileSync3 } from "node:fs";
30297
+ import { existsSync as existsSync8, mkdirSync as mkdirSync3, readFileSync as readFileSync5, writeFileSync as writeFileSync3 } from "node:fs";
29381
30298
  import path10 from "node:path";
30299
+ import { fileURLToPath as fileURLToPath3 } from "node:url";
30300
+ function readCliVersion() {
30301
+ const bundledPkgJsonPath = path10.resolve(path10.dirname(thisFile), "../package.json");
30302
+ const devPkgJsonPath = path10.resolve(path10.dirname(thisFile), "../../package.json");
30303
+ const pkgJsonPath = existsSync8(bundledPkgJsonPath) ? bundledPkgJsonPath : devPkgJsonPath;
30304
+ const { version } = JSON.parse(readFileSync5(pkgJsonPath, "utf-8"));
30305
+ if (typeof version !== "string" || version.length === 0) {
30306
+ throw new Error(
30307
+ `Could not read the CLI's own version from ${pkgJsonPath} \u2014 cannot pin @barefootjs/* scaffold dependencies.`
30308
+ );
30309
+ }
30310
+ return version;
30311
+ }
30312
+ function pinBarefootDeps(deps) {
30313
+ const pinned = {};
30314
+ for (const [name2, version] of Object.entries(deps)) {
30315
+ pinned[name2] = name2.startsWith("@barefootjs/") && version === "latest" ? `^${CLI_VERSION}` : version;
30316
+ }
30317
+ return pinned;
30318
+ }
29382
30319
  function parseFlags(args2) {
29383
30320
  const flags = {};
29384
30321
  for (let i = 0; i < args2.length; i++) {
@@ -29403,6 +30340,10 @@ async function run4(args2, ctx2) {
29403
30340
  console.error(" # or: pnpm create barefootjs");
29404
30341
  process.exit(1);
29405
30342
  }
30343
+ if (args2.includes("--list-adapters")) {
30344
+ printAdapterList();
30345
+ process.exit(0);
30346
+ }
29406
30347
  const projectDir = process.cwd();
29407
30348
  const flags = parseFlags(args2);
29408
30349
  const tsConfigPath = path10.join(projectDir, "barefoot.config.ts");
@@ -29457,10 +30398,16 @@ async function run4(args2, ctx2) {
29457
30398
  async function resolveAdapter(flag) {
29458
30399
  if (flag) {
29459
30400
  if (!ADAPTERS[flag]) {
29460
- const known = Object.keys(ADAPTERS).join(", ");
29461
- console.error(`Error: unknown adapter "${flag}". Available: ${known}`);
30401
+ const hint = LANGUAGE_ADAPTER_HINTS[flag.toLowerCase()];
30402
+ if (hint) {
30403
+ console.error(`Error: unknown adapter "${flag}". ${hint}`);
30404
+ } else {
30405
+ const known = Object.keys(ADAPTERS).join(", ");
30406
+ console.error(`Error: unknown adapter "${flag}". Available: ${known}`);
30407
+ }
29462
30408
  process.exit(1);
29463
30409
  }
30410
+ printSelectConfirmation(ADAPTER_SELECT_MESSAGE, ADAPTERS[flag]);
29464
30411
  return flag;
29465
30412
  }
29466
30413
  const options2 = Object.entries(ADAPTERS).map(([value2, t]) => ({
@@ -29469,7 +30416,7 @@ async function resolveAdapter(flag) {
29469
30416
  shortLabel: t.shortLabel
29470
30417
  }));
29471
30418
  try {
29472
- return await select({ message: "Choose a framework or runtime", options: options2, defaultValue: DEFAULT_ADAPTER });
30419
+ return await select({ message: ADAPTER_SELECT_MESSAGE, options: options2, defaultValue: DEFAULT_ADAPTER });
29473
30420
  } catch (err) {
29474
30421
  bailOnSelectError(err);
29475
30422
  }
@@ -29481,15 +30428,36 @@ async function resolveCssLibrary(flag) {
29481
30428
  console.error(`Error: unknown CSS library "${flag}". Available: ${known}`);
29482
30429
  process.exit(1);
29483
30430
  }
30431
+ printSelectConfirmation(CSS_SELECT_MESSAGE, CSS_LIBRARIES[flag]);
29484
30432
  return flag;
29485
30433
  }
29486
30434
  const options2 = Object.entries(CSS_LIBRARIES).map(([value2, t]) => ({ value: value2, label: t.label }));
29487
30435
  try {
29488
- return await select({ message: "Choose a CSS library", options: options2, defaultValue: DEFAULT_CSS_LIBRARY });
30436
+ return await select({ message: CSS_SELECT_MESSAGE, options: options2, defaultValue: DEFAULT_CSS_LIBRARY });
29489
30437
  } catch (err) {
29490
30438
  bailOnSelectError(err);
29491
30439
  }
29492
30440
  }
30441
+ function printSelectConfirmation(message, opt) {
30442
+ const label2 = confirmationLabel(opt);
30443
+ const highlighted = process.stdout.isTTY ? `\x1B[1;32m${label2}\x1B[0m` : label2;
30444
+ console.log(`\u2714 ${message} ${highlighted}`);
30445
+ }
30446
+ function printAdapterList() {
30447
+ const adapterIds = Object.keys(ADAPTERS);
30448
+ const idWidth = Math.max(...adapterIds.map((id2) => id2.length));
30449
+ console.log("Adapters (--adapter <id>):");
30450
+ for (const id2 of adapterIds) {
30451
+ console.log(` ${id2.padEnd(idWidth)} ${ADAPTERS[id2].label}`);
30452
+ }
30453
+ console.log("");
30454
+ const cssIds = Object.keys(CSS_LIBRARIES);
30455
+ const cssWidth = Math.max(...cssIds.map((id2) => id2.length));
30456
+ console.log("CSS libraries (--css <id>):");
30457
+ for (const id2 of cssIds) {
30458
+ console.log(` ${id2.padEnd(cssWidth)} ${CSS_LIBRARIES[id2].label}`);
30459
+ }
30460
+ }
29493
30461
  function bailOnSelectError(err) {
29494
30462
  if (err instanceof SelectCancelled) {
29495
30463
  console.error("Cancelled \u2014 nothing scaffolded.");
@@ -29545,6 +30513,11 @@ async function scaffoldApp(projectDir, adapter, flags, usesUno, _ctx) {
29545
30513
  JSON.stringify({ version: 1, generatedAt: (/* @__PURE__ */ new Date()).toISOString(), components: [] }, null, 2) + "\n"
29546
30514
  );
29547
30515
  created++;
30516
+ const readmePath = path10.join(projectDir, "README.md");
30517
+ if (!existsSync8(readmePath)) {
30518
+ writeFileSync3(readmePath, generateReadmeMd(pkgName, adapter, pm));
30519
+ created++;
30520
+ }
29548
30521
  const pkgJsonPath = path10.join(projectDir, "package.json");
29549
30522
  const resolvedAdapterScripts = {};
29550
30523
  for (const [k, v] of Object.entries(adapter.scripts)) {
@@ -29574,8 +30547,10 @@ async function scaffoldApp(projectDir, adapter, flags, usesUno, _ctx) {
29574
30547
  // without manual migration. See `testRunnerFor` in `../lib/pm.ts`.
29575
30548
  test: runner.scriptValue
29576
30549
  },
29577
- dependencies: { ...adapter.dependencies },
29578
- devDependencies: { ...adapterDevDeps, ...pmDevDeps }
30550
+ // `@barefootjs/*` entries are pinned from the `'latest'` sentinel to
30551
+ // `^<CLI_VERSION>` here see `pinBarefootDeps` above.
30552
+ dependencies: pinBarefootDeps({ ...adapter.dependencies }),
30553
+ devDependencies: pinBarefootDeps({ ...adapterDevDeps, ...pmDevDeps })
29579
30554
  };
29580
30555
  if (!existsSync8(pkgJsonPath)) {
29581
30556
  writeFileSync3(pkgJsonPath, JSON.stringify(pkgJson, null, 2) + "\n");
@@ -29623,7 +30598,7 @@ function heading(s) {
29623
30598
  function dim(s) {
29624
30599
  return process.stdout.isTTY ? `\x1B[2m${s}\x1B[0m` : s;
29625
30600
  }
29626
- var INIT_GATE_ENV, DEFAULT_REGISTRY_URL2;
30601
+ var thisFile, CLI_VERSION, INIT_GATE_ENV, ADAPTER_SELECT_MESSAGE, CSS_SELECT_MESSAGE, LANGUAGE_ADAPTER_HINTS, DEFAULT_REGISTRY_URL2;
29627
30602
  var init_init = __esm({
29628
30603
  "src/commands/init.ts"() {
29629
30604
  "use strict";
@@ -29634,7 +30609,17 @@ var init_init = __esm({
29634
30609
  init_spinner();
29635
30610
  init_css();
29636
30611
  init_shared2();
30612
+ init_readme();
30613
+ thisFile = fileURLToPath3(import.meta.url);
30614
+ CLI_VERSION = readCliVersion();
29637
30615
  INIT_GATE_ENV = "BAREFOOT_INIT_VIA_CREATE";
30616
+ ADAPTER_SELECT_MESSAGE = "Choose a framework or runtime";
30617
+ CSS_SELECT_MESSAGE = "Choose a CSS library";
30618
+ LANGUAGE_ADAPTER_HINTS = {
30619
+ go: "Go apps use one of the Go web-framework adapters: echo, gin, chi, nethttp (e.g. --adapter chi)",
30620
+ golang: "Go apps use one of the Go web-framework adapters: echo, gin, chi, nethttp (e.g. --adapter chi)",
30621
+ perl: "Perl apps use one of the Perl adapters: mojo, xslate (e.g. --adapter mojo)"
30622
+ };
29638
30623
  DEFAULT_REGISTRY_URL2 = "https://ui.barefootjs.dev/r/";
29639
30624
  }
29640
30625
  });
@@ -29732,7 +30717,7 @@ var init_resolve_source = __esm({
29732
30717
  });
29733
30718
 
29734
30719
  // src/lib/meta-loader.ts
29735
- import { readFileSync as readFileSync5, existsSync as existsSync10, readdirSync as readdirSync4 } from "node:fs";
30720
+ import { readFileSync as readFileSync6, existsSync as existsSync10, readdirSync as readdirSync4 } from "node:fs";
29736
30721
  import path12 from "node:path";
29737
30722
  function loadIndex(metaDir) {
29738
30723
  const indexPath = path12.join(metaDir, "index.json");
@@ -29740,7 +30725,7 @@ function loadIndex(metaDir) {
29740
30725
  console.error(`Error: ${indexPath} not found.`);
29741
30726
  process.exit(1);
29742
30727
  }
29743
- return JSON.parse(readFileSync5(indexPath, "utf-8"));
30728
+ return JSON.parse(readFileSync6(indexPath, "utf-8"));
29744
30729
  }
29745
30730
  function registryIndexUrl(registryUrl) {
29746
30731
  return registryUrl.endsWith("/") ? `${registryUrl}index.json` : `${registryUrl}/index.json`;
@@ -29775,7 +30760,7 @@ async function tryFetchIndex(registryUrl) {
29775
30760
  }
29776
30761
  function tryLoadComponent(metaDir, name2) {
29777
30762
  const filePath = path12.join(metaDir, `${name2}.json`);
29778
- if (existsSync10(filePath)) return JSON.parse(readFileSync5(filePath, "utf-8"));
30763
+ if (existsSync10(filePath)) return JSON.parse(readFileSync6(filePath, "utf-8"));
29779
30764
  if (!existsSync10(metaDir)) return null;
29780
30765
  const wanted = `${name2.toLowerCase()}.json`;
29781
30766
  let entries2;
@@ -29789,7 +30774,7 @@ function tryLoadComponent(metaDir, name2) {
29789
30774
  if (entry.toLowerCase() === wanted) matches.push(entry);
29790
30775
  }
29791
30776
  if (matches.length !== 1) return null;
29792
- return JSON.parse(readFileSync5(path12.join(metaDir, matches[0]), "utf-8"));
30777
+ return JSON.parse(readFileSync6(path12.join(metaDir, matches[0]), "utf-8"));
29793
30778
  }
29794
30779
  function formatMissingComponentError(metaDir, name2, ctx2) {
29795
30780
  const filePath = path12.join(metaDir, `${name2}.json`);
@@ -30123,11 +31108,11 @@ __export(guide_exports, {
30123
31108
  });
30124
31109
  import path15 from "node:path";
30125
31110
  import { existsSync as existsSync11 } from "node:fs";
30126
- import { fileURLToPath as fileURLToPath3 } from "node:url";
31111
+ import { fileURLToPath as fileURLToPath4 } from "node:url";
30127
31112
  function findDocsDir(ctx2) {
30128
31113
  const monorepoDocs = path15.join(ctx2.root, "docs/core");
30129
31114
  if (existsSync11(monorepoDocs)) return monorepoDocs;
30130
- const bundledDocs = path15.resolve(path15.dirname(thisFile), "docs/core");
31115
+ const bundledDocs = path15.resolve(path15.dirname(thisFile2), "docs/core");
30131
31116
  if (existsSync11(bundledDocs)) return bundledDocs;
30132
31117
  return null;
30133
31118
  }
@@ -30169,7 +31154,7 @@ function run7(args2, ctx2) {
30169
31154
  const docsDir = findDocsDir(ctx2);
30170
31155
  if (!docsDir) {
30171
31156
  const monorepoDocs = path15.join(ctx2.root, "docs/core");
30172
- const bundledDocs = path15.resolve(path15.dirname(thisFile), "docs/core");
31157
+ const bundledDocs = path15.resolve(path15.dirname(thisFile2), "docs/core");
30173
31158
  console.error("Error: Core documentation not found.");
30174
31159
  console.error("Looked in:");
30175
31160
  console.error(` - ${monorepoDocs} (monorepo)`);
@@ -30201,12 +31186,12 @@ function run7(args2, ctx2) {
30201
31186
  }
30202
31187
  printDoc(doc, ctx2.jsonFlag);
30203
31188
  }
30204
- var thisFile;
31189
+ var thisFile2;
30205
31190
  var init_guide = __esm({
30206
31191
  "src/commands/guide.ts"() {
30207
31192
  "use strict";
30208
31193
  init_docs_loader();
30209
- thisFile = fileURLToPath3(import.meta.url);
31194
+ thisFile2 = fileURLToPath4(import.meta.url);
30210
31195
  }
30211
31196
  });
30212
31197
 
@@ -30487,8 +31472,8 @@ var init_compile = __esm({
30487
31472
  // src/lib/tokens.ts
30488
31473
  import { readFile as readFile3 } from "node:fs/promises";
30489
31474
  import { existsSync as existsSync13 } from "node:fs";
30490
- import { resolve as resolve8, dirname as dirname4 } from "node:path";
30491
- import { fileURLToPath as fileURLToPath4 } from "node:url";
31475
+ import { resolve as resolve8, dirname as dirname5 } from "node:path";
31476
+ import { fileURLToPath as fileURLToPath5 } from "node:url";
30492
31477
  async function loadTokens(jsonPath) {
30493
31478
  const content2 = await readFile3(jsonPath, "utf-8");
30494
31479
  return JSON.parse(content2);
@@ -30564,7 +31549,7 @@ ${darkLines.join("\n")}
30564
31549
  function findBaseTokensJson(ctx2) {
30565
31550
  const monorepoTokens = resolve8(ctx2.root, "site/shared/tokens/tokens.json");
30566
31551
  if (existsSync13(monorepoTokens)) return monorepoTokens;
30567
- const bundledTokens = resolve8(dirname4(thisFile2), "tokens.json");
31552
+ const bundledTokens = resolve8(dirname5(thisFile3), "tokens.json");
30568
31553
  if (existsSync13(bundledTokens)) return bundledTokens;
30569
31554
  return null;
30570
31555
  }
@@ -30589,12 +31574,12 @@ async function loadTokenSet(ctx2) {
30589
31574
  async function loadTokensCss(ctx2) {
30590
31575
  return generateCSS(await loadTokenSet(ctx2));
30591
31576
  }
30592
- var thisFile2;
31577
+ var thisFile3;
30593
31578
  var init_tokens = __esm({
30594
31579
  "src/lib/tokens.ts"() {
30595
31580
  "use strict";
30596
31581
  init_runtime();
30597
- thisFile2 = fileURLToPath4(import.meta.url);
31582
+ thisFile3 = fileURLToPath5(import.meta.url);
30598
31583
  }
30599
31584
  });
30600
31585
 
@@ -30611,8 +31596,8 @@ var init_errors2 = __esm({
30611
31596
  // src/lib/preview/assets.ts
30612
31597
  import { existsSync as existsSync14 } from "node:fs";
30613
31598
  import { readFile as readFile4 } from "node:fs/promises";
30614
- import { resolve as resolve9, dirname as dirname5 } from "node:path";
30615
- import { fileURLToPath as fileURLToPath5 } from "node:url";
31599
+ import { resolve as resolve9, dirname as dirname6 } from "node:path";
31600
+ import { fileURLToPath as fileURLToPath6 } from "node:url";
30616
31601
  function firstExisting(...candidates) {
30617
31602
  return candidates.find((c) => !!c && existsSync14(c));
30618
31603
  }
@@ -30682,7 +31667,7 @@ var init_assets = __esm({
30682
31667
  "use strict";
30683
31668
  init_tokens();
30684
31669
  init_errors2();
30685
- assetDir = dirname5(fileURLToPath5(import.meta.url));
31670
+ assetDir = dirname6(fileURLToPath6(import.meta.url));
30686
31671
  }
30687
31672
  });
30688
31673
 
@@ -30960,7 +31945,7 @@ var init_preview_generate = __esm({
30960
31945
 
30961
31946
  // src/lib/preview/run.ts
30962
31947
  import { resolve as resolve10, relative as relative4 } from "node:path";
30963
- import { readFileSync as readFileSync6, writeFileSync as writeFileSync4, existsSync as existsSync15 } from "node:fs";
31948
+ import { readFileSync as readFileSync7, writeFileSync as writeFileSync4, existsSync as existsSync15 } from "node:fs";
30964
31949
  async function runPreview(componentName, ctx2, opts = {}) {
30965
31950
  const assets = await resolvePreviewAssets(ctx2);
30966
31951
  const previewsPath = resolve10(assets.srcComponentsDir, componentName, "index.preview.tsx");
@@ -30977,7 +31962,7 @@ Run: bf gen preview ${componentName}`
30977
31962
  );
30978
31963
  }
30979
31964
  }
30980
- const source = readFileSync6(previewsPath, "utf-8");
31965
+ const source = readFileSync7(previewsPath, "utf-8");
30981
31966
  const previewNames = [
30982
31967
  ...source.matchAll(/export\s+(?:async\s+)?function\s+(\w+)/g),
30983
31968
  ...source.matchAll(/export\s+const\s+(\w+)\s*=/g)
@@ -31219,7 +32204,7 @@ __export(tokens_apply_exports, {
31219
32204
  resolveTokensCss: () => resolveTokensCss,
31220
32205
  run: () => run9
31221
32206
  });
31222
- import { existsSync as existsSync18, readFileSync as readFileSync7, writeFileSync as writeFileSync5 } from "node:fs";
32207
+ import { existsSync as existsSync18, readFileSync as readFileSync8, writeFileSync as writeFileSync5 } from "node:fs";
31223
32208
  import path17 from "node:path";
31224
32209
  async function run9(args2, ctx2) {
31225
32210
  const url2 = args2[0];
@@ -31302,7 +32287,7 @@ function buildBlockOverrides(config) {
31302
32287
  }
31303
32288
  function applyCssOverrides(cssPath, config) {
31304
32289
  const overrides = buildBlockOverrides(config);
31305
- let css = readFileSync7(cssPath, "utf-8");
32290
+ let css = readFileSync8(cssPath, "utf-8");
31306
32291
  css = patchBlock(css, /:root\s*\{/, overrides.root);
31307
32292
  css = patchBlock(css, /\.dark\s*\{/, overrides.dark);
31308
32293
  writeFileSync5(cssPath, css);
@@ -31351,7 +32336,7 @@ function escapeRegex2(s) {
31351
32336
  return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
31352
32337
  }
31353
32338
  function applyTokenOverrides(tokensJsonPath, config) {
31354
- const raw = readFileSync7(tokensJsonPath, "utf-8");
32339
+ const raw = readFileSync8(tokensJsonPath, "utf-8");
31355
32340
  const tokensData = JSON.parse(raw);
31356
32341
  if (config.tokens) {
31357
32342
  for (const [name2, values2] of Object.entries(config.tokens)) {
@@ -31535,12 +32520,12 @@ var init_tokens2 = __esm({
31535
32520
  });
31536
32521
 
31537
32522
  // src/lib/scaffold.ts
31538
- import { readFileSync as readFileSync8, existsSync as existsSync19 } from "node:fs";
32523
+ import { readFileSync as readFileSync9, existsSync as existsSync19 } from "node:fs";
31539
32524
  import path18 from "node:path";
31540
32525
  function loadMeta(metaDir, name2) {
31541
32526
  const filePath = path18.join(metaDir, `${name2}.json`);
31542
32527
  if (!existsSync19(filePath)) return null;
31543
- return JSON.parse(readFileSync8(filePath, "utf-8"));
32528
+ return JSON.parse(readFileSync9(filePath, "utf-8"));
31544
32529
  }
31545
32530
  function toPascalCase2(kebab) {
31546
32531
  return kebab.split("-").map((w2) => w2[0].toUpperCase() + w2.slice(1)).join("");
@@ -31956,11 +32941,11 @@ var init_parse_component = __esm({
31956
32941
  });
31957
32942
 
31958
32943
  // src/lib/test-template.ts
31959
- import { readFileSync as readFileSync9 } from "node:fs";
32944
+ import { readFileSync as readFileSync10 } from "node:fs";
31960
32945
  import path20 from "node:path";
31961
32946
  function generateTestTemplate(componentPath, options2 = {}) {
31962
32947
  const importSource = options2.importSource ?? "bun:test";
31963
- const source = readFileSync9(componentPath, "utf-8");
32948
+ const source = readFileSync10(componentPath, "utf-8");
31964
32949
  const parsed = parseComponent(source);
31965
32950
  const fileName = path20.basename(componentPath);
31966
32951
  const baseName = fileName.replace(/\.tsx$/, "");
@@ -32300,7 +33285,7 @@ var debug_graph_exports = {};
32300
33285
  __export(debug_graph_exports, {
32301
33286
  run: () => run14
32302
33287
  });
32303
- import { readFileSync as readFileSync10 } from "node:fs";
33288
+ import { readFileSync as readFileSync11 } from "node:fs";
32304
33289
  import "node:path";
32305
33290
  async function run14(args2, ctx2) {
32306
33291
  const componentName = args2[0];
@@ -32318,7 +33303,7 @@ async function run14(args2, ctx2) {
32318
33303
  for (const p of searched) console.error(` - ${p}`);
32319
33304
  process.exit(1);
32320
33305
  }
32321
- const source = readFileSync10(resolved.filePath, "utf-8");
33306
+ const source = readFileSync11(resolved.filePath, "utf-8");
32322
33307
  const graph = buildComponentGraph2(source, resolved.filePath, resolved.componentName);
32323
33308
  if (ctx2.jsonFlag) {
32324
33309
  console.log(JSON.stringify(graphToJSON2(graph), null, 2));
@@ -32338,7 +33323,7 @@ var debug_trace_exports = {};
32338
33323
  __export(debug_trace_exports, {
32339
33324
  run: () => run15
32340
33325
  });
32341
- import { readFileSync as readFileSync11 } from "node:fs";
33326
+ import { readFileSync as readFileSync12 } from "node:fs";
32342
33327
  async function run15(args2, ctx2) {
32343
33328
  const componentName = args2[0];
32344
33329
  const targetName = args2[1];
@@ -32356,7 +33341,7 @@ async function run15(args2, ctx2) {
32356
33341
  for (const p of searched) console.error(` - ${p}`);
32357
33342
  process.exit(1);
32358
33343
  }
32359
- const source = readFileSync11(resolved.filePath, "utf-8");
33344
+ const source = readFileSync12(resolved.filePath, "utf-8");
32360
33345
  const graph = buildComponentGraph2(source, resolved.filePath, resolved.componentName);
32361
33346
  const path25 = traceUpdatePath2(graph, targetName);
32362
33347
  if (!path25) {
@@ -32388,7 +33373,7 @@ var debug_fallbacks_exports = {};
32388
33373
  __export(debug_fallbacks_exports, {
32389
33374
  run: () => run16
32390
33375
  });
32391
- import { readFileSync as readFileSync12 } from "node:fs";
33376
+ import { readFileSync as readFileSync13 } from "node:fs";
32392
33377
  async function run16(args2, ctx2) {
32393
33378
  const componentName = args2[0];
32394
33379
  if (!componentName) {
@@ -32405,7 +33390,7 @@ async function run16(args2, ctx2) {
32405
33390
  for (const p of searched) console.error(` - ${p}`);
32406
33391
  process.exit(1);
32407
33392
  }
32408
- const source = readFileSync12(resolved.filePath, "utf-8");
33393
+ const source = readFileSync13(resolved.filePath, "utf-8");
32409
33394
  const graph = buildComponentGraph2(source, resolved.filePath, resolved.componentName);
32410
33395
  const isEventHandlerProp = (d) => d.type === "attribute" && /^on[A-Z]/.test(d.label.split(".").pop() ?? "");
32411
33396
  const fallbacks = graph.domBindings.filter((d) => d.classification === "fallback" && !isEventHandlerProp(d));
@@ -32447,7 +33432,7 @@ var debug_signals_exports = {};
32447
33432
  __export(debug_signals_exports, {
32448
33433
  run: () => run17
32449
33434
  });
32450
- import { readFileSync as readFileSync13 } from "node:fs";
33435
+ import { readFileSync as readFileSync14 } from "node:fs";
32451
33436
  async function run17(args2, ctx2) {
32452
33437
  const componentName = args2[0];
32453
33438
  if (!componentName) {
@@ -32464,7 +33449,7 @@ async function run17(args2, ctx2) {
32464
33449
  for (const p of searched) console.error(` - ${p}`);
32465
33450
  process.exit(1);
32466
33451
  }
32467
- const source = readFileSync13(resolved.filePath, "utf-8");
33452
+ const source = readFileSync14(resolved.filePath, "utf-8");
32468
33453
  const graph = buildComponentGraph2(source, resolved.filePath, resolved.componentName);
32469
33454
  const trace = generateStaticTrace2(graph);
32470
33455
  if (ctx2.jsonFlag) {
@@ -32487,7 +33472,7 @@ var debug_events_exports = {};
32487
33472
  __export(debug_events_exports, {
32488
33473
  run: () => run18
32489
33474
  });
32490
- import { readFileSync as readFileSync14 } from "node:fs";
33475
+ import { readFileSync as readFileSync15 } from "node:fs";
32491
33476
  async function run18(args2, ctx2) {
32492
33477
  const componentName = args2[0];
32493
33478
  if (!componentName) {
@@ -32504,7 +33489,7 @@ async function run18(args2, ctx2) {
32504
33489
  for (const p of searched) console.error(` - ${p}`);
32505
33490
  process.exit(1);
32506
33491
  }
32507
- const source = readFileSync14(resolved.filePath, "utf-8");
33492
+ const source = readFileSync15(resolved.filePath, "utf-8");
32508
33493
  const summary = buildEventSummary2(source, resolved.filePath, resolved.componentName);
32509
33494
  if (ctx2.jsonFlag) {
32510
33495
  console.log(JSON.stringify({ componentName: summary.componentName, sourceFile: summary.sourceFile, events: summary.events }, null, 2));
@@ -32524,7 +33509,7 @@ var debug_loops_exports = {};
32524
33509
  __export(debug_loops_exports, {
32525
33510
  run: () => run19
32526
33511
  });
32527
- import { readFileSync as readFileSync15 } from "node:fs";
33512
+ import { readFileSync as readFileSync16 } from "node:fs";
32528
33513
  async function run19(args2, ctx2) {
32529
33514
  const componentName = args2[0];
32530
33515
  if (!componentName) {
@@ -32541,7 +33526,7 @@ async function run19(args2, ctx2) {
32541
33526
  for (const p of searched) console.error(` - ${p}`);
32542
33527
  process.exit(1);
32543
33528
  }
32544
- const source = readFileSync15(resolved.filePath, "utf-8");
33529
+ const source = readFileSync16(resolved.filePath, "utf-8");
32545
33530
  const summary = buildLoopSummary2(source, resolved.filePath, resolved.componentName);
32546
33531
  if (ctx2.jsonFlag) {
32547
33532
  console.log(JSON.stringify(summary, null, 2));
@@ -32561,7 +33546,7 @@ var debug_why_update_exports = {};
32561
33546
  __export(debug_why_update_exports, {
32562
33547
  run: () => run20
32563
33548
  });
32564
- import { readFileSync as readFileSync16 } from "node:fs";
33549
+ import { readFileSync as readFileSync17 } from "node:fs";
32565
33550
  async function run20(args2, ctx2) {
32566
33551
  const componentName = args2[0];
32567
33552
  const bindingLabel = args2[1];
@@ -32581,7 +33566,7 @@ async function run20(args2, ctx2) {
32581
33566
  for (const p of searched) console.error(` - ${p}`);
32582
33567
  process.exit(1);
32583
33568
  }
32584
- const source = readFileSync16(resolved.filePath, "utf-8");
33569
+ const source = readFileSync17(resolved.filePath, "utf-8");
32585
33570
  const result2 = buildWhyUpdate2(source, resolved.filePath, bindingLabel, resolved.componentName);
32586
33571
  if (!result2) {
32587
33572
  const graph = buildComponentGraph2(source, resolved.filePath, resolved.componentName);
@@ -32619,7 +33604,7 @@ var debug_summary_exports = {};
32619
33604
  __export(debug_summary_exports, {
32620
33605
  run: () => run21
32621
33606
  });
32622
- import { readFileSync as readFileSync17 } from "node:fs";
33607
+ import { readFileSync as readFileSync18 } from "node:fs";
32623
33608
  async function run21(args2, ctx2) {
32624
33609
  const componentName = args2[0];
32625
33610
  if (!componentName) {
@@ -32636,7 +33621,7 @@ async function run21(args2, ctx2) {
32636
33621
  for (const p of searched) console.error(` - ${p}`);
32637
33622
  process.exit(1);
32638
33623
  }
32639
- const source = readFileSync17(resolved.filePath, "utf-8");
33624
+ const source = readFileSync18(resolved.filePath, "utf-8");
32640
33625
  const summary = buildComponentSummary2(source, resolved.filePath, resolved.componentName);
32641
33626
  if (ctx2.jsonFlag) {
32642
33627
  console.log(JSON.stringify(summary, null, 2));
@@ -105108,10 +106093,10 @@ __export(scenario_driver_exports, {
105108
106093
  runAutoScenario: () => runAutoScenario,
105109
106094
  runFileScenario: () => runFileScenario
105110
106095
  });
105111
- import { writeFileSync as writeFileSync9, mkdtempSync, rmSync, readFileSync as readFileSync18, existsSync as existsSync23, statSync as statSync3 } from "node:fs";
105112
- import { join as join2, dirname as dirname6, resolve as resolve11 } from "node:path";
106096
+ import { writeFileSync as writeFileSync9, mkdtempSync, rmSync, readFileSync as readFileSync19, existsSync as existsSync23, statSync as statSync3 } from "node:fs";
106097
+ import { join as join2, dirname as dirname7, resolve as resolve11 } from "node:path";
105113
106098
  import { tmpdir } from "node:os";
105114
- import ts24 from "typescript";
106099
+ import ts25 from "typescript";
105115
106100
  function externalRuntimeImport(clientJs) {
105116
106101
  const chunks = Array.isArray(clientJs) ? clientJs : [clientJs];
105117
106102
  for (const chunk of chunks) {
@@ -105180,12 +106165,12 @@ function resolveLocalFile(spec) {
105180
106165
  return null;
105181
106166
  }
105182
106167
  function rewriteLocalImports(js, chunkPath, inlined) {
105183
- const chunkDir = dirname6(chunkPath);
105184
- const sf = ts24.createSourceFile("chunk.mjs", js, ts24.ScriptTarget.Latest, false, ts24.ScriptKind.JS);
106168
+ const chunkDir = dirname7(chunkPath);
106169
+ const sf = ts25.createSourceFile("chunk.mjs", js, ts25.ScriptTarget.Latest, false, ts25.ScriptKind.JS);
105185
106170
  const edits = [];
105186
106171
  for (const stmt of sf.statements) {
105187
- if (!ts24.isImportDeclaration(stmt)) continue;
105188
- if (!ts24.isStringLiteral(stmt.moduleSpecifier)) continue;
106172
+ if (!ts25.isImportDeclaration(stmt)) continue;
106173
+ if (!ts25.isStringLiteral(stmt.moduleSpecifier)) continue;
105189
106174
  const spec = stmt.moduleSpecifier.text;
105190
106175
  if (!spec.startsWith(".")) continue;
105191
106176
  const resolved = resolveLocalFile(join2(chunkDir, spec.replace(/\.client\.js$/, "")));
@@ -105197,13 +106182,13 @@ function rewriteLocalImports(js, chunkPath, inlined) {
105197
106182
  const abs = resolve11(resolved);
105198
106183
  if (inlined.has(abs)) {
105199
106184
  const clause = stmt.importClause;
105200
- if (clause && (clause.name || clause.namedBindings && ts24.isNamespaceImport(clause.namedBindings))) {
106185
+ if (clause && (clause.name || clause.namedBindings && ts25.isNamespaceImport(clause.namedBindings))) {
105201
106186
  throw new Error(
105202
106187
  `"${spec}" (imported by ${chunkPath}) uses a default or namespace import of a sibling client file, which the dynamic scenario runner cannot bind after inlining that file into the run. Import its exports by name, or use the static budget (\`bf debug profile <component>\`), which needs no run.`
105203
106188
  );
105204
106189
  }
105205
106190
  const shims = [];
105206
- if (clause?.namedBindings && ts24.isNamedImports(clause.namedBindings)) {
106191
+ if (clause?.namedBindings && ts25.isNamedImports(clause.namedBindings)) {
105207
106192
  for (const el of clause.namedBindings.elements) {
105208
106193
  if (el.propertyName) shims.push(`var ${el.name.text} = ${el.propertyName.text}`);
105209
106194
  }
@@ -105228,17 +106213,17 @@ function loadWithLocalImports(entryPath, seedSource) {
105228
106213
  const resolved = resolveLocalFile(p);
105229
106214
  if (!resolved || visited.has(resolved)) return;
105230
106215
  visited.add(resolved);
105231
- const source = readFileSync18(resolved, "utf-8");
106216
+ const source = readFileSync19(resolved, "utf-8");
105232
106217
  for (const m of source.matchAll(/from\s+['"](\.[^'"]+)['"]/g)) {
105233
- visitImport(join2(dirname6(resolved), m[1]));
106218
+ visitImport(join2(dirname7(resolved), m[1]));
105234
106219
  }
105235
106220
  out.push({ source, filePath: resolved });
105236
106221
  };
105237
106222
  const entryResolved = resolveLocalFile(entryPath);
105238
106223
  if (seedSource === void 0 && !entryResolved) return out;
105239
106224
  if (entryResolved) visited.add(entryResolved);
105240
- const entrySource = seedSource ?? readFileSync18(entryResolved, "utf-8");
105241
- const entryDir = dirname6(entryResolved ?? entryPath);
106225
+ const entrySource = seedSource ?? readFileSync19(entryResolved, "utf-8");
106226
+ const entryDir = dirname7(entryResolved ?? entryPath);
105242
106227
  for (const m of entrySource.matchAll(/from\s+['"](\.[^'"]+)['"]/g)) {
105243
106228
  visitImport(join2(entryDir, m[1]));
105244
106229
  }
@@ -105369,7 +106354,7 @@ __export(debug_profile_exports, {
105369
106354
  run: () => run22
105370
106355
  });
105371
106356
  import { execFileSync as execFileSync2 } from "node:child_process";
105372
- import { readFileSync as readFileSync19 } from "node:fs";
106357
+ import { readFileSync as readFileSync20 } from "node:fs";
105373
106358
  import path24 from "node:path";
105374
106359
  function parseFailOn(raw) {
105375
106360
  const parts = raw.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
@@ -105472,7 +106457,7 @@ async function run22(args2, ctx2) {
105472
106457
  if (resolved.isPreview) {
105473
106458
  console.error(`Note: "${componentName}" has no index.tsx \u2014 profiling its preview (index.preview.tsx).`);
105474
106459
  }
105475
- const source = readFileSync19(resolved.filePath, "utf-8");
106460
+ const source = readFileSync20(resolved.filePath, "utf-8");
105476
106461
  if (flags.scenario) {
105477
106462
  try {
105478
106463
  const { runAutoScenario: runAutoScenario2, runFileScenario: runFileScenario2 } = await Promise.resolve().then(() => (init_scenario_driver(), scenario_driver_exports));
@@ -105712,9 +106697,9 @@ function findProjectConfig(startDir) {
105712
106697
  let dir = path.resolve(startDir);
105713
106698
  const { root: fsRoot } = path.parse(dir);
105714
106699
  while (true) {
105715
- const ts25 = path.join(dir, "barefoot.config.ts");
105716
- if (existsSync2(ts25)) {
105717
- return { dir, tsConfigPath: ts25 };
106700
+ const ts26 = path.join(dir, "barefoot.config.ts");
106701
+ if (existsSync2(ts26)) {
106702
+ return { dir, tsConfigPath: ts26 };
105718
106703
  }
105719
106704
  if (dir === fsRoot) return null;
105720
106705
  dir = path.dirname(dir);