@barefootjs/go-template 0.31.8 → 0.31.10
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 +71 -0
- package/dist/adapter/go-template-adapter.d.ts.map +1 -1
- package/dist/adapter/index.js +97 -15
- package/dist/adapter/lib/compile-state.d.ts +43 -1
- 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 +99 -16
- 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 +160 -17
- package/package.json +5 -5
- package/src/__tests__/go-template-adapter.test.ts +276 -16
- package/src/adapter/go-template-adapter.ts +249 -11
- package/src/adapter/lib/compile-state.ts +50 -0
- package/src/adapter/props/prop-types.ts +36 -8
- package/src/adapter/type/type-codegen.ts +19 -2
- package/src/render-divergences.ts +31 -5
- package/src/test-render.ts +273 -52
|
@@ -944,7 +944,8 @@ export function List() {
|
|
|
944
944
|
// A struct is synthesised with one field per inferred key + Go type.
|
|
945
945
|
expect(types).toMatch(/type \w+ struct \{[\s\S]*ID string[\s\S]*N int[\s\S]*Ok bool[\s\S]*\}/)
|
|
946
946
|
// The signal field is a slice of the synthesised struct, not []interface{}.
|
|
947
|
-
|
|
947
|
+
// Excluded from bf-p (json:"-") — component-internal signal state (#2672).
|
|
948
|
+
expect(types).toMatch(/Items \[\]\w+ `json:"-"`/)
|
|
948
949
|
expect(types).not.toContain('Items []interface{}')
|
|
949
950
|
// The initial items are baked, not nil.
|
|
950
951
|
expect(types).not.toContain('Items: nil,')
|
|
@@ -1092,15 +1093,18 @@ export function Rows() {
|
|
|
1092
1093
|
expect(types).toContain('Rows: []Row{Row{DataID: "a"}},')
|
|
1093
1094
|
})
|
|
1094
1095
|
|
|
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
|
-
//
|
|
1096
|
+
test('bakes a non-Go-identifier key inside a nested INLINE object via the sanitizer-named synthesized struct (#2089 review, superseded by #2674)', () => {
|
|
1097
|
+
// A nested inline-object property (`meta: { 'data-x': string }`) used
|
|
1098
|
+
// to have no named Go struct and lower to `map[string]interface{}`,
|
|
1099
|
+
// baked as a Go map literal with a `goFieldNameForKey`-sanitized key
|
|
1100
|
+
// (`"DataX"`, not the invalid-accessor `"Data-x"` — #2089). #2674's
|
|
1101
|
+
// struct-synthesis pre-pass now gives it a real struct (`RowMeta`)
|
|
1102
|
+
// instead — the FIELD name still comes from the same sanitizer
|
|
1103
|
+
// (`goFieldNameForKey`), so the accessor (`.Meta.DataX`) and the json
|
|
1104
|
+
// tag (the ORIGINAL unsanitized source key, `"data-x"` — the struct
|
|
1105
|
+
// field's json tag, not the map-baking convention's capitalized key)
|
|
1106
|
+
// both still resolve correctly; only the container changed from a map
|
|
1107
|
+
// to a struct.
|
|
1104
1108
|
const adapter = new GoTemplateAdapter()
|
|
1105
1109
|
const ir = compileToIR(`
|
|
1106
1110
|
"use client"
|
|
@@ -1113,8 +1117,11 @@ export function Rows() {
|
|
|
1113
1117
|
}
|
|
1114
1118
|
`)
|
|
1115
1119
|
const types = adapter.generate(ir).types!
|
|
1116
|
-
expect(types).toContain('
|
|
1120
|
+
expect(types).toContain('type RowMeta struct {')
|
|
1121
|
+
expect(types).toContain('DataX string `json:"data-x"`')
|
|
1117
1122
|
expect(types).not.toContain('"Data-x"')
|
|
1123
|
+
expect(types).toContain('Row{ID: "r1", Meta: RowMeta{DataX: "v"}}')
|
|
1124
|
+
expect(types).not.toContain('map[string]interface{}')
|
|
1118
1125
|
})
|
|
1119
1126
|
|
|
1120
1127
|
test('snake_case keys keep their underscore in the generated field name (#2089 review)', () => {
|
|
@@ -1692,6 +1699,56 @@ export function Box({ children }: { children: any }) {
|
|
|
1692
1699
|
const types = adapter.generateTypes(ir)!
|
|
1693
1700
|
expect(types).toMatch(/Children\s+\S+\s+`json:"-"`/)
|
|
1694
1701
|
})
|
|
1702
|
+
|
|
1703
|
+
test('signal and memo fields are excluded from bf-p serialization — user props only (#2672)', () => {
|
|
1704
|
+
// A signal-bearing component whose only DECLARED prop is `label`, plus
|
|
1705
|
+
// a signal (`count`, not itself a prop) and a memo derived from it
|
|
1706
|
+
// (`doubled`). Only `label` may carry a real json tag — `count` and
|
|
1707
|
+
// `doubled` are component-internal state/derivation the client
|
|
1708
|
+
// re-derives itself and never reads back off `_p`.
|
|
1709
|
+
const adapter = new GoTemplateAdapter()
|
|
1710
|
+
const ir = compileToIR(`
|
|
1711
|
+
'use client'
|
|
1712
|
+
import { createSignal, createMemo } from '@barefootjs/client'
|
|
1713
|
+
export function Counter(props: { label: string }) {
|
|
1714
|
+
const [count, setCount] = createSignal(0)
|
|
1715
|
+
const doubled = createMemo(() => count() * 2)
|
|
1716
|
+
return <button onClick={() => setCount(count() + 1)}>{props.label}: {count()} / {doubled()}</button>
|
|
1717
|
+
}
|
|
1718
|
+
`, adapter)
|
|
1719
|
+
const types = adapter.generateTypes(ir)!
|
|
1720
|
+
// The real prop keeps its real tag — hydration reads `_p.label`.
|
|
1721
|
+
expect(types).toMatch(/Label\s+\S+\s+`json:"label"`/)
|
|
1722
|
+
// The signal field is component-internal state, not caller input.
|
|
1723
|
+
expect(types).toMatch(/Count\s+\S+\s+`json:"-"`/)
|
|
1724
|
+
// The memo field is component-internal derivation, not caller input.
|
|
1725
|
+
expect(types).toMatch(/Doubled\s+\S+\s+`json:"-"`/)
|
|
1726
|
+
})
|
|
1727
|
+
|
|
1728
|
+
test('prop-backed signal default still keeps the PROP field a real tag (#2672)', () => {
|
|
1729
|
+
// Mirrors the `signal-default-from-jsx` adapter-tests fixture: `x` is a
|
|
1730
|
+
// declared PROP whose default happens to come from a signal initial
|
|
1731
|
+
// value (keeps a real tag, seeds client-side `createSignal(_p.x ?? 7)`),
|
|
1732
|
+
// while `incremented` is a pure memo derivation the client never reads
|
|
1733
|
+
// off `_p`. Flipping signal/memo fields to `json:"-"` must NOT touch
|
|
1734
|
+
// this prop field — it is the loop-guard case CLAUDE.md's "don't flip
|
|
1735
|
+
// prop-backed fields" warns against.
|
|
1736
|
+
const adapter = new GoTemplateAdapter()
|
|
1737
|
+
const ir = compileToIR(`
|
|
1738
|
+
'use client'
|
|
1739
|
+
import { createSignal, createMemo } from '@barefootjs/client'
|
|
1740
|
+
export function SignalDefaultFromJsx(props: { x?: number }) {
|
|
1741
|
+
const [x, setX] = createSignal(props.x ?? 7)
|
|
1742
|
+
const incremented = createMemo(() => x() + 1)
|
|
1743
|
+
return <div onClick={() => setX(x() + 1)}>{x()} / {incremented()}</div>
|
|
1744
|
+
}
|
|
1745
|
+
`, adapter)
|
|
1746
|
+
const types = adapter.generateTypes(ir)!
|
|
1747
|
+
// The prop-backed field keeps its real tag — hydration reads `_p.x`.
|
|
1748
|
+
expect(types).toMatch(/X\s+\S+\s+`json:"x"`/)
|
|
1749
|
+
// The memo field is component-internal derivation, not caller input.
|
|
1750
|
+
expect(types).toMatch(/Incremented\s+\S+\s+`json:"-"`/)
|
|
1751
|
+
})
|
|
1695
1752
|
})
|
|
1696
1753
|
|
|
1697
1754
|
describe('generateTypes', () => {
|
|
@@ -4854,6 +4911,39 @@ export function Counter({ count: initialCount }: { count: number }) {
|
|
|
4854
4911
|
expect((types.match(/json:"count"/g) ?? []).length).toBe(1)
|
|
4855
4912
|
})
|
|
4856
4913
|
|
|
4914
|
+
// #2672 near-miss: a prop-derived dynamic loop's nested-array field is
|
|
4915
|
+
// normally redundant (the client re-derives every row from the real prop
|
|
4916
|
+
// field, e.g. `_p.items`) and gets `json:"-"`. But when the array field's
|
|
4917
|
+
// Go name collides with its OWN driving prop's Go name — `toggleItems`
|
|
4918
|
+
// driving a `.map()` into `<ToggleItem>` both capitalize to `ToggleItems`
|
|
4919
|
+
// — `emitPropsDataFields` already shadows the prop's own field to avoid a
|
|
4920
|
+
// Go redeclaration, so the nested-array field is the ONLY struct field
|
|
4921
|
+
// carrying that prop's data. Flipping it to `-` there would silently drop
|
|
4922
|
+
// caller input from `bf-p` instead of trimming a redundant copy. Caught by
|
|
4923
|
+
// the `hydration-props-inventory` oracle against the `toggle-shared`
|
|
4924
|
+
// fixture (a real `integrations/shared/components/Toggle.tsx` shape)
|
|
4925
|
+
// before landing; this pins the same shape directly.
|
|
4926
|
+
test('a prop-derived nested-array field keeps a real tag when it shadows its own driving prop (#2672)', () => {
|
|
4927
|
+
const result = compileJSX(`
|
|
4928
|
+
type ToggleItemProps = { label: string; defaultOn?: boolean }
|
|
4929
|
+
function ToggleItem(props: ToggleItemProps) {
|
|
4930
|
+
return <div>{props.label}</div>
|
|
4931
|
+
}
|
|
4932
|
+
type ToggleProps = { toggleItems: ToggleItemProps[] }
|
|
4933
|
+
export function Toggle({ toggleItems }: ToggleProps) {
|
|
4934
|
+
return <div>{toggleItems.map((item) => <ToggleItem key={item.label} label={item.label} defaultOn={item.defaultOn} />)}</div>
|
|
4935
|
+
}
|
|
4936
|
+
`.trimStart(), 'test.tsx', { adapter: new GoTemplateAdapter(), outputIR: false })
|
|
4937
|
+
expect((result.errors ?? []).filter(e => e.code === 'BF101')).toHaveLength(0)
|
|
4938
|
+
const types = result.files.find(f => f.type === 'types')!.content
|
|
4939
|
+
const togglePropsAndAfter = types.slice(types.indexOf('type ToggleProps struct'))
|
|
4940
|
+
// The shadowed scalar prop field is absent...
|
|
4941
|
+
expect(togglePropsAndAfter.slice(0, togglePropsAndAfter.indexOf('}'))).not.toMatch(/\bItems\b/)
|
|
4942
|
+
// ...and the nested-array field is the sole carrier, with a REAL tag —
|
|
4943
|
+
// not `json:"-"`, which would drop `toggleItems` from `bf-p` entirely.
|
|
4944
|
+
expect((togglePropsAndAfter.match(/json:"toggleItems"/g) ?? []).length).toBe(1)
|
|
4945
|
+
})
|
|
4946
|
+
|
|
4857
4947
|
// The nested-array skip check must union both namings at all four sites
|
|
4858
4948
|
// (`isNestedArrayShadowed`) — a one-sided check lets Input and
|
|
4859
4949
|
// Props/NewProps disagree on whether an aliased prop's field exists.
|
|
@@ -5029,11 +5119,14 @@ export function TodoItem(props: Props) {
|
|
|
5029
5119
|
// field Done in type TodoItemProps`) or — since `bf_sort_eval`-style
|
|
5030
5120
|
// evaluators are untouched by this fix, only the html/template dot-path
|
|
5031
5121
|
// is — renders wrong. Post-fix: only the not-done todo (id 2) survives
|
|
5032
|
-
// the 'active' filter.
|
|
5033
|
-
|
|
5034
|
-
|
|
5035
|
-
|
|
5036
|
-
expect(
|
|
5122
|
+
// the 'active' filter. The bf-p hydration payload legitimately carries
|
|
5123
|
+
// the full unfiltered `initialTodos` (both quote styles, mirroring
|
|
5124
|
+
// `normalizeHTML`), so the assertions target the rendered list only.
|
|
5125
|
+
const rendered = html.replace(/\s*bf-p=(?:"[^"]*"|'[^']*')/g, '')
|
|
5126
|
+
expect(rendered).not.toContain('Eat breakfast')
|
|
5127
|
+
expect(rendered).toContain('Write tests')
|
|
5128
|
+
expect(rendered).toContain('data-key="2"')
|
|
5129
|
+
expect(rendered).not.toContain('data-key="1"')
|
|
5037
5130
|
})
|
|
5038
5131
|
})
|
|
5039
5132
|
|
|
@@ -5909,3 +6002,170 @@ export function Counter() {
|
|
|
5909
6002
|
expect(template).not.toContain('<link')
|
|
5910
6003
|
})
|
|
5911
6004
|
})
|
|
6005
|
+
|
|
6006
|
+
describe('GoTemplateAdapter - #2674 anonymous object types synthesize named structs', () => {
|
|
6007
|
+
// Plan A: a type with no name — an inline array-element type or a nested
|
|
6008
|
+
// anonymous property inside a named type — used to lower to
|
|
6009
|
+
// `map[string]interface{}` with DELIBERATELY PascalCased keys
|
|
6010
|
+
// (`bakeInlineObjectAsGoMap`, #2087/#1487: `html/template`'s dot access on
|
|
6011
|
+
// a map does an exact-string `MapIndex`). SSR rendered fine off that
|
|
6012
|
+
// convention, but `BfPropsAttr`'s `json.Marshal` ships the SAME map, so
|
|
6013
|
+
// the hydration payload leaked Go casing (`{"Name":"Ada"}` instead of
|
|
6014
|
+
// `{"name":"Ada"}`). `emitSynthPropStructs` now synthesizes a
|
|
6015
|
+
// deterministically-named, json-tagged struct for these types instead, so
|
|
6016
|
+
// the SAME value bakes as a typed struct literal and `json.Marshal`
|
|
6017
|
+
// produces the correct camelCase payload without changing SSR at all.
|
|
6018
|
+
|
|
6019
|
+
test('inline array-element type synthesizes a named json-tagged struct (case i)', () => {
|
|
6020
|
+
// `items: { id: number; tags: string[] }[]` has no backing
|
|
6021
|
+
// `TypeDefinition` at all — only `ir.metadata.propsParams`' own
|
|
6022
|
+
// `TypeInfo` tree carries its shape.
|
|
6023
|
+
const adapter = new GoTemplateAdapter()
|
|
6024
|
+
const ir = compileToIR(`
|
|
6025
|
+
function TaggedList(props: { items: { title: string; tags: string[] }[] }) {
|
|
6026
|
+
return <ul>{props.items.map((p) => <li key={p.title}>{p.title}</li>)}</ul>
|
|
6027
|
+
}
|
|
6028
|
+
export { TaggedList }
|
|
6029
|
+
`)
|
|
6030
|
+
const types = adapter.generateTypes(ir)!
|
|
6031
|
+
expect(types).toContain('type TaggedListItemsItem struct {')
|
|
6032
|
+
expect(types).toContain('Title string `json:"title"`')
|
|
6033
|
+
expect(types).toContain('Tags []string `json:"tags"`')
|
|
6034
|
+
// The Input/Props field is a typed struct slice, not the old
|
|
6035
|
+
// `[]map[string]interface{}` fallback.
|
|
6036
|
+
expect(types).toMatch(/Items \[\]TaggedListItemsItem/)
|
|
6037
|
+
expect(types).not.toContain('map[string]interface{}')
|
|
6038
|
+
})
|
|
6039
|
+
|
|
6040
|
+
test('nested anonymous property inside a NAMED type synthesizes a named struct (case ii, Row.user)', () => {
|
|
6041
|
+
// `Row` is a real `TypeDefinition`; its `user` property has no name of
|
|
6042
|
+
// its own — only `ir.metadata.typeDefinitions` carries `Row`'s property
|
|
6043
|
+
// list (a `{kind:'interface', raw:'Row'}` prop reference does not).
|
|
6044
|
+
const adapter = new GoTemplateAdapter()
|
|
6045
|
+
const ir = compileToIR(`
|
|
6046
|
+
"use client"
|
|
6047
|
+
import { createSignal } from "@barefootjs/client"
|
|
6048
|
+
|
|
6049
|
+
type Row = { id: string; user: { name: string } }
|
|
6050
|
+
export function NestedNames() {
|
|
6051
|
+
const [rows, setRows] = createSignal<Row[]>([
|
|
6052
|
+
{ id: "r1", user: { name: "Ada" } },
|
|
6053
|
+
{ id: "r2", user: { name: "Grace" } },
|
|
6054
|
+
])
|
|
6055
|
+
return (
|
|
6056
|
+
<ul onClick={() => setRows((r) => r)}>
|
|
6057
|
+
{rows().map(({ id, user: { name } }) => (
|
|
6058
|
+
<li key={id}>{name}</li>
|
|
6059
|
+
))}
|
|
6060
|
+
</ul>
|
|
6061
|
+
)
|
|
6062
|
+
}
|
|
6063
|
+
`)
|
|
6064
|
+
const types = adapter.generateTypes(ir)!
|
|
6065
|
+
expect(types).toContain('type RowUser struct {')
|
|
6066
|
+
expect(types).toContain('Name string `json:"name"`')
|
|
6067
|
+
expect(types).toContain('User RowUser `json:"user"`')
|
|
6068
|
+
// The signal's inline initial value bakes through the STRUCT literal
|
|
6069
|
+
// path, not `bakeInlineObjectAsGoMap`'s capitalized-key map convention.
|
|
6070
|
+
expect(types).toContain('Row{ID: "r1", User: RowUser{Name: "Ada"}}')
|
|
6071
|
+
expect(types).not.toContain('map[string]interface{}')
|
|
6072
|
+
})
|
|
6073
|
+
|
|
6074
|
+
test('a synthesized-name collision gracefully falls back to the pre-#2674 map convention, not a regression', () => {
|
|
6075
|
+
// `synthesizeStructFromSignal`'s existing collision precedent
|
|
6076
|
+
// (#1680, `keeps nil when the synthesised name collides with a user
|
|
6077
|
+
// type`), mirrored for `emitSynthPropStructs`: when the deterministic
|
|
6078
|
+
// name (`Row><Prop>`) is already taken by a real user type, synthesis
|
|
6079
|
+
// is declined for that ONE type and it keeps the historical map
|
|
6080
|
+
// fallback — SSR stays correct (`bakeInlineObjectAsGoMap` still bakes
|
|
6081
|
+
// it), only the hydration-payload casing fix doesn't apply to it.
|
|
6082
|
+
const adapter = new GoTemplateAdapter()
|
|
6083
|
+
const ir = compileToIR(`
|
|
6084
|
+
"use client"
|
|
6085
|
+
import { createSignal } from "@barefootjs/client"
|
|
6086
|
+
|
|
6087
|
+
type RowUser = { handle: string }
|
|
6088
|
+
type Row = { id: string; user: { name: string } }
|
|
6089
|
+
export function NestedNames() {
|
|
6090
|
+
const [rows] = createSignal<Row[]>([{ id: "r1", user: { name: "Ada" } }])
|
|
6091
|
+
return <ul>{rows().map(({ id, user: { name } }) => <li key={id}>{name}</li>)}</ul>
|
|
6092
|
+
}
|
|
6093
|
+
`)
|
|
6094
|
+
const types = adapter.generateTypes(ir)!
|
|
6095
|
+
// The user's own `RowUser` struct is emitted, untouched...
|
|
6096
|
+
expect(types).toContain('type RowUser struct {')
|
|
6097
|
+
expect(types).toContain('Handle string `json:"handle"`')
|
|
6098
|
+
// ...and `Row.user` — whose synthesized name collides with it — falls
|
|
6099
|
+
// back to the map convention rather than being silently mistyped as
|
|
6100
|
+
// the unrelated user type.
|
|
6101
|
+
expect(types).toMatch(/User map\[string\]interface\{\}/)
|
|
6102
|
+
expect(types).toContain('map[string]interface{}{"Name": "Ada"}')
|
|
6103
|
+
})
|
|
6104
|
+
|
|
6105
|
+
test('real go-run render: bf-p carries camelCase keys for an inline array-element prop (case i)', async () => {
|
|
6106
|
+
const source = `
|
|
6107
|
+
function TaggedList(props: { items: { title: string; tags: string[] }[] }) {
|
|
6108
|
+
return <ul>{props.items.map((p) => <li key={p.title}>{p.title}</li>)}</ul>
|
|
6109
|
+
}
|
|
6110
|
+
export { TaggedList }
|
|
6111
|
+
`
|
|
6112
|
+
let html: string
|
|
6113
|
+
try {
|
|
6114
|
+
html = await renderGoTemplateComponent({
|
|
6115
|
+
source,
|
|
6116
|
+
adapter: new GoTemplateAdapter(),
|
|
6117
|
+
props: { items: [{ title: 'Alpha', tags: ['a', 'b'] }] },
|
|
6118
|
+
})
|
|
6119
|
+
} catch (err) {
|
|
6120
|
+
if (err instanceof GoNotAvailableError) return // Go toolchain not installed on this host
|
|
6121
|
+
throw err
|
|
6122
|
+
}
|
|
6123
|
+
const bfPMatch = html.match(/bf-p="([^"]*)"/)
|
|
6124
|
+
expect(bfPMatch).not.toBeNull()
|
|
6125
|
+
const decoded = bfPMatch![1]
|
|
6126
|
+
.replace(/"/g, '"')
|
|
6127
|
+
.replace(/"/g, '"')
|
|
6128
|
+
const payload = JSON.parse(decoded)
|
|
6129
|
+
expect(payload).toEqual({ items: [{ title: 'Alpha', tags: ['a', 'b'] }] })
|
|
6130
|
+
})
|
|
6131
|
+
|
|
6132
|
+
test('real go-run render: a signal-backed nested anonymous object is baked into SSR HTML but excluded from bf-p (#2672)', async () => {
|
|
6133
|
+
// `rows` here is a SIGNAL, not a prop — `NestedNames` takes no props at
|
|
6134
|
+
// all. Its data still resolves the synthesized-struct-type branch of
|
|
6135
|
+
// `emitPropsDataFields` (the case ii struct-synthesis this describe
|
|
6136
|
+
// block exists to cover), so this doubles as a real-go-run confirmation
|
|
6137
|
+
// that branch's json tag is also `-` now, not just the general one.
|
|
6138
|
+
// `{{.Field}}` SSR access is unaffected by json tags — the rendered
|
|
6139
|
+
// `<li>Ada</li>` still bakes correctly; only `bf-p` (component-internal
|
|
6140
|
+
// signal state, not caller input) drops the redundant `rows` key.
|
|
6141
|
+
const source = `
|
|
6142
|
+
"use client"
|
|
6143
|
+
import { createSignal } from "@barefootjs/client"
|
|
6144
|
+
|
|
6145
|
+
type Row = { id: string; user: { name: string } }
|
|
6146
|
+
export function NestedNames() {
|
|
6147
|
+
const [rows] = createSignal<Row[]>([{ id: "r1", user: { name: "Ada" } }])
|
|
6148
|
+
return <ul>{rows().map(({ id, user: { name } }) => <li key={id}>{name}</li>)}</ul>
|
|
6149
|
+
}
|
|
6150
|
+
`
|
|
6151
|
+
let html: string
|
|
6152
|
+
try {
|
|
6153
|
+
html = await renderGoTemplateComponent({ source, adapter: new GoTemplateAdapter() })
|
|
6154
|
+
} catch (err) {
|
|
6155
|
+
if (err instanceof GoNotAvailableError) return // Go toolchain not installed on this host
|
|
6156
|
+
throw err
|
|
6157
|
+
}
|
|
6158
|
+
// SSR HTML still bakes the signal's initial data — unaffected by the
|
|
6159
|
+
// json tag, since `{{.Field}}` template access doesn't consult it.
|
|
6160
|
+
expect(html).toContain('<li')
|
|
6161
|
+
expect(html).toContain('Ada')
|
|
6162
|
+
const bfPMatch = html.match(/bf-p="([^"]*)"/)
|
|
6163
|
+
expect(bfPMatch).not.toBeNull()
|
|
6164
|
+
const decoded = bfPMatch![1]
|
|
6165
|
+
.replace(/"/g, '"')
|
|
6166
|
+
.replace(/"/g, '"')
|
|
6167
|
+
const payload = JSON.parse(decoded)
|
|
6168
|
+
// No props at all on this component — bf-p carries nothing.
|
|
6169
|
+
expect(payload).toEqual({})
|
|
6170
|
+
})
|
|
6171
|
+
})
|