@barefootjs/go-template 0.19.0 → 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/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/index.js +156 -22
- 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
|
@@ -301,10 +301,17 @@ export function P(props: { items: { id: string }[] }) {
|
|
|
301
301
|
`
|
|
302
302
|
const { template } = generate(src)
|
|
303
303
|
expect(template).not.toContain('len .Visible')
|
|
304
|
-
|
|
304
|
+
expect(template).not.toContain('bf_length .Visible')
|
|
305
|
+
// The loop over visible() is handler-filled as `.Rows`; this is one of
|
|
306
|
+
// the specialized ARRAY-only `.length` shapes (memo-backed loop slice
|
|
307
|
+
// count) that stays on native `len` even after #2255 — see the
|
|
308
|
+
// `member()` docstring on the generic `bf_length` fallback.
|
|
305
309
|
expect(template).toContain('len .Rows')
|
|
306
|
-
// props.items.length is
|
|
307
|
-
|
|
310
|
+
// props.items.length is the GENERIC (non-specialized) `.length`
|
|
311
|
+
// fallback, which #2255 routed through `bf_length` (UTF-16-aware for
|
|
312
|
+
// strings, and functionally equivalent to `len` for an array/slice —
|
|
313
|
+
// both dispatch on the underlying value's shape).
|
|
314
|
+
expect(template).toContain('bf_length .Items')
|
|
308
315
|
})
|
|
309
316
|
})
|
|
310
317
|
|
|
@@ -119,37 +119,7 @@ runAdapterConformanceTests({
|
|
|
119
119
|
// (#1897) data-table no longer skipped — loop body children + wrapper
|
|
120
120
|
// struct + block-body memo baking render correctly on Go.
|
|
121
121
|
]),
|
|
122
|
-
skipDataPoints: new Set<string>(
|
|
123
|
-
// #2255 — Go `len` counts bytes; JS counts UTF-16 code units
|
|
124
|
-
// ('日本語' is 3 in JS, 9 here; '👍' is 2 in JS, 4 here).
|
|
125
|
-
'string-length-text:multibyte',
|
|
126
|
-
'string-length-text:astral',
|
|
127
|
-
// #2256 — the nullish gate covers only BARE nillable prop refs; a
|
|
128
|
-
// member-access left operand (`user?.name ?? '…'`) still lowers to
|
|
129
|
-
// the truthiness `or`, so a present-but-empty member falls back.
|
|
130
|
-
'optional-chaining-prop:empty-name',
|
|
131
|
-
// #2260 — controlled/derived boolean props: the SSR seed evaluates
|
|
132
|
-
// only the static fallback of `props.X ?? internal()` chains, so a
|
|
133
|
-
// caller-supplied true never reaches aria-*/data-state.
|
|
134
|
-
'toggle:gen:defaultPressed:true',
|
|
135
|
-
'toggle:gen:pressed:true',
|
|
136
|
-
'switch:gen:defaultChecked:true',
|
|
137
|
-
'switch:gen:checked:true',
|
|
138
|
-
'checkbox:gen:defaultChecked:true',
|
|
139
|
-
'checkbox:gen:checked:true',
|
|
140
|
-
// #2261 — invalid dynamic CSS value: html/template emits the
|
|
141
|
-
// ZgotmplZ sentinel where the oracle drops the property.
|
|
142
|
-
'style-object-dynamic:gen:color:markup',
|
|
143
|
-
// #2262 — dynamic `.flat` depth 0/negative renders empty instead of
|
|
144
|
-
// the contract's shallow copy.
|
|
145
|
-
'array-flat-dynamic-depth:gen:depth:zero',
|
|
146
|
-
'array-flat-dynamic-depth:gen:depth:negative',
|
|
147
|
-
// #2266 — asChild=true with plain-text children: the Slot define
|
|
148
|
-
// dereferences `.Children.Props.Children`, which hard-errors on a
|
|
149
|
-
// string child (`can't evaluate field Props in type interface {}`).
|
|
150
|
-
'button:gen:asChild:true',
|
|
151
|
-
'kbd:gen:asChild:true',
|
|
152
|
-
]),
|
|
122
|
+
skipDataPoints: new Set<string>(),
|
|
153
123
|
onRenderError: (err, id) => {
|
|
154
124
|
if (err instanceof GoNotAvailableError) {
|
|
155
125
|
console.log(`Skipping [${id}]: ${err.message}`)
|
|
@@ -1301,10 +1271,15 @@ export function Widget(props: P) {
|
|
|
1301
1271
|
expect(types).toContain('Classes: "a b" + " " + "c d" + " " + in.ClassName + " tail"')
|
|
1302
1272
|
})
|
|
1303
1273
|
|
|
1304
|
-
// A boolean ternary memo (`isChecked = ctrl() ? c() : i()`)
|
|
1305
|
-
// SSR
|
|
1306
|
-
// matches Hono's `aria-checked="false"
|
|
1307
|
-
|
|
1274
|
+
// A boolean ternary memo (`isChecked = ctrl() ? c() : i()`) types its
|
|
1275
|
+
// SSR field as `bool` (not `int`), so `aria-checked={isChecked()}`
|
|
1276
|
+
// matches Hono's `aria-checked="false"` shape. Since #2260, `checked`'s
|
|
1277
|
+
// presence is expressible (nillable `interface{}`), so the constructor
|
|
1278
|
+
// bakes a runtime presence check + type-asserted read instead of the
|
|
1279
|
+
// pre-#2260 unconditional `false` — see the `Controlled/derived
|
|
1280
|
+
// boolean props honour a caller-supplied true` describe block below for
|
|
1281
|
+
// the caller-supplied-`true` case this unlocks.
|
|
1282
|
+
test('boolean ternary memo types as bool, resolves controlled presence at SSR', () => {
|
|
1308
1283
|
const adapter = new GoTemplateAdapter()
|
|
1309
1284
|
const source = `
|
|
1310
1285
|
"use client"
|
|
@@ -1319,7 +1294,9 @@ export function Toggle(props: { checked?: boolean; defaultChecked?: boolean }) {
|
|
|
1319
1294
|
`
|
|
1320
1295
|
const types = adapter.generateTypes(compileToIR(source, adapter))!
|
|
1321
1296
|
expect(types).toContain('IsChecked bool')
|
|
1322
|
-
expect(types).toContain(
|
|
1297
|
+
expect(types).toContain(
|
|
1298
|
+
'IsChecked: func() bool { if in.Checked != nil { return func() bool { if v, ok := in.Checked.(bool); ok { return v }; return false }() }; return in.DefaultChecked }(),',
|
|
1299
|
+
)
|
|
1323
1300
|
})
|
|
1324
1301
|
})
|
|
1325
1302
|
|
|
@@ -4720,3 +4697,134 @@ export function List() {
|
|
|
4720
4697
|
}
|
|
4721
4698
|
})
|
|
4722
4699
|
})
|
|
4700
|
+
|
|
4701
|
+
// #2254: `??` in CONDITION position (ternaries/`{{if}}` via
|
|
4702
|
+
// `convertConditionToGo` → `renderConditionExpr`'s `logical` case) still
|
|
4703
|
+
// emitted Go's truthiness-based `or` even for a nillable-lowered prop, so a
|
|
4704
|
+
// present-but-empty (`''`) value wrongly fell back to the `??` default —
|
|
4705
|
+
// diverging from JS, where `??` only falls back on null/undefined. The
|
|
4706
|
+
// sibling `logical()` emitter (used for non-condition expression positions,
|
|
4707
|
+
// e.g. plain interpolation) already routed nillable operands through
|
|
4708
|
+
// `bf_nullish` since #2248/#2252; this brings the condition-position
|
|
4709
|
+
// emitter in line with it.
|
|
4710
|
+
describe('GoTemplateAdapter - #2254 `??` in condition position on a nillable prop', () => {
|
|
4711
|
+
test('ternary test comparing `props.label ?? "Default"` lowers to bf_nullish, not or', () => {
|
|
4712
|
+
const adapter = new GoTemplateAdapter()
|
|
4713
|
+
const result = compileJSX(`
|
|
4714
|
+
"use client"
|
|
4715
|
+
type Props = { label?: string }
|
|
4716
|
+
export function C(props: Props) {
|
|
4717
|
+
return <div>{(props.label ?? 'Default') === 'Default' ? <span>fallback</span> : <span>set</span>}</div>
|
|
4718
|
+
}
|
|
4719
|
+
`.trimStart(), 'test.tsx', { adapter })
|
|
4720
|
+
expect(result.errors ?? []).toEqual([])
|
|
4721
|
+
const template = result.files?.find(f => f.path.endsWith('.tmpl'))?.content ?? ''
|
|
4722
|
+
expect(template).toContain('bf_nullish .Label "Default"')
|
|
4723
|
+
expect(template).not.toContain('or .Label "Default"')
|
|
4724
|
+
})
|
|
4725
|
+
|
|
4726
|
+
test('end-to-end via real `go run`: a present-but-empty label does NOT take the ?? default branch', async () => {
|
|
4727
|
+
try {
|
|
4728
|
+
const html = await renderGoTemplateComponent({
|
|
4729
|
+
source: `
|
|
4730
|
+
'use client'
|
|
4731
|
+
type Props = { label?: string }
|
|
4732
|
+
export function C(props: Props) {
|
|
4733
|
+
return <div>{(props.label ?? 'Default') === 'Default' ? <span>fallback</span> : <span>set</span>}</div>
|
|
4734
|
+
}
|
|
4735
|
+
`,
|
|
4736
|
+
adapter: new GoTemplateAdapter(),
|
|
4737
|
+
props: { label: '' },
|
|
4738
|
+
})
|
|
4739
|
+
// JS: '' ?? 'Default' keeps '' (present, not nullish) → '' === 'Default'
|
|
4740
|
+
// is false → the "set" branch renders. The pre-fix `or` lowering would
|
|
4741
|
+
// truthiness-fallback the empty string to 'Default' and wrongly render
|
|
4742
|
+
// "fallback" instead.
|
|
4743
|
+
expect(html).toContain('>set</span>')
|
|
4744
|
+
expect(html).not.toContain('>fallback</span>')
|
|
4745
|
+
} catch (err) {
|
|
4746
|
+
if (err instanceof GoNotAvailableError) {
|
|
4747
|
+
console.log('Skipping #2254 e2e: go command not found')
|
|
4748
|
+
return
|
|
4749
|
+
}
|
|
4750
|
+
throw err
|
|
4751
|
+
}
|
|
4752
|
+
})
|
|
4753
|
+
|
|
4754
|
+
test('an omitted label DOES take the ?? default branch', async () => {
|
|
4755
|
+
try {
|
|
4756
|
+
const html = await renderGoTemplateComponent({
|
|
4757
|
+
source: `
|
|
4758
|
+
'use client'
|
|
4759
|
+
type Props = { label?: string }
|
|
4760
|
+
export function C(props: Props) {
|
|
4761
|
+
return <div>{(props.label ?? 'Default') === 'Default' ? <span>fallback</span> : <span>set</span>}</div>
|
|
4762
|
+
}
|
|
4763
|
+
`,
|
|
4764
|
+
adapter: new GoTemplateAdapter(),
|
|
4765
|
+
props: {},
|
|
4766
|
+
})
|
|
4767
|
+
expect(html).toContain('>fallback</span>')
|
|
4768
|
+
expect(html).not.toContain('>set</span>')
|
|
4769
|
+
} catch (err) {
|
|
4770
|
+
if (err instanceof GoNotAvailableError) {
|
|
4771
|
+
console.log('Skipping #2254 e2e: go command not found')
|
|
4772
|
+
return
|
|
4773
|
+
}
|
|
4774
|
+
throw err
|
|
4775
|
+
}
|
|
4776
|
+
})
|
|
4777
|
+
})
|
|
4778
|
+
|
|
4779
|
+
// Review finding on #2271 (the #2254 fix): `renderConditionExpr`'s `logical`
|
|
4780
|
+
// case wrapped operands via `needsParens` (AST-kind-based — only
|
|
4781
|
+
// `logical`/`unary`/`conditional`), missing any OTHER multi-token rendering
|
|
4782
|
+
// (`len .X`, `bf_add a b`, a call). Unwrapped, Go's `and`/`or`/`bf_nullish`
|
|
4783
|
+
// (all prefix builtins) parse a multi-token operand as extra sibling args
|
|
4784
|
+
// instead of one operand. Switched to `wrapIfMultiToken` (whitespace-based,
|
|
4785
|
+
// on the rendered string), matching the main `logical()` emitter.
|
|
4786
|
+
describe('GoTemplateAdapter - condition-position logical operand wrapping (#2271 review)', () => {
|
|
4787
|
+
test('a `.length` (member → "len .X") operand of `&&` inside a ternary TEST is parenthesized', () => {
|
|
4788
|
+
// A bare `{cond && <Elem/>}` is recognized as JSX conditional-rendering
|
|
4789
|
+
// shorthand and its test is extracted directly, never reaching
|
|
4790
|
+
// `renderConditionExpr`'s `logical` case as a literal `&&` node — so the
|
|
4791
|
+
// `&&` must be nested inside an explicit ternary test to exercise it.
|
|
4792
|
+
const adapter = new GoTemplateAdapter()
|
|
4793
|
+
const result = compileJSX(`
|
|
4794
|
+
"use client"
|
|
4795
|
+
type Item = { id: number }
|
|
4796
|
+
export function C({ items, enabled }: { items: Item[]; enabled: boolean }) {
|
|
4797
|
+
return <div>{(items.length && enabled) ? <span>on</span> : <span>off</span>}</div>
|
|
4798
|
+
}
|
|
4799
|
+
`.trimStart(), 'test.tsx', { adapter })
|
|
4800
|
+
expect(result.errors ?? []).toEqual([])
|
|
4801
|
+
const template = result.files?.find(f => f.path.endsWith('.tmpl'))?.content ?? ''
|
|
4802
|
+
// `and (len .Items) .Enabled` — NOT the broken 3-sibling-arg
|
|
4803
|
+
// `and len .Items .Enabled` a bare `needsParens` miss would produce.
|
|
4804
|
+
expect(template).toContain('and (len .Items) .Enabled')
|
|
4805
|
+
})
|
|
4806
|
+
|
|
4807
|
+
test('end-to-end via real `go run`: the multi-token `&&` ternary-test operand compiles and renders correctly', async () => {
|
|
4808
|
+
try {
|
|
4809
|
+
const html = await renderGoTemplateComponent({
|
|
4810
|
+
source: `
|
|
4811
|
+
'use client'
|
|
4812
|
+
type Item = { id: number }
|
|
4813
|
+
export function C({ items, enabled }: { items: Item[]; enabled: boolean }) {
|
|
4814
|
+
return <div>{(items.length && enabled) ? <span>on</span> : <span>off</span>}</div>
|
|
4815
|
+
}
|
|
4816
|
+
`,
|
|
4817
|
+
adapter: new GoTemplateAdapter(),
|
|
4818
|
+
props: { items: [{ id: 1 }], enabled: true },
|
|
4819
|
+
})
|
|
4820
|
+
expect(html).toContain('>on</span>')
|
|
4821
|
+
expect(html).not.toContain('>off</span>')
|
|
4822
|
+
} catch (err) {
|
|
4823
|
+
if (err instanceof GoNotAvailableError) {
|
|
4824
|
+
console.log('Skipping #2271 review e2e: go command not found')
|
|
4825
|
+
return
|
|
4826
|
+
}
|
|
4827
|
+
throw err
|
|
4828
|
+
}
|
|
4829
|
+
})
|
|
4830
|
+
})
|
|
@@ -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, collectNullishConsumedPropNames, collectOmittableAttrConsumedPropNames, NULLISH_SCALAR_GO_TYPES } 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"
|
|
@@ -419,6 +419,8 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
419
419
|
this.buildLocalTypeTables(ir, ir.metadata.componentName)
|
|
420
420
|
this.state.nullishConsumedPropNames = collectNullishConsumedPropNames(this.emitCtx, ir)
|
|
421
421
|
this.state.omittableAttrConsumedPropNames = collectOmittableAttrConsumedPropNames(this.emitCtx, ir)
|
|
422
|
+
this.state.textConsumedPropNames = collectTextConsumedPropNames(this.emitCtx, ir)
|
|
423
|
+
this.state.presenceCheckedPropNames = collectPresenceCheckedPropNames(this.emitCtx, ir)
|
|
422
424
|
this.state.nillablePropNames = collectNillablePropNames(this.emitCtx, ir)
|
|
423
425
|
}
|
|
424
426
|
|
|
@@ -3093,13 +3095,52 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
3093
3095
|
return goExpr
|
|
3094
3096
|
}
|
|
3095
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
|
+
|
|
3096
3110
|
// Mark expressions with slotId using comment nodes for client JS to find.
|
|
3097
3111
|
// This includes reactive expressions AND loop-param-dependent expressions.
|
|
3098
3112
|
if (expr.slotId) {
|
|
3099
|
-
return `{{bfTextStart "${expr.slotId}"}}{{${
|
|
3113
|
+
return `{{bfTextStart "${expr.slotId}"}}{{${finalExpr}}}{{bfTextEnd}}`
|
|
3100
3114
|
}
|
|
3101
3115
|
|
|
3102
|
-
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
|
|
3103
3144
|
}
|
|
3104
3145
|
|
|
3105
3146
|
/**
|
|
@@ -3512,7 +3553,13 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
3512
3553
|
}
|
|
3513
3554
|
|
|
3514
3555
|
const obj = emit(object)
|
|
3515
|
-
|
|
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)}`
|
|
3516
3563
|
// A `?.`-written access (`user?.name`, #2168 optional-chaining-prop):
|
|
3517
3564
|
// a plain `.Field` dot-chain panics evaluating a field on a nil
|
|
3518
3565
|
// interface/pointer (`nil pointer evaluating interface {}.Name`), so
|
|
@@ -3679,13 +3726,19 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
3679
3726
|
let name: string | null = null
|
|
3680
3727
|
if (expr.kind === 'identifier') {
|
|
3681
3728
|
name = expr.name
|
|
3682
|
-
} else if (
|
|
3683
|
-
expr.
|
|
3684
|
-
|
|
3685
|
-
|
|
3686
|
-
|
|
3687
|
-
|
|
3688
|
-
|
|
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
|
+
}
|
|
3689
3742
|
}
|
|
3690
3743
|
return name !== null &&
|
|
3691
3744
|
this.state.nullishConsumedPropNames.has(name) &&
|
|
@@ -4417,11 +4470,6 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
4417
4470
|
return this.renderFilterExpr(pred, param, new Map(), datumField ?? undefined)
|
|
4418
4471
|
}
|
|
4419
4472
|
|
|
4420
|
-
/** Whether an expression needs parentheses when used in and/or. */
|
|
4421
|
-
private needsParens(expr: ParsedExpr): boolean {
|
|
4422
|
-
return expr.kind === 'logical' || expr.kind === 'unary' || expr.kind === 'conditional'
|
|
4423
|
-
}
|
|
4424
|
-
|
|
4425
4473
|
/**
|
|
4426
4474
|
* Split a rendered template block into preamble + final expression.
|
|
4427
4475
|
* The last `{{...}}` must be a variable reference (`$bf_rN` or
|
|
@@ -5100,19 +5148,26 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
5100
5148
|
if (expr.callee.kind === 'identifier' && expr.args.length === 0) {
|
|
5101
5149
|
return plain(this.rootFieldRef(expr.callee.name))
|
|
5102
5150
|
}
|
|
5103
|
-
// `isValidElement(x)` — the framework "is this a renderable element
|
|
5104
|
-
//
|
|
5105
|
-
//
|
|
5106
|
-
//
|
|
5107
|
-
//
|
|
5108
|
-
//
|
|
5109
|
-
//
|
|
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`.
|
|
5110
5164
|
if (
|
|
5111
5165
|
expr.callee.kind === 'identifier' &&
|
|
5112
5166
|
(identifierPath(expr.callee) ?? expr.callee.name) === 'isValidElement' &&
|
|
5113
5167
|
expr.args.length === 1
|
|
5114
5168
|
) {
|
|
5115
|
-
|
|
5169
|
+
const inner = this.renderConditionExpr(expr.args[0])
|
|
5170
|
+
return { preamble: inner.preamble, expr: `(bf_is_element ${wrapIfMultiToken(inner.expr)})` }
|
|
5116
5171
|
}
|
|
5117
5172
|
// Any other user-defined predicate call with arguments (e.g.
|
|
5118
5173
|
// `isAdmin(user)`) has no server-side evaluator and is not a registered
|
|
@@ -5252,11 +5307,27 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
5252
5307
|
const leftResult = this.renderConditionExpr(expr.left)
|
|
5253
5308
|
const rightResult = this.renderConditionExpr(expr.right)
|
|
5254
5309
|
const preamble = leftResult.preamble + rightResult.preamble
|
|
5255
|
-
|
|
5256
|
-
|
|
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`.
|
|
5257
5326
|
const result = expr.op === '&&'
|
|
5258
5327
|
? `and ${wrapLeft} ${wrapRight}`
|
|
5259
|
-
:
|
|
5328
|
+
: expr.op === '??' && this.nillablePropNameOf(expr.left) !== null
|
|
5329
|
+
? `bf_nullish ${wrapLeft} ${wrapRight}`
|
|
5330
|
+
: `or ${wrapLeft} ${wrapRight}`
|
|
5260
5331
|
return { preamble, expr: result }
|
|
5261
5332
|
}
|
|
5262
5333
|
|
|
@@ -6119,17 +6190,22 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
6119
6190
|
for (const e of entries) {
|
|
6120
6191
|
if (e.kind === 'expr' && !isSupported(parseExpression(e.expr)).supported) return null
|
|
6121
6192
|
}
|
|
6122
|
-
//
|
|
6123
|
-
//
|
|
6124
|
-
//
|
|
6125
|
-
//
|
|
6126
|
-
|
|
6127
|
-
|
|
6128
|
-
|
|
6129
|
-
|
|
6130
|
-
|
|
6131
|
-
|
|
6132
|
-
|
|
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(' ')}}}`
|
|
6133
6209
|
}
|
|
6134
6210
|
|
|
6135
6211
|
private renderAttributes(element: IRElement): string {
|
|
@@ -164,6 +164,24 @@ export class CompileState {
|
|
|
164
164
|
*/
|
|
165
165
|
omittableAttrConsumedPropNames: Set<string> = new Set()
|
|
166
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
|
+
|
|
167
185
|
/**
|
|
168
186
|
* String-typed signal getter / prop names (#2168 string-concat-plus).
|
|
169
187
|
* Feeds `isStringName` for `isStringConcatBinary`, which decides whether a
|