@barefootjs/go-template 0.31.10 → 0.32.0

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.
@@ -255,6 +255,7 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
255
255
  extractPropNameFromInitialValue: (initialValue, preParsed) =>
256
256
  this.extractPropNameFromInitialValue(initialValue, preParsed),
257
257
  extractPropFallback: (initialValue, preParsed) => this.extractPropFallback(initialValue, preParsed),
258
+ extractCollisionDerivation: (parsed) => this.extractCollisionDerivation(parsed),
258
259
  resolveModuleStringConst: (name) => this.resolveModuleStringConst(name),
259
260
  }
260
261
 
@@ -1873,6 +1874,8 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
1873
1874
  lines.push('')
1874
1875
  }
1875
1876
 
1877
+ this.emitCallerPropsInit(lines, ir, nestedComponents, staticWithoutBody, staticWithBody, dynamicWithBody, emittedWrapperVars, propTypeOverrides)
1878
+
1876
1879
  lines.push(`\treturn ${propsTypeName}{`)
1877
1880
  lines.push('\t\tScopeID: scopeID,')
1878
1881
  // Host context, for when *this* component is itself a slot-attached child.
@@ -1881,6 +1884,7 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
1881
1884
  if (this.usesSearchParams(ir)) {
1882
1885
  lines.push('\t\tSearchParams: in.SearchParams,')
1883
1886
  }
1887
+ lines.push('\t\tBfCallerProps: bfCallerProps,')
1884
1888
 
1885
1889
  const nestedArrayFields = this.propDerivedNestedArrayFields(nestedComponents)
1886
1890
 
@@ -1918,7 +1922,17 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
1918
1922
  if (this.isNestedArrayShadowed(param, nestedArrayFields)) continue
1919
1923
  const hoisted = propFallbackVars.get(param.name)
1920
1924
  if (hoisted) {
1921
- lines.push(`\t\t${fieldName}: ${hoisted.varName},`)
1925
+ // #2683: a `collisionWrap` entry means this field is ALSO a
1926
+ // colliding signal's shared field — `hoisted.varName` alone only
1927
+ // carries the coalesced `props.X ?? <lit>` value, so the surrounding
1928
+ // arithmetic (`* 2`, etc.) is composed back on here, giving the
1929
+ // FULLY DERIVED value the signal's own initializer computes. Without
1930
+ // this the field would silently carry just the coalesce result (the
1931
+ // same class of bug this fix addresses, one operator short).
1932
+ const value = hoisted.collisionWrap
1933
+ ? `${hoisted.varName} ${hoisted.collisionWrap.operator} ${hoisted.collisionWrap.operand}`
1934
+ : hoisted.varName
1935
+ lines.push(`\t\t${fieldName}: ${value},`)
1922
1936
  } else {
1923
1937
  const paramDefault = goPropDefault(param.defaultValue)
1924
1938
  const memoFold = memoFallbacks.get(fieldName)
@@ -2038,6 +2052,139 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
2038
2052
  lines.push('}')
2039
2053
  }
2040
2054
 
