@barefootjs/go-template 0.18.7 → 0.19.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 +11 -5
- package/dist/adapter/emit-context.d.ts.map +1 -1
- package/dist/adapter/go-template-adapter.d.ts +37 -4
- package/dist/adapter/go-template-adapter.d.ts.map +1 -1
- package/dist/adapter/index.js +175 -21
- package/dist/adapter/lib/compile-state.d.ts +16 -0
- package/dist/adapter/lib/compile-state.d.ts.map +1 -1
- package/dist/adapter/lib/types.d.ts +9 -0
- package/dist/adapter/lib/types.d.ts.map +1 -1
- package/dist/adapter/props/prop-types.d.ts +45 -0
- package/dist/adapter/props/prop-types.d.ts.map +1 -1
- package/dist/build.js +175 -21
- package/dist/index.js +175 -21
- package/package.json +3 -3
- package/src/__tests__/go-template-adapter.test.ts +169 -18
- package/src/adapter/emit-context.ts +14 -5
- package/src/adapter/go-template-adapter.ts +185 -20
- package/src/adapter/lib/compile-state.ts +18 -0
- package/src/adapter/lib/types.ts +9 -0
- package/src/adapter/props/prop-types.ts +178 -1
- package/src/adapter/value/value-lowering.ts +1 -1
- package/src/test-render.ts +18 -4
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import type { ComponentIR, IRMetadata, IRNode, ParsedExpr } from '@barefootjs/jsx'
|
|
9
|
+
import { isBooleanAttr } from '@barefootjs/jsx'
|
|
9
10
|
|
|
10
11
|
import type { GoEmitContext } from '../emit-context.ts'
|
|
11
12
|
import { typeInfoToGo } from '../type/type-codegen.ts'
|
|
@@ -20,7 +21,7 @@ export function buildPropTypeOverrides(ctx: GoEmitContext, ir: ComponentIR): Map
|
|
|
20
21
|
const overrides = new Map<string, string>()
|
|
21
22
|
for (const signal of ir.metadata.signals) {
|
|
22
23
|
const propNames = [signal.initialValue]
|
|
23
|
-
const extracted = ctx.extractPropNameFromInitialValue(signal.initialValue)
|
|
24
|
+
const extracted = ctx.extractPropNameFromInitialValue(signal.initialValue, signal.parsed)
|
|
24
25
|
if (extracted) propNames.push(extracted)
|
|
25
26
|
|
|
26
27
|
for (const propName of propNames) {
|
|
@@ -106,6 +107,151 @@ function collectToFixedPropNames(root: IRNode): Set<string> {
|
|
|
106
107
|
return names
|
|
107
108
|
}
|
|
108
109
|
|
|
110
|
+
/**
|
|
111
|
+
* Concrete scalar Go types eligible for the nullish (`??`) nillable flip in
|
|
112
|
+
* `resolvePropGoType`. Only these can round-trip through an `interface{}`
|
|
113
|
+
* field and back via a constructor type assertion / `bf.ToInt`-style
|
|
114
|
+
* coercion (see the fallback-var emission in `generateNewPropsFunction`).
|
|
115
|
+
*/
|
|
116
|
+
export const NULLISH_SCALAR_GO_TYPES: ReadonlySet<string> = new Set(['string', 'int', 'float64', 'bool'])
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Names of OPTIONAL props consumed nullish-sensitively — the left operand of
|
|
120
|
+
* a `??` anywhere in the component's parsed expression trees, or a signal's
|
|
121
|
+
* `props.X ?? <literal>` initial value (#2248).
|
|
122
|
+
*
|
|
123
|
+
* Why this matters: JS `??` falls back only on `null`/`undefined`, keeping
|
|
124
|
+
* `''`/`0`/`false`. A Go zero-valued scalar field cannot represent "absent",
|
|
125
|
+
* so a `??`-consumed optional scalar must lower to the adapter's established
|
|
126
|
+
* nillable representation (`interface{}`) for the distinction to exist at
|
|
127
|
+
* render time. `resolvePropGoType` applies that flip using this set.
|
|
128
|
+
*
|
|
129
|
+
* The IR walk is generic (any nested object/array is descended, so parsed
|
|
130
|
+
* trees inside expressions, conditions, attributes, and loops are all seen)
|
|
131
|
+
* and the match is shape-precise: `identifier` left (destructured props) or
|
|
132
|
+
* `<propsObject>.<name>` member left. KNOWN LIMITATION (same class as
|
|
133
|
+
* `collectToFixedPropNames`): an `identifier` left shadowed by a same-named
|
|
134
|
+
* loop/callback param is misattributed to the prop — the flip is then merely
|
|
135
|
+
* unnecessary, not incorrect.
|
|
136
|
+
*/
|
|
137
|
+
export function collectNullishConsumedPropNames(ctx: GoEmitContext, ir: ComponentIR): Set<string> {
|
|
138
|
+
const names = new Set<string>()
|
|
139
|
+
// A destructure default (`{ className = '' }`) means the binding is never
|
|
140
|
+
// nullish in JS — the default already applied — so such props never need
|
|
141
|
+
// the nillable flip (and `applyGoFallback`'s concrete-typed baking relies
|
|
142
|
+
// on them staying concrete).
|
|
143
|
+
const optionalParams = new Set(
|
|
144
|
+
ir.metadata.propsParams.filter(p => p.optional && p.defaultValue == null).map(p => p.name),
|
|
145
|
+
)
|
|
146
|
+
if (optionalParams.size === 0) return names
|
|
147
|
+
|
|
148
|
+
const propsObject = ctx.state.propsObjectName
|
|
149
|
+
const propNameOfLeft = (left: ParsedExpr): string | null => {
|
|
150
|
+
if (left.kind === 'identifier') return left.name
|
|
151
|
+
if (
|
|
152
|
+
left.kind === 'member' &&
|
|
153
|
+
!left.computed &&
|
|
154
|
+
left.object.kind === 'identifier' &&
|
|
155
|
+
left.object.name === propsObject
|
|
156
|
+
) {
|
|
157
|
+
return left.property
|
|
158
|
+
}
|
|
159
|
+
return null
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// `?? <zero-equivalent literal>` (`?? ''`, `?? 0`, `?? false`) is the one
|
|
163
|
+
// shape where nullish and truthiness semantics coincide — nil and the zero
|
|
164
|
+
// value both land on the same output — so it earns no flip. Only a
|
|
165
|
+
// fallback the zero value must NOT collapse into makes the distinction
|
|
166
|
+
// observable.
|
|
167
|
+
const isZeroEquivalentLiteral = (right: ParsedExpr): boolean =>
|
|
168
|
+
right.kind === 'literal' &&
|
|
169
|
+
(right.value === '' || right.value === false || right.value === null ||
|
|
170
|
+
(right.literalType === 'number' && Number(right.value) === 0))
|
|
171
|
+
|
|
172
|
+
const walk = (node: unknown): void => {
|
|
173
|
+
if (!node || typeof node !== 'object') return
|
|
174
|
+
if (Array.isArray(node)) {
|
|
175
|
+
for (const item of node) walk(item)
|
|
176
|
+
return
|
|
177
|
+
}
|
|
178
|
+
const rec = node as Record<string, unknown>
|
|
179
|
+
if (rec.kind === 'logical' && rec.op === '??' && rec.left && rec.right) {
|
|
180
|
+
const propName = propNameOfLeft(rec.left as ParsedExpr)
|
|
181
|
+
if (propName && optionalParams.has(propName) && !isZeroEquivalentLiteral(rec.right as ParsedExpr)) {
|
|
182
|
+
names.add(propName)
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
for (const value of Object.values(rec)) walk(value)
|
|
186
|
+
}
|
|
187
|
+
walk(ir.root)
|
|
188
|
+
|
|
189
|
+
// Signal seeds (`createSignal(props.X ?? 1)`) live in metadata, not the
|
|
190
|
+
// tree — reuse the seam's existing `props.X ?? <literal>` recognizer. The
|
|
191
|
+
// zero-equivalent exclusion matches on the Go-formatted fallback literal.
|
|
192
|
+
for (const signal of ir.metadata.signals) {
|
|
193
|
+
const match = ctx.extractPropFallback(signal.initialValue, signal.parsed)
|
|
194
|
+
if (!match || !optionalParams.has(match.propName)) continue
|
|
195
|
+
const f = match.goFallback
|
|
196
|
+
if (f === '""' || f === 'false' || f === 'nil' || Number(f) === 0) continue
|
|
197
|
+
names.add(match.propName)
|
|
198
|
+
}
|
|
199
|
+
return names
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Names of OPTIONAL no-default props consumed as the BARE value of an
|
|
204
|
+
* omittable attribute (`rows={rows}` / `rows={props.rows}`) anywhere in the
|
|
205
|
+
* component's element tree.
|
|
206
|
+
*
|
|
207
|
+
* Why this matters: Hono omits an attribute whose value is `undefined`, and
|
|
208
|
+
* the adapter's `emitExpression` mirrors that with a `{{if ne .X nil}}` guard
|
|
209
|
+
* — which needs a nillable field to test. A concrete scalar field would
|
|
210
|
+
* render the zero value (`rows="0"`) instead of dropping the attribute, so
|
|
211
|
+
* these props take the same `interface{}` flip as `??` consumption
|
|
212
|
+
* (`resolvePropGoType`). The match mirrors `emitExpression`'s guard branch:
|
|
213
|
+
* bare identifiers / `<propsObject>.<name>` only, skipping `class`/`style`
|
|
214
|
+
* (own lowerings) and boolean/presence attrs (truthiness-tested, where a nil
|
|
215
|
+
* and a zero value already coincide). Same shadowing caveat as
|
|
216
|
+
* `collectNullishConsumedPropNames`: a same-named loop/callback param can
|
|
217
|
+
* misattribute, making the flip merely unnecessary, not incorrect.
|
|
218
|
+
*/
|
|
219
|
+
export function collectOmittableAttrConsumedPropNames(ctx: GoEmitContext, ir: ComponentIR): Set<string> {
|
|
220
|
+
const names = new Set<string>()
|
|
221
|
+
const optionalParams = new Set(
|
|
222
|
+
ir.metadata.propsParams.filter(p => p.optional && p.defaultValue == null).map(p => p.name),
|
|
223
|
+
)
|
|
224
|
+
if (optionalParams.size === 0) return names
|
|
225
|
+
|
|
226
|
+
const propsObject = ctx.state.propsObjectName
|
|
227
|
+
const walk = (node: unknown): void => {
|
|
228
|
+
if (!node || typeof node !== 'object') return
|
|
229
|
+
if (Array.isArray(node)) {
|
|
230
|
+
for (const item of node) walk(item)
|
|
231
|
+
return
|
|
232
|
+
}
|
|
233
|
+
const rec = node as Record<string, unknown>
|
|
234
|
+
if (rec.type === 'element' && Array.isArray(rec.attrs)) {
|
|
235
|
+
for (const attr of rec.attrs as Array<{ name: string; value: Record<string, unknown> }>) {
|
|
236
|
+
if (attr.name === 'class' || attr.name === 'className' || attr.name === 'style') continue
|
|
237
|
+
if (isBooleanAttr(attr.name)) continue
|
|
238
|
+
if (attr.value?.kind !== 'expression' || attr.value.presenceOrUndefined) continue
|
|
239
|
+
const bareId = String(attr.value.expr ?? '').trim()
|
|
240
|
+
const propName =
|
|
241
|
+
propsObject && bareId.startsWith(`${propsObject}.`)
|
|
242
|
+
? bareId.slice(propsObject.length + 1)
|
|
243
|
+
: bareId
|
|
244
|
+
if (/^[A-Za-z_$][\w$]*$/.test(propName) && optionalParams.has(propName)) {
|
|
245
|
+
names.add(propName)
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
for (const value of Object.values(rec)) walk(value)
|
|
250
|
+
}
|
|
251
|
+
walk(ir.root)
|
|
252
|
+
return names
|
|
253
|
+
}
|
|
254
|
+
|
|
109
255
|
/**
|
|
110
256
|
* Resolve a prop param's Go struct-field type using the SAME logic
|
|
111
257
|
* `generatePropsStruct` / `generateInputStruct` use: a `propTypeOverrides` entry
|
|
@@ -131,6 +277,37 @@ export function resolvePropGoType(
|
|
|
131
277
|
if (param.optional && ctx.state.localStructFields.has(base)) {
|
|
132
278
|
return 'map[string]interface{}'
|
|
133
279
|
}
|
|
280
|
+
// An OPTIONAL scalar prop consumed by `??` lowers to `interface{}` (#2248):
|
|
281
|
+
// a zero-valued `string`/`int` field cannot distinguish "absent" from
|
|
282
|
+
// `''`/`0`, so JS nullish semantics (which KEEP `''`/`0`) are unexpressible
|
|
283
|
+
// on the concrete type. `interface{}` is the adapter's established nillable
|
|
284
|
+
// representation; the `??` template lowering then tests nil-ness via
|
|
285
|
+
// `bf_nullish` and the constructor seeds via a nil check + assertion.
|
|
286
|
+
// Assignment ergonomics are unchanged (plain literals assign into
|
|
287
|
+
// `interface{}`), and the prop joins the existing nillable behaviours
|
|
288
|
+
// (bare-attribute omission) by construction.
|
|
289
|
+
//
|
|
290
|
+
// Gated to `kind: 'primitive'` — a string-union prop
|
|
291
|
+
// (`placement?: 'top' | 'bottom'`) must stay its scalar Go type (the same
|
|
292
|
+
// invariant the struct-map gate above documents): the union-typed
|
|
293
|
+
// class-composition lowerings key off the concrete type, AND the flip
|
|
294
|
+
// would buy nothing — the zero value (`""`) is never a legal union
|
|
295
|
+
// member, so the zero-check already coincides with nullish semantics for
|
|
296
|
+
// every valid input. (A literal-number union containing 0 would be the
|
|
297
|
+
// exception; none exists in the corpus and the conflation is the
|
|
298
|
+
// documented pre-#2248 trade-off there.)
|
|
299
|
+
// A bare-attribute consumption (`rows={rows}`) takes the same flip (#2259):
|
|
300
|
+
// attribute omission for an absent optional needs a nil to test — see
|
|
301
|
+
// `collectOmittableAttrConsumedPropNames`.
|
|
302
|
+
if (
|
|
303
|
+
param.optional &&
|
|
304
|
+
param.type.kind === 'primitive' &&
|
|
305
|
+
(ctx.state.nullishConsumedPropNames.has(param.name) ||
|
|
306
|
+
ctx.state.omittableAttrConsumedPropNames.has(param.name)) &&
|
|
307
|
+
NULLISH_SCALAR_GO_TYPES.has(base)
|
|
308
|
+
) {
|
|
309
|
+
return 'interface{}'
|
|
310
|
+
}
|
|
134
311
|
return base
|
|
135
312
|
}
|
|
136
313
|
|
|
@@ -32,7 +32,7 @@ export function convertInitialValue(
|
|
|
32
32
|
}
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
-
const propName = ctx.extractPropNameFromInitialValue(value)
|
|
35
|
+
const propName = ctx.extractPropNameFromInitialValue(value, preParsed)
|
|
36
36
|
if (propName && propsParams?.some(p => p.name === propName)) {
|
|
37
37
|
return `in.${capitalizeFieldName(propName)}`
|
|
38
38
|
}
|
package/src/test-render.ts
CHANGED
|
@@ -699,7 +699,7 @@ function buildGoPropsInit(
|
|
|
699
699
|
// not the naive `Id`) — see `goMapLiteralFromObject`'s identical fix.
|
|
700
700
|
const goField = capitalizeFieldName(key)
|
|
701
701
|
if (typeof value === 'string') {
|
|
702
|
-
lines.push(`\t\t${goField}:
|
|
702
|
+
lines.push(`\t\t${goField}: ${goStringLit(value)},`)
|
|
703
703
|
} else if (typeof value === 'number') {
|
|
704
704
|
lines.push(`\t\t${goField}: ${value},`)
|
|
705
705
|
} else if (typeof value === 'boolean') {
|
|
@@ -828,6 +828,20 @@ function goTypedMapSliceLiteralFromArray(arr: unknown[], elemType: string): stri
|
|
|
828
828
|
* names. Only the keys the caller supplied are set, so an omitted optional prop
|
|
829
829
|
* (e.g. `defaultOn` on the third toggle item) takes the Go zero value. (#1297)
|
|
830
830
|
*/
|
|
831
|
+
/**
|
|
832
|
+
* Emit a JS string as a Go interpreted string literal. JSON string
|
|
833
|
+
* escaping is a subset of Go's (`\"`, `\\`, `\n`, `\uXXXX` are all valid
|
|
834
|
+
* Go escapes), so `JSON.stringify` is a correct emitter — unlike the
|
|
835
|
+
* previous quote-only `.replace(/"/g, '\\"')`, it also survives
|
|
836
|
+
* backslashes, newlines, and control characters (data-point conformance
|
|
837
|
+
* caught the quote case: a `"` in a string prop broke the generated
|
|
838
|
+
* `main.go` at compile time). Lone surrogates emit as `\uDXXX`, which Go
|
|
839
|
+
* rejects — acceptable until a fixture needs malformed-UTF-16 props.
|
|
840
|
+
*/
|
|
841
|
+
function goStringLit(v: string): string {
|
|
842
|
+
return JSON.stringify(v)
|
|
843
|
+
}
|
|
844
|
+
|
|
831
845
|
function goStructLiteral(obj: Record<string, unknown>, typeName: string): string {
|
|
832
846
|
const fields: string[] = []
|
|
833
847
|
for (const [k, v] of Object.entries(obj)) {
|
|
@@ -836,7 +850,7 @@ function goStructLiteral(obj: Record<string, unknown>, typeName: string): string
|
|
|
836
850
|
// adapter's own struct-literal baking (`parsed-literal-to-go.ts`)
|
|
837
851
|
// sanitizes those to `DataX`, not `Data-x` (Copilot review, #2202).
|
|
838
852
|
const goField = goFieldNameForKey(k)
|
|
839
|
-
if (typeof v === 'string') fields.push(`${goField}:
|
|
853
|
+
if (typeof v === 'string') fields.push(`${goField}: ${goStringLit(v)}`)
|
|
840
854
|
else if (typeof v === 'number' || typeof v === 'boolean') fields.push(`${goField}: ${v}`)
|
|
841
855
|
else if (v === null) fields.push(`${goField}: nil`)
|
|
842
856
|
else if (Array.isArray(v)) fields.push(`${goField}: ${goArrayLiteralFromArray(v)}`)
|
|
@@ -848,7 +862,7 @@ function goStructLiteral(obj: Record<string, unknown>, typeName: string): string
|
|
|
848
862
|
function goArrayLiteralFromArray(arr: unknown[]): string {
|
|
849
863
|
const entries: string[] = []
|
|
850
864
|
for (const v of arr) {
|
|
851
|
-
if (typeof v === 'string') entries.push(
|
|
865
|
+
if (typeof v === 'string') entries.push(goStringLit(v))
|
|
852
866
|
else if (typeof v === 'number') entries.push(String(v))
|
|
853
867
|
else if (typeof v === 'boolean') entries.push(String(v))
|
|
854
868
|
else if (v === null) entries.push('nil')
|
|
@@ -886,7 +900,7 @@ function goMapLiteralFromObject(
|
|
|
886
900
|
// uses for exactly this key-to-Go-field sanitization.
|
|
887
901
|
const emittedKey = capitalizeKeys ? goFieldNameForKey(k) : k
|
|
888
902
|
const key = JSON.stringify(emittedKey)
|
|
889
|
-
if (typeof v === 'string') entries.push(`${key}:
|
|
903
|
+
if (typeof v === 'string') entries.push(`${key}: ${goStringLit(v)}`)
|
|
890
904
|
else if (typeof v === 'number') entries.push(`${key}: ${v}`)
|
|
891
905
|
else if (typeof v === 'boolean') entries.push(`${key}: ${v}`)
|
|
892
906
|
else if (v === null) entries.push(`${key}: nil`)
|