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