@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.
Files changed (35) hide show
  1. package/dist/adapter/go-template-adapter.d.ts +49 -41
  2. package/dist/adapter/go-template-adapter.d.ts.map +1 -1
  3. package/dist/adapter/index.js +167 -101
  4. package/dist/adapter/lib/types.d.ts +4 -1
  5. package/dist/adapter/lib/types.d.ts.map +1 -1
  6. package/dist/adapter/memo/memo-compute.d.ts +9 -0
  7. package/dist/adapter/memo/memo-compute.d.ts.map +1 -1
  8. package/dist/adapter/memo/memo-type.d.ts +2 -0
  9. package/dist/adapter/memo/memo-type.d.ts.map +1 -1
  10. package/dist/adapter/props/prop-types.d.ts +2 -1
  11. package/dist/adapter/props/prop-types.d.ts.map +1 -1
  12. package/dist/adapter/spread/spread-codegen.d.ts.map +1 -1
  13. package/dist/adapter/type/type-codegen.d.ts +22 -5
  14. package/dist/adapter/type/type-codegen.d.ts.map +1 -1
  15. package/dist/adapter/value/parsed-literal-to-go.d.ts +12 -0
  16. package/dist/adapter/value/parsed-literal-to-go.d.ts.map +1 -1
  17. package/dist/adapter/value/value-lowering.d.ts +3 -0
  18. package/dist/adapter/value/value-lowering.d.ts.map +1 -1
  19. package/dist/index.js +168 -104
  20. package/dist/render-divergences.d.ts +6 -0
  21. package/dist/render-divergences.d.ts.map +1 -1
  22. package/dist/vite.js +493 -192
  23. package/package.json +5 -5
  24. package/src/__tests__/go-template-adapter.test.ts +155 -10
  25. package/src/adapter/go-template-adapter.ts +213 -128
  26. package/src/adapter/lib/types.ts +4 -1
  27. package/src/adapter/memo/memo-compute.ts +21 -17
  28. package/src/adapter/memo/memo-type.ts +5 -5
  29. package/src/adapter/props/prop-types.ts +6 -5
  30. package/src/adapter/spread/spread-codegen.ts +12 -5
  31. package/src/adapter/type/type-codegen.ts +86 -25
  32. package/src/adapter/value/parsed-literal-to-go.ts +28 -10
  33. package/src/adapter/value/value-lowering.ts +65 -29
  34. package/src/render-divergences.ts +6 -15
  35. 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.0",
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.0"
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.0",
71
- "@barefootjs/jsx": "0.31.0",
72
- "@barefootjs/vite": "0.31.0",
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: the
4605
- // reconciliation now happens ONCE, at the PARENT's emission site
4606
- // (`loopRowChildPropOverrides`, via `childPropFieldNames`) the parent
4607
- // emits the child's own field name directly, so the rebuilder's switch
4608
- // (`recordRepropsSpec` / `emitRepropsRegistration`) needs no separate
4609
- // "wire" name and is keyed uniformly by that one field on both sides.
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 OWN field name ("Count"), not the
4635
- // JSX attribute ("N").
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
- expect(types).toContain('&in.Count,')
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 both sides, and the parent already
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