2055
+ /**
2056
+ * Build the `bfCallerProps` local (assigned to `BfCallerProps` in the
2057
+ * returned struct) — the hydration-only, caller-supplied-keys-only view
2058
+ * of this component's props (#2684). See the field's doc comment
2059
+ * (`emitPropsStructHeader`) for the two-consumers rationale.
2060
+ *
2061
+ * Three classes, mirroring the issue's taxonomy — gated on `param.optional`
2062
+ * (NOT on the resolved Go type's nillability alone, even though that reads
2063
+ * as the more "obvious" test): a REQUIRED prop must stay unconditional
2064
+ * even when its resolved type happens to be nillable (`ctx: unknown`, the
2065
+ * `array-flatmap-thisarg` fixture passes `ctx: null` explicitly) — Go's
2066
+ * `interface{}` cannot tell "explicit null" from "omitted" once
2067
+ * unmarshaled (both are the nil zero value), so nil-checking a required
2068
+ * field would silently turn an intentional `null` into a dropped key.
2069
+ * 1. Required prop → always included, raw `in.<InputField>` (never the
2070
+ * defaulted Props-field value — a caller who passes the same value
2071
+ * as the author's default is indistinguishable from one who didn't,
2072
+ * but that's inherent to a required prop having no "omitted" state).
2073
+ * 2. Optional prop whose resolved Go type is nillable (`interface{}`,
2074
+ * `map[string]interface{}`, or any slice `[]T`) → included only
2075
+ * when `in.<InputField> != nil`, i.e. only when the caller actually
2076
+ * passed something.
2077
+ * 3. Optional prop that nonetheless resolved to a CONCRETE type (a
2078
+ * string-union alias like `placement?: 'top' | 'bottom'`, a bare
2079
+ * scalar never consumed nullish/attr/text/presence-wise, OR a type
2080
+ * that resolves to `interface{}` some OTHER way — e.g. an inherited
2081
+ * `extends` clause the analyzer can't fully resolve — while
2082
+ * `param.optional` itself is, possibly inaccurately, `false`) —
2083
+ * presence is unknowable from Input alone, so it's included
2084
+ * unconditionally, same as class 1. A documented residual, not
2085
+ * silently dropped; see the PR that introduced this method for named
2086
+ * instances found in the corpus (e.g. `textarea`'s `rows`, inherited
2087
+ * through `TextareaHTMLAttributes`). Do NOT widen these props' types
2088
+ * to "fix" this — that's `resolvePropGoType`'s call, not this
2089
+ * method's, and doing so would risk the same `ctx`-style regression.
2090
+ *
2091
+ * `children` is excluded outright (#1952 — a separately-declared
2092
+ * position, not relitigated here). Rest-props bags are never added to
2093
+ * the Props struct's JSON at all (checked: no field exists for them
2094
+ * outside Input), so nothing to exclude here either — already at parity
2095
+ * with the reference, which never serializes rest keys.
2096
+ *
2097
+ * A prop shadowed by its own driving nested-array field (`isNestedArrayShadowed`
2098
+ * — e.g. `toggleItems` colliding with a `<ToggleItem>` loop's derived
2099
+ * `ToggleItems` field, #2672/#2525) has NO Props/Input field of its own;
2100
+ * the reshaped nested-array local (`varName`, already built above by the
2101
+ * static/dynamic body-wrapper emission) is its only remaining carrier, so
2102
+ * that's what gets keyed in here instead of `in.<InputField>`. KNOWN
2103
+ * RESIDUAL: each item within that array is still the OLD whole-struct
2104
+ * marshal (baked author defaults / null-for-absent / zero-for-required
2105
+ * inside every item) — recursing the sidecar into embedded Props-struct
2106
+ * arrays needs either a generated `MarshalJSON` per Props type or a
2107
+ * runtime array-flattening helper, a bigger surface this method's design
2108
+ * doesn't cover. Reported, not chased (see PR description).
2109
+ */
2110
+ private emitCallerPropsInit(
2111
+ lines: string[],
2112
+ ir: ComponentIR,
2113
+ nestedComponents: NestedComponentInfo[],
2114
+ staticWithoutBody: NestedComponentInfo[],
2115
+ staticWithBody: NestedComponentInfo[],
2116
+ dynamicWithBody: NestedComponentInfo[],
2117
+ emittedWrapperVars: Set<string>,
2118
+ propTypeOverrides: Map<string, string>,
2119
+ ): void {
2120
+ lines.push('\tbfCallerProps := map[string]interface{}{}')
2121
+ const bfCallerPropsTakenTags = new Set<string>()
2122
+ const nestedArrayFields = this.propDerivedNestedArrayFields(nestedComponents)
2123
+
2124
+ for (const param of ir.metadata.propsParams) {
2125
+ // Children are DOM content, not hydration data (#1952) — excluded the
2126
+ // same way `emitPropsDataFields` excludes them from the Props struct.
2127
+ if (param.name === 'children') continue
2128
+ // Shadowed by its own driving nested-array field (#2672/#2525) — no
2129
+ // Input field exists for it at all; handled below via the array local.
2130
+ if (this.isNestedArrayShadowed(param, nestedArrayFields)) continue
2131
+ // Same tag algorithm `emitPropsDataFields` uses for the Props field's
2132
+ // own `json:"…"` tag, re-derived against a FRESH set: props are always
2133
+ // processed first, in the same order, over an empty set in BOTH
2134
+ // places, so the two computations can't disagree.
2135
+ const callerKey = this.claimJsonTag(this.toJsonTag(param.sourceName ?? param.name), bfCallerPropsTakenTags)
2136
+ if (callerKey === '-') continue // tag collision — dropped from the wire, same as the Props field itself would be
2137
+ const inputField = `in.${capitalizeFieldName(param.sourceName ?? param.name)}`
2138
+ const goType = resolvePropGoType(this.emitCtx, param, propTypeOverrides)
2139
+ const isNillable = goType === 'interface{}' || goType === 'map[string]interface{}' || goType.startsWith('[]')
2140
+ // `param.optional` gates the branch (not "isNillable alone") because a
2141
+ // REQUIRED prop must stay unconditional even when its resolved type
2142
+ // happens to be nillable (`ctx: unknown`, #2094's array-flatmap-thisarg
2143
+ // fixture passes `ctx: null` explicitly) — Go's `interface{}` cannot
2144
+ // tell "explicit null" from "omitted" once unmarshaled (both are the
2145
+ // nil zero value), so a nil-check here would silently turn an
2146
+ // intentional `null` into a dropped key, regressing a fixture that
2147
+ // matched the reference before this method existed. `resolvePropGoType`
2148
+ // never applies its nillable flip to a required prop, so this ONLY
2149
+ // affects the rare case where a type resolves to `interface{}` some
2150
+ // OTHER way (e.g. `unknown`, or a type inherited through an
2151
+ // unresolved external `extends` clause) while `param.optional` is
2152
+ // (accurately or not) false — see the method docstring's residual list.
2153
+ if (param.optional && isNillable) {
2154
+ lines.push(`\tif ${inputField} != nil {`)
2155
+ lines.push(`\t\tbfCallerProps["${callerKey}"] = ${inputField}`)
2156
+ lines.push(`\t}`)
2157
+ } else {
2158
+ // Required (class 1), or optional-but-concrete residual (class 3),
2159
+ // or optional-but-misresolved-required residual — see docstring.
2160
+ lines.push(`\tbfCallerProps["${callerKey}"] = ${inputField}`)
2161
+ }
2162
+ }
2163
+
2164
+ // Shadowed nested-array props — see method docstring.
2165
+ for (const nested of [...staticWithoutBody, ...staticWithBody, ...dynamicWithBody]) {
2166
+ if (!nested.isPropDerived) continue
2167
+ const varName = `${nested.name.charAt(0).toLowerCase()}${nested.name.slice(1)}s`
2168
+ const isBuilt = staticWithoutBody.includes(nested) || emittedWrapperVars.has(varName)
2169
+ if (!isBuilt) continue
2170
+ const arrayFieldName = capitalizeFieldName(`${nested.name}s`)
2171
+ const param = ir.metadata.propsParams.find(
2172
+ p => capitalizeFieldName(p.name) === arrayFieldName || capitalizeFieldName(p.sourceName ?? p.name) === arrayFieldName,
2173
+ )
2174
+ if (!param || !this.isNestedArrayShadowed(param, nestedArrayFields)) continue
2175
+ const callerKey = this.claimJsonTag(this.toJsonTag(param.sourceName ?? param.name), bfCallerPropsTakenTags)
2176
+ if (callerKey === '-') continue
2177
+ if (param.optional) {
2178
+ lines.push(`\tif in.${nested.name}s != nil {`)
2179
+ lines.push(`\t\tbfCallerProps["${callerKey}"] = ${varName}`)
2180
+ lines.push(`\t}`)
2181
+ } else {
2182
+ lines.push(`\tbfCallerProps["${callerKey}"] = ${varName}`)
2183
+ }
2184
+ }
2185
+ lines.push('')
2186
+ }
2187
+
2041
2188
  private emitStaticChildInstances(lines: string[], ir: ComponentIR): void {
2042
2189
  const staticChildren = this.collectStaticChildInstances(ir.root, ir.metadata.propsParams)
2043
2190
  for (const child of staticChildren) {
@@ -2741,6 +2888,24 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
2741
2888
  if (this.usesSearchParams(ir)) {
2742
2889
  lines.push('\tSearchParams bf.SearchParams `json:"-"`')
2743
2890
  }
2891
+
2892
+ // #2684: the raw, CALLER-SUPPLIED view of this component's props —
2893
+ // populated by `NewXxxProps` with exactly the keys the caller passed
2894
+ // (required props always; optional ones only when not nil), holding
2895
+ // the UNDEFAULTED value. This exists because every prop field above
2896
+ // (e.g. `X`) serves TWO different consumers that want two different
2897
+ // values: the Go TEMPLATE wants the DEFAULTED value so `{{.X}}` renders
2898
+ // the author's fallback, while the HYDRATION PAYLOAD wants to know only
2899
+ // what the caller actually supplied — baking the author's default into
2900
+ // it makes an omitted prop indistinguishable from one explicitly passed
2901
+ // with that value, and a Go zero value indistinguishable from an
2902
+ // explicit zero/empty/false (the reference, Hono, only ever serializes
2903
+ // caller-passed keys). One field can't answer both questions at once,
2904
+ // so this sidecar gives the hydration payload its own carrier;
2905
+ // `BfPropsAttr` (runtime/bf.go) marshals THIS map instead of the whole
2906
+ // struct when it's present. `json:"-"` — never marshaled as an ordinary
2907
+ // struct field; `BfPropsAttr` reads it directly by name via reflection.
2908
+ lines.push('\tBfCallerProps map[string]interface{} `json:"-"`')
2744
2909
  }
2745
2910
 
2746
2911
  private emitPropsDataFields(
@@ -3588,7 +3753,32 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
3588
3753
 
3589
3754
  const propTypeOverrides = buildPropTypeOverrides(this.emitCtx, ir)
3590
3755
  for (const signal of ir.metadata.signals) {
3591
- const match = this.extractPropFallback(signal.initialValue, this.resolvedSignalParsed(signal))
3756
+ let match = this.extractPropFallback(signal.initialValue, this.resolvedSignalParsed(signal))
3757
+ // #2683: the idempotent fold above declines a non-idempotent
3758
+ // derivation (`(props.count ?? 1) * 2`) at the top level. When that
3759
+ // signal's Go field name COLLIDES with the prop's own field — the
3760
+ // exact condition under which the props-field loop below would
3761
+ // otherwise silently drop the derivation and emit the raw prop
3762
+ // instead — fall back to the collision-derivation matcher so the
3763
+ // coalesced value still gets hoisted; the props-field loop composes
3764
+ // the surrounding arithmetic back on top of it (`collisionWrap`).
3765
+ // Gated strictly on the collision so a differently-named signal
3766
+ // deriving from the same prop (not colliding) is untouched — that
3767
+ // path's own dedicated field keeps today's behavior byte-for-byte.
3768
+ let collisionWrap: { operator: string; operand: string } | undefined
3769
+ if (!match) {
3770
+ const collision = this.extractCollisionDerivation(this.resolvedSignalParsed(signal))
3771
+ if (collision) {
3772
+ const collisionParam = ir.metadata.propsParams.find(p => p.name === collision.propName)
3773
+ const collisionField = collisionParam
3774
+ ? capitalizeFieldName(collisionParam.sourceName ?? collision.propName)
3775
+ : null
3776
+ if (collisionField && capitalizeFieldName(signal.getter) === collisionField) {
3777
+ match = { propName: collision.propName, goFallback: collision.goFallback }
3778
+ collisionWrap = { operator: collision.operator, operand: collision.operand }
3779
+ }
3780
+ }
3781
+ }
3592
3782
  if (!match) continue
3593
3783
  if (result.has(match.propName)) continue
3594
3784
  const param = ir.metadata.propsParams.find(p => p.name === match.propName)
@@ -3648,6 +3838,7 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
3648
3838
  goFallback: match.goFallback,
3649
3839
  zeroLiteral,
3650
3840
  ...(nullishLowered ? { assertType: concreteType } : {}),
3841
+ ...(collisionWrap ? { collisionWrap } : {}),
3651
3842
  })
3652
3843
  }
