@barefootjs/go-template 0.31.10 → 0.33.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/expr/helper-inline.d.ts.map +1 -1
- package/dist/adapter/go-template-adapter.d.ts +75 -1
- package/dist/adapter/go-template-adapter.d.ts.map +1 -1
- package/dist/adapter/index.js +165 -15
- package/dist/adapter/lib/types.d.ts +13 -0
- package/dist/adapter/lib/types.d.ts.map +1 -1
- package/dist/adapter/memo/memo-compute.d.ts.map +1 -1
- package/dist/adapter/memo/memo-value.d.ts.map +1 -1
- package/dist/adapter/memo/template-interp.d.ts.map +1 -1
- package/dist/adapter/props/prop-types.d.ts.map +1 -1
- package/dist/adapter/spread/spread-codegen.d.ts.map +1 -1
- package/dist/adapter/value/parsed-literal-to-go.d.ts.map +1 -1
- package/dist/adapter/value/value-lowering.d.ts +15 -12
- package/dist/adapter/value/value-lowering.d.ts.map +1 -1
- package/dist/index.js +166 -17
- package/dist/render-divergences.d.ts.map +1 -1
- package/dist/vite.js +259 -52
- 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/expr/helper-inline.ts +4 -2
- package/src/adapter/go-template-adapter.ts +296 -9
- package/src/adapter/lib/types.ts +10 -0
- package/src/adapter/memo/memo-compute.ts +12 -1
- package/src/adapter/memo/memo-value.ts +3 -0
- package/src/adapter/memo/template-interp.ts +5 -0
- package/src/adapter/props/prop-types.ts +26 -1
- package/src/adapter/spread/spread-codegen.ts +8 -0
- package/src/adapter/value/parsed-literal-to-go.ts +7 -0
- package/src/adapter/value/value-lowering.ts +96 -22
- package/src/render-divergences.ts +2 -23
|
@@ -58,6 +58,7 @@ import {
|
|
|
58
58
|
asCallbackMethodCall,
|
|
59
59
|
sortComparatorFromArrow,
|
|
60
60
|
emitParsedExpr,
|
|
61
|
+
groupObjectLiteralSegments,
|
|
61
62
|
emitIRNode,
|
|
62
63
|
emitAttrValue,
|
|
63
64
|
augmentInheritedPropAccesses,
|
|
@@ -255,6 +256,7 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
255
256
|
extractPropNameFromInitialValue: (initialValue, preParsed) =>
|
|
256
257
|
this.extractPropNameFromInitialValue(initialValue, preParsed),
|
|
257
258
|
extractPropFallback: (initialValue, preParsed) => this.extractPropFallback(initialValue, preParsed),
|
|
259
|
+
extractCollisionDerivation: (parsed) => this.extractCollisionDerivation(parsed),
|
|
258
260
|
resolveModuleStringConst: (name) => this.resolveModuleStringConst(name),
|
|
259
261
|
}
|
|
260
262
|
|
|
@@ -1369,6 +1371,10 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
1369
1371
|
if (el.kind !== 'object-literal') return null
|
|
1370
1372
|
const seen = new Set<string>()
|
|
1371
1373
|
for (const prop of el.properties) {
|
|
1374
|
+
// A spread (`{ ...t, a: 1 }`, #2696 Step 2) isn't a plain per-key
|
|
1375
|
+
// scalar shape this fast path bakes — fall back (the caller's
|
|
1376
|
+
// `ts.createSourceFile` path, or refusal) rather than mis-baking.
|
|
1377
|
+
if (prop.kind === 'spread') return null
|
|
1372
1378
|
if (prop.shorthand) return null
|
|
1373
1379
|
const key = prop.key
|
|
1374
1380
|
if (!GO_IDENTIFIER.test(key)) return null
|
|
@@ -1873,6 +1879,8 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
1873
1879
|
lines.push('')
|
|
1874
1880
|
}
|
|
1875
1881
|
|
|
1882
|
+
this.emitCallerPropsInit(lines, ir, nestedComponents, staticWithoutBody, staticWithBody, dynamicWithBody, emittedWrapperVars, propTypeOverrides)
|
|
1883
|
+
|
|
1876
1884
|
lines.push(`\treturn ${propsTypeName}{`)
|
|
1877
1885
|
lines.push('\t\tScopeID: scopeID,')
|
|
1878
1886
|
// Host context, for when *this* component is itself a slot-attached child.
|
|
@@ -1881,6 +1889,7 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
1881
1889
|
if (this.usesSearchParams(ir)) {
|
|
1882
1890
|
lines.push('\t\tSearchParams: in.SearchParams,')
|
|
1883
1891
|
}
|
|
1892
|
+
lines.push('\t\tBfCallerProps: bfCallerProps,')
|
|
1884
1893
|
|
|
1885
1894
|
const nestedArrayFields = this.propDerivedNestedArrayFields(nestedComponents)
|
|
1886
1895
|
|
|
@@ -1918,7 +1927,17 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
1918
1927
|
if (this.isNestedArrayShadowed(param, nestedArrayFields)) continue
|
|
1919
1928
|
const hoisted = propFallbackVars.get(param.name)
|
|
1920
1929
|
if (hoisted) {
|
|
1921
|
-
|
|
1930
|
+
// #2683: a `collisionWrap` entry means this field is ALSO a
|
|
1931
|
+
// colliding signal's shared field — `hoisted.varName` alone only
|
|
1932
|
+
// carries the coalesced `props.X ?? <lit>` value, so the surrounding
|
|
1933
|
+
// arithmetic (`* 2`, etc.) is composed back on here, giving the
|
|
1934
|
+
// FULLY DERIVED value the signal's own initializer computes. Without
|
|
1935
|
+
// this the field would silently carry just the coalesce result (the
|
|
1936
|
+
// same class of bug this fix addresses, one operator short).
|
|
1937
|
+
const value = hoisted.collisionWrap
|
|
1938
|
+
? `${hoisted.varName} ${hoisted.collisionWrap.operator} ${hoisted.collisionWrap.operand}`
|
|
1939
|
+
: hoisted.varName
|
|
1940
|
+
lines.push(`\t\t${fieldName}: ${value},`)
|
|
1922
1941
|
} else {
|
|
1923
1942
|
const paramDefault = goPropDefault(param.defaultValue)
|
|
1924
1943
|
const memoFold = memoFallbacks.get(fieldName)
|
|
@@ -2038,6 +2057,139 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
2038
2057
|
lines.push('}')
|
|
2039
2058
|
}
|
|
2040
2059
|
|
|
2060
|
+
/**
|
|
2061
|
+
* Build the `bfCallerProps` local (assigned to `BfCallerProps` in the
|
|
2062
|
+
* returned struct) — the hydration-only, caller-supplied-keys-only view
|
|
2063
|
+
* of this component's props (#2684). See the field's doc comment
|
|
2064
|
+
* (`emitPropsStructHeader`) for the two-consumers rationale.
|
|
2065
|
+
*
|
|
2066
|
+
* Three classes, mirroring the issue's taxonomy — gated on `param.optional`
|
|
2067
|
+
* (NOT on the resolved Go type's nillability alone, even though that reads
|
|
2068
|
+
* as the more "obvious" test): a REQUIRED prop must stay unconditional
|
|
2069
|
+
* even when its resolved type happens to be nillable (`ctx: unknown`, the
|
|
2070
|
+
* `array-flatmap-thisarg` fixture passes `ctx: null` explicitly) — Go's
|
|
2071
|
+
* `interface{}` cannot tell "explicit null" from "omitted" once
|
|
2072
|
+
* unmarshaled (both are the nil zero value), so nil-checking a required
|
|
2073
|
+
* field would silently turn an intentional `null` into a dropped key.
|
|
2074
|
+
* 1. Required prop → always included, raw `in.<InputField>` (never the
|
|
2075
|
+
* defaulted Props-field value — a caller who passes the same value
|
|
2076
|
+
* as the author's default is indistinguishable from one who didn't,
|
|
2077
|
+
* but that's inherent to a required prop having no "omitted" state).
|
|
2078
|
+
* 2. Optional prop whose resolved Go type is nillable (`interface{}`,
|
|
2079
|
+
* `map[string]interface{}`, or any slice `[]T`) → included only
|
|
2080
|
+
* when `in.<InputField> != nil`, i.e. only when the caller actually
|
|
2081
|
+
* passed something.
|
|
2082
|
+
* 3. Optional prop that nonetheless resolved to a CONCRETE type (a
|
|
2083
|
+
* string-union alias like `placement?: 'top' | 'bottom'`, a bare
|
|
2084
|
+
* scalar never consumed nullish/attr/text/presence-wise, OR a type
|
|
2085
|
+
* that resolves to `interface{}` some OTHER way — e.g. an inherited
|
|
2086
|
+
* `extends` clause the analyzer can't fully resolve — while
|
|
2087
|
+
* `param.optional` itself is, possibly inaccurately, `false`) —
|
|
2088
|
+
* presence is unknowable from Input alone, so it's included
|
|
2089
|
+
* unconditionally, same as class 1. A documented residual, not
|
|
2090
|
+
* silently dropped; see the PR that introduced this method for named
|
|
2091
|
+
* instances found in the corpus (e.g. `textarea`'s `rows`, inherited
|
|
2092
|
+
* through `TextareaHTMLAttributes`). Do NOT widen these props' types
|
|
2093
|
+
* to "fix" this — that's `resolvePropGoType`'s call, not this
|
|
2094
|
+
* method's, and doing so would risk the same `ctx`-style regression.
|
|
2095
|
+
*
|
|
2096
|
+
* `children` is excluded outright (#1952 — a separately-declared
|
|
2097
|
+
* position, not relitigated here). Rest-props bags are never added to
|
|
2098
|
+
* the Props struct's JSON at all (checked: no field exists for them
|
|
2099
|
+
* outside Input), so nothing to exclude here either — already at parity
|
|
2100
|
+
* with the reference, which never serializes rest keys.
|
|
2101
|
+
*
|
|
2102
|
+
* A prop shadowed by its own driving nested-array field (`isNestedArrayShadowed`
|
|
2103
|
+
* — e.g. `toggleItems` colliding with a `<ToggleItem>` loop's derived
|
|
2104
|
+
* `ToggleItems` field, #2672/#2525) has NO Props/Input field of its own;
|
|
2105
|
+
* the reshaped nested-array local (`varName`, already built above by the
|
|
2106
|
+
* static/dynamic body-wrapper emission) is its only remaining carrier, so
|
|
2107
|
+
* that's what gets keyed in here instead of `in.<InputField>`. KNOWN
|
|
2108
|
+
* RESIDUAL: each item within that array is still the OLD whole-struct
|
|
2109
|
+
* marshal (baked author defaults / null-for-absent / zero-for-required
|
|
2110
|
+
* inside every item) — recursing the sidecar into embedded Props-struct
|
|
2111
|
+
* arrays needs either a generated `MarshalJSON` per Props type or a
|
|
2112
|
+
* runtime array-flattening helper, a bigger surface this method's design
|
|
2113
|
+
* doesn't cover. Reported, not chased (see PR description).
|
|
2114
|
+
*/
|
|
2115
|
+
private emitCallerPropsInit(
|
|
2116
|
+
lines: string[],
|
|
2117
|
+
ir: ComponentIR,
|
|
2118
|
+
nestedComponents: NestedComponentInfo[],
|
|
2119
|
+
staticWithoutBody: NestedComponentInfo[],
|
|
2120
|
+
staticWithBody: NestedComponentInfo[],
|
|
2121
|
+
dynamicWithBody: NestedComponentInfo[],
|
|
2122
|
+
emittedWrapperVars: Set<string>,
|
|
2123
|
+
propTypeOverrides: Map<string, string>,
|
|
2124
|
+
): void {
|
|
2125
|
+
lines.push('\tbfCallerProps := map[string]interface{}{}')
|
|
2126
|
+
const bfCallerPropsTakenTags = new Set<string>()
|
|
2127
|
+
const nestedArrayFields = this.propDerivedNestedArrayFields(nestedComponents)
|
|
2128
|
+
|
|
2129
|
+
for (const param of ir.metadata.propsParams) {
|
|
2130
|
+
// Children are DOM content, not hydration data (#1952) — excluded the
|
|
2131
|
+
// same way `emitPropsDataFields` excludes them from the Props struct.
|
|
2132
|
+
if (param.name === 'children') continue
|
|
2133
|
+
// Shadowed by its own driving nested-array field (#2672/#2525) — no
|
|
2134
|
+
// Input field exists for it at all; handled below via the array local.
|
|
2135
|
+
if (this.isNestedArrayShadowed(param, nestedArrayFields)) continue
|
|
2136
|
+
// Same tag algorithm `emitPropsDataFields` uses for the Props field's
|
|
2137
|
+
// own `json:"…"` tag, re-derived against a FRESH set: props are always
|
|
2138
|
+
// processed first, in the same order, over an empty set in BOTH
|
|
2139
|
+
// places, so the two computations can't disagree.
|
|
2140
|
+
const callerKey = this.claimJsonTag(this.toJsonTag(param.sourceName ?? param.name), bfCallerPropsTakenTags)
|
|
2141
|
+
if (callerKey === '-') continue // tag collision — dropped from the wire, same as the Props field itself would be
|
|
2142
|
+
const inputField = `in.${capitalizeFieldName(param.sourceName ?? param.name)}`
|
|
2143
|
+
const goType = resolvePropGoType(this.emitCtx, param, propTypeOverrides)
|
|
2144
|
+
const isNillable = goType === 'interface{}' || goType === 'map[string]interface{}' || goType.startsWith('[]')
|
|
2145
|
+
// `param.optional` gates the branch (not "isNillable alone") because a
|
|
2146
|
+
// REQUIRED prop must stay unconditional even when its resolved type
|
|
2147
|
+
// happens to be nillable (`ctx: unknown`, #2094's array-flatmap-thisarg
|
|
2148
|
+
// fixture passes `ctx: null` explicitly) — Go's `interface{}` cannot
|
|
2149
|
+
// tell "explicit null" from "omitted" once unmarshaled (both are the
|
|
2150
|
+
// nil zero value), so a nil-check here would silently turn an
|
|
2151
|
+
// intentional `null` into a dropped key, regressing a fixture that
|
|
2152
|
+
// matched the reference before this method existed. `resolvePropGoType`
|
|
2153
|
+
// never applies its nillable flip to a required prop, so this ONLY
|
|
2154
|
+
// affects the rare case where a type resolves to `interface{}` some
|
|
2155
|
+
// OTHER way (e.g. `unknown`, or a type inherited through an
|
|
2156
|
+
// unresolved external `extends` clause) while `param.optional` is
|
|
2157
|
+
// (accurately or not) false — see the method docstring's residual list.
|
|
2158
|
+
if (param.optional && isNillable) {
|
|
2159
|
+
lines.push(`\tif ${inputField} != nil {`)
|
|
2160
|
+
lines.push(`\t\tbfCallerProps["${callerKey}"] = ${inputField}`)
|
|
2161
|
+
lines.push(`\t}`)
|
|
2162
|
+
} else {
|
|
2163
|
+
// Required (class 1), or optional-but-concrete residual (class 3),
|
|
2164
|
+
// or optional-but-misresolved-required residual — see docstring.
|
|
2165
|
+
lines.push(`\tbfCallerProps["${callerKey}"] = ${inputField}`)
|
|
2166
|
+
}
|
|
2167
|
+
}
|
|
2168
|
+
|
|
2169
|
+
// Shadowed nested-array props — see method docstring.
|
|
2170
|
+
for (const nested of [...staticWithoutBody, ...staticWithBody, ...dynamicWithBody]) {
|
|
2171
|
+
if (!nested.isPropDerived) continue
|
|
2172
|
+
const varName = `${nested.name.charAt(0).toLowerCase()}${nested.name.slice(1)}s`
|
|
2173
|
+
const isBuilt = staticWithoutBody.includes(nested) || emittedWrapperVars.has(varName)
|
|
2174
|
+
if (!isBuilt) continue
|
|
2175
|
+
const arrayFieldName = capitalizeFieldName(`${nested.name}s`)
|
|
2176
|
+
const param = ir.metadata.propsParams.find(
|
|
2177
|
+
p => capitalizeFieldName(p.name) === arrayFieldName || capitalizeFieldName(p.sourceName ?? p.name) === arrayFieldName,
|
|
2178
|
+
)
|
|
2179
|
+
if (!param || !this.isNestedArrayShadowed(param, nestedArrayFields)) continue
|
|
2180
|
+
const callerKey = this.claimJsonTag(this.toJsonTag(param.sourceName ?? param.name), bfCallerPropsTakenTags)
|
|
2181
|
+
if (callerKey === '-') continue
|
|
2182
|
+
if (param.optional) {
|
|
2183
|
+
lines.push(`\tif in.${nested.name}s != nil {`)
|
|
2184
|
+
lines.push(`\t\tbfCallerProps["${callerKey}"] = ${varName}`)
|
|
2185
|
+
lines.push(`\t}`)
|
|
2186
|
+
} else {
|
|
2187
|
+
lines.push(`\tbfCallerProps["${callerKey}"] = ${varName}`)
|
|
2188
|
+
}
|
|
2189
|
+
}
|
|
2190
|
+
lines.push('')
|
|
2191
|
+
}
|
|
2192
|
+
|
|
2041
2193
|
private emitStaticChildInstances(lines: string[], ir: ComponentIR): void {
|
|
2042
2194
|
const staticChildren = this.collectStaticChildInstances(ir.root, ir.metadata.propsParams)
|
|
2043
2195
|
for (const child of staticChildren) {
|
|
@@ -2741,6 +2893,24 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
2741
2893
|
if (this.usesSearchParams(ir)) {
|
|
2742
2894
|
lines.push('\tSearchParams bf.SearchParams `json:"-"`')
|
|
2743
2895
|
}
|
|
2896
|
+
|
|
2897
|
+
// #2684: the raw, CALLER-SUPPLIED view of this component's props —
|
|
2898
|
+
// populated by `NewXxxProps` with exactly the keys the caller passed
|
|
2899
|
+
// (required props always; optional ones only when not nil), holding
|
|
2900
|
+
// the UNDEFAULTED value. This exists because every prop field above
|
|
2901
|
+
// (e.g. `X`) serves TWO different consumers that want two different
|
|
2902
|
+
// values: the Go TEMPLATE wants the DEFAULTED value so `{{.X}}` renders
|
|
2903
|
+
// the author's fallback, while the HYDRATION PAYLOAD wants to know only
|
|
2904
|
+
// what the caller actually supplied — baking the author's default into
|
|
2905
|
+
// it makes an omitted prop indistinguishable from one explicitly passed
|
|
2906
|
+
// with that value, and a Go zero value indistinguishable from an
|
|
2907
|
+
// explicit zero/empty/false (the reference, Hono, only ever serializes
|
|
2908
|
+
// caller-passed keys). One field can't answer both questions at once,
|
|
2909
|
+
// so this sidecar gives the hydration payload its own carrier;
|
|
2910
|
+
// `BfPropsAttr` (runtime/bf.go) marshals THIS map instead of the whole
|
|
2911
|
+
// struct when it's present. `json:"-"` — never marshaled as an ordinary
|
|
2912
|
+
// struct field; `BfPropsAttr` reads it directly by name via reflection.
|
|
2913
|
+
lines.push('\tBfCallerProps map[string]interface{} `json:"-"`')
|
|
2744
2914
|
}
|
|
2745
2915
|
|
|
2746
2916
|
private emitPropsDataFields(
|
|
@@ -3236,6 +3406,10 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
3236
3406
|
if (parsed.kind !== 'object-literal') return null
|
|
3237
3407
|
const entries: string[] = []
|
|
3238
3408
|
for (const prop of parsed.properties) {
|
|
3409
|
+
// A spread (`{ ...t }`, #2696 Step 2) isn't a per-key member this Go
|
|
3410
|
+
// struct-literal lowering can express — fail the whole value, same as
|
|
3411
|
+
// any other unsupported member (the consumer keeps its default).
|
|
3412
|
+
if (prop.kind === 'spread') return null
|
|
3239
3413
|
if (prop.shorthand) return null
|
|
3240
3414
|
const goVal = this.lowerProviderMapMemberValue(prop.value, propsParams)
|
|
3241
3415
|
if (goVal === null) return null
|
|
@@ -3588,7 +3762,32 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
3588
3762
|
|
|
3589
3763
|
const propTypeOverrides = buildPropTypeOverrides(this.emitCtx, ir)
|
|
3590
3764
|
for (const signal of ir.metadata.signals) {
|
|
3591
|
-
|
|
3765
|
+
let match = this.extractPropFallback(signal.initialValue, this.resolvedSignalParsed(signal))
|
|
3766
|
+
// #2683: the idempotent fold above declines a non-idempotent
|
|
3767
|
+
// derivation (`(props.count ?? 1) * 2`) at the top level. When that
|
|
3768
|
+
// signal's Go field name COLLIDES with the prop's own field — the
|
|
3769
|
+
// exact condition under which the props-field loop below would
|
|
3770
|
+
// otherwise silently drop the derivation and emit the raw prop
|
|
3771
|
+
// instead — fall back to the collision-derivation matcher so the
|
|
3772
|
+
// coalesced value still gets hoisted; the props-field loop composes
|
|
3773
|
+
// the surrounding arithmetic back on top of it (`collisionWrap`).
|
|
3774
|
+
// Gated strictly on the collision so a differently-named signal
|
|
3775
|
+
// deriving from the same prop (not colliding) is untouched — that
|
|
3776
|
+
// path's own dedicated field keeps today's behavior byte-for-byte.
|
|
3777
|
+
let collisionWrap: { operator: string; operand: string } | undefined
|
|
3778
|
+
if (!match) {
|
|
3779
|
+
const collision = this.extractCollisionDerivation(this.resolvedSignalParsed(signal))
|
|
3780
|
+
if (collision) {
|
|
3781
|
+
const collisionParam = ir.metadata.propsParams.find(p => p.name === collision.propName)
|
|
3782
|
+
const collisionField = collisionParam
|
|
3783
|
+
? capitalizeFieldName(collisionParam.sourceName ?? collision.propName)
|
|
3784
|
+
: null
|
|
3785
|
+
if (collisionField && capitalizeFieldName(signal.getter) === collisionField) {
|
|
3786
|
+
match = { propName: collision.propName, goFallback: collision.goFallback }
|
|
3787
|
+
collisionWrap = { operator: collision.operator, operand: collision.operand }
|
|
3788
|
+
}
|
|
3789
|
+
}
|
|
3790
|
+
}
|
|
3592
3791
|
if (!match) continue
|
|
3593
3792
|
if (result.has(match.propName)) continue
|
|
3594
3793
|
const param = ir.metadata.propsParams.find(p => p.name === match.propName)
|
|
@@ -3648,6 +3847,7 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
3648
3847
|
goFallback: match.goFallback,
|
|
3649
3848
|
zeroLiteral,
|
|
3650
3849
|
...(nullishLowered ? { assertType: concreteType } : {}),
|
|
3850
|
+
...(collisionWrap ? { collisionWrap } : {}),
|
|
3651
3851
|
})
|
|
3652
3852
|
}
|
|
3653
3853
|
return result
|
|
@@ -3742,6 +3942,67 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
3742
3942
|
return { propName, goFallback }
|
|
3743
3943
|
}
|
|
3744
3944
|
|
|
3945
|
+
/**
|
|
3946
|
+
* #2683: match a signal's collision-derivation shape — `(props.X ?? <lit>)
|
|
3947
|
+
* <op> <int>` — the ONE non-idempotent shape this adapter faithfully lowers
|
|
3948
|
+
* when a signal's Go field name collides with its own prop's field (see the
|
|
3949
|
+
* collision-override loop in `collectPropFallbackVars` / the props-field
|
|
3950
|
+
* loop in `generateNewPropsFunction`). Composes two ALREADY-EXISTING
|
|
3951
|
+
* lowerings instead of adding a third: `extractPropFallbackFromParsed`
|
|
3952
|
+
* recognizes the embedded `props.X ?? <lit>` presence check exactly as it
|
|
3953
|
+
* does for the idempotent fold, and the wrap mirrors the SAME `<ref> <op>
|
|
3954
|
+
* <int>` arithmetic shape `memoInitialFromParsedBody`'s multiply-family
|
|
3955
|
+
* branch already supports for a bare `props.X <op> N` (non-negative
|
|
3956
|
+
* integer operand only, matching that branch's own restriction).
|
|
3957
|
+
*
|
|
3958
|
+
* Returns null for any other shape — the caller then keeps today's
|
|
3959
|
+
* raw-passthrough behavior (sound-or-loud; the remaining #2683 pin, if
|
|
3960
|
+
* any, covers whatever this doesn't reach).
|
|
3961
|
+
*/
|
|
3962
|
+
private extractCollisionDerivation(
|
|
3963
|
+
parsed: ParsedExpr | undefined,
|
|
3964
|
+
): { propName: string; goFallback: string; operator: string; operand: string } | null {
|
|
3965
|
+
if (!parsed || parsed.kind !== 'binary') return null
|
|
3966
|
+
if (!['*', '+', '-', '/'].includes(parsed.op)) return null
|
|
3967
|
+
const { right } = parsed
|
|
3968
|
+
if (
|
|
3969
|
+
right.kind !== 'literal' ||
|
|
3970
|
+
right.literalType !== 'number' ||
|
|
3971
|
+
typeof right.value !== 'number' ||
|
|
3972
|
+
!Number.isInteger(right.value) ||
|
|
3973
|
+
right.value < 0
|
|
3974
|
+
) {
|
|
3975
|
+
return null
|
|
3976
|
+
}
|
|
3977
|
+
// Constant division by zero is a Go COMPILE error (`invalid operation:
|
|
3978
|
+
// division by zero`), unlike JS's Infinity — declining keeps that shape
|
|
3979
|
+
// on the raw-passthrough path instead of breaking the build.
|
|
3980
|
+
if (parsed.op === '/' && right.value === 0) return null
|
|
3981
|
+
// The `??` fallback itself must be a NUMERIC literal before delegating
|
|
3982
|
+
// to the fold: `extractPropFallbackFromParsed` happily matches a string
|
|
3983
|
+
// fallback (`(props.label ?? 'x') + 2`), which would hoist
|
|
3984
|
+
// `var label string = "x"` and emit `Label: label + 2,` — invalid Go
|
|
3985
|
+
// (string + int), a compile break where the pre-existing behavior at
|
|
3986
|
+
// least built (Copilot review on #2694). JS's `'x' + 2` is string
|
|
3987
|
+
// concatenation besides, so a numeric compose could never be faithful
|
|
3988
|
+
// for it — the shape belongs on the raw-passthrough path until someone
|
|
3989
|
+
// lowers it through ConcatStr deliberately. Gating on `??` (not `||`)
|
|
3990
|
+
// also keeps JS nullish semantics exact for numeric props, where a
|
|
3991
|
+
// caller's explicit `0` must NOT trigger the fallback.
|
|
3992
|
+
const coalesce = parsed.left
|
|
3993
|
+
if (
|
|
3994
|
+
coalesce.kind !== 'logical' ||
|
|
3995
|
+
coalesce.op !== '??' ||
|
|
3996
|
+
coalesce.right.kind !== 'literal' ||
|
|
3997
|
+
coalesce.right.literalType !== 'number'
|
|
3998
|
+
) {
|
|
3999
|
+
return null
|
|
4000
|
+
}
|
|
4001
|
+
const inner = this.extractPropFallbackFromParsed(parsed.left)
|
|
4002
|
+
if (!inner) return null
|
|
4003
|
+
return { ...inner, operator: parsed.op, operand: String(right.value) }
|
|
4004
|
+
}
|
|
4005
|
+
|
|
3745
4006
|
/**
|
|
3746
4007
|
* Extract the prop name from a signal's `props.xxx`-pattern initialValue,
|
|
3747
4008
|
* e.g. `"props.initial ?? 0"` → `"initial"`, `"props.checked"` → `"checked"`.
|
|
@@ -4739,12 +5000,33 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
4739
5000
|
return `bf_arr ${parts.join(' ')}`
|
|
4740
5001
|
}
|
|
4741
5002
|
|
|
4742
|
-
objectLiteral(
|
|
4743
|
-
//
|
|
4744
|
-
// (
|
|
4745
|
-
//
|
|
4746
|
-
//
|
|
4747
|
-
//
|
|
5003
|
+
objectLiteral(properties: ObjectLiteralProperty[], raw: string, emit: (e: ParsedExpr) => string): string {
|
|
5004
|
+
// A POPULATED literal is reachable here only in VALUE position (a
|
|
5005
|
+
// `.map()` receiver/callback body, an array-literal element, …) —
|
|
5006
|
+
// `isSupportedValue` admits it when every property value is itself
|
|
5007
|
+
// supported (expression-parser.ts, `checkSupport`'s `pos` parameter).
|
|
5008
|
+
// `text/template` has no map-literal syntax, so lower through the
|
|
5009
|
+
// variadic `bf_map` runtime helper (the object counterpart of
|
|
5010
|
+
// `arrayLiteral`'s `bf_arr`) instead of trying to spell a literal.
|
|
5011
|
+
if (properties.length > 0) {
|
|
5012
|
+
const wrap = (rendered: string) => (rendered.includes(' ') ? `(${rendered})` : rendered)
|
|
5013
|
+
const literalOf = (run: readonly Extract<ObjectLiteralProperty, { kind: 'prop' }>[]) =>
|
|
5014
|
+
`bf_map ${run.map(p => `"${escapeGoString(p.key)}" ${wrap(emit(p.value))}`).join(' ')}`
|
|
5015
|
+
if (!properties.some(p => p.kind === 'spread')) {
|
|
5016
|
+
return literalOf(properties as Extract<ObjectLiteralProperty, { kind: 'prop' }>[])
|
|
5017
|
+
}
|
|
5018
|
+
// Spread (`{ ...t, editing: false }`, #2696 Step 2): `bf_merge`
|
|
5019
|
+
// (runtime/eval.go, a `bf_map` sibling) is a variadic, null-safe map
|
|
5020
|
+
// merge — a non-map argument (nil included) is skipped rather than
|
|
5021
|
+
// panicking, matching JS's null/undefined-spread no-op — folding
|
|
5022
|
+
// every segment (a `bf_map` run, or a spread's own emitted value) in
|
|
5023
|
+
// ONE call, later segments winning, exactly JS spread's semantics.
|
|
5024
|
+
const segments = groupObjectLiteralSegments(properties, literalOf, e => wrap(emit(e)))
|
|
5025
|
+
return `bf_merge ${segments.join(' ')}`
|
|
5026
|
+
}
|
|
5027
|
+
// The EMPTY object literal (`?? {}`) reaches here as `??`'s right
|
|
5028
|
+
// operand (expression-parser.ts, `logical` case) — a RENDERED-position
|
|
5029
|
+
// admission, not a value-position one. Unlike the sibling adapters, Go
|
|
4748
5030
|
// can't silently fall back to a safe sentinel text: `this.unsupported`'s
|
|
4749
5031
|
// `[UNSUPPORTED: …]` marker would be spliced into a Go template action
|
|
4750
5032
|
// (e.g. as an `or`/`and` operand) and break template parsing, and the
|
|
@@ -6054,7 +6336,12 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
6054
6336
|
// (icon registries, variant/size class maps), so this fully covers them.
|
|
6055
6337
|
const carried = constInfo.parsed
|
|
6056
6338
|
if (carried?.kind === 'object-literal') {
|
|
6057
|
-
|
|
6339
|
+
// A spread entry (`{ ...t }`, #2696 Step 2) has no static key to match
|
|
6340
|
+
// against — skip it, same as before this kind existed (a record const
|
|
6341
|
+
// containing spread already couldn't reach here as a whole).
|
|
6342
|
+
const hit = carried.properties.find(
|
|
6343
|
+
(prop): prop is Extract<ObjectLiteralProperty, { kind: 'prop' }> => prop.kind === 'prop' && prop.key === key,
|
|
6344
|
+
)
|
|
6058
6345
|
if (hit && hit.value.kind === 'literal') {
|
|
6059
6346
|
if (hit.value.literalType === 'string') return JSON.stringify(hit.value.value)
|
|
6060
6347
|
if (hit.value.literalType === 'number') return hit.value.raw ?? String(hit.value.value)
|
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
|
/**
|
|
@@ -592,6 +592,17 @@ export function memoInitialFromParsedBody(
|
|
|
592
592
|
if (concatGo !== null) return concatGo
|
|
593
593
|
}
|
|
594
594
|
|
|
595
|
+
// () => <object>.map(cb).join(sep) as the memo's WHOLE body (no `+`
|
|
596
|
+
// concatenation) — `map-object-literal-body`'s `ids` memo: `rows().map(t
|
|
597
|
+
// => ({ id: t.id, done: false })).map(r => r.id).join(',')`. The `+`-chain
|
|
598
|
+
// arm above only reaches `matchMapJoinChain` as ONE leaf of a
|
|
599
|
+
// concatenation; a bare chain memo never entered it (#2696 review).
|
|
600
|
+
const bareChain = matchMapJoinChain(body)
|
|
601
|
+
if (bareChain) {
|
|
602
|
+
const chainGo = mapJoinChainToGo(ctx, bareChain, signals, propsParams, propFallbackVars)
|
|
603
|
+
if (chainGo !== null) return chainGo
|
|
604
|
+
}
|
|
605
|
+
|
|
595
606
|
return null
|
|
596
607
|
}
|
|
597
608
|
|
|
@@ -954,7 +965,7 @@ export function collectPropsReadByCtorInit(
|
|
|
954
965
|
e.elements.forEach(el => visit(el, bound))
|
|
955
966
|
return
|
|
956
967
|
case 'object-literal':
|
|
957
|
-
for (const p of e.properties) visit(p.value, bound)
|
|
968
|
+
for (const p of e.properties) visit(p.kind === 'spread' ? p.expr : p.value, bound)
|
|
958
969
|
return
|
|
959
970
|
case 'array-method':
|
|
960
971
|
visit(e.object, bound)
|
|
@@ -181,6 +181,9 @@ export function computeObjectMemoInitialValue(
|
|
|
181
181
|
const env: CtorLowerEnv = { searchParamsVars: new Set(), params: new Map() }
|
|
182
182
|
const entries: string[] = []
|
|
183
183
|
for (const prop of retObj.properties) {
|
|
184
|
+
// A spread (`{ ...t }`, #2696 Step 2) has no per-key `.Params.<Field>`
|
|
185
|
+
// accessor to match against — bail, same as any other unsupported shape.
|
|
186
|
+
if (prop.kind === 'spread') return null
|
|
184
187
|
// Bail on a shorthand property (`return { tag }`): its value is a bare
|
|
185
188
|
// identifier whose name need not match a `.Params.<Field>` accessor.
|
|
186
189
|
if (prop.shorthand) return null
|
|
@@ -200,6 +200,11 @@ function recordIndexInterpolationToGo(
|
|
|
200
200
|
|
|
201
201
|
const entries: { key: string; value: { kind: 'number' | 'string'; text: string } }[] = []
|
|
202
202
|
for (const prop of parsedConst.properties) {
|
|
203
|
+
// A spread (`{ ...t }`, #2696 Step 2) has no static key/value pair this
|
|
204
|
+
// record-index lowering can bake — bail, same as any other unsupported
|
|
205
|
+
// member (a record const containing spread already couldn't reach here
|
|
206
|
+
// as a whole before this kind existed).
|
|
207
|
+
if (prop.kind === 'spread') return null
|
|
203
208
|
const v = prop.value
|
|
204
209
|
if (v.kind === 'literal' && v.literalType === 'number') {
|
|
205
210
|
entries.push({ key: prop.key, value: { kind: 'number', text: v.raw ?? String(v.value) } })
|
|
@@ -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
|
|
@@ -143,6 +143,10 @@ function parsedObjectLiteralToGoMap(parsed: ParsedExpr | undefined): string | nu
|
|
|
143
143
|
if (!parsed || parsed.kind !== 'object-literal') return null
|
|
144
144
|
const entries: string[] = []
|
|
145
145
|
for (const prop of parsed.properties) {
|
|
146
|
+
// A spread (`{ ...t }`, #2696 Step 2) has no static key/value pair this
|
|
147
|
+
// conservative Go-map bake can express — bail, same as any other
|
|
148
|
+
// unsupported shape.
|
|
149
|
+
if (prop.kind === 'spread') return null
|
|
146
150
|
// Reject a numeric key (`{ 1: 'a' }`); `keyKind` distinguishes it from a
|
|
147
151
|
// string `'1'` key.
|
|
148
152
|
if (prop.keyKind === 'numeric') return null
|
|
@@ -386,6 +390,10 @@ function objectLiteralToGoSpreadMap(
|
|
|
386
390
|
): string | null {
|
|
387
391
|
const entries: string[] = []
|
|
388
392
|
for (const prop of obj.properties) {
|
|
393
|
+
// A spread (`{ ...t }`, #2696 Step 2) isn't a per-key entry this
|
|
394
|
+
// conditional-spread inline lowering can express — bail (the docstring
|
|
395
|
+
// already declares spread out-of-scope for this function).
|
|
396
|
+
if (prop.kind === 'spread') return null
|
|
389
397
|
// Shorthand (`{ describedBy }`) is unsupported.
|
|
390
398
|
if (prop.shorthand) return null
|
|
391
399
|
// Reject a numeric key (`{ 1: x }`); `keyKind` distinguishes it from a
|
|
@@ -63,6 +63,9 @@ function bakeInlineObjectAsGoMap(ctx: GoEmitContext, expr: ParsedExpr): string |
|
|
|
63
63
|
if (expr.kind !== 'object-literal') return null
|
|
64
64
|
const entries: string[] = []
|
|
65
65
|
for (const prop of expr.properties) {
|
|
66
|
+
// A spread (`{ ...t }`, #2696 Step 2) has no static key to bake as a Go
|
|
67
|
+
// map entry — bail, same as any other unsupported shape.
|
|
68
|
+
if (prop.kind === 'spread') return null
|
|
66
69
|
if (prop.shorthand) return null
|
|
67
70
|
const go =
|
|
68
71
|
prop.value.kind === 'object-literal'
|
|
@@ -150,6 +153,10 @@ export function parsedLiteralToGo(
|
|
|
150
153
|
if (!structFields) return null
|
|
151
154
|
const entries: string[] = []
|
|
152
155
|
for (const prop of expr.properties) {
|
|
156
|
+
// A spread (`{ ...t }`, #2696 Step 2) has no static key to match a
|
|
157
|
+
// struct field against — defer the whole object, same as any other
|
|
158
|
+
// unsupported shape.
|
|
159
|
+
if (prop.kind === 'spread') return null
|
|
153
160
|
// A shorthand `{ a }` carries an identifier value → lowers to null below
|
|
154
161
|
// and defers the whole object.
|
|
155
162
|
const goField = structFields.get(prop.key)
|