@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.
- package/dist/adapter/emit-context.d.ts +16 -0
- package/dist/adapter/emit-context.d.ts.map +1 -1
- package/dist/adapter/go-template-adapter.d.ts +74 -0
- package/dist/adapter/go-template-adapter.d.ts.map +1 -1
- package/dist/adapter/index.js +100 -4
- package/dist/adapter/lib/types.d.ts +13 -0
- package/dist/adapter/lib/types.d.ts.map +1 -1
- package/dist/adapter/props/prop-types.d.ts.map +1 -1
- package/dist/index.js +101 -8
- package/dist/render-divergences.d.ts.map +1 -1
- package/dist/vite.js +100 -4
- package/package.json +5 -5
- package/src/__tests__/go-template-adapter.test.ts +246 -6
- package/src/adapter/emit-context.ts +14 -0
- package/src/adapter/go-template-adapter.ts +254 -2
- package/src/adapter/lib/types.ts +10 -0
- package/src/adapter/props/prop-types.ts +26 -1
- package/src/render-divergences.ts +1 -25
|
@@ -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
|
-
|
|
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
|
-
|
|
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"`.
|
package/src/adapter/lib/types.ts
CHANGED
|
@@ -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
|
|
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 = {}
|