@barefootjs/go-template 0.19.0 → 0.20.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/go-template-adapter.d.ts +8 -2
- package/dist/adapter/go-template-adapter.d.ts.map +1 -1
- package/dist/adapter/index.js +156 -22
- package/dist/adapter/lib/compile-state.d.ts +16 -0
- package/dist/adapter/lib/compile-state.d.ts.map +1 -1
- package/dist/adapter/memo/memo-compute.d.ts +8 -0
- package/dist/adapter/memo/memo-compute.d.ts.map +1 -1
- package/dist/adapter/props/prop-types.d.ts +52 -0
- package/dist/adapter/props/prop-types.d.ts.map +1 -1
- package/dist/adapter/value/value-lowering.d.ts +9 -1
- package/dist/adapter/value/value-lowering.d.ts.map +1 -1
- package/dist/build.js +156 -22
- package/dist/conformance-pins.d.ts.map +1 -1
- package/dist/index.js +158 -23
- package/package.json +3 -3
- package/src/__tests__/derived-state-memo.test.ts +10 -3
- package/src/__tests__/go-template-adapter.test.ts +144 -36
- package/src/adapter/go-template-adapter.ts +114 -38
- package/src/adapter/lib/compile-state.ts +18 -0
- package/src/adapter/memo/memo-compute.ts +123 -9
- package/src/adapter/props/prop-types.ts +141 -9
- package/src/adapter/value/value-lowering.ts +57 -4
- package/src/conformance-pins.ts +6 -0
|
@@ -35,6 +35,34 @@ function getterCallName(e: ParsedExpr): string | null {
|
|
|
35
35
|
: null
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
+
/** Whether `t` is `boolean`, or a `T | undefined`/`T | null` union whose
|
|
39
|
+
* non-nullish branch is `boolean` (the controlled-signal shape,
|
|
40
|
+
* `createSignal<boolean | undefined>(...)`). */
|
|
41
|
+
function isBooleanTypeInfo(t: TypeInfo): boolean {
|
|
42
|
+
if (t.kind === 'primitive') return t.primitive === 'boolean'
|
|
43
|
+
if (t.kind === 'union' && t.unionTypes?.length === 2) {
|
|
44
|
+
const scalar = t.unionTypes.find(u => u.primitive !== 'undefined' && u.primitive !== 'null')
|
|
45
|
+
return scalar?.kind === 'primitive' && scalar.primitive === 'boolean'
|
|
46
|
+
}
|
|
47
|
+
return false
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Whether the getter NAME (a signal or memo) has a boolean-compatible
|
|
51
|
+
* declared type — gates the ternary-over-getter-calls branch below
|
|
52
|
+
* (#2260) so it only fires for the boolean shape it hardcodes a `func()
|
|
53
|
+
* bool { ... }()` wrapper for; a string/number-typed getter falls through
|
|
54
|
+
* to the caller's normal handling instead of emitting invalid Go. */
|
|
55
|
+
function isBooleanTypedGetter(
|
|
56
|
+
ctx: GoEmitContext,
|
|
57
|
+
name: string,
|
|
58
|
+
signals: { getter: string; initialValue: string; type?: TypeInfo }[],
|
|
59
|
+
): boolean {
|
|
60
|
+
const signal = signals.find(s => s.getter === name)
|
|
61
|
+
if (signal) return signal.type !== undefined && isBooleanTypeInfo(signal.type)
|
|
62
|
+
const memo = (ctx.state.currentMemos ?? []).find(m => m.name === name)
|
|
63
|
+
return memo?.type !== undefined && isBooleanTypeInfo(memo.type)
|
|
64
|
+
}
|
|
65
|
+
|
|
38
66
|
/** A `props.X` member access → the prop name, else null. */
|
|
39
67
|
function propsMemberName(e: ParsedExpr): string | null {
|
|
40
68
|
return e.kind === 'member' &&
|
|
@@ -45,6 +73,25 @@ function propsMemberName(e: ParsedExpr): string | null {
|
|
|
45
73
|
: null
|
|
46
74
|
}
|
|
47
75
|
|
|
76
|
+
/**
|
|
77
|
+
* A prop reference resolved against the component's ACTUAL props-object
|
|
78
|
+
* binding (`ctx.state.propsObjectName` — may be a non-`props` name, or
|
|
79
|
+
* `null` for a destructured signature), else null. Mirrors
|
|
80
|
+
* `collectPresenceCheckedPropNames`'s (prop-types.ts) exact shape so the
|
|
81
|
+
* presence-check codegen branch below can't drift from the collector that
|
|
82
|
+
* decides which props got the nillable flip in the first place — unlike
|
|
83
|
+
* `propsMemberName` (hardcoded to the literal name `props`), which only
|
|
84
|
+
* matches the conventional object-props signature.
|
|
85
|
+
*/
|
|
86
|
+
function propNameForPropsBinding(ctx: GoEmitContext, e: ParsedExpr): string | null {
|
|
87
|
+
const propsObject = ctx.state.propsObjectName
|
|
88
|
+
if (e.kind === 'member' && !e.computed && e.object.kind === 'identifier' && e.object.name === propsObject) {
|
|
89
|
+
return e.property
|
|
90
|
+
}
|
|
91
|
+
if (!propsObject && e.kind === 'identifier') return e.name
|
|
92
|
+
return null
|
|
93
|
+
}
|
|
94
|
+
|
|
48
95
|
/** A `() => props.X.filter((p) => <predicate>)` match: the array prop name,
|
|
49
96
|
* the predicate serialized to the runtime evaluator's ParsedExpr JSON, the
|
|
50
97
|
* arrow's param name, and the free variable names its predicate captures.
|
|
@@ -56,7 +103,7 @@ function propsMemberName(e: ParsedExpr): string | null {
|
|
|
56
103
|
export function matchFilterArmMemo(
|
|
57
104
|
ctx: GoEmitContext,
|
|
58
105
|
body: ParsedExpr,
|
|
59
|
-
signals: { getter: string; initialValue: string }[],
|
|
106
|
+
signals: { getter: string; initialValue: string; type?: TypeInfo }[],
|
|
60
107
|
propsParams: { name: string; type?: TypeInfo; defaultValue?: string }[],
|
|
61
108
|
): { propName: string; predJSON: string; paramName: string; freeVars: string[] } | null {
|
|
62
109
|
const cb = asCallbackMethodCall(body)
|
|
@@ -93,7 +140,7 @@ export function matchFilterArmMemo(
|
|
|
93
140
|
export function filterArmEarlierSiblingRefs(
|
|
94
141
|
ctx: GoEmitContext,
|
|
95
142
|
memo: { name: string; parsed?: ParsedExpr },
|
|
96
|
-
signals: { getter: string; initialValue: string }[],
|
|
143
|
+
signals: { getter: string; initialValue: string; type?: TypeInfo }[],
|
|
97
144
|
propsParams: { name: string; type?: TypeInfo; defaultValue?: string }[],
|
|
98
145
|
): string[] {
|
|
99
146
|
if (!memo.parsed) return []
|
|
@@ -126,7 +173,7 @@ export function filterArmEarlierSiblingRefs(
|
|
|
126
173
|
export function computeMemoInitialValue(
|
|
127
174
|
ctx: GoEmitContext,
|
|
128
175
|
memo: { name: string; computation: string; deps: string[]; parsed?: ParsedExpr },
|
|
129
|
-
signals: { getter: string; initialValue: string }[],
|
|
176
|
+
signals: { getter: string; initialValue: string; type?: TypeInfo }[],
|
|
130
177
|
propsParams: { name: string; type?: TypeInfo; defaultValue?: string }[],
|
|
131
178
|
propFallbackVars: ReadonlyMap<string, PropFallbackVar> = EMPTY_PROP_FALLBACK_VARS,
|
|
132
179
|
goType?: string,
|
|
@@ -168,7 +215,7 @@ export function computeMemoInitialValue(
|
|
|
168
215
|
export function memoInitialFromParsedBody(
|
|
169
216
|
ctx: GoEmitContext,
|
|
170
217
|
body: ParsedExpr,
|
|
171
|
-
signals: { getter: string; initialValue: string }[],
|
|
218
|
+
signals: { getter: string; initialValue: string; type?: TypeInfo }[],
|
|
172
219
|
propsParams: { name: string; type?: TypeInfo; defaultValue?: string }[],
|
|
173
220
|
propFallbackVars: ReadonlyMap<string, PropFallbackVar>,
|
|
174
221
|
currentMemoName: string,
|
|
@@ -310,6 +357,42 @@ export function memoInitialFromParsedBody(
|
|
|
310
357
|
if (propName) return propRef(propName)
|
|
311
358
|
}
|
|
312
359
|
|
|
360
|
+
// () => props.X !== undefined (or !=/===/==) — the "controlled component"
|
|
361
|
+
// idiom's `isControlled` memo (#2260). Distinguishing "caller passed a
|
|
362
|
+
// value" from "caller omitted the prop" needs the nillable `interface{}`
|
|
363
|
+
// representation `collectPresenceCheckedPropNames`/`resolvePropGoType`
|
|
364
|
+
// flip `props.X` to; the input field's Go zero value for `interface{}` IS
|
|
365
|
+
// `nil`, so a plain nil-check is exact (no `bf_nullish`-style helper
|
|
366
|
+
// needed — this is presence, not a `??` fallback).
|
|
367
|
+
if (
|
|
368
|
+
body.kind === 'binary' &&
|
|
369
|
+
(body.op === '!==' || body.op === '!=' || body.op === '===' || body.op === '==')
|
|
370
|
+
) {
|
|
371
|
+
const other =
|
|
372
|
+
body.right.kind === 'identifier' && body.right.name === 'undefined' ? body.left :
|
|
373
|
+
body.left.kind === 'identifier' && body.left.name === 'undefined' ? body.right : null
|
|
374
|
+
const propName = other ? propNameForPropsBinding(ctx, other) : null
|
|
375
|
+
if (propName) {
|
|
376
|
+
const param = propsParams.find(p => p.name === propName)
|
|
377
|
+
// Guard on the field ACTUALLY being nillable-flipped — a presence
|
|
378
|
+
// check against a CONCRETE-typed field (a required prop, or an
|
|
379
|
+
// optional one this memo's shape didn't trigger the flip for, e.g.
|
|
380
|
+
// via `collectPresenceCheckedPropNames`'s primitive-only /
|
|
381
|
+
// no-default gate) can't be tested this way (`x != nil` doesn't even
|
|
382
|
+
// compile against a `string`/`bool` field) — fall through to the
|
|
383
|
+
// caller's zero-value default instead of emitting invalid Go. Reads
|
|
384
|
+
// the RAW `in.<Field>` directly — NOT through `propRef`, which
|
|
385
|
+
// prefers a hoisted `?? <fallback>` local when one exists: that local
|
|
386
|
+
// already collapsed "absent" into the fallback value at hoist time,
|
|
387
|
+
// so it's never nil and testing it here would be nonsensical (and,
|
|
388
|
+
// being concretely typed, wouldn't compile against `nil` either).
|
|
389
|
+
if (param && ctx.state.nillablePropNames.has(propName)) {
|
|
390
|
+
const isNe = body.op === '!==' || body.op === '!='
|
|
391
|
+
return `in.${capitalizeFieldName(propName)} ${isNe ? '!=' : '=='} nil`
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
|
|
313
396
|
// () => cond() ? A : B where each branch is a module string const or a
|
|
314
397
|
// string literal, and `cond` is a signal/memo this resolver can evaluate.
|
|
315
398
|
if (body.kind === 'conditional') {
|
|
@@ -350,6 +433,37 @@ export function memoInitialFromParsedBody(
|
|
|
350
433
|
}
|
|
351
434
|
}
|
|
352
435
|
}
|
|
436
|
+
|
|
437
|
+
// () => cond() ? branchA() : branchB() — a ternary whose CONDITION and
|
|
438
|
+
// BOTH branches are getter calls (signals/memos), not string literals
|
|
439
|
+
// (#2260's `isPressed = isControlled() ? controlledPressed() :
|
|
440
|
+
// internalPressed()`). `resolveGetterValueAsGo` resolves each — a
|
|
441
|
+
// presence-check memo like `isControlled` above already yields a Go
|
|
442
|
+
// `bool` expression, so the condition needs no extra wrapping.
|
|
443
|
+
//
|
|
444
|
+
// GATED to both branches being boolean-typed signals/memos
|
|
445
|
+
// (`isBooleanTypedGetter`) — `getterCallName` only checks call SHAPE,
|
|
446
|
+
// not type, so an ungated version would also match a derived
|
|
447
|
+
// string/number memo (`label = isActive() ? activeLabel() :
|
|
448
|
+
// inactiveLabel()`) and hardcode an invalid `func() bool { return
|
|
449
|
+
// "..." }()` (Copilot review finding on the initial version of this
|
|
450
|
+
// branch).
|
|
451
|
+
if (condName) {
|
|
452
|
+
const tName = getterCallName(body.consequent)
|
|
453
|
+
const fName = getterCallName(body.alternate)
|
|
454
|
+
if (
|
|
455
|
+
tName && fName &&
|
|
456
|
+
isBooleanTypedGetter(ctx, tName, signals) &&
|
|
457
|
+
isBooleanTypedGetter(ctx, fName, signals)
|
|
458
|
+
) {
|
|
459
|
+
const condGo = resolveGetterValueAsGo(ctx, condName, signals, propsParams, propFallbackVars, resolving)
|
|
460
|
+
const tGo = resolveGetterValueAsGo(ctx, tName, signals, propsParams, propFallbackVars, resolving)
|
|
461
|
+
const fGo = resolveGetterValueAsGo(ctx, fName, signals, propsParams, propFallbackVars, resolving)
|
|
462
|
+
if (condGo !== null && tGo !== null && fGo !== null) {
|
|
463
|
+
return `func() bool { if ${condGo} { return ${tGo} }; return ${fGo} }()`
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
}
|
|
353
467
|
}
|
|
354
468
|
|
|
355
469
|
// () => <ref> <*|+|-|/> <non-negative int>. The operand must be a
|
|
@@ -473,7 +587,7 @@ export function memoInitialFromParsedBody(
|
|
|
473
587
|
export function computeMemoInitialValueOrNull(
|
|
474
588
|
ctx: GoEmitContext,
|
|
475
589
|
memo: { name: string; computation: string; deps: string[]; parsed?: ParsedExpr; parsedBlock?: ParsedStatement[]; parsedBlockComplete?: boolean },
|
|
476
|
-
signals: { getter: string; initialValue: string }[],
|
|
590
|
+
signals: { getter: string; initialValue: string; type?: TypeInfo }[],
|
|
477
591
|
propsParams: { name: string; type?: TypeInfo; defaultValue?: string }[],
|
|
478
592
|
propFallbackVars: ReadonlyMap<string, PropFallbackVar> = EMPTY_PROP_FALLBACK_VARS,
|
|
479
593
|
/**
|
|
@@ -555,14 +669,14 @@ export function computeMemoInitialValueOrNull(
|
|
|
555
669
|
export function resolveGetterValueAsGo(
|
|
556
670
|
ctx: GoEmitContext,
|
|
557
671
|
name: string,
|
|
558
|
-
signals: { getter: string; initialValue: string }[],
|
|
672
|
+
signals: { getter: string; initialValue: string; type?: TypeInfo }[],
|
|
559
673
|
propsParams: { name: string; type?: TypeInfo; defaultValue?: string }[],
|
|
560
674
|
propFallbackVars: ReadonlyMap<string, PropFallbackVar>,
|
|
561
675
|
resolving: ReadonlySet<string> = new Set(),
|
|
562
676
|
): string | null {
|
|
563
677
|
const signal = signals.find(s => s.getter === name)
|
|
564
678
|
if (signal) {
|
|
565
|
-
return getSignalInitialValueAsGo(ctx, signal.initialValue, propsParams, propFallbackVars)
|
|
679
|
+
return getSignalInitialValueAsGo(ctx, signal.initialValue, propsParams, propFallbackVars, signal.type)
|
|
566
680
|
}
|
|
567
681
|
const memo = (ctx.state.currentMemos ?? []).find(m => m.name === name)
|
|
568
682
|
if (memo) {
|
|
@@ -598,7 +712,7 @@ export function resolveGetterValueAsGo(
|
|
|
598
712
|
export function computeComparisonTernaryGo(
|
|
599
713
|
ctx: GoEmitContext,
|
|
600
714
|
parsed: ParsedExpr | undefined,
|
|
601
|
-
signals: { getter: string; initialValue: string }[],
|
|
715
|
+
signals: { getter: string; initialValue: string; type?: TypeInfo }[],
|
|
602
716
|
propsParams: { name: string; type?: TypeInfo; defaultValue?: string }[],
|
|
603
717
|
propFallbackVars: ReadonlyMap<string, PropFallbackVar>,
|
|
604
718
|
resolving: ReadonlySet<string> = new Set(),
|
|
@@ -654,7 +768,7 @@ export function computeComparisonTernaryGo(
|
|
|
654
768
|
export function resolveComparisonOperandGo(
|
|
655
769
|
ctx: GoEmitContext,
|
|
656
770
|
node: ParsedExpr,
|
|
657
|
-
signals: { getter: string; initialValue: string }[],
|
|
771
|
+
signals: { getter: string; initialValue: string; type?: TypeInfo }[],
|
|
658
772
|
propsParams: { name: string; type?: TypeInfo; defaultValue?: string }[],
|
|
659
773
|
propFallbackVars: ReadonlyMap<string, PropFallbackVar>,
|
|
660
774
|
resolving: ReadonlySet<string> = new Set(),
|
|
@@ -148,13 +148,19 @@ export function collectNullishConsumedPropNames(ctx: GoEmitContext, ir: Componen
|
|
|
148
148
|
const propsObject = ctx.state.propsObjectName
|
|
149
149
|
const propNameOfLeft = (left: ParsedExpr): string | null => {
|
|
150
150
|
if (left.kind === 'identifier') return left.name
|
|
151
|
-
if (
|
|
152
|
-
left.
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
151
|
+
if (left.kind === 'member' && !left.computed && left.object.kind === 'identifier') {
|
|
152
|
+
if (left.object.name === propsObject) return left.property
|
|
153
|
+
// Single-hop member access rooted at a bare destructured optional
|
|
154
|
+
// OBJECT prop (`user?.name ?? '…'`, #2256): the flip belongs to the
|
|
155
|
+
// ROOT prop (`user`) — an optional-object prop already lowers to a
|
|
156
|
+
// nillable `map[string]interface{}` by construction
|
|
157
|
+
// (`resolvePropGoType`'s struct-map branch), so it's the root's
|
|
158
|
+
// membership here (not the accessed field) that lets
|
|
159
|
+
// `nillablePropNameOf` route the `??` to `bf_nullish`. A deeper
|
|
160
|
+
// chain (`props.user?.name`, `user?.address?.city`) isn't matched —
|
|
161
|
+
// same single-hop `?.` caveat documented on the `member` ParsedExpr
|
|
162
|
+
// variant.
|
|
163
|
+
return left.object.name
|
|
158
164
|
}
|
|
159
165
|
return null
|
|
160
166
|
}
|
|
@@ -252,6 +258,126 @@ export function collectOmittableAttrConsumedPropNames(ctx: GoEmitContext, ir: Co
|
|
|
252
258
|
return names
|
|
253
259
|
}
|
|
254
260
|
|
|
261
|
+
/**
|
|
262
|
+
* Names of OPTIONAL no-default props consumed as the BARE value of a
|
|
263
|
+
* TEXT-position expression (`{size}` / `{props.size}`) anywhere in the
|
|
264
|
+
* component's element tree.
|
|
265
|
+
*
|
|
266
|
+
* Why this matters (#2267): with no flip, an absent `size?: number` prop
|
|
267
|
+
* resolves to a concrete `int` field and `{{.Size}}` prints its zero value
|
|
268
|
+
* (`0`) instead of empty — the JS reference renders `undefined` as "".
|
|
269
|
+
* These props take the same `interface{}` flip as `??`/omittable-attribute
|
|
270
|
+
* consumption (`resolvePropGoType`); `renderExpression`'s emitter then
|
|
271
|
+
* routes the flipped field through `bf_string` (nil-safe stringify)
|
|
272
|
+
* instead of a bare `{{.X}}`, since `text/template` prints a nil
|
|
273
|
+
* `interface{}` as the literal `<no value>`, not "".
|
|
274
|
+
*
|
|
275
|
+
* Generic recursive walk (matching `type: 'expression'` ANYWHERE, like
|
|
276
|
+
* `collectNullishConsumedPropNames`) rather than only `element.children` —
|
|
277
|
+
* a fragment return (`<>{size}</>`) or a conditional branch's children
|
|
278
|
+
* would otherwise be missed. Same shadowing caveat as the sibling
|
|
279
|
+
* collectors: a same-named loop/callback param can misattribute, making
|
|
280
|
+
* the flip merely unnecessary, not incorrect.
|
|
281
|
+
*
|
|
282
|
+
* Filtered to `kind: 'primitive'` — unlike the `??`/omittable-attribute
|
|
283
|
+
* collectors (whose consumers, `bf_nullish` and the `{{if ne .X nil}}`
|
|
284
|
+
* guard, are nil-test-only and pass the underlying value through
|
|
285
|
+
* untouched), this collector's consumer (`renderExpression`'s `bf_string`
|
|
286
|
+
* wrap) RESTRINGIFIES the value via `fmt.Sprintf`. An optional prop
|
|
287
|
+
* carrying already-rendered markup (a JSX-element/children prop) also
|
|
288
|
+
* independently resolves to `interface{}` via `resolvePropGoType`'s
|
|
289
|
+
* struct-map branch — routing THAT through `bf_string` would strip its
|
|
290
|
+
* `template.HTML` safe-markup typing, so `text/template` re-escapes
|
|
291
|
+
* already-escaped HTML on print (observed as literal `<div>...`).
|
|
292
|
+
* Restricting to primitives keeps this collector disjoint from that flip.
|
|
293
|
+
*/
|
|
294
|
+
export function collectTextConsumedPropNames(ctx: GoEmitContext, ir: ComponentIR): Set<string> {
|
|
295
|
+
const names = new Set<string>()
|
|
296
|
+
const optionalParams = new Set(
|
|
297
|
+
ir.metadata.propsParams
|
|
298
|
+
.filter(p => p.optional && p.defaultValue == null && p.type.kind === 'primitive')
|
|
299
|
+
.map(p => p.name),
|
|
300
|
+
)
|
|
301
|
+
if (optionalParams.size === 0) return names
|
|
302
|
+
|
|
303
|
+
const propsObject = ctx.state.propsObjectName
|
|
304
|
+
const walk = (node: unknown): void => {
|
|
305
|
+
if (!node || typeof node !== 'object') return
|
|
306
|
+
if (Array.isArray(node)) {
|
|
307
|
+
for (const item of node) walk(item)
|
|
308
|
+
return
|
|
309
|
+
}
|
|
310
|
+
const rec = node as Record<string, unknown>
|
|
311
|
+
if (rec.type === 'expression') {
|
|
312
|
+
const bareId = String(rec.expr ?? '').trim()
|
|
313
|
+
const propName =
|
|
314
|
+
propsObject && bareId.startsWith(`${propsObject}.`)
|
|
315
|
+
? bareId.slice(propsObject.length + 1)
|
|
316
|
+
: bareId
|
|
317
|
+
if (/^[A-Za-z_$][\w$]*$/.test(propName) && optionalParams.has(propName)) {
|
|
318
|
+
names.add(propName)
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
for (const value of Object.values(rec)) walk(value)
|
|
322
|
+
}
|
|
323
|
+
walk(ir.root)
|
|
324
|
+
return names
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* Names of OPTIONAL no-default props whose PRESENCE is tested — `props.X
|
|
329
|
+
* !== undefined` / `!= undefined` (or the `===`/`==` negation) — anywhere in
|
|
330
|
+
* a memo's computation (#2260). This is the "controlled component" idiom's
|
|
331
|
+
* `isControlled` check (`createMemo(() => props.pressed !== undefined)`):
|
|
332
|
+
* distinguishing "caller passed a value" from "caller omitted the prop" is
|
|
333
|
+
* only expressible on the nillable `interface{}` representation (a
|
|
334
|
+
* concrete `bool` field can't tell `pressed={false}` from omitted `pressed`
|
|
335
|
+
* apart — both are the Go zero value `false`), so these props take the
|
|
336
|
+
* same `interface{}` flip as `??`/attribute/text consumption
|
|
337
|
+
* (`resolvePropGoType`). `memoInitialFromParsedBody`'s presence-check
|
|
338
|
+
* branch then lowers the comparison to `in.X != nil`.
|
|
339
|
+
*
|
|
340
|
+
* Scoped to `ir.metadata.memos[].parsed` (not the generic tree walk the
|
|
341
|
+
* sibling collectors use) — a presence check is a memo-computation shape,
|
|
342
|
+
* never a JSX child/attribute expression.
|
|
343
|
+
*/
|
|
344
|
+
export function collectPresenceCheckedPropNames(ctx: GoEmitContext, ir: ComponentIR): Set<string> {
|
|
345
|
+
const names = new Set<string>()
|
|
346
|
+
const optionalParams = new Set(
|
|
347
|
+
ir.metadata.propsParams
|
|
348
|
+
.filter(p => p.optional && p.defaultValue == null && p.type.kind === 'primitive')
|
|
349
|
+
.map(p => p.name),
|
|
350
|
+
)
|
|
351
|
+
if (optionalParams.size === 0) return names
|
|
352
|
+
|
|
353
|
+
const propsObject = ctx.state.propsObjectName
|
|
354
|
+
const isUndefinedCheck = (op: string): boolean =>
|
|
355
|
+
op === '!==' || op === '!=' || op === '===' || op === '=='
|
|
356
|
+
|
|
357
|
+
for (const memo of ir.metadata.memos) {
|
|
358
|
+
const body = memo.parsed
|
|
359
|
+
if (!body || body.kind !== 'binary' || !isUndefinedCheck(body.op)) continue
|
|
360
|
+
const other = body.right.kind === 'identifier' && body.right.name === 'undefined'
|
|
361
|
+
? body.left
|
|
362
|
+
: body.left.kind === 'identifier' && body.left.name === 'undefined'
|
|
363
|
+
? body.right
|
|
364
|
+
: null
|
|
365
|
+
if (!other) continue
|
|
366
|
+
// Object-props style (`props.X`) — `other` is a member access rooted at
|
|
367
|
+
// the props object. Destructured style (`X` bare) — `other` is a bare
|
|
368
|
+
// identifier naming the prop directly.
|
|
369
|
+
const propName =
|
|
370
|
+
other.kind === 'member' && !other.computed &&
|
|
371
|
+
other.object.kind === 'identifier' && other.object.name === propsObject
|
|
372
|
+
? other.property
|
|
373
|
+
: !propsObject && other.kind === 'identifier'
|
|
374
|
+
? other.name
|
|
375
|
+
: null
|
|
376
|
+
if (propName && optionalParams.has(propName)) names.add(propName)
|
|
377
|
+
}
|
|
378
|
+
return names
|
|
379
|
+
}
|
|
380
|
+
|
|
255
381
|
/**
|
|
256
382
|
* Resolve a prop param's Go struct-field type using the SAME logic
|
|
257
383
|
* `generatePropsStruct` / `generateInputStruct` use: a `propTypeOverrides` entry
|
|
@@ -298,12 +424,18 @@ export function resolvePropGoType(
|
|
|
298
424
|
// documented pre-#2248 trade-off there.)
|
|
299
425
|
// A bare-attribute consumption (`rows={rows}`) takes the same flip (#2259):
|
|
300
426
|
// attribute omission for an absent optional needs a nil to test — see
|
|
301
|
-
// `collectOmittableAttrConsumedPropNames`.
|
|
427
|
+
// `collectOmittableAttrConsumedPropNames`. A bare TEXT-position
|
|
428
|
+
// consumption (`{size}`) takes the same flip too (#2267) — see
|
|
429
|
+
// `collectTextConsumedPropNames`. A presence check (`props.X !==
|
|
430
|
+
// undefined`, the "controlled component" idiom's `isControlled` memo)
|
|
431
|
+
// takes the same flip too (#2260) — see `collectPresenceCheckedPropNames`.
|
|
302
432
|
if (
|
|
303
433
|
param.optional &&
|
|
304
434
|
param.type.kind === 'primitive' &&
|
|
305
435
|
(ctx.state.nullishConsumedPropNames.has(param.name) ||
|
|
306
|
-
ctx.state.omittableAttrConsumedPropNames.has(param.name)
|
|
436
|
+
ctx.state.omittableAttrConsumedPropNames.has(param.name) ||
|
|
437
|
+
ctx.state.textConsumedPropNames.has(param.name) ||
|
|
438
|
+
ctx.state.presenceCheckedPropNames.has(param.name)) &&
|
|
307
439
|
NULLISH_SCALAR_GO_TYPES.has(base)
|
|
308
440
|
) {
|
|
309
441
|
return 'interface{}'
|
|
@@ -15,6 +15,45 @@ import { parsedLiteralToGo } from './parsed-literal-to-go.ts'
|
|
|
15
15
|
/** Default for `getSignalInitialValueAsGo`'s optional fallback-var map. */
|
|
16
16
|
const EMPTY_PROP_FALLBACK_VARS: ReadonlyMap<string, PropFallbackVar> = new Map()
|
|
17
17
|
|
|
18
|
+
/**
|
|
19
|
+
* A bare prop-field reference (`in.<Field>`), type-asserted when the prop
|
|
20
|
+
* was flipped to nillable `interface{}` (#2248/#2259/#2260's
|
|
21
|
+
* `resolvePropGoType` flips) while THE CONSUMER's own expected type is a
|
|
22
|
+
* concrete scalar — e.g. `createSignal<boolean | undefined>(props.pressed)`
|
|
23
|
+
* resolves to a plain `bool` signal field (the `| undefined` half doesn't
|
|
24
|
+
* itself trigger a flip), but `props.pressed` now bakes as `interface{}`.
|
|
25
|
+
* A bare `interface{}` value can't assign into a `bool` field/branch (Go
|
|
26
|
+
* compile error) — safely type-assert with a zero-value fallback for the
|
|
27
|
+
* concrete-scalar case instead of the bare field reference. Object/array
|
|
28
|
+
* expected types are left alone (already `interface{}`-compatible).
|
|
29
|
+
*
|
|
30
|
+
* `expectedType` may be `kind: 'union'` — a `T | undefined` signal type
|
|
31
|
+
* annotation (the controlled-component idiom's controlled signal) — the `|
|
|
32
|
+
* undefined` half is source-level documentation of nullability, not a
|
|
33
|
+
* Go-representable branch, so it's unwrapped to its single non-
|
|
34
|
+
* undefined/null primitive branch.
|
|
35
|
+
*/
|
|
36
|
+
function nillableAwarePropRef(ctx: GoEmitContext, propName: string, expectedType: TypeInfo): string {
|
|
37
|
+
const fieldRef = `in.${capitalizeFieldName(propName)}`
|
|
38
|
+
const scalar =
|
|
39
|
+
expectedType.kind === 'primitive'
|
|
40
|
+
? expectedType
|
|
41
|
+
: expectedType.kind === 'union' && expectedType.unionTypes?.length === 2
|
|
42
|
+
? expectedType.unionTypes.find(t => t.primitive !== 'undefined' && t.primitive !== 'null')
|
|
43
|
+
: undefined
|
|
44
|
+
if (ctx.state.nillablePropNames.has(propName) && scalar?.kind === 'primitive') {
|
|
45
|
+
const goType =
|
|
46
|
+
scalar.primitive === 'boolean' ? 'bool' :
|
|
47
|
+
scalar.primitive === 'number' ? 'float64' :
|
|
48
|
+
scalar.primitive === 'string' ? 'string' : null
|
|
49
|
+
if (goType) {
|
|
50
|
+
const zero = goType === 'bool' ? 'false' : goType === 'string' ? '""' : '0'
|
|
51
|
+
return `func() ${goType} { if v, ok := ${fieldRef}.(${goType}); ok { return v }; return ${zero} }()`
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return fieldRef
|
|
55
|
+
}
|
|
56
|
+
|
|
18
57
|
/**
|
|
19
58
|
* Lower a signal/const initial value to its Go SSR literal: a prop reference
|
|
20
59
|
* becomes `in.<Field>`, a non-literal falls back to the type's zero value.
|
|
@@ -26,15 +65,17 @@ export function convertInitialValue(
|
|
|
26
65
|
propsParams?: { name: string }[],
|
|
27
66
|
preParsed?: ParsedExpr,
|
|
28
67
|
): string {
|
|
68
|
+
const propRef = (propName: string): string => nillableAwarePropRef(ctx, propName, typeInfo)
|
|
69
|
+
|
|
29
70
|
if (/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(value)) {
|
|
30
71
|
if (propsParams?.some(p => p.name === value)) {
|
|
31
|
-
return
|
|
72
|
+
return propRef(value)
|
|
32
73
|
}
|
|
33
74
|
}
|
|
34
75
|
|
|
35
76
|
const propName = ctx.extractPropNameFromInitialValue(value, preParsed)
|
|
36
77
|
if (propName && propsParams?.some(p => p.name === propName)) {
|
|
37
|
-
return
|
|
78
|
+
return propRef(propName)
|
|
38
79
|
}
|
|
39
80
|
|
|
40
81
|
if (typeInfo.kind === 'primitive') {
|
|
@@ -148,24 +189,36 @@ export function objectLiteralToGoMap(ctx: GoEmitContext, expr: ParsedExpr): stri
|
|
|
148
189
|
* Get a signal's initial value as Go code — a literal, or a props reference
|
|
149
190
|
* (`in.<Field>`, or the hoisted fallback var when `props.X ?? N` has one).
|
|
150
191
|
* Unrecognized values default to `0`.
|
|
192
|
+
*
|
|
193
|
+
* `signalType`, when passed, drives the same nillable-prop type-assertion
|
|
194
|
+
* `convertInitialValue` applies (#2260) — a caller resolving a getter as the
|
|
195
|
+
* operand of a boolean condition/ternary branch (`resolveGetterValueAsGo`)
|
|
196
|
+
* needs a concrete-typed result, not a bare `interface{}` field reference,
|
|
197
|
+
* when the referenced prop was flipped to nillable. Omitted by call sites
|
|
198
|
+
* that splice the result into an `interface{}`-typed context (e.g. a
|
|
199
|
+
* `map[string]any{...}` env entry), where the bare reference is fine.
|
|
151
200
|
*/
|
|
152
201
|
export function getSignalInitialValueAsGo(
|
|
153
202
|
ctx: GoEmitContext,
|
|
154
203
|
initialValue: string,
|
|
155
204
|
propsParams: { name: string }[],
|
|
156
205
|
propFallbackVars: ReadonlyMap<string, PropFallbackVar> = EMPTY_PROP_FALLBACK_VARS,
|
|
206
|
+
signalType?: TypeInfo,
|
|
157
207
|
): string {
|
|
208
|
+
const propRef = (propName: string): string =>
|
|
209
|
+
signalType ? nillableAwarePropRef(ctx, propName, signalType) : `in.${capitalizeFieldName(propName)}`
|
|
210
|
+
|
|
158
211
|
if (propsParams.some(p => p.name === initialValue)) {
|
|
159
212
|
const hoisted = propFallbackVars.get(initialValue)
|
|
160
213
|
if (hoisted) return hoisted.varName
|
|
161
|
-
return
|
|
214
|
+
return propRef(initialValue)
|
|
162
215
|
}
|
|
163
216
|
|
|
164
217
|
const propName = ctx.extractPropNameFromInitialValue(initialValue)
|
|
165
218
|
if (propName && propsParams.some(p => p.name === propName)) {
|
|
166
219
|
const hoisted = propFallbackVars.get(propName)
|
|
167
220
|
if (hoisted) return hoisted.varName
|
|
168
|
-
return
|
|
221
|
+
return propRef(propName)
|
|
169
222
|
}
|
|
170
223
|
|
|
171
224
|
// single quotes are normalized to Go double quotes
|
package/src/conformance-pins.ts
CHANGED
|
@@ -137,4 +137,10 @@ export const conformancePins: ConformancePins = {
|
|
|
137
137
|
// A dynamic/signal-derived value still refuses with BF101 — see the
|
|
138
138
|
// `dangerous-inner-html-dynamic` fixture/pin below (tracked: #2215).
|
|
139
139
|
'dangerous-inner-html-dynamic': [{ code: 'BF101', severity: 'error', issue: 'https://github.com/piconic-ai/barefootjs/issues/2215' }],
|
|
140
|
+
// #2273: a method call on a prop typed as a built-in host rich type
|
|
141
|
+
// (Date, Map, …) has no catalogued lowering in any adapter — this is a
|
|
142
|
+
// compiler-level refusal (`checkRichTypeMethodCalls`, wired ahead of
|
|
143
|
+
// `adapter.generate()`), not an adapter-specific gap, so it is pinned
|
|
144
|
+
// identically across every adapter package including Hono.
|
|
145
|
+
'date-method-uncatalogued': [{ code: 'BF021', severity: 'error', issue: 'https://github.com/piconic-ai/barefootjs/issues/2274' }],
|
|
140
146
|
}
|