3653
3844
  return result
@@ -3742,6 +3933,67 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
3742
3933
  return { propName, goFallback }
3743
3934
  }
3744
3935
 
3936
+ /**
3937
+ * #2683: match a signal's collision-derivation shape — `(props.X ?? <lit>)
3938
+ * <op> <int>` — the ONE non-idempotent shape this adapter faithfully lowers
3939
+ * when a signal's Go field name collides with its own prop's field (see the
3940
+ * collision-override loop in `collectPropFallbackVars` / the props-field
3941
+ * loop in `generateNewPropsFunction`). Composes two ALREADY-EXISTING
3942
+ * lowerings instead of adding a third: `extractPropFallbackFromParsed`
3943
+ * recognizes the embedded `props.X ?? <lit>` presence check exactly as it
3944
+ * does for the idempotent fold, and the wrap mirrors the SAME `<ref> <op>
3945
+ * <int>` arithmetic shape `memoInitialFromParsedBody`'s multiply-family
3946
+ * branch already supports for a bare `props.X <op> N` (non-negative
3947
+ * integer operand only, matching that branch's own restriction).
3948
+ *
3949
+ * Returns null for any other shape — the caller then keeps today's
3950
+ * raw-passthrough behavior (sound-or-loud; the remaining #2683 pin, if
3951
+ * any, covers whatever this doesn't reach).
3952
+ */
3953
+ private extractCollisionDerivation(
3954
+ parsed: ParsedExpr | undefined,
3955
+ ): { propName: string; goFallback: string; operator: string; operand: string } | null {
3956
+ if (!parsed || parsed.kind !== 'binary') return null
3957
+ if (!['*', '+', '-', '/'].includes(parsed.op)) return null
3958
+ const { right } = parsed
3959
+ if (
3960
+ right.kind !== 'literal' ||
3961
+ right.literalType !== 'number' ||
3962
+ typeof right.value !== 'number' ||
3963
+ !Number.isInteger(right.value) ||
3964
+ right.value < 0
3965
+ ) {
3966
+ return null
3967
+ }
3968
+ // Constant division by zero is a Go COMPILE error (`invalid operation:
3969
+ // division by zero`), unlike JS's Infinity — declining keeps that shape
3970
+ // on the raw-passthrough path instead of breaking the build.
3971
+ if (parsed.op === '/' && right.value === 0) return null
3972
+ // The `??` fallback itself must be a NUMERIC literal before delegating
3973
+ // to the fold: `extractPropFallbackFromParsed` happily matches a string
3974
+ // fallback (`(props.label ?? 'x') + 2`), which would hoist
3975
+ // `var label string = "x"` and emit `Label: label + 2,` — invalid Go
3976
+ // (string + int), a compile break where the pre-existing behavior at
3977
+ // least built (Copilot review on #2694). JS's `'x' + 2` is string
3978
+ // concatenation besides, so a numeric compose could never be faithful
3979
+ // for it — the shape belongs on the raw-passthrough path until someone
3980
+ // lowers it through ConcatStr deliberately. Gating on `??` (not `||`)
3981
+ // also keeps JS nullish semantics exact for numeric props, where a
3982
+ // caller's explicit `0` must NOT trigger the fallback.
3983
+ const coalesce = parsed.left
3984
+ if (
3985
+ coalesce.kind !== 'logical' ||
3986
+ coalesce.op !== '??' ||
3987
+ coalesce.right.kind !== 'literal' ||
3988
+ coalesce.right.literalType !== 'number'
3989
+ ) {
3990
+ return null
3991
+ }
3992
+ const inner = this.extractPropFallbackFromParsed(parsed.left)
3993
+ if (!inner) return null
3994
+ return { ...inner, operator: parsed.op, operand: String(right.value) }
3995
+ }
3996
+
3745
3997
  /**
3746
3998
  * Extract the prop name from a signal's `props.xxx`-pattern initialValue,
3747
3999
  * e.g. `"props.initial ?? 0"` → `"initial"`, `"props.checked"` → `"checked"`.
@@ -167,6 +167,16 @@ export interface PropFallbackVar {
167
167
  * distinguishable from an absent one.
168
168
  */
