@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.
@@ -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 bare zero-arg getter call (`count()`) its name, else null.
86
- const getterCallName = (e: ParsedExpr): string | null =>
87
- e.kind === 'call' && e.callee.kind === 'identifier' && e.args.length === 0
88
- ? e.callee.name
89
- : null
90
- // A `props.X` member access the prop name, else null.
91
- const propsMemberName = (e: ParsedExpr): string | null =>
92
- e.kind === 'member' &&
93
- !e.computed &&
94
- e.object.kind === 'identifier' &&
95
- e.object.name === 'props'
96
- ? e.property
97
- : null
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
- if (condMemo) {
154
- condGo = computeMemoInitialValueOrNull(ctx, condMemo, signals, propsParams, propFallbackVars)
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(ctx, memo, signals, propsParams, propFallbackVars)
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.
@@ -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
- lines.push(
619
- `\t\t${goField}: ${
620
- elemType
621
- ? goTypedSliceLiteralFromArray(value, elemType)
622
- : goArrayLiteralFromArray(value)
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
- const field = struct[1].match(new RegExp(`\\n\\s*${goField}\\s+\\[\\]([\\w.]+)`))
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