@barefootjs/go-template 0.16.0 → 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.
Files changed (73) hide show
  1. package/dist/adapter/analysis/component-tree.d.ts +23 -0
  2. package/dist/adapter/analysis/component-tree.d.ts.map +1 -0
  3. package/dist/adapter/emit-context.d.ts +53 -0
  4. package/dist/adapter/emit-context.d.ts.map +1 -0
  5. package/dist/adapter/expr/helper-inline.d.ts +31 -0
  6. package/dist/adapter/expr/helper-inline.d.ts.map +1 -0
  7. package/dist/adapter/expr/url-builder.d.ts +23 -0
  8. package/dist/adapter/expr/url-builder.d.ts.map +1 -0
  9. package/dist/adapter/go-template-adapter.d.ts +291 -1070
  10. package/dist/adapter/go-template-adapter.d.ts.map +1 -1
  11. package/dist/adapter/index.js +2857 -2618
  12. package/dist/adapter/lib/compile-state.d.ts +116 -0
  13. package/dist/adapter/lib/compile-state.d.ts.map +1 -0
  14. package/dist/adapter/lib/constants.d.ts +11 -0
  15. package/dist/adapter/lib/constants.d.ts.map +1 -0
  16. package/dist/adapter/lib/go-emit.d.ts +117 -0
  17. package/dist/adapter/lib/go-emit.d.ts.map +1 -0
  18. package/dist/adapter/lib/go-naming.d.ts +39 -0
  19. package/dist/adapter/lib/go-naming.d.ts.map +1 -0
  20. package/dist/adapter/lib/ir-scope.d.ts +13 -0
  21. package/dist/adapter/lib/ir-scope.d.ts.map +1 -0
  22. package/dist/adapter/lib/types.d.ts +165 -0
  23. package/dist/adapter/lib/types.d.ts.map +1 -0
  24. package/dist/adapter/memo/ctor-lowering.d.ts +39 -0
  25. package/dist/adapter/memo/ctor-lowering.d.ts.map +1 -0
  26. package/dist/adapter/memo/memo-compute.d.ts +124 -0
  27. package/dist/adapter/memo/memo-compute.d.ts.map +1 -0
  28. package/dist/adapter/memo/memo-type.d.ts +38 -0
  29. package/dist/adapter/memo/memo-type.d.ts.map +1 -0
  30. package/dist/adapter/memo/memo-value.d.ts +64 -0
  31. package/dist/adapter/memo/memo-value.d.ts.map +1 -0
  32. package/dist/adapter/memo/template-interp.d.ts +43 -0
  33. package/dist/adapter/memo/template-interp.d.ts.map +1 -0
  34. package/dist/adapter/props/prop-types.d.ts +31 -0
  35. package/dist/adapter/props/prop-types.d.ts.map +1 -0
  36. package/dist/adapter/spread/spread-codegen.d.ts +40 -0
  37. package/dist/adapter/spread/spread-codegen.d.ts.map +1 -0
  38. package/dist/adapter/type/type-codegen.d.ts +25 -0
  39. package/dist/adapter/type/type-codegen.d.ts.map +1 -0
  40. package/dist/adapter/value/parsed-literal-to-go.d.ts +17 -0
  41. package/dist/adapter/value/parsed-literal-to-go.d.ts.map +1 -0
  42. package/dist/adapter/value/value-lowering.d.ts +46 -0
  43. package/dist/adapter/value/value-lowering.d.ts.map +1 -0
  44. package/dist/build.js +2856 -2617
  45. package/dist/index.js +2857 -2618
  46. package/dist/test-render.d.ts.map +1 -1
  47. package/package.json +3 -3
  48. package/src/__tests__/derived-state-memo.test.ts +155 -84
  49. package/src/__tests__/go-template-adapter.test.ts +118 -54
  50. package/src/__tests__/lowering-plugin.test.ts +109 -0
  51. package/src/__tests__/query-href.test.ts +179 -0
  52. package/src/adapter/analysis/component-tree.ts +174 -0
  53. package/src/adapter/emit-context.ts +59 -0
  54. package/src/adapter/expr/helper-inline.ts +274 -0
  55. package/src/adapter/expr/url-builder.ts +123 -0
  56. package/src/adapter/go-template-adapter.ts +2099 -5325
  57. package/src/adapter/lib/compile-state.ts +150 -0
  58. package/src/adapter/lib/constants.ts +24 -0
  59. package/src/adapter/lib/go-emit.ts +289 -0
  60. package/src/adapter/lib/go-naming.ts +84 -0
  61. package/src/adapter/lib/ir-scope.ts +31 -0
  62. package/src/adapter/lib/types.ts +182 -0
  63. package/src/adapter/memo/ctor-lowering.ts +267 -0
  64. package/src/adapter/memo/memo-compute.ts +451 -0
  65. package/src/adapter/memo/memo-type.ts +74 -0
  66. package/src/adapter/memo/memo-value.ts +197 -0
  67. package/src/adapter/memo/template-interp.ts +246 -0
  68. package/src/adapter/props/prop-types.ts +84 -0
  69. package/src/adapter/spread/spread-codegen.ts +458 -0
  70. package/src/adapter/type/type-codegen.ts +95 -0
  71. package/src/adapter/value/parsed-literal-to-go.ts +94 -0
  72. package/src/adapter/value/value-lowering.ts +162 -0
  73. 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
+ }