169
169
  assertType?: string
170
+ /**
171
+ * #2683: set when this fallback var was hoisted from a signal's
172
+ * COLLISION-DERIVATION initializer (`(props.X ?? <lit>) <op> <int>`, where
173
+ * the signal's Go field name collides with prop X's) rather than a plain
174
+ * `props.X ?? <lit>`. `varName` above still holds only the coalesced RAW
175
+ * value (the presence-check declaration is unchanged) — this records the
176
+ * arithmetic wrap the props-field loop applies ON TOP of it so the shared
177
+ * field carries the FULLY DERIVED value, not just the coalesce result.
178
+ */
179
+ collisionWrap?: { operator: string; operand: string }
170
180
  }
171
181
 
172
182
  /**
@@ -10,6 +10,7 @@ import { isBooleanAttr } from '@barefootjs/jsx'
10
10
 
11
11
  import type { GoEmitContext } from '../emit-context.ts'
12
12
  import { resolveSignalParsedThroughSeedPlan } from '../lib/compile-state.ts'
13
+ import { capitalizeFieldName } from '../lib/go-naming.ts'
13
14
  import { typeInfoToGo } from '../type/type-codegen.ts'
14
15
 
15
16
  /**
@@ -224,7 +225,31 @@ export function collectNullishConsumedPropNames(ctx: GoEmitContext, ir: Componen
224
225
  // between `props.X` and `createSignal` doesn't leave this loop and
225
226
  // `collectPropFallbackVars`'s identical extraction disagreeing on
226
227
  // whether the prop needs the nillable flip.
227
- const match = ctx.extractPropFallback(signal.initialValue, resolveSignalParsedThroughSeedPlan(ctx.state, signal))
228
+ const resolvedParsed = resolveSignalParsedThroughSeedPlan(ctx.state, signal)
229
+ let match = ctx.extractPropFallback(signal.initialValue, resolvedParsed)
230
+ // #2683: the idempotent fold above declines a non-idempotent derivation
231
+ // (`(props.count ?? 1) * 2`) at the top level. When that signal's Go
232
+ // field name COLLIDES with the prop's own field, the props-struct
233
+ // emitter composes the derivation onto the SHARED field (see
234
+ // `collectPropFallbackVars`'s identical collision gate) — which needs
235
+ // the SAME nillable flip an ordinary `??` fallback gets, so an absent
236
+ // prop (nil) stays distinguishable from an explicit zero-equivalent
237
+ // value. Gated strictly on the collision, matching that other site, so
238
+ // a differently-named signal deriving from the same prop (not
239
+ // colliding — its own dedicated field, untouched by this fix) doesn't
240
+ // spuriously flip the prop's type.
241
+ if (!match) {
242
+ const collision = ctx.extractCollisionDerivation(resolvedParsed)
243
+ if (collision) {
244
+ const collisionParam = ir.metadata.propsParams.find(p => p.name === collision.propName)
245
+ const collisionField = collisionParam
246
+ ? capitalizeFieldName(collisionParam.sourceName ?? collision.propName)
247
+ : null
248
+ if (collisionField && capitalizeFieldName(signal.getter) === collisionField) {
249
+ match = { propName: collision.propName, goFallback: collision.goFallback }
250
+ }
251
+ }
252
+ }
228
253
  if (!match || !optionalParams.has(match.propName)) continue
229
254
  const f = match.goFallback
230
255
  if (f === '""' || f === 'false' || f === 'nil' || Number(f) === 0) continue
@@ -17,28 +17,4 @@
17
17
 
18
18
  import type { RenderDivergences } from '@barefootjs/jsx'
19
19
 
20
- export const renderDivergences: RenderDivergences = {
21
- // #2683: the props-struct emitter (`go-template-adapter.ts`) skips a
22
- // signal whose Go field name collides with a prop field, keyed purely on
23
- // the NAME collision — never on whether `extractPropFallback` actually
24
- // matched a supported `props.x ?? <default>` shape. For a non-idempotent
25
- // derivation (`createSignal((props.count ?? 1) * 2)`) the fallback
26
- // extractor correctly declines to fold `* 2` into the struct default, but
27
- // the `continue` fires anyway on the name match alone, so the emitted
28
- // struct field silently drops the `* 2` and the signal's initial value
29
- // renders as the raw prop instead of its derived value. Not a one-liner:
30
- // two Go struct fields can't share an identifier, so simply removing the
31
- // skip emits a duplicate field — the real fix needs its own PR.
32
- 'signal-prop-same-name-derived':
33
- 'self-derived signal collides with its prop field name in the generated Go props struct — the non-idempotent `* 2` derivation is dropped and the signal renders the raw prop value instead (https://github.com/piconic-ai/barefootjs/issues/2683)',
34
- // #2685 review: same #2683 bug, one hop of const indirection removed —
35
- // the props-struct field-name collision is keyed on the SIGNAL's name,
36
- // not on how its initializer reaches the prop, so
37
- // `const mid = props.count; createSignal((mid ?? 1) * 2)` collides
38
- // exactly like the direct-access form above. This is go's PRE-EXISTING
39
- // #2683 defect surfacing through a new fixture, not a regression from
40
- // the #2685 review fix (which lands correctly on every other
41
- // template-stash adapter — see those adapters' conformance runs).
42
- 'signal-prop-same-name-via-const-derived':
43
- 'self-derived signal (reached through a component-scope const) collides with its prop field name in the generated Go props struct — the non-idempotent `* 2` derivation is dropped and the signal renders the raw prop value instead (https://github.com/piconic-ai/barefootjs/issues/2683)',
44
- }
20
+ export const renderDivergences: RenderDivergences = {}