@barefootjs/go-template 0.6.1 → 0.7.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/adapter/go-template-adapter.d.ts +42 -0
- package/dist/adapter/go-template-adapter.d.ts.map +1 -1
- package/dist/adapter/index.js +74 -1
- package/dist/build.js +74 -1
- package/dist/index.js +74 -1
- package/package.json +2 -2
- package/src/__tests__/go-template-adapter.test.ts +33 -10
- package/src/__tests__/slot-dynamic-tag.test.ts +79 -0
- package/src/adapter/go-template-adapter.ts +159 -1
- package/src/test-render.ts +33 -1
|
@@ -109,6 +109,20 @@ export declare class GoTemplateAdapter extends BaseAdapter implements ParsedExpr
|
|
|
109
109
|
/** Set during type generation when any emit references
|
|
110
110
|
* `template.HTML(...)`; toggles the `"html/template"` import. */
|
|
111
111
|
private usesHtmlTemplate;
|
|
112
|
+
/**
|
|
113
|
+
* Module-scope pure string-literal constants (`const X = 'literal'` at
|
|
114
|
+
* file top-level), keyed by name → resolved literal value. Populated at
|
|
115
|
+
* `generate()` entry from `ir.metadata.localConstants`. When an identifier
|
|
116
|
+
* in an expression resolves to one of these, the adapter inlines the
|
|
117
|
+
* literal value instead of emitting a struct-field reference
|
|
118
|
+
* (`{{.X}}`) — the field never exists on the Props struct, so without
|
|
119
|
+
* inlining Go's template engine fails with `can't evaluate field X`.
|
|
120
|
+
* Hono inlines it for free (it evaluates real JS); this restores parity.
|
|
121
|
+
* Only module-scope, pure string literals qualify — function-scope
|
|
122
|
+
* locals legitimately become template vars/props, and `Record<T,string>`
|
|
123
|
+
* indexed lookups / memos / signals are deliberately excluded.
|
|
124
|
+
*/
|
|
125
|
+
private moduleStringConsts;
|
|
112
126
|
constructor(options?: GoTemplateAdapterOptions);
|
|
113
127
|
/**
|
|
114
128
|
* Generate template output for a component.
|
|
@@ -554,6 +568,34 @@ export declare class GoTemplateAdapter extends BaseAdapter implements ParsedExpr
|
|
|
554
568
|
* rebinds. Outside any loop the root *is* the dot, so we emit `.Field` (#1677).
|
|
555
569
|
*/
|
|
556
570
|
private rootFieldRef;
|
|
571
|
+
/**
|
|
572
|
+
* Build the module pure-string-const map from the IR's localConstants.
|
|
573
|
+
* A const qualifies only when it is module-scope (`isModule`) and its
|
|
574
|
+
* initializer parses to a single string literal (`ts.StringLiteral` or
|
|
575
|
+
* `ts.NoSubstitutionTemplateLiteral` — a backtick string with no `${}`).
|
|
576
|
+
* Template literals *with* interpolations, numeric/object initializers,
|
|
577
|
+
* `Record<T,string>` maps, memos, and signals are all excluded: only a
|
|
578
|
+
* pure compile-time string can be safely inlined byte-for-byte.
|
|
579
|
+
*/
|
|
580
|
+
private collectModuleStringConsts;
|
|
581
|
+
/**
|
|
582
|
+
* Parse a const initializer's source text. Returns the unescaped string
|
|
583
|
+
* value when the whole initializer is a single string literal (or a
|
|
584
|
+
* no-substitution template literal), else `null`. Uses the TS parser so
|
|
585
|
+
* escapes/quotes are resolved exactly as JS would, matching the value
|
|
586
|
+
* the Hono reference inlines at runtime.
|
|
587
|
+
*/
|
|
588
|
+
private parsePureStringLiteral;
|
|
589
|
+
/**
|
|
590
|
+
* Resolve an identifier to its inlined Go string literal when it names a
|
|
591
|
+
* module pure-string const. Returns the Go template literal form
|
|
592
|
+
* (`"<escaped>"`) so callers can drop it straight into a `{{...}}` action,
|
|
593
|
+
* or `null` when the name is not such a const (the caller then falls back
|
|
594
|
+
* to its normal field-ref lowering). The value is escaped for a Go
|
|
595
|
+
* double-quoted string literal — Go's `html/template` then applies the
|
|
596
|
+
* same contextual auto-escaping it applies to any literal, matching Hono.
|
|
597
|
+
*/
|
|
598
|
+
private resolveModuleStringConst;
|
|
557
599
|
literal(value: string | number | boolean | null, literalType: LiteralType): string;
|
|
558
600
|
call(callee: ParsedExpr, args: ParsedExpr[], emit: (e: ParsedExpr) => string): string;
|
|
559
601
|
member(object: ParsedExpr, property: string, _computed: boolean, emit: (e: ParsedExpr) => string): string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"go-template-adapter.d.ts","sourceRoot":"","sources":["../../src/adapter/go-template-adapter.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAIH,OAAO,KAAK,EACV,WAAW,EACX,MAAM,EACN,SAAS,EACT,MAAM,EACN,YAAY,EACZ,aAAa,EACb,MAAM,EAEN,WAAW,EACX,UAAU,EACV,MAAM,EAON,UAAU,EAEV,cAAc,EACd,QAAQ,EACR,SAAS,EACT,SAAS,EACT,YAAY,EACZ,aAAa,EACb,UAAU,EACV,OAAO,
|
|
1
|
+
{"version":3,"file":"go-template-adapter.d.ts","sourceRoot":"","sources":["../../src/adapter/go-template-adapter.ts"],"names":[],"mappings":"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"}
|
package/dist/adapter/index.js
CHANGED
|
@@ -117,6 +117,7 @@ class GoTemplateAdapter extends BaseAdapter {
|
|
|
117
117
|
localStructFields = new Map;
|
|
118
118
|
synthStructTypes = new Map;
|
|
119
119
|
usesHtmlTemplate = false;
|
|
120
|
+
moduleStringConsts = new Map;
|
|
120
121
|
constructor(options = {}) {
|
|
121
122
|
super();
|
|
122
123
|
this.options = {
|
|
@@ -131,6 +132,7 @@ class GoTemplateAdapter extends BaseAdapter {
|
|
|
131
132
|
this.templateVarCounter = 0;
|
|
132
133
|
this.propsObjectName = ir.metadata.propsObjectName;
|
|
133
134
|
this.restPropsName = ir.metadata.restPropsName ?? null;
|
|
135
|
+
this.moduleStringConsts = this.collectModuleStringConsts(ir.metadata.localConstants);
|
|
134
136
|
if (!options?.siblingTemplatesRegistered) {
|
|
135
137
|
this.checkImportedLoopChildComponents(ir);
|
|
136
138
|
}
|
|
@@ -912,6 +914,12 @@ ${goFields.join(`
|
|
|
912
914
|
collectStaticChildInstancesRecursive(node, result, inLoop) {
|
|
913
915
|
if (node.type === "component") {
|
|
914
916
|
const comp = node;
|
|
917
|
+
if (comp.dynamicTag) {
|
|
918
|
+
for (const child of comp.children) {
|
|
919
|
+
this.collectStaticChildInstancesRecursive(child, result, inLoop);
|
|
920
|
+
}
|
|
921
|
+
return;
|
|
922
|
+
}
|
|
915
923
|
if (comp.name !== "Portal" && !inLoop && comp.slotId) {
|
|
916
924
|
const suffix = slotIdToFieldSuffix(comp.slotId);
|
|
917
925
|
result.push({
|
|
@@ -1748,6 +1756,9 @@ ${goFields.join(`
|
|
|
1748
1756
|
return emitParsedExpr(expr, this);
|
|
1749
1757
|
}
|
|
1750
1758
|
identifier(name) {
|
|
1759
|
+
const inlined = this.resolveModuleStringConst(name);
|
|
1760
|
+
if (inlined !== null)
|
|
1761
|
+
return inlined;
|
|
1751
1762
|
const currentLoopParam = this.loopParamStack[this.loopParamStack.length - 1];
|
|
1752
1763
|
if (currentLoopParam && name === currentLoopParam)
|
|
1753
1764
|
return ".";
|
|
@@ -1769,6 +1780,48 @@ ${goFields.join(`
|
|
|
1769
1780
|
const prefix = this.loopParamStack.length > 0 ? "$." : ".";
|
|
1770
1781
|
return `${prefix}${this.capitalizeFieldName(name)}`;
|
|
1771
1782
|
}
|
|
1783
|
+
collectModuleStringConsts(constants) {
|
|
1784
|
+
const map = new Map;
|
|
1785
|
+
for (const c of constants ?? []) {
|
|
1786
|
+
if (!c.isModule)
|
|
1787
|
+
continue;
|
|
1788
|
+
if (c.value === undefined)
|
|
1789
|
+
continue;
|
|
1790
|
+
const literal = this.parsePureStringLiteral(c.value);
|
|
1791
|
+
if (literal !== null)
|
|
1792
|
+
map.set(c.name, literal);
|
|
1793
|
+
}
|
|
1794
|
+
return map;
|
|
1795
|
+
}
|
|
1796
|
+
parsePureStringLiteral(source) {
|
|
1797
|
+
const sf = ts.createSourceFile("__const.ts", `const __x = (${source});`, ts.ScriptTarget.Latest, false);
|
|
1798
|
+
const stmt = sf.statements[0];
|
|
1799
|
+
if (!stmt || !ts.isVariableStatement(stmt))
|
|
1800
|
+
return null;
|
|
1801
|
+
const decl = stmt.declarationList.declarations[0];
|
|
1802
|
+
let init = decl?.initializer;
|
|
1803
|
+
while (init && ts.isParenthesizedExpression(init))
|
|
1804
|
+
init = init.expression;
|
|
1805
|
+
if (!init)
|
|
1806
|
+
return null;
|
|
1807
|
+
if (ts.isStringLiteral(init) || ts.isNoSubstitutionTemplateLiteral(init)) {
|
|
1808
|
+
return init.text;
|
|
1809
|
+
}
|
|
1810
|
+
return null;
|
|
1811
|
+
}
|
|
1812
|
+
resolveModuleStringConst(name) {
|
|
1813
|
+
if (this.loopParamStack.length > 0 && this.loopParamStack[this.loopParamStack.length - 1] === name) {
|
|
1814
|
+
return null;
|
|
1815
|
+
}
|
|
1816
|
+
if (this.loopVarRefCount.has(name))
|
|
1817
|
+
return null;
|
|
1818
|
+
if (this.isOuterLoopParam(name))
|
|
1819
|
+
return null;
|
|
1820
|
+
const value = this.moduleStringConsts.get(name);
|
|
1821
|
+
if (value === undefined)
|
|
1822
|
+
return null;
|
|
1823
|
+
return `"${this.escapeGoString(value)}"`;
|
|
1824
|
+
}
|
|
1772
1825
|
literal(value, literalType) {
|
|
1773
1826
|
if (literalType === "string")
|
|
1774
1827
|
return `"${value}"`;
|
|
@@ -2609,6 +2662,9 @@ ${goFields.join(`
|
|
|
2609
2662
|
switch (expr.kind) {
|
|
2610
2663
|
case "identifier":
|
|
2611
2664
|
{
|
|
2665
|
+
const inlined = this.resolveModuleStringConst(expr.name);
|
|
2666
|
+
if (inlined !== null)
|
|
2667
|
+
return plain(inlined);
|
|
2612
2668
|
const currentLoopParam = this.loopParamStack[this.loopParamStack.length - 1];
|
|
2613
2669
|
if (currentLoopParam && expr.name === currentLoopParam) {
|
|
2614
2670
|
return plain(".");
|
|
@@ -2631,6 +2687,20 @@ ${goFields.join(`
|
|
|
2631
2687
|
if (expr.callee.kind === "identifier" && expr.args.length === 0) {
|
|
2632
2688
|
return plain(this.rootFieldRef(expr.callee.name));
|
|
2633
2689
|
}
|
|
2690
|
+
if (expr.callee.kind === "identifier" && (identifierPath(expr.callee) ?? expr.callee.name) === "isValidElement" && expr.args.length === 1) {
|
|
2691
|
+
return this.renderConditionExpr(expr.args[0]);
|
|
2692
|
+
}
|
|
2693
|
+
if (expr.callee.kind === "identifier" && !this.templatePrimitives[identifierPath(expr.callee) ?? ""]) {
|
|
2694
|
+
const path = identifierPath(expr.callee) ?? expr.callee.name;
|
|
2695
|
+
this.errors.push({
|
|
2696
|
+
code: "BF102",
|
|
2697
|
+
severity: "error",
|
|
2698
|
+
message: `Predicate '${path}(...)' cannot be evaluated in a Go template. ` + `A server-side template cannot call user-defined JavaScript predicates.`,
|
|
2699
|
+
loc: this.makeLoc(),
|
|
2700
|
+
suggestion: { message: GO_REMEDIATION_OPTIONS }
|
|
2701
|
+
});
|
|
2702
|
+
return plain("false");
|
|
2703
|
+
}
|
|
2634
2704
|
return plain(this.renderParsedExpr(expr));
|
|
2635
2705
|
}
|
|
2636
2706
|
case "member": {
|
|
@@ -2842,6 +2912,9 @@ ${goFields.join(`
|
|
|
2842
2912
|
if (comp.name === "Portal") {
|
|
2843
2913
|
return this.renderPortalComponent(comp);
|
|
2844
2914
|
}
|
|
2915
|
+
if (comp.dynamicTag) {
|
|
2916
|
+
return this.renderChildren(comp.children);
|
|
2917
|
+
}
|
|
2845
2918
|
let templateCall;
|
|
2846
2919
|
if (this.inLoop) {
|
|
2847
2920
|
templateCall = `{{template "${comp.name}" .}}`;
|
|
@@ -2985,7 +3058,7 @@ ${children}`;
|
|
|
2985
3058
|
continue;
|
|
2986
3059
|
const branches = caseEntries.map(([k, v], i) => {
|
|
2987
3060
|
const head = i === 0 ? "{{if" : "{{else if";
|
|
2988
|
-
return `${head} eq ${keyExpr} ${JSON.stringify(k)}}}${v}`;
|
|
3061
|
+
return `${head} eq ${keyExpr} ${JSON.stringify(k)}}}${this.escapeAttrText(v)}`;
|
|
2989
3062
|
});
|
|
2990
3063
|
output += branches.join("") + "{{end}}";
|
|
2991
3064
|
}
|
package/dist/build.js
CHANGED
|
@@ -457,6 +457,7 @@ class GoTemplateAdapter extends BaseAdapter {
|
|
|
457
457
|
localStructFields = new Map;
|
|
458
458
|
synthStructTypes = new Map;
|
|
459
459
|
usesHtmlTemplate = false;
|
|
460
|
+
moduleStringConsts = new Map;
|
|
460
461
|
constructor(options = {}) {
|
|
461
462
|
super();
|
|
462
463
|
this.options = {
|
|
@@ -471,6 +472,7 @@ class GoTemplateAdapter extends BaseAdapter {
|
|
|
471
472
|
this.templateVarCounter = 0;
|
|
472
473
|
this.propsObjectName = ir.metadata.propsObjectName;
|
|
473
474
|
this.restPropsName = ir.metadata.restPropsName ?? null;
|
|
475
|
+
this.moduleStringConsts = this.collectModuleStringConsts(ir.metadata.localConstants);
|
|
474
476
|
if (!options?.siblingTemplatesRegistered) {
|
|
475
477
|
this.checkImportedLoopChildComponents(ir);
|
|
476
478
|
}
|
|
@@ -1252,6 +1254,12 @@ ${goFields.join(`
|
|
|
1252
1254
|
collectStaticChildInstancesRecursive(node, result, inLoop) {
|
|
1253
1255
|
if (node.type === "component") {
|
|
1254
1256
|
const comp = node;
|
|
1257
|
+
if (comp.dynamicTag) {
|
|
1258
|
+
for (const child of comp.children) {
|
|
1259
|
+
this.collectStaticChildInstancesRecursive(child, result, inLoop);
|
|
1260
|
+
}
|
|
1261
|
+
return;
|
|
1262
|
+
}
|
|
1255
1263
|
if (comp.name !== "Portal" && !inLoop && comp.slotId) {
|
|
1256
1264
|
const suffix = slotIdToFieldSuffix(comp.slotId);
|
|
1257
1265
|
result.push({
|
|
@@ -2088,6 +2096,9 @@ ${goFields.join(`
|
|
|
2088
2096
|
return emitParsedExpr(expr, this);
|
|
2089
2097
|
}
|
|
2090
2098
|
identifier(name) {
|
|
2099
|
+
const inlined = this.resolveModuleStringConst(name);
|
|
2100
|
+
if (inlined !== null)
|
|
2101
|
+
return inlined;
|
|
2091
2102
|
const currentLoopParam = this.loopParamStack[this.loopParamStack.length - 1];
|
|
2092
2103
|
if (currentLoopParam && name === currentLoopParam)
|
|
2093
2104
|
return ".";
|
|
@@ -2109,6 +2120,48 @@ ${goFields.join(`
|
|
|
2109
2120
|
const prefix = this.loopParamStack.length > 0 ? "$." : ".";
|
|
2110
2121
|
return `${prefix}${this.capitalizeFieldName(name)}`;
|
|
2111
2122
|
}
|
|
2123
|
+
collectModuleStringConsts(constants) {
|
|
2124
|
+
const map = new Map;
|
|
2125
|
+
for (const c of constants ?? []) {
|
|
2126
|
+
if (!c.isModule)
|
|
2127
|
+
continue;
|
|
2128
|
+
if (c.value === undefined)
|
|
2129
|
+
continue;
|
|
2130
|
+
const literal = this.parsePureStringLiteral(c.value);
|
|
2131
|
+
if (literal !== null)
|
|
2132
|
+
map.set(c.name, literal);
|
|
2133
|
+
}
|
|
2134
|
+
return map;
|
|
2135
|
+
}
|
|
2136
|
+
parsePureStringLiteral(source) {
|
|
2137
|
+
const sf = ts.createSourceFile("__const.ts", `const __x = (${source});`, ts.ScriptTarget.Latest, false);
|
|
2138
|
+
const stmt = sf.statements[0];
|
|
2139
|
+
if (!stmt || !ts.isVariableStatement(stmt))
|
|
2140
|
+
return null;
|
|
2141
|
+
const decl = stmt.declarationList.declarations[0];
|
|
2142
|
+
let init = decl?.initializer;
|
|
2143
|
+
while (init && ts.isParenthesizedExpression(init))
|
|
2144
|
+
init = init.expression;
|
|
2145
|
+
if (!init)
|
|
2146
|
+
return null;
|
|
2147
|
+
if (ts.isStringLiteral(init) || ts.isNoSubstitutionTemplateLiteral(init)) {
|
|
2148
|
+
return init.text;
|
|
2149
|
+
}
|
|
2150
|
+
return null;
|
|
2151
|
+
}
|
|
2152
|
+
resolveModuleStringConst(name) {
|
|
2153
|
+
if (this.loopParamStack.length > 0 && this.loopParamStack[this.loopParamStack.length - 1] === name) {
|
|
2154
|
+
return null;
|
|
2155
|
+
}
|
|
2156
|
+
if (this.loopVarRefCount.has(name))
|
|
2157
|
+
return null;
|
|
2158
|
+
if (this.isOuterLoopParam(name))
|
|
2159
|
+
return null;
|
|
2160
|
+
const value = this.moduleStringConsts.get(name);
|
|
2161
|
+
if (value === undefined)
|
|
2162
|
+
return null;
|
|
2163
|
+
return `"${this.escapeGoString(value)}"`;
|
|
2164
|
+
}
|
|
2112
2165
|
literal(value, literalType) {
|
|
2113
2166
|
if (literalType === "string")
|
|
2114
2167
|
return `"${value}"`;
|
|
@@ -2949,6 +3002,9 @@ ${goFields.join(`
|
|
|
2949
3002
|
switch (expr.kind) {
|
|
2950
3003
|
case "identifier":
|
|
2951
3004
|
{
|
|
3005
|
+
const inlined = this.resolveModuleStringConst(expr.name);
|
|
3006
|
+
if (inlined !== null)
|
|
3007
|
+
return plain(inlined);
|
|
2952
3008
|
const currentLoopParam = this.loopParamStack[this.loopParamStack.length - 1];
|
|
2953
3009
|
if (currentLoopParam && expr.name === currentLoopParam) {
|
|
2954
3010
|
return plain(".");
|
|
@@ -2971,6 +3027,20 @@ ${goFields.join(`
|
|
|
2971
3027
|
if (expr.callee.kind === "identifier" && expr.args.length === 0) {
|
|
2972
3028
|
return plain(this.rootFieldRef(expr.callee.name));
|
|
2973
3029
|
}
|
|
3030
|
+
if (expr.callee.kind === "identifier" && (identifierPath(expr.callee) ?? expr.callee.name) === "isValidElement" && expr.args.length === 1) {
|
|
3031
|
+
return this.renderConditionExpr(expr.args[0]);
|
|
3032
|
+
}
|
|
3033
|
+
if (expr.callee.kind === "identifier" && !this.templatePrimitives[identifierPath(expr.callee) ?? ""]) {
|
|
3034
|
+
const path = identifierPath(expr.callee) ?? expr.callee.name;
|
|
3035
|
+
this.errors.push({
|
|
3036
|
+
code: "BF102",
|
|
3037
|
+
severity: "error",
|
|
3038
|
+
message: `Predicate '${path}(...)' cannot be evaluated in a Go template. ` + `A server-side template cannot call user-defined JavaScript predicates.`,
|
|
3039
|
+
loc: this.makeLoc(),
|
|
3040
|
+
suggestion: { message: GO_REMEDIATION_OPTIONS }
|
|
3041
|
+
});
|
|
3042
|
+
return plain("false");
|
|
3043
|
+
}
|
|
2974
3044
|
return plain(this.renderParsedExpr(expr));
|
|
2975
3045
|
}
|
|
2976
3046
|
case "member": {
|
|
@@ -3182,6 +3252,9 @@ ${goFields.join(`
|
|
|
3182
3252
|
if (comp.name === "Portal") {
|
|
3183
3253
|
return this.renderPortalComponent(comp);
|
|
3184
3254
|
}
|
|
3255
|
+
if (comp.dynamicTag) {
|
|
3256
|
+
return this.renderChildren(comp.children);
|
|
3257
|
+
}
|
|
3185
3258
|
let templateCall;
|
|
3186
3259
|
if (this.inLoop) {
|
|
3187
3260
|
templateCall = `{{template "${comp.name}" .}}`;
|
|
@@ -3325,7 +3398,7 @@ ${children}`;
|
|
|
3325
3398
|
continue;
|
|
3326
3399
|
const branches = caseEntries.map(([k, v], i) => {
|
|
3327
3400
|
const head = i === 0 ? "{{if" : "{{else if";
|
|
3328
|
-
return `${head} eq ${keyExpr} ${JSON.stringify(k)}}}${v}`;
|
|
3401
|
+
return `${head} eq ${keyExpr} ${JSON.stringify(k)}}}${this.escapeAttrText(v)}`;
|
|
3329
3402
|
});
|
|
3330
3403
|
output += branches.join("") + "{{end}}";
|
|
3331
3404
|
}
|
package/dist/index.js
CHANGED
|
@@ -117,6 +117,7 @@ class GoTemplateAdapter extends BaseAdapter {
|
|
|
117
117
|
localStructFields = new Map;
|
|
118
118
|
synthStructTypes = new Map;
|
|
119
119
|
usesHtmlTemplate = false;
|
|
120
|
+
moduleStringConsts = new Map;
|
|
120
121
|
constructor(options = {}) {
|
|
121
122
|
super();
|
|
122
123
|
this.options = {
|
|
@@ -131,6 +132,7 @@ class GoTemplateAdapter extends BaseAdapter {
|
|
|
131
132
|
this.templateVarCounter = 0;
|
|
132
133
|
this.propsObjectName = ir.metadata.propsObjectName;
|
|
133
134
|
this.restPropsName = ir.metadata.restPropsName ?? null;
|
|
135
|
+
this.moduleStringConsts = this.collectModuleStringConsts(ir.metadata.localConstants);
|
|
134
136
|
if (!options?.siblingTemplatesRegistered) {
|
|
135
137
|
this.checkImportedLoopChildComponents(ir);
|
|
136
138
|
}
|
|
@@ -912,6 +914,12 @@ ${goFields.join(`
|
|
|
912
914
|
collectStaticChildInstancesRecursive(node, result, inLoop) {
|
|
913
915
|
if (node.type === "component") {
|
|
914
916
|
const comp = node;
|
|
917
|
+
if (comp.dynamicTag) {
|
|
918
|
+
for (const child of comp.children) {
|
|
919
|
+
this.collectStaticChildInstancesRecursive(child, result, inLoop);
|
|
920
|
+
}
|
|
921
|
+
return;
|
|
922
|
+
}
|
|
915
923
|
if (comp.name !== "Portal" && !inLoop && comp.slotId) {
|
|
916
924
|
const suffix = slotIdToFieldSuffix(comp.slotId);
|
|
917
925
|
result.push({
|
|
@@ -1748,6 +1756,9 @@ ${goFields.join(`
|
|
|
1748
1756
|
return emitParsedExpr(expr, this);
|
|
1749
1757
|
}
|
|
1750
1758
|
identifier(name) {
|
|
1759
|
+
const inlined = this.resolveModuleStringConst(name);
|
|
1760
|
+
if (inlined !== null)
|
|
1761
|
+
return inlined;
|
|
1751
1762
|
const currentLoopParam = this.loopParamStack[this.loopParamStack.length - 1];
|
|
1752
1763
|
if (currentLoopParam && name === currentLoopParam)
|
|
1753
1764
|
return ".";
|
|
@@ -1769,6 +1780,48 @@ ${goFields.join(`
|
|
|
1769
1780
|
const prefix = this.loopParamStack.length > 0 ? "$." : ".";
|
|
1770
1781
|
return `${prefix}${this.capitalizeFieldName(name)}`;
|
|
1771
1782
|
}
|
|
1783
|
+
collectModuleStringConsts(constants) {
|
|
1784
|
+
const map = new Map;
|
|
1785
|
+
for (const c of constants ?? []) {
|
|
1786
|
+
if (!c.isModule)
|
|
1787
|
+
continue;
|
|
1788
|
+
if (c.value === undefined)
|
|
1789
|
+
continue;
|
|
1790
|
+
const literal = this.parsePureStringLiteral(c.value);
|
|
1791
|
+
if (literal !== null)
|
|
1792
|
+
map.set(c.name, literal);
|
|
1793
|
+
}
|
|
1794
|
+
return map;
|
|
1795
|
+
}
|
|
1796
|
+
parsePureStringLiteral(source) {
|
|
1797
|
+
const sf = ts.createSourceFile("__const.ts", `const __x = (${source});`, ts.ScriptTarget.Latest, false);
|
|
1798
|
+
const stmt = sf.statements[0];
|
|
1799
|
+
if (!stmt || !ts.isVariableStatement(stmt))
|
|
1800
|
+
return null;
|
|
1801
|
+
const decl = stmt.declarationList.declarations[0];
|
|
1802
|
+
let init = decl?.initializer;
|
|
1803
|
+
while (init && ts.isParenthesizedExpression(init))
|
|
1804
|
+
init = init.expression;
|
|
1805
|
+
if (!init)
|
|
1806
|
+
return null;
|
|
1807
|
+
if (ts.isStringLiteral(init) || ts.isNoSubstitutionTemplateLiteral(init)) {
|
|
1808
|
+
return init.text;
|
|
1809
|
+
}
|
|
1810
|
+
return null;
|
|
1811
|
+
}
|
|
1812
|
+
resolveModuleStringConst(name) {
|
|
1813
|
+
if (this.loopParamStack.length > 0 && this.loopParamStack[this.loopParamStack.length - 1] === name) {
|
|
1814
|
+
return null;
|
|
1815
|
+
}
|
|
1816
|
+
if (this.loopVarRefCount.has(name))
|
|
1817
|
+
return null;
|
|
1818
|
+
if (this.isOuterLoopParam(name))
|
|
1819
|
+
return null;
|
|
1820
|
+
const value = this.moduleStringConsts.get(name);
|
|
1821
|
+
if (value === undefined)
|
|
1822
|
+
return null;
|
|
1823
|
+
return `"${this.escapeGoString(value)}"`;
|
|
1824
|
+
}
|
|
1772
1825
|
literal(value, literalType) {
|
|
1773
1826
|
if (literalType === "string")
|
|
1774
1827
|
return `"${value}"`;
|
|
@@ -2609,6 +2662,9 @@ ${goFields.join(`
|
|
|
2609
2662
|
switch (expr.kind) {
|
|
2610
2663
|
case "identifier":
|
|
2611
2664
|
{
|
|
2665
|
+
const inlined = this.resolveModuleStringConst(expr.name);
|
|
2666
|
+
if (inlined !== null)
|
|
2667
|
+
return plain(inlined);
|
|
2612
2668
|
const currentLoopParam = this.loopParamStack[this.loopParamStack.length - 1];
|
|
2613
2669
|
if (currentLoopParam && expr.name === currentLoopParam) {
|
|
2614
2670
|
return plain(".");
|
|
@@ -2631,6 +2687,20 @@ ${goFields.join(`
|
|
|
2631
2687
|
if (expr.callee.kind === "identifier" && expr.args.length === 0) {
|
|
2632
2688
|
return plain(this.rootFieldRef(expr.callee.name));
|
|
2633
2689
|
}
|
|
2690
|
+
if (expr.callee.kind === "identifier" && (identifierPath(expr.callee) ?? expr.callee.name) === "isValidElement" && expr.args.length === 1) {
|
|
2691
|
+
return this.renderConditionExpr(expr.args[0]);
|
|
2692
|
+
}
|
|
2693
|
+
if (expr.callee.kind === "identifier" && !this.templatePrimitives[identifierPath(expr.callee) ?? ""]) {
|
|
2694
|
+
const path = identifierPath(expr.callee) ?? expr.callee.name;
|
|
2695
|
+
this.errors.push({
|
|
2696
|
+
code: "BF102",
|
|
2697
|
+
severity: "error",
|
|
2698
|
+
message: `Predicate '${path}(...)' cannot be evaluated in a Go template. ` + `A server-side template cannot call user-defined JavaScript predicates.`,
|
|
2699
|
+
loc: this.makeLoc(),
|
|
2700
|
+
suggestion: { message: GO_REMEDIATION_OPTIONS }
|
|
2701
|
+
});
|
|
2702
|
+
return plain("false");
|
|
2703
|
+
}
|
|
2634
2704
|
return plain(this.renderParsedExpr(expr));
|
|
2635
2705
|
}
|
|
2636
2706
|
case "member": {
|
|
@@ -2842,6 +2912,9 @@ ${goFields.join(`
|
|
|
2842
2912
|
if (comp.name === "Portal") {
|
|
2843
2913
|
return this.renderPortalComponent(comp);
|
|
2844
2914
|
}
|
|
2915
|
+
if (comp.dynamicTag) {
|
|
2916
|
+
return this.renderChildren(comp.children);
|
|
2917
|
+
}
|
|
2845
2918
|
let templateCall;
|
|
2846
2919
|
if (this.inLoop) {
|
|
2847
2920
|
templateCall = `{{template "${comp.name}" .}}`;
|
|
@@ -2985,7 +3058,7 @@ ${children}`;
|
|
|
2985
3058
|
continue;
|
|
2986
3059
|
const branches = caseEntries.map(([k, v], i) => {
|
|
2987
3060
|
const head = i === 0 ? "{{if" : "{{else if";
|
|
2988
|
-
return `${head} eq ${keyExpr} ${JSON.stringify(k)}}}${v}`;
|
|
3061
|
+
return `${head} eq ${keyExpr} ${JSON.stringify(k)}}}${this.escapeAttrText(v)}`;
|
|
2989
3062
|
});
|
|
2990
3063
|
output += branches.join("") + "{{end}}";
|
|
2991
3064
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@barefootjs/go-template",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
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",
|
|
@@ -54,6 +54,6 @@
|
|
|
54
54
|
},
|
|
55
55
|
"devDependencies": {
|
|
56
56
|
"@barefootjs/adapter-tests": "0.1.0",
|
|
57
|
-
"@barefootjs/jsx": "0.
|
|
57
|
+
"@barefootjs/jsx": "0.7.0"
|
|
58
58
|
}
|
|
59
59
|
}
|
|
@@ -76,16 +76,17 @@ runAdapterConformanceTests({
|
|
|
76
76
|
// option fixed on the Hono side. Separate follow-up.
|
|
77
77
|
'toggle-shared',
|
|
78
78
|
'props-reactivity-comparison',
|
|
79
|
-
// #1467 Phase
|
|
80
|
-
//
|
|
81
|
-
//
|
|
82
|
-
//
|
|
83
|
-
//
|
|
84
|
-
//
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
'
|
|
79
|
+
// #1467 Phase 2b: basic interactive `site/ui` primitives. Cross-adapter
|
|
80
|
+
// parity for the `site/ui` corpus is Phase 3, so these participate only
|
|
81
|
+
// in Hono SSR conformance + the fixture-hydrate runtime layer for now.
|
|
82
|
+
// (`label` and `kbd` are static helpers; `toggle` / `switch` /
|
|
83
|
+
// `checkbox` carry uncontrolled state; `input` / `textarea` are
|
|
84
|
+
// pass-through native controls.)
|
|
85
|
+
'toggle',
|
|
86
|
+
'switch',
|
|
87
|
+
'checkbox',
|
|
88
|
+
'textarea',
|
|
89
|
+
'kbd',
|
|
89
90
|
],
|
|
90
91
|
// Per-fixture build-time contracts for shapes the Go template
|
|
91
92
|
// adapter intentionally refuses to lower. Lives here (not on the
|
|
@@ -298,6 +299,28 @@ export function Counter(props: { initial?: number }) {
|
|
|
298
299
|
expect(result.types).toContain('Count int')
|
|
299
300
|
})
|
|
300
301
|
|
|
302
|
+
test('module pure-string const referenced in className inlines the literal (#1467 Phase 2b)', () => {
|
|
303
|
+
// A module-scope `const X = 'literal'` used inside a className
|
|
304
|
+
// template literal must inline its value, NOT emit `{{.X}}` against a
|
|
305
|
+
// Props field that never exists (Go fails `can't evaluate field X`).
|
|
306
|
+
// Hono inlines it at runtime; this restores byte-parity.
|
|
307
|
+
const source = `
|
|
308
|
+
"use client"
|
|
309
|
+
const labelClasses = 'flex items-center group-data-[disabled=true]:opacity-50'
|
|
310
|
+
export function Label({ className = '' }: { className?: string }) {
|
|
311
|
+
return <label className={\`\${labelClasses} \${className}\`} />
|
|
312
|
+
}
|
|
313
|
+
`
|
|
314
|
+
const { template, types } = compileAndGenerate(source)
|
|
315
|
+
// The literal is inlined as a Go string literal, escaped tokens intact.
|
|
316
|
+
expect(template).toContain(
|
|
317
|
+
'{{"flex items-center group-data-[disabled=true]:opacity-50"}}',
|
|
318
|
+
)
|
|
319
|
+
// No struct-field reference to the const, and no Props field for it.
|
|
320
|
+
expect(template).not.toContain('.LabelClasses')
|
|
321
|
+
expect(types).not.toContain('LabelClasses')
|
|
322
|
+
})
|
|
323
|
+
|
|
301
324
|
test('dynamic loop with child component → NewXxxProps carries a populate-this-slice doc comment (#1442 echo TodoApp repro)', () => {
|
|
302
325
|
// Regression: a `todos().map(t => <TodoItem todo={t} />)` loop with a
|
|
303
326
|
// dynamic array (signal getter, not a static prop) declares
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pins the Go template lowering of the `Slot` component's dynamic tag.
|
|
3
|
+
*
|
|
4
|
+
* `Slot` does `const Tag = children.tag; return <Tag …>{…}</Tag>` inside
|
|
5
|
+
* an `if (isValidElement(children)) { … }` block. A naive lowering would:
|
|
6
|
+
* 1. emit `{{template "Tag" .TagSlot0}}` — a template that can never be
|
|
7
|
+
* registered. Go's html/template escape-walks ALL registered
|
|
8
|
+
* templates (even dead branches), so the whole render fails with
|
|
9
|
+
* `no such template "Tag"`.
|
|
10
|
+
* 2. lower `isValidElement(children)` to a bogus `.IsValidElement`
|
|
11
|
+
* struct-field access (a user type-guard predicate Go can't evaluate).
|
|
12
|
+
*
|
|
13
|
+
* The `dynamicTag` IR flag (jsx-to-ir) plus the call-condition fallback
|
|
14
|
+
* (go-template-adapter) defuse both. This test asserts the *emitted Go
|
|
15
|
+
* template string* carries neither pathology, complementing the full
|
|
16
|
+
* conformance render in `go-template-adapter.test.ts`.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { describe, test, expect } from 'bun:test'
|
|
20
|
+
import { GoTemplateAdapter } from '../adapter/go-template-adapter'
|
|
21
|
+
import { compileJSX } from '@barefootjs/jsx'
|
|
22
|
+
|
|
23
|
+
// The committed SSR-precompiled Slot module the conformance harness feeds
|
|
24
|
+
// to Go (`children.tag` dynamic tag inside an isValidElement guard).
|
|
25
|
+
const SLOT_SSR = `/** @jsxImportSource hono/jsx */
|
|
26
|
+
interface SlotProps { children?: unknown; className?: string; [key: string]: unknown }
|
|
27
|
+
function isValidElement(element: unknown): element is { tag: unknown; props: Record<string, unknown> } {
|
|
28
|
+
return !!(element && typeof element === 'object' && 'tag' in element && 'props' in element)
|
|
29
|
+
}
|
|
30
|
+
export function Slot({ children, className, ...props }: SlotProps) {
|
|
31
|
+
if (children && isValidElement(children)) {
|
|
32
|
+
const Tag = children.tag as any
|
|
33
|
+
const childProps = children.props
|
|
34
|
+
const childClass = (childProps.className as string) || ''
|
|
35
|
+
const childChildren = childProps.children
|
|
36
|
+
const mergedClass = [className, childClass].filter(Boolean).join(' ')
|
|
37
|
+
return <Tag {...childProps} {...props} className={mergedClass || undefined}>{childChildren}</Tag>
|
|
38
|
+
}
|
|
39
|
+
return <>{children}</>
|
|
40
|
+
}
|
|
41
|
+
`
|
|
42
|
+
|
|
43
|
+
describe('Slot dynamic-tag Go lowering', () => {
|
|
44
|
+
test('emitted Go template has no `{{template "Tag"` call and no `.IsValidElement` field', () => {
|
|
45
|
+
const adapter = new GoTemplateAdapter()
|
|
46
|
+
const result = compileJSX(SLOT_SSR, 'slot.tsx', { adapter, componentName: 'Slot' })
|
|
47
|
+
expect(result.errors.filter(e => e.severity === 'error')).toEqual([])
|
|
48
|
+
const template = result.files.find(f => f.type === 'markedTemplate')!
|
|
49
|
+
expect(template).toBeDefined()
|
|
50
|
+
expect(template.content).not.toContain('{{template "Tag"')
|
|
51
|
+
expect(template.content).not.toContain('.IsValidElement')
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
test('`isValidElement(children)` lowers to a real truthiness check — no diagnostic, no forced literal', () => {
|
|
55
|
+
const adapter = new GoTemplateAdapter()
|
|
56
|
+
const result = compileJSX(SLOT_SSR, 'slot.tsx', { adapter, componentName: 'Slot' })
|
|
57
|
+
// The guard evaluates faithfully on Go (element ⟺ has children to render),
|
|
58
|
+
// so there is neither an error nor an ignorable warning, and the condition
|
|
59
|
+
// is a real `.Children` truthiness check rather than a fudged literal.
|
|
60
|
+
expect(result.errors).toEqual([])
|
|
61
|
+
const template = result.files.find(f => f.type === 'markedTemplate')!
|
|
62
|
+
expect(template.content).toContain('.Children')
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
test('a non-isValidElement user predicate (e.g. `isAdmin`) is a hard BF102 error, not a silent literal', () => {
|
|
66
|
+
const SRC = `/** @jsxImportSource hono/jsx */
|
|
67
|
+
declare function isAdmin(u: unknown): boolean
|
|
68
|
+
export function Gate({ user }: { user?: unknown }) {
|
|
69
|
+
return <div>{isAdmin(user) ? <span>secret</span> : null}</div>
|
|
70
|
+
}
|
|
71
|
+
`
|
|
72
|
+
const adapter = new GoTemplateAdapter()
|
|
73
|
+
const result = compileJSX(SRC, 'gate.tsx', { adapter, componentName: 'Gate' })
|
|
74
|
+
const errors = result.errors.filter(e => e.severity === 'error')
|
|
75
|
+
expect(
|
|
76
|
+
errors.some(e => e.code === 'BF102' && /cannot be evaluated/i.test(e.message))
|
|
77
|
+
).toBe(true)
|
|
78
|
+
})
|
|
79
|
+
})
|
|
@@ -34,6 +34,7 @@ import type {
|
|
|
34
34
|
IRIfStatement,
|
|
35
35
|
IRProvider,
|
|
36
36
|
IRAsync,
|
|
37
|
+
IRMetadata,
|
|
37
38
|
TemplatePrimitiveRegistry,
|
|
38
39
|
} from '@barefootjs/jsx'
|
|
39
40
|
import {
|
|
@@ -418,6 +419,21 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
418
419
|
* `template.HTML(...)`; toggles the `"html/template"` import. */
|
|
419
420
|
private usesHtmlTemplate: boolean = false
|
|
420
421
|
|
|
422
|
+
/**
|
|
423
|
+
* Module-scope pure string-literal constants (`const X = 'literal'` at
|
|
424
|
+
* file top-level), keyed by name → resolved literal value. Populated at
|
|
425
|
+
* `generate()` entry from `ir.metadata.localConstants`. When an identifier
|
|
426
|
+
* in an expression resolves to one of these, the adapter inlines the
|
|
427
|
+
* literal value instead of emitting a struct-field reference
|
|
428
|
+
* (`{{.X}}`) — the field never exists on the Props struct, so without
|
|
429
|
+
* inlining Go's template engine fails with `can't evaluate field X`.
|
|
430
|
+
* Hono inlines it for free (it evaluates real JS); this restores parity.
|
|
431
|
+
* Only module-scope, pure string literals qualify — function-scope
|
|
432
|
+
* locals legitimately become template vars/props, and `Record<T,string>`
|
|
433
|
+
* indexed lookups / memos / signals are deliberately excluded.
|
|
434
|
+
*/
|
|
435
|
+
private moduleStringConsts: Map<string, string> = new Map()
|
|
436
|
+
|
|
421
437
|
constructor(options: GoTemplateAdapterOptions = {}) {
|
|
422
438
|
super()
|
|
423
439
|
this.options = {
|
|
@@ -438,6 +454,7 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
438
454
|
this.templateVarCounter = 0
|
|
439
455
|
this.propsObjectName = ir.metadata.propsObjectName
|
|
440
456
|
this.restPropsName = ir.metadata.restPropsName ?? null
|
|
457
|
+
this.moduleStringConsts = this.collectModuleStringConsts(ir.metadata.localConstants)
|
|
441
458
|
|
|
442
459
|
// Surface loop-body usages of components imported from sibling
|
|
443
460
|
// .tsx files. The adapter emits `{{template "X" .}}` for these,
|
|
@@ -1624,6 +1641,16 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
1624
1641
|
): void {
|
|
1625
1642
|
if (node.type === 'component') {
|
|
1626
1643
|
const comp = node as IRComponent
|
|
1644
|
+
// Dynamic-tag locals (`const Tag = children.tag`) have no registrable
|
|
1645
|
+
// template, so they get no `.<Name>SlotN` struct field. Recurse into
|
|
1646
|
+
// their children (which lower as a passthrough) so any real static
|
|
1647
|
+
// child components nested inside still get their slot fields.
|
|
1648
|
+
if (comp.dynamicTag) {
|
|
1649
|
+
for (const child of comp.children) {
|
|
1650
|
+
this.collectStaticChildInstancesRecursive(child, result, inLoop)
|
|
1651
|
+
}
|
|
1652
|
+
return
|
|
1653
|
+
}
|
|
1627
1654
|
// Skip Portal components (handled separately via PortalCollector)
|
|
1628
1655
|
// Skip components inside loops (handled by nestedComponents)
|
|
1629
1656
|
if (comp.name !== 'Portal' && !inLoop && comp.slotId) {
|
|
@@ -2891,6 +2918,11 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
2891
2918
|
// ===========================================================================
|
|
2892
2919
|
|
|
2893
2920
|
identifier(name: string): string {
|
|
2921
|
+
// Module pure-string const (e.g. `const baseClasses = '...'` used in a
|
|
2922
|
+
// className template literal): inline the literal value rather than
|
|
2923
|
+
// emit `{{.BaseClasses}}` against a Props field that never exists.
|
|
2924
|
+
const inlined = this.resolveModuleStringConst(name)
|
|
2925
|
+
if (inlined !== null) return inlined
|
|
2894
2926
|
const currentLoopParam = this.loopParamStack[this.loopParamStack.length - 1]
|
|
2895
2927
|
if (currentLoopParam && name === currentLoopParam) return '.'
|
|
2896
2928
|
// An *outer* loop's value variable (we're in a nested loop) is in scope as
|
|
@@ -2927,6 +2959,72 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
2927
2959
|
return `${prefix}${this.capitalizeFieldName(name)}`
|
|
2928
2960
|
}
|
|
2929
2961
|
|
|
2962
|
+
/**
|
|
2963
|
+
* Build the module pure-string-const map from the IR's localConstants.
|
|
2964
|
+
* A const qualifies only when it is module-scope (`isModule`) and its
|
|
2965
|
+
* initializer parses to a single string literal (`ts.StringLiteral` or
|
|
2966
|
+
* `ts.NoSubstitutionTemplateLiteral` — a backtick string with no `${}`).
|
|
2967
|
+
* Template literals *with* interpolations, numeric/object initializers,
|
|
2968
|
+
* `Record<T,string>` maps, memos, and signals are all excluded: only a
|
|
2969
|
+
* pure compile-time string can be safely inlined byte-for-byte.
|
|
2970
|
+
*/
|
|
2971
|
+
private collectModuleStringConsts(constants: IRMetadata['localConstants']): Map<string, string> {
|
|
2972
|
+
const map = new Map<string, string>()
|
|
2973
|
+
for (const c of constants ?? []) {
|
|
2974
|
+
if (!c.isModule) continue
|
|
2975
|
+
if (c.value === undefined) continue
|
|
2976
|
+
const literal = this.parsePureStringLiteral(c.value)
|
|
2977
|
+
if (literal !== null) map.set(c.name, literal)
|
|
2978
|
+
}
|
|
2979
|
+
return map
|
|
2980
|
+
}
|
|
2981
|
+
|
|
2982
|
+
/**
|
|
2983
|
+
* Parse a const initializer's source text. Returns the unescaped string
|
|
2984
|
+
* value when the whole initializer is a single string literal (or a
|
|
2985
|
+
* no-substitution template literal), else `null`. Uses the TS parser so
|
|
2986
|
+
* escapes/quotes are resolved exactly as JS would, matching the value
|
|
2987
|
+
* the Hono reference inlines at runtime.
|
|
2988
|
+
*/
|
|
2989
|
+
private parsePureStringLiteral(source: string): string | null {
|
|
2990
|
+
const sf = ts.createSourceFile(
|
|
2991
|
+
'__const.ts',
|
|
2992
|
+
`const __x = (${source});`,
|
|
2993
|
+
ts.ScriptTarget.Latest,
|
|
2994
|
+
/*setParentNodes*/ false,
|
|
2995
|
+
)
|
|
2996
|
+
const stmt = sf.statements[0]
|
|
2997
|
+
if (!stmt || !ts.isVariableStatement(stmt)) return null
|
|
2998
|
+
const decl = stmt.declarationList.declarations[0]
|
|
2999
|
+
let init = decl?.initializer
|
|
3000
|
+
while (init && ts.isParenthesizedExpression(init)) init = init.expression
|
|
3001
|
+
if (!init) return null
|
|
3002
|
+
if (ts.isStringLiteral(init) || ts.isNoSubstitutionTemplateLiteral(init)) {
|
|
3003
|
+
return init.text
|
|
3004
|
+
}
|
|
3005
|
+
return null
|
|
3006
|
+
}
|
|
3007
|
+
|
|
3008
|
+
/**
|
|
3009
|
+
* Resolve an identifier to its inlined Go string literal when it names a
|
|
3010
|
+
* module pure-string const. Returns the Go template literal form
|
|
3011
|
+
* (`"<escaped>"`) so callers can drop it straight into a `{{...}}` action,
|
|
3012
|
+
* or `null` when the name is not such a const (the caller then falls back
|
|
3013
|
+
* to its normal field-ref lowering). The value is escaped for a Go
|
|
3014
|
+
* double-quoted string literal — Go's `html/template` then applies the
|
|
3015
|
+
* same contextual auto-escaping it applies to any literal, matching Hono.
|
|
3016
|
+
*/
|
|
3017
|
+
private resolveModuleStringConst(name: string): string | null {
|
|
3018
|
+
if (this.loopParamStack.length > 0 && this.loopParamStack[this.loopParamStack.length - 1] === name) {
|
|
3019
|
+
return null
|
|
3020
|
+
}
|
|
3021
|
+
if (this.loopVarRefCount.has(name)) return null
|
|
3022
|
+
if (this.isOuterLoopParam(name)) return null
|
|
3023
|
+
const value = this.moduleStringConsts.get(name)
|
|
3024
|
+
if (value === undefined) return null
|
|
3025
|
+
return `"${this.escapeGoString(value)}"`
|
|
3026
|
+
}
|
|
3027
|
+
|
|
2930
3028
|
literal(value: string | number | boolean | null, literalType: LiteralType): string {
|
|
2931
3029
|
if (literalType === 'string') return `"${value}"`
|
|
2932
3030
|
if (literalType === 'null') return 'nil'
|
|
@@ -4313,6 +4411,8 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
4313
4411
|
switch (expr.kind) {
|
|
4314
4412
|
case 'identifier':
|
|
4315
4413
|
{
|
|
4414
|
+
const inlined = this.resolveModuleStringConst(expr.name)
|
|
4415
|
+
if (inlined !== null) return plain(inlined)
|
|
4316
4416
|
const currentLoopParam = this.loopParamStack[this.loopParamStack.length - 1]
|
|
4317
4417
|
if (currentLoopParam && expr.name === currentLoopParam) {
|
|
4318
4418
|
return plain('.')
|
|
@@ -4336,6 +4436,46 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
4336
4436
|
if (expr.callee.kind === 'identifier' && expr.args.length === 0) {
|
|
4337
4437
|
return plain(this.rootFieldRef(expr.callee.name))
|
|
4338
4438
|
}
|
|
4439
|
+
// `isValidElement(x)` — the framework "is this a renderable element?"
|
|
4440
|
+
// predicate. In the Go SSR children model an element is represented by
|
|
4441
|
+
// its already-rendered markup, so this evaluates faithfully as a
|
|
4442
|
+
// truthiness check on the argument (an element is "valid" when there is
|
|
4443
|
+
// something to render). Lowering it as a real, evaluatable expression —
|
|
4444
|
+
// rather than a fabricated `.IsValidElement` field access — is what lets
|
|
4445
|
+
// the `Slot` dynamic-tag guard register and run cleanly on Go.
|
|
4446
|
+
if (
|
|
4447
|
+
expr.callee.kind === 'identifier' &&
|
|
4448
|
+
(identifierPath(expr.callee) ?? expr.callee.name) === 'isValidElement' &&
|
|
4449
|
+
expr.args.length === 1
|
|
4450
|
+
) {
|
|
4451
|
+
return this.renderConditionExpr(expr.args[0])
|
|
4452
|
+
}
|
|
4453
|
+
// Any other user-defined predicate call with arguments (e.g.
|
|
4454
|
+
// `isAdmin(user)`) has no server-side evaluator and is not a registered
|
|
4455
|
+
// template primitive. There is no honest way to evaluate it at SSR time,
|
|
4456
|
+
// and silently forcing it (to true OR false) is a correctness hazard —
|
|
4457
|
+
// a forced-true could expose auth-gated content, a forced-false could
|
|
4458
|
+
// hide required content, and a warning is too easily ignored. Refuse
|
|
4459
|
+
// with a hard BF102 error so the author must move the predicate to a
|
|
4460
|
+
// supported primitive or defer it with `/* @client */`.
|
|
4461
|
+
if (
|
|
4462
|
+
expr.callee.kind === 'identifier' &&
|
|
4463
|
+
!this.templatePrimitives[identifierPath(expr.callee) ?? '']
|
|
4464
|
+
) {
|
|
4465
|
+
const path = identifierPath(expr.callee) ?? expr.callee.name
|
|
4466
|
+
this.errors.push({
|
|
4467
|
+
code: 'BF102',
|
|
4468
|
+
severity: 'error',
|
|
4469
|
+
message:
|
|
4470
|
+
`Predicate '${path}(...)' cannot be evaluated in a Go template. ` +
|
|
4471
|
+
`A server-side template cannot call user-defined JavaScript predicates.`,
|
|
4472
|
+
loc: this.makeLoc(),
|
|
4473
|
+
suggestion: { message: GO_REMEDIATION_OPTIONS },
|
|
4474
|
+
})
|
|
4475
|
+
// Go template actions must be self-contained; emit a literal so the
|
|
4476
|
+
// partial still parses while the build fails on the BF102 error.
|
|
4477
|
+
return plain('false')
|
|
4478
|
+
}
|
|
4339
4479
|
return plain(this.renderParsedExpr(expr))
|
|
4340
4480
|
}
|
|
4341
4481
|
|
|
@@ -4634,6 +4774,18 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
4634
4774
|
return this.renderPortalComponent(comp)
|
|
4635
4775
|
}
|
|
4636
4776
|
|
|
4777
|
+
// Dynamic-tag local (`const Tag = children.tag`): there is no template
|
|
4778
|
+
// named `<Tag>` to call — emitting `{{template "Tag" .TagSlot0}}` would
|
|
4779
|
+
// reference a template that can never be registered, and Go's
|
|
4780
|
+
// html/template escape-walks ALL registered templates (even dead
|
|
4781
|
+
// branches), so the whole render fails with `no such template "Tag"`.
|
|
4782
|
+
// Lower it to its children passthrough instead, so the dead branch
|
|
4783
|
+
// renders harmlessly and the Slot template registers cleanly. (Real
|
|
4784
|
+
// server-side asChild prop-merge on Go is a separate, deferred concern.)
|
|
4785
|
+
if (comp.dynamicTag) {
|
|
4786
|
+
return this.renderChildren(comp.children)
|
|
4787
|
+
}
|
|
4788
|
+
|
|
4637
4789
|
// In Go templates, components are rendered using {{template "name" data}}
|
|
4638
4790
|
let templateCall: string
|
|
4639
4791
|
if (this.inLoop) {
|
|
@@ -4907,7 +5059,13 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
4907
5059
|
if (caseEntries.length === 0) continue
|
|
4908
5060
|
const branches = caseEntries.map(([k, v], i) => {
|
|
4909
5061
|
const head = i === 0 ? '{{if' : '{{else if'
|
|
4910
|
-
|
|
5062
|
+
// The case value is a static Record<T,string> literal emitted
|
|
5063
|
+
// straight into attribute-value text, so HTML-escape it the same
|
|
5064
|
+
// way `string` parts are (via substituteJsInterpolations →
|
|
5065
|
+
// escapeAttrText). Without this, UnoCSS tokens like
|
|
5066
|
+
// `has-[>svg]:px-2.5` would leak a raw `>` and diverge from the
|
|
5067
|
+
// Hono reference, which escapes it to `>`.
|
|
5068
|
+
return `${head} eq ${keyExpr} ${JSON.stringify(k)}}}${this.escapeAttrText(v)}`
|
|
4911
5069
|
})
|
|
4912
5070
|
output += branches.join('') + '{{end}}'
|
|
4913
5071
|
}
|
package/src/test-render.ts
CHANGED
|
@@ -180,7 +180,7 @@ export async function renderGoTemplateComponent(options: RenderOptions): Promise
|
|
|
180
180
|
const escapedTemplate = template.replace(/`/g, '` + "`" + `')
|
|
181
181
|
|
|
182
182
|
// Build props initialization
|
|
183
|
-
const propsInit = buildGoPropsInit(componentName, props)
|
|
183
|
+
const propsInit = buildGoPropsInit(componentName, props, ir)
|
|
184
184
|
|
|
185
185
|
// Honour `__instanceId` from props for the root scope id so
|
|
186
186
|
// shared-component fixtures (which pin `<ComponentName>_test`) match
|
|
@@ -280,10 +280,26 @@ ${propsInit}
|
|
|
280
280
|
function buildGoPropsInit(
|
|
281
281
|
_componentName: string,
|
|
282
282
|
props?: Record<string, unknown>,
|
|
283
|
+
ir?: ComponentIR,
|
|
283
284
|
): string {
|
|
284
285
|
if (!props) return ''
|
|
285
286
|
|
|
287
|
+
// A `{...props}` rest spread on a component means props that are NOT
|
|
288
|
+
// declared as named params don't get their own top-level Input struct
|
|
289
|
+
// field — they flow through the open-ended rest bag (`Props map[string]any`,
|
|
290
|
+
// the `Capitalize(restPropsName)` field). Without routing them there, a
|
|
291
|
+
// fixture passing e.g. `placeholder` to the `Input` component (whose only
|
|
292
|
+
// declared params are `className` / `type`) emits a top-level
|
|
293
|
+
// `Placeholder:` initializer and Go fails with `unknown field Placeholder
|
|
294
|
+
// in struct literal of type InputInput`. (#1467 Phase 2b)
|
|
295
|
+
const declaredParams = new Set((ir?.metadata.propsParams ?? []).map(p => p.name))
|
|
296
|
+
const restPropsName = ir?.metadata.restPropsName ?? null
|
|
297
|
+
const restBagField = restPropsName
|
|
298
|
+
? restPropsName.charAt(0).toUpperCase() + restPropsName.slice(1)
|
|
299
|
+
: null
|
|
300
|
+
|
|
286
301
|
const lines: string[] = []
|
|
302
|
+
const restBagEntries: Array<[string, unknown]> = []
|
|
287
303
|
for (const [key, value] of Object.entries(props)) {
|
|
288
304
|
// Skip internal hydration markers — `__instanceId` / `__bfScope`
|
|
289
305
|
// / `__bfChild` are routed by the framework (consumed via the
|
|
@@ -291,6 +307,15 @@ function buildGoPropsInit(
|
|
|
291
307
|
// appear on the user-facing input struct). Including them produces
|
|
292
308
|
// `unknown field __instanceId in struct literal of type XxxInput`.
|
|
293
309
|
if (key.startsWith('__')) continue
|
|
310
|
+
// A prop that isn't a declared named param on a rest-spread component
|
|
311
|
+
// belongs in the rest bag, not a top-level field. A key that literally
|
|
312
|
+
// matches `restPropsName` already carries a pre-formed bag object and
|
|
313
|
+
// maps straight onto the `Capitalize(restPropsName)` field, so it falls
|
|
314
|
+
// through to the normal (object → map literal) emit below.
|
|
315
|
+
if (restBagField && key !== restPropsName && !declaredParams.has(key)) {
|
|
316
|
+
restBagEntries.push([key, value])
|
|
317
|
+
continue
|
|
318
|
+
}
|
|
294
319
|
// Capitalize first letter for Go field name
|
|
295
320
|
const goField = key.charAt(0).toUpperCase() + key.slice(1)
|
|
296
321
|
if (typeof value === 'string') {
|
|
@@ -319,6 +344,13 @@ function buildGoPropsInit(
|
|
|
319
344
|
lines.push(`\t\t${goField}: ${goMapLiteralFromObject(value as Record<string, unknown>)},`)
|
|
320
345
|
}
|
|
321
346
|
}
|
|
347
|
+
// Emit the collected rest-bag entries as the open-ended bag field. Skip
|
|
348
|
+
// when a direct `restPropsName`-keyed prop already populated it above
|
|
349
|
+
// (merging two sources isn't a shape any fixture needs).
|
|
350
|
+
if (restBagField && restBagEntries.length > 0 && !(restPropsName! in props)) {
|
|
351
|
+
const bag = Object.fromEntries(restBagEntries)
|
|
352
|
+
lines.push(`\t\t${restBagField}: ${goMapLiteralFromObject(bag)},`)
|
|
353
|
+
}
|
|
322
354
|
return lines.join('\n')
|
|
323
355
|
}
|
|
324
356
|
|