@barefootjs/go-template 0.31.7 → 0.31.9
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 +63 -0
- package/dist/adapter/go-template-adapter.d.ts.map +1 -1
- package/dist/adapter/index.js +76 -8
- package/dist/adapter/lib/compile-state.d.ts +15 -0
- 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 +77 -11
- 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 +104 -8
- package/package.json +5 -5
- package/src/__tests__/go-template-adapter.test.ts +178 -15
- package/src/adapter/go-template-adapter.ts +172 -2
- package/src/adapter/lib/compile-state.ts +16 -0
- package/src/adapter/props/prop-types.ts +29 -7
- package/src/adapter/type/type-codegen.ts +19 -2
- package/src/render-divergences.ts +9 -7
- package/src/test-render.ts +255 -51
|
@@ -14,8 +14,31 @@ import { typeInfoToGo } from '../type/type-codegen.ts'
|
|
|
14
14
|
/**
|
|
15
15
|
* Build a map from prop name to a better Go type inferred from signals. When a
|
|
16
16
|
* signal is initialized from a prop (`createSignal(props.initial ?? 0)`), the
|
|
17
|
-
* signal's type annotation may be more specific than the prop's `TypeInfo`.
|
|
18
|
-
*
|
|
17
|
+
* signal's type annotation may be more specific than the prop's `TypeInfo`.
|
|
18
|
+
*
|
|
19
|
+
* Two trigger conditions, mirroring `emitPropsDataFields`'s identical
|
|
20
|
+
* signal-vs-prop reconciliation for the PROPS struct field (`generate - Go
|
|
21
|
+
* struct types` — "Let a specific signal type override a less-specific prop
|
|
22
|
+
* type in either direction"), so the Input/Props field type this override
|
|
23
|
+
* feeds (via `resolvePropGoType`) and the constructor's baked VALUE
|
|
24
|
+
* (`convertInitialValue`'s `extractPropNameFromInitialValue` prop-passthrough
|
|
25
|
+
* shortcut) never disagree about which type is authoritative:
|
|
26
|
+
*
|
|
27
|
+
* 1. A generic prop type (containing `interface{}`) — the historical case.
|
|
28
|
+
* 2. #2674: BOTH sides resolve to a CONCRETE type that DISAGREES — e.g. an
|
|
29
|
+
* inline array-element prop type (`initialTodos: Array<{ id, text,
|
|
30
|
+
* done }>`) now independently synthesizes its OWN named struct
|
|
31
|
+
* (`TodoAppInitialTodosItem`) rather than falling to `interface{}`, but
|
|
32
|
+
* a signal seeded from it via a shape-widening transform
|
|
33
|
+
* (`createSignal<Todo[]>((props.initialTodos ?? []).map(t => ({...t,
|
|
34
|
+
* editing: false})))`, `Todo` carrying an EXTRA `editing` field) still
|
|
35
|
+
* wants the signal's own `Todo` element type. Before #2674 this case
|
|
36
|
+
* was unreachable — every inline object/array prop type WAS
|
|
37
|
+
* `interface{}`-containing, so case 1 always fired — leaving the
|
|
38
|
+
* passthrough shortcut safe by accident (prop and signal Go types were
|
|
39
|
+
* always forced equal). Widening to case 2 restores that same
|
|
40
|
+
* equality now that a prop's own type can independently resolve to
|
|
41
|
+
* something concrete but narrower.
|
|
19
42
|
*/
|
|
20
43
|
export function buildPropTypeOverrides(ctx: GoEmitContext, ir: ComponentIR): Map<string, string> {
|
|
21
44
|
const overrides = new Map<string, string>()
|
|
@@ -28,11 +51,10 @@ export function buildPropTypeOverrides(ctx: GoEmitContext, ir: ComponentIR): Map
|
|
|
28
51
|
const param = ir.metadata.propsParams.find(p => p.name === propName)
|
|
29
52
|
if (!param) continue
|
|
30
53
|
const propGoType = typeInfoToGo(ctx, param.type, param.defaultValue, param.parsed)
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
}
|
|
54
|
+
const signalGoType = typeInfoToGo(ctx, signal.type, signal.initialValue, signal.parsed)
|
|
55
|
+
if (signalGoType.includes('interface{}')) continue // never widen TO something less resolved
|
|
56
|
+
if (propGoType.includes('interface{}') || signalGoType !== propGoType) {
|
|
57
|
+
overrides.set(propName, signalGoType)
|
|
36
58
|
}
|
|
37
59
|
}
|
|
38
60
|
}
|
|
@@ -141,8 +141,25 @@ export function typeInfoToGo(
|
|
|
141
141
|
return `[]${typeInfoToGo(ctx, typeInfo.elementType)}`
|
|
142
142
|
}
|
|
143
143
|
return '[]interface{}'
|
|
144
|
-
case 'object':
|
|
145
|
-
|
|
144
|
+
case 'object': {
|
|
145
|
+
// #2674 Plan A: an ANONYMOUS object type — an inline array-element
|
|
146
|
+
// type (`items: { id: number }[]`) or a nested anonymous property
|
|
147
|
+
// inside a named type (`Row.user`) — synthesizes a deterministically-
|
|
148
|
+
// named, json-tagged struct in `emitSynthPropStructs`, registered here
|
|
149
|
+
// by the exact `TypeInfo` object's IDENTITY (not name/shape — two
|
|
150
|
+
// anonymous types at different structural positions get different
|
|
151
|
+
// synthesized names even when shaped identically). That pre-pass runs
|
|
152
|
+
// before this function is ever consulted for the current compile (see
|
|
153
|
+
// `generateTypes()`'s ordering), so a hit here means a real struct was
|
|
154
|
+
// emitted; a miss (no pre-pass ran, or the pre-pass skipped this exact
|
|
155
|
+
// type on a synthesized-name collision) falls back to the historical
|
|
156
|
+
// `map[string]interface{}` — the same PascalCase-baked map
|
|
157
|
+
// `bakeInlineObjectAsGoMap` (`parsed-literal-to-go.ts`) still targets,
|
|
158
|
+
// so SSR stays correct and only the hydration-payload leak the
|
|
159
|
+
// synthesis closes remains open for that one skipped type.
|
|
160
|
+
const synthName = ctx.state.synthObjectStructNames.get(typeInfo)
|
|
161
|
+
return synthName ?? 'map[string]interface{}'
|
|
162
|
+
}
|
|
146
163
|
case 'interface':
|
|
147
164
|
// Gate on an ACTUAL backing (a generated struct — `localStructFields` —
|
|
148
165
|
// or a string-union alias — `localTypeAliases`, which emits `type X =
|
|
@@ -5,14 +5,16 @@
|
|
|
5
5
|
* one object, so the skip list and the declaration can't drift. Keep the
|
|
6
6
|
* file even when the set is empty — the next divergence lands here, not in
|
|
7
7
|
* a re-created file.
|
|
8
|
+
*
|
|
9
|
+
* Empty — #2630's `static-array-from-props-with-component-precomputed`
|
|
10
|
+
* divergence graduated once the harness (`test-render.ts`'s
|
|
11
|
+
* `buildDynamicChildLoopSeeding`, despite the name — see its doc comment)
|
|
12
|
+
* learned to seed a prop-backed static child-component loop's Props slice
|
|
13
|
+
* the same way it already seeded a signal-backed dynamic one: the adapter's
|
|
14
|
+
* own `emission` was never the bug, only this harness's route-handler
|
|
15
|
+
* stand-in was missing the prop-derived case.
|
|
8
16
|
*/
|
|
9
17
|
|
|
10
18
|
import type { RenderDivergences } from '@barefootjs/jsx'
|
|
11
19
|
|
|
12
|
-
export const renderDivergences: RenderDivergences = {
|
|
13
|
-
// #2630: the exact shape BF101's pass-as-prop suggestion (#2321) steers
|
|
14
|
-
// go-template users into — compiles clean, runs clean, silently renders the
|
|
15
|
-
// loop host empty. Full analysis in the issue.
|
|
16
|
-
'static-array-from-props-with-component-precomputed':
|
|
17
|
-
'prop-backed child-component loop renders the loop host empty at SSR (https://github.com/piconic-ai/barefootjs/issues/2630)',
|
|
18
|
-
}
|
|
20
|
+
export const renderDivergences: RenderDivergences = {}
|
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,20 @@ 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.
|
|
802
917
|
*/
|
|
803
|
-
function goTypedSliceLiteralFromArray(arr: unknown[], elemType: string): string {
|
|
918
|
+
function goTypedSliceLiteralFromArray(arr: unknown[], elemType: string, goTypes?: string): string {
|
|
804
919
|
const entries = arr.map(v => {
|
|
805
920
|
if (v instanceof Date) return goStringLit(v.toISOString())
|
|
806
921
|
if (v && typeof v === 'object' && !Array.isArray(v)) {
|
|
807
|
-
return goStructLiteral(v as Record<string, unknown>, elemType)
|
|
922
|
+
return goStructLiteral(v as Record<string, unknown>, elemType, goTypes)
|
|
808
923
|
}
|
|
809
924
|
if (typeof v === 'string') return `"${v.replace(/"/g, '\\"')}"`
|
|
810
925
|
if (typeof v === 'number' || typeof v === 'boolean') return String(v)
|
|
@@ -839,11 +954,6 @@ function goTypedMapSliceLiteralFromArray(arr: unknown[], elemType: string): stri
|
|
|
839
954
|
return `[]${elemType}{${entries.join(', ')}}`
|
|
840
955
|
}
|
|
841
956
|
|
|
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
957
|
/**
|
|
848
958
|
* Emit a JS string as a Go interpreted string literal. JSON string
|
|
849
959
|
* escaping is a subset of Go's (`\"`, `\\`, `\n`, `\uXXXX` are all valid
|
|
@@ -858,7 +968,71 @@ function goStringLit(v: string): string {
|
|
|
858
968
|
return JSON.stringify(v)
|
|
859
969
|
}
|
|
860
970
|
|
|
861
|
-
|
|
971
|
+
/**
|
|
972
|
+
* #2674: parse a named Go struct's OWN field → declared-type map out of
|
|
973
|
+
* `goTypes` (the generated `types.go` text), by struct name — the SAME
|
|
974
|
+
* text-scrape strategy `goSliceElemType` uses for `<Component>Input`,
|
|
975
|
+
* generalized to ANY struct name (a synthesized element type, `Row`, a
|
|
976
|
+
* loop-body wrapper, …) so `goStructLiteral` can bake each of ITS fields
|
|
977
|
+
* against what the struct ACTUALLY declares instead of guessing generically.
|
|
978
|
+
* Returns `null` when `goTypes` is absent or the struct isn't found — callers
|
|
979
|
+
* degrade to the pre-#2674 generic (`[]any` / `map[string]interface{}`)
|
|
980
|
+
* baking for every field, same as before this existed.
|
|
981
|
+
*/
|
|
982
|
+
function parseGoStructFields(goTypes: string | undefined, typeName: string): Map<string, string> | null {
|
|
983
|
+
if (!goTypes) return null
|
|
984
|
+
const struct = goTypes.match(new RegExp(`type ${typeName} struct \\{([\\s\\S]*?)\\n\\}`))
|
|
985
|
+
if (!struct) return null
|
|
986
|
+
const fields = new Map<string, string>()
|
|
987
|
+
// Field lines are always `\t<GoName> <GoType>[ \`json:"..."\`][ // comment]`
|
|
988
|
+
// — GoType is one whitespace-free token in every shape this harness bakes
|
|
989
|
+
// (`string`, `[]string`, `map[string]interface{}`, `[]TaggedListItemsItem`,
|
|
990
|
+
// `interface{}`); a pointer-typed framework field (`*bf.ScriptCollector`)
|
|
991
|
+
// doesn't match this class and is skipped — this harness never bakes a
|
|
992
|
+
// VALUE for one of those.
|
|
993
|
+
const fieldRe = /\n[ \t]*(\w+)[ \t]+([\w.[\]{}]+)/g
|
|
994
|
+
let m: RegExpExecArray | null
|
|
995
|
+
while ((m = fieldRe.exec(struct[1])) !== null) {
|
|
996
|
+
fields.set(m[1], m[2])
|
|
997
|
+
}
|
|
998
|
+
return fields
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
/**
|
|
1002
|
+
* Emit a typed Go scalar slice literal (`[]string{"a", "b"}`) for a struct
|
|
1003
|
+
* field whose declared element type is a Go scalar (#2674 — `Tags []string`
|
|
1004
|
+
* on a synthesized element struct). Falls back to `nil` for a shape that
|
|
1005
|
+
* shouldn't reach a scalar-typed field (an object/array value) — dead in
|
|
1006
|
+
* practice since the caller only routes here for an `Array.isArray(v)` prop
|
|
1007
|
+
* value.
|
|
1008
|
+
*/
|
|
1009
|
+
function goScalarSliceLiteral(arr: unknown[], elemGoType: string): string {
|
|
1010
|
+
const entries = arr.map(v => {
|
|
1011
|
+
if (typeof v === 'string') return goStringLit(v)
|
|
1012
|
+
if (typeof v === 'number' || typeof v === 'boolean') return String(v)
|
|
1013
|
+
if (v === null) return 'nil'
|
|
1014
|
+
if (v instanceof Date) return goStringLit(v.toISOString())
|
|
1015
|
+
return 'nil'
|
|
1016
|
+
})
|
|
1017
|
+
return `[]${elemGoType}{${entries.join(', ')}}`
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
/**
|
|
1021
|
+
* Emit a keyed Go struct literal (`Elem{Field: val, …}`) with PascalCase field
|
|
1022
|
+
* names. Only the keys the caller supplied are set, so an omitted optional prop
|
|
1023
|
+
* (e.g. `defaultOn` on the third toggle item) takes the Go zero value. (#1297)
|
|
1024
|
+
*
|
|
1025
|
+
* #2674: when `goTypes` is supplied, each field's VALUE bakes against what
|
|
1026
|
+
* `typeName` ACTUALLY declares for that field (via `parseGoStructFields`) —
|
|
1027
|
+
* a synthesized element struct can now carry a concretely-typed nested slice
|
|
1028
|
+
* (`Tags []string`) or nested struct (`RowUser`), not just scalars, and a
|
|
1029
|
+
* blind `[]any{…}` / `map[string]interface{}{…}` (this function's pre-#2674
|
|
1030
|
+
* behavior, still the fallback when `goTypes` is absent or the field is
|
|
1031
|
+
* unresolved/genuinely `interface{}`/`map[string]interface{}`-typed) no
|
|
1032
|
+
* longer compiles against those.
|
|
1033
|
+
*/
|
|
1034
|
+
function goStructLiteral(obj: Record<string, unknown>, typeName: string, goTypes?: string): string {
|
|
1035
|
+
const fieldTypes = parseGoStructFields(goTypes, typeName)
|
|
862
1036
|
const fields: string[] = []
|
|
863
1037
|
for (const [k, v] of Object.entries(obj)) {
|
|
864
1038
|
// `goFieldNameForKey`, not the bare `capitalizeFieldName` — a data-driven
|
|
@@ -866,12 +1040,42 @@ function goStructLiteral(obj: Record<string, unknown>, typeName: string): string
|
|
|
866
1040
|
// adapter's own struct-literal baking (`parsed-literal-to-go.ts`)
|
|
867
1041
|
// sanitizes those to `DataX`, not `Data-x` (Copilot review, #2202).
|
|
868
1042
|
const goField = goFieldNameForKey(k)
|
|
1043
|
+
const fieldGoType = fieldTypes?.get(goField)
|
|
869
1044
|
if (typeof v === 'string') fields.push(`${goField}: ${goStringLit(v)}`)
|
|
870
1045
|
else if (typeof v === 'number' || typeof v === 'boolean') fields.push(`${goField}: ${v}`)
|
|
871
1046
|
else if (v === null) fields.push(`${goField}: nil`)
|
|
872
1047
|
else if (v instanceof Date) fields.push(`${goField}: ${goStringLit(v.toISOString())}`)
|
|
873
|
-
else if (Array.isArray(v))
|
|
874
|
-
|
|
1048
|
+
else if (Array.isArray(v)) {
|
|
1049
|
+
const elemGoType = fieldGoType?.startsWith('[]') ? fieldGoType.slice(2) : null
|
|
1050
|
+
if (elemGoType === 'interface{}' || elemGoType === 'any') {
|
|
1051
|
+
fields.push(`${goField}: ${goArrayLiteralFromArray(v)}`)
|
|
1052
|
+
} else if (elemGoType?.startsWith('map[')) {
|
|
1053
|
+
fields.push(`${goField}: ${goTypedMapSliceLiteralFromArray(v, elemGoType)}`)
|
|
1054
|
+
} else if (elemGoType && (v.length === 0 || typeof v[0] !== 'object')) {
|
|
1055
|
+
// A scalar-element slice field (`Tags []string`) — bake a typed
|
|
1056
|
+
// scalar literal, not the generic `[]any` `goArrayLiteralFromArray`
|
|
1057
|
+
// would emit (doesn't compile against a concrete `[]string` field).
|
|
1058
|
+
fields.push(`${goField}: ${goScalarSliceLiteral(v, elemGoType)}`)
|
|
1059
|
+
} else if (elemGoType) {
|
|
1060
|
+
// A struct-element slice field — recurse with the SAME `goTypes` so
|
|
1061
|
+
// nesting keeps resolving (a synthesized type nested inside another
|
|
1062
|
+
// synthesized type, #2674's `RowUserAddress`-style chaining).
|
|
1063
|
+
fields.push(`${goField}: ${goTypedSliceLiteralFromArray(v, elemGoType, goTypes)}`)
|
|
1064
|
+
} else {
|
|
1065
|
+
fields.push(`${goField}: ${goArrayLiteralFromArray(v)}`)
|
|
1066
|
+
}
|
|
1067
|
+
}
|
|
1068
|
+
else if (v && typeof v === 'object') {
|
|
1069
|
+
if (fieldGoType && fieldGoType !== 'interface{}' && !fieldGoType.startsWith('map[')) {
|
|
1070
|
+
// A nested named-struct field (#2674 — `RowUser`, `Row.user`'s
|
|
1071
|
+
// synthesized type): recurse as a struct literal, not a map — a
|
|
1072
|
+
// `map[string]interface{}{…}` literal doesn't compile against a
|
|
1073
|
+
// concretely-typed struct field.
|
|
1074
|
+
fields.push(`${goField}: ${goStructLiteral(v as Record<string, unknown>, fieldGoType, goTypes)}`)
|
|
1075
|
+
} else {
|
|
1076
|
+
fields.push(`${goField}: ${goMapLiteralFromObject(v as Record<string, unknown>, true)}`)
|
|
1077
|
+
}
|
|
1078
|
+
}
|
|
875
1079
|
}
|
|
876
1080
|
return `${typeName}{${fields.join(', ')}}`
|
|
877
1081
|
}
|