@barefootjs/cli 0.17.1 → 0.18.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/docs/core/adapters/php-adapter.md +193 -0
- package/dist/docs/core/adapters/python-adapter.md +100 -0
- package/dist/docs/core/adapters/ruby-adapter.md +88 -0
- package/dist/docs/core/adapters/rust-adapter.md +136 -0
- package/dist/docs/core/adapters.md +10 -0
- package/dist/docs/core/advanced/error-codes.md +12 -4
- package/dist/docs/core/llms.txt +4 -0
- package/dist/docs/core/rendering/jsx-compatibility.md +35 -6
- package/dist/index.js +1383 -398
- package/package.json +4 -4
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
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
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 {
|
|
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
|
-
|
|
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 {
|
|
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")
|
|
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 (!
|
|
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
|
-
|
|
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:
|
|
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
|
|
8961
|
-
|
|
8962
|
-
|
|
8963
|
-
|
|
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 (
|
|
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(
|
|
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
|
-
|
|
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
|
|
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(
|
|
9592
|
-
if (js !==
|
|
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
|
|
12310
|
-
if (
|
|
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
|
|
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[
|
|
12319
|
-
if (env.acceptsTemplateCall && env.acceptsTemplateCall(
|
|
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
|
-
|
|
15833
|
-
|
|
15834
|
-
|
|
15835
|
-
|
|
15836
|
-
|
|
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
|
-
|
|
17966
|
-
|
|
17967
|
-
|
|
17968
|
-
|
|
17969
|
-
|
|
17970
|
-
|
|
17971
|
-
|
|
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))
|
|
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)
|
|
18313
|
-
|
|
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 === "
|
|
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(
|
|
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
|
|
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.
|
|
20053
|
-
} else if (!
|
|
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
|
|
20058
|
-
if (
|
|
20059
|
-
return !restNamesMisused(loop,
|
|
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
|
|
20689
|
+
var isLowerableObjectRestDestructure;
|
|
20166
20690
|
var init_loop_destructure = __esm({
|
|
20167
20691
|
"../jsx/src/loop-destructure.ts"() {
|
|
20168
20692
|
"use strict";
|
|
20169
|
-
|
|
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/
|
|
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
|
|
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
|
|
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 =
|
|
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 =
|
|
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:${
|
|
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
|
-
|
|
23904
|
-
|
|
23905
|
-
|
|
23906
|
-
|
|
23907
|
-
runtimeContent
|
|
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
|
-
|
|
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
|
-
|
|
23922
|
-
|
|
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] = [
|
|
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(
|
|
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 } =
|
|
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 =
|
|
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 =
|
|
24315
|
-
const outputDir =
|
|
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 =
|
|
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 =
|
|
25087
|
+
const sourceFile = ts24.createSourceFile(
|
|
24341
25088
|
"merge.js",
|
|
24342
25089
|
content2,
|
|
24343
|
-
|
|
25090
|
+
ts24.ScriptTarget.Latest,
|
|
24344
25091
|
/*setParentNodes*/
|
|
24345
25092
|
true,
|
|
24346
|
-
|
|
25093
|
+
ts24.ScriptKind.JS
|
|
24347
25094
|
);
|
|
24348
25095
|
for (const stmt of sourceFile.statements) {
|
|
24349
|
-
if (
|
|
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 =
|
|
25105
|
+
const sourceFile = ts24.createSourceFile(
|
|
24359
25106
|
"client.js",
|
|
24360
25107
|
content2,
|
|
24361
|
-
|
|
25108
|
+
ts24.ScriptTarget.Latest,
|
|
24362
25109
|
/*setParentNodes*/
|
|
24363
25110
|
true,
|
|
24364
|
-
|
|
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 (
|
|
25116
|
+
if (ts24.isImportDeclaration(node) || ts24.isExportDeclaration(node)) {
|
|
24370
25117
|
const ms = node.moduleSpecifier;
|
|
24371
|
-
if (ms &&
|
|
25118
|
+
if (ms && ts24.isStringLiteral(ms) && isBarefootClient(ms.text)) {
|
|
24372
25119
|
spans.push([ms.getStart(sourceFile), ms.getEnd()]);
|
|
24373
25120
|
}
|
|
24374
|
-
} else if (
|
|
25121
|
+
} else if (ts24.isCallExpression(node) && node.expression.kind === ts24.SyntaxKind.ImportKeyword) {
|
|
24375
25122
|
const arg = node.arguments[0];
|
|
24376
|
-
if (arg &&
|
|
25123
|
+
if (arg && ts24.isStringLiteral(arg) && isBarefootClient(arg.text)) {
|
|
24377
25124
|
spans.push([arg.getStart(sourceFile), arg.getEnd()]);
|
|
24378
25125
|
}
|
|
24379
25126
|
}
|
|
24380
|
-
|
|
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)
|
|
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
|
|
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
|
|
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 =
|
|
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(
|
|
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(
|
|
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
|
|
24762
|
-
|
|
24763
|
-
(f) => f.type === "ssrDefaults" && f.path ===
|
|
24764
|
-
|
|
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
|
-
|
|
25535
|
+
return JSON.parse(file.content);
|
|
24769
25536
|
} catch {
|
|
24770
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
28133
|
-
|
|
28134
|
-
|
|
28135
|
-
|
|
28136
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
29461
|
-
|
|
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:
|
|
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:
|
|
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
|
-
|
|
29578
|
-
|
|
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
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
|
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(
|
|
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(
|
|
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
|
|
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
|
-
|
|
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
|
|
30491
|
-
import { fileURLToPath as
|
|
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(
|
|
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
|
|
31577
|
+
var thisFile3;
|
|
30593
31578
|
var init_tokens = __esm({
|
|
30594
31579
|
"src/lib/tokens.ts"() {
|
|
30595
31580
|
"use strict";
|
|
30596
31581
|
init_runtime();
|
|
30597
|
-
|
|
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
|
|
30615
|
-
import { fileURLToPath as
|
|
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 =
|
|
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
|
|
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 =
|
|
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
|
|
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 =
|
|
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 =
|
|
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
|
|
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(
|
|
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
|
|
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 =
|
|
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
|
|
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 =
|
|
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
|
|
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 =
|
|
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
|
|
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 =
|
|
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
|
|
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 =
|
|
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
|
|
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 =
|
|
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
|
|
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 =
|
|
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
|
|
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 =
|
|
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
|
|
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 =
|
|
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
|
|
105112
|
-
import { join as join2, dirname as
|
|
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
|
|
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 =
|
|
105184
|
-
const sf =
|
|
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 (!
|
|
105188
|
-
if (!
|
|
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 &&
|
|
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 &&
|
|
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 =
|
|
106216
|
+
const source = readFileSync19(resolved, "utf-8");
|
|
105232
106217
|
for (const m of source.matchAll(/from\s+['"](\.[^'"]+)['"]/g)) {
|
|
105233
|
-
visitImport(join2(
|
|
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 ??
|
|
105241
|
-
const entryDir =
|
|
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
|
|
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 =
|
|
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
|
|
105716
|
-
if (existsSync2(
|
|
105717
|
-
return { dir, tsConfigPath:
|
|
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);
|