@barefootjs/go-template 0.31.7 → 0.31.9
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 +63 -0
- package/dist/adapter/go-template-adapter.d.ts.map +1 -1
- package/dist/adapter/index.js +76 -8
- package/dist/adapter/lib/compile-state.d.ts +15 -0
- package/dist/adapter/lib/compile-state.d.ts.map +1 -1
- package/dist/adapter/props/prop-types.d.ts +25 -2
- package/dist/adapter/props/prop-types.d.ts.map +1 -1
- package/dist/adapter/type/type-codegen.d.ts.map +1 -1
- package/dist/index.js +77 -11
- package/dist/render-divergences.d.ts +8 -0
- package/dist/render-divergences.d.ts.map +1 -1
- package/dist/test-render.d.ts.map +1 -1
- package/dist/vite.js +104 -8
- package/package.json +5 -5
- package/src/__tests__/go-template-adapter.test.ts +178 -15
- package/src/adapter/go-template-adapter.ts +172 -2
- package/src/adapter/lib/compile-state.ts +16 -0
- package/src/adapter/props/prop-types.ts +29 -7
- package/src/adapter/type/type-codegen.ts +19 -2
- package/src/render-divergences.ts +9 -7
- package/src/test-render.ts +255 -51
package/dist/vite.js
CHANGED
|
@@ -3095,6 +3095,34 @@ var SVG_ROOT_TAGS = new Set([
|
|
|
3095
3095
|
"animateTransform",
|
|
3096
3096
|
"animateMotion"
|
|
3097
3097
|
]);
|
|
3098
|
+
var MATHML_ROOT_TAGS = new Set([
|
|
3099
|
+
"math",
|
|
3100
|
+
"mrow",
|
|
3101
|
+
"mfrac",
|
|
3102
|
+
"msup",
|
|
3103
|
+
"msub",
|
|
3104
|
+
"msubsup",
|
|
3105
|
+
"mn",
|
|
3106
|
+
"mi",
|
|
3107
|
+
"mo",
|
|
3108
|
+
"mtext",
|
|
3109
|
+
"munder",
|
|
3110
|
+
"mover",
|
|
3111
|
+
"munderover",
|
|
3112
|
+
"mtable",
|
|
3113
|
+
"mtr",
|
|
3114
|
+
"mtd",
|
|
3115
|
+
"msqrt",
|
|
3116
|
+
"mroot",
|
|
3117
|
+
"mstyle",
|
|
3118
|
+
"merror",
|
|
3119
|
+
"mpadded",
|
|
3120
|
+
"mphantom",
|
|
3121
|
+
"menclose",
|
|
3122
|
+
"semantics",
|
|
3123
|
+
"annotation",
|
|
3124
|
+
"annotation-xml"
|
|
3125
|
+
]);
|
|
3098
3126
|
|
|
3099
3127
|
// ../jsx/src/ir-to-client-js/collect-elements.ts
|
|
3100
3128
|
var EMPTY_RENDER_EXPRS = new Set(["null", "undefined", "false", "''", '""', "``"]);
|
|
@@ -5427,6 +5455,7 @@ class CompileState {
|
|
|
5427
5455
|
localTypeAliases = new Map;
|
|
5428
5456
|
localStructFields = new Map;
|
|
5429
5457
|
synthStructTypes = new Map;
|
|
5458
|
+
synthObjectStructNames = new Map;
|
|
5430
5459
|
needsStringsImport = false;
|
|
5431
5460
|
}
|
|
5432
5461
|
|
|
@@ -6075,8 +6104,10 @@ function typeInfoToGo(ctx, _typeInfo, defaultValue, preParsed) {
|
|
|
6075
6104
|
return `[]${typeInfoToGo(ctx, typeInfo.elementType)}`;
|
|
6076
6105
|
}
|
|
6077
6106
|
return "[]interface{}";
|
|
6078
|
-
case "object":
|
|
6079
|
-
|
|
6107
|
+
case "object": {
|
|
6108
|
+
const synthName = ctx.state.synthObjectStructNames.get(typeInfo);
|
|
6109
|
+
return synthName ?? "map[string]interface{}";
|
|
6110
|
+
}
|
|
6080
6111
|
case "interface":
|
|
6081
6112
|
if (typeInfo.raw && (ctx.state.localStructFields.has(typeInfo.raw) || ctx.state.localTypeAliases.has(typeInfo.raw))) {
|
|
6082
6113
|
return typeInfo.raw;
|
|
@@ -7622,11 +7653,11 @@ function buildPropTypeOverrides(ctx, ir) {
|
|
|
7622
7653
|
if (!param)
|
|
7623
7654
|
continue;
|
|
7624
7655
|
const propGoType = typeInfoToGo(ctx, param.type, param.defaultValue, param.parsed);
|
|
7625
|
-
|
|
7626
|
-
|
|
7627
|
-
|
|
7628
|
-
|
|
7629
|
-
|
|
7656
|
+
const signalGoType = typeInfoToGo(ctx, signal.type, signal.initialValue, signal.parsed);
|
|
7657
|
+
if (signalGoType.includes("interface{}"))
|
|
7658
|
+
continue;
|
|
7659
|
+
if (propGoType.includes("interface{}") || signalGoType !== propGoType) {
|
|
7660
|
+
overrides.set(propName, signalGoType);
|
|
7630
7661
|
}
|
|
7631
7662
|
}
|
|
7632
7663
|
}
|
|
@@ -7856,6 +7887,11 @@ function collectStringValueNames(ir) {
|
|
|
7856
7887
|
}
|
|
7857
7888
|
|
|
7858
7889
|
// src/adapter/go-template-adapter.ts
|
|
7890
|
+
var SYNTH_TYPE_LOC = {
|
|
7891
|
+
file: "<synthesized>",
|
|
7892
|
+
start: { line: 0, column: 0 },
|
|
7893
|
+
end: { line: 0, column: 0 }
|
|
7894
|
+
};
|
|
7859
7895
|
var STRING_METHODS = new Set([
|
|
7860
7896
|
"replace",
|
|
7861
7897
|
"trim",
|
|
@@ -8217,6 +8253,7 @@ ${scriptRegistrations}${templateBody}
|
|
|
8217
8253
|
const lines = [];
|
|
8218
8254
|
const componentName = ir.metadata.componentName;
|
|
8219
8255
|
this.buildLocalTypeTables(ir, componentName);
|
|
8256
|
+
this.emitSynthPropStructs(lines, ir, componentName);
|
|
8220
8257
|
this.emitLocalTypeStructs(lines, ir, componentName);
|
|
8221
8258
|
this.emitSynthStructs(lines, ir, componentName);
|
|
8222
8259
|
const nestedComponents = findNestedComponents(ir.root);
|
|
@@ -9076,6 +9113,65 @@ ${goFields.join(`
|
|
|
9076
9113
|
}
|
|
9077
9114
|
}
|
|
9078
9115
|
}
|
|
9116
|
+
emitSynthPropStructs(lines, ir, componentName) {
|
|
9117
|
+
this.state.synthObjectStructNames = new Map;
|
|
9118
|
+
this.state.currentTypeDefinitions = [...this.state.currentTypeDefinitions];
|
|
9119
|
+
const visitObject = (typeInfo, desiredName) => {
|
|
9120
|
+
if (this.state.synthObjectStructNames.has(typeInfo))
|
|
9121
|
+
return;
|
|
9122
|
+
if (this.state.localTypeNames.has(desiredName))
|
|
9123
|
+
return;
|
|
9124
|
+
this.state.localTypeNames.add(desiredName);
|
|
9125
|
+
this.state.synthObjectStructNames.set(typeInfo, desiredName);
|
|
9126
|
+
for (const prop of typeInfo.properties ?? []) {
|
|
9127
|
+
visit(prop.type, desiredName, prop.name);
|
|
9128
|
+
}
|
|
9129
|
+
const fields = this.structFieldsFor(typeInfo);
|
|
9130
|
+
this.state.localStructFields.set(desiredName, new Map(fields.map((f) => [f.tsName, f.goName])));
|
|
9131
|
+
this.state.currentTypeDefinitions.push({
|
|
9132
|
+
kind: "type",
|
|
9133
|
+
name: desiredName,
|
|
9134
|
+
definition: "",
|
|
9135
|
+
properties: typeInfo.properties ?? [],
|
|
9136
|
+
loc: SYNTH_TYPE_LOC
|
|
9137
|
+
});
|
|
9138
|
+
const goFields = fields.map((f) => ` ${f.goName} ${f.goType} \`json:"${this.toJsonTag(f.tsName)}"\``);
|
|
9139
|
+
lines.push(`// ${desiredName} is a synthesised type for an anonymous object type (#2674).`);
|
|
9140
|
+
lines.push(`type ${desiredName} struct {
|
|
9141
|
+
${goFields.join(`
|
|
9142
|
+
`)}
|
|
9143
|
+
}`);
|
|
9144
|
+
lines.push("");
|
|
9145
|
+
};
|
|
9146
|
+
const visitArrayElem = (elemType, parentName, propName) => {
|
|
9147
|
+
if (!elemType)
|
|
9148
|
+
return;
|
|
9149
|
+
if (elemType.kind === "array") {
|
|
9150
|
+
visitArrayElem(elemType.elementType, parentName, propName);
|
|
9151
|
+
} else if (elemType.kind === "object") {
|
|
9152
|
+
visitObject(elemType, `${parentName}${goFieldNameForKey(propName)}Item`);
|
|
9153
|
+
}
|
|
9154
|
+
};
|
|
9155
|
+
const visit = (typeInfo, parentName, propName) => {
|
|
9156
|
+
if (typeInfo.kind === "array") {
|
|
9157
|
+
visitArrayElem(typeInfo.elementType, parentName, propName);
|
|
9158
|
+
} else if (typeInfo.kind === "object") {
|
|
9159
|
+
visitObject(typeInfo, `${parentName}${goFieldNameForKey(propName)}`);
|
|
9160
|
+
}
|
|
9161
|
+
};
|
|
9162
|
+
for (const td of ir.metadata.typeDefinitions) {
|
|
9163
|
+
if (td.name === "Props" || td.name === `${componentName}Props`)
|
|
9164
|
+
continue;
|
|
9165
|
+
if (td.name.endsWith("Props"))
|
|
9166
|
+
continue;
|
|
9167
|
+
for (const prop of td.properties ?? []) {
|
|
9168
|
+
visit(prop.type, td.name, prop.name);
|
|
9169
|
+
}
|
|
9170
|
+
}
|
|
9171
|
+
for (const param of ir.metadata.propsParams) {
|
|
9172
|
+
visit(param.type, componentName, param.name);
|
|
9173
|
+
}
|
|
9174
|
+
}
|
|
9079
9175
|
emitLocalTypeStructs(lines, ir, componentName) {
|
|
9080
9176
|
for (const td of ir.metadata.typeDefinitions) {
|
|
9081
9177
|
if (td.name === "Props" || td.name === `${componentName}Props`)
|
|
@@ -9159,7 +9255,7 @@ ${goFields.join(`
|
|
|
9159
9255
|
emitPropsStructHeader(lines, ir, propsTypeName, componentName) {
|
|
9160
9256
|
lines.push(`// ${propsTypeName} is the props type for the ${componentName} component.`);
|
|
9161
9257
|
lines.push(`type ${propsTypeName} struct {`);
|
|
9162
|
-
lines.push('\tScopeID string `json:"
|
|
9258
|
+
lines.push('\tScopeID string `json:"-"`');
|
|
9163
9259
|
lines.push('\tBfIsRoot bool `json:"-"`');
|
|
9164
9260
|
lines.push('\tBfIsChild bool `json:"-"`');
|
|
9165
9261
|
lines.push('\tBfParent string `json:"-"`');
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@barefootjs/go-template",
|
|
3
|
-
"version": "0.31.
|
|
3
|
+
"version": "0.31.9",
|
|
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.31.
|
|
52
|
+
"@barefootjs/shared": "0.31.9"
|
|
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.31.
|
|
71
|
-
"@barefootjs/jsx": "0.31.
|
|
72
|
-
"@barefootjs/vite": "0.31.
|
|
70
|
+
"@barefootjs/client": "0.31.9",
|
|
71
|
+
"@barefootjs/jsx": "0.31.9",
|
|
72
|
+
"@barefootjs/vite": "0.31.9",
|
|
73
73
|
"vite": "^6.0.0"
|
|
74
74
|
}
|
|
75
75
|
}
|
|
@@ -1092,15 +1092,18 @@ export function Rows() {
|
|
|
1092
1092
|
expect(types).toContain('Rows: []Row{Row{DataID: "a"}},')
|
|
1093
1093
|
})
|
|
1094
1094
|
|
|
1095
|
-
test('bakes a non-Go-identifier key inside a nested INLINE object
|
|
1096
|
-
// A nested inline-object property (`meta: { 'data-x': string }`
|
|
1097
|
-
// named Go struct
|
|
1098
|
-
//
|
|
1099
|
-
//
|
|
1100
|
-
//
|
|
1101
|
-
//
|
|
1102
|
-
// `
|
|
1103
|
-
//
|
|
1095
|
+
test('bakes a non-Go-identifier key inside a nested INLINE object via the sanitizer-named synthesized struct (#2089 review, superseded by #2674)', () => {
|
|
1096
|
+
// A nested inline-object property (`meta: { 'data-x': string }`) used
|
|
1097
|
+
// to have no named Go struct and lower to `map[string]interface{}`,
|
|
1098
|
+
// baked as a Go map literal with a `goFieldNameForKey`-sanitized key
|
|
1099
|
+
// (`"DataX"`, not the invalid-accessor `"Data-x"` — #2089). #2674's
|
|
1100
|
+
// struct-synthesis pre-pass now gives it a real struct (`RowMeta`)
|
|
1101
|
+
// instead — the FIELD name still comes from the same sanitizer
|
|
1102
|
+
// (`goFieldNameForKey`), so the accessor (`.Meta.DataX`) and the json
|
|
1103
|
+
// tag (the ORIGINAL unsanitized source key, `"data-x"` — the struct
|
|
1104
|
+
// field's json tag, not the map-baking convention's capitalized key)
|
|
1105
|
+
// both still resolve correctly; only the container changed from a map
|
|
1106
|
+
// to a struct.
|
|
1104
1107
|
const adapter = new GoTemplateAdapter()
|
|
1105
1108
|
const ir = compileToIR(`
|
|
1106
1109
|
"use client"
|
|
@@ -1113,8 +1116,11 @@ export function Rows() {
|
|
|
1113
1116
|
}
|
|
1114
1117
|
`)
|
|
1115
1118
|
const types = adapter.generate(ir).types!
|
|
1116
|
-
expect(types).toContain('
|
|
1119
|
+
expect(types).toContain('type RowMeta struct {')
|
|
1120
|
+
expect(types).toContain('DataX string `json:"data-x"`')
|
|
1117
1121
|
expect(types).not.toContain('"Data-x"')
|
|
1122
|
+
expect(types).toContain('Row{ID: "r1", Meta: RowMeta{DataX: "v"}}')
|
|
1123
|
+
expect(types).not.toContain('map[string]interface{}')
|
|
1118
1124
|
})
|
|
1119
1125
|
|
|
1120
1126
|
test('snake_case keys keep their underscore in the generated field name (#2089 review)', () => {
|
|
@@ -5029,11 +5035,14 @@ export function TodoItem(props: Props) {
|
|
|
5029
5035
|
// field Done in type TodoItemProps`) or — since `bf_sort_eval`-style
|
|
5030
5036
|
// evaluators are untouched by this fix, only the html/template dot-path
|
|
5031
5037
|
// is — renders wrong. Post-fix: only the not-done todo (id 2) survives
|
|
5032
|
-
// the 'active' filter.
|
|
5033
|
-
|
|
5034
|
-
|
|
5035
|
-
|
|
5036
|
-
expect(
|
|
5038
|
+
// the 'active' filter. The bf-p hydration payload legitimately carries
|
|
5039
|
+
// the full unfiltered `initialTodos` (both quote styles, mirroring
|
|
5040
|
+
// `normalizeHTML`), so the assertions target the rendered list only.
|
|
5041
|
+
const rendered = html.replace(/\s*bf-p=(?:"[^"]*"|'[^']*')/g, '')
|
|
5042
|
+
expect(rendered).not.toContain('Eat breakfast')
|
|
5043
|
+
expect(rendered).toContain('Write tests')
|
|
5044
|
+
expect(rendered).toContain('data-key="2"')
|
|
5045
|
+
expect(rendered).not.toContain('data-key="1"')
|
|
5037
5046
|
})
|
|
5038
5047
|
})
|
|
5039
5048
|
|
|
@@ -5909,3 +5918,157 @@ export function Counter() {
|
|
|
5909
5918
|
expect(template).not.toContain('<link')
|
|
5910
5919
|
})
|
|
5911
5920
|
})
|
|
5921
|
+
|
|
5922
|
+
describe('GoTemplateAdapter - #2674 anonymous object types synthesize named structs', () => {
|
|
5923
|
+
// Plan A: a type with no name — an inline array-element type or a nested
|
|
5924
|
+
// anonymous property inside a named type — used to lower to
|
|
5925
|
+
// `map[string]interface{}` with DELIBERATELY PascalCased keys
|
|
5926
|
+
// (`bakeInlineObjectAsGoMap`, #2087/#1487: `html/template`'s dot access on
|
|
5927
|
+
// a map does an exact-string `MapIndex`). SSR rendered fine off that
|
|
5928
|
+
// convention, but `BfPropsAttr`'s `json.Marshal` ships the SAME map, so
|
|
5929
|
+
// the hydration payload leaked Go casing (`{"Name":"Ada"}` instead of
|
|
5930
|
+
// `{"name":"Ada"}`). `emitSynthPropStructs` now synthesizes a
|
|
5931
|
+
// deterministically-named, json-tagged struct for these types instead, so
|
|
5932
|
+
// the SAME value bakes as a typed struct literal and `json.Marshal`
|
|
5933
|
+
// produces the correct camelCase payload without changing SSR at all.
|
|
5934
|
+
|
|
5935
|
+
test('inline array-element type synthesizes a named json-tagged struct (case i)', () => {
|
|
5936
|
+
// `items: { id: number; tags: string[] }[]` has no backing
|
|
5937
|
+
// `TypeDefinition` at all — only `ir.metadata.propsParams`' own
|
|
5938
|
+
// `TypeInfo` tree carries its shape.
|
|
5939
|
+
const adapter = new GoTemplateAdapter()
|
|
5940
|
+
const ir = compileToIR(`
|
|
5941
|
+
function TaggedList(props: { items: { title: string; tags: string[] }[] }) {
|
|
5942
|
+
return <ul>{props.items.map((p) => <li key={p.title}>{p.title}</li>)}</ul>
|
|
5943
|
+
}
|
|
5944
|
+
export { TaggedList }
|
|
5945
|
+
`)
|
|
5946
|
+
const types = adapter.generateTypes(ir)!
|
|
5947
|
+
expect(types).toContain('type TaggedListItemsItem struct {')
|
|
5948
|
+
expect(types).toContain('Title string `json:"title"`')
|
|
5949
|
+
expect(types).toContain('Tags []string `json:"tags"`')
|
|
5950
|
+
// The Input/Props field is a typed struct slice, not the old
|
|
5951
|
+
// `[]map[string]interface{}` fallback.
|
|
5952
|
+
expect(types).toMatch(/Items \[\]TaggedListItemsItem/)
|
|
5953
|
+
expect(types).not.toContain('map[string]interface{}')
|
|
5954
|
+
})
|
|
5955
|
+
|
|
5956
|
+
test('nested anonymous property inside a NAMED type synthesizes a named struct (case ii, Row.user)', () => {
|
|
5957
|
+
// `Row` is a real `TypeDefinition`; its `user` property has no name of
|
|
5958
|
+
// its own — only `ir.metadata.typeDefinitions` carries `Row`'s property
|
|
5959
|
+
// list (a `{kind:'interface', raw:'Row'}` prop reference does not).
|
|
5960
|
+
const adapter = new GoTemplateAdapter()
|
|
5961
|
+
const ir = compileToIR(`
|
|
5962
|
+
"use client"
|
|
5963
|
+
import { createSignal } from "@barefootjs/client"
|
|
5964
|
+
|
|
5965
|
+
type Row = { id: string; user: { name: string } }
|
|
5966
|
+
export function NestedNames() {
|
|
5967
|
+
const [rows, setRows] = createSignal<Row[]>([
|
|
5968
|
+
{ id: "r1", user: { name: "Ada" } },
|
|
5969
|
+
{ id: "r2", user: { name: "Grace" } },
|
|
5970
|
+
])
|
|
5971
|
+
return (
|
|
5972
|
+
<ul onClick={() => setRows((r) => r)}>
|
|
5973
|
+
{rows().map(({ id, user: { name } }) => (
|
|
5974
|
+
<li key={id}>{name}</li>
|
|
5975
|
+
))}
|
|
5976
|
+
</ul>
|
|
5977
|
+
)
|
|
5978
|
+
}
|
|
5979
|
+
`)
|
|
5980
|
+
const types = adapter.generateTypes(ir)!
|
|
5981
|
+
expect(types).toContain('type RowUser struct {')
|
|
5982
|
+
expect(types).toContain('Name string `json:"name"`')
|
|
5983
|
+
expect(types).toContain('User RowUser `json:"user"`')
|
|
5984
|
+
// The signal's inline initial value bakes through the STRUCT literal
|
|
5985
|
+
// path, not `bakeInlineObjectAsGoMap`'s capitalized-key map convention.
|
|
5986
|
+
expect(types).toContain('Row{ID: "r1", User: RowUser{Name: "Ada"}}')
|
|
5987
|
+
expect(types).not.toContain('map[string]interface{}')
|
|
5988
|
+
})
|
|
5989
|
+
|
|
5990
|
+
test('a synthesized-name collision gracefully falls back to the pre-#2674 map convention, not a regression', () => {
|
|
5991
|
+
// `synthesizeStructFromSignal`'s existing collision precedent
|
|
5992
|
+
// (#1680, `keeps nil when the synthesised name collides with a user
|
|
5993
|
+
// type`), mirrored for `emitSynthPropStructs`: when the deterministic
|
|
5994
|
+
// name (`Row><Prop>`) is already taken by a real user type, synthesis
|
|
5995
|
+
// is declined for that ONE type and it keeps the historical map
|
|
5996
|
+
// fallback — SSR stays correct (`bakeInlineObjectAsGoMap` still bakes
|
|
5997
|
+
// it), only the hydration-payload casing fix doesn't apply to it.
|
|
5998
|
+
const adapter = new GoTemplateAdapter()
|
|
5999
|
+
const ir = compileToIR(`
|
|
6000
|
+
"use client"
|
|
6001
|
+
import { createSignal } from "@barefootjs/client"
|
|
6002
|
+
|
|
6003
|
+
type RowUser = { handle: string }
|
|
6004
|
+
type Row = { id: string; user: { name: string } }
|
|
6005
|
+
export function NestedNames() {
|
|
6006
|
+
const [rows] = createSignal<Row[]>([{ id: "r1", user: { name: "Ada" } }])
|
|
6007
|
+
return <ul>{rows().map(({ id, user: { name } }) => <li key={id}>{name}</li>)}</ul>
|
|
6008
|
+
}
|
|
6009
|
+
`)
|
|
6010
|
+
const types = adapter.generateTypes(ir)!
|
|
6011
|
+
// The user's own `RowUser` struct is emitted, untouched...
|
|
6012
|
+
expect(types).toContain('type RowUser struct {')
|
|
6013
|
+
expect(types).toContain('Handle string `json:"handle"`')
|
|
6014
|
+
// ...and `Row.user` — whose synthesized name collides with it — falls
|
|
6015
|
+
// back to the map convention rather than being silently mistyped as
|
|
6016
|
+
// the unrelated user type.
|
|
6017
|
+
expect(types).toMatch(/User map\[string\]interface\{\}/)
|
|
6018
|
+
expect(types).toContain('map[string]interface{}{"Name": "Ada"}')
|
|
6019
|
+
})
|
|
6020
|
+
|
|
6021
|
+
test('real go-run render: bf-p carries camelCase keys for an inline array-element prop (case i)', async () => {
|
|
6022
|
+
const source = `
|
|
6023
|
+
function TaggedList(props: { items: { title: string; tags: string[] }[] }) {
|
|
6024
|
+
return <ul>{props.items.map((p) => <li key={p.title}>{p.title}</li>)}</ul>
|
|
6025
|
+
}
|
|
6026
|
+
export { TaggedList }
|
|
6027
|
+
`
|
|
6028
|
+
let html: string
|
|
6029
|
+
try {
|
|
6030
|
+
html = await renderGoTemplateComponent({
|
|
6031
|
+
source,
|
|
6032
|
+
adapter: new GoTemplateAdapter(),
|
|
6033
|
+
props: { items: [{ title: 'Alpha', tags: ['a', 'b'] }] },
|
|
6034
|
+
})
|
|
6035
|
+
} catch (err) {
|
|
6036
|
+
if (err instanceof GoNotAvailableError) return // Go toolchain not installed on this host
|
|
6037
|
+
throw err
|
|
6038
|
+
}
|
|
6039
|
+
const bfPMatch = html.match(/bf-p="([^"]*)"/)
|
|
6040
|
+
expect(bfPMatch).not.toBeNull()
|
|
6041
|
+
const decoded = bfPMatch![1]
|
|
6042
|
+
.replace(/"/g, '"')
|
|
6043
|
+
.replace(/"/g, '"')
|
|
6044
|
+
const payload = JSON.parse(decoded)
|
|
6045
|
+
expect(payload).toEqual({ items: [{ title: 'Alpha', tags: ['a', 'b'] }] })
|
|
6046
|
+
})
|
|
6047
|
+
|
|
6048
|
+
test('real go-run render: bf-p carries camelCase keys for a nested anonymous object inside a named type (case ii)', async () => {
|
|
6049
|
+
const source = `
|
|
6050
|
+
"use client"
|
|
6051
|
+
import { createSignal } from "@barefootjs/client"
|
|
6052
|
+
|
|
6053
|
+
type Row = { id: string; user: { name: string } }
|
|
6054
|
+
export function NestedNames() {
|
|
6055
|
+
const [rows] = createSignal<Row[]>([{ id: "r1", user: { name: "Ada" } }])
|
|
6056
|
+
return <ul>{rows().map(({ id, user: { name } }) => <li key={id}>{name}</li>)}</ul>
|
|
6057
|
+
}
|
|
6058
|
+
`
|
|
6059
|
+
let html: string
|
|
6060
|
+
try {
|
|
6061
|
+
html = await renderGoTemplateComponent({ source, adapter: new GoTemplateAdapter() })
|
|
6062
|
+
} catch (err) {
|
|
6063
|
+
if (err instanceof GoNotAvailableError) return // Go toolchain not installed on this host
|
|
6064
|
+
throw err
|
|
6065
|
+
}
|
|
6066
|
+
const bfPMatch = html.match(/bf-p="([^"]*)"/)
|
|
6067
|
+
expect(bfPMatch).not.toBeNull()
|
|
6068
|
+
const decoded = bfPMatch![1]
|
|
6069
|
+
.replace(/"/g, '"')
|
|
6070
|
+
.replace(/"/g, '"')
|
|
6071
|
+
const payload = JSON.parse(decoded)
|
|
6072
|
+
expect(payload).toEqual({ rows: [{ id: 'r1', user: { name: 'Ada' } }] })
|
|
6073
|
+
})
|
|
6074
|
+
})
|
|
@@ -18,6 +18,7 @@ import type {
|
|
|
18
18
|
IRProp,
|
|
19
19
|
TypeInfo,
|
|
20
20
|
TypeDefinition,
|
|
21
|
+
PropertyInfo,
|
|
21
22
|
CompilerError,
|
|
22
23
|
SourceLocation,
|
|
23
24
|
ParsedExpr,
|
|
@@ -142,6 +143,20 @@ import { collectStringValueNames } from "./props/prop-classes.ts"
|
|
|
142
143
|
|
|
143
144
|
export type { GoTemplateAdapterOptions } from "./lib/types.ts"
|
|
144
145
|
|
|
146
|
+
/**
|
|
147
|
+
* Placeholder `SourceLocation` for a `TypeDefinition` `emitSynthPropStructs`
|
|
148
|
+
* (#2674) pushes onto `ctx.state.currentTypeDefinitions` for a synthesized
|
|
149
|
+
* anonymous-object struct. Never rendered or used for diagnostics — the
|
|
150
|
+
* synthesized entry exists only so `parsed-literal-to-go.ts`'s
|
|
151
|
+
* `structPropertyType` can look its properties up BY NAME the same way it
|
|
152
|
+
* looks up a real user type.
|
|
153
|
+
*/
|
|
154
|
+
const SYNTH_TYPE_LOC: SourceLocation = {
|
|
155
|
+
file: '<synthesized>',
|
|
156
|
+
start: { line: 0, column: 0 },
|
|
157
|
+
end: { line: 0, column: 0 },
|
|
158
|
+
}
|
|
159
|
+
|
|
145
160
|
/**
|
|
146
161
|
* Local re-materialisation of the (removed) `higher-order` ParsedExpr variant
|
|
147
162
|
* (#2018 P5). Predicate callback methods (`.filter`/`.find`/`.every`/…) now
|
|
@@ -1076,6 +1091,12 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
1076
1091
|
|
|
1077
1092
|
this.buildLocalTypeTables(ir, componentName)
|
|
1078
1093
|
|
|
1094
|
+
// #2674 Plan A: synthesize named structs for anonymous object types
|
|
1095
|
+
// BEFORE emitting named-type structs — a nested anonymous property
|
|
1096
|
+
// inside a named type (`Row.user`) needs its synthesized name registered
|
|
1097
|
+
// before `emitLocalTypeStructs` computes `Row`'s own struct fields.
|
|
1098
|
+
this.emitSynthPropStructs(lines, ir, componentName)
|
|
1099
|
+
|
|
1079
1100
|
this.emitLocalTypeStructs(lines, ir, componentName)
|
|
1080
1101
|
|
|
1081
1102
|
this.emitSynthStructs(lines, ir, componentName)
|
|
@@ -1295,8 +1316,14 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
1295
1316
|
* needs a real field to bake into, or the whole literal defers to nil); a
|
|
1296
1317
|
* dedup guard drops a later key that sanitizes to a Go name already taken
|
|
1297
1318
|
* (rare, but two fields can't share one Go identifier).
|
|
1319
|
+
*
|
|
1320
|
+
* Accepts anything carrying a `PropertyInfo[]` — a `TypeDefinition` (a
|
|
1321
|
+
* user-named type) OR a bare `TypeInfo` of `kind: 'object'` (an anonymous
|
|
1322
|
+
* type `emitSynthPropStructs` is synthesizing a struct for, #2674) — so
|
|
1323
|
+
* both the named-type struct emitter and the anonymous-type synthesis
|
|
1324
|
+
* pre-pass share one field-derivation path.
|
|
1298
1325
|
*/
|
|
1299
|
-
private structFieldsFor(td:
|
|
1326
|
+
private structFieldsFor(td: { properties?: PropertyInfo[] }): Array<{ tsName: string; goName: string; goType: string }> {
|
|
1300
1327
|
const fields: Array<{ tsName: string; goName: string; goType: string }> = []
|
|
1301
1328
|
const seenGoNames = new Set<string>()
|
|
1302
1329
|
for (const prop of td.properties ?? []) {
|
|
@@ -2450,6 +2477,142 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
2450
2477
|
}
|
|
2451
2478
|
}
|
|
2452
2479
|
|
|
2480
|
+
/**
|
|
2481
|
+
* #2674 Plan A: synthesize a deterministically-named, json-tagged struct
|
|
2482
|
+
* for every ANONYMOUS (`kind: 'object'`) type reachable from the
|
|
2483
|
+
* component's type surface, so `typeInfoToGo`'s `'object'` case (consulted
|
|
2484
|
+
* by `emitLocalTypeStructs`/`generateInputStruct` right after this runs)
|
|
2485
|
+
* resolves a real struct instead of falling to `map[string]interface{}` —
|
|
2486
|
+
* the map `bakeInlineObjectAsGoMap` (`parsed-literal-to-go.ts`) bakes with
|
|
2487
|
+
* DELIBERATELY PascalCased keys for `html/template`'s exact-case
|
|
2488
|
+
* `MapIndex` (#2087/#1487), which `BfPropsAttr`'s `json.Marshal` then
|
|
2489
|
+
* ships verbatim — the source of the hydration-payload Go-casing leak
|
|
2490
|
+
* this pass closes.
|
|
2491
|
+
*
|
|
2492
|
+
* Two independent walk roots, because a NAMED type reference
|
|
2493
|
+
* (`{kind:'interface', raw:'Row'}`) carries no inline `properties` — only
|
|
2494
|
+
* `ir.metadata.typeDefinitions` has `Row`'s own property list:
|
|
2495
|
+
*
|
|
2496
|
+
* 1. Every user `TypeDefinition`'s properties (closes a nested anonymous
|
|
2497
|
+
* property inside a named type — `type Row = { id: string; user: {
|
|
2498
|
+
* name: string } }` → `Row.user`). MUST run before
|
|
2499
|
+
* `emitLocalTypeStructs`: `Row`'s own struct-field computation
|
|
2500
|
+
* (`typeDefinitionToGo` → `structFieldsFor` → `typeInfoToGo`) needs
|
|
2501
|
+
* the synthesized name for its `user` field already registered.
|
|
2502
|
+
* 2. Every props param's own `TypeInfo` tree (closes an inline
|
|
2503
|
+
* array-element type with no backing `TypeDefinition` at all —
|
|
2504
|
+
* `items: { id: number; tags: string[] }[]`).
|
|
2505
|
+
*
|
|
2506
|
+
* Naming is deterministic on STRUCTURAL POSITION (matching
|
|
2507
|
+
* `synthesizeStructFromSignal`'s `<component><Getter>Item` convention, not
|
|
2508
|
+
* shape/content — two anonymous types shaped identically at different
|
|
2509
|
+
* positions get different names, and the same position always yields the
|
|
2510
|
+
* same name run-to-run): an array-element object gets
|
|
2511
|
+
* `<parent><Prop>Item`; a direct nested object property gets
|
|
2512
|
+
* `<parent><Prop>`. `<parent>` is the enclosing struct's OWN Go name for a
|
|
2513
|
+
* walk-root-1 type (or the newly-synthesized name of an enclosing
|
|
2514
|
+
* anonymous type, for a doubly-nested object — `RowUserAddress`), or
|
|
2515
|
+
* `componentName` for a walk-root-2 (top-level props) type — threaded
|
|
2516
|
+
* through the recursion so nesting chains correctly regardless of which
|
|
2517
|
+
* root reached it.
|
|
2518
|
+
*
|
|
2519
|
+
* A synthesized name colliding with an EXISTING local type (rare: two
|
|
2520
|
+
* structurally-unrelated anonymous types resolving to the same
|
|
2521
|
+
* deterministic name) skips synthesis for that ONE type — and its own
|
|
2522
|
+
* subtree, since there is no struct to attach nested field names to —
|
|
2523
|
+
* gracefully, not as a regression: `typeInfoToGo` keeps returning the
|
|
2524
|
+
* pre-#2674 map fallback for exactly that type, so the corpus never
|
|
2525
|
+
* breaks, it just doesn't graduate for that one shape (see the
|
|
2526
|
+
* `synthObjectStructNames` docstring on `CompileState`).
|
|
2527
|
+
*
|
|
2528
|
+
* A synthesized struct is ALSO pushed onto `ctx.state.currentTypeDefinitions`
|
|
2529
|
+
* as a `TypeDefinition` (empty `definition`, a dummy `loc` — never
|
|
2530
|
+
* rendered or used for diagnostics, only looked up by name) so
|
|
2531
|
+
* `parsed-literal-to-go.ts`'s `structPropertyType` — which resolves a
|
|
2532
|
+
* struct literal's nested-property TYPE by struct name against that same
|
|
2533
|
+
* list — finds a synthesized parent's properties exactly like it finds a
|
|
2534
|
+
* real named type's, with no separate lookup path to keep in sync.
|
|
2535
|
+
*/
|
|
2536
|
+
private emitSynthPropStructs(lines: string[], ir: ComponentIR, componentName: string): void {
|
|
2537
|
+
this.state.synthObjectStructNames = new Map<TypeInfo, string>()
|
|
2538
|
+
// `primeCompileState` assigns `currentTypeDefinitions` the SAME array
|
|
2539
|
+
// reference as `ir.metadata.typeDefinitions` (no clone) — copy before
|
|
2540
|
+
// pushing synthesized entries onto it below, so this per-compile
|
|
2541
|
+
// scratch state never mutates the IR's own metadata (which could be
|
|
2542
|
+
// compiled again, or read by another consumer sharing the same IR).
|
|
2543
|
+
this.state.currentTypeDefinitions = [...this.state.currentTypeDefinitions]
|
|
2544
|
+
|
|
2545
|
+
const visitObject = (typeInfo: TypeInfo, desiredName: string): void => {
|
|
2546
|
+
// Identity guard: the exact same anonymous TypeInfo object reached via
|
|
2547
|
+
// both walk roots (defensive — not expected given how the analyzer
|
|
2548
|
+
// builds distinct TypeInfo instances per source occurrence).
|
|
2549
|
+
if (this.state.synthObjectStructNames.has(typeInfo)) return
|
|
2550
|
+
// Name-collision guard: graceful fallback to the map convention for
|
|
2551
|
+
// just this type (see docstring above).
|
|
2552
|
+
if (this.state.localTypeNames.has(desiredName)) return
|
|
2553
|
+
this.state.localTypeNames.add(desiredName)
|
|
2554
|
+
this.state.synthObjectStructNames.set(typeInfo, desiredName)
|
|
2555
|
+
// Register nested children FIRST (depth-first) so this struct's OWN
|
|
2556
|
+
// field-type resolution below (`structFieldsFor` → `typeInfoToGo`)
|
|
2557
|
+
// sees synthesized names for any of ITS OWN nested object /
|
|
2558
|
+
// array-of-object properties instead of racing ahead of them.
|
|
2559
|
+
for (const prop of typeInfo.properties ?? []) {
|
|
2560
|
+
visit(prop.type, desiredName, prop.name)
|
|
2561
|
+
}
|
|
2562
|
+
const fields = this.structFieldsFor(typeInfo)
|
|
2563
|
+
this.state.localStructFields.set(desiredName, new Map(fields.map(f => [f.tsName, f.goName])))
|
|
2564
|
+
this.state.currentTypeDefinitions.push({
|
|
2565
|
+
kind: 'type',
|
|
2566
|
+
name: desiredName,
|
|
2567
|
+
definition: '',
|
|
2568
|
+
properties: typeInfo.properties ?? [],
|
|
2569
|
+
loc: SYNTH_TYPE_LOC,
|
|
2570
|
+
})
|
|
2571
|
+
const goFields = fields.map(
|
|
2572
|
+
f => `\t${f.goName} ${f.goType} \`json:"${this.toJsonTag(f.tsName)}"\``,
|
|
2573
|
+
)
|
|
2574
|
+
lines.push(`// ${desiredName} is a synthesised type for an anonymous object type (#2674).`)
|
|
2575
|
+
lines.push(`type ${desiredName} struct {\n${goFields.join('\n')}\n}`)
|
|
2576
|
+
lines.push('')
|
|
2577
|
+
}
|
|
2578
|
+
|
|
2579
|
+
const visitArrayElem = (elemType: TypeInfo | undefined, parentName: string, propName: string): void => {
|
|
2580
|
+
if (!elemType) return
|
|
2581
|
+
if (elemType.kind === 'array') {
|
|
2582
|
+
// Array-of-array (`matrix: {id:number}[][]`): keep the same
|
|
2583
|
+
// parent/prop naming context at every depth — rare shape, not one
|
|
2584
|
+
// the two documented #2674 cases exercise, so this just needs to
|
|
2585
|
+
// stay deterministic and non-colliding, not maximally descriptive.
|
|
2586
|
+
visitArrayElem(elemType.elementType, parentName, propName)
|
|
2587
|
+
} else if (elemType.kind === 'object') {
|
|
2588
|
+
visitObject(elemType, `${parentName}${goFieldNameForKey(propName)}Item`)
|
|
2589
|
+
}
|
|
2590
|
+
}
|
|
2591
|
+
|
|
2592
|
+
const visit = (typeInfo: TypeInfo, parentName: string, propName: string): void => {
|
|
2593
|
+
if (typeInfo.kind === 'array') {
|
|
2594
|
+
visitArrayElem(typeInfo.elementType, parentName, propName)
|
|
2595
|
+
} else if (typeInfo.kind === 'object') {
|
|
2596
|
+
visitObject(typeInfo, `${parentName}${goFieldNameForKey(propName)}`)
|
|
2597
|
+
}
|
|
2598
|
+
}
|
|
2599
|
+
|
|
2600
|
+
// Walk root 1: named types' own properties (closes `Row.user`).
|
|
2601
|
+
for (const td of ir.metadata.typeDefinitions) {
|
|
2602
|
+
if (td.name === 'Props' || td.name === `${componentName}Props`) continue
|
|
2603
|
+
if (td.name.endsWith('Props')) continue
|
|
2604
|
+
for (const prop of td.properties ?? []) {
|
|
2605
|
+
visit(prop.type, td.name, prop.name)
|
|
2606
|
+
}
|
|
2607
|
+
}
|
|
2608
|
+
|
|
2609
|
+
// Walk root 2: inline prop types with no backing TypeDefinition (closes
|
|
2610
|
+
// `items: { id: number; tags: string[] }[]`).
|
|
2611
|
+
for (const param of ir.metadata.propsParams) {
|
|
2612
|
+
visit(param.type, componentName, param.name)
|
|
2613
|
+
}
|
|
2614
|
+
}
|
|
2615
|
+
|
|
2453
2616
|
private emitLocalTypeStructs(lines: string[], ir: ComponentIR, componentName: string): void {
|
|
2454
2617
|
for (const td of ir.metadata.typeDefinitions) {
|
|
2455
2618
|
if (td.name === 'Props' || td.name === `${componentName}Props`) continue
|
|
@@ -2552,7 +2715,14 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
2552
2715
|
private emitPropsStructHeader(lines: string[], ir: ComponentIR, propsTypeName: string, componentName: string): void {
|
|
2553
2716
|
lines.push(`// ${propsTypeName} is the props type for the ${componentName} component.`)
|
|
2554
2717
|
lines.push(`type ${propsTypeName} struct {`)
|
|
2555
|
-
|
|
2718
|
+
// Internal scope id: used by the Go template (`{{.ScopeID}}`) to render bf-s
|
|
2719
|
+
// markers, but no client runtime consumer ever reads it back out of the
|
|
2720
|
+
// hydration bf-p JSON (audited: the only bf-p parser is
|
|
2721
|
+
// packages/client/src/runtime/hydrate.ts's parseProps/runInit, and nothing
|
|
2722
|
+
// downstream of it reads scopeID). Excluded from Marshal via `json:"-"` — Go's
|
|
2723
|
+
// json tag only affects (un)marshalling, not template field access, so
|
|
2724
|
+
// `{{.ScopeID}}` keeps working unchanged.
|
|
2725
|
+
lines.push('\tScopeID string `json:"-"`')
|
|
2556
2726
|
lines.push('\tBfIsRoot bool `json:"-"`')
|
|
2557
2727
|
lines.push('\tBfIsChild bool `json:"-"`')
|
|
2558
2728
|
// Slot identity for child scopes: host scope id + slot id. Emitted as bf-h /
|
|
@@ -242,6 +242,22 @@ export class CompileState {
|
|
|
242
242
|
*/
|
|
243
243
|
synthStructTypes: Map<string, TypeInfo> = new Map()
|
|
244
244
|
|
|
245
|
+
/**
|
|
246
|
+
* #2674 Plan A: ANONYMOUS (`kind: 'object'`) `TypeInfo` instance → the
|
|
247
|
+
* deterministically-named json-tagged struct `emitSynthPropStructs`
|
|
248
|
+
* synthesized for it, populated during generateTypes. Keyed by object
|
|
249
|
+
* IDENTITY, not name/shape: two anonymous object types at different
|
|
250
|
+
* structural positions (an inline array-element type vs. a nested
|
|
251
|
+
* property inside a named type) get different synthesized names even
|
|
252
|
+
* when shaped identically, so a content/shape key would wrongly unify
|
|
253
|
+
* them. `typeInfoToGo`'s `'object'` case consults this before falling
|
|
254
|
+
* back to `map[string]interface{}` — the map fallback stays reachable
|
|
255
|
+
* for the ONE case this pass declines: a synthesized name colliding with
|
|
256
|
+
* an existing local type (graceful, not a regression — see
|
|
257
|
+
* `emitSynthPropStructs`'s docstring).
|
|
258
|
+
*/
|
|
259
|
+
synthObjectStructNames: Map<TypeInfo, string> = new Map()
|
|
260
|
+
|
|
245
261
|
/** Set when a constructor-context lowering emits a `strings.` call, so
|
|
246
262
|
* `strings` is added to the generated types file's import block. */
|
|
247
263
|
needsStringsImport = false
|