@barefootjs/cli 0.7.0 → 0.9.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
@@ -10,9 +10,9 @@ var __export = (target, all) => {
10
10
  };
11
11
 
12
12
  // src/lib/config-loader.ts
13
- import { existsSync, unlinkSync } from "fs";
14
- import { dirname, resolve } from "path";
15
- import { pathToFileURL } from "url";
13
+ import { existsSync, unlinkSync } from "node:fs";
14
+ import { dirname, resolve } from "node:path";
15
+ import { pathToFileURL } from "node:url";
16
16
  function findBuildConfig(startDir) {
17
17
  const candidate = resolve(startDir, CONFIG_FILENAME);
18
18
  return existsSync(candidate) ? candidate : null;
@@ -75,10 +75,10 @@ var init_config_loader = __esm({
75
75
  });
76
76
 
77
77
  // src/lib/pm.ts
78
- import { existsSync as existsSync3 } from "fs";
79
- import path2 from "path";
78
+ import { existsSync as existsSync3 } from "node:fs";
79
+ import path2 from "node:path";
80
80
  function detectPackageManager(dir, env = process.env, versions = process.versions) {
81
- for (const pm of ["bun", "pnpm", "yarn", "npm"]) {
81
+ for (const pm of ["bun", "pnpm", "yarn", "npm", "deno"]) {
82
82
  for (const file of LOCKFILES[pm]) {
83
83
  if (existsSync3(path2.join(dir, file))) return pm;
84
84
  }
@@ -94,8 +94,10 @@ function detectInvokingPackageManager(env = process.env, versions = process.vers
94
94
  if (ua.startsWith("pnpm/")) return "pnpm";
95
95
  if (ua.startsWith("yarn/")) return "yarn";
96
96
  if (ua.startsWith("npm/")) return "npm";
97
+ if (ua.startsWith("deno/")) return "deno";
97
98
  }
98
99
  if (versions.bun) return "bun";
100
+ if (versions.deno) return "deno";
99
101
  return null;
100
102
  }
101
103
  function commandsFor(pm) {
@@ -121,6 +123,24 @@ function commandsFor(pm) {
121
123
  exec: (c) => `yarn dlx ${c}`,
122
124
  test: (p) => p ? `yarn test ${p}` : "yarn test"
123
125
  };
126
+ case "deno":
127
+ return {
128
+ // Deno 2 reads `package.json` (and `deno.json` tasks), so
129
+ // `deno install` materialises the same dependency tree the
130
+ // other PMs produce — no separate `deno cache` step needed.
131
+ install: "deno install",
132
+ // package.json scripts surface as Deno tasks, so `deno task
133
+ // <script>` mirrors `bun run <script>` / `pnpm <script>`.
134
+ run: (s) => `deno task ${s}`,
135
+ // One-shot binaries run straight from npm via the `npm:`
136
+ // specifier; `-A` grants the filesystem/network access `bf`
137
+ // needs to download and write components.
138
+ exec: (c) => `deno run -A npm:${c}`,
139
+ // The scaffold's `test` task forwards extra args without a `--`
140
+ // separator (like bun/pnpm/yarn), so the targeted form just
141
+ // appends the path.
142
+ test: (p) => p ? `deno task test ${p}` : "deno task test"
143
+ };
124
144
  case "npm":
125
145
  default:
126
146
  return {
@@ -156,7 +176,8 @@ var init_pm = __esm({
156
176
  bun: ["bun.lock", "bun.lockb"],
157
177
  pnpm: ["pnpm-lock.yaml"],
158
178
  yarn: ["yarn.lock"],
159
- npm: ["package-lock.json"]
179
+ npm: ["package-lock.json"],
180
+ deno: ["deno.lock", "deno.json", "deno.jsonc"]
160
181
  };
161
182
  }
162
183
  });
@@ -2665,7 +2686,7 @@ var init_analyzer_context = __esm({
2665
2686
  });
2666
2687
 
2667
2688
  // ../jsx/src/errors.ts
2668
- import path3 from "path";
2689
+ import path3 from "node:path";
2669
2690
  function createError(code, loc, options) {
2670
2691
  if (code === void 0 || !(code in errorMessages)) {
2671
2692
  throw new Error(
@@ -2848,8 +2869,8 @@ var init_errors = __esm({
2848
2869
 
2849
2870
  // ../jsx/src/analyzer.ts
2850
2871
  import ts7 from "typescript";
2851
- import path4 from "path";
2852
- import fs from "fs";
2872
+ import path4 from "node:path";
2873
+ import fs from "node:fs";
2853
2874
  function needsTypeBasedDetection(source) {
2854
2875
  if (REACTIVE_BRAND_PACKAGES.some((pkg) => source.includes(pkg))) return true;
2855
2876
  if (/\.map\s*\(/.test(source)) return true;
@@ -8196,11 +8217,11 @@ function extractLoopParamBindings(pattern) {
8196
8217
  });
8197
8218
  return;
8198
8219
  }
8199
- const path23 = `${prefix}[${index}]`;
8220
+ const path24 = `${prefix}[${index}]`;
8200
8221
  if (ts11.isIdentifier(el.name)) {
8201
- bindings.push({ name: el.name.text, path: path23 });
8222
+ bindings.push({ name: el.name.text, path: path24 });
8202
8223
  } else {
8203
- walk(el.name, path23);
8224
+ walk(el.name, path24);
8204
8225
  }
8205
8226
  }
8206
8227
  return;
@@ -8243,11 +8264,11 @@ function extractLoopParamBindings(pattern) {
8243
8264
  return;
8244
8265
  }
8245
8266
  collectedKeys.push({ key: keyText, isIdent: isIdent(keyText) });
8246
- const path23 = appendDotAccess(prefix, keyText);
8267
+ const path24 = appendDotAccess(prefix, keyText);
8247
8268
  if (ts11.isIdentifier(el.name)) {
8248
- bindings.push({ name: el.name.text, path: path23 });
8269
+ bindings.push({ name: el.name.text, path: path24 });
8249
8270
  } else {
8250
- walk(el.name, path23);
8271
+ walk(el.name, path24);
8251
8272
  }
8252
8273
  }
8253
8274
  };
@@ -8701,11 +8722,11 @@ function transformMapCall(node, ctx2, isClientOnly = false, method = "map") {
8701
8722
  if (stmt === returnStmt) break;
8702
8723
  const js = ctx2.getJS(stmt);
8703
8724
  const tjs = ctx2.getTemplateJS(stmt);
8704
- const ts21 = stmt.getText(ctx2.sourceFile);
8725
+ const ts22 = stmt.getText(ctx2.sourceFile);
8705
8726
  preambleStmts.push(js.endsWith(";") ? js : js + ";");
8706
8727
  templatePreambleStmts.push(tjs.endsWith(";") ? tjs : tjs + ";");
8707
- typedPreambleStmts.push(ts21.endsWith(";") ? ts21 : ts21 + ";");
8708
- if (js !== ts21) hasTypeDiff = true;
8728
+ typedPreambleStmts.push(ts22.endsWith(";") ? ts22 : ts22 + ";");
8729
+ if (js !== ts22) hasTypeDiff = true;
8709
8730
  if (js !== tjs) hasTemplateDiff = true;
8710
8731
  }
8711
8732
  if (preambleStmts.length > 0) {
@@ -16934,6 +16955,12 @@ function extractSsrDefaults(metadata) {
16934
16955
  out[metadata.restPropsName] = { isRestProps: true, value: {} };
16935
16956
  }
16936
16957
  const bindings = {};
16958
+ for (const c of metadata.localConstants ?? []) {
16959
+ if (!c.isModule || c.value === void 0) continue;
16960
+ if (c.name in bindings) continue;
16961
+ const v = tryStaticEval(c.value, { bindings, propsLike });
16962
+ if (v !== UNRESOLVED) bindings[c.name] = v;
16963
+ }
16937
16964
  for (const sig of metadata.signals) {
16938
16965
  if (!sig.getter || sig.isModule) continue;
16939
16966
  const value = tryStaticEval(sig.initialValue, { bindings, propsLike });
@@ -16979,8 +17006,22 @@ function evalNode(node, ctx2) {
16979
17006
  if (ts16.isTypeAssertionExpression(node)) return evalNode(node.expression, ctx2);
16980
17007
  if (ts16.isNonNullExpression(node)) return evalNode(node.expression, ctx2);
16981
17008
  if (ts16.isArrowFunction(node)) {
16982
- if (node.parameters.length === 0 && !ts16.isBlock(node.body)) {
16983
- return evalNode(node.body, ctx2);
17009
+ if (node.parameters.length !== 0) return UNRESOLVED;
17010
+ if (!ts16.isBlock(node.body)) return evalNode(node.body, ctx2);
17011
+ const localBindings = { ...ctx2.bindings };
17012
+ const localCtx = { ...ctx2, bindings: localBindings };
17013
+ for (const stmt of node.body.statements) {
17014
+ if (ts16.isVariableStatement(stmt)) {
17015
+ for (const d of stmt.declarationList.declarations) {
17016
+ if (!ts16.isIdentifier(d.name) || !d.initializer) continue;
17017
+ const v = evalNode(d.initializer, localCtx);
17018
+ if (v !== UNRESOLVED) localBindings[d.name.text] = v;
17019
+ }
17020
+ } else if (ts16.isReturnStatement(stmt)) {
17021
+ return stmt.expression ? evalNode(stmt.expression, localCtx) : UNRESOLVED;
17022
+ } else {
17023
+ return UNRESOLVED;
17024
+ }
16984
17025
  }
16985
17026
  return UNRESOLVED;
16986
17027
  }
@@ -17036,7 +17077,17 @@ function evalNode(node, ctx2) {
17036
17077
  }
17037
17078
  return arr;
17038
17079
  }
17039
- if (ts16.isPropertyAccessExpression(node) || ts16.isElementAccessExpression(node)) {
17080
+ if (ts16.isElementAccessExpression(node)) {
17081
+ const base = evalNode(node.expression, ctx2);
17082
+ if (base === void 0) return void 0;
17083
+ if (base === UNRESOLVED || base === null || typeof base !== "object") return UNRESOLVED;
17084
+ if (!node.argumentExpression) return UNRESOLVED;
17085
+ const key = evalNode(node.argumentExpression, ctx2);
17086
+ if (key === UNRESOLVED || key === void 0 || key === null) return UNRESOLVED;
17087
+ const k = String(key);
17088
+ return Object.prototype.hasOwnProperty.call(base, k) ? base[k] : void 0;
17089
+ }
17090
+ if (ts16.isPropertyAccessExpression(node)) {
17040
17091
  const baseResult = evalNode(node.expression, ctx2);
17041
17092
  if (baseResult === void 0) return void 0;
17042
17093
  return UNRESOLVED;
@@ -17045,6 +17096,19 @@ function evalNode(node, ctx2) {
17045
17096
  if (node.arguments.length === 0 && ts16.isIdentifier(node.expression) && node.expression.text in ctx2.bindings) {
17046
17097
  return ctx2.bindings[node.expression.text];
17047
17098
  }
17099
+ if (ts16.isPropertyAccessExpression(node.expression) && node.expression.name.text === "join") {
17100
+ const recv = evalNode(node.expression.expression, ctx2);
17101
+ if (Array.isArray(recv)) {
17102
+ let sep = ",";
17103
+ if (node.arguments.length >= 1) {
17104
+ const sepVal = evalNode(node.arguments[0], ctx2);
17105
+ if (typeof sepVal !== "string") return UNRESOLVED;
17106
+ sep = sepVal;
17107
+ }
17108
+ return recv.map((x) => x === null || x === void 0 ? "" : `${x}`).join(sep);
17109
+ }
17110
+ return UNRESOLVED;
17111
+ }
17048
17112
  return UNRESOLVED;
17049
17113
  }
17050
17114
  if (ts16.isConditionalExpression(node)) {
@@ -17117,6 +17181,39 @@ var init_ssr_defaults = __esm({
17117
17181
  });
17118
17182
 
17119
17183
  // ../jsx/src/compiler.ts
17184
+ function mergeTemplateImports(lines) {
17185
+ const result = [];
17186
+ const valueIdx = /* @__PURE__ */ new Map();
17187
+ const valueNames = /* @__PURE__ */ new Map();
17188
+ const typeIdx = /* @__PURE__ */ new Map();
17189
+ const typeNames = /* @__PURE__ */ new Map();
17190
+ const seenOther = /* @__PURE__ */ new Set();
17191
+ const fold = (src, rawNames, idx, names, render) => {
17192
+ if (!idx.has(src)) {
17193
+ idx.set(src, result.length);
17194
+ names.set(src, /* @__PURE__ */ new Set());
17195
+ result.push("");
17196
+ }
17197
+ const set = names.get(src);
17198
+ for (const n of rawNames.split(",").map((s) => s.trim()).filter(Boolean)) set.add(n);
17199
+ result[idx.get(src)] = render(src, set);
17200
+ };
17201
+ for (const raw of lines) {
17202
+ const line = raw.trim();
17203
+ if (!line) continue;
17204
+ const typeMatch = line.match(/^import\s+type\s*\{([^}]+)\}\s*from\s*['"]([^'"]+)['"]\s*;?$/);
17205
+ const valueMatch = line.match(/^import\s*\{([^}]+)\}\s*from\s*['"]([^'"]+)['"]\s*;?$/);
17206
+ if (valueMatch) {
17207
+ fold(valueMatch[2], valueMatch[1], valueIdx, valueNames, (s, n) => `import { ${[...n].join(", ")} } from '${s}'`);
17208
+ } else if (typeMatch) {
17209
+ fold(typeMatch[2], typeMatch[1], typeIdx, typeNames, (s, n) => `import type { ${[...n].join(", ")} } from '${s}'`);
17210
+ } else if (!seenOther.has(line)) {
17211
+ seenOther.add(line);
17212
+ result.push(line);
17213
+ }
17214
+ }
17215
+ return result.filter(Boolean).join("\n");
17216
+ }
17120
17217
  function compileMultipleComponents(source, filePath, componentNames, options) {
17121
17218
  const files = [];
17122
17219
  const errors = [];
@@ -17279,19 +17376,9 @@ function compileMultipleComponents(source, filePath, componentNames, options) {
17279
17376
  }
17280
17377
  return { files, errors };
17281
17378
  }
17282
- const seenImportLines = /* @__PURE__ */ new Set();
17283
- const uniqueImports = [];
17284
- for (const output of allOutputs) {
17285
- if (output.imports) {
17286
- for (const line of output.imports.split("\n")) {
17287
- if (line.trim() && !seenImportLines.has(line)) {
17288
- seenImportLines.add(line);
17289
- uniqueImports.push(line);
17290
- }
17291
- }
17292
- }
17293
- }
17294
- const mergedImports = uniqueImports.join("\n");
17379
+ const mergedImports = mergeTemplateImports(
17380
+ allOutputs.flatMap((o) => o.imports ? o.imports.split("\n") : [])
17381
+ );
17295
17382
  const seenTypes = /* @__PURE__ */ new Set();
17296
17383
  const uniqueTypes = [];
17297
17384
  for (const output of allOutputs) {
@@ -17605,7 +17692,7 @@ var init_compiler = __esm({
17605
17692
 
17606
17693
  // ../jsx/src/shared-program.ts
17607
17694
  import ts17 from "typescript";
17608
- import path5 from "path";
17695
+ import path5 from "node:path";
17609
17696
  function commonParent(paths) {
17610
17697
  if (paths.length === 0) return process.cwd();
17611
17698
  if (paths.length === 1) return path5.dirname(paths[0]);
@@ -18460,9 +18547,9 @@ function formatEventSummary(summary, graph) {
18460
18547
  if (updatedSignals.size > 0) {
18461
18548
  const targets = [];
18462
18549
  for (const sig of updatedSignals) {
18463
- const path23 = traceUpdatePath(graph, sig);
18464
- if (path23 && path23.dependents.length > 0) {
18465
- const downstream = flattenUpdateTargets(path23.dependents);
18550
+ const path24 = traceUpdatePath(graph, sig);
18551
+ if (path24 && path24.dependents.length > 0) {
18552
+ const downstream = flattenUpdateTargets(path24.dependents);
18466
18553
  targets.push(`${sig} -> ${downstream.join(", ")}`);
18467
18554
  }
18468
18555
  }
@@ -18872,10 +18959,10 @@ function formatComponentGraph(graph) {
18872
18959
  }
18873
18960
  return lines.join("\n");
18874
18961
  }
18875
- function formatUpdatePath(path23) {
18962
+ function formatUpdatePath(path24) {
18876
18963
  const lines = [];
18877
- lines.push(`${path23.target} (${path23.kind})`);
18878
- for (const entry of path23.dependents) {
18964
+ lines.push(`${path24.target} (${path24.kind})`);
18965
+ for (const entry of path24.dependents) {
18879
18966
  formatEntry(entry, lines, " ");
18880
18967
  }
18881
18968
  return lines.join("\n");
@@ -19352,6 +19439,207 @@ var init_debug = __esm({
19352
19439
  }
19353
19440
  });
19354
19441
 
19442
+ // ../jsx/src/augment-inherited-props.ts
19443
+ import ts19 from "typescript";
19444
+ function collectContextConsumers(metadata) {
19445
+ const constants = metadata.localConstants ?? [];
19446
+ const contextDefaults = /* @__PURE__ */ new Map();
19447
+ for (const c of constants) {
19448
+ if (c.systemConstructKind !== "createContext" || c.value === void 0) continue;
19449
+ contextDefaults.set(c.name, parseCreateContextDefault(c.value));
19450
+ }
19451
+ if (contextDefaults.size === 0) return [];
19452
+ const consumers = [];
19453
+ for (const c of constants) {
19454
+ if (c.value === void 0) continue;
19455
+ const ctxName = parseUseContextArg(c.value);
19456
+ if (ctxName === null || !contextDefaults.has(ctxName)) continue;
19457
+ consumers.push({
19458
+ localName: c.name,
19459
+ contextName: ctxName,
19460
+ defaultValue: contextDefaults.get(ctxName) ?? null
19461
+ });
19462
+ }
19463
+ return consumers;
19464
+ }
19465
+ function parseUseContextArg(source) {
19466
+ const expr = parseSingleExpression(source);
19467
+ if (!expr || !ts19.isCallExpression(expr)) return null;
19468
+ if (!ts19.isIdentifier(expr.expression) || expr.expression.text !== "useContext") return null;
19469
+ if (expr.arguments.length !== 1) return null;
19470
+ const arg = expr.arguments[0];
19471
+ return ts19.isIdentifier(arg) ? arg.text : null;
19472
+ }
19473
+ function parseCreateContextDefault(source) {
19474
+ const expr = parseSingleExpression(source);
19475
+ if (!expr || !ts19.isCallExpression(expr)) return null;
19476
+ if (expr.arguments.length === 0) return null;
19477
+ const arg = expr.arguments[0];
19478
+ if (ts19.isStringLiteral(arg) || ts19.isNoSubstitutionTemplateLiteral(arg)) return arg.text;
19479
+ if (ts19.isNumericLiteral(arg)) return Number(arg.text);
19480
+ if (arg.kind === ts19.SyntaxKind.TrueKeyword) return true;
19481
+ if (arg.kind === ts19.SyntaxKind.FalseKeyword) return false;
19482
+ return null;
19483
+ }
19484
+ function parseSingleExpression(source) {
19485
+ const sf = ts19.createSourceFile("__ctx.ts", `(${source})`, ts19.ScriptTarget.Latest, false);
19486
+ const stmt = sf.statements[0];
19487
+ if (!stmt || !ts19.isExpressionStatement(stmt)) return null;
19488
+ let e = stmt.expression;
19489
+ while (ts19.isParenthesizedExpression(e)) e = e.expression;
19490
+ return e;
19491
+ }
19492
+ function augmentInheritedPropAccesses(ir) {
19493
+ const propsObj = ir.metadata.propsObjectName;
19494
+ if (!propsObj) return;
19495
+ const existing = new Set(ir.metadata.propsParams.map((p) => p.name));
19496
+ const bareRefProps = /* @__PURE__ */ new Set();
19497
+ const booleanAttrProps = /* @__PURE__ */ new Set();
19498
+ const accessed = /* @__PURE__ */ new Set();
19499
+ const accessRe = new RegExp(`(?:^|[^\\w$.])${propsObj}\\.([A-Za-z_$][\\w$]*)`, "g");
19500
+ const scan = (s) => {
19501
+ if (!s) return;
19502
+ for (const m of s.matchAll(accessRe)) accessed.add(m[1]);
19503
+ };
19504
+ for (const memo of ir.metadata.memos) scan(memo.computation);
19505
+ for (const signal of ir.metadata.signals) scan(signal.initialValue);
19506
+ for (const stmt of ir.metadata.initStatements ?? []) scan(stmt.body);
19507
+ for (const eff of ir.metadata.effects ?? []) scan(eff.body);
19508
+ for (const c of ir.metadata.localConstants ?? []) {
19509
+ if (c.isModule) continue;
19510
+ scan(c.value);
19511
+ }
19512
+ const walk = (node) => {
19513
+ if (!node) return;
19514
+ const el = node;
19515
+ for (const attr of el.attrs ?? []) {
19516
+ const v = attr.value;
19517
+ if (v?.kind === "expression" && typeof v.expr === "string") {
19518
+ scan(v.expr);
19519
+ const expr = v.expr.trim();
19520
+ const prefix = `${propsObj}.`;
19521
+ if (isBooleanAttr(attr.name) || v.presenceOrUndefined) {
19522
+ const m = expr.match(new RegExp(`^${propsObj}\\.([A-Za-z_$][\\w$]*)`));
19523
+ if (m) booleanAttrProps.add(m[1]);
19524
+ } else if (expr.startsWith(prefix)) {
19525
+ const rest2 = expr.slice(prefix.length);
19526
+ if (/^[A-Za-z_$][\w$]*$/.test(rest2)) bareRefProps.add(rest2);
19527
+ }
19528
+ }
19529
+ }
19530
+ for (const child of el.children ?? []) {
19531
+ const c = child;
19532
+ walk(c.element ?? child);
19533
+ }
19534
+ };
19535
+ walk(ir.root);
19536
+ for (const name of accessed) {
19537
+ if (existing.has(name)) continue;
19538
+ let raw;
19539
+ if (booleanAttrProps.has(name)) raw = "boolean";
19540
+ else if (bareRefProps.has(name)) raw = "unknown";
19541
+ else raw = "string";
19542
+ const type = raw === "boolean" ? { kind: "primitive", raw: "boolean", primitive: "boolean" } : raw === "string" ? { kind: "primitive", raw: "string", primitive: "string" } : { kind: "unknown", raw: "unknown" };
19543
+ ir.metadata.propsParams.push({ name, type, optional: true });
19544
+ existing.add(name);
19545
+ }
19546
+ }
19547
+ function evalStringArrayJoin(source) {
19548
+ const sf = ts19.createSourceFile(
19549
+ "__join.ts",
19550
+ `const __x = (${source});`,
19551
+ ts19.ScriptTarget.Latest,
19552
+ /*setParentNodes*/
19553
+ false
19554
+ );
19555
+ const stmt = sf.statements[0];
19556
+ if (!stmt || !ts19.isVariableStatement(stmt)) return null;
19557
+ let node = stmt.declarationList.declarations[0]?.initializer;
19558
+ while (node && ts19.isParenthesizedExpression(node)) node = node.expression;
19559
+ if (!node || !ts19.isCallExpression(node)) return null;
19560
+ const callee = node.expression;
19561
+ if (!ts19.isPropertyAccessExpression(callee)) return null;
19562
+ if (callee.name.text !== "join") return null;
19563
+ let recv = callee.expression;
19564
+ while (ts19.isParenthesizedExpression(recv)) recv = recv.expression;
19565
+ if (!ts19.isArrayLiteralExpression(recv)) return null;
19566
+ const parts = [];
19567
+ for (const el of recv.elements) {
19568
+ if (ts19.isStringLiteral(el) || ts19.isNoSubstitutionTemplateLiteral(el)) {
19569
+ parts.push(el.text);
19570
+ } else {
19571
+ return null;
19572
+ }
19573
+ }
19574
+ let sep = ",";
19575
+ if (node.arguments.length >= 1) {
19576
+ const arg = node.arguments[0];
19577
+ if (ts19.isStringLiteral(arg) || ts19.isNoSubstitutionTemplateLiteral(arg)) sep = arg.text;
19578
+ else return null;
19579
+ }
19580
+ return parts.join(sep);
19581
+ }
19582
+ function parseRecordIndexAccess(val, localConstants, propsParams, resolveKey) {
19583
+ if (!ts19.isElementAccessExpression(val)) return null;
19584
+ const obj = val.expression;
19585
+ const arg = val.argumentExpression;
19586
+ if (!ts19.isIdentifier(obj) || !ts19.isIdentifier(arg)) return null;
19587
+ let indexPropName;
19588
+ let defaultKey;
19589
+ const resolved = resolveKey?.(arg.text);
19590
+ if (resolved) {
19591
+ indexPropName = resolved.propName;
19592
+ defaultKey = resolved.defaultLiteral;
19593
+ } else if (propsParams.some((p) => p.name === arg.text)) {
19594
+ indexPropName = arg.text;
19595
+ } else {
19596
+ return null;
19597
+ }
19598
+ const constInfo = localConstants.find((c) => c.name === obj.text && c.isModule);
19599
+ if (constInfo?.value === void 0) return null;
19600
+ const sf = ts19.createSourceFile(
19601
+ "__rec.ts",
19602
+ `(${constInfo.value})`,
19603
+ ts19.ScriptTarget.Latest,
19604
+ /* setParentNodes */
19605
+ true
19606
+ );
19607
+ if (sf.statements.length !== 1) return null;
19608
+ const stmt = sf.statements[0];
19609
+ if (!ts19.isExpressionStatement(stmt)) return null;
19610
+ let parsed = stmt.expression;
19611
+ while (ts19.isParenthesizedExpression(parsed)) parsed = parsed.expression;
19612
+ if (!ts19.isObjectLiteralExpression(parsed)) return null;
19613
+ const entries = [];
19614
+ for (const prop of parsed.properties) {
19615
+ if (!ts19.isPropertyAssignment(prop)) return null;
19616
+ let key;
19617
+ if (ts19.isIdentifier(prop.name)) {
19618
+ key = prop.name.text;
19619
+ } else if (ts19.isStringLiteral(prop.name) || ts19.isNoSubstitutionTemplateLiteral(prop.name)) {
19620
+ key = prop.name.text;
19621
+ } else {
19622
+ return null;
19623
+ }
19624
+ let v = prop.initializer;
19625
+ while (ts19.isParenthesizedExpression(v)) v = v.expression;
19626
+ if (ts19.isNumericLiteral(v)) {
19627
+ entries.push({ key, value: { kind: "number", text: v.text } });
19628
+ } else if (ts19.isStringLiteral(v) || ts19.isNoSubstitutionTemplateLiteral(v)) {
19629
+ entries.push({ key, value: { kind: "string", text: v.text } });
19630
+ } else {
19631
+ return null;
19632
+ }
19633
+ }
19634
+ return { indexPropName, entries, defaultKey };
19635
+ }
19636
+ var init_augment_inherited_props = __esm({
19637
+ "../jsx/src/augment-inherited-props.ts"() {
19638
+ "use strict";
19639
+ init_html_constants();
19640
+ }
19641
+ });
19642
+
19355
19643
  // ../jsx/src/index.ts
19356
19644
  var src_exports = {};
19357
19645
  __export(src_exports, {
@@ -19366,6 +19654,7 @@ __export(src_exports, {
19366
19654
  analyzeClientNeeds: () => analyzeClientNeeds,
19367
19655
  analyzeComponent: () => analyzeComponent,
19368
19656
  applyCssLayerPrefix: () => applyCssLayerPrefix,
19657
+ augmentInheritedPropAccesses: () => augmentInheritedPropAccesses,
19369
19658
  buildComponentAnalysis: () => buildComponentAnalysis,
19370
19659
  buildComponentGraph: () => buildComponentGraph,
19371
19660
  buildComponentSummary: () => buildComponentSummary,
@@ -19377,6 +19666,7 @@ __export(src_exports, {
19377
19666
  buildMetadata: () => buildMetadata,
19378
19667
  buildSourceMapFromIR: () => buildSourceMapFromIR,
19379
19668
  buildWhyUpdate: () => buildWhyUpdate,
19669
+ collectContextConsumers: () => collectContextConsumers,
19380
19670
  combineParentChildClientJs: () => combineParentChildClientJs,
19381
19671
  compileJSX: () => compileJSX,
19382
19672
  containsHigherOrder: () => containsHigherOrder,
@@ -19389,6 +19679,7 @@ __export(src_exports, {
19389
19679
  emitIRNode: () => emitIRNode,
19390
19680
  emitParsedExpr: () => emitParsedExpr,
19391
19681
  enableCompilerInstrumentation: () => enableCompilerInstrumentation,
19682
+ evalStringArrayJoin: () => evalStringArrayJoin,
19392
19683
  exprToString: () => exprToString,
19393
19684
  extractFunctionParams: () => extractFunctionParams,
19394
19685
  extractSsrDefaults: () => extractSsrDefaults,
@@ -19420,6 +19711,7 @@ __export(src_exports, {
19420
19711
  needsTypeBasedDetection: () => needsTypeBasedDetection,
19421
19712
  parseBlockBody: () => parseBlockBody,
19422
19713
  parseExpression: () => parseExpression,
19714
+ parseRecordIndexAccess: () => parseRecordIndexAccess,
19423
19715
  renderImportMapHtml: () => renderImportMapHtml,
19424
19716
  resetCompilerCounters: () => resetCompilerCounters,
19425
19717
  resolveSetters: () => resolveSetters,
@@ -19454,6 +19746,7 @@ var init_src2 = __esm({
19454
19746
  init_loop_chain();
19455
19747
  init_debug();
19456
19748
  init_html_constants();
19749
+ init_augment_inherited_props();
19457
19750
  }
19458
19751
  });
19459
19752
 
@@ -19462,23 +19755,23 @@ import { createHash } from "node:crypto";
19462
19755
  import { access, readFile, writeFile, glob as fsGlob } from "node:fs/promises";
19463
19756
  import { constants as fsConstants } from "node:fs";
19464
19757
  import { transformSync } from "esbuild";
19465
- async function readText(path23) {
19466
- return readFile(path23, "utf8");
19758
+ async function readText(path24) {
19759
+ return readFile(path24, "utf8");
19467
19760
  }
19468
- async function readBytes(path23) {
19469
- const buf = await readFile(path23);
19761
+ async function readBytes(path24) {
19762
+ const buf = await readFile(path24);
19470
19763
  return new Uint8Array(buf.buffer, buf.byteOffset, buf.byteLength);
19471
19764
  }
19472
- async function writeText(path23, content) {
19473
- await writeFile(path23, content, "utf8");
19765
+ async function writeText(path24, content) {
19766
+ await writeFile(path24, content, "utf8");
19474
19767
  }
19475
- async function writeBytes(path23, content) {
19768
+ async function writeBytes(path24, content) {
19476
19769
  const bytes = content instanceof ArrayBuffer ? new Uint8Array(content) : content;
19477
- await writeFile(path23, bytes);
19770
+ await writeFile(path24, bytes);
19478
19771
  }
19479
- async function fileExists(path23) {
19772
+ async function fileExists(path24) {
19480
19773
  try {
19481
- await access(path23, fsConstants.F_OK);
19774
+ await access(path24, fsConstants.F_OK);
19482
19775
  return true;
19483
19776
  } catch {
19484
19777
  return false;
@@ -19518,7 +19811,7 @@ var init_runtime = __esm({
19518
19811
 
19519
19812
  // src/lib/resolve-imports.ts
19520
19813
  import { dirname as dirname2, resolve as resolve2 } from "node:path";
19521
- import ts19 from "typescript";
19814
+ import ts20 from "typescript";
19522
19815
  function shapeFromDecl(decl) {
19523
19816
  const clause = decl.importClause;
19524
19817
  if (!clause) return null;
@@ -19528,7 +19821,7 @@ function shapeFromDecl(decl) {
19528
19821
  }
19529
19822
  const bindings = clause.namedBindings;
19530
19823
  if (bindings) {
19531
- if (ts19.isNamespaceImport(bindings)) {
19824
+ if (ts20.isNamespaceImport(bindings)) {
19532
19825
  shape.namespace = bindings.name.text;
19533
19826
  } else {
19534
19827
  for (const el of bindings.elements) {
@@ -19542,38 +19835,38 @@ function shapeFromDecl(decl) {
19542
19835
  }
19543
19836
  function collectExportedNames(source) {
19544
19837
  const names = /* @__PURE__ */ new Set();
19545
- const sourceFile = ts19.createSourceFile(
19838
+ const sourceFile = ts20.createSourceFile(
19546
19839
  "mod.ts",
19547
19840
  source,
19548
- ts19.ScriptTarget.Latest,
19841
+ ts20.ScriptTarget.Latest,
19549
19842
  /*setParents*/
19550
19843
  false,
19551
- ts19.ScriptKind.TS
19844
+ ts20.ScriptKind.TS
19552
19845
  );
19553
19846
  function hasExport(node) {
19554
- if (!ts19.canHaveModifiers(node)) return false;
19555
- const mods = ts19.getModifiers(node);
19556
- return mods?.some((m) => m.kind === ts19.SyntaxKind.ExportKeyword) ?? false;
19847
+ if (!ts20.canHaveModifiers(node)) return false;
19848
+ const mods = ts20.getModifiers(node);
19849
+ return mods?.some((m) => m.kind === ts20.SyntaxKind.ExportKeyword) ?? false;
19557
19850
  }
19558
19851
  function collectFromBindingName(name) {
19559
- if (ts19.isIdentifier(name)) {
19852
+ if (ts20.isIdentifier(name)) {
19560
19853
  names.add(name.text);
19561
19854
  return;
19562
19855
  }
19563
19856
  for (const el of name.elements) {
19564
- if (ts19.isBindingElement(el)) collectFromBindingName(el.name);
19857
+ if (ts20.isBindingElement(el)) collectFromBindingName(el.name);
19565
19858
  }
19566
19859
  }
19567
19860
  for (const stmt of sourceFile.statements) {
19568
- if (ts19.isVariableStatement(stmt) && hasExport(stmt)) {
19861
+ if (ts20.isVariableStatement(stmt) && hasExport(stmt)) {
19569
19862
  for (const d of stmt.declarationList.declarations) {
19570
19863
  collectFromBindingName(d.name);
19571
19864
  }
19572
- } else if (ts19.isFunctionDeclaration(stmt) && hasExport(stmt) && stmt.name) {
19865
+ } else if (ts20.isFunctionDeclaration(stmt) && hasExport(stmt) && stmt.name) {
19573
19866
  names.add(stmt.name.text);
19574
- } else if (ts19.isClassDeclaration(stmt) && hasExport(stmt) && stmt.name) {
19867
+ } else if (ts20.isClassDeclaration(stmt) && hasExport(stmt) && stmt.name) {
19575
19868
  names.add(stmt.name.text);
19576
- } else if (ts19.isExportDeclaration(stmt) && !stmt.moduleSpecifier && stmt.exportClause && ts19.isNamedExports(stmt.exportClause)) {
19869
+ } else if (ts20.isExportDeclaration(stmt) && !stmt.moduleSpecifier && stmt.exportClause && ts20.isNamedExports(stmt.exportClause)) {
19577
19870
  if (stmt.isTypeOnly) continue;
19578
19871
  for (const el of stmt.exportClause.elements) {
19579
19872
  if (el.isTypeOnly) continue;
@@ -19584,16 +19877,16 @@ function collectExportedNames(source) {
19584
19877
  return [...names];
19585
19878
  }
19586
19879
  function hasUseClientDirective(source) {
19587
- const sourceFile = ts19.createSourceFile(
19880
+ const sourceFile = ts20.createSourceFile(
19588
19881
  "check.tsx",
19589
19882
  source,
19590
- ts19.ScriptTarget.Latest,
19883
+ ts20.ScriptTarget.Latest,
19591
19884
  /*setParents*/
19592
19885
  false,
19593
- ts19.ScriptKind.TSX
19886
+ ts20.ScriptKind.TSX
19594
19887
  );
19595
19888
  for (const stmt of sourceFile.statements) {
19596
- if (!ts19.isExpressionStatement(stmt) || !ts19.isStringLiteral(stmt.expression)) {
19889
+ if (!ts20.isExpressionStatement(stmt) || !ts20.isStringLiteral(stmt.expression)) {
19597
19890
  return false;
19598
19891
  }
19599
19892
  if (stmt.expression.text === "use client") return true;
@@ -19602,55 +19895,55 @@ function hasUseClientDirective(source) {
19602
19895
  }
19603
19896
  function collectTopLevelBindings(source) {
19604
19897
  const names = /* @__PURE__ */ new Set();
19605
- const sourceFile = ts19.createSourceFile(
19898
+ const sourceFile = ts20.createSourceFile(
19606
19899
  "bundle.ts",
19607
19900
  source,
19608
- ts19.ScriptTarget.Latest,
19901
+ ts20.ScriptTarget.Latest,
19609
19902
  /*setParents*/
19610
19903
  false,
19611
- ts19.ScriptKind.TS
19904
+ ts20.ScriptKind.TS
19612
19905
  );
19613
19906
  function collectFromBindingName(name) {
19614
- if (ts19.isIdentifier(name)) {
19907
+ if (ts20.isIdentifier(name)) {
19615
19908
  names.add(name.text);
19616
19909
  return;
19617
19910
  }
19618
19911
  for (const el of name.elements) {
19619
- if (ts19.isBindingElement(el)) collectFromBindingName(el.name);
19912
+ if (ts20.isBindingElement(el)) collectFromBindingName(el.name);
19620
19913
  }
19621
19914
  }
19622
19915
  for (const stmt of sourceFile.statements) {
19623
- if (ts19.isVariableStatement(stmt)) {
19916
+ if (ts20.isVariableStatement(stmt)) {
19624
19917
  for (const d of stmt.declarationList.declarations) {
19625
19918
  collectFromBindingName(d.name);
19626
19919
  }
19627
- } else if (ts19.isFunctionDeclaration(stmt) && stmt.name) {
19920
+ } else if (ts20.isFunctionDeclaration(stmt) && stmt.name) {
19628
19921
  names.add(stmt.name.text);
19629
- } else if (ts19.isClassDeclaration(stmt) && stmt.name) {
19922
+ } else if (ts20.isClassDeclaration(stmt) && stmt.name) {
19630
19923
  names.add(stmt.name.text);
19631
19924
  }
19632
19925
  }
19633
19926
  return names;
19634
19927
  }
19635
19928
  function stripImportsAndExports(body) {
19636
- const sourceFile = ts19.createSourceFile(
19929
+ const sourceFile = ts20.createSourceFile(
19637
19930
  "body.ts",
19638
19931
  body,
19639
- ts19.ScriptTarget.Latest,
19932
+ ts20.ScriptTarget.Latest,
19640
19933
  /*setParents*/
19641
19934
  false,
19642
- ts19.ScriptKind.TS
19935
+ ts20.ScriptKind.TS
19643
19936
  );
19644
19937
  const spans = [];
19645
19938
  const hoistedImports = [];
19646
19939
  for (const stmt of sourceFile.statements) {
19647
- if (ts19.isImportDeclaration(stmt)) {
19940
+ if (ts20.isImportDeclaration(stmt)) {
19648
19941
  const start = stmt.getStart(sourceFile);
19649
19942
  const end = stmt.getEnd();
19650
19943
  const specifier = stmt.moduleSpecifier;
19651
- if (ts19.isStringLiteral(specifier)) {
19652
- const path23 = specifier.text;
19653
- const isRelative = path23.startsWith("./") || path23.startsWith("../");
19944
+ if (ts20.isStringLiteral(specifier)) {
19945
+ const path24 = specifier.text;
19946
+ const isRelative = path24.startsWith("./") || path24.startsWith("../");
19654
19947
  if (!isRelative) {
19655
19948
  hoistedImports.push(body.slice(start, end));
19656
19949
  }
@@ -19658,24 +19951,24 @@ function stripImportsAndExports(body) {
19658
19951
  spans.push([start, end]);
19659
19952
  continue;
19660
19953
  }
19661
- if (ts19.isExportDeclaration(stmt)) {
19954
+ if (ts20.isExportDeclaration(stmt)) {
19662
19955
  spans.push([stmt.getStart(sourceFile), stmt.getEnd()]);
19663
19956
  continue;
19664
19957
  }
19665
- if (ts19.isExportAssignment(stmt)) {
19666
- const exportKw = stmt.getChildren(sourceFile).find((c) => c.kind === ts19.SyntaxKind.ExportKeyword);
19667
- const defaultKw = stmt.getChildren(sourceFile).find((c) => c.kind === ts19.SyntaxKind.DefaultKeyword);
19668
- const equalsKw = stmt.getChildren(sourceFile).find((c) => c.kind === ts19.SyntaxKind.EqualsToken);
19958
+ if (ts20.isExportAssignment(stmt)) {
19959
+ const exportKw = stmt.getChildren(sourceFile).find((c) => c.kind === ts20.SyntaxKind.ExportKeyword);
19960
+ const defaultKw = stmt.getChildren(sourceFile).find((c) => c.kind === ts20.SyntaxKind.DefaultKeyword);
19961
+ const equalsKw = stmt.getChildren(sourceFile).find((c) => c.kind === ts20.SyntaxKind.EqualsToken);
19669
19962
  const start = exportKw?.getStart(sourceFile) ?? stmt.getStart(sourceFile);
19670
19963
  const end = (defaultKw ?? equalsKw)?.getEnd() ?? exportKw?.getEnd() ?? stmt.getStart(sourceFile);
19671
19964
  if (end > start) spans.push([start, end]);
19672
19965
  continue;
19673
19966
  }
19674
- if (ts19.canHaveModifiers(stmt)) {
19675
- const mods = ts19.getModifiers(stmt);
19967
+ if (ts20.canHaveModifiers(stmt)) {
19968
+ const mods = ts20.getModifiers(stmt);
19676
19969
  if (!mods) continue;
19677
19970
  for (const mod of mods) {
19678
- if (mod.kind === ts19.SyntaxKind.ExportKeyword) {
19971
+ if (mod.kind === ts20.SyntaxKind.ExportKeyword) {
19679
19972
  const start = mod.getStart(sourceFile);
19680
19973
  let end = mod.getEnd();
19681
19974
  while (end < body.length && /\s/.test(body[end])) end++;
@@ -19784,48 +20077,48 @@ function buildDanglingReferenceMessage(binding, s) {
19784
20077
  function isValueReference(id) {
19785
20078
  const parent = id.parent;
19786
20079
  if (!parent) return false;
19787
- if (ts19.isPropertyAccessExpression(parent) && parent.name === id) return false;
19788
- if (ts19.isPropertyAssignment(parent) && parent.name === id) return false;
19789
- if ((ts19.isMethodDeclaration(parent) || ts19.isGetAccessorDeclaration(parent) || ts19.isSetAccessorDeclaration(parent)) && parent.name === id) {
20080
+ if (ts20.isPropertyAccessExpression(parent) && parent.name === id) return false;
20081
+ if (ts20.isPropertyAssignment(parent) && parent.name === id) return false;
20082
+ if ((ts20.isMethodDeclaration(parent) || ts20.isGetAccessorDeclaration(parent) || ts20.isSetAccessorDeclaration(parent)) && parent.name === id) {
19790
20083
  return false;
19791
20084
  }
19792
- if (ts19.isVariableDeclaration(parent) && parent.name === id) return false;
19793
- if (ts19.isFunctionDeclaration(parent) && parent.name === id) return false;
19794
- if (ts19.isFunctionExpression(parent) && parent.name === id) return false;
19795
- if (ts19.isClassDeclaration(parent) && parent.name === id) return false;
19796
- if (ts19.isClassExpression(parent) && parent.name === id) return false;
19797
- if (ts19.isParameter(parent) && parent.name === id) return false;
19798
- if (ts19.isBindingElement(parent) && (parent.name === id || parent.propertyName === id)) return false;
19799
- if (ts19.isLabeledStatement(parent) && parent.label === id) return false;
19800
- if (ts19.isBreakOrContinueStatement(parent) && parent.label === id) return false;
19801
- if (ts19.isImportSpecifier(parent) && (parent.name === id || parent.propertyName === id)) return false;
19802
- if (ts19.isExportSpecifier(parent) && (parent.name === id || parent.propertyName === id)) return false;
19803
- if (ts19.isImportClause(parent) && parent.name === id) return false;
19804
- if (ts19.isNamespaceImport(parent) && parent.name === id) return false;
19805
- if (ts19.isQualifiedName(parent) && parent.right === id) return false;
20085
+ if (ts20.isVariableDeclaration(parent) && parent.name === id) return false;
20086
+ if (ts20.isFunctionDeclaration(parent) && parent.name === id) return false;
20087
+ if (ts20.isFunctionExpression(parent) && parent.name === id) return false;
20088
+ if (ts20.isClassDeclaration(parent) && parent.name === id) return false;
20089
+ if (ts20.isClassExpression(parent) && parent.name === id) return false;
20090
+ if (ts20.isParameter(parent) && parent.name === id) return false;
20091
+ if (ts20.isBindingElement(parent) && (parent.name === id || parent.propertyName === id)) return false;
20092
+ if (ts20.isLabeledStatement(parent) && parent.label === id) return false;
20093
+ if (ts20.isBreakOrContinueStatement(parent) && parent.label === id) return false;
20094
+ if (ts20.isImportSpecifier(parent) && (parent.name === id || parent.propertyName === id)) return false;
20095
+ if (ts20.isExportSpecifier(parent) && (parent.name === id || parent.propertyName === id)) return false;
20096
+ if (ts20.isImportClause(parent) && parent.name === id) return false;
20097
+ if (ts20.isNamespaceImport(parent) && parent.name === id) return false;
20098
+ if (ts20.isQualifiedName(parent) && parent.right === id) return false;
19806
20099
  return true;
19807
20100
  }
19808
20101
  function detectStrippedReferences(bundleSource, stripped) {
19809
20102
  if (stripped.length === 0) return [];
19810
20103
  let sf;
19811
20104
  try {
19812
- sf = ts19.createSourceFile(
20105
+ sf = ts20.createSourceFile(
19813
20106
  "bundle.js",
19814
20107
  bundleSource,
19815
- ts19.ScriptTarget.Latest,
20108
+ ts20.ScriptTarget.Latest,
19816
20109
  /*setParents*/
19817
20110
  true,
19818
- ts19.ScriptKind.JS
20111
+ ts20.ScriptKind.JS
19819
20112
  );
19820
20113
  } catch {
19821
20114
  return [];
19822
20115
  }
19823
20116
  const firstReference = /* @__PURE__ */ new Map();
19824
20117
  function visit3(node) {
19825
- if (ts19.isIdentifier(node) && isValueReference(node)) {
20118
+ if (ts20.isIdentifier(node) && isValueReference(node)) {
19826
20119
  if (!firstReference.has(node.text)) firstReference.set(node.text, node);
19827
20120
  }
19828
- ts19.forEachChild(node, visit3);
20121
+ ts20.forEachChild(node, visit3);
19829
20122
  }
19830
20123
  visit3(sf);
19831
20124
  const errors = [];
@@ -19855,18 +20148,18 @@ function detectStrippedReferences(bundleSource, stripped) {
19855
20148
  return errors;
19856
20149
  }
19857
20150
  async function walkAndCollect(content, searchDirs, modules, visiting, loggingPath, stripped, stubDeps, nextId) {
19858
- const sourceFile = ts19.createSourceFile(
20151
+ const sourceFile = ts20.createSourceFile(
19859
20152
  "walk.js",
19860
20153
  content,
19861
- ts19.ScriptTarget.Latest,
20154
+ ts20.ScriptTarget.Latest,
19862
20155
  /*setParents*/
19863
20156
  false,
19864
- ts19.ScriptKind.JS
20157
+ ts20.ScriptKind.JS
19865
20158
  );
19866
20159
  const sites = [];
19867
20160
  for (const stmt of sourceFile.statements) {
19868
- if (!ts19.isImportDeclaration(stmt)) continue;
19869
- if (!ts19.isStringLiteral(stmt.moduleSpecifier)) continue;
20161
+ if (!ts20.isImportDeclaration(stmt)) continue;
20162
+ if (!ts20.isStringLiteral(stmt.moduleSpecifier)) continue;
19870
20163
  const spec = stmt.moduleSpecifier.text;
19871
20164
  if (!spec.startsWith("./") && !spec.startsWith("../")) continue;
19872
20165
  const start = stmt.getStart(sourceFile);
@@ -19976,15 +20269,15 @@ async function walkAndCollect(content, searchDirs, modules, visiting, loggingPat
19976
20269
  function topoSort(modules) {
19977
20270
  const visited = /* @__PURE__ */ new Set();
19978
20271
  const order = [];
19979
- function visit3(path23) {
19980
- if (visited.has(path23)) return;
19981
- visited.add(path23);
19982
- const mod = modules.get(path23);
20272
+ function visit3(path24) {
20273
+ if (visited.has(path24)) return;
20274
+ visited.add(path24);
20275
+ const mod = modules.get(path24);
19983
20276
  if (!mod) return;
19984
20277
  for (const dep of mod.imports) visit3(dep);
19985
20278
  order.push(mod);
19986
20279
  }
19987
- for (const path23 of modules.keys()) visit3(path23);
20280
+ for (const path24 of modules.keys()) visit3(path24);
19988
20281
  return order;
19989
20282
  }
19990
20283
  async function inlineRelativeImports(content, searchDirs, loggingPath, hoistedAcc, errorAcc, stubDeps) {
@@ -20089,10 +20382,10 @@ function emptyCache(globalHash) {
20089
20382
  return { globalHash, entries: {} };
20090
20383
  }
20091
20384
  async function loadCache(outDir) {
20092
- const path23 = resolve3(outDir, CACHE_FILENAME);
20093
- if (!await fileExists(path23)) return null;
20385
+ const path24 = resolve3(outDir, CACHE_FILENAME);
20386
+ if (!await fileExists(path24)) return null;
20094
20387
  try {
20095
- const parsed = JSON.parse(await readText(path23));
20388
+ const parsed = JSON.parse(await readText(path24));
20096
20389
  if (typeof parsed.globalHash !== "string" || typeof parsed.entries !== "object") {
20097
20390
  return null;
20098
20391
  }
@@ -20102,8 +20395,8 @@ async function loadCache(outDir) {
20102
20395
  }
20103
20396
  }
20104
20397
  async function saveCache(outDir, cache) {
20105
- const path23 = resolve3(outDir, CACHE_FILENAME);
20106
- await writeText(path23, JSON.stringify(cache, null, 2));
20398
+ const path24 = resolve3(outDir, CACHE_FILENAME);
20399
+ await writeText(path24, JSON.stringify(cache, null, 2));
20107
20400
  }
20108
20401
  function isEntryFresh(entry, currentSourceHash, depHash) {
20109
20402
  if (entry.hash !== currentSourceHash) return false;
@@ -20123,24 +20416,24 @@ var init_build_cache = __esm({
20123
20416
  });
20124
20417
 
20125
20418
  // src/lib/fs-utils.ts
20126
- async function writeIfChanged(path23, content) {
20127
- if (await fileExists(path23)) {
20419
+ async function writeIfChanged(path24, content) {
20420
+ if (await fileExists(path24)) {
20128
20421
  if (typeof content === "string") {
20129
- const prev2 = await readText(path23);
20422
+ const prev2 = await readText(path24);
20130
20423
  if (prev2 === content) return false;
20131
- await writeText(path23, content);
20424
+ await writeText(path24, content);
20132
20425
  return true;
20133
20426
  }
20134
- const prev = await readBytes(path23);
20427
+ const prev = await readBytes(path24);
20135
20428
  const next = toUint8Array(content);
20136
20429
  if (equalBytes(prev, next)) return false;
20137
- await writeBytes(path23, next);
20430
+ await writeBytes(path24, next);
20138
20431
  return true;
20139
20432
  }
20140
20433
  if (typeof content === "string") {
20141
- await writeText(path23, content);
20434
+ await writeText(path24, content);
20142
20435
  } else {
20143
- await writeBytes(path23, toUint8Array(content));
20436
+ await writeBytes(path24, toUint8Array(content));
20144
20437
  }
20145
20438
  return true;
20146
20439
  }
@@ -20195,10 +20488,10 @@ function denormalizeKey(diskKey, projectDir) {
20195
20488
  return resolve4(projectDir, diskKey);
20196
20489
  }
20197
20490
  async function loadEmitLedger(outDir, projectDir) {
20198
- const path23 = resolve4(outDir, EMIT_LEDGER_FILENAME);
20199
- if (!await fileExists(path23)) return null;
20491
+ const path24 = resolve4(outDir, EMIT_LEDGER_FILENAME);
20492
+ if (!await fileExists(path24)) return null;
20200
20493
  try {
20201
- const parsed = JSON.parse(await readText(path23));
20494
+ const parsed = JSON.parse(await readText(path24));
20202
20495
  if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed) || parsed.version !== EMIT_LEDGER_VERSION || typeof parsed.entries !== "object" || parsed.entries === null || Array.isArray(parsed.entries)) {
20203
20496
  return null;
20204
20497
  }
@@ -20215,13 +20508,13 @@ async function loadEmitLedger(outDir, projectDir) {
20215
20508
  }
20216
20509
  }
20217
20510
  async function saveEmitLedger(outDir, projectDir, ledger) {
20218
- const path23 = resolve4(outDir, EMIT_LEDGER_FILENAME);
20511
+ const path24 = resolve4(outDir, EMIT_LEDGER_FILENAME);
20219
20512
  const serialized = {};
20220
20513
  for (const [absKey, outputs] of Object.entries(ledger.entries)) {
20221
20514
  serialized[normalizeKey(absKey, projectDir)] = outputs;
20222
20515
  }
20223
20516
  const onDisk = { version: ledger.version, entries: serialized };
20224
- await writeIfChanged(path23, JSON.stringify(onDisk, null, 2));
20517
+ await writeIfChanged(path24, JSON.stringify(onDisk, null, 2));
20225
20518
  }
20226
20519
  function extractLedgerFromCache(cache) {
20227
20520
  if (!cache) return {};
@@ -20291,8 +20584,8 @@ function buildManagedBlock(entries) {
20291
20584
  ].join("\n");
20292
20585
  }
20293
20586
  async function writeAssetsIgnore(outDir, entries) {
20294
- const path23 = resolve5(outDir, ASSETS_IGNORE_FILENAME);
20295
- const existing = await fileExists(path23) ? await readText(path23) : "";
20587
+ const path24 = resolve5(outDir, ASSETS_IGNORE_FILENAME);
20588
+ const existing = await fileExists(path24) ? await readText(path24) : "";
20296
20589
  const userContent = stripManagedBlock(existing);
20297
20590
  const block = buildManagedBlock(entries);
20298
20591
  const merged = userContent.length > 0 ? `${userContent}
@@ -20300,7 +20593,7 @@ async function writeAssetsIgnore(outDir, entries) {
20300
20593
  ${block}
20301
20594
  ` : `${block}
20302
20595
  `;
20303
- return writeIfChanged(path23, merged);
20596
+ return writeIfChanged(path24, merged);
20304
20597
  }
20305
20598
  var ASSETS_IGNORE_FILENAME, BLOCK_BEGIN, BLOCK_END, WRANGLER_CONFIG_NAMES;
20306
20599
  var init_assets_ignore = __esm({
@@ -20318,7 +20611,7 @@ var init_assets_ignore = __esm({
20318
20611
  });
20319
20612
 
20320
20613
  // src/lib/build.ts
20321
- import ts20 from "typescript";
20614
+ import ts21 from "typescript";
20322
20615
  import { mkdir, readdir, stat, unlink } from "node:fs/promises";
20323
20616
  import { resolve as resolve6, basename, relative as relative2, dirname as dirname3, isAbsolute as isAbsolute2 } from "node:path";
20324
20617
  import { fileURLToPath as fileURLToPath2 } from "node:url";
@@ -20355,7 +20648,9 @@ async function discoverComponentFiles(dir, options) {
20355
20648
  const skipDirs = options?.skipDirs ? new Set(options.skipDirs) : null;
20356
20649
  let entries;
20357
20650
  try {
20358
- entries = await readdir(dir, { withFileTypes: true });
20651
+ entries = (await readdir(dir, { withFileTypes: true })).sort(
20652
+ (a, b) => String(a.name).localeCompare(String(b.name))
20653
+ );
20359
20654
  } catch {
20360
20655
  return results;
20361
20656
  }
@@ -20864,7 +21159,7 @@ async function build(config, options = {}) {
20864
21159
  };
20865
21160
  }
20866
21161
  function extractBareImports(code) {
20867
- const { importedFiles } = ts20.preProcessFile(code, true, true);
21162
+ const { importedFiles } = ts21.preProcessFile(code, true, true);
20868
21163
  const specifiers = /* @__PURE__ */ new Set();
20869
21164
  for (const { fileName } of importedFiles) {
20870
21165
  if (!fileName.startsWith(".") && !fileName.startsWith("/") && !fileName.includes("://")) {
@@ -20931,16 +21226,16 @@ function effectiveOutName(tplPath, entryBaseNoExt) {
20931
21226
  }
20932
21227
  function topLevelImportLines(content) {
20933
21228
  const lines = /* @__PURE__ */ new Set();
20934
- const sourceFile = ts20.createSourceFile(
21229
+ const sourceFile = ts21.createSourceFile(
20935
21230
  "merge.js",
20936
21231
  content,
20937
- ts20.ScriptTarget.Latest,
21232
+ ts21.ScriptTarget.Latest,
20938
21233
  /*setParentNodes*/
20939
21234
  true,
20940
- ts20.ScriptKind.JS
21235
+ ts21.ScriptKind.JS
20941
21236
  );
20942
21237
  for (const stmt of sourceFile.statements) {
20943
- if (ts20.isImportDeclaration(stmt)) {
21238
+ if (ts21.isImportDeclaration(stmt)) {
20944
21239
  const { line } = sourceFile.getLineAndCharacterOfPosition(stmt.getStart(sourceFile));
20945
21240
  lines.add(line);
20946
21241
  }
@@ -20949,29 +21244,29 @@ function topLevelImportLines(content) {
20949
21244
  }
20950
21245
  function rewriteBarefootClientSpecifiers(content, rel) {
20951
21246
  if (!content.includes("@barefootjs/client")) return content;
20952
- const sourceFile = ts20.createSourceFile(
21247
+ const sourceFile = ts21.createSourceFile(
20953
21248
  "client.js",
20954
21249
  content,
20955
- ts20.ScriptTarget.Latest,
21250
+ ts21.ScriptTarget.Latest,
20956
21251
  /*setParentNodes*/
20957
21252
  true,
20958
- ts20.ScriptKind.JS
21253
+ ts21.ScriptKind.JS
20959
21254
  );
20960
21255
  const isBarefootClient = (s) => s === "@barefootjs/client" || s.startsWith("@barefootjs/client/");
20961
21256
  const spans = [];
20962
21257
  const visit3 = (node) => {
20963
- if (ts20.isImportDeclaration(node) || ts20.isExportDeclaration(node)) {
21258
+ if (ts21.isImportDeclaration(node) || ts21.isExportDeclaration(node)) {
20964
21259
  const ms = node.moduleSpecifier;
20965
- if (ms && ts20.isStringLiteral(ms) && isBarefootClient(ms.text)) {
21260
+ if (ms && ts21.isStringLiteral(ms) && isBarefootClient(ms.text)) {
20966
21261
  spans.push([ms.getStart(sourceFile), ms.getEnd()]);
20967
21262
  }
20968
- } else if (ts20.isCallExpression(node) && node.expression.kind === ts20.SyntaxKind.ImportKeyword) {
21263
+ } else if (ts21.isCallExpression(node) && node.expression.kind === ts21.SyntaxKind.ImportKeyword) {
20969
21264
  const arg = node.arguments[0];
20970
- if (arg && ts20.isStringLiteral(arg) && isBarefootClient(arg.text)) {
21265
+ if (arg && ts21.isStringLiteral(arg) && isBarefootClient(arg.text)) {
20971
21266
  spans.push([arg.getStart(sourceFile), arg.getEnd()]);
20972
21267
  }
20973
21268
  }
20974
- ts20.forEachChild(node, visit3);
21269
+ ts21.forEachChild(node, visit3);
20975
21270
  };
20976
21271
  visit3(sourceFile);
20977
21272
  if (spans.length === 0) return content;
@@ -21384,8 +21679,8 @@ async function writeBuildId(outDir, result) {
21384
21679
  if (!result.changed) return;
21385
21680
  const devDir = resolve6(outDir, DEV_SENTINEL_SUBDIR);
21386
21681
  await mkdir(devDir, { recursive: true });
21387
- const path23 = resolve6(devDir, DEV_SENTINEL_FILENAME);
21388
- await writeIfChanged(path23, String(Date.now()));
21682
+ const path24 = resolve6(devDir, DEV_SENTINEL_FILENAME);
21683
+ await writeIfChanged(path24, String(Date.now()));
21389
21684
  }
21390
21685
  async function watch(config, options = {}) {
21391
21686
  const { debounceMs = 100, signal } = options;
@@ -21610,8 +21905,8 @@ var init_build2 = __esm({
21610
21905
  });
21611
21906
 
21612
21907
  // src/lib/dependency-resolver.ts
21613
- import { existsSync as existsSync4, readFileSync } from "fs";
21614
- import path6 from "path";
21908
+ import { existsSync as existsSync4, readFileSync } from "node:fs";
21909
+ import path6 from "node:path";
21615
21910
  function resolveDependenciesFromSource(requested, srcComponentsDir) {
21616
21911
  const visited = /* @__PURE__ */ new Set();
21617
21912
  const queue = [...requested];
@@ -21916,8 +22211,8 @@ var init_mdx = __esm({
21916
22211
  });
21917
22212
 
21918
22213
  // src/lib/docs-loader.ts
21919
- import { readFileSync as readFileSync2, existsSync as existsSync5, readdirSync, statSync } from "fs";
21920
- import path7 from "path";
22214
+ import { readFileSync as readFileSync2, existsSync as existsSync5, readdirSync, statSync } from "node:fs";
22215
+ import path7 from "node:path";
21921
22216
  function parseFrontmatter2(content) {
21922
22217
  if (content.startsWith("---\n") || content.startsWith("---\r\n")) {
21923
22218
  const endIdx = content.indexOf("\n---", 3);
@@ -22203,8 +22498,8 @@ __export(meta_extract_exports, {
22203
22498
  pickGeneratedAt: () => pickGeneratedAt,
22204
22499
  run: () => run2
22205
22500
  });
22206
- import { readFileSync as readFileSync3, writeFileSync, mkdirSync, existsSync as existsSync6 } from "fs";
22207
- import path8 from "path";
22501
+ import { readFileSync as readFileSync3, writeFileSync, mkdirSync, existsSync as existsSync6 } from "node:fs";
22502
+ import path8 from "node:path";
22208
22503
  function pickGeneratedAt(previousIndexJson, nextEntries, now = () => (/* @__PURE__ */ new Date()).toISOString()) {
22209
22504
  if (previousIndexJson === null) return now();
22210
22505
  try {
@@ -22484,8 +22779,8 @@ __export(add_exports, {
22484
22779
  run: () => run3,
22485
22780
  toRegistryName: () => toRegistryName
22486
22781
  });
22487
- import { existsSync as existsSync7, mkdirSync as mkdirSync2, copyFileSync, writeFileSync as writeFileSync2, readFileSync as readFileSync4, readdirSync as readdirSync2 } from "fs";
22488
- import path9 from "path";
22782
+ import { existsSync as existsSync7, mkdirSync as mkdirSync2, copyFileSync, writeFileSync as writeFileSync2, readFileSync as readFileSync4, readdirSync as readdirSync2 } from "node:fs";
22783
+ import path9 from "node:path";
22489
22784
  async function run3(args2, ctx2) {
22490
22785
  const force = args2.includes("--force");
22491
22786
  let registryUrl;
@@ -23043,7 +23338,7 @@ var bfGoSource, streamingGoSource, bfdevGoSource, barefootPmSource, barefootBack
23043
23338
  var init_runtimes_generated = __esm({
23044
23339
  "src/lib/adapters/runtimes.generated.ts"() {
23045
23340
  "use strict";
23046
- bfGoSource = '// Package bf provides runtime helper functions for BarefootJS Go templates.\n// These functions mirror JavaScript behavior for consistent SSR output.\npackage bf\n\nimport (\n "bytes"\n "encoding/json"\n "fmt"\n "html/template"\n "math"\n "math/rand"\n "os"\n "reflect"\n "sort"\n "strconv"\n "strings"\n "unicode/utf8"\n)\n\n// FuncMap returns a template.FuncMap with all BarefootJS helper functions.\n// Usage:\n//\n// tmpl := template.New("").Funcs(bf.FuncMap())\nfunc FuncMap() template.FuncMap {\n return template.FuncMap{\n // Arithmetic\n "bf_add": Add,\n "bf_sub": Sub,\n "bf_mul": Mul,\n "bf_div": Div,\n "bf_mod": Mod,\n "bf_neg": Neg,\n\n // String\n "bf_lower": Lower,\n "bf_upper": Upper,\n "bf_trim": Trim,\n "bf_contains": Contains,\n "bf_join": Join,\n "bf_split": Split,\n "bf_starts_with": StartsWith,\n "bf_ends_with": EndsWith,\n "bf_replace": Replace,\n "bf_repeat": Repeat,\n "bf_pad_start": PadStart,\n "bf_pad_end": PadEnd,\n "bf_string": String,\n\n // JSON / numeric primitives \u2014 JS-compat callees registered on\n // the Go adapter\'s `templatePrimitives` map (#1188).\n "bf_json": JSON,\n "bf_number": Number,\n "bf_floor": Floor,\n "bf_ceil": Ceil,\n "bf_round": Round,\n\n // Array/Slice\n "bf_len": Len,\n "bf_at": At,\n "bf_includes": Includes,\n "bf_index_of": IndexOf,\n "bf_last_index_of": LastIndexOf,\n "bf_concat": Concat,\n "bf_slice": Slice,\n "bf_reverse": Reverse,\n "bf_flat": Flat,\n "bf_flat_map": FlatMap,\n "bf_flat_map_tuple": FlatMapTuple,\n "bf_first": First,\n "bf_last": Last,\n "bf_arr": Arr,\n "bf_filter_truthy": FilterTruthy,\n\n // Higher-order Array Methods\n "bf_every": Every,\n "bf_some": Some,\n "bf_filter": Filter,\n "bf_find": Find,\n "bf_find_index": FindIndex,\n "bf_find_last": FindLast,\n "bf_find_last_index": FindLastIndex,\n "bf_sort": Sort,\n "bf_reduce": Reduce,\n\n // Comment marker (for hydration)\n "bfComment": Comment,\n "bfTextStart": TextStart,\n "bfTextEnd": TextEnd,\n\n // Script collection\n "bfScripts": BfScripts,\n\n // Scope attribute value (#1249: bare scope id, no `~` prefix)\n "bfScopeAttr": ScopeAttr,\n\n // Slot-identity markers (#1249): bf-h, bf-m, bf-r\n "bfHydrationAttrs": HydrationAttrs,\n\n // Child component marker (kept for backward compatibility)\n "bfIsChild": IsChild,\n\n // Props attribute for hydration\n "bfPropsAttr": BfPropsAttr,\n\n // Portal HTML rendering (parses and executes template string)\n "bfPortalHTML": PortalHTML,\n\n // Scope comment for fragment roots\n "bfScopeComment": ScopeComment,\n\n // JSX intrinsic-element spread lowering (#1407)\n "bf_spread_attrs": SpreadAttrs,\n }\n}\n\n// ScopeAttr returns the bare bf-s scope id (#1249).\nfunc ScopeAttr(props interface{}) string {\n return getStringField(props, "ScopeID")\n}\n\n// HydrationAttrs emits `bf-h="<host>" bf-m="<slot>" bf-r=""` conditionally.\n// See spec/compiler.md "Slot identity".\nfunc HydrationAttrs(props interface{}) template.HTMLAttr {\n parts := []string{}\n if host := getStringField(props, "BfParent"); host != "" {\n parts = append(parts, fmt.Sprintf(`bf-h="%s"`, template.HTMLEscapeString(host)))\n }\n if mount := getStringField(props, "BfMount"); mount != "" {\n parts = append(parts, fmt.Sprintf(`bf-m="%s"`, template.HTMLEscapeString(mount)))\n }\n if !getBoolField(props, "BfIsChild") {\n parts = append(parts, `bf-r=""`)\n }\n if len(parts) == 0 {\n return ""\n }\n return template.HTMLAttr(strings.Join(parts, " "))\n}\n\n// IsChild is a deprecated no-op stub. Child status is signalled by bf-h\n// presence (#1249); use HydrationAttrs instead.\nfunc IsChild(props interface{}) template.HTMLAttr {\n return ""\n}\n\n// svgCamelCaseAttrs mirrors SVG_CAMEL_CASE_ATTRS from\n// packages/client/src/runtime/spread-attrs.ts. SVG XML attribute\n// names are case-sensitive; the default camelCase \u2192 kebab-case\n// rewrite must NOT apply to these or the SVG stops rendering\n// (#1407). Coordinates with the compile-time SVG_CAMEL_TO_KEBAB\n// table in packages/jsx/src/ir-to-client-js/utils.ts: presentation\n// attrs (clipPath, strokeWidth, \u2026) live there and must NOT appear\n// here, or the same JSX prop would lower to clip-path via the\n// explicit-attr path and stay clipPath via the spread path.\nvar svgCamelCaseAttrs = map[string]struct{}{\n "allowReorder": {}, "attributeName": {}, "attributeType": {}, "autoReverse": {},\n "baseFrequency": {}, "baseProfile": {}, "calcMode": {}, "clipPathUnits": {},\n "contentScriptType": {}, "contentStyleType": {}, "diffuseConstant": {}, "edgeMode": {},\n "externalResourcesRequired": {}, "filterRes": {}, "filterUnits": {}, "glyphRef": {},\n "gradientTransform": {}, "gradientUnits": {}, "kernelMatrix": {}, "kernelUnitLength": {},\n "keyPoints": {}, "keySplines": {}, "keyTimes": {}, "lengthAdjust": {}, "limitingConeAngle": {},\n "markerHeight": {}, "markerUnits": {}, "markerWidth": {}, "maskContentUnits": {},\n "maskUnits": {}, "numOctaves": {}, "pathLength": {}, "patternContentUnits": {},\n "patternTransform": {}, "patternUnits": {}, "pointsAtX": {}, "pointsAtY": {}, "pointsAtZ": {},\n "preserveAlpha": {}, "preserveAspectRatio": {}, "primitiveUnits": {}, "refX": {}, "refY": {},\n "repeatCount": {}, "repeatDur": {}, "requiredExtensions": {}, "requiredFeatures": {},\n "specularConstant": {}, "specularExponent": {}, "spreadMethod": {}, "startOffset": {},\n "stdDeviation": {}, "stitchTiles": {}, "surfaceScale": {}, "systemLanguage": {},\n "tableValues": {}, "targetX": {}, "targetY": {}, "textLength": {}, "viewBox": {}, "viewTarget": {},\n "xChannelSelector": {}, "yChannelSelector": {}, "zoomAndPan": {},\n}\n\n// toAttrName mirrors the JSX\u2192HTML attribute-name rewrite from\n// packages/client/src/runtime/spread-attrs.ts. className \u2192 class,\n// htmlFor \u2192 for, SVG camelCase attrs preserved, other camelCase\n// keys lowered to kebab-case.\nfunc toAttrName(key string) string {\n if key == "className" {\n return "class"\n }\n if key == "htmlFor" {\n return "for"\n }\n if _, ok := svgCamelCaseAttrs[key]; ok {\n return key\n }\n // camelCase \u2192 kebab-case: mirror the JS reference exactly\n // (`key.replace(/([A-Z])/g, \'-$1\').toLowerCase()`). The JS shape\n // produces a leading `-` for an initial uppercase letter\n // (`XData` \u2192 `-x-data`); both this Go path and the matching JS\n // runtime are wrong-by-construction for that case (the resulting\n // HTML attribute name is invalid), but keeping them byte-equal\n // avoids silent SSR/CSR divergence (#1411 review).\n var b strings.Builder\n for _, r := range key {\n if r >= \'A\' && r <= \'Z\' {\n b.WriteByte(\'-\')\n b.WriteRune(r + 32)\n } else {\n b.WriteRune(r)\n }\n }\n return b.String()\n}\n\n// StyleToCss mirrors styleToCss from\n// packages/client/src/runtime/style.ts. Accepts a string passthrough,\n// or a map (JSON-deserialized object) whose camelCase keys are\n// lowered to kebab-case and joined with `;`. Returns ("", false) for\n// nullish/empty input so callers can omit the attribute entirely.\nfunc StyleToCss(v any) (string, bool) {\n if v == nil {\n return "", false\n }\n rv := reflect.ValueOf(v)\n for rv.Kind() == reflect.Interface || rv.Kind() == reflect.Pointer {\n if rv.IsNil() {\n return "", false\n }\n rv = rv.Elem()\n }\n if rv.Kind() != reflect.Map {\n // Non-object: stringify and return as-is, matching the JS\n // `typeof value !== \'object\'` branch.\n s := fmt.Sprint(v)\n if s == "" {\n return "", false\n }\n return s, true\n }\n keys := rv.MapKeys()\n sorted := make([]string, 0, len(keys))\n for _, k := range keys {\n if k.Kind() == reflect.String {\n sorted = append(sorted, k.String())\n }\n }\n sort.Strings(sorted)\n parts := make([]string, 0, len(sorted))\n for _, k := range sorted {\n val := rv.MapIndex(reflect.ValueOf(k))\n // Skip nil entries (matches the JS `if (v == null) continue`).\n if !val.IsValid() {\n continue\n }\n if val.Kind() == reflect.Interface || val.Kind() == reflect.Pointer {\n if val.IsNil() {\n continue\n }\n val = val.Elem()\n }\n prop := toAttrName(k)\n parts = append(parts, fmt.Sprintf("%s:%v", prop, val.Interface()))\n }\n if len(parts) == 0 {\n return "", false\n }\n return strings.Join(parts, ";"), true\n}\n\n// SpreadAttrs lowers a JSX intrinsic-element spread bag (#1407) to\n// an HTML attribute string. Mirrors spreadAttrs from\n// packages/client/src/runtime/spread-attrs.ts so SSR output matches\n// what CSR\'s `applyRestAttrs` writes at hydration.\n//\n// Skip rules: nil/false values, event handlers (`on[A-Z]*`),\n// `children`, `ref`.\n//\n// Key remap: className \u2192 class, htmlFor \u2192 for, SVG camelCase\n// preserved, other camelCase \u2192 kebab-case.\n//\n// `style` is routed through StyleToCss so object literals serialize\n// to a real CSS string instead of Go\'s default `map[k:v]` form.\n//\n// Booleans: true \u2192 bare attribute name, false \u2192 omitted.\n// Other scalar values are HTML-escaped via template.HTMLEscapeString.\n// Returns a `template.HTMLAttr` so html/template emits the result\n// verbatim (the function does its own escaping).\n//\n// Keys are sorted alphabetically before emission for deterministic\n// output. SSR/CSR attribute-order divergence is acceptable per the\n// rest-destructure-object-spread-in-map fixture\'s documented policy\n// \u2014 browsers honor the LAST value when a key is duplicated, so\n// pairing with static attrs (`<div class="x" {...rest}>`) is\n// last-wins regardless of order.\nfunc SpreadAttrs(bag any) template.HTMLAttr {\n if bag == nil {\n return ""\n }\n rv := reflect.ValueOf(bag)\n for rv.Kind() == reflect.Interface || rv.Kind() == reflect.Pointer {\n if rv.IsNil() {\n return ""\n }\n rv = rv.Elem()\n }\n if rv.Kind() != reflect.Map {\n return ""\n }\n keys := rv.MapKeys()\n sortedKeys := make([]string, 0, len(keys))\n for _, k := range keys {\n if k.Kind() == reflect.String {\n sortedKeys = append(sortedKeys, k.String())\n }\n }\n sort.Strings(sortedKeys)\n parts := make([]string, 0, len(sortedKeys))\n for _, key := range sortedKeys {\n // Event handlers \u2014 skip at SSR the same way\n // packages/client/src/runtime/spread-attrs.ts does at\n // hydration. The JS predicate is\n // `key.startsWith(\'on\') && key.length > 2 && key[2] === key[2].toUpperCase()`,\n // which is true for any character whose uppercase form is\n // itself: ASCII A-Z, digits, underscore, and non-letter\n // symbols. Mirror that here by skipping when key[2] is NOT\n // a lowercase ASCII letter \u2014 so `onClick`, `on_custom`, and\n // `on0` all match (#1411 review).\n if len(key) > 2 && key[0] == \'o\' && key[1] == \'n\' && !(key[2] >= \'a\' && key[2] <= \'z\') {\n continue\n }\n // `children` is a JSX construct rendered inside the element,\n // never a DOM attribute. `ref` is intentionally NOT filtered\n // here so output stays byte-equal with the JS reference\n // `spreadAttrs` in packages/client/src/runtime/spread-attrs.ts\n // (which only filters null/false, event handlers, and\n // children) \u2014 aligning Go\'s filter set diverges from JS in\n // the opposite direction. Filtering `ref` consistently across\n // both SSR runtimes is a separate concern tracked alongside\n // the JS `applyRestAttrs` vs `spreadAttrs` mismatch (#1411\n // review).\n if key == "children" {\n continue\n }\n val := rv.MapIndex(reflect.ValueOf(key))\n if !val.IsValid() {\n continue\n }\n // Unwrap interface wrappers (json.Unmarshal produces\n // interface{}-wrapped values for map[string]any).\n v := val\n for v.Kind() == reflect.Interface || v.Kind() == reflect.Pointer {\n if v.IsNil() {\n // Skip null entries.\n v = reflect.Value{}\n break\n }\n v = v.Elem()\n }\n if !v.IsValid() {\n continue\n }\n // Boolean values: true \u2192 bare attribute, false \u2192 omitted.\n if v.Kind() == reflect.Bool {\n if !v.Bool() {\n continue\n }\n parts = append(parts, toAttrName(key))\n continue\n }\n // `style` routes through StyleToCss so object literals get a\n // real CSS string. The JS side does the same.\n if key == "style" {\n css, ok := StyleToCss(v.Interface())\n if !ok {\n continue\n }\n parts = append(parts, fmt.Sprintf(`style="%s"`, template.HTMLEscapeString(css)))\n continue\n }\n // Stringify and escape. fmt.Sprint handles numbers, bools-as-\n // strings, and arbitrary stringer types the same way the JS\n // `String(value)` coercion does for the analogous cases.\n s := fmt.Sprint(v.Interface())\n parts = append(parts, fmt.Sprintf(`%s="%s"`, toAttrName(key), template.HTMLEscapeString(s)))\n }\n if len(parts) == 0 {\n return ""\n }\n return template.HTMLAttr(strings.Join(parts, " "))\n}\n\n// BfPropsAttr returns the bf-p attribute with the JSON-serialized\n// props in flat format. Output format: `bf-p=\'{"propName":value,...}\'`.\n// Only emits the attribute for root components (BfIsRoot == true);\n// child components receive props from their parent via initChild().\n//\n// Returns the marshal error so a `template.Execute` call fails\n// loudly on cycles / unsupported props rather than silently\n// dropping the bf-p attribute and breaking client-side hydration.\n// Same loud-failure policy as `JSON` \u2014 user data going through\n// `encoding/json` shouldn\'t fail invisibly.\nfunc BfPropsAttr(props interface{}) (template.HTMLAttr, error) {\n // Only root components should emit bf-p\n if !getBoolField(props, "BfIsRoot") {\n return "", nil\n }\n\n propsJSON, err := json.Marshal(props)\n if err != nil {\n return "", err\n }\n\n escaped := template.HTMLEscapeString(string(propsJSON))\n return template.HTMLAttr(`bf-p="` + escaped + `"`), nil\n}\n\n// =============================================================================\n// Arithmetic Operations\n// =============================================================================\n\n// Add returns a + b. Supports int and float64.\nfunc Add(a, b any) any {\n av, bv := toFloat64(a), toFloat64(b)\n result := av + bv\n // Return int if both inputs were int-like\n if isIntLike(a) && isIntLike(b) && result == float64(int(result)) {\n return int(result)\n }\n return result\n}\n\n// Sub returns a - b. Supports int and float64.\nfunc Sub(a, b any) any {\n av, bv := toFloat64(a), toFloat64(b)\n result := av - bv\n if isIntLike(a) && isIntLike(b) && result == float64(int(result)) {\n return int(result)\n }\n return result\n}\n\n// Mul returns a * b. Supports int and float64.\nfunc Mul(a, b any) any {\n av, bv := toFloat64(a), toFloat64(b)\n result := av * bv\n if isIntLike(a) && isIntLike(b) && result == float64(int(result)) {\n return int(result)\n }\n return result\n}\n\n// Div returns a / b. Returns float64 to match JavaScript behavior.\n// Returns 0 if b is 0 (instead of panicking).\nfunc Div(a, b any) any {\n av, bv := toFloat64(a), toFloat64(b)\n if bv == 0 {\n return 0\n }\n return av / bv\n}\n\n// Mod returns a % b (modulo). Supports int only.\nfunc Mod(a, b any) int {\n av, bv := toInt(a), toInt(b)\n if bv == 0 {\n return 0\n }\n return av % bv\n}\n\n// Neg returns -a (negation).\nfunc Neg(a any) any {\n if v, ok := a.(int); ok {\n return -v\n }\n return -toFloat64(a)\n}\n\n// =============================================================================\n// String Operations\n// =============================================================================\n\n// Lower returns the lowercase version of s.\nfunc Lower(s string) string {\n return strings.ToLower(s)\n}\n\n// Upper returns the uppercase version of s.\nfunc Upper(s string) string {\n return strings.ToUpper(s)\n}\n\n// Trim returns s with leading and trailing whitespace removed.\nfunc Trim(s string) string {\n return strings.TrimSpace(s)\n}\n\n// Contains returns true if s contains substr.\nfunc Contains(s, substr string) bool {\n return strings.Contains(s, substr)\n}\n\n// Split lowers `String.prototype.split(sep, limit?)` (#1448 Tier B). It\n// wraps `strings.Split` and normalises the result to `[]any` so the\n// slice composes with the array-method surface downstream (`bf_join`,\n// range loops, `bf_len`, \u2026) the same way `bf_slice` / `bf_reverse`\n// results do. Like JS, an empty separator splits into individual UTF-8\n// characters and trailing empty fields are preserved (`"a,".split(",")`\n// \u2192 `["a", ""]`). An optional `limit` caps the number of returned\n// pieces (`"a,b,c".split(",", 2)` \u2192 `["a", "b"]`); a negative limit is\n// ignored (JS would also return every piece \u2014 its ToUint32 wrap makes\n// the limit effectively unbounded). The no-separator form is handled by\n// the adapter (it emits `bf_arr` for the whole-string single element).\nfunc Split(s, sep string, limit ...int) []any {\n parts := strings.Split(s, sep)\n if len(limit) > 0 && limit[0] >= 0 && limit[0] < len(parts) {\n parts = parts[:limit[0]]\n }\n out := make([]any, len(parts))\n for i, p := range parts {\n out[i] = p\n }\n return out\n}\n\n// StartsWith lowers `String.prototype.startsWith(prefix, position?)`\n// (#1448 Tier B). Wraps `strings.HasPrefix`; an empty prefix is always\n// true (JS parity). The optional `position` re-anchors the test to start\n// at that index (clamped to `[0, len]` so it never panics), matching JS\n// `"abc".startsWith("b", 1) === true`.\nfunc StartsWith(s, prefix string, position ...int) bool {\n if len(position) > 0 {\n p := position[0]\n if p < 0 {\n p = 0\n }\n if p > len(s) {\n p = len(s)\n }\n s = s[p:]\n }\n return strings.HasPrefix(s, prefix)\n}\n\n// EndsWith lowers `String.prototype.endsWith(suffix, endPosition?)`\n// (#1448 Tier B). Wraps `strings.HasSuffix`; an empty suffix is always\n// true (JS parity). The optional `endPosition` treats the string as if\n// it were only that many bytes long (clamped to `[0, len]`), matching JS\n// `"abc".endsWith("b", 2) === true`.\nfunc EndsWith(s, suffix string, endPosition ...int) bool {\n if len(endPosition) > 0 {\n e := endPosition[0]\n if e < 0 {\n e = 0\n }\n if e > len(s) {\n e = len(s)\n }\n s = s[:e]\n }\n return strings.HasSuffix(s, suffix)\n}\n\n// Replace lowers the string-pattern form of `String.prototype.replace`\n// (#1448 Tier B). JS replaces only the FIRST occurrence for a string\n// pattern, so the count is 1 (`strings.Replace` with n=1; n<0 would\n// replace all \u2014 that\'s `.replaceAll`, still refused). The replacement\n// is treated literally: unlike JS, special replacement patterns like\n// `$&` / `$1` are NOT interpreted (Go and Perl agree on literal\n// replacement, keeping the two template adapters byte-equal; this\n// diverges from the Hono/CSR JS path only for replacement strings that\n// contain `$`-patterns, which are rare in template position).\nfunc Replace(s, old, new string) string {\n return strings.Replace(s, old, new, 1)\n}\n\n// Repeat lowers `String.prototype.repeat(n)` (#1448 Tier B): the\n// receiver concatenated n times. JS throws RangeError for a negative\n// count and `strings.Repeat` panics, so a negative count clamps to the\n// empty string \u2014 SSR templates degrade rather than crash the render.\n// A zero count is the empty string (JS parity).\nfunc Repeat(s string, n int) string {\n if n <= 0 {\n return ""\n }\n return strings.Repeat(s, n)\n}\n\n// padTo lowers the shared body of `String.prototype.padStart` /\n// `padEnd` (#1448 Tier B): pad `s` to `target` code points using `pad`\n// repeated and truncated to fill, prepended (atStart) or appended.\n// Length is measured in runes (not bytes) so the result matches the\n// Perl `bf->pad_*` helpers \u2014 this diverges from JS\'s UTF-16-unit length\n// only for astral-plane input. An empty pad, or a receiver already at\n// least `target` long, returns `s` unchanged (JS parity).\nfunc padTo(s string, target int, pad string, atStart bool) string {\n if pad == "" {\n return s\n }\n sLen := utf8.RuneCountInString(s)\n if sLen >= target {\n return s\n }\n need := target - sLen\n padRunes := []rune(pad)\n fill := make([]rune, 0, need)\n for len(fill) < need {\n for _, r := range padRunes {\n if len(fill) >= need {\n break\n }\n fill = append(fill, r)\n }\n }\n if atStart {\n return string(fill) + s\n }\n return s + string(fill)\n}\n\n// PadStart lowers `String.prototype.padStart(target, pad?)` (#1448 Tier\n// B). The pad string defaults to a single space when omitted.\nfunc PadStart(s string, target int, pad ...string) string {\n p := " "\n if len(pad) > 0 {\n p = pad[0]\n }\n return padTo(s, target, p, true)\n}\n\n// PadEnd lowers `String.prototype.padEnd(target, pad?)` (#1448 Tier B).\nfunc PadEnd(s string, target int, pad ...string) string {\n p := " "\n if len(pad) > 0 {\n p = pad[0]\n }\n return padTo(s, target, p, false)\n}\n\n// Join concatenates elements of a slice with sep. Accepts both\n// reflect.Slice (the common case \u2014 `bf_arr` and `bf_filter_truthy`\n// both return `[]any`) AND reflect.Array (fixed-size Go arrays like\n// `[3]string{...}`), mirroring JS `Array.prototype.join` which\n// doesn\'t distinguish between the two. Pre-fix this returned "" for\n// fixed-size arrays passed through template data (Copilot review on\n// #1445).\nfunc Join(items any, sep string) string {\n v := reflect.ValueOf(items)\n if v.Kind() != reflect.Slice && v.Kind() != reflect.Array {\n return ""\n }\n\n parts := make([]string, v.Len())\n for i := 0; i < v.Len(); i++ {\n parts[i] = toString(v.Index(i).Interface())\n }\n return strings.Join(parts, sep)\n}\n\n// String returns the string form of v. Mirrors JS `String(v)` for\n// non-nil values via `fmt.Sprintf("%v", ...)`. Diverges from JS on\n// nil: JS `String(null)` is "null", but the template path renders\n// `nil` as the empty string here so an unset prop doesn\'t surface\n// as a literal "null"/"undefined" in user-facing HTML. Document the\n// divergence explicitly so callers don\'t rely on JS-exact parity.\nfunc String(v any) string {\n if v == nil {\n return ""\n }\n return fmt.Sprintf("%v", v)\n}\n\n// JSON returns the JSON encoding of v as a string. Mirrors\n// JS `JSON.stringify(v)` for the V1 single-arg shape (no `replacer`\n// or `space`). Object key order is determined by Go\'s `encoding/json`\n// (alphabetical for maps, declaration order for structs) \u2014 the\n// #1187 contract requires value-compat, not order-compat.\n//\n// Top-level NaN / \xB1Inf are pre-handled to match JS \u2014 JS\'s\n// `JSON.stringify(NaN)` and `JSON.stringify(Infinity)` both produce\n// `"null"`, but Go\'s `encoding/json` rejects them with\n// `UnsupportedValueError`. Without this carve-out the common\n// composition `JSON.stringify(Number("garbage"))` would error\n// instead of emitting `"null"` like JS does. Nested NaN/Inf inside\n// a struct/map still surfaces an error \u2014 covering that needs a\n// custom marshaller; out of V1 scope.\n//\n// Returns the marshal error so a `template.Execute` call fails\n// loudly on cycles / unsupported values rather than silently\n// producing `""` and reintroducing the SSR data-loss class\n// #1187 was filed against. Go\'s text/template treats a non-nil\n// error return from a func as an execution failure.\nfunc JSON(v any) (string, error) {\n if f, ok := v.(float64); ok && (math.IsNaN(f) || math.IsInf(f, 0)) {\n return "null", nil\n }\n b, err := json.Marshal(v)\n if err != nil {\n return "", err\n }\n return string(b), nil\n}\n\n// Number coerces v to a float64. Mirrors JS `Number(v)` semantics:\n// numeric / boolean inputs convert as expected; non-numeric strings\n// and other unsupported shapes return `NaN` (matching JS rather\n// than silently substituting 0, which would mis-shape downstream\n// arithmetic and template-side comparisons). Templates that need\n// a deterministic fallback should compose with the user-side\n// default (e.g. `Number(props.x ?? 0)` in JSX).\nfunc Number(v any) float64 {\n if v == nil {\n return math.NaN()\n }\n switch x := v.(type) {\n case float64:\n return x\n case float32:\n return float64(x)\n case int:\n return float64(x)\n case int32:\n return float64(x)\n case int64:\n return float64(x)\n case bool:\n if x {\n return 1\n }\n return 0\n case string:\n f, err := strconv.ParseFloat(x, 64)\n if err != nil {\n return math.NaN()\n }\n return f\n }\n return math.NaN()\n}\n\n// Floor returns the largest integer \u2264 v as a float64. Mirrors JS\n// `Math.floor`. The return type stays float64 so chained primitives\n// (`bf_floor` then `bf_string`) line up with JS\'s number type.\nfunc Floor(v any) float64 {\n return math.Floor(Number(v))\n}\n\n// Ceil returns the smallest integer \u2265 v as a float64. Mirrors JS\n// `Math.ceil`.\nfunc Ceil(v any) float64 {\n return math.Ceil(Number(v))\n}\n\n// Round returns v rounded to the nearest integer as a float64.\n// Mirrors JS `Math.round` \u2014 half-away-from-zero (Go\'s `math.Round`\n// matches; JS rounds half toward +Infinity which differs at .5\n// negatives; we accept that minor divergence since the conformance\n// contract is value-compat for the common positive case).\nfunc Round(v any) float64 {\n return math.Round(Number(v))\n}\n\n// =============================================================================\n// Array/Slice Operations\n// =============================================================================\n\n// Len returns the length of a slice, array, map, string, or channel.\n// Returns 0 for nil or unsupported types.\nfunc Len(v any) int {\n if v == nil {\n return 0\n }\n rv := reflect.ValueOf(v)\n switch rv.Kind() {\n case reflect.Slice, reflect.Array, reflect.Map, reflect.String, reflect.Chan:\n return rv.Len()\n default:\n return 0\n }\n}\n\n// At returns the element at index i from a slice.\n// Supports negative indices (e.g., -1 for last element).\n// Returns nil if index is out of bounds.\nfunc At(items any, index int) any {\n v := reflect.ValueOf(items)\n if v.Kind() != reflect.Slice && v.Kind() != reflect.Array {\n return nil\n }\n\n length := v.Len()\n if length == 0 {\n return nil\n }\n\n // Handle negative indices\n if index < 0 {\n index = length + index\n }\n\n if index < 0 || index >= length {\n return nil\n }\n\n return v.Index(index).Interface()\n}\n\n// Includes returns true if items contains elem. Lowers both\n// `Array.prototype.includes` and `String.prototype.includes` \u2014\n// the adapter can\'t disambiguate the receiver at compile time,\n// so this helper dispatches at runtime on `reflect.Kind()`:\n//\n// - slice/array receiver: DeepEqual element search\n// - string receiver: strings.Contains substring search\n//\n// Anything else returns false (matches the JS semantic where\n// `.includes` is only defined on Array / TypedArray / String).\nfunc Includes(recv any, elem any) bool {\n v := reflect.ValueOf(recv)\n if v.Kind() == reflect.String {\n // JS `String.prototype.includes` accepts only string args;\n // non-string `elem` would TypeError in real JS but our\n // callers have lowered through `convertExpressionToGo`\n // where the arg type is whatever the template binds. Stringify\n // via fmt to keep the helper total.\n needle, ok := elem.(string)\n if !ok {\n needle = fmt.Sprintf("%v", elem)\n }\n return strings.Contains(v.String(), needle)\n }\n if v.Kind() != reflect.Slice && v.Kind() != reflect.Array {\n return false\n }\n for i := 0; i < v.Len(); i++ {\n if reflect.DeepEqual(v.Index(i).Interface(), elem) {\n return true\n }\n }\n return false\n}\n\n// IndexOf returns the 0-based position of the first item that\n// DeepEquals `elem`, or -1 if not found. Lowers\n// `Array.prototype.indexOf(x)` (#1448 Tier A). The existing\n// `FindIndex` helper does struct-field equality (used by the\n// higher-order `.find` lowering); this one does value equality\n// against scalar / struct items so callers don\'t have to compose\n// a synthetic predicate.\n//\n// Non-array / non-slice receivers return -1 (matches the JS\n// semantic that `.indexOf` is only defined on Array / TypedArray).\nfunc IndexOf(items any, elem any) int {\n v := reflect.ValueOf(items)\n if v.Kind() != reflect.Slice && v.Kind() != reflect.Array {\n return -1\n }\n for i := 0; i < v.Len(); i++ {\n if reflect.DeepEqual(v.Index(i).Interface(), elem) {\n return i\n }\n }\n return -1\n}\n\n// LastIndexOf returns the 0-based position of the last item that\n// DeepEquals `elem`, or -1 if not found. Mirrors\n// `Array.prototype.lastIndexOf(x)`. The reverse traversal is the\n// only behavioural difference vs `IndexOf` \u2014 disambiguating a\n// duplicated value\'s first vs last position is the canonical\n// reason a JS author reaches for `lastIndexOf`.\nfunc LastIndexOf(items any, elem any) int {\n v := reflect.ValueOf(items)\n if v.Kind() != reflect.Slice && v.Kind() != reflect.Array {\n return -1\n }\n for i := v.Len() - 1; i >= 0; i-- {\n if reflect.DeepEqual(v.Index(i).Interface(), elem) {\n return i\n }\n }\n return -1\n}\n\n// Concat merges two arrays (or slices) into a single `[]any`,\n// preserving order: receiver elements first, then `other`\'s.\n// Lowers `Array.prototype.concat(other)` (#1448 Tier A). Non-array\n// operands collapse to an empty source \u2014 matches the JS semantic\n// where `.concat` on a non-Array reads it as a single element only\n// if its `Symbol.isConcatSpreadable` is true; the template-language\n// path doesn\'t have user objects with that flag, so treating\n// non-arrays as empty is the conservative lowering. Variadic\n// `.concat(a, b, c)` is out of scope here (parser gates to a single\n// arg); the helper itself stays binary so a future variadic IR can\n// fold via repeated calls without changing this signature.\nfunc Concat(a, b any) []any {\n flatten := func(v reflect.Value) []any {\n if !v.IsValid() {\n return nil\n }\n if v.Kind() != reflect.Slice && v.Kind() != reflect.Array {\n return nil\n }\n out := make([]any, v.Len())\n for i := 0; i < v.Len(); i++ {\n out[i] = v.Index(i).Interface()\n }\n return out\n }\n left := flatten(reflect.ValueOf(a))\n right := flatten(reflect.ValueOf(b))\n return append(left, right...)\n}\n\n// Slice carves out a sub-range from `items`. Lowers\n// `Array.prototype.slice(start, end?)` (#1448 Tier A). The variadic\n// `end` arg lets Go template\'s call dispatcher pass either 2 or 3\n// arguments; an absent end means "to length".\n//\n// JS-compat clamping:\n// - start < 0 \u2192 length + start (e.g. -1 = last index)\n// - end < 0 \u2192 length + end\n// - start < 0 after clamp \u2192 0\n// - end > length \u2192 length\n// - start >= end \u2192 empty slice (no panic)\n//\n// Non-array receivers return an empty `[]any`.\nfunc Slice(items any, start int, end ...int) []any {\n v := reflect.ValueOf(items)\n if v.Kind() != reflect.Slice && v.Kind() != reflect.Array {\n return []any{}\n }\n length := v.Len()\n\n // Normalise start (negative = from end).\n if start < 0 {\n start = length + start\n }\n if start < 0 {\n start = 0\n }\n if start > length {\n start = length\n }\n\n // Normalise end (optional; absent = length).\n stop := length\n if len(end) > 0 {\n stop = end[0]\n if stop < 0 {\n stop = length + stop\n }\n if stop < 0 {\n stop = 0\n }\n if stop > length {\n stop = length\n }\n }\n\n if start >= stop {\n return []any{}\n }\n\n out := make([]any, 0, stop-start)\n for i := start; i < stop; i++ {\n out = append(out, v.Index(i).Interface())\n }\n return out\n}\n\n// Reverse returns a new slice with `items`\'s elements in reverse\n// order. Lowers both `Array.prototype.reverse()` and\n// `Array.prototype.toReversed()` (#1448 Tier A) \u2014 SSR templates\n// render a snapshot, so JS\'s mutate-receiver vs return-new-array\n// distinction has no template-level meaning, and the safer\n// non-mutating shape is used uniformly.\n//\n// Non-array receivers return an empty `[]any`.\nfunc Reverse(items any) []any {\n v := reflect.ValueOf(items)\n if v.Kind() != reflect.Slice && v.Kind() != reflect.Array {\n return []any{}\n }\n length := v.Len()\n out := make([]any, length)\n for i := 0; i < length; i++ {\n out[length-1-i] = v.Index(i).Interface()\n }\n return out\n}\n\n// Flat flattens nested slices/arrays `depth` levels deep. Lowers\n// `Array.prototype.flat(depth?)` (#1448 Tier C). A `depth` of `-1` is the\n// `Infinity` sentinel (flatten fully); `0` (or negative-from-JS, already\n// normalised to 0 at compile time) returns a shallow copy. Non-array\n// elements are kept as-is (JS only flattens nested arrays). A non-array\n// receiver returns an empty `[]any`.\nfunc Flat(items any, depth int) []any {\n v := reflect.ValueOf(items)\n if v.Kind() != reflect.Slice && v.Kind() != reflect.Array {\n return []any{}\n }\n out := make([]any, 0, v.Len())\n for i := 0; i < v.Len(); i++ {\n el := v.Index(i).Interface()\n ev := reflect.ValueOf(el)\n if depth != 0 && (ev.Kind() == reflect.Slice || ev.Kind() == reflect.Array) {\n // `-1` (Infinity) recurses unbounded; a finite depth spends one level.\n next := depth\n if depth > 0 {\n next = depth - 1\n }\n out = append(out, Flat(el, next)...)\n } else {\n out = append(out, el)\n }\n }\n return out\n}\n\n// FlatMap projects each element through a `self` / `field` projection and\n// flattens the result one level. Lowers value-returning\n// `Array.prototype.flatMap(fn)` for the field-projection catalogue\n// (#1448 Tier C): `items.flatMap(i => i)` (self) and\n// `items.flatMap(i => i.field)` (field). A projected non-array value is\n// kept as-is (flatMap = map + flat(1)). Non-array receiver \u2192 empty.\nfunc FlatMap(items any, keyKind, keyName string) []any {\n v := reflect.ValueOf(items)\n if v.Kind() != reflect.Slice && v.Kind() != reflect.Array {\n return []any{}\n }\n projected := make([]any, 0, v.Len())\n for i := 0; i < v.Len(); i++ {\n el := v.Index(i).Interface()\n if keyKind == "field" {\n projected = append(projected, getFieldValue(el, keyName))\n } else {\n projected = append(projected, el)\n }\n }\n return Flat(projected, 1)\n}\n\n// FlatMapTuple lowers an array-literal flatMap projection\n// `items.flatMap(i => [i.a, i.b])` (#1448 Tier C). `specs` is a flat list\n// of (kind, name) pairs, one per array-literal leaf: ("self", "") for the\n// item itself, ("field", "<Name>") for a struct field. For each item it\n// appends every leaf\'s value in order. Unlike the scalar `FlatMap`, the\n// per-item array is flattened only one level (flat(1) removes the literal\n// wrapper), so an array-valued leaf is appended verbatim rather than\n// spread \u2014 which is exactly "append each leaf". Non-array receiver \u2192 empty.\nfunc FlatMapTuple(items any, specs ...string) []any {\n v := reflect.ValueOf(items)\n if v.Kind() != reflect.Slice && v.Kind() != reflect.Array {\n return []any{}\n }\n out := make([]any, 0, v.Len())\n for i := 0; i < v.Len(); i++ {\n el := v.Index(i).Interface()\n for j := 0; j+1 < len(specs); j += 2 {\n if specs[j] == "field" {\n out = append(out, getFieldValue(el, specs[j+1]))\n } else {\n out = append(out, el)\n }\n }\n }\n return out\n}\n\n// First returns the first element of a slice, or nil if empty.\nfunc First(items any) any {\n return At(items, 0)\n}\n\n// Last returns the last element of a slice, or nil if empty.\nfunc Last(items any) any {\n return At(items, -1)\n}\n\n// Arr builds an []any from variadic args. Used to lower JS array\n// literals like `[a, b]` for the registry Slot\'s\n// `[className, childClass].filter(Boolean).join(\' \')` shape (#1443) \u2014\n// Go templates have no array-literal syntax, so the codegen routes\n// array-literal IR nodes through this helper.\nfunc Arr(items ...any) []any {\n return items\n}\n\n// FilterTruthy returns a new slice containing only truthy items.\n// Mirrors `arr.filter(Boolean)` semantics: drop nil, false, 0, "" \u2014 the\n// same falsy set JavaScript\'s `Boolean(x)` recognises. Used to lower\n// the registry Slot\'s class-merge pattern (#1443); generalising to\n// arbitrary callable predicates would need the callee-resolution path\n// blocked by #1389, so this stays Boolean-specific.\nfunc FilterTruthy(items any) []any {\n v := reflect.ValueOf(items)\n if !v.IsValid() || (v.Kind() != reflect.Slice && v.Kind() != reflect.Array) {\n return nil\n }\n result := make([]any, 0, v.Len())\n for i := 0; i < v.Len(); i++ {\n raw := v.Index(i).Interface()\n if isTruthy(raw) {\n result = append(result, raw)\n }\n }\n return result\n}\n\n// isTruthy mirrors JavaScript\'s `Boolean(x)` for the value shapes the\n// template path actually receives \u2014 nil / false / 0 / "" are falsy.\n// Other shapes (non-empty maps, slices, structs, true) are truthy, in\n// line with JS\'s "objects are truthy" rule.\nfunc isTruthy(v any) bool {\n if v == nil {\n return false\n }\n switch x := v.(type) {\n case bool:\n return x\n case string:\n return x != ""\n case int:\n return x != 0\n case int8, int16, int32, int64:\n return reflect.ValueOf(v).Int() != 0\n case uint, uint8, uint16, uint32, uint64:\n return reflect.ValueOf(v).Uint() != 0\n case float32:\n // JS `Boolean(NaN)` is false regardless of float width \u2014 the\n // float64 arm below was the only one checking IsNaN, which\n // diverged from JS for `float32` NaN inputs (Copilot review on\n // #1445). Widening to float64 for the IsNaN check keeps the\n // two branches in lock-step.\n return x != 0 && !math.IsNaN(float64(x))\n case float64:\n return x != 0 && !math.IsNaN(x)\n }\n return true\n}\n\n// =============================================================================\n// Higher-order Array Methods\n// =============================================================================\n\n// Every returns true if all items have the specified field set to true.\n// Mirrors JavaScript\'s Array.prototype.every(item => item.field).\nfunc Every(items any, field string) bool {\n v := reflect.ValueOf(items)\n if v.Kind() != reflect.Slice && v.Kind() != reflect.Array {\n return false\n }\n\n capitalizedField := capitalize(field)\n for i := 0; i < v.Len(); i++ {\n item := v.Index(i)\n if item.Kind() == reflect.Interface {\n item = item.Elem()\n }\n if item.Kind() == reflect.Ptr {\n item = item.Elem()\n }\n if item.Kind() != reflect.Struct {\n continue\n }\n\n fieldVal := item.FieldByName(capitalizedField)\n if !fieldVal.IsValid() {\n return false\n }\n if fieldVal.Kind() == reflect.Bool && !fieldVal.Bool() {\n return false\n }\n }\n return true\n}\n\n// Some returns true if at least one item has the specified field set to true.\n// Mirrors JavaScript\'s Array.prototype.some(item => item.field).\nfunc Some(items any, field string) bool {\n v := reflect.ValueOf(items)\n if v.Kind() != reflect.Slice && v.Kind() != reflect.Array {\n return false\n }\n\n capitalizedField := capitalize(field)\n for i := 0; i < v.Len(); i++ {\n item := v.Index(i)\n if item.Kind() == reflect.Interface {\n item = item.Elem()\n }\n if item.Kind() == reflect.Ptr {\n item = item.Elem()\n }\n if item.Kind() != reflect.Struct {\n continue\n }\n\n fieldVal := item.FieldByName(capitalizedField)\n if fieldVal.IsValid() && fieldVal.Kind() == reflect.Bool && fieldVal.Bool() {\n return true\n }\n }\n return false\n}\n\n// Filter returns items where item.field == value.\n// Mirrors JavaScript\'s Array.prototype.filter(item => item.field === value).\n// Returns []any to allow chaining with other bf_* functions.\nfunc Filter(items any, field string, value any) []any {\n v := reflect.ValueOf(items)\n if v.Kind() != reflect.Slice && v.Kind() != reflect.Array {\n return nil\n }\n\n capitalizedField := capitalize(field)\n var result []any\n\n for i := 0; i < v.Len(); i++ {\n item := v.Index(i)\n if item.Kind() == reflect.Interface {\n item = item.Elem()\n }\n if item.Kind() == reflect.Ptr {\n item = item.Elem()\n }\n if item.Kind() != reflect.Struct {\n continue\n }\n\n fieldVal := item.FieldByName(capitalizedField)\n if !fieldVal.IsValid() {\n continue\n }\n\n // Compare field value with target value\n if reflect.DeepEqual(fieldVal.Interface(), value) {\n result = append(result, v.Index(i).Interface())\n }\n }\n return result\n}\n\n// Find returns the first item where item.field == value, or nil if not found.\n// Mirrors JavaScript\'s Array.prototype.find(item => item.field === value).\nfunc Find(items any, field string, value any) any {\n v := reflect.ValueOf(items)\n if v.Kind() != reflect.Slice && v.Kind() != reflect.Array {\n return nil\n }\n\n capitalizedField := capitalize(field)\n for i := 0; i < v.Len(); i++ {\n item := v.Index(i)\n if item.Kind() == reflect.Interface {\n item = item.Elem()\n }\n if item.Kind() == reflect.Ptr {\n item = item.Elem()\n }\n if item.Kind() != reflect.Struct {\n continue\n }\n\n fieldVal := item.FieldByName(capitalizedField)\n if !fieldVal.IsValid() {\n continue\n }\n\n if reflect.DeepEqual(fieldVal.Interface(), value) {\n return v.Index(i).Interface()\n }\n }\n return nil\n}\n\n// FindIndex returns the index of the first item where item.field == value, or -1.\n// Mirrors JavaScript\'s Array.prototype.findIndex(item => item.field === value).\nfunc FindIndex(items any, field string, value any) int {\n v := reflect.ValueOf(items)\n if v.Kind() != reflect.Slice && v.Kind() != reflect.Array {\n return -1\n }\n\n capitalizedField := capitalize(field)\n for i := 0; i < v.Len(); i++ {\n item := v.Index(i)\n if item.Kind() == reflect.Interface {\n item = item.Elem()\n }\n if item.Kind() == reflect.Ptr {\n item = item.Elem()\n }\n if item.Kind() != reflect.Struct {\n continue\n }\n\n fieldVal := item.FieldByName(capitalizedField)\n if !fieldVal.IsValid() {\n continue\n }\n\n if reflect.DeepEqual(fieldVal.Interface(), value) {\n return i\n }\n }\n return -1\n}\n\n// FindLast returns the last item where item.field == value, or nil if not found.\n// Mirrors JavaScript\'s Array.prototype.findLast(item => item.field === value).\nfunc FindLast(items any, field string, value any) any {\n v := reflect.ValueOf(items)\n if v.Kind() != reflect.Slice && v.Kind() != reflect.Array {\n return nil\n }\n\n capitalizedField := capitalize(field)\n for i := v.Len() - 1; i >= 0; i-- {\n item := v.Index(i)\n if item.Kind() == reflect.Interface {\n if item.IsNil() {\n continue\n }\n item = item.Elem()\n }\n if item.Kind() == reflect.Ptr {\n if item.IsNil() {\n continue\n }\n item = item.Elem()\n }\n if item.Kind() != reflect.Struct {\n continue\n }\n\n fieldVal := item.FieldByName(capitalizedField)\n if !fieldVal.IsValid() {\n continue\n }\n\n if reflect.DeepEqual(fieldVal.Interface(), value) {\n return v.Index(i).Interface()\n }\n }\n return nil\n}\n\n// FindLastIndex returns the index of the last item where item.field == value, or -1.\n// Mirrors JavaScript\'s Array.prototype.findLastIndex(item => item.field === value).\nfunc FindLastIndex(items any, field string, value any) int {\n v := reflect.ValueOf(items)\n if v.Kind() != reflect.Slice && v.Kind() != reflect.Array {\n return -1\n }\n\n capitalizedField := capitalize(field)\n for i := v.Len() - 1; i >= 0; i-- {\n item := v.Index(i)\n if item.Kind() == reflect.Interface {\n if item.IsNil() {\n continue\n }\n item = item.Elem()\n }\n if item.Kind() == reflect.Ptr {\n if item.IsNil() {\n continue\n }\n item = item.Elem()\n }\n if item.Kind() != reflect.Struct {\n continue\n }\n\n fieldVal := item.FieldByName(capitalizedField)\n if !fieldVal.IsValid() {\n continue\n }\n\n if reflect.DeepEqual(fieldVal.Interface(), value) {\n return i\n }\n }\n return -1\n}\n\n// sortKeySpec is one parsed comparison key. A simple comparator has\n// one; a `||`-chained multi-key comparator has several, applied in\n// order as tie-breakers.\ntype sortKeySpec struct {\n kind string // "self" | "field"\n name string // capitalised field name, or "" for "self"\n compareType string // "numeric" | "string" | "auto"\n direction string // "asc" | "desc"\n}\n\n// Sort returns a new stable-sorted slice. Lowers\n// `Array.prototype.sort` / `Array.prototype.toSorted` (#1448 Tier B).\n// Non-mutating \u2014 JS\'s mutate-vs-new distinction is moot in SSR\n// template context (templates render a snapshot).\n//\n// Call shape (the compiler emits one 4-string group per key):\n//\n// bf_sort <items> (<keyKind> <keyName> <compareType> <direction>)+\n//\n// keyKind: "self" | "field"\n// keyName: "" when keyKind == "self"; capitalised struct field\n// name (e.g. "Price") otherwise\n// compareType: "numeric" | "string" | "auto"\n// direction: "asc" | "desc"\n//\n// The groups cover the accepted comparator catalogue: `a.f - b.f`,\n// `a - b`, `a[.f].localeCompare(b[.f])`, and relational-ternary keys\n// (`a.f > b.f ? 1 : -1` \u2192 "auto"), each `||`-chainable for multi-key\n// tie-breaks. Anything outside refuses at compile time (BF101 from the\n// JSX compiler) and never reaches this helper.\n//\n// "auto" compares numerically when both projected keys parse as\n// numbers, else lexically \u2014 mirroring the Perl `bf->sort` helper\'s\n// `looks_like_number` rule so the two template adapters stay\n// byte-equal. This diverges from JS `<`/`>` only for numeric strings.\n//\n// A future `nulls` knob can extend the per-key group without rewriting\n// existing call sites \u2014 each key already projects before comparing.\nfunc Sort(items any, spec ...string) []any {\n v := reflect.ValueOf(items)\n if v.Kind() != reflect.Slice && v.Kind() != reflect.Array {\n return nil\n }\n\n length := v.Len()\n if length == 0 {\n return []any{}\n }\n\n // Copy into a fresh []any so the sort is non-mutating regardless\n // of whether the receiver is `[]T` or `[]any`.\n result := make([]any, length)\n for i := 0; i < length; i++ {\n result[i] = v.Index(i).Interface()\n }\n\n keys := parseSortSpec(spec)\n sort.SliceStable(result, func(i, j int) bool {\n for _, k := range keys {\n ki := projectSortKey(result[i], k.kind, k.name)\n kj := projectSortKey(result[j], k.kind, k.name)\n c := compareSortKey(ki, kj, k.compareType)\n if c == 0 {\n continue // tie on this key \u2014 fall through to the next\n }\n if k.direction == "desc" {\n return c > 0\n }\n return c < 0\n }\n return false\n })\n\n return result\n}\n\n// parseSortSpec chunks the variadic operand list into 4-string key\n// groups. A trailing partial group (malformed emit) is ignored rather\n// than panicking \u2014 defensive, mirroring the helper\'s nil-safe stance.\nfunc parseSortSpec(spec []string) []sortKeySpec {\n var keys []sortKeySpec\n for i := 0; i+3 < len(spec); i += 4 {\n keys = append(keys, sortKeySpec{\n kind: spec[i],\n name: spec[i+1],\n compareType: spec[i+2],\n direction: spec[i+3],\n })\n }\n return keys\n}\n\n// compareSortKey returns -1 / 0 / 1 for two projected keys under the\n// given compare type (ascending orientation; the caller flips for\n// "desc"). "string" stringifies both (nil \u2192 "", matching the\n// documented `bf->string(undef) === ""` divergence). "auto" compares\n// numerically when both parse as numbers, else lexically.\nfunc compareSortKey(ki, kj any, compareType string) int {\n switch compareType {\n case "string":\n return strings.Compare(toString(ki), toString(kj))\n case "auto":\n ni, okI := toFloat64WithOK(ki)\n nj, okJ := toFloat64WithOK(kj)\n if okI && okJ {\n return cmpFloat(ni, nj)\n }\n return strings.Compare(toString(ki), toString(kj))\n default: // numeric\n return cmpFloat(toFloat64(ki), toFloat64(kj))\n }\n}\n\nfunc cmpFloat(a, b float64) int {\n if a < b {\n return -1\n }\n if a > b {\n return 1\n }\n return 0\n}\n\n// toFloat64WithOK reports a value\'s numeric float and whether it is\n// number-like. Genuine numeric kinds always qualify; strings qualify\n// when they parse as a float (so the "auto" compare path matches the\n// Perl `looks_like_number` rule). Everything else is non-numeric.\nfunc toFloat64WithOK(v any) (float64, bool) {\n switch n := v.(type) {\n case int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64, float32, float64:\n return toFloat64(v), true\n case string:\n f, err := strconv.ParseFloat(strings.TrimSpace(n), 64)\n if err != nil {\n return 0, false\n }\n return f, true\n default:\n return 0, false\n }\n}\n\n// projectSortKey reduces an item to the value the comparator\n// actually compares. For `keyKind == "field"` it reads the named\n// struct field; for `keyKind == "self"` (primitive arrays) it\n// returns the item unchanged.\nfunc projectSortKey(item any, keyKind, keyName string) any {\n if keyKind == "field" {\n return getFieldValue(item, keyName)\n }\n return item\n}\n\n// getFieldValue extracts a struct field value using reflection. For\n// map receivers it falls back to case-variant lookup so JSON-decoded\n// user data (`map[string]any{"price": 30}`) and PascalCase-emitted\n// test data both resolve under a single key name. (#1487)\nfunc getFieldValue(item any, field string) any {\n v := reflect.ValueOf(item)\n // Defensive IsNil guards mirror `SpreadAttrs` \u2014 keeps the helper\n // safe against typed-nil pointer / nil-interface items inside a\n // `[]any` so a single bad row doesn\'t crash the whole sort.\n if v.Kind() == reflect.Interface {\n if v.IsNil() {\n return nil\n }\n v = v.Elem()\n }\n if v.Kind() == reflect.Ptr {\n if v.IsNil() {\n return nil\n }\n v = v.Elem()\n }\n\n if v.Kind() == reflect.Map {\n keyType := v.Type().Key()\n if keyType.Kind() != reflect.String {\n return nil\n }\n // Convert the lookup string to the map\'s actual key type so\n // maps keyed by a named string type (`type Key string`) don\'t\n // panic with `value of type string is not assignable to type X`.\n lookup := func(s string) (any, bool) {\n k := reflect.ValueOf(s).Convert(keyType)\n if mv := v.MapIndex(k); mv.IsValid() {\n return mv.Interface(), true\n }\n return nil, false\n }\n if r, ok := lookup(field); ok {\n return r\n }\n if cap := capitalize(field); cap != field {\n if r, ok := lookup(cap); ok {\n return r\n }\n }\n if low := decapitalize(field); low != field {\n if r, ok := lookup(low); ok {\n return r\n }\n }\n // All-lowercase fallback: a Go-initialism field projects as an\n // all-caps key (`id` \u2192 `ID`), and `decapitalize("ID")` only\n // lowers the first char (`iD`), so the JS-keyed map ("id") still\n // misses. Try the fully-lowered key last to resolve it.\n if lower := strings.ToLower(field); lower != field && lower != decapitalize(field) {\n if r, ok := lookup(lower); ok {\n return r\n }\n }\n return nil\n }\n\n if v.Kind() != reflect.Struct {\n return nil\n }\n\n fieldVal := v.FieldByName(field)\n if !fieldVal.IsValid() {\n return nil\n }\n return fieldVal.Interface()\n}\n\n// capitalize uppercases the first character of a string.\nfunc capitalize(s string) string {\n if s == "" {\n return s\n }\n return strings.ToUpper(s[:1]) + s[1:]\n}\n\n// decapitalize lowercases the first character of a string. Used by\n// `getFieldValue`\'s map-receiver fallback when the projected key\n// name is PascalCase but the receiver carries lowercase JS-style\n// keys (the inverse of the `capitalize` lookup).\nfunc decapitalize(s string) string {\n if s == "" {\n return s\n }\n return strings.ToLower(s[:1]) + s[1:]\n}\n\n// Reduce folds an array into a scalar via the arithmetic-fold\n// catalogue (#1448 Tier C). It lowers `Array.prototype.reduce(fn, init)`\n// and `Array.prototype.reduceRight(fn, init)` for the shapes\n// `(acc, x) => acc <op> x` and `(acc, x) => acc <op> x.field`:\n//\n// bf_reduce <items> "<op>" "<keyKind>" "<keyName>" "<type>" "<init>" "<direction>"\n//\n// direction: "left" (reduce) | "right" (reduceRight). Only changes the\n// result for string concatenation; numeric folds commute.\n//\n// op: "+" | "*"\n// keyKind: "self" | "field"\n// keyName: "" when keyKind == "self"; capitalised struct field name\n// (e.g. "Duration") otherwise\n// type: "numeric" | "string"\n// init: the fold\'s start value \u2014 the compiler emits the *decoded*\n// seed, so numeric inits arrive as canonical decimal\n// (`1_000`/`0x10` already normalised to `1000`/`16`) that\n// ParseFloat accepts, and string inits arrive as escape-free\n// contents\n//\n// Numeric folds accumulate as float64; each projected key is read via\n// `toFloat64WithOK`, so numeric *strings* ("5" \u2192 5) parse and\n// non-numeric values fold as 0 \u2014 matching Perl\'s\n// `looks_like_number ? $n : 0` so the two template adapters stay\n// byte-equal. String folds concatenate (toString per projected key,\n// matching the documented `bf->string(undef) === ""` convention). The\n// init seeds the accumulator, so an empty receiver returns the init\n// unchanged \u2014 exactly like JS `reduce(fn, init)`. Anything outside the\n// catalogue refuses at compile time (BF101 from the JSX compiler) and\n// never reaches here.\n//\n// Two documented divergences from the JS / Hono path, both rare and\n// mirroring the `bf_sort` "auto" caveat:\n// - float64 stringification differs for sums whose binary expansion\n// isn\'t exact (e.g. 0.1 + 0.2);\n// - numeric-*string* keys fold numerically here, but JS `+`\n// string-concatenates once an operand is a string, so\n// numeric-string data can render differently under CSR.\n// Genuine numbers \u2014 the common SSR case \u2014 agree across all three.\nfunc Reduce(items any, op, keyKind, keyName, typ, init, direction string) any {\n v := reflect.ValueOf(items)\n isSlice := v.Kind() == reflect.Slice || v.Kind() == reflect.Array\n\n // `direction == "right"` (reduceRight) folds right-to-left. Only\n // observable for string concatenation \u2014 numeric sum / product are\n // commutative, so the order doesn\'t change the result there. Build a\n // start/stop/step triple so both folds share one loop shape.\n start, stop, step := 0, 0, 1\n if isSlice {\n stop = v.Len()\n if direction == "right" {\n start, stop, step = v.Len()-1, -1, -1\n }\n }\n\n if typ == "string" {\n acc := init\n if isSlice {\n for i := start; i != stop; i += step {\n key := projectSortKey(v.Index(i).Interface(), keyKind, keyName)\n acc += toString(key)\n }\n }\n return acc\n }\n\n // numeric fold\n acc, _ := strconv.ParseFloat(strings.TrimSpace(init), 64)\n if isSlice {\n for i := start; i != stop; i += step {\n key := projectSortKey(v.Index(i).Interface(), keyKind, keyName)\n // `toFloat64WithOK` parses numeric *strings* ("5" \u2192 5) and\n // returns 0 for non-numeric values \u2014 mirroring Perl\'s\n // `looks_like_number ? $n : 0` so numeric-string data folds\n // byte-equal across adapters (the same rule `bf_sort`\'s\n // "auto" compare uses). Plain `toFloat64` would zero "5".\n n, _ := toFloat64WithOK(key)\n if op == "*" {\n acc *= n\n } else {\n acc += n\n }\n }\n }\n return acc\n}\n\n// =============================================================================\n// HTML/Template Helpers\n// =============================================================================\n\n// Comment returns an HTML comment string for hydration markers.\n// The "bf-" prefix is automatically added.\nfunc Comment(content string) template.HTML {\n return template.HTML("<!--bf-" + content + "-->")\n}\n\n// TextStart returns an HTML comment start marker for reactive text expressions.\n// Format: <!--bf:slotId-->\nfunc TextStart(slotId string) template.HTML {\n return template.HTML("<!--bf:" + slotId + "-->")\n}\n\n// TextEnd returns an HTML comment end marker for reactive text expressions.\n// Format: <!--/-->\nfunc TextEnd() template.HTML {\n return "<!--/-->"\n}\n\n// ScopeComment emits a fragment-rooted scope marker. See spec/compiler.md\n// "Slot identity" for the wire format. Loud-fails on marshal errors\n// (same policy as JSON / BfPropsAttr).\nfunc ScopeComment(props interface{}) (template.HTML, error) {\n scopeID := getStringField(props, "ScopeID")\n hostSegment := ""\n if host := getStringField(props, "BfParent"); host != "" {\n mount := getStringField(props, "BfMount")\n hostSegment = "|h=" + host + "|m=" + mount\n }\n propsJSON := ""\n if getBoolField(props, "BfIsRoot") {\n pJSON, err := json.Marshal(props)\n if err != nil {\n return "", err\n }\n propsJSON = "|" + string(pJSON)\n }\n return template.HTML("<!--bf-scope:" + scopeID + hostSegment + propsJSON + "-->"), nil\n}\n\n// PortalHTML parses and executes a template string with the provided data.\n// Used for rendering dynamic portal content where the template string\n// contains Go template expressions (e.g., {{if .Open}}open{{end}}).\n//\n// The template string is parsed fresh each time to support dynamic content.\n// Standard Go template functions (if, range, eq, etc.) are available.\nfunc PortalHTML(data interface{}, tmplStr string) template.HTML {\n // Create a new template with the FuncMap for custom functions\n t, err := template.New("portal").Funcs(FuncMap()).Parse(tmplStr)\n if err != nil {\n // Return error message as HTML comment for debugging\n return template.HTML("<!-- bfPortalHTML error: " + err.Error() + " -->")\n }\n\n var buf bytes.Buffer\n if err := t.Execute(&buf, data); err != nil {\n return template.HTML("<!-- bfPortalHTML exec error: " + err.Error() + " -->")\n }\n\n return template.HTML(buf.String())\n}\n\n// =============================================================================\n// Portal Collection\n// =============================================================================\n\n// PortalContent represents a single portal\'s content to be rendered at body end.\ntype PortalContent struct {\n ID string // Unique portal ID for hydration matching\n OwnerID string // Owner scope ID for find() support\n Content template.HTML // Portal HTML content\n}\n\n// PortalCollector collects portal content during template rendering.\n// Portal content is rendered at </body> to avoid z-index issues.\ntype PortalCollector struct {\n portals []PortalContent\n counter int\n}\n\n// NewPortalCollector creates a new PortalCollector.\nfunc NewPortalCollector() *PortalCollector {\n return &PortalCollector{\n portals: []PortalContent{},\n counter: 0,\n }\n}\n\n// Add registers portal content to be rendered at body end.\nfunc (pc *PortalCollector) Add(ownerID string, content template.HTML) string {\n pc.counter++\n id := "bf-portal-" + strconv.Itoa(pc.counter)\n pc.portals = append(pc.portals, PortalContent{\n ID: id,\n OwnerID: ownerID,\n Content: content,\n })\n return "" // Return empty string for template use\n}\n\n// Render outputs all collected portals as HTML.\n// Each portal is wrapped in a div with bf-pi (portal ID) and bf-po (portal owner).\nfunc (pc *PortalCollector) Render() template.HTML {\n if pc == nil || len(pc.portals) == 0 {\n return ""\n }\n var buf strings.Builder\n for _, p := range pc.portals {\n buf.WriteString(`<div bf-pi="`)\n buf.WriteString(p.ID)\n buf.WriteString(`" bf-po="`)\n buf.WriteString(p.OwnerID)\n buf.WriteString(`">`)\n buf.WriteString(string(p.Content))\n buf.WriteString("</div>\\n")\n }\n return template.HTML(buf.String())\n}\n\n// =============================================================================\n// Script Collection\n// =============================================================================\n\n// ScriptCollector collects client scripts with deduplication.\n// It preserves insertion order for deterministic output.\ntype ScriptCollector struct {\n scripts map[string]bool\n order []string\n}\n\n// NewScriptCollector creates a new ScriptCollector.\nfunc NewScriptCollector() *ScriptCollector {\n return &ScriptCollector{\n scripts: make(map[string]bool),\n order: []string{},\n }\n}\n\n// Register adds a script source to the collection.\n// Duplicate scripts are ignored (only first registration counts).\nfunc (sc *ScriptCollector) Register(src string) string {\n if sc.scripts[src] {\n return "" // Already registered\n }\n sc.scripts[src] = true\n sc.order = append(sc.order, src)\n return "" // Return empty string for template use\n}\n\n// Scripts returns all registered scripts in insertion order.\nfunc (sc *ScriptCollector) Scripts() []string {\n return sc.order\n}\n\n// BfScripts generates script tags for all registered scripts.\n// Returns HTML safe for embedding in templates.\nfunc BfScripts(collector *ScriptCollector) template.HTML {\n if collector == nil {\n return ""\n }\n var result strings.Builder\n for _, src := range collector.Scripts() {\n result.WriteString(`<script type="module" src="`)\n result.WriteString(src)\n result.WriteString(`"></script>`)\n result.WriteString("\\n")\n }\n return template.HTML(result.String())\n}\n\n// =============================================================================\n// Component Renderer\n// =============================================================================\n\n// RenderContext contains all data needed to render a component page.\n// The layout function receives this context to build the final HTML.\ntype RenderContext struct {\n // ComponentName is the template name being rendered\n ComponentName string\n\n // Props is the component props (for layout to access if needed)\n Props interface{}\n\n // ComponentHTML is the rendered component template output\n ComponentHTML template.HTML\n\n // Portals contains collected portal content to render at body end\n Portals template.HTML\n\n // Scripts contains the collected JS script tags\n Scripts template.HTML\n\n // Title is the page title (defaults to "{ComponentName} - BarefootJS")\n Title string\n\n // Heading is the page heading. Empty string means no heading.\n Heading string\n\n // Extra holds additional user-defined data for the layout\n Extra map[string]interface{}\n}\n\n// LayoutFunc renders the final HTML page given the render context.\ntype LayoutFunc func(ctx *RenderContext) string\n\n// Renderer renders BarefootJS components with a customizable layout.\ntype Renderer struct {\n templates *template.Template\n layout LayoutFunc\n}\n\n// NewRenderer creates a Renderer with the given templates and layout function.\n//\n// Example usage:\n//\n// renderer := bf.NewRenderer(templates, func(ctx *bf.RenderContext) string {\n// return fmt.Sprintf(`<!DOCTYPE html>\n// <html>\n// <head><title>%s</title></head>\n// <body>%s%s</body>\n// </html>`, ctx.Title, ctx.ComponentHTML, ctx.Scripts)\n// })\nfunc NewRenderer(tmpl *template.Template, layout LayoutFunc) *Renderer {\n return &Renderer{\n templates: tmpl,\n layout: layout,\n }\n}\n\n// RenderOptions configures a single render call.\ntype RenderOptions struct {\n // ComponentName is the template name to render (required)\n ComponentName string\n\n // Props is the component props (must be a pointer to struct with Scripts field)\n Props interface{}\n\n // Title is the page title. If empty, defaults to "{ComponentName} - BarefootJS"\n Title string\n\n // Heading is the page heading. If empty, no heading is shown.\n Heading string\n\n // Extra holds additional data to pass to the layout\n Extra map[string]interface{}\n}\n\n// Render renders a component to a full HTML page using the configured layout.\n// Child component props are automatically detected (any slice field with ScopeID/Scripts).\n// renderTemplateErrorPanel formats a Go template execution error into a\n// fragment of HTML that\'s visible in the browser. The panel is\n// HTML-escaped so a faulty template name (anything from `template:\n// "..."`) can\'t smuggle markup back into the page. Keep the styling\n// inline so the panel surfaces even when the project\'s CSS hasn\'t\n// loaded yet (e.g. the failure aborted before the stylesheet links\n// emitted).\n//\n// Surfaced for the #1442 echo repro: a template referencing\n// `.Todo.Done` (instead of the range dot\'s `.Done`) used to fail\n// silently \u2014 Go\'s html/template aborted mid-stream, the partial body\n// flushed as a 200, and the user saw a truncated list with no console\n// signal. With this panel they get the template name, the error\n// message, and a "what to look at" hint inline.\nfunc renderTemplateErrorPanel(componentName string, err error) string {\n return `<div style="margin:1em 0;padding:1em;border:2px solid #d33;background:#fff5f5;color:#900;font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;font-size:13px;line-height:1.5"><strong style="display:block;margin-bottom:.5em">Template error in <code>` +\n template.HTMLEscapeString(componentName) +\n `</code></strong><pre style="margin:0;white-space:pre-wrap;word-break:break-word">` +\n template.HTMLEscapeString(err.Error()) +\n `</pre><div style="margin-top:.75em;font-size:12px;opacity:.7">Common cause: a JSX expression referenced a name the adapter could not resolve to a struct field. Open the matching <code>dist/templates/*.tmpl</code> for the unresolved reference, then fix the source component.</div></div>`\n}\n\nfunc (r *Renderer) Render(opts RenderOptions) string {\n // Create script collector and inject into props\n scriptCollector := NewScriptCollector()\n setScriptsField(opts.Props, scriptCollector)\n\n // Create portal collector and inject into props\n portalCollector := NewPortalCollector()\n setPortalsField(opts.Props, portalCollector)\n\n // Auto-detect and process child component props (slices)\n childSlices := findChildComponentSlices(opts.Props)\n for _, slice := range childSlices {\n setScopeIDsOnSlice(slice)\n setScriptsOnSlice(slice, scriptCollector)\n setPortalsOnSlice(slice, portalCollector)\n setBoolOnSlice(slice, "BfIsChild", true)\n }\n\n // Auto-detect and process single child component props\n singleChildren := findSingleChildComponents(opts.Props)\n for _, child := range singleChildren {\n setScopeIDOnSingle(child)\n setScriptsOnSingle(child, scriptCollector)\n setPortalsOnSingle(child, portalCollector)\n setBoolField(child, "BfIsChild", true)\n }\n\n // Mark the root component so BfPropsAttr emits bf-p only for it\n setBoolField(opts.Props, "BfIsRoot", true)\n\n // Render the component template.\n //\n // Errors here are NOT silently dropped. The original implementation\n // ignored the return value of `ExecuteTemplate`, which masked a real\n // onboarding failure mode: a template referencing a non-existent\n // field (`.Todo.Done` instead of the range dot\'s `.Done`) caused\n // html/template to abort mid-stream, the partial output got\n // returned, and the HTTP server happily flushed a 200 with a\n // truncated body. No error log, no signal \u2014 the user just saw a\n // blank list (#1442 echo TodoApp repro).\n //\n // Now we capture the error and replace the partial output with a\n // visible inline panel (dev mode) or a fenced error comment\n // (production), so the cause is on-screen and grep-able in logs.\n // Either way the renderer also writes to stderr so structured log\n // aggregators see it.\n var componentBuf strings.Builder\n if err := r.templates.ExecuteTemplate(&componentBuf, opts.ComponentName, opts.Props); err != nil {\n fmt.Fprintf(os.Stderr, "barefoot: template %q failed to render: %v\\n", opts.ComponentName, err)\n // Preserve whatever the template did manage to emit before\n // failing (Go\'s text/template flushes incrementally), but\n // follow it with a clearly-marked error block so the user\n // notices something is wrong instead of seeing a silent\n // truncation.\n componentBuf.WriteString(renderTemplateErrorPanel(opts.ComponentName, err))\n }\n\n // Determine title (default: "{ComponentName} - BarefootJS")\n title := opts.Title\n if title == "" {\n title = opts.ComponentName + " - BarefootJS"\n }\n\n // Heading (empty means no heading)\n heading := opts.Heading\n\n // Build render context\n ctx := &RenderContext{\n ComponentName: opts.ComponentName,\n Props: opts.Props,\n ComponentHTML: template.HTML(componentBuf.String()),\n Portals: portalCollector.Render(),\n Scripts: BfScripts(scriptCollector),\n Title: title,\n Heading: heading,\n Extra: opts.Extra,\n }\n\n return r.layout(ctx)\n}\n\n// setScriptsField sets the Scripts field on a struct using reflection.\nfunc setScriptsField(v interface{}, collector *ScriptCollector) {\n val := reflect.ValueOf(v)\n if val.Kind() == reflect.Ptr {\n val = val.Elem()\n }\n if val.Kind() != reflect.Struct {\n return\n }\n field := val.FieldByName("Scripts")\n if field.IsValid() && field.CanSet() {\n field.Set(reflect.ValueOf(collector))\n }\n}\n\n// setPortalsField sets the Portals field on a struct using reflection.\nfunc setPortalsField(v interface{}, collector *PortalCollector) {\n val := reflect.ValueOf(v)\n if val.Kind() == reflect.Ptr {\n val = val.Elem()\n }\n if val.Kind() != reflect.Struct {\n return\n }\n field := val.FieldByName("Portals")\n if field.IsValid() && field.CanSet() {\n field.Set(reflect.ValueOf(collector))\n }\n}\n\n// getStringField extracts a string field from a struct using reflection.\nfunc setBoolField(v interface{}, fieldName string, val bool) {\n rv := reflect.ValueOf(v)\n if rv.Kind() == reflect.Ptr {\n rv = rv.Elem()\n }\n if rv.Kind() != reflect.Struct {\n return\n }\n field := rv.FieldByName(fieldName)\n if field.IsValid() && field.CanSet() && field.Kind() == reflect.Bool {\n field.SetBool(val)\n }\n}\n\nfunc getBoolField(v interface{}, fieldName string) bool {\n val := reflect.ValueOf(v)\n if val.Kind() == reflect.Ptr {\n val = val.Elem()\n }\n if val.Kind() != reflect.Struct {\n return false\n }\n field := val.FieldByName(fieldName)\n if !field.IsValid() || field.Kind() != reflect.Bool {\n return false\n }\n return field.Bool()\n}\n\nfunc getStringField(v interface{}, fieldName string) string {\n val := reflect.ValueOf(v)\n if val.Kind() == reflect.Ptr {\n val = val.Elem()\n }\n if val.Kind() != reflect.Struct {\n return ""\n }\n field := val.FieldByName(fieldName)\n if !field.IsValid() || field.Kind() != reflect.String {\n return ""\n }\n return field.String()\n}\n\n// scopeIDChars is the alphabet for auto-generated ScopeID suffixes. It\n// mirrors the `randomID` helper the go-template adapter emits into the\n// generated New<Component>Props constructors so runtime-assigned and\n// constructor-assigned ids are indistinguishable.\nconst scopeIDChars = "abcdefghijklmnopqrstuvwxyz0123456789"\n\n// randomScopeSuffix returns a random lowercase-alphanumeric string of\n// length n. math/rand (auto-seeded since Go 1.20) is sufficient here: the\n// suffix only needs to be unique enough to keep a page\'s bf-s scope ids\n// from colliding, not cryptographically unpredictable.\nfunc randomScopeSuffix(n int) string {\n b := make([]byte, n)\n for i := range b {\n b[i] = scopeIDChars[rand.Intn(len(scopeIDChars))]\n }\n return string(b)\n}\n\n// scopeIDPrefix derives the human-readable ScopeID prefix from a child\n// component\'s type, e.g. `TodoItemProps` \u2192 `TodoItem`. Matches the\n// `"<Component>_" + randomID(6)` shape the generated constructors use.\nfunc scopeIDPrefix(t reflect.Type) string {\n for t.Kind() == reflect.Ptr {\n t = t.Elem()\n }\n return strings.TrimSuffix(t.Name(), "Props")\n}\n\n// assignScopeID fills a child component\'s ScopeID with a generated id when\n// the caller left it empty, so application code doesn\'t have to mint scope\n// ids by hand (the parent\'s New<Component>Props constructor does the same\n// for components built through it). A non-empty ScopeID is left untouched,\n// so callers can still pin a stable id when they need one.\nfunc assignScopeID(structVal reflect.Value, prefix string) {\n field := structVal.FieldByName("ScopeID")\n if !field.IsValid() || !field.CanSet() || field.Kind() != reflect.String {\n return\n }\n if field.String() != "" {\n return\n }\n id := randomScopeSuffix(6)\n if prefix != "" {\n id = prefix + "_" + id\n }\n field.SetString(id)\n}\n\n// setScopeIDsOnSlice assigns a generated ScopeID to every child in a slice\n// whose ScopeID is empty.\nfunc setScopeIDsOnSlice(slice interface{}) {\n v := reflect.ValueOf(slice)\n if v.Kind() != reflect.Slice {\n return\n }\n prefix := scopeIDPrefix(v.Type().Elem())\n for i := 0; i < v.Len(); i++ {\n item := v.Index(i)\n if item.Kind() == reflect.Ptr {\n if item.IsNil() {\n continue\n }\n item = item.Elem()\n }\n if item.Kind() == reflect.Struct {\n assignScopeID(item, prefix)\n }\n }\n}\n\n// setScopeIDOnSingle assigns a generated ScopeID to a single child\n// component when its ScopeID is empty.\nfunc setScopeIDOnSingle(child interface{}) {\n v := reflect.ValueOf(child)\n if v.Kind() == reflect.Ptr {\n v = v.Elem()\n }\n if v.Kind() != reflect.Struct {\n return\n }\n assignScopeID(v, scopeIDPrefix(v.Type()))\n}\n\n// findChildComponentSlices finds slice fields containing child component props.\n// Child props are identified by having ScopeID and Scripts fields.\nfunc findChildComponentSlices(props interface{}) []interface{} {\n var result []interface{}\n\n val := reflect.ValueOf(props)\n if val.Kind() == reflect.Ptr {\n val = val.Elem()\n }\n if val.Kind() != reflect.Struct {\n return result\n }\n\n for i := 0; i < val.NumField(); i++ {\n field := val.Field(i)\n if field.Kind() != reflect.Slice || field.Len() == 0 {\n continue\n }\n\n elem := field.Index(0)\n if elem.Kind() == reflect.Ptr {\n elem = elem.Elem()\n }\n if elem.Kind() != reflect.Struct {\n continue\n }\n\n hasScopeID := elem.FieldByName("ScopeID").IsValid()\n hasScripts := elem.FieldByName("Scripts").IsValid()\n\n if hasScopeID && hasScripts {\n result = append(result, field.Interface())\n }\n }\n\n return result\n}\n\n// setScriptsOnSlice sets Scripts on all items in a slice.\nfunc setScriptsOnSlice(slice interface{}, collector *ScriptCollector) {\n val := reflect.ValueOf(slice)\n if val.Kind() != reflect.Slice {\n return\n }\n for i := 0; i < val.Len(); i++ {\n item := val.Index(i)\n if item.Kind() == reflect.Ptr {\n item = item.Elem()\n }\n if item.Kind() == reflect.Struct {\n field := item.FieldByName("Scripts")\n if field.IsValid() && field.CanSet() {\n field.Set(reflect.ValueOf(collector))\n }\n }\n }\n}\n\n// setBoolOnSlice sets a bool field on all items in a slice.\nfunc setBoolOnSlice(slice interface{}, fieldName string, val bool) {\n v := reflect.ValueOf(slice)\n if v.Kind() != reflect.Slice {\n return\n }\n for i := 0; i < v.Len(); i++ {\n item := v.Index(i)\n if item.Kind() == reflect.Ptr {\n item = item.Elem()\n }\n if item.Kind() == reflect.Struct {\n field := item.FieldByName(fieldName)\n if field.IsValid() && field.CanSet() && field.Kind() == reflect.Bool {\n field.SetBool(val)\n }\n }\n }\n}\n\n// setPortalsOnSlice sets Portals on all items in a slice.\nfunc setPortalsOnSlice(slice interface{}, collector *PortalCollector) {\n val := reflect.ValueOf(slice)\n if val.Kind() != reflect.Slice {\n return\n }\n for i := 0; i < val.Len(); i++ {\n item := val.Index(i)\n if item.Kind() == reflect.Ptr {\n item = item.Elem()\n }\n if item.Kind() == reflect.Struct {\n field := item.FieldByName("Portals")\n if field.IsValid() && field.CanSet() {\n field.Set(reflect.ValueOf(collector))\n }\n }\n }\n}\n\n\n// findSingleChildComponents finds single struct fields containing child component props.\n// Child props are identified by having ScopeID and Scripts fields.\nfunc findSingleChildComponents(props interface{}) []interface{} {\n var result []interface{}\n\n val := reflect.ValueOf(props)\n if val.Kind() == reflect.Ptr {\n val = val.Elem()\n }\n if val.Kind() != reflect.Struct {\n return result\n }\n\n for i := 0; i < val.NumField(); i++ {\n field := val.Field(i)\n\n // Handle pointer to struct\n if field.Kind() == reflect.Ptr {\n if field.IsNil() {\n continue\n }\n field = field.Elem()\n }\n\n // Skip non-struct fields (slices handled by findChildComponentSlices)\n if field.Kind() != reflect.Struct {\n continue\n }\n\n hasScopeID := field.FieldByName("ScopeID").IsValid()\n hasScripts := field.FieldByName("Scripts").IsValid()\n\n if hasScopeID && hasScripts {\n result = append(result, field.Addr().Interface())\n }\n }\n\n return result\n}\n\n// setScriptsOnSingle sets Scripts on a single struct child component.\nfunc setScriptsOnSingle(child interface{}, collector *ScriptCollector) {\n val := reflect.ValueOf(child)\n if val.Kind() == reflect.Ptr {\n val = val.Elem()\n }\n if val.Kind() == reflect.Struct {\n field := val.FieldByName("Scripts")\n if field.IsValid() && field.CanSet() {\n field.Set(reflect.ValueOf(collector))\n }\n }\n}\n\n// setPortalsOnSingle sets Portals on a single struct child component.\nfunc setPortalsOnSingle(child interface{}, collector *PortalCollector) {\n val := reflect.ValueOf(child)\n if val.Kind() == reflect.Ptr {\n val = val.Elem()\n }\n if val.Kind() == reflect.Struct {\n field := val.FieldByName("Portals")\n if field.IsValid() && field.CanSet() {\n field.Set(reflect.ValueOf(collector))\n }\n }\n}\n\n\n// =============================================================================\n// Internal Helpers\n// =============================================================================\n\nfunc toFloat64(v any) float64 {\n switch n := v.(type) {\n case int:\n return float64(n)\n case int8:\n return float64(n)\n case int16:\n return float64(n)\n case int32:\n return float64(n)\n case int64:\n return float64(n)\n case uint:\n return float64(n)\n case uint8:\n return float64(n)\n case uint16:\n return float64(n)\n case uint32:\n return float64(n)\n case uint64:\n return float64(n)\n case float32:\n return float64(n)\n case float64:\n return n\n default:\n return 0\n }\n}\n\nfunc toInt(v any) int {\n switch n := v.(type) {\n case int:\n return n\n case int8:\n return int(n)\n case int16:\n return int(n)\n case int32:\n return int(n)\n case int64:\n return int(n)\n case uint:\n return int(n)\n case uint8:\n return int(n)\n case uint16:\n return int(n)\n case uint32:\n return int(n)\n case uint64:\n return int(n)\n case float32:\n return int(n)\n case float64:\n return int(n)\n default:\n return 0\n }\n}\n\nfunc isIntLike(v any) bool {\n switch v.(type) {\n case int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64:\n return true\n default:\n return false\n }\n}\n\nfunc toString(v any) string {\n switch s := v.(type) {\n case string:\n return s\n case int:\n return strconv.Itoa(s)\n case int64:\n return strconv.FormatInt(s, 10)\n case float64:\n return strconv.FormatFloat(s, \'f\', -1, 64)\n case bool:\n return strconv.FormatBool(s)\n default:\n return ""\n }\n}\n';
23341
+ bfGoSource = '// Package bf provides runtime helper functions for BarefootJS Go templates.\n// These functions mirror JavaScript behavior for consistent SSR output.\npackage bf\n\nimport (\n "bytes"\n "encoding/json"\n "fmt"\n "html/template"\n "math"\n "math/rand"\n "os"\n "reflect"\n "sort"\n "strconv"\n "strings"\n "unicode/utf8"\n)\n\n// FuncMap returns a template.FuncMap with all BarefootJS helper functions.\n// Usage:\n//\n// tmpl := template.New("").Funcs(bf.FuncMap())\nfunc FuncMap() template.FuncMap {\n return template.FuncMap{\n // Arithmetic\n "bf_add": Add,\n "bf_sub": Sub,\n "bf_mul": Mul,\n "bf_div": Div,\n "bf_mod": Mod,\n "bf_neg": Neg,\n\n // String\n "bf_lower": Lower,\n "bf_upper": Upper,\n "bf_trim": Trim,\n "bf_contains": Contains,\n "bf_join": Join,\n "bf_split": Split,\n "bf_starts_with": StartsWith,\n "bf_ends_with": EndsWith,\n "bf_replace": Replace,\n "bf_repeat": Repeat,\n "bf_pad_start": PadStart,\n "bf_pad_end": PadEnd,\n "bf_string": String,\n\n // JSON / numeric primitives \u2014 JS-compat callees registered on\n // the Go adapter\'s `templatePrimitives` map (#1188).\n "bf_json": JSON,\n "bf_number": Number,\n "bf_floor": Floor,\n "bf_ceil": Ceil,\n "bf_round": Round,\n\n // Array/Slice\n "bf_len": Len,\n "bf_at": At,\n "bf_includes": Includes,\n "bf_index_of": IndexOf,\n "bf_last_index_of": LastIndexOf,\n "bf_concat": Concat,\n "bf_slice": Slice,\n "bf_reverse": Reverse,\n "bf_flat": Flat,\n "bf_flat_map": FlatMap,\n "bf_flat_map_tuple": FlatMapTuple,\n "bf_first": First,\n "bf_last": Last,\n "bf_arr": Arr,\n "bf_filter_truthy": FilterTruthy,\n\n // Higher-order Array Methods\n "bf_every": Every,\n "bf_some": Some,\n "bf_filter": Filter,\n "bf_find": Find,\n "bf_find_index": FindIndex,\n "bf_find_last": FindLast,\n "bf_find_last_index": FindLastIndex,\n "bf_sort": Sort,\n "bf_reduce": Reduce,\n\n // Comment marker (for hydration)\n "bfComment": Comment,\n "bfTextStart": TextStart,\n "bfTextEnd": TextEnd,\n\n // Script collection\n "bfScripts": BfScripts,\n\n // Scope attribute value (#1249: bare scope id, no `~` prefix)\n "bfScopeAttr": ScopeAttr,\n\n // Slot-identity markers (#1249): bf-h, bf-m, bf-r\n "bfHydrationAttrs": HydrationAttrs,\n\n // Child component marker (kept for backward compatibility)\n "bfIsChild": IsChild,\n\n // Props attribute for hydration\n "bfPropsAttr": BfPropsAttr,\n\n // Portal HTML rendering (parses and executes template string)\n "bfPortalHTML": PortalHTML,\n\n // Scope comment for fragment roots\n "bfScopeComment": ScopeComment,\n\n // JSX intrinsic-element spread lowering (#1407)\n "bf_spread_attrs": SpreadAttrs,\n }\n}\n\n// ScopeAttr returns the bare bf-s scope id (#1249).\nfunc ScopeAttr(props interface{}) string {\n return getStringField(props, "ScopeID")\n}\n\n// HydrationAttrs emits `bf-h="<host>" bf-m="<slot>" bf-r=""` conditionally.\n// See spec/compiler.md "Slot identity".\nfunc HydrationAttrs(props interface{}) template.HTMLAttr {\n parts := []string{}\n if host := getStringField(props, "BfParent"); host != "" {\n parts = append(parts, fmt.Sprintf(`bf-h="%s"`, template.HTMLEscapeString(host)))\n }\n if mount := getStringField(props, "BfMount"); mount != "" {\n parts = append(parts, fmt.Sprintf(`bf-m="%s"`, template.HTMLEscapeString(mount)))\n }\n if !getBoolField(props, "BfIsChild") {\n parts = append(parts, `bf-r=""`)\n }\n if len(parts) == 0 {\n return ""\n }\n return template.HTMLAttr(strings.Join(parts, " "))\n}\n\n// IsChild is a deprecated no-op stub. Child status is signalled by bf-h\n// presence (#1249); use HydrationAttrs instead.\nfunc IsChild(props interface{}) template.HTMLAttr {\n return ""\n}\n\n// svgCamelCaseAttrs mirrors SVG_CAMEL_CASE_ATTRS from\n// packages/client/src/runtime/spread-attrs.ts. SVG XML attribute\n// names are case-sensitive; the default camelCase \u2192 kebab-case\n// rewrite must NOT apply to these or the SVG stops rendering\n// (#1407). Coordinates with the compile-time SVG_CAMEL_TO_KEBAB\n// table in packages/jsx/src/ir-to-client-js/utils.ts: presentation\n// attrs (clipPath, strokeWidth, \u2026) live there and must NOT appear\n// here, or the same JSX prop would lower to clip-path via the\n// explicit-attr path and stay clipPath via the spread path.\nvar svgCamelCaseAttrs = map[string]struct{}{\n "allowReorder": {}, "attributeName": {}, "attributeType": {}, "autoReverse": {},\n "baseFrequency": {}, "baseProfile": {}, "calcMode": {}, "clipPathUnits": {},\n "contentScriptType": {}, "contentStyleType": {}, "diffuseConstant": {}, "edgeMode": {},\n "externalResourcesRequired": {}, "filterRes": {}, "filterUnits": {}, "glyphRef": {},\n "gradientTransform": {}, "gradientUnits": {}, "kernelMatrix": {}, "kernelUnitLength": {},\n "keyPoints": {}, "keySplines": {}, "keyTimes": {}, "lengthAdjust": {}, "limitingConeAngle": {},\n "markerHeight": {}, "markerUnits": {}, "markerWidth": {}, "maskContentUnits": {},\n "maskUnits": {}, "numOctaves": {}, "pathLength": {}, "patternContentUnits": {},\n "patternTransform": {}, "patternUnits": {}, "pointsAtX": {}, "pointsAtY": {}, "pointsAtZ": {},\n "preserveAlpha": {}, "preserveAspectRatio": {}, "primitiveUnits": {}, "refX": {}, "refY": {},\n "repeatCount": {}, "repeatDur": {}, "requiredExtensions": {}, "requiredFeatures": {},\n "specularConstant": {}, "specularExponent": {}, "spreadMethod": {}, "startOffset": {},\n "stdDeviation": {}, "stitchTiles": {}, "surfaceScale": {}, "systemLanguage": {},\n "tableValues": {}, "targetX": {}, "targetY": {}, "textLength": {}, "viewBox": {}, "viewTarget": {},\n "xChannelSelector": {}, "yChannelSelector": {}, "zoomAndPan": {},\n}\n\n// toAttrName mirrors the JSX\u2192HTML attribute-name rewrite from\n// packages/client/src/runtime/spread-attrs.ts. className \u2192 class,\n// htmlFor \u2192 for, SVG camelCase attrs preserved, other camelCase\n// keys lowered to kebab-case.\nfunc toAttrName(key string) string {\n if key == "className" {\n return "class"\n }\n if key == "htmlFor" {\n return "for"\n }\n if _, ok := svgCamelCaseAttrs[key]; ok {\n return key\n }\n // camelCase \u2192 kebab-case: mirror the JS reference exactly\n // (`key.replace(/([A-Z])/g, \'-$1\').toLowerCase()`). The JS shape\n // produces a leading `-` for an initial uppercase letter\n // (`XData` \u2192 `-x-data`); both this Go path and the matching JS\n // runtime are wrong-by-construction for that case (the resulting\n // HTML attribute name is invalid), but keeping them byte-equal\n // avoids silent SSR/CSR divergence (#1411 review).\n var b strings.Builder\n for _, r := range key {\n if r >= \'A\' && r <= \'Z\' {\n b.WriteByte(\'-\')\n b.WriteRune(r + 32)\n } else {\n b.WriteRune(r)\n }\n }\n return b.String()\n}\n\n// StyleToCss mirrors styleToCss from\n// packages/client/src/runtime/style.ts. Accepts a string passthrough,\n// or a map (JSON-deserialized object) whose camelCase keys are\n// lowered to kebab-case and joined with `;`. Returns ("", false) for\n// nullish/empty input so callers can omit the attribute entirely.\nfunc StyleToCss(v any) (string, bool) {\n if v == nil {\n return "", false\n }\n rv := reflect.ValueOf(v)\n for rv.Kind() == reflect.Interface || rv.Kind() == reflect.Pointer {\n if rv.IsNil() {\n return "", false\n }\n rv = rv.Elem()\n }\n if rv.Kind() != reflect.Map {\n // Non-object: stringify and return as-is, matching the JS\n // `typeof value !== \'object\'` branch.\n s := fmt.Sprint(v)\n if s == "" {\n return "", false\n }\n return s, true\n }\n keys := rv.MapKeys()\n sorted := make([]string, 0, len(keys))\n for _, k := range keys {\n if k.Kind() == reflect.String {\n sorted = append(sorted, k.String())\n }\n }\n sort.Strings(sorted)\n parts := make([]string, 0, len(sorted))\n for _, k := range sorted {\n val := rv.MapIndex(reflect.ValueOf(k))\n // Skip nil entries (matches the JS `if (v == null) continue`).\n if !val.IsValid() {\n continue\n }\n if val.Kind() == reflect.Interface || val.Kind() == reflect.Pointer {\n if val.IsNil() {\n continue\n }\n val = val.Elem()\n }\n prop := toAttrName(k)\n parts = append(parts, fmt.Sprintf("%s:%v", prop, val.Interface()))\n }\n if len(parts) == 0 {\n return "", false\n }\n return strings.Join(parts, ";"), true\n}\n\n// SpreadAttrs lowers a JSX intrinsic-element spread bag (#1407) to\n// an HTML attribute string. Mirrors spreadAttrs from\n// packages/client/src/runtime/spread-attrs.ts so SSR output matches\n// what CSR\'s `applyRestAttrs` writes at hydration.\n//\n// Skip rules: nil/false values, event handlers (`on[A-Z]*`),\n// `children`, `ref`.\n//\n// Key remap: className \u2192 class, htmlFor \u2192 for, SVG camelCase\n// preserved, other camelCase \u2192 kebab-case.\n//\n// `style` is routed through StyleToCss so object literals serialize\n// to a real CSS string instead of Go\'s default `map[k:v]` form.\n//\n// Booleans: true \u2192 bare attribute name, false \u2192 omitted.\n// Other scalar values are HTML-escaped via template.HTMLEscapeString.\n// Returns a `template.HTMLAttr` so html/template emits the result\n// verbatim (the function does its own escaping).\n//\n// Keys are sorted alphabetically before emission for deterministic\n// output. SSR/CSR attribute-order divergence is acceptable per the\n// rest-destructure-object-spread-in-map fixture\'s documented policy\n// \u2014 browsers honor the LAST value when a key is duplicated, so\n// pairing with static attrs (`<div class="x" {...rest}>`) is\n// last-wins regardless of order.\nfunc SpreadAttrs(bag any) template.HTMLAttr {\n if bag == nil {\n return ""\n }\n rv := reflect.ValueOf(bag)\n for rv.Kind() == reflect.Interface || rv.Kind() == reflect.Pointer {\n if rv.IsNil() {\n return ""\n }\n rv = rv.Elem()\n }\n if rv.Kind() != reflect.Map {\n return ""\n }\n keys := rv.MapKeys()\n sortedKeys := make([]string, 0, len(keys))\n for _, k := range keys {\n if k.Kind() == reflect.String {\n sortedKeys = append(sortedKeys, k.String())\n }\n }\n sort.Strings(sortedKeys)\n parts := make([]string, 0, len(sortedKeys))\n for _, key := range sortedKeys {\n // Event handlers \u2014 skip at SSR the same way\n // packages/client/src/runtime/spread-attrs.ts does at\n // hydration. The JS predicate is\n // `key.startsWith(\'on\') && key.length > 2 && key[2] === key[2].toUpperCase()`,\n // which is true for any character whose uppercase form is\n // itself: ASCII A-Z, digits, underscore, and non-letter\n // symbols. Mirror that here by skipping when key[2] is NOT\n // a lowercase ASCII letter \u2014 so `onClick`, `on_custom`, and\n // `on0` all match (#1411 review).\n if len(key) > 2 && key[0] == \'o\' && key[1] == \'n\' && !(key[2] >= \'a\' && key[2] <= \'z\') {\n continue\n }\n // `children` is a JSX construct rendered inside the element,\n // never a DOM attribute. `ref` is intentionally NOT filtered\n // here so output stays byte-equal with the JS reference\n // `spreadAttrs` in packages/client/src/runtime/spread-attrs.ts\n // (which only filters null/false, event handlers, and\n // children) \u2014 aligning Go\'s filter set diverges from JS in\n // the opposite direction. Filtering `ref` consistently across\n // both SSR runtimes is a separate concern tracked alongside\n // the JS `applyRestAttrs` vs `spreadAttrs` mismatch (#1411\n // review).\n if key == "children" {\n continue\n }\n val := rv.MapIndex(reflect.ValueOf(key))\n if !val.IsValid() {\n continue\n }\n // Unwrap interface wrappers (json.Unmarshal produces\n // interface{}-wrapped values for map[string]any).\n v := val\n for v.Kind() == reflect.Interface || v.Kind() == reflect.Pointer {\n if v.IsNil() {\n // Skip null entries.\n v = reflect.Value{}\n break\n }\n v = v.Elem()\n }\n if !v.IsValid() {\n continue\n }\n // Boolean values: true \u2192 bare attribute, false \u2192 omitted.\n if v.Kind() == reflect.Bool {\n if !v.Bool() {\n continue\n }\n parts = append(parts, toAttrName(key))\n continue\n }\n // `style` routes through StyleToCss so object literals get a\n // real CSS string. The JS side does the same.\n if key == "style" {\n css, ok := StyleToCss(v.Interface())\n if !ok {\n continue\n }\n parts = append(parts, fmt.Sprintf(`style="%s"`, template.HTMLEscapeString(css)))\n continue\n }\n // Stringify and escape. fmt.Sprint handles numbers, bools-as-\n // strings, and arbitrary stringer types the same way the JS\n // `String(value)` coercion does for the analogous cases.\n s := fmt.Sprint(v.Interface())\n parts = append(parts, fmt.Sprintf(`%s="%s"`, toAttrName(key), template.HTMLEscapeString(s)))\n }\n if len(parts) == 0 {\n return ""\n }\n return template.HTMLAttr(strings.Join(parts, " "))\n}\n\n// BfPropsAttr returns the bf-p attribute with the JSON-serialized\n// props in flat format. Output format: `bf-p=\'{"propName":value,...}\'`.\n// Only emits the attribute for root components (BfIsRoot == true);\n// child components receive props from their parent via initChild().\n//\n// Returns the marshal error so a `template.Execute` call fails\n// loudly on cycles / unsupported props rather than silently\n// dropping the bf-p attribute and breaking client-side hydration.\n// Same loud-failure policy as `JSON` \u2014 user data going through\n// `encoding/json` shouldn\'t fail invisibly.\nfunc BfPropsAttr(props interface{}) (template.HTMLAttr, error) {\n // Only root components should emit bf-p\n if !getBoolField(props, "BfIsRoot") {\n return "", nil\n }\n\n propsJSON, err := json.Marshal(props)\n if err != nil {\n return "", err\n }\n\n escaped := template.HTMLEscapeString(string(propsJSON))\n return template.HTMLAttr(`bf-p="` + escaped + `"`), nil\n}\n\n// =============================================================================\n// Arithmetic Operations\n// =============================================================================\n\n// Add returns a + b. Supports int and float64.\nfunc Add(a, b any) any {\n av, bv := toFloat64(a), toFloat64(b)\n result := av + bv\n // Return int if both inputs were int-like\n if isIntLike(a) && isIntLike(b) && result == float64(int(result)) {\n return int(result)\n }\n return result\n}\n\n// Sub returns a - b. Supports int and float64.\nfunc Sub(a, b any) any {\n av, bv := toFloat64(a), toFloat64(b)\n result := av - bv\n if isIntLike(a) && isIntLike(b) && result == float64(int(result)) {\n return int(result)\n }\n return result\n}\n\n// Mul returns a * b. Supports int and float64.\nfunc Mul(a, b any) any {\n av, bv := toFloat64(a), toFloat64(b)\n result := av * bv\n if isIntLike(a) && isIntLike(b) && result == float64(int(result)) {\n return int(result)\n }\n return result\n}\n\n// Div returns a / b. Returns float64 to match JavaScript behavior.\n// Returns 0 if b is 0 (instead of panicking).\nfunc Div(a, b any) any {\n av, bv := toFloat64(a), toFloat64(b)\n if bv == 0 {\n return 0\n }\n return av / bv\n}\n\n// Mod returns a % b (modulo). Supports int only.\nfunc Mod(a, b any) int {\n av, bv := toInt(a), toInt(b)\n if bv == 0 {\n return 0\n }\n return av % bv\n}\n\n// Neg returns -a (negation).\nfunc Neg(a any) any {\n if v, ok := a.(int); ok {\n return -v\n }\n return -toFloat64(a)\n}\n\n// =============================================================================\n// String Operations\n// =============================================================================\n\n// Lower returns the lowercase version of s.\nfunc Lower(s string) string {\n return strings.ToLower(s)\n}\n\n// Upper returns the uppercase version of s.\nfunc Upper(s string) string {\n return strings.ToUpper(s)\n}\n\n// Trim returns s with leading and trailing whitespace removed.\nfunc Trim(s string) string {\n return strings.TrimSpace(s)\n}\n\n// Contains returns true if s contains substr.\nfunc Contains(s, substr string) bool {\n return strings.Contains(s, substr)\n}\n\n// Split lowers `String.prototype.split(sep, limit?)` (#1448 Tier B). It\n// wraps `strings.Split` and normalises the result to `[]any` so the\n// slice composes with the array-method surface downstream (`bf_join`,\n// range loops, `bf_len`, \u2026) the same way `bf_slice` / `bf_reverse`\n// results do. Like JS, an empty separator splits into individual UTF-8\n// characters and trailing empty fields are preserved (`"a,".split(",")`\n// \u2192 `["a", ""]`). An optional `limit` caps the number of returned\n// pieces (`"a,b,c".split(",", 2)` \u2192 `["a", "b"]`); a negative limit is\n// ignored (JS would also return every piece \u2014 its ToUint32 wrap makes\n// the limit effectively unbounded). The no-separator form is handled by\n// the adapter (it emits `bf_arr` for the whole-string single element).\nfunc Split(s, sep string, limit ...int) []any {\n parts := strings.Split(s, sep)\n if len(limit) > 0 && limit[0] >= 0 && limit[0] < len(parts) {\n parts = parts[:limit[0]]\n }\n out := make([]any, len(parts))\n for i, p := range parts {\n out[i] = p\n }\n return out\n}\n\n// StartsWith lowers `String.prototype.startsWith(prefix, position?)`\n// (#1448 Tier B). Wraps `strings.HasPrefix`; an empty prefix is always\n// true (JS parity). The optional `position` re-anchors the test to start\n// at that index (clamped to `[0, len]` so it never panics), matching JS\n// `"abc".startsWith("b", 1) === true`.\nfunc StartsWith(s, prefix string, position ...int) bool {\n if len(position) > 0 {\n p := position[0]\n if p < 0 {\n p = 0\n }\n if p > len(s) {\n p = len(s)\n }\n s = s[p:]\n }\n return strings.HasPrefix(s, prefix)\n}\n\n// EndsWith lowers `String.prototype.endsWith(suffix, endPosition?)`\n// (#1448 Tier B). Wraps `strings.HasSuffix`; an empty suffix is always\n// true (JS parity). The optional `endPosition` treats the string as if\n// it were only that many bytes long (clamped to `[0, len]`), matching JS\n// `"abc".endsWith("b", 2) === true`.\nfunc EndsWith(s, suffix string, endPosition ...int) bool {\n if len(endPosition) > 0 {\n e := endPosition[0]\n if e < 0 {\n e = 0\n }\n if e > len(s) {\n e = len(s)\n }\n s = s[:e]\n }\n return strings.HasSuffix(s, suffix)\n}\n\n// Replace lowers the string-pattern form of `String.prototype.replace`\n// (#1448 Tier B). JS replaces only the FIRST occurrence for a string\n// pattern, so the count is 1 (`strings.Replace` with n=1; n<0 would\n// replace all \u2014 that\'s `.replaceAll`, still refused). The replacement\n// is treated literally: unlike JS, special replacement patterns like\n// `$&` / `$1` are NOT interpreted (Go and Perl agree on literal\n// replacement, keeping the two template adapters byte-equal; this\n// diverges from the Hono/CSR JS path only for replacement strings that\n// contain `$`-patterns, which are rare in template position).\nfunc Replace(s, old, new string) string {\n return strings.Replace(s, old, new, 1)\n}\n\n// Repeat lowers `String.prototype.repeat(n)` (#1448 Tier B): the\n// receiver concatenated n times. JS throws RangeError for a negative\n// count and `strings.Repeat` panics, so a negative count clamps to the\n// empty string \u2014 SSR templates degrade rather than crash the render.\n// A zero count is the empty string (JS parity).\nfunc Repeat(s string, n int) string {\n if n <= 0 {\n return ""\n }\n return strings.Repeat(s, n)\n}\n\n// padTo lowers the shared body of `String.prototype.padStart` /\n// `padEnd` (#1448 Tier B): pad `s` to `target` code points using `pad`\n// repeated and truncated to fill, prepended (atStart) or appended.\n// Length is measured in runes (not bytes) so the result matches the\n// Perl `bf->pad_*` helpers \u2014 this diverges from JS\'s UTF-16-unit length\n// only for astral-plane input. An empty pad, or a receiver already at\n// least `target` long, returns `s` unchanged (JS parity).\nfunc padTo(s string, target int, pad string, atStart bool) string {\n if pad == "" {\n return s\n }\n sLen := utf8.RuneCountInString(s)\n if sLen >= target {\n return s\n }\n need := target - sLen\n padRunes := []rune(pad)\n fill := make([]rune, 0, need)\n for len(fill) < need {\n for _, r := range padRunes {\n if len(fill) >= need {\n break\n }\n fill = append(fill, r)\n }\n }\n if atStart {\n return string(fill) + s\n }\n return s + string(fill)\n}\n\n// PadStart lowers `String.prototype.padStart(target, pad?)` (#1448 Tier\n// B). The pad string defaults to a single space when omitted.\nfunc PadStart(s string, target int, pad ...string) string {\n p := " "\n if len(pad) > 0 {\n p = pad[0]\n }\n return padTo(s, target, p, true)\n}\n\n// PadEnd lowers `String.prototype.padEnd(target, pad?)` (#1448 Tier B).\nfunc PadEnd(s string, target int, pad ...string) string {\n p := " "\n if len(pad) > 0 {\n p = pad[0]\n }\n return padTo(s, target, p, false)\n}\n\n// Join concatenates elements of a slice with sep. Accepts both\n// reflect.Slice (the common case \u2014 `bf_arr` and `bf_filter_truthy`\n// both return `[]any`) AND reflect.Array (fixed-size Go arrays like\n// `[3]string{...}`), mirroring JS `Array.prototype.join` which\n// doesn\'t distinguish between the two. Pre-fix this returned "" for\n// fixed-size arrays passed through template data (Copilot review on\n// #1445).\nfunc Join(items any, sep string) string {\n v := reflect.ValueOf(items)\n if v.Kind() != reflect.Slice && v.Kind() != reflect.Array {\n return ""\n }\n\n parts := make([]string, v.Len())\n for i := 0; i < v.Len(); i++ {\n parts[i] = toString(v.Index(i).Interface())\n }\n return strings.Join(parts, sep)\n}\n\n// String returns the string form of v. Mirrors JS `String(v)` for\n// non-nil values via `fmt.Sprintf("%v", ...)`. Diverges from JS on\n// nil: JS `String(null)` is "null", but the template path renders\n// `nil` as the empty string here so an unset prop doesn\'t surface\n// as a literal "null"/"undefined" in user-facing HTML. Document the\n// divergence explicitly so callers don\'t rely on JS-exact parity.\nfunc String(v any) string {\n if v == nil {\n return ""\n }\n return fmt.Sprintf("%v", v)\n}\n\n// JSON returns the JSON encoding of v as a string. Mirrors\n// JS `JSON.stringify(v)` for the V1 single-arg shape (no `replacer`\n// or `space`). Object key order is determined by Go\'s `encoding/json`\n// (alphabetical for maps, declaration order for structs) \u2014 the\n// #1187 contract requires value-compat, not order-compat.\n//\n// Top-level NaN / \xB1Inf are pre-handled to match JS \u2014 JS\'s\n// `JSON.stringify(NaN)` and `JSON.stringify(Infinity)` both produce\n// `"null"`, but Go\'s `encoding/json` rejects them with\n// `UnsupportedValueError`. Without this carve-out the common\n// composition `JSON.stringify(Number("garbage"))` would error\n// instead of emitting `"null"` like JS does. Nested NaN/Inf inside\n// a struct/map still surfaces an error \u2014 covering that needs a\n// custom marshaller; out of V1 scope.\n//\n// Returns the marshal error so a `template.Execute` call fails\n// loudly on cycles / unsupported values rather than silently\n// producing `""` and reintroducing the SSR data-loss class\n// #1187 was filed against. Go\'s text/template treats a non-nil\n// error return from a func as an execution failure.\nfunc JSON(v any) (string, error) {\n if f, ok := v.(float64); ok && (math.IsNaN(f) || math.IsInf(f, 0)) {\n return "null", nil\n }\n b, err := json.Marshal(v)\n if err != nil {\n return "", err\n }\n return string(b), nil\n}\n\n// Number coerces v to a float64. Mirrors JS `Number(v)` semantics:\n// numeric / boolean inputs convert as expected; non-numeric strings\n// and other unsupported shapes return `NaN` (matching JS rather\n// than silently substituting 0, which would mis-shape downstream\n// arithmetic and template-side comparisons). Templates that need\n// a deterministic fallback should compose with the user-side\n// default (e.g. `Number(props.x ?? 0)` in JSX).\nfunc Number(v any) float64 {\n if v == nil {\n return math.NaN()\n }\n switch x := v.(type) {\n case float64:\n return x\n case float32:\n return float64(x)\n case int:\n return float64(x)\n case int32:\n return float64(x)\n case int64:\n return float64(x)\n case bool:\n if x {\n return 1\n }\n return 0\n case string:\n f, err := strconv.ParseFloat(x, 64)\n if err != nil {\n return math.NaN()\n }\n return f\n }\n return math.NaN()\n}\n\n// Floor returns the largest integer \u2264 v as a float64. Mirrors JS\n// `Math.floor`. The return type stays float64 so chained primitives\n// (`bf_floor` then `bf_string`) line up with JS\'s number type.\nfunc Floor(v any) float64 {\n return math.Floor(Number(v))\n}\n\n// Ceil returns the smallest integer \u2265 v as a float64. Mirrors JS\n// `Math.ceil`.\nfunc Ceil(v any) float64 {\n return math.Ceil(Number(v))\n}\n\n// Round returns v rounded to the nearest integer as a float64.\n// Mirrors JS `Math.round` \u2014 half-away-from-zero (Go\'s `math.Round`\n// matches; JS rounds half toward +Infinity which differs at .5\n// negatives; we accept that minor divergence since the conformance\n// contract is value-compat for the common positive case).\nfunc Round(v any) float64 {\n return math.Round(Number(v))\n}\n\n// =============================================================================\n// Array/Slice Operations\n// =============================================================================\n\n// Len returns the length of a slice, array, map, string, or channel.\n// Returns 0 for nil or unsupported types.\nfunc Len(v any) int {\n if v == nil {\n return 0\n }\n rv := reflect.ValueOf(v)\n switch rv.Kind() {\n case reflect.Slice, reflect.Array, reflect.Map, reflect.String, reflect.Chan:\n return rv.Len()\n default:\n return 0\n }\n}\n\n// At returns the element at index i from a slice.\n// Supports negative indices (e.g., -1 for last element).\n// Returns nil if index is out of bounds.\nfunc At(items any, index int) any {\n v := reflect.ValueOf(items)\n if v.Kind() != reflect.Slice && v.Kind() != reflect.Array {\n return nil\n }\n\n length := v.Len()\n if length == 0 {\n return nil\n }\n\n // Handle negative indices\n if index < 0 {\n index = length + index\n }\n\n if index < 0 || index >= length {\n return nil\n }\n\n return v.Index(index).Interface()\n}\n\n// Includes returns true if items contains elem. Lowers both\n// `Array.prototype.includes` and `String.prototype.includes` \u2014\n// the adapter can\'t disambiguate the receiver at compile time,\n// so this helper dispatches at runtime on `reflect.Kind()`:\n//\n// - slice/array receiver: DeepEqual element search\n// - string receiver: strings.Contains substring search\n//\n// Anything else returns false (matches the JS semantic where\n// `.includes` is only defined on Array / TypedArray / String).\nfunc Includes(recv any, elem any) bool {\n v := reflect.ValueOf(recv)\n if v.Kind() == reflect.String {\n // JS `String.prototype.includes` accepts only string args;\n // non-string `elem` would TypeError in real JS but our\n // callers have lowered through `convertExpressionToGo`\n // where the arg type is whatever the template binds. Stringify\n // via fmt to keep the helper total.\n needle, ok := elem.(string)\n if !ok {\n needle = fmt.Sprintf("%v", elem)\n }\n return strings.Contains(v.String(), needle)\n }\n if v.Kind() != reflect.Slice && v.Kind() != reflect.Array {\n return false\n }\n for i := 0; i < v.Len(); i++ {\n if reflect.DeepEqual(v.Index(i).Interface(), elem) {\n return true\n }\n }\n return false\n}\n\n// IndexOf returns the 0-based position of the first item that\n// DeepEquals `elem`, or -1 if not found. Lowers\n// `Array.prototype.indexOf(x)` (#1448 Tier A). The existing\n// `FindIndex` helper does struct-field equality (used by the\n// higher-order `.find` lowering); this one does value equality\n// against scalar / struct items so callers don\'t have to compose\n// a synthetic predicate.\n//\n// Non-array / non-slice receivers return -1 (matches the JS\n// semantic that `.indexOf` is only defined on Array / TypedArray).\nfunc IndexOf(items any, elem any) int {\n v := reflect.ValueOf(items)\n if v.Kind() != reflect.Slice && v.Kind() != reflect.Array {\n return -1\n }\n for i := 0; i < v.Len(); i++ {\n if reflect.DeepEqual(v.Index(i).Interface(), elem) {\n return i\n }\n }\n return -1\n}\n\n// LastIndexOf returns the 0-based position of the last item that\n// DeepEquals `elem`, or -1 if not found. Mirrors\n// `Array.prototype.lastIndexOf(x)`. The reverse traversal is the\n// only behavioural difference vs `IndexOf` \u2014 disambiguating a\n// duplicated value\'s first vs last position is the canonical\n// reason a JS author reaches for `lastIndexOf`.\nfunc LastIndexOf(items any, elem any) int {\n v := reflect.ValueOf(items)\n if v.Kind() != reflect.Slice && v.Kind() != reflect.Array {\n return -1\n }\n for i := v.Len() - 1; i >= 0; i-- {\n if reflect.DeepEqual(v.Index(i).Interface(), elem) {\n return i\n }\n }\n return -1\n}\n\n// Concat merges two arrays (or slices) into a single `[]any`,\n// preserving order: receiver elements first, then `other`\'s.\n// Lowers `Array.prototype.concat(other)` (#1448 Tier A). Non-array\n// operands collapse to an empty source \u2014 matches the JS semantic\n// where `.concat` on a non-Array reads it as a single element only\n// if its `Symbol.isConcatSpreadable` is true; the template-language\n// path doesn\'t have user objects with that flag, so treating\n// non-arrays as empty is the conservative lowering. Variadic\n// `.concat(a, b, c)` is out of scope here (parser gates to a single\n// arg); the helper itself stays binary so a future variadic IR can\n// fold via repeated calls without changing this signature.\nfunc Concat(a, b any) []any {\n flatten := func(v reflect.Value) []any {\n if !v.IsValid() {\n return nil\n }\n if v.Kind() != reflect.Slice && v.Kind() != reflect.Array {\n return nil\n }\n out := make([]any, v.Len())\n for i := 0; i < v.Len(); i++ {\n out[i] = v.Index(i).Interface()\n }\n return out\n }\n left := flatten(reflect.ValueOf(a))\n right := flatten(reflect.ValueOf(b))\n return append(left, right...)\n}\n\n// Slice carves out a sub-range from `items`. Lowers\n// `Array.prototype.slice(start, end?)` (#1448 Tier A). The variadic\n// `end` arg lets Go template\'s call dispatcher pass either 2 or 3\n// arguments; an absent end means "to length".\n//\n// JS-compat clamping:\n// - start < 0 \u2192 length + start (e.g. -1 = last index)\n// - end < 0 \u2192 length + end\n// - start < 0 after clamp \u2192 0\n// - end > length \u2192 length\n// - start >= end \u2192 empty slice (no panic)\n//\n// Non-array receivers return an empty `[]any`.\nfunc Slice(items any, start int, end ...int) []any {\n v := reflect.ValueOf(items)\n if v.Kind() != reflect.Slice && v.Kind() != reflect.Array {\n return []any{}\n }\n length := v.Len()\n\n // Normalise start (negative = from end).\n if start < 0 {\n start = length + start\n }\n if start < 0 {\n start = 0\n }\n if start > length {\n start = length\n }\n\n // Normalise end (optional; absent = length).\n stop := length\n if len(end) > 0 {\n stop = end[0]\n if stop < 0 {\n stop = length + stop\n }\n if stop < 0 {\n stop = 0\n }\n if stop > length {\n stop = length\n }\n }\n\n if start >= stop {\n return []any{}\n }\n\n out := make([]any, 0, stop-start)\n for i := start; i < stop; i++ {\n out = append(out, v.Index(i).Interface())\n }\n return out\n}\n\n// Reverse returns a new slice with `items`\'s elements in reverse\n// order. Lowers both `Array.prototype.reverse()` and\n// `Array.prototype.toReversed()` (#1448 Tier A) \u2014 SSR templates\n// render a snapshot, so JS\'s mutate-receiver vs return-new-array\n// distinction has no template-level meaning, and the safer\n// non-mutating shape is used uniformly.\n//\n// Non-array receivers return an empty `[]any`.\nfunc Reverse(items any) []any {\n v := reflect.ValueOf(items)\n if v.Kind() != reflect.Slice && v.Kind() != reflect.Array {\n return []any{}\n }\n length := v.Len()\n out := make([]any, length)\n for i := 0; i < length; i++ {\n out[length-1-i] = v.Index(i).Interface()\n }\n return out\n}\n\n// Flat flattens nested slices/arrays `depth` levels deep. Lowers\n// `Array.prototype.flat(depth?)` (#1448 Tier C). A `depth` of `-1` is the\n// `Infinity` sentinel (flatten fully); `0` (or negative-from-JS, already\n// normalised to 0 at compile time) returns a shallow copy. Non-array\n// elements are kept as-is (JS only flattens nested arrays). A non-array\n// receiver returns an empty `[]any`.\nfunc Flat(items any, depth int) []any {\n v := reflect.ValueOf(items)\n if v.Kind() != reflect.Slice && v.Kind() != reflect.Array {\n return []any{}\n }\n out := make([]any, 0, v.Len())\n for i := 0; i < v.Len(); i++ {\n el := v.Index(i).Interface()\n ev := reflect.ValueOf(el)\n if depth != 0 && (ev.Kind() == reflect.Slice || ev.Kind() == reflect.Array) {\n // `-1` (Infinity) recurses unbounded; a finite depth spends one level.\n next := depth\n if depth > 0 {\n next = depth - 1\n }\n out = append(out, Flat(el, next)...)\n } else {\n out = append(out, el)\n }\n }\n return out\n}\n\n// FlatMap projects each element through a `self` / `field` projection and\n// flattens the result one level. Lowers value-returning\n// `Array.prototype.flatMap(fn)` for the field-projection catalogue\n// (#1448 Tier C): `items.flatMap(i => i)` (self) and\n// `items.flatMap(i => i.field)` (field). A projected non-array value is\n// kept as-is (flatMap = map + flat(1)). Non-array receiver \u2192 empty.\nfunc FlatMap(items any, keyKind, keyName string) []any {\n v := reflect.ValueOf(items)\n if v.Kind() != reflect.Slice && v.Kind() != reflect.Array {\n return []any{}\n }\n projected := make([]any, 0, v.Len())\n for i := 0; i < v.Len(); i++ {\n el := v.Index(i).Interface()\n if keyKind == "field" {\n projected = append(projected, getFieldValue(el, keyName))\n } else {\n projected = append(projected, el)\n }\n }\n return Flat(projected, 1)\n}\n\n// FlatMapTuple lowers an array-literal flatMap projection\n// `items.flatMap(i => [i.a, i.b])` (#1448 Tier C). `specs` is a flat list\n// of (kind, name) pairs, one per array-literal leaf: ("self", "") for the\n// item itself, ("field", "<Name>") for a struct field. For each item it\n// appends every leaf\'s value in order. Unlike the scalar `FlatMap`, the\n// per-item array is flattened only one level (flat(1) removes the literal\n// wrapper), so an array-valued leaf is appended verbatim rather than\n// spread \u2014 which is exactly "append each leaf". Non-array receiver \u2192 empty.\nfunc FlatMapTuple(items any, specs ...string) []any {\n v := reflect.ValueOf(items)\n if v.Kind() != reflect.Slice && v.Kind() != reflect.Array {\n return []any{}\n }\n out := make([]any, 0, v.Len())\n for i := 0; i < v.Len(); i++ {\n el := v.Index(i).Interface()\n for j := 0; j+1 < len(specs); j += 2 {\n if specs[j] == "field" {\n out = append(out, getFieldValue(el, specs[j+1]))\n } else {\n out = append(out, el)\n }\n }\n }\n return out\n}\n\n// First returns the first element of a slice, or nil if empty.\nfunc First(items any) any {\n return At(items, 0)\n}\n\n// Last returns the last element of a slice, or nil if empty.\nfunc Last(items any) any {\n return At(items, -1)\n}\n\n// Arr builds an []any from variadic args. Used to lower JS array\n// literals like `[a, b]` for the registry Slot\'s\n// `[className, childClass].filter(Boolean).join(\' \')` shape (#1443) \u2014\n// Go templates have no array-literal syntax, so the codegen routes\n// array-literal IR nodes through this helper.\nfunc Arr(items ...any) []any {\n return items\n}\n\n// FilterTruthy returns a new slice containing only truthy items.\n// Mirrors `arr.filter(Boolean)` semantics: drop nil, false, 0, "" \u2014 the\n// same falsy set JavaScript\'s `Boolean(x)` recognises. Used to lower\n// the registry Slot\'s class-merge pattern (#1443); generalising to\n// arbitrary callable predicates would need the callee-resolution path\n// blocked by #1389, so this stays Boolean-specific.\nfunc FilterTruthy(items any) []any {\n v := reflect.ValueOf(items)\n if !v.IsValid() || (v.Kind() != reflect.Slice && v.Kind() != reflect.Array) {\n return nil\n }\n result := make([]any, 0, v.Len())\n for i := 0; i < v.Len(); i++ {\n raw := v.Index(i).Interface()\n if isTruthy(raw) {\n result = append(result, raw)\n }\n }\n return result\n}\n\n// Truthy is the exported form of isTruthy \u2014 JavaScript\'s `Boolean(x)`\n// semantics \u2014 for generated `NewXxxProps` code lowering a conditional\n// inline-object spread condition on an `interface{}` prop (whose runtime\n// value may be a string, number, bool, \u2026). Keeps the spread bag\'s\n// inclusion test faithful to JS rather than string-biased (#1752).\nfunc Truthy(v any) bool { return isTruthy(v) }\n\n// isTruthy mirrors JavaScript\'s `Boolean(x)` for the value shapes the\n// template path actually receives \u2014 nil / false / 0 / "" are falsy.\n// Other shapes (non-empty maps, slices, structs, true) are truthy, in\n// line with JS\'s "objects are truthy" rule.\nfunc isTruthy(v any) bool {\n if v == nil {\n return false\n }\n switch x := v.(type) {\n case bool:\n return x\n case string:\n return x != ""\n case int:\n return x != 0\n case int8, int16, int32, int64:\n return reflect.ValueOf(v).Int() != 0\n case uint, uint8, uint16, uint32, uint64:\n return reflect.ValueOf(v).Uint() != 0\n case float32:\n // JS `Boolean(NaN)` is false regardless of float width \u2014 the\n // float64 arm below was the only one checking IsNaN, which\n // diverged from JS for `float32` NaN inputs (Copilot review on\n // #1445). Widening to float64 for the IsNaN check keeps the\n // two branches in lock-step.\n return x != 0 && !math.IsNaN(float64(x))\n case float64:\n return x != 0 && !math.IsNaN(x)\n }\n return true\n}\n\n// =============================================================================\n// Higher-order Array Methods\n// =============================================================================\n\n// Every returns true if all items have the specified field set to true.\n// Mirrors JavaScript\'s Array.prototype.every(item => item.field).\nfunc Every(items any, field string) bool {\n v := reflect.ValueOf(items)\n if v.Kind() != reflect.Slice && v.Kind() != reflect.Array {\n return false\n }\n\n capitalizedField := capitalize(field)\n for i := 0; i < v.Len(); i++ {\n item := v.Index(i)\n if item.Kind() == reflect.Interface {\n item = item.Elem()\n }\n if item.Kind() == reflect.Ptr {\n item = item.Elem()\n }\n if item.Kind() != reflect.Struct {\n continue\n }\n\n fieldVal := item.FieldByName(capitalizedField)\n if !fieldVal.IsValid() {\n return false\n }\n if fieldVal.Kind() == reflect.Bool && !fieldVal.Bool() {\n return false\n }\n }\n return true\n}\n\n// Some returns true if at least one item has the specified field set to true.\n// Mirrors JavaScript\'s Array.prototype.some(item => item.field).\nfunc Some(items any, field string) bool {\n v := reflect.ValueOf(items)\n if v.Kind() != reflect.Slice && v.Kind() != reflect.Array {\n return false\n }\n\n capitalizedField := capitalize(field)\n for i := 0; i < v.Len(); i++ {\n item := v.Index(i)\n if item.Kind() == reflect.Interface {\n item = item.Elem()\n }\n if item.Kind() == reflect.Ptr {\n item = item.Elem()\n }\n if item.Kind() != reflect.Struct {\n continue\n }\n\n fieldVal := item.FieldByName(capitalizedField)\n if fieldVal.IsValid() && fieldVal.Kind() == reflect.Bool && fieldVal.Bool() {\n return true\n }\n }\n return false\n}\n\n// Filter returns items where item.field == value.\n// Mirrors JavaScript\'s Array.prototype.filter(item => item.field === value).\n// Returns []any to allow chaining with other bf_* functions.\nfunc Filter(items any, field string, value any) []any {\n v := reflect.ValueOf(items)\n if v.Kind() != reflect.Slice && v.Kind() != reflect.Array {\n return nil\n }\n\n capitalizedField := capitalize(field)\n var result []any\n\n for i := 0; i < v.Len(); i++ {\n item := v.Index(i)\n if item.Kind() == reflect.Interface {\n item = item.Elem()\n }\n if item.Kind() == reflect.Ptr {\n item = item.Elem()\n }\n if item.Kind() != reflect.Struct {\n continue\n }\n\n fieldVal := item.FieldByName(capitalizedField)\n if !fieldVal.IsValid() {\n continue\n }\n\n // Compare field value with target value\n if reflect.DeepEqual(fieldVal.Interface(), value) {\n result = append(result, v.Index(i).Interface())\n }\n }\n return result\n}\n\n// Find returns the first item where item.field == value, or nil if not found.\n// Mirrors JavaScript\'s Array.prototype.find(item => item.field === value).\nfunc Find(items any, field string, value any) any {\n v := reflect.ValueOf(items)\n if v.Kind() != reflect.Slice && v.Kind() != reflect.Array {\n return nil\n }\n\n capitalizedField := capitalize(field)\n for i := 0; i < v.Len(); i++ {\n item := v.Index(i)\n if item.Kind() == reflect.Interface {\n item = item.Elem()\n }\n if item.Kind() == reflect.Ptr {\n item = item.Elem()\n }\n if item.Kind() != reflect.Struct {\n continue\n }\n\n fieldVal := item.FieldByName(capitalizedField)\n if !fieldVal.IsValid() {\n continue\n }\n\n if reflect.DeepEqual(fieldVal.Interface(), value) {\n return v.Index(i).Interface()\n }\n }\n return nil\n}\n\n// FindIndex returns the index of the first item where item.field == value, or -1.\n// Mirrors JavaScript\'s Array.prototype.findIndex(item => item.field === value).\nfunc FindIndex(items any, field string, value any) int {\n v := reflect.ValueOf(items)\n if v.Kind() != reflect.Slice && v.Kind() != reflect.Array {\n return -1\n }\n\n capitalizedField := capitalize(field)\n for i := 0; i < v.Len(); i++ {\n item := v.Index(i)\n if item.Kind() == reflect.Interface {\n item = item.Elem()\n }\n if item.Kind() == reflect.Ptr {\n item = item.Elem()\n }\n if item.Kind() != reflect.Struct {\n continue\n }\n\n fieldVal := item.FieldByName(capitalizedField)\n if !fieldVal.IsValid() {\n continue\n }\n\n if reflect.DeepEqual(fieldVal.Interface(), value) {\n return i\n }\n }\n return -1\n}\n\n// FindLast returns the last item where item.field == value, or nil if not found.\n// Mirrors JavaScript\'s Array.prototype.findLast(item => item.field === value).\nfunc FindLast(items any, field string, value any) any {\n v := reflect.ValueOf(items)\n if v.Kind() != reflect.Slice && v.Kind() != reflect.Array {\n return nil\n }\n\n capitalizedField := capitalize(field)\n for i := v.Len() - 1; i >= 0; i-- {\n item := v.Index(i)\n if item.Kind() == reflect.Interface {\n if item.IsNil() {\n continue\n }\n item = item.Elem()\n }\n if item.Kind() == reflect.Ptr {\n if item.IsNil() {\n continue\n }\n item = item.Elem()\n }\n if item.Kind() != reflect.Struct {\n continue\n }\n\n fieldVal := item.FieldByName(capitalizedField)\n if !fieldVal.IsValid() {\n continue\n }\n\n if reflect.DeepEqual(fieldVal.Interface(), value) {\n return v.Index(i).Interface()\n }\n }\n return nil\n}\n\n// FindLastIndex returns the index of the last item where item.field == value, or -1.\n// Mirrors JavaScript\'s Array.prototype.findLastIndex(item => item.field === value).\nfunc FindLastIndex(items any, field string, value any) int {\n v := reflect.ValueOf(items)\n if v.Kind() != reflect.Slice && v.Kind() != reflect.Array {\n return -1\n }\n\n capitalizedField := capitalize(field)\n for i := v.Len() - 1; i >= 0; i-- {\n item := v.Index(i)\n if item.Kind() == reflect.Interface {\n if item.IsNil() {\n continue\n }\n item = item.Elem()\n }\n if item.Kind() == reflect.Ptr {\n if item.IsNil() {\n continue\n }\n item = item.Elem()\n }\n if item.Kind() != reflect.Struct {\n continue\n }\n\n fieldVal := item.FieldByName(capitalizedField)\n if !fieldVal.IsValid() {\n continue\n }\n\n if reflect.DeepEqual(fieldVal.Interface(), value) {\n return i\n }\n }\n return -1\n}\n\n// sortKeySpec is one parsed comparison key. A simple comparator has\n// one; a `||`-chained multi-key comparator has several, applied in\n// order as tie-breakers.\ntype sortKeySpec struct {\n kind string // "self" | "field"\n name string // capitalised field name, or "" for "self"\n compareType string // "numeric" | "string" | "auto"\n direction string // "asc" | "desc"\n}\n\n// Sort returns a new stable-sorted slice. Lowers\n// `Array.prototype.sort` / `Array.prototype.toSorted` (#1448 Tier B).\n// Non-mutating \u2014 JS\'s mutate-vs-new distinction is moot in SSR\n// template context (templates render a snapshot).\n//\n// Call shape (the compiler emits one 4-string group per key):\n//\n// bf_sort <items> (<keyKind> <keyName> <compareType> <direction>)+\n//\n// keyKind: "self" | "field"\n// keyName: "" when keyKind == "self"; capitalised struct field\n// name (e.g. "Price") otherwise\n// compareType: "numeric" | "string" | "auto"\n// direction: "asc" | "desc"\n//\n// The groups cover the accepted comparator catalogue: `a.f - b.f`,\n// `a - b`, `a[.f].localeCompare(b[.f])`, and relational-ternary keys\n// (`a.f > b.f ? 1 : -1` \u2192 "auto"), each `||`-chainable for multi-key\n// tie-breaks. Anything outside refuses at compile time (BF101 from the\n// JSX compiler) and never reaches this helper.\n//\n// "auto" compares numerically when both projected keys parse as\n// numbers, else lexically \u2014 mirroring the Perl `bf->sort` helper\'s\n// `looks_like_number` rule so the two template adapters stay\n// byte-equal. This diverges from JS `<`/`>` only for numeric strings.\n//\n// A future `nulls` knob can extend the per-key group without rewriting\n// existing call sites \u2014 each key already projects before comparing.\nfunc Sort(items any, spec ...string) []any {\n v := reflect.ValueOf(items)\n if v.Kind() != reflect.Slice && v.Kind() != reflect.Array {\n return nil\n }\n\n length := v.Len()\n if length == 0 {\n return []any{}\n }\n\n // Copy into a fresh []any so the sort is non-mutating regardless\n // of whether the receiver is `[]T` or `[]any`.\n result := make([]any, length)\n for i := 0; i < length; i++ {\n result[i] = v.Index(i).Interface()\n }\n\n keys := parseSortSpec(spec)\n sort.SliceStable(result, func(i, j int) bool {\n for _, k := range keys {\n ki := projectSortKey(result[i], k.kind, k.name)\n kj := projectSortKey(result[j], k.kind, k.name)\n c := compareSortKey(ki, kj, k.compareType)\n if c == 0 {\n continue // tie on this key \u2014 fall through to the next\n }\n if k.direction == "desc" {\n return c > 0\n }\n return c < 0\n }\n return false\n })\n\n return result\n}\n\n// parseSortSpec chunks the variadic operand list into 4-string key\n// groups. A trailing partial group (malformed emit) is ignored rather\n// than panicking \u2014 defensive, mirroring the helper\'s nil-safe stance.\nfunc parseSortSpec(spec []string) []sortKeySpec {\n var keys []sortKeySpec\n for i := 0; i+3 < len(spec); i += 4 {\n keys = append(keys, sortKeySpec{\n kind: spec[i],\n name: spec[i+1],\n compareType: spec[i+2],\n direction: spec[i+3],\n })\n }\n return keys\n}\n\n// compareSortKey returns -1 / 0 / 1 for two projected keys under the\n// given compare type (ascending orientation; the caller flips for\n// "desc"). "string" stringifies both (nil \u2192 "", matching the\n// documented `bf->string(undef) === ""` divergence). "auto" compares\n// numerically when both parse as numbers, else lexically.\nfunc compareSortKey(ki, kj any, compareType string) int {\n switch compareType {\n case "string":\n return strings.Compare(toString(ki), toString(kj))\n case "auto":\n ni, okI := toFloat64WithOK(ki)\n nj, okJ := toFloat64WithOK(kj)\n if okI && okJ {\n return cmpFloat(ni, nj)\n }\n return strings.Compare(toString(ki), toString(kj))\n default: // numeric\n return cmpFloat(toFloat64(ki), toFloat64(kj))\n }\n}\n\nfunc cmpFloat(a, b float64) int {\n if a < b {\n return -1\n }\n if a > b {\n return 1\n }\n return 0\n}\n\n// toFloat64WithOK reports a value\'s numeric float and whether it is\n// number-like. Genuine numeric kinds always qualify; strings qualify\n// when they parse as a float (so the "auto" compare path matches the\n// Perl `looks_like_number` rule). Everything else is non-numeric.\nfunc toFloat64WithOK(v any) (float64, bool) {\n switch n := v.(type) {\n case int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64, float32, float64:\n return toFloat64(v), true\n case string:\n f, err := strconv.ParseFloat(strings.TrimSpace(n), 64)\n if err != nil {\n return 0, false\n }\n return f, true\n default:\n return 0, false\n }\n}\n\n// projectSortKey reduces an item to the value the comparator\n// actually compares. For `keyKind == "field"` it reads the named\n// struct field; for `keyKind == "self"` (primitive arrays) it\n// returns the item unchanged.\nfunc projectSortKey(item any, keyKind, keyName string) any {\n if keyKind == "field" {\n return getFieldValue(item, keyName)\n }\n return item\n}\n\n// getFieldValue extracts a struct field value using reflection. For\n// map receivers it falls back to case-variant lookup so JSON-decoded\n// user data (`map[string]any{"price": 30}`) and PascalCase-emitted\n// test data both resolve under a single key name. (#1487)\nfunc getFieldValue(item any, field string) any {\n v := reflect.ValueOf(item)\n // Defensive IsNil guards mirror `SpreadAttrs` \u2014 keeps the helper\n // safe against typed-nil pointer / nil-interface items inside a\n // `[]any` so a single bad row doesn\'t crash the whole sort.\n if v.Kind() == reflect.Interface {\n if v.IsNil() {\n return nil\n }\n v = v.Elem()\n }\n if v.Kind() == reflect.Ptr {\n if v.IsNil() {\n return nil\n }\n v = v.Elem()\n }\n\n if v.Kind() == reflect.Map {\n keyType := v.Type().Key()\n if keyType.Kind() != reflect.String {\n return nil\n }\n // Convert the lookup string to the map\'s actual key type so\n // maps keyed by a named string type (`type Key string`) don\'t\n // panic with `value of type string is not assignable to type X`.\n lookup := func(s string) (any, bool) {\n k := reflect.ValueOf(s).Convert(keyType)\n if mv := v.MapIndex(k); mv.IsValid() {\n return mv.Interface(), true\n }\n return nil, false\n }\n if r, ok := lookup(field); ok {\n return r\n }\n if cap := capitalize(field); cap != field {\n if r, ok := lookup(cap); ok {\n return r\n }\n }\n if low := decapitalize(field); low != field {\n if r, ok := lookup(low); ok {\n return r\n }\n }\n // All-lowercase fallback: a Go-initialism field projects as an\n // all-caps key (`id` \u2192 `ID`), and `decapitalize("ID")` only\n // lowers the first char (`iD`), so the JS-keyed map ("id") still\n // misses. Try the fully-lowered key last to resolve it.\n if lower := strings.ToLower(field); lower != field && lower != decapitalize(field) {\n if r, ok := lookup(lower); ok {\n return r\n }\n }\n return nil\n }\n\n if v.Kind() != reflect.Struct {\n return nil\n }\n\n fieldVal := v.FieldByName(field)\n if !fieldVal.IsValid() {\n return nil\n }\n return fieldVal.Interface()\n}\n\n// capitalize uppercases the first character of a string.\nfunc capitalize(s string) string {\n if s == "" {\n return s\n }\n return strings.ToUpper(s[:1]) + s[1:]\n}\n\n// decapitalize lowercases the first character of a string. Used by\n// `getFieldValue`\'s map-receiver fallback when the projected key\n// name is PascalCase but the receiver carries lowercase JS-style\n// keys (the inverse of the `capitalize` lookup).\nfunc decapitalize(s string) string {\n if s == "" {\n return s\n }\n return strings.ToLower(s[:1]) + s[1:]\n}\n\n// Reduce folds an array into a scalar via the arithmetic-fold\n// catalogue (#1448 Tier C). It lowers `Array.prototype.reduce(fn, init)`\n// and `Array.prototype.reduceRight(fn, init)` for the shapes\n// `(acc, x) => acc <op> x` and `(acc, x) => acc <op> x.field`:\n//\n// bf_reduce <items> "<op>" "<keyKind>" "<keyName>" "<type>" "<init>" "<direction>"\n//\n// direction: "left" (reduce) | "right" (reduceRight). Only changes the\n// result for string concatenation; numeric folds commute.\n//\n// op: "+" | "*"\n// keyKind: "self" | "field"\n// keyName: "" when keyKind == "self"; capitalised struct field name\n// (e.g. "Duration") otherwise\n// type: "numeric" | "string"\n// init: the fold\'s start value \u2014 the compiler emits the *decoded*\n// seed, so numeric inits arrive as canonical decimal\n// (`1_000`/`0x10` already normalised to `1000`/`16`) that\n// ParseFloat accepts, and string inits arrive as escape-free\n// contents\n//\n// Numeric folds accumulate as float64; each projected key is read via\n// `toFloat64WithOK`, so numeric *strings* ("5" \u2192 5) parse and\n// non-numeric values fold as 0 \u2014 matching Perl\'s\n// `looks_like_number ? $n : 0` so the two template adapters stay\n// byte-equal. String folds concatenate (toString per projected key,\n// matching the documented `bf->string(undef) === ""` convention). The\n// init seeds the accumulator, so an empty receiver returns the init\n// unchanged \u2014 exactly like JS `reduce(fn, init)`. Anything outside the\n// catalogue refuses at compile time (BF101 from the JSX compiler) and\n// never reaches here.\n//\n// Two documented divergences from the JS / Hono path, both rare and\n// mirroring the `bf_sort` "auto" caveat:\n// - float64 stringification differs for sums whose binary expansion\n// isn\'t exact (e.g. 0.1 + 0.2);\n// - numeric-*string* keys fold numerically here, but JS `+`\n// string-concatenates once an operand is a string, so\n// numeric-string data can render differently under CSR.\n// Genuine numbers \u2014 the common SSR case \u2014 agree across all three.\nfunc Reduce(items any, op, keyKind, keyName, typ, init, direction string) any {\n v := reflect.ValueOf(items)\n isSlice := v.Kind() == reflect.Slice || v.Kind() == reflect.Array\n\n // `direction == "right"` (reduceRight) folds right-to-left. Only\n // observable for string concatenation \u2014 numeric sum / product are\n // commutative, so the order doesn\'t change the result there. Build a\n // start/stop/step triple so both folds share one loop shape.\n start, stop, step := 0, 0, 1\n if isSlice {\n stop = v.Len()\n if direction == "right" {\n start, stop, step = v.Len()-1, -1, -1\n }\n }\n\n if typ == "string" {\n acc := init\n if isSlice {\n for i := start; i != stop; i += step {\n key := projectSortKey(v.Index(i).Interface(), keyKind, keyName)\n acc += toString(key)\n }\n }\n return acc\n }\n\n // numeric fold\n acc, _ := strconv.ParseFloat(strings.TrimSpace(init), 64)\n if isSlice {\n for i := start; i != stop; i += step {\n key := projectSortKey(v.Index(i).Interface(), keyKind, keyName)\n // `toFloat64WithOK` parses numeric *strings* ("5" \u2192 5) and\n // returns 0 for non-numeric values \u2014 mirroring Perl\'s\n // `looks_like_number ? $n : 0` so numeric-string data folds\n // byte-equal across adapters (the same rule `bf_sort`\'s\n // "auto" compare uses). Plain `toFloat64` would zero "5".\n n, _ := toFloat64WithOK(key)\n if op == "*" {\n acc *= n\n } else {\n acc += n\n }\n }\n }\n return acc\n}\n\n// =============================================================================\n// HTML/Template Helpers\n// =============================================================================\n\n// Comment returns an HTML comment string for hydration markers.\n// The "bf-" prefix is automatically added.\nfunc Comment(content string) template.HTML {\n return template.HTML("<!--bf-" + content + "-->")\n}\n\n// TextStart returns an HTML comment start marker for reactive text expressions.\n// Format: <!--bf:slotId-->\nfunc TextStart(slotId string) template.HTML {\n return template.HTML("<!--bf:" + slotId + "-->")\n}\n\n// TextEnd returns an HTML comment end marker for reactive text expressions.\n// Format: <!--/-->\nfunc TextEnd() template.HTML {\n return "<!--/-->"\n}\n\n// ScopeComment emits a fragment-rooted scope marker. See spec/compiler.md\n// "Slot identity" for the wire format. Loud-fails on marshal errors\n// (same policy as JSON / BfPropsAttr).\nfunc ScopeComment(props interface{}) (template.HTML, error) {\n scopeID := getStringField(props, "ScopeID")\n hostSegment := ""\n if host := getStringField(props, "BfParent"); host != "" {\n mount := getStringField(props, "BfMount")\n hostSegment = "|h=" + host + "|m=" + mount\n }\n propsJSON := ""\n if getBoolField(props, "BfIsRoot") {\n pJSON, err := json.Marshal(props)\n if err != nil {\n return "", err\n }\n propsJSON = "|" + string(pJSON)\n }\n return template.HTML("<!--bf-scope:" + scopeID + hostSegment + propsJSON + "-->"), nil\n}\n\n// PortalHTML parses and executes a template string with the provided data.\n// Used for rendering dynamic portal content where the template string\n// contains Go template expressions (e.g., {{if .Open}}open{{end}}).\n//\n// The template string is parsed fresh each time to support dynamic content.\n// Standard Go template functions (if, range, eq, etc.) are available.\nfunc PortalHTML(data interface{}, tmplStr string) template.HTML {\n // Create a new template with the FuncMap for custom functions\n t, err := template.New("portal").Funcs(FuncMap()).Parse(tmplStr)\n if err != nil {\n // Return error message as HTML comment for debugging\n return template.HTML("<!-- bfPortalHTML error: " + err.Error() + " -->")\n }\n\n var buf bytes.Buffer\n if err := t.Execute(&buf, data); err != nil {\n return template.HTML("<!-- bfPortalHTML exec error: " + err.Error() + " -->")\n }\n\n return template.HTML(buf.String())\n}\n\n// =============================================================================\n// Portal Collection\n// =============================================================================\n\n// PortalContent represents a single portal\'s content to be rendered at body end.\ntype PortalContent struct {\n ID string // Unique portal ID for hydration matching\n OwnerID string // Owner scope ID for find() support\n Content template.HTML // Portal HTML content\n}\n\n// PortalCollector collects portal content during template rendering.\n// Portal content is rendered at </body> to avoid z-index issues.\ntype PortalCollector struct {\n portals []PortalContent\n counter int\n}\n\n// NewPortalCollector creates a new PortalCollector.\nfunc NewPortalCollector() *PortalCollector {\n return &PortalCollector{\n portals: []PortalContent{},\n counter: 0,\n }\n}\n\n// Add registers portal content to be rendered at body end.\nfunc (pc *PortalCollector) Add(ownerID string, content template.HTML) string {\n pc.counter++\n id := "bf-portal-" + strconv.Itoa(pc.counter)\n pc.portals = append(pc.portals, PortalContent{\n ID: id,\n OwnerID: ownerID,\n Content: content,\n })\n return "" // Return empty string for template use\n}\n\n// Render outputs all collected portals as HTML.\n// Each portal is wrapped in a div with bf-pi (portal ID) and bf-po (portal owner).\nfunc (pc *PortalCollector) Render() template.HTML {\n if pc == nil || len(pc.portals) == 0 {\n return ""\n }\n var buf strings.Builder\n for _, p := range pc.portals {\n buf.WriteString(`<div bf-pi="`)\n buf.WriteString(p.ID)\n buf.WriteString(`" bf-po="`)\n buf.WriteString(p.OwnerID)\n buf.WriteString(`">`)\n buf.WriteString(string(p.Content))\n buf.WriteString("</div>\\n")\n }\n return template.HTML(buf.String())\n}\n\n// =============================================================================\n// Script Collection\n// =============================================================================\n\n// ScriptCollector collects client scripts with deduplication.\n// It preserves insertion order for deterministic output.\ntype ScriptCollector struct {\n scripts map[string]bool\n order []string\n}\n\n// NewScriptCollector creates a new ScriptCollector.\nfunc NewScriptCollector() *ScriptCollector {\n return &ScriptCollector{\n scripts: make(map[string]bool),\n order: []string{},\n }\n}\n\n// Register adds a script source to the collection.\n// Duplicate scripts are ignored (only first registration counts).\nfunc (sc *ScriptCollector) Register(src string) string {\n if sc.scripts[src] {\n return "" // Already registered\n }\n sc.scripts[src] = true\n sc.order = append(sc.order, src)\n return "" // Return empty string for template use\n}\n\n// Scripts returns all registered scripts in insertion order.\nfunc (sc *ScriptCollector) Scripts() []string {\n return sc.order\n}\n\n// BfScripts generates script tags for all registered scripts.\n// Returns HTML safe for embedding in templates.\nfunc BfScripts(collector *ScriptCollector) template.HTML {\n if collector == nil {\n return ""\n }\n var result strings.Builder\n for _, src := range collector.Scripts() {\n result.WriteString(`<script type="module" src="`)\n result.WriteString(src)\n result.WriteString(`"></script>`)\n result.WriteString("\\n")\n }\n return template.HTML(result.String())\n}\n\n// =============================================================================\n// Component Renderer\n// =============================================================================\n\n// RenderContext contains all data needed to render a component page.\n// The layout function receives this context to build the final HTML.\ntype RenderContext struct {\n // ComponentName is the template name being rendered\n ComponentName string\n\n // Props is the component props (for layout to access if needed)\n Props interface{}\n\n // ComponentHTML is the rendered component template output\n ComponentHTML template.HTML\n\n // Portals contains collected portal content to render at body end\n Portals template.HTML\n\n // Scripts contains the collected JS script tags\n Scripts template.HTML\n\n // Title is the page title (defaults to "{ComponentName} - BarefootJS")\n Title string\n\n // Heading is the page heading. Empty string means no heading.\n Heading string\n\n // Extra holds additional user-defined data for the layout\n Extra map[string]interface{}\n}\n\n// LayoutFunc renders the final HTML page given the render context.\ntype LayoutFunc func(ctx *RenderContext) string\n\n// Renderer renders BarefootJS components with a customizable layout.\ntype Renderer struct {\n templates *template.Template\n layout LayoutFunc\n}\n\n// NewRenderer creates a Renderer with the given templates and layout function.\n//\n// Example usage:\n//\n// renderer := bf.NewRenderer(templates, func(ctx *bf.RenderContext) string {\n// return fmt.Sprintf(`<!DOCTYPE html>\n// <html>\n// <head><title>%s</title></head>\n// <body>%s%s</body>\n// </html>`, ctx.Title, ctx.ComponentHTML, ctx.Scripts)\n// })\nfunc NewRenderer(tmpl *template.Template, layout LayoutFunc) *Renderer {\n return &Renderer{\n templates: tmpl,\n layout: layout,\n }\n}\n\n// RenderOptions configures a single render call.\ntype RenderOptions struct {\n // ComponentName is the template name to render (required)\n ComponentName string\n\n // Props is the component props (must be a pointer to struct with Scripts field)\n Props interface{}\n\n // Title is the page title. If empty, defaults to "{ComponentName} - BarefootJS"\n Title string\n\n // Heading is the page heading. If empty, no heading is shown.\n Heading string\n\n // Extra holds additional data to pass to the layout\n Extra map[string]interface{}\n}\n\n// Render renders a component to a full HTML page using the configured layout.\n// Child component props are automatically detected (any slice field with ScopeID/Scripts).\n// renderTemplateErrorPanel formats a Go template execution error into a\n// fragment of HTML that\'s visible in the browser. The panel is\n// HTML-escaped so a faulty template name (anything from `template:\n// "..."`) can\'t smuggle markup back into the page. Keep the styling\n// inline so the panel surfaces even when the project\'s CSS hasn\'t\n// loaded yet (e.g. the failure aborted before the stylesheet links\n// emitted).\n//\n// Surfaced for the #1442 echo repro: a template referencing\n// `.Todo.Done` (instead of the range dot\'s `.Done`) used to fail\n// silently \u2014 Go\'s html/template aborted mid-stream, the partial body\n// flushed as a 200, and the user saw a truncated list with no console\n// signal. With this panel they get the template name, the error\n// message, and a "what to look at" hint inline.\nfunc renderTemplateErrorPanel(componentName string, err error) string {\n return `<div style="margin:1em 0;padding:1em;border:2px solid #d33;background:#fff5f5;color:#900;font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;font-size:13px;line-height:1.5"><strong style="display:block;margin-bottom:.5em">Template error in <code>` +\n template.HTMLEscapeString(componentName) +\n `</code></strong><pre style="margin:0;white-space:pre-wrap;word-break:break-word">` +\n template.HTMLEscapeString(err.Error()) +\n `</pre><div style="margin-top:.75em;font-size:12px;opacity:.7">Common cause: a JSX expression referenced a name the adapter could not resolve to a struct field. Open the matching <code>dist/templates/*.tmpl</code> for the unresolved reference, then fix the source component.</div></div>`\n}\n\nfunc (r *Renderer) Render(opts RenderOptions) string {\n // Create script collector and inject into props\n scriptCollector := NewScriptCollector()\n setScriptsField(opts.Props, scriptCollector)\n\n // Create portal collector and inject into props\n portalCollector := NewPortalCollector()\n setPortalsField(opts.Props, portalCollector)\n\n // Auto-detect and process child component props (slices)\n childSlices := findChildComponentSlices(opts.Props)\n for _, slice := range childSlices {\n setScopeIDsOnSlice(slice)\n setScriptsOnSlice(slice, scriptCollector)\n setPortalsOnSlice(slice, portalCollector)\n setBoolOnSlice(slice, "BfIsChild", true)\n }\n\n // Auto-detect and process single child component props\n singleChildren := findSingleChildComponents(opts.Props)\n for _, child := range singleChildren {\n setScopeIDOnSingle(child)\n setScriptsOnSingle(child, scriptCollector)\n setPortalsOnSingle(child, portalCollector)\n setBoolField(child, "BfIsChild", true)\n }\n\n // Mark the root component so BfPropsAttr emits bf-p only for it\n setBoolField(opts.Props, "BfIsRoot", true)\n\n // Render the component template.\n //\n // Errors here are NOT silently dropped. The original implementation\n // ignored the return value of `ExecuteTemplate`, which masked a real\n // onboarding failure mode: a template referencing a non-existent\n // field (`.Todo.Done` instead of the range dot\'s `.Done`) caused\n // html/template to abort mid-stream, the partial output got\n // returned, and the HTTP server happily flushed a 200 with a\n // truncated body. No error log, no signal \u2014 the user just saw a\n // blank list (#1442 echo TodoApp repro).\n //\n // Now we capture the error and replace the partial output with a\n // visible inline panel (dev mode) or a fenced error comment\n // (production), so the cause is on-screen and grep-able in logs.\n // Either way the renderer also writes to stderr so structured log\n // aggregators see it.\n var componentBuf strings.Builder\n if err := r.templates.ExecuteTemplate(&componentBuf, opts.ComponentName, opts.Props); err != nil {\n fmt.Fprintf(os.Stderr, "barefoot: template %q failed to render: %v\\n", opts.ComponentName, err)\n // Preserve whatever the template did manage to emit before\n // failing (Go\'s text/template flushes incrementally), but\n // follow it with a clearly-marked error block so the user\n // notices something is wrong instead of seeing a silent\n // truncation.\n componentBuf.WriteString(renderTemplateErrorPanel(opts.ComponentName, err))\n }\n\n // Determine title (default: "{ComponentName} - BarefootJS")\n title := opts.Title\n if title == "" {\n title = opts.ComponentName + " - BarefootJS"\n }\n\n // Heading (empty means no heading)\n heading := opts.Heading\n\n // Build render context\n ctx := &RenderContext{\n ComponentName: opts.ComponentName,\n Props: opts.Props,\n ComponentHTML: template.HTML(componentBuf.String()),\n Portals: portalCollector.Render(),\n Scripts: BfScripts(scriptCollector),\n Title: title,\n Heading: heading,\n Extra: opts.Extra,\n }\n\n return r.layout(ctx)\n}\n\n// setScriptsField sets the Scripts field on a struct using reflection.\nfunc setScriptsField(v interface{}, collector *ScriptCollector) {\n val := reflect.ValueOf(v)\n if val.Kind() == reflect.Ptr {\n val = val.Elem()\n }\n if val.Kind() != reflect.Struct {\n return\n }\n field := val.FieldByName("Scripts")\n if field.IsValid() && field.CanSet() {\n field.Set(reflect.ValueOf(collector))\n }\n}\n\n// setPortalsField sets the Portals field on a struct using reflection.\nfunc setPortalsField(v interface{}, collector *PortalCollector) {\n val := reflect.ValueOf(v)\n if val.Kind() == reflect.Ptr {\n val = val.Elem()\n }\n if val.Kind() != reflect.Struct {\n return\n }\n field := val.FieldByName("Portals")\n if field.IsValid() && field.CanSet() {\n field.Set(reflect.ValueOf(collector))\n }\n}\n\n// getStringField extracts a string field from a struct using reflection.\nfunc setBoolField(v interface{}, fieldName string, val bool) {\n rv := reflect.ValueOf(v)\n if rv.Kind() == reflect.Ptr {\n rv = rv.Elem()\n }\n if rv.Kind() != reflect.Struct {\n return\n }\n field := rv.FieldByName(fieldName)\n if field.IsValid() && field.CanSet() && field.Kind() == reflect.Bool {\n field.SetBool(val)\n }\n}\n\nfunc getBoolField(v interface{}, fieldName string) bool {\n val := reflect.ValueOf(v)\n if val.Kind() == reflect.Ptr {\n val = val.Elem()\n }\n if val.Kind() != reflect.Struct {\n return false\n }\n field := val.FieldByName(fieldName)\n if !field.IsValid() || field.Kind() != reflect.Bool {\n return false\n }\n return field.Bool()\n}\n\nfunc getStringField(v interface{}, fieldName string) string {\n val := reflect.ValueOf(v)\n if val.Kind() == reflect.Ptr {\n val = val.Elem()\n }\n if val.Kind() != reflect.Struct {\n return ""\n }\n field := val.FieldByName(fieldName)\n if !field.IsValid() || field.Kind() != reflect.String {\n return ""\n }\n return field.String()\n}\n\n// scopeIDChars is the alphabet for auto-generated ScopeID suffixes. It\n// mirrors the `randomID` helper the go-template adapter emits into the\n// generated New<Component>Props constructors so runtime-assigned and\n// constructor-assigned ids are indistinguishable.\nconst scopeIDChars = "abcdefghijklmnopqrstuvwxyz0123456789"\n\n// randomScopeSuffix returns a random lowercase-alphanumeric string of\n// length n. math/rand (auto-seeded since Go 1.20) is sufficient here: the\n// suffix only needs to be unique enough to keep a page\'s bf-s scope ids\n// from colliding, not cryptographically unpredictable.\nfunc randomScopeSuffix(n int) string {\n b := make([]byte, n)\n for i := range b {\n b[i] = scopeIDChars[rand.Intn(len(scopeIDChars))]\n }\n return string(b)\n}\n\n// scopeIDPrefix derives the human-readable ScopeID prefix from a child\n// component\'s type, e.g. `TodoItemProps` \u2192 `TodoItem`. Matches the\n// `"<Component>_" + randomID(6)` shape the generated constructors use.\nfunc scopeIDPrefix(t reflect.Type) string {\n for t.Kind() == reflect.Ptr {\n t = t.Elem()\n }\n return strings.TrimSuffix(t.Name(), "Props")\n}\n\n// assignScopeID fills a child component\'s ScopeID with a generated id when\n// the caller left it empty, so application code doesn\'t have to mint scope\n// ids by hand (the parent\'s New<Component>Props constructor does the same\n// for components built through it). A non-empty ScopeID is left untouched,\n// so callers can still pin a stable id when they need one.\nfunc assignScopeID(structVal reflect.Value, prefix string) {\n field := structVal.FieldByName("ScopeID")\n if !field.IsValid() || !field.CanSet() || field.Kind() != reflect.String {\n return\n }\n if field.String() != "" {\n return\n }\n id := randomScopeSuffix(6)\n if prefix != "" {\n id = prefix + "_" + id\n }\n field.SetString(id)\n}\n\n// setScopeIDsOnSlice assigns a generated ScopeID to every child in a slice\n// whose ScopeID is empty.\nfunc setScopeIDsOnSlice(slice interface{}) {\n v := reflect.ValueOf(slice)\n if v.Kind() != reflect.Slice {\n return\n }\n prefix := scopeIDPrefix(v.Type().Elem())\n for i := 0; i < v.Len(); i++ {\n item := v.Index(i)\n if item.Kind() == reflect.Ptr {\n if item.IsNil() {\n continue\n }\n item = item.Elem()\n }\n if item.Kind() == reflect.Struct {\n assignScopeID(item, prefix)\n }\n }\n}\n\n// setScopeIDOnSingle assigns a generated ScopeID to a single child\n// component when its ScopeID is empty.\nfunc setScopeIDOnSingle(child interface{}) {\n v := reflect.ValueOf(child)\n if v.Kind() == reflect.Ptr {\n v = v.Elem()\n }\n if v.Kind() != reflect.Struct {\n return\n }\n assignScopeID(v, scopeIDPrefix(v.Type()))\n}\n\n// findChildComponentSlices finds slice fields containing child component props.\n// Child props are identified by having ScopeID and Scripts fields.\nfunc findChildComponentSlices(props interface{}) []interface{} {\n var result []interface{}\n\n val := reflect.ValueOf(props)\n if val.Kind() == reflect.Ptr {\n val = val.Elem()\n }\n if val.Kind() != reflect.Struct {\n return result\n }\n\n for i := 0; i < val.NumField(); i++ {\n field := val.Field(i)\n if field.Kind() != reflect.Slice || field.Len() == 0 {\n continue\n }\n\n elem := field.Index(0)\n if elem.Kind() == reflect.Ptr {\n elem = elem.Elem()\n }\n if elem.Kind() != reflect.Struct {\n continue\n }\n\n hasScopeID := elem.FieldByName("ScopeID").IsValid()\n hasScripts := elem.FieldByName("Scripts").IsValid()\n\n if hasScopeID && hasScripts {\n result = append(result, field.Interface())\n }\n }\n\n return result\n}\n\n// setScriptsOnSlice sets Scripts on all items in a slice.\nfunc setScriptsOnSlice(slice interface{}, collector *ScriptCollector) {\n val := reflect.ValueOf(slice)\n if val.Kind() != reflect.Slice {\n return\n }\n for i := 0; i < val.Len(); i++ {\n item := val.Index(i)\n if item.Kind() == reflect.Ptr {\n item = item.Elem()\n }\n if item.Kind() == reflect.Struct {\n field := item.FieldByName("Scripts")\n if field.IsValid() && field.CanSet() {\n field.Set(reflect.ValueOf(collector))\n }\n }\n }\n}\n\n// setBoolOnSlice sets a bool field on all items in a slice.\nfunc setBoolOnSlice(slice interface{}, fieldName string, val bool) {\n v := reflect.ValueOf(slice)\n if v.Kind() != reflect.Slice {\n return\n }\n for i := 0; i < v.Len(); i++ {\n item := v.Index(i)\n if item.Kind() == reflect.Ptr {\n item = item.Elem()\n }\n if item.Kind() == reflect.Struct {\n field := item.FieldByName(fieldName)\n if field.IsValid() && field.CanSet() && field.Kind() == reflect.Bool {\n field.SetBool(val)\n }\n }\n }\n}\n\n// setPortalsOnSlice sets Portals on all items in a slice.\nfunc setPortalsOnSlice(slice interface{}, collector *PortalCollector) {\n val := reflect.ValueOf(slice)\n if val.Kind() != reflect.Slice {\n return\n }\n for i := 0; i < val.Len(); i++ {\n item := val.Index(i)\n if item.Kind() == reflect.Ptr {\n item = item.Elem()\n }\n if item.Kind() == reflect.Struct {\n field := item.FieldByName("Portals")\n if field.IsValid() && field.CanSet() {\n field.Set(reflect.ValueOf(collector))\n }\n }\n }\n}\n\n\n// findSingleChildComponents finds single struct fields containing child component props.\n// Child props are identified by having ScopeID and Scripts fields.\nfunc findSingleChildComponents(props interface{}) []interface{} {\n var result []interface{}\n\n val := reflect.ValueOf(props)\n if val.Kind() == reflect.Ptr {\n val = val.Elem()\n }\n if val.Kind() != reflect.Struct {\n return result\n }\n\n for i := 0; i < val.NumField(); i++ {\n field := val.Field(i)\n\n // Handle pointer to struct\n if field.Kind() == reflect.Ptr {\n if field.IsNil() {\n continue\n }\n field = field.Elem()\n }\n\n // Skip non-struct fields (slices handled by findChildComponentSlices)\n if field.Kind() != reflect.Struct {\n continue\n }\n\n hasScopeID := field.FieldByName("ScopeID").IsValid()\n hasScripts := field.FieldByName("Scripts").IsValid()\n\n if hasScopeID && hasScripts {\n result = append(result, field.Addr().Interface())\n }\n }\n\n return result\n}\n\n// setScriptsOnSingle sets Scripts on a single struct child component.\nfunc setScriptsOnSingle(child interface{}, collector *ScriptCollector) {\n val := reflect.ValueOf(child)\n if val.Kind() == reflect.Ptr {\n val = val.Elem()\n }\n if val.Kind() == reflect.Struct {\n field := val.FieldByName("Scripts")\n if field.IsValid() && field.CanSet() {\n field.Set(reflect.ValueOf(collector))\n }\n }\n}\n\n// setPortalsOnSingle sets Portals on a single struct child component.\nfunc setPortalsOnSingle(child interface{}, collector *PortalCollector) {\n val := reflect.ValueOf(child)\n if val.Kind() == reflect.Ptr {\n val = val.Elem()\n }\n if val.Kind() == reflect.Struct {\n field := val.FieldByName("Portals")\n if field.IsValid() && field.CanSet() {\n field.Set(reflect.ValueOf(collector))\n }\n }\n}\n\n\n// =============================================================================\n// Internal Helpers\n// =============================================================================\n\nfunc toFloat64(v any) float64 {\n switch n := v.(type) {\n case int:\n return float64(n)\n case int8:\n return float64(n)\n case int16:\n return float64(n)\n case int32:\n return float64(n)\n case int64:\n return float64(n)\n case uint:\n return float64(n)\n case uint8:\n return float64(n)\n case uint16:\n return float64(n)\n case uint32:\n return float64(n)\n case uint64:\n return float64(n)\n case float32:\n return float64(n)\n case float64:\n return n\n default:\n return 0\n }\n}\n\nfunc toInt(v any) int {\n switch n := v.(type) {\n case int:\n return n\n case int8:\n return int(n)\n case int16:\n return int(n)\n case int32:\n return int(n)\n case int64:\n return int(n)\n case uint:\n return int(n)\n case uint8:\n return int(n)\n case uint16:\n return int(n)\n case uint32:\n return int(n)\n case uint64:\n return int(n)\n case float32:\n return int(n)\n case float64:\n return int(n)\n default:\n return 0\n }\n}\n\nfunc isIntLike(v any) bool {\n switch v.(type) {\n case int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64:\n return true\n default:\n return false\n }\n}\n\nfunc toString(v any) string {\n switch s := v.(type) {\n case string:\n return s\n case int:\n return strconv.Itoa(s)\n case int64:\n return strconv.FormatInt(s, 10)\n case float64:\n return strconv.FormatFloat(s, \'f\', -1, 64)\n case bool:\n return strconv.FormatBool(s)\n default:\n return ""\n }\n}\n';
23047
23342
  streamingGoSource = `// Package bf \u2014 Out-of-Order Streaming SSR helpers
23048
23343
  //
23049
23344
  // Provides StreamRenderer for progressive page rendering using HTTP
@@ -23251,11 +23546,11 @@ func StreamingFuncMap() template.FuncMap {
23251
23546
  }
23252
23547
  `;
23253
23548
  bfdevGoSource = '// Package bfdev provides a dev-only browser auto-reload handler.\n//\n// It watches `<distDir>/.dev/build-id` (produced by `bf build --watch`\n// in the @barefootjs/cli package) and streams SSE `event: reload` whenever\n// the sentinel changes. Combined with the inline client snippet returned by\n// Snippet, editing a .tsx component triggers a browser reload automatically.\n//\n// The handler is framework-agnostic (net/http.Handler). Echo users can mount\n// it via echo.WrapHandler; other routers use it as-is.\n//\n// Example (Echo):\n//\n// if bfdev.IsDevDefault() {\n// e.GET("/_bf/reload", echo.WrapHandler(bfdev.NewReloadHandler(bfdev.Config{\n// DistDir: "./dist",\n// })))\n// }\n//\n// Example (net/http):\n//\n// http.Handle("/_bf/reload", bfdev.NewReloadHandler(bfdev.Config{DistDir: "./dist"}))\npackage bfdev\n\nimport (\n "fmt"\n "html/template"\n "net/http"\n "os"\n "path/filepath"\n "strings"\n "time"\n)\n\n// Sentinel path contract with `@barefootjs/cli`\n// (`packages/cli/src/lib/build.ts`, DEV_SENTINEL_SUBDIR / DEV_SENTINEL_FILENAME).\n// Duplicated here so the Go runtime avoids a dependency on the CLI. If the\n// CLI changes these values, update this package in the same PR.\nconst (\n devSubdir = ".dev"\n buildIDFile = "build-id"\n scrollStorageKey = "__bf_devreload_scroll"\n\n // heartbeatInterval keeps the SSE stream under the framework\'s idle\n // timeout (Bun.serve defaults to 10s; Go/Echo defaults are more forgiving\n // but middleware-level timeouts exist in the wild). 5s leaves comfortable\n // headroom.\n heartbeatInterval = 5 * time.Second\n\n // pollInterval is how often the handler checks `.dev/build-id`. Uses\n // polling instead of fsnotify to keep the runtime dependency-free \u2014 dev\n // latency of ~500ms is imperceptible next to the browser\'s reload time.\n pollInterval = 500 * time.Millisecond\n)\n\n// Config configures a dev reload handler or snippet.\ntype Config struct {\n // DistDir is the directory that `bf build` writes output into\n // (contains `.dev/build-id`). Required for the handler; ignored by\n // Snippet.\n DistDir string\n\n // Endpoint is the public SSE URL the client will connect to. Used only by\n // Snippet to populate the EventSource URL. Defaults to "/_bf/reload" when\n // empty.\n Endpoint string\n\n // Disabled, when true, makes NewReloadHandler return a 404 handler and\n // Snippet return an empty fragment. Intended for production builds.\n Disabled bool\n}\n\n// IsDevDefault reports whether the process is running in a development\n// environment using the common Go convention of APP_ENV=development.\n// Callers can use this to populate Config.Disabled:\n//\n// cfg := bfdev.Config{DistDir: "./dist", Disabled: !bfdev.IsDevDefault()}\nfunc IsDevDefault() bool {\n return os.Getenv("APP_ENV") == "development"\n}\n\n// NewReloadHandler returns an http.Handler that streams Server-Sent Events\n// and emits `event: reload` whenever `<DistDir>/.dev/build-id` changes. When\n// cfg.Disabled is true, the handler responds 404 and never opens a stream.\nfunc NewReloadHandler(cfg Config) http.Handler {\n if cfg.Disabled {\n return http.HandlerFunc(http.NotFound)\n }\n devDir := filepath.Join(cfg.DistDir, devSubdir)\n buildIDPath := filepath.Join(devDir, buildIDFile)\n // Ensure the directory exists so the first read does not race with the\n // initial build. Ignore the error: subsequent reads simply return "".\n _ = os.MkdirAll(devDir, 0o755)\n\n return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n flusher, ok := w.(http.Flusher)\n if !ok {\n http.Error(w, "streaming unsupported", http.StatusInternalServerError)\n return\n }\n h := w.Header()\n h.Set("Content-Type", "text/event-stream")\n h.Set("Cache-Control", "no-cache, no-transform")\n h.Set("Connection", "keep-alive")\n h.Set("X-Accel-Buffering", "no")\n\n send := func(chunk string) bool {\n if _, err := fmt.Fprint(w, chunk); err != nil {\n return false\n }\n flusher.Flush()\n return true\n }\n\n if !send("retry: 1000\\n\\n") {\n return\n }\n\n lastEventID := strings.TrimSpace(r.Header.Get("Last-Event-ID"))\n initialID := readBuildID(buildIDPath)\n lastSent := ""\n if initialID != "" {\n lastSent = initialID\n // When the client reconnects with a stale Last-Event-ID, a build\n // happened during its disconnected window \u2014 fire `reload`\n // immediately so the missed rebuild does not silently stay\n // unpainted until the next change.\n event := "hello"\n if lastEventID != "" && lastEventID != initialID {\n event = "reload"\n }\n if !send(fmt.Sprintf("event: %s\\nid: %s\\ndata: %s\\n\\n", event, initialID, initialID)) {\n return\n }\n }\n\n ctx := r.Context()\n hbTicker := time.NewTicker(heartbeatInterval)\n defer hbTicker.Stop()\n pollTicker := time.NewTicker(pollInterval)\n defer pollTicker.Stop()\n\n for {\n select {\n case <-ctx.Done():\n return\n case <-hbTicker.C:\n if !send(": hb\\n\\n") {\n return\n }\n case <-pollTicker.C:\n id := readBuildID(buildIDPath)\n if id == "" || id == lastSent {\n continue\n }\n lastSent = id\n if !send(fmt.Sprintf("event: reload\\nid: %s\\ndata: %s\\n\\n", id, id)) {\n return\n }\n }\n }\n })\n}\n\nfunc readBuildID(path string) string {\n b, err := os.ReadFile(path)\n if err != nil {\n return ""\n }\n return strings.TrimSpace(string(b))\n}\n\n// Snippet returns an inline <script> that subscribes to the SSE endpoint,\n// reloads on `reload`, and preserves window.scrollY across reloads via\n// sessionStorage. Returns an empty fragment when cfg.Disabled is true.\n//\n// Place it before </body>, typically just after the rendered scripts.\nfunc Snippet(cfg Config) template.HTML {\n if cfg.Disabled {\n return ""\n }\n endpoint := cfg.Endpoint\n if endpoint == "" {\n endpoint = "/_bf/reload"\n }\n // Small IIFE: EventSource subscriber + scrollY preservation. Idempotent\n // across duplicate mounts (guarded by window.__bfDevReload).\n js := fmt.Sprintf(\n `(function(){if(window.__bfDevReload)return;window.__bfDevReload=1;`+\n `try{var s=sessionStorage.getItem(%q);if(s){sessionStorage.removeItem(%q);`+\n `var y=parseInt(s,10);if(!isNaN(y)){var restore=function(){window.scrollTo(0,y)};`+\n `if(document.readyState===\'loading\'){addEventListener(\'DOMContentLoaded\',restore,{once:true})}else{restore()}}}}catch(e){}`+\n `var es=new EventSource(%q);`+\n `es.addEventListener(\'reload\',function(){try{sessionStorage.setItem(%q,String(window.scrollY))}catch(e){}location.reload()});`+\n `es.addEventListener(\'error\',function(){})})();`,\n scrollStorageKey, scrollStorageKey, endpoint, scrollStorageKey,\n )\n // Safe: `js` is assembled from package-internal literals plus `endpoint`\n // escaped by %q (Go-syntax quoting == valid JS string literal for the\n // ASCII endpoint paths this accepts).\n return template.HTML("<script>" + js + "</script>") //nolint:gosec\n}\n';
23254
- barefootPmSource = "package BarefootJS;\nour $VERSION = \"0.01\";\nuse strict;\nuse warnings;\nuse utf8;\nuse feature 'signatures';\nno warnings 'experimental::signatures';\n\nuse POSIX ();\nuse Scalar::Util qw(looks_like_number weaken);\n\n# NOTE: This runtime is template-engine-agnostic AND framework-agnostic by\n# design, so it can ship as a standalone CPAN distribution. It depends only on\n# core Perl (subroutine signatures + the hand-rolled minimal accessor base\n# below \u2014 no Mojo::Base, no Class::Tiny). Every operation that depends on *how*\n# a template is rendered \u2014 JSON marshalling, raw-string marking, JSX-children\n# materialisation, and named-template rendering \u2014 is delegated to a pluggable\n# `backend` (see BarefootJS::Backend::Mojo for the reference Mojolicious\n# implementation), which is the only component that pulls in the Mojo\n# distribution, and only when it is actually used.\n\n# ---------------------------------------------------------------------------\n# Minimal accessor base (no Mojo::Base / Class::Tiny dependency)\n# ---------------------------------------------------------------------------\n#\n# Generates read/write accessors with optional lazy defaults so the runtime\n# stays free of any non-core OO base. Semantics mirror the Mojo::Base `has`\n# this class used to inherit: a getter returns the stored value (building it\n# from the default on first access if unset); a setter stores the value and\n# returns $self for chaining. A default is either a plain scalar or a coderef\n# invoked as `$default->($self)` (for per-instance refs like `[]` / `{}` and\n# the lazily-required Mojo backend).\nmy %ATTR_DEFAULT = (\n _scripts => sub { [] },\n _script_seen => sub { {} },\n _child_renderers => sub { {} },\n _is_child => 0,\n # Lazily fall back to the Mojo reference backend so a bare-blessed\n # instance (the pure-function unit tests) and the historical\n # `BarefootJS->new($c, ...)` callers keep working unchanged. A non-Mojo\n # host injects its own backend via `BarefootJS->new($c, { backend => $b })`\n # and never triggers this require \u2014 keeping the core load Mojo-free.\n backend => sub {\n require BarefootJS::Backend::Mojo;\n return BarefootJS::Backend::Mojo->new;\n },\n);\n\n# c \u2014 Mojolicious controller (kept for back-compat accessors)\n# config \u2014 plugin / instance config\n# backend \u2014 the template-engine seam (#engine-abstraction)\n# _scope_id \u2014 addressable scope id\n# _bf_parent / _bf_mount \u2014 slot identity when this scope is slot-attached\n# _props \u2014 props serialised into bf-p / the scope comment\nfor my $attr (qw(\n c config backend\n _scripts _script_seen _scope_id _is_child _bf_parent _bf_mount _props\n _child_renderers\n)) {\n no strict 'refs';\n *{\"BarefootJS::$attr\"} = sub {\n my $self = shift;\n if (@_) { $self->{$attr} = shift; return $self; }\n if (!exists $self->{$attr} && exists $ATTR_DEFAULT{$attr}) {\n my $d = $ATTR_DEFAULT{$attr};\n $self->{$attr} = ref($d) eq 'CODE' ? $d->($self) : $d;\n }\n return $self->{$attr};\n };\n}\n\nsub new ($class, $c, $config = {}) {\n # Build (or accept an injected) rendering backend. The default Mojo\n # backend wraps the controller and honours an optional `json_encoder`\n # override so a host can swap in a faster XS JSON implementation\n # without subclassing. A caller targeting another template engine\n # passes its own backend via `$config->{backend}`.\n my $backend = $config->{backend};\n unless ($backend) {\n require BarefootJS::Backend::Mojo;\n $backend = BarefootJS::Backend::Mojo->new(\n c => $c,\n ($config->{json_encoder}\n ? (json_encoder => $config->{json_encoder})\n : ()),\n );\n }\n my $self = bless {\n c => $c,\n config => $config,\n backend => $backend,\n }, $class;\n # Hold the controller weakly. Mojolicious stashes this bf instance under\n # `$c->stash->{'bf.instance'}`, so a strong bf -> controller back-reference\n # closes a per-request cycle ($c -> stash -> bf -> $c) that Perl's\n # refcount GC cannot reclaim, leaking one controller + bf + child-renderer\n # closures per request. The controller owns (outlives) the per-request bf,\n # so the weak ref stays valid for the whole render. Callers that need the\n # controller to outlive the bf instance independently must keep their own\n # strong reference (the normal Mojo request scope already does).\n weaken($self->{c}) if defined $c;\n return $self;\n}\n\n# ---------------------------------------------------------------------------\n# Scope & Props\n# ---------------------------------------------------------------------------\n\nsub scope_attr ($self) {\n # bf-s is the addressable scope id only (#1249).\n return $self->_scope_id // '';\n}\n\n# Emits `bf-h=\"<host>\" bf-m=\"<slot>\" bf-r=\"\"` conditionally.\n# See spec/compiler.md \"Slot identity\".\nsub hydration_attrs ($self) {\n my @parts;\n my $host = $self->_bf_parent;\n my $mount = $self->_bf_mount;\n if (defined $host && length $host) {\n my $h = $host =~ s/\"/&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 # encode_json returns a character string (not bytes) for safe embedding\n # in templates (the Mojo backend uses Mojo::JSON::to_json).\n my $json = $self->backend->encode_json($props);\n return qq{ bf-p='$json'};\n}\n\n# ---------------------------------------------------------------------------\n# Comment Markers\n# ---------------------------------------------------------------------------\n\nsub comment ($self, $text) {\n return \"<!--bf-$text-->\";\n}\n\n# ---------------------------------------------------------------------------\n# JS-equivalent value stringification\n# ---------------------------------------------------------------------------\n\n# Map a Perl boolean-shaped value to the JS `String(bool)` form.\n# Used by the Mojo adapter when emitting reactive attribute bindings\n# whose JS source `isBooleanResultExpr` classified as boolean \u2014\n# a comparison (`count() > 0`), a logical negation (`!ok()`), or a\n# literal `true` / `false`. Perl's auto-stringification of those\n# expressions yields `''` / `1`; Hono and Go emit `'false'` / `'true'`.\n# Centralising the bool \u2192 string mapping here keeps the contract\n# testable and the template-emit syntax tidy\n# (`<%= bf->bool_str(...) %>` vs an inline ternary).\n#\n# Contract is boolean-only: callers must have classified the\n# expression as boolean-result before routing through this helper.\n# Non-boolean values reaching here will be Perl-truthy-coerced to\n# 'true' / 'false', which is generally wrong \u2014 non-boolean attribute\n# bindings stay on the plain `<%= expr %>` emit path and never reach\n# this function.\nsub bool_str ($self, $value) {\n return $value ? 'true' : 'false';\n}\n\nsub text_start ($self, $slot_id) {\n return \"<!--bf:$slot_id-->\";\n}\n\nsub text_end ($self) {\n return \"<!--/-->\";\n}\n\n# See spec/compiler.md \"Slot identity\" for the comment-scope wire format.\nsub scope_comment ($self) {\n my $scope_id = $self->_scope_id // '';\n my $host_segment = '';\n my $host = $self->_bf_parent;\n my $mount = $self->_bf_mount;\n if (defined $host && length $host) {\n $host_segment = \"|h=$host|m=\" . ($mount // '');\n }\n my $props_json = '';\n if ($self->_props && %{$self->_props}) {\n $props_json = '|' . $self->backend->encode_json($self->_props);\n }\n return \"<!--bf-scope:$scope_id$host_segment$props_json-->\";\n}\n\n# ---------------------------------------------------------------------------\n# Script Registration\n# ---------------------------------------------------------------------------\n\nsub register_script ($self, $path) {\n return if $self->_script_seen->{$path};\n $self->_script_seen->{$path} = 1;\n push @{$self->_scripts}, $path;\n}\n\n# ---------------------------------------------------------------------------\n# Child Component Rendering\n# ---------------------------------------------------------------------------\n# (`_child_renderers` accessor is generated by the minimal accessor base above.)\n\nsub register_child_renderer ($self, $name, $renderer) {\n $self->_child_renderers->{$name} = $renderer;\n}\n\nsub render_child ($self, $name, @args) {\n my $renderer = $self->_child_renderers->{$name};\n die \"No renderer registered for child component '$name'\" unless $renderer;\n # Accept both the Mojo list form \u2014 `bf->render_child($name, k => v, ...)`\n # \u2014 and the single-hashref form \u2014 `$bf.render_child($name, { k => v })`.\n # Template languages whose method calls can't splat a hash into positional\n # args (Text::Xslate Kolon, Template Toolkit) pass one hashref instead.\n my %props = (@args == 1 && ref $args[0] eq 'HASH') ? %{ $args[0] } : @args;\n # JSX children come in via the engine's children-capture mechanism\n # (Mojo's `begin %>...<% end`, which produces a CODE ref returning a\n # Mojo::ByteStream). Materialize it through the backend before handing\n # the props to the child renderer so the child template sees\n # `$children` as already-rendered HTML. Guard on `exists` so a\n # childless invocation (`bf->render_child('counter')`) doesn't gain a\n # spurious `children => undef` key \u2014 preserving the historical \"only\n # touch children when present\" behaviour.\n $props{children} = $self->backend->materialize($props{children})\n if exists $props{children};\n return $renderer->(\\%props);\n}\n\n# ---------------------------------------------------------------------------\n# Bulk registration from build manifest\n# ---------------------------------------------------------------------------\n#\n# `bf build` emits dist/templates/manifest.json describing every\n# component the page might invoke (Counter, ui/button/index, ...).\n# This helper walks that manifest and registers one child renderer per\n# UI registry entry \u2014 the path shape `ui/<name>/index` maps to the\n# `<name>` slot key Counter.html.ep and friends use via\n# `<%= bf->render_child('<name>', ...) %>`.\n#\n# Each manifest entry carries an `ssrDefaults` hash derived statically\n# from the component's JSX (prop destructure defaults + signal /\n# memo initial values, see packages/jsx/src/ssr-defaults.ts). The\n# child renderer seeds every template variable from that hash,\n# preferring the caller's matching prop where one exists. This\n# replaces the per-component `signal_init` callback that every\n# scaffold's `app.pl` used to hand-roll for items 1/3 of issue #1416.\n#\n# `signal_init` remains as an opt-in override for cases the static\n# extractor can't see through (e.g. signal initial values that\n# reference imported helpers). When supplied for a given slot key\n# it takes precedence over the manifest's `ssrDefaults` for that\n# child, allowing callers to mix manual overrides with auto-derived\n# defaults for siblings.\nsub register_components_from_manifest ($self, $manifest, %opts) {\n my $signal_inits = $opts{signal_init} // {};\n my $parent_scope = $self->_scope_id;\n # Weaken the parent capture so the child-renderer closures stored on\n # `$self->_child_renderers` don't keep `$self` alive (the direct\n # closure <-> parent cycle). The controller is reached through `$parent`\n # at call time rather than captured strongly here, so the closures hold\n # no strong reference to `$c` either \u2014 see the controller-cycle note in\n # `new`. `$parent` is always live whenever a closure runs (the closure is\n # stored on `$parent`, so `$parent` outlives every invocation).\n weaken(my $parent = $self);\n\n for my $entry_name (keys %$manifest) {\n # `__barefoot__` is the runtime entry, not a component.\n next if $entry_name eq '__barefoot__';\n # Only UI registry components (path shape `ui/<name>/index`)\n # become child renderers; top-level page components are the\n # render target rather than a child.\n next unless $entry_name =~ m{^ui/([^/]+)/index$};\n my $slot_key = $1;\n my $marked = $manifest->{$entry_name}{markedTemplate} // '';\n next unless $marked;\n # `templates/ui/button/index.html.ep` \u2192 `ui/button/index`\n my $template_name = $marked;\n $template_name =~ s{^templates/}{};\n $template_name =~ s{\\.html\\.ep$}{};\n\n my $signal_init = $signal_inits->{$slot_key};\n my $manifest_defaults = $manifest->{$entry_name}{ssrDefaults};\n $self->register_child_renderer($slot_key, sub {\n my ($props) = @_;\n # Child shares the parent's backend so nested renders go\n # through the same engine + controller (and inherit any\n # injected json_encoder). The controller is fetched via the weak\n # `$parent` at call time \u2014 never captured strongly \u2014 so the\n # closure adds no edge to the per-request reference cycle.\n my $child_bf = BarefootJS->new($parent->c, { backend => $parent->backend });\n my $slot_id = delete $props->{_bf_slot};\n $child_bf->_scope_id(\n $slot_id ? $parent_scope . '_' . $slot_id\n : $template_name . '_' . substr(rand() =~ s/^0\\.//r, 0, 6)\n );\n $child_bf->_is_child(1);\n # (#1249) Slot identity: host scope + slot id. Emitted as\n # bf-h / bf-m attributes by hydration_attrs.\n if ($slot_id) {\n $child_bf->_bf_parent($parent_scope);\n $child_bf->_bf_mount($slot_id);\n }\n $child_bf->_scripts($parent->_scripts);\n $child_bf->_script_seen($parent->_script_seen);\n\n my %extra;\n if ($signal_init) {\n %extra = $signal_init->($props);\n } elsif ($manifest_defaults) {\n %extra = _derive_stash_from_defaults($manifest_defaults, $props);\n }\n\n # Render the child template with $child_bf bound as the active\n # instance for the nested render. The backend owns the\n # engine-specific binding + restore (stash juggle for Mojo).\n my $html = $parent->backend->render_named(\n $template_name, $child_bf, { %$props, %extra },\n );\n chomp $html;\n return $html;\n });\n }\n}\n\n# Derive template-stash kvs from a manifest entry's `ssrDefaults`\n# section. Each entry shape:\n# { value => <static-fallback>, propName => <prop>, isRestProps => bool }\n# For `isRestProps`, the rest bag passes through unchanged (or the\n# static `{}` if the caller didn't supply one). For ordinary entries\n# the caller's `$props->{propName}` wins when defined, otherwise the\n# static `value` does. `propName`-less entries (signal / memo locals)\n# always use the static value \u2014 the caller cannot override them.\nsub _derive_stash_from_defaults ($defaults, $props) {\n my %extra;\n for my $name (keys %$defaults) {\n my $d = $defaults->{$name};\n if (ref($d) ne 'HASH') {\n $extra{$name} = $d;\n next;\n }\n if ($d->{isRestProps}) {\n $extra{$name} = exists $props->{$name} ? $props->{$name} : $d->{value};\n next;\n }\n my $prop_name = $d->{propName};\n if (defined $prop_name && exists $props->{$prop_name} && defined $props->{$prop_name}) {\n $extra{$name} = $props->{$prop_name};\n } else {\n $extra{$name} = $d->{value};\n }\n }\n return %extra;\n}\n\n# ---------------------------------------------------------------------------\n# Script Output\n# ---------------------------------------------------------------------------\n\nsub scripts ($self) {\n my @tags;\n for my $path (@{$self->_scripts}) {\n push @tags, qq{<script type=\"module\" src=\"$path\"></script>};\n }\n return join(\"\\n\", @tags);\n}\n\n# ---------------------------------------------------------------------------\n# Streaming SSR (Out-of-Order)\n# ---------------------------------------------------------------------------\n\nsub streaming_bootstrap ($self) {\n return q{<script>(function(){function s(id){var a=document.querySelector('[bf-async=\"'+id+'\"]');var t=document.querySelector('template[bf-async-resolve=\"'+id+'\"]');if(!a||!t)return;a.replaceChildren(t.content.cloneNode(true));a.removeAttribute('bf-async');t.remove();requestAnimationFrame(function(){if(window.__bf_hydrate)window.__bf_hydrate()})};window.__bf_swap=s})()</script>};\n}\n\nsub async_boundary ($self, $id, $fallback_html) {\n # The fallback comes in via Mojo `begin %>...<% end` capture (see\n # MojoAdapter::renderAsync), which produces a CODE ref returning a\n # Mojo::ByteStream. Materialize it through the backend so the rendered\n # HTML embeds in the placeholder rather than the CODE ref's\n # stringification.\n $fallback_html = $self->backend->materialize($fallback_html);\n return qq{<div bf-async=\"$id\">$fallback_html</div>};\n}\n\nsub async_resolve ($self, $id, $content_html) {\n return qq{<template bf-async-resolve=\"$id\">$content_html</template><script>__bf_swap(\"$id\")</script>};\n}\n\n# ---------------------------------------------------------------------------\n# JS-compat callees (#1189) \u2014 invoked from generated Mojo templates as\n# <%= bf->json($val) %>, <%= bf->floor($val) %>, etc. The MojoAdapter's\n# `templatePrimitives` registry emits these helper calls in place of the\n# corresponding JS callees (`JSON.stringify`, `Math.floor`, \u2026) so the SSR\n# template can render value-equivalent output without a JS engine.\n#\n# Failure policy mirrors the Go adapter (#1188): user-data marshalling\n# (json) bubbles errors so Mojolicious aborts loudly on cycles /\n# unsupported values rather than silently producing an empty payload.\n# Numeric coercion follows JS semantics (NaN propagates as the special\n# string 'NaN'; non-numeric input returns 'NaN' rather than 0). Strings\n# always coerce to a string representation.\n# ---------------------------------------------------------------------------\n\nsub json ($self, $value) {\n # Mojo::JSON::to_json returns a character string (not bytes), suitable\n # for embedding in HTML output via Mojo::ByteStream / `<%==`.\n #\n # Documented divergence from JS: JS distinguishes `null` (renders as\n # \"null\") from `undefined` (`JSON.stringify(undefined)` returns the\n # JS value `undefined`, not a string). Perl has no such distinction\n # \u2014 both map to `undef`. We choose the `null` rendering for SSR\n # ergonomics: an unset prop becomes the string \"null\" rather than\n # the literal text \"undefined\" or an empty attribute. Matches the\n # `null` case of JS exactly; diverges from the `undefined` case.\n return $self->backend->encode_json($value);\n}\n\nsub string ($self, $value) {\n # JS `String(v)` mirror. `undef` renders as the empty string here so\n # an unset prop doesn't surface as a literal \"undefined\" / \"null\"\n # in user-facing HTML \u2014 same divergence the Go adapter documents\n # for `bf_string`.\n return defined $value ? \"$value\" : '';\n}\n\nsub number ($self, $value) {\n # JS `Number(v)` mirror. Numeric coerces via Perl's implicit\n # numeric context; non-numeric / undef yield real numeric NaN\n # (`'nan' + 0`) so downstream arithmetic propagates correctly\n # (`Math.floor(NaN) === NaN`). Returning the literal string\n # \"NaN\" would conflate the user-passing-the-string-\"NaN\" case\n # with the parse-failure case, and break NaN detection in\n # downstream helpers.\n return 0 + 'nan' unless defined $value;\n return $value + 0 if looks_like_number($value);\n return 0 + 'nan';\n}\n\n# NaN is the only float for which `$x != $x` holds. Used as the\n# portable sentinel check in floor/ceil/round.\nsub _is_nan { my $n = shift; return $n != $n }\n\nsub floor ($self, $value) {\n my $n = $self->number($value);\n return $n if _is_nan($n);\n return POSIX::floor($n);\n}\n\nsub ceil ($self, $value) {\n my $n = $self->number($value);\n return $n if _is_nan($n);\n return POSIX::ceil($n);\n}\n\nsub round ($self, $value) {\n my $n = $self->number($value);\n return $n if _is_nan($n);\n # POSIX has no `round`. JS `Math.round` rounds half toward\n # +Infinity (so `Math.round(-1.5) === -1`, not -2). `floor(n\n # + 0.5)` reproduces that for both signs.\n return POSIX::floor($n + 0.5);\n}\n\n# ---------------------------------------------------------------------------\n# Array / String method helpers (#1448 Tier A)\n# ---------------------------------------------------------------------------\n#\n# `Array.prototype.includes(x)` and `String.prototype.includes(sub)`\n# share a method name in JS; the JSX parser can't tell the two\n# receiver shapes apart without TS type inference, so both lower to\n# the same IR node (`array-method` / method `includes`). This helper\n# dispatches at the Perl level via `ref()`:\n# - ARRAY ref: scan elements with `eq`; one defined-vs-undef\n# hop matches JS's `===` for null/undefined.\n# - scalar: `index($recv, $sub) != -1`, with both args\n# coerced through `// ''` so an undef receiver /\n# needle doesn't trip Perl's substr warning.\n# Anything else (HASH ref, code ref) returns false \u2014 matches the\n# JS semantic where `.includes` is only defined on Array /\n# TypedArray / String.\n\nsub includes ($self, $recv, $elem) {\n if (ref($recv) eq 'ARRAY') {\n for my $item (@$recv) {\n if (!defined $item) {\n return 1 if !defined $elem;\n next;\n }\n return 1 if defined $elem && $item eq $elem;\n }\n return 0;\n }\n return 0 if ref($recv);\n return index($recv // '', $elem // '') != -1 ? 1 : 0;\n}\n\n# `Array.prototype.indexOf(x)` / `Array.prototype.lastIndexOf(x)`\n# value-equality search (#1448 Tier A). Returns the 0-based position\n# of the first / last matching element, or -1 if not found.\n# Non-array receivers return -1 \u2014 matches the JS semantic that\n# `.indexOf` / `.lastIndexOf` are only defined on Array / TypedArray.\n# (The string-position `indexOf` form isn't in Tier A; if it lands\n# later the helper can grow a ref()-dispatch branch like `includes`.)\n\nsub _array_index_of ($recv, $elem, $reverse) {\n return -1 unless ref($recv) eq 'ARRAY';\n my @indices = $reverse ? (reverse 0 .. $#{$recv}) : (0 .. $#{$recv});\n for my $i (@indices) {\n my $item = $recv->[$i];\n if (!defined $item) {\n return $i if !defined $elem;\n next;\n }\n return $i if defined $elem && $item eq $elem;\n }\n return -1;\n}\n\nsub index_of ($self, $recv, $elem) {\n return _array_index_of($recv, $elem, 0);\n}\n\nsub last_index_of ($self, $recv, $elem) {\n return _array_index_of($recv, $elem, 1);\n}\n\n# `Array.prototype.at(i)` \u2014 supports negative indices (`.at(-1)` is\n# the last element); out-of-bounds returns undef (which Mojo's\n# auto-escape renders as the empty string, matching JS's `undefined`).\n# Non-array receivers return undef. Matches the Go `bf_at` arithmetic\n# (`length + i` for i < 0) so adapter output stays symmetric.\n\nsub at ($self, $recv, $i) {\n return undef unless ref($recv) eq 'ARRAY';\n return undef if !defined $i;\n my $len = scalar @$recv;\n return undef if $len == 0;\n my $idx = $i < 0 ? $len + $i : $i;\n return undef if $idx < 0 || $idx >= $len;\n return $recv->[$idx];\n}\n\n# `Array.prototype.concat(other)` \u2014 merges two arrays in order\n# into a new ARRAY ref. Non-array operands collapse to empty\n# (matches the Go `bf_concat` semantic so cross-adapter output\n# stays symmetric; differs from JS where a non-Array argument\n# with `Symbol.isConcatSpreadable` would be spread, a behaviour\n# the template-language path never observes).\n\nsub concat ($self, $a, $b) {\n my @out;\n push @out, @$a if ref($a) eq 'ARRAY';\n push @out, @$b if ref($b) eq 'ARRAY';\n return \\@out;\n}\n\n# `Array.prototype.slice(start, end?)` \u2014 carves out a sub-range\n# into a new ARRAY ref. Mirrors the Go `bf_slice` arithmetic so\n# adapter output stays symmetric:\n# - start < 0 \u2192 length + start (e.g. -1 = last index)\n# - end < 0 \u2192 length + end\n# - start < 0 after clamp \u2192 0\n# - end > length \u2192 length\n# - start >= end \u2192 empty\n# - end undef \u2192 \"to length\"\n# Non-array receivers return an empty ARRAY ref.\n\nsub slice ($self, $recv, $start, $end) {\n return [] unless ref($recv) eq 'ARRAY';\n my $len = scalar @$recv;\n return [] if $len == 0;\n\n my $s = $start // 0;\n $s = $len + $s if $s < 0;\n $s = 0 if $s < 0;\n $s = $len if $s > $len;\n\n my $e = defined $end ? $end : $len;\n $e = $len + $e if $e < 0;\n $e = 0 if $e < 0;\n $e = $len if $e > $len;\n\n return [] if $s >= $e;\n return [ @{$recv}[$s .. $e - 1] ];\n}\n\n# `Array.prototype.reverse()` / `Array.prototype.toReversed()` \u2014\n# both shapes share this lowering. SSR templates render a snapshot\n# of state, so JS's mutate-receiver (`reverse`) vs\n# return-new-array (`toReversed`) distinction has no template-\n# level meaning. Always returns a new ARRAY ref to keep callers\n# safe from accidental aliasing. Non-array receivers return an\n# empty ARRAY ref.\n\nsub reverse ($self, $recv) {\n return [] unless ref($recv) eq 'ARRAY';\n return [ reverse @$recv ];\n}\n\n# `Array.prototype.flat(depth?)` (#1448 Tier C) \u2014 flatten nested ARRAY\n# refs `$depth` levels deep. A `$depth` of -1 is the `Infinity` sentinel\n# (flatten fully); 0 returns a shallow copy. Non-ARRAY elements are kept\n# as-is (JS only flattens nested arrays). Non-ARRAY receiver \u2192 [].\nsub flat ($self, $recv, $depth = 1) {\n return [] unless ref($recv) eq 'ARRAY';\n my @out;\n for my $el (@$recv) {\n if ($depth != 0 && ref($el) eq 'ARRAY') {\n my $next = $depth > 0 ? $depth - 1 : $depth;\n push @out, @{ $self->flat($el, $next) };\n }\n else {\n push @out, $el;\n }\n }\n return \\@out;\n}\n\n# `Array.prototype.flatMap(fn)` value-returning field projection\n# (#1448 Tier C) \u2014 map each element through a self / field projection,\n# then flatten one level. `field` reads a HASH-ref key (the raw JS prop\n# name, as `bf->reduce` does); a projected non-ARRAY value is kept as-is\n# (flatMap = map + flat(1)). Non-ARRAY receiver \u2192 [].\nsub flat_map ($self, $recv, $key_kind, $key) {\n return [] unless ref($recv) eq 'ARRAY';\n my @projected;\n for my $el (@$recv) {\n if ($key_kind eq 'field') {\n # JS `i => i.field` on a non-object yields `undefined`, not the\n # element itself \u2014 push `undef` so a scalar element doesn't leak\n # into the output (matches Go's `getFieldValue` returning nil).\n push @projected, ref($el) eq 'HASH' ? $el->{$key} : undef;\n }\n else {\n push @projected, $el;\n }\n }\n return $self->flat(\\@projected, 1);\n}\n\n# `Array.prototype.flatMap(i => [i.a, i.b])` \u2014 array-literal tuple\n# projection (#1448 Tier C). Each `@specs` entry is a [kind, key] arrayref\n# (['self', ''] or ['field', 'a']). For each element, every leaf's value\n# is appended in order. flat(1) removes only the literal wrapper, so an\n# array-valued leaf is appended verbatim (no spread) \u2014 i.e. just append\n# each leaf. A non-HASH element under a `field` leaf yields undef (JS\n# `i.field` on a non-object). Non-ARRAY receiver \u2192 [].\nsub flat_map_tuple ($self, $recv, @specs) {\n return [] unless ref($recv) eq 'ARRAY';\n my @out;\n for my $el (@$recv) {\n for my $spec (@specs) {\n my ($kind, $key) = @$spec;\n if ($kind eq 'field') {\n push @out, ref($el) eq 'HASH' ? $el->{$key} : undef;\n }\n else {\n push @out, $el;\n }\n }\n }\n return \\@out;\n}\n\n# `String.prototype.trim()` \u2014 strip leading + trailing whitespace.\n# JS's `String.prototype.trim` matches `\\s` in the Unicode sense\n# (any whitespace including non-breaking space U+00A0); Perl's `\\s`\n# inside a regex with `/u` flag is the same. Undef receivers return\n# the empty string (matches JS's `String(undefined).trim()` which\n# would be \"undefined\" \u2192 \"undefined\", but in our template context\n# undef commonly means \"missing prop\"; rendering the empty string\n# is the safer choice and mirrors the JS-compat divergence we\n# already document for `bf->string(undef) === \"\"`).\n\nsub trim ($self, $recv) {\n return '' unless defined $recv;\n return '' if ref($recv);\n my $s = \"$recv\";\n $s =~ s/^\\s+|\\s+$//gu;\n return $s;\n}\n\n# `String.prototype.split(sep)` (#1448 Tier B) \u2014 string \u2192 ARRAY ref.\n#\n# Two JS-parity wrinkles drive the helper (a bare `split` emit would\n# diverge from both JS and Go):\n#\n# * Perl's `split` treats its first argument as a *regex*, so a\n# separator like '.' or '|' would match far too much. We\n# `quotemeta` it to force literal-string matching, mirroring JS's\n# string-separator semantics (the regex-separator form stays\n# refused upstream \u2014 see the parser arm).\n# * Perl's `split` drops trailing empty fields by default; JS keeps\n# them (`\"a,\".split(\",\")` is `[\"a\", \"\"]`). Passing the `-1` limit\n# preserves them, matching JS and Go's `strings.Split`.\n#\n# An empty separator splits into individual characters (JS + Go agree).\n# Undef receiver renders as the single-element `['']` \u2014 the same\n# \"missing prop \u2192 empty string\" convention `bf->trim` uses.\n\nsub split ($self, $recv, $sep = undef, $limit = undef) {\n my $s = defined $recv && !ref($recv) ? \"$recv\" : '';\n\n my @parts;\n if (!defined $sep) {\n # No separator \u2192 the whole string in a single-element array\n # (matches JS `\"x\".split()` / `.split(undefined)`).\n @parts = ($s);\n }\n elsif (\"$sep\" eq '') {\n # Empty separator \u2192 individual characters. No `-1` limit here:\n # on an empty pattern Perl's `split` with `-1` appends a spurious\n # trailing empty field (\"abc\" \u2192 'a','b','c',''), which JS/Go don't.\n @parts = split //, $s;\n }\n elsif ($s eq '') {\n # Empty input with a non-empty separator: JS `\"\".split(\",\")` is\n # `[\"\"]` and Go's `strings.Split(\"\", \",\")` is `[\"\"]`, but Perl's\n # `split /,/, ''` returns the empty list \u2014 special-case for parity.\n @parts = ('');\n }\n else {\n # `quotemeta` forces literal-string matching (JS string-separator\n # semantics); the `-1` keeps trailing empty fields (JS keeps them,\n # Perl's bare `split` drops them).\n my $q = quotemeta(\"$sep\");\n @parts = split /$q/, $s, -1;\n }\n\n # Optional `limit` caps the number of pieces (JS `split(sep, limit)`).\n # 0 \u2192 empty; a negative limit keeps all (JS ToUint32 wrap makes it\n # effectively unbounded) \u2014 both match Go's `bf_split`.\n if (defined $limit) {\n my $n = int($limit);\n if ($n == 0) { @parts = () }\n elsif ($n > 0 && $n < scalar @parts) { @parts = @parts[0 .. $n - 1] }\n }\n\n return [@parts];\n}\n\n# `String.prototype.startsWith(prefix, position?)` (#1448 Tier B) \u2014\n# string \u2192 boolean (1 / 0). `substr`-anchored literal comparison mirrors\n# Go's `strings.HasPrefix`. An empty prefix is always true (JS parity);\n# undef / non-string receivers coerce to the empty string first. The\n# optional `position` re-anchors the test (clamped to `[0, length]`),\n# matching JS `\"abc\".startsWith(\"b\", 1)`.\n\nsub starts_with ($self, $recv, $prefix, $position = undef) {\n my $s = defined $recv && !ref($recv) ? \"$recv\" : '';\n my $p = defined $prefix ? \"$prefix\" : '';\n if (defined $position) {\n my $n = int($position);\n $n = 0 if $n < 0;\n $n = length($s) if $n > length($s);\n $s = substr($s, $n);\n }\n return substr($s, 0, length $p) eq $p ? 1 : 0;\n}\n\n# `String.prototype.endsWith(suffix, endPosition?)` (#1448 Tier B) \u2014\n# string \u2192 boolean (1 / 0). Mirrors Go's `strings.HasSuffix`. An empty\n# suffix is always true (JS parity); a suffix longer than the string is\n# false. `substr($s, -length $x)` would mis-read the whole string when\n# `length $x == 0`, so that case short-circuits. The optional\n# `endPosition` treats the string as if it were only that many chars\n# long (clamped to `[0, length]`), matching JS `\"abc\".endsWith(\"b\", 2)`.\n\nsub ends_with ($self, $recv, $suffix, $end_position = undef) {\n my $s = defined $recv && !ref($recv) ? \"$recv\" : '';\n my $x = defined $suffix ? \"$suffix\" : '';\n if (defined $end_position) {\n my $e = int($end_position);\n $e = 0 if $e < 0;\n $e = length($s) if $e > length($s);\n $s = substr($s, 0, $e);\n }\n return 1 if $x eq '';\n return 0 if length($s) < length($x);\n return substr($s, -length $x) eq $x ? 1 : 0;\n}\n\n# `String.prototype.replace(pattern, replacement)` \u2014 string-pattern\n# form only (#1448 Tier B), replacing the FIRST occurrence (JS string-\n# pattern semantics). Spliced via index/substr rather than `s///` so\n# BOTH the pattern and the replacement are literal: no Perl regex\n# metacharacters in the pattern and no `$1` / `$&` interpolation in the\n# replacement. Go's `bf_replace` (strings.Replace, n=1) treats the\n# replacement literally too, so the two adapters stay byte-equal \u2014 this\n# diverges from JS only for replacement strings containing `$`-patterns\n# (rare in template position). An empty pattern inserts the replacement\n# at the front (`\"abc\".replace(\"\", \"X\")` \u2192 \"Xabc\"), matching JS + Go.\n\nsub replace ($self, $recv, $pattern, $replacement) {\n my $s = defined $recv && !ref($recv) ? \"$recv\" : '';\n my $o = defined $pattern ? \"$pattern\" : '';\n my $n = defined $replacement ? \"$replacement\" : '';\n return $n . $s if $o eq '';\n my $i = index($s, $o);\n return $s if $i < 0;\n return substr($s, 0, $i) . $n . substr($s, $i + length($o));\n}\n\n# `String.prototype.repeat(n)` \u2014 the receiver concatenated n times\n# (#1448 Tier B), via Perl's `x` operator. JS throws RangeError for a\n# negative count, but SSR templates degrade to the empty string rather\n# than dying mid-render, so a count <= 0 returns \"\" (Go's `bf_repeat`\n# applies the same clamp). The count is truncated toward zero\n# (`int`), matching JS's ToIntegerOrInfinity on `\"a\".repeat(3.7)`.\n\nsub repeat ($self, $recv, $count) {\n my $s = defined $recv && !ref($recv) ? \"$recv\" : '';\n my $n = defined $count ? int($count) : 0;\n return $n <= 0 ? '' : $s x $n;\n}\n\n# `String.prototype.padStart` / `padEnd` (#1448 Tier B) \u2014 pad the\n# receiver to `$target` characters with `$pad` (default a single space)\n# repeated and truncated to fill, prepended or appended. Length is\n# measured in characters (Perl `length`), matching Go's rune-based\n# `bf_pad_*` \u2014 diverges from JS's UTF-16-unit length only for\n# astral-plane input. An empty pad, or a receiver already >= `$target`,\n# returns the receiver unchanged (JS parity). The `$target` is\n# truncated toward zero (JS ToLength on the first arg).\n\nsub _pad ($s, $target, $pad, $at_start) {\n $pad = ' ' unless defined $pad;\n $pad = \"$pad\";\n return $s if $pad eq '';\n my $len = length $s;\n my $t = int($target // 0);\n return $s if $len >= $t;\n my $need = $t - $len;\n # Repeat enough copies to cover $need, then trim to exactly $need.\n my $fill = substr($pad x (int($need / length($pad)) + 1), 0, $need);\n return $at_start ? $fill . $s : $s . $fill;\n}\n\nsub pad_start ($self, $recv, $target, $pad = undef) {\n my $s = defined $recv && !ref($recv) ? \"$recv\" : '';\n return _pad($s, $target, $pad, 1);\n}\n\nsub pad_end ($self, $recv, $target, $pad = undef) {\n my $s = defined $recv && !ref($recv) ? \"$recv\" : '';\n return _pad($s, $target, $pad, 0);\n}\n\n# `Array.prototype.sort(cmp)` / `Array.prototype.toSorted(cmp)`\n# lowering (#1448 Tier B). Non-mutating \u2014 JS's mutate-vs-new\n# distinction is moot in SSR template context.\n#\n# Opts hash-ref. The compiler emits a `keys` list of per-key hashes\n# in priority order; each hash carries:\n#\n# key_kind => 'self' | 'field'\n# key => '' when key_kind eq 'self'; field name verbatim\n# from the comparator AST (e.g. 'price', 'createdAt')\n# when key_kind eq 'field' \u2014 no case normalisation\n# applied. Perl hash lookups are case-sensitive so\n# the key here must match the actual hash key the\n# user populated.\n# compare_type => 'numeric' | 'string' | 'auto'\n# direction => 'asc' | 'desc'\n#\n# Accepted comparator catalogue (gated upstream at parse time \u2014\n# anything outside refuses with BF101 before reaching this helper):\n#\n# (a,b) => a.f - b.f \u2192 field, numeric\n# (a,b) => a - b \u2192 self, numeric\n# (a,b) => a[.f].localeCompare(b[.f]) \u2192 field|self, string\n# (a,b) => a.f > b.f ? 1 : -1 \u2192 field|self, auto\n# any of the above ||-chained \u2192 multi-key tie-breaks\n# (and reversed-operand variants for `desc`).\n#\n# `auto` (relational-ternary lowering) compares numerically when both\n# keys `looks_like_number`, else lexically \u2014 Go's `bf_sort` applies the\n# same rule so the two template adapters stay byte-equal.\n#\n# A future `nulls => 'first' | 'last'` knob can land per key without\n# churn \u2014 the opts hash is the right place to grow.\n\nsub sort ($self, $recv, $opts = {}) {\n return [] unless ref($recv) eq 'ARRAY';\n\n # Normalise the per-key specs (priority order, length >= 1).\n my @spec = map {\n {\n key_kind => $_->{key_kind} // 'self',\n key => $_->{key} // '',\n compare_type => $_->{compare_type} // 'numeric',\n direction => $_->{direction} // 'asc',\n }\n } @{ $opts->{keys} // [] };\n return [ @$recv ] unless @spec;\n\n # Schwartzian transform: project each item to all its sort keys\n # once, then compare projected keys. Cheaper than re-resolving the\n # field accessors inside every comparison for non-trivial arrays.\n my @keyed = map {\n my $item = $_;\n my @ks = map {\n $_->{key_kind} eq 'field' && ref($item) eq 'HASH' ? $item->{ $_->{key} } : $item;\n } @spec;\n [ \\@ks, $item ];\n } @$recv;\n\n my $cmp = sub {\n for my $i (0 .. $#spec) {\n my $sp = $spec[$i];\n my $c = _compare_sort_key($a->[0][$i], $b->[0][$i], $sp->{compare_type});\n next if $c == 0; # tie on this key \u2014 try the next\n return $sp->{direction} eq 'desc' ? -$c : $c;\n }\n return 0;\n };\n\n my @sorted = sort $cmp @keyed;\n return [ map { $_->[1] } @sorted ];\n}\n\n# Compare two projected keys, ascending orientation (-1 / 0 / 1); the\n# caller negates for 'desc'. 'auto' compares numerically when both\n# keys look like numbers, else lexically (matches Go's `bf_sort`).\n# undef coalesces to '' / 0 so the order stays total without warnings.\nsub _compare_sort_key ($av, $bv, $compare_type) {\n if ($compare_type eq 'string') {\n return ($av // '') cmp ($bv // '');\n }\n if ($compare_type eq 'auto') {\n if (looks_like_number($av // '') && looks_like_number($bv // '')) {\n return ($av // 0) <=> ($bv // 0);\n }\n return ($av // '') cmp ($bv // '');\n }\n return ($av // 0) <=> ($bv // 0); # numeric\n}\n\n# Fold an array into a scalar via the arithmetic-fold catalogue\n# (#1448 Tier C). Mirrors Go's `bf_reduce` and JS `reduce(fn, init)` /\n# `reduceRight(fn, init)` for the shapes `(acc, x) => acc <op> x` /\n# `(acc, x) => acc <op> x.field`:\n#\n# bf->reduce($recv, {\n# op => '+' | '*',\n# key_kind => 'self' | 'field',\n# key => '<field>', # when key_kind eq 'field'\n# type => 'numeric' | 'string',\n# init => <seed>, # number, or string for concat\n# direction => 'left' | 'right', # 'right' = reduceRight (default 'left')\n# })\n#\n# Numeric folds accumulate with `+` / `*` (non-numeric keys coalesce to\n# 0); string folds concatenate via `bf->string` (undef \u2192 ''). The init\n# seeds the accumulator, so an empty array returns it unchanged \u2014 exactly\n# like JS. `direction => 'right'` folds right-to-left (reduceRight); only\n# observable for string concat, since numeric sum / product commute.\n# Float stringification can diverge from Go's for inexact binary\n# fractions (e.g. 0.1 + 0.2); integer sums \u2014 the common case \u2014 agree.\nsub reduce ($self, $recv, $opts = {}) {\n my $op = $opts->{op} // '+';\n my $key_kind = $opts->{key_kind} // 'self';\n my $key = $opts->{key} // '';\n my $type = $opts->{type} // 'numeric';\n my $direction = $opts->{direction} // 'left';\n\n my @items = ref($recv) eq 'ARRAY' ? @$recv : ();\n # reduceRight folds right-to-left; reversing the snapshot keeps the\n # single forward loop below. Only observable for string concat \u2014\n # numeric sum / product commute. Qualify as CORE::reverse \u2014 this\n # package defines `sub reverse` (the `.reverse()` helper), so a bare\n # `reverse` is ambiguous under `use warnings`.\n @items = CORE::reverse(@items) if $direction eq 'right';\n my $project = sub ($item) {\n $key_kind eq 'field' && ref($item) eq 'HASH' ? $item->{$key} : $item;\n };\n\n if ($type eq 'string') {\n my $acc = $opts->{init} // '';\n $acc .= $self->string($project->($_)) for @items;\n return $acc;\n }\n\n my $acc = $opts->{init} // 0;\n for my $item (@items) {\n my $n = $project->($item);\n # Guard `defined` before `looks_like_number` so a missing field\n # (undef) folds as 0 without an \"uninitialized value\" warning\n # under `use warnings` \u2014 matching the `$av // ''` style `sort` uses.\n $n = 0 unless defined $n && looks_like_number($n);\n $op eq '*' ? ($acc *= $n) : ($acc += $n);\n }\n return $acc;\n}\n\n# ---------------------------------------------------------------------------\n# JSX intrinsic-element spread (#1407)\n# ---------------------------------------------------------------------------\n#\n# Mirrors the JS `spreadAttrs` runtime\n# (`packages/client/src/runtime/spread-attrs.ts`) and the Go adapter's\n# `bf.SpreadAttrs` so SSR output stays byte-equal across the three\n# adapters. Generated Mojo templates invoke this as\n# `<%== bf->spread_attrs($bag) %>`.\n#\n# Skip rules: nil/false values, event handlers (`on[A-Z]\u2026` shape\n# matching JS `key[2] === key[2].toUpperCase()` \u2014 true for any\n# character whose uppercase is itself, including digits and\n# underscore), `children`. `ref` is intentionally NOT filtered,\n# matching the JS reference.\n#\n# Key remap: className \u2192 class, htmlFor \u2192 for; SVG camelCase\n# attrs preserved (case-sensitive XML spec); other camelCase keys\n# lowered to kebab-case with a leading `-` for an initial\n# uppercase letter (mirrors JS `key.replace(/([A-Z])/g, '-$1')`).\n#\n# `style` is routed through `_style_to_css` so object literals\n# serialise to a real CSS string instead of Perl's default\n# `HASH(0x...)` form.\n#\n# Output is deterministic: keys are sorted alphabetically before\n# emission, matching the Go adapter's `sort.Strings(keys)` policy\n# and Mojo::JSON's marshal order.\n#\n# The return value is a Mojo::ByteStream so the calling template's\n# `<%==` raw-emit skips re-escaping (the helper has already\n# HTML-escaped each value).\n\nmy %SVG_CAMEL_CASE_ATTRS = map { $_ => 1 } qw(\n allowReorder attributeName attributeType autoReverse\n baseFrequency baseProfile calcMode clipPathUnits\n contentScriptType contentStyleType diffuseConstant edgeMode\n externalResourcesRequired filterRes filterUnits glyphRef\n gradientTransform gradientUnits kernelMatrix kernelUnitLength\n keyPoints keySplines keyTimes lengthAdjust limitingConeAngle\n markerHeight markerUnits markerWidth maskContentUnits\n maskUnits numOctaves pathLength patternContentUnits\n patternTransform patternUnits pointsAtX pointsAtY pointsAtZ\n preserveAlpha preserveAspectRatio primitiveUnits refX refY\n repeatCount repeatDur requiredExtensions requiredFeatures\n specularConstant specularExponent spreadMethod startOffset\n stdDeviation stitchTiles surfaceScale systemLanguage\n tableValues targetX targetY textLength viewBox viewTarget\n xChannelSelector yChannelSelector zoomAndPan\n);\n\nsub _to_attr_name ($key) {\n return 'class' if $key eq 'className';\n return 'for' if $key eq 'htmlFor';\n return $key if $SVG_CAMEL_CASE_ATTRS{$key};\n # camelCase \u2192 kebab-case, with a leading `-` for an initial\n # uppercase letter (JS-reference parity, even though that case\n # produces an HTML-invalid attribute name \u2014 same documented\n # behaviour as the Go adapter's `toAttrName`).\n my $out = $key;\n $out =~ s/([A-Z])/-\\L$1/g;\n return $out;\n}\n\nsub _html_escape ($value) {\n # HTML attribute-value escape for SSR string emission. The\n # spread bag's values reach the browser as part of a generated\n # `key=\"...\"` substring inside the rendered HTML, so the\n # escape set has to cover everything that could break either\n # the surrounding double-quoted attribute or the enclosing\n # tag: `&`, `<`, `>`, `\"`, and `'`. Matches Go's\n # `template.HTMLEscapeString` semantics byte-for-byte (using\n # `&#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 # Mark the result raw so the calling template's `<%==` raw-emit\n # doesn't re-escape the already-escaped values (the Mojo backend\n # returns a Mojo::ByteStream).\n return $self->backend->mark_raw(join(' ', @parts));\n}\n\n1;\n__END__\n\n=encoding utf8\n\n=head1 NAME\n\nBarefootJS - Engine- and framework-agnostic server runtime for BarefootJS marked templates\n\n=head1 SYNOPSIS\n\n use BarefootJS;\n\n # A host injects a rendering backend (see BarefootJS::Backend::Xslate or\n # Mojolicious::Plugin::BarefootJS for shipping backends).\n my $bf = BarefootJS->new($context, { backend => $backend });\n\n # The compiled marked template calls the runtime as a `bf` object:\n # <: $bf.scope_attr() :> <: $bf.json($data) :> <: $bf.spread_attrs($h) :>\n\n=head1 DESCRIPTION\n\nBarefootJS compiles JSX/TSX into a marked template plus client JS. This module\nis the server-side runtime the marked templates call into at render time. It is\ndeliberately template-engine- and web-framework-agnostic: every operation that\ndepends on I<how> a template is rendered \u2014 JSON marshalling, raw-string marking,\nJSX-children materialisation, and named-template rendering \u2014 is delegated to a\npluggable C<backend>.\n\nThat design lets the one runtime drive any backend. Shipping backends:\n\n=over 4\n\n=item * L<BarefootJS::Backend::Xslate> \u2014 Text::Xslate (Kolon); runs under any PSGI/Plack app.\n\n=item * L<BarefootJS::Backend::Mojo> \u2014 Mojolicious (via L<Mojolicious::Plugin::BarefootJS>).\n\n=back\n\nThe core itself pulls in only core Perl modules (C<POSIX>, C<Scalar::Util>);\nno template engine or web framework is loaded unless a backend that needs one\nis used.\n\n=head1 SEE ALSO\n\nL<BarefootJS::Backend::Xslate>, L<Mojolicious::Plugin::BarefootJS>,\nL<https://github.com/piconic-ai/barefootjs>\n\n=head1 AUTHOR\n\nkobaken E<lt>kentafly88@gmail.comE<gt>\n\n=head1 LICENSE\n\nCopyright (c) 2025-present BarefootJS Contributors.\n\nThis library is free software; you can redistribute it and/or modify it under\nthe MIT License. See the F<LICENSE> file in the distribution for the full text.\n\n=cut\n";
23255
- barefootBackendMojoPmSource = "package BarefootJS::Backend::Mojo;\nour $VERSION = \"0.01\";\nuse Mojo::Base -base, -signatures;\n\nuse Mojo::ByteStream qw(b);\nuse Mojo::JSON qw(to_json);\nuse Scalar::Util qw(weaken);\n\n# ---------------------------------------------------------------------------\n# Reference rendering backend (Mojolicious / Mojo::Template).\n# ---------------------------------------------------------------------------\n#\n# BarefootJS.pm holds all the template-engine-agnostic logic (the JS-compat\n# value helpers, array/string methods, hydration markers). Everything that is\n# specific to *how a template is rendered* \u2014 JSON marshalling, raw-string\n# marking, JSX-children materialisation, and named-template rendering \u2014 lives\n# behind this backend object so the same runtime can drive a different Perl\n# template engine (Text::Xslate, Template Toolkit, \u2026) without rewriting the\n# helper surface.\n#\n# A backend MUST implement:\n# - encode_json($data) -> string\n# - mark_raw($str) -> value the engine emits without escaping\n# - materialize($value) -> string (resolve a captured-children ref)\n# - render_named($name, $bf, \\%vars) -> string\n#\n# This Mojo implementation is the reference. To target another engine, write a\n# sibling backend (BarefootJS::Backend::Xslate, \u2026) implementing the same four\n# methods and pass it via `BarefootJS->new($c, { backend => $b })`.\n\n# The Mojolicious controller. Optional: the value-marshalling helpers\n# (`encode_json` / `mark_raw` / `materialize`) work without it; only\n# `render_named` reaches into the controller's renderer + stash.\nhas 'c';\n\n# Pluggable JSON encoder (#engine-abstraction). Defaults to\n# `Mojo::JSON::to_json`, which returns a *character* string (not bytes)\n# suitable for embedding in HTML output via `<%==` / Mojo::ByteStream.\n#\n# Override with any `sub ($data) { ... }` to swap in a faster XS encoder \u2014\n# e.g. `json_encoder => sub { Cpanel::JSON::XS->new->canonical->encode($_[0]) }`.\n# The pure-Perl JSON::PP fallback Mojo::JSON uses can be a hot spot for large\n# props payloads; the seam lets a host pick its own implementation without\n# touching the runtime.\nhas 'json_encoder' => sub { \\&to_json };\n\n# Hold the controller weakly for the same reason BarefootJS does: the\n# controller owns the bf instance (which owns this backend) via its stash,\n# so a strong back-reference would close a per-request cycle the refcount GC\n# can't reclaim. `render_named` only touches `$self->c` mid-render, while the\n# controller is still alive on the request stack.\nsub new ($class, %args) {\n my $self = $class->SUPER::new(%args);\n weaken($self->{c}) if $self->{c};\n return $self;\n}\n\nsub encode_json ($self, $data) {\n return $self->json_encoder->($data);\n}\n\n# Mark a string as already-safe so the template engine emits it verbatim\n# (no re-escaping). In Mojo this is a Mojo::ByteStream, which the calling\n# template's `<%==` raw-emit passes through unescaped.\nsub mark_raw ($self, $str) {\n return b($str);\n}\n\n# JSX children / async fallbacks arrive via Mojo's `begin %>...<% end`\n# capture, which produces a CODE ref returning a Mojo::ByteStream. Resolve\n# it to a string before embedding. Plain (already-rendered) strings pass\n# through unchanged.\nsub materialize ($self, $value) {\n return ref($value) eq 'CODE' ? $value->() : $value;\n}\n\n# Render a named template with `$child_bf` bound as the active runtime\n# instance for that render. The Mojo `bf` helper resolves the current\n# instance off `$c->stash->{'bf.instance'}`; swap it for the duration of\n# the nested render and restore it afterwards so sibling renders are\n# unaffected.\nsub render_named ($self, $template_name, $child_bf, $vars) {\n my $c = $self->c;\n my $prev = $c->stash->{'bf.instance'};\n $c->stash->{'bf.instance'} = $child_bf;\n my $html = $c->render_to_string(template => $template_name, %$vars);\n $c->stash->{'bf.instance'} = $prev;\n return $html;\n}\n\n1;\n";
23256
- barefootPluginPmSource = "package Mojolicious::Plugin::BarefootJS;\nour $VERSION = \"0.01\";\nuse Mojo::Base 'Mojolicious::Plugin', -signatures;\n\nuse Mojo::File qw(path);\nuse Mojo::JSON qw(decode_json);\n\nuse BarefootJS;\n\n# Plugin entry point. Wires up:\n#\n# 1. The `bf` controller helper. Lazily instantiates one\n# BarefootJS object per request and stashes it under\n# `bf.instance`.\n#\n# 2. A `before_render` hook that, when the rendered template name\n# matches a top-level component in the build manifest, fills the\n# heavy boilerplate the user previously hand-rolled in `app.pl`:\n# generates the scope id, registers every UI-registry child\n# renderer from the manifest, and seeds the stash with each\n# template variable's static default (issue #1416).\n#\n# Configuration (all optional):\n# - manifest_path: absolute path to the `bf build`-emitted\n# `manifest.json`. Defaults to `<app->home>/dist/templates/manifest.json`.\n# Pass `undef` to disable manifest-driven auto-init entirely; the\n# bf helper is still installed and callers can drive everything\n# manually as before.\nsub register ($self, $app, $config = {}) {\n $app->helper(bf => sub ($c) {\n $c->stash->{'bf.instance'} //= BarefootJS->new($c, $config);\n });\n\n my $manifest = _load_manifest($app, $config);\n return unless $manifest;\n\n # Cache the set of UI-registry slot keys so we can answer\n # \"is this template name a child or a top-level page?\" with a\n # single hash lookup at render time. Top-level entries are\n # everything that isn't `__barefoot__` and doesn't match\n # `ui/<name>/index` \u2014 the same partition `register_components_from_manifest`\n # applies internally.\n my %is_child_entry;\n for my $entry_name (keys %$manifest) {\n next if $entry_name eq '__barefoot__';\n next unless $entry_name =~ m{^ui/[^/]+/index$};\n $is_child_entry{$entry_name} = 1;\n }\n\n $app->hook(before_render => sub ($c, $args) {\n my $template = $args->{template};\n return unless defined $template && length $template;\n my $entry = $manifest->{$template};\n return unless $entry;\n return if $is_child_entry{$template};\n # Idempotency guard for nested renders. A controller might\n # call `render_to_string` inside an action and then `render`\n # \u2014 without this we'd re-init `bf` on the second pass and\n # wipe the script registrations the first pass collected.\n return if $c->stash->{'bf.auto_init_done'};\n\n # Escape hatch for callers that wire `bf` up by hand (the\n # existing `render_component` helper in the showcase app does\n # this). If `_scope_id` is already set we treat the request as\n # \"manually managed\" and leave it alone \u2014 same outcome as\n # before the plugin gained auto-init.\n my $bf = $c->bf;\n if (defined $bf->_scope_id && length $bf->_scope_id) {\n $c->stash->{'bf.auto_init_done'} = 1;\n return;\n }\n $c->stash->{'bf.auto_init_done'} = 1;\n\n $bf->_scope_id($template . '_' . substr(rand() =~ s/^0\\.//r, 0, 6));\n $bf->register_components_from_manifest($manifest);\n\n # Seed each ssrDefault into the stash unless the caller has\n # already supplied a value for that key \u2014 callers always win.\n my $defaults = $entry->{ssrDefaults};\n if (ref($defaults) eq 'HASH') {\n for my $name (keys %$defaults) {\n next if exists $c->stash->{$name};\n my $d = $defaults->{$name};\n my $value = ref($d) eq 'HASH' ? $d->{value} : $d;\n $c->stash->{$name} = $value;\n }\n }\n });\n}\n\nsub _load_manifest ($app, $config) {\n return undef if exists $config->{manifest_path} && !defined $config->{manifest_path};\n my $manifest_path = $config->{manifest_path}\n // $app->home->child('dist/templates/manifest.json');\n my $file = path($manifest_path);\n return undef unless -r $file;\n my $manifest = eval { decode_json($file->slurp) };\n if ($@ || ref($manifest) ne 'HASH') {\n $app->log->warn(\"BarefootJS: cannot parse manifest at $file: $@\") if $@;\n return undef;\n }\n return $manifest;\n}\n\n1;\n__END__\n\n=encoding utf8\n\n=head1 NAME\n\nMojolicious::Plugin::BarefootJS - Mojolicious integration for BarefootJS\n\n=head1 SYNOPSIS\n\n # Mojolicious application\n $self->plugin('BarefootJS');\n\n # In a controller / template, the `bf` helper exposes a per-request\n # BarefootJS runtime backed by BarefootJS::Backend::Mojo.\n\n=head1 DESCRIPTION\n\nWires the L<BarefootJS> server runtime into L<Mojolicious>. It registers a\nC<bf> controller helper that lazily instantiates one BarefootJS object per\nrequest (rendering via L<BarefootJS::Backend::Mojo>), and supports rendering\ncompiled marked templates as Mojolicious templates.\n\nFor non-Mojolicious / PSGI hosts, see L<BarefootJS::Backend::Xslate>, which\ndrives the same runtime with Text::Xslate and no web framework.\n\n=head1 METHODS\n\nL<Mojolicious::Plugin::BarefootJS> inherits all methods from\nL<Mojolicious::Plugin> and implements the following new one.\n\n=head2 register\n\n $plugin->register(Mojolicious->new, \\%conf);\n\nRegisters the plugin (the C<bf> helper and supporting hooks) in a Mojolicious\napplication.\n\n=head1 SEE ALSO\n\nL<BarefootJS>, L<BarefootJS::Backend::Mojo>, L<BarefootJS::Backend::Xslate>,\nL<Mojolicious>, L<https://github.com/piconic-ai/barefootjs>\n\n=head1 AUTHOR\n\nkobaken E<lt>kentafly88@gmail.comE<gt>\n\n=head1 LICENSE\n\nCopyright (c) 2025-present BarefootJS Contributors.\n\nThis library is free software; you can redistribute it and/or modify it under\nthe MIT License. See the F<LICENSE> file in the distribution for the full text.\n\n=cut\n";
23549
+ barefootPmSource = "package BarefootJS;\nour $VERSION = \"0.8.0\";\nuse strict;\nuse warnings;\nuse utf8;\nuse feature 'signatures';\nno warnings 'experimental::signatures';\n\nuse POSIX ();\nuse Scalar::Util qw(looks_like_number weaken);\n\n# NOTE: This runtime is template-engine-agnostic AND framework-agnostic by\n# design, so it can ship as a standalone CPAN distribution. It depends only on\n# core Perl (subroutine signatures + the hand-rolled minimal accessor base\n# below \u2014 no Mojo::Base, no Class::Tiny). Every operation that depends on *how*\n# a template is rendered \u2014 JSON marshalling, raw-string marking, JSX-children\n# materialisation, and named-template rendering \u2014 is delegated to a pluggable\n# `backend` (see BarefootJS::Backend::Mojo for the reference Mojolicious\n# implementation), which is the only component that pulls in the Mojo\n# distribution, and only when it is actually used.\n\n# ---------------------------------------------------------------------------\n# Minimal accessor base (no Mojo::Base / Class::Tiny dependency)\n# ---------------------------------------------------------------------------\n#\n# Generates read/write accessors with optional lazy defaults so the runtime\n# stays free of any non-core OO base. Semantics mirror the Mojo::Base `has`\n# this class used to inherit: a getter returns the stored value (building it\n# from the default on first access if unset); a setter stores the value and\n# returns $self for chaining. A default is either a plain scalar or a coderef\n# invoked as `$default->($self)` (for per-instance refs like `[]` / `{}` and\n# the lazily-required Mojo backend).\nmy %ATTR_DEFAULT = (\n _scripts => sub { [] },\n _script_seen => sub { {} },\n _child_renderers => sub { {} },\n _is_child => 0,\n # Lazily fall back to the Mojo reference backend so a bare-blessed\n # instance (the pure-function unit tests) and the historical\n # `BarefootJS->new($c, ...)` callers keep working unchanged. A non-Mojo\n # host injects its own backend via `BarefootJS->new($c, { backend => $b })`\n # and never triggers this require \u2014 keeping the core load Mojo-free.\n backend => sub {\n require BarefootJS::Backend::Mojo;\n return BarefootJS::Backend::Mojo->new;\n },\n);\n\n# c \u2014 Mojolicious controller (kept for back-compat accessors)\n# config \u2014 plugin / instance config\n# backend \u2014 the template-engine seam (#engine-abstraction)\n# _scope_id \u2014 addressable scope id\n# _bf_parent / _bf_mount \u2014 slot identity when this scope is slot-attached\n# _props \u2014 props serialised into bf-p / the scope comment\nfor my $attr (qw(\n c config backend\n _scripts _script_seen _scope_id _is_child _bf_parent _bf_mount _props\n _child_renderers\n)) {\n no strict 'refs';\n *{\"BarefootJS::$attr\"} = sub {\n my $self = shift;\n if (@_) { $self->{$attr} = shift; return $self; }\n if (!exists $self->{$attr} && exists $ATTR_DEFAULT{$attr}) {\n my $d = $ATTR_DEFAULT{$attr};\n $self->{$attr} = ref($d) eq 'CODE' ? $d->($self) : $d;\n }\n return $self->{$attr};\n };\n}\n\nsub new ($class, $c, $config = {}) {\n # Build (or accept an injected) rendering backend. The default Mojo\n # backend wraps the controller and honours an optional `json_encoder`\n # override so a host can swap in a faster XS JSON implementation\n # without subclassing. A caller targeting another template engine\n # passes its own backend via `$config->{backend}`.\n my $backend = $config->{backend};\n unless ($backend) {\n require BarefootJS::Backend::Mojo;\n $backend = BarefootJS::Backend::Mojo->new(\n c => $c,\n ($config->{json_encoder}\n ? (json_encoder => $config->{json_encoder})\n : ()),\n );\n }\n my $self = bless {\n c => $c,\n config => $config,\n backend => $backend,\n }, $class;\n # Hold the controller weakly. Mojolicious stashes this bf instance under\n # `$c->stash->{'bf.instance'}`, so a strong bf -> controller back-reference\n # closes a per-request cycle ($c -> stash -> bf -> $c) that Perl's\n # refcount GC cannot reclaim, leaking one controller + bf + child-renderer\n # closures per request. The controller owns (outlives) the per-request bf,\n # so the weak ref stays valid for the whole render. Callers that need the\n # controller to outlive the bf instance independently must keep their own\n # strong reference (the normal Mojo request scope already does).\n weaken($self->{c}) if defined $c;\n return $self;\n}\n\n# ---------------------------------------------------------------------------\n# Scope & Props\n# ---------------------------------------------------------------------------\n\nsub scope_attr ($self) {\n # bf-s is the addressable scope id only (#1249).\n return $self->_scope_id // '';\n}\n\n# Emits `bf-h=\"<host>\" bf-m=\"<slot>\" bf-r=\"\"` conditionally.\n# See spec/compiler.md \"Slot identity\".\nsub hydration_attrs ($self) {\n my @parts;\n my $host = $self->_bf_parent;\n my $mount = $self->_bf_mount;\n if (defined $host && length $host) {\n my $h = $host =~ s/\"/&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 # encode_json returns a character string (not bytes) for safe embedding\n # in templates (the Mojo backend uses Mojo::JSON::to_json).\n my $json = $self->backend->encode_json($props);\n return qq{ bf-p='$json'};\n}\n\n# ---------------------------------------------------------------------------\n# Comment Markers\n# ---------------------------------------------------------------------------\n\nsub comment ($self, $text) {\n return \"<!--bf-$text-->\";\n}\n\n# ---------------------------------------------------------------------------\n# JS-equivalent value stringification\n# ---------------------------------------------------------------------------\n\n# Map a Perl boolean-shaped value to the JS `String(bool)` form.\n# Used by the Mojo adapter when emitting reactive attribute bindings\n# whose JS source `isBooleanResultExpr` classified as boolean \u2014\n# a comparison (`count() > 0`), a logical negation (`!ok()`), or a\n# literal `true` / `false`. Perl's auto-stringification of those\n# expressions yields `''` / `1`; Hono and Go emit `'false'` / `'true'`.\n# Centralising the bool \u2192 string mapping here keeps the contract\n# testable and the template-emit syntax tidy\n# (`<%= bf->bool_str(...) %>` vs an inline ternary).\n#\n# Contract is boolean-only: callers must have classified the\n# expression as boolean-result before routing through this helper.\n# Non-boolean values reaching here will be Perl-truthy-coerced to\n# 'true' / 'false', which is generally wrong \u2014 non-boolean attribute\n# bindings stay on the plain `<%= expr %>` emit path and never reach\n# this function.\nsub bool_str ($self, $value) {\n return $value ? 'true' : 'false';\n}\n\nsub text_start ($self, $slot_id) {\n return \"<!--bf:$slot_id-->\";\n}\n\nsub text_end ($self) {\n return \"<!--/-->\";\n}\n\n# See spec/compiler.md \"Slot identity\" for the comment-scope wire format.\nsub scope_comment ($self) {\n my $scope_id = $self->_scope_id // '';\n my $host_segment = '';\n my $host = $self->_bf_parent;\n my $mount = $self->_bf_mount;\n if (defined $host && length $host) {\n $host_segment = \"|h=$host|m=\" . ($mount // '');\n }\n my $props_json = '';\n if ($self->_props && %{$self->_props}) {\n $props_json = '|' . $self->backend->encode_json($self->_props);\n }\n return \"<!--bf-scope:$scope_id$host_segment$props_json-->\";\n}\n\n# ---------------------------------------------------------------------------\n# Script Registration\n# ---------------------------------------------------------------------------\n\nsub register_script ($self, $path) {\n return if $self->_script_seen->{$path};\n $self->_script_seen->{$path} = 1;\n push @{$self->_scripts}, $path;\n}\n\n# ---------------------------------------------------------------------------\n# Child Component Rendering\n# ---------------------------------------------------------------------------\n# (`_child_renderers` accessor is generated by the minimal accessor base above.)\n\nsub register_child_renderer ($self, $name, $renderer) {\n $self->_child_renderers->{$name} = $renderer;\n}\n\nsub render_child ($self, $name, @args) {\n my $renderer = $self->_child_renderers->{$name};\n die \"No renderer registered for child component '$name'\" unless $renderer;\n # Accept both the Mojo list form \u2014 `bf->render_child($name, k => v, ...)`\n # \u2014 and the single-hashref form \u2014 `$bf.render_child($name, { k => v })`.\n # Template languages whose method calls can't splat a hash into positional\n # args (Text::Xslate Kolon, Template Toolkit) pass one hashref instead.\n my %props = (@args == 1 && ref $args[0] eq 'HASH') ? %{ $args[0] } : @args;\n # JSX children come in via the engine's children-capture mechanism\n # (Mojo's `begin %>...<% end`, which produces a CODE ref returning a\n # Mojo::ByteStream). Materialize it through the backend before handing\n # the props to the child renderer so the child template sees\n # `$children` as already-rendered HTML. Guard on `exists` so a\n # childless invocation (`bf->render_child('counter')`) doesn't gain a\n # spurious `children => undef` key \u2014 preserving the historical \"only\n # touch children when present\" behaviour.\n $props{children} = $self->backend->materialize($props{children})\n if exists $props{children};\n return $renderer->(\\%props);\n}\n\n# ---------------------------------------------------------------------------\n# Bulk registration from build manifest\n# ---------------------------------------------------------------------------\n#\n# `bf build` emits dist/templates/manifest.json describing every\n# component the page might invoke (Counter, ui/button/index, ...).\n# This helper walks that manifest and registers one child renderer per\n# UI registry entry \u2014 the path shape `ui/<name>/index` maps to the\n# `<name>` slot key Counter.html.ep and friends use via\n# `<%= bf->render_child('<name>', ...) %>`.\n#\n# Each manifest entry carries an `ssrDefaults` hash derived statically\n# from the component's JSX (prop destructure defaults + signal /\n# memo initial values, see packages/jsx/src/ssr-defaults.ts). The\n# child renderer seeds every template variable from that hash,\n# preferring the caller's matching prop where one exists. This\n# replaces the per-component `signal_init` callback that every\n# scaffold's `app.pl` used to hand-roll for items 1/3 of issue #1416.\n#\n# `signal_init` remains as an opt-in override for cases the static\n# extractor can't see through (e.g. signal initial values that\n# reference imported helpers). When supplied for a given slot key\n# it takes precedence over the manifest's `ssrDefaults` for that\n# child, allowing callers to mix manual overrides with auto-derived\n# defaults for siblings.\nsub register_components_from_manifest ($self, $manifest, %opts) {\n my $signal_inits = $opts{signal_init} // {};\n my $parent_scope = $self->_scope_id;\n # Weaken the parent capture so the child-renderer closures stored on\n # `$self->_child_renderers` don't keep `$self` alive (the direct\n # closure <-> parent cycle). The controller is reached through `$parent`\n # at call time rather than captured strongly here, so the closures hold\n # no strong reference to `$c` either \u2014 see the controller-cycle note in\n # `new`. `$parent` is always live whenever a closure runs (the closure is\n # stored on `$parent`, so `$parent` outlives every invocation).\n weaken(my $parent = $self);\n\n for my $entry_name (keys %$manifest) {\n # `__barefoot__` is the runtime entry, not a component.\n next if $entry_name eq '__barefoot__';\n # Only UI registry components (path shape `ui/<name>/index`)\n # become child renderers; top-level page components are the\n # render target rather than a child.\n next unless $entry_name =~ m{^ui/([^/]+)/index$};\n my $slot_key = $1;\n my $marked = $manifest->{$entry_name}{markedTemplate} // '';\n next unless $marked;\n # `templates/ui/button/index.html.ep` \u2192 `ui/button/index`\n my $template_name = $marked;\n $template_name =~ s{^templates/}{};\n $template_name =~ s{\\.html\\.ep$}{};\n\n my $signal_init = $signal_inits->{$slot_key};\n my $manifest_defaults = $manifest->{$entry_name}{ssrDefaults};\n $self->register_child_renderer($slot_key, sub {\n my ($props) = @_;\n # Child shares the parent's backend so nested renders go\n # through the same engine + controller (and inherit any\n # injected json_encoder). The controller is fetched via the weak\n # `$parent` at call time \u2014 never captured strongly \u2014 so the\n # closure adds no edge to the per-request reference cycle.\n my $child_bf = BarefootJS->new($parent->c, { backend => $parent->backend });\n my $slot_id = delete $props->{_bf_slot};\n $child_bf->_scope_id(\n $slot_id ? $parent_scope . '_' . $slot_id\n : $template_name . '_' . substr(rand() =~ s/^0\\.//r, 0, 6)\n );\n $child_bf->_is_child(1);\n # (#1249) Slot identity: host scope + slot id. Emitted as\n # bf-h / bf-m attributes by hydration_attrs.\n if ($slot_id) {\n $child_bf->_bf_parent($parent_scope);\n $child_bf->_bf_mount($slot_id);\n }\n $child_bf->_scripts($parent->_scripts);\n $child_bf->_script_seen($parent->_script_seen);\n\n my %extra;\n if ($signal_init) {\n %extra = $signal_init->($props);\n } elsif ($manifest_defaults) {\n %extra = _derive_stash_from_defaults($manifest_defaults, $props);\n }\n\n # Render the child template with $child_bf bound as the active\n # instance for the nested render. The backend owns the\n # engine-specific binding + restore (stash juggle for Mojo).\n my $html = $parent->backend->render_named(\n $template_name, $child_bf, { %$props, %extra },\n );\n chomp $html;\n return $html;\n });\n }\n}\n\n# Derive template-stash kvs from a manifest entry's `ssrDefaults`\n# section. Each entry shape:\n# { value => <static-fallback>, propName => <prop>, isRestProps => bool }\n# For `isRestProps`, the rest bag passes through unchanged (or the\n# static `{}` if the caller didn't supply one). For ordinary entries\n# the caller's `$props->{propName}` wins when defined, otherwise the\n# static `value` does. `propName`-less entries (signal / memo locals)\n# always use the static value \u2014 the caller cannot override them.\nsub _derive_stash_from_defaults ($defaults, $props) {\n my %extra;\n for my $name (keys %$defaults) {\n my $d = $defaults->{$name};\n if (ref($d) ne 'HASH') {\n $extra{$name} = $d;\n next;\n }\n if ($d->{isRestProps}) {\n $extra{$name} = exists $props->{$name} ? $props->{$name} : $d->{value};\n next;\n }\n my $prop_name = $d->{propName};\n if (defined $prop_name && exists $props->{$prop_name} && defined $props->{$prop_name}) {\n $extra{$name} = $props->{$prop_name};\n } else {\n $extra{$name} = $d->{value};\n }\n }\n return %extra;\n}\n\n# ---------------------------------------------------------------------------\n# Script Output\n# ---------------------------------------------------------------------------\n\nsub scripts ($self) {\n my @tags;\n for my $path (@{$self->_scripts}) {\n push @tags, qq{<script type=\"module\" src=\"$path\"></script>};\n }\n return join(\"\\n\", @tags);\n}\n\n# ---------------------------------------------------------------------------\n# Streaming SSR (Out-of-Order)\n# ---------------------------------------------------------------------------\n\nsub streaming_bootstrap ($self) {\n return q{<script>(function(){function s(id){var a=document.querySelector('[bf-async=\"'+id+'\"]');var t=document.querySelector('template[bf-async-resolve=\"'+id+'\"]');if(!a||!t)return;a.replaceChildren(t.content.cloneNode(true));a.removeAttribute('bf-async');t.remove();requestAnimationFrame(function(){if(window.__bf_hydrate)window.__bf_hydrate()})};window.__bf_swap=s})()</script>};\n}\n\nsub async_boundary ($self, $id, $fallback_html) {\n # The fallback comes in via Mojo `begin %>...<% end` capture (see\n # MojoAdapter::renderAsync), which produces a CODE ref returning a\n # Mojo::ByteStream. Materialize it through the backend so the rendered\n # HTML embeds in the placeholder rather than the CODE ref's\n # stringification.\n $fallback_html = $self->backend->materialize($fallback_html);\n return qq{<div bf-async=\"$id\">$fallback_html</div>};\n}\n\nsub async_resolve ($self, $id, $content_html) {\n return qq{<template bf-async-resolve=\"$id\">$content_html</template><script>__bf_swap(\"$id\")</script>};\n}\n\n# ---------------------------------------------------------------------------\n# JS-compat callees (#1189) \u2014 invoked from generated Mojo templates as\n# <%= bf->json($val) %>, <%= bf->floor($val) %>, etc. The MojoAdapter's\n# `templatePrimitives` registry emits these helper calls in place of the\n# corresponding JS callees (`JSON.stringify`, `Math.floor`, \u2026) so the SSR\n# template can render value-equivalent output without a JS engine.\n#\n# Failure policy mirrors the Go adapter (#1188): user-data marshalling\n# (json) bubbles errors so Mojolicious aborts loudly on cycles /\n# unsupported values rather than silently producing an empty payload.\n# Numeric coercion follows JS semantics (NaN propagates as the special\n# string 'NaN'; non-numeric input returns 'NaN' rather than 0). Strings\n# always coerce to a string representation.\n# ---------------------------------------------------------------------------\n\nsub json ($self, $value) {\n # Mojo::JSON::to_json returns a character string (not bytes), suitable\n # for embedding in HTML output via Mojo::ByteStream / `<%==`.\n #\n # Documented divergence from JS: JS distinguishes `null` (renders as\n # \"null\") from `undefined` (`JSON.stringify(undefined)` returns the\n # JS value `undefined`, not a string). Perl has no such distinction\n # \u2014 both map to `undef`. We choose the `null` rendering for SSR\n # ergonomics: an unset prop becomes the string \"null\" rather than\n # the literal text \"undefined\" or an empty attribute. Matches the\n # `null` case of JS exactly; diverges from the `undefined` case.\n return $self->backend->encode_json($value);\n}\n\nsub string ($self, $value) {\n # JS `String(v)` mirror. `undef` renders as the empty string here so\n # an unset prop doesn't surface as a literal \"undefined\" / \"null\"\n # in user-facing HTML \u2014 same divergence the Go adapter documents\n # for `bf_string`.\n return defined $value ? \"$value\" : '';\n}\n\nsub number ($self, $value) {\n # JS `Number(v)` mirror. Numeric coerces via Perl's implicit\n # numeric context; non-numeric / undef yield real numeric NaN\n # (`'nan' + 0`) so downstream arithmetic propagates correctly\n # (`Math.floor(NaN) === NaN`). Returning the literal string\n # \"NaN\" would conflate the user-passing-the-string-\"NaN\" case\n # with the parse-failure case, and break NaN detection in\n # downstream helpers.\n return 0 + 'nan' unless defined $value;\n return $value + 0 if looks_like_number($value);\n return 0 + 'nan';\n}\n\n# NaN is the only float for which `$x != $x` holds. Used as the\n# portable sentinel check in floor/ceil/round.\nsub _is_nan { my $n = shift; return $n != $n }\n\nsub floor ($self, $value) {\n my $n = $self->number($value);\n return $n if _is_nan($n);\n return POSIX::floor($n);\n}\n\nsub ceil ($self, $value) {\n my $n = $self->number($value);\n return $n if _is_nan($n);\n return POSIX::ceil($n);\n}\n\nsub round ($self, $value) {\n my $n = $self->number($value);\n return $n if _is_nan($n);\n # POSIX has no `round`. JS `Math.round` rounds half toward\n # +Infinity (so `Math.round(-1.5) === -1`, not -2). `floor(n\n # + 0.5)` reproduces that for both signs.\n return POSIX::floor($n + 0.5);\n}\n\n# ---------------------------------------------------------------------------\n# Array / String method helpers (#1448 Tier A)\n# ---------------------------------------------------------------------------\n#\n# `Array.prototype.includes(x)` and `String.prototype.includes(sub)`\n# share a method name in JS; the JSX parser can't tell the two\n# receiver shapes apart without TS type inference, so both lower to\n# the same IR node (`array-method` / method `includes`). This helper\n# dispatches at the Perl level via `ref()`:\n# - ARRAY ref: scan elements with `eq`; one defined-vs-undef\n# hop matches JS's `===` for null/undefined.\n# - scalar: `index($recv, $sub) != -1`, with both args\n# coerced through `// ''` so an undef receiver /\n# needle doesn't trip Perl's substr warning.\n# Anything else (HASH ref, code ref) returns false \u2014 matches the\n# JS semantic where `.includes` is only defined on Array /\n# TypedArray / String.\n\nsub includes ($self, $recv, $elem) {\n if (ref($recv) eq 'ARRAY') {\n for my $item (@$recv) {\n if (!defined $item) {\n return 1 if !defined $elem;\n next;\n }\n return 1 if defined $elem && $item eq $elem;\n }\n return 0;\n }\n return 0 if ref($recv);\n return index($recv // '', $elem // '') != -1 ? 1 : 0;\n}\n\n# `Array.prototype.filter(fn)` / `.every(fn)` / `.some(fn)`. The Xslate adapter\n# lowers a JS arrow predicate to a Kolon lambda (`-> $x { ... }`), which is\n# callable from Perl as a code ref, and emits `$bf.filter($arr, <lambda>)`.\n# `filter` returns a new arrayref; `every` / `some` return 1/0. Non-array /\n# empty receivers follow JS (`filter` \u2192 [], `every` \u2192 true, `some` \u2192 false).\n# (The Mojo adapter lowers these shapes inline and never reaches these methods.)\nsub filter ($self, $recv, $pred) {\n return [] unless ref($recv) eq 'ARRAY';\n return [ grep { $pred->($_) } @$recv ];\n}\n\nsub every ($self, $recv, $pred) {\n return 1 unless ref($recv) eq 'ARRAY';\n for my $item (@$recv) { return 0 unless $pred->($item) }\n return 1;\n}\n\nsub some ($self, $recv, $pred) {\n return 0 unless ref($recv) eq 'ARRAY';\n for my $item (@$recv) { return 1 if $pred->($item) }\n return 0;\n}\n\n# `Array.prototype.find(fn)` / `.findIndex(fn)` / `.findLast(fn)` /\n# `.findLastIndex(fn)` \u2014 same Kolon-lambda predicate mechanism as filter. The\n# camelCase JS names lower to these snake_case methods (like index_of /\n# last_index_of). `find` / `find_last` return the matching element (or undef \u2192\n# JS `undefined`); the index forms return the 0-based position (or -1).\nsub find ($self, $recv, $pred) {\n return undef unless ref($recv) eq 'ARRAY';\n for my $item (@$recv) { return $item if $pred->($item) }\n return undef;\n}\n\nsub find_index ($self, $recv, $pred) {\n return -1 unless ref($recv) eq 'ARRAY';\n for my $i (0 .. $#$recv) { return $i if $pred->($recv->[$i]) }\n return -1;\n}\n\nsub find_last ($self, $recv, $pred) {\n return undef unless ref($recv) eq 'ARRAY';\n for my $i (reverse 0 .. $#$recv) { return $recv->[$i] if $pred->($recv->[$i]) }\n return undef;\n}\n\nsub find_last_index ($self, $recv, $pred) {\n return -1 unless ref($recv) eq 'ARRAY';\n for my $i (reverse 0 .. $#$recv) { return $i if $pred->($recv->[$i]) }\n return -1;\n}\n\n# `String.prototype.toLowerCase()` / `.toUpperCase()`. Kolon has a builtin\n# `.join` array method (so the adapter uses that directly) but no builtin\n# `lc` / `uc`, so these live on the runtime object. `CORE::` avoids recursing\n# into these methods.\nsub lc ($self, $s) { return defined $s ? CORE::lc($s) : '' }\nsub uc ($self, $s) { return defined $s ? CORE::uc($s) : '' }\n\n# `Array.prototype.join(sep)` with JS semantics: separator defaults to \",\",\n# and undefined / null elements render as empty (`[1,,2].join(\",\")` \u2192 \"1,,2\").\n# Kolon has a builtin `.join`, but routing through the runtime keeps the\n# JS-compat element handling in one place. `CORE::join` avoids recursing.\nsub join ($self, $recv, $sep = undef) {\n return '' unless ref($recv) eq 'ARRAY';\n $sep //= ',';\n return CORE::join($sep, map { defined $_ ? $_ : '' } @$recv);\n}\n\n# `.length` \u2014 JS works on BOTH arrays (element count) and strings (character\n# count); Kolon's builtin `.size()` is array-only and faults on a string. So\n# dispatch on ref type here. `CORE::length` avoids recursing into this method.\nsub length ($self, $recv) {\n return scalar @$recv if ref($recv) eq 'ARRAY';\n return 0 if ref($recv);\n return CORE::length($recv // '');\n}\n\n# `Array.prototype.indexOf(x)` / `Array.prototype.lastIndexOf(x)`\n# value-equality search (#1448 Tier A). Returns the 0-based position\n# of the first / last matching element, or -1 if not found.\n# Non-array receivers return -1 \u2014 matches the JS semantic that\n# `.indexOf` / `.lastIndexOf` are only defined on Array / TypedArray.\n# (The string-position `indexOf` form isn't in Tier A; if it lands\n# later the helper can grow a ref()-dispatch branch like `includes`.)\n\nsub _array_index_of ($recv, $elem, $reverse) {\n return -1 unless ref($recv) eq 'ARRAY';\n my @indices = $reverse ? (reverse 0 .. $#{$recv}) : (0 .. $#{$recv});\n for my $i (@indices) {\n my $item = $recv->[$i];\n if (!defined $item) {\n return $i if !defined $elem;\n next;\n }\n return $i if defined $elem && $item eq $elem;\n }\n return -1;\n}\n\nsub index_of ($self, $recv, $elem) {\n return _array_index_of($recv, $elem, 0);\n}\n\nsub last_index_of ($self, $recv, $elem) {\n return _array_index_of($recv, $elem, 1);\n}\n\n# `Array.prototype.at(i)` \u2014 supports negative indices (`.at(-1)` is\n# the last element); out-of-bounds returns undef (which Mojo's\n# auto-escape renders as the empty string, matching JS's `undefined`).\n# Non-array receivers return undef. Matches the Go `bf_at` arithmetic\n# (`length + i` for i < 0) so adapter output stays symmetric.\n\nsub at ($self, $recv, $i) {\n return undef unless ref($recv) eq 'ARRAY';\n return undef if !defined $i;\n my $len = scalar @$recv;\n return undef if $len == 0;\n my $idx = $i < 0 ? $len + $i : $i;\n return undef if $idx < 0 || $idx >= $len;\n return $recv->[$idx];\n}\n\n# `Array.prototype.concat(other)` \u2014 merges two arrays in order\n# into a new ARRAY ref. Non-array operands collapse to empty\n# (matches the Go `bf_concat` semantic so cross-adapter output\n# stays symmetric; differs from JS where a non-Array argument\n# with `Symbol.isConcatSpreadable` would be spread, a behaviour\n# the template-language path never observes).\n\nsub concat ($self, $a, $b) {\n my @out;\n push @out, @$a if ref($a) eq 'ARRAY';\n push @out, @$b if ref($b) eq 'ARRAY';\n return \\@out;\n}\n\n# `Array.prototype.slice(start, end?)` \u2014 carves out a sub-range\n# into a new ARRAY ref. Mirrors the Go `bf_slice` arithmetic so\n# adapter output stays symmetric:\n# - start < 0 \u2192 length + start (e.g. -1 = last index)\n# - end < 0 \u2192 length + end\n# - start < 0 after clamp \u2192 0\n# - end > length \u2192 length\n# - start >= end \u2192 empty\n# - end undef \u2192 \"to length\"\n# Non-array receivers return an empty ARRAY ref.\n\nsub slice ($self, $recv, $start, $end) {\n return [] unless ref($recv) eq 'ARRAY';\n my $len = scalar @$recv;\n return [] if $len == 0;\n\n my $s = $start // 0;\n $s = $len + $s if $s < 0;\n $s = 0 if $s < 0;\n $s = $len if $s > $len;\n\n my $e = defined $end ? $end : $len;\n $e = $len + $e if $e < 0;\n $e = 0 if $e < 0;\n $e = $len if $e > $len;\n\n return [] if $s >= $e;\n return [ @{$recv}[$s .. $e - 1] ];\n}\n\n# `Array.prototype.reverse()` / `Array.prototype.toReversed()` \u2014\n# both shapes share this lowering. SSR templates render a snapshot\n# of state, so JS's mutate-receiver (`reverse`) vs\n# return-new-array (`toReversed`) distinction has no template-\n# level meaning. Always returns a new ARRAY ref to keep callers\n# safe from accidental aliasing. Non-array receivers return an\n# empty ARRAY ref.\n\nsub reverse ($self, $recv) {\n return [] unless ref($recv) eq 'ARRAY';\n return [ reverse @$recv ];\n}\n\n# `Array.prototype.flat(depth?)` (#1448 Tier C) \u2014 flatten nested ARRAY\n# refs `$depth` levels deep. A `$depth` of -1 is the `Infinity` sentinel\n# (flatten fully); 0 returns a shallow copy. Non-ARRAY elements are kept\n# as-is (JS only flattens nested arrays). Non-ARRAY receiver \u2192 [].\nsub flat ($self, $recv, $depth = 1) {\n return [] unless ref($recv) eq 'ARRAY';\n my @out;\n for my $el (@$recv) {\n if ($depth != 0 && ref($el) eq 'ARRAY') {\n my $next = $depth > 0 ? $depth - 1 : $depth;\n push @out, @{ $self->flat($el, $next) };\n }\n else {\n push @out, $el;\n }\n }\n return \\@out;\n}\n\n# `Array.prototype.flatMap(fn)` value-returning field projection\n# (#1448 Tier C) \u2014 map each element through a self / field projection,\n# then flatten one level. `field` reads a HASH-ref key (the raw JS prop\n# name, as `bf->reduce` does); a projected non-ARRAY value is kept as-is\n# (flatMap = map + flat(1)). Non-ARRAY receiver \u2192 [].\nsub flat_map ($self, $recv, $key_kind, $key) {\n return [] unless ref($recv) eq 'ARRAY';\n my @projected;\n for my $el (@$recv) {\n if ($key_kind eq 'field') {\n # JS `i => i.field` on a non-object yields `undefined`, not the\n # element itself \u2014 push `undef` so a scalar element doesn't leak\n # into the output (matches Go's `getFieldValue` returning nil).\n push @projected, ref($el) eq 'HASH' ? $el->{$key} : undef;\n }\n else {\n push @projected, $el;\n }\n }\n return $self->flat(\\@projected, 1);\n}\n\n# `Array.prototype.flatMap(i => [i.a, i.b])` \u2014 array-literal tuple\n# projection (#1448 Tier C). Each `@specs` entry is a [kind, key] arrayref\n# (['self', ''] or ['field', 'a']). For each element, every leaf's value\n# is appended in order. flat(1) removes only the literal wrapper, so an\n# array-valued leaf is appended verbatim (no spread) \u2014 i.e. just append\n# each leaf. A non-HASH element under a `field` leaf yields undef (JS\n# `i.field` on a non-object). Non-ARRAY receiver \u2192 [].\nsub flat_map_tuple ($self, $recv, @specs) {\n return [] unless ref($recv) eq 'ARRAY';\n my @out;\n for my $el (@$recv) {\n for my $spec (@specs) {\n my ($kind, $key) = @$spec;\n if ($kind eq 'field') {\n push @out, ref($el) eq 'HASH' ? $el->{$key} : undef;\n }\n else {\n push @out, $el;\n }\n }\n }\n return \\@out;\n}\n\n# `String.prototype.trim()` \u2014 strip leading + trailing whitespace.\n# JS's `String.prototype.trim` matches `\\s` in the Unicode sense\n# (any whitespace including non-breaking space U+00A0); Perl's `\\s`\n# inside a regex with `/u` flag is the same. Undef receivers return\n# the empty string (matches JS's `String(undefined).trim()` which\n# would be \"undefined\" \u2192 \"undefined\", but in our template context\n# undef commonly means \"missing prop\"; rendering the empty string\n# is the safer choice and mirrors the JS-compat divergence we\n# already document for `bf->string(undef) === \"\"`).\n\nsub trim ($self, $recv) {\n return '' unless defined $recv;\n return '' if ref($recv);\n my $s = \"$recv\";\n $s =~ s/^\\s+|\\s+$//gu;\n return $s;\n}\n\n# `String.prototype.split(sep)` (#1448 Tier B) \u2014 string \u2192 ARRAY ref.\n#\n# Two JS-parity wrinkles drive the helper (a bare `split` emit would\n# diverge from both JS and Go):\n#\n# * Perl's `split` treats its first argument as a *regex*, so a\n# separator like '.' or '|' would match far too much. We\n# `quotemeta` it to force literal-string matching, mirroring JS's\n# string-separator semantics (the regex-separator form stays\n# refused upstream \u2014 see the parser arm).\n# * Perl's `split` drops trailing empty fields by default; JS keeps\n# them (`\"a,\".split(\",\")` is `[\"a\", \"\"]`). Passing the `-1` limit\n# preserves them, matching JS and Go's `strings.Split`.\n#\n# An empty separator splits into individual characters (JS + Go agree).\n# Undef receiver renders as the single-element `['']` \u2014 the same\n# \"missing prop \u2192 empty string\" convention `bf->trim` uses.\n\nsub split ($self, $recv, $sep = undef, $limit = undef) {\n my $s = defined $recv && !ref($recv) ? \"$recv\" : '';\n\n my @parts;\n if (!defined $sep) {\n # No separator \u2192 the whole string in a single-element array\n # (matches JS `\"x\".split()` / `.split(undefined)`).\n @parts = ($s);\n }\n elsif (\"$sep\" eq '') {\n # Empty separator \u2192 individual characters. No `-1` limit here:\n # on an empty pattern Perl's `split` with `-1` appends a spurious\n # trailing empty field (\"abc\" \u2192 'a','b','c',''), which JS/Go don't.\n @parts = split //, $s;\n }\n elsif ($s eq '') {\n # Empty input with a non-empty separator: JS `\"\".split(\",\")` is\n # `[\"\"]` and Go's `strings.Split(\"\", \",\")` is `[\"\"]`, but Perl's\n # `split /,/, ''` returns the empty list \u2014 special-case for parity.\n @parts = ('');\n }\n else {\n # `quotemeta` forces literal-string matching (JS string-separator\n # semantics); the `-1` keeps trailing empty fields (JS keeps them,\n # Perl's bare `split` drops them).\n my $q = quotemeta(\"$sep\");\n @parts = split /$q/, $s, -1;\n }\n\n # Optional `limit` caps the number of pieces (JS `split(sep, limit)`).\n # 0 \u2192 empty; a negative limit keeps all (JS ToUint32 wrap makes it\n # effectively unbounded) \u2014 both match Go's `bf_split`.\n if (defined $limit) {\n my $n = int($limit);\n if ($n == 0) { @parts = () }\n elsif ($n > 0 && $n < scalar @parts) { @parts = @parts[0 .. $n - 1] }\n }\n\n return [@parts];\n}\n\n# `String.prototype.startsWith(prefix, position?)` (#1448 Tier B) \u2014\n# string \u2192 boolean (1 / 0). `substr`-anchored literal comparison mirrors\n# Go's `strings.HasPrefix`. An empty prefix is always true (JS parity);\n# undef / non-string receivers coerce to the empty string first. The\n# optional `position` re-anchors the test (clamped to `[0, length]`),\n# matching JS `\"abc\".startsWith(\"b\", 1)`.\n\nsub starts_with ($self, $recv, $prefix, $position = undef) {\n my $s = defined $recv && !ref($recv) ? \"$recv\" : '';\n my $p = defined $prefix ? \"$prefix\" : '';\n if (defined $position) {\n my $n = int($position);\n $n = 0 if $n < 0;\n $n = CORE::length($s) if $n > CORE::length($s);\n $s = substr($s, $n);\n }\n return substr($s, 0, CORE::length $p) eq $p ? 1 : 0;\n}\n\n# `String.prototype.endsWith(suffix, endPosition?)` (#1448 Tier B) \u2014\n# string \u2192 boolean (1 / 0). Mirrors Go's `strings.HasSuffix`. An empty\n# suffix is always true (JS parity); a suffix longer than the string is\n# false. `substr($s, -length $x)` would mis-read the whole string when\n# `length $x == 0`, so that case short-circuits. The optional\n# `endPosition` treats the string as if it were only that many chars\n# long (clamped to `[0, length]`), matching JS `\"abc\".endsWith(\"b\", 2)`.\n\nsub ends_with ($self, $recv, $suffix, $end_position = undef) {\n my $s = defined $recv && !ref($recv) ? \"$recv\" : '';\n my $x = defined $suffix ? \"$suffix\" : '';\n if (defined $end_position) {\n my $e = int($end_position);\n $e = 0 if $e < 0;\n $e = CORE::length($s) if $e > CORE::length($s);\n $s = substr($s, 0, $e);\n }\n return 1 if $x eq '';\n return 0 if CORE::length($s) < CORE::length($x);\n return substr($s, -CORE::length $x) eq $x ? 1 : 0;\n}\n\n# `String.prototype.replace(pattern, replacement)` \u2014 string-pattern\n# form only (#1448 Tier B), replacing the FIRST occurrence (JS string-\n# pattern semantics). Spliced via index/substr rather than `s///` so\n# BOTH the pattern and the replacement are literal: no Perl regex\n# metacharacters in the pattern and no `$1` / `$&` interpolation in the\n# replacement. Go's `bf_replace` (strings.Replace, n=1) treats the\n# replacement literally too, so the two adapters stay byte-equal \u2014 this\n# diverges from JS only for replacement strings containing `$`-patterns\n# (rare in template position). An empty pattern inserts the replacement\n# at the front (`\"abc\".replace(\"\", \"X\")` \u2192 \"Xabc\"), matching JS + Go.\n\nsub replace ($self, $recv, $pattern, $replacement) {\n my $s = defined $recv && !ref($recv) ? \"$recv\" : '';\n my $o = defined $pattern ? \"$pattern\" : '';\n my $n = defined $replacement ? \"$replacement\" : '';\n return $n . $s if $o eq '';\n my $i = index($s, $o);\n return $s if $i < 0;\n return substr($s, 0, $i) . $n . substr($s, $i + CORE::length($o));\n}\n\n# `String.prototype.repeat(n)` \u2014 the receiver concatenated n times\n# (#1448 Tier B), via Perl's `x` operator. JS throws RangeError for a\n# negative count, but SSR templates degrade to the empty string rather\n# than dying mid-render, so a count <= 0 returns \"\" (Go's `bf_repeat`\n# applies the same clamp). The count is truncated toward zero\n# (`int`), matching JS's ToIntegerOrInfinity on `\"a\".repeat(3.7)`.\n\nsub repeat ($self, $recv, $count) {\n my $s = defined $recv && !ref($recv) ? \"$recv\" : '';\n my $n = defined $count ? int($count) : 0;\n return $n <= 0 ? '' : $s x $n;\n}\n\n# `String.prototype.padStart` / `padEnd` (#1448 Tier B) \u2014 pad the\n# receiver to `$target` characters with `$pad` (default a single space)\n# repeated and truncated to fill, prepended or appended. Length is\n# measured in characters (Perl `length`), matching Go's rune-based\n# `bf_pad_*` \u2014 diverges from JS's UTF-16-unit length only for\n# astral-plane input. An empty pad, or a receiver already >= `$target`,\n# returns the receiver unchanged (JS parity). The `$target` is\n# truncated toward zero (JS ToLength on the first arg).\n\nsub _pad ($s, $target, $pad, $at_start) {\n $pad = ' ' unless defined $pad;\n $pad = \"$pad\";\n return $s if $pad eq '';\n my $len = CORE::length $s;\n my $t = int($target // 0);\n return $s if $len >= $t;\n my $need = $t - $len;\n # Repeat enough copies to cover $need, then trim to exactly $need.\n my $fill = substr($pad x (int($need / CORE::length($pad)) + 1), 0, $need);\n return $at_start ? $fill . $s : $s . $fill;\n}\n\nsub pad_start ($self, $recv, $target, $pad = undef) {\n my $s = defined $recv && !ref($recv) ? \"$recv\" : '';\n return _pad($s, $target, $pad, 1);\n}\n\nsub pad_end ($self, $recv, $target, $pad = undef) {\n my $s = defined $recv && !ref($recv) ? \"$recv\" : '';\n return _pad($s, $target, $pad, 0);\n}\n\n# `Array.prototype.sort(cmp)` / `Array.prototype.toSorted(cmp)`\n# lowering (#1448 Tier B). Non-mutating \u2014 JS's mutate-vs-new\n# distinction is moot in SSR template context.\n#\n# Opts hash-ref. The compiler emits a `keys` list of per-key hashes\n# in priority order; each hash carries:\n#\n# key_kind => 'self' | 'field'\n# key => '' when key_kind eq 'self'; field name verbatim\n# from the comparator AST (e.g. 'price', 'createdAt')\n# when key_kind eq 'field' \u2014 no case normalisation\n# applied. Perl hash lookups are case-sensitive so\n# the key here must match the actual hash key the\n# user populated.\n# compare_type => 'numeric' | 'string' | 'auto'\n# direction => 'asc' | 'desc'\n#\n# Accepted comparator catalogue (gated upstream at parse time \u2014\n# anything outside refuses with BF101 before reaching this helper):\n#\n# (a,b) => a.f - b.f \u2192 field, numeric\n# (a,b) => a - b \u2192 self, numeric\n# (a,b) => a[.f].localeCompare(b[.f]) \u2192 field|self, string\n# (a,b) => a.f > b.f ? 1 : -1 \u2192 field|self, auto\n# any of the above ||-chained \u2192 multi-key tie-breaks\n# (and reversed-operand variants for `desc`).\n#\n# `auto` (relational-ternary lowering) compares numerically when both\n# keys `looks_like_number`, else lexically \u2014 Go's `bf_sort` applies the\n# same rule so the two template adapters stay byte-equal.\n#\n# A future `nulls => 'first' | 'last'` knob can land per key without\n# churn \u2014 the opts hash is the right place to grow.\n\nsub sort ($self, $recv, $opts = {}) {\n return [] unless ref($recv) eq 'ARRAY';\n\n # Normalise the per-key specs (priority order, length >= 1).\n my @spec = map {\n {\n key_kind => $_->{key_kind} // 'self',\n key => $_->{key} // '',\n compare_type => $_->{compare_type} // 'numeric',\n direction => $_->{direction} // 'asc',\n }\n } @{ $opts->{keys} // [] };\n return [ @$recv ] unless @spec;\n\n # Schwartzian transform: project each item to all its sort keys\n # once, then compare projected keys. Cheaper than re-resolving the\n # field accessors inside every comparison for non-trivial arrays.\n my @keyed = map {\n my $item = $_;\n my @ks = map {\n $_->{key_kind} eq 'field' && ref($item) eq 'HASH' ? $item->{ $_->{key} } : $item;\n } @spec;\n [ \\@ks, $item ];\n } @$recv;\n\n my $cmp = sub {\n for my $i (0 .. $#spec) {\n my $sp = $spec[$i];\n my $c = _compare_sort_key($a->[0][$i], $b->[0][$i], $sp->{compare_type});\n next if $c == 0; # tie on this key \u2014 try the next\n return $sp->{direction} eq 'desc' ? -$c : $c;\n }\n return 0;\n };\n\n my @sorted = sort $cmp @keyed;\n return [ map { $_->[1] } @sorted ];\n}\n\n# Compare two projected keys, ascending orientation (-1 / 0 / 1); the\n# caller negates for 'desc'. 'auto' compares numerically when both\n# keys look like numbers, else lexically (matches Go's `bf_sort`).\n# undef coalesces to '' / 0 so the order stays total without warnings.\nsub _compare_sort_key ($av, $bv, $compare_type) {\n if ($compare_type eq 'string') {\n return ($av // '') cmp ($bv // '');\n }\n if ($compare_type eq 'auto') {\n if (looks_like_number($av // '') && looks_like_number($bv // '')) {\n return ($av // 0) <=> ($bv // 0);\n }\n return ($av // '') cmp ($bv // '');\n }\n return ($av // 0) <=> ($bv // 0); # numeric\n}\n\n# Fold an array into a scalar via the arithmetic-fold catalogue\n# (#1448 Tier C). Mirrors Go's `bf_reduce` and JS `reduce(fn, init)` /\n# `reduceRight(fn, init)` for the shapes `(acc, x) => acc <op> x` /\n# `(acc, x) => acc <op> x.field`:\n#\n# bf->reduce($recv, {\n# op => '+' | '*',\n# key_kind => 'self' | 'field',\n# key => '<field>', # when key_kind eq 'field'\n# type => 'numeric' | 'string',\n# init => <seed>, # number, or string for concat\n# direction => 'left' | 'right', # 'right' = reduceRight (default 'left')\n# })\n#\n# Numeric folds accumulate with `+` / `*` (non-numeric keys coalesce to\n# 0); string folds concatenate via `bf->string` (undef \u2192 ''). The init\n# seeds the accumulator, so an empty array returns it unchanged \u2014 exactly\n# like JS. `direction => 'right'` folds right-to-left (reduceRight); only\n# observable for string concat, since numeric sum / product commute.\n# Float stringification can diverge from Go's for inexact binary\n# fractions (e.g. 0.1 + 0.2); integer sums \u2014 the common case \u2014 agree.\nsub reduce ($self, $recv, $opts = {}) {\n my $op = $opts->{op} // '+';\n my $key_kind = $opts->{key_kind} // 'self';\n my $key = $opts->{key} // '';\n my $type = $opts->{type} // 'numeric';\n my $direction = $opts->{direction} // 'left';\n\n my @items = ref($recv) eq 'ARRAY' ? @$recv : ();\n # reduceRight folds right-to-left; reversing the snapshot keeps the\n # single forward loop below. Only observable for string concat \u2014\n # numeric sum / product commute. Qualify as CORE::reverse \u2014 this\n # package defines `sub reverse` (the `.reverse()` helper), so a bare\n # `reverse` is ambiguous under `use warnings`.\n @items = CORE::reverse(@items) if $direction eq 'right';\n my $project = sub ($item) {\n $key_kind eq 'field' && ref($item) eq 'HASH' ? $item->{$key} : $item;\n };\n\n if ($type eq 'string') {\n my $acc = $opts->{init} // '';\n $acc .= $self->string($project->($_)) for @items;\n return $acc;\n }\n\n my $acc = $opts->{init} // 0;\n for my $item (@items) {\n my $n = $project->($item);\n # Guard `defined` before `looks_like_number` so a missing field\n # (undef) folds as 0 without an \"uninitialized value\" warning\n # under `use warnings` \u2014 matching the `$av // ''` style `sort` uses.\n $n = 0 unless defined $n && looks_like_number($n);\n $op eq '*' ? ($acc *= $n) : ($acc += $n);\n }\n return $acc;\n}\n\n# ---------------------------------------------------------------------------\n# JSX intrinsic-element spread (#1407)\n# ---------------------------------------------------------------------------\n#\n# Mirrors the JS `spreadAttrs` runtime\n# (`packages/client/src/runtime/spread-attrs.ts`) and the Go adapter's\n# `bf.SpreadAttrs` so SSR output stays byte-equal across the three\n# adapters. Generated Mojo templates invoke this as\n# `<%== bf->spread_attrs($bag) %>`.\n#\n# Skip rules: nil/false values, event handlers (`on[A-Z]\u2026` shape\n# matching JS `key[2] === key[2].toUpperCase()` \u2014 true for any\n# character whose uppercase is itself, including digits and\n# underscore), `children`. `ref` is intentionally NOT filtered,\n# matching the JS reference.\n#\n# Key remap: className \u2192 class, htmlFor \u2192 for; SVG camelCase\n# attrs preserved (case-sensitive XML spec); other camelCase keys\n# lowered to kebab-case with a leading `-` for an initial\n# uppercase letter (mirrors JS `key.replace(/([A-Z])/g, '-$1')`).\n#\n# `style` is routed through `_style_to_css` so object literals\n# serialise to a real CSS string instead of Perl's default\n# `HASH(0x...)` form.\n#\n# Output is deterministic: keys are sorted alphabetically before\n# emission, matching the Go adapter's `sort.Strings(keys)` policy\n# and Mojo::JSON's marshal order.\n#\n# The return value is a Mojo::ByteStream so the calling template's\n# `<%==` raw-emit skips re-escaping (the helper has already\n# HTML-escaped each value).\n\nmy %SVG_CAMEL_CASE_ATTRS = map { $_ => 1 } qw(\n allowReorder attributeName attributeType autoReverse\n baseFrequency baseProfile calcMode clipPathUnits\n contentScriptType contentStyleType diffuseConstant edgeMode\n externalResourcesRequired filterRes filterUnits glyphRef\n gradientTransform gradientUnits kernelMatrix kernelUnitLength\n keyPoints keySplines keyTimes lengthAdjust limitingConeAngle\n markerHeight markerUnits markerWidth maskContentUnits\n maskUnits numOctaves pathLength patternContentUnits\n patternTransform patternUnits pointsAtX pointsAtY pointsAtZ\n preserveAlpha preserveAspectRatio primitiveUnits refX refY\n repeatCount repeatDur requiredExtensions requiredFeatures\n specularConstant specularExponent spreadMethod startOffset\n stdDeviation stitchTiles surfaceScale systemLanguage\n tableValues targetX targetY textLength viewBox viewTarget\n xChannelSelector yChannelSelector zoomAndPan\n);\n\nsub _to_attr_name ($key) {\n return 'class' if $key eq 'className';\n return 'for' if $key eq 'htmlFor';\n return $key if $SVG_CAMEL_CASE_ATTRS{$key};\n # camelCase \u2192 kebab-case, with a leading `-` for an initial\n # uppercase letter (JS-reference parity, even though that case\n # produces an HTML-invalid attribute name \u2014 same documented\n # behaviour as the Go adapter's `toAttrName`).\n my $out = $key;\n $out =~ s/([A-Z])/-\\L$1/g;\n return $out;\n}\n\nsub _html_escape ($value) {\n # HTML attribute-value escape for SSR string emission. The\n # spread bag's values reach the browser as part of a generated\n # `key=\"...\"` substring inside the rendered HTML, so the\n # escape set has to cover everything that could break either\n # the surrounding double-quoted attribute or the enclosing\n # tag: `&`, `<`, `>`, `\"`, and `'`. Matches Go's\n # `template.HTMLEscapeString` semantics byte-for-byte (using\n # `&#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 CORE::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 ? CORE::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 (CORE::length($key) > 2 && substr($key, 0, 2) eq 'on') {\n my $c = substr($key, 2, 1);\n next if CORE::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 && CORE::length $css;\n push @parts, qq{style=\"} . _html_escape($css) . qq{\"};\n next;\n }\n my $name = _to_attr_name($key);\n push @parts, $name . qq{=\"} . _html_escape($val) . qq{\"};\n }\n return '' unless @parts;\n # Mark the result raw so the calling template's `<%==` raw-emit\n # doesn't re-escape the already-escaped values (the Mojo backend\n # returns a Mojo::ByteStream).\n return $self->backend->mark_raw(CORE::join(' ', @parts));\n}\n\n1;\n__END__\n\n=encoding utf8\n\n=head1 NAME\n\nBarefootJS - Engine- and framework-agnostic server runtime for BarefootJS marked templates\n\n=head1 SYNOPSIS\n\n use BarefootJS;\n\n # A host injects a rendering backend (see BarefootJS::Backend::Xslate or\n # Mojolicious::Plugin::BarefootJS for shipping backends).\n my $bf = BarefootJS->new($context, { backend => $backend });\n\n # The compiled marked template calls the runtime as a `bf` object:\n # <: $bf.scope_attr() :> <: $bf.json($data) :> <: $bf.spread_attrs($h) :>\n\n=head1 DESCRIPTION\n\nBarefootJS compiles JSX/TSX into a marked template plus client JS. This module\nis the server-side runtime the marked templates call into at render time. It is\ndeliberately template-engine- and web-framework-agnostic: every operation that\ndepends on I<how> a template is rendered \u2014 JSON marshalling, raw-string marking,\nJSX-children materialisation, and named-template rendering \u2014 is delegated to a\npluggable C<backend>.\n\nThat design lets the one runtime drive any backend. Shipping backends:\n\n=over 4\n\n=item * L<BarefootJS::Backend::Xslate> \u2014 Text::Xslate (Kolon); runs under any PSGI/Plack app.\n\n=item * L<BarefootJS::Backend::Mojo> \u2014 Mojolicious (via L<Mojolicious::Plugin::BarefootJS>).\n\n=back\n\nThe core itself pulls in only core Perl modules (C<POSIX>, C<Scalar::Util>);\nno template engine or web framework is loaded unless a backend that needs one\nis used.\n\n=head1 SEE ALSO\n\nL<BarefootJS::Backend::Xslate>, L<Mojolicious::Plugin::BarefootJS>,\nL<https://github.com/piconic-ai/barefootjs>\n\n=head1 AUTHOR\n\nkobaken E<lt>kentafly88@gmail.comE<gt>\n\n=head1 LICENSE\n\nCopyright (c) 2025-present BarefootJS Contributors.\n\nThis library is free software; you can redistribute it and/or modify it under\nthe MIT License. See the F<LICENSE> file in the distribution for the full text.\n\n=cut\n";
23550
+ barefootBackendMojoPmSource = "package BarefootJS::Backend::Mojo;\nour $VERSION = \"0.8.0\";\nuse Mojo::Base -base, -signatures;\n\nuse Mojo::ByteStream qw(b);\nuse Mojo::JSON qw(to_json);\nuse Scalar::Util qw(weaken);\n\n# ---------------------------------------------------------------------------\n# Reference rendering backend (Mojolicious / Mojo::Template).\n# ---------------------------------------------------------------------------\n#\n# BarefootJS.pm holds all the template-engine-agnostic logic (the JS-compat\n# value helpers, array/string methods, hydration markers). Everything that is\n# specific to *how a template is rendered* \u2014 JSON marshalling, raw-string\n# marking, JSX-children materialisation, and named-template rendering \u2014 lives\n# behind this backend object so the same runtime can drive a different Perl\n# template engine (Text::Xslate, Template Toolkit, \u2026) without rewriting the\n# helper surface.\n#\n# A backend MUST implement:\n# - encode_json($data) -> string\n# - mark_raw($str) -> value the engine emits without escaping\n# - materialize($value) -> string (resolve a captured-children ref)\n# - render_named($name, $bf, \\%vars) -> string\n#\n# This Mojo implementation is the reference. To target another engine, write a\n# sibling backend (BarefootJS::Backend::Xslate, \u2026) implementing the same four\n# methods and pass it via `BarefootJS->new($c, { backend => $b })`.\n\n# The Mojolicious controller. Optional: the value-marshalling helpers\n# (`encode_json` / `mark_raw` / `materialize`) work without it; only\n# `render_named` reaches into the controller's renderer + stash.\nhas 'c';\n\n# Pluggable JSON encoder (#engine-abstraction). Defaults to\n# `Mojo::JSON::to_json`, which returns a *character* string (not bytes)\n# suitable for embedding in HTML output via `<%==` / Mojo::ByteStream.\n#\n# Override with any `sub ($data) { ... }` to swap in a faster XS encoder \u2014\n# e.g. `json_encoder => sub { Cpanel::JSON::XS->new->canonical->encode($_[0]) }`.\n# The pure-Perl JSON::PP fallback Mojo::JSON uses can be a hot spot for large\n# props payloads; the seam lets a host pick its own implementation without\n# touching the runtime.\nhas 'json_encoder' => sub { \\&to_json };\n\n# Hold the controller weakly for the same reason BarefootJS does: the\n# controller owns the bf instance (which owns this backend) via its stash,\n# so a strong back-reference would close a per-request cycle the refcount GC\n# can't reclaim. `render_named` only touches `$self->c` mid-render, while the\n# controller is still alive on the request stack.\nsub new ($class, %args) {\n my $self = $class->SUPER::new(%args);\n weaken($self->{c}) if $self->{c};\n return $self;\n}\n\nsub encode_json ($self, $data) {\n return $self->json_encoder->($data);\n}\n\n# Mark a string as already-safe so the template engine emits it verbatim\n# (no re-escaping). In Mojo this is a Mojo::ByteStream, which the calling\n# template's `<%==` raw-emit passes through unescaped.\nsub mark_raw ($self, $str) {\n return b($str);\n}\n\n# JSX children / async fallbacks arrive via Mojo's `begin %>...<% end`\n# capture, which produces a CODE ref returning a Mojo::ByteStream. Resolve\n# it to a string before embedding. Plain (already-rendered) strings pass\n# through unchanged.\nsub materialize ($self, $value) {\n return ref($value) eq 'CODE' ? $value->() : $value;\n}\n\n# Render a named template with `$child_bf` bound as the active runtime\n# instance for that render. The Mojo `bf` helper resolves the current\n# instance off `$c->stash->{'bf.instance'}`; swap it for the duration of\n# the nested render and restore it afterwards so sibling renders are\n# unaffected.\nsub render_named ($self, $template_name, $child_bf, $vars) {\n my $c = $self->c;\n my $prev = $c->stash->{'bf.instance'};\n $c->stash->{'bf.instance'} = $child_bf;\n my $html = $c->render_to_string(template => $template_name, %$vars);\n $c->stash->{'bf.instance'} = $prev;\n return $html;\n}\n\n1;\n";
23551
+ barefootPluginPmSource = "package Mojolicious::Plugin::BarefootJS;\nour $VERSION = \"0.8.0\";\nuse Mojo::Base 'Mojolicious::Plugin', -signatures;\n\nuse Mojo::File qw(path);\nuse Mojo::JSON qw(decode_json);\n\nuse BarefootJS;\n\n# Plugin entry point. Wires up:\n#\n# 1. The `bf` controller helper. Lazily instantiates one\n# BarefootJS object per request and stashes it under\n# `bf.instance`.\n#\n# 2. A `before_render` hook that, when the rendered template name\n# matches a top-level component in the build manifest, fills the\n# heavy boilerplate the user previously hand-rolled in `app.pl`:\n# generates the scope id, registers every UI-registry child\n# renderer from the manifest, and seeds the stash with each\n# template variable's static default (issue #1416).\n#\n# Configuration (all optional):\n# - manifest_path: absolute path to the `bf build`-emitted\n# `manifest.json`. Defaults to `<app->home>/dist/templates/manifest.json`.\n# Pass `undef` to disable manifest-driven auto-init entirely; the\n# bf helper is still installed and callers can drive everything\n# manually as before.\nsub register ($self, $app, $config = {}) {\n $app->helper(bf => sub ($c) {\n $c->stash->{'bf.instance'} //= BarefootJS->new($c, $config);\n });\n\n my $manifest = _load_manifest($app, $config);\n return unless $manifest;\n\n # Cache the set of UI-registry slot keys so we can answer\n # \"is this template name a child or a top-level page?\" with a\n # single hash lookup at render time. Top-level entries are\n # everything that isn't `__barefoot__` and doesn't match\n # `ui/<name>/index` \u2014 the same partition `register_components_from_manifest`\n # applies internally.\n my %is_child_entry;\n for my $entry_name (keys %$manifest) {\n next if $entry_name eq '__barefoot__';\n next unless $entry_name =~ m{^ui/[^/]+/index$};\n $is_child_entry{$entry_name} = 1;\n }\n\n $app->hook(before_render => sub ($c, $args) {\n my $template = $args->{template};\n return unless defined $template && length $template;\n my $entry = $manifest->{$template};\n return unless $entry;\n return if $is_child_entry{$template};\n # Idempotency guard for nested renders. A controller might\n # call `render_to_string` inside an action and then `render`\n # \u2014 without this we'd re-init `bf` on the second pass and\n # wipe the script registrations the first pass collected.\n return if $c->stash->{'bf.auto_init_done'};\n\n # Escape hatch for callers that wire `bf` up by hand (the\n # existing `render_component` helper in the showcase app does\n # this). If `_scope_id` is already set we treat the request as\n # \"manually managed\" and leave it alone \u2014 same outcome as\n # before the plugin gained auto-init.\n my $bf = $c->bf;\n if (defined $bf->_scope_id && length $bf->_scope_id) {\n $c->stash->{'bf.auto_init_done'} = 1;\n return;\n }\n $c->stash->{'bf.auto_init_done'} = 1;\n\n $bf->_scope_id($template . '_' . substr(rand() =~ s/^0\\.//r, 0, 6));\n $bf->register_components_from_manifest($manifest);\n\n # Seed each ssrDefault into the stash unless the caller has\n # already supplied a value for that key \u2014 callers always win.\n my $defaults = $entry->{ssrDefaults};\n if (ref($defaults) eq 'HASH') {\n for my $name (keys %$defaults) {\n next if exists $c->stash->{$name};\n my $d = $defaults->{$name};\n my $value = ref($d) eq 'HASH' ? $d->{value} : $d;\n $c->stash->{$name} = $value;\n }\n }\n });\n}\n\nsub _load_manifest ($app, $config) {\n return undef if exists $config->{manifest_path} && !defined $config->{manifest_path};\n my $manifest_path = $config->{manifest_path}\n // $app->home->child('dist/templates/manifest.json');\n my $file = path($manifest_path);\n return undef unless -r $file;\n my $manifest = eval { decode_json($file->slurp) };\n if ($@ || ref($manifest) ne 'HASH') {\n $app->log->warn(\"BarefootJS: cannot parse manifest at $file: $@\") if $@;\n return undef;\n }\n return $manifest;\n}\n\n1;\n__END__\n\n=encoding utf8\n\n=head1 NAME\n\nMojolicious::Plugin::BarefootJS - Mojolicious integration for BarefootJS\n\n=head1 SYNOPSIS\n\n # Mojolicious application\n $self->plugin('BarefootJS');\n\n # In a controller / template, the `bf` helper exposes a per-request\n # BarefootJS runtime backed by BarefootJS::Backend::Mojo.\n\n=head1 DESCRIPTION\n\nWires the L<BarefootJS> server runtime into L<Mojolicious>. It registers a\nC<bf> controller helper that lazily instantiates one BarefootJS object per\nrequest (rendering via L<BarefootJS::Backend::Mojo>), and supports rendering\ncompiled marked templates as Mojolicious templates.\n\nFor non-Mojolicious / PSGI hosts, see L<BarefootJS::Backend::Xslate>, which\ndrives the same runtime with Text::Xslate and no web framework.\n\n=head1 METHODS\n\nL<Mojolicious::Plugin::BarefootJS> inherits all methods from\nL<Mojolicious::Plugin> and implements the following new one.\n\n=head2 register\n\n $plugin->register(Mojolicious->new, \\%conf);\n\nRegisters the plugin (the C<bf> helper and supporting hooks) in a Mojolicious\napplication.\n\n=head1 SEE ALSO\n\nL<BarefootJS>, L<BarefootJS::Backend::Mojo>, L<BarefootJS::Backend::Xslate>,\nL<Mojolicious>, L<https://github.com/piconic-ai/barefootjs>\n\n=head1 AUTHOR\n\nkobaken E<lt>kentafly88@gmail.comE<gt>\n\n=head1 LICENSE\n\nCopyright (c) 2025-present BarefootJS Contributors.\n\nThis library is free software; you can redistribute it and/or modify it under\nthe MIT License. See the F<LICENSE> file in the distribution for the full text.\n\n=cut\n";
23257
23552
  barefootDevReloadPmSource = `package Mojolicious::Plugin::BarefootJS::DevReload;
23258
- our $VERSION = "0.01";
23553
+ our $VERSION = "0.8.0";
23259
23554
  use Mojo::Base 'Mojolicious::Plugin', -signatures;
23260
23555
 
23261
23556
  =head1 NAME
@@ -23286,21 +23581,12 @@ C<< enabled => 1 >> to force-enable.
23286
23581
  use Mojo::ByteStream qw(b);
23287
23582
  use Mojo::IOLoop;
23288
23583
  use File::Spec;
23584
+ use BarefootJS::DevReload ();
23289
23585
 
23290
- # Sentinel path contract with @barefootjs/cli (DEV_SENTINEL_SUBDIR /
23291
- # DEV_SENTINEL_FILENAME in packages/cli/src/lib/build.ts). Duplicated so this
23292
- # package avoids a runtime dep on the CLI \u2014 keep in sync with the CLI.
23293
- my $DEV_SUBDIR = '.dev';
23294
- my $BUILD_ID_FILE = 'build-id';
23295
- my $SCROLL_STORAGE_KEY = '__bf_devreload_scroll';
23296
-
23297
- # Heartbeat < any reasonable proxy/IOLoop idle timeout so a quiet connection
23298
- # doesn't get reaped between rebuilds.
23299
- my $HEARTBEAT_S = 5;
23300
-
23301
- # Polling instead of Linux::Inotify2 / Mac::FSEvents keeps the runtime
23302
- # dependency-free. Sub-second latency is imperceptible next to browser reload.
23303
- my $POLL_S = 0.5;
23586
+ # Engine-agnostic snippet, build-id reading, and timing constants are shared
23587
+ # with the PSGI/Plack path in BarefootJS::DevReload \u2014 one source of truth.
23588
+ my $HEARTBEAT_S = $BarefootJS::DevReload::HEARTBEAT_S;
23589
+ my $POLL_S = $BarefootJS::DevReload::POLL_S;
23304
23590
 
23305
23591
  sub register ($self, $app, $config = {}) {
23306
23592
  my $dist_dir = $config->{dist_dir} // 'dist';
@@ -23313,7 +23599,7 @@ sub register ($self, $app, $config = {}) {
23313
23599
  # on mode \u2014 it simply returns an empty ByteStream when disabled.
23314
23600
  $app->helper(bf_dev_snippet => sub ($c) {
23315
23601
  return b('') unless $enabled;
23316
- return b(_snippet($endpoint));
23602
+ return b(BarefootJS::DevReload->snippet($endpoint));
23317
23603
  });
23318
23604
 
23319
23605
  return unless $enabled;
@@ -23324,9 +23610,8 @@ sub register ($self, $app, $config = {}) {
23324
23610
  my $dist_abs = File::Spec->file_name_is_absolute($dist_dir)
23325
23611
  ? $dist_dir
23326
23612
  : $app->home->child($dist_dir)->to_string;
23327
- my $dev_dir = File::Spec->catdir($dist_abs, $DEV_SUBDIR);
23328
- my $build_id_path = File::Spec->catfile($dev_dir, $BUILD_ID_FILE);
23329
- mkdir $dev_dir unless -d $dev_dir;
23613
+ BarefootJS::DevReload->ensure_dev_dir($dist_abs);
23614
+ my $build_id_path = BarefootJS::DevReload->build_id_path($dist_abs);
23330
23615
 
23331
23616
  $app->routes->get($endpoint => sub ($c) {
23332
23617
  my $last_event_id = $c->req->headers->header('Last-Event-ID') // '';
@@ -23339,7 +23624,7 @@ sub register ($self, $app, $config = {}) {
23339
23624
 
23340
23625
  $c->write("retry: 1000\\n\\n");
23341
23626
 
23342
- my $initial_id = _read_build_id($build_id_path);
23627
+ my $initial_id = BarefootJS::DevReload->read_build_id($build_id_path);
23343
23628
  my $last_sent = '';
23344
23629
  if (length $initial_id) {
23345
23630
  $last_sent = $initial_id;
@@ -23362,7 +23647,7 @@ sub register ($self, $app, $config = {}) {
23362
23647
  $c->write(": hb\\n\\n");
23363
23648
  });
23364
23649
  $poll_id = Mojo::IOLoop->recurring($POLL_S => sub {
23365
- my $id = _read_build_id($build_id_path);
23650
+ my $id = BarefootJS::DevReload->read_build_id($build_id_path);
23366
23651
  return unless length $id;
23367
23652
  return if $id eq $last_sent;
23368
23653
  $last_sent = $id;
@@ -23373,37 +23658,6 @@ sub register ($self, $app, $config = {}) {
23373
23658
  return;
23374
23659
  }
23375
23660
 
23376
- sub _read_build_id ($path) {
23377
- return '' unless -f $path;
23378
- open my $fh, '<', $path or return '';
23379
- local $/;
23380
- my $content = <$fh>;
23381
- close $fh;
23382
- $content //= '';
23383
- $content =~ s/^\\s+|\\s+$//g;
23384
- return $content;
23385
- }
23386
-
23387
- sub _snippet ($endpoint) {
23388
- my $ep = _js_str($endpoint);
23389
- my $sk = _js_str($SCROLL_STORAGE_KEY);
23390
- # Small IIFE: EventSource subscriber + scrollY preservation. Idempotent
23391
- # across duplicate mounts (window.__bfDevReload guard).
23392
- return qq{<script>(function(){if(window.__bfDevReload)return;window.__bfDevReload=1;try{var s=sessionStorage.getItem($sk);if(s){sessionStorage.removeItem($sk);var y=parseInt(s,10);if(!isNaN(y)){var restore=function(){window.scrollTo(0,y)};if(document.readyState==='loading'){addEventListener('DOMContentLoaded',restore,{once:true})}else{restore()}}}}catch(e){}var es=new EventSource($ep);es.addEventListener('reload',function(){try{sessionStorage.setItem($sk,String(window.scrollY))}catch(e){}location.reload()});es.addEventListener('error',function(){})})();</script>};
23393
- }
23394
-
23395
- sub _js_str ($s) {
23396
- # Minimal JS string escape for the handful of characters that can appear
23397
- # in a URL path or storage key. Good enough for package-internal + trusted
23398
- # operator-supplied strings; never interpolate untrusted input here.
23399
- my $t = $s;
23400
- $t =~ s/\\\\/\\\\\\\\/g;
23401
- $t =~ s/"/\\\\"/g;
23402
- $t =~ s/\\n/\\\\n/g;
23403
- $t =~ s/\\r/\\\\r/g;
23404
- return qq{"$t"};
23405
- }
23406
-
23407
23661
  1;
23408
23662
  `;
23409
23663
  }
@@ -24667,7 +24921,7 @@ export default app
24667
24921
  HONO_RENDERER_TSX = `import { jsxRenderer } from 'hono/jsx-renderer'
24668
24922
  import { BfImportMap } from '@barefootjs/hono/app'
24669
24923
  import { BfScripts } from '@barefootjs/hono/scripts'
24670
- import manifest from './public/components/manifest.json'
24924
+ import manifest from './public/components/manifest.json' with { type: 'json' }
24671
24925
 
24672
24926
  declare module 'hono' {
24673
24927
  interface ContextRenderer {
@@ -24938,7 +25192,7 @@ import { BfImportMap, BfDevReload } from '@barefootjs/hono/app'
24938
25192
  import { BfScripts } from '@barefootjs/hono/scripts'
24939
25193
  import { readFileSync } from 'node:fs'
24940
25194
  import { resolve } from 'node:path'
24941
- import staticManifest from './dist/components/manifest.json'
25195
+ import staticManifest from './dist/components/manifest.json' with { type: 'json' }
24942
25196
  import { isDev } from './env'
24943
25197
 
24944
25198
  declare module 'hono' {
@@ -25629,8 +25883,8 @@ var init_exports = {};
25629
25883
  __export(init_exports, {
25630
25884
  run: () => run4
25631
25885
  });
25632
- import { existsSync as existsSync8, mkdirSync as mkdirSync3, writeFileSync as writeFileSync3 } from "fs";
25633
- import path10 from "path";
25886
+ import { existsSync as existsSync8, mkdirSync as mkdirSync3, writeFileSync as writeFileSync3 } from "node:fs";
25887
+ import path10 from "node:path";
25634
25888
  function parseFlags(args2) {
25635
25889
  const flags = {};
25636
25890
  for (let i = 0; i < args2.length; i++) {
@@ -25869,8 +26123,8 @@ var init_init = __esm({
25869
26123
  });
25870
26124
 
25871
26125
  // src/lib/resolve-source.ts
25872
- import { existsSync as existsSync9, readdirSync as readdirSync3 } from "fs";
25873
- import path11 from "path";
26126
+ import { existsSync as existsSync9, readdirSync as readdirSync3 } from "node:fs";
26127
+ import path11 from "node:path";
25874
26128
  function tryCandidate(candidate, searched) {
25875
26129
  searched.push(candidate);
25876
26130
  return existsSync9(candidate) ? candidate : null;
@@ -25956,8 +26210,8 @@ var init_resolve_source = __esm({
25956
26210
  });
25957
26211
 
25958
26212
  // src/lib/meta-loader.ts
25959
- import { readFileSync as readFileSync5, existsSync as existsSync10, readdirSync as readdirSync4 } from "fs";
25960
- import path12 from "path";
26213
+ import { readFileSync as readFileSync5, existsSync as existsSync10, readdirSync as readdirSync4 } from "node:fs";
26214
+ import path12 from "node:path";
25961
26215
  function loadIndex(metaDir) {
25962
26216
  const indexPath = path12.join(metaDir, "index.json");
25963
26217
  if (!existsSync10(indexPath)) {
@@ -26080,7 +26334,7 @@ __export(search_exports, {
26080
26334
  run: () => run5,
26081
26335
  search: () => search
26082
26336
  });
26083
- import path13 from "path";
26337
+ import path13 from "node:path";
26084
26338
  function search(query, index, source, coreDocs) {
26085
26339
  const q = query.toLowerCase();
26086
26340
  const aliasCategories = categoryAliases[q] || [];
@@ -26225,7 +26479,7 @@ var docs_exports = {};
26225
26479
  __export(docs_exports, {
26226
26480
  run: () => run6
26227
26481
  });
26228
- import path14 from "path";
26482
+ import path14 from "node:path";
26229
26483
  function printComponent(meta, jsonFlag2, banner) {
26230
26484
  if (jsonFlag2) {
26231
26485
  console.log(JSON.stringify(meta, null, 2));
@@ -26345,8 +26599,8 @@ var guide_exports = {};
26345
26599
  __export(guide_exports, {
26346
26600
  run: () => run7
26347
26601
  });
26348
- import path15 from "path";
26349
- import { existsSync as existsSync11 } from "fs";
26602
+ import path15 from "node:path";
26603
+ import { existsSync as existsSync11 } from "node:fs";
26350
26604
  import { fileURLToPath as fileURLToPath3 } from "node:url";
26351
26605
  function findDocsDir(ctx2) {
26352
26606
  const monorepoDocs = path15.join(ctx2.root, "docs/core");
@@ -27280,8 +27534,8 @@ var preview_exports = {};
27280
27534
  __export(preview_exports, {
27281
27535
  run: () => run8
27282
27536
  });
27283
- import { existsSync as existsSync17, readdirSync as readdirSync5, watch as fsWatch } from "fs";
27284
- import path16 from "path";
27537
+ import { existsSync as existsSync17, readdirSync as readdirSync5, watch as fsWatch } from "node:fs";
27538
+ import path16 from "node:path";
27285
27539
  function parseArgs(args2) {
27286
27540
  const out = { serve: false, watch: false, help: false, port: DEFAULT_PORT };
27287
27541
  for (let i = 0; i < args2.length; i++) {
@@ -27443,8 +27697,8 @@ __export(tokens_apply_exports, {
27443
27697
  resolveTokensCss: () => resolveTokensCss,
27444
27698
  run: () => run9
27445
27699
  });
27446
- import { existsSync as existsSync18, readFileSync as readFileSync7, writeFileSync as writeFileSync5 } from "fs";
27447
- import path17 from "path";
27700
+ import { existsSync as existsSync18, readFileSync as readFileSync7, writeFileSync as writeFileSync5 } from "node:fs";
27701
+ import path17 from "node:path";
27448
27702
  async function run9(args2, ctx2) {
27449
27703
  const url = args2[0];
27450
27704
  if (!url) {
@@ -27759,8 +28013,8 @@ var init_tokens2 = __esm({
27759
28013
  });
27760
28014
 
27761
28015
  // src/lib/scaffold.ts
27762
- import { readFileSync as readFileSync8, existsSync as existsSync19 } from "fs";
27763
- import path18 from "path";
28016
+ import { readFileSync as readFileSync8, existsSync as existsSync19 } from "node:fs";
28017
+ import path18 from "node:path";
27764
28018
  function loadMeta(metaDir, name) {
27765
28019
  const filePath = path18.join(metaDir, `${name}.json`);
27766
28020
  if (!existsSync19(filePath)) return null;
@@ -27915,8 +28169,8 @@ var gen_component_exports = {};
27915
28169
  __export(gen_component_exports, {
27916
28170
  run: () => run11
27917
28171
  });
27918
- import { writeFileSync as writeFileSync6, mkdirSync as mkdirSync4, existsSync as existsSync20 } from "fs";
27919
- import path19 from "path";
28172
+ import { writeFileSync as writeFileSync6, mkdirSync as mkdirSync4, existsSync as existsSync20 } from "node:fs";
28173
+ import path19 from "node:path";
27920
28174
  function run11(args2, ctx2) {
27921
28175
  if (args2.length < 1) {
27922
28176
  console.error("Usage: bf gen component <component-name> [use-component1] [use-component2] ...");
@@ -28180,8 +28434,8 @@ var init_parse_component = __esm({
28180
28434
  });
28181
28435
 
28182
28436
  // src/lib/test-template.ts
28183
- import { readFileSync as readFileSync9 } from "fs";
28184
- import path20 from "path";
28437
+ import { readFileSync as readFileSync9 } from "node:fs";
28438
+ import path20 from "node:path";
28185
28439
  function generateTestTemplate(componentPath, options = {}) {
28186
28440
  const importSource = options.importSource ?? "bun:test";
28187
28441
  const source = readFileSync9(componentPath, "utf-8");
@@ -28430,8 +28684,8 @@ var gen_test_exports = {};
28430
28684
  __export(gen_test_exports, {
28431
28685
  run: () => run12
28432
28686
  });
28433
- import { existsSync as existsSync21, writeFileSync as writeFileSync7 } from "fs";
28434
- import path21 from "path";
28687
+ import { existsSync as existsSync21, writeFileSync as writeFileSync7 } from "node:fs";
28688
+ import path21 from "node:path";
28435
28689
  function run12(args2, ctx2) {
28436
28690
  const positional = args2.filter((a) => !a.startsWith("-"));
28437
28691
  const flagSet = new Set(args2.filter((a) => a.startsWith("-")));
@@ -28485,8 +28739,8 @@ var gen_preview_exports = {};
28485
28739
  __export(gen_preview_exports, {
28486
28740
  run: () => run13
28487
28741
  });
28488
- import { existsSync as existsSync22, writeFileSync as writeFileSync8, mkdirSync as mkdirSync5 } from "fs";
28489
- import path22 from "path";
28742
+ import { existsSync as existsSync22, writeFileSync as writeFileSync8, mkdirSync as mkdirSync5 } from "node:fs";
28743
+ import path22 from "node:path";
28490
28744
  async function run13(args2, ctx2) {
28491
28745
  const force = args2.includes("--force");
28492
28746
  const name = args2.find((a) => !a.startsWith("--"));
@@ -28524,7 +28778,8 @@ var debug_graph_exports = {};
28524
28778
  __export(debug_graph_exports, {
28525
28779
  run: () => run14
28526
28780
  });
28527
- import { readFileSync as readFileSync10 } from "fs";
28781
+ import { readFileSync as readFileSync10 } from "node:fs";
28782
+ import "node:path";
28528
28783
  async function run14(args2, ctx2) {
28529
28784
  const componentName = args2[0];
28530
28785
  if (!componentName) {
@@ -28561,7 +28816,7 @@ var debug_trace_exports = {};
28561
28816
  __export(debug_trace_exports, {
28562
28817
  run: () => run15
28563
28818
  });
28564
- import { readFileSync as readFileSync11 } from "fs";
28819
+ import { readFileSync as readFileSync11 } from "node:fs";
28565
28820
  async function run15(args2, ctx2) {
28566
28821
  const componentName = args2[0];
28567
28822
  const targetName = args2[1];
@@ -28581,8 +28836,8 @@ async function run15(args2, ctx2) {
28581
28836
  }
28582
28837
  const source = readFileSync11(resolved.filePath, "utf-8");
28583
28838
  const graph = buildComponentGraph2(source, resolved.filePath, resolved.componentName);
28584
- const path23 = traceUpdatePath2(graph, targetName);
28585
- if (!path23) {
28839
+ const path24 = traceUpdatePath2(graph, targetName);
28840
+ if (!path24) {
28586
28841
  console.error(`Error: Signal or memo "${targetName}" not found in ${graph.componentName}.`);
28587
28842
  const available = [
28588
28843
  ...graph.signals.map((s) => s.name),
@@ -28594,9 +28849,9 @@ async function run15(args2, ctx2) {
28594
28849
  process.exit(1);
28595
28850
  }
28596
28851
  if (ctx2.jsonFlag) {
28597
- console.log(JSON.stringify(path23, null, 2));
28852
+ console.log(JSON.stringify(path24, null, 2));
28598
28853
  } else {
28599
- console.log(formatUpdatePath2(path23));
28854
+ console.log(formatUpdatePath2(path24));
28600
28855
  }
28601
28856
  }
28602
28857
  var init_debug_trace = __esm({
@@ -28611,7 +28866,7 @@ var debug_fallbacks_exports = {};
28611
28866
  __export(debug_fallbacks_exports, {
28612
28867
  run: () => run16
28613
28868
  });
28614
- import { readFileSync as readFileSync12 } from "fs";
28869
+ import { readFileSync as readFileSync12 } from "node:fs";
28615
28870
  async function run16(args2, ctx2) {
28616
28871
  const componentName = args2[0];
28617
28872
  if (!componentName) {
@@ -28670,7 +28925,7 @@ var debug_signals_exports = {};
28670
28925
  __export(debug_signals_exports, {
28671
28926
  run: () => run17
28672
28927
  });
28673
- import { readFileSync as readFileSync13 } from "fs";
28928
+ import { readFileSync as readFileSync13 } from "node:fs";
28674
28929
  async function run17(args2, ctx2) {
28675
28930
  const componentName = args2[0];
28676
28931
  if (!componentName) {
@@ -28710,7 +28965,7 @@ var debug_events_exports = {};
28710
28965
  __export(debug_events_exports, {
28711
28966
  run: () => run18
28712
28967
  });
28713
- import { readFileSync as readFileSync14 } from "fs";
28968
+ import { readFileSync as readFileSync14 } from "node:fs";
28714
28969
  async function run18(args2, ctx2) {
28715
28970
  const componentName = args2[0];
28716
28971
  if (!componentName) {
@@ -28747,7 +29002,7 @@ var debug_loops_exports = {};
28747
29002
  __export(debug_loops_exports, {
28748
29003
  run: () => run19
28749
29004
  });
28750
- import { readFileSync as readFileSync15 } from "fs";
29005
+ import { readFileSync as readFileSync15 } from "node:fs";
28751
29006
  async function run19(args2, ctx2) {
28752
29007
  const componentName = args2[0];
28753
29008
  if (!componentName) {
@@ -28784,7 +29039,7 @@ var debug_why_update_exports = {};
28784
29039
  __export(debug_why_update_exports, {
28785
29040
  run: () => run20
28786
29041
  });
28787
- import { readFileSync as readFileSync16 } from "fs";
29042
+ import { readFileSync as readFileSync16 } from "node:fs";
28788
29043
  async function run20(args2, ctx2) {
28789
29044
  const componentName = args2[0];
28790
29045
  const bindingLabel = args2[1];
@@ -28842,7 +29097,7 @@ var debug_summary_exports = {};
28842
29097
  __export(debug_summary_exports, {
28843
29098
  run: () => run21
28844
29099
  });
28845
- import { readFileSync as readFileSync17 } from "fs";
29100
+ import { readFileSync as readFileSync17 } from "node:fs";
28846
29101
  async function run21(args2, ctx2) {
28847
29102
  const componentName = args2[0];
28848
29103
  if (!componentName) {
@@ -28875,8 +29130,8 @@ var init_debug_summary = __esm({
28875
29130
  });
28876
29131
 
28877
29132
  // src/context.ts
28878
- import { existsSync as existsSync2 } from "fs";
28879
- import path from "path";
29133
+ import { existsSync as existsSync2 } from "node:fs";
29134
+ import path from "node:path";
28880
29135
  import { fileURLToPath } from "node:url";
28881
29136
 
28882
29137
  // src/config.ts
@@ -28893,9 +29148,9 @@ function findProjectConfig(startDir) {
28893
29148
  let dir = path.resolve(startDir);
28894
29149
  const { root: fsRoot } = path.parse(dir);
28895
29150
  while (true) {
28896
- const ts21 = path.join(dir, "barefoot.config.ts");
28897
- if (existsSync2(ts21)) {
28898
- return { dir, tsConfigPath: ts21 };
29151
+ const ts22 = path.join(dir, "barefoot.config.ts");
29152
+ if (existsSync2(ts22)) {
29153
+ return { dir, tsConfigPath: ts22 };
28899
29154
  }
28900
29155
  if (dir === fsRoot) return null;
28901
29156
  dir = path.dirname(dir);