@barefootjs/go-template 0.17.0 → 0.18.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/expr/url-builder.d.ts.map +1 -1
- package/dist/adapter/go-template-adapter.d.ts +144 -14
- package/dist/adapter/go-template-adapter.d.ts.map +1 -1
- package/dist/adapter/index.js +449 -118
- package/dist/adapter/lib/compile-state.d.ts +27 -1
- package/dist/adapter/lib/compile-state.d.ts.map +1 -1
- package/dist/adapter/lib/go-emit.d.ts +9 -0
- package/dist/adapter/lib/go-emit.d.ts.map +1 -1
- package/dist/adapter/lib/go-naming.d.ts +23 -0
- package/dist/adapter/lib/go-naming.d.ts.map +1 -1
- package/dist/adapter/memo/memo-compute.d.ts +54 -5
- package/dist/adapter/memo/memo-compute.d.ts.map +1 -1
- package/dist/adapter/memo/memo-type.d.ts +10 -0
- package/dist/adapter/memo/memo-type.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 +449 -118
- 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 +468 -118
- package/package.json +3 -3
- package/src/__tests__/go-template-adapter.test.ts +652 -129
- 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 +637 -140
- package/src/adapter/lib/compile-state.ts +31 -0
- package/src/adapter/lib/go-emit.ts +20 -0
- package/src/adapter/lib/go-naming.ts +29 -0
- package/src/adapter/memo/memo-compute.ts +228 -18
- package/src/adapter/memo/memo-type.ts +19 -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
- package/src/test-render.ts +46 -8
|
@@ -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',
|
|
@@ -58,119 +63,45 @@ runAdapterConformanceTests({
|
|
|
58
63
|
// a `SearchParams bf.SearchParams` binding (zero value → empty query → the
|
|
59
64
|
// author's default). See #1922; Mojo / Xslate stay skipped pending their own
|
|
60
65
|
// env-signal lowering + per-request Perl reader.
|
|
61
|
-
|
|
66
|
+
//
|
|
67
|
+
// `search-params-derived-filter` (#2075) is no longer skipped: a LIST-
|
|
68
|
+
// valued memo derived from the searchParams() env signal (the blog
|
|
69
|
+
// `visible` filter shape) now SSR-computes via `bf.FilterEval` — the
|
|
70
|
+
// memo's field type is `[]any` and its constructor init serializes the
|
|
71
|
+
// `.filter` predicate to the runtime evaluator. See the #2075 constructor
|
|
72
|
+
// pins below.
|
|
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.
|
|
62
87
|
// Per-fixture build-time contracts for shapes the Go template
|
|
63
|
-
// adapter intentionally refuses to lower. Lives
|
|
64
|
-
// shared fixtures) so adding a new adapter doesn't require
|
|
65
|
-
// 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
|
|
66
91
|
// refusal set against the canonical fixture corpus.
|
|
67
|
-
expectedDiagnostics:
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
// call it inside a keyed `.map`. Same BF103 surface as
|
|
81
|
-
// `static-array-children` above — pinned at adapter level so the
|
|
82
|
-
// shared-component corpus stays adapter-neutral.
|
|
83
|
-
'todo-app': [{ code: 'BF103', severity: 'error' }],
|
|
84
|
-
'todo-app-ssr': [{ code: 'BF103', severity: 'error' }],
|
|
85
|
-
// Array-destructure loop param (`([k, v]) => ...`): Go's `{{range
|
|
86
|
-
// $a, $b := ...}}` only supports single-name bindings, so the
|
|
87
|
-
// adapter would otherwise emit invalid template syntax.
|
|
88
|
-
'static-array-from-props': [{ code: 'BF104', severity: 'error' }],
|
|
89
|
-
// Same destructure shape with a child component body — fires both
|
|
90
|
-
// BF103 (imported child in loop) and BF104 (destructure param).
|
|
91
|
-
'static-array-from-props-with-component': [
|
|
92
|
-
{ code: 'BF103', severity: 'error' },
|
|
93
|
-
{ code: 'BF104', severity: 'error' },
|
|
94
|
-
],
|
|
95
|
-
// (`style-3-signals` graduated alongside `style-object-dynamic` — see note
|
|
96
|
-
// above; the `style={{ … }}` object now lowers to a CSS string.)
|
|
97
|
-
// #1244 stress catalog: tagged-template-literal callees
|
|
98
|
-
// (`cn\`base \${tone()}\``) likewise can't lower into Go template
|
|
99
|
-
// syntax — same BF101 refusal.
|
|
100
|
-
'tagged-template-classname': [{ code: 'BF101', severity: 'error' }],
|
|
101
|
-
// #1310: rest destructure in .map() callback. The object-rest shape read
|
|
102
|
-
// via member access (`rest-destructure-object-in-map`) now lowers — each
|
|
103
|
-
// binding resolves to a field on a synthetic `$__bf_item0` range var (the
|
|
104
|
-
// reserved `__bf_item` name, depth-suffixed) and `rest.flag` →
|
|
105
|
-
// `$__bf_item0.Flag` (`destructureBindingsSupportable`). The
|
|
106
|
-
// other three stay refused: rest SPREAD (`{...rest}`) needs a residual
|
|
107
|
-
// object, and array-index / nested paths (`[a, ...t]`, `{ cells: [h] }`)
|
|
108
|
-
// need index/slice machinery Go's `{{range}}` can't express inline.
|
|
109
|
-
'rest-destructure-object-spread-in-map': [{ code: 'BF104', severity: 'error' }],
|
|
110
|
-
'rest-destructure-array-in-map': [{ code: 'BF104', severity: 'error' }],
|
|
111
|
-
'rest-destructure-nested-in-map': [{ code: 'BF104', severity: 'error' }],
|
|
112
|
-
// #1443: `[a, b].filter(Boolean).join(' ')` (registry Slot) now
|
|
113
|
-
// lowers to `bf_join (bf_filter_truthy (bf_arr ...)) " "`. No
|
|
114
|
-
// BF101 expected — pinned positively by the
|
|
115
|
-
// `branch-local-filter-join-go` template-output test below.
|
|
116
|
-
//
|
|
117
|
-
// #1448 Tier A — JS Array / String methods that the Go template
|
|
118
|
-
// adapter hasn't lowered yet. Each row drops once the
|
|
119
|
-
// corresponding method PR lands. Hono / CSR pass these out of
|
|
120
|
-
// the box (they evaluate JS at runtime) so the pin only applies
|
|
121
|
-
// here.
|
|
122
|
-
//
|
|
123
|
-
// `array-includes` / `string-includes` no longer pinned — both
|
|
124
|
-
// shapes lower via the shared `array-method` IR + the polymorphic
|
|
125
|
-
// `bf_includes` runtime helper that dispatches on
|
|
126
|
-
// `reflect.Kind()` (slice/array → element search, string →
|
|
127
|
-
// substring search). The condition-position lowering picks up
|
|
128
|
-
// the same emit through the `array-method` arm of
|
|
129
|
-
// `renderConditionExpr` (#1448 Tier A first PR).
|
|
130
|
-
//
|
|
131
|
-
// Remaining fixtures land at expression position and surface BF101
|
|
132
|
-
// via `convertExpressionToGo`. Distinct codes for the two paths is
|
|
133
|
-
// pre-existing adapter behaviour, not something this catalog
|
|
134
|
-
// should paper over — pinned literally here.
|
|
135
|
-
// `array-indexOf` / `array-lastIndexOf` no longer pinned —
|
|
136
|
-
// value-equality `bf_index_of` / `bf_last_index_of` Go runtime
|
|
137
|
-
// helpers handle the shape (#1448 Tier A second PR).
|
|
138
|
-
// `array-at` no longer pinned — the pre-existing `bf_at` runtime
|
|
139
|
-
// helper now lowers `.at(i)` (#1448 Tier A third PR).
|
|
140
|
-
// `array-concat` no longer pinned — the new `bf_concat` runtime
|
|
141
|
-
// helper merges two arrays into a single `[]any` (#1448 Tier A
|
|
142
|
-
// fourth PR).
|
|
143
|
-
// `array-slice` no longer pinned — the new `bf_slice` runtime
|
|
144
|
-
// helper carves out a sub-range with JS-compat clamping
|
|
145
|
-
// (#1448 Tier A fifth PR).
|
|
146
|
-
// `array-reverse` / `array-toReversed` no longer pinned —
|
|
147
|
-
// both share the `bf_reverse` helper since SSR templates
|
|
148
|
-
// render a snapshot and the JS mutate-vs-new distinction has
|
|
149
|
-
// no template-level meaning (#1448 Tier A sixth PR).
|
|
150
|
-
// `string-toLowerCase` / `string-toUpperCase` no longer pinned —
|
|
151
|
-
// pre-existing `bf_lower` / `bf_upper` runtime helpers wire to
|
|
152
|
-
// the JS method names at the adapter layer (#1448 Tier A
|
|
153
|
-
// seventh + eighth PRs).
|
|
154
|
-
// `string-trim` no longer pinned — pre-existing `bf_trim`
|
|
155
|
-
// (wraps `strings.TrimSpace`) handles the strip (#1448 Tier A
|
|
156
|
-
// ninth PR, closing out Tier A).
|
|
157
|
-
},
|
|
158
|
-
// `JSON_STRINGIFY_VIA_CONST` and `MATH_FLOOR_VIA_CONST` now pass
|
|
159
|
-
// via `GoTemplateAdapter.templatePrimitives` (#1188). The two
|
|
160
|
-
// remaining cases stay skipped because the V1 registry is
|
|
161
|
-
// identifier-path-only and explicit:
|
|
162
|
-
// - `USER_IMPORT_VIA_CONST` — a bespoke user import isn't in
|
|
163
|
-
// the registry and can't be rendered server-side without
|
|
164
|
-
// user-supplied template-fn mappings.
|
|
165
|
-
// - `NO_DOUBLE_REWRITE_OF_PROPS_OBJECT` — uses `customSerialize`
|
|
166
|
-
// too, same reason.
|
|
167
|
-
// Adding new entries to `templatePrimitives` should narrow this
|
|
168
|
-
// skip set; see `templatePrimitives` declaration in
|
|
169
|
-
// `go-template-adapter.ts` for the full V1 surface.
|
|
170
|
-
skipTemplatePrimitives: new Set([
|
|
171
|
-
TemplatePrimitiveCaseId.USER_IMPORT_VIA_CONST,
|
|
172
|
-
TemplatePrimitiveCaseId.NO_DOUBLE_REWRITE_OF_PROPS_OBJECT,
|
|
173
|
-
]),
|
|
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").
|
|
174
105
|
skipMarkerConformance: new Set<string>([
|
|
175
106
|
// Same as Hono / Mojo: `/* @client */` markers on TodoApp's keyed
|
|
176
107
|
// `.map` intentionally elide a slot id from the SSR template that
|
|
@@ -899,10 +830,18 @@ export function Dyn() {
|
|
|
899
830
|
expect(types).toContain('Items: nil,')
|
|
900
831
|
})
|
|
901
832
|
|
|
902
|
-
test('
|
|
903
|
-
// A quoted key like "data-id"
|
|
904
|
-
// valid Go struct field identifier
|
|
905
|
-
//
|
|
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.
|
|
906
845
|
const adapter = new GoTemplateAdapter()
|
|
907
846
|
const ir = compileToIR(`
|
|
908
847
|
"use client"
|
|
@@ -915,7 +854,56 @@ export function Rows() {
|
|
|
915
854
|
}
|
|
916
855
|
`)
|
|
917
856
|
const types = adapter.generate(ir).types!
|
|
918
|
-
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')
|
|
919
907
|
})
|
|
920
908
|
|
|
921
909
|
test('collapses whitespace-padded empty array literal to nil (#1675 review)', () => {
|
|
@@ -999,10 +987,10 @@ function Box({ label }: { label: string }) {
|
|
|
999
987
|
`
|
|
1000
988
|
const { types } = compileAndGenerate(source)
|
|
1001
989
|
// The condition prop and the value reference resolve to `in.<Field>`,
|
|
1002
|
-
// the static key is preserved.
|
|
1003
|
-
//
|
|
1004
|
-
//
|
|
1005
|
-
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 != "" {')
|
|
1006
994
|
expect(types).toContain('return map[string]any{"data-label": in.Label}')
|
|
1007
995
|
})
|
|
1008
996
|
|
|
@@ -1830,6 +1818,31 @@ export function TodoList() {
|
|
|
1830
1818
|
const bf101 = adapter.errors.filter(e => e.code === 'BF101')
|
|
1831
1819
|
expect(bf101).toEqual([])
|
|
1832
1820
|
})
|
|
1821
|
+
|
|
1822
|
+
test('nested-callback refusal keeps the emitted template syntactically valid (#2038)', () => {
|
|
1823
|
+
// The BF101 contract itself is pinned at the shared conformance layer
|
|
1824
|
+
// (`filter-nested-callback-predicate` / `filter-nested-find-predicate`
|
|
1825
|
+
// expectedDiagnostics; the `/* @client */` suppression twin renders
|
|
1826
|
+
// clean). This test pins the GO-SPECIFIC half: the refusal must emit
|
|
1827
|
+
// the `false` sentinel — not a half-rendered predicate like `.Some` —
|
|
1828
|
+
// so `text/template` parsing doesn't cascade into secondary errors.
|
|
1829
|
+
const adapter = new GoTemplateAdapter()
|
|
1830
|
+
const result = compileAndGenerate(`
|
|
1831
|
+
"use client"
|
|
1832
|
+
import { createSignal } from "@barefootjs/client"
|
|
1833
|
+
|
|
1834
|
+
type Item = { id: number }
|
|
1835
|
+
|
|
1836
|
+
export function Picker() {
|
|
1837
|
+
const [items, setItems] = createSignal<Item[]>([])
|
|
1838
|
+
const [picked, setPicked] = createSignal<Item[]>([])
|
|
1839
|
+
return <ul>{items().filter(t => !picked().some(p => p.id === t.id)).map(t => <li key={t.id}>{t.id}</li>)}</ul>
|
|
1840
|
+
}
|
|
1841
|
+
`, adapter)
|
|
1842
|
+
expect(adapter.errors.some(e => e.code === 'BF101')).toBe(true)
|
|
1843
|
+
expect(result.template).not.toContain('.Some')
|
|
1844
|
+
expect(result.template).toContain('{{if false}}')
|
|
1845
|
+
})
|
|
1833
1846
|
})
|
|
1834
1847
|
|
|
1835
1848
|
describe('find/findIndex - adapter specific', () => {
|
|
@@ -2991,6 +3004,238 @@ export { C }
|
|
|
2991
3004
|
})
|
|
2992
3005
|
})
|
|
2993
3006
|
|
|
3007
|
+
describe('GoTemplateAdapter - #2075 searchParams()-derived memo constructor', () => {
|
|
3008
|
+
// A memo derived from the createSearchParams() env signal SSR-computes in
|
|
3009
|
+
// the generated constructor from the canonical `in.SearchParams` reader —
|
|
3010
|
+
// including under a local alias. `Get` returns "" for an absent key, so
|
|
3011
|
+
// the `??` default fires on "" — the same documented `?? → or` divergence
|
|
3012
|
+
// as the template-position lowering.
|
|
3013
|
+
test('computes an aliased `?? default` derived memo from in.SearchParams', () => {
|
|
3014
|
+
const adapter = new GoTemplateAdapter()
|
|
3015
|
+
const ir = compileToIR(`
|
|
3016
|
+
'use client'
|
|
3017
|
+
import { createMemo, createSearchParams } from '@barefootjs/client'
|
|
3018
|
+
export function SortStatus() {
|
|
3019
|
+
const [sp] = createSearchParams()
|
|
3020
|
+
const sort = createMemo(() => sp().get('sort') ?? 'date')
|
|
3021
|
+
return <p>sort: {sort()}</p>
|
|
3022
|
+
}
|
|
3023
|
+
`, adapter)
|
|
3024
|
+
const { types } = adapter.generate(ir)
|
|
3025
|
+
expect(types).toContain(
|
|
3026
|
+
'Sort: func() string { if v := in.SearchParams.Get("sort"); v != "" { return v }; return "date" }(),',
|
|
3027
|
+
)
|
|
3028
|
+
})
|
|
3029
|
+
|
|
3030
|
+
test('computes a bare env read memo from in.SearchParams', () => {
|
|
3031
|
+
const adapter = new GoTemplateAdapter()
|
|
3032
|
+
const ir = compileToIR(`
|
|
3033
|
+
'use client'
|
|
3034
|
+
import { createMemo, createSearchParams } from '@barefootjs/client'
|
|
3035
|
+
export function QueryEcho() {
|
|
3036
|
+
const [searchParams] = createSearchParams()
|
|
3037
|
+
const q = createMemo(() => searchParams().get('q'))
|
|
3038
|
+
return <p>{q()}</p>
|
|
3039
|
+
}
|
|
3040
|
+
`, adapter)
|
|
3041
|
+
const { types } = adapter.generate(ir)
|
|
3042
|
+
expect(types).toContain('Q: in.SearchParams.Get("q"),')
|
|
3043
|
+
})
|
|
3044
|
+
|
|
3045
|
+
// #2075 residual: a LIST-valued memo (`.filter(arrow)` over a props array)
|
|
3046
|
+
// chained off the env-derived `tag` memo — the `search-params-derived-filter`
|
|
3047
|
+
// shared fixture's shape. The field is `[]any` (not the misclassified `bool`
|
|
3048
|
+
// the `/=>\s*!/` heuristic used to produce from the predicate's `!tag()`),
|
|
3049
|
+
// and the constructor seeds it via `bf.FilterEval`, whose serialized
|
|
3050
|
+
// predicate JSON is embedded (escaped) in the Go string literal.
|
|
3051
|
+
test('computes a list-filter memo chained off a searchParams()-derived memo via bf.FilterEval', () => {
|
|
3052
|
+
const adapter = new GoTemplateAdapter()
|
|
3053
|
+
const ir = compileToIR(`
|
|
3054
|
+
'use client'
|
|
3055
|
+
import { createMemo, createSearchParams } from '@barefootjs/client'
|
|
3056
|
+
export function TaggedList(props: { items: { title: string; tags: string[] }[] }) {
|
|
3057
|
+
const [searchParams] = createSearchParams()
|
|
3058
|
+
const tag = createMemo(() => searchParams().get('tag') ?? '')
|
|
3059
|
+
const visible = createMemo(() => props.items.filter((p) => !tag() || p.tags.includes(tag())))
|
|
3060
|
+
return <ul>{visible().map((p) => <li key={p.title}>{p.title}</li>)}</ul>
|
|
3061
|
+
}
|
|
3062
|
+
`, adapter)
|
|
3063
|
+
const { types } = adapter.generate(ir)
|
|
3064
|
+
expect(types).toContain('Visible []any')
|
|
3065
|
+
expect(types).toContain('Visible: bf.FilterEval(in.Items,')
|
|
3066
|
+
// The predicate JSON is escaped inside a Go double-quoted string literal,
|
|
3067
|
+
// so assert on the substring that survives escaping (backslash-escaped
|
|
3068
|
+
// quotes around the JSON keys/values).
|
|
3069
|
+
expect(types).toContain('\\"method\\":\\"includes\\"')
|
|
3070
|
+
})
|
|
3071
|
+
|
|
3072
|
+
// #2077 review finding 3: `tag` (an earlier sibling the `visible` filter's
|
|
3073
|
+
// predicate closes over) used to be recomputed verbatim a second time
|
|
3074
|
+
// inline in `visible`'s `bf.FilterEval` env map — the same
|
|
3075
|
+
// `in.SearchParams.Get("tag")` expression emitted twice in the constructor.
|
|
3076
|
+
// It's now hoisted to a shared local before the `return`, referenced by
|
|
3077
|
+
// BOTH the `Tag:` field and the env-map entry.
|
|
3078
|
+
test('hoists the earlier sibling memo instead of duplicating its expression', () => {
|
|
3079
|
+
const adapter = new GoTemplateAdapter()
|
|
3080
|
+
const ir = compileToIR(`
|
|
3081
|
+
'use client'
|
|
3082
|
+
import { createMemo, createSearchParams } from '@barefootjs/client'
|
|
3083
|
+
export function TaggedList(props: { items: { title: string; tags: string[] }[] }) {
|
|
3084
|
+
const [searchParams] = createSearchParams()
|
|
3085
|
+
const tag = createMemo(() => searchParams().get('tag') ?? '')
|
|
3086
|
+
const visible = createMemo(() => props.items.filter((p) => !tag() || p.tags.includes(tag())))
|
|
3087
|
+
return <ul>{visible().map((p) => <li key={p.title}>{p.title}</li>)}</ul>
|
|
3088
|
+
}
|
|
3089
|
+
`, adapter)
|
|
3090
|
+
const { types } = adapter.generate(ir)
|
|
3091
|
+
expect(types).toContain('var memoTag string =')
|
|
3092
|
+
expect(types).toContain('Tag: memoTag,')
|
|
3093
|
+
expect(types).toContain('"tag": memoTag')
|
|
3094
|
+
// The multi-token `Get("tag")` expression appears exactly once in the
|
|
3095
|
+
// whole constructor — the hoisted local, not a second inline copy.
|
|
3096
|
+
expect(types.split('in.SearchParams.Get("tag")').length).toBe(2)
|
|
3097
|
+
})
|
|
3098
|
+
})
|
|
3099
|
+
|
|
3100
|
+
describe('GoTemplateAdapter - env-derived memo sourced from the SSR seed plan (Package I)', () => {
|
|
3101
|
+
// `CompileState.ssrSeedPlan` (and `envSignalReadersByLocal` derived from it)
|
|
3102
|
+
// is now the single authority for env-reader recognition. `compileToIR`
|
|
3103
|
+
// already JSON-round-trips the IR (mirrors the adapter conformance
|
|
3104
|
+
// harness), so each `env-reader` step's `reader.methods` — a ReadonlySet —
|
|
3105
|
+
// arrives serialized to `{}`; `primeCompileState` re-resolves a live reader
|
|
3106
|
+
// via `envSignalReaderFor(step.reader.key)` before trusting `.methods`.
|
|
3107
|
+
// Pins that the env-derived memo constructor still SSR-computes through
|
|
3108
|
+
// that re-resolution instead of silently falling back to a zero value.
|
|
3109
|
+
test('computes an env-derived memo constructor through the JSON-round-tripped ssrSeedPlan', () => {
|
|
3110
|
+
const adapter = new GoTemplateAdapter()
|
|
3111
|
+
const ir = compileToIR(`
|
|
3112
|
+
'use client'
|
|
3113
|
+
import { createMemo, createSearchParams } from '@barefootjs/client'
|
|
3114
|
+
export function QueryEcho() {
|
|
3115
|
+
const [searchParams] = createSearchParams()
|
|
3116
|
+
const q = createMemo(() => searchParams().get('q'))
|
|
3117
|
+
return <p>{q()}</p>
|
|
3118
|
+
}
|
|
3119
|
+
`, adapter)
|
|
3120
|
+
const { types } = adapter.generate(ir)
|
|
3121
|
+
expect(types).toContain('Q: in.SearchParams.Get("q"),')
|
|
3122
|
+
})
|
|
3123
|
+
})
|
|
3124
|
+
|
|
3125
|
+
describe('GoTemplateAdapter - #2077 review sibling-memo recursion guard', () => {
|
|
3126
|
+
// Finding 1: mutually-referencing memos (each resolving the other as a
|
|
3127
|
+
// filter-arm free var, or as a bare-getter ternary condition) used to
|
|
3128
|
+
// recurse without bound and blow the compiler's call stack
|
|
3129
|
+
// (`RangeError: Maximum call stack size exceeded`, verified by probe before
|
|
3130
|
+
// the fix). The cycle/declaration-order guard makes both directions fall
|
|
3131
|
+
// back to the field's Go zero value instead of throwing.
|
|
3132
|
+
test('mutual filter-arm memos compile without throwing and fall back to zero values', () => {
|
|
3133
|
+
const adapter = new GoTemplateAdapter()
|
|
3134
|
+
const ir = compileToIR(`
|
|
3135
|
+
'use client'
|
|
3136
|
+
import { createMemo } from '@barefootjs/client'
|
|
3137
|
+
export function C(props: { items: { title: string }[] }) {
|
|
3138
|
+
const a = createMemo(() => props.items.filter((x) => Boolean(b())))
|
|
3139
|
+
const b = createMemo(() => props.items.filter((x) => Boolean(a())))
|
|
3140
|
+
return <ul>{a().map((p) => <li key={p.title}>{p.title}</li>)}</ul>
|
|
3141
|
+
}
|
|
3142
|
+
`, adapter)
|
|
3143
|
+
let types = ''
|
|
3144
|
+
expect(() => { types = adapter.generate(ir).types }).not.toThrow()
|
|
3145
|
+
expect(types).toContain('A []any')
|
|
3146
|
+
expect(types).toContain('B []any')
|
|
3147
|
+
// Neither memo can resolve the OTHER as a free var (order rules out both
|
|
3148
|
+
// directions at once — `a` can't see `b` because `b` is declared later,
|
|
3149
|
+
// and vice versa), so `a`'s own computation falls back to the `[]any`
|
|
3150
|
+
// zero value (hoisted since `b` references it), and `b`'s free-var
|
|
3151
|
+
// resolution of `a` reuses that hoisted zero-valued local.
|
|
3152
|
+
expect(types).toContain('var memoA []any = nil')
|
|
3153
|
+
expect(types).toContain('A: memoA,')
|
|
3154
|
+
expect(types).toContain('B: bf.FilterEval(in.Items,')
|
|
3155
|
+
expect(types).toContain('map[string]any{"a": memoA}')
|
|
3156
|
+
})
|
|
3157
|
+
|
|
3158
|
+
test('mutual ternary memos (bare-getter condition) compile without throwing and fall back to zero values', () => {
|
|
3159
|
+
const adapter = new GoTemplateAdapter()
|
|
3160
|
+
const ir = compileToIR(`
|
|
3161
|
+
'use client'
|
|
3162
|
+
import { createMemo } from '@barefootjs/client'
|
|
3163
|
+
export function C() {
|
|
3164
|
+
const a = createMemo(() => (b() ? 'A' : 'Z'))
|
|
3165
|
+
const b = createMemo(() => (a() ? 'B' : 'Y'))
|
|
3166
|
+
return <p>{a()}{b()}</p>
|
|
3167
|
+
}
|
|
3168
|
+
`, adapter)
|
|
3169
|
+
let types = ''
|
|
3170
|
+
expect(() => { types = adapter.generate(ir).types }).not.toThrow()
|
|
3171
|
+
expect(types).toContain('A: "",')
|
|
3172
|
+
expect(types).toContain('B: "",')
|
|
3173
|
+
})
|
|
3174
|
+
|
|
3175
|
+
test('self-referencing filter-arm memo compiles without throwing', () => {
|
|
3176
|
+
const adapter = new GoTemplateAdapter()
|
|
3177
|
+
const ir = compileToIR(`
|
|
3178
|
+
'use client'
|
|
3179
|
+
import { createMemo } from '@barefootjs/client'
|
|
3180
|
+
export function C(props: { items: { title: string }[] }) {
|
|
3181
|
+
const a = createMemo(() => props.items.filter((x) => Boolean(a())))
|
|
3182
|
+
return <ul>{a().map((p) => <li key={p.title}>{p.title}</li>)}</ul>
|
|
3183
|
+
}
|
|
3184
|
+
`, adapter)
|
|
3185
|
+
let types = ''
|
|
3186
|
+
expect(() => { types = adapter.generate(ir).types }).not.toThrow()
|
|
3187
|
+
expect(types).toContain('A: nil,')
|
|
3188
|
+
})
|
|
3189
|
+
|
|
3190
|
+
test('self-referencing ternary memo compiles without throwing', () => {
|
|
3191
|
+
const adapter = new GoTemplateAdapter()
|
|
3192
|
+
const ir = compileToIR(`
|
|
3193
|
+
'use client'
|
|
3194
|
+
import { createMemo } from '@barefootjs/client'
|
|
3195
|
+
export function C() {
|
|
3196
|
+
const a = createMemo(() => (a() ? 'A' : 'Z'))
|
|
3197
|
+
return <p>{a()}</p>
|
|
3198
|
+
}
|
|
3199
|
+
`, adapter)
|
|
3200
|
+
let types = ''
|
|
3201
|
+
expect(() => { types = adapter.generate(ir).types }).not.toThrow()
|
|
3202
|
+
expect(types).toContain('A: "",')
|
|
3203
|
+
})
|
|
3204
|
+
})
|
|
3205
|
+
|
|
3206
|
+
describe('GoTemplateAdapter - #2073 value-producing .map(cb)', () => {
|
|
3207
|
+
function emitMap(expr: string): string {
|
|
3208
|
+
const adapter = new GoTemplateAdapter()
|
|
3209
|
+
const ir = compileToIR(`
|
|
3210
|
+
function C({ tags, users }: { tags: string[]; users: { name: string }[] }) {
|
|
3211
|
+
return <div>{${expr}}</div>
|
|
3212
|
+
}
|
|
3213
|
+
export { C }
|
|
3214
|
+
`, adapter)
|
|
3215
|
+
return adapter.generate(ir).template ?? ''
|
|
3216
|
+
}
|
|
3217
|
+
|
|
3218
|
+
// The blog-showcase shape (#1938/#1939): a value-returning `.map` (string
|
|
3219
|
+
// projection, not JSX) lowers through the evaluator — `bf_map_eval` projects
|
|
3220
|
+
// each element (no flatten) and composes through `bf_join`. (Like the
|
|
3221
|
+
// flatMap pins below, the projection JSON's quotes are backslash-escaped in
|
|
3222
|
+
// the Go-template string; the JSON itself is verified by the render
|
|
3223
|
+
// conformance fixtures + the runtime TestMapEval.)
|
|
3224
|
+
test('.map(t => `#${t}`).join(" ") emits bf_map_eval composed into bf_join', () => {
|
|
3225
|
+
const t = emitMap("tags.map(t => `#${t}`).join(' ')")
|
|
3226
|
+
expect(t).toContain('bf_join (bf_map_eval .Tags')
|
|
3227
|
+
})
|
|
3228
|
+
|
|
3229
|
+
test('.map(u => u.name) emits bf_map_eval with the receiver', () => {
|
|
3230
|
+
const t = emitMap("users.map(u => u.name).join(', ')")
|
|
3231
|
+
expect(t).toContain('bf_map_eval .Users')
|
|
3232
|
+
})
|
|
3233
|
+
|
|
3234
|
+
// The function-reference `.map(format)` BF101 refusal is now covered
|
|
3235
|
+
// cross-adapter by the `array-map-function-reference` shared fixture's
|
|
3236
|
+
// `expectedDiagnostics` entry above.
|
|
3237
|
+
})
|
|
3238
|
+
|
|
2994
3239
|
describe('GoTemplateAdapter - #1448 Tier C .flatMap(field projection)', () => {
|
|
2995
3240
|
function emitFlatMap(expr: string): string {
|
|
2996
3241
|
const adapter = new GoTemplateAdapter()
|
|
@@ -3194,7 +3439,6 @@ export { C }
|
|
|
3194
3439
|
|
|
3195
3440
|
// Tier B `.sort` / `.toSorted` follow-ups still refused with BF021.
|
|
3196
3441
|
const unsupportedSort: Array<[string, string]> = [
|
|
3197
|
-
['function-reference comparator', `items().toSorted(myCmp).map(x => <li key={x.name}>{x.name}</li>)`],
|
|
3198
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>)`],
|
|
3199
3443
|
]
|
|
3200
3444
|
for (const [label, chain] of unsupportedSort) {
|
|
@@ -3217,6 +3461,17 @@ export function C() {
|
|
|
3217
3461
|
})
|
|
3218
3462
|
}
|
|
3219
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
|
+
|
|
3220
3475
|
// End-to-end proof via `go run`: the `@client` form renders a
|
|
3221
3476
|
// `<!--bf-client:sN-->` placeholder. The bare form is now caught at
|
|
3222
3477
|
// build with BF101 and degrades to an empty, render-safe slot (no
|
|
@@ -3309,6 +3564,14 @@ export function C() {
|
|
|
3309
3564
|
|
|
3310
3565
|
test('@client attribute inside a keyed .map() loop body is also deferred', () => {
|
|
3311
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.
|
|
3312
3575
|
const result = compileJSX(`
|
|
3313
3576
|
"use client"
|
|
3314
3577
|
import { createSignal } from "@barefootjs/client"
|
|
@@ -3316,10 +3579,10 @@ type Day = { n: number }
|
|
|
3316
3579
|
export function C() {
|
|
3317
3580
|
const [sel] = createSignal(0)
|
|
3318
3581
|
const pred = (d: Day) => sel() === d.n
|
|
3319
|
-
const days
|
|
3582
|
+
const [days] = createSignal<Day[]>([{ n: 1 }, { n: 2 }])
|
|
3320
3583
|
return (
|
|
3321
3584
|
<div>
|
|
3322
|
-
{days.map((day: Day) => (
|
|
3585
|
+
{days().map((day: Day) => (
|
|
3323
3586
|
<div key={day.n} data-x={/* @client */ pred(day)}>cell</div>
|
|
3324
3587
|
))}
|
|
3325
3588
|
</div>
|
|
@@ -3328,9 +3591,269 @@ export function C() {
|
|
|
3328
3591
|
`.trimStart(), 'test.tsx', { adapter })
|
|
3329
3592
|
const template = result.files?.find(f => f.path.endsWith('.tmpl'))?.content ?? ''
|
|
3330
3593
|
expect(result.errors ?? []).toEqual([])
|
|
3331
|
-
// The loop still renders (the array is
|
|
3594
|
+
// The loop still renders (the array is signal-backed) but the deferred
|
|
3332
3595
|
// attribute is absent from the emitted `<div>` cell.
|
|
3333
3596
|
expect(template).toContain('range')
|
|
3334
3597
|
expect(template).not.toContain('data-x')
|
|
3335
3598
|
})
|
|
3336
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
|
+
})
|