@barefootjs/cli 0.17.0 → 0.17.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/index.js +647 -452
- package/package.json +4 -4
package/dist/index.js
CHANGED
|
@@ -1733,6 +1733,57 @@ function stringifyParsedExpr(expr) {
|
|
|
1733
1733
|
return expr.raw;
|
|
1734
1734
|
}
|
|
1735
1735
|
}
|
|
1736
|
+
function materializeGetterCalls(expr, names) {
|
|
1737
|
+
const rw = (e) => materializeGetterCalls(e, names);
|
|
1738
|
+
switch (expr.kind) {
|
|
1739
|
+
case "call":
|
|
1740
|
+
if (expr.args.length === 0 && expr.callee.kind === "identifier" && names.has(expr.callee.name)) {
|
|
1741
|
+
return { kind: "identifier", name: expr.callee.name };
|
|
1742
|
+
}
|
|
1743
|
+
return { kind: "call", callee: rw(expr.callee), args: expr.args.map(rw) };
|
|
1744
|
+
case "binary":
|
|
1745
|
+
return { kind: "binary", op: expr.op, left: rw(expr.left), right: rw(expr.right) };
|
|
1746
|
+
case "logical":
|
|
1747
|
+
return { kind: "logical", op: expr.op, left: rw(expr.left), right: rw(expr.right) };
|
|
1748
|
+
case "unary":
|
|
1749
|
+
return { kind: "unary", op: expr.op, argument: rw(expr.argument) };
|
|
1750
|
+
case "conditional":
|
|
1751
|
+
return {
|
|
1752
|
+
kind: "conditional",
|
|
1753
|
+
test: rw(expr.test),
|
|
1754
|
+
consequent: rw(expr.consequent),
|
|
1755
|
+
alternate: rw(expr.alternate)
|
|
1756
|
+
};
|
|
1757
|
+
case "member":
|
|
1758
|
+
return { kind: "member", object: rw(expr.object), property: expr.property, computed: expr.computed };
|
|
1759
|
+
case "index-access":
|
|
1760
|
+
return { kind: "index-access", object: rw(expr.object), index: rw(expr.index) };
|
|
1761
|
+
case "template-literal":
|
|
1762
|
+
return {
|
|
1763
|
+
kind: "template-literal",
|
|
1764
|
+
parts: expr.parts.map((p) => p.type === "string" ? p : { type: "expression", expr: rw(p.expr) })
|
|
1765
|
+
};
|
|
1766
|
+
case "array-literal":
|
|
1767
|
+
return { kind: "array-literal", elements: expr.elements.map(rw) };
|
|
1768
|
+
case "array-method":
|
|
1769
|
+
if (expr.method === "flat") return { ...expr, object: rw(expr.object) };
|
|
1770
|
+
return { ...expr, object: rw(expr.object), args: expr.args.map(rw) };
|
|
1771
|
+
case "object-literal":
|
|
1772
|
+
return {
|
|
1773
|
+
kind: "object-literal",
|
|
1774
|
+
raw: expr.raw,
|
|
1775
|
+
properties: expr.properties.map((p) => ({ ...p, value: rw(p.value) }))
|
|
1776
|
+
};
|
|
1777
|
+
case "arrow":
|
|
1778
|
+
return { kind: "arrow", params: expr.params, body: rw(expr.body) };
|
|
1779
|
+
// Leaves / opaque shapes — nothing to rewrite.
|
|
1780
|
+
case "identifier":
|
|
1781
|
+
case "literal":
|
|
1782
|
+
case "regex":
|
|
1783
|
+
case "unsupported":
|
|
1784
|
+
return expr;
|
|
1785
|
+
}
|
|
1786
|
+
}
|
|
1736
1787
|
function serializeParsedExpr(expr) {
|
|
1737
1788
|
const node = toEvalNode(expr);
|
|
1738
1789
|
return node === null ? null : JSON.stringify(node);
|
|
@@ -1777,10 +1828,15 @@ function freeVarsInBody(body2, params) {
|
|
|
1777
1828
|
case "object-literal":
|
|
1778
1829
|
for (const p of e.properties) visit3(p.value);
|
|
1779
1830
|
return;
|
|
1831
|
+
case "array-method":
|
|
1832
|
+
if (e.method === "includes") {
|
|
1833
|
+
visit3(e.object);
|
|
1834
|
+
e.args.forEach(visit3);
|
|
1835
|
+
}
|
|
1836
|
+
return;
|
|
1780
1837
|
// Non-serializable kinds don't occur in a serializable body
|
|
1781
1838
|
// (serializeParsedExpr returns null for them); nothing to collect.
|
|
1782
1839
|
case "literal":
|
|
1783
|
-
case "array-method":
|
|
1784
1840
|
case "arrow":
|
|
1785
1841
|
case "regex":
|
|
1786
1842
|
case "unsupported":
|
|
@@ -1790,6 +1846,59 @@ function freeVarsInBody(body2, params) {
|
|
|
1790
1846
|
visit3(body2);
|
|
1791
1847
|
return [...found].sort();
|
|
1792
1848
|
}
|
|
1849
|
+
function freeIdentifiers(expr) {
|
|
1850
|
+
const free = /* @__PURE__ */ new Set();
|
|
1851
|
+
function visit3(e, bound) {
|
|
1852
|
+
switch (e.kind) {
|
|
1853
|
+
case "literal":
|
|
1854
|
+
case "regex":
|
|
1855
|
+
return true;
|
|
1856
|
+
case "identifier":
|
|
1857
|
+
if (!bound.has(e.name)) free.add(e.name);
|
|
1858
|
+
return true;
|
|
1859
|
+
case "call": {
|
|
1860
|
+
const isBuiltinCallee = evalBuiltinCalleeName(e.callee) !== null;
|
|
1861
|
+
if (!isBuiltinCallee && !visit3(e.callee, bound)) return false;
|
|
1862
|
+
for (const a of e.args) if (!visit3(a, bound)) return false;
|
|
1863
|
+
return true;
|
|
1864
|
+
}
|
|
1865
|
+
case "member":
|
|
1866
|
+
return visit3(e.object, bound);
|
|
1867
|
+
case "index-access":
|
|
1868
|
+
return visit3(e.object, bound) && visit3(e.index, bound);
|
|
1869
|
+
case "binary":
|
|
1870
|
+
case "logical":
|
|
1871
|
+
return visit3(e.left, bound) && visit3(e.right, bound);
|
|
1872
|
+
case "unary":
|
|
1873
|
+
return visit3(e.argument, bound);
|
|
1874
|
+
case "conditional":
|
|
1875
|
+
return visit3(e.test, bound) && visit3(e.consequent, bound) && visit3(e.alternate, bound);
|
|
1876
|
+
case "template-literal":
|
|
1877
|
+
for (const p of e.parts) {
|
|
1878
|
+
if (p.type === "expression" && !visit3(p.expr, bound)) return false;
|
|
1879
|
+
}
|
|
1880
|
+
return true;
|
|
1881
|
+
case "array-literal":
|
|
1882
|
+
for (const el of e.elements) if (!visit3(el, bound)) return false;
|
|
1883
|
+
return true;
|
|
1884
|
+
case "array-method":
|
|
1885
|
+
if (!visit3(e.object, bound)) return false;
|
|
1886
|
+
for (const a of e.args) if (!visit3(a, bound)) return false;
|
|
1887
|
+
return true;
|
|
1888
|
+
case "object-literal":
|
|
1889
|
+
for (const p of e.properties) if (!visit3(p.value, bound)) return false;
|
|
1890
|
+
return true;
|
|
1891
|
+
case "arrow": {
|
|
1892
|
+
const inner = new Set(bound);
|
|
1893
|
+
for (const p of e.params) inner.add(p);
|
|
1894
|
+
return visit3(e.body, inner);
|
|
1895
|
+
}
|
|
1896
|
+
case "unsupported":
|
|
1897
|
+
return false;
|
|
1898
|
+
}
|
|
1899
|
+
}
|
|
1900
|
+
return visit3(expr, /* @__PURE__ */ new Set()) ? free : null;
|
|
1901
|
+
}
|
|
1793
1902
|
function evalBuiltinCalleeName(callee) {
|
|
1794
1903
|
if (callee.kind === "identifier") {
|
|
1795
1904
|
return EVAL_BUILTIN_IDENTS.has(callee.name) ? callee.name : null;
|
|
@@ -1882,10 +1991,17 @@ function toEvalNode(e) {
|
|
|
1882
1991
|
}
|
|
1883
1992
|
return { kind: "object-literal", properties };
|
|
1884
1993
|
}
|
|
1994
|
+
case "array-method": {
|
|
1995
|
+
if (e.method === "includes" && e.args.length === 1) {
|
|
1996
|
+
const object = toEvalNode(e.object);
|
|
1997
|
+
const arg = toEvalNode(e.args[0]);
|
|
1998
|
+
return object && arg ? { kind: "array-method", method: "includes", object, args: [arg] } : null;
|
|
1999
|
+
}
|
|
2000
|
+
return null;
|
|
2001
|
+
}
|
|
1885
2002
|
// Outside the evaluator's pure-expression surface — refuse so the caller
|
|
1886
2003
|
// falls back to BF101 / `@client`. A nested `arrow` (a callback inside the
|
|
1887
2004
|
// body) is refused here, keeping the evaluator non-recursive.
|
|
1888
|
-
case "array-method":
|
|
1889
2005
|
case "arrow":
|
|
1890
2006
|
case "regex":
|
|
1891
2007
|
case "unsupported":
|
|
@@ -1907,8 +2023,12 @@ var init_expression_parser = __esm({
|
|
|
1907
2023
|
UNSUPPORTED_METHODS = /* @__PURE__ */ new Set([
|
|
1908
2024
|
// Higher-order array methods. Seven of these (`filter`, `every`,
|
|
1909
2025
|
// `some`, `find`, `findIndex`, `findLast`, `findLastIndex`) are
|
|
1910
|
-
// intercepted as `higher-order` IR before reaching this gate
|
|
1911
|
-
// `map` is intercepted as an IRLoop
|
|
2026
|
+
// intercepted as `higher-order` IR before reaching this gate.
|
|
2027
|
+
// `map` is intercepted as an IRLoop when its callback returns JSX,
|
|
2028
|
+
// and as a `CALLBACK_METHODS` evaluator lowering (`map_eval`, #2073)
|
|
2029
|
+
// when it returns a value — it stays listed here so the fall-throughs
|
|
2030
|
+
// (a bare `arr.map` reference, a function-reference callback) still
|
|
2031
|
+
// refuse loudly. `reduce` / `reduceRight` stay
|
|
1912
2032
|
// listed here so the shapes the Tier C catalogue can't lower still
|
|
1913
2033
|
// refuse loudly: the `convertNode` call branch intercepts a matching
|
|
1914
2034
|
// `.reduce(fn, init)` / `.reduceRight(fn, init)` into the structured
|
|
@@ -2021,6 +2141,7 @@ var init_expression_parser = __esm({
|
|
|
2021
2141
|
]);
|
|
2022
2142
|
CALLBACK_METHODS = /* @__PURE__ */ new Set([
|
|
2023
2143
|
"filter",
|
|
2144
|
+
"map",
|
|
2024
2145
|
"every",
|
|
2025
2146
|
"some",
|
|
2026
2147
|
"find",
|
|
@@ -3869,8 +3990,8 @@ function propResolvesUnsafe(prop, env, unsafeLocalNames) {
|
|
|
3869
3990
|
return false;
|
|
3870
3991
|
}
|
|
3871
3992
|
if (!source) return false;
|
|
3872
|
-
const { freeIdentifiers } = csrSubstitute(source, env);
|
|
3873
|
-
return setIntersects(
|
|
3993
|
+
const { freeIdentifiers: freeIdentifiers2 } = csrSubstitute(source, env);
|
|
3994
|
+
return setIntersects(freeIdentifiers2, unsafeLocalNames);
|
|
3874
3995
|
}
|
|
3875
3996
|
function computeDeferredChildSlots(node, ctx2, inlinableConstants, unsafeLocalNames, propsObjectName) {
|
|
3876
3997
|
const deferred = /* @__PURE__ */ new Set();
|
|
@@ -3917,8 +4038,8 @@ function generateCsrTemplateWithOpts(node, opts) {
|
|
|
3917
4038
|
const transformExpr = (expr, templateExpr) => {
|
|
3918
4039
|
const source = templateExpr ?? expr;
|
|
3919
4040
|
if (!source) return source;
|
|
3920
|
-
const { rewritten, freeIdentifiers } = csrSubstitute(source, env);
|
|
3921
|
-
if (unsafeLocalNames && unsafeLocalNames.size > 0 && setIntersects(
|
|
4041
|
+
const { rewritten, freeIdentifiers: freeIdentifiers2 } = csrSubstitute(source, env);
|
|
4042
|
+
if (unsafeLocalNames && unsafeLocalNames.size > 0 && setIntersects(freeIdentifiers2, unsafeLocalNames)) {
|
|
3922
4043
|
return UNSAFE_TEMPLATE_EXPR;
|
|
3923
4044
|
}
|
|
3924
4045
|
return applyPropsRewrite(rewritten, propsObjectName ?? null);
|
|
@@ -6195,7 +6316,7 @@ function collectConstant(node, ctx2, _isModule, declarationKind = "const", isExp
|
|
|
6195
6316
|
const baseValue = `${propsName}.${sourceKey}`;
|
|
6196
6317
|
const value3 = defaultValueExpr ? `${baseValue} ?? ${defaultValueExpr}` : baseValue;
|
|
6197
6318
|
const containsArrow2 = el.initializer ? nodeContainsArrow(el.initializer) : false;
|
|
6198
|
-
const
|
|
6319
|
+
const freeIdentifiers3 = el.initializer ? extractFreeIdentifiersFromNode(el.initializer) : /* @__PURE__ */ new Set([propsName]);
|
|
6199
6320
|
ctx2.localConstants.push({
|
|
6200
6321
|
name: localName2,
|
|
6201
6322
|
value: value3,
|
|
@@ -6203,7 +6324,7 @@ function collectConstant(node, ctx2, _isModule, declarationKind = "const", isExp
|
|
|
6203
6324
|
isExported,
|
|
6204
6325
|
type: null,
|
|
6205
6326
|
loc: getSourceLocation(node, ctx2.sourceFile, ctx2.filePath),
|
|
6206
|
-
freeIdentifiers:
|
|
6327
|
+
freeIdentifiers: freeIdentifiers3,
|
|
6207
6328
|
containsArrow: containsArrow2 || void 0,
|
|
6208
6329
|
// Body-level destructure-from-props is collected only when
|
|
6209
6330
|
// _isModule === false; binding lives in init scope.
|
|
@@ -6281,7 +6402,7 @@ function collectConstant(node, ctx2, _isModule, declarationKind = "const", isExp
|
|
|
6281
6402
|
} else if (value2) {
|
|
6282
6403
|
type2 = inferTypeFromValue(value2);
|
|
6283
6404
|
}
|
|
6284
|
-
const
|
|
6405
|
+
const freeIdentifiers2 = node.initializer ? extractFreeIdentifiersFromNode(node.initializer) : void 0;
|
|
6285
6406
|
const containsArrow = node.initializer ? nodeContainsArrow(node.initializer) : false;
|
|
6286
6407
|
const systemConstructKind = node.initializer ? getSystemConstructKind(node.initializer) : void 0;
|
|
6287
6408
|
let templateValue;
|
|
@@ -6328,7 +6449,7 @@ function collectConstant(node, ctx2, _isModule, declarationKind = "const", isExp
|
|
|
6328
6449
|
isModule: isModule || void 0,
|
|
6329
6450
|
type: type2,
|
|
6330
6451
|
loc: getSourceLocation(node, ctx2.sourceFile, ctx2.filePath),
|
|
6331
|
-
freeIdentifiers,
|
|
6452
|
+
freeIdentifiers: freeIdentifiers2,
|
|
6332
6453
|
isJsx,
|
|
6333
6454
|
isJsxFunction: isJsxFunction || void 0,
|
|
6334
6455
|
containsArrow: containsArrow || void 0,
|
|
@@ -9776,14 +9897,14 @@ function processAttributes(attributes2, ctx2) {
|
|
|
9776
9897
|
clientOnly = true;
|
|
9777
9898
|
}
|
|
9778
9899
|
}
|
|
9779
|
-
const
|
|
9900
|
+
const freeIdentifiers2 = attrFreeIdentifiers(attr);
|
|
9780
9901
|
attrs.push({
|
|
9781
9902
|
name: name2,
|
|
9782
9903
|
value: value2,
|
|
9783
9904
|
clientOnly,
|
|
9784
9905
|
loc: getSourceLocation(attr, ctx2.sourceFile, ctx2.filePath),
|
|
9785
9906
|
...computeReactivityFlags(attr, ctx2),
|
|
9786
|
-
...
|
|
9907
|
+
...freeIdentifiers2 !== void 0 && { freeIdentifiers: freeIdentifiers2 }
|
|
9787
9908
|
});
|
|
9788
9909
|
}
|
|
9789
9910
|
return { attrs, events, ref };
|
|
@@ -10090,14 +10211,14 @@ function processComponentProps(attributes2, ctx2) {
|
|
|
10090
10211
|
clientOnly = true;
|
|
10091
10212
|
}
|
|
10092
10213
|
}
|
|
10093
|
-
const
|
|
10214
|
+
const freeIdentifiers2 = attrFreeIdentifiers(attr);
|
|
10094
10215
|
props.push({
|
|
10095
10216
|
name: name2,
|
|
10096
10217
|
value: value2,
|
|
10097
10218
|
clientOnly,
|
|
10098
10219
|
loc: getSourceLocation(attr, ctx2.sourceFile, ctx2.filePath),
|
|
10099
10220
|
...computeReactivityFlags(attr, ctx2),
|
|
10100
|
-
...
|
|
10221
|
+
...freeIdentifiers2 !== void 0 && { freeIdentifiers: freeIdentifiers2 }
|
|
10101
10222
|
});
|
|
10102
10223
|
}
|
|
10103
10224
|
return props;
|
|
@@ -10521,7 +10642,7 @@ function decideWrapForChildProp(expandedValue, ctx2, prop) {
|
|
|
10521
10642
|
if (expandedValue.includes("props.")) return { wrap: true, reason: "props-access" };
|
|
10522
10643
|
return decideWrapForAttr(expandedValue, ctx2, prop);
|
|
10523
10644
|
}
|
|
10524
|
-
function needsEffectWrapper(expr, ctx2,
|
|
10645
|
+
function needsEffectWrapper(expr, ctx2, freeIdentifiers2) {
|
|
10525
10646
|
for (const signal2 of ctx2.signals) {
|
|
10526
10647
|
if (new RegExp(`\\b${signal2.getter}\\s*\\(`).test(expr)) {
|
|
10527
10648
|
return true;
|
|
@@ -10534,7 +10655,7 @@ function needsEffectWrapper(expr, ctx2, freeIdentifiers) {
|
|
|
10534
10655
|
}
|
|
10535
10656
|
for (const prop of ctx2.propsParams) {
|
|
10536
10657
|
if (prop.name === "children") continue;
|
|
10537
|
-
if (
|
|
10658
|
+
if (freeIdentifiers2 ? freeIdentifiers2.has(prop.name) : tokenContainsIdent(expr, prop.name)) {
|
|
10538
10659
|
return true;
|
|
10539
10660
|
}
|
|
10540
10661
|
}
|
|
@@ -10544,8 +10665,8 @@ function needsEffectWrapper(expr, ctx2, freeIdentifiers) {
|
|
|
10544
10665
|
}
|
|
10545
10666
|
return false;
|
|
10546
10667
|
}
|
|
10547
|
-
function classifyReactivity(expr, ctx2, loopParam, loopParamBindings,
|
|
10548
|
-
const has = (name2) =>
|
|
10668
|
+
function classifyReactivity(expr, ctx2, loopParam, loopParamBindings, freeIdentifiers2) {
|
|
10669
|
+
const has = (name2) => freeIdentifiers2 ? freeIdentifiers2.has(name2) : tokenContainsIdent(expr, name2);
|
|
10549
10670
|
if (loopParamBindings && loopParamBindings.length > 0) {
|
|
10550
10671
|
for (const b of loopParamBindings) {
|
|
10551
10672
|
if (has(b.name)) {
|
|
@@ -10555,7 +10676,7 @@ function classifyReactivity(expr, ctx2, loopParam, loopParamBindings, freeIdenti
|
|
|
10555
10676
|
} else if (loopParam && has(loopParam)) {
|
|
10556
10677
|
return { kind: "loop-param", param: loopParam };
|
|
10557
10678
|
}
|
|
10558
|
-
if (needsEffectWrapper(expr, ctx2,
|
|
10679
|
+
if (needsEffectWrapper(expr, ctx2, freeIdentifiers2)) {
|
|
10559
10680
|
return { kind: "signal-or-memo-or-prop" };
|
|
10560
10681
|
}
|
|
10561
10682
|
return { kind: "none" };
|
|
@@ -12483,9 +12604,9 @@ function populateCsrInlinable(ctx2, relocateEnv) {
|
|
|
12483
12604
|
if (finalised.has(c.name)) continue;
|
|
12484
12605
|
const env = buildEnvWithConsts();
|
|
12485
12606
|
const source = c.value.trim();
|
|
12486
|
-
const { rewritten, freeIdentifiers } = csrSubstitute(source, env);
|
|
12607
|
+
const { rewritten, freeIdentifiers: freeIdentifiers2 } = csrSubstitute(source, env);
|
|
12487
12608
|
let pendingDependency = false;
|
|
12488
|
-
for (const id2 of
|
|
12609
|
+
for (const id2 of freeIdentifiers2) {
|
|
12489
12610
|
if (id2 === c.name) continue;
|
|
12490
12611
|
const dep = ctx2.localConstants.find((o) => o.name === id2);
|
|
12491
12612
|
if (dep && !finalised.has(dep.name)) {
|
|
@@ -12499,7 +12620,7 @@ function populateCsrInlinable(ctx2, relocateEnv) {
|
|
|
12499
12620
|
ctx2.csrInlinable.set(c.name, null);
|
|
12500
12621
|
} else {
|
|
12501
12622
|
const bridgedRewritten = inlineResult.rewrittenValue;
|
|
12502
|
-
const bridgedFreeIdentifiers = recomputeFreeIdentifiers(bridgedRewritten,
|
|
12623
|
+
const bridgedFreeIdentifiers = recomputeFreeIdentifiers(bridgedRewritten, freeIdentifiers2);
|
|
12503
12624
|
ctx2.csrInlinable.set(c.name, { rewrittenValue: bridgedRewritten, freeIdentifiers: bridgedFreeIdentifiers });
|
|
12504
12625
|
constSubs.set(c.name, {
|
|
12505
12626
|
kind: "identifier",
|
|
@@ -13124,13 +13245,22 @@ var init_declaration_sort = __esm({
|
|
|
13124
13245
|
});
|
|
13125
13246
|
|
|
13126
13247
|
// ../jsx/src/adapters/env-signal.ts
|
|
13127
|
-
function
|
|
13248
|
+
function envSignalReaderFor(key) {
|
|
13249
|
+
if (key === void 0) return null;
|
|
13250
|
+
return ENV_SIGNAL_READERS.get(key) ?? null;
|
|
13251
|
+
}
|
|
13252
|
+
function envSignalLocalNames(metadata, key) {
|
|
13128
13253
|
const names = /* @__PURE__ */ new Set();
|
|
13129
13254
|
for (const s of metadata.signals) {
|
|
13130
|
-
if (s.envReader ===
|
|
13255
|
+
if (s.envReader !== void 0 && (key === void 0 || s.envReader === key)) {
|
|
13256
|
+
names.add(s.getter);
|
|
13257
|
+
}
|
|
13131
13258
|
}
|
|
13132
13259
|
return names;
|
|
13133
13260
|
}
|
|
13261
|
+
function searchParamsLocalNames(metadata) {
|
|
13262
|
+
return envSignalLocalNames(metadata, "search");
|
|
13263
|
+
}
|
|
13134
13264
|
function importsSearchParams(metadata) {
|
|
13135
13265
|
return searchParamsLocalNames(metadata).size > 0;
|
|
13136
13266
|
}
|
|
@@ -13153,13 +13283,16 @@ function matchSearchParamsMethodCall(callee, args2, localNames) {
|
|
|
13153
13283
|
}
|
|
13154
13284
|
return { method: callee.property, args: args2 };
|
|
13155
13285
|
}
|
|
13156
|
-
var ENV_SIGNAL_CLIENT_FACTORY, QUERY_HREF_SOURCES;
|
|
13286
|
+
var ENV_SIGNAL_CLIENT_FACTORY, ENV_SIGNAL_READERS, QUERY_HREF_SOURCES;
|
|
13157
13287
|
var init_env_signal = __esm({
|
|
13158
13288
|
"../jsx/src/adapters/env-signal.ts"() {
|
|
13159
13289
|
"use strict";
|
|
13160
13290
|
ENV_SIGNAL_CLIENT_FACTORY = {
|
|
13161
13291
|
search: "createSearchParams"
|
|
13162
13292
|
};
|
|
13293
|
+
ENV_SIGNAL_READERS = /* @__PURE__ */ new Map([
|
|
13294
|
+
["search", { key: "search", canonicalName: "searchParams", methods: /* @__PURE__ */ new Set(["get"]) }]
|
|
13295
|
+
]);
|
|
13163
13296
|
QUERY_HREF_SOURCES = /* @__PURE__ */ new Set([
|
|
13164
13297
|
"@barefootjs/client",
|
|
13165
13298
|
"@barefootjs/client/runtime"
|
|
@@ -18114,105 +18247,463 @@ var init_ssr_defaults = __esm({
|
|
|
18114
18247
|
}
|
|
18115
18248
|
});
|
|
18116
18249
|
|
|
18117
|
-
// ../jsx/src/
|
|
18118
|
-
|
|
18119
|
-
|
|
18120
|
-
const
|
|
18121
|
-
const
|
|
18122
|
-
const
|
|
18123
|
-
|
|
18124
|
-
|
|
18125
|
-
const fold = (src, rawNames, idx, names, render) => {
|
|
18126
|
-
if (!idx.has(src)) {
|
|
18127
|
-
idx.set(src, result2.length);
|
|
18128
|
-
names.set(src, /* @__PURE__ */ new Set());
|
|
18129
|
-
result2.push("");
|
|
18130
|
-
}
|
|
18131
|
-
const set = names.get(src);
|
|
18132
|
-
for (const n of rawNames.split(",").map((s) => s.trim()).filter(Boolean)) set.add(n);
|
|
18133
|
-
result2[idx.get(src)] = render(src, set);
|
|
18134
|
-
};
|
|
18135
|
-
for (const raw of lines) {
|
|
18136
|
-
const line = raw.trim();
|
|
18137
|
-
if (!line) continue;
|
|
18138
|
-
const typeMatch = line.match(/^import\s+type\s*\{([^}]+)\}\s*from\s*['"]([^'"]+)['"]\s*;?$/);
|
|
18139
|
-
const valueMatch = line.match(/^import\s*\{([^}]+)\}\s*from\s*['"]([^'"]+)['"]\s*;?$/);
|
|
18140
|
-
if (valueMatch) {
|
|
18141
|
-
fold(valueMatch[2], valueMatch[1], valueIdx, valueNames, (s, n) => `import { ${[...n].join(", ")} } from '${s}'`);
|
|
18142
|
-
} else if (typeMatch) {
|
|
18143
|
-
fold(typeMatch[2], typeMatch[1], typeIdx, typeNames, (s, n) => `import type { ${[...n].join(", ")} } from '${s}'`);
|
|
18144
|
-
} else if (!seenOther.has(line)) {
|
|
18145
|
-
seenOther.add(line);
|
|
18146
|
-
result2.push(line);
|
|
18147
|
-
}
|
|
18250
|
+
// ../jsx/src/augment-inherited-props.ts
|
|
18251
|
+
import ts17 from "typescript";
|
|
18252
|
+
function collectContextConsumers(metadata) {
|
|
18253
|
+
const constants = metadata.localConstants ?? [];
|
|
18254
|
+
const contextDefaults = /* @__PURE__ */ new Map();
|
|
18255
|
+
for (const c of constants) {
|
|
18256
|
+
if (c.systemConstructKind !== "createContext" || c.value === void 0) continue;
|
|
18257
|
+
contextDefaults.set(c.name, parseCreateContextDefault(c.value));
|
|
18148
18258
|
}
|
|
18149
|
-
|
|
18150
|
-
|
|
18151
|
-
|
|
18152
|
-
|
|
18153
|
-
|
|
18154
|
-
|
|
18155
|
-
|
|
18156
|
-
|
|
18157
|
-
|
|
18158
|
-
|
|
18159
|
-
|
|
18160
|
-
errors.push(...ctx2.errors);
|
|
18161
|
-
continue;
|
|
18162
|
-
}
|
|
18163
|
-
const ir = jsxToIR(ctx2);
|
|
18164
|
-
errors.push(...ctx2.errors);
|
|
18165
|
-
if (!ir) continue;
|
|
18166
|
-
const componentIR = {
|
|
18167
|
-
version: "0.1",
|
|
18168
|
-
metadata: buildMetadata(ctx2),
|
|
18169
|
-
root: ir,
|
|
18170
|
-
errors: []
|
|
18171
|
-
};
|
|
18172
|
-
componentIR.metadata.clientAnalysis = analyzeClientNeeds(componentIR);
|
|
18173
|
-
if (options2.cssLayerPrefix) {
|
|
18174
|
-
applyCssLayerPrefix(componentIR, options2.cssLayerPrefix);
|
|
18175
|
-
}
|
|
18176
|
-
entries2.push({ componentIR, ctx: ctx2 });
|
|
18259
|
+
if (contextDefaults.size === 0) return [];
|
|
18260
|
+
const consumers = [];
|
|
18261
|
+
for (const c of constants) {
|
|
18262
|
+
if (c.value === void 0) continue;
|
|
18263
|
+
const ctxName = parseUseContextArg(c.value);
|
|
18264
|
+
if (ctxName === null || !contextDefaults.has(ctxName)) continue;
|
|
18265
|
+
consumers.push({
|
|
18266
|
+
localName: c.name,
|
|
18267
|
+
contextName: ctxName,
|
|
18268
|
+
defaultValue: contextDefaults.get(ctxName) ?? null
|
|
18269
|
+
});
|
|
18177
18270
|
}
|
|
18178
|
-
|
|
18179
|
-
|
|
18180
|
-
|
|
18181
|
-
|
|
18182
|
-
|
|
18183
|
-
|
|
18184
|
-
|
|
18185
|
-
|
|
18186
|
-
|
|
18271
|
+
return consumers;
|
|
18272
|
+
}
|
|
18273
|
+
function parseUseContextArg(source) {
|
|
18274
|
+
const expr = parseSingleExpression(source);
|
|
18275
|
+
if (!expr || !ts17.isCallExpression(expr)) return null;
|
|
18276
|
+
if (!ts17.isIdentifier(expr.expression) || expr.expression.text !== "useContext") return null;
|
|
18277
|
+
if (expr.arguments.length !== 1) return null;
|
|
18278
|
+
const arg = expr.arguments[0];
|
|
18279
|
+
return ts17.isIdentifier(arg) ? arg.text : null;
|
|
18280
|
+
}
|
|
18281
|
+
function parseCreateContextDefault(source) {
|
|
18282
|
+
const expr = parseSingleExpression(source);
|
|
18283
|
+
if (!expr || !ts17.isCallExpression(expr)) return null;
|
|
18284
|
+
if (expr.arguments.length === 0) return null;
|
|
18285
|
+
const arg = expr.arguments[0];
|
|
18286
|
+
if (ts17.isStringLiteral(arg) || ts17.isNoSubstitutionTemplateLiteral(arg)) return arg.text;
|
|
18287
|
+
if (ts17.isNumericLiteral(arg)) return Number(arg.text);
|
|
18288
|
+
if (arg.kind === ts17.SyntaxKind.TrueKeyword) return true;
|
|
18289
|
+
if (arg.kind === ts17.SyntaxKind.FalseKeyword) return false;
|
|
18290
|
+
return null;
|
|
18291
|
+
}
|
|
18292
|
+
function parseSingleExpression(source) {
|
|
18293
|
+
const sf = ts17.createSourceFile("__ctx.ts", `(${source})`, ts17.ScriptTarget.Latest, false);
|
|
18294
|
+
const stmt = sf.statements[0];
|
|
18295
|
+
if (!stmt || !ts17.isExpressionStatement(stmt)) return null;
|
|
18296
|
+
let e = stmt.expression;
|
|
18297
|
+
while (ts17.isParenthesizedExpression(e)) e = e.expression;
|
|
18298
|
+
return e;
|
|
18299
|
+
}
|
|
18300
|
+
function augmentInheritedPropAccesses(ir) {
|
|
18301
|
+
const propsObj = ir.metadata.propsObjectName;
|
|
18302
|
+
if (!propsObj) return;
|
|
18303
|
+
const existing = new Set(ir.metadata.propsParams.map((p) => p.name));
|
|
18304
|
+
const bareRefProps = /* @__PURE__ */ new Set();
|
|
18305
|
+
const booleanAttrProps = /* @__PURE__ */ new Set();
|
|
18306
|
+
const accessed = /* @__PURE__ */ new Set();
|
|
18307
|
+
const accessRe = new RegExp(`(?:^|[^\\w$.])${propsObj}\\.([A-Za-z_$][\\w$]*)`, "g");
|
|
18308
|
+
const scan = (s) => {
|
|
18309
|
+
if (!s) return;
|
|
18310
|
+
for (const m of s.matchAll(accessRe)) accessed.add(m[1]);
|
|
18311
|
+
};
|
|
18312
|
+
for (const memo of ir.metadata.memos) scan(memo.computation);
|
|
18313
|
+
for (const signal2 of ir.metadata.signals) scan(signal2.initialValue);
|
|
18314
|
+
for (const stmt of ir.metadata.initStatements ?? []) scan(stmt.body);
|
|
18315
|
+
for (const eff of ir.metadata.effects ?? []) scan(eff.body);
|
|
18316
|
+
for (const c of ir.metadata.localConstants ?? []) {
|
|
18317
|
+
if (c.isModule) continue;
|
|
18318
|
+
scan(c.value);
|
|
18187
18319
|
}
|
|
18188
|
-
const
|
|
18189
|
-
|
|
18190
|
-
|
|
18191
|
-
|
|
18192
|
-
|
|
18193
|
-
|
|
18320
|
+
const walk = (node) => {
|
|
18321
|
+
if (!node) return;
|
|
18322
|
+
const el = node;
|
|
18323
|
+
for (const attr of el.attrs ?? []) {
|
|
18324
|
+
const v = attr.value;
|
|
18325
|
+
if (v?.parts) {
|
|
18326
|
+
for (const part of v.parts) {
|
|
18327
|
+
if (part.type === "string") scan(part.value);
|
|
18328
|
+
else if (part.type === "ternary") {
|
|
18329
|
+
scan(part.condition);
|
|
18330
|
+
scan(part.whenTrue);
|
|
18331
|
+
scan(part.whenFalse);
|
|
18332
|
+
} else if (part.type === "lookup") scan(part.key);
|
|
18333
|
+
}
|
|
18334
|
+
}
|
|
18335
|
+
if (v?.kind === "expression" && typeof v.expr === "string") {
|
|
18336
|
+
scan(v.expr);
|
|
18337
|
+
const expr = v.expr.trim();
|
|
18338
|
+
const prefix2 = `${propsObj}.`;
|
|
18339
|
+
if (isBooleanAttr(attr.name) || v.presenceOrUndefined) {
|
|
18340
|
+
const m = expr.match(new RegExp(`^${propsObj}\\.([A-Za-z_$][\\w$]*)`));
|
|
18341
|
+
if (m) booleanAttrProps.add(m[1]);
|
|
18342
|
+
} else if (expr.startsWith(prefix2)) {
|
|
18343
|
+
const rest2 = expr.slice(prefix2.length);
|
|
18344
|
+
if (/^[A-Za-z_$][\w$]*$/.test(rest2)) bareRefProps.add(rest2);
|
|
18345
|
+
}
|
|
18346
|
+
}
|
|
18194
18347
|
}
|
|
18195
|
-
|
|
18196
|
-
|
|
18197
|
-
|
|
18198
|
-
const fileScope = computeFileScope(filePath);
|
|
18199
|
-
const nonExportedSiblings = /* @__PURE__ */ new Set();
|
|
18200
|
-
for (const { componentIR } of entries2) {
|
|
18201
|
-
if (!componentIR.metadata.isExported) {
|
|
18202
|
-
nonExportedSiblings.add(componentIR.metadata.componentName);
|
|
18348
|
+
for (const child of el.children ?? []) {
|
|
18349
|
+
const c = child;
|
|
18350
|
+
walk(c.element ?? child);
|
|
18203
18351
|
}
|
|
18204
|
-
|
|
18205
|
-
|
|
18206
|
-
|
|
18207
|
-
|
|
18208
|
-
|
|
18352
|
+
const branchy = node;
|
|
18353
|
+
walk(branchy.whenTrue);
|
|
18354
|
+
walk(branchy.whenFalse);
|
|
18355
|
+
walk(branchy.consequent);
|
|
18356
|
+
walk(branchy.alternate);
|
|
18209
18357
|
};
|
|
18210
|
-
|
|
18211
|
-
|
|
18212
|
-
|
|
18213
|
-
|
|
18214
|
-
|
|
18215
|
-
|
|
18358
|
+
walk(ir.root);
|
|
18359
|
+
for (const name2 of accessed) {
|
|
18360
|
+
if (existing.has(name2)) continue;
|
|
18361
|
+
let raw;
|
|
18362
|
+
if (booleanAttrProps.has(name2)) raw = "boolean";
|
|
18363
|
+
else if (bareRefProps.has(name2)) raw = "unknown";
|
|
18364
|
+
else raw = "string";
|
|
18365
|
+
const type2 = raw === "boolean" ? { kind: "primitive", raw: "boolean", primitive: "boolean" } : raw === "string" ? { kind: "primitive", raw: "string", primitive: "string" } : { kind: "unknown", raw: "unknown" };
|
|
18366
|
+
ir.metadata.propsParams.push({ name: name2, type: type2, optional: true });
|
|
18367
|
+
existing.add(name2);
|
|
18368
|
+
}
|
|
18369
|
+
}
|
|
18370
|
+
function parseStaticStringConst(source) {
|
|
18371
|
+
const sf = ts17.createSourceFile(
|
|
18372
|
+
"__const.ts",
|
|
18373
|
+
`const __x = (${source});`,
|
|
18374
|
+
ts17.ScriptTarget.Latest,
|
|
18375
|
+
/*setParentNodes*/
|
|
18376
|
+
false
|
|
18377
|
+
);
|
|
18378
|
+
const stmt = sf.statements[0];
|
|
18379
|
+
if (!stmt || !ts17.isVariableStatement(stmt)) return null;
|
|
18380
|
+
let init = stmt.declarationList.declarations[0]?.initializer;
|
|
18381
|
+
while (init && ts17.isParenthesizedExpression(init)) init = init.expression;
|
|
18382
|
+
if (!init) return null;
|
|
18383
|
+
if (ts17.isStringLiteral(init) || ts17.isNoSubstitutionTemplateLiteral(init)) {
|
|
18384
|
+
return init.text;
|
|
18385
|
+
}
|
|
18386
|
+
return evalStringArrayJoin(source);
|
|
18387
|
+
}
|
|
18388
|
+
function evalTemplateOfStringConsts(source, resolved) {
|
|
18389
|
+
const sf = ts17.createSourceFile(
|
|
18390
|
+
"__const.ts",
|
|
18391
|
+
`const __x = (${source});`,
|
|
18392
|
+
ts17.ScriptTarget.Latest,
|
|
18393
|
+
/*setParentNodes*/
|
|
18394
|
+
false
|
|
18395
|
+
);
|
|
18396
|
+
const stmt = sf.statements[0];
|
|
18397
|
+
if (!stmt || !ts17.isVariableStatement(stmt)) return null;
|
|
18398
|
+
let init = stmt.declarationList.declarations[0]?.initializer;
|
|
18399
|
+
while (init && ts17.isParenthesizedExpression(init)) init = init.expression;
|
|
18400
|
+
if (!init || !ts17.isTemplateExpression(init)) return null;
|
|
18401
|
+
let out = init.head.text;
|
|
18402
|
+
for (const span of init.templateSpans) {
|
|
18403
|
+
if (!ts17.isIdentifier(span.expression)) return null;
|
|
18404
|
+
const value2 = resolved.get(span.expression.text);
|
|
18405
|
+
if (value2 === void 0) return null;
|
|
18406
|
+
out += value2 + span.literal.text;
|
|
18407
|
+
}
|
|
18408
|
+
return out;
|
|
18409
|
+
}
|
|
18410
|
+
function collectModuleStringConsts(constants) {
|
|
18411
|
+
const map = /* @__PURE__ */ new Map();
|
|
18412
|
+
const candidates = (constants ?? []).filter(
|
|
18413
|
+
(c) => c.isModule && c.value !== void 0
|
|
18414
|
+
);
|
|
18415
|
+
let progressed = true;
|
|
18416
|
+
while (progressed) {
|
|
18417
|
+
progressed = false;
|
|
18418
|
+
for (const c of candidates) {
|
|
18419
|
+
if (map.has(c.name)) continue;
|
|
18420
|
+
const literal = parseStaticStringConst(c.value) ?? evalTemplateOfStringConsts(c.value, map);
|
|
18421
|
+
if (literal !== null) {
|
|
18422
|
+
map.set(c.name, literal);
|
|
18423
|
+
progressed = true;
|
|
18424
|
+
}
|
|
18425
|
+
}
|
|
18426
|
+
}
|
|
18427
|
+
return map;
|
|
18428
|
+
}
|
|
18429
|
+
function lookupStaticRecordLiteral(objectName, key, constants) {
|
|
18430
|
+
const constInfo = (constants ?? []).find((c) => c.name === objectName && c.isModule);
|
|
18431
|
+
if (constInfo?.value === void 0) return null;
|
|
18432
|
+
const sf = ts17.createSourceFile(
|
|
18433
|
+
"__rec.ts",
|
|
18434
|
+
`(${constInfo.value})`,
|
|
18435
|
+
ts17.ScriptTarget.Latest,
|
|
18436
|
+
/*setParentNodes*/
|
|
18437
|
+
true
|
|
18438
|
+
);
|
|
18439
|
+
if (sf.statements.length !== 1) return null;
|
|
18440
|
+
const stmt = sf.statements[0];
|
|
18441
|
+
if (!ts17.isExpressionStatement(stmt)) return null;
|
|
18442
|
+
let parsed = stmt.expression;
|
|
18443
|
+
while (ts17.isParenthesizedExpression(parsed)) parsed = parsed.expression;
|
|
18444
|
+
if (!ts17.isObjectLiteralExpression(parsed)) return null;
|
|
18445
|
+
for (const prop of parsed.properties) {
|
|
18446
|
+
if (!ts17.isPropertyAssignment(prop)) continue;
|
|
18447
|
+
const name2 = prop.name;
|
|
18448
|
+
const propKey = ts17.isIdentifier(name2) || ts17.isStringLiteral(name2) || ts17.isNoSubstitutionTemplateLiteral(name2) ? name2.text : null;
|
|
18449
|
+
if (propKey !== key) continue;
|
|
18450
|
+
let v = prop.initializer;
|
|
18451
|
+
while (ts17.isParenthesizedExpression(v)) v = v.expression;
|
|
18452
|
+
if (ts17.isNumericLiteral(v)) return { kind: "number", text: v.text };
|
|
18453
|
+
if (ts17.isStringLiteral(v) || ts17.isNoSubstitutionTemplateLiteral(v)) {
|
|
18454
|
+
return { kind: "string", text: v.text };
|
|
18455
|
+
}
|
|
18456
|
+
return null;
|
|
18457
|
+
}
|
|
18458
|
+
return null;
|
|
18459
|
+
}
|
|
18460
|
+
function evalStringArrayJoin(source) {
|
|
18461
|
+
const sf = ts17.createSourceFile(
|
|
18462
|
+
"__join.ts",
|
|
18463
|
+
`const __x = (${source});`,
|
|
18464
|
+
ts17.ScriptTarget.Latest,
|
|
18465
|
+
/*setParentNodes*/
|
|
18466
|
+
false
|
|
18467
|
+
);
|
|
18468
|
+
const stmt = sf.statements[0];
|
|
18469
|
+
if (!stmt || !ts17.isVariableStatement(stmt)) return null;
|
|
18470
|
+
let node = stmt.declarationList.declarations[0]?.initializer;
|
|
18471
|
+
while (node && ts17.isParenthesizedExpression(node)) node = node.expression;
|
|
18472
|
+
if (!node || !ts17.isCallExpression(node)) return null;
|
|
18473
|
+
const callee = node.expression;
|
|
18474
|
+
if (!ts17.isPropertyAccessExpression(callee)) return null;
|
|
18475
|
+
if (callee.name.text !== "join") return null;
|
|
18476
|
+
let recv = callee.expression;
|
|
18477
|
+
while (ts17.isParenthesizedExpression(recv)) recv = recv.expression;
|
|
18478
|
+
if (!ts17.isArrayLiteralExpression(recv)) return null;
|
|
18479
|
+
const parts = [];
|
|
18480
|
+
for (const el of recv.elements) {
|
|
18481
|
+
if (ts17.isStringLiteral(el) || ts17.isNoSubstitutionTemplateLiteral(el)) {
|
|
18482
|
+
parts.push(el.text);
|
|
18483
|
+
} else {
|
|
18484
|
+
return null;
|
|
18485
|
+
}
|
|
18486
|
+
}
|
|
18487
|
+
let sep = ",";
|
|
18488
|
+
if (node.arguments.length >= 1) {
|
|
18489
|
+
const arg = node.arguments[0];
|
|
18490
|
+
if (ts17.isStringLiteral(arg) || ts17.isNoSubstitutionTemplateLiteral(arg)) sep = arg.text;
|
|
18491
|
+
else return null;
|
|
18492
|
+
}
|
|
18493
|
+
return parts.join(sep);
|
|
18494
|
+
}
|
|
18495
|
+
function parseRecordIndexAccess(val, localConstants, propsParams, resolveKey) {
|
|
18496
|
+
if (!ts17.isElementAccessExpression(val)) return null;
|
|
18497
|
+
const obj = val.expression;
|
|
18498
|
+
const arg = val.argumentExpression;
|
|
18499
|
+
if (!ts17.isIdentifier(obj) || !ts17.isIdentifier(arg)) return null;
|
|
18500
|
+
let indexPropName;
|
|
18501
|
+
let defaultKey;
|
|
18502
|
+
const resolved = resolveKey?.(arg.text);
|
|
18503
|
+
if (resolved) {
|
|
18504
|
+
indexPropName = resolved.propName;
|
|
18505
|
+
defaultKey = resolved.defaultLiteral;
|
|
18506
|
+
} else if (propsParams.some((p) => p.name === arg.text)) {
|
|
18507
|
+
indexPropName = arg.text;
|
|
18508
|
+
} else {
|
|
18509
|
+
return null;
|
|
18510
|
+
}
|
|
18511
|
+
const constInfo = localConstants.find((c) => c.name === obj.text && c.isModule);
|
|
18512
|
+
if (constInfo?.value === void 0) return null;
|
|
18513
|
+
const sf = ts17.createSourceFile(
|
|
18514
|
+
"__rec.ts",
|
|
18515
|
+
`(${constInfo.value})`,
|
|
18516
|
+
ts17.ScriptTarget.Latest,
|
|
18517
|
+
/* setParentNodes */
|
|
18518
|
+
true
|
|
18519
|
+
);
|
|
18520
|
+
if (sf.statements.length !== 1) return null;
|
|
18521
|
+
const stmt = sf.statements[0];
|
|
18522
|
+
if (!ts17.isExpressionStatement(stmt)) return null;
|
|
18523
|
+
let parsed = stmt.expression;
|
|
18524
|
+
while (ts17.isParenthesizedExpression(parsed)) parsed = parsed.expression;
|
|
18525
|
+
if (!ts17.isObjectLiteralExpression(parsed)) return null;
|
|
18526
|
+
const entries2 = [];
|
|
18527
|
+
for (const prop of parsed.properties) {
|
|
18528
|
+
if (!ts17.isPropertyAssignment(prop)) return null;
|
|
18529
|
+
let key;
|
|
18530
|
+
if (ts17.isIdentifier(prop.name)) {
|
|
18531
|
+
key = prop.name.text;
|
|
18532
|
+
} else if (ts17.isStringLiteral(prop.name) || ts17.isNoSubstitutionTemplateLiteral(prop.name)) {
|
|
18533
|
+
key = prop.name.text;
|
|
18534
|
+
} else {
|
|
18535
|
+
return null;
|
|
18536
|
+
}
|
|
18537
|
+
let v = prop.initializer;
|
|
18538
|
+
while (ts17.isParenthesizedExpression(v)) v = v.expression;
|
|
18539
|
+
if (ts17.isNumericLiteral(v)) {
|
|
18540
|
+
entries2.push({ key, value: { kind: "number", text: v.text } });
|
|
18541
|
+
} else if (ts17.isStringLiteral(v) || ts17.isNoSubstitutionTemplateLiteral(v)) {
|
|
18542
|
+
entries2.push({ key, value: { kind: "string", text: v.text } });
|
|
18543
|
+
} else {
|
|
18544
|
+
return null;
|
|
18545
|
+
}
|
|
18546
|
+
}
|
|
18547
|
+
return { indexPropName, entries: entries2, defaultKey };
|
|
18548
|
+
}
|
|
18549
|
+
var init_augment_inherited_props = __esm({
|
|
18550
|
+
"../jsx/src/augment-inherited-props.ts"() {
|
|
18551
|
+
"use strict";
|
|
18552
|
+
init_html_constants();
|
|
18553
|
+
}
|
|
18554
|
+
});
|
|
18555
|
+
|
|
18556
|
+
// ../jsx/src/ssr-seed-plan.ts
|
|
18557
|
+
function classify2(name2, origin, expr, parsed, available) {
|
|
18558
|
+
if (!isSupported(parsed).supported) return { kind: "opaque", name: name2, origin };
|
|
18559
|
+
const frees = freeIdentifiers(parsed);
|
|
18560
|
+
if (frees === null) return { kind: "opaque", name: name2, origin };
|
|
18561
|
+
for (const free of frees) {
|
|
18562
|
+
if (!available.has(free)) return { kind: "opaque", name: name2, origin };
|
|
18563
|
+
}
|
|
18564
|
+
return { kind: "derived", name: name2, origin, expr, parsed, frees: [...frees] };
|
|
18565
|
+
}
|
|
18566
|
+
function computeSsrSeedPlan(metadata) {
|
|
18567
|
+
const baseScope = metadata.propsParams.map((p) => p.name);
|
|
18568
|
+
if (metadata.propsObjectName) baseScope.push(metadata.propsObjectName);
|
|
18569
|
+
for (const name2 of collectModuleStringConsts(metadata.localConstants).keys()) {
|
|
18570
|
+
baseScope.push(name2);
|
|
18571
|
+
}
|
|
18572
|
+
const available = new Set(baseScope);
|
|
18573
|
+
const steps = [];
|
|
18574
|
+
for (const signal2 of metadata.signals) {
|
|
18575
|
+
if (signal2.envReader) {
|
|
18576
|
+
const reader = envSignalReaderFor(signal2.envReader);
|
|
18577
|
+
if (reader) {
|
|
18578
|
+
steps.push({ kind: "env-reader", name: signal2.getter, reader });
|
|
18579
|
+
available.add(signal2.getter);
|
|
18580
|
+
continue;
|
|
18581
|
+
}
|
|
18582
|
+
}
|
|
18583
|
+
const expr = signal2.initialValue.trim();
|
|
18584
|
+
steps.push(
|
|
18585
|
+
expr === "" ? { kind: "opaque", name: signal2.getter, origin: "signal" } : classify2(signal2.getter, "signal", expr, parseExpression(expr), available)
|
|
18586
|
+
);
|
|
18587
|
+
available.add(signal2.getter);
|
|
18588
|
+
}
|
|
18589
|
+
for (const memo of metadata.memos) {
|
|
18590
|
+
const body2 = extractArrowBodyExpression(memo.computation);
|
|
18591
|
+
const expr = body2?.trim() ?? "";
|
|
18592
|
+
steps.push(
|
|
18593
|
+
expr === "" ? { kind: "opaque", name: memo.name, origin: "memo" } : classify2(memo.name, "memo", expr, memo.parsed ?? parseExpression(expr), available)
|
|
18594
|
+
);
|
|
18595
|
+
available.add(memo.name);
|
|
18596
|
+
}
|
|
18597
|
+
return { baseScope, steps };
|
|
18598
|
+
}
|
|
18599
|
+
var init_ssr_seed_plan = __esm({
|
|
18600
|
+
"../jsx/src/ssr-seed-plan.ts"() {
|
|
18601
|
+
"use strict";
|
|
18602
|
+
init_augment_inherited_props();
|
|
18603
|
+
init_env_signal();
|
|
18604
|
+
init_expression_parser();
|
|
18605
|
+
}
|
|
18606
|
+
});
|
|
18607
|
+
|
|
18608
|
+
// ../jsx/src/compiler.ts
|
|
18609
|
+
function mergeTemplateImports(lines) {
|
|
18610
|
+
const result2 = [];
|
|
18611
|
+
const valueIdx = /* @__PURE__ */ new Map();
|
|
18612
|
+
const valueNames = /* @__PURE__ */ new Map();
|
|
18613
|
+
const typeIdx = /* @__PURE__ */ new Map();
|
|
18614
|
+
const typeNames = /* @__PURE__ */ new Map();
|
|
18615
|
+
const seenOther = /* @__PURE__ */ new Set();
|
|
18616
|
+
const fold = (src, rawNames, idx, names, render) => {
|
|
18617
|
+
if (!idx.has(src)) {
|
|
18618
|
+
idx.set(src, result2.length);
|
|
18619
|
+
names.set(src, /* @__PURE__ */ new Set());
|
|
18620
|
+
result2.push("");
|
|
18621
|
+
}
|
|
18622
|
+
const set = names.get(src);
|
|
18623
|
+
for (const n of rawNames.split(",").map((s) => s.trim()).filter(Boolean)) set.add(n);
|
|
18624
|
+
result2[idx.get(src)] = render(src, set);
|
|
18625
|
+
};
|
|
18626
|
+
for (const raw of lines) {
|
|
18627
|
+
const line = raw.trim();
|
|
18628
|
+
if (!line) continue;
|
|
18629
|
+
const typeMatch = line.match(/^import\s+type\s*\{([^}]+)\}\s*from\s*['"]([^'"]+)['"]\s*;?$/);
|
|
18630
|
+
const valueMatch = line.match(/^import\s*\{([^}]+)\}\s*from\s*['"]([^'"]+)['"]\s*;?$/);
|
|
18631
|
+
if (valueMatch) {
|
|
18632
|
+
fold(valueMatch[2], valueMatch[1], valueIdx, valueNames, (s, n) => `import { ${[...n].join(", ")} } from '${s}'`);
|
|
18633
|
+
} else if (typeMatch) {
|
|
18634
|
+
fold(typeMatch[2], typeMatch[1], typeIdx, typeNames, (s, n) => `import type { ${[...n].join(", ")} } from '${s}'`);
|
|
18635
|
+
} else if (!seenOther.has(line)) {
|
|
18636
|
+
seenOther.add(line);
|
|
18637
|
+
result2.push(line);
|
|
18638
|
+
}
|
|
18639
|
+
}
|
|
18640
|
+
return result2.filter(Boolean).join("\n");
|
|
18641
|
+
}
|
|
18642
|
+
function compileMultipleComponents(source, filePath, componentNames, options2) {
|
|
18643
|
+
const files2 = [];
|
|
18644
|
+
const errors = [];
|
|
18645
|
+
const adapter = options2.adapter;
|
|
18646
|
+
const entries2 = [];
|
|
18647
|
+
const program = options2.program ?? (needsTypeBasedDetection(source) ? createProgramForFile(source, filePath)?.program : void 0);
|
|
18648
|
+
for (const componentName of componentNames) {
|
|
18649
|
+
const ctx2 = analyzeComponent(source, filePath, componentName, program);
|
|
18650
|
+
if (!ctx2.jsxReturn) {
|
|
18651
|
+
errors.push(...ctx2.errors);
|
|
18652
|
+
continue;
|
|
18653
|
+
}
|
|
18654
|
+
const ir = jsxToIR(ctx2);
|
|
18655
|
+
errors.push(...ctx2.errors);
|
|
18656
|
+
if (!ir) continue;
|
|
18657
|
+
const componentIR = {
|
|
18658
|
+
version: "0.1",
|
|
18659
|
+
metadata: buildMetadata(ctx2),
|
|
18660
|
+
root: ir,
|
|
18661
|
+
errors: []
|
|
18662
|
+
};
|
|
18663
|
+
componentIR.metadata.clientAnalysis = analyzeClientNeeds(componentIR);
|
|
18664
|
+
if (options2.cssLayerPrefix) {
|
|
18665
|
+
applyCssLayerPrefix(componentIR, options2.cssLayerPrefix);
|
|
18666
|
+
}
|
|
18667
|
+
entries2.push({ componentIR, ctx: ctx2 });
|
|
18668
|
+
}
|
|
18669
|
+
if (options2.outputIR) {
|
|
18670
|
+
for (const { componentIR } of entries2) {
|
|
18671
|
+
const componentName = componentIR.metadata.componentName;
|
|
18672
|
+
files2.push({
|
|
18673
|
+
path: filePath.replace(/\.tsx?$/, `.${componentName}.ir.json`),
|
|
18674
|
+
content: JSON.stringify(componentIR, null, 2),
|
|
18675
|
+
type: "ir"
|
|
18676
|
+
});
|
|
18677
|
+
}
|
|
18678
|
+
}
|
|
18679
|
+
const allOutputs = [];
|
|
18680
|
+
const defaultExportName = entries2.find((e) => e.componentIR.metadata.hasDefaultExport)?.componentIR.metadata.componentName;
|
|
18681
|
+
const fileWideInlineExported = /* @__PURE__ */ new Set();
|
|
18682
|
+
for (const { componentIR } of entries2) {
|
|
18683
|
+
for (const name2 of collectInlineExportedNames(componentIR)) {
|
|
18684
|
+
fileWideInlineExported.add(name2);
|
|
18685
|
+
}
|
|
18686
|
+
}
|
|
18687
|
+
const moduleConstantsSet = /* @__PURE__ */ new Set();
|
|
18688
|
+
const moduleConstantsOrdered = [];
|
|
18689
|
+
const fileScope = computeFileScope(filePath);
|
|
18690
|
+
const nonExportedSiblings = /* @__PURE__ */ new Set();
|
|
18691
|
+
for (const { componentIR } of entries2) {
|
|
18692
|
+
if (!componentIR.metadata.isExported) {
|
|
18693
|
+
nonExportedSiblings.add(componentIR.metadata.componentName);
|
|
18694
|
+
}
|
|
18695
|
+
}
|
|
18696
|
+
setActiveComponentScope({ fileScope, nonExportedSiblings });
|
|
18697
|
+
const multiAdapterCaps = {
|
|
18698
|
+
templatePrimitives: options2.adapter.templatePrimitives,
|
|
18699
|
+
acceptsTemplateCall: options2.adapter.acceptsTemplateCall
|
|
18700
|
+
};
|
|
18701
|
+
try {
|
|
18702
|
+
for (const { componentIR } of entries2) {
|
|
18703
|
+
const scriptBaseName = options2.scriptBaseName ?? (!componentIR.metadata.hasDefaultExport && defaultExportName ? defaultExportName : void 0);
|
|
18704
|
+
const adapterOutput = adapter.generate(componentIR, {
|
|
18705
|
+
scriptBaseName,
|
|
18706
|
+
siblingTemplatesRegistered: options2.siblingTemplatesRegistered,
|
|
18216
18707
|
rewriteRelativeImport: options2.rewriteRelativeImport
|
|
18217
18708
|
});
|
|
18218
18709
|
const moduleExports = generateModuleExports(
|
|
@@ -18417,7 +18908,7 @@ function compileMultipleComponents(source, filePath, componentNames, options2) {
|
|
|
18417
18908
|
return { files: files2, errors };
|
|
18418
18909
|
}
|
|
18419
18910
|
function buildMetadata(ctx2) {
|
|
18420
|
-
|
|
18911
|
+
const metadata = {
|
|
18421
18912
|
componentName: ctx2.componentName || "Unknown",
|
|
18422
18913
|
hasDefaultExport: ctx2.hasDefaultExport,
|
|
18423
18914
|
isExported: ctx2.isExported,
|
|
@@ -18447,6 +18938,8 @@ function buildMetadata(ctx2) {
|
|
|
18447
18938
|
localFunctions: ctx2.localFunctions,
|
|
18448
18939
|
localConstants: ctx2.localConstants
|
|
18449
18940
|
};
|
|
18941
|
+
metadata.ssrSeedPlan = computeSsrSeedPlan(metadata);
|
|
18942
|
+
return metadata;
|
|
18450
18943
|
}
|
|
18451
18944
|
function compileJSX(source, filePath, options2) {
|
|
18452
18945
|
const files2 = [];
|
|
@@ -18628,11 +19121,12 @@ var init_compiler = __esm({
|
|
|
18628
19121
|
init_css_layer_prefixer();
|
|
18629
19122
|
init_preprocess_inline_jsx_callbacks();
|
|
18630
19123
|
init_ssr_defaults();
|
|
19124
|
+
init_ssr_seed_plan();
|
|
18631
19125
|
}
|
|
18632
19126
|
});
|
|
18633
19127
|
|
|
18634
19128
|
// ../jsx/src/shared-program.ts
|
|
18635
|
-
import
|
|
19129
|
+
import ts18 from "typescript";
|
|
18636
19130
|
import path5 from "node:path";
|
|
18637
19131
|
function commonParent(paths) {
|
|
18638
19132
|
if (paths.length === 0) return process.cwd();
|
|
@@ -18650,10 +19144,10 @@ function commonParent(paths) {
|
|
|
18650
19144
|
function createProgramForCorpus(files2, options2 = {}) {
|
|
18651
19145
|
const baseUrl = options2.baseUrl ?? commonParent(files2);
|
|
18652
19146
|
const compilerOptions = {
|
|
18653
|
-
target:
|
|
18654
|
-
module:
|
|
18655
|
-
moduleResolution:
|
|
18656
|
-
jsx:
|
|
19147
|
+
target: ts18.ScriptTarget.Latest,
|
|
19148
|
+
module: ts18.ModuleKind.ESNext,
|
|
19149
|
+
moduleResolution: ts18.ModuleResolutionKind.Bundler,
|
|
19150
|
+
jsx: ts18.JsxEmit.ReactJSX,
|
|
18657
19151
|
strict: true,
|
|
18658
19152
|
skipLibCheck: true,
|
|
18659
19153
|
noEmit: true,
|
|
@@ -18663,7 +19157,7 @@ function createProgramForCorpus(files2, options2 = {}) {
|
|
|
18663
19157
|
...options2.compilerOptions
|
|
18664
19158
|
};
|
|
18665
19159
|
const absolute = files2.map((f) => path5.resolve(f));
|
|
18666
|
-
return
|
|
19160
|
+
return ts18.createProgram(absolute, compilerOptions, void 0, options2.oldProgram);
|
|
18667
19161
|
}
|
|
18668
19162
|
var init_shared_program = __esm({
|
|
18669
19163
|
"../jsx/src/shared-program.ts"() {
|
|
@@ -19415,7 +19909,7 @@ var init_attr_value_emitter = __esm({
|
|
|
19415
19909
|
});
|
|
19416
19910
|
|
|
19417
19911
|
// ../jsx/src/combine-client-js.ts
|
|
19418
|
-
import
|
|
19912
|
+
import ts19 from "typescript";
|
|
19419
19913
|
function combineParentChildClientJs(files2) {
|
|
19420
19914
|
const result2 = /* @__PURE__ */ new Map();
|
|
19421
19915
|
const lookup = /* @__PURE__ */ new Map();
|
|
@@ -19472,17 +19966,17 @@ function combineParentChildClientJs(files2) {
|
|
|
19472
19966
|
return result2;
|
|
19473
19967
|
}
|
|
19474
19968
|
function parseAndMerge(content2, importsBySource, otherImports, codeSections) {
|
|
19475
|
-
const sourceFile =
|
|
19969
|
+
const sourceFile = ts19.createSourceFile(
|
|
19476
19970
|
"combine.js",
|
|
19477
19971
|
content2,
|
|
19478
|
-
|
|
19972
|
+
ts19.ScriptTarget.Latest,
|
|
19479
19973
|
/*setParentNodes*/
|
|
19480
19974
|
false,
|
|
19481
|
-
|
|
19975
|
+
ts19.ScriptKind.JS
|
|
19482
19976
|
);
|
|
19483
19977
|
const importSpans = [];
|
|
19484
19978
|
for (const stmt of sourceFile.statements) {
|
|
19485
|
-
if (!
|
|
19979
|
+
if (!ts19.isImportDeclaration(stmt)) continue;
|
|
19486
19980
|
const start2 = stmt.getStart(sourceFile);
|
|
19487
19981
|
const end2 = stmt.getEnd();
|
|
19488
19982
|
importSpans.push([start2, end2]);
|
|
@@ -19490,8 +19984,8 @@ function parseAndMerge(content2, importsBySource, otherImports, codeSections) {
|
|
|
19490
19984
|
if (stmtText.includes("@bf-child:")) continue;
|
|
19491
19985
|
const clause = stmt.importClause;
|
|
19492
19986
|
const bindings = clause?.namedBindings;
|
|
19493
|
-
const specifier =
|
|
19494
|
-
if (clause && !clause.name && bindings &&
|
|
19987
|
+
const specifier = ts19.isStringLiteral(stmt.moduleSpecifier) ? stmt.moduleSpecifier.text : "";
|
|
19988
|
+
if (clause && !clause.name && bindings && ts19.isNamedImports(bindings)) {
|
|
19495
19989
|
if (!importsBySource.has(specifier)) {
|
|
19496
19990
|
importsBySource.set(specifier, /* @__PURE__ */ new Set());
|
|
19497
19991
|
}
|
|
@@ -19677,7 +20171,7 @@ var init_loop_destructure = __esm({
|
|
|
19677
20171
|
});
|
|
19678
20172
|
|
|
19679
20173
|
// ../jsx/src/debug.ts
|
|
19680
|
-
import
|
|
20174
|
+
import ts20 from "typescript";
|
|
19681
20175
|
function buildComponentGraph(source, filePath, componentName) {
|
|
19682
20176
|
const ctx2 = analyzeComponent(source, filePath, componentName);
|
|
19683
20177
|
if (!ctx2.jsxReturn) {
|
|
@@ -20889,18 +21383,18 @@ function truncateExpr(expr, max = 40) {
|
|
|
20889
21383
|
function exprReadsPropMember(expr, propsObjectName) {
|
|
20890
21384
|
let sf;
|
|
20891
21385
|
try {
|
|
20892
|
-
sf =
|
|
21386
|
+
sf = ts20.createSourceFile("__attr.tsx", `(${expr})`, ts20.ScriptTarget.Latest, true, ts20.ScriptKind.TSX);
|
|
20893
21387
|
} catch {
|
|
20894
21388
|
return false;
|
|
20895
21389
|
}
|
|
20896
21390
|
let found = false;
|
|
20897
21391
|
const visit3 = (n) => {
|
|
20898
21392
|
if (found) return;
|
|
20899
|
-
if (
|
|
21393
|
+
if (ts20.isPropertyAccessExpression(n) && ts20.isIdentifier(n.expression) && n.expression.text === propsObjectName && n.name.text !== "children") {
|
|
20900
21394
|
found = true;
|
|
20901
21395
|
return;
|
|
20902
21396
|
}
|
|
20903
|
-
|
|
21397
|
+
ts20.forEachChild(n, visit3);
|
|
20904
21398
|
};
|
|
20905
21399
|
visit3(sf);
|
|
20906
21400
|
return found;
|
|
@@ -20977,7 +21471,7 @@ var init_debug = __esm({
|
|
|
20977
21471
|
});
|
|
20978
21472
|
|
|
20979
21473
|
// ../jsx/src/profiler.ts
|
|
20980
|
-
import
|
|
21474
|
+
import ts21 from "typescript";
|
|
20981
21475
|
function buildStaticBudget(source, filePath, componentName, options2 = {}) {
|
|
20982
21476
|
const threshold = options2.fanOutThreshold ?? DEFAULT_FANOUT_THRESHOLD;
|
|
20983
21477
|
const program = createProgramForFile(source, filePath)?.program;
|
|
@@ -21232,14 +21726,14 @@ function joinProfilerEvents(events, index) {
|
|
|
21232
21726
|
return { joined, unattributed, diagnostics };
|
|
21233
21727
|
}
|
|
21234
21728
|
function findUninstrumentedEffects(source, filePath, instrumentedLines) {
|
|
21235
|
-
const sf =
|
|
21729
|
+
const sf = ts21.createSourceFile(filePath, source, ts21.ScriptTarget.Latest, true, ts21.ScriptKind.TSX);
|
|
21236
21730
|
const out = [];
|
|
21237
21731
|
const visit3 = (node) => {
|
|
21238
|
-
if (
|
|
21732
|
+
if (ts21.isCallExpression(node) && ts21.isIdentifier(node.expression) && node.expression.text === "createEffect") {
|
|
21239
21733
|
const line = sf.getLineAndCharacterOfPosition(node.getStart(sf)).line + 1;
|
|
21240
21734
|
if (!instrumentedLines.has(line)) out.push({ file: filePath, line });
|
|
21241
21735
|
}
|
|
21242
|
-
|
|
21736
|
+
ts21.forEachChild(node, visit3);
|
|
21243
21737
|
};
|
|
21244
21738
|
visit3(sf);
|
|
21245
21739
|
out.sort((a, b) => a.line - b.line);
|
|
@@ -21525,19 +22019,19 @@ function assessBatchSafety(args2) {
|
|
|
21525
22019
|
const signalGetters = new Set(args2.graph.signals.map((s) => s.name));
|
|
21526
22020
|
let sf;
|
|
21527
22021
|
try {
|
|
21528
|
-
sf =
|
|
22022
|
+
sf = ts21.createSourceFile("__h.ts", `const __h = ${args2.handler}`, ts21.ScriptTarget.Latest, true);
|
|
21529
22023
|
} catch {
|
|
21530
22024
|
return "unverified";
|
|
21531
22025
|
}
|
|
21532
22026
|
const calls = [];
|
|
21533
22027
|
const visit3 = (node) => {
|
|
21534
|
-
if (
|
|
22028
|
+
if (ts21.isCallExpression(node) && ts21.isIdentifier(node.expression)) {
|
|
21535
22029
|
const name2 = node.expression.text;
|
|
21536
22030
|
if (setters.has(name2)) calls.push({ pos: node.getStart(sf), kind: "write" });
|
|
21537
22031
|
else if (D.has(name2) && node.arguments.length === 0) calls.push({ pos: node.getStart(sf), kind: "memoRead" });
|
|
21538
22032
|
else if (!signalGetters.has(name2) && !memoNames.has(name2)) calls.push({ pos: node.getStart(sf), kind: "risky" });
|
|
21539
22033
|
}
|
|
21540
|
-
|
|
22034
|
+
ts21.forEachChild(node, visit3);
|
|
21541
22035
|
};
|
|
21542
22036
|
visit3(sf);
|
|
21543
22037
|
calls.sort((a, b) => a.pos - b.pos);
|
|
@@ -22184,312 +22678,6 @@ var init_debug_profile = __esm({
|
|
|
22184
22678
|
}
|
|
22185
22679
|
});
|
|
22186
22680
|
|
|
22187
|
-
// ../jsx/src/augment-inherited-props.ts
|
|
22188
|
-
import ts21 from "typescript";
|
|
22189
|
-
function collectContextConsumers(metadata) {
|
|
22190
|
-
const constants = metadata.localConstants ?? [];
|
|
22191
|
-
const contextDefaults = /* @__PURE__ */ new Map();
|
|
22192
|
-
for (const c of constants) {
|
|
22193
|
-
if (c.systemConstructKind !== "createContext" || c.value === void 0) continue;
|
|
22194
|
-
contextDefaults.set(c.name, parseCreateContextDefault(c.value));
|
|
22195
|
-
}
|
|
22196
|
-
if (contextDefaults.size === 0) return [];
|
|
22197
|
-
const consumers = [];
|
|
22198
|
-
for (const c of constants) {
|
|
22199
|
-
if (c.value === void 0) continue;
|
|
22200
|
-
const ctxName = parseUseContextArg(c.value);
|
|
22201
|
-
if (ctxName === null || !contextDefaults.has(ctxName)) continue;
|
|
22202
|
-
consumers.push({
|
|
22203
|
-
localName: c.name,
|
|
22204
|
-
contextName: ctxName,
|
|
22205
|
-
defaultValue: contextDefaults.get(ctxName) ?? null
|
|
22206
|
-
});
|
|
22207
|
-
}
|
|
22208
|
-
return consumers;
|
|
22209
|
-
}
|
|
22210
|
-
function parseUseContextArg(source) {
|
|
22211
|
-
const expr = parseSingleExpression(source);
|
|
22212
|
-
if (!expr || !ts21.isCallExpression(expr)) return null;
|
|
22213
|
-
if (!ts21.isIdentifier(expr.expression) || expr.expression.text !== "useContext") return null;
|
|
22214
|
-
if (expr.arguments.length !== 1) return null;
|
|
22215
|
-
const arg = expr.arguments[0];
|
|
22216
|
-
return ts21.isIdentifier(arg) ? arg.text : null;
|
|
22217
|
-
}
|
|
22218
|
-
function parseCreateContextDefault(source) {
|
|
22219
|
-
const expr = parseSingleExpression(source);
|
|
22220
|
-
if (!expr || !ts21.isCallExpression(expr)) return null;
|
|
22221
|
-
if (expr.arguments.length === 0) return null;
|
|
22222
|
-
const arg = expr.arguments[0];
|
|
22223
|
-
if (ts21.isStringLiteral(arg) || ts21.isNoSubstitutionTemplateLiteral(arg)) return arg.text;
|
|
22224
|
-
if (ts21.isNumericLiteral(arg)) return Number(arg.text);
|
|
22225
|
-
if (arg.kind === ts21.SyntaxKind.TrueKeyword) return true;
|
|
22226
|
-
if (arg.kind === ts21.SyntaxKind.FalseKeyword) return false;
|
|
22227
|
-
return null;
|
|
22228
|
-
}
|
|
22229
|
-
function parseSingleExpression(source) {
|
|
22230
|
-
const sf = ts21.createSourceFile("__ctx.ts", `(${source})`, ts21.ScriptTarget.Latest, false);
|
|
22231
|
-
const stmt = sf.statements[0];
|
|
22232
|
-
if (!stmt || !ts21.isExpressionStatement(stmt)) return null;
|
|
22233
|
-
let e = stmt.expression;
|
|
22234
|
-
while (ts21.isParenthesizedExpression(e)) e = e.expression;
|
|
22235
|
-
return e;
|
|
22236
|
-
}
|
|
22237
|
-
function augmentInheritedPropAccesses(ir) {
|
|
22238
|
-
const propsObj = ir.metadata.propsObjectName;
|
|
22239
|
-
if (!propsObj) return;
|
|
22240
|
-
const existing = new Set(ir.metadata.propsParams.map((p) => p.name));
|
|
22241
|
-
const bareRefProps = /* @__PURE__ */ new Set();
|
|
22242
|
-
const booleanAttrProps = /* @__PURE__ */ new Set();
|
|
22243
|
-
const accessed = /* @__PURE__ */ new Set();
|
|
22244
|
-
const accessRe = new RegExp(`(?:^|[^\\w$.])${propsObj}\\.([A-Za-z_$][\\w$]*)`, "g");
|
|
22245
|
-
const scan = (s) => {
|
|
22246
|
-
if (!s) return;
|
|
22247
|
-
for (const m of s.matchAll(accessRe)) accessed.add(m[1]);
|
|
22248
|
-
};
|
|
22249
|
-
for (const memo of ir.metadata.memos) scan(memo.computation);
|
|
22250
|
-
for (const signal2 of ir.metadata.signals) scan(signal2.initialValue);
|
|
22251
|
-
for (const stmt of ir.metadata.initStatements ?? []) scan(stmt.body);
|
|
22252
|
-
for (const eff of ir.metadata.effects ?? []) scan(eff.body);
|
|
22253
|
-
for (const c of ir.metadata.localConstants ?? []) {
|
|
22254
|
-
if (c.isModule) continue;
|
|
22255
|
-
scan(c.value);
|
|
22256
|
-
}
|
|
22257
|
-
const walk = (node) => {
|
|
22258
|
-
if (!node) return;
|
|
22259
|
-
const el = node;
|
|
22260
|
-
for (const attr of el.attrs ?? []) {
|
|
22261
|
-
const v = attr.value;
|
|
22262
|
-
if (v?.parts) {
|
|
22263
|
-
for (const part of v.parts) {
|
|
22264
|
-
if (part.type === "string") scan(part.value);
|
|
22265
|
-
else if (part.type === "ternary") {
|
|
22266
|
-
scan(part.condition);
|
|
22267
|
-
scan(part.whenTrue);
|
|
22268
|
-
scan(part.whenFalse);
|
|
22269
|
-
} else if (part.type === "lookup") scan(part.key);
|
|
22270
|
-
}
|
|
22271
|
-
}
|
|
22272
|
-
if (v?.kind === "expression" && typeof v.expr === "string") {
|
|
22273
|
-
scan(v.expr);
|
|
22274
|
-
const expr = v.expr.trim();
|
|
22275
|
-
const prefix2 = `${propsObj}.`;
|
|
22276
|
-
if (isBooleanAttr(attr.name) || v.presenceOrUndefined) {
|
|
22277
|
-
const m = expr.match(new RegExp(`^${propsObj}\\.([A-Za-z_$][\\w$]*)`));
|
|
22278
|
-
if (m) booleanAttrProps.add(m[1]);
|
|
22279
|
-
} else if (expr.startsWith(prefix2)) {
|
|
22280
|
-
const rest2 = expr.slice(prefix2.length);
|
|
22281
|
-
if (/^[A-Za-z_$][\w$]*$/.test(rest2)) bareRefProps.add(rest2);
|
|
22282
|
-
}
|
|
22283
|
-
}
|
|
22284
|
-
}
|
|
22285
|
-
for (const child of el.children ?? []) {
|
|
22286
|
-
const c = child;
|
|
22287
|
-
walk(c.element ?? child);
|
|
22288
|
-
}
|
|
22289
|
-
const branchy = node;
|
|
22290
|
-
walk(branchy.whenTrue);
|
|
22291
|
-
walk(branchy.whenFalse);
|
|
22292
|
-
walk(branchy.consequent);
|
|
22293
|
-
walk(branchy.alternate);
|
|
22294
|
-
};
|
|
22295
|
-
walk(ir.root);
|
|
22296
|
-
for (const name2 of accessed) {
|
|
22297
|
-
if (existing.has(name2)) continue;
|
|
22298
|
-
let raw;
|
|
22299
|
-
if (booleanAttrProps.has(name2)) raw = "boolean";
|
|
22300
|
-
else if (bareRefProps.has(name2)) raw = "unknown";
|
|
22301
|
-
else raw = "string";
|
|
22302
|
-
const type2 = raw === "boolean" ? { kind: "primitive", raw: "boolean", primitive: "boolean" } : raw === "string" ? { kind: "primitive", raw: "string", primitive: "string" } : { kind: "unknown", raw: "unknown" };
|
|
22303
|
-
ir.metadata.propsParams.push({ name: name2, type: type2, optional: true });
|
|
22304
|
-
existing.add(name2);
|
|
22305
|
-
}
|
|
22306
|
-
}
|
|
22307
|
-
function parseStaticStringConst(source) {
|
|
22308
|
-
const sf = ts21.createSourceFile(
|
|
22309
|
-
"__const.ts",
|
|
22310
|
-
`const __x = (${source});`,
|
|
22311
|
-
ts21.ScriptTarget.Latest,
|
|
22312
|
-
/*setParentNodes*/
|
|
22313
|
-
false
|
|
22314
|
-
);
|
|
22315
|
-
const stmt = sf.statements[0];
|
|
22316
|
-
if (!stmt || !ts21.isVariableStatement(stmt)) return null;
|
|
22317
|
-
let init = stmt.declarationList.declarations[0]?.initializer;
|
|
22318
|
-
while (init && ts21.isParenthesizedExpression(init)) init = init.expression;
|
|
22319
|
-
if (!init) return null;
|
|
22320
|
-
if (ts21.isStringLiteral(init) || ts21.isNoSubstitutionTemplateLiteral(init)) {
|
|
22321
|
-
return init.text;
|
|
22322
|
-
}
|
|
22323
|
-
return evalStringArrayJoin(source);
|
|
22324
|
-
}
|
|
22325
|
-
function evalTemplateOfStringConsts(source, resolved) {
|
|
22326
|
-
const sf = ts21.createSourceFile(
|
|
22327
|
-
"__const.ts",
|
|
22328
|
-
`const __x = (${source});`,
|
|
22329
|
-
ts21.ScriptTarget.Latest,
|
|
22330
|
-
/*setParentNodes*/
|
|
22331
|
-
false
|
|
22332
|
-
);
|
|
22333
|
-
const stmt = sf.statements[0];
|
|
22334
|
-
if (!stmt || !ts21.isVariableStatement(stmt)) return null;
|
|
22335
|
-
let init = stmt.declarationList.declarations[0]?.initializer;
|
|
22336
|
-
while (init && ts21.isParenthesizedExpression(init)) init = init.expression;
|
|
22337
|
-
if (!init || !ts21.isTemplateExpression(init)) return null;
|
|
22338
|
-
let out = init.head.text;
|
|
22339
|
-
for (const span of init.templateSpans) {
|
|
22340
|
-
if (!ts21.isIdentifier(span.expression)) return null;
|
|
22341
|
-
const value2 = resolved.get(span.expression.text);
|
|
22342
|
-
if (value2 === void 0) return null;
|
|
22343
|
-
out += value2 + span.literal.text;
|
|
22344
|
-
}
|
|
22345
|
-
return out;
|
|
22346
|
-
}
|
|
22347
|
-
function collectModuleStringConsts(constants) {
|
|
22348
|
-
const map = /* @__PURE__ */ new Map();
|
|
22349
|
-
const candidates = (constants ?? []).filter(
|
|
22350
|
-
(c) => c.isModule && c.value !== void 0
|
|
22351
|
-
);
|
|
22352
|
-
let progressed = true;
|
|
22353
|
-
while (progressed) {
|
|
22354
|
-
progressed = false;
|
|
22355
|
-
for (const c of candidates) {
|
|
22356
|
-
if (map.has(c.name)) continue;
|
|
22357
|
-
const literal = parseStaticStringConst(c.value) ?? evalTemplateOfStringConsts(c.value, map);
|
|
22358
|
-
if (literal !== null) {
|
|
22359
|
-
map.set(c.name, literal);
|
|
22360
|
-
progressed = true;
|
|
22361
|
-
}
|
|
22362
|
-
}
|
|
22363
|
-
}
|
|
22364
|
-
return map;
|
|
22365
|
-
}
|
|
22366
|
-
function lookupStaticRecordLiteral(objectName, key, constants) {
|
|
22367
|
-
const constInfo = (constants ?? []).find((c) => c.name === objectName && c.isModule);
|
|
22368
|
-
if (constInfo?.value === void 0) return null;
|
|
22369
|
-
const sf = ts21.createSourceFile(
|
|
22370
|
-
"__rec.ts",
|
|
22371
|
-
`(${constInfo.value})`,
|
|
22372
|
-
ts21.ScriptTarget.Latest,
|
|
22373
|
-
/*setParentNodes*/
|
|
22374
|
-
true
|
|
22375
|
-
);
|
|
22376
|
-
if (sf.statements.length !== 1) return null;
|
|
22377
|
-
const stmt = sf.statements[0];
|
|
22378
|
-
if (!ts21.isExpressionStatement(stmt)) return null;
|
|
22379
|
-
let parsed = stmt.expression;
|
|
22380
|
-
while (ts21.isParenthesizedExpression(parsed)) parsed = parsed.expression;
|
|
22381
|
-
if (!ts21.isObjectLiteralExpression(parsed)) return null;
|
|
22382
|
-
for (const prop of parsed.properties) {
|
|
22383
|
-
if (!ts21.isPropertyAssignment(prop)) continue;
|
|
22384
|
-
const name2 = prop.name;
|
|
22385
|
-
const propKey = ts21.isIdentifier(name2) || ts21.isStringLiteral(name2) || ts21.isNoSubstitutionTemplateLiteral(name2) ? name2.text : null;
|
|
22386
|
-
if (propKey !== key) continue;
|
|
22387
|
-
let v = prop.initializer;
|
|
22388
|
-
while (ts21.isParenthesizedExpression(v)) v = v.expression;
|
|
22389
|
-
if (ts21.isNumericLiteral(v)) return { kind: "number", text: v.text };
|
|
22390
|
-
if (ts21.isStringLiteral(v) || ts21.isNoSubstitutionTemplateLiteral(v)) {
|
|
22391
|
-
return { kind: "string", text: v.text };
|
|
22392
|
-
}
|
|
22393
|
-
return null;
|
|
22394
|
-
}
|
|
22395
|
-
return null;
|
|
22396
|
-
}
|
|
22397
|
-
function evalStringArrayJoin(source) {
|
|
22398
|
-
const sf = ts21.createSourceFile(
|
|
22399
|
-
"__join.ts",
|
|
22400
|
-
`const __x = (${source});`,
|
|
22401
|
-
ts21.ScriptTarget.Latest,
|
|
22402
|
-
/*setParentNodes*/
|
|
22403
|
-
false
|
|
22404
|
-
);
|
|
22405
|
-
const stmt = sf.statements[0];
|
|
22406
|
-
if (!stmt || !ts21.isVariableStatement(stmt)) return null;
|
|
22407
|
-
let node = stmt.declarationList.declarations[0]?.initializer;
|
|
22408
|
-
while (node && ts21.isParenthesizedExpression(node)) node = node.expression;
|
|
22409
|
-
if (!node || !ts21.isCallExpression(node)) return null;
|
|
22410
|
-
const callee = node.expression;
|
|
22411
|
-
if (!ts21.isPropertyAccessExpression(callee)) return null;
|
|
22412
|
-
if (callee.name.text !== "join") return null;
|
|
22413
|
-
let recv = callee.expression;
|
|
22414
|
-
while (ts21.isParenthesizedExpression(recv)) recv = recv.expression;
|
|
22415
|
-
if (!ts21.isArrayLiteralExpression(recv)) return null;
|
|
22416
|
-
const parts = [];
|
|
22417
|
-
for (const el of recv.elements) {
|
|
22418
|
-
if (ts21.isStringLiteral(el) || ts21.isNoSubstitutionTemplateLiteral(el)) {
|
|
22419
|
-
parts.push(el.text);
|
|
22420
|
-
} else {
|
|
22421
|
-
return null;
|
|
22422
|
-
}
|
|
22423
|
-
}
|
|
22424
|
-
let sep = ",";
|
|
22425
|
-
if (node.arguments.length >= 1) {
|
|
22426
|
-
const arg = node.arguments[0];
|
|
22427
|
-
if (ts21.isStringLiteral(arg) || ts21.isNoSubstitutionTemplateLiteral(arg)) sep = arg.text;
|
|
22428
|
-
else return null;
|
|
22429
|
-
}
|
|
22430
|
-
return parts.join(sep);
|
|
22431
|
-
}
|
|
22432
|
-
function parseRecordIndexAccess(val, localConstants, propsParams, resolveKey) {
|
|
22433
|
-
if (!ts21.isElementAccessExpression(val)) return null;
|
|
22434
|
-
const obj = val.expression;
|
|
22435
|
-
const arg = val.argumentExpression;
|
|
22436
|
-
if (!ts21.isIdentifier(obj) || !ts21.isIdentifier(arg)) return null;
|
|
22437
|
-
let indexPropName;
|
|
22438
|
-
let defaultKey;
|
|
22439
|
-
const resolved = resolveKey?.(arg.text);
|
|
22440
|
-
if (resolved) {
|
|
22441
|
-
indexPropName = resolved.propName;
|
|
22442
|
-
defaultKey = resolved.defaultLiteral;
|
|
22443
|
-
} else if (propsParams.some((p) => p.name === arg.text)) {
|
|
22444
|
-
indexPropName = arg.text;
|
|
22445
|
-
} else {
|
|
22446
|
-
return null;
|
|
22447
|
-
}
|
|
22448
|
-
const constInfo = localConstants.find((c) => c.name === obj.text && c.isModule);
|
|
22449
|
-
if (constInfo?.value === void 0) return null;
|
|
22450
|
-
const sf = ts21.createSourceFile(
|
|
22451
|
-
"__rec.ts",
|
|
22452
|
-
`(${constInfo.value})`,
|
|
22453
|
-
ts21.ScriptTarget.Latest,
|
|
22454
|
-
/* setParentNodes */
|
|
22455
|
-
true
|
|
22456
|
-
);
|
|
22457
|
-
if (sf.statements.length !== 1) return null;
|
|
22458
|
-
const stmt = sf.statements[0];
|
|
22459
|
-
if (!ts21.isExpressionStatement(stmt)) return null;
|
|
22460
|
-
let parsed = stmt.expression;
|
|
22461
|
-
while (ts21.isParenthesizedExpression(parsed)) parsed = parsed.expression;
|
|
22462
|
-
if (!ts21.isObjectLiteralExpression(parsed)) return null;
|
|
22463
|
-
const entries2 = [];
|
|
22464
|
-
for (const prop of parsed.properties) {
|
|
22465
|
-
if (!ts21.isPropertyAssignment(prop)) return null;
|
|
22466
|
-
let key;
|
|
22467
|
-
if (ts21.isIdentifier(prop.name)) {
|
|
22468
|
-
key = prop.name.text;
|
|
22469
|
-
} else if (ts21.isStringLiteral(prop.name) || ts21.isNoSubstitutionTemplateLiteral(prop.name)) {
|
|
22470
|
-
key = prop.name.text;
|
|
22471
|
-
} else {
|
|
22472
|
-
return null;
|
|
22473
|
-
}
|
|
22474
|
-
let v = prop.initializer;
|
|
22475
|
-
while (ts21.isParenthesizedExpression(v)) v = v.expression;
|
|
22476
|
-
if (ts21.isNumericLiteral(v)) {
|
|
22477
|
-
entries2.push({ key, value: { kind: "number", text: v.text } });
|
|
22478
|
-
} else if (ts21.isStringLiteral(v) || ts21.isNoSubstitutionTemplateLiteral(v)) {
|
|
22479
|
-
entries2.push({ key, value: { kind: "string", text: v.text } });
|
|
22480
|
-
} else {
|
|
22481
|
-
return null;
|
|
22482
|
-
}
|
|
22483
|
-
}
|
|
22484
|
-
return { indexPropName, entries: entries2, defaultKey };
|
|
22485
|
-
}
|
|
22486
|
-
var init_augment_inherited_props = __esm({
|
|
22487
|
-
"../jsx/src/augment-inherited-props.ts"() {
|
|
22488
|
-
"use strict";
|
|
22489
|
-
init_html_constants();
|
|
22490
|
-
}
|
|
22491
|
-
});
|
|
22492
|
-
|
|
22493
22681
|
// ../jsx/src/index.ts
|
|
22494
22682
|
var src_exports = {};
|
|
22495
22683
|
__export(src_exports, {
|
|
@@ -22499,6 +22687,7 @@ __export(src_exports, {
|
|
|
22499
22687
|
BUILTIN_LOWERING_PLUGINS: () => BUILTIN_LOWERING_PLUGINS,
|
|
22500
22688
|
BaseAdapter: () => BaseAdapter,
|
|
22501
22689
|
CALLBACK_METHODS: () => CALLBACK_METHODS,
|
|
22690
|
+
ENV_SIGNAL_READERS: () => ENV_SIGNAL_READERS,
|
|
22502
22691
|
ErrorCodes: () => ErrorCodes,
|
|
22503
22692
|
JsxAdapter: () => JsxAdapter,
|
|
22504
22693
|
PROFILE_SCHEMA_VERSION: () => PROFILE_SCHEMA_VERSION,
|
|
@@ -22534,6 +22723,7 @@ __export(src_exports, {
|
|
|
22534
22723
|
collectModuleStringConsts: () => collectModuleStringConsts,
|
|
22535
22724
|
combineParentChildClientJs: () => combineParentChildClientJs,
|
|
22536
22725
|
compileJSX: () => compileJSX,
|
|
22726
|
+
computeSsrSeedPlan: () => computeSsrSeedPlan,
|
|
22537
22727
|
containsHigherOrder: () => containsHigherOrder,
|
|
22538
22728
|
createError: () => createError,
|
|
22539
22729
|
createProgramForCorpus: () => createProgramForCorpus,
|
|
@@ -22546,6 +22736,8 @@ __export(src_exports, {
|
|
|
22546
22736
|
emitIRNode: () => emitIRNode,
|
|
22547
22737
|
emitParsedExpr: () => emitParsedExpr,
|
|
22548
22738
|
enableCompilerInstrumentation: () => enableCompilerInstrumentation,
|
|
22739
|
+
envSignalLocalNames: () => envSignalLocalNames,
|
|
22740
|
+
envSignalReaderFor: () => envSignalReaderFor,
|
|
22549
22741
|
evalStringArrayJoin: () => evalStringArrayJoin,
|
|
22550
22742
|
evaluateProfileGates: () => evaluateProfileGates,
|
|
22551
22743
|
exprToString: () => exprToString,
|
|
@@ -22574,6 +22766,7 @@ __export(src_exports, {
|
|
|
22574
22766
|
formatUpdatePath: () => formatUpdatePath,
|
|
22575
22767
|
formatWastedReReruns: () => formatWastedReReruns,
|
|
22576
22768
|
formatWhyUpdate: () => formatWhyUpdate,
|
|
22769
|
+
freeIdentifiers: () => freeIdentifiers,
|
|
22577
22770
|
freeVarsInBody: () => freeVarsInBody,
|
|
22578
22771
|
generateClientJs: () => generateClientJs,
|
|
22579
22772
|
generateClientJsWithSourceMap: () => generateClientJsWithSourceMap,
|
|
@@ -22597,6 +22790,7 @@ __export(src_exports, {
|
|
|
22597
22790
|
matchLoweringCall: () => matchLoweringCall,
|
|
22598
22791
|
matchQueryHrefCall: () => matchQueryHrefCall,
|
|
22599
22792
|
matchSearchParamsMethodCall: () => matchSearchParamsMethodCall,
|
|
22793
|
+
materializeGetterCalls: () => materializeGetterCalls,
|
|
22600
22794
|
needsTypeBasedDetection: () => needsTypeBasedDetection,
|
|
22601
22795
|
parseBlockBody: () => parseBlockBody,
|
|
22602
22796
|
parseBlockBodyTolerant: () => parseBlockBodyTolerant,
|
|
@@ -22630,6 +22824,7 @@ var init_src2 = __esm({
|
|
|
22630
22824
|
"use strict";
|
|
22631
22825
|
init_compiler();
|
|
22632
22826
|
init_ssr_defaults();
|
|
22827
|
+
init_ssr_seed_plan();
|
|
22633
22828
|
init_analyzer();
|
|
22634
22829
|
init_shared_program();
|
|
22635
22830
|
init_jsx_to_ir();
|
|
@@ -26323,7 +26518,7 @@ var bfGoSource, streamingGoSource, bfdevGoSource;
|
|
|
26323
26518
|
var init_runtimes_generated = __esm({
|
|
26324
26519
|
"src/lib/adapters/runtimes.generated.ts"() {
|
|
26325
26520
|
"use strict";
|
|
26326
|
-
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\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: DeepEqual element search\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 reflect.DeepEqual(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';
|
|
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';
|
|
26327
26522
|
streamingGoSource = `// Package bf \u2014 Out-of-Order Streaming SSR helpers
|
|
26328
26523
|
//
|
|
26329
26524
|
// Provides StreamRenderer for progressive page rendering using HTTP
|