@barefootjs/cli 0.5.2 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +493 -151
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -518,6 +518,19 @@ function buildChainedArrayExpr(elem) {
|
|
|
518
518
|
chainOrder: elem.chainOrder
|
|
519
519
|
});
|
|
520
520
|
}
|
|
521
|
+
function loopOffsetTerms(offset) {
|
|
522
|
+
if (!offset) return [];
|
|
523
|
+
const terms = [];
|
|
524
|
+
if (offset.staticCount) terms.push(String(offset.staticCount));
|
|
525
|
+
terms.push(...offset.dynamicTerms);
|
|
526
|
+
return terms;
|
|
527
|
+
}
|
|
528
|
+
function buildLoopChildIndexExpr(indexParam, offset) {
|
|
529
|
+
return [indexParam, ...loopOffsetTerms(offset)].join(" + ");
|
|
530
|
+
}
|
|
531
|
+
function buildLoopChildIndexSubtraction(offset) {
|
|
532
|
+
return loopOffsetTerms(offset).map((term) => ` - ${term}`).join("");
|
|
533
|
+
}
|
|
521
534
|
function toDomEventName(eventName) {
|
|
522
535
|
return jsxToDomEventMap[eventName] ?? eventName;
|
|
523
536
|
}
|
|
@@ -1274,16 +1287,28 @@ function maybeHoistedScopeAttr(inHoistedChildren, node) {
|
|
|
1274
1287
|
return inHoistedChildren && node.needsScope ? `${BF_SCOPE}="${BF_PARENT_SCOPE_PLACEHOLDER}"` : null;
|
|
1275
1288
|
}
|
|
1276
1289
|
function templateAttrExpr(attrName, valExpr, presenceOrUndefined) {
|
|
1290
|
+
if (attrName === "dangerouslySetInnerHTML") return "";
|
|
1277
1291
|
if (isBooleanAttr(attrName) || presenceOrUndefined) {
|
|
1278
1292
|
return `\${${valExpr} ? '${attrName}' : ''}`;
|
|
1279
1293
|
}
|
|
1280
1294
|
if (attrName === "style") {
|
|
1281
|
-
return `\${((v) => v != null ? 'style="' + v + '"' : '')(styleToCss(${valExpr}))}`;
|
|
1295
|
+
return `\${((v) => v != null ? 'style="' + ${escapeAttrValueExpr("v")} + '"' : '')(styleToCss(${valExpr}))}`;
|
|
1282
1296
|
}
|
|
1283
1297
|
if (attrName === "data-key" || attrName.startsWith("data-key-")) {
|
|
1284
1298
|
return `${attrName}="\${${valExpr}}"`;
|
|
1285
1299
|
}
|
|
1286
|
-
return `\${(${valExpr}) != null ? '${attrName}="' +
|
|
1300
|
+
return `\${(${valExpr}) != null ? '${attrName}="' + ${escapeAttrValueExpr(valExpr)} + '"' : ''}`;
|
|
1301
|
+
}
|
|
1302
|
+
function escapeAttrValueExpr(valExpr) {
|
|
1303
|
+
return `escapeAttr(${valExpr})`;
|
|
1304
|
+
}
|
|
1305
|
+
function escapeTextSlotExpr(innerExpr) {
|
|
1306
|
+
return `escapeText(${innerExpr})`;
|
|
1307
|
+
}
|
|
1308
|
+
function dangerouslyHtmlChildren(attrs, toExpr) {
|
|
1309
|
+
const attr = attrs.find((a) => a.name === "dangerouslySetInnerHTML");
|
|
1310
|
+
if (!attr || attr.value.kind !== "expression") return null;
|
|
1311
|
+
return `\${((${toExpr(attr.value)}) ?? {}).__html ?? ''}`;
|
|
1287
1312
|
}
|
|
1288
1313
|
function transformKeyValue(value, transformExpr) {
|
|
1289
1314
|
switch (value.kind) {
|
|
@@ -1328,6 +1353,7 @@ function renderTemplateAttrPart(attr, attrName, wrap, restSpreadNames) {
|
|
|
1328
1353
|
function isMergeableAttr(a, ctx2) {
|
|
1329
1354
|
if (ctx2.honorClientOnly && a.clientOnly) return false;
|
|
1330
1355
|
if (a.name === "key") return false;
|
|
1356
|
+
if (a.name === "dangerouslySetInnerHTML") return false;
|
|
1331
1357
|
const v = a.value;
|
|
1332
1358
|
if (v.kind === "jsx-children") return false;
|
|
1333
1359
|
if (v.kind === "boolean-shorthand") return false;
|
|
@@ -1415,7 +1441,7 @@ function irToHtmlTemplate(node, restSpreadNames, loopDepth = 0, loopParams, bran
|
|
|
1415
1441
|
}
|
|
1416
1442
|
const attrs = attrParts.join(" ");
|
|
1417
1443
|
const childrenRecurse = (n) => irToHtmlTemplate(n, restSpreadNames, loopDepth, loopParams, branchSlotsVar, insideLoop, false);
|
|
1418
|
-
const children = node.children.map(childrenRecurse).join("");
|
|
1444
|
+
const children = dangerouslyHtmlChildren(node.attrs, (v) => wrapExpr(v.expr)) ?? node.children.map(childrenRecurse).join("");
|
|
1419
1445
|
if (children || !VOID_ELEMENTS.has(node.tag)) {
|
|
1420
1446
|
return `<${node.tag}${attrs ? " " + attrs : ""}>${children}</${node.tag}>`;
|
|
1421
1447
|
}
|
|
@@ -1426,7 +1452,9 @@ function irToHtmlTemplate(node, restSpreadNames, loopDepth = 0, loopParams, bran
|
|
|
1426
1452
|
case "expression":
|
|
1427
1453
|
if (node.expr === "null" || node.expr === "undefined") return "";
|
|
1428
1454
|
if (node.slotId) {
|
|
1429
|
-
|
|
1455
|
+
const inner = wrapInterpolation(wrapExpr(node.expr));
|
|
1456
|
+
const slotted = branchSlotsVar ? inner : escapeTextSlotExpr(inner);
|
|
1457
|
+
return `<!--bf:${node.slotId}-->\${${slotted}}<!--/-->`;
|
|
1430
1458
|
}
|
|
1431
1459
|
return `\${${wrapInterpolation(wrapExpr(node.expr))}}`;
|
|
1432
1460
|
case "conditional": {
|
|
@@ -1522,7 +1550,7 @@ function irToPlaceholderTemplate(node, restSpreadNames, loopDepth = 0, loopParam
|
|
|
1522
1550
|
attrParts.push(`bf="${node.slotId}"`);
|
|
1523
1551
|
}
|
|
1524
1552
|
const attrs = attrParts.join(" ");
|
|
1525
|
-
const children = node.children.map(recurse).join("");
|
|
1553
|
+
const children = dangerouslyHtmlChildren(node.attrs, (v) => wrapExpr(v.expr)) ?? node.children.map(recurse).join("");
|
|
1526
1554
|
if (children || !VOID_ELEMENTS.has(node.tag)) {
|
|
1527
1555
|
return `<${node.tag}${attrs ? " " + attrs : ""}>${children}</${node.tag}>`;
|
|
1528
1556
|
}
|
|
@@ -1533,7 +1561,7 @@ function irToPlaceholderTemplate(node, restSpreadNames, loopDepth = 0, loopParam
|
|
|
1533
1561
|
case "expression":
|
|
1534
1562
|
if (node.expr === "null" || node.expr === "undefined") return "";
|
|
1535
1563
|
if (node.slotId) {
|
|
1536
|
-
return `<!--bf:${node.slotId}-->\${${wrapExpr(node.expr)}}<!--/-->`;
|
|
1564
|
+
return `<!--bf:${node.slotId}-->\${${escapeTextSlotExpr(wrapExpr(node.expr))}}<!--/-->`;
|
|
1537
1565
|
}
|
|
1538
1566
|
return `\${${wrapExpr(node.expr)}}`;
|
|
1539
1567
|
case "conditional": {
|
|
@@ -1752,7 +1780,7 @@ function irToComponentTemplateWithOpts(node, opts) {
|
|
|
1752
1780
|
attrParts.push(`bf="${node.slotId}"`);
|
|
1753
1781
|
}
|
|
1754
1782
|
const attrs = attrParts.join(" ");
|
|
1755
|
-
const children = node.children.map(childrenRecurse).join("");
|
|
1783
|
+
const children = dangerouslyHtmlChildren(node.attrs, (v) => transformExpr(v.expr, v.templateExpr)) ?? node.children.map(childrenRecurse).join("");
|
|
1756
1784
|
if (children || !VOID_ELEMENTS.has(node.tag)) {
|
|
1757
1785
|
return `<${node.tag}${attrs ? " " + attrs : ""}>${children}</${node.tag}>`;
|
|
1758
1786
|
}
|
|
@@ -1763,7 +1791,7 @@ function irToComponentTemplateWithOpts(node, opts) {
|
|
|
1763
1791
|
case "expression":
|
|
1764
1792
|
if (node.expr === "null" || node.expr === "undefined") return "";
|
|
1765
1793
|
if (node.slotId) {
|
|
1766
|
-
return `<!--bf:${node.slotId}-->\${${transformExpr(node.expr, node.templateExpr)}}<!--/-->`;
|
|
1794
|
+
return `<!--bf:${node.slotId}-->\${${escapeTextSlotExpr(transformExpr(node.expr, node.templateExpr))}}<!--/-->`;
|
|
1767
1795
|
}
|
|
1768
1796
|
return `\${${transformExpr(node.expr, node.templateExpr)}}`;
|
|
1769
1797
|
case "conditional": {
|
|
@@ -1892,7 +1920,19 @@ function canGenerateStaticTemplate(node, propNames, inlinableConstants, unsafeLo
|
|
|
1892
1920
|
return assertNever(node);
|
|
1893
1921
|
}
|
|
1894
1922
|
}
|
|
1895
|
-
function generateCsrTemplate(node, inlinableConstants, ctx2, insideLoop, restSpreadNames, propsObjectName, unsafeLocalNames) {
|
|
1923
|
+
function generateCsrTemplate(node, inlinableConstants, ctx2, insideLoop, restSpreadNames, propsObjectName, unsafeLocalNames, deferredChildSlots) {
|
|
1924
|
+
const base = buildSignalMemoEnv(ctx2.signals, ctx2.memos, propsObjectName ?? null);
|
|
1925
|
+
const csrEnv = { substitutions: new Map(base.substitutions), propsObjectName: base.propsObjectName };
|
|
1926
|
+
if (inlinableConstants) {
|
|
1927
|
+
for (const [name, value] of inlinableConstants) {
|
|
1928
|
+
if (!csrEnv.substitutions.has(name)) {
|
|
1929
|
+
csrEnv.substitutions.set(name, { kind: "identifier", replacement: value, freeIdentifiers: /* @__PURE__ */ new Set() });
|
|
1930
|
+
}
|
|
1931
|
+
}
|
|
1932
|
+
}
|
|
1933
|
+
return generateCsrTemplateWithOpts(node, { inlinableConstants, restSpreadNames, propsObjectName, csrEnv, insideLoop, unsafeLocalNames, deferredChildSlots, loopDepth: -1 });
|
|
1934
|
+
}
|
|
1935
|
+
function buildCsrEnvForCtx(ctx2, inlinableConstants, propsObjectName) {
|
|
1896
1936
|
const base = buildSignalMemoEnv(ctx2.signals, ctx2.memos, propsObjectName ?? null);
|
|
1897
1937
|
const csrEnv = { substitutions: new Map(base.substitutions), propsObjectName: base.propsObjectName };
|
|
1898
1938
|
if (inlinableConstants) {
|
|
@@ -1902,7 +1942,64 @@ function generateCsrTemplate(node, inlinableConstants, ctx2, insideLoop, restSpr
|
|
|
1902
1942
|
}
|
|
1903
1943
|
}
|
|
1904
1944
|
}
|
|
1905
|
-
return
|
|
1945
|
+
return csrEnv;
|
|
1946
|
+
}
|
|
1947
|
+
function propResolvesUnsafe(prop, env, unsafeLocalNames) {
|
|
1948
|
+
if (unsafeLocalNames.size === 0) return false;
|
|
1949
|
+
let source;
|
|
1950
|
+
switch (prop.value.kind) {
|
|
1951
|
+
case "expression":
|
|
1952
|
+
case "spread":
|
|
1953
|
+
source = prop.value.expr;
|
|
1954
|
+
break;
|
|
1955
|
+
case "template":
|
|
1956
|
+
source = attrValueToString(prop.value, { useTemplate: true }) ?? void 0;
|
|
1957
|
+
break;
|
|
1958
|
+
default:
|
|
1959
|
+
return false;
|
|
1960
|
+
}
|
|
1961
|
+
if (!source) return false;
|
|
1962
|
+
const { freeIdentifiers } = csrSubstitute(source, env);
|
|
1963
|
+
return setIntersects(freeIdentifiers, unsafeLocalNames);
|
|
1964
|
+
}
|
|
1965
|
+
function computeDeferredChildSlots(node, ctx2, inlinableConstants, unsafeLocalNames, propsObjectName) {
|
|
1966
|
+
const deferred = /* @__PURE__ */ new Set();
|
|
1967
|
+
if (!unsafeLocalNames || unsafeLocalNames.size === 0) return deferred;
|
|
1968
|
+
const env = buildCsrEnvForCtx(ctx2, inlinableConstants, propsObjectName);
|
|
1969
|
+
const visit3 = (n) => {
|
|
1970
|
+
switch (n.type) {
|
|
1971
|
+
case "component": {
|
|
1972
|
+
if (n.name === "Portal") {
|
|
1973
|
+
n.children.forEach(visit3);
|
|
1974
|
+
return;
|
|
1975
|
+
}
|
|
1976
|
+
if (n.slotId) {
|
|
1977
|
+
const dropped = n.props.some((p) => {
|
|
1978
|
+
if (p.name === "..." || p.name.startsWith("...") || p.name === "key") return false;
|
|
1979
|
+
if (p.name.startsWith("on") && p.name.length > 2 && p.name[2] === p.name[2].toUpperCase()) return false;
|
|
1980
|
+
if (p.clientOnly) return false;
|
|
1981
|
+
return propResolvesUnsafe(p, env, unsafeLocalNames);
|
|
1982
|
+
});
|
|
1983
|
+
if (dropped) deferred.add(n.slotId);
|
|
1984
|
+
}
|
|
1985
|
+
return;
|
|
1986
|
+
}
|
|
1987
|
+
case "element":
|
|
1988
|
+
n.children.forEach(visit3);
|
|
1989
|
+
return;
|
|
1990
|
+
case "fragment":
|
|
1991
|
+
n.children.forEach(visit3);
|
|
1992
|
+
return;
|
|
1993
|
+
case "conditional":
|
|
1994
|
+
return;
|
|
1995
|
+
case "loop":
|
|
1996
|
+
return;
|
|
1997
|
+
default:
|
|
1998
|
+
return;
|
|
1999
|
+
}
|
|
2000
|
+
};
|
|
2001
|
+
visit3(node);
|
|
2002
|
+
return deferred;
|
|
1906
2003
|
}
|
|
1907
2004
|
function generateCsrTemplateWithOpts(node, opts) {
|
|
1908
2005
|
const { restSpreadNames, propsObjectName, csrEnv, insideLoop, unsafeLocalNames, loopDepth = 0 } = opts;
|
|
@@ -1966,7 +2063,7 @@ function generateCsrTemplateWithOpts(node, opts) {
|
|
|
1966
2063
|
attrParts.push(`bf="${node.slotId}"`);
|
|
1967
2064
|
}
|
|
1968
2065
|
const attrs = attrParts.join(" ");
|
|
1969
|
-
const children = node.children.map(childrenRecurse).join("");
|
|
2066
|
+
const children = dangerouslyHtmlChildren(node.attrs, (v) => transformExpr(v.expr, v.templateExpr)) ?? node.children.map(childrenRecurse).join("");
|
|
1970
2067
|
if (children || !VOID_ELEMENTS.has(node.tag)) {
|
|
1971
2068
|
return `<${node.tag}${attrs ? " " + attrs : ""}>${children}</${node.tag}>`;
|
|
1972
2069
|
}
|
|
@@ -1983,7 +2080,7 @@ function generateCsrTemplateWithOpts(node, opts) {
|
|
|
1983
2080
|
const transformed = transformExpr(node.expr, node.templateExpr);
|
|
1984
2081
|
const expr = transformed === UNSAFE_TEMPLATE_EXPR ? "''" : transformed;
|
|
1985
2082
|
if (node.slotId) {
|
|
1986
|
-
return `<!--bf:${node.slotId}-->\${${expr}}<!--/-->`;
|
|
2083
|
+
return `<!--bf:${node.slotId}-->\${${escapeTextSlotExpr(expr)}}<!--/-->`;
|
|
1987
2084
|
}
|
|
1988
2085
|
return `\${${expr}}`;
|
|
1989
2086
|
}
|
|
@@ -2003,6 +2100,9 @@ function generateCsrTemplateWithOpts(node, opts) {
|
|
|
2003
2100
|
if (node.name === "Portal") {
|
|
2004
2101
|
return node.children.map(recurse).join("");
|
|
2005
2102
|
}
|
|
2103
|
+
if (node.slotId && opts.deferredChildSlots?.has(node.slotId)) {
|
|
2104
|
+
return `<div ${BF_PLACEHOLDER}="${node.slotId}"></div>`;
|
|
2105
|
+
}
|
|
2006
2106
|
const propsEntries = node.props.filter((p) => p.name !== "..." && !p.name.startsWith("...") && p.name !== "key").filter((p) => !(p.name.startsWith("on") && p.name.length > 2 && p.name[2] === p.name[2].toUpperCase())).map((p) => {
|
|
2007
2107
|
if (p.clientOnly) return null;
|
|
2008
2108
|
switch (p.value.kind) {
|
|
@@ -5134,7 +5234,7 @@ function convertNode(node, raw) {
|
|
|
5134
5234
|
}
|
|
5135
5235
|
}
|
|
5136
5236
|
if (callee.kind === "member" && !callee.computed) {
|
|
5137
|
-
if (callee.property === "join"
|
|
5237
|
+
if (callee.property === "join") {
|
|
5138
5238
|
return { kind: "array-method", method: "join", object: callee.object, args: args2 };
|
|
5139
5239
|
}
|
|
5140
5240
|
if (callee.property === "includes" && args2.length === 1) {
|
|
@@ -5143,27 +5243,77 @@ function convertNode(node, raw) {
|
|
|
5143
5243
|
if ((callee.property === "indexOf" || callee.property === "lastIndexOf") && args2.length === 1) {
|
|
5144
5244
|
return { kind: "array-method", method: callee.property, object: callee.object, args: args2 };
|
|
5145
5245
|
}
|
|
5146
|
-
if (callee.property === "at"
|
|
5246
|
+
if (callee.property === "at") {
|
|
5147
5247
|
return { kind: "array-method", method: "at", object: callee.object, args: args2 };
|
|
5148
5248
|
}
|
|
5149
|
-
if (callee.property === "concat" && args2.length
|
|
5249
|
+
if (callee.property === "concat" && args2.length <= 1) {
|
|
5150
5250
|
return { kind: "array-method", method: "concat", object: callee.object, args: args2 };
|
|
5151
5251
|
}
|
|
5152
|
-
if (callee.property === "slice"
|
|
5252
|
+
if (callee.property === "slice") {
|
|
5153
5253
|
return { kind: "array-method", method: "slice", object: callee.object, args: args2 };
|
|
5154
5254
|
}
|
|
5155
|
-
if (
|
|
5255
|
+
if (callee.property === "reverse" || callee.property === "toReversed") {
|
|
5156
5256
|
return { kind: "array-method", method: callee.property, object: callee.object, args: args2 };
|
|
5157
5257
|
}
|
|
5158
|
-
if (callee.property === "toLowerCase"
|
|
5258
|
+
if (callee.property === "toLowerCase") {
|
|
5159
5259
|
return { kind: "array-method", method: "toLowerCase", object: callee.object, args: args2 };
|
|
5160
5260
|
}
|
|
5161
|
-
if (callee.property === "toUpperCase"
|
|
5261
|
+
if (callee.property === "toUpperCase") {
|
|
5162
5262
|
return { kind: "array-method", method: "toUpperCase", object: callee.object, args: args2 };
|
|
5163
5263
|
}
|
|
5164
|
-
if (callee.property === "trim"
|
|
5264
|
+
if (callee.property === "trim") {
|
|
5165
5265
|
return { kind: "array-method", method: "trim", object: callee.object, args: args2 };
|
|
5166
5266
|
}
|
|
5267
|
+
if (callee.property === "split") {
|
|
5268
|
+
return { kind: "array-method", method: "split", object: callee.object, args: args2 };
|
|
5269
|
+
}
|
|
5270
|
+
if (LOWERED_ARRAY_METHODS.has(callee.property)) {
|
|
5271
|
+
const argName = callee.property === "concat" ? "other" : "x";
|
|
5272
|
+
const detail = callee.property === "concat" ? "the variadic `.concat(a, b, \u2026)` form" : `\`.${callee.property}(\u2026)\` with ${args2.length} argument(s)`;
|
|
5273
|
+
return {
|
|
5274
|
+
kind: "unsupported",
|
|
5275
|
+
raw,
|
|
5276
|
+
reason: `${detail} is not yet lowered to the Go/Mojo template adapters. Use the single-argument \`.${callee.property}(${argName})\` form, or pre-compute the value before the template.`
|
|
5277
|
+
};
|
|
5278
|
+
}
|
|
5279
|
+
if (callee.property === "startsWith" || callee.property === "endsWith") {
|
|
5280
|
+
if (args2.length === 0) {
|
|
5281
|
+
return {
|
|
5282
|
+
kind: "unsupported",
|
|
5283
|
+
raw,
|
|
5284
|
+
reason: `\`.${callee.property}()\` with no search string is not lowered \u2014 JS coerces the missing argument to the string "undefined", a degenerate result. Pass an explicit search string, or pre-compute the value before the template.`
|
|
5285
|
+
};
|
|
5286
|
+
}
|
|
5287
|
+
return { kind: "array-method", method: callee.property, object: callee.object, args: args2 };
|
|
5288
|
+
}
|
|
5289
|
+
if (callee.property === "replace") {
|
|
5290
|
+
if (args2.length < 2) {
|
|
5291
|
+
return {
|
|
5292
|
+
kind: "unsupported",
|
|
5293
|
+
raw,
|
|
5294
|
+
reason: `\`.replace(${args2.length === 0 ? "" : "pattern"})\` needs both a pattern and a replacement \u2014 JS coerces the missing argument to the string "undefined", a degenerate result. Pass both arguments, or pre-compute the value before the template.`
|
|
5295
|
+
};
|
|
5296
|
+
}
|
|
5297
|
+
const patternNode = node.arguments[0];
|
|
5298
|
+
if (patternNode && ts8.isRegularExpressionLiteral(patternNode)) {
|
|
5299
|
+
return {
|
|
5300
|
+
kind: "unsupported",
|
|
5301
|
+
raw,
|
|
5302
|
+
reason: "String.prototype.replace supports only a string pattern + string replacement (the regex form is deferred); use a string pattern or wrap the expression in /* @client */"
|
|
5303
|
+
};
|
|
5304
|
+
}
|
|
5305
|
+
const badArg = args2[0].kind === "unsupported" ? args2[0] : args2[1].kind === "unsupported" ? args2[1] : void 0;
|
|
5306
|
+
if (badArg && badArg.kind === "unsupported") {
|
|
5307
|
+
return { kind: "unsupported", raw, reason: badArg.reason };
|
|
5308
|
+
}
|
|
5309
|
+
return { kind: "array-method", method: "replace", object: callee.object, args: args2 };
|
|
5310
|
+
}
|
|
5311
|
+
if (callee.property === "repeat") {
|
|
5312
|
+
return { kind: "array-method", method: "repeat", object: callee.object, args: args2 };
|
|
5313
|
+
}
|
|
5314
|
+
if (callee.property === "padStart" || callee.property === "padEnd") {
|
|
5315
|
+
return { kind: "array-method", method: callee.property, object: callee.object, args: args2 };
|
|
5316
|
+
}
|
|
5167
5317
|
if ((callee.property === "sort" || callee.property === "toSorted") && node.arguments.length === 1) {
|
|
5168
5318
|
const comparator = extractSortComparatorFromTS(node.arguments[0], callee.property);
|
|
5169
5319
|
if (comparator) {
|
|
@@ -6176,7 +6326,7 @@ function identifierPath(callee) {
|
|
|
6176
6326
|
}
|
|
6177
6327
|
return null;
|
|
6178
6328
|
}
|
|
6179
|
-
var UNSUPPORTED_METHODS;
|
|
6329
|
+
var UNSUPPORTED_METHODS, LOWERED_ARRAY_METHODS;
|
|
6180
6330
|
var init_expression_parser = __esm({
|
|
6181
6331
|
"../jsx/src/expression-parser.ts"() {
|
|
6182
6332
|
"use strict";
|
|
@@ -6235,14 +6385,25 @@ var init_expression_parser = __esm({
|
|
|
6235
6385
|
// unsupported array methods above get), pointing users at the
|
|
6236
6386
|
// `/* @client */` escape hatch. Each name drops off as its lowering
|
|
6237
6387
|
// lands. See #1448 "Unsupported string methods" Tier B / Tier C.
|
|
6238
|
-
|
|
6239
|
-
|
|
6240
|
-
|
|
6241
|
-
|
|
6388
|
+
// `split` is no longer here — `String.prototype.split(sep)` lowers
|
|
6389
|
+
// via the `array-method` IR + `bf_split` (Go) / `bf->split` (Mojo),
|
|
6390
|
+
// returning an array that composes with `.join()` / `.map()` / etc.
|
|
6391
|
+
// See #1448 Tier B.
|
|
6392
|
+
// `startsWith` / `endsWith` are no longer here — both lower via the
|
|
6393
|
+
// `array-method` IR + `bf_starts_with` / `bf_ends_with` (Go) and
|
|
6394
|
+
// `bf->starts_with` / `bf->ends_with` (Mojo). See #1448 Tier B.
|
|
6395
|
+
// `replace` is no longer here — the string-pattern form lowers via
|
|
6396
|
+
// the `array-method` IR + `bf_replace` (Go) / `bf->replace` (Mojo);
|
|
6397
|
+
// the regex-pattern form is refused at the parse arm below (it would
|
|
6398
|
+
// need the per-adapter regex-flavour decision). `replaceAll` stays
|
|
6399
|
+
// refused. See #1448 Tier B.
|
|
6400
|
+
// `repeat` is no longer here — `String.prototype.repeat(n)` lowers via
|
|
6401
|
+
// the `array-method` IR + `bf_repeat` (Go) / `bf->repeat` (Mojo).
|
|
6402
|
+
// See #1448 Tier B.
|
|
6403
|
+
// `padStart` / `padEnd` are no longer here — both lower via the
|
|
6404
|
+
// `array-method` IR + `bf_pad_start` / `bf_pad_end` (Go) and
|
|
6405
|
+
// `bf->pad_start` / `bf->pad_end` (Mojo). See #1448 Tier B.
|
|
6242
6406
|
"replaceAll",
|
|
6243
|
-
"repeat",
|
|
6244
|
-
"padStart",
|
|
6245
|
-
"padEnd",
|
|
6246
6407
|
"charAt",
|
|
6247
6408
|
"charCodeAt",
|
|
6248
6409
|
"codePointAt",
|
|
@@ -6253,6 +6414,12 @@ var init_expression_parser = __esm({
|
|
|
6253
6414
|
"matchAll",
|
|
6254
6415
|
"search"
|
|
6255
6416
|
]);
|
|
6417
|
+
LOWERED_ARRAY_METHODS = /* @__PURE__ */ new Set([
|
|
6418
|
+
"includes",
|
|
6419
|
+
"indexOf",
|
|
6420
|
+
"lastIndexOf",
|
|
6421
|
+
"concat"
|
|
6422
|
+
]);
|
|
6256
6423
|
}
|
|
6257
6424
|
});
|
|
6258
6425
|
|
|
@@ -8310,11 +8477,11 @@ function transformMapCall(node, ctx2, isClientOnly = false, method = "map") {
|
|
|
8310
8477
|
if (stmt === returnStmt) break;
|
|
8311
8478
|
const js = ctx2.getJS(stmt);
|
|
8312
8479
|
const tjs = ctx2.getTemplateJS(stmt);
|
|
8313
|
-
const
|
|
8480
|
+
const ts21 = stmt.getText(ctx2.sourceFile);
|
|
8314
8481
|
preambleStmts.push(js.endsWith(";") ? js : js + ";");
|
|
8315
8482
|
templatePreambleStmts.push(tjs.endsWith(";") ? tjs : tjs + ";");
|
|
8316
|
-
typedPreambleStmts.push(
|
|
8317
|
-
if (js !==
|
|
8483
|
+
typedPreambleStmts.push(ts21.endsWith(";") ? ts21 : ts21 + ";");
|
|
8484
|
+
if (js !== ts21) hasTypeDiff = true;
|
|
8318
8485
|
if (js !== tjs) hasTemplateDiff = true;
|
|
8319
8486
|
}
|
|
8320
8487
|
if (preambleStmts.length > 0) {
|
|
@@ -9590,23 +9757,72 @@ var init_reactivity = __esm({
|
|
|
9590
9757
|
});
|
|
9591
9758
|
|
|
9592
9759
|
// ../jsx/src/ir-to-client-js/collect-elements.ts
|
|
9593
|
-
function
|
|
9594
|
-
|
|
9760
|
+
function domElementCount(node) {
|
|
9761
|
+
switch (node.type) {
|
|
9762
|
+
case "element":
|
|
9763
|
+
case "component":
|
|
9764
|
+
case "provider":
|
|
9765
|
+
case "async":
|
|
9766
|
+
return 1;
|
|
9767
|
+
case "text":
|
|
9768
|
+
return 0;
|
|
9769
|
+
case "expression":
|
|
9770
|
+
return EMPTY_RENDER_EXPRS.has(node.expr.trim()) ? 0 : null;
|
|
9771
|
+
case "loop":
|
|
9772
|
+
if (node.bodyIsItemConditional || node.method === "flatMap") return null;
|
|
9773
|
+
return `(${buildLoopChainExpr({
|
|
9774
|
+
base: node.array,
|
|
9775
|
+
sortComparator: node.sortComparator,
|
|
9776
|
+
filterPredicate: node.filterPredicate,
|
|
9777
|
+
chainOrder: node.chainOrder
|
|
9778
|
+
})}).length`;
|
|
9779
|
+
case "conditional": {
|
|
9780
|
+
const t = domElementCount(node.whenTrue);
|
|
9781
|
+
const f = domElementCount(node.whenFalse);
|
|
9782
|
+
if (t === null || f === null) return null;
|
|
9783
|
+
if (typeof t === "number" && typeof f === "number" && t === f) return t;
|
|
9784
|
+
return `(${node.condition} ? ${t} : ${f})`;
|
|
9785
|
+
}
|
|
9786
|
+
case "fragment":
|
|
9787
|
+
return sumElementCounts(node.children);
|
|
9788
|
+
default:
|
|
9789
|
+
return null;
|
|
9790
|
+
}
|
|
9791
|
+
}
|
|
9792
|
+
function sumElementCounts(nodes) {
|
|
9793
|
+
let staticCount = 0;
|
|
9794
|
+
const dynamic = [];
|
|
9795
|
+
for (const n of nodes) {
|
|
9796
|
+
const c = domElementCount(n);
|
|
9797
|
+
if (c === null) return null;
|
|
9798
|
+
if (typeof c === "number") staticCount += c;
|
|
9799
|
+
else dynamic.push(c);
|
|
9800
|
+
}
|
|
9801
|
+
if (dynamic.length === 0) return staticCount;
|
|
9802
|
+
const parts = staticCount > 0 ? [String(staticCount), ...dynamic] : dynamic;
|
|
9803
|
+
return parts.length === 1 ? parts[0] : `(${parts.join(" + ")})`;
|
|
9804
|
+
}
|
|
9805
|
+
function legacyElementCount(node) {
|
|
9806
|
+
return node.type === "element" || node.type === "component" || node.type === "provider" || node.type === "async" || node.type === "text" || node.type === "expression" && !node.reactive || node.type === "conditional" ? 1 : 0;
|
|
9595
9807
|
}
|
|
9596
9808
|
function computeLoopSiblingOffsets(root) {
|
|
9597
9809
|
const offsets = /* @__PURE__ */ new Map();
|
|
9598
|
-
const
|
|
9599
|
-
let nonLoopCount = 0;
|
|
9810
|
+
const recordRun = (children, preceding) => {
|
|
9600
9811
|
for (const child of children) {
|
|
9601
9812
|
if (child.type === "loop") {
|
|
9602
|
-
if (
|
|
9603
|
-
|
|
9604
|
-
|
|
9813
|
+
if (preceding.length > 0 && !offsets.has(child)) {
|
|
9814
|
+
offsets.set(child, [...preceding]);
|
|
9815
|
+
}
|
|
9816
|
+
preceding.push(child);
|
|
9817
|
+
} else if (child.type === "fragment" || child.type === "provider" || child.type === "async") {
|
|
9818
|
+
recordRun(child.children, preceding);
|
|
9819
|
+
} else {
|
|
9820
|
+
preceding.push(child);
|
|
9605
9821
|
}
|
|
9606
9822
|
}
|
|
9607
9823
|
};
|
|
9608
9824
|
const containerVisit = ({ node, descend }) => {
|
|
9609
|
-
|
|
9825
|
+
recordRun(node.children, []);
|
|
9610
9826
|
descend();
|
|
9611
9827
|
};
|
|
9612
9828
|
walkIR(root, null, {
|
|
@@ -9622,6 +9838,19 @@ function computeLoopSiblingOffsets(root) {
|
|
|
9622
9838
|
});
|
|
9623
9839
|
return offsets;
|
|
9624
9840
|
}
|
|
9841
|
+
function resolveLoopOffset(preceding) {
|
|
9842
|
+
if (!preceding || preceding.length === 0) return void 0;
|
|
9843
|
+
let staticCount = 0;
|
|
9844
|
+
const dynamicTerms = [];
|
|
9845
|
+
for (const node of preceding) {
|
|
9846
|
+
const c = domElementCount(node);
|
|
9847
|
+
if (c === null) staticCount += legacyElementCount(node);
|
|
9848
|
+
else if (typeof c === "number") staticCount += c;
|
|
9849
|
+
else dynamicTerms.push(c);
|
|
9850
|
+
}
|
|
9851
|
+
if (staticCount === 0 && dynamicTerms.length === 0) return void 0;
|
|
9852
|
+
return { staticCount, dynamicTerms };
|
|
9853
|
+
}
|
|
9625
9854
|
function collectInnerLoops(nodes, siblingOffsets, outerLoopParam, ctx2, options) {
|
|
9626
9855
|
const result = [];
|
|
9627
9856
|
const flat = options?.flatBranchMode === true;
|
|
@@ -9707,7 +9936,7 @@ function collectInnerLoops(nodes, siblingOffsets, outerLoopParam, ctx2, options)
|
|
|
9707
9936
|
refsOuterParam: refsOuter,
|
|
9708
9937
|
childComponents,
|
|
9709
9938
|
insideConditional: !flat && scope.insideCond ? true : void 0,
|
|
9710
|
-
|
|
9939
|
+
offset: flat ? void 0 : resolveLoopOffset(siblingOffsets.get(n)),
|
|
9711
9940
|
bindings
|
|
9712
9941
|
});
|
|
9713
9942
|
if (!flat) {
|
|
@@ -9917,7 +10146,7 @@ function collectElements(node, ctx2, siblingOffsets, insideConditional = false)
|
|
|
9917
10146
|
isStaticArray: l.isStaticArray,
|
|
9918
10147
|
useElementReconciliation,
|
|
9919
10148
|
innerLoops: useElementReconciliation || l.isStaticArray && innerLoops?.length ? innerLoops : void 0,
|
|
9920
|
-
|
|
10149
|
+
offset: resolveLoopOffset(siblingOffsets.get(l)),
|
|
9921
10150
|
filterPredicate: l.filterPredicate ? {
|
|
9922
10151
|
param: l.filterPredicate.param,
|
|
9923
10152
|
raw: l.filterPredicate.raw
|
|
@@ -10218,7 +10447,7 @@ function summarizeLoopChildBranch(node, ctx2, siblingOffsets, loopParam, loopPar
|
|
|
10218
10447
|
events: collectConditionalBranchEvents(node)
|
|
10219
10448
|
};
|
|
10220
10449
|
}
|
|
10221
|
-
var branchInnerLoopOptions;
|
|
10450
|
+
var EMPTY_RENDER_EXPRS, branchInnerLoopOptions;
|
|
10222
10451
|
var init_collect_elements = __esm({
|
|
10223
10452
|
"../jsx/src/ir-to-client-js/collect-elements.ts"() {
|
|
10224
10453
|
"use strict";
|
|
@@ -10228,6 +10457,8 @@ var init_collect_elements = __esm({
|
|
|
10228
10457
|
init_html_template();
|
|
10229
10458
|
init_prop_handling();
|
|
10230
10459
|
init_walker();
|
|
10460
|
+
init_loop_chain();
|
|
10461
|
+
EMPTY_RENDER_EXPRS = /* @__PURE__ */ new Set(["null", "undefined", "false", "''", '""', "``"]);
|
|
10231
10462
|
branchInnerLoopOptions = {
|
|
10232
10463
|
collectItemBindings: true,
|
|
10233
10464
|
templateDepth: 1,
|
|
@@ -10787,6 +11018,8 @@ var init_imports = __esm({
|
|
|
10787
11018
|
"splitProps",
|
|
10788
11019
|
"spreadAttrs",
|
|
10789
11020
|
"styleToCss",
|
|
11021
|
+
"escapeAttr",
|
|
11022
|
+
"escapeText",
|
|
10790
11023
|
"qsa",
|
|
10791
11024
|
"qsaItem",
|
|
10792
11025
|
"qsaChildScope",
|
|
@@ -11583,7 +11816,8 @@ function emitRegistrationAndHydration(lines, ctx2, _ir, graph, inlinability) {
|
|
|
11583
11816
|
void 0,
|
|
11584
11817
|
restSpreadNames,
|
|
11585
11818
|
ctx2.propsObjectName,
|
|
11586
|
-
unsafeLocalNames
|
|
11819
|
+
unsafeLocalNames,
|
|
11820
|
+
ctx2.deferredChildSlots
|
|
11587
11821
|
);
|
|
11588
11822
|
if (templateHtml) {
|
|
11589
11823
|
defParts.push(`template: (${PROPS_PARAM}) => \`${templateHtml}\``);
|
|
@@ -12305,8 +12539,13 @@ function emitProviderAndChildInits(lines, ctx2) {
|
|
|
12305
12539
|
lines.push("");
|
|
12306
12540
|
lines.push(` // Initialize child components with props`);
|
|
12307
12541
|
for (const child of ctx2.childInits) {
|
|
12542
|
+
const registryName = nameForRegistryRef(child.name);
|
|
12543
|
+
if (child.slotId && ctx2.deferredChildSlots.has(child.slotId)) {
|
|
12544
|
+
lines.push(` upsertChild(__scope, '${registryName}', '${child.slotId}', ${child.propsExpr})`);
|
|
12545
|
+
continue;
|
|
12546
|
+
}
|
|
12308
12547
|
const scopeRef = child.slotId ? `_${varSlotId(child.slotId)}` : "__scope";
|
|
12309
|
-
lines.push(` initChild('${
|
|
12548
|
+
lines.push(` initChild('${registryName}', ${scopeRef}, ${child.propsExpr})`);
|
|
12310
12549
|
}
|
|
12311
12550
|
}
|
|
12312
12551
|
}
|
|
@@ -12624,7 +12863,7 @@ function buildOuterNestedPlan(elem, comp) {
|
|
|
12624
12863
|
arrayExpr: elem.array,
|
|
12625
12864
|
param: elem.param,
|
|
12626
12865
|
indexParam,
|
|
12627
|
-
offsetExpr:
|
|
12866
|
+
offsetExpr: buildLoopChildIndexExpr(indexParam, elem.offset),
|
|
12628
12867
|
outerPreludeStatements: elem.mapPreamble ? [elem.mapPreamble] : [],
|
|
12629
12868
|
propsExpr: buildStaticPropsExpr(comp.props)
|
|
12630
12869
|
};
|
|
@@ -12642,12 +12881,12 @@ function buildInnerLoopNestedPlan(elem, innerLoop, innerComps) {
|
|
|
12642
12881
|
outerArrayExpr: elem.array,
|
|
12643
12882
|
outerParam: elem.param,
|
|
12644
12883
|
outerIndexParam,
|
|
12645
|
-
outerOffsetExpr:
|
|
12884
|
+
outerOffsetExpr: buildLoopChildIndexExpr(outerIndexParam, elem.offset),
|
|
12646
12885
|
outerPreludeStatements: elem.mapPreamble ? [elem.mapPreamble] : [],
|
|
12647
12886
|
innerContainerSlotId: innerLoop.containerSlotId ?? null,
|
|
12648
12887
|
innerArrayExpr: innerLoop.array,
|
|
12649
12888
|
innerParam: innerLoop.param,
|
|
12650
|
-
innerOffsetExpr:
|
|
12889
|
+
innerOffsetExpr: buildLoopChildIndexExpr("__innerIdx", innerLoop.offset),
|
|
12651
12890
|
innerPreludeStatements: innerLoop.mapPreamble ? [innerLoop.mapPreamble] : [],
|
|
12652
12891
|
depth: innerLoop.depth,
|
|
12653
12892
|
comps
|
|
@@ -13464,7 +13703,7 @@ function buildStaticArrayDelegationPlan(elem) {
|
|
|
13464
13703
|
arrayExpr: buildChainedArrayExpr(elem),
|
|
13465
13704
|
param: elem.param,
|
|
13466
13705
|
mapPreamble: elem.mapPreamble ?? null,
|
|
13467
|
-
|
|
13706
|
+
offset: elem.offset ?? null
|
|
13468
13707
|
}
|
|
13469
13708
|
};
|
|
13470
13709
|
}
|
|
@@ -13618,6 +13857,11 @@ var init_build_insert = __esm({
|
|
|
13618
13857
|
// ../jsx/src/ir-to-client-js/emit-reactive.ts
|
|
13619
13858
|
function emitAttrUpdate(target, attrName, expression, meta) {
|
|
13620
13859
|
const htmlName = toHTMLAttrName(attrName);
|
|
13860
|
+
if (attrName === "dangerouslySetInnerHTML" || htmlName === "dangerouslySetInnerHTML") {
|
|
13861
|
+
return [
|
|
13862
|
+
`{ const __v = ${expression}; ${target}.innerHTML = __v != null && __v.__html != null ? String(__v.__html) : '' }`
|
|
13863
|
+
];
|
|
13864
|
+
}
|
|
13621
13865
|
if (htmlName === "style") {
|
|
13622
13866
|
return [
|
|
13623
13867
|
`{ const __v = styleToCss(${expression}); if (__v != null) ${target}.setAttribute('style', __v); else ${target}.removeAttribute('style') }`
|
|
@@ -14681,11 +14925,11 @@ function emitDynamicIndexLookup(ls, ev, handlerCall, lookup) {
|
|
|
14681
14925
|
ls.push(` }`);
|
|
14682
14926
|
}
|
|
14683
14927
|
function emitStaticIndexLookup(ls, ev, handlerCall, lookup, containerVar) {
|
|
14684
|
-
const { arrayExpr, param, mapPreamble,
|
|
14928
|
+
const { arrayExpr, param, mapPreamble, offset } = lookup;
|
|
14685
14929
|
ls.push(` let __el = ${varSlotId(ev.childSlotId)}El`);
|
|
14686
14930
|
ls.push(` while (__el.parentElement && __el.parentElement !== ${containerVar}) __el = __el.parentElement`);
|
|
14687
14931
|
ls.push(` if (__el.parentElement === ${containerVar}) {`);
|
|
14688
|
-
const idxOffset =
|
|
14932
|
+
const idxOffset = buildLoopChildIndexSubtraction(offset ?? void 0);
|
|
14689
14933
|
ls.push(` const __idx = Array.from(${containerVar}.children).indexOf(__el)${idxOffset}`);
|
|
14690
14934
|
ls.push(` const ${param} = ${arrayExpr}[__idx]`);
|
|
14691
14935
|
if (mapPreamble) ls.push(` ${mapPreamble}`);
|
|
@@ -15013,7 +15257,7 @@ function buildStaticLoopPlan(elem, unsafeLocalNames) {
|
|
|
15013
15257
|
}
|
|
15014
15258
|
}
|
|
15015
15259
|
const indexParam = elem.index || "__idx";
|
|
15016
|
-
const childIndexExpr =
|
|
15260
|
+
const childIndexExpr = buildLoopChildIndexExpr(indexParam, elem.offset);
|
|
15017
15261
|
return {
|
|
15018
15262
|
kind: "static",
|
|
15019
15263
|
containerVar: `_${varSlotId(elem.slotId)}`,
|
|
@@ -15386,6 +15630,13 @@ function generateInitFunction(ir, ctx2, siblingComponents, localImportPrefixes)
|
|
|
15386
15630
|
const classification = classifyLocalDeclarations(ctx2, graph);
|
|
15387
15631
|
const propUsage = computePropUsage(ctx2, classification.neededConstants);
|
|
15388
15632
|
const inlinability = buildInlinableConstants(ctx2, graph, ir.root);
|
|
15633
|
+
ctx2.deferredChildSlots = computeDeferredChildSlots(
|
|
15634
|
+
ir.root,
|
|
15635
|
+
ctx2,
|
|
15636
|
+
csrInlinableConstantsFromCtx(ctx2),
|
|
15637
|
+
inlinability.unsafeLocalNames,
|
|
15638
|
+
ctx2.propsObjectName
|
|
15639
|
+
);
|
|
15389
15640
|
const phaseCtx = buildPhaseCtx({
|
|
15390
15641
|
ctx: ctx2,
|
|
15391
15642
|
ir,
|
|
@@ -15415,6 +15666,7 @@ var init_generate_init = __esm({
|
|
|
15415
15666
|
init_compute_prop_usage();
|
|
15416
15667
|
init_imports();
|
|
15417
15668
|
init_emit_registration();
|
|
15669
|
+
init_html_template();
|
|
15418
15670
|
init_child_components();
|
|
15419
15671
|
init_init_declarations();
|
|
15420
15672
|
init_emit_module_level();
|
|
@@ -15690,6 +15942,7 @@ function createContext2(ir, scope, adapterCapabilities) {
|
|
|
15690
15942
|
loopElements: [],
|
|
15691
15943
|
refElements: [],
|
|
15692
15944
|
childInits: [],
|
|
15945
|
+
deferredChildSlots: /* @__PURE__ */ new Set(),
|
|
15693
15946
|
reactiveProps: [],
|
|
15694
15947
|
reactiveChildProps: [],
|
|
15695
15948
|
reactiveAttrs: [],
|
|
@@ -17409,6 +17662,7 @@ var init_attr_value_emitter = __esm({
|
|
|
17409
17662
|
});
|
|
17410
17663
|
|
|
17411
17664
|
// ../jsx/src/combine-client-js.ts
|
|
17665
|
+
import ts18 from "typescript";
|
|
17412
17666
|
function combineParentChildClientJs(files) {
|
|
17413
17667
|
const result = /* @__PURE__ */ new Map();
|
|
17414
17668
|
const lookup = /* @__PURE__ */ new Map();
|
|
@@ -17465,30 +17719,48 @@ function combineParentChildClientJs(files) {
|
|
|
17465
17719
|
return result;
|
|
17466
17720
|
}
|
|
17467
17721
|
function parseAndMerge(content, importsBySource, otherImports, codeSections) {
|
|
17468
|
-
const
|
|
17469
|
-
|
|
17470
|
-
|
|
17471
|
-
|
|
17472
|
-
|
|
17473
|
-
|
|
17474
|
-
|
|
17475
|
-
|
|
17476
|
-
|
|
17477
|
-
|
|
17478
|
-
|
|
17479
|
-
|
|
17480
|
-
|
|
17481
|
-
|
|
17482
|
-
|
|
17483
|
-
|
|
17484
|
-
|
|
17485
|
-
|
|
17722
|
+
const sourceFile = ts18.createSourceFile(
|
|
17723
|
+
"combine.js",
|
|
17724
|
+
content,
|
|
17725
|
+
ts18.ScriptTarget.Latest,
|
|
17726
|
+
/*setParentNodes*/
|
|
17727
|
+
false,
|
|
17728
|
+
ts18.ScriptKind.JS
|
|
17729
|
+
);
|
|
17730
|
+
const importSpans = [];
|
|
17731
|
+
for (const stmt of sourceFile.statements) {
|
|
17732
|
+
if (!ts18.isImportDeclaration(stmt)) continue;
|
|
17733
|
+
const start = stmt.getStart(sourceFile);
|
|
17734
|
+
const end = stmt.getEnd();
|
|
17735
|
+
importSpans.push([start, end]);
|
|
17736
|
+
const stmtText = content.slice(start, end);
|
|
17737
|
+
if (stmtText.includes("@bf-child:")) continue;
|
|
17738
|
+
const clause = stmt.importClause;
|
|
17739
|
+
const bindings = clause?.namedBindings;
|
|
17740
|
+
const specifier = ts18.isStringLiteral(stmt.moduleSpecifier) ? stmt.moduleSpecifier.text : "";
|
|
17741
|
+
if (clause && !clause.name && bindings && ts18.isNamedImports(bindings)) {
|
|
17742
|
+
if (!importsBySource.has(specifier)) {
|
|
17743
|
+
importsBySource.set(specifier, /* @__PURE__ */ new Set());
|
|
17744
|
+
}
|
|
17745
|
+
const set = importsBySource.get(specifier);
|
|
17746
|
+
for (const el of bindings.elements) {
|
|
17747
|
+
const name = el.propertyName ? `${el.propertyName.text} as ${el.name.text}` : el.name.text;
|
|
17748
|
+
set.add(name);
|
|
17486
17749
|
}
|
|
17487
17750
|
} else {
|
|
17488
|
-
|
|
17751
|
+
if (!otherImports.includes(stmtText)) {
|
|
17752
|
+
otherImports.push(stmtText);
|
|
17753
|
+
}
|
|
17489
17754
|
}
|
|
17490
17755
|
}
|
|
17491
|
-
|
|
17756
|
+
let code = "";
|
|
17757
|
+
let cursor = 0;
|
|
17758
|
+
for (const [start, end] of importSpans) {
|
|
17759
|
+
code += content.slice(cursor, start);
|
|
17760
|
+
cursor = end;
|
|
17761
|
+
}
|
|
17762
|
+
code += content.slice(cursor);
|
|
17763
|
+
code = code.trim();
|
|
17492
17764
|
if (code) {
|
|
17493
17765
|
codeSections.push(code);
|
|
17494
17766
|
}
|
|
@@ -18921,7 +19193,7 @@ var init_runtime = __esm({
|
|
|
18921
19193
|
|
|
18922
19194
|
// src/lib/resolve-imports.ts
|
|
18923
19195
|
import { dirname as dirname2, resolve as resolve2 } from "node:path";
|
|
18924
|
-
import
|
|
19196
|
+
import ts19 from "typescript";
|
|
18925
19197
|
function shapeFromDecl(decl) {
|
|
18926
19198
|
const clause = decl.importClause;
|
|
18927
19199
|
if (!clause) return null;
|
|
@@ -18931,7 +19203,7 @@ function shapeFromDecl(decl) {
|
|
|
18931
19203
|
}
|
|
18932
19204
|
const bindings = clause.namedBindings;
|
|
18933
19205
|
if (bindings) {
|
|
18934
|
-
if (
|
|
19206
|
+
if (ts19.isNamespaceImport(bindings)) {
|
|
18935
19207
|
shape.namespace = bindings.name.text;
|
|
18936
19208
|
} else {
|
|
18937
19209
|
for (const el of bindings.elements) {
|
|
@@ -18945,38 +19217,38 @@ function shapeFromDecl(decl) {
|
|
|
18945
19217
|
}
|
|
18946
19218
|
function collectExportedNames(source) {
|
|
18947
19219
|
const names = /* @__PURE__ */ new Set();
|
|
18948
|
-
const sourceFile =
|
|
19220
|
+
const sourceFile = ts19.createSourceFile(
|
|
18949
19221
|
"mod.ts",
|
|
18950
19222
|
source,
|
|
18951
|
-
|
|
19223
|
+
ts19.ScriptTarget.Latest,
|
|
18952
19224
|
/*setParents*/
|
|
18953
19225
|
false,
|
|
18954
|
-
|
|
19226
|
+
ts19.ScriptKind.TS
|
|
18955
19227
|
);
|
|
18956
19228
|
function hasExport(node) {
|
|
18957
|
-
if (!
|
|
18958
|
-
const mods =
|
|
18959
|
-
return mods?.some((m) => m.kind ===
|
|
19229
|
+
if (!ts19.canHaveModifiers(node)) return false;
|
|
19230
|
+
const mods = ts19.getModifiers(node);
|
|
19231
|
+
return mods?.some((m) => m.kind === ts19.SyntaxKind.ExportKeyword) ?? false;
|
|
18960
19232
|
}
|
|
18961
19233
|
function collectFromBindingName(name) {
|
|
18962
|
-
if (
|
|
19234
|
+
if (ts19.isIdentifier(name)) {
|
|
18963
19235
|
names.add(name.text);
|
|
18964
19236
|
return;
|
|
18965
19237
|
}
|
|
18966
19238
|
for (const el of name.elements) {
|
|
18967
|
-
if (
|
|
19239
|
+
if (ts19.isBindingElement(el)) collectFromBindingName(el.name);
|
|
18968
19240
|
}
|
|
18969
19241
|
}
|
|
18970
19242
|
for (const stmt of sourceFile.statements) {
|
|
18971
|
-
if (
|
|
19243
|
+
if (ts19.isVariableStatement(stmt) && hasExport(stmt)) {
|
|
18972
19244
|
for (const d of stmt.declarationList.declarations) {
|
|
18973
19245
|
collectFromBindingName(d.name);
|
|
18974
19246
|
}
|
|
18975
|
-
} else if (
|
|
19247
|
+
} else if (ts19.isFunctionDeclaration(stmt) && hasExport(stmt) && stmt.name) {
|
|
18976
19248
|
names.add(stmt.name.text);
|
|
18977
|
-
} else if (
|
|
19249
|
+
} else if (ts19.isClassDeclaration(stmt) && hasExport(stmt) && stmt.name) {
|
|
18978
19250
|
names.add(stmt.name.text);
|
|
18979
|
-
} else if (
|
|
19251
|
+
} else if (ts19.isExportDeclaration(stmt) && !stmt.moduleSpecifier && stmt.exportClause && ts19.isNamedExports(stmt.exportClause)) {
|
|
18980
19252
|
if (stmt.isTypeOnly) continue;
|
|
18981
19253
|
for (const el of stmt.exportClause.elements) {
|
|
18982
19254
|
if (el.isTypeOnly) continue;
|
|
@@ -18987,16 +19259,16 @@ function collectExportedNames(source) {
|
|
|
18987
19259
|
return [...names];
|
|
18988
19260
|
}
|
|
18989
19261
|
function hasUseClientDirective(source) {
|
|
18990
|
-
const sourceFile =
|
|
19262
|
+
const sourceFile = ts19.createSourceFile(
|
|
18991
19263
|
"check.tsx",
|
|
18992
19264
|
source,
|
|
18993
|
-
|
|
19265
|
+
ts19.ScriptTarget.Latest,
|
|
18994
19266
|
/*setParents*/
|
|
18995
19267
|
false,
|
|
18996
|
-
|
|
19268
|
+
ts19.ScriptKind.TSX
|
|
18997
19269
|
);
|
|
18998
19270
|
for (const stmt of sourceFile.statements) {
|
|
18999
|
-
if (!
|
|
19271
|
+
if (!ts19.isExpressionStatement(stmt) || !ts19.isStringLiteral(stmt.expression)) {
|
|
19000
19272
|
return false;
|
|
19001
19273
|
}
|
|
19002
19274
|
if (stmt.expression.text === "use client") return true;
|
|
@@ -19005,53 +19277,53 @@ function hasUseClientDirective(source) {
|
|
|
19005
19277
|
}
|
|
19006
19278
|
function collectTopLevelBindings(source) {
|
|
19007
19279
|
const names = /* @__PURE__ */ new Set();
|
|
19008
|
-
const sourceFile =
|
|
19280
|
+
const sourceFile = ts19.createSourceFile(
|
|
19009
19281
|
"bundle.ts",
|
|
19010
19282
|
source,
|
|
19011
|
-
|
|
19283
|
+
ts19.ScriptTarget.Latest,
|
|
19012
19284
|
/*setParents*/
|
|
19013
19285
|
false,
|
|
19014
|
-
|
|
19286
|
+
ts19.ScriptKind.TS
|
|
19015
19287
|
);
|
|
19016
19288
|
function collectFromBindingName(name) {
|
|
19017
|
-
if (
|
|
19289
|
+
if (ts19.isIdentifier(name)) {
|
|
19018
19290
|
names.add(name.text);
|
|
19019
19291
|
return;
|
|
19020
19292
|
}
|
|
19021
19293
|
for (const el of name.elements) {
|
|
19022
|
-
if (
|
|
19294
|
+
if (ts19.isBindingElement(el)) collectFromBindingName(el.name);
|
|
19023
19295
|
}
|
|
19024
19296
|
}
|
|
19025
19297
|
for (const stmt of sourceFile.statements) {
|
|
19026
|
-
if (
|
|
19298
|
+
if (ts19.isVariableStatement(stmt)) {
|
|
19027
19299
|
for (const d of stmt.declarationList.declarations) {
|
|
19028
19300
|
collectFromBindingName(d.name);
|
|
19029
19301
|
}
|
|
19030
|
-
} else if (
|
|
19302
|
+
} else if (ts19.isFunctionDeclaration(stmt) && stmt.name) {
|
|
19031
19303
|
names.add(stmt.name.text);
|
|
19032
|
-
} else if (
|
|
19304
|
+
} else if (ts19.isClassDeclaration(stmt) && stmt.name) {
|
|
19033
19305
|
names.add(stmt.name.text);
|
|
19034
19306
|
}
|
|
19035
19307
|
}
|
|
19036
19308
|
return names;
|
|
19037
19309
|
}
|
|
19038
19310
|
function stripImportsAndExports(body) {
|
|
19039
|
-
const sourceFile =
|
|
19311
|
+
const sourceFile = ts19.createSourceFile(
|
|
19040
19312
|
"body.ts",
|
|
19041
19313
|
body,
|
|
19042
|
-
|
|
19314
|
+
ts19.ScriptTarget.Latest,
|
|
19043
19315
|
/*setParents*/
|
|
19044
19316
|
false,
|
|
19045
|
-
|
|
19317
|
+
ts19.ScriptKind.TS
|
|
19046
19318
|
);
|
|
19047
19319
|
const spans = [];
|
|
19048
19320
|
const hoistedImports = [];
|
|
19049
19321
|
for (const stmt of sourceFile.statements) {
|
|
19050
|
-
if (
|
|
19322
|
+
if (ts19.isImportDeclaration(stmt)) {
|
|
19051
19323
|
const start = stmt.getStart(sourceFile);
|
|
19052
19324
|
const end = stmt.getEnd();
|
|
19053
19325
|
const specifier = stmt.moduleSpecifier;
|
|
19054
|
-
if (
|
|
19326
|
+
if (ts19.isStringLiteral(specifier)) {
|
|
19055
19327
|
const path23 = specifier.text;
|
|
19056
19328
|
const isRelative = path23.startsWith("./") || path23.startsWith("../");
|
|
19057
19329
|
if (!isRelative) {
|
|
@@ -19061,24 +19333,24 @@ function stripImportsAndExports(body) {
|
|
|
19061
19333
|
spans.push([start, end]);
|
|
19062
19334
|
continue;
|
|
19063
19335
|
}
|
|
19064
|
-
if (
|
|
19336
|
+
if (ts19.isExportDeclaration(stmt)) {
|
|
19065
19337
|
spans.push([stmt.getStart(sourceFile), stmt.getEnd()]);
|
|
19066
19338
|
continue;
|
|
19067
19339
|
}
|
|
19068
|
-
if (
|
|
19069
|
-
const exportKw = stmt.getChildren(sourceFile).find((c) => c.kind ===
|
|
19070
|
-
const defaultKw = stmt.getChildren(sourceFile).find((c) => c.kind ===
|
|
19071
|
-
const equalsKw = stmt.getChildren(sourceFile).find((c) => c.kind ===
|
|
19340
|
+
if (ts19.isExportAssignment(stmt)) {
|
|
19341
|
+
const exportKw = stmt.getChildren(sourceFile).find((c) => c.kind === ts19.SyntaxKind.ExportKeyword);
|
|
19342
|
+
const defaultKw = stmt.getChildren(sourceFile).find((c) => c.kind === ts19.SyntaxKind.DefaultKeyword);
|
|
19343
|
+
const equalsKw = stmt.getChildren(sourceFile).find((c) => c.kind === ts19.SyntaxKind.EqualsToken);
|
|
19072
19344
|
const start = exportKw?.getStart(sourceFile) ?? stmt.getStart(sourceFile);
|
|
19073
19345
|
const end = (defaultKw ?? equalsKw)?.getEnd() ?? exportKw?.getEnd() ?? stmt.getStart(sourceFile);
|
|
19074
19346
|
if (end > start) spans.push([start, end]);
|
|
19075
19347
|
continue;
|
|
19076
19348
|
}
|
|
19077
|
-
if (
|
|
19078
|
-
const mods =
|
|
19349
|
+
if (ts19.canHaveModifiers(stmt)) {
|
|
19350
|
+
const mods = ts19.getModifiers(stmt);
|
|
19079
19351
|
if (!mods) continue;
|
|
19080
19352
|
for (const mod of mods) {
|
|
19081
|
-
if (mod.kind ===
|
|
19353
|
+
if (mod.kind === ts19.SyntaxKind.ExportKeyword) {
|
|
19082
19354
|
const start = mod.getStart(sourceFile);
|
|
19083
19355
|
let end = mod.getEnd();
|
|
19084
19356
|
while (end < body.length && /\s/.test(body[end])) end++;
|
|
@@ -19187,48 +19459,48 @@ function buildDanglingReferenceMessage(binding, s) {
|
|
|
19187
19459
|
function isValueReference(id) {
|
|
19188
19460
|
const parent = id.parent;
|
|
19189
19461
|
if (!parent) return false;
|
|
19190
|
-
if (
|
|
19191
|
-
if (
|
|
19192
|
-
if ((
|
|
19462
|
+
if (ts19.isPropertyAccessExpression(parent) && parent.name === id) return false;
|
|
19463
|
+
if (ts19.isPropertyAssignment(parent) && parent.name === id) return false;
|
|
19464
|
+
if ((ts19.isMethodDeclaration(parent) || ts19.isGetAccessorDeclaration(parent) || ts19.isSetAccessorDeclaration(parent)) && parent.name === id) {
|
|
19193
19465
|
return false;
|
|
19194
19466
|
}
|
|
19195
|
-
if (
|
|
19196
|
-
if (
|
|
19197
|
-
if (
|
|
19198
|
-
if (
|
|
19199
|
-
if (
|
|
19200
|
-
if (
|
|
19201
|
-
if (
|
|
19202
|
-
if (
|
|
19203
|
-
if (
|
|
19204
|
-
if (
|
|
19205
|
-
if (
|
|
19206
|
-
if (
|
|
19207
|
-
if (
|
|
19208
|
-
if (
|
|
19467
|
+
if (ts19.isVariableDeclaration(parent) && parent.name === id) return false;
|
|
19468
|
+
if (ts19.isFunctionDeclaration(parent) && parent.name === id) return false;
|
|
19469
|
+
if (ts19.isFunctionExpression(parent) && parent.name === id) return false;
|
|
19470
|
+
if (ts19.isClassDeclaration(parent) && parent.name === id) return false;
|
|
19471
|
+
if (ts19.isClassExpression(parent) && parent.name === id) return false;
|
|
19472
|
+
if (ts19.isParameter(parent) && parent.name === id) return false;
|
|
19473
|
+
if (ts19.isBindingElement(parent) && (parent.name === id || parent.propertyName === id)) return false;
|
|
19474
|
+
if (ts19.isLabeledStatement(parent) && parent.label === id) return false;
|
|
19475
|
+
if (ts19.isBreakOrContinueStatement(parent) && parent.label === id) return false;
|
|
19476
|
+
if (ts19.isImportSpecifier(parent) && (parent.name === id || parent.propertyName === id)) return false;
|
|
19477
|
+
if (ts19.isExportSpecifier(parent) && (parent.name === id || parent.propertyName === id)) return false;
|
|
19478
|
+
if (ts19.isImportClause(parent) && parent.name === id) return false;
|
|
19479
|
+
if (ts19.isNamespaceImport(parent) && parent.name === id) return false;
|
|
19480
|
+
if (ts19.isQualifiedName(parent) && parent.right === id) return false;
|
|
19209
19481
|
return true;
|
|
19210
19482
|
}
|
|
19211
19483
|
function detectStrippedReferences(bundleSource, stripped) {
|
|
19212
19484
|
if (stripped.length === 0) return [];
|
|
19213
19485
|
let sf;
|
|
19214
19486
|
try {
|
|
19215
|
-
sf =
|
|
19487
|
+
sf = ts19.createSourceFile(
|
|
19216
19488
|
"bundle.js",
|
|
19217
19489
|
bundleSource,
|
|
19218
|
-
|
|
19490
|
+
ts19.ScriptTarget.Latest,
|
|
19219
19491
|
/*setParents*/
|
|
19220
19492
|
true,
|
|
19221
|
-
|
|
19493
|
+
ts19.ScriptKind.JS
|
|
19222
19494
|
);
|
|
19223
19495
|
} catch {
|
|
19224
19496
|
return [];
|
|
19225
19497
|
}
|
|
19226
19498
|
const firstReference = /* @__PURE__ */ new Map();
|
|
19227
19499
|
function visit3(node) {
|
|
19228
|
-
if (
|
|
19500
|
+
if (ts19.isIdentifier(node) && isValueReference(node)) {
|
|
19229
19501
|
if (!firstReference.has(node.text)) firstReference.set(node.text, node);
|
|
19230
19502
|
}
|
|
19231
|
-
|
|
19503
|
+
ts19.forEachChild(node, visit3);
|
|
19232
19504
|
}
|
|
19233
19505
|
visit3(sf);
|
|
19234
19506
|
const errors = [];
|
|
@@ -19258,18 +19530,18 @@ function detectStrippedReferences(bundleSource, stripped) {
|
|
|
19258
19530
|
return errors;
|
|
19259
19531
|
}
|
|
19260
19532
|
async function walkAndCollect(content, searchDirs, modules, visiting, loggingPath, stripped, stubDeps, nextId) {
|
|
19261
|
-
const sourceFile =
|
|
19533
|
+
const sourceFile = ts19.createSourceFile(
|
|
19262
19534
|
"walk.js",
|
|
19263
19535
|
content,
|
|
19264
|
-
|
|
19536
|
+
ts19.ScriptTarget.Latest,
|
|
19265
19537
|
/*setParents*/
|
|
19266
19538
|
false,
|
|
19267
|
-
|
|
19539
|
+
ts19.ScriptKind.JS
|
|
19268
19540
|
);
|
|
19269
19541
|
const sites = [];
|
|
19270
19542
|
for (const stmt of sourceFile.statements) {
|
|
19271
|
-
if (!
|
|
19272
|
-
if (!
|
|
19543
|
+
if (!ts19.isImportDeclaration(stmt)) continue;
|
|
19544
|
+
if (!ts19.isStringLiteral(stmt.moduleSpecifier)) continue;
|
|
19273
19545
|
const spec = stmt.moduleSpecifier.text;
|
|
19274
19546
|
if (!spec.startsWith("./") && !spec.startsWith("../")) continue;
|
|
19275
19547
|
const start = stmt.getStart(sourceFile);
|
|
@@ -19721,7 +19993,7 @@ var init_assets_ignore = __esm({
|
|
|
19721
19993
|
});
|
|
19722
19994
|
|
|
19723
19995
|
// src/lib/build.ts
|
|
19724
|
-
import
|
|
19996
|
+
import ts20 from "typescript";
|
|
19725
19997
|
import { mkdir, readdir, stat, unlink } from "node:fs/promises";
|
|
19726
19998
|
import { resolve as resolve6, basename, relative as relative2, dirname as dirname3, isAbsolute as isAbsolute2 } from "node:path";
|
|
19727
19999
|
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
@@ -20178,12 +20450,7 @@ async function build(config, options = {}) {
|
|
|
20178
20450
|
try {
|
|
20179
20451
|
let content = await readText(filePath);
|
|
20180
20452
|
const before = content;
|
|
20181
|
-
|
|
20182
|
-
content = content.replace(
|
|
20183
|
-
/from ['"]@barefootjs\/client(?:\/[^'"]*)?['"]/g,
|
|
20184
|
-
`from '${rel}'`
|
|
20185
|
-
);
|
|
20186
|
-
}
|
|
20453
|
+
content = rewriteBarefootClientSpecifiers(content, rel);
|
|
20187
20454
|
content = mergeDuplicateNamedImports(content);
|
|
20188
20455
|
if (content !== before && await writeIfChanged(filePath, content)) {
|
|
20189
20456
|
anyOutputChanged = true;
|
|
@@ -20272,7 +20539,7 @@ async function build(config, options = {}) {
|
|
|
20272
20539
|
};
|
|
20273
20540
|
}
|
|
20274
20541
|
function extractBareImports(code) {
|
|
20275
|
-
const { importedFiles } =
|
|
20542
|
+
const { importedFiles } = ts20.preProcessFile(code, true, true);
|
|
20276
20543
|
const specifiers = /* @__PURE__ */ new Set();
|
|
20277
20544
|
for (const { fileName } of importedFiles) {
|
|
20278
20545
|
if (!fileName.startsWith(".") && !fileName.startsWith("/") && !fileName.includes("://")) {
|
|
@@ -20337,13 +20604,82 @@ function effectiveOutName(tplPath, entryBaseNoExt) {
|
|
|
20337
20604
|
const entryDir = entryBaseNoExt.includes("/") ? entryBaseNoExt.slice(0, entryBaseNoExt.lastIndexOf("/")) : "";
|
|
20338
20605
|
return entryDir ? `${entryDir}/${bn}` : bn;
|
|
20339
20606
|
}
|
|
20607
|
+
function topLevelImportLines(content) {
|
|
20608
|
+
const lines = /* @__PURE__ */ new Set();
|
|
20609
|
+
const sourceFile = ts20.createSourceFile(
|
|
20610
|
+
"merge.js",
|
|
20611
|
+
content,
|
|
20612
|
+
ts20.ScriptTarget.Latest,
|
|
20613
|
+
/*setParentNodes*/
|
|
20614
|
+
true,
|
|
20615
|
+
ts20.ScriptKind.JS
|
|
20616
|
+
);
|
|
20617
|
+
for (const stmt of sourceFile.statements) {
|
|
20618
|
+
if (ts20.isImportDeclaration(stmt)) {
|
|
20619
|
+
const { line } = sourceFile.getLineAndCharacterOfPosition(stmt.getStart(sourceFile));
|
|
20620
|
+
lines.add(line);
|
|
20621
|
+
}
|
|
20622
|
+
}
|
|
20623
|
+
return lines;
|
|
20624
|
+
}
|
|
20625
|
+
function rewriteBarefootClientSpecifiers(content, rel) {
|
|
20626
|
+
if (!content.includes("@barefootjs/client")) return content;
|
|
20627
|
+
const sourceFile = ts20.createSourceFile(
|
|
20628
|
+
"client.js",
|
|
20629
|
+
content,
|
|
20630
|
+
ts20.ScriptTarget.Latest,
|
|
20631
|
+
/*setParentNodes*/
|
|
20632
|
+
true,
|
|
20633
|
+
ts20.ScriptKind.JS
|
|
20634
|
+
);
|
|
20635
|
+
const isBarefootClient = (s) => s === "@barefootjs/client" || s.startsWith("@barefootjs/client/");
|
|
20636
|
+
const spans = [];
|
|
20637
|
+
const visit3 = (node) => {
|
|
20638
|
+
if (ts20.isImportDeclaration(node) || ts20.isExportDeclaration(node)) {
|
|
20639
|
+
const ms = node.moduleSpecifier;
|
|
20640
|
+
if (ms && ts20.isStringLiteral(ms) && isBarefootClient(ms.text)) {
|
|
20641
|
+
spans.push([ms.getStart(sourceFile), ms.getEnd()]);
|
|
20642
|
+
}
|
|
20643
|
+
} else if (ts20.isCallExpression(node) && node.expression.kind === ts20.SyntaxKind.ImportKeyword) {
|
|
20644
|
+
const arg = node.arguments[0];
|
|
20645
|
+
if (arg && ts20.isStringLiteral(arg) && isBarefootClient(arg.text)) {
|
|
20646
|
+
spans.push([arg.getStart(sourceFile), arg.getEnd()]);
|
|
20647
|
+
}
|
|
20648
|
+
}
|
|
20649
|
+
ts20.forEachChild(node, visit3);
|
|
20650
|
+
};
|
|
20651
|
+
visit3(sourceFile);
|
|
20652
|
+
if (spans.length === 0) return content;
|
|
20653
|
+
spans.sort((a, b) => b[0] - a[0]);
|
|
20654
|
+
let out = content;
|
|
20655
|
+
for (const [start, end] of spans) {
|
|
20656
|
+
out = out.slice(0, start) + `'${rel}'` + out.slice(end);
|
|
20657
|
+
}
|
|
20658
|
+
return out;
|
|
20659
|
+
}
|
|
20340
20660
|
function mergeDuplicateNamedImports(content) {
|
|
20341
20661
|
const lines = content.split("\n");
|
|
20342
20662
|
const namedImportRe = /^import\s+\{\s*([^}]+)\s*\}\s+from\s+(['"])([^'"]+)\2\s*;?\s*$/;
|
|
20343
20663
|
const bySource = /* @__PURE__ */ new Map();
|
|
20344
20664
|
const dropIndices = /* @__PURE__ */ new Set();
|
|
20345
20665
|
let changed = false;
|
|
20666
|
+
{
|
|
20667
|
+
const seen = /* @__PURE__ */ new Set();
|
|
20668
|
+
let possibleDuplicate = false;
|
|
20669
|
+
for (const line of lines) {
|
|
20670
|
+
const m = line.match(namedImportRe);
|
|
20671
|
+
if (!m) continue;
|
|
20672
|
+
if (seen.has(m[3])) {
|
|
20673
|
+
possibleDuplicate = true;
|
|
20674
|
+
break;
|
|
20675
|
+
}
|
|
20676
|
+
seen.add(m[3]);
|
|
20677
|
+
}
|
|
20678
|
+
if (!possibleDuplicate) return content;
|
|
20679
|
+
}
|
|
20680
|
+
const realImportLines = topLevelImportLines(content);
|
|
20346
20681
|
lines.forEach((line, idx) => {
|
|
20682
|
+
if (!realImportLines.has(idx)) return;
|
|
20347
20683
|
const m = line.match(namedImportRe);
|
|
20348
20684
|
if (!m) return;
|
|
20349
20685
|
const names = m[1].split(",").map((s) => s.trim()).filter(Boolean);
|
|
@@ -22576,7 +22912,7 @@ var bfGoSource, streamingGoSource, barefootPmSource, barefootPluginPmSource, bar
|
|
|
22576
22912
|
var init_runtimes_generated = __esm({
|
|
22577
22913
|
"src/lib/adapters/runtimes.generated.ts"() {
|
|
22578
22914
|
"use strict";
|
|
22579
|
-
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 "os"\n "reflect"\n "sort"\n "strconv"\n "strings"\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\n // String\n "bf_lower": Lower,\n "bf_upper": Upper,\n "bf_trim": Trim,\n "bf_contains": Contains,\n "bf_join": Join,\n "bf_string": String,\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\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_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\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 // 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// 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// 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// 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// 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// 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// 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// Every returns true if all items have the specified field set to true.\n// Mirrors JavaScript\'s Array.prototype.every(item => item.field).\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\n capitalizedField := capitalize(field)\n for i := 0; i < v.Len(); i++ {\n item := v.Index(i)\n if item.Kind() == reflect.Interface {\n item = item.Elem()\n }\n if item.Kind() == reflect.Ptr {\n item = item.Elem()\n }\n if item.Kind() != reflect.Struct {\n continue\n }\n\n fieldVal := item.FieldByName(capitalizedField)\n if !fieldVal.IsValid() {\n return false\n }\n if fieldVal.Kind() == reflect.Bool && !fieldVal.Bool() {\n return false\n }\n }\n return true\n}\n\n// Some returns true if at least one item has the specified field set to true.\n// Mirrors 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\n capitalizedField := capitalize(field)\n for i := 0; i < v.Len(); i++ {\n item := v.Index(i)\n if item.Kind() == reflect.Interface {\n item = item.Elem()\n }\n if item.Kind() == reflect.Ptr {\n item = item.Elem()\n }\n if item.Kind() != reflect.Struct {\n continue\n }\n\n fieldVal := item.FieldByName(capitalizedField)\n if fieldVal.IsValid() && fieldVal.Kind() == reflect.Bool && fieldVal.Bool() {\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\n capitalizedField := capitalize(field)\n var result []any\n\n for i := 0; i < v.Len(); i++ {\n item := v.Index(i)\n if item.Kind() == reflect.Interface {\n item = item.Elem()\n }\n if item.Kind() == reflect.Ptr {\n item = item.Elem()\n }\n if item.Kind() != reflect.Struct {\n continue\n }\n\n fieldVal := item.FieldByName(capitalizedField)\n if !fieldVal.IsValid() {\n continue\n }\n\n // Compare field value with target value\n if reflect.DeepEqual(fieldVal.Interface(), value) {\n result = append(result, v.Index(i).Interface())\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\n capitalizedField := capitalize(field)\n for i := 0; i < v.Len(); i++ {\n item := v.Index(i)\n if item.Kind() == reflect.Interface {\n item = item.Elem()\n }\n if item.Kind() == reflect.Ptr {\n item = item.Elem()\n }\n if item.Kind() != reflect.Struct {\n continue\n }\n\n fieldVal := item.FieldByName(capitalizedField)\n if !fieldVal.IsValid() {\n continue\n }\n\n if reflect.DeepEqual(fieldVal.Interface(), value) {\n return v.Index(i).Interface()\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\n capitalizedField := capitalize(field)\n for i := 0; i < v.Len(); i++ {\n item := v.Index(i)\n if item.Kind() == reflect.Interface {\n item = item.Elem()\n }\n if item.Kind() == reflect.Ptr {\n item = item.Elem()\n }\n if item.Kind() != reflect.Struct {\n continue\n }\n\n fieldVal := item.FieldByName(capitalizedField)\n if !fieldVal.IsValid() {\n continue\n }\n\n if reflect.DeepEqual(fieldVal.Interface(), 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\n capitalizedField := capitalize(field)\n for i := v.Len() - 1; i >= 0; i-- {\n item := v.Index(i)\n if item.Kind() == reflect.Interface {\n if item.IsNil() {\n continue\n }\n item = item.Elem()\n }\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 continue\n }\n\n fieldVal := item.FieldByName(capitalizedField)\n if !fieldVal.IsValid() {\n continue\n }\n\n if reflect.DeepEqual(fieldVal.Interface(), value) {\n return v.Index(i).Interface()\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\n capitalizedField := capitalize(field)\n for i := v.Len() - 1; i >= 0; i-- {\n item := v.Index(i)\n if item.Kind() == reflect.Interface {\n if item.IsNil() {\n continue\n }\n item = item.Elem()\n }\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 continue\n }\n\n fieldVal := item.FieldByName(capitalizedField)\n if !fieldVal.IsValid() {\n continue\n }\n\n if reflect.DeepEqual(fieldVal.Interface(), 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 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 return nil\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// =============================================================================\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// 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\nfunc (r *Renderer) Render(opts RenderOptions) string {\n // Create script collector and inject into props\n scriptCollector := NewScriptCollector()\n setScriptsField(opts.Props, scriptCollector)\n\n // Create portal collector and inject into props\n portalCollector := NewPortalCollector()\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 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 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 // 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: template.HTML(componentBuf.String()),\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// 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// 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\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// =============================================================================\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';
|
|
22915
|
+
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 "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\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 // 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\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_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\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 // 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// 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// 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// 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// 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// 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// Every returns true if all items have the specified field set to true.\n// Mirrors JavaScript\'s Array.prototype.every(item => item.field).\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\n capitalizedField := capitalize(field)\n for i := 0; i < v.Len(); i++ {\n item := v.Index(i)\n if item.Kind() == reflect.Interface {\n item = item.Elem()\n }\n if item.Kind() == reflect.Ptr {\n item = item.Elem()\n }\n if item.Kind() != reflect.Struct {\n continue\n }\n\n fieldVal := item.FieldByName(capitalizedField)\n if !fieldVal.IsValid() {\n return false\n }\n if fieldVal.Kind() == reflect.Bool && !fieldVal.Bool() {\n return false\n }\n }\n return true\n}\n\n// Some returns true if at least one item has the specified field set to true.\n// Mirrors 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\n capitalizedField := capitalize(field)\n for i := 0; i < v.Len(); i++ {\n item := v.Index(i)\n if item.Kind() == reflect.Interface {\n item = item.Elem()\n }\n if item.Kind() == reflect.Ptr {\n item = item.Elem()\n }\n if item.Kind() != reflect.Struct {\n continue\n }\n\n fieldVal := item.FieldByName(capitalizedField)\n if fieldVal.IsValid() && fieldVal.Kind() == reflect.Bool && fieldVal.Bool() {\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\n capitalizedField := capitalize(field)\n var result []any\n\n for i := 0; i < v.Len(); i++ {\n item := v.Index(i)\n if item.Kind() == reflect.Interface {\n item = item.Elem()\n }\n if item.Kind() == reflect.Ptr {\n item = item.Elem()\n }\n if item.Kind() != reflect.Struct {\n continue\n }\n\n fieldVal := item.FieldByName(capitalizedField)\n if !fieldVal.IsValid() {\n continue\n }\n\n // Compare field value with target value\n if reflect.DeepEqual(fieldVal.Interface(), value) {\n result = append(result, v.Index(i).Interface())\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\n capitalizedField := capitalize(field)\n for i := 0; i < v.Len(); i++ {\n item := v.Index(i)\n if item.Kind() == reflect.Interface {\n item = item.Elem()\n }\n if item.Kind() == reflect.Ptr {\n item = item.Elem()\n }\n if item.Kind() != reflect.Struct {\n continue\n }\n\n fieldVal := item.FieldByName(capitalizedField)\n if !fieldVal.IsValid() {\n continue\n }\n\n if reflect.DeepEqual(fieldVal.Interface(), value) {\n return v.Index(i).Interface()\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\n capitalizedField := capitalize(field)\n for i := 0; i < v.Len(); i++ {\n item := v.Index(i)\n if item.Kind() == reflect.Interface {\n item = item.Elem()\n }\n if item.Kind() == reflect.Ptr {\n item = item.Elem()\n }\n if item.Kind() != reflect.Struct {\n continue\n }\n\n fieldVal := item.FieldByName(capitalizedField)\n if !fieldVal.IsValid() {\n continue\n }\n\n if reflect.DeepEqual(fieldVal.Interface(), 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\n capitalizedField := capitalize(field)\n for i := v.Len() - 1; i >= 0; i-- {\n item := v.Index(i)\n if item.Kind() == reflect.Interface {\n if item.IsNil() {\n continue\n }\n item = item.Elem()\n }\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 continue\n }\n\n fieldVal := item.FieldByName(capitalizedField)\n if !fieldVal.IsValid() {\n continue\n }\n\n if reflect.DeepEqual(fieldVal.Interface(), value) {\n return v.Index(i).Interface()\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\n capitalizedField := capitalize(field)\n for i := v.Len() - 1; i >= 0; i-- {\n item := v.Index(i)\n if item.Kind() == reflect.Interface {\n if item.IsNil() {\n continue\n }\n item = item.Elem()\n }\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 continue\n }\n\n fieldVal := item.FieldByName(capitalizedField)\n if !fieldVal.IsValid() {\n continue\n }\n\n if reflect.DeepEqual(fieldVal.Interface(), 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 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 return nil\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// =============================================================================\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// 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\nfunc (r *Renderer) Render(opts RenderOptions) string {\n // Create script collector and inject into props\n scriptCollector := NewScriptCollector()\n setScriptsField(opts.Props, scriptCollector)\n\n // Create portal collector and inject into props\n portalCollector := NewPortalCollector()\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 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 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 // 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: template.HTML(componentBuf.String()),\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// 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// 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\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// =============================================================================\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';
|
|
22580
22916
|
streamingGoSource = `// Package bf \u2014 Out-of-Order Streaming SSR helpers
|
|
22581
22917
|
//
|
|
22582
22918
|
// Provides StreamRenderer for progressive page rendering using HTTP
|
|
@@ -22783,7 +23119,7 @@ func StreamingFuncMap() template.FuncMap {
|
|
|
22783
23119
|
}
|
|
22784
23120
|
}
|
|
22785
23121
|
`;
|
|
22786
|
-
barefootPmSource = "package BarefootJS;\nuse Mojo::Base -base, -signatures;\n\nuse Mojo::ByteStream qw(b);\nuse Mojo::JSON qw(encode_json to_json);\nuse POSIX ();\nuse Scalar::Util qw(looks_like_number weaken);\n\nhas 'c'; # Mojolicious controller\nhas 'config'; # Plugin config\n\n# Internal state\nhas '_scripts' => sub { [] };\nhas '_script_seen' => sub { {} };\nhas '_scope_id';\nhas '_is_child' => 0;\nhas '_bf_parent'; # Host scope id when this scope is a slot-attached child\nhas '_bf_mount'; # Slot id in host\nhas '_props';\n\nsub new ($class, $c, $config = {}) {\n return $class->SUPER::new(\n c => $c,\n config => $config,\n );\n}\n\n# ---------------------------------------------------------------------------\n# Scope & Props\n# ---------------------------------------------------------------------------\n\nsub scope_attr ($self) {\n # bf-s is the addressable scope id only (#1249).\n return $self->_scope_id // '';\n}\n\n# Emits `bf-h=\"<host>\" bf-m=\"<slot>\" bf-r=\"\"` conditionally.\n# See spec/compiler.md \"Slot identity\".\nsub hydration_attrs ($self) {\n my @parts;\n my $host = $self->_bf_parent;\n my $mount = $self->_bf_mount;\n if (defined $host && length $host) {\n my $h = $host =~ s/\"/"/gr;\n push @parts, qq{bf-h=\"$h\"};\n }\n if (defined $mount && length $mount) {\n my $m = $mount =~ s/\"/"/gr;\n push @parts, qq{bf-m=\"$m\"};\n }\n unless ($self->_is_child) {\n push @parts, q{bf-r=\"\"};\n }\n return join(' ', @parts);\n}\n\nsub props_attr ($self) {\n my $props = $self->_props;\n return '' unless $props && %$props;\n # to_json returns a character string (not bytes) for safe embedding in templates\n my $json = to_json($props);\n return qq{ bf-p='$json'};\n}\n\n# ---------------------------------------------------------------------------\n# Comment Markers\n# ---------------------------------------------------------------------------\n\nsub comment ($self, $text) {\n return \"<!--bf-$text-->\";\n}\n\n# ---------------------------------------------------------------------------\n# JS-equivalent value stringification\n# ---------------------------------------------------------------------------\n\n# Map a Perl boolean-shaped value to the JS `String(bool)` form.\n# Used by the Mojo adapter when emitting reactive attribute bindings\n# whose JS source `isBooleanResultExpr` classified as boolean \u2014\n# a comparison (`count() > 0`), a logical negation (`!ok()`), or a\n# literal `true` / `false`. Perl's auto-stringification of those\n# expressions yields `''` / `1`; Hono and Go emit `'false'` / `'true'`.\n# Centralising the bool \u2192 string mapping here keeps the contract\n# testable and the template-emit syntax tidy\n# (`<%= bf->bool_str(...) %>` vs an inline ternary).\n#\n# Contract is boolean-only: callers must have classified the\n# expression as boolean-result before routing through this helper.\n# Non-boolean values reaching here will be Perl-truthy-coerced to\n# 'true' / 'false', which is generally wrong \u2014 non-boolean attribute\n# bindings stay on the plain `<%= expr %>` emit path and never reach\n# this function.\nsub bool_str ($self, $value) {\n return $value ? 'true' : 'false';\n}\n\nsub text_start ($self, $slot_id) {\n return \"<!--bf:$slot_id-->\";\n}\n\nsub text_end ($self) {\n return \"<!--/-->\";\n}\n\n# See spec/compiler.md \"Slot identity\" for the comment-scope wire format.\nsub scope_comment ($self) {\n my $scope_id = $self->_scope_id // '';\n my $host_segment = '';\n my $host = $self->_bf_parent;\n my $mount = $self->_bf_mount;\n if (defined $host && length $host) {\n $host_segment = \"|h=$host|m=\" . ($mount // '');\n }\n my $props_json = '';\n if ($self->_props && %{$self->_props}) {\n $props_json = '|' . to_json($self->_props);\n }\n return \"<!--bf-scope:$scope_id$host_segment$props_json-->\";\n}\n\n# ---------------------------------------------------------------------------\n# Script Registration\n# ---------------------------------------------------------------------------\n\nsub register_script ($self, $path) {\n return if $self->_script_seen->{$path};\n $self->_script_seen->{$path} = 1;\n push @{$self->_scripts}, $path;\n}\n\n# ---------------------------------------------------------------------------\n# Child Component Rendering\n# ---------------------------------------------------------------------------\n\nhas '_child_renderers' => sub { {} };\n\nsub register_child_renderer ($self, $name, $renderer) {\n $self->_child_renderers->{$name} = $renderer;\n}\n\nsub render_child ($self, $name, %props) {\n my $renderer = $self->_child_renderers->{$name};\n die \"No renderer registered for child component '$name'\" unless $renderer;\n # JSX children come in via Mojo `begin %>...<% end` capture, which\n # produces a CODE ref returning a Mojo::ByteStream. Materialize it\n # before handing the props to the child renderer so the child\n # template sees `$children` as already-rendered HTML.\n $props{children} = $props{children}->() if ref($props{children}) eq 'CODE';\n return $renderer->(\\%props);\n}\n\n# ---------------------------------------------------------------------------\n# Bulk registration from build manifest\n# ---------------------------------------------------------------------------\n#\n# `bf build` emits dist/templates/manifest.json describing every\n# component the page might invoke (Counter, ui/button/index, ...).\n# This helper walks that manifest and registers one child renderer per\n# UI registry entry \u2014 the path shape `ui/<name>/index` maps to the\n# `<name>` slot key Counter.html.ep and friends use via\n# `<%= bf->render_child('<name>', ...) %>`.\n#\n# Each manifest entry carries an `ssrDefaults` hash derived statically\n# from the component's JSX (prop destructure defaults + signal /\n# memo initial values, see packages/jsx/src/ssr-defaults.ts). The\n# child renderer seeds every template variable from that hash,\n# preferring the caller's matching prop where one exists. This\n# replaces the per-component `signal_init` callback that every\n# scaffold's `app.pl` used to hand-roll for items 1/3 of issue #1416.\n#\n# `signal_init` remains as an opt-in override for cases the static\n# extractor can't see through (e.g. signal initial values that\n# reference imported helpers). When supplied for a given slot key\n# it takes precedence over the manifest's `ssrDefaults` for that\n# child, allowing callers to mix manual overrides with auto-derived\n# defaults for siblings.\nsub register_components_from_manifest ($self, $manifest, %opts) {\n my $c = $self->c;\n my $signal_inits = $opts{signal_init} // {};\n my $parent_scope = $self->_scope_id;\n weaken(my $parent = $self);\n\n for my $entry_name (keys %$manifest) {\n # `__barefoot__` is the runtime entry, not a component.\n next if $entry_name eq '__barefoot__';\n # Only UI registry components (path shape `ui/<name>/index`)\n # become child renderers; top-level page components are the\n # render target rather than a child.\n next unless $entry_name =~ m{^ui/([^/]+)/index$};\n my $slot_key = $1;\n my $marked = $manifest->{$entry_name}{markedTemplate} // '';\n next unless $marked;\n # `templates/ui/button/index.html.ep` \u2192 `ui/button/index`\n my $template_name = $marked;\n $template_name =~ s{^templates/}{};\n $template_name =~ s{\\.html\\.ep$}{};\n\n my $signal_init = $signal_inits->{$slot_key};\n my $manifest_defaults = $manifest->{$entry_name}{ssrDefaults};\n $self->register_child_renderer($slot_key, sub {\n my ($props) = @_;\n my $child_bf = BarefootJS->new($c, {});\n my $slot_id = delete $props->{_bf_slot};\n $child_bf->_scope_id(\n $slot_id ? $parent_scope . '_' . $slot_id\n : $template_name . '_' . substr(rand() =~ s/^0\\.//r, 0, 6)\n );\n $child_bf->_is_child(1);\n # (#1249) Slot identity: host scope + slot id. Emitted as\n # bf-h / bf-m attributes by hydration_attrs.\n if ($slot_id) {\n $child_bf->_bf_parent($parent_scope);\n $child_bf->_bf_mount($slot_id);\n }\n $child_bf->_scripts($parent->_scripts);\n $child_bf->_script_seen($parent->_script_seen);\n\n my %extra;\n if ($signal_init) {\n %extra = $signal_init->($props);\n } elsif ($manifest_defaults) {\n %extra = _derive_stash_from_defaults($manifest_defaults, $props);\n }\n\n my $prev = $c->stash->{'bf.instance'};\n $c->stash->{'bf.instance'} = $child_bf;\n my $html = $c->render_to_string(\n template => $template_name, %$props, %extra,\n );\n $c->stash->{'bf.instance'} = $prev;\n chomp $html;\n return $html;\n });\n }\n}\n\n# Derive template-stash kvs from a manifest entry's `ssrDefaults`\n# section. Each entry shape:\n# { value => <static-fallback>, propName => <prop>, isRestProps => bool }\n# For `isRestProps`, the rest bag passes through unchanged (or the\n# static `{}` if the caller didn't supply one). For ordinary entries\n# the caller's `$props->{propName}` wins when defined, otherwise the\n# static `value` does. `propName`-less entries (signal / memo locals)\n# always use the static value \u2014 the caller cannot override them.\nsub _derive_stash_from_defaults ($defaults, $props) {\n my %extra;\n for my $name (keys %$defaults) {\n my $d = $defaults->{$name};\n if (ref($d) ne 'HASH') {\n $extra{$name} = $d;\n next;\n }\n if ($d->{isRestProps}) {\n $extra{$name} = exists $props->{$name} ? $props->{$name} : $d->{value};\n next;\n }\n my $prop_name = $d->{propName};\n if (defined $prop_name && exists $props->{$prop_name} && defined $props->{$prop_name}) {\n $extra{$name} = $props->{$prop_name};\n } else {\n $extra{$name} = $d->{value};\n }\n }\n return %extra;\n}\n\n# ---------------------------------------------------------------------------\n# Script Output\n# ---------------------------------------------------------------------------\n\nsub scripts ($self) {\n my @tags;\n for my $path (@{$self->_scripts}) {\n push @tags, qq{<script type=\"module\" src=\"$path\"></script>};\n }\n return join(\"\\n\", @tags);\n}\n\n# ---------------------------------------------------------------------------\n# Streaming SSR (Out-of-Order)\n# ---------------------------------------------------------------------------\n\nsub streaming_bootstrap ($self) {\n return q{<script>(function(){function s(id){var a=document.querySelector('[bf-async=\"'+id+'\"]');var t=document.querySelector('template[bf-async-resolve=\"'+id+'\"]');if(!a||!t)return;a.replaceChildren(t.content.cloneNode(true));a.removeAttribute('bf-async');t.remove();requestAnimationFrame(function(){if(window.__bf_hydrate)window.__bf_hydrate()})};window.__bf_swap=s})()</script>};\n}\n\nsub async_boundary ($self, $id, $fallback_html) {\n # The fallback comes in via Mojo `begin %>...<% end` capture (see\n # MojoAdapter::renderAsync), which produces a CODE ref returning a\n # Mojo::ByteStream. Materialize it so the rendered HTML embeds in\n # the placeholder rather than the CODE ref's stringification.\n $fallback_html = $fallback_html->() if ref($fallback_html) eq 'CODE';\n return qq{<div bf-async=\"$id\">$fallback_html</div>};\n}\n\nsub async_resolve ($self, $id, $content_html) {\n return qq{<template bf-async-resolve=\"$id\">$content_html</template><script>__bf_swap(\"$id\")</script>};\n}\n\n# ---------------------------------------------------------------------------\n# JS-compat callees (#1189) \u2014 invoked from generated Mojo templates as\n# <%= bf->json($val) %>, <%= bf->floor($val) %>, etc. The MojoAdapter's\n# `templatePrimitives` registry emits these helper calls in place of the\n# corresponding JS callees (`JSON.stringify`, `Math.floor`, \u2026) so the SSR\n# template can render value-equivalent output without a JS engine.\n#\n# Failure policy mirrors the Go adapter (#1188): user-data marshalling\n# (json) bubbles errors so Mojolicious aborts loudly on cycles /\n# unsupported values rather than silently producing an empty payload.\n# Numeric coercion follows JS semantics (NaN propagates as the special\n# string 'NaN'; non-numeric input returns 'NaN' rather than 0). Strings\n# always coerce to a string representation.\n# ---------------------------------------------------------------------------\n\nsub json ($self, $value) {\n # Mojo::JSON::to_json returns a character string (not bytes), suitable\n # for embedding in HTML output via Mojo::ByteStream / `<%==`.\n #\n # Documented divergence from JS: JS distinguishes `null` (renders as\n # \"null\") from `undefined` (`JSON.stringify(undefined)` returns the\n # JS value `undefined`, not a string). Perl has no such distinction\n # \u2014 both map to `undef`. We choose the `null` rendering for SSR\n # ergonomics: an unset prop becomes the string \"null\" rather than\n # the literal text \"undefined\" or an empty attribute. Matches the\n # `null` case of JS exactly; diverges from the `undefined` case.\n return to_json($value);\n}\n\nsub string ($self, $value) {\n # JS `String(v)` mirror. `undef` renders as the empty string here so\n # an unset prop doesn't surface as a literal \"undefined\" / \"null\"\n # in user-facing HTML \u2014 same divergence the Go adapter documents\n # for `bf_string`.\n return defined $value ? \"$value\" : '';\n}\n\nsub number ($self, $value) {\n # JS `Number(v)` mirror. Numeric coerces via Perl's implicit\n # numeric context; non-numeric / undef yield real numeric NaN\n # (`'nan' + 0`) so downstream arithmetic propagates correctly\n # (`Math.floor(NaN) === NaN`). Returning the literal string\n # \"NaN\" would conflate the user-passing-the-string-\"NaN\" case\n # with the parse-failure case, and break NaN detection in\n # downstream helpers.\n return 0 + 'nan' unless defined $value;\n return $value + 0 if looks_like_number($value);\n return 0 + 'nan';\n}\n\n# NaN is the only float for which `$x != $x` holds. Used as the\n# portable sentinel check in floor/ceil/round.\nsub _is_nan { my $n = shift; return $n != $n }\n\nsub floor ($self, $value) {\n my $n = $self->number($value);\n return $n if _is_nan($n);\n return POSIX::floor($n);\n}\n\nsub ceil ($self, $value) {\n my $n = $self->number($value);\n return $n if _is_nan($n);\n return POSIX::ceil($n);\n}\n\nsub round ($self, $value) {\n my $n = $self->number($value);\n return $n if _is_nan($n);\n # POSIX has no `round`. JS `Math.round` rounds half toward\n # +Infinity (so `Math.round(-1.5) === -1`, not -2). `floor(n\n # + 0.5)` reproduces that for both signs.\n return POSIX::floor($n + 0.5);\n}\n\n# ---------------------------------------------------------------------------\n# Array / String method helpers (#1448 Tier A)\n# ---------------------------------------------------------------------------\n#\n# `Array.prototype.includes(x)` and `String.prototype.includes(sub)`\n# share a method name in JS; the JSX parser can't tell the two\n# receiver shapes apart without TS type inference, so both lower to\n# the same IR node (`array-method` / method `includes`). This helper\n# dispatches at the Perl level via `ref()`:\n# - ARRAY ref: scan elements with `eq`; one defined-vs-undef\n# hop matches JS's `===` for null/undefined.\n# - scalar: `index($recv, $sub) != -1`, with both args\n# coerced through `// ''` so an undef receiver /\n# needle doesn't trip Perl's substr warning.\n# Anything else (HASH ref, code ref) returns false \u2014 matches the\n# JS semantic where `.includes` is only defined on Array /\n# TypedArray / String.\n\nsub includes ($self, $recv, $elem) {\n if (ref($recv) eq 'ARRAY') {\n for my $item (@$recv) {\n if (!defined $item) {\n return 1 if !defined $elem;\n next;\n }\n return 1 if defined $elem && $item eq $elem;\n }\n return 0;\n }\n return 0 if ref($recv);\n return index($recv // '', $elem // '') != -1 ? 1 : 0;\n}\n\n# `Array.prototype.indexOf(x)` / `Array.prototype.lastIndexOf(x)`\n# value-equality search (#1448 Tier A). Returns the 0-based position\n# of the first / last matching element, or -1 if not found.\n# Non-array receivers return -1 \u2014 matches the JS semantic that\n# `.indexOf` / `.lastIndexOf` are only defined on Array / TypedArray.\n# (The string-position `indexOf` form isn't in Tier A; if it lands\n# later the helper can grow a ref()-dispatch branch like `includes`.)\n\nsub _array_index_of ($recv, $elem, $reverse) {\n return -1 unless ref($recv) eq 'ARRAY';\n my @indices = $reverse ? (reverse 0 .. $#{$recv}) : (0 .. $#{$recv});\n for my $i (@indices) {\n my $item = $recv->[$i];\n if (!defined $item) {\n return $i if !defined $elem;\n next;\n }\n return $i if defined $elem && $item eq $elem;\n }\n return -1;\n}\n\nsub index_of ($self, $recv, $elem) {\n return _array_index_of($recv, $elem, 0);\n}\n\nsub last_index_of ($self, $recv, $elem) {\n return _array_index_of($recv, $elem, 1);\n}\n\n# `Array.prototype.at(i)` \u2014 supports negative indices (`.at(-1)` is\n# the last element); out-of-bounds returns undef (which Mojo's\n# auto-escape renders as the empty string, matching JS's `undefined`).\n# Non-array receivers return undef. Matches the Go `bf_at` arithmetic\n# (`length + i` for i < 0) so adapter output stays symmetric.\n\nsub at ($self, $recv, $i) {\n return undef unless ref($recv) eq 'ARRAY';\n return undef if !defined $i;\n my $len = scalar @$recv;\n return undef if $len == 0;\n my $idx = $i < 0 ? $len + $i : $i;\n return undef if $idx < 0 || $idx >= $len;\n return $recv->[$idx];\n}\n\n# `Array.prototype.concat(other)` \u2014 merges two arrays in order\n# into a new ARRAY ref. Non-array operands collapse to empty\n# (matches the Go `bf_concat` semantic so cross-adapter output\n# stays symmetric; differs from JS where a non-Array argument\n# with `Symbol.isConcatSpreadable` would be spread, a behaviour\n# the template-language path never observes).\n\nsub concat ($self, $a, $b) {\n my @out;\n push @out, @$a if ref($a) eq 'ARRAY';\n push @out, @$b if ref($b) eq 'ARRAY';\n return \\@out;\n}\n\n# `Array.prototype.slice(start, end?)` \u2014 carves out a sub-range\n# into a new ARRAY ref. Mirrors the Go `bf_slice` arithmetic so\n# adapter output stays symmetric:\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\n# - end undef \u2192 \"to length\"\n# Non-array receivers return an empty ARRAY ref.\n\nsub slice ($self, $recv, $start, $end) {\n return [] unless ref($recv) eq 'ARRAY';\n my $len = scalar @$recv;\n return [] if $len == 0;\n\n my $s = $start // 0;\n $s = $len + $s if $s < 0;\n $s = 0 if $s < 0;\n $s = $len if $s > $len;\n\n my $e = defined $end ? $end : $len;\n $e = $len + $e if $e < 0;\n $e = 0 if $e < 0;\n $e = $len if $e > $len;\n\n return [] if $s >= $e;\n return [ @{$recv}[$s .. $e - 1] ];\n}\n\n# `Array.prototype.reverse()` / `Array.prototype.toReversed()` \u2014\n# both shapes share this lowering. SSR templates render a snapshot\n# of state, so JS's mutate-receiver (`reverse`) vs\n# return-new-array (`toReversed`) distinction has no template-\n# level meaning. Always returns a new ARRAY ref to keep callers\n# safe from accidental aliasing. Non-array receivers return an\n# empty ARRAY ref.\n\nsub reverse ($self, $recv) {\n return [] unless ref($recv) eq 'ARRAY';\n return [ reverse @$recv ];\n}\n\n# `String.prototype.trim()` \u2014 strip leading + trailing whitespace.\n# JS's `String.prototype.trim` matches `\\s` in the Unicode sense\n# (any whitespace including non-breaking space U+00A0); Perl's `\\s`\n# inside a regex with `/u` flag is the same. Undef receivers return\n# the empty string (matches JS's `String(undefined).trim()` which\n# would be \"undefined\" \u2192 \"undefined\", but in our template context\n# undef commonly means \"missing prop\"; rendering the empty string\n# is the safer choice and mirrors the JS-compat divergence we\n# already document for `bf->string(undef) === \"\"`).\n\nsub trim ($self, $recv) {\n return '' unless defined $recv;\n return '' if ref($recv);\n my $s = \"$recv\";\n $s =~ s/^\\s+|\\s+$//gu;\n return $s;\n}\n\n# `Array.prototype.sort(cmp)` / `Array.prototype.toSorted(cmp)`\n# lowering (#1448 Tier B). Non-mutating \u2014 JS's mutate-vs-new\n# distinction is moot in SSR template context.\n#\n# Opts hash-ref. The compiler emits a `keys` list of per-key hashes\n# in priority order; each hash carries:\n#\n# key_kind => 'self' | 'field'\n# key => '' when key_kind eq 'self'; field name verbatim\n# from the comparator AST (e.g. 'price', 'createdAt')\n# when key_kind eq 'field' \u2014 no case normalisation\n# applied. Perl hash lookups are case-sensitive so\n# the key here must match the actual hash key the\n# user populated.\n# compare_type => 'numeric' | 'string' | 'auto'\n# direction => 'asc' | 'desc'\n#\n# Accepted comparator catalogue (gated upstream at parse time \u2014\n# anything outside refuses with BF101 before reaching this helper):\n#\n# (a,b) => a.f - b.f \u2192 field, numeric\n# (a,b) => a - b \u2192 self, numeric\n# (a,b) => a[.f].localeCompare(b[.f]) \u2192 field|self, string\n# (a,b) => a.f > b.f ? 1 : -1 \u2192 field|self, auto\n# any of the above ||-chained \u2192 multi-key tie-breaks\n# (and reversed-operand variants for `desc`).\n#\n# `auto` (relational-ternary lowering) compares numerically when both\n# keys `looks_like_number`, else lexically \u2014 Go's `bf_sort` applies the\n# same rule so the two template adapters stay byte-equal.\n#\n# A future `nulls => 'first' | 'last'` knob can land per key without\n# churn \u2014 the opts hash is the right place to grow.\n\nsub sort ($self, $recv, $opts = {}) {\n return [] unless ref($recv) eq 'ARRAY';\n\n # Normalise the per-key specs (priority order, length >= 1).\n my @spec = map {\n {\n key_kind => $_->{key_kind} // 'self',\n key => $_->{key} // '',\n compare_type => $_->{compare_type} // 'numeric',\n direction => $_->{direction} // 'asc',\n }\n } @{ $opts->{keys} // [] };\n return [ @$recv ] unless @spec;\n\n # Schwartzian transform: project each item to all its sort keys\n # once, then compare projected keys. Cheaper than re-resolving the\n # field accessors inside every comparison for non-trivial arrays.\n my @keyed = map {\n my $item = $_;\n my @ks = map {\n $_->{key_kind} eq 'field' && ref($item) eq 'HASH' ? $item->{ $_->{key} } : $item;\n } @spec;\n [ \\@ks, $item ];\n } @$recv;\n\n my $cmp = sub {\n for my $i (0 .. $#spec) {\n my $sp = $spec[$i];\n my $c = _compare_sort_key($a->[0][$i], $b->[0][$i], $sp->{compare_type});\n next if $c == 0; # tie on this key \u2014 try the next\n return $sp->{direction} eq 'desc' ? -$c : $c;\n }\n return 0;\n };\n\n my @sorted = sort $cmp @keyed;\n return [ map { $_->[1] } @sorted ];\n}\n\n# Compare two projected keys, ascending orientation (-1 / 0 / 1); the\n# caller negates for 'desc'. 'auto' compares numerically when both\n# keys look like numbers, else lexically (matches Go's `bf_sort`).\n# undef coalesces to '' / 0 so the order stays total without warnings.\nsub _compare_sort_key ($av, $bv, $compare_type) {\n if ($compare_type eq 'string') {\n return ($av // '') cmp ($bv // '');\n }\n if ($compare_type eq 'auto') {\n if (looks_like_number($av // '') && looks_like_number($bv // '')) {\n return ($av // 0) <=> ($bv // 0);\n }\n return ($av // '') cmp ($bv // '');\n }\n return ($av // 0) <=> ($bv // 0); # numeric\n}\n\n# ---------------------------------------------------------------------------\n# JSX intrinsic-element spread (#1407)\n# ---------------------------------------------------------------------------\n#\n# Mirrors the JS `spreadAttrs` runtime\n# (`packages/client/src/runtime/spread-attrs.ts`) and the Go adapter's\n# `bf.SpreadAttrs` so SSR output stays byte-equal across the three\n# adapters. Generated Mojo templates invoke this as\n# `<%== bf->spread_attrs($bag) %>`.\n#\n# Skip rules: nil/false values, event handlers (`on[A-Z]\u2026` shape\n# matching JS `key[2] === key[2].toUpperCase()` \u2014 true for any\n# character whose uppercase is itself, including digits and\n# underscore), `children`. `ref` is intentionally NOT filtered,\n# matching the JS reference.\n#\n# Key remap: className \u2192 class, htmlFor \u2192 for; SVG camelCase\n# attrs preserved (case-sensitive XML spec); other camelCase keys\n# lowered to kebab-case with a leading `-` for an initial\n# uppercase letter (mirrors JS `key.replace(/([A-Z])/g, '-$1')`).\n#\n# `style` is routed through `_style_to_css` so object literals\n# serialise to a real CSS string instead of Perl's default\n# `HASH(0x...)` form.\n#\n# Output is deterministic: keys are sorted alphabetically before\n# emission, matching the Go adapter's `sort.Strings(keys)` policy\n# and Mojo::JSON's marshal order.\n#\n# The return value is a Mojo::ByteStream so the calling template's\n# `<%==` raw-emit skips re-escaping (the helper has already\n# HTML-escaped each value).\n\nmy %SVG_CAMEL_CASE_ATTRS = map { $_ => 1 } qw(\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\nsub _to_attr_name ($key) {\n return 'class' if $key eq 'className';\n return 'for' if $key eq 'htmlFor';\n return $key if $SVG_CAMEL_CASE_ATTRS{$key};\n # camelCase \u2192 kebab-case, with a leading `-` for an initial\n # uppercase letter (JS-reference parity, even though that case\n # produces an HTML-invalid attribute name \u2014 same documented\n # behaviour as the Go adapter's `toAttrName`).\n my $out = $key;\n $out =~ s/([A-Z])/-\\L$1/g;\n return $out;\n}\n\nsub _html_escape ($value) {\n # HTML attribute-value escape for SSR string emission. The\n # spread bag's values reach the browser as part of a generated\n # `key=\"...\"` substring inside the rendered HTML, so the\n # escape set has to cover everything that could break either\n # the surrounding double-quoted attribute or the enclosing\n # tag: `&`, `<`, `>`, `\"`, and `'`. Matches Go's\n # `template.HTMLEscapeString` semantics byte-for-byte (using\n # `"` / `'` for quotes rather than the named entities)\n # so the SSR output is identical across the Go and Mojo\n # adapters (#1407, #1413 review). The CSR-side\n # `applyRestAttrs` calls `el.setAttribute(name, String(value))`\n # \u2014 which does its own DOM-level escaping in the browser \u2014\n # so JS doesn't need an explicit escape pass; Perl/Go emit a\n # string, so we do.\n my $s = defined $value ? \"$value\" : '';\n $s =~ s/&/&/g;\n $s =~ s/</</g;\n $s =~ s/>/>/g;\n $s =~ s/\"/"/g;\n $s =~ s/'/'/g;\n return $s;\n}\n\nsub _style_to_css ($value) {\n return undef unless defined $value;\n # Non-hashref values pass through stringified \u2014 matches the JS\n # `typeof value !== 'object'` branch in `styleToCss`.\n if (ref($value) ne 'HASH') {\n my $s = \"$value\";\n return length $s ? $s : undef;\n }\n my @parts;\n for my $key (sort keys %$value) {\n my $v = $value->{$key};\n next unless defined $v;\n my $prop = $key;\n $prop =~ s/([A-Z])/-\\L$1/g;\n push @parts, \"$prop:$v\";\n }\n return @parts ? join(';', @parts) : undef;\n}\n\nsub spread_attrs ($self, $bag) {\n return '' unless defined $bag && ref($bag) eq 'HASH';\n my @parts;\n for my $key (sort keys %$bag) {\n # Event handlers: skip when key starts `on` and the third\n # character is its own uppercase form (uppercase letter,\n # digit, underscore, \u2026). Mirrors the JS predicate.\n if (length($key) > 2 && substr($key, 0, 2) eq 'on') {\n my $c = substr($key, 2, 1);\n next if uc($c) eq $c;\n }\n next if $key eq 'children';\n my $val = $bag->{$key};\n # null / undef \u2192 drop.\n next unless defined $val;\n # Boolean values arrive as Mojo::JSON sentinel objects\n # (`Mojo::JSON::true` / `false`) \u2014 both from JSON-deserialised\n # props and from the test harness's `toPerlLiteral`\n # (which emits the sentinels rather than plain 0/1 to avoid\n # conflating booleans with numeric attribute values like\n # `tabindex=\"0\"`). The contract is: callers MUST use the\n # sentinels for boolean values; plain Perl scalars 0/1\n # render as numeric attribute values, matching how JS\n # `spreadAttrs` treats a `0`/`1` JS number.\n if (ref($val) eq 'JSON::PP::Boolean' || ref($val) eq 'Mojo::JSON::_Bool') {\n next unless $val;\n push @parts, _to_attr_name($key);\n next;\n }\n # `style` routes through `_style_to_css` so object literals\n # serialise to a real CSS string.\n if ($key eq 'style') {\n my $css = _style_to_css($val);\n next unless defined $css && length $css;\n push @parts, qq{style=\"} . _html_escape($css) . qq{\"};\n next;\n }\n my $name = _to_attr_name($key);\n push @parts, $name . qq{=\"} . _html_escape($val) . qq{\"};\n }\n return '' unless @parts;\n # Return a Mojo::ByteStream so the calling template's `<%==`\n # raw-emit doesn't re-escape the already-escaped values.\n return b(join(' ', @parts));\n}\n\n1;\n";
|
|
23122
|
+
barefootPmSource = "package BarefootJS;\nuse Mojo::Base -base, -signatures;\n\nuse Mojo::ByteStream qw(b);\nuse Mojo::JSON qw(encode_json to_json);\nuse POSIX ();\nuse Scalar::Util qw(looks_like_number weaken);\n\nhas 'c'; # Mojolicious controller\nhas 'config'; # Plugin config\n\n# Internal state\nhas '_scripts' => sub { [] };\nhas '_script_seen' => sub { {} };\nhas '_scope_id';\nhas '_is_child' => 0;\nhas '_bf_parent'; # Host scope id when this scope is a slot-attached child\nhas '_bf_mount'; # Slot id in host\nhas '_props';\n\nsub new ($class, $c, $config = {}) {\n return $class->SUPER::new(\n c => $c,\n config => $config,\n );\n}\n\n# ---------------------------------------------------------------------------\n# Scope & Props\n# ---------------------------------------------------------------------------\n\nsub scope_attr ($self) {\n # bf-s is the addressable scope id only (#1249).\n return $self->_scope_id // '';\n}\n\n# Emits `bf-h=\"<host>\" bf-m=\"<slot>\" bf-r=\"\"` conditionally.\n# See spec/compiler.md \"Slot identity\".\nsub hydration_attrs ($self) {\n my @parts;\n my $host = $self->_bf_parent;\n my $mount = $self->_bf_mount;\n if (defined $host && length $host) {\n my $h = $host =~ s/\"/"/gr;\n push @parts, qq{bf-h=\"$h\"};\n }\n if (defined $mount && length $mount) {\n my $m = $mount =~ s/\"/"/gr;\n push @parts, qq{bf-m=\"$m\"};\n }\n unless ($self->_is_child) {\n push @parts, q{bf-r=\"\"};\n }\n return join(' ', @parts);\n}\n\nsub props_attr ($self) {\n my $props = $self->_props;\n return '' unless $props && %$props;\n # to_json returns a character string (not bytes) for safe embedding in templates\n my $json = to_json($props);\n return qq{ bf-p='$json'};\n}\n\n# ---------------------------------------------------------------------------\n# Comment Markers\n# ---------------------------------------------------------------------------\n\nsub comment ($self, $text) {\n return \"<!--bf-$text-->\";\n}\n\n# ---------------------------------------------------------------------------\n# JS-equivalent value stringification\n# ---------------------------------------------------------------------------\n\n# Map a Perl boolean-shaped value to the JS `String(bool)` form.\n# Used by the Mojo adapter when emitting reactive attribute bindings\n# whose JS source `isBooleanResultExpr` classified as boolean \u2014\n# a comparison (`count() > 0`), a logical negation (`!ok()`), or a\n# literal `true` / `false`. Perl's auto-stringification of those\n# expressions yields `''` / `1`; Hono and Go emit `'false'` / `'true'`.\n# Centralising the bool \u2192 string mapping here keeps the contract\n# testable and the template-emit syntax tidy\n# (`<%= bf->bool_str(...) %>` vs an inline ternary).\n#\n# Contract is boolean-only: callers must have classified the\n# expression as boolean-result before routing through this helper.\n# Non-boolean values reaching here will be Perl-truthy-coerced to\n# 'true' / 'false', which is generally wrong \u2014 non-boolean attribute\n# bindings stay on the plain `<%= expr %>` emit path and never reach\n# this function.\nsub bool_str ($self, $value) {\n return $value ? 'true' : 'false';\n}\n\nsub text_start ($self, $slot_id) {\n return \"<!--bf:$slot_id-->\";\n}\n\nsub text_end ($self) {\n return \"<!--/-->\";\n}\n\n# See spec/compiler.md \"Slot identity\" for the comment-scope wire format.\nsub scope_comment ($self) {\n my $scope_id = $self->_scope_id // '';\n my $host_segment = '';\n my $host = $self->_bf_parent;\n my $mount = $self->_bf_mount;\n if (defined $host && length $host) {\n $host_segment = \"|h=$host|m=\" . ($mount // '');\n }\n my $props_json = '';\n if ($self->_props && %{$self->_props}) {\n $props_json = '|' . to_json($self->_props);\n }\n return \"<!--bf-scope:$scope_id$host_segment$props_json-->\";\n}\n\n# ---------------------------------------------------------------------------\n# Script Registration\n# ---------------------------------------------------------------------------\n\nsub register_script ($self, $path) {\n return if $self->_script_seen->{$path};\n $self->_script_seen->{$path} = 1;\n push @{$self->_scripts}, $path;\n}\n\n# ---------------------------------------------------------------------------\n# Child Component Rendering\n# ---------------------------------------------------------------------------\n\nhas '_child_renderers' => sub { {} };\n\nsub register_child_renderer ($self, $name, $renderer) {\n $self->_child_renderers->{$name} = $renderer;\n}\n\nsub render_child ($self, $name, %props) {\n my $renderer = $self->_child_renderers->{$name};\n die \"No renderer registered for child component '$name'\" unless $renderer;\n # JSX children come in via Mojo `begin %>...<% end` capture, which\n # produces a CODE ref returning a Mojo::ByteStream. Materialize it\n # before handing the props to the child renderer so the child\n # template sees `$children` as already-rendered HTML.\n $props{children} = $props{children}->() if ref($props{children}) eq 'CODE';\n return $renderer->(\\%props);\n}\n\n# ---------------------------------------------------------------------------\n# Bulk registration from build manifest\n# ---------------------------------------------------------------------------\n#\n# `bf build` emits dist/templates/manifest.json describing every\n# component the page might invoke (Counter, ui/button/index, ...).\n# This helper walks that manifest and registers one child renderer per\n# UI registry entry \u2014 the path shape `ui/<name>/index` maps to the\n# `<name>` slot key Counter.html.ep and friends use via\n# `<%= bf->render_child('<name>', ...) %>`.\n#\n# Each manifest entry carries an `ssrDefaults` hash derived statically\n# from the component's JSX (prop destructure defaults + signal /\n# memo initial values, see packages/jsx/src/ssr-defaults.ts). The\n# child renderer seeds every template variable from that hash,\n# preferring the caller's matching prop where one exists. This\n# replaces the per-component `signal_init` callback that every\n# scaffold's `app.pl` used to hand-roll for items 1/3 of issue #1416.\n#\n# `signal_init` remains as an opt-in override for cases the static\n# extractor can't see through (e.g. signal initial values that\n# reference imported helpers). When supplied for a given slot key\n# it takes precedence over the manifest's `ssrDefaults` for that\n# child, allowing callers to mix manual overrides with auto-derived\n# defaults for siblings.\nsub register_components_from_manifest ($self, $manifest, %opts) {\n my $c = $self->c;\n my $signal_inits = $opts{signal_init} // {};\n my $parent_scope = $self->_scope_id;\n weaken(my $parent = $self);\n\n for my $entry_name (keys %$manifest) {\n # `__barefoot__` is the runtime entry, not a component.\n next if $entry_name eq '__barefoot__';\n # Only UI registry components (path shape `ui/<name>/index`)\n # become child renderers; top-level page components are the\n # render target rather than a child.\n next unless $entry_name =~ m{^ui/([^/]+)/index$};\n my $slot_key = $1;\n my $marked = $manifest->{$entry_name}{markedTemplate} // '';\n next unless $marked;\n # `templates/ui/button/index.html.ep` \u2192 `ui/button/index`\n my $template_name = $marked;\n $template_name =~ s{^templates/}{};\n $template_name =~ s{\\.html\\.ep$}{};\n\n my $signal_init = $signal_inits->{$slot_key};\n my $manifest_defaults = $manifest->{$entry_name}{ssrDefaults};\n $self->register_child_renderer($slot_key, sub {\n my ($props) = @_;\n my $child_bf = BarefootJS->new($c, {});\n my $slot_id = delete $props->{_bf_slot};\n $child_bf->_scope_id(\n $slot_id ? $parent_scope . '_' . $slot_id\n : $template_name . '_' . substr(rand() =~ s/^0\\.//r, 0, 6)\n );\n $child_bf->_is_child(1);\n # (#1249) Slot identity: host scope + slot id. Emitted as\n # bf-h / bf-m attributes by hydration_attrs.\n if ($slot_id) {\n $child_bf->_bf_parent($parent_scope);\n $child_bf->_bf_mount($slot_id);\n }\n $child_bf->_scripts($parent->_scripts);\n $child_bf->_script_seen($parent->_script_seen);\n\n my %extra;\n if ($signal_init) {\n %extra = $signal_init->($props);\n } elsif ($manifest_defaults) {\n %extra = _derive_stash_from_defaults($manifest_defaults, $props);\n }\n\n my $prev = $c->stash->{'bf.instance'};\n $c->stash->{'bf.instance'} = $child_bf;\n my $html = $c->render_to_string(\n template => $template_name, %$props, %extra,\n );\n $c->stash->{'bf.instance'} = $prev;\n chomp $html;\n return $html;\n });\n }\n}\n\n# Derive template-stash kvs from a manifest entry's `ssrDefaults`\n# section. Each entry shape:\n# { value => <static-fallback>, propName => <prop>, isRestProps => bool }\n# For `isRestProps`, the rest bag passes through unchanged (or the\n# static `{}` if the caller didn't supply one). For ordinary entries\n# the caller's `$props->{propName}` wins when defined, otherwise the\n# static `value` does. `propName`-less entries (signal / memo locals)\n# always use the static value \u2014 the caller cannot override them.\nsub _derive_stash_from_defaults ($defaults, $props) {\n my %extra;\n for my $name (keys %$defaults) {\n my $d = $defaults->{$name};\n if (ref($d) ne 'HASH') {\n $extra{$name} = $d;\n next;\n }\n if ($d->{isRestProps}) {\n $extra{$name} = exists $props->{$name} ? $props->{$name} : $d->{value};\n next;\n }\n my $prop_name = $d->{propName};\n if (defined $prop_name && exists $props->{$prop_name} && defined $props->{$prop_name}) {\n $extra{$name} = $props->{$prop_name};\n } else {\n $extra{$name} = $d->{value};\n }\n }\n return %extra;\n}\n\n# ---------------------------------------------------------------------------\n# Script Output\n# ---------------------------------------------------------------------------\n\nsub scripts ($self) {\n my @tags;\n for my $path (@{$self->_scripts}) {\n push @tags, qq{<script type=\"module\" src=\"$path\"></script>};\n }\n return join(\"\\n\", @tags);\n}\n\n# ---------------------------------------------------------------------------\n# Streaming SSR (Out-of-Order)\n# ---------------------------------------------------------------------------\n\nsub streaming_bootstrap ($self) {\n return q{<script>(function(){function s(id){var a=document.querySelector('[bf-async=\"'+id+'\"]');var t=document.querySelector('template[bf-async-resolve=\"'+id+'\"]');if(!a||!t)return;a.replaceChildren(t.content.cloneNode(true));a.removeAttribute('bf-async');t.remove();requestAnimationFrame(function(){if(window.__bf_hydrate)window.__bf_hydrate()})};window.__bf_swap=s})()</script>};\n}\n\nsub async_boundary ($self, $id, $fallback_html) {\n # The fallback comes in via Mojo `begin %>...<% end` capture (see\n # MojoAdapter::renderAsync), which produces a CODE ref returning a\n # Mojo::ByteStream. Materialize it so the rendered HTML embeds in\n # the placeholder rather than the CODE ref's stringification.\n $fallback_html = $fallback_html->() if ref($fallback_html) eq 'CODE';\n return qq{<div bf-async=\"$id\">$fallback_html</div>};\n}\n\nsub async_resolve ($self, $id, $content_html) {\n return qq{<template bf-async-resolve=\"$id\">$content_html</template><script>__bf_swap(\"$id\")</script>};\n}\n\n# ---------------------------------------------------------------------------\n# JS-compat callees (#1189) \u2014 invoked from generated Mojo templates as\n# <%= bf->json($val) %>, <%= bf->floor($val) %>, etc. The MojoAdapter's\n# `templatePrimitives` registry emits these helper calls in place of the\n# corresponding JS callees (`JSON.stringify`, `Math.floor`, \u2026) so the SSR\n# template can render value-equivalent output without a JS engine.\n#\n# Failure policy mirrors the Go adapter (#1188): user-data marshalling\n# (json) bubbles errors so Mojolicious aborts loudly on cycles /\n# unsupported values rather than silently producing an empty payload.\n# Numeric coercion follows JS semantics (NaN propagates as the special\n# string 'NaN'; non-numeric input returns 'NaN' rather than 0). Strings\n# always coerce to a string representation.\n# ---------------------------------------------------------------------------\n\nsub json ($self, $value) {\n # Mojo::JSON::to_json returns a character string (not bytes), suitable\n # for embedding in HTML output via Mojo::ByteStream / `<%==`.\n #\n # Documented divergence from JS: JS distinguishes `null` (renders as\n # \"null\") from `undefined` (`JSON.stringify(undefined)` returns the\n # JS value `undefined`, not a string). Perl has no such distinction\n # \u2014 both map to `undef`. We choose the `null` rendering for SSR\n # ergonomics: an unset prop becomes the string \"null\" rather than\n # the literal text \"undefined\" or an empty attribute. Matches the\n # `null` case of JS exactly; diverges from the `undefined` case.\n return to_json($value);\n}\n\nsub string ($self, $value) {\n # JS `String(v)` mirror. `undef` renders as the empty string here so\n # an unset prop doesn't surface as a literal \"undefined\" / \"null\"\n # in user-facing HTML \u2014 same divergence the Go adapter documents\n # for `bf_string`.\n return defined $value ? \"$value\" : '';\n}\n\nsub number ($self, $value) {\n # JS `Number(v)` mirror. Numeric coerces via Perl's implicit\n # numeric context; non-numeric / undef yield real numeric NaN\n # (`'nan' + 0`) so downstream arithmetic propagates correctly\n # (`Math.floor(NaN) === NaN`). Returning the literal string\n # \"NaN\" would conflate the user-passing-the-string-\"NaN\" case\n # with the parse-failure case, and break NaN detection in\n # downstream helpers.\n return 0 + 'nan' unless defined $value;\n return $value + 0 if looks_like_number($value);\n return 0 + 'nan';\n}\n\n# NaN is the only float for which `$x != $x` holds. Used as the\n# portable sentinel check in floor/ceil/round.\nsub _is_nan { my $n = shift; return $n != $n }\n\nsub floor ($self, $value) {\n my $n = $self->number($value);\n return $n if _is_nan($n);\n return POSIX::floor($n);\n}\n\nsub ceil ($self, $value) {\n my $n = $self->number($value);\n return $n if _is_nan($n);\n return POSIX::ceil($n);\n}\n\nsub round ($self, $value) {\n my $n = $self->number($value);\n return $n if _is_nan($n);\n # POSIX has no `round`. JS `Math.round` rounds half toward\n # +Infinity (so `Math.round(-1.5) === -1`, not -2). `floor(n\n # + 0.5)` reproduces that for both signs.\n return POSIX::floor($n + 0.5);\n}\n\n# ---------------------------------------------------------------------------\n# Array / String method helpers (#1448 Tier A)\n# ---------------------------------------------------------------------------\n#\n# `Array.prototype.includes(x)` and `String.prototype.includes(sub)`\n# share a method name in JS; the JSX parser can't tell the two\n# receiver shapes apart without TS type inference, so both lower to\n# the same IR node (`array-method` / method `includes`). This helper\n# dispatches at the Perl level via `ref()`:\n# - ARRAY ref: scan elements with `eq`; one defined-vs-undef\n# hop matches JS's `===` for null/undefined.\n# - scalar: `index($recv, $sub) != -1`, with both args\n# coerced through `// ''` so an undef receiver /\n# needle doesn't trip Perl's substr warning.\n# Anything else (HASH ref, code ref) returns false \u2014 matches the\n# JS semantic where `.includes` is only defined on Array /\n# TypedArray / String.\n\nsub includes ($self, $recv, $elem) {\n if (ref($recv) eq 'ARRAY') {\n for my $item (@$recv) {\n if (!defined $item) {\n return 1 if !defined $elem;\n next;\n }\n return 1 if defined $elem && $item eq $elem;\n }\n return 0;\n }\n return 0 if ref($recv);\n return index($recv // '', $elem // '') != -1 ? 1 : 0;\n}\n\n# `Array.prototype.indexOf(x)` / `Array.prototype.lastIndexOf(x)`\n# value-equality search (#1448 Tier A). Returns the 0-based position\n# of the first / last matching element, or -1 if not found.\n# Non-array receivers return -1 \u2014 matches the JS semantic that\n# `.indexOf` / `.lastIndexOf` are only defined on Array / TypedArray.\n# (The string-position `indexOf` form isn't in Tier A; if it lands\n# later the helper can grow a ref()-dispatch branch like `includes`.)\n\nsub _array_index_of ($recv, $elem, $reverse) {\n return -1 unless ref($recv) eq 'ARRAY';\n my @indices = $reverse ? (reverse 0 .. $#{$recv}) : (0 .. $#{$recv});\n for my $i (@indices) {\n my $item = $recv->[$i];\n if (!defined $item) {\n return $i if !defined $elem;\n next;\n }\n return $i if defined $elem && $item eq $elem;\n }\n return -1;\n}\n\nsub index_of ($self, $recv, $elem) {\n return _array_index_of($recv, $elem, 0);\n}\n\nsub last_index_of ($self, $recv, $elem) {\n return _array_index_of($recv, $elem, 1);\n}\n\n# `Array.prototype.at(i)` \u2014 supports negative indices (`.at(-1)` is\n# the last element); out-of-bounds returns undef (which Mojo's\n# auto-escape renders as the empty string, matching JS's `undefined`).\n# Non-array receivers return undef. Matches the Go `bf_at` arithmetic\n# (`length + i` for i < 0) so adapter output stays symmetric.\n\nsub at ($self, $recv, $i) {\n return undef unless ref($recv) eq 'ARRAY';\n return undef if !defined $i;\n my $len = scalar @$recv;\n return undef if $len == 0;\n my $idx = $i < 0 ? $len + $i : $i;\n return undef if $idx < 0 || $idx >= $len;\n return $recv->[$idx];\n}\n\n# `Array.prototype.concat(other)` \u2014 merges two arrays in order\n# into a new ARRAY ref. Non-array operands collapse to empty\n# (matches the Go `bf_concat` semantic so cross-adapter output\n# stays symmetric; differs from JS where a non-Array argument\n# with `Symbol.isConcatSpreadable` would be spread, a behaviour\n# the template-language path never observes).\n\nsub concat ($self, $a, $b) {\n my @out;\n push @out, @$a if ref($a) eq 'ARRAY';\n push @out, @$b if ref($b) eq 'ARRAY';\n return \\@out;\n}\n\n# `Array.prototype.slice(start, end?)` \u2014 carves out a sub-range\n# into a new ARRAY ref. Mirrors the Go `bf_slice` arithmetic so\n# adapter output stays symmetric:\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\n# - end undef \u2192 \"to length\"\n# Non-array receivers return an empty ARRAY ref.\n\nsub slice ($self, $recv, $start, $end) {\n return [] unless ref($recv) eq 'ARRAY';\n my $len = scalar @$recv;\n return [] if $len == 0;\n\n my $s = $start // 0;\n $s = $len + $s if $s < 0;\n $s = 0 if $s < 0;\n $s = $len if $s > $len;\n\n my $e = defined $end ? $end : $len;\n $e = $len + $e if $e < 0;\n $e = 0 if $e < 0;\n $e = $len if $e > $len;\n\n return [] if $s >= $e;\n return [ @{$recv}[$s .. $e - 1] ];\n}\n\n# `Array.prototype.reverse()` / `Array.prototype.toReversed()` \u2014\n# both shapes share this lowering. SSR templates render a snapshot\n# of state, so JS's mutate-receiver (`reverse`) vs\n# return-new-array (`toReversed`) distinction has no template-\n# level meaning. Always returns a new ARRAY ref to keep callers\n# safe from accidental aliasing. Non-array receivers return an\n# empty ARRAY ref.\n\nsub reverse ($self, $recv) {\n return [] unless ref($recv) eq 'ARRAY';\n return [ reverse @$recv ];\n}\n\n# `String.prototype.trim()` \u2014 strip leading + trailing whitespace.\n# JS's `String.prototype.trim` matches `\\s` in the Unicode sense\n# (any whitespace including non-breaking space U+00A0); Perl's `\\s`\n# inside a regex with `/u` flag is the same. Undef receivers return\n# the empty string (matches JS's `String(undefined).trim()` which\n# would be \"undefined\" \u2192 \"undefined\", but in our template context\n# undef commonly means \"missing prop\"; rendering the empty string\n# is the safer choice and mirrors the JS-compat divergence we\n# already document for `bf->string(undef) === \"\"`).\n\nsub trim ($self, $recv) {\n return '' unless defined $recv;\n return '' if ref($recv);\n my $s = \"$recv\";\n $s =~ s/^\\s+|\\s+$//gu;\n return $s;\n}\n\n# `String.prototype.split(sep)` (#1448 Tier B) \u2014 string \u2192 ARRAY ref.\n#\n# Two JS-parity wrinkles drive the helper (a bare `split` emit would\n# diverge from both JS and Go):\n#\n# * Perl's `split` treats its first argument as a *regex*, so a\n# separator like '.' or '|' would match far too much. We\n# `quotemeta` it to force literal-string matching, mirroring JS's\n# string-separator semantics (the regex-separator form stays\n# refused upstream \u2014 see the parser arm).\n# * Perl's `split` drops trailing empty fields by default; JS keeps\n# them (`\"a,\".split(\",\")` is `[\"a\", \"\"]`). Passing the `-1` limit\n# preserves them, matching JS and Go's `strings.Split`.\n#\n# An empty separator splits into individual characters (JS + Go agree).\n# Undef receiver renders as the single-element `['']` \u2014 the same\n# \"missing prop \u2192 empty string\" convention `bf->trim` uses.\n\nsub split ($self, $recv, $sep = undef, $limit = undef) {\n my $s = defined $recv && !ref($recv) ? \"$recv\" : '';\n\n my @parts;\n if (!defined $sep) {\n # No separator \u2192 the whole string in a single-element array\n # (matches JS `\"x\".split()` / `.split(undefined)`).\n @parts = ($s);\n }\n elsif (\"$sep\" eq '') {\n # Empty separator \u2192 individual characters. No `-1` limit here:\n # on an empty pattern Perl's `split` with `-1` appends a spurious\n # trailing empty field (\"abc\" \u2192 'a','b','c',''), which JS/Go don't.\n @parts = split //, $s;\n }\n elsif ($s eq '') {\n # Empty input with a non-empty separator: JS `\"\".split(\",\")` is\n # `[\"\"]` and Go's `strings.Split(\"\", \",\")` is `[\"\"]`, but Perl's\n # `split /,/, ''` returns the empty list \u2014 special-case for parity.\n @parts = ('');\n }\n else {\n # `quotemeta` forces literal-string matching (JS string-separator\n # semantics); the `-1` keeps trailing empty fields (JS keeps them,\n # Perl's bare `split` drops them).\n my $q = quotemeta(\"$sep\");\n @parts = split /$q/, $s, -1;\n }\n\n # Optional `limit` caps the number of pieces (JS `split(sep, limit)`).\n # 0 \u2192 empty; a negative limit keeps all (JS ToUint32 wrap makes it\n # effectively unbounded) \u2014 both match Go's `bf_split`.\n if (defined $limit) {\n my $n = int($limit);\n if ($n == 0) { @parts = () }\n elsif ($n > 0 && $n < scalar @parts) { @parts = @parts[0 .. $n - 1] }\n }\n\n return [@parts];\n}\n\n# `String.prototype.startsWith(prefix, position?)` (#1448 Tier B) \u2014\n# string \u2192 boolean (1 / 0). `substr`-anchored literal comparison mirrors\n# Go's `strings.HasPrefix`. An empty prefix is always true (JS parity);\n# undef / non-string receivers coerce to the empty string first. The\n# optional `position` re-anchors the test (clamped to `[0, length]`),\n# matching JS `\"abc\".startsWith(\"b\", 1)`.\n\nsub starts_with ($self, $recv, $prefix, $position = undef) {\n my $s = defined $recv && !ref($recv) ? \"$recv\" : '';\n my $p = defined $prefix ? \"$prefix\" : '';\n if (defined $position) {\n my $n = int($position);\n $n = 0 if $n < 0;\n $n = length($s) if $n > length($s);\n $s = substr($s, $n);\n }\n return substr($s, 0, length $p) eq $p ? 1 : 0;\n}\n\n# `String.prototype.endsWith(suffix, endPosition?)` (#1448 Tier B) \u2014\n# string \u2192 boolean (1 / 0). Mirrors Go's `strings.HasSuffix`. An empty\n# suffix is always true (JS parity); a suffix longer than the string is\n# false. `substr($s, -length $x)` would mis-read the whole string when\n# `length $x == 0`, so that case short-circuits. The optional\n# `endPosition` treats the string as if it were only that many chars\n# long (clamped to `[0, length]`), matching JS `\"abc\".endsWith(\"b\", 2)`.\n\nsub ends_with ($self, $recv, $suffix, $end_position = undef) {\n my $s = defined $recv && !ref($recv) ? \"$recv\" : '';\n my $x = defined $suffix ? \"$suffix\" : '';\n if (defined $end_position) {\n my $e = int($end_position);\n $e = 0 if $e < 0;\n $e = length($s) if $e > length($s);\n $s = substr($s, 0, $e);\n }\n return 1 if $x eq '';\n return 0 if length($s) < length($x);\n return substr($s, -length $x) eq $x ? 1 : 0;\n}\n\n# `String.prototype.replace(pattern, replacement)` \u2014 string-pattern\n# form only (#1448 Tier B), replacing the FIRST occurrence (JS string-\n# pattern semantics). Spliced via index/substr rather than `s///` so\n# BOTH the pattern and the replacement are literal: no Perl regex\n# metacharacters in the pattern and no `$1` / `$&` interpolation in the\n# replacement. Go's `bf_replace` (strings.Replace, n=1) treats the\n# replacement literally too, so the two adapters stay byte-equal \u2014 this\n# diverges from JS only for replacement strings containing `$`-patterns\n# (rare in template position). An empty pattern inserts the replacement\n# at the front (`\"abc\".replace(\"\", \"X\")` \u2192 \"Xabc\"), matching JS + Go.\n\nsub replace ($self, $recv, $pattern, $replacement) {\n my $s = defined $recv && !ref($recv) ? \"$recv\" : '';\n my $o = defined $pattern ? \"$pattern\" : '';\n my $n = defined $replacement ? \"$replacement\" : '';\n return $n . $s if $o eq '';\n my $i = index($s, $o);\n return $s if $i < 0;\n return substr($s, 0, $i) . $n . substr($s, $i + length($o));\n}\n\n# `String.prototype.repeat(n)` \u2014 the receiver concatenated n times\n# (#1448 Tier B), via Perl's `x` operator. JS throws RangeError for a\n# negative count, but SSR templates degrade to the empty string rather\n# than dying mid-render, so a count <= 0 returns \"\" (Go's `bf_repeat`\n# applies the same clamp). The count is truncated toward zero\n# (`int`), matching JS's ToIntegerOrInfinity on `\"a\".repeat(3.7)`.\n\nsub repeat ($self, $recv, $count) {\n my $s = defined $recv && !ref($recv) ? \"$recv\" : '';\n my $n = defined $count ? int($count) : 0;\n return $n <= 0 ? '' : $s x $n;\n}\n\n# `String.prototype.padStart` / `padEnd` (#1448 Tier B) \u2014 pad the\n# receiver to `$target` characters with `$pad` (default a single space)\n# repeated and truncated to fill, prepended or appended. Length is\n# measured in characters (Perl `length`), matching Go's rune-based\n# `bf_pad_*` \u2014 diverges from JS's UTF-16-unit length only for\n# astral-plane input. An empty pad, or a receiver already >= `$target`,\n# returns the receiver unchanged (JS parity). The `$target` is\n# truncated toward zero (JS ToLength on the first arg).\n\nsub _pad ($s, $target, $pad, $at_start) {\n $pad = ' ' unless defined $pad;\n $pad = \"$pad\";\n return $s if $pad eq '';\n my $len = length $s;\n my $t = int($target // 0);\n return $s if $len >= $t;\n my $need = $t - $len;\n # Repeat enough copies to cover $need, then trim to exactly $need.\n my $fill = substr($pad x (int($need / length($pad)) + 1), 0, $need);\n return $at_start ? $fill . $s : $s . $fill;\n}\n\nsub pad_start ($self, $recv, $target, $pad = undef) {\n my $s = defined $recv && !ref($recv) ? \"$recv\" : '';\n return _pad($s, $target, $pad, 1);\n}\n\nsub pad_end ($self, $recv, $target, $pad = undef) {\n my $s = defined $recv && !ref($recv) ? \"$recv\" : '';\n return _pad($s, $target, $pad, 0);\n}\n\n# `Array.prototype.sort(cmp)` / `Array.prototype.toSorted(cmp)`\n# lowering (#1448 Tier B). Non-mutating \u2014 JS's mutate-vs-new\n# distinction is moot in SSR template context.\n#\n# Opts hash-ref. The compiler emits a `keys` list of per-key hashes\n# in priority order; each hash carries:\n#\n# key_kind => 'self' | 'field'\n# key => '' when key_kind eq 'self'; field name verbatim\n# from the comparator AST (e.g. 'price', 'createdAt')\n# when key_kind eq 'field' \u2014 no case normalisation\n# applied. Perl hash lookups are case-sensitive so\n# the key here must match the actual hash key the\n# user populated.\n# compare_type => 'numeric' | 'string' | 'auto'\n# direction => 'asc' | 'desc'\n#\n# Accepted comparator catalogue (gated upstream at parse time \u2014\n# anything outside refuses with BF101 before reaching this helper):\n#\n# (a,b) => a.f - b.f \u2192 field, numeric\n# (a,b) => a - b \u2192 self, numeric\n# (a,b) => a[.f].localeCompare(b[.f]) \u2192 field|self, string\n# (a,b) => a.f > b.f ? 1 : -1 \u2192 field|self, auto\n# any of the above ||-chained \u2192 multi-key tie-breaks\n# (and reversed-operand variants for `desc`).\n#\n# `auto` (relational-ternary lowering) compares numerically when both\n# keys `looks_like_number`, else lexically \u2014 Go's `bf_sort` applies the\n# same rule so the two template adapters stay byte-equal.\n#\n# A future `nulls => 'first' | 'last'` knob can land per key without\n# churn \u2014 the opts hash is the right place to grow.\n\nsub sort ($self, $recv, $opts = {}) {\n return [] unless ref($recv) eq 'ARRAY';\n\n # Normalise the per-key specs (priority order, length >= 1).\n my @spec = map {\n {\n key_kind => $_->{key_kind} // 'self',\n key => $_->{key} // '',\n compare_type => $_->{compare_type} // 'numeric',\n direction => $_->{direction} // 'asc',\n }\n } @{ $opts->{keys} // [] };\n return [ @$recv ] unless @spec;\n\n # Schwartzian transform: project each item to all its sort keys\n # once, then compare projected keys. Cheaper than re-resolving the\n # field accessors inside every comparison for non-trivial arrays.\n my @keyed = map {\n my $item = $_;\n my @ks = map {\n $_->{key_kind} eq 'field' && ref($item) eq 'HASH' ? $item->{ $_->{key} } : $item;\n } @spec;\n [ \\@ks, $item ];\n } @$recv;\n\n my $cmp = sub {\n for my $i (0 .. $#spec) {\n my $sp = $spec[$i];\n my $c = _compare_sort_key($a->[0][$i], $b->[0][$i], $sp->{compare_type});\n next if $c == 0; # tie on this key \u2014 try the next\n return $sp->{direction} eq 'desc' ? -$c : $c;\n }\n return 0;\n };\n\n my @sorted = sort $cmp @keyed;\n return [ map { $_->[1] } @sorted ];\n}\n\n# Compare two projected keys, ascending orientation (-1 / 0 / 1); the\n# caller negates for 'desc'. 'auto' compares numerically when both\n# keys look like numbers, else lexically (matches Go's `bf_sort`).\n# undef coalesces to '' / 0 so the order stays total without warnings.\nsub _compare_sort_key ($av, $bv, $compare_type) {\n if ($compare_type eq 'string') {\n return ($av // '') cmp ($bv // '');\n }\n if ($compare_type eq 'auto') {\n if (looks_like_number($av // '') && looks_like_number($bv // '')) {\n return ($av // 0) <=> ($bv // 0);\n }\n return ($av // '') cmp ($bv // '');\n }\n return ($av // 0) <=> ($bv // 0); # numeric\n}\n\n# ---------------------------------------------------------------------------\n# JSX intrinsic-element spread (#1407)\n# ---------------------------------------------------------------------------\n#\n# Mirrors the JS `spreadAttrs` runtime\n# (`packages/client/src/runtime/spread-attrs.ts`) and the Go adapter's\n# `bf.SpreadAttrs` so SSR output stays byte-equal across the three\n# adapters. Generated Mojo templates invoke this as\n# `<%== bf->spread_attrs($bag) %>`.\n#\n# Skip rules: nil/false values, event handlers (`on[A-Z]\u2026` shape\n# matching JS `key[2] === key[2].toUpperCase()` \u2014 true for any\n# character whose uppercase is itself, including digits and\n# underscore), `children`. `ref` is intentionally NOT filtered,\n# matching the JS reference.\n#\n# Key remap: className \u2192 class, htmlFor \u2192 for; SVG camelCase\n# attrs preserved (case-sensitive XML spec); other camelCase keys\n# lowered to kebab-case with a leading `-` for an initial\n# uppercase letter (mirrors JS `key.replace(/([A-Z])/g, '-$1')`).\n#\n# `style` is routed through `_style_to_css` so object literals\n# serialise to a real CSS string instead of Perl's default\n# `HASH(0x...)` form.\n#\n# Output is deterministic: keys are sorted alphabetically before\n# emission, matching the Go adapter's `sort.Strings(keys)` policy\n# and Mojo::JSON's marshal order.\n#\n# The return value is a Mojo::ByteStream so the calling template's\n# `<%==` raw-emit skips re-escaping (the helper has already\n# HTML-escaped each value).\n\nmy %SVG_CAMEL_CASE_ATTRS = map { $_ => 1 } qw(\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\nsub _to_attr_name ($key) {\n return 'class' if $key eq 'className';\n return 'for' if $key eq 'htmlFor';\n return $key if $SVG_CAMEL_CASE_ATTRS{$key};\n # camelCase \u2192 kebab-case, with a leading `-` for an initial\n # uppercase letter (JS-reference parity, even though that case\n # produces an HTML-invalid attribute name \u2014 same documented\n # behaviour as the Go adapter's `toAttrName`).\n my $out = $key;\n $out =~ s/([A-Z])/-\\L$1/g;\n return $out;\n}\n\nsub _html_escape ($value) {\n # HTML attribute-value escape for SSR string emission. The\n # spread bag's values reach the browser as part of a generated\n # `key=\"...\"` substring inside the rendered HTML, so the\n # escape set has to cover everything that could break either\n # the surrounding double-quoted attribute or the enclosing\n # tag: `&`, `<`, `>`, `\"`, and `'`. Matches Go's\n # `template.HTMLEscapeString` semantics byte-for-byte (using\n # `"` / `'` for quotes rather than the named entities)\n # so the SSR output is identical across the Go and Mojo\n # adapters (#1407, #1413 review). The CSR-side\n # `applyRestAttrs` calls `el.setAttribute(name, String(value))`\n # \u2014 which does its own DOM-level escaping in the browser \u2014\n # so JS doesn't need an explicit escape pass; Perl/Go emit a\n # string, so we do.\n my $s = defined $value ? \"$value\" : '';\n $s =~ s/&/&/g;\n $s =~ s/</</g;\n $s =~ s/>/>/g;\n $s =~ s/\"/"/g;\n $s =~ s/'/'/g;\n return $s;\n}\n\nsub _style_to_css ($value) {\n return undef unless defined $value;\n # Non-hashref values pass through stringified \u2014 matches the JS\n # `typeof value !== 'object'` branch in `styleToCss`.\n if (ref($value) ne 'HASH') {\n my $s = \"$value\";\n return length $s ? $s : undef;\n }\n my @parts;\n for my $key (sort keys %$value) {\n my $v = $value->{$key};\n next unless defined $v;\n my $prop = $key;\n $prop =~ s/([A-Z])/-\\L$1/g;\n push @parts, \"$prop:$v\";\n }\n return @parts ? join(';', @parts) : undef;\n}\n\nsub spread_attrs ($self, $bag) {\n return '' unless defined $bag && ref($bag) eq 'HASH';\n my @parts;\n for my $key (sort keys %$bag) {\n # Event handlers: skip when key starts `on` and the third\n # character is its own uppercase form (uppercase letter,\n # digit, underscore, \u2026). Mirrors the JS predicate.\n if (length($key) > 2 && substr($key, 0, 2) eq 'on') {\n my $c = substr($key, 2, 1);\n next if uc($c) eq $c;\n }\n next if $key eq 'children';\n my $val = $bag->{$key};\n # null / undef \u2192 drop.\n next unless defined $val;\n # Boolean values arrive as Mojo::JSON sentinel objects\n # (`Mojo::JSON::true` / `false`) \u2014 both from JSON-deserialised\n # props and from the test harness's `toPerlLiteral`\n # (which emits the sentinels rather than plain 0/1 to avoid\n # conflating booleans with numeric attribute values like\n # `tabindex=\"0\"`). The contract is: callers MUST use the\n # sentinels for boolean values; plain Perl scalars 0/1\n # render as numeric attribute values, matching how JS\n # `spreadAttrs` treats a `0`/`1` JS number.\n if (ref($val) eq 'JSON::PP::Boolean' || ref($val) eq 'Mojo::JSON::_Bool') {\n next unless $val;\n push @parts, _to_attr_name($key);\n next;\n }\n # `style` routes through `_style_to_css` so object literals\n # serialise to a real CSS string.\n if ($key eq 'style') {\n my $css = _style_to_css($val);\n next unless defined $css && length $css;\n push @parts, qq{style=\"} . _html_escape($css) . qq{\"};\n next;\n }\n my $name = _to_attr_name($key);\n push @parts, $name . qq{=\"} . _html_escape($val) . qq{\"};\n }\n return '' unless @parts;\n # Return a Mojo::ByteStream so the calling template's `<%==`\n # raw-emit doesn't re-escape the already-escaped values.\n return b(join(' ', @parts));\n}\n\n1;\n";
|
|
22787
23123
|
barefootPluginPmSource = "package Mojolicious::Plugin::BarefootJS;\nuse Mojo::Base 'Mojolicious::Plugin', -signatures;\n\nuse Mojo::File qw(path);\nuse Mojo::JSON qw(decode_json);\n\nuse BarefootJS;\n\n# Plugin entry point. Wires up:\n#\n# 1. The `bf` controller helper. Lazily instantiates one\n# BarefootJS object per request and stashes it under\n# `bf.instance`.\n#\n# 2. A `before_render` hook that, when the rendered template name\n# matches a top-level component in the build manifest, fills the\n# heavy boilerplate the user previously hand-rolled in `app.pl`:\n# generates the scope id, registers every UI-registry child\n# renderer from the manifest, and seeds the stash with each\n# template variable's static default (issue #1416).\n#\n# Configuration (all optional):\n# - manifest_path: absolute path to the `bf build`-emitted\n# `manifest.json`. Defaults to `<app->home>/dist/templates/manifest.json`.\n# Pass `undef` to disable manifest-driven auto-init entirely; the\n# bf helper is still installed and callers can drive everything\n# manually as before.\nsub register ($self, $app, $config = {}) {\n $app->helper(bf => sub ($c) {\n $c->stash->{'bf.instance'} //= BarefootJS->new($c, $config);\n });\n\n my $manifest = _load_manifest($app, $config);\n return unless $manifest;\n\n # Cache the set of UI-registry slot keys so we can answer\n # \"is this template name a child or a top-level page?\" with a\n # single hash lookup at render time. Top-level entries are\n # everything that isn't `__barefoot__` and doesn't match\n # `ui/<name>/index` \u2014 the same partition `register_components_from_manifest`\n # applies internally.\n my %is_child_entry;\n for my $entry_name (keys %$manifest) {\n next if $entry_name eq '__barefoot__';\n next unless $entry_name =~ m{^ui/[^/]+/index$};\n $is_child_entry{$entry_name} = 1;\n }\n\n $app->hook(before_render => sub ($c, $args) {\n my $template = $args->{template};\n return unless defined $template && length $template;\n my $entry = $manifest->{$template};\n return unless $entry;\n return if $is_child_entry{$template};\n # Idempotency guard for nested renders. A controller might\n # call `render_to_string` inside an action and then `render`\n # \u2014 without this we'd re-init `bf` on the second pass and\n # wipe the script registrations the first pass collected.\n return if $c->stash->{'bf.auto_init_done'};\n\n # Escape hatch for callers that wire `bf` up by hand (the\n # existing `render_component` helper in the showcase app does\n # this). If `_scope_id` is already set we treat the request as\n # \"manually managed\" and leave it alone \u2014 same outcome as\n # before the plugin gained auto-init.\n my $bf = $c->bf;\n if (defined $bf->_scope_id && length $bf->_scope_id) {\n $c->stash->{'bf.auto_init_done'} = 1;\n return;\n }\n $c->stash->{'bf.auto_init_done'} = 1;\n\n $bf->_scope_id($template . '_' . substr(rand() =~ s/^0\\.//r, 0, 6));\n $bf->register_components_from_manifest($manifest);\n\n # Seed each ssrDefault into the stash unless the caller has\n # already supplied a value for that key \u2014 callers always win.\n my $defaults = $entry->{ssrDefaults};\n if (ref($defaults) eq 'HASH') {\n for my $name (keys %$defaults) {\n next if exists $c->stash->{$name};\n my $d = $defaults->{$name};\n my $value = ref($d) eq 'HASH' ? $d->{value} : $d;\n $c->stash->{$name} = $value;\n }\n }\n });\n}\n\nsub _load_manifest ($app, $config) {\n return undef if exists $config->{manifest_path} && !defined $config->{manifest_path};\n my $manifest_path = $config->{manifest_path}\n // $app->home->child('dist/templates/manifest.json');\n my $file = path($manifest_path);\n return undef unless -r $file;\n my $manifest = eval { decode_json($file->slurp) };\n if ($@ || ref($manifest) ne 'HASH') {\n $app->log->warn(\"BarefootJS: cannot parse manifest at $file: $@\") if $@;\n return undef;\n }\n return $manifest;\n}\n\n1;\n";
|
|
22788
23124
|
barefootDevReloadPmSource = `package Mojolicious::Plugin::BarefootJS::DevReload;
|
|
22789
23125
|
use Mojo::Base 'Mojolicious::Plugin', -signatures;
|
|
@@ -23847,7 +24183,13 @@ export default createConfig({
|
|
|
23847
24183
|
}
|
|
23848
24184
|
},
|
|
23849
24185
|
"include": ["**/*.ts", "**/*.tsx"],
|
|
23850
|
-
|
|
24186
|
+
// Do NOT exclude dist/components here. The server imports the compiled
|
|
24187
|
+
// SSR templates from there (see the @/components/* paths above), and
|
|
24188
|
+
// tsx applies the JSX transform per-file honouring this include/
|
|
24189
|
+
// exclude \u2014 an excluded .tsx loses jsxImportSource and falls back to
|
|
24190
|
+
// the classic React runtime, so SSR throws "ReferenceError: React is
|
|
24191
|
+
// not defined" at the first render. They must stay in scope.
|
|
24192
|
+
"exclude": ["node_modules"]
|
|
23851
24193
|
}
|
|
23852
24194
|
`;
|
|
23853
24195
|
HONO_NODE_ADAPTER = {
|
|
@@ -27567,9 +27909,9 @@ function findProjectConfig(startDir) {
|
|
|
27567
27909
|
let dir = path.resolve(startDir);
|
|
27568
27910
|
const { root: fsRoot } = path.parse(dir);
|
|
27569
27911
|
while (true) {
|
|
27570
|
-
const
|
|
27571
|
-
if (existsSync2(
|
|
27572
|
-
return { dir, tsConfigPath:
|
|
27912
|
+
const ts21 = path.join(dir, "barefoot.config.ts");
|
|
27913
|
+
if (existsSync2(ts21)) {
|
|
27914
|
+
return { dir, tsConfigPath: ts21 };
|
|
27573
27915
|
}
|
|
27574
27916
|
if (dir === fsRoot) return null;
|
|
27575
27917
|
dir = path.dirname(dir);
|