@barefootjs/cli 0.3.0 → 0.5.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
@@ -247,6 +247,9 @@ function canRegexStartHere(prev) {
247
247
  return true;
248
248
  }
249
249
  }
250
+ function isIdentifierLikeToken(kind) {
251
+ return kind === ts.SyntaxKind.Identifier || kind >= ts.SyntaxKind.FirstKeyword && kind <= ts.SyntaxKind.LastKeyword;
252
+ }
250
253
  function isOpaqueContentKind(kind) {
251
254
  return kind === ts.SyntaxKind.StringLiteral || kind === ts.SyntaxKind.NoSubstitutionTemplateLiteral || kind === ts.SyntaxKind.RegularExpressionLiteral || kind === ts.SyntaxKind.SingleLineCommentTrivia || kind === ts.SyntaxKind.MultiLineCommentTrivia;
252
255
  }
@@ -641,143 +644,14 @@ function tokenContainsIdent(expr, ident) {
641
644
  return scanForIdentifiers(expr, (token) => token === ident);
642
645
  }
643
646
  function scanForIdentifiers(expr, predicate) {
644
- const n = expr.length;
645
- let i = 0;
646
- let state = 0;
647
- const tmplExprStack = [];
648
- let braceDepth = 0;
649
- while (i < n) {
650
- const ch = expr[i];
651
- switch (state) {
652
- case 0:
653
- // code
654
- case 4: {
655
- if (ch === "'") {
656
- state = 1;
657
- i++;
658
- continue;
659
- }
660
- if (ch === '"') {
661
- state = 2;
662
- i++;
663
- continue;
664
- }
665
- if (ch === "`") {
666
- state = 3;
667
- i++;
668
- continue;
669
- }
670
- if (ch === "/" && i + 1 < n) {
671
- const next = expr[i + 1];
672
- if (next === "/") {
673
- state = 5;
674
- i += 2;
675
- continue;
676
- }
677
- if (next === "*") {
678
- state = 6;
679
- i += 2;
680
- continue;
681
- }
682
- }
683
- if (state === 4) {
684
- if (ch === "{") {
685
- braceDepth++;
686
- i++;
687
- continue;
688
- }
689
- if (ch === "}") {
690
- if (braceDepth === 0) {
691
- const restored = tmplExprStack.pop();
692
- braceDepth = restored ?? 0;
693
- state = 3;
694
- i++;
695
- continue;
696
- }
697
- braceDepth--;
698
- i++;
699
- continue;
700
- }
701
- }
702
- if (IDENT_START_RE.test(ch)) {
703
- let j = i + 1;
704
- while (j < n && IDENT_PART_RE.test(expr[j])) j++;
705
- const token = expr.slice(i, j);
706
- let prev = i - 1;
707
- while (prev >= 0 && (expr[prev] === " " || expr[prev] === " " || expr[prev] === "\n" || expr[prev] === "\r")) prev--;
708
- const isMemberTail = prev >= 0 && expr[prev] === "." && (prev === 0 || expr[prev - 1] !== ".");
709
- if (!isMemberTail && predicate(token)) return true;
710
- i = j;
711
- continue;
712
- }
713
- i++;
714
- continue;
715
- }
716
- case 1: {
717
- if (ch === "\\" && i + 1 < n) {
718
- i += 2;
719
- continue;
720
- }
721
- if (ch === "'") {
722
- state = 0;
723
- i++;
724
- continue;
725
- }
726
- i++;
727
- continue;
728
- }
729
- case 2: {
730
- if (ch === "\\" && i + 1 < n) {
731
- i += 2;
732
- continue;
733
- }
734
- if (ch === '"') {
735
- state = 0;
736
- i++;
737
- continue;
738
- }
739
- i++;
740
- continue;
741
- }
742
- case 3: {
743
- if (ch === "\\" && i + 1 < n) {
744
- i += 2;
745
- continue;
746
- }
747
- if (ch === "`") {
748
- state = tmplExprStack.length > 0 ? 4 : 0;
749
- i++;
750
- continue;
751
- }
752
- if (ch === "$" && i + 1 < n && expr[i + 1] === "{") {
753
- tmplExprStack.push(braceDepth);
754
- braceDepth = 0;
755
- state = 4;
756
- i += 2;
757
- continue;
758
- }
759
- i++;
760
- continue;
761
- }
762
- case 5: {
763
- if (ch === "\n" || ch === "\r") {
764
- state = 0;
765
- i++;
766
- continue;
767
- }
768
- i++;
769
- continue;
770
- }
771
- case 6: {
772
- if (ch === "*" && i + 1 < n && expr[i + 1] === "/") {
773
- state = 0;
774
- i += 2;
775
- continue;
776
- }
777
- i++;
778
- continue;
779
- }
647
+ let prevSignificant;
648
+ for (const tok of iterateJsTokens(expr)) {
649
+ if (isTriviaKind(tok.kind)) continue;
650
+ if (isIdentifierLikeToken(tok.kind)) {
651
+ const isMemberTail = prevSignificant === ts2.SyntaxKind.DotToken || prevSignificant === ts2.SyntaxKind.QuestionDotToken;
652
+ if (!isMemberTail && predicate(expr.slice(tok.pos, tok.end))) return true;
780
653
  }
654
+ prevSignificant = tok.kind;
781
655
  }
782
656
  return false;
783
657
  }
@@ -858,7 +732,7 @@ function wrapExprWithLoopParams(expr, loopParams) {
858
732
  }
859
733
  return result;
860
734
  }
861
- var PROPS_PARAM, jsxToDomEventMap, IDENT_START_RE, IDENT_PART_RE;
735
+ var PROPS_PARAM, jsxToDomEventMap;
862
736
  var init_utils = __esm({
863
737
  "../jsx/src/ir-to-client-js/utils.ts"() {
864
738
  "use strict";
@@ -869,8 +743,6 @@ var init_utils = __esm({
869
743
  jsxToDomEventMap = {
870
744
  doubleclick: "dblclick"
871
745
  };
872
- IDENT_START_RE = /[A-Za-z_$]/;
873
- IDENT_PART_RE = /[A-Za-z0-9_$]/;
874
746
  }
875
747
  });
876
748
 
@@ -1885,6 +1757,9 @@ function irToComponentTemplateWithOpts(node, opts) {
1885
1757
  }
1886
1758
  return `\${${transformExpr(node.expr, node.templateExpr)}}`;
1887
1759
  case "conditional": {
1760
+ if (node.clientOnly && node.slotId) {
1761
+ return `<!--bf-cond-start:${node.slotId}--><!--bf-cond-end:${node.slotId}-->`;
1762
+ }
1888
1763
  const trueBranch = recurse(node.whenTrue);
1889
1764
  const falseBranch = recurse(node.whenFalse);
1890
1765
  const trueHtml = node.slotId ? addCondAttrToTemplate(trueBranch, node.slotId) : trueBranch;
@@ -2103,6 +1978,9 @@ function generateCsrTemplateWithOpts(node, opts) {
2103
1978
  return `\${${expr}}`;
2104
1979
  }
2105
1980
  case "conditional": {
1981
+ if (node.clientOnly && node.slotId) {
1982
+ return `<!--bf-cond-start:${node.slotId}--><!--bf-cond-end:${node.slotId}-->`;
1983
+ }
2106
1984
  const trueBranch = recurse(node.whenTrue);
2107
1985
  const falseBranch = recurse(node.whenFalse);
2108
1986
  const trueHtml = node.slotId ? addCondAttrToTemplate(trueBranch, node.slotId) : trueBranch;
@@ -5270,6 +5148,8 @@ function convertNode(node, raw) {
5270
5148
  (a, b) => a.field - b.field
5271
5149
  (a, b) => a.localeCompare(b)
5272
5150
  (a, b) => a.field.localeCompare(b.field)
5151
+ (a, b) => a.field > b.field ? 1 : -1 (relational ternary)
5152
+ any of the above ||-chained for multi-key tie-breaks
5273
5153
  (reverse the operands for descending order). Wrap the call in /* @client */ to evaluate at hydration.`
5274
5154
  };
5275
5155
  }
@@ -5425,17 +5305,40 @@ function extractSortComparatorFromTS(node, method) {
5425
5305
  const paramA = pA.name.text;
5426
5306
  const paramB = pB.name.text;
5427
5307
  let body;
5428
- if (ts8.isArrowFunction(node)) {
5429
- if (ts8.isBlock(node.body)) return null;
5308
+ if (ts8.isArrowFunction(node) && !ts8.isBlock(node.body)) {
5430
5309
  body = node.body;
5431
5310
  } else {
5432
- const stmts = node.body.statements;
5311
+ const block = node.body;
5312
+ const stmts = block.statements;
5433
5313
  if (stmts.length !== 1 || !ts8.isReturnStatement(stmts[0]) || !stmts[0].expression) return null;
5434
5314
  body = stmts[0].expression;
5435
5315
  }
5436
5316
  const raw = body.getText();
5317
+ const keys = [];
5318
+ for (const operand of flattenLogicalOr(body)) {
5319
+ const key = classifyLeafComparator(operand, paramA, paramB);
5320
+ if (!key) return null;
5321
+ keys.push(key);
5322
+ }
5323
+ if (keys.length === 0) return null;
5324
+ return { keys, raw, paramA, paramB, method };
5325
+ }
5326
+ function unwrapParens(expr) {
5327
+ let e = expr;
5328
+ while (ts8.isParenthesizedExpression(e)) e = e.expression;
5329
+ return e;
5330
+ }
5331
+ function flattenLogicalOr(expr) {
5332
+ const inner = unwrapParens(expr);
5333
+ if (ts8.isBinaryExpression(inner) && inner.operatorToken.kind === ts8.SyntaxKind.BarBarToken) {
5334
+ return [...flattenLogicalOr(inner.left), ...flattenLogicalOr(inner.right)];
5335
+ }
5336
+ return [inner];
5337
+ }
5338
+ function classifyLeafComparator(expr, paramA, paramB) {
5339
+ const body = unwrapParens(expr);
5437
5340
  if (ts8.isBinaryExpression(body) && body.operatorToken.kind === ts8.SyntaxKind.MinusToken) {
5438
- return classifyComparatorOperands(body.left, body.right, paramA, paramB, "numeric", method, raw);
5341
+ return classifyComparatorOperands(body.left, body.right, paramA, paramB, "numeric");
5439
5342
  }
5440
5343
  if (ts8.isCallExpression(body) && ts8.isPropertyAccessExpression(body.expression) && body.expression.name.text === "localeCompare" && body.arguments.length === 1) {
5441
5344
  return classifyComparatorOperands(
@@ -5444,14 +5347,15 @@ function extractSortComparatorFromTS(node, method) {
5444
5347
  body.arguments[0],
5445
5348
  paramA,
5446
5349
  paramB,
5447
- "string",
5448
- method,
5449
- raw
5350
+ "string"
5450
5351
  );
5451
5352
  }
5353
+ if (ts8.isConditionalExpression(body)) {
5354
+ return classifyTernaryComparator(body, paramA, paramB);
5355
+ }
5452
5356
  return null;
5453
5357
  }
5454
- function classifyComparatorOperands(left, right, paramA, paramB, type, method, raw) {
5358
+ function classifyComparatorOperands(left, right, paramA, paramB, type) {
5455
5359
  const leftRef = classifySortOperand(left, paramA, paramB);
5456
5360
  const rightRef = classifySortOperand(right, paramA, paramB);
5457
5361
  if (!leftRef || !rightRef) return null;
@@ -5461,15 +5365,73 @@ function classifyComparatorOperands(left, right, paramA, paramB, type, method, r
5461
5365
  return null;
5462
5366
  }
5463
5367
  const direction = leftRef.param === "A" ? "asc" : "desc";
5464
- return {
5465
- key: leftRef.key,
5466
- type,
5467
- direction,
5468
- raw,
5469
- paramA,
5470
- paramB,
5471
- method
5472
- };
5368
+ return { key: leftRef.key, type, direction };
5369
+ }
5370
+ function classifyTernaryComparator(node, paramA, paramB) {
5371
+ const cond = unwrapParens(node.condition);
5372
+ if (ts8.isBinaryExpression(cond) && (cond.operatorToken.kind === ts8.SyntaxKind.EqualsEqualsEqualsToken || cond.operatorToken.kind === ts8.SyntaxKind.EqualsEqualsToken) && sameKeyOperands(cond.left, cond.right, paramA, paramB) && numericSign(node.whenTrue) === 0) {
5373
+ const elseBranch = unwrapParens(node.whenFalse);
5374
+ if (ts8.isConditionalExpression(elseBranch)) {
5375
+ return classifyTernaryComparator(elseBranch, paramA, paramB);
5376
+ }
5377
+ return null;
5378
+ }
5379
+ if (!ts8.isBinaryExpression(cond)) return null;
5380
+ const op = cond.operatorToken.kind;
5381
+ const isGreater = op === ts8.SyntaxKind.GreaterThanToken || op === ts8.SyntaxKind.GreaterThanEqualsToken;
5382
+ const isLess = op === ts8.SyntaxKind.LessThanToken || op === ts8.SyntaxKind.LessThanEqualsToken;
5383
+ if (!isGreater && !isLess) return null;
5384
+ const leftRef = classifySortOperand(cond.left, paramA, paramB);
5385
+ const rightRef = classifySortOperand(cond.right, paramA, paramB);
5386
+ if (!leftRef || !rightRef) return null;
5387
+ if (leftRef.param === rightRef.param) return null;
5388
+ if (leftRef.key.kind !== rightRef.key.kind) return null;
5389
+ if (leftRef.key.kind === "field" && rightRef.key.kind === "field" && leftRef.key.field !== rightRef.key.field) {
5390
+ return null;
5391
+ }
5392
+ const trueSign = numericSign(node.whenTrue);
5393
+ if (trueSign === null || trueSign === 0) return null;
5394
+ if (!isBoundedTernaryElse(node.whenFalse, leftRef.key, paramA, paramB)) return null;
5395
+ const greaterForA = leftRef.param === "A" ? isGreater : !isGreater;
5396
+ const asc = greaterForA ? trueSign > 0 : trueSign < 0;
5397
+ return { key: leftRef.key, type: "auto", direction: asc ? "asc" : "desc" };
5398
+ }
5399
+ function sameKeyOperands(left, right, paramA, paramB) {
5400
+ const l = classifySortOperand(left, paramA, paramB);
5401
+ const r = classifySortOperand(right, paramA, paramB);
5402
+ if (!l || !r) return false;
5403
+ if (l.param === r.param) return false;
5404
+ if (l.key.kind !== r.key.kind) return false;
5405
+ if (l.key.kind === "field" && r.key.kind === "field" && l.key.field !== r.key.field) return false;
5406
+ return true;
5407
+ }
5408
+ function numericSign(expr) {
5409
+ const e = unwrapParens(expr);
5410
+ if (ts8.isPrefixUnaryExpression(e) && e.operator === ts8.SyntaxKind.MinusToken) {
5411
+ const inner = numericSign(e.operand);
5412
+ return inner === null ? null : -inner;
5413
+ }
5414
+ if (ts8.isNumericLiteral(e)) {
5415
+ const n = Number(e.text);
5416
+ if (Number.isNaN(n)) return null;
5417
+ if (n === 0) return 0;
5418
+ return n > 0 ? 1 : -1;
5419
+ }
5420
+ return null;
5421
+ }
5422
+ function isBoundedTernaryElse(expr, key, paramA, paramB) {
5423
+ const e = unwrapParens(expr);
5424
+ if (numericSign(e) !== null) return true;
5425
+ if (ts8.isConditionalExpression(e)) {
5426
+ const nested = classifyTernaryComparator(e, paramA, paramB);
5427
+ return nested !== null && sortKeyEquals(nested.key, key);
5428
+ }
5429
+ return false;
5430
+ }
5431
+ function sortKeyEquals(a, b) {
5432
+ if (a.kind !== b.kind) return false;
5433
+ if (a.kind === "field" && b.kind === "field") return a.field === b.field;
5434
+ return true;
5473
5435
  }
5474
5436
  function classifySortOperand(expr, paramA, paramB) {
5475
5437
  if (ts8.isIdentifier(expr)) {
@@ -8221,11 +8183,11 @@ function transformMapCall(node, ctx2, isClientOnly = false, method = "map") {
8221
8183
  if (stmt === returnStmt) break;
8222
8184
  const js = ctx2.getJS(stmt);
8223
8185
  const tjs = ctx2.getTemplateJS(stmt);
8224
- const ts19 = stmt.getText(ctx2.sourceFile);
8186
+ const ts20 = stmt.getText(ctx2.sourceFile);
8225
8187
  preambleStmts.push(js.endsWith(";") ? js : js + ";");
8226
8188
  templatePreambleStmts.push(tjs.endsWith(";") ? tjs : tjs + ";");
8227
- typedPreambleStmts.push(ts19.endsWith(";") ? ts19 : ts19 + ";");
8228
- if (js !== ts19) hasTypeDiff = true;
8189
+ typedPreambleStmts.push(ts20.endsWith(";") ? ts20 : ts20 + ";");
8190
+ if (js !== ts20) hasTypeDiff = true;
8229
8191
  if (js !== tjs) hasTemplateDiff = true;
8230
8192
  }
8231
8193
  if (preambleStmts.length > 0) {
@@ -9033,10 +8995,10 @@ function hasDynamicContent(children) {
9033
8995
  function inferExpressionType(_node, _ctx) {
9034
8996
  return null;
9035
8997
  }
9036
- function replaceBranchLocalRefs(text, branchNames, resolve10) {
8998
+ function replaceBranchLocalRefs(text, branchNames, resolve11) {
9037
8999
  if (branchNames.length === 0) return text;
9038
9000
  const pattern = new RegExp(`(?<![\\w$])(${branchNames.join("|")})(?![\\w$])`, "g");
9039
- return replaceInExprContexts(text, pattern, (_match, name) => resolve10(name));
9001
+ return replaceInExprContexts(text, pattern, (_match, name) => resolve11(name));
9040
9002
  }
9041
9003
  function buildIfStatementChain(analyzer, ctx2) {
9042
9004
  const conditionalReturns = analyzer.conditionalReturns;
@@ -12660,10 +12622,11 @@ function emitInnerLoopNested(lines, plan) {
12660
12622
  for (const stmt of innerPreludeStatements) {
12661
12623
  lines.push(` ${stmt}`);
12662
12624
  }
12663
- for (const comp of comps) {
12664
- lines.push(` const __compEl = qsaChildScope(__innerEl, ${comp.selector})`);
12665
- lines.push(` if (__compEl) initChild('${nameForRegistryRef(comp.componentName)}', __compEl, ${comp.propsExpr})`);
12666
- }
12625
+ comps.forEach((comp, i) => {
12626
+ const compElVar = comps.length > 1 ? `__compEl${i}` : "__compEl";
12627
+ lines.push(` const ${compElVar} = qsaChildScope(__innerEl, ${comp.selector})`);
12628
+ lines.push(` if (${compElVar}) initChild('${nameForRegistryRef(comp.componentName)}', ${compElVar}, ${comp.propsExpr})`);
12629
+ });
12667
12630
  lines.push(` })`);
12668
12631
  lines.push(` })`);
12669
12632
  lines.push(` }`);
@@ -17331,6 +17294,25 @@ var init_combine_client_js = __esm({
17331
17294
  }
17332
17295
  });
17333
17296
 
17297
+ // ../jsx/src/import-map.ts
17298
+ function escapeHtmlAttr(value) {
17299
+ return value.replace(/&/g, "&amp;").replace(/"/g, "&quot;").replace(/</g, "&lt;");
17300
+ }
17301
+ function renderImportMapHtml(manifest) {
17302
+ const imports = manifest.importmap?.imports ?? {};
17303
+ const json = JSON.stringify({ imports }).replace(/</g, "\\u003c");
17304
+ const lines = [`<script type="importmap">${json}</script>`];
17305
+ for (const href of manifest.preloads ?? []) {
17306
+ lines.push(`<link rel="modulepreload" href="${escapeHtmlAttr(href)}" crossorigin>`);
17307
+ }
17308
+ return lines.join("\n") + "\n";
17309
+ }
17310
+ var init_import_map = __esm({
17311
+ "../jsx/src/import-map.ts"() {
17312
+ "use strict";
17313
+ }
17314
+ });
17315
+
17334
17316
  // ../jsx/src/debug.ts
17335
17317
  function buildComponentGraph(source, filePath, componentName) {
17336
17318
  const ctx2 = analyzeComponent(source, filePath, componentName);
@@ -17503,7 +17485,7 @@ function buildLocalFunctionSetterMap(meta, setterToSignal) {
17503
17485
  }
17504
17486
  directCalls.set(name, calls);
17505
17487
  }
17506
- const resolve10 = (name, stack) => {
17488
+ const resolve11 = (name, stack) => {
17507
17489
  const out = [];
17508
17490
  const seen = /* @__PURE__ */ new Set();
17509
17491
  for (const setter of directSetters.get(name) ?? []) {
@@ -17514,7 +17496,7 @@ function buildLocalFunctionSetterMap(meta, setterToSignal) {
17514
17496
  }
17515
17497
  for (const callee of directCalls.get(name) ?? []) {
17516
17498
  if (stack.has(callee)) continue;
17517
- const sub2 = resolve10(callee, /* @__PURE__ */ new Set([...stack, callee]));
17499
+ const sub2 = resolve11(callee, /* @__PURE__ */ new Set([...stack, callee]));
17518
17500
  for (const r of sub2) {
17519
17501
  if (!seen.has(r.setter)) {
17520
17502
  out.push({ setter: r.setter, chain: [callee, ...r.chain] });
@@ -17526,7 +17508,7 @@ function buildLocalFunctionSetterMap(meta, setterToSignal) {
17526
17508
  };
17527
17509
  const result = /* @__PURE__ */ new Map();
17528
17510
  for (const name of bodies.keys()) {
17529
- const resolved = resolve10(name, /* @__PURE__ */ new Set([name]));
17511
+ const resolved = resolve11(name, /* @__PURE__ */ new Set([name]));
17530
17512
  if (resolved.length > 0) result.set(name, resolved);
17531
17513
  }
17532
17514
  return result;
@@ -18634,6 +18616,7 @@ __export(src_exports, {
18634
18616
  needsTypeBasedDetection: () => needsTypeBasedDetection,
18635
18617
  parseBlockBody: () => parseBlockBody,
18636
18618
  parseExpression: () => parseExpression,
18619
+ renderImportMapHtml: () => renderImportMapHtml,
18637
18620
  resetCompilerCounters: () => resetCompilerCounters,
18638
18621
  resolveSetters: () => resolveSetters,
18639
18622
  rewriteImportsForTemplate: () => rewriteImportsForTemplate,
@@ -18658,6 +18641,7 @@ var init_src2 = __esm({
18658
18641
  init_ir_to_client_js();
18659
18642
  init_source_map();
18660
18643
  init_combine_client_js();
18644
+ init_import_map();
18661
18645
  init_types();
18662
18646
  init_css_layer_prefixer();
18663
18647
  init_instrumentation();
@@ -19461,9 +19445,78 @@ var init_emit_ledger = __esm({
19461
19445
  }
19462
19446
  });
19463
19447
 
19448
+ // src/lib/assets-ignore.ts
19449
+ import { resolve as resolve5 } from "node:path";
19450
+ async function isCloudflareWorkersProject(projectDir) {
19451
+ for (const name of WRANGLER_CONFIG_NAMES) {
19452
+ if (await fileExists(resolve5(projectDir, name))) return true;
19453
+ }
19454
+ return false;
19455
+ }
19456
+ function collectServerOnlyAssets(input) {
19457
+ const entries = /* @__PURE__ */ new Set();
19458
+ entries.add(`${input.devSentinelSubdir}/`);
19459
+ entries.add(EMIT_LEDGER_FILENAME);
19460
+ entries.add(CACHE_FILENAME);
19461
+ if (input.hasExternals) entries.add("barefoot-externals.json");
19462
+ if (!input.clientOnly) {
19463
+ entries.add(`${input.templatesSubdir}/manifest.json`);
19464
+ for (const row of Object.values(input.manifest)) {
19465
+ if (row.markedTemplate) entries.add(row.markedTemplate);
19466
+ }
19467
+ }
19468
+ return [...entries].sort();
19469
+ }
19470
+ function stripManagedBlock(content) {
19471
+ const lines = content.split("\n");
19472
+ const begin = lines.indexOf(BLOCK_BEGIN);
19473
+ const end = lines.indexOf(BLOCK_END);
19474
+ if (begin !== -1 && end !== -1 && end > begin) {
19475
+ const kept = [...lines.slice(0, begin), ...lines.slice(end + 1)];
19476
+ return kept.join("\n").replace(/\n{3,}/g, "\n\n").trim();
19477
+ }
19478
+ return content.trim();
19479
+ }
19480
+ function buildManagedBlock(entries) {
19481
+ return [
19482
+ BLOCK_BEGIN,
19483
+ "# Server/build-only barefoot outputs \u2014 not browser-served. Regenerated on",
19484
+ "# every `bf build`; add your own entries outside this block.",
19485
+ ...entries,
19486
+ BLOCK_END
19487
+ ].join("\n");
19488
+ }
19489
+ async function writeAssetsIgnore(outDir, entries) {
19490
+ const path23 = resolve5(outDir, ASSETS_IGNORE_FILENAME);
19491
+ const existing = await fileExists(path23) ? await readText(path23) : "";
19492
+ const userContent = stripManagedBlock(existing);
19493
+ const block = buildManagedBlock(entries);
19494
+ const merged = userContent.length > 0 ? `${userContent}
19495
+
19496
+ ${block}
19497
+ ` : `${block}
19498
+ `;
19499
+ return writeIfChanged(path23, merged);
19500
+ }
19501
+ var ASSETS_IGNORE_FILENAME, BLOCK_BEGIN, BLOCK_END, WRANGLER_CONFIG_NAMES;
19502
+ var init_assets_ignore = __esm({
19503
+ "src/lib/assets-ignore.ts"() {
19504
+ "use strict";
19505
+ init_runtime();
19506
+ init_fs_utils();
19507
+ init_build_cache();
19508
+ init_emit_ledger();
19509
+ ASSETS_IGNORE_FILENAME = ".assetsignore";
19510
+ BLOCK_BEGIN = "# >>> barefoot managed block (generated by `bf build`) >>>";
19511
+ BLOCK_END = "# <<< barefoot managed block <<<";
19512
+ WRANGLER_CONFIG_NAMES = ["wrangler.toml", "wrangler.json", "wrangler.jsonc"];
19513
+ }
19514
+ });
19515
+
19464
19516
  // src/lib/build.ts
19517
+ import ts19 from "typescript";
19465
19518
  import { mkdir, readdir, stat, unlink } from "node:fs/promises";
19466
- import { resolve as resolve5, basename, relative as relative2, dirname as dirname3, isAbsolute as isAbsolute2 } from "node:path";
19519
+ import { resolve as resolve6, basename, relative as relative2, dirname as dirname3, isAbsolute as isAbsolute2 } from "node:path";
19467
19520
  import { fileURLToPath as fileURLToPath2 } from "node:url";
19468
19521
  import { build as esbuildBuild } from "esbuild";
19469
19522
  function detectMissingUseClient(content) {
@@ -19503,7 +19556,7 @@ async function discoverComponentFiles(dir, options) {
19503
19556
  return results;
19504
19557
  }
19505
19558
  for (const entry of entries) {
19506
- const fullPath = resolve5(dir, String(entry.name));
19559
+ const fullPath = resolve6(dir, String(entry.name));
19507
19560
  if (entry.isDirectory()) {
19508
19561
  if (skipDirs?.has(String(entry.name))) continue;
19509
19562
  results.push(...await discoverComponentFiles(fullPath, options));
@@ -19518,9 +19571,9 @@ function generateHash(content) {
19518
19571
  }
19519
19572
  function resolveBuildConfigFromTs(projectDir, tsConfig, overrides) {
19520
19573
  const componentDirs = (tsConfig.components ?? ["components"]).map(
19521
- (dir) => resolve5(projectDir, dir)
19574
+ (dir) => resolve6(projectDir, dir)
19522
19575
  );
19523
- const outDir = resolve5(projectDir, tsConfig.outDir ?? "dist");
19576
+ const outDir = resolve6(projectDir, tsConfig.outDir ?? "dist");
19524
19577
  return {
19525
19578
  projectDir,
19526
19579
  adapter: tsConfig.adapter,
@@ -19535,7 +19588,7 @@ function resolveBuildConfigFromTs(projectDir, tsConfig, overrides) {
19535
19588
  externals: tsConfig.externals,
19536
19589
  externalsBasePath: tsConfig.externalsBasePath,
19537
19590
  bundleEntries: tsConfig.bundleEntries?.map((e) => ({
19538
- entry: resolve5(projectDir, e.entry),
19591
+ entry: resolve6(projectDir, e.entry),
19539
19592
  outfile: e.outfile,
19540
19593
  externals: e.externals
19541
19594
  })),
@@ -19545,9 +19598,9 @@ function resolveBuildConfigFromTs(projectDir, tsConfig, overrides) {
19545
19598
  async function findCliPackageJson() {
19546
19599
  const here = dirname3(fileURLToPath2(import.meta.url));
19547
19600
  const candidates = [
19548
- resolve5(here, "../package.json"),
19601
+ resolve6(here, "../package.json"),
19549
19602
  // bundled dist/index.js
19550
- resolve5(here, "../../package.json")
19603
+ resolve6(here, "../../package.json")
19551
19604
  // source src/lib/build.ts
19552
19605
  ];
19553
19606
  for (const cand of candidates) {
@@ -19559,7 +19612,7 @@ async function findNearestLockfile(projectDir) {
19559
19612
  let dir = projectDir;
19560
19613
  while (true) {
19561
19614
  for (const name of LOCKFILE_NAMES) {
19562
- const candidate = resolve5(dir, name);
19615
+ const candidate = resolve6(dir, name);
19563
19616
  if (await fileExists(candidate)) return candidate;
19564
19617
  }
19565
19618
  const parent = dirname3(dir);
@@ -19580,9 +19633,9 @@ async function computeGlobalHash(config) {
19580
19633
  parts.push(await readText(cliPkgPath));
19581
19634
  }
19582
19635
  const configCandidates = [
19583
- resolve5(config.projectDir, "barefoot.config.ts"),
19584
- resolve5(config.projectDir, "barefoot.config.js"),
19585
- resolve5(config.projectDir, "barefoot.config.mjs")
19636
+ resolve6(config.projectDir, "barefoot.config.ts"),
19637
+ resolve6(config.projectDir, "barefoot.config.js"),
19638
+ resolve6(config.projectDir, "barefoot.config.mjs")
19586
19639
  ];
19587
19640
  for (const cand of configCandidates) {
19588
19641
  if (await fileExists(cand)) {
@@ -19603,9 +19656,9 @@ async function build(config, options = {}) {
19603
19656
  const templatesSubdir = layout?.templates ?? "components";
19604
19657
  const clientJsSubdir = layout?.clientJs ?? "components";
19605
19658
  const runtimeSubdir = layout?.runtime ?? clientJsSubdir;
19606
- const templatesOutDir = resolve5(config.outDir, templatesSubdir);
19607
- const clientJsOutDir = resolve5(config.outDir, clientJsSubdir);
19608
- const runtimeOutDir = resolve5(config.outDir, runtimeSubdir);
19659
+ const templatesOutDir = resolve6(config.outDir, templatesSubdir);
19660
+ const clientJsOutDir = resolve6(config.outDir, clientJsSubdir);
19661
+ const runtimeOutDir = resolve6(config.outDir, runtimeSubdir);
19609
19662
  await Promise.all([
19610
19663
  mkdir(templatesOutDir, { recursive: true }),
19611
19664
  mkdir(clientJsOutDir, { recursive: true }),
@@ -19619,14 +19672,14 @@ async function build(config, options = {}) {
19619
19672
  let anyOutputChanged = false;
19620
19673
  const loadedLedger = await loadEmitLedger(config.outDir, config.projectDir);
19621
19674
  const previousEmitEntries = loadedLedger?.entries ?? extractLedgerFromCache(onDiskCache);
19622
- const domPkgDir = resolve5(config.projectDir, "node_modules/@barefootjs/client");
19675
+ const domPkgDir = resolve6(config.projectDir, "node_modules/@barefootjs/client");
19623
19676
  const domDistCandidates = [
19624
- resolve5(config.projectDir, "../../packages/client/dist/runtime/standalone.js"),
19625
- resolve5(domPkgDir, "dist/runtime/standalone.js"),
19677
+ resolve6(config.projectDir, "../../packages/client/dist/runtime/standalone.js"),
19678
+ resolve6(domPkgDir, "dist/runtime/standalone.js"),
19626
19679
  // Legacy fallback for older @barefootjs/client dists that only shipped
19627
19680
  // the single runtime entry.
19628
- resolve5(config.projectDir, "../../packages/client/dist/runtime/index.js"),
19629
- resolve5(domPkgDir, "dist/runtime/index.js")
19681
+ resolve6(config.projectDir, "../../packages/client/dist/runtime/index.js"),
19682
+ resolve6(domPkgDir, "dist/runtime/index.js")
19630
19683
  ];
19631
19684
  let domDistFile = null;
19632
19685
  for (const candidate of domDistCandidates) {
@@ -19638,7 +19691,7 @@ async function build(config, options = {}) {
19638
19691
  }
19639
19692
  }
19640
19693
  if (domDistFile) {
19641
- const runtimeOutPath = resolve5(runtimeOutDir, "barefoot.js");
19694
+ const runtimeOutPath = resolve6(runtimeOutDir, "barefoot.js");
19642
19695
  let runtimeContent;
19643
19696
  if (config.minify) {
19644
19697
  runtimeContent = transpile(await readText(domDistFile), { loader: "js", minify: true });
@@ -19811,9 +19864,9 @@ async function build(config, options = {}) {
19811
19864
  if (!currentEmitSet.has(output)) orphanedOutputs.add(output);
19812
19865
  }
19813
19866
  }
19814
- const outDirAbs = resolve5(config.outDir);
19867
+ const outDirAbs = resolve6(config.outDir);
19815
19868
  for (const output of orphanedOutputs) {
19816
- const abs = resolve5(config.outDir, output);
19869
+ const abs = resolve6(config.outDir, output);
19817
19870
  const rel = relative2(outDirAbs, abs);
19818
19871
  const escapes = rel === "" || rel === "." || rel.startsWith("..") || isAbsolute2(rel);
19819
19872
  if (escapes) {
@@ -19841,7 +19894,7 @@ async function build(config, options = {}) {
19841
19894
  if (!entry.clientJs) continue;
19842
19895
  let raw = compiledClientJsByKey.get(name);
19843
19896
  if (!raw) {
19844
- const filePath = resolve5(config.outDir, entry.clientJs);
19897
+ const filePath = resolve6(config.outDir, entry.clientJs);
19845
19898
  try {
19846
19899
  raw = await readText(filePath);
19847
19900
  } catch {
@@ -19856,7 +19909,7 @@ async function build(config, options = {}) {
19856
19909
  for (const [name, content] of combined) {
19857
19910
  const entry = manifest[name];
19858
19911
  if (!entry?.clientJs) continue;
19859
- const filePath = resolve5(config.outDir, entry.clientJs);
19912
+ const filePath = resolve6(config.outDir, entry.clientJs);
19860
19913
  if (await writeIfChanged(filePath, content)) {
19861
19914
  anyOutputChanged = true;
19862
19915
  console.log(`Combined: ${entry.clientJs}`);
@@ -19909,10 +19962,10 @@ async function build(config, options = {}) {
19909
19962
  }
19910
19963
  }
19911
19964
  {
19912
- const runtimeAbs = resolve5(config.outDir, runtimeSubdir, "barefoot.js");
19965
+ const runtimeAbs = resolve6(config.outDir, runtimeSubdir, "barefoot.js");
19913
19966
  for (const [name, entry] of Object.entries(manifest)) {
19914
19967
  if (!entry.clientJs || name === "__barefoot__") continue;
19915
- const filePath = resolve5(config.outDir, entry.clientJs);
19968
+ const filePath = resolve6(config.outDir, entry.clientJs);
19916
19969
  let rel = relative2(dirname3(filePath), runtimeAbs);
19917
19970
  if (!rel.startsWith(".")) rel = "./" + rel;
19918
19971
  try {
@@ -19935,7 +19988,7 @@ async function build(config, options = {}) {
19935
19988
  if (config.minify) {
19936
19989
  for (const [name, entry] of Object.entries(manifest)) {
19937
19990
  if (!entry.clientJs || name === "__barefoot__") continue;
19938
- const filePath = resolve5(config.outDir, entry.clientJs);
19991
+ const filePath = resolve6(config.outDir, entry.clientJs);
19939
19992
  try {
19940
19993
  const content = await readText(filePath);
19941
19994
  if (content) {
@@ -19960,8 +20013,8 @@ async function build(config, options = {}) {
19960
20013
  });
19961
20014
  }
19962
20015
  if (!config.clientOnly) {
19963
- const manifestDir = resolve5(config.outDir, templatesSubdir);
19964
- const manifestPath = resolve5(manifestDir, "manifest.json");
20016
+ const manifestDir = resolve6(config.outDir, templatesSubdir);
20017
+ const manifestPath = resolve6(manifestDir, "manifest.json");
19965
20018
  const manifestContent = JSON.stringify(manifest, null, 2);
19966
20019
  if (await writeIfChanged(manifestPath, manifestContent)) {
19967
20020
  anyOutputChanged = true;
@@ -19989,6 +20042,18 @@ async function build(config, options = {}) {
19989
20042
  saveCache(config.outDir, nextCache),
19990
20043
  saveEmitLedger(config.outDir, config.projectDir, nextLedger)
19991
20044
  ]);
20045
+ if (await isCloudflareWorkersProject(config.projectDir)) {
20046
+ const ignored = collectServerOnlyAssets({
20047
+ devSentinelSubdir: DEV_SENTINEL_SUBDIR,
20048
+ templatesSubdir,
20049
+ manifest,
20050
+ hasExternals: !!config.externals && Object.keys(config.externals).length > 0,
20051
+ clientOnly: config.clientOnly
20052
+ });
20053
+ if (await writeAssetsIgnore(config.outDir, ignored)) {
20054
+ console.log(`Generated: ${ASSETS_IGNORE_FILENAME}`);
20055
+ }
20056
+ }
19992
20057
  return {
19993
20058
  compiledCount,
19994
20059
  skippedCount,
@@ -19999,6 +20064,28 @@ async function build(config, options = {}) {
19999
20064
  sharedProgram
20000
20065
  };
20001
20066
  }
20067
+ function extractBareImports(code) {
20068
+ const { importedFiles } = ts19.preProcessFile(code, true, true);
20069
+ const specifiers = /* @__PURE__ */ new Set();
20070
+ for (const { fileName } of importedFiles) {
20071
+ if (!fileName.startsWith(".") && !fileName.startsWith("/") && !fileName.includes("://")) {
20072
+ specifiers.add(fileName);
20073
+ }
20074
+ }
20075
+ return [...specifiers];
20076
+ }
20077
+ function unresolvedBareImports(code, externals) {
20078
+ const keys = /* @__PURE__ */ new Set([...Object.keys(externals), ...BF_CLIENT_DEDUP_KEYS]);
20079
+ const isResolved = (spec) => keys.has(spec) || [...keys].some((k) => k.endsWith("/") && spec.startsWith(k));
20080
+ return extractBareImports(code).filter((spec) => !isResolved(spec));
20081
+ }
20082
+ function rebundleExternalsFor(pkgName, externals) {
20083
+ return [.../* @__PURE__ */ new Set([
20084
+ ...Object.keys(externals).filter((k) => k !== pkgName),
20085
+ ...BF_CLIENT_DEDUP_KEYS,
20086
+ "@barefootjs/client/*"
20087
+ ])];
20088
+ }
20002
20089
  function vendorChunkFilename(pkgName) {
20003
20090
  const base = pkgName.includes("/") ? pkgName.split("/").pop() : pkgName;
20004
20091
  return `${base}.js`;
@@ -20007,7 +20094,7 @@ function effectiveNamesFor(entryPath, componentDirs) {
20007
20094
  const bn = basename(entryPath);
20008
20095
  if (componentDirs && componentDirs.length > 0) {
20009
20096
  for (const dir of componentDirs) {
20010
- const root = resolve5(dir);
20097
+ const root = resolve6(dir);
20011
20098
  if (entryPath !== root && entryPath.startsWith(root + "/")) {
20012
20099
  const rel = entryPath.slice(root.length + 1);
20013
20100
  const noExt2 = rel.replace(/\.[^.]+$/, "");
@@ -20021,14 +20108,14 @@ function effectiveNamesFor(entryPath, componentDirs) {
20021
20108
  function buildRelativeImportRewriter(sourcePath, outputPath, componentDirs, templatesOutDir) {
20022
20109
  const sourceDir = dirname3(sourcePath);
20023
20110
  const outputDir = dirname3(outputPath);
20024
- const resolvedComponentDirs = componentDirs.map((d) => resolve5(d));
20111
+ const resolvedComponentDirs = componentDirs.map((d) => resolve6(d));
20025
20112
  return (importPath) => {
20026
- const srcAbs = resolve5(sourceDir, importPath);
20113
+ const srcAbs = resolve6(sourceDir, importPath);
20027
20114
  let targetAbs = srcAbs;
20028
20115
  for (const componentDir of resolvedComponentDirs) {
20029
20116
  if (srcAbs === componentDir || srcAbs.startsWith(componentDir + "/")) {
20030
20117
  const relUnderComponentDir = srcAbs.slice(componentDir.length + 1);
20031
- targetAbs = relUnderComponentDir ? resolve5(templatesOutDir, relUnderComponentDir) : templatesOutDir;
20118
+ targetAbs = relUnderComponentDir ? resolve6(templatesOutDir, relUnderComponentDir) : templatesOutDir;
20032
20119
  break;
20033
20120
  }
20034
20121
  }
@@ -20081,7 +20168,7 @@ function mergeDuplicateNamedImports(content) {
20081
20168
  return out.join("\n");
20082
20169
  }
20083
20170
  async function resolvePkgBrowserEntry(pkgDir) {
20084
- const pkgJsonPath = resolve5(pkgDir, "package.json");
20171
+ const pkgJsonPath = resolve6(pkgDir, "package.json");
20085
20172
  if (!await fileExists(pkgJsonPath)) return null;
20086
20173
  const pkg = JSON.parse(await readText(pkgJsonPath));
20087
20174
  const browserCandidates = [
@@ -20090,7 +20177,7 @@ async function resolvePkgBrowserEntry(pkgDir) {
20090
20177
  pkg.jsdelivr
20091
20178
  ].filter((v) => typeof v === "string");
20092
20179
  for (const rel of browserCandidates) {
20093
- const abs = resolve5(pkgDir, rel);
20180
+ const abs = resolve6(pkgDir, rel);
20094
20181
  if (await fileExists(abs)) return { path: abs, isBrowserReady: true };
20095
20182
  }
20096
20183
  const fallbackCandidates = [
@@ -20098,7 +20185,7 @@ async function resolvePkgBrowserEntry(pkgDir) {
20098
20185
  pkg.main
20099
20186
  ].filter((v) => typeof v === "string");
20100
20187
  for (const rel of fallbackCandidates) {
20101
- const abs = resolve5(pkgDir, rel);
20188
+ const abs = resolve6(pkgDir, rel);
20102
20189
  if (await fileExists(abs)) return { path: abs, isBrowserReady: false };
20103
20190
  }
20104
20191
  return null;
@@ -20121,35 +20208,41 @@ async function processExternals(config, runtimeSubdir, runtimeOutDir) {
20121
20208
  continue;
20122
20209
  }
20123
20210
  if (isChunk) {
20124
- const pkgDir = resolve5(config.projectDir, "node_modules", pkgName);
20211
+ const pkgDir = resolve6(config.projectDir, "node_modules", pkgName);
20125
20212
  const entry = await resolvePkgBrowserEntry(pkgDir);
20126
20213
  if (!entry) {
20127
20214
  console.warn(`Warning: externals \u2014 could not resolve browser entry for "${pkgName}". Skipping.`);
20128
20215
  continue;
20129
20216
  }
20130
20217
  const wantRebundle = typeof spec === "object" && !("url" in spec) && spec.rebundle === true;
20131
- if (!entry.isBrowserReady && !wantRebundle) {
20132
- console.warn(
20133
- `Warning: externals \u2014 "${pkgName}" resolved via import/main entry (no umd/unpkg/jsdelivr found). The copied file may contain external imports that are not browser-ready. Set rebundle: true to re-bundle it into a self-contained ESM file.`
20134
- );
20135
- }
20136
20218
  const srcFile = entry.path;
20137
20219
  const filename = vendorChunkFilename(pkgName);
20138
- const destPath = resolve5(runtimeOutDir, filename);
20220
+ const destPath = resolve6(runtimeOutDir, filename);
20139
20221
  if (wantRebundle) {
20140
20222
  await esbuildBuild({
20141
20223
  entryPoints: [srcFile],
20142
20224
  outfile: destPath,
20143
20225
  format: "esm",
20144
20226
  bundle: true,
20145
- minify: config.minify ?? false
20227
+ minify: config.minify ?? false,
20228
+ external: rebundleExternalsFor(pkgName, config.externals)
20146
20229
  });
20147
20230
  anyChanged = true;
20148
20231
  console.log(`Generated (bundled): ${runtimeSubdir}/${filename}`);
20149
20232
  } else {
20150
- let content = await readBytes(srcFile);
20233
+ const bytes = await readBytes(srcFile);
20234
+ const text = new TextDecoder().decode(bytes);
20235
+ if (!entry.isBrowserReady) {
20236
+ const unresolved = unresolvedBareImports(text, config.externals);
20237
+ if (unresolved.length > 0) {
20238
+ console.warn(
20239
+ `Warning: externals \u2014 "${pkgName}" resolved via import/main entry (no umd/unpkg/jsdelivr found) and imports packages not in the importmap: ${unresolved.join(", ")}. These are not browser-ready. Set rebundle: true to re-bundle it into a self-contained ESM file.`
20240
+ );
20241
+ }
20242
+ }
20243
+ let content = bytes;
20151
20244
  if (config.minify) {
20152
- content = transpile(content instanceof Uint8Array ? new TextDecoder().decode(content) : content, { loader: "js", minify: true });
20245
+ content = transpile(text, { loader: "js", minify: true });
20153
20246
  }
20154
20247
  if (await writeIfChanged(destPath, content)) {
20155
20248
  anyChanged = true;
@@ -20174,11 +20267,18 @@ async function processExternals(config, runtimeSubdir, runtimeOutDir) {
20174
20267
  preloads,
20175
20268
  externals: allExternals
20176
20269
  };
20177
- const manifestPath = resolve5(config.outDir, "barefoot-externals.json");
20270
+ const manifestPath = resolve6(config.outDir, "barefoot-externals.json");
20178
20271
  if (await writeIfChanged(manifestPath, JSON.stringify(manifest, null, 2))) {
20179
20272
  anyChanged = true;
20180
20273
  console.log("Generated: barefoot-externals.json");
20181
20274
  }
20275
+ if (config.adapter.importMapInjection === "html-snippet") {
20276
+ const snippetPath = resolve6(config.outDir, "barefoot-importmap.html");
20277
+ if (await writeIfChanged(snippetPath, renderImportMapHtml(manifest))) {
20278
+ anyChanged = true;
20279
+ console.log("Generated: barefoot-importmap.html");
20280
+ }
20281
+ }
20182
20282
  return { changed: anyChanged, allExternals };
20183
20283
  }
20184
20284
  async function processBundleEntries(config, clientJsOutDir, clientJsSubdir, allExternals, cache, nextEntries, force) {
@@ -20186,8 +20286,8 @@ async function processBundleEntries(config, clientJsOutDir, clientJsSubdir, allE
20186
20286
  let anyChanged = false;
20187
20287
  for (const entry of config.bundleEntries) {
20188
20288
  const entryExternals = [...allExternals, ...entry.externals ?? []];
20189
- const outfilePath = resolve5(clientJsOutDir, entry.outfile);
20190
- const absEntry = resolve5(entry.entry);
20289
+ const outfilePath = resolve6(clientJsOutDir, entry.outfile);
20290
+ const absEntry = resolve6(entry.entry);
20191
20291
  const cacheKey = `${BUNDLE_KEY_PREFIX}${absEntry}`;
20192
20292
  const sourceContent = await readText(absEntry);
20193
20293
  const sourceHash = hashContent(sourceContent);
@@ -20227,7 +20327,7 @@ async function processBundleEntries(config, clientJsOutDir, clientJsSubdir, allE
20227
20327
  if (metafile) {
20228
20328
  for (const inputPath of Object.keys(metafile.inputs)) {
20229
20329
  if (externalsSet.has(inputPath)) continue;
20230
- const abs = resolve5(absWorkingDir, inputPath);
20330
+ const abs = resolve6(absWorkingDir, inputPath);
20231
20331
  if (abs === absEntry) continue;
20232
20332
  if (abs.includes("/node_modules/")) continue;
20233
20333
  if (await fileExists(abs)) {
@@ -20252,7 +20352,7 @@ async function collectRelativeImportDeps(entryPath, sourceContent) {
20252
20352
  for (const match of sourceContent.matchAll(RELATIVE_IMPORT_SCAN_RE)) {
20253
20353
  const rel = match[1];
20254
20354
  if (!rel.startsWith(".")) continue;
20255
- const base = resolve5(baseDir, rel);
20355
+ const base = resolve6(baseDir, rel);
20256
20356
  const candidates = [base, ...EXT_CANDIDATES.map((ext) => base + ext)];
20257
20357
  for (const cand of candidates) {
20258
20358
  if (seen.has(cand)) continue;
@@ -20286,7 +20386,7 @@ async function compileEntry(args2) {
20286
20386
  for (const depPath of await collectRelativeImportDeps(entryPath, sourceContent)) {
20287
20387
  deps[depPath] = hashContent(await readText(depPath));
20288
20388
  }
20289
- const presumedOutputPath = resolve5(
20389
+ const presumedOutputPath = resolve6(
20290
20390
  templatesOutDir,
20291
20391
  baseFileName.replace(/\.tsx?$/, config.adapter.extension)
20292
20392
  );
@@ -20352,7 +20452,7 @@ async function compileEntry(args2) {
20352
20452
  if (hasClientJs) {
20353
20453
  const rel = `${clientJsSubdir}/${clientJsFilename}`;
20354
20454
  outputs.push(rel);
20355
- const target = resolve5(clientJsOutDir, clientJsFilename);
20455
+ const target = resolve6(clientJsOutDir, clientJsFilename);
20356
20456
  await mkdir(dirname3(target), { recursive: true });
20357
20457
  if (await writeIfChanged(target, clientJsContent)) {
20358
20458
  wroteAny = true;
@@ -20369,7 +20469,7 @@ async function compileEntry(args2) {
20369
20469
  }
20370
20470
  const rel = `${templatesSubdir}/${outName}`;
20371
20471
  outputs.push(rel);
20372
- const target = resolve5(templatesOutDir, outName);
20472
+ const target = resolve6(templatesOutDir, outName);
20373
20473
  await mkdir(dirname3(target), { recursive: true });
20374
20474
  if (await writeIfChanged(target, outputContent)) {
20375
20475
  wroteAny = true;
@@ -20414,9 +20514,9 @@ async function compileEntry(args2) {
20414
20514
  }
20415
20515
  async function writeBuildId(outDir, result) {
20416
20516
  if (!result.changed) return;
20417
- const devDir = resolve5(outDir, DEV_SENTINEL_SUBDIR);
20517
+ const devDir = resolve6(outDir, DEV_SENTINEL_SUBDIR);
20418
20518
  await mkdir(devDir, { recursive: true });
20419
- const path23 = resolve5(devDir, DEV_SENTINEL_FILENAME);
20519
+ const path23 = resolve6(devDir, DEV_SENTINEL_FILENAME);
20420
20520
  await writeIfChanged(path23, String(Date.now()));
20421
20521
  }
20422
20522
  async function watch(config, options = {}) {
@@ -20559,6 +20659,7 @@ var init_build = __esm({
20559
20659
  init_build_cache();
20560
20660
  init_emit_ledger();
20561
20661
  init_fs_utils();
20662
+ init_assets_ignore();
20562
20663
  init_runtime();
20563
20664
  init_resolve_imports();
20564
20665
  BF001_TRIPWIRE_IMPORTS = /* @__PURE__ */ new Set([
@@ -22268,7 +22369,7 @@ var bfGoSource, streamingGoSource, barefootPmSource, barefootPluginPmSource, bar
22268
22369
  var init_runtimes_generated = __esm({
22269
22370
  "src/lib/adapters/runtimes.generated.ts"() {
22270
22371
  "use strict";
22271
- bfGoSource = '// Package bf provides runtime helper functions for BarefootJS Go templates.\n// These functions mirror JavaScript behavior for consistent SSR output.\npackage bf\n\nimport (\n "bytes"\n "encoding/json"\n "fmt"\n "html/template"\n "math"\n "os"\n "reflect"\n "sort"\n "strconv"\n "strings"\n)\n\n// FuncMap returns a template.FuncMap with all BarefootJS helper functions.\n// Usage:\n//\n// tmpl := template.New("").Funcs(bf.FuncMap())\nfunc FuncMap() template.FuncMap {\n return template.FuncMap{\n // Arithmetic\n "bf_add": Add,\n "bf_sub": Sub,\n "bf_mul": Mul,\n "bf_div": Div,\n "bf_mod": Mod,\n "bf_neg": Neg,\n\n // String\n "bf_lower": Lower,\n "bf_upper": Upper,\n "bf_trim": Trim,\n "bf_contains": Contains,\n "bf_join": Join,\n "bf_string": String,\n\n // JSON / numeric primitives \u2014 JS-compat callees registered on\n // the Go adapter\'s `templatePrimitives` map (#1188).\n "bf_json": JSON,\n "bf_number": Number,\n "bf_floor": Floor,\n "bf_ceil": Ceil,\n "bf_round": Round,\n\n // Array/Slice\n "bf_len": Len,\n "bf_at": At,\n "bf_includes": Includes,\n "bf_index_of": IndexOf,\n "bf_last_index_of": LastIndexOf,\n "bf_concat": Concat,\n "bf_slice": Slice,\n "bf_reverse": Reverse,\n "bf_first": First,\n "bf_last": Last,\n "bf_arr": Arr,\n "bf_filter_truthy": FilterTruthy,\n\n // Higher-order Array Methods\n "bf_every": Every,\n "bf_some": Some,\n "bf_filter": Filter,\n "bf_find": Find,\n "bf_find_index": FindIndex,\n "bf_find_last": FindLast,\n "bf_find_last_index": FindLastIndex,\n "bf_sort": Sort,\n\n // Comment marker (for hydration)\n "bfComment": Comment,\n "bfTextStart": TextStart,\n "bfTextEnd": TextEnd,\n\n // Script collection\n "bfScripts": BfScripts,\n\n // Scope attribute value (#1249: bare scope id, no `~` prefix)\n "bfScopeAttr": ScopeAttr,\n\n // Slot-identity markers (#1249): bf-h, bf-m, bf-r\n "bfHydrationAttrs": HydrationAttrs,\n\n // Child component marker (kept for backward compatibility)\n "bfIsChild": IsChild,\n\n // Props attribute for hydration\n "bfPropsAttr": BfPropsAttr,\n\n // Portal HTML rendering (parses and executes template string)\n "bfPortalHTML": PortalHTML,\n\n // Scope comment for fragment roots\n "bfScopeComment": ScopeComment,\n\n // JSX intrinsic-element spread lowering (#1407)\n "bf_spread_attrs": SpreadAttrs,\n }\n}\n\n// ScopeAttr returns the bare bf-s scope id (#1249).\nfunc ScopeAttr(props interface{}) string {\n return getStringField(props, "ScopeID")\n}\n\n// HydrationAttrs emits `bf-h="<host>" bf-m="<slot>" bf-r=""` conditionally.\n// See spec/compiler.md "Slot identity".\nfunc HydrationAttrs(props interface{}) template.HTMLAttr {\n parts := []string{}\n if host := getStringField(props, "BfParent"); host != "" {\n parts = append(parts, fmt.Sprintf(`bf-h="%s"`, template.HTMLEscapeString(host)))\n }\n if mount := getStringField(props, "BfMount"); mount != "" {\n parts = append(parts, fmt.Sprintf(`bf-m="%s"`, template.HTMLEscapeString(mount)))\n }\n if !getBoolField(props, "BfIsChild") {\n parts = append(parts, `bf-r=""`)\n }\n if len(parts) == 0 {\n return ""\n }\n return template.HTMLAttr(strings.Join(parts, " "))\n}\n\n// IsChild is a deprecated no-op stub. Child status is signalled by bf-h\n// presence (#1249); use HydrationAttrs instead.\nfunc IsChild(props interface{}) template.HTMLAttr {\n return ""\n}\n\n// svgCamelCaseAttrs mirrors SVG_CAMEL_CASE_ATTRS from\n// packages/client/src/runtime/spread-attrs.ts. SVG XML attribute\n// names are case-sensitive; the default camelCase \u2192 kebab-case\n// rewrite must NOT apply to these or the SVG stops rendering\n// (#1407). Coordinates with the compile-time SVG_CAMEL_TO_KEBAB\n// table in packages/jsx/src/ir-to-client-js/utils.ts: presentation\n// attrs (clipPath, strokeWidth, \u2026) live there and must NOT appear\n// here, or the same JSX prop would lower to clip-path via the\n// explicit-attr path and stay clipPath via the spread path.\nvar svgCamelCaseAttrs = map[string]struct{}{\n "allowReorder": {}, "attributeName": {}, "attributeType": {}, "autoReverse": {},\n "baseFrequency": {}, "baseProfile": {}, "calcMode": {}, "clipPathUnits": {},\n "contentScriptType": {}, "contentStyleType": {}, "diffuseConstant": {}, "edgeMode": {},\n "externalResourcesRequired": {}, "filterRes": {}, "filterUnits": {}, "glyphRef": {},\n "gradientTransform": {}, "gradientUnits": {}, "kernelMatrix": {}, "kernelUnitLength": {},\n "keyPoints": {}, "keySplines": {}, "keyTimes": {}, "lengthAdjust": {}, "limitingConeAngle": {},\n "markerHeight": {}, "markerUnits": {}, "markerWidth": {}, "maskContentUnits": {},\n "maskUnits": {}, "numOctaves": {}, "pathLength": {}, "patternContentUnits": {},\n "patternTransform": {}, "patternUnits": {}, "pointsAtX": {}, "pointsAtY": {}, "pointsAtZ": {},\n "preserveAlpha": {}, "preserveAspectRatio": {}, "primitiveUnits": {}, "refX": {}, "refY": {},\n "repeatCount": {}, "repeatDur": {}, "requiredExtensions": {}, "requiredFeatures": {},\n "specularConstant": {}, "specularExponent": {}, "spreadMethod": {}, "startOffset": {},\n "stdDeviation": {}, "stitchTiles": {}, "surfaceScale": {}, "systemLanguage": {},\n "tableValues": {}, "targetX": {}, "targetY": {}, "textLength": {}, "viewBox": {}, "viewTarget": {},\n "xChannelSelector": {}, "yChannelSelector": {}, "zoomAndPan": {},\n}\n\n// toAttrName mirrors the JSX\u2192HTML attribute-name rewrite from\n// packages/client/src/runtime/spread-attrs.ts. className \u2192 class,\n// htmlFor \u2192 for, SVG camelCase attrs preserved, other camelCase\n// keys lowered to kebab-case.\nfunc toAttrName(key string) string {\n if key == "className" {\n return "class"\n }\n if key == "htmlFor" {\n return "for"\n }\n if _, ok := svgCamelCaseAttrs[key]; ok {\n return key\n }\n // camelCase \u2192 kebab-case: mirror the JS reference exactly\n // (`key.replace(/([A-Z])/g, \'-$1\').toLowerCase()`). The JS shape\n // produces a leading `-` for an initial uppercase letter\n // (`XData` \u2192 `-x-data`); both this Go path and the matching JS\n // runtime are wrong-by-construction for that case (the resulting\n // HTML attribute name is invalid), but keeping them byte-equal\n // avoids silent SSR/CSR divergence (#1411 review).\n var b strings.Builder\n for _, r := range key {\n if r >= \'A\' && r <= \'Z\' {\n b.WriteByte(\'-\')\n b.WriteRune(r + 32)\n } else {\n b.WriteRune(r)\n }\n }\n return b.String()\n}\n\n// StyleToCss mirrors styleToCss from\n// packages/client/src/runtime/style.ts. Accepts a string passthrough,\n// or a map (JSON-deserialized object) whose camelCase keys are\n// lowered to kebab-case and joined with `;`. Returns ("", false) for\n// nullish/empty input so callers can omit the attribute entirely.\nfunc StyleToCss(v any) (string, bool) {\n if v == nil {\n return "", false\n }\n rv := reflect.ValueOf(v)\n for rv.Kind() == reflect.Interface || rv.Kind() == reflect.Pointer {\n if rv.IsNil() {\n return "", false\n }\n rv = rv.Elem()\n }\n if rv.Kind() != reflect.Map {\n // Non-object: stringify and return as-is, matching the JS\n // `typeof value !== \'object\'` branch.\n s := fmt.Sprint(v)\n if s == "" {\n return "", false\n }\n return s, true\n }\n keys := rv.MapKeys()\n sorted := make([]string, 0, len(keys))\n for _, k := range keys {\n if k.Kind() == reflect.String {\n sorted = append(sorted, k.String())\n }\n }\n sort.Strings(sorted)\n parts := make([]string, 0, len(sorted))\n for _, k := range sorted {\n val := rv.MapIndex(reflect.ValueOf(k))\n // Skip nil entries (matches the JS `if (v == null) continue`).\n if !val.IsValid() {\n continue\n }\n if val.Kind() == reflect.Interface || val.Kind() == reflect.Pointer {\n if val.IsNil() {\n continue\n }\n val = val.Elem()\n }\n prop := toAttrName(k)\n parts = append(parts, fmt.Sprintf("%s:%v", prop, val.Interface()))\n }\n if len(parts) == 0 {\n return "", false\n }\n return strings.Join(parts, ";"), true\n}\n\n// SpreadAttrs lowers a JSX intrinsic-element spread bag (#1407) to\n// an HTML attribute string. Mirrors spreadAttrs from\n// packages/client/src/runtime/spread-attrs.ts so SSR output matches\n// what CSR\'s `applyRestAttrs` writes at hydration.\n//\n// Skip rules: nil/false values, event handlers (`on[A-Z]*`),\n// `children`, `ref`.\n//\n// Key remap: className \u2192 class, htmlFor \u2192 for, SVG camelCase\n// preserved, other camelCase \u2192 kebab-case.\n//\n// `style` is routed through StyleToCss so object literals serialize\n// to a real CSS string instead of Go\'s default `map[k:v]` form.\n//\n// Booleans: true \u2192 bare attribute name, false \u2192 omitted.\n// Other scalar values are HTML-escaped via template.HTMLEscapeString.\n// Returns a `template.HTMLAttr` so html/template emits the result\n// verbatim (the function does its own escaping).\n//\n// Keys are sorted alphabetically before emission for deterministic\n// output. SSR/CSR attribute-order divergence is acceptable per the\n// rest-destructure-object-spread-in-map fixture\'s documented policy\n// \u2014 browsers honor the LAST value when a key is duplicated, so\n// pairing with static attrs (`<div class="x" {...rest}>`) is\n// last-wins regardless of order.\nfunc SpreadAttrs(bag any) template.HTMLAttr {\n if bag == nil {\n return ""\n }\n rv := reflect.ValueOf(bag)\n for rv.Kind() == reflect.Interface || rv.Kind() == reflect.Pointer {\n if rv.IsNil() {\n return ""\n }\n rv = rv.Elem()\n }\n if rv.Kind() != reflect.Map {\n return ""\n }\n keys := rv.MapKeys()\n sortedKeys := make([]string, 0, len(keys))\n for _, k := range keys {\n if k.Kind() == reflect.String {\n sortedKeys = append(sortedKeys, k.String())\n }\n }\n sort.Strings(sortedKeys)\n parts := make([]string, 0, len(sortedKeys))\n for _, key := range sortedKeys {\n // Event handlers \u2014 skip at SSR the same way\n // packages/client/src/runtime/spread-attrs.ts does at\n // hydration. The JS predicate is\n // `key.startsWith(\'on\') && key.length > 2 && key[2] === key[2].toUpperCase()`,\n // which is true for any character whose uppercase form is\n // itself: ASCII A-Z, digits, underscore, and non-letter\n // symbols. Mirror that here by skipping when key[2] is NOT\n // a lowercase ASCII letter \u2014 so `onClick`, `on_custom`, and\n // `on0` all match (#1411 review).\n if len(key) > 2 && key[0] == \'o\' && key[1] == \'n\' && !(key[2] >= \'a\' && key[2] <= \'z\') {\n continue\n }\n // `children` is a JSX construct rendered inside the element,\n // never a DOM attribute. `ref` is intentionally NOT filtered\n // here so output stays byte-equal with the JS reference\n // `spreadAttrs` in packages/client/src/runtime/spread-attrs.ts\n // (which only filters null/false, event handlers, and\n // children) \u2014 aligning Go\'s filter set diverges from JS in\n // the opposite direction. Filtering `ref` consistently across\n // both SSR runtimes is a separate concern tracked alongside\n // the JS `applyRestAttrs` vs `spreadAttrs` mismatch (#1411\n // review).\n if key == "children" {\n continue\n }\n val := rv.MapIndex(reflect.ValueOf(key))\n if !val.IsValid() {\n continue\n }\n // Unwrap interface wrappers (json.Unmarshal produces\n // interface{}-wrapped values for map[string]any).\n v := val\n for v.Kind() == reflect.Interface || v.Kind() == reflect.Pointer {\n if v.IsNil() {\n // Skip null entries.\n v = reflect.Value{}\n break\n }\n v = v.Elem()\n }\n if !v.IsValid() {\n continue\n }\n // Boolean values: true \u2192 bare attribute, false \u2192 omitted.\n if v.Kind() == reflect.Bool {\n if !v.Bool() {\n continue\n }\n parts = append(parts, toAttrName(key))\n continue\n }\n // `style` routes through StyleToCss so object literals get a\n // real CSS string. The JS side does the same.\n if key == "style" {\n css, ok := StyleToCss(v.Interface())\n if !ok {\n continue\n }\n parts = append(parts, fmt.Sprintf(`style="%s"`, template.HTMLEscapeString(css)))\n continue\n }\n // Stringify and escape. fmt.Sprint handles numbers, bools-as-\n // strings, and arbitrary stringer types the same way the JS\n // `String(value)` coercion does for the analogous cases.\n s := fmt.Sprint(v.Interface())\n parts = append(parts, fmt.Sprintf(`%s="%s"`, toAttrName(key), template.HTMLEscapeString(s)))\n }\n if len(parts) == 0 {\n return ""\n }\n return template.HTMLAttr(strings.Join(parts, " "))\n}\n\n// BfPropsAttr returns the bf-p attribute with the JSON-serialized\n// props in flat format. Output format: `bf-p=\'{"propName":value,...}\'`.\n// Only emits the attribute for root components (BfIsRoot == true);\n// child components receive props from their parent via initChild().\n//\n// Returns the marshal error so a `template.Execute` call fails\n// loudly on cycles / unsupported props rather than silently\n// dropping the bf-p attribute and breaking client-side hydration.\n// Same loud-failure policy as `JSON` \u2014 user data going through\n// `encoding/json` shouldn\'t fail invisibly.\nfunc BfPropsAttr(props interface{}) (template.HTMLAttr, error) {\n // Only root components should emit bf-p\n if !getBoolField(props, "BfIsRoot") {\n return "", nil\n }\n\n propsJSON, err := json.Marshal(props)\n if err != nil {\n return "", err\n }\n\n escaped := template.HTMLEscapeString(string(propsJSON))\n return template.HTMLAttr(`bf-p="` + escaped + `"`), nil\n}\n\n// =============================================================================\n// Arithmetic Operations\n// =============================================================================\n\n// Add returns a + b. Supports int and float64.\nfunc Add(a, b any) any {\n av, bv := toFloat64(a), toFloat64(b)\n result := av + bv\n // Return int if both inputs were int-like\n if isIntLike(a) && isIntLike(b) && result == float64(int(result)) {\n return int(result)\n }\n return result\n}\n\n// Sub returns a - b. Supports int and float64.\nfunc Sub(a, b any) any {\n av, bv := toFloat64(a), toFloat64(b)\n result := av - bv\n if isIntLike(a) && isIntLike(b) && result == float64(int(result)) {\n return int(result)\n }\n return result\n}\n\n// Mul returns a * b. Supports int and float64.\nfunc Mul(a, b any) any {\n av, bv := toFloat64(a), toFloat64(b)\n result := av * bv\n if isIntLike(a) && isIntLike(b) && result == float64(int(result)) {\n return int(result)\n }\n return result\n}\n\n// Div returns a / b. Returns float64 to match JavaScript behavior.\n// Returns 0 if b is 0 (instead of panicking).\nfunc Div(a, b any) any {\n av, bv := toFloat64(a), toFloat64(b)\n if bv == 0 {\n return 0\n }\n return av / bv\n}\n\n// Mod returns a % b (modulo). Supports int only.\nfunc Mod(a, b any) int {\n av, bv := toInt(a), toInt(b)\n if bv == 0 {\n return 0\n }\n return av % bv\n}\n\n// Neg returns -a (negation).\nfunc Neg(a any) any {\n if v, ok := a.(int); ok {\n return -v\n }\n return -toFloat64(a)\n}\n\n// =============================================================================\n// String Operations\n// =============================================================================\n\n// Lower returns the lowercase version of s.\nfunc Lower(s string) string {\n return strings.ToLower(s)\n}\n\n// Upper returns the uppercase version of s.\nfunc Upper(s string) string {\n return strings.ToUpper(s)\n}\n\n// Trim returns s with leading and trailing whitespace removed.\nfunc Trim(s string) string {\n return strings.TrimSpace(s)\n}\n\n// Contains returns true if s contains substr.\nfunc Contains(s, substr string) bool {\n return strings.Contains(s, substr)\n}\n\n// Join concatenates elements of a slice with sep. Accepts both\n// reflect.Slice (the common case \u2014 `bf_arr` and `bf_filter_truthy`\n// both return `[]any`) AND reflect.Array (fixed-size Go arrays like\n// `[3]string{...}`), mirroring JS `Array.prototype.join` which\n// doesn\'t distinguish between the two. Pre-fix this returned "" for\n// fixed-size arrays passed through template data (Copilot review on\n// #1445).\nfunc Join(items any, sep string) string {\n v := reflect.ValueOf(items)\n if v.Kind() != reflect.Slice && v.Kind() != reflect.Array {\n return ""\n }\n\n parts := make([]string, v.Len())\n for i := 0; i < v.Len(); i++ {\n parts[i] = toString(v.Index(i).Interface())\n }\n return strings.Join(parts, sep)\n}\n\n// String returns the string form of v. Mirrors JS `String(v)` for\n// non-nil values via `fmt.Sprintf("%v", ...)`. Diverges from JS on\n// nil: JS `String(null)` is "null", but the template path renders\n// `nil` as the empty string here so an unset prop doesn\'t surface\n// as a literal "null"/"undefined" in user-facing HTML. Document the\n// divergence explicitly so callers don\'t rely on JS-exact parity.\nfunc String(v any) string {\n if v == nil {\n return ""\n }\n return fmt.Sprintf("%v", v)\n}\n\n// JSON returns the JSON encoding of v as a string. Mirrors\n// JS `JSON.stringify(v)` for the V1 single-arg shape (no `replacer`\n// or `space`). Object key order is determined by Go\'s `encoding/json`\n// (alphabetical for maps, declaration order for structs) \u2014 the\n// #1187 contract requires value-compat, not order-compat.\n//\n// Top-level NaN / \xB1Inf are pre-handled to match JS \u2014 JS\'s\n// `JSON.stringify(NaN)` and `JSON.stringify(Infinity)` both produce\n// `"null"`, but Go\'s `encoding/json` rejects them with\n// `UnsupportedValueError`. Without this carve-out the common\n// composition `JSON.stringify(Number("garbage"))` would error\n// instead of emitting `"null"` like JS does. Nested NaN/Inf inside\n// a struct/map still surfaces an error \u2014 covering that needs a\n// custom marshaller; out of V1 scope.\n//\n// Returns the marshal error so a `template.Execute` call fails\n// loudly on cycles / unsupported values rather than silently\n// producing `""` and reintroducing the SSR data-loss class\n// #1187 was filed against. Go\'s text/template treats a non-nil\n// error return from a func as an execution failure.\nfunc JSON(v any) (string, error) {\n if f, ok := v.(float64); ok && (math.IsNaN(f) || math.IsInf(f, 0)) {\n return "null", nil\n }\n b, err := json.Marshal(v)\n if err != nil {\n return "", err\n }\n return string(b), nil\n}\n\n// Number coerces v to a float64. Mirrors JS `Number(v)` semantics:\n// numeric / boolean inputs convert as expected; non-numeric strings\n// and other unsupported shapes return `NaN` (matching JS rather\n// than silently substituting 0, which would mis-shape downstream\n// arithmetic and template-side comparisons). Templates that need\n// a deterministic fallback should compose with the user-side\n// default (e.g. `Number(props.x ?? 0)` in JSX).\nfunc Number(v any) float64 {\n if v == nil {\n return math.NaN()\n }\n switch x := v.(type) {\n case float64:\n return x\n case float32:\n return float64(x)\n case int:\n return float64(x)\n case int32:\n return float64(x)\n case int64:\n return float64(x)\n case bool:\n if x {\n return 1\n }\n return 0\n case string:\n f, err := strconv.ParseFloat(x, 64)\n if err != nil {\n return math.NaN()\n }\n return f\n }\n return math.NaN()\n}\n\n// Floor returns the largest integer \u2264 v as a float64. Mirrors JS\n// `Math.floor`. The return type stays float64 so chained primitives\n// (`bf_floor` then `bf_string`) line up with JS\'s number type.\nfunc Floor(v any) float64 {\n return math.Floor(Number(v))\n}\n\n// Ceil returns the smallest integer \u2265 v as a float64. Mirrors JS\n// `Math.ceil`.\nfunc Ceil(v any) float64 {\n return math.Ceil(Number(v))\n}\n\n// Round returns v rounded to the nearest integer as a float64.\n// Mirrors JS `Math.round` \u2014 half-away-from-zero (Go\'s `math.Round`\n// matches; JS rounds half toward +Infinity which differs at .5\n// negatives; we accept that minor divergence since the conformance\n// contract is value-compat for the common positive case).\nfunc Round(v any) float64 {\n return math.Round(Number(v))\n}\n\n// =============================================================================\n// Array/Slice Operations\n// =============================================================================\n\n// Len returns the length of a slice, array, map, string, or channel.\n// Returns 0 for nil or unsupported types.\nfunc Len(v any) int {\n if v == nil {\n return 0\n }\n rv := reflect.ValueOf(v)\n switch rv.Kind() {\n case reflect.Slice, reflect.Array, reflect.Map, reflect.String, reflect.Chan:\n return rv.Len()\n default:\n return 0\n }\n}\n\n// At returns the element at index i from a slice.\n// Supports negative indices (e.g., -1 for last element).\n// Returns nil if index is out of bounds.\nfunc At(items any, index int) any {\n v := reflect.ValueOf(items)\n if v.Kind() != reflect.Slice && v.Kind() != reflect.Array {\n return nil\n }\n\n length := v.Len()\n if length == 0 {\n return nil\n }\n\n // Handle negative indices\n if index < 0 {\n index = length + index\n }\n\n if index < 0 || index >= length {\n return nil\n }\n\n return v.Index(index).Interface()\n}\n\n// Includes returns true if items contains elem. Lowers both\n// `Array.prototype.includes` and `String.prototype.includes` \u2014\n// the adapter can\'t disambiguate the receiver at compile time,\n// so this helper dispatches at runtime on `reflect.Kind()`:\n//\n// - slice/array receiver: DeepEqual element search\n// - string receiver: strings.Contains substring search\n//\n// Anything else returns false (matches the JS semantic where\n// `.includes` is only defined on Array / TypedArray / String).\nfunc Includes(recv any, elem any) bool {\n v := reflect.ValueOf(recv)\n if v.Kind() == reflect.String {\n // JS `String.prototype.includes` accepts only string args;\n // non-string `elem` would TypeError in real JS but our\n // callers have lowered through `convertExpressionToGo`\n // where the arg type is whatever the template binds. Stringify\n // via fmt to keep the helper total.\n needle, ok := elem.(string)\n if !ok {\n needle = fmt.Sprintf("%v", elem)\n }\n return strings.Contains(v.String(), needle)\n }\n if v.Kind() != reflect.Slice && v.Kind() != reflect.Array {\n return false\n }\n for i := 0; i < v.Len(); i++ {\n if reflect.DeepEqual(v.Index(i).Interface(), elem) {\n return true\n }\n }\n return false\n}\n\n// IndexOf returns the 0-based position of the first item that\n// DeepEquals `elem`, or -1 if not found. Lowers\n// `Array.prototype.indexOf(x)` (#1448 Tier A). The existing\n// `FindIndex` helper does struct-field equality (used by the\n// higher-order `.find` lowering); this one does value equality\n// against scalar / struct items so callers don\'t have to compose\n// a synthetic predicate.\n//\n// Non-array / non-slice receivers return -1 (matches the JS\n// semantic that `.indexOf` is only defined on Array / TypedArray).\nfunc IndexOf(items any, elem any) int {\n v := reflect.ValueOf(items)\n if v.Kind() != reflect.Slice && v.Kind() != reflect.Array {\n return -1\n }\n for i := 0; i < v.Len(); i++ {\n if reflect.DeepEqual(v.Index(i).Interface(), elem) {\n return i\n }\n }\n return -1\n}\n\n// LastIndexOf returns the 0-based position of the last item that\n// DeepEquals `elem`, or -1 if not found. Mirrors\n// `Array.prototype.lastIndexOf(x)`. The reverse traversal is the\n// only behavioural difference vs `IndexOf` \u2014 disambiguating a\n// duplicated value\'s first vs last position is the canonical\n// reason a JS author reaches for `lastIndexOf`.\nfunc LastIndexOf(items any, elem any) int {\n v := reflect.ValueOf(items)\n if v.Kind() != reflect.Slice && v.Kind() != reflect.Array {\n return -1\n }\n for i := v.Len() - 1; i >= 0; i-- {\n if reflect.DeepEqual(v.Index(i).Interface(), elem) {\n return i\n }\n }\n return -1\n}\n\n// Concat merges two arrays (or slices) into a single `[]any`,\n// preserving order: receiver elements first, then `other`\'s.\n// Lowers `Array.prototype.concat(other)` (#1448 Tier A). Non-array\n// operands collapse to an empty source \u2014 matches the JS semantic\n// where `.concat` on a non-Array reads it as a single element only\n// if its `Symbol.isConcatSpreadable` is true; the template-language\n// path doesn\'t have user objects with that flag, so treating\n// non-arrays as empty is the conservative lowering. Variadic\n// `.concat(a, b, c)` is out of scope here (parser gates to a single\n// arg); the helper itself stays binary so a future variadic IR can\n// fold via repeated calls without changing this signature.\nfunc Concat(a, b any) []any {\n flatten := func(v reflect.Value) []any {\n if !v.IsValid() {\n return nil\n }\n if v.Kind() != reflect.Slice && v.Kind() != reflect.Array {\n return nil\n }\n out := make([]any, v.Len())\n for i := 0; i < v.Len(); i++ {\n out[i] = v.Index(i).Interface()\n }\n return out\n }\n left := flatten(reflect.ValueOf(a))\n right := flatten(reflect.ValueOf(b))\n return append(left, right...)\n}\n\n// Slice carves out a sub-range from `items`. Lowers\n// `Array.prototype.slice(start, end?)` (#1448 Tier A). The variadic\n// `end` arg lets Go template\'s call dispatcher pass either 2 or 3\n// arguments; an absent end means "to length".\n//\n// JS-compat clamping:\n// - start < 0 \u2192 length + start (e.g. -1 = last index)\n// - end < 0 \u2192 length + end\n// - start < 0 after clamp \u2192 0\n// - end > length \u2192 length\n// - start >= end \u2192 empty slice (no panic)\n//\n// Non-array receivers return an empty `[]any`.\nfunc Slice(items any, start int, end ...int) []any {\n v := reflect.ValueOf(items)\n if v.Kind() != reflect.Slice && v.Kind() != reflect.Array {\n return []any{}\n }\n length := v.Len()\n\n // Normalise start (negative = from end).\n if start < 0 {\n start = length + start\n }\n if start < 0 {\n start = 0\n }\n if start > length {\n start = length\n }\n\n // Normalise end (optional; absent = length).\n stop := length\n if len(end) > 0 {\n stop = end[0]\n if stop < 0 {\n stop = length + stop\n }\n if stop < 0 {\n stop = 0\n }\n if stop > length {\n stop = length\n }\n }\n\n if start >= stop {\n return []any{}\n }\n\n out := make([]any, 0, stop-start)\n for i := start; i < stop; i++ {\n out = append(out, v.Index(i).Interface())\n }\n return out\n}\n\n// Reverse returns a new slice with `items`\'s elements in reverse\n// order. Lowers both `Array.prototype.reverse()` and\n// `Array.prototype.toReversed()` (#1448 Tier A) \u2014 SSR templates\n// render a snapshot, so JS\'s mutate-receiver vs return-new-array\n// distinction has no template-level meaning, and the safer\n// non-mutating shape is used uniformly.\n//\n// Non-array receivers return an empty `[]any`.\nfunc Reverse(items any) []any {\n v := reflect.ValueOf(items)\n if v.Kind() != reflect.Slice && v.Kind() != reflect.Array {\n return []any{}\n }\n length := v.Len()\n out := make([]any, length)\n for i := 0; i < length; i++ {\n out[length-1-i] = v.Index(i).Interface()\n }\n return out\n}\n\n// First returns the first element of a slice, or nil if empty.\nfunc First(items any) any {\n return At(items, 0)\n}\n\n// Last returns the last element of a slice, or nil if empty.\nfunc Last(items any) any {\n return At(items, -1)\n}\n\n// Arr builds an []any from variadic args. Used to lower JS array\n// literals like `[a, b]` for the registry Slot\'s\n// `[className, childClass].filter(Boolean).join(\' \')` shape (#1443) \u2014\n// Go templates have no array-literal syntax, so the codegen routes\n// array-literal IR nodes through this helper.\nfunc Arr(items ...any) []any {\n return items\n}\n\n// FilterTruthy returns a new slice containing only truthy items.\n// Mirrors `arr.filter(Boolean)` semantics: drop nil, false, 0, "" \u2014 the\n// same falsy set JavaScript\'s `Boolean(x)` recognises. Used to lower\n// the registry Slot\'s class-merge pattern (#1443); generalising to\n// arbitrary callable predicates would need the callee-resolution path\n// blocked by #1389, so this stays Boolean-specific.\nfunc FilterTruthy(items any) []any {\n v := reflect.ValueOf(items)\n if !v.IsValid() || (v.Kind() != reflect.Slice && v.Kind() != reflect.Array) {\n return nil\n }\n result := make([]any, 0, v.Len())\n for i := 0; i < v.Len(); i++ {\n raw := v.Index(i).Interface()\n if isTruthy(raw) {\n result = append(result, raw)\n }\n }\n return result\n}\n\n// isTruthy mirrors JavaScript\'s `Boolean(x)` for the value shapes the\n// template path actually receives \u2014 nil / false / 0 / "" are falsy.\n// Other shapes (non-empty maps, slices, structs, true) are truthy, in\n// line with JS\'s "objects are truthy" rule.\nfunc isTruthy(v any) bool {\n if v == nil {\n return false\n }\n switch x := v.(type) {\n case bool:\n return x\n case string:\n return x != ""\n case int:\n return x != 0\n case int8, int16, int32, int64:\n return reflect.ValueOf(v).Int() != 0\n case uint, uint8, uint16, uint32, uint64:\n return reflect.ValueOf(v).Uint() != 0\n case float32:\n // JS `Boolean(NaN)` is false regardless of float width \u2014 the\n // float64 arm below was the only one checking IsNaN, which\n // diverged from JS for `float32` NaN inputs (Copilot review on\n // #1445). Widening to float64 for the IsNaN check keeps the\n // two branches in lock-step.\n return x != 0 && !math.IsNaN(float64(x))\n case float64:\n return x != 0 && !math.IsNaN(x)\n }\n return true\n}\n\n// =============================================================================\n// Higher-order Array Methods\n// =============================================================================\n\n// Every returns true if all items have the specified field set to true.\n// Mirrors JavaScript\'s Array.prototype.every(item => item.field).\nfunc Every(items any, field string) bool {\n v := reflect.ValueOf(items)\n if v.Kind() != reflect.Slice && v.Kind() != reflect.Array {\n return false\n }\n\n capitalizedField := capitalize(field)\n for i := 0; i < v.Len(); i++ {\n item := v.Index(i)\n if item.Kind() == reflect.Interface {\n item = item.Elem()\n }\n if item.Kind() == reflect.Ptr {\n item = item.Elem()\n }\n if item.Kind() != reflect.Struct {\n continue\n }\n\n fieldVal := item.FieldByName(capitalizedField)\n if !fieldVal.IsValid() {\n return false\n }\n if fieldVal.Kind() == reflect.Bool && !fieldVal.Bool() {\n return false\n }\n }\n return true\n}\n\n// Some returns true if at least one item has the specified field set to true.\n// Mirrors JavaScript\'s Array.prototype.some(item => item.field).\nfunc Some(items any, field string) bool {\n v := reflect.ValueOf(items)\n if v.Kind() != reflect.Slice && v.Kind() != reflect.Array {\n return false\n }\n\n capitalizedField := capitalize(field)\n for i := 0; i < v.Len(); i++ {\n item := v.Index(i)\n if item.Kind() == reflect.Interface {\n item = item.Elem()\n }\n if item.Kind() == reflect.Ptr {\n item = item.Elem()\n }\n if item.Kind() != reflect.Struct {\n continue\n }\n\n fieldVal := item.FieldByName(capitalizedField)\n if fieldVal.IsValid() && fieldVal.Kind() == reflect.Bool && fieldVal.Bool() {\n return true\n }\n }\n return false\n}\n\n// Filter returns items where item.field == value.\n// Mirrors JavaScript\'s Array.prototype.filter(item => item.field === value).\n// Returns []any to allow chaining with other bf_* functions.\nfunc Filter(items any, field string, value any) []any {\n v := reflect.ValueOf(items)\n if v.Kind() != reflect.Slice && v.Kind() != reflect.Array {\n return nil\n }\n\n capitalizedField := capitalize(field)\n var result []any\n\n for i := 0; i < v.Len(); i++ {\n item := v.Index(i)\n if item.Kind() == reflect.Interface {\n item = item.Elem()\n }\n if item.Kind() == reflect.Ptr {\n item = item.Elem()\n }\n if item.Kind() != reflect.Struct {\n continue\n }\n\n fieldVal := item.FieldByName(capitalizedField)\n if !fieldVal.IsValid() {\n continue\n }\n\n // Compare field value with target value\n if reflect.DeepEqual(fieldVal.Interface(), value) {\n result = append(result, v.Index(i).Interface())\n }\n }\n return result\n}\n\n// Find returns the first item where item.field == value, or nil if not found.\n// Mirrors JavaScript\'s Array.prototype.find(item => item.field === value).\nfunc Find(items any, field string, value any) any {\n v := reflect.ValueOf(items)\n if v.Kind() != reflect.Slice && v.Kind() != reflect.Array {\n return nil\n }\n\n capitalizedField := capitalize(field)\n for i := 0; i < v.Len(); i++ {\n item := v.Index(i)\n if item.Kind() == reflect.Interface {\n item = item.Elem()\n }\n if item.Kind() == reflect.Ptr {\n item = item.Elem()\n }\n if item.Kind() != reflect.Struct {\n continue\n }\n\n fieldVal := item.FieldByName(capitalizedField)\n if !fieldVal.IsValid() {\n continue\n }\n\n if reflect.DeepEqual(fieldVal.Interface(), value) {\n return v.Index(i).Interface()\n }\n }\n return nil\n}\n\n// FindIndex returns the index of the first item where item.field == value, or -1.\n// Mirrors JavaScript\'s Array.prototype.findIndex(item => item.field === value).\nfunc FindIndex(items any, field string, value any) int {\n v := reflect.ValueOf(items)\n if v.Kind() != reflect.Slice && v.Kind() != reflect.Array {\n return -1\n }\n\n capitalizedField := capitalize(field)\n for i := 0; i < v.Len(); i++ {\n item := v.Index(i)\n if item.Kind() == reflect.Interface {\n item = item.Elem()\n }\n if item.Kind() == reflect.Ptr {\n item = item.Elem()\n }\n if item.Kind() != reflect.Struct {\n continue\n }\n\n fieldVal := item.FieldByName(capitalizedField)\n if !fieldVal.IsValid() {\n continue\n }\n\n if reflect.DeepEqual(fieldVal.Interface(), value) {\n return i\n }\n }\n return -1\n}\n\n// FindLast returns the last item where item.field == value, or nil if not found.\n// Mirrors JavaScript\'s Array.prototype.findLast(item => item.field === value).\nfunc FindLast(items any, field string, value any) any {\n v := reflect.ValueOf(items)\n if v.Kind() != reflect.Slice && v.Kind() != reflect.Array {\n return nil\n }\n\n capitalizedField := capitalize(field)\n for i := v.Len() - 1; i >= 0; i-- {\n item := v.Index(i)\n if item.Kind() == reflect.Interface {\n if item.IsNil() {\n continue\n }\n item = item.Elem()\n }\n if item.Kind() == reflect.Ptr {\n if item.IsNil() {\n continue\n }\n item = item.Elem()\n }\n if item.Kind() != reflect.Struct {\n continue\n }\n\n fieldVal := item.FieldByName(capitalizedField)\n if !fieldVal.IsValid() {\n continue\n }\n\n if reflect.DeepEqual(fieldVal.Interface(), value) {\n return v.Index(i).Interface()\n }\n }\n return nil\n}\n\n// FindLastIndex returns the index of the last item where item.field == value, or -1.\n// Mirrors JavaScript\'s Array.prototype.findLastIndex(item => item.field === value).\nfunc FindLastIndex(items any, field string, value any) int {\n v := reflect.ValueOf(items)\n if v.Kind() != reflect.Slice && v.Kind() != reflect.Array {\n return -1\n }\n\n capitalizedField := capitalize(field)\n for i := v.Len() - 1; i >= 0; i-- {\n item := v.Index(i)\n if item.Kind() == reflect.Interface {\n if item.IsNil() {\n continue\n }\n item = item.Elem()\n }\n if item.Kind() == reflect.Ptr {\n if item.IsNil() {\n continue\n }\n item = item.Elem()\n }\n if item.Kind() != reflect.Struct {\n continue\n }\n\n fieldVal := item.FieldByName(capitalizedField)\n if !fieldVal.IsValid() {\n continue\n }\n\n if reflect.DeepEqual(fieldVal.Interface(), value) {\n return i\n }\n }\n return -1\n}\n\n// 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 exactly 5 args):\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"\n// direction: "asc" | "desc"\n//\n// The 4 string operands cover the accepted comparator catalogue:\n// `(a,b) => a.f - b.f`, `(a,b) => a - b`, and\n// `(a,b) => a[.f].localeCompare(b[.f])`, each with a reversed\n// counterpart for `desc`. Anything outside the catalogue refuses at\n// compile time (BF101 from the JSX compiler) and never reaches this\n// helper.\n//\n// A future `nulls => "first" | "last"` knob can land as a 6th arg\n// without rewriting the existing call sites \u2014 the keyKind / keyName\n// split already gives the helper everything it needs to project\n// each item\'s sort key before comparing.\nfunc Sort(items any, keyKind string, keyName string, compareType string, direction 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 desc := direction == "desc"\n sort.SliceStable(result, func(i, j int) bool {\n ki := projectSortKey(result[i], keyKind, keyName)\n kj := projectSortKey(result[j], keyKind, keyName)\n if compareType == "string" {\n // `toString` maps nil / unknown types to "" \u2014 matches\n // the documented `bf->string(undef) === ""` divergence\n // from JS and the Perl `bf->sort` helper\'s `// \'\'`\n // undef-coalesce. Using `fmt.Sprint` here would sort\n // missing fields against the literal string "<nil>".\n si := toString(ki)\n sj := toString(kj)\n if desc {\n return si > sj\n }\n return si < sj\n }\n // numeric\n ni := toFloat64(ki)\n nj := toFloat64(kj)\n if desc {\n return ni > nj\n }\n return ni < nj\n })\n\n return result\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';
22372
+ bfGoSource = '// Package bf provides runtime helper functions for BarefootJS Go templates.\n// These functions mirror JavaScript behavior for consistent SSR output.\npackage bf\n\nimport (\n "bytes"\n "encoding/json"\n "fmt"\n "html/template"\n "math"\n "os"\n "reflect"\n "sort"\n "strconv"\n "strings"\n)\n\n// FuncMap returns a template.FuncMap with all BarefootJS helper functions.\n// Usage:\n//\n// tmpl := template.New("").Funcs(bf.FuncMap())\nfunc FuncMap() template.FuncMap {\n return template.FuncMap{\n // Arithmetic\n "bf_add": Add,\n "bf_sub": Sub,\n "bf_mul": Mul,\n "bf_div": Div,\n "bf_mod": Mod,\n "bf_neg": Neg,\n\n // String\n "bf_lower": Lower,\n "bf_upper": Upper,\n "bf_trim": Trim,\n "bf_contains": Contains,\n "bf_join": Join,\n "bf_string": String,\n\n // JSON / numeric primitives \u2014 JS-compat callees registered on\n // the Go adapter\'s `templatePrimitives` map (#1188).\n "bf_json": JSON,\n "bf_number": Number,\n "bf_floor": Floor,\n "bf_ceil": Ceil,\n "bf_round": Round,\n\n // Array/Slice\n "bf_len": Len,\n "bf_at": At,\n "bf_includes": Includes,\n "bf_index_of": IndexOf,\n "bf_last_index_of": LastIndexOf,\n "bf_concat": Concat,\n "bf_slice": Slice,\n "bf_reverse": Reverse,\n "bf_first": First,\n "bf_last": Last,\n "bf_arr": Arr,\n "bf_filter_truthy": FilterTruthy,\n\n // Higher-order Array Methods\n "bf_every": Every,\n "bf_some": Some,\n "bf_filter": Filter,\n "bf_find": Find,\n "bf_find_index": FindIndex,\n "bf_find_last": FindLast,\n "bf_find_last_index": FindLastIndex,\n "bf_sort": Sort,\n\n // Comment marker (for hydration)\n "bfComment": Comment,\n "bfTextStart": TextStart,\n "bfTextEnd": TextEnd,\n\n // Script collection\n "bfScripts": BfScripts,\n\n // Scope attribute value (#1249: bare scope id, no `~` prefix)\n "bfScopeAttr": ScopeAttr,\n\n // Slot-identity markers (#1249): bf-h, bf-m, bf-r\n "bfHydrationAttrs": HydrationAttrs,\n\n // Child component marker (kept for backward compatibility)\n "bfIsChild": IsChild,\n\n // Props attribute for hydration\n "bfPropsAttr": BfPropsAttr,\n\n // Portal HTML rendering (parses and executes template string)\n "bfPortalHTML": PortalHTML,\n\n // Scope comment for fragment roots\n "bfScopeComment": ScopeComment,\n\n // JSX intrinsic-element spread lowering (#1407)\n "bf_spread_attrs": SpreadAttrs,\n }\n}\n\n// ScopeAttr returns the bare bf-s scope id (#1249).\nfunc ScopeAttr(props interface{}) string {\n return getStringField(props, "ScopeID")\n}\n\n// HydrationAttrs emits `bf-h="<host>" bf-m="<slot>" bf-r=""` conditionally.\n// See spec/compiler.md "Slot identity".\nfunc HydrationAttrs(props interface{}) template.HTMLAttr {\n parts := []string{}\n if host := getStringField(props, "BfParent"); host != "" {\n parts = append(parts, fmt.Sprintf(`bf-h="%s"`, template.HTMLEscapeString(host)))\n }\n if mount := getStringField(props, "BfMount"); mount != "" {\n parts = append(parts, fmt.Sprintf(`bf-m="%s"`, template.HTMLEscapeString(mount)))\n }\n if !getBoolField(props, "BfIsChild") {\n parts = append(parts, `bf-r=""`)\n }\n if len(parts) == 0 {\n return ""\n }\n return template.HTMLAttr(strings.Join(parts, " "))\n}\n\n// IsChild is a deprecated no-op stub. Child status is signalled by bf-h\n// presence (#1249); use HydrationAttrs instead.\nfunc IsChild(props interface{}) template.HTMLAttr {\n return ""\n}\n\n// svgCamelCaseAttrs mirrors SVG_CAMEL_CASE_ATTRS from\n// packages/client/src/runtime/spread-attrs.ts. SVG XML attribute\n// names are case-sensitive; the default camelCase \u2192 kebab-case\n// rewrite must NOT apply to these or the SVG stops rendering\n// (#1407). Coordinates with the compile-time SVG_CAMEL_TO_KEBAB\n// table in packages/jsx/src/ir-to-client-js/utils.ts: presentation\n// attrs (clipPath, strokeWidth, \u2026) live there and must NOT appear\n// here, or the same JSX prop would lower to clip-path via the\n// explicit-attr path and stay clipPath via the spread path.\nvar svgCamelCaseAttrs = map[string]struct{}{\n "allowReorder": {}, "attributeName": {}, "attributeType": {}, "autoReverse": {},\n "baseFrequency": {}, "baseProfile": {}, "calcMode": {}, "clipPathUnits": {},\n "contentScriptType": {}, "contentStyleType": {}, "diffuseConstant": {}, "edgeMode": {},\n "externalResourcesRequired": {}, "filterRes": {}, "filterUnits": {}, "glyphRef": {},\n "gradientTransform": {}, "gradientUnits": {}, "kernelMatrix": {}, "kernelUnitLength": {},\n "keyPoints": {}, "keySplines": {}, "keyTimes": {}, "lengthAdjust": {}, "limitingConeAngle": {},\n "markerHeight": {}, "markerUnits": {}, "markerWidth": {}, "maskContentUnits": {},\n "maskUnits": {}, "numOctaves": {}, "pathLength": {}, "patternContentUnits": {},\n "patternTransform": {}, "patternUnits": {}, "pointsAtX": {}, "pointsAtY": {}, "pointsAtZ": {},\n "preserveAlpha": {}, "preserveAspectRatio": {}, "primitiveUnits": {}, "refX": {}, "refY": {},\n "repeatCount": {}, "repeatDur": {}, "requiredExtensions": {}, "requiredFeatures": {},\n "specularConstant": {}, "specularExponent": {}, "spreadMethod": {}, "startOffset": {},\n "stdDeviation": {}, "stitchTiles": {}, "surfaceScale": {}, "systemLanguage": {},\n "tableValues": {}, "targetX": {}, "targetY": {}, "textLength": {}, "viewBox": {}, "viewTarget": {},\n "xChannelSelector": {}, "yChannelSelector": {}, "zoomAndPan": {},\n}\n\n// toAttrName mirrors the JSX\u2192HTML attribute-name rewrite from\n// packages/client/src/runtime/spread-attrs.ts. className \u2192 class,\n// htmlFor \u2192 for, SVG camelCase attrs preserved, other camelCase\n// keys lowered to kebab-case.\nfunc toAttrName(key string) string {\n if key == "className" {\n return "class"\n }\n if key == "htmlFor" {\n return "for"\n }\n if _, ok := svgCamelCaseAttrs[key]; ok {\n return key\n }\n // camelCase \u2192 kebab-case: mirror the JS reference exactly\n // (`key.replace(/([A-Z])/g, \'-$1\').toLowerCase()`). The JS shape\n // produces a leading `-` for an initial uppercase letter\n // (`XData` \u2192 `-x-data`); both this Go path and the matching JS\n // runtime are wrong-by-construction for that case (the resulting\n // HTML attribute name is invalid), but keeping them byte-equal\n // avoids silent SSR/CSR divergence (#1411 review).\n var b strings.Builder\n for _, r := range key {\n if r >= \'A\' && r <= \'Z\' {\n b.WriteByte(\'-\')\n b.WriteRune(r + 32)\n } else {\n b.WriteRune(r)\n }\n }\n return b.String()\n}\n\n// StyleToCss mirrors styleToCss from\n// packages/client/src/runtime/style.ts. Accepts a string passthrough,\n// or a map (JSON-deserialized object) whose camelCase keys are\n// lowered to kebab-case and joined with `;`. Returns ("", false) for\n// nullish/empty input so callers can omit the attribute entirely.\nfunc StyleToCss(v any) (string, bool) {\n if v == nil {\n return "", false\n }\n rv := reflect.ValueOf(v)\n for rv.Kind() == reflect.Interface || rv.Kind() == reflect.Pointer {\n if rv.IsNil() {\n return "", false\n }\n rv = rv.Elem()\n }\n if rv.Kind() != reflect.Map {\n // Non-object: stringify and return as-is, matching the JS\n // `typeof value !== \'object\'` branch.\n s := fmt.Sprint(v)\n if s == "" {\n return "", false\n }\n return s, true\n }\n keys := rv.MapKeys()\n sorted := make([]string, 0, len(keys))\n for _, k := range keys {\n if k.Kind() == reflect.String {\n sorted = append(sorted, k.String())\n }\n }\n sort.Strings(sorted)\n parts := make([]string, 0, len(sorted))\n for _, k := range sorted {\n val := rv.MapIndex(reflect.ValueOf(k))\n // Skip nil entries (matches the JS `if (v == null) continue`).\n if !val.IsValid() {\n continue\n }\n if val.Kind() == reflect.Interface || val.Kind() == reflect.Pointer {\n if val.IsNil() {\n continue\n }\n val = val.Elem()\n }\n prop := toAttrName(k)\n parts = append(parts, fmt.Sprintf("%s:%v", prop, val.Interface()))\n }\n if len(parts) == 0 {\n return "", false\n }\n return strings.Join(parts, ";"), true\n}\n\n// SpreadAttrs lowers a JSX intrinsic-element spread bag (#1407) to\n// an HTML attribute string. Mirrors spreadAttrs from\n// packages/client/src/runtime/spread-attrs.ts so SSR output matches\n// what CSR\'s `applyRestAttrs` writes at hydration.\n//\n// Skip rules: nil/false values, event handlers (`on[A-Z]*`),\n// `children`, `ref`.\n//\n// Key remap: className \u2192 class, htmlFor \u2192 for, SVG camelCase\n// preserved, other camelCase \u2192 kebab-case.\n//\n// `style` is routed through StyleToCss so object literals serialize\n// to a real CSS string instead of Go\'s default `map[k:v]` form.\n//\n// Booleans: true \u2192 bare attribute name, false \u2192 omitted.\n// Other scalar values are HTML-escaped via template.HTMLEscapeString.\n// Returns a `template.HTMLAttr` so html/template emits the result\n// verbatim (the function does its own escaping).\n//\n// Keys are sorted alphabetically before emission for deterministic\n// output. SSR/CSR attribute-order divergence is acceptable per the\n// rest-destructure-object-spread-in-map fixture\'s documented policy\n// \u2014 browsers honor the LAST value when a key is duplicated, so\n// pairing with static attrs (`<div class="x" {...rest}>`) is\n// last-wins regardless of order.\nfunc SpreadAttrs(bag any) template.HTMLAttr {\n if bag == nil {\n return ""\n }\n rv := reflect.ValueOf(bag)\n for rv.Kind() == reflect.Interface || rv.Kind() == reflect.Pointer {\n if rv.IsNil() {\n return ""\n }\n rv = rv.Elem()\n }\n if rv.Kind() != reflect.Map {\n return ""\n }\n keys := rv.MapKeys()\n sortedKeys := make([]string, 0, len(keys))\n for _, k := range keys {\n if k.Kind() == reflect.String {\n sortedKeys = append(sortedKeys, k.String())\n }\n }\n sort.Strings(sortedKeys)\n parts := make([]string, 0, len(sortedKeys))\n for _, key := range sortedKeys {\n // Event handlers \u2014 skip at SSR the same way\n // packages/client/src/runtime/spread-attrs.ts does at\n // hydration. The JS predicate is\n // `key.startsWith(\'on\') && key.length > 2 && key[2] === key[2].toUpperCase()`,\n // which is true for any character whose uppercase form is\n // itself: ASCII A-Z, digits, underscore, and non-letter\n // symbols. Mirror that here by skipping when key[2] is NOT\n // a lowercase ASCII letter \u2014 so `onClick`, `on_custom`, and\n // `on0` all match (#1411 review).\n if len(key) > 2 && key[0] == \'o\' && key[1] == \'n\' && !(key[2] >= \'a\' && key[2] <= \'z\') {\n continue\n }\n // `children` is a JSX construct rendered inside the element,\n // never a DOM attribute. `ref` is intentionally NOT filtered\n // here so output stays byte-equal with the JS reference\n // `spreadAttrs` in packages/client/src/runtime/spread-attrs.ts\n // (which only filters null/false, event handlers, and\n // children) \u2014 aligning Go\'s filter set diverges from JS in\n // the opposite direction. Filtering `ref` consistently across\n // both SSR runtimes is a separate concern tracked alongside\n // the JS `applyRestAttrs` vs `spreadAttrs` mismatch (#1411\n // review).\n if key == "children" {\n continue\n }\n val := rv.MapIndex(reflect.ValueOf(key))\n if !val.IsValid() {\n continue\n }\n // Unwrap interface wrappers (json.Unmarshal produces\n // interface{}-wrapped values for map[string]any).\n v := val\n for v.Kind() == reflect.Interface || v.Kind() == reflect.Pointer {\n if v.IsNil() {\n // Skip null entries.\n v = reflect.Value{}\n break\n }\n v = v.Elem()\n }\n if !v.IsValid() {\n continue\n }\n // Boolean values: true \u2192 bare attribute, false \u2192 omitted.\n if v.Kind() == reflect.Bool {\n if !v.Bool() {\n continue\n }\n parts = append(parts, toAttrName(key))\n continue\n }\n // `style` routes through StyleToCss so object literals get a\n // real CSS string. The JS side does the same.\n if key == "style" {\n css, ok := StyleToCss(v.Interface())\n if !ok {\n continue\n }\n parts = append(parts, fmt.Sprintf(`style="%s"`, template.HTMLEscapeString(css)))\n continue\n }\n // Stringify and escape. fmt.Sprint handles numbers, bools-as-\n // strings, and arbitrary stringer types the same way the JS\n // `String(value)` coercion does for the analogous cases.\n s := fmt.Sprint(v.Interface())\n parts = append(parts, fmt.Sprintf(`%s="%s"`, toAttrName(key), template.HTMLEscapeString(s)))\n }\n if len(parts) == 0 {\n return ""\n }\n return template.HTMLAttr(strings.Join(parts, " "))\n}\n\n// BfPropsAttr returns the bf-p attribute with the JSON-serialized\n// props in flat format. Output format: `bf-p=\'{"propName":value,...}\'`.\n// Only emits the attribute for root components (BfIsRoot == true);\n// child components receive props from their parent via initChild().\n//\n// Returns the marshal error so a `template.Execute` call fails\n// loudly on cycles / unsupported props rather than silently\n// dropping the bf-p attribute and breaking client-side hydration.\n// Same loud-failure policy as `JSON` \u2014 user data going through\n// `encoding/json` shouldn\'t fail invisibly.\nfunc BfPropsAttr(props interface{}) (template.HTMLAttr, error) {\n // Only root components should emit bf-p\n if !getBoolField(props, "BfIsRoot") {\n return "", nil\n }\n\n propsJSON, err := json.Marshal(props)\n if err != nil {\n return "", err\n }\n\n escaped := template.HTMLEscapeString(string(propsJSON))\n return template.HTMLAttr(`bf-p="` + escaped + `"`), nil\n}\n\n// =============================================================================\n// Arithmetic Operations\n// =============================================================================\n\n// Add returns a + b. Supports int and float64.\nfunc Add(a, b any) any {\n av, bv := toFloat64(a), toFloat64(b)\n result := av + bv\n // Return int if both inputs were int-like\n if isIntLike(a) && isIntLike(b) && result == float64(int(result)) {\n return int(result)\n }\n return result\n}\n\n// Sub returns a - b. Supports int and float64.\nfunc Sub(a, b any) any {\n av, bv := toFloat64(a), toFloat64(b)\n result := av - bv\n if isIntLike(a) && isIntLike(b) && result == float64(int(result)) {\n return int(result)\n }\n return result\n}\n\n// Mul returns a * b. Supports int and float64.\nfunc Mul(a, b any) any {\n av, bv := toFloat64(a), toFloat64(b)\n result := av * bv\n if isIntLike(a) && isIntLike(b) && result == float64(int(result)) {\n return int(result)\n }\n return result\n}\n\n// Div returns a / b. Returns float64 to match JavaScript behavior.\n// Returns 0 if b is 0 (instead of panicking).\nfunc Div(a, b any) any {\n av, bv := toFloat64(a), toFloat64(b)\n if bv == 0 {\n return 0\n }\n return av / bv\n}\n\n// Mod returns a % b (modulo). Supports int only.\nfunc Mod(a, b any) int {\n av, bv := toInt(a), toInt(b)\n if bv == 0 {\n return 0\n }\n return av % bv\n}\n\n// Neg returns -a (negation).\nfunc Neg(a any) any {\n if v, ok := a.(int); ok {\n return -v\n }\n return -toFloat64(a)\n}\n\n// =============================================================================\n// String Operations\n// =============================================================================\n\n// Lower returns the lowercase version of s.\nfunc Lower(s string) string {\n return strings.ToLower(s)\n}\n\n// Upper returns the uppercase version of s.\nfunc Upper(s string) string {\n return strings.ToUpper(s)\n}\n\n// Trim returns s with leading and trailing whitespace removed.\nfunc Trim(s string) string {\n return strings.TrimSpace(s)\n}\n\n// Contains returns true if s contains substr.\nfunc Contains(s, substr string) bool {\n return strings.Contains(s, substr)\n}\n\n// Join concatenates elements of a slice with sep. Accepts both\n// reflect.Slice (the common case \u2014 `bf_arr` and `bf_filter_truthy`\n// both return `[]any`) AND reflect.Array (fixed-size Go arrays like\n// `[3]string{...}`), mirroring JS `Array.prototype.join` which\n// doesn\'t distinguish between the two. Pre-fix this returned "" for\n// fixed-size arrays passed through template data (Copilot review on\n// #1445).\nfunc Join(items any, sep string) string {\n v := reflect.ValueOf(items)\n if v.Kind() != reflect.Slice && v.Kind() != reflect.Array {\n return ""\n }\n\n parts := make([]string, v.Len())\n for i := 0; i < v.Len(); i++ {\n parts[i] = toString(v.Index(i).Interface())\n }\n return strings.Join(parts, sep)\n}\n\n// String returns the string form of v. Mirrors JS `String(v)` for\n// non-nil values via `fmt.Sprintf("%v", ...)`. Diverges from JS on\n// nil: JS `String(null)` is "null", but the template path renders\n// `nil` as the empty string here so an unset prop doesn\'t surface\n// as a literal "null"/"undefined" in user-facing HTML. Document the\n// divergence explicitly so callers don\'t rely on JS-exact parity.\nfunc String(v any) string {\n if v == nil {\n return ""\n }\n return fmt.Sprintf("%v", v)\n}\n\n// JSON returns the JSON encoding of v as a string. Mirrors\n// JS `JSON.stringify(v)` for the V1 single-arg shape (no `replacer`\n// or `space`). Object key order is determined by Go\'s `encoding/json`\n// (alphabetical for maps, declaration order for structs) \u2014 the\n// #1187 contract requires value-compat, not order-compat.\n//\n// Top-level NaN / \xB1Inf are pre-handled to match JS \u2014 JS\'s\n// `JSON.stringify(NaN)` and `JSON.stringify(Infinity)` both produce\n// `"null"`, but Go\'s `encoding/json` rejects them with\n// `UnsupportedValueError`. Without this carve-out the common\n// composition `JSON.stringify(Number("garbage"))` would error\n// instead of emitting `"null"` like JS does. Nested NaN/Inf inside\n// a struct/map still surfaces an error \u2014 covering that needs a\n// custom marshaller; out of V1 scope.\n//\n// Returns the marshal error so a `template.Execute` call fails\n// loudly on cycles / unsupported values rather than silently\n// producing `""` and reintroducing the SSR data-loss class\n// #1187 was filed against. Go\'s text/template treats a non-nil\n// error return from a func as an execution failure.\nfunc JSON(v any) (string, error) {\n if f, ok := v.(float64); ok && (math.IsNaN(f) || math.IsInf(f, 0)) {\n return "null", nil\n }\n b, err := json.Marshal(v)\n if err != nil {\n return "", err\n }\n return string(b), nil\n}\n\n// Number coerces v to a float64. Mirrors JS `Number(v)` semantics:\n// numeric / boolean inputs convert as expected; non-numeric strings\n// and other unsupported shapes return `NaN` (matching JS rather\n// than silently substituting 0, which would mis-shape downstream\n// arithmetic and template-side comparisons). Templates that need\n// a deterministic fallback should compose with the user-side\n// default (e.g. `Number(props.x ?? 0)` in JSX).\nfunc Number(v any) float64 {\n if v == nil {\n return math.NaN()\n }\n switch x := v.(type) {\n case float64:\n return x\n case float32:\n return float64(x)\n case int:\n return float64(x)\n case int32:\n return float64(x)\n case int64:\n return float64(x)\n case bool:\n if x {\n return 1\n }\n return 0\n case string:\n f, err := strconv.ParseFloat(x, 64)\n if err != nil {\n return math.NaN()\n }\n return f\n }\n return math.NaN()\n}\n\n// Floor returns the largest integer \u2264 v as a float64. Mirrors JS\n// `Math.floor`. The return type stays float64 so chained primitives\n// (`bf_floor` then `bf_string`) line up with JS\'s number type.\nfunc Floor(v any) float64 {\n return math.Floor(Number(v))\n}\n\n// Ceil returns the smallest integer \u2265 v as a float64. Mirrors JS\n// `Math.ceil`.\nfunc Ceil(v any) float64 {\n return math.Ceil(Number(v))\n}\n\n// Round returns v rounded to the nearest integer as a float64.\n// Mirrors JS `Math.round` \u2014 half-away-from-zero (Go\'s `math.Round`\n// matches; JS rounds half toward +Infinity which differs at .5\n// negatives; we accept that minor divergence since the conformance\n// contract is value-compat for the common positive case).\nfunc Round(v any) float64 {\n return math.Round(Number(v))\n}\n\n// =============================================================================\n// Array/Slice Operations\n// =============================================================================\n\n// Len returns the length of a slice, array, map, string, or channel.\n// Returns 0 for nil or unsupported types.\nfunc Len(v any) int {\n if v == nil {\n return 0\n }\n rv := reflect.ValueOf(v)\n switch rv.Kind() {\n case reflect.Slice, reflect.Array, reflect.Map, reflect.String, reflect.Chan:\n return rv.Len()\n default:\n return 0\n }\n}\n\n// At returns the element at index i from a slice.\n// Supports negative indices (e.g., -1 for last element).\n// Returns nil if index is out of bounds.\nfunc At(items any, index int) any {\n v := reflect.ValueOf(items)\n if v.Kind() != reflect.Slice && v.Kind() != reflect.Array {\n return nil\n }\n\n length := v.Len()\n if length == 0 {\n return nil\n }\n\n // Handle negative indices\n if index < 0 {\n index = length + index\n }\n\n if index < 0 || index >= length {\n return nil\n }\n\n return v.Index(index).Interface()\n}\n\n// Includes returns true if items contains elem. Lowers both\n// `Array.prototype.includes` and `String.prototype.includes` \u2014\n// the adapter can\'t disambiguate the receiver at compile time,\n// so this helper dispatches at runtime on `reflect.Kind()`:\n//\n// - slice/array receiver: DeepEqual element search\n// - string receiver: strings.Contains substring search\n//\n// Anything else returns false (matches the JS semantic where\n// `.includes` is only defined on Array / TypedArray / String).\nfunc Includes(recv any, elem any) bool {\n v := reflect.ValueOf(recv)\n if v.Kind() == reflect.String {\n // JS `String.prototype.includes` accepts only string args;\n // non-string `elem` would TypeError in real JS but our\n // callers have lowered through `convertExpressionToGo`\n // where the arg type is whatever the template binds. Stringify\n // via fmt to keep the helper total.\n needle, ok := elem.(string)\n if !ok {\n needle = fmt.Sprintf("%v", elem)\n }\n return strings.Contains(v.String(), needle)\n }\n if v.Kind() != reflect.Slice && v.Kind() != reflect.Array {\n return false\n }\n for i := 0; i < v.Len(); i++ {\n if reflect.DeepEqual(v.Index(i).Interface(), elem) {\n return true\n }\n }\n return false\n}\n\n// IndexOf returns the 0-based position of the first item that\n// DeepEquals `elem`, or -1 if not found. Lowers\n// `Array.prototype.indexOf(x)` (#1448 Tier A). The existing\n// `FindIndex` helper does struct-field equality (used by the\n// higher-order `.find` lowering); this one does value equality\n// against scalar / struct items so callers don\'t have to compose\n// a synthetic predicate.\n//\n// Non-array / non-slice receivers return -1 (matches the JS\n// semantic that `.indexOf` is only defined on Array / TypedArray).\nfunc IndexOf(items any, elem any) int {\n v := reflect.ValueOf(items)\n if v.Kind() != reflect.Slice && v.Kind() != reflect.Array {\n return -1\n }\n for i := 0; i < v.Len(); i++ {\n if reflect.DeepEqual(v.Index(i).Interface(), elem) {\n return i\n }\n }\n return -1\n}\n\n// LastIndexOf returns the 0-based position of the last item that\n// DeepEquals `elem`, or -1 if not found. Mirrors\n// `Array.prototype.lastIndexOf(x)`. The reverse traversal is the\n// only behavioural difference vs `IndexOf` \u2014 disambiguating a\n// duplicated value\'s first vs last position is the canonical\n// reason a JS author reaches for `lastIndexOf`.\nfunc LastIndexOf(items any, elem any) int {\n v := reflect.ValueOf(items)\n if v.Kind() != reflect.Slice && v.Kind() != reflect.Array {\n return -1\n }\n for i := v.Len() - 1; i >= 0; i-- {\n if reflect.DeepEqual(v.Index(i).Interface(), elem) {\n return i\n }\n }\n return -1\n}\n\n// Concat merges two arrays (or slices) into a single `[]any`,\n// preserving order: receiver elements first, then `other`\'s.\n// Lowers `Array.prototype.concat(other)` (#1448 Tier A). Non-array\n// operands collapse to an empty source \u2014 matches the JS semantic\n// where `.concat` on a non-Array reads it as a single element only\n// if its `Symbol.isConcatSpreadable` is true; the template-language\n// path doesn\'t have user objects with that flag, so treating\n// non-arrays as empty is the conservative lowering. Variadic\n// `.concat(a, b, c)` is out of scope here (parser gates to a single\n// arg); the helper itself stays binary so a future variadic IR can\n// fold via repeated calls without changing this signature.\nfunc Concat(a, b any) []any {\n flatten := func(v reflect.Value) []any {\n if !v.IsValid() {\n return nil\n }\n if v.Kind() != reflect.Slice && v.Kind() != reflect.Array {\n return nil\n }\n out := make([]any, v.Len())\n for i := 0; i < v.Len(); i++ {\n out[i] = v.Index(i).Interface()\n }\n return out\n }\n left := flatten(reflect.ValueOf(a))\n right := flatten(reflect.ValueOf(b))\n return append(left, right...)\n}\n\n// Slice carves out a sub-range from `items`. Lowers\n// `Array.prototype.slice(start, end?)` (#1448 Tier A). The variadic\n// `end` arg lets Go template\'s call dispatcher pass either 2 or 3\n// arguments; an absent end means "to length".\n//\n// JS-compat clamping:\n// - start < 0 \u2192 length + start (e.g. -1 = last index)\n// - end < 0 \u2192 length + end\n// - start < 0 after clamp \u2192 0\n// - end > length \u2192 length\n// - start >= end \u2192 empty slice (no panic)\n//\n// Non-array receivers return an empty `[]any`.\nfunc Slice(items any, start int, end ...int) []any {\n v := reflect.ValueOf(items)\n if v.Kind() != reflect.Slice && v.Kind() != reflect.Array {\n return []any{}\n }\n length := v.Len()\n\n // Normalise start (negative = from end).\n if start < 0 {\n start = length + start\n }\n if start < 0 {\n start = 0\n }\n if start > length {\n start = length\n }\n\n // Normalise end (optional; absent = length).\n stop := length\n if len(end) > 0 {\n stop = end[0]\n if stop < 0 {\n stop = length + stop\n }\n if stop < 0 {\n stop = 0\n }\n if stop > length {\n stop = length\n }\n }\n\n if start >= stop {\n return []any{}\n }\n\n out := make([]any, 0, stop-start)\n for i := start; i < stop; i++ {\n out = append(out, v.Index(i).Interface())\n }\n return out\n}\n\n// Reverse returns a new slice with `items`\'s elements in reverse\n// order. Lowers both `Array.prototype.reverse()` and\n// `Array.prototype.toReversed()` (#1448 Tier A) \u2014 SSR templates\n// render a snapshot, so JS\'s mutate-receiver vs return-new-array\n// distinction has no template-level meaning, and the safer\n// non-mutating shape is used uniformly.\n//\n// Non-array receivers return an empty `[]any`.\nfunc Reverse(items any) []any {\n v := reflect.ValueOf(items)\n if v.Kind() != reflect.Slice && v.Kind() != reflect.Array {\n return []any{}\n }\n length := v.Len()\n out := make([]any, length)\n for i := 0; i < length; i++ {\n out[length-1-i] = v.Index(i).Interface()\n }\n return out\n}\n\n// First returns the first element of a slice, or nil if empty.\nfunc First(items any) any {\n return At(items, 0)\n}\n\n// Last returns the last element of a slice, or nil if empty.\nfunc Last(items any) any {\n return At(items, -1)\n}\n\n// Arr builds an []any from variadic args. Used to lower JS array\n// literals like `[a, b]` for the registry Slot\'s\n// `[className, childClass].filter(Boolean).join(\' \')` shape (#1443) \u2014\n// Go templates have no array-literal syntax, so the codegen routes\n// array-literal IR nodes through this helper.\nfunc Arr(items ...any) []any {\n return items\n}\n\n// FilterTruthy returns a new slice containing only truthy items.\n// Mirrors `arr.filter(Boolean)` semantics: drop nil, false, 0, "" \u2014 the\n// same falsy set JavaScript\'s `Boolean(x)` recognises. Used to lower\n// the registry Slot\'s class-merge pattern (#1443); generalising to\n// arbitrary callable predicates would need the callee-resolution path\n// blocked by #1389, so this stays Boolean-specific.\nfunc FilterTruthy(items any) []any {\n v := reflect.ValueOf(items)\n if !v.IsValid() || (v.Kind() != reflect.Slice && v.Kind() != reflect.Array) {\n return nil\n }\n result := make([]any, 0, v.Len())\n for i := 0; i < v.Len(); i++ {\n raw := v.Index(i).Interface()\n if isTruthy(raw) {\n result = append(result, raw)\n }\n }\n return result\n}\n\n// isTruthy mirrors JavaScript\'s `Boolean(x)` for the value shapes the\n// template path actually receives \u2014 nil / false / 0 / "" are falsy.\n// Other shapes (non-empty maps, slices, structs, true) are truthy, in\n// line with JS\'s "objects are truthy" rule.\nfunc isTruthy(v any) bool {\n if v == nil {\n return false\n }\n switch x := v.(type) {\n case bool:\n return x\n case string:\n return x != ""\n case int:\n return x != 0\n case int8, int16, int32, int64:\n return reflect.ValueOf(v).Int() != 0\n case uint, uint8, uint16, uint32, uint64:\n return reflect.ValueOf(v).Uint() != 0\n case float32:\n // JS `Boolean(NaN)` is false regardless of float width \u2014 the\n // float64 arm below was the only one checking IsNaN, which\n // diverged from JS for `float32` NaN inputs (Copilot review on\n // #1445). Widening to float64 for the IsNaN check keeps the\n // two branches in lock-step.\n return x != 0 && !math.IsNaN(float64(x))\n case float64:\n return x != 0 && !math.IsNaN(x)\n }\n return true\n}\n\n// =============================================================================\n// Higher-order Array Methods\n// =============================================================================\n\n// Every returns true if all items have the specified field set to true.\n// Mirrors JavaScript\'s Array.prototype.every(item => item.field).\nfunc Every(items any, field string) bool {\n v := reflect.ValueOf(items)\n if v.Kind() != reflect.Slice && v.Kind() != reflect.Array {\n return false\n }\n\n capitalizedField := capitalize(field)\n for i := 0; i < v.Len(); i++ {\n item := v.Index(i)\n if item.Kind() == reflect.Interface {\n item = item.Elem()\n }\n if item.Kind() == reflect.Ptr {\n item = item.Elem()\n }\n if item.Kind() != reflect.Struct {\n continue\n }\n\n fieldVal := item.FieldByName(capitalizedField)\n if !fieldVal.IsValid() {\n return false\n }\n if fieldVal.Kind() == reflect.Bool && !fieldVal.Bool() {\n return false\n }\n }\n return true\n}\n\n// Some returns true if at least one item has the specified field set to true.\n// Mirrors JavaScript\'s Array.prototype.some(item => item.field).\nfunc Some(items any, field string) bool {\n v := reflect.ValueOf(items)\n if v.Kind() != reflect.Slice && v.Kind() != reflect.Array {\n return false\n }\n\n capitalizedField := capitalize(field)\n for i := 0; i < v.Len(); i++ {\n item := v.Index(i)\n if item.Kind() == reflect.Interface {\n item = item.Elem()\n }\n if item.Kind() == reflect.Ptr {\n item = item.Elem()\n }\n if item.Kind() != reflect.Struct {\n continue\n }\n\n fieldVal := item.FieldByName(capitalizedField)\n if fieldVal.IsValid() && fieldVal.Kind() == reflect.Bool && fieldVal.Bool() {\n return true\n }\n }\n return false\n}\n\n// Filter returns items where item.field == value.\n// Mirrors JavaScript\'s Array.prototype.filter(item => item.field === value).\n// Returns []any to allow chaining with other bf_* functions.\nfunc Filter(items any, field string, value any) []any {\n v := reflect.ValueOf(items)\n if v.Kind() != reflect.Slice && v.Kind() != reflect.Array {\n return nil\n }\n\n capitalizedField := capitalize(field)\n var result []any\n\n for i := 0; i < v.Len(); i++ {\n item := v.Index(i)\n if item.Kind() == reflect.Interface {\n item = item.Elem()\n }\n if item.Kind() == reflect.Ptr {\n item = item.Elem()\n }\n if item.Kind() != reflect.Struct {\n continue\n }\n\n fieldVal := item.FieldByName(capitalizedField)\n if !fieldVal.IsValid() {\n continue\n }\n\n // Compare field value with target value\n if reflect.DeepEqual(fieldVal.Interface(), value) {\n result = append(result, v.Index(i).Interface())\n }\n }\n return result\n}\n\n// Find returns the first item where item.field == value, or nil if not found.\n// Mirrors JavaScript\'s Array.prototype.find(item => item.field === value).\nfunc Find(items any, field string, value any) any {\n v := reflect.ValueOf(items)\n if v.Kind() != reflect.Slice && v.Kind() != reflect.Array {\n return nil\n }\n\n capitalizedField := capitalize(field)\n for i := 0; i < v.Len(); i++ {\n item := v.Index(i)\n if item.Kind() == reflect.Interface {\n item = item.Elem()\n }\n if item.Kind() == reflect.Ptr {\n item = item.Elem()\n }\n if item.Kind() != reflect.Struct {\n continue\n }\n\n fieldVal := item.FieldByName(capitalizedField)\n if !fieldVal.IsValid() {\n continue\n }\n\n if reflect.DeepEqual(fieldVal.Interface(), value) {\n return v.Index(i).Interface()\n }\n }\n return nil\n}\n\n// FindIndex returns the index of the first item where item.field == value, or -1.\n// Mirrors JavaScript\'s Array.prototype.findIndex(item => item.field === value).\nfunc FindIndex(items any, field string, value any) int {\n v := reflect.ValueOf(items)\n if v.Kind() != reflect.Slice && v.Kind() != reflect.Array {\n return -1\n }\n\n capitalizedField := capitalize(field)\n for i := 0; i < v.Len(); i++ {\n item := v.Index(i)\n if item.Kind() == reflect.Interface {\n item = item.Elem()\n }\n if item.Kind() == reflect.Ptr {\n item = item.Elem()\n }\n if item.Kind() != reflect.Struct {\n continue\n }\n\n fieldVal := item.FieldByName(capitalizedField)\n if !fieldVal.IsValid() {\n continue\n }\n\n if reflect.DeepEqual(fieldVal.Interface(), value) {\n return i\n }\n }\n return -1\n}\n\n// FindLast returns the last item where item.field == value, or nil if not found.\n// Mirrors JavaScript\'s Array.prototype.findLast(item => item.field === value).\nfunc FindLast(items any, field string, value any) any {\n v := reflect.ValueOf(items)\n if v.Kind() != reflect.Slice && v.Kind() != reflect.Array {\n return nil\n }\n\n capitalizedField := capitalize(field)\n for i := v.Len() - 1; i >= 0; i-- {\n item := v.Index(i)\n if item.Kind() == reflect.Interface {\n if item.IsNil() {\n continue\n }\n item = item.Elem()\n }\n if item.Kind() == reflect.Ptr {\n if item.IsNil() {\n continue\n }\n item = item.Elem()\n }\n if item.Kind() != reflect.Struct {\n continue\n }\n\n fieldVal := item.FieldByName(capitalizedField)\n if !fieldVal.IsValid() {\n continue\n }\n\n if reflect.DeepEqual(fieldVal.Interface(), value) {\n return v.Index(i).Interface()\n }\n }\n return nil\n}\n\n// FindLastIndex returns the index of the last item where item.field == value, or -1.\n// Mirrors JavaScript\'s Array.prototype.findLastIndex(item => item.field === value).\nfunc FindLastIndex(items any, field string, value any) int {\n v := reflect.ValueOf(items)\n if v.Kind() != reflect.Slice && v.Kind() != reflect.Array {\n return -1\n }\n\n capitalizedField := capitalize(field)\n for i := v.Len() - 1; i >= 0; i-- {\n item := v.Index(i)\n if item.Kind() == reflect.Interface {\n if item.IsNil() {\n continue\n }\n item = item.Elem()\n }\n if item.Kind() == reflect.Ptr {\n if item.IsNil() {\n continue\n }\n item = item.Elem()\n }\n if item.Kind() != reflect.Struct {\n continue\n }\n\n fieldVal := item.FieldByName(capitalizedField)\n if !fieldVal.IsValid() {\n continue\n }\n\n if reflect.DeepEqual(fieldVal.Interface(), value) {\n return i\n }\n }\n return -1\n}\n\n// sortKeySpec is one parsed comparison key. A simple comparator has\n// one; a `||`-chained multi-key comparator has several, applied in\n// order as tie-breakers.\ntype sortKeySpec struct {\n kind string // "self" | "field"\n name string // capitalised field name, or "" for "self"\n compareType string // "numeric" | "string" | "auto"\n direction string // "asc" | "desc"\n}\n\n// Sort returns a new stable-sorted slice. Lowers\n// `Array.prototype.sort` / `Array.prototype.toSorted` (#1448 Tier B).\n// Non-mutating \u2014 JS\'s mutate-vs-new distinction is moot in SSR\n// template context (templates render a snapshot).\n//\n// Call shape (the compiler emits one 4-string group per key):\n//\n// bf_sort <items> (<keyKind> <keyName> <compareType> <direction>)+\n//\n// keyKind: "self" | "field"\n// keyName: "" when keyKind == "self"; capitalised struct field\n// name (e.g. "Price") otherwise\n// compareType: "numeric" | "string" | "auto"\n// direction: "asc" | "desc"\n//\n// The groups cover the accepted comparator catalogue: `a.f - b.f`,\n// `a - b`, `a[.f].localeCompare(b[.f])`, and relational-ternary keys\n// (`a.f > b.f ? 1 : -1` \u2192 "auto"), each `||`-chainable for multi-key\n// tie-breaks. Anything outside refuses at compile time (BF101 from the\n// JSX compiler) and never reaches this helper.\n//\n// "auto" compares numerically when both projected keys parse as\n// numbers, else lexically \u2014 mirroring the Perl `bf->sort` helper\'s\n// `looks_like_number` rule so the two template adapters stay\n// byte-equal. This diverges from JS `<`/`>` only for numeric strings.\n//\n// A future `nulls` knob can extend the per-key group without rewriting\n// existing call sites \u2014 each key already projects before comparing.\nfunc Sort(items any, spec ...string) []any {\n v := reflect.ValueOf(items)\n if v.Kind() != reflect.Slice && v.Kind() != reflect.Array {\n return nil\n }\n\n length := v.Len()\n if length == 0 {\n return []any{}\n }\n\n // Copy into a fresh []any so the sort is non-mutating regardless\n // of whether the receiver is `[]T` or `[]any`.\n result := make([]any, length)\n for i := 0; i < length; i++ {\n result[i] = v.Index(i).Interface()\n }\n\n keys := parseSortSpec(spec)\n sort.SliceStable(result, func(i, j int) bool {\n for _, k := range keys {\n ki := projectSortKey(result[i], k.kind, k.name)\n kj := projectSortKey(result[j], k.kind, k.name)\n c := compareSortKey(ki, kj, k.compareType)\n if c == 0 {\n continue // tie on this key \u2014 fall through to the next\n }\n if k.direction == "desc" {\n return c > 0\n }\n return c < 0\n }\n return false\n })\n\n return result\n}\n\n// parseSortSpec chunks the variadic operand list into 4-string key\n// groups. A trailing partial group (malformed emit) is ignored rather\n// than panicking \u2014 defensive, mirroring the helper\'s nil-safe stance.\nfunc parseSortSpec(spec []string) []sortKeySpec {\n var keys []sortKeySpec\n for i := 0; i+3 < len(spec); i += 4 {\n keys = append(keys, sortKeySpec{\n kind: spec[i],\n name: spec[i+1],\n compareType: spec[i+2],\n direction: spec[i+3],\n })\n }\n return keys\n}\n\n// compareSortKey returns -1 / 0 / 1 for two projected keys under the\n// given compare type (ascending orientation; the caller flips for\n// "desc"). "string" stringifies both (nil \u2192 "", matching the\n// documented `bf->string(undef) === ""` divergence). "auto" compares\n// numerically when both parse as numbers, else lexically.\nfunc compareSortKey(ki, kj any, compareType string) int {\n switch compareType {\n case "string":\n return strings.Compare(toString(ki), toString(kj))\n case "auto":\n ni, okI := toFloat64WithOK(ki)\n nj, okJ := toFloat64WithOK(kj)\n if okI && okJ {\n return cmpFloat(ni, nj)\n }\n return strings.Compare(toString(ki), toString(kj))\n default: // numeric\n return cmpFloat(toFloat64(ki), toFloat64(kj))\n }\n}\n\nfunc cmpFloat(a, b float64) int {\n if a < b {\n return -1\n }\n if a > b {\n return 1\n }\n return 0\n}\n\n// toFloat64WithOK reports a value\'s numeric float and whether it is\n// number-like. Genuine numeric kinds always qualify; strings qualify\n// when they parse as a float (so the "auto" compare path matches the\n// Perl `looks_like_number` rule). Everything else is non-numeric.\nfunc toFloat64WithOK(v any) (float64, bool) {\n switch n := v.(type) {\n case int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64, float32, float64:\n return toFloat64(v), true\n case string:\n f, err := strconv.ParseFloat(strings.TrimSpace(n), 64)\n if err != nil {\n return 0, false\n }\n return f, true\n default:\n return 0, false\n }\n}\n\n// projectSortKey reduces an item to the value the comparator\n// actually compares. For `keyKind == "field"` it reads the named\n// struct field; for `keyKind == "self"` (primitive arrays) it\n// returns the item unchanged.\nfunc projectSortKey(item any, keyKind, keyName string) any {\n if keyKind == "field" {\n return getFieldValue(item, keyName)\n }\n return item\n}\n\n// getFieldValue extracts a struct field value using reflection. For\n// map receivers it falls back to case-variant lookup so JSON-decoded\n// user data (`map[string]any{"price": 30}`) and PascalCase-emitted\n// test data both resolve under a single key name. (#1487)\nfunc getFieldValue(item any, field string) any {\n v := reflect.ValueOf(item)\n // Defensive IsNil guards mirror `SpreadAttrs` \u2014 keeps the helper\n // safe against typed-nil pointer / nil-interface items inside a\n // `[]any` so a single bad row doesn\'t crash the whole sort.\n if v.Kind() == reflect.Interface {\n if v.IsNil() {\n return nil\n }\n v = v.Elem()\n }\n if v.Kind() == reflect.Ptr {\n if v.IsNil() {\n return nil\n }\n v = v.Elem()\n }\n\n if v.Kind() == reflect.Map {\n keyType := v.Type().Key()\n if keyType.Kind() != reflect.String {\n return nil\n }\n // Convert the lookup string to the map\'s actual key type so\n // maps keyed by a named string type (`type Key string`) don\'t\n // panic with `value of type string is not assignable to type X`.\n lookup := func(s string) (any, bool) {\n k := reflect.ValueOf(s).Convert(keyType)\n if mv := v.MapIndex(k); mv.IsValid() {\n return mv.Interface(), true\n }\n return nil, false\n }\n if r, ok := lookup(field); ok {\n return r\n }\n if cap := capitalize(field); cap != field {\n if r, ok := lookup(cap); ok {\n return r\n }\n }\n if low := decapitalize(field); low != field {\n if r, ok := lookup(low); ok {\n return r\n }\n }\n return nil\n }\n\n if v.Kind() != reflect.Struct {\n return nil\n }\n\n fieldVal := v.FieldByName(field)\n if !fieldVal.IsValid() {\n return nil\n }\n return fieldVal.Interface()\n}\n\n// capitalize uppercases the first character of a string.\nfunc capitalize(s string) string {\n if s == "" {\n return s\n }\n return strings.ToUpper(s[:1]) + s[1:]\n}\n\n// decapitalize lowercases the first character of a string. Used by\n// `getFieldValue`\'s map-receiver fallback when the projected key\n// name is PascalCase but the receiver carries lowercase JS-style\n// keys (the inverse of the `capitalize` lookup).\nfunc decapitalize(s string) string {\n if s == "" {\n return s\n }\n return strings.ToLower(s[:1]) + s[1:]\n}\n\n// =============================================================================\n// HTML/Template Helpers\n// =============================================================================\n\n// Comment returns an HTML comment string for hydration markers.\n// The "bf-" prefix is automatically added.\nfunc Comment(content string) template.HTML {\n return template.HTML("<!--bf-" + content + "-->")\n}\n\n// TextStart returns an HTML comment start marker for reactive text expressions.\n// Format: <!--bf:slotId-->\nfunc TextStart(slotId string) template.HTML {\n return template.HTML("<!--bf:" + slotId + "-->")\n}\n\n// TextEnd returns an HTML comment end marker for reactive text expressions.\n// Format: <!--/-->\nfunc TextEnd() template.HTML {\n return "<!--/-->"\n}\n\n// ScopeComment emits a fragment-rooted scope marker. See spec/compiler.md\n// "Slot identity" for the wire format. Loud-fails on marshal errors\n// (same policy as JSON / BfPropsAttr).\nfunc ScopeComment(props interface{}) (template.HTML, error) {\n scopeID := getStringField(props, "ScopeID")\n hostSegment := ""\n if host := getStringField(props, "BfParent"); host != "" {\n mount := getStringField(props, "BfMount")\n hostSegment = "|h=" + host + "|m=" + mount\n }\n propsJSON := ""\n if getBoolField(props, "BfIsRoot") {\n pJSON, err := json.Marshal(props)\n if err != nil {\n return "", err\n }\n propsJSON = "|" + string(pJSON)\n }\n return template.HTML("<!--bf-scope:" + scopeID + hostSegment + propsJSON + "-->"), nil\n}\n\n// PortalHTML parses and executes a template string with the provided data.\n// Used for rendering dynamic portal content where the template string\n// contains Go template expressions (e.g., {{if .Open}}open{{end}}).\n//\n// The template string is parsed fresh each time to support dynamic content.\n// Standard Go template functions (if, range, eq, etc.) are available.\nfunc PortalHTML(data interface{}, tmplStr string) template.HTML {\n // Create a new template with the FuncMap for custom functions\n t, err := template.New("portal").Funcs(FuncMap()).Parse(tmplStr)\n if err != nil {\n // Return error message as HTML comment for debugging\n return template.HTML("<!-- bfPortalHTML error: " + err.Error() + " -->")\n }\n\n var buf bytes.Buffer\n if err := t.Execute(&buf, data); err != nil {\n return template.HTML("<!-- bfPortalHTML exec error: " + err.Error() + " -->")\n }\n\n return template.HTML(buf.String())\n}\n\n// =============================================================================\n// Portal Collection\n// =============================================================================\n\n// PortalContent represents a single portal\'s content to be rendered at body end.\ntype PortalContent struct {\n ID string // Unique portal ID for hydration matching\n OwnerID string // Owner scope ID for find() support\n Content template.HTML // Portal HTML content\n}\n\n// PortalCollector collects portal content during template rendering.\n// Portal content is rendered at </body> to avoid z-index issues.\ntype PortalCollector struct {\n portals []PortalContent\n counter int\n}\n\n// NewPortalCollector creates a new PortalCollector.\nfunc NewPortalCollector() *PortalCollector {\n return &PortalCollector{\n portals: []PortalContent{},\n counter: 0,\n }\n}\n\n// Add registers portal content to be rendered at body end.\nfunc (pc *PortalCollector) Add(ownerID string, content template.HTML) string {\n pc.counter++\n id := "bf-portal-" + strconv.Itoa(pc.counter)\n pc.portals = append(pc.portals, PortalContent{\n ID: id,\n OwnerID: ownerID,\n Content: content,\n })\n return "" // Return empty string for template use\n}\n\n// Render outputs all collected portals as HTML.\n// Each portal is wrapped in a div with bf-pi (portal ID) and bf-po (portal owner).\nfunc (pc *PortalCollector) Render() template.HTML {\n if pc == nil || len(pc.portals) == 0 {\n return ""\n }\n var buf strings.Builder\n for _, p := range pc.portals {\n buf.WriteString(`<div bf-pi="`)\n buf.WriteString(p.ID)\n buf.WriteString(`" bf-po="`)\n buf.WriteString(p.OwnerID)\n buf.WriteString(`">`)\n buf.WriteString(string(p.Content))\n buf.WriteString("</div>\\n")\n }\n return template.HTML(buf.String())\n}\n\n// =============================================================================\n// Script Collection\n// =============================================================================\n\n// ScriptCollector collects client scripts with deduplication.\n// It preserves insertion order for deterministic output.\ntype ScriptCollector struct {\n scripts map[string]bool\n order []string\n}\n\n// NewScriptCollector creates a new ScriptCollector.\nfunc NewScriptCollector() *ScriptCollector {\n return &ScriptCollector{\n scripts: make(map[string]bool),\n order: []string{},\n }\n}\n\n// Register adds a script source to the collection.\n// Duplicate scripts are ignored (only first registration counts).\nfunc (sc *ScriptCollector) Register(src string) string {\n if sc.scripts[src] {\n return "" // Already registered\n }\n sc.scripts[src] = true\n sc.order = append(sc.order, src)\n return "" // Return empty string for template use\n}\n\n// Scripts returns all registered scripts in insertion order.\nfunc (sc *ScriptCollector) Scripts() []string {\n return sc.order\n}\n\n// BfScripts generates script tags for all registered scripts.\n// Returns HTML safe for embedding in templates.\nfunc BfScripts(collector *ScriptCollector) template.HTML {\n if collector == nil {\n return ""\n }\n var result strings.Builder\n for _, src := range collector.Scripts() {\n result.WriteString(`<script type="module" src="`)\n result.WriteString(src)\n result.WriteString(`"></script>`)\n result.WriteString("\\n")\n }\n return template.HTML(result.String())\n}\n\n// =============================================================================\n// Component Renderer\n// =============================================================================\n\n// RenderContext contains all data needed to render a component page.\n// The layout function receives this context to build the final HTML.\ntype RenderContext struct {\n // ComponentName is the template name being rendered\n ComponentName string\n\n // Props is the component props (for layout to access if needed)\n Props interface{}\n\n // ComponentHTML is the rendered component template output\n ComponentHTML template.HTML\n\n // Portals contains collected portal content to render at body end\n Portals template.HTML\n\n // Scripts contains the collected JS script tags\n Scripts template.HTML\n\n // Title is the page title (defaults to "{ComponentName} - BarefootJS")\n Title string\n\n // Heading is the page heading. Empty string means no heading.\n Heading string\n\n // Extra holds additional user-defined data for the layout\n Extra map[string]interface{}\n}\n\n// LayoutFunc renders the final HTML page given the render context.\ntype LayoutFunc func(ctx *RenderContext) string\n\n// Renderer renders BarefootJS components with a customizable layout.\ntype Renderer struct {\n templates *template.Template\n layout LayoutFunc\n}\n\n// NewRenderer creates a Renderer with the given templates and layout function.\n//\n// Example usage:\n//\n// renderer := bf.NewRenderer(templates, func(ctx *bf.RenderContext) string {\n// return fmt.Sprintf(`<!DOCTYPE html>\n// <html>\n// <head><title>%s</title></head>\n// <body>%s%s</body>\n// </html>`, ctx.Title, ctx.ComponentHTML, ctx.Scripts)\n// })\nfunc NewRenderer(tmpl *template.Template, layout LayoutFunc) *Renderer {\n return &Renderer{\n templates: tmpl,\n layout: layout,\n }\n}\n\n// RenderOptions configures a single render call.\ntype RenderOptions struct {\n // ComponentName is the template name to render (required)\n ComponentName string\n\n // Props is the component props (must be a pointer to struct with Scripts field)\n Props interface{}\n\n // Title is the page title. If empty, defaults to "{ComponentName} - BarefootJS"\n Title string\n\n // Heading is the page heading. If empty, no heading is shown.\n Heading string\n\n // Extra holds additional data to pass to the layout\n Extra map[string]interface{}\n}\n\n// Render renders a component to a full HTML page using the configured layout.\n// Child component props are automatically detected (any slice field with ScopeID/Scripts).\n// renderTemplateErrorPanel formats a Go template execution error into a\n// fragment of HTML that\'s visible in the browser. The panel is\n// HTML-escaped so a faulty template name (anything from `template:\n// "..."`) can\'t smuggle markup back into the page. Keep the styling\n// inline so the panel surfaces even when the project\'s CSS hasn\'t\n// loaded yet (e.g. the failure aborted before the stylesheet links\n// emitted).\n//\n// Surfaced for the #1442 echo repro: a template referencing\n// `.Todo.Done` (instead of the range dot\'s `.Done`) used to fail\n// silently \u2014 Go\'s html/template aborted mid-stream, the partial body\n// flushed as a 200, and the user saw a truncated list with no console\n// signal. With this panel they get the template name, the error\n// message, and a "what to look at" hint inline.\nfunc renderTemplateErrorPanel(componentName string, err error) string {\n return `<div style="margin:1em 0;padding:1em;border:2px solid #d33;background:#fff5f5;color:#900;font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;font-size:13px;line-height:1.5"><strong style="display:block;margin-bottom:.5em">Template error in <code>` +\n template.HTMLEscapeString(componentName) +\n `</code></strong><pre style="margin:0;white-space:pre-wrap;word-break:break-word">` +\n template.HTMLEscapeString(err.Error()) +\n `</pre><div style="margin-top:.75em;font-size:12px;opacity:.7">Common cause: a JSX expression referenced a name the adapter could not resolve to a struct field. Open the matching <code>dist/templates/*.tmpl</code> for the unresolved reference, then fix the source component.</div></div>`\n}\n\nfunc (r *Renderer) Render(opts RenderOptions) string {\n // Create script collector and inject into props\n scriptCollector := NewScriptCollector()\n setScriptsField(opts.Props, scriptCollector)\n\n // Create portal collector and inject into props\n portalCollector := NewPortalCollector()\n setPortalsField(opts.Props, portalCollector)\n\n // Auto-detect and process child component props (slices)\n childSlices := findChildComponentSlices(opts.Props)\n for _, slice := range childSlices {\n setScriptsOnSlice(slice, scriptCollector)\n setPortalsOnSlice(slice, portalCollector)\n setBoolOnSlice(slice, "BfIsChild", true)\n }\n\n // Auto-detect and process single child component props\n singleChildren := findSingleChildComponents(opts.Props)\n for _, child := range singleChildren {\n setScriptsOnSingle(child, scriptCollector)\n setPortalsOnSingle(child, portalCollector)\n setBoolField(child, "BfIsChild", true)\n }\n\n // Mark the root component so BfPropsAttr emits bf-p only for it\n setBoolField(opts.Props, "BfIsRoot", true)\n\n // Render the component template.\n //\n // Errors here are NOT silently dropped. The original implementation\n // ignored the return value of `ExecuteTemplate`, which masked a real\n // onboarding failure mode: a template referencing a non-existent\n // field (`.Todo.Done` instead of the range dot\'s `.Done`) caused\n // html/template to abort mid-stream, the partial output got\n // returned, and the HTTP server happily flushed a 200 with a\n // truncated body. No error log, no signal \u2014 the user just saw a\n // blank list (#1442 echo TodoApp repro).\n //\n // Now we capture the error and replace the partial output with a\n // visible inline panel (dev mode) or a fenced error comment\n // (production), so the cause is on-screen and grep-able in logs.\n // Either way the renderer also writes to stderr so structured log\n // aggregators see it.\n var componentBuf strings.Builder\n if err := r.templates.ExecuteTemplate(&componentBuf, opts.ComponentName, opts.Props); err != nil {\n fmt.Fprintf(os.Stderr, "barefoot: template %q failed to render: %v\\n", opts.ComponentName, err)\n // Preserve whatever the template did manage to emit before\n // failing (Go\'s text/template flushes incrementally), but\n // follow it with a clearly-marked error block so the user\n // notices something is wrong instead of seeing a silent\n // truncation.\n componentBuf.WriteString(renderTemplateErrorPanel(opts.ComponentName, err))\n }\n\n // Determine title (default: "{ComponentName} - BarefootJS")\n title := opts.Title\n if title == "" {\n title = opts.ComponentName + " - BarefootJS"\n }\n\n // Heading (empty means no heading)\n heading := opts.Heading\n\n // Build render context\n ctx := &RenderContext{\n ComponentName: opts.ComponentName,\n Props: opts.Props,\n ComponentHTML: template.HTML(componentBuf.String()),\n Portals: portalCollector.Render(),\n Scripts: BfScripts(scriptCollector),\n Title: title,\n Heading: heading,\n Extra: opts.Extra,\n }\n\n return r.layout(ctx)\n}\n\n// setScriptsField sets the Scripts field on a struct using reflection.\nfunc setScriptsField(v interface{}, collector *ScriptCollector) {\n val := reflect.ValueOf(v)\n if val.Kind() == reflect.Ptr {\n val = val.Elem()\n }\n if val.Kind() != reflect.Struct {\n return\n }\n field := val.FieldByName("Scripts")\n if field.IsValid() && field.CanSet() {\n field.Set(reflect.ValueOf(collector))\n }\n}\n\n// setPortalsField sets the Portals field on a struct using reflection.\nfunc setPortalsField(v interface{}, collector *PortalCollector) {\n val := reflect.ValueOf(v)\n if val.Kind() == reflect.Ptr {\n val = val.Elem()\n }\n if val.Kind() != reflect.Struct {\n return\n }\n field := val.FieldByName("Portals")\n if field.IsValid() && field.CanSet() {\n field.Set(reflect.ValueOf(collector))\n }\n}\n\n// getStringField extracts a string field from a struct using reflection.\nfunc setBoolField(v interface{}, fieldName string, val bool) {\n rv := reflect.ValueOf(v)\n if rv.Kind() == reflect.Ptr {\n rv = rv.Elem()\n }\n if rv.Kind() != reflect.Struct {\n return\n }\n field := rv.FieldByName(fieldName)\n if field.IsValid() && field.CanSet() && field.Kind() == reflect.Bool {\n field.SetBool(val)\n }\n}\n\nfunc getBoolField(v interface{}, fieldName string) bool {\n val := reflect.ValueOf(v)\n if val.Kind() == reflect.Ptr {\n val = val.Elem()\n }\n if val.Kind() != reflect.Struct {\n return false\n }\n field := val.FieldByName(fieldName)\n if !field.IsValid() || field.Kind() != reflect.Bool {\n return false\n }\n return field.Bool()\n}\n\nfunc getStringField(v interface{}, fieldName string) string {\n val := reflect.ValueOf(v)\n if val.Kind() == reflect.Ptr {\n val = val.Elem()\n }\n if val.Kind() != reflect.Struct {\n return ""\n }\n field := val.FieldByName(fieldName)\n if !field.IsValid() || field.Kind() != reflect.String {\n return ""\n }\n return field.String()\n}\n\n// findChildComponentSlices finds slice fields containing child component props.\n// Child props are identified by having ScopeID and Scripts fields.\nfunc findChildComponentSlices(props interface{}) []interface{} {\n var result []interface{}\n\n val := reflect.ValueOf(props)\n if val.Kind() == reflect.Ptr {\n val = val.Elem()\n }\n if val.Kind() != reflect.Struct {\n return result\n }\n\n for i := 0; i < val.NumField(); i++ {\n field := val.Field(i)\n if field.Kind() != reflect.Slice || field.Len() == 0 {\n continue\n }\n\n elem := field.Index(0)\n if elem.Kind() == reflect.Ptr {\n elem = elem.Elem()\n }\n if elem.Kind() != reflect.Struct {\n continue\n }\n\n hasScopeID := elem.FieldByName("ScopeID").IsValid()\n hasScripts := elem.FieldByName("Scripts").IsValid()\n\n if hasScopeID && hasScripts {\n result = append(result, field.Interface())\n }\n }\n\n return result\n}\n\n// setScriptsOnSlice sets Scripts on all items in a slice.\nfunc setScriptsOnSlice(slice interface{}, collector *ScriptCollector) {\n val := reflect.ValueOf(slice)\n if val.Kind() != reflect.Slice {\n return\n }\n for i := 0; i < val.Len(); i++ {\n item := val.Index(i)\n if item.Kind() == reflect.Ptr {\n item = item.Elem()\n }\n if item.Kind() == reflect.Struct {\n field := item.FieldByName("Scripts")\n if field.IsValid() && field.CanSet() {\n field.Set(reflect.ValueOf(collector))\n }\n }\n }\n}\n\n// setBoolOnSlice sets a bool field on all items in a slice.\nfunc setBoolOnSlice(slice interface{}, fieldName string, val bool) {\n v := reflect.ValueOf(slice)\n if v.Kind() != reflect.Slice {\n return\n }\n for i := 0; i < v.Len(); i++ {\n item := v.Index(i)\n if item.Kind() == reflect.Ptr {\n item = item.Elem()\n }\n if item.Kind() == reflect.Struct {\n field := item.FieldByName(fieldName)\n if field.IsValid() && field.CanSet() && field.Kind() == reflect.Bool {\n field.SetBool(val)\n }\n }\n }\n}\n\n// setPortalsOnSlice sets Portals on all items in a slice.\nfunc setPortalsOnSlice(slice interface{}, collector *PortalCollector) {\n val := reflect.ValueOf(slice)\n if val.Kind() != reflect.Slice {\n return\n }\n for i := 0; i < val.Len(); i++ {\n item := val.Index(i)\n if item.Kind() == reflect.Ptr {\n item = item.Elem()\n }\n if item.Kind() == reflect.Struct {\n field := item.FieldByName("Portals")\n if field.IsValid() && field.CanSet() {\n field.Set(reflect.ValueOf(collector))\n }\n }\n }\n}\n\n\n// findSingleChildComponents finds single struct fields containing child component props.\n// Child props are identified by having ScopeID and Scripts fields.\nfunc findSingleChildComponents(props interface{}) []interface{} {\n var result []interface{}\n\n val := reflect.ValueOf(props)\n if val.Kind() == reflect.Ptr {\n val = val.Elem()\n }\n if val.Kind() != reflect.Struct {\n return result\n }\n\n for i := 0; i < val.NumField(); i++ {\n field := val.Field(i)\n\n // Handle pointer to struct\n if field.Kind() == reflect.Ptr {\n if field.IsNil() {\n continue\n }\n field = field.Elem()\n }\n\n // Skip non-struct fields (slices handled by findChildComponentSlices)\n if field.Kind() != reflect.Struct {\n continue\n }\n\n hasScopeID := field.FieldByName("ScopeID").IsValid()\n hasScripts := field.FieldByName("Scripts").IsValid()\n\n if hasScopeID && hasScripts {\n result = append(result, field.Addr().Interface())\n }\n }\n\n return result\n}\n\n// setScriptsOnSingle sets Scripts on a single struct child component.\nfunc setScriptsOnSingle(child interface{}, collector *ScriptCollector) {\n val := reflect.ValueOf(child)\n if val.Kind() == reflect.Ptr {\n val = val.Elem()\n }\n if val.Kind() == reflect.Struct {\n field := val.FieldByName("Scripts")\n if field.IsValid() && field.CanSet() {\n field.Set(reflect.ValueOf(collector))\n }\n }\n}\n\n// setPortalsOnSingle sets Portals on a single struct child component.\nfunc setPortalsOnSingle(child interface{}, collector *PortalCollector) {\n val := reflect.ValueOf(child)\n if val.Kind() == reflect.Ptr {\n val = val.Elem()\n }\n if val.Kind() == reflect.Struct {\n field := val.FieldByName("Portals")\n if field.IsValid() && field.CanSet() {\n field.Set(reflect.ValueOf(collector))\n }\n }\n}\n\n\n// =============================================================================\n// Internal Helpers\n// =============================================================================\n\nfunc toFloat64(v any) float64 {\n switch n := v.(type) {\n case int:\n return float64(n)\n case int8:\n return float64(n)\n case int16:\n return float64(n)\n case int32:\n return float64(n)\n case int64:\n return float64(n)\n case uint:\n return float64(n)\n case uint8:\n return float64(n)\n case uint16:\n return float64(n)\n case uint32:\n return float64(n)\n case uint64:\n return float64(n)\n case float32:\n return float64(n)\n case float64:\n return n\n default:\n return 0\n }\n}\n\nfunc toInt(v any) int {\n switch n := v.(type) {\n case int:\n return n\n case int8:\n return int(n)\n case int16:\n return int(n)\n case int32:\n return int(n)\n case int64:\n return int(n)\n case uint:\n return int(n)\n case uint8:\n return int(n)\n case uint16:\n return int(n)\n case uint32:\n return int(n)\n case uint64:\n return int(n)\n case float32:\n return int(n)\n case float64:\n return int(n)\n default:\n return 0\n }\n}\n\nfunc isIntLike(v any) bool {\n switch v.(type) {\n case int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64:\n return true\n default:\n return false\n }\n}\n\nfunc toString(v any) string {\n switch s := v.(type) {\n case string:\n return s\n case int:\n return strconv.Itoa(s)\n case int64:\n return strconv.FormatInt(s, 10)\n case float64:\n return strconv.FormatFloat(s, \'f\', -1, 64)\n case bool:\n return strconv.FormatBool(s)\n default:\n return ""\n }\n}\n';
22272
22373
  streamingGoSource = `// Package bf \u2014 Out-of-Order Streaming SSR helpers
22273
22374
  //
22274
22375
  // Provides StreamRenderer for progressive page rendering using HTTP
@@ -22475,7 +22576,7 @@ func StreamingFuncMap() template.FuncMap {
22475
22576
  }
22476
22577
  }
22477
22578
  `;
22478
- 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/\"/&quot;/gr;\n push @parts, qq{bf-h=\"$h\"};\n }\n if (defined $mount && length $mount) {\n my $m = $mount =~ s/\"/&quot;/gr;\n push @parts, qq{bf-m=\"$m\"};\n }\n unless ($self->_is_child) {\n push @parts, q{bf-r=\"\"};\n }\n return join(' ', @parts);\n}\n\nsub props_attr ($self) {\n my $props = $self->_props;\n return '' unless $props && %$props;\n # to_json returns a character string (not bytes) for safe embedding in templates\n my $json = to_json($props);\n return qq{ bf-p='$json'};\n}\n\n# ---------------------------------------------------------------------------\n# Comment Markers\n# ---------------------------------------------------------------------------\n\nsub comment ($self, $text) {\n return \"<!--bf-$text-->\";\n}\n\n# ---------------------------------------------------------------------------\n# JS-equivalent value stringification\n# ---------------------------------------------------------------------------\n\n# Map a Perl boolean-shaped value to the JS `String(bool)` form.\n# Used by the Mojo adapter when emitting reactive attribute bindings\n# whose JS source `isBooleanResultExpr` classified as boolean \u2014\n# a comparison (`count() > 0`), a logical negation (`!ok()`), or a\n# literal `true` / `false`. Perl's auto-stringification of those\n# expressions yields `''` / `1`; Hono and Go emit `'false'` / `'true'`.\n# Centralising the bool \u2192 string mapping here keeps the contract\n# testable and the template-emit syntax tidy\n# (`<%= bf->bool_str(...) %>` vs an inline ternary).\n#\n# Contract is boolean-only: callers must have classified the\n# expression as boolean-result before routing through this helper.\n# Non-boolean values reaching here will be Perl-truthy-coerced to\n# 'true' / 'false', which is generally wrong \u2014 non-boolean attribute\n# bindings stay on the plain `<%= expr %>` emit path and never reach\n# this function.\nsub bool_str ($self, $value) {\n return $value ? 'true' : 'false';\n}\n\nsub text_start ($self, $slot_id) {\n return \"<!--bf:$slot_id-->\";\n}\n\nsub text_end ($self) {\n return \"<!--/-->\";\n}\n\n# See spec/compiler.md \"Slot identity\" for the comment-scope wire format.\nsub scope_comment ($self) {\n my $scope_id = $self->_scope_id // '';\n my $host_segment = '';\n my $host = $self->_bf_parent;\n my $mount = $self->_bf_mount;\n if (defined $host && length $host) {\n $host_segment = \"|h=$host|m=\" . ($mount // '');\n }\n my $props_json = '';\n if ($self->_props && %{$self->_props}) {\n $props_json = '|' . to_json($self->_props);\n }\n return \"<!--bf-scope:$scope_id$host_segment$props_json-->\";\n}\n\n# ---------------------------------------------------------------------------\n# Script Registration\n# ---------------------------------------------------------------------------\n\nsub register_script ($self, $path) {\n return if $self->_script_seen->{$path};\n $self->_script_seen->{$path} = 1;\n push @{$self->_scripts}, $path;\n}\n\n# ---------------------------------------------------------------------------\n# Child Component Rendering\n# ---------------------------------------------------------------------------\n\nhas '_child_renderers' => sub { {} };\n\nsub register_child_renderer ($self, $name, $renderer) {\n $self->_child_renderers->{$name} = $renderer;\n}\n\nsub render_child ($self, $name, %props) {\n my $renderer = $self->_child_renderers->{$name};\n die \"No renderer registered for child component '$name'\" unless $renderer;\n # JSX children come in via Mojo `begin %>...<% end` capture, which\n # produces a CODE ref returning a Mojo::ByteStream. Materialize it\n # before handing the props to the child renderer so the child\n # template sees `$children` as already-rendered HTML.\n $props{children} = $props{children}->() if ref($props{children}) eq 'CODE';\n return $renderer->(\\%props);\n}\n\n# ---------------------------------------------------------------------------\n# Bulk registration from build manifest\n# ---------------------------------------------------------------------------\n#\n# `bf build` emits dist/templates/manifest.json describing every\n# component the page might invoke (Counter, ui/button/index, ...).\n# This helper walks that manifest and registers one child renderer per\n# UI registry entry \u2014 the path shape `ui/<name>/index` maps to the\n# `<name>` slot key Counter.html.ep and friends use via\n# `<%= bf->render_child('<name>', ...) %>`.\n#\n# Each manifest entry carries an `ssrDefaults` hash derived statically\n# from the component's JSX (prop destructure defaults + signal /\n# memo initial values, see packages/jsx/src/ssr-defaults.ts). The\n# child renderer seeds every template variable from that hash,\n# preferring the caller's matching prop where one exists. This\n# replaces the per-component `signal_init` callback that every\n# scaffold's `app.pl` used to hand-roll for items 1/3 of issue #1416.\n#\n# `signal_init` remains as an opt-in override for cases the static\n# extractor can't see through (e.g. signal initial values that\n# reference imported helpers). When supplied for a given slot key\n# it takes precedence over the manifest's `ssrDefaults` for that\n# child, allowing callers to mix manual overrides with auto-derived\n# defaults for siblings.\nsub register_components_from_manifest ($self, $manifest, %opts) {\n my $c = $self->c;\n my $signal_inits = $opts{signal_init} // {};\n my $parent_scope = $self->_scope_id;\n weaken(my $parent = $self);\n\n for my $entry_name (keys %$manifest) {\n # `__barefoot__` is the runtime entry, not a component.\n next if $entry_name eq '__barefoot__';\n # Only UI registry components (path shape `ui/<name>/index`)\n # become child renderers; top-level page components are the\n # render target rather than a child.\n next unless $entry_name =~ m{^ui/([^/]+)/index$};\n my $slot_key = $1;\n my $marked = $manifest->{$entry_name}{markedTemplate} // '';\n next unless $marked;\n # `templates/ui/button/index.html.ep` \u2192 `ui/button/index`\n my $template_name = $marked;\n $template_name =~ s{^templates/}{};\n $template_name =~ s{\\.html\\.ep$}{};\n\n my $signal_init = $signal_inits->{$slot_key};\n my $manifest_defaults = $manifest->{$entry_name}{ssrDefaults};\n $self->register_child_renderer($slot_key, sub {\n my ($props) = @_;\n my $child_bf = BarefootJS->new($c, {});\n my $slot_id = delete $props->{_bf_slot};\n $child_bf->_scope_id(\n $slot_id ? $parent_scope . '_' . $slot_id\n : $template_name . '_' . substr(rand() =~ s/^0\\.//r, 0, 6)\n );\n $child_bf->_is_child(1);\n # (#1249) Slot identity: host scope + slot id. Emitted as\n # bf-h / bf-m attributes by hydration_attrs.\n if ($slot_id) {\n $child_bf->_bf_parent($parent_scope);\n $child_bf->_bf_mount($slot_id);\n }\n $child_bf->_scripts($parent->_scripts);\n $child_bf->_script_seen($parent->_script_seen);\n\n my %extra;\n if ($signal_init) {\n %extra = $signal_init->($props);\n } elsif ($manifest_defaults) {\n %extra = _derive_stash_from_defaults($manifest_defaults, $props);\n }\n\n my $prev = $c->stash->{'bf.instance'};\n $c->stash->{'bf.instance'} = $child_bf;\n my $html = $c->render_to_string(\n template => $template_name, %$props, %extra,\n );\n $c->stash->{'bf.instance'} = $prev;\n chomp $html;\n return $html;\n });\n }\n}\n\n# Derive template-stash kvs from a manifest entry's `ssrDefaults`\n# section. Each entry shape:\n# { value => <static-fallback>, propName => <prop>, isRestProps => bool }\n# For `isRestProps`, the rest bag passes through unchanged (or the\n# static `{}` if the caller didn't supply one). For ordinary entries\n# the caller's `$props->{propName}` wins when defined, otherwise the\n# static `value` does. `propName`-less entries (signal / memo locals)\n# always use the static value \u2014 the caller cannot override them.\nsub _derive_stash_from_defaults ($defaults, $props) {\n my %extra;\n for my $name (keys %$defaults) {\n my $d = $defaults->{$name};\n if (ref($d) ne 'HASH') {\n $extra{$name} = $d;\n next;\n }\n if ($d->{isRestProps}) {\n $extra{$name} = exists $props->{$name} ? $props->{$name} : $d->{value};\n next;\n }\n my $prop_name = $d->{propName};\n if (defined $prop_name && exists $props->{$prop_name} && defined $props->{$prop_name}) {\n $extra{$name} = $props->{$prop_name};\n } else {\n $extra{$name} = $d->{value};\n }\n }\n return %extra;\n}\n\n# ---------------------------------------------------------------------------\n# Script Output\n# ---------------------------------------------------------------------------\n\nsub scripts ($self) {\n my @tags;\n for my $path (@{$self->_scripts}) {\n push @tags, qq{<script type=\"module\" src=\"$path\"></script>};\n }\n return join(\"\\n\", @tags);\n}\n\n# ---------------------------------------------------------------------------\n# Streaming SSR (Out-of-Order)\n# ---------------------------------------------------------------------------\n\nsub streaming_bootstrap ($self) {\n return q{<script>(function(){function s(id){var a=document.querySelector('[bf-async=\"'+id+'\"]');var t=document.querySelector('template[bf-async-resolve=\"'+id+'\"]');if(!a||!t)return;a.replaceChildren(t.content.cloneNode(true));a.removeAttribute('bf-async');t.remove();requestAnimationFrame(function(){if(window.__bf_hydrate)window.__bf_hydrate()})};window.__bf_swap=s})()</script>};\n}\n\nsub async_boundary ($self, $id, $fallback_html) {\n # The fallback comes in via Mojo `begin %>...<% end` capture (see\n # MojoAdapter::renderAsync), which produces a CODE ref returning a\n # Mojo::ByteStream. Materialize it so the rendered HTML embeds in\n # the placeholder rather than the CODE ref's stringification.\n $fallback_html = $fallback_html->() if ref($fallback_html) eq 'CODE';\n return qq{<div bf-async=\"$id\">$fallback_html</div>};\n}\n\nsub async_resolve ($self, $id, $content_html) {\n return qq{<template bf-async-resolve=\"$id\">$content_html</template><script>__bf_swap(\"$id\")</script>};\n}\n\n# ---------------------------------------------------------------------------\n# JS-compat callees (#1189) \u2014 invoked from generated Mojo templates as\n# <%= bf->json($val) %>, <%= bf->floor($val) %>, etc. The MojoAdapter's\n# `templatePrimitives` registry emits these helper calls in place of the\n# corresponding JS callees (`JSON.stringify`, `Math.floor`, \u2026) so the SSR\n# template can render value-equivalent output without a JS engine.\n#\n# Failure policy mirrors the Go adapter (#1188): user-data marshalling\n# (json) bubbles errors so Mojolicious aborts loudly on cycles /\n# unsupported values rather than silently producing an empty payload.\n# Numeric coercion follows JS semantics (NaN propagates as the special\n# string 'NaN'; non-numeric input returns 'NaN' rather than 0). Strings\n# always coerce to a string representation.\n# ---------------------------------------------------------------------------\n\nsub json ($self, $value) {\n # Mojo::JSON::to_json returns a character string (not bytes), suitable\n # for embedding in HTML output via Mojo::ByteStream / `<%==`.\n #\n # Documented divergence from JS: JS distinguishes `null` (renders as\n # \"null\") from `undefined` (`JSON.stringify(undefined)` returns the\n # JS value `undefined`, not a string). Perl has no such distinction\n # \u2014 both map to `undef`. We choose the `null` rendering for SSR\n # ergonomics: an unset prop becomes the string \"null\" rather than\n # the literal text \"undefined\" or an empty attribute. Matches the\n # `null` case of JS exactly; diverges from the `undefined` case.\n return to_json($value);\n}\n\nsub string ($self, $value) {\n # JS `String(v)` mirror. `undef` renders as the empty string here so\n # an unset prop doesn't surface as a literal \"undefined\" / \"null\"\n # in user-facing HTML \u2014 same divergence the Go adapter documents\n # for `bf_string`.\n return defined $value ? \"$value\" : '';\n}\n\nsub number ($self, $value) {\n # JS `Number(v)` mirror. Numeric coerces via Perl's implicit\n # numeric context; non-numeric / undef yield real numeric NaN\n # (`'nan' + 0`) so downstream arithmetic propagates correctly\n # (`Math.floor(NaN) === NaN`). Returning the literal string\n # \"NaN\" would conflate the user-passing-the-string-\"NaN\" case\n # with the parse-failure case, and break NaN detection in\n # downstream helpers.\n return 0 + 'nan' unless defined $value;\n return $value + 0 if looks_like_number($value);\n return 0 + 'nan';\n}\n\n# NaN is the only float for which `$x != $x` holds. Used as the\n# portable sentinel check in floor/ceil/round.\nsub _is_nan { my $n = shift; return $n != $n }\n\nsub floor ($self, $value) {\n my $n = $self->number($value);\n return $n if _is_nan($n);\n return POSIX::floor($n);\n}\n\nsub ceil ($self, $value) {\n my $n = $self->number($value);\n return $n if _is_nan($n);\n return POSIX::ceil($n);\n}\n\nsub round ($self, $value) {\n my $n = $self->number($value);\n return $n if _is_nan($n);\n # POSIX has no `round`. JS `Math.round` rounds half toward\n # +Infinity (so `Math.round(-1.5) === -1`, not -2). `floor(n\n # + 0.5)` reproduces that for both signs.\n return POSIX::floor($n + 0.5);\n}\n\n# ---------------------------------------------------------------------------\n# Array / String method helpers (#1448 Tier A)\n# ---------------------------------------------------------------------------\n#\n# `Array.prototype.includes(x)` and `String.prototype.includes(sub)`\n# share a method name in JS; the JSX parser can't tell the two\n# receiver shapes apart without TS type inference, so both lower to\n# the same IR node (`array-method` / method `includes`). This helper\n# dispatches at the Perl level via `ref()`:\n# - ARRAY ref: scan elements with `eq`; one defined-vs-undef\n# hop matches JS's `===` for null/undefined.\n# - scalar: `index($recv, $sub) != -1`, with both args\n# coerced through `// ''` so an undef receiver /\n# needle doesn't trip Perl's substr warning.\n# Anything else (HASH ref, code ref) returns false \u2014 matches the\n# JS semantic where `.includes` is only defined on Array /\n# TypedArray / String.\n\nsub includes ($self, $recv, $elem) {\n if (ref($recv) eq 'ARRAY') {\n for my $item (@$recv) {\n if (!defined $item) {\n return 1 if !defined $elem;\n next;\n }\n return 1 if defined $elem && $item eq $elem;\n }\n return 0;\n }\n return 0 if ref($recv);\n return index($recv // '', $elem // '') != -1 ? 1 : 0;\n}\n\n# `Array.prototype.indexOf(x)` / `Array.prototype.lastIndexOf(x)`\n# value-equality search (#1448 Tier A). Returns the 0-based position\n# of the first / last matching element, or -1 if not found.\n# Non-array receivers return -1 \u2014 matches the JS semantic that\n# `.indexOf` / `.lastIndexOf` are only defined on Array / TypedArray.\n# (The string-position `indexOf` form isn't in Tier A; if it lands\n# later the helper can grow a ref()-dispatch branch like `includes`.)\n\nsub _array_index_of ($recv, $elem, $reverse) {\n return -1 unless ref($recv) eq 'ARRAY';\n my @indices = $reverse ? (reverse 0 .. $#{$recv}) : (0 .. $#{$recv});\n for my $i (@indices) {\n my $item = $recv->[$i];\n if (!defined $item) {\n return $i if !defined $elem;\n next;\n }\n return $i if defined $elem && $item eq $elem;\n }\n return -1;\n}\n\nsub index_of ($self, $recv, $elem) {\n return _array_index_of($recv, $elem, 0);\n}\n\nsub last_index_of ($self, $recv, $elem) {\n return _array_index_of($recv, $elem, 1);\n}\n\n# `Array.prototype.at(i)` \u2014 supports negative indices (`.at(-1)` is\n# the last element); out-of-bounds returns undef (which Mojo's\n# auto-escape renders as the empty string, matching JS's `undefined`).\n# Non-array receivers return undef. Matches the Go `bf_at` arithmetic\n# (`length + i` for i < 0) so adapter output stays symmetric.\n\nsub at ($self, $recv, $i) {\n return undef unless ref($recv) eq 'ARRAY';\n return undef if !defined $i;\n my $len = scalar @$recv;\n return undef if $len == 0;\n my $idx = $i < 0 ? $len + $i : $i;\n return undef if $idx < 0 || $idx >= $len;\n return $recv->[$idx];\n}\n\n# `Array.prototype.concat(other)` \u2014 merges two arrays in order\n# into a new ARRAY ref. Non-array operands collapse to empty\n# (matches the Go `bf_concat` semantic so cross-adapter output\n# stays symmetric; differs from JS where a non-Array argument\n# with `Symbol.isConcatSpreadable` would be spread, a behaviour\n# the template-language path never observes).\n\nsub concat ($self, $a, $b) {\n my @out;\n push @out, @$a if ref($a) eq 'ARRAY';\n push @out, @$b if ref($b) eq 'ARRAY';\n return \\@out;\n}\n\n# `Array.prototype.slice(start, end?)` \u2014 carves out a sub-range\n# into a new ARRAY ref. Mirrors the Go `bf_slice` arithmetic so\n# adapter output stays symmetric:\n# - start < 0 \u2192 length + start (e.g. -1 = last index)\n# - end < 0 \u2192 length + end\n# - start < 0 after clamp \u2192 0\n# - end > length \u2192 length\n# - start >= end \u2192 empty\n# - end undef \u2192 \"to length\"\n# Non-array receivers return an empty ARRAY ref.\n\nsub slice ($self, $recv, $start, $end) {\n return [] unless ref($recv) eq 'ARRAY';\n my $len = scalar @$recv;\n return [] if $len == 0;\n\n my $s = $start // 0;\n $s = $len + $s if $s < 0;\n $s = 0 if $s < 0;\n $s = $len if $s > $len;\n\n my $e = defined $end ? $end : $len;\n $e = $len + $e if $e < 0;\n $e = 0 if $e < 0;\n $e = $len if $e > $len;\n\n return [] if $s >= $e;\n return [ @{$recv}[$s .. $e - 1] ];\n}\n\n# `Array.prototype.reverse()` / `Array.prototype.toReversed()` \u2014\n# both shapes share this lowering. SSR templates render a snapshot\n# of state, so JS's mutate-receiver (`reverse`) vs\n# return-new-array (`toReversed`) distinction has no template-\n# level meaning. Always returns a new ARRAY ref to keep callers\n# safe from accidental aliasing. Non-array receivers return an\n# empty ARRAY ref.\n\nsub reverse ($self, $recv) {\n return [] unless ref($recv) eq 'ARRAY';\n return [ reverse @$recv ];\n}\n\n# `String.prototype.trim()` \u2014 strip leading + trailing whitespace.\n# JS's `String.prototype.trim` matches `\\s` in the Unicode sense\n# (any whitespace including non-breaking space U+00A0); Perl's `\\s`\n# inside a regex with `/u` flag is the same. Undef receivers return\n# the empty string (matches JS's `String(undefined).trim()` which\n# would be \"undefined\" \u2192 \"undefined\", but in our template context\n# undef commonly means \"missing prop\"; rendering the empty string\n# is the safer choice and mirrors the JS-compat divergence we\n# already document for `bf->string(undef) === \"\"`).\n\nsub trim ($self, $recv) {\n return '' unless defined $recv;\n return '' if ref($recv);\n my $s = \"$recv\";\n $s =~ s/^\\s+|\\s+$//gu;\n return $s;\n}\n\n# `Array.prototype.sort(cmp)` / `Array.prototype.toSorted(cmp)`\n# lowering (#1448 Tier B). Non-mutating \u2014 JS's mutate-vs-new\n# distinction is moot in SSR template context.\n#\n# Opts hash-ref (compiler emits exactly these four keys):\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'\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# (and reversed-operand variants for `desc`).\n#\n# A future `nulls => 'first' | 'last'` knob can land alongside\n# without 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 my $key_kind = $opts->{key_kind} // 'self';\n my $key = $opts->{key} // '';\n my $compare_type = $opts->{compare_type} // 'numeric';\n my $direction = $opts->{direction} // 'asc';\n\n # Schwartzian transform: project each item to its sort key once,\n # then sort by key, then drop the keys. Cheaper than re-resolving\n # the field accessor inside every comparison for non-trivial arrays.\n my @keyed = map {\n my $item = $_;\n my $k = $key_kind eq 'field' && ref($item) eq 'HASH' ? $item->{$key} : $item;\n [$k, $item]\n } @$recv;\n\n my $cmp;\n if ($compare_type eq 'string') {\n $cmp = $direction eq 'desc'\n ? sub { ($b->[0] // '') cmp ($a->[0] // '') }\n : sub { ($a->[0] // '') cmp ($b->[0] // '') };\n } else {\n # Numeric: undef projects to 0 so the sort is total without\n # warnings on missing fields. Documented divergence from JS\n # (which would coerce undef \u2192 NaN and produce indeterminate\n # ordering); matching Go's `toFloat64(nil) == 0` keeps the\n # adapter outputs symmetric.\n $cmp = $direction eq 'desc'\n ? sub { ($b->[0] // 0) <=> ($a->[0] // 0) }\n : sub { ($a->[0] // 0) <=> ($b->[0] // 0) };\n }\n\n my @sorted = sort $cmp @keyed;\n return [ map { $_->[1] } @sorted ];\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 # `&#34;` / `&#39;` 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/&/&amp;/g;\n $s =~ s/</&lt;/g;\n $s =~ s/>/&gt;/g;\n $s =~ s/\"/&#34;/g;\n $s =~ s/'/&#39;/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";
22579
+ 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/\"/&quot;/gr;\n push @parts, qq{bf-h=\"$h\"};\n }\n if (defined $mount && length $mount) {\n my $m = $mount =~ s/\"/&quot;/gr;\n push @parts, qq{bf-m=\"$m\"};\n }\n unless ($self->_is_child) {\n push @parts, q{bf-r=\"\"};\n }\n return join(' ', @parts);\n}\n\nsub props_attr ($self) {\n my $props = $self->_props;\n return '' unless $props && %$props;\n # to_json returns a character string (not bytes) for safe embedding in templates\n my $json = to_json($props);\n return qq{ bf-p='$json'};\n}\n\n# ---------------------------------------------------------------------------\n# Comment Markers\n# ---------------------------------------------------------------------------\n\nsub comment ($self, $text) {\n return \"<!--bf-$text-->\";\n}\n\n# ---------------------------------------------------------------------------\n# JS-equivalent value stringification\n# ---------------------------------------------------------------------------\n\n# Map a Perl boolean-shaped value to the JS `String(bool)` form.\n# Used by the Mojo adapter when emitting reactive attribute bindings\n# whose JS source `isBooleanResultExpr` classified as boolean \u2014\n# a comparison (`count() > 0`), a logical negation (`!ok()`), or a\n# literal `true` / `false`. Perl's auto-stringification of those\n# expressions yields `''` / `1`; Hono and Go emit `'false'` / `'true'`.\n# Centralising the bool \u2192 string mapping here keeps the contract\n# testable and the template-emit syntax tidy\n# (`<%= bf->bool_str(...) %>` vs an inline ternary).\n#\n# Contract is boolean-only: callers must have classified the\n# expression as boolean-result before routing through this helper.\n# Non-boolean values reaching here will be Perl-truthy-coerced to\n# 'true' / 'false', which is generally wrong \u2014 non-boolean attribute\n# bindings stay on the plain `<%= expr %>` emit path and never reach\n# this function.\nsub bool_str ($self, $value) {\n return $value ? 'true' : 'false';\n}\n\nsub text_start ($self, $slot_id) {\n return \"<!--bf:$slot_id-->\";\n}\n\nsub text_end ($self) {\n return \"<!--/-->\";\n}\n\n# See spec/compiler.md \"Slot identity\" for the comment-scope wire format.\nsub scope_comment ($self) {\n my $scope_id = $self->_scope_id // '';\n my $host_segment = '';\n my $host = $self->_bf_parent;\n my $mount = $self->_bf_mount;\n if (defined $host && length $host) {\n $host_segment = \"|h=$host|m=\" . ($mount // '');\n }\n my $props_json = '';\n if ($self->_props && %{$self->_props}) {\n $props_json = '|' . to_json($self->_props);\n }\n return \"<!--bf-scope:$scope_id$host_segment$props_json-->\";\n}\n\n# ---------------------------------------------------------------------------\n# Script Registration\n# ---------------------------------------------------------------------------\n\nsub register_script ($self, $path) {\n return if $self->_script_seen->{$path};\n $self->_script_seen->{$path} = 1;\n push @{$self->_scripts}, $path;\n}\n\n# ---------------------------------------------------------------------------\n# Child Component Rendering\n# ---------------------------------------------------------------------------\n\nhas '_child_renderers' => sub { {} };\n\nsub register_child_renderer ($self, $name, $renderer) {\n $self->_child_renderers->{$name} = $renderer;\n}\n\nsub render_child ($self, $name, %props) {\n my $renderer = $self->_child_renderers->{$name};\n die \"No renderer registered for child component '$name'\" unless $renderer;\n # JSX children come in via Mojo `begin %>...<% end` capture, which\n # produces a CODE ref returning a Mojo::ByteStream. Materialize it\n # before handing the props to the child renderer so the child\n # template sees `$children` as already-rendered HTML.\n $props{children} = $props{children}->() if ref($props{children}) eq 'CODE';\n return $renderer->(\\%props);\n}\n\n# ---------------------------------------------------------------------------\n# Bulk registration from build manifest\n# ---------------------------------------------------------------------------\n#\n# `bf build` emits dist/templates/manifest.json describing every\n# component the page might invoke (Counter, ui/button/index, ...).\n# This helper walks that manifest and registers one child renderer per\n# UI registry entry \u2014 the path shape `ui/<name>/index` maps to the\n# `<name>` slot key Counter.html.ep and friends use via\n# `<%= bf->render_child('<name>', ...) %>`.\n#\n# Each manifest entry carries an `ssrDefaults` hash derived statically\n# from the component's JSX (prop destructure defaults + signal /\n# memo initial values, see packages/jsx/src/ssr-defaults.ts). The\n# child renderer seeds every template variable from that hash,\n# preferring the caller's matching prop where one exists. This\n# replaces the per-component `signal_init` callback that every\n# scaffold's `app.pl` used to hand-roll for items 1/3 of issue #1416.\n#\n# `signal_init` remains as an opt-in override for cases the static\n# extractor can't see through (e.g. signal initial values that\n# reference imported helpers). When supplied for a given slot key\n# it takes precedence over the manifest's `ssrDefaults` for that\n# child, allowing callers to mix manual overrides with auto-derived\n# defaults for siblings.\nsub register_components_from_manifest ($self, $manifest, %opts) {\n my $c = $self->c;\n my $signal_inits = $opts{signal_init} // {};\n my $parent_scope = $self->_scope_id;\n weaken(my $parent = $self);\n\n for my $entry_name (keys %$manifest) {\n # `__barefoot__` is the runtime entry, not a component.\n next if $entry_name eq '__barefoot__';\n # Only UI registry components (path shape `ui/<name>/index`)\n # become child renderers; top-level page components are the\n # render target rather than a child.\n next unless $entry_name =~ m{^ui/([^/]+)/index$};\n my $slot_key = $1;\n my $marked = $manifest->{$entry_name}{markedTemplate} // '';\n next unless $marked;\n # `templates/ui/button/index.html.ep` \u2192 `ui/button/index`\n my $template_name = $marked;\n $template_name =~ s{^templates/}{};\n $template_name =~ s{\\.html\\.ep$}{};\n\n my $signal_init = $signal_inits->{$slot_key};\n my $manifest_defaults = $manifest->{$entry_name}{ssrDefaults};\n $self->register_child_renderer($slot_key, sub {\n my ($props) = @_;\n my $child_bf = BarefootJS->new($c, {});\n my $slot_id = delete $props->{_bf_slot};\n $child_bf->_scope_id(\n $slot_id ? $parent_scope . '_' . $slot_id\n : $template_name . '_' . substr(rand() =~ s/^0\\.//r, 0, 6)\n );\n $child_bf->_is_child(1);\n # (#1249) Slot identity: host scope + slot id. Emitted as\n # bf-h / bf-m attributes by hydration_attrs.\n if ($slot_id) {\n $child_bf->_bf_parent($parent_scope);\n $child_bf->_bf_mount($slot_id);\n }\n $child_bf->_scripts($parent->_scripts);\n $child_bf->_script_seen($parent->_script_seen);\n\n my %extra;\n if ($signal_init) {\n %extra = $signal_init->($props);\n } elsif ($manifest_defaults) {\n %extra = _derive_stash_from_defaults($manifest_defaults, $props);\n }\n\n my $prev = $c->stash->{'bf.instance'};\n $c->stash->{'bf.instance'} = $child_bf;\n my $html = $c->render_to_string(\n template => $template_name, %$props, %extra,\n );\n $c->stash->{'bf.instance'} = $prev;\n chomp $html;\n return $html;\n });\n }\n}\n\n# Derive template-stash kvs from a manifest entry's `ssrDefaults`\n# section. Each entry shape:\n# { value => <static-fallback>, propName => <prop>, isRestProps => bool }\n# For `isRestProps`, the rest bag passes through unchanged (or the\n# static `{}` if the caller didn't supply one). For ordinary entries\n# the caller's `$props->{propName}` wins when defined, otherwise the\n# static `value` does. `propName`-less entries (signal / memo locals)\n# always use the static value \u2014 the caller cannot override them.\nsub _derive_stash_from_defaults ($defaults, $props) {\n my %extra;\n for my $name (keys %$defaults) {\n my $d = $defaults->{$name};\n if (ref($d) ne 'HASH') {\n $extra{$name} = $d;\n next;\n }\n if ($d->{isRestProps}) {\n $extra{$name} = exists $props->{$name} ? $props->{$name} : $d->{value};\n next;\n }\n my $prop_name = $d->{propName};\n if (defined $prop_name && exists $props->{$prop_name} && defined $props->{$prop_name}) {\n $extra{$name} = $props->{$prop_name};\n } else {\n $extra{$name} = $d->{value};\n }\n }\n return %extra;\n}\n\n# ---------------------------------------------------------------------------\n# Script Output\n# ---------------------------------------------------------------------------\n\nsub scripts ($self) {\n my @tags;\n for my $path (@{$self->_scripts}) {\n push @tags, qq{<script type=\"module\" src=\"$path\"></script>};\n }\n return join(\"\\n\", @tags);\n}\n\n# ---------------------------------------------------------------------------\n# Streaming SSR (Out-of-Order)\n# ---------------------------------------------------------------------------\n\nsub streaming_bootstrap ($self) {\n return q{<script>(function(){function s(id){var a=document.querySelector('[bf-async=\"'+id+'\"]');var t=document.querySelector('template[bf-async-resolve=\"'+id+'\"]');if(!a||!t)return;a.replaceChildren(t.content.cloneNode(true));a.removeAttribute('bf-async');t.remove();requestAnimationFrame(function(){if(window.__bf_hydrate)window.__bf_hydrate()})};window.__bf_swap=s})()</script>};\n}\n\nsub async_boundary ($self, $id, $fallback_html) {\n # The fallback comes in via Mojo `begin %>...<% end` capture (see\n # MojoAdapter::renderAsync), which produces a CODE ref returning a\n # Mojo::ByteStream. Materialize it so the rendered HTML embeds in\n # the placeholder rather than the CODE ref's stringification.\n $fallback_html = $fallback_html->() if ref($fallback_html) eq 'CODE';\n return qq{<div bf-async=\"$id\">$fallback_html</div>};\n}\n\nsub async_resolve ($self, $id, $content_html) {\n return qq{<template bf-async-resolve=\"$id\">$content_html</template><script>__bf_swap(\"$id\")</script>};\n}\n\n# ---------------------------------------------------------------------------\n# JS-compat callees (#1189) \u2014 invoked from generated Mojo templates as\n# <%= bf->json($val) %>, <%= bf->floor($val) %>, etc. The MojoAdapter's\n# `templatePrimitives` registry emits these helper calls in place of the\n# corresponding JS callees (`JSON.stringify`, `Math.floor`, \u2026) so the SSR\n# template can render value-equivalent output without a JS engine.\n#\n# Failure policy mirrors the Go adapter (#1188): user-data marshalling\n# (json) bubbles errors so Mojolicious aborts loudly on cycles /\n# unsupported values rather than silently producing an empty payload.\n# Numeric coercion follows JS semantics (NaN propagates as the special\n# string 'NaN'; non-numeric input returns 'NaN' rather than 0). Strings\n# always coerce to a string representation.\n# ---------------------------------------------------------------------------\n\nsub json ($self, $value) {\n # Mojo::JSON::to_json returns a character string (not bytes), suitable\n # for embedding in HTML output via Mojo::ByteStream / `<%==`.\n #\n # Documented divergence from JS: JS distinguishes `null` (renders as\n # \"null\") from `undefined` (`JSON.stringify(undefined)` returns the\n # JS value `undefined`, not a string). Perl has no such distinction\n # \u2014 both map to `undef`. We choose the `null` rendering for SSR\n # ergonomics: an unset prop becomes the string \"null\" rather than\n # the literal text \"undefined\" or an empty attribute. Matches the\n # `null` case of JS exactly; diverges from the `undefined` case.\n return to_json($value);\n}\n\nsub string ($self, $value) {\n # JS `String(v)` mirror. `undef` renders as the empty string here so\n # an unset prop doesn't surface as a literal \"undefined\" / \"null\"\n # in user-facing HTML \u2014 same divergence the Go adapter documents\n # for `bf_string`.\n return defined $value ? \"$value\" : '';\n}\n\nsub number ($self, $value) {\n # JS `Number(v)` mirror. Numeric coerces via Perl's implicit\n # numeric context; non-numeric / undef yield real numeric NaN\n # (`'nan' + 0`) so downstream arithmetic propagates correctly\n # (`Math.floor(NaN) === NaN`). Returning the literal string\n # \"NaN\" would conflate the user-passing-the-string-\"NaN\" case\n # with the parse-failure case, and break NaN detection in\n # downstream helpers.\n return 0 + 'nan' unless defined $value;\n return $value + 0 if looks_like_number($value);\n return 0 + 'nan';\n}\n\n# NaN is the only float for which `$x != $x` holds. Used as the\n# portable sentinel check in floor/ceil/round.\nsub _is_nan { my $n = shift; return $n != $n }\n\nsub floor ($self, $value) {\n my $n = $self->number($value);\n return $n if _is_nan($n);\n return POSIX::floor($n);\n}\n\nsub ceil ($self, $value) {\n my $n = $self->number($value);\n return $n if _is_nan($n);\n return POSIX::ceil($n);\n}\n\nsub round ($self, $value) {\n my $n = $self->number($value);\n return $n if _is_nan($n);\n # POSIX has no `round`. JS `Math.round` rounds half toward\n # +Infinity (so `Math.round(-1.5) === -1`, not -2). `floor(n\n # + 0.5)` reproduces that for both signs.\n return POSIX::floor($n + 0.5);\n}\n\n# ---------------------------------------------------------------------------\n# Array / String method helpers (#1448 Tier A)\n# ---------------------------------------------------------------------------\n#\n# `Array.prototype.includes(x)` and `String.prototype.includes(sub)`\n# share a method name in JS; the JSX parser can't tell the two\n# receiver shapes apart without TS type inference, so both lower to\n# the same IR node (`array-method` / method `includes`). This helper\n# dispatches at the Perl level via `ref()`:\n# - ARRAY ref: scan elements with `eq`; one defined-vs-undef\n# hop matches JS's `===` for null/undefined.\n# - scalar: `index($recv, $sub) != -1`, with both args\n# coerced through `// ''` so an undef receiver /\n# needle doesn't trip Perl's substr warning.\n# Anything else (HASH ref, code ref) returns false \u2014 matches the\n# JS semantic where `.includes` is only defined on Array /\n# TypedArray / String.\n\nsub includes ($self, $recv, $elem) {\n if (ref($recv) eq 'ARRAY') {\n for my $item (@$recv) {\n if (!defined $item) {\n return 1 if !defined $elem;\n next;\n }\n return 1 if defined $elem && $item eq $elem;\n }\n return 0;\n }\n return 0 if ref($recv);\n return index($recv // '', $elem // '') != -1 ? 1 : 0;\n}\n\n# `Array.prototype.indexOf(x)` / `Array.prototype.lastIndexOf(x)`\n# value-equality search (#1448 Tier A). Returns the 0-based position\n# of the first / last matching element, or -1 if not found.\n# Non-array receivers return -1 \u2014 matches the JS semantic that\n# `.indexOf` / `.lastIndexOf` are only defined on Array / TypedArray.\n# (The string-position `indexOf` form isn't in Tier A; if it lands\n# later the helper can grow a ref()-dispatch branch like `includes`.)\n\nsub _array_index_of ($recv, $elem, $reverse) {\n return -1 unless ref($recv) eq 'ARRAY';\n my @indices = $reverse ? (reverse 0 .. $#{$recv}) : (0 .. $#{$recv});\n for my $i (@indices) {\n my $item = $recv->[$i];\n if (!defined $item) {\n return $i if !defined $elem;\n next;\n }\n return $i if defined $elem && $item eq $elem;\n }\n return -1;\n}\n\nsub index_of ($self, $recv, $elem) {\n return _array_index_of($recv, $elem, 0);\n}\n\nsub last_index_of ($self, $recv, $elem) {\n return _array_index_of($recv, $elem, 1);\n}\n\n# `Array.prototype.at(i)` \u2014 supports negative indices (`.at(-1)` is\n# the last element); out-of-bounds returns undef (which Mojo's\n# auto-escape renders as the empty string, matching JS's `undefined`).\n# Non-array receivers return undef. Matches the Go `bf_at` arithmetic\n# (`length + i` for i < 0) so adapter output stays symmetric.\n\nsub at ($self, $recv, $i) {\n return undef unless ref($recv) eq 'ARRAY';\n return undef if !defined $i;\n my $len = scalar @$recv;\n return undef if $len == 0;\n my $idx = $i < 0 ? $len + $i : $i;\n return undef if $idx < 0 || $idx >= $len;\n return $recv->[$idx];\n}\n\n# `Array.prototype.concat(other)` \u2014 merges two arrays in order\n# into a new ARRAY ref. Non-array operands collapse to empty\n# (matches the Go `bf_concat` semantic so cross-adapter output\n# stays symmetric; differs from JS where a non-Array argument\n# with `Symbol.isConcatSpreadable` would be spread, a behaviour\n# the template-language path never observes).\n\nsub concat ($self, $a, $b) {\n my @out;\n push @out, @$a if ref($a) eq 'ARRAY';\n push @out, @$b if ref($b) eq 'ARRAY';\n return \\@out;\n}\n\n# `Array.prototype.slice(start, end?)` \u2014 carves out a sub-range\n# into a new ARRAY ref. Mirrors the Go `bf_slice` arithmetic so\n# adapter output stays symmetric:\n# - start < 0 \u2192 length + start (e.g. -1 = last index)\n# - end < 0 \u2192 length + end\n# - start < 0 after clamp \u2192 0\n# - end > length \u2192 length\n# - start >= end \u2192 empty\n# - end undef \u2192 \"to length\"\n# Non-array receivers return an empty ARRAY ref.\n\nsub slice ($self, $recv, $start, $end) {\n return [] unless ref($recv) eq 'ARRAY';\n my $len = scalar @$recv;\n return [] if $len == 0;\n\n my $s = $start // 0;\n $s = $len + $s if $s < 0;\n $s = 0 if $s < 0;\n $s = $len if $s > $len;\n\n my $e = defined $end ? $end : $len;\n $e = $len + $e if $e < 0;\n $e = 0 if $e < 0;\n $e = $len if $e > $len;\n\n return [] if $s >= $e;\n return [ @{$recv}[$s .. $e - 1] ];\n}\n\n# `Array.prototype.reverse()` / `Array.prototype.toReversed()` \u2014\n# both shapes share this lowering. SSR templates render a snapshot\n# of state, so JS's mutate-receiver (`reverse`) vs\n# return-new-array (`toReversed`) distinction has no template-\n# level meaning. Always returns a new ARRAY ref to keep callers\n# safe from accidental aliasing. Non-array receivers return an\n# empty ARRAY ref.\n\nsub reverse ($self, $recv) {\n return [] unless ref($recv) eq 'ARRAY';\n return [ reverse @$recv ];\n}\n\n# `String.prototype.trim()` \u2014 strip leading + trailing whitespace.\n# JS's `String.prototype.trim` matches `\\s` in the Unicode sense\n# (any whitespace including non-breaking space U+00A0); Perl's `\\s`\n# inside a regex with `/u` flag is the same. Undef receivers return\n# the empty string (matches JS's `String(undefined).trim()` which\n# would be \"undefined\" \u2192 \"undefined\", but in our template context\n# undef commonly means \"missing prop\"; rendering the empty string\n# is the safer choice and mirrors the JS-compat divergence we\n# already document for `bf->string(undef) === \"\"`).\n\nsub trim ($self, $recv) {\n return '' unless defined $recv;\n return '' if ref($recv);\n my $s = \"$recv\";\n $s =~ s/^\\s+|\\s+$//gu;\n return $s;\n}\n\n# `Array.prototype.sort(cmp)` / `Array.prototype.toSorted(cmp)`\n# lowering (#1448 Tier B). Non-mutating \u2014 JS's mutate-vs-new\n# distinction is moot in SSR template context.\n#\n# Opts hash-ref. The compiler emits a `keys` list of per-key hashes\n# in priority order; each hash carries:\n#\n# key_kind => 'self' | 'field'\n# key => '' when key_kind eq 'self'; field name verbatim\n# from the comparator AST (e.g. 'price', 'createdAt')\n# when key_kind eq 'field' \u2014 no case normalisation\n# applied. Perl hash lookups are case-sensitive so\n# the key here must match the actual hash key the\n# user populated.\n# compare_type => 'numeric' | 'string' | 'auto'\n# direction => 'asc' | 'desc'\n#\n# Accepted comparator catalogue (gated upstream at parse time \u2014\n# anything outside refuses with BF101 before reaching this helper):\n#\n# (a,b) => a.f - b.f \u2192 field, numeric\n# (a,b) => a - b \u2192 self, numeric\n# (a,b) => a[.f].localeCompare(b[.f]) \u2192 field|self, string\n# (a,b) => a.f > b.f ? 1 : -1 \u2192 field|self, auto\n# any of the above ||-chained \u2192 multi-key tie-breaks\n# (and reversed-operand variants for `desc`).\n#\n# `auto` (relational-ternary lowering) compares numerically when both\n# keys `looks_like_number`, else lexically \u2014 Go's `bf_sort` applies the\n# same rule so the two template adapters stay byte-equal.\n#\n# A future `nulls => 'first' | 'last'` knob can land per key without\n# churn \u2014 the opts hash is the right place to grow.\n\nsub sort ($self, $recv, $opts = {}) {\n return [] unless ref($recv) eq 'ARRAY';\n\n # Normalise the per-key specs (priority order, length >= 1).\n my @spec = map {\n {\n key_kind => $_->{key_kind} // 'self',\n key => $_->{key} // '',\n compare_type => $_->{compare_type} // 'numeric',\n direction => $_->{direction} // 'asc',\n }\n } @{ $opts->{keys} // [] };\n return [ @$recv ] unless @spec;\n\n # Schwartzian transform: project each item to all its sort keys\n # once, then compare projected keys. Cheaper than re-resolving the\n # field accessors inside every comparison for non-trivial arrays.\n my @keyed = map {\n my $item = $_;\n my @ks = map {\n $_->{key_kind} eq 'field' && ref($item) eq 'HASH' ? $item->{ $_->{key} } : $item;\n } @spec;\n [ \\@ks, $item ];\n } @$recv;\n\n my $cmp = sub {\n for my $i (0 .. $#spec) {\n my $sp = $spec[$i];\n my $c = _compare_sort_key($a->[0][$i], $b->[0][$i], $sp->{compare_type});\n next if $c == 0; # tie on this key \u2014 try the next\n return $sp->{direction} eq 'desc' ? -$c : $c;\n }\n return 0;\n };\n\n my @sorted = sort $cmp @keyed;\n return [ map { $_->[1] } @sorted ];\n}\n\n# Compare two projected keys, ascending orientation (-1 / 0 / 1); the\n# caller negates for 'desc'. 'auto' compares numerically when both\n# keys look like numbers, else lexically (matches Go's `bf_sort`).\n# undef coalesces to '' / 0 so the order stays total without warnings.\nsub _compare_sort_key ($av, $bv, $compare_type) {\n if ($compare_type eq 'string') {\n return ($av // '') cmp ($bv // '');\n }\n if ($compare_type eq 'auto') {\n if (looks_like_number($av // '') && looks_like_number($bv // '')) {\n return ($av // 0) <=> ($bv // 0);\n }\n return ($av // '') cmp ($bv // '');\n }\n return ($av // 0) <=> ($bv // 0); # numeric\n}\n\n# ---------------------------------------------------------------------------\n# JSX intrinsic-element spread (#1407)\n# ---------------------------------------------------------------------------\n#\n# Mirrors the JS `spreadAttrs` runtime\n# (`packages/client/src/runtime/spread-attrs.ts`) and the Go adapter's\n# `bf.SpreadAttrs` so SSR output stays byte-equal across the three\n# adapters. Generated Mojo templates invoke this as\n# `<%== bf->spread_attrs($bag) %>`.\n#\n# Skip rules: nil/false values, event handlers (`on[A-Z]\u2026` shape\n# matching JS `key[2] === key[2].toUpperCase()` \u2014 true for any\n# character whose uppercase is itself, including digits and\n# underscore), `children`. `ref` is intentionally NOT filtered,\n# matching the JS reference.\n#\n# Key remap: className \u2192 class, htmlFor \u2192 for; SVG camelCase\n# attrs preserved (case-sensitive XML spec); other camelCase keys\n# lowered to kebab-case with a leading `-` for an initial\n# uppercase letter (mirrors JS `key.replace(/([A-Z])/g, '-$1')`).\n#\n# `style` is routed through `_style_to_css` so object literals\n# serialise to a real CSS string instead of Perl's default\n# `HASH(0x...)` form.\n#\n# Output is deterministic: keys are sorted alphabetically before\n# emission, matching the Go adapter's `sort.Strings(keys)` policy\n# and Mojo::JSON's marshal order.\n#\n# The return value is a Mojo::ByteStream so the calling template's\n# `<%==` raw-emit skips re-escaping (the helper has already\n# HTML-escaped each value).\n\nmy %SVG_CAMEL_CASE_ATTRS = map { $_ => 1 } qw(\n allowReorder attributeName attributeType autoReverse\n baseFrequency baseProfile calcMode clipPathUnits\n contentScriptType contentStyleType diffuseConstant edgeMode\n externalResourcesRequired filterRes filterUnits glyphRef\n gradientTransform gradientUnits kernelMatrix kernelUnitLength\n keyPoints keySplines keyTimes lengthAdjust limitingConeAngle\n markerHeight markerUnits markerWidth maskContentUnits\n maskUnits numOctaves pathLength patternContentUnits\n patternTransform patternUnits pointsAtX pointsAtY pointsAtZ\n preserveAlpha preserveAspectRatio primitiveUnits refX refY\n repeatCount repeatDur requiredExtensions requiredFeatures\n specularConstant specularExponent spreadMethod startOffset\n stdDeviation stitchTiles surfaceScale systemLanguage\n tableValues targetX targetY textLength viewBox viewTarget\n xChannelSelector yChannelSelector zoomAndPan\n);\n\nsub _to_attr_name ($key) {\n return 'class' if $key eq 'className';\n return 'for' if $key eq 'htmlFor';\n return $key if $SVG_CAMEL_CASE_ATTRS{$key};\n # camelCase \u2192 kebab-case, with a leading `-` for an initial\n # uppercase letter (JS-reference parity, even though that case\n # produces an HTML-invalid attribute name \u2014 same documented\n # behaviour as the Go adapter's `toAttrName`).\n my $out = $key;\n $out =~ s/([A-Z])/-\\L$1/g;\n return $out;\n}\n\nsub _html_escape ($value) {\n # HTML attribute-value escape for SSR string emission. The\n # spread bag's values reach the browser as part of a generated\n # `key=\"...\"` substring inside the rendered HTML, so the\n # escape set has to cover everything that could break either\n # the surrounding double-quoted attribute or the enclosing\n # tag: `&`, `<`, `>`, `\"`, and `'`. Matches Go's\n # `template.HTMLEscapeString` semantics byte-for-byte (using\n # `&#34;` / `&#39;` 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/&/&amp;/g;\n $s =~ s/</&lt;/g;\n $s =~ s/>/&gt;/g;\n $s =~ s/\"/&#34;/g;\n $s =~ s/'/&#39;/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";
22479
22580
  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";
22480
22581
  barefootDevReloadPmSource = `package Mojolicious::Plugin::BarefootJS::DevReload;
22481
22582
  use Mojo::Base 'Mojolicious::Plugin', -signatures;
@@ -23875,7 +23976,7 @@ async function select(args2) {
23875
23976
  output.write(`\x1B[33m?\x1B[0m \x1B[1m${args2.message}\x1B[0m
23876
23977
  `);
23877
23978
  render(true);
23878
- return new Promise((resolve10, reject) => {
23979
+ return new Promise((resolve11, reject) => {
23879
23980
  readline.emitKeypressEvents(input);
23880
23981
  input.setRawMode?.(true);
23881
23982
  input.resume();
@@ -23920,7 +24021,7 @@ async function select(args2) {
23920
24021
  output.write(`\x1B[${totalLines}A`);
23921
24022
  output.write(`\u2714 ${args2.message} \x1B[1;32m${shortLabel}\x1B[0m
23922
24023
  `);
23923
- resolve10(args2.options[cursor].value);
24024
+ resolve11(args2.options[cursor].value);
23924
24025
  return;
23925
24026
  }
23926
24027
  };
@@ -24823,21 +24924,21 @@ var init_scaffold_layout = __esm({
24823
24924
  import { mkdir as mkdir2, readFile as readFile2, writeFile as writeFile2 } from "node:fs/promises";
24824
24925
  import { existsSync as existsSync12 } from "node:fs";
24825
24926
  import { execFileSync } from "node:child_process";
24826
- import { resolve as resolve6, relative as relative3 } from "node:path";
24927
+ import { resolve as resolve7, relative as relative3 } from "node:path";
24827
24928
  import { build as build2 } from "esbuild";
24828
24929
  async function compile(options) {
24829
24930
  const { assets, previewsPath, previewNames, componentName, liveReload } = options;
24830
24931
  const { rootDir, srcComponentsDir, tokensCss, globalsCss, runtimeStandalone, uno } = assets;
24831
- const DIST_DIR = resolve6(rootDir, ".preview-dist");
24832
- const MODULES_DIR = resolve6(DIST_DIR, "_modules");
24932
+ const DIST_DIR = resolve7(rootDir, ".preview-dist");
24933
+ const MODULES_DIR = resolve7(DIST_DIR, "_modules");
24833
24934
  await mkdir2(MODULES_DIR, { recursive: true });
24834
24935
  console.log("Generating CSS...");
24835
- await writeFile2(resolve6(DIST_DIR, "globals.css"), tokensCss + "\n" + globalsCss);
24936
+ await writeFile2(resolve7(DIST_DIR, "globals.css"), tokensCss + "\n" + globalsCss);
24836
24937
  console.log("Generated: .preview-dist/globals.css");
24837
24938
  console.log("Generating UnoCSS...");
24838
24939
  let unoConfigPath = uno.configPath;
24839
24940
  if (uno.configIsBundled) {
24840
- unoConfigPath = resolve6(DIST_DIR, "uno.config.ts");
24941
+ unoConfigPath = resolve7(DIST_DIR, "uno.config.ts");
24841
24942
  await writeFile2(unoConfigPath, await readFile2(uno.configPath, "utf-8"));
24842
24943
  }
24843
24944
  execFileSync(uno.bin, [
@@ -24845,7 +24946,7 @@ async function compile(options) {
24845
24946
  "--config",
24846
24947
  unoConfigPath,
24847
24948
  "-o",
24848
- resolve6(DIST_DIR, "uno.css")
24949
+ resolve7(DIST_DIR, "uno.css")
24849
24950
  ], { cwd: uno.cwd, stdio: "inherit" });
24850
24951
  console.log("Generated: .preview-dist/uno.css");
24851
24952
  const previewSource = await readFile2(previewsPath, "utf-8");
@@ -24855,7 +24956,7 @@ async function compile(options) {
24855
24956
  const componentFiles = resolveDependenciesFromSource(
24856
24957
  [componentName, ...previewSiblings],
24857
24958
  srcComponentsDir
24858
- ).map((name) => resolve6(srcComponentsDir, name, "index.tsx")).filter(existsSync12);
24959
+ ).map((name) => resolve7(srcComponentsDir, name, "index.tsx")).filter(existsSync12);
24859
24960
  console.log(`Compiling ${componentFiles.length + 1} files...`);
24860
24961
  const allFiles = [...componentFiles, previewsPath];
24861
24962
  const adapter = new PreviewCsrAdapter();
@@ -24891,16 +24992,16 @@ async function compile(options) {
24891
24992
  const allModules = new Map([...clientJsByKey, ...combined]);
24892
24993
  for (const [key, content] of allModules) {
24893
24994
  const safeName = key.replace(/[/\\]/g, "__") + ".js";
24894
- await writeFile2(resolve6(MODULES_DIR, safeName), content);
24995
+ await writeFile2(resolve7(MODULES_DIR, safeName), content);
24895
24996
  }
24896
24997
  const previewModuleFile = previewKey.replace(/[/\\]/g, "__") + ".js";
24897
24998
  const entrySource = generateEntryScript(previewModuleFile, previewNames);
24898
- const entryPath = resolve6(DIST_DIR, "_entry.js");
24999
+ const entryPath = resolve7(DIST_DIR, "_entry.js");
24899
25000
  await writeFile2(entryPath, entrySource);
24900
25001
  console.log("Bundling for browser...");
24901
25002
  await build2({
24902
25003
  entryPoints: [entryPath],
24903
- outfile: resolve6(DIST_DIR, "_bundle.js"),
25004
+ outfile: resolve7(DIST_DIR, "_bundle.js"),
24904
25005
  bundle: true,
24905
25006
  format: "esm",
24906
25007
  platform: "browser",
@@ -24911,7 +25012,7 @@ async function compile(options) {
24911
25012
  define: { "process.env.NODE_ENV": '"development"' }
24912
25013
  });
24913
25014
  console.log("Generated: .preview-dist/_bundle.js");
24914
- await writeFile2(resolve6(DIST_DIR, "index.html"), generateHTML(componentName, liveReload));
25015
+ await writeFile2(resolve7(DIST_DIR, "index.html"), generateHTML(componentName, liveReload));
24915
25016
  console.log("Generated: .preview-dist/index.html");
24916
25017
  return { distDir: DIST_DIR };
24917
25018
  }
@@ -25077,7 +25178,7 @@ var init_compile = __esm({
25077
25178
  // src/lib/tokens.ts
25078
25179
  import { readFile as readFile3 } from "node:fs/promises";
25079
25180
  import { existsSync as existsSync13 } from "node:fs";
25080
- import { resolve as resolve7, dirname as dirname4 } from "node:path";
25181
+ import { resolve as resolve8, dirname as dirname4 } from "node:path";
25081
25182
  import { fileURLToPath as fileURLToPath4 } from "node:url";
25082
25183
  async function loadTokens(jsonPath) {
25083
25184
  const content = await readFile3(jsonPath, "utf-8");
@@ -25152,15 +25253,15 @@ ${darkLines.join("\n")}
25152
25253
  return css;
25153
25254
  }
25154
25255
  function findBaseTokensJson(ctx2) {
25155
- const monorepoTokens = resolve7(ctx2.root, "site/shared/tokens/tokens.json");
25256
+ const monorepoTokens = resolve8(ctx2.root, "site/shared/tokens/tokens.json");
25156
25257
  if (existsSync13(monorepoTokens)) return monorepoTokens;
25157
- const bundledTokens = resolve7(dirname4(thisFile2), "tokens.json");
25258
+ const bundledTokens = resolve8(dirname4(thisFile2), "tokens.json");
25158
25259
  if (existsSync13(bundledTokens)) return bundledTokens;
25159
25260
  return null;
25160
25261
  }
25161
25262
  async function loadTokenSet(ctx2) {
25162
25263
  if (ctx2.projectDir && ctx2.config?.paths.tokens) {
25163
- const userTokens = resolve7(ctx2.projectDir, ctx2.config.paths.tokens, "tokens.json");
25264
+ const userTokens = resolve8(ctx2.projectDir, ctx2.config.paths.tokens, "tokens.json");
25164
25265
  if (await fileExists(userTokens)) return loadTokens(userTokens);
25165
25266
  }
25166
25267
  const basePath = findBaseTokensJson(ctx2);
@@ -25170,7 +25271,7 @@ async function loadTokenSet(ctx2) {
25170
25271
  );
25171
25272
  }
25172
25273
  const base = await loadTokens(basePath);
25173
- const uiJsonPath = resolve7(ctx2.root, "site/ui/tokens.json");
25274
+ const uiJsonPath = resolve8(ctx2.root, "site/ui/tokens.json");
25174
25275
  if (await fileExists(uiJsonPath)) {
25175
25276
  return mergeTokenSets(base, await loadTokens(uiJsonPath));
25176
25277
  }
@@ -25201,33 +25302,33 @@ var init_errors2 = __esm({
25201
25302
  // src/lib/preview/assets.ts
25202
25303
  import { existsSync as existsSync14 } from "node:fs";
25203
25304
  import { readFile as readFile4 } from "node:fs/promises";
25204
- import { resolve as resolve8, dirname as dirname5 } from "node:path";
25305
+ import { resolve as resolve9, dirname as dirname5 } from "node:path";
25205
25306
  import { fileURLToPath as fileURLToPath5 } from "node:url";
25206
25307
  function firstExisting(...candidates) {
25207
25308
  return candidates.find((c) => !!c && existsSync14(c));
25208
25309
  }
25209
25310
  function unoBinCandidates(dir) {
25210
- return ["unocss", "unocss.cmd", "unocss.CMD"].map((n) => resolve8(dir, "node_modules/.bin", n));
25311
+ return ["unocss", "unocss.cmd", "unocss.CMD"].map((n) => resolve9(dir, "node_modules/.bin", n));
25211
25312
  }
25212
25313
  async function resolvePreviewAssets(ctx2) {
25213
25314
  const monorepo = ctx2.config === null;
25214
25315
  const projectDir = ctx2.projectDir;
25215
25316
  const rootDir = projectDir ?? ctx2.root;
25216
- const srcComponentsDir = monorepo ? resolve8(ctx2.root, "ui/components/ui") : resolve8(projectDir, ctx2.config.paths.components);
25317
+ const srcComponentsDir = monorepo ? resolve9(ctx2.root, "ui/components/ui") : resolve9(projectDir, ctx2.config.paths.components);
25217
25318
  const tokensCss = await loadTokensCss(ctx2);
25218
25319
  const globalsPath = firstExisting(
25219
- projectDir && resolve8(projectDir, "styles/globals.css"),
25220
- projectDir && resolve8(projectDir, "globals.css"),
25221
- projectDir && resolve8(projectDir, "app/globals.css"),
25222
- monorepo ? resolve8(ctx2.root, "site/ui/styles/globals.css") : void 0,
25223
- resolve8(assetDir, "preview-globals.css")
25320
+ projectDir && resolve9(projectDir, "styles/globals.css"),
25321
+ projectDir && resolve9(projectDir, "globals.css"),
25322
+ projectDir && resolve9(projectDir, "app/globals.css"),
25323
+ monorepo ? resolve9(ctx2.root, "site/ui/styles/globals.css") : void 0,
25324
+ resolve9(assetDir, "preview-globals.css")
25224
25325
  );
25225
25326
  const globalsCss = globalsPath ? await readFile4(globalsPath, "utf-8") : "";
25226
- const bundledUnoConfig = resolve8(assetDir, "preview-uno.config.ts");
25327
+ const bundledUnoConfig = resolve9(assetDir, "preview-uno.config.ts");
25227
25328
  const configPath = firstExisting(
25228
- projectDir && resolve8(projectDir, "uno.config.ts"),
25229
- projectDir && resolve8(projectDir, "uno.config.js"),
25230
- monorepo ? resolve8(ctx2.root, "site/ui/uno.config.ts") : void 0,
25329
+ projectDir && resolve9(projectDir, "uno.config.ts"),
25330
+ projectDir && resolve9(projectDir, "uno.config.js"),
25331
+ monorepo ? resolve9(ctx2.root, "site/ui/uno.config.ts") : void 0,
25231
25332
  bundledUnoConfig
25232
25333
  );
25233
25334
  if (!configPath) {
@@ -25235,11 +25336,11 @@ async function resolvePreviewAssets(ctx2) {
25235
25336
  "No UnoCSS config found and the bundled default is missing \u2014 reinstall @barefootjs/cli."
25236
25337
  );
25237
25338
  }
25238
- const unoCwd = monorepo ? resolve8(ctx2.root, "site/ui") : rootDir;
25239
- const globs = monorepo ? ["../../ui/components/**/*.tsx", "./**/*.tsx", "./dist/**/*.tsx"] : [resolve8(srcComponentsDir, "**/*.tsx")];
25339
+ const unoCwd = monorepo ? resolve9(ctx2.root, "site/ui") : rootDir;
25340
+ const globs = monorepo ? ["../../ui/components/**/*.tsx", "./**/*.tsx", "./dist/**/*.tsx"] : [resolve9(srcComponentsDir, "**/*.tsx")];
25240
25341
  const unoBin = firstExisting(
25241
25342
  ...unoBinCandidates(rootDir),
25242
- ...monorepo ? unoBinCandidates(resolve8(ctx2.root, "site/ui")) : [],
25343
+ ...monorepo ? unoBinCandidates(resolve9(ctx2.root, "site/ui")) : [],
25243
25344
  ...monorepo ? unoBinCandidates(ctx2.root) : []
25244
25345
  );
25245
25346
  if (!unoBin) {
@@ -25248,9 +25349,9 @@ async function resolvePreviewAssets(ctx2) {
25248
25349
  );
25249
25350
  }
25250
25351
  const runtimeStandalone = firstExisting(
25251
- monorepo ? resolve8(ctx2.root, "packages/client/dist/runtime/standalone.js") : void 0,
25252
- resolve8(rootDir, "node_modules/@barefootjs/client/dist/runtime/standalone.js"),
25253
- resolve8(rootDir, "node_modules/@barefootjs/client/dist/runtime/index.js")
25352
+ monorepo ? resolve9(ctx2.root, "packages/client/dist/runtime/standalone.js") : void 0,
25353
+ resolve9(rootDir, "node_modules/@barefootjs/client/dist/runtime/standalone.js"),
25354
+ resolve9(rootDir, "node_modules/@barefootjs/client/dist/runtime/index.js")
25254
25355
  );
25255
25356
  if (!runtimeStandalone) {
25256
25357
  throw new PreviewError(
@@ -25549,11 +25650,11 @@ var init_preview_generate = __esm({
25549
25650
  });
25550
25651
 
25551
25652
  // src/lib/preview/run.ts
25552
- import { resolve as resolve9, relative as relative4 } from "node:path";
25653
+ import { resolve as resolve10, relative as relative4 } from "node:path";
25553
25654
  import { readFileSync as readFileSync6, writeFileSync as writeFileSync4, existsSync as existsSync15 } from "node:fs";
25554
25655
  async function runPreview(componentName, ctx2, opts = {}) {
25555
25656
  const assets = await resolvePreviewAssets(ctx2);
25556
- const previewsPath = resolve9(assets.srcComponentsDir, componentName, "index.preview.tsx");
25657
+ const previewsPath = resolve10(assets.srcComponentsDir, componentName, "index.preview.tsx");
25557
25658
  if (!existsSync15(previewsPath)) {
25558
25659
  try {
25559
25660
  const meta = loadComponent(ctx2.metaDir, componentName);
@@ -25725,7 +25826,7 @@ async function run8(args2, ctx2) {
25725
25826
  Serving ${server.url}`);
25726
25827
  if (!opts.watch) {
25727
25828
  console.log(" Press Ctrl+C to stop.");
25728
- await new Promise((resolve10) => process.on("SIGINT", resolve10));
25829
+ await new Promise((resolve11) => process.on("SIGINT", resolve11));
25729
25830
  server.close();
25730
25831
  return;
25731
25832
  }
@@ -25775,7 +25876,7 @@ async function run8(args2, ctx2) {
25775
25876
  const watchers = watchTargets.map(
25776
25877
  (target) => fsWatch(target, { recursive: true }, schedule)
25777
25878
  );
25778
- await new Promise((resolve10) => process.on("SIGINT", resolve10));
25879
+ await new Promise((resolve11) => process.on("SIGINT", resolve11));
25779
25880
  for (const w of watchers) w.close();
25780
25881
  server.close();
25781
25882
  }
@@ -27259,9 +27360,9 @@ function findProjectConfig(startDir) {
27259
27360
  let dir = path.resolve(startDir);
27260
27361
  const { root: fsRoot } = path.parse(dir);
27261
27362
  while (true) {
27262
- const ts19 = path.join(dir, "barefoot.config.ts");
27263
- if (existsSync2(ts19)) {
27264
- return { dir, tsConfigPath: ts19 };
27363
+ const ts20 = path.join(dir, "barefoot.config.ts");
27364
+ if (existsSync2(ts20)) {
27365
+ return { dir, tsConfigPath: ts20 };
27265
27366
  }
27266
27367
  if (dir === fsRoot) return null;
27267
27368
  dir = path.dirname(dir);