@barefootjs/go-template 0.7.0 → 0.9.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.
@@ -1 +1 @@
1
- {"version":3,"file":"test-render.d.ts","sourceRoot":"","sources":["../src/test-render.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAGH,OAAO,KAAK,EAAE,eAAe,EAAe,MAAM,iBAAiB,CAAA;AAOnE,qBAAa,mBAAoB,SAAQ,KAAK;IAC5C,YAAY,OAAO,EAAE,MAAM,EAG1B;CACF;AAgCD,MAAM,WAAW,aAAa;IAC5B,sBAAsB;IACtB,MAAM,EAAE,MAAM,CAAA;IACd,8BAA8B;IAC9B,OAAO,EAAE,eAAe,CAAA;IACxB,iCAAiC;IACjC,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;IAC/B,qDAAqD;IACrD,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;CACpC;AAED,wBAAsB,yBAAyB,CAAC,OAAO,EAAE,aAAa,GAAG,OAAO,CAAC,MAAM,CAAC,CAmNvF"}
1
+ {"version":3,"file":"test-render.d.ts","sourceRoot":"","sources":["../src/test-render.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAGH,OAAO,KAAK,EAAE,eAAe,EAAe,MAAM,iBAAiB,CAAA;AAOnE,qBAAa,mBAAoB,SAAQ,KAAK;IAC5C,YAAY,OAAO,EAAE,MAAM,EAG1B;CACF;AAgCD,MAAM,WAAW,aAAa;IAC5B,sBAAsB;IACtB,MAAM,EAAE,MAAM,CAAA;IACd,8BAA8B;IAC9B,OAAO,EAAE,eAAe,CAAA;IACxB,iCAAiC;IACjC,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;IAC/B,qDAAqD;IACrD,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;CACpC;AAED,wBAAsB,yBAAyB,CAAC,OAAO,EAAE,aAAa,GAAG,OAAO,CAAC,MAAM,CAAC,CAsRvF"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@barefootjs/go-template",
3
- "version": "0.7.0",
3
+ "version": "0.9.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.7.0"
57
+ "@barefootjs/jsx": "0.9.0"
58
58
  }
59
59
  }
@@ -35,17 +35,6 @@ runAdapterConformanceTests({
35
35
  // `BF104` at build time instead of silently emitting invalid
36
36
  // template syntax (#1266).
37
37
  skipJsx: [
38
- 'return-map',
39
- // #1297 fixed the harness-side IR emission gate (multi-component
40
- // sources now emit one `ir` file per component, and the harness
41
- // picks the entry-point IR). The remaining gap is adapter-side:
42
- // the go-template adapter has no SSR context-propagation
43
- // mechanism, so `<Ctx.Provider value="dark">` doesn't make
44
- // `useContext(Ctx)` resolve to `"dark"` at template-eval time —
45
- // the template emits `.Theme` against a struct that has no
46
- // `Theme` field. Provider SSR coverage on go-template waits on
47
- // that adapter feature; see #1297 follow-up.
48
- 'context-provider',
49
38
  // #1244 stress catalog (#1326): `children={<span/>}` — the IR
50
39
  // hoists the span with `needsScope: true` so the Hono reference
51
40
  // emits `bf-s` on the inner `<span>`. The Go adapter renders the
@@ -76,17 +65,6 @@ runAdapterConformanceTests({
76
65
  // option fixed on the Hono side. Separate follow-up.
77
66
  'toggle-shared',
78
67
  'props-reactivity-comparison',
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',
90
68
  ],
91
69
  // Per-fixture build-time contracts for shapes the Go template
92
70
  // adapter intentionally refuses to lower. Lives here (not on the
@@ -818,6 +796,279 @@ export function List() {
818
796
  })
819
797
  })
820
798
 
799
+ describe('conditional inline-object spread (textarea aria-describedby)', () => {
800
+ // `{...(cond ? { 'aria-describedby': cond } : {})}` lowers to an
801
+ // IIFE-of-maps in `NewXxxProps` so the falsy branch OMITS the key
802
+ // (SpreadAttrs does not filter empty strings). The fixture only
803
+ // exercises the falsy branch; this pins the TRUTHY branch.
804
+ test('lowers to a conditional map IIFE and keeps the {{bf_spread_attrs}} template', () => {
805
+ const source = `
806
+ function Box({ describedBy }: { describedBy?: string }) {
807
+ return <div {...(describedBy ? { 'aria-describedby': describedBy } : {})} />
808
+ }
809
+ `
810
+ const { template, types } = compileAndGenerate(source)
811
+ // Template emission is unchanged from the proven {...props} path.
812
+ expect(template).toContain('{{bf_spread_attrs .Spread_0}}')
813
+ // The bag value is a conditional map built in NewBoxProps. The
814
+ // prop type is unresolved (interface{}), so the condition routes
815
+ // through `bf.Truthy` for a faithful JS `Boolean(x)` test.
816
+ expect(types).toContain('Spread_0: func() map[string]any {')
817
+ expect(types).toContain('if bf.Truthy(in.DescribedBy) {')
818
+ expect(types).toContain('return map[string]any{"aria-describedby": in.DescribedBy}')
819
+ expect(types).toContain('return map[string]any{}')
820
+ })
821
+
822
+ test('resolves the object value reference and key for a second prop', () => {
823
+ const source = `
824
+ function Box({ label }: { label: string }) {
825
+ return <div {...(label ? { 'data-label': label } : {})} />
826
+ }
827
+ `
828
+ const { types } = compileAndGenerate(source)
829
+ // The condition prop and the value reference resolve to `in.<Field>`,
830
+ // the static key is preserved. (The analyzer surfaces these
831
+ // destructured props as `unknown`/`interface{}`, so the condition
832
+ // routes through `bf.Truthy` for a faithful JS truthiness test.)
833
+ expect(types).toContain('if bf.Truthy(in.Label) {')
834
+ expect(types).toContain('return map[string]any{"data-label": in.Label}')
835
+ })
836
+
837
+ test('refuses a non-identifier condition with BF101 (out-of-shape fallback)', () => {
838
+ const adapter = new GoTemplateAdapter()
839
+ const source = `
840
+ function Box({ a, b }: { a?: string; b?: string }) {
841
+ return <div {...(a === b ? { 'data-x': a } : {})} />
842
+ }
843
+ `
844
+ const ir = compileToIR(source, adapter)
845
+ adapter.generate(ir)
846
+ const errs = (adapter as unknown as { errors: { code: string }[] }).errors
847
+ expect(errs.some(e => e.code === 'BF101')).toBe(true)
848
+ })
849
+ })
850
+
851
+ describe('local-const conditional-spread resolution (#checkbox icon)', () => {
852
+ // A FUNCTION-scope const holding a `cond ? {…} : {}` ternary, then
853
+ // spread as a bare identifier (`{...sizeAttrs}`), resolves through the
854
+ // same conditional-spread lowering as the inline form. CheckIcon's
855
+ // `const sizeAttrs = size ? {…} : {}` is exactly this shape.
856
+ test('resolves a bare-identifier spread of a function-scope conditional const', () => {
857
+ const source = `
858
+ function Box({ flag }: { flag?: boolean }) {
859
+ const attrs = flag ? { 'data-on': 'yes' } : {}
860
+ return <div {...attrs} />
861
+ }
862
+ `
863
+ const { template, types } = compileAndGenerate(source)
864
+ expect(template).toContain('{{bf_spread_attrs .Spread_0}}')
865
+ expect(types).toContain('Spread_0: func() map[string]any {')
866
+ expect(types).toContain('return map[string]any{"data-on": "yes"}')
867
+ expect(types).toContain('return map[string]any{}')
868
+ })
869
+
870
+ // A const that resolves to another bare identifier must NOT be
871
+ // forwarded (loop guard) — it falls through to BF101 like any other
872
+ // unsupported spread identifier.
873
+ test('does not forward a const that aliases another identifier (loop guard)', () => {
874
+ const adapter = new GoTemplateAdapter()
875
+ const source = `
876
+ function Box({ other }: { other?: object }) {
877
+ const attrs = other
878
+ return <div {...attrs} />
879
+ }
880
+ `
881
+ const ir = compileToIR(source, adapter)
882
+ adapter.generate(ir)
883
+ const errs = (adapter as unknown as { errors: { code: string }[] }).errors
884
+ expect(errs.some(e => e.code === 'BF101')).toBe(true)
885
+ })
886
+ })
887
+
888
+ describe('Record<staticKeys,scalar>[propKey] spread value (#checkbox icon)', () => {
889
+ // `const sizeMap: Record<IconSize, number> = { sm: 16, ... }` indexed
890
+ // by a prop inside a conditional-spread object value lowers to an
891
+ // inline indexed Go map keyed via `fmt.Sprint(in.<Field>)`. This is
892
+ // CheckIcon's `{ width: sizeMap[size], height: sizeMap[size] }` shape.
893
+ test('lowers an indexed module-const map to an inline fmt.Sprint-keyed map and adds the fmt import', () => {
894
+ const source = `
895
+ const sizeMap: Record<string, number> = { sm: 16, md: 20, lg: 24, xl: 32 }
896
+ function Box({ size }: { size?: string }) {
897
+ const attrs = size ? { width: sizeMap[size] } : {}
898
+ return <div {...attrs} />
899
+ }
900
+ `
901
+ const { types } = compileAndGenerate(source)
902
+ expect(types).toContain(
903
+ 'map[string]any{"sm": 16, "md": 20, "lg": 24, "xl": 32}[fmt.Sprint(in.Size)]',
904
+ )
905
+ // The `"fmt"` import is emitted only when this lowering fires.
906
+ expect(types).toContain('\t"fmt"')
907
+ })
908
+
909
+ test('lowers string-valued record maps too', () => {
910
+ const source = `
911
+ const labelMap: Record<string, string> = { a: 'Alpha', b: 'Beta' }
912
+ function Box({ k }: { k?: string }) {
913
+ const attrs = k ? { 'data-label': labelMap[k] } : {}
914
+ return <div {...attrs} />
915
+ }
916
+ `
917
+ const { types } = compileAndGenerate(source)
918
+ expect(types).toContain('map[string]any{"a": "Alpha", "b": "Beta"}[fmt.Sprint(in.K)]')
919
+ })
920
+
921
+ // A non-scalar record value (object / array / call) is out of shape:
922
+ // the spread object value can't lower, so the whole spread falls back
923
+ // to BF101 rather than emitting an invalid map.
924
+ test('refuses a non-scalar record value with BF101 (out-of-shape fallback)', () => {
925
+ const adapter = new GoTemplateAdapter()
926
+ const source = `
927
+ const sizeMap: Record<string, object> = { sm: { w: 1 } }
928
+ function Box({ size }: { size?: string }) {
929
+ const attrs = size ? { width: sizeMap[size] } : {}
930
+ return <div {...attrs} />
931
+ }
932
+ `
933
+ const ir = compileToIR(source, adapter)
934
+ adapter.generate(ir)
935
+ const errs = (adapter as unknown as { errors: { code: string }[] }).errors
936
+ expect(errs.some(e => e.code === 'BF101')).toBe(true)
937
+ })
938
+ })
939
+
940
+ describe('props-object inherited-attribute enumeration (#checkbox)', () => {
941
+ // A SolidJS props-object component (`function C(props: P)`) that reads
942
+ // inherited attributes (`props.className` in a memo, `props.id` /
943
+ // `props.disabled` on the root) must expose Input/Props fields for them,
944
+ // even though `propsParams` only enumerates `P`'s own members. Without
945
+ // this the caller's `className: ''` has no field — `unknown field
946
+ // ClassName in struct literal of type CInput`.
947
+ test('reads of props.className / props.id / props.disabled become Input fields', () => {
948
+ const adapter = new GoTemplateAdapter()
949
+ const source = `
950
+ "use client"
951
+ import { createMemo } from "@barefootjs/client"
952
+ interface P { tone?: string }
953
+ export function Widget(props: P) {
954
+ const classes = createMemo(() => \`base \${props.className ?? ''}\`)
955
+ return <button id={props.id} disabled={props.disabled ?? false} class={classes()}>x</button>
956
+ }
957
+ `
958
+ const ir = compileToIR(source, adapter)
959
+ const types = adapter.generateTypes(ir)!
960
+ expect(types).toContain('ClassName string')
961
+ // `id` is a bare-reference optional → interface{} (nillable, omittable).
962
+ expect(types).toContain('ID interface{}')
963
+ expect(types).toContain('Disabled bool')
964
+ })
965
+
966
+ // The className memo's SSR initial value must inline module string consts
967
+ // (incl. `[...].join(' ')`) and resolve `props.className ?? ''` to the
968
+ // ClassName field — not render the historical `0` placeholder.
969
+ test('template-literal className memo inlines consts + props.className field', () => {
970
+ const adapter = new GoTemplateAdapter()
971
+ const source = `
972
+ "use client"
973
+ import { createMemo } from "@barefootjs/client"
974
+ const base = 'a b'
975
+ const states = ['c', 'd'].join(' ')
976
+ interface P { tone?: string }
977
+ export function Widget(props: P) {
978
+ const classes = createMemo(() => \`\${base} \${states} \${props.className ?? ''} tail\`)
979
+ return <button class={classes()}>x</button>
980
+ }
981
+ `
982
+ const types = adapter.generateTypes(compileToIR(source, adapter))!
983
+ expect(types).toContain('Classes: "a b" + " " + "c d" + " " + in.ClassName + " tail"')
984
+ })
985
+
986
+ // A boolean ternary memo (`isChecked = ctrl() ? c() : i()`) renders its
987
+ // SSR zero as `false`, not the int `0`, so `aria-checked={isChecked()}`
988
+ // matches Hono's `aria-checked="false"`.
989
+ test('boolean ternary memo defaults to false, not 0', () => {
990
+ const adapter = new GoTemplateAdapter()
991
+ const source = `
992
+ "use client"
993
+ import { createSignal, createMemo } from "@barefootjs/client"
994
+ export function Toggle(props: { checked?: boolean; defaultChecked?: boolean }) {
995
+ const [internal] = createSignal(props.defaultChecked ?? false)
996
+ const [controlled] = createSignal<boolean | undefined>(props.checked)
997
+ const isControlled = createMemo(() => props.checked !== undefined)
998
+ const isChecked = createMemo(() => isControlled() ? controlled() : internal())
999
+ return <button aria-checked={isChecked()}>x</button>
1000
+ }
1001
+ `
1002
+ const types = adapter.generateTypes(compileToIR(source, adapter))!
1003
+ expect(types).toContain('IsChecked bool')
1004
+ expect(types).toContain('IsChecked: false,')
1005
+ })
1006
+ })
1007
+
1008
+ describe('cross-component child rest-bag routing (#checkbox)', () => {
1009
+ // A parent rendering a child with a non-param attribute whose name isn't a
1010
+ // valid Go identifier (`<CheckIcon data-slot="..."/>`) must route it into
1011
+ // the child's rest bag (`Props: map[string]any{...}`), not a hyphenated
1012
+ // top-level field (`Data-slot:`), when the child has a `...props` rest
1013
+ // spread. Requires the child's shape registered first.
1014
+ test('routes a hyphenated non-param child attr into the child rest bag', () => {
1015
+ const adapter = new GoTemplateAdapter()
1016
+ const childSource = `
1017
+ "use client"
1018
+ export function Leaf({ size, ...props }: { size?: string }) {
1019
+ return <span {...props}>{size}</span>
1020
+ }
1021
+ `
1022
+ const childIr = compileToIR(childSource, adapter)
1023
+ adapter.registerChildComponentShape(childIr)
1024
+ const parentSource = `
1025
+ "use client"
1026
+ import { Leaf } from './leaf'
1027
+ export function Host() {
1028
+ return <div><Leaf data-slot="indicator" size="sm" /></div>
1029
+ }
1030
+ `
1031
+ const types = adapter.generateTypes(compileToIR(parentSource, adapter))!
1032
+ // Non-param hyphenated attr lands in the rest bag, not a Data-slot field.
1033
+ expect(types).not.toContain('Data-slot:')
1034
+ expect(types).toContain('Props: map[string]any{"data-slot": "indicator"}')
1035
+ // A declared param (`size`) still binds as a top-level field.
1036
+ expect(types).toContain('Size: "sm"')
1037
+ })
1038
+ })
1039
+
1040
+ describe('nullish optional-attribute omission (textarea rows)', () => {
1041
+ // An optional, no-default prop whose Go field type resolves to
1042
+ // `interface{}` (nillable) is emitted with a `ne .X nil` guard so an
1043
+ // unset value DROPS the attribute instead of rendering `attr=""` —
1044
+ // matching Hono's nullish-attribute omission. Concrete/defaulted
1045
+ // props are never nil and stay unconditional.
1046
+ test('guards a nillable optional attr with {{if ne .X nil}}', () => {
1047
+ const source = `
1048
+ function C({ rows }: { rows?: number }) {
1049
+ return <textarea rows={rows} />
1050
+ }
1051
+ `
1052
+ const { template } = compileAndGenerate(source)
1053
+ expect(template).toContain('{{if ne .Rows nil}}rows="{{.Rows}}"{{end}}')
1054
+ // Must NOT emit the bare unconditional form.
1055
+ expect(template).not.toMatch(/(?<!if ne \.Rows nil}})rows="\{\{\.Rows\}\}"/)
1056
+ })
1057
+
1058
+ test('leaves a concrete/defaulted attr unconditional (scope did not widen)', () => {
1059
+ const source = `
1060
+ function C({ value = '' }: { value?: string }) {
1061
+ return <textarea value={value} />
1062
+ }
1063
+ `
1064
+ const { template } = compileAndGenerate(source)
1065
+ // `value` has a destructure default → concrete `string` field →
1066
+ // never nil → emitted unconditionally, exactly like Hono's value="".
1067
+ expect(template).toContain('value="{{.Value}}"')
1068
+ expect(template).not.toContain('if ne .Value nil')
1069
+ })
1070
+ })
1071
+
821
1072
  describe('loop body outer-scope references (#1677)', () => {
822
1073
  test('references an outer signal inside a loop via $ root scope, not the element', () => {
823
1074
  // Inside `{{range $_, $t := .Items}}` the dot is rebound to the loop