@barefootjs/go-template 0.18.7 → 0.19.1
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 +45 -6
- package/dist/adapter/go-template-adapter.d.ts.map +1 -1
- package/dist/adapter/index.js +326 -38
- package/dist/adapter/lib/compile-state.d.ts +32 -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/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 +97 -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 +326 -38
- package/dist/index.js +326 -38
- package/package.json +3 -3
- package/src/__tests__/derived-state-memo.test.ts +10 -3
- package/src/__tests__/go-template-adapter.test.ts +282 -23
- package/src/adapter/emit-context.ts +14 -5
- package/src/adapter/go-template-adapter.ts +291 -50
- package/src/adapter/lib/compile-state.ts +36 -0
- package/src/adapter/lib/types.ts +9 -0
- package/src/adapter/memo/memo-compute.ts +123 -9
- package/src/adapter/props/prop-types.ts +310 -1
- package/src/adapter/value/value-lowering.ts +58 -5
- package/src/test-render.ts +18 -4
|
@@ -135,7 +135,7 @@ import { lowerCtorExpr } from "./memo/ctor-lowering.ts"
|
|
|
135
135
|
import { resolveBlockBodyMemoModuleConst } from "./memo/memo-value.ts"
|
|
136
136
|
import { computeMemoInitialValue, computeMemoInitialValueOrNull, filterArmEarlierSiblingRefs } from "./memo/memo-compute.ts"
|
|
137
137
|
import { collectSpreadSlots, buildSpreadInitializer } from "./spread/spread-codegen.ts"
|
|
138
|
-
import { buildPropTypeOverrides, resolvePropGoType, collectNillablePropNames } from "./props/prop-types.ts"
|
|
138
|
+
import { buildPropTypeOverrides, resolvePropGoType, collectNillablePropNames, collectNullishConsumedPropNames, collectOmittableAttrConsumedPropNames, collectTextConsumedPropNames, collectPresenceCheckedPropNames, NULLISH_SCALAR_GO_TYPES } from "./props/prop-types.ts"
|
|
139
139
|
import { collectStringValueNames } from "./props/prop-classes.ts"
|
|
140
140
|
|
|
141
141
|
export type { GoTemplateAdapterOptions } from "./lib/types.ts"
|
|
@@ -224,8 +224,9 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
224
224
|
this.convertExpressionToGo(jsExpr, out, preParsed),
|
|
225
225
|
convertConditionToGo: (jsCondition, preParsed) =>
|
|
226
226
|
this.convertConditionToGo(jsCondition, preParsed),
|
|
227
|
-
extractPropNameFromInitialValue: (initialValue) =>
|
|
228
|
-
|
|
227
|
+
extractPropNameFromInitialValue: (initialValue, preParsed) =>
|
|
228
|
+
this.extractPropNameFromInitialValue(initialValue, preParsed),
|
|
229
|
+
extractPropFallback: (initialValue, preParsed) => this.extractPropFallback(initialValue, preParsed),
|
|
229
230
|
resolveModuleStringConst: (name) => this.resolveModuleStringConst(name),
|
|
230
231
|
}
|
|
231
232
|
|
|
@@ -406,6 +407,21 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
406
407
|
}
|
|
407
408
|
this.state.loweringMatchers = prepareLoweringMatchers(ir.metadata)
|
|
408
409
|
augmentInheritedPropAccesses(ir)
|
|
410
|
+
// The consumed-prop sets feed `resolvePropGoType`'s interface{} flips
|
|
411
|
+
// (#2248/#2259) and `collectNillablePropNames` is derived from
|
|
412
|
+
// `resolvePropGoType`, which also consults the local type tables — so
|
|
413
|
+
// build the tables first and populate all three HERE, where both entry
|
|
414
|
+
// points share them. Computing them only in `generate()` left the
|
|
415
|
+
// standalone `generateTypes()` entry (sibling IRs in the conformance
|
|
416
|
+
// harness) resolving structs against another component's sets, and
|
|
417
|
+
// `generate()` itself computing nillability against the PREVIOUS
|
|
418
|
+
// compile's type tables.
|
|
419
|
+
this.buildLocalTypeTables(ir, ir.metadata.componentName)
|
|
420
|
+
this.state.nullishConsumedPropNames = collectNullishConsumedPropNames(this.emitCtx, ir)
|
|
421
|
+
this.state.omittableAttrConsumedPropNames = collectOmittableAttrConsumedPropNames(this.emitCtx, ir)
|
|
422
|
+
this.state.textConsumedPropNames = collectTextConsumedPropNames(this.emitCtx, ir)
|
|
423
|
+
this.state.presenceCheckedPropNames = collectPresenceCheckedPropNames(this.emitCtx, ir)
|
|
424
|
+
this.state.nillablePropNames = collectNillablePropNames(this.emitCtx, ir)
|
|
409
425
|
}
|
|
410
426
|
|
|
411
427
|
/** Generate template output for a component. */
|
|
@@ -416,7 +432,6 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
416
432
|
this.state.templateVarCounter = 0
|
|
417
433
|
this.state.pendingChildrenDefines = []
|
|
418
434
|
this.primeCompileState(ir)
|
|
419
|
-
this.state.nillablePropNames = collectNillablePropNames(this.emitCtx, ir)
|
|
420
435
|
this.state.stringValueNames = collectStringValueNames(ir)
|
|
421
436
|
|
|
422
437
|
// Surface loop-body usages of sibling-imported components (see
|
|
@@ -1213,10 +1228,26 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
1213
1228
|
// from an omitted field, so it also fires on the type's zero value.
|
|
1214
1229
|
const propFallbackVars = this.collectPropFallbackVars(ir)
|
|
1215
1230
|
for (const [, info] of propFallbackVars) {
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1231
|
+
if (info.assertType) {
|
|
1232
|
+
// Nillable-lowered prop (#2248): the `interface{}` field makes
|
|
1233
|
+
// "absent" (nil) distinguishable from an explicit `''`/`0`/`false`,
|
|
1234
|
+
// so the fallback applies ONLY on nil — JS `??` semantics. Numbers
|
|
1235
|
+
// coerce through the runtime (an untyped `Size: 3` literal boxes as
|
|
1236
|
+
// int even into a float64-shaped prop); string/bool assert directly.
|
|
1237
|
+
const deref =
|
|
1238
|
+
info.assertType === 'int' ? `bf.ToInt(in.${info.fieldName})`
|
|
1239
|
+
: info.assertType === 'float64' ? `bf.ToFloat64(in.${info.fieldName})`
|
|
1240
|
+
: `in.${info.fieldName}.(${info.assertType})`
|
|
1241
|
+
lines.push(`\tvar ${info.varName} ${info.assertType} = ${info.goFallback}`)
|
|
1242
|
+
lines.push(`\tif in.${info.fieldName} != nil {`)
|
|
1243
|
+
lines.push(`\t\t${info.varName} = ${deref}`)
|
|
1244
|
+
lines.push(`\t}`)
|
|
1245
|
+
} else {
|
|
1246
|
+
lines.push(`\t${info.varName} := in.${info.fieldName}`)
|
|
1247
|
+
lines.push(`\tif ${info.varName} == ${info.zeroLiteral} {`)
|
|
1248
|
+
lines.push(`\t\t${info.varName} = ${info.goFallback}`)
|
|
1249
|
+
lines.push(`\t}`)
|
|
1250
|
+
}
|
|
1220
1251
|
}
|
|
1221
1252
|
if (propFallbackVars.size > 0) lines.push('')
|
|
1222
1253
|
|
|
@@ -1325,7 +1356,7 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
1325
1356
|
if (propFieldNames.has(fieldName)) continue
|
|
1326
1357
|
// `props.X ?? N` reuses the hoisted fallback var so signal and memo share
|
|
1327
1358
|
// one value.
|
|
1328
|
-
const fallbackMatch = this.extractPropFallback(signal.initialValue)
|
|
1359
|
+
const fallbackMatch = this.extractPropFallback(signal.initialValue, signal.parsed)
|
|
1329
1360
|
const hoisted = fallbackMatch ? propFallbackVars.get(fallbackMatch.propName) : undefined
|
|
1330
1361
|
if (hoisted) {
|
|
1331
1362
|
lines.push(`\t\t${fieldName}: ${hoisted.varName},`)
|
|
@@ -2715,8 +2746,9 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
2715
2746
|
localTaken.add(`${nested.name.charAt(0).toLowerCase()}${nested.name.slice(1)}s`)
|
|
2716
2747
|
}
|
|
2717
2748
|
|
|
2749
|
+
const propTypeOverrides = buildPropTypeOverrides(this.emitCtx, ir)
|
|
2718
2750
|
for (const signal of ir.metadata.signals) {
|
|
2719
|
-
const match = this.extractPropFallback(signal.initialValue)
|
|
2751
|
+
const match = this.extractPropFallback(signal.initialValue, signal.parsed)
|
|
2720
2752
|
if (!match) continue
|
|
2721
2753
|
if (result.has(match.propName)) continue
|
|
2722
2754
|
const param = ir.metadata.propsParams.find(p => p.name === match.propName)
|
|
@@ -2724,6 +2756,14 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
2724
2756
|
// A destructure default already wins via applyGoFallback below.
|
|
2725
2757
|
if (goPropDefault(param.defaultValue) !== null) continue
|
|
2726
2758
|
const fieldName = capitalizeFieldName(match.propName)
|
|
2759
|
+
// A `??`-consumed optional scalar lowered to `interface{}` (#2248) —
|
|
2760
|
+
// detected off the SAME `resolvePropGoType` pipeline the struct
|
|
2761
|
+
// generators use, so this can't drift from the emitted field type. The
|
|
2762
|
+
// concrete pre-flip type is what the hoisted local materializes as.
|
|
2763
|
+
const concreteType = propTypeOverrides.get(param.name) ?? typeInfoToGo(this.emitCtx, param.type, param.defaultValue)
|
|
2764
|
+
const nullishLowered =
|
|
2765
|
+
NULLISH_SCALAR_GO_TYPES.has(concreteType) &&
|
|
2766
|
+
resolvePropGoType(this.emitCtx, param, propTypeOverrides) === 'interface{}'
|
|
2727
2767
|
// Pick the zero literal based on the fallback's literal shape. Bool
|
|
2728
2768
|
// fallbacks (`?? true`) hoist against the `false` zero — the same Go-zero
|
|
2729
2769
|
// conflation the int / string cases accept: the caller can't distinguish
|
|
@@ -2742,8 +2782,16 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
2742
2782
|
// (`?? 0`, `?? ''`, `?? false`, `?? 0.0`). Compare against the computed
|
|
2743
2783
|
// zeroLiteral so spelling variants like `0.0` collapse to the same skip
|
|
2744
2784
|
// as `0`.
|
|
2745
|
-
|
|
2746
|
-
|
|
2785
|
+
//
|
|
2786
|
+
// NOT a no-op for a nillable-lowered prop (#2248): its `interface{}`
|
|
2787
|
+
// field can be nil, and the typed hoisted local is what downstream
|
|
2788
|
+
// consumers (`Count: size`, memo env maps) assign from — a nil
|
|
2789
|
+
// interface into a concrete field is a compile error, so the local
|
|
2790
|
+
// must exist even when the fallback equals the zero value.
|
|
2791
|
+
if (!nullishLowered) {
|
|
2792
|
+
if (match.goFallback === zeroLiteral) continue
|
|
2793
|
+
if (zeroLiteral === '0' && Number(match.goFallback) === 0) continue
|
|
2794
|
+
}
|
|
2747
2795
|
// The JSX-side identifier is the natural local name; suffix with `_` if it
|
|
2748
2796
|
// collides with a Go keyword or a local we already emit.
|
|
2749
2797
|
let varName = match.propName
|
|
@@ -2751,21 +2799,43 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
2751
2799
|
varName += '_'
|
|
2752
2800
|
}
|
|
2753
2801
|
localTaken.add(varName)
|
|
2754
|
-
result.set(match.propName, {
|
|
2802
|
+
result.set(match.propName, {
|
|
2803
|
+
varName,
|
|
2804
|
+
fieldName,
|
|
2805
|
+
goFallback: match.goFallback,
|
|
2806
|
+
zeroLiteral,
|
|
2807
|
+
...(nullishLowered ? { assertType: concreteType } : {}),
|
|
2808
|
+
})
|
|
2755
2809
|
}
|
|
2756
2810
|
return result
|
|
2757
2811
|
}
|
|
2758
2812
|
|
|
2759
2813
|
/**
|
|
2760
|
-
* Parse a signal-time initial value of the form `props.X ?? <literal>`
|
|
2761
|
-
*
|
|
2762
|
-
*
|
|
2763
|
-
* the fallback isn't a simple literal `goPropDefault`
|
|
2814
|
+
* Parse a signal-time initial value of the form `props.X ?? <literal>` —
|
|
2815
|
+
* or, for destructured components, `x ?? <literal>` — into the source prop
|
|
2816
|
+
* name and the Go-formatted fallback. Returns null when the expression
|
|
2817
|
+
* isn't that shape or the fallback isn't a simple literal `goPropDefault`
|
|
2818
|
+
* can translate.
|
|
2819
|
+
*
|
|
2820
|
+
* `preParsed` (the signal's best-effort `ParsedExpr`) is matched
|
|
2821
|
+
* structurally when available; the regex handles only the props-object
|
|
2822
|
+
* member form for callers without a tree (memo computations). A bare
|
|
2823
|
+
* identifier can shadow a same-named prop (loop/callback params — the
|
|
2824
|
+
* `collectNullishConsumedPropNames` limitation class), so every caller
|
|
2825
|
+
* validates the returned name against `ir.metadata.propsParams`.
|
|
2764
2826
|
*
|
|
2765
2827
|
* Keeps the original prop reference (not just the resolved value) so
|
|
2766
2828
|
* caller-supplied non-zero inputs are honoured.
|
|
2767
2829
|
*/
|
|
2768
|
-
private extractPropFallback(
|
|
2830
|
+
private extractPropFallback(
|
|
2831
|
+
initialValue: string,
|
|
2832
|
+
preParsed?: ParsedExpr,
|
|
2833
|
+
): { propName: string; goFallback: string } | null {
|
|
2834
|
+
const structural = preParsed ? this.extractPropFallbackFromParsed(preParsed) : null
|
|
2835
|
+
if (structural) return structural
|
|
2836
|
+
|
|
2837
|
+
// Regex fallback for callers without a tree (memo computation strings)
|
|
2838
|
+
// and for props-object shapes the structural match doesn't model.
|
|
2769
2839
|
if (!this.state.propsObjectName) return null
|
|
2770
2840
|
const trimmed = initialValue.trim()
|
|
2771
2841
|
const name = this.state.propsObjectName
|
|
@@ -2779,12 +2849,64 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
2779
2849
|
return { propName: m[1], goFallback }
|
|
2780
2850
|
}
|
|
2781
2851
|
|
|
2852
|
+
/** Structural half of {@link extractPropFallback}. */
|
|
2853
|
+
private extractPropFallbackFromParsed(
|
|
2854
|
+
preParsed: ParsedExpr,
|
|
2855
|
+
): { propName: string; goFallback: string } | null {
|
|
2856
|
+
if (preParsed.kind !== 'logical' || preParsed.op !== '??') return null
|
|
2857
|
+
const left = preParsed.left
|
|
2858
|
+
const propName =
|
|
2859
|
+
left.kind === 'identifier' && !this.state.propsObjectName
|
|
2860
|
+
? left.name
|
|
2861
|
+
: left.kind === 'member' &&
|
|
2862
|
+
!left.computed &&
|
|
2863
|
+
left.object.kind === 'identifier' &&
|
|
2864
|
+
left.object.name === this.state.propsObjectName
|
|
2865
|
+
? left.property
|
|
2866
|
+
: null
|
|
2867
|
+
if (!propName) return null
|
|
2868
|
+
// A negative fallback (`?? -1`) parses as unary minus around a number
|
|
2869
|
+
// literal, not a literal.
|
|
2870
|
+
let right = preParsed.right
|
|
2871
|
+
let negate = ''
|
|
2872
|
+
if (right.kind === 'unary' && right.op === '-') {
|
|
2873
|
+
negate = '-'
|
|
2874
|
+
right = right.argument
|
|
2875
|
+
}
|
|
2876
|
+
if (right.kind !== 'literal') return null
|
|
2877
|
+
if (negate && right.literalType !== 'number') return null
|
|
2878
|
+
// A string fallback is already the decoded value — quote it directly;
|
|
2879
|
+
// routing it through `goPropDefault` would strip JSON.stringify's outer
|
|
2880
|
+
// quotes and escape the body a second time (`"` → `\\\"`).
|
|
2881
|
+
if (right.literalType === 'string') {
|
|
2882
|
+
return { propName, goFallback: JSON.stringify(right.value) }
|
|
2883
|
+
}
|
|
2884
|
+
// Numbers keep their source spelling when available (`raw` is only
|
|
2885
|
+
// populated for numbers); booleans/null stringify losslessly.
|
|
2886
|
+
const goFallback = goPropDefault(negate + (right.raw ?? String(right.value)))
|
|
2887
|
+
if (goFallback === null) return null
|
|
2888
|
+
return { propName, goFallback }
|
|
2889
|
+
}
|
|
2890
|
+
|
|
2782
2891
|
/**
|
|
2783
2892
|
* Extract the prop name from a signal's `props.xxx`-pattern initialValue,
|
|
2784
2893
|
* e.g. `"props.initial ?? 0"` → `"initial"`, `"props.checked"` → `"checked"`.
|
|
2894
|
+
* For destructured components (`propsObjectName` null) the same shapes are
|
|
2895
|
+
* matched structurally on `preParsed` with an identifier left operand
|
|
2896
|
+
* (`size ?? 0` → `"size"`); callers validate the name against
|
|
2897
|
+
* `propsParams`, which filters shadowing locals.
|
|
2785
2898
|
*/
|
|
2786
|
-
private extractPropNameFromInitialValue(initialValue: string): string | null {
|
|
2787
|
-
if (!this.state.propsObjectName)
|
|
2899
|
+
private extractPropNameFromInitialValue(initialValue: string, preParsed?: ParsedExpr): string | null {
|
|
2900
|
+
if (!this.state.propsObjectName) {
|
|
2901
|
+
if (
|
|
2902
|
+
preParsed?.kind === 'logical' &&
|
|
2903
|
+
(preParsed.op === '??' || preParsed.op === '||') &&
|
|
2904
|
+
preParsed.left.kind === 'identifier'
|
|
2905
|
+
) {
|
|
2906
|
+
return preParsed.left.name
|
|
2907
|
+
}
|
|
2908
|
+
return null
|
|
2909
|
+
}
|
|
2788
2910
|
const trimmed = initialValue.trim()
|
|
2789
2911
|
const name = this.state.propsObjectName
|
|
2790
2912
|
|
|
@@ -2973,13 +3095,52 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
2973
3095
|
return goExpr
|
|
2974
3096
|
}
|
|
2975
3097
|
|
|
3098
|
+
// A bare TEXT-position reference to an optional no-default scalar prop
|
|
3099
|
+
// that `resolvePropGoType` flipped to nillable `interface{}` (#2267,
|
|
3100
|
+
// `collectTextConsumedPropNames`) needs a nil-safe stringify: plain
|
|
3101
|
+
// `{{.X}}` prints a nil `interface{}` as the literal `<no value>`, not
|
|
3102
|
+
// "" — `bf_string` (the runtime's nil-safe `String()`) prints "" for
|
|
3103
|
+
// nil and formats a present value identically to `text/template`'s own
|
|
3104
|
+
// default printing, so this is a no-op for the non-nil case.
|
|
3105
|
+
const finalExpr =
|
|
3106
|
+
this.textNillablePropNameOf(classify.parsed) !== null
|
|
3107
|
+
? `bf_string ${wrapIfMultiToken(goExpr)}`
|
|
3108
|
+
: goExpr
|
|
3109
|
+
|
|
2976
3110
|
// Mark expressions with slotId using comment nodes for client JS to find.
|
|
2977
3111
|
// This includes reactive expressions AND loop-param-dependent expressions.
|
|
2978
3112
|
if (expr.slotId) {
|
|
2979
|
-
return `{{bfTextStart "${expr.slotId}"}}{{${
|
|
3113
|
+
return `{{bfTextStart "${expr.slotId}"}}{{${finalExpr}}}{{bfTextEnd}}`
|
|
2980
3114
|
}
|
|
2981
3115
|
|
|
2982
|
-
return `{{${
|
|
3116
|
+
return `{{${finalExpr}}}`
|
|
3117
|
+
}
|
|
3118
|
+
|
|
3119
|
+
/**
|
|
3120
|
+
* The nillable-prop name a bare TEXT-position expression refers to, or
|
|
3121
|
+
* null. Mirrors `nillablePropNameOf` (the `??`-lowering gate) but keys on
|
|
3122
|
+
* `textConsumedPropNames` instead of `nullishConsumedPropNames` — a
|
|
3123
|
+
* text-only optional scalar prop (`{size}`, never consumed by `??` or a
|
|
3124
|
+
* bare omittable attribute) is in neither of those other two sets.
|
|
3125
|
+
*/
|
|
3126
|
+
private textNillablePropNameOf(expr: ParsedExpr | undefined): string | null {
|
|
3127
|
+
if (!expr) return null
|
|
3128
|
+
let name: string | null = null
|
|
3129
|
+
if (expr.kind === 'identifier') {
|
|
3130
|
+
name = expr.name
|
|
3131
|
+
} else if (
|
|
3132
|
+
expr.kind === 'member' &&
|
|
3133
|
+
!expr.computed &&
|
|
3134
|
+
expr.object.kind === 'identifier' &&
|
|
3135
|
+
expr.object.name === this.state.propsObjectName
|
|
3136
|
+
) {
|
|
3137
|
+
name = expr.property
|
|
3138
|
+
}
|
|
3139
|
+
return name !== null &&
|
|
3140
|
+
this.state.textConsumedPropNames.has(name) &&
|
|
3141
|
+
this.state.nillablePropNames.has(name)
|
|
3142
|
+
? name
|
|
3143
|
+
: null
|
|
2983
3144
|
}
|
|
2984
3145
|
|
|
2985
3146
|
/**
|
|
@@ -3392,7 +3553,13 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
3392
3553
|
}
|
|
3393
3554
|
|
|
3394
3555
|
const obj = emit(object)
|
|
3395
|
-
|
|
3556
|
+
// JS `.length`: array element count OR, for a string, UTF-16 code-unit
|
|
3557
|
+
// count (#2255) — NOT Go's native `len`, which is byte count for a
|
|
3558
|
+
// string. `Length`/`bf_length` (bf.go) dispatches on the runtime value's
|
|
3559
|
+
// shape to match. The specialized array-only `.length` shapes above
|
|
3560
|
+
// (filter-result count, memo-backed loop slice count) stay on `len`,
|
|
3561
|
+
// since arrays never hit the UTF-16 divergence.
|
|
3562
|
+
if (property === 'length') return `bf_length ${wrapIfMultiToken(obj)}`
|
|
3396
3563
|
// A `?.`-written access (`user?.name`, #2168 optional-chaining-prop):
|
|
3397
3564
|
// a plain `.Field` dot-chain panics evaluating a field on a nil
|
|
3398
3565
|
// interface/pointer (`nil pointer evaluating interface {}.Name`), so
|
|
@@ -3526,9 +3693,60 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
3526
3693
|
const wrapLeft = wrapIfMultiToken(emit(left))
|
|
3527
3694
|
const wrapRight = wrapIfMultiToken(emit(right))
|
|
3528
3695
|
if (op === '&&') return `and ${wrapLeft} ${wrapRight}`
|
|
3696
|
+
// `??` on a nillable prop needs true JS nullish semantics (#2248): Go's
|
|
3697
|
+
// `or` is truthiness-based, so `{{or .Label "Default"}}` falls back on a
|
|
3698
|
+
// present-but-empty `""` — JS `??` keeps it. `bf_nullish` tests nil-ness
|
|
3699
|
+
// only. Non-nillable operands keep `or`: their concrete Go type can't
|
|
3700
|
+
// represent "absent" at all, so `or` and `??` are indistinguishable there.
|
|
3701
|
+
if (op === '??' && this.nillablePropNameOf(left) !== null) {
|
|
3702
|
+
return `bf_nullish ${wrapLeft} ${wrapRight}`
|
|
3703
|
+
}
|
|
3529
3704
|
return `or ${wrapLeft} ${wrapRight}`
|
|
3530
3705
|
}
|
|
3531
3706
|
|
|
3707
|
+
/**
|
|
3708
|
+
* The nillable-prop name a `??` left operand refers to, or null. Matches
|
|
3709
|
+
* the two prop-reference shapes (`label` destructured, `props.label`
|
|
3710
|
+
* object-style) against `nullishConsumedPropNames ∩ nillablePropNames`.
|
|
3711
|
+
*
|
|
3712
|
+
* The intersection matters: `nillablePropNames` alone OVERAPPROXIMATES —
|
|
3713
|
+
* it is collected before local type aliases are registered, so an
|
|
3714
|
+
* alias-typed prop (`placement?: TooltipPlacement`) can sit in the set
|
|
3715
|
+
* while its emitted struct field is the concrete alias type. On such a
|
|
3716
|
+
* concrete field "absent" is invisible (the zero value), so the
|
|
3717
|
+
* truthiness-based `or` is the correct approximation and `bf_nullish`
|
|
3718
|
+
* would wrongly KEEP the zero value (e.g. Tooltip's
|
|
3719
|
+
* `placementClasses[props.placement ?? 'top']` would resolve to no
|
|
3720
|
+
* class for an omitted placement). Requiring `nullishConsumedPropNames`
|
|
3721
|
+
* membership pins the
|
|
3722
|
+
* gate to props the `??` analysis actually saw — the same set that drives
|
|
3723
|
+
* the `interface{}` flip in `resolvePropGoType`.
|
|
3724
|
+
*/
|
|
3725
|
+
private nillablePropNameOf(expr: ParsedExpr): string | null {
|
|
3726
|
+
let name: string | null = null
|
|
3727
|
+
if (expr.kind === 'identifier') {
|
|
3728
|
+
name = expr.name
|
|
3729
|
+
} else if (expr.kind === 'member' && !expr.computed && expr.object.kind === 'identifier') {
|
|
3730
|
+
if (expr.object.name === this.state.propsObjectName) {
|
|
3731
|
+
name = expr.property
|
|
3732
|
+
} else {
|
|
3733
|
+
// Single-hop member access rooted at a bare destructured optional
|
|
3734
|
+
// OBJECT prop (`user?.name ?? '…'`, #2256) — the ROOT prop is what
|
|
3735
|
+
// needs to be in the nillable/nullish-consumed sets, not the
|
|
3736
|
+
// accessed field; `member()`'s own `bf_get` lowering for the `?.`
|
|
3737
|
+
// hop already returns Go `nil` on a nil/missing root, so gating on
|
|
3738
|
+
// the root here is sufficient. Mirrors
|
|
3739
|
+
// `collectNullishConsumedPropNames`'s `propNameOfLeft`.
|
|
3740
|
+
name = expr.object.name
|
|
3741
|
+
}
|
|
3742
|
+
}
|
|
3743
|
+
return name !== null &&
|
|
3744
|
+
this.state.nullishConsumedPropNames.has(name) &&
|
|
3745
|
+
this.state.nillablePropNames.has(name)
|
|
3746
|
+
? name
|
|
3747
|
+
: null
|
|
3748
|
+
}
|
|
3749
|
+
|
|
3532
3750
|
// JSX-level ternaries (`{expr ? a : b}`) are handled at the IR level as
|
|
3533
3751
|
// IRConditional (via convertConditionToGo → renderConditionExpr). This method
|
|
3534
3752
|
// is only reached for ternaries nested inside other ParsedExpr trees (e.g.
|
|
@@ -4252,11 +4470,6 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
4252
4470
|
return this.renderFilterExpr(pred, param, new Map(), datumField ?? undefined)
|
|
4253
4471
|
}
|
|
4254
4472
|
|
|
4255
|
-
/** Whether an expression needs parentheses when used in and/or. */
|
|
4256
|
-
private needsParens(expr: ParsedExpr): boolean {
|
|
4257
|
-
return expr.kind === 'logical' || expr.kind === 'unary' || expr.kind === 'conditional'
|
|
4258
|
-
}
|
|
4259
|
-
|
|
4260
4473
|
/**
|
|
4261
4474
|
* Split a rendered template block into preamble + final expression.
|
|
4262
4475
|
* The last `{{...}}` must be a variable reference (`$bf_rN` or
|
|
@@ -4935,19 +5148,26 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
4935
5148
|
if (expr.callee.kind === 'identifier' && expr.args.length === 0) {
|
|
4936
5149
|
return plain(this.rootFieldRef(expr.callee.name))
|
|
4937
5150
|
}
|
|
4938
|
-
// `isValidElement(x)` — the framework "is this a renderable element
|
|
4939
|
-
//
|
|
4940
|
-
//
|
|
4941
|
-
//
|
|
4942
|
-
//
|
|
4943
|
-
//
|
|
4944
|
-
//
|
|
5151
|
+
// `isValidElement(x)` — the framework "is this a renderable element
|
|
5152
|
+
// (not plain text)?" predicate. #2266: a passed-through JSX child is
|
|
5153
|
+
// ALSO represented as pre-rendered markup on Go's SSR model, so a
|
|
5154
|
+
// plain non-empty STRING child is truthy but is NOT a valid element
|
|
5155
|
+
// — a bare truthiness check (the pre-#2266 lowering) wrongly took
|
|
5156
|
+
// `Slot`'s element-merge branch and panicked dereferencing
|
|
5157
|
+
// `.Props`/`.Tag` on a string (`can't evaluate field Props in type
|
|
5158
|
+
// interface {}`). `bf_is_element` (bf.go) does a real reflect-based
|
|
5159
|
+
// shape check (map/struct carrying both `tag`+`props` keys/fields,
|
|
5160
|
+
// case-insensitively), matching JS's `'tag' in x && 'props' in x`.
|
|
5161
|
+
// Pre-parenthesised — `call`-kind results aren't covered by
|
|
5162
|
+
// `needsParens`, so an unparenthesised `bf_is_element X` splices as
|
|
5163
|
+
// extra sibling args into an enclosing `and`/`or`.
|
|
4945
5164
|
if (
|
|
4946
5165
|
expr.callee.kind === 'identifier' &&
|
|
4947
5166
|
(identifierPath(expr.callee) ?? expr.callee.name) === 'isValidElement' &&
|
|
4948
5167
|
expr.args.length === 1
|
|
4949
5168
|
) {
|
|
4950
|
-
|
|
5169
|
+
const inner = this.renderConditionExpr(expr.args[0])
|
|
5170
|
+
return { preamble: inner.preamble, expr: `(bf_is_element ${wrapIfMultiToken(inner.expr)})` }
|
|
4951
5171
|
}
|
|
4952
5172
|
// Any other user-defined predicate call with arguments (e.g.
|
|
4953
5173
|
// `isAdmin(user)`) has no server-side evaluator and is not a registered
|
|
@@ -5087,11 +5307,27 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
5087
5307
|
const leftResult = this.renderConditionExpr(expr.left)
|
|
5088
5308
|
const rightResult = this.renderConditionExpr(expr.right)
|
|
5089
5309
|
const preamble = leftResult.preamble + rightResult.preamble
|
|
5090
|
-
|
|
5091
|
-
|
|
5310
|
+
// `wrapIfMultiToken` (whitespace-based, on the RENDERED string) — not
|
|
5311
|
+
// `needsParens` (AST-kind-based, only `logical`/`unary`/`conditional`)
|
|
5312
|
+
// — matches the main `logical()` emitter's own wrapping. `needsParens`
|
|
5313
|
+
// misses any other multi-token rendering (`len .X`, `bf_add a b`,
|
|
5314
|
+
// `.SearchParams.Get "k"`), which `and`/`or`/`bf_nullish` (all prefix
|
|
5315
|
+
// builtins) would otherwise parse as extra sibling args instead of one
|
|
5316
|
+
// operand.
|
|
5317
|
+
const wrapLeft = wrapIfMultiToken(leftResult.expr)
|
|
5318
|
+
const wrapRight = wrapIfMultiToken(rightResult.expr)
|
|
5319
|
+
// `??` on a nillable prop needs true JS nullish semantics (#2254,
|
|
5320
|
+
// sibling of #2248/#2252's fix for text-expression/signal-seed
|
|
5321
|
+
// positions): Go's `or` is truthiness-based, so a present-but-empty
|
|
5322
|
+
// `""`/`0`/`false` operand wrongly falls back. `bf_nullish` tests
|
|
5323
|
+
// nil-ness only. Mirrors the `logical()` emitter's own gate (used
|
|
5324
|
+
// for non-condition expression positions) — same
|
|
5325
|
+
// `nullishConsumed ∩ nillable` check via `nillablePropNameOf`.
|
|
5092
5326
|
const result = expr.op === '&&'
|
|
5093
5327
|
? `and ${wrapLeft} ${wrapRight}`
|
|
5094
|
-
:
|
|
5328
|
+
: expr.op === '??' && this.nillablePropNameOf(expr.left) !== null
|
|
5329
|
+
? `bf_nullish ${wrapLeft} ${wrapRight}`
|
|
5330
|
+
: `or ${wrapLeft} ${wrapRight}`
|
|
5095
5331
|
return { preamble, expr: result }
|
|
5096
5332
|
}
|
|
5097
5333
|
|
|
@@ -5954,17 +6190,22 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
5954
6190
|
for (const e of entries) {
|
|
5955
6191
|
if (e.kind === 'expr' && !isSupported(parseExpression(e.expr)).supported) return null
|
|
5956
6192
|
}
|
|
5957
|
-
//
|
|
5958
|
-
//
|
|
5959
|
-
//
|
|
5960
|
-
//
|
|
5961
|
-
|
|
5962
|
-
|
|
5963
|
-
|
|
5964
|
-
|
|
5965
|
-
|
|
5966
|
-
|
|
5967
|
-
|
|
6193
|
+
// Routed through the single `bf_style_object` runtime call (#2261)
|
|
6194
|
+
// rather than per-pair `key:value` template interpolation — a dynamic
|
|
6195
|
+
// value that fails the ported `hasUnsafeStyleValue` CSS-injection scan
|
|
6196
|
+
// must DROP its entire pair to match Hono's oracle behavior, which
|
|
6197
|
+
// isn't expressible as a per-pair inline substitution (a dropped
|
|
6198
|
+
// MIDDLE pair would otherwise leave a dangling `key:` / stray `;;` in
|
|
6199
|
+
// a `.map().join(';')` splice). `String()` (already returns "" for
|
|
6200
|
+
// nil) also has an established zero-value contract for numbers/bools,
|
|
6201
|
+
// avoiding html/template's own contextual CSS auto-escaper (whose
|
|
6202
|
+
// `ZgotmplZ` sentinel — the pre-#2261 divergence — is bypassed by the
|
|
6203
|
+
// call returning a trusted `template.CSS`, not a plain `string`).
|
|
6204
|
+
const args = entries.flatMap(e => [
|
|
6205
|
+
JSON.stringify(e.cssKey),
|
|
6206
|
+
e.kind === 'literal' ? JSON.stringify(e.value) : wrapIfMultiToken(this.convertExpressionToGo(e.expr)),
|
|
6207
|
+
])
|
|
6208
|
+
return `{{bf_style_object ${args.join(' ')}}}`
|
|
5968
6209
|
}
|
|
5969
6210
|
|
|
5970
6211
|
private renderAttributes(element: IRElement): string {
|
|
@@ -146,6 +146,42 @@ export class CompileState {
|
|
|
146
146
|
*/
|
|
147
147
|
nillablePropNames: Set<string> = new Set()
|
|
148
148
|
|
|
149
|
+
/**
|
|
150
|
+
* OPTIONAL prop names consumed nullish-sensitively (`??` left operand in a
|
|
151
|
+
* parsed expression tree, or a signal's `props.X ?? <literal>` seed) —
|
|
152
|
+
* #2248. Consulted by `resolvePropGoType` to flip an optional scalar to the
|
|
153
|
+
* nillable `interface{}` representation, so it MUST be populated before the
|
|
154
|
+
* first `resolvePropGoType` call of a compile (see `generate()`'s ordering
|
|
155
|
+
* against `collectNillablePropNames`).
|
|
156
|
+
*/
|
|
157
|
+
nullishConsumedPropNames: Set<string> = new Set()
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* OPTIONAL no-default prop names consumed as a BARE omittable-attribute
|
|
161
|
+
* value (`rows={rows}`) — #2259. Same `resolvePropGoType` flip and same
|
|
162
|
+
* populate-before-first-resolve ordering as `nullishConsumedPropNames`:
|
|
163
|
+
* the attribute-omission guard (`{{if ne .X nil}}`) needs a nillable field.
|
|
164
|
+
*/
|
|
165
|
+
omittableAttrConsumedPropNames: Set<string> = new Set()
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* OPTIONAL no-default prop names consumed as a BARE TEXT-position
|
|
169
|
+
* expression value (`{size}`) — #2267. Same `resolvePropGoType` flip and
|
|
170
|
+
* same populate-before-first-resolve ordering as
|
|
171
|
+
* `nullishConsumedPropNames`: the text emitter's nil-safe `bf_string`
|
|
172
|
+
* wrap needs a nillable field to have something to guard.
|
|
173
|
+
*/
|
|
174
|
+
textConsumedPropNames: Set<string> = new Set()
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* OPTIONAL no-default prop names whose PRESENCE is tested — `props.X !==
|
|
178
|
+
* undefined` (the "controlled component" idiom's `isControlled` memo) —
|
|
179
|
+
* #2260. Same `resolvePropGoType` flip and same populate-before-first-
|
|
180
|
+
* resolve ordering as `nullishConsumedPropNames`: distinguishing "caller
|
|
181
|
+
* passed a value" from "caller omitted the prop" needs a nillable field.
|
|
182
|
+
*/
|
|
183
|
+
presenceCheckedPropNames: Set<string> = new Set()
|
|
184
|
+
|
|
149
185
|
/**
|
|
150
186
|
* String-typed signal getter / prop names (#2168 string-concat-plus).
|
|
151
187
|
* Feeds `isStringName` for `isStringConcatBinary`, which decides whether a
|
package/src/adapter/lib/types.ts
CHANGED
|
@@ -140,6 +140,15 @@ export interface PropFallbackVar {
|
|
|
140
140
|
goFallback: string
|
|
141
141
|
/** Go zero literal for the prop's type (`0`, `""`, etc.). */
|
|
142
142
|
zeroLiteral: string
|
|
143
|
+
/**
|
|
144
|
+
* Set when the prop lowered to the nillable `interface{}` representation
|
|
145
|
+
* (#2248): the concrete scalar Go type (`string`/`int`/`float64`/`bool`)
|
|
146
|
+
* the constructor materializes the hoisted local as. Presence switches
|
|
147
|
+
* the emission from the zero-value check (`if v == 0`) to a nil check
|
|
148
|
+
* (`if in.X != nil`), which is what makes an explicit `''`/`0` input
|
|
149
|
+
* distinguishable from an absent one.
|
|
150
|
+
*/
|
|
151
|
+
assertType?: string
|
|
143
152
|
}
|
|
144
153
|
|
|
145
154
|
/**
|