@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
|
@@ -9,10 +9,17 @@
|
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
11
|
import type { ParsedExpr, ParsedStatement, TypeInfo } from '@barefootjs/jsx'
|
|
12
|
+
import {
|
|
13
|
+
asCallbackMethodCall,
|
|
14
|
+
freeVarsInBody,
|
|
15
|
+
materializeGetterCalls,
|
|
16
|
+
serializeParsedExpr,
|
|
17
|
+
} from '@barefootjs/jsx'
|
|
12
18
|
|
|
13
19
|
import type { GoEmitContext } from '../emit-context.ts'
|
|
14
20
|
import type { PropFallbackVar } from '../lib/types.ts'
|
|
15
21
|
import { capitalizeFieldName } from '../lib/go-naming.ts'
|
|
22
|
+
import { escapeGoString } from '../lib/go-emit.ts'
|
|
16
23
|
import { convertInitialValue, getSignalInitialValueAsGo } from '../value/value-lowering.ts'
|
|
17
24
|
import { typeInfoToGo } from '../type/type-codegen.ts'
|
|
18
25
|
import { computeTemplateLiteralMemoInitialValue } from './template-interp.ts'
|
|
@@ -21,6 +28,91 @@ import { resolveBlockBodyMemoModuleConst, computeObjectMemoInitialValue } from '
|
|
|
21
28
|
/** Default for the optional `propFallbackVars` argument. */
|
|
22
29
|
const EMPTY_PROP_FALLBACK_VARS: ReadonlyMap<string, PropFallbackVar> = new Map()
|
|
23
30
|
|
|
31
|
+
/** A bare zero-arg getter call (`count()`) → its name, else null. */
|
|
32
|
+
function getterCallName(e: ParsedExpr): string | null {
|
|
33
|
+
return e.kind === 'call' && e.callee.kind === 'identifier' && e.args.length === 0
|
|
34
|
+
? e.callee.name
|
|
35
|
+
: null
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** A `props.X` member access → the prop name, else null. */
|
|
39
|
+
function propsMemberName(e: ParsedExpr): string | null {
|
|
40
|
+
return e.kind === 'member' &&
|
|
41
|
+
!e.computed &&
|
|
42
|
+
e.object.kind === 'identifier' &&
|
|
43
|
+
e.object.name === 'props'
|
|
44
|
+
? e.property
|
|
45
|
+
: null
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** A `() => props.X.filter((p) => <predicate>)` match: the array prop name,
|
|
49
|
+
* the predicate serialized to the runtime evaluator's ParsedExpr JSON, the
|
|
50
|
+
* arrow's param name, and the free variable names its predicate captures.
|
|
51
|
+
* Null when `body` isn't this shape, `propName` doesn't resolve to a known
|
|
52
|
+
* prop, or the predicate isn't representable (`serializeParsedExpr` refusal).
|
|
53
|
+
* Shared by the SSR-value emitter ({@link memoInitialFromParsedBody}) and the
|
|
54
|
+
* constructor's sibling-memo hoisting pre-pass so both walk the shape
|
|
55
|
+
* identically. */
|
|
56
|
+
export function matchFilterArmMemo(
|
|
57
|
+
ctx: GoEmitContext,
|
|
58
|
+
body: ParsedExpr,
|
|
59
|
+
signals: { getter: string; initialValue: string }[],
|
|
60
|
+
propsParams: { name: string; type?: TypeInfo; defaultValue?: string }[],
|
|
61
|
+
): { propName: string; predJSON: string; paramName: string; freeVars: string[] } | null {
|
|
62
|
+
const cb = asCallbackMethodCall(body)
|
|
63
|
+
if (!cb || cb.method !== 'filter') return null
|
|
64
|
+
const propName = propsMemberName(cb.object)
|
|
65
|
+
const param = propName ? propsParams.find(p => p.name === propName) : undefined
|
|
66
|
+
if (!propName || !param) return null
|
|
67
|
+
|
|
68
|
+
// Single authority (Package G plan): every step that isn't `env-reader` is
|
|
69
|
+
// value-materializable at SSR time — env readers are per-request readers
|
|
70
|
+
// the runtime evaluator can't invoke, so they're excluded here exactly as
|
|
71
|
+
// the plan's own consumers exclude them.
|
|
72
|
+
const knownGetterNames = new Set<string>(
|
|
73
|
+
ctx.state.ssrSeedPlan.steps.filter(s => s.kind !== 'env-reader').map(s => s.name),
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
const materialized = materializeGetterCalls(cb.arrow.body, knownGetterNames)
|
|
77
|
+
const predJSON = serializeParsedExpr(materialized)
|
|
78
|
+
if (predJSON === null) return null
|
|
79
|
+
|
|
80
|
+
const paramName = cb.arrow.params[0] ?? '_'
|
|
81
|
+
const freeVars = freeVarsInBody(materialized, new Set(cb.arrow.params))
|
|
82
|
+
return { propName, predJSON, paramName, freeVars }
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Names of EARLIER sibling memos (per `ctx.state.currentMemos` declaration
|
|
87
|
+
* order) that a filter-arm memo's predicate free variables resolve to — the
|
|
88
|
+
* constructor generator hoists these into a shared local so the sibling's
|
|
89
|
+
* expression isn't emitted twice (once for its own field, once inlined in
|
|
90
|
+
* this memo's `bf.FilterEval` env map, #2075/#2077 review finding 3). Empty
|
|
91
|
+
* when `memo` isn't a filter-arm memo or references nothing hoistable.
|
|
92
|
+
*/
|
|
93
|
+
export function filterArmEarlierSiblingRefs(
|
|
94
|
+
ctx: GoEmitContext,
|
|
95
|
+
memo: { name: string; parsed?: ParsedExpr },
|
|
96
|
+
signals: { getter: string; initialValue: string }[],
|
|
97
|
+
propsParams: { name: string; type?: TypeInfo; defaultValue?: string }[],
|
|
98
|
+
): string[] {
|
|
99
|
+
if (!memo.parsed) return []
|
|
100
|
+
const match = matchFilterArmMemo(ctx, memo.parsed, signals, propsParams)
|
|
101
|
+
if (!match) return []
|
|
102
|
+
// `currentMemos` order equals the plan's memo-step order (plan lists
|
|
103
|
+
// signals first, then memos, both in declaration order) — kept here rather
|
|
104
|
+
// than filtering `ssrSeedPlan.steps` since this index is memo-only already.
|
|
105
|
+
const memos = ctx.state.currentMemos ?? []
|
|
106
|
+
const currentIndex = memos.findIndex(m => m.name === memo.name)
|
|
107
|
+
if (currentIndex < 0) return []
|
|
108
|
+
const refs: string[] = []
|
|
109
|
+
for (const name of match.freeVars) {
|
|
110
|
+
const idx = memos.findIndex(m => m.name === name)
|
|
111
|
+
if (idx >= 0 && idx < currentIndex) refs.push(name)
|
|
112
|
+
}
|
|
113
|
+
return refs
|
|
114
|
+
}
|
|
115
|
+
|
|
24
116
|
/**
|
|
25
117
|
* Compute a memo's SSR initial value as a Go expression — e.g.
|
|
26
118
|
* `() => count() * 2` → `in.Initial * 2`, `() => props.value * 10` →
|
|
@@ -39,8 +131,11 @@ export function computeMemoInitialValue(
|
|
|
39
131
|
propFallbackVars: ReadonlyMap<string, PropFallbackVar> = EMPTY_PROP_FALLBACK_VARS,
|
|
40
132
|
goType?: string,
|
|
41
133
|
): string {
|
|
134
|
+
// Seed the resolution stack with this memo's own name — every external call
|
|
135
|
+
// is a fresh top-level computation, so self-reference must be caught on the
|
|
136
|
+
// very first recursion, not only on the second visit.
|
|
42
137
|
const resolved = computeMemoInitialValueOrNull(
|
|
43
|
-
ctx, memo, signals, propsParams, propFallbackVars,
|
|
138
|
+
ctx, memo, signals, propsParams, propFallbackVars, new Set([memo.name]),
|
|
44
139
|
)
|
|
45
140
|
if (resolved !== null) return resolved
|
|
46
141
|
// Zero value for the memo's Go type: `false` for bool, `""` for string,
|
|
@@ -76,25 +171,112 @@ export function memoInitialFromParsedBody(
|
|
|
76
171
|
signals: { getter: string; initialValue: string }[],
|
|
77
172
|
propsParams: { name: string; type?: TypeInfo; defaultValue?: string }[],
|
|
78
173
|
propFallbackVars: ReadonlyMap<string, PropFallbackVar>,
|
|
174
|
+
currentMemoName: string,
|
|
175
|
+
resolving: ReadonlySet<string> = new Set(),
|
|
79
176
|
): string | null {
|
|
80
177
|
const propRef = (propName: string): string => {
|
|
81
178
|
const hoisted = propFallbackVars.get(propName)
|
|
82
179
|
if (hoisted) return hoisted.varName
|
|
83
180
|
return `in.${capitalizeFieldName(propName)}`
|
|
84
181
|
}
|
|
85
|
-
// A
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
//
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
e.object
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
182
|
+
// A request-scoped env-signal read (`searchParams().get('k')`, any local
|
|
183
|
+
// alias, #1922) → the literal key and the reader's canonical field name,
|
|
184
|
+
// else null. Constraints: the receiver must be a zero-arg call of a local
|
|
185
|
+
// bound to a registered env signal (`ctx.state.envSignalReadersByLocal`,
|
|
186
|
+
// keyed by that reader's own registration — not a hardcoded key); the
|
|
187
|
+
// method must be one that reader's `methods` set recognises; the key
|
|
188
|
+
// argument must be a string literal.
|
|
189
|
+
const envGetKey = (e: ParsedExpr): { key: string; fieldName: string } | null => {
|
|
190
|
+
if (e.kind !== 'call' || e.callee.kind !== 'member' || e.callee.computed) return null
|
|
191
|
+
const recvName = getterCallName(e.callee.object)
|
|
192
|
+
if (recvName === null) return null
|
|
193
|
+
const reader = ctx.state.envSignalReadersByLocal.get(recvName)
|
|
194
|
+
if (!reader || !reader.methods.has(e.callee.property)) return null
|
|
195
|
+
const arg = e.args[0]
|
|
196
|
+
if (!arg || arg.kind !== 'literal' || arg.literalType !== 'string') return null
|
|
197
|
+
return { key: String(arg.value), fieldName: capitalizeFieldName(reader.canonicalName) }
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// () => searchParams().get('k') — a derived memo over the env signal
|
|
201
|
+
// (#2069). The constructor reads the canonical `in.SearchParams` reader the
|
|
202
|
+
// handler fills per request, so the memo SSR-computes instead of zero-valuing.
|
|
203
|
+
{
|
|
204
|
+
const m = envGetKey(body)
|
|
205
|
+
if (m !== null) return `in.${m.fieldName}.Get(${JSON.stringify(m.key)})`
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// () => searchParams().get('k') ?? '<lit>' — the defaulted form. Go's
|
|
209
|
+
// `SearchParams.Get` returns "" for an absent key (it can't surface JS's
|
|
210
|
+
// null), so the default fires on "" — the same documented `?? → or`
|
|
211
|
+
// divergence as the template-position lowering (`or (.SearchParams.Get
|
|
212
|
+
// "k") "<lit>"`); the two positions stay consistent.
|
|
213
|
+
if (
|
|
214
|
+
body.kind === 'logical' &&
|
|
215
|
+
(body.op === '??' || body.op === '||') &&
|
|
216
|
+
body.right.kind === 'literal' &&
|
|
217
|
+
body.right.literalType === 'string'
|
|
218
|
+
) {
|
|
219
|
+
const m = envGetKey(body.left)
|
|
220
|
+
if (m !== null) {
|
|
221
|
+
const def = JSON.stringify(String(body.right.value))
|
|
222
|
+
return `func() string { if v := in.${m.fieldName}.Get(${JSON.stringify(m.key)}); v != "" { return v }; return ${def} }()`
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// () => props.X.filter((p) => <predicate>) — a LIST-valued derived memo
|
|
227
|
+
// chained off a sibling memo the predicate closes over (e.g. a
|
|
228
|
+
// searchParams()-derived `tag`). Env-signal getters are excluded from the
|
|
229
|
+
// free-var materialization set: they're per-request READERS, not values,
|
|
230
|
+
// and the runtime evaluator has no way to invoke one.
|
|
231
|
+
{
|
|
232
|
+
const match = matchFilterArmMemo(ctx, body, signals, propsParams)
|
|
233
|
+
if (match) {
|
|
234
|
+
const { propName, predJSON, paramName, freeVars } = match
|
|
235
|
+
const currentIndex = (ctx.state.currentMemos ?? []).findIndex(m => m.name === currentMemoName)
|
|
236
|
+
const envEntries: string[] = []
|
|
237
|
+
let unresolved = false
|
|
238
|
+
for (const name of freeVars) {
|
|
239
|
+
let goExpr: string | null = null
|
|
240
|
+
const siblingIndex = (ctx.state.currentMemos ?? []).findIndex(m => m.name === name)
|
|
241
|
+
if (siblingIndex >= 0) {
|
|
242
|
+
// Only a sibling declared EARLIER than the memo being computed is
|
|
243
|
+
// eligible — mirrors JS declaration order and rules out both
|
|
244
|
+
// self-reference and any mutual pair (at most one direction can
|
|
245
|
+
// satisfy `earlier`), independent of the resolving-stack guard
|
|
246
|
+
// below. A hoisted local (the constructor emitted it once already,
|
|
247
|
+
// #2075/#2077 review finding 3) is reused verbatim instead of
|
|
248
|
+
// recomputing the expression a second time.
|
|
249
|
+
const siblingMemo = ctx.state.currentMemos![siblingIndex]
|
|
250
|
+
const eligible = currentIndex >= 0 && siblingIndex < currentIndex && !resolving.has(siblingMemo.name)
|
|
251
|
+
if (eligible) {
|
|
252
|
+
const hoisted = ctx.state.hoistedMemoLocals.get(siblingMemo.name)
|
|
253
|
+
goExpr = hoisted ?? computeMemoInitialValueOrNull(
|
|
254
|
+
ctx, siblingMemo, signals, propsParams, propFallbackVars,
|
|
255
|
+
new Set([...resolving, siblingMemo.name]),
|
|
256
|
+
)
|
|
257
|
+
}
|
|
258
|
+
} else {
|
|
259
|
+
const sig = signals.find(s => s.getter === name)
|
|
260
|
+
if (sig) {
|
|
261
|
+
goExpr = getSignalInitialValueAsGo(ctx, sig.initialValue, propsParams, propFallbackVars)
|
|
262
|
+
} else {
|
|
263
|
+
const p = propsParams.find(pp => pp.name === name)
|
|
264
|
+
if (p) goExpr = propRef(name)
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
if (goExpr === null) {
|
|
268
|
+
unresolved = true
|
|
269
|
+
break
|
|
270
|
+
}
|
|
271
|
+
envEntries.push(`${JSON.stringify(name)}: ${goExpr}`)
|
|
272
|
+
}
|
|
273
|
+
if (!unresolved) {
|
|
274
|
+
const itemsField = `in.${capitalizeFieldName(propName)}`
|
|
275
|
+
const envMap = `map[string]any{${envEntries.join(', ')}}`
|
|
276
|
+
return `bf.FilterEval(${itemsField}, "${escapeGoString(predJSON)}", ${JSON.stringify(paramName)}, ${envMap})`
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
}
|
|
98
280
|
|
|
99
281
|
// () => getter() === 'lit' / !== 'lit' — a selection memo. Resolves to a Go
|
|
100
282
|
// bool when the signal's initial value is itself a string literal.
|
|
@@ -150,8 +332,15 @@ export function memoInitialFromParsedBody(
|
|
|
150
332
|
condGo = getSignalInitialValueAsGo(ctx, condSignal.initialValue, propsParams, propFallbackVars)
|
|
151
333
|
} else {
|
|
152
334
|
const condMemo = (ctx.state.currentMemos ?? []).find(m => m.name === condName)
|
|
153
|
-
|
|
154
|
-
|
|
335
|
+
// A condition memo already on the resolution stack (self- or
|
|
336
|
+
// mutually-referencing ternaries) falls back to null — the same
|
|
337
|
+
// zero-value default as any other unresolved shape — instead of
|
|
338
|
+
// recursing forever.
|
|
339
|
+
if (condMemo && !resolving.has(condMemo.name)) {
|
|
340
|
+
condGo = computeMemoInitialValueOrNull(
|
|
341
|
+
ctx, condMemo, signals, propsParams, propFallbackVars,
|
|
342
|
+
new Set([...resolving, condMemo.name]),
|
|
343
|
+
)
|
|
155
344
|
}
|
|
156
345
|
}
|
|
157
346
|
if (condGo === 'true') return t
|
|
@@ -259,6 +448,15 @@ export function computeMemoInitialValueOrNull(
|
|
|
259
448
|
signals: { getter: string; initialValue: string }[],
|
|
260
449
|
propsParams: { name: string; type?: TypeInfo; defaultValue?: string }[],
|
|
261
450
|
propFallbackVars: ReadonlyMap<string, PropFallbackVar> = EMPTY_PROP_FALLBACK_VARS,
|
|
451
|
+
/**
|
|
452
|
+
* Memo names currently being resolved on this call stack — guards against
|
|
453
|
+
* unbounded recursion when memos reference each other (mutually, or a
|
|
454
|
+
* self-reference). Callers that start a fresh top-level computation seed
|
|
455
|
+
* this with `memo.name`; recursive calls extend it with the sibling memo
|
|
456
|
+
* about to be entered. A candidate already in the set resolves to `null`
|
|
457
|
+
* (the shape's usual "unsupported" fallback) instead of recursing.
|
|
458
|
+
*/
|
|
459
|
+
resolving: ReadonlySet<string> = new Set(),
|
|
262
460
|
): string | null {
|
|
263
461
|
const computation = memo.computation
|
|
264
462
|
|
|
@@ -275,6 +473,8 @@ export function computeMemoInitialValueOrNull(
|
|
|
275
473
|
signals,
|
|
276
474
|
propsParams,
|
|
277
475
|
propFallbackVars,
|
|
476
|
+
memo.name,
|
|
477
|
+
resolving,
|
|
278
478
|
)
|
|
279
479
|
if (fromParsed !== null) return fromParsed
|
|
280
480
|
}
|
|
@@ -288,6 +488,7 @@ export function computeMemoInitialValueOrNull(
|
|
|
288
488
|
signals,
|
|
289
489
|
propsParams,
|
|
290
490
|
propFallbackVars,
|
|
491
|
+
resolving,
|
|
291
492
|
)
|
|
292
493
|
if (cmpTernary !== null) return cmpTernary
|
|
293
494
|
|
|
@@ -329,6 +530,7 @@ export function resolveGetterValueAsGo(
|
|
|
329
530
|
signals: { getter: string; initialValue: string }[],
|
|
330
531
|
propsParams: { name: string; type?: TypeInfo; defaultValue?: string }[],
|
|
331
532
|
propFallbackVars: ReadonlyMap<string, PropFallbackVar>,
|
|
533
|
+
resolving: ReadonlySet<string> = new Set(),
|
|
332
534
|
): string | null {
|
|
333
535
|
const signal = signals.find(s => s.getter === name)
|
|
334
536
|
if (signal) {
|
|
@@ -336,13 +538,18 @@ export function resolveGetterValueAsGo(
|
|
|
336
538
|
}
|
|
337
539
|
const memo = (ctx.state.currentMemos ?? []).find(m => m.name === name)
|
|
338
540
|
if (memo) {
|
|
541
|
+
// Already on the resolution stack (self- or mutually-referencing
|
|
542
|
+
// ternary operands) — fall back to null rather than recurse forever.
|
|
543
|
+
if (resolving.has(memo.name)) return null
|
|
339
544
|
const stripped = memo.computation.replace(/^\(\)\s*=>\s*/, '')
|
|
340
545
|
const fb = ctx.extractPropFallback(stripped)
|
|
341
546
|
if (fb && capitalizeFieldName(fb.propName) === capitalizeFieldName(memo.name)) {
|
|
342
547
|
const field = `in.${capitalizeFieldName(fb.propName)}`
|
|
343
548
|
return `func() interface{} { v := interface{}(${field}); if v == nil || v == "" { return ${fb.goFallback} }; return v }()`
|
|
344
549
|
}
|
|
345
|
-
return computeMemoInitialValueOrNull(
|
|
550
|
+
return computeMemoInitialValueOrNull(
|
|
551
|
+
ctx, memo, signals, propsParams, propFallbackVars, new Set([...resolving, memo.name]),
|
|
552
|
+
)
|
|
346
553
|
}
|
|
347
554
|
const param = propsParams.find(p => p.name === name)
|
|
348
555
|
if (param) {
|
|
@@ -366,6 +573,7 @@ export function computeComparisonTernaryGo(
|
|
|
366
573
|
signals: { getter: string; initialValue: string }[],
|
|
367
574
|
propsParams: { name: string; type?: TypeInfo; defaultValue?: string }[],
|
|
368
575
|
propFallbackVars: ReadonlyMap<string, PropFallbackVar>,
|
|
576
|
+
resolving: ReadonlySet<string> = new Set(),
|
|
369
577
|
): string | null {
|
|
370
578
|
// Matches any `conditional` `parsed` — an expression-bodied ternary or, since
|
|
371
579
|
// #2040, a block-bodied memo whose value `if` / early-return folded to one.
|
|
@@ -398,6 +606,7 @@ export function computeComparisonTernaryGo(
|
|
|
398
606
|
signals,
|
|
399
607
|
propsParams,
|
|
400
608
|
propFallbackVars,
|
|
609
|
+
resolving,
|
|
401
610
|
)
|
|
402
611
|
if (condGo === null) return null
|
|
403
612
|
|
|
@@ -420,10 +629,11 @@ export function resolveComparisonOperandGo(
|
|
|
420
629
|
signals: { getter: string; initialValue: string }[],
|
|
421
630
|
propsParams: { name: string; type?: TypeInfo; defaultValue?: string }[],
|
|
422
631
|
propFallbackVars: ReadonlyMap<string, PropFallbackVar>,
|
|
632
|
+
resolving: ReadonlySet<string> = new Set(),
|
|
423
633
|
): string | null {
|
|
424
634
|
// getter(): a signal or memo getter call.
|
|
425
635
|
if (node.kind === 'call' && node.callee.kind === 'identifier' && node.args.length === 0) {
|
|
426
|
-
return resolveGetterValueAsGo(ctx, node.callee.name, signals, propsParams, propFallbackVars)
|
|
636
|
+
return resolveGetterValueAsGo(ctx, node.callee.name, signals, propsParams, propFallbackVars, resolving)
|
|
427
637
|
}
|
|
428
638
|
// props.X ?? 'def' — nil/empty-tolerant field read with the default folded in.
|
|
429
639
|
if (node.kind === 'logical' && node.op === '??' && node.right.kind === 'literal' && node.right.literalType === 'string') {
|
|
@@ -6,11 +6,25 @@
|
|
|
6
6
|
* right SSR zero value).
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
+
import { asCallbackMethodCall } from '@barefootjs/jsx'
|
|
9
10
|
import type { ParsedExpr, TypeInfo } from '@barefootjs/jsx'
|
|
10
11
|
|
|
11
12
|
import type { GoEmitContext } from '../emit-context.ts'
|
|
12
13
|
import { typeInfoToGo } from '../type/type-codegen.ts'
|
|
13
14
|
|
|
15
|
+
/**
|
|
16
|
+
* True when a memo's body is a `.filter(<arrow>)` callback-method call
|
|
17
|
+
* (#2075) — a LIST-valued derived memo (the blog PostList `visible` shape:
|
|
18
|
+
* `createMemo(() => props.items.filter((p) => …))`), not a scalar. Exported
|
|
19
|
+
* so both `isBooleanMemo` (guard below) and `inferMemoType`'s field-type
|
|
20
|
+
* decision (go-template-adapter.ts) share the one recognition point.
|
|
21
|
+
*/
|
|
22
|
+
export function isListFilterMemo(memo: { parsed?: ParsedExpr }): boolean {
|
|
23
|
+
if (!memo.parsed) return false
|
|
24
|
+
const cb = asCallbackMethodCall(memo.parsed)
|
|
25
|
+
return cb !== null && cb.method === 'filter'
|
|
26
|
+
}
|
|
27
|
+
|
|
14
28
|
/**
|
|
15
29
|
* Heuristic: does this memo evaluate to a boolean? True when its computation is
|
|
16
30
|
* a comparison (`!==`/`===`/`!=`/`==`), a negation (`!x`), or a ternary whose
|
|
@@ -24,6 +38,11 @@ export function isBooleanMemo(
|
|
|
24
38
|
propsParamMap: Map<string, { name: string; type: TypeInfo; defaultValue?: string }>,
|
|
25
39
|
): boolean {
|
|
26
40
|
const c = memo.computation
|
|
41
|
+
// A LIST-valued `.filter(arrow)` memo (#2075) is never boolean, even though
|
|
42
|
+
// its predicate arrow body often contains a `!` negation (`!tag() || …`) —
|
|
43
|
+
// that would otherwise trip the `/=>\s*!/` heuristic below into
|
|
44
|
+
// misclassifying the whole memo. Bail before any regex runs.
|
|
45
|
+
if (isListFilterMemo(memo)) return false
|
|
27
46
|
// A ternary whose two branches are string literals is a STRING memo, not
|
|
28
47
|
// boolean — the `===` lives in the *condition*, so the blanket comparison
|
|
29
48
|
// check below would misclassify it as bool and bake `false`. Bail first.
|
package/src/test-render.ts
CHANGED
|
@@ -615,13 +615,23 @@ function buildGoPropsInit(
|
|
|
615
615
|
// a bare `[]any{…}` would fail to compile against the typed field.
|
|
616
616
|
// Fall back to `[]any` when the field type is `[]any` / unknown.
|
|
617
617
|
const elemType = goSliceElemType(goTypes, componentName, goField)
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
}
|
|
624
|
-
|
|
618
|
+
let sliceLiteral: string
|
|
619
|
+
if (elemType && elemType.startsWith('map[')) {
|
|
620
|
+
// An untyped object-array Input field (an inline prop object type
|
|
621
|
+
// that didn't synthesize a named struct, e.g. `items: { title:
|
|
622
|
+
// string; tags: string[] }[]` — `typeInfoToGo`'s 'object' case,
|
|
623
|
+
// type-codegen.ts) resolves to `[]map[string]interface{}`, not a
|
|
624
|
+
// named struct. `goTypedSliceLiteralFromArray`'s `goStructLiteral`
|
|
625
|
+
// emits bare `Field: value` entries, which is struct-literal syntax
|
|
626
|
+
// and doesn't compile as a map literal's keys — route these through
|
|
627
|
+
// the map-literal builder instead (#2075, search-params-derived-filter).
|
|
628
|
+
sliceLiteral = goTypedMapSliceLiteralFromArray(value, elemType)
|
|
629
|
+
} else if (elemType) {
|
|
630
|
+
sliceLiteral = goTypedSliceLiteralFromArray(value, elemType)
|
|
631
|
+
} else {
|
|
632
|
+
sliceLiteral = goArrayLiteralFromArray(value)
|
|
633
|
+
}
|
|
634
|
+
lines.push(`\t\t${goField}: ${sliceLiteral},`)
|
|
625
635
|
} else if (value && typeof value === 'object') {
|
|
626
636
|
// Plain object → Go `map[string]any` literal (#1407 follow-up).
|
|
627
637
|
// Used by `jsx-spread-rest-prop` to populate the input-bag
|
|
@@ -658,7 +668,11 @@ function goSliceElemType(
|
|
|
658
668
|
new RegExp(`type ${componentName}Input struct \\{([\\s\\S]*?)\\n\\}`),
|
|
659
669
|
)
|
|
660
670
|
if (!struct) return null
|
|
661
|
-
|
|
671
|
+
// The element-type token can carry brackets/braces of its own (a map type
|
|
672
|
+
// like `map[string]interface{}`), not just word chars — broadened so the
|
|
673
|
+
// capture doesn't truncate at the first `[`. The token has no internal
|
|
674
|
+
// whitespace, so it still stops cleanly before a trailing `// comment`.
|
|
675
|
+
const field = struct[1].match(new RegExp(`\\n\\s*${goField}\\s+\\[\\]([\\w.[\\]{}]+)`))
|
|
662
676
|
if (!field) return null
|
|
663
677
|
const elem = field[1]
|
|
664
678
|
if (elem === 'any' || elem === 'interface{}') return null
|
|
@@ -683,6 +697,30 @@ function goTypedSliceLiteralFromArray(arr: unknown[], elemType: string): string
|
|
|
683
697
|
return `[]${elemType}{${entries.join(', ')}}`
|
|
684
698
|
}
|
|
685
699
|
|
|
700
|
+
/**
|
|
701
|
+
* Emit a typed Go slice-of-map literal (`[]map[string]interface{}{map[string]any{…}, …}`)
|
|
702
|
+
* for an untyped object-array Input field. Object elements become
|
|
703
|
+
* `map[string]any{"Field": val, …}` literals with PascalCase keys — Go
|
|
704
|
+
* template map lookup is case-sensitive, so `{{.Title}}` needs a capitalized
|
|
705
|
+
* key, same as the untyped `goArrayLiteralFromArray` fallback's object
|
|
706
|
+
* entries. `map[string]any` and `map[string]interface{}` are the identical
|
|
707
|
+
* type (`any` is the builtin alias for `interface{}`), so the emitted
|
|
708
|
+
* literal is assignable to `elemType` regardless of which spelling the
|
|
709
|
+
* Input struct field carries. (#2075, search-params-derived-filter)
|
|
710
|
+
*/
|
|
711
|
+
function goTypedMapSliceLiteralFromArray(arr: unknown[], elemType: string): string {
|
|
712
|
+
const entries = arr.map(v => {
|
|
713
|
+
if (v && typeof v === 'object' && !Array.isArray(v)) {
|
|
714
|
+
return goMapLiteralFromObject(v as Record<string, unknown>, true)
|
|
715
|
+
}
|
|
716
|
+
if (typeof v === 'string') return `"${v.replace(/"/g, '\\"')}"`
|
|
717
|
+
if (typeof v === 'number' || typeof v === 'boolean') return String(v)
|
|
718
|
+
if (v === null) return 'nil'
|
|
719
|
+
return goArrayLiteralFromArray(v as unknown[])
|
|
720
|
+
})
|
|
721
|
+
return `[]${elemType}{${entries.join(', ')}}`
|
|
722
|
+
}
|
|
723
|
+
|
|
686
724
|
/**
|
|
687
725
|
* Emit a keyed Go struct literal (`Elem{Field: val, …}`) with PascalCase field
|
|
688
726
|
* names. Only the keys the caller supplied are set, so an omitted optional prop
|