@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.
- package/dist/adapter/go-template-adapter.d.ts +71 -0
- package/dist/adapter/go-template-adapter.d.ts.map +1 -1
- package/dist/adapter/index.js +97 -15
- package/dist/adapter/lib/compile-state.d.ts +43 -1
- package/dist/adapter/lib/compile-state.d.ts.map +1 -1
- package/dist/adapter/props/prop-types.d.ts +25 -2
- package/dist/adapter/props/prop-types.d.ts.map +1 -1
- package/dist/adapter/type/type-codegen.d.ts.map +1 -1
- package/dist/index.js +99 -16
- package/dist/render-divergences.d.ts +8 -0
- package/dist/render-divergences.d.ts.map +1 -1
- package/dist/test-render.d.ts.map +1 -1
- package/dist/vite.js +160 -17
- package/package.json +5 -5
- package/src/__tests__/go-template-adapter.test.ts +276 -16
- package/src/adapter/go-template-adapter.ts +249 -11
- package/src/adapter/lib/compile-state.ts +50 -0
- package/src/adapter/props/prop-types.ts +36 -8
- package/src/adapter/type/type-codegen.ts +19 -2
- package/src/render-divergences.ts +31 -5
- package/src/test-render.ts +273 -52
package/src/test-render.ts
CHANGED
|
@@ -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
|
-
* (#
|
|
591
|
-
*
|
|
592
|
-
*
|
|
593
|
-
*
|
|
594
|
-
*
|
|
595
|
-
*
|
|
596
|
-
*
|
|
597
|
-
*
|
|
598
|
-
*
|
|
599
|
-
*
|
|
600
|
-
*
|
|
601
|
-
*
|
|
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
|
|
605
|
-
*
|
|
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
|
-
*
|
|
609
|
-
*
|
|
610
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
633
|
-
prop.value.kind === 'expression' &&
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
) {
|
|
637
|
-
|
|
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
|
|
738
|
-
//
|
|
739
|
-
//
|
|
740
|
-
//
|
|
741
|
-
//
|
|
742
|
-
//
|
|
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
|
-
//
|
|
754
|
-
//
|
|
755
|
-
//
|
|
756
|
-
//
|
|
757
|
-
|
|
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 (
|
|
807
|
-
return
|
|
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
|
-
|
|
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))
|
|
874
|
-
|
|
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
|
}
|