@barefootjs/cli 0.6.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js
CHANGED
|
@@ -5255,6 +5255,32 @@ function convertNode(node, raw) {
|
|
|
5255
5255
|
if (callee.property === "reverse" || callee.property === "toReversed") {
|
|
5256
5256
|
return { kind: "array-method", method: callee.property, object: callee.object, args: args2 };
|
|
5257
5257
|
}
|
|
5258
|
+
if (callee.property === "flat") {
|
|
5259
|
+
const depthNode = node.arguments[0];
|
|
5260
|
+
let flatDepth;
|
|
5261
|
+
if (depthNode === void 0) {
|
|
5262
|
+
flatDepth = 1;
|
|
5263
|
+
} else if (ts8.isIdentifier(depthNode) && depthNode.text === "Infinity") {
|
|
5264
|
+
flatDepth = "infinity";
|
|
5265
|
+
} else {
|
|
5266
|
+
let n;
|
|
5267
|
+
if (ts8.isNumericLiteral(depthNode)) {
|
|
5268
|
+
n = Number(depthNode.text);
|
|
5269
|
+
} else if (ts8.isPrefixUnaryExpression(depthNode) && depthNode.operator === ts8.SyntaxKind.MinusToken && ts8.isNumericLiteral(depthNode.operand)) {
|
|
5270
|
+
n = -Number(depthNode.operand.text);
|
|
5271
|
+
}
|
|
5272
|
+
if (n === void 0 || Number.isNaN(n)) {
|
|
5273
|
+
return {
|
|
5274
|
+
kind: "unsupported",
|
|
5275
|
+
raw,
|
|
5276
|
+
reason: `\`.flat(depth)\` needs a literal integer or \`Infinity\` depth \u2014 a computed depth can't be resolved at template time. Use a literal depth, or pre-compute the value before the template.`
|
|
5277
|
+
};
|
|
5278
|
+
}
|
|
5279
|
+
const truncated = Math.trunc(n);
|
|
5280
|
+
flatDepth = truncated < 0 ? 0 : truncated;
|
|
5281
|
+
}
|
|
5282
|
+
return { kind: "array-method", method: "flat", object: callee.object, args: [], flatDepth };
|
|
5283
|
+
}
|
|
5258
5284
|
if (callee.property === "toLowerCase") {
|
|
5259
5285
|
return { kind: "array-method", method: "toLowerCase", object: callee.object, args: args2 };
|
|
5260
5286
|
}
|
|
@@ -5338,6 +5364,50 @@ function convertNode(node, raw) {
|
|
|
5338
5364
|
(reverse the operands for descending order). Wrap the call in /* @client */ to evaluate at hydration.`
|
|
5339
5365
|
};
|
|
5340
5366
|
}
|
|
5367
|
+
if ((callee.property === "reduce" || callee.property === "reduceRight") && node.arguments.length === 2) {
|
|
5368
|
+
const reduceOp = extractReduceOpFromTS(node.arguments[0], node.arguments[1]);
|
|
5369
|
+
if (reduceOp) {
|
|
5370
|
+
return {
|
|
5371
|
+
kind: "array-method",
|
|
5372
|
+
method: callee.property,
|
|
5373
|
+
object: callee.object,
|
|
5374
|
+
args: [],
|
|
5375
|
+
reduceOp
|
|
5376
|
+
};
|
|
5377
|
+
}
|
|
5378
|
+
const m = callee.property;
|
|
5379
|
+
return {
|
|
5380
|
+
kind: "unsupported",
|
|
5381
|
+
raw,
|
|
5382
|
+
reason: `Reduce shape not supported. Accepted (arithmetic fold, explicit init):
|
|
5383
|
+
arr.${m}((acc, x) => acc + x, 0)
|
|
5384
|
+
arr.${m}((acc, x) => acc + x.field, 0)
|
|
5385
|
+
arr.${m}((acc, x) => acc * x.field, 1)
|
|
5386
|
+
arr.${m}((acc, x) => acc + x.field, '') (string concat)
|
|
5387
|
+
The accumulator must be the left operand and the initial value a number / string literal. Wrap the call in /* @client */ to evaluate at hydration.`
|
|
5388
|
+
};
|
|
5389
|
+
}
|
|
5390
|
+
if (callee.property === "flatMap") {
|
|
5391
|
+
const flatMapOp = node.arguments.length === 1 ? extractFlatMapOpFromTS(node.arguments[0]) : null;
|
|
5392
|
+
if (flatMapOp) {
|
|
5393
|
+
return {
|
|
5394
|
+
kind: "array-method",
|
|
5395
|
+
method: "flatMap",
|
|
5396
|
+
object: callee.object,
|
|
5397
|
+
args: [],
|
|
5398
|
+
flatMapOp
|
|
5399
|
+
};
|
|
5400
|
+
}
|
|
5401
|
+
return {
|
|
5402
|
+
kind: "unsupported",
|
|
5403
|
+
raw,
|
|
5404
|
+
reason: `flatMap shape not supported. Accepted (self / field leaves, no thisArg):
|
|
5405
|
+
arr.flatMap(i => i) (flatten one level)
|
|
5406
|
+
arr.flatMap(i => i.field) (flatten a per-item array field)
|
|
5407
|
+
arr.flatMap(i => [i.a, i.b]) (gather per-item fields)
|
|
5408
|
+
Richer callbacks (computed / nested access, arithmetic, calls, literal elements) and the 2-arg \`flatMap(fn, thisArg)\` form aren't lowered. Wrap the call in /* @client */ to evaluate at hydration.`
|
|
5409
|
+
};
|
|
5410
|
+
}
|
|
5341
5411
|
}
|
|
5342
5412
|
return { kind: "call", callee, args: args2 };
|
|
5343
5413
|
}
|
|
@@ -5634,6 +5704,95 @@ function classifySortOperand(expr, paramA, paramB) {
|
|
|
5634
5704
|
}
|
|
5635
5705
|
return null;
|
|
5636
5706
|
}
|
|
5707
|
+
function extractReduceOpFromTS(reducerNode, initNode) {
|
|
5708
|
+
const init = classifyReduceInit(initNode);
|
|
5709
|
+
if (!init) return null;
|
|
5710
|
+
if (!ts8.isArrowFunction(reducerNode) && !ts8.isFunctionExpression(reducerNode)) return null;
|
|
5711
|
+
if (reducerNode.parameters.length !== 2) return null;
|
|
5712
|
+
const pAcc = reducerNode.parameters[0];
|
|
5713
|
+
const pItem = reducerNode.parameters[1];
|
|
5714
|
+
if (!ts8.isIdentifier(pAcc.name) || !ts8.isIdentifier(pItem.name)) return null;
|
|
5715
|
+
const paramAcc = pAcc.name.text;
|
|
5716
|
+
const paramItem = pItem.name.text;
|
|
5717
|
+
let body;
|
|
5718
|
+
if (ts8.isArrowFunction(reducerNode) && !ts8.isBlock(reducerNode.body)) {
|
|
5719
|
+
body = reducerNode.body;
|
|
5720
|
+
} else {
|
|
5721
|
+
const block = reducerNode.body;
|
|
5722
|
+
const stmts = block.statements;
|
|
5723
|
+
if (stmts.length !== 1 || !ts8.isReturnStatement(stmts[0]) || !stmts[0].expression) return null;
|
|
5724
|
+
body = stmts[0].expression;
|
|
5725
|
+
}
|
|
5726
|
+
const raw = body.getText();
|
|
5727
|
+
const expr = unwrapParens(body);
|
|
5728
|
+
if (!ts8.isBinaryExpression(expr)) return null;
|
|
5729
|
+
let op;
|
|
5730
|
+
if (expr.operatorToken.kind === ts8.SyntaxKind.PlusToken) op = "+";
|
|
5731
|
+
else if (expr.operatorToken.kind === ts8.SyntaxKind.AsteriskToken) op = "*";
|
|
5732
|
+
else return null;
|
|
5733
|
+
const left = unwrapParens(expr.left);
|
|
5734
|
+
if (!ts8.isIdentifier(left) || left.text !== paramAcc) return null;
|
|
5735
|
+
const key = classifyReduceKey(unwrapParens(expr.right), paramItem);
|
|
5736
|
+
if (!key) return null;
|
|
5737
|
+
const type = init.type;
|
|
5738
|
+
if (type === "string" && op !== "+") return null;
|
|
5739
|
+
return { op, key, type, init: init.value, raw, paramAcc, paramItem };
|
|
5740
|
+
}
|
|
5741
|
+
function extractFlatMapOpFromTS(cbNode) {
|
|
5742
|
+
if (!ts8.isArrowFunction(cbNode) && !ts8.isFunctionExpression(cbNode)) return null;
|
|
5743
|
+
if (cbNode.parameters.length !== 1) return null;
|
|
5744
|
+
const p = cbNode.parameters[0];
|
|
5745
|
+
if (!ts8.isIdentifier(p.name)) return null;
|
|
5746
|
+
const param = p.name.text;
|
|
5747
|
+
let body;
|
|
5748
|
+
if (ts8.isArrowFunction(cbNode) && !ts8.isBlock(cbNode.body)) {
|
|
5749
|
+
body = cbNode.body;
|
|
5750
|
+
} else {
|
|
5751
|
+
const block = cbNode.body;
|
|
5752
|
+
const stmts = block.statements;
|
|
5753
|
+
if (stmts.length !== 1 || !ts8.isReturnStatement(stmts[0]) || !stmts[0].expression) return null;
|
|
5754
|
+
body = stmts[0].expression;
|
|
5755
|
+
}
|
|
5756
|
+
const raw = body.getText();
|
|
5757
|
+
const inner = unwrapParens(body);
|
|
5758
|
+
if (ts8.isArrayLiteralExpression(inner)) {
|
|
5759
|
+
if (inner.elements.length === 0) return null;
|
|
5760
|
+
const elements = [];
|
|
5761
|
+
for (const el of inner.elements) {
|
|
5762
|
+
if (ts8.isSpreadElement(el) || ts8.isOmittedExpression(el)) return null;
|
|
5763
|
+
const leaf2 = classifyReduceKey(unwrapParens(el), param);
|
|
5764
|
+
if (!leaf2) return null;
|
|
5765
|
+
elements.push(leaf2);
|
|
5766
|
+
}
|
|
5767
|
+
return { projection: { kind: "tuple", elements }, param, raw };
|
|
5768
|
+
}
|
|
5769
|
+
const leaf = classifyReduceKey(inner, param);
|
|
5770
|
+
if (!leaf) return null;
|
|
5771
|
+
return { projection: leaf, param, raw };
|
|
5772
|
+
}
|
|
5773
|
+
function classifyReduceKey(expr, paramItem) {
|
|
5774
|
+
if (ts8.isIdentifier(expr)) {
|
|
5775
|
+
return expr.text === paramItem ? { kind: "self" } : null;
|
|
5776
|
+
}
|
|
5777
|
+
if (ts8.isPropertyAccessExpression(expr) && ts8.isIdentifier(expr.expression)) {
|
|
5778
|
+
if (expr.expression.text === paramItem) return { kind: "field", field: expr.name.text };
|
|
5779
|
+
}
|
|
5780
|
+
return null;
|
|
5781
|
+
}
|
|
5782
|
+
function classifyReduceInit(node) {
|
|
5783
|
+
let n = unwrapParens(node);
|
|
5784
|
+
if (ts8.isPrefixUnaryExpression(n) && n.operator === ts8.SyntaxKind.MinusToken) {
|
|
5785
|
+
if (ts8.isNumericLiteral(n.operand)) return { type: "numeric", value: "-" + n.operand.text };
|
|
5786
|
+
return null;
|
|
5787
|
+
}
|
|
5788
|
+
if (ts8.isNumericLiteral(n)) return { type: "numeric", value: n.text };
|
|
5789
|
+
if (ts8.isStringLiteral(n)) {
|
|
5790
|
+
const raw = n.getText();
|
|
5791
|
+
if (raw.length < 2 || raw.slice(1, -1) !== n.text) return null;
|
|
5792
|
+
return { type: "string", value: n.text };
|
|
5793
|
+
}
|
|
5794
|
+
return null;
|
|
5795
|
+
}
|
|
5637
5796
|
function collectDestructureBindings(pattern, pathPrefix, fieldMap, raw, excludedTopKeys) {
|
|
5638
5797
|
let restName;
|
|
5639
5798
|
for (const el of pattern.elements) {
|
|
@@ -5935,6 +6094,15 @@ function substituteDestructuredFields(expr, fieldMap, syntheticParam, restName)
|
|
|
5935
6094
|
if (e.method === "sort" || e.method === "toSorted") {
|
|
5936
6095
|
return { kind: "array-method", method: e.method, object: walk(e.object), args: [], comparator: e.comparator };
|
|
5937
6096
|
}
|
|
6097
|
+
if (e.method === "reduce" || e.method === "reduceRight") {
|
|
6098
|
+
return { kind: "array-method", method: e.method, object: walk(e.object), args: [], reduceOp: e.reduceOp };
|
|
6099
|
+
}
|
|
6100
|
+
if (e.method === "flat") {
|
|
6101
|
+
return { kind: "array-method", method: "flat", object: walk(e.object), args: [], flatDepth: e.flatDepth };
|
|
6102
|
+
}
|
|
6103
|
+
if (e.method === "flatMap") {
|
|
6104
|
+
return { kind: "array-method", method: "flatMap", object: walk(e.object), args: [], flatMapOp: e.flatMapOp };
|
|
6105
|
+
}
|
|
5938
6106
|
return { kind: "array-method", method: e.method, object: walk(e.object), args: e.args.map(walk) };
|
|
5939
6107
|
case "literal":
|
|
5940
6108
|
case "unsupported":
|
|
@@ -6054,7 +6222,8 @@ function checkSupport(expr) {
|
|
|
6054
6222
|
return {
|
|
6055
6223
|
supported: false,
|
|
6056
6224
|
level: "L5_UNSUPPORTED",
|
|
6057
|
-
|
|
6225
|
+
selfContained: true,
|
|
6226
|
+
reason: UNSUPPORTED_METHOD_REASONS[methodName] ?? `'${methodName}()' can't render on the server. Pre-compute the value, or add /* @client */ for client-only (no SSR).`
|
|
6058
6227
|
};
|
|
6059
6228
|
}
|
|
6060
6229
|
}
|
|
@@ -6269,6 +6438,20 @@ function exprToString(expr) {
|
|
|
6269
6438
|
const { paramA, paramB, raw } = expr.comparator;
|
|
6270
6439
|
return `${exprToString(expr.object)}.${expr.method}((${paramA},${paramB}) => ${raw})`;
|
|
6271
6440
|
}
|
|
6441
|
+
if (expr.method === "reduce" || expr.method === "reduceRight") {
|
|
6442
|
+
const { paramAcc, paramItem, raw, type, init } = expr.reduceOp;
|
|
6443
|
+
const initSrc = type === "string" ? JSON.stringify(init) : init;
|
|
6444
|
+
return `${exprToString(expr.object)}.${expr.method}((${paramAcc},${paramItem}) => ${raw}, ${initSrc})`;
|
|
6445
|
+
}
|
|
6446
|
+
if (expr.method === "flat") {
|
|
6447
|
+
const d = expr.flatDepth;
|
|
6448
|
+
const depthSrc = d === "infinity" ? "Infinity" : String(d);
|
|
6449
|
+
return `${exprToString(expr.object)}.flat(${d === 1 ? "" : depthSrc})`;
|
|
6450
|
+
}
|
|
6451
|
+
if (expr.method === "flatMap") {
|
|
6452
|
+
const { param, raw } = expr.flatMapOp;
|
|
6453
|
+
return `${exprToString(expr.object)}.flatMap(${param} => ${raw})`;
|
|
6454
|
+
}
|
|
6272
6455
|
return `${exprToString(expr.object)}.${expr.method}(${expr.args.map(exprToString).join(", ")})`;
|
|
6273
6456
|
case "unsupported":
|
|
6274
6457
|
return `[UNSUPPORTED: ${expr.raw}]`;
|
|
@@ -6313,6 +6496,20 @@ function stringifyParsedExpr(expr) {
|
|
|
6313
6496
|
const { paramA, paramB, raw } = expr.comparator;
|
|
6314
6497
|
return `${stringifyParsedExpr(expr.object)}.${expr.method}((${paramA},${paramB}) => ${raw})`;
|
|
6315
6498
|
}
|
|
6499
|
+
if (expr.method === "reduce" || expr.method === "reduceRight") {
|
|
6500
|
+
const { paramAcc, paramItem, raw, type, init } = expr.reduceOp;
|
|
6501
|
+
const initSrc = type === "string" ? JSON.stringify(init) : init;
|
|
6502
|
+
return `${stringifyParsedExpr(expr.object)}.${expr.method}((${paramAcc},${paramItem}) => ${raw}, ${initSrc})`;
|
|
6503
|
+
}
|
|
6504
|
+
if (expr.method === "flat") {
|
|
6505
|
+
const d = expr.flatDepth;
|
|
6506
|
+
const depthSrc = d === "infinity" ? "Infinity" : String(d);
|
|
6507
|
+
return `${stringifyParsedExpr(expr.object)}.flat(${d === 1 ? "" : depthSrc})`;
|
|
6508
|
+
}
|
|
6509
|
+
if (expr.method === "flatMap") {
|
|
6510
|
+
const { param, raw } = expr.flatMapOp;
|
|
6511
|
+
return `${stringifyParsedExpr(expr.object)}.flatMap(${param} => ${raw})`;
|
|
6512
|
+
}
|
|
6316
6513
|
return `${stringifyParsedExpr(expr.object)}.${expr.method}(${expr.args.map(stringifyParsedExpr).join(", ")})`;
|
|
6317
6514
|
case "unsupported":
|
|
6318
6515
|
return expr.raw;
|
|
@@ -6326,7 +6523,7 @@ function identifierPath(callee) {
|
|
|
6326
6523
|
}
|
|
6327
6524
|
return null;
|
|
6328
6525
|
}
|
|
6329
|
-
var UNSUPPORTED_METHODS, LOWERED_ARRAY_METHODS;
|
|
6526
|
+
var UNSUPPORTED_METHODS, UNSUPPORTED_METHOD_REASONS, LOWERED_ARRAY_METHODS;
|
|
6330
6527
|
var init_expression_parser = __esm({
|
|
6331
6528
|
"../jsx/src/expression-parser.ts"() {
|
|
6332
6529
|
"use strict";
|
|
@@ -6334,8 +6531,28 @@ var init_expression_parser = __esm({
|
|
|
6334
6531
|
// Higher-order array methods. Seven of these (`filter`, `every`,
|
|
6335
6532
|
// `some`, `find`, `findIndex`, `findLast`, `findLastIndex`) are
|
|
6336
6533
|
// intercepted as `higher-order` IR before reaching this gate;
|
|
6337
|
-
// `map` is intercepted as an IRLoop.
|
|
6338
|
-
//
|
|
6534
|
+
// `map` is intercepted as an IRLoop. `reduce` / `reduceRight` stay
|
|
6535
|
+
// listed here so the shapes the Tier C catalogue can't lower still
|
|
6536
|
+
// refuse loudly: the `convertNode` call branch intercepts a matching
|
|
6537
|
+
// `.reduce(fn, init)` / `.reduceRight(fn, init)` into the structured
|
|
6538
|
+
// `array-method` + `ReduceOp` form *before* this gate (returning
|
|
6539
|
+
// early), so only the unlowerable fall-throughs (a `.reduce(fn)` with
|
|
6540
|
+
// no initial value, or a bare method reference) reach the gate and
|
|
6541
|
+
// refuse. A 2-arg call whose reducer/init shape is off-catalogue
|
|
6542
|
+
// returns an explicit `unsupported` from the call branch with a richer
|
|
6543
|
+
// message. The rest stay refused — see #1448 Tier C for the design
|
|
6544
|
+
// questions. `forEach` carries a tailored reason (see
|
|
6545
|
+
// `UNSUPPORTED_METHOD_REASONS`).
|
|
6546
|
+
// `flat` is no longer here — `.flat(depth?)` lowers via the
|
|
6547
|
+
// `array-method` IR (structured `FlatDepth`) + `bf_flat` (Go) /
|
|
6548
|
+
// `bf->flat` (Mojo). `flatMap` stays listed as a fallback: the
|
|
6549
|
+
// field-projection form (`i => i` / `i => i.field`) lowers via a
|
|
6550
|
+
// structured `FlatMapOp`. The convertNode arm intercepts EVERY
|
|
6551
|
+
// `.flatMap(...)` call before this gate — matching shapes lower, and the
|
|
6552
|
+
// off-catalogue / wrong-arity forms get a tailored `unsupported` reason
|
|
6553
|
+
// there — so only a bare method *reference* (`arr.flatMap` uncalled)
|
|
6554
|
+
// falls through to this gate. The JSX-returning `.flatMap` lowers as an
|
|
6555
|
+
// `IRLoop` upstream. See #1448.
|
|
6339
6556
|
"filter",
|
|
6340
6557
|
"map",
|
|
6341
6558
|
"reduce",
|
|
@@ -6344,7 +6561,6 @@ var init_expression_parser = __esm({
|
|
|
6344
6561
|
"some",
|
|
6345
6562
|
"forEach",
|
|
6346
6563
|
"flatMap",
|
|
6347
|
-
"flat",
|
|
6348
6564
|
// #1448 Tier A — Array methods. Each method PR adds the lowering
|
|
6349
6565
|
// (typically a new `array-method` variant or runtime helper) and
|
|
6350
6566
|
// removes its row here. See packages/adapter-tests/fixtures/methods/.
|
|
@@ -6414,6 +6630,12 @@ var init_expression_parser = __esm({
|
|
|
6414
6630
|
"matchAll",
|
|
6415
6631
|
"search"
|
|
6416
6632
|
]);
|
|
6633
|
+
UNSUPPORTED_METHOD_REASONS = {
|
|
6634
|
+
// `forEach` returns `undefined`, so the generic pre-compute / @client hint
|
|
6635
|
+
// is misleading (renders nothing either way) — steer to `.map(...)` /
|
|
6636
|
+
// `createEffect`. Rationale pinned in foreach-client-only.test.ts.
|
|
6637
|
+
forEach: `'.forEach()' returns undefined and has no template-position meaning. Use it for side effects inside an event handler or createEffect callback (client JS), or use '.map(...)' if you meant to render each item.`
|
|
6638
|
+
};
|
|
6417
6639
|
LOWERED_ARRAY_METHODS = /* @__PURE__ */ new Set([
|
|
6418
6640
|
"includes",
|
|
6419
6641
|
"indexOf",
|
|
@@ -7225,6 +7447,7 @@ function transformComponentElement(node, ctx2, name) {
|
|
|
7225
7447
|
children,
|
|
7226
7448
|
template: name.toLowerCase(),
|
|
7227
7449
|
slotId,
|
|
7450
|
+
...isDynamicTagLocal(name, ctx2) ? { dynamicTag: true } : {},
|
|
7228
7451
|
loc: getSourceLocation(node, ctx2.sourceFile, ctx2.filePath)
|
|
7229
7452
|
};
|
|
7230
7453
|
}
|
|
@@ -7240,6 +7463,7 @@ function transformSelfClosingComponent(node, ctx2, name) {
|
|
|
7240
7463
|
children: [],
|
|
7241
7464
|
template: name.toLowerCase(),
|
|
7242
7465
|
slotId,
|
|
7466
|
+
...isDynamicTagLocal(name, ctx2) ? { dynamicTag: true } : {},
|
|
7243
7467
|
loc: getSourceLocation(node, ctx2.sourceFile, ctx2.filePath)
|
|
7244
7468
|
};
|
|
7245
7469
|
}
|
|
@@ -8945,6 +9169,34 @@ function findLocalConst(name, ctx2) {
|
|
|
8945
9169
|
const pool = fnScoped.length > 0 ? fnScoped : matches;
|
|
8946
9170
|
return pool[pool.length - 1];
|
|
8947
9171
|
}
|
|
9172
|
+
function isDynamicTagLocal(name, ctx2) {
|
|
9173
|
+
if (!hasDynamicTagBinding(name, ctx2.sourceFile)) return false;
|
|
9174
|
+
const a = ctx2.analyzer;
|
|
9175
|
+
if (a.jsxConstants?.has(name)) return false;
|
|
9176
|
+
if (a.jsxFunctions?.has(name)) return false;
|
|
9177
|
+
if (a.jsxMultiReturnFunctions?.has(name)) return false;
|
|
9178
|
+
if (a.inlineableJsxConsts?.has(name)) return false;
|
|
9179
|
+
return true;
|
|
9180
|
+
}
|
|
9181
|
+
function hasDynamicTagBinding(name, sourceFile) {
|
|
9182
|
+
let found = false;
|
|
9183
|
+
const visit3 = (node) => {
|
|
9184
|
+
if (found) return;
|
|
9185
|
+
if (ts11.isVariableDeclaration(node) && ts11.isIdentifier(node.name) && node.name.text === name && node.initializer) {
|
|
9186
|
+
let init = node.initializer;
|
|
9187
|
+
while (ts11.isAsExpression(init) || ts11.isSatisfiesExpression(init) || ts11.isParenthesizedExpression(init) || ts11.isNonNullExpression(init)) {
|
|
9188
|
+
init = init.expression;
|
|
9189
|
+
}
|
|
9190
|
+
if (ts11.isPropertyAccessExpression(init) && init.name.text === "tag") {
|
|
9191
|
+
found = true;
|
|
9192
|
+
return;
|
|
9193
|
+
}
|
|
9194
|
+
}
|
|
9195
|
+
ts11.forEachChild(node, visit3);
|
|
9196
|
+
};
|
|
9197
|
+
visit3(sourceFile);
|
|
9198
|
+
return found;
|
|
9199
|
+
}
|
|
8948
9200
|
function tryResolveIdentifierAsTemplateLiteral(ident, ctx2) {
|
|
8949
9201
|
const constInfo = findLocalConst(ident.text, ctx2);
|
|
8950
9202
|
if (!constInfo) return null;
|
|
@@ -9950,7 +10202,7 @@ function collectInnerLoops(nodes, siblingOffsets, outerLoopParam, ctx2, options)
|
|
|
9950
10202
|
}
|
|
9951
10203
|
function decideLoopRendering(loop, siblingOffsets, ctx2) {
|
|
9952
10204
|
const hasNestedComps = (loop.nestedComponents?.length ?? 0) > 0;
|
|
9953
|
-
const innerLoops =
|
|
10205
|
+
const innerLoops = collectInnerLoops(loop.children, siblingOffsets, loop.param, ctx2);
|
|
9954
10206
|
const hasInnerLoops = (innerLoops?.length ?? 0) > 0;
|
|
9955
10207
|
const useElementReconciliation = !loop.childComponent && !loop.isStaticArray && (hasNestedComps || hasInnerLoops);
|
|
9956
10208
|
return { useElementReconciliation, innerLoops };
|
|
@@ -10224,7 +10476,9 @@ function collectFromElement(element, ctx2, insideConditional = false) {
|
|
|
10224
10476
|
const elemRestName = ctx2.restPropsName;
|
|
10225
10477
|
const elemPropsObjName = ctx2.propsObjectName;
|
|
10226
10478
|
if (spreadVal && (spreadVal === elemRestName || spreadVal === elemPropsObjName)) {
|
|
10227
|
-
const
|
|
10479
|
+
const consumedKeys = spreadVal === elemRestName ? ctx2.propsParams.map((p) => p.name) : [];
|
|
10480
|
+
const staticAttrKeys = element.attrs.filter((a) => a.name !== "...").map((a) => a.name);
|
|
10481
|
+
const excludeKeys = [.../* @__PURE__ */ new Set([...consumedKeys, ...staticAttrKeys])];
|
|
10228
10482
|
ctx2.restAttrElements.push({
|
|
10229
10483
|
slotId: element.slotId,
|
|
10230
10484
|
source: PROPS_PARAM,
|
|
@@ -12831,7 +13085,9 @@ function buildStaticArrayChildInitsPlan(ctx2) {
|
|
|
12831
13085
|
(c) => (c.loopDepth ?? 0) === innerLoop.depth && c.innerLoopArray === innerLoop.array
|
|
12832
13086
|
);
|
|
12833
13087
|
if (innerComps.length === 0) continue;
|
|
12834
|
-
plans.push(
|
|
13088
|
+
plans.push(
|
|
13089
|
+
elem.childComponent ? buildComponentRootedInnerLoopPlan(elem, innerLoop, innerComps) : buildInnerLoopNestedPlan(elem, innerLoop, innerComps)
|
|
13090
|
+
);
|
|
12835
13091
|
}
|
|
12836
13092
|
}
|
|
12837
13093
|
}
|
|
@@ -12892,6 +13148,25 @@ function buildInnerLoopNestedPlan(elem, innerLoop, innerComps) {
|
|
|
12892
13148
|
comps
|
|
12893
13149
|
};
|
|
12894
13150
|
}
|
|
13151
|
+
function buildComponentRootedInnerLoopPlan(elem, innerLoop, innerComps) {
|
|
13152
|
+
const comps = innerComps.map((comp) => ({
|
|
13153
|
+
componentName: comp.name,
|
|
13154
|
+
selector: buildCompSelector(comp),
|
|
13155
|
+
propsExpr: buildStaticPropsExpr(comp.props)
|
|
13156
|
+
}));
|
|
13157
|
+
return {
|
|
13158
|
+
kind: "component-rooted-inner-loop",
|
|
13159
|
+
containerVar: `_${varSlotId(elem.slotId)}`,
|
|
13160
|
+
outerArrayExpr: elem.array,
|
|
13161
|
+
outerParam: elem.param,
|
|
13162
|
+
outerPreludeStatements: elem.mapPreamble ? [elem.mapPreamble] : [],
|
|
13163
|
+
innerArrayExpr: innerLoop.array,
|
|
13164
|
+
innerParam: innerLoop.param,
|
|
13165
|
+
innerPreludeStatements: innerLoop.mapPreamble ? [innerLoop.mapPreamble] : [],
|
|
13166
|
+
depth: innerLoop.depth,
|
|
13167
|
+
comps
|
|
13168
|
+
};
|
|
13169
|
+
}
|
|
12895
13170
|
function buildStaticPropsExpr(props) {
|
|
12896
13171
|
const entries = props.map((p) => {
|
|
12897
13172
|
if (p.isEventHandler) {
|
|
@@ -12939,6 +13214,9 @@ function stringifyOne(lines, plan) {
|
|
|
12939
13214
|
case "inner-loop-nested":
|
|
12940
13215
|
emitInnerLoopNested(lines, plan);
|
|
12941
13216
|
break;
|
|
13217
|
+
case "component-rooted-inner-loop":
|
|
13218
|
+
emitComponentRootedInnerLoop(lines, plan);
|
|
13219
|
+
break;
|
|
12942
13220
|
}
|
|
12943
13221
|
}
|
|
12944
13222
|
function emitSingleComp(lines, plan) {
|
|
@@ -13018,6 +13296,44 @@ function emitInnerLoopNested(lines, plan) {
|
|
|
13018
13296
|
lines.push(` }`);
|
|
13019
13297
|
lines.push("");
|
|
13020
13298
|
}
|
|
13299
|
+
function emitComponentRootedInnerLoop(lines, plan) {
|
|
13300
|
+
const {
|
|
13301
|
+
containerVar,
|
|
13302
|
+
outerArrayExpr,
|
|
13303
|
+
outerParam,
|
|
13304
|
+
outerPreludeStatements,
|
|
13305
|
+
innerArrayExpr,
|
|
13306
|
+
innerParam,
|
|
13307
|
+
innerPreludeStatements,
|
|
13308
|
+
depth,
|
|
13309
|
+
comps
|
|
13310
|
+
} = plan;
|
|
13311
|
+
const scopesVar = (i) => comps.length > 1 ? `__compScopes${i}` : "__compScopes";
|
|
13312
|
+
const cursorVar = (i) => comps.length > 1 ? `__ci${i}` : "__ci";
|
|
13313
|
+
const compElVar = (i) => comps.length > 1 ? `__compEl${i}` : "__compEl";
|
|
13314
|
+
lines.push(` // Initialize component-rooted inner-loop components (depth ${depth})`);
|
|
13315
|
+
lines.push(` if (${containerVar}) {`);
|
|
13316
|
+
comps.forEach((comp, i) => {
|
|
13317
|
+
lines.push(` const ${scopesVar(i)} = qsaChildScopes(${containerVar}, ${comp.selector})`);
|
|
13318
|
+
lines.push(` let ${cursorVar(i)} = 0`);
|
|
13319
|
+
});
|
|
13320
|
+
lines.push(` ${outerArrayExpr}.forEach((${outerParam}) => {`);
|
|
13321
|
+
for (const stmt of outerPreludeStatements) {
|
|
13322
|
+
lines.push(` ${stmt}`);
|
|
13323
|
+
}
|
|
13324
|
+
lines.push(` ${innerArrayExpr}.forEach((${innerParam}) => {`);
|
|
13325
|
+
for (const stmt of innerPreludeStatements) {
|
|
13326
|
+
lines.push(` ${stmt}`);
|
|
13327
|
+
}
|
|
13328
|
+
comps.forEach((comp, i) => {
|
|
13329
|
+
lines.push(` const ${compElVar(i)} = ${scopesVar(i)}[${cursorVar(i)}++]`);
|
|
13330
|
+
lines.push(` if (${compElVar(i)}) initChild('${nameForRegistryRef(comp.componentName)}', ${compElVar(i)}, ${comp.propsExpr})`);
|
|
13331
|
+
});
|
|
13332
|
+
lines.push(` })`);
|
|
13333
|
+
lines.push(` })`);
|
|
13334
|
+
lines.push(` }`);
|
|
13335
|
+
lines.push("");
|
|
13336
|
+
}
|
|
13021
13337
|
var init_static_array_child_init = __esm({
|
|
13022
13338
|
"../jsx/src/ir-to-client-js/stringify/static-array-child-init.ts"() {
|
|
13023
13339
|
"use strict";
|
|
@@ -17573,6 +17889,15 @@ function emitParsedExpr(expr, emitter) {
|
|
|
17573
17889
|
if (expr.method === "sort" || expr.method === "toSorted") {
|
|
17574
17890
|
return emitter.sortMethod(expr.method, expr.object, expr.comparator, emit);
|
|
17575
17891
|
}
|
|
17892
|
+
if (expr.method === "reduce" || expr.method === "reduceRight") {
|
|
17893
|
+
return emitter.reduceMethod(expr.method, expr.object, expr.reduceOp, emit);
|
|
17894
|
+
}
|
|
17895
|
+
if (expr.method === "flat") {
|
|
17896
|
+
return emitter.flatMethod(expr.object, expr.flatDepth, emit);
|
|
17897
|
+
}
|
|
17898
|
+
if (expr.method === "flatMap") {
|
|
17899
|
+
return emitter.flatMapMethod(expr.object, expr.flatMapOp, emit);
|
|
17900
|
+
}
|
|
17576
17901
|
return emitter.arrayMethod(expr.method, expr.object, expr.args, emit);
|
|
17577
17902
|
case "unsupported":
|
|
17578
17903
|
return emitter.unsupported(expr.raw, expr.reason);
|
|
@@ -22713,264 +23038,70 @@ main {
|
|
|
22713
23038
|
}
|
|
22714
23039
|
});
|
|
22715
23040
|
|
|
22716
|
-
// src/lib/adapters/
|
|
22717
|
-
|
|
22718
|
-
|
|
22719
|
-
|
|
22720
|
-
execSync("bun --version", { stdio: "ignore" });
|
|
22721
|
-
return [];
|
|
22722
|
-
} catch {
|
|
22723
|
-
return [
|
|
22724
|
-
"Bun not found on PATH. The CSR starter's server uses `Bun.serve`. Install Bun (https://bun.sh) before `bun run dev`."
|
|
22725
|
-
];
|
|
22726
|
-
}
|
|
22727
|
-
}
|
|
22728
|
-
var CSR_GITIGNORE, CSR_BAREFOOT_CONFIG_TS, CSR_SERVER_TS, CSR_INDEX_HTML, CSR_TSCONFIG, CSR_ADAPTER;
|
|
22729
|
-
var init_csr = __esm({
|
|
22730
|
-
"src/lib/adapters/csr.ts"() {
|
|
23041
|
+
// src/lib/adapters/runtimes.generated.ts
|
|
23042
|
+
var bfGoSource, streamingGoSource, bfdevGoSource, barefootPmSource, barefootBackendMojoPmSource, barefootPluginPmSource, barefootDevReloadPmSource;
|
|
23043
|
+
var init_runtimes_generated = __esm({
|
|
23044
|
+
"src/lib/adapters/runtimes.generated.ts"() {
|
|
22731
23045
|
"use strict";
|
|
22732
|
-
init_shared2();
|
|
22733
|
-
|
|
22734
|
-
|
|
22735
|
-
|
|
22736
|
-
|
|
22737
|
-
|
|
22738
|
-
]);
|
|
22739
|
-
CSR_BAREFOOT_CONFIG_TS = `import { createConfig } from '@barefootjs/client/build'
|
|
22740
|
-
|
|
22741
|
-
export default createConfig({
|
|
22742
|
-
paths: {
|
|
22743
|
-
components: 'components/ui',
|
|
22744
|
-
tokens: 'tokens',
|
|
22745
|
-
meta: 'meta',
|
|
22746
|
-
},
|
|
22747
|
-
components: ['components'],
|
|
22748
|
-
outDir: 'dist',
|
|
22749
|
-
})
|
|
22750
|
-
`;
|
|
22751
|
-
CSR_SERVER_TS = `// Tiny Bun server for the CSR starter:
|
|
22752
|
-
// - HTML pages from \`./pages/<name>.html\` (\`/\` \u2192 index.html)
|
|
22753
|
-
// - Compiled component bundles from \`./dist/components/\` at /static/components/
|
|
22754
|
-
// - Other static assets from \`./public/\` at /static/
|
|
23046
|
+
bfGoSource = '// Package bf provides runtime helper functions for BarefootJS Go templates.\n// These functions mirror JavaScript behavior for consistent SSR output.\npackage bf\n\nimport (\n "bytes"\n "encoding/json"\n "fmt"\n "html/template"\n "math"\n "math/rand"\n "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_flat": Flat,\n "bf_flat_map": FlatMap,\n "bf_flat_map_tuple": FlatMapTuple,\n "bf_first": First,\n "bf_last": Last,\n "bf_arr": Arr,\n "bf_filter_truthy": FilterTruthy,\n\n // Higher-order Array Methods\n "bf_every": Every,\n "bf_some": Some,\n "bf_filter": Filter,\n "bf_find": Find,\n "bf_find_index": FindIndex,\n "bf_find_last": FindLast,\n "bf_find_last_index": FindLastIndex,\n "bf_sort": Sort,\n "bf_reduce": Reduce,\n\n // 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// Flat flattens nested slices/arrays `depth` levels deep. Lowers\n// `Array.prototype.flat(depth?)` (#1448 Tier C). A `depth` of `-1` is the\n// `Infinity` sentinel (flatten fully); `0` (or negative-from-JS, already\n// normalised to 0 at compile time) returns a shallow copy. Non-array\n// elements are kept as-is (JS only flattens nested arrays). A non-array\n// receiver returns an empty `[]any`.\nfunc Flat(items any, depth int) []any {\n v := reflect.ValueOf(items)\n if v.Kind() != reflect.Slice && v.Kind() != reflect.Array {\n return []any{}\n }\n out := make([]any, 0, v.Len())\n for i := 0; i < v.Len(); i++ {\n el := v.Index(i).Interface()\n ev := reflect.ValueOf(el)\n if depth != 0 && (ev.Kind() == reflect.Slice || ev.Kind() == reflect.Array) {\n // `-1` (Infinity) recurses unbounded; a finite depth spends one level.\n next := depth\n if depth > 0 {\n next = depth - 1\n }\n out = append(out, Flat(el, next)...)\n } else {\n out = append(out, el)\n }\n }\n return out\n}\n\n// FlatMap projects each element through a `self` / `field` projection and\n// flattens the result one level. Lowers value-returning\n// `Array.prototype.flatMap(fn)` for the field-projection catalogue\n// (#1448 Tier C): `items.flatMap(i => i)` (self) and\n// `items.flatMap(i => i.field)` (field). A projected non-array value is\n// kept as-is (flatMap = map + flat(1)). Non-array receiver \u2192 empty.\nfunc FlatMap(items any, keyKind, keyName string) []any {\n v := reflect.ValueOf(items)\n if v.Kind() != reflect.Slice && v.Kind() != reflect.Array {\n return []any{}\n }\n projected := make([]any, 0, v.Len())\n for i := 0; i < v.Len(); i++ {\n el := v.Index(i).Interface()\n if keyKind == "field" {\n projected = append(projected, getFieldValue(el, keyName))\n } else {\n projected = append(projected, el)\n }\n }\n return Flat(projected, 1)\n}\n\n// FlatMapTuple lowers an array-literal flatMap projection\n// `items.flatMap(i => [i.a, i.b])` (#1448 Tier C). `specs` is a flat list\n// of (kind, name) pairs, one per array-literal leaf: ("self", "") for the\n// item itself, ("field", "<Name>") for a struct field. For each item it\n// appends every leaf\'s value in order. Unlike the scalar `FlatMap`, the\n// per-item array is flattened only one level (flat(1) removes the literal\n// wrapper), so an array-valued leaf is appended verbatim rather than\n// spread \u2014 which is exactly "append each leaf". Non-array receiver \u2192 empty.\nfunc FlatMapTuple(items any, specs ...string) []any {\n v := reflect.ValueOf(items)\n if v.Kind() != reflect.Slice && v.Kind() != reflect.Array {\n return []any{}\n }\n out := make([]any, 0, v.Len())\n for i := 0; i < v.Len(); i++ {\n el := v.Index(i).Interface()\n for j := 0; j+1 < len(specs); j += 2 {\n if specs[j] == "field" {\n out = append(out, getFieldValue(el, specs[j+1]))\n } else {\n out = append(out, el)\n }\n }\n }\n return out\n}\n\n// First returns the first element of a slice, or nil if empty.\nfunc First(items any) any {\n return At(items, 0)\n}\n\n// Last returns the last element of a slice, or nil if empty.\nfunc Last(items any) any {\n return At(items, -1)\n}\n\n// Arr builds an []any from variadic args. Used to lower JS array\n// literals like `[a, b]` for the registry Slot\'s\n// `[className, childClass].filter(Boolean).join(\' \')` shape (#1443) \u2014\n// Go templates have no array-literal syntax, so the codegen routes\n// array-literal IR nodes through this helper.\nfunc Arr(items ...any) []any {\n return items\n}\n\n// FilterTruthy returns a new slice containing only truthy items.\n// Mirrors `arr.filter(Boolean)` semantics: drop nil, false, 0, "" \u2014 the\n// same falsy set JavaScript\'s `Boolean(x)` recognises. Used to lower\n// the registry Slot\'s class-merge pattern (#1443); generalising to\n// arbitrary callable predicates would need the callee-resolution path\n// blocked by #1389, so this stays Boolean-specific.\nfunc FilterTruthy(items any) []any {\n v := reflect.ValueOf(items)\n if !v.IsValid() || (v.Kind() != reflect.Slice && v.Kind() != reflect.Array) {\n return nil\n }\n result := make([]any, 0, v.Len())\n for i := 0; i < v.Len(); i++ {\n raw := v.Index(i).Interface()\n if isTruthy(raw) {\n result = append(result, raw)\n }\n }\n return result\n}\n\n// 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 // All-lowercase fallback: a Go-initialism field projects as an\n // all-caps key (`id` \u2192 `ID`), and `decapitalize("ID")` only\n // lowers the first char (`iD`), so the JS-keyed map ("id") still\n // misses. Try the fully-lowered key last to resolve it.\n if lower := strings.ToLower(field); lower != field && lower != decapitalize(field) {\n if r, ok := lookup(lower); ok {\n return r\n }\n }\n return nil\n }\n\n if v.Kind() != reflect.Struct {\n return nil\n }\n\n fieldVal := v.FieldByName(field)\n if !fieldVal.IsValid() {\n 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// Reduce folds an array into a scalar via the arithmetic-fold\n// catalogue (#1448 Tier C). It lowers `Array.prototype.reduce(fn, init)`\n// and `Array.prototype.reduceRight(fn, init)` for the shapes\n// `(acc, x) => acc <op> x` and `(acc, x) => acc <op> x.field`:\n//\n// bf_reduce <items> "<op>" "<keyKind>" "<keyName>" "<type>" "<init>" "<direction>"\n//\n// direction: "left" (reduce) | "right" (reduceRight). Only changes the\n// result for string concatenation; numeric folds commute.\n//\n// op: "+" | "*"\n// keyKind: "self" | "field"\n// keyName: "" when keyKind == "self"; capitalised struct field name\n// (e.g. "Duration") otherwise\n// type: "numeric" | "string"\n// init: the fold\'s start value \u2014 the compiler emits the *decoded*\n// seed, so numeric inits arrive as canonical decimal\n// (`1_000`/`0x10` already normalised to `1000`/`16`) that\n// ParseFloat accepts, and string inits arrive as escape-free\n// contents\n//\n// Numeric folds accumulate as float64; each projected key is read via\n// `toFloat64WithOK`, so numeric *strings* ("5" \u2192 5) parse and\n// non-numeric values fold as 0 \u2014 matching Perl\'s\n// `looks_like_number ? $n : 0` so the two template adapters stay\n// byte-equal. String folds concatenate (toString per projected key,\n// matching the documented `bf->string(undef) === ""` convention). The\n// init seeds the accumulator, so an empty receiver returns the init\n// unchanged \u2014 exactly like JS `reduce(fn, init)`. Anything outside the\n// catalogue refuses at compile time (BF101 from the JSX compiler) and\n// never reaches here.\n//\n// Two documented divergences from the JS / Hono path, both rare and\n// mirroring the `bf_sort` "auto" caveat:\n// - float64 stringification differs for sums whose binary expansion\n// isn\'t exact (e.g. 0.1 + 0.2);\n// - numeric-*string* keys fold numerically here, but JS `+`\n// string-concatenates once an operand is a string, so\n// numeric-string data can render differently under CSR.\n// Genuine numbers \u2014 the common SSR case \u2014 agree across all three.\nfunc Reduce(items any, op, keyKind, keyName, typ, init, direction string) any {\n v := reflect.ValueOf(items)\n isSlice := v.Kind() == reflect.Slice || v.Kind() == reflect.Array\n\n // `direction == "right"` (reduceRight) folds right-to-left. Only\n // observable for string concatenation \u2014 numeric sum / product are\n // commutative, so the order doesn\'t change the result there. Build a\n // start/stop/step triple so both folds share one loop shape.\n start, stop, step := 0, 0, 1\n if isSlice {\n stop = v.Len()\n if direction == "right" {\n start, stop, step = v.Len()-1, -1, -1\n }\n }\n\n if typ == "string" {\n acc := init\n if isSlice {\n for i := start; i != stop; i += step {\n key := projectSortKey(v.Index(i).Interface(), keyKind, keyName)\n acc += toString(key)\n }\n }\n return acc\n }\n\n // numeric fold\n acc, _ := strconv.ParseFloat(strings.TrimSpace(init), 64)\n if isSlice {\n for i := start; i != stop; i += step {\n key := projectSortKey(v.Index(i).Interface(), keyKind, keyName)\n // `toFloat64WithOK` parses numeric *strings* ("5" \u2192 5) and\n // returns 0 for non-numeric values \u2014 mirroring Perl\'s\n // `looks_like_number ? $n : 0` so numeric-string data folds\n // byte-equal across adapters (the same rule `bf_sort`\'s\n // "auto" compare uses). Plain `toFloat64` would zero "5".\n n, _ := toFloat64WithOK(key)\n if op == "*" {\n acc *= n\n } else {\n acc += n\n }\n }\n }\n return acc\n}\n\n// =============================================================================\n// HTML/Template Helpers\n// =============================================================================\n\n// Comment returns an HTML comment string for hydration markers.\n// The "bf-" prefix is automatically added.\nfunc Comment(content string) template.HTML {\n return template.HTML("<!--bf-" + content + "-->")\n}\n\n// TextStart returns an HTML comment start marker for reactive text expressions.\n// Format: <!--bf:slotId-->\nfunc TextStart(slotId string) template.HTML {\n return template.HTML("<!--bf:" + slotId + "-->")\n}\n\n// TextEnd returns an HTML comment end marker for reactive text expressions.\n// Format: <!--/-->\nfunc TextEnd() template.HTML {\n return "<!--/-->"\n}\n\n// ScopeComment emits a fragment-rooted scope marker. See spec/compiler.md\n// "Slot identity" for the wire format. Loud-fails on marshal errors\n// (same policy as JSON / BfPropsAttr).\nfunc ScopeComment(props interface{}) (template.HTML, error) {\n scopeID := getStringField(props, "ScopeID")\n hostSegment := ""\n if host := getStringField(props, "BfParent"); host != "" {\n mount := getStringField(props, "BfMount")\n hostSegment = "|h=" + host + "|m=" + mount\n }\n propsJSON := ""\n if getBoolField(props, "BfIsRoot") {\n pJSON, err := json.Marshal(props)\n if err != nil {\n return "", err\n }\n propsJSON = "|" + string(pJSON)\n }\n return template.HTML("<!--bf-scope:" + scopeID + hostSegment + propsJSON + "-->"), nil\n}\n\n// 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 setScopeIDsOnSlice(slice)\n setScriptsOnSlice(slice, scriptCollector)\n setPortalsOnSlice(slice, portalCollector)\n setBoolOnSlice(slice, "BfIsChild", true)\n }\n\n // Auto-detect and process single child component props\n singleChildren := findSingleChildComponents(opts.Props)\n for _, child := range singleChildren {\n setScopeIDOnSingle(child)\n setScriptsOnSingle(child, scriptCollector)\n setPortalsOnSingle(child, portalCollector)\n setBoolField(child, "BfIsChild", true)\n }\n\n // Mark the root component so BfPropsAttr emits bf-p only for it\n setBoolField(opts.Props, "BfIsRoot", true)\n\n // Render the component template.\n //\n // Errors here are NOT silently dropped. The original implementation\n // ignored the return value of `ExecuteTemplate`, which masked a real\n // onboarding failure mode: a template referencing a non-existent\n // field (`.Todo.Done` instead of the range dot\'s `.Done`) caused\n // html/template to abort mid-stream, the partial output got\n // returned, and the HTTP server happily flushed a 200 with a\n // truncated body. No error log, no signal \u2014 the user just saw a\n // blank list (#1442 echo TodoApp repro).\n //\n // Now we capture the error and replace the partial output with a\n // visible inline panel (dev mode) or a fenced error comment\n // (production), so the cause is on-screen and grep-able in logs.\n // Either way the renderer also writes to stderr so structured log\n // aggregators see it.\n var componentBuf strings.Builder\n if err := r.templates.ExecuteTemplate(&componentBuf, opts.ComponentName, opts.Props); err != nil {\n fmt.Fprintf(os.Stderr, "barefoot: template %q failed to render: %v\\n", opts.ComponentName, err)\n // Preserve whatever the template did manage to emit before\n // failing (Go\'s text/template flushes incrementally), but\n // follow it with a clearly-marked error block so the user\n // notices something is wrong instead of seeing a silent\n // truncation.\n componentBuf.WriteString(renderTemplateErrorPanel(opts.ComponentName, err))\n }\n\n // 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// scopeIDChars is the alphabet for auto-generated ScopeID suffixes. It\n// mirrors the `randomID` helper the go-template adapter emits into the\n// generated New<Component>Props constructors so runtime-assigned and\n// constructor-assigned ids are indistinguishable.\nconst scopeIDChars = "abcdefghijklmnopqrstuvwxyz0123456789"\n\n// randomScopeSuffix returns a random lowercase-alphanumeric string of\n// length n. math/rand (auto-seeded since Go 1.20) is sufficient here: the\n// suffix only needs to be unique enough to keep a page\'s bf-s scope ids\n// from colliding, not cryptographically unpredictable.\nfunc randomScopeSuffix(n int) string {\n b := make([]byte, n)\n for i := range b {\n b[i] = scopeIDChars[rand.Intn(len(scopeIDChars))]\n }\n return string(b)\n}\n\n// scopeIDPrefix derives the human-readable ScopeID prefix from a child\n// component\'s type, e.g. `TodoItemProps` \u2192 `TodoItem`. Matches the\n// `"<Component>_" + randomID(6)` shape the generated constructors use.\nfunc scopeIDPrefix(t reflect.Type) string {\n for t.Kind() == reflect.Ptr {\n t = t.Elem()\n }\n return strings.TrimSuffix(t.Name(), "Props")\n}\n\n// assignScopeID fills a child component\'s ScopeID with a generated id when\n// the caller left it empty, so application code doesn\'t have to mint scope\n// ids by hand (the parent\'s New<Component>Props constructor does the same\n// for components built through it). A non-empty ScopeID is left untouched,\n// so callers can still pin a stable id when they need one.\nfunc assignScopeID(structVal reflect.Value, prefix string) {\n field := structVal.FieldByName("ScopeID")\n if !field.IsValid() || !field.CanSet() || field.Kind() != reflect.String {\n return\n }\n if field.String() != "" {\n return\n }\n id := randomScopeSuffix(6)\n if prefix != "" {\n id = prefix + "_" + id\n }\n field.SetString(id)\n}\n\n// setScopeIDsOnSlice assigns a generated ScopeID to every child in a slice\n// whose ScopeID is empty.\nfunc setScopeIDsOnSlice(slice interface{}) {\n v := reflect.ValueOf(slice)\n if v.Kind() != reflect.Slice {\n return\n }\n prefix := scopeIDPrefix(v.Type().Elem())\n for i := 0; i < v.Len(); i++ {\n item := v.Index(i)\n if item.Kind() == reflect.Ptr {\n if item.IsNil() {\n continue\n }\n item = item.Elem()\n }\n if item.Kind() == reflect.Struct {\n assignScopeID(item, prefix)\n }\n }\n}\n\n// setScopeIDOnSingle assigns a generated ScopeID to a single child\n// component when its ScopeID is empty.\nfunc setScopeIDOnSingle(child interface{}) {\n v := reflect.ValueOf(child)\n if v.Kind() == reflect.Ptr {\n v = v.Elem()\n }\n if v.Kind() != reflect.Struct {\n return\n }\n assignScopeID(v, scopeIDPrefix(v.Type()))\n}\n\n// findChildComponentSlices finds slice fields containing child component props.\n// Child props are identified by having ScopeID and Scripts fields.\nfunc findChildComponentSlices(props interface{}) []interface{} {\n var result []interface{}\n\n val := reflect.ValueOf(props)\n if val.Kind() == reflect.Ptr {\n val = val.Elem()\n }\n if val.Kind() != reflect.Struct {\n return result\n }\n\n for i := 0; i < val.NumField(); i++ {\n field := val.Field(i)\n if field.Kind() != reflect.Slice || field.Len() == 0 {\n continue\n }\n\n elem := field.Index(0)\n if elem.Kind() == reflect.Ptr {\n elem = elem.Elem()\n }\n if elem.Kind() != reflect.Struct {\n continue\n }\n\n hasScopeID := elem.FieldByName("ScopeID").IsValid()\n hasScripts := elem.FieldByName("Scripts").IsValid()\n\n if hasScopeID && hasScripts {\n result = append(result, field.Interface())\n }\n }\n\n return result\n}\n\n// setScriptsOnSlice sets Scripts on all items in a slice.\nfunc setScriptsOnSlice(slice interface{}, collector *ScriptCollector) {\n val := reflect.ValueOf(slice)\n if val.Kind() != reflect.Slice {\n return\n }\n for i := 0; i < val.Len(); i++ {\n item := val.Index(i)\n if item.Kind() == reflect.Ptr {\n item = item.Elem()\n }\n if item.Kind() == reflect.Struct {\n field := item.FieldByName("Scripts")\n if field.IsValid() && field.CanSet() {\n field.Set(reflect.ValueOf(collector))\n }\n }\n }\n}\n\n// setBoolOnSlice sets a bool field on all items in a slice.\nfunc setBoolOnSlice(slice interface{}, fieldName string, val bool) {\n v := reflect.ValueOf(slice)\n if v.Kind() != reflect.Slice {\n return\n }\n for i := 0; i < v.Len(); i++ {\n item := v.Index(i)\n if item.Kind() == reflect.Ptr {\n item = item.Elem()\n }\n if item.Kind() == reflect.Struct {\n field := item.FieldByName(fieldName)\n if field.IsValid() && field.CanSet() && field.Kind() == reflect.Bool {\n field.SetBool(val)\n }\n }\n }\n}\n\n// setPortalsOnSlice sets Portals on all items in a slice.\nfunc setPortalsOnSlice(slice interface{}, collector *PortalCollector) {\n val := reflect.ValueOf(slice)\n if val.Kind() != reflect.Slice {\n return\n }\n for i := 0; i < val.Len(); i++ {\n item := val.Index(i)\n if item.Kind() == reflect.Ptr {\n item = item.Elem()\n }\n if item.Kind() == reflect.Struct {\n field := item.FieldByName("Portals")\n if field.IsValid() && field.CanSet() {\n field.Set(reflect.ValueOf(collector))\n }\n }\n }\n}\n\n\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';
|
|
23047
|
+
streamingGoSource = `// Package bf \u2014 Out-of-Order Streaming SSR helpers
|
|
23048
|
+
//
|
|
23049
|
+
// Provides StreamRenderer for progressive page rendering using HTTP
|
|
23050
|
+
// chunked transfer encoding. Async boundaries display fallback content
|
|
23051
|
+
// immediately (fast TTFB), then swap in resolved content as data arrives.
|
|
22755
23052
|
//
|
|
22756
|
-
//
|
|
22757
|
-
//
|
|
23053
|
+
// Works with any Go HTTP server that supports http.Flusher (net/http,
|
|
23054
|
+
// chi, gorilla/mux, echo, fiber, etc.).
|
|
23055
|
+
package bf
|
|
22758
23056
|
|
|
22759
|
-
import
|
|
23057
|
+
import (
|
|
23058
|
+
"bytes"
|
|
23059
|
+
"fmt"
|
|
23060
|
+
"html/template"
|
|
23061
|
+
"net/http"
|
|
23062
|
+
"sync"
|
|
23063
|
+
)
|
|
22760
23064
|
|
|
22761
|
-
|
|
22762
|
-
|
|
22763
|
-
|
|
22764
|
-
|
|
23065
|
+
// AsyncBoundary defines a region of the page that loads asynchronously.
|
|
23066
|
+
// The fallback is sent immediately; resolved content streams in later.
|
|
23067
|
+
type AsyncBoundary struct {
|
|
23068
|
+
// ID is the unique boundary identifier (e.g., "a0", "a1").
|
|
23069
|
+
ID string
|
|
22765
23070
|
|
|
22766
|
-
|
|
23071
|
+
// FallbackHTML is the loading/skeleton content shown immediately.
|
|
23072
|
+
FallbackHTML string
|
|
22767
23073
|
|
|
22768
|
-
|
|
22769
|
-
|
|
22770
|
-
|
|
22771
|
-
|
|
22772
|
-
|
|
23074
|
+
// Resolve produces the final HTML content for this boundary.
|
|
23075
|
+
// Called after the initial page flush; may perform I/O (DB, API, etc.).
|
|
23076
|
+
// Return an error to skip this boundary (fallback remains visible).
|
|
23077
|
+
Resolve func() (string, error)
|
|
23078
|
+
}
|
|
22773
23079
|
|
|
22774
|
-
|
|
22775
|
-
|
|
22776
|
-
|
|
22777
|
-
|
|
22778
|
-
return serveFromDir(PUBLIC_DIR, path.slice('/static/'.length))
|
|
22779
|
-
}
|
|
23080
|
+
// StreamOptions configures a single streaming render.
|
|
23081
|
+
type StreamOptions struct {
|
|
23082
|
+
// ComponentName is the template to render.
|
|
23083
|
+
ComponentName string
|
|
22780
23084
|
|
|
22781
|
-
|
|
22782
|
-
|
|
22783
|
-
},
|
|
22784
|
-
})
|
|
23085
|
+
// Props is the component props (same as RenderOptions.Props).
|
|
23086
|
+
Props interface{}
|
|
22785
23087
|
|
|
22786
|
-
|
|
22787
|
-
|
|
22788
|
-
|
|
22789
|
-
|
|
22790
|
-
|
|
22791
|
-
|
|
22792
|
-
|
|
22793
|
-
|
|
22794
|
-
|
|
22795
|
-
|
|
22796
|
-
|
|
23088
|
+
// Title is the page title (defaults to "{ComponentName} - BarefootJS").
|
|
23089
|
+
Title string
|
|
23090
|
+
|
|
23091
|
+
// Heading is the page heading.
|
|
23092
|
+
Heading string
|
|
23093
|
+
|
|
23094
|
+
// Extra holds additional data for the layout.
|
|
23095
|
+
Extra map[string]interface{}
|
|
23096
|
+
|
|
23097
|
+
// Boundaries lists async regions that will be streamed.
|
|
23098
|
+
Boundaries []AsyncBoundary
|
|
22797
23099
|
}
|
|
22798
23100
|
|
|
22799
|
-
|
|
22800
|
-
|
|
22801
|
-
|
|
22802
|
-
|
|
22803
|
-
case 'js': return 'application/javascript; charset=utf-8'
|
|
22804
|
-
case 'css': return 'text/css; charset=utf-8'
|
|
22805
|
-
case 'json': return 'application/json; charset=utf-8'
|
|
22806
|
-
case 'svg': return 'image/svg+xml'
|
|
22807
|
-
case 'png': return 'image/png'
|
|
22808
|
-
case 'jpg':
|
|
22809
|
-
case 'jpeg': return 'image/jpeg'
|
|
22810
|
-
default: return 'application/octet-stream'
|
|
22811
|
-
}
|
|
22812
|
-
}
|
|
22813
|
-
|
|
22814
|
-
console.log(\` \u279C http://localhost:\${server.port}\`)
|
|
22815
|
-
`;
|
|
22816
|
-
CSR_INDEX_HTML = `<!DOCTYPE html>
|
|
22817
|
-
<html lang="en">
|
|
22818
|
-
<head>
|
|
22819
|
-
<meta charset="utf-8">
|
|
22820
|
-
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
22821
|
-
<title>BarefootJS app</title>
|
|
22822
|
-
<script type="importmap">
|
|
22823
|
-
{ "imports": { "@barefootjs/client/runtime": "/static/components/barefoot.js" } }
|
|
22824
|
-
</script>
|
|
22825
|
-
<!-- Link all three sheets so the browser fetches them in parallel \u2014
|
|
22826
|
-
chaining via styles.css @import would defer tokens/uno to a
|
|
22827
|
-
second round-trip and flash unstyled DOM. tokens first so its
|
|
22828
|
-
CSS variables are defined before any rule references them. -->
|
|
22829
|
-
<link rel="stylesheet" href="/static/tokens.css">
|
|
22830
|
-
<link rel="stylesheet" href="/static/styles.css">
|
|
22831
|
-
<link rel="stylesheet" href="/static/uno.css">
|
|
22832
|
-
</head>
|
|
22833
|
-
<body>
|
|
22834
|
-
<main>
|
|
22835
|
-
<div id="app"></div>
|
|
22836
|
-
</main>
|
|
22837
|
-
<script type="module">
|
|
22838
|
-
import { render } from '@barefootjs/client/runtime'
|
|
22839
|
-
await import('/static/components/Counter.client.js')
|
|
22840
|
-
render(document.getElementById('app'), 'Counter', { initial: 0 })
|
|
22841
|
-
</script>
|
|
22842
|
-
</body>
|
|
22843
|
-
</html>
|
|
22844
|
-
`;
|
|
22845
|
-
CSR_TSCONFIG = `{
|
|
22846
|
-
"compilerOptions": {
|
|
22847
|
-
"target": "ESNext",
|
|
22848
|
-
"module": "ESNext",
|
|
22849
|
-
"moduleResolution": "bundler",
|
|
22850
|
-
"jsx": "react-jsx",
|
|
22851
|
-
"jsxImportSource": "@barefootjs/jsx",
|
|
22852
|
-
"types": ["bun-types"],
|
|
22853
|
-
"strict": true,
|
|
22854
|
-
"skipLibCheck": true,
|
|
22855
|
-
"esModuleInterop": true,
|
|
22856
|
-
"resolveJsonModule": true,
|
|
22857
|
-
"noEmit": true,
|
|
22858
|
-
"baseUrl": ".",
|
|
22859
|
-
"paths": {
|
|
22860
|
-
"@/components/*": ["./components/*"]
|
|
22861
|
-
}
|
|
22862
|
-
},
|
|
22863
|
-
"include": ["**/*.ts", "**/*.tsx"],
|
|
22864
|
-
"exclude": ["node_modules", "dist"]
|
|
22865
|
-
}
|
|
22866
|
-
`;
|
|
22867
|
-
CSR_ADAPTER = {
|
|
22868
|
-
label: "CSR (Bun, client-side rendering only)",
|
|
22869
|
-
port: 3003,
|
|
22870
|
-
files: {
|
|
22871
|
-
"server.ts": CSR_SERVER_TS,
|
|
22872
|
-
"pages/index.html": CSR_INDEX_HTML,
|
|
22873
|
-
"barefoot.config.ts": CSR_BAREFOOT_CONFIG_TS,
|
|
22874
|
-
"tsconfig.json": CSR_TSCONFIG,
|
|
22875
|
-
"uno.config.ts": unoConfigTs([
|
|
22876
|
-
"components/**/*.tsx",
|
|
22877
|
-
"dist/components/**/*.tsx",
|
|
22878
|
-
"pages/**/*.html"
|
|
22879
|
-
]),
|
|
22880
|
-
"components/Counter.tsx": SHARED_COUNTER_TSX,
|
|
22881
|
-
"components/Counter.test.tsx": SHARED_COUNTER_TEST_TSX,
|
|
22882
|
-
"public/styles.css": STYLES_CSS,
|
|
22883
|
-
"public/tokens.css": TOKENS_CSS,
|
|
22884
|
-
"public/uno.css": UNO_CSS_PLACEHOLDER,
|
|
22885
|
-
".gitignore": CSR_GITIGNORE
|
|
22886
|
-
},
|
|
22887
|
-
scripts: {
|
|
22888
|
-
dev: 'bf build && unocss && concurrently -k -n build,uno,server -c blue,magenta,green "bf build --watch" "unocss --watch" "bun --watch server.ts"',
|
|
22889
|
-
build: "bf build && unocss",
|
|
22890
|
-
start: "bun server.ts"
|
|
22891
|
-
},
|
|
22892
|
-
dependencies: {
|
|
22893
|
-
"@barefootjs/client": "latest",
|
|
22894
|
-
"@barefootjs/jsx": "latest",
|
|
22895
|
-
"@barefootjs/shared": "latest"
|
|
22896
|
-
},
|
|
22897
|
-
devDependencies: {
|
|
22898
|
-
...UNOCSS_DEV_DEPENDENCIES,
|
|
22899
|
-
"@barefootjs/cli": "latest",
|
|
22900
|
-
"@barefootjs/test": "latest",
|
|
22901
|
-
"@types/bun": "^1.1.0",
|
|
22902
|
-
concurrently: "^9.0.0",
|
|
22903
|
-
typescript: "^5.6.0"
|
|
22904
|
-
},
|
|
22905
|
-
prereqWarnings: () => bunPrereqs()
|
|
22906
|
-
};
|
|
22907
|
-
}
|
|
22908
|
-
});
|
|
22909
|
-
|
|
22910
|
-
// src/lib/adapters/runtimes.generated.ts
|
|
22911
|
-
var bfGoSource, streamingGoSource, barefootPmSource, barefootPluginPmSource, barefootDevReloadPmSource;
|
|
22912
|
-
var init_runtimes_generated = __esm({
|
|
22913
|
-
"src/lib/adapters/runtimes.generated.ts"() {
|
|
22914
|
-
"use strict";
|
|
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';
|
|
22916
|
-
streamingGoSource = `// Package bf \u2014 Out-of-Order Streaming SSR helpers
|
|
22917
|
-
//
|
|
22918
|
-
// Provides StreamRenderer for progressive page rendering using HTTP
|
|
22919
|
-
// chunked transfer encoding. Async boundaries display fallback content
|
|
22920
|
-
// immediately (fast TTFB), then swap in resolved content as data arrives.
|
|
22921
|
-
//
|
|
22922
|
-
// Works with any Go HTTP server that supports http.Flusher (net/http,
|
|
22923
|
-
// chi, gorilla/mux, echo, fiber, etc.).
|
|
22924
|
-
package bf
|
|
22925
|
-
|
|
22926
|
-
import (
|
|
22927
|
-
"bytes"
|
|
22928
|
-
"fmt"
|
|
22929
|
-
"html/template"
|
|
22930
|
-
"net/http"
|
|
22931
|
-
"sync"
|
|
22932
|
-
)
|
|
22933
|
-
|
|
22934
|
-
// AsyncBoundary defines a region of the page that loads asynchronously.
|
|
22935
|
-
// The fallback is sent immediately; resolved content streams in later.
|
|
22936
|
-
type AsyncBoundary struct {
|
|
22937
|
-
// ID is the unique boundary identifier (e.g., "a0", "a1").
|
|
22938
|
-
ID string
|
|
22939
|
-
|
|
22940
|
-
// FallbackHTML is the loading/skeleton content shown immediately.
|
|
22941
|
-
FallbackHTML string
|
|
22942
|
-
|
|
22943
|
-
// Resolve produces the final HTML content for this boundary.
|
|
22944
|
-
// Called after the initial page flush; may perform I/O (DB, API, etc.).
|
|
22945
|
-
// Return an error to skip this boundary (fallback remains visible).
|
|
22946
|
-
Resolve func() (string, error)
|
|
22947
|
-
}
|
|
22948
|
-
|
|
22949
|
-
// StreamOptions configures a single streaming render.
|
|
22950
|
-
type StreamOptions struct {
|
|
22951
|
-
// ComponentName is the template to render.
|
|
22952
|
-
ComponentName string
|
|
22953
|
-
|
|
22954
|
-
// Props is the component props (same as RenderOptions.Props).
|
|
22955
|
-
Props interface{}
|
|
22956
|
-
|
|
22957
|
-
// Title is the page title (defaults to "{ComponentName} - BarefootJS").
|
|
22958
|
-
Title string
|
|
22959
|
-
|
|
22960
|
-
// Heading is the page heading.
|
|
22961
|
-
Heading string
|
|
22962
|
-
|
|
22963
|
-
// Extra holds additional data for the layout.
|
|
22964
|
-
Extra map[string]interface{}
|
|
22965
|
-
|
|
22966
|
-
// Boundaries lists async regions that will be streamed.
|
|
22967
|
-
Boundaries []AsyncBoundary
|
|
22968
|
-
}
|
|
22969
|
-
|
|
22970
|
-
// StreamRenderer renders pages with out-of-order streaming support.
|
|
22971
|
-
type StreamRenderer struct {
|
|
22972
|
-
templates *template.Template
|
|
22973
|
-
layout LayoutFunc
|
|
23101
|
+
// StreamRenderer renders pages with out-of-order streaming support.
|
|
23102
|
+
type StreamRenderer struct {
|
|
23103
|
+
templates *template.Template
|
|
23104
|
+
layout LayoutFunc
|
|
22974
23105
|
}
|
|
22975
23106
|
|
|
22976
23107
|
// NewStreamRenderer creates a StreamRenderer with the given templates and layout.
|
|
@@ -23119,9 +23250,12 @@ func StreamingFuncMap() template.FuncMap {
|
|
|
23119
23250
|
}
|
|
23120
23251
|
}
|
|
23121
23252
|
`;
|
|
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";
|
|
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";
|
|
23253
|
+
bfdevGoSource = '// Package bfdev provides a dev-only browser auto-reload handler.\n//\n// It watches `<distDir>/.dev/build-id` (produced by `bf build --watch`\n// in the @barefootjs/cli package) and streams SSE `event: reload` whenever\n// the sentinel changes. Combined with the inline client snippet returned by\n// Snippet, editing a .tsx component triggers a browser reload automatically.\n//\n// The handler is framework-agnostic (net/http.Handler). Echo users can mount\n// it via echo.WrapHandler; other routers use it as-is.\n//\n// Example (Echo):\n//\n// if bfdev.IsDevDefault() {\n// e.GET("/_bf/reload", echo.WrapHandler(bfdev.NewReloadHandler(bfdev.Config{\n// DistDir: "./dist",\n// })))\n// }\n//\n// Example (net/http):\n//\n// http.Handle("/_bf/reload", bfdev.NewReloadHandler(bfdev.Config{DistDir: "./dist"}))\npackage bfdev\n\nimport (\n "fmt"\n "html/template"\n "net/http"\n "os"\n "path/filepath"\n "strings"\n "time"\n)\n\n// Sentinel path contract with `@barefootjs/cli`\n// (`packages/cli/src/lib/build.ts`, DEV_SENTINEL_SUBDIR / DEV_SENTINEL_FILENAME).\n// Duplicated here so the Go runtime avoids a dependency on the CLI. If the\n// CLI changes these values, update this package in the same PR.\nconst (\n devSubdir = ".dev"\n buildIDFile = "build-id"\n scrollStorageKey = "__bf_devreload_scroll"\n\n // heartbeatInterval keeps the SSE stream under the framework\'s idle\n // timeout (Bun.serve defaults to 10s; Go/Echo defaults are more forgiving\n // but middleware-level timeouts exist in the wild). 5s leaves comfortable\n // headroom.\n heartbeatInterval = 5 * time.Second\n\n // pollInterval is how often the handler checks `.dev/build-id`. Uses\n // polling instead of fsnotify to keep the runtime dependency-free \u2014 dev\n // latency of ~500ms is imperceptible next to the browser\'s reload time.\n pollInterval = 500 * time.Millisecond\n)\n\n// Config configures a dev reload handler or snippet.\ntype Config struct {\n // DistDir is the directory that `bf build` writes output into\n // (contains `.dev/build-id`). Required for the handler; ignored by\n // Snippet.\n DistDir string\n\n // Endpoint is the public SSE URL the client will connect to. Used only by\n // Snippet to populate the EventSource URL. Defaults to "/_bf/reload" when\n // empty.\n Endpoint string\n\n // Disabled, when true, makes NewReloadHandler return a 404 handler and\n // Snippet return an empty fragment. Intended for production builds.\n Disabled bool\n}\n\n// IsDevDefault reports whether the process is running in a development\n// environment using the common Go convention of APP_ENV=development.\n// Callers can use this to populate Config.Disabled:\n//\n// cfg := bfdev.Config{DistDir: "./dist", Disabled: !bfdev.IsDevDefault()}\nfunc IsDevDefault() bool {\n return os.Getenv("APP_ENV") == "development"\n}\n\n// NewReloadHandler returns an http.Handler that streams Server-Sent Events\n// and emits `event: reload` whenever `<DistDir>/.dev/build-id` changes. When\n// cfg.Disabled is true, the handler responds 404 and never opens a stream.\nfunc NewReloadHandler(cfg Config) http.Handler {\n if cfg.Disabled {\n return http.HandlerFunc(http.NotFound)\n }\n devDir := filepath.Join(cfg.DistDir, devSubdir)\n buildIDPath := filepath.Join(devDir, buildIDFile)\n // Ensure the directory exists so the first read does not race with the\n // initial build. Ignore the error: subsequent reads simply return "".\n _ = os.MkdirAll(devDir, 0o755)\n\n return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n flusher, ok := w.(http.Flusher)\n if !ok {\n http.Error(w, "streaming unsupported", http.StatusInternalServerError)\n return\n }\n h := w.Header()\n h.Set("Content-Type", "text/event-stream")\n h.Set("Cache-Control", "no-cache, no-transform")\n h.Set("Connection", "keep-alive")\n h.Set("X-Accel-Buffering", "no")\n\n send := func(chunk string) bool {\n if _, err := fmt.Fprint(w, chunk); err != nil {\n return false\n }\n flusher.Flush()\n return true\n }\n\n if !send("retry: 1000\\n\\n") {\n return\n }\n\n lastEventID := strings.TrimSpace(r.Header.Get("Last-Event-ID"))\n initialID := readBuildID(buildIDPath)\n lastSent := ""\n if initialID != "" {\n lastSent = initialID\n // When the client reconnects with a stale Last-Event-ID, a build\n // happened during its disconnected window \u2014 fire `reload`\n // immediately so the missed rebuild does not silently stay\n // unpainted until the next change.\n event := "hello"\n if lastEventID != "" && lastEventID != initialID {\n event = "reload"\n }\n if !send(fmt.Sprintf("event: %s\\nid: %s\\ndata: %s\\n\\n", event, initialID, initialID)) {\n return\n }\n }\n\n ctx := r.Context()\n hbTicker := time.NewTicker(heartbeatInterval)\n defer hbTicker.Stop()\n pollTicker := time.NewTicker(pollInterval)\n defer pollTicker.Stop()\n\n for {\n select {\n case <-ctx.Done():\n return\n case <-hbTicker.C:\n if !send(": hb\\n\\n") {\n return\n }\n case <-pollTicker.C:\n id := readBuildID(buildIDPath)\n if id == "" || id == lastSent {\n continue\n }\n lastSent = id\n if !send(fmt.Sprintf("event: reload\\nid: %s\\ndata: %s\\n\\n", id, id)) {\n return\n }\n }\n }\n })\n}\n\nfunc readBuildID(path string) string {\n b, err := os.ReadFile(path)\n if err != nil {\n return ""\n }\n return strings.TrimSpace(string(b))\n}\n\n// Snippet returns an inline <script> that subscribes to the SSE endpoint,\n// reloads on `reload`, and preserves window.scrollY across reloads via\n// sessionStorage. Returns an empty fragment when cfg.Disabled is true.\n//\n// Place it before </body>, typically just after the rendered scripts.\nfunc Snippet(cfg Config) template.HTML {\n if cfg.Disabled {\n return ""\n }\n endpoint := cfg.Endpoint\n if endpoint == "" {\n endpoint = "/_bf/reload"\n }\n // Small IIFE: EventSource subscriber + scrollY preservation. Idempotent\n // across duplicate mounts (guarded by window.__bfDevReload).\n js := fmt.Sprintf(\n `(function(){if(window.__bfDevReload)return;window.__bfDevReload=1;`+\n `try{var s=sessionStorage.getItem(%q);if(s){sessionStorage.removeItem(%q);`+\n `var y=parseInt(s,10);if(!isNaN(y)){var restore=function(){window.scrollTo(0,y)};`+\n `if(document.readyState===\'loading\'){addEventListener(\'DOMContentLoaded\',restore,{once:true})}else{restore()}}}}catch(e){}`+\n `var es=new EventSource(%q);`+\n `es.addEventListener(\'reload\',function(){try{sessionStorage.setItem(%q,String(window.scrollY))}catch(e){}location.reload()});`+\n `es.addEventListener(\'error\',function(){})})();`,\n scrollStorageKey, scrollStorageKey, endpoint, scrollStorageKey,\n )\n // Safe: `js` is assembled from package-internal literals plus `endpoint`\n // escaped by %q (Go-syntax quoting == valid JS string literal for the\n // ASCII endpoint paths this accepts).\n return template.HTML("<script>" + js + "</script>") //nolint:gosec\n}\n';
|
|
23254
|
+
barefootPmSource = "package BarefootJS;\nour $VERSION = \"0.01\";\nuse strict;\nuse warnings;\nuse utf8;\nuse feature 'signatures';\nno warnings 'experimental::signatures';\n\nuse POSIX ();\nuse Scalar::Util qw(looks_like_number weaken);\n\n# NOTE: This runtime is template-engine-agnostic AND framework-agnostic by\n# design, so it can ship as a standalone CPAN distribution. It depends only on\n# core Perl (subroutine signatures + the hand-rolled minimal accessor base\n# below \u2014 no Mojo::Base, no Class::Tiny). Every operation that depends on *how*\n# a template is rendered \u2014 JSON marshalling, raw-string marking, JSX-children\n# materialisation, and named-template rendering \u2014 is delegated to a pluggable\n# `backend` (see BarefootJS::Backend::Mojo for the reference Mojolicious\n# implementation), which is the only component that pulls in the Mojo\n# distribution, and only when it is actually used.\n\n# ---------------------------------------------------------------------------\n# Minimal accessor base (no Mojo::Base / Class::Tiny dependency)\n# ---------------------------------------------------------------------------\n#\n# Generates read/write accessors with optional lazy defaults so the runtime\n# stays free of any non-core OO base. Semantics mirror the Mojo::Base `has`\n# this class used to inherit: a getter returns the stored value (building it\n# from the default on first access if unset); a setter stores the value and\n# returns $self for chaining. A default is either a plain scalar or a coderef\n# invoked as `$default->($self)` (for per-instance refs like `[]` / `{}` and\n# the lazily-required Mojo backend).\nmy %ATTR_DEFAULT = (\n _scripts => sub { [] },\n _script_seen => sub { {} },\n _child_renderers => sub { {} },\n _is_child => 0,\n # Lazily fall back to the Mojo reference backend so a bare-blessed\n # instance (the pure-function unit tests) and the historical\n # `BarefootJS->new($c, ...)` callers keep working unchanged. A non-Mojo\n # host injects its own backend via `BarefootJS->new($c, { backend => $b })`\n # and never triggers this require \u2014 keeping the core load Mojo-free.\n backend => sub {\n require BarefootJS::Backend::Mojo;\n return BarefootJS::Backend::Mojo->new;\n },\n);\n\n# c \u2014 Mojolicious controller (kept for back-compat accessors)\n# config \u2014 plugin / instance config\n# backend \u2014 the template-engine seam (#engine-abstraction)\n# _scope_id \u2014 addressable scope id\n# _bf_parent / _bf_mount \u2014 slot identity when this scope is slot-attached\n# _props \u2014 props serialised into bf-p / the scope comment\nfor my $attr (qw(\n c config backend\n _scripts _script_seen _scope_id _is_child _bf_parent _bf_mount _props\n _child_renderers\n)) {\n no strict 'refs';\n *{\"BarefootJS::$attr\"} = sub {\n my $self = shift;\n if (@_) { $self->{$attr} = shift; return $self; }\n if (!exists $self->{$attr} && exists $ATTR_DEFAULT{$attr}) {\n my $d = $ATTR_DEFAULT{$attr};\n $self->{$attr} = ref($d) eq 'CODE' ? $d->($self) : $d;\n }\n return $self->{$attr};\n };\n}\n\nsub new ($class, $c, $config = {}) {\n # Build (or accept an injected) rendering backend. The default Mojo\n # backend wraps the controller and honours an optional `json_encoder`\n # override so a host can swap in a faster XS JSON implementation\n # without subclassing. A caller targeting another template engine\n # passes its own backend via `$config->{backend}`.\n my $backend = $config->{backend};\n unless ($backend) {\n require BarefootJS::Backend::Mojo;\n $backend = BarefootJS::Backend::Mojo->new(\n c => $c,\n ($config->{json_encoder}\n ? (json_encoder => $config->{json_encoder})\n : ()),\n );\n }\n my $self = bless {\n c => $c,\n config => $config,\n backend => $backend,\n }, $class;\n # Hold the controller weakly. Mojolicious stashes this bf instance under\n # `$c->stash->{'bf.instance'}`, so a strong bf -> controller back-reference\n # closes a per-request cycle ($c -> stash -> bf -> $c) that Perl's\n # refcount GC cannot reclaim, leaking one controller + bf + child-renderer\n # closures per request. The controller owns (outlives) the per-request bf,\n # so the weak ref stays valid for the whole render. Callers that need the\n # controller to outlive the bf instance independently must keep their own\n # strong reference (the normal Mojo request scope already does).\n weaken($self->{c}) if defined $c;\n return $self;\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 # encode_json returns a character string (not bytes) for safe embedding\n # in templates (the Mojo backend uses Mojo::JSON::to_json).\n my $json = $self->backend->encode_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 = '|' . $self->backend->encode_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# (`_child_renderers` accessor is generated by the minimal accessor base above.)\n\nsub register_child_renderer ($self, $name, $renderer) {\n $self->_child_renderers->{$name} = $renderer;\n}\n\nsub render_child ($self, $name, @args) {\n my $renderer = $self->_child_renderers->{$name};\n die \"No renderer registered for child component '$name'\" unless $renderer;\n # Accept both the Mojo list form \u2014 `bf->render_child($name, k => v, ...)`\n # \u2014 and the single-hashref form \u2014 `$bf.render_child($name, { k => v })`.\n # Template languages whose method calls can't splat a hash into positional\n # args (Text::Xslate Kolon, Template Toolkit) pass one hashref instead.\n my %props = (@args == 1 && ref $args[0] eq 'HASH') ? %{ $args[0] } : @args;\n # JSX children come in via the engine's children-capture mechanism\n # (Mojo's `begin %>...<% end`, which produces a CODE ref returning a\n # Mojo::ByteStream). Materialize it through the backend before handing\n # the props to the child renderer so the child template sees\n # `$children` as already-rendered HTML. Guard on `exists` so a\n # childless invocation (`bf->render_child('counter')`) doesn't gain a\n # spurious `children => undef` key \u2014 preserving the historical \"only\n # touch children when present\" behaviour.\n $props{children} = $self->backend->materialize($props{children})\n if exists $props{children};\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 $signal_inits = $opts{signal_init} // {};\n my $parent_scope = $self->_scope_id;\n # Weaken the parent capture so the child-renderer closures stored on\n # `$self->_child_renderers` don't keep `$self` alive (the direct\n # closure <-> parent cycle). The controller is reached through `$parent`\n # at call time rather than captured strongly here, so the closures hold\n # no strong reference to `$c` either \u2014 see the controller-cycle note in\n # `new`. `$parent` is always live whenever a closure runs (the closure is\n # stored on `$parent`, so `$parent` outlives every invocation).\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 # Child shares the parent's backend so nested renders go\n # through the same engine + controller (and inherit any\n # injected json_encoder). The controller is fetched via the weak\n # `$parent` at call time \u2014 never captured strongly \u2014 so the\n # closure adds no edge to the per-request reference cycle.\n my $child_bf = BarefootJS->new($parent->c, { backend => $parent->backend });\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 # Render the child template with $child_bf bound as the active\n # instance for the nested render. The backend owns the\n # engine-specific binding + restore (stash juggle for Mojo).\n my $html = $parent->backend->render_named(\n $template_name, $child_bf, { %$props, %extra },\n );\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 through the backend so the rendered\n # HTML embeds in the placeholder rather than the CODE ref's\n # stringification.\n $fallback_html = $self->backend->materialize($fallback_html);\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 $self->backend->encode_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# `Array.prototype.flat(depth?)` (#1448 Tier C) \u2014 flatten nested ARRAY\n# refs `$depth` levels deep. A `$depth` of -1 is the `Infinity` sentinel\n# (flatten fully); 0 returns a shallow copy. Non-ARRAY elements are kept\n# as-is (JS only flattens nested arrays). Non-ARRAY receiver \u2192 [].\nsub flat ($self, $recv, $depth = 1) {\n return [] unless ref($recv) eq 'ARRAY';\n my @out;\n for my $el (@$recv) {\n if ($depth != 0 && ref($el) eq 'ARRAY') {\n my $next = $depth > 0 ? $depth - 1 : $depth;\n push @out, @{ $self->flat($el, $next) };\n }\n else {\n push @out, $el;\n }\n }\n return \\@out;\n}\n\n# `Array.prototype.flatMap(fn)` value-returning field projection\n# (#1448 Tier C) \u2014 map each element through a self / field projection,\n# then flatten one level. `field` reads a HASH-ref key (the raw JS prop\n# name, as `bf->reduce` does); a projected non-ARRAY value is kept as-is\n# (flatMap = map + flat(1)). Non-ARRAY receiver \u2192 [].\nsub flat_map ($self, $recv, $key_kind, $key) {\n return [] unless ref($recv) eq 'ARRAY';\n my @projected;\n for my $el (@$recv) {\n if ($key_kind eq 'field') {\n # JS `i => i.field` on a non-object yields `undefined`, not the\n # element itself \u2014 push `undef` so a scalar element doesn't leak\n # into the output (matches Go's `getFieldValue` returning nil).\n push @projected, ref($el) eq 'HASH' ? $el->{$key} : undef;\n }\n else {\n push @projected, $el;\n }\n }\n return $self->flat(\\@projected, 1);\n}\n\n# `Array.prototype.flatMap(i => [i.a, i.b])` \u2014 array-literal tuple\n# projection (#1448 Tier C). Each `@specs` entry is a [kind, key] arrayref\n# (['self', ''] or ['field', 'a']). For each element, every leaf's value\n# is appended in order. flat(1) removes only the literal wrapper, so an\n# array-valued leaf is appended verbatim (no spread) \u2014 i.e. just append\n# each leaf. A non-HASH element under a `field` leaf yields undef (JS\n# `i.field` on a non-object). Non-ARRAY receiver \u2192 [].\nsub flat_map_tuple ($self, $recv, @specs) {\n return [] unless ref($recv) eq 'ARRAY';\n my @out;\n for my $el (@$recv) {\n for my $spec (@specs) {\n my ($kind, $key) = @$spec;\n if ($kind eq 'field') {\n push @out, ref($el) eq 'HASH' ? $el->{$key} : undef;\n }\n else {\n push @out, $el;\n }\n }\n }\n return \\@out;\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# Fold an array into a scalar via the arithmetic-fold catalogue\n# (#1448 Tier C). Mirrors Go's `bf_reduce` and JS `reduce(fn, init)` /\n# `reduceRight(fn, init)` for the shapes `(acc, x) => acc <op> x` /\n# `(acc, x) => acc <op> x.field`:\n#\n# bf->reduce($recv, {\n# op => '+' | '*',\n# key_kind => 'self' | 'field',\n# key => '<field>', # when key_kind eq 'field'\n# type => 'numeric' | 'string',\n# init => <seed>, # number, or string for concat\n# direction => 'left' | 'right', # 'right' = reduceRight (default 'left')\n# })\n#\n# Numeric folds accumulate with `+` / `*` (non-numeric keys coalesce to\n# 0); string folds concatenate via `bf->string` (undef \u2192 ''). The init\n# seeds the accumulator, so an empty array returns it unchanged \u2014 exactly\n# like JS. `direction => 'right'` folds right-to-left (reduceRight); only\n# observable for string concat, since numeric sum / product commute.\n# Float stringification can diverge from Go's for inexact binary\n# fractions (e.g. 0.1 + 0.2); integer sums \u2014 the common case \u2014 agree.\nsub reduce ($self, $recv, $opts = {}) {\n my $op = $opts->{op} // '+';\n my $key_kind = $opts->{key_kind} // 'self';\n my $key = $opts->{key} // '';\n my $type = $opts->{type} // 'numeric';\n my $direction = $opts->{direction} // 'left';\n\n my @items = ref($recv) eq 'ARRAY' ? @$recv : ();\n # reduceRight folds right-to-left; reversing the snapshot keeps the\n # single forward loop below. Only observable for string concat \u2014\n # numeric sum / product commute. Qualify as CORE::reverse \u2014 this\n # package defines `sub reverse` (the `.reverse()` helper), so a bare\n # `reverse` is ambiguous under `use warnings`.\n @items = CORE::reverse(@items) if $direction eq 'right';\n my $project = sub ($item) {\n $key_kind eq 'field' && ref($item) eq 'HASH' ? $item->{$key} : $item;\n };\n\n if ($type eq 'string') {\n my $acc = $opts->{init} // '';\n $acc .= $self->string($project->($_)) for @items;\n return $acc;\n }\n\n my $acc = $opts->{init} // 0;\n for my $item (@items) {\n my $n = $project->($item);\n # Guard `defined` before `looks_like_number` so a missing field\n # (undef) folds as 0 without an \"uninitialized value\" warning\n # under `use warnings` \u2014 matching the `$av // ''` style `sort` uses.\n $n = 0 unless defined $n && looks_like_number($n);\n $op eq '*' ? ($acc *= $n) : ($acc += $n);\n }\n return $acc;\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 # Mark the result raw so the calling template's `<%==` raw-emit\n # doesn't re-escape the already-escaped values (the Mojo backend\n # returns a Mojo::ByteStream).\n return $self->backend->mark_raw(join(' ', @parts));\n}\n\n1;\n__END__\n\n=encoding utf8\n\n=head1 NAME\n\nBarefootJS - Engine- and framework-agnostic server runtime for BarefootJS marked templates\n\n=head1 SYNOPSIS\n\n use BarefootJS;\n\n # A host injects a rendering backend (see BarefootJS::Backend::Xslate or\n # Mojolicious::Plugin::BarefootJS for shipping backends).\n my $bf = BarefootJS->new($context, { backend => $backend });\n\n # The compiled marked template calls the runtime as a `bf` object:\n # <: $bf.scope_attr() :> <: $bf.json($data) :> <: $bf.spread_attrs($h) :>\n\n=head1 DESCRIPTION\n\nBarefootJS compiles JSX/TSX into a marked template plus client JS. This module\nis the server-side runtime the marked templates call into at render time. It is\ndeliberately template-engine- and web-framework-agnostic: every operation that\ndepends on I<how> a template is rendered \u2014 JSON marshalling, raw-string marking,\nJSX-children materialisation, and named-template rendering \u2014 is delegated to a\npluggable C<backend>.\n\nThat design lets the one runtime drive any backend. Shipping backends:\n\n=over 4\n\n=item * L<BarefootJS::Backend::Xslate> \u2014 Text::Xslate (Kolon); runs under any PSGI/Plack app.\n\n=item * L<BarefootJS::Backend::Mojo> \u2014 Mojolicious (via L<Mojolicious::Plugin::BarefootJS>).\n\n=back\n\nThe core itself pulls in only core Perl modules (C<POSIX>, C<Scalar::Util>);\nno template engine or web framework is loaded unless a backend that needs one\nis used.\n\n=head1 SEE ALSO\n\nL<BarefootJS::Backend::Xslate>, L<Mojolicious::Plugin::BarefootJS>,\nL<https://github.com/piconic-ai/barefootjs>\n\n=head1 AUTHOR\n\nkobaken E<lt>kentafly88@gmail.comE<gt>\n\n=head1 LICENSE\n\nCopyright (c) 2025-present BarefootJS Contributors.\n\nThis library is free software; you can redistribute it and/or modify it under\nthe MIT License. See the F<LICENSE> file in the distribution for the full text.\n\n=cut\n";
|
|
23255
|
+
barefootBackendMojoPmSource = "package BarefootJS::Backend::Mojo;\nour $VERSION = \"0.01\";\nuse Mojo::Base -base, -signatures;\n\nuse Mojo::ByteStream qw(b);\nuse Mojo::JSON qw(to_json);\nuse Scalar::Util qw(weaken);\n\n# ---------------------------------------------------------------------------\n# Reference rendering backend (Mojolicious / Mojo::Template).\n# ---------------------------------------------------------------------------\n#\n# BarefootJS.pm holds all the template-engine-agnostic logic (the JS-compat\n# value helpers, array/string methods, hydration markers). Everything that is\n# specific to *how a template is rendered* \u2014 JSON marshalling, raw-string\n# marking, JSX-children materialisation, and named-template rendering \u2014 lives\n# behind this backend object so the same runtime can drive a different Perl\n# template engine (Text::Xslate, Template Toolkit, \u2026) without rewriting the\n# helper surface.\n#\n# A backend MUST implement:\n# - encode_json($data) -> string\n# - mark_raw($str) -> value the engine emits without escaping\n# - materialize($value) -> string (resolve a captured-children ref)\n# - render_named($name, $bf, \\%vars) -> string\n#\n# This Mojo implementation is the reference. To target another engine, write a\n# sibling backend (BarefootJS::Backend::Xslate, \u2026) implementing the same four\n# methods and pass it via `BarefootJS->new($c, { backend => $b })`.\n\n# The Mojolicious controller. Optional: the value-marshalling helpers\n# (`encode_json` / `mark_raw` / `materialize`) work without it; only\n# `render_named` reaches into the controller's renderer + stash.\nhas 'c';\n\n# Pluggable JSON encoder (#engine-abstraction). Defaults to\n# `Mojo::JSON::to_json`, which returns a *character* string (not bytes)\n# suitable for embedding in HTML output via `<%==` / Mojo::ByteStream.\n#\n# Override with any `sub ($data) { ... }` to swap in a faster XS encoder \u2014\n# e.g. `json_encoder => sub { Cpanel::JSON::XS->new->canonical->encode($_[0]) }`.\n# The pure-Perl JSON::PP fallback Mojo::JSON uses can be a hot spot for large\n# props payloads; the seam lets a host pick its own implementation without\n# touching the runtime.\nhas 'json_encoder' => sub { \\&to_json };\n\n# Hold the controller weakly for the same reason BarefootJS does: the\n# controller owns the bf instance (which owns this backend) via its stash,\n# so a strong back-reference would close a per-request cycle the refcount GC\n# can't reclaim. `render_named` only touches `$self->c` mid-render, while the\n# controller is still alive on the request stack.\nsub new ($class, %args) {\n my $self = $class->SUPER::new(%args);\n weaken($self->{c}) if $self->{c};\n return $self;\n}\n\nsub encode_json ($self, $data) {\n return $self->json_encoder->($data);\n}\n\n# Mark a string as already-safe so the template engine emits it verbatim\n# (no re-escaping). In Mojo this is a Mojo::ByteStream, which the calling\n# template's `<%==` raw-emit passes through unescaped.\nsub mark_raw ($self, $str) {\n return b($str);\n}\n\n# JSX children / async fallbacks arrive via Mojo's `begin %>...<% end`\n# capture, which produces a CODE ref returning a Mojo::ByteStream. Resolve\n# it to a string before embedding. Plain (already-rendered) strings pass\n# through unchanged.\nsub materialize ($self, $value) {\n return ref($value) eq 'CODE' ? $value->() : $value;\n}\n\n# Render a named template with `$child_bf` bound as the active runtime\n# instance for that render. The Mojo `bf` helper resolves the current\n# instance off `$c->stash->{'bf.instance'}`; swap it for the duration of\n# the nested render and restore it afterwards so sibling renders are\n# unaffected.\nsub render_named ($self, $template_name, $child_bf, $vars) {\n my $c = $self->c;\n my $prev = $c->stash->{'bf.instance'};\n $c->stash->{'bf.instance'} = $child_bf;\n my $html = $c->render_to_string(template => $template_name, %$vars);\n $c->stash->{'bf.instance'} = $prev;\n return $html;\n}\n\n1;\n";
|
|
23256
|
+
barefootPluginPmSource = "package Mojolicious::Plugin::BarefootJS;\nour $VERSION = \"0.01\";\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__END__\n\n=encoding utf8\n\n=head1 NAME\n\nMojolicious::Plugin::BarefootJS - Mojolicious integration for BarefootJS\n\n=head1 SYNOPSIS\n\n # Mojolicious application\n $self->plugin('BarefootJS');\n\n # In a controller / template, the `bf` helper exposes a per-request\n # BarefootJS runtime backed by BarefootJS::Backend::Mojo.\n\n=head1 DESCRIPTION\n\nWires the L<BarefootJS> server runtime into L<Mojolicious>. It registers a\nC<bf> controller helper that lazily instantiates one BarefootJS object per\nrequest (rendering via L<BarefootJS::Backend::Mojo>), and supports rendering\ncompiled marked templates as Mojolicious templates.\n\nFor non-Mojolicious / PSGI hosts, see L<BarefootJS::Backend::Xslate>, which\ndrives the same runtime with Text::Xslate and no web framework.\n\n=head1 METHODS\n\nL<Mojolicious::Plugin::BarefootJS> inherits all methods from\nL<Mojolicious::Plugin> and implements the following new one.\n\n=head2 register\n\n $plugin->register(Mojolicious->new, \\%conf);\n\nRegisters the plugin (the C<bf> helper and supporting hooks) in a Mojolicious\napplication.\n\n=head1 SEE ALSO\n\nL<BarefootJS>, L<BarefootJS::Backend::Mojo>, L<BarefootJS::Backend::Xslate>,\nL<Mojolicious>, L<https://github.com/piconic-ai/barefootjs>\n\n=head1 AUTHOR\n\nkobaken E<lt>kentafly88@gmail.comE<gt>\n\n=head1 LICENSE\n\nCopyright (c) 2025-present BarefootJS Contributors.\n\nThis library is free software; you can redistribute it and/or modify it under\nthe MIT License. See the F<LICENSE> file in the distribution for the full text.\n\n=cut\n";
|
|
23124
23257
|
barefootDevReloadPmSource = `package Mojolicious::Plugin::BarefootJS::DevReload;
|
|
23258
|
+
our $VERSION = "0.01";
|
|
23125
23259
|
use Mojo::Base 'Mojolicious::Plugin', -signatures;
|
|
23126
23260
|
|
|
23127
23261
|
=head1 NAME
|
|
@@ -23258,32 +23392,632 @@ sub _snippet ($endpoint) {
|
|
|
23258
23392
|
return qq{<script>(function(){if(window.__bfDevReload)return;window.__bfDevReload=1;try{var s=sessionStorage.getItem($sk);if(s){sessionStorage.removeItem($sk);var y=parseInt(s,10);if(!isNaN(y)){var restore=function(){window.scrollTo(0,y)};if(document.readyState==='loading'){addEventListener('DOMContentLoaded',restore,{once:true})}else{restore()}}}}catch(e){}var es=new EventSource($ep);es.addEventListener('reload',function(){try{sessionStorage.setItem($sk,String(window.scrollY))}catch(e){}location.reload()});es.addEventListener('error',function(){})})();</script>};
|
|
23259
23393
|
}
|
|
23260
23394
|
|
|
23261
|
-
sub _js_str ($s) {
|
|
23262
|
-
# Minimal JS string escape for the handful of characters that can appear
|
|
23263
|
-
# in a URL path or storage key. Good enough for package-internal + trusted
|
|
23264
|
-
# operator-supplied strings; never interpolate untrusted input here.
|
|
23265
|
-
my $t = $s;
|
|
23266
|
-
$t =~ s/\\\\/\\\\\\\\/g;
|
|
23267
|
-
$t =~ s/"/\\\\"/g;
|
|
23268
|
-
$t =~ s/\\n/\\\\n/g;
|
|
23269
|
-
$t =~ s/\\r/\\\\r/g;
|
|
23270
|
-
return qq{"$t"};
|
|
23395
|
+
sub _js_str ($s) {
|
|
23396
|
+
# Minimal JS string escape for the handful of characters that can appear
|
|
23397
|
+
# in a URL path or storage key. Good enough for package-internal + trusted
|
|
23398
|
+
# operator-supplied strings; never interpolate untrusted input here.
|
|
23399
|
+
my $t = $s;
|
|
23400
|
+
$t =~ s/\\\\/\\\\\\\\/g;
|
|
23401
|
+
$t =~ s/"/\\\\"/g;
|
|
23402
|
+
$t =~ s/\\n/\\\\n/g;
|
|
23403
|
+
$t =~ s/\\r/\\\\r/g;
|
|
23404
|
+
return qq{"$t"};
|
|
23405
|
+
}
|
|
23406
|
+
|
|
23407
|
+
1;
|
|
23408
|
+
`;
|
|
23409
|
+
}
|
|
23410
|
+
});
|
|
23411
|
+
|
|
23412
|
+
// src/lib/adapters/go-shared.ts
|
|
23413
|
+
import { execSync } from "node:child_process";
|
|
23414
|
+
function goCommonFiles() {
|
|
23415
|
+
return {
|
|
23416
|
+
"renderer.go": GO_RENDERER_GO,
|
|
23417
|
+
"env.go": GO_ENV_GO,
|
|
23418
|
+
"bf-runtime/bf.go": bfGoSource,
|
|
23419
|
+
"bf-runtime/streaming.go": streamingGoSource,
|
|
23420
|
+
"bf-runtime/bfdev/bfdev.go": bfdevGoSource,
|
|
23421
|
+
"bf-runtime/go.mod": GO_BF_RUNTIME_GO_MOD,
|
|
23422
|
+
"barefoot.config.ts": GO_BAREFOOT_CONFIG_TS,
|
|
23423
|
+
"tsconfig.json": GO_TSCONFIG,
|
|
23424
|
+
"uno.config.ts": unoConfigTs([
|
|
23425
|
+
"components/**/*.tsx",
|
|
23426
|
+
"dist/components/**/*.tsx"
|
|
23427
|
+
]),
|
|
23428
|
+
"components/Counter.tsx": SHARED_COUNTER_TSX,
|
|
23429
|
+
"components/Counter.test.tsx": SHARED_COUNTER_TEST_TSX,
|
|
23430
|
+
"public/styles.css": STYLES_CSS,
|
|
23431
|
+
"public/tokens.css": TOKENS_CSS,
|
|
23432
|
+
"public/uno.css": UNO_CSS_PLACEHOLDER,
|
|
23433
|
+
".gitignore": GO_GITIGNORE
|
|
23434
|
+
};
|
|
23435
|
+
}
|
|
23436
|
+
function goScripts() {
|
|
23437
|
+
return {
|
|
23438
|
+
dev: 'go mod tidy && bf build && unocss && concurrently -k -n build,uno,server -c blue,magenta,green "bf build --watch" "unocss --watch" "go run ."',
|
|
23439
|
+
build: "go mod tidy && bf build && unocss",
|
|
23440
|
+
start: "go run ."
|
|
23441
|
+
};
|
|
23442
|
+
}
|
|
23443
|
+
function goDependencies() {
|
|
23444
|
+
return {
|
|
23445
|
+
"@barefootjs/client": "latest",
|
|
23446
|
+
"@barefootjs/go-template": "latest",
|
|
23447
|
+
"@barefootjs/jsx": "latest",
|
|
23448
|
+
"@barefootjs/shared": "latest"
|
|
23449
|
+
};
|
|
23450
|
+
}
|
|
23451
|
+
function goDevDependencies() {
|
|
23452
|
+
return {
|
|
23453
|
+
...UNOCSS_DEV_DEPENDENCIES,
|
|
23454
|
+
"@barefootjs/cli": "latest",
|
|
23455
|
+
"@barefootjs/test": "latest",
|
|
23456
|
+
concurrently: "^9.0.0",
|
|
23457
|
+
typescript: "^5.6.0"
|
|
23458
|
+
};
|
|
23459
|
+
}
|
|
23460
|
+
function goPrereqs() {
|
|
23461
|
+
try {
|
|
23462
|
+
execSync("go version", { stdio: "ignore" });
|
|
23463
|
+
return [];
|
|
23464
|
+
} catch {
|
|
23465
|
+
return [
|
|
23466
|
+
"Go toolchain not found on PATH. Install Go 1.22+ (https://go.dev/dl/) before starting the dev server."
|
|
23467
|
+
];
|
|
23468
|
+
}
|
|
23469
|
+
}
|
|
23470
|
+
function makeGoAdapter(opts) {
|
|
23471
|
+
return {
|
|
23472
|
+
label: opts.label,
|
|
23473
|
+
port: 3001,
|
|
23474
|
+
files: {
|
|
23475
|
+
"main.go": opts.mainGo,
|
|
23476
|
+
"bf_render.go": opts.bfRenderGo,
|
|
23477
|
+
"go.mod": opts.goMod,
|
|
23478
|
+
...goCommonFiles()
|
|
23479
|
+
},
|
|
23480
|
+
scripts: goScripts(),
|
|
23481
|
+
dependencies: goDependencies(),
|
|
23482
|
+
devDependencies: goDevDependencies(),
|
|
23483
|
+
prereqWarnings: () => goPrereqs()
|
|
23484
|
+
};
|
|
23485
|
+
}
|
|
23486
|
+
var GO_BF_RUNTIME_GO_MOD, GO_BAREFOOT_CONFIG_TS, GO_RENDERER_GO, GO_ENV_GO, GO_TSCONFIG, GO_GITIGNORE, GO_RENDER_SHARED_FNS;
|
|
23487
|
+
var init_go_shared = __esm({
|
|
23488
|
+
"src/lib/adapters/go-shared.ts"() {
|
|
23489
|
+
"use strict";
|
|
23490
|
+
init_shared2();
|
|
23491
|
+
init_runtimes_generated();
|
|
23492
|
+
GO_BF_RUNTIME_GO_MOD = `module github.com/barefootjs/runtime/bf
|
|
23493
|
+
|
|
23494
|
+
go 1.22
|
|
23495
|
+
`;
|
|
23496
|
+
GO_BAREFOOT_CONFIG_TS = `import { createConfig } from '@barefootjs/go-template/build'
|
|
23497
|
+
|
|
23498
|
+
export default createConfig({
|
|
23499
|
+
paths: {
|
|
23500
|
+
components: 'components/ui',
|
|
23501
|
+
tokens: 'tokens',
|
|
23502
|
+
meta: 'meta',
|
|
23503
|
+
},
|
|
23504
|
+
components: ['components'],
|
|
23505
|
+
outDir: 'dist',
|
|
23506
|
+
adapterOptions: {
|
|
23507
|
+
packageName: 'main',
|
|
23508
|
+
// Compiled client bundles are served from /client/ (see main.go).
|
|
23509
|
+
clientJsBasePath: '/client/',
|
|
23510
|
+
barefootJsPath: '/client/barefoot.js',
|
|
23511
|
+
},
|
|
23512
|
+
// Generated Go struct types for every component, written next to main.go.
|
|
23513
|
+
// Overwritten on every \`bf build\` run.
|
|
23514
|
+
typesOutputFile: 'components.go',
|
|
23515
|
+
})
|
|
23516
|
+
`;
|
|
23517
|
+
GO_RENDERER_GO = `package main
|
|
23518
|
+
|
|
23519
|
+
import (
|
|
23520
|
+
"fmt"
|
|
23521
|
+
|
|
23522
|
+
bf "github.com/barefootjs/runtime/bf"
|
|
23523
|
+
)
|
|
23524
|
+
|
|
23525
|
+
// defaultLayout is the wrapping HTML the BarefootJS render pipeline
|
|
23526
|
+
// hands every component. Edit freely \u2014 this file is yours.
|
|
23527
|
+
func defaultLayout(ctx *bf.RenderContext) string {
|
|
23528
|
+
return fmt.Sprintf(\`<!DOCTYPE html>
|
|
23529
|
+
<html lang="en">
|
|
23530
|
+
<head>
|
|
23531
|
+
<meta charset="utf-8" />
|
|
23532
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
23533
|
+
<title>%s</title>
|
|
23534
|
+
<!-- Link all three sheets so the browser fetches them in parallel \u2014
|
|
23535
|
+
chaining via styles.css @import would defer tokens/uno to a
|
|
23536
|
+
second round-trip and flash unstyled DOM. tokens first so its
|
|
23537
|
+
CSS variables are defined before any rule references them. -->
|
|
23538
|
+
<link rel="stylesheet" href="/static/tokens.css" />
|
|
23539
|
+
<link rel="stylesheet" href="/static/styles.css" />
|
|
23540
|
+
<link rel="stylesheet" href="/static/uno.css" />
|
|
23541
|
+
</head>
|
|
23542
|
+
<body>
|
|
23543
|
+
<main>%s</main>
|
|
23544
|
+
%s
|
|
23545
|
+
%s
|
|
23546
|
+
</body>
|
|
23547
|
+
</html>\`,
|
|
23548
|
+
ctx.Title,
|
|
23549
|
+
ctx.ComponentHTML,
|
|
23550
|
+
ctx.Portals,
|
|
23551
|
+
ctx.Scripts,
|
|
23552
|
+
)
|
|
23553
|
+
}
|
|
23554
|
+
`;
|
|
23555
|
+
GO_ENV_GO = `package main
|
|
23556
|
+
|
|
23557
|
+
import "os"
|
|
23558
|
+
|
|
23559
|
+
// IsDev reports whether the server is running in development mode.
|
|
23560
|
+
// Driven by APP_ENV \u2014 \`production\` flips into prod mode; anything
|
|
23561
|
+
// else (including unset) is dev.
|
|
23562
|
+
func IsDev() bool {
|
|
23563
|
+
return os.Getenv("APP_ENV") != "production"
|
|
23564
|
+
}
|
|
23565
|
+
`;
|
|
23566
|
+
GO_TSCONFIG = `{
|
|
23567
|
+
"compilerOptions": {
|
|
23568
|
+
"target": "ESNext",
|
|
23569
|
+
"module": "ESNext",
|
|
23570
|
+
"moduleResolution": "bundler",
|
|
23571
|
+
"jsx": "react-jsx",
|
|
23572
|
+
"jsxImportSource": "@barefootjs/jsx",
|
|
23573
|
+
"types": ["node"{{__PM_TYPES_ENTRY__}}],
|
|
23574
|
+
"strict": true,
|
|
23575
|
+
"skipLibCheck": true,
|
|
23576
|
+
"esModuleInterop": true,
|
|
23577
|
+
"resolveJsonModule": true,
|
|
23578
|
+
"noEmit": true,
|
|
23579
|
+
"baseUrl": ".",
|
|
23580
|
+
"paths": {
|
|
23581
|
+
"@/components/*": ["./components/*"]
|
|
23582
|
+
}
|
|
23583
|
+
},
|
|
23584
|
+
"include": ["**/*.ts", "**/*.tsx"],
|
|
23585
|
+
"exclude": ["node_modules", "dist", "bf-runtime"]
|
|
23586
|
+
}
|
|
23587
|
+
`;
|
|
23588
|
+
GO_GITIGNORE = buildGitignore([
|
|
23589
|
+
{
|
|
23590
|
+
heading: "bf build outputs (regenerated by `bf build` / `bf build --watch`)",
|
|
23591
|
+
entries: ["dist/"]
|
|
23592
|
+
},
|
|
23593
|
+
{
|
|
23594
|
+
heading: "Compiled Go binary (`go build` writes alongside main.go)",
|
|
23595
|
+
entries: ["/main", "*.exe", "*.test", "*.out"]
|
|
23596
|
+
},
|
|
23597
|
+
{
|
|
23598
|
+
heading: "Local environment",
|
|
23599
|
+
entries: [".env", ".env.local", ".env.*.local"]
|
|
23600
|
+
}
|
|
23601
|
+
]);
|
|
23602
|
+
GO_RENDER_SHARED_FNS = `// loadTemplates walks dist/templates/ recursively and parses every
|
|
23603
|
+
// .tmpl file. ParseGlob("dist/templates/*.tmpl") only catches the
|
|
23604
|
+
// top-level directory, missing per-component subdirectories like
|
|
23605
|
+
// dist/templates/ui/button/index.tmpl that a parent invokes via
|
|
23606
|
+
// {{template "Button" ...}}.
|
|
23607
|
+
//
|
|
23608
|
+
// Also registers a no-op "Tag" template so html/template's escape pass
|
|
23609
|
+
// (which validates references in unreachable branches too) doesn't
|
|
23610
|
+
// crash the first call to any template that transitively includes Slot
|
|
23611
|
+
// \u2014 the go-template adapter emits a {{template "Tag" ...}} reference
|
|
23612
|
+
// inside Slot that's only reachable when AsChild=true.
|
|
23613
|
+
func loadTemplates() (*template.Template, error) {
|
|
23614
|
+
root := template.New("").Funcs(bf.FuncMap())
|
|
23615
|
+
if _, err := root.New("Tag").Parse(""); err != nil {
|
|
23616
|
+
return nil, err
|
|
23617
|
+
}
|
|
23618
|
+
err := filepath.WalkDir("dist/templates", func(path string, d fs.DirEntry, err error) error {
|
|
23619
|
+
if err != nil {
|
|
23620
|
+
return err
|
|
23621
|
+
}
|
|
23622
|
+
if d.IsDir() || filepath.Ext(path) != ".tmpl" {
|
|
23623
|
+
return nil
|
|
23624
|
+
}
|
|
23625
|
+
_, parseErr := root.ParseFiles(path)
|
|
23626
|
+
return parseErr
|
|
23627
|
+
})
|
|
23628
|
+
return root, err
|
|
23629
|
+
}
|
|
23630
|
+
|
|
23631
|
+
// layoutWithDev wraps the user's defaultLayout (renderer.go) with the
|
|
23632
|
+
// dev auto-reload snippet, injected just before </body>. In production
|
|
23633
|
+
// (APP_ENV=production) the snippet is empty and the layout passes
|
|
23634
|
+
// through unchanged.
|
|
23635
|
+
func layoutWithDev(ctx *bf.RenderContext) string {
|
|
23636
|
+
html := defaultLayout(ctx)
|
|
23637
|
+
snippet := bfdev.Snippet(bfdev.Config{Disabled: !IsDev()})
|
|
23638
|
+
if snippet == "" {
|
|
23639
|
+
return html
|
|
23640
|
+
}
|
|
23641
|
+
return strings.Replace(html, "</body>", string(snippet)+"\\n</body>", 1)
|
|
23642
|
+
}
|
|
23643
|
+
|
|
23644
|
+
// mustNewRenderer loads templates once at startup and pairs them with
|
|
23645
|
+
// layoutWithDev. On parse failure we surface the cause to stderr and
|
|
23646
|
+
// exit non-zero, so the cause shows up before the Go panic stack trace.
|
|
23647
|
+
func mustNewRenderer() *bf.Renderer {
|
|
23648
|
+
tmpl, err := loadTemplates()
|
|
23649
|
+
if err != nil {
|
|
23650
|
+
fmt.Fprintf(os.Stderr, "barefoot: failed to load templates from dist/templates: %v\\n", err)
|
|
23651
|
+
fmt.Fprintln(os.Stderr, "barefoot: did you run \`bf build\` first?")
|
|
23652
|
+
os.Exit(1)
|
|
23653
|
+
}
|
|
23654
|
+
return bf.NewRenderer(tmpl, layoutWithDev)
|
|
23655
|
+
}
|
|
23656
|
+
|
|
23657
|
+
// renderToString renders a component to a full HTML page. In dev mode
|
|
23658
|
+
// templates are re-parsed on every call so a \`bf build --watch\` update
|
|
23659
|
+
// surfaces as soon as the browser refreshes \u2014 no Go restart needed. In
|
|
23660
|
+
// production the parse happens once (renderer is reused).
|
|
23661
|
+
func renderToString(renderer *bf.Renderer, name string, opts bf.RenderOptions) (string, error) {
|
|
23662
|
+
r := renderer
|
|
23663
|
+
if IsDev() {
|
|
23664
|
+
tmpl, err := loadTemplates()
|
|
23665
|
+
if err != nil {
|
|
23666
|
+
return "", err
|
|
23667
|
+
}
|
|
23668
|
+
r = bf.NewRenderer(tmpl, layoutWithDev)
|
|
23669
|
+
}
|
|
23670
|
+
opts.ComponentName = name
|
|
23671
|
+
return r.Render(opts), nil
|
|
23672
|
+
}
|
|
23673
|
+
`;
|
|
23674
|
+
}
|
|
23675
|
+
});
|
|
23676
|
+
|
|
23677
|
+
// src/lib/adapters/chi.ts
|
|
23678
|
+
var CHI_MAIN_GO, CHI_BF_RENDER_GO, CHI_GO_MOD, CHI_ADAPTER;
|
|
23679
|
+
var init_chi = __esm({
|
|
23680
|
+
"src/lib/adapters/chi.ts"() {
|
|
23681
|
+
"use strict";
|
|
23682
|
+
init_go_shared();
|
|
23683
|
+
CHI_MAIN_GO = `package main
|
|
23684
|
+
|
|
23685
|
+
import (
|
|
23686
|
+
"fmt"
|
|
23687
|
+
"net/http"
|
|
23688
|
+
"os"
|
|
23689
|
+
|
|
23690
|
+
bf "github.com/barefootjs/runtime/bf"
|
|
23691
|
+
"github.com/go-chi/chi/v5"
|
|
23692
|
+
"github.com/go-chi/chi/v5/middleware"
|
|
23693
|
+
)
|
|
23694
|
+
|
|
23695
|
+
func main() {
|
|
23696
|
+
// Load + cache templates once (re-parsed per request in dev so a
|
|
23697
|
+
// \`bf build --watch\` rebuild shows up on refresh). See bf_render.go.
|
|
23698
|
+
renderer := mustNewRenderer()
|
|
23699
|
+
|
|
23700
|
+
r := chi.NewRouter()
|
|
23701
|
+
r.Use(middleware.Logger)
|
|
23702
|
+
r.Use(middleware.Recoverer)
|
|
23703
|
+
|
|
23704
|
+
// Dev auto-reload SSE \u2014 mounts /_bf/reload only when APP_ENV is not
|
|
23705
|
+
// "production"; a no-op otherwise. Pairs with the snippet
|
|
23706
|
+
// bf_render.go injects into <body>.
|
|
23707
|
+
MountDevReload(r)
|
|
23708
|
+
|
|
23709
|
+
// The go-template adapter emits client bundles under dist/client/
|
|
23710
|
+
// and templates reference them at /client/ (per barefoot.config.ts).
|
|
23711
|
+
// Public assets (CSS) live under public/ and are served at /static/.
|
|
23712
|
+
r.Handle("/client/*", http.StripPrefix("/client/", http.FileServer(http.Dir("dist/client"))))
|
|
23713
|
+
r.Handle("/static/*", http.StripPrefix("/static/", http.FileServer(http.Dir("public"))))
|
|
23714
|
+
|
|
23715
|
+
r.Get("/", func(w http.ResponseWriter, req *http.Request) {
|
|
23716
|
+
// \`NewCounterProps\` / \`CounterInput\` are generated into
|
|
23717
|
+
// components.go by \`bf build\` \u2014 they wire up scope IDs,
|
|
23718
|
+
// child-slot props, and signal initial values. Use them instead
|
|
23719
|
+
// of constructing props by hand.
|
|
23720
|
+
props := NewCounterProps(CounterInput{Initial: 0})
|
|
23721
|
+
render(w, renderer, http.StatusOK, "Counter", bf.RenderOptions{
|
|
23722
|
+
Title: "BarefootJS app",
|
|
23723
|
+
Props: &props,
|
|
23724
|
+
})
|
|
23725
|
+
})
|
|
23726
|
+
|
|
23727
|
+
port := os.Getenv("PORT")
|
|
23728
|
+
if port == "" {
|
|
23729
|
+
port = "3001"
|
|
23730
|
+
}
|
|
23731
|
+
fmt.Printf(" \u279C http://localhost:%s\\n", port)
|
|
23732
|
+
if err := http.ListenAndServe(":"+port, r); err != nil {
|
|
23733
|
+
fmt.Fprintf(os.Stderr, "barefoot: server error: %v\\n", err)
|
|
23734
|
+
os.Exit(1)
|
|
23735
|
+
}
|
|
23736
|
+
}
|
|
23737
|
+
`;
|
|
23738
|
+
CHI_BF_RENDER_GO = `package main
|
|
23739
|
+
|
|
23740
|
+
// Code generated by BarefootJS for the Chi adapter. DO NOT EDIT.
|
|
23741
|
+
// To refresh, delete this file and re-run \`bf init\` from a fresh
|
|
23742
|
+
// directory \u2014 \`bf init\` skips existing files, so it won't clobber your
|
|
23743
|
+
// edits in place.
|
|
23744
|
+
|
|
23745
|
+
import (
|
|
23746
|
+
"fmt"
|
|
23747
|
+
"html/template"
|
|
23748
|
+
"io/fs"
|
|
23749
|
+
"net/http"
|
|
23750
|
+
"os"
|
|
23751
|
+
"path/filepath"
|
|
23752
|
+
"strings"
|
|
23753
|
+
|
|
23754
|
+
bf "github.com/barefootjs/runtime/bf"
|
|
23755
|
+
"github.com/barefootjs/runtime/bf/bfdev"
|
|
23756
|
+
"github.com/go-chi/chi/v5"
|
|
23757
|
+
)
|
|
23758
|
+
|
|
23759
|
+
${GO_RENDER_SHARED_FNS}
|
|
23760
|
+
// render writes a fully laid-out HTML page for the named component.
|
|
23761
|
+
func render(w http.ResponseWriter, renderer *bf.Renderer, status int, name string, opts bf.RenderOptions) {
|
|
23762
|
+
html, err := renderToString(renderer, name, opts)
|
|
23763
|
+
if err != nil {
|
|
23764
|
+
http.Error(w, fmt.Sprintf("template error: %v", err), http.StatusInternalServerError)
|
|
23765
|
+
return
|
|
23766
|
+
}
|
|
23767
|
+
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
|
23768
|
+
w.WriteHeader(status)
|
|
23769
|
+
_, _ = w.Write([]byte(html))
|
|
23770
|
+
}
|
|
23771
|
+
|
|
23772
|
+
// MountDevReload registers the SSE auto-reload endpoint in dev mode.
|
|
23773
|
+
// In production it is a no-op.
|
|
23774
|
+
func MountDevReload(r chi.Router) {
|
|
23775
|
+
if !IsDev() {
|
|
23776
|
+
return
|
|
23777
|
+
}
|
|
23778
|
+
r.Handle("/_bf/reload", bfdev.NewReloadHandler(bfdev.Config{DistDir: "./dist"}))
|
|
23779
|
+
}
|
|
23780
|
+
`;
|
|
23781
|
+
CHI_GO_MOD = `module barefoot-app
|
|
23782
|
+
|
|
23783
|
+
go 1.22
|
|
23784
|
+
|
|
23785
|
+
require (
|
|
23786
|
+
github.com/barefootjs/runtime/bf v0.0.0
|
|
23787
|
+
github.com/go-chi/chi/v5 v5.1.0
|
|
23788
|
+
)
|
|
23789
|
+
|
|
23790
|
+
// The BarefootJS Go runtime ships vendored under ./bf-runtime so this
|
|
23791
|
+
// scaffold runs without depending on a published Go module.
|
|
23792
|
+
replace github.com/barefootjs/runtime/bf => ./bf-runtime
|
|
23793
|
+
`;
|
|
23794
|
+
CHI_ADAPTER = makeGoAdapter({
|
|
23795
|
+
label: "Chi (Go, html/template SSR)",
|
|
23796
|
+
mainGo: CHI_MAIN_GO,
|
|
23797
|
+
bfRenderGo: CHI_BF_RENDER_GO,
|
|
23798
|
+
goMod: CHI_GO_MOD
|
|
23799
|
+
});
|
|
23800
|
+
}
|
|
23801
|
+
});
|
|
23802
|
+
|
|
23803
|
+
// src/lib/adapters/csr.ts
|
|
23804
|
+
var CSR_GITIGNORE, CSR_BAREFOOT_CONFIG_TS, CSR_SERVER_TS, CSR_INDEX_HTML, CSR_TSCONFIG, CSR_ADAPTER;
|
|
23805
|
+
var init_csr = __esm({
|
|
23806
|
+
"src/lib/adapters/csr.ts"() {
|
|
23807
|
+
"use strict";
|
|
23808
|
+
init_shared2();
|
|
23809
|
+
CSR_GITIGNORE = buildGitignore([
|
|
23810
|
+
{
|
|
23811
|
+
heading: "bf build outputs (regenerated by `bf build` / `bf build --watch`)",
|
|
23812
|
+
entries: ["dist/"]
|
|
23813
|
+
}
|
|
23814
|
+
]);
|
|
23815
|
+
CSR_BAREFOOT_CONFIG_TS = `import { createConfig } from '@barefootjs/client/build'
|
|
23816
|
+
|
|
23817
|
+
export default createConfig({
|
|
23818
|
+
paths: {
|
|
23819
|
+
components: 'components/ui',
|
|
23820
|
+
tokens: 'tokens',
|
|
23821
|
+
meta: 'meta',
|
|
23822
|
+
},
|
|
23823
|
+
components: ['components'],
|
|
23824
|
+
outDir: 'dist',
|
|
23825
|
+
})
|
|
23826
|
+
`;
|
|
23827
|
+
CSR_SERVER_TS = `// Tiny \`node:http\` server for the CSR starter:
|
|
23828
|
+
// - HTML pages from \`./pages/<name>.html\` (\`/\` \u2192 index.html)
|
|
23829
|
+
// - Compiled component bundles from \`./dist/components/\` at /static/components/
|
|
23830
|
+
// - Other static assets from \`./public/\` at /static/
|
|
23831
|
+
//
|
|
23832
|
+
// Plain \`node:http\` + \`node:fs\` so the starter runs on any JS runtime
|
|
23833
|
+
// (Node via \`tsx\`, Bun, Deno) \u2014 no runtime is forced on you. No backend
|
|
23834
|
+
// logic, no API. Replace with Hono / Express / Fastify / etc. when the
|
|
23835
|
+
// app outgrows static-pages-plus-API.
|
|
23836
|
+
|
|
23837
|
+
import { createServer, type ServerResponse } from 'node:http'
|
|
23838
|
+
import { readFile } from 'node:fs/promises'
|
|
23839
|
+
import { dirname, resolve, relative, isAbsolute, sep } from 'node:path'
|
|
23840
|
+
import { fileURLToPath } from 'node:url'
|
|
23841
|
+
|
|
23842
|
+
const ROOT = dirname(fileURLToPath(import.meta.url))
|
|
23843
|
+
const PAGES_DIR = resolve(ROOT, 'pages')
|
|
23844
|
+
const DIST_DIR = resolve(ROOT, 'dist')
|
|
23845
|
+
const PUBLIC_DIR = resolve(ROOT, 'public')
|
|
23846
|
+
|
|
23847
|
+
const port = Number(process.env.PORT ?? 3003)
|
|
23848
|
+
|
|
23849
|
+
const server = createServer(async (req, res) => {
|
|
23850
|
+
let path: string
|
|
23851
|
+
try {
|
|
23852
|
+
path = decodeURIComponent((req.url ?? '/').split('?')[0])
|
|
23853
|
+
} catch {
|
|
23854
|
+
// Malformed percent-encoding (e.g. \`/%%\`) \u2014 answer 400 instead of
|
|
23855
|
+
// letting the decode throw take down the dev server.
|
|
23856
|
+
res.writeHead(400).end('Bad Request')
|
|
23857
|
+
return
|
|
23858
|
+
}
|
|
23859
|
+
|
|
23860
|
+
if (path.startsWith('/static/components/')) {
|
|
23861
|
+
return serveFromDir(res, resolve(DIST_DIR, 'components'), path.slice('/static/components/'.length))
|
|
23862
|
+
}
|
|
23863
|
+
if (path.startsWith('/static/')) {
|
|
23864
|
+
return serveFromDir(res, PUBLIC_DIR, path.slice('/static/'.length))
|
|
23865
|
+
}
|
|
23866
|
+
|
|
23867
|
+
const pageName = path === '/' ? 'index' : path.slice(1).replace(/\\/$/, '')
|
|
23868
|
+
return serveFromDir(res, PAGES_DIR, \`\${pageName}.html\`)
|
|
23869
|
+
})
|
|
23870
|
+
|
|
23871
|
+
async function serveFromDir(res: ServerResponse, dir: string, rel: string): Promise<void> {
|
|
23872
|
+
const target = resolve(dir, rel)
|
|
23873
|
+
// Defense-in-depth path traversal guard. Compare via relative() so it
|
|
23874
|
+
// holds on Windows (\`\\\` separators) too \u2014 a literal \`dir + '/'\` prefix
|
|
23875
|
+
// check would 403 every request there.
|
|
23876
|
+
const rel2 = relative(dir, target)
|
|
23877
|
+
if (rel2 === '..' || rel2.startsWith('..' + sep) || isAbsolute(rel2)) {
|
|
23878
|
+
res.writeHead(403).end('Forbidden')
|
|
23879
|
+
return
|
|
23880
|
+
}
|
|
23881
|
+
try {
|
|
23882
|
+
const body = await readFile(target)
|
|
23883
|
+
res.writeHead(200, { 'Content-Type': contentTypeFor(target) }).end(body)
|
|
23884
|
+
} catch (err) {
|
|
23885
|
+
// Missing path \u2192 404; anything else (permissions, IO) \u2192 500, so real
|
|
23886
|
+
// problems surface in development instead of masquerading as 404s.
|
|
23887
|
+
const code = (err as { code?: string }).code
|
|
23888
|
+
if (code === 'ENOENT' || code === 'ENOTDIR' || code === 'EISDIR') {
|
|
23889
|
+
res.writeHead(404).end('Not Found')
|
|
23890
|
+
} else {
|
|
23891
|
+
res.writeHead(500).end('Internal Server Error')
|
|
23892
|
+
}
|
|
23893
|
+
}
|
|
23894
|
+
}
|
|
23895
|
+
|
|
23896
|
+
function contentTypeFor(path: string): string {
|
|
23897
|
+
const ext = path.split('.').pop() ?? ''
|
|
23898
|
+
switch (ext) {
|
|
23899
|
+
case 'html': return 'text/html; charset=utf-8'
|
|
23900
|
+
case 'js': return 'application/javascript; charset=utf-8'
|
|
23901
|
+
case 'css': return 'text/css; charset=utf-8'
|
|
23902
|
+
case 'json': return 'application/json; charset=utf-8'
|
|
23903
|
+
case 'svg': return 'image/svg+xml'
|
|
23904
|
+
case 'png': return 'image/png'
|
|
23905
|
+
case 'jpg':
|
|
23906
|
+
case 'jpeg': return 'image/jpeg'
|
|
23907
|
+
default: return 'application/octet-stream'
|
|
23908
|
+
}
|
|
23271
23909
|
}
|
|
23272
23910
|
|
|
23273
|
-
|
|
23911
|
+
server.listen(port, () => {
|
|
23912
|
+
console.log(\` \u279C http://localhost:\${port}\`)
|
|
23913
|
+
})
|
|
23914
|
+
`;
|
|
23915
|
+
CSR_INDEX_HTML = `<!DOCTYPE html>
|
|
23916
|
+
<html lang="en">
|
|
23917
|
+
<head>
|
|
23918
|
+
<meta charset="utf-8">
|
|
23919
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
23920
|
+
<title>BarefootJS app</title>
|
|
23921
|
+
<script type="importmap">
|
|
23922
|
+
{ "imports": { "@barefootjs/client/runtime": "/static/components/barefoot.js" } }
|
|
23923
|
+
</script>
|
|
23924
|
+
<!-- Link all three sheets so the browser fetches them in parallel \u2014
|
|
23925
|
+
chaining via styles.css @import would defer tokens/uno to a
|
|
23926
|
+
second round-trip and flash unstyled DOM. tokens first so its
|
|
23927
|
+
CSS variables are defined before any rule references them. -->
|
|
23928
|
+
<link rel="stylesheet" href="/static/tokens.css">
|
|
23929
|
+
<link rel="stylesheet" href="/static/styles.css">
|
|
23930
|
+
<link rel="stylesheet" href="/static/uno.css">
|
|
23931
|
+
</head>
|
|
23932
|
+
<body>
|
|
23933
|
+
<main>
|
|
23934
|
+
<div id="app"></div>
|
|
23935
|
+
</main>
|
|
23936
|
+
<script type="module">
|
|
23937
|
+
import { render } from '@barefootjs/client/runtime'
|
|
23938
|
+
await import('/static/components/Counter.client.js')
|
|
23939
|
+
render(document.getElementById('app'), 'Counter', { initial: 0 })
|
|
23940
|
+
</script>
|
|
23941
|
+
</body>
|
|
23942
|
+
</html>
|
|
23943
|
+
`;
|
|
23944
|
+
CSR_TSCONFIG = `{
|
|
23945
|
+
"compilerOptions": {
|
|
23946
|
+
"target": "ESNext",
|
|
23947
|
+
"module": "ESNext",
|
|
23948
|
+
"moduleResolution": "bundler",
|
|
23949
|
+
"jsx": "react-jsx",
|
|
23950
|
+
"jsxImportSource": "@barefootjs/jsx",
|
|
23951
|
+
"types": ["node"],
|
|
23952
|
+
"strict": true,
|
|
23953
|
+
"skipLibCheck": true,
|
|
23954
|
+
"esModuleInterop": true,
|
|
23955
|
+
"resolveJsonModule": true,
|
|
23956
|
+
"noEmit": true,
|
|
23957
|
+
"baseUrl": ".",
|
|
23958
|
+
"paths": {
|
|
23959
|
+
"@/components/*": ["./components/*"]
|
|
23960
|
+
}
|
|
23961
|
+
},
|
|
23962
|
+
"include": ["**/*.ts", "**/*.tsx"],
|
|
23963
|
+
"exclude": ["node_modules", "dist"]
|
|
23964
|
+
}
|
|
23274
23965
|
`;
|
|
23966
|
+
CSR_ADAPTER = {
|
|
23967
|
+
label: "CSR (client-side rendering only)",
|
|
23968
|
+
port: 3003,
|
|
23969
|
+
files: {
|
|
23970
|
+
"server.ts": CSR_SERVER_TS,
|
|
23971
|
+
"pages/index.html": CSR_INDEX_HTML,
|
|
23972
|
+
"barefoot.config.ts": CSR_BAREFOOT_CONFIG_TS,
|
|
23973
|
+
"tsconfig.json": CSR_TSCONFIG,
|
|
23974
|
+
"uno.config.ts": unoConfigTs([
|
|
23975
|
+
"components/**/*.tsx",
|
|
23976
|
+
"dist/components/**/*.tsx",
|
|
23977
|
+
"pages/**/*.html"
|
|
23978
|
+
]),
|
|
23979
|
+
"components/Counter.tsx": SHARED_COUNTER_TSX,
|
|
23980
|
+
"components/Counter.test.tsx": SHARED_COUNTER_TEST_TSX,
|
|
23981
|
+
"public/styles.css": STYLES_CSS,
|
|
23982
|
+
"public/tokens.css": TOKENS_CSS,
|
|
23983
|
+
"public/uno.css": UNO_CSS_PLACEHOLDER,
|
|
23984
|
+
".gitignore": CSR_GITIGNORE
|
|
23985
|
+
},
|
|
23986
|
+
scripts: {
|
|
23987
|
+
dev: 'bf build && unocss && concurrently -k -n build,uno,server -c blue,magenta,green "bf build --watch" "unocss --watch" "tsx watch server.ts"',
|
|
23988
|
+
build: "bf build && unocss",
|
|
23989
|
+
start: "tsx server.ts"
|
|
23990
|
+
},
|
|
23991
|
+
dependencies: {
|
|
23992
|
+
"@barefootjs/client": "latest",
|
|
23993
|
+
"@barefootjs/jsx": "latest",
|
|
23994
|
+
"@barefootjs/shared": "latest"
|
|
23995
|
+
},
|
|
23996
|
+
devDependencies: {
|
|
23997
|
+
...UNOCSS_DEV_DEPENDENCIES,
|
|
23998
|
+
"@barefootjs/cli": "latest",
|
|
23999
|
+
"@barefootjs/test": "latest",
|
|
24000
|
+
"@types/node": "^22.0.0",
|
|
24001
|
+
concurrently: "^9.0.0",
|
|
24002
|
+
tsx: "^4.19.0",
|
|
24003
|
+
typescript: "^5.6.0"
|
|
24004
|
+
},
|
|
24005
|
+
// Runs on plain Node via `tsx` (same as the hono-node starter), so
|
|
24006
|
+
// there's no Bun-on-PATH prerequisite to warn about.
|
|
24007
|
+
prereqWarnings: () => []
|
|
24008
|
+
};
|
|
23275
24009
|
}
|
|
23276
24010
|
});
|
|
23277
24011
|
|
|
23278
24012
|
// src/lib/adapters/echo.ts
|
|
23279
24013
|
import { execSync as execSync2 } from "node:child_process";
|
|
23280
|
-
function
|
|
24014
|
+
function goPrereqs2() {
|
|
23281
24015
|
try {
|
|
23282
24016
|
execSync2("go version", { stdio: "ignore" });
|
|
23283
24017
|
return [];
|
|
23284
24018
|
} catch {
|
|
23285
24019
|
return [
|
|
23286
|
-
"Go toolchain not found on PATH. Install Go 1.22+ (https://go.dev/dl/) before
|
|
24020
|
+
"Go toolchain not found on PATH. Install Go 1.22+ (https://go.dev/dl/) before starting the dev server."
|
|
23287
24021
|
];
|
|
23288
24022
|
}
|
|
23289
24023
|
}
|
|
@@ -23504,7 +24238,7 @@ func mustNewRenderer() echo.Renderer {
|
|
|
23504
24238
|
tmpl, err := loadTemplates()
|
|
23505
24239
|
if err != nil {
|
|
23506
24240
|
fmt.Fprintf(os.Stderr, "barefoot: failed to load templates from dist/templates: %v\\n", err)
|
|
23507
|
-
fmt.Fprintln(os.Stderr, "barefoot: did you run \`
|
|
24241
|
+
fmt.Fprintln(os.Stderr, "barefoot: did you run \`bf build\` first?")
|
|
23508
24242
|
os.Exit(1)
|
|
23509
24243
|
}
|
|
23510
24244
|
return &echoRenderer{
|
|
@@ -23776,11 +24510,134 @@ replace github.com/barefootjs/runtime/bf => ./bf-runtime
|
|
|
23776
24510
|
concurrently: "^9.0.0",
|
|
23777
24511
|
typescript: "^5.6.0"
|
|
23778
24512
|
},
|
|
23779
|
-
prereqWarnings: () =>
|
|
24513
|
+
prereqWarnings: () => goPrereqs2()
|
|
23780
24514
|
};
|
|
23781
24515
|
}
|
|
23782
24516
|
});
|
|
23783
24517
|
|
|
24518
|
+
// src/lib/adapters/gin.ts
|
|
24519
|
+
var GIN_MAIN_GO, GIN_BF_RENDER_GO, GIN_GO_MOD, GIN_ADAPTER;
|
|
24520
|
+
var init_gin = __esm({
|
|
24521
|
+
"src/lib/adapters/gin.ts"() {
|
|
24522
|
+
"use strict";
|
|
24523
|
+
init_go_shared();
|
|
24524
|
+
GIN_MAIN_GO = `package main
|
|
24525
|
+
|
|
24526
|
+
import (
|
|
24527
|
+
"fmt"
|
|
24528
|
+
"net/http"
|
|
24529
|
+
"os"
|
|
24530
|
+
|
|
24531
|
+
bf "github.com/barefootjs/runtime/bf"
|
|
24532
|
+
"github.com/gin-gonic/gin"
|
|
24533
|
+
)
|
|
24534
|
+
|
|
24535
|
+
func main() {
|
|
24536
|
+
// Load + cache templates once (re-parsed per request in dev so a
|
|
24537
|
+
// \`bf build --watch\` rebuild shows up on refresh). See bf_render.go.
|
|
24538
|
+
renderer := mustNewRenderer()
|
|
24539
|
+
|
|
24540
|
+
r := gin.Default()
|
|
24541
|
+
|
|
24542
|
+
// Dev auto-reload SSE \u2014 mounts /_bf/reload only when APP_ENV is not
|
|
24543
|
+
// "production"; a no-op otherwise. Pairs with the snippet
|
|
24544
|
+
// bf_render.go injects into <body>.
|
|
24545
|
+
MountDevReload(r)
|
|
24546
|
+
|
|
24547
|
+
// The go-template adapter emits client bundles under dist/client/
|
|
24548
|
+
// and templates reference them at /client/ (per barefoot.config.ts).
|
|
24549
|
+
// Public assets (CSS) live under public/ and are served at /static/.
|
|
24550
|
+
// The two prefixes are kept disjoint so Gin's router tree doesn't
|
|
24551
|
+
// panic on nested catch-alls.
|
|
24552
|
+
r.Static("/client", "dist/client")
|
|
24553
|
+
r.Static("/static", "public")
|
|
24554
|
+
|
|
24555
|
+
r.GET("/", func(c *gin.Context) {
|
|
24556
|
+
// \`NewCounterProps\` / \`CounterInput\` are generated into
|
|
24557
|
+
// components.go by \`bf build\` \u2014 they wire up scope IDs,
|
|
24558
|
+
// child-slot props, and signal initial values. Use them instead
|
|
24559
|
+
// of constructing props by hand.
|
|
24560
|
+
props := NewCounterProps(CounterInput{Initial: 0})
|
|
24561
|
+
render(c, renderer, http.StatusOK, "Counter", bf.RenderOptions{
|
|
24562
|
+
Title: "BarefootJS app",
|
|
24563
|
+
Props: &props,
|
|
24564
|
+
})
|
|
24565
|
+
})
|
|
24566
|
+
|
|
24567
|
+
port := os.Getenv("PORT")
|
|
24568
|
+
if port == "" {
|
|
24569
|
+
port = "3001"
|
|
24570
|
+
}
|
|
24571
|
+
fmt.Printf(" \u279C http://localhost:%s\\n", port)
|
|
24572
|
+
if err := r.Run(":" + port); err != nil {
|
|
24573
|
+
fmt.Fprintf(os.Stderr, "barefoot: server error: %v\\n", err)
|
|
24574
|
+
os.Exit(1)
|
|
24575
|
+
}
|
|
24576
|
+
}
|
|
24577
|
+
`;
|
|
24578
|
+
GIN_BF_RENDER_GO = `package main
|
|
24579
|
+
|
|
24580
|
+
// Code generated by BarefootJS for the Gin adapter. DO NOT EDIT.
|
|
24581
|
+
// To refresh, delete this file and re-run \`bf init\` from a fresh
|
|
24582
|
+
// directory \u2014 \`bf init\` skips existing files, so it won't clobber your
|
|
24583
|
+
// edits in place.
|
|
24584
|
+
|
|
24585
|
+
import (
|
|
24586
|
+
"fmt"
|
|
24587
|
+
"html/template"
|
|
24588
|
+
"io/fs"
|
|
24589
|
+
"net/http"
|
|
24590
|
+
"os"
|
|
24591
|
+
"path/filepath"
|
|
24592
|
+
"strings"
|
|
24593
|
+
|
|
24594
|
+
bf "github.com/barefootjs/runtime/bf"
|
|
24595
|
+
"github.com/barefootjs/runtime/bf/bfdev"
|
|
24596
|
+
"github.com/gin-gonic/gin"
|
|
24597
|
+
)
|
|
24598
|
+
|
|
24599
|
+
${GO_RENDER_SHARED_FNS}
|
|
24600
|
+
// render writes a fully laid-out HTML page for the named component.
|
|
24601
|
+
func render(c *gin.Context, renderer *bf.Renderer, status int, name string, opts bf.RenderOptions) {
|
|
24602
|
+
html, err := renderToString(renderer, name, opts)
|
|
24603
|
+
if err != nil {
|
|
24604
|
+
c.String(http.StatusInternalServerError, "template error: %v", err)
|
|
24605
|
+
return
|
|
24606
|
+
}
|
|
24607
|
+
c.Data(status, "text/html; charset=utf-8", []byte(html))
|
|
24608
|
+
}
|
|
24609
|
+
|
|
24610
|
+
// MountDevReload registers the SSE auto-reload endpoint in dev mode.
|
|
24611
|
+
// In production it is a no-op.
|
|
24612
|
+
func MountDevReload(r *gin.Engine) {
|
|
24613
|
+
if !IsDev() {
|
|
24614
|
+
return
|
|
24615
|
+
}
|
|
24616
|
+
r.GET("/_bf/reload", gin.WrapH(bfdev.NewReloadHandler(bfdev.Config{DistDir: "./dist"})))
|
|
24617
|
+
}
|
|
24618
|
+
`;
|
|
24619
|
+
GIN_GO_MOD = `module barefoot-app
|
|
24620
|
+
|
|
24621
|
+
go 1.22
|
|
24622
|
+
|
|
24623
|
+
require (
|
|
24624
|
+
github.com/barefootjs/runtime/bf v0.0.0
|
|
24625
|
+
github.com/gin-gonic/gin v1.10.0
|
|
24626
|
+
)
|
|
24627
|
+
|
|
24628
|
+
// The BarefootJS Go runtime ships vendored under ./bf-runtime so this
|
|
24629
|
+
// scaffold runs without depending on a published Go module.
|
|
24630
|
+
replace github.com/barefootjs/runtime/bf => ./bf-runtime
|
|
24631
|
+
`;
|
|
24632
|
+
GIN_ADAPTER = makeGoAdapter({
|
|
24633
|
+
label: "Gin (Go, html/template SSR)",
|
|
24634
|
+
mainGo: GIN_MAIN_GO,
|
|
24635
|
+
bfRenderGo: GIN_BF_RENDER_GO,
|
|
24636
|
+
goMod: GIN_GO_MOD
|
|
24637
|
+
});
|
|
24638
|
+
}
|
|
24639
|
+
});
|
|
24640
|
+
|
|
23784
24641
|
// src/lib/adapters/hono.ts
|
|
23785
24642
|
var HONO_SERVER_TSX, HONO_RENDERER_TSX, HONO_BAREFOOT_CONFIG_TS, HONO_TSCONFIG, HONO_WRANGLER_JSONC, HONO_GITIGNORE, HONO_ADAPTER;
|
|
23786
24643
|
var init_hono = __esm({
|
|
@@ -24251,13 +25108,13 @@ function perlPrereqs() {
|
|
|
24251
25108
|
try {
|
|
24252
25109
|
execSync3("perl --version", { stdio: "ignore" });
|
|
24253
25110
|
} catch {
|
|
24254
|
-
warnings.push("Perl not found on PATH. Install Perl 5.20+ before
|
|
25111
|
+
warnings.push("Perl not found on PATH. Install Perl 5.20+ before starting the dev server.");
|
|
24255
25112
|
}
|
|
24256
25113
|
try {
|
|
24257
25114
|
execSync3("perl -MMojolicious -e1", { stdio: "ignore" });
|
|
24258
25115
|
} catch {
|
|
24259
25116
|
warnings.push(
|
|
24260
|
-
"Mojolicious not installed. Run `cpanm --installdeps .` (or `cpan Mojolicious`) before
|
|
25117
|
+
"Mojolicious not installed. Run `cpanm --installdeps .` (or `cpan Mojolicious`) before starting the dev server."
|
|
24261
25118
|
);
|
|
24262
25119
|
}
|
|
24263
25120
|
return warnings;
|
|
@@ -24399,6 +25256,7 @@ requires 'Mojolicious', '>= 9.34';
|
|
|
24399
25256
|
"app.pl": MOJO_APP_PL,
|
|
24400
25257
|
"cpanfile": MOJO_CPANFILE,
|
|
24401
25258
|
"lib/BarefootJS.pm": barefootPmSource,
|
|
25259
|
+
"lib/BarefootJS/Backend/Mojo.pm": barefootBackendMojoPmSource,
|
|
24402
25260
|
"lib/Mojolicious/Plugin/BarefootJS.pm": barefootPluginPmSource,
|
|
24403
25261
|
"lib/Mojolicious/Plugin/BarefootJS/DevReload.pm": barefootDevReloadPmSource,
|
|
24404
25262
|
"barefoot.config.ts": MOJO_BAREFOOT_CONFIG_TS,
|
|
@@ -24472,16 +25330,139 @@ requires 'Mojolicious', '>= 9.34';
|
|
|
24472
25330
|
}
|
|
24473
25331
|
});
|
|
24474
25332
|
|
|
25333
|
+
// src/lib/adapters/nethttp.ts
|
|
25334
|
+
var NETHTTP_MAIN_GO, NETHTTP_BF_RENDER_GO, NETHTTP_GO_MOD, NETHTTP_ADAPTER;
|
|
25335
|
+
var init_nethttp = __esm({
|
|
25336
|
+
"src/lib/adapters/nethttp.ts"() {
|
|
25337
|
+
"use strict";
|
|
25338
|
+
init_go_shared();
|
|
25339
|
+
NETHTTP_MAIN_GO = `package main
|
|
25340
|
+
|
|
25341
|
+
import (
|
|
25342
|
+
"fmt"
|
|
25343
|
+
"net/http"
|
|
25344
|
+
"os"
|
|
25345
|
+
|
|
25346
|
+
bf "github.com/barefootjs/runtime/bf"
|
|
25347
|
+
)
|
|
25348
|
+
|
|
25349
|
+
func main() {
|
|
25350
|
+
// Load + cache templates once (re-parsed per request in dev so a
|
|
25351
|
+
// \`bf build --watch\` rebuild shows up on refresh). See bf_render.go.
|
|
25352
|
+
renderer := mustNewRenderer()
|
|
25353
|
+
|
|
25354
|
+
mux := http.NewServeMux()
|
|
25355
|
+
|
|
25356
|
+
// Dev auto-reload SSE \u2014 registers /_bf/reload only when APP_ENV is
|
|
25357
|
+
// not "production"; a no-op otherwise. Pairs with the snippet
|
|
25358
|
+
// bf_render.go injects into <body>.
|
|
25359
|
+
MountDevReload(mux)
|
|
25360
|
+
|
|
25361
|
+
// The go-template adapter emits client bundles under dist/client/
|
|
25362
|
+
// and templates reference them at /client/ (per barefoot.config.ts).
|
|
25363
|
+
// Public assets (CSS) live under public/ and are served at /static/.
|
|
25364
|
+
mux.Handle("/client/", http.StripPrefix("/client/", http.FileServer(http.Dir("dist/client"))))
|
|
25365
|
+
mux.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.Dir("public"))))
|
|
25366
|
+
|
|
25367
|
+
// Go 1.22+ ServeMux matches "GET /{$}" only for the exact root path,
|
|
25368
|
+
// so static-asset requests don't fall through to the home handler.
|
|
25369
|
+
mux.HandleFunc("GET /{$}", func(w http.ResponseWriter, req *http.Request) {
|
|
25370
|
+
// \`NewCounterProps\` / \`CounterInput\` are generated into
|
|
25371
|
+
// components.go by \`bf build\` \u2014 they wire up scope IDs,
|
|
25372
|
+
// child-slot props, and signal initial values. Use them instead
|
|
25373
|
+
// of constructing props by hand.
|
|
25374
|
+
props := NewCounterProps(CounterInput{Initial: 0})
|
|
25375
|
+
render(w, renderer, http.StatusOK, "Counter", bf.RenderOptions{
|
|
25376
|
+
Title: "BarefootJS app",
|
|
25377
|
+
Props: &props,
|
|
25378
|
+
})
|
|
25379
|
+
})
|
|
25380
|
+
|
|
25381
|
+
port := os.Getenv("PORT")
|
|
25382
|
+
if port == "" {
|
|
25383
|
+
port = "3001"
|
|
25384
|
+
}
|
|
25385
|
+
fmt.Printf(" \u279C http://localhost:%s\\n", port)
|
|
25386
|
+
if err := http.ListenAndServe(":"+port, mux); err != nil {
|
|
25387
|
+
fmt.Fprintf(os.Stderr, "barefoot: server error: %v\\n", err)
|
|
25388
|
+
os.Exit(1)
|
|
25389
|
+
}
|
|
25390
|
+
}
|
|
25391
|
+
`;
|
|
25392
|
+
NETHTTP_BF_RENDER_GO = `package main
|
|
25393
|
+
|
|
25394
|
+
// Code generated by BarefootJS for the net/http adapter. DO NOT EDIT.
|
|
25395
|
+
// To refresh, delete this file and re-run \`bf init\` from a fresh
|
|
25396
|
+
// directory \u2014 \`bf init\` skips existing files, so it won't clobber your
|
|
25397
|
+
// edits in place.
|
|
25398
|
+
|
|
25399
|
+
import (
|
|
25400
|
+
"fmt"
|
|
25401
|
+
"html/template"
|
|
25402
|
+
"io/fs"
|
|
25403
|
+
"net/http"
|
|
25404
|
+
"os"
|
|
25405
|
+
"path/filepath"
|
|
25406
|
+
"strings"
|
|
25407
|
+
|
|
25408
|
+
bf "github.com/barefootjs/runtime/bf"
|
|
25409
|
+
"github.com/barefootjs/runtime/bf/bfdev"
|
|
25410
|
+
)
|
|
25411
|
+
|
|
25412
|
+
${GO_RENDER_SHARED_FNS}
|
|
25413
|
+
// render writes a fully laid-out HTML page for the named component.
|
|
25414
|
+
func render(w http.ResponseWriter, renderer *bf.Renderer, status int, name string, opts bf.RenderOptions) {
|
|
25415
|
+
html, err := renderToString(renderer, name, opts)
|
|
25416
|
+
if err != nil {
|
|
25417
|
+
http.Error(w, fmt.Sprintf("template error: %v", err), http.StatusInternalServerError)
|
|
25418
|
+
return
|
|
25419
|
+
}
|
|
25420
|
+
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
|
25421
|
+
w.WriteHeader(status)
|
|
25422
|
+
_, _ = w.Write([]byte(html))
|
|
25423
|
+
}
|
|
25424
|
+
|
|
25425
|
+
// MountDevReload registers the SSE auto-reload endpoint in dev mode.
|
|
25426
|
+
// In production it is a no-op.
|
|
25427
|
+
func MountDevReload(mux *http.ServeMux) {
|
|
25428
|
+
if !IsDev() {
|
|
25429
|
+
return
|
|
25430
|
+
}
|
|
25431
|
+
mux.Handle("/_bf/reload", bfdev.NewReloadHandler(bfdev.Config{DistDir: "./dist"}))
|
|
25432
|
+
}
|
|
25433
|
+
`;
|
|
25434
|
+
NETHTTP_GO_MOD = `module barefoot-app
|
|
25435
|
+
|
|
25436
|
+
go 1.22
|
|
25437
|
+
|
|
25438
|
+
require github.com/barefootjs/runtime/bf v0.0.0
|
|
25439
|
+
|
|
25440
|
+
// The BarefootJS Go runtime ships vendored under ./bf-runtime so this
|
|
25441
|
+
// scaffold runs without depending on a published Go module.
|
|
25442
|
+
replace github.com/barefootjs/runtime/bf => ./bf-runtime
|
|
25443
|
+
`;
|
|
25444
|
+
NETHTTP_ADAPTER = makeGoAdapter({
|
|
25445
|
+
label: "net/http (Go standard library, html/template SSR)",
|
|
25446
|
+
mainGo: NETHTTP_MAIN_GO,
|
|
25447
|
+
bfRenderGo: NETHTTP_BF_RENDER_GO,
|
|
25448
|
+
goMod: NETHTTP_GO_MOD
|
|
25449
|
+
});
|
|
25450
|
+
}
|
|
25451
|
+
});
|
|
25452
|
+
|
|
24475
25453
|
// src/lib/templates.ts
|
|
24476
25454
|
var CSS_LIBRARIES, DEFAULT_CSS_LIBRARY, ADAPTERS, DEFAULT_ADAPTER;
|
|
24477
25455
|
var init_templates = __esm({
|
|
24478
25456
|
"src/lib/templates.ts"() {
|
|
24479
25457
|
"use strict";
|
|
25458
|
+
init_chi();
|
|
24480
25459
|
init_csr();
|
|
24481
25460
|
init_echo();
|
|
25461
|
+
init_gin();
|
|
24482
25462
|
init_hono();
|
|
24483
25463
|
init_hono_node();
|
|
24484
25464
|
init_mojo();
|
|
25465
|
+
init_nethttp();
|
|
24485
25466
|
CSS_LIBRARIES = {
|
|
24486
25467
|
unocss: { label: "UnoCSS" }
|
|
24487
25468
|
};
|
|
@@ -24490,6 +25471,9 @@ var init_templates = __esm({
|
|
|
24490
25471
|
hono: HONO_ADAPTER,
|
|
24491
25472
|
"hono-node": HONO_NODE_ADAPTER,
|
|
24492
25473
|
echo: ECHO_ADAPTER,
|
|
25474
|
+
gin: GIN_ADAPTER,
|
|
25475
|
+
chi: CHI_ADAPTER,
|
|
25476
|
+
nethttp: NETHTTP_ADAPTER,
|
|
24493
25477
|
mojo: MOJO_ADAPTER,
|
|
24494
25478
|
csr: CSR_ADAPTER
|
|
24495
25479
|
};
|