@barefootjs/go-template 0.31.1 → 0.31.2

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.
Files changed (35) hide show
  1. package/dist/adapter/go-template-adapter.d.ts +49 -41
  2. package/dist/adapter/go-template-adapter.d.ts.map +1 -1
  3. package/dist/adapter/index.js +167 -101
  4. package/dist/adapter/lib/types.d.ts +4 -1
  5. package/dist/adapter/lib/types.d.ts.map +1 -1
  6. package/dist/adapter/memo/memo-compute.d.ts +9 -0
  7. package/dist/adapter/memo/memo-compute.d.ts.map +1 -1
  8. package/dist/adapter/memo/memo-type.d.ts +2 -0
  9. package/dist/adapter/memo/memo-type.d.ts.map +1 -1
  10. package/dist/adapter/props/prop-types.d.ts +2 -1
  11. package/dist/adapter/props/prop-types.d.ts.map +1 -1
  12. package/dist/adapter/spread/spread-codegen.d.ts.map +1 -1
  13. package/dist/adapter/type/type-codegen.d.ts +22 -5
  14. package/dist/adapter/type/type-codegen.d.ts.map +1 -1
  15. package/dist/adapter/value/parsed-literal-to-go.d.ts +12 -0
  16. package/dist/adapter/value/parsed-literal-to-go.d.ts.map +1 -1
  17. package/dist/adapter/value/value-lowering.d.ts +3 -0
  18. package/dist/adapter/value/value-lowering.d.ts.map +1 -1
  19. package/dist/index.js +168 -104
  20. package/dist/render-divergences.d.ts +6 -0
  21. package/dist/render-divergences.d.ts.map +1 -1
  22. package/dist/vite.js +493 -192
  23. package/package.json +5 -5
  24. package/src/__tests__/go-template-adapter.test.ts +155 -10
  25. package/src/adapter/go-template-adapter.ts +213 -128
  26. package/src/adapter/lib/types.ts +4 -1
  27. package/src/adapter/memo/memo-compute.ts +21 -17
  28. package/src/adapter/memo/memo-type.ts +5 -5
  29. package/src/adapter/props/prop-types.ts +6 -5
  30. package/src/adapter/spread/spread-codegen.ts +12 -5
  31. package/src/adapter/type/type-codegen.ts +86 -25
  32. package/src/adapter/value/parsed-literal-to-go.ts +28 -10
  33. package/src/adapter/value/value-lowering.ts +65 -29
  34. package/src/render-divergences.ts +6 -15
  35. package/src/test-render.ts +6 -1
@@ -158,17 +158,12 @@ type HigherOrderShape = {
158
158
  /**
159
159
  * Everything needed to emit a component's #2448 props rebuilder, captured when
160
160
  * its own types are generated so a PARENT can emit the registration into its
161
- * own type block (`emitOwnedReprops`). `params` are this component's own
162
- * Input fields since #2457, the PARENT (`loopRowChildPropOverrides`, via
163
- * `childPropFieldNames`) already resolves the JSX attribute name to the
164
- * child's own field name before emitting the call, so the switch this spec
165
- * drives is keyed by that one name on both sides; there is no longer a
166
- * separate "wire" name to carry (an aliased destructure used to make the
167
- * parent's JSX-attribute name and the child's field name differ — `"N"` vs
168
- * `Count` — and this spec used to carry both).
161
+ * own type block (`emitOwnedReprops`). `propsField` (LOCAL) and `inputField`
162
+ * (caller-facing) diverge only for an aliased destructured prop — one name is
163
+ * not enough (see `emitRepropsRegistration`).
169
164
  */
170
165
  type RepropsSpec = {
171
- params: string[]
166
+ params: { propsField: string; inputField: string }[]
172
167
  usesSearchParams: boolean
173
168
  }
174
169
 
@@ -688,35 +683,18 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
688
683
  private childDerivedFieldDeps: Map<string, Map<string, ReadonlySet<string>>> = new Map()
689
684
 
690
685
  /**
691
- * component name -> (the prop name as WRITTEN AT THE JSX CALL SITE -> that
692
- * component's own Go field name). #2457.
686
+ * component name -> (JSX-attribute name -> that component's own Props
687
+ * field, LOCAL binding). `bf.WithProps` treats an unknown field as a
688
+ * silent passthrough, so `bf_with_props`/`bf_reprops` call sites cannot
689
+ * just capitalize the JSX attribute: for `{ n: count }` that names a
690
+ * field (`N`) the Props struct doesn't have, dropping the override with
691
+ * no diagnostic. Input composite literals need no such lookup — Input
692
+ * fields are caller-facing, so `capitalizeFieldName(jsxName)` already
693
+ * names them (see `emitChildField`).
693
694
  *
694
- * `generateInputStruct` / `emitPropsDataFields` name a child's Go field from
695
- * its LOCAL binding (`capitalizeFieldName(param.name)`), but a parent
696
- * emitting a per-row override
697
- * (`loopRowChildPropOverrides`) only knows the JSX attribute it wrote. For
698
- * an un-aliased prop those are the same string, so this map is an identity
699
- * for every existing shape and every currently-passing fixture stays
700
- * byte-identical. For an ALIASED destructure (`{ n: count }`) they differ
701
- * (`"N"` vs `Count`) — `bf.WithProps` documents unknown-field pairs as a
702
- * silent passthrough, so emitting the JSX name against a struct that has no
703
- * such field drops the override with no diagnostic. Resolving through this
704
- * map once, at the parent's emission site (the only place that has both
705
- * the JSX name and the child's shape), replaces that silent drop with the
706
- * correct field.
707
- *
708
- * Key = `p.sourceName ?? p.name` (identity for an un-aliased param, the JSX
709
- * attribute name for an aliased one). Value = `capitalizeFieldName(p.name)`
710
- * — the child's own field, from its LOCAL binding, same as
711
- * `generateInputStruct` uses.
712
- *
713
- * Populated from the same two doors as `childDerivedFieldDeps` — inside
714
- * `recordDerivedFieldDeps`, unconditionally (before that method's own
715
- * `deps.size > 0` gate), so every component gets an entry here regardless
716
- * of whether it has a derived field. `recordRepropsSpec` (the other nearby
717
- * populator) returns early for components with no derived field or an
718
- * ineligible shape — this map must NOT inherit either gate, since an
719
- * aliased child with no derived field at all is exactly the #2457 shape.
695
+ * Populated inside `recordDerivedFieldDeps` BEFORE its `deps.size > 0`
696
+ * gate: an aliased child with no derived field at all is exactly the
697
+ * #2457 shape, so this map must not inherit `recordRepropsSpec`'s gates.
720
698
  */
721
699
  private childPropFieldNames: Map<string, Map<string, string>> = new Map()
722
700
 
@@ -798,9 +776,8 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
798
776
  )
