@barefootjs/go-template 0.17.1 → 0.18.1
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/expr/url-builder.d.ts.map +1 -1
- package/dist/adapter/go-template-adapter.d.ts +134 -14
- package/dist/adapter/go-template-adapter.d.ts.map +1 -1
- package/dist/adapter/index.js +228 -82
- package/dist/adapter/lib/go-naming.d.ts +23 -0
- package/dist/adapter/lib/go-naming.d.ts.map +1 -1
- package/dist/adapter/type/type-codegen.d.ts +5 -1
- package/dist/adapter/type/type-codegen.d.ts.map +1 -1
- package/dist/adapter/value/parsed-literal-to-go.d.ts +13 -4
- package/dist/adapter/value/parsed-literal-to-go.d.ts.map +1 -1
- package/dist/build.js +228 -82
- package/dist/conformance-pins.d.ts +12 -0
- package/dist/conformance-pins.d.ts.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +247 -82
- package/package.json +3 -3
- package/src/__tests__/go-template-adapter.test.ts +388 -145
- package/src/__tests__/lowering-plugin.test.ts +56 -0
- package/src/adapter/expr/url-builder.ts +14 -3
- package/src/adapter/go-template-adapter.ts +511 -109
- package/src/adapter/lib/go-naming.ts +29 -0
- package/src/adapter/type/type-codegen.ts +22 -3
- package/src/adapter/value/parsed-literal-to-go.ts +82 -9
- package/src/conformance-pins.ts +135 -0
- package/src/index.ts +1 -0
|
@@ -6,12 +6,17 @@
|
|
|
6
6
|
|
|
7
7
|
import { describe, test, expect } from 'bun:test'
|
|
8
8
|
import { GoTemplateAdapter } from '../adapter/go-template-adapter'
|
|
9
|
-
import {
|
|
10
|
-
runAdapterConformanceTests,
|
|
11
|
-
TemplatePrimitiveCaseId,
|
|
12
|
-
} from '@barefootjs/adapter-tests'
|
|
9
|
+
import { runAdapterConformanceTests } from '@barefootjs/adapter-tests'
|
|
13
10
|
import { renderGoTemplateComponent, GoNotAvailableError } from '@barefootjs/go-template/test-render'
|
|
14
|
-
import {
|
|
11
|
+
import {
|
|
12
|
+
compileJSX,
|
|
13
|
+
analyzeComponent,
|
|
14
|
+
buildMetadata,
|
|
15
|
+
jsxToIR,
|
|
16
|
+
type ComponentIR,
|
|
17
|
+
type IRExpression,
|
|
18
|
+
} from '@barefootjs/jsx'
|
|
19
|
+
import { conformancePins } from '../conformance-pins'
|
|
15
20
|
|
|
16
21
|
runAdapterConformanceTests({
|
|
17
22
|
name: 'go-template',
|
|
@@ -65,135 +70,38 @@ runAdapterConformanceTests({
|
|
|
65
70
|
// memo's field type is `[]any` and its constructor init serializes the
|
|
66
71
|
// `.filter` predicate to the runtime evaluator. See the #2075 constructor
|
|
67
72
|
// pins below.
|
|
68
|
-
|
|
73
|
+
//
|
|
74
|
+
// `context-provider-nullish-object-fallback` (#2087) is no longer skipped:
|
|
75
|
+
// the Go adapter now models an OBJECT-shaped `createContext` default
|
|
76
|
+
// (`ContextConsumer.defaultKind === 'object'`, `augment-inherited-props.ts`)
|
|
77
|
+
// as a real `map[string]interface{}` consumer field instead of falling
|
|
78
|
+
// through to the scalar `string` default, and `extendProviderContext`
|
|
79
|
+
// lowers the chart shape's `<Ctx.Provider value={{ config: props.config ??
|
|
80
|
+
// {} }}>` into a `map[string]interface{}` Go expression bound into the
|
|
81
|
+
// descendant's constructor call (`providerObjectValueToGoMap` /
|
|
82
|
+
// `lowerProviderMapMemberValue`). The consumer's `ctx.config.label` read
|
|
83
|
+
// lowers through the runtime's case-tolerant `bf_get` (`getFieldValue`,
|
|
84
|
+
// bf.go) instead of a plain `.Ctx.Config.Label` dot-chain, which would
|
|
85
|
+
// require an exact-cased struct/map field that never exists. See
|
|
86
|
+
// go-template-adapter.ts's `member()` `isMapRootedContextChain` branch.
|
|
69
87
|
// Per-fixture build-time contracts for shapes the Go template
|
|
70
|
-
// adapter intentionally refuses to lower. Lives
|
|
71
|
-
// shared fixtures) so adding a new adapter doesn't require
|
|
72
|
-
// any cross-adapter file — every adapter declares its own
|
|
88
|
+
// adapter intentionally refuses to lower. Lives in `../conformance-pins`
|
|
89
|
+
// (not on the shared fixtures) so adding a new adapter doesn't require
|
|
90
|
+
// touching any cross-adapter file — every adapter declares its own
|
|
73
91
|
// refusal set against the canonical fixture corpus.
|
|
74
|
-
expectedDiagnostics:
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
// call it inside a keyed `.map`. Same BF103 surface as
|
|
88
|
-
// `static-array-children` above — pinned at adapter level so the
|
|
89
|
-
// shared-component corpus stays adapter-neutral.
|
|
90
|
-
'todo-app': [{ code: 'BF103', severity: 'error' }],
|
|
91
|
-
'todo-app-ssr': [{ code: 'BF103', severity: 'error' }],
|
|
92
|
-
// Array-destructure loop param (`([k, v]) => ...`): Go's `{{range
|
|
93
|
-
// $a, $b := ...}}` only supports single-name bindings, so the
|
|
94
|
-
// adapter would otherwise emit invalid template syntax.
|
|
95
|
-
'static-array-from-props': [{ code: 'BF104', severity: 'error' }],
|
|
96
|
-
// Same destructure shape with a child component body — fires both
|
|
97
|
-
// BF103 (imported child in loop) and BF104 (destructure param).
|
|
98
|
-
'static-array-from-props-with-component': [
|
|
99
|
-
{ code: 'BF103', severity: 'error' },
|
|
100
|
-
{ code: 'BF104', severity: 'error' },
|
|
101
|
-
],
|
|
102
|
-
// (`style-3-signals` graduated alongside `style-object-dynamic` — see note
|
|
103
|
-
// above; the `style={{ … }}` object now lowers to a CSS string.)
|
|
104
|
-
// #1244 stress catalog: tagged-template-literal callees
|
|
105
|
-
// (`cn\`base \${tone()}\``) likewise can't lower into Go template
|
|
106
|
-
// syntax — same BF101 refusal.
|
|
107
|
-
'tagged-template-classname': [{ code: 'BF101', severity: 'error' }],
|
|
108
|
-
// #2038: a filter predicate whose body contains a NESTED callback call
|
|
109
|
-
// (`t => !picked().some(p => …)` / `t => picked().find(p => …)`). The
|
|
110
|
-
// evaluator refuses nested arrows and `renderFilterExpr` has no faithful
|
|
111
|
-
// Go form for the inner call (its `call` arm used to silently drop the
|
|
112
|
-
// arrow argument and render only the callee) — the compiler is loud
|
|
113
|
-
// instead of lossy. The `/* @client */` twin
|
|
114
|
-
// (`filter-nested-callback-predicate-client`) has no pin here: it must
|
|
115
|
-
// render clean on every adapter, which asserts the suppression contract.
|
|
116
|
-
// https://github.com/piconic-ai/barefootjs/issues/2038
|
|
117
|
-
'filter-nested-callback-predicate': [{ code: 'BF101', severity: 'error' }],
|
|
118
|
-
'filter-nested-find-predicate': [{ code: 'BF101', severity: 'error' }],
|
|
119
|
-
// #1310: rest destructure in .map() callback. The object-rest shape read
|
|
120
|
-
// via member access (`rest-destructure-object-in-map`) now lowers — each
|
|
121
|
-
// binding resolves to a field on a synthetic `$__bf_item0` range var (the
|
|
122
|
-
// reserved `__bf_item` name, depth-suffixed) and `rest.flag` →
|
|
123
|
-
// `$__bf_item0.Flag` (`destructureBindingsSupportable`). The
|
|
124
|
-
// other three stay refused: rest SPREAD (`{...rest}`) needs a residual
|
|
125
|
-
// object, and array-index / nested paths (`[a, ...t]`, `{ cells: [h] }`)
|
|
126
|
-
// need index/slice machinery Go's `{{range}}` can't express inline.
|
|
127
|
-
'rest-destructure-object-spread-in-map': [{ code: 'BF104', severity: 'error' }],
|
|
128
|
-
'rest-destructure-array-in-map': [{ code: 'BF104', severity: 'error' }],
|
|
129
|
-
'rest-destructure-nested-in-map': [{ code: 'BF104', severity: 'error' }],
|
|
130
|
-
// #1443: `[a, b].filter(Boolean).join(' ')` (registry Slot) now
|
|
131
|
-
// lowers to `bf_join (bf_filter_truthy (bf_arr ...)) " "`. No
|
|
132
|
-
// BF101 expected — pinned positively by the
|
|
133
|
-
// `branch-local-filter-join-go` template-output test below.
|
|
134
|
-
//
|
|
135
|
-
// #1448 Tier A — JS Array / String methods that the Go template
|
|
136
|
-
// adapter hasn't lowered yet. Each row drops once the
|
|
137
|
-
// corresponding method PR lands. Hono / CSR pass these out of
|
|
138
|
-
// the box (they evaluate JS at runtime) so the pin only applies
|
|
139
|
-
// here.
|
|
140
|
-
//
|
|
141
|
-
// `array-includes` / `string-includes` no longer pinned — both
|
|
142
|
-
// shapes lower via the shared `array-method` IR + the polymorphic
|
|
143
|
-
// `bf_includes` runtime helper that dispatches on
|
|
144
|
-
// `reflect.Kind()` (slice/array → element search, string →
|
|
145
|
-
// substring search). The condition-position lowering picks up
|
|
146
|
-
// the same emit through the `array-method` arm of
|
|
147
|
-
// `renderConditionExpr` (#1448 Tier A first PR).
|
|
148
|
-
//
|
|
149
|
-
// Remaining fixtures land at expression position and surface BF101
|
|
150
|
-
// via `convertExpressionToGo`. Distinct codes for the two paths is
|
|
151
|
-
// pre-existing adapter behaviour, not something this catalog
|
|
152
|
-
// should paper over — pinned literally here.
|
|
153
|
-
// `array-indexOf` / `array-lastIndexOf` no longer pinned —
|
|
154
|
-
// value-equality `bf_index_of` / `bf_last_index_of` Go runtime
|
|
155
|
-
// helpers handle the shape (#1448 Tier A second PR).
|
|
156
|
-
// `array-at` no longer pinned — the pre-existing `bf_at` runtime
|
|
157
|
-
// helper now lowers `.at(i)` (#1448 Tier A third PR).
|
|
158
|
-
// `array-concat` no longer pinned — the new `bf_concat` runtime
|
|
159
|
-
// helper merges two arrays into a single `[]any` (#1448 Tier A
|
|
160
|
-
// fourth PR).
|
|
161
|
-
// `array-slice` no longer pinned — the new `bf_slice` runtime
|
|
162
|
-
// helper carves out a sub-range with JS-compat clamping
|
|
163
|
-
// (#1448 Tier A fifth PR).
|
|
164
|
-
// `array-reverse` / `array-toReversed` no longer pinned —
|
|
165
|
-
// both share the `bf_reverse` helper since SSR templates
|
|
166
|
-
// render a snapshot and the JS mutate-vs-new distinction has
|
|
167
|
-
// no template-level meaning (#1448 Tier A sixth PR).
|
|
168
|
-
// `string-toLowerCase` / `string-toUpperCase` no longer pinned —
|
|
169
|
-
// pre-existing `bf_lower` / `bf_upper` runtime helpers wire to
|
|
170
|
-
// the JS method names at the adapter layer (#1448 Tier A
|
|
171
|
-
// seventh + eighth PRs).
|
|
172
|
-
// `string-trim` no longer pinned — pre-existing `bf_trim`
|
|
173
|
-
// (wraps `strings.TrimSpace`) handles the strip (#1448 Tier A
|
|
174
|
-
// ninth PR, closing out Tier A).
|
|
175
|
-
// #2073 follow-up: a function-reference `.map(format)` callback has no
|
|
176
|
-
// arrow body to serialize — not a CALLBACK_METHODS shape — so the
|
|
177
|
-
// UNSUPPORTED_METHODS gate refuses it with BF101 rather than emitting
|
|
178
|
-
// a broken template.
|
|
179
|
-
'array-map-function-reference': [{ code: 'BF101', severity: 'error' }],
|
|
180
|
-
},
|
|
181
|
-
// `JSON_STRINGIFY_VIA_CONST` and `MATH_FLOOR_VIA_CONST` now pass
|
|
182
|
-
// via `GoTemplateAdapter.templatePrimitives` (#1188). The two
|
|
183
|
-
// remaining cases stay skipped because the V1 registry is
|
|
184
|
-
// identifier-path-only and explicit:
|
|
185
|
-
// - `USER_IMPORT_VIA_CONST` — a bespoke user import isn't in
|
|
186
|
-
// the registry and can't be rendered server-side without
|
|
187
|
-
// user-supplied template-fn mappings.
|
|
188
|
-
// - `NO_DOUBLE_REWRITE_OF_PROPS_OBJECT` — uses `customSerialize`
|
|
189
|
-
// too, same reason.
|
|
190
|
-
// Adding new entries to `templatePrimitives` should narrow this
|
|
191
|
-
// skip set; see `templatePrimitives` declaration in
|
|
192
|
-
// `go-template-adapter.ts` for the full V1 surface.
|
|
193
|
-
skipTemplatePrimitives: new Set([
|
|
194
|
-
TemplatePrimitiveCaseId.USER_IMPORT_VIA_CONST,
|
|
195
|
-
TemplatePrimitiveCaseId.NO_DOUBLE_REWRITE_OF_PROPS_OBJECT,
|
|
196
|
-
]),
|
|
92
|
+
expectedDiagnostics: conformancePins,
|
|
93
|
+
// `JSON_STRINGIFY_VIA_CONST` and `MATH_FLOOR_VIA_CONST` pass via
|
|
94
|
+
// `GoTemplateAdapter.templatePrimitives` (#1188) — the identifier-path
|
|
95
|
+
// registry for well-known JS builtins. `USER_IMPORT_VIA_CONST` and
|
|
96
|
+
// `NO_DOUBLE_REWRITE_OF_PROPS_OBJECT` now ALSO pass (#2069): a bespoke
|
|
97
|
+
// user import can never be added to the string-keyed registry (the
|
|
98
|
+
// adapter has no way to know about it ahead of time), but the shared
|
|
99
|
+
// `RelocateEnv.loweringMatchers` acceptance path recognises it via a
|
|
100
|
+
// `LoweringPlugin` the case setup registers around the compile (see
|
|
101
|
+
// `packages/adapter-tests/src/cases/template-primitives.ts`) — the same
|
|
102
|
+
// seam a real userland plugin author would use. No skips left, so
|
|
103
|
+
// `skipTemplatePrimitives` is omitted entirely (defaults to "skip
|
|
104
|
+
// nothing").
|
|
197
105
|
skipMarkerConformance: new Set<string>([
|
|
198
106
|
// Same as Hono / Mojo: `/* @client */` markers on TodoApp's keyed
|
|
199
107
|
// `.map` intentionally elide a slot id from the SSR template that
|
|
@@ -922,10 +830,18 @@ export function Dyn() {
|
|
|
922
830
|
expect(types).toContain('Items: nil,')
|
|
923
831
|
})
|
|
924
832
|
|
|
925
|
-
test('
|
|
926
|
-
// A quoted key like "data-id"
|
|
927
|
-
// valid Go struct field identifier
|
|
928
|
-
//
|
|
833
|
+
test('bakes a non-Go-identifier object key via a sanitized struct field (#1675 review, revised #2087)', () => {
|
|
834
|
+
// A quoted key like "data-id" used to capitalise to `Data-id` (not a
|
|
835
|
+
// valid Go struct field identifier), so the struct field was dropped
|
|
836
|
+
// entirely and the whole array baked to nil rather than emit invalid
|
|
837
|
+
// Go — silently losing the data. #2087 Phase B's `goFieldNameForKey`
|
|
838
|
+
// (packages/adapter-go-template/src/adapter/lib/go-naming.ts) instead
|
|
839
|
+
// PascalCases each segment split on the hyphen (`data-id` → `DataID`,
|
|
840
|
+
// matching the `id` whole-word initialism), giving the field a real
|
|
841
|
+
// Go name — needed so the destructure-rest residual lowering
|
|
842
|
+
// (`rest-destructure-object-spread-in-map`, #2087) has something to
|
|
843
|
+
// read a non-identifier sibling key back from. The array now bakes for
|
|
844
|
+
// real instead of deferring to nil.
|
|
929
845
|
const adapter = new GoTemplateAdapter()
|
|
930
846
|
const ir = compileToIR(`
|
|
931
847
|
"use client"
|
|
@@ -938,7 +854,56 @@ export function Rows() {
|
|
|
938
854
|
}
|
|
939
855
|
`)
|
|
940
856
|
const types = adapter.generate(ir).types!
|
|
941
|
-
expect(types).toContain('
|
|
857
|
+
expect(types).toContain('DataID string `json:"data-id"`')
|
|
858
|
+
expect(types).toContain('Rows: []Row{Row{DataID: "a"}},')
|
|
859
|
+
})
|
|
860
|
+
|
|
861
|
+
test('bakes a non-Go-identifier key inside a nested INLINE object with the same sanitizer as the accessor side (#2089 review)', () => {
|
|
862
|
+
// A nested inline-object property (`meta: { 'data-x': string }` — no
|
|
863
|
+
// named Go struct, lowered to map[string]interface{}) bakes as a Go map
|
|
864
|
+
// literal. The map KEY must be produced by `goFieldNameForKey` — the
|
|
865
|
+
// same function `buildSegmentAccessor`/`structFieldsFor` use — so the
|
|
866
|
+
// emitted accessor (`.Meta.DataX`, an exact-string MapIndex on maps)
|
|
867
|
+
// actually finds the value. `capitalizeFieldName` alone would bake
|
|
868
|
+
// `"Data-x"`, a key no emitted accessor can reach, silently rendering
|
|
869
|
+
// empty (flagged by Copilot on PR #2089).
|
|
870
|
+
const adapter = new GoTemplateAdapter()
|
|
871
|
+
const ir = compileToIR(`
|
|
872
|
+
"use client"
|
|
873
|
+
import { createSignal } from "@barefootjs/client"
|
|
874
|
+
|
|
875
|
+
type Row = { id: string; meta: { "data-x": string } }
|
|
876
|
+
export function Rows() {
|
|
877
|
+
const [rows] = createSignal<Row[]>([{ id: "r1", meta: { "data-x": "v" } }])
|
|
878
|
+
return <ul>{rows().map(({ id, meta }) => <li key={id}>{meta["data-x"]}</li>)}</ul>
|
|
879
|
+
}
|
|
880
|
+
`)
|
|
881
|
+
const types = adapter.generate(ir).types!
|
|
882
|
+
expect(types).toContain('map[string]interface{}{"DataX": "v"}')
|
|
883
|
+
expect(types).not.toContain('"Data-x"')
|
|
884
|
+
})
|
|
885
|
+
|
|
886
|
+
test('snake_case keys keep their underscore in the generated field name (#2089 review)', () => {
|
|
887
|
+
// Underscores are VALID Go identifier characters, and `Foo_bar` is the
|
|
888
|
+
// field name this adapter has always generated for a snake_case key —
|
|
889
|
+
// `goFieldNameForKey` must NOT split on `_` (that would rename the
|
|
890
|
+
// generated field to `FooBar`, silently diverging from the `.Foo_bar`
|
|
891
|
+
// dot access the member emitter produces AND breaking consumers'
|
|
892
|
+
// hand-written constructors against previously generated types).
|
|
893
|
+
const adapter = new GoTemplateAdapter()
|
|
894
|
+
const ir = compileToIR(`
|
|
895
|
+
"use client"
|
|
896
|
+
import { createSignal } from "@barefootjs/client"
|
|
897
|
+
|
|
898
|
+
type Row = { foo_bar: string }
|
|
899
|
+
export function Rows() {
|
|
900
|
+
const [rows] = createSignal<Row[]>([{ foo_bar: "a" }])
|
|
901
|
+
return <ul>{rows().map((r) => <li key={r.foo_bar}>{r.foo_bar}</li>)}</ul>
|
|
902
|
+
}
|
|
903
|
+
`)
|
|
904
|
+
const out = adapter.generate(ir)
|
|
905
|
+
expect(out.types!).toContain('Foo_bar string `json:"foo_bar"`')
|
|
906
|
+
expect(out.template).toContain('.Foo_bar')
|
|
942
907
|
})
|
|
943
908
|
|
|
944
909
|
test('collapses whitespace-padded empty array literal to nil (#1675 review)', () => {
|
|
@@ -1022,10 +987,10 @@ function Box({ label }: { label: string }) {
|
|
|
1022
987
|
`
|
|
1023
988
|
const { types } = compileAndGenerate(source)
|
|
1024
989
|
// The condition prop and the value reference resolve to `in.<Field>`,
|
|
1025
|
-
// the static key is preserved.
|
|
1026
|
-
//
|
|
1027
|
-
//
|
|
1028
|
-
expect(types).toContain('if
|
|
990
|
+
// the static key is preserved. `label` is a required primitive, so the
|
|
991
|
+
// analyzer resolves it to `string` (issue #2150) and the field-typed
|
|
992
|
+
// condition is a faithful `!= ""` truthiness test rather than reflection.
|
|
993
|
+
expect(types).toContain('if in.Label != "" {')
|
|
1029
994
|
expect(types).toContain('return map[string]any{"data-label": in.Label}')
|
|
1030
995
|
})
|
|
1031
996
|
|
|
@@ -3474,7 +3439,6 @@ export { C }
|
|
|
3474
3439
|
|
|
3475
3440
|
// Tier B `.sort` / `.toSorted` follow-ups still refused with BF021.
|
|
3476
3441
|
const unsupportedSort: Array<[string, string]> = [
|
|
3477
|
-
['function-reference comparator', `items().toSorted(myCmp).map(x => <li key={x.name}>{x.name}</li>)`],
|
|
3478
3442
|
['localeCompare locale/options arg', `items().toSorted((a, b) => a.name.localeCompare(b.name, "ja", { numeric: true })).map(x => <li key={x.name}>{x.name}</li>)`],
|
|
3479
3443
|
]
|
|
3480
3444
|
for (const [label, chain] of unsupportedSort) {
|
|
@@ -3497,6 +3461,17 @@ export function C() {
|
|
|
3497
3461
|
})
|
|
3498
3462
|
}
|
|
3499
3463
|
|
|
3464
|
+
// #2090: a function-reference comparator (`.toSorted(myCmp)`, `myCmp` a
|
|
3465
|
+
// same-file const arrow) now resolves through the analyzer's scope
|
|
3466
|
+
// machinery and compiles — no BF021, and the sort lowers exactly like an
|
|
3467
|
+
// inline comparator (`bf_sort` / `bf_sort_eval` group in the template).
|
|
3468
|
+
test('sort follow-up (function-reference comparator): resolves and compiles without BF021', () => {
|
|
3469
|
+
const chain = `items().toSorted(myCmp).map(x => <li key={x.name}>{x.name}</li>)`
|
|
3470
|
+
const result = emitLoop(chain, false)
|
|
3471
|
+
expect(result.errors).toEqual([])
|
|
3472
|
+
expect(result.template).toMatch(/bf_sort/)
|
|
3473
|
+
})
|
|
3474
|
+
|
|
3500
3475
|
// End-to-end proof via `go run`: the `@client` form renders a
|
|
3501
3476
|
// `<!--bf-client:sN-->` placeholder. The bare form is now caught at
|
|
3502
3477
|
// build with BF101 and degrades to an empty, render-safe slot (no
|
|
@@ -3589,6 +3564,14 @@ export function C() {
|
|
|
3589
3564
|
|
|
3590
3565
|
test('@client attribute inside a keyed .map() loop body is also deferred', () => {
|
|
3591
3566
|
const adapter = new GoTemplateAdapter()
|
|
3567
|
+
// `days` is signal-backed (not a bare function-scope local) so the loop
|
|
3568
|
+
// array itself resolves to a real `.Days []Day` field — a plain
|
|
3569
|
+
// `const days: Day[] = [...]` array-of-objects local has no such backing
|
|
3570
|
+
// (the Go adapter only binds a STRING-derived function-scope const as a
|
|
3571
|
+
// field; #2087's `renderLoop` BF101 check now catches that case loudly
|
|
3572
|
+
// instead of emitting a template that references a nonexistent field).
|
|
3573
|
+
// This test's actual subject is the `/* @client */` attribute deferral
|
|
3574
|
+
// inside a loop body, which is orthogonal to how the array is sourced.
|
|
3592
3575
|
const result = compileJSX(`
|
|
3593
3576
|
"use client"
|
|
3594
3577
|
import { createSignal } from "@barefootjs/client"
|
|
@@ -3596,10 +3579,10 @@ type Day = { n: number }
|
|
|
3596
3579
|
export function C() {
|
|
3597
3580
|
const [sel] = createSignal(0)
|
|
3598
3581
|
const pred = (d: Day) => sel() === d.n
|
|
3599
|
-
const days
|
|
3582
|
+
const [days] = createSignal<Day[]>([{ n: 1 }, { n: 2 }])
|
|
3600
3583
|
return (
|
|
3601
3584
|
<div>
|
|
3602
|
-
{days.map((day: Day) => (
|
|
3585
|
+
{days().map((day: Day) => (
|
|
3603
3586
|
<div key={day.n} data-x={/* @client */ pred(day)}>cell</div>
|
|
3604
3587
|
))}
|
|
3605
3588
|
</div>
|
|
@@ -3608,9 +3591,269 @@ export function C() {
|
|
|
3608
3591
|
`.trimStart(), 'test.tsx', { adapter })
|
|
3609
3592
|
const template = result.files?.find(f => f.path.endsWith('.tmpl'))?.content ?? ''
|
|
3610
3593
|
expect(result.errors ?? []).toEqual([])
|
|
3611
|
-
// The loop still renders (the array is
|
|
3594
|
+
// The loop still renders (the array is signal-backed) but the deferred
|
|
3612
3595
|
// attribute is absent from the emitted `<div>` cell.
|
|
3613
3596
|
expect(template).toContain('range')
|
|
3614
3597
|
expect(template).not.toContain('data-x')
|
|
3615
3598
|
})
|
|
3616
3599
|
})
|
|
3600
|
+
|
|
3601
|
+
// #2087: the shared `isSupported` gate (expression-parser.ts, `logical` case)
|
|
3602
|
+
// now admits an EMPTY object-literal fallback (`x ?? {}`) as `??`'s right
|
|
3603
|
+
// operand — needed for the chart UI component's `<ChartConfigContext.Provider
|
|
3604
|
+
// value={{ config: props.config ?? {} }}>`. Every other template adapter has
|
|
3605
|
+
// a native `{}` dict/hashref to emit for this shape; Go's `text/template` has
|
|
3606
|
+
// none, so `GoTemplateAdapter.objectLiteral` is the one place left to refuse
|
|
3607
|
+
// it — self-reporting BF101 (the shared gate no longer does, since it now
|
|
3608
|
+
// considers the expression supported) and falling back to the safe `""`
|
|
3609
|
+
// sentinel so the emitted action stays valid Go template syntax rather than
|
|
3610
|
+
// splicing the `[UNSUPPORTED: …]` marker into an `or`/`and` operand.
|
|
3611
|
+
describe('GoTemplateAdapter - #2087 empty object-literal `?? {}` fallback', () => {
|
|
3612
|
+
test('a value-position `?? {}` (outside a Provider) raises BF101 and still emits a syntactically valid template', () => {
|
|
3613
|
+
const adapter = new GoTemplateAdapter()
|
|
3614
|
+
const result = compileJSX(`
|
|
3615
|
+
"use client"
|
|
3616
|
+
type Props = { config?: Record<string, string> }
|
|
3617
|
+
export function C(props: Props) {
|
|
3618
|
+
return <div>{props.config ?? {}}</div>
|
|
3619
|
+
}
|
|
3620
|
+
`.trimStart(), 'test.tsx', { adapter })
|
|
3621
|
+
expect(result.errors?.some(e => e.code === 'BF101')).toBe(true)
|
|
3622
|
+
const template = result.files?.find(f => f.path.endsWith('.tmpl'))?.content ?? ''
|
|
3623
|
+
// The `{}` fallback lowers to the safe `""` Go string sentinel — never the
|
|
3624
|
+
// `[UNSUPPORTED: …]` marker text, which would break `text/template` parsing
|
|
3625
|
+
// once spliced as an `or` operand.
|
|
3626
|
+
expect(template).toContain('{{or .Config ""}}')
|
|
3627
|
+
expect(template).not.toContain('UNSUPPORTED')
|
|
3628
|
+
})
|
|
3629
|
+
|
|
3630
|
+
test('the chart shape — a Provider value member falling back to `?? {}` — compiles clean with no BF101', () => {
|
|
3631
|
+
const adapter = new GoTemplateAdapter()
|
|
3632
|
+
const result = compileJSX(`
|
|
3633
|
+
"use client"
|
|
3634
|
+
import { createContext, useContext } from "@barefootjs/client"
|
|
3635
|
+
type ChartConfig = Record<string, string>
|
|
3636
|
+
const Ctx = createContext<{ config: ChartConfig }>({ config: {} })
|
|
3637
|
+
function Consumer() {
|
|
3638
|
+
const ctx = useContext(Ctx)
|
|
3639
|
+
return <span>{ctx.config.label ?? "none"}</span>
|
|
3640
|
+
}
|
|
3641
|
+
type Props = { config?: ChartConfig }
|
|
3642
|
+
export function C(props: Props) {
|
|
3643
|
+
return (
|
|
3644
|
+
<div>
|
|
3645
|
+
<Ctx.Provider value={{ config: props.config ?? {} }}>
|
|
3646
|
+
<Consumer />
|
|
3647
|
+
</Ctx.Provider>
|
|
3648
|
+
</div>
|
|
3649
|
+
)
|
|
3650
|
+
}
|
|
3651
|
+
`.trimStart(), 'test.tsx', { adapter })
|
|
3652
|
+
// The object-literal provider value never reaches `objectLiteral` (it's
|
|
3653
|
+
// handled structurally by `extendProviderContext` /
|
|
3654
|
+
// `providerObjectValueToGoMap` instead), so no BF101 fires here — matching
|
|
3655
|
+
// the `ui/compat.lock.json` `chart` × `go-template` `ok: true` entry.
|
|
3656
|
+
expect(result.errors ?? []).toEqual([])
|
|
3657
|
+
})
|
|
3658
|
+
|
|
3659
|
+
test('#2087: an object-shaped createContext default lowers the consumer field to a map, and the Provider bakes a matching Go map into the child constructor call', () => {
|
|
3660
|
+
// Mirrors the real cross-component wiring `renderGoTemplateComponent` does
|
|
3661
|
+
// (`registerChildComponentShape` for every sibling before the parent's
|
|
3662
|
+
// types/template are generated) — a bare `compileJSX` multi-component
|
|
3663
|
+
// compile never calls that hook, so it can't show the Provider→consumer
|
|
3664
|
+
// binding this test pins.
|
|
3665
|
+
const source = `
|
|
3666
|
+
"use client"
|
|
3667
|
+
import { createContext, useContext } from "@barefootjs/client"
|
|
3668
|
+
type ChartConfig = Record<string, string>
|
|
3669
|
+
const Ctx = createContext<{ config: ChartConfig }>({ config: {} })
|
|
3670
|
+
function Consumer() {
|
|
3671
|
+
const ctx = useContext(Ctx)
|
|
3672
|
+
return <span>{ctx.config.label ?? "none"}</span>
|
|
3673
|
+
}
|
|
3674
|
+
type Props = { config?: ChartConfig }
|
|
3675
|
+
export function Root(props: Props) {
|
|
3676
|
+
return (
|
|
3677
|
+
<div>
|
|
3678
|
+
<Ctx.Provider value={{ config: props.config ?? {} }}>
|
|
3679
|
+
<Consumer />
|
|
3680
|
+
</Ctx.Provider>
|
|
3681
|
+
</div>
|
|
3682
|
+
)
|
|
3683
|
+
}
|
|
3684
|
+
`.trimStart()
|
|
3685
|
+
|
|
3686
|
+
const consumerCtx = analyzeComponent(source, 'test.tsx', 'Consumer')
|
|
3687
|
+
const consumerIR: ComponentIR = {
|
|
3688
|
+
version: '0.1',
|
|
3689
|
+
metadata: buildMetadata(consumerCtx),
|
|
3690
|
+
root: jsxToIR(consumerCtx)!,
|
|
3691
|
+
errors: [],
|
|
3692
|
+
}
|
|
3693
|
+
const rootCtx = analyzeComponent(source, 'test.tsx', 'Root')
|
|
3694
|
+
const rootIR: ComponentIR = {
|
|
3695
|
+
version: '0.1',
|
|
3696
|
+
metadata: buildMetadata(rootCtx),
|
|
3697
|
+
root: jsxToIR(rootCtx)!,
|
|
3698
|
+
errors: [],
|
|
3699
|
+
}
|
|
3700
|
+
|
|
3701
|
+
const adapter = new GoTemplateAdapter()
|
|
3702
|
+
// Order matters: the child's shape (incl. its context consumers) must be
|
|
3703
|
+
// known before the parent's types/constructor are generated.
|
|
3704
|
+
adapter.registerChildComponentShape(consumerIR)
|
|
3705
|
+
const consumerTypes = adapter.generateTypes(consumerIR)!
|
|
3706
|
+
const rootTypes = adapter.generateTypes(rootIR)!
|
|
3707
|
+
const consumerTemplate = adapter.generate(consumerIR, { skipScriptRegistration: true }).template
|
|
3708
|
+
const rootTemplate = adapter.generate(rootIR, { skipScriptRegistration: true }).template
|
|
3709
|
+
|
|
3710
|
+
// Consumer's `Ctx` field is a real map, not the scalar `string` fallback
|
|
3711
|
+
// every other non-literal default used to produce.
|
|
3712
|
+
expect(consumerTypes).toContain('Ctx map[string]interface{}')
|
|
3713
|
+
|
|
3714
|
+
// The Provider's `{ config: props.config ?? {} }` bakes into a
|
|
3715
|
+
// `map[string]interface{}` Go expression, normalising the parent's
|
|
3716
|
+
// `interface{}`-typed `Config` field through `bf.AsMap` — NOT a bare
|
|
3717
|
+
// `.(map[string]interface{})` type assertion, which would silently drop
|
|
3718
|
+
// a caller-supplied typed map like `map[string]string` (the natural Go
|
|
3719
|
+
// shape for a `Record<string, string>` prop — #2111 review) — and
|
|
3720
|
+
// falling back to a real empty map when the value is nil/absent.
|
|
3721
|
+
expect(rootTypes).toContain('ConsumerSlot0: NewConsumerProps(ConsumerInput{')
|
|
3722
|
+
expect(rootTypes).toContain('Ctx: map[string]interface{}{"config": func() map[string]interface{} { if m := bf.AsMap(in.Config); m != nil { return m }; return map[string]interface{}{} }()},')
|
|
3723
|
+
|
|
3724
|
+
// The consumer's `ctx.config.label ?? 'none'` reads through `bf_get`
|
|
3725
|
+
// (case-tolerant `getFieldValue`), not a plain `.Ctx.Config.Label` dot
|
|
3726
|
+
// chain — the latter would require an exact-cased struct/map field that
|
|
3727
|
+
// never exists and crashes real `go run` execution.
|
|
3728
|
+
expect(consumerTemplate).toContain('{{or (bf_get (bf_get .Ctx "config") "label") "none"}}')
|
|
3729
|
+
expect(consumerTemplate).not.toContain('.Ctx.Config')
|
|
3730
|
+
})
|
|
3731
|
+
})
|
|
3732
|
+
|
|
3733
|
+
describe('GoTemplateAdapter - #2130 loop with element-wrapped child component', () => {
|
|
3734
|
+
// The `.{Name}s` wrapper-slice retarget is ONLY valid when the loop body IS
|
|
3735
|
+
// a single component (`loop.childComponent`) — that's the sole condition
|
|
3736
|
+
// under which `findNestedComponents` generates the slice field. A component
|
|
3737
|
+
// merely nested inside an element item used to retarget the range at a
|
|
3738
|
+
// slice that never exists (`{{range … := .Tags}}` with no `Tags` field →
|
|
3739
|
+
// `can't evaluate field Tags in type *LoopChildProbeProps`, a 500 at
|
|
3740
|
+
// render time).
|
|
3741
|
+
test('element-wrapped child: range iterates the real collection, child renders via the parent slot instance', () => {
|
|
3742
|
+
const adapter = new GoTemplateAdapter()
|
|
3743
|
+
const ir = compileToIR(`
|
|
3744
|
+
'use client'
|
|
3745
|
+
import { createSignal } from '@barefootjs/client'
|
|
3746
|
+
import { Tag } from '@ui/components/ui/tag'
|
|
3747
|
+
|
|
3748
|
+
interface Row { id: number; label: string }
|
|
3749
|
+
|
|
3750
|
+
export function LoopChildProbe(props: { rows?: Row[] }) {
|
|
3751
|
+
const [rows] = createSignal(props.rows ?? [])
|
|
3752
|
+
return (
|
|
3753
|
+
<ul>
|
|
3754
|
+
{rows().map(row => (
|
|
3755
|
+
<li key={row.id}><Tag>{row.label}</Tag></li>
|
|
3756
|
+
))}
|
|
3757
|
+
</ul>
|
|
3758
|
+
)
|
|
3759
|
+
}
|
|
3760
|
+
`, adapter)
|
|
3761
|
+
const { template, types } = adapter.generate(ir)
|
|
3762
|
+
|
|
3763
|
+
// The range targets the generated collection field, never a
|
|
3764
|
+
// `.{ChildName}s` slice that no struct declares.
|
|
3765
|
+
expect(template).toContain(':= .Rows}}')
|
|
3766
|
+
expect(template).not.toContain('.Tags}}')
|
|
3767
|
+
expect(types).not.toContain('Tags []')
|
|
3768
|
+
|
|
3769
|
+
// The wrapped child calls through the parent's once-per-slot instance
|
|
3770
|
+
// (root context `$`), with per-item children via the loop-body define.
|
|
3771
|
+
expect(template).toContain('{{template "Tag" (bf_with_children $.TagSlot1')
|
|
3772
|
+
expect(types).toContain('TagSlot1 TagProps')
|
|
3773
|
+
expect(types).toContain('TagSlot1: NewTagProps(TagInput{')
|
|
3774
|
+
})
|
|
3775
|
+
|
|
3776
|
+
// End-to-end on real Go: the emitted template + structs must compile AND
|
|
3777
|
+
// execute (`go run`) — the original failure mode was a request-time 500
|
|
3778
|
+
// (`html/template` resolves struct fields at execute time, not at Go
|
|
3779
|
+
// compile time). Cross-adapter note: this stays a Go-package test rather
|
|
3780
|
+
// than a shared conformance fixture because the EP-family adapters
|
|
3781
|
+
// (Mojo / ERB / Jinja / ...) render loop-nested children with per-item
|
|
3782
|
+
// `ComponentName_<random>` scope ids by design (`comp.slotId &&
|
|
3783
|
+
// !this.inLoop` in their renderComponent), while Hono / Go emit the
|
|
3784
|
+
// parent-slot-derived id — same DOM, different id shape, so a single
|
|
3785
|
+
// `expectedHtml` cannot pin both families.
|
|
3786
|
+
test('element-wrapped child renders per-item content on real Go', async () => {
|
|
3787
|
+
const adapter = new GoTemplateAdapter()
|
|
3788
|
+
let html: string
|
|
3789
|
+
try {
|
|
3790
|
+
html = await renderGoTemplateComponent({
|
|
3791
|
+
source: `
|
|
3792
|
+
'use client'
|
|
3793
|
+
import { createSignal } from '@barefootjs/client'
|
|
3794
|
+
import { Tag } from '@ui/components/ui/tag'
|
|
3795
|
+
|
|
3796
|
+
interface Row { id: number; label: string }
|
|
3797
|
+
|
|
3798
|
+
export function LoopChildProbe(props: { rows?: Row[] }) {
|
|
3799
|
+
const [rows] = createSignal(props.rows ?? [])
|
|
3800
|
+
return (
|
|
3801
|
+
<ul>
|
|
3802
|
+
{rows().map(row => (
|
|
3803
|
+
<li key={row.id}><Tag>{row.label}</Tag></li>
|
|
3804
|
+
))}
|
|
3805
|
+
</ul>
|
|
3806
|
+
)
|
|
3807
|
+
}
|
|
3808
|
+
`.trimStart(),
|
|
3809
|
+
adapter,
|
|
3810
|
+
components: {
|
|
3811
|
+
'@ui/components/ui/tag': `
|
|
3812
|
+
export function Tag(props: { children?: any }) {
|
|
3813
|
+
return <span class="tag">{props.children}</span>
|
|
3814
|
+
}
|
|
3815
|
+
`.trimStart(),
|
|
3816
|
+
},
|
|
3817
|
+
props: { rows: [{ id: 1, label: 'one' }, { id: 2, label: 'two' }] },
|
|
3818
|
+
})
|
|
3819
|
+
} catch (err) {
|
|
3820
|
+
if (err instanceof GoNotAvailableError) return
|
|
3821
|
+
throw err
|
|
3822
|
+
}
|
|
3823
|
+
// One <li> per row, each wrapping a rendered Tag with the row's label —
|
|
3824
|
+
// not a template error and not an empty loop.
|
|
3825
|
+
expect(html).toContain('data-key="1"')
|
|
3826
|
+
expect(html).toContain('data-key="2"')
|
|
3827
|
+
expect(html).toContain('>one<')
|
|
3828
|
+
expect(html).toContain('>two<')
|
|
3829
|
+
expect(html).toContain('class="tag"')
|
|
3830
|
+
})
|
|
3831
|
+
|
|
3832
|
+
// Companion guard: the wrapper-slice path is untouched — a loop body that
|
|
3833
|
+
// IS a single component still iterates `.{Name}s` (the todo-app / data-table
|
|
3834
|
+
// machinery this fix must not regress).
|
|
3835
|
+
test('single-component body keeps iterating the wrapper slice', () => {
|
|
3836
|
+
const adapter = new GoTemplateAdapter()
|
|
3837
|
+
const ir = compileToIR(`
|
|
3838
|
+
'use client'
|
|
3839
|
+
import { createSignal } from '@barefootjs/client'
|
|
3840
|
+
import { TodoItem } from '@ui/components/ui/todo-item'
|
|
3841
|
+
|
|
3842
|
+
interface Todo { id: number; text: string }
|
|
3843
|
+
|
|
3844
|
+
export function TodoList(props: { todos?: Todo[] }) {
|
|
3845
|
+
const [todos] = createSignal(props.todos ?? [])
|
|
3846
|
+
return (
|
|
3847
|
+
<ul>
|
|
3848
|
+
{todos().map(todo => (
|
|
3849
|
+
<TodoItem key={todo.id} text={todo.text} />
|
|
3850
|
+
))}
|
|
3851
|
+
</ul>
|
|
3852
|
+
)
|
|
3853
|
+
}
|
|
3854
|
+
`, adapter)
|
|
3855
|
+
const { template, types } = adapter.generate(ir)
|
|
3856
|
+
expect(template).toContain(':= .TodoItems}}')
|
|
3857
|
+
expect(types).toContain('TodoItems []')
|
|
3858
|
+
})
|
|
3859
|
+
})
|