@barefootjs/go-template 0.35.0 → 0.35.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/adapter/expr/url-builder.d.ts +37 -1
- package/dist/adapter/expr/url-builder.d.ts.map +1 -1
- package/dist/adapter/go-template-adapter.d.ts +18 -1
- package/dist/adapter/go-template-adapter.d.ts.map +1 -1
- package/dist/adapter/index.js +42 -22
- package/dist/conformance-pins.d.ts.map +1 -1
- package/dist/index.js +45 -25
- package/dist/vite.js +70 -50
- package/package.json +5 -5
- package/src/__tests__/go-template-adapter.test.ts +167 -7
- package/src/adapter/expr/url-builder.ts +63 -28
- package/src/adapter/go-template-adapter.ts +62 -37
- package/src/conformance-pins.ts +13 -7
package/dist/vite.js
CHANGED
|
@@ -8,7 +8,7 @@ import { devModuleUrl, loadManifest, resolveDevOrigin, resolveScriptAssets, toPo
|
|
|
8
8
|
import ts25 from "typescript";
|
|
9
9
|
|
|
10
10
|
// ../jsx/src/analyzer.ts
|
|
11
|
-
import
|
|
11
|
+
import ts11 from "typescript";
|
|
12
12
|
|
|
13
13
|
// ../jsx/src/expression-parser.ts
|
|
14
14
|
import ts from "typescript";
|
|
@@ -2462,14 +2462,17 @@ function freshCounters() {
|
|
|
2462
2462
|
}
|
|
2463
2463
|
|
|
2464
2464
|
// ../jsx/src/analyzer-context.ts
|
|
2465
|
-
import
|
|
2465
|
+
import ts10 from "typescript";
|
|
2466
2466
|
|
|
2467
2467
|
// ../jsx/src/strip-types.ts
|
|
2468
2468
|
import ts8 from "typescript";
|
|
2469
2469
|
|
|
2470
|
+
// ../jsx/src/reactivity-checker.ts
|
|
2471
|
+
import ts9 from "typescript";
|
|
2472
|
+
|
|
2470
2473
|
// ../jsx/src/analyzer-context.ts
|
|
2471
|
-
var _typePrinter =
|
|
2472
|
-
var _blankTypeSourceFile =
|
|
2474
|
+
var _typePrinter = ts10.createPrinter({ removeComments: true, omitTrailingSemicolon: true });
|
|
2475
|
+
var _blankTypeSourceFile = ts10.createSourceFile("__bf_types__.ts", "", ts10.ScriptTarget.Latest);
|
|
2473
2476
|
|
|
2474
2477
|
// ../jsx/src/errors.ts
|
|
2475
2478
|
var ErrorCodes = {
|
|
@@ -2702,46 +2705,46 @@ function extractFreeIdentifiersFromNode(node) {
|
|
|
2702
2705
|
const ids = new Set;
|
|
2703
2706
|
const boundNames = new Set;
|
|
2704
2707
|
function addBindingNames(name, out) {
|
|
2705
|
-
if (
|
|
2708
|
+
if (ts11.isIdentifier(name))
|
|
2706
2709
|
out.push(name.text);
|
|
2707
|
-
else if (
|
|
2710
|
+
else if (ts11.isObjectBindingPattern(name))
|
|
2708
2711
|
name.elements.forEach((e) => addBindingNames(e.name, out));
|
|
2709
|
-
else if (
|
|
2712
|
+
else if (ts11.isArrayBindingPattern(name))
|
|
2710
2713
|
name.elements.forEach((e) => {
|
|
2711
|
-
if (!
|
|
2714
|
+
if (!ts11.isOmittedExpression(e))
|
|
2712
2715
|
addBindingNames(e.name, out);
|
|
2713
2716
|
});
|
|
2714
2717
|
}
|
|
2715
2718
|
function visit(n) {
|
|
2716
|
-
if (
|
|
2719
|
+
if (ts11.isTypeNode(n))
|
|
2717
2720
|
return;
|
|
2718
|
-
if (
|
|
2721
|
+
if (ts11.isIdentifier(n)) {
|
|
2719
2722
|
const parent = n.parent;
|
|
2720
|
-
if (parent &&
|
|
2723
|
+
if (parent && ts11.isPropertyAccessExpression(parent) && parent.name === n)
|
|
2721
2724
|
return;
|
|
2722
|
-
if (parent &&
|
|
2725
|
+
if (parent && ts11.isPropertyAssignment(parent) && parent.name === n)
|
|
2723
2726
|
return;
|
|
2724
|
-
if (parent &&
|
|
2727
|
+
if (parent && ts11.isParameter(parent) && parent.name === n)
|
|
2725
2728
|
return;
|
|
2726
|
-
if (parent &&
|
|
2729
|
+
if (parent && ts11.isVariableDeclaration(parent) && parent.name === n)
|
|
2727
2730
|
return;
|
|
2728
2731
|
if (boundNames.has(n.text))
|
|
2729
2732
|
return;
|
|
2730
2733
|
ids.add(n.text);
|
|
2731
2734
|
return;
|
|
2732
2735
|
}
|
|
2733
|
-
if (
|
|
2736
|
+
if (ts11.isArrowFunction(n)) {
|
|
2734
2737
|
const params = [];
|
|
2735
2738
|
for (const p of n.parameters)
|
|
2736
2739
|
addBindingNames(p.name, params);
|
|
2737
2740
|
for (const name of params)
|
|
2738
2741
|
boundNames.add(name);
|
|
2739
|
-
|
|
2742
|
+
ts11.forEachChild(n, visit);
|
|
2740
2743
|
for (const name of params)
|
|
2741
2744
|
boundNames.delete(name);
|
|
2742
2745
|
return;
|
|
2743
2746
|
}
|
|
2744
|
-
|
|
2747
|
+
ts11.forEachChild(n, visit);
|
|
2745
2748
|
}
|
|
2746
2749
|
visit(node);
|
|
2747
2750
|
return ids;
|
|
@@ -2815,7 +2818,7 @@ function preambleAnalysisTemplateText(p) {
|
|
|
2815
2818
|
}
|
|
2816
2819
|
|
|
2817
2820
|
// ../jsx/src/module-exports.ts
|
|
2818
|
-
import
|
|
2821
|
+
import ts12 from "typescript";
|
|
2819
2822
|
function formatParamWithType(p) {
|
|
2820
2823
|
const rest = p.isRest ? "..." : "";
|
|
2821
2824
|
const optional = p.optional ? "?" : "";
|
|
@@ -2850,21 +2853,21 @@ function findAssignedNames(bodyText, candidates) {
|
|
|
2850
2853
|
const assigned = new Set;
|
|
2851
2854
|
if (candidates.size === 0)
|
|
2852
2855
|
return assigned;
|
|
2853
|
-
const sf =
|
|
2856
|
+
const sf = ts12.createSourceFile("bf-assignment-scan.tsx", bodyText, ts12.ScriptTarget.Latest, false, ts12.ScriptKind.TSX);
|
|
2854
2857
|
const record = (target) => {
|
|
2855
|
-
if (
|
|
2858
|
+
if (ts12.isIdentifier(target) && candidates.has(target.text)) {
|
|
2856
2859
|
assigned.add(target.text);
|
|
2857
2860
|
}
|
|
2858
2861
|
};
|
|
2859
2862
|
const visit = (node) => {
|
|
2860
|
-
if (
|
|
2863
|
+
if (ts12.isBinaryExpression(node) && isAssignmentOperator(node.operatorToken.kind)) {
|
|
2861
2864
|
record(node.left);
|
|
2862
|
-
} else if ((
|
|
2865
|
+
} else if ((ts12.isPrefixUnaryExpression(node) || ts12.isPostfixUnaryExpression(node)) && (node.operator === ts12.SyntaxKind.PlusPlusToken || node.operator === ts12.SyntaxKind.MinusMinusToken)) {
|
|
2863
2866
|
record(node.operand);
|
|
2864
2867
|
}
|
|
2865
|
-
|
|
2868
|
+
ts12.forEachChild(node, visit);
|
|
2866
2869
|
};
|
|
2867
|
-
|
|
2870
|
+
ts12.forEachChild(sf, visit);
|
|
2868
2871
|
return assigned;
|
|
2869
2872
|
}
|
|
2870
2873
|
function closeOverWritersOfMutableBindings(primaryRefs, declarations, mutableNames) {
|
|
@@ -2887,12 +2890,9 @@ function closeOverWritersOfMutableBindings(primaryRefs, declarations, mutableNam
|
|
|
2887
2890
|
return reachable;
|
|
2888
2891
|
}
|
|
2889
2892
|
function isAssignmentOperator(kind) {
|
|
2890
|
-
return kind >=
|
|
2893
|
+
return kind >= ts12.SyntaxKind.FirstAssignment && kind <= ts12.SyntaxKind.LastAssignment;
|
|
2891
2894
|
}
|
|
2892
2895
|
|
|
2893
|
-
// ../jsx/src/reactivity-checker.ts
|
|
2894
|
-
import ts12 from "typescript";
|
|
2895
|
-
|
|
2896
2896
|
// ../jsx/src/free-refs.ts
|
|
2897
2897
|
import ts13 from "typescript";
|
|
2898
2898
|
var _bindingMapCache = new WeakMap;
|
|
@@ -6183,21 +6183,38 @@ function lowerUrlGuard(ctx, g) {
|
|
|
6183
6183
|
if (isBoolShape) {
|
|
6184
6184
|
return ctx.convertConditionToGo(stringifyParsedExpr(g), g).condition;
|
|
6185
6185
|
}
|
|
6186
|
-
const valueGo =
|
|
6186
|
+
const valueGo = lowerValueOperand(ctx, g);
|
|
6187
6187
|
return `ne ${valueGo} ""`;
|
|
6188
6188
|
}
|
|
6189
6189
|
function lowerTernary(ctx, test, consequent, alternate) {
|
|
6190
6190
|
const t = lowerTernaryTest(ctx, test);
|
|
6191
|
-
return `(bf_ternary ${t} ${
|
|
6191
|
+
return `(bf_ternary ${t} ${lowerValueOperand(ctx, consequent)} ${lowerValueOperand(ctx, alternate)})`;
|
|
6192
6192
|
}
|
|
6193
|
-
function
|
|
6193
|
+
function lowerTemplateLiteralValue(ctx, parts) {
|
|
6194
|
+
const terms = [];
|
|
6195
|
+
for (const part of parts) {
|
|
6196
|
+
if (part.type === "string") {
|
|
6197
|
+
if (part.value !== "")
|
|
6198
|
+
terms.push(`"${escapeGoString(part.value)}"`);
|
|
6199
|
+
} else {
|
|
6200
|
+
terms.push(lowerValueOperand(ctx, part.expr));
|
|
6201
|
+
}
|
|
6202
|
+
}
|
|
6203
|
+
if (terms.length === 0)
|
|
6204
|
+
return '""';
|
|
6205
|
+
return terms.slice(1).reduce((acc, t) => `(bf_concat_str ${acc} ${t})`, terms[0]);
|
|
6206
|
+
}
|
|
6207
|
+
function lowerValueOperand(ctx, n) {
|
|
6194
6208
|
if (n.kind === "conditional") {
|
|
6195
6209
|
return lowerTernary(ctx, n.test, n.consequent, n.alternate);
|
|
6196
6210
|
}
|
|
6211
|
+
if (n.kind === "template-literal") {
|
|
6212
|
+
return wrapIfMultiToken(lowerTemplateLiteralValue(ctx, n.parts));
|
|
6213
|
+
}
|
|
6197
6214
|
return wrapIfMultiToken(ctx.convertExpressionToGo(stringifyParsedExpr(n), undefined, n));
|
|
6198
6215
|
}
|
|
6199
6216
|
function lowerTernaryTest(ctx, test) {
|
|
6200
|
-
const go =
|
|
6217
|
+
const go = lowerValueOperand(ctx, test);
|
|
6201
6218
|
const isBoolShape = test.kind === "binary" && BOOL_COMPARISON_OPS.has(test.op) || test.kind === "unary" && test.op === "!" || test.kind === "literal" && test.literalType === "boolean";
|
|
6202
6219
|
return isBoolShape ? go : `(bf_truthy ${go})`;
|
|
6203
6220
|
}
|
|
@@ -6258,18 +6275,16 @@ function renderLoweringNode(ctx, node) {
|
|
|
6258
6275
|
const helper = goHelperName(node.helper);
|
|
6259
6276
|
if (!helper)
|
|
6260
6277
|
return null;
|
|
6261
|
-
const lowerExpr = (n) => ctx.convertExpressionToGo(stringifyParsedExpr(n), undefined, n);
|
|
6262
|
-
const lowerArg = (n) => n.kind === "conditional" ? lowerTernary(ctx, n.test, n.consequent, n.alternate) : wrapIfMultiToken(lowerExpr(n));
|
|
6263
6278
|
if (node.kind === "helper-call") {
|
|
6264
|
-
const args = node.args.map((a) =>
|
|
6279
|
+
const args = node.args.map((a) => lowerValueOperand(ctx, a));
|
|
6265
6280
|
return [helper, ...args].join(" ");
|
|
6266
6281
|
}
|
|
6267
|
-
const parts = [
|
|
6282
|
+
const parts = [lowerValueOperand(ctx, node.base)];
|
|
6268
6283
|
for (const t of node.triples) {
|
|
6269
6284
|
const includeGo = t.guard === null ? "true" : lowerUrlGuard(ctx, t.guard);
|
|
6270
6285
|
parts.push(`(${includeGo})`);
|
|
6271
6286
|
parts.push(JSON.stringify(t.key));
|
|
6272
|
-
parts.push(
|
|
6287
|
+
parts.push(lowerValueOperand(ctx, t.value));
|
|
6273
6288
|
}
|
|
6274
6289
|
return `${helper} ${parts.join(" ")}`;
|
|
6275
6290
|
}
|
|
@@ -10640,11 +10655,17 @@ ${goFields.join(`
|
|
|
10640
10655
|
return `${obj}.${goFieldNameForKey(property)}`;
|
|
10641
10656
|
}
|
|
10642
10657
|
indexAccess(object, index, emit) {
|
|
10643
|
-
return `bf_get ${wrapIfMultiToken(
|
|
10658
|
+
return `bf_get ${wrapIfMultiToken(this.emitOperand(object, emit))} ${wrapIfMultiToken(this.emitOperand(index, emit))}`;
|
|
10659
|
+
}
|
|
10660
|
+
emitOperand(n, emit) {
|
|
10661
|
+
if (n.kind === "template-literal") {
|
|
10662
|
+
return wrapIfMultiToken(lowerTemplateLiteralValue(this.emitCtx, n.parts));
|
|
10663
|
+
}
|
|
10664
|
+
return emit(n);
|
|
10644
10665
|
}
|
|
10645
10666
|
binary(op, left, right, emit) {
|
|
10646
|
-
const l =
|
|
10647
|
-
const r =
|
|
10667
|
+
const l = this.emitOperand(left, emit);
|
|
10668
|
+
const r = this.emitOperand(right, emit);
|
|
10648
10669
|
const wl = wrapIfMultiToken(l);
|
|
10649
10670
|
const wr = wrapIfMultiToken(r);
|
|
10650
10671
|
switch (op) {
|
|
@@ -10690,7 +10711,7 @@ ${goFields.join(`
|
|
|
10690
10711
|
return this.state.stringValueNames.has(name);
|
|
10691
10712
|
}
|
|
10692
10713
|
unary(op, argument, emit) {
|
|
10693
|
-
const arg =
|
|
10714
|
+
const arg = this.emitOperand(argument, emit);
|
|
10694
10715
|
if (op === "!")
|
|
10695
10716
|
return `not ${wrapIfMultiToken(arg)}`;
|
|
10696
10717
|
if (op === "-")
|
|
@@ -10698,8 +10719,8 @@ ${goFields.join(`
|
|
|
10698
10719
|
return arg;
|
|
10699
10720
|
}
|
|
10700
10721
|
logical(op, left, right, emit) {
|
|
10701
|
-
const wrapLeft = wrapIfMultiToken(
|
|
10702
|
-
const wrapRight = wrapIfMultiToken(
|
|
10722
|
+
const wrapLeft = wrapIfMultiToken(this.emitOperand(left, emit));
|
|
10723
|
+
const wrapRight = wrapIfMultiToken(this.emitOperand(right, emit));
|
|
10703
10724
|
if (op === "&&")
|
|
10704
10725
|
return `and ${wrapLeft} ${wrapRight}`;
|
|
10705
10726
|
if (op === "??" && this.nillablePropNameOf(left) !== null) {
|
|
@@ -10864,7 +10885,8 @@ ${goFields.join(`
|
|
|
10864
10885
|
}
|
|
10865
10886
|
return this.pushCallbackBF101(method, true);
|
|
10866
10887
|
}
|
|
10867
|
-
arrayMethod(method, object, args,
|
|
10888
|
+
arrayMethod(method, object, args, rawEmit) {
|
|
10889
|
+
const emit = (e) => this.emitOperand(e, rawEmit);
|
|
10868
10890
|
switch (method) {
|
|
10869
10891
|
case "join": {
|
|
10870
10892
|
const obj = emit(object);
|
|
@@ -11454,11 +11476,9 @@ ${goFields.join(`
|
|
|
11454
11476
|
}
|
|
11455
11477
|
continue;
|
|
11456
11478
|
}
|
|
11457
|
-
|
|
11458
|
-
|
|
11459
|
-
|
|
11460
|
-
}
|
|
11461
|
-
if (!singlePartTemplateLiteral && this.isTemplateFragment(go, exprOut.parsed?.kind)) {
|
|
11479
|
+
if (exprOut.parsed?.kind === "template-literal" || exprOut.parsed?.kind === "conditional") {
|
|
11480
|
+
go = lowerValueOperand(this.emitCtx, exprOut.parsed);
|
|
11481
|
+
} else if (this.isTemplateFragment(go, exprOut.parsed?.kind)) {
|
|
11462
11482
|
this.state.errors.push({
|
|
11463
11483
|
code: "BF101",
|
|
11464
11484
|
severity: "error",
|
|
@@ -11761,7 +11781,7 @@ ${goFields.join(`
|
|
|
11761
11781
|
return plain(lowerTernary(this.emitCtx, expr.test, expr.consequent, expr.alternate));
|
|
11762
11782
|
}
|
|
11763
11783
|
case "template-literal":
|
|
11764
|
-
return plain(this.
|
|
11784
|
+
return plain(lowerTemplateLiteralValue(this.emitCtx, expr.parts));
|
|
11765
11785
|
case "arrow":
|
|
11766
11786
|
return plain("[ARROW-FN]");
|
|
11767
11787
|
case "regex":
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@barefootjs/go-template",
|
|
3
|
-
"version": "0.35.
|
|
3
|
+
"version": "0.35.1",
|
|
4
4
|
"description": "Go html/template adapter for BarefootJS - generates Go template files from IR",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -49,7 +49,7 @@
|
|
|
49
49
|
"directory": "packages/adapter-go-template"
|
|
50
50
|
},
|
|
51
51
|
"dependencies": {
|
|
52
|
-
"@barefootjs/shared": "0.35.
|
|
52
|
+
"@barefootjs/shared": "0.35.1"
|
|
53
53
|
},
|
|
54
54
|
"peerDependencies": {
|
|
55
55
|
"@barefootjs/jsx": ">=0.2.0",
|
|
@@ -67,9 +67,9 @@
|
|
|
67
67
|
},
|
|
68
68
|
"devDependencies": {
|
|
69
69
|
"@barefootjs/adapter-tests": "0.1.0",
|
|
70
|
-
"@barefootjs/client": "0.35.
|
|
71
|
-
"@barefootjs/jsx": "0.35.
|
|
72
|
-
"@barefootjs/vite": "0.35.
|
|
70
|
+
"@barefootjs/client": "0.35.1",
|
|
71
|
+
"@barefootjs/jsx": "0.35.1",
|
|
72
|
+
"@barefootjs/vite": "0.35.1",
|
|
73
73
|
"vite": "^6.0.0"
|
|
74
74
|
}
|
|
75
75
|
}
|
|
@@ -376,6 +376,161 @@ describe('GoTemplateAdapter - bf_ternary value-position lowering (#2335)', () =>
|
|
|
376
376
|
test('emits no {{if}} action fragment for a value-position ternary', () => {
|
|
377
377
|
expect(render("flag ? 'a' : 'b'")).not.toContain('{{if')
|
|
378
378
|
})
|
|
379
|
+
|
|
380
|
+
// #2863: a multi-part template literal (mixed literal text and MULTIPLE
|
|
381
|
+
// interpolations) as a ternary BRANCH used to lower through the TEXT-
|
|
382
|
+
// position `templateLiteral()` path (`{{.Start}}-{{.End}}`), nesting raw
|
|
383
|
+
// `{{`/`}}` delimiters inside `bf_ternary`'s own argument list — a
|
|
384
|
+
// `html/template` parse error ("unexpected \"{\" in operand") at Go
|
|
385
|
+
// application startup, even though `bf build` itself succeeded. It must
|
|
386
|
+
// instead fold to one pipeline value, chained through the same
|
|
387
|
+
// `bf_concat_str` runtime helper JS string-concat `+` already uses (#2168).
|
|
388
|
+
test('a multi-part template-literal branch folds to a bf_concat_str chain, not a raw fragment (#2863)', () => {
|
|
389
|
+
expect(render('end ? `${start}-${end}` : start')).toBe(
|
|
390
|
+
'{{(bf_ternary (bf_truthy .End) (bf_concat_str (bf_concat_str .Start "-") .End) .Start)}}',
|
|
391
|
+
)
|
|
392
|
+
})
|
|
393
|
+
|
|
394
|
+
test('a template-literal branch never leaks a raw {{ or }} into the surrounding action (#2863)', () => {
|
|
395
|
+
const out = render('end ? `${start}-${end}` : start')
|
|
396
|
+
const inner = out.slice(2, -2) // strip the outer {{ }} the whole expression is wrapped in
|
|
397
|
+
expect(inner).not.toContain('{{')
|
|
398
|
+
expect(inner).not.toContain('}}')
|
|
399
|
+
})
|
|
400
|
+
|
|
401
|
+
test('a single-part template-literal branch reduces to the bare value, no bf_concat_str wrap (#2863)', () => {
|
|
402
|
+
expect(render('end ? `${start}` : end')).toBe('{{(bf_ternary (bf_truthy .End) .Start .End)}}')
|
|
403
|
+
})
|
|
404
|
+
|
|
405
|
+
test('static text in a template-literal branch is an escaped Go string literal (#2863)', () => {
|
|
406
|
+
expect(render('end ? `say "hi" ${start}` : end')).toBe(
|
|
407
|
+
'{{(bf_ternary (bf_truthy .End) (bf_concat_str "say \\"hi\\" " .Start) .End)}}',
|
|
408
|
+
)
|
|
409
|
+
})
|
|
410
|
+
|
|
411
|
+
test('a nested ternary inside a template-literal branch still lowers to bf_ternary, not {{if}} (#2863)', () => {
|
|
412
|
+
const out = render('end ? `x ${flag ? \'a\' : \'b\'}` : end')
|
|
413
|
+
expect(out).toBe(
|
|
414
|
+
'{{(bf_ternary (bf_truthy .End) (bf_concat_str "x " (bf_ternary (bf_truthy .Flag) "a" "b")) .End)}}',
|
|
415
|
+
)
|
|
416
|
+
expect(out).not.toContain('{{if')
|
|
417
|
+
})
|
|
418
|
+
})
|
|
419
|
+
|
|
420
|
+
// #2863 follow-up: the same value-position leak existed for a `+`/`||`
|
|
421
|
+
// operand and a registered-lowering (`queryHref`) argument, not just a
|
|
422
|
+
// `bf_ternary` branch — every such position funnels through the Go
|
|
423
|
+
// adapter's shared `lowerValueOperand` door (`expr/url-builder.ts`).
|
|
424
|
+
describe('GoTemplateAdapter - value-position template literal in other operand positions (#2863)', () => {
|
|
425
|
+
const adapter = new GoTemplateAdapter()
|
|
426
|
+
const render = (expr: string) => adapter.renderExpression({ expr } as IRExpression)
|
|
427
|
+
|
|
428
|
+
test('a template-literal operand of `+` folds through bf_concat_str, not a raw fragment', () => {
|
|
429
|
+
const out = render('`${a}-${b}` + a')
|
|
430
|
+
const inner = out.slice(2, -2)
|
|
431
|
+
expect(inner).not.toContain('{{')
|
|
432
|
+
expect(inner).toContain('bf_concat_str')
|
|
433
|
+
})
|
|
434
|
+
|
|
435
|
+
test('a template-literal operand of `||` folds through bf_concat_str, not a raw fragment', () => {
|
|
436
|
+
const out = render('a || `${a}-${b}`')
|
|
437
|
+
const inner = out.slice(2, -2)
|
|
438
|
+
expect(inner).not.toContain('{{')
|
|
439
|
+
})
|
|
440
|
+
|
|
441
|
+
// The issue's exact repro shape: a local, expression-bodied helper arrow
|
|
442
|
+
// (`eventTime`) whose body is the ternary+template-literal, called from a
|
|
443
|
+
// `.map()` row. A ternary written DIRECTLY inline in JSX instead compiles
|
|
444
|
+
// to a text-position `IRConditional` (`{{if}}…{{else}}…{{end}}`, correct
|
|
445
|
+
// and unaffected by this bug) — the buggy VALUE-position path is only
|
|
446
|
+
// reached once `inlineLocalHelperCall` substitutes the helper's body into
|
|
447
|
+
// the `eventTime(event)` call site and the Go adapter re-parses the
|
|
448
|
+
// result as a plain expression, not a JSX conditional.
|
|
449
|
+
test('the issue #2863 repro: a local-helper ternary+template-literal inlined in a .map() row lowers to valid Go source', () => {
|
|
450
|
+
const { template } = compileAndGenerate(`
|
|
451
|
+
'use client'
|
|
452
|
+
import { createSignal } from '@barefootjs/client'
|
|
453
|
+
type Event = { id: string; time: string; endTime: string }
|
|
454
|
+
type Dashboard = { events: Event[] }
|
|
455
|
+
const empty: Dashboard = { events: [] }
|
|
456
|
+
export function Schedule() {
|
|
457
|
+
const [data] = createSignal<Dashboard>(empty)
|
|
458
|
+
const eventTime = (event: Event) =>
|
|
459
|
+
event.endTime ? \`\${event.time}–\${event.endTime}\` : event.time
|
|
460
|
+
return (
|
|
461
|
+
<ol>
|
|
462
|
+
{data().events.map((event) => (
|
|
463
|
+
<li key={event.id}><time>{eventTime(event)}</time></li>
|
|
464
|
+
))}
|
|
465
|
+
</ol>
|
|
466
|
+
)
|
|
467
|
+
}
|
|
468
|
+
`)
|
|
469
|
+
expect(template).toContain(
|
|
470
|
+
'(bf_ternary (bf_truthy .EndTime) (bf_concat_str (bf_concat_str .Time "–") .EndTime) .Time)',
|
|
471
|
+
)
|
|
472
|
+
expect(template).not.toContain('{{.Time}}–{{.EndTime}}')
|
|
473
|
+
})
|
|
474
|
+
})
|
|
475
|
+
|
|
476
|
+
// #2863 (pullfrog review on #2877): three more argument/operand positions
|
|
477
|
+
// that reach a template literal without going through `lowerValueOperand`/
|
|
478
|
+
// `emitOperand` — verified to reproduce the identical `unexpected "{" in
|
|
479
|
+
// operand` class of bug before this fix, via a standalone script driving
|
|
480
|
+
// `html/template.Parse` on the pre-fix output.
|
|
481
|
+
describe('GoTemplateAdapter - value-position template literal in condition/array-method/index positions (#2863 follow-up)', () => {
|
|
482
|
+
// `renderConditionExpr`'s own `binary`/`unary`/`logical` recursion is a
|
|
483
|
+
// SEPARATE walker from the generic `emit()` dispatcher (it threads a
|
|
484
|
+
// `preamble` string `emitOperand` doesn't track) — reached whenever a
|
|
485
|
+
// ternary/`&&`/`||`/`!` TEST is a comparison against (or otherwise
|
|
486
|
+
// contains) a template literal, since `lowerTernaryTest`/`lowerUrlGuard`'s
|
|
487
|
+
// "bool-shape" branch routes through `convertConditionToGo` instead of
|
|
488
|
+
// `lowerValueOperand`.
|
|
489
|
+
test('a ternary TEST comparing to a template literal folds through bf_concat_str inside the {{if}}', () => {
|
|
490
|
+
const { template } = compileAndGenerate(`
|
|
491
|
+
'use client'
|
|
492
|
+
import { createSignal } from '@barefootjs/client'
|
|
493
|
+
export function T() {
|
|
494
|
+
const [x] = createSignal('x')
|
|
495
|
+
const [y] = createSignal('y')
|
|
496
|
+
const [z] = createSignal('z')
|
|
497
|
+
return <span>{(x() === \`\${y()}-\${z()}\`) ? 'a' : 'b'}</span>
|
|
498
|
+
}
|
|
499
|
+
`)
|
|
500
|
+
expect(template).toContain('{{if eq .X (bf_concat_str (bf_concat_str .Y "-") .Z)}}')
|
|
501
|
+
})
|
|
502
|
+
|
|
503
|
+
// `arrayMethod()`'s per-method cases (`join`, `includes`, `indexOf`, …) all
|
|
504
|
+
// lower their receiver/args via the dispatcher's plain `emit`, not
|
|
505
|
+
// `emitOperand` — no ternary or helper-inlining needed to reach the bug.
|
|
506
|
+
test('Array.join with a template-literal separator folds through bf_concat_str', () => {
|
|
507
|
+
const { template } = compileAndGenerate(`
|
|
508
|
+
'use client'
|
|
509
|
+
import { createSignal } from '@barefootjs/client'
|
|
510
|
+
export function T() {
|
|
511
|
+
const [items] = createSignal<string[]>([])
|
|
512
|
+
const [y] = createSignal('y')
|
|
513
|
+
const [z] = createSignal('z')
|
|
514
|
+
return <span>{items().join(\`\${y()}-\${z()}\`)}</span>
|
|
515
|
+
}
|
|
516
|
+
`)
|
|
517
|
+
expect(template).toContain('bf_join (.Items) (bf_concat_str (bf_concat_str .Y "-") .Z)')
|
|
518
|
+
})
|
|
519
|
+
|
|
520
|
+
// `indexAccess()` lowers both the object and the index via plain `emit`.
|
|
521
|
+
test('a computed index-access with a template-literal key folds through bf_concat_str', () => {
|
|
522
|
+
const { template } = compileAndGenerate(`
|
|
523
|
+
'use client'
|
|
524
|
+
import { createSignal } from '@barefootjs/client'
|
|
525
|
+
export function T() {
|
|
526
|
+
const [obj] = createSignal<Record<string, string>>({})
|
|
527
|
+
const [y] = createSignal('y')
|
|
528
|
+
const [z] = createSignal('z')
|
|
529
|
+
return <span>{obj()[\`\${y()}-\${z()}\`]}</span>
|
|
530
|
+
}
|
|
531
|
+
`)
|
|
532
|
+
expect(template).toContain('bf_get .Obj (bf_concat_str (bf_concat_str .Y "-") .Z)')
|
|
533
|
+
})
|
|
379
534
|
})
|
|
380
535
|
|
|
381
536
|
// #2335 item 2 (correctness): a ternary used as a boolean CONDITION used to
|
|
@@ -4701,11 +4856,14 @@ export function CompositeRowChildComponent(props: { items: Item[] }) {
|
|
|
4701
4856
|
})
|
|
4702
4857
|
|
|
4703
4858
|
// A multi-part template literal (mixed literal text and interpolation,
|
|
4704
|
-
// `` `#${row.id} ${row.label}` ``)
|
|
4705
|
-
//
|
|
4706
|
-
//
|
|
4707
|
-
//
|
|
4708
|
-
|
|
4859
|
+
// `` `#${row.id} ${row.label}` ``) folds through the shared value-operand
|
|
4860
|
+
// door (#2863) — a left-folded `bf_concat_str` chain — instead of being
|
|
4861
|
+
// refused (BF101) the way it used to be here (this call site had no
|
|
4862
|
+
// reduction for the multi-part case before #2863, only a single-part
|
|
4863
|
+
// unwrap). It must still NOT silently emit the stale constructor-only
|
|
4864
|
+
// value (the #2445 bug this whole fix exists to close) — it now emits the
|
|
4865
|
+
// correct live per-row value instead.
|
|
4866
|
+
test('a multi-part template-literal per-row prop folds through bf_concat_str, not silently stale (#2445, #2863)', () => {
|
|
4709
4867
|
const result = compileJSX(`
|
|
4710
4868
|
'use client'
|
|
4711
4869
|
import { createSignal } from '@barefootjs/client'
|
|
@@ -4726,9 +4884,11 @@ export function CompositeRowChildComponent(props: { items: Item[] }) {
|
|
|
4726
4884
|
)
|
|
4727
4885
|
}
|
|
4728
4886
|
`.trimStart(), 'test.tsx', { adapter: new GoTemplateAdapter(), outputIR: false })
|
|
4729
|
-
expect(
|
|
4887
|
+
expect(result.errors ?? []).toEqual([])
|
|
4730
4888
|
const template = result.files.find(f => f.type === 'markedTemplate')!.content
|
|
4731
|
-
expect(template).
|
|
4889
|
+
expect(template).toContain(
|
|
4890
|
+
'{{template "Badge" (bf_with_props $.BadgeSlot0 "Text" (bf_concat_str (bf_concat_str (bf_concat_str "#" .ID) " ") .Label))}}',
|
|
4891
|
+
)
|
|
4732
4892
|
})
|
|
4733
4893
|
|
|
4734
4894
|
// A prop whose expression `convertExpressionToGo` itself refuses (an
|
|
@@ -10,13 +10,14 @@
|
|
|
10
10
|
import {
|
|
11
11
|
type LoweringNode,
|
|
12
12
|
type ParsedExpr,
|
|
13
|
+
type TemplatePart,
|
|
13
14
|
parseExpression,
|
|
14
15
|
stringifyParsedExpr,
|
|
15
16
|
isValidHelperId,
|
|
16
17
|
} from '@barefootjs/jsx'
|
|
17
18
|
|
|
18
19
|
import type { GoEmitContext } from '../emit-context.ts'
|
|
19
|
-
import { wrapIfMultiToken } from '../lib/go-emit.ts'
|
|
20
|
+
import { escapeGoString, wrapIfMultiToken } from '../lib/go-emit.ts'
|
|
20
21
|
|
|
21
22
|
/**
|
|
22
23
|
* Logical helper id → Go template helper name. `bf_<helper>` is a formula,
|
|
@@ -61,7 +62,7 @@ function lowerUrlGuard(ctx: GoEmitContext, g: ParsedExpr): string {
|
|
|
61
62
|
if (isBoolShape) {
|
|
62
63
|
return ctx.convertConditionToGo(stringifyParsedExpr(g), g).condition
|
|
63
64
|
}
|
|
64
|
-
const valueGo =
|
|
65
|
+
const valueGo = lowerValueOperand(ctx, g)
|
|
65
66
|
return `ne ${valueGo} ""`
|
|
66
67
|
}
|
|
67
68
|
|
|
@@ -91,21 +92,63 @@ export function lowerTernary(
|
|
|
91
92
|
alternate: ParsedExpr,
|
|
92
93
|
): string {
|
|
93
94
|
const t = lowerTernaryTest(ctx, test)
|
|
94
|
-
return `(bf_ternary ${t} ${
|
|
95
|
+
return `(bf_ternary ${t} ${lowerValueOperand(ctx, consequent)} ${lowerValueOperand(ctx, alternate)})`
|
|
95
96
|
}
|
|
96
97
|
|
|
97
98
|
/**
|
|
98
|
-
*
|
|
99
|
-
* `
|
|
100
|
-
*
|
|
101
|
-
*
|
|
102
|
-
*
|
|
103
|
-
*
|
|
99
|
+
* Lower a template literal to ONE Go pipeline VALUE (#2863): a left fold of
|
|
100
|
+
* `bf_concat_str` over the parts — static text as an escaped Go string
|
|
101
|
+
* literal, each interpolation as its own value operand. The TEXT-position
|
|
102
|
+
* form (`GoTemplateAdapter.templateLiteral()`: literal text interleaved with
|
|
103
|
+
* `{{…}}` actions) is only legal where the result is spliced directly into
|
|
104
|
+
* markup; inside another action's argument list (a `bf_ternary` branch, a
|
|
105
|
+
* helper-call arg, a `bf_query` guard value, …) Go rejects nested `{{`/`}}`
|
|
106
|
+
* delimiters ("unexpected \"{\" in operand"). This is the Go twin of the
|
|
107
|
+
* concatenation every other DSL adapter already emits for the same nested
|
|
108
|
+
* shape (e.g. Jinja/Twig `bf.string(a) ~ '–' ~ bf.string(b)`) — Go was the
|
|
109
|
+
* lone outlier because `text/template` has no expression-level string
|
|
110
|
+
* concatenation operator of its own, only this runtime helper (already used
|
|
111
|
+
* by `binary()`'s string-typed `+`, #2168).
|
|
112
|
+
*
|
|
113
|
+
* A single-part literal (`` `${x}` `` alone) reduces to that one value with
|
|
114
|
+
* no `bf_concat_str` wrapper — mirrors the sibling single-part unwrap at the
|
|
115
|
+
* child-prop call site. An all-static literal (already reduced to a plain
|
|
116
|
+
* `literal` ParsedExpr by the parser in practice, but handled here too)
|
|
117
|
+
* folds to one escaped string.
|
|
104
118
|
*/
|
|
105
|
-
function
|
|
119
|
+
export function lowerTemplateLiteralValue(ctx: GoEmitContext, parts: readonly TemplatePart[]): string {
|
|
120
|
+
const terms: string[] = []
|
|
121
|
+
for (const part of parts) {
|
|
122
|
+
if (part.type === 'string') {
|
|
123
|
+
if (part.value !== '') terms.push(`"${escapeGoString(part.value)}"`)
|
|
124
|
+
} else {
|
|
125
|
+
terms.push(lowerValueOperand(ctx, part.expr))
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
if (terms.length === 0) return '""'
|
|
129
|
+
return terms.slice(1).reduce((acc, t) => `(bf_concat_str ${acc} ${t})`, terms[0])
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* The single door for "this ParsedExpr sits in Go pipeline ARGUMENT
|
|
134
|
+
* position" — a `bf_ternary` branch, a ternary/guard test's value form, a
|
|
135
|
+
* helper-call arg, a `bf_query` base/guard/value. A nested ternary recurses
|
|
136
|
+
* to another `bf_ternary`; a template literal folds through
|
|
137
|
+
* `lowerTemplateLiteralValue` (#2863) rather than the generic expression
|
|
138
|
+
* path, which would otherwise return the TEXT-position mixed
|
|
139
|
+
* literal-text-plus-`{{…}}`-actions form; anything else lowers to its Go
|
|
140
|
+
* value (parenthesised when multi-token so it stays one argument). String
|
|
141
|
+
* branches become quoted, html/template-escaped values — not the bare
|
|
142
|
+
* unquoted text the old `{{if}}` fragment path emitted — which is exactly
|
|
143
|
+
* right for the value positions this is reached for.
|
|
144
|
+
*/
|
|
145
|
+
export function lowerValueOperand(ctx: GoEmitContext, n: ParsedExpr): string {
|
|
106
146
|
if (n.kind === 'conditional') {
|
|
107
147
|
return lowerTernary(ctx, n.test, n.consequent, n.alternate)
|
|
108
148
|
}
|
|
149
|
+
if (n.kind === 'template-literal') {
|
|
150
|
+
return wrapIfMultiToken(lowerTemplateLiteralValue(ctx, n.parts))
|
|
151
|
+
}
|
|
109
152
|
return wrapIfMultiToken(ctx.convertExpressionToGo(stringifyParsedExpr(n), undefined, n))
|
|
110
153
|
}
|
|
111
154
|
|
|
@@ -120,7 +163,7 @@ function lowerTernaryOperand(ctx: GoEmitContext, n: ParsedExpr): string {
|
|
|
120
163
|
* counterpart of `lowerUrlGuard`'s string-only `ne <value> ""`.
|
|
121
164
|
*/
|
|
122
165
|
function lowerTernaryTest(ctx: GoEmitContext, test: ParsedExpr): string {
|
|
123
|
-
const go =
|
|
166
|
+
const go = lowerValueOperand(ctx, test)
|
|
124
167
|
const isBoolShape =
|
|
125
168
|
(test.kind === 'binary' && BOOL_COMPARISON_OPS.has(test.op)) ||
|
|
126
169
|
(test.kind === 'unary' && test.op === '!') ||
|
|
@@ -280,22 +323,14 @@ function ternaryHasQueryBranch(
|
|
|
280
323
|
function renderLoweringNode(ctx: GoEmitContext, node: LoweringNode): string | null {
|
|
281
324
|
const helper = goHelperName(node.helper)
|
|
282
325
|
if (!helper) return null
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
//
|
|
286
|
-
// `
|
|
287
|
-
//
|
|
288
|
-
//
|
|
289
|
-
// generic `lowerExpr`, whose `conditional()` dispatch now reaches the very
|
|
290
|
-
// same helper) keeps the arg lowering self-contained and its intent local;
|
|
291
|
-
// `lowerTernary` itself right-folds a nested chain (the #2324 union stage's
|
|
292
|
-
// locale→pattern table) into `(bf_ternary <cond> <a> (bf_ternary …))`.
|
|
293
|
-
const lowerArg = (n: ParsedExpr): string =>
|
|
294
|
-
n.kind === 'conditional'
|
|
295
|
-
? lowerTernary(ctx, n.test, n.consequent, n.alternate)
|
|
296
|
-
: wrapIfMultiToken(lowerExpr(n))
|
|
326
|
+
// Every argument here is Go pipeline ARGUMENT position — a `conditional`
|
|
327
|
+
// lowers to `(bf_ternary …)`, a `template-literal` folds through
|
|
328
|
+
// `bf_concat_str` (#2863) rather than leaking the TEXT-position
|
|
329
|
+
// `{{…}}`-actions form into this call's argument list, anything else takes
|
|
330
|
+
// the generic value path. `lowerValueOperand` is the one shared door every
|
|
331
|
+
// such position in this file now goes through.
|
|
297
332
|
if (node.kind === 'helper-call') {
|
|
298
|
-
const args = node.args.map(a =>
|
|
333
|
+
const args = node.args.map(a => lowerValueOperand(ctx, a))
|
|
299
334
|
return [helper, ...args].join(' ')
|
|
300
335
|
}
|
|
301
336
|
// guard-list — `queryHref`-shaped. Inclusion mirrors the client exactly, where
|
|
@@ -304,12 +339,12 @@ function renderLoweringNode(ctx: GoEmitContext, node: LoweringNode): string | nu
|
|
|
304
339
|
// - plain `key: v` (guard null) → `(true) "key" v`
|
|
305
340
|
// - conditional `key: cond ? a : <omit>` → `(<cond>) "key" a`, where the
|
|
306
341
|
// non-empty check is done by bf_query, not folded into the guard.
|
|
307
|
-
const parts: string[] = [
|
|
342
|
+
const parts: string[] = [lowerValueOperand(ctx, node.base)]
|
|
308
343
|
for (const t of node.triples) {
|
|
309
344
|
const includeGo = t.guard === null ? 'true' : lowerUrlGuard(ctx, t.guard)
|
|
310
345
|
parts.push(`(${includeGo})`)
|
|
311
346
|
parts.push(JSON.stringify(t.key))
|
|
312
|
-
parts.push(
|
|
347
|
+
parts.push(lowerValueOperand(ctx, t.value))
|
|
313
348
|
}
|
|
314
349
|
return `${helper} ${parts.join(' ')}`
|
|
315
350
|
}
|