@barefootjs/go-template 0.8.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.
- package/dist/adapter/go-template-adapter.d.ts +142 -0
- package/dist/adapter/go-template-adapter.d.ts.map +1 -1
- package/dist/adapter/index.js +402 -27
- package/dist/build.js +402 -27
- package/dist/index.js +402 -27
- package/dist/test-render.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/__tests__/go-template-adapter.test.ts +189 -29
- package/src/adapter/go-template-adapter.ts +689 -25
- package/src/test-render.ts +189 -10
|
@@ -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,24 +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` is a pass-through
|
|
84
|
-
// native control.)
|
|
85
|
-
//
|
|
86
|
-
// `textarea` now participates: its conditional inline-object spread
|
|
87
|
-
// (`{...(describedBy ? {...} : {})}`) lowers via
|
|
88
|
-
// `buildConditionalSpreadInitializer`, and its optional
|
|
89
|
-
// `rows={rows}` attribute is omitted when nil via the nillable-field
|
|
90
|
-
// guard in `elementAttrEmitter.emitExpression`
|
|
91
|
-
// (`{{if ne .Rows nil}}rows="{{.Rows}}"{{end}}`), matching Hono's
|
|
92
|
-
// nullish-attribute omission.
|
|
93
|
-
'toggle',
|
|
94
|
-
'switch',
|
|
95
|
-
'checkbox',
|
|
96
|
-
'kbd',
|
|
97
68
|
],
|
|
98
69
|
// Per-fixture build-time contracts for shapes the Go template
|
|
99
70
|
// adapter intentionally refuses to lower. Lives here (not on the
|
|
@@ -877,6 +848,195 @@ function Box({ a, b }: { a?: string; b?: string }) {
|
|
|
877
848
|
})
|
|
878
849
|
})
|
|
879
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
|
+
|
|
880
1040
|
describe('nullish optional-attribute omission (textarea rows)', () => {
|
|
881
1041
|
// An optional, no-default prop whose Go field type resolves to
|
|
882
1042
|
// `interface{}` (nillable) is emitted with a `ne .X nil` guard so an
|