@barefootjs/go-template 0.31.0 → 0.31.2
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 +49 -41
- package/dist/adapter/go-template-adapter.d.ts.map +1 -1
- package/dist/adapter/index.js +167 -101
- package/dist/adapter/lib/types.d.ts +4 -1
- package/dist/adapter/lib/types.d.ts.map +1 -1
- package/dist/adapter/memo/memo-compute.d.ts +9 -0
- package/dist/adapter/memo/memo-compute.d.ts.map +1 -1
- package/dist/adapter/memo/memo-type.d.ts +2 -0
- package/dist/adapter/memo/memo-type.d.ts.map +1 -1
- package/dist/adapter/props/prop-types.d.ts +2 -1
- package/dist/adapter/props/prop-types.d.ts.map +1 -1
- package/dist/adapter/spread/spread-codegen.d.ts.map +1 -1
- package/dist/adapter/type/type-codegen.d.ts +22 -5
- package/dist/adapter/type/type-codegen.d.ts.map +1 -1
- package/dist/adapter/value/parsed-literal-to-go.d.ts +12 -0
- package/dist/adapter/value/parsed-literal-to-go.d.ts.map +1 -1
- package/dist/adapter/value/value-lowering.d.ts +3 -0
- package/dist/adapter/value/value-lowering.d.ts.map +1 -1
- package/dist/index.js +168 -104
- package/dist/render-divergences.d.ts +6 -0
- package/dist/render-divergences.d.ts.map +1 -1
- package/dist/vite.js +493 -192
- package/package.json +5 -5
- package/src/__tests__/go-template-adapter.test.ts +155 -10
- package/src/adapter/go-template-adapter.ts +213 -128
- package/src/adapter/lib/types.ts +4 -1
- package/src/adapter/memo/memo-compute.ts +21 -17
- package/src/adapter/memo/memo-type.ts +5 -5
- package/src/adapter/props/prop-types.ts +6 -5
- package/src/adapter/spread/spread-codegen.ts +12 -5
- package/src/adapter/type/type-codegen.ts +86 -25
- package/src/adapter/value/parsed-literal-to-go.ts +28 -10
- package/src/adapter/value/value-lowering.ts +65 -29
- package/src/render-divergences.ts +6 -15
- package/src/test-render.ts +6 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@barefootjs/go-template",
|
|
3
|
-
"version": "0.31.
|
|
3
|
+
"version": "0.31.2",
|
|
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.2"
|
|
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.2",
|
|
71
|
+
"@barefootjs/jsx": "0.31.2",
|
|
72
|
+
"@barefootjs/vite": "0.31.2",
|
|
73
73
|
"vite": "^6.0.0"
|
|
74
74
|
}
|
|
75
75
|
}
|
|
@@ -4601,12 +4601,14 @@ export function SignalRowChildDerivedProp(props: { rows: Row[] }) {
|
|
|
4601
4601
|
})
|
|
4602
4602
|
|
|
4603
4603
|
// An ALIASED destructure (`{ n: count }`) is where the JSX attribute name
|
|
4604
|
-
// (`n` → `"N"`) and the child's own Go field (`Count`) diverge. #2457:
|
|
4605
|
-
//
|
|
4606
|
-
//
|
|
4607
|
-
//
|
|
4608
|
-
// (`
|
|
4609
|
-
//
|
|
4604
|
+
// (`n` → `"N"`) and the child's own Go PROPS field (`Count`) diverge. #2457:
|
|
4605
|
+
// the parent's call-site reconciliation (`loopRowChildPropOverrides`, via
|
|
4606
|
+
// `childPropFieldNames`'s `propsField`) still emits the child's PROPS field
|
|
4607
|
+
// directly, so the pipeline call is unaffected. #2525 separately makes the
|
|
4608
|
+
// child's own Input field CALLER-facing (`"N"`, for a caller-side composite
|
|
4609
|
+
// `BadgeInput{N: 5}` literal) — so the rebuilder's switch, unlike before,
|
|
4610
|
+
// once again needs BOTH names: the Props field as the case label (matching
|
|
4611
|
+
// the call site) and the Input field as the assignment target.
|
|
4610
4612
|
test('an aliased destructured prop maps the parent name onto the child field', () => {
|
|
4611
4613
|
const result = compileJSX(`
|
|
4612
4614
|
'use client'
|
|
@@ -4631,18 +4633,55 @@ export function AliasedRowChildDerivedProp(props: { rows: Row[] }) {
|
|
|
4631
4633
|
`.trimStart(), 'test.tsx', { adapter: new GoTemplateAdapter(), outputIR: false })
|
|
4632
4634
|
expect((result.errors ?? []).filter(e => e.code === 'BF101')).toHaveLength(0)
|
|
4633
4635
|
const template = result.files.find(f => f.type === 'markedTemplate')!.content
|
|
4634
|
-
// The parent now writes the child's
|
|
4635
|
-
//
|
|
4636
|
+
// The parent now writes the child's own PROPS field name ("Count") —
|
|
4637
|
+
// `bf_with_props`/`bf_reprops` patch the already-constructed Props
|
|
4638
|
+
// instance, so the call site stays keyed by the LOCAL binding.
|
|
4636
4639
|
expect(template).toContain('(bf_reprops "Badge" $.BadgeSlot0 "Text" .Label "Count" .N)')
|
|
4637
4640
|
const types = result.files.find(f => f.type === 'types')!.content
|
|
4641
|
+
// Case label: the PROPS field, matching the call site above.
|
|
4638
4642
|
expect(types).toContain('case "Count":')
|
|
4639
|
-
|
|
4643
|
+
// Assignment target: the child's own INPUT field — CALLER-facing (#2525),
|
|
4644
|
+
// so it's "N" here, not "Count". 437f822 removed this field/wire pairing
|
|
4645
|
+
// when Input and Props field names were unified everywhere; #2525
|
|
4646
|
+
// reintroduces it because they diverge again for an aliased prop.
|
|
4647
|
+
expect(types).toContain('&in.N,')
|
|
4648
|
+
expect(types).not.toContain('&in.Count,')
|
|
4640
4649
|
// "N" — the JSX attribute name — is never a case label: the switch is
|
|
4641
|
-
// keyed by the child's field on
|
|
4650
|
+
// keyed by the child's PROPS field on that side, and the parent already
|
|
4642
4651
|
// resolved the name before emitting the call.
|
|
4643
4652
|
expect(types).not.toContain('case "N":')
|
|
4644
4653
|
})
|
|
4645
4654
|
|
|
4655
|
+
// #2525: the Input struct is keyed by the CALLER-facing name
|
|
4656
|
+
// (`sourceName ?? name`), so `BadgeInput{N: 5}` — the name a handler
|
|
4657
|
+
// actually writes — type-checks. The Props struct's field stays the LOCAL
|
|
4658
|
+
// binding (what `{{.X}}` in the template executes against), and its json
|
|
4659
|
+
// tag flips to the caller-facing name to match the hydration payload key
|
|
4660
|
+
// the shared client JS reads (PR A, #2524, 2e40dc6). `NewBadgeProps`
|
|
4661
|
+
// bridges the two: `Count: in.N`.
|
|
4662
|
+
test('an aliased destructured prop keys the Input struct by the caller-facing name', () => {
|
|
4663
|
+
const result = compileJSX(`
|
|
4664
|
+
export function Badge({ text, n: count }: { text: string; n: number }) {
|
|
4665
|
+
return <span class="badge">{text}:{count}</span>
|
|
4666
|
+
}
|
|
4667
|
+
`.trimStart(), 'test.tsx', { adapter: new GoTemplateAdapter(), outputIR: false })
|
|
4668
|
+
expect((result.errors ?? []).filter(e => e.code === 'BF101')).toHaveLength(0)
|
|
4669
|
+
const types = result.files.find(f => f.type === 'types')!.content
|
|
4670
|
+
// Input struct: caller-facing field, no json tag (Input is never
|
|
4671
|
+
// marshalled).
|
|
4672
|
+
expect(types).toContain('type BadgeInput struct {')
|
|
4673
|
+
// Exact-field match (not `.toContain('N int')`, which also matches
|
|
4674
|
+
// `N interface{}`) — tightened to match the adjacent negative's style.
|
|
4675
|
+
expect(types).toContain('\tN int\n')
|
|
4676
|
+
expect(types).not.toContain('Count int\n')
|
|
4677
|
+
// Props struct: LOCAL field, json tag flipped to the caller-facing key.
|
|
4678
|
+
expect(types).toContain('type BadgeProps struct {')
|
|
4679
|
+
expect(types).toContain('Count int `json:"n"`')
|
|
4680
|
+
expect(types).not.toContain('json:"count"')
|
|
4681
|
+
// NewBadgeProps bridges Props(local) from Input(caller-facing).
|
|
4682
|
+
expect(types).toContain('Count: in.N,')
|
|
4683
|
+
})
|
|
4684
|
+
|
|
4646
4685
|
// #2457: the SAME aliased destructure, but with NO derived field — the
|
|
4647
4686
|
// shape `bf_with_props` mishandled even before #2448's rebuilder existed.
|
|
4648
4687
|
// `bf.WithProps` documents an unknown-field pair as a silent passthrough,
|
|
@@ -4786,6 +4825,112 @@ export function CompositeRowChildComponent(props: { items: Item[] }) {
|
|
|
4786
4825
|
})
|
|
4787
4826
|
})
|
|
4788
4827
|
|
|
4828
|
+
describe('GoTemplateAdapter - #2525 collision handling', () => {
|
|
4829
|
+
// A caller-facing prop tag and a same-named signal's LOCAL tag are
|
|
4830
|
+
// different Go identifiers wanting the same string, and
|
|
4831
|
+
// `encoding/json.Marshal` drops BOTH fields under an ambiguous tag — the
|
|
4832
|
+
// identifier de-dup sets cannot catch this (see `claimJsonTag`). The
|
|
4833
|
+
// prop must win: on hono only props occupy `_p`.
|
|
4834
|
+
test("an aliased prop's caller-facing json tag wins over a same-named signal's", () => {
|
|
4835
|
+
const result = compileJSX(`
|
|
4836
|
+
'use client'
|
|
4837
|
+
import { createSignal } from '@barefootjs/client'
|
|
4838
|
+
export function Counter({ count: initialCount }: { count: number }) {
|
|
4839
|
+
const [count, setCount] = createSignal(initialCount)
|
|
4840
|
+
return <button onClick={() => setCount(count() + 1)}>Count: {count()}</button>
|
|
4841
|
+
}
|
|
4842
|
+
`.trimStart(), 'test.tsx', { adapter: new GoTemplateAdapter(), outputIR: false })
|
|
4843
|
+
expect((result.errors ?? []).filter(e => e.code === 'BF101')).toHaveLength(0)
|
|
4844
|
+
const types = result.files.find(f => f.type === 'types')!.content
|
|
4845
|
+
// The prop keeps the shared "count" wire key...
|
|
4846
|
+
expect(types).toContain('InitialCount int `json:"count"`')
|
|
4847
|
+
// ...and the colliding signal field falls back to `json:"-"` instead of
|
|
4848
|
+
// silently carrying the same tag. Leading `\t` (not `.toContain('Count
|
|
4849
|
+
// int ...')`) so the assertion doesn't false-match inside
|
|
4850
|
+
// "InitialCount int ..." above.
|
|
4851
|
+
expect(types).toContain('\tCount int `json:"-"`')
|
|
4852
|
+
expect(types).not.toContain('\tCount int `json:"count"`')
|
|
4853
|
+
// Exactly one field claims "count".
|
|
4854
|
+
expect((types.match(/json:"count"/g) ?? []).length).toBe(1)
|
|
4855
|
+
})
|
|
4856
|
+
|
|
4857
|
+
// The nested-array skip check must union both namings at all four sites
|
|
4858
|
+
// (`isNestedArrayShadowed`) — a one-sided check lets Input and
|
|
4859
|
+
// Props/NewProps disagree on whether an aliased prop's field exists.
|
|
4860
|
+
test('a nested-array field name collision skips the prop everywhere ({ rows: items } direction)', () => {
|
|
4861
|
+
const result = compileJSX(`
|
|
4862
|
+
function Row(props: { label: string }) {
|
|
4863
|
+
return <li>{props.label}</li>
|
|
4864
|
+
}
|
|
4865
|
+
export function List({ rows: items }: { rows: { id: number; label: string }[] }) {
|
|
4866
|
+
return <ul>{items.map(item => <Row key={item.id} label={item.label} />)}</ul>
|
|
4867
|
+
}
|
|
4868
|
+
`.trimStart(), 'test.tsx', { adapter: new GoTemplateAdapter(), outputIR: false })
|
|
4869
|
+
expect((result.errors ?? []).filter(e => e.code === 'BF101')).toHaveLength(0)
|
|
4870
|
+
const types = result.files.find(f => f.type === 'types')!.content
|
|
4871
|
+
const listInput = types.slice(types.indexOf('type ListInput struct'), types.indexOf('type ListProps struct'))
|
|
4872
|
+
const listPropsAndAfter = types.slice(types.indexOf('type ListProps struct'))
|
|
4873
|
+
// Input carries only the typed nested-array field — no scalar "Items"
|
|
4874
|
+
// field for the aliased prop the LOCAL-only check used to miss.
|
|
4875
|
+
expect(listInput).toContain('Rows []RowInput')
|
|
4876
|
+
expect(listInput).not.toContain('Items')
|
|
4877
|
+
// Props carries exactly ONE `json:"rows"` tag (the nested array's) —
|
|
4878
|
+
// a surviving duplicate would make encoding/json drop both fields.
|
|
4879
|
+
expect((listPropsAndAfter.match(/json:"rows"/g) ?? []).length).toBe(1)
|
|
4880
|
+
expect(listPropsAndAfter).not.toContain('Items')
|
|
4881
|
+
})
|
|
4882
|
+
|
|
4883
|
+
// Same collision, opposite alias direction: the LOCAL name now matches
|
|
4884
|
+
// the nested-array field (so Props/NewProps's LOCAL-only check happened
|
|
4885
|
+
// to skip it by luck), but the CALLER name doesn't — Input's
|
|
4886
|
+
// caller-facing-only check used to keep a live "Items" field with no
|
|
4887
|
+
// Props counterpart, so a caller's override silently never reached the
|
|
4888
|
+
// constructed Props at all.
|
|
4889
|
+
test('a nested-array field name collision skips the prop everywhere ({ items: rows } direction)', () => {
|
|
4890
|
+
const result = compileJSX(`
|
|
4891
|
+
function Row(props: { label: string }) {
|
|
4892
|
+
return <li>{props.label}</li>
|
|
4893
|
+
}
|
|
4894
|
+
export function List({ items: rows }: { items: { id: number; label: string }[] }) {
|
|
4895
|
+
return <ul>{rows.map(item => <Row key={item.id} label={item.label} />)}</ul>
|
|
4896
|
+
}
|
|
4897
|
+
`.trimStart(), 'test.tsx', { adapter: new GoTemplateAdapter(), outputIR: false })
|
|
4898
|
+
expect((result.errors ?? []).filter(e => e.code === 'BF101')).toHaveLength(0)
|
|
4899
|
+
const types = result.files.find(f => f.type === 'types')!.content
|
|
4900
|
+
const listInput = types.slice(types.indexOf('type ListInput struct'), types.indexOf('type ListProps struct'))
|
|
4901
|
+
// Input carries only the typed nested-array field — no dead "Items"
|
|
4902
|
+
// field the caller could write to with no effect.
|
|
4903
|
+
expect(listInput).toContain('Rows []RowInput')
|
|
4904
|
+
expect(listInput).not.toContain('Items')
|
|
4905
|
+
expect(types).not.toContain('in.Items')
|
|
4906
|
+
})
|
|
4907
|
+
|
|
4908
|
+
// `usesSearchParams` gates the field in Input (caller-facing names),
|
|
4909
|
+
// Props (LOCAL) and NewProps with ONE boolean, so its collision check
|
|
4910
|
+
// must union both namings — `{ q: searchParams }` collides only under
|
|
4911
|
+
// the LOCAL one, and a one-sided check redeclares the Go field.
|
|
4912
|
+
test('an aliased prop whose LOCAL binding is `searchParams` does not redeclare the field', () => {
|
|
4913
|
+
const result = compileJSX(`
|
|
4914
|
+
'use client'
|
|
4915
|
+
import { createSearchParams } from '@barefootjs/client'
|
|
4916
|
+
export function Foo({ q: searchParams }: { q: string }) {
|
|
4917
|
+
const [sp] = createSearchParams()
|
|
4918
|
+
return <p>{searchParams}:{sp().get('tag') ?? 'none'}</p>
|
|
4919
|
+
}
|
|
4920
|
+
`.trimStart(), 'test.tsx', { adapter: new GoTemplateAdapter(), outputIR: false })
|
|
4921
|
+
expect((result.errors ?? []).filter(e => e.code === 'BF101')).toHaveLength(0)
|
|
4922
|
+
const types = result.files.find(f => f.type === 'types')!.content
|
|
4923
|
+
const structStart = types.indexOf('type FooProps struct')
|
|
4924
|
+
const fooPropsBody = types.slice(structStart, types.indexOf('}', structStart))
|
|
4925
|
+
// Exactly one "SearchParams" field in the struct body — the prop's
|
|
4926
|
+
// own — never a second `SearchParams bf.SearchParams` reader field
|
|
4927
|
+
// colliding with it (a Go redeclaration error).
|
|
4928
|
+
expect((fooPropsBody.match(/SearchParams/g) ?? []).length).toBe(1)
|
|
4929
|
+
expect(fooPropsBody).toContain('SearchParams string `json:"q"`')
|
|
4930
|
+
expect(types).not.toContain('bf.SearchParams')
|
|
4931
|
+
})
|
|
4932
|
+
})
|
|
4933
|
+
|
|
4789
4934
|
// #2228: a `.filter(t => …).map(todo => <Child todo={todo} .../>)` loop whose
|
|
4790
4935
|
// body is a single child component ranges the WRAPPER slice (`.TodoItems`,
|
|
4791
4936
|
// `.{ChildName}s` — see #2130 above), so `{{if}}`'s dot context for the
|