@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.
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 +2859 -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 +2857 -2616
  45. package/dist/index.js +2859 -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 +211 -60
  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 +2100 -5316
  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,451 @@
1
+ /**
2
+ * Memo initial-value computation — lower a memo's computation to its SSR initial
3
+ * value as a Go expression.
4
+ *
5
+ * Free functions over a {@link GoEmitContext}. `computeMemoInitialValue` is the
6
+ * typed-field entry (zero-value defaulting); `computeMemoInitialValueOrNull` is
7
+ * the pattern-matching core dispatching over template-literal, parsed-body,
8
+ * comparison-ternary, block-body and object memo shapes.
9
+ */
10
+
11
+ import type { ParsedExpr, ParsedStatement, TypeInfo } from '@barefootjs/jsx'
12
+
13
+ import type { GoEmitContext } from '../emit-context.ts'
14
+ import type { PropFallbackVar } from '../lib/types.ts'
15
+ import { capitalizeFieldName } from '../lib/go-naming.ts'
16
+ import { convertInitialValue, getSignalInitialValueAsGo } from '../value/value-lowering.ts'
17
+ import { typeInfoToGo } from '../type/type-codegen.ts'
18
+ import { computeTemplateLiteralMemoInitialValue } from './template-interp.ts'
19
+ import { resolveBlockBodyMemoModuleConst, computeObjectMemoInitialValue } from './memo-value.ts'
20
+
21
+ /** Default for the optional `propFallbackVars` argument. */
22
+ const EMPTY_PROP_FALLBACK_VARS: ReadonlyMap<string, PropFallbackVar> = new Map()
23
+
24
+ /**
25
+ * Compute a memo's SSR initial value as a Go expression — e.g.
26
+ * `() => count() * 2` → `in.Initial * 2`, `() => props.value * 10` →
27
+ * `in.Value * 10`. Unresolved computations default to the memo's Go zero value.
28
+ *
29
+ * @param propFallbackVars when a hoisted fallback var exists for a referenced
30
+ * prop, it is substituted for `in.FieldName` so the memo inherits the
31
+ * signal-time `??` fallback
32
+ * @param goType Go type of the memo field, used to pick the zero-value fallback
33
+ */
34
+ export function computeMemoInitialValue(
35
+ ctx: GoEmitContext,
36
+ memo: { name: string; computation: string; deps: string[]; parsed?: ParsedExpr },
37
+ signals: { getter: string; initialValue: string }[],
38
+ propsParams: { name: string; type?: TypeInfo; defaultValue?: string }[],
39
+ propFallbackVars: ReadonlyMap<string, PropFallbackVar> = EMPTY_PROP_FALLBACK_VARS,
40
+ goType?: string,
41
+ ): string {
42
+ const resolved = computeMemoInitialValueOrNull(
43
+ ctx, memo, signals, propsParams, propFallbackVars,
44
+ )
45
+ if (resolved !== null) return resolved
46
+ // Zero value for the memo's Go type: `false` for bool, `""` for string,
47
+ // `nil` for reference types (map / slice / interface / pointer — `0` is not
48
+ // assignable to them), else the int `0`.
49
+ if (goType === 'bool') return 'false'
50
+ if (goType === 'string') return '""'
51
+ if (
52
+ goType !== undefined &&
53
+ (goType.startsWith('map[') ||
54
+ goType.startsWith('[]') ||
55
+ goType.startsWith('*') ||
56
+ goType.includes('interface{}') ||
57
+ goType === 'any')
58
+ ) {
59
+ return 'nil'
60
+ }
61
+ return '0'
62
+ }
63
+
64
+ /**
65
+ * Structural matcher for the common expression-bodied memo shapes, driven by
66
+ * the analyzer-attached `MemoInfo.parsed` (the memo arrow's body): `getter() ===
67
+ * 'lit'`, `props.X ?? false`, `cond() ? A : B`, `<getter()|props.X|var> <*+-/>
68
+ * <int>`, and bare `getter()` / `props.X` / `var`.
69
+ *
70
+ * @returns the Go SSR value, or null when the body is none of these (the caller
71
+ * then falls through to comparison-ternary / block-body / object-memo handling)
72
+ */
73
+ export function memoInitialFromParsedBody(
74
+ ctx: GoEmitContext,
75
+ body: ParsedExpr,
76
+ signals: { getter: string; initialValue: string }[],
77
+ propsParams: { name: string; type?: TypeInfo; defaultValue?: string }[],
78
+ propFallbackVars: ReadonlyMap<string, PropFallbackVar>,
79
+ ): string | null {
80
+ const propRef = (propName: string): string => {
81
+ const hoisted = propFallbackVars.get(propName)
82
+ if (hoisted) return hoisted.varName
83
+ return `in.${capitalizeFieldName(propName)}`
84
+ }
85
+ // A bare zero-arg getter call (`count()`) → its name, else null.
86
+ const getterCallName = (e: ParsedExpr): string | null =>
87
+ e.kind === 'call' && e.callee.kind === 'identifier' && e.args.length === 0
88
+ ? e.callee.name
89
+ : null
90
+ // A `props.X` member access → the prop name, else null.
91
+ const propsMemberName = (e: ParsedExpr): string | null =>
92
+ e.kind === 'member' &&
93
+ !e.computed &&
94
+ e.object.kind === 'identifier' &&
95
+ e.object.name === 'props'
96
+ ? e.property
97
+ : null
98
+
99
+ // () => getter() === 'lit' / !== 'lit' — a selection memo. Resolves to a Go
100
+ // bool when the signal's initial value is itself a string literal.
101
+ if (
102
+ body.kind === 'binary' &&
103
+ ['===', '!==', '==', '!='].includes(body.op) &&
104
+ body.right.kind === 'literal' &&
105
+ body.right.literalType === 'string'
106
+ ) {
107
+ const depName = getterCallName(body.left)
108
+ if (depName) {
109
+ const lit = String(body.right.value)
110
+ const signal = signals.find(sg => sg.getter === depName)
111
+ const initLit = signal ? /^'([^'\\]*)'$/.exec(signal.initialValue.trim()) : null
112
+ if (initLit) {
113
+ const equal = initLit[1] === lit
114
+ return String(body.op.startsWith('!') ? !equal : equal)
115
+ }
116
+ }
117
+ }
118
+
119
+ // () => props.X ?? false — a boolean passthrough memo. Go's bool zero value
120
+ // IS the `?? false` fallback, so the raw input field carries it exactly.
121
+ if (
122
+ body.kind === 'logical' &&
123
+ body.op === '??' &&
124
+ body.right.kind === 'literal' &&
125
+ body.right.value === false
126
+ ) {
127
+ const propName = propsMemberName(body.left)
128
+ if (propName) return propRef(propName)
129
+ }
130
+
131
+ // () => cond() ? A : B where each branch is a module string const or a
132
+ // string literal, and `cond` is a signal/memo this resolver can evaluate.
133
+ if (body.kind === 'conditional') {
134
+ const condName = getterCallName(body.test)
135
+ if (condName) {
136
+ const resolveBranch = (b: ParsedExpr): string | null => {
137
+ if (b.kind === 'literal' && b.literalType === 'string') return JSON.stringify(b.value)
138
+ if (b.kind === 'identifier') {
139
+ const constVal = ctx.state.moduleStringConsts.get(b.name)
140
+ return constVal !== undefined ? JSON.stringify(constVal) : null
141
+ }
142
+ return null
143
+ }
144
+ const t = resolveBranch(body.consequent)
145
+ const f = resolveBranch(body.alternate)
146
+ if (t !== null && f !== null) {
147
+ let condGo: string | null = null
148
+ const condSignal = signals.find(sg => sg.getter === condName)
149
+ if (condSignal) {
150
+ condGo = getSignalInitialValueAsGo(ctx, condSignal.initialValue, propsParams, propFallbackVars)
151
+ } else {
152
+ const condMemo = (ctx.state.currentMemos ?? []).find(m => m.name === condName)
153
+ if (condMemo) {
154
+ condGo = computeMemoInitialValueOrNull(ctx, condMemo, signals, propsParams, propFallbackVars)
155
+ }
156
+ }
157
+ if (condGo === 'true') return t
158
+ if (condGo === 'false') return f
159
+ if (condGo !== null) {
160
+ return `func() string { if ${condGo} { return ${t} }; return ${f} }()`
161
+ }
162
+ }
163
+ }
164
+ }
165
+
166
+ // () => <ref> <*|+|-|/> <non-negative int>. The operand must be a
167
+ // non-negative integer literal; float/negative bodies fall through unchanged.
168
+ if (
169
+ body.kind === 'binary' &&
170
+ ['*', '+', '-', '/'].includes(body.op) &&
171
+ body.right.kind === 'literal' &&
172
+ body.right.literalType === 'number' &&
173
+ typeof body.right.value === 'number' &&
174
+ Number.isInteger(body.right.value) &&
175
+ body.right.value >= 0
176
+ ) {
177
+ const operator = body.op
178
+ const operand = String(body.right.value)
179
+
180
+ // getter() * N — return the signal's Go initial value times N.
181
+ const depName = getterCallName(body.left)
182
+ if (depName) {
183
+ const signal = signals.find(s => s.getter === depName)
184
+ if (signal) {
185
+ const signalInitial = getSignalInitialValueAsGo(ctx, signal.initialValue, propsParams, propFallbackVars)
186
+ return `${signalInitial} ${operator} ${operand}`
187
+ }
188
+ }
189
+
190
+ // props.X * N — hoisted var if any, else the input field (asserting int
191
+ // when the field lowers to interface{}).
192
+ const propName = propsMemberName(body.left)
193
+ if (propName) {
194
+ const param = propsParams.find(p => p.name === propName)
195
+ if (param) {
196
+ const hoisted = propFallbackVars.get(propName)
197
+ if (hoisted) return `${hoisted.varName} ${operator} ${operand}`
198
+ const fieldName = capitalizeFieldName(propName)
199
+ if (param.type) {
200
+ const goType = typeInfoToGo(ctx, param.type, param.defaultValue)
201
+ if (goType === 'interface{}') return `in.${fieldName}.(int) ${operator} ${operand}`
202
+ }
203
+ return `in.${fieldName} ${operator} ${operand}`
204
+ }
205
+ }
206
+
207
+ // var * N — destructured prop (no hoisted-var lookup).
208
+ if (body.left.kind === 'identifier') {
209
+ const varName = body.left.name
210
+ const param = propsParams.find(p => p.name === varName)
211
+ if (param) {
212
+ const fieldName = capitalizeFieldName(varName)
213
+ if (param.type) {
214
+ const goType = typeInfoToGo(ctx, param.type, param.defaultValue)
215
+ if (goType === 'interface{}') return `in.${fieldName}.(int) ${operator} ${operand}`
216
+ }
217
+ return `in.${fieldName} ${operator} ${operand}`
218
+ }
219
+ }
220
+ }
221
+
222
+ // () => getter() — just return the signal's Go initial value.
223
+ const simpleDep = getterCallName(body)
224
+ if (simpleDep) {
225
+ const signal = signals.find(s => s.getter === simpleDep)
226
+ if (signal) {
227
+ return getSignalInitialValueAsGo(ctx, signal.initialValue, propsParams, propFallbackVars)
228
+ }
229
+ }
230
+
231
+ // () => props.X — return the prop value (hoisted-aware).
232
+ const simpleProp = propsMemberName(body)
233
+ if (simpleProp) {
234
+ const param = propsParams.find(p => p.name === simpleProp)
235
+ if (param) return propRef(simpleProp)
236
+ }
237
+
238
+ // () => var — destructured prop, return the input field directly.
239
+ if (body.kind === 'identifier') {
240
+ const param = propsParams.find(p => p.name === body.name)
241
+ if (param) return `in.${capitalizeFieldName(body.name)}`
242
+ }
243
+
244
+ return null
245
+ }
246
+
247
+ /**
248
+ * Pattern-matching core of `computeMemoInitialValue`.
249
+ *
250
+ * @returns the memo's SSR initial value as a Go expression, or `null` when no
251
+ * pattern applies. Callers with a typed field to fill use the
252
+ * zero-value-defaulting wrapper above; callers that can OMIT the field (a
253
+ * child-instance prop init, where Go's zero values then apply with the right
254
+ * type) use this directly.
255
+ */
256
+ export function computeMemoInitialValueOrNull(
257
+ ctx: GoEmitContext,
258
+ memo: { name: string; computation: string; deps: string[]; parsed?: ParsedExpr; parsedBlock?: ParsedStatement[]; parsedBlockComplete?: boolean },
259
+ signals: { getter: string; initialValue: string }[],
260
+ propsParams: { name: string; type?: TypeInfo; defaultValue?: string }[],
261
+ propFallbackVars: ReadonlyMap<string, PropFallbackVar> = EMPTY_PROP_FALLBACK_VARS,
262
+ ): string | null {
263
+ const computation = memo.computation
264
+
265
+ // () => `...${expr}...` — template-literal memo (the classes memo). Builds a
266
+ // Go string concatenation; null when any interpolation isn't representable.
267
+ const tmplMemo = computeTemplateLiteralMemoInitialValue(ctx, memo, propsParams)
268
+ if (tmplMemo !== null) return tmplMemo
269
+
270
+ // Expression-bodied memo shapes, matched structurally on `parsed`.
271
+ if (memo.parsed) {
272
+ const fromParsed = memoInitialFromParsedBody(
273
+ ctx,
274
+ memo.parsed,
275
+ signals,
276
+ propsParams,
277
+ propFallbackVars,
278
+ )
279
+ if (fromParsed !== null) return fromParsed
280
+ }
281
+
282
+ // () => <operand> ===/!== 'lit' ? A : B, where each branch is a string
283
+ // literal/module-const and <operand> is a getter call, an inline
284
+ // nullish-defaulted prop (`props.X ?? 'horizontal'`), or a bare `props.X`.
285
+ const cmpTernary = computeComparisonTernaryGo(
286
+ ctx,
287
+ memo.parsed,
288
+ signals,
289
+ propsParams,
290
+ propFallbackVars,
291
+ )
292
+ if (cmpTernary !== null) return cmpTernary
293
+
294
+ // Block-body memo that early-returns a module-const array when a guard signal
295
+ // is falsy: `() => { const k = getter(); if (!k) return MODULE_ARRAY; … }`.
296
+ // When the signal starts null the SSR value is that array; its literal value
297
+ // (not the identifier) is passed to the baker so it reduces to a Go slice.
298
+ const blockReturn = resolveBlockBodyMemoModuleConst(ctx, memo, signals)
299
+ if (blockReturn !== null && blockReturn.constValue && blockReturn.constType) {
300
+ return convertInitialValue(ctx,
301
+ blockReturn.constValue,
302
+ blockReturn.constType,
303
+ propsParams,
304
+ blockReturn.constParsed,
305
+ )
306
+ }
307
+
308
+ // Object-returning block-body memo derived from `searchParams()`. Computes a
309
+ // Go `map[string]interface{}` whose values are lowered from the request
310
+ // query, so `.Params.Sort` etc. resolve at execute time instead of reading a
311
+ // nil map; null for any unsupported shape (→ nil fallback).
312
+ const objMemo = computeObjectMemoInitialValue(ctx, memo)
313
+ if (objMemo !== null) return objMemo
314
+
315
+ return null
316
+ }
317
+
318
+ /**
319
+ * Resolve a signal/memo getter NAME to a Go value expression, for use as the
320
+ * condition operand of another memo's ternary. Handles a plain signal, a
321
+ * prop-shadow memo `() => props.X ?? 'lit'` (→ a nil/empty-tolerant field
322
+ * read), else recurses.
323
+ *
324
+ * @returns the Go expression, or null when the shape isn't representable
325
+ */
326
+ export function resolveGetterValueAsGo(
327
+ ctx: GoEmitContext,
328
+ name: string,
329
+ signals: { getter: string; initialValue: string }[],
330
+ propsParams: { name: string; type?: TypeInfo; defaultValue?: string }[],
331
+ propFallbackVars: ReadonlyMap<string, PropFallbackVar>,
332
+ ): string | null {
333
+ const signal = signals.find(s => s.getter === name)
334
+ if (signal) {
335
+ return getSignalInitialValueAsGo(ctx, signal.initialValue, propsParams, propFallbackVars)
336
+ }
337
+ const memo = (ctx.state.currentMemos ?? []).find(m => m.name === name)
338
+ if (memo) {
339
+ const stripped = memo.computation.replace(/^\(\)\s*=>\s*/, '')
340
+ const fb = ctx.extractPropFallback(stripped)
341
+ if (fb && capitalizeFieldName(fb.propName) === capitalizeFieldName(memo.name)) {
342
+ const field = `in.${capitalizeFieldName(fb.propName)}`
343
+ return `func() interface{} { v := interface{}(${field}); if v == nil || v == "" { return ${fb.goFallback} }; return v }()`
344
+ }
345
+ return computeMemoInitialValueOrNull(ctx, memo, signals, propsParams, propFallbackVars)
346
+ }
347
+ const param = propsParams.find(p => p.name === name)
348
+ if (param) {
349
+ const hoisted = propFallbackVars.get(name)
350
+ return hoisted ? hoisted.varName : `in.${capitalizeFieldName(name)}`
351
+ }
352
+ return null
353
+ }
354
+
355
+ /**
356
+ * Resolve a string-ternary memo whose condition is a literal comparison —
357
+ * `() => <operand> === 'lit' ? A : B` (or `!==`) — to a Go runtime conditional.
358
+ * Branches must be string literals/module-string-consts; the operand resolves
359
+ * via `resolveComparisonOperandGo`.
360
+ *
361
+ * @returns the Go conditional, or null when the shape isn't supported
362
+ */
363
+ export function computeComparisonTernaryGo(
364
+ ctx: GoEmitContext,
365
+ parsed: ParsedExpr | undefined,
366
+ signals: { getter: string; initialValue: string }[],
367
+ propsParams: { name: string; type?: TypeInfo; defaultValue?: string }[],
368
+ propFallbackVars: ReadonlyMap<string, PropFallbackVar>,
369
+ ): string | null {
370
+ // Matches any `conditional` `parsed` — an expression-bodied ternary or, since
371
+ // #2040, a block-bodied memo whose value `if` / early-return folded to one.
372
+ if (!parsed || parsed.kind !== 'conditional') return null
373
+ const cond = parsed.test
374
+ if (cond.kind !== 'binary' || !(cond.right.kind === 'literal' && cond.right.literalType === 'string')) {
375
+ return null
376
+ }
377
+ const isEq = cond.op === '===' || cond.op === '=='
378
+ const isNe = cond.op === '!==' || cond.op === '!='
379
+ if (!isEq && !isNe) return null
380
+
381
+ const branch = (bn: ParsedExpr): string | null => {
382
+ if (bn.kind === 'literal' && bn.literalType === 'string') {
383
+ return JSON.stringify(bn.value)
384
+ }
385
+ if (bn.kind === 'identifier') {
386
+ const cv = ctx.state.moduleStringConsts.get(bn.name)
387
+ return cv !== undefined ? JSON.stringify(cv) : null
388
+ }
389
+ return null
390
+ }
391
+ const t = branch(parsed.consequent)
392
+ const f = branch(parsed.alternate)
393
+ if (t === null || f === null) return null
394
+
395
+ const condGo = resolveComparisonOperandGo(
396
+ ctx,
397
+ cond.left,
398
+ signals,
399
+ propsParams,
400
+ propFallbackVars,
401
+ )
402
+ if (condGo === null) return null
403
+
404
+ // `consequent` fires when the comparison holds; for `!==` the branches swap.
405
+ const eqBranch = isEq ? t : f
406
+ const neBranch = isEq ? f : t
407
+ return `func() string { if ${condGo} == ${JSON.stringify(cond.right.value)} { return ${eqBranch} }; return ${neBranch} }()`
408
+ }
409
+
410
+ /**
411
+ * Resolve the left operand of a string-ternary memo's comparison condition to a
412
+ * Go expression: a zero-arg getter call (a signal/prop-shadow memo), an inline
413
+ * `props.X ?? 'def'` (folds the default), or a bare `props.X`.
414
+ *
415
+ * @returns the Go expression, or null for anything else
416
+ */
417
+ export function resolveComparisonOperandGo(
418
+ ctx: GoEmitContext,
419
+ node: ParsedExpr,
420
+ signals: { getter: string; initialValue: string }[],
421
+ propsParams: { name: string; type?: TypeInfo; defaultValue?: string }[],
422
+ propFallbackVars: ReadonlyMap<string, PropFallbackVar>,
423
+ ): string | null {
424
+ // getter(): a signal or memo getter call.
425
+ if (node.kind === 'call' && node.callee.kind === 'identifier' && node.args.length === 0) {
426
+ return resolveGetterValueAsGo(ctx, node.callee.name, signals, propsParams, propFallbackVars)
427
+ }
428
+ // props.X ?? 'def' — nil/empty-tolerant field read with the default folded in.
429
+ if (node.kind === 'logical' && node.op === '??' && node.right.kind === 'literal' && node.right.literalType === 'string') {
430
+ const propName = propsAccessNameFromParsed(ctx, node.left)
431
+ if (propName) {
432
+ const field = `in.${capitalizeFieldName(propName)}`
433
+ return `func() interface{} { v := interface{}(${field}); if v == nil || v == "" { return ${JSON.stringify(node.right.value)} }; return v }()`
434
+ }
435
+ }
436
+ // Bare props.X.
437
+ const direct = propsAccessNameFromParsed(ctx, node)
438
+ if (direct) return `in.${capitalizeFieldName(direct)}`
439
+ return null
440
+ }
441
+
442
+ /**
443
+ * `ParsedExpr` counterpart of `propsAccessName`: if `node` is a
444
+ * `<propsObjectName>.<name>` member access, return `<name>`, else null.
445
+ */
446
+ function propsAccessNameFromParsed(ctx: GoEmitContext, node: ParsedExpr): string | null {
447
+ if (node.kind !== 'member' || node.computed) return null
448
+ if (node.object.kind !== 'identifier') return null
449
+ if (!ctx.state.propsObjectName || node.object.name !== ctx.state.propsObjectName) return null
450
+ return node.property
451
+ }
@@ -0,0 +1,74 @@
1
+ /**
2
+ * Memo type inference predicates.
3
+ *
4
+ * Pure free functions over a {@link GoEmitContext} that classify a memo's
5
+ * computation so `inferMemoType` can pick the right Go field type (and thus the
6
+ * right SSR zero value).
7
+ */
8
+
9
+ import type { ParsedExpr, TypeInfo } from '@barefootjs/jsx'
10
+
11
+ import type { GoEmitContext } from '../emit-context.ts'
12
+ import { typeInfoToGo } from '../type/type-codegen.ts'
13
+
14
+ /**
15
+ * Heuristic: does this memo evaluate to a boolean? True when its computation is
16
+ * a comparison (`!==`/`===`/`!=`/`==`), a negation (`!x`), or a ternary whose
17
+ * branches are all boolean signals/props. Used to pick `bool` (zero value
18
+ * `false`) over the int `0` default for the SSR initial value.
19
+ */
20
+ export function isBooleanMemo(
21
+ ctx: GoEmitContext,
22
+ memo: { computation: string; deps: string[]; parsed?: ParsedExpr },
23
+ signals: { getter: string; initialValue: string; type: TypeInfo }[],
24
+ propsParamMap: Map<string, { name: string; type: TypeInfo; defaultValue?: string }>,
25
+ ): boolean {
26
+ const c = memo.computation
27
+ // A ternary whose two branches are string literals is a STRING memo, not
28
+ // boolean — the `===` lives in the *condition*, so the blanket comparison
29
+ // check below would misclassify it as bool and bake `false`. Bail first.
30
+ if (isStringTernaryMemo(ctx, memo.parsed)) return false
31
+ if (/(!==|===|!=(?!=)|==(?!=))/.test(c)) return true
32
+ if (/=>\s*!/.test(c)) return true
33
+ // Ternary `() => cond() ? a() : b()` — boolean when both branches are
34
+ // boolean-resolving getters (signals whose value is boolean, or boolean
35
+ // props).
36
+ const isBoolGetter = (name: string): boolean => {
37
+ const sig = signals.find(s => s.getter === name)
38
+ if (sig) {
39
+ if (typeInfoToGo(ctx, sig.type) === 'bool') return true
40
+ // Signal initialised from `props.X ?? false` / a boolean prop.
41
+ if (/\?\?\s*(true|false)\b/.test(sig.initialValue)) return true
42
+ const propName = ctx.extractPropNameFromInitialValue(sig.initialValue) ?? sig.initialValue
43
+ const prop = propsParamMap.get(propName)
44
+ if (prop && typeInfoToGo(ctx, prop.type, prop.defaultValue) === 'bool') return true
45
+ return false
46
+ }
47
+ const prop = propsParamMap.get(name)
48
+ return !!prop && typeInfoToGo(ctx, prop.type, prop.defaultValue) === 'bool'
49
+ }
50
+ const ternary = c.match(/=>\s*\w+\(\)\s*\?\s*(\w+)\(\)\s*:\s*(\w+)\(\)/)
51
+ if (ternary) {
52
+ return isBoolGetter(ternary[1]) && isBoolGetter(ternary[2])
53
+ }
54
+ return false
55
+ }
56
+
57
+ /**
58
+ * Does this memo's body resolve to a ternary whose BOTH branches are
59
+ * string-valued — e.g. `() => orientation() === 'vertical' ? 'flex-col' :
60
+ * 'flex'`? Such a memo is a string, not a bool, even though its condition
61
+ * contains `===`. Since #2040 a block-bodied memo can also carry `parsed` (a
62
+ * value `if` / early-return folds to a ternary), so this now applies to those
63
+ * too — the inferred field type follows the folded shape, not the syntax.
64
+ */
65
+ export function isStringTernaryMemo(ctx: GoEmitContext, parsed: ParsedExpr | undefined): boolean {
66
+ if (!parsed || parsed.kind !== 'conditional') return false
67
+ // A branch is string-valued when it's a string literal or an identifier
68
+ // bound to a module-scope string const (carousel's `positionClasses` →
69
+ // `prevVerticalClasses` / `prevHorizontalClasses`).
70
+ const isStr = (n: ParsedExpr): boolean =>
71
+ (n.kind === 'literal' && n.literalType === 'string') ||
72
+ (n.kind === 'identifier' && ctx.state.moduleStringConsts.has(n.name))
73
+ return isStr(parsed.consequent) && isStr(parsed.alternate)
74
+ }