@barefootjs/go-template 0.16.0 → 0.17.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/adapter/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 +302 -1071
- package/dist/adapter/go-template-adapter.d.ts.map +1 -1
- package/dist/adapter/index.js +3116 -2692
- package/dist/adapter/lib/compile-state.d.ts +142 -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 +126 -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 +173 -0
- package/dist/adapter/memo/memo-compute.d.ts.map +1 -0
- package/dist/adapter/memo/memo-type.d.ts +48 -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 +3114 -2690
- package/dist/index.js +3116 -2692
- 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 +398 -54
- 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 +2189 -5320
- package/src/adapter/lib/compile-state.ts +181 -0
- package/src/adapter/lib/constants.ts +24 -0
- package/src/adapter/lib/go-emit.ts +309 -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 +661 -0
- package/src/adapter/memo/memo-type.ts +93 -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 +48 -22
|
@@ -0,0 +1,661 @@
|
|
|
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
|
+
import {
|
|
13
|
+
asCallbackMethodCall,
|
|
14
|
+
freeVarsInBody,
|
|
15
|
+
materializeGetterCalls,
|
|
16
|
+
serializeParsedExpr,
|
|
17
|
+
} from '@barefootjs/jsx'
|
|
18
|
+
|
|
19
|
+
import type { GoEmitContext } from '../emit-context.ts'
|
|
20
|
+
import type { PropFallbackVar } from '../lib/types.ts'
|
|
21
|
+
import { capitalizeFieldName } from '../lib/go-naming.ts'
|
|
22
|
+
import { escapeGoString } from '../lib/go-emit.ts'
|
|
23
|
+
import { convertInitialValue, getSignalInitialValueAsGo } from '../value/value-lowering.ts'
|
|
24
|
+
import { typeInfoToGo } from '../type/type-codegen.ts'
|
|
25
|
+
import { computeTemplateLiteralMemoInitialValue } from './template-interp.ts'
|
|
26
|
+
import { resolveBlockBodyMemoModuleConst, computeObjectMemoInitialValue } from './memo-value.ts'
|
|
27
|
+
|
|
28
|
+
/** Default for the optional `propFallbackVars` argument. */
|
|
29
|
+
const EMPTY_PROP_FALLBACK_VARS: ReadonlyMap<string, PropFallbackVar> = new Map()
|
|
30
|
+
|
|
31
|
+
/** A bare zero-arg getter call (`count()`) → its name, else null. */
|
|
32
|
+
function getterCallName(e: ParsedExpr): string | null {
|
|
33
|
+
return e.kind === 'call' && e.callee.kind === 'identifier' && e.args.length === 0
|
|
34
|
+
? e.callee.name
|
|
35
|
+
: null
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** A `props.X` member access → the prop name, else null. */
|
|
39
|
+
function propsMemberName(e: ParsedExpr): string | null {
|
|
40
|
+
return e.kind === 'member' &&
|
|
41
|
+
!e.computed &&
|
|
42
|
+
e.object.kind === 'identifier' &&
|
|
43
|
+
e.object.name === 'props'
|
|
44
|
+
? e.property
|
|
45
|
+
: null
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** A `() => props.X.filter((p) => <predicate>)` match: the array prop name,
|
|
49
|
+
* the predicate serialized to the runtime evaluator's ParsedExpr JSON, the
|
|
50
|
+
* arrow's param name, and the free variable names its predicate captures.
|
|
51
|
+
* Null when `body` isn't this shape, `propName` doesn't resolve to a known
|
|
52
|
+
* prop, or the predicate isn't representable (`serializeParsedExpr` refusal).
|
|
53
|
+
* Shared by the SSR-value emitter ({@link memoInitialFromParsedBody}) and the
|
|
54
|
+
* constructor's sibling-memo hoisting pre-pass so both walk the shape
|
|
55
|
+
* identically. */
|
|
56
|
+
export function matchFilterArmMemo(
|
|
57
|
+
ctx: GoEmitContext,
|
|
58
|
+
body: ParsedExpr,
|
|
59
|
+
signals: { getter: string; initialValue: string }[],
|
|
60
|
+
propsParams: { name: string; type?: TypeInfo; defaultValue?: string }[],
|
|
61
|
+
): { propName: string; predJSON: string; paramName: string; freeVars: string[] } | null {
|
|
62
|
+
const cb = asCallbackMethodCall(body)
|
|
63
|
+
if (!cb || cb.method !== 'filter') return null
|
|
64
|
+
const propName = propsMemberName(cb.object)
|
|
65
|
+
const param = propName ? propsParams.find(p => p.name === propName) : undefined
|
|
66
|
+
if (!propName || !param) return null
|
|
67
|
+
|
|
68
|
+
// Single authority (Package G plan): every step that isn't `env-reader` is
|
|
69
|
+
// value-materializable at SSR time — env readers are per-request readers
|
|
70
|
+
// the runtime evaluator can't invoke, so they're excluded here exactly as
|
|
71
|
+
// the plan's own consumers exclude them.
|
|
72
|
+
const knownGetterNames = new Set<string>(
|
|
73
|
+
ctx.state.ssrSeedPlan.steps.filter(s => s.kind !== 'env-reader').map(s => s.name),
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
const materialized = materializeGetterCalls(cb.arrow.body, knownGetterNames)
|
|
77
|
+
const predJSON = serializeParsedExpr(materialized)
|
|
78
|
+
if (predJSON === null) return null
|
|
79
|
+
|
|
80
|
+
const paramName = cb.arrow.params[0] ?? '_'
|
|
81
|
+
const freeVars = freeVarsInBody(materialized, new Set(cb.arrow.params))
|
|
82
|
+
return { propName, predJSON, paramName, freeVars }
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Names of EARLIER sibling memos (per `ctx.state.currentMemos` declaration
|
|
87
|
+
* order) that a filter-arm memo's predicate free variables resolve to — the
|
|
88
|
+
* constructor generator hoists these into a shared local so the sibling's
|
|
89
|
+
* expression isn't emitted twice (once for its own field, once inlined in
|
|
90
|
+
* this memo's `bf.FilterEval` env map, #2075/#2077 review finding 3). Empty
|
|
91
|
+
* when `memo` isn't a filter-arm memo or references nothing hoistable.
|
|
92
|
+
*/
|
|
93
|
+
export function filterArmEarlierSiblingRefs(
|
|
94
|
+
ctx: GoEmitContext,
|
|
95
|
+
memo: { name: string; parsed?: ParsedExpr },
|
|
96
|
+
signals: { getter: string; initialValue: string }[],
|
|
97
|
+
propsParams: { name: string; type?: TypeInfo; defaultValue?: string }[],
|
|
98
|
+
): string[] {
|
|
99
|
+
if (!memo.parsed) return []
|
|
100
|
+
const match = matchFilterArmMemo(ctx, memo.parsed, signals, propsParams)
|
|
101
|
+
if (!match) return []
|
|
102
|
+
// `currentMemos` order equals the plan's memo-step order (plan lists
|
|
103
|
+
// signals first, then memos, both in declaration order) — kept here rather
|
|
104
|
+
// than filtering `ssrSeedPlan.steps` since this index is memo-only already.
|
|
105
|
+
const memos = ctx.state.currentMemos ?? []
|
|
106
|
+
const currentIndex = memos.findIndex(m => m.name === memo.name)
|
|
107
|
+
if (currentIndex < 0) return []
|
|
108
|
+
const refs: string[] = []
|
|
109
|
+
for (const name of match.freeVars) {
|
|
110
|
+
const idx = memos.findIndex(m => m.name === name)
|
|
111
|
+
if (idx >= 0 && idx < currentIndex) refs.push(name)
|
|
112
|
+
}
|
|
113
|
+
return refs
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Compute a memo's SSR initial value as a Go expression — e.g.
|
|
118
|
+
* `() => count() * 2` → `in.Initial * 2`, `() => props.value * 10` →
|
|
119
|
+
* `in.Value * 10`. Unresolved computations default to the memo's Go zero value.
|
|
120
|
+
*
|
|
121
|
+
* @param propFallbackVars when a hoisted fallback var exists for a referenced
|
|
122
|
+
* prop, it is substituted for `in.FieldName` so the memo inherits the
|
|
123
|
+
* signal-time `??` fallback
|
|
124
|
+
* @param goType Go type of the memo field, used to pick the zero-value fallback
|
|
125
|
+
*/
|
|
126
|
+
export function computeMemoInitialValue(
|
|
127
|
+
ctx: GoEmitContext,
|
|
128
|
+
memo: { name: string; computation: string; deps: string[]; parsed?: ParsedExpr },
|
|
129
|
+
signals: { getter: string; initialValue: string }[],
|
|
130
|
+
propsParams: { name: string; type?: TypeInfo; defaultValue?: string }[],
|
|
131
|
+
propFallbackVars: ReadonlyMap<string, PropFallbackVar> = EMPTY_PROP_FALLBACK_VARS,
|
|
132
|
+
goType?: string,
|
|
133
|
+
): string {
|
|
134
|
+
// Seed the resolution stack with this memo's own name — every external call
|
|
135
|
+
// is a fresh top-level computation, so self-reference must be caught on the
|
|
136
|
+
// very first recursion, not only on the second visit.
|
|
137
|
+
const resolved = computeMemoInitialValueOrNull(
|
|
138
|
+
ctx, memo, signals, propsParams, propFallbackVars, new Set([memo.name]),
|
|
139
|
+
)
|
|
140
|
+
if (resolved !== null) return resolved
|
|
141
|
+
// Zero value for the memo's Go type: `false` for bool, `""` for string,
|
|
142
|
+
// `nil` for reference types (map / slice / interface / pointer — `0` is not
|
|
143
|
+
// assignable to them), else the int `0`.
|
|
144
|
+
if (goType === 'bool') return 'false'
|
|
145
|
+
if (goType === 'string') return '""'
|
|
146
|
+
if (
|
|
147
|
+
goType !== undefined &&
|
|
148
|
+
(goType.startsWith('map[') ||
|
|
149
|
+
goType.startsWith('[]') ||
|
|
150
|
+
goType.startsWith('*') ||
|
|
151
|
+
goType.includes('interface{}') ||
|
|
152
|
+
goType === 'any')
|
|
153
|
+
) {
|
|
154
|
+
return 'nil'
|
|
155
|
+
}
|
|
156
|
+
return '0'
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Structural matcher for the common expression-bodied memo shapes, driven by
|
|
161
|
+
* the analyzer-attached `MemoInfo.parsed` (the memo arrow's body): `getter() ===
|
|
162
|
+
* 'lit'`, `props.X ?? false`, `cond() ? A : B`, `<getter()|props.X|var> <*+-/>
|
|
163
|
+
* <int>`, and bare `getter()` / `props.X` / `var`.
|
|
164
|
+
*
|
|
165
|
+
* @returns the Go SSR value, or null when the body is none of these (the caller
|
|
166
|
+
* then falls through to comparison-ternary / block-body / object-memo handling)
|
|
167
|
+
*/
|
|
168
|
+
export function memoInitialFromParsedBody(
|
|
169
|
+
ctx: GoEmitContext,
|
|
170
|
+
body: ParsedExpr,
|
|
171
|
+
signals: { getter: string; initialValue: string }[],
|
|
172
|
+
propsParams: { name: string; type?: TypeInfo; defaultValue?: string }[],
|
|
173
|
+
propFallbackVars: ReadonlyMap<string, PropFallbackVar>,
|
|
174
|
+
currentMemoName: string,
|
|
175
|
+
resolving: ReadonlySet<string> = new Set(),
|
|
176
|
+
): string | null {
|
|
177
|
+
const propRef = (propName: string): string => {
|
|
178
|
+
const hoisted = propFallbackVars.get(propName)
|
|
179
|
+
if (hoisted) return hoisted.varName
|
|
180
|
+
return `in.${capitalizeFieldName(propName)}`
|
|
181
|
+
}
|
|
182
|
+
// A request-scoped env-signal read (`searchParams().get('k')`, any local
|
|
183
|
+
// alias, #1922) → the literal key and the reader's canonical field name,
|
|
184
|
+
// else null. Constraints: the receiver must be a zero-arg call of a local
|
|
185
|
+
// bound to a registered env signal (`ctx.state.envSignalReadersByLocal`,
|
|
186
|
+
// keyed by that reader's own registration — not a hardcoded key); the
|
|
187
|
+
// method must be one that reader's `methods` set recognises; the key
|
|
188
|
+
// argument must be a string literal.
|
|
189
|
+
const envGetKey = (e: ParsedExpr): { key: string; fieldName: string } | null => {
|
|
190
|
+
if (e.kind !== 'call' || e.callee.kind !== 'member' || e.callee.computed) return null
|
|
191
|
+
const recvName = getterCallName(e.callee.object)
|
|
192
|
+
if (recvName === null) return null
|
|
193
|
+
const reader = ctx.state.envSignalReadersByLocal.get(recvName)
|
|
194
|
+
if (!reader || !reader.methods.has(e.callee.property)) return null
|
|
195
|
+
const arg = e.args[0]
|
|
196
|
+
if (!arg || arg.kind !== 'literal' || arg.literalType !== 'string') return null
|
|
197
|
+
return { key: String(arg.value), fieldName: capitalizeFieldName(reader.canonicalName) }
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// () => searchParams().get('k') — a derived memo over the env signal
|
|
201
|
+
// (#2069). The constructor reads the canonical `in.SearchParams` reader the
|
|
202
|
+
// handler fills per request, so the memo SSR-computes instead of zero-valuing.
|
|
203
|
+
{
|
|
204
|
+
const m = envGetKey(body)
|
|
205
|
+
if (m !== null) return `in.${m.fieldName}.Get(${JSON.stringify(m.key)})`
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// () => searchParams().get('k') ?? '<lit>' — the defaulted form. Go's
|
|
209
|
+
// `SearchParams.Get` returns "" for an absent key (it can't surface JS's
|
|
210
|
+
// null), so the default fires on "" — the same documented `?? → or`
|
|
211
|
+
// divergence as the template-position lowering (`or (.SearchParams.Get
|
|
212
|
+
// "k") "<lit>"`); the two positions stay consistent.
|
|
213
|
+
if (
|
|
214
|
+
body.kind === 'logical' &&
|
|
215
|
+
(body.op === '??' || body.op === '||') &&
|
|
216
|
+
body.right.kind === 'literal' &&
|
|
217
|
+
body.right.literalType === 'string'
|
|
218
|
+
) {
|
|
219
|
+
const m = envGetKey(body.left)
|
|
220
|
+
if (m !== null) {
|
|
221
|
+
const def = JSON.stringify(String(body.right.value))
|
|
222
|
+
return `func() string { if v := in.${m.fieldName}.Get(${JSON.stringify(m.key)}); v != "" { return v }; return ${def} }()`
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// () => props.X.filter((p) => <predicate>) — a LIST-valued derived memo
|
|
227
|
+
// chained off a sibling memo the predicate closes over (e.g. a
|
|
228
|
+
// searchParams()-derived `tag`). Env-signal getters are excluded from the
|
|
229
|
+
// free-var materialization set: they're per-request READERS, not values,
|
|
230
|
+
// and the runtime evaluator has no way to invoke one.
|
|
231
|
+
{
|
|
232
|
+
const match = matchFilterArmMemo(ctx, body, signals, propsParams)
|
|
233
|
+
if (match) {
|
|
234
|
+
const { propName, predJSON, paramName, freeVars } = match
|
|
235
|
+
const currentIndex = (ctx.state.currentMemos ?? []).findIndex(m => m.name === currentMemoName)
|
|
236
|
+
const envEntries: string[] = []
|
|
237
|
+
let unresolved = false
|
|
238
|
+
for (const name of freeVars) {
|
|
239
|
+
let goExpr: string | null = null
|
|
240
|
+
const siblingIndex = (ctx.state.currentMemos ?? []).findIndex(m => m.name === name)
|
|
241
|
+
if (siblingIndex >= 0) {
|
|
242
|
+
// Only a sibling declared EARLIER than the memo being computed is
|
|
243
|
+
// eligible — mirrors JS declaration order and rules out both
|
|
244
|
+
// self-reference and any mutual pair (at most one direction can
|
|
245
|
+
// satisfy `earlier`), independent of the resolving-stack guard
|
|
246
|
+
// below. A hoisted local (the constructor emitted it once already,
|
|
247
|
+
// #2075/#2077 review finding 3) is reused verbatim instead of
|
|
248
|
+
// recomputing the expression a second time.
|
|
249
|
+
const siblingMemo = ctx.state.currentMemos![siblingIndex]
|
|
250
|
+
const eligible = currentIndex >= 0 && siblingIndex < currentIndex && !resolving.has(siblingMemo.name)
|
|
251
|
+
if (eligible) {
|
|
252
|
+
const hoisted = ctx.state.hoistedMemoLocals.get(siblingMemo.name)
|
|
253
|
+
goExpr = hoisted ?? computeMemoInitialValueOrNull(
|
|
254
|
+
ctx, siblingMemo, signals, propsParams, propFallbackVars,
|
|
255
|
+
new Set([...resolving, siblingMemo.name]),
|
|
256
|
+
)
|
|
257
|
+
}
|
|
258
|
+
} else {
|
|
259
|
+
const sig = signals.find(s => s.getter === name)
|
|
260
|
+
if (sig) {
|
|
261
|
+
goExpr = getSignalInitialValueAsGo(ctx, sig.initialValue, propsParams, propFallbackVars)
|
|
262
|
+
} else {
|
|
263
|
+
const p = propsParams.find(pp => pp.name === name)
|
|
264
|
+
if (p) goExpr = propRef(name)
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
if (goExpr === null) {
|
|
268
|
+
unresolved = true
|
|
269
|
+
break
|
|
270
|
+
}
|
|
271
|
+
envEntries.push(`${JSON.stringify(name)}: ${goExpr}`)
|
|
272
|
+
}
|
|
273
|
+
if (!unresolved) {
|
|
274
|
+
const itemsField = `in.${capitalizeFieldName(propName)}`
|
|
275
|
+
const envMap = `map[string]any{${envEntries.join(', ')}}`
|
|
276
|
+
return `bf.FilterEval(${itemsField}, "${escapeGoString(predJSON)}", ${JSON.stringify(paramName)}, ${envMap})`
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// () => getter() === 'lit' / !== 'lit' — a selection memo. Resolves to a Go
|
|
282
|
+
// bool when the signal's initial value is itself a string literal.
|
|
283
|
+
if (
|
|
284
|
+
body.kind === 'binary' &&
|
|
285
|
+
['===', '!==', '==', '!='].includes(body.op) &&
|
|
286
|
+
body.right.kind === 'literal' &&
|
|
287
|
+
body.right.literalType === 'string'
|
|
288
|
+
) {
|
|
289
|
+
const depName = getterCallName(body.left)
|
|
290
|
+
if (depName) {
|
|
291
|
+
const lit = String(body.right.value)
|
|
292
|
+
const signal = signals.find(sg => sg.getter === depName)
|
|
293
|
+
const initLit = signal ? /^'([^'\\]*)'$/.exec(signal.initialValue.trim()) : null
|
|
294
|
+
if (initLit) {
|
|
295
|
+
const equal = initLit[1] === lit
|
|
296
|
+
return String(body.op.startsWith('!') ? !equal : equal)
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// () => props.X ?? false — a boolean passthrough memo. Go's bool zero value
|
|
302
|
+
// IS the `?? false` fallback, so the raw input field carries it exactly.
|
|
303
|
+
if (
|
|
304
|
+
body.kind === 'logical' &&
|
|
305
|
+
body.op === '??' &&
|
|
306
|
+
body.right.kind === 'literal' &&
|
|
307
|
+
body.right.value === false
|
|
308
|
+
) {
|
|
309
|
+
const propName = propsMemberName(body.left)
|
|
310
|
+
if (propName) return propRef(propName)
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// () => cond() ? A : B where each branch is a module string const or a
|
|
314
|
+
// string literal, and `cond` is a signal/memo this resolver can evaluate.
|
|
315
|
+
if (body.kind === 'conditional') {
|
|
316
|
+
const condName = getterCallName(body.test)
|
|
317
|
+
if (condName) {
|
|
318
|
+
const resolveBranch = (b: ParsedExpr): string | null => {
|
|
319
|
+
if (b.kind === 'literal' && b.literalType === 'string') return JSON.stringify(b.value)
|
|
320
|
+
if (b.kind === 'identifier') {
|
|
321
|
+
const constVal = ctx.state.moduleStringConsts.get(b.name)
|
|
322
|
+
return constVal !== undefined ? JSON.stringify(constVal) : null
|
|
323
|
+
}
|
|
324
|
+
return null
|
|
325
|
+
}
|
|
326
|
+
const t = resolveBranch(body.consequent)
|
|
327
|
+
const f = resolveBranch(body.alternate)
|
|
328
|
+
if (t !== null && f !== null) {
|
|
329
|
+
let condGo: string | null = null
|
|
330
|
+
const condSignal = signals.find(sg => sg.getter === condName)
|
|
331
|
+
if (condSignal) {
|
|
332
|
+
condGo = getSignalInitialValueAsGo(ctx, condSignal.initialValue, propsParams, propFallbackVars)
|
|
333
|
+
} else {
|
|
334
|
+
const condMemo = (ctx.state.currentMemos ?? []).find(m => m.name === condName)
|
|
335
|
+
// A condition memo already on the resolution stack (self- or
|
|
336
|
+
// mutually-referencing ternaries) falls back to null — the same
|
|
337
|
+
// zero-value default as any other unresolved shape — instead of
|
|
338
|
+
// recursing forever.
|
|
339
|
+
if (condMemo && !resolving.has(condMemo.name)) {
|
|
340
|
+
condGo = computeMemoInitialValueOrNull(
|
|
341
|
+
ctx, condMemo, signals, propsParams, propFallbackVars,
|
|
342
|
+
new Set([...resolving, condMemo.name]),
|
|
343
|
+
)
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
if (condGo === 'true') return t
|
|
347
|
+
if (condGo === 'false') return f
|
|
348
|
+
if (condGo !== null) {
|
|
349
|
+
return `func() string { if ${condGo} { return ${t} }; return ${f} }()`
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// () => <ref> <*|+|-|/> <non-negative int>. The operand must be a
|
|
356
|
+
// non-negative integer literal; float/negative bodies fall through unchanged.
|
|
357
|
+
if (
|
|
358
|
+
body.kind === 'binary' &&
|
|
359
|
+
['*', '+', '-', '/'].includes(body.op) &&
|
|
360
|
+
body.right.kind === 'literal' &&
|
|
361
|
+
body.right.literalType === 'number' &&
|
|
362
|
+
typeof body.right.value === 'number' &&
|
|
363
|
+
Number.isInteger(body.right.value) &&
|
|
364
|
+
body.right.value >= 0
|
|
365
|
+
) {
|
|
366
|
+
const operator = body.op
|
|
367
|
+
const operand = String(body.right.value)
|
|
368
|
+
|
|
369
|
+
// getter() * N — return the signal's Go initial value times N.
|
|
370
|
+
const depName = getterCallName(body.left)
|
|
371
|
+
if (depName) {
|
|
372
|
+
const signal = signals.find(s => s.getter === depName)
|
|
373
|
+
if (signal) {
|
|
374
|
+
const signalInitial = getSignalInitialValueAsGo(ctx, signal.initialValue, propsParams, propFallbackVars)
|
|
375
|
+
return `${signalInitial} ${operator} ${operand}`
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
// props.X * N — hoisted var if any, else the input field (asserting int
|
|
380
|
+
// when the field lowers to interface{}).
|
|
381
|
+
const propName = propsMemberName(body.left)
|
|
382
|
+
if (propName) {
|
|
383
|
+
const param = propsParams.find(p => p.name === propName)
|
|
384
|
+
if (param) {
|
|
385
|
+
const hoisted = propFallbackVars.get(propName)
|
|
386
|
+
if (hoisted) return `${hoisted.varName} ${operator} ${operand}`
|
|
387
|
+
const fieldName = capitalizeFieldName(propName)
|
|
388
|
+
if (param.type) {
|
|
389
|
+
const goType = typeInfoToGo(ctx, param.type, param.defaultValue)
|
|
390
|
+
if (goType === 'interface{}') return `in.${fieldName}.(int) ${operator} ${operand}`
|
|
391
|
+
}
|
|
392
|
+
return `in.${fieldName} ${operator} ${operand}`
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
// var * N — destructured prop (no hoisted-var lookup).
|
|
397
|
+
if (body.left.kind === 'identifier') {
|
|
398
|
+
const varName = body.left.name
|
|
399
|
+
const param = propsParams.find(p => p.name === varName)
|
|
400
|
+
if (param) {
|
|
401
|
+
const fieldName = capitalizeFieldName(varName)
|
|
402
|
+
if (param.type) {
|
|
403
|
+
const goType = typeInfoToGo(ctx, param.type, param.defaultValue)
|
|
404
|
+
if (goType === 'interface{}') return `in.${fieldName}.(int) ${operator} ${operand}`
|
|
405
|
+
}
|
|
406
|
+
return `in.${fieldName} ${operator} ${operand}`
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
// () => getter() — just return the signal's Go initial value.
|
|
412
|
+
const simpleDep = getterCallName(body)
|
|
413
|
+
if (simpleDep) {
|
|
414
|
+
const signal = signals.find(s => s.getter === simpleDep)
|
|
415
|
+
if (signal) {
|
|
416
|
+
return getSignalInitialValueAsGo(ctx, signal.initialValue, propsParams, propFallbackVars)
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
// () => props.X — return the prop value (hoisted-aware).
|
|
421
|
+
const simpleProp = propsMemberName(body)
|
|
422
|
+
if (simpleProp) {
|
|
423
|
+
const param = propsParams.find(p => p.name === simpleProp)
|
|
424
|
+
if (param) return propRef(simpleProp)
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
// () => var — destructured prop, return the input field directly.
|
|
428
|
+
if (body.kind === 'identifier') {
|
|
429
|
+
const param = propsParams.find(p => p.name === body.name)
|
|
430
|
+
if (param) return `in.${capitalizeFieldName(body.name)}`
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
return null
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
/**
|
|
437
|
+
* Pattern-matching core of `computeMemoInitialValue`.
|
|
438
|
+
*
|
|
439
|
+
* @returns the memo's SSR initial value as a Go expression, or `null` when no
|
|
440
|
+
* pattern applies. Callers with a typed field to fill use the
|
|
441
|
+
* zero-value-defaulting wrapper above; callers that can OMIT the field (a
|
|
442
|
+
* child-instance prop init, where Go's zero values then apply with the right
|
|
443
|
+
* type) use this directly.
|
|
444
|
+
*/
|
|
445
|
+
export function computeMemoInitialValueOrNull(
|
|
446
|
+
ctx: GoEmitContext,
|
|
447
|
+
memo: { name: string; computation: string; deps: string[]; parsed?: ParsedExpr; parsedBlock?: ParsedStatement[]; parsedBlockComplete?: boolean },
|
|
448
|
+
signals: { getter: string; initialValue: string }[],
|
|
449
|
+
propsParams: { name: string; type?: TypeInfo; defaultValue?: string }[],
|
|
450
|
+
propFallbackVars: ReadonlyMap<string, PropFallbackVar> = EMPTY_PROP_FALLBACK_VARS,
|
|
451
|
+
/**
|
|
452
|
+
* Memo names currently being resolved on this call stack — guards against
|
|
453
|
+
* unbounded recursion when memos reference each other (mutually, or a
|
|
454
|
+
* self-reference). Callers that start a fresh top-level computation seed
|
|
455
|
+
* this with `memo.name`; recursive calls extend it with the sibling memo
|
|
456
|
+
* about to be entered. A candidate already in the set resolves to `null`
|
|
457
|
+
* (the shape's usual "unsupported" fallback) instead of recursing.
|
|
458
|
+
*/
|
|
459
|
+
resolving: ReadonlySet<string> = new Set(),
|
|
460
|
+
): string | null {
|
|
461
|
+
const computation = memo.computation
|
|
462
|
+
|
|
463
|
+
// () => `...${expr}...` — template-literal memo (the classes memo). Builds a
|
|
464
|
+
// Go string concatenation; null when any interpolation isn't representable.
|
|
465
|
+
const tmplMemo = computeTemplateLiteralMemoInitialValue(ctx, memo, propsParams)
|
|
466
|
+
if (tmplMemo !== null) return tmplMemo
|
|
467
|
+
|
|
468
|
+
// Expression-bodied memo shapes, matched structurally on `parsed`.
|
|
469
|
+
if (memo.parsed) {
|
|
470
|
+
const fromParsed = memoInitialFromParsedBody(
|
|
471
|
+
ctx,
|
|
472
|
+
memo.parsed,
|
|
473
|
+
signals,
|
|
474
|
+
propsParams,
|
|
475
|
+
propFallbackVars,
|
|
476
|
+
memo.name,
|
|
477
|
+
resolving,
|
|
478
|
+
)
|
|
479
|
+
if (fromParsed !== null) return fromParsed
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
// () => <operand> ===/!== 'lit' ? A : B, where each branch is a string
|
|
483
|
+
// literal/module-const and <operand> is a getter call, an inline
|
|
484
|
+
// nullish-defaulted prop (`props.X ?? 'horizontal'`), or a bare `props.X`.
|
|
485
|
+
const cmpTernary = computeComparisonTernaryGo(
|
|
486
|
+
ctx,
|
|
487
|
+
memo.parsed,
|
|
488
|
+
signals,
|
|
489
|
+
propsParams,
|
|
490
|
+
propFallbackVars,
|
|
491
|
+
resolving,
|
|
492
|
+
)
|
|
493
|
+
if (cmpTernary !== null) return cmpTernary
|
|
494
|
+
|
|
495
|
+
// Block-body memo that early-returns a module-const array when a guard signal
|
|
496
|
+
// is falsy: `() => { const k = getter(); if (!k) return MODULE_ARRAY; … }`.
|
|
497
|
+
// When the signal starts null the SSR value is that array; its literal value
|
|
498
|
+
// (not the identifier) is passed to the baker so it reduces to a Go slice.
|
|
499
|
+
const blockReturn = resolveBlockBodyMemoModuleConst(ctx, memo, signals)
|
|
500
|
+
if (blockReturn !== null && blockReturn.constValue && blockReturn.constType) {
|
|
501
|
+
return convertInitialValue(ctx,
|
|
502
|
+
blockReturn.constValue,
|
|
503
|
+
blockReturn.constType,
|
|
504
|
+
propsParams,
|
|
505
|
+
blockReturn.constParsed,
|
|
506
|
+
)
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
// Object-returning block-body memo derived from `searchParams()`. Computes a
|
|
510
|
+
// Go `map[string]interface{}` whose values are lowered from the request
|
|
511
|
+
// query, so `.Params.Sort` etc. resolve at execute time instead of reading a
|
|
512
|
+
// nil map; null for any unsupported shape (→ nil fallback).
|
|
513
|
+
const objMemo = computeObjectMemoInitialValue(ctx, memo)
|
|
514
|
+
if (objMemo !== null) return objMemo
|
|
515
|
+
|
|
516
|
+
return null
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
/**
|
|
520
|
+
* Resolve a signal/memo getter NAME to a Go value expression, for use as the
|
|
521
|
+
* condition operand of another memo's ternary. Handles a plain signal, a
|
|
522
|
+
* prop-shadow memo `() => props.X ?? 'lit'` (→ a nil/empty-tolerant field
|
|
523
|
+
* read), else recurses.
|
|
524
|
+
*
|
|
525
|
+
* @returns the Go expression, or null when the shape isn't representable
|
|
526
|
+
*/
|
|
527
|
+
export function resolveGetterValueAsGo(
|
|
528
|
+
ctx: GoEmitContext,
|
|
529
|
+
name: string,
|
|
530
|
+
signals: { getter: string; initialValue: string }[],
|
|
531
|
+
propsParams: { name: string; type?: TypeInfo; defaultValue?: string }[],
|
|
532
|
+
propFallbackVars: ReadonlyMap<string, PropFallbackVar>,
|
|
533
|
+
resolving: ReadonlySet<string> = new Set(),
|
|
534
|
+
): string | null {
|
|
535
|
+
const signal = signals.find(s => s.getter === name)
|
|
536
|
+
if (signal) {
|
|
537
|
+
return getSignalInitialValueAsGo(ctx, signal.initialValue, propsParams, propFallbackVars)
|
|
538
|
+
}
|
|
539
|
+
const memo = (ctx.state.currentMemos ?? []).find(m => m.name === name)
|
|
540
|
+
if (memo) {
|
|
541
|
+
// Already on the resolution stack (self- or mutually-referencing
|
|
542
|
+
// ternary operands) — fall back to null rather than recurse forever.
|
|
543
|
+
if (resolving.has(memo.name)) return null
|
|
544
|
+
const stripped = memo.computation.replace(/^\(\)\s*=>\s*/, '')
|
|
545
|
+
const fb = ctx.extractPropFallback(stripped)
|
|
546
|
+
if (fb && capitalizeFieldName(fb.propName) === capitalizeFieldName(memo.name)) {
|
|
547
|
+
const field = `in.${capitalizeFieldName(fb.propName)}`
|
|
548
|
+
return `func() interface{} { v := interface{}(${field}); if v == nil || v == "" { return ${fb.goFallback} }; return v }()`
|
|
549
|
+
}
|
|
550
|
+
return computeMemoInitialValueOrNull(
|
|
551
|
+
ctx, memo, signals, propsParams, propFallbackVars, new Set([...resolving, memo.name]),
|
|
552
|
+
)
|
|
553
|
+
}
|
|
554
|
+
const param = propsParams.find(p => p.name === name)
|
|
555
|
+
if (param) {
|
|
556
|
+
const hoisted = propFallbackVars.get(name)
|
|
557
|
+
return hoisted ? hoisted.varName : `in.${capitalizeFieldName(name)}`
|
|
558
|
+
}
|
|
559
|
+
return null
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
/**
|
|
563
|
+
* Resolve a string-ternary memo whose condition is a literal comparison —
|
|
564
|
+
* `() => <operand> === 'lit' ? A : B` (or `!==`) — to a Go runtime conditional.
|
|
565
|
+
* Branches must be string literals/module-string-consts; the operand resolves
|
|
566
|
+
* via `resolveComparisonOperandGo`.
|
|
567
|
+
*
|
|
568
|
+
* @returns the Go conditional, or null when the shape isn't supported
|
|
569
|
+
*/
|
|
570
|
+
export function computeComparisonTernaryGo(
|
|
571
|
+
ctx: GoEmitContext,
|
|
572
|
+
parsed: ParsedExpr | undefined,
|
|
573
|
+
signals: { getter: string; initialValue: string }[],
|
|
574
|
+
propsParams: { name: string; type?: TypeInfo; defaultValue?: string }[],
|
|
575
|
+
propFallbackVars: ReadonlyMap<string, PropFallbackVar>,
|
|
576
|
+
resolving: ReadonlySet<string> = new Set(),
|
|
577
|
+
): string | null {
|
|
578
|
+
// Matches any `conditional` `parsed` — an expression-bodied ternary or, since
|
|
579
|
+
// #2040, a block-bodied memo whose value `if` / early-return folded to one.
|
|
580
|
+
if (!parsed || parsed.kind !== 'conditional') return null
|
|
581
|
+
const cond = parsed.test
|
|
582
|
+
if (cond.kind !== 'binary' || !(cond.right.kind === 'literal' && cond.right.literalType === 'string')) {
|
|
583
|
+
return null
|
|
584
|
+
}
|
|
585
|
+
const isEq = cond.op === '===' || cond.op === '=='
|
|
586
|
+
const isNe = cond.op === '!==' || cond.op === '!='
|
|
587
|
+
if (!isEq && !isNe) return null
|
|
588
|
+
|
|
589
|
+
const branch = (bn: ParsedExpr): string | null => {
|
|
590
|
+
if (bn.kind === 'literal' && bn.literalType === 'string') {
|
|
591
|
+
return JSON.stringify(bn.value)
|
|
592
|
+
}
|
|
593
|
+
if (bn.kind === 'identifier') {
|
|
594
|
+
const cv = ctx.state.moduleStringConsts.get(bn.name)
|
|
595
|
+
return cv !== undefined ? JSON.stringify(cv) : null
|
|
596
|
+
}
|
|
597
|
+
return null
|
|
598
|
+
}
|
|
599
|
+
const t = branch(parsed.consequent)
|
|
600
|
+
const f = branch(parsed.alternate)
|
|
601
|
+
if (t === null || f === null) return null
|
|
602
|
+
|
|
603
|
+
const condGo = resolveComparisonOperandGo(
|
|
604
|
+
ctx,
|
|
605
|
+
cond.left,
|
|
606
|
+
signals,
|
|
607
|
+
propsParams,
|
|
608
|
+
propFallbackVars,
|
|
609
|
+
resolving,
|
|
610
|
+
)
|
|
611
|
+
if (condGo === null) return null
|
|
612
|
+
|
|
613
|
+
// `consequent` fires when the comparison holds; for `!==` the branches swap.
|
|
614
|
+
const eqBranch = isEq ? t : f
|
|
615
|
+
const neBranch = isEq ? f : t
|
|
616
|
+
return `func() string { if ${condGo} == ${JSON.stringify(cond.right.value)} { return ${eqBranch} }; return ${neBranch} }()`
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
/**
|
|
620
|
+
* Resolve the left operand of a string-ternary memo's comparison condition to a
|
|
621
|
+
* Go expression: a zero-arg getter call (a signal/prop-shadow memo), an inline
|
|
622
|
+
* `props.X ?? 'def'` (folds the default), or a bare `props.X`.
|
|
623
|
+
*
|
|
624
|
+
* @returns the Go expression, or null for anything else
|
|
625
|
+
*/
|
|
626
|
+
export function resolveComparisonOperandGo(
|
|
627
|
+
ctx: GoEmitContext,
|
|
628
|
+
node: ParsedExpr,
|
|
629
|
+
signals: { getter: string; initialValue: string }[],
|
|
630
|
+
propsParams: { name: string; type?: TypeInfo; defaultValue?: string }[],
|
|
631
|
+
propFallbackVars: ReadonlyMap<string, PropFallbackVar>,
|
|
632
|
+
resolving: ReadonlySet<string> = new Set(),
|
|
633
|
+
): string | null {
|
|
634
|
+
// getter(): a signal or memo getter call.
|
|
635
|
+
if (node.kind === 'call' && node.callee.kind === 'identifier' && node.args.length === 0) {
|
|
636
|
+
return resolveGetterValueAsGo(ctx, node.callee.name, signals, propsParams, propFallbackVars, resolving)
|
|
637
|
+
}
|
|
638
|
+
// props.X ?? 'def' — nil/empty-tolerant field read with the default folded in.
|
|
639
|
+
if (node.kind === 'logical' && node.op === '??' && node.right.kind === 'literal' && node.right.literalType === 'string') {
|
|
640
|
+
const propName = propsAccessNameFromParsed(ctx, node.left)
|
|
641
|
+
if (propName) {
|
|
642
|
+
const field = `in.${capitalizeFieldName(propName)}`
|
|
643
|
+
return `func() interface{} { v := interface{}(${field}); if v == nil || v == "" { return ${JSON.stringify(node.right.value)} }; return v }()`
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
// Bare props.X.
|
|
647
|
+
const direct = propsAccessNameFromParsed(ctx, node)
|
|
648
|
+
if (direct) return `in.${capitalizeFieldName(direct)}`
|
|
649
|
+
return null
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
/**
|
|
653
|
+
* `ParsedExpr` counterpart of `propsAccessName`: if `node` is a
|
|
654
|
+
* `<propsObjectName>.<name>` member access, return `<name>`, else null.
|
|
655
|
+
*/
|
|
656
|
+
function propsAccessNameFromParsed(ctx: GoEmitContext, node: ParsedExpr): string | null {
|
|
657
|
+
if (node.kind !== 'member' || node.computed) return null
|
|
658
|
+
if (node.object.kind !== 'identifier') return null
|
|
659
|
+
if (!ctx.state.propsObjectName || node.object.name !== ctx.state.propsObjectName) return null
|
|
660
|
+
return node.property
|
|
661
|
+
}
|