@barefootjs/go-template 0.31.8 → 0.31.10

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.
@@ -382,6 +382,12 @@ func main() {
382
382
  ScopeID: ${JSON.stringify(rootScopeId)},
383
383
  ${propsInit}
384
384
  })
385
+ // Mirrors production's Renderer.renderComponentInto (bf.go), which marks
386
+ // the top-level component so BfPropsAttr emits bf-p: this harness calls
387
+ // tmpl.ExecuteTemplate directly instead of going through Renderer, so
388
+ // nothing else sets this flag. Without it every fixture's bf-p attribute
389
+ // is silently absent regardless of what the adapter itself does.
390
+ props.BfIsRoot = true
385
391
  ${dynamicSeedingLines.length > 0 ? dynamicSeedingLines.join('\n') + '\n' : ''} if err := tmpl.ExecuteTemplate(os.Stdout, "${componentName}", props); err != nil {
386
392
  os.Stderr.WriteString("template error: " + err.Error() + "\\n")
387
393
  os.Exit(1)
@@ -587,57 +593,148 @@ function findBaseSignalGetter(expr: ParsedExpr | undefined, signalGetters: Reado
587
593
  }
588
594
 
589
595
  /**
590
- * (#2209 part 2) Replicate, in the generated `main.go`, the documented
591
- * "the route handler populates the loop-body child-component slice at
592
- * request time" contract for a signal-backed dynamic loop
593
- * `generateNewPropsFunction`'s doc comment on `<Name>s []<Name>Props`
594
- * in `adapter/go-template-adapter.ts`. The constructor only ever seeds
595
- * the loop's DATUM slice (e.g. `.Todos`, straight from the caller's
596
- * Input); the child-component Props slice the template actually
597
- * ranges over (`.TodoItems`) has no server-side population path in
598
- * this harness the Hono reference materializes it by literally
599
- * executing the component, so this closes the gap the same way:
600
- * derive each item's child Props from the datum slice, exactly as a
601
- * real route handler is documented to.
596
+ * (#2630) Resolve a `isPropDerived` loop's `arrayParsed` to the JS-level
597
+ * prop name it reads. `isArrayExprDirectPropRef` (`jsx-to-ir.ts`) is what
598
+ * sets `isPropDerivedArray` in the first place, and it only recognizes two
599
+ * shapes a bare destructured-prop identifier (`entries.map(...)` inside
600
+ * `function({ entries })`) or a direct `props.<name>` member access — so
601
+ * unlike `findBaseSignalGetter` there is no `call`/chain walking to do; the
602
+ * compiler already restricted the shape. Returns the prop's LOCAL binding
603
+ * name; the caller resolves it to the CALLER-FACING field
604
+ * (`sourceName ?? name`) via `propsParams`, matching how
605
+ * `generateInputStruct`/`generateNewPropsFunction` key the Go field and how
606
+ * `buildGoPropsInit` keys the harness's own prop initializer (both by the
607
+ * JS `props` object's key, not the local destructure binding).
608
+ */
609
+ function findLoopPropField(expr: ParsedExpr | undefined): string | null {
610
+ if (!expr) return null
611
+ if (expr.kind === 'identifier') return expr.name
612
+ if (expr.kind === 'member' && expr.object.kind === 'identifier') return expr.property
613
+ return null
614
+ }
615
+
616
+ /**
617
+ * Resolve a loop-body child prop's VALUE expression to the Go expression
618
+ * that reads it off the per-row `item` local: a bare pass-through of the
619
+ * loop param (`todo={todo}` → `item`) or a member access on it
620
+ * (`variant={entry.variant}` → `item.Variant`, #2630 — the
621
+ * `static-array-from-props-with-component-precomputed` fixture's `<Tag
622
+ * id={entry.id} variant={entry.variant} />`). `null` for anything else (a
623
+ * prop that doesn't derive from the loop row at all).
624
+ */
625
+ function goItemExprForLoopProp(parsed: ParsedExpr, loopParam: string): string | null {
626
+ if (parsed.kind === 'identifier' && parsed.name === loopParam) return 'item'
627
+ if (
628
+ parsed.kind === 'member' &&
629
+ !parsed.computed &&
630
+ parsed.object.kind === 'identifier' &&
631
+ parsed.object.name === loopParam
632
+ ) {
633
+ return `item.${capitalizeFieldName(parsed.property)}`
634
+ }
635
+ return null
636
+ }
637
+
638
+ /**
639
+ * (#2209 part 2, extended by #2630) Replicate, in the generated `main.go`,
640
+ * the documented "the route handler populates the loop-body child-component
641
+ * slice at request time" contract from `generateNewPropsFunction`'s doc
642
+ * comment on `<Name>s []<Name>Props` in `adapter/go-template-adapter.ts`.
643
+ * Covers TWO loop shapes that both leave `.<Name>s` unpopulated in this
644
+ * harness, for different reasons:
645
+ *
646
+ * - **Signal-backed dynamic** (`isDynamic && !isPropDerived`, e.g.
647
+ * `todos().map(...)`): the constructor only ever seeds the loop's DATUM
648
+ * slice (`.Todos`, straight from the caller's Input) — the child Props
649
+ * slice the template ranges over (`.TodoItems`) is documented as
650
+ * handler-populated and never touched by `NewXxxProps` at all.
651
+ * - **Prop-backed static** (`isPropDerived`, e.g. `props.entries.map(...)`,
652
+ * #2630): `NewXxxProps` DOES try to build `.Tags` from `in.Tags` — but
653
+ * `in.Tags` is a SEPARATE Input field from the driving prop (`Entries`)
654
+ * whenever the child's plural name doesn't literally coincide with the
655
+ * prop's own name (`propDerivedNestedArrayFields`/`isNestedArrayShadowed`
656
+ * in the real adapter), and this harness's `buildGoPropsInit` only ever
657
+ * populates fields from the JS `props` object's own keys — `tags` isn't
658
+ * one. `in.Tags` stays a nil slice, so the constructor's own range over
659
+ * it produces zero rows. Both shapes are closed the same way: derive
660
+ * each item's child Props from the resolved datum slice AFTER
661
+ * construction, overwriting whatever (empty, for the prop-backed case)
662
+ * slice the constructor built — exactly as a real route handler is
663
+ * documented to.
664
+ *
665
+ * The Hono reference needs none of this because it materializes children by
666
+ * literally executing the component.
602
667
  *
603
668
  * Deliberately narrow: only fires for a loop whose (a) array source
604
- * resolves, through `call`/`member` chains, to a component signal
605
- * getter, (b) generated Go TEMPLATE text actually ranges over
669
+ * resolves to a signal getter (dynamic) or a direct prop reference
670
+ * (prop-derived) never a `call`/member chain beyond what each shape's own
671
+ * resolver recognizes, (b) generated Go TEMPLATE text actually ranges over
606
672
  * `.<Name>s` (a plain substring check on GENERATED GO OUTPUT — not JS
607
- * parsing — so a `/* @client *\/`-marked loop, whose SSR template has
608
- * no such range, is untouched by construction), and (c) at least one
609
- * child prop is a bare pass-through of the loop item (`todo={todo}`).
610
- * Returns the Go statements to splice into `main()` plus whether `fmt`
611
- * needs importing.
673
+ * parsing — so a `/* @client *\/`-marked loop, whose SSR template has no
674
+ * such range, is untouched by construction), and (c) every resolved child
675
+ * prop is either a bare pass-through of the loop item (`todo={todo}`) or a
676
+ * member access on it (`variant={entry.variant}`). Returns the Go
677
+ * statements to splice into `main()` plus whether `fmt` needs importing.
612
678
  */
613
679
  function buildDynamicChildLoopSeeding(
614
680
  ir: ComponentIR,
615
681
  template: string,
616
682
  ): { lines: string[]; needsFmt: boolean } {
617
683
  const signalGetters = new Set(ir.metadata.signals.map(s => s.getter))
684
+ const propsParams = ir.metadata.propsParams
618
685
  const lines: string[] = []
619
686
  let needsFmt = false
620
687
  for (const nested of findNestedComponents(ir.root) as NestedComponentInfo[]) {
621
- if (!nested.isDynamic || nested.isPropDerived) continue
622
688
  if (nested.bodyChildren && nested.bodyChildren.length > 0) continue
623
689
  if (!nested.loopParam) continue
624
690
  if (!template.includes(`:= .${nested.name}s}}`)) continue
625
- const datumField = findBaseSignalGetter(nested.loopArrayParsed, signalGetters)
691
+
692
+ let datumField: string | null
693
+ if (nested.isPropDerived) {
694
+ const localName = findLoopPropField(nested.loopArrayParsed)
695
+ const param = localName ? propsParams.find(p => p.name === localName) : undefined
696
+ datumField = param ? (param.sourceName ?? param.name) : null
697
+ // (#2627/#2628 shadowing) When the driving prop's Go field name
698
+ // coincides with the child's own plural (`tags` prop → `Tag`
699
+ // component), the REAL adapter's `isNestedArrayShadowed` drops the
700
+ // prop's separate field entirely — `props.Tags` IS the only field,
701
+ // pre-shaped `TagInput` rows and all, already populated straight from
702
+ // the harness's own `buildGoPropsInit` (which types the literal
703
+ // against the Input struct's declared `[]TagInput` field). Seeding
704
+ // here would `range` the very slice it just emptied with `make` on
705
+ // the line above it — skip so that already-correct path stays
706
+ // untouched.
707
+ if (datumField && capitalizeFieldName(datumField) === capitalizeFieldName(`${nested.name}s`)) continue
708
+ } else if (nested.isDynamic) {
709
+ datumField = findBaseSignalGetter(nested.loopArrayParsed, signalGetters)
710
+ } else {
711
+ continue
712
+ }
626
713
  if (!datumField) continue
627
714
 
715
+ // All-or-nothing (Copilot review on #2630's PR): a prop that is
716
+ // legitimately SSR-irrelevant (event handler, `key`, dashed debug
717
+ // attrs) is exempt, but any OTHER prop this resolver can't express as
718
+ // an `item` read means the seeded `<Name>Input{...}` would carry Go
719
+ // zero values for it — silently wrong SSR instead of the loudly-empty
720
+ // host the unseeded path produces. Bail on the whole component in
721
+ // that case rather than seed an incomplete slice.
628
722
  const inputFields: string[] = []
723
+ let unresolvableProp = false
629
724
  for (const prop of nested.props) {
630
725
  if (prop.isEventHandler) continue
631
726
  if (prop.name === 'key' || prop.name.includes('-')) continue
632
- if (
633
- prop.value.kind === 'expression' &&
634
- prop.value.parsed?.kind === 'identifier' &&
635
- prop.value.parsed.name === nested.loopParam
636
- ) {
637
- inputFields.push(`${capitalizeFieldName(prop.name)}: item`)
727
+ const itemExpr =
728
+ prop.value.kind === 'expression' && prop.value.parsed
729
+ ? goItemExprForLoopProp(prop.value.parsed, nested.loopParam)
730
+ : null
731
+ if (itemExpr === null) {
732
+ unresolvableProp = true
733
+ break
638
734
  }
735
+ inputFields.push(`${capitalizeFieldName(prop.name)}: ${itemExpr}`)
639
736
  }
640
- if (inputFields.length === 0) continue
737
+ if (unresolvableProp || inputFields.length === 0) continue
641
738
 
642
739
  lines.push(`\tprops.${nested.name}s = make([]${nested.name}Props, len(props.${capitalizeFieldName(datumField)}))`)
643
740
  lines.push(`\tfor i, item := range props.${capitalizeFieldName(datumField)} {`)
@@ -734,27 +831,37 @@ function buildGoPropsInit(
734
831
  let sliceLiteral: string
735
832
  if (elemType && elemType.startsWith('map[')) {
736
833
  // An untyped object-array Input field (an inline prop object type
737
- // that didn't synthesize a named struct, e.g. `items: { title:
738
- // string; tags: string[] }[]` `typeInfoToGo`'s 'object' case,
739
- // type-codegen.ts) resolves to `[]map[string]interface{}`, not a
740
- // named struct. `goTypedSliceLiteralFromArray`'s `goStructLiteral`
741
- // emits bare `Field: value` entries, which is struct-literal syntax
742
- // and doesn't compile as a map literal's keys — route these through
834
+ // that didn't synthesize a named struct a synthesized-name
835
+ // collision, #2674's graceful fallback) resolves to
836
+ // `[]map[string]interface{}`, not a named struct.
837
+ // `goTypedSliceLiteralFromArray`'s `goStructLiteral` emits bare
838
+ // `Field: value` entries, which is struct-literal syntax and
839
+ // doesn't compile as a map literal's keys — route these through
743
840
  // the map-literal builder instead (#2075, search-params-derived-filter).
744
841
  sliceLiteral = goTypedMapSliceLiteralFromArray(value, elemType)
745
842
  } else if (elemType) {
746
- sliceLiteral = goTypedSliceLiteralFromArray(value, elemType)
843
+ sliceLiteral = goTypedSliceLiteralFromArray(value, elemType, goTypes)
747
844
  } else {
748
845
  sliceLiteral = goArrayLiteralFromArray(value)
749
846
  }
750
847
  lines.push(`\t\t${goField}: ${sliceLiteral},`)
751
848
  } else if (value && typeof value === 'object') {
752
- // Plain object → Go `map[string]any` literal (#1407 follow-up).
753
- // Used by `jsx-spread-rest-prop` to populate the input-bag
754
- // Spread_<N> field that carries the destructured-rest payload.
755
- // The same harness change is needed when any future fixture
756
- // passes a `Record<string, unknown>`-shaped prop through.
757
- lines.push(`\t\t${goField}: ${goMapLiteralFromObject(value as Record<string, unknown>)},`)
849
+ // Plain object → Go `map[string]any` literal (#1407 follow-up), UNLESS
850
+ // the Input field is itself a synthesized named struct (#2674 — an
851
+ // inline object-typed prop, e.g. `cfg: { id: number; label?: string
852
+ // }`, now resolves its OWN concrete struct instead of falling to
853
+ // `map[string]interface{}`) a `map[string]any{…}` literal doesn't
854
+ // compile against a concretely-typed struct field.
855
+ const fieldGoType = parseGoStructFields(goTypes, `${componentName}Input`)?.get(goField)
856
+ if (fieldGoType && fieldGoType !== 'interface{}' && fieldGoType !== 'any' && !fieldGoType.startsWith('map[')) {
857
+ lines.push(`\t\t${goField}: ${goStructLiteral(value as Record<string, unknown>, fieldGoType, goTypes)},`)
858
+ } else {
859
+ // Used by `jsx-spread-rest-prop` to populate the input-bag
860
+ // Spread_<N> field that carries the destructured-rest payload.
861
+ // The same harness change is needed when any future fixture
862
+ // passes a `Record<string, unknown>`-shaped prop through.
863
+ lines.push(`\t\t${goField}: ${goMapLiteralFromObject(value as Record<string, unknown>)},`)
864
+ }
758
865
  }
759
866
  }
760
867
  // Emit the collected rest-bag entries as the open-ended bag field. Skip
@@ -799,12 +906,37 @@ function goSliceElemType(
799
906
  * Emit a typed Go slice literal (`[]Elem{Elem{…}, …}`). Object elements become
800
907
  * keyed struct literals with PascalCase field names; scalar elements (for an
801
908
  * `[]string` / `[]int` field) are emitted bare. (#1297, toggle-shared)
909
+ *
910
+ * `goTypes` (when supplied) threads through to `goStructLiteral` so it can
911
+ * look up each of ITS OWN fields' declared Go types — needed since #2674:
912
+ * `elemType` may now be a synthesized struct (`TaggedListItemsItem`) with a
913
+ * concretely-typed slice/nested-struct field (`Tags []string`), and a blind
914
+ * `[]any{…}`/`map[string]interface{}{…}` for that field no longer compiles
915
+ * against it the way it always did against the old `map[string]interface{}`
916
+ * element type.
917
+ *
918
+ * An ARRAY element (`elemType` itself starting with `[]`, i.e. the field is
919
+ * doubly-nested — `Rows [][]int`) recurses with the INNER element type
920
+ * (`int`) instead of falling to `goArrayLiteralFromArray`'s generic
921
+ * `[]any{…}` — #2677 widened the analyzer's destructured-parameter gate to
922
+ * resolve a nested-array prop (`{ rows }: { rows: number[][] }`) to a real
923
+ * `[][]int` field (previously `unknown` → `interface{}` → `[]any`, which
924
+ * `goArrayLiteralFromArray`'s untyped literal always compiled against fine).
925
+ * Without this, the harness's OWN convenience literal-builder — not the
926
+ * production adapter's `typeInfoToGo`, which already recurses correctly —
927
+ * bakes `[]any{1, 2}` for a `[1, 2]` row, and `[][]int{[]any{1, 2}, …}`
928
+ * fails to compile against the now-concrete field.
802
929
  */
803
- function goTypedSliceLiteralFromArray(arr: unknown[], elemType: string): string {
930
+ function goTypedSliceLiteralFromArray(arr: unknown[], elemType: string, goTypes?: string): string {
804
931
  const entries = arr.map(v => {
805
932
  if (v instanceof Date) return goStringLit(v.toISOString())
806
- if (v && typeof v === 'object' && !Array.isArray(v)) {
807
- return goStructLiteral(v as Record<string, unknown>, elemType)
933
+ if (Array.isArray(v)) {
934
+ return elemType.startsWith('[]')
935
+ ? goTypedSliceLiteralFromArray(v, elemType.slice(2), goTypes)
936
+ : goArrayLiteralFromArray(v)
937
+ }
938
+ if (v && typeof v === 'object') {
939
+ return goStructLiteral(v as Record<string, unknown>, elemType, goTypes)
808
940
  }
809
941
  if (typeof v === 'string') return `"${v.replace(/"/g, '\\"')}"`
810
942
  if (typeof v === 'number' || typeof v === 'boolean') return String(v)
@@ -839,11 +971,6 @@ function goTypedMapSliceLiteralFromArray(arr: unknown[], elemType: string): stri
839
971
  return `[]${elemType}{${entries.join(', ')}}`
840
972
  }
841
973
 
842
- /**
843
- * Emit a keyed Go struct literal (`Elem{Field: val, …}`) with PascalCase field
844
- * names. Only the keys the caller supplied are set, so an omitted optional prop
845
- * (e.g. `defaultOn` on the third toggle item) takes the Go zero value. (#1297)
846
- */
847
974
  /**
848
975
  * Emit a JS string as a Go interpreted string literal. JSON string
849
976
  * escaping is a subset of Go's (`\"`, `\\`, `\n`, `\uXXXX` are all valid
@@ -858,7 +985,71 @@ function goStringLit(v: string): string {
858
985
  return JSON.stringify(v)
859
986
  }
860
987
 
861
- function goStructLiteral(obj: Record<string, unknown>, typeName: string): string {
988
+ /**
989
+ * #2674: parse a named Go struct's OWN field → declared-type map out of
990
+ * `goTypes` (the generated `types.go` text), by struct name — the SAME
991
+ * text-scrape strategy `goSliceElemType` uses for `<Component>Input`,
992
+ * generalized to ANY struct name (a synthesized element type, `Row`, a
993
+ * loop-body wrapper, …) so `goStructLiteral` can bake each of ITS fields
994
+ * against what the struct ACTUALLY declares instead of guessing generically.
995
+ * Returns `null` when `goTypes` is absent or the struct isn't found — callers
996
+ * degrade to the pre-#2674 generic (`[]any` / `map[string]interface{}`)
997
+ * baking for every field, same as before this existed.
998
+ */
999
+ function parseGoStructFields(goTypes: string | undefined, typeName: string): Map<string, string> | null {
1000
+ if (!goTypes) return null
1001
+ const struct = goTypes.match(new RegExp(`type ${typeName} struct \\{([\\s\\S]*?)\\n\\}`))
1002
+ if (!struct) return null
1003
+ const fields = new Map<string, string>()
1004
+ // Field lines are always `\t<GoName> <GoType>[ \`json:"..."\`][ // comment]`
1005
+ // — GoType is one whitespace-free token in every shape this harness bakes
1006
+ // (`string`, `[]string`, `map[string]interface{}`, `[]TaggedListItemsItem`,
1007
+ // `interface{}`); a pointer-typed framework field (`*bf.ScriptCollector`)
1008
+ // doesn't match this class and is skipped — this harness never bakes a
1009
+ // VALUE for one of those.
1010
+ const fieldRe = /\n[ \t]*(\w+)[ \t]+([\w.[\]{}]+)/g
1011
+ let m: RegExpExecArray | null
1012
+ while ((m = fieldRe.exec(struct[1])) !== null) {
1013
+ fields.set(m[1], m[2])
1014
+ }
1015
+ return fields
1016
+ }
1017
+
1018
+ /**
1019
+ * Emit a typed Go scalar slice literal (`[]string{"a", "b"}`) for a struct
1020
+ * field whose declared element type is a Go scalar (#2674 — `Tags []string`
1021
+ * on a synthesized element struct). Falls back to `nil` for a shape that
1022
+ * shouldn't reach a scalar-typed field (an object/array value) — dead in
1023
+ * practice since the caller only routes here for an `Array.isArray(v)` prop
1024
+ * value.
1025
+ */
1026
+ function goScalarSliceLiteral(arr: unknown[], elemGoType: string): string {
1027
+ const entries = arr.map(v => {
1028
+ if (typeof v === 'string') return goStringLit(v)
1029
+ if (typeof v === 'number' || typeof v === 'boolean') return String(v)
1030
+ if (v === null) return 'nil'
1031
+ if (v instanceof Date) return goStringLit(v.toISOString())
1032
+ return 'nil'
1033
+ })
1034
+ return `[]${elemGoType}{${entries.join(', ')}}`
1035
+ }
1036
+
1037
+ /**
1038
+ * Emit a keyed Go struct literal (`Elem{Field: val, …}`) with PascalCase field
1039
+ * names. Only the keys the caller supplied are set, so an omitted optional prop
1040
+ * (e.g. `defaultOn` on the third toggle item) takes the Go zero value. (#1297)
1041
+ *
1042
+ * #2674: when `goTypes` is supplied, each field's VALUE bakes against what
1043
+ * `typeName` ACTUALLY declares for that field (via `parseGoStructFields`) —
1044
+ * a synthesized element struct can now carry a concretely-typed nested slice
1045
+ * (`Tags []string`) or nested struct (`RowUser`), not just scalars, and a
1046
+ * blind `[]any{…}` / `map[string]interface{}{…}` (this function's pre-#2674
1047
+ * behavior, still the fallback when `goTypes` is absent or the field is
1048
+ * unresolved/genuinely `interface{}`/`map[string]interface{}`-typed) no
1049
+ * longer compiles against those.
1050
+ */
1051
+ function goStructLiteral(obj: Record<string, unknown>, typeName: string, goTypes?: string): string {
1052
+ const fieldTypes = parseGoStructFields(goTypes, typeName)
862
1053
  const fields: string[] = []
863
1054
  for (const [k, v] of Object.entries(obj)) {
864
1055
  // `goFieldNameForKey`, not the bare `capitalizeFieldName` — a data-driven
@@ -866,12 +1057,42 @@ function goStructLiteral(obj: Record<string, unknown>, typeName: string): string
866
1057
  // adapter's own struct-literal baking (`parsed-literal-to-go.ts`)
867
1058
  // sanitizes those to `DataX`, not `Data-x` (Copilot review, #2202).
868
1059
  const goField = goFieldNameForKey(k)
1060
+ const fieldGoType = fieldTypes?.get(goField)
869
1061
  if (typeof v === 'string') fields.push(`${goField}: ${goStringLit(v)}`)
870
1062
  else if (typeof v === 'number' || typeof v === 'boolean') fields.push(`${goField}: ${v}`)
871
1063
  else if (v === null) fields.push(`${goField}: nil`)
872
1064
  else if (v instanceof Date) fields.push(`${goField}: ${goStringLit(v.toISOString())}`)
873
- else if (Array.isArray(v)) fields.push(`${goField}: ${goArrayLiteralFromArray(v)}`)
874
- else if (v && typeof v === 'object') fields.push(`${goField}: ${goMapLiteralFromObject(v as Record<string, unknown>, true)}`)
1065
+ else if (Array.isArray(v)) {
1066
+ const elemGoType = fieldGoType?.startsWith('[]') ? fieldGoType.slice(2) : null
1067
+ if (elemGoType === 'interface{}' || elemGoType === 'any') {
1068
+ fields.push(`${goField}: ${goArrayLiteralFromArray(v)}`)
1069
+ } else if (elemGoType?.startsWith('map[')) {
1070
+ fields.push(`${goField}: ${goTypedMapSliceLiteralFromArray(v, elemGoType)}`)
1071
+ } else if (elemGoType && (v.length === 0 || typeof v[0] !== 'object')) {
1072
+ // A scalar-element slice field (`Tags []string`) — bake a typed
1073
+ // scalar literal, not the generic `[]any` `goArrayLiteralFromArray`
1074
+ // would emit (doesn't compile against a concrete `[]string` field).
1075
+ fields.push(`${goField}: ${goScalarSliceLiteral(v, elemGoType)}`)
1076
+ } else if (elemGoType) {
1077
+ // A struct-element slice field — recurse with the SAME `goTypes` so
1078
+ // nesting keeps resolving (a synthesized type nested inside another
1079
+ // synthesized type, #2674's `RowUserAddress`-style chaining).
1080
+ fields.push(`${goField}: ${goTypedSliceLiteralFromArray(v, elemGoType, goTypes)}`)
1081
+ } else {
1082
+ fields.push(`${goField}: ${goArrayLiteralFromArray(v)}`)
1083
+ }
1084
+ }
1085
+ else if (v && typeof v === 'object') {
1086
+ if (fieldGoType && fieldGoType !== 'interface{}' && !fieldGoType.startsWith('map[')) {
1087
+ // A nested named-struct field (#2674 — `RowUser`, `Row.user`'s
1088
+ // synthesized type): recurse as a struct literal, not a map — a
1089
+ // `map[string]interface{}{…}` literal doesn't compile against a
1090
+ // concretely-typed struct field.
1091
+ fields.push(`${goField}: ${goStructLiteral(v as Record<string, unknown>, fieldGoType, goTypes)}`)
1092
+ } else {
1093
+ fields.push(`${goField}: ${goMapLiteralFromObject(v as Record<string, unknown>, true)}`)
1094
+ }
1095
+ }
875
1096
  }
876
1097
  return `${typeName}{${fields.join(', ')}}`
877
1098
  }