799
777
  const canonical = (local: string): string => capitalizeFieldName(sourceOf.get(local) ?? local)
800
778
  const propFieldNames = new Set([...paramNames].map(canonical))
801
- // #2457: record the JSX-attribute-name -> child's-own-field-name map for
802
- // EVERY component, unconditionally — see `childPropFieldNames`'s
803
- // docstring for why this can't share `recordRepropsSpec`'s gates.
779
+ // Recorded for EVERY component this must not share `recordRepropsSpec`'s
780
+ // gates (see `childPropFieldNames`).
804
781
  const fieldNames = new Map<string, string>()
805
782
  for (const p of ir.metadata.propsParams ?? []) {
806
783
  fieldNames.set(p.sourceName ?? p.name, capitalizeFieldName(p.name))
@@ -944,6 +921,53 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
944
921
  return this.state.contextConsumers.filter(c => !taken.has(this.contextFieldName(c)))
945
922
  }
946
923
 
924
+ /**
925
+ * True when `param`'s prop is subsumed by a same-name nested-component
926
+ * array field (`<Row>` children collected into `Rows []RowProps`). Must
927
+ * check the LOCAL and caller-facing name together — Input keys its fields
928
+ * caller-facing while Props/NewProps key local, so a one-sided check lets
929
+ * the structs disagree on whether the field exists (a duplicate json tag,
930
+ * or a dead Input field whose caller writes are silently ignored).
931
+ */
932
+ private isNestedArrayShadowed(
933
+ param: { name: string; sourceName?: string },
934
+ nestedArrayFields: ReadonlySet<string>,
935
+ ): boolean {
936
+ return (
937
+ nestedArrayFields.has(capitalizeFieldName(param.name)) ||
938
+ nestedArrayFields.has(capitalizeFieldName(param.sourceName ?? param.name))
939
+ )
940
+ }
941
+
942
+ /**
943
+ * Every Go field name these props params could claim — LOCAL and
944
+ * caller-facing. A context-consumer field must exist in ALL of
945
+ * Input/Props/NewProps or none, and the three key prop fields under
946
+ * different namings, so their shared collision gate cannot check just one
947
+ * (`{ n: searchParams }` collides in Props but not Input).
948
+ */
949
+ private propParamFieldNamesUnion(params: ReadonlyArray<{ name: string; sourceName?: string }>): string[] {
950
+ return params.flatMap(p => [capitalizeFieldName(p.name), capitalizeFieldName(p.sourceName ?? p.name)])
951
+ }
952
+
953
+ /**
954
+ * Claim `desired` in the struct-wide set of emitted json tags, or fall
955
+ * back to `json:"-"` on collision. The identifier de-dup sets cannot cover
956
+ * this: an aliased prop's tag is caller-facing while other fields' tags
957
+ * stay local, so two DIFFERENT Go identifiers can want the same tag — and
958
+ * `encoding/json` silently drops every field sharing an ambiguous tag,
959
+ * losing the key from the bf-p payload entirely. Callers thread one set
960
+ * through the field-emitting loops in declaration order, so props win;
961
+ * that matches hono, whose hydration blob is built only from props params
962
+ * (a signal seeds by reading the prop's key, never a key of its own, so
963
+ * the dropped tag was never a client read path).
964
+ */
965
+ private claimJsonTag(desired: string, taken: Set<string>): string {
966
+ if (taken.has(desired)) return '-'
967
+ taken.add(desired)
968
+ return desired
969
+ }
970
+
947
971
  generateTypes(ir: ComponentIR): string | null {
948
972
  this.state.usesHtmlTemplate = false
949
973
  this.state.usesFmt = false
@@ -1004,8 +1028,12 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
1004
1028
  * Two invariants the emitted code depends on:
1005
1029
  *
1006
1030
  * - **Input is recoverable from Props.** `emitPropsDataFields` and
1007
- * `generateInputStruct` emit each props param with the SAME field name and
1008
- * the SAME `resolvePropGoType`, so `in.<F> = b.<F>` is exact. A prop read
1031
+ * `generateInputStruct` emit each props param with the SAME
1032
+ * `resolvePropGoType`, and the same field name EXCEPT for an aliased
1033
+ * destructured prop, where Props keeps the LOCAL field and Input keys by
1034
+ * the CALLER-facing name (a caller-side `BadgeInput{N: 5}` literal must
1035
+ * type-check). `in.<inputField> = b.<propsField>` bridges the pair
1036
+ * (identity when un-aliased). A prop read
1009
1037
  * only by a memo still gets its passthrough field, so nothing is lost.
1010
1038
  * Shapes that add Input fields with no Props counterpart (a rest bag, a
1011
1039
  * spread slot, context consumers, nested child Inputs) are declined —
@@ -1016,17 +1044,8 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
1016
1044
  * BfMount come from the base, and the Props-only fields (Scripts,
1017
1045
  * BfIsRoot/BfIsChild/BfDataKey) are reapplied after the constructor.
1018
1046
  *
1019
- * The override switch is keyed by the child's own Input field
1020
- * (`ParamInfo.name`) on both the case label and the assignment target. That
1021
- * used to differ under an aliased destructure — the case label carried the
1022
- * name the PARENT wrote (the JSX attribute, `ParamInfo.sourceName ?? name`)
1023
- * while the assignment targeted the child's own field, so this switch was
1024
- * where those two sides were reconciled. #2457 moved that reconciliation to
1025
- * the PARENT (`loopRowChildPropOverrides`, via `childPropFieldNames`): the
1026
- * parent now emits the child's own field name at the call site, so by the
1027
- * time a `bf_reprops` pipeline reaches this switch both sides already agree
1028
- * and there's exactly one place — the parent's emission — where the naming
1029
- * gets reconciled, instead of two.
1047
+ * The override switch built from `params` is emitted by
1048
+ * `emitRepropsRegistration`.
1030
1049
  */
1031
1050
  private recordRepropsSpec(
1032
1051
  ir: ComponentIR,
@@ -1036,11 +1055,14 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
1036
1055
  ): void {
1037
1056
  if (!this.childDerivedFieldDeps.has(componentName)) return
1038
1057
 
1058
+ // Must mirror `generateInputStruct`'s exclusion/collision checks exactly —
1059
+ // both walk the Input struct's actual (caller-keyed) field names, or the
1060
+ // reprops switch references fields the struct doesn't have.
1039
1061
  const nestedArrayFields = new Set(nestedComponents.map(n => `${n.name}s`))
1040
1062
  const params = (ir.metadata.propsParams ?? []).filter(
1041
- p => !nestedArrayFields.has(capitalizeFieldName(p.name)),
1063
+ p => !this.isNestedArrayShadowed(p, nestedArrayFields),
1042
1064
  )
1043
- const takenInput = new Set((ir.metadata.propsParams ?? []).map(p => capitalizeFieldName(p.name)))
1065
+ const takenInput = new Set(this.propParamFieldNamesUnion(ir.metadata.propsParams ?? []))
1044
1066
  const eligible =
1045
1067
  nestedComponents.every(n => n.isDynamic && !n.isPropDerived) &&
1046
1068
  spreadSlots.length === 0 &&
@@ -1049,7 +1071,10 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
1049
1071
  if (!eligible) return
1050
1072
 
1051
1073
  this.childRepropsReady.set(componentName, {
1052
- params: params.map(p => capitalizeFieldName(p.name)),
1074
+ params: params.map(p => ({
1075
+ propsField: capitalizeFieldName(p.name),
1076
+ inputField: capitalizeFieldName(p.sourceName ?? p.name),
1077
+ })),
1053
1078
  usesSearchParams: this.usesSearchParams(ir),
1054
1079
  })
1055
1080
  }
@@ -1108,22 +1133,22 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
1108
1133
  lines.push('\t\t\tBfParent: b.BfParent,')
1109
1134
  lines.push('\t\t\tBfMount: b.BfMount,')
1110
1135
  if (usesSearchParams) lines.push('\t\t\tSearchParams: b.SearchParams,')
1111
- for (const field of params) {
1112
- lines.push(`\t\t\t${field}: b.${field},`)
1136
+ for (const { propsField, inputField } of params) {
1137
+ lines.push(`\t\t\t${inputField}: b.${propsField},`)
1113
1138
  }
1114
1139
  lines.push('\t\t}')
1115
1140
  lines.push('\t\tfor i := 0; i < len(kv); i += 2 {')
1116
1141
  lines.push('\t\t\tname, _ := kv[i].(string)')
1117
1142
  lines.push('\t\t\tvar err error')
1118
1143
  lines.push('\t\t\tswitch name {')
1119
- for (const field of params) {
1120
- // Case label and assignment target are the same name: the child's own
1121
- // Input field. The parent (`loopRowChildPropOverrides`, via
1122
- // `childPropFieldNames`, #2457) already resolved the JSX attribute to
1123
- // this field before emitting the call, so there is nothing left to
1124
- // reconcile here.
1125
- lines.push(`\t\t\tcase ${JSON.stringify(field)}:`)
1126
- lines.push(`\t\t\t\terr = bf.RepropsAssign(${q}, ${JSON.stringify(field)}, &in.${field}, kv[i+1])`)
1144
+ for (const { propsField, inputField } of params) {
1145
+ // Case label = the child's own PROPS field (what the parent's
1146
+ // `bf_with_props`/`bf_reprops` call is keyed by); target = the Input
1147
+ // field. One name is not enough: an aliased destructure gives
1148
+ // `{ n: count }` Props `Count`, Input `N` (437f822 had unified the
1149
+ // pair while the two could never diverge).
1150
+ lines.push(`\t\t\tcase ${JSON.stringify(propsField)}:`)
1151
+ lines.push(`\t\t\t\terr = bf.RepropsAssign(${q}, ${JSON.stringify(propsField)}, &in.${inputField}, kv[i+1])`)
1127
1152
  }
1128
1153
  // No silent drop. `bf_with_props` passes an unknown field through because
1129
1154
  // a rest-bag prop legitimately has no named field — but a rebuilder is
@@ -1301,10 +1326,14 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
1301
1326
  */
1302
1327
  private usesSearchParams(ir: ComponentIR): boolean {
1303
1328
  if (this.state.searchParamsLocals.size === 0) return false
1304
- // Every other field-producing source: a `SearchParams` collision would
1305
- // redeclare the field and break the Go compile.
1329
+ // A `SearchParams` collision would redeclare the field and break the Go
1330
+ // compile. This ONE boolean gates the field in Input (caller-facing
1331
+ // names), Props (LOCAL names) and NewProps at once, so a prop cannot be
1332
+ // checked under just one naming: `{ x: searchParams }` collides only in
1333
+ // Props, `{ searchParams: x }` only in Input.
1306
1334
  const taken = new Set<string>([
1307
1335
  ...ir.metadata.propsParams.map(p => capitalizeFieldName(p.name)),
1336
+ ...ir.metadata.propsParams.map(p => capitalizeFieldName(p.sourceName ?? p.name)),
1308
1337
  // Env signals (`createSearchParams()`) don't produce a normal value field —
1309
1338
  // they ARE the `SearchParams` binding — so they must not poison this
1310
1339
  // collision set (a getter named `searchParams` would otherwise capitalise
@@ -1351,8 +1380,14 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
1351
1380
  const nestedArrayFields = new Set(nestedComponents.map(n => `${n.name}s`))
1352
1381
 
1353
1382
  for (const param of ir.metadata.propsParams) {
1354
- const fieldName = capitalizeFieldName(param.name)
1355
- if (nestedArrayFields.has(fieldName)) continue
1383
+ // #2525: caller-facing name, not the local destructure binding — a
1384
+ // caller-side composite `BadgeInput{N: 5}` literal is written against
1385
+ // the name the callee's `{ n: count }` destructure was GIVEN, not the
1386
+ // name it renamed to internally. `generatePropsStruct`'s field (what
1387
+ // `{{.X}}` executes against) stays keyed by the local binding — see
1388
+ // `emitPropsDataFields`.
1389
+ const fieldName = capitalizeFieldName(param.sourceName ?? param.name)
1390
+ if (this.isNestedArrayShadowed(param, nestedArrayFields)) continue
1356
1391
  const goType = resolvePropGoType(this.emitCtx, param, propTypeOverrides)
1357
1392
  lines.push(`\t${fieldName} ${goType}`)
1358
1393
  }
@@ -1374,8 +1409,10 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
1374
1409
  }
1375
1410
 
1376
1411
  // `useContext` consumer fields — settable by an enclosing provider; default
1377
- // applied in NewXxxProps.
1378
- const takenInput = new Set(ir.metadata.propsParams.map(p => capitalizeFieldName(p.name)))
1412
+ // applied in NewXxxProps. `taken` must union the local and caller-facing
1413
+ // prop names (`propParamFieldNamesUnion`) or Input disagrees with
1414
+ // Props/NewProps on whether the consumer field exists.
1415
+ const takenInput = new Set(this.propParamFieldNamesUnion(ir.metadata.propsParams))
1379
1416
  for (const c of this.nonCollidingContextConsumers(takenInput)) {
1380
1417
  lines.push(`\t${this.contextFieldName(c)} ${this.contextConsumerGoType(c)}`)
1381
1418
  }
@@ -1413,9 +1450,14 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
1413
1450
  const propsTypeName = `${componentName}Props`
1414
1451
  this.emitPropsStructHeader(lines, ir, propsTypeName, componentName)
1415
1452
 
1416
- this.emitPropsDataFields(lines, ir, nestedComponents, propTypeOverrides)
1453
+ // ONE taken-tags set spans every field-emitting loop below — per-loop
1454
+ // sets would let two loops emit the same tag, and `encoding/json` drops
1455
+ // both such fields silently (see `claimJsonTag`).
1456
+ const takenJsonTags = new Set<string>()
1417
1457
 
1418
- this.emitPropsAuxFields(lines, ir, componentName, nestedComponents, spreadSlots)
1458
+ this.emitPropsDataFields(lines, ir, nestedComponents, propTypeOverrides, takenJsonTags)
1459
+
1460
+ this.emitPropsAuxFields(lines, ir, componentName, nestedComponents, spreadSlots, takenJsonTags)
1419
1461
 
1420
1462
  lines.push('}')
1421
1463
  lines.push('')
@@ -1472,7 +1514,12 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
1472
1514
  itemType: TypeInfo | null | undefined,
1473
1515
  ): Array<{ tsName: string; goName: string; goType: string }> {
1474
1516
  if (!itemType) return []
1475
- const typeName = itemType.raw?.replace(/\[\]$/, '') ?? itemType.raw
1517
+ // `itemType` is documented as the loop ITEM's type, but historically
1518
+ // could arrive already ARRAY-shaped (`Todo[]`) from a caller that hadn't
1519
+ // unwrapped it — `typeNodeToTypeInfo` normalises every array spelling to
1520
+ // `kind: 'array'` + `elementType`, so read the element structurally
1521
+ // (#2484) instead of regexing a trailing `[]` off `raw`.
1522
+ const typeName = itemType.kind === 'array' ? (itemType.elementType?.raw ?? itemType.raw) : itemType.raw
1476
1523
  if (!typeName) return []
1477
1524
  for (const td of this.state.currentTypeDefinitions) {
1478
1525
  if (td.name === typeName) {
@@ -1734,8 +1781,12 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
1734
1781
 
1735
1782
  const propFieldNames = new Set<string>()
1736
1783
  for (const param of ir.metadata.propsParams) {
1784
+ // Props field: LOCAL binding, what the template reads (`{{.X}}`).
1785
+ // Everything on the right of `:` below reads `in.<inputField>`
1786
+ // (caller-facing), never `in.<fieldName>` (local).
1737
1787
  const fieldName = capitalizeFieldName(param.name)
1738
- if (nestedArrayFields.has(fieldName)) continue
1788
+ const inputField = capitalizeFieldName(param.sourceName ?? param.name)
1789
+ if (this.isNestedArrayShadowed(param, nestedArrayFields)) continue
1739
1790
  const hoisted = propFallbackVars.get(param.name)
1740
1791
  if (hoisted) {
1741
1792
  lines.push(`\t\t${fieldName}: ${hoisted.varName},`)
@@ -1743,17 +1794,17 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
1743
1794
  const paramDefault = goPropDefault(param.defaultValue)
1744
1795
  const memoFold = memoFallbacks.get(fieldName)
1745
1796
  if (paramDefault !== null) {
1746
- lines.push(`\t\t${fieldName}: ${applyGoFallback(`in.${fieldName}`, paramDefault)},`)
1797
+ lines.push(`\t\t${fieldName}: ${applyGoFallback(`in.${inputField}`, paramDefault)},`)
1747
1798
  } else if (memoFold !== undefined && memoFold.goType === 'string') {
1748
- lines.push(`\t\t${fieldName}: ${applyGoFallback(`in.${fieldName}`, memoFold.goFallback)},`)
1799
+ lines.push(`\t\t${fieldName}: ${applyGoFallback(`in.${inputField}`, memoFold.goFallback)},`)
1749
1800
  } else if (memoFold !== undefined) {
1750
1801
  // interface{} field (`size ?? 'icon'`): the string zero-check doesn't
1751
1802
  // compile, so wrap in a nil/empty-tolerant IIFE.
1752
1803
  lines.push(
1753
- `\t\t${fieldName}: func() interface{} { v := interface{}(in.${fieldName}); if v == nil || v == "" { return ${memoFold.goFallback} }; return v }(),`,
1804
+ `\t\t${fieldName}: func() interface{} { v := interface{}(in.${inputField}); if v == nil || v == "" { return ${memoFold.goFallback} }; return v }(),`,
1754
1805
  )
1755
1806
  } else {
1756
- lines.push(`\t\t${fieldName}: in.${fieldName},`)
1807
+ lines.push(`\t\t${fieldName}: in.${inputField},`)
1757
1808
  }
1758
1809
  }
1759
1810
  propFieldNames.add(fieldName)
@@ -1823,9 +1874,11 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
1823
1874
  }
1824
1875
 
1825
1876
  // `useContext` consumer fields default to the `createContext` default when
1826
- // the provider didn't set them.
1877
+ // the provider didn't set them. Props params must contribute BOTH namings
1878
+ // (`propParamFieldNamesUnion`) or this disagrees with Input/Props on
1879
+ // whether the consumer field exists.
1827
1880
  const takenInit = new Set<string>([
1828
- ...ir.metadata.propsParams.map(p => capitalizeFieldName(p.name)),
1881
+ ...this.propParamFieldNamesUnion(ir.metadata.propsParams),
1829
1882
  ...ir.metadata.signals.map(s => capitalizeFieldName(s.getter)),
1830
1883
  ...ir.metadata.memos.map(m => capitalizeFieldName(m.name)),
1831
1884
  ])
@@ -1891,14 +1944,12 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
1891
1944
  // A hyphenated attr (`aria-label`) can't be a Go field and, with no rest
1892
1945
  // bag to route it into, has nowhere to go — skip over emitting invalid Go.
1893
1946
  if (jsxName.includes('-')) return
1894
- // Same resolution as `loopRowChildPropOverrides` (#2457): the child's
1895
- // own Go field, which differs from the JSX attribute under an aliased
1896
- // destructure (`{ n: count }` field `Count`). Emitting the attribute
1897
- // name here put an unknown field in a Go struct literal — a build
1898
- // error rather than a silent drop, but wrong either way. Falls back to
1899
- // the capitalized attribute for a child this run never registered.
1900
- const fieldName =
1901
- this.childPropFieldNames.get(child.name)?.get(jsxName) ?? capitalizeFieldName(jsxName)
1947
+ // `<Child>Input{...}` composite literal: Input fields are
1948
+ // caller-facing and `jsxName` IS the caller-facing name, so no
1949
+ // `childPropFieldNames` lookup that map resolves the child's
1950
+ // LOCAL/Props field for `loopRowChildPropOverrides`'s different
1951
+ // struct.
1952
+ const fieldName = capitalizeFieldName(jsxName)
1902
1953
  lines.push(`\t\t\t${fieldName}: ${goValue},`)
1903
1954
  }
1904
1955
  for (const prop of child.props) {
@@ -2425,6 +2476,7 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
2425
2476
  ir: ComponentIR,
2426
2477
  nestedComponents: NestedComponentInfo[],
2427
2478
  propTypeOverrides: Map<string, string>,
2479
+ takenJsonTags: Set<string>,
2428
2480
  ): void {
2429
2481
  // Nested-component array fields are emitted as typed arrays below, not as
2430
2482
  // their raw prop; track them (and emitted names) to skip duplicates.
@@ -2433,12 +2485,21 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
2433
2485
 
2434
2486
  for (const param of ir.metadata.propsParams) {
2435
2487
  const fieldName = capitalizeFieldName(param.name)
2436
- if (nestedArrayFields.has(fieldName)) continue
2488
+ if (this.isNestedArrayShadowed(param, nestedArrayFields)) continue
2437
2489
  const goType = resolvePropGoType(this.emitCtx, param, propTypeOverrides)
2438
2490
  // Children are already rendered in the DOM; serialising them into bf-p
2439
2491
  // leaks nested scope ids and bloats the attribute. Exclude from JSON so
2440
2492
  // BfPropsAttr never marshals them.
2441
- const jsonTag = param.name === 'children' ? '-' : this.toJsonTag(param.name)
2493
+ //
2494
+ // The tag is CALLER-facing even though `fieldName` stays LOCAL: the
2495
+ // tag is the hydration wire format and the client JS reads
2496
+ // `_p.<callerKey>` (#2524) — a local tag would make hydration read the
2497
+ // wrong key. Props are emitted FIRST into `takenJsonTags`, so a prop
2498
+ // always wins a tag collision against a later signal/memo/etc field.
2499
+ const jsonTag =
2500
+ param.name === 'children'
2501
+ ? '-'
2502
+ : this.claimJsonTag(this.toJsonTag(param.sourceName ?? param.name), takenJsonTags)
2442
2503
  lines.push(`\t${fieldName} ${goType} \`json:"${jsonTag}"\``)
2443
2504
  propFieldNames.add(fieldName)
2444
2505
  }
@@ -2451,7 +2512,7 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
2451
2512
  if (signal.envReader) continue
2452
2513
  const fieldName = capitalizeFieldName(signal.getter)
2453
2514
  if (propFieldNames.has(fieldName)) continue
2454
- const jsonTag = this.toJsonTag(signal.getter)
2515
+ const jsonTag = this.claimJsonTag(this.toJsonTag(signal.getter), takenJsonTags)
2455
2516
  // A synthesised struct type wins outright — the signal is an untyped
2456
2517
  // object array we gave a concrete element type.
2457
2518
  const synthType = this.state.synthStructTypes.get(signal.getter)
@@ -2462,12 +2523,12 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
2462
2523
  let goType: string
2463
2524
  let referencedProp = propsParamMap.get(signal.initialValue)
2464
2525
  if (!referencedProp) {
2465
- const propName = this.extractPropNameFromInitialValue(signal.initialValue)
2526
+ const propName = this.extractPropNameFromInitialValue(signal.initialValue, signal.parsed)
2466
2527
  if (propName) referencedProp = propsParamMap.get(propName)
2467
2528
  }
2468
2529
  if (referencedProp) {
2469
- const propGoType = typeInfoToGo(this.emitCtx, referencedProp.type, referencedProp.defaultValue)
2470
- const signalGoType = typeInfoToGo(this.emitCtx, signal.type, signal.initialValue)
2530
+ const propGoType = typeInfoToGo(this.emitCtx, referencedProp.type, referencedProp.defaultValue, referencedProp.parsed)
2531
+ const signalGoType = typeInfoToGo(this.emitCtx, signal.type, signal.initialValue, signal.parsed)
2471
2532
  // The "prop type wins" heuristic helps when the signal infer is less
2472
2533
  // specific than the prop (e.g. `createSignal(props.todos)` wants
2473
2534
  // `[]Todo`, not `interface{}`). It HURTS when the initial expression
@@ -2489,7 +2550,7 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
2489
2550
  goType = propGoType
2490
2551
  }
2491
2552
  } else {
2492
- goType = typeInfoToGo(this.emitCtx, signal.type, signal.initialValue)
2553
+ goType = typeInfoToGo(this.emitCtx, signal.type, signal.initialValue, signal.parsed)
2493
2554
  }
2494
2555
  lines.push(`\t${fieldName} ${goType} \`json:"${jsonTag}"\``)
2495
2556
  }
@@ -2501,7 +2562,7 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
2501
2562
  for (const memo of ir.metadata.memos) {
2502
2563
  const fieldName = capitalizeFieldName(memo.name)
2503
2564
  if (propFieldNames.has(fieldName)) continue
2504
- const jsonTag = this.toJsonTag(memo.name)
2565
+ const jsonTag = this.claimJsonTag(this.toJsonTag(memo.name), takenJsonTags)
2505
2566
  const goType = this.inferMemoType(memo, ir.metadata.signals, propsParamMap)
2506
2567
  lines.push(`\t${fieldName} ${goType} \`json:"${jsonTag}"\``)
2507
2568
  }
@@ -2513,6 +2574,7 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
2513
2574
  componentName: string,
2514
2575
  nestedComponents: NestedComponentInfo[],
2515
2576
  spreadSlots: SpreadSlotInfo[],
2577
+ takenJsonTags: Set<string>,
2516
2578
  ): void {
2517
2579
  // Computed fields for component-scope derived string consts the template
2518
2580
  // references (e.g. `root = base || '/'`). Not serialised — the route handler
@@ -2527,14 +2589,16 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
2527
2589
  }
2528
2590
 
2529
2591
  // `useContext` consumer fields (skip names already taken by a prop /
2530
- // signal / memo field).
2592
+ // signal / memo field). Props params must contribute BOTH namings
2593
+ // (`propParamFieldNamesUnion`) or this disagrees with Input on whether
2594
+ // the consumer field exists.
2531
2595
  const takenProps = new Set<string>([
2532
- ...ir.metadata.propsParams.map(p => capitalizeFieldName(p.name)),
2596
+ ...this.propParamFieldNamesUnion(ir.metadata.propsParams),
2533
2597
  ...ir.metadata.signals.map(s => capitalizeFieldName(s.getter)),
2534
2598
  ...ir.metadata.memos.map(m => capitalizeFieldName(m.name)),
2535
2599
  ])
2536
2600
  for (const c of this.nonCollidingContextConsumers(takenProps)) {
2537
- const jsonTag = this.toJsonTag(c.localName)
2601
+ const jsonTag = this.claimJsonTag(this.toJsonTag(c.localName), takenJsonTags)
2538
2602
  lines.push(`\t${this.contextFieldName(c)} ${this.contextConsumerGoType(c)} \`json:"${jsonTag}"\``)
2539
2603
  }
2540
2604
 
@@ -2548,7 +2612,10 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
2548
2612
  lines.push(`\t${nested.name}s []${elemType} \`json:"-"\``)
2549
2613
  } else {
2550
2614
  // Static + prop-derived arrays go in JSON so the client can hydrate.
2551
- const jsonTag = this.toJsonTag(`${nested.name.charAt(0).toLowerCase()}${nested.name.slice(1)}s`)
2615
+ const jsonTag = this.claimJsonTag(
2616
+ this.toJsonTag(`${nested.name.charAt(0).toLowerCase()}${nested.name.slice(1)}s`),
2617
+ takenJsonTags,
2618
+ )
2552
2619
  lines.push(`\t${nested.name}s []${elemType} \`json:"${jsonTag}"\``)
2553
2620
  }
2554
2621
  }
@@ -2562,7 +2629,7 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
2562
2629
  // map[string]any` field the template reads via `{{bf_spread_attrs}}`.
2563
2630
  // Loop-internal spreads emit inline and don't appear here.
2564
2631
  for (const slot of spreadSlots) {
2565
- const jsonTag = this.toJsonTag(slot.slotId)
2632
+ const jsonTag = this.claimJsonTag(this.toJsonTag(slot.slotId), takenJsonTags)
2566
2633
  lines.push(`\t${slot.slotId} map[string]any \`json:"${jsonTag}"\``)
2567
2634
  }
2568
2635
  }
@@ -2870,7 +2937,7 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
2870
2937
  */
2871
2938
  private lowerProviderMapMemberValue(
2872
2939
  node: ParsedExpr,
2873
- propsParams: ReadonlyArray<{ name: string }>,
2940
+ propsParams: ReadonlyArray<{ name: string; sourceName?: string }>,
2874
2941
  ): string | null {
2875
2942
  if (node.kind === 'object-literal') return objectLiteralToGoMap(this.emitCtx, node)
2876
2943
  const literal = parsedLiteralToGo(this.emitCtx, node)
@@ -2889,8 +2956,15 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
2889
2956
  // closure below — TS drops property-path narrowing (`node.left.kind
2890
2957
  // === 'member'`) across a nested arrow function boundary.
2891
2958
  const propName = node.left.property
2892
- if (propsParams.some(param => param.name === propName)) {
2893
- const fieldRef = `in.${capitalizeFieldName(propName)}`
2959
+ const matchedParam = propsParams.find(param => param.name === propName)
2960
+ if (matchedParam) {
2961
+ // `sourceName ?? name`: the Input struct's field is caller-facing
2962
+ // (#2525). A `props.<key>` member read (this function's shape) has
2963
+ // no destructure aliasing to begin with — `key` IS the caller-facing
2964
+ // name — but resolving through the matched param keeps this in step
2965
+ // with every other `in.<field>` read in the file rather than
2966
+ // re-deriving the assumption locally.
2967
+ const fieldRef = `in.${capitalizeFieldName(matchedParam.sourceName ?? matchedParam.name)}`
2894
2968
  return (
2895
2969
  `func() map[string]interface{} { ` +
2896
2970
  `if m := bf.AsMap(${fieldRef}); m != nil { return m }; ` +
@@ -2911,7 +2985,7 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
2911
2985
  */
2912
2986
  private templatePartsToGoCode(
2913
2987
  parts: IRTemplatePart[],
2914
- propsParams: { name: string }[]
2988
+ propsParams: { name: string; sourceName?: string }[]
2915
2989
  ): string | null {
2916
2990
  const segments: string[] = []
2917
2991
  for (const part of parts) {
@@ -2929,7 +3003,9 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
2929
3003
  const keyExpr = part.key.trim()
2930
3004
  const param = propsParams.find(p => p.name === keyExpr)
2931
3005
  if (!param) return null
2932
- const fieldName = capitalizeFieldName(keyExpr)
3006
+ // Input field is caller-facing (#2525); `keyExpr` is the LOCAL prop
3007
+ // binding used in the source expression.
3008
+ const fieldName = capitalizeFieldName(param.sourceName ?? keyExpr)
2933
3009
  const caseEntries = Object.entries(part.cases)
2934
3010
  if (caseEntries.length === 0) {
2935
3011
  segments.push('""')
@@ -2964,7 +3040,7 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
2964
3040
  expr: string,
2965
3041
  signals: { getter: string; setter: string | null; initialValue: string; type: TypeInfo; parsed?: ParsedExpr }[],
2966
3042
  memos: { name: string; computation: string; deps: string[] }[],
2967
- propsParams: { name: string }[]
3043
+ propsParams: { name: string; sourceName?: string }[]
2968
3044
  ): string | null {
2969
3045
  // `getter() === 'lit'` / `!==` as a child-instance prop value
2970
3046
  // (`open={openItem() === 'item-1'}`): resolves to a Go bool when the
@@ -3069,8 +3145,16 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
3069
3145
  passthroughName !== null &&
3070
3146
  ((localConst !== undefined && !isPropsDestructureAlias) ||
3071
3147
  this.state.localHelperNames.has(passthroughName))
3072
- if (passthroughName && !shadowedByLocal && propsParams.some(p => p.name === passthroughName)) {
3073
- return `in.${capitalizeFieldName(passthroughName)}`
3148
+ const passthroughParam =
3149
+ passthroughName && !shadowedByLocal
3150
+ ? propsParams.find(p => p.name === passthroughName)
3151
+ : undefined
3152
+ if (passthroughParam) {
3153
+ // Input field is caller-facing (#2525); `passthroughName` is the LOCAL
3154
+ // binding (`bareIdentifier`) or the `props.<key>` source key
3155
+ // (`barePropAccess`, never aliased) — resolve through the matched
3156
+ // param rather than assuming the two coincide.
3157
+ return `in.${capitalizeFieldName(passthroughParam.sourceName ?? passthroughParam.name)}`
3074
3158
  }
3075
3159
 
3076
3160
  return null
@@ -3079,8 +3163,8 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
3079
3163
  /** Infer the Go type for a memo from its computation and dependencies. */
3080
3164
  private inferMemoType(
3081
3165
  memo: { name: string; computation: string; type: TypeInfo; deps: string[]; bodyIsTemplateLiteral?: boolean; parsed?: ParsedExpr; parsedBlock?: ParsedStatement[] },
3082
- signals: { getter: string; initialValue: string; type: TypeInfo }[],
3083
- propsParamMap: Map<string, { name: string; type: TypeInfo; defaultValue?: string }>
3166
+ signals: { getter: string; initialValue: string; type: TypeInfo; parsed?: ParsedExpr }[],
3167
+ propsParamMap: Map<string, { name: string; type: TypeInfo; defaultValue?: string; parsed?: ParsedExpr }>
3084
3168
  ): string {
3085
3169
  // A LIST-valued `.filter(arrow)` memo (#2075 — the blog PostList `visible`
3086
3170
  // shape) is a slice of the receiver's boxed elements, not a scalar.
@@ -3110,16 +3194,16 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
3110
3194
  if (signal) {
3111
3195
  let referencedProp = propsParamMap.get(signal.initialValue)
3112
3196
  if (!referencedProp) {
3113
- const propName = this.extractPropNameFromInitialValue(signal.initialValue)
3197
+ const propName = this.extractPropNameFromInitialValue(signal.initialValue, signal.parsed)
3114
3198
  if (propName) referencedProp = propsParamMap.get(propName)
3115
3199
  }
3116
3200
  if (referencedProp) {
3117
- const propType = typeInfoToGo(this.emitCtx, referencedProp.type, referencedProp.defaultValue)
3201
+ const propType = typeInfoToGo(this.emitCtx, referencedProp.type, referencedProp.defaultValue, referencedProp.parsed)
3118
3202
  if (propType === 'int' || propType === 'float64') {
3119
3203
  return 'int'
3120
3204
  }
3121
3205
  }
3122
- const signalType = typeInfoToGo(this.emitCtx, signal.type, signal.initialValue)
3206
+ const signalType = typeInfoToGo(this.emitCtx, signal.type, signal.initialValue, signal.parsed)
3123
3207
  if (signalType === 'int' || signalType === 'float64') {
3124
3208
  return 'int'
3125
3209
  }
@@ -3175,12 +3259,15 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
3175
3259
  if (!param) continue
3176
3260
  // A destructure default already wins via applyGoFallback below.
3177
3261
  if (goPropDefault(param.defaultValue) !== null) continue
3178
- const fieldName = capitalizeFieldName(match.propName)
3262
+ // `PropFallbackVar.fieldName` is read off the INPUT struct (every
3263
+ // consumer does `in.${fieldName}`, #2525) — caller-facing, not
3264
+ // `match.propName`'s local binding.
3265
+ const fieldName = capitalizeFieldName(param.sourceName ?? match.propName)
3179
3266
  // A `??`-consumed optional scalar lowered to `interface{}` (#2248) —
3180
3267
  // detected off the SAME `resolvePropGoType` pipeline the struct
3181
3268
  // generators use, so this can't drift from the emitted field type. The
3182
3269
  // concrete pre-flip type is what the hoisted local materializes as.
3183
- const concreteType = propTypeOverrides.get(param.name) ?? typeInfoToGo(this.emitCtx, param.type, param.defaultValue)
3270
+ const concreteType = propTypeOverrides.get(param.name) ?? typeInfoToGo(this.emitCtx, param.type, param.defaultValue, param.parsed)
3184
3271
  const nullishLowered =
3185
3272
  NULLISH_SCALAR_GO_TYPES.has(concreteType) &&
3186
3273
  resolvePropGoType(this.emitCtx, param, propTypeOverrides) === 'interface{}'
@@ -5561,16 +5648,14 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
5561
5648
  })
5562
5649
  continue
5563
5650
  }
5564
- // #2457: emit the CHILD's own Go field name, not the JSX attribute name.
5565
- // They differ under an aliased destructure (`{ n: count }` → attribute
5566
- // `n`, field `Count`); resolving through `childPropFieldNames` here
5567
- // the parent's emission site, the only place that has both the JSX
5568
- // name and the child's shape means `bf.WithProps`/`bf.RepropsAssign`
5569
- // never see a name the struct doesn't have. Falls back to today's
5570
- // capitalized-attribute behaviour for a cross-file child this run's
5571
- // pre-pass never registered (`childPropFieldNames` has no entry).
5572
- const fieldName =
5573
- this.childPropFieldNames.get(comp.name)?.get(prop.name) ?? capitalizeFieldName(prop.name)
5651
+ // Emit the CHILD's own PROPS field name, not the JSX attribute name
5652
+ // `bf.WithProps`/`bf.RepropsAssign` patch the constructed PROPS
5653
+ // instance, and passing a name the struct doesn't have (an aliased
5654
+ // `{ n: count }` attribute) is a silent passthrough, not an error
5655
+ // (#2457). Stays LOCAL even though the child's Input field is
5656
+ // caller-facing. Falls back to the capitalized attribute for a
5657
+ // cross-file child this run's pre-pass never registered.
5658
+ const fieldName = this.childPropFieldNames.get(comp.name)?.get(prop.name) ?? capitalizeFieldName(prop.name)
5574
5659
  args.push(`${JSON.stringify(fieldName)} ${wrapIfMultiToken(go)}`)
5575
5660
  }
5576
5661
  if (args.length === 0) return null