@barefootjs/go-template 0.17.0 → 0.17.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/go-template-adapter.d.ts +10 -0
- package/dist/adapter/go-template-adapter.d.ts.map +1 -1
- package/dist/adapter/index.js +221 -36
- 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/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/build.js +221 -36
- package/dist/index.js +221 -36
- package/package.json +3 -3
- package/src/__tests__/go-template-adapter.test.ts +280 -0
- package/src/adapter/go-template-adapter.ts +126 -31
- package/src/adapter/lib/compile-state.ts +31 -0
- package/src/adapter/lib/go-emit.ts +20 -0
- package/src/adapter/memo/memo-compute.ts +228 -18
- package/src/adapter/memo/memo-type.ts +19 -0
- package/src/test-render.ts +46 -8
|
@@ -58,6 +58,13 @@ runAdapterConformanceTests({
|
|
|
58
58
|
// a `SearchParams bf.SearchParams` binding (zero value → empty query → the
|
|
59
59
|
// author's default). See #1922; Mojo / Xslate stay skipped pending their own
|
|
60
60
|
// env-signal lowering + per-request Perl reader.
|
|
61
|
+
//
|
|
62
|
+
// `search-params-derived-filter` (#2075) is no longer skipped: a LIST-
|
|
63
|
+
// valued memo derived from the searchParams() env signal (the blog
|
|
64
|
+
// `visible` filter shape) now SSR-computes via `bf.FilterEval` — the
|
|
65
|
+
// memo's field type is `[]any` and its constructor init serializes the
|
|
66
|
+
// `.filter` predicate to the runtime evaluator. See the #2075 constructor
|
|
67
|
+
// pins below.
|
|
61
68
|
skipJsx: [],
|
|
62
69
|
// Per-fixture build-time contracts for shapes the Go template
|
|
63
70
|
// adapter intentionally refuses to lower. Lives here (not on the
|
|
@@ -98,6 +105,17 @@ runAdapterConformanceTests({
|
|
|
98
105
|
// (`cn\`base \${tone()}\``) likewise can't lower into Go template
|
|
99
106
|
// syntax — same BF101 refusal.
|
|
100
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' }],
|
|
101
119
|
// #1310: rest destructure in .map() callback. The object-rest shape read
|
|
102
120
|
// via member access (`rest-destructure-object-in-map`) now lowers — each
|
|
103
121
|
// binding resolves to a field on a synthetic `$__bf_item0` range var (the
|
|
@@ -154,6 +172,11 @@ runAdapterConformanceTests({
|
|
|
154
172
|
// `string-trim` no longer pinned — pre-existing `bf_trim`
|
|
155
173
|
// (wraps `strings.TrimSpace`) handles the strip (#1448 Tier A
|
|
156
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' }],
|
|
157
180
|
},
|
|
158
181
|
// `JSON_STRINGIFY_VIA_CONST` and `MATH_FLOOR_VIA_CONST` now pass
|
|
159
182
|
// via `GoTemplateAdapter.templatePrimitives` (#1188). The two
|
|
@@ -1830,6 +1853,31 @@ export function TodoList() {
|
|
|
1830
1853
|
const bf101 = adapter.errors.filter(e => e.code === 'BF101')
|
|
1831
1854
|
expect(bf101).toEqual([])
|
|
1832
1855
|
})
|
|
1856
|
+
|
|
1857
|
+
test('nested-callback refusal keeps the emitted template syntactically valid (#2038)', () => {
|
|
1858
|
+
// The BF101 contract itself is pinned at the shared conformance layer
|
|
1859
|
+
// (`filter-nested-callback-predicate` / `filter-nested-find-predicate`
|
|
1860
|
+
// expectedDiagnostics; the `/* @client */` suppression twin renders
|
|
1861
|
+
// clean). This test pins the GO-SPECIFIC half: the refusal must emit
|
|
1862
|
+
// the `false` sentinel — not a half-rendered predicate like `.Some` —
|
|
1863
|
+
// so `text/template` parsing doesn't cascade into secondary errors.
|
|
1864
|
+
const adapter = new GoTemplateAdapter()
|
|
1865
|
+
const result = compileAndGenerate(`
|
|
1866
|
+
"use client"
|
|
1867
|
+
import { createSignal } from "@barefootjs/client"
|
|
1868
|
+
|
|
1869
|
+
type Item = { id: number }
|
|
1870
|
+
|
|
1871
|
+
export function Picker() {
|
|
1872
|
+
const [items, setItems] = createSignal<Item[]>([])
|
|
1873
|
+
const [picked, setPicked] = createSignal<Item[]>([])
|
|
1874
|
+
return <ul>{items().filter(t => !picked().some(p => p.id === t.id)).map(t => <li key={t.id}>{t.id}</li>)}</ul>
|
|
1875
|
+
}
|
|
1876
|
+
`, adapter)
|
|
1877
|
+
expect(adapter.errors.some(e => e.code === 'BF101')).toBe(true)
|
|
1878
|
+
expect(result.template).not.toContain('.Some')
|
|
1879
|
+
expect(result.template).toContain('{{if false}}')
|
|
1880
|
+
})
|
|
1833
1881
|
})
|
|
1834
1882
|
|
|
1835
1883
|
describe('find/findIndex - adapter specific', () => {
|
|
@@ -2991,6 +3039,238 @@ export { C }
|
|
|
2991
3039
|
})
|
|
2992
3040
|
})
|
|
2993
3041
|
|
|
3042
|
+
describe('GoTemplateAdapter - #2075 searchParams()-derived memo constructor', () => {
|
|
3043
|
+
// A memo derived from the createSearchParams() env signal SSR-computes in
|
|
3044
|
+
// the generated constructor from the canonical `in.SearchParams` reader —
|
|
3045
|
+
// including under a local alias. `Get` returns "" for an absent key, so
|
|
3046
|
+
// the `??` default fires on "" — the same documented `?? → or` divergence
|
|
3047
|
+
// as the template-position lowering.
|
|
3048
|
+
test('computes an aliased `?? default` derived memo from in.SearchParams', () => {
|
|
3049
|
+
const adapter = new GoTemplateAdapter()
|
|
3050
|
+
const ir = compileToIR(`
|
|
3051
|
+
'use client'
|
|
3052
|
+
import { createMemo, createSearchParams } from '@barefootjs/client'
|
|
3053
|
+
export function SortStatus() {
|
|
3054
|
+
const [sp] = createSearchParams()
|
|
3055
|
+
const sort = createMemo(() => sp().get('sort') ?? 'date')
|
|
3056
|
+
return <p>sort: {sort()}</p>
|
|
3057
|
+
}
|
|
3058
|
+
`, adapter)
|
|
3059
|
+
const { types } = adapter.generate(ir)
|
|
3060
|
+
expect(types).toContain(
|
|
3061
|
+
'Sort: func() string { if v := in.SearchParams.Get("sort"); v != "" { return v }; return "date" }(),',
|
|
3062
|
+
)
|
|
3063
|
+
})
|
|
3064
|
+
|
|
3065
|
+
test('computes a bare env read memo from in.SearchParams', () => {
|
|
3066
|
+
const adapter = new GoTemplateAdapter()
|
|
3067
|
+
const ir = compileToIR(`
|
|
3068
|
+
'use client'
|
|
3069
|
+
import { createMemo, createSearchParams } from '@barefootjs/client'
|
|
3070
|
+
export function QueryEcho() {
|
|
3071
|
+
const [searchParams] = createSearchParams()
|
|
3072
|
+
const q = createMemo(() => searchParams().get('q'))
|
|
3073
|
+
return <p>{q()}</p>
|
|
3074
|
+
}
|
|
3075
|
+
`, adapter)
|
|
3076
|
+
const { types } = adapter.generate(ir)
|
|
3077
|
+
expect(types).toContain('Q: in.SearchParams.Get("q"),')
|
|
3078
|
+
})
|
|
3079
|
+
|
|
3080
|
+
// #2075 residual: a LIST-valued memo (`.filter(arrow)` over a props array)
|
|
3081
|
+
// chained off the env-derived `tag` memo — the `search-params-derived-filter`
|
|
3082
|
+
// shared fixture's shape. The field is `[]any` (not the misclassified `bool`
|
|
3083
|
+
// the `/=>\s*!/` heuristic used to produce from the predicate's `!tag()`),
|
|
3084
|
+
// and the constructor seeds it via `bf.FilterEval`, whose serialized
|
|
3085
|
+
// predicate JSON is embedded (escaped) in the Go string literal.
|
|
3086
|
+
test('computes a list-filter memo chained off a searchParams()-derived memo via bf.FilterEval', () => {
|
|
3087
|
+
const adapter = new GoTemplateAdapter()
|
|
3088
|
+
const ir = compileToIR(`
|
|
3089
|
+
'use client'
|
|
3090
|
+
import { createMemo, createSearchParams } from '@barefootjs/client'
|
|
3091
|
+
export function TaggedList(props: { items: { title: string; tags: string[] }[] }) {
|
|
3092
|
+
const [searchParams] = createSearchParams()
|
|
3093
|
+
const tag = createMemo(() => searchParams().get('tag') ?? '')
|
|
3094
|
+
const visible = createMemo(() => props.items.filter((p) => !tag() || p.tags.includes(tag())))
|
|
3095
|
+
return <ul>{visible().map((p) => <li key={p.title}>{p.title}</li>)}</ul>
|
|
3096
|
+
}
|
|
3097
|
+
`, adapter)
|
|
3098
|
+
const { types } = adapter.generate(ir)
|
|
3099
|
+
expect(types).toContain('Visible []any')
|
|
3100
|
+
expect(types).toContain('Visible: bf.FilterEval(in.Items,')
|
|
3101
|
+
// The predicate JSON is escaped inside a Go double-quoted string literal,
|
|
3102
|
+
// so assert on the substring that survives escaping (backslash-escaped
|
|
3103
|
+
// quotes around the JSON keys/values).
|
|
3104
|
+
expect(types).toContain('\\"method\\":\\"includes\\"')
|
|
3105
|
+
})
|
|
3106
|
+
|
|
3107
|
+
// #2077 review finding 3: `tag` (an earlier sibling the `visible` filter's
|
|
3108
|
+
// predicate closes over) used to be recomputed verbatim a second time
|
|
3109
|
+
// inline in `visible`'s `bf.FilterEval` env map — the same
|
|
3110
|
+
// `in.SearchParams.Get("tag")` expression emitted twice in the constructor.
|
|
3111
|
+
// It's now hoisted to a shared local before the `return`, referenced by
|
|
3112
|
+
// BOTH the `Tag:` field and the env-map entry.
|
|
3113
|
+
test('hoists the earlier sibling memo instead of duplicating its expression', () => {
|
|
3114
|
+
const adapter = new GoTemplateAdapter()
|
|
3115
|
+
const ir = compileToIR(`
|
|
3116
|
+
'use client'
|
|
3117
|
+
import { createMemo, createSearchParams } from '@barefootjs/client'
|
|
3118
|
+
export function TaggedList(props: { items: { title: string; tags: string[] }[] }) {
|
|
3119
|
+
const [searchParams] = createSearchParams()
|
|
3120
|
+
const tag = createMemo(() => searchParams().get('tag') ?? '')
|
|
3121
|
+
const visible = createMemo(() => props.items.filter((p) => !tag() || p.tags.includes(tag())))
|
|
3122
|
+
return <ul>{visible().map((p) => <li key={p.title}>{p.title}</li>)}</ul>
|
|
3123
|
+
}
|
|
3124
|
+
`, adapter)
|
|
3125
|
+
const { types } = adapter.generate(ir)
|
|
3126
|
+
expect(types).toContain('var memoTag string =')
|
|
3127
|
+
expect(types).toContain('Tag: memoTag,')
|
|
3128
|
+
expect(types).toContain('"tag": memoTag')
|
|
3129
|
+
// The multi-token `Get("tag")` expression appears exactly once in the
|
|
3130
|
+
// whole constructor — the hoisted local, not a second inline copy.
|
|
3131
|
+
expect(types.split('in.SearchParams.Get("tag")').length).toBe(2)
|
|
3132
|
+
})
|
|
3133
|
+
})
|
|
3134
|
+
|
|
3135
|
+
describe('GoTemplateAdapter - env-derived memo sourced from the SSR seed plan (Package I)', () => {
|
|
3136
|
+
// `CompileState.ssrSeedPlan` (and `envSignalReadersByLocal` derived from it)
|
|
3137
|
+
// is now the single authority for env-reader recognition. `compileToIR`
|
|
3138
|
+
// already JSON-round-trips the IR (mirrors the adapter conformance
|
|
3139
|
+
// harness), so each `env-reader` step's `reader.methods` — a ReadonlySet —
|
|
3140
|
+
// arrives serialized to `{}`; `primeCompileState` re-resolves a live reader
|
|
3141
|
+
// via `envSignalReaderFor(step.reader.key)` before trusting `.methods`.
|
|
3142
|
+
// Pins that the env-derived memo constructor still SSR-computes through
|
|
3143
|
+
// that re-resolution instead of silently falling back to a zero value.
|
|
3144
|
+
test('computes an env-derived memo constructor through the JSON-round-tripped ssrSeedPlan', () => {
|
|
3145
|
+
const adapter = new GoTemplateAdapter()
|
|
3146
|
+
const ir = compileToIR(`
|
|
3147
|
+
'use client'
|
|
3148
|
+
import { createMemo, createSearchParams } from '@barefootjs/client'
|
|
3149
|
+
export function QueryEcho() {
|
|
3150
|
+
const [searchParams] = createSearchParams()
|
|
3151
|
+
const q = createMemo(() => searchParams().get('q'))
|
|
3152
|
+
return <p>{q()}</p>
|
|
3153
|
+
}
|
|
3154
|
+
`, adapter)
|
|
3155
|
+
const { types } = adapter.generate(ir)
|
|
3156
|
+
expect(types).toContain('Q: in.SearchParams.Get("q"),')
|
|
3157
|
+
})
|
|
3158
|
+
})
|
|
3159
|
+
|
|
3160
|
+
describe('GoTemplateAdapter - #2077 review sibling-memo recursion guard', () => {
|
|
3161
|
+
// Finding 1: mutually-referencing memos (each resolving the other as a
|
|
3162
|
+
// filter-arm free var, or as a bare-getter ternary condition) used to
|
|
3163
|
+
// recurse without bound and blow the compiler's call stack
|
|
3164
|
+
// (`RangeError: Maximum call stack size exceeded`, verified by probe before
|
|
3165
|
+
// the fix). The cycle/declaration-order guard makes both directions fall
|
|
3166
|
+
// back to the field's Go zero value instead of throwing.
|
|
3167
|
+
test('mutual filter-arm memos compile without throwing and fall back to zero values', () => {
|
|
3168
|
+
const adapter = new GoTemplateAdapter()
|
|
3169
|
+
const ir = compileToIR(`
|
|
3170
|
+
'use client'
|
|
3171
|
+
import { createMemo } from '@barefootjs/client'
|
|
3172
|
+
export function C(props: { items: { title: string }[] }) {
|
|
3173
|
+
const a = createMemo(() => props.items.filter((x) => Boolean(b())))
|
|
3174
|
+
const b = createMemo(() => props.items.filter((x) => Boolean(a())))
|
|
3175
|
+
return <ul>{a().map((p) => <li key={p.title}>{p.title}</li>)}</ul>
|
|
3176
|
+
}
|
|
3177
|
+
`, adapter)
|
|
3178
|
+
let types = ''
|
|
3179
|
+
expect(() => { types = adapter.generate(ir).types }).not.toThrow()
|
|
3180
|
+
expect(types).toContain('A []any')
|
|
3181
|
+
expect(types).toContain('B []any')
|
|
3182
|
+
// Neither memo can resolve the OTHER as a free var (order rules out both
|
|
3183
|
+
// directions at once — `a` can't see `b` because `b` is declared later,
|
|
3184
|
+
// and vice versa), so `a`'s own computation falls back to the `[]any`
|
|
3185
|
+
// zero value (hoisted since `b` references it), and `b`'s free-var
|
|
3186
|
+
// resolution of `a` reuses that hoisted zero-valued local.
|
|
3187
|
+
expect(types).toContain('var memoA []any = nil')
|
|
3188
|
+
expect(types).toContain('A: memoA,')
|
|
3189
|
+
expect(types).toContain('B: bf.FilterEval(in.Items,')
|
|
3190
|
+
expect(types).toContain('map[string]any{"a": memoA}')
|
|
3191
|
+
})
|
|
3192
|
+
|
|
3193
|
+
test('mutual ternary memos (bare-getter condition) compile without throwing and fall back to zero values', () => {
|
|
3194
|
+
const adapter = new GoTemplateAdapter()
|
|
3195
|
+
const ir = compileToIR(`
|
|
3196
|
+
'use client'
|
|
3197
|
+
import { createMemo } from '@barefootjs/client'
|
|
3198
|
+
export function C() {
|
|
3199
|
+
const a = createMemo(() => (b() ? 'A' : 'Z'))
|
|
3200
|
+
const b = createMemo(() => (a() ? 'B' : 'Y'))
|
|
3201
|
+
return <p>{a()}{b()}</p>
|
|
3202
|
+
}
|
|
3203
|
+
`, adapter)
|
|
3204
|
+
let types = ''
|
|
3205
|
+
expect(() => { types = adapter.generate(ir).types }).not.toThrow()
|
|
3206
|
+
expect(types).toContain('A: "",')
|
|
3207
|
+
expect(types).toContain('B: "",')
|
|
3208
|
+
})
|
|
3209
|
+
|
|
3210
|
+
test('self-referencing filter-arm memo compiles without throwing', () => {
|
|
3211
|
+
const adapter = new GoTemplateAdapter()
|
|
3212
|
+
const ir = compileToIR(`
|
|
3213
|
+
'use client'
|
|
3214
|
+
import { createMemo } from '@barefootjs/client'
|
|
3215
|
+
export function C(props: { items: { title: string }[] }) {
|
|
3216
|
+
const a = createMemo(() => props.items.filter((x) => Boolean(a())))
|
|
3217
|
+
return <ul>{a().map((p) => <li key={p.title}>{p.title}</li>)}</ul>
|
|
3218
|
+
}
|
|
3219
|
+
`, adapter)
|
|
3220
|
+
let types = ''
|
|
3221
|
+
expect(() => { types = adapter.generate(ir).types }).not.toThrow()
|
|
3222
|
+
expect(types).toContain('A: nil,')
|
|
3223
|
+
})
|
|
3224
|
+
|
|
3225
|
+
test('self-referencing ternary memo compiles without throwing', () => {
|
|
3226
|
+
const adapter = new GoTemplateAdapter()
|
|
3227
|
+
const ir = compileToIR(`
|
|
3228
|
+
'use client'
|
|
3229
|
+
import { createMemo } from '@barefootjs/client'
|
|
3230
|
+
export function C() {
|
|
3231
|
+
const a = createMemo(() => (a() ? 'A' : 'Z'))
|
|
3232
|
+
return <p>{a()}</p>
|
|
3233
|
+
}
|
|
3234
|
+
`, adapter)
|
|
3235
|
+
let types = ''
|
|
3236
|
+
expect(() => { types = adapter.generate(ir).types }).not.toThrow()
|
|
3237
|
+
expect(types).toContain('A: "",')
|
|
3238
|
+
})
|
|
3239
|
+
})
|
|
3240
|
+
|
|
3241
|
+
describe('GoTemplateAdapter - #2073 value-producing .map(cb)', () => {
|
|
3242
|
+
function emitMap(expr: string): string {
|
|
3243
|
+
const adapter = new GoTemplateAdapter()
|
|
3244
|
+
const ir = compileToIR(`
|
|
3245
|
+
function C({ tags, users }: { tags: string[]; users: { name: string }[] }) {
|
|
3246
|
+
return <div>{${expr}}</div>
|
|
3247
|
+
}
|
|
3248
|
+
export { C }
|
|
3249
|
+
`, adapter)
|
|
3250
|
+
return adapter.generate(ir).template ?? ''
|
|
3251
|
+
}
|
|
3252
|
+
|
|
3253
|
+
// The blog-showcase shape (#1938/#1939): a value-returning `.map` (string
|
|
3254
|
+
// projection, not JSX) lowers through the evaluator — `bf_map_eval` projects
|
|
3255
|
+
// each element (no flatten) and composes through `bf_join`. (Like the
|
|
3256
|
+
// flatMap pins below, the projection JSON's quotes are backslash-escaped in
|
|
3257
|
+
// the Go-template string; the JSON itself is verified by the render
|
|
3258
|
+
// conformance fixtures + the runtime TestMapEval.)
|
|
3259
|
+
test('.map(t => `#${t}`).join(" ") emits bf_map_eval composed into bf_join', () => {
|
|
3260
|
+
const t = emitMap("tags.map(t => `#${t}`).join(' ')")
|
|
3261
|
+
expect(t).toContain('bf_join (bf_map_eval .Tags')
|
|
3262
|
+
})
|
|
3263
|
+
|
|
3264
|
+
test('.map(u => u.name) emits bf_map_eval with the receiver', () => {
|
|
3265
|
+
const t = emitMap("users.map(u => u.name).join(', ')")
|
|
3266
|
+
expect(t).toContain('bf_map_eval .Users')
|
|
3267
|
+
})
|
|
3268
|
+
|
|
3269
|
+
// The function-reference `.map(format)` BF101 refusal is now covered
|
|
3270
|
+
// cross-adapter by the `array-map-function-reference` shared fixture's
|
|
3271
|
+
// `expectedDiagnostics` entry above.
|
|
3272
|
+
})
|
|
3273
|
+
|
|
2994
3274
|
describe('GoTemplateAdapter - #1448 Tier C .flatMap(field projection)', () => {
|
|
2995
3275
|
function emitFlatMap(expr: string): string {
|
|
2996
3276
|
const adapter = new GoTemplateAdapter()
|
|
@@ -62,8 +62,9 @@ import {
|
|
|
62
62
|
isLowerableObjectRestDestructure,
|
|
63
63
|
type ContextConsumer,
|
|
64
64
|
collectModuleStringConsts as collectModuleStringConstsShared,
|
|
65
|
-
|
|
66
|
-
|
|
65
|
+
prepareLoweringMatchers,
|
|
66
|
+
envSignalReaderFor,
|
|
67
|
+
computeSsrSeedPlan,
|
|
67
68
|
} from '@barefootjs/jsx'
|
|
68
69
|
import { findInterpolationEnd } from '@barefootjs/jsx/scanner'
|
|
69
70
|
import { BF_REGION } from '@barefootjs/shared'
|
|
@@ -85,6 +86,7 @@ import {
|
|
|
85
86
|
emitReduceEval,
|
|
86
87
|
emitPredicateEval,
|
|
87
88
|
emitFlatMapEval,
|
|
89
|
+
emitMapEval,
|
|
88
90
|
stringTolerantEqOperands,
|
|
89
91
|
buildUnsupportedSuggestion,
|
|
90
92
|
GO_REMEDIATION_OPTIONS,
|
|
@@ -115,10 +117,10 @@ import {
|
|
|
115
117
|
objectLiteralToGoMap,
|
|
116
118
|
} from "./value/value-lowering.ts"
|
|
117
119
|
import { typeInfoToGo } from "./type/type-codegen.ts"
|
|
118
|
-
import { isBooleanMemo, isStringTernaryMemo } from "./memo/memo-type.ts"
|
|
120
|
+
import { isBooleanMemo, isListFilterMemo, isStringTernaryMemo } from "./memo/memo-type.ts"
|
|
119
121
|
import { lowerCtorExpr } from "./memo/ctor-lowering.ts"
|
|
120
122
|
import { resolveBlockBodyMemoModuleConst } from "./memo/memo-value.ts"
|
|
121
|
-
import { computeMemoInitialValue, computeMemoInitialValueOrNull } from "./memo/memo-compute.ts"
|
|
123
|
+
import { computeMemoInitialValue, computeMemoInitialValueOrNull, filterArmEarlierSiblingRefs } from "./memo/memo-compute.ts"
|
|
122
124
|
import { collectSpreadSlots, buildSpreadInitializer } from "./spread/spread-codegen.ts"
|
|
123
125
|
import { buildPropTypeOverrides, resolvePropGoType, collectNillablePropNames } from "./props/prop-types.ts"
|
|
124
126
|
|
|
@@ -272,7 +274,22 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
272
274
|
this.state.currentMemos = ir.metadata.memos ?? []
|
|
273
275
|
this.state.currentTypeDefinitions = ir.metadata.typeDefinitions ?? []
|
|
274
276
|
this.state.contextConsumers = collectContextConsumers(ir.metadata)
|
|
275
|
-
|
|
277
|
+
// Single authority (Package G): the plan already decided which signals are
|
|
278
|
+
// per-request env readers, in declaration order. `ir.metadata.ssrSeedPlan`
|
|
279
|
+
// may be absent for hand-built metadata (tests), hence the fallback to the
|
|
280
|
+
// same shared computation rather than a second, divergent derivation.
|
|
281
|
+
this.state.ssrSeedPlan = ir.metadata.ssrSeedPlan ?? computeSsrSeedPlan(ir.metadata)
|
|
282
|
+
this.state.envSignalReadersByLocal = new Map()
|
|
283
|
+
this.state.searchParamsLocals = new Set()
|
|
284
|
+
for (const step of this.state.ssrSeedPlan.steps) {
|
|
285
|
+
if (step.kind !== 'env-reader') continue
|
|
286
|
+
// `reader.methods` is a ReadonlySet — JSON round-tripping (adapter
|
|
287
|
+
// conformance harness) serializes it to `{}`, so re-resolve a live
|
|
288
|
+
// reader from the registry by key instead of trusting `step.reader`.
|
|
289
|
+
const reader = envSignalReaderFor(step.reader.key)
|
|
290
|
+
if (reader) this.state.envSignalReadersByLocal.set(step.name, reader)
|
|
291
|
+
if (step.reader.key === 'search') this.state.searchParamsLocals.add(step.name)
|
|
292
|
+
}
|
|
276
293
|
this.state.loweringMatchers = prepareLoweringMatchers(ir.metadata)
|
|
277
294
|
augmentInheritedPropAccesses(ir)
|
|
278
295
|
}
|
|
@@ -1000,6 +1017,38 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
1000
1017
|
|
|
1001
1018
|
this.emitDynamicBodyWrappers(lines, ir, componentName, dynamicWithBody, propFallbackVars, emittedWrapperVars)
|
|
1002
1019
|
|
|
1020
|
+
// Sibling-memo hoisting (#2075/#2077 review finding 3): a filter-arm memo
|
|
1021
|
+
// whose predicate free vars reference an EARLIER sibling memo would
|
|
1022
|
+
// otherwise recompute that sibling's whole expression a second time
|
|
1023
|
+
// inline in its `bf.FilterEval` env map, duplicating it against the
|
|
1024
|
+
// sibling's own field. Two-pass: first collect which memos are
|
|
1025
|
+
// referenced this way, then hoist each into a local emitted once before
|
|
1026
|
+
// the `return`; the memo-field loop below (and the referencing memo's own
|
|
1027
|
+
// env-map entry, via `ctx.state.hoistedMemoLocals`) both reuse the local
|
|
1028
|
+
// instead of re-emitting the expression. Declared `var name Type = value`
|
|
1029
|
+
// (not `:=`) because an unresolved computation can fall back to the bare
|
|
1030
|
+
// `nil` literal, which `:=` can't type-infer.
|
|
1031
|
+
const memoPropsParamMap = new Map(ir.metadata.propsParams.map(p => [p.name, p]))
|
|
1032
|
+
this.state.hoistedMemoLocals = new Map()
|
|
1033
|
+
const hoistNames = new Set<string>()
|
|
1034
|
+
for (const memo of ir.metadata.memos) {
|
|
1035
|
+
for (const name of filterArmEarlierSiblingRefs(this.emitCtx, memo, ir.metadata.signals, ir.metadata.propsParams)) {
|
|
1036
|
+
hoistNames.add(name)
|
|
1037
|
+
}
|
|
1038
|
+
}
|
|
1039
|
+
if (hoistNames.size > 0) {
|
|
1040
|
+
for (const memo of ir.metadata.memos) {
|
|
1041
|
+
if (!hoistNames.has(memo.name)) continue
|
|
1042
|
+
const goType = this.inferMemoType(memo, ir.metadata.signals, memoPropsParamMap)
|
|
1043
|
+
const value = computeMemoInitialValue(this.emitCtx, memo, ir.metadata.signals, ir.metadata.propsParams, propFallbackVars, goType)
|
|
1044
|
+
let localName = `memo${capitalizeFieldName(memo.name)}`
|
|
1045
|
+
while (GO_KEYWORDS.has(localName)) localName += '_'
|
|
1046
|
+
lines.push(`\tvar ${localName} ${goType} = ${value}`)
|
|
1047
|
+
this.state.hoistedMemoLocals.set(memo.name, localName)
|
|
1048
|
+
}
|
|
1049
|
+
lines.push('')
|
|
1050
|
+
}
|
|
1051
|
+
|
|
1003
1052
|
lines.push(`\treturn ${propsTypeName}{`)
|
|
1004
1053
|
lines.push('\t\tScopeID: scopeID,')
|
|
1005
1054
|
// Host context, for when *this* component is itself a slot-attached child.
|
|
@@ -1097,11 +1146,16 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
1097
1146
|
}
|
|
1098
1147
|
|
|
1099
1148
|
// Memo initial values (from signal initials). Prop-shadowing memos were
|
|
1100
|
-
// folded into the prop field above
|
|
1101
|
-
|
|
1149
|
+
// folded into the prop field above; a memo the pre-pass already hoisted
|
|
1150
|
+
// (above) reuses that local instead of recomputing its expression.
|
|
1102
1151
|
for (const memo of ir.metadata.memos) {
|
|
1103
1152
|
const fieldName = capitalizeFieldName(memo.name)
|
|
1104
1153
|
if (propFieldNames.has(fieldName)) continue
|
|
1154
|
+
const hoistedLocal = this.state.hoistedMemoLocals.get(memo.name)
|
|
1155
|
+
if (hoistedLocal) {
|
|
1156
|
+
lines.push(`\t\t${fieldName}: ${hoistedLocal},`)
|
|
1157
|
+
continue
|
|
1158
|
+
}
|
|
1105
1159
|
// Pass the inferred Go type so an unresolved computation zeroes to that
|
|
1106
1160
|
// type (`false` for a boolean memo), not the int `0`.
|
|
1107
1161
|
const goType = this.inferMemoType(memo, ir.metadata.signals, memoPropsParamMap)
|
|
@@ -2095,10 +2149,14 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
2095
2149
|
}
|
|
2096
2150
|
|
|
2097
2151
|
// A memo: when no pattern applies, return null so the caller OMITS the
|
|
2098
|
-
// field and Go's typed zero value applies.
|
|
2152
|
+
// field and Go's typed zero value applies. Seed the resolution stack
|
|
2153
|
+
// with this memo's own name — a fresh top-level computation, so
|
|
2154
|
+
// self-reference must be caught on the first recursion.
|
|
2099
2155
|
const memo = memos.find(m => m.name === getterName)
|
|
2100
2156
|
if (memo) {
|
|
2101
|
-
return computeMemoInitialValueOrNull(
|
|
2157
|
+
return computeMemoInitialValueOrNull(
|
|
2158
|
+
this.emitCtx, memo, signals, propsParams, undefined, new Set([memo.name]),
|
|
2159
|
+
)
|
|
2102
2160
|
}
|
|
2103
2161
|
}
|
|
2104
2162
|
|
|
@@ -2111,6 +2169,19 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
2111
2169
|
signals: { getter: string; initialValue: string; type: TypeInfo }[],
|
|
2112
2170
|
propsParamMap: Map<string, { name: string; type: TypeInfo; defaultValue?: string }>
|
|
2113
2171
|
): string {
|
|
2172
|
+
// A LIST-valued `.filter(arrow)` memo (#2075 — the blog PostList `visible`
|
|
2173
|
+
// shape) is a slice of the receiver's boxed elements, not a scalar.
|
|
2174
|
+
// Decided FIRST, ahead of the memo's declared `type` and every other
|
|
2175
|
+
// heuristic below: the analyzer's simple per-memo type inference doesn't
|
|
2176
|
+
// model `.filter`'s predicate shape and can land on a bogus primitive
|
|
2177
|
+
// (observed: `boolean`, from the predicate's own `!`/`||` structure),
|
|
2178
|
+
// which would otherwise sail through the `typeInfoToGo(memo.type) ===
|
|
2179
|
+
// 'interface{}'` gates below unchallenged. `bf.FilterEval` (the SSR
|
|
2180
|
+
// constructor lowering, memo-compute.ts) returns `[]any`; the template's
|
|
2181
|
+
// `range` / reflective field access handle the boxed elements the same
|
|
2182
|
+
// way it already handles other `interface{}`-typed slices.
|
|
2183
|
+
if (isListFilterMemo(memo)) return '[]any'
|
|
2184
|
+
|
|
2114
2185
|
// A template-literal memo always produces a string. Decide this first so a
|
|
2115
2186
|
// class-string `/` (e.g. `ring-ring/50`) doesn't trip the arithmetic
|
|
2116
2187
|
// heuristic below into `int`. The analyzer classified the body shape from
|
|
@@ -3077,6 +3148,14 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
3077
3148
|
return this.pushCallbackBF101(method, true)
|
|
3078
3149
|
}
|
|
3079
3150
|
|
|
3151
|
+
// Value-producing `.map(cb)` (#2073): eval-only. (The JSX-returning
|
|
3152
|
+
// `.map` is an IRLoop upstream and never reaches this dispatch.)
|
|
3153
|
+
if (method === 'map') {
|
|
3154
|
+
const evalForm = emitMapEval(recv, body, params[0] ?? '_', emit)
|
|
3155
|
+
if (evalForm !== null) return evalForm
|
|
3156
|
+
return this.pushCallbackBF101(method, true)
|
|
3157
|
+
}
|
|
3158
|
+
|
|
3080
3159
|
return this.pushCallbackBF101(method, true)
|
|
3081
3160
|
}
|
|
3082
3161
|
|
|
@@ -3657,6 +3736,16 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
3657
3736
|
if (expr.callee.kind === 'identifier' && expr.args.length === 0) {
|
|
3658
3737
|
return `$.${capitalizeFieldName(expr.callee.name)}`
|
|
3659
3738
|
}
|
|
3739
|
+
// A nested callback method call (`other.some(r => …)`) reaching this
|
|
3740
|
+
// arm has no Go template form in filter context — the fallthrough
|
|
3741
|
+
// below renders only the callee and silently DROPS the arrow argument,
|
|
3742
|
+
// changing predicate semantics (#2038). The one faithful nested shape
|
|
3743
|
+
// (`.filter(cb).length` → `len (bf_filter_eval …)`) is intercepted at
|
|
3744
|
+
// the `member` arm before this node is visited; everything else must
|
|
3745
|
+
// be loud.
|
|
3746
|
+
if (asCallbackMethodCall(expr) !== null) {
|
|
3747
|
+
return this.refuseFilterExprNode(expr)
|
|
3748
|
+
}
|
|
3660
3749
|
const result = this.renderFilterExpr(expr.callee, param, localVarMap)
|
|
3661
3750
|
if (this.filterExprUnsupported) return 'false'
|
|
3662
3751
|
return result
|
|
@@ -3721,31 +3810,37 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
3721
3810
|
return `or (${left}) (${right})`
|
|
3722
3811
|
}
|
|
3723
3812
|
|
|
3724
|
-
default:
|
|
3725
|
-
// The filter predicate body contains a node kind we can't lower to a
|
|
3726
|
-
// template action
|
|
3727
|
-
//
|
|
3728
|
-
|
|
3729
|
-
// flag so parent branches return `false` instead of wrapping the
|
|
3730
|
-
// sentinel into `false.Length` / `gt false.Length 0` etc.: the build
|
|
3731
|
-
// fails on BF101 anyway, but the emitted template must stay
|
|
3732
|
-
// syntactically valid so `text/template` parsing doesn't cascade into
|
|
3733
|
-
// confusing secondary errors.
|
|
3734
|
-
this.filterExprUnsupported = true
|
|
3735
|
-
this.state.errors.push({
|
|
3736
|
-
code: 'BF101',
|
|
3737
|
-
severity: 'error',
|
|
3738
|
-
message: `Filter predicate contains an expression that cannot be lowered to a Go template action: ${exprToString(expr)}`,
|
|
3739
|
-
loc: this.makeLoc(),
|
|
3740
|
-
suggestion: {
|
|
3741
|
-
message: 'Options:\n1. Use /* @client */ for client-side evaluation\n2. Rewrite the predicate to avoid nested higher-order methods (`.filter()` / `.map()` / etc. inside the predicate body)',
|
|
3742
|
-
},
|
|
3743
|
-
})
|
|
3744
|
-
return 'false'
|
|
3745
|
-
}
|
|
3813
|
+
default:
|
|
3814
|
+
// The filter predicate body contains a node kind we can't lower to a
|
|
3815
|
+
// Go template action. Shared refusal with the nested-callback guard in
|
|
3816
|
+
// the `call` arm (#2038).
|
|
3817
|
+
return this.refuseFilterExprNode(expr)
|
|
3746
3818
|
}
|
|
3747
3819
|
}
|
|
3748
3820
|
|
|
3821
|
+
/**
|
|
3822
|
+
* Refuse a filter-predicate node that has no Go template lowering. Surfaces
|
|
3823
|
+
* BF101 with the offending expression and sets the recursion-wide
|
|
3824
|
+
* `filterExprUnsupported` flag so parent branches return `false` instead of
|
|
3825
|
+
* wrapping the sentinel into `false.Length` / `gt false.Length 0` etc.: the
|
|
3826
|
+
* build fails on BF101 anyway, but the emitted template must stay
|
|
3827
|
+
* syntactically valid so `text/template` parsing doesn't cascade into
|
|
3828
|
+
* confusing secondary errors.
|
|
3829
|
+
*/
|
|
3830
|
+
private refuseFilterExprNode(expr: ParsedExpr): string {
|
|
3831
|
+
this.filterExprUnsupported = true
|
|
3832
|
+
this.state.errors.push({
|
|
3833
|
+
code: 'BF101',
|
|
3834
|
+
severity: 'error',
|
|
3835
|
+
message: `Filter predicate contains an expression that cannot be lowered to a Go template action: ${exprToString(expr)}`,
|
|
3836
|
+
loc: this.makeLoc(),
|
|
3837
|
+
suggestion: {
|
|
3838
|
+
message: 'Options:\n1. Use /* @client */ for client-side evaluation\n2. Rewrite the predicate to avoid nested higher-order methods (`.filter()` / `.map()` / etc. inside the predicate body)',
|
|
3839
|
+
},
|
|
3840
|
+
})
|
|
3841
|
+
return 'false'
|
|
3842
|
+
}
|
|
3843
|
+
|
|
3749
3844
|
/**
|
|
3750
3845
|
* Whether a ParsedExpr renders as a Go template function call (so it needs
|
|
3751
3846
|
* parentheses around it).
|
|
@@ -13,10 +13,12 @@
|
|
|
13
13
|
import type {
|
|
14
14
|
CompilerError,
|
|
15
15
|
ContextConsumer,
|
|
16
|
+
EnvSignalReader,
|
|
16
17
|
IRMetadata,
|
|
17
18
|
IRNode,
|
|
18
19
|
LoweringMatcher,
|
|
19
20
|
MemoInfo,
|
|
21
|
+
SsrSeedPlan,
|
|
20
22
|
TypeDefinition,
|
|
21
23
|
TypeInfo,
|
|
22
24
|
} from '@barefootjs/jsx'
|
|
@@ -90,6 +92,35 @@ export class CompileState {
|
|
|
90
92
|
*/
|
|
91
93
|
searchParamsLocals: Set<string> = new Set()
|
|
92
94
|
|
|
95
|
+
/**
|
|
96
|
+
* Env-signal getter (local binding name, e.g. `searchParams` or an alias) →
|
|
97
|
+
* its registered {@link EnvSignalReader}, keyed by the RECEIVER signal's own
|
|
98
|
+
* `envReader` registration rather than a hardcoded key — so a memo body's
|
|
99
|
+
* `<local>().<method>(...)` resolves to whichever reader that particular
|
|
100
|
+
* local belongs to (open-closed for a future second env signal).
|
|
101
|
+
*/
|
|
102
|
+
envSignalReadersByLocal: Map<string, EnvSignalReader> = new Map()
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* The backend-neutral SSR seed plan (`computeSsrSeedPlan`, Package G) for
|
|
106
|
+
* the IR currently being compiled — single authority for which signal/memo
|
|
107
|
+
* getters are value-materializable at SSR time (`env-reader` steps are
|
|
108
|
+
* per-request readers; everything else is `derived`/`opaque`). Populated in
|
|
109
|
+
* `primeCompileState`; `envSignalReadersByLocal` and `searchParamsLocals`
|
|
110
|
+
* derive from it there, and the memo-analysis path in `memo-compute.ts`
|
|
111
|
+
* reads its `steps` directly instead of re-deriving from `metadata`.
|
|
112
|
+
*/
|
|
113
|
+
ssrSeedPlan: SsrSeedPlan = { baseScope: [], steps: [] }
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Memo name → the hoisted Go local variable name the constructor assigned
|
|
117
|
+
* its SSR value to (populated by the sibling-memo hoisting pre-pass before
|
|
118
|
+
* the main memo-field loop runs). When a later filter-arm memo's free-var
|
|
119
|
+
* resolution finds an entry here, it reuses the local instead of
|
|
120
|
+
* recomputing the sibling's expression a second time.
|
|
121
|
+
*/
|
|
122
|
+
hoistedMemoLocals: Map<string, string> = new Map()
|
|
123
|
+
|
|
93
124
|
/**
|
|
94
125
|
* Call-lowering matchers active for this component (#2057), bound to its
|
|
95
126
|
* metadata at init via `prepareLoweringMatchers`. Each maps a recognised call
|
|
@@ -192,6 +192,26 @@ export function emitFlatMapEval(
|
|
|
192
192
|
return `bf_flat_map_eval ${wrapIfMultiToken(recv)} "${escapeGoString(json)}" "${param}" ${env}`
|
|
193
193
|
}
|
|
194
194
|
|
|
195
|
+
/**
|
|
196
|
+
* Emit a value-producing `.map(cb)` via the evaluator (#2073): the projection
|
|
197
|
+
* body is serialized and evaluated per element by `bf_map_eval`, one result
|
|
198
|
+
* per element (no flatten — the JS `.map` contract). Composes through the
|
|
199
|
+
* array-method chain (`bf_join (bf_map_eval …) " "`). Returns null when the
|
|
200
|
+
* projection is outside the evaluator surface (→ caller pushes BF101). The
|
|
201
|
+
* JSX-returning `.map` is an IRLoop upstream and never reaches this emit.
|
|
202
|
+
*/
|
|
203
|
+
export function emitMapEval(
|
|
204
|
+
recv: string,
|
|
205
|
+
body: ParsedExpr,
|
|
206
|
+
param: string,
|
|
207
|
+
emit: (e: ParsedExpr) => string,
|
|
208
|
+
): string | null {
|
|
209
|
+
const json = serializeParsedExpr(body)
|
|
210
|
+
if (json === null) return null
|
|
211
|
+
const env = emitEvalEnvArg(body, [param], emit)
|
|
212
|
+
return `bf_map_eval ${wrapIfMultiToken(recv)} "${escapeGoString(json)}" "${param}" ${env}`
|
|
213
|
+
}
|
|
214
|
+
|
|
195
215
|
/**
|
|
196
216
|
* Make an equality comparison string-tolerant when exactly one side is a Go
|
|
197
217
|
* string literal: JS `sorted === 'asc'` is loosely false for `sorted = false`,
|