@barefootjs/go-template 0.16.0 → 0.17.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 +2857 -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 +2856 -2617
- package/dist/index.js +2857 -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 +118 -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 +2099 -5325
- 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,197 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Memo value computation for block-body / object-returning memos.
|
|
3
|
+
*
|
|
4
|
+
* Free functions over a {@link GoEmitContext}:
|
|
5
|
+
* - `resolveBlockBodyMemoModuleConst` recognises a guard-and-return-module-
|
|
6
|
+
* const memo and reports the constant, reading `state.localConstants`.
|
|
7
|
+
* - `computeObjectMemoInitialValue` lowers a `searchParams()`-derived
|
|
8
|
+
* object-returning memo to a Go `map[string]interface{}` literal, reading
|
|
9
|
+
* the analyzer-carried `parsedBlock` and `state.searchParamsLocals` and
|
|
10
|
+
* delegating value lowering to `lowerCtorExpr`.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { foldBlockToExpr } from '@barefootjs/jsx'
|
|
14
|
+
import type { ParsedExpr, ParsedStatement, TypeInfo } from '@barefootjs/jsx'
|
|
15
|
+
|
|
16
|
+
import type { GoEmitContext } from '../emit-context.ts'
|
|
17
|
+
import type { CtorLowerEnv } from '../lib/types.ts'
|
|
18
|
+
import { capitalizeFieldName } from '../lib/go-naming.ts'
|
|
19
|
+
import { lowerCtorExpr } from './ctor-lowering.ts'
|
|
20
|
+
|
|
21
|
+
type GuardConstResult = {
|
|
22
|
+
constName: string
|
|
23
|
+
constValue: string | undefined
|
|
24
|
+
constType: TypeInfo | undefined
|
|
25
|
+
constParsed: ParsedExpr | undefined
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const FALSY_INITS = new Set(['null', "''", '""', '0', 'false'])
|
|
29
|
+
|
|
30
|
+
/** Look the returned const name up as a module-scope constant and package it. */
|
|
31
|
+
function packageModuleConst(ctx: GoEmitContext, name: string): GuardConstResult | null {
|
|
32
|
+
const constant = ctx.state.localConstants.find(
|
|
33
|
+
c => c.name === name && c.origin?.scope === 'module',
|
|
34
|
+
)
|
|
35
|
+
if (!constant) return null
|
|
36
|
+
return {
|
|
37
|
+
constName: constant.name,
|
|
38
|
+
constValue: constant.value,
|
|
39
|
+
constType: constant.type ?? undefined,
|
|
40
|
+
constParsed: constant.parsed,
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Recognise a block-body memo whose SSR path returns a module-const array when
|
|
46
|
+
* the guard signal starts falsy:
|
|
47
|
+
* `() => { const k = getter(); if (!k) return MODULE_CONST; … }`
|
|
48
|
+
*
|
|
49
|
+
* Primary path: the block is normalized to a single expression upstream (#2040,
|
|
50
|
+
* `foldBlockToExpr` in the analyzer), so this reads the folded `MemoInfo.parsed`
|
|
51
|
+
* conditional `!getter() ? MODULE_CONST : <derived>`. When the guard signal's
|
|
52
|
+
* initial value is falsy, `!guard` is `true`, so the `consequent` is the const
|
|
53
|
+
* rendered at SSR.
|
|
54
|
+
*
|
|
55
|
+
* Fallback path: a block the fold REFUSES (e.g. an impure local binding that
|
|
56
|
+
* isn't used exactly once per path sits alongside the guard) leaves
|
|
57
|
+
* `MemoInfo.parsed` unset, but the guard prefix is still present in the tolerant
|
|
58
|
+
* `MemoInfo.parsedBlock`. Scan that prefix so the SSR const bake matches `main`
|
|
59
|
+
* exactly — the bake depends only on the guard, not on the unfoldable tail.
|
|
60
|
+
*
|
|
61
|
+
* @returns the constant's name, value, inferred type and parsed tree, or null
|
|
62
|
+
*/
|
|
63
|
+
export function resolveBlockBodyMemoModuleConst(
|
|
64
|
+
ctx: GoEmitContext,
|
|
65
|
+
memo: { parsed?: ParsedExpr; parsedBlock?: ParsedStatement[] },
|
|
66
|
+
signals: { getter: string; initialValue: string }[],
|
|
67
|
+
): GuardConstResult | null {
|
|
68
|
+
return (
|
|
69
|
+
fromFoldedConditional(ctx, memo.parsed, signals) ??
|
|
70
|
+
fromStatementPrefix(ctx, memo.parsedBlock, signals)
|
|
71
|
+
)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Primary: read the folded `!getter() ? MODULE_CONST : <derived>` conditional. */
|
|
75
|
+
function fromFoldedConditional(
|
|
76
|
+
ctx: GoEmitContext,
|
|
77
|
+
parsed: ParsedExpr | undefined,
|
|
78
|
+
signals: { getter: string; initialValue: string }[],
|
|
79
|
+
): GuardConstResult | null {
|
|
80
|
+
if (!parsed || parsed.kind !== 'conditional') return null
|
|
81
|
+
const test = parsed.test
|
|
82
|
+
if (test.kind !== 'unary' || test.op !== '!') return null
|
|
83
|
+
const guardCall = test.argument
|
|
84
|
+
if (
|
|
85
|
+
guardCall.kind !== 'call' ||
|
|
86
|
+
guardCall.callee.kind !== 'identifier' ||
|
|
87
|
+
guardCall.args.length !== 0
|
|
88
|
+
) {
|
|
89
|
+
return null
|
|
90
|
+
}
|
|
91
|
+
const guardSignal = signals.find(sg => sg.getter === (guardCall.callee as { name: string }).name)
|
|
92
|
+
if (!guardSignal || !FALSY_INITS.has(guardSignal.initialValue.trim())) return null
|
|
93
|
+
// `!falsy` is true → the consequent is the returned module const.
|
|
94
|
+
if (parsed.consequent.kind !== 'identifier') return null
|
|
95
|
+
return packageModuleConst(ctx, parsed.consequent.name)
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Fallback for blocks the fold refused: scan the tolerant statements for the
|
|
100
|
+
* guard prefix `const k = getter(); if (!k) return MODULE_CONST` (later
|
|
101
|
+
* statements are ignored — they don't affect the guard-falsy SSR value).
|
|
102
|
+
*/
|
|
103
|
+
function fromStatementPrefix(
|
|
104
|
+
ctx: GoEmitContext,
|
|
105
|
+
parsedBlock: ParsedStatement[] | undefined,
|
|
106
|
+
signals: { getter: string; initialValue: string }[],
|
|
107
|
+
): GuardConstResult | null {
|
|
108
|
+
if (!parsedBlock) return null
|
|
109
|
+
const varToSignal = new Map<string, string>()
|
|
110
|
+
let guardSignalGetter: string | null = null
|
|
111
|
+
let returnedConst: string | null = null
|
|
112
|
+
|
|
113
|
+
for (const s of parsedBlock) {
|
|
114
|
+
if (s.kind === 'var-decl' && s.init.kind === 'call' && s.init.callee.kind === 'identifier') {
|
|
115
|
+
const callee = s.init.callee.name
|
|
116
|
+
if (signals.some(sg => sg.getter === callee)) varToSignal.set(s.name, callee)
|
|
117
|
+
}
|
|
118
|
+
if (
|
|
119
|
+
s.kind === 'if' &&
|
|
120
|
+
s.condition.kind === 'unary' &&
|
|
121
|
+
s.condition.op === '!' &&
|
|
122
|
+
s.condition.argument.kind === 'identifier'
|
|
123
|
+
) {
|
|
124
|
+
const signalGetter = varToSignal.get(s.condition.argument.name)
|
|
125
|
+
if (!signalGetter) continue
|
|
126
|
+
for (const rs of s.consequent) {
|
|
127
|
+
if (rs.kind === 'return' && rs.value.kind === 'identifier') {
|
|
128
|
+
guardSignalGetter = signalGetter
|
|
129
|
+
returnedConst = rs.value.name
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
if (guardSignalGetter && returnedConst) break
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (!guardSignalGetter || !returnedConst) return null
|
|
137
|
+
const guardSignal = signals.find(sg => sg.getter === guardSignalGetter)
|
|
138
|
+
if (!guardSignal || !FALSY_INITS.has(guardSignal.initialValue.trim())) return null
|
|
139
|
+
return packageModuleConst(ctx, returnedConst)
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Compute the SSR value of an object-returning block-body memo derived from
|
|
144
|
+
* `searchParams()`:
|
|
145
|
+
* () => { const sp = searchParams(); return { sort: asSortKey(sp.get('sort')),
|
|
146
|
+
* tag: sp.get('tag') ?? '' } }
|
|
147
|
+
* Emits a Go `map[string]interface{}{ "Sort": …, "Tag": … }` whose values are
|
|
148
|
+
* lowered from the request query (see `lowerCtorExpr`). Keys are capitalized to
|
|
149
|
+
* match the template's `.Params.<Field>` map access.
|
|
150
|
+
*
|
|
151
|
+
* @returns the Go map literal, or null for any shape the lowerer can't
|
|
152
|
+
* represent (→ nil-map fallback)
|
|
153
|
+
*/
|
|
154
|
+
export function computeObjectMemoInitialValue(
|
|
155
|
+
ctx: GoEmitContext,
|
|
156
|
+
memo: { parsed?: ParsedExpr; parsedBlock?: ParsedStatement[]; parsedBlockComplete?: boolean },
|
|
157
|
+
): string | null {
|
|
158
|
+
// Normalize the block to a single object-literal expression (#2040). The
|
|
159
|
+
// analyzer's fold treats only signal/memo reads as pure, so a
|
|
160
|
+
// `const sp = searchParams()` (read twice) isn't folded there; redo the fold
|
|
161
|
+
// here with `searchParams` added to the purity oracle — it is an idempotent
|
|
162
|
+
// request-query read, so inlining `sp` at each `sp.get('k')` site is sound.
|
|
163
|
+
// The folded object's values become `searchParams().get('k')` (sp inlined),
|
|
164
|
+
// lowered by `lowerCtorExpr`. A block that doesn't fold to an object literal
|
|
165
|
+
// (extra control flow, an impure non-searchParams binding) yields null → the
|
|
166
|
+
// caller's nil fallback, exactly as the previous statement walk did.
|
|
167
|
+
let retObj = memo.parsed?.kind === 'object-literal' ? memo.parsed : null
|
|
168
|
+
if (!retObj) {
|
|
169
|
+
if (!memo.parsedBlock || !memo.parsedBlockComplete) return null
|
|
170
|
+
const folded = foldBlockToExpr(memo.parsedBlock, {
|
|
171
|
+
pureCallNames: ctx.state.searchParamsLocals,
|
|
172
|
+
})
|
|
173
|
+
if (!folded.ok || folded.expr.kind !== 'object-literal') return null
|
|
174
|
+
retObj = folded.expr
|
|
175
|
+
}
|
|
176
|
+
if (retObj.properties.length === 0) return null
|
|
177
|
+
|
|
178
|
+
// `sp` is inlined to `searchParams()` in the folded values, so no
|
|
179
|
+
// local→searchParams var env is needed; `lowerCtorExpr` recognises the
|
|
180
|
+
// `searchParams().get('k')` receiver shape directly.
|
|
181
|
+
const env: CtorLowerEnv = { searchParamsVars: new Set(), params: new Map() }
|
|
182
|
+
const entries: string[] = []
|
|
183
|
+
for (const prop of retObj.properties) {
|
|
184
|
+
// Bail on a shorthand property (`return { tag }`): its value is a bare
|
|
185
|
+
// identifier whose name need not match a `.Params.<Field>` accessor.
|
|
186
|
+
if (prop.shorthand) return null
|
|
187
|
+
// The key must be identifier- or string-named (a numeric key has no
|
|
188
|
+
// matching `.Params.<Field>` accessor).
|
|
189
|
+
if (prop.keyKind !== undefined && prop.keyKind !== 'identifier' && prop.keyKind !== 'string') {
|
|
190
|
+
return null
|
|
191
|
+
}
|
|
192
|
+
const go = lowerCtorExpr(ctx, prop.value, env)
|
|
193
|
+
if (go === null) return null
|
|
194
|
+
entries.push(`"${capitalizeFieldName(prop.key)}": ${go}`)
|
|
195
|
+
}
|
|
196
|
+
return `map[string]interface{}{\n\t\t${entries.join(',\n\t\t')},\n\t}`
|
|
197
|
+
}
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Template-literal memo lowering.
|
|
3
|
+
*
|
|
4
|
+
* Free functions over a {@link GoEmitContext} that compute the SSR initial
|
|
5
|
+
* value of a template-literal memo (`() => `${a} ${props.x ?? ''} grid``) as a
|
|
6
|
+
* Go `string` expression. Each quasi becomes a Go string literal; each
|
|
7
|
+
* interpolation resolves to a module string const, a `Record`-index access, or
|
|
8
|
+
* a `props.<name>` field read. Sets `state.usesFmt` when a `Record`-index
|
|
9
|
+
* interpolation emits `fmt.Sprint`.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import ts from 'typescript'
|
|
13
|
+
|
|
14
|
+
import type { ParsedExpr, ParsedStatement, TypeInfo } from '@barefootjs/jsx'
|
|
15
|
+
|
|
16
|
+
import type { GoEmitContext } from '../emit-context.ts'
|
|
17
|
+
import { escapeGoString } from '../lib/go-emit.ts'
|
|
18
|
+
import { capitalizeFieldName } from '../lib/go-naming.ts'
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Compute the SSR initial value of a template-literal memo as a Go `string`
|
|
22
|
+
* expression, e.g. `() => `${a} ${props.className ?? ''} grid``.
|
|
23
|
+
*
|
|
24
|
+
* Each quasi becomes a Go string literal; each interpolation is resolved:
|
|
25
|
+
* - an identifier naming a module string const → its inlined literal (covers
|
|
26
|
+
* pure-string and `[...].join(' ')` consts);
|
|
27
|
+
* - `props.<name> ?? '<fallback>'` or bare `props.<name>` → `in.<Field>` when
|
|
28
|
+
* `<name>` is a known string-typed prop param (the `?? ''` fallback maps to
|
|
29
|
+
* Go's zero value for an unset string field).
|
|
30
|
+
*
|
|
31
|
+
* @returns the `"a" + in.Field + " grid..."` concatenation, or null when the
|
|
32
|
+
* computation isn't a single template literal or any interpolation isn't
|
|
33
|
+
* representable
|
|
34
|
+
*/
|
|
35
|
+
export function computeTemplateLiteralMemoInitialValue(
|
|
36
|
+
ctx: GoEmitContext,
|
|
37
|
+
memo: { parsed?: ParsedExpr; parsedBlock?: ParsedStatement[]; parsedBlockComplete?: boolean },
|
|
38
|
+
propsParams: { name: string; type?: TypeInfo; defaultValue?: string }[],
|
|
39
|
+
): string | null {
|
|
40
|
+
// Resolve the template value (a `template-literal`, or a plain string
|
|
41
|
+
// `literal` for a no-substitution `` `plain` `` which folds to one) plus any
|
|
42
|
+
// memo-local `const X = props.Y ?? 'lit'` key bindings.
|
|
43
|
+
const localKeyBindings = new Map<string, { propName: string; defaultLiteral?: string }>()
|
|
44
|
+
let templateValue: ParsedExpr | undefined
|
|
45
|
+
if (memo.parsedBlock) {
|
|
46
|
+
// Block-bodied arrow (the Toggle `classes` memo): collect leading key
|
|
47
|
+
// bindings, then resolve against the single returned template literal. Any
|
|
48
|
+
// statement that isn't a var-decl or the return (an `if`, a loop) is a
|
|
49
|
+
// shape we don't model — bail.
|
|
50
|
+
//
|
|
51
|
+
// `parsedBlock` is tolerant: it OMITS statements it can't represent
|
|
52
|
+
// (`for`/`while`/`switch`), so an incomplete block could look like just
|
|
53
|
+
// `var-decl`s + `return`. Bail when it isn't complete.
|
|
54
|
+
if (!memo.parsedBlockComplete) return null
|
|
55
|
+
for (const s of memo.parsedBlock) {
|
|
56
|
+
if (s.kind === 'var-decl') {
|
|
57
|
+
const binding = parseLocalKeyBinding(ctx, s.init)
|
|
58
|
+
if (binding) localKeyBindings.set(s.name, binding)
|
|
59
|
+
} else if (s.kind === 'return') {
|
|
60
|
+
templateValue = s.value
|
|
61
|
+
} else {
|
|
62
|
+
return null
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
if (!templateValue) return null
|
|
66
|
+
} else if (memo.parsed) {
|
|
67
|
+
templateValue = memo.parsed
|
|
68
|
+
} else {
|
|
69
|
+
return null
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const propNames = new Set(propsParams.map(p => p.name))
|
|
73
|
+
const escGo = (s: string) => `"${escapeGoString(s)}"`
|
|
74
|
+
|
|
75
|
+
// A no-substitution template literal folds to a plain string literal.
|
|
76
|
+
if (templateValue.kind === 'literal' && templateValue.literalType === 'string') {
|
|
77
|
+
return escGo(String(templateValue.value))
|
|
78
|
+
}
|
|
79
|
+
if (templateValue.kind !== 'template-literal') return null
|
|
80
|
+
|
|
81
|
+
const segments: string[] = []
|
|
82
|
+
for (const part of templateValue.parts) {
|
|
83
|
+
if (part.type === 'string') {
|
|
84
|
+
if (part.value) segments.push(escGo(part.value))
|
|
85
|
+
} else {
|
|
86
|
+
const resolved = resolveTemplateInterpolation(ctx, part.expr, propNames, localKeyBindings)
|
|
87
|
+
if (resolved === null) return null
|
|
88
|
+
segments.push(resolved)
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
if (segments.length === 0) return '""'
|
|
92
|
+
return segments.join(' + ')
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Resolve one `${expr}` interpolation of a template-literal memo to a Go string
|
|
97
|
+
* expression, or null when unsupported. See
|
|
98
|
+
* `computeTemplateLiteralMemoInitialValue` for the supported shapes.
|
|
99
|
+
*/
|
|
100
|
+
function resolveTemplateInterpolation(
|
|
101
|
+
ctx: GoEmitContext,
|
|
102
|
+
node: ParsedExpr,
|
|
103
|
+
propNames: Set<string>,
|
|
104
|
+
localKeyBindings: ReadonlyMap<string, { propName: string; defaultLiteral?: string }>,
|
|
105
|
+
): string | null {
|
|
106
|
+
// Identifier → module string const inline.
|
|
107
|
+
if (node.kind === 'identifier') {
|
|
108
|
+
return ctx.resolveModuleStringConst(node.name)
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// `recordConst[key]` → inline indexed Go map (the Toggle `classes` memo's
|
|
112
|
+
// `variantClasses[variant]` / `sizeClasses[size]`). A bare-identifier index
|
|
113
|
+
// parses as `index-access`.
|
|
114
|
+
if (node.kind === 'index-access') {
|
|
115
|
+
return recordIndexInterpolationToGo(ctx, node, propNames, localKeyBindings)
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// `props.X ?? ''` — string-typed prop with an empty-string fallback.
|
|
119
|
+
if (node.kind === 'logical' && node.op === '??') {
|
|
120
|
+
const right = node.right
|
|
121
|
+
const isEmptyStr = right.kind === 'literal' && right.literalType === 'string' && right.value === ''
|
|
122
|
+
const propName = propsAccessNameFromParsed(ctx, node.left)
|
|
123
|
+
if (propName && propNames.has(propName) && isEmptyStr) {
|
|
124
|
+
// Unset string field is "" in Go — same as `?? ''`.
|
|
125
|
+
return `in.${capitalizeFieldName(propName)}`
|
|
126
|
+
}
|
|
127
|
+
return null
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Bare `props.X` (string-typed prop).
|
|
131
|
+
const propName = propsAccessNameFromParsed(ctx, node)
|
|
132
|
+
if (propName && propNames.has(propName)) {
|
|
133
|
+
return `in.${capitalizeFieldName(propName)}`
|
|
134
|
+
}
|
|
135
|
+
return null
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Parse a memo-local `const X = …` initializer (`ParsedExpr`) into a
|
|
140
|
+
* `Record`-index key binding: `props.Y ?? 'lit'` → `{ propName: 'Y',
|
|
141
|
+
* defaultLiteral: 'lit' }`, or bare `props.Y` → `{ propName: 'Y' }`. Returns
|
|
142
|
+
* null for any other shape, so it isn't registered as a key.
|
|
143
|
+
*/
|
|
144
|
+
function parseLocalKeyBinding(
|
|
145
|
+
ctx: GoEmitContext,
|
|
146
|
+
node: ParsedExpr,
|
|
147
|
+
): { propName: string; defaultLiteral?: string } | null {
|
|
148
|
+
if (node.kind === 'logical' && node.op === '??') {
|
|
149
|
+
const propName = propsAccessNameFromParsed(ctx, node.left)
|
|
150
|
+
const right = node.right
|
|
151
|
+
if (propName && right.kind === 'literal' && right.literalType === 'string') {
|
|
152
|
+
return { propName, defaultLiteral: String(right.value) }
|
|
153
|
+
}
|
|
154
|
+
return null
|
|
155
|
+
}
|
|
156
|
+
const propName = propsAccessNameFromParsed(ctx, node)
|
|
157
|
+
if (propName) return { propName }
|
|
158
|
+
return null
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Lower a `recordConst[key]` interpolation to an inline indexed Go map:
|
|
163
|
+
* `map[string]string{…}[fmt.Sprint(in.Field)]` (or `map[string]any` for mixed
|
|
164
|
+
* values). `recordConst` is a module-scope `Record<keys, scalar>` object
|
|
165
|
+
* literal; `key` is a bare prop or a memo-local const bound to `props.X ??
|
|
166
|
+
* 'default'`, whose `'default'` fallback also maps `""` so an unset prop
|
|
167
|
+
* renders the default.
|
|
168
|
+
*
|
|
169
|
+
* @returns the indexed-map expression, or null for any non-record /
|
|
170
|
+
* non-resolvable key
|
|
171
|
+
*/
|
|
172
|
+
function recordIndexInterpolationToGo(
|
|
173
|
+
ctx: GoEmitContext,
|
|
174
|
+
node: ParsedExpr & { kind: 'index-access' },
|
|
175
|
+
propNames: Set<string>,
|
|
176
|
+
localKeyBindings: ReadonlyMap<string, { propName: string; defaultLiteral?: string }>,
|
|
177
|
+
): string | null {
|
|
178
|
+
if (node.object.kind !== 'identifier' || node.index.kind !== 'identifier') return null
|
|
179
|
+
const objName = node.object.name
|
|
180
|
+
const keyName = node.index.name
|
|
181
|
+
|
|
182
|
+
// KEY resolution: a memo-local binding wins (carries the `'default'` key),
|
|
183
|
+
// else the key must be a bare prop.
|
|
184
|
+
let indexPropName: string
|
|
185
|
+
let defaultKey: string | undefined
|
|
186
|
+
const resolved = localKeyBindings.get(keyName)
|
|
187
|
+
if (resolved) {
|
|
188
|
+
indexPropName = resolved.propName
|
|
189
|
+
defaultKey = resolved.defaultLiteral
|
|
190
|
+
} else if (propNames.has(keyName)) {
|
|
191
|
+
indexPropName = keyName
|
|
192
|
+
} else {
|
|
193
|
+
return null
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// IDENT must be a module-scope object-literal const carried as `parsed`.
|
|
197
|
+
const constInfo = (ctx.state.localConstants ?? []).find(c => c.name === objName && c.isModule)
|
|
198
|
+
const parsedConst = constInfo?.parsed
|
|
199
|
+
if (!parsedConst || parsedConst.kind !== 'object-literal') return null
|
|
200
|
+
|
|
201
|
+
const entries: { key: string; value: { kind: 'number' | 'string'; text: string } }[] = []
|
|
202
|
+
for (const prop of parsedConst.properties) {
|
|
203
|
+
const v = prop.value
|
|
204
|
+
if (v.kind === 'literal' && v.literalType === 'number') {
|
|
205
|
+
entries.push({ key: prop.key, value: { kind: 'number', text: v.raw ?? String(v.value) } })
|
|
206
|
+
} else if (v.kind === 'literal' && v.literalType === 'string') {
|
|
207
|
+
entries.push({ key: prop.key, value: { kind: 'string', text: String(v.value) } })
|
|
208
|
+
} else {
|
|
209
|
+
return null
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const goVal = (v: { kind: 'number' | 'string'; text: string }) =>
|
|
214
|
+
v.kind === 'number' ? v.text : JSON.stringify(v.text)
|
|
215
|
+
const ents = entries.map(e => `${JSON.stringify(e.key)}: ${goVal(e.value)}`)
|
|
216
|
+
if (defaultKey !== undefined) {
|
|
217
|
+
const def = entries.find(e => e.key === defaultKey)
|
|
218
|
+
if (def) ents.unshift(`"": ${goVal(def.value)}`)
|
|
219
|
+
}
|
|
220
|
+
const allString = entries.every(e => e.value.kind === 'string')
|
|
221
|
+
const mapType = allString ? 'map[string]string' : 'map[string]any'
|
|
222
|
+
ctx.state.usesFmt = true
|
|
223
|
+
return `${mapType}{${ents.join(', ')}}[fmt.Sprint(in.${capitalizeFieldName(indexPropName)})]`
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* `ParsedExpr` counterpart of `propsAccessName`: if `node` is a
|
|
228
|
+
* `<propsObjectName>.<name>` member access, return `<name>`, else null.
|
|
229
|
+
*/
|
|
230
|
+
function propsAccessNameFromParsed(ctx: GoEmitContext, node: ParsedExpr): string | null {
|
|
231
|
+
if (node.kind !== 'member' || node.computed) return null
|
|
232
|
+
if (node.object.kind !== 'identifier') return null
|
|
233
|
+
if (!ctx.state.propsObjectName || node.object.name !== ctx.state.propsObjectName) return null
|
|
234
|
+
return node.property
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* If `node` is a `<propsObjectName>.<name>` access, return `<name>`, else
|
|
239
|
+
* null. Used to recognize props-object reads inside memo interpolations.
|
|
240
|
+
*/
|
|
241
|
+
export function propsAccessName(ctx: GoEmitContext, node: ts.Expression): string | null {
|
|
242
|
+
if (!ts.isPropertyAccessExpression(node)) return null
|
|
243
|
+
if (!ts.isIdentifier(node.expression)) return null
|
|
244
|
+
if (!ctx.state.propsObjectName || node.expression.text !== ctx.state.propsObjectName) return null
|
|
245
|
+
return node.name.text
|
|
246
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Prop-type resolution: decide each prop's Go struct-field type.
|
|
3
|
+
*
|
|
4
|
+
* Free functions over a {@link GoEmitContext}, shared by the Input/Props struct
|
|
5
|
+
* generators and the nillable-field set so they can't drift.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { ComponentIR, IRMetadata } from '@barefootjs/jsx'
|
|
9
|
+
|
|
10
|
+
import type { GoEmitContext } from '../emit-context.ts'
|
|
11
|
+
import { typeInfoToGo } from '../type/type-codegen.ts'
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Build a map from prop name to a better Go type inferred from signals. When a
|
|
15
|
+
* signal is initialized from a prop (`createSignal(props.initial ?? 0)`), the
|
|
16
|
+
* signal's type annotation may be more specific than the prop's `TypeInfo`. Only
|
|
17
|
+
* generic prop types (containing `interface{}`) are overridden.
|
|
18
|
+
*/
|
|
19
|
+
export function buildPropTypeOverrides(ctx: GoEmitContext, ir: ComponentIR): Map<string, string> {
|
|
20
|
+
const overrides = new Map<string, string>()
|
|
21
|
+
for (const signal of ir.metadata.signals) {
|
|
22
|
+
const propNames = [signal.initialValue]
|
|
23
|
+
const extracted = ctx.extractPropNameFromInitialValue(signal.initialValue)
|
|
24
|
+
if (extracted) propNames.push(extracted)
|
|
25
|
+
|
|
26
|
+
for (const propName of propNames) {
|
|
27
|
+
const param = ir.metadata.propsParams.find(p => p.name === propName)
|
|
28
|
+
if (!param) continue
|
|
29
|
+
const propGoType = typeInfoToGo(ctx, param.type, param.defaultValue)
|
|
30
|
+
if (propGoType.includes('interface{}')) {
|
|
31
|
+
const signalGoType = typeInfoToGo(ctx, signal.type, signal.initialValue)
|
|
32
|
+
if (!signalGoType.includes('interface{}')) {
|
|
33
|
+
overrides.set(propName, signalGoType)
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return overrides
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Resolve a prop param's Go struct-field type using the SAME logic
|
|
43
|
+
* `generatePropsStruct` / `generateInputStruct` use: a `propTypeOverrides` entry
|
|
44
|
+
* wins, otherwise `typeInfoToGo(param.type, param.defaultValue)`. Factored out so
|
|
45
|
+
* the nillable-field set (`collectNillablePropNames`) can't drift from the
|
|
46
|
+
* emitted field types.
|
|
47
|
+
*/
|
|
48
|
+
export function resolvePropGoType(
|
|
49
|
+
ctx: GoEmitContext,
|
|
50
|
+
param: IRMetadata['propsParams'][number],
|
|
51
|
+
propTypeOverrides: Map<string, string>,
|
|
52
|
+
): string {
|
|
53
|
+
const base = propTypeOverrides.get(param.name) ?? typeInfoToGo(ctx, param.type, param.defaultValue)
|
|
54
|
+
// An OPTIONAL prop typed as a named struct (`opts?: EmblaOptionsType`) lowers
|
|
55
|
+
// to `map[string]interface{}`, not the value struct: a value struct is always
|
|
56
|
+
// truthy in Go templates (so a `{{if .Opts}}`-guarded attribute could never be
|
|
57
|
+
// omitted), whereas a nil/empty map is falsy and round-trips through `bf_json`
|
|
58
|
+
// with only the supplied keys — matching JS `JSON.stringify` of a partial
|
|
59
|
+
// object instead of a zero-filled struct.
|
|
60
|
+
// Gate on `localStructFields` (an actual generated struct), NOT
|
|
61
|
+
// `localTypeNames` — the latter also covers string-union aliases
|
|
62
|
+
// (`placement?: 'top' | 'right' | …`), which must stay their scalar Go type.
|
|
63
|
+
if (param.optional && ctx.state.localStructFields.has(base)) {
|
|
64
|
+
return 'map[string]interface{}'
|
|
65
|
+
}
|
|
66
|
+
return base
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Build the set of prop NAMES whose resolved Go field type is exactly
|
|
71
|
+
* `interface{}` (nillable, for Hono-style attribute omission). Uses the same
|
|
72
|
+
* `propTypeOverrides` + `resolvePropGoType` pipeline as the struct generators.
|
|
73
|
+
* Concrete (`string`/`int`/`bool`/`[]T`/struct) types are excluded.
|
|
74
|
+
*/
|
|
75
|
+
export function collectNillablePropNames(ctx: GoEmitContext, ir: ComponentIR): Set<string> {
|
|
76
|
+
const propTypeOverrides = buildPropTypeOverrides(ctx, ir)
|
|
77
|
+
const nillable = new Set<string>()
|
|
78
|
+
for (const param of ir.metadata.propsParams) {
|
|
79
|
+
if (resolvePropGoType(ctx, param, propTypeOverrides) === 'interface{}') {
|
|
80
|
+
nillable.add(param.name)
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return nillable
|
|
84
|
+
}
|