@barefootjs/go-template 0.15.2 → 0.17.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/analysis/component-tree.d.ts +23 -0
- package/dist/adapter/analysis/component-tree.d.ts.map +1 -0
- package/dist/adapter/emit-context.d.ts +53 -0
- package/dist/adapter/emit-context.d.ts.map +1 -0
- package/dist/adapter/expr/helper-inline.d.ts +31 -0
- package/dist/adapter/expr/helper-inline.d.ts.map +1 -0
- package/dist/adapter/expr/url-builder.d.ts +23 -0
- package/dist/adapter/expr/url-builder.d.ts.map +1 -0
- package/dist/adapter/go-template-adapter.d.ts +291 -1070
- package/dist/adapter/go-template-adapter.d.ts.map +1 -1
- package/dist/adapter/index.js +2859 -2618
- package/dist/adapter/lib/compile-state.d.ts +116 -0
- package/dist/adapter/lib/compile-state.d.ts.map +1 -0
- package/dist/adapter/lib/constants.d.ts +11 -0
- package/dist/adapter/lib/constants.d.ts.map +1 -0
- package/dist/adapter/lib/go-emit.d.ts +117 -0
- package/dist/adapter/lib/go-emit.d.ts.map +1 -0
- package/dist/adapter/lib/go-naming.d.ts +39 -0
- package/dist/adapter/lib/go-naming.d.ts.map +1 -0
- package/dist/adapter/lib/ir-scope.d.ts +13 -0
- package/dist/adapter/lib/ir-scope.d.ts.map +1 -0
- package/dist/adapter/lib/types.d.ts +165 -0
- package/dist/adapter/lib/types.d.ts.map +1 -0
- package/dist/adapter/memo/ctor-lowering.d.ts +39 -0
- package/dist/adapter/memo/ctor-lowering.d.ts.map +1 -0
- package/dist/adapter/memo/memo-compute.d.ts +124 -0
- package/dist/adapter/memo/memo-compute.d.ts.map +1 -0
- package/dist/adapter/memo/memo-type.d.ts +38 -0
- package/dist/adapter/memo/memo-type.d.ts.map +1 -0
- package/dist/adapter/memo/memo-value.d.ts +64 -0
- package/dist/adapter/memo/memo-value.d.ts.map +1 -0
- package/dist/adapter/memo/template-interp.d.ts +43 -0
- package/dist/adapter/memo/template-interp.d.ts.map +1 -0
- package/dist/adapter/props/prop-types.d.ts +31 -0
- package/dist/adapter/props/prop-types.d.ts.map +1 -0
- package/dist/adapter/spread/spread-codegen.d.ts +40 -0
- package/dist/adapter/spread/spread-codegen.d.ts.map +1 -0
- package/dist/adapter/type/type-codegen.d.ts +25 -0
- package/dist/adapter/type/type-codegen.d.ts.map +1 -0
- package/dist/adapter/value/parsed-literal-to-go.d.ts +17 -0
- package/dist/adapter/value/parsed-literal-to-go.d.ts.map +1 -0
- package/dist/adapter/value/value-lowering.d.ts +46 -0
- package/dist/adapter/value/value-lowering.d.ts.map +1 -0
- package/dist/build.js +2857 -2616
- package/dist/index.js +2859 -2618
- package/dist/test-render.d.ts.map +1 -1
- package/package.json +3 -3
- package/src/__tests__/derived-state-memo.test.ts +155 -84
- package/src/__tests__/go-template-adapter.test.ts +211 -60
- package/src/__tests__/lowering-plugin.test.ts +109 -0
- package/src/__tests__/query-href.test.ts +179 -0
- package/src/adapter/analysis/component-tree.ts +174 -0
- package/src/adapter/emit-context.ts +59 -0
- package/src/adapter/expr/helper-inline.ts +274 -0
- package/src/adapter/expr/url-builder.ts +123 -0
- package/src/adapter/go-template-adapter.ts +2100 -5316
- package/src/adapter/lib/compile-state.ts +150 -0
- package/src/adapter/lib/constants.ts +24 -0
- package/src/adapter/lib/go-emit.ts +289 -0
- package/src/adapter/lib/go-naming.ts +84 -0
- package/src/adapter/lib/ir-scope.ts +31 -0
- package/src/adapter/lib/types.ts +182 -0
- package/src/adapter/memo/ctor-lowering.ts +267 -0
- package/src/adapter/memo/memo-compute.ts +451 -0
- package/src/adapter/memo/memo-type.ts +74 -0
- package/src/adapter/memo/memo-value.ts +197 -0
- package/src/adapter/memo/template-interp.ts +246 -0
- package/src/adapter/props/prop-types.ts +84 -0
- package/src/adapter/spread/spread-codegen.ts +458 -0
- package/src/adapter/type/type-codegen.ts +95 -0
- package/src/adapter/value/parsed-literal-to-go.ts +94 -0
- package/src/adapter/value/value-lowering.ts +162 -0
- package/src/test-render.ts +2 -14
|
@@ -0,0 +1,458 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Spread-bag codegen: lower JSX `{...spread}` attributes to Go.
|
|
3
|
+
*
|
|
4
|
+
* Free functions over a {@link GoEmitContext}. Two entry points:
|
|
5
|
+
* - `collectSpreadSlots` walks the IR (stopping at loop bodies) to gather the
|
|
6
|
+
* `SpreadSlotInfo` entries plumbed onto the Input/Props structs.
|
|
7
|
+
* - `buildSpreadInitializer` builds the Go expression for a spread bag's
|
|
8
|
+
* initial value placed inside `NewXxxProps`: signal-getter object literals,
|
|
9
|
+
* destructured / SolidJS-style / rest props, and conditional inline-object
|
|
10
|
+
* spreads.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import ts from 'typescript'
|
|
14
|
+
|
|
15
|
+
import { parseExpression, parseRecordIndexAccess } from '@barefootjs/jsx'
|
|
16
|
+
import type {
|
|
17
|
+
ComponentIR,
|
|
18
|
+
IRNode,
|
|
19
|
+
IRElement,
|
|
20
|
+
IRFragment,
|
|
21
|
+
IRConditional,
|
|
22
|
+
IRIfStatement,
|
|
23
|
+
IRComponent,
|
|
24
|
+
IRProvider,
|
|
25
|
+
IRAsync,
|
|
26
|
+
ParsedExpr,
|
|
27
|
+
} from '@barefootjs/jsx'
|
|
28
|
+
|
|
29
|
+
import type { GoEmitContext } from '../emit-context.ts'
|
|
30
|
+
import type { SpreadSlotInfo } from '../lib/types.ts'
|
|
31
|
+
import { capitalizeFieldName } from '../lib/go-naming.ts'
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Walk the IR (elements, fragments, conditionals, providers, async, components)
|
|
35
|
+
* but stop at loop bodies. Each `'spread'` attr value with a `slotId` becomes
|
|
36
|
+
* one `SpreadSlotInfo` entry.
|
|
37
|
+
*/
|
|
38
|
+
export function collectSpreadSlots(ctx: GoEmitContext, node: IRNode): SpreadSlotInfo[] {
|
|
39
|
+
const result: SpreadSlotInfo[] = []
|
|
40
|
+
collectSpreadSlotsRecursive(ctx, node, result)
|
|
41
|
+
return result
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Decide how a spread bag is plumbed onto the Input/Props structs. A
|
|
46
|
+
* bare-identifier spread matching `restPropsName` is open-ended (Go can't
|
|
47
|
+
* enumerate the keys), so the caller supplies the bag via an Input-side
|
|
48
|
+
* `map[string]any` field (`input-bag`). Every other shape — signal getter,
|
|
49
|
+
* `propsObjectName`, plain propsParam, object literal — is built inline in
|
|
50
|
+
* `NewXxxProps` from compile-time-known data (`inline`).
|
|
51
|
+
*/
|
|
52
|
+
function classifySpreadBagSource(ctx: GoEmitContext, spreadExpr: string): 'input-bag' | 'inline' {
|
|
53
|
+
const trimmed = spreadExpr.trim()
|
|
54
|
+
if (/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(trimmed)
|
|
55
|
+
&& ctx.state.restPropsName === trimmed) {
|
|
56
|
+
return 'input-bag'
|
|
57
|
+
}
|
|
58
|
+
return 'inline'
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function collectSpreadSlotsRecursive(ctx: GoEmitContext, node: IRNode, result: SpreadSlotInfo[]): void {
|
|
62
|
+
if (node.type === 'element') {
|
|
63
|
+
const element = node as IRElement
|
|
64
|
+
for (const attr of element.attrs) {
|
|
65
|
+
if (attr.value.kind !== 'spread') continue
|
|
66
|
+
if (!attr.value.slotId) continue
|
|
67
|
+
result.push({
|
|
68
|
+
slotId: attr.value.slotId,
|
|
69
|
+
expr: attr.value.expr,
|
|
70
|
+
parsed: attr.value.parsed,
|
|
71
|
+
templateExpr: attr.value.templateExpr,
|
|
72
|
+
bagSource: classifySpreadBagSource(ctx, attr.value.expr),
|
|
73
|
+
})
|
|
74
|
+
}
|
|
75
|
+
for (const child of element.children) {
|
|
76
|
+
collectSpreadSlotsRecursive(ctx, child, result)
|
|
77
|
+
}
|
|
78
|
+
return
|
|
79
|
+
}
|
|
80
|
+
if (node.type === 'fragment') {
|
|
81
|
+
const fragment = node as IRFragment
|
|
82
|
+
for (const child of fragment.children) {
|
|
83
|
+
collectSpreadSlotsRecursive(ctx, child, result)
|
|
84
|
+
}
|
|
85
|
+
return
|
|
86
|
+
}
|
|
87
|
+
if (node.type === 'conditional') {
|
|
88
|
+
const cond = node as IRConditional
|
|
89
|
+
collectSpreadSlotsRecursive(ctx, cond.whenTrue, result)
|
|
90
|
+
if (cond.whenFalse) collectSpreadSlotsRecursive(ctx, cond.whenFalse, result)
|
|
91
|
+
return
|
|
92
|
+
}
|
|
93
|
+
if (node.type === 'if-statement') {
|
|
94
|
+
const stmt = node as IRIfStatement
|
|
95
|
+
collectSpreadSlotsRecursive(ctx, stmt.consequent, result)
|
|
96
|
+
if (stmt.alternate) collectSpreadSlotsRecursive(ctx, stmt.alternate, result)
|
|
97
|
+
return
|
|
98
|
+
}
|
|
99
|
+
if (node.type === 'component') {
|
|
100
|
+
const comp = node as IRComponent
|
|
101
|
+
// `IRComponent.children` are the JSX children passed to *this* instance at
|
|
102
|
+
// the call site (`<Child>...</Child>`) — part of the PARENT's IR, evaluated
|
|
103
|
+
// in the parent's render scope, so spreads inside them belong on the
|
|
104
|
+
// parent's Props struct. The child's own template body is a separate
|
|
105
|
+
// `ComponentIR` compiled in a separate `generate()` pass, so the recursion
|
|
106
|
+
// never crosses a component boundary and per-component `spreadIdCounter`
|
|
107
|
+
// can't collide across unrelated components.
|
|
108
|
+
for (const child of comp.children) {
|
|
109
|
+
collectSpreadSlotsRecursive(ctx, child, result)
|
|
110
|
+
}
|
|
111
|
+
return
|
|
112
|
+
}
|
|
113
|
+
if (node.type === 'provider') {
|
|
114
|
+
const p = node as IRProvider
|
|
115
|
+
for (const child of p.children) {
|
|
116
|
+
collectSpreadSlotsRecursive(ctx, child, result)
|
|
117
|
+
}
|
|
118
|
+
return
|
|
119
|
+
}
|
|
120
|
+
if (node.type === 'async') {
|
|
121
|
+
const a = node as IRAsync
|
|
122
|
+
collectSpreadSlotsRecursive(ctx, a.fallback, result)
|
|
123
|
+
for (const child of a.children) {
|
|
124
|
+
collectSpreadSlotsRecursive(ctx, child, result)
|
|
125
|
+
}
|
|
126
|
+
return
|
|
127
|
+
}
|
|
128
|
+
// Loops are intentionally not descended — loop-internal spreads emit
|
|
129
|
+
// `{{bf_spread_attrs <go-expr>}}` inline from `elementAttrEmitter.emitSpread`
|
|
130
|
+
// instead of plumbing through a Props struct field.
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Lower a signal's carried object-literal initial value into a Go
|
|
135
|
+
* `map[string]any{...}` literal. Conservative subset: string/number/boolean/null
|
|
136
|
+
* values (and a signed-number `{count: -1}`) keyed by identifier or
|
|
137
|
+
* string-literal keys.
|
|
138
|
+
*
|
|
139
|
+
* @returns `null` (→ caller falls back to BF101) for any other shape — a
|
|
140
|
+
* non-object init, or a shorthand / nested-object / computed / call value.
|
|
141
|
+
*/
|
|
142
|
+
function parsedObjectLiteralToGoMap(parsed: ParsedExpr | undefined): string | null {
|
|
143
|
+
if (!parsed || parsed.kind !== 'object-literal') return null
|
|
144
|
+
const entries: string[] = []
|
|
145
|
+
for (const prop of parsed.properties) {
|
|
146
|
+
// Reject a numeric key (`{ 1: 'a' }`); `keyKind` distinguishes it from a
|
|
147
|
+
// string `'1'` key.
|
|
148
|
+
if (prop.keyKind === 'numeric') return null
|
|
149
|
+
const v = prop.value
|
|
150
|
+
let goVal: string
|
|
151
|
+
if (v.kind === 'literal' && v.literalType === 'string') {
|
|
152
|
+
goVal = JSON.stringify(v.value)
|
|
153
|
+
} else if (v.kind === 'literal' && v.literalType === 'number') {
|
|
154
|
+
// `raw` is the exact numeric token; `value` is the fallback.
|
|
155
|
+
goVal = v.raw ?? String(v.value)
|
|
156
|
+
} else if (
|
|
157
|
+
// `-1` / `+1` parse as a unary over a numeric literal — accept both signs.
|
|
158
|
+
v.kind === 'unary'
|
|
159
|
+
&& (v.op === '-' || v.op === '+')
|
|
160
|
+
&& v.argument.kind === 'literal'
|
|
161
|
+
&& v.argument.literalType === 'number'
|
|
162
|
+
) {
|
|
163
|
+
const sign = v.op === '-' ? '-' : ''
|
|
164
|
+
goVal = `${sign}${v.argument.raw ?? String(v.argument.value)}`
|
|
165
|
+
} else if (v.kind === 'literal' && v.literalType === 'boolean') {
|
|
166
|
+
goVal = v.value ? 'true' : 'false'
|
|
167
|
+
} else if (v.kind === 'literal' && v.literalType === 'null') {
|
|
168
|
+
goVal = 'nil'
|
|
169
|
+
} else {
|
|
170
|
+
return null
|
|
171
|
+
}
|
|
172
|
+
entries.push(`${JSON.stringify(prop.key)}: ${goVal}`)
|
|
173
|
+
}
|
|
174
|
+
return `map[string]any{${entries.join(', ')}}`
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Build a Go expression for a JSX spread bag's initial value, placed inside
|
|
179
|
+
* `NewXxxProps`'s return literal.
|
|
180
|
+
*
|
|
181
|
+
* Supported shapes:
|
|
182
|
+
* - Signal-getter call (`attrs()`): emit the signal's object literal as a Go
|
|
183
|
+
* `map[string]any{...}`.
|
|
184
|
+
* - Bare identifier matching a destructured `propsParam`: emit `in.<Field>`.
|
|
185
|
+
* - Bare identifier matching `propsObjectName` (SolidJS-style `props`):
|
|
186
|
+
* enumerate `propsParams` into an inline `map[string]any{...}` (each Input
|
|
187
|
+
* field becomes a bag key).
|
|
188
|
+
* - Bare identifier matching `restPropsName` (destructured rest): emit
|
|
189
|
+
* `in.<Field>` against the `map[string]any` Input field added for
|
|
190
|
+
* `input-bag` slots; the caller populates the open-ended rest values.
|
|
191
|
+
*
|
|
192
|
+
* @returns `null` for unsupported shapes so the caller can raise a narrowed
|
|
193
|
+
* BF101 with the offending expression.
|
|
194
|
+
*/
|
|
195
|
+
export function buildSpreadInitializer(
|
|
196
|
+
ctx: GoEmitContext,
|
|
197
|
+
spreadExpr: string,
|
|
198
|
+
ir: ComponentIR,
|
|
199
|
+
parsed?: ParsedExpr,
|
|
200
|
+
): string | null {
|
|
201
|
+
const trimmed = spreadExpr.trim()
|
|
202
|
+
// Conditional inline-object spread `{...(COND ? { 'k': v } : {})}` (either
|
|
203
|
+
// branch possibly `{}`). The falsy branch yields an empty map so the key is
|
|
204
|
+
// OMITTED rather than rendered as `k=""` (`SpreadAttrs` does NOT filter empty
|
|
205
|
+
// strings). Consume the carried `parsed` tree; when absent (older/hand-built
|
|
206
|
+
// IR), parse `trimmed` once as a fallback.
|
|
207
|
+
const conditionalTree = parsed ?? parseExpression(trimmed)
|
|
208
|
+
const conditional = buildConditionalSpreadInitializer(ctx, conditionalTree, ir)
|
|
209
|
+
if (conditional !== undefined) return conditional
|
|
210
|
+
// Signal-getter call `attrs()` — translate the signal's object literal to a
|
|
211
|
+
// Go map literal.
|
|
212
|
+
const callMatch = /^([a-zA-Z_][a-zA-Z0-9_]*)\s*\(\s*\)$/.exec(trimmed)
|
|
213
|
+
if (callMatch) {
|
|
214
|
+
const getterName = callMatch[1]
|
|
215
|
+
const signal = ir.metadata.signals.find(s => s.getter === getterName)
|
|
216
|
+
if (signal && signal.initialValue) {
|
|
217
|
+
const goMap = parsedObjectLiteralToGoMap(signal.parsed)
|
|
218
|
+
if (goMap) return goMap
|
|
219
|
+
}
|
|
220
|
+
return null
|
|
221
|
+
}
|
|
222
|
+
// Bare-identifier paths.
|
|
223
|
+
if (/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(trimmed)) {
|
|
224
|
+
// 1. Destructured-from-props parameter `function({ extras }: P)` →
|
|
225
|
+
// `{...extras}` resolves to `in.Extras`.
|
|
226
|
+
const param = ir.metadata.propsParams.find(p => p.name === trimmed)
|
|
227
|
+
if (param) {
|
|
228
|
+
return `in.${capitalizeFieldName(param.name)}`
|
|
229
|
+
}
|
|
230
|
+
// 2. SolidJS-style props object: `function(props: P)` → spread
|
|
231
|
+
// `{...props}` enumerates all propsParams into a `map[string]any`
|
|
232
|
+
// literal; every Input field becomes a bag key. When `propsParams` is
|
|
233
|
+
// empty (analyzer couldn't enumerate the type), the literal is
|
|
234
|
+
// `map[string]any{}`: SSR renders no spread attrs, but the CSR
|
|
235
|
+
// `applyRestAttrs` hydrate path still applies them — worse than full
|
|
236
|
+
// enumeration, better than BF101 blocking the build.
|
|
237
|
+
if (ir.metadata.propsObjectName === trimmed) {
|
|
238
|
+
const entries = ir.metadata.propsParams.map(p =>
|
|
239
|
+
`${JSON.stringify(p.name)}: in.${capitalizeFieldName(p.name)}`,
|
|
240
|
+
)
|
|
241
|
+
return `map[string]any{${entries.join(', ')}}`
|
|
242
|
+
}
|
|
243
|
+
// 3. Destructured-rest identifier `function({a, ...rest}: P)`. The rest's
|
|
244
|
+
// key set is open-ended, so `generateInputStruct` added an Input field
|
|
245
|
+
// named after the rest binding (`rest` → `Rest`); callers write
|
|
246
|
+
// `XxxInput{Rest: ...}` with the same identifier they saw in source.
|
|
247
|
+
// Forward it through.
|
|
248
|
+
if (ir.metadata.restPropsName === trimmed) {
|
|
249
|
+
return `in.${capitalizeFieldName(trimmed)}`
|
|
250
|
+
}
|
|
251
|
+
// 4. Function-scope local const holding a conditional inline-object spread:
|
|
252
|
+
// `const sizeAttrs = size ? {…} : {}` then `{...sizeAttrs}`. Resolve to
|
|
253
|
+
// its initializer text and route through the conditional-spread lowering.
|
|
254
|
+
// Only function-scope (`!isModule`) consts qualify, and the initializer
|
|
255
|
+
// must itself be a conditional-of-object-literals (else fall through to
|
|
256
|
+
// BF101).
|
|
257
|
+
const localConst = (ir.metadata.localConstants ?? []).find(
|
|
258
|
+
c => c.name === trimmed && !c.isModule,
|
|
259
|
+
)
|
|
260
|
+
if (localConst?.value !== undefined) {
|
|
261
|
+
const initTrimmed = localConst.value.trim()
|
|
262
|
+
// Reject a const resolving to a bare identifier to avoid an unbounded
|
|
263
|
+
// resolution loop / non-literal forwarding.
|
|
264
|
+
if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(initTrimmed)) {
|
|
265
|
+
const resolved = buildConditionalSpreadInitializer(ctx, parseExpression(initTrimmed), ir)
|
|
266
|
+
// `undefined` → not a conditional-spread shape; fall through to
|
|
267
|
+
// BF101. `null` → that shape but unconvertible; also BF101.
|
|
268
|
+
if (resolved) return resolved
|
|
269
|
+
if (resolved === null) return null
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
return null
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Lower a conditional inline-object spread `(COND ? { 'aria-describedby':
|
|
278
|
+
* describedBy } : {})` into a Go IIFE that conditionally builds the map (the
|
|
279
|
+
* falsy branch OMITS the key rather than rendering it as an empty string, which
|
|
280
|
+
* `SpreadAttrs` does not filter):
|
|
281
|
+
*
|
|
282
|
+
* func() map[string]any {
|
|
283
|
+
* if bf.Truthy(in.DescribedBy) {
|
|
284
|
+
* return map[string]any{"aria-describedby": in.DescribedBy}
|
|
285
|
+
* }
|
|
286
|
+
* return map[string]any{}
|
|
287
|
+
* }()
|
|
288
|
+
*
|
|
289
|
+
* @returns `undefined` when the expression is NOT a ternary of object literals
|
|
290
|
+
* (caller tries other shapes); `null` when it IS that shape but a part can't
|
|
291
|
+
* be converted (non-static key, unsupported condition) → caller raises BF101;
|
|
292
|
+
* the Go IIFE string when fully convertible.
|
|
293
|
+
*/
|
|
294
|
+
function buildConditionalSpreadInitializer(
|
|
295
|
+
ctx: GoEmitContext,
|
|
296
|
+
expr: ParsedExpr | undefined,
|
|
297
|
+
ir: ComponentIR,
|
|
298
|
+
): string | null | undefined {
|
|
299
|
+
// `parseExpression` already unwraps redundant parentheses.
|
|
300
|
+
if (!expr || expr.kind !== 'conditional') return undefined
|
|
301
|
+
const whenTrue = expr.consequent
|
|
302
|
+
const whenFalse = expr.alternate
|
|
303
|
+
if (whenTrue.kind !== 'object-literal' || whenFalse.kind !== 'object-literal') {
|
|
304
|
+
return undefined
|
|
305
|
+
}
|
|
306
|
+
// Condition → Go bool against `in.`, type-aware on the prop.
|
|
307
|
+
const goCond = conditionToGoBool(expr.test, ir)
|
|
308
|
+
if (goCond === null) return null
|
|
309
|
+
const trueMap = objectLiteralToGoSpreadMap(ctx, whenTrue, ir)
|
|
310
|
+
const falseMap = objectLiteralToGoSpreadMap(ctx, whenFalse, ir)
|
|
311
|
+
if (trueMap === null || falseMap === null) return null
|
|
312
|
+
return (
|
|
313
|
+
`func() map[string]any {\n` +
|
|
314
|
+
`\t\tif ${goCond} {\n` +
|
|
315
|
+
`\t\t\treturn ${trueMap}\n` +
|
|
316
|
+
`\t\t}\n` +
|
|
317
|
+
`\t\treturn ${falseMap}\n` +
|
|
318
|
+
`\t}()`
|
|
319
|
+
)
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* Convert a conditional-spread condition to a Go bool in the `in.` context.
|
|
324
|
+
* Supports a bare prop identifier (`describedBy`) and its negation, type-aware:
|
|
325
|
+
* string → `in.X != ""`
|
|
326
|
+
* boolean → `in.X`
|
|
327
|
+
* number → `in.X != 0`
|
|
328
|
+
* unknown / interface{} → `bf.Truthy(in.X)`
|
|
329
|
+
* For interface{}, `bf.Truthy` gives faithful JS truthiness; a string-biased
|
|
330
|
+
* `!= ""` test would misread an interface holding `0` / `false` as truthy.
|
|
331
|
+
*
|
|
332
|
+
* @returns `null` for any other shape (caller → BF101).
|
|
333
|
+
*/
|
|
334
|
+
function conditionToGoBool(
|
|
335
|
+
condition: ParsedExpr,
|
|
336
|
+
ir: ComponentIR,
|
|
337
|
+
): string | null {
|
|
338
|
+
let node = condition
|
|
339
|
+
let negate = false
|
|
340
|
+
if (node.kind === 'unary' && node.op === '!') {
|
|
341
|
+
negate = true
|
|
342
|
+
node = node.argument
|
|
343
|
+
}
|
|
344
|
+
if (node.kind !== 'identifier') return null
|
|
345
|
+
const param = ir.metadata.propsParams.find(p => p.name === node.name)
|
|
346
|
+
if (!param) return null
|
|
347
|
+
const field = `in.${capitalizeFieldName(param.name)}`
|
|
348
|
+
const prim = param.type.kind === 'primitive' ? param.type.primitive : undefined
|
|
349
|
+
let truthy: string
|
|
350
|
+
if (prim === 'boolean') {
|
|
351
|
+
truthy = field
|
|
352
|
+
} else if (prim === 'number') {
|
|
353
|
+
truthy = `${field} != 0`
|
|
354
|
+
} else if (prim === 'string') {
|
|
355
|
+
truthy = `${field} != ""`
|
|
356
|
+
} else {
|
|
357
|
+
// unknown / interface{}: route through `bf.Truthy` (the `Boolean(x)`
|
|
358
|
+
// equivalent) for faithful JS truthiness; `!= ""` would misread an
|
|
359
|
+
// interface holding `0` / `false`.
|
|
360
|
+
truthy = `bf.Truthy(${field})`
|
|
361
|
+
}
|
|
362
|
+
if (!negate) return truthy
|
|
363
|
+
// Negation: `!` applies to the whole truthiness test.
|
|
364
|
+
if (prim === 'boolean') return `!${field}`
|
|
365
|
+
if (prim === 'number') return `${field} == 0`
|
|
366
|
+
if (prim === 'string') return `${field} == ""`
|
|
367
|
+
return `!bf.Truthy(${field})`
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
/**
|
|
371
|
+
* Convert a static object literal (`{ 'aria-describedby': describedBy }`) into a
|
|
372
|
+
* Go `map[string]any{...}` for a conditional spread. Only static
|
|
373
|
+
* string/identifier keys; values resolve prop identifiers to `in.Field` and
|
|
374
|
+
* string literals to Go string literals. Empty object → `map[string]any{}`.
|
|
375
|
+
*
|
|
376
|
+
* @returns `null` for any computed/spread/dynamic key or unsupported value
|
|
377
|
+
* (caller → BF101).
|
|
378
|
+
*/
|
|
379
|
+
function objectLiteralToGoSpreadMap(
|
|
380
|
+
ctx: GoEmitContext,
|
|
381
|
+
obj: Extract<ParsedExpr, { kind: 'object-literal' }>,
|
|
382
|
+
ir: ComponentIR,
|
|
383
|
+
): string | null {
|
|
384
|
+
const entries: string[] = []
|
|
385
|
+
for (const prop of obj.properties) {
|
|
386
|
+
// Shorthand (`{ describedBy }`) is unsupported.
|
|
387
|
+
if (prop.shorthand) return null
|
|
388
|
+
// Reject a numeric key (`{ 1: x }`); `keyKind` distinguishes it from a
|
|
389
|
+
// string `'1'` key.
|
|
390
|
+
if (prop.keyKind === 'numeric') return null
|
|
391
|
+
const key = prop.key
|
|
392
|
+
const val = prop.value
|
|
393
|
+
let goVal: string
|
|
394
|
+
if (val.kind === 'literal' && val.literalType === 'string') {
|
|
395
|
+
goVal = JSON.stringify(val.value)
|
|
396
|
+
} else if (val.kind === 'identifier') {
|
|
397
|
+
const param = ir.metadata.propsParams.find(p => p.name === val.name)
|
|
398
|
+
if (!param) return null
|
|
399
|
+
goVal = `in.${capitalizeFieldName(param.name)}`
|
|
400
|
+
} else {
|
|
401
|
+
const indexed = recordIndexAccessToGoMap(ctx, val, ir)
|
|
402
|
+
if (indexed === null) return null
|
|
403
|
+
goVal = indexed
|
|
404
|
+
}
|
|
405
|
+
entries.push(`${JSON.stringify(key)}: ${goVal}`)
|
|
406
|
+
}
|
|
407
|
+
return `map[string]any{${entries.join(', ')}}`
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
/**
|
|
411
|
+
* Lower a spread-object VALUE of the form `IDENT[KEY]` where:
|
|
412
|
+
* - `IDENT` resolves via `localConstants` to a MODULE-scope object literal
|
|
413
|
+
* whose property values are all scalar literals under static keys (a
|
|
414
|
+
* `Record<staticKeys, scalar>` map like `sizeMap`), AND
|
|
415
|
+
* - `KEY` is a bare identifier that is a prop.
|
|
416
|
+
* Emits an inline indexed Go map:
|
|
417
|
+
* `map[string]any{"sm": 16, ...}[fmt.Sprint(in.Size)]`
|
|
418
|
+
* (`fmt.Sprint` coerces the prop to the map's string key space — sets `usesFmt`
|
|
419
|
+
* so the `"fmt"` import is added).
|
|
420
|
+
*
|
|
421
|
+
* @returns the Go string, else `null` (caller → BF101) for any non-scalar
|
|
422
|
+
* value, non-static key, or non-prop index.
|
|
423
|
+
*/
|
|
424
|
+
function recordIndexAccessToGoMap(
|
|
425
|
+
ctx: GoEmitContext,
|
|
426
|
+
val: ParsedExpr,
|
|
427
|
+
ir: ComponentIR,
|
|
428
|
+
): string | null {
|
|
429
|
+
// `parseRecordIndexAccess` (shared parser) takes a `ts.Expression` and only
|
|
430
|
+
// accepts `IDENT[KEY]` with identifier object and index, so rebuild exactly
|
|
431
|
+
// that node from the carried tree via `ts.factory`. Any other shape
|
|
432
|
+
// short-circuits to `null` here.
|
|
433
|
+
if (
|
|
434
|
+
val.kind !== 'index-access'
|
|
435
|
+
|| val.object.kind !== 'identifier'
|
|
436
|
+
|| val.index.kind !== 'identifier'
|
|
437
|
+
) {
|
|
438
|
+
return null
|
|
439
|
+
}
|
|
440
|
+
const tsVal = ts.factory.createElementAccessExpression(
|
|
441
|
+
ts.factory.createIdentifier(val.object.name),
|
|
442
|
+
ts.factory.createIdentifier(val.index.name),
|
|
443
|
+
)
|
|
444
|
+
// Shared structural parse; this wrapper only does the Go-specific emit.
|
|
445
|
+
const parsed = parseRecordIndexAccess(
|
|
446
|
+
tsVal,
|
|
447
|
+
ir.metadata.localConstants ?? [],
|
|
448
|
+
ir.metadata.propsParams,
|
|
449
|
+
)
|
|
450
|
+
if (!parsed) return null
|
|
451
|
+
const entries = parsed.entries.map(e => {
|
|
452
|
+
const mapVal = e.value.kind === 'number' ? e.value.text : JSON.stringify(e.value.text)
|
|
453
|
+
return `${JSON.stringify(e.key)}: ${mapVal}`
|
|
454
|
+
})
|
|
455
|
+
ctx.state.usesFmt = true
|
|
456
|
+
const field = `in.${capitalizeFieldName(parsed.indexPropName)}`
|
|
457
|
+
return `map[string]any{${entries.join(', ')}}[fmt.Sprint(${field})]`
|
|
458
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Type codegen: render TypeScript types as Go type strings.
|
|
3
|
+
*
|
|
4
|
+
* Free functions over a {@link GoEmitContext}. They resolve a prop/signal/const's
|
|
5
|
+
* type (`TypeInfo`, a raw type string, or — as a last resort — an inferred shape
|
|
6
|
+
* from a literal value) into the Go type used for its struct field. They read
|
|
7
|
+
* only `state.localTypeNames`; `inferTypeFromValue` is fully pure.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { TypeInfo } from '@barefootjs/jsx'
|
|
11
|
+
|
|
12
|
+
import type { GoEmitContext } from '../emit-context.ts'
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Convert a `TypeInfo` to a Go type string.
|
|
16
|
+
*
|
|
17
|
+
* @param defaultValue used to infer the type when `typeInfo.kind` is `unknown`
|
|
18
|
+
* @returns the Go type, falling back to `interface{}` when unresolvable
|
|
19
|
+
*/
|
|
20
|
+
export function typeInfoToGo(
|
|
21
|
+
ctx: GoEmitContext,
|
|
22
|
+
typeInfo: TypeInfo,
|
|
23
|
+
defaultValue?: string,
|
|
24
|
+
): string {
|
|
25
|
+
switch (typeInfo.kind) {
|
|
26
|
+
case 'primitive':
|
|
27
|
+
switch (typeInfo.primitive) {
|
|
28
|
+
case 'string':
|
|
29
|
+
return 'string'
|
|
30
|
+
case 'number':
|
|
31
|
+
return 'int'
|
|
32
|
+
case 'boolean':
|
|
33
|
+
return 'bool'
|
|
34
|
+
default:
|
|
35
|
+
return 'interface{}'
|
|
36
|
+
}
|
|
37
|
+
case 'array':
|
|
38
|
+
if (typeInfo.elementType) {
|
|
39
|
+
return `[]${typeInfoToGo(ctx, typeInfo.elementType)}`
|
|
40
|
+
}
|
|
41
|
+
return '[]interface{}'
|
|
42
|
+
case 'object':
|
|
43
|
+
return 'map[string]interface{}'
|
|
44
|
+
case 'interface':
|
|
45
|
+
if (typeInfo.raw && ctx.state.localTypeNames.has(typeInfo.raw)) {
|
|
46
|
+
return typeInfo.raw
|
|
47
|
+
}
|
|
48
|
+
// Resolve a raw type string pattern (e.g. `Array<Todo>`).
|
|
49
|
+
if (typeInfo.raw) {
|
|
50
|
+
const resolved = tsTypeStringToGo(ctx, typeInfo.raw)
|
|
51
|
+
if (resolved !== 'interface{}') return resolved
|
|
52
|
+
}
|
|
53
|
+
return 'interface{}'
|
|
54
|
+
case 'unknown':
|
|
55
|
+
if (defaultValue !== undefined) {
|
|
56
|
+
return inferTypeFromValue(defaultValue)
|
|
57
|
+
}
|
|
58
|
+
return 'interface{}'
|
|
59
|
+
default:
|
|
60
|
+
return 'interface{}'
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Convert a raw TypeScript type string to a Go type string. Handles primitives,
|
|
66
|
+
* `T[]` / `Array<T>` arrays, and known local types; else `interface{}`.
|
|
67
|
+
*/
|
|
68
|
+
export function tsTypeStringToGo(ctx: GoEmitContext, tsType: string): string {
|
|
69
|
+
const t = tsType.trim()
|
|
70
|
+
if (t === 'number') return 'int'
|
|
71
|
+
if (t === 'string') return 'string'
|
|
72
|
+
if (t === 'boolean' || t === 'bool') return 'bool'
|
|
73
|
+
if (t.endsWith('[]')) {
|
|
74
|
+
const elem = t.slice(0, -2)
|
|
75
|
+
return `[]${tsTypeStringToGo(ctx, elem)}`
|
|
76
|
+
}
|
|
77
|
+
const arrayMatch = t.match(/^Array<(.+)>$/)
|
|
78
|
+
if (arrayMatch) return `[]${tsTypeStringToGo(ctx, arrayMatch[1])}`
|
|
79
|
+
if (ctx.state.localTypeNames.has(t)) return t
|
|
80
|
+
return 'interface{}'
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** Infer a Go type from a JS value literal; `interface{}` when unrecognized. */
|
|
84
|
+
export function inferTypeFromValue(value: string): string {
|
|
85
|
+
if (value === 'true' || value === 'false') return 'bool'
|
|
86
|
+
if (/^-?\d+$/.test(value)) return 'int'
|
|
87
|
+
if (/^-?\d+\.\d+$/.test(value)) return 'float64'
|
|
88
|
+
if ((value.startsWith("'") && value.endsWith("'")) ||
|
|
89
|
+
(value.startsWith('"') && value.endsWith('"'))) {
|
|
90
|
+
return 'string'
|
|
91
|
+
}
|
|
92
|
+
if (value === '""' || value === "''") return 'string'
|
|
93
|
+
if (value.startsWith('[')) return '[]interface{}'
|
|
94
|
+
return 'interface{}'
|
|
95
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lower a structured literal `ParsedExpr` (carried on the IR) to a Go literal,
|
|
3
|
+
* for baking a signal's inline initial value into the SSR data context.
|
|
4
|
+
*
|
|
5
|
+
* Covers scalar literals, a unary-minus number, arrays of those, and object
|
|
6
|
+
* literals baked against a concrete local struct.
|
|
7
|
+
*
|
|
8
|
+
* Contract is **null-means-defer**: returns null for anything not reproduced
|
|
9
|
+
* exactly — an object whose target type isn't a known struct, a key the struct
|
|
10
|
+
* doesn't declare, a nested object/array property value, an empty array, an
|
|
11
|
+
* identifier/call, or a numeric literal missing its `raw` token. The caller
|
|
12
|
+
* then keeps `nil`.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import type { ParsedExpr, TypeInfo } from '@barefootjs/jsx'
|
|
16
|
+
|
|
17
|
+
import type { GoEmitContext } from '../emit-context.ts'
|
|
18
|
+
import { typeInfoToGo } from '../type/type-codegen.ts'
|
|
19
|
+
|
|
20
|
+
export function parsedLiteralToGo(
|
|
21
|
+
ctx: GoEmitContext,
|
|
22
|
+
expr: ParsedExpr,
|
|
23
|
+
typeInfo?: TypeInfo,
|
|
24
|
+
): string | null {
|
|
25
|
+
// Leading unary minus on a numeric literal (`-1`), from the carried `raw`
|
|
26
|
+
// token.
|
|
27
|
+
if (
|
|
28
|
+
expr.kind === 'unary' &&
|
|
29
|
+
expr.op === '-' &&
|
|
30
|
+
expr.argument.kind === 'literal' &&
|
|
31
|
+
expr.argument.literalType === 'number'
|
|
32
|
+
) {
|
|
33
|
+
return expr.argument.raw !== undefined ? `-${expr.argument.raw}` : null
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (expr.kind === 'literal') {
|
|
37
|
+
switch (expr.literalType) {
|
|
38
|
+
case 'string':
|
|
39
|
+
// `value` is the unquoted text; `JSON.stringify` re-quotes it for Go.
|
|
40
|
+
return JSON.stringify(expr.value)
|
|
41
|
+
case 'number':
|
|
42
|
+
// Need the exact source token; without it the value could change
|
|
43
|
+
// spelling / lose precision, so defer.
|
|
44
|
+
return expr.raw ?? null
|
|
45
|
+
case 'boolean':
|
|
46
|
+
return expr.value ? 'true' : 'false'
|
|
47
|
+
case 'null':
|
|
48
|
+
return 'nil'
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (expr.kind === 'array-literal') {
|
|
53
|
+
// Empty array → null so the field stays nil (JSON-marshals as `null`)
|
|
54
|
+
// rather than an empty slice.
|
|
55
|
+
if (expr.elements.length === 0) return null
|
|
56
|
+
const elemType = typeInfo?.kind === 'array' ? typeInfo.elementType : undefined
|
|
57
|
+
const sliceHeader = typeInfo?.kind === 'array' ? typeInfoToGo(ctx, typeInfo) : '[]interface{}'
|
|
58
|
+
const elems: string[] = []
|
|
59
|
+
for (const el of expr.elements) {
|
|
60
|
+
const go = parsedLiteralToGo(ctx, el, elemType)
|
|
61
|
+
// Any non-scalar element (object / call / identifier) defers the WHOLE
|
|
62
|
+
// array — struct-element baking is owned elsewhere.
|
|
63
|
+
if (go === null) return null
|
|
64
|
+
elems.push(go)
|
|
65
|
+
}
|
|
66
|
+
return `${sliceHeader}{${elems.join(', ')}}`
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (expr.kind === 'object-literal') {
|
|
70
|
+
// Bake against a concrete local struct only. The struct's field map is the
|
|
71
|
+
// source of truth for each source key's Go field name (and, by omission,
|
|
72
|
+
// which keys it declares); defer when the target type isn't a known struct.
|
|
73
|
+
const goType = typeInfo ? typeInfoToGo(ctx, typeInfo) : 'interface{}'
|
|
74
|
+
const structFields = ctx.state.localStructFields.get(goType)
|
|
75
|
+
if (!structFields) return null
|
|
76
|
+
const entries: string[] = []
|
|
77
|
+
for (const prop of expr.properties) {
|
|
78
|
+
// A shorthand `{ a }` carries an identifier value → lowers to null below
|
|
79
|
+
// and defers the whole object.
|
|
80
|
+
const goField = structFields.get(prop.key)
|
|
81
|
+
if (!goField) return null
|
|
82
|
+
// Nested object/array property values aren't baked here (field types
|
|
83
|
+
// untracked) — defer.
|
|
84
|
+
if (prop.value.kind === 'object-literal' || prop.value.kind === 'array-literal') return null
|
|
85
|
+
const go = parsedLiteralToGo(ctx, prop.value)
|
|
86
|
+
if (go === null) return null
|
|
87
|
+
entries.push(`${goField}: ${go}`)
|
|
88
|
+
}
|
|
89
|
+
return `${goType}{${entries.join(', ')}}`
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// identifier / call / member / … → defer.
|
|
93
|
+
return null
|
|
94
|
+
}
|