@barefootjs/cli 0.8.0 → 0.9.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/docs/core/README.mdx +2 -2
- package/dist/docs/core/quick-start.mdx +1 -1
- package/dist/index.js +514 -218
- package/package.json +2 -2
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
|
|
8220
|
+
const path24 = `${prefix}[${index}]`;
|
|
8200
8221
|
if (ts11.isIdentifier(el.name)) {
|
|
8201
|
-
bindings.push({ name: el.name.text, path:
|
|
8222
|
+
bindings.push({ name: el.name.text, path: path24 });
|
|
8202
8223
|
} else {
|
|
8203
|
-
walk(el.name,
|
|
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
|
|
8267
|
+
const path24 = appendDotAccess(prefix, keyText);
|
|
8247
8268
|
if (ts11.isIdentifier(el.name)) {
|
|
8248
|
-
bindings.push({ name: el.name.text, path:
|
|
8269
|
+
bindings.push({ name: el.name.text, path: path24 });
|
|
8249
8270
|
} else {
|
|
8250
|
-
walk(el.name,
|
|
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
|
|
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(
|
|
8708
|
-
if (js !==
|
|
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
|
|
16983
|
-
|
|
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.
|
|
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
|
|
17283
|
-
|
|
17284
|
-
|
|
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
|
|
18464
|
-
if (
|
|
18465
|
-
const downstream = flattenUpdateTargets(
|
|
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(
|
|
18962
|
+
function formatUpdatePath(path24) {
|
|
18876
18963
|
const lines = [];
|
|
18877
|
-
lines.push(`${
|
|
18878
|
-
for (const entry of
|
|
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(
|
|
19466
|
-
return readFile(
|
|
19758
|
+
async function readText(path24) {
|
|
19759
|
+
return readFile(path24, "utf8");
|
|
19467
19760
|
}
|
|
19468
|
-
async function readBytes(
|
|
19469
|
-
const buf = await readFile(
|
|
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(
|
|
19473
|
-
await writeFile(
|
|
19765
|
+
async function writeText(path24, content) {
|
|
19766
|
+
await writeFile(path24, content, "utf8");
|
|
19474
19767
|
}
|
|
19475
|
-
async function writeBytes(
|
|
19768
|
+
async function writeBytes(path24, content) {
|
|
19476
19769
|
const bytes = content instanceof ArrayBuffer ? new Uint8Array(content) : content;
|
|
19477
|
-
await writeFile(
|
|
19770
|
+
await writeFile(path24, bytes);
|
|
19478
19771
|
}
|
|
19479
|
-
async function fileExists(
|
|
19772
|
+
async function fileExists(path24) {
|
|
19480
19773
|
try {
|
|
19481
|
-
await access(
|
|
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
|
|
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 (
|
|
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 =
|
|
19838
|
+
const sourceFile = ts20.createSourceFile(
|
|
19546
19839
|
"mod.ts",
|
|
19547
19840
|
source,
|
|
19548
|
-
|
|
19841
|
+
ts20.ScriptTarget.Latest,
|
|
19549
19842
|
/*setParents*/
|
|
19550
19843
|
false,
|
|
19551
|
-
|
|
19844
|
+
ts20.ScriptKind.TS
|
|
19552
19845
|
);
|
|
19553
19846
|
function hasExport(node) {
|
|
19554
|
-
if (!
|
|
19555
|
-
const mods =
|
|
19556
|
-
return mods?.some((m) => m.kind ===
|
|
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 (
|
|
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 (
|
|
19857
|
+
if (ts20.isBindingElement(el)) collectFromBindingName(el.name);
|
|
19565
19858
|
}
|
|
19566
19859
|
}
|
|
19567
19860
|
for (const stmt of sourceFile.statements) {
|
|
19568
|
-
if (
|
|
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 (
|
|
19865
|
+
} else if (ts20.isFunctionDeclaration(stmt) && hasExport(stmt) && stmt.name) {
|
|
19573
19866
|
names.add(stmt.name.text);
|
|
19574
|
-
} else if (
|
|
19867
|
+
} else if (ts20.isClassDeclaration(stmt) && hasExport(stmt) && stmt.name) {
|
|
19575
19868
|
names.add(stmt.name.text);
|
|
19576
|
-
} else if (
|
|
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 =
|
|
19880
|
+
const sourceFile = ts20.createSourceFile(
|
|
19588
19881
|
"check.tsx",
|
|
19589
19882
|
source,
|
|
19590
|
-
|
|
19883
|
+
ts20.ScriptTarget.Latest,
|
|
19591
19884
|
/*setParents*/
|
|
19592
19885
|
false,
|
|
19593
|
-
|
|
19886
|
+
ts20.ScriptKind.TSX
|
|
19594
19887
|
);
|
|
19595
19888
|
for (const stmt of sourceFile.statements) {
|
|
19596
|
-
if (!
|
|
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 =
|
|
19898
|
+
const sourceFile = ts20.createSourceFile(
|
|
19606
19899
|
"bundle.ts",
|
|
19607
19900
|
source,
|
|
19608
|
-
|
|
19901
|
+
ts20.ScriptTarget.Latest,
|
|
19609
19902
|
/*setParents*/
|
|
19610
19903
|
false,
|
|
19611
|
-
|
|
19904
|
+
ts20.ScriptKind.TS
|
|
19612
19905
|
);
|
|
19613
19906
|
function collectFromBindingName(name) {
|
|
19614
|
-
if (
|
|
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 (
|
|
19912
|
+
if (ts20.isBindingElement(el)) collectFromBindingName(el.name);
|
|
19620
19913
|
}
|
|
19621
19914
|
}
|
|
19622
19915
|
for (const stmt of sourceFile.statements) {
|
|
19623
|
-
if (
|
|
19916
|
+
if (ts20.isVariableStatement(stmt)) {
|
|
19624
19917
|
for (const d of stmt.declarationList.declarations) {
|
|
19625
19918
|
collectFromBindingName(d.name);
|
|
19626
19919
|
}
|
|
19627
|
-
} else if (
|
|
19920
|
+
} else if (ts20.isFunctionDeclaration(stmt) && stmt.name) {
|
|
19628
19921
|
names.add(stmt.name.text);
|
|
19629
|
-
} else if (
|
|
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 =
|
|
19929
|
+
const sourceFile = ts20.createSourceFile(
|
|
19637
19930
|
"body.ts",
|
|
19638
19931
|
body,
|
|
19639
|
-
|
|
19932
|
+
ts20.ScriptTarget.Latest,
|
|
19640
19933
|
/*setParents*/
|
|
19641
19934
|
false,
|
|
19642
|
-
|
|
19935
|
+
ts20.ScriptKind.TS
|
|
19643
19936
|
);
|
|
19644
19937
|
const spans = [];
|
|
19645
19938
|
const hoistedImports = [];
|
|
19646
19939
|
for (const stmt of sourceFile.statements) {
|
|
19647
|
-
if (
|
|
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 (
|
|
19652
|
-
const
|
|
19653
|
-
const isRelative =
|
|
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 (
|
|
19954
|
+
if (ts20.isExportDeclaration(stmt)) {
|
|
19662
19955
|
spans.push([stmt.getStart(sourceFile), stmt.getEnd()]);
|
|
19663
19956
|
continue;
|
|
19664
19957
|
}
|
|
19665
|
-
if (
|
|
19666
|
-
const exportKw = stmt.getChildren(sourceFile).find((c) => c.kind ===
|
|
19667
|
-
const defaultKw = stmt.getChildren(sourceFile).find((c) => c.kind ===
|
|
19668
|
-
const equalsKw = stmt.getChildren(sourceFile).find((c) => c.kind ===
|
|
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 (
|
|
19675
|
-
const mods =
|
|
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 ===
|
|
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 (
|
|
19788
|
-
if (
|
|
19789
|
-
if ((
|
|
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 (
|
|
19793
|
-
if (
|
|
19794
|
-
if (
|
|
19795
|
-
if (
|
|
19796
|
-
if (
|
|
19797
|
-
if (
|
|
19798
|
-
if (
|
|
19799
|
-
if (
|
|
19800
|
-
if (
|
|
19801
|
-
if (
|
|
19802
|
-
if (
|
|
19803
|
-
if (
|
|
19804
|
-
if (
|
|
19805
|
-
if (
|
|
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 =
|
|
20105
|
+
sf = ts20.createSourceFile(
|
|
19813
20106
|
"bundle.js",
|
|
19814
20107
|
bundleSource,
|
|
19815
|
-
|
|
20108
|
+
ts20.ScriptTarget.Latest,
|
|
19816
20109
|
/*setParents*/
|
|
19817
20110
|
true,
|
|
19818
|
-
|
|
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 (
|
|
20118
|
+
if (ts20.isIdentifier(node) && isValueReference(node)) {
|
|
19826
20119
|
if (!firstReference.has(node.text)) firstReference.set(node.text, node);
|
|
19827
20120
|
}
|
|
19828
|
-
|
|
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 =
|
|
20151
|
+
const sourceFile = ts20.createSourceFile(
|
|
19859
20152
|
"walk.js",
|
|
19860
20153
|
content,
|
|
19861
|
-
|
|
20154
|
+
ts20.ScriptTarget.Latest,
|
|
19862
20155
|
/*setParents*/
|
|
19863
20156
|
false,
|
|
19864
|
-
|
|
20157
|
+
ts20.ScriptKind.JS
|
|
19865
20158
|
);
|
|
19866
20159
|
const sites = [];
|
|
19867
20160
|
for (const stmt of sourceFile.statements) {
|
|
19868
|
-
if (!
|
|
19869
|
-
if (!
|
|
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(
|
|
19980
|
-
if (visited.has(
|
|
19981
|
-
visited.add(
|
|
19982
|
-
const mod = modules.get(
|
|
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
|
|
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
|
|
20093
|
-
if (!await fileExists(
|
|
20385
|
+
const path24 = resolve3(outDir, CACHE_FILENAME);
|
|
20386
|
+
if (!await fileExists(path24)) return null;
|
|
20094
20387
|
try {
|
|
20095
|
-
const parsed = JSON.parse(await readText(
|
|
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
|
|
20106
|
-
await writeText(
|
|
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(
|
|
20127
|
-
if (await fileExists(
|
|
20419
|
+
async function writeIfChanged(path24, content) {
|
|
20420
|
+
if (await fileExists(path24)) {
|
|
20128
20421
|
if (typeof content === "string") {
|
|
20129
|
-
const prev2 = await readText(
|
|
20422
|
+
const prev2 = await readText(path24);
|
|
20130
20423
|
if (prev2 === content) return false;
|
|
20131
|
-
await writeText(
|
|
20424
|
+
await writeText(path24, content);
|
|
20132
20425
|
return true;
|
|
20133
20426
|
}
|
|
20134
|
-
const prev = await readBytes(
|
|
20427
|
+
const prev = await readBytes(path24);
|
|
20135
20428
|
const next = toUint8Array(content);
|
|
20136
20429
|
if (equalBytes(prev, next)) return false;
|
|
20137
|
-
await writeBytes(
|
|
20430
|
+
await writeBytes(path24, next);
|
|
20138
20431
|
return true;
|
|
20139
20432
|
}
|
|
20140
20433
|
if (typeof content === "string") {
|
|
20141
|
-
await writeText(
|
|
20434
|
+
await writeText(path24, content);
|
|
20142
20435
|
} else {
|
|
20143
|
-
await writeBytes(
|
|
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
|
|
20199
|
-
if (!await fileExists(
|
|
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(
|
|
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
|
|
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(
|
|
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
|
|
20295
|
-
const existing = await fileExists(
|
|
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(
|
|
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
|
|
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 } =
|
|
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 =
|
|
21229
|
+
const sourceFile = ts21.createSourceFile(
|
|
20935
21230
|
"merge.js",
|
|
20936
21231
|
content,
|
|
20937
|
-
|
|
21232
|
+
ts21.ScriptTarget.Latest,
|
|
20938
21233
|
/*setParentNodes*/
|
|
20939
21234
|
true,
|
|
20940
|
-
|
|
21235
|
+
ts21.ScriptKind.JS
|
|
20941
21236
|
);
|
|
20942
21237
|
for (const stmt of sourceFile.statements) {
|
|
20943
|
-
if (
|
|
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 =
|
|
21247
|
+
const sourceFile = ts21.createSourceFile(
|
|
20953
21248
|
"client.js",
|
|
20954
21249
|
content,
|
|
20955
|
-
|
|
21250
|
+
ts21.ScriptTarget.Latest,
|
|
20956
21251
|
/*setParentNodes*/
|
|
20957
21252
|
true,
|
|
20958
|
-
|
|
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 (
|
|
21258
|
+
if (ts21.isImportDeclaration(node) || ts21.isExportDeclaration(node)) {
|
|
20964
21259
|
const ms = node.moduleSpecifier;
|
|
20965
|
-
if (ms &&
|
|
21260
|
+
if (ms && ts21.isStringLiteral(ms) && isBarefootClient(ms.text)) {
|
|
20966
21261
|
spans.push([ms.getStart(sourceFile), ms.getEnd()]);
|
|
20967
21262
|
}
|
|
20968
|
-
} else if (
|
|
21263
|
+
} else if (ts21.isCallExpression(node) && node.expression.kind === ts21.SyntaxKind.ImportKeyword) {
|
|
20969
21264
|
const arg = node.arguments[0];
|
|
20970
|
-
if (arg &&
|
|
21265
|
+
if (arg && ts21.isStringLiteral(arg) && isBarefootClient(arg.text)) {
|
|
20971
21266
|
spans.push([arg.getStart(sourceFile), arg.getEnd()]);
|
|
20972
21267
|
}
|
|
20973
21268
|
}
|
|
20974
|
-
|
|
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
|
|
21388
|
-
await writeIfChanged(
|
|
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;
|
|
@@ -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/\"/"/gr;\n push @parts, qq{bf-h=\"$h\"};\n }\n if (defined $mount && length $mount) {\n my $m = $mount =~ s/\"/"/gr;\n push @parts, qq{bf-m=\"$m\"};\n }\n unless ($self->_is_child) {\n push @parts, q{bf-r=\"\"};\n }\n return join(' ', @parts);\n}\n\nsub props_attr ($self) {\n my $props = $self->_props;\n return '' unless $props && %$props;\n # encode_json returns a character string (not bytes) for safe embedding\n # in templates (the Mojo backend uses Mojo::JSON::to_json).\n my $json = $self->backend->encode_json($props);\n return qq{ bf-p='$json'};\n}\n\n# ---------------------------------------------------------------------------\n# Comment Markers\n# ---------------------------------------------------------------------------\n\nsub comment ($self, $text) {\n return \"<!--bf-$text-->\";\n}\n\n# ---------------------------------------------------------------------------\n# JS-equivalent value stringification\n# ---------------------------------------------------------------------------\n\n# Map a Perl boolean-shaped value to the JS `String(bool)` form.\n# Used by the Mojo adapter when emitting reactive attribute bindings\n# whose JS source `isBooleanResultExpr` classified as boolean \u2014\n# a comparison (`count() > 0`), a logical negation (`!ok()`), or a\n# literal `true` / `false`. Perl's auto-stringification of those\n# expressions yields `''` / `1`; Hono and Go emit `'false'` / `'true'`.\n# Centralising the bool \u2192 string mapping here keeps the contract\n# testable and the template-emit syntax tidy\n# (`<%= bf->bool_str(...) %>` vs an inline ternary).\n#\n# Contract is boolean-only: callers must have classified the\n# expression as boolean-result before routing through this helper.\n# Non-boolean values reaching here will be Perl-truthy-coerced to\n# 'true' / 'false', which is generally wrong \u2014 non-boolean attribute\n# bindings stay on the plain `<%= expr %>` emit path and never reach\n# this function.\nsub bool_str ($self, $value) {\n return $value ? 'true' : 'false';\n}\n\nsub text_start ($self, $slot_id) {\n return \"<!--bf:$slot_id-->\";\n}\n\nsub text_end ($self) {\n return \"<!--/-->\";\n}\n\n# See spec/compiler.md \"Slot identity\" for the comment-scope wire format.\nsub scope_comment ($self) {\n my $scope_id = $self->_scope_id // '';\n my $host_segment = '';\n my $host = $self->_bf_parent;\n my $mount = $self->_bf_mount;\n if (defined $host && length $host) {\n $host_segment = \"|h=$host|m=\" . ($mount // '');\n }\n my $props_json = '';\n if ($self->_props && %{$self->_props}) {\n $props_json = '|' . $self->backend->encode_json($self->_props);\n }\n return \"<!--bf-scope:$scope_id$host_segment$props_json-->\";\n}\n\n# ---------------------------------------------------------------------------\n# Script Registration\n# ---------------------------------------------------------------------------\n\nsub register_script ($self, $path) {\n return if $self->_script_seen->{$path};\n $self->_script_seen->{$path} = 1;\n push @{$self->_scripts}, $path;\n}\n\n# ---------------------------------------------------------------------------\n# Child Component Rendering\n# ---------------------------------------------------------------------------\n# (`_child_renderers` accessor is generated by the minimal accessor base above.)\n\nsub register_child_renderer ($self, $name, $renderer) {\n $self->_child_renderers->{$name} = $renderer;\n}\n\nsub render_child ($self, $name, @args) {\n my $renderer = $self->_child_renderers->{$name};\n die \"No renderer registered for child component '$name'\" unless $renderer;\n # Accept both the Mojo list form \u2014 `bf->render_child($name, k => v, ...)`\n # \u2014 and the single-hashref form \u2014 `$bf.render_child($name, { k => v })`.\n # Template languages whose method calls can't splat a hash into positional\n # args (Text::Xslate Kolon, Template Toolkit) pass one hashref instead.\n my %props = (@args == 1 && ref $args[0] eq 'HASH') ? %{ $args[0] } : @args;\n # JSX children come in via the engine's children-capture mechanism\n # (Mojo's `begin %>...<% end`, which produces a CODE ref returning a\n # Mojo::ByteStream). Materialize it through the backend before handing\n # the props to the child renderer so the child template sees\n # `$children` as already-rendered HTML. Guard on `exists` so a\n # childless invocation (`bf->render_child('counter')`) doesn't gain a\n # spurious `children => undef` key \u2014 preserving the historical \"only\n # touch children when present\" behaviour.\n $props{children} = $self->backend->materialize($props{children})\n if exists $props{children};\n return $renderer->(\\%props);\n}\n\n# ---------------------------------------------------------------------------\n# Bulk registration from build manifest\n# ---------------------------------------------------------------------------\n#\n# `bf build` emits dist/templates/manifest.json describing every\n# component the page might invoke (Counter, ui/button/index, ...).\n# This helper walks that manifest and registers one child renderer per\n# UI registry entry \u2014 the path shape `ui/<name>/index` maps to the\n# `<name>` slot key Counter.html.ep and friends use via\n# `<%= bf->render_child('<name>', ...) %>`.\n#\n# Each manifest entry carries an `ssrDefaults` hash derived statically\n# from the component's JSX (prop destructure defaults + signal /\n# memo initial values, see packages/jsx/src/ssr-defaults.ts). The\n# child renderer seeds every template variable from that hash,\n# preferring the caller's matching prop where one exists. This\n# replaces the per-component `signal_init` callback that every\n# scaffold's `app.pl` used to hand-roll for items 1/3 of issue #1416.\n#\n# `signal_init` remains as an opt-in override for cases the static\n# extractor can't see through (e.g. signal initial values that\n# reference imported helpers). When supplied for a given slot key\n# it takes precedence over the manifest's `ssrDefaults` for that\n# child, allowing callers to mix manual overrides with auto-derived\n# defaults for siblings.\nsub register_components_from_manifest ($self, $manifest, %opts) {\n my $signal_inits = $opts{signal_init} // {};\n my $parent_scope = $self->_scope_id;\n # Weaken the parent capture so the child-renderer closures stored on\n # `$self->_child_renderers` don't keep `$self` alive (the direct\n # closure <-> parent cycle). The controller is reached through `$parent`\n # at call time rather than captured strongly here, so the closures hold\n # no strong reference to `$c` either \u2014 see the controller-cycle note in\n # `new`. `$parent` is always live whenever a closure runs (the closure is\n # stored on `$parent`, so `$parent` outlives every invocation).\n weaken(my $parent = $self);\n\n for my $entry_name (keys %$manifest) {\n # `__barefoot__` is the runtime entry, not a component.\n next if $entry_name eq '__barefoot__';\n # Only UI registry components (path shape `ui/<name>/index`)\n # become child renderers; top-level page components are the\n # render target rather than a child.\n next unless $entry_name =~ m{^ui/([^/]+)/index$};\n my $slot_key = $1;\n my $marked = $manifest->{$entry_name}{markedTemplate} // '';\n next unless $marked;\n # `templates/ui/button/index.html.ep` \u2192 `ui/button/index`\n my $template_name = $marked;\n $template_name =~ s{^templates/}{};\n $template_name =~ s{\\.html\\.ep$}{};\n\n my $signal_init = $signal_inits->{$slot_key};\n my $manifest_defaults = $manifest->{$entry_name}{ssrDefaults};\n $self->register_child_renderer($slot_key, sub {\n my ($props) = @_;\n # Child shares the parent's backend so nested renders go\n # through the same engine + controller (and inherit any\n # injected json_encoder). The controller is fetched via the weak\n # `$parent` at call time \u2014 never captured strongly \u2014 so the\n # closure adds no edge to the per-request reference cycle.\n my $child_bf = BarefootJS->new($parent->c, { backend => $parent->backend });\n my $slot_id = delete $props->{_bf_slot};\n $child_bf->_scope_id(\n $slot_id ? $parent_scope . '_' . $slot_id\n : $template_name . '_' . substr(rand() =~ s/^0\\.//r, 0, 6)\n );\n $child_bf->_is_child(1);\n # (#1249) Slot identity: host scope + slot id. Emitted as\n # bf-h / bf-m attributes by hydration_attrs.\n if ($slot_id) {\n $child_bf->_bf_parent($parent_scope);\n $child_bf->_bf_mount($slot_id);\n }\n $child_bf->_scripts($parent->_scripts);\n $child_bf->_script_seen($parent->_script_seen);\n\n my %extra;\n if ($signal_init) {\n %extra = $signal_init->($props);\n } elsif ($manifest_defaults) {\n %extra = _derive_stash_from_defaults($manifest_defaults, $props);\n }\n\n # Render the child template with $child_bf bound as the active\n # instance for the nested render. The backend owns the\n # engine-specific binding + restore (stash juggle for Mojo).\n my $html = $parent->backend->render_named(\n $template_name, $child_bf, { %$props, %extra },\n );\n chomp $html;\n return $html;\n });\n }\n}\n\n# Derive template-stash kvs from a manifest entry's `ssrDefaults`\n# section. Each entry shape:\n# { value => <static-fallback>, propName => <prop>, isRestProps => bool }\n# For `isRestProps`, the rest bag passes through unchanged (or the\n# static `{}` if the caller didn't supply one). For ordinary entries\n# the caller's `$props->{propName}` wins when defined, otherwise the\n# static `value` does. `propName`-less entries (signal / memo locals)\n# always use the static value \u2014 the caller cannot override them.\nsub _derive_stash_from_defaults ($defaults, $props) {\n my %extra;\n for my $name (keys %$defaults) {\n my $d = $defaults->{$name};\n if (ref($d) ne 'HASH') {\n $extra{$name} = $d;\n next;\n }\n if ($d->{isRestProps}) {\n $extra{$name} = exists $props->{$name} ? $props->{$name} : $d->{value};\n next;\n }\n my $prop_name = $d->{propName};\n if (defined $prop_name && exists $props->{$prop_name} && defined $props->{$prop_name}) {\n $extra{$name} = $props->{$prop_name};\n } else {\n $extra{$name} = $d->{value};\n }\n }\n return %extra;\n}\n\n# ---------------------------------------------------------------------------\n# Script Output\n# ---------------------------------------------------------------------------\n\nsub scripts ($self) {\n my @tags;\n for my $path (@{$self->_scripts}) {\n push @tags, qq{<script type=\"module\" src=\"$path\"></script>};\n }\n return join(\"\\n\", @tags);\n}\n\n# ---------------------------------------------------------------------------\n# Streaming SSR (Out-of-Order)\n# ---------------------------------------------------------------------------\n\nsub streaming_bootstrap ($self) {\n return q{<script>(function(){function s(id){var a=document.querySelector('[bf-async=\"'+id+'\"]');var t=document.querySelector('template[bf-async-resolve=\"'+id+'\"]');if(!a||!t)return;a.replaceChildren(t.content.cloneNode(true));a.removeAttribute('bf-async');t.remove();requestAnimationFrame(function(){if(window.__bf_hydrate)window.__bf_hydrate()})};window.__bf_swap=s})()</script>};\n}\n\nsub async_boundary ($self, $id, $fallback_html) {\n # The fallback comes in via Mojo `begin %>...<% end` capture (see\n # MojoAdapter::renderAsync), which produces a CODE ref returning a\n # Mojo::ByteStream. Materialize it through the backend so the rendered\n # HTML embeds in the placeholder rather than the CODE ref's\n # stringification.\n $fallback_html = $self->backend->materialize($fallback_html);\n return qq{<div bf-async=\"$id\">$fallback_html</div>};\n}\n\nsub async_resolve ($self, $id, $content_html) {\n return qq{<template bf-async-resolve=\"$id\">$content_html</template><script>__bf_swap(\"$id\")</script>};\n}\n\n# ---------------------------------------------------------------------------\n# JS-compat callees (#1189) \u2014 invoked from generated Mojo templates as\n# <%= bf->json($val) %>, <%= bf->floor($val) %>, etc. The MojoAdapter's\n# `templatePrimitives` registry emits these helper calls in place of the\n# corresponding JS callees (`JSON.stringify`, `Math.floor`, \u2026) so the SSR\n# template can render value-equivalent output without a JS engine.\n#\n# Failure policy mirrors the Go adapter (#1188): user-data marshalling\n# (json) bubbles errors so Mojolicious aborts loudly on cycles /\n# unsupported values rather than silently producing an empty payload.\n# Numeric coercion follows JS semantics (NaN propagates as the special\n# string 'NaN'; non-numeric input returns 'NaN' rather than 0). Strings\n# always coerce to a string representation.\n# ---------------------------------------------------------------------------\n\nsub json ($self, $value) {\n # Mojo::JSON::to_json returns a character string (not bytes), suitable\n # for embedding in HTML output via Mojo::ByteStream / `<%==`.\n #\n # Documented divergence from JS: JS distinguishes `null` (renders as\n # \"null\") from `undefined` (`JSON.stringify(undefined)` returns the\n # JS value `undefined`, not a string). Perl has no such distinction\n # \u2014 both map to `undef`. We choose the `null` rendering for SSR\n # ergonomics: an unset prop becomes the string \"null\" rather than\n # the literal text \"undefined\" or an empty attribute. Matches the\n # `null` case of JS exactly; diverges from the `undefined` case.\n return $self->backend->encode_json($value);\n}\n\nsub string ($self, $value) {\n # JS `String(v)` mirror. `undef` renders as the empty string here so\n # an unset prop doesn't surface as a literal \"undefined\" / \"null\"\n # in user-facing HTML \u2014 same divergence the Go adapter documents\n # for `bf_string`.\n return defined $value ? \"$value\" : '';\n}\n\nsub number ($self, $value) {\n # JS `Number(v)` mirror. Numeric coerces via Perl's implicit\n # numeric context; non-numeric / undef yield real numeric NaN\n # (`'nan' + 0`) so downstream arithmetic propagates correctly\n # (`Math.floor(NaN) === NaN`). Returning the literal string\n # \"NaN\" would conflate the user-passing-the-string-\"NaN\" case\n # with the parse-failure case, and break NaN detection in\n # downstream helpers.\n return 0 + 'nan' unless defined $value;\n return $value + 0 if looks_like_number($value);\n return 0 + 'nan';\n}\n\n# NaN is the only float for which `$x != $x` holds. Used as the\n# portable sentinel check in floor/ceil/round.\nsub _is_nan { my $n = shift; return $n != $n }\n\nsub floor ($self, $value) {\n my $n = $self->number($value);\n return $n if _is_nan($n);\n return POSIX::floor($n);\n}\n\nsub ceil ($self, $value) {\n my $n = $self->number($value);\n return $n if _is_nan($n);\n return POSIX::ceil($n);\n}\n\nsub round ($self, $value) {\n my $n = $self->number($value);\n return $n if _is_nan($n);\n # POSIX has no `round`. JS `Math.round` rounds half toward\n # +Infinity (so `Math.round(-1.5) === -1`, not -2). `floor(n\n # + 0.5)` reproduces that for both signs.\n return POSIX::floor($n + 0.5);\n}\n\n# ---------------------------------------------------------------------------\n# Array / String method helpers (#1448 Tier A)\n# ---------------------------------------------------------------------------\n#\n# `Array.prototype.includes(x)` and `String.prototype.includes(sub)`\n# share a method name in JS; the JSX parser can't tell the two\n# receiver shapes apart without TS type inference, so both lower to\n# the same IR node (`array-method` / method `includes`). This helper\n# dispatches at the Perl level via `ref()`:\n# - ARRAY ref: scan elements with `eq`; one defined-vs-undef\n# hop matches JS's `===` for null/undefined.\n# - scalar: `index($recv, $sub) != -1`, with both args\n# coerced through `// ''` so an undef receiver /\n# needle doesn't trip Perl's substr warning.\n# Anything else (HASH ref, code ref) returns false \u2014 matches the\n# JS semantic where `.includes` is only defined on Array /\n# TypedArray / String.\n\nsub includes ($self, $recv, $elem) {\n if (ref($recv) eq 'ARRAY') {\n for my $item (@$recv) {\n if (!defined $item) {\n return 1 if !defined $elem;\n next;\n }\n return 1 if defined $elem && $item eq $elem;\n }\n return 0;\n }\n return 0 if ref($recv);\n return index($recv // '', $elem // '') != -1 ? 1 : 0;\n}\n\n# `Array.prototype.indexOf(x)` / `Array.prototype.lastIndexOf(x)`\n# value-equality search (#1448 Tier A). Returns the 0-based position\n# of the first / last matching element, or -1 if not found.\n# Non-array receivers return -1 \u2014 matches the JS semantic that\n# `.indexOf` / `.lastIndexOf` are only defined on Array / TypedArray.\n# (The string-position `indexOf` form isn't in Tier A; if it lands\n# later the helper can grow a ref()-dispatch branch like `includes`.)\n\nsub _array_index_of ($recv, $elem, $reverse) {\n return -1 unless ref($recv) eq 'ARRAY';\n my @indices = $reverse ? (reverse 0 .. $#{$recv}) : (0 .. $#{$recv});\n for my $i (@indices) {\n my $item = $recv->[$i];\n if (!defined $item) {\n return $i if !defined $elem;\n next;\n }\n return $i if defined $elem && $item eq $elem;\n }\n return -1;\n}\n\nsub index_of ($self, $recv, $elem) {\n return _array_index_of($recv, $elem, 0);\n}\n\nsub last_index_of ($self, $recv, $elem) {\n return _array_index_of($recv, $elem, 1);\n}\n\n# `Array.prototype.at(i)` \u2014 supports negative indices (`.at(-1)` is\n# the last element); out-of-bounds returns undef (which Mojo's\n# auto-escape renders as the empty string, matching JS's `undefined`).\n# Non-array receivers return undef. Matches the Go `bf_at` arithmetic\n# (`length + i` for i < 0) so adapter output stays symmetric.\n\nsub at ($self, $recv, $i) {\n return undef unless ref($recv) eq 'ARRAY';\n return undef if !defined $i;\n my $len = scalar @$recv;\n return undef if $len == 0;\n my $idx = $i < 0 ? $len + $i : $i;\n return undef if $idx < 0 || $idx >= $len;\n return $recv->[$idx];\n}\n\n# `Array.prototype.concat(other)` \u2014 merges two arrays in order\n# into a new ARRAY ref. Non-array operands collapse to empty\n# (matches the Go `bf_concat` semantic so cross-adapter output\n# stays symmetric; differs from JS where a non-Array argument\n# with `Symbol.isConcatSpreadable` would be spread, a behaviour\n# the template-language path never observes).\n\nsub concat ($self, $a, $b) {\n my @out;\n push @out, @$a if ref($a) eq 'ARRAY';\n push @out, @$b if ref($b) eq 'ARRAY';\n return \\@out;\n}\n\n# `Array.prototype.slice(start, end?)` \u2014 carves out a sub-range\n# into a new ARRAY ref. Mirrors the Go `bf_slice` arithmetic so\n# adapter output stays symmetric:\n# - start < 0 \u2192 length + start (e.g. -1 = last index)\n# - end < 0 \u2192 length + end\n# - start < 0 after clamp \u2192 0\n# - end > length \u2192 length\n# - start >= end \u2192 empty\n# - end undef \u2192 \"to length\"\n# Non-array receivers return an empty ARRAY ref.\n\nsub slice ($self, $recv, $start, $end) {\n return [] unless ref($recv) eq 'ARRAY';\n my $len = scalar @$recv;\n return [] if $len == 0;\n\n my $s = $start // 0;\n $s = $len + $s if $s < 0;\n $s = 0 if $s < 0;\n $s = $len if $s > $len;\n\n my $e = defined $end ? $end : $len;\n $e = $len + $e if $e < 0;\n $e = 0 if $e < 0;\n $e = $len if $e > $len;\n\n return [] if $s >= $e;\n return [ @{$recv}[$s .. $e - 1] ];\n}\n\n# `Array.prototype.reverse()` / `Array.prototype.toReversed()` \u2014\n# both shapes share this lowering. SSR templates render a snapshot\n# of state, so JS's mutate-receiver (`reverse`) vs\n# return-new-array (`toReversed`) distinction has no template-\n# level meaning. Always returns a new ARRAY ref to keep callers\n# safe from accidental aliasing. Non-array receivers return an\n# empty ARRAY ref.\n\nsub reverse ($self, $recv) {\n return [] unless ref($recv) eq 'ARRAY';\n return [ reverse @$recv ];\n}\n\n# `Array.prototype.flat(depth?)` (#1448 Tier C) \u2014 flatten nested ARRAY\n# refs `$depth` levels deep. A `$depth` of -1 is the `Infinity` sentinel\n# (flatten fully); 0 returns a shallow copy. Non-ARRAY elements are kept\n# as-is (JS only flattens nested arrays). Non-ARRAY receiver \u2192 [].\nsub flat ($self, $recv, $depth = 1) {\n return [] unless ref($recv) eq 'ARRAY';\n my @out;\n for my $el (@$recv) {\n if ($depth != 0 && ref($el) eq 'ARRAY') {\n my $next = $depth > 0 ? $depth - 1 : $depth;\n push @out, @{ $self->flat($el, $next) };\n }\n else {\n push @out, $el;\n }\n }\n return \\@out;\n}\n\n# `Array.prototype.flatMap(fn)` value-returning field projection\n# (#1448 Tier C) \u2014 map each element through a self / field projection,\n# then flatten one level. `field` reads a HASH-ref key (the raw JS prop\n# name, as `bf->reduce` does); a projected non-ARRAY value is kept as-is\n# (flatMap = map + flat(1)). Non-ARRAY receiver \u2192 [].\nsub flat_map ($self, $recv, $key_kind, $key) {\n return [] unless ref($recv) eq 'ARRAY';\n my @projected;\n for my $el (@$recv) {\n if ($key_kind eq 'field') {\n # JS `i => i.field` on a non-object yields `undefined`, not the\n # element itself \u2014 push `undef` so a scalar element doesn't leak\n # into the output (matches Go's `getFieldValue` returning nil).\n push @projected, ref($el) eq 'HASH' ? $el->{$key} : undef;\n }\n else {\n push @projected, $el;\n }\n }\n return $self->flat(\\@projected, 1);\n}\n\n# `Array.prototype.flatMap(i => [i.a, i.b])` \u2014 array-literal tuple\n# projection (#1448 Tier C). Each `@specs` entry is a [kind, key] arrayref\n# (['self', ''] or ['field', 'a']). For each element, every leaf's value\n# is appended in order. flat(1) removes only the literal wrapper, so an\n# array-valued leaf is appended verbatim (no spread) \u2014 i.e. just append\n# each leaf. A non-HASH element under a `field` leaf yields undef (JS\n# `i.field` on a non-object). Non-ARRAY receiver \u2192 [].\nsub flat_map_tuple ($self, $recv, @specs) {\n return [] unless ref($recv) eq 'ARRAY';\n my @out;\n for my $el (@$recv) {\n for my $spec (@specs) {\n my ($kind, $key) = @$spec;\n if ($kind eq 'field') {\n push @out, ref($el) eq 'HASH' ? $el->{$key} : undef;\n }\n else {\n push @out, $el;\n }\n }\n }\n return \\@out;\n}\n\n# `String.prototype.trim()` \u2014 strip leading + trailing whitespace.\n# JS's `String.prototype.trim` matches `\\s` in the Unicode sense\n# (any whitespace including non-breaking space U+00A0); Perl's `\\s`\n# inside a regex with `/u` flag is the same. Undef receivers return\n# the empty string (matches JS's `String(undefined).trim()` which\n# would be \"undefined\" \u2192 \"undefined\", but in our template context\n# undef commonly means \"missing prop\"; rendering the empty string\n# is the safer choice and mirrors the JS-compat divergence we\n# already document for `bf->string(undef) === \"\"`).\n\nsub trim ($self, $recv) {\n return '' unless defined $recv;\n return '' if ref($recv);\n my $s = \"$recv\";\n $s =~ s/^\\s+|\\s+$//gu;\n return $s;\n}\n\n# `String.prototype.split(sep)` (#1448 Tier B) \u2014 string \u2192 ARRAY ref.\n#\n# Two JS-parity wrinkles drive the helper (a bare `split` emit would\n# diverge from both JS and Go):\n#\n# * Perl's `split` treats its first argument as a *regex*, so a\n# separator like '.' or '|' would match far too much. We\n# `quotemeta` it to force literal-string matching, mirroring JS's\n# string-separator semantics (the regex-separator form stays\n# refused upstream \u2014 see the parser arm).\n# * Perl's `split` drops trailing empty fields by default; JS keeps\n# them (`\"a,\".split(\",\")` is `[\"a\", \"\"]`). Passing the `-1` limit\n# preserves them, matching JS and Go's `strings.Split`.\n#\n# An empty separator splits into individual characters (JS + Go agree).\n# Undef receiver renders as the single-element `['']` \u2014 the same\n# \"missing prop \u2192 empty string\" convention `bf->trim` uses.\n\nsub split ($self, $recv, $sep = undef, $limit = undef) {\n my $s = defined $recv && !ref($recv) ? \"$recv\" : '';\n\n my @parts;\n if (!defined $sep) {\n # No separator \u2192 the whole string in a single-element array\n # (matches JS `\"x\".split()` / `.split(undefined)`).\n @parts = ($s);\n }\n elsif (\"$sep\" eq '') {\n # Empty separator \u2192 individual characters. No `-1` limit here:\n # on an empty pattern Perl's `split` with `-1` appends a spurious\n # trailing empty field (\"abc\" \u2192 'a','b','c',''), which JS/Go don't.\n @parts = split //, $s;\n }\n elsif ($s eq '') {\n # Empty input with a non-empty separator: JS `\"\".split(\",\")` is\n # `[\"\"]` and Go's `strings.Split(\"\", \",\")` is `[\"\"]`, but Perl's\n # `split /,/, ''` returns the empty list \u2014 special-case for parity.\n @parts = ('');\n }\n else {\n # `quotemeta` forces literal-string matching (JS string-separator\n # semantics); the `-1` keeps trailing empty fields (JS keeps them,\n # Perl's bare `split` drops them).\n my $q = quotemeta(\"$sep\");\n @parts = split /$q/, $s, -1;\n }\n\n # Optional `limit` caps the number of pieces (JS `split(sep, limit)`).\n # 0 \u2192 empty; a negative limit keeps all (JS ToUint32 wrap makes it\n # effectively unbounded) \u2014 both match Go's `bf_split`.\n if (defined $limit) {\n my $n = int($limit);\n if ($n == 0) { @parts = () }\n elsif ($n > 0 && $n < scalar @parts) { @parts = @parts[0 .. $n - 1] }\n }\n\n return [@parts];\n}\n\n# `String.prototype.startsWith(prefix, position?)` (#1448 Tier B) \u2014\n# string \u2192 boolean (1 / 0). `substr`-anchored literal comparison mirrors\n# Go's `strings.HasPrefix`. An empty prefix is always true (JS parity);\n# undef / non-string receivers coerce to the empty string first. The\n# optional `position` re-anchors the test (clamped to `[0, length]`),\n# matching JS `\"abc\".startsWith(\"b\", 1)`.\n\nsub starts_with ($self, $recv, $prefix, $position = undef) {\n my $s = defined $recv && !ref($recv) ? \"$recv\" : '';\n my $p = defined $prefix ? \"$prefix\" : '';\n if (defined $position) {\n my $n = int($position);\n $n = 0 if $n < 0;\n $n = length($s) if $n > length($s);\n $s = substr($s, $n);\n }\n return substr($s, 0, length $p) eq $p ? 1 : 0;\n}\n\n# `String.prototype.endsWith(suffix, endPosition?)` (#1448 Tier B) \u2014\n# string \u2192 boolean (1 / 0). Mirrors Go's `strings.HasSuffix`. An empty\n# suffix is always true (JS parity); a suffix longer than the string is\n# false. `substr($s, -length $x)` would mis-read the whole string when\n# `length $x == 0`, so that case short-circuits. The optional\n# `endPosition` treats the string as if it were only that many chars\n# long (clamped to `[0, length]`), matching JS `\"abc\".endsWith(\"b\", 2)`.\n\nsub ends_with ($self, $recv, $suffix, $end_position = undef) {\n my $s = defined $recv && !ref($recv) ? \"$recv\" : '';\n my $x = defined $suffix ? \"$suffix\" : '';\n if (defined $end_position) {\n my $e = int($end_position);\n $e = 0 if $e < 0;\n $e = length($s) if $e > length($s);\n $s = substr($s, 0, $e);\n }\n return 1 if $x eq '';\n return 0 if length($s) < length($x);\n return substr($s, -length $x) eq $x ? 1 : 0;\n}\n\n# `String.prototype.replace(pattern, replacement)` \u2014 string-pattern\n# form only (#1448 Tier B), replacing the FIRST occurrence (JS string-\n# pattern semantics). Spliced via index/substr rather than `s///` so\n# BOTH the pattern and the replacement are literal: no Perl regex\n# metacharacters in the pattern and no `$1` / `$&` interpolation in the\n# replacement. Go's `bf_replace` (strings.Replace, n=1) treats the\n# replacement literally too, so the two adapters stay byte-equal \u2014 this\n# diverges from JS only for replacement strings containing `$`-patterns\n# (rare in template position). An empty pattern inserts the replacement\n# at the front (`\"abc\".replace(\"\", \"X\")` \u2192 \"Xabc\"), matching JS + Go.\n\nsub replace ($self, $recv, $pattern, $replacement) {\n my $s = defined $recv && !ref($recv) ? \"$recv\" : '';\n my $o = defined $pattern ? \"$pattern\" : '';\n my $n = defined $replacement ? \"$replacement\" : '';\n return $n . $s if $o eq '';\n my $i = index($s, $o);\n return $s if $i < 0;\n return substr($s, 0, $i) . $n . substr($s, $i + length($o));\n}\n\n# `String.prototype.repeat(n)` \u2014 the receiver concatenated n times\n# (#1448 Tier B), via Perl's `x` operator. JS throws RangeError for a\n# negative count, but SSR templates degrade to the empty string rather\n# than dying mid-render, so a count <= 0 returns \"\" (Go's `bf_repeat`\n# applies the same clamp). The count is truncated toward zero\n# (`int`), matching JS's ToIntegerOrInfinity on `\"a\".repeat(3.7)`.\n\nsub repeat ($self, $recv, $count) {\n my $s = defined $recv && !ref($recv) ? \"$recv\" : '';\n my $n = defined $count ? int($count) : 0;\n return $n <= 0 ? '' : $s x $n;\n}\n\n# `String.prototype.padStart` / `padEnd` (#1448 Tier B) \u2014 pad the\n# receiver to `$target` characters with `$pad` (default a single space)\n# repeated and truncated to fill, prepended or appended. Length is\n# measured in characters (Perl `length`), matching Go's rune-based\n# `bf_pad_*` \u2014 diverges from JS's UTF-16-unit length only for\n# astral-plane input. An empty pad, or a receiver already >= `$target`,\n# returns the receiver unchanged (JS parity). The `$target` is\n# truncated toward zero (JS ToLength on the first arg).\n\nsub _pad ($s, $target, $pad, $at_start) {\n $pad = ' ' unless defined $pad;\n $pad = \"$pad\";\n return $s if $pad eq '';\n my $len = length $s;\n my $t = int($target // 0);\n return $s if $len >= $t;\n my $need = $t - $len;\n # Repeat enough copies to cover $need, then trim to exactly $need.\n my $fill = substr($pad x (int($need / length($pad)) + 1), 0, $need);\n return $at_start ? $fill . $s : $s . $fill;\n}\n\nsub pad_start ($self, $recv, $target, $pad = undef) {\n my $s = defined $recv && !ref($recv) ? \"$recv\" : '';\n return _pad($s, $target, $pad, 1);\n}\n\nsub pad_end ($self, $recv, $target, $pad = undef) {\n my $s = defined $recv && !ref($recv) ? \"$recv\" : '';\n return _pad($s, $target, $pad, 0);\n}\n\n# `Array.prototype.sort(cmp)` / `Array.prototype.toSorted(cmp)`\n# lowering (#1448 Tier B). Non-mutating \u2014 JS's mutate-vs-new\n# distinction is moot in SSR template context.\n#\n# Opts hash-ref. The compiler emits a `keys` list of per-key hashes\n# in priority order; each hash carries:\n#\n# key_kind => 'self' | 'field'\n# key => '' when key_kind eq 'self'; field name verbatim\n# from the comparator AST (e.g. 'price', 'createdAt')\n# when key_kind eq 'field' \u2014 no case normalisation\n# applied. Perl hash lookups are case-sensitive so\n# the key here must match the actual hash key the\n# user populated.\n# compare_type => 'numeric' | 'string' | 'auto'\n# direction => 'asc' | 'desc'\n#\n# Accepted comparator catalogue (gated upstream at parse time \u2014\n# anything outside refuses with BF101 before reaching this helper):\n#\n# (a,b) => a.f - b.f \u2192 field, numeric\n# (a,b) => a - b \u2192 self, numeric\n# (a,b) => a[.f].localeCompare(b[.f]) \u2192 field|self, string\n# (a,b) => a.f > b.f ? 1 : -1 \u2192 field|self, auto\n# any of the above ||-chained \u2192 multi-key tie-breaks\n# (and reversed-operand variants for `desc`).\n#\n# `auto` (relational-ternary lowering) compares numerically when both\n# keys `looks_like_number`, else lexically \u2014 Go's `bf_sort` applies the\n# same rule so the two template adapters stay byte-equal.\n#\n# A future `nulls => 'first' | 'last'` knob can land per key without\n# churn \u2014 the opts hash is the right place to grow.\n\nsub sort ($self, $recv, $opts = {}) {\n return [] unless ref($recv) eq 'ARRAY';\n\n # Normalise the per-key specs (priority order, length >= 1).\n my @spec = map {\n {\n key_kind => $_->{key_kind} // 'self',\n key => $_->{key} // '',\n compare_type => $_->{compare_type} // 'numeric',\n direction => $_->{direction} // 'asc',\n }\n } @{ $opts->{keys} // [] };\n return [ @$recv ] unless @spec;\n\n # Schwartzian transform: project each item to all its sort keys\n # once, then compare projected keys. Cheaper than re-resolving the\n # field accessors inside every comparison for non-trivial arrays.\n my @keyed = map {\n my $item = $_;\n my @ks = map {\n $_->{key_kind} eq 'field' && ref($item) eq 'HASH' ? $item->{ $_->{key} } : $item;\n } @spec;\n [ \\@ks, $item ];\n } @$recv;\n\n my $cmp = sub {\n for my $i (0 .. $#spec) {\n my $sp = $spec[$i];\n my $c = _compare_sort_key($a->[0][$i], $b->[0][$i], $sp->{compare_type});\n next if $c == 0; # tie on this key \u2014 try the next\n return $sp->{direction} eq 'desc' ? -$c : $c;\n }\n return 0;\n };\n\n my @sorted = sort $cmp @keyed;\n return [ map { $_->[1] } @sorted ];\n}\n\n# Compare two projected keys, ascending orientation (-1 / 0 / 1); the\n# caller negates for 'desc'. 'auto' compares numerically when both\n# keys look like numbers, else lexically (matches Go's `bf_sort`).\n# undef coalesces to '' / 0 so the order stays total without warnings.\nsub _compare_sort_key ($av, $bv, $compare_type) {\n if ($compare_type eq 'string') {\n return ($av // '') cmp ($bv // '');\n }\n if ($compare_type eq 'auto') {\n if (looks_like_number($av // '') && looks_like_number($bv // '')) {\n return ($av // 0) <=> ($bv // 0);\n }\n return ($av // '') cmp ($bv // '');\n }\n return ($av // 0) <=> ($bv // 0); # numeric\n}\n\n# Fold an array into a scalar via the arithmetic-fold catalogue\n# (#1448 Tier C). Mirrors Go's `bf_reduce` and JS `reduce(fn, init)` /\n# `reduceRight(fn, init)` for the shapes `(acc, x) => acc <op> x` /\n# `(acc, x) => acc <op> x.field`:\n#\n# bf->reduce($recv, {\n# op => '+' | '*',\n# key_kind => 'self' | 'field',\n# key => '<field>', # when key_kind eq 'field'\n# type => 'numeric' | 'string',\n# init => <seed>, # number, or string for concat\n# direction => 'left' | 'right', # 'right' = reduceRight (default 'left')\n# })\n#\n# Numeric folds accumulate with `+` / `*` (non-numeric keys coalesce to\n# 0); string folds concatenate via `bf->string` (undef \u2192 ''). The init\n# seeds the accumulator, so an empty array returns it unchanged \u2014 exactly\n# like JS. `direction => 'right'` folds right-to-left (reduceRight); only\n# observable for string concat, since numeric sum / product commute.\n# Float stringification can diverge from Go's for inexact binary\n# fractions (e.g. 0.1 + 0.2); integer sums \u2014 the common case \u2014 agree.\nsub reduce ($self, $recv, $opts = {}) {\n my $op = $opts->{op} // '+';\n my $key_kind = $opts->{key_kind} // 'self';\n my $key = $opts->{key} // '';\n my $type = $opts->{type} // 'numeric';\n my $direction = $opts->{direction} // 'left';\n\n my @items = ref($recv) eq 'ARRAY' ? @$recv : ();\n # reduceRight folds right-to-left; reversing the snapshot keeps the\n # single forward loop below. Only observable for string concat \u2014\n # numeric sum / product commute. Qualify as CORE::reverse \u2014 this\n # package defines `sub reverse` (the `.reverse()` helper), so a bare\n # `reverse` is ambiguous under `use warnings`.\n @items = CORE::reverse(@items) if $direction eq 'right';\n my $project = sub ($item) {\n $key_kind eq 'field' && ref($item) eq 'HASH' ? $item->{$key} : $item;\n };\n\n if ($type eq 'string') {\n my $acc = $opts->{init} // '';\n $acc .= $self->string($project->($_)) for @items;\n return $acc;\n }\n\n my $acc = $opts->{init} // 0;\n for my $item (@items) {\n my $n = $project->($item);\n # Guard `defined` before `looks_like_number` so a missing field\n # (undef) folds as 0 without an \"uninitialized value\" warning\n # under `use warnings` \u2014 matching the `$av // ''` style `sort` uses.\n $n = 0 unless defined $n && looks_like_number($n);\n $op eq '*' ? ($acc *= $n) : ($acc += $n);\n }\n return $acc;\n}\n\n# ---------------------------------------------------------------------------\n# JSX intrinsic-element spread (#1407)\n# ---------------------------------------------------------------------------\n#\n# Mirrors the JS `spreadAttrs` runtime\n# (`packages/client/src/runtime/spread-attrs.ts`) and the Go adapter's\n# `bf.SpreadAttrs` so SSR output stays byte-equal across the three\n# adapters. Generated Mojo templates invoke this as\n# `<%== bf->spread_attrs($bag) %>`.\n#\n# Skip rules: nil/false values, event handlers (`on[A-Z]\u2026` shape\n# matching JS `key[2] === key[2].toUpperCase()` \u2014 true for any\n# character whose uppercase is itself, including digits and\n# underscore), `children`. `ref` is intentionally NOT filtered,\n# matching the JS reference.\n#\n# Key remap: className \u2192 class, htmlFor \u2192 for; SVG camelCase\n# attrs preserved (case-sensitive XML spec); other camelCase keys\n# lowered to kebab-case with a leading `-` for an initial\n# uppercase letter (mirrors JS `key.replace(/([A-Z])/g, '-$1')`).\n#\n# `style` is routed through `_style_to_css` so object literals\n# serialise to a real CSS string instead of Perl's default\n# `HASH(0x...)` form.\n#\n# Output is deterministic: keys are sorted alphabetically before\n# emission, matching the Go adapter's `sort.Strings(keys)` policy\n# and Mojo::JSON's marshal order.\n#\n# The return value is a Mojo::ByteStream so the calling template's\n# `<%==` raw-emit skips re-escaping (the helper has already\n# HTML-escaped each value).\n\nmy %SVG_CAMEL_CASE_ATTRS = map { $_ => 1 } qw(\n allowReorder attributeName attributeType autoReverse\n baseFrequency baseProfile calcMode clipPathUnits\n contentScriptType contentStyleType diffuseConstant edgeMode\n externalResourcesRequired filterRes filterUnits glyphRef\n gradientTransform gradientUnits kernelMatrix kernelUnitLength\n keyPoints keySplines keyTimes lengthAdjust limitingConeAngle\n markerHeight markerUnits markerWidth maskContentUnits\n maskUnits numOctaves pathLength patternContentUnits\n patternTransform patternUnits pointsAtX pointsAtY pointsAtZ\n preserveAlpha preserveAspectRatio primitiveUnits refX refY\n repeatCount repeatDur requiredExtensions requiredFeatures\n specularConstant specularExponent spreadMethod startOffset\n stdDeviation stitchTiles surfaceScale systemLanguage\n tableValues targetX targetY textLength viewBox viewTarget\n xChannelSelector yChannelSelector zoomAndPan\n);\n\nsub _to_attr_name ($key) {\n return 'class' if $key eq 'className';\n return 'for' if $key eq 'htmlFor';\n return $key if $SVG_CAMEL_CASE_ATTRS{$key};\n # camelCase \u2192 kebab-case, with a leading `-` for an initial\n # uppercase letter (JS-reference parity, even though that case\n # produces an HTML-invalid attribute name \u2014 same documented\n # behaviour as the Go adapter's `toAttrName`).\n my $out = $key;\n $out =~ s/([A-Z])/-\\L$1/g;\n return $out;\n}\n\nsub _html_escape ($value) {\n # HTML attribute-value escape for SSR string emission. The\n # spread bag's values reach the browser as part of a generated\n # `key=\"...\"` substring inside the rendered HTML, so the\n # escape set has to cover everything that could break either\n # the surrounding double-quoted attribute or the enclosing\n # tag: `&`, `<`, `>`, `\"`, and `'`. Matches Go's\n # `template.HTMLEscapeString` semantics byte-for-byte (using\n # `"` / `'` for quotes rather than the named entities)\n # so the SSR output is identical across the Go and Mojo\n # adapters (#1407, #1413 review). The CSR-side\n # `applyRestAttrs` calls `el.setAttribute(name, String(value))`\n # \u2014 which does its own DOM-level escaping in the browser \u2014\n # so JS doesn't need an explicit escape pass; Perl/Go emit a\n # string, so we do.\n my $s = defined $value ? \"$value\" : '';\n $s =~ s/&/&/g;\n $s =~ s/</</g;\n $s =~ s/>/>/g;\n $s =~ s/\"/"/g;\n $s =~ s/'/'/g;\n return $s;\n}\n\nsub _style_to_css ($value) {\n return undef unless defined $value;\n # Non-hashref values pass through stringified \u2014 matches the JS\n # `typeof value !== 'object'` branch in `styleToCss`.\n if (ref($value) ne 'HASH') {\n my $s = \"$value\";\n return length $s ? $s : undef;\n }\n my @parts;\n for my $key (sort keys %$value) {\n my $v = $value->{$key};\n next unless defined $v;\n my $prop = $key;\n $prop =~ s/([A-Z])/-\\L$1/g;\n push @parts, \"$prop:$v\";\n }\n return @parts ? join(';', @parts) : undef;\n}\n\nsub spread_attrs ($self, $bag) {\n return '' unless defined $bag && ref($bag) eq 'HASH';\n my @parts;\n for my $key (sort keys %$bag) {\n # Event handlers: skip when key starts `on` and the third\n # character is its own uppercase form (uppercase letter,\n # digit, underscore, \u2026). Mirrors the JS predicate.\n if (length($key) > 2 && substr($key, 0, 2) eq 'on') {\n my $c = substr($key, 2, 1);\n next if uc($c) eq $c;\n }\n next if $key eq 'children';\n my $val = $bag->{$key};\n # null / undef \u2192 drop.\n next unless defined $val;\n # Boolean values arrive as Mojo::JSON sentinel objects\n # (`Mojo::JSON::true` / `false`) \u2014 both from JSON-deserialised\n # props and from the test harness's `toPerlLiteral`\n # (which emits the sentinels rather than plain 0/1 to avoid\n # conflating booleans with numeric attribute values like\n # `tabindex=\"0\"`). The contract is: callers MUST use the\n # sentinels for boolean values; plain Perl scalars 0/1\n # render as numeric attribute values, matching how JS\n # `spreadAttrs` treats a `0`/`1` JS number.\n if (ref($val) eq 'JSON::PP::Boolean' || ref($val) eq 'Mojo::JSON::_Bool') {\n next unless $val;\n push @parts, _to_attr_name($key);\n next;\n }\n # `style` routes through `_style_to_css` so object literals\n # serialise to a real CSS string.\n if ($key eq 'style') {\n my $css = _style_to_css($val);\n next unless defined $css && length $css;\n push @parts, qq{style=\"} . _html_escape($css) . qq{\"};\n next;\n }\n my $name = _to_attr_name($key);\n push @parts, $name . qq{=\"} . _html_escape($val) . qq{\"};\n }\n return '' unless @parts;\n # Mark the result raw so the calling template's `<%==` raw-emit\n # doesn't re-escape the already-escaped values (the Mojo backend\n # returns a Mojo::ByteStream).\n return $self->backend->mark_raw(join(' ', @parts));\n}\n\n1;\n__END__\n\n=encoding utf8\n\n=head1 NAME\n\nBarefootJS - Engine- and framework-agnostic server runtime for BarefootJS marked templates\n\n=head1 SYNOPSIS\n\n use BarefootJS;\n\n # A host injects a rendering backend (see BarefootJS::Backend::Xslate or\n # Mojolicious::Plugin::BarefootJS for shipping backends).\n my $bf = BarefootJS->new($context, { backend => $backend });\n\n # The compiled marked template calls the runtime as a `bf` object:\n # <: $bf.scope_attr() :> <: $bf.json($data) :> <: $bf.spread_attrs($h) :>\n\n=head1 DESCRIPTION\n\nBarefootJS compiles JSX/TSX into a marked template plus client JS. This module\nis the server-side runtime the marked templates call into at render time. It is\ndeliberately template-engine- and web-framework-agnostic: every operation that\ndepends on I<how> a template is rendered \u2014 JSON marshalling, raw-string marking,\nJSX-children materialisation, and named-template rendering \u2014 is delegated to a\npluggable C<backend>.\n\nThat design lets the one runtime drive any backend. Shipping backends:\n\n=over 4\n\n=item * L<BarefootJS::Backend::Xslate> \u2014 Text::Xslate (Kolon); runs under any PSGI/Plack app.\n\n=item * L<BarefootJS::Backend::Mojo> \u2014 Mojolicious (via L<Mojolicious::Plugin::BarefootJS>).\n\n=back\n\nThe core itself pulls in only core Perl modules (C<POSIX>, C<Scalar::Util>);\nno template engine or web framework is loaded unless a backend that needs one\nis used.\n\n=head1 SEE ALSO\n\nL<BarefootJS::Backend::Xslate>, L<Mojolicious::Plugin::BarefootJS>,\nL<https://github.com/piconic-ai/barefootjs>\n\n=head1 AUTHOR\n\nkobaken E<lt>kentafly88@gmail.comE<gt>\n\n=head1 LICENSE\n\nCopyright (c) 2025-present BarefootJS Contributors.\n\nThis library is free software; you can redistribute it and/or modify it under\nthe MIT License. See the F<LICENSE> file in the distribution for the full text.\n\n=cut\n";
|
|
23255
|
-
barefootBackendMojoPmSource = "package BarefootJS::Backend::Mojo;\nour $VERSION = \"0.
|
|
23256
|
-
barefootPluginPmSource = "package Mojolicious::Plugin::BarefootJS;\nour $VERSION = \"0.
|
|
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/\"/"/gr;\n push @parts, qq{bf-h=\"$h\"};\n }\n if (defined $mount && length $mount) {\n my $m = $mount =~ s/\"/"/gr;\n push @parts, qq{bf-m=\"$m\"};\n }\n unless ($self->_is_child) {\n push @parts, q{bf-r=\"\"};\n }\n return join(' ', @parts);\n}\n\nsub props_attr ($self) {\n my $props = $self->_props;\n return '' unless $props && %$props;\n # encode_json returns a character string (not bytes) for safe embedding\n # in templates (the Mojo backend uses Mojo::JSON::to_json).\n my $json = $self->backend->encode_json($props);\n return qq{ bf-p='$json'};\n}\n\n# ---------------------------------------------------------------------------\n# Comment Markers\n# ---------------------------------------------------------------------------\n\nsub comment ($self, $text) {\n return \"<!--bf-$text-->\";\n}\n\n# ---------------------------------------------------------------------------\n# JS-equivalent value stringification\n# ---------------------------------------------------------------------------\n\n# Map a Perl boolean-shaped value to the JS `String(bool)` form.\n# Used by the Mojo adapter when emitting reactive attribute bindings\n# whose JS source `isBooleanResultExpr` classified as boolean \u2014\n# a comparison (`count() > 0`), a logical negation (`!ok()`), or a\n# literal `true` / `false`. Perl's auto-stringification of those\n# expressions yields `''` / `1`; Hono and Go emit `'false'` / `'true'`.\n# Centralising the bool \u2192 string mapping here keeps the contract\n# testable and the template-emit syntax tidy\n# (`<%= bf->bool_str(...) %>` vs an inline ternary).\n#\n# Contract is boolean-only: callers must have classified the\n# expression as boolean-result before routing through this helper.\n# Non-boolean values reaching here will be Perl-truthy-coerced to\n# 'true' / 'false', which is generally wrong \u2014 non-boolean attribute\n# bindings stay on the plain `<%= expr %>` emit path and never reach\n# this function.\nsub bool_str ($self, $value) {\n return $value ? 'true' : 'false';\n}\n\nsub text_start ($self, $slot_id) {\n return \"<!--bf:$slot_id-->\";\n}\n\nsub text_end ($self) {\n return \"<!--/-->\";\n}\n\n# See spec/compiler.md \"Slot identity\" for the comment-scope wire format.\nsub scope_comment ($self) {\n my $scope_id = $self->_scope_id // '';\n my $host_segment = '';\n my $host = $self->_bf_parent;\n my $mount = $self->_bf_mount;\n if (defined $host && length $host) {\n $host_segment = \"|h=$host|m=\" . ($mount // '');\n }\n my $props_json = '';\n if ($self->_props && %{$self->_props}) {\n $props_json = '|' . $self->backend->encode_json($self->_props);\n }\n return \"<!--bf-scope:$scope_id$host_segment$props_json-->\";\n}\n\n# ---------------------------------------------------------------------------\n# Script Registration\n# ---------------------------------------------------------------------------\n\nsub register_script ($self, $path) {\n return if $self->_script_seen->{$path};\n $self->_script_seen->{$path} = 1;\n push @{$self->_scripts}, $path;\n}\n\n# ---------------------------------------------------------------------------\n# Child Component Rendering\n# ---------------------------------------------------------------------------\n# (`_child_renderers` accessor is generated by the minimal accessor base above.)\n\nsub register_child_renderer ($self, $name, $renderer) {\n $self->_child_renderers->{$name} = $renderer;\n}\n\nsub render_child ($self, $name, @args) {\n my $renderer = $self->_child_renderers->{$name};\n die \"No renderer registered for child component '$name'\" unless $renderer;\n # Accept both the Mojo list form \u2014 `bf->render_child($name, k => v, ...)`\n # \u2014 and the single-hashref form \u2014 `$bf.render_child($name, { k => v })`.\n # Template languages whose method calls can't splat a hash into positional\n # args (Text::Xslate Kolon, Template Toolkit) pass one hashref instead.\n my %props = (@args == 1 && ref $args[0] eq 'HASH') ? %{ $args[0] } : @args;\n # JSX children come in via the engine's children-capture mechanism\n # (Mojo's `begin %>...<% end`, which produces a CODE ref returning a\n # Mojo::ByteStream). Materialize it through the backend before handing\n # the props to the child renderer so the child template sees\n # `$children` as already-rendered HTML. Guard on `exists` so a\n # childless invocation (`bf->render_child('counter')`) doesn't gain a\n # spurious `children => undef` key \u2014 preserving the historical \"only\n # touch children when present\" behaviour.\n $props{children} = $self->backend->materialize($props{children})\n if exists $props{children};\n return $renderer->(\\%props);\n}\n\n# ---------------------------------------------------------------------------\n# Bulk registration from build manifest\n# ---------------------------------------------------------------------------\n#\n# `bf build` emits dist/templates/manifest.json describing every\n# component the page might invoke (Counter, ui/button/index, ...).\n# This helper walks that manifest and registers one child renderer per\n# UI registry entry \u2014 the path shape `ui/<name>/index` maps to the\n# `<name>` slot key Counter.html.ep and friends use via\n# `<%= bf->render_child('<name>', ...) %>`.\n#\n# Each manifest entry carries an `ssrDefaults` hash derived statically\n# from the component's JSX (prop destructure defaults + signal /\n# memo initial values, see packages/jsx/src/ssr-defaults.ts). The\n# child renderer seeds every template variable from that hash,\n# preferring the caller's matching prop where one exists. This\n# replaces the per-component `signal_init` callback that every\n# scaffold's `app.pl` used to hand-roll for items 1/3 of issue #1416.\n#\n# `signal_init` remains as an opt-in override for cases the static\n# extractor can't see through (e.g. signal initial values that\n# reference imported helpers). When supplied for a given slot key\n# it takes precedence over the manifest's `ssrDefaults` for that\n# child, allowing callers to mix manual overrides with auto-derived\n# defaults for siblings.\nsub register_components_from_manifest ($self, $manifest, %opts) {\n my $signal_inits = $opts{signal_init} // {};\n my $parent_scope = $self->_scope_id;\n # Weaken the parent capture so the child-renderer closures stored on\n # `$self->_child_renderers` don't keep `$self` alive (the direct\n # closure <-> parent cycle). The controller is reached through `$parent`\n # at call time rather than captured strongly here, so the closures hold\n # no strong reference to `$c` either \u2014 see the controller-cycle note in\n # `new`. `$parent` is always live whenever a closure runs (the closure is\n # stored on `$parent`, so `$parent` outlives every invocation).\n weaken(my $parent = $self);\n\n for my $entry_name (keys %$manifest) {\n # `__barefoot__` is the runtime entry, not a component.\n next if $entry_name eq '__barefoot__';\n # Only UI registry components (path shape `ui/<name>/index`)\n # become child renderers; top-level page components are the\n # render target rather than a child.\n next unless $entry_name =~ m{^ui/([^/]+)/index$};\n my $slot_key = $1;\n my $marked = $manifest->{$entry_name}{markedTemplate} // '';\n next unless $marked;\n # `templates/ui/button/index.html.ep` \u2192 `ui/button/index`\n my $template_name = $marked;\n $template_name =~ s{^templates/}{};\n $template_name =~ s{\\.html\\.ep$}{};\n\n my $signal_init = $signal_inits->{$slot_key};\n my $manifest_defaults = $manifest->{$entry_name}{ssrDefaults};\n $self->register_child_renderer($slot_key, sub {\n my ($props) = @_;\n # Child shares the parent's backend so nested renders go\n # through the same engine + controller (and inherit any\n # injected json_encoder). The controller is fetched via the weak\n # `$parent` at call time \u2014 never captured strongly \u2014 so the\n # closure adds no edge to the per-request reference cycle.\n my $child_bf = BarefootJS->new($parent->c, { backend => $parent->backend });\n my $slot_id = delete $props->{_bf_slot};\n $child_bf->_scope_id(\n $slot_id ? $parent_scope . '_' . $slot_id\n : $template_name . '_' . substr(rand() =~ s/^0\\.//r, 0, 6)\n );\n $child_bf->_is_child(1);\n # (#1249) Slot identity: host scope + slot id. Emitted as\n # bf-h / bf-m attributes by hydration_attrs.\n if ($slot_id) {\n $child_bf->_bf_parent($parent_scope);\n $child_bf->_bf_mount($slot_id);\n }\n $child_bf->_scripts($parent->_scripts);\n $child_bf->_script_seen($parent->_script_seen);\n\n my %extra;\n if ($signal_init) {\n %extra = $signal_init->($props);\n } elsif ($manifest_defaults) {\n %extra = _derive_stash_from_defaults($manifest_defaults, $props);\n }\n\n # Render the child template with $child_bf bound as the active\n # instance for the nested render. The backend owns the\n # engine-specific binding + restore (stash juggle for Mojo).\n my $html = $parent->backend->render_named(\n $template_name, $child_bf, { %$props, %extra },\n );\n chomp $html;\n return $html;\n });\n }\n}\n\n# Derive template-stash kvs from a manifest entry's `ssrDefaults`\n# section. Each entry shape:\n# { value => <static-fallback>, propName => <prop>, isRestProps => bool }\n# For `isRestProps`, the rest bag passes through unchanged (or the\n# static `{}` if the caller didn't supply one). For ordinary entries\n# the caller's `$props->{propName}` wins when defined, otherwise the\n# static `value` does. `propName`-less entries (signal / memo locals)\n# always use the static value \u2014 the caller cannot override them.\nsub _derive_stash_from_defaults ($defaults, $props) {\n my %extra;\n for my $name (keys %$defaults) {\n my $d = $defaults->{$name};\n if (ref($d) ne 'HASH') {\n $extra{$name} = $d;\n next;\n }\n if ($d->{isRestProps}) {\n $extra{$name} = exists $props->{$name} ? $props->{$name} : $d->{value};\n next;\n }\n my $prop_name = $d->{propName};\n if (defined $prop_name && exists $props->{$prop_name} && defined $props->{$prop_name}) {\n $extra{$name} = $props->{$prop_name};\n } else {\n $extra{$name} = $d->{value};\n }\n }\n return %extra;\n}\n\n# ---------------------------------------------------------------------------\n# Script Output\n# ---------------------------------------------------------------------------\n\nsub scripts ($self) {\n my @tags;\n for my $path (@{$self->_scripts}) {\n push @tags, qq{<script type=\"module\" src=\"$path\"></script>};\n }\n return join(\"\\n\", @tags);\n}\n\n# ---------------------------------------------------------------------------\n# Streaming SSR (Out-of-Order)\n# ---------------------------------------------------------------------------\n\nsub streaming_bootstrap ($self) {\n return q{<script>(function(){function s(id){var a=document.querySelector('[bf-async=\"'+id+'\"]');var t=document.querySelector('template[bf-async-resolve=\"'+id+'\"]');if(!a||!t)return;a.replaceChildren(t.content.cloneNode(true));a.removeAttribute('bf-async');t.remove();requestAnimationFrame(function(){if(window.__bf_hydrate)window.__bf_hydrate()})};window.__bf_swap=s})()</script>};\n}\n\nsub async_boundary ($self, $id, $fallback_html) {\n # The fallback comes in via Mojo `begin %>...<% end` capture (see\n # MojoAdapter::renderAsync), which produces a CODE ref returning a\n # Mojo::ByteStream. Materialize it through the backend so the rendered\n # HTML embeds in the placeholder rather than the CODE ref's\n # stringification.\n $fallback_html = $self->backend->materialize($fallback_html);\n return qq{<div bf-async=\"$id\">$fallback_html</div>};\n}\n\nsub async_resolve ($self, $id, $content_html) {\n return qq{<template bf-async-resolve=\"$id\">$content_html</template><script>__bf_swap(\"$id\")</script>};\n}\n\n# ---------------------------------------------------------------------------\n# JS-compat callees (#1189) \u2014 invoked from generated Mojo templates as\n# <%= bf->json($val) %>, <%= bf->floor($val) %>, etc. The MojoAdapter's\n# `templatePrimitives` registry emits these helper calls in place of the\n# corresponding JS callees (`JSON.stringify`, `Math.floor`, \u2026) so the SSR\n# template can render value-equivalent output without a JS engine.\n#\n# Failure policy mirrors the Go adapter (#1188): user-data marshalling\n# (json) bubbles errors so Mojolicious aborts loudly on cycles /\n# unsupported values rather than silently producing an empty payload.\n# Numeric coercion follows JS semantics (NaN propagates as the special\n# string 'NaN'; non-numeric input returns 'NaN' rather than 0). Strings\n# always coerce to a string representation.\n# ---------------------------------------------------------------------------\n\nsub json ($self, $value) {\n # Mojo::JSON::to_json returns a character string (not bytes), suitable\n # for embedding in HTML output via Mojo::ByteStream / `<%==`.\n #\n # Documented divergence from JS: JS distinguishes `null` (renders as\n # \"null\") from `undefined` (`JSON.stringify(undefined)` returns the\n # JS value `undefined`, not a string). Perl has no such distinction\n # \u2014 both map to `undef`. We choose the `null` rendering for SSR\n # ergonomics: an unset prop becomes the string \"null\" rather than\n # the literal text \"undefined\" or an empty attribute. Matches the\n # `null` case of JS exactly; diverges from the `undefined` case.\n return $self->backend->encode_json($value);\n}\n\nsub string ($self, $value) {\n # JS `String(v)` mirror. `undef` renders as the empty string here so\n # an unset prop doesn't surface as a literal \"undefined\" / \"null\"\n # in user-facing HTML \u2014 same divergence the Go adapter documents\n # for `bf_string`.\n return defined $value ? \"$value\" : '';\n}\n\nsub number ($self, $value) {\n # JS `Number(v)` mirror. Numeric coerces via Perl's implicit\n # numeric context; non-numeric / undef yield real numeric NaN\n # (`'nan' + 0`) so downstream arithmetic propagates correctly\n # (`Math.floor(NaN) === NaN`). Returning the literal string\n # \"NaN\" would conflate the user-passing-the-string-\"NaN\" case\n # with the parse-failure case, and break NaN detection in\n # downstream helpers.\n return 0 + 'nan' unless defined $value;\n return $value + 0 if looks_like_number($value);\n return 0 + 'nan';\n}\n\n# NaN is the only float for which `$x != $x` holds. Used as the\n# portable sentinel check in floor/ceil/round.\nsub _is_nan { my $n = shift; return $n != $n }\n\nsub floor ($self, $value) {\n my $n = $self->number($value);\n return $n if _is_nan($n);\n return POSIX::floor($n);\n}\n\nsub ceil ($self, $value) {\n my $n = $self->number($value);\n return $n if _is_nan($n);\n return POSIX::ceil($n);\n}\n\nsub round ($self, $value) {\n my $n = $self->number($value);\n return $n if _is_nan($n);\n # POSIX has no `round`. JS `Math.round` rounds half toward\n # +Infinity (so `Math.round(-1.5) === -1`, not -2). `floor(n\n # + 0.5)` reproduces that for both signs.\n return POSIX::floor($n + 0.5);\n}\n\n# ---------------------------------------------------------------------------\n# Array / String method helpers (#1448 Tier A)\n# ---------------------------------------------------------------------------\n#\n# `Array.prototype.includes(x)` and `String.prototype.includes(sub)`\n# share a method name in JS; the JSX parser can't tell the two\n# receiver shapes apart without TS type inference, so both lower to\n# the same IR node (`array-method` / method `includes`). This helper\n# dispatches at the Perl level via `ref()`:\n# - ARRAY ref: scan elements with `eq`; one defined-vs-undef\n# hop matches JS's `===` for null/undefined.\n# - scalar: `index($recv, $sub) != -1`, with both args\n# coerced through `// ''` so an undef receiver /\n# needle doesn't trip Perl's substr warning.\n# Anything else (HASH ref, code ref) returns false \u2014 matches the\n# JS semantic where `.includes` is only defined on Array /\n# TypedArray / String.\n\nsub includes ($self, $recv, $elem) {\n if (ref($recv) eq 'ARRAY') {\n for my $item (@$recv) {\n if (!defined $item) {\n return 1 if !defined $elem;\n next;\n }\n return 1 if defined $elem && $item eq $elem;\n }\n return 0;\n }\n return 0 if ref($recv);\n return index($recv // '', $elem // '') != -1 ? 1 : 0;\n}\n\n# `Array.prototype.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 # `"` / `'` for quotes rather than the named entities)\n # so the SSR output is identical across the Go and Mojo\n # adapters (#1407, #1413 review). The CSR-side\n # `applyRestAttrs` calls `el.setAttribute(name, String(value))`\n # \u2014 which does its own DOM-level escaping in the browser \u2014\n # so JS doesn't need an explicit escape pass; Perl/Go emit a\n # string, so we do.\n my $s = defined $value ? \"$value\" : '';\n $s =~ s/&/&/g;\n $s =~ s/</</g;\n $s =~ s/>/>/g;\n $s =~ s/\"/"/g;\n $s =~ s/'/'/g;\n return $s;\n}\n\nsub _style_to_css ($value) {\n return undef unless defined $value;\n # Non-hashref values pass through stringified \u2014 matches the JS\n # `typeof value !== 'object'` branch in `styleToCss`.\n if (ref($value) ne 'HASH') {\n my $s = \"$value\";\n return 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.
|
|
23553
|
+
our $VERSION = "0.8.0";
|
|
23259
23554
|
use Mojo::Base 'Mojolicious::Plugin', -signatures;
|
|
23260
23555
|
|
|
23261
23556
|
=head1 NAME
|
|
@@ -24626,7 +24921,7 @@ export default app
|
|
|
24626
24921
|
HONO_RENDERER_TSX = `import { jsxRenderer } from 'hono/jsx-renderer'
|
|
24627
24922
|
import { BfImportMap } from '@barefootjs/hono/app'
|
|
24628
24923
|
import { BfScripts } from '@barefootjs/hono/scripts'
|
|
24629
|
-
import manifest from './public/components/manifest.json'
|
|
24924
|
+
import manifest from './public/components/manifest.json' with { type: 'json' }
|
|
24630
24925
|
|
|
24631
24926
|
declare module 'hono' {
|
|
24632
24927
|
interface ContextRenderer {
|
|
@@ -24897,7 +25192,7 @@ import { BfImportMap, BfDevReload } from '@barefootjs/hono/app'
|
|
|
24897
25192
|
import { BfScripts } from '@barefootjs/hono/scripts'
|
|
24898
25193
|
import { readFileSync } from 'node:fs'
|
|
24899
25194
|
import { resolve } from 'node:path'
|
|
24900
|
-
import staticManifest from './dist/components/manifest.json'
|
|
25195
|
+
import staticManifest from './dist/components/manifest.json' with { type: 'json' }
|
|
24901
25196
|
import { isDev } from './env'
|
|
24902
25197
|
|
|
24903
25198
|
declare module 'hono' {
|
|
@@ -25588,8 +25883,8 @@ var init_exports = {};
|
|
|
25588
25883
|
__export(init_exports, {
|
|
25589
25884
|
run: () => run4
|
|
25590
25885
|
});
|
|
25591
|
-
import { existsSync as existsSync8, mkdirSync as mkdirSync3, writeFileSync as writeFileSync3 } from "fs";
|
|
25592
|
-
import path10 from "path";
|
|
25886
|
+
import { existsSync as existsSync8, mkdirSync as mkdirSync3, writeFileSync as writeFileSync3 } from "node:fs";
|
|
25887
|
+
import path10 from "node:path";
|
|
25593
25888
|
function parseFlags(args2) {
|
|
25594
25889
|
const flags = {};
|
|
25595
25890
|
for (let i = 0; i < args2.length; i++) {
|
|
@@ -25828,8 +26123,8 @@ var init_init = __esm({
|
|
|
25828
26123
|
});
|
|
25829
26124
|
|
|
25830
26125
|
// src/lib/resolve-source.ts
|
|
25831
|
-
import { existsSync as existsSync9, readdirSync as readdirSync3 } from "fs";
|
|
25832
|
-
import path11 from "path";
|
|
26126
|
+
import { existsSync as existsSync9, readdirSync as readdirSync3 } from "node:fs";
|
|
26127
|
+
import path11 from "node:path";
|
|
25833
26128
|
function tryCandidate(candidate, searched) {
|
|
25834
26129
|
searched.push(candidate);
|
|
25835
26130
|
return existsSync9(candidate) ? candidate : null;
|
|
@@ -25915,8 +26210,8 @@ var init_resolve_source = __esm({
|
|
|
25915
26210
|
});
|
|
25916
26211
|
|
|
25917
26212
|
// src/lib/meta-loader.ts
|
|
25918
|
-
import { readFileSync as readFileSync5, existsSync as existsSync10, readdirSync as readdirSync4 } from "fs";
|
|
25919
|
-
import path12 from "path";
|
|
26213
|
+
import { readFileSync as readFileSync5, existsSync as existsSync10, readdirSync as readdirSync4 } from "node:fs";
|
|
26214
|
+
import path12 from "node:path";
|
|
25920
26215
|
function loadIndex(metaDir) {
|
|
25921
26216
|
const indexPath = path12.join(metaDir, "index.json");
|
|
25922
26217
|
if (!existsSync10(indexPath)) {
|
|
@@ -26039,7 +26334,7 @@ __export(search_exports, {
|
|
|
26039
26334
|
run: () => run5,
|
|
26040
26335
|
search: () => search
|
|
26041
26336
|
});
|
|
26042
|
-
import path13 from "path";
|
|
26337
|
+
import path13 from "node:path";
|
|
26043
26338
|
function search(query, index, source, coreDocs) {
|
|
26044
26339
|
const q = query.toLowerCase();
|
|
26045
26340
|
const aliasCategories = categoryAliases[q] || [];
|
|
@@ -26184,7 +26479,7 @@ var docs_exports = {};
|
|
|
26184
26479
|
__export(docs_exports, {
|
|
26185
26480
|
run: () => run6
|
|
26186
26481
|
});
|
|
26187
|
-
import path14 from "path";
|
|
26482
|
+
import path14 from "node:path";
|
|
26188
26483
|
function printComponent(meta, jsonFlag2, banner) {
|
|
26189
26484
|
if (jsonFlag2) {
|
|
26190
26485
|
console.log(JSON.stringify(meta, null, 2));
|
|
@@ -26304,8 +26599,8 @@ var guide_exports = {};
|
|
|
26304
26599
|
__export(guide_exports, {
|
|
26305
26600
|
run: () => run7
|
|
26306
26601
|
});
|
|
26307
|
-
import path15 from "path";
|
|
26308
|
-
import { existsSync as existsSync11 } from "fs";
|
|
26602
|
+
import path15 from "node:path";
|
|
26603
|
+
import { existsSync as existsSync11 } from "node:fs";
|
|
26309
26604
|
import { fileURLToPath as fileURLToPath3 } from "node:url";
|
|
26310
26605
|
function findDocsDir(ctx2) {
|
|
26311
26606
|
const monorepoDocs = path15.join(ctx2.root, "docs/core");
|
|
@@ -27239,8 +27534,8 @@ var preview_exports = {};
|
|
|
27239
27534
|
__export(preview_exports, {
|
|
27240
27535
|
run: () => run8
|
|
27241
27536
|
});
|
|
27242
|
-
import { existsSync as existsSync17, readdirSync as readdirSync5, watch as fsWatch } from "fs";
|
|
27243
|
-
import path16 from "path";
|
|
27537
|
+
import { existsSync as existsSync17, readdirSync as readdirSync5, watch as fsWatch } from "node:fs";
|
|
27538
|
+
import path16 from "node:path";
|
|
27244
27539
|
function parseArgs(args2) {
|
|
27245
27540
|
const out = { serve: false, watch: false, help: false, port: DEFAULT_PORT };
|
|
27246
27541
|
for (let i = 0; i < args2.length; i++) {
|
|
@@ -27402,8 +27697,8 @@ __export(tokens_apply_exports, {
|
|
|
27402
27697
|
resolveTokensCss: () => resolveTokensCss,
|
|
27403
27698
|
run: () => run9
|
|
27404
27699
|
});
|
|
27405
|
-
import { existsSync as existsSync18, readFileSync as readFileSync7, writeFileSync as writeFileSync5 } from "fs";
|
|
27406
|
-
import path17 from "path";
|
|
27700
|
+
import { existsSync as existsSync18, readFileSync as readFileSync7, writeFileSync as writeFileSync5 } from "node:fs";
|
|
27701
|
+
import path17 from "node:path";
|
|
27407
27702
|
async function run9(args2, ctx2) {
|
|
27408
27703
|
const url = args2[0];
|
|
27409
27704
|
if (!url) {
|
|
@@ -27718,8 +28013,8 @@ var init_tokens2 = __esm({
|
|
|
27718
28013
|
});
|
|
27719
28014
|
|
|
27720
28015
|
// src/lib/scaffold.ts
|
|
27721
|
-
import { readFileSync as readFileSync8, existsSync as existsSync19 } from "fs";
|
|
27722
|
-
import path18 from "path";
|
|
28016
|
+
import { readFileSync as readFileSync8, existsSync as existsSync19 } from "node:fs";
|
|
28017
|
+
import path18 from "node:path";
|
|
27723
28018
|
function loadMeta(metaDir, name) {
|
|
27724
28019
|
const filePath = path18.join(metaDir, `${name}.json`);
|
|
27725
28020
|
if (!existsSync19(filePath)) return null;
|
|
@@ -27874,8 +28169,8 @@ var gen_component_exports = {};
|
|
|
27874
28169
|
__export(gen_component_exports, {
|
|
27875
28170
|
run: () => run11
|
|
27876
28171
|
});
|
|
27877
|
-
import { writeFileSync as writeFileSync6, mkdirSync as mkdirSync4, existsSync as existsSync20 } from "fs";
|
|
27878
|
-
import path19 from "path";
|
|
28172
|
+
import { writeFileSync as writeFileSync6, mkdirSync as mkdirSync4, existsSync as existsSync20 } from "node:fs";
|
|
28173
|
+
import path19 from "node:path";
|
|
27879
28174
|
function run11(args2, ctx2) {
|
|
27880
28175
|
if (args2.length < 1) {
|
|
27881
28176
|
console.error("Usage: bf gen component <component-name> [use-component1] [use-component2] ...");
|
|
@@ -28139,8 +28434,8 @@ var init_parse_component = __esm({
|
|
|
28139
28434
|
});
|
|
28140
28435
|
|
|
28141
28436
|
// src/lib/test-template.ts
|
|
28142
|
-
import { readFileSync as readFileSync9 } from "fs";
|
|
28143
|
-
import path20 from "path";
|
|
28437
|
+
import { readFileSync as readFileSync9 } from "node:fs";
|
|
28438
|
+
import path20 from "node:path";
|
|
28144
28439
|
function generateTestTemplate(componentPath, options = {}) {
|
|
28145
28440
|
const importSource = options.importSource ?? "bun:test";
|
|
28146
28441
|
const source = readFileSync9(componentPath, "utf-8");
|
|
@@ -28389,8 +28684,8 @@ var gen_test_exports = {};
|
|
|
28389
28684
|
__export(gen_test_exports, {
|
|
28390
28685
|
run: () => run12
|
|
28391
28686
|
});
|
|
28392
|
-
import { existsSync as existsSync21, writeFileSync as writeFileSync7 } from "fs";
|
|
28393
|
-
import path21 from "path";
|
|
28687
|
+
import { existsSync as existsSync21, writeFileSync as writeFileSync7 } from "node:fs";
|
|
28688
|
+
import path21 from "node:path";
|
|
28394
28689
|
function run12(args2, ctx2) {
|
|
28395
28690
|
const positional = args2.filter((a) => !a.startsWith("-"));
|
|
28396
28691
|
const flagSet = new Set(args2.filter((a) => a.startsWith("-")));
|
|
@@ -28444,8 +28739,8 @@ var gen_preview_exports = {};
|
|
|
28444
28739
|
__export(gen_preview_exports, {
|
|
28445
28740
|
run: () => run13
|
|
28446
28741
|
});
|
|
28447
|
-
import { existsSync as existsSync22, writeFileSync as writeFileSync8, mkdirSync as mkdirSync5 } from "fs";
|
|
28448
|
-
import path22 from "path";
|
|
28742
|
+
import { existsSync as existsSync22, writeFileSync as writeFileSync8, mkdirSync as mkdirSync5 } from "node:fs";
|
|
28743
|
+
import path22 from "node:path";
|
|
28449
28744
|
async function run13(args2, ctx2) {
|
|
28450
28745
|
const force = args2.includes("--force");
|
|
28451
28746
|
const name = args2.find((a) => !a.startsWith("--"));
|
|
@@ -28483,7 +28778,8 @@ var debug_graph_exports = {};
|
|
|
28483
28778
|
__export(debug_graph_exports, {
|
|
28484
28779
|
run: () => run14
|
|
28485
28780
|
});
|
|
28486
|
-
import { readFileSync as readFileSync10 } from "fs";
|
|
28781
|
+
import { readFileSync as readFileSync10 } from "node:fs";
|
|
28782
|
+
import "node:path";
|
|
28487
28783
|
async function run14(args2, ctx2) {
|
|
28488
28784
|
const componentName = args2[0];
|
|
28489
28785
|
if (!componentName) {
|
|
@@ -28520,7 +28816,7 @@ var debug_trace_exports = {};
|
|
|
28520
28816
|
__export(debug_trace_exports, {
|
|
28521
28817
|
run: () => run15
|
|
28522
28818
|
});
|
|
28523
|
-
import { readFileSync as readFileSync11 } from "fs";
|
|
28819
|
+
import { readFileSync as readFileSync11 } from "node:fs";
|
|
28524
28820
|
async function run15(args2, ctx2) {
|
|
28525
28821
|
const componentName = args2[0];
|
|
28526
28822
|
const targetName = args2[1];
|
|
@@ -28540,8 +28836,8 @@ async function run15(args2, ctx2) {
|
|
|
28540
28836
|
}
|
|
28541
28837
|
const source = readFileSync11(resolved.filePath, "utf-8");
|
|
28542
28838
|
const graph = buildComponentGraph2(source, resolved.filePath, resolved.componentName);
|
|
28543
|
-
const
|
|
28544
|
-
if (!
|
|
28839
|
+
const path24 = traceUpdatePath2(graph, targetName);
|
|
28840
|
+
if (!path24) {
|
|
28545
28841
|
console.error(`Error: Signal or memo "${targetName}" not found in ${graph.componentName}.`);
|
|
28546
28842
|
const available = [
|
|
28547
28843
|
...graph.signals.map((s) => s.name),
|
|
@@ -28553,9 +28849,9 @@ async function run15(args2, ctx2) {
|
|
|
28553
28849
|
process.exit(1);
|
|
28554
28850
|
}
|
|
28555
28851
|
if (ctx2.jsonFlag) {
|
|
28556
|
-
console.log(JSON.stringify(
|
|
28852
|
+
console.log(JSON.stringify(path24, null, 2));
|
|
28557
28853
|
} else {
|
|
28558
|
-
console.log(formatUpdatePath2(
|
|
28854
|
+
console.log(formatUpdatePath2(path24));
|
|
28559
28855
|
}
|
|
28560
28856
|
}
|
|
28561
28857
|
var init_debug_trace = __esm({
|
|
@@ -28570,7 +28866,7 @@ var debug_fallbacks_exports = {};
|
|
|
28570
28866
|
__export(debug_fallbacks_exports, {
|
|
28571
28867
|
run: () => run16
|
|
28572
28868
|
});
|
|
28573
|
-
import { readFileSync as readFileSync12 } from "fs";
|
|
28869
|
+
import { readFileSync as readFileSync12 } from "node:fs";
|
|
28574
28870
|
async function run16(args2, ctx2) {
|
|
28575
28871
|
const componentName = args2[0];
|
|
28576
28872
|
if (!componentName) {
|
|
@@ -28629,7 +28925,7 @@ var debug_signals_exports = {};
|
|
|
28629
28925
|
__export(debug_signals_exports, {
|
|
28630
28926
|
run: () => run17
|
|
28631
28927
|
});
|
|
28632
|
-
import { readFileSync as readFileSync13 } from "fs";
|
|
28928
|
+
import { readFileSync as readFileSync13 } from "node:fs";
|
|
28633
28929
|
async function run17(args2, ctx2) {
|
|
28634
28930
|
const componentName = args2[0];
|
|
28635
28931
|
if (!componentName) {
|
|
@@ -28669,7 +28965,7 @@ var debug_events_exports = {};
|
|
|
28669
28965
|
__export(debug_events_exports, {
|
|
28670
28966
|
run: () => run18
|
|
28671
28967
|
});
|
|
28672
|
-
import { readFileSync as readFileSync14 } from "fs";
|
|
28968
|
+
import { readFileSync as readFileSync14 } from "node:fs";
|
|
28673
28969
|
async function run18(args2, ctx2) {
|
|
28674
28970
|
const componentName = args2[0];
|
|
28675
28971
|
if (!componentName) {
|
|
@@ -28706,7 +29002,7 @@ var debug_loops_exports = {};
|
|
|
28706
29002
|
__export(debug_loops_exports, {
|
|
28707
29003
|
run: () => run19
|
|
28708
29004
|
});
|
|
28709
|
-
import { readFileSync as readFileSync15 } from "fs";
|
|
29005
|
+
import { readFileSync as readFileSync15 } from "node:fs";
|
|
28710
29006
|
async function run19(args2, ctx2) {
|
|
28711
29007
|
const componentName = args2[0];
|
|
28712
29008
|
if (!componentName) {
|
|
@@ -28743,7 +29039,7 @@ var debug_why_update_exports = {};
|
|
|
28743
29039
|
__export(debug_why_update_exports, {
|
|
28744
29040
|
run: () => run20
|
|
28745
29041
|
});
|
|
28746
|
-
import { readFileSync as readFileSync16 } from "fs";
|
|
29042
|
+
import { readFileSync as readFileSync16 } from "node:fs";
|
|
28747
29043
|
async function run20(args2, ctx2) {
|
|
28748
29044
|
const componentName = args2[0];
|
|
28749
29045
|
const bindingLabel = args2[1];
|
|
@@ -28801,7 +29097,7 @@ var debug_summary_exports = {};
|
|
|
28801
29097
|
__export(debug_summary_exports, {
|
|
28802
29098
|
run: () => run21
|
|
28803
29099
|
});
|
|
28804
|
-
import { readFileSync as readFileSync17 } from "fs";
|
|
29100
|
+
import { readFileSync as readFileSync17 } from "node:fs";
|
|
28805
29101
|
async function run21(args2, ctx2) {
|
|
28806
29102
|
const componentName = args2[0];
|
|
28807
29103
|
if (!componentName) {
|
|
@@ -28834,8 +29130,8 @@ var init_debug_summary = __esm({
|
|
|
28834
29130
|
});
|
|
28835
29131
|
|
|
28836
29132
|
// src/context.ts
|
|
28837
|
-
import { existsSync as existsSync2 } from "fs";
|
|
28838
|
-
import path from "path";
|
|
29133
|
+
import { existsSync as existsSync2 } from "node:fs";
|
|
29134
|
+
import path from "node:path";
|
|
28839
29135
|
import { fileURLToPath } from "node:url";
|
|
28840
29136
|
|
|
28841
29137
|
// src/config.ts
|
|
@@ -28852,9 +29148,9 @@ function findProjectConfig(startDir) {
|
|
|
28852
29148
|
let dir = path.resolve(startDir);
|
|
28853
29149
|
const { root: fsRoot } = path.parse(dir);
|
|
28854
29150
|
while (true) {
|
|
28855
|
-
const
|
|
28856
|
-
if (existsSync2(
|
|
28857
|
-
return { dir, tsConfigPath:
|
|
29151
|
+
const ts22 = path.join(dir, "barefoot.config.ts");
|
|
29152
|
+
if (existsSync2(ts22)) {
|
|
29153
|
+
return { dir, tsConfigPath: ts22 };
|
|
28858
29154
|
}
|
|
28859
29155
|
if (dir === fsRoot) return null;
|
|
28860
29156
|
dir = path.dirname(dir);
|