@barefootjs/cli 0.29.0 → 0.30.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +1098 -480
- package/package.json +4 -4
package/dist/index.js
CHANGED
|
@@ -3374,13 +3374,13 @@ function csrSubstituteOnce(value2, env) {
|
|
|
3374
3374
|
const recordSubstitution = (start2, end2, sub2) => {
|
|
3375
3375
|
splices.push({ start: start2 - OFFSET, end: end2 - OFFSET, text: `(${sub2.replacement})` });
|
|
3376
3376
|
};
|
|
3377
|
-
const
|
|
3377
|
+
const collectBindingNames5 = (name2, out) => {
|
|
3378
3378
|
if (ts4.isIdentifier(name2)) out.add(name2.text);
|
|
3379
3379
|
else if (ts4.isObjectBindingPattern(name2)) {
|
|
3380
|
-
for (const el of name2.elements)
|
|
3380
|
+
for (const el of name2.elements) collectBindingNames5(el.name, out);
|
|
3381
3381
|
} else if (ts4.isArrayBindingPattern(name2)) {
|
|
3382
3382
|
for (const el of name2.elements) {
|
|
3383
|
-
if (!ts4.isOmittedExpression(el))
|
|
3383
|
+
if (!ts4.isOmittedExpression(el)) collectBindingNames5(el.name, out);
|
|
3384
3384
|
}
|
|
3385
3385
|
}
|
|
3386
3386
|
};
|
|
@@ -3388,7 +3388,7 @@ function csrSubstituteOnce(value2, env) {
|
|
|
3388
3388
|
for (const stmt2 of block.statements) {
|
|
3389
3389
|
if (ts4.isVariableStatement(stmt2)) {
|
|
3390
3390
|
for (const decl of stmt2.declarationList.declarations) {
|
|
3391
|
-
|
|
3391
|
+
collectBindingNames5(decl.name, out);
|
|
3392
3392
|
}
|
|
3393
3393
|
} else if (ts4.isFunctionDeclaration(stmt2) && stmt2.name) {
|
|
3394
3394
|
out.add(stmt2.name.text);
|
|
@@ -3431,7 +3431,7 @@ function csrSubstituteOnce(value2, env) {
|
|
|
3431
3431
|
}
|
|
3432
3432
|
if (ts4.isArrowFunction(node) || ts4.isFunctionExpression(node)) {
|
|
3433
3433
|
const bound = /* @__PURE__ */ new Set();
|
|
3434
|
-
for (const p of node.parameters)
|
|
3434
|
+
for (const p of node.parameters) collectBindingNames5(p.name, bound);
|
|
3435
3435
|
if (node.body && ts4.isBlock(node.body)) {
|
|
3436
3436
|
collectBlockDeclarations(node.body, bound);
|
|
3437
3437
|
}
|
|
@@ -3535,7 +3535,12 @@ function buildSignalMemoEnv(signals, memos, propsObjectName) {
|
|
|
3535
3535
|
for (const m of memos) {
|
|
3536
3536
|
substitutions.set(m.name, {
|
|
3537
3537
|
kind: "call",
|
|
3538
|
-
|
|
3538
|
+
// Destructured mode (#2468): `templateComputation` (when present) has
|
|
3539
|
+
// bare destructured prop refs already rewritten to `_p.X` — the
|
|
3540
|
+
// spliced body lands in the module-scope template arrow, which is not
|
|
3541
|
+
// a closure over init's `const value = _p.value` extraction. Mirrors
|
|
3542
|
+
// the signal `templateInitialValue` handling above (#2265).
|
|
3543
|
+
replacement: extractMemoBodyExpr(m.templateComputation ?? m.computation),
|
|
3539
3544
|
freeIdentifiers: m.computationFreeIdentifiers ?? /* @__PURE__ */ new Set()
|
|
3540
3545
|
});
|
|
3541
3546
|
}
|
|
@@ -3558,6 +3563,16 @@ var init_csr_substitute = __esm({
|
|
|
3558
3563
|
}
|
|
3559
3564
|
});
|
|
3560
3565
|
|
|
3566
|
+
// ../jsx/src/adapters/child-scope.ts
|
|
3567
|
+
function derivesScopeFromSlot(comp) {
|
|
3568
|
+
return comp.slotId != null && comp.loopItemRoot !== true;
|
|
3569
|
+
}
|
|
3570
|
+
var init_child_scope = __esm({
|
|
3571
|
+
"../jsx/src/adapters/child-scope.ts"() {
|
|
3572
|
+
"use strict";
|
|
3573
|
+
}
|
|
3574
|
+
});
|
|
3575
|
+
|
|
3561
3576
|
// ../jsx/src/ir-to-client-js/html-template.ts
|
|
3562
3577
|
function createStringProtector() {
|
|
3563
3578
|
const strings = [];
|
|
@@ -3848,7 +3863,7 @@ function renderFlatMapClientBody(cb, restSpreadNames) {
|
|
|
3848
3863
|
rawLeaf: true,
|
|
3849
3864
|
renderLeaf: (ir) => {
|
|
3850
3865
|
const key = flatMapLeafKeyExpr(ir);
|
|
3851
|
-
const html = irToHtmlTemplate(stripLeafKeyAttr(ir), restSpreadNames, 1, void 0, void 0
|
|
3866
|
+
const html = irToHtmlTemplate(stripLeafKeyAttr(ir), restSpreadNames, 1, void 0, void 0);
|
|
3852
3867
|
return `({ k: ${key ?? "undefined"}, h: \`${html}\` })`;
|
|
3853
3868
|
}
|
|
3854
3869
|
});
|
|
@@ -3860,7 +3875,7 @@ function renderFlatMapProjectionClientBody(inner, restSpreadNames) {
|
|
|
3860
3875
|
const chained = applyLoopChain(inner);
|
|
3861
3876
|
const params = inner.index ? `(${inner.param}, ${inner.index})` : `(${inner.param})`;
|
|
3862
3877
|
const key = inner.key ? `(${inner.key})` : "undefined";
|
|
3863
|
-
const html = inner.children.map((c) => irToHtmlTemplate(escapeLeafTextExpressions(c), restSpreadNames, 1, void 0, void 0
|
|
3878
|
+
const html = inner.children.map((c) => irToHtmlTemplate(escapeLeafTextExpressions(c), restSpreadNames, 1, void 0, void 0)).join("");
|
|
3864
3879
|
return `${chained}.map(${params} => ({ k: ${key}, h: \`${html}\` }))`;
|
|
3865
3880
|
}
|
|
3866
3881
|
function escapeLeafTextExpressions(ir) {
|
|
@@ -3884,8 +3899,8 @@ function escapeLeafTextExpressions(ir) {
|
|
|
3884
3899
|
return ir;
|
|
3885
3900
|
}
|
|
3886
3901
|
}
|
|
3887
|
-
function irToHtmlTemplate(node, restSpreadNames, loopDepth = 0, loopParams, branchSlotsVar,
|
|
3888
|
-
const recurse = (n) => irToHtmlTemplate(n, restSpreadNames, loopDepth, loopParams, branchSlotsVar,
|
|
3902
|
+
function irToHtmlTemplate(node, restSpreadNames, loopDepth = 0, loopParams, branchSlotsVar, inHoistedChildren = false) {
|
|
3903
|
+
const recurse = (n) => irToHtmlTemplate(n, restSpreadNames, loopDepth, loopParams, branchSlotsVar, inHoistedChildren);
|
|
3889
3904
|
const wrapExpr = (expr) => wrapExprWithLoopParams(expr, loopParams);
|
|
3890
3905
|
const wrapInterpolation = (expr) => branchSlotsVar ? `__bfSlot(${expr}, ${branchSlotsVar})` : expr;
|
|
3891
3906
|
switch (node.type) {
|
|
@@ -3915,7 +3930,7 @@ function irToHtmlTemplate(node, restSpreadNames, loopDepth = 0, loopParams, bran
|
|
|
3915
3930
|
attrParts.push(`bf="${node.slotId}"`);
|
|
3916
3931
|
}
|
|
3917
3932
|
const attrs = attrParts.join(" ");
|
|
3918
|
-
const childrenRecurse = (n) => irToHtmlTemplate(n, restSpreadNames, loopDepth, loopParams, branchSlotsVar,
|
|
3933
|
+
const childrenRecurse = (n) => irToHtmlTemplate(n, restSpreadNames, loopDepth, loopParams, branchSlotsVar, false);
|
|
3919
3934
|
const children2 = dangerouslyHtmlChildren(node.attrs, (v) => wrapExpr(v.expr)) ?? node.children.map(childrenRecurse).join("");
|
|
3920
3935
|
if (children2 || !VOID_ELEMENTS.has(node.tag)) {
|
|
3921
3936
|
return `<${node.tag}${attrs ? " " + attrs : ""}>${children2}</${node.tag}>`;
|
|
@@ -3951,11 +3966,11 @@ function irToHtmlTemplate(node, restSpreadNames, loopDepth = 0, loopParams, bran
|
|
|
3951
3966
|
if (node.name === "Portal") {
|
|
3952
3967
|
return node.children.map(recurse).join("");
|
|
3953
3968
|
}
|
|
3954
|
-
const propsEntries = node.props.filter((p) => p.name !== "..." && !p.name.startsWith("...") && p.name !== "key").filter((p) => !(p.name.startsWith("on") && p.name.length > 2 && p.name[2] === p.name[2].toUpperCase())).map((p) => {
|
|
3969
|
+
const propsEntries = node.props.filter((p) => p.name !== "..." && !p.name.startsWith("...") && p.name !== "key").filter((p) => p.name !== "ref" && !(p.name.startsWith("on") && p.name.length > 2 && p.name[2] === p.name[2].toUpperCase())).map((p) => {
|
|
3955
3970
|
if (p.clientOnly) return null;
|
|
3956
3971
|
switch (p.value.kind) {
|
|
3957
3972
|
case "jsx-children": {
|
|
3958
|
-
const hoistedRecurse = (n) => irToHtmlTemplate(n, restSpreadNames, loopDepth, loopParams, branchSlotsVar,
|
|
3973
|
+
const hoistedRecurse = (n) => irToHtmlTemplate(n, restSpreadNames, loopDepth, loopParams, branchSlotsVar, true);
|
|
3959
3974
|
const childHtml = p.value.children.map((c) => hoistedRecurse(c)).join("");
|
|
3960
3975
|
return `${quotePropName(p.name)}: \`${childHtml}\``;
|
|
3961
3976
|
}
|
|
@@ -3968,7 +3983,7 @@ function irToHtmlTemplate(node, restSpreadNames, loopDepth = 0, loopParams, bran
|
|
|
3968
3983
|
case "expression":
|
|
3969
3984
|
case "template":
|
|
3970
3985
|
case "spread": {
|
|
3971
|
-
const expr = attrValueToString(p.value) ?? "undefined";
|
|
3986
|
+
const expr = attrValueToString(p.value, { useTemplate: true }) ?? "undefined";
|
|
3972
3987
|
return `${quotePropName(p.name)}: ${expr}`;
|
|
3973
3988
|
}
|
|
3974
3989
|
}
|
|
@@ -3978,11 +3993,11 @@ function irToHtmlTemplate(node, restSpreadNames, loopDepth = 0, loopParams, bran
|
|
|
3978
3993
|
const propsExpr = propsEntries.length > 0 ? `{${propsEntries.join(", ")}}` : "{}";
|
|
3979
3994
|
const keyProp = node.props.find((p) => p.name === "key");
|
|
3980
3995
|
const keyArg = keyProp ? `, ${attrValueToString(keyProp.value) ?? "undefined"}` : "";
|
|
3981
|
-
const slotArg =
|
|
3996
|
+
const slotArg = derivesScopeFromSlot(node) ? `, '${node.slotId}'` : "";
|
|
3982
3997
|
return `\${renderChild('${nameForRegistryRef(node.name)}', ${propsExpr}${keyArg || (slotArg ? ", undefined" : "")}${slotArg})}`;
|
|
3983
3998
|
}
|
|
3984
3999
|
case "loop": {
|
|
3985
|
-
const innerRecurse = (n) => irToHtmlTemplate(n, restSpreadNames, loopDepth + 1, loopParams, branchSlotsVar
|
|
4000
|
+
const innerRecurse = (n) => irToHtmlTemplate(n, restSpreadNames, loopDepth + 1, loopParams, branchSlotsVar);
|
|
3986
4001
|
let childTemplate = node.children.map(innerRecurse).join("");
|
|
3987
4002
|
if (node.bodyIsItemConditional && node.key) {
|
|
3988
4003
|
childTemplate = `${itemAnchorTemplate(node.key)}${childTemplate}`;
|
|
@@ -3995,11 +4010,11 @@ function irToHtmlTemplate(node, restSpreadNames, loopDepth = 0, loopParams, bran
|
|
|
3995
4010
|
let mapExpr;
|
|
3996
4011
|
if (node.flatMapCallback) {
|
|
3997
4012
|
const body2 = renderPreamble(node.flatMapCallback, {
|
|
3998
|
-
renderLeaf: (ir) => irToHtmlTemplate(stripLeafKeyAttr(ir), restSpreadNames, loopDepth + 1, loopParams, branchSlotsVar
|
|
4013
|
+
renderLeaf: (ir) => irToHtmlTemplate(stripLeafKeyAttr(ir), restSpreadNames, loopDepth + 1, loopParams, branchSlotsVar)
|
|
3999
4014
|
});
|
|
4000
4015
|
mapExpr = `\${${wrappedArray}.flatMap(${node.flatMapCallback.params} => ${body2}).join('')}`;
|
|
4001
4016
|
} else if (node.preamble) {
|
|
4002
|
-
const preamble = renderPreamble(node.preamble, { textVariant: "client", renderLeaf: (ir) => irToHtmlTemplate(ir, restSpreadNames, loopDepth + 1, loopParams, branchSlotsVar
|
|
4017
|
+
const preamble = renderPreamble(node.preamble, { textVariant: "client", renderLeaf: (ir) => irToHtmlTemplate(ir, restSpreadNames, loopDepth + 1, loopParams, branchSlotsVar) });
|
|
4003
4018
|
mapExpr = `\${${wrappedArray}.${iterMethod}(${callbackParam} => { ${preamble} return \`${childTemplate}\` }).join('')}`;
|
|
4004
4019
|
} else {
|
|
4005
4020
|
mapExpr = `\${${wrappedArray}.${iterMethod}(${callbackParam} => \`${childTemplate}\`).join('')}`;
|
|
@@ -4468,7 +4483,7 @@ function irToComponentTemplateWithOpts(node, opts) {
|
|
|
4468
4483
|
if (node.name === "Portal") {
|
|
4469
4484
|
return node.children.map(recurse).join("");
|
|
4470
4485
|
}
|
|
4471
|
-
const propsEntries = node.props.filter((p) => p.name !== "..." && !p.name.startsWith("...") && p.name !== "key").filter((p) => !(p.name.startsWith("on") && p.name.length > 2 && p.name[2] === p.name[2].toUpperCase())).map((p) => {
|
|
4486
|
+
const propsEntries = node.props.filter((p) => p.name !== "..." && !p.name.startsWith("...") && p.name !== "key").filter((p) => p.name !== "ref" && !(p.name.startsWith("on") && p.name.length > 2 && p.name[2] === p.name[2].toUpperCase())).map((p) => {
|
|
4472
4487
|
if (p.clientOnly) return null;
|
|
4473
4488
|
switch (p.value.kind) {
|
|
4474
4489
|
case "jsx-children": {
|
|
@@ -4578,7 +4593,7 @@ function canGenerateStaticTemplate(node, propNames, inlinableConstants, unsafeLo
|
|
|
4578
4593
|
return assertNever(node);
|
|
4579
4594
|
}
|
|
4580
4595
|
}
|
|
4581
|
-
function generateCsrTemplate(node, inlinableConstants, ctx2,
|
|
4596
|
+
function generateCsrTemplate(node, inlinableConstants, ctx2, restSpreadNames, propsObjectName, unsafeLocalNames, deferredChildSlots) {
|
|
4582
4597
|
const base = buildSignalMemoEnv(ctx2.signals, ctx2.memos, propsObjectName ?? null);
|
|
4583
4598
|
const csrEnv = { substitutions: new Map(base.substitutions), propsObjectName: base.propsObjectName };
|
|
4584
4599
|
if (inlinableConstants) {
|
|
@@ -4589,7 +4604,7 @@ function generateCsrTemplate(node, inlinableConstants, ctx2, insideLoop, restSpr
|
|
|
4589
4604
|
}
|
|
4590
4605
|
}
|
|
4591
4606
|
const effectiveUnsafeLocalNames = mergeCsrNullUnsafe(ctx2, unsafeLocalNames);
|
|
4592
|
-
return generateCsrTemplateWithOpts(node, { inlinableConstants, restSpreadNames, propsObjectName, csrEnv,
|
|
4607
|
+
return generateCsrTemplateWithOpts(node, { inlinableConstants, restSpreadNames, propsObjectName, csrEnv, unsafeLocalNames: effectiveUnsafeLocalNames, deferredChildSlots, loopDepth: -1 });
|
|
4593
4608
|
}
|
|
4594
4609
|
function mergeCsrNullUnsafe(ctx2, unsafeLocalNames) {
|
|
4595
4610
|
let merged = null;
|
|
@@ -4647,6 +4662,7 @@ function computeDeferredChildSlots(node, ctx2, inlinableConstants, unsafeLocalNa
|
|
|
4647
4662
|
if (n.slotId) {
|
|
4648
4663
|
const dropped = n.props.some((p) => {
|
|
4649
4664
|
if (p.name === "..." || p.name.startsWith("...") || p.name === "key") return false;
|
|
4665
|
+
if (p.name === "ref") return false;
|
|
4650
4666
|
if (p.name.startsWith("on") && p.name.length > 2 && p.name[2] === p.name[2].toUpperCase()) return false;
|
|
4651
4667
|
if (p.clientOnly) return false;
|
|
4652
4668
|
return propResolvesUnsafe(p, env, unsafeLocalNames);
|
|
@@ -4673,7 +4689,7 @@ function computeDeferredChildSlots(node, ctx2, inlinableConstants, unsafeLocalNa
|
|
|
4673
4689
|
return deferred;
|
|
4674
4690
|
}
|
|
4675
4691
|
function generateCsrTemplateWithOpts(node, opts) {
|
|
4676
|
-
const { restSpreadNames, propsObjectName, csrEnv,
|
|
4692
|
+
const { restSpreadNames, propsObjectName, csrEnv, unsafeLocalNames, loopDepth = 0 } = opts;
|
|
4677
4693
|
const env = csrEnv ?? { substitutions: /* @__PURE__ */ new Map(), propsObjectName: propsObjectName ?? null };
|
|
4678
4694
|
const transformExpr = (expr, templateExpr) => {
|
|
4679
4695
|
const source = templateExpr ?? expr;
|
|
@@ -4686,7 +4702,6 @@ function generateCsrTemplateWithOpts(node, opts) {
|
|
|
4686
4702
|
};
|
|
4687
4703
|
const recurse = (n) => generateCsrTemplateWithOpts(n, opts);
|
|
4688
4704
|
const childrenRecurse = (n) => generateCsrTemplateWithOpts(n, { ...opts, inHoistedChildren: false });
|
|
4689
|
-
const recurseInLoop = (n) => generateCsrTemplateWithOpts(n, { ...opts, insideLoop: true, loopDepth: loopDepth + 1, inHoistedChildren: false });
|
|
4690
4705
|
switch (node.type) {
|
|
4691
4706
|
case "element": {
|
|
4692
4707
|
const mergeCtx = {
|
|
@@ -4775,7 +4790,7 @@ function generateCsrTemplateWithOpts(node, opts) {
|
|
|
4775
4790
|
if (node.slotId && opts.deferredChildSlots?.has(node.slotId)) {
|
|
4776
4791
|
return `<div ${BF_PLACEHOLDER}="${node.slotId}"></div>`;
|
|
4777
4792
|
}
|
|
4778
|
-
const propsEntries = node.props.filter((p) => p.name !== "..." && !p.name.startsWith("...") && p.name !== "key").filter((p) => !(p.name.startsWith("on") && p.name.length > 2 && p.name[2] === p.name[2].toUpperCase())).map((p) => {
|
|
4793
|
+
const propsEntries = node.props.filter((p) => p.name !== "..." && !p.name.startsWith("...") && p.name !== "key").filter((p) => p.name !== "ref" && !(p.name.startsWith("on") && p.name.length > 2 && p.name[2] === p.name[2].toUpperCase())).map((p) => {
|
|
4779
4794
|
if (p.clientOnly) return null;
|
|
4780
4795
|
switch (p.value.kind) {
|
|
4781
4796
|
case "jsx-children": {
|
|
@@ -4808,7 +4823,7 @@ function generateCsrTemplateWithOpts(node, opts) {
|
|
|
4808
4823
|
const propsExpr = propsEntries.length > 0 ? `{${propsEntries.join(", ")}}` : "{}";
|
|
4809
4824
|
const keyProp = node.props.find((p) => p.name === "key");
|
|
4810
4825
|
const keyArg = keyProp ? `, ${transformKeyValue(keyProp.value, transformExpr)}` : "";
|
|
4811
|
-
const slotArg =
|
|
4826
|
+
const slotArg = derivesScopeFromSlot(node) ? `, '${node.slotId}'` : "";
|
|
4812
4827
|
return `\${renderChild('${nameForRegistryRef(node.name)}', ${propsExpr}${keyArg || (slotArg ? ", undefined" : "")}${slotArg})}`;
|
|
4813
4828
|
}
|
|
4814
4829
|
case "loop": {
|
|
@@ -4825,7 +4840,6 @@ function generateCsrTemplateWithOpts(node, opts) {
|
|
|
4825
4840
|
};
|
|
4826
4841
|
const recurseInLoopBody = (n) => generateCsrTemplateWithOpts(n, {
|
|
4827
4842
|
...opts,
|
|
4828
|
-
insideLoop: true,
|
|
4829
4843
|
loopDepth: loopDepth + 1,
|
|
4830
4844
|
inHoistedChildren: false,
|
|
4831
4845
|
loopBoundNames: boundHere,
|
|
@@ -4897,6 +4911,7 @@ var init_html_template = __esm({
|
|
|
4897
4911
|
init_csr_substitute();
|
|
4898
4912
|
init_src();
|
|
4899
4913
|
init_loop_chain();
|
|
4914
|
+
init_child_scope();
|
|
4900
4915
|
VOID_ELEMENTS = /* @__PURE__ */ new Set([
|
|
4901
4916
|
"area",
|
|
4902
4917
|
"base",
|
|
@@ -4946,17 +4961,94 @@ var init_html_template = __esm({
|
|
|
4946
4961
|
|
|
4947
4962
|
// ../jsx/src/prop-rewrite.ts
|
|
4948
4963
|
import ts5 from "typescript";
|
|
4964
|
+
function collectBindingNames(name2, out) {
|
|
4965
|
+
if (ts5.isIdentifier(name2)) {
|
|
4966
|
+
out.add(name2.text);
|
|
4967
|
+
return;
|
|
4968
|
+
}
|
|
4969
|
+
for (const el of name2.elements) {
|
|
4970
|
+
if (ts5.isBindingElement(el)) collectBindingNames(el.name, out);
|
|
4971
|
+
}
|
|
4972
|
+
}
|
|
4973
|
+
function scopeFrameOf(n) {
|
|
4974
|
+
if (ts5.isFunctionLike(n)) {
|
|
4975
|
+
const frame = /* @__PURE__ */ new Set();
|
|
4976
|
+
for (const p of n.parameters) collectBindingNames(p.name, frame);
|
|
4977
|
+
if ((ts5.isFunctionExpression(n) || ts5.isFunctionDeclaration(n)) && n.name) frame.add(n.name.text);
|
|
4978
|
+
return frame.size > 0 ? frame : null;
|
|
4979
|
+
}
|
|
4980
|
+
if (ts5.isBlock(n)) {
|
|
4981
|
+
const frame = /* @__PURE__ */ new Set();
|
|
4982
|
+
for (const st of n.statements) {
|
|
4983
|
+
if (ts5.isVariableStatement(st)) {
|
|
4984
|
+
for (const d of st.declarationList.declarations) collectBindingNames(d.name, frame);
|
|
4985
|
+
} else if (ts5.isFunctionDeclaration(st) && st.name) {
|
|
4986
|
+
frame.add(st.name.text);
|
|
4987
|
+
}
|
|
4988
|
+
}
|
|
4989
|
+
return frame.size > 0 ? frame : null;
|
|
4990
|
+
}
|
|
4991
|
+
if (ts5.isCatchClause(n) && n.variableDeclaration) {
|
|
4992
|
+
const frame = /* @__PURE__ */ new Set();
|
|
4993
|
+
collectBindingNames(n.variableDeclaration.name, frame);
|
|
4994
|
+
return frame.size > 0 ? frame : null;
|
|
4995
|
+
}
|
|
4996
|
+
return null;
|
|
4997
|
+
}
|
|
4998
|
+
function walkWithScope(root2, visit3) {
|
|
4999
|
+
const scopeStack = [];
|
|
5000
|
+
const isShadowed = (name2) => scopeStack.some((frame) => frame.has(name2));
|
|
5001
|
+
function rec(n, parent2) {
|
|
5002
|
+
const frame = scopeFrameOf(n);
|
|
5003
|
+
if (frame) scopeStack.push(frame);
|
|
5004
|
+
if (ts5.isIdentifier(n)) visit3(n, parent2, isShadowed(n.text));
|
|
5005
|
+
ts5.forEachChild(n, (child) => rec(child, n));
|
|
5006
|
+
if (frame) scopeStack.pop();
|
|
5007
|
+
}
|
|
5008
|
+
rec(root2);
|
|
5009
|
+
}
|
|
5010
|
+
function isNonValuePosition(n, parent2) {
|
|
5011
|
+
if (!parent2) return false;
|
|
5012
|
+
if (ts5.isPropertyAssignment(parent2) && parent2.name === n) return true;
|
|
5013
|
+
if (ts5.isPropertyAccessExpression(parent2) && parent2.name === n) return true;
|
|
5014
|
+
if (ts5.isQualifiedName(parent2) && parent2.right === n) return true;
|
|
5015
|
+
if ((ts5.isParameter(parent2) || ts5.isVariableDeclaration(parent2) || ts5.isBindingElement(parent2)) && parent2.name === n) return true;
|
|
5016
|
+
if (ts5.isTypeReferenceNode(parent2)) return true;
|
|
5017
|
+
return false;
|
|
5018
|
+
}
|
|
4949
5019
|
function collectAstPropRefs(node, propNames, out) {
|
|
4950
|
-
|
|
4951
|
-
if (
|
|
4952
|
-
|
|
4953
|
-
|
|
4954
|
-
|
|
4955
|
-
|
|
5020
|
+
walkWithScope(node, (n, parent2, shadowed) => {
|
|
5021
|
+
if (shadowed || !propNames.has(n.text)) return;
|
|
5022
|
+
if (parent2 && ts5.isShorthandPropertyAssignment(parent2) && parent2.name === n) return;
|
|
5023
|
+
if (isNonValuePosition(n, parent2)) return;
|
|
5024
|
+
out.add(n.text);
|
|
5025
|
+
});
|
|
5026
|
+
}
|
|
5027
|
+
function applyScopedPropRefRewrite(text, propRefs) {
|
|
5028
|
+
const prefix2 = "(";
|
|
5029
|
+
const sf = ts5.createSourceFile("__bf_prop_rewrite.ts", `${prefix2}${text}
|
|
5030
|
+
)`, ts5.ScriptTarget.Latest, true);
|
|
5031
|
+
const parseDiagnostics = sf.parseDiagnostics;
|
|
5032
|
+
if (parseDiagnostics && parseDiagnostics.length > 0) return null;
|
|
5033
|
+
const edits = [];
|
|
5034
|
+
walkWithScope(sf, (n, parent2, shadowed) => {
|
|
5035
|
+
if (shadowed || !propRefs.has(n.text)) return;
|
|
5036
|
+
if (isNonValuePosition(n, parent2)) return;
|
|
5037
|
+
const start2 = n.getStart(sf) - prefix2.length;
|
|
5038
|
+
const end2 = n.getEnd() - prefix2.length;
|
|
5039
|
+
if (start2 < 0 || end2 > text.length) return;
|
|
5040
|
+
if (parent2 && ts5.isShorthandPropertyAssignment(parent2) && parent2.name === n) {
|
|
5041
|
+
edits.push({ start: start2, end: end2, replacement: `${n.text}: ${PROPS_PARAM}.${n.text}` });
|
|
5042
|
+
return;
|
|
4956
5043
|
}
|
|
4957
|
-
|
|
5044
|
+
edits.push({ start: start2, end: end2, replacement: `${PROPS_PARAM}.${n.text}` });
|
|
5045
|
+
});
|
|
5046
|
+
if (edits.length === 0) return text;
|
|
5047
|
+
let result2 = text;
|
|
5048
|
+
for (const edit of edits.sort((a, b) => b.start - a.start)) {
|
|
5049
|
+
result2 = result2.slice(0, edit.start) + edit.replacement + result2.slice(edit.end);
|
|
4958
5050
|
}
|
|
4959
|
-
|
|
5051
|
+
return result2;
|
|
4960
5052
|
}
|
|
4961
5053
|
function applyRegexPropRefRewrite(text, propRefs) {
|
|
4962
5054
|
const { protect, restore } = createTemplateAwareStringProtector();
|
|
@@ -4983,7 +5075,7 @@ function rewriteBarePropRefs(text, node, propNames, extraPropRefs) {
|
|
|
4983
5075
|
}
|
|
4984
5076
|
}
|
|
4985
5077
|
if (foundPropRefs.size === 0) return void 0;
|
|
4986
|
-
return applyRegexPropRefRewrite(text, foundPropRefs);
|
|
5078
|
+
return applyScopedPropRefRewrite(text, foundPropRefs) ?? applyRegexPropRefRewrite(text, foundPropRefs);
|
|
4987
5079
|
}
|
|
4988
5080
|
var init_prop_rewrite = __esm({
|
|
4989
5081
|
"../jsx/src/prop-rewrite.ts"() {
|
|
@@ -5355,6 +5447,30 @@ function typeNodeToTypeInfo(typeNode, sourceFile, rawOf) {
|
|
|
5355
5447
|
if (ts7.isArrayTypeNode(typeNode)) {
|
|
5356
5448
|
return { kind: "array", raw, elementType: recurse(typeNode.elementType) };
|
|
5357
5449
|
}
|
|
5450
|
+
if (ts7.isLiteralTypeNode(typeNode)) {
|
|
5451
|
+
const lit = typeNode.literal;
|
|
5452
|
+
if (ts7.isStringLiteral(lit) || ts7.isNoSubstitutionTemplateLiteral(lit)) {
|
|
5453
|
+
return { kind: "primitive", raw, primitive: "string", literalValue: lit.text };
|
|
5454
|
+
}
|
|
5455
|
+
if (ts7.isNumericLiteral(lit)) {
|
|
5456
|
+
return { kind: "primitive", raw, primitive: "number", literalValue: lit.text };
|
|
5457
|
+
}
|
|
5458
|
+
if (ts7.isPrefixUnaryExpression(lit) && lit.operator === ts7.SyntaxKind.MinusToken && ts7.isNumericLiteral(lit.operand)) {
|
|
5459
|
+
return { kind: "primitive", raw, primitive: "number", literalValue: `-${lit.operand.text}` };
|
|
5460
|
+
}
|
|
5461
|
+
if (lit.kind === ts7.SyntaxKind.TrueKeyword || lit.kind === ts7.SyntaxKind.FalseKeyword) {
|
|
5462
|
+
return {
|
|
5463
|
+
kind: "primitive",
|
|
5464
|
+
raw,
|
|
5465
|
+
primitive: "boolean",
|
|
5466
|
+
literalValue: lit.kind === ts7.SyntaxKind.TrueKeyword ? "true" : "false"
|
|
5467
|
+
};
|
|
5468
|
+
}
|
|
5469
|
+
if (lit.kind === ts7.SyntaxKind.NullKeyword) {
|
|
5470
|
+
return { kind: "primitive", raw, primitive: "null" };
|
|
5471
|
+
}
|
|
5472
|
+
return { kind: "unknown", raw };
|
|
5473
|
+
}
|
|
5358
5474
|
if (ts7.isUnionTypeNode(typeNode)) {
|
|
5359
5475
|
return { kind: "union", raw, unionTypes: typeNode.types.map(recurse) };
|
|
5360
5476
|
}
|
|
@@ -5647,8 +5763,7 @@ function baseTypeName(raw) {
|
|
|
5647
5763
|
return (idx === -1 ? raw : raw.slice(0, idx)).trim();
|
|
5648
5764
|
}
|
|
5649
5765
|
function isNullishArm(t) {
|
|
5650
|
-
|
|
5651
|
-
return t.kind === "unknown" && (t.raw === "null" || t.raw === "undefined");
|
|
5766
|
+
return t.kind === "primitive" && (t.primitive === "null" || t.primitive === "undefined");
|
|
5652
5767
|
}
|
|
5653
5768
|
function stripUnion(type2) {
|
|
5654
5769
|
if (!type2 || type2.kind !== "union" || !type2.unionTypes) return type2;
|
|
@@ -6339,14 +6454,16 @@ function collectSignal(node, ctx2) {
|
|
|
6339
6454
|
const pattern = node.name;
|
|
6340
6455
|
const callExpr = node.initializer;
|
|
6341
6456
|
const elements2 = pattern.elements;
|
|
6342
|
-
|
|
6457
|
+
const getterElided = elements2.length === 2 && ts8.isOmittedExpression(elements2[0]);
|
|
6458
|
+
if (elements2.length < 1 || elements2.length > 2 || !getterElided && (!ts8.isBindingElement(elements2[0]) || !ts8.isIdentifier(elements2[0].name))) {
|
|
6343
6459
|
return;
|
|
6344
6460
|
}
|
|
6345
6461
|
if (elements2.length === 2 && (!ts8.isBindingElement(elements2[1]) || !ts8.isIdentifier(elements2[1].name))) {
|
|
6346
6462
|
return;
|
|
6347
6463
|
}
|
|
6348
|
-
const getter = elements2[0].name.text;
|
|
6349
6464
|
const setter = elements2.length === 2 && ts8.isBindingElement(elements2[1]) && ts8.isIdentifier(elements2[1].name) ? elements2[1].name.text : null;
|
|
6465
|
+
if (getterElided && !setter) return;
|
|
6466
|
+
const getter = getterElided ? `__bfGet_${setter}` : elements2[0].name.text;
|
|
6350
6467
|
const initialValue = callExpr.arguments[0] ? ctx2.getJS(callExpr.arguments[0]) : "";
|
|
6351
6468
|
const typedInitialValue = callExpr.arguments[0] ? callExpr.arguments[0].getText(ctx2.sourceFile) : void 0;
|
|
6352
6469
|
let type2 = { kind: "unknown", raw: "unknown" };
|
|
@@ -6367,6 +6484,7 @@ function collectSignal(node, ctx2) {
|
|
|
6367
6484
|
ctx2.signals.push({
|
|
6368
6485
|
getter,
|
|
6369
6486
|
setter,
|
|
6487
|
+
getterElided: getterElided || void 0,
|
|
6370
6488
|
initialValue,
|
|
6371
6489
|
typedInitialValue: typedInitialValue !== initialValue ? typedInitialValue : void 0,
|
|
6372
6490
|
templateInitialValue,
|
|
@@ -6538,9 +6656,17 @@ function collectMemo(node, ctx2) {
|
|
|
6538
6656
|
const blockBody = arrowNode && ts8.isArrowFunction(arrowNode) && ts8.isBlock(arrowNode.body) ? arrowNode.body : void 0;
|
|
6539
6657
|
const parsedBlock = blockBody ? parseBlockBodyTolerant(blockBody, ctx2.sourceFile, (node2) => ctx2.getJS(node2)) : void 0;
|
|
6540
6658
|
const parsedBlockComplete = parsedBlock && blockBody ? parsedBlock.length === blockBody.statements.length : void 0;
|
|
6659
|
+
let templateComputation;
|
|
6660
|
+
if (!ctx2.propsObjectName && callExpr.arguments[0]) {
|
|
6661
|
+
const propNames = new Set(ctx2.propsParams.map((p) => p.name));
|
|
6662
|
+
if (propNames.size > 0) {
|
|
6663
|
+
templateComputation = rewriteBarePropRefs(computation, callExpr.arguments[0], propNames);
|
|
6664
|
+
}
|
|
6665
|
+
}
|
|
6541
6666
|
ctx2.memos.push({
|
|
6542
6667
|
name: name2,
|
|
6543
6668
|
computation,
|
|
6669
|
+
templateComputation,
|
|
6544
6670
|
parsedBlock,
|
|
6545
6671
|
parsedBlockComplete,
|
|
6546
6672
|
typedComputation: typedComputation !== computation ? typedComputation : void 0,
|
|
@@ -9627,8 +9753,10 @@ function deriveFormat(locale, probeOptions) {
|
|
|
9627
9753
|
return { pattern, names };
|
|
9628
9754
|
}
|
|
9629
9755
|
function unionMemberLiteral(member) {
|
|
9630
|
-
|
|
9631
|
-
|
|
9756
|
+
if (member.kind === "primitive" && member.primitive === "string" && member.literalValue !== void 0) {
|
|
9757
|
+
return member.literalValue;
|
|
9758
|
+
}
|
|
9759
|
+
return null;
|
|
9632
9760
|
}
|
|
9633
9761
|
function resolveLocaleUnionMembers(locale, metadata) {
|
|
9634
9762
|
let sourcePropName = null;
|
|
@@ -10185,6 +10313,14 @@ function jsxToIR(analyzer) {
|
|
|
10185
10313
|
function buildIRRoot(analyzer) {
|
|
10186
10314
|
if (analyzer.conditionalReturns.length > 0) {
|
|
10187
10315
|
const ctx3 = createTransformContext(analyzer);
|
|
10316
|
+
const allReactiveNoLocals = analyzer.jsxReturn != null && analyzer.conditionalReturns.every(
|
|
10317
|
+
(cr) => cr.scopeVariables.length === 0 && exprCallsReactiveGetters(cr.condition, ctx3)
|
|
10318
|
+
);
|
|
10319
|
+
if (allReactiveNoLocals) {
|
|
10320
|
+
const chain = buildIfStatementChain(analyzer, ctx3, { asConditional: true });
|
|
10321
|
+
if (!chain) return null;
|
|
10322
|
+
return chain.type === "conditional" ? wrapInScopeElement(chain) : chain;
|
|
10323
|
+
}
|
|
10188
10324
|
return buildIfStatementChain(analyzer, ctx3);
|
|
10189
10325
|
}
|
|
10190
10326
|
if (!analyzer.jsxReturn) return null;
|
|
@@ -10336,6 +10472,50 @@ function transformJsxElement(node, ctx2) {
|
|
|
10336
10472
|
}
|
|
10337
10473
|
return transformHtmlElement(node, ctx2, tagName2);
|
|
10338
10474
|
}
|
|
10475
|
+
function lowerFormControlValueSsr(tagName2, attrs, children2) {
|
|
10476
|
+
if (tagName2 !== "textarea" && tagName2 !== "select") return;
|
|
10477
|
+
const valueAttr = attrs.find((a) => a.name === "value");
|
|
10478
|
+
if (!valueAttr || valueAttr.clientOnly || valueAttr.value.kind !== "expression") return;
|
|
10479
|
+
const { expr, templateExpr } = valueAttr.value;
|
|
10480
|
+
valueAttr.clientOnly = true;
|
|
10481
|
+
if (tagName2 === "textarea") {
|
|
10482
|
+
if (children2.length > 0) return;
|
|
10483
|
+
children2.push({
|
|
10484
|
+
type: "expression",
|
|
10485
|
+
expr,
|
|
10486
|
+
// The client-side registration template interpolates slotless
|
|
10487
|
+
// expression children RAW (no text-slot `escapeText` wrapper), so a
|
|
10488
|
+
// value containing `</textarea>` would break out of the element on
|
|
10489
|
+
// CSR mount. Escape in the client-only template variant; `expr`
|
|
10490
|
+
// stays clean for the SSR adapters, whose template engines (and
|
|
10491
|
+
// hono/jsx) already escape text children natively.
|
|
10492
|
+
templateExpr: `escapeText(${templateExpr ?? expr})`,
|
|
10493
|
+
typeInfo: null,
|
|
10494
|
+
reactive: false,
|
|
10495
|
+
slotId: null,
|
|
10496
|
+
loc: valueAttr.loc,
|
|
10497
|
+
origin: { phase: "ssr", scope: "template", effect: "pure" }
|
|
10498
|
+
});
|
|
10499
|
+
return;
|
|
10500
|
+
}
|
|
10501
|
+
const selectedFor = (optValue) => AttrValueOf.expression(
|
|
10502
|
+
`(${expr}) === ${JSON.stringify(optValue)}`,
|
|
10503
|
+
templateExpr !== void 0 ? { templateExpr: `(${templateExpr}) === ${JSON.stringify(optValue)}` } : void 0
|
|
10504
|
+
);
|
|
10505
|
+
const distribute = (nodes) => {
|
|
10506
|
+
for (const n of nodes) {
|
|
10507
|
+
if (n.type === "element" && n.tag === "option") {
|
|
10508
|
+
if (n.attrs.some((a) => a.name === "selected")) continue;
|
|
10509
|
+
const optValue = n.attrs.find((a) => a.name === "value");
|
|
10510
|
+
if (!optValue || optValue.value.kind !== "literal") continue;
|
|
10511
|
+
n.attrs.push({ name: "selected", value: selectedFor(optValue.value.value), loc: n.loc });
|
|
10512
|
+
} else if (n.type === "fragment" || n.type === "element" && n.tag === "optgroup") {
|
|
10513
|
+
distribute(n.children);
|
|
10514
|
+
}
|
|
10515
|
+
}
|
|
10516
|
+
};
|
|
10517
|
+
distribute(children2);
|
|
10518
|
+
}
|
|
10339
10519
|
function transformHtmlElement(node, ctx2, tagName2) {
|
|
10340
10520
|
const { attrs, events, ref } = processAttributes(
|
|
10341
10521
|
node.openingElement.attributes,
|
|
@@ -10344,6 +10524,7 @@ function transformHtmlElement(node, ctx2, tagName2) {
|
|
|
10344
10524
|
const needsScope = ctx2.isRoot;
|
|
10345
10525
|
ctx2.isRoot = false;
|
|
10346
10526
|
const children2 = transformChildren(node.children, ctx2);
|
|
10527
|
+
lowerFormControlValueSsr(tagName2, attrs, children2);
|
|
10347
10528
|
const needsSlot = events.length > 0 || hasDynamicContent(children2) || hasReactiveAttributes(attrs, ctx2) || ref !== null;
|
|
10348
10529
|
const slotId = needsSlot ? generateSlotId(ctx2) : null;
|
|
10349
10530
|
if (slotId) {
|
|
@@ -10380,6 +10561,8 @@ function transformSelfClosingElement(node, ctx2) {
|
|
|
10380
10561
|
return transformSelfClosingComponent(node, ctx2, resolved ?? tagName2);
|
|
10381
10562
|
}
|
|
10382
10563
|
const { attrs, events, ref } = processAttributes(node.attributes, ctx2);
|
|
10564
|
+
const selfClosingChildren = [];
|
|
10565
|
+
lowerFormControlValueSsr(tagName2, attrs, selfClosingChildren);
|
|
10383
10566
|
const needsSlot = events.length > 0 || hasReactiveAttributes(attrs, ctx2) || ref !== null;
|
|
10384
10567
|
const slotId = needsSlot ? generateSlotId(ctx2) : null;
|
|
10385
10568
|
const needsScope = ctx2.isRoot;
|
|
@@ -10390,7 +10573,7 @@ function transformSelfClosingElement(node, ctx2) {
|
|
|
10390
10573
|
attrs,
|
|
10391
10574
|
events,
|
|
10392
10575
|
ref,
|
|
10393
|
-
children:
|
|
10576
|
+
children: selfClosingChildren,
|
|
10394
10577
|
slotId,
|
|
10395
10578
|
needsScope,
|
|
10396
10579
|
loc: getSourceLocation(node, ctx2.sourceFile, ctx2.filePath)
|
|
@@ -11747,6 +11930,15 @@ function branchHasNoElement(node) {
|
|
|
11747
11930
|
}
|
|
11748
11931
|
return true;
|
|
11749
11932
|
}
|
|
11933
|
+
function tagLoopItemRootComponents(nodes) {
|
|
11934
|
+
for (const node of nodes) {
|
|
11935
|
+
if (node.type === "component") {
|
|
11936
|
+
node.loopItemRoot = true;
|
|
11937
|
+
} else if (node.type === "conditional") {
|
|
11938
|
+
tagLoopItemRootComponents([node.whenTrue, node.whenFalse]);
|
|
11939
|
+
}
|
|
11940
|
+
}
|
|
11941
|
+
}
|
|
11750
11942
|
function loopBodyItemConditional(children2) {
|
|
11751
11943
|
const real = children2.filter(
|
|
11752
11944
|
(c) => !(c.type === "text" && typeof c.value === "string" && !c.value.trim())
|
|
@@ -12015,10 +12207,10 @@ function transformMapCall(node, ctx2, isClientOnly = false, method2 = "map") {
|
|
|
12015
12207
|
const pre = multiReturn.preamble ?? [];
|
|
12016
12208
|
if (pre.length > 0) {
|
|
12017
12209
|
preamble = preambleFromValueStatements(pre, ctx2);
|
|
12018
|
-
if (!isClientOnly && !(ctx2.analyzer.acceptsCallbackBody?.("map") ?? false)) {
|
|
12210
|
+
if (!preamble.declarations && !isClientOnly && !(ctx2.analyzer.acceptsCallbackBody?.("map") ?? false)) {
|
|
12019
12211
|
ctx2.analyzer.errors.push(
|
|
12020
12212
|
createError(ErrorCodes.UNSUPPORTED_JSX_PATTERN, loc, {
|
|
12021
|
-
message: "A .map() callback body with a
|
|
12213
|
+
message: "A .map() callback body with a preamble before its branches cannot be lowered to a template: the preamble is not a sequence of value declarations, so this backend has no per-row local to carry into the branches.",
|
|
12022
12214
|
suggestion: {
|
|
12023
12215
|
message: "Add /* @client */ to evaluate this expression on the client only"
|
|
12024
12216
|
}
|
|
@@ -12104,6 +12296,20 @@ function transformMapCall(node, ctx2, isClientOnly = false, method2 = "map") {
|
|
|
12104
12296
|
}
|
|
12105
12297
|
if (valueStmts.length > 0) {
|
|
12106
12298
|
preamble = preambleFromValueStatements(valueStmts, ctx2);
|
|
12299
|
+
if (!preamble.declarations && !isClientOnly && !(ctx2.analyzer.acceptsCallbackBody?.("map") ?? false)) {
|
|
12300
|
+
ctx2.analyzer.errors.push(
|
|
12301
|
+
createError(
|
|
12302
|
+
ErrorCodes.UNSUPPORTED_JSX_PATTERN,
|
|
12303
|
+
getSourceLocation(body2, ctx2.sourceFile, ctx2.filePath),
|
|
12304
|
+
{
|
|
12305
|
+
message: "A .map() callback preamble that is not a sequence of value declarations cannot be lowered to a template: this backend can declare a per-row local, but cannot run arbitrary statements per row.",
|
|
12306
|
+
suggestion: {
|
|
12307
|
+
message: "Add /* @client */ to evaluate this expression on the client only"
|
|
12308
|
+
}
|
|
12309
|
+
}
|
|
12310
|
+
)
|
|
12311
|
+
);
|
|
12312
|
+
}
|
|
12107
12313
|
}
|
|
12108
12314
|
}
|
|
12109
12315
|
}
|
|
@@ -12202,6 +12408,7 @@ function transformMapCall(node, ctx2, isClientOnly = false, method2 = "map") {
|
|
|
12202
12408
|
);
|
|
12203
12409
|
preamble = void 0;
|
|
12204
12410
|
}
|
|
12411
|
+
tagLoopItemRootComponents(children2);
|
|
12205
12412
|
let childComponent;
|
|
12206
12413
|
if (children2.length === 1 && children2[0].type === "component") {
|
|
12207
12414
|
const comp = children2[0];
|
|
@@ -12222,6 +12429,9 @@ function transformMapCall(node, ctx2, isClientOnly = false, method2 = "map") {
|
|
|
12222
12429
|
const isDirectPropArray = method2 !== "flatMap" && isArrayExprDirectPropRef(arrayExpr, ctx2);
|
|
12223
12430
|
const isStaticArray = !isSignalOrMemoArray(array, ctx2) && !isDirectPropArray && !hasCalls && !objectIteration;
|
|
12224
12431
|
const preambleRegions = preamble && !isStaticArray ? collectPreambleRegions(children2, new Set(preamble.declaredNames), ctx2) : void 0;
|
|
12432
|
+
if (preamble && !isStaticArray) {
|
|
12433
|
+
markPreambleAttrSlots(children2, new Set(preamble.declaredNames), ctx2);
|
|
12434
|
+
}
|
|
12225
12435
|
const nestedComponents = collectNestedComponents(children2).filter((c) => c.name !== childComponent?.name);
|
|
12226
12436
|
return {
|
|
12227
12437
|
type: "loop",
|
|
@@ -12460,19 +12670,56 @@ function collectPreambleRegions(nodes, declared, ctx2) {
|
|
|
12460
12670
|
visit3(nodes);
|
|
12461
12671
|
return regions;
|
|
12462
12672
|
}
|
|
12463
|
-
function
|
|
12673
|
+
function markPreambleAttrSlots(nodes, declared, ctx2) {
|
|
12674
|
+
const readsDeclared = (attr) => {
|
|
12675
|
+
if (attr.name === "key") return false;
|
|
12676
|
+
const value2 = attr.value;
|
|
12677
|
+
if (value2.kind !== "expression" && value2.kind !== "template") return false;
|
|
12678
|
+
const refs = attr.freeIdentifiers ?? extractFreeIdentifiersFromText(attrValueText(value2));
|
|
12679
|
+
for (const r2 of refs) if (declared.has(r2)) return true;
|
|
12680
|
+
return false;
|
|
12681
|
+
};
|
|
12682
|
+
const visit3 = (list) => {
|
|
12683
|
+
for (const node of list) {
|
|
12684
|
+
switch (node.type) {
|
|
12685
|
+
case "element":
|
|
12686
|
+
if (!node.slotId && node.attrs.some(readsDeclared)) node.slotId = generateSlotId(ctx2);
|
|
12687
|
+
visit3(node.children);
|
|
12688
|
+
break;
|
|
12689
|
+
case "fragment":
|
|
12690
|
+
visit3(node.children);
|
|
12691
|
+
break;
|
|
12692
|
+
case "conditional":
|
|
12693
|
+
visit3([node.whenTrue, ...node.whenFalse ? [node.whenFalse] : []]);
|
|
12694
|
+
break;
|
|
12695
|
+
}
|
|
12696
|
+
}
|
|
12697
|
+
};
|
|
12698
|
+
visit3(nodes);
|
|
12699
|
+
}
|
|
12700
|
+
function attrValueText(value2) {
|
|
12701
|
+
if (value2.kind === "expression") return value2.expr;
|
|
12702
|
+
if (value2.kind !== "template") return "";
|
|
12703
|
+
const out = [];
|
|
12704
|
+
for (const p of value2.parts) {
|
|
12705
|
+
if (p.type === "ternary") out.push(p.condition, p.whenTrue, p.whenFalse);
|
|
12706
|
+
else if (p.type === "lookup") out.push(p.key);
|
|
12707
|
+
}
|
|
12708
|
+
return out.join(" ");
|
|
12709
|
+
}
|
|
12710
|
+
function collectBindingNames2(name2, out) {
|
|
12464
12711
|
if (ts11.isIdentifier(name2)) {
|
|
12465
12712
|
out.add(name2.text);
|
|
12466
12713
|
return;
|
|
12467
12714
|
}
|
|
12468
12715
|
for (const el of name2.elements) {
|
|
12469
|
-
if (ts11.isBindingElement(el))
|
|
12716
|
+
if (ts11.isBindingElement(el)) collectBindingNames2(el.name, out);
|
|
12470
12717
|
}
|
|
12471
12718
|
}
|
|
12472
12719
|
function collectPreambleDeclaredNames(stmt, out) {
|
|
12473
12720
|
if (ts11.isVariableStatement(stmt)) {
|
|
12474
12721
|
for (const decl of stmt.declarationList.declarations) {
|
|
12475
|
-
|
|
12722
|
+
collectBindingNames2(decl.name, out);
|
|
12476
12723
|
}
|
|
12477
12724
|
} else if (ts11.isFunctionDeclaration(stmt) && stmt.name) {
|
|
12478
12725
|
out.add(stmt.name.text);
|
|
@@ -12497,9 +12744,28 @@ function preambleFromValueStatements(statements, ctx2) {
|
|
|
12497
12744
|
ssrText: tsxSourceText(typedParts.join(" ")),
|
|
12498
12745
|
declaredNames: [...declared],
|
|
12499
12746
|
// Value-only preambles accumulate no JSX, so no child needs the array join.
|
|
12500
|
-
builderNames: []
|
|
12747
|
+
builderNames: [],
|
|
12748
|
+
declarations: neutralPreambleDeclarations(statements, ctx2) ?? void 0
|
|
12501
12749
|
};
|
|
12502
12750
|
}
|
|
12751
|
+
function neutralPreambleDeclarations(statements, ctx2) {
|
|
12752
|
+
const out = [];
|
|
12753
|
+
for (const stmt of statements) {
|
|
12754
|
+
if (!ts11.isVariableStatement(stmt)) return null;
|
|
12755
|
+
for (const decl of stmt.declarationList.declarations) {
|
|
12756
|
+
if (!ts11.isIdentifier(decl.name)) return null;
|
|
12757
|
+
if (!decl.initializer) return null;
|
|
12758
|
+
const valueParsed = tsNodeToParsedExpr(decl.initializer);
|
|
12759
|
+
if (!isSupported(valueParsed).supported) return null;
|
|
12760
|
+
out.push({
|
|
12761
|
+
name: decl.name.text,
|
|
12762
|
+
valueParsed,
|
|
12763
|
+
raw: decl.initializer.getText(ctx2.sourceFile)
|
|
12764
|
+
});
|
|
12765
|
+
}
|
|
12766
|
+
}
|
|
12767
|
+
return out.length > 0 ? out : null;
|
|
12768
|
+
}
|
|
12503
12769
|
function trimPreambleSegments(segments) {
|
|
12504
12770
|
const last = segments[segments.length - 1];
|
|
12505
12771
|
if (last?.kind === "js") {
|
|
@@ -12846,6 +13112,7 @@ function parseTemplateLiteral(expr, ctx2) {
|
|
|
12846
13112
|
}
|
|
12847
13113
|
function tryResolveTemplateSpanFromConst(expr, ctx2) {
|
|
12848
13114
|
if (ts11.isIdentifier(expr)) {
|
|
13115
|
+
if (ctx2.loopParams.has(expr.text)) return null;
|
|
12849
13116
|
const constInfo = findLocalConst(expr.text, ctx2.analyzer);
|
|
12850
13117
|
if (!constInfo) return null;
|
|
12851
13118
|
const ast = parseConstInitializer(constInfo);
|
|
@@ -12857,6 +13124,7 @@ function tryResolveTemplateSpanFromConst(expr, ctx2) {
|
|
|
12857
13124
|
}
|
|
12858
13125
|
if (ts11.isElementAccessExpression(expr)) {
|
|
12859
13126
|
if (!ts11.isIdentifier(expr.expression)) return null;
|
|
13127
|
+
if (ctx2.loopParams.has(expr.expression.text)) return null;
|
|
12860
13128
|
const constInfo = findLocalConst(expr.expression.text, ctx2.analyzer);
|
|
12861
13129
|
if (!constInfo) return null;
|
|
12862
13130
|
const ast = parseConstInitializer(constInfo);
|
|
@@ -12930,6 +13198,9 @@ function tryResolveIdentifierAsTemplateLiteral(ident, ctx2) {
|
|
|
12930
13198
|
if (ts11.isNoSubstitutionTemplateLiteral(ast) || ts11.isStringLiteral(ast)) {
|
|
12931
13199
|
return [{ type: "string", value: ast.text }];
|
|
12932
13200
|
}
|
|
13201
|
+
if (ts11.isElementAccessExpression(ast) && !ts11.isStringLiteralLike(ast.argumentExpression) && !ts11.isNumericLiteral(ast.argumentExpression)) {
|
|
13202
|
+
return tryResolveTemplateSpanFromConst(ast, ctx2);
|
|
13203
|
+
}
|
|
12933
13204
|
if (!ts11.isTemplateExpression(ast)) return null;
|
|
12934
13205
|
let resolvedAny = false;
|
|
12935
13206
|
const parts = [];
|
|
@@ -13158,8 +13429,11 @@ function processComponentProps(attributes2, ctx2) {
|
|
|
13158
13429
|
}
|
|
13159
13430
|
let value2 = getAttributeValue(attr, ctx2);
|
|
13160
13431
|
if (value2.kind === "template") {
|
|
13161
|
-
|
|
13162
|
-
|
|
13432
|
+
const collapsed = templatePartsToJsString(value2.parts);
|
|
13433
|
+
const collapsedTemplate = templatePartsToJsString(value2.parts, { useTemplate: true });
|
|
13434
|
+
value2 = AttrValueOf.expression(collapsed, {
|
|
13435
|
+
parts: value2.parts,
|
|
13436
|
+
...collapsedTemplate !== collapsed && { templateExpr: collapsedTemplate }
|
|
13163
13437
|
});
|
|
13164
13438
|
} else if (value2.kind === "boolean-attr") {
|
|
13165
13439
|
value2 = AttrValueOf.booleanShorthand();
|
|
@@ -13188,18 +13462,20 @@ function processComponentProps(attributes2, ctx2) {
|
|
|
13188
13462
|
}
|
|
13189
13463
|
return props;
|
|
13190
13464
|
}
|
|
13191
|
-
function templatePartsToJsString(parts) {
|
|
13465
|
+
function templatePartsToJsString(parts, opts) {
|
|
13192
13466
|
let result2 = "`";
|
|
13193
13467
|
for (const part of parts) {
|
|
13194
13468
|
if (part.type === "string") {
|
|
13195
|
-
result2 += part.value;
|
|
13469
|
+
result2 += opts?.useTemplate && part.templateValue ? part.templateValue : part.value;
|
|
13196
13470
|
} else if (part.type === "ternary") {
|
|
13197
|
-
|
|
13471
|
+
const cond = opts?.useTemplate && part.templateCondition ? part.templateCondition : part.condition;
|
|
13472
|
+
result2 += `\${${cond} ? '${part.whenTrue}' : '${part.whenFalse}'}`;
|
|
13198
13473
|
} else if (part.type === "lookup") {
|
|
13474
|
+
const key = opts?.useTemplate && part.templateKey ? part.templateKey : part.key;
|
|
13199
13475
|
const obj = "{" + Object.entries(part.cases).map(
|
|
13200
13476
|
([k, v]) => `${JSON.stringify(k)}: ${JSON.stringify(v)}`
|
|
13201
13477
|
).join(", ") + "}";
|
|
13202
|
-
result2 += `\${(${obj})[${
|
|
13478
|
+
result2 += `\${(${obj})[${key}]}`;
|
|
13203
13479
|
}
|
|
13204
13480
|
}
|
|
13205
13481
|
result2 += "`";
|
|
@@ -13400,11 +13676,12 @@ function replaceBranchLocalRefs(text, branchNames, resolve12) {
|
|
|
13400
13676
|
const pattern = new RegExp(`(?<![\\w$])(${branchNames.join("|")})(?![\\w$])`, "g");
|
|
13401
13677
|
return replaceInExprContexts(text, pattern, (_match, name2) => resolve12(name2));
|
|
13402
13678
|
}
|
|
13403
|
-
function buildIfStatementChain(analyzer, ctx2) {
|
|
13679
|
+
function buildIfStatementChain(analyzer, ctx2, opts) {
|
|
13404
13680
|
const conditionalReturns = analyzer.conditionalReturns;
|
|
13681
|
+
const asConditional = opts?.asConditional === true;
|
|
13405
13682
|
let alternate = null;
|
|
13406
13683
|
if (analyzer.jsxReturn) {
|
|
13407
|
-
ctx2.isRoot =
|
|
13684
|
+
ctx2.isRoot = !asConditional;
|
|
13408
13685
|
alternate = transformNode(analyzer.jsxReturn, ctx2);
|
|
13409
13686
|
}
|
|
13410
13687
|
for (let i = conditionalReturns.length - 1; i >= 0; i--) {
|
|
@@ -13468,7 +13745,7 @@ function buildIfStatementChain(analyzer, ctx2) {
|
|
|
13468
13745
|
ctx2.getJS = substitutedGetJS;
|
|
13469
13746
|
ctx2.analyzer.getJS = substitutedGetJS;
|
|
13470
13747
|
}
|
|
13471
|
-
ctx2.isRoot =
|
|
13748
|
+
ctx2.isRoot = !asConditional;
|
|
13472
13749
|
let consequent;
|
|
13473
13750
|
try {
|
|
13474
13751
|
consequent = transformNode(condReturn.jsxReturn, ctx2);
|
|
@@ -13502,6 +13779,27 @@ function buildIfStatementChain(analyzer, ctx2) {
|
|
|
13502
13779
|
analyzer.sourceFile,
|
|
13503
13780
|
analyzer.filePath
|
|
13504
13781
|
);
|
|
13782
|
+
if (asConditional && alternate) {
|
|
13783
|
+
const conditional = {
|
|
13784
|
+
type: "conditional",
|
|
13785
|
+
condition,
|
|
13786
|
+
templateCondition,
|
|
13787
|
+
conditionType: null,
|
|
13788
|
+
reactive: true,
|
|
13789
|
+
whenTrue: consequent,
|
|
13790
|
+
whenFalse: alternate,
|
|
13791
|
+
slotId: generateSlotId(ctx2),
|
|
13792
|
+
loc,
|
|
13793
|
+
origin: {
|
|
13794
|
+
phase: "tick",
|
|
13795
|
+
scope: "template",
|
|
13796
|
+
effect: "pure",
|
|
13797
|
+
freeRefs: resolveFreeRefs(condReturn.condition, makeBindingEnv(ctx2))
|
|
13798
|
+
}
|
|
13799
|
+
};
|
|
13800
|
+
alternate = conditional;
|
|
13801
|
+
continue;
|
|
13802
|
+
}
|
|
13505
13803
|
const ifStmt = {
|
|
13506
13804
|
type: "if-statement",
|
|
13507
13805
|
condition,
|
|
@@ -13846,7 +14144,11 @@ function collectLoopChildReactiveTexts(node, ctx2, loopParam, loopParamBindings,
|
|
|
13846
14144
|
});
|
|
13847
14145
|
return texts;
|
|
13848
14146
|
}
|
|
13849
|
-
function
|
|
14147
|
+
function anyNameIn(names, set) {
|
|
14148
|
+
for (const n of names) if (set.has(n)) return true;
|
|
14149
|
+
return false;
|
|
14150
|
+
}
|
|
14151
|
+
function collectLoopChildReactiveAttrs(node, ctx2, loopParam, loopParamBindings, stopAtReactiveConditionals = false, preambleNames) {
|
|
13850
14152
|
const attrs = [];
|
|
13851
14153
|
traverseElements(node, (el) => {
|
|
13852
14154
|
if (el.slotId) {
|
|
@@ -13857,14 +14159,16 @@ function collectLoopChildReactiveAttrs(node, ctx2, loopParam, loopParamBindings,
|
|
|
13857
14159
|
const valueStr = attrValueToString(attr.value);
|
|
13858
14160
|
if (!valueStr) continue;
|
|
13859
14161
|
const expanded = expandConstantForReactivity(valueStr, ctx2, attr.freeIdentifiers);
|
|
13860
|
-
const
|
|
14162
|
+
const readsPreamble = preambleNames !== void 0 && preambleNames.size > 0 && anyNameIn(expanded.freeIds ?? extractFreeIdentifiersFromText(expanded.expr), preambleNames);
|
|
14163
|
+
const reactive = classifyReactivity(expanded.expr, ctx2, loopParam, loopParamBindings, expanded.freeIds).kind !== "none" || readsPreamble || attr.callsReactiveGetters || attr.hasFunctionCalls;
|
|
13861
14164
|
if (!attr.clientOnly && !reactive) continue;
|
|
13862
14165
|
attrs.push({
|
|
13863
14166
|
childSlotId: el.slotId,
|
|
13864
14167
|
attrName: attr.name,
|
|
13865
14168
|
expression: expanded.expr,
|
|
13866
14169
|
...pickAttrMetaFromIR(attr),
|
|
13867
|
-
...expanded.freeIds !== void 0 && { freeIdentifiers: expanded.freeIds }
|
|
14170
|
+
...expanded.freeIds !== void 0 && { freeIdentifiers: expanded.freeIds },
|
|
14171
|
+
...readsPreamble && { readsPreamble: true }
|
|
13868
14172
|
});
|
|
13869
14173
|
}
|
|
13870
14174
|
}
|
|
@@ -13877,6 +14181,7 @@ var init_reactivity = __esm({
|
|
|
13877
14181
|
init_types();
|
|
13878
14182
|
init_utils();
|
|
13879
14183
|
init_prop_handling();
|
|
14184
|
+
init_csr_substitute();
|
|
13880
14185
|
init_walker();
|
|
13881
14186
|
}
|
|
13882
14187
|
});
|
|
@@ -14152,10 +14457,11 @@ function collectInnerLoops(nodes, siblingOffsets, outerLoopParam, ctx2, options2
|
|
|
14152
14457
|
const template = n.children.map((c) => irToPlaceholderTemplate(c, void 0, emitDepth, loopParamsForTemplate)).join("");
|
|
14153
14458
|
const refsOuter = outerLoopParam ? new RegExp(`\\b${outerLoopParam}\\b`).test(n.array) : false;
|
|
14154
14459
|
const bindings = emptyLoopChildBindings();
|
|
14460
|
+
const innerPreambleNames = preambleNamesOf(n);
|
|
14155
14461
|
if (ctx2) {
|
|
14156
14462
|
for (const child of n.children) {
|
|
14157
14463
|
bindings.reactiveTexts.push(...collectLoopChildReactiveTexts(child, ctx2, n.param, n.paramBindings));
|
|
14158
|
-
bindings.reactiveAttrs.push(...collectLoopChildReactiveAttrs(child, ctx2, n.param, n.paramBindings));
|
|
14464
|
+
bindings.reactiveAttrs.push(...collectLoopChildReactiveAttrs(child, ctx2, n.param, n.paramBindings, false, innerPreambleNames));
|
|
14159
14465
|
bindings.refs.push(...collectLoopChildRefs(child));
|
|
14160
14466
|
}
|
|
14161
14467
|
}
|
|
@@ -14369,7 +14675,7 @@ function collectElements(node, ctx2, siblingOffsets, insideConditional = false)
|
|
|
14369
14675
|
if (!l.slotId || inCond) return;
|
|
14370
14676
|
const projectionInner = l.method === "flatMap" && l.children.length === 1 && l.children[0].type === "loop" ? l.children[0] : void 0;
|
|
14371
14677
|
const childHandlers = [];
|
|
14372
|
-
const bindings = projectionInner ? emptyLoopChildBindings() : collectLoopChildBindings(l.children, ctx2, siblingOffsets, l.param, l.paramBindings);
|
|
14678
|
+
const bindings = projectionInner ? emptyLoopChildBindings() : collectLoopChildBindings(l.children, ctx2, siblingOffsets, l.param, l.paramBindings, preambleNamesOf(l));
|
|
14373
14679
|
if (!projectionInner) {
|
|
14374
14680
|
for (const child of l.children) {
|
|
14375
14681
|
childHandlers.push(...collectEventHandlersFromIR(child));
|
|
@@ -14391,15 +14697,7 @@ function collectElements(node, ctx2, siblingOffsets, insideConditional = false)
|
|
|
14391
14697
|
if (l.childComponent) {
|
|
14392
14698
|
template = "";
|
|
14393
14699
|
if (l.isStaticArray && l.children[0]) {
|
|
14394
|
-
staticItemTemplate = irToHtmlTemplate(
|
|
14395
|
-
l.children[0],
|
|
14396
|
-
buildRestSpreadNames(ctx2),
|
|
14397
|
-
0,
|
|
14398
|
-
void 0,
|
|
14399
|
-
void 0,
|
|
14400
|
-
/* insideLoop */
|
|
14401
|
-
true
|
|
14402
|
-
);
|
|
14700
|
+
staticItemTemplate = irToHtmlTemplate(l.children[0], buildRestSpreadNames(ctx2), 0, void 0, void 0);
|
|
14403
14701
|
}
|
|
14404
14702
|
} else if (l.children[0] && !projectionInner) {
|
|
14405
14703
|
const loopParamSpec = [{ param: l.param, bindings: l.paramBindings }];
|
|
@@ -14629,7 +14927,7 @@ function collectBranchLoops(node, ctx2, siblingOffsets) {
|
|
|
14629
14927
|
} else {
|
|
14630
14928
|
childTemplate = n.children.map((c) => irToHtmlTemplate(c, void 0, 0, branchLoopParamSpec)).join("");
|
|
14631
14929
|
}
|
|
14632
|
-
const branchBindings = ctx2 && !projectionInner ? collectLoopChildBindings(n.children, ctx2, siblingOffsets, n.param, n.paramBindings) : emptyLoopChildBindings();
|
|
14930
|
+
const branchBindings = ctx2 && !projectionInner ? collectLoopChildBindings(n.children, ctx2, siblingOffsets, n.param, n.paramBindings, preambleNamesOf(n)) : emptyLoopChildBindings();
|
|
14633
14931
|
loops.push({
|
|
14634
14932
|
kind: "branch",
|
|
14635
14933
|
array: n.array,
|
|
@@ -14710,11 +15008,16 @@ function collectBranchConditionals(node, ctx2, siblingOffsets) {
|
|
|
14710
15008
|
});
|
|
14711
15009
|
return result2;
|
|
14712
15010
|
}
|
|
14713
|
-
function
|
|
15011
|
+
function preambleNamesOf(loop) {
|
|
15012
|
+
if (loop.isStaticArray) return void 0;
|
|
15013
|
+
const declared = loop.preamble?.declaredNames;
|
|
15014
|
+
return declared && declared.length > 0 ? new Set(declared) : void 0;
|
|
15015
|
+
}
|
|
15016
|
+
function collectLoopChildBindings(children2, ctx2, siblingOffsets, loopParam, loopParamBindings, preambleNames) {
|
|
14714
15017
|
const bindings = emptyLoopChildBindings();
|
|
14715
15018
|
for (const child of children2) {
|
|
14716
15019
|
bindings.events.push(...collectLoopChildEventsWithNesting(child));
|
|
14717
|
-
bindings.reactiveAttrs.push(...collectLoopChildReactiveAttrs(child, ctx2, loopParam, loopParamBindings, true));
|
|
15020
|
+
bindings.reactiveAttrs.push(...collectLoopChildReactiveAttrs(child, ctx2, loopParam, loopParamBindings, true, preambleNames));
|
|
14718
15021
|
bindings.reactiveTexts.push(...collectLoopChildReactiveTexts(child, ctx2, loopParam, loopParamBindings, true));
|
|
14719
15022
|
bindings.refs.push(...collectLoopChildRefs(child));
|
|
14720
15023
|
bindings.conditionals.push(...collectLoopChildConditionals(child, ctx2, siblingOffsets, loopParam, loopParamBindings));
|
|
@@ -16396,7 +16699,6 @@ function emitRegistrationAndHydration(lines, ctx2, _ir, graph, inlinability) {
|
|
|
16396
16699
|
_ir.root,
|
|
16397
16700
|
csrInlinableConstants,
|
|
16398
16701
|
ctx2,
|
|
16399
|
-
void 0,
|
|
16400
16702
|
restSpreadNames,
|
|
16401
16703
|
ctx2.propsObjectName,
|
|
16402
16704
|
unsafeLocalNames,
|
|
@@ -16800,6 +17102,7 @@ function buildSignalPlan(signal2, ctx2, lookups) {
|
|
|
16800
17102
|
kind: "signal",
|
|
16801
17103
|
getter: signal2.getter,
|
|
16802
17104
|
setter: signal2.setter,
|
|
17105
|
+
getterElided: signal2.getterElided,
|
|
16803
17106
|
initialValueExpr: "",
|
|
16804
17107
|
controlledEffect: null,
|
|
16805
17108
|
initializerOverride: `${factory}()`
|
|
@@ -16814,6 +17117,7 @@ function buildSignalPlan(signal2, ctx2, lookups) {
|
|
|
16814
17117
|
kind: "signal",
|
|
16815
17118
|
getter: signal2.getter,
|
|
16816
17119
|
setter: signal2.setter,
|
|
17120
|
+
getterElided: signal2.getterElided,
|
|
16817
17121
|
initialValueExpr: resolveSignalInitialValue(signal2, ctx2, lookups),
|
|
16818
17122
|
controlledEffect,
|
|
16819
17123
|
branchCondition: signal2.branchCondition,
|
|
@@ -16890,9 +17194,10 @@ function bfIdArg(bfId) {
|
|
|
16890
17194
|
return bfId ? `, ${JSON.stringify(bfId)}` : "";
|
|
16891
17195
|
}
|
|
16892
17196
|
function emitSignal(lines, plan) {
|
|
17197
|
+
const getterSlot = plan.getterElided ? "" : plan.getter;
|
|
16893
17198
|
if (plan.initializerOverride) {
|
|
16894
17199
|
if (plan.setter) {
|
|
16895
|
-
lines.push(` const [${
|
|
17200
|
+
lines.push(` const [${getterSlot}, ${plan.setter}] = ${plan.initializerOverride}`);
|
|
16896
17201
|
} else {
|
|
16897
17202
|
lines.push(` const [${plan.getter}] = ${plan.initializerOverride}`);
|
|
16898
17203
|
}
|
|
@@ -16901,9 +17206,9 @@ function emitSignal(lines, plan) {
|
|
|
16901
17206
|
const id2 = bfIdArg(plan.bfId);
|
|
16902
17207
|
if (plan.branchCondition) {
|
|
16903
17208
|
if (plan.setter) {
|
|
16904
|
-
lines.push(` let ${plan.getter}, ${plan.setter}`);
|
|
17209
|
+
lines.push(plan.getterElided ? ` let ${plan.setter}` : ` let ${plan.getter}, ${plan.setter}`);
|
|
16905
17210
|
lines.push(` if (${plan.branchCondition}) {`);
|
|
16906
|
-
lines.push(` ;[${
|
|
17211
|
+
lines.push(` ;[${getterSlot}, ${plan.setter}] = createSignal(${plan.initialValueExpr}${id2})`);
|
|
16907
17212
|
lines.push(` }`);
|
|
16908
17213
|
} else {
|
|
16909
17214
|
lines.push(` let ${plan.getter}`);
|
|
@@ -16914,7 +17219,7 @@ function emitSignal(lines, plan) {
|
|
|
16914
17219
|
return;
|
|
16915
17220
|
}
|
|
16916
17221
|
if (plan.setter) {
|
|
16917
|
-
lines.push(` const [${
|
|
17222
|
+
lines.push(` const [${getterSlot}, ${plan.setter}] = createSignal(${plan.initialValueExpr}${id2})`);
|
|
16918
17223
|
} else {
|
|
16919
17224
|
lines.push(` const [${plan.getter}] = createSignal(${plan.initialValueExpr}${id2})`);
|
|
16920
17225
|
}
|
|
@@ -17541,7 +17846,7 @@ var init_shared = __esm({
|
|
|
17541
17846
|
// ../jsx/src/ir-to-client-js/plan/build-static-array-child-init.ts
|
|
17542
17847
|
function staticPreludeStatements(preamble) {
|
|
17543
17848
|
return preamble ? [renderPreamble(preamble, {
|
|
17544
|
-
renderLeaf: (ir) => irToHtmlTemplate(ir, void 0, 1, void 0, void 0
|
|
17849
|
+
renderLeaf: (ir) => irToHtmlTemplate(ir, void 0, 1, void 0, void 0)
|
|
17545
17850
|
})] : [];
|
|
17546
17851
|
}
|
|
17547
17852
|
function buildStaticArrayChildInitsPlan(ctx2) {
|
|
@@ -18134,7 +18439,8 @@ function buildReactiveEffectsPlan(args2) {
|
|
|
18134
18439
|
attrs: slotAttrs.map((attr) => ({
|
|
18135
18440
|
attrName: attr.attrName,
|
|
18136
18441
|
wrappedExpression: wrap(attr.expression),
|
|
18137
|
-
meta: pickAttrMeta(attr)
|
|
18442
|
+
meta: pickAttrMeta(attr),
|
|
18443
|
+
...attr.readsPreamble && { readsPreamble: true }
|
|
18138
18444
|
}))
|
|
18139
18445
|
});
|
|
18140
18446
|
}
|
|
@@ -18342,7 +18648,7 @@ function buildReactiveEmit(inner, level, wrapOuter, uidSuffix, outerLoopParam, o
|
|
|
18342
18648
|
] : [{ param: inner.param, bindings: inner.paramBindings }];
|
|
18343
18649
|
preludeStatements.push(renderPreamble(inner.preamble, {
|
|
18344
18650
|
transformJs: (t) => wrapInner(wrapOuter(t)),
|
|
18345
|
-
renderLeaf: (ir) => irToHtmlTemplate(ir, void 0, 1, leafLoopParams, void 0
|
|
18651
|
+
renderLeaf: (ir) => irToHtmlTemplate(ir, void 0, 1, leafLoopParams, void 0)
|
|
18346
18652
|
}));
|
|
18347
18653
|
}
|
|
18348
18654
|
const childRefs = buildChildRefBindings(inner.bindings.refs, inner.param, inner.paramBindings);
|
|
@@ -18367,7 +18673,7 @@ function buildStaticEmit(inner, level, uidSuffix) {
|
|
|
18367
18673
|
if (indexAlias) preludeStatements.push(indexAlias);
|
|
18368
18674
|
if (inner.preamble) {
|
|
18369
18675
|
preludeStatements.push(renderPreamble(inner.preamble, {
|
|
18370
|
-
renderLeaf: (ir) => irToHtmlTemplate(ir, void 0, 1, void 0, void 0
|
|
18676
|
+
renderLeaf: (ir) => irToHtmlTemplate(ir, void 0, 1, void 0, void 0)
|
|
18371
18677
|
}));
|
|
18372
18678
|
}
|
|
18373
18679
|
return {
|
|
@@ -18409,7 +18715,7 @@ function buildTopLevelCompositePlan(elem, profileComponentName) {
|
|
|
18409
18715
|
indexParam: elem.index || "__idx",
|
|
18410
18716
|
mapPreambleWrapped: elem.preamble ? renderPreamble(elem.preamble, {
|
|
18411
18717
|
transformJs: wrap,
|
|
18412
|
-
renderLeaf: (ir) => irToHtmlTemplate(ir, void 0, 1, [{ param: elem.param, bindings: elem.paramBindings }], void 0
|
|
18718
|
+
renderLeaf: (ir) => irToHtmlTemplate(ir, void 0, 1, [{ param: elem.param, bindings: elem.paramBindings }], void 0)
|
|
18413
18719
|
}) : "",
|
|
18414
18720
|
template: elem.template,
|
|
18415
18721
|
outerComps: filterCondCompsOut(outerCompsByDepth, elem.bindings.conditionals),
|
|
@@ -18462,7 +18768,7 @@ function buildBranchCompositePlan(loop, cv, profileComponentName) {
|
|
|
18462
18768
|
indexParam: loop.index || "__idx",
|
|
18463
18769
|
mapPreambleWrapped: loop.preamble ? renderPreamble(loop.preamble, {
|
|
18464
18770
|
transformJs: wrap,
|
|
18465
|
-
renderLeaf: (ir) => irToHtmlTemplate(ir, void 0, 1, [{ param: loop.param, bindings: loop.paramBindings }], void 0
|
|
18771
|
+
renderLeaf: (ir) => irToHtmlTemplate(ir, void 0, 1, [{ param: loop.param, bindings: loop.paramBindings }], void 0)
|
|
18466
18772
|
}) : "",
|
|
18467
18773
|
template: loop.template,
|
|
18468
18774
|
outerComps: filterCondCompsOut(outerCompsByDepth, loop.bindings.conditionals),
|
|
@@ -18544,7 +18850,7 @@ function buildDynamicLoopDelegationPlan(elem, profileComponentName) {
|
|
|
18544
18850
|
// BUG-3: it rewrote leaf refs to accessor-call form (`t().name`),
|
|
18545
18851
|
// which throws since `t` is a plain object in this scope.
|
|
18546
18852
|
mapPreamble: elem.preamble ? renderPreamble(elem.preamble, {
|
|
18547
|
-
renderLeaf: (ir) => irToHtmlTemplate(ir, void 0, 1, void 0, void 0
|
|
18853
|
+
renderLeaf: (ir) => irToHtmlTemplate(ir, void 0, 1, void 0, void 0)
|
|
18548
18854
|
}) : null,
|
|
18549
18855
|
mapPreambleDeclaredNames: elem.preamble?.declaredNames ?? []
|
|
18550
18856
|
})
|
|
@@ -18566,7 +18872,7 @@ function buildBranchLoopDelegationPlan(loop, cv, profileComponentName) {
|
|
|
18566
18872
|
// See note in `buildDynamicLoopDelegationPlan` above (BUG-3): no
|
|
18567
18873
|
// loopParams spec — leaf refs must stay in plain-object form here.
|
|
18568
18874
|
mapPreamble: loop.preamble ? renderPreamble(loop.preamble, {
|
|
18569
|
-
renderLeaf: (ir) => irToHtmlTemplate(ir, void 0, 1, void 0, void 0
|
|
18875
|
+
renderLeaf: (ir) => irToHtmlTemplate(ir, void 0, 1, void 0, void 0)
|
|
18570
18876
|
}) : null,
|
|
18571
18877
|
mapPreambleDeclaredNames: loop.preamble?.declaredNames ?? []
|
|
18572
18878
|
})
|
|
@@ -18588,7 +18894,7 @@ function buildStaticArrayDelegationPlan(elem, profileComponentName) {
|
|
|
18588
18894
|
// See note in `buildDynamicLoopDelegationPlan` above (BUG-3): no
|
|
18589
18895
|
// loopParams spec — leaf refs must stay in plain-object form here.
|
|
18590
18896
|
mapPreamble: elem.preamble ? renderPreamble(elem.preamble, {
|
|
18591
|
-
renderLeaf: (ir) => irToHtmlTemplate(ir, void 0, 1, void 0, void 0
|
|
18897
|
+
renderLeaf: (ir) => irToHtmlTemplate(ir, void 0, 1, void 0, void 0)
|
|
18592
18898
|
}) : null,
|
|
18593
18899
|
mapPreambleDeclaredNames: elem.preamble?.declaredNames ?? [],
|
|
18594
18900
|
offset: elem.offset ?? null,
|
|
@@ -18630,36 +18936,219 @@ var init_build_event_delegation = __esm({
|
|
|
18630
18936
|
}
|
|
18631
18937
|
});
|
|
18632
18938
|
|
|
18939
|
+
// ../jsx/src/ir-to-client-js/control-flow/plan/lazy-conditional.ts
|
|
18940
|
+
function wiringOn(branch) {
|
|
18941
|
+
const found = [];
|
|
18942
|
+
if (branch.childComponents.length > 0) found.push("child components");
|
|
18943
|
+
if (branch.innerLoops && branch.innerLoops.length > 0) found.push("an inner loop");
|
|
18944
|
+
if (branch.conditionals && branch.conditionals.length > 0) found.push("a nested conditional");
|
|
18945
|
+
if (branch.events && branch.events.length > 0) found.push("events");
|
|
18946
|
+
if (branch.reactiveAttrs && branch.reactiveAttrs.length > 0) found.push("reactive attrs");
|
|
18947
|
+
if (branch.reactiveTexts && branch.reactiveTexts.length > 0) found.push("reactive text");
|
|
18948
|
+
return found;
|
|
18949
|
+
}
|
|
18950
|
+
function analyzeLazyConditional(cond, indexParam, arms) {
|
|
18951
|
+
for (const [label2, branch] of [["true", cond.whenTrue], ["false", cond.whenFalse]]) {
|
|
18952
|
+
const wiring = wiringOn(branch);
|
|
18953
|
+
if (wiring.length > 0) {
|
|
18954
|
+
return NO(`conditional on slot ${cond.slotId}: its ${label2} arm owns ${wiring.join(" + ")}`);
|
|
18955
|
+
}
|
|
18956
|
+
}
|
|
18957
|
+
for (const [label2, html] of [["true", arms.whenTrueHtml], ["false", arms.whenFalseHtml]]) {
|
|
18958
|
+
if (html.includes("bf-cond-start:")) {
|
|
18959
|
+
return NO(`conditional on slot ${cond.slotId}: its ${label2} arm is a fragment conditional`);
|
|
18960
|
+
}
|
|
18961
|
+
if (!html.includes(`bf-c="${cond.slotId}"`)) {
|
|
18962
|
+
return NO(`conditional on slot ${cond.slotId}: its ${label2} arm has no single bf-c root`);
|
|
18963
|
+
}
|
|
18964
|
+
if (html.includes("${")) {
|
|
18965
|
+
return NO(`conditional on slot ${cond.slotId}: its ${label2} arm interpolates a value`);
|
|
18966
|
+
}
|
|
18967
|
+
}
|
|
18968
|
+
if (!cond.conditionFreeIdentifiers) {
|
|
18969
|
+
return NO(`conditional on slot ${cond.slotId}: condition has no analyzable identifier set`);
|
|
18970
|
+
}
|
|
18971
|
+
if (cond.conditionFreeIdentifiers.has(indexParam)) {
|
|
18972
|
+
return NO(`conditional on slot ${cond.slotId}: condition reads the loop index parameter '${indexParam}'`);
|
|
18973
|
+
}
|
|
18974
|
+
return {
|
|
18975
|
+
lazySafe: true,
|
|
18976
|
+
facts: {
|
|
18977
|
+
slotId: cond.slotId,
|
|
18978
|
+
condition: cond.condition,
|
|
18979
|
+
whenTrueHtml: arms.whenTrueHtml,
|
|
18980
|
+
whenFalseHtml: arms.whenFalseHtml
|
|
18981
|
+
}
|
|
18982
|
+
};
|
|
18983
|
+
}
|
|
18984
|
+
var NO;
|
|
18985
|
+
var init_lazy_conditional = __esm({
|
|
18986
|
+
"../jsx/src/ir-to-client-js/control-flow/plan/lazy-conditional.ts"() {
|
|
18987
|
+
"use strict";
|
|
18988
|
+
NO = (reason2) => ({ lazySafe: false, reason: reason2 });
|
|
18989
|
+
}
|
|
18990
|
+
});
|
|
18991
|
+
|
|
18992
|
+
// ../jsx/src/ir-to-client-js/control-flow/plan/lazy-preamble.ts
|
|
18993
|
+
import ts15 from "typescript";
|
|
18994
|
+
function analyzeLazyPreamble(preamble, indexParam, primableNames) {
|
|
18995
|
+
if (!preamble) return NO_PREAMBLE;
|
|
18996
|
+
if (preamble.builderNames.length > 0) {
|
|
18997
|
+
return NO2(`map-callback preamble accumulates JSX leaves (${preamble.builderNames.join(", ")})`);
|
|
18998
|
+
}
|
|
18999
|
+
for (const seg of preamble.segments) {
|
|
19000
|
+
if (seg.kind !== "js") return NO2("map-callback preamble contains a JSX leaf");
|
|
19001
|
+
}
|
|
19002
|
+
const text = preambleAnalysisText(preamble);
|
|
19003
|
+
if (text.trim().length === 0) return NO_PREAMBLE;
|
|
19004
|
+
const declaredNames = /* @__PURE__ */ new Set();
|
|
19005
|
+
const sf = ts15.createSourceFile(
|
|
19006
|
+
"__lazy_preamble__.ts",
|
|
19007
|
+
text,
|
|
19008
|
+
ts15.ScriptTarget.Latest,
|
|
19009
|
+
/* setParentNodes */
|
|
19010
|
+
true,
|
|
19011
|
+
ts15.ScriptKind.TS
|
|
19012
|
+
);
|
|
19013
|
+
for (const stmt of sf.statements) {
|
|
19014
|
+
if (!ts15.isVariableStatement(stmt)) {
|
|
19015
|
+
return NO2(`map-callback preamble has a non-declaration statement (${ts15.SyntaxKind[stmt.kind]})`);
|
|
19016
|
+
}
|
|
19017
|
+
const isConst = (stmt.declarationList.flags & ts15.NodeFlags.Const) !== 0;
|
|
19018
|
+
if (!isConst) return NO2("map-callback preamble declares a mutable binding (let/var)");
|
|
19019
|
+
for (const decl of stmt.declarationList.declarations) {
|
|
19020
|
+
collectBindingNames3(decl.name, declaredNames);
|
|
19021
|
+
if (!decl.initializer) {
|
|
19022
|
+
return NO2("map-callback preamble has a declaration with no initializer");
|
|
19023
|
+
}
|
|
19024
|
+
const impure = findImpureNode(decl.initializer, primableNames);
|
|
19025
|
+
if (impure) {
|
|
19026
|
+
return NO2(`map-callback preamble initializer is not re-runnable (${impure})`);
|
|
19027
|
+
}
|
|
19028
|
+
}
|
|
19029
|
+
}
|
|
19030
|
+
for (const name2 of declaredNames) {
|
|
19031
|
+
if (primableNames.has(name2)) {
|
|
19032
|
+
return NO2(`map-callback preamble shadows the signal/memo getter '${name2}'`);
|
|
19033
|
+
}
|
|
19034
|
+
}
|
|
19035
|
+
const readNames = extractFreeIdentifiersFromStatementText(text);
|
|
19036
|
+
if (readNames.has(indexParam) && !declaredNames.has(indexParam)) {
|
|
19037
|
+
return NO2(`map-callback preamble reads the loop index parameter '${indexParam}'`);
|
|
19038
|
+
}
|
|
19039
|
+
const freeNames = new Set(readNames);
|
|
19040
|
+
for (const declared of declaredNames) freeNames.delete(declared);
|
|
19041
|
+
return { lazySafe: true, facts: { declaredNames, freeNames } };
|
|
19042
|
+
}
|
|
19043
|
+
function collectBindingNames3(name2, out) {
|
|
19044
|
+
if (ts15.isIdentifier(name2)) {
|
|
19045
|
+
out.add(name2.text);
|
|
19046
|
+
return;
|
|
19047
|
+
}
|
|
19048
|
+
for (const element of name2.elements) {
|
|
19049
|
+
if (ts15.isOmittedExpression(element)) continue;
|
|
19050
|
+
collectBindingNames3(element.name, out);
|
|
19051
|
+
}
|
|
19052
|
+
}
|
|
19053
|
+
function findImpureNode(root2, primableNames) {
|
|
19054
|
+
let found = null;
|
|
19055
|
+
const visit3 = (node) => {
|
|
19056
|
+
if (found) return;
|
|
19057
|
+
if (ts15.isCallExpression(node)) {
|
|
19058
|
+
const callee = node.expression;
|
|
19059
|
+
const isSignalRead = ts15.isIdentifier(callee) && primableNames.has(callee.text) && node.arguments.length === 0 && node.questionDotToken === void 0;
|
|
19060
|
+
if (!isSignalRead) {
|
|
19061
|
+
found = `call to ${callee.getText(callee.getSourceFile())}`;
|
|
19062
|
+
return;
|
|
19063
|
+
}
|
|
19064
|
+
}
|
|
19065
|
+
if (ts15.isNewExpression(node)) {
|
|
19066
|
+
found = "new expression";
|
|
19067
|
+
return;
|
|
19068
|
+
}
|
|
19069
|
+
if (ts15.isTaggedTemplateExpression(node)) {
|
|
19070
|
+
found = "tagged template";
|
|
19071
|
+
return;
|
|
19072
|
+
}
|
|
19073
|
+
if (ts15.isAwaitExpression(node)) {
|
|
19074
|
+
found = "await";
|
|
19075
|
+
return;
|
|
19076
|
+
}
|
|
19077
|
+
if (ts15.isYieldExpression(node)) {
|
|
19078
|
+
found = "yield";
|
|
19079
|
+
return;
|
|
19080
|
+
}
|
|
19081
|
+
if (ts15.isPrefixUnaryExpression(node) || ts15.isPostfixUnaryExpression(node)) {
|
|
19082
|
+
const op = node.operator;
|
|
19083
|
+
if (op === ts15.SyntaxKind.PlusPlusToken || op === ts15.SyntaxKind.MinusMinusToken) {
|
|
19084
|
+
found = "increment/decrement";
|
|
19085
|
+
return;
|
|
19086
|
+
}
|
|
19087
|
+
}
|
|
19088
|
+
if (ts15.isDeleteExpression(node)) {
|
|
19089
|
+
found = "delete";
|
|
19090
|
+
return;
|
|
19091
|
+
}
|
|
19092
|
+
if (ts15.isFunctionExpression(node) || ts15.isArrowFunction(node) || ts15.isClassExpression(node)) {
|
|
19093
|
+
found = "function or class expression";
|
|
19094
|
+
return;
|
|
19095
|
+
}
|
|
19096
|
+
if (ts15.isBinaryExpression(node) && isAssignmentOperator(node.operatorToken.kind)) {
|
|
19097
|
+
found = "assignment";
|
|
19098
|
+
return;
|
|
19099
|
+
}
|
|
19100
|
+
ts15.forEachChild(node, visit3);
|
|
19101
|
+
};
|
|
19102
|
+
visit3(root2);
|
|
19103
|
+
return found;
|
|
19104
|
+
}
|
|
19105
|
+
function isAssignmentOperator(kind2) {
|
|
19106
|
+
return kind2 >= ts15.SyntaxKind.FirstAssignment && kind2 <= ts15.SyntaxKind.LastAssignment;
|
|
19107
|
+
}
|
|
19108
|
+
var NO_PREAMBLE, NO2;
|
|
19109
|
+
var init_lazy_preamble = __esm({
|
|
19110
|
+
"../jsx/src/ir-to-client-js/control-flow/plan/lazy-preamble.ts"() {
|
|
19111
|
+
"use strict";
|
|
19112
|
+
init_types();
|
|
19113
|
+
init_csr_substitute();
|
|
19114
|
+
NO_PREAMBLE = {
|
|
19115
|
+
lazySafe: true,
|
|
19116
|
+
facts: { declaredNames: /* @__PURE__ */ new Set(), freeNames: /* @__PURE__ */ new Set() }
|
|
19117
|
+
};
|
|
19118
|
+
NO2 = (reason2) => ({ lazySafe: false, reason: reason2 });
|
|
19119
|
+
}
|
|
19120
|
+
});
|
|
19121
|
+
|
|
18633
19122
|
// ../jsx/src/ir-to-client-js/control-flow/plan/lazy-row-eligibility.ts
|
|
18634
19123
|
function lazyRowEligibility(args2) {
|
|
18635
19124
|
const { shape, bindings, arraySourceIdentifiers, scope } = args2;
|
|
18636
|
-
if (scope.profile) return
|
|
19125
|
+
if (scope.profile) return NO3("profile mode keeps the granular eager emission");
|
|
18637
19126
|
if (shape.callSite !== "plain" && shape.callSite !== "branch-plain") {
|
|
18638
|
-
return
|
|
18639
|
-
}
|
|
18640
|
-
if (shape.flatMapLeafItem) return
|
|
18641
|
-
if (shape.anchored) return
|
|
18642
|
-
if (shape.bodyIsMultiRoot) return
|
|
18643
|
-
if (!shape.hasExplicitKey) return
|
|
18644
|
-
if (shape.
|
|
18645
|
-
if (shape.childRefCount > 0) return
|
|
18646
|
-
if (shape.hasChildComponent) return
|
|
18647
|
-
if (shape.nestedComponentCount > 0) return
|
|
18648
|
-
if (shape.innerLoopCount > 0) return
|
|
18649
|
-
if (shape.
|
|
18650
|
-
if (shape.preambleRegionCount > 0) return
|
|
18651
|
-
if (shape.hasParamUnwrap) return
|
|
19127
|
+
return NO3(`call site '${shape.callSite}' is not a plain loop row`);
|
|
19128
|
+
}
|
|
19129
|
+
if (shape.flatMapLeafItem) return NO3("flatMap descriptor loop (build-or-patch renderItem)");
|
|
19130
|
+
if (shape.anchored) return NO3("anchored whole-item-conditional loop");
|
|
19131
|
+
if (shape.bodyIsMultiRoot) return NO3("multi-root (Fragment) row");
|
|
19132
|
+
if (!shape.hasExplicitKey) return NO3("index-keyed loop (no explicit key)");
|
|
19133
|
+
if (shape.conditionalRefusal) return NO3(shape.conditionalRefusal);
|
|
19134
|
+
if (shape.childRefCount > 0) return NO3("row has imperative child refs");
|
|
19135
|
+
if (shape.hasChildComponent) return NO3("row body is a child component");
|
|
19136
|
+
if (shape.nestedComponentCount > 0) return NO3("row contains nested child components");
|
|
19137
|
+
if (shape.innerLoopCount > 0) return NO3("row contains an inner loop");
|
|
19138
|
+
if (shape.mapPreambleRefusal) return NO3(shape.mapPreambleRefusal);
|
|
19139
|
+
if (shape.preambleRegionCount > 0) return NO3("row has preamble-patched regions");
|
|
19140
|
+
if (shape.hasParamUnwrap) return NO3("destructured loop param without param bindings");
|
|
18652
19141
|
for (const b of bindings) {
|
|
18653
19142
|
if (b.referencesIndex) {
|
|
18654
|
-
return
|
|
19143
|
+
return NO3(`binding on slot ${b.slotId} references the loop index parameter`);
|
|
18655
19144
|
}
|
|
18656
19145
|
if (b.opaqueOuterNames.includes(UNKNOWN_IDENTIFIERS)) {
|
|
18657
|
-
return
|
|
19146
|
+
return NO3(`binding on slot ${b.slotId} has no analyzable identifier set`);
|
|
18658
19147
|
}
|
|
18659
19148
|
}
|
|
18660
|
-
if (!arraySourceIdentifiers) return
|
|
19149
|
+
if (!arraySourceIdentifiers) return NO3("loop source free identifiers unavailable");
|
|
18661
19150
|
const sourceGate = checkSourceConsistency(arraySourceIdentifiers, scope);
|
|
18662
|
-
if (sourceGate) return
|
|
19151
|
+
if (sourceGate) return NO3(`loop source is not provably hydration-consistent: ${sourceGate}`);
|
|
18663
19152
|
return { eligible: true };
|
|
18664
19153
|
}
|
|
18665
19154
|
function checkSourceConsistency(names, scope) {
|
|
@@ -18701,6 +19190,7 @@ function checkSourceConsistency(names, scope) {
|
|
|
18701
19190
|
}
|
|
18702
19191
|
function classifyLazyBinding(args2) {
|
|
18703
19192
|
const { kind: kind2, slotId, free, rowLocalNames, indexParam, scope } = args2;
|
|
19193
|
+
const preamble = args2.preamble;
|
|
18704
19194
|
if (free === null) {
|
|
18705
19195
|
return {
|
|
18706
19196
|
kind: kind2,
|
|
@@ -18713,30 +19203,43 @@ function classifyLazyBinding(args2) {
|
|
|
18713
19203
|
// `UNKNOWN_IDENTIFIERS` must keep refusing the loop: with no
|
|
18714
19204
|
// identifier set we cannot rule out an index read either, and
|
|
18715
19205
|
// `applyItem` / `applyOuter` have no index parameter to give it.
|
|
18716
|
-
referencesIndex: false
|
|
19206
|
+
referencesIndex: false,
|
|
19207
|
+
// An assumption like `referencesIndex` above, and the conservative
|
|
19208
|
+
// one: with no identifier set we cannot rule out a preamble read
|
|
19209
|
+
// either. The loop is refused for `UNKNOWN_IDENTIFIERS` regardless.
|
|
19210
|
+
readsPreamble: preamble != null && preamble.declaredNames.size > 0
|
|
18717
19211
|
};
|
|
18718
19212
|
}
|
|
18719
19213
|
let readsItem = false;
|
|
18720
19214
|
let referencesIndex = false;
|
|
19215
|
+
let readsPreamble = false;
|
|
18721
19216
|
const reactiveOuterNames = [];
|
|
18722
19217
|
const opaqueOuterNames = [];
|
|
18723
|
-
|
|
19218
|
+
const classifyName = (name2) => {
|
|
18724
19219
|
if (rowLocalNames.has(name2)) {
|
|
18725
19220
|
readsItem = true;
|
|
18726
|
-
|
|
19221
|
+
return;
|
|
18727
19222
|
}
|
|
18728
19223
|
if (name2 === indexParam) {
|
|
18729
19224
|
referencesIndex = true;
|
|
18730
|
-
|
|
19225
|
+
return;
|
|
18731
19226
|
}
|
|
18732
|
-
if (INERT_BINDING_GLOBALS.has(name2))
|
|
19227
|
+
if (INERT_BINDING_GLOBALS.has(name2)) return;
|
|
18733
19228
|
if (scope.signals.has(name2) || scope.memos.has(name2)) {
|
|
18734
19229
|
if (!reactiveOuterNames.includes(name2)) reactiveOuterNames.push(name2);
|
|
18735
|
-
|
|
19230
|
+
return;
|
|
18736
19231
|
}
|
|
18737
19232
|
const constFree = scope.constants.get(name2);
|
|
18738
|
-
if (constFree !== void 0 && constFree !== null && constFree.size === 0)
|
|
19233
|
+
if (constFree !== void 0 && constFree !== null && constFree.size === 0) return;
|
|
18739
19234
|
opaqueOuterNames.push(name2);
|
|
19235
|
+
};
|
|
19236
|
+
for (const name2 of free) {
|
|
19237
|
+
if (preamble?.declaredNames.has(name2)) {
|
|
19238
|
+
readsPreamble = true;
|
|
19239
|
+
for (const dep of preamble.freeNames) classifyName(dep);
|
|
19240
|
+
continue;
|
|
19241
|
+
}
|
|
19242
|
+
classifyName(name2);
|
|
18740
19243
|
}
|
|
18741
19244
|
return {
|
|
18742
19245
|
kind: kind2,
|
|
@@ -18745,10 +19248,11 @@ function classifyLazyBinding(args2) {
|
|
|
18745
19248
|
readsOuter: reactiveOuterNames.length > 0 || opaqueOuterNames.length > 0,
|
|
18746
19249
|
reactiveOuterNames,
|
|
18747
19250
|
opaqueOuterNames,
|
|
18748
|
-
referencesIndex
|
|
19251
|
+
referencesIndex,
|
|
19252
|
+
readsPreamble
|
|
18749
19253
|
};
|
|
18750
19254
|
}
|
|
18751
|
-
var PURE_SOURCE_GLOBALS, INERT_BINDING_GLOBALS, UNKNOWN_IDENTIFIERS,
|
|
19255
|
+
var PURE_SOURCE_GLOBALS, INERT_BINDING_GLOBALS, UNKNOWN_IDENTIFIERS, NO3;
|
|
18752
19256
|
var init_lazy_row_eligibility = __esm({
|
|
18753
19257
|
"../jsx/src/ir-to-client-js/control-flow/plan/lazy-row-eligibility.ts"() {
|
|
18754
19258
|
"use strict";
|
|
@@ -18794,7 +19298,7 @@ var init_lazy_row_eligibility = __esm({
|
|
|
18794
19298
|
"decodeURI"
|
|
18795
19299
|
]);
|
|
18796
19300
|
UNKNOWN_IDENTIFIERS = "<unknown>";
|
|
18797
|
-
|
|
19301
|
+
NO3 = (reason2) => ({ eligible: false, reason: reason2 });
|
|
18798
19302
|
}
|
|
18799
19303
|
});
|
|
18800
19304
|
|
|
@@ -18810,6 +19314,23 @@ function decideLazyRow(args2) {
|
|
|
18810
19314
|
const wrap = (expr) => wrapLoopParamAsAccessor(expr, loop.param, loop.paramBindings);
|
|
18811
19315
|
const rowLocalNames = /* @__PURE__ */ new Set([loop.param]);
|
|
18812
19316
|
for (const b of loop.paramBindings ?? []) rowLocalNames.add(b.name);
|
|
19317
|
+
const primableNames = /* @__PURE__ */ new Set([...scope.signals.keys(), ...scope.memos]);
|
|
19318
|
+
const preambleAnalysis = analyzeLazyPreamble(loop.preamble, args2.indexParam, primableNames);
|
|
19319
|
+
const rawConditionals = loop.bindings.conditionals ?? [];
|
|
19320
|
+
const condFacts = [];
|
|
19321
|
+
let conditionalRefusal = null;
|
|
19322
|
+
for (const cond of rawConditionals) {
|
|
19323
|
+
const verdict = analyzeLazyConditional(cond, args2.indexParam, {
|
|
19324
|
+
whenTrueHtml: addCondAttrToTemplate(wrap(cond.whenTrueHtml), cond.slotId),
|
|
19325
|
+
whenFalseHtml: addCondAttrToTemplate(wrap(cond.whenFalseHtml), cond.slotId)
|
|
19326
|
+
});
|
|
19327
|
+
if (!verdict.lazySafe) {
|
|
19328
|
+
conditionalRefusal = verdict.reason;
|
|
19329
|
+
break;
|
|
19330
|
+
}
|
|
19331
|
+
condFacts.push(verdict.facts);
|
|
19332
|
+
}
|
|
19333
|
+
const preambleFacts = preambleAnalysis.lazySafe ? preambleAnalysis.facts : void 0;
|
|
18813
19334
|
const classified = [];
|
|
18814
19335
|
const attrClass = /* @__PURE__ */ new Map();
|
|
18815
19336
|
loop.bindings.reactiveAttrs.forEach((attr, i) => {
|
|
@@ -18819,7 +19340,8 @@ function decideLazyRow(args2) {
|
|
|
18819
19340
|
free: attrFreeIdentifiers2(attr.expression),
|
|
18820
19341
|
rowLocalNames,
|
|
18821
19342
|
indexParam: args2.indexParam,
|
|
18822
|
-
scope
|
|
19343
|
+
scope,
|
|
19344
|
+
preamble: preambleFacts
|
|
18823
19345
|
});
|
|
18824
19346
|
attrClass.set(i, c);
|
|
18825
19347
|
classified.push(c);
|
|
@@ -18832,23 +19354,38 @@ function decideLazyRow(args2) {
|
|
|
18832
19354
|
free: text.freeIdentifiers ?? null,
|
|
18833
19355
|
rowLocalNames,
|
|
18834
19356
|
indexParam: args2.indexParam,
|
|
18835
|
-
scope
|
|
19357
|
+
scope,
|
|
19358
|
+
preamble: preambleFacts
|
|
18836
19359
|
});
|
|
18837
19360
|
textClass.set(i, c);
|
|
18838
19361
|
classified.push(c);
|
|
18839
19362
|
});
|
|
19363
|
+
const condClass = /* @__PURE__ */ new Map();
|
|
19364
|
+
condFacts.forEach((c, i) => {
|
|
19365
|
+
const k = classifyLazyBinding({
|
|
19366
|
+
kind: "attr",
|
|
19367
|
+
slotId: c.slotId,
|
|
19368
|
+
free: rawConditionals[i].conditionFreeIdentifiers ?? null,
|
|
19369
|
+
rowLocalNames,
|
|
19370
|
+
indexParam: args2.indexParam,
|
|
19371
|
+
scope,
|
|
19372
|
+
preamble: preambleFacts
|
|
19373
|
+
});
|
|
19374
|
+
condClass.set(i, k);
|
|
19375
|
+
classified.push(k);
|
|
19376
|
+
});
|
|
18840
19377
|
const shape = {
|
|
18841
19378
|
callSite: args2.callSite,
|
|
18842
19379
|
flatMapLeafItem: args2.flatMapLeafItem,
|
|
18843
19380
|
anchored: args2.anchored,
|
|
18844
19381
|
bodyIsMultiRoot: loop.bodyIsMultiRoot ?? false,
|
|
18845
19382
|
hasExplicitKey: loop.key != null,
|
|
18846
|
-
|
|
19383
|
+
conditionalRefusal,
|
|
18847
19384
|
childRefCount: loop.bindings.refs?.length ?? 0,
|
|
18848
19385
|
nestedComponentCount: loop.nestedComponents?.length ?? 0,
|
|
18849
19386
|
innerLoopCount: loop.innerLoops?.length ?? 0,
|
|
18850
19387
|
hasChildComponent: "childComponent" in loop && loop.childComponent != null,
|
|
18851
|
-
|
|
19388
|
+
mapPreambleRefusal: preambleAnalysis.lazySafe ? null : preambleAnalysis.reason,
|
|
18852
19389
|
preambleRegionCount: args2.preambleRegionCount,
|
|
18853
19390
|
hasParamUnwrap: args2.paramUnwrap.length > 0
|
|
18854
19391
|
};
|
|
@@ -18874,7 +19411,8 @@ function decideLazyRow(args2) {
|
|
|
18874
19411
|
refIndex: attrSlotIds.indexOf(attr.childSlotId),
|
|
18875
19412
|
ordinal: ordinal++,
|
|
18876
19413
|
readsItem: c.readsItem,
|
|
18877
|
-
readsOuter: c.readsOuter
|
|
19414
|
+
readsOuter: c.readsOuter,
|
|
19415
|
+
readsPreamble: c.readsPreamble
|
|
18878
19416
|
};
|
|
18879
19417
|
});
|
|
18880
19418
|
const texts = loop.bindings.reactiveTexts.map((text, i) => {
|
|
@@ -18888,7 +19426,8 @@ function decideLazyRow(args2) {
|
|
|
18888
19426
|
// silently never being written. Anything the classifier did place in
|
|
18889
19427
|
// a list keeps exactly the classifier's answer.
|
|
18890
19428
|
readsItem: c.readsItem || !c.readsOuter,
|
|
18891
|
-
readsOuter: c.readsOuter
|
|
19429
|
+
readsOuter: c.readsOuter,
|
|
19430
|
+
readsPreamble: c.readsPreamble
|
|
18892
19431
|
};
|
|
18893
19432
|
});
|
|
18894
19433
|
const outerPrimeGetters = [];
|
|
@@ -18897,6 +19436,25 @@ function decideLazyRow(args2) {
|
|
|
18897
19436
|
if (!outerPrimeGetters.includes(name2)) outerPrimeGetters.push(name2);
|
|
18898
19437
|
}
|
|
18899
19438
|
}
|
|
19439
|
+
const condRefBase = attrSlotIds.length + (texts.length > 0 ? 1 : 0);
|
|
19440
|
+
const conditionals = condFacts.map((c, i) => {
|
|
19441
|
+
const klass = condClass.get(i);
|
|
19442
|
+
return {
|
|
19443
|
+
slotId: c.slotId,
|
|
19444
|
+
wrappedCondition: wrap(c.condition),
|
|
19445
|
+
whenTrueHtml: c.whenTrueHtml,
|
|
19446
|
+
whenFalseHtml: c.whenFalseHtml,
|
|
19447
|
+
refIndex: condRefBase + i,
|
|
19448
|
+
ordinal: ordinal++,
|
|
19449
|
+
// A condition reading neither the item nor a reactive outer name still
|
|
19450
|
+
// has to be applied somewhere; `applyItem` is the harmless choice (the
|
|
19451
|
+
// dedup makes a repeat a no-op), matching how a text that classified as
|
|
19452
|
+
// neither is handled above.
|
|
19453
|
+
readsItem: klass.readsItem || !klass.readsOuter,
|
|
19454
|
+
readsOuter: klass.readsOuter,
|
|
19455
|
+
readsPreamble: klass.readsPreamble
|
|
19456
|
+
};
|
|
19457
|
+
});
|
|
18900
19458
|
return {
|
|
18901
19459
|
plan: {
|
|
18902
19460
|
attrSlotIds,
|
|
@@ -18906,7 +19464,14 @@ function decideLazyRow(args2) {
|
|
|
18906
19464
|
textNeedsRead: texts.some((t) => t.readsOuter),
|
|
18907
19465
|
lastCount: ordinal,
|
|
18908
19466
|
outerPrimeGetters,
|
|
18909
|
-
|
|
19467
|
+
preambleStatements: args2.mapPreambleWrapped,
|
|
19468
|
+
// Which apply bodies must re-run the preamble — computed from the FINAL
|
|
19469
|
+
// binding lists, not the raw classification, so a text/conditional that
|
|
19470
|
+
// classified as neither item- nor outer-driven (and was therefore forced
|
|
19471
|
+
// into `applyItem` above) is counted in the body it actually lands in.
|
|
19472
|
+
itemNeedsPreamble: [...attrs, ...texts, ...conditionals].some((b) => b.readsItem && b.readsPreamble),
|
|
19473
|
+
outerNeedsPreamble: [...attrs, ...texts, ...conditionals].some((b) => b.readsOuter && b.readsPreamble),
|
|
19474
|
+
conditionals
|
|
18910
19475
|
},
|
|
18911
19476
|
decision
|
|
18912
19477
|
};
|
|
@@ -18957,7 +19522,10 @@ var init_build_lazy_row = __esm({
|
|
|
18957
19522
|
init_expression_parser();
|
|
18958
19523
|
init_types();
|
|
18959
19524
|
init_csr_substitute();
|
|
19525
|
+
init_html_template();
|
|
18960
19526
|
init_utils();
|
|
19527
|
+
init_lazy_conditional();
|
|
19528
|
+
init_lazy_preamble();
|
|
18961
19529
|
init_lazy_row_eligibility();
|
|
18962
19530
|
}
|
|
18963
19531
|
});
|
|
@@ -18983,7 +19551,7 @@ function buildBranchLoopPlan(loop, profileComponentName, lazyScope) {
|
|
|
18983
19551
|
const indexParam = loop.index || "__idx";
|
|
18984
19552
|
const mapPreambleWrapped = loop.preamble ? renderPreamble(loop.preamble, {
|
|
18985
19553
|
transformJs: (t) => wrapLoopParamAsAccessor(t, loop.param, loop.paramBindings),
|
|
18986
|
-
renderLeaf: (ir) => irToHtmlTemplate(ir, void 0, 1, [{ param: loop.param, bindings: loop.paramBindings }], void 0
|
|
19554
|
+
renderLeaf: (ir) => irToHtmlTemplate(ir, void 0, 1, [{ param: loop.param, bindings: loop.paramBindings }], void 0)
|
|
18987
19555
|
}) : "";
|
|
18988
19556
|
const preambleRegions = buildPreambleRegionPlans(loop.preambleRegions, loop.param, loop.paramBindings);
|
|
18989
19557
|
const plan = {
|
|
@@ -19133,7 +19701,7 @@ var init_claim_plan = __esm({
|
|
|
19133
19701
|
});
|
|
19134
19702
|
|
|
19135
19703
|
// ../jsx/src/ir-to-client-js/emit-reactive.ts
|
|
19136
|
-
import
|
|
19704
|
+
import ts16 from "typescript";
|
|
19137
19705
|
function bindingIdArg(ctx2, slotId) {
|
|
19138
19706
|
if (!ctx2.profile || !slotId) return "";
|
|
19139
19707
|
return `, ${JSON.stringify(`${ctx2.componentName}#binding:${slotId}`)}`;
|
|
@@ -19213,19 +19781,19 @@ function lowerToLocaleCallsInReactiveExpr(expr, matcher) {
|
|
|
19213
19781
|
if (!matcher) return expr;
|
|
19214
19782
|
let sourceFile;
|
|
19215
19783
|
try {
|
|
19216
|
-
sourceFile =
|
|
19784
|
+
sourceFile = ts16.createSourceFile("__reactive_expr__.ts", `(${expr});`, ts16.ScriptTarget.Latest, true, ts16.ScriptKind.TS);
|
|
19217
19785
|
} catch {
|
|
19218
19786
|
return expr;
|
|
19219
19787
|
}
|
|
19220
19788
|
const stmt = sourceFile.statements[0];
|
|
19221
|
-
if (!stmt || !
|
|
19222
|
-
const root2 =
|
|
19789
|
+
if (!stmt || !ts16.isExpressionStatement(stmt)) return expr;
|
|
19790
|
+
const root2 = ts16.isParenthesizedExpression(stmt.expression) ? stmt.expression.expression : stmt.expression;
|
|
19223
19791
|
const candidates = [];
|
|
19224
19792
|
const visit3 = (n) => {
|
|
19225
|
-
if (
|
|
19793
|
+
if (ts16.isCallExpression(n) && n.arguments.length === 2 && ts16.isPropertyAccessExpression(n.expression) && !n.expression.questionDotToken && n.expression.name.text === "toLocaleDateString") {
|
|
19226
19794
|
candidates.push(n);
|
|
19227
19795
|
}
|
|
19228
|
-
|
|
19796
|
+
ts16.forEachChild(n, visit3);
|
|
19229
19797
|
};
|
|
19230
19798
|
visit3(root2);
|
|
19231
19799
|
if (candidates.length === 0) return expr;
|
|
@@ -19262,19 +19830,19 @@ function lowerDateCallsInReactiveExpr(expr, matcher) {
|
|
|
19262
19830
|
if (!matcher) return expr;
|
|
19263
19831
|
let sourceFile;
|
|
19264
19832
|
try {
|
|
19265
|
-
sourceFile =
|
|
19833
|
+
sourceFile = ts16.createSourceFile("__reactive_expr__.ts", `(${expr});`, ts16.ScriptTarget.Latest, true, ts16.ScriptKind.TS);
|
|
19266
19834
|
} catch {
|
|
19267
19835
|
return expr;
|
|
19268
19836
|
}
|
|
19269
19837
|
const stmt = sourceFile.statements[0];
|
|
19270
|
-
if (!stmt || !
|
|
19271
|
-
const root2 =
|
|
19838
|
+
if (!stmt || !ts16.isExpressionStatement(stmt)) return expr;
|
|
19839
|
+
const root2 = ts16.isParenthesizedExpression(stmt.expression) ? stmt.expression.expression : stmt.expression;
|
|
19272
19840
|
const candidates = [];
|
|
19273
19841
|
const visit3 = (n) => {
|
|
19274
|
-
if (
|
|
19842
|
+
if (ts16.isCallExpression(n) && n.arguments.length === 0 && ts16.isPropertyAccessExpression(n.expression) && !n.expression.questionDotToken && DATE_METHODS.has(n.expression.name.text)) {
|
|
19275
19843
|
candidates.push(n);
|
|
19276
19844
|
}
|
|
19277
|
-
|
|
19845
|
+
ts16.forEachChild(n, visit3);
|
|
19278
19846
|
};
|
|
19279
19847
|
visit3(root2);
|
|
19280
19848
|
if (candidates.length === 0) return expr;
|
|
@@ -19614,7 +20182,7 @@ function stringifyReactiveEffects(lines, plan, opts) {
|
|
|
19614
20182
|
const outerTexts = plan?.outerTexts ?? [];
|
|
19615
20183
|
const conditionals = plan?.conditionals ?? [];
|
|
19616
20184
|
if (pc) {
|
|
19617
|
-
emitAttrSlotsGranular(lines, indent, elVar, lookup, attrSlots, elementIndexBySlot, bindingBfId);
|
|
20185
|
+
emitAttrSlotsGranular(lines, indent, elVar, lookup, attrSlots, elementIndexBySlot, bindingBfId, mapPreambleWrapped);
|
|
19618
20186
|
emitOuterTexts(lines, indent, elVar, outerTexts, bindingBfId, textClaimPathExprs);
|
|
19619
20187
|
emitPreambleRegionsEffect(lines, indent, elVar, preambleRegions, mapPreambleWrapped);
|
|
19620
20188
|
} else {
|
|
@@ -19635,7 +20203,7 @@ function stringifyReactiveEffects(lines, plan, opts) {
|
|
|
19635
20203
|
emitOuterConditional(lines, indent, elVar, cond, pc);
|
|
19636
20204
|
}
|
|
19637
20205
|
}
|
|
19638
|
-
function emitAttrSlotsGranular(lines, indent, elVar, lookup, attrSlots, elementIndexBySlot, bindingBfId) {
|
|
20206
|
+
function emitAttrSlotsGranular(lines, indent, elVar, lookup, attrSlots, elementIndexBySlot, bindingBfId, mapPreambleWrapped) {
|
|
19639
20207
|
for (const slot of attrSlots) {
|
|
19640
20208
|
const varName = `__ra_${varSlotId(slot.slotId)}`;
|
|
19641
20209
|
const lookupExpr = attrLookupExpr(slot.slotId, varName, elVar, lookup, elementIndexBySlot);
|
|
@@ -19643,6 +20211,9 @@ function emitAttrSlotsGranular(lines, indent, elVar, lookup, attrSlots, elementI
|
|
|
19643
20211
|
lines.push(`${indent}if (${varName}) {`);
|
|
19644
20212
|
for (const attr of slot.attrs) {
|
|
19645
20213
|
lines.push(`${indent} createEffect(() => {`);
|
|
20214
|
+
if (attr.readsPreamble && mapPreambleWrapped) {
|
|
20215
|
+
lines.push(`${indent} ${mapPreambleWrapped}`);
|
|
20216
|
+
}
|
|
19646
20217
|
for (const stmt of emitAttrUpdate(varName, attr.attrName, attr.wrappedExpression, attr.meta)) {
|
|
19647
20218
|
lines.push(`${indent} ${stmt}`);
|
|
19648
20219
|
}
|
|
@@ -19663,6 +20234,9 @@ function emitPreambleRegionsEffect(lines, indent, elVar, preambleRegions, mapPre
|
|
|
19663
20234
|
}
|
|
19664
20235
|
lines.push(`${indent}})`);
|
|
19665
20236
|
}
|
|
20237
|
+
function attrsReadPreamble(attrSlots) {
|
|
20238
|
+
return attrSlots.some((slot) => slot.attrs.some((a) => a.readsPreamble));
|
|
20239
|
+
}
|
|
19666
20240
|
function attrLookupExpr(slotId, varName, elVar, lookup, elementIndexBySlot) {
|
|
19667
20241
|
const pIdx = elementIndexBySlot?.get(slotId);
|
|
19668
20242
|
return pIdx !== void 0 ? `__p ? __p[${pIdx}] : ${lookup}(${elVar}, '[bf="${slotId}"]')` : `${lookup}(${elVar}, '[bf="${slotId}"]')`;
|
|
@@ -19687,6 +20261,9 @@ function emitConsolidatedRowEffect(lines, indent, elVar, lookup, attrSlots, oute
|
|
|
19687
20261
|
return;
|
|
19688
20262
|
}
|
|
19689
20263
|
lines.push(`${indent}createEffect(() => {`);
|
|
20264
|
+
if (mapPreambleWrapped && (preambleRegions.length > 0 || attrsReadPreamble(attrSlots))) {
|
|
20265
|
+
lines.push(`${indent} ${mapPreambleWrapped}`);
|
|
20266
|
+
}
|
|
19690
20267
|
for (const slot of attrSlots) {
|
|
19691
20268
|
const varName = `__ra_${varSlotId(slot.slotId)}`;
|
|
19692
20269
|
lines.push(`${indent} if (${varName}) {`);
|
|
@@ -19699,9 +20276,6 @@ function emitConsolidatedRowEffect(lines, indent, elVar, lookup, attrSlots, oute
|
|
|
19699
20276
|
}
|
|
19700
20277
|
lines.push(`${indent} }`);
|
|
19701
20278
|
}
|
|
19702
|
-
if (preambleRegions.length > 0 && mapPreambleWrapped) {
|
|
19703
|
-
lines.push(`${indent} ${mapPreambleWrapped}`);
|
|
19704
|
-
}
|
|
19705
20279
|
for (const text of outerTexts) {
|
|
19706
20280
|
lines.push(`${indent} ${writer}('${text.slotId}', String(${text.wrappedExpression}))`);
|
|
19707
20281
|
}
|
|
@@ -19847,9 +20421,8 @@ function stringifyLazyRowLoop(lines, o) {
|
|
|
19847
20421
|
const { indent, lazyRow, paramHead } = o;
|
|
19848
20422
|
const mid = o.markerId.replace(/[^A-Za-z0-9_$]/g, "_");
|
|
19849
20423
|
const tplVar = `__tpl_${mid}`;
|
|
19850
|
-
const claimVar = `__lzc_${mid}`;
|
|
19851
20424
|
const hasRefs = lazyRow.attrSlotIds.length > 0 || lazyRow.texts.length > 0;
|
|
19852
|
-
const hasBindings = lazyRow.attrs.length > 0 || lazyRow.texts.length > 0;
|
|
20425
|
+
const hasBindings = lazyRow.attrs.length > 0 || lazyRow.texts.length > 0 || lazyRow.conditionals.length > 0;
|
|
19853
20426
|
const rwDoor = lazyRow.textNeedsRead;
|
|
19854
20427
|
const paths = o.skeletonPaths;
|
|
19855
20428
|
const useHoisted = Boolean(o.skeletonTemplate);
|
|
@@ -19878,12 +20451,10 @@ function stringifyLazyRowLoop(lines, o) {
|
|
|
19878
20451
|
freshPlanVar = adoptedPlanVar;
|
|
19879
20452
|
}
|
|
19880
20453
|
}
|
|
19881
|
-
|
|
19882
|
-
const
|
|
19883
|
-
lines
|
|
19884
|
-
|
|
19885
|
-
lines.push(`${indent} return [${parts.join(", ")}]`);
|
|
19886
|
-
lines.push(`${indent}}`);
|
|
20454
|
+
for (const c of lazyRow.conditionals) {
|
|
20455
|
+
const v = condVars(mid, c.slotId);
|
|
20456
|
+
emitHoistedTemplateDecl(lines, indent, v.trueTpl, c.whenTrueHtml);
|
|
20457
|
+
emitHoistedTemplateDecl(lines, indent, v.falseTpl, c.whenFalseHtml);
|
|
19887
20458
|
}
|
|
19888
20459
|
const call = `mapArrayLazy(() => ${o.arrayExpr}, ${o.containerVar}, ${o.keyFn}, {`;
|
|
19889
20460
|
lines.push(`${indent}${o.guardContainer ? `if (${o.containerVar}) ` : ""}${call}`);
|
|
@@ -19891,6 +20462,7 @@ function stringifyLazyRowLoop(lines, o) {
|
|
|
19891
20462
|
const b2 = `${indent} `;
|
|
19892
20463
|
lines.push(`${b1}createRow: (__e, ${o.indexParam}) => {`);
|
|
19893
20464
|
lines.push(`${b2}const ${paramHead} = () => __e.item`);
|
|
20465
|
+
if (lazyRow.preambleStatements) lines.push(`${b2}${lazyRow.preambleStatements}`);
|
|
19894
20466
|
const cloneExpr = useHoisted ? hoistedCloneExpr(tplVar, o.skeletonTemplate) : `(() => { ${emitTemplateCloneInline(o.template)} })()`;
|
|
19895
20467
|
lines.push(`${b2}const __el = ${cloneExpr}`);
|
|
19896
20468
|
if (hasRefs) {
|
|
@@ -19902,55 +20474,70 @@ function stringifyLazyRowLoop(lines, o) {
|
|
|
19902
20474
|
const createDoor = `__r[${lazyRow.writerIndex}]`;
|
|
19903
20475
|
for (const t of lazyRow.texts) emitTextBinding(lines, b2, t, createDoor, "create", rwDoor);
|
|
19904
20476
|
}
|
|
20477
|
+
for (const c of lazyRow.conditionals) emitConditional(lines, b2, mid, c, "create");
|
|
19905
20478
|
lines.push(`${b2}return __el`);
|
|
19906
20479
|
lines.push(`${b1}},`);
|
|
19907
20480
|
const itemAttrs = lazyRow.attrs.filter((a) => a.readsItem);
|
|
19908
20481
|
const itemTexts = lazyRow.texts.filter((t) => t.readsItem);
|
|
19909
|
-
|
|
20482
|
+
const itemConds = lazyRow.conditionals.filter((c) => c.readsItem);
|
|
20483
|
+
if (itemAttrs.length === 0 && itemTexts.length === 0 && itemConds.length === 0) {
|
|
19910
20484
|
lines.push(`${b1}applyItem: () => {},`);
|
|
19911
20485
|
} else {
|
|
19912
20486
|
lines.push(`${b1}applyItem: (__e) => {`);
|
|
19913
20487
|
lines.push(`${b2}const ${paramHead} = () => __e.item`);
|
|
19914
|
-
lines.push(`${b2}const __r = __e.refs ?? (__e.refs =
|
|
20488
|
+
lines.push(`${b2}const __r = __e.refs ?? (__e.refs = [])`);
|
|
19915
20489
|
lines.push(`${b2}const __l = __e.last ?? (__e.last = [])`);
|
|
20490
|
+
if (lazyRow.itemNeedsPreamble && lazyRow.preambleStatements) {
|
|
20491
|
+
lines.push(`${b2}${lazyRow.preambleStatements}`);
|
|
20492
|
+
}
|
|
19916
20493
|
for (const a of itemAttrs) emitAttrBinding(lines, b2, a, "item");
|
|
19917
20494
|
if (itemTexts.length > 0) {
|
|
19918
20495
|
lines.push(`${b2}const __d = ${doorAccess(lazyRow, lazyRow.writerIndex, adoptedPlanVar)}`);
|
|
19919
20496
|
for (const t of itemTexts) emitTextBinding(lines, b2, t, "__d", "item", rwDoor);
|
|
19920
20497
|
}
|
|
20498
|
+
for (const c of itemConds) emitConditional(lines, b2, mid, c, "item");
|
|
19921
20499
|
lines.push(`${b1}},`);
|
|
19922
20500
|
}
|
|
19923
20501
|
const outerAttrs = lazyRow.attrs.filter((a) => a.readsOuter);
|
|
19924
20502
|
const outerTexts = lazyRow.texts.filter((t) => t.readsOuter);
|
|
19925
|
-
|
|
20503
|
+
const outerConds = lazyRow.conditionals.filter((c) => c.readsOuter);
|
|
20504
|
+
if (outerAttrs.length > 0 || outerTexts.length > 0 || outerConds.length > 0) {
|
|
19926
20505
|
const b3 = `${indent} `;
|
|
19927
20506
|
lines.push(`${b1}applyOuter: (__es, __seed) => {`);
|
|
19928
20507
|
for (const g of lazyRow.outerPrimeGetters) lines.push(`${b2}${g}()`);
|
|
19929
20508
|
lines.push(`${b2}for (const __e of __es) {`);
|
|
19930
20509
|
lines.push(`${b3}const ${paramHead} = () => __e.item`);
|
|
19931
|
-
lines.push(`${b3}const __r = __e.refs ?? (__e.refs =
|
|
20510
|
+
lines.push(`${b3}const __r = __e.refs ?? (__e.refs = [])`);
|
|
19932
20511
|
lines.push(`${b3}const __l = __e.last ?? (__e.last = [])`);
|
|
20512
|
+
if (lazyRow.outerNeedsPreamble && lazyRow.preambleStatements) {
|
|
20513
|
+
lines.push(`${b3}${lazyRow.preambleStatements}`);
|
|
20514
|
+
}
|
|
19933
20515
|
for (const a of outerAttrs) emitAttrBinding(lines, b3, a, "outer");
|
|
19934
20516
|
if (outerTexts.length > 0) {
|
|
19935
20517
|
lines.push(`${b3}const __d = ${doorAccess(lazyRow, lazyRow.writerIndex, adoptedPlanVar)}`);
|
|
19936
20518
|
for (const t of outerTexts) emitTextBinding(lines, b3, t, "__d", "outer", rwDoor);
|
|
19937
20519
|
}
|
|
20520
|
+
for (const c of outerConds) emitConditional(lines, b3, mid, c, "outer");
|
|
19938
20521
|
lines.push(`${b2}}`);
|
|
19939
20522
|
lines.push(`${b1}},`);
|
|
19940
20523
|
}
|
|
19941
20524
|
lines.push(`${indent}}, '${o.markerId}')`);
|
|
19942
20525
|
}
|
|
19943
|
-
function refParts(lazyRow, elVar, skeletonPaths, planVar
|
|
20526
|
+
function refParts(lazyRow, elVar, skeletonPaths, planVar) {
|
|
19944
20527
|
const parts = [];
|
|
19945
20528
|
for (const slotId of lazyRow.attrSlotIds) {
|
|
19946
20529
|
const path25 = skeletonPaths?.elementPaths.get(slotId);
|
|
19947
20530
|
parts.push(path25 ? pathExpr(elVar, path25) : `qsa(${elVar}, '[bf="${slotId}"]')`);
|
|
19948
20531
|
}
|
|
19949
20532
|
if (lazyRow.texts.length > 0) {
|
|
19950
|
-
parts.push(
|
|
20533
|
+
parts.push(`${doorCtor(lazyRow)}(${elVar}, ${planVar})`);
|
|
19951
20534
|
}
|
|
19952
20535
|
return parts;
|
|
19953
20536
|
}
|
|
20537
|
+
function elementAccess(a) {
|
|
20538
|
+
const slot = `__r[${a.refIndex}]`;
|
|
20539
|
+
return `${a.refIndex} in __r ? ${slot} : (${slot} = qsa(__e.primaryEl, '[bf="${a.slotId}"]'))`;
|
|
20540
|
+
}
|
|
19954
20541
|
function doorCtor(lazyRow) {
|
|
19955
20542
|
return lazyRow.textNeedsRead ? "lazyClaimSlots" : "lazySlots";
|
|
19956
20543
|
}
|
|
@@ -19958,11 +20545,36 @@ function doorAccess(lazyRow, writerIndex, adoptedPlanVar) {
|
|
|
19958
20545
|
const slot = `__r[${writerIndex}]`;
|
|
19959
20546
|
return `${slot} ?? (${slot} = ${doorCtor(lazyRow)}(__e.primaryEl, ${adoptedPlanVar}))`;
|
|
19960
20547
|
}
|
|
20548
|
+
function condVars(mid, slotId) {
|
|
20549
|
+
const key = `${mid}_${slotId.replace(/[^A-Za-z0-9_$]/g, "_")}`;
|
|
20550
|
+
return { trueTpl: `__cbt_${key}`, falseTpl: `__cbf_${key}` };
|
|
20551
|
+
}
|
|
20552
|
+
function emitConditional(lines, ind, mid, c, mode2) {
|
|
20553
|
+
const v = condVars(mid, c.slotId);
|
|
20554
|
+
if (mode2 === "create") {
|
|
20555
|
+
lines.push(`${ind}__l[${c.ordinal}] = !!(${c.wrappedCondition})`);
|
|
20556
|
+
return;
|
|
20557
|
+
}
|
|
20558
|
+
const slot = `__r[${c.refIndex}]`;
|
|
20559
|
+
lines.push(`${ind}{ const __c = ${c.refIndex} in __r ? ${slot} : (${slot} = qsa(__e.primaryEl, '[bf-c="${c.slotId}"]'))`);
|
|
20560
|
+
lines.push(`${ind}if (__c) {`);
|
|
20561
|
+
lines.push(`${ind} const __x = !!(${c.wrappedCondition})`);
|
|
20562
|
+
lines.push(`${ind} const __w = (__x ? ${v.trueTpl} : ${v.falseTpl}).content.firstElementChild`);
|
|
20563
|
+
const guard = mode2 === "item" ? dedupGuard(c.ordinal) : `__seed ? (__c.outerHTML !== __w.outerHTML) : (${dedupGuard(c.ordinal)})`;
|
|
20564
|
+
lines.push(`${ind} if (${guard}) {`);
|
|
20565
|
+
lines.push(`${ind} const __n = __w.cloneNode(true)`);
|
|
20566
|
+
lines.push(`${ind} __c.replaceWith(__n)`);
|
|
20567
|
+
lines.push(`${ind} ${slot} = __n`);
|
|
20568
|
+
lines.push(`${ind} }`);
|
|
20569
|
+
lines.push(`${ind} __l[${c.ordinal}] = __x`);
|
|
20570
|
+
lines.push(`${ind}} }`);
|
|
20571
|
+
}
|
|
19961
20572
|
function dedupGuard(ordinal) {
|
|
19962
20573
|
return `!(${ordinal} in __l) || !Object.is(__l[${ordinal}], __x)`;
|
|
19963
20574
|
}
|
|
19964
20575
|
function emitAttrBinding(lines, ind, a, mode2) {
|
|
19965
|
-
|
|
20576
|
+
const target2 = mode2 === "create" ? `__r[${a.refIndex}]` : elementAccess(a);
|
|
20577
|
+
lines.push(`${ind}{ const __t = ${target2}`);
|
|
19966
20578
|
lines.push(`${ind}if (__t) {`);
|
|
19967
20579
|
lines.push(`${ind} const __x = ${a.wrappedExpression}`);
|
|
19968
20580
|
const guard = mode2 === "create" ? null : mode2 === "item" ? dedupGuard(a.ordinal) : `__seed ? (${seedDiffersExpr("__t", a)}) : (${dedupGuard(a.ordinal)})`;
|
|
@@ -21029,7 +21641,7 @@ function buildPlainLoopPlan(elem, profileComponentName, lazyScope) {
|
|
|
21029
21641
|
const indexParam = elem.index || "__idx";
|
|
21030
21642
|
const mapPreambleWrapped = elem.preamble ? renderPreamble(elem.preamble, {
|
|
21031
21643
|
transformJs: wrap,
|
|
21032
|
-
renderLeaf: (ir) => irToHtmlTemplate(ir, void 0, 1, [{ param: elem.param, bindings: elem.paramBindings }], void 0
|
|
21644
|
+
renderLeaf: (ir) => irToHtmlTemplate(ir, void 0, 1, [{ param: elem.param, bindings: elem.paramBindings }], void 0)
|
|
21033
21645
|
}) : "";
|
|
21034
21646
|
const preambleRegions = buildPreambleRegionPlans(elem.preambleRegions, elem.param, elem.paramBindings);
|
|
21035
21647
|
return {
|
|
@@ -21117,7 +21729,7 @@ function buildStaticLoopMaterialize(elem, unsafeLocalNames) {
|
|
|
21117
21729
|
return {
|
|
21118
21730
|
itemTemplate: elem.staticItemTemplate,
|
|
21119
21731
|
mapPreamble: elem.preamble ? renderPreamble(elem.preamble, {
|
|
21120
|
-
renderLeaf: (ir) => irToHtmlTemplate(ir, void 0, 1, [{ param: elem.param, bindings: elem.paramBindings }], void 0
|
|
21732
|
+
renderLeaf: (ir) => irToHtmlTemplate(ir, void 0, 1, [{ param: elem.param, bindings: elem.paramBindings }], void 0)
|
|
21121
21733
|
}) : "",
|
|
21122
21734
|
bodyIsMultiRoot: elem.bodyIsMultiRoot ?? false
|
|
21123
21735
|
};
|
|
@@ -21413,25 +22025,25 @@ var init_phases = __esm({
|
|
|
21413
22025
|
});
|
|
21414
22026
|
|
|
21415
22027
|
// ../jsx/src/ir-to-client-js/rewrite-props-object.ts
|
|
21416
|
-
import
|
|
22028
|
+
import ts17 from "typescript";
|
|
21417
22029
|
function rewritePropsObjectRef(code, propsObjectName) {
|
|
21418
22030
|
const srcPropsName = propsObjectName ?? "props";
|
|
21419
22031
|
if (srcPropsName === PROPS_PARAM) return code;
|
|
21420
22032
|
if (!new RegExp(`\\b${srcPropsName}\\b`).test(code)) return code;
|
|
21421
|
-
const sourceFile =
|
|
22033
|
+
const sourceFile = ts17.createSourceFile(
|
|
21422
22034
|
"init-body.ts",
|
|
21423
22035
|
code,
|
|
21424
|
-
|
|
22036
|
+
ts17.ScriptTarget.Latest,
|
|
21425
22037
|
/*setParentNodes*/
|
|
21426
22038
|
true,
|
|
21427
|
-
|
|
22039
|
+
ts17.ScriptKind.TS
|
|
21428
22040
|
);
|
|
21429
22041
|
const spans = [];
|
|
21430
22042
|
function visit3(node) {
|
|
21431
|
-
if (
|
|
22043
|
+
if (ts17.isIdentifier(node) && node.text === srcPropsName && shouldRewrite(node)) {
|
|
21432
22044
|
spans.push([node.getStart(sourceFile), node.getEnd()]);
|
|
21433
22045
|
}
|
|
21434
|
-
|
|
22046
|
+
ts17.forEachChild(node, visit3);
|
|
21435
22047
|
}
|
|
21436
22048
|
visit3(sourceFile);
|
|
21437
22049
|
if (spans.length === 0) return code;
|
|
@@ -21445,12 +22057,12 @@ function rewritePropsObjectRef(code, propsObjectName) {
|
|
|
21445
22057
|
function shouldRewrite(node) {
|
|
21446
22058
|
const parent2 = node.parent;
|
|
21447
22059
|
if (!parent2) return true;
|
|
21448
|
-
if (
|
|
21449
|
-
if (
|
|
21450
|
-
if (
|
|
21451
|
-
if (
|
|
21452
|
-
if (
|
|
21453
|
-
if (
|
|
22060
|
+
if (ts17.isPropertyAccessExpression(parent2) && parent2.name === node) return false;
|
|
22061
|
+
if (ts17.isPropertyAssignment(parent2) && parent2.name === node) return false;
|
|
22062
|
+
if (ts17.isShorthandPropertyAssignment(parent2) && parent2.name === node) return false;
|
|
22063
|
+
if (ts17.isPropertySignature(parent2) && parent2.name === node) return false;
|
|
22064
|
+
if (ts17.isPropertyDeclaration(parent2) && parent2.name === node) return false;
|
|
22065
|
+
if (ts17.isBindingElement(parent2) && parent2.name === node) return false;
|
|
21454
22066
|
return true;
|
|
21455
22067
|
}
|
|
21456
22068
|
var init_rewrite_props_object = __esm({
|
|
@@ -21839,7 +22451,6 @@ function generateTemplateOnlyMount(ir, ctx2) {
|
|
|
21839
22451
|
ir.root,
|
|
21840
22452
|
csrInlinableConstants,
|
|
21841
22453
|
ctx2,
|
|
21842
|
-
void 0,
|
|
21843
22454
|
restSpreadNames,
|
|
21844
22455
|
ctx2.propsObjectName,
|
|
21845
22456
|
unsafeLocalNames
|
|
@@ -22134,7 +22745,7 @@ var init_css_layer_prefixer = __esm({
|
|
|
22134
22745
|
});
|
|
22135
22746
|
|
|
22136
22747
|
// ../jsx/src/preprocess-inline-jsx-callbacks.ts
|
|
22137
|
-
import
|
|
22748
|
+
import ts18 from "typescript";
|
|
22138
22749
|
function preprocessInlineJsxCallbacks(source, filePath) {
|
|
22139
22750
|
const errors = [];
|
|
22140
22751
|
const syntheticNames = [];
|
|
@@ -22154,15 +22765,15 @@ function preprocessInlineJsxCallbacks(source, filePath) {
|
|
|
22154
22765
|
return { source: current, errors, syntheticNames };
|
|
22155
22766
|
}
|
|
22156
22767
|
function runSinglePass(source, filePath, startingCounter) {
|
|
22157
|
-
const sourceFile =
|
|
22768
|
+
const sourceFile = ts18.createSourceFile(
|
|
22158
22769
|
filePath,
|
|
22159
22770
|
source,
|
|
22160
|
-
|
|
22771
|
+
ts18.ScriptTarget.Latest,
|
|
22161
22772
|
true,
|
|
22162
|
-
|
|
22773
|
+
ts18.ScriptKind.TSX
|
|
22163
22774
|
);
|
|
22164
22775
|
const hasUseClient = sourceFile.statements.some(
|
|
22165
|
-
(stmt) =>
|
|
22776
|
+
(stmt) => ts18.isExpressionStatement(stmt) && ts18.isStringLiteral(stmt.expression) && (stmt.expression.text === "use client" || stmt.expression.text === "'use client'")
|
|
22166
22777
|
);
|
|
22167
22778
|
if (!hasUseClient) {
|
|
22168
22779
|
return { source, errors: [], syntheticNames: [], counterAfter: startingCounter };
|
|
@@ -22185,20 +22796,20 @@ function runSinglePass(source, filePath, startingCounter) {
|
|
|
22185
22796
|
}
|
|
22186
22797
|
}
|
|
22187
22798
|
function visit3(node) {
|
|
22188
|
-
if (
|
|
22799
|
+
if (ts18.isJsxAttribute(node) && node.initializer && ts18.isJsxExpression(node.initializer) && node.initializer.expression) {
|
|
22189
22800
|
if (tryHandleArrowValue(node.initializer.expression)) {
|
|
22190
22801
|
return;
|
|
22191
22802
|
}
|
|
22192
22803
|
}
|
|
22193
|
-
if (
|
|
22804
|
+
if (ts18.isPropertyAssignment(node) && node.initializer) {
|
|
22194
22805
|
if (tryHandleArrowValue(node.initializer)) return;
|
|
22195
22806
|
}
|
|
22196
|
-
|
|
22807
|
+
ts18.forEachChild(node, visit3);
|
|
22197
22808
|
}
|
|
22198
22809
|
function tryHandleArrowValue(initializer) {
|
|
22199
22810
|
let expr = initializer;
|
|
22200
|
-
while (
|
|
22201
|
-
if (
|
|
22811
|
+
while (ts18.isParenthesizedExpression(expr)) expr = expr.expression;
|
|
22812
|
+
if (ts18.isArrowFunction(expr) && arrowBodyContainsJsx(expr)) {
|
|
22202
22813
|
return handleInlineArrow(expr);
|
|
22203
22814
|
}
|
|
22204
22815
|
return false;
|
|
@@ -22233,7 +22844,7 @@ function runSinglePass(source, filePath, startingCounter) {
|
|
|
22233
22844
|
replacements.push({ start: arrowStart, end: arrowEnd, text: name2 });
|
|
22234
22845
|
return true;
|
|
22235
22846
|
}
|
|
22236
|
-
|
|
22847
|
+
ts18.forEachChild(sourceFile, visit3);
|
|
22237
22848
|
if (replacements.length === 0) {
|
|
22238
22849
|
return { source, errors, syntheticNames, counterAfter: counter };
|
|
22239
22850
|
}
|
|
@@ -22252,48 +22863,48 @@ function errorMessageForCapture(captures) {
|
|
|
22252
22863
|
return `Inline JSX-returning arrow function captures non-module identifier(s): ${captures.sort().join(", ")}. Extract the callback into a top-level '\\'use client\\'' component (e.g. \`function MyNode(n) { return <div/> }\` then \`renderNode={MyNode}\`) or pass captured values via component props.`;
|
|
22253
22864
|
}
|
|
22254
22865
|
function arrowBodyContainsJsx(arrow) {
|
|
22255
|
-
if (
|
|
22866
|
+
if (ts18.isBlock(arrow.body)) {
|
|
22256
22867
|
return blockReturnsJsx(arrow.body);
|
|
22257
22868
|
}
|
|
22258
22869
|
let body2 = arrow.body;
|
|
22259
|
-
while (
|
|
22870
|
+
while (ts18.isParenthesizedExpression(body2)) body2 = body2.expression;
|
|
22260
22871
|
return isJsxLike(body2);
|
|
22261
22872
|
}
|
|
22262
22873
|
function blockReturnsJsx(block) {
|
|
22263
22874
|
let found = false;
|
|
22264
22875
|
function visit3(n) {
|
|
22265
22876
|
if (found) return;
|
|
22266
|
-
if (
|
|
22877
|
+
if (ts18.isReturnStatement(n) && n.expression) {
|
|
22267
22878
|
let e = n.expression;
|
|
22268
|
-
while (
|
|
22879
|
+
while (ts18.isParenthesizedExpression(e)) e = e.expression;
|
|
22269
22880
|
if (isJsxLike(e)) {
|
|
22270
22881
|
found = true;
|
|
22271
22882
|
return;
|
|
22272
22883
|
}
|
|
22273
22884
|
}
|
|
22274
|
-
if (
|
|
22275
|
-
|
|
22885
|
+
if (ts18.isArrowFunction(n) || ts18.isFunctionDeclaration(n) || ts18.isFunctionExpression(n)) return;
|
|
22886
|
+
ts18.forEachChild(n, visit3);
|
|
22276
22887
|
}
|
|
22277
|
-
|
|
22888
|
+
ts18.forEachChild(block, visit3);
|
|
22278
22889
|
return found;
|
|
22279
22890
|
}
|
|
22280
22891
|
function isJsxLike(expr) {
|
|
22281
|
-
return
|
|
22892
|
+
return ts18.isJsxElement(expr) || ts18.isJsxSelfClosingElement(expr) || ts18.isJsxFragment(expr);
|
|
22282
22893
|
}
|
|
22283
22894
|
function collectArrowParamNames(arrow) {
|
|
22284
22895
|
const names = /* @__PURE__ */ new Set();
|
|
22285
|
-
for (const p of arrow.parameters)
|
|
22896
|
+
for (const p of arrow.parameters) collectBindingNames4(p.name, names);
|
|
22286
22897
|
return names;
|
|
22287
22898
|
}
|
|
22288
|
-
function
|
|
22899
|
+
function collectBindingNames4(name2, out) {
|
|
22289
22900
|
const push = Array.isArray(out) ? (n) => out.push(n) : (n) => out.add(n);
|
|
22290
|
-
if (
|
|
22901
|
+
if (ts18.isIdentifier(name2)) {
|
|
22291
22902
|
push(name2.text);
|
|
22292
|
-
} else if (
|
|
22293
|
-
name2.elements.forEach((el) =>
|
|
22294
|
-
} else if (
|
|
22903
|
+
} else if (ts18.isObjectBindingPattern(name2)) {
|
|
22904
|
+
name2.elements.forEach((el) => collectBindingNames4(el.name, out));
|
|
22905
|
+
} else if (ts18.isArrayBindingPattern(name2)) {
|
|
22295
22906
|
name2.elements.forEach((el) => {
|
|
22296
|
-
if (!
|
|
22907
|
+
if (!ts18.isOmittedExpression(el)) collectBindingNames4(el.name, out);
|
|
22297
22908
|
});
|
|
22298
22909
|
}
|
|
22299
22910
|
}
|
|
@@ -22302,12 +22913,12 @@ function collectFreeIdentifiers(arrow) {
|
|
|
22302
22913
|
const bound = [];
|
|
22303
22914
|
for (const p of arrow.parameters) {
|
|
22304
22915
|
const names = [];
|
|
22305
|
-
|
|
22916
|
+
collectBindingNames4(p.name, names);
|
|
22306
22917
|
bound.push(...names);
|
|
22307
22918
|
}
|
|
22308
22919
|
function pushBindings(name2) {
|
|
22309
22920
|
const names = [];
|
|
22310
|
-
|
|
22921
|
+
collectBindingNames4(name2, names);
|
|
22311
22922
|
bound.push(...names);
|
|
22312
22923
|
return names;
|
|
22313
22924
|
}
|
|
@@ -22318,71 +22929,71 @@ function collectFreeIdentifiers(arrow) {
|
|
|
22318
22929
|
return bound.includes(name2);
|
|
22319
22930
|
}
|
|
22320
22931
|
function visit3(node) {
|
|
22321
|
-
if (
|
|
22932
|
+
if (ts18.isIdentifier(node)) {
|
|
22322
22933
|
const parent2 = node.parent;
|
|
22323
|
-
if (parent2 &&
|
|
22324
|
-
if (parent2 &&
|
|
22325
|
-
if (parent2 &&
|
|
22326
|
-
if (parent2 &&
|
|
22327
|
-
if (parent2 &&
|
|
22328
|
-
if (parent2 &&
|
|
22329
|
-
if (parent2 &&
|
|
22330
|
-
if (parent2 &&
|
|
22331
|
-
if (parent2 &&
|
|
22332
|
-
if (parent2 &&
|
|
22333
|
-
if (parent2 &&
|
|
22934
|
+
if (parent2 && ts18.isPropertyAccessExpression(parent2) && parent2.name === node) return;
|
|
22935
|
+
if (parent2 && ts18.isPropertyAssignment(parent2) && parent2.name === node) return;
|
|
22936
|
+
if (parent2 && ts18.isPropertySignature(parent2) && parent2.name === node) return;
|
|
22937
|
+
if (parent2 && ts18.isPropertyDeclaration(parent2) && parent2.name === node) return;
|
|
22938
|
+
if (parent2 && ts18.isMethodDeclaration(parent2) && parent2.name === node) return;
|
|
22939
|
+
if (parent2 && ts18.isMethodSignature(parent2) && parent2.name === node) return;
|
|
22940
|
+
if (parent2 && ts18.isGetAccessorDeclaration(parent2) && parent2.name === node) return;
|
|
22941
|
+
if (parent2 && ts18.isSetAccessorDeclaration(parent2) && parent2.name === node) return;
|
|
22942
|
+
if (parent2 && ts18.isEnumMember(parent2) && parent2.name === node) return;
|
|
22943
|
+
if (parent2 && ts18.isBindingElement(parent2) && parent2.propertyName === node) return;
|
|
22944
|
+
if (parent2 && ts18.isShorthandPropertyAssignment(parent2) && parent2.name === node) {
|
|
22334
22945
|
if (!isBound(node.text)) ids.add(node.text);
|
|
22335
22946
|
return;
|
|
22336
22947
|
}
|
|
22337
|
-
if (parent2 &&
|
|
22338
|
-
if (parent2 &&
|
|
22339
|
-
if (parent2 &&
|
|
22340
|
-
if (parent2 &&
|
|
22341
|
-
if (parent2 &&
|
|
22342
|
-
if (parent2 &&
|
|
22948
|
+
if (parent2 && ts18.isParameter(parent2) && parent2.name === node) return;
|
|
22949
|
+
if (parent2 && ts18.isVariableDeclaration(parent2) && parent2.name === node) return;
|
|
22950
|
+
if (parent2 && ts18.isFunctionDeclaration(parent2) && parent2.name === node) return;
|
|
22951
|
+
if (parent2 && ts18.isClassDeclaration(parent2) && parent2.name === node) return;
|
|
22952
|
+
if (parent2 && ts18.isJsxAttribute(parent2) && parent2.name === node) return;
|
|
22953
|
+
if (parent2 && ts18.isJsxOpeningElement(parent2) && parent2.tagName === node) {
|
|
22343
22954
|
if (/^[a-z]/.test(node.text)) return;
|
|
22344
22955
|
}
|
|
22345
|
-
if (parent2 &&
|
|
22956
|
+
if (parent2 && ts18.isJsxClosingElement(parent2) && parent2.tagName === node) {
|
|
22346
22957
|
if (/^[a-z]/.test(node.text)) return;
|
|
22347
22958
|
}
|
|
22348
22959
|
if (isBound(node.text)) return;
|
|
22349
22960
|
ids.add(node.text);
|
|
22350
22961
|
return;
|
|
22351
22962
|
}
|
|
22352
|
-
if (
|
|
22963
|
+
if (ts18.isVariableDeclaration(node)) {
|
|
22353
22964
|
const declared = pushBindings(node.name);
|
|
22354
22965
|
if (node.initializer) visit3(node.initializer);
|
|
22355
22966
|
declared;
|
|
22356
22967
|
return;
|
|
22357
22968
|
}
|
|
22358
|
-
if (
|
|
22969
|
+
if (ts18.isFunctionDeclaration(node)) {
|
|
22359
22970
|
if (node.name) bound.push(node.name.text);
|
|
22360
22971
|
visitInsideNewScope(node);
|
|
22361
22972
|
return;
|
|
22362
22973
|
}
|
|
22363
|
-
if (
|
|
22974
|
+
if (ts18.isClassDeclaration(node)) {
|
|
22364
22975
|
if (node.name) bound.push(node.name.text);
|
|
22365
|
-
|
|
22976
|
+
ts18.forEachChild(node, visit3);
|
|
22366
22977
|
return;
|
|
22367
22978
|
}
|
|
22368
|
-
if (
|
|
22979
|
+
if (ts18.isArrowFunction(node) || ts18.isFunctionExpression(node)) {
|
|
22369
22980
|
visitInsideNewScope(node);
|
|
22370
22981
|
return;
|
|
22371
22982
|
}
|
|
22372
|
-
if (
|
|
22983
|
+
if (ts18.isCatchClause(node)) {
|
|
22373
22984
|
const before = bound.length;
|
|
22374
22985
|
if (node.variableDeclaration) pushBindings(node.variableDeclaration.name);
|
|
22375
|
-
|
|
22986
|
+
ts18.forEachChild(node, visit3);
|
|
22376
22987
|
popN(bound.length - before);
|
|
22377
22988
|
return;
|
|
22378
22989
|
}
|
|
22379
|
-
if (
|
|
22990
|
+
if (ts18.isBlock(node)) {
|
|
22380
22991
|
const before = bound.length;
|
|
22381
|
-
|
|
22992
|
+
ts18.forEachChild(node, visit3);
|
|
22382
22993
|
popN(bound.length - before);
|
|
22383
22994
|
return;
|
|
22384
22995
|
}
|
|
22385
|
-
|
|
22996
|
+
ts18.forEachChild(node, visit3);
|
|
22386
22997
|
}
|
|
22387
22998
|
function visitInsideNewScope(fn) {
|
|
22388
22999
|
const before = bound.length;
|
|
@@ -22402,27 +23013,27 @@ function collectFreeIdentifiers(arrow) {
|
|
|
22402
23013
|
function collectModuleScopeNames(sourceFile) {
|
|
22403
23014
|
const names = /* @__PURE__ */ new Set();
|
|
22404
23015
|
for (const stmt of sourceFile.statements) {
|
|
22405
|
-
if (
|
|
22406
|
-
else if (
|
|
22407
|
-
else if (
|
|
22408
|
-
for (const decl of stmt.declarationList.declarations)
|
|
22409
|
-
} else if (
|
|
23016
|
+
if (ts18.isFunctionDeclaration(stmt) && stmt.name) names.add(stmt.name.text);
|
|
23017
|
+
else if (ts18.isClassDeclaration(stmt) && stmt.name) names.add(stmt.name.text);
|
|
23018
|
+
else if (ts18.isVariableStatement(stmt)) {
|
|
23019
|
+
for (const decl of stmt.declarationList.declarations) collectBindingNames4(decl.name, names);
|
|
23020
|
+
} else if (ts18.isImportDeclaration(stmt) && stmt.importClause) {
|
|
22410
23021
|
const ic = stmt.importClause;
|
|
22411
23022
|
if (ic.name) names.add(ic.name.text);
|
|
22412
23023
|
if (ic.namedBindings) {
|
|
22413
|
-
if (
|
|
23024
|
+
if (ts18.isNamespaceImport(ic.namedBindings)) names.add(ic.namedBindings.name.text);
|
|
22414
23025
|
else for (const e of ic.namedBindings.elements) names.add(e.name.text);
|
|
22415
23026
|
}
|
|
22416
|
-
} else if (
|
|
22417
|
-
else if (
|
|
22418
|
-
else if (
|
|
23027
|
+
} else if (ts18.isTypeAliasDeclaration(stmt)) names.add(stmt.name.text);
|
|
23028
|
+
else if (ts18.isInterfaceDeclaration(stmt)) names.add(stmt.name.text);
|
|
23029
|
+
else if (ts18.isEnumDeclaration(stmt)) names.add(stmt.name.text);
|
|
22419
23030
|
}
|
|
22420
23031
|
return names;
|
|
22421
23032
|
}
|
|
22422
23033
|
function buildSyntheticDeclaration(name2, arrow, sourceFile) {
|
|
22423
23034
|
const paramsText = arrow.parameters.length === 0 ? "" : arrow.parameters.map((p) => p.getText(sourceFile)).join(", ");
|
|
22424
23035
|
let bodyText;
|
|
22425
|
-
if (
|
|
23036
|
+
if (ts18.isBlock(arrow.body)) {
|
|
22426
23037
|
bodyText = arrow.body.getText(sourceFile);
|
|
22427
23038
|
} else {
|
|
22428
23039
|
const expr = arrow.body.getText(sourceFile);
|
|
@@ -22441,7 +23052,7 @@ var init_preprocess_inline_jsx_callbacks = __esm({
|
|
|
22441
23052
|
});
|
|
22442
23053
|
|
|
22443
23054
|
// ../jsx/src/ssr-defaults.ts
|
|
22444
|
-
import
|
|
23055
|
+
import ts19 from "typescript";
|
|
22445
23056
|
function extractSsrDefaults(metadata) {
|
|
22446
23057
|
const out = {};
|
|
22447
23058
|
const propsLike = /* @__PURE__ */ new Set();
|
|
@@ -22449,11 +23060,12 @@ function extractSsrDefaults(metadata) {
|
|
|
22449
23060
|
for (const p of metadata.propsParams) propsLike.add(p.name);
|
|
22450
23061
|
for (const p of metadata.propsParams) {
|
|
22451
23062
|
if (p.isRest) continue;
|
|
23063
|
+
const callerPropName = p.sourceName ?? p.name;
|
|
22452
23064
|
if (metadata.propsObjectName === null && p.defaultValue !== void 0) {
|
|
22453
23065
|
const value2 = tryStaticEval(p.defaultValue, { bindings: {}, propsLike });
|
|
22454
|
-
out[p.name] = { propName:
|
|
23066
|
+
out[p.name] = { propName: callerPropName, value: resultToJsonable(value2) };
|
|
22455
23067
|
} else {
|
|
22456
|
-
out[p.name] = { propName:
|
|
23068
|
+
out[p.name] = { propName: callerPropName, value: null };
|
|
22457
23069
|
}
|
|
22458
23070
|
}
|
|
22459
23071
|
if (metadata.restPropsName) {
|
|
@@ -22501,11 +23113,11 @@ function collectPropRefs(expr, propsObjectName, out) {
|
|
|
22501
23113
|
const node = parseExpression2(expr);
|
|
22502
23114
|
if (!node) return;
|
|
22503
23115
|
const visit3 = (n) => {
|
|
22504
|
-
if (
|
|
23116
|
+
if (ts19.isPropertyAccessExpression(n) && ts19.isIdentifier(n.expression) && n.expression.text === propsObjectName && ts19.isIdentifier(n.name)) {
|
|
22505
23117
|
out.add(n.name.text);
|
|
22506
23118
|
return;
|
|
22507
23119
|
}
|
|
22508
|
-
|
|
23120
|
+
ts19.forEachChild(n, visit3);
|
|
22509
23121
|
};
|
|
22510
23122
|
visit3(node);
|
|
22511
23123
|
}
|
|
@@ -22522,21 +23134,21 @@ function tryStaticEval(expr, ctx2) {
|
|
|
22522
23134
|
}
|
|
22523
23135
|
function evalStatementsForReturn(statements, ctx2) {
|
|
22524
23136
|
for (const stmt of statements) {
|
|
22525
|
-
if (
|
|
23137
|
+
if (ts19.isVariableStatement(stmt)) {
|
|
22526
23138
|
for (const d of stmt.declarationList.declarations) {
|
|
22527
|
-
if (!
|
|
23139
|
+
if (!ts19.isIdentifier(d.name) || !d.initializer) continue;
|
|
22528
23140
|
const v = evalNode(d.initializer, ctx2);
|
|
22529
23141
|
if (v !== UNRESOLVED) ctx2.bindings[d.name.text] = v;
|
|
22530
23142
|
}
|
|
22531
|
-
} else if (
|
|
23143
|
+
} else if (ts19.isReturnStatement(stmt)) {
|
|
22532
23144
|
return stmt.expression ? evalNode(stmt.expression, ctx2) : UNRESOLVED;
|
|
22533
|
-
} else if (
|
|
23145
|
+
} else if (ts19.isIfStatement(stmt)) {
|
|
22534
23146
|
const cond = evalNode(stmt.expression, ctx2);
|
|
22535
23147
|
if (cond === UNRESOLVED) return UNRESOLVED;
|
|
22536
23148
|
const branch = cond ? stmt.thenStatement : stmt.elseStatement;
|
|
22537
23149
|
if (branch) {
|
|
22538
23150
|
const taken = evalStatementsForReturn(
|
|
22539
|
-
|
|
23151
|
+
ts19.isBlock(branch) ? branch.statements : [branch],
|
|
22540
23152
|
ctx2
|
|
22541
23153
|
);
|
|
22542
23154
|
if (taken !== NO_RETURN) return taken;
|
|
@@ -22548,64 +23160,64 @@ function evalStatementsForReturn(statements, ctx2) {
|
|
|
22548
23160
|
return NO_RETURN;
|
|
22549
23161
|
}
|
|
22550
23162
|
function parseExpression2(expr) {
|
|
22551
|
-
const sf =
|
|
23163
|
+
const sf = ts19.createSourceFile(
|
|
22552
23164
|
"__ssr_default__.ts",
|
|
22553
23165
|
`(${expr})`,
|
|
22554
|
-
|
|
23166
|
+
ts19.ScriptTarget.Latest,
|
|
22555
23167
|
false,
|
|
22556
|
-
|
|
23168
|
+
ts19.ScriptKind.TS
|
|
22557
23169
|
);
|
|
22558
23170
|
const stmt = sf.statements[0];
|
|
22559
|
-
if (!stmt || !
|
|
22560
|
-
const inner =
|
|
23171
|
+
if (!stmt || !ts19.isExpressionStatement(stmt)) return null;
|
|
23172
|
+
const inner = ts19.isParenthesizedExpression(stmt.expression) ? stmt.expression.expression : stmt.expression;
|
|
22561
23173
|
return inner;
|
|
22562
23174
|
}
|
|
22563
23175
|
function evalNode(node, ctx2) {
|
|
22564
|
-
if (
|
|
22565
|
-
if (
|
|
22566
|
-
if (
|
|
22567
|
-
if (
|
|
22568
|
-
if (
|
|
22569
|
-
if (
|
|
23176
|
+
if (ts19.isParenthesizedExpression(node)) return evalNode(node.expression, ctx2);
|
|
23177
|
+
if (ts19.isAsExpression(node)) return evalNode(node.expression, ctx2);
|
|
23178
|
+
if (ts19.isSatisfiesExpression(node)) return evalNode(node.expression, ctx2);
|
|
23179
|
+
if (ts19.isTypeAssertionExpression(node)) return evalNode(node.expression, ctx2);
|
|
23180
|
+
if (ts19.isNonNullExpression(node)) return evalNode(node.expression, ctx2);
|
|
23181
|
+
if (ts19.isArrowFunction(node)) {
|
|
22570
23182
|
if (node.parameters.length !== 0) return UNRESOLVED;
|
|
22571
|
-
if (!
|
|
23183
|
+
if (!ts19.isBlock(node.body)) return evalNode(node.body, ctx2);
|
|
22572
23184
|
const localBindings = { ...ctx2.bindings };
|
|
22573
23185
|
const localCtx = { ...ctx2, bindings: localBindings };
|
|
22574
23186
|
const result2 = evalStatementsForReturn(node.body.statements, localCtx);
|
|
22575
23187
|
return result2 === NO_RETURN ? UNRESOLVED : result2;
|
|
22576
23188
|
}
|
|
22577
|
-
if (
|
|
22578
|
-
if (
|
|
22579
|
-
if (node.kind ===
|
|
22580
|
-
if (node.kind ===
|
|
22581
|
-
if (node.kind ===
|
|
22582
|
-
if (
|
|
23189
|
+
if (ts19.isNumericLiteral(node)) return Number(node.text);
|
|
23190
|
+
if (ts19.isStringLiteralLike(node)) return node.text;
|
|
23191
|
+
if (node.kind === ts19.SyntaxKind.TrueKeyword) return true;
|
|
23192
|
+
if (node.kind === ts19.SyntaxKind.FalseKeyword) return false;
|
|
23193
|
+
if (node.kind === ts19.SyntaxKind.NullKeyword) return null;
|
|
23194
|
+
if (ts19.isIdentifier(node)) {
|
|
22583
23195
|
if (node.text === "undefined") return void 0;
|
|
22584
23196
|
if (node.text in ctx2.bindings) return ctx2.bindings[node.text];
|
|
22585
23197
|
if (ctx2.propsLike.has(node.text)) return void 0;
|
|
22586
23198
|
return UNRESOLVED;
|
|
22587
23199
|
}
|
|
22588
|
-
if (
|
|
23200
|
+
if (ts19.isPrefixUnaryExpression(node)) {
|
|
22589
23201
|
const arg = evalNode(node.operand, ctx2);
|
|
22590
23202
|
if (arg === UNRESOLVED) return UNRESOLVED;
|
|
22591
23203
|
switch (node.operator) {
|
|
22592
|
-
case
|
|
23204
|
+
case ts19.SyntaxKind.MinusToken:
|
|
22593
23205
|
return typeof arg === "number" ? -arg : UNRESOLVED;
|
|
22594
|
-
case
|
|
23206
|
+
case ts19.SyntaxKind.PlusToken:
|
|
22595
23207
|
return typeof arg === "number" ? +arg : UNRESOLVED;
|
|
22596
|
-
case
|
|
23208
|
+
case ts19.SyntaxKind.ExclamationToken:
|
|
22597
23209
|
return !arg;
|
|
22598
23210
|
}
|
|
22599
23211
|
return UNRESOLVED;
|
|
22600
23212
|
}
|
|
22601
|
-
if (
|
|
23213
|
+
if (ts19.isObjectLiteralExpression(node)) {
|
|
22602
23214
|
const obj = {};
|
|
22603
23215
|
for (const prop of node.properties) {
|
|
22604
|
-
if (!
|
|
23216
|
+
if (!ts19.isPropertyAssignment(prop)) return UNRESOLVED;
|
|
22605
23217
|
let key;
|
|
22606
|
-
if (
|
|
23218
|
+
if (ts19.isIdentifier(prop.name) || ts19.isStringLiteralLike(prop.name)) {
|
|
22607
23219
|
key = prop.name.text;
|
|
22608
|
-
} else if (
|
|
23220
|
+
} else if (ts19.isNumericLiteral(prop.name)) {
|
|
22609
23221
|
key = prop.name.text;
|
|
22610
23222
|
} else {
|
|
22611
23223
|
return UNRESOLVED;
|
|
@@ -22616,17 +23228,17 @@ function evalNode(node, ctx2) {
|
|
|
22616
23228
|
}
|
|
22617
23229
|
return obj;
|
|
22618
23230
|
}
|
|
22619
|
-
if (
|
|
23231
|
+
if (ts19.isArrayLiteralExpression(node)) {
|
|
22620
23232
|
const arr = [];
|
|
22621
23233
|
for (const elem of node.elements) {
|
|
22622
|
-
if (
|
|
23234
|
+
if (ts19.isOmittedExpression(elem)) return UNRESOLVED;
|
|
22623
23235
|
const v = evalNode(elem, ctx2);
|
|
22624
23236
|
if (v === UNRESOLVED) return UNRESOLVED;
|
|
22625
23237
|
arr.push(v === void 0 ? null : v);
|
|
22626
23238
|
}
|
|
22627
23239
|
return arr;
|
|
22628
23240
|
}
|
|
22629
|
-
if (
|
|
23241
|
+
if (ts19.isElementAccessExpression(node)) {
|
|
22630
23242
|
const base = evalNode(node.expression, ctx2);
|
|
22631
23243
|
if (base === void 0) return void 0;
|
|
22632
23244
|
if (base === UNRESOLVED || base === null || typeof base !== "object") return UNRESOLVED;
|
|
@@ -22636,16 +23248,16 @@ function evalNode(node, ctx2) {
|
|
|
22636
23248
|
const k = String(key);
|
|
22637
23249
|
return Object.prototype.hasOwnProperty.call(base, k) ? base[k] : void 0;
|
|
22638
23250
|
}
|
|
22639
|
-
if (
|
|
23251
|
+
if (ts19.isPropertyAccessExpression(node)) {
|
|
22640
23252
|
const baseResult = evalNode(node.expression, ctx2);
|
|
22641
23253
|
if (baseResult === void 0) return void 0;
|
|
22642
23254
|
return UNRESOLVED;
|
|
22643
23255
|
}
|
|
22644
|
-
if (
|
|
22645
|
-
if (node.arguments.length === 0 &&
|
|
23256
|
+
if (ts19.isCallExpression(node)) {
|
|
23257
|
+
if (node.arguments.length === 0 && ts19.isIdentifier(node.expression) && node.expression.text in ctx2.bindings) {
|
|
22646
23258
|
return ctx2.bindings[node.expression.text];
|
|
22647
23259
|
}
|
|
22648
|
-
if (
|
|
23260
|
+
if (ts19.isPropertyAccessExpression(node.expression) && node.expression.name.text === "join") {
|
|
22649
23261
|
const recv = evalNode(node.expression.expression, ctx2);
|
|
22650
23262
|
if (Array.isArray(recv)) {
|
|
22651
23263
|
let sep = ",";
|
|
@@ -22660,24 +23272,24 @@ function evalNode(node, ctx2) {
|
|
|
22660
23272
|
}
|
|
22661
23273
|
return UNRESOLVED;
|
|
22662
23274
|
}
|
|
22663
|
-
if (
|
|
23275
|
+
if (ts19.isConditionalExpression(node)) {
|
|
22664
23276
|
const cond = evalNode(node.condition, ctx2);
|
|
22665
23277
|
if (cond === UNRESOLVED) return UNRESOLVED;
|
|
22666
23278
|
return cond ? evalNode(node.whenTrue, ctx2) : evalNode(node.whenFalse, ctx2);
|
|
22667
23279
|
}
|
|
22668
|
-
if (
|
|
23280
|
+
if (ts19.isBinaryExpression(node)) {
|
|
22669
23281
|
const op = node.operatorToken.kind;
|
|
22670
|
-
if (op ===
|
|
23282
|
+
if (op === ts19.SyntaxKind.QuestionQuestionToken) {
|
|
22671
23283
|
const l2 = evalNode(node.left, ctx2);
|
|
22672
23284
|
if (l2 !== UNRESOLVED && l2 !== null && l2 !== void 0) return l2;
|
|
22673
23285
|
return evalNode(node.right, ctx2);
|
|
22674
23286
|
}
|
|
22675
|
-
if (op ===
|
|
23287
|
+
if (op === ts19.SyntaxKind.BarBarToken) {
|
|
22676
23288
|
const l2 = evalNode(node.left, ctx2);
|
|
22677
23289
|
if (l2 !== UNRESOLVED && l2) return l2;
|
|
22678
23290
|
return evalNode(node.right, ctx2);
|
|
22679
23291
|
}
|
|
22680
|
-
if (op ===
|
|
23292
|
+
if (op === ts19.SyntaxKind.AmpersandAmpersandToken) {
|
|
22681
23293
|
const l2 = evalNode(node.left, ctx2);
|
|
22682
23294
|
if (l2 === UNRESOLVED) return UNRESOLVED;
|
|
22683
23295
|
if (!l2) return l2;
|
|
@@ -22687,28 +23299,28 @@ function evalNode(node, ctx2) {
|
|
|
22687
23299
|
const r2 = evalNode(node.right, ctx2);
|
|
22688
23300
|
if (l === UNRESOLVED || r2 === UNRESOLVED) return UNRESOLVED;
|
|
22689
23301
|
switch (op) {
|
|
22690
|
-
case
|
|
23302
|
+
case ts19.SyntaxKind.PlusToken:
|
|
22691
23303
|
if (typeof l === "string" || typeof r2 === "string") return `${l}${r2}`;
|
|
22692
23304
|
if (typeof l === "number" && typeof r2 === "number") return l + r2;
|
|
22693
23305
|
return UNRESOLVED;
|
|
22694
|
-
case
|
|
23306
|
+
case ts19.SyntaxKind.MinusToken:
|
|
22695
23307
|
return typeof l === "number" && typeof r2 === "number" ? l - r2 : UNRESOLVED;
|
|
22696
|
-
case
|
|
23308
|
+
case ts19.SyntaxKind.AsteriskToken:
|
|
22697
23309
|
return typeof l === "number" && typeof r2 === "number" ? l * r2 : UNRESOLVED;
|
|
22698
|
-
case
|
|
23310
|
+
case ts19.SyntaxKind.SlashToken:
|
|
22699
23311
|
return typeof l === "number" && typeof r2 === "number" && r2 !== 0 ? l / r2 : UNRESOLVED;
|
|
22700
|
-
case
|
|
23312
|
+
case ts19.SyntaxKind.PercentToken:
|
|
22701
23313
|
return typeof l === "number" && typeof r2 === "number" && r2 !== 0 ? l % r2 : UNRESOLVED;
|
|
22702
|
-
case
|
|
22703
|
-
case
|
|
23314
|
+
case ts19.SyntaxKind.EqualsEqualsEqualsToken:
|
|
23315
|
+
case ts19.SyntaxKind.EqualsEqualsToken:
|
|
22704
23316
|
return l === r2;
|
|
22705
|
-
case
|
|
22706
|
-
case
|
|
23317
|
+
case ts19.SyntaxKind.ExclamationEqualsEqualsToken:
|
|
23318
|
+
case ts19.SyntaxKind.ExclamationEqualsToken:
|
|
22707
23319
|
return l !== r2;
|
|
22708
23320
|
}
|
|
22709
23321
|
return UNRESOLVED;
|
|
22710
23322
|
}
|
|
22711
|
-
if (
|
|
23323
|
+
if (ts19.isTemplateExpression(node)) {
|
|
22712
23324
|
if (node.templateSpans.length === 0) return node.head.text;
|
|
22713
23325
|
let acc = node.head.text;
|
|
22714
23326
|
for (const span of node.templateSpans) {
|
|
@@ -22718,7 +23330,7 @@ function evalNode(node, ctx2) {
|
|
|
22718
23330
|
}
|
|
22719
23331
|
return acc;
|
|
22720
23332
|
}
|
|
22721
|
-
if (
|
|
23333
|
+
if (ts19.isNoSubstitutionTemplateLiteral(node)) return node.text;
|
|
22722
23334
|
return UNRESOLVED;
|
|
22723
23335
|
}
|
|
22724
23336
|
var UNRESOLVED, NO_RETURN;
|
|
@@ -22731,7 +23343,7 @@ var init_ssr_defaults = __esm({
|
|
|
22731
23343
|
});
|
|
22732
23344
|
|
|
22733
23345
|
// ../jsx/src/augment-inherited-props.ts
|
|
22734
|
-
import
|
|
23346
|
+
import ts20 from "typescript";
|
|
22735
23347
|
function collectContextConsumers(metadata) {
|
|
22736
23348
|
const constants = metadata.localConstants ?? [];
|
|
22737
23349
|
const contextDefaults = /* @__PURE__ */ new Map();
|
|
@@ -22758,35 +23370,35 @@ function collectContextConsumers(metadata) {
|
|
|
22758
23370
|
}
|
|
22759
23371
|
function parseUseContextArg(source) {
|
|
22760
23372
|
const expr = parseSingleExpression(source);
|
|
22761
|
-
if (!expr || !
|
|
22762
|
-
if (!
|
|
23373
|
+
if (!expr || !ts20.isCallExpression(expr)) return null;
|
|
23374
|
+
if (!ts20.isIdentifier(expr.expression) || expr.expression.text !== "useContext") return null;
|
|
22763
23375
|
if (expr.arguments.length !== 1) return null;
|
|
22764
23376
|
const arg = expr.arguments[0];
|
|
22765
|
-
return
|
|
23377
|
+
return ts20.isIdentifier(arg) ? arg.text : null;
|
|
22766
23378
|
}
|
|
22767
23379
|
function parseCreateContextDefault(source) {
|
|
22768
23380
|
const expr = parseSingleExpression(source);
|
|
22769
|
-
if (!expr || !
|
|
23381
|
+
if (!expr || !ts20.isCallExpression(expr)) return null;
|
|
22770
23382
|
if (expr.arguments.length === 0) return null;
|
|
22771
23383
|
const arg = expr.arguments[0];
|
|
22772
|
-
if (
|
|
22773
|
-
if (
|
|
22774
|
-
if (arg.kind ===
|
|
22775
|
-
if (arg.kind ===
|
|
23384
|
+
if (ts20.isStringLiteral(arg) || ts20.isNoSubstitutionTemplateLiteral(arg)) return arg.text;
|
|
23385
|
+
if (ts20.isNumericLiteral(arg)) return Number(arg.text);
|
|
23386
|
+
if (arg.kind === ts20.SyntaxKind.TrueKeyword) return true;
|
|
23387
|
+
if (arg.kind === ts20.SyntaxKind.FalseKeyword) return false;
|
|
22776
23388
|
return null;
|
|
22777
23389
|
}
|
|
22778
23390
|
function isObjectLiteralCreateContextDefault(source) {
|
|
22779
23391
|
const expr = parseSingleExpression(source);
|
|
22780
|
-
if (!expr || !
|
|
23392
|
+
if (!expr || !ts20.isCallExpression(expr)) return false;
|
|
22781
23393
|
if (expr.arguments.length === 0) return false;
|
|
22782
|
-
return
|
|
23394
|
+
return ts20.isObjectLiteralExpression(expr.arguments[0]);
|
|
22783
23395
|
}
|
|
22784
23396
|
function parseSingleExpression(source) {
|
|
22785
|
-
const sf =
|
|
23397
|
+
const sf = ts20.createSourceFile("__ctx.ts", `(${source})`, ts20.ScriptTarget.Latest, false);
|
|
22786
23398
|
const stmt = sf.statements[0];
|
|
22787
|
-
if (!stmt || !
|
|
23399
|
+
if (!stmt || !ts20.isExpressionStatement(stmt)) return null;
|
|
22788
23400
|
let e = stmt.expression;
|
|
22789
|
-
while (
|
|
23401
|
+
while (ts20.isParenthesizedExpression(e)) e = e.expression;
|
|
22790
23402
|
return e;
|
|
22791
23403
|
}
|
|
22792
23404
|
function augmentInheritedPropAccesses(ir) {
|
|
@@ -22807,21 +23419,21 @@ function augmentInheritedPropAccesses(ir) {
|
|
|
22807
23419
|
const coalesceLiteralTypes = /* @__PURE__ */ new Map();
|
|
22808
23420
|
const pinCoalesceLiterals = (s) => {
|
|
22809
23421
|
if (!s || !s.includes(propsObj)) return;
|
|
22810
|
-
const sf =
|
|
23422
|
+
const sf = ts20.createSourceFile("__aug.ts", `(${s})`, ts20.ScriptTarget.Latest, false);
|
|
22811
23423
|
const visit3 = (n) => {
|
|
22812
|
-
if (
|
|
23424
|
+
if (ts20.isBinaryExpression(n) && (n.operatorToken.kind === ts20.SyntaxKind.QuestionQuestionToken || n.operatorToken.kind === ts20.SyntaxKind.BarBarToken)) {
|
|
22813
23425
|
let left = n.left;
|
|
22814
|
-
while (
|
|
22815
|
-
if (
|
|
23426
|
+
while (ts20.isParenthesizedExpression(left)) left = left.expression;
|
|
23427
|
+
if (ts20.isPropertyAccessExpression(left) && ts20.isIdentifier(left.expression) && left.expression.text === propsObj) {
|
|
22816
23428
|
const name2 = left.name.text;
|
|
22817
23429
|
let right = n.right;
|
|
22818
|
-
while (
|
|
22819
|
-
if (
|
|
22820
|
-
const kind2 =
|
|
23430
|
+
while (ts20.isParenthesizedExpression(right)) right = right.expression;
|
|
23431
|
+
if (ts20.isPrefixUnaryExpression(right)) right = right.operand;
|
|
23432
|
+
const kind2 = ts20.isNumericLiteral(right) ? "number" : right.kind === ts20.SyntaxKind.TrueKeyword || right.kind === ts20.SyntaxKind.FalseKeyword ? "boolean" : ts20.isStringLiteralLike(right) ? "string" : null;
|
|
22821
23433
|
if (kind2 && !coalesceLiteralTypes.has(name2)) coalesceLiteralTypes.set(name2, kind2);
|
|
22822
23434
|
}
|
|
22823
23435
|
}
|
|
22824
|
-
|
|
23436
|
+
ts20.forEachChild(n, visit3);
|
|
22825
23437
|
};
|
|
22826
23438
|
visit3(sf);
|
|
22827
23439
|
};
|
|
@@ -22917,39 +23529,39 @@ function augmentInheritedPropAccesses(ir) {
|
|
|
22917
23529
|
}
|
|
22918
23530
|
}
|
|
22919
23531
|
function parseStaticStringConst(source) {
|
|
22920
|
-
const sf =
|
|
23532
|
+
const sf = ts20.createSourceFile(
|
|
22921
23533
|
"__const.ts",
|
|
22922
23534
|
`const __x = (${source});`,
|
|
22923
|
-
|
|
23535
|
+
ts20.ScriptTarget.Latest,
|
|
22924
23536
|
/*setParentNodes*/
|
|
22925
23537
|
false
|
|
22926
23538
|
);
|
|
22927
23539
|
const stmt = sf.statements[0];
|
|
22928
|
-
if (!stmt || !
|
|
23540
|
+
if (!stmt || !ts20.isVariableStatement(stmt)) return null;
|
|
22929
23541
|
let init = stmt.declarationList.declarations[0]?.initializer;
|
|
22930
|
-
while (init &&
|
|
23542
|
+
while (init && ts20.isParenthesizedExpression(init)) init = init.expression;
|
|
22931
23543
|
if (!init) return null;
|
|
22932
|
-
if (
|
|
23544
|
+
if (ts20.isStringLiteral(init) || ts20.isNoSubstitutionTemplateLiteral(init)) {
|
|
22933
23545
|
return init.text;
|
|
22934
23546
|
}
|
|
22935
23547
|
return evalStringArrayJoin(source);
|
|
22936
23548
|
}
|
|
22937
23549
|
function evalTemplateOfStringConsts(source, resolved) {
|
|
22938
|
-
const sf =
|
|
23550
|
+
const sf = ts20.createSourceFile(
|
|
22939
23551
|
"__const.ts",
|
|
22940
23552
|
`const __x = (${source});`,
|
|
22941
|
-
|
|
23553
|
+
ts20.ScriptTarget.Latest,
|
|
22942
23554
|
/*setParentNodes*/
|
|
22943
23555
|
false
|
|
22944
23556
|
);
|
|
22945
23557
|
const stmt = sf.statements[0];
|
|
22946
|
-
if (!stmt || !
|
|
23558
|
+
if (!stmt || !ts20.isVariableStatement(stmt)) return null;
|
|
22947
23559
|
let init = stmt.declarationList.declarations[0]?.initializer;
|
|
22948
|
-
while (init &&
|
|
22949
|
-
if (!init || !
|
|
23560
|
+
while (init && ts20.isParenthesizedExpression(init)) init = init.expression;
|
|
23561
|
+
if (!init || !ts20.isTemplateExpression(init)) return null;
|
|
22950
23562
|
let out = init.head.text;
|
|
22951
23563
|
for (const span of init.templateSpans) {
|
|
22952
|
-
if (!
|
|
23564
|
+
if (!ts20.isIdentifier(span.expression)) return null;
|
|
22953
23565
|
const value2 = resolved.get(span.expression.text);
|
|
22954
23566
|
if (value2 === void 0) return null;
|
|
22955
23567
|
out += value2 + span.literal.text;
|
|
@@ -22978,28 +23590,28 @@ function collectModuleStringConsts(constants) {
|
|
|
22978
23590
|
function lookupStaticRecordLiteral(objectName, key, constants) {
|
|
22979
23591
|
const constInfo = (constants ?? []).find((c) => c.name === objectName && c.isModule);
|
|
22980
23592
|
if (constInfo?.value === void 0) return null;
|
|
22981
|
-
const sf =
|
|
23593
|
+
const sf = ts20.createSourceFile(
|
|
22982
23594
|
"__rec.ts",
|
|
22983
23595
|
`(${constInfo.value})`,
|
|
22984
|
-
|
|
23596
|
+
ts20.ScriptTarget.Latest,
|
|
22985
23597
|
/*setParentNodes*/
|
|
22986
23598
|
true
|
|
22987
23599
|
);
|
|
22988
23600
|
if (sf.statements.length !== 1) return null;
|
|
22989
23601
|
const stmt = sf.statements[0];
|
|
22990
|
-
if (!
|
|
23602
|
+
if (!ts20.isExpressionStatement(stmt)) return null;
|
|
22991
23603
|
let parsed = stmt.expression;
|
|
22992
|
-
while (
|
|
22993
|
-
if (!
|
|
23604
|
+
while (ts20.isParenthesizedExpression(parsed)) parsed = parsed.expression;
|
|
23605
|
+
if (!ts20.isObjectLiteralExpression(parsed)) return null;
|
|
22994
23606
|
for (const prop of parsed.properties) {
|
|
22995
|
-
if (!
|
|
23607
|
+
if (!ts20.isPropertyAssignment(prop)) continue;
|
|
22996
23608
|
const name2 = prop.name;
|
|
22997
|
-
const propKey =
|
|
23609
|
+
const propKey = ts20.isIdentifier(name2) || ts20.isStringLiteral(name2) || ts20.isNoSubstitutionTemplateLiteral(name2) ? name2.text : null;
|
|
22998
23610
|
if (propKey !== key) continue;
|
|
22999
23611
|
let v = prop.initializer;
|
|
23000
|
-
while (
|
|
23001
|
-
if (
|
|
23002
|
-
if (
|
|
23612
|
+
while (ts20.isParenthesizedExpression(v)) v = v.expression;
|
|
23613
|
+
if (ts20.isNumericLiteral(v)) return { kind: "number", text: v.text };
|
|
23614
|
+
if (ts20.isStringLiteral(v) || ts20.isNoSubstitutionTemplateLiteral(v)) {
|
|
23003
23615
|
return { kind: "string", text: v.text };
|
|
23004
23616
|
}
|
|
23005
23617
|
return null;
|
|
@@ -23007,27 +23619,27 @@ function lookupStaticRecordLiteral(objectName, key, constants) {
|
|
|
23007
23619
|
return null;
|
|
23008
23620
|
}
|
|
23009
23621
|
function evalStringArrayJoin(source) {
|
|
23010
|
-
const sf =
|
|
23622
|
+
const sf = ts20.createSourceFile(
|
|
23011
23623
|
"__join.ts",
|
|
23012
23624
|
`const __x = (${source});`,
|
|
23013
|
-
|
|
23625
|
+
ts20.ScriptTarget.Latest,
|
|
23014
23626
|
/*setParentNodes*/
|
|
23015
23627
|
false
|
|
23016
23628
|
);
|
|
23017
23629
|
const stmt = sf.statements[0];
|
|
23018
|
-
if (!stmt || !
|
|
23630
|
+
if (!stmt || !ts20.isVariableStatement(stmt)) return null;
|
|
23019
23631
|
let node = stmt.declarationList.declarations[0]?.initializer;
|
|
23020
|
-
while (node &&
|
|
23021
|
-
if (!node || !
|
|
23632
|
+
while (node && ts20.isParenthesizedExpression(node)) node = node.expression;
|
|
23633
|
+
if (!node || !ts20.isCallExpression(node)) return null;
|
|
23022
23634
|
const callee = node.expression;
|
|
23023
|
-
if (!
|
|
23635
|
+
if (!ts20.isPropertyAccessExpression(callee)) return null;
|
|
23024
23636
|
if (callee.name.text !== "join") return null;
|
|
23025
23637
|
let recv = callee.expression;
|
|
23026
|
-
while (
|
|
23027
|
-
if (!
|
|
23638
|
+
while (ts20.isParenthesizedExpression(recv)) recv = recv.expression;
|
|
23639
|
+
if (!ts20.isArrayLiteralExpression(recv)) return null;
|
|
23028
23640
|
const parts = [];
|
|
23029
23641
|
for (const el of recv.elements) {
|
|
23030
|
-
if (
|
|
23642
|
+
if (ts20.isStringLiteral(el) || ts20.isNoSubstitutionTemplateLiteral(el)) {
|
|
23031
23643
|
parts.push(el.text);
|
|
23032
23644
|
} else {
|
|
23033
23645
|
return null;
|
|
@@ -23036,16 +23648,16 @@ function evalStringArrayJoin(source) {
|
|
|
23036
23648
|
let sep = ",";
|
|
23037
23649
|
if (node.arguments.length >= 1) {
|
|
23038
23650
|
const arg = node.arguments[0];
|
|
23039
|
-
if (
|
|
23651
|
+
if (ts20.isStringLiteral(arg) || ts20.isNoSubstitutionTemplateLiteral(arg)) sep = arg.text;
|
|
23040
23652
|
else return null;
|
|
23041
23653
|
}
|
|
23042
23654
|
return parts.join(sep);
|
|
23043
23655
|
}
|
|
23044
23656
|
function parseRecordIndexAccess(val, localConstants, propsParams, resolveKey) {
|
|
23045
|
-
if (!
|
|
23657
|
+
if (!ts20.isElementAccessExpression(val)) return null;
|
|
23046
23658
|
const obj = val.expression;
|
|
23047
23659
|
const arg = val.argumentExpression;
|
|
23048
|
-
if (!
|
|
23660
|
+
if (!ts20.isIdentifier(obj) || !ts20.isIdentifier(arg)) return null;
|
|
23049
23661
|
let indexPropName;
|
|
23050
23662
|
let defaultKey;
|
|
23051
23663
|
const resolved = resolveKey?.(arg.text);
|
|
@@ -23059,35 +23671,35 @@ function parseRecordIndexAccess(val, localConstants, propsParams, resolveKey) {
|
|
|
23059
23671
|
}
|
|
23060
23672
|
const constInfo = localConstants.find((c) => c.name === obj.text && c.isModule);
|
|
23061
23673
|
if (constInfo?.value === void 0) return null;
|
|
23062
|
-
const sf =
|
|
23674
|
+
const sf = ts20.createSourceFile(
|
|
23063
23675
|
"__rec.ts",
|
|
23064
23676
|
`(${constInfo.value})`,
|
|
23065
|
-
|
|
23677
|
+
ts20.ScriptTarget.Latest,
|
|
23066
23678
|
/* setParentNodes */
|
|
23067
23679
|
true
|
|
23068
23680
|
);
|
|
23069
23681
|
if (sf.statements.length !== 1) return null;
|
|
23070
23682
|
const stmt = sf.statements[0];
|
|
23071
|
-
if (!
|
|
23683
|
+
if (!ts20.isExpressionStatement(stmt)) return null;
|
|
23072
23684
|
let parsed = stmt.expression;
|
|
23073
|
-
while (
|
|
23074
|
-
if (!
|
|
23685
|
+
while (ts20.isParenthesizedExpression(parsed)) parsed = parsed.expression;
|
|
23686
|
+
if (!ts20.isObjectLiteralExpression(parsed)) return null;
|
|
23075
23687
|
const entries2 = [];
|
|
23076
23688
|
for (const prop of parsed.properties) {
|
|
23077
|
-
if (!
|
|
23689
|
+
if (!ts20.isPropertyAssignment(prop)) return null;
|
|
23078
23690
|
let key;
|
|
23079
|
-
if (
|
|
23691
|
+
if (ts20.isIdentifier(prop.name)) {
|
|
23080
23692
|
key = prop.name.text;
|
|
23081
|
-
} else if (
|
|
23693
|
+
} else if (ts20.isStringLiteral(prop.name) || ts20.isNoSubstitutionTemplateLiteral(prop.name)) {
|
|
23082
23694
|
key = prop.name.text;
|
|
23083
23695
|
} else {
|
|
23084
23696
|
return null;
|
|
23085
23697
|
}
|
|
23086
23698
|
let v = prop.initializer;
|
|
23087
|
-
while (
|
|
23088
|
-
if (
|
|
23699
|
+
while (ts20.isParenthesizedExpression(v)) v = v.expression;
|
|
23700
|
+
if (ts20.isNumericLiteral(v)) {
|
|
23089
23701
|
entries2.push({ key, value: { kind: "number", text: v.text } });
|
|
23090
|
-
} else if (
|
|
23702
|
+
} else if (ts20.isStringLiteral(v) || ts20.isNoSubstitutionTemplateLiteral(v)) {
|
|
23091
23703
|
entries2.push({ key, value: { kind: "string", text: v.text } });
|
|
23092
23704
|
} else {
|
|
23093
23705
|
return null;
|
|
@@ -23891,7 +24503,7 @@ var init_compiler = __esm({
|
|
|
23891
24503
|
});
|
|
23892
24504
|
|
|
23893
24505
|
// ../jsx/src/shared-program.ts
|
|
23894
|
-
import
|
|
24506
|
+
import ts21 from "typescript";
|
|
23895
24507
|
import path5 from "node:path";
|
|
23896
24508
|
function commonParent(paths) {
|
|
23897
24509
|
if (paths.length === 0) return process.cwd();
|
|
@@ -23909,10 +24521,10 @@ function commonParent(paths) {
|
|
|
23909
24521
|
function createProgramForCorpus(files2, options2 = {}) {
|
|
23910
24522
|
const baseUrl = options2.baseUrl ?? commonParent(files2);
|
|
23911
24523
|
const compilerOptions = {
|
|
23912
|
-
target:
|
|
23913
|
-
module:
|
|
23914
|
-
moduleResolution:
|
|
23915
|
-
jsx:
|
|
24524
|
+
target: ts21.ScriptTarget.Latest,
|
|
24525
|
+
module: ts21.ModuleKind.ESNext,
|
|
24526
|
+
moduleResolution: ts21.ModuleResolutionKind.Bundler,
|
|
24527
|
+
jsx: ts21.JsxEmit.ReactJSX,
|
|
23916
24528
|
strict: true,
|
|
23917
24529
|
skipLibCheck: true,
|
|
23918
24530
|
noEmit: true,
|
|
@@ -23922,7 +24534,7 @@ function createProgramForCorpus(files2, options2 = {}) {
|
|
|
23922
24534
|
...options2.compilerOptions
|
|
23923
24535
|
};
|
|
23924
24536
|
const absolute = files2.map((f) => path5.resolve(f));
|
|
23925
|
-
return
|
|
24537
|
+
return ts21.createProgram(absolute, compilerOptions, void 0, options2.oldProgram);
|
|
23926
24538
|
}
|
|
23927
24539
|
var init_shared_program = __esm({
|
|
23928
24540
|
"../jsx/src/shared-program.ts"() {
|
|
@@ -24533,6 +25145,7 @@ function collectLoopBoundNames(ir) {
|
|
|
24533
25145
|
if (node.index) names.add(node.index);
|
|
24534
25146
|
for (const binding of node.paramBindings ?? []) names.add(binding.name);
|
|
24535
25147
|
if (node.filterPredicate) names.add(node.filterPredicate.param);
|
|
25148
|
+
for (const name2 of node.preamble?.declaredNames ?? []) names.add(name2);
|
|
24536
25149
|
for (const child of node.children) visit3(child);
|
|
24537
25150
|
if (node.childComponent) {
|
|
24538
25151
|
for (const child of node.childComponent.children) visit3(child);
|
|
@@ -24991,7 +25604,7 @@ var init_dangerous_inner_html = __esm({
|
|
|
24991
25604
|
});
|
|
24992
25605
|
|
|
24993
25606
|
// ../jsx/src/combine-client-js.ts
|
|
24994
|
-
import
|
|
25607
|
+
import ts22 from "typescript";
|
|
24995
25608
|
function combineParentChildClientJs(files2) {
|
|
24996
25609
|
const result2 = /* @__PURE__ */ new Map();
|
|
24997
25610
|
const lookup = /* @__PURE__ */ new Map();
|
|
@@ -25048,17 +25661,17 @@ function combineParentChildClientJs(files2) {
|
|
|
25048
25661
|
return result2;
|
|
25049
25662
|
}
|
|
25050
25663
|
function parseAndMerge(content2, importsBySource, otherImports, codeSections) {
|
|
25051
|
-
const sourceFile =
|
|
25664
|
+
const sourceFile = ts22.createSourceFile(
|
|
25052
25665
|
"combine.js",
|
|
25053
25666
|
content2,
|
|
25054
|
-
|
|
25667
|
+
ts22.ScriptTarget.Latest,
|
|
25055
25668
|
/*setParentNodes*/
|
|
25056
25669
|
false,
|
|
25057
|
-
|
|
25670
|
+
ts22.ScriptKind.JS
|
|
25058
25671
|
);
|
|
25059
25672
|
const importSpans = [];
|
|
25060
25673
|
for (const stmt of sourceFile.statements) {
|
|
25061
|
-
if (!
|
|
25674
|
+
if (!ts22.isImportDeclaration(stmt)) continue;
|
|
25062
25675
|
const start2 = stmt.getStart(sourceFile);
|
|
25063
25676
|
const end2 = stmt.getEnd();
|
|
25064
25677
|
importSpans.push([start2, end2]);
|
|
@@ -25066,8 +25679,8 @@ function parseAndMerge(content2, importsBySource, otherImports, codeSections) {
|
|
|
25066
25679
|
if (stmtText.includes("@bf-child:")) continue;
|
|
25067
25680
|
const clause = stmt.importClause;
|
|
25068
25681
|
const bindings = clause?.namedBindings;
|
|
25069
|
-
const specifier =
|
|
25070
|
-
if (clause && !clause.name && bindings &&
|
|
25682
|
+
const specifier = ts22.isStringLiteral(stmt.moduleSpecifier) ? stmt.moduleSpecifier.text : "";
|
|
25683
|
+
if (clause && !clause.name && bindings && ts22.isNamedImports(bindings)) {
|
|
25071
25684
|
if (!importsBySource.has(specifier)) {
|
|
25072
25685
|
importsBySource.set(specifier, /* @__PURE__ */ new Set());
|
|
25073
25686
|
}
|
|
@@ -25261,7 +25874,7 @@ var init_loop_destructure = __esm({
|
|
|
25261
25874
|
});
|
|
25262
25875
|
|
|
25263
25876
|
// ../jsx/src/debug.ts
|
|
25264
|
-
import
|
|
25877
|
+
import ts23 from "typescript";
|
|
25265
25878
|
function buildComponentGraph(source, filePath, componentName) {
|
|
25266
25879
|
const ctx2 = analyzeComponent(source, filePath, componentName);
|
|
25267
25880
|
if (!ctx2.jsxReturn) {
|
|
@@ -26473,18 +27086,18 @@ function truncateExpr(expr, max = 40) {
|
|
|
26473
27086
|
function exprReadsPropMember(expr, propsObjectName) {
|
|
26474
27087
|
let sf;
|
|
26475
27088
|
try {
|
|
26476
|
-
sf =
|
|
27089
|
+
sf = ts23.createSourceFile("__attr.tsx", `(${expr})`, ts23.ScriptTarget.Latest, true, ts23.ScriptKind.TSX);
|
|
26477
27090
|
} catch {
|
|
26478
27091
|
return false;
|
|
26479
27092
|
}
|
|
26480
27093
|
let found = false;
|
|
26481
27094
|
const visit3 = (n) => {
|
|
26482
27095
|
if (found) return;
|
|
26483
|
-
if (
|
|
27096
|
+
if (ts23.isPropertyAccessExpression(n) && ts23.isIdentifier(n.expression) && n.expression.text === propsObjectName && n.name.text !== "children") {
|
|
26484
27097
|
found = true;
|
|
26485
27098
|
return;
|
|
26486
27099
|
}
|
|
26487
|
-
|
|
27100
|
+
ts23.forEachChild(n, visit3);
|
|
26488
27101
|
};
|
|
26489
27102
|
visit3(sf);
|
|
26490
27103
|
return found;
|
|
@@ -26561,7 +27174,7 @@ var init_debug = __esm({
|
|
|
26561
27174
|
});
|
|
26562
27175
|
|
|
26563
27176
|
// ../jsx/src/profiler.ts
|
|
26564
|
-
import
|
|
27177
|
+
import ts24 from "typescript";
|
|
26565
27178
|
function buildStaticBudget(source, filePath, componentName, options2 = {}) {
|
|
26566
27179
|
const threshold = options2.fanOutThreshold ?? DEFAULT_FANOUT_THRESHOLD;
|
|
26567
27180
|
const program = createProgramForFile(source, filePath)?.program;
|
|
@@ -26816,14 +27429,14 @@ function joinProfilerEvents(events, index) {
|
|
|
26816
27429
|
return { joined, unattributed, diagnostics };
|
|
26817
27430
|
}
|
|
26818
27431
|
function findUninstrumentedEffects(source, filePath, instrumentedLines) {
|
|
26819
|
-
const sf =
|
|
27432
|
+
const sf = ts24.createSourceFile(filePath, source, ts24.ScriptTarget.Latest, true, ts24.ScriptKind.TSX);
|
|
26820
27433
|
const out = [];
|
|
26821
27434
|
const visit3 = (node) => {
|
|
26822
|
-
if (
|
|
27435
|
+
if (ts24.isCallExpression(node) && ts24.isIdentifier(node.expression) && node.expression.text === "createEffect") {
|
|
26823
27436
|
const line = sf.getLineAndCharacterOfPosition(node.getStart(sf)).line + 1;
|
|
26824
27437
|
if (!instrumentedLines.has(line)) out.push({ file: filePath, line });
|
|
26825
27438
|
}
|
|
26826
|
-
|
|
27439
|
+
ts24.forEachChild(node, visit3);
|
|
26827
27440
|
};
|
|
26828
27441
|
visit3(sf);
|
|
26829
27442
|
out.sort((a, b) => a.line - b.line);
|
|
@@ -27109,19 +27722,19 @@ function assessBatchSafety(args2) {
|
|
|
27109
27722
|
const signalGetters = new Set(args2.graph.signals.map((s) => s.name));
|
|
27110
27723
|
let sf;
|
|
27111
27724
|
try {
|
|
27112
|
-
sf =
|
|
27725
|
+
sf = ts24.createSourceFile("__h.ts", `const __h = ${args2.handler}`, ts24.ScriptTarget.Latest, true);
|
|
27113
27726
|
} catch {
|
|
27114
27727
|
return "unverified";
|
|
27115
27728
|
}
|
|
27116
27729
|
const calls = [];
|
|
27117
27730
|
const visit3 = (node) => {
|
|
27118
|
-
if (
|
|
27731
|
+
if (ts24.isCallExpression(node) && ts24.isIdentifier(node.expression)) {
|
|
27119
27732
|
const name2 = node.expression.text;
|
|
27120
27733
|
if (setters.has(name2)) calls.push({ pos: node.getStart(sf), kind: "write" });
|
|
27121
27734
|
else if (D.has(name2) && node.arguments.length === 0) calls.push({ pos: node.getStart(sf), kind: "memoRead" });
|
|
27122
27735
|
else if (!signalGetters.has(name2) && !memoNames.has(name2)) calls.push({ pos: node.getStart(sf), kind: "risky" });
|
|
27123
27736
|
}
|
|
27124
|
-
|
|
27737
|
+
ts24.forEachChild(node, visit3);
|
|
27125
27738
|
};
|
|
27126
27739
|
visit3(sf);
|
|
27127
27740
|
calls.sort((a, b) => a.pos - b.pos);
|
|
@@ -27825,6 +28438,7 @@ __export(src_exports, {
|
|
|
27825
28438
|
dangerousInnerHtmlDiagnostic: () => dangerousInnerHtmlDiagnostic,
|
|
27826
28439
|
dangerousInnerHtmlMetacharViolation: () => dangerousInnerHtmlMetacharViolation,
|
|
27827
28440
|
decideClientOnlyElision: () => decideClientOnlyElision,
|
|
28441
|
+
derivesScopeFromSlot: () => derivesScopeFromSlot,
|
|
27828
28442
|
describeFallback: () => describeFallback,
|
|
27829
28443
|
diffProfiles: () => diffProfiles,
|
|
27830
28444
|
diffStaticBudget: () => diffStaticBudget,
|
|
@@ -27948,6 +28562,7 @@ var init_src2 = __esm({
|
|
|
27948
28562
|
init_template_imports();
|
|
27949
28563
|
init_parsed_expr_emitter();
|
|
27950
28564
|
init_loop_bound_names();
|
|
28565
|
+
init_child_scope();
|
|
27951
28566
|
init_signal_init_eval();
|
|
27952
28567
|
init_static_literal();
|
|
27953
28568
|
init_env_signal();
|
|
@@ -28041,7 +28656,7 @@ var init_runtime = __esm({
|
|
|
28041
28656
|
|
|
28042
28657
|
// src/lib/resolve-imports.ts
|
|
28043
28658
|
import { dirname as dirname2, resolve as resolve2 } from "node:path";
|
|
28044
|
-
import
|
|
28659
|
+
import ts25 from "typescript";
|
|
28045
28660
|
function shapeFromDecl(decl) {
|
|
28046
28661
|
const clause = decl.importClause;
|
|
28047
28662
|
if (!clause) return null;
|
|
@@ -28051,7 +28666,7 @@ function shapeFromDecl(decl) {
|
|
|
28051
28666
|
}
|
|
28052
28667
|
const bindings = clause.namedBindings;
|
|
28053
28668
|
if (bindings) {
|
|
28054
|
-
if (
|
|
28669
|
+
if (ts25.isNamespaceImport(bindings)) {
|
|
28055
28670
|
shape.namespace = bindings.name.text;
|
|
28056
28671
|
} else {
|
|
28057
28672
|
for (const el of bindings.elements) {
|
|
@@ -28068,50 +28683,50 @@ function collectExportInfo(source) {
|
|
|
28068
28683
|
const otherValueExports = /* @__PURE__ */ new Set();
|
|
28069
28684
|
const reExportedNames = /* @__PURE__ */ new Set();
|
|
28070
28685
|
let hasStarReExport = false;
|
|
28071
|
-
const sourceFile =
|
|
28686
|
+
const sourceFile = ts25.createSourceFile(
|
|
28072
28687
|
"mod.ts",
|
|
28073
28688
|
source,
|
|
28074
|
-
|
|
28689
|
+
ts25.ScriptTarget.Latest,
|
|
28075
28690
|
/*setParents*/
|
|
28076
28691
|
false,
|
|
28077
|
-
|
|
28692
|
+
ts25.ScriptKind.TS
|
|
28078
28693
|
);
|
|
28079
28694
|
function hasExport(node) {
|
|
28080
|
-
if (!
|
|
28081
|
-
const mods =
|
|
28082
|
-
return mods?.some((m) => m.kind ===
|
|
28695
|
+
if (!ts25.canHaveModifiers(node)) return false;
|
|
28696
|
+
const mods = ts25.getModifiers(node);
|
|
28697
|
+
return mods?.some((m) => m.kind === ts25.SyntaxKind.ExportKeyword) ?? false;
|
|
28083
28698
|
}
|
|
28084
28699
|
function isAmbient(node) {
|
|
28085
|
-
if (!
|
|
28086
|
-
const mods =
|
|
28087
|
-
return mods?.some((m) => m.kind ===
|
|
28700
|
+
if (!ts25.canHaveModifiers(node)) return false;
|
|
28701
|
+
const mods = ts25.getModifiers(node);
|
|
28702
|
+
return mods?.some((m) => m.kind === ts25.SyntaxKind.DeclareKeyword) ?? false;
|
|
28088
28703
|
}
|
|
28089
28704
|
function collectFromBindingName(name2) {
|
|
28090
|
-
if (
|
|
28705
|
+
if (ts25.isIdentifier(name2)) {
|
|
28091
28706
|
localValueExports.add(name2.text);
|
|
28092
28707
|
return;
|
|
28093
28708
|
}
|
|
28094
28709
|
for (const el of name2.elements) {
|
|
28095
|
-
if (
|
|
28710
|
+
if (ts25.isBindingElement(el)) collectFromBindingName(el.name);
|
|
28096
28711
|
}
|
|
28097
28712
|
}
|
|
28098
28713
|
for (const stmt of sourceFile.statements) {
|
|
28099
|
-
if (
|
|
28714
|
+
if (ts25.isVariableStatement(stmt) && hasExport(stmt)) {
|
|
28100
28715
|
for (const d of stmt.declarationList.declarations) {
|
|
28101
28716
|
collectFromBindingName(d.name);
|
|
28102
28717
|
}
|
|
28103
|
-
} else if (
|
|
28718
|
+
} else if (ts25.isFunctionDeclaration(stmt) && hasExport(stmt) && stmt.name) {
|
|
28104
28719
|
localValueExports.add(stmt.name.text);
|
|
28105
|
-
} else if (
|
|
28720
|
+
} else if (ts25.isClassDeclaration(stmt) && hasExport(stmt) && stmt.name) {
|
|
28106
28721
|
localValueExports.add(stmt.name.text);
|
|
28107
|
-
} else if (
|
|
28722
|
+
} else if (ts25.isEnumDeclaration(stmt) && hasExport(stmt) && stmt.name) {
|
|
28108
28723
|
otherValueExports.add(stmt.name.text);
|
|
28109
|
-
} else if (
|
|
28724
|
+
} else if (ts25.isModuleDeclaration(stmt) && hasExport(stmt) && ts25.isIdentifier(stmt.name)) {
|
|
28110
28725
|
if (!isAmbient(stmt)) otherValueExports.add(stmt.name.text);
|
|
28111
|
-
} else if (
|
|
28726
|
+
} else if (ts25.isExportDeclaration(stmt)) {
|
|
28112
28727
|
if (stmt.isTypeOnly) continue;
|
|
28113
28728
|
if (!stmt.moduleSpecifier) {
|
|
28114
|
-
if (stmt.exportClause &&
|
|
28729
|
+
if (stmt.exportClause && ts25.isNamedExports(stmt.exportClause)) {
|
|
28115
28730
|
for (const el of stmt.exportClause.elements) {
|
|
28116
28731
|
if (el.isTypeOnly) continue;
|
|
28117
28732
|
localValueExports.add(el.name.text);
|
|
@@ -28119,9 +28734,9 @@ function collectExportInfo(source) {
|
|
|
28119
28734
|
}
|
|
28120
28735
|
} else if (!stmt.exportClause) {
|
|
28121
28736
|
hasStarReExport = true;
|
|
28122
|
-
} else if (
|
|
28737
|
+
} else if (ts25.isNamespaceExport(stmt.exportClause)) {
|
|
28123
28738
|
reExportedNames.add(stmt.exportClause.name.text);
|
|
28124
|
-
} else if (
|
|
28739
|
+
} else if (ts25.isNamedExports(stmt.exportClause)) {
|
|
28125
28740
|
for (const el of stmt.exportClause.elements) {
|
|
28126
28741
|
if (el.isTypeOnly) continue;
|
|
28127
28742
|
reExportedNames.add(el.name.text);
|
|
@@ -28132,16 +28747,16 @@ function collectExportInfo(source) {
|
|
|
28132
28747
|
return { localValueExports, otherValueExports, reExportedNames, hasStarReExport };
|
|
28133
28748
|
}
|
|
28134
28749
|
function hasUseClientDirective(source) {
|
|
28135
|
-
const sourceFile =
|
|
28750
|
+
const sourceFile = ts25.createSourceFile(
|
|
28136
28751
|
"check.tsx",
|
|
28137
28752
|
source,
|
|
28138
|
-
|
|
28753
|
+
ts25.ScriptTarget.Latest,
|
|
28139
28754
|
/*setParents*/
|
|
28140
28755
|
false,
|
|
28141
|
-
|
|
28756
|
+
ts25.ScriptKind.TSX
|
|
28142
28757
|
);
|
|
28143
28758
|
for (const stmt of sourceFile.statements) {
|
|
28144
|
-
if (!
|
|
28759
|
+
if (!ts25.isExpressionStatement(stmt) || !ts25.isStringLiteral(stmt.expression)) {
|
|
28145
28760
|
return false;
|
|
28146
28761
|
}
|
|
28147
28762
|
if (stmt.expression.text === "use client") return true;
|
|
@@ -28150,53 +28765,53 @@ function hasUseClientDirective(source) {
|
|
|
28150
28765
|
}
|
|
28151
28766
|
function collectTopLevelBindings(source) {
|
|
28152
28767
|
const names = /* @__PURE__ */ new Set();
|
|
28153
|
-
const sourceFile =
|
|
28768
|
+
const sourceFile = ts25.createSourceFile(
|
|
28154
28769
|
"bundle.ts",
|
|
28155
28770
|
source,
|
|
28156
|
-
|
|
28771
|
+
ts25.ScriptTarget.Latest,
|
|
28157
28772
|
/*setParents*/
|
|
28158
28773
|
false,
|
|
28159
|
-
|
|
28774
|
+
ts25.ScriptKind.TS
|
|
28160
28775
|
);
|
|
28161
28776
|
function collectFromBindingName(name2) {
|
|
28162
|
-
if (
|
|
28777
|
+
if (ts25.isIdentifier(name2)) {
|
|
28163
28778
|
names.add(name2.text);
|
|
28164
28779
|
return;
|
|
28165
28780
|
}
|
|
28166
28781
|
for (const el of name2.elements) {
|
|
28167
|
-
if (
|
|
28782
|
+
if (ts25.isBindingElement(el)) collectFromBindingName(el.name);
|
|
28168
28783
|
}
|
|
28169
28784
|
}
|
|
28170
28785
|
for (const stmt of sourceFile.statements) {
|
|
28171
|
-
if (
|
|
28786
|
+
if (ts25.isVariableStatement(stmt)) {
|
|
28172
28787
|
for (const d of stmt.declarationList.declarations) {
|
|
28173
28788
|
collectFromBindingName(d.name);
|
|
28174
28789
|
}
|
|
28175
|
-
} else if (
|
|
28790
|
+
} else if (ts25.isFunctionDeclaration(stmt) && stmt.name) {
|
|
28176
28791
|
names.add(stmt.name.text);
|
|
28177
|
-
} else if (
|
|
28792
|
+
} else if (ts25.isClassDeclaration(stmt) && stmt.name) {
|
|
28178
28793
|
names.add(stmt.name.text);
|
|
28179
28794
|
}
|
|
28180
28795
|
}
|
|
28181
28796
|
return names;
|
|
28182
28797
|
}
|
|
28183
28798
|
function stripImportsAndExports(body2) {
|
|
28184
|
-
const sourceFile =
|
|
28799
|
+
const sourceFile = ts25.createSourceFile(
|
|
28185
28800
|
"body.ts",
|
|
28186
28801
|
body2,
|
|
28187
|
-
|
|
28802
|
+
ts25.ScriptTarget.Latest,
|
|
28188
28803
|
/*setParents*/
|
|
28189
28804
|
false,
|
|
28190
|
-
|
|
28805
|
+
ts25.ScriptKind.TS
|
|
28191
28806
|
);
|
|
28192
28807
|
const spans = [];
|
|
28193
28808
|
const hoistedImports = [];
|
|
28194
28809
|
for (const stmt of sourceFile.statements) {
|
|
28195
|
-
if (
|
|
28810
|
+
if (ts25.isImportDeclaration(stmt)) {
|
|
28196
28811
|
const start2 = stmt.getStart(sourceFile);
|
|
28197
28812
|
const end2 = stmt.getEnd();
|
|
28198
28813
|
const specifier = stmt.moduleSpecifier;
|
|
28199
|
-
if (
|
|
28814
|
+
if (ts25.isStringLiteral(specifier)) {
|
|
28200
28815
|
const path25 = specifier.text;
|
|
28201
28816
|
const isRelative = path25.startsWith("./") || path25.startsWith("../");
|
|
28202
28817
|
if (!isRelative) {
|
|
@@ -28206,24 +28821,24 @@ function stripImportsAndExports(body2) {
|
|
|
28206
28821
|
spans.push([start2, end2]);
|
|
28207
28822
|
continue;
|
|
28208
28823
|
}
|
|
28209
|
-
if (
|
|
28824
|
+
if (ts25.isExportDeclaration(stmt)) {
|
|
28210
28825
|
spans.push([stmt.getStart(sourceFile), stmt.getEnd()]);
|
|
28211
28826
|
continue;
|
|
28212
28827
|
}
|
|
28213
|
-
if (
|
|
28214
|
-
const exportKw = stmt.getChildren(sourceFile).find((c) => c.kind ===
|
|
28215
|
-
const defaultKw = stmt.getChildren(sourceFile).find((c) => c.kind ===
|
|
28216
|
-
const equalsKw = stmt.getChildren(sourceFile).find((c) => c.kind ===
|
|
28828
|
+
if (ts25.isExportAssignment(stmt)) {
|
|
28829
|
+
const exportKw = stmt.getChildren(sourceFile).find((c) => c.kind === ts25.SyntaxKind.ExportKeyword);
|
|
28830
|
+
const defaultKw = stmt.getChildren(sourceFile).find((c) => c.kind === ts25.SyntaxKind.DefaultKeyword);
|
|
28831
|
+
const equalsKw = stmt.getChildren(sourceFile).find((c) => c.kind === ts25.SyntaxKind.EqualsToken);
|
|
28217
28832
|
const start2 = exportKw?.getStart(sourceFile) ?? stmt.getStart(sourceFile);
|
|
28218
28833
|
const end2 = (defaultKw ?? equalsKw)?.getEnd() ?? exportKw?.getEnd() ?? stmt.getStart(sourceFile);
|
|
28219
28834
|
if (end2 > start2) spans.push([start2, end2]);
|
|
28220
28835
|
continue;
|
|
28221
28836
|
}
|
|
28222
|
-
if (
|
|
28223
|
-
const mods =
|
|
28837
|
+
if (ts25.canHaveModifiers(stmt)) {
|
|
28838
|
+
const mods = ts25.getModifiers(stmt);
|
|
28224
28839
|
if (!mods) continue;
|
|
28225
28840
|
for (const mod of mods) {
|
|
28226
|
-
if (mod.kind ===
|
|
28841
|
+
if (mod.kind === ts25.SyntaxKind.ExportKeyword) {
|
|
28227
28842
|
const start2 = mod.getStart(sourceFile);
|
|
28228
28843
|
let end2 = mod.getEnd();
|
|
28229
28844
|
while (end2 < body2.length && /\s/.test(body2[end2])) end2++;
|
|
@@ -28363,23 +28978,23 @@ function detectStrippedReferences(bundleSource, stripped) {
|
|
|
28363
28978
|
if (stripped.length === 0) return [];
|
|
28364
28979
|
let sf;
|
|
28365
28980
|
try {
|
|
28366
|
-
sf =
|
|
28981
|
+
sf = ts25.createSourceFile(
|
|
28367
28982
|
"bundle.js",
|
|
28368
28983
|
bundleSource,
|
|
28369
|
-
|
|
28984
|
+
ts25.ScriptTarget.Latest,
|
|
28370
28985
|
/*setParents*/
|
|
28371
28986
|
true,
|
|
28372
|
-
|
|
28987
|
+
ts25.ScriptKind.JS
|
|
28373
28988
|
);
|
|
28374
28989
|
} catch {
|
|
28375
28990
|
return [];
|
|
28376
28991
|
}
|
|
28377
28992
|
const firstReference = /* @__PURE__ */ new Map();
|
|
28378
28993
|
function visit3(node) {
|
|
28379
|
-
if (
|
|
28994
|
+
if (ts25.isIdentifier(node) && isValueReferenceIdentifier(node)) {
|
|
28380
28995
|
if (!firstReference.has(node.text)) firstReference.set(node.text, node);
|
|
28381
28996
|
}
|
|
28382
|
-
|
|
28997
|
+
ts25.forEachChild(node, visit3);
|
|
28383
28998
|
}
|
|
28384
28999
|
visit3(sf);
|
|
28385
29000
|
const errors = [];
|
|
@@ -28409,18 +29024,18 @@ function detectStrippedReferences(bundleSource, stripped) {
|
|
|
28409
29024
|
return errors;
|
|
28410
29025
|
}
|
|
28411
29026
|
async function walkAndCollect(content2, searchDirs, modules2, visiting, loggingPath, stripped, stubDeps, nextId) {
|
|
28412
|
-
const sourceFile =
|
|
29027
|
+
const sourceFile = ts25.createSourceFile(
|
|
28413
29028
|
"walk.js",
|
|
28414
29029
|
content2,
|
|
28415
|
-
|
|
29030
|
+
ts25.ScriptTarget.Latest,
|
|
28416
29031
|
/*setParents*/
|
|
28417
29032
|
false,
|
|
28418
|
-
|
|
29033
|
+
ts25.ScriptKind.JS
|
|
28419
29034
|
);
|
|
28420
29035
|
const sites = [];
|
|
28421
29036
|
for (const stmt of sourceFile.statements) {
|
|
28422
|
-
if (!
|
|
28423
|
-
if (!
|
|
29037
|
+
if (!ts25.isImportDeclaration(stmt)) continue;
|
|
29038
|
+
if (!ts25.isStringLiteral(stmt.moduleSpecifier)) continue;
|
|
28424
29039
|
const spec = stmt.moduleSpecifier.text;
|
|
28425
29040
|
if (!spec.startsWith("./") && !spec.startsWith("../")) continue;
|
|
28426
29041
|
const start2 = stmt.getStart(sourceFile);
|
|
@@ -28874,7 +29489,7 @@ var init_assets_ignore = __esm({
|
|
|
28874
29489
|
});
|
|
28875
29490
|
|
|
28876
29491
|
// src/lib/runtime-treeshake.ts
|
|
28877
|
-
import
|
|
29492
|
+
import ts26 from "typescript";
|
|
28878
29493
|
import { basename, dirname as dirname3 } from "node:path";
|
|
28879
29494
|
import { build as esbuildBuild } from "esbuild";
|
|
28880
29495
|
function isBarefootClientSpecifier(spec) {
|
|
@@ -28891,13 +29506,13 @@ function collectUsedRuntimeExports(code, sourceLabel = "<input>") {
|
|
|
28891
29506
|
if (!code.includes("@barefootjs/client") && !code.includes("barefoot.js")) return result2;
|
|
28892
29507
|
let sourceFile;
|
|
28893
29508
|
try {
|
|
28894
|
-
sourceFile =
|
|
29509
|
+
sourceFile = ts26.createSourceFile(
|
|
28895
29510
|
sourceLabel,
|
|
28896
29511
|
code,
|
|
28897
|
-
|
|
29512
|
+
ts26.ScriptTarget.Latest,
|
|
28898
29513
|
/*setParentNodes*/
|
|
28899
29514
|
false,
|
|
28900
|
-
|
|
29515
|
+
ts26.ScriptKind.JS
|
|
28901
29516
|
);
|
|
28902
29517
|
} catch (err) {
|
|
28903
29518
|
result2.unsafe = true;
|
|
@@ -28905,13 +29520,13 @@ function collectUsedRuntimeExports(code, sourceLabel = "<input>") {
|
|
|
28905
29520
|
return result2;
|
|
28906
29521
|
}
|
|
28907
29522
|
const visit3 = (node) => {
|
|
28908
|
-
if (
|
|
29523
|
+
if (ts26.isImportDeclaration(node)) {
|
|
28909
29524
|
const spec = node.moduleSpecifier;
|
|
28910
|
-
if (
|
|
29525
|
+
if (ts26.isStringLiteral(spec) && isEmittedRuntimeSpecifier(spec.text)) {
|
|
28911
29526
|
const clause = node.importClause;
|
|
28912
29527
|
if (!clause) {
|
|
28913
29528
|
} else if (clause.isTypeOnly) {
|
|
28914
|
-
} else if (clause.namedBindings &&
|
|
29529
|
+
} else if (clause.namedBindings && ts26.isNamedImports(clause.namedBindings)) {
|
|
28915
29530
|
for (const el of clause.namedBindings.elements) {
|
|
28916
29531
|
if (el.isTypeOnly) continue;
|
|
28917
29532
|
const imported = (el.propertyName ?? el.name).text;
|
|
@@ -28921,7 +29536,7 @@ function collectUsedRuntimeExports(code, sourceLabel = "<input>") {
|
|
|
28921
29536
|
result2.unsafe = true;
|
|
28922
29537
|
result2.reasons.push(`default import of "${spec.text}" in ${sourceLabel}`);
|
|
28923
29538
|
}
|
|
28924
|
-
} else if (clause.namedBindings &&
|
|
29539
|
+
} else if (clause.namedBindings && ts26.isNamespaceImport(clause.namedBindings)) {
|
|
28925
29540
|
result2.unsafe = true;
|
|
28926
29541
|
result2.reasons.push(`namespace import (* as ${clause.namedBindings.name.text}) of "${spec.text}" in ${sourceLabel}`);
|
|
28927
29542
|
} else if (clause.name) {
|
|
@@ -28929,14 +29544,14 @@ function collectUsedRuntimeExports(code, sourceLabel = "<input>") {
|
|
|
28929
29544
|
result2.reasons.push(`default import of "${spec.text}" in ${sourceLabel}`);
|
|
28930
29545
|
}
|
|
28931
29546
|
}
|
|
28932
|
-
} else if (
|
|
29547
|
+
} else if (ts26.isCallExpression(node) && node.expression.kind === ts26.SyntaxKind.ImportKeyword) {
|
|
28933
29548
|
const arg = node.arguments[0];
|
|
28934
|
-
if (arg &&
|
|
29549
|
+
if (arg && ts26.isStringLiteral(arg) && isEmittedRuntimeSpecifier(arg.text)) {
|
|
28935
29550
|
result2.unsafe = true;
|
|
28936
29551
|
result2.reasons.push(`dynamic import("${arg.text}") in ${sourceLabel}`);
|
|
28937
29552
|
}
|
|
28938
29553
|
}
|
|
28939
|
-
|
|
29554
|
+
ts26.forEachChild(node, visit3);
|
|
28940
29555
|
};
|
|
28941
29556
|
visit3(sourceFile);
|
|
28942
29557
|
return result2;
|
|
@@ -29009,7 +29624,7 @@ var init_runtime_treeshake = __esm({
|
|
|
29009
29624
|
});
|
|
29010
29625
|
|
|
29011
29626
|
// src/lib/build.ts
|
|
29012
|
-
import
|
|
29627
|
+
import ts27 from "typescript";
|
|
29013
29628
|
import { mkdir, readdir, stat, unlink } from "node:fs/promises";
|
|
29014
29629
|
import { resolve as resolve6, basename as basename2, relative as relative2, dirname as dirname4, isAbsolute as isAbsolute2 } from "node:path";
|
|
29015
29630
|
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
@@ -29657,7 +30272,7 @@ async function build(config, options2 = {}) {
|
|
|
29657
30272
|
};
|
|
29658
30273
|
}
|
|
29659
30274
|
function extractBareImports(code) {
|
|
29660
|
-
const { importedFiles } =
|
|
30275
|
+
const { importedFiles } = ts27.preProcessFile(code, true, true);
|
|
29661
30276
|
const specifiers = /* @__PURE__ */ new Set();
|
|
29662
30277
|
for (const { fileName } of importedFiles) {
|
|
29663
30278
|
if (!fileName.startsWith(".") && !fileName.startsWith("/") && !fileName.includes("://")) {
|
|
@@ -29724,16 +30339,16 @@ function effectiveOutName(tplPath, entryBaseNoExt) {
|
|
|
29724
30339
|
}
|
|
29725
30340
|
function topLevelImportLines(content2) {
|
|
29726
30341
|
const lines = /* @__PURE__ */ new Set();
|
|
29727
|
-
const sourceFile =
|
|
30342
|
+
const sourceFile = ts27.createSourceFile(
|
|
29728
30343
|
"merge.js",
|
|
29729
30344
|
content2,
|
|
29730
|
-
|
|
30345
|
+
ts27.ScriptTarget.Latest,
|
|
29731
30346
|
/*setParentNodes*/
|
|
29732
30347
|
true,
|
|
29733
|
-
|
|
30348
|
+
ts27.ScriptKind.JS
|
|
29734
30349
|
);
|
|
29735
30350
|
for (const stmt of sourceFile.statements) {
|
|
29736
|
-
if (
|
|
30351
|
+
if (ts27.isImportDeclaration(stmt)) {
|
|
29737
30352
|
const { line } = sourceFile.getLineAndCharacterOfPosition(stmt.getStart(sourceFile));
|
|
29738
30353
|
lines.add(line);
|
|
29739
30354
|
}
|
|
@@ -29742,29 +30357,29 @@ function topLevelImportLines(content2) {
|
|
|
29742
30357
|
}
|
|
29743
30358
|
function rewriteBarefootClientSpecifiers(content2, rel) {
|
|
29744
30359
|
if (!content2.includes("@barefootjs/client")) return content2;
|
|
29745
|
-
const sourceFile =
|
|
30360
|
+
const sourceFile = ts27.createSourceFile(
|
|
29746
30361
|
"client.js",
|
|
29747
30362
|
content2,
|
|
29748
|
-
|
|
30363
|
+
ts27.ScriptTarget.Latest,
|
|
29749
30364
|
/*setParentNodes*/
|
|
29750
30365
|
true,
|
|
29751
|
-
|
|
30366
|
+
ts27.ScriptKind.JS
|
|
29752
30367
|
);
|
|
29753
30368
|
const isBarefootClient = (s) => s === "@barefootjs/client" || s.startsWith("@barefootjs/client/");
|
|
29754
30369
|
const spans = [];
|
|
29755
30370
|
const visit3 = (node) => {
|
|
29756
|
-
if (
|
|
30371
|
+
if (ts27.isImportDeclaration(node) || ts27.isExportDeclaration(node)) {
|
|
29757
30372
|
const ms = node.moduleSpecifier;
|
|
29758
|
-
if (ms &&
|
|
30373
|
+
if (ms && ts27.isStringLiteral(ms) && isBarefootClient(ms.text)) {
|
|
29759
30374
|
spans.push([ms.getStart(sourceFile), ms.getEnd()]);
|
|
29760
30375
|
}
|
|
29761
|
-
} else if (
|
|
30376
|
+
} else if (ts27.isCallExpression(node) && node.expression.kind === ts27.SyntaxKind.ImportKeyword) {
|
|
29762
30377
|
const arg = node.arguments[0];
|
|
29763
|
-
if (arg &&
|
|
30378
|
+
if (arg && ts27.isStringLiteral(arg) && isBarefootClient(arg.text)) {
|
|
29764
30379
|
spans.push([arg.getStart(sourceFile), arg.getEnd()]);
|
|
29765
30380
|
}
|
|
29766
30381
|
}
|
|
29767
|
-
|
|
30382
|
+
ts27.forEachChild(node, visit3);
|
|
29768
30383
|
};
|
|
29769
30384
|
visit3(sourceFile);
|
|
29770
30385
|
if (spans.length === 0) return content2;
|
|
@@ -31965,12 +32580,13 @@ main {
|
|
|
31965
32580
|
});
|
|
31966
32581
|
|
|
31967
32582
|
// src/lib/adapters/runtimes.generated.ts
|
|
31968
|
-
var bfGoSource, evalGoSource, streamingGoSource, bfdevGoSource;
|
|
32583
|
+
var bfGoSource, evalGoSource, repropsGoSource, streamingGoSource, bfdevGoSource;
|
|
31969
32584
|
var init_runtimes_generated = __esm({
|
|
31970
32585
|
"src/lib/adapters/runtimes.generated.ts"() {
|
|
31971
32586
|
"use strict";
|
|
31972
|
-
bfGoSource = '// Package bf provides runtime helper functions for BarefootJS Go templates.\n// These functions mirror JavaScript behavior for consistent SSR output.\npackage bf\n\nimport (\n "bytes"\n "encoding/json"\n "fmt"\n "html/template"\n "math"\n "math/rand"\n "net/url"\n "os"\n "reflect"\n "regexp"\n "sort"\n "strconv"\n "strings"\n "time"\n "unicode"\n "unicode/utf8"\n)\n\n// FuncMap returns a template.FuncMap with all BarefootJS helper functions.\n// Usage:\n//\n// tmpl := template.New("").Funcs(bf.FuncMap())\nfunc FuncMap() template.FuncMap {\n return template.FuncMap{\n // Nullish coalescing (#2248): JS `??` semantics \u2014 fall back only on\n // nil, keeping present-but-falsy values (`""`, `0`, `false`) that\n // Go\'s truthiness-based `or` would replace.\n "bf_nullish": Nullish,\n\n // Arithmetic\n "bf_add": Add,\n "bf_concat_str": ConcatStr,\n "bf_sub": Sub,\n "bf_mul": Mul,\n "bf_div": Div,\n "bf_mod": Mod,\n "bf_neg": Neg,\n "bf_min": Min,\n "bf_max": Max,\n\n // String\n "bf_lower": Lower,\n "bf_upper": Upper,\n "bf_trim": Trim,\n "bf_trim_start": TrimStart,\n "bf_trim_end": TrimEnd,\n "bf_contains": Contains,\n "bf_join": Join,\n "bf_split": Split,\n "bf_starts_with": StartsWith,\n "bf_ends_with": EndsWith,\n "bf_replace": Replace,\n "bf_replace_all": ReplaceAll,\n "bf_repeat": Repeat,\n "bf_pad_start": PadStart,\n "bf_pad_end": PadEnd,\n "bf_string": String,\n "bf_raw_html": RawHTML,\n "bf_ternary": Ternary,\n "bf_truthy": Truthy,\n\n // URL query builder (#1897 PostList href helpers): conditional\n // (include, key, value) triples \u2192 "base?k=v&\u2026", mirroring a\n // URLSearchParams builder with guarded `.set()` calls.\n "bf_query": Query,\n\n // Date method lowering (#2274, spec entry "date"): the lowering\n // target for a zero-arg call on a Date-typed prop.\n "bf_date": Date,\n\n // formatDate(date, pattern, tz) lowering (#2324, spec entry\n // "format_date"): the total, locale-free date-pattern formatter \u2014\n // see FormatDate\'s docstring for the full contract.\n "bf_format_date": FormatDate,\n\n // JSON / numeric primitives \u2014 JS-compat callees registered on\n // the Go adapter\'s `templatePrimitives` map (#1188).\n "bf_json": JSON,\n "bf_number": Number,\n "bf_floor": Floor,\n "bf_ceil": Ceil,\n "bf_round": Round,\n "bf_abs": Abs,\n "bf_to_fixed": ToFixed,\n\n // Array/Slice\n "bf_len": Len,\n "bf_length": Length,\n "bf_is_element": IsValidElement,\n "bf_style_object": StyleObjectToCSS,\n "bf_at": At,\n "bf_includes": Includes,\n "bf_index_of": IndexOf,\n "bf_last_index_of": LastIndexOf,\n "bf_concat": Concat,\n "bf_slice": Slice,\n "bf_reverse": Reverse,\n "bf_flat": Flat,\n "bf_flat_dynamic": FlatDynamicDepth,\n "bf_flat_map": FlatMap,\n "bf_flat_map_tuple": FlatMapTuple,\n "bf_first": First,\n "bf_last": Last,\n "bf_arr": Arr,\n "bf_filter_truthy": FilterTruthy,\n\n // Higher-order Array Methods\n "bf_every": Every,\n "bf_some": Some,\n "bf_filter": Filter,\n "bf_find": Find,\n "bf_find_index": FindIndex,\n "bf_find_last": FindLast,\n "bf_find_last_index": FindLastIndex,\n "bf_sort": Sort,\n "bf_reduce": Reduce,\n\n // Evaluator-driven higher-order folds (#2018): the comparator / reducer\n // body travels as a serialized ParsedExpr (JSON) evaluated per element,\n // generalizing bf_sort / bf_reduce beyond their fixed catalogues. The\n // adapter falls back to bf_sort for a comparator the evaluator can\'t\n // model (e.g. localeCompare). `bf_env` builds the captured-free-var\n // environment passed as the trailing base_env argument.\n "bf_sort_eval": SortEval,\n "bf_reduce_eval": FoldEval,\n "bf_env": Env,\n\n // Evaluator-driven higher-order predicates (#2018, P2): the predicate\n // body travels as a serialized ParsedExpr (JSON) evaluated per element,\n // generalizing bf_filter / bf_find / bf_find_index / bf_every / bf_some\n // beyond their field-equality / truthiness catalogues. `bf_find_eval` /\n // `bf_find_index_eval` take a `forward` bool (false \u2192 findLast variants).\n "bf_filter_eval": FilterEval,\n "bf_every_eval": EveryEval,\n "bf_some_eval": SomeEval,\n "bf_find_eval": FindEval,\n "bf_find_index_eval": FindIndexEval,\n // `.flatMap(proj)`: project each element through the serialized\n // projection body, then flatten one level.\n "bf_flat_map_eval": FlatMapEval,\n // Value-producing `.map(cb)` (#2073): project each element, one\n // result per element (no flatten).\n "bf_map_eval": MapEval,\n\n // Comment marker (for hydration)\n "bfComment": Comment,\n "bfTextStart": TextStart,\n "bfTextEnd": TextEnd,\n\n // Script collection\n "bfScripts": BfScripts,\n\n // Scope attribute value (#1249: bare scope id, no `~` prefix)\n "bfScopeAttr": ScopeAttr,\n\n // Slot-identity markers (#1249): bf-h, bf-m, bf-r\n "bfHydrationAttrs": HydrationAttrs,\n\n // Child component marker (kept for backward compatibility)\n "bfIsChild": IsChild,\n\n // Props attribute for hydration\n "bfPropsAttr": BfPropsAttr,\n\n // Portal HTML rendering (parses and executes template string)\n "bfPortalHTML": PortalHTML,\n\n // JSX children passed to an imported child component (#1896):\n // the parent renders the children fragment via a companion\n // define (executed through bf_tmpl from TemplateFuncMap) and\n // injects the result into the child\'s Children field.\n "bf_with_children": WithChildren,\n\n // Scope comment for fragment roots\n "bfScopeComment": ScopeComment,\n "bfScopeCommentEnd": ScopeCommentEnd,\n\n // JSX intrinsic-element spread lowering (#1407)\n "bf_spread_attrs": SpreadAttrs,\n\n // Destructure object-rest spread-onto-element residual (#2087):\n // "every field except these" for a struct/map, keyed by json tag.\n "bf_omit": Omit,\n\n // Case-tolerant single-field read off a map or struct (#2087): a\n // `useContext` local whose `createContext` default is object-shaped\n // (e.g. `createContext<{ config: X }>({ config: {} })`) is typed\n // `map[string]interface{}`, and its keys are the SOURCE (JS-cased)\n // property names a `<Ctx.Provider value={{ \u2026 }}>` bakes\n // (`providerObjectValueToGoMap`, go-template-adapter.ts) \u2014 plain\n // `text/template` dot access does an exact-string `MapIndex`, so\n // `ctx.config.label` lowers to nested `bf_get` calls instead of\n // `.Ctx.Config.Label`. Reuses the same `getFieldValue` the\n // project/sort helpers already use for a dynamic field-name lookup;\n // safe on a nil map/interface (returns nil) so a missing Provider or\n // an absent key falls through to `??`\'s fallback.\n "bf_get": getFieldValue,\n }\n}\n\n// Query builds a URL from a base path plus a query string assembled from\n// (include, key, value) triples, in order. A pair is considered only when its\n// `include` flag is true \u2014 mirroring a JS URLSearchParams builder whose\n// `.set(key, value)` calls are each guarded by an `if`. The compiler lowers a\n// conditional `cond ? v : undefined` to the `include` bool and a plain `key: v`\n// to a `true` include; the emptiness check is applied HERE (an included but\n// empty value is dropped), matching the client `queryHref` and the Perl `query`\n// helper. Keys and values use formEscape (application/x-www-form-urlencoded),\n// so the rendered query is byte-for-byte identical to the browser\'s\n// URLSearchParams. An empty query yields the bare base.\n//\n// A value may be a string slice ([]string or []any), which APPENDS one pair per\n// non-empty member (URLSearchParams.append) \u2014 `{tag: [a, b]}` \u2192 `tag=a&tag=b`.\n// A scalar value follows URLSearchParams.set() semantics: repeating a key\n// overwrites the value at the key\'s first position rather than duplicating it\n// (object literals have unique keys, so this is defensive). Trailing args that\n// don\'t complete a triple are ignored.\n//\n// formEscape differs from url.QueryEscape only on `~` (kept by QueryEscape,\n// `%7E` here) and `*` (`%2A` by QueryEscape, kept here).\nfunc Query(base string, triples ...any) string {\n type kv struct{ key, val string }\n pairs := make([]kv, 0, len(triples)/3)\n pos := make(map[string]int)\n for i := 0; i+2 < len(triples); i += 3 {\n include, _ := triples[i].(bool)\n if !include {\n continue\n }\n k := String(triples[i+1])\n if members, ok := asStringSlice(triples[i+2]); ok {\n // Array value \u2192 append each non-empty member; appended pairs never\n // overwrite, so they don\'t participate in the set()-position map.\n for _, m := range members {\n if m == "" {\n continue\n }\n pairs = append(pairs, kv{k, m})\n }\n continue\n }\n v := String(triples[i+2])\n if v == "" {\n continue // omit an included-but-empty value (client / Perl parity)\n }\n if at, ok := pos[k]; ok {\n pairs[at].val = v // set(): overwrite the first occurrence\'s value\n } else {\n pos[k] = len(pairs)\n pairs = append(pairs, kv{k, v})\n }\n }\n var b strings.Builder\n for _, p := range pairs {\n if b.Len() == 0 {\n b.WriteByte(\'?\')\n } else {\n b.WriteByte(\'&\')\n }\n b.WriteString(formEscape(p.key))\n b.WriteByte(\'=\')\n b.WriteString(formEscape(p.val))\n }\n return base + b.String()\n}\n\n// Date implements the `date` helper (spec/template-helpers.md, #2274) \u2014 the\n// lowering target for a zero-arg call on a Date-typed prop\n// (`createdAt.toISOString()`). recv accepts the runtime\'s own `time.Time` /\n// `*time.Time` (however the host framework populated the prop) OR an\n// ISO-8601 string (the wire form a JSON-sourced prop arrives as); either is\n// normalized to UTC before dispatching op, matching the client `Date`\'s own\n// instant semantics regardless of which shape reaches this helper. A nil /\n// unparsable receiver yields this runtime\'s zero value for the requested op\n// (0 for every numeric accessor, "" for toISOString) rather than panicking\n// mid-render \u2014 the same tolerance `String`/`Number` already extend to a nil\n// prop. `getUTCMonth` subtracts 1: Go\'s `time.Month` is 1-based, JS\'s is not\n// (spec entry "date" is explicit that JS wins here).\nfunc Date(recv any, op string) any {\n t, ok := toTime(recv)\n if !ok {\n if op == "toISOString" {\n return ""\n }\n return 0\n }\n t = t.UTC()\n switch op {\n case "getUTCFullYear":\n return t.Year()\n case "getUTCMonth":\n return int(t.Month()) - 1\n case "getUTCDate":\n return t.Day()\n case "getUTCHours":\n return t.Hour()\n case "getUTCMinutes":\n return t.Minute()\n case "getUTCSeconds":\n return t.Second()\n case "getTime":\n return t.UnixMilli()\n case "toISOString":\n return t.Format("2006-01-02T15:04:05.000Z")\n default:\n return 0\n }\n}\n\n// tzOffsetRE matches a fixed UTC offset `\xB1HH:MM` within ECMA-402\'s valid\n// range \u2014 hours 00\u201323, minutes 00\u201359 (`\'+09:00\'`, `\'-05:30\'`) \u2014 one of the\n// three `tz` shapes FormatDate accepts (mirrors OFFSET_RE in\n// packages/client/src/format-date.ts). An out-of-range shape (`\'+25:00\'`)\n// falls through to the tzdata lookup, fails it, and errors \u2014 matching the\n// JS reference\'s RangeError (#2344).\nvar tzOffsetRE = regexp.MustCompile(`^([+-])([01][0-9]|2[0-3]):([0-5][0-9])$`)\n\n// formatDateTokenRE is the longest-match pattern-token alternation (mirrors\n// TOKEN_RE in packages/client/src/format-date.ts). Order matters: MMMM\n// before MMM before MM before M (and dddd before ddd, DD before D) so the\n// longer token wins at a position where a shorter one could also match \u2014\n// Go\'s regexp, like JS\'s, resolves alternation leftmost-first (not POSIX\n// leftmost-longest), so listing the longer alternative first is what makes\n// e.g. "MMMM" consume all four characters instead of "MM" + "MM".\nvar formatDateTokenRE = regexp.MustCompile(`YYYY|MMMM|MMM|MM|DD|dddd|ddd|M|D`)\n\n// nameTable section offsets (#2334, mirrors MONTHS_WIDE / MONTHS_ABBR /\n// WEEKDAYS_WIDE / WEEKDAYS_ABBR in packages/client/src/format-date.ts).\nconst (\n monthsWide = 0\n monthsAbbr = 12\n weekdaysWide = 24\n weekdaysAbbr = 31\n)\n\n// formatDateName reads a name-token table entry: index out of range, or a\n// non-string element, both render "" \u2014 the same total, zero-value\n// discipline as an unparseable date (mirrors the JS reference\'s\n// `names[index] ?? ""` fallback, where every table element the vectors ever\n// carry is a string).\nfunc formatDateName(names []any, index int) string {\n if index < 0 || index >= len(names) {\n return ""\n }\n s, ok := names[index].(string)\n if !ok {\n return ""\n }\n return s\n}\n\n// FormatDate implements the `format_date` helper (#2324, #2334, spec entry\n// "format_date") \u2014 the lowering target for\n// `formatDate(date, pattern, tz, names)`\n// (packages/client/src/format-date.ts, the JS-normative reference this must\n// match byte-for-byte). Total and deterministic: no locale, no host\n// timezone, no "now".\n//\n// recv: same receiver contract as the `date` helper above \u2014 the runtime\'s\n// own `time.Time` / `*time.Time`, or an ISO-8601 string \u2014 normalized via\n// `toTime`. A nil / unparseable receiver returns "" (never panics).\n//\n// tz (#2344): "UTC", a range-valid fixed offset `\xB1HH:MM` (shifts by\n// sign*(HH*60+MM) minutes), or a canonical IANA zone name ("Asia/Tokyo")\n// resolved through tzdata via time.LoadLocation \u2014 the zone\'s UTC offset AT\n// THE INSTANT being formatted (DST-aware, historical-transition-aware,\n// seconds precision: pre-standard LMT offsets like Tokyo\'s +09:18:59\n// count). ANY other value \u2014 an unknown zone, a malformed or out-of-range\n// offset ("+9:00", "+25:00"), the empty string or "Local" (LoadLocation\'s\n// implicit-environment aliases) \u2014 returns an ERROR, aborting template\n// execution loudly: the JS reference throws a RangeError there, and a\n// silently substituted timezone is the one failure mode this helper must\n// not have (the pre-#2344 normalize-to-UTC total function is gone). The\n// shifted instant\'s UTC calendar fields (not the original instant\'s) are\n// what pattern tokens read \u2014 the shifted UTC clock face IS the local clock\n// face in that zone, same reasoning as the JS reference.\n//\n// names (#2334): a flat name table in fixed layout \u2014 `[0..11]` wide month\n// names, `[12..23]` abbreviated month names, `[24..30]` wide weekday names\n// (Sunday-first), `[31..37]` abbreviated weekday names. The caller owns the\n// values; this helper only indexes the table.\n//\n// pattern: longest-match token substitution\n// (`YYYY|MMMM|MMM|MM|DD|dddd|ddd|M|D`); every other character \u2014 including\n// multi-byte ones like \u5E74/\u6708/\u65E5 \u2014 passes through literally. `YYYY` is\n// `abs(year)` zero-padded to 4 digits, `-`-prefixed for a negative year;\n// `MM`/`DD` zero-pad to 2; `M`/`D` are bare; `MMMM`/`MMM` and `dddd`/`ddd`\n// read the `names` table (weekday computed on the offset-shifted instant,\n// Sunday-first, matching `time.Time.Weekday()`\'s own Sunday=0 encoding).\nfunc FormatDate(recv any, pattern string, tz string, names []any) (string, error) {\n t, ok := toTime(recv)\n if !ok {\n // Receiver contract precedes tz validation (spec receiver-first\n // discipline, mirrored by every port).\n return "", nil\n }\n offsetSeconds := 0\n if tz != "UTC" {\n if m := tzOffsetRE.FindStringSubmatch(tz); m != nil {\n hh, _ := strconv.Atoi(m[2])\n mm, _ := strconv.Atoi(m[3])\n offsetSeconds = (hh*60 + mm) * 60\n if m[1] == "-" {\n offsetSeconds = -offsetSeconds\n }\n } else if tz == "" || tz == "Local" {\n // LoadLocation("") is UTC and LoadLocation("Local") is the host\n // zone \u2014 both implicit-environment reads the contract refuses.\n return "", fmt.Errorf("format_date: unresolvable timeZone %q", tz)\n } else {\n loc, err := time.LoadLocation(tz)\n if err != nil {\n return "", fmt.Errorf("format_date: unresolvable timeZone %q", tz)\n }\n _, offsetSeconds = t.UTC().In(loc).Zone()\n }\n }\n shifted := t.UTC().Add(time.Duration(offsetSeconds) * time.Second)\n year := shifted.Year()\n month := int(shifted.Month())\n day := shifted.Day()\n weekday := int(shifted.Weekday()) // time.Sunday == 0, matching the table\'s Sunday-first layout\n absYear := year\n if absYear < 0 {\n absYear = -absYear\n }\n yyyy := fmt.Sprintf("%04d", absYear)\n if year < 0 {\n yyyy = "-" + yyyy\n }\n out := formatDateTokenRE.ReplaceAllStringFunc(pattern, func(token string) string {\n switch token {\n case "YYYY":\n return yyyy\n case "MMMM":\n return formatDateName(names, monthsWide+month-1)\n case "MMM":\n return formatDateName(names, monthsAbbr+month-1)\n case "MM":\n return fmt.Sprintf("%02d", month)\n case "M":\n return strconv.Itoa(month)\n case "DD":\n return fmt.Sprintf("%02d", day)\n case "D":\n return strconv.Itoa(day)\n case "dddd":\n return formatDateName(names, weekdaysWide+weekday)\n case "ddd":\n return formatDateName(names, weekdaysAbbr+weekday)\n default:\n return token\n }\n })\n return out, nil\n}\n\n// toTime normalizes a `Date` helper receiver to a `time.Time`: the runtime\'s\n// own `time.Time` / `*time.Time`, or an ISO-8601 string parsed with\n// `time.RFC3339Nano` (accepts both the `Z`-suffixed and numeric-offset\n// forms, and any sub-second precision \u2014 including the millisecond precision\n// every value this runtime itself ever produces via `toISOString` above).\n// Anything else (nil, an unparsable string, an unrelated type) reports !ok\n// so `Date` can apply its documented zero-value fallback instead of\n// panicking.\nfunc toTime(recv any) (time.Time, bool) {\n switch v := recv.(type) {\n case time.Time:\n return v, true\n case *time.Time:\n if v == nil {\n return time.Time{}, false\n }\n return *v, true\n case string:\n t, err := time.Parse(time.RFC3339Nano, v)\n if err != nil {\n return time.Time{}, false\n }\n return t, true\n default:\n return time.Time{}, false\n }\n}\n\n// asStringSlice reports whether v is a query *array* value and, if so, returns\n// its members stringified. A compiled template passes a `[]string` field; the\n// golden conformance vectors decode JSON arrays to `[]any`. Anything else is a\n// scalar (false), handled by the set() path.\nfunc asStringSlice(v any) ([]string, bool) {\n switch s := v.(type) {\n case []string:\n return s, true\n case []any:\n out := make([]string, len(s))\n for i, m := range s {\n out[i] = String(m)\n }\n return out, true\n default:\n return nil, false\n }\n}\n\nconst hexUpper = "0123456789ABCDEF"\n\n// formEscape percent-encodes s with the application/x-www-form-urlencoded byte\n// set, matching the browser\'s URLSearchParams serialization (and the Perl\n// `query` helper) so SSR query strings render byte-for-byte identically across\n// adapters. The unreserved set kept verbatim is A-Z a-z 0-9 and `* - . _`; a\n// space becomes `+`; every other byte is `%XX` with uppercase hex. Encoding is\n// byte-wise, so multi-byte UTF-8 is percent-encoded per byte (`\xE9` \u2192 `%C3%A9`).\n//\n// This differs from url.QueryEscape only for `~` (kept by QueryEscape, encoded\n// to `%7E` here) and `*` (encoded to `%2A` by QueryEscape, kept here).\nfunc formEscape(s string) string {\n var b strings.Builder\n for i := 0; i < len(s); i++ {\n c := s[i]\n switch {\n case c >= \'A\' && c <= \'Z\', c >= \'a\' && c <= \'z\', c >= \'0\' && c <= \'9\',\n c == \'*\', c == \'-\', c == \'.\', c == \'_\':\n b.WriteByte(c)\n case c == \' \':\n b.WriteByte(\'+\')\n default:\n b.WriteByte(\'%\')\n b.WriteByte(hexUpper[c>>4])\n b.WriteByte(hexUpper[c&0x0F])\n }\n }\n return b.String()\n}\n\n// ScopeAttr returns the bare bf-s scope id (#1249).\nfunc ScopeAttr(props interface{}) string {\n return getStringField(props, "ScopeID")\n}\n\n// HydrationAttrs emits `bf-h="<host>" bf-m="<slot>" bf-r=""` conditionally.\n// See spec/compiler.md "Slot identity".\nfunc HydrationAttrs(props interface{}) template.HTMLAttr {\n parts := []string{}\n if host := getStringField(props, "BfParent"); host != "" {\n parts = append(parts, fmt.Sprintf(`bf-h="%s"`, template.HTMLEscapeString(host)))\n }\n if mount := getStringField(props, "BfMount"); mount != "" {\n parts = append(parts, fmt.Sprintf(`bf-m="%s"`, template.HTMLEscapeString(mount)))\n }\n if !getBoolField(props, "BfIsChild") {\n parts = append(parts, `bf-r=""`)\n }\n if len(parts) == 0 {\n return ""\n }\n return template.HTMLAttr(strings.Join(parts, " "))\n}\n\n// IsChild is a deprecated no-op stub. Child status is signalled by bf-h\n// presence (#1249); use HydrationAttrs instead.\nfunc IsChild(props interface{}) template.HTMLAttr {\n return ""\n}\n\n// svgCamelCaseAttrs mirrors SVG_CAMEL_CASE_ATTRS from\n// packages/client/src/runtime/spread-attrs.ts. SVG XML attribute\n// names are case-sensitive; the default camelCase \u2192 kebab-case\n// rewrite must NOT apply to these or the SVG stops rendering\n// (#1407). Coordinates with the compile-time SVG_CAMEL_TO_KEBAB\n// table in packages/jsx/src/ir-to-client-js/utils.ts: presentation\n// attrs (clipPath, strokeWidth, \u2026) live there and must NOT appear\n// here, or the same JSX prop would lower to clip-path via the\n// explicit-attr path and stay clipPath via the spread path.\nvar svgCamelCaseAttrs = map[string]struct{}{\n "allowReorder": {}, "attributeName": {}, "attributeType": {}, "autoReverse": {},\n "baseFrequency": {}, "baseProfile": {}, "calcMode": {}, "clipPathUnits": {},\n "contentScriptType": {}, "contentStyleType": {}, "diffuseConstant": {}, "edgeMode": {},\n "externalResourcesRequired": {}, "filterRes": {}, "filterUnits": {}, "glyphRef": {},\n "gradientTransform": {}, "gradientUnits": {}, "kernelMatrix": {}, "kernelUnitLength": {},\n "keyPoints": {}, "keySplines": {}, "keyTimes": {}, "lengthAdjust": {}, "limitingConeAngle": {},\n "markerHeight": {}, "markerUnits": {}, "markerWidth": {}, "maskContentUnits": {},\n "maskUnits": {}, "numOctaves": {}, "pathLength": {}, "patternContentUnits": {},\n "patternTransform": {}, "patternUnits": {}, "pointsAtX": {}, "pointsAtY": {}, "pointsAtZ": {},\n "preserveAlpha": {}, "preserveAspectRatio": {}, "primitiveUnits": {}, "refX": {}, "refY": {},\n "repeatCount": {}, "repeatDur": {}, "requiredExtensions": {}, "requiredFeatures": {},\n "specularConstant": {}, "specularExponent": {}, "spreadMethod": {}, "startOffset": {},\n "stdDeviation": {}, "stitchTiles": {}, "surfaceScale": {}, "systemLanguage": {},\n "tableValues": {}, "targetX": {}, "targetY": {}, "textLength": {}, "viewBox": {}, "viewTarget": {},\n "xChannelSelector": {}, "yChannelSelector": {}, "zoomAndPan": {},\n}\n\n// toAttrName mirrors the JSX\u2192HTML attribute-name rewrite from\n// packages/client/src/runtime/spread-attrs.ts. className \u2192 class,\n// htmlFor \u2192 for, SVG camelCase attrs preserved, other camelCase\n// keys lowered to kebab-case.\nfunc toAttrName(key string) string {\n if key == "className" {\n return "class"\n }\n if key == "htmlFor" {\n return "for"\n }\n if _, ok := svgCamelCaseAttrs[key]; ok {\n return key\n }\n // camelCase \u2192 kebab-case: mirror the JS reference exactly\n // (`key.replace(/([A-Z])/g, \'-$1\').toLowerCase()`). The JS shape\n // produces a leading `-` for an initial uppercase letter\n // (`XData` \u2192 `-x-data`); both this Go path and the matching JS\n // runtime are wrong-by-construction for that case (the resulting\n // HTML attribute name is invalid), but keeping them byte-equal\n // avoids silent SSR/CSR divergence (#1411 review).\n var b strings.Builder\n for _, r := range key {\n if r >= \'A\' && r <= \'Z\' {\n b.WriteByte(\'-\')\n b.WriteRune(r + 32)\n } else {\n b.WriteRune(r)\n }\n }\n return b.String()\n}\n\n// hasUnsafeStyleValue mirrors Hono\'s own CSS-injection guard\n// (`hono/jsx/utils.ts`\'s `hasUnsafeStyleValue` \u2014 the ORACLE this adapter\'s\n// dynamic `style={{...}}` values must match, #2261): a hand-rolled\n// structural scan for characters that could break out of a CSS\n// declaration, NOT real CSSOM property validation. Ported byte-for-byte \u2014\n// every character this scan tests is ASCII, so scanning by byte (Go\n// string indexing) agrees with Hono\'s UTF-16-code-unit scan for every\n// input; a multibyte UTF-8 sequence has no byte in the ASCII range, so it\n// can never spuriously match one of these single-byte comparisons. Skips\n// the reference implementation\'s regex fast-path (a pure optimization \u2014\n// the scan below already returns `false` promptly for a clean value).\nfunc hasUnsafeStyleValue(value string) bool {\n quote := byte(0)\n blockStack := make([]byte, 0, 4)\n for i := 0; i < len(value); i++ {\n c := value[i]\n switch {\n case c == \'\\\\\':\n if i == len(value)-1 {\n return true\n }\n i++\n case quote != 0:\n if c == \'\\n\' || c == \'\\f\' || c == \'\\r\' {\n return true\n }\n if c == quote {\n quote = 0\n }\n case c == \'/\' && i+1 < len(value) && value[i+1] == \'*\':\n end := strings.Index(value[i+2:], "*/")\n if end == -1 {\n return true\n }\n i = i + 2 + end + 1\n case c == \'"\' || c == \'\\\'\':\n quote = c\n case c == \'(\':\n blockStack = append(blockStack, \')\')\n case c == \'[\':\n blockStack = append(blockStack, \']\')\n case c == \'{\' || c == \'}\':\n return true\n case c == \')\' || c == \']\':\n if len(blockStack) == 0 || blockStack[len(blockStack)-1] != c {\n return true\n }\n blockStack = blockStack[:len(blockStack)-1]\n case c == \';\' && len(blockStack) == 0:\n return true\n }\n }\n return quote != 0 || len(blockStack) != 0\n}\n\n// StyleObjectToCSS builds the CSS string for a `style={{...}}` JSX\n// object-literal attribute (#2261) \u2014 `pairs` alternates CSS key (always a\n// compile-time-known literal), then value (`any`, possibly a runtime\n// expression\'s result). A value that fails `hasUnsafeStyleValue` (after\n// JS-`String()`-style stringification) is DROPPED \u2014 the whole `key:value`\n// pair is omitted \u2014 matching Hono\'s oracle behavior exactly, rather than\n// html/template\'s own contextual CSS auto-escaper (which instead emits its\n// `ZgotmplZ` unsafe-content sentinel for the same input). The final joined\n// string is STILL HTML-escaped (mirroring Hono\'s own `escapeToBuffer` call\n// on its accumulated style string) \u2014 a "safe" value can still carry a\n// literal `"`/`\'`/`&` (e.g. a BALANCED-quote CSS string value like\n// `"hello"` passes the structural scan; the quote chars survive into the\n// value) that would otherwise break out of the double-quoted `style="..."`\n// attribute. Returns `template.CSS` (over the escaped result) so\n// html/template treats it as trusted CSS content instead of ALSO applying\n// its own contextual CSS auto-escaper (which would re-derive the exact\n// `ZgotmplZ` divergence this function exists to avoid).\nfunc StyleObjectToCSS(pairs ...any) template.CSS {\n parts := make([]string, 0, len(pairs)/2)\n for i := 0; i+1 < len(pairs); i += 2 {\n key := fmt.Sprint(pairs[i])\n value := String(pairs[i+1])\n if hasUnsafeStyleValue(value) {\n continue\n }\n parts = append(parts, template.HTMLEscapeString(key)+":"+template.HTMLEscapeString(value))\n }\n return template.CSS(strings.Join(parts, ";"))\n}\n\n// StyleToCss mirrors styleToCss from\n// packages/client/src/runtime/style.ts. Accepts a string passthrough,\n// or a map (JSON-deserialized object) whose camelCase keys are\n// lowered to kebab-case and joined with `;`. Returns ("", false) for\n// nullish/empty input so callers can omit the attribute entirely.\nfunc StyleToCss(v any) (string, bool) {\n if v == nil {\n return "", false\n }\n rv := reflect.ValueOf(v)\n for rv.Kind() == reflect.Interface || rv.Kind() == reflect.Pointer {\n if rv.IsNil() {\n return "", false\n }\n rv = rv.Elem()\n }\n if rv.Kind() != reflect.Map {\n // Non-object: stringify and return as-is, matching the JS\n // `typeof value !== \'object\'` branch.\n s := fmt.Sprint(v)\n if s == "" {\n return "", false\n }\n return s, true\n }\n keys := rv.MapKeys()\n sorted := make([]string, 0, len(keys))\n for _, k := range keys {\n if k.Kind() == reflect.String {\n sorted = append(sorted, k.String())\n }\n }\n sort.Strings(sorted)\n parts := make([]string, 0, len(sorted))\n for _, k := range sorted {\n val := rv.MapIndex(reflect.ValueOf(k))\n // Skip nil entries (matches the JS `if (v == null) continue`).\n if !val.IsValid() {\n continue\n }\n if val.Kind() == reflect.Interface || val.Kind() == reflect.Pointer {\n if val.IsNil() {\n continue\n }\n val = val.Elem()\n }\n prop := toAttrName(k)\n parts = append(parts, fmt.Sprintf("%s:%v", prop, val.Interface()))\n }\n if len(parts) == 0 {\n return "", false\n }\n return strings.Join(parts, ";"), true\n}\n\n// SpreadAttrs lowers a JSX intrinsic-element spread bag (#1407) to\n// an HTML attribute string. Mirrors spreadAttrs from\n// packages/client/src/runtime/spread-attrs.ts so SSR output matches\n// what CSR\'s `applyRestAttrs` writes at hydration.\n//\n// Skip rules: nil/false values, event handlers (`on[A-Z]*`),\n// `children`, `ref`.\n//\n// Key remap: className \u2192 class, htmlFor \u2192 for, SVG camelCase\n// preserved, other camelCase \u2192 kebab-case.\n//\n// `style` is routed through StyleToCss so object literals serialize\n// to a real CSS string instead of Go\'s default `map[k:v]` form.\n//\n// Booleans: true \u2192 bare attribute name, false \u2192 omitted.\n// Other scalar values are HTML-escaped via template.HTMLEscapeString.\n// Returns a `template.HTMLAttr` so html/template emits the result\n// verbatim (the function does its own escaping).\n//\n// Keys are sorted alphabetically before emission for deterministic\n// output. SSR/CSR attribute-order divergence is acceptable per the\n// rest-destructure-object-spread-in-map fixture\'s documented policy\n// \u2014 browsers honor the LAST value when a key is duplicated, so\n// pairing with static attrs (`<div class="x" {...rest}>`) is\n// last-wins regardless of order.\nfunc SpreadAttrs(bag any) template.HTMLAttr {\n if bag == nil {\n return ""\n }\n rv := reflect.ValueOf(bag)\n for rv.Kind() == reflect.Interface || rv.Kind() == reflect.Pointer {\n if rv.IsNil() {\n return ""\n }\n rv = rv.Elem()\n }\n if rv.Kind() != reflect.Map {\n return ""\n }\n keys := rv.MapKeys()\n sortedKeys := make([]string, 0, len(keys))\n for _, k := range keys {\n if k.Kind() == reflect.String {\n sortedKeys = append(sortedKeys, k.String())\n }\n }\n sort.Strings(sortedKeys)\n parts := make([]string, 0, len(sortedKeys))\n for _, key := range sortedKeys {\n // Event handlers \u2014 skip at SSR the same way\n // packages/client/src/runtime/spread-attrs.ts does at\n // hydration. The JS predicate is\n // `key.startsWith(\'on\') && key.length > 2 && key[2] === key[2].toUpperCase()`,\n // which is true for any character whose uppercase form is\n // itself: ASCII A-Z, digits, underscore, and non-letter\n // symbols. Mirror that here by skipping when key[2] is NOT\n // a lowercase ASCII letter \u2014 so `onClick`, `on_custom`, and\n // `on0` all match (#1411 review).\n if len(key) > 2 && key[0] == \'o\' && key[1] == \'n\' && !(key[2] >= \'a\' && key[2] <= \'z\') {\n continue\n }\n // `children` is a JSX construct rendered inside the element,\n // never a DOM attribute. `ref` is intentionally NOT filtered\n // here so output stays byte-equal with the JS reference\n // `spreadAttrs` in packages/client/src/runtime/spread-attrs.ts\n // (which only filters null/false, event handlers, and\n // children) \u2014 aligning Go\'s filter set diverges from JS in\n // the opposite direction. Filtering `ref` consistently across\n // both SSR runtimes is a separate concern tracked alongside\n // the JS `applyRestAttrs` vs `spreadAttrs` mismatch (#1411\n // review).\n if key == "children" {\n continue\n }\n val := rv.MapIndex(reflect.ValueOf(key))\n if !val.IsValid() {\n continue\n }\n // Unwrap interface wrappers (json.Unmarshal produces\n // interface{}-wrapped values for map[string]any).\n v := val\n for v.Kind() == reflect.Interface || v.Kind() == reflect.Pointer {\n if v.IsNil() {\n // Skip null entries.\n v = reflect.Value{}\n break\n }\n v = v.Elem()\n }\n if !v.IsValid() {\n continue\n }\n // Boolean values: true \u2192 bare attribute, false \u2192 omitted.\n if v.Kind() == reflect.Bool {\n if !v.Bool() {\n continue\n }\n parts = append(parts, toAttrName(key))\n continue\n }\n // `style` routes through StyleToCss so object literals get a\n // real CSS string. The JS side does the same.\n if key == "style" {\n css, ok := StyleToCss(v.Interface())\n if !ok {\n continue\n }\n parts = append(parts, fmt.Sprintf(`style="%s"`, template.HTMLEscapeString(css)))\n continue\n }\n // Stringify and escape. fmt.Sprint handles numbers, bools-as-\n // strings, and arbitrary stringer types the same way the JS\n // `String(value)` coercion does for the analogous cases.\n s := fmt.Sprint(v.Interface())\n parts = append(parts, fmt.Sprintf(`%s="%s"`, toAttrName(key), template.HTMLEscapeString(s)))\n }\n if len(parts) == 0 {\n return ""\n }\n return template.HTMLAttr(strings.Join(parts, " "))\n}\n\n// Omit builds a `map[string]any` residual bag from a struct or map value,\n// excluding the given keys \u2014 powers the `{...rest}` spread-onto-element\n// lowering for a destructured `.map()` loop-item\'s object-rest binding\n// (#2087): `.map(({ id, title, ...rest }) => <li {...rest}>)` needs "every\n// field EXCEPT the ones the pattern already destructured out", and a static\n// Go struct type has no way to express "minus a field" \u2014 the exclude set is\n// known at COMPILE TIME (the sibling keys the destructure pattern names), so\n// the compiler passes them here and this does the per-item field-vs-key\n// matching a static type can\'t. The result feeds `bf_spread_attrs`\n// (`SpreadAttrs`), same as a top-level `{...attrs()}` bag.\n//\n// Struct receiver: iterates exported fields via reflection, keyed by each\n// field\'s `json` struct tag (falling back to the Go field name when absent)\n// \u2014 the generated struct\'s json tag is always the ORIGINAL source property\n// name (see `structFieldsFor` / `typeDefinitionToGo` in the Go adapter), so\n// this reproduces the exact JS key `SpreadAttrs`\'s `toAttrName` expects\n// (`"data-priority"`, not a re-derived `"DataPriority"`). A tag of `"-"`\n// (opt-out) is skipped like `encoding/json` does.\n//\n// Map receiver: copies string keys through directly, same exclude/skip\n// rules.\n//\n// Anything else (nil, a non-struct/non-map interface) returns an empty map.\nfunc Omit(item any, excludeKeys ...string) map[string]any {\n exclude := make(map[string]struct{}, len(excludeKeys))\n for _, k := range excludeKeys {\n exclude[k] = struct{}{}\n }\n out := map[string]any{}\n rv := reflect.ValueOf(item)\n for rv.Kind() == reflect.Interface || rv.Kind() == reflect.Pointer {\n if rv.IsNil() {\n return out\n }\n rv = rv.Elem()\n }\n switch rv.Kind() {\n case reflect.Struct:\n rt := rv.Type()\n for i := 0; i < rt.NumField(); i++ {\n field := rt.Field(i)\n if !field.IsExported() {\n continue\n }\n key := field.Name\n if tag, ok := field.Tag.Lookup("json"); ok {\n if comma := strings.Index(tag, ","); comma >= 0 {\n tag = tag[:comma]\n }\n if tag == "-" {\n continue\n }\n if tag != "" {\n key = tag\n }\n }\n if _, skip := exclude[key]; skip {\n continue\n }\n out[key] = rv.Field(i).Interface()\n }\n case reflect.Map:\n for _, k := range rv.MapKeys() {\n if k.Kind() != reflect.String {\n continue\n }\n key := k.String()\n if _, skip := exclude[key]; skip {\n continue\n }\n val := rv.MapIndex(k)\n if val.IsValid() {\n out[key] = val.Interface()\n }\n }\n }\n return out\n}\n\n// BfPropsAttr returns the bf-p attribute with the JSON-serialized\n// props in flat format. Output format: `bf-p=\'{"propName":value,...}\'`.\n// Only emits the attribute for root components (BfIsRoot == true);\n// child components receive props from their parent via initChild().\n//\n// Returns the marshal error so a `template.Execute` call fails\n// loudly on cycles / unsupported props rather than silently\n// dropping the bf-p attribute and breaking client-side hydration.\n// Same loud-failure policy as `JSON` \u2014 user data going through\n// `encoding/json` shouldn\'t fail invisibly.\nfunc BfPropsAttr(props interface{}) (template.HTMLAttr, error) {\n // Only root components should emit bf-p\n if !getBoolField(props, "BfIsRoot") {\n return "", nil\n }\n\n propsJSON, err := json.Marshal(props)\n if err != nil {\n return "", err\n }\n\n escaped := template.HTMLEscapeString(string(propsJSON))\n return template.HTMLAttr(`bf-p="` + escaped + `"`), nil\n}\n\n// =============================================================================\n// Arithmetic Operations\n// =============================================================================\n\n// Add returns a + b. Supports int and float64.\nfunc Add(a, b any) any {\n av, bv := toFloat64(a), toFloat64(b)\n result := av + bv\n // Return int if both inputs were int-like\n if isIntLike(a) && isIntLike(b) && result == float64(int(result)) {\n return int(result)\n }\n return result\n}\n\n// ConcatStr returns a and b concatenated as strings \u2014 the string-typed half\n// of JS `+` (#2168 string-concat-plus). JS `+` is addition when BOTH\n// operands are numeric and concatenation when EITHER is a string; `Add`\n// (above) covers the numeric case, this covers the string one \u2014 `Add`\n// itself can\'t (`toFloat64` returns 0 for a string operand, so `\'Hello, \' +\n// name` silently rendered "0" before this existed).\nfunc ConcatStr(a, b any) string {\n return toString(a) + toString(b)\n}\n\n// Sub returns a - b. Supports int and float64.\nfunc Sub(a, b any) any {\n av, bv := toFloat64(a), toFloat64(b)\n result := av - bv\n if isIntLike(a) && isIntLike(b) && result == float64(int(result)) {\n return int(result)\n }\n return result\n}\n\n// Mul returns a * b. Supports int and float64.\nfunc Mul(a, b any) any {\n av, bv := toFloat64(a), toFloat64(b)\n result := av * bv\n if isIntLike(a) && isIntLike(b) && result == float64(int(result)) {\n return int(result)\n }\n return result\n}\n\n// Div returns a / b. Returns float64 to match JavaScript behavior.\n// Returns 0 if b is 0 (instead of panicking).\nfunc Div(a, b any) any {\n av, bv := toFloat64(a), toFloat64(b)\n if bv == 0 {\n return 0\n }\n return av / bv\n}\n\n// Min returns the smaller of a and b (two-arg `Math.min`). Like Mul, it keeps\n// an integer result when both operands are int-like so a CSS value such as\n// `bf_min 100 x` stays `100` rather than `100.000000`. Uses `Number` (not\n// `toFloat64`, which silently zeroes an unrecognized type like a non-numeric\n// string) plus explicit NaN checks, since IEEE-754 `<`/`>` comparisons\n// against NaN are always false and would otherwise let a non-NaN operand\n// win instead of propagating NaN like JS `Math.min`/`Math.max` do.\nfunc Min(a, b any) any {\n av, bv := Number(a), Number(b)\n if math.IsNaN(av) {\n return av\n }\n if math.IsNaN(bv) {\n return bv\n }\n r := av\n if bv < av {\n r = bv\n }\n if isIntLike(a) && isIntLike(b) && r == float64(int(r)) {\n return int(r)\n }\n return r\n}\n\n// Max returns the larger of a and b (two-arg `Math.max`), with the same\n// int-preserving rule and NaN-propagation as Min.\nfunc Max(a, b any) any {\n av, bv := Number(a), Number(b)\n if math.IsNaN(av) {\n return av\n }\n if math.IsNaN(bv) {\n return bv\n }\n r := av\n if bv > av {\n r = bv\n }\n if isIntLike(a) && isIntLike(b) && r == float64(int(r)) {\n return int(r)\n }\n return r\n}\n\n// Mod returns a % b (modulo). Supports int only.\nfunc Mod(a, b any) int {\n av, bv := toInt(a), toInt(b)\n if bv == 0 {\n return 0\n }\n return av % bv\n}\n\n// Neg returns -a (negation).\nfunc Neg(a any) any {\n if v, ok := a.(int); ok {\n return -v\n }\n return -toFloat64(a)\n}\n\n// =============================================================================\n// String Operations\n// =============================================================================\n\n// Lower returns the lowercase version of s.\nfunc Lower(s string) string {\n return strings.ToLower(s)\n}\n\n// Upper returns the uppercase version of s.\nfunc Upper(s string) string {\n return strings.ToUpper(s)\n}\n\n// Trim returns s with leading and trailing whitespace removed.\nfunc Trim(s string) string {\n return strings.TrimSpace(s)\n}\n\n// TrimStart returns s with leading whitespace removed\n// (String.prototype.trimStart, #2183) \u2014 the one-sided sibling of\n// Trim above, using the same unicode.IsSpace predicate strings.TrimSpace\n// applies to both sides.\nfunc TrimStart(s string) string {\n return strings.TrimLeftFunc(s, unicode.IsSpace)\n}\n\n// TrimEnd returns s with trailing whitespace removed\n// (String.prototype.trimEnd, #2183) \u2014 the one-sided sibling of Trim.\nfunc TrimEnd(s string) string {\n return strings.TrimRightFunc(s, unicode.IsSpace)\n}\n\n// Contains returns true if s contains substr.\nfunc Contains(s, substr string) bool {\n return strings.Contains(s, substr)\n}\n\n// Split lowers `String.prototype.split(sep, limit?)` (#1448 Tier B). It\n// wraps `strings.Split` and normalises the result to `[]any` so the\n// slice composes with the array-method surface downstream (`bf_join`,\n// range loops, `bf_len`, \u2026) the same way `bf_slice` / `bf_reverse`\n// results do. Like JS, an empty separator splits into individual UTF-8\n// characters and trailing empty fields are preserved (`"a,".split(",")`\n// \u2192 `["a", ""]`). An optional `limit` caps the number of returned\n// pieces (`"a,b,c".split(",", 2)` \u2192 `["a", "b"]`); a negative limit is\n// ignored (JS would also return every piece \u2014 its ToUint32 wrap makes\n// the limit effectively unbounded). The no-separator form is handled by\n// the adapter (it emits `bf_arr` for the whole-string single element).\nfunc Split(s, sep string, limit ...int) []any {\n parts := strings.Split(s, sep)\n if len(limit) > 0 && limit[0] >= 0 && limit[0] < len(parts) {\n parts = parts[:limit[0]]\n }\n out := make([]any, len(parts))\n for i, p := range parts {\n out[i] = p\n }\n return out\n}\n\n// StartsWith lowers `String.prototype.startsWith(prefix, position?)`\n// (#1448 Tier B). Wraps `strings.HasPrefix`; an empty prefix is always\n// true (JS parity). The optional `position` re-anchors the test to start\n// at that index (clamped to `[0, len]` so it never panics), matching JS\n// `"abc".startsWith("b", 1) === true`.\nfunc StartsWith(s, prefix string, position ...int) bool {\n if len(position) > 0 {\n p := position[0]\n if p < 0 {\n p = 0\n }\n if p > len(s) {\n p = len(s)\n }\n s = s[p:]\n }\n return strings.HasPrefix(s, prefix)\n}\n\n// EndsWith lowers `String.prototype.endsWith(suffix, endPosition?)`\n// (#1448 Tier B). Wraps `strings.HasSuffix`; an empty suffix is always\n// true (JS parity). The optional `endPosition` treats the string as if\n// it were only that many bytes long (clamped to `[0, len]`), matching JS\n// `"abc".endsWith("b", 2) === true`.\nfunc EndsWith(s, suffix string, endPosition ...int) bool {\n if len(endPosition) > 0 {\n e := endPosition[0]\n if e < 0 {\n e = 0\n }\n if e > len(s) {\n e = len(s)\n }\n s = s[:e]\n }\n return strings.HasSuffix(s, suffix)\n}\n\n// Replace lowers the string-pattern form of `String.prototype.replace`\n// (#1448 Tier B). JS replaces only the FIRST occurrence for a string\n// pattern, so the count is 1 (`strings.Replace` with n=1; `ReplaceAll`\n// below is the every-occurrence sibling, `.replaceAll`, #2182). The\n// replacement is treated literally: unlike JS, special replacement\n// patterns like `$&` / `$1` are NOT interpreted (Go and Perl agree on\n// literal replacement, keeping the two template adapters byte-equal;\n// this diverges from the Hono/CSR JS path only for replacement strings\n// that contain `$`-patterns, which are rare in template position).\nfunc Replace(s, old, new string) string {\n return strings.Replace(s, old, new, 1)\n}\n\n// ReplaceAll lowers the string-pattern form of\n// `String.prototype.replaceAll` (#2182): every occurrence, via\n// `strings.ReplaceAll` (equivalent to `strings.Replace` with n=-1).\n// Same literal-replacement caveat as `Replace` above.\nfunc ReplaceAll(s, old, new string) string {\n return strings.ReplaceAll(s, old, new)\n}\n\n// Repeat lowers `String.prototype.repeat(n)` (#1448 Tier B): the\n// receiver concatenated n times. JS throws RangeError for a negative\n// count and `strings.Repeat` panics, so a negative count clamps to the\n// empty string \u2014 SSR templates degrade rather than crash the render.\n// A zero count is the empty string (JS parity).\nfunc Repeat(s string, n int) string {\n if n <= 0 {\n return ""\n }\n return strings.Repeat(s, n)\n}\n\n// padTo lowers the shared body of `String.prototype.padStart` /\n// `padEnd` (#1448 Tier B): pad `s` to `target` code points using `pad`\n// repeated and truncated to fill, prepended (atStart) or appended.\n// Length is measured in runes (not bytes) so the result matches the\n// Perl `bf->pad_*` helpers \u2014 this diverges from JS\'s UTF-16-unit length\n// only for astral-plane input. An empty pad, or a receiver already at\n// least `target` long, returns `s` unchanged (JS parity).\nfunc padTo(s string, target int, pad string, atStart bool) string {\n if pad == "" {\n return s\n }\n sLen := utf8.RuneCountInString(s)\n if sLen >= target {\n return s\n }\n need := target - sLen\n padRunes := []rune(pad)\n fill := make([]rune, 0, need)\n for len(fill) < need {\n for _, r := range padRunes {\n if len(fill) >= need {\n break\n }\n fill = append(fill, r)\n }\n }\n if atStart {\n return string(fill) + s\n }\n return s + string(fill)\n}\n\n// PadStart lowers `String.prototype.padStart(target, pad?)` (#1448 Tier\n// B). The pad string defaults to a single space when omitted.\nfunc PadStart(s string, target int, pad ...string) string {\n p := " "\n if len(pad) > 0 {\n p = pad[0]\n }\n return padTo(s, target, p, true)\n}\n\n// PadEnd lowers `String.prototype.padEnd(target, pad?)` (#1448 Tier B).\nfunc PadEnd(s string, target int, pad ...string) string {\n p := " "\n if len(pad) > 0 {\n p = pad[0]\n }\n return padTo(s, target, p, false)\n}\n\n// Join concatenates elements of a slice with sep. Accepts both\n// reflect.Slice (the common case \u2014 `bf_arr` and `bf_filter_truthy`\n// both return `[]any`) AND reflect.Array (fixed-size Go arrays like\n// `[3]string{...}`), mirroring JS `Array.prototype.join` which\n// doesn\'t distinguish between the two. Pre-fix this returned "" for\n// fixed-size arrays passed through template data (Copilot review on\n// #1445).\nfunc Join(items any, sep string) string {\n v := reflect.ValueOf(items)\n if v.Kind() != reflect.Slice && v.Kind() != reflect.Array {\n return ""\n }\n\n parts := make([]string, v.Len())\n for i := 0; i < v.Len(); i++ {\n parts[i] = toString(v.Index(i).Interface())\n }\n return strings.Join(parts, sep)\n}\n\n// Ternary returns a when cond is true, else b \u2014 the pipeline-position\n// counterpart of a template {{if}} action. Go templates have no\n// expression-level conditional, so a conditional value sitting in\n// ARGUMENT position (a lowering-node helper arg, e.g. the #2324 union\n// stage\'s locale\u2192pattern ternary) cannot be emitted as an {{if}}\n// fragment; the adapter renders it as `(bf_ternary <cond> <a> <b>)`\n// instead. Both branches are evaluated (function-call semantics) \u2014\n// fine for the value shapes the emitter feeds it, wrong for anything\n// with side effects, which template values never have.\nfunc Ternary(cond bool, a, b any) any {\n if cond {\n return a\n }\n return b\n}\n\n// String returns the string form of v. Mirrors JS `String(v)` for\n// non-nil values via `fmt.Sprintf("%v", ...)`. Diverges from JS on\n// nil: JS `String(null)` is "null", but the template path renders\n// `nil` as the empty string here so an unset prop doesn\'t surface\n// as a literal "null"/"undefined" in user-facing HTML. Document the\n// divergence explicitly so callers don\'t rely on JS-exact parity.\nfunc String(v any) string {\n if v == nil {\n return ""\n }\n return fmt.Sprintf("%v", v)\n}\n\n// RawHTML marks a value as trusted, pre-formatted HTML so html/template\'s\n// contextual escaper emits it verbatim instead of escaping it. It is the SSR\n// half of a dynamic `dangerouslySetInnerHTML={{ __html: expr }}` (#2319) \u2014\n// the one raw-output sink Go lacks as bare template syntax, the counterpart\n// to Blade `{!! !!}`, ERB `<%= %>`, Jinja/MiniJinja `| safe`, Twig `| raw`,\n// Mojolicious `<%== %>`, and Xslate `mark_raw`. The caller owns the value\'s\n// safety (React\'s "dangerously" contract); a nil value renders "".\nfunc RawHTML(v any) template.HTML {\n return template.HTML(String(v))\n}\n\n// JSON returns the JSON encoding of v as a string. Mirrors\n// JS `JSON.stringify(v)` for the V1 single-arg shape (no `replacer`\n// or `space`). Object key order is determined by Go\'s `encoding/json`\n// (alphabetical for maps, declaration order for structs) \u2014 the\n// #1187 contract requires value-compat, not order-compat.\n//\n// Top-level NaN / \xB1Inf are pre-handled to match JS \u2014 JS\'s\n// `JSON.stringify(NaN)` and `JSON.stringify(Infinity)` both produce\n// `"null"`, but Go\'s `encoding/json` rejects them with\n// `UnsupportedValueError`. Without this carve-out the common\n// composition `JSON.stringify(Number("garbage"))` would error\n// instead of emitting `"null"` like JS does. Nested NaN/Inf inside\n// a struct/map still surfaces an error \u2014 covering that needs a\n// custom marshaller; out of V1 scope.\n//\n// Returns the marshal error so a `template.Execute` call fails\n// loudly on cycles / unsupported values rather than silently\n// producing `""` and reintroducing the SSR data-loss class\n// #1187 was filed against. Go\'s text/template treats a non-nil\n// error return from a func as an execution failure.\nfunc JSON(v any) (string, error) {\n if f, ok := v.(float64); ok && (math.IsNaN(f) || math.IsInf(f, 0)) {\n return "null", nil\n }\n b, err := json.Marshal(v)\n if err != nil {\n return "", err\n }\n return string(b), nil\n}\n\n// Number coerces v to a float64. Mirrors JS `Number(v)` semantics:\n// numeric / boolean inputs convert as expected; non-numeric strings\n// and other unsupported shapes return `NaN` (matching JS rather\n// than silently substituting 0, which would mis-shape downstream\n// arithmetic and template-side comparisons). Templates that need\n// a deterministic fallback should compose with the user-side\n// default (e.g. `Number(props.x ?? 0)` in JSX).\nfunc Number(v any) float64 {\n if v == nil {\n return math.NaN()\n }\n switch x := v.(type) {\n case float64:\n return x\n case float32:\n return float64(x)\n case int:\n return float64(x)\n case int32:\n return float64(x)\n case int64:\n return float64(x)\n case bool:\n if x {\n return 1\n }\n return 0\n case string:\n f, err := strconv.ParseFloat(x, 64)\n if err != nil {\n return math.NaN()\n }\n return f\n }\n return math.NaN()\n}\n\n// Floor returns the largest integer \u2264 v as a float64. Mirrors JS\n// `Math.floor`. The return type stays float64 so chained primitives\n// (`bf_floor` then `bf_string`) line up with JS\'s number type.\nfunc Floor(v any) float64 {\n return math.Floor(Number(v))\n}\n\n// Abs returns the absolute value of v as a float64, mirroring JS\n// `Math.abs`. #2168 math-methods.\nfunc Abs(v any) float64 {\n return math.Abs(Number(v))\n}\n\n// ToFixed formats v with exactly `digits` decimal places, mirroring JS\n// `Number.prototype.toFixed` (zero-padding + half-toward-+Infinity\n// rounding). JS rounds the scaled integer half up (`(2.5).toFixed(0)`\n// is "3"); bare `fmt.Sprintf("%.*f")` rounds half-to-even ("2"), so we\n// scale, round with `Floor(x + 0.5)` (matching `Round`), then format\n// the exact multiple. #1897.\nfunc ToFixed(v any, digits int) string {\n if digits < 0 {\n digits = 0\n }\n n := Number(v)\n // JS toFixed returns the strings "NaN" / "Infinity" / "-Infinity" for\n // non-finite inputs; fmt would render "NaN"/"+Inf"/"-Inf".\n if math.IsNaN(n) {\n return "NaN"\n }\n if math.IsInf(n, 1) {\n return "Infinity"\n }\n if math.IsInf(n, -1) {\n return "-Infinity"\n }\n factor := math.Pow(10, float64(digits))\n rounded := math.Floor(n*factor + 0.5)\n return fmt.Sprintf("%.*f", digits, rounded/factor)\n}\n\n// Ceil returns the smallest integer \u2265 v as a float64. Mirrors JS\n// `Math.ceil`.\nfunc Ceil(v any) float64 {\n return math.Ceil(Number(v))\n}\n\n// Round returns v rounded to the nearest integer as a float64.\n// Mirrors JS `Math.round` \u2014 half-away-from-zero (Go\'s `math.Round`\n// matches; JS rounds half toward +Infinity which differs at .5\n// negatives; we accept that minor divergence since the conformance\n// contract is value-compat for the common positive case).\nfunc Round(v any) float64 {\n return math.Round(Number(v))\n}\n\n// =============================================================================\n// Array/Slice Operations\n// =============================================================================\n\n// Length lowers JS `.length`, matching JS semantics per receiver shape\n// (#2255): a slice/array/map counts ELEMENTS (`reflect.Value.Len`, same as\n// `Len` below), but a STRING counts UTF-16 CODE UNITS \u2014 JS\n// `String.prototype.length` counts UTF-16 code units, not bytes (Go\'s\n// native `len`) or codepoints. A codepoint outside the Basic Multilingual\n// Plane (astral, U+10000-U+10FFFF \u2014 e.g. \'\u{1F44D}\') is a surrogate PAIR in\n// UTF-16, so it counts as 2, not 1; `\'\u65E5\u672C\u8A9E\'` is 3 either way (BMP-only).\n// Routed from the `.length` member lowering\'s generic (non-array,\n// non-loop-slice) fallback \u2014 see `member()`\'s `bf_length` call site.\nfunc Length(v any) int {\n if v == nil {\n return 0\n }\n rv := reflect.ValueOf(v)\n switch rv.Kind() {\n case reflect.Slice, reflect.Array, reflect.Map, reflect.Chan:\n return rv.Len()\n case reflect.String:\n n := 0\n for _, r := range rv.String() {\n if r > 0xFFFF {\n n += 2\n } else {\n n++\n }\n }\n return n\n default:\n return 0\n }\n}\n\n// IsValidElement lowers React/Hono-style `isValidElement(x)` \u2014 the "is this\n// a renderable element (not plain text)?" predicate the `Slot` component\'s\n// `asChild` pattern (#2266) uses to decide whether to merge props into a\n// child ELEMENT (`children.tag`/`children.props`) or fall back to rendering\n// `children` as-is. The JS runtime checks `\'tag\' in x && \'props\' in x`; on\n// Go SSR a passed-through JSX child is represented as pre-rendered markup\n// (a plain string) OR \u2014 where a struct/map shape carrying `Tag`/`Props`\n// (case-insensitively, mirroring `bf_get`\'s field lookup) is available \u2014 an\n// element-shaped value. A plain string/number/bool/nil is never a valid\n// element, so `isValidElement` must NOT be lowered as bare truthiness\n// (previously done via `renderConditionExpr`) \u2014 a truthy non-empty STRING\n// child wrongly took the element-merge branch and panicked dereferencing\n// `.Props` on a string (`can\'t evaluate field Props in type interface {}`).\nfunc IsValidElement(v any) bool {\n rv := reflect.ValueOf(v)\n for rv.Kind() == reflect.Interface || rv.Kind() == reflect.Pointer {\n if rv.IsNil() {\n return false\n }\n rv = rv.Elem()\n }\n switch rv.Kind() {\n case reflect.Map:\n hasTag, hasProps := false, false\n for _, k := range rv.MapKeys() {\n key := fmt.Sprintf("%v", k.Interface())\n if strings.EqualFold(key, "tag") {\n hasTag = true\n }\n if strings.EqualFold(key, "props") {\n hasProps = true\n }\n }\n return hasTag && hasProps\n case reflect.Struct:\n return fieldByFoldedName(rv, "tag").IsValid() && fieldByFoldedName(rv, "props").IsValid()\n default:\n return false\n }\n}\n\n// fieldByFoldedName finds a struct field by case-insensitive name match \u2014\n// shared by IsValidElement; mirrors getFieldValue\'s (bf_get) struct-branch\n// lookup so the two case-tolerant field resolutions stay consistent.\nfunc fieldByFoldedName(rv reflect.Value, name string) reflect.Value {\n t := rv.Type()\n for i := 0; i < t.NumField(); i++ {\n if strings.EqualFold(t.Field(i).Name, name) {\n return rv.Field(i)\n }\n }\n return reflect.Value{}\n}\n\n// Len returns the length of a slice, array, map, string, or channel.\n// Returns 0 for nil or unsupported types.\nfunc Len(v any) int {\n if v == nil {\n return 0\n }\n rv := reflect.ValueOf(v)\n switch rv.Kind() {\n case reflect.Slice, reflect.Array, reflect.Map, reflect.String, reflect.Chan:\n return rv.Len()\n default:\n return 0\n }\n}\n\n// At returns the element at index i from a slice.\n// Supports negative indices (e.g., -1 for last element).\n// Returns nil if index is out of bounds.\nfunc At(items any, index int) any {\n v := reflect.ValueOf(items)\n if v.Kind() != reflect.Slice && v.Kind() != reflect.Array {\n return nil\n }\n\n length := v.Len()\n if length == 0 {\n return nil\n }\n\n // Handle negative indices\n if index < 0 {\n index = length + index\n }\n\n if index < 0 || index >= length {\n return nil\n }\n\n return v.Index(index).Interface()\n}\n\n// Includes returns true if items contains elem. Lowers both\n// `Array.prototype.includes` and `String.prototype.includes` \u2014\n// the adapter can\'t disambiguate the receiver at compile time,\n// so this helper dispatches at runtime on `reflect.Kind()`:\n//\n// - slice/array receiver: SameValueZero element search (matches\n// the evaluator\'s `evalSameValueZero`/`evalIncludes` in eval.go,\n// which back the serialized-callback path) \u2014 numeric types compare\n// by value across int/float64 the way JS\'s single "number" type\n// does, and NaN matches NaN (unlike `===`). This used to be\n// `reflect.DeepEqual`, which is type-strict (`int(2)` != `float64(2)`)\n// and never matches NaN to NaN; that diverged from the evaluator\'s\n// `.includes` and from JS itself, so it was unified here.\n// - string receiver: strings.Contains substring search\n//\n// Anything else returns false (matches the JS semantic where\n// `.includes` is only defined on Array / TypedArray / String).\nfunc Includes(recv any, elem any) bool {\n v := reflect.ValueOf(recv)\n if v.Kind() == reflect.String {\n // JS `String.prototype.includes` accepts only string args;\n // non-string `elem` would TypeError in real JS but our\n // callers have lowered through `convertExpressionToGo`\n // where the arg type is whatever the template binds. Stringify\n // via fmt to keep the helper total.\n needle, ok := elem.(string)\n if !ok {\n needle = fmt.Sprintf("%v", elem)\n }\n return strings.Contains(v.String(), needle)\n }\n if v.Kind() != reflect.Slice && v.Kind() != reflect.Array {\n return false\n }\n for i := 0; i < v.Len(); i++ {\n if evalSameValueZero(v.Index(i).Interface(), elem) {\n return true\n }\n }\n return false\n}\n\n// IndexOf returns the 0-based position of the first item that\n// DeepEquals `elem`, or -1 if not found. Lowers\n// `Array.prototype.indexOf(x)` (#1448 Tier A). The existing\n// `FindIndex` helper does struct-field equality (used by the\n// higher-order `.find` lowering); this one does value equality\n// against scalar / struct items so callers don\'t have to compose\n// a synthetic predicate.\n//\n// Non-array / non-slice receivers return -1 (matches the JS\n// semantic that `.indexOf` is only defined on Array / TypedArray).\nfunc IndexOf(items any, elem any) int {\n v := reflect.ValueOf(items)\n if v.Kind() != reflect.Slice && v.Kind() != reflect.Array {\n return -1\n }\n for i := 0; i < v.Len(); i++ {\n if reflect.DeepEqual(v.Index(i).Interface(), elem) {\n return i\n }\n }\n return -1\n}\n\n// LastIndexOf returns the 0-based position of the last item that\n// DeepEquals `elem`, or -1 if not found. Mirrors\n// `Array.prototype.lastIndexOf(x)`. The reverse traversal is the\n// only behavioural difference vs `IndexOf` \u2014 disambiguating a\n// duplicated value\'s first vs last position is the canonical\n// reason a JS author reaches for `lastIndexOf`.\nfunc LastIndexOf(items any, elem any) int {\n v := reflect.ValueOf(items)\n if v.Kind() != reflect.Slice && v.Kind() != reflect.Array {\n return -1\n }\n for i := v.Len() - 1; i >= 0; i-- {\n if reflect.DeepEqual(v.Index(i).Interface(), elem) {\n return i\n }\n }\n return -1\n}\n\n// Concat merges two arrays (or slices) into a single `[]any`,\n// preserving order: receiver elements first, then `other`\'s.\n// Lowers `Array.prototype.concat(other)` (#1448 Tier A). Non-array\n// operands collapse to an empty source \u2014 matches the JS semantic\n// where `.concat` on a non-Array reads it as a single element only\n// if its `Symbol.isConcatSpreadable` is true; the template-language\n// path doesn\'t have user objects with that flag, so treating\n// non-arrays as empty is the conservative lowering. Variadic\n// `.concat(a, b, c)` is out of scope here (parser gates to a single\n// arg); the helper itself stays binary so a future variadic IR can\n// fold via repeated calls without changing this signature.\nfunc Concat(a, b any) []any {\n flatten := func(v reflect.Value) []any {\n if !v.IsValid() {\n return nil\n }\n if v.Kind() != reflect.Slice && v.Kind() != reflect.Array {\n return nil\n }\n out := make([]any, v.Len())\n for i := 0; i < v.Len(); i++ {\n out[i] = v.Index(i).Interface()\n }\n return out\n }\n left := flatten(reflect.ValueOf(a))\n right := flatten(reflect.ValueOf(b))\n return append(left, right...)\n}\n\n// clampSliceRange normalizes JS `.slice(start, end?)` bounds against a\n// receiver of `length` elements (runes, for the string branch of\n// `Slice` below; array elements, for the array branch) \u2014 shared so\n// both branches clamp identically.\n//\n// JS-compat clamping:\n// - start < 0 \u2192 length + start (e.g. -1 = last index)\n// - end < 0 \u2192 length + end\n// - start < 0 after clamp \u2192 0\n// - end > length \u2192 length\nfunc clampSliceRange(length, start int, end []int) (int, int) {\n if start < 0 {\n start = length + start\n }\n if start < 0 {\n start = 0\n }\n if start > length {\n start = length\n }\n\n stop := length\n if len(end) > 0 {\n stop = end[0]\n if stop < 0 {\n stop = length + stop\n }\n if stop < 0 {\n stop = 0\n }\n if stop > length {\n stop = length\n }\n }\n return start, stop\n}\n\n// Slice carves out a sub-range from `items`. Lowers\n// `Array.prototype.slice(start, end?)` (#1448 Tier A) AND\n// `String.prototype.slice(start, end?)` (the `string-slice`\n// divergence) \u2014 the adapter emits the same `bf_slice` call for both\n// receiver shapes (it can\'t disambiguate string vs. array at compile\n// time), so this helper dispatches at runtime on `reflect.Kind()`,\n// mirroring `Includes` above. The variadic `end` arg lets Go\n// template\'s call dispatcher pass either 2 or 3 arguments; an absent\n// end means "to length".\n//\n// String length/positions are measured in runes, not UTF-16 code\n// units \u2014 the same divergence boundary `padTo` already accepts\n// (differs from JS only for astral-plane input). `start >= end`\n// (after clamping) returns an empty result for either receiver.\n//\n// Any other receiver kind returns an empty `[]any`.\nfunc Slice(items any, start int, end ...int) any {\n v := reflect.ValueOf(items)\n\n if v.Kind() == reflect.String {\n runes := []rune(v.String())\n s, e := clampSliceRange(len(runes), start, end)\n if s >= e {\n return ""\n }\n return string(runes[s:e])\n }\n\n if v.Kind() != reflect.Slice && v.Kind() != reflect.Array {\n return []any{}\n }\n s, e := clampSliceRange(v.Len(), start, end)\n if s >= e {\n return []any{}\n }\n out := make([]any, 0, e-s)\n for i := s; i < e; i++ {\n out = append(out, v.Index(i).Interface())\n }\n return out\n}\n\n// Reverse returns a new slice with `items`\'s elements in reverse\n// order. Lowers both `Array.prototype.reverse()` and\n// `Array.prototype.toReversed()` (#1448 Tier A) \u2014 SSR templates\n// render a snapshot, so JS\'s mutate-receiver vs return-new-array\n// distinction has no template-level meaning, and the safer\n// non-mutating shape is used uniformly.\n//\n// Non-array receivers return an empty `[]any`.\nfunc Reverse(items any) []any {\n v := reflect.ValueOf(items)\n if v.Kind() != reflect.Slice && v.Kind() != reflect.Array {\n return []any{}\n }\n length := v.Len()\n out := make([]any, length)\n for i := 0; i < length; i++ {\n out[length-1-i] = v.Index(i).Interface()\n }\n return out\n}\n\n// Flat flattens nested slices/arrays `depth` levels deep. Lowers\n// `Array.prototype.flat(depth?)` (#1448 Tier C). A `depth` of `-1` is the\n// `Infinity` sentinel (flatten fully); `0` (or negative-from-JS, already\n// normalised to 0 at compile time) returns a shallow copy. Non-array\n// elements are kept as-is (JS only flattens nested arrays). A non-array\n// receiver returns an empty `[]any`.\nfunc Flat(items any, depth int) []any {\n v := reflect.ValueOf(items)\n if v.Kind() != reflect.Slice && v.Kind() != reflect.Array {\n return []any{}\n }\n out := make([]any, 0, v.Len())\n for i := 0; i < v.Len(); i++ {\n el := v.Index(i).Interface()\n ev := reflect.ValueOf(el)\n if depth != 0 && (ev.Kind() == reflect.Slice || ev.Kind() == reflect.Array) {\n // `-1` (Infinity) recurses unbounded; a finite depth spends one level.\n next := depth\n if depth > 0 {\n next = depth - 1\n }\n out = append(out, Flat(el, next)...)\n } else {\n out = append(out, el)\n }\n }\n return out\n}\n\n// FlatDynamicDepth coerces `depth` via JS\'s `ToIntegerOrInfinity` and\n// flattens `items` that many levels. Lowers a DYNAMIC `.flat(depth)`\n// (#2094) \u2014 one whose depth isn\'t a compile-time literal, so (unlike\n// `Flat` above) the coercion happens here at render time instead of in the\n// parser.\n//\n// This is a SEPARATE helper from `Flat`/`bf_flat` \u2014 NOT a drop-in\n// replacement \u2014 because `Flat`\'s `depth int` parameter treats `-1` as a\n// compile-time SENTINEL meaning "flatten fully" (the parser\'s own\n// normalisation of a literal `Infinity`). A genuinely dynamic depth value\n// of `-1` means the JS-correct OPPOSITE: `Array.prototype.flat(-1)` never\n// recurses (same as `.flat(0)`, a shallow copy), because\n// `FlattenIntoArray` only recurses when `depth > 0`. Reusing `Flat`\'s int\n// contract for a raw dynamic value would silently invert that case, so\n// this function coerces FIRST \u2014 mapping a real `+Infinity` / huge finite\n// value to `Flat`\'s own `-1` sentinel, and a real negative value to `0` \u2014\n// and only then delegates to `Flat`\'s recursion.\n//\n// Coercion rules (JS `ToIntegerOrInfinity`, mirrored exactly; pinned by the\n// `flat_dynamic` golden-vector cases in\n// packages/adapter-tests/vectors/cases.ts):\n// - the value converts via `ToNumber` first (numeric string / bool /\n// number all coerce; see `flatDepthToFloat`);\n// - a NaN result (including a non-numeric string) \u2192 `0`;\n// - truncates toward zero (`2.7` \u2192 `2`);\n// - negative \u2192 `0`;\n// - `+Infinity` / a huge finite value \u2192 flattens fully.\nfunc FlatDynamicDepth(items any, depth any) []any {\n return Flat(items, coerceFlatDepth(depth))\n}\n\n// coerceFlatDepth implements JS\'s `ToIntegerOrInfinity` for a dynamic\n// `.flat(depth)` argument, returning an int in `Flat`\'s own contract (`-1`\n// = unbounded, `>= 0` = that many levels).\nfunc coerceFlatDepth(depth any) int {\n f, ok := flatDepthToFloat(depth)\n if !ok || math.IsNaN(f) {\n return 0\n }\n if math.IsInf(f, 1) {\n return -1 // Flat\'s "flatten fully" sentinel\n }\n if math.IsInf(f, -1) {\n return 0\n }\n trunc := math.Trunc(f)\n if trunc < 0 {\n return 0\n }\n // A huge finite depth behaves identically to "flatten fully" in\n // practice \u2014 real data bottoms out at its actual nesting depth long\n // before a counter this large would ever reach zero. Capping it here\n // avoids an absurd countdown without needing a second sentinel.\n if trunc > 1_000_000 {\n return -1\n }\n return int(trunc)\n}\n\n// flatDepthToFloat converts a dynamic `.flat(depth)` argument to a float64,\n// mirroring JS\'s `ToNumber` across the value shapes a Go template data\n// model can carry (every numeric kind, bool, numeric string). `ok` is\n// false for a shape `ToNumber` can\'t coerce meaningfully (`nil`, or a\n// non-numeric string) \u2014 `coerceFlatDepth` treats that the same as NaN\n// (\u2192 depth `0`), matching JS.\nfunc flatDepthToFloat(v any) (float64, bool) {\n switch n := v.(type) {\n case nil:\n return 0, false\n case float64:\n return n, true\n case float32:\n return float64(n), true\n case int:\n return float64(n), true\n case int8:\n return float64(n), true\n case int16:\n return float64(n), true\n case int32:\n return float64(n), true\n case int64:\n return float64(n), true\n case uint:\n return float64(n), true\n case uint8:\n return float64(n), true\n case uint16:\n return float64(n), true\n case uint32:\n return float64(n), true\n case uint64:\n return float64(n), true\n case bool:\n if n {\n return 1, true\n }\n return 0, true\n case string:\n s := strings.TrimSpace(n)\n if s == "" {\n return 0, true // JS: Number("") is 0\n }\n f, err := strconv.ParseFloat(s, 64)\n if err != nil {\n return 0, false // not numeric \u2192 NaN path\n }\n return f, true\n default:\n return 0, false\n }\n}\n\n// FlatMap projects each element through a `self` / `field` projection and\n// flattens the result one level. Lowers value-returning\n// `Array.prototype.flatMap(fn)` for the field-projection catalogue\n// (#1448 Tier C): `items.flatMap(i => i)` (self) and\n// `items.flatMap(i => i.field)` (field). A projected non-array value is\n// kept as-is (flatMap = map + flat(1)). Non-array receiver \u2192 empty.\nfunc FlatMap(items any, keyKind, keyName string) []any {\n v := reflect.ValueOf(items)\n if v.Kind() != reflect.Slice && v.Kind() != reflect.Array {\n return []any{}\n }\n projected := make([]any, 0, v.Len())\n for i := 0; i < v.Len(); i++ {\n el := v.Index(i).Interface()\n if keyKind == "field" {\n projected = append(projected, getFieldValue(el, keyName))\n } else {\n projected = append(projected, el)\n }\n }\n return Flat(projected, 1)\n}\n\n// FlatMapTuple lowers an array-literal flatMap projection\n// `items.flatMap(i => [i.a, i.b])` (#1448 Tier C). `specs` is a flat list\n// of (kind, name) pairs, one per array-literal leaf: ("self", "") for the\n// item itself, ("field", "<Name>") for a struct field. For each item it\n// appends every leaf\'s value in order. Unlike the scalar `FlatMap`, the\n// per-item array is flattened only one level (flat(1) removes the literal\n// wrapper), so an array-valued leaf is appended verbatim rather than\n// spread \u2014 which is exactly "append each leaf". Non-array receiver \u2192 empty.\nfunc FlatMapTuple(items any, specs ...string) []any {\n v := reflect.ValueOf(items)\n if v.Kind() != reflect.Slice && v.Kind() != reflect.Array {\n return []any{}\n }\n out := make([]any, 0, v.Len())\n for i := 0; i < v.Len(); i++ {\n el := v.Index(i).Interface()\n for j := 0; j+1 < len(specs); j += 2 {\n if specs[j] == "field" {\n out = append(out, getFieldValue(el, specs[j+1]))\n } else {\n out = append(out, el)\n }\n }\n }\n return out\n}\n\n// First returns the first element of a slice, or nil if empty.\nfunc First(items any) any {\n return At(items, 0)\n}\n\n// Last returns the last element of a slice, or nil if empty.\nfunc Last(items any) any {\n return At(items, -1)\n}\n\n// Arr builds an []any from variadic args. Used to lower JS array\n// literals like `[a, b]` for the registry Slot\'s\n// `[className, childClass].filter(Boolean).join(\' \')` shape (#1443) \u2014\n// Go templates have no array-literal syntax, so the codegen routes\n// array-literal IR nodes through this helper.\nfunc Arr(items ...any) []any {\n return items\n}\n\n// FilterTruthy returns a new slice containing only truthy items.\n// Mirrors `arr.filter(Boolean)` semantics: drop nil, false, 0, "" \u2014 the\n// same falsy set JavaScript\'s `Boolean(x)` recognises. Used to lower\n// the registry Slot\'s class-merge pattern (#1443); generalising to\n// arbitrary callable predicates would need the callee-resolution path\n// blocked by #1389, so this stays Boolean-specific.\nfunc FilterTruthy(items any) []any {\n v := reflect.ValueOf(items)\n if !v.IsValid() || (v.Kind() != reflect.Slice && v.Kind() != reflect.Array) {\n return nil\n }\n result := make([]any, 0, v.Len())\n for i := 0; i < v.Len(); i++ {\n raw := v.Index(i).Interface()\n if isTruthy(raw) {\n result = append(result, raw)\n }\n }\n return result\n}\n\n// Truthy is the exported form of isTruthy \u2014 JavaScript\'s `Boolean(x)`\n// semantics. Two callers:\n// - generated `NewXxxProps` code lowering a conditional inline-object\n// spread condition on an `interface{}` prop (whose runtime value may be\n// a string, number, bool, \u2026), keeping the spread bag\'s inclusion test\n// faithful to JS rather than string-biased (#1752); and\n// - the `bf_truthy` template FuncMap entry (#2335), which coerces a\n// `bf_ternary` test to a real bool when it isn\'t already a comparison /\n// negation (`Ternary`\'s `cond` parameter is typed `bool`, unlike\n// `{{if}}`\'s built-in truthiness). Uniform across string/number/bool/nil,\n// so a `bf_ternary` test on any prop type can\'t hit a `bool`-vs-`string`\n// comparison error the way a string-only `ne <value> ""` would.\nfunc Truthy(v any) bool { return isTruthy(v) }\n\n// isTruthy mirrors JavaScript\'s `Boolean(x)` for the value shapes the\n// template path actually receives \u2014 nil / false / 0 / "" are falsy.\n// Other shapes (non-empty maps, slices, structs, true) are truthy, in\n// line with JS\'s "objects are truthy" rule.\nfunc isTruthy(v any) bool {\n if v == nil {\n return false\n }\n switch x := v.(type) {\n case bool:\n return x\n case string:\n return x != ""\n case int:\n return x != 0\n case int8, int16, int32, int64:\n return reflect.ValueOf(v).Int() != 0\n case uint, uint8, uint16, uint32, uint64:\n return reflect.ValueOf(v).Uint() != 0\n case float32:\n // JS `Boolean(NaN)` is false regardless of float width \u2014 the\n // float64 arm below was the only one checking IsNaN, which\n // diverged from JS for `float32` NaN inputs (Copilot review on\n // #1445). Widening to float64 for the IsNaN check keeps the\n // two branches in lock-step.\n return x != 0 && !math.IsNaN(float64(x))\n case float64:\n return x != 0 && !math.IsNaN(x)\n }\n return true\n}\n\n// =============================================================================\n// Higher-order Array Methods\n// =============================================================================\n\n// fieldValue projects item.field for the higher-order predicate\n// helpers. The field name arrives in JS casing; structs resolve via\n// the capitalized Go convention (FieldByName inside getFieldValue),\n// maps via getFieldValue\'s case-variant lookup \u2014 the same dual\n// support Sort/Reduce gained in #1487, extended here so JSON-decoded\n// data (map items) participates instead of being silently skipped.\n// nil-safe: missing fields and nil items project to nil.\nfunc fieldValue(item any, field string) any {\n return getFieldValue(item, capitalize(field))\n}\n\n// Every returns true if every item\'s field is truthy under JS\n// `Boolean(item.field)` semantics. Mirrors JavaScript\'s\n// Array.prototype.every(item => item.field) \u2014 including being\n// vacuously true for an empty receiver.\nfunc Every(items any, field string) bool {\n v := reflect.ValueOf(items)\n if v.Kind() != reflect.Slice && v.Kind() != reflect.Array {\n return false\n }\n for i := 0; i < v.Len(); i++ {\n if !isTruthy(fieldValue(v.Index(i).Interface(), field)) {\n return false\n }\n }\n return true\n}\n\n// Some returns true if at least one item\'s field is truthy. Mirrors\n// JavaScript\'s Array.prototype.some(item => item.field).\nfunc Some(items any, field string) bool {\n v := reflect.ValueOf(items)\n if v.Kind() != reflect.Slice && v.Kind() != reflect.Array {\n return false\n }\n for i := 0; i < v.Len(); i++ {\n if isTruthy(fieldValue(v.Index(i).Interface(), field)) {\n return true\n }\n }\n return false\n}\n\n// Filter returns items where item.field == value.\n// Mirrors JavaScript\'s Array.prototype.filter(item => item.field === value).\n// Returns []any to allow chaining with other bf_* functions.\nfunc Filter(items any, field string, value any) []any {\n v := reflect.ValueOf(items)\n if v.Kind() != reflect.Slice && v.Kind() != reflect.Array {\n return nil\n }\n var result []any\n for i := 0; i < v.Len(); i++ {\n item := v.Index(i).Interface()\n if reflect.DeepEqual(fieldValue(item, field), value) {\n result = append(result, item)\n }\n }\n return result\n}\n\n// Find returns the first item where item.field == value, or nil if not found.\n// Mirrors JavaScript\'s Array.prototype.find(item => item.field === value).\nfunc Find(items any, field string, value any) any {\n v := reflect.ValueOf(items)\n if v.Kind() != reflect.Slice && v.Kind() != reflect.Array {\n return nil\n }\n for i := 0; i < v.Len(); i++ {\n item := v.Index(i).Interface()\n if reflect.DeepEqual(fieldValue(item, field), value) {\n return item\n }\n }\n return nil\n}\n\n// FindIndex returns the index of the first item where item.field == value, or -1.\n// Mirrors JavaScript\'s Array.prototype.findIndex(item => item.field === value).\nfunc FindIndex(items any, field string, value any) int {\n v := reflect.ValueOf(items)\n if v.Kind() != reflect.Slice && v.Kind() != reflect.Array {\n return -1\n }\n for i := 0; i < v.Len(); i++ {\n if reflect.DeepEqual(fieldValue(v.Index(i).Interface(), field), value) {\n return i\n }\n }\n return -1\n}\n\n// FindLast returns the last item where item.field == value, or nil if not found.\n// Mirrors JavaScript\'s Array.prototype.findLast(item => item.field === value).\nfunc FindLast(items any, field string, value any) any {\n v := reflect.ValueOf(items)\n if v.Kind() != reflect.Slice && v.Kind() != reflect.Array {\n return nil\n }\n for i := v.Len() - 1; i >= 0; i-- {\n item := v.Index(i).Interface()\n if reflect.DeepEqual(fieldValue(item, field), value) {\n return item\n }\n }\n return nil\n}\n\n// FindLastIndex returns the index of the last item where item.field == value, or -1.\n// Mirrors JavaScript\'s Array.prototype.findLastIndex(item => item.field === value).\nfunc FindLastIndex(items any, field string, value any) int {\n v := reflect.ValueOf(items)\n if v.Kind() != reflect.Slice && v.Kind() != reflect.Array {\n return -1\n }\n for i := v.Len() - 1; i >= 0; i-- {\n if reflect.DeepEqual(fieldValue(v.Index(i).Interface(), field), value) {\n return i\n }\n }\n return -1\n}\n\n// sortKeySpec is one parsed comparison key. A simple comparator has\n// one; a `||`-chained multi-key comparator has several, applied in\n// order as tie-breakers.\ntype sortKeySpec struct {\n kind string // "self" | "field"\n name string // capitalised field name, or "" for "self"\n compareType string // "numeric" | "string" | "auto"\n direction string // "asc" | "desc"\n}\n\n// Sort returns a new stable-sorted slice. Lowers\n// `Array.prototype.sort` / `Array.prototype.toSorted` (#1448 Tier B).\n// Non-mutating \u2014 JS\'s mutate-vs-new distinction is moot in SSR\n// template context (templates render a snapshot).\n//\n// Call shape (the compiler emits one 4-string group per key):\n//\n// bf_sort <items> (<keyKind> <keyName> <compareType> <direction>)+\n//\n// keyKind: "self" | "field"\n// keyName: "" when keyKind == "self"; capitalised struct field\n// name (e.g. "Price") otherwise\n// compareType: "numeric" | "string" | "auto"\n// direction: "asc" | "desc"\n//\n// The groups cover the accepted comparator catalogue: `a.f - b.f`,\n// `a - b`, `a[.f].localeCompare(b[.f])`, and relational-ternary keys\n// (`a.f > b.f ? 1 : -1` \u2192 "auto"), each `||`-chainable for multi-key\n// tie-breaks. Anything outside refuses at compile time (BF101 from the\n// JSX compiler) and never reaches this helper.\n//\n// "auto" compares numerically when both projected keys parse as\n// numbers, else lexically \u2014 mirroring the Perl `bf->sort` helper\'s\n// `looks_like_number` rule so the two template adapters stay\n// byte-equal. This diverges from JS `<`/`>` only for numeric strings.\n//\n// A future `nulls` knob can extend the per-key group without rewriting\n// existing call sites \u2014 each key already projects before comparing.\nfunc Sort(items any, spec ...string) []any {\n v := reflect.ValueOf(items)\n if v.Kind() != reflect.Slice && v.Kind() != reflect.Array {\n return nil\n }\n\n length := v.Len()\n if length == 0 {\n return []any{}\n }\n\n // Copy into a fresh []any so the sort is non-mutating regardless\n // of whether the receiver is `[]T` or `[]any`.\n result := make([]any, length)\n for i := 0; i < length; i++ {\n result[i] = v.Index(i).Interface()\n }\n\n keys := parseSortSpec(spec)\n sort.SliceStable(result, func(i, j int) bool {\n for _, k := range keys {\n ki := projectSortKey(result[i], k.kind, k.name)\n kj := projectSortKey(result[j], k.kind, k.name)\n c := compareSortKey(ki, kj, k.compareType)\n if c == 0 {\n continue // tie on this key \u2014 fall through to the next\n }\n if k.direction == "desc" {\n return c > 0\n }\n return c < 0\n }\n return false\n })\n\n return result\n}\n\n// parseSortSpec chunks the variadic operand list into 4-string key\n// groups. A trailing partial group (malformed emit) is ignored rather\n// than panicking \u2014 defensive, mirroring the helper\'s nil-safe stance.\nfunc parseSortSpec(spec []string) []sortKeySpec {\n var keys []sortKeySpec\n for i := 0; i+3 < len(spec); i += 4 {\n keys = append(keys, sortKeySpec{\n kind: spec[i],\n name: spec[i+1],\n compareType: spec[i+2],\n direction: spec[i+3],\n })\n }\n return keys\n}\n\n// compareSortKey returns -1 / 0 / 1 for two projected keys under the\n// given compare type (ascending orientation; the caller flips for\n// "desc"). "string" stringifies both (nil \u2192 "", matching the\n// documented `bf->string(undef) === ""` divergence). "auto" compares\n// numerically when both parse as numbers, else lexically.\nfunc compareSortKey(ki, kj any, compareType string) int {\n switch compareType {\n case "string":\n return strings.Compare(toString(ki), toString(kj))\n case "auto":\n ni, okI := toFloat64WithOK(ki)\n nj, okJ := toFloat64WithOK(kj)\n if okI && okJ {\n return cmpFloat(ni, nj)\n }\n return strings.Compare(toString(ki), toString(kj))\n default: // numeric\n return cmpFloat(toFloat64(ki), toFloat64(kj))\n }\n}\n\nfunc cmpFloat(a, b float64) int {\n if a < b {\n return -1\n }\n if a > b {\n return 1\n }\n return 0\n}\n\n// toFloat64WithOK reports a value\'s numeric float and whether it is\n// number-like. Genuine numeric kinds always qualify; strings qualify\n// when they parse as a float (so the "auto" compare path matches the\n// Perl `looks_like_number` rule). Everything else is non-numeric.\nfunc toFloat64WithOK(v any) (float64, bool) {\n switch n := v.(type) {\n case int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64, float32, float64:\n return toFloat64(v), true\n case string:\n f, err := strconv.ParseFloat(strings.TrimSpace(n), 64)\n if err != nil {\n return 0, false\n }\n return f, true\n default:\n return 0, false\n }\n}\n\n// projectSortKey reduces an item to the value the comparator\n// actually compares. For `keyKind == "field"` it reads the named\n// struct field; for `keyKind == "self"` (primitive arrays) it\n// returns the item unchanged.\nfunc projectSortKey(item any, keyKind, keyName string) any {\n if keyKind == "field" {\n return getFieldValue(item, keyName)\n }\n return item\n}\n\n// getFieldValue extracts a struct field value using reflection. For\n// map receivers it falls back to case-variant lookup so JSON-decoded\n// user data (`map[string]any{"price": 30}`) and PascalCase-emitted\n// test data both resolve under a single key name. (#1487)\nfunc getFieldValue(item any, field string) any {\n v := reflect.ValueOf(item)\n // Defensive IsNil guards mirror `SpreadAttrs` \u2014 keeps the helper\n // safe against typed-nil pointer / nil-interface items inside a\n // `[]any` so a single bad row doesn\'t crash the whole sort.\n if v.Kind() == reflect.Interface {\n if v.IsNil() {\n return nil\n }\n v = v.Elem()\n }\n if v.Kind() == reflect.Ptr {\n if v.IsNil() {\n return nil\n }\n v = v.Elem()\n }\n\n if v.Kind() == reflect.Map {\n keyType := v.Type().Key()\n if keyType.Kind() != reflect.String {\n return nil\n }\n // Convert the lookup string to the map\'s actual key type so\n // maps keyed by a named string type (`type Key string`) don\'t\n // panic with `value of type string is not assignable to type X`.\n lookup := func(s string) (any, bool) {\n k := reflect.ValueOf(s).Convert(keyType)\n if mv := v.MapIndex(k); mv.IsValid() {\n return mv.Interface(), true\n }\n return nil, false\n }\n if r, ok := lookup(field); ok {\n return r\n }\n if cap := capitalize(field); cap != field {\n if r, ok := lookup(cap); ok {\n return r\n }\n }\n if low := decapitalize(field); low != field {\n if r, ok := lookup(low); ok {\n return r\n }\n }\n // All-lowercase fallback: a Go-initialism field projects as an\n // all-caps key (`id` \u2192 `ID`), and `decapitalize("ID")` only\n // lowers the first char (`iD`), so the JS-keyed map ("id") still\n // misses. Try the fully-lowered key last to resolve it.\n if lower := strings.ToLower(field); lower != field && lower != decapitalize(field) {\n if r, ok := lookup(lower); ok {\n return r\n }\n }\n return nil\n }\n\n if v.Kind() != reflect.Struct {\n return nil\n }\n\n fieldVal := v.FieldByName(field)\n if !fieldVal.IsValid() {\n // Case-variant fallback: the evaluator carries the JS field name\n // (`id` / `url`) against a Go-capitalised struct field (`ID` / `URL`),\n // which exact `FieldByName` misses and the initialism rules can\'t be\n // reproduced char-for-char here. Match case-insensitively instead \u2014\n // `FieldByNameFunc` returns the zero Value (\u2192 nil) for an ambiguous\n // match, so it stays safe. The legacy bf_sort/bf_reduce pass an\n // already-capitalised name, so they hit the exact match above and never\n // reach this fallback.\n fieldVal = v.FieldByNameFunc(func(n string) bool { return strings.EqualFold(n, field) })\n if !fieldVal.IsValid() {\n return nil\n }\n }\n return fieldVal.Interface()\n}\n\n// AsMap normalizes a dynamically-typed prop value into a\n// map[string]interface{} for object-valued context bindings\n// (`lowerProviderMapMemberValue`, go-template-adapter.ts). A caller-side\n// `interface{}` field can legally hold ANY string-keyed map kind \u2014 a Go\n// handler modelling `Record<string, string>` naturally passes\n// map[string]string \u2014 so a bare `.(map[string]interface{})` type assertion\n// would silently drop provided values (#2111 review). Returns nil (never an\n// empty map) when the value is absent \u2014 nil interface, typed-nil map or\n// pointer, or any non-map / non-string-keyed value \u2014 so the generated\n// `?? {}` fallback can distinguish "missing" (fall back) from "present but\n// empty" (use as-is). map[string]interface{} passes through without copying.\nfunc AsMap(v any) map[string]interface{} {\n if v == nil {\n return nil\n }\n if m, ok := v.(map[string]interface{}); ok {\n if m == nil {\n return nil\n }\n return m\n }\n rv := reflect.ValueOf(v)\n if rv.Kind() == reflect.Ptr {\n if rv.IsNil() {\n return nil\n }\n rv = rv.Elem()\n }\n if rv.Kind() != reflect.Map || rv.Type().Key().Kind() != reflect.String || rv.IsNil() {\n return nil\n }\n out := make(map[string]interface{}, rv.Len())\n iter := rv.MapRange()\n for iter.Next() {\n out[iter.Key().String()] = iter.Value().Interface()\n }\n return out\n}\n\n// capitalize uppercases the first character of a string.\nfunc capitalize(s string) string {\n if s == "" {\n return s\n }\n return strings.ToUpper(s[:1]) + s[1:]\n}\n\n// decapitalize lowercases the first character of a string. Used by\n// `getFieldValue`\'s map-receiver fallback when the projected key\n// name is PascalCase but the receiver carries lowercase JS-style\n// keys (the inverse of the `capitalize` lookup).\nfunc decapitalize(s string) string {\n if s == "" {\n return s\n }\n return strings.ToLower(s[:1]) + s[1:]\n}\n\n// Reduce folds an array into a scalar via the arithmetic-fold\n// catalogue (#1448 Tier C). It lowers `Array.prototype.reduce(fn, init)`\n// and `Array.prototype.reduceRight(fn, init)` for the shapes\n// `(acc, x) => acc <op> x` and `(acc, x) => acc <op> x.field`:\n//\n// bf_reduce <items> "<op>" "<keyKind>" "<keyName>" "<type>" "<init>" "<direction>"\n//\n// direction: "left" (reduce) | "right" (reduceRight). Only changes the\n// result for string concatenation; numeric folds commute.\n//\n// op: "+" | "*"\n// keyKind: "self" | "field"\n// keyName: "" when keyKind == "self"; capitalised struct field name\n// (e.g. "Duration") otherwise\n// type: "numeric" | "string"\n// init: the fold\'s start value \u2014 the compiler emits the *decoded*\n// seed, so numeric inits arrive as canonical decimal\n// (`1_000`/`0x10` already normalised to `1000`/`16`) that\n// ParseFloat accepts, and string inits arrive as escape-free\n// contents\n//\n// Numeric folds accumulate as float64; each projected key is read via\n// `toFloat64WithOK`, so numeric *strings* ("5" \u2192 5) parse and\n// non-numeric values fold as 0 \u2014 matching Perl\'s\n// `looks_like_number ? $n : 0` so the two template adapters stay\n// byte-equal. String folds concatenate (toString per projected key,\n// matching the documented `bf->string(undef) === ""` convention). The\n// init seeds the accumulator, so an empty receiver returns the init\n// unchanged \u2014 exactly like JS `reduce(fn, init)`. Anything outside the\n// catalogue refuses at compile time (BF101 from the JSX compiler) and\n// never reaches here.\n//\n// Two documented divergences from the JS / Hono path, both rare and\n// mirroring the `bf_sort` "auto" caveat:\n// - float64 stringification differs for sums whose binary expansion\n// isn\'t exact (e.g. 0.1 + 0.2);\n// - numeric-*string* keys fold numerically here, but JS `+`\n// string-concatenates once an operand is a string, so\n// numeric-string data can render differently under CSR.\n//\n// Genuine numbers \u2014 the common SSR case \u2014 agree across all three.\nfunc Reduce(items any, op, keyKind, keyName, typ, init, direction string) any {\n v := reflect.ValueOf(items)\n isSlice := v.Kind() == reflect.Slice || v.Kind() == reflect.Array\n\n // `direction == "right"` (reduceRight) folds right-to-left. Only\n // observable for string concatenation \u2014 numeric sum / product are\n // commutative, so the order doesn\'t change the result there. Build a\n // start/stop/step triple so both folds share one loop shape.\n start, stop, step := 0, 0, 1\n if isSlice {\n stop = v.Len()\n if direction == "right" {\n start, stop, step = v.Len()-1, -1, -1\n }\n }\n\n if typ == "string" {\n acc := init\n if isSlice {\n for i := start; i != stop; i += step {\n key := projectSortKey(v.Index(i).Interface(), keyKind, keyName)\n acc += toString(key)\n }\n }\n return acc\n }\n\n // numeric fold\n acc, _ := strconv.ParseFloat(strings.TrimSpace(init), 64)\n if isSlice {\n for i := start; i != stop; i += step {\n key := projectSortKey(v.Index(i).Interface(), keyKind, keyName)\n // `toFloat64WithOK` parses numeric *strings* ("5" \u2192 5) and\n // returns 0 for non-numeric values \u2014 mirroring Perl\'s\n // `looks_like_number ? $n : 0` so numeric-string data folds\n // byte-equal across adapters (the same rule `bf_sort`\'s\n // "auto" compare uses). Plain `toFloat64` would zero "5".\n n, _ := toFloat64WithOK(key)\n if op == "*" {\n acc *= n\n } else {\n acc += n\n }\n }\n }\n return acc\n}\n\n// =============================================================================\n// HTML/Template Helpers\n// =============================================================================\n\n// Comment returns an HTML comment string for hydration markers.\n// The "bf-" prefix is automatically added.\nfunc Comment(content string) template.HTML {\n return template.HTML("<!--bf-" + content + "-->")\n}\n\n// TextStart returns an HTML comment start marker for reactive text expressions.\n// Format: <!--bf:slotId-->\nfunc TextStart(slotId string) template.HTML {\n return template.HTML("<!--bf:" + slotId + "-->")\n}\n\n// TextEnd returns an HTML comment end marker for reactive text expressions.\n// Format: <!--/-->\nfunc TextEnd() template.HTML {\n return "<!--/-->"\n}\n\n// ScopeComment emits a fragment-rooted scope marker. See spec/compiler.md\n// "Slot identity" for the wire format. Loud-fails on marshal errors\n// (same policy as JSON / BfPropsAttr).\nfunc ScopeComment(props interface{}) (template.HTML, error) {\n scopeID := getStringField(props, "ScopeID")\n hostSegment := ""\n if host := getStringField(props, "BfParent"); host != "" {\n mount := getStringField(props, "BfMount")\n hostSegment = "|h=" + host + "|m=" + mount\n }\n propsJSON := ""\n if getBoolField(props, "BfIsRoot") {\n pJSON, err := json.Marshal(props)\n if err != nil {\n return "", err\n }\n propsJSON = "|" + string(pJSON)\n }\n return template.HTML("<!--bf-scope:" + scopeID + hostSegment + propsJSON + "-->"), nil\n}\n\n// ScopeCommentEnd emits the paired end marker for a fragment-rooted scope\n// (#2289): a fragment root has no single wrapping element to bound the\n// client\'s scope query, so the range leaks onto later siblings without an\n// explicit terminator. Carries only the scope id \u2014 no `|h=`/`|m=`/props\n// segment, unlike ScopeComment \u2014 since the client only needs it to confirm\n// the range closes on the matching scope (getCommentScopeBoundary in\n// packages/client/src/runtime/scope.ts).\nfunc ScopeCommentEnd(props interface{}) template.HTML {\n scopeID := getStringField(props, "ScopeID")\n return template.HTML("<!--bf-/scope:" + scopeID + "-->")\n}\n\n// TemplateFuncMap returns the helpers that need access to the executing\n// template set itself, closed over the *template.Template the component\n// defines are parsed into. Register it alongside FuncMap BEFORE parsing:\n//\n// t := template.New("")\n// t.Funcs(bf.FuncMap()).Funcs(bf.TemplateFuncMap(t))\n// template.Must(t.Parse(src))\n//\n// bf_tmpl executes a named define from the same set and returns its\n// output \u2014 used for the per-call-site children defines the Go adapter\n// emits when JSX children passed to an imported component contain\n// template actions (nested components, dynamic text) and therefore\n// cannot be baked to a static HTML string (#1896). Reentrant execution\n// of an html/template set from inside a FuncMap function is safe: the\n// escape analysis over every define completes before the outer\n// Execute begins evaluating.\nfunc TemplateFuncMap(t *template.Template) template.FuncMap {\n return template.FuncMap{\n "bf_tmpl": func(name string, data interface{}) (template.HTML, error) {\n var buf bytes.Buffer\n if err := t.ExecuteTemplate(&buf, name, data); err != nil {\n return "", err\n }\n return template.HTML(buf.String()), nil\n },\n }\n}\n\n// WithChildren returns a shallow copy of a component Props struct with its\n// Children field replaced by the given pre-rendered fragment (#1896). The\n// props value stays by-value semantics: callers\' originals are untouched.\n// A props type without a Children field passes through unchanged \u2014 the\n// child template then simply has no children to render, matching the\n// pre-#1896 behaviour.\nfunc WithChildren(props interface{}, children template.HTML) (interface{}, error) {\n v := reflect.ValueOf(props)\n if v.Kind() == reflect.Ptr {\n v = v.Elem()\n }\n if v.Kind() != reflect.Struct {\n return props, nil\n }\n field := v.FieldByName("Children")\n if !field.IsValid() {\n return props, nil\n }\n copyPtr := reflect.New(v.Type())\n copyPtr.Elem().Set(v)\n target := copyPtr.Elem().FieldByName("Children")\n switch {\n case target.Kind() == reflect.Interface:\n target.Set(reflect.ValueOf(children))\n case target.Kind() == reflect.String:\n // Covers both `string` and `template.HTML`-typed fields.\n target.SetString(string(children))\n default:\n return props, fmt.Errorf("bf_with_children: unsupported Children field type %s", target.Type())\n }\n return copyPtr.Elem().Interface(), nil\n}\n\n// PortalHTML parses and executes a template string with the provided data.\n// Used for rendering dynamic portal content where the template string\n// contains Go template expressions (e.g., {{if .Open}}open{{end}}).\n//\n// The template string is parsed fresh each time to support dynamic content.\n// Standard Go template functions (if, range, eq, etc.) are available.\nfunc PortalHTML(data interface{}, tmplStr string) template.HTML {\n // Create a new template with the FuncMap for custom functions\n t, err := template.New("portal").Funcs(FuncMap()).Parse(tmplStr)\n if err != nil {\n // Return error message as HTML comment for debugging\n return template.HTML("<!-- bfPortalHTML error: " + err.Error() + " -->")\n }\n\n var buf bytes.Buffer\n if err := t.Execute(&buf, data); err != nil {\n return template.HTML("<!-- bfPortalHTML exec error: " + err.Error() + " -->")\n }\n\n return template.HTML(buf.String())\n}\n\n// =============================================================================\n// Portal Collection\n// =============================================================================\n\n// PortalContent represents a single portal\'s content to be rendered at body end.\ntype PortalContent struct {\n ID string // Unique portal ID for hydration matching\n OwnerID string // Owner scope ID for find() support\n Content template.HTML // Portal HTML content\n}\n\n// PortalCollector collects portal content during template rendering.\n// Portal content is rendered at </body> to avoid z-index issues.\ntype PortalCollector struct {\n portals []PortalContent\n counter int\n}\n\n// NewPortalCollector creates a new PortalCollector.\nfunc NewPortalCollector() *PortalCollector {\n return &PortalCollector{\n portals: []PortalContent{},\n counter: 0,\n }\n}\n\n// Add registers portal content to be rendered at body end.\nfunc (pc *PortalCollector) Add(ownerID string, content template.HTML) string {\n pc.counter++\n id := "bf-portal-" + strconv.Itoa(pc.counter)\n pc.portals = append(pc.portals, PortalContent{\n ID: id,\n OwnerID: ownerID,\n Content: content,\n })\n return "" // Return empty string for template use\n}\n\n// Render outputs all collected portals as HTML.\n// Each portal is wrapped in a div with bf-pi (portal ID) and bf-po (portal owner).\nfunc (pc *PortalCollector) Render() template.HTML {\n if pc == nil || len(pc.portals) == 0 {\n return ""\n }\n var buf strings.Builder\n for _, p := range pc.portals {\n buf.WriteString(`<div bf-pi="`)\n buf.WriteString(p.ID)\n buf.WriteString(`" bf-po="`)\n buf.WriteString(p.OwnerID)\n buf.WriteString(`">`)\n buf.WriteString(string(p.Content))\n buf.WriteString("</div>\\n")\n }\n return template.HTML(buf.String())\n}\n\n// =============================================================================\n// Script Collection\n// =============================================================================\n\n// ScriptCollector collects client scripts with deduplication.\n// It preserves insertion order for deterministic output.\ntype ScriptCollector struct {\n scripts map[string]bool\n order []string\n}\n\n// NewScriptCollector creates a new ScriptCollector.\nfunc NewScriptCollector() *ScriptCollector {\n return &ScriptCollector{\n scripts: make(map[string]bool),\n order: []string{},\n }\n}\n\n// Register adds a script source to the collection.\n// Duplicate scripts are ignored (only first registration counts).\nfunc (sc *ScriptCollector) Register(src string) string {\n if sc.scripts[src] {\n return "" // Already registered\n }\n sc.scripts[src] = true\n sc.order = append(sc.order, src)\n return "" // Return empty string for template use\n}\n\n// Scripts returns all registered scripts in insertion order.\nfunc (sc *ScriptCollector) Scripts() []string {\n return sc.order\n}\n\n// BfScripts generates script tags for all registered scripts.\n// Returns HTML safe for embedding in templates.\nfunc BfScripts(collector *ScriptCollector) template.HTML {\n if collector == nil {\n return ""\n }\n var result strings.Builder\n for _, src := range collector.Scripts() {\n result.WriteString(`<script type="module" src="`)\n result.WriteString(src)\n result.WriteString(`"></script>`)\n result.WriteString("\\n")\n }\n return template.HTML(result.String())\n}\n\n// =============================================================================\n// Component Renderer\n// =============================================================================\n\n// RenderContext contains all data needed to render a component page.\n// The layout function receives this context to build the final HTML.\ntype RenderContext struct {\n // ComponentName is the template name being rendered\n ComponentName string\n\n // Props is the component props (for layout to access if needed)\n Props interface{}\n\n // ComponentHTML is the rendered component template output\n ComponentHTML template.HTML\n\n // Portals contains collected portal content to render at body end\n Portals template.HTML\n\n // Scripts contains the collected JS script tags\n Scripts template.HTML\n\n // Title is the page title (defaults to "{ComponentName} - BarefootJS")\n Title string\n\n // Heading is the page heading. Empty string means no heading.\n Heading string\n\n // Extra holds additional user-defined data for the layout\n Extra map[string]interface{}\n}\n\n// LayoutFunc renders the final HTML page given the render context.\ntype LayoutFunc func(ctx *RenderContext) string\n\n// Renderer renders BarefootJS components with a customizable layout.\ntype Renderer struct {\n templates *template.Template\n layout LayoutFunc\n}\n\n// NewRenderer creates a Renderer with the given templates and layout function.\n//\n// Example usage:\n//\n// renderer := bf.NewRenderer(templates, func(ctx *bf.RenderContext) string {\n// return fmt.Sprintf(`<!DOCTYPE html>\n// <html>\n// <head><title>%s</title></head>\n// <body>%s%s</body>\n// </html>`, ctx.Title, ctx.ComponentHTML, ctx.Scripts)\n// })\nfunc NewRenderer(tmpl *template.Template, layout LayoutFunc) *Renderer {\n return &Renderer{\n templates: tmpl,\n layout: layout,\n }\n}\n\n// RenderOptions configures a single render call.\ntype RenderOptions struct {\n // ComponentName is the template name to render (required)\n ComponentName string\n\n // Props is the component props (must be a pointer to struct with Scripts field)\n Props interface{}\n\n // Title is the page title. If empty, defaults to "{ComponentName} - BarefootJS"\n Title string\n\n // Heading is the page heading. If empty, no heading is shown.\n Heading string\n\n // Extra holds additional data to pass to the layout\n Extra map[string]interface{}\n}\n\n// Render renders a component to a full HTML page using the configured layout.\n// Child component props are automatically detected (any slice field with ScopeID/Scripts).\n// renderTemplateErrorPanel formats a Go template execution error into a\n// fragment of HTML that\'s visible in the browser. The panel is\n// HTML-escaped so a faulty template name (anything from `template:\n// "..."`) can\'t smuggle markup back into the page. Keep the styling\n// inline so the panel surfaces even when the project\'s CSS hasn\'t\n// loaded yet (e.g. the failure aborted before the stylesheet links\n// emitted).\n//\n// Surfaced for the #1442 echo repro: a template referencing\n// `.Todo.Done` (instead of the range dot\'s `.Done`) used to fail\n// silently \u2014 Go\'s html/template aborted mid-stream, the partial body\n// flushed as a 200, and the user saw a truncated list with no console\n// signal. With this panel they get the template name, the error\n// message, and a "what to look at" hint inline.\nfunc renderTemplateErrorPanel(componentName string, err error) string {\n return `<div style="margin:1em 0;padding:1em;border:2px solid #d33;background:#fff5f5;color:#900;font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;font-size:13px;line-height:1.5"><strong style="display:block;margin-bottom:.5em">Template error in <code>` +\n template.HTMLEscapeString(componentName) +\n `</code></strong><pre style="margin:0;white-space:pre-wrap;word-break:break-word">` +\n template.HTMLEscapeString(err.Error()) +\n `</pre><div style="margin-top:.75em;font-size:12px;opacity:.7">Common cause: a JSX expression referenced a name the adapter could not resolve to a struct field. Open the matching <code>dist/templates/*.tmpl</code> for the unresolved reference, then fix the source component.</div></div>`\n}\n\n// renderComponentInto wires a component\'s props (script/portal collectors,\n// child-slot scope ids + hydration, root marking) against the PROVIDED\n// collectors and returns just the component\'s HTML \u2014 no layout. Both Render\n// (one fresh collector pair per page) and RenderFragment (a shared pair across\n// several islands) funnel through here so their wiring stays identical.\nfunc (r *Renderer) renderComponentInto(opts RenderOptions, scriptCollector *ScriptCollector, portalCollector *PortalCollector) template.HTML {\n // Inject the shared collectors into the props.\n setScriptsField(opts.Props, scriptCollector)\n setPortalsField(opts.Props, portalCollector)\n\n // Auto-detect and process child component props (slices)\n childSlices := findChildComponentSlices(opts.Props)\n for _, slice := range childSlices {\n setScopeIDsOnSlice(slice)\n setScriptsOnSlice(slice, scriptCollector)\n setPortalsOnSlice(slice, portalCollector)\n setBoolOnSlice(slice, "BfIsChild", true)\n }\n\n // Auto-detect and process single child component props\n singleChildren := findSingleChildComponents(opts.Props)\n for _, child := range singleChildren {\n setScopeIDOnSingle(child)\n setScriptsOnSingle(child, scriptCollector)\n setPortalsOnSingle(child, portalCollector)\n setBoolField(child, "BfIsChild", true)\n }\n\n // Mark the root component so BfPropsAttr emits bf-p only for it\n setBoolField(opts.Props, "BfIsRoot", true)\n\n // Render the component template.\n //\n // Errors here are NOT silently dropped. The original implementation\n // ignored the return value of `ExecuteTemplate`, which masked a real\n // onboarding failure mode: a template referencing a non-existent\n // field (`.Todo.Done` instead of the range dot\'s `.Done`) caused\n // html/template to abort mid-stream, the partial output got\n // returned, and the HTTP server happily flushed a 200 with a\n // truncated body. No error log, no signal \u2014 the user just saw a\n // blank list (#1442 echo TodoApp repro).\n //\n // Now we capture the error and replace the partial output with a\n // visible inline panel (dev mode) or a fenced error comment\n // (production), so the cause is on-screen and grep-able in logs.\n // Either way the renderer also writes to stderr so structured log\n // aggregators see it.\n var componentBuf strings.Builder\n if err := r.templates.ExecuteTemplate(&componentBuf, opts.ComponentName, opts.Props); err != nil {\n fmt.Fprintf(os.Stderr, "barefoot: template %q failed to render: %v\\n", opts.ComponentName, err)\n // Preserve whatever the template did manage to emit before\n // failing (Go\'s text/template flushes incrementally), but\n // follow it with a clearly-marked error block so the user\n // notices something is wrong instead of seeing a silent\n // truncation.\n componentBuf.WriteString(renderTemplateErrorPanel(opts.ComponentName, err))\n }\n\n return template.HTML(componentBuf.String())\n}\n\nfunc (r *Renderer) Render(opts RenderOptions) string {\n // One script + portal collector pair for the whole page.\n scriptCollector := NewScriptCollector()\n portalCollector := NewPortalCollector()\n\n componentHTML := r.renderComponentInto(opts, scriptCollector, portalCollector)\n\n // Determine title (default: "{ComponentName} - BarefootJS")\n title := opts.Title\n if title == "" {\n title = opts.ComponentName + " - BarefootJS"\n }\n\n // Heading (empty means no heading)\n heading := opts.Heading\n\n // Build render context\n ctx := &RenderContext{\n ComponentName: opts.ComponentName,\n Props: opts.Props,\n ComponentHTML: componentHTML,\n Portals: portalCollector.Render(),\n Scripts: BfScripts(scriptCollector),\n Title: title,\n Heading: heading,\n Extra: opts.Extra,\n }\n\n return r.layout(ctx)\n}\n\n// RenderFragment renders a single island subtree into the caller-provided\n// script and portal collectors and returns just its HTML \u2014 no page layout.\n//\n// It exists for hand-authored "region shell" pages (the `@barefootjs/router`\n// showcase): a layout that places several independent islands \u2014 e.g. a header\n// ThemeToggle, an `<aside bf-region>` Sidebar, and a `<PageShell>` wrapping the\n// route content \u2014 must collect ALL their scripts and portals into ONE place so\n// the runtime (`barefoot.js`) and each island\'s client JS are emitted exactly\n// once and share a single reactive instance. Render each island with the same\n// collectors, splice the returned HTML into the shell, then emit\n// `BfScripts(sc)` and `pc.Render()` once at the end of the document.\n//\n// Each fragment is treated as its own root (it emits `bf-p` like any top-level\n// island); nested children declared in its props are wired as children, exactly\n// as in Render.\nfunc (r *Renderer) RenderFragment(opts RenderOptions, scriptCollector *ScriptCollector, portalCollector *PortalCollector) template.HTML {\n return r.renderComponentInto(opts, scriptCollector, portalCollector)\n}\n\n// setScriptsField sets the Scripts field on a struct using reflection.\nfunc setScriptsField(v interface{}, collector *ScriptCollector) {\n val := reflect.ValueOf(v)\n if val.Kind() == reflect.Ptr {\n val = val.Elem()\n }\n if val.Kind() != reflect.Struct {\n return\n }\n field := val.FieldByName("Scripts")\n if field.IsValid() && field.CanSet() {\n field.Set(reflect.ValueOf(collector))\n }\n}\n\n// setPortalsField sets the Portals field on a struct using reflection.\nfunc setPortalsField(v interface{}, collector *PortalCollector) {\n val := reflect.ValueOf(v)\n if val.Kind() == reflect.Ptr {\n val = val.Elem()\n }\n if val.Kind() != reflect.Struct {\n return\n }\n field := val.FieldByName("Portals")\n if field.IsValid() && field.CanSet() {\n field.Set(reflect.ValueOf(collector))\n }\n}\n\n// getStringField extracts a string field from a struct using reflection.\nfunc setBoolField(v interface{}, fieldName string, val bool) {\n rv := reflect.ValueOf(v)\n if rv.Kind() == reflect.Ptr {\n rv = rv.Elem()\n }\n if rv.Kind() != reflect.Struct {\n return\n }\n field := rv.FieldByName(fieldName)\n if field.IsValid() && field.CanSet() && field.Kind() == reflect.Bool {\n field.SetBool(val)\n }\n}\n\nfunc getBoolField(v interface{}, fieldName string) bool {\n val := reflect.ValueOf(v)\n if val.Kind() == reflect.Ptr {\n val = val.Elem()\n }\n if val.Kind() != reflect.Struct {\n return false\n }\n field := val.FieldByName(fieldName)\n if !field.IsValid() || field.Kind() != reflect.Bool {\n return false\n }\n return field.Bool()\n}\n\nfunc getStringField(v interface{}, fieldName string) string {\n val := reflect.ValueOf(v)\n if val.Kind() == reflect.Ptr {\n val = val.Elem()\n }\n if val.Kind() != reflect.Struct {\n return ""\n }\n field := val.FieldByName(fieldName)\n if !field.IsValid() || field.Kind() != reflect.String {\n return ""\n }\n return field.String()\n}\n\n// scopeIDChars is the alphabet for auto-generated ScopeID suffixes. It\n// mirrors the `randomID` helper the go-template adapter emits into the\n// generated New<Component>Props constructors so runtime-assigned and\n// constructor-assigned ids are indistinguishable.\nconst scopeIDChars = "abcdefghijklmnopqrstuvwxyz0123456789"\n\n// randomScopeSuffix returns a random lowercase-alphanumeric string of\n// length n. math/rand (auto-seeded since Go 1.20) is sufficient here: the\n// suffix only needs to be unique enough to keep a page\'s bf-s scope ids\n// from colliding, not cryptographically unpredictable.\nfunc randomScopeSuffix(n int) string {\n b := make([]byte, n)\n for i := range b {\n b[i] = scopeIDChars[rand.Intn(len(scopeIDChars))]\n }\n return string(b)\n}\n\n// scopeIDPrefix derives the human-readable ScopeID prefix from a child\n// component\'s type, e.g. `TodoItemProps` \u2192 `TodoItem`. Matches the\n// `"<Component>_" + randomID(6)` shape the generated constructors use.\nfunc scopeIDPrefix(t reflect.Type) string {\n for t.Kind() == reflect.Ptr {\n t = t.Elem()\n }\n return strings.TrimSuffix(t.Name(), "Props")\n}\n\n// assignScopeID fills a child component\'s ScopeID with a generated id when\n// the caller left it empty, so application code doesn\'t have to mint scope\n// ids by hand (the parent\'s New<Component>Props constructor does the same\n// for components built through it). A non-empty ScopeID is left untouched,\n// so callers can still pin a stable id when they need one.\nfunc assignScopeID(structVal reflect.Value, prefix string) {\n field := structVal.FieldByName("ScopeID")\n if !field.IsValid() || !field.CanSet() || field.Kind() != reflect.String {\n return\n }\n if field.String() != "" {\n return\n }\n id := randomScopeSuffix(6)\n if prefix != "" {\n id = prefix + "_" + id\n }\n field.SetString(id)\n}\n\n// setScopeIDsOnSlice assigns a generated ScopeID to every child in a slice\n// whose ScopeID is empty.\nfunc setScopeIDsOnSlice(slice interface{}) {\n v := reflect.ValueOf(slice)\n if v.Kind() != reflect.Slice {\n return\n }\n prefix := scopeIDPrefix(v.Type().Elem())\n for i := 0; i < v.Len(); i++ {\n item := v.Index(i)\n if item.Kind() == reflect.Ptr {\n if item.IsNil() {\n continue\n }\n item = item.Elem()\n }\n if item.Kind() == reflect.Struct {\n assignScopeID(item, prefix)\n }\n }\n}\n\n// setScopeIDOnSingle assigns a generated ScopeID to a single child\n// component when its ScopeID is empty.\nfunc setScopeIDOnSingle(child interface{}) {\n v := reflect.ValueOf(child)\n if v.Kind() == reflect.Ptr {\n v = v.Elem()\n }\n if v.Kind() != reflect.Struct {\n return\n }\n assignScopeID(v, scopeIDPrefix(v.Type()))\n}\n\n// findChildComponentSlices finds slice fields containing child component props.\n// Child props are identified by having ScopeID and Scripts fields.\nfunc findChildComponentSlices(props interface{}) []interface{} {\n var result []interface{}\n\n val := reflect.ValueOf(props)\n if val.Kind() == reflect.Ptr {\n val = val.Elem()\n }\n if val.Kind() != reflect.Struct {\n return result\n }\n\n for i := 0; i < val.NumField(); i++ {\n field := val.Field(i)\n if field.Kind() != reflect.Slice || field.Len() == 0 {\n continue\n }\n\n elem := field.Index(0)\n if elem.Kind() == reflect.Ptr {\n elem = elem.Elem()\n }\n if elem.Kind() != reflect.Struct {\n continue\n }\n\n hasScopeID := elem.FieldByName("ScopeID").IsValid()\n hasScripts := elem.FieldByName("Scripts").IsValid()\n\n if hasScopeID && hasScripts {\n result = append(result, field.Interface())\n }\n }\n\n return result\n}\n\n// setScriptsOnSlice sets Scripts on all items in a slice.\nfunc setScriptsOnSlice(slice interface{}, collector *ScriptCollector) {\n val := reflect.ValueOf(slice)\n if val.Kind() != reflect.Slice {\n return\n }\n for i := 0; i < val.Len(); i++ {\n item := val.Index(i)\n if item.Kind() == reflect.Ptr {\n item = item.Elem()\n }\n if item.Kind() == reflect.Struct {\n field := item.FieldByName("Scripts")\n if field.IsValid() && field.CanSet() {\n field.Set(reflect.ValueOf(collector))\n }\n }\n }\n}\n\n// setBoolOnSlice sets a bool field on all items in a slice.\nfunc setBoolOnSlice(slice interface{}, fieldName string, val bool) {\n v := reflect.ValueOf(slice)\n if v.Kind() != reflect.Slice {\n return\n }\n for i := 0; i < v.Len(); i++ {\n item := v.Index(i)\n if item.Kind() == reflect.Ptr {\n item = item.Elem()\n }\n if item.Kind() == reflect.Struct {\n field := item.FieldByName(fieldName)\n if field.IsValid() && field.CanSet() && field.Kind() == reflect.Bool {\n field.SetBool(val)\n }\n }\n }\n}\n\n// setPortalsOnSlice sets Portals on all items in a slice.\nfunc setPortalsOnSlice(slice interface{}, collector *PortalCollector) {\n val := reflect.ValueOf(slice)\n if val.Kind() != reflect.Slice {\n return\n }\n for i := 0; i < val.Len(); i++ {\n item := val.Index(i)\n if item.Kind() == reflect.Ptr {\n item = item.Elem()\n }\n if item.Kind() == reflect.Struct {\n field := item.FieldByName("Portals")\n if field.IsValid() && field.CanSet() {\n field.Set(reflect.ValueOf(collector))\n }\n }\n }\n}\n\n// findSingleChildComponents finds single struct fields containing child component props.\n// Child props are identified by having ScopeID and Scripts fields.\nfunc findSingleChildComponents(props interface{}) []interface{} {\n var result []interface{}\n\n val := reflect.ValueOf(props)\n if val.Kind() == reflect.Ptr {\n val = val.Elem()\n }\n if val.Kind() != reflect.Struct {\n return result\n }\n\n for i := 0; i < val.NumField(); i++ {\n field := val.Field(i)\n\n // Handle pointer to struct\n if field.Kind() == reflect.Ptr {\n if field.IsNil() {\n continue\n }\n field = field.Elem()\n }\n\n // Skip non-struct fields (slices handled by findChildComponentSlices)\n if field.Kind() != reflect.Struct {\n continue\n }\n\n hasScopeID := field.FieldByName("ScopeID").IsValid()\n hasScripts := field.FieldByName("Scripts").IsValid()\n\n if hasScopeID && hasScripts {\n result = append(result, field.Addr().Interface())\n }\n }\n\n return result\n}\n\n// setScriptsOnSingle sets Scripts on a single struct child component.\nfunc setScriptsOnSingle(child interface{}, collector *ScriptCollector) {\n val := reflect.ValueOf(child)\n if val.Kind() == reflect.Ptr {\n val = val.Elem()\n }\n if val.Kind() == reflect.Struct {\n field := val.FieldByName("Scripts")\n if field.IsValid() && field.CanSet() {\n field.Set(reflect.ValueOf(collector))\n }\n }\n}\n\n// setPortalsOnSingle sets Portals on a single struct child component.\nfunc setPortalsOnSingle(child interface{}, collector *PortalCollector) {\n val := reflect.ValueOf(child)\n if val.Kind() == reflect.Ptr {\n val = val.Elem()\n }\n if val.Kind() == reflect.Struct {\n field := val.FieldByName("Portals")\n if field.IsValid() && field.CanSet() {\n field.Set(reflect.ValueOf(collector))\n }\n }\n}\n\n// Nullish implements JS `??` for template use (`bf_nullish`, #2248): returns\n// fallback iff v is nil (untyped nil or a nil pointer/map/slice boxed in the\n// interface), otherwise v \u2014 so present-but-falsy `""`/`0`/`false` are KEPT,\n// unlike the truthiness-based template `or`.\nfunc Nullish(v, fallback any) any {\n if v == nil {\n return fallback\n }\n rv := reflect.ValueOf(v)\n switch rv.Kind() {\n case reflect.Ptr, reflect.Map, reflect.Slice, reflect.Interface, reflect.Func, reflect.Chan:\n if rv.IsNil() {\n return fallback\n }\n }\n return v\n}\n\n// ToInt exposes the runtime\'s numeric coercion for generated constructors\n// (#2248): a nillable-lowered numeric prop arrives as `interface{}`, and an\n// untyped Go literal (`Size: 3`) boxes as int even when the prop is\n// float64-shaped \u2014 a direct type assertion would panic where JS accepts the\n// number. Non-numeric values coerce to 0, matching the helpers\' behaviour.\nfunc ToInt(v any) int { return toInt(v) }\n\n// ToFloat64 is ToInt\'s float64 counterpart \u2014 see ToInt.\nfunc ToFloat64(v any) float64 { return toFloat64(v) }\n\n// =============================================================================\n// Internal Helpers\n// =============================================================================\n\nfunc toFloat64(v any) float64 {\n switch n := v.(type) {\n case int:\n return float64(n)\n case int8:\n return float64(n)\n case int16:\n return float64(n)\n case int32:\n return float64(n)\n case int64:\n return float64(n)\n case uint:\n return float64(n)\n case uint8:\n return float64(n)\n case uint16:\n return float64(n)\n case uint32:\n return float64(n)\n case uint64:\n return float64(n)\n case float32:\n return float64(n)\n case float64:\n return n\n default:\n return 0\n }\n}\n\nfunc toInt(v any) int {\n switch n := v.(type) {\n case int:\n return n\n case int8:\n return int(n)\n case int16:\n return int(n)\n case int32:\n return int(n)\n case int64:\n return int(n)\n case uint:\n return int(n)\n case uint8:\n return int(n)\n case uint16:\n return int(n)\n case uint32:\n return int(n)\n case uint64:\n return int(n)\n case float32:\n return int(n)\n case float64:\n return int(n)\n default:\n return 0\n }\n}\n\nfunc isIntLike(v any) bool {\n switch v.(type) {\n case int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64:\n return true\n default:\n return false\n }\n}\n\nfunc toString(v any) string {\n switch s := v.(type) {\n case string:\n return s\n case int:\n return strconv.Itoa(s)\n case int64:\n return strconv.FormatInt(s, 10)\n case float64:\n return strconv.FormatFloat(s, \'f\', -1, 64)\n case bool:\n return strconv.FormatBool(s)\n default:\n rv := reflect.ValueOf(v)\n if rv.Kind() == reflect.Slice || rv.Kind() == reflect.Array {\n // JS `Array.prototype.toString` is `this.join(\',\')`, applied\n // recursively \u2014 a nested array element stringifies the same\n // way rather than via Go\'s `%v`. Reached via `Join`/`ConcatStr`\n // on an element that is itself an array (e.g. `.flat(0)`\'s\n // shallow copy joined afterwards, #2262).\n parts := make([]string, rv.Len())\n for i := 0; i < rv.Len(); i++ {\n parts[i] = toString(rv.Index(i).Interface())\n }\n return strings.Join(parts, ",")\n }\n return ""\n }\n}\n\n// =============================================================================\n// searchParams() \u2014 request-scoped environment signal (router v0.5, #1922)\n// =============================================================================\n\n// SearchParams is the SSR view of the request query string behind the\n// reactive searchParams() environment signal. The route handler builds it\n// from the request URL and assigns it to the component\'s SearchParams input\n// field; the generated template reads it via `.SearchParams.Get "key"`.\n//\n// The zero value is an empty query (url.Values.Get tolerates a nil map), so a\n// render with no request query \u2014 e.g. the adapter conformance harness, which\n// issues no query string \u2014 resolves every key to "", which the template\'s\n// `or`/`??` fallback turns into the author\'s default.\ntype SearchParams struct {\n values url.Values\n}\n\n// NewSearchParams parses a raw query string (with or without a leading "?")\n// into a SearchParams. A malformed query yields an empty set rather than an\n// error, mirroring the browser\'s URLSearchParams, which never throws on junk.\n//\n// Typical handler use (net/http):\n//\n// in := MyComponentInput{SearchParams: bf.NewSearchParams(r.URL.RawQuery)}\nfunc NewSearchParams(raw string) SearchParams {\n raw = strings.TrimPrefix(raw, "?")\n values, err := url.ParseQuery(raw)\n if err != nil {\n values = url.Values{}\n }\n return SearchParams{values: values}\n}\n\n// Get returns the first value associated with key, or "" when the key is\n// absent. This mirrors url.Values.Get, which also returns "" for a\n// present-but-empty value (`?sort=`). Safe on the zero value (nil map).\n//\n// This is not byte-for-byte URLSearchParams.get under the template\'s `??`\n// lowering. JS distinguishes absent (`null`) from present-but-empty (`""`):\n// `null ?? d` yields the default, but `"" ?? d` keeps the empty string. The\n// Go adapter lowers `??` to the `or` builtin \u2014 Go templates have no\n// null-coalescing operator \u2014 so here BOTH an absent key and a present-but-\n// empty value fall back to the author\'s default. The conformance fixture only\n// exercises the absent-key default, where the two runtimes agree; the\n// empty-string divergence is the same general `?? \u2192 or` limitation that\n// applies to any `x ?? default` the Go adapter lowers.\nfunc (s SearchParams) Get(key string) string {\n return s.values.Get(key)\n}\n';
|
|
32587
|
+
bfGoSource = '// Package bf provides runtime helper functions for BarefootJS Go templates.\n// These functions mirror JavaScript behavior for consistent SSR output.\npackage bf\n\nimport (\n "bytes"\n "encoding/json"\n "fmt"\n "html/template"\n "math"\n "math/rand"\n "net/url"\n "os"\n "reflect"\n "regexp"\n "sort"\n "strconv"\n "strings"\n "time"\n "unicode"\n "unicode/utf8"\n)\n\n// FuncMap returns a template.FuncMap with all BarefootJS helper functions.\n// Usage:\n//\n// tmpl := template.New("").Funcs(bf.FuncMap())\nfunc FuncMap() template.FuncMap {\n return template.FuncMap{\n // Nullish coalescing (#2248): JS `??` semantics \u2014 fall back only on\n // nil, keeping present-but-falsy values (`""`, `0`, `false`) that\n // Go\'s truthiness-based `or` would replace.\n "bf_nullish": Nullish,\n\n // Arithmetic\n "bf_add": Add,\n "bf_concat_str": ConcatStr,\n "bf_sub": Sub,\n "bf_mul": Mul,\n "bf_div": Div,\n "bf_mod": Mod,\n "bf_neg": Neg,\n "bf_min": Min,\n "bf_max": Max,\n\n // String\n "bf_lower": Lower,\n "bf_upper": Upper,\n "bf_trim": Trim,\n "bf_trim_start": TrimStart,\n "bf_trim_end": TrimEnd,\n "bf_contains": Contains,\n "bf_join": Join,\n "bf_split": Split,\n "bf_starts_with": StartsWith,\n "bf_ends_with": EndsWith,\n "bf_replace": Replace,\n "bf_replace_all": ReplaceAll,\n "bf_repeat": Repeat,\n "bf_pad_start": PadStart,\n "bf_pad_end": PadEnd,\n "bf_string": String,\n "bf_raw_html": RawHTML,\n "bf_ternary": Ternary,\n "bf_truthy": Truthy,\n\n // URL query builder (#1897 PostList href helpers): conditional\n // (include, key, value) triples \u2192 "base?k=v&\u2026", mirroring a\n // URLSearchParams builder with guarded `.set()` calls.\n "bf_query": Query,\n\n // Date method lowering (#2274, spec entry "date"): the lowering\n // target for a zero-arg call on a Date-typed prop.\n "bf_date": Date,\n\n // formatDate(date, pattern, tz) lowering (#2324, spec entry\n // "format_date"): the total, locale-free date-pattern formatter \u2014\n // see FormatDate\'s docstring for the full contract.\n "bf_format_date": FormatDate,\n\n // JSON / numeric primitives \u2014 JS-compat callees registered on\n // the Go adapter\'s `templatePrimitives` map (#1188).\n "bf_json": JSON,\n "bf_number": Number,\n "bf_floor": Floor,\n "bf_ceil": Ceil,\n "bf_round": Round,\n "bf_abs": Abs,\n "bf_to_fixed": ToFixed,\n\n // Array/Slice\n "bf_len": Len,\n "bf_length": Length,\n "bf_is_element": IsValidElement,\n "bf_style_object": StyleObjectToCSS,\n "bf_at": At,\n "bf_includes": Includes,\n "bf_index_of": IndexOf,\n "bf_last_index_of": LastIndexOf,\n "bf_concat": Concat,\n "bf_slice": Slice,\n "bf_reverse": Reverse,\n "bf_flat": Flat,\n "bf_flat_dynamic": FlatDynamicDepth,\n "bf_flat_map": FlatMap,\n "bf_flat_map_tuple": FlatMapTuple,\n "bf_first": First,\n "bf_last": Last,\n "bf_arr": Arr,\n "bf_filter_truthy": FilterTruthy,\n\n // Higher-order Array Methods\n "bf_every": Every,\n "bf_some": Some,\n "bf_filter": Filter,\n "bf_find": Find,\n "bf_find_index": FindIndex,\n "bf_find_last": FindLast,\n "bf_find_last_index": FindLastIndex,\n "bf_sort": Sort,\n "bf_reduce": Reduce,\n\n // Evaluator-driven higher-order folds (#2018): the comparator / reducer\n // body travels as a serialized ParsedExpr (JSON) evaluated per element,\n // generalizing bf_sort / bf_reduce beyond their fixed catalogues. The\n // adapter falls back to bf_sort for a comparator the evaluator can\'t\n // model (e.g. localeCompare). `bf_env` builds the captured-free-var\n // environment passed as the trailing base_env argument.\n "bf_sort_eval": SortEval,\n "bf_reduce_eval": FoldEval,\n "bf_env": Env,\n\n // Evaluator-driven higher-order predicates (#2018, P2): the predicate\n // body travels as a serialized ParsedExpr (JSON) evaluated per element,\n // generalizing bf_filter / bf_find / bf_find_index / bf_every / bf_some\n // beyond their field-equality / truthiness catalogues. `bf_find_eval` /\n // `bf_find_index_eval` take a `forward` bool (false \u2192 findLast variants).\n "bf_filter_eval": FilterEval,\n "bf_every_eval": EveryEval,\n "bf_some_eval": SomeEval,\n "bf_find_eval": FindEval,\n "bf_find_index_eval": FindIndexEval,\n // `.flatMap(proj)`: project each element through the serialized\n // projection body, then flatten one level.\n "bf_flat_map_eval": FlatMapEval,\n // Value-producing `.map(cb)` (#2073): project each element, one\n // result per element (no flatten).\n "bf_map_eval": MapEval,\n\n // Comment marker (for hydration)\n "bfComment": Comment,\n "bfTextStart": TextStart,\n "bfTextEnd": TextEnd,\n\n // Script collection\n "bfScripts": BfScripts,\n\n // Scope attribute value (#1249: bare scope id, no `~` prefix)\n "bfScopeAttr": ScopeAttr,\n\n // Slot-identity markers (#1249): bf-h, bf-m, bf-r\n "bfHydrationAttrs": HydrationAttrs,\n\n // Child component marker (kept for backward compatibility)\n "bfIsChild": IsChild,\n\n // Props attribute for hydration\n "bfPropsAttr": BfPropsAttr,\n\n // Portal HTML rendering (parses and executes template string)\n "bfPortalHTML": PortalHTML,\n\n // JSX children passed to an imported child component (#1896):\n // the parent renders the children fragment via a companion\n // define (executed through bf_tmpl from TemplateFuncMap) and\n // injects the result into the child\'s Children field.\n "bf_with_children": WithChildren,\n\n // Per-row props for a child component nested inside a composite\n // loop row (#2445): the parent\'s once-per-slot instance is shared\n // across rows, so a prop that depends on the row is reapplied as a\n // copy inside {{range}}, the props-argument sibling of\n // bf_with_children.\n "bf_with_props": WithProps,\n\n // Per-row props for a child whose CONSTRUCTOR derives a field from\n // the overridden prop (#2448). bf_with_props patches fields on the\n // shared instance and cannot re-run New<Child>Props, so a memo body\n // or a signal initial value computed there would stay at the shared\n // instance\'s one-shot value on every row. This entry looks up the\n // component\'s generated rebuilder and re-runs the real constructor\n // instead. See reprops.go for why the lookup is deferred to execute\n // time rather than merged into this map.\n "bf_reprops": Reprops,\n\n // Scope comment for fragment roots\n "bfScopeComment": ScopeComment,\n "bfScopeCommentEnd": ScopeCommentEnd,\n\n // JSX intrinsic-element spread lowering (#1407)\n "bf_spread_attrs": SpreadAttrs,\n\n // Destructure object-rest spread-onto-element residual (#2087):\n // "every field except these" for a struct/map, keyed by json tag.\n "bf_omit": Omit,\n\n // Case-tolerant single-field read off a map or struct (#2087): a\n // `useContext` local whose `createContext` default is object-shaped\n // (e.g. `createContext<{ config: X }>({ config: {} })`) is typed\n // `map[string]interface{}`, and its keys are the SOURCE (JS-cased)\n // property names a `<Ctx.Provider value={{ \u2026 }}>` bakes\n // (`providerObjectValueToGoMap`, go-template-adapter.ts) \u2014 plain\n // `text/template` dot access does an exact-string `MapIndex`, so\n // `ctx.config.label` lowers to nested `bf_get` calls instead of\n // `.Ctx.Config.Label`. Reuses the same `getFieldValue` the\n // project/sort helpers already use for a dynamic field-name lookup;\n // safe on a nil map/interface (returns nil) so a missing Provider or\n // an absent key falls through to `??`\'s fallback.\n "bf_get": getFieldValue,\n }\n}\n\n// Query builds a URL from a base path plus a query string assembled from\n// (include, key, value) triples, in order. A pair is considered only when its\n// `include` flag is true \u2014 mirroring a JS URLSearchParams builder whose\n// `.set(key, value)` calls are each guarded by an `if`. The compiler lowers a\n// conditional `cond ? v : undefined` to the `include` bool and a plain `key: v`\n// to a `true` include; the emptiness check is applied HERE (an included but\n// empty value is dropped), matching the client `queryHref` and the Perl `query`\n// helper. Keys and values use formEscape (application/x-www-form-urlencoded),\n// so the rendered query is byte-for-byte identical to the browser\'s\n// URLSearchParams. An empty query yields the bare base.\n//\n// A value may be a string slice ([]string or []any), which APPENDS one pair per\n// non-empty member (URLSearchParams.append) \u2014 `{tag: [a, b]}` \u2192 `tag=a&tag=b`.\n// A scalar value follows URLSearchParams.set() semantics: repeating a key\n// overwrites the value at the key\'s first position rather than duplicating it\n// (object literals have unique keys, so this is defensive). Trailing args that\n// don\'t complete a triple are ignored.\n//\n// formEscape differs from url.QueryEscape only on `~` (kept by QueryEscape,\n// `%7E` here) and `*` (`%2A` by QueryEscape, kept here).\nfunc Query(base string, triples ...any) string {\n type kv struct{ key, val string }\n pairs := make([]kv, 0, len(triples)/3)\n pos := make(map[string]int)\n for i := 0; i+2 < len(triples); i += 3 {\n include, _ := triples[i].(bool)\n if !include {\n continue\n }\n k := String(triples[i+1])\n if members, ok := asStringSlice(triples[i+2]); ok {\n // Array value \u2192 append each non-empty member; appended pairs never\n // overwrite, so they don\'t participate in the set()-position map.\n for _, m := range members {\n if m == "" {\n continue\n }\n pairs = append(pairs, kv{k, m})\n }\n continue\n }\n v := String(triples[i+2])\n if v == "" {\n continue // omit an included-but-empty value (client / Perl parity)\n }\n if at, ok := pos[k]; ok {\n pairs[at].val = v // set(): overwrite the first occurrence\'s value\n } else {\n pos[k] = len(pairs)\n pairs = append(pairs, kv{k, v})\n }\n }\n var b strings.Builder\n for _, p := range pairs {\n if b.Len() == 0 {\n b.WriteByte(\'?\')\n } else {\n b.WriteByte(\'&\')\n }\n b.WriteString(formEscape(p.key))\n b.WriteByte(\'=\')\n b.WriteString(formEscape(p.val))\n }\n return base + b.String()\n}\n\n// Date implements the `date` helper (spec/template-helpers.md, #2274) \u2014 the\n// lowering target for a zero-arg call on a Date-typed prop\n// (`createdAt.toISOString()`). recv accepts the runtime\'s own `time.Time` /\n// `*time.Time` (however the host framework populated the prop) OR an\n// ISO-8601 string (the wire form a JSON-sourced prop arrives as); either is\n// normalized to UTC before dispatching op, matching the client `Date`\'s own\n// instant semantics regardless of which shape reaches this helper. A nil /\n// unparsable receiver yields this runtime\'s zero value for the requested op\n// (0 for every numeric accessor, "" for toISOString) rather than panicking\n// mid-render \u2014 the same tolerance `String`/`Number` already extend to a nil\n// prop. `getUTCMonth` subtracts 1: Go\'s `time.Month` is 1-based, JS\'s is not\n// (spec entry "date" is explicit that JS wins here).\nfunc Date(recv any, op string) any {\n t, ok := toTime(recv)\n if !ok {\n if op == "toISOString" {\n return ""\n }\n return 0\n }\n t = t.UTC()\n switch op {\n case "getUTCFullYear":\n return t.Year()\n case "getUTCMonth":\n return int(t.Month()) - 1\n case "getUTCDate":\n return t.Day()\n case "getUTCHours":\n return t.Hour()\n case "getUTCMinutes":\n return t.Minute()\n case "getUTCSeconds":\n return t.Second()\n case "getTime":\n return t.UnixMilli()\n case "toISOString":\n return t.Format("2006-01-02T15:04:05.000Z")\n default:\n return 0\n }\n}\n\n// tzOffsetRE matches a fixed UTC offset `\xB1HH:MM` within ECMA-402\'s valid\n// range \u2014 hours 00\u201323, minutes 00\u201359 (`\'+09:00\'`, `\'-05:30\'`) \u2014 one of the\n// three `tz` shapes FormatDate accepts (mirrors OFFSET_RE in\n// packages/client/src/format-date.ts). An out-of-range shape (`\'+25:00\'`)\n// falls through to the tzdata lookup, fails it, and errors \u2014 matching the\n// JS reference\'s RangeError (#2344).\nvar tzOffsetRE = regexp.MustCompile(`^([+-])([01][0-9]|2[0-3]):([0-5][0-9])$`)\n\n// formatDateTokenRE is the longest-match pattern-token alternation (mirrors\n// TOKEN_RE in packages/client/src/format-date.ts). Order matters: MMMM\n// before MMM before MM before M (and dddd before ddd, DD before D) so the\n// longer token wins at a position where a shorter one could also match \u2014\n// Go\'s regexp, like JS\'s, resolves alternation leftmost-first (not POSIX\n// leftmost-longest), so listing the longer alternative first is what makes\n// e.g. "MMMM" consume all four characters instead of "MM" + "MM".\nvar formatDateTokenRE = regexp.MustCompile(`YYYY|MMMM|MMM|MM|DD|dddd|ddd|M|D`)\n\n// nameTable section offsets (#2334, mirrors MONTHS_WIDE / MONTHS_ABBR /\n// WEEKDAYS_WIDE / WEEKDAYS_ABBR in packages/client/src/format-date.ts).\nconst (\n monthsWide = 0\n monthsAbbr = 12\n weekdaysWide = 24\n weekdaysAbbr = 31\n)\n\n// formatDateName reads a name-token table entry: index out of range, or a\n// non-string element, both render "" \u2014 the same total, zero-value\n// discipline as an unparseable date (mirrors the JS reference\'s\n// `names[index] ?? ""` fallback, where every table element the vectors ever\n// carry is a string).\nfunc formatDateName(names []any, index int) string {\n if index < 0 || index >= len(names) {\n return ""\n }\n s, ok := names[index].(string)\n if !ok {\n return ""\n }\n return s\n}\n\n// FormatDate implements the `format_date` helper (#2324, #2334, spec entry\n// "format_date") \u2014 the lowering target for\n// `formatDate(date, pattern, tz, names)`\n// (packages/client/src/format-date.ts, the JS-normative reference this must\n// match byte-for-byte). Total and deterministic: no locale, no host\n// timezone, no "now".\n//\n// recv: same receiver contract as the `date` helper above \u2014 the runtime\'s\n// own `time.Time` / `*time.Time`, or an ISO-8601 string \u2014 normalized via\n// `toTime`. A nil / unparseable receiver returns "" (never panics).\n//\n// tz (#2344): "UTC", a range-valid fixed offset `\xB1HH:MM` (shifts by\n// sign*(HH*60+MM) minutes), or a canonical IANA zone name ("Asia/Tokyo")\n// resolved through tzdata via time.LoadLocation \u2014 the zone\'s UTC offset AT\n// THE INSTANT being formatted (DST-aware, historical-transition-aware,\n// seconds precision: pre-standard LMT offsets like Tokyo\'s +09:18:59\n// count). ANY other value \u2014 an unknown zone, a malformed or out-of-range\n// offset ("+9:00", "+25:00"), the empty string or "Local" (LoadLocation\'s\n// implicit-environment aliases) \u2014 returns an ERROR, aborting template\n// execution loudly: the JS reference throws a RangeError there, and a\n// silently substituted timezone is the one failure mode this helper must\n// not have (the pre-#2344 normalize-to-UTC total function is gone). The\n// shifted instant\'s UTC calendar fields (not the original instant\'s) are\n// what pattern tokens read \u2014 the shifted UTC clock face IS the local clock\n// face in that zone, same reasoning as the JS reference.\n//\n// names (#2334): a flat name table in fixed layout \u2014 `[0..11]` wide month\n// names, `[12..23]` abbreviated month names, `[24..30]` wide weekday names\n// (Sunday-first), `[31..37]` abbreviated weekday names. The caller owns the\n// values; this helper only indexes the table.\n//\n// pattern: longest-match token substitution\n// (`YYYY|MMMM|MMM|MM|DD|dddd|ddd|M|D`); every other character \u2014 including\n// multi-byte ones like \u5E74/\u6708/\u65E5 \u2014 passes through literally. `YYYY` is\n// `abs(year)` zero-padded to 4 digits, `-`-prefixed for a negative year;\n// `MM`/`DD` zero-pad to 2; `M`/`D` are bare; `MMMM`/`MMM` and `dddd`/`ddd`\n// read the `names` table (weekday computed on the offset-shifted instant,\n// Sunday-first, matching `time.Time.Weekday()`\'s own Sunday=0 encoding).\nfunc FormatDate(recv any, pattern string, tz string, names []any) (string, error) {\n t, ok := toTime(recv)\n if !ok {\n // Receiver contract precedes tz validation (spec receiver-first\n // discipline, mirrored by every port).\n return "", nil\n }\n offsetSeconds := 0\n if tz != "UTC" {\n if m := tzOffsetRE.FindStringSubmatch(tz); m != nil {\n hh, _ := strconv.Atoi(m[2])\n mm, _ := strconv.Atoi(m[3])\n offsetSeconds = (hh*60 + mm) * 60\n if m[1] == "-" {\n offsetSeconds = -offsetSeconds\n }\n } else if tz == "" || tz == "Local" {\n // LoadLocation("") is UTC and LoadLocation("Local") is the host\n // zone \u2014 both implicit-environment reads the contract refuses.\n return "", fmt.Errorf("format_date: unresolvable timeZone %q", tz)\n } else {\n loc, err := time.LoadLocation(tz)\n if err != nil {\n return "", fmt.Errorf("format_date: unresolvable timeZone %q", tz)\n }\n _, offsetSeconds = t.UTC().In(loc).Zone()\n }\n }\n shifted := t.UTC().Add(time.Duration(offsetSeconds) * time.Second)\n year := shifted.Year()\n month := int(shifted.Month())\n day := shifted.Day()\n weekday := int(shifted.Weekday()) // time.Sunday == 0, matching the table\'s Sunday-first layout\n absYear := year\n if absYear < 0 {\n absYear = -absYear\n }\n yyyy := fmt.Sprintf("%04d", absYear)\n if year < 0 {\n yyyy = "-" + yyyy\n }\n out := formatDateTokenRE.ReplaceAllStringFunc(pattern, func(token string) string {\n switch token {\n case "YYYY":\n return yyyy\n case "MMMM":\n return formatDateName(names, monthsWide+month-1)\n case "MMM":\n return formatDateName(names, monthsAbbr+month-1)\n case "MM":\n return fmt.Sprintf("%02d", month)\n case "M":\n return strconv.Itoa(month)\n case "DD":\n return fmt.Sprintf("%02d", day)\n case "D":\n return strconv.Itoa(day)\n case "dddd":\n return formatDateName(names, weekdaysWide+weekday)\n case "ddd":\n return formatDateName(names, weekdaysAbbr+weekday)\n default:\n return token\n }\n })\n return out, nil\n}\n\n// toTime normalizes a `Date` helper receiver to a `time.Time`: the runtime\'s\n// own `time.Time` / `*time.Time`, or an ISO-8601 string parsed with\n// `time.RFC3339Nano` (accepts both the `Z`-suffixed and numeric-offset\n// forms, and any sub-second precision \u2014 including the millisecond precision\n// every value this runtime itself ever produces via `toISOString` above).\n// Anything else (nil, an unparsable string, an unrelated type) reports !ok\n// so `Date` can apply its documented zero-value fallback instead of\n// panicking.\nfunc toTime(recv any) (time.Time, bool) {\n switch v := recv.(type) {\n case time.Time:\n return v, true\n case *time.Time:\n if v == nil {\n return time.Time{}, false\n }\n return *v, true\n case string:\n t, err := time.Parse(time.RFC3339Nano, v)\n if err != nil {\n return time.Time{}, false\n }\n return t, true\n default:\n return time.Time{}, false\n }\n}\n\n// asStringSlice reports whether v is a query *array* value and, if so, returns\n// its members stringified. A compiled template passes a `[]string` field; the\n// golden conformance vectors decode JSON arrays to `[]any`. Anything else is a\n// scalar (false), handled by the set() path.\nfunc asStringSlice(v any) ([]string, bool) {\n switch s := v.(type) {\n case []string:\n return s, true\n case []any:\n out := make([]string, len(s))\n for i, m := range s {\n out[i] = String(m)\n }\n return out, true\n default:\n return nil, false\n }\n}\n\nconst hexUpper = "0123456789ABCDEF"\n\n// formEscape percent-encodes s with the application/x-www-form-urlencoded byte\n// set, matching the browser\'s URLSearchParams serialization (and the Perl\n// `query` helper) so SSR query strings render byte-for-byte identically across\n// adapters. The unreserved set kept verbatim is A-Z a-z 0-9 and `* - . _`; a\n// space becomes `+`; every other byte is `%XX` with uppercase hex. Encoding is\n// byte-wise, so multi-byte UTF-8 is percent-encoded per byte (`\xE9` \u2192 `%C3%A9`).\n//\n// This differs from url.QueryEscape only for `~` (kept by QueryEscape, encoded\n// to `%7E` here) and `*` (encoded to `%2A` by QueryEscape, kept here).\nfunc formEscape(s string) string {\n var b strings.Builder\n for i := 0; i < len(s); i++ {\n c := s[i]\n switch {\n case c >= \'A\' && c <= \'Z\', c >= \'a\' && c <= \'z\', c >= \'0\' && c <= \'9\',\n c == \'*\', c == \'-\', c == \'.\', c == \'_\':\n b.WriteByte(c)\n case c == \' \':\n b.WriteByte(\'+\')\n default:\n b.WriteByte(\'%\')\n b.WriteByte(hexUpper[c>>4])\n b.WriteByte(hexUpper[c&0x0F])\n }\n }\n return b.String()\n}\n\n// ScopeAttr returns the bare bf-s scope id (#1249).\nfunc ScopeAttr(props interface{}) string {\n return getStringField(props, "ScopeID")\n}\n\n// HydrationAttrs emits `bf-h="<host>" bf-m="<slot>" bf-r=""` conditionally.\n// See spec/compiler.md "Slot identity".\nfunc HydrationAttrs(props interface{}) template.HTMLAttr {\n parts := []string{}\n if host := getStringField(props, "BfParent"); host != "" {\n parts = append(parts, fmt.Sprintf(`bf-h="%s"`, template.HTMLEscapeString(host)))\n }\n if mount := getStringField(props, "BfMount"); mount != "" {\n parts = append(parts, fmt.Sprintf(`bf-m="%s"`, template.HTMLEscapeString(mount)))\n }\n if !getBoolField(props, "BfIsChild") {\n parts = append(parts, `bf-r=""`)\n }\n if len(parts) == 0 {\n return ""\n }\n return template.HTMLAttr(strings.Join(parts, " "))\n}\n\n// IsChild is a deprecated no-op stub. Child status is signalled by bf-h\n// presence (#1249); use HydrationAttrs instead.\nfunc IsChild(props interface{}) template.HTMLAttr {\n return ""\n}\n\n// svgCamelCaseAttrs mirrors SVG_CAMEL_CASE_ATTRS from\n// packages/client/src/runtime/spread-attrs.ts. SVG XML attribute\n// names are case-sensitive; the default camelCase \u2192 kebab-case\n// rewrite must NOT apply to these or the SVG stops rendering\n// (#1407). Coordinates with the compile-time SVG_CAMEL_TO_KEBAB\n// table in packages/jsx/src/ir-to-client-js/utils.ts: presentation\n// attrs (clipPath, strokeWidth, \u2026) live there and must NOT appear\n// here, or the same JSX prop would lower to clip-path via the\n// explicit-attr path and stay clipPath via the spread path.\nvar svgCamelCaseAttrs = map[string]struct{}{\n "allowReorder": {}, "attributeName": {}, "attributeType": {}, "autoReverse": {},\n "baseFrequency": {}, "baseProfile": {}, "calcMode": {}, "clipPathUnits": {},\n "contentScriptType": {}, "contentStyleType": {}, "diffuseConstant": {}, "edgeMode": {},\n "externalResourcesRequired": {}, "filterRes": {}, "filterUnits": {}, "glyphRef": {},\n "gradientTransform": {}, "gradientUnits": {}, "kernelMatrix": {}, "kernelUnitLength": {},\n "keyPoints": {}, "keySplines": {}, "keyTimes": {}, "lengthAdjust": {}, "limitingConeAngle": {},\n "markerHeight": {}, "markerUnits": {}, "markerWidth": {}, "maskContentUnits": {},\n "maskUnits": {}, "numOctaves": {}, "pathLength": {}, "patternContentUnits": {},\n "patternTransform": {}, "patternUnits": {}, "pointsAtX": {}, "pointsAtY": {}, "pointsAtZ": {},\n "preserveAlpha": {}, "preserveAspectRatio": {}, "primitiveUnits": {}, "refX": {}, "refY": {},\n "repeatCount": {}, "repeatDur": {}, "requiredExtensions": {}, "requiredFeatures": {},\n "specularConstant": {}, "specularExponent": {}, "spreadMethod": {}, "startOffset": {},\n "stdDeviation": {}, "stitchTiles": {}, "surfaceScale": {}, "systemLanguage": {},\n "tableValues": {}, "targetX": {}, "targetY": {}, "textLength": {}, "viewBox": {}, "viewTarget": {},\n "xChannelSelector": {}, "yChannelSelector": {}, "zoomAndPan": {},\n}\n\n// toAttrName mirrors the JSX\u2192HTML attribute-name rewrite from\n// packages/client/src/runtime/spread-attrs.ts. className \u2192 class,\n// htmlFor \u2192 for, SVG camelCase attrs preserved, other camelCase\n// keys lowered to kebab-case.\nfunc toAttrName(key string) string {\n if key == "className" {\n return "class"\n }\n if key == "htmlFor" {\n return "for"\n }\n if _, ok := svgCamelCaseAttrs[key]; ok {\n return key\n }\n // camelCase \u2192 kebab-case: mirror the JS reference exactly\n // (`key.replace(/([A-Z])/g, \'-$1\').toLowerCase()`). The JS shape\n // produces a leading `-` for an initial uppercase letter\n // (`XData` \u2192 `-x-data`); both this Go path and the matching JS\n // runtime are wrong-by-construction for that case (the resulting\n // HTML attribute name is invalid), but keeping them byte-equal\n // avoids silent SSR/CSR divergence (#1411 review).\n var b strings.Builder\n for _, r := range key {\n if r >= \'A\' && r <= \'Z\' {\n b.WriteByte(\'-\')\n b.WriteRune(r + 32)\n } else {\n b.WriteRune(r)\n }\n }\n return b.String()\n}\n\n// hasUnsafeStyleValue mirrors Hono\'s own CSS-injection guard\n// (`hono/jsx/utils.ts`\'s `hasUnsafeStyleValue` \u2014 the ORACLE this adapter\'s\n// dynamic `style={{...}}` values must match, #2261): a hand-rolled\n// structural scan for characters that could break out of a CSS\n// declaration, NOT real CSSOM property validation. Ported byte-for-byte \u2014\n// every character this scan tests is ASCII, so scanning by byte (Go\n// string indexing) agrees with Hono\'s UTF-16-code-unit scan for every\n// input; a multibyte UTF-8 sequence has no byte in the ASCII range, so it\n// can never spuriously match one of these single-byte comparisons. Skips\n// the reference implementation\'s regex fast-path (a pure optimization \u2014\n// the scan below already returns `false` promptly for a clean value).\nfunc hasUnsafeStyleValue(value string) bool {\n quote := byte(0)\n blockStack := make([]byte, 0, 4)\n for i := 0; i < len(value); i++ {\n c := value[i]\n switch {\n case c == \'\\\\\':\n if i == len(value)-1 {\n return true\n }\n i++\n case quote != 0:\n if c == \'\\n\' || c == \'\\f\' || c == \'\\r\' {\n return true\n }\n if c == quote {\n quote = 0\n }\n case c == \'/\' && i+1 < len(value) && value[i+1] == \'*\':\n end := strings.Index(value[i+2:], "*/")\n if end == -1 {\n return true\n }\n i = i + 2 + end + 1\n case c == \'"\' || c == \'\\\'\':\n quote = c\n case c == \'(\':\n blockStack = append(blockStack, \')\')\n case c == \'[\':\n blockStack = append(blockStack, \']\')\n case c == \'{\' || c == \'}\':\n return true\n case c == \')\' || c == \']\':\n if len(blockStack) == 0 || blockStack[len(blockStack)-1] != c {\n return true\n }\n blockStack = blockStack[:len(blockStack)-1]\n case c == \';\' && len(blockStack) == 0:\n return true\n }\n }\n return quote != 0 || len(blockStack) != 0\n}\n\n// StyleObjectToCSS builds the CSS string for a `style={{...}}` JSX\n// object-literal attribute (#2261) \u2014 `pairs` alternates CSS key (always a\n// compile-time-known literal), then value (`any`, possibly a runtime\n// expression\'s result). A value that fails `hasUnsafeStyleValue` (after\n// JS-`String()`-style stringification) is DROPPED \u2014 the whole `key:value`\n// pair is omitted \u2014 matching Hono\'s oracle behavior exactly, rather than\n// html/template\'s own contextual CSS auto-escaper (which instead emits its\n// `ZgotmplZ` unsafe-content sentinel for the same input). The final joined\n// string is STILL HTML-escaped (mirroring Hono\'s own `escapeToBuffer` call\n// on its accumulated style string) \u2014 a "safe" value can still carry a\n// literal `"`/`\'`/`&` (e.g. a BALANCED-quote CSS string value like\n// `"hello"` passes the structural scan; the quote chars survive into the\n// value) that would otherwise break out of the double-quoted `style="..."`\n// attribute. Returns `template.CSS` (over the escaped result) so\n// html/template treats it as trusted CSS content instead of ALSO applying\n// its own contextual CSS auto-escaper (which would re-derive the exact\n// `ZgotmplZ` divergence this function exists to avoid).\nfunc StyleObjectToCSS(pairs ...any) template.CSS {\n parts := make([]string, 0, len(pairs)/2)\n for i := 0; i+1 < len(pairs); i += 2 {\n key := fmt.Sprint(pairs[i])\n value := String(pairs[i+1])\n if hasUnsafeStyleValue(value) {\n continue\n }\n parts = append(parts, template.HTMLEscapeString(key)+":"+template.HTMLEscapeString(value))\n }\n return template.CSS(strings.Join(parts, ";"))\n}\n\n// StyleToCss mirrors styleToCss from\n// packages/client/src/runtime/style.ts. Accepts a string passthrough,\n// or a map (JSON-deserialized object) whose camelCase keys are\n// lowered to kebab-case and joined with `;`. Returns ("", false) for\n// nullish/empty input so callers can omit the attribute entirely.\nfunc StyleToCss(v any) (string, bool) {\n if v == nil {\n return "", false\n }\n rv := reflect.ValueOf(v)\n for rv.Kind() == reflect.Interface || rv.Kind() == reflect.Pointer {\n if rv.IsNil() {\n return "", false\n }\n rv = rv.Elem()\n }\n if rv.Kind() != reflect.Map {\n // Non-object: stringify and return as-is, matching the JS\n // `typeof value !== \'object\'` branch.\n s := fmt.Sprint(v)\n if s == "" {\n return "", false\n }\n return s, true\n }\n keys := rv.MapKeys()\n sorted := make([]string, 0, len(keys))\n for _, k := range keys {\n if k.Kind() == reflect.String {\n sorted = append(sorted, k.String())\n }\n }\n sort.Strings(sorted)\n parts := make([]string, 0, len(sorted))\n for _, k := range sorted {\n val := rv.MapIndex(reflect.ValueOf(k))\n // Skip nil entries (matches the JS `if (v == null) continue`).\n if !val.IsValid() {\n continue\n }\n if val.Kind() == reflect.Interface || val.Kind() == reflect.Pointer {\n if val.IsNil() {\n continue\n }\n val = val.Elem()\n }\n prop := toAttrName(k)\n parts = append(parts, fmt.Sprintf("%s:%v", prop, val.Interface()))\n }\n if len(parts) == 0 {\n return "", false\n }\n return strings.Join(parts, ";"), true\n}\n\n// SpreadAttrs lowers a JSX intrinsic-element spread bag (#1407) to\n// an HTML attribute string. Mirrors spreadAttrs from\n// packages/client/src/runtime/spread-attrs.ts so SSR output matches\n// what CSR\'s `applyRestAttrs` writes at hydration.\n//\n// Skip rules: nil/false values, event handlers (`on[A-Z]*`),\n// `children`, `ref`.\n//\n// Key remap: className \u2192 class, htmlFor \u2192 for, SVG camelCase\n// preserved, other camelCase \u2192 kebab-case.\n//\n// `style` is routed through StyleToCss so object literals serialize\n// to a real CSS string instead of Go\'s default `map[k:v]` form.\n//\n// Booleans: true \u2192 bare attribute name, false \u2192 omitted.\n// Other scalar values are HTML-escaped via template.HTMLEscapeString.\n// Returns a `template.HTMLAttr` so html/template emits the result\n// verbatim (the function does its own escaping).\n//\n// Keys are sorted alphabetically before emission for deterministic\n// output. SSR/CSR attribute-order divergence is acceptable per the\n// rest-destructure-object-spread-in-map fixture\'s documented policy\n// \u2014 browsers honor the LAST value when a key is duplicated, so\n// pairing with static attrs (`<div class="x" {...rest}>`) is\n// last-wins regardless of order.\nfunc SpreadAttrs(bag any) template.HTMLAttr {\n if bag == nil {\n return ""\n }\n rv := reflect.ValueOf(bag)\n for rv.Kind() == reflect.Interface || rv.Kind() == reflect.Pointer {\n if rv.IsNil() {\n return ""\n }\n rv = rv.Elem()\n }\n if rv.Kind() != reflect.Map {\n return ""\n }\n keys := rv.MapKeys()\n sortedKeys := make([]string, 0, len(keys))\n for _, k := range keys {\n if k.Kind() == reflect.String {\n sortedKeys = append(sortedKeys, k.String())\n }\n }\n sort.Strings(sortedKeys)\n parts := make([]string, 0, len(sortedKeys))\n for _, key := range sortedKeys {\n // Event handlers \u2014 skip at SSR the same way\n // packages/client/src/runtime/spread-attrs.ts does at\n // hydration. The JS predicate is\n // `key.startsWith(\'on\') && key.length > 2 && key[2] === key[2].toUpperCase()`,\n // which is true for any character whose uppercase form is\n // itself: ASCII A-Z, digits, underscore, and non-letter\n // symbols. Mirror that here by skipping when key[2] is NOT\n // a lowercase ASCII letter \u2014 so `onClick`, `on_custom`, and\n // `on0` all match (#1411 review).\n if len(key) > 2 && key[0] == \'o\' && key[1] == \'n\' && !(key[2] >= \'a\' && key[2] <= \'z\') {\n continue\n }\n // `children` is a JSX construct rendered inside the element,\n // never a DOM attribute. `ref` is intentionally NOT filtered\n // here so output stays byte-equal with the JS reference\n // `spreadAttrs` in packages/client/src/runtime/spread-attrs.ts\n // (which only filters null/false, event handlers, and\n // children) \u2014 aligning Go\'s filter set diverges from JS in\n // the opposite direction. Filtering `ref` consistently across\n // both SSR runtimes is a separate concern tracked alongside\n // the JS `applyRestAttrs` vs `spreadAttrs` mismatch (#1411\n // review).\n if key == "children" {\n continue\n }\n val := rv.MapIndex(reflect.ValueOf(key))\n if !val.IsValid() {\n continue\n }\n // Unwrap interface wrappers (json.Unmarshal produces\n // interface{}-wrapped values for map[string]any).\n v := val\n for v.Kind() == reflect.Interface || v.Kind() == reflect.Pointer {\n if v.IsNil() {\n // Skip null entries.\n v = reflect.Value{}\n break\n }\n v = v.Elem()\n }\n if !v.IsValid() {\n continue\n }\n // Boolean values: true \u2192 bare attribute, false \u2192 omitted.\n if v.Kind() == reflect.Bool {\n if !v.Bool() {\n continue\n }\n parts = append(parts, toAttrName(key))\n continue\n }\n // `style` routes through StyleToCss so object literals get a\n // real CSS string. The JS side does the same.\n if key == "style" {\n css, ok := StyleToCss(v.Interface())\n if !ok {\n continue\n }\n parts = append(parts, fmt.Sprintf(`style="%s"`, template.HTMLEscapeString(css)))\n continue\n }\n // Stringify and escape. fmt.Sprint handles numbers, bools-as-\n // strings, and arbitrary stringer types the same way the JS\n // `String(value)` coercion does for the analogous cases.\n s := fmt.Sprint(v.Interface())\n parts = append(parts, fmt.Sprintf(`%s="%s"`, toAttrName(key), template.HTMLEscapeString(s)))\n }\n if len(parts) == 0 {\n return ""\n }\n return template.HTMLAttr(strings.Join(parts, " "))\n}\n\n// Omit builds a `map[string]any` residual bag from a struct or map value,\n// excluding the given keys \u2014 powers the `{...rest}` spread-onto-element\n// lowering for a destructured `.map()` loop-item\'s object-rest binding\n// (#2087): `.map(({ id, title, ...rest }) => <li {...rest}>)` needs "every\n// field EXCEPT the ones the pattern already destructured out", and a static\n// Go struct type has no way to express "minus a field" \u2014 the exclude set is\n// known at COMPILE TIME (the sibling keys the destructure pattern names), so\n// the compiler passes them here and this does the per-item field-vs-key\n// matching a static type can\'t. The result feeds `bf_spread_attrs`\n// (`SpreadAttrs`), same as a top-level `{...attrs()}` bag.\n//\n// Struct receiver: iterates exported fields via reflection, keyed by each\n// field\'s `json` struct tag (falling back to the Go field name when absent)\n// \u2014 the generated struct\'s json tag is always the ORIGINAL source property\n// name (see `structFieldsFor` / `typeDefinitionToGo` in the Go adapter), so\n// this reproduces the exact JS key `SpreadAttrs`\'s `toAttrName` expects\n// (`"data-priority"`, not a re-derived `"DataPriority"`). A tag of `"-"`\n// (opt-out) is skipped like `encoding/json` does.\n//\n// Map receiver: copies string keys through directly, same exclude/skip\n// rules.\n//\n// Anything else (nil, a non-struct/non-map interface) returns an empty map.\nfunc Omit(item any, excludeKeys ...string) map[string]any {\n exclude := make(map[string]struct{}, len(excludeKeys))\n for _, k := range excludeKeys {\n exclude[k] = struct{}{}\n }\n out := map[string]any{}\n rv := reflect.ValueOf(item)\n for rv.Kind() == reflect.Interface || rv.Kind() == reflect.Pointer {\n if rv.IsNil() {\n return out\n }\n rv = rv.Elem()\n }\n switch rv.Kind() {\n case reflect.Struct:\n rt := rv.Type()\n for i := 0; i < rt.NumField(); i++ {\n field := rt.Field(i)\n if !field.IsExported() {\n continue\n }\n key := field.Name\n if tag, ok := field.Tag.Lookup("json"); ok {\n if comma := strings.Index(tag, ","); comma >= 0 {\n tag = tag[:comma]\n }\n if tag == "-" {\n continue\n }\n if tag != "" {\n key = tag\n }\n }\n if _, skip := exclude[key]; skip {\n continue\n }\n out[key] = rv.Field(i).Interface()\n }\n case reflect.Map:\n for _, k := range rv.MapKeys() {\n if k.Kind() != reflect.String {\n continue\n }\n key := k.String()\n if _, skip := exclude[key]; skip {\n continue\n }\n val := rv.MapIndex(k)\n if val.IsValid() {\n out[key] = val.Interface()\n }\n }\n }\n return out\n}\n\n// BfPropsAttr returns the bf-p attribute with the JSON-serialized\n// props in flat format. Output format: `bf-p=\'{"propName":value,...}\'`.\n// Only emits the attribute for root components (BfIsRoot == true);\n// child components receive props from their parent via initChild().\n//\n// Returns the marshal error so a `template.Execute` call fails\n// loudly on cycles / unsupported props rather than silently\n// dropping the bf-p attribute and breaking client-side hydration.\n// Same loud-failure policy as `JSON` \u2014 user data going through\n// `encoding/json` shouldn\'t fail invisibly.\nfunc BfPropsAttr(props interface{}) (template.HTMLAttr, error) {\n // Only root components should emit bf-p\n if !getBoolField(props, "BfIsRoot") {\n return "", nil\n }\n\n propsJSON, err := json.Marshal(props)\n if err != nil {\n return "", err\n }\n\n escaped := template.HTMLEscapeString(string(propsJSON))\n return template.HTMLAttr(`bf-p="` + escaped + `"`), nil\n}\n\n// =============================================================================\n// Arithmetic Operations\n// =============================================================================\n\n// Add returns a + b. Supports int and float64.\nfunc Add(a, b any) any {\n av, bv := toFloat64(a), toFloat64(b)\n result := av + bv\n // Return int if both inputs were int-like\n if isIntLike(a) && isIntLike(b) && result == float64(int(result)) {\n return int(result)\n }\n return result\n}\n\n// ConcatStr returns a and b concatenated as strings \u2014 the string-typed half\n// of JS `+` (#2168 string-concat-plus). JS `+` is addition when BOTH\n// operands are numeric and concatenation when EITHER is a string; `Add`\n// (above) covers the numeric case, this covers the string one \u2014 `Add`\n// itself can\'t (`toFloat64` returns 0 for a string operand, so `\'Hello, \' +\n// name` silently rendered "0" before this existed).\nfunc ConcatStr(a, b any) string {\n return toString(a) + toString(b)\n}\n\n// Sub returns a - b. Supports int and float64.\nfunc Sub(a, b any) any {\n av, bv := toFloat64(a), toFloat64(b)\n result := av - bv\n if isIntLike(a) && isIntLike(b) && result == float64(int(result)) {\n return int(result)\n }\n return result\n}\n\n// Mul returns a * b. Supports int and float64.\nfunc Mul(a, b any) any {\n av, bv := toFloat64(a), toFloat64(b)\n result := av * bv\n if isIntLike(a) && isIntLike(b) && result == float64(int(result)) {\n return int(result)\n }\n return result\n}\n\n// Div returns a / b. Returns float64 to match JavaScript behavior.\n// Returns 0 if b is 0 (instead of panicking).\nfunc Div(a, b any) any {\n av, bv := toFloat64(a), toFloat64(b)\n if bv == 0 {\n return 0\n }\n return av / bv\n}\n\n// Min returns the smaller of a and b (two-arg `Math.min`). Like Mul, it keeps\n// an integer result when both operands are int-like so a CSS value such as\n// `bf_min 100 x` stays `100` rather than `100.000000`. Uses `Number` (not\n// `toFloat64`, which silently zeroes an unrecognized type like a non-numeric\n// string) plus explicit NaN checks, since IEEE-754 `<`/`>` comparisons\n// against NaN are always false and would otherwise let a non-NaN operand\n// win instead of propagating NaN like JS `Math.min`/`Math.max` do.\nfunc Min(a, b any) any {\n av, bv := Number(a), Number(b)\n if math.IsNaN(av) {\n return av\n }\n if math.IsNaN(bv) {\n return bv\n }\n r := av\n if bv < av {\n r = bv\n }\n if isIntLike(a) && isIntLike(b) && r == float64(int(r)) {\n return int(r)\n }\n return r\n}\n\n// Max returns the larger of a and b (two-arg `Math.max`), with the same\n// int-preserving rule and NaN-propagation as Min.\nfunc Max(a, b any) any {\n av, bv := Number(a), Number(b)\n if math.IsNaN(av) {\n return av\n }\n if math.IsNaN(bv) {\n return bv\n }\n r := av\n if bv > av {\n r = bv\n }\n if isIntLike(a) && isIntLike(b) && r == float64(int(r)) {\n return int(r)\n }\n return r\n}\n\n// Mod returns a % b (modulo). Supports int only.\nfunc Mod(a, b any) int {\n av, bv := toInt(a), toInt(b)\n if bv == 0 {\n return 0\n }\n return av % bv\n}\n\n// Neg returns -a (negation).\nfunc Neg(a any) any {\n if v, ok := a.(int); ok {\n return -v\n }\n return -toFloat64(a)\n}\n\n// =============================================================================\n// String Operations\n// =============================================================================\n\n// Lower returns the lowercase version of s.\nfunc Lower(s string) string {\n return strings.ToLower(s)\n}\n\n// Upper returns the uppercase version of s.\nfunc Upper(s string) string {\n return strings.ToUpper(s)\n}\n\n// Trim returns s with leading and trailing whitespace removed.\nfunc Trim(s string) string {\n return strings.TrimSpace(s)\n}\n\n// TrimStart returns s with leading whitespace removed\n// (String.prototype.trimStart, #2183) \u2014 the one-sided sibling of\n// Trim above, using the same unicode.IsSpace predicate strings.TrimSpace\n// applies to both sides.\nfunc TrimStart(s string) string {\n return strings.TrimLeftFunc(s, unicode.IsSpace)\n}\n\n// TrimEnd returns s with trailing whitespace removed\n// (String.prototype.trimEnd, #2183) \u2014 the one-sided sibling of Trim.\nfunc TrimEnd(s string) string {\n return strings.TrimRightFunc(s, unicode.IsSpace)\n}\n\n// Contains returns true if s contains substr.\nfunc Contains(s, substr string) bool {\n return strings.Contains(s, substr)\n}\n\n// Split lowers `String.prototype.split(sep, limit?)` (#1448 Tier B). It\n// wraps `strings.Split` and normalises the result to `[]any` so the\n// slice composes with the array-method surface downstream (`bf_join`,\n// range loops, `bf_len`, \u2026) the same way `bf_slice` / `bf_reverse`\n// results do. Like JS, an empty separator splits into individual UTF-8\n// characters and trailing empty fields are preserved (`"a,".split(",")`\n// \u2192 `["a", ""]`). An optional `limit` caps the number of returned\n// pieces (`"a,b,c".split(",", 2)` \u2192 `["a", "b"]`); a negative limit is\n// ignored (JS would also return every piece \u2014 its ToUint32 wrap makes\n// the limit effectively unbounded). The no-separator form is handled by\n// the adapter (it emits `bf_arr` for the whole-string single element).\nfunc Split(s, sep string, limit ...int) []any {\n parts := strings.Split(s, sep)\n if len(limit) > 0 && limit[0] >= 0 && limit[0] < len(parts) {\n parts = parts[:limit[0]]\n }\n out := make([]any, len(parts))\n for i, p := range parts {\n out[i] = p\n }\n return out\n}\n\n// StartsWith lowers `String.prototype.startsWith(prefix, position?)`\n// (#1448 Tier B). Wraps `strings.HasPrefix`; an empty prefix is always\n// true (JS parity). The optional `position` re-anchors the test to start\n// at that index (clamped to `[0, len]` so it never panics), matching JS\n// `"abc".startsWith("b", 1) === true`.\nfunc StartsWith(s, prefix string, position ...int) bool {\n if len(position) > 0 {\n p := position[0]\n if p < 0 {\n p = 0\n }\n if p > len(s) {\n p = len(s)\n }\n s = s[p:]\n }\n return strings.HasPrefix(s, prefix)\n}\n\n// EndsWith lowers `String.prototype.endsWith(suffix, endPosition?)`\n// (#1448 Tier B). Wraps `strings.HasSuffix`; an empty suffix is always\n// true (JS parity). The optional `endPosition` treats the string as if\n// it were only that many bytes long (clamped to `[0, len]`), matching JS\n// `"abc".endsWith("b", 2) === true`.\nfunc EndsWith(s, suffix string, endPosition ...int) bool {\n if len(endPosition) > 0 {\n e := endPosition[0]\n if e < 0 {\n e = 0\n }\n if e > len(s) {\n e = len(s)\n }\n s = s[:e]\n }\n return strings.HasSuffix(s, suffix)\n}\n\n// Replace lowers the string-pattern form of `String.prototype.replace`\n// (#1448 Tier B). JS replaces only the FIRST occurrence for a string\n// pattern, so the count is 1 (`strings.Replace` with n=1; `ReplaceAll`\n// below is the every-occurrence sibling, `.replaceAll`, #2182). The\n// replacement is treated literally: unlike JS, special replacement\n// patterns like `$&` / `$1` are NOT interpreted (Go and Perl agree on\n// literal replacement, keeping the two template adapters byte-equal;\n// this diverges from the Hono/CSR JS path only for replacement strings\n// that contain `$`-patterns, which are rare in template position).\nfunc Replace(s, old, new string) string {\n return strings.Replace(s, old, new, 1)\n}\n\n// ReplaceAll lowers the string-pattern form of\n// `String.prototype.replaceAll` (#2182): every occurrence, via\n// `strings.ReplaceAll` (equivalent to `strings.Replace` with n=-1).\n// Same literal-replacement caveat as `Replace` above.\nfunc ReplaceAll(s, old, new string) string {\n return strings.ReplaceAll(s, old, new)\n}\n\n// Repeat lowers `String.prototype.repeat(n)` (#1448 Tier B): the\n// receiver concatenated n times. JS throws RangeError for a negative\n// count and `strings.Repeat` panics, so a negative count clamps to the\n// empty string \u2014 SSR templates degrade rather than crash the render.\n// A zero count is the empty string (JS parity).\nfunc Repeat(s string, n int) string {\n if n <= 0 {\n return ""\n }\n return strings.Repeat(s, n)\n}\n\n// padTo lowers the shared body of `String.prototype.padStart` /\n// `padEnd` (#1448 Tier B): pad `s` to `target` code points using `pad`\n// repeated and truncated to fill, prepended (atStart) or appended.\n// Length is measured in runes (not bytes) so the result matches the\n// Perl `bf->pad_*` helpers \u2014 this diverges from JS\'s UTF-16-unit length\n// only for astral-plane input. An empty pad, or a receiver already at\n// least `target` long, returns `s` unchanged (JS parity).\nfunc padTo(s string, target int, pad string, atStart bool) string {\n if pad == "" {\n return s\n }\n sLen := utf8.RuneCountInString(s)\n if sLen >= target {\n return s\n }\n need := target - sLen\n padRunes := []rune(pad)\n fill := make([]rune, 0, need)\n for len(fill) < need {\n for _, r := range padRunes {\n if len(fill) >= need {\n break\n }\n fill = append(fill, r)\n }\n }\n if atStart {\n return string(fill) + s\n }\n return s + string(fill)\n}\n\n// PadStart lowers `String.prototype.padStart(target, pad?)` (#1448 Tier\n// B). The pad string defaults to a single space when omitted.\nfunc PadStart(s string, target int, pad ...string) string {\n p := " "\n if len(pad) > 0 {\n p = pad[0]\n }\n return padTo(s, target, p, true)\n}\n\n// PadEnd lowers `String.prototype.padEnd(target, pad?)` (#1448 Tier B).\nfunc PadEnd(s string, target int, pad ...string) string {\n p := " "\n if len(pad) > 0 {\n p = pad[0]\n }\n return padTo(s, target, p, false)\n}\n\n// Join concatenates elements of a slice with sep. Accepts both\n// reflect.Slice (the common case \u2014 `bf_arr` and `bf_filter_truthy`\n// both return `[]any`) AND reflect.Array (fixed-size Go arrays like\n// `[3]string{...}`), mirroring JS `Array.prototype.join` which\n// doesn\'t distinguish between the two. Pre-fix this returned "" for\n// fixed-size arrays passed through template data (Copilot review on\n// #1445).\nfunc Join(items any, sep string) string {\n v := reflect.ValueOf(items)\n if v.Kind() != reflect.Slice && v.Kind() != reflect.Array {\n return ""\n }\n\n parts := make([]string, v.Len())\n for i := 0; i < v.Len(); i++ {\n parts[i] = toString(v.Index(i).Interface())\n }\n return strings.Join(parts, sep)\n}\n\n// Ternary returns a when cond is true, else b \u2014 the pipeline-position\n// counterpart of a template {{if}} action. Go templates have no\n// expression-level conditional, so a conditional value sitting in\n// ARGUMENT position (a lowering-node helper arg, e.g. the #2324 union\n// stage\'s locale\u2192pattern ternary) cannot be emitted as an {{if}}\n// fragment; the adapter renders it as `(bf_ternary <cond> <a> <b>)`\n// instead. Both branches are evaluated (function-call semantics) \u2014\n// fine for the value shapes the emitter feeds it, wrong for anything\n// with side effects, which template values never have.\nfunc Ternary(cond bool, a, b any) any {\n if cond {\n return a\n }\n return b\n}\n\n// String returns the string form of v. Mirrors JS `String(v)` for\n// non-nil values via `fmt.Sprintf("%v", ...)`. Diverges from JS on\n// nil: JS `String(null)` is "null", but the template path renders\n// `nil` as the empty string here so an unset prop doesn\'t surface\n// as a literal "null"/"undefined" in user-facing HTML. Document the\n// divergence explicitly so callers don\'t rely on JS-exact parity.\nfunc String(v any) string {\n if v == nil {\n return ""\n }\n return fmt.Sprintf("%v", v)\n}\n\n// RawHTML marks a value as trusted, pre-formatted HTML so html/template\'s\n// contextual escaper emits it verbatim instead of escaping it. It is the SSR\n// half of a dynamic `dangerouslySetInnerHTML={{ __html: expr }}` (#2319) \u2014\n// the one raw-output sink Go lacks as bare template syntax, the counterpart\n// to Blade `{!! !!}`, ERB `<%= %>`, Jinja/MiniJinja `| safe`, Twig `| raw`,\n// Mojolicious `<%== %>`, and Xslate `mark_raw`. The caller owns the value\'s\n// safety (React\'s "dangerously" contract); a nil value renders "".\nfunc RawHTML(v any) template.HTML {\n return template.HTML(String(v))\n}\n\n// JSON returns the JSON encoding of v as a string. Mirrors\n// JS `JSON.stringify(v)` for the V1 single-arg shape (no `replacer`\n// or `space`). Object key order is determined by Go\'s `encoding/json`\n// (alphabetical for maps, declaration order for structs) \u2014 the\n// #1187 contract requires value-compat, not order-compat.\n//\n// Top-level NaN / \xB1Inf are pre-handled to match JS \u2014 JS\'s\n// `JSON.stringify(NaN)` and `JSON.stringify(Infinity)` both produce\n// `"null"`, but Go\'s `encoding/json` rejects them with\n// `UnsupportedValueError`. Without this carve-out the common\n// composition `JSON.stringify(Number("garbage"))` would error\n// instead of emitting `"null"` like JS does. Nested NaN/Inf inside\n// a struct/map still surfaces an error \u2014 covering that needs a\n// custom marshaller; out of V1 scope.\n//\n// Returns the marshal error so a `template.Execute` call fails\n// loudly on cycles / unsupported values rather than silently\n// producing `""` and reintroducing the SSR data-loss class\n// #1187 was filed against. Go\'s text/template treats a non-nil\n// error return from a func as an execution failure.\nfunc JSON(v any) (string, error) {\n if f, ok := v.(float64); ok && (math.IsNaN(f) || math.IsInf(f, 0)) {\n return "null", nil\n }\n b, err := json.Marshal(v)\n if err != nil {\n return "", err\n }\n return string(b), nil\n}\n\n// Number coerces v to a float64. Mirrors JS `Number(v)` semantics:\n// numeric / boolean inputs convert as expected; non-numeric strings\n// and other unsupported shapes return `NaN` (matching JS rather\n// than silently substituting 0, which would mis-shape downstream\n// arithmetic and template-side comparisons). Templates that need\n// a deterministic fallback should compose with the user-side\n// default (e.g. `Number(props.x ?? 0)` in JSX).\nfunc Number(v any) float64 {\n if v == nil {\n return math.NaN()\n }\n switch x := v.(type) {\n case float64:\n return x\n case float32:\n return float64(x)\n case int:\n return float64(x)\n case int32:\n return float64(x)\n case int64:\n return float64(x)\n case bool:\n if x {\n return 1\n }\n return 0\n case string:\n f, err := strconv.ParseFloat(x, 64)\n if err != nil {\n return math.NaN()\n }\n return f\n }\n return math.NaN()\n}\n\n// Floor returns the largest integer \u2264 v as a float64. Mirrors JS\n// `Math.floor`. The return type stays float64 so chained primitives\n// (`bf_floor` then `bf_string`) line up with JS\'s number type.\nfunc Floor(v any) float64 {\n return math.Floor(Number(v))\n}\n\n// Abs returns the absolute value of v as a float64, mirroring JS\n// `Math.abs`. #2168 math-methods.\nfunc Abs(v any) float64 {\n return math.Abs(Number(v))\n}\n\n// ToFixed formats v with exactly `digits` decimal places, mirroring JS\n// `Number.prototype.toFixed` (zero-padding + half-toward-+Infinity\n// rounding). JS rounds the scaled integer half up (`(2.5).toFixed(0)`\n// is "3"); bare `fmt.Sprintf("%.*f")` rounds half-to-even ("2"), so we\n// scale, round with `Floor(x + 0.5)` (matching `Round`), then format\n// the exact multiple. #1897.\nfunc ToFixed(v any, digits int) string {\n if digits < 0 {\n digits = 0\n }\n n := Number(v)\n // JS toFixed returns the strings "NaN" / "Infinity" / "-Infinity" for\n // non-finite inputs; fmt would render "NaN"/"+Inf"/"-Inf".\n if math.IsNaN(n) {\n return "NaN"\n }\n if math.IsInf(n, 1) {\n return "Infinity"\n }\n if math.IsInf(n, -1) {\n return "-Infinity"\n }\n factor := math.Pow(10, float64(digits))\n rounded := math.Floor(n*factor + 0.5)\n return fmt.Sprintf("%.*f", digits, rounded/factor)\n}\n\n// Ceil returns the smallest integer \u2265 v as a float64. Mirrors JS\n// `Math.ceil`.\nfunc Ceil(v any) float64 {\n return math.Ceil(Number(v))\n}\n\n// Round returns v rounded to the nearest integer as a float64.\n// Mirrors JS `Math.round` \u2014 half-away-from-zero (Go\'s `math.Round`\n// matches; JS rounds half toward +Infinity which differs at .5\n// negatives; we accept that minor divergence since the conformance\n// contract is value-compat for the common positive case).\nfunc Round(v any) float64 {\n return math.Round(Number(v))\n}\n\n// =============================================================================\n// Array/Slice Operations\n// =============================================================================\n\n// Length lowers JS `.length`, matching JS semantics per receiver shape\n// (#2255): a slice/array/map counts ELEMENTS (`reflect.Value.Len`, same as\n// `Len` below), but a STRING counts UTF-16 CODE UNITS \u2014 JS\n// `String.prototype.length` counts UTF-16 code units, not bytes (Go\'s\n// native `len`) or codepoints. A codepoint outside the Basic Multilingual\n// Plane (astral, U+10000-U+10FFFF \u2014 e.g. \'\u{1F44D}\') is a surrogate PAIR in\n// UTF-16, so it counts as 2, not 1; `\'\u65E5\u672C\u8A9E\'` is 3 either way (BMP-only).\n// Routed from the `.length` member lowering\'s generic (non-array,\n// non-loop-slice) fallback \u2014 see `member()`\'s `bf_length` call site.\nfunc Length(v any) int {\n if v == nil {\n return 0\n }\n rv := reflect.ValueOf(v)\n switch rv.Kind() {\n case reflect.Slice, reflect.Array, reflect.Map, reflect.Chan:\n return rv.Len()\n case reflect.String:\n n := 0\n for _, r := range rv.String() {\n if r > 0xFFFF {\n n += 2\n } else {\n n++\n }\n }\n return n\n default:\n return 0\n }\n}\n\n// IsValidElement lowers React/Hono-style `isValidElement(x)` \u2014 the "is this\n// a renderable element (not plain text)?" predicate the `Slot` component\'s\n// `asChild` pattern (#2266) uses to decide whether to merge props into a\n// child ELEMENT (`children.tag`/`children.props`) or fall back to rendering\n// `children` as-is. The JS runtime checks `\'tag\' in x && \'props\' in x`; on\n// Go SSR a passed-through JSX child is represented as pre-rendered markup\n// (a plain string) OR \u2014 where a struct/map shape carrying `Tag`/`Props`\n// (case-insensitively, mirroring `bf_get`\'s field lookup) is available \u2014 an\n// element-shaped value. A plain string/number/bool/nil is never a valid\n// element, so `isValidElement` must NOT be lowered as bare truthiness\n// (previously done via `renderConditionExpr`) \u2014 a truthy non-empty STRING\n// child wrongly took the element-merge branch and panicked dereferencing\n// `.Props` on a string (`can\'t evaluate field Props in type interface {}`).\nfunc IsValidElement(v any) bool {\n rv := reflect.ValueOf(v)\n for rv.Kind() == reflect.Interface || rv.Kind() == reflect.Pointer {\n if rv.IsNil() {\n return false\n }\n rv = rv.Elem()\n }\n switch rv.Kind() {\n case reflect.Map:\n hasTag, hasProps := false, false\n for _, k := range rv.MapKeys() {\n key := fmt.Sprintf("%v", k.Interface())\n if strings.EqualFold(key, "tag") {\n hasTag = true\n }\n if strings.EqualFold(key, "props") {\n hasProps = true\n }\n }\n return hasTag && hasProps\n case reflect.Struct:\n return fieldByFoldedName(rv, "tag").IsValid() && fieldByFoldedName(rv, "props").IsValid()\n default:\n return false\n }\n}\n\n// fieldByFoldedName finds a struct field by case-insensitive name match \u2014\n// shared by IsValidElement; mirrors getFieldValue\'s (bf_get) struct-branch\n// lookup so the two case-tolerant field resolutions stay consistent.\nfunc fieldByFoldedName(rv reflect.Value, name string) reflect.Value {\n t := rv.Type()\n for i := 0; i < t.NumField(); i++ {\n if strings.EqualFold(t.Field(i).Name, name) {\n return rv.Field(i)\n }\n }\n return reflect.Value{}\n}\n\n// Len returns the length of a slice, array, map, string, or channel.\n// Returns 0 for nil or unsupported types.\nfunc Len(v any) int {\n if v == nil {\n return 0\n }\n rv := reflect.ValueOf(v)\n switch rv.Kind() {\n case reflect.Slice, reflect.Array, reflect.Map, reflect.String, reflect.Chan:\n return rv.Len()\n default:\n return 0\n }\n}\n\n// At returns the element at index i from a slice.\n// Supports negative indices (e.g., -1 for last element).\n// Returns nil if index is out of bounds.\nfunc At(items any, index int) any {\n v := reflect.ValueOf(items)\n if v.Kind() != reflect.Slice && v.Kind() != reflect.Array {\n return nil\n }\n\n length := v.Len()\n if length == 0 {\n return nil\n }\n\n // Handle negative indices\n if index < 0 {\n index = length + index\n }\n\n if index < 0 || index >= length {\n return nil\n }\n\n return v.Index(index).Interface()\n}\n\n// Includes returns true if items contains elem. Lowers both\n// `Array.prototype.includes` and `String.prototype.includes` \u2014\n// the adapter can\'t disambiguate the receiver at compile time,\n// so this helper dispatches at runtime on `reflect.Kind()`:\n//\n// - slice/array receiver: SameValueZero element search (matches\n// the evaluator\'s `evalSameValueZero`/`evalIncludes` in eval.go,\n// which back the serialized-callback path) \u2014 numeric types compare\n// by value across int/float64 the way JS\'s single "number" type\n// does, and NaN matches NaN (unlike `===`). This used to be\n// `reflect.DeepEqual`, which is type-strict (`int(2)` != `float64(2)`)\n// and never matches NaN to NaN; that diverged from the evaluator\'s\n// `.includes` and from JS itself, so it was unified here.\n// - string receiver: strings.Contains substring search\n//\n// Anything else returns false (matches the JS semantic where\n// `.includes` is only defined on Array / TypedArray / String).\nfunc Includes(recv any, elem any) bool {\n v := reflect.ValueOf(recv)\n if v.Kind() == reflect.String {\n // JS `String.prototype.includes` accepts only string args;\n // non-string `elem` would TypeError in real JS but our\n // callers have lowered through `convertExpressionToGo`\n // where the arg type is whatever the template binds. Stringify\n // via fmt to keep the helper total.\n needle, ok := elem.(string)\n if !ok {\n needle = fmt.Sprintf("%v", elem)\n }\n return strings.Contains(v.String(), needle)\n }\n if v.Kind() != reflect.Slice && v.Kind() != reflect.Array {\n return false\n }\n for i := 0; i < v.Len(); i++ {\n if evalSameValueZero(v.Index(i).Interface(), elem) {\n return true\n }\n }\n return false\n}\n\n// IndexOf returns the 0-based position of the first item that\n// DeepEquals `elem`, or -1 if not found. Lowers\n// `Array.prototype.indexOf(x)` (#1448 Tier A). The existing\n// `FindIndex` helper does struct-field equality (used by the\n// higher-order `.find` lowering); this one does value equality\n// against scalar / struct items so callers don\'t have to compose\n// a synthetic predicate.\n//\n// Non-array / non-slice receivers return -1 (matches the JS\n// semantic that `.indexOf` is only defined on Array / TypedArray).\nfunc IndexOf(items any, elem any) int {\n v := reflect.ValueOf(items)\n if v.Kind() != reflect.Slice && v.Kind() != reflect.Array {\n return -1\n }\n for i := 0; i < v.Len(); i++ {\n if reflect.DeepEqual(v.Index(i).Interface(), elem) {\n return i\n }\n }\n return -1\n}\n\n// LastIndexOf returns the 0-based position of the last item that\n// DeepEquals `elem`, or -1 if not found. Mirrors\n// `Array.prototype.lastIndexOf(x)`. The reverse traversal is the\n// only behavioural difference vs `IndexOf` \u2014 disambiguating a\n// duplicated value\'s first vs last position is the canonical\n// reason a JS author reaches for `lastIndexOf`.\nfunc LastIndexOf(items any, elem any) int {\n v := reflect.ValueOf(items)\n if v.Kind() != reflect.Slice && v.Kind() != reflect.Array {\n return -1\n }\n for i := v.Len() - 1; i >= 0; i-- {\n if reflect.DeepEqual(v.Index(i).Interface(), elem) {\n return i\n }\n }\n return -1\n}\n\n// Concat merges two arrays (or slices) into a single `[]any`,\n// preserving order: receiver elements first, then `other`\'s.\n// Lowers `Array.prototype.concat(other)` (#1448 Tier A). Non-array\n// operands collapse to an empty source \u2014 matches the JS semantic\n// where `.concat` on a non-Array reads it as a single element only\n// if its `Symbol.isConcatSpreadable` is true; the template-language\n// path doesn\'t have user objects with that flag, so treating\n// non-arrays as empty is the conservative lowering. Variadic\n// `.concat(a, b, c)` is out of scope here (parser gates to a single\n// arg); the helper itself stays binary so a future variadic IR can\n// fold via repeated calls without changing this signature.\nfunc Concat(a, b any) []any {\n flatten := func(v reflect.Value) []any {\n if !v.IsValid() {\n return nil\n }\n if v.Kind() != reflect.Slice && v.Kind() != reflect.Array {\n return nil\n }\n out := make([]any, v.Len())\n for i := 0; i < v.Len(); i++ {\n out[i] = v.Index(i).Interface()\n }\n return out\n }\n left := flatten(reflect.ValueOf(a))\n right := flatten(reflect.ValueOf(b))\n return append(left, right...)\n}\n\n// clampSliceRange normalizes JS `.slice(start, end?)` bounds against a\n// receiver of `length` elements (runes, for the string branch of\n// `Slice` below; array elements, for the array branch) \u2014 shared so\n// both branches clamp identically.\n//\n// JS-compat clamping:\n// - start < 0 \u2192 length + start (e.g. -1 = last index)\n// - end < 0 \u2192 length + end\n// - start < 0 after clamp \u2192 0\n// - end > length \u2192 length\nfunc clampSliceRange(length, start int, end []int) (int, int) {\n if start < 0 {\n start = length + start\n }\n if start < 0 {\n start = 0\n }\n if start > length {\n start = length\n }\n\n stop := length\n if len(end) > 0 {\n stop = end[0]\n if stop < 0 {\n stop = length + stop\n }\n if stop < 0 {\n stop = 0\n }\n if stop > length {\n stop = length\n }\n }\n return start, stop\n}\n\n// Slice carves out a sub-range from `items`. Lowers\n// `Array.prototype.slice(start, end?)` (#1448 Tier A) AND\n// `String.prototype.slice(start, end?)` (the `string-slice`\n// divergence) \u2014 the adapter emits the same `bf_slice` call for both\n// receiver shapes (it can\'t disambiguate string vs. array at compile\n// time), so this helper dispatches at runtime on `reflect.Kind()`,\n// mirroring `Includes` above. The variadic `end` arg lets Go\n// template\'s call dispatcher pass either 2 or 3 arguments; an absent\n// end means "to length".\n//\n// String length/positions are measured in runes, not UTF-16 code\n// units \u2014 the same divergence boundary `padTo` already accepts\n// (differs from JS only for astral-plane input). `start >= end`\n// (after clamping) returns an empty result for either receiver.\n//\n// Any other receiver kind returns an empty `[]any`.\nfunc Slice(items any, start int, end ...int) any {\n v := reflect.ValueOf(items)\n\n if v.Kind() == reflect.String {\n runes := []rune(v.String())\n s, e := clampSliceRange(len(runes), start, end)\n if s >= e {\n return ""\n }\n return string(runes[s:e])\n }\n\n if v.Kind() != reflect.Slice && v.Kind() != reflect.Array {\n return []any{}\n }\n s, e := clampSliceRange(v.Len(), start, end)\n if s >= e {\n return []any{}\n }\n out := make([]any, 0, e-s)\n for i := s; i < e; i++ {\n out = append(out, v.Index(i).Interface())\n }\n return out\n}\n\n// Reverse returns a new slice with `items`\'s elements in reverse\n// order. Lowers both `Array.prototype.reverse()` and\n// `Array.prototype.toReversed()` (#1448 Tier A) \u2014 SSR templates\n// render a snapshot, so JS\'s mutate-receiver vs return-new-array\n// distinction has no template-level meaning, and the safer\n// non-mutating shape is used uniformly.\n//\n// Non-array receivers return an empty `[]any`.\nfunc Reverse(items any) []any {\n v := reflect.ValueOf(items)\n if v.Kind() != reflect.Slice && v.Kind() != reflect.Array {\n return []any{}\n }\n length := v.Len()\n out := make([]any, length)\n for i := 0; i < length; i++ {\n out[length-1-i] = v.Index(i).Interface()\n }\n return out\n}\n\n// Flat flattens nested slices/arrays `depth` levels deep. Lowers\n// `Array.prototype.flat(depth?)` (#1448 Tier C). A `depth` of `-1` is the\n// `Infinity` sentinel (flatten fully); `0` (or negative-from-JS, already\n// normalised to 0 at compile time) returns a shallow copy. Non-array\n// elements are kept as-is (JS only flattens nested arrays). A non-array\n// receiver returns an empty `[]any`.\nfunc Flat(items any, depth int) []any {\n v := reflect.ValueOf(items)\n if v.Kind() != reflect.Slice && v.Kind() != reflect.Array {\n return []any{}\n }\n out := make([]any, 0, v.Len())\n for i := 0; i < v.Len(); i++ {\n el := v.Index(i).Interface()\n ev := reflect.ValueOf(el)\n if depth != 0 && (ev.Kind() == reflect.Slice || ev.Kind() == reflect.Array) {\n // `-1` (Infinity) recurses unbounded; a finite depth spends one level.\n next := depth\n if depth > 0 {\n next = depth - 1\n }\n out = append(out, Flat(el, next)...)\n } else {\n out = append(out, el)\n }\n }\n return out\n}\n\n// FlatDynamicDepth coerces `depth` via JS\'s `ToIntegerOrInfinity` and\n// flattens `items` that many levels. Lowers a DYNAMIC `.flat(depth)`\n// (#2094) \u2014 one whose depth isn\'t a compile-time literal, so (unlike\n// `Flat` above) the coercion happens here at render time instead of in the\n// parser.\n//\n// This is a SEPARATE helper from `Flat`/`bf_flat` \u2014 NOT a drop-in\n// replacement \u2014 because `Flat`\'s `depth int` parameter treats `-1` as a\n// compile-time SENTINEL meaning "flatten fully" (the parser\'s own\n// normalisation of a literal `Infinity`). A genuinely dynamic depth value\n// of `-1` means the JS-correct OPPOSITE: `Array.prototype.flat(-1)` never\n// recurses (same as `.flat(0)`, a shallow copy), because\n// `FlattenIntoArray` only recurses when `depth > 0`. Reusing `Flat`\'s int\n// contract for a raw dynamic value would silently invert that case, so\n// this function coerces FIRST \u2014 mapping a real `+Infinity` / huge finite\n// value to `Flat`\'s own `-1` sentinel, and a real negative value to `0` \u2014\n// and only then delegates to `Flat`\'s recursion.\n//\n// Coercion rules (JS `ToIntegerOrInfinity`, mirrored exactly; pinned by the\n// `flat_dynamic` golden-vector cases in\n// packages/adapter-tests/vectors/cases.ts):\n// - the value converts via `ToNumber` first (numeric string / bool /\n// number all coerce; see `flatDepthToFloat`);\n// - a NaN result (including a non-numeric string) \u2192 `0`;\n// - truncates toward zero (`2.7` \u2192 `2`);\n// - negative \u2192 `0`;\n// - `+Infinity` / a huge finite value \u2192 flattens fully.\nfunc FlatDynamicDepth(items any, depth any) []any {\n return Flat(items, coerceFlatDepth(depth))\n}\n\n// coerceFlatDepth implements JS\'s `ToIntegerOrInfinity` for a dynamic\n// `.flat(depth)` argument, returning an int in `Flat`\'s own contract (`-1`\n// = unbounded, `>= 0` = that many levels).\nfunc coerceFlatDepth(depth any) int {\n f, ok := flatDepthToFloat(depth)\n if !ok || math.IsNaN(f) {\n return 0\n }\n if math.IsInf(f, 1) {\n return -1 // Flat\'s "flatten fully" sentinel\n }\n if math.IsInf(f, -1) {\n return 0\n }\n trunc := math.Trunc(f)\n if trunc < 0 {\n return 0\n }\n // A huge finite depth behaves identically to "flatten fully" in\n // practice \u2014 real data bottoms out at its actual nesting depth long\n // before a counter this large would ever reach zero. Capping it here\n // avoids an absurd countdown without needing a second sentinel.\n if trunc > 1_000_000 {\n return -1\n }\n return int(trunc)\n}\n\n// flatDepthToFloat converts a dynamic `.flat(depth)` argument to a float64,\n// mirroring JS\'s `ToNumber` across the value shapes a Go template data\n// model can carry (every numeric kind, bool, numeric string). `ok` is\n// false for a shape `ToNumber` can\'t coerce meaningfully (`nil`, or a\n// non-numeric string) \u2014 `coerceFlatDepth` treats that the same as NaN\n// (\u2192 depth `0`), matching JS.\nfunc flatDepthToFloat(v any) (float64, bool) {\n switch n := v.(type) {\n case nil:\n return 0, false\n case float64:\n return n, true\n case float32:\n return float64(n), true\n case int:\n return float64(n), true\n case int8:\n return float64(n), true\n case int16:\n return float64(n), true\n case int32:\n return float64(n), true\n case int64:\n return float64(n), true\n case uint:\n return float64(n), true\n case uint8:\n return float64(n), true\n case uint16:\n return float64(n), true\n case uint32:\n return float64(n), true\n case uint64:\n return float64(n), true\n case bool:\n if n {\n return 1, true\n }\n return 0, true\n case string:\n s := strings.TrimSpace(n)\n if s == "" {\n return 0, true // JS: Number("") is 0\n }\n f, err := strconv.ParseFloat(s, 64)\n if err != nil {\n return 0, false // not numeric \u2192 NaN path\n }\n return f, true\n default:\n return 0, false\n }\n}\n\n// FlatMap projects each element through a `self` / `field` projection and\n// flattens the result one level. Lowers value-returning\n// `Array.prototype.flatMap(fn)` for the field-projection catalogue\n// (#1448 Tier C): `items.flatMap(i => i)` (self) and\n// `items.flatMap(i => i.field)` (field). A projected non-array value is\n// kept as-is (flatMap = map + flat(1)). Non-array receiver \u2192 empty.\nfunc FlatMap(items any, keyKind, keyName string) []any {\n v := reflect.ValueOf(items)\n if v.Kind() != reflect.Slice && v.Kind() != reflect.Array {\n return []any{}\n }\n projected := make([]any, 0, v.Len())\n for i := 0; i < v.Len(); i++ {\n el := v.Index(i).Interface()\n if keyKind == "field" {\n projected = append(projected, getFieldValue(el, keyName))\n } else {\n projected = append(projected, el)\n }\n }\n return Flat(projected, 1)\n}\n\n// FlatMapTuple lowers an array-literal flatMap projection\n// `items.flatMap(i => [i.a, i.b])` (#1448 Tier C). `specs` is a flat list\n// of (kind, name) pairs, one per array-literal leaf: ("self", "") for the\n// item itself, ("field", "<Name>") for a struct field. For each item it\n// appends every leaf\'s value in order. Unlike the scalar `FlatMap`, the\n// per-item array is flattened only one level (flat(1) removes the literal\n// wrapper), so an array-valued leaf is appended verbatim rather than\n// spread \u2014 which is exactly "append each leaf". Non-array receiver \u2192 empty.\nfunc FlatMapTuple(items any, specs ...string) []any {\n v := reflect.ValueOf(items)\n if v.Kind() != reflect.Slice && v.Kind() != reflect.Array {\n return []any{}\n }\n out := make([]any, 0, v.Len())\n for i := 0; i < v.Len(); i++ {\n el := v.Index(i).Interface()\n for j := 0; j+1 < len(specs); j += 2 {\n if specs[j] == "field" {\n out = append(out, getFieldValue(el, specs[j+1]))\n } else {\n out = append(out, el)\n }\n }\n }\n return out\n}\n\n// First returns the first element of a slice, or nil if empty.\nfunc First(items any) any {\n return At(items, 0)\n}\n\n// Last returns the last element of a slice, or nil if empty.\nfunc Last(items any) any {\n return At(items, -1)\n}\n\n// Arr builds an []any from variadic args. Used to lower JS array\n// literals like `[a, b]` for the registry Slot\'s\n// `[className, childClass].filter(Boolean).join(\' \')` shape (#1443) \u2014\n// Go templates have no array-literal syntax, so the codegen routes\n// array-literal IR nodes through this helper.\nfunc Arr(items ...any) []any {\n return items\n}\n\n// FilterTruthy returns a new slice containing only truthy items.\n// Mirrors `arr.filter(Boolean)` semantics: drop nil, false, 0, "" \u2014 the\n// same falsy set JavaScript\'s `Boolean(x)` recognises. Used to lower\n// the registry Slot\'s class-merge pattern (#1443); generalising to\n// arbitrary callable predicates would need the callee-resolution path\n// blocked by #1389, so this stays Boolean-specific.\nfunc FilterTruthy(items any) []any {\n v := reflect.ValueOf(items)\n if !v.IsValid() || (v.Kind() != reflect.Slice && v.Kind() != reflect.Array) {\n return nil\n }\n result := make([]any, 0, v.Len())\n for i := 0; i < v.Len(); i++ {\n raw := v.Index(i).Interface()\n if isTruthy(raw) {\n result = append(result, raw)\n }\n }\n return result\n}\n\n// Truthy is the exported form of isTruthy \u2014 JavaScript\'s `Boolean(x)`\n// semantics. Two callers:\n// - generated `NewXxxProps` code lowering a conditional inline-object\n// spread condition on an `interface{}` prop (whose runtime value may be\n// a string, number, bool, \u2026), keeping the spread bag\'s inclusion test\n// faithful to JS rather than string-biased (#1752); and\n// - the `bf_truthy` template FuncMap entry (#2335), which coerces a\n// `bf_ternary` test to a real bool when it isn\'t already a comparison /\n// negation (`Ternary`\'s `cond` parameter is typed `bool`, unlike\n// `{{if}}`\'s built-in truthiness). Uniform across string/number/bool/nil,\n// so a `bf_ternary` test on any prop type can\'t hit a `bool`-vs-`string`\n// comparison error the way a string-only `ne <value> ""` would.\nfunc Truthy(v any) bool { return isTruthy(v) }\n\n// isTruthy mirrors JavaScript\'s `Boolean(x)` for the value shapes the\n// template path actually receives \u2014 nil / false / 0 / "" are falsy.\n// Other shapes (non-empty maps, slices, structs, true) are truthy, in\n// line with JS\'s "objects are truthy" rule.\nfunc isTruthy(v any) bool {\n if v == nil {\n return false\n }\n switch x := v.(type) {\n case bool:\n return x\n case string:\n return x != ""\n case int:\n return x != 0\n case int8, int16, int32, int64:\n return reflect.ValueOf(v).Int() != 0\n case uint, uint8, uint16, uint32, uint64:\n return reflect.ValueOf(v).Uint() != 0\n case float32:\n // JS `Boolean(NaN)` is false regardless of float width \u2014 the\n // float64 arm below was the only one checking IsNaN, which\n // diverged from JS for `float32` NaN inputs (Copilot review on\n // #1445). Widening to float64 for the IsNaN check keeps the\n // two branches in lock-step.\n return x != 0 && !math.IsNaN(float64(x))\n case float64:\n return x != 0 && !math.IsNaN(x)\n }\n return true\n}\n\n// =============================================================================\n// Higher-order Array Methods\n// =============================================================================\n\n// fieldValue projects item.field for the higher-order predicate\n// helpers. The field name arrives in JS casing; structs resolve via\n// the capitalized Go convention (FieldByName inside getFieldValue),\n// maps via getFieldValue\'s case-variant lookup \u2014 the same dual\n// support Sort/Reduce gained in #1487, extended here so JSON-decoded\n// data (map items) participates instead of being silently skipped.\n// nil-safe: missing fields and nil items project to nil.\nfunc fieldValue(item any, field string) any {\n return getFieldValue(item, capitalize(field))\n}\n\n// Every returns true if every item\'s field is truthy under JS\n// `Boolean(item.field)` semantics. Mirrors JavaScript\'s\n// Array.prototype.every(item => item.field) \u2014 including being\n// vacuously true for an empty receiver.\nfunc Every(items any, field string) bool {\n v := reflect.ValueOf(items)\n if v.Kind() != reflect.Slice && v.Kind() != reflect.Array {\n return false\n }\n for i := 0; i < v.Len(); i++ {\n if !isTruthy(fieldValue(v.Index(i).Interface(), field)) {\n return false\n }\n }\n return true\n}\n\n// Some returns true if at least one item\'s field is truthy. Mirrors\n// JavaScript\'s Array.prototype.some(item => item.field).\nfunc Some(items any, field string) bool {\n v := reflect.ValueOf(items)\n if v.Kind() != reflect.Slice && v.Kind() != reflect.Array {\n return false\n }\n for i := 0; i < v.Len(); i++ {\n if isTruthy(fieldValue(v.Index(i).Interface(), field)) {\n return true\n }\n }\n return false\n}\n\n// Filter returns items where item.field == value.\n// Mirrors JavaScript\'s Array.prototype.filter(item => item.field === value).\n// Returns []any to allow chaining with other bf_* functions.\nfunc Filter(items any, field string, value any) []any {\n v := reflect.ValueOf(items)\n if v.Kind() != reflect.Slice && v.Kind() != reflect.Array {\n return nil\n }\n var result []any\n for i := 0; i < v.Len(); i++ {\n item := v.Index(i).Interface()\n if reflect.DeepEqual(fieldValue(item, field), value) {\n result = append(result, item)\n }\n }\n return result\n}\n\n// Find returns the first item where item.field == value, or nil if not found.\n// Mirrors JavaScript\'s Array.prototype.find(item => item.field === value).\nfunc Find(items any, field string, value any) any {\n v := reflect.ValueOf(items)\n if v.Kind() != reflect.Slice && v.Kind() != reflect.Array {\n return nil\n }\n for i := 0; i < v.Len(); i++ {\n item := v.Index(i).Interface()\n if reflect.DeepEqual(fieldValue(item, field), value) {\n return item\n }\n }\n return nil\n}\n\n// FindIndex returns the index of the first item where item.field == value, or -1.\n// Mirrors JavaScript\'s Array.prototype.findIndex(item => item.field === value).\nfunc FindIndex(items any, field string, value any) int {\n v := reflect.ValueOf(items)\n if v.Kind() != reflect.Slice && v.Kind() != reflect.Array {\n return -1\n }\n for i := 0; i < v.Len(); i++ {\n if reflect.DeepEqual(fieldValue(v.Index(i).Interface(), field), value) {\n return i\n }\n }\n return -1\n}\n\n// FindLast returns the last item where item.field == value, or nil if not found.\n// Mirrors JavaScript\'s Array.prototype.findLast(item => item.field === value).\nfunc FindLast(items any, field string, value any) any {\n v := reflect.ValueOf(items)\n if v.Kind() != reflect.Slice && v.Kind() != reflect.Array {\n return nil\n }\n for i := v.Len() - 1; i >= 0; i-- {\n item := v.Index(i).Interface()\n if reflect.DeepEqual(fieldValue(item, field), value) {\n return item\n }\n }\n return nil\n}\n\n// FindLastIndex returns the index of the last item where item.field == value, or -1.\n// Mirrors JavaScript\'s Array.prototype.findLastIndex(item => item.field === value).\nfunc FindLastIndex(items any, field string, value any) int {\n v := reflect.ValueOf(items)\n if v.Kind() != reflect.Slice && v.Kind() != reflect.Array {\n return -1\n }\n for i := v.Len() - 1; i >= 0; i-- {\n if reflect.DeepEqual(fieldValue(v.Index(i).Interface(), field), value) {\n return i\n }\n }\n return -1\n}\n\n// sortKeySpec is one parsed comparison key. A simple comparator has\n// one; a `||`-chained multi-key comparator has several, applied in\n// order as tie-breakers.\ntype sortKeySpec struct {\n kind string // "self" | "field"\n name string // capitalised field name, or "" for "self"\n compareType string // "numeric" | "string" | "auto"\n direction string // "asc" | "desc"\n}\n\n// Sort returns a new stable-sorted slice. Lowers\n// `Array.prototype.sort` / `Array.prototype.toSorted` (#1448 Tier B).\n// Non-mutating \u2014 JS\'s mutate-vs-new distinction is moot in SSR\n// template context (templates render a snapshot).\n//\n// Call shape (the compiler emits one 4-string group per key):\n//\n// bf_sort <items> (<keyKind> <keyName> <compareType> <direction>)+\n//\n// keyKind: "self" | "field"\n// keyName: "" when keyKind == "self"; capitalised struct field\n// name (e.g. "Price") otherwise\n// compareType: "numeric" | "string" | "auto"\n// direction: "asc" | "desc"\n//\n// The groups cover the accepted comparator catalogue: `a.f - b.f`,\n// `a - b`, `a[.f].localeCompare(b[.f])`, and relational-ternary keys\n// (`a.f > b.f ? 1 : -1` \u2192 "auto"), each `||`-chainable for multi-key\n// tie-breaks. Anything outside refuses at compile time (BF101 from the\n// JSX compiler) and never reaches this helper.\n//\n// "auto" compares numerically when both projected keys parse as\n// numbers, else lexically \u2014 mirroring the Perl `bf->sort` helper\'s\n// `looks_like_number` rule so the two template adapters stay\n// byte-equal. This diverges from JS `<`/`>` only for numeric strings.\n//\n// A future `nulls` knob can extend the per-key group without rewriting\n// existing call sites \u2014 each key already projects before comparing.\nfunc Sort(items any, spec ...string) []any {\n v := reflect.ValueOf(items)\n if v.Kind() != reflect.Slice && v.Kind() != reflect.Array {\n return nil\n }\n\n length := v.Len()\n if length == 0 {\n return []any{}\n }\n\n // Copy into a fresh []any so the sort is non-mutating regardless\n // of whether the receiver is `[]T` or `[]any`.\n result := make([]any, length)\n for i := 0; i < length; i++ {\n result[i] = v.Index(i).Interface()\n }\n\n keys := parseSortSpec(spec)\n sort.SliceStable(result, func(i, j int) bool {\n for _, k := range keys {\n ki := projectSortKey(result[i], k.kind, k.name)\n kj := projectSortKey(result[j], k.kind, k.name)\n c := compareSortKey(ki, kj, k.compareType)\n if c == 0 {\n continue // tie on this key \u2014 fall through to the next\n }\n if k.direction == "desc" {\n return c > 0\n }\n return c < 0\n }\n return false\n })\n\n return result\n}\n\n// parseSortSpec chunks the variadic operand list into 4-string key\n// groups. A trailing partial group (malformed emit) is ignored rather\n// than panicking \u2014 defensive, mirroring the helper\'s nil-safe stance.\nfunc parseSortSpec(spec []string) []sortKeySpec {\n var keys []sortKeySpec\n for i := 0; i+3 < len(spec); i += 4 {\n keys = append(keys, sortKeySpec{\n kind: spec[i],\n name: spec[i+1],\n compareType: spec[i+2],\n direction: spec[i+3],\n })\n }\n return keys\n}\n\n// compareSortKey returns -1 / 0 / 1 for two projected keys under the\n// given compare type (ascending orientation; the caller flips for\n// "desc"). "string" stringifies both (nil \u2192 "", matching the\n// documented `bf->string(undef) === ""` divergence). "auto" compares\n// numerically when both parse as numbers, else lexically.\nfunc compareSortKey(ki, kj any, compareType string) int {\n switch compareType {\n case "string":\n return strings.Compare(toString(ki), toString(kj))\n case "auto":\n ni, okI := toFloat64WithOK(ki)\n nj, okJ := toFloat64WithOK(kj)\n if okI && okJ {\n return cmpFloat(ni, nj)\n }\n return strings.Compare(toString(ki), toString(kj))\n default: // numeric\n return cmpFloat(toFloat64(ki), toFloat64(kj))\n }\n}\n\nfunc cmpFloat(a, b float64) int {\n if a < b {\n return -1\n }\n if a > b {\n return 1\n }\n return 0\n}\n\n// toFloat64WithOK reports a value\'s numeric float and whether it is\n// number-like. Genuine numeric kinds always qualify; strings qualify\n// when they parse as a float (so the "auto" compare path matches the\n// Perl `looks_like_number` rule). Everything else is non-numeric.\nfunc toFloat64WithOK(v any) (float64, bool) {\n switch n := v.(type) {\n case int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64, float32, float64:\n return toFloat64(v), true\n case string:\n f, err := strconv.ParseFloat(strings.TrimSpace(n), 64)\n if err != nil {\n return 0, false\n }\n return f, true\n default:\n return 0, false\n }\n}\n\n// projectSortKey reduces an item to the value the comparator\n// actually compares. For `keyKind == "field"` it reads the named\n// struct field; for `keyKind == "self"` (primitive arrays) it\n// returns the item unchanged.\nfunc projectSortKey(item any, keyKind, keyName string) any {\n if keyKind == "field" {\n return getFieldValue(item, keyName)\n }\n return item\n}\n\n// getFieldValue extracts a struct field value using reflection. For\n// map receivers it falls back to case-variant lookup so JSON-decoded\n// user data (`map[string]any{"price": 30}`) and PascalCase-emitted\n// test data both resolve under a single key name. (#1487)\nfunc getFieldValue(item any, field string) any {\n v := reflect.ValueOf(item)\n // Defensive IsNil guards mirror `SpreadAttrs` \u2014 keeps the helper\n // safe against typed-nil pointer / nil-interface items inside a\n // `[]any` so a single bad row doesn\'t crash the whole sort.\n if v.Kind() == reflect.Interface {\n if v.IsNil() {\n return nil\n }\n v = v.Elem()\n }\n if v.Kind() == reflect.Ptr {\n if v.IsNil() {\n return nil\n }\n v = v.Elem()\n }\n\n if v.Kind() == reflect.Map {\n keyType := v.Type().Key()\n if keyType.Kind() != reflect.String {\n return nil\n }\n // Convert the lookup string to the map\'s actual key type so\n // maps keyed by a named string type (`type Key string`) don\'t\n // panic with `value of type string is not assignable to type X`.\n lookup := func(s string) (any, bool) {\n k := reflect.ValueOf(s).Convert(keyType)\n if mv := v.MapIndex(k); mv.IsValid() {\n return mv.Interface(), true\n }\n return nil, false\n }\n if r, ok := lookup(field); ok {\n return r\n }\n if cap := capitalize(field); cap != field {\n if r, ok := lookup(cap); ok {\n return r\n }\n }\n if low := decapitalize(field); low != field {\n if r, ok := lookup(low); ok {\n return r\n }\n }\n // All-lowercase fallback: a Go-initialism field projects as an\n // all-caps key (`id` \u2192 `ID`), and `decapitalize("ID")` only\n // lowers the first char (`iD`), so the JS-keyed map ("id") still\n // misses. Try the fully-lowered key last to resolve it.\n if lower := strings.ToLower(field); lower != field && lower != decapitalize(field) {\n if r, ok := lookup(lower); ok {\n return r\n }\n }\n return nil\n }\n\n if v.Kind() != reflect.Struct {\n return nil\n }\n\n fieldVal := v.FieldByName(field)\n if !fieldVal.IsValid() {\n // Case-variant fallback: the evaluator carries the JS field name\n // (`id` / `url`) against a Go-capitalised struct field (`ID` / `URL`),\n // which exact `FieldByName` misses and the initialism rules can\'t be\n // reproduced char-for-char here. Match case-insensitively instead \u2014\n // `FieldByNameFunc` returns the zero Value (\u2192 nil) for an ambiguous\n // match, so it stays safe. The legacy bf_sort/bf_reduce pass an\n // already-capitalised name, so they hit the exact match above and never\n // reach this fallback.\n fieldVal = v.FieldByNameFunc(func(n string) bool { return strings.EqualFold(n, field) })\n if !fieldVal.IsValid() {\n return nil\n }\n }\n return fieldVal.Interface()\n}\n\n// AsMap normalizes a dynamically-typed prop value into a\n// map[string]interface{} for object-valued context bindings\n// (`lowerProviderMapMemberValue`, go-template-adapter.ts). A caller-side\n// `interface{}` field can legally hold ANY string-keyed map kind \u2014 a Go\n// handler modelling `Record<string, string>` naturally passes\n// map[string]string \u2014 so a bare `.(map[string]interface{})` type assertion\n// would silently drop provided values (#2111 review). Returns nil (never an\n// empty map) when the value is absent \u2014 nil interface, typed-nil map or\n// pointer, or any non-map / non-string-keyed value \u2014 so the generated\n// `?? {}` fallback can distinguish "missing" (fall back) from "present but\n// empty" (use as-is). map[string]interface{} passes through without copying.\nfunc AsMap(v any) map[string]interface{} {\n if v == nil {\n return nil\n }\n if m, ok := v.(map[string]interface{}); ok {\n if m == nil {\n return nil\n }\n return m\n }\n rv := reflect.ValueOf(v)\n if rv.Kind() == reflect.Ptr {\n if rv.IsNil() {\n return nil\n }\n rv = rv.Elem()\n }\n if rv.Kind() != reflect.Map || rv.Type().Key().Kind() != reflect.String || rv.IsNil() {\n return nil\n }\n out := make(map[string]interface{}, rv.Len())\n iter := rv.MapRange()\n for iter.Next() {\n out[iter.Key().String()] = iter.Value().Interface()\n }\n return out\n}\n\n// capitalize uppercases the first character of a string.\nfunc capitalize(s string) string {\n if s == "" {\n return s\n }\n return strings.ToUpper(s[:1]) + s[1:]\n}\n\n// decapitalize lowercases the first character of a string. Used by\n// `getFieldValue`\'s map-receiver fallback when the projected key\n// name is PascalCase but the receiver carries lowercase JS-style\n// keys (the inverse of the `capitalize` lookup).\nfunc decapitalize(s string) string {\n if s == "" {\n return s\n }\n return strings.ToLower(s[:1]) + s[1:]\n}\n\n// Reduce folds an array into a scalar via the arithmetic-fold\n// catalogue (#1448 Tier C). It lowers `Array.prototype.reduce(fn, init)`\n// and `Array.prototype.reduceRight(fn, init)` for the shapes\n// `(acc, x) => acc <op> x` and `(acc, x) => acc <op> x.field`:\n//\n// bf_reduce <items> "<op>" "<keyKind>" "<keyName>" "<type>" "<init>" "<direction>"\n//\n// direction: "left" (reduce) | "right" (reduceRight). Only changes the\n// result for string concatenation; numeric folds commute.\n//\n// op: "+" | "*"\n// keyKind: "self" | "field"\n// keyName: "" when keyKind == "self"; capitalised struct field name\n// (e.g. "Duration") otherwise\n// type: "numeric" | "string"\n// init: the fold\'s start value \u2014 the compiler emits the *decoded*\n// seed, so numeric inits arrive as canonical decimal\n// (`1_000`/`0x10` already normalised to `1000`/`16`) that\n// ParseFloat accepts, and string inits arrive as escape-free\n// contents\n//\n// Numeric folds accumulate as float64; each projected key is read via\n// `toFloat64WithOK`, so numeric *strings* ("5" \u2192 5) parse and\n// non-numeric values fold as 0 \u2014 matching Perl\'s\n// `looks_like_number ? $n : 0` so the two template adapters stay\n// byte-equal. String folds concatenate (toString per projected key,\n// matching the documented `bf->string(undef) === ""` convention). The\n// init seeds the accumulator, so an empty receiver returns the init\n// unchanged \u2014 exactly like JS `reduce(fn, init)`. Anything outside the\n// catalogue refuses at compile time (BF101 from the JSX compiler) and\n// never reaches here.\n//\n// Two documented divergences from the JS / Hono path, both rare and\n// mirroring the `bf_sort` "auto" caveat:\n// - float64 stringification differs for sums whose binary expansion\n// isn\'t exact (e.g. 0.1 + 0.2);\n// - numeric-*string* keys fold numerically here, but JS `+`\n// string-concatenates once an operand is a string, so\n// numeric-string data can render differently under CSR.\n//\n// Genuine numbers \u2014 the common SSR case \u2014 agree across all three.\nfunc Reduce(items any, op, keyKind, keyName, typ, init, direction string) any {\n v := reflect.ValueOf(items)\n isSlice := v.Kind() == reflect.Slice || v.Kind() == reflect.Array\n\n // `direction == "right"` (reduceRight) folds right-to-left. Only\n // observable for string concatenation \u2014 numeric sum / product are\n // commutative, so the order doesn\'t change the result there. Build a\n // start/stop/step triple so both folds share one loop shape.\n start, stop, step := 0, 0, 1\n if isSlice {\n stop = v.Len()\n if direction == "right" {\n start, stop, step = v.Len()-1, -1, -1\n }\n }\n\n if typ == "string" {\n acc := init\n if isSlice {\n for i := start; i != stop; i += step {\n key := projectSortKey(v.Index(i).Interface(), keyKind, keyName)\n acc += toString(key)\n }\n }\n return acc\n }\n\n // numeric fold\n acc, _ := strconv.ParseFloat(strings.TrimSpace(init), 64)\n if isSlice {\n for i := start; i != stop; i += step {\n key := projectSortKey(v.Index(i).Interface(), keyKind, keyName)\n // `toFloat64WithOK` parses numeric *strings* ("5" \u2192 5) and\n // returns 0 for non-numeric values \u2014 mirroring Perl\'s\n // `looks_like_number ? $n : 0` so numeric-string data folds\n // byte-equal across adapters (the same rule `bf_sort`\'s\n // "auto" compare uses). Plain `toFloat64` would zero "5".\n n, _ := toFloat64WithOK(key)\n if op == "*" {\n acc *= n\n } else {\n acc += n\n }\n }\n }\n return acc\n}\n\n// =============================================================================\n// HTML/Template Helpers\n// =============================================================================\n\n// Comment returns an HTML comment string for hydration markers.\n// The "bf-" prefix is automatically added.\nfunc Comment(content string) template.HTML {\n return template.HTML("<!--bf-" + content + "-->")\n}\n\n// TextStart returns an HTML comment start marker for reactive text expressions.\n// Format: <!--bf:slotId-->\nfunc TextStart(slotId string) template.HTML {\n return template.HTML("<!--bf:" + slotId + "-->")\n}\n\n// TextEnd returns an HTML comment end marker for reactive text expressions.\n// Format: <!--/-->\nfunc TextEnd() template.HTML {\n return "<!--/-->"\n}\n\n// ScopeComment emits a fragment-rooted scope marker. See spec/compiler.md\n// "Slot identity" for the wire format. Loud-fails on marshal errors\n// (same policy as JSON / BfPropsAttr).\nfunc ScopeComment(props interface{}) (template.HTML, error) {\n scopeID := getStringField(props, "ScopeID")\n hostSegment := ""\n if host := getStringField(props, "BfParent"); host != "" {\n mount := getStringField(props, "BfMount")\n hostSegment = "|h=" + host + "|m=" + mount\n }\n propsJSON := ""\n if getBoolField(props, "BfIsRoot") {\n pJSON, err := json.Marshal(props)\n if err != nil {\n return "", err\n }\n propsJSON = "|" + string(pJSON)\n }\n return template.HTML("<!--bf-scope:" + scopeID + hostSegment + propsJSON + "-->"), nil\n}\n\n// ScopeCommentEnd emits the paired end marker for a fragment-rooted scope\n// (#2289): a fragment root has no single wrapping element to bound the\n// client\'s scope query, so the range leaks onto later siblings without an\n// explicit terminator. Carries only the scope id \u2014 no `|h=`/`|m=`/props\n// segment, unlike ScopeComment \u2014 since the client only needs it to confirm\n// the range closes on the matching scope (getCommentScopeBoundary in\n// packages/client/src/runtime/scope.ts).\nfunc ScopeCommentEnd(props interface{}) template.HTML {\n scopeID := getStringField(props, "ScopeID")\n return template.HTML("<!--bf-/scope:" + scopeID + "-->")\n}\n\n// TemplateFuncMap returns the helpers that need access to the executing\n// template set itself, closed over the *template.Template the component\n// defines are parsed into. Register it alongside FuncMap BEFORE parsing:\n//\n// t := template.New("")\n// t.Funcs(bf.FuncMap()).Funcs(bf.TemplateFuncMap(t))\n// template.Must(t.Parse(src))\n//\n// bf_tmpl executes a named define from the same set and returns its\n// output \u2014 used for the per-call-site children defines the Go adapter\n// emits when JSX children passed to an imported component contain\n// template actions (nested components, dynamic text) and therefore\n// cannot be baked to a static HTML string (#1896). Reentrant execution\n// of an html/template set from inside a FuncMap function is safe: the\n// escape analysis over every define completes before the outer\n// Execute begins evaluating.\nfunc TemplateFuncMap(t *template.Template) template.FuncMap {\n return template.FuncMap{\n "bf_tmpl": func(name string, data interface{}) (template.HTML, error) {\n var buf bytes.Buffer\n if err := t.ExecuteTemplate(&buf, name, data); err != nil {\n return "", err\n }\n return template.HTML(buf.String()), nil\n },\n }\n}\n\n// WithChildren returns a shallow copy of a component Props struct with its\n// Children field replaced by the given pre-rendered fragment (#1896). The\n// props value stays by-value semantics: callers\' originals are untouched.\n// A props type without a Children field passes through unchanged \u2014 the\n// child template then simply has no children to render, matching the\n// pre-#1896 behaviour.\nfunc WithChildren(props interface{}, children template.HTML) (interface{}, error) {\n v := reflect.ValueOf(props)\n if v.Kind() == reflect.Ptr {\n v = v.Elem()\n }\n if v.Kind() != reflect.Struct {\n return props, nil\n }\n field := v.FieldByName("Children")\n if !field.IsValid() {\n return props, nil\n }\n copyPtr := reflect.New(v.Type())\n copyPtr.Elem().Set(v)\n target := copyPtr.Elem().FieldByName("Children")\n switch {\n case target.Kind() == reflect.Interface:\n target.Set(reflect.ValueOf(children))\n case target.Kind() == reflect.String:\n // Covers both `string` and `template.HTML`-typed fields.\n target.SetString(string(children))\n default:\n return props, fmt.Errorf("bf_with_children: unsupported Children field type %s", target.Type())\n }\n return copyPtr.Elem().Interface(), nil\n}\n\n// WithProps returns a shallow copy of a component Props struct with the\n// given fields overridden (#2445): a child component nested inside a\n// COMPOSITE loop row (row root is a plain element, not the child itself) is\n// constructed ONCE outside `{{range}}` \u2014 every row shares that instance for\n// scope/parent/mount identity \u2014 so a prop that depends on the row\n// (`text={row.label}`) has to be applied per row, at template-execution\n// time, the same way WithChildren applies per-row JSX children to that\n// shared instance. `kv` is a flat name/value list ("Text", .Label, "Count",\n// .N, ...). The props value stays by-value semantics: the caller\'s original\n// is untouched. A name with no matching settable field is left alone for\n// that pair \u2014 the prop routes elsewhere (e.g. a rest bag) and the base\n// instance\'s constructor-built value stands, mirroring WithChildren\'s\n// "props type without a Children field" passthrough.\n//\n// This overrides fields on the ALREADY-CONSTRUCTED instance \u2014 it does not\n// re-run New<Child>Props. A field the child derives FROM the overridden prop\n// at construction time (a memo body, or a signal\'s initial value \u2014 the\n// constructor bakes both) would keep whatever the one-shot constructor\n// computed and never update per row; only the directly-overridden field is\n// correct per row. The compiler therefore does not route that case here: a\n// child with any constructor-derived field gets a generated props rebuilder\n// and the call site emits bf_reprops instead, which re-runs the real\n// constructor per row (#2448, see reprops.go). What reaches this helper is\n// the plain-passthrough case, where patching the field IS the whole update.\n//\n// Still exported and still registered: templates generated before #2448 call\n// it, and it remains the cheaper path when nothing is derived.\nfunc WithProps(props interface{}, kv ...interface{}) (interface{}, error) {\n if len(kv)%2 != 0 {\n return nil, fmt.Errorf("bf_with_props: odd number of key/value arguments (%d)", len(kv))\n }\n v := reflect.ValueOf(props)\n if v.Kind() == reflect.Ptr {\n v = v.Elem()\n }\n if v.Kind() != reflect.Struct {\n return props, nil\n }\n copyPtr := reflect.New(v.Type())\n copyPtr.Elem().Set(v)\n for i := 0; i < len(kv); i += 2 {\n name, ok := kv[i].(string)\n if !ok {\n return nil, fmt.Errorf("bf_with_props: field name at position %d must be a string, got %T", i, kv[i])\n }\n target := copyPtr.Elem().FieldByName(name)\n if !target.IsValid() || !target.CanSet() {\n continue\n }\n if err := setStructFieldValue(target, kv[i+1]); err != nil {\n return nil, fmt.Errorf("bf_with_props: field %s: %w", name, err)\n }\n }\n return copyPtr.Elem().Interface(), nil\n}\n\n// setStructFieldValue assigns val into target, a settable struct field\n// obtained via reflect.Value.FieldByName. Mirrors WithChildren\'s\n// Interface/String branches (a String-kind target covers both `string` and\n// `template.HTML` fields, via String() rather than reflect.Convert \u2014 Go\'s\n// int-to-string conversion produces a rune, not a decimal string) and adds\n// the general assignable/convertible fallback for other field kinds\n// (numeric widening, etc.).\nfunc setStructFieldValue(target reflect.Value, val interface{}) error {\n if val == nil {\n target.Set(reflect.Zero(target.Type()))\n return nil\n }\n switch {\n case target.Kind() == reflect.Interface:\n target.Set(reflect.ValueOf(val))\n return nil\n case target.Kind() == reflect.String:\n target.SetString(String(val))\n return nil\n }\n rv := reflect.ValueOf(val)\n switch {\n case rv.Type().AssignableTo(target.Type()):\n target.Set(rv)\n case rv.Type().ConvertibleTo(target.Type()):\n target.Set(rv.Convert(target.Type()))\n default:\n return fmt.Errorf("cannot assign %T to %s", val, target.Type())\n }\n return nil\n}\n\n// PortalHTML parses and executes a template string with the provided data.\n// Used for rendering dynamic portal content where the template string\n// contains Go template expressions (e.g., {{if .Open}}open{{end}}).\n//\n// The template string is parsed fresh each time to support dynamic content.\n// Standard Go template functions (if, range, eq, etc.) are available.\nfunc PortalHTML(data interface{}, tmplStr string) template.HTML {\n // Create a new template with the FuncMap for custom functions\n t, err := template.New("portal").Funcs(FuncMap()).Parse(tmplStr)\n if err != nil {\n // Return error message as HTML comment for debugging\n return template.HTML("<!-- bfPortalHTML error: " + err.Error() + " -->")\n }\n\n var buf bytes.Buffer\n if err := t.Execute(&buf, data); err != nil {\n return template.HTML("<!-- bfPortalHTML exec error: " + err.Error() + " -->")\n }\n\n return template.HTML(buf.String())\n}\n\n// =============================================================================\n// Portal Collection\n// =============================================================================\n\n// PortalContent represents a single portal\'s content to be rendered at body end.\ntype PortalContent struct {\n ID string // Unique portal ID for hydration matching\n OwnerID string // Owner scope ID for find() support\n Content template.HTML // Portal HTML content\n}\n\n// PortalCollector collects portal content during template rendering.\n// Portal content is rendered at </body> to avoid z-index issues.\ntype PortalCollector struct {\n portals []PortalContent\n counter int\n}\n\n// NewPortalCollector creates a new PortalCollector.\nfunc NewPortalCollector() *PortalCollector {\n return &PortalCollector{\n portals: []PortalContent{},\n counter: 0,\n }\n}\n\n// Add registers portal content to be rendered at body end.\nfunc (pc *PortalCollector) Add(ownerID string, content template.HTML) string {\n pc.counter++\n id := "bf-portal-" + strconv.Itoa(pc.counter)\n pc.portals = append(pc.portals, PortalContent{\n ID: id,\n OwnerID: ownerID,\n Content: content,\n })\n return "" // Return empty string for template use\n}\n\n// Render outputs all collected portals as HTML.\n// Each portal is wrapped in a div with bf-pi (portal ID) and bf-po (portal owner).\nfunc (pc *PortalCollector) Render() template.HTML {\n if pc == nil || len(pc.portals) == 0 {\n return ""\n }\n var buf strings.Builder\n for _, p := range pc.portals {\n buf.WriteString(`<div bf-pi="`)\n buf.WriteString(p.ID)\n buf.WriteString(`" bf-po="`)\n buf.WriteString(p.OwnerID)\n buf.WriteString(`">`)\n buf.WriteString(string(p.Content))\n buf.WriteString("</div>\\n")\n }\n return template.HTML(buf.String())\n}\n\n// =============================================================================\n// Script Collection\n// =============================================================================\n\n// ScriptCollector collects client scripts with deduplication.\n// It preserves insertion order for deterministic output.\ntype ScriptCollector struct {\n scripts map[string]bool\n order []string\n}\n\n// NewScriptCollector creates a new ScriptCollector.\nfunc NewScriptCollector() *ScriptCollector {\n return &ScriptCollector{\n scripts: make(map[string]bool),\n order: []string{},\n }\n}\n\n// Register adds a script source to the collection.\n// Duplicate scripts are ignored (only first registration counts).\nfunc (sc *ScriptCollector) Register(src string) string {\n if sc.scripts[src] {\n return "" // Already registered\n }\n sc.scripts[src] = true\n sc.order = append(sc.order, src)\n return "" // Return empty string for template use\n}\n\n// Scripts returns all registered scripts in insertion order.\nfunc (sc *ScriptCollector) Scripts() []string {\n return sc.order\n}\n\n// BfScripts generates script tags for all registered scripts.\n// Returns HTML safe for embedding in templates.\nfunc BfScripts(collector *ScriptCollector) template.HTML {\n if collector == nil {\n return ""\n }\n var result strings.Builder\n for _, src := range collector.Scripts() {\n result.WriteString(`<script type="module" src="`)\n result.WriteString(src)\n result.WriteString(`"></script>`)\n result.WriteString("\\n")\n }\n return template.HTML(result.String())\n}\n\n// =============================================================================\n// Component Renderer\n// =============================================================================\n\n// RenderContext contains all data needed to render a component page.\n// The layout function receives this context to build the final HTML.\ntype RenderContext struct {\n // ComponentName is the template name being rendered\n ComponentName string\n\n // Props is the component props (for layout to access if needed)\n Props interface{}\n\n // ComponentHTML is the rendered component template output\n ComponentHTML template.HTML\n\n // Portals contains collected portal content to render at body end\n Portals template.HTML\n\n // Scripts contains the collected JS script tags\n Scripts template.HTML\n\n // Title is the page title (defaults to "{ComponentName} - BarefootJS")\n Title string\n\n // Heading is the page heading. Empty string means no heading.\n Heading string\n\n // Extra holds additional user-defined data for the layout\n Extra map[string]interface{}\n}\n\n// LayoutFunc renders the final HTML page given the render context.\ntype LayoutFunc func(ctx *RenderContext) string\n\n// Renderer renders BarefootJS components with a customizable layout.\ntype Renderer struct {\n templates *template.Template\n layout LayoutFunc\n}\n\n// NewRenderer creates a Renderer with the given templates and layout function.\n//\n// Example usage:\n//\n// renderer := bf.NewRenderer(templates, func(ctx *bf.RenderContext) string {\n// return fmt.Sprintf(`<!DOCTYPE html>\n// <html>\n// <head><title>%s</title></head>\n// <body>%s%s</body>\n// </html>`, ctx.Title, ctx.ComponentHTML, ctx.Scripts)\n// })\nfunc NewRenderer(tmpl *template.Template, layout LayoutFunc) *Renderer {\n return &Renderer{\n templates: tmpl,\n layout: layout,\n }\n}\n\n// RenderOptions configures a single render call.\ntype RenderOptions struct {\n // ComponentName is the template name to render (required)\n ComponentName string\n\n // Props is the component props (must be a pointer to struct with Scripts field)\n Props interface{}\n\n // Title is the page title. If empty, defaults to "{ComponentName} - BarefootJS"\n Title string\n\n // Heading is the page heading. If empty, no heading is shown.\n Heading string\n\n // Extra holds additional data to pass to the layout\n Extra map[string]interface{}\n}\n\n// Render renders a component to a full HTML page using the configured layout.\n// Child component props are automatically detected (any slice field with ScopeID/Scripts).\n// renderTemplateErrorPanel formats a Go template execution error into a\n// fragment of HTML that\'s visible in the browser. The panel is\n// HTML-escaped so a faulty template name (anything from `template:\n// "..."`) can\'t smuggle markup back into the page. Keep the styling\n// inline so the panel surfaces even when the project\'s CSS hasn\'t\n// loaded yet (e.g. the failure aborted before the stylesheet links\n// emitted).\n//\n// Surfaced for the #1442 echo repro: a template referencing\n// `.Todo.Done` (instead of the range dot\'s `.Done`) used to fail\n// silently \u2014 Go\'s html/template aborted mid-stream, the partial body\n// flushed as a 200, and the user saw a truncated list with no console\n// signal. With this panel they get the template name, the error\n// message, and a "what to look at" hint inline.\nfunc renderTemplateErrorPanel(componentName string, err error) string {\n return `<div style="margin:1em 0;padding:1em;border:2px solid #d33;background:#fff5f5;color:#900;font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;font-size:13px;line-height:1.5"><strong style="display:block;margin-bottom:.5em">Template error in <code>` +\n template.HTMLEscapeString(componentName) +\n `</code></strong><pre style="margin:0;white-space:pre-wrap;word-break:break-word">` +\n template.HTMLEscapeString(err.Error()) +\n `</pre><div style="margin-top:.75em;font-size:12px;opacity:.7">Common cause: a JSX expression referenced a name the adapter could not resolve to a struct field. Open the matching <code>dist/templates/*.tmpl</code> for the unresolved reference, then fix the source component.</div></div>`\n}\n\n// renderComponentInto wires a component\'s props (script/portal collectors,\n// child-slot scope ids + hydration, root marking) against the PROVIDED\n// collectors and returns just the component\'s HTML \u2014 no layout. Both Render\n// (one fresh collector pair per page) and RenderFragment (a shared pair across\n// several islands) funnel through here so their wiring stays identical.\nfunc (r *Renderer) renderComponentInto(opts RenderOptions, scriptCollector *ScriptCollector, portalCollector *PortalCollector) template.HTML {\n // Inject the shared collectors into the props.\n setScriptsField(opts.Props, scriptCollector)\n setPortalsField(opts.Props, portalCollector)\n\n // Auto-detect and process child component props (slices)\n childSlices := findChildComponentSlices(opts.Props)\n for _, slice := range childSlices {\n setScopeIDsOnSlice(slice)\n setScriptsOnSlice(slice, scriptCollector)\n setPortalsOnSlice(slice, portalCollector)\n setBoolOnSlice(slice, "BfIsChild", true)\n }\n\n // Auto-detect and process single child component props\n singleChildren := findSingleChildComponents(opts.Props)\n for _, child := range singleChildren {\n setScopeIDOnSingle(child)\n setScriptsOnSingle(child, scriptCollector)\n setPortalsOnSingle(child, portalCollector)\n setBoolField(child, "BfIsChild", true)\n }\n\n // Mark the root component so BfPropsAttr emits bf-p only for it\n setBoolField(opts.Props, "BfIsRoot", true)\n\n // Render the component template.\n //\n // Errors here are NOT silently dropped. The original implementation\n // ignored the return value of `ExecuteTemplate`, which masked a real\n // onboarding failure mode: a template referencing a non-existent\n // field (`.Todo.Done` instead of the range dot\'s `.Done`) caused\n // html/template to abort mid-stream, the partial output got\n // returned, and the HTTP server happily flushed a 200 with a\n // truncated body. No error log, no signal \u2014 the user just saw a\n // blank list (#1442 echo TodoApp repro).\n //\n // Now we capture the error and replace the partial output with a\n // visible inline panel (dev mode) or a fenced error comment\n // (production), so the cause is on-screen and grep-able in logs.\n // Either way the renderer also writes to stderr so structured log\n // aggregators see it.\n var componentBuf strings.Builder\n if err := r.templates.ExecuteTemplate(&componentBuf, opts.ComponentName, opts.Props); err != nil {\n fmt.Fprintf(os.Stderr, "barefoot: template %q failed to render: %v\\n", opts.ComponentName, err)\n // Preserve whatever the template did manage to emit before\n // failing (Go\'s text/template flushes incrementally), but\n // follow it with a clearly-marked error block so the user\n // notices something is wrong instead of seeing a silent\n // truncation.\n componentBuf.WriteString(renderTemplateErrorPanel(opts.ComponentName, err))\n }\n\n return template.HTML(componentBuf.String())\n}\n\nfunc (r *Renderer) Render(opts RenderOptions) string {\n // One script + portal collector pair for the whole page.\n scriptCollector := NewScriptCollector()\n portalCollector := NewPortalCollector()\n\n componentHTML := r.renderComponentInto(opts, scriptCollector, portalCollector)\n\n // Determine title (default: "{ComponentName} - BarefootJS")\n title := opts.Title\n if title == "" {\n title = opts.ComponentName + " - BarefootJS"\n }\n\n // Heading (empty means no heading)\n heading := opts.Heading\n\n // Build render context\n ctx := &RenderContext{\n ComponentName: opts.ComponentName,\n Props: opts.Props,\n ComponentHTML: componentHTML,\n Portals: portalCollector.Render(),\n Scripts: BfScripts(scriptCollector),\n Title: title,\n Heading: heading,\n Extra: opts.Extra,\n }\n\n return r.layout(ctx)\n}\n\n// RenderFragment renders a single island subtree into the caller-provided\n// script and portal collectors and returns just its HTML \u2014 no page layout.\n//\n// It exists for hand-authored "region shell" pages (the `@barefootjs/router`\n// showcase): a layout that places several independent islands \u2014 e.g. a header\n// ThemeToggle, an `<aside bf-region>` Sidebar, and a `<PageShell>` wrapping the\n// route content \u2014 must collect ALL their scripts and portals into ONE place so\n// the runtime (`barefoot.js`) and each island\'s client JS are emitted exactly\n// once and share a single reactive instance. Render each island with the same\n// collectors, splice the returned HTML into the shell, then emit\n// `BfScripts(sc)` and `pc.Render()` once at the end of the document.\n//\n// Each fragment is treated as its own root (it emits `bf-p` like any top-level\n// island); nested children declared in its props are wired as children, exactly\n// as in Render.\nfunc (r *Renderer) RenderFragment(opts RenderOptions, scriptCollector *ScriptCollector, portalCollector *PortalCollector) template.HTML {\n return r.renderComponentInto(opts, scriptCollector, portalCollector)\n}\n\n// setScriptsField sets the Scripts field on a struct using reflection.\nfunc setScriptsField(v interface{}, collector *ScriptCollector) {\n val := reflect.ValueOf(v)\n if val.Kind() == reflect.Ptr {\n val = val.Elem()\n }\n if val.Kind() != reflect.Struct {\n return\n }\n field := val.FieldByName("Scripts")\n if field.IsValid() && field.CanSet() {\n field.Set(reflect.ValueOf(collector))\n }\n}\n\n// setPortalsField sets the Portals field on a struct using reflection.\nfunc setPortalsField(v interface{}, collector *PortalCollector) {\n val := reflect.ValueOf(v)\n if val.Kind() == reflect.Ptr {\n val = val.Elem()\n }\n if val.Kind() != reflect.Struct {\n return\n }\n field := val.FieldByName("Portals")\n if field.IsValid() && field.CanSet() {\n field.Set(reflect.ValueOf(collector))\n }\n}\n\n// getStringField extracts a string field from a struct using reflection.\nfunc setBoolField(v interface{}, fieldName string, val bool) {\n rv := reflect.ValueOf(v)\n if rv.Kind() == reflect.Ptr {\n rv = rv.Elem()\n }\n if rv.Kind() != reflect.Struct {\n return\n }\n field := rv.FieldByName(fieldName)\n if field.IsValid() && field.CanSet() && field.Kind() == reflect.Bool {\n field.SetBool(val)\n }\n}\n\nfunc getBoolField(v interface{}, fieldName string) bool {\n val := reflect.ValueOf(v)\n if val.Kind() == reflect.Ptr {\n val = val.Elem()\n }\n if val.Kind() != reflect.Struct {\n return false\n }\n field := val.FieldByName(fieldName)\n if !field.IsValid() || field.Kind() != reflect.Bool {\n return false\n }\n return field.Bool()\n}\n\nfunc getStringField(v interface{}, fieldName string) string {\n val := reflect.ValueOf(v)\n if val.Kind() == reflect.Ptr {\n val = val.Elem()\n }\n if val.Kind() != reflect.Struct {\n return ""\n }\n field := val.FieldByName(fieldName)\n if !field.IsValid() || field.Kind() != reflect.String {\n return ""\n }\n return field.String()\n}\n\n// scopeIDChars is the alphabet for auto-generated ScopeID suffixes. It\n// mirrors the `randomID` helper the go-template adapter emits into the\n// generated New<Component>Props constructors so runtime-assigned and\n// constructor-assigned ids are indistinguishable.\nconst scopeIDChars = "abcdefghijklmnopqrstuvwxyz0123456789"\n\n// randomScopeSuffix returns a random lowercase-alphanumeric string of\n// length n. math/rand (auto-seeded since Go 1.20) is sufficient here: the\n// suffix only needs to be unique enough to keep a page\'s bf-s scope ids\n// from colliding, not cryptographically unpredictable.\nfunc randomScopeSuffix(n int) string {\n b := make([]byte, n)\n for i := range b {\n b[i] = scopeIDChars[rand.Intn(len(scopeIDChars))]\n }\n return string(b)\n}\n\n// scopeIDPrefix derives the human-readable ScopeID prefix from a child\n// component\'s type, e.g. `TodoItemProps` \u2192 `TodoItem`. Matches the\n// `"<Component>_" + randomID(6)` shape the generated constructors use.\nfunc scopeIDPrefix(t reflect.Type) string {\n for t.Kind() == reflect.Ptr {\n t = t.Elem()\n }\n return strings.TrimSuffix(t.Name(), "Props")\n}\n\n// assignScopeID fills a child component\'s ScopeID with a generated id when\n// the caller left it empty, so application code doesn\'t have to mint scope\n// ids by hand (the parent\'s New<Component>Props constructor does the same\n// for components built through it). A non-empty ScopeID is left untouched,\n// so callers can still pin a stable id when they need one.\nfunc assignScopeID(structVal reflect.Value, prefix string) {\n field := structVal.FieldByName("ScopeID")\n if !field.IsValid() || !field.CanSet() || field.Kind() != reflect.String {\n return\n }\n if field.String() != "" {\n return\n }\n id := randomScopeSuffix(6)\n if prefix != "" {\n id = prefix + "_" + id\n }\n field.SetString(id)\n}\n\n// setScopeIDsOnSlice assigns a generated ScopeID to every child in a slice\n// whose ScopeID is empty.\nfunc setScopeIDsOnSlice(slice interface{}) {\n v := reflect.ValueOf(slice)\n if v.Kind() != reflect.Slice {\n return\n }\n prefix := scopeIDPrefix(v.Type().Elem())\n for i := 0; i < v.Len(); i++ {\n item := v.Index(i)\n if item.Kind() == reflect.Ptr {\n if item.IsNil() {\n continue\n }\n item = item.Elem()\n }\n if item.Kind() == reflect.Struct {\n assignScopeID(item, prefix)\n }\n }\n}\n\n// setScopeIDOnSingle assigns a generated ScopeID to a single child\n// component when its ScopeID is empty.\nfunc setScopeIDOnSingle(child interface{}) {\n v := reflect.ValueOf(child)\n if v.Kind() == reflect.Ptr {\n v = v.Elem()\n }\n if v.Kind() != reflect.Struct {\n return\n }\n assignScopeID(v, scopeIDPrefix(v.Type()))\n}\n\n// findChildComponentSlices finds slice fields containing child component props.\n// Child props are identified by having ScopeID and Scripts fields.\nfunc findChildComponentSlices(props interface{}) []interface{} {\n var result []interface{}\n\n val := reflect.ValueOf(props)\n if val.Kind() == reflect.Ptr {\n val = val.Elem()\n }\n if val.Kind() != reflect.Struct {\n return result\n }\n\n for i := 0; i < val.NumField(); i++ {\n field := val.Field(i)\n if field.Kind() != reflect.Slice || field.Len() == 0 {\n continue\n }\n\n elem := field.Index(0)\n if elem.Kind() == reflect.Ptr {\n elem = elem.Elem()\n }\n if elem.Kind() != reflect.Struct {\n continue\n }\n\n hasScopeID := elem.FieldByName("ScopeID").IsValid()\n hasScripts := elem.FieldByName("Scripts").IsValid()\n\n if hasScopeID && hasScripts {\n result = append(result, field.Interface())\n }\n }\n\n return result\n}\n\n// setScriptsOnSlice sets Scripts on all items in a slice.\nfunc setScriptsOnSlice(slice interface{}, collector *ScriptCollector) {\n val := reflect.ValueOf(slice)\n if val.Kind() != reflect.Slice {\n return\n }\n for i := 0; i < val.Len(); i++ {\n item := val.Index(i)\n if item.Kind() == reflect.Ptr {\n item = item.Elem()\n }\n if item.Kind() == reflect.Struct {\n field := item.FieldByName("Scripts")\n if field.IsValid() && field.CanSet() {\n field.Set(reflect.ValueOf(collector))\n }\n }\n }\n}\n\n// setBoolOnSlice sets a bool field on all items in a slice.\nfunc setBoolOnSlice(slice interface{}, fieldName string, val bool) {\n v := reflect.ValueOf(slice)\n if v.Kind() != reflect.Slice {\n return\n }\n for i := 0; i < v.Len(); i++ {\n item := v.Index(i)\n if item.Kind() == reflect.Ptr {\n item = item.Elem()\n }\n if item.Kind() == reflect.Struct {\n field := item.FieldByName(fieldName)\n if field.IsValid() && field.CanSet() && field.Kind() == reflect.Bool {\n field.SetBool(val)\n }\n }\n }\n}\n\n// setPortalsOnSlice sets Portals on all items in a slice.\nfunc setPortalsOnSlice(slice interface{}, collector *PortalCollector) {\n val := reflect.ValueOf(slice)\n if val.Kind() != reflect.Slice {\n return\n }\n for i := 0; i < val.Len(); i++ {\n item := val.Index(i)\n if item.Kind() == reflect.Ptr {\n item = item.Elem()\n }\n if item.Kind() == reflect.Struct {\n field := item.FieldByName("Portals")\n if field.IsValid() && field.CanSet() {\n field.Set(reflect.ValueOf(collector))\n }\n }\n }\n}\n\n// findSingleChildComponents finds single struct fields containing child component props.\n// Child props are identified by having ScopeID and Scripts fields.\nfunc findSingleChildComponents(props interface{}) []interface{} {\n var result []interface{}\n\n val := reflect.ValueOf(props)\n if val.Kind() == reflect.Ptr {\n val = val.Elem()\n }\n if val.Kind() != reflect.Struct {\n return result\n }\n\n for i := 0; i < val.NumField(); i++ {\n field := val.Field(i)\n\n // Handle pointer to struct\n if field.Kind() == reflect.Ptr {\n if field.IsNil() {\n continue\n }\n field = field.Elem()\n }\n\n // Skip non-struct fields (slices handled by findChildComponentSlices)\n if field.Kind() != reflect.Struct {\n continue\n }\n\n hasScopeID := field.FieldByName("ScopeID").IsValid()\n hasScripts := field.FieldByName("Scripts").IsValid()\n\n if hasScopeID && hasScripts {\n result = append(result, field.Addr().Interface())\n }\n }\n\n return result\n}\n\n// setScriptsOnSingle sets Scripts on a single struct child component.\nfunc setScriptsOnSingle(child interface{}, collector *ScriptCollector) {\n val := reflect.ValueOf(child)\n if val.Kind() == reflect.Ptr {\n val = val.Elem()\n }\n if val.Kind() == reflect.Struct {\n field := val.FieldByName("Scripts")\n if field.IsValid() && field.CanSet() {\n field.Set(reflect.ValueOf(collector))\n }\n }\n}\n\n// setPortalsOnSingle sets Portals on a single struct child component.\nfunc setPortalsOnSingle(child interface{}, collector *PortalCollector) {\n val := reflect.ValueOf(child)\n if val.Kind() == reflect.Ptr {\n val = val.Elem()\n }\n if val.Kind() == reflect.Struct {\n field := val.FieldByName("Portals")\n if field.IsValid() && field.CanSet() {\n field.Set(reflect.ValueOf(collector))\n }\n }\n}\n\n// Nullish implements JS `??` for template use (`bf_nullish`, #2248): returns\n// fallback iff v is nil (untyped nil or a nil pointer/map/slice boxed in the\n// interface), otherwise v \u2014 so present-but-falsy `""`/`0`/`false` are KEPT,\n// unlike the truthiness-based template `or`.\nfunc Nullish(v, fallback any) any {\n if v == nil {\n return fallback\n }\n rv := reflect.ValueOf(v)\n switch rv.Kind() {\n case reflect.Ptr, reflect.Map, reflect.Slice, reflect.Interface, reflect.Func, reflect.Chan:\n if rv.IsNil() {\n return fallback\n }\n }\n return v\n}\n\n// ToInt exposes the runtime\'s numeric coercion for generated constructors\n// (#2248): a nillable-lowered numeric prop arrives as `interface{}`, and an\n// untyped Go literal (`Size: 3`) boxes as int even when the prop is\n// float64-shaped \u2014 a direct type assertion would panic where JS accepts the\n// number. Non-numeric values coerce to 0, matching the helpers\' behaviour.\nfunc ToInt(v any) int { return toInt(v) }\n\n// ToFloat64 is ToInt\'s float64 counterpart \u2014 see ToInt.\nfunc ToFloat64(v any) float64 { return toFloat64(v) }\n\n// =============================================================================\n// Internal Helpers\n// =============================================================================\n\nfunc toFloat64(v any) float64 {\n switch n := v.(type) {\n case int:\n return float64(n)\n case int8:\n return float64(n)\n case int16:\n return float64(n)\n case int32:\n return float64(n)\n case int64:\n return float64(n)\n case uint:\n return float64(n)\n case uint8:\n return float64(n)\n case uint16:\n return float64(n)\n case uint32:\n return float64(n)\n case uint64:\n return float64(n)\n case float32:\n return float64(n)\n case float64:\n return n\n default:\n return 0\n }\n}\n\nfunc toInt(v any) int {\n switch n := v.(type) {\n case int:\n return n\n case int8:\n return int(n)\n case int16:\n return int(n)\n case int32:\n return int(n)\n case int64:\n return int(n)\n case uint:\n return int(n)\n case uint8:\n return int(n)\n case uint16:\n return int(n)\n case uint32:\n return int(n)\n case uint64:\n return int(n)\n case float32:\n return int(n)\n case float64:\n return int(n)\n default:\n return 0\n }\n}\n\nfunc isIntLike(v any) bool {\n switch v.(type) {\n case int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64:\n return true\n default:\n return false\n }\n}\n\nfunc toString(v any) string {\n switch s := v.(type) {\n case string:\n return s\n case int:\n return strconv.Itoa(s)\n case int64:\n return strconv.FormatInt(s, 10)\n case float64:\n return strconv.FormatFloat(s, \'f\', -1, 64)\n case bool:\n return strconv.FormatBool(s)\n default:\n rv := reflect.ValueOf(v)\n if rv.Kind() == reflect.Slice || rv.Kind() == reflect.Array {\n // JS `Array.prototype.toString` is `this.join(\',\')`, applied\n // recursively \u2014 a nested array element stringifies the same\n // way rather than via Go\'s `%v`. Reached via `Join`/`ConcatStr`\n // on an element that is itself an array (e.g. `.flat(0)`\'s\n // shallow copy joined afterwards, #2262).\n parts := make([]string, rv.Len())\n for i := 0; i < rv.Len(); i++ {\n parts[i] = toString(rv.Index(i).Interface())\n }\n return strings.Join(parts, ",")\n }\n return ""\n }\n}\n\n// =============================================================================\n// searchParams() \u2014 request-scoped environment signal (router v0.5, #1922)\n// =============================================================================\n\n// SearchParams is the SSR view of the request query string behind the\n// reactive searchParams() environment signal. The route handler builds it\n// from the request URL and assigns it to the component\'s SearchParams input\n// field; the generated template reads it via `.SearchParams.Get "key"`.\n//\n// The zero value is an empty query (url.Values.Get tolerates a nil map), so a\n// render with no request query \u2014 e.g. the adapter conformance harness, which\n// issues no query string \u2014 resolves every key to "", which the template\'s\n// `or`/`??` fallback turns into the author\'s default.\ntype SearchParams struct {\n values url.Values\n}\n\n// NewSearchParams parses a raw query string (with or without a leading "?")\n// into a SearchParams. A malformed query yields an empty set rather than an\n// error, mirroring the browser\'s URLSearchParams, which never throws on junk.\n//\n// Typical handler use (net/http):\n//\n// in := MyComponentInput{SearchParams: bf.NewSearchParams(r.URL.RawQuery)}\nfunc NewSearchParams(raw string) SearchParams {\n raw = strings.TrimPrefix(raw, "?")\n values, err := url.ParseQuery(raw)\n if err != nil {\n values = url.Values{}\n }\n return SearchParams{values: values}\n}\n\n// Get returns the first value associated with key, or "" when the key is\n// absent. This mirrors url.Values.Get, which also returns "" for a\n// present-but-empty value (`?sort=`). Safe on the zero value (nil map).\n//\n// This is not byte-for-byte URLSearchParams.get under the template\'s `??`\n// lowering. JS distinguishes absent (`null`) from present-but-empty (`""`):\n// `null ?? d` yields the default, but `"" ?? d` keeps the empty string. The\n// Go adapter lowers `??` to the `or` builtin \u2014 Go templates have no\n// null-coalescing operator \u2014 so here BOTH an absent key and a present-but-\n// empty value fall back to the author\'s default. The conformance fixture only\n// exercises the absent-key default, where the two runtimes agree; the\n// empty-string divergence is the same general `?? \u2192 or` limitation that\n// applies to any `x ?? default` the Go adapter lowers.\nfunc (s SearchParams) Get(key string) string {\n return s.values.Get(key)\n}\n';
|
|
31973
32588
|
evalGoSource = 'package bf\n\nimport (\n "encoding/json"\n "errors"\n "math"\n "reflect"\n "regexp"\n "sort"\n "strconv"\n "strings"\n "unicode/utf8"\n)\n\n// =============================================================================\n// Lightweight ParsedExpr evaluator (issue #2018)\n// =============================================================================\n//\n// Templates cannot carry a lambda in expression position, which is why the\n// adapter historically special-cased higher-order callbacks (reduce / sort /\n// map / filter / find) into fixed shapes (bf_sort\'s comparator catalogue,\n// bf_reduce\'s +/* fold). This evaluator replaces that ad-hoc list: a callback\n// BODY is carried as a pure `ParsedExpr` subtree (the same structured IR the\n// compiler already produces) and evaluated here against an environment\n// (`{acc, item, \u2026captured free vars}`).\n//\n// Scope: higher-order callback bodies only. Ordinary expressions stay lowered\n// to template-native syntax \u2014 this is NOT a general expression engine.\n//\n// The accepted pure subset and its semantics (evaluation order, coercion,\n// equality, allowed operators / builtins) are documented in spec/compiler.md\n// ("ParsedExpr Evaluator Semantics") and pinned isomorphically by the golden\n// vectors in packages/adapter-tests/vectors/eval-vectors.json (shared\n// with the Perl evaluator). The coercion rules below are the literal JS rules\n// (ToNumber / ToString / ToBoolean) \u2014 deliberately NOT the divergent\n// bf->string / bf_reduce helper conventions \u2014 so the contract is unambiguous\n// and the backends stay byte-isomorphic.\n//\n// String semantics operate on Unicode code points / UTF-8 bytes: `.length`\n// counts code points and relational `<`/`>` compares byte order. This equals\n// the JS reference (UTF-16 code units) across the BMP \u2014 the range template\n// data uses \u2014 and matches the Perl evaluator exactly (the primary "same input\n// \u2192 same output" contract is between the two SSR backends). Astral-plane\n// characters (where JS counts a surrogate pair as length 2 and orders by\n// surrogate code units) are a documented divergence region, alongside the\n// already-documented non-ASCII relational / localeCompare carve-outs\n// (spec/compiler.md). Both backends stay equal; only the JS reference differs,\n// and no corpus vector exercises the astral range.\n\n// EvalExpr evaluates a pure ParsedExpr (carried as its JSON encoding) against\n// env. The result is a value in the JSON domain: a number (Go int or float64 \u2014\n// both are the single JS number type; e.g. `.length` returns int, arithmetic\n// returns float64), string, bool, nil, []any, or map[string]any. A malformed\n// tree yields nil.\nfunc EvalExpr(exprJSON string, env map[string]any) any {\n var node any\n if err := json.Unmarshal([]byte(exprJSON), &node); err != nil {\n return nil\n }\n return EvalNode(node, env)\n}\n\n// EvalNode evaluates an already-decoded ParsedExpr node (a map[string]any with\n// a "kind" discriminator) against env. Exported so callers that already hold a\n// decoded tree (e.g. the golden-vector harness) skip a re-marshal.\nfunc EvalNode(node any, env map[string]any) any {\n n, ok := node.(map[string]any)\n if !ok {\n return nil\n }\n kind, _ := n["kind"].(string)\n switch kind {\n case "literal":\n return n["value"]\n\n case "identifier":\n name, _ := n["name"].(string)\n return env[name]\n\n case "binary":\n op, _ := n["op"].(string)\n return evalBinary(op, EvalNode(n["left"], env), EvalNode(n["right"], env))\n\n case "unary":\n op, _ := n["op"].(string)\n return evalUnary(op, EvalNode(n["argument"], env))\n\n case "logical":\n op, _ := n["op"].(string)\n left := EvalNode(n["left"], env)\n switch op {\n case "&&":\n if !evalTruthy(left) {\n return left\n }\n return EvalNode(n["right"], env)\n case "||":\n if evalTruthy(left) {\n return left\n }\n return EvalNode(n["right"], env)\n case "??":\n if left == nil {\n return EvalNode(n["right"], env)\n }\n return left\n }\n return nil\n\n case "conditional":\n if evalTruthy(EvalNode(n["test"], env)) {\n return EvalNode(n["consequent"], env)\n }\n return EvalNode(n["alternate"], env)\n\n case "member":\n prop, _ := n["property"].(string)\n return evalReadProperty(EvalNode(n["object"], env), prop)\n\n case "index-access":\n return evalReadIndex(EvalNode(n["object"], env), EvalNode(n["index"], env))\n\n case "call":\n // A nested `.map(cb)` / `.filter(cb)` callback call (#2094): syntactically\n // a `call` whose callee is `<recv>.map`/`<recv>.filter` and whose first\n // argument is an `arrow` \u2014 the SAME shape `asCallbackMethodCall`\n // recognizes at compile time, and the shape the `eval-vectors.json`\n // golden corpus itself carries (it stores the genuine `ParsedExpr`, not\n // a bespoke wrapper). Checked BEFORE the builtin-callee gate below,\n // since `<recv>.map` would otherwise resolve to a non-builtin member\n // callee and refuse.\n if method, objNode, arrowNode, ok := evalArrayCallbackCall(n); ok {\n return evalArrayCallback(method, objNode, arrowNode, env)\n }\n name := evalBuiltinName(n["callee"])\n if name == "" {\n return nil\n }\n rawArgs, _ := n["args"].([]any)\n args := make([]any, len(rawArgs))\n for i, a := range rawArgs {\n args[i] = EvalNode(a, env)\n }\n return evalCallBuiltin(name, args)\n\n case "template-literal":\n parts, _ := n["parts"].([]any)\n var sb strings.Builder\n for _, p := range parts {\n pm, _ := p.(map[string]any)\n if t, _ := pm["type"].(string); t == "string" {\n s, _ := pm["value"].(string)\n sb.WriteString(s)\n } else {\n sb.WriteString(evalToString(EvalNode(pm["expr"], env)))\n }\n }\n return sb.String()\n\n case "array-literal":\n elems, _ := n["elements"].([]any)\n out := make([]any, len(elems))\n for i, e := range elems {\n out[i] = EvalNode(e, env)\n }\n return out\n\n case "object-literal":\n props, _ := n["properties"].([]any)\n out := make(map[string]any, len(props))\n for _, p := range props {\n pm, _ := p.(map[string]any)\n key, _ := pm["key"].(string)\n out[key] = EvalNode(pm["value"], env)\n }\n return out\n\n case "array-method":\n // `.includes(x)` / `.join(sep?)` are the `array-method` shapes the\n // evaluator executes (the JS reference\'s `evaluate` "array-method" arm,\n // eval-reference.ts). A nested `.map`/`.filter` is NOT an\n // `array-method` node \u2014 it reaches the `call` case above (it carries\n // an `arrow` callback, not a plain `args` list). Every other\n // array/string method (`slice`, `flat`, \u2026) is refused upstream\n // (BF101) and never reaches here.\n method, _ := n["method"].(string)\n rawArgs, _ := n["args"].([]any)\n if method == "includes" && len(rawArgs) == 1 {\n return evalIncludes(EvalNode(n["object"], env), EvalNode(rawArgs[0], env))\n }\n if method == "join" && len(rawArgs) <= 1 {\n sep := ","\n if len(rawArgs) == 1 {\n sep = evalToString(EvalNode(rawArgs[0], env))\n }\n return evalJoin(EvalNode(n["object"], env), sep)\n }\n return nil\n }\n // arrow-fn / higher-order / unsupported: a callback body containing these\n // is refused upstream (BF101); never reached here.\n return nil\n}\n\n// evalArrayCallbackCall reports whether the decoded `call` node `n` is a\n// nested `.map(cb)` / `.filter(cb)` callback call (#2094): its callee is a\n// non-computed member `<recv>.map`/`<recv>.filter` and its first argument is\n// an `arrow` node. Returns the method name, the (still-encoded) receiver\n// object node, and the (still-encoded) arrow node.\nfunc evalArrayCallbackCall(n map[string]any) (method string, object any, arrow map[string]any, ok bool) {\n callee, _ := n["callee"].(map[string]any)\n if callee == nil || callee["kind"] != "member" {\n return "", nil, nil, false\n }\n if computed, _ := callee["computed"].(bool); computed {\n return "", nil, nil, false\n }\n prop, _ := callee["property"].(string)\n if prop != "map" && prop != "filter" {\n return "", nil, nil, false\n }\n rawArgs, _ := n["args"].([]any)\n if len(rawArgs) == 0 {\n return "", nil, nil, false\n }\n arrowNode, _ := rawArgs[0].(map[string]any)\n if arrowNode == nil || arrowNode["kind"] != "arrow" {\n return "", nil, nil, false\n }\n return prop, callee["object"], arrowNode, true\n}\n\n// evalArrayCallback executes a nested `.map`/`.filter` callback call: evaluates\n// the receiver, then evaluates the arrow body per element in a CHILD env that\n// binds the arrow\'s first param to the element and (when the arrow declares a\n// second param) the second to the integer index \u2014 both 1- and 2-param arrows\n// are supported. `map` keeps one result per element (order-preserving);\n// `filter` keeps the elements whose body evaluates truthy. A non-array\n// receiver degrades to nil (unreachable for a body the compiler validated,\n// since the receiver of a nested `.map`/`.filter` is itself gated upstream).\nfunc evalArrayCallback(method string, objectNode any, arrowNode map[string]any, env map[string]any) any {\n arr := toAnySlice(EvalNode(objectNode, env))\n if arr == nil {\n return nil\n }\n rawParams, _ := arrowNode["params"].([]any)\n params := make([]string, len(rawParams))\n for i, p := range rawParams {\n params[i], _ = p.(string)\n }\n body := arrowNode["body"]\n callCb := func(item any, index int) any {\n inner := make(map[string]any, len(env)+2)\n for k, v := range env {\n inner[k] = v\n }\n if len(params) > 0 {\n inner[params[0]] = item\n }\n if len(params) > 1 {\n inner[params[1]] = index\n }\n return EvalNode(body, inner)\n }\n if method == "map" {\n out := make([]any, len(arr))\n for i, item := range arr {\n out[i] = callCb(item, i)\n }\n return out\n }\n out := []any{}\n for i, item := range arr {\n if evalTruthy(callCb(item, i)) {\n out = append(out, item)\n }\n }\n return out\n}\n\n// evalJoin implements `.join(sep)`: elements ToString\'d and joined; a\n// null/undefined element ToStrings to the empty string (matching JS\n// `Array.prototype.join`, which skips null/undefined rather than rendering\n// the literal string "null"/"undefined"). A non-array receiver degrades to\n// the empty string (unreachable for a validated body).\nfunc evalJoin(obj any, sep string) string {\n arr := toAnySlice(obj)\n if arr == nil {\n return ""\n }\n parts := make([]string, len(arr))\n for i, el := range arr {\n if el == nil {\n parts[i] = ""\n continue\n }\n parts[i] = evalToString(el)\n }\n return strings.Join(parts, sep)\n}\n\n// ---------------------------------------------------------------------------\n// JS coercion primitives (ToNumber / ToString / ToBoolean), pinned so the\n// evaluator matches the JS reference. These are JS-faithful and intentionally\n// distinct from the bf->string / Number helpers, which diverge (null \u2192 "",\n// null/"" \u2192 NaN) for SSR-survival reasons that do not apply to the evaluator.\n// ---------------------------------------------------------------------------\n\n// jsDecimalNumberRe matches the JS StringToNumber decimal numeric literal\n// grammar (ASCII digits only): optional sign, then integer/fraction digits,\n// then an optional exponent. It deliberately excludes underscore digit\n// separators, radix prefixes (0x/0o/0b), and hex-float forms \u2014 none of which\n// are valid JS decimal numeric literals.\nvar jsDecimalNumberRe = regexp.MustCompile(`^[+-]?(?:[0-9]+\\.?[0-9]*|\\.[0-9]+)(?:[eE][+-]?[0-9]+)?$`)\n\nfunc evalToNumber(v any) float64 {\n switch x := v.(type) {\n case nil:\n return 0\n case bool:\n if x {\n return 1\n }\n return 0\n case string:\n t := strings.TrimSpace(x)\n if t == "" {\n return 0\n }\n // Exact JS Infinity spellings (case-sensitive, no other aliases like\n // "infinity"/"inf" are valid JS numeric strings).\n switch t {\n case "Infinity", "+Infinity":\n return math.Inf(1)\n case "-Infinity":\n return math.Inf(-1)\n }\n // Decimal / exponent numeric strings parse JS-faithfully, including\n // overflow: strconv.ParseFloat rejects underscores, hex-floats, and\n // non-canonical "inf"/"nan" spellings via the anchored decimal-grammar\n // gate below, so those correctly yield NaN. The radix-prefixed forms\n // JS Number() also accepts ("0x10" / "0o17" / "0b101") are a\n // documented divergence region: they fail the decimal grammar (a\n // leading "0x"/"0o"/"0b" is not a valid decimal literal) and yield\n // NaN here, as they do in the Perl evaluator (looks_like_number is\n // false for them), so Go==Perl while differing from the JS\n // reference. Template data carries JSON numbers, not radix-string\n // literals, so this never arises in practice.\n if !jsDecimalNumberRe.MatchString(t) {\n return math.NaN()\n }\n f, err := strconv.ParseFloat(t, 64)\n if err == nil {\n return f\n }\n if errors.Is(err, strconv.ErrRange) {\n // ParseFloat still returns the correctly-signed \xB1Inf (or a\n // subnormal) as its best-effort value on overflow/underflow;\n // JS Number() on an overflowing decimal literal yields \xB1Infinity\n // (e.g. "1e1000" -> +Infinity), so surface that value as-is.\n return f\n }\n return math.NaN()\n default:\n if evalIsNumeric(v) {\n return toFloat64(v)\n }\n return math.NaN()\n }\n}\n\n// evalIsNumeric reports whether v is one of the Go numeric types (all of\n// which are the single JS "number" type to the evaluator).\nfunc evalIsNumeric(v any) bool {\n switch v.(type) {\n case int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64, float32, float64:\n return true\n }\n return false\n}\n\nfunc evalToString(v any) string {\n if v == nil {\n return "null"\n }\n // JS spells the non-finite doubles "Infinity" / "-Infinity" / "NaN"; the\n // runtime String() helper (fmt %v) would render "+Inf" / "-Inf" / "NaN",\n // so the non-finite cases are pinned here to stay JS-faithful (and match\n // the Perl evaluator\'s _to_string). Strings, bools and nil are JS-faithful\n // through String() (nil is already handled above as "null").\n //\n // Finite-number formatting is a documented divergence region. Go\'s fmt is\n // shortest-round-trip (it matches JS\'s *digits*, e.g. 0.1+0.2 \u2192\n // "0.30000000000000004"), but its exponent threshold/padding differ from\n // JS Number::toString for very large / very small magnitudes (Go renders\n // 1e6 as "1e+06" where JS keeps "1000000", and "1e-07" vs JS "1e-7").\n // Perl\'s `%.15g` instead diverges on *precision* (the pinned helper-vector\n // "0.3" case). A fully JS-faithful Number::toString is not reimplemented\n // here because Perl has no shortest-round-trip formatter, so the three\n // could never all agree; the common integer / short-decimal range \u2014 what\n // arithmetic over template data produces \u2014 renders identically across all\n // three. Only the \xB10 sign is normalised below, since it is cheap and the\n // realisticly-reachable case.\n if f, ok := v.(float64); ok {\n if math.IsNaN(f) {\n return "NaN"\n }\n if math.IsInf(f, 1) {\n return "Infinity"\n }\n if math.IsInf(f, -1) {\n return "-Infinity"\n }\n // JS `String(-0)` is "0", but fmt %v renders Go\'s negative zero as\n // "-0". `f == 0` matches both \xB10, normalising to JS\'s spelling. (A\n // unary `-` on a zero operand is the way the evaluator can produce -0.)\n if f == 0 {\n return "0"\n }\n }\n // Non-primitive operands (arrays / objects) in ToString position are\n // outside the evaluator subset \u2014 the JS reference refuses them, and the\n // compiler gates such a callback body with BF101 before it ever reaches\n // the runtime. This evaluator runs already-validated bodies, so it does\n // not re-reject here; String() (fmt %v) is a best-effort fallback for that\n // unreachable path, never exercised by the in-subset corpus.\n return String(v)\n}\n\nfunc evalTruthy(v any) bool {\n return isTruthy(v)\n}\n\n// ---------------------------------------------------------------------------\n// Operators\n// ---------------------------------------------------------------------------\n\nfunc evalBinary(op string, l, r any) any {\n switch op {\n case "+":\n // JS `+`: string concatenation once either operand is a string,\n // numeric addition otherwise.\n if _, lok := l.(string); lok {\n return evalToString(l) + evalToString(r)\n }\n if _, rok := r.(string); rok {\n return evalToString(l) + evalToString(r)\n }\n return evalToNumber(l) + evalToNumber(r)\n case "-":\n return evalToNumber(l) - evalToNumber(r)\n case "*":\n return evalToNumber(l) * evalToNumber(r)\n case "/":\n return evalToNumber(l) / evalToNumber(r)\n case "%":\n return math.Mod(evalToNumber(l), evalToNumber(r))\n case "<", "<=", ">", ">=":\n return evalRelational(op, l, r)\n case "===":\n return evalStrictEq(l, r)\n case "!==":\n return !evalStrictEq(l, r)\n }\n // Loose equality / bitwise / shift are out of the subset.\n return nil\n}\n\nfunc evalRelational(op string, l, r any) bool {\n // JS Abstract Relational Comparison: both strings \u2192 compare by code\n // unit; otherwise coerce both to numbers (a NaN operand makes every\n // comparison false).\n var c int\n ls, lok := l.(string)\n rs, rok := r.(string)\n if lok && rok {\n if ls < rs {\n c = -1\n } else if ls > rs {\n c = 1\n }\n } else {\n ln := evalToNumber(l)\n rn := evalToNumber(r)\n if math.IsNaN(ln) || math.IsNaN(rn) {\n return false\n }\n if ln < rn {\n c = -1\n } else if ln > rn {\n c = 1\n }\n }\n switch op {\n case "<":\n return c < 0\n case "<=":\n return c <= 0\n case ">":\n return c > 0\n case ">=":\n return c >= 0\n }\n return false\n}\n\nfunc evalStrictEq(l, r any) bool {\n // Strict `===`: equal JS type and value, no coercion. All numeric Go\n // types are the single JS "number" type, so int 2 === float64 2.\n lnum := evalIsNumeric(l)\n rnum := evalIsNumeric(r)\n if lnum && rnum {\n lf, rf := toFloat64(l), toFloat64(r)\n if math.IsNaN(lf) || math.IsNaN(rf) {\n return false\n }\n return lf == rf\n }\n if lnum != rnum {\n return false\n }\n switch lv := l.(type) {\n case nil:\n return r == nil\n case string:\n rv, ok := r.(string)\n return ok && rv == lv\n case bool:\n rv, ok := r.(bool)\n return ok && rv == lv\n }\n // Non-primitive operands (arrays / objects) are outside the subset: the JS\n // reference refuses `===` on them and the compiler gates such a body with\n // BF101 upstream, so this is unreachable for an in-subset corpus. The\n // runtime trusts that gate rather than re-validating, returning false\n // here (it does not attempt JS reference identity, which templates can\'t\n // model anyway).\n return false\n}\n\n// evalSameValueZero implements `Array.prototype.includes`\'s membership\n// comparison: `===` except `NaN` equals itself (JS\'s SameValueZero). Reuses\n// evalStrictEq \u2014 the one divergence, both operands NaN, is checked first;\n// every other pair (including "one NaN, one not") falls through to the same\n// equality `evalBinary`\'s `===` uses, so the two operators stay in lockstep.\nfunc evalSameValueZero(a, b any) bool {\n if evalIsNumeric(a) && evalIsNumeric(b) {\n af, bf := toFloat64(a), toFloat64(b)\n if math.IsNaN(af) && math.IsNaN(bf) {\n return true\n }\n }\n return evalStrictEq(a, b)\n}\n\n// evalIncludes implements `.includes(needle)`, shared between\n// `Array.prototype.includes` (SameValueZero membership over a slice/array\n// receiver) and `String.prototype.includes` (substring search), mirroring\n// the receiver-type dispatch the SSR template lowering already does\n// (`bf_includes`). Any other receiver type is not a JS `.includes` target;\n// this degrades to false rather than panicking (there is no receiver here\n// for which JS itself would throw), matching the JS reference\n// (eval-reference.ts `includes`).\nfunc evalIncludes(obj, needle any) bool {\n if arr := toAnySlice(obj); arr != nil {\n for _, el := range arr {\n if evalSameValueZero(el, needle) {\n return true\n }\n }\n return false\n }\n if s, ok := obj.(string); ok {\n return strings.Contains(s, evalToString(needle))\n }\n return false\n}\n\nfunc evalUnary(op string, v any) any {\n switch op {\n case "!":\n return !evalTruthy(v)\n case "-":\n return -evalToNumber(v)\n case "+":\n return evalToNumber(v)\n }\n return nil\n}\n\n// ---------------------------------------------------------------------------\n// Built-in calls (the deterministic allowlist). Locale-sensitive builtins\n// (localeCompare) are deliberately excluded to keep the backends isomorphic.\n// ---------------------------------------------------------------------------\n\n// evalBuiltinName resolves a `call` callee to its builtin name (e.g.\n// "Math.max"), or "" if the callee is not an allowlisted builtin reference.\nfunc evalBuiltinName(callee any) string {\n cm, ok := callee.(map[string]any)\n if !ok {\n return ""\n }\n switch cm["kind"] {\n case "identifier":\n name, _ := cm["name"].(string)\n return name\n case "member":\n if computed, _ := cm["computed"].(bool); computed {\n return ""\n }\n obj, _ := cm["object"].(map[string]any)\n if obj == nil || obj["kind"] != "identifier" {\n return ""\n }\n objName, _ := obj["name"].(string)\n prop, _ := cm["property"].(string)\n return objName + "." + prop\n }\n return ""\n}\n\n// evalMathRound rounds a half toward +Infinity (JS Math.round: 2.5\u21923,\n// -2.5\u2192-2), matching the existing `round` helper rather than Go\'s math.Round.\nfunc evalMathRound(n float64) float64 {\n // floor(n+0.5) yields +0 for x in [-0.5, -0], where JS Math.round returns\n // -0. That sign is only observable through a subsequent division\n // (`1 / Math.round(-0.5)` is -Infinity in JS, +Infinity here) \u2014 through\n // ToString both \xB10 render "0". It is left as +0 deliberately: the two SSR\n // backends must stay equal, and Perl can\'t reproduce the -0 divisor sign\n // without fragile, version-dependent zero handling (its native `/` even\n // dies on a zero divisor). So Math.round\'s -0 is a JS-reference-only\n // divergence region, like the astral-plane / radix-string carve-outs.\n return math.Floor(n + 0.5)\n}\n\nfunc evalCallBuiltin(name string, args []any) any {\n switch name {\n case "Math.max":\n if len(args) == 0 {\n return math.Inf(-1)\n }\n m := evalToNumber(args[0])\n for _, a := range args[1:] {\n m = math.Max(m, evalToNumber(a))\n }\n return m\n case "Math.min":\n if len(args) == 0 {\n return math.Inf(1)\n }\n m := evalToNumber(args[0])\n for _, a := range args[1:] {\n m = math.Min(m, evalToNumber(a))\n }\n return m\n case "Math.abs":\n return math.Abs(evalToNumber(arg0(args)))\n case "Math.floor":\n return math.Floor(evalToNumber(arg0(args)))\n case "Math.ceil":\n return math.Ceil(evalToNumber(arg0(args)))\n case "Math.round":\n return evalMathRound(evalToNumber(arg0(args)))\n case "String":\n return evalToString(arg0(args))\n case "Number":\n return evalToNumber(arg0(args))\n case "Boolean":\n return evalTruthy(arg0(args))\n }\n // Any other callee is outside the subset (refused upstream).\n return nil\n}\n\nfunc arg0(args []any) any {\n if len(args) == 0 {\n return nil\n }\n return args[0]\n}\n\n// ---------------------------------------------------------------------------\n// Member / index access\n// ---------------------------------------------------------------------------\n\nfunc evalReadProperty(obj any, key string) any {\n switch o := obj.(type) {\n case string:\n if key == "length" {\n // Code-point count (== JS UTF-16 length across the BMP, == Perl\n // `length`). RuneCountInString avoids the []rune allocation since\n // this can run inside comparator / reducer evaluation.\n return utf8.RuneCountInString(o)\n }\n return nil\n case []any:\n if key == "length" {\n return len(o)\n }\n return nil\n case map[string]any:\n // Case-variant lookup: a callback body reads the raw JS property\n // (`t.duration`), but test data / Go-keyed maps may carry PascalCase\n // keys (`{"Duration": \u2026}`). Reuse the field reader the sort / reduce\n // helpers use \u2014 it resolves `duration` \u2192 `Duration` and reads a\n // genuinely missing key as null (the backends\' single absent value).\n return getFieldValue(o, key)\n case nil:\n return nil\n default:\n // Real template data (Go structs): reuse the field reader the sort /\n // reduce helpers use, which handles case-variant keys.\n return getFieldValue(obj, key)\n }\n}\n\n// ---------------------------------------------------------------------------\n// Evaluator-driven higher-order folds (the generalization of bf_reduce /\n// bf_sort onto the evaluator)\n//\n// These prove the evaluator subsumes the special-cased callback catalogue:\n// the callback BODY is carried as a pure ParsedExpr (JSON) and evaluated per\n// element against an environment, so the op restriction (bf_reduce\'s +/*),\n// the acc-canonical form, and the comparator pattern restriction (bf_sort)\n// all disappear \u2014 any pure reducer / comparator body works. They are the\n// runtime half of the integration; the compiler-side emit migration (carrying\n// callback bodies as ParsedExpr) and the byte-equal divergence decision for\n// the string-`localeCompare` sort path (won\'t-fix for byte-equal SSR, see\n// spec/compiler.md Known limitations) are the remaining follow-up.\n// ---------------------------------------------------------------------------\n\n// toAnySlice copies a reflect-iterable receiver into a fresh []any, returning\n// nil for a non-slice/array (matching the bf_sort / bf_reduce nil-tolerance).\nfunc toAnySlice(items any) []any {\n v := reflect.ValueOf(items)\n // A nil interface yields an invalid Value (Kind() == Invalid, not a\n // panic), so the Slice/Array guard below already tolerates nil; the\n // explicit IsValid check just documents the nil-tolerance intent.\n if !v.IsValid() || (v.Kind() != reflect.Slice && v.Kind() != reflect.Array) {\n return nil\n }\n out := make([]any, v.Len())\n for i := range out {\n out[i] = v.Index(i).Interface()\n }\n return out\n}\n\n// FoldEval folds items into a value via the ParsedExpr evaluator. The reducer\n// body is a pure ParsedExpr (JSON) evaluated against `{accName: acc, itemName:\n// item}` plus the captured free vars in `baseEnv` for each element; `init`\n// seeds the accumulator and `direction` is "left" (reduce) or "right"\n// (reduceRight). This is the evaluator-based generalization of bf_reduce \u2014 any\n// reducer body, not just the `+`/`*` arithmetic catalogue, and `acc` may\n// appear anywhere in the body. `baseEnv` may be nil when the body captures no\n// outer references; the accName / itemName keys shadow any same-named base key.\nfunc FoldEval(items any, bodyJSON, accName, itemName string, init any, direction string, baseEnv map[string]any) any {\n var body any\n if err := json.Unmarshal([]byte(bodyJSON), &body); err != nil {\n return init\n }\n arr := toAnySlice(items)\n if direction == "right" {\n for i, j := 0, len(arr)-1; i < j; i, j = i+1, j-1 {\n arr[i], arr[j] = arr[j], arr[i]\n }\n }\n acc := init\n // Seed the env from the captured free vars once; acc / item are\n // overwritten each iteration (constant base keys carry through).\n env := make(map[string]any, len(baseEnv)+2)\n for k, v := range baseEnv {\n env[k] = v\n }\n for _, item := range arr {\n env[accName] = acc\n env[itemName] = item\n acc = EvalNode(body, env)\n }\n return acc\n}\n\n// SortEval returns a new stable-sorted slice ordered by a ParsedExpr\n// comparator body (JSON) evaluated against `{paramA: a, paramB: b}` plus the\n// captured free vars in `baseEnv` to a number (negative / zero / positive,\n// like a JS comparator). This is the evaluator-based generalization of bf_sort\n// \u2014 any comparator body, not just the subtraction / relational-ternary\n// catalogue. `baseEnv` may be nil. Non-mutating.\nfunc SortEval(items any, cmpJSON, paramA, paramB string, baseEnv map[string]any) []any {\n arr := toAnySlice(items)\n if arr == nil {\n return nil\n }\n var cmp any\n if err := json.Unmarshal([]byte(cmpJSON), &cmp); err != nil {\n return arr\n }\n // One env seeded from the captured free vars; the two operand keys are\n // overwritten per comparison (the comparator runs synchronously).\n env := make(map[string]any, len(baseEnv)+2)\n for k, v := range baseEnv {\n env[k] = v\n }\n sort.SliceStable(arr, func(i, j int) bool {\n env[paramA] = arr[i]\n env[paramB] = arr[j]\n return evalToNumber(EvalNode(cmp, env)) < 0\n })\n return arr\n}\n\n// ---------------------------------------------------------------------------\n// Evaluator-driven higher-order predicates (#2018, P2) \u2014 the generalization\n// of bf_filter / bf_find / bf_find_index / bf_every / bf_some onto the\n// evaluator. The predicate BODY travels as a pure ParsedExpr (JSON) and is\n// evaluated per element against `{param: item}` plus the captured free vars in\n// `baseEnv`, lifting the field-equality / truthiness restriction of the\n// special-cased helpers to any pure predicate. `baseEnv` may be nil.\n// ---------------------------------------------------------------------------\n\n// decodeEvalBody unmarshals a serialized ParsedExpr body; ok is false on bad\n// JSON (only reachable by a corrupt emit \u2014 the adapter always emits valid JSON).\nfunc decodeEvalBody(bodyJSON string) (any, bool) {\n var body any\n if err := json.Unmarshal([]byte(bodyJSON), &body); err != nil {\n return nil, false\n }\n return body, true\n}\n\n// seedPredEnv copies the captured free vars into a fresh env with room for the\n// single predicate param, which the callers overwrite per element.\nfunc seedPredEnv(baseEnv map[string]any) map[string]any {\n env := make(map[string]any, len(baseEnv)+1)\n for k, v := range baseEnv {\n env[k] = v\n }\n return env\n}\n\n// FilterEval returns a new slice of the elements for which the predicate body\n// evaluates truthy \u2014 the evaluator generalization of bf_filter. Returns a\n// non-nil empty slice when nothing matches (so a downstream `range` / `bf_join`\n// sees a real slice); returns nil only on a bad body.\nfunc FilterEval(items any, predJSON, param string, baseEnv map[string]any) []any {\n pred, ok := decodeEvalBody(predJSON)\n if !ok {\n return nil\n }\n env := seedPredEnv(baseEnv)\n out := []any{}\n for _, item := range toAnySlice(items) {\n env[param] = item\n if evalTruthy(EvalNode(pred, env)) {\n out = append(out, item)\n }\n }\n return out\n}\n\n// EveryEval reports whether every element satisfies the predicate (vacuously\n// true for an empty receiver, like JS) \u2014 the generalization of bf_every.\nfunc EveryEval(items any, predJSON, param string, baseEnv map[string]any) bool {\n pred, ok := decodeEvalBody(predJSON)\n if !ok {\n return false\n }\n env := seedPredEnv(baseEnv)\n for _, item := range toAnySlice(items) {\n env[param] = item\n if !evalTruthy(EvalNode(pred, env)) {\n return false\n }\n }\n return true\n}\n\n// SomeEval reports whether any element satisfies the predicate (false for an\n// empty receiver, like JS) \u2014 the generalization of bf_some.\nfunc SomeEval(items any, predJSON, param string, baseEnv map[string]any) bool {\n pred, ok := decodeEvalBody(predJSON)\n if !ok {\n return false\n }\n env := seedPredEnv(baseEnv)\n for _, item := range toAnySlice(items) {\n env[param] = item\n if evalTruthy(EvalNode(pred, env)) {\n return true\n }\n }\n return false\n}\n\n// FindEval returns the first element satisfying the predicate, or nil when none\n// does \u2014 the generalization of bf_find. `forward` false searches from the end\n// (findLast).\nfunc FindEval(items any, predJSON, param string, forward bool, baseEnv map[string]any) any {\n pred, ok := decodeEvalBody(predJSON)\n if !ok {\n return nil\n }\n env := seedPredEnv(baseEnv)\n arr := toAnySlice(items)\n for n := range arr {\n i := n\n if !forward {\n i = len(arr) - 1 - n\n }\n env[param] = arr[i]\n if evalTruthy(EvalNode(pred, env)) {\n return arr[i]\n }\n }\n return nil\n}\n\n// FindIndexEval returns the index of the first element satisfying the predicate,\n// or -1 when none does \u2014 the generalization of bf_find_index. `forward` false\n// searches from the end (findLastIndex).\nfunc FindIndexEval(items any, predJSON, param string, forward bool, baseEnv map[string]any) int {\n pred, ok := decodeEvalBody(predJSON)\n if !ok {\n return -1\n }\n env := seedPredEnv(baseEnv)\n arr := toAnySlice(items)\n for n := range arr {\n i := n\n if !forward {\n i = len(arr) - 1 - n\n }\n env[param] = arr[i]\n if evalTruthy(EvalNode(pred, env)) {\n return i\n }\n }\n return -1\n}\n\n// FlatMapEval projects each element through the projection body (a pure\n// ParsedExpr JSON evaluated against `{param: item}` + baseEnv) and flattens the\n// results one level \u2014 the evaluator generalization of bf_flat_map /\n// bf_flat_map_tuple. A projection that yields a slice contributes its elements;\n// any other value contributes itself (matching JS `.flatMap`, where a non-array\n// return is kept as a single element). Returns a non-nil empty slice on a bad\n// body so a downstream `range` / `bf_join` sees a real slice.\nfunc FlatMapEval(items any, projJSON, param string, baseEnv map[string]any) []any {\n proj, ok := decodeEvalBody(projJSON)\n if !ok {\n return []any{}\n }\n env := seedPredEnv(baseEnv)\n out := []any{}\n for _, item := range toAnySlice(items) {\n env[param] = item\n v := EvalNode(proj, env)\n // Flatten any slice/array kind one level, not just []any: real Go\n // template data projects a field like `i.tags` to a typed slice\n // (`[]string` / `[]int`), which `toAnySlice` normalizes to []any. A\n // non-slice value (string / number / struct / nil) contributes itself,\n // matching JS `.flatMap` (a non-array return is kept as one element).\n if sub := toAnySlice(v); sub != nil {\n out = append(out, sub...)\n } else {\n out = append(out, v)\n }\n }\n return out\n}\n\n// MapEval projects each element through the projection body (a pure ParsedExpr\n// JSON evaluated against `{param: item}` + baseEnv), keeping one result per\n// element \u2014 the value-producing `.map(cb)` lowering (#2073). Unlike\n// FlatMapEval there is no flatten: a projection yielding a slice contributes\n// that slice as a single element, matching JS `.map`. Returns a non-nil empty\n// slice on a bad body so a downstream `range` / `bf_join` sees a real slice.\nfunc MapEval(items any, projJSON, param string, baseEnv map[string]any) []any {\n proj, ok := decodeEvalBody(projJSON)\n if !ok {\n return []any{}\n }\n env := seedPredEnv(baseEnv)\n out := []any{}\n for _, item := range toAnySlice(items) {\n env[param] = item\n out = append(out, EvalNode(proj, env))\n }\n return out\n}\n\n// Env builds the captured-free-var environment for FoldEval / SortEval from a\n// flat key, value, key, value, \u2026 argument list \u2014 the adapter emits\n// `bf_env "k1" v1 "k2" v2 \u2026` for the free variables a callback body references\n// beyond its own params. An odd trailing key with no value is ignored; with no\n// pairs it returns an empty (non-nil) map, the no-capture case. A non-string\n// key (only reachable by a malformed template call, never by the adapter\'s\n// quoted-literal emit) is skipped rather than collapsed into an `env[""]` slot.\nfunc Env(pairs ...any) map[string]any {\n env := make(map[string]any, len(pairs)/2)\n for i := 0; i+1 < len(pairs); i += 2 {\n key, ok := pairs[i].(string)\n if !ok {\n continue\n }\n env[key] = pairs[i+1]\n }\n return env\n}\n\nfunc evalReadIndex(obj any, index any) any {\n switch o := obj.(type) {\n case []any:\n f := evalToNumber(index)\n i := int(f)\n if float64(i) != f || i < 0 || i >= len(o) {\n return nil\n }\n return o[i]\n case map[string]any:\n return o[evalToString(index)]\n case nil:\n return nil\n default:\n return getFieldValue(obj, evalToString(index))\n }\n}\n';
|
|
32589
|
+
repropsGoSource = '// Per-row child props reconstruction (#2448).\n//\n// `bf_with_props` (#2445) overrides fields on a child\'s already-constructed\n// shared instance by reflection. That is correct for a plain passthrough prop\n// and WRONG for anything the child\'s constructor DERIVES from it: a\n// `createMemo` body and a `createSignal` initial value are both baked into the\n// struct once by `New<Child>Props`, and reflection cannot re-run that.\n//\n// This file is the fix. Instead of patching fields, the parent asks the child\n// to REBUILD its props: reconstruct the constructor Input from the base\n// instance, apply the row\'s overrides, re-run the real constructor. Every\n// derived field recomputes because the real Go code runs again.\n//\n// The constructor cannot be called from a template directly \u2014 `html/template`\n// has no expression language and can only call FuncMap entries. So the\n// compiler emits, per component, a closure that does the rebuild in generated\n// Go (typed field assignments, no reflection), and registers it here from the\n// generated package\'s `init()`. `FuncMap()` gains exactly ONE fixed entry,\n// `bf_reprops`, so `t.Funcs(bf.FuncMap())` keeps working unchanged.\n//\n// The registry is consulted at template EXECUTE time, never at `Funcs()` time.\n// That is load-bearing, not incidental: Go initializes a package\'s variables\n// BEFORE its `init()` functions, so an app that builds its template set in a\n// package-level var \u2014\n//\n// var tmpl = template.Must(template.New("").Funcs(bf.FuncMap()).ParseGlob(...))\n//\n// \u2014 calls `FuncMap()` while the registry is still empty. Merging the\n// constructors into `FuncMap()`\'s return value would fail that app at parse\n// time with `function "bf_new_Badge" not defined`. Looking them up behind one\n// fixed entry, at execute time, makes the ordering irrelevant.\npackage bf\n\nimport (\n "fmt"\n "reflect"\n "sync"\n)\n\n// RepropsFunc rebuilds a child component\'s props from a base instance plus a\n// flat name/value override list (`"Text", .Label, "N", .N, \u2026`), by re-running\n// the component\'s generated constructor.\n//\n// Names in `kv` are the Go FIELD names the PARENT computed from the JSX\n// attribute (`n=` \u2192 `"N"`). A generated implementation maps those onto its own\n// constructor Input, which is what makes an aliased destructure\n// (`{ n: count }`, whose field is `Count`) land correctly \u2014 the mapping lives\n// in generated code that knows both sides.\n//\n// Identity fields (ScopeID / BfParent / BfMount) MUST be carried over from the\n// base rather than re-derived: `New<Child>Props` mints a random ScopeID when\n// given an empty one, so a naive re-run would give every row its own scope and\n// break hydration. Fields that live only on Props and never on Input (Scripts,\n// BfIsChild, BfDataKey) must be carried over for the same reason.\ntype RepropsFunc func(base interface{}, kv ...interface{}) (interface{}, error)\n\nvar (\n repropsMu sync.RWMutex\n repropsRegistry = map[string]RepropsFunc{}\n)\n\n// RegisterReprops registers a component\'s props rebuilder under its component\n// name. Called from the generated package\'s `init()`; re-registering the same\n// name replaces the previous entry, so a rebuilt components file in a\n// long-lived dev process wins.\nfunc RegisterReprops(name string, fn RepropsFunc) {\n repropsMu.Lock()\n defer repropsMu.Unlock()\n repropsRegistry[name] = fn\n}\n\n// Reprops is the `bf_reprops` FuncMap entry: rebuild `base` with the row\'s\n// overrides applied, by re-running the named component\'s constructor.\n//\n// {{template "Badge" (bf_reprops "Badge" $.BadgeSlot0 "Text" .Label "N" .N)}}\n//\n// An unregistered name is an error rather than a silent passthrough: the\n// compiler only emits this call for a component whose rebuilder it also\n// emitted, so a missing entry means the generated package was not linked in,\n// and falling back to the stale shared instance would reintroduce exactly the\n// silently-wrong output this exists to prevent.\nfunc Reprops(name string, base interface{}, kv ...interface{}) (interface{}, error) {\n if len(kv)%2 != 0 {\n return nil, fmt.Errorf("bf_reprops: odd number of key/value arguments (%d) for %q", len(kv), name)\n }\n // Field names are validated HERE, not in each generated rebuilder, so the\n // generated `name, _ := kv[i].(string)` is safe by construction. Rejecting\n // them matches `bf_with_props`: a non-string name would otherwise degrade\n // to `""`, match no case, and silently drop the override \u2014 the same class\n // of silent misrender this whole path exists to remove.\n for i := 0; i < len(kv); i += 2 {\n if _, ok := kv[i].(string); !ok {\n return nil, fmt.Errorf(\n "bf_reprops: %s field name at position %d must be a string, got %T", name, i, kv[i])\n }\n }\n repropsMu.RLock()\n fn, ok := repropsRegistry[name]\n repropsMu.RUnlock()\n if !ok {\n return nil, fmt.Errorf(\n "bf_reprops: no props rebuilder registered for %q \u2014 is the generated components package linked into this binary?",\n name,\n )\n }\n return fn(base, kv...)\n}\n\n// RepropsTypeError is the error a generated rebuilder returns when handed a\n// value that is not its own props struct. Kept here so the generated code\n// stays a fixed shape instead of formatting its own message.\nfunc RepropsTypeError(name string, base interface{}) error {\n return fmt.Errorf("bf_reprops: the %s rebuilder got %T, not its own props struct", name, base)\n}\n\n// RepropsUnknownFieldError is what a generated rebuilder returns for a field\n// name it has no case for.\n//\n// Unlike `bf_with_props`, whose unknown-field passthrough is load-bearing (a\n// prop routed into a rest bag has no named field to set), a rebuilder is only\n// emitted for components with NO rest bag and no spread slot, and the compiler\n// emits a case for every prop the parent can override. So an unknown name here\n// is a compiler gap, and dropping it silently would leave the override\n// unapplied \u2014 the failure mode this path replaced.\nfunc RepropsUnknownFieldError(component, field string) error {\n return fmt.Errorf("bf_reprops: %s has no overridable field %q", component, field)\n}\n\n// RepropsAssign writes `val` into `*target`, which a generated rebuilder passes\n// as a pointer to one field of its constructor Input (`&in.N`).\n//\n// This is the one place the rebuild uses reflection, and it does so on purpose:\n// it delegates to the SAME `setStructFieldValue` that `bf_with_props` uses, so\n// a prop that already assigned correctly under the old helper assigns\n// identically under this one. Re-deriving the conversion rules per Go type in\n// the generator would be more code and would drift from that behaviour.\n//\n// `field` names the field for the error message; `component` names the owner.\nfunc RepropsAssign(component, field string, target interface{}, val interface{}) error {\n p := reflect.ValueOf(target)\n if p.Kind() != reflect.Ptr || p.IsNil() {\n return fmt.Errorf("bf_reprops: %s.%s: target must be a non-nil pointer, got %T", component, field, target)\n }\n if err := setStructFieldValue(p.Elem(), val); err != nil {\n return fmt.Errorf("bf_reprops: %s.%s: %w", component, field, err)\n }\n return nil\n}\n\n// RepropsRegistered reports whether a rebuilder is registered for `name`.\n// For tests and diagnostics.\nfunc RepropsRegistered(name string) bool {\n repropsMu.RLock()\n defer repropsMu.RUnlock()\n _, ok := repropsRegistry[name]\n return ok\n}\n';
|
|
31974
32590
|
streamingGoSource = `// Package bf \u2014 Out-of-Order Streaming SSR helpers
|
|
31975
32591
|
//
|
|
31976
32592
|
// Provides StreamRenderer for progressive page rendering using HTTP
|
|
@@ -32189,6 +32805,7 @@ function goCommonFiles() {
|
|
|
32189
32805
|
"env.go": GO_ENV_GO,
|
|
32190
32806
|
"bf-runtime/bf.go": bfGoSource,
|
|
32191
32807
|
"bf-runtime/eval.go": evalGoSource,
|
|
32808
|
+
"bf-runtime/reprops.go": repropsGoSource,
|
|
32192
32809
|
"bf-runtime/streaming.go": streamingGoSource,
|
|
32193
32810
|
"bf-runtime/bfdev/bfdev.go": bfdevGoSource,
|
|
32194
32811
|
"bf-runtime/go.mod": GO_BF_RUNTIME_GO_MOD,
|
|
@@ -33268,6 +33885,7 @@ replace github.com/barefootjs/runtime/bf => ./bf-runtime
|
|
|
33268
33885
|
"go.mod": ECHO_GO_MOD,
|
|
33269
33886
|
"bf-runtime/bf.go": bfGoSource,
|
|
33270
33887
|
"bf-runtime/eval.go": evalGoSource,
|
|
33888
|
+
"bf-runtime/reprops.go": repropsGoSource,
|
|
33271
33889
|
"bf-runtime/streaming.go": streamingGoSource,
|
|
33272
33890
|
"bf-runtime/go.mod": ECHO_BF_RUNTIME_GO_MOD,
|
|
33273
33891
|
"barefoot.config.ts": ECHO_BAREFOOT_CONFIG_TS,
|
|
@@ -111027,7 +111645,7 @@ __export(scenario_driver_exports, {
|
|
|
111027
111645
|
import { writeFileSync as writeFileSync9, mkdtempSync, rmSync, readFileSync as readFileSync19, existsSync as existsSync23, statSync as statSync3 } from "node:fs";
|
|
111028
111646
|
import { join as join2, dirname as dirname7, resolve as resolve11 } from "node:path";
|
|
111029
111647
|
import { tmpdir } from "node:os";
|
|
111030
|
-
import
|
|
111648
|
+
import ts28 from "typescript";
|
|
111031
111649
|
function externalRuntimeImport(clientJs) {
|
|
111032
111650
|
const chunks = Array.isArray(clientJs) ? clientJs : [clientJs];
|
|
111033
111651
|
for (const chunk of chunks) {
|
|
@@ -111097,11 +111715,11 @@ function resolveLocalFile(spec) {
|
|
|
111097
111715
|
}
|
|
111098
111716
|
function rewriteLocalImports(js, chunkPath, inlined) {
|
|
111099
111717
|
const chunkDir = dirname7(chunkPath);
|
|
111100
|
-
const sf =
|
|
111718
|
+
const sf = ts28.createSourceFile("chunk.mjs", js, ts28.ScriptTarget.Latest, false, ts28.ScriptKind.JS);
|
|
111101
111719
|
const edits = [];
|
|
111102
111720
|
for (const stmt of sf.statements) {
|
|
111103
|
-
if (!
|
|
111104
|
-
if (!
|
|
111721
|
+
if (!ts28.isImportDeclaration(stmt)) continue;
|
|
111722
|
+
if (!ts28.isStringLiteral(stmt.moduleSpecifier)) continue;
|
|
111105
111723
|
const spec = stmt.moduleSpecifier.text;
|
|
111106
111724
|
if (!spec.startsWith(".")) continue;
|
|
111107
111725
|
const resolved = resolveLocalFile(join2(chunkDir, spec.replace(/\.client\.js$/, "")));
|
|
@@ -111113,13 +111731,13 @@ function rewriteLocalImports(js, chunkPath, inlined) {
|
|
|
111113
111731
|
const abs = resolve11(resolved);
|
|
111114
111732
|
if (inlined.has(abs)) {
|
|
111115
111733
|
const clause = stmt.importClause;
|
|
111116
|
-
if (clause && (clause.name || clause.namedBindings &&
|
|
111734
|
+
if (clause && (clause.name || clause.namedBindings && ts28.isNamespaceImport(clause.namedBindings))) {
|
|
111117
111735
|
throw new Error(
|
|
111118
111736
|
`"${spec}" (imported by ${chunkPath}) uses a default or namespace import of a sibling client file, which the dynamic scenario runner cannot bind after inlining that file into the run. Import its exports by name, or use the static budget (\`bf debug profile <component>\`), which needs no run.`
|
|
111119
111737
|
);
|
|
111120
111738
|
}
|
|
111121
111739
|
const shims = [];
|
|
111122
|
-
if (clause?.namedBindings &&
|
|
111740
|
+
if (clause?.namedBindings && ts28.isNamedImports(clause.namedBindings)) {
|
|
111123
111741
|
for (const el of clause.namedBindings.elements) {
|
|
111124
111742
|
if (el.propertyName) shims.push(`var ${el.name.text} = ${el.propertyName.text}`);
|
|
111125
111743
|
}
|
|
@@ -111628,9 +112246,9 @@ function findProjectConfig(startDir) {
|
|
|
111628
112246
|
let dir = path.resolve(startDir);
|
|
111629
112247
|
const { root: fsRoot } = path.parse(dir);
|
|
111630
112248
|
while (true) {
|
|
111631
|
-
const
|
|
111632
|
-
if (existsSync2(
|
|
111633
|
-
return { dir, tsConfigPath:
|
|
112249
|
+
const ts29 = path.join(dir, "barefoot.config.ts");
|
|
112250
|
+
if (existsSync2(ts29)) {
|
|
112251
|
+
return { dir, tsConfigPath: ts29 };
|
|
111634
112252
|
}
|
|
111635
112253
|
if (dir === fsRoot) return null;
|
|
111636
112254
|
dir = path.dirname(dir);
|