@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,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
|
+
}
|