@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,182 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Internal type definitions for the Go html/template adapter: the adapter's
|
|
3
|
+
* intermediate bookkeeping shapes (nested-component info, static child
|
|
4
|
+
* instances, spread slots, ctor-lowering scope, …) plus the public
|
|
5
|
+
* `GoTemplateAdapterOptions`. Pure type surface — no behaviour.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type {
|
|
9
|
+
IRLoopChildComponent,
|
|
10
|
+
IRNode,
|
|
11
|
+
IRProp,
|
|
12
|
+
ParsedExpr,
|
|
13
|
+
TypeInfo,
|
|
14
|
+
} from '@barefootjs/jsx'
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Go-template adapter's IRNode render context. Only `isRootOfClientComponent`
|
|
18
|
+
* is consumed today (forwarded into `renderComponent` / `renderIfStatement`);
|
|
19
|
+
* the type stays open so future render-position flags can be added without
|
|
20
|
+
* widening the `IRNodeEmitter` contract.
|
|
21
|
+
*/
|
|
22
|
+
export type GoRenderCtx = {
|
|
23
|
+
isRootOfClientComponent?: boolean
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Extended nested component info that tracks whether the component
|
|
28
|
+
* comes from a dynamic (signal) array loop vs a static array loop.
|
|
29
|
+
*/
|
|
30
|
+
export interface NestedComponentInfo extends IRLoopChildComponent {
|
|
31
|
+
isDynamic: boolean
|
|
32
|
+
isPropDerived: boolean
|
|
33
|
+
/** The enclosing loop's `key` expression (e.g. `item.label`) and map param
|
|
34
|
+
* name (`item`), so the loop-child init can stamp `data-key` per item. */
|
|
35
|
+
loopKey?: string
|
|
36
|
+
loopParam?: string
|
|
37
|
+
/** The loop body component's JSX children. Non-empty when those children need
|
|
38
|
+
* a companion define rendered via `bf_with_children` + `bf_tmpl`. */
|
|
39
|
+
bodyChildren?: IRNode[]
|
|
40
|
+
/** The loop's array expression for baking (e.g. `sortedData()`) */
|
|
41
|
+
loopArray?: string
|
|
42
|
+
/** Structured parse of `loopArray` (the loop's `array` string), carried so
|
|
43
|
+
* scalar-literal loop typing reads the tree instead of re-parsing. */
|
|
44
|
+
loopArrayParsed?: ParsedExpr
|
|
45
|
+
/** The enclosing loop's `markerId` (e.g. `l0`) for unique naming */
|
|
46
|
+
loopMarkerId?: string
|
|
47
|
+
/** The loop item's TS type (`Payment` from `sortedData().map(payment => …)`),
|
|
48
|
+
* resolved to Go struct fields for the wrapper struct's datum fields. */
|
|
49
|
+
loopItemType?: TypeInfo | null
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface StaticChildInstance {
|
|
53
|
+
name: string
|
|
54
|
+
slotId: string
|
|
55
|
+
props: IRProp[]
|
|
56
|
+
fieldName: string
|
|
57
|
+
/** Concatenated text content from JSX children (e.g. `+1` for
|
|
58
|
+
* `<Button>+1</Button>`). Null when children include any non-text node;
|
|
59
|
+
* those take the `childrenHtml` path if purely static HTML, else dropped. */
|
|
60
|
+
childrenText: string | null
|
|
61
|
+
/** Rendered Go-template fragment for purely-static, non-text JSX children,
|
|
62
|
+
* forwarded via `Children: template.HTML(...)` so the child's
|
|
63
|
+
* `{{or .Children ""}}` skips re-escaping. Null when children are text-only
|
|
64
|
+
* or absent, OR when the fragment contains any `{{...}}` action (those
|
|
65
|
+
* wouldn't re-evaluate through the parent's `{{.Children}}` read — kept on
|
|
66
|
+
* the drop path). */
|
|
67
|
+
childrenHtml: string | null
|
|
68
|
+
/** Go string-concat expression for hoisted-JSX children that carry a
|
|
69
|
+
* `needsScope` root (`children={<span/>}`). The root's `bf-s` resolves to
|
|
70
|
+
* the PARENT scope, so the fragment can't bake to a static string — the
|
|
71
|
+
* runtime `scopeID` is spliced in (`"<span bf-s=\"" + scopeID + "\">x</span>"`).
|
|
72
|
+
* Null when static `childrenHtml` already covers the children, or when any
|
|
73
|
+
* other template action survives (genuinely dynamic — drop path). */
|
|
74
|
+
childrenScopedHtmlExpr: string | null
|
|
75
|
+
/**
|
|
76
|
+
* Context values from enclosing `<Ctx.Provider value>` ancestors
|
|
77
|
+
* (`createContext` identifier → Go value literal), wired into this child
|
|
78
|
+
* slot's input against its own context-consumer fields. Empty/undefined when
|
|
79
|
+
* the child isn't under any provider.
|
|
80
|
+
*/
|
|
81
|
+
contextBindings?: ReadonlyMap<string, string>
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Cross-component shape of a child component the parent renders.
|
|
86
|
+
* `paramNames` are the child's declared `propsParams`; `restBagField` is the
|
|
87
|
+
* Go field name of the child's open-ended rest bag
|
|
88
|
+
* (`Capitalize(restPropsName)`), or null when the child has no `...props` rest.
|
|
89
|
+
*/
|
|
90
|
+
export interface ChildComponentShape {
|
|
91
|
+
paramNames: Set<string>
|
|
92
|
+
restBagField: string | null
|
|
93
|
+
/**
|
|
94
|
+
* Child param names whose Go field is `map[string]interface{}` — an optional
|
|
95
|
+
* object/named-interface prop (`opts?: EmblaOptionsType`). A parent passing
|
|
96
|
+
* an inline object literal to such a param bakes it to a Go map literal so
|
|
97
|
+
* the keys round-trip faithfully.
|
|
98
|
+
*/
|
|
99
|
+
mapTypedParamNames: Set<string>
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Top-level (non-loop) JSX intrinsic-element spread slot. The adapter emits one
|
|
104
|
+
* `Spread_<slotId> map[string]any` field on the component's Props struct and
|
|
105
|
+
* initialises it in `NewXxxProps` from the source JS expression. Loop-internal
|
|
106
|
+
* spreads don't appear here — they emit the bag inline via the loop's iteration
|
|
107
|
+
* variable instead.
|
|
108
|
+
*
|
|
109
|
+
* `bagSource` records how the bag is supplied:
|
|
110
|
+
* - `'inline'`: constructed inside `NewXxxProps` from compile-time-known data
|
|
111
|
+
* (signal initial values, prop refs, propsObject enumeration). No Input field.
|
|
112
|
+
* - `'input-bag'`: provided by the caller as a `Spread_<slotId> map[string]any`
|
|
113
|
+
* field on the Input struct (for `restPropsName` spreads whose keys are
|
|
114
|
+
* open-ended and can't be enumerated under Go's static typing).
|
|
115
|
+
*/
|
|
116
|
+
export interface SpreadSlotInfo {
|
|
117
|
+
slotId: string
|
|
118
|
+
expr: string
|
|
119
|
+
/**
|
|
120
|
+
* Best-effort structured parse of `expr`. Lets the conditional inline-object
|
|
121
|
+
* spread lower from the tree instead of re-parsing `expr`. When absent, a
|
|
122
|
+
* non-conditional / `unsupported` tree falls through to the other spread
|
|
123
|
+
* shapes.
|
|
124
|
+
*/
|
|
125
|
+
parsed: ParsedExpr | undefined
|
|
126
|
+
templateExpr: string | undefined
|
|
127
|
+
bagSource: 'inline' | 'input-bag'
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Hoisted local var representing a prop with a signal-time `??` fallback. Used
|
|
132
|
+
* to share the fallback-applied value across the prop, signal, and memo fields.
|
|
133
|
+
*/
|
|
134
|
+
export interface PropFallbackVar {
|
|
135
|
+
/** Local variable name (typically the lowercase prop identifier). */
|
|
136
|
+
varName: string
|
|
137
|
+
/** Capitalised Go field name on the `Input` struct. */
|
|
138
|
+
fieldName: string
|
|
139
|
+
/** Go literal used when the input value equals its zero value. */
|
|
140
|
+
goFallback: string
|
|
141
|
+
/** Go zero literal for the prop's type (`0`, `""`, etc.). */
|
|
142
|
+
zeroLiteral: string
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Scope for `lowerCtorExpr` — lowering a JS expression to Go in the
|
|
147
|
+
* `NewXxxProps` constructor context.
|
|
148
|
+
*/
|
|
149
|
+
export interface CtorLowerEnv {
|
|
150
|
+
/** Local names bound to `searchParams()` (`const sp = searchParams()`). */
|
|
151
|
+
searchParamsVars: Set<string>
|
|
152
|
+
/** Helper-param name → its already-lowered Go argument, for inlining. */
|
|
153
|
+
params: Map<string, string>
|
|
154
|
+
/** Component-scope const names currently being inlined (cycle guard). */
|
|
155
|
+
consts?: Set<string>
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export interface GoTemplateAdapterOptions {
|
|
159
|
+
/** Go package name for generated types (default: 'components') */
|
|
160
|
+
packageName?: string
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Base path for client JS files (e.g., '/static/client/').
|
|
164
|
+
* Used to generate script registration paths.
|
|
165
|
+
*/
|
|
166
|
+
clientJsBasePath?: string
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Path to barefoot.js runtime (e.g., '/static/client/barefoot.js').
|
|
170
|
+
*/
|
|
171
|
+
barefootJsPath?: string
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Single source of truth for the Go adapter's template-primitive surface. Each
|
|
176
|
+
* entry pairs the expected arity with the emit function so the two derived maps
|
|
177
|
+
* (`templatePrimitives` and `templatePrimitiveArities`) can't drift out of sync.
|
|
178
|
+
*/
|
|
179
|
+
export interface PrimitiveSpec {
|
|
180
|
+
arity: number
|
|
181
|
+
emit: (args: string[]) => string
|
|
182
|
+
}
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Constructor-context expression lowering for derived-state memos.
|
|
3
|
+
*
|
|
4
|
+
* Free functions over a {@link GoEmitContext} that lower the narrow surface of
|
|
5
|
+
* JS expressions a derived-state memo needs into Go *code* (not template
|
|
6
|
+
* syntax) evaluated in the `NewXxxProps` constructor — e.g. a search-param read
|
|
7
|
+
* becomes `in.SearchParams.Get("k")`. `lowerCtorExpr` and `lowerCtorCond` are
|
|
8
|
+
* mutually recursive and set `state.needsStringsImport` when they emit a
|
|
9
|
+
* `strings.*` call. Anything outside the supported surface returns null so the
|
|
10
|
+
* caller can fall back to nil safely.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { type ParsedExpr } from '@barefootjs/jsx'
|
|
14
|
+
|
|
15
|
+
import type { GoEmitContext } from '../emit-context.ts'
|
|
16
|
+
import type { CtorLowerEnv } from '../lib/types.ts'
|
|
17
|
+
import { capitalizeFieldName } from '../lib/go-naming.ts'
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Whether `node` is the trailing-slash strip `<recv>.replace(/\/+$/, '')`. The
|
|
21
|
+
* structured parser carries the deferred regex form of `String.replace` as an
|
|
22
|
+
* `array-method` whose first arg is a `regex` node (#2039), so the one pattern
|
|
23
|
+
* the ctor lowering recognises is matched off the IR — no emit-time re-parse.
|
|
24
|
+
* Validates the exact regex pattern and the empty-string replacement.
|
|
25
|
+
*/
|
|
26
|
+
function isTrailingSlashReplace(node: ParsedExpr): boolean {
|
|
27
|
+
if (node.kind !== 'array-method' || node.method !== 'replace') return false
|
|
28
|
+
const [pattern, replacement] = node.args
|
|
29
|
+
return (
|
|
30
|
+
pattern?.kind === 'regex' &&
|
|
31
|
+
pattern.raw === '/\\/+$/' &&
|
|
32
|
+
replacement?.kind === 'literal' &&
|
|
33
|
+
replacement.literalType === 'string' &&
|
|
34
|
+
replacement.value === ''
|
|
35
|
+
)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Lower a JS expression to a Go expression in the `NewXxxProps` constructor
|
|
40
|
+
* context. This is Go *code*, not template syntax — so a search-param read
|
|
41
|
+
* becomes `in.SearchParams.Get("k")` (method call), not the template's
|
|
42
|
+
* `.SearchParams.Get "k"`. Supports the narrow surface derived-state memos
|
|
43
|
+
* need: string/number literals, `<sp>.get('k')`, `<arr>.includes(<x>)`,
|
|
44
|
+
* module arrow-helper inlining, `<expr> ?? <fallback>`, and string ternaries.
|
|
45
|
+
* Returns null for anything else so the caller can fall back safely.
|
|
46
|
+
*/
|
|
47
|
+
export function lowerCtorExpr(
|
|
48
|
+
ctx: GoEmitContext,
|
|
49
|
+
node: ParsedExpr,
|
|
50
|
+
env: CtorLowerEnv,
|
|
51
|
+
): string | null {
|
|
52
|
+
if (node.kind === 'literal' && node.literalType === 'string') {
|
|
53
|
+
return JSON.stringify(node.value)
|
|
54
|
+
}
|
|
55
|
+
if (node.kind === 'literal' && node.literalType === 'number') {
|
|
56
|
+
return node.raw ?? String(node.value)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Identifier: a substituted helper param, a module string const, or a
|
|
60
|
+
// component-scope derived const inlined recursively (e.g. `root` → its
|
|
61
|
+
// `base || '/'` value).
|
|
62
|
+
if (node.kind === 'identifier') {
|
|
63
|
+
const sub = env.params.get(node.name)
|
|
64
|
+
if (sub !== undefined) return sub
|
|
65
|
+
const c = ctx.state.localConstants.find(lc => lc.name === node.name)
|
|
66
|
+
if (c?.value !== undefined) {
|
|
67
|
+
if (c.isModule) {
|
|
68
|
+
const lit = c.parsed
|
|
69
|
+
if (lit && lit.kind === 'literal' && lit.literalType === 'string') {
|
|
70
|
+
return JSON.stringify(lit.value)
|
|
71
|
+
}
|
|
72
|
+
return null
|
|
73
|
+
}
|
|
74
|
+
// Component-scope const: inline its computed value, guarding cycles.
|
|
75
|
+
if (env.consts?.has(node.name)) return null
|
|
76
|
+
const inner = c.parsed
|
|
77
|
+
if (!inner) return null
|
|
78
|
+
return lowerCtorExpr(ctx, inner, {
|
|
79
|
+
...env,
|
|
80
|
+
consts: new Set([...(env.consts ?? []), node.name]),
|
|
81
|
+
})
|
|
82
|
+
}
|
|
83
|
+
return null
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// `props.<X>` → the constructor input field `in.<X>`.
|
|
87
|
+
if (
|
|
88
|
+
node.kind === 'member' &&
|
|
89
|
+
node.object.kind === 'identifier' &&
|
|
90
|
+
node.object.name === ctx.state.propsObjectName &&
|
|
91
|
+
!node.computed
|
|
92
|
+
) {
|
|
93
|
+
return `in.${capitalizeFieldName(node.property)}`
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (node.kind === 'call' && node.callee.kind === 'member') {
|
|
97
|
+
const method = node.callee.property
|
|
98
|
+
const recv = node.callee.object
|
|
99
|
+
// `<sp>.get('k')` where the receiver is `searchParams()`: either a local
|
|
100
|
+
// bound to it (`const sp = searchParams()` → `sp.get(...)`, the env path) or
|
|
101
|
+
// — once the block memo is folded (#2040) — the inlined `searchParams()`
|
|
102
|
+
// call directly.
|
|
103
|
+
const isSearchParamsRecv =
|
|
104
|
+
(recv.kind === 'identifier' && env.searchParamsVars.has(recv.name)) ||
|
|
105
|
+
(recv.kind === 'call' &&
|
|
106
|
+
recv.callee.kind === 'identifier' &&
|
|
107
|
+
recv.args.length === 0 &&
|
|
108
|
+
ctx.state.searchParamsLocals.has(recv.callee.name))
|
|
109
|
+
if (
|
|
110
|
+
method === 'get' &&
|
|
111
|
+
isSearchParamsRecv &&
|
|
112
|
+
node.args.length === 1 &&
|
|
113
|
+
node.args[0].kind === 'literal' &&
|
|
114
|
+
node.args[0].literalType === 'string'
|
|
115
|
+
) {
|
|
116
|
+
return `in.SearchParams.Get(${JSON.stringify(node.args[0].value)})`
|
|
117
|
+
}
|
|
118
|
+
return null
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// `<arr>.includes(<x>)` → bf.Includes(<[]string{…}>, <x>) (bool). The
|
|
122
|
+
// collapsed `parseExpression` folds `.includes` into a generic `array-method`
|
|
123
|
+
// node (not a `call` with a member callee), so it's matched here.
|
|
124
|
+
if (node.kind === 'array-method' && node.method === 'includes' && node.args.length === 1) {
|
|
125
|
+
const arr = lowerCtorStringArray(ctx, node.object)
|
|
126
|
+
const elem = lowerCtorExpr(ctx, node.args[0], env)
|
|
127
|
+
if (arr !== null && elem !== null) return `bf.Includes(${arr}, ${elem})`
|
|
128
|
+
return null
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// `<s>.replace(/\/+$/, '')` — strip trailing slashes → strings.TrimRight. The
|
|
132
|
+
// parser defers the regex form of `String.replace` but carries its shape
|
|
133
|
+
// structurally (an `array-method` replace with a `regex` first arg, #2039), so
|
|
134
|
+
// the one recognized pattern is matched off the IR — no re-parse. Only this
|
|
135
|
+
// exact trailing-slash regex is supported (a general regex replace would need
|
|
136
|
+
// Go's regexp).
|
|
137
|
+
if (node.kind === 'array-method' && node.method === 'replace') {
|
|
138
|
+
if (!isTrailingSlashReplace(node)) return null
|
|
139
|
+
const recvGo = lowerCtorExpr(ctx, node.object, env)
|
|
140
|
+
if (recvGo === null) return null
|
|
141
|
+
ctx.state.needsStringsImport = true
|
|
142
|
+
return `strings.TrimRight(${recvGo}, "/")`
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// `helper(<args>)` where helper is a module arrow const → inline its body.
|
|
146
|
+
if (node.kind === 'call' && node.callee.kind === 'identifier') {
|
|
147
|
+
const calleeName = node.callee.name
|
|
148
|
+
const fnConst = ctx.state.localConstants.find(
|
|
149
|
+
lc => lc.name === calleeName && lc.isModule,
|
|
150
|
+
)
|
|
151
|
+
if (fnConst?.value) {
|
|
152
|
+
const fn = fnConst.parsed
|
|
153
|
+
if (fn && fn.kind === 'arrow' && fn.params.length === node.args.length) {
|
|
154
|
+
const params = new Map(env.params)
|
|
155
|
+
for (let i = 0; i < fn.params.length; i++) {
|
|
156
|
+
const argGo = lowerCtorExpr(ctx, node.args[i], env)
|
|
157
|
+
if (argGo === null) return null
|
|
158
|
+
params.set(fn.params[i], argGo)
|
|
159
|
+
}
|
|
160
|
+
return lowerCtorExpr(ctx, fn.body, { searchParamsVars: env.searchParamsVars, params })
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
return null
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// `<expr> ?? <fallback>`
|
|
167
|
+
if (node.kind === 'logical' && node.op === '??') {
|
|
168
|
+
const left = lowerCtorExpr(ctx, node.left, env)
|
|
169
|
+
if (left === null) return null
|
|
170
|
+
const r = node.right
|
|
171
|
+
// `?? ''` is a no-op: SearchParams.Get already returns "" for a missing key.
|
|
172
|
+
if (r.kind === 'literal' && r.literalType === 'string' && r.value === '') {
|
|
173
|
+
return left
|
|
174
|
+
}
|
|
175
|
+
const right = lowerCtorExpr(ctx, node.right, env)
|
|
176
|
+
if (right === null) return null
|
|
177
|
+
return `func() string { v := ${left}; if v != "" { return v }; return ${right} }()`
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// `<expr> || <fallback>` (string `||` — falsy = empty, like `?? ''` but the
|
|
181
|
+
// left can itself be empty): `base || '/'`.
|
|
182
|
+
if (node.kind === 'logical' && node.op === '||') {
|
|
183
|
+
const left = lowerCtorExpr(ctx, node.left, env)
|
|
184
|
+
const right = lowerCtorExpr(ctx, node.right, env)
|
|
185
|
+
if (left === null || right === null) return null
|
|
186
|
+
return `func() string { v := ${left}; if v != "" { return v }; return ${right} }()`
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// `<cond> ? <t> : <f>` (string result)
|
|
190
|
+
if (node.kind === 'conditional') {
|
|
191
|
+
// The condition must be lowered as a *boolean* (`lowerCtorCond`), not a
|
|
192
|
+
// value: a string-valued JS condition like `sp.get('tag') ? a : b` is
|
|
193
|
+
// truthy in JS, but `if "<string>"` does not compile in Go — such shapes
|
|
194
|
+
// return null so the memo falls back to nil rather than emitting invalid Go.
|
|
195
|
+
const cond = lowerCtorCond(ctx, node.test, env)
|
|
196
|
+
const t = lowerCtorExpr(ctx, node.consequent, env)
|
|
197
|
+
const f = lowerCtorExpr(ctx, node.alternate, env)
|
|
198
|
+
if (cond !== null && t !== null && f !== null) {
|
|
199
|
+
return `func() string { if ${cond} { return ${t} }; return ${f} }()`
|
|
200
|
+
}
|
|
201
|
+
return null
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
return null
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Lower a JS expression used as a *boolean* condition to a Go bool expression,
|
|
209
|
+
* or null when it is not provably boolean. Distinct from `lowerCtorExpr`, which
|
|
210
|
+
* lowers value expressions: a string-valued condition (`sp.get('tag')`) is
|
|
211
|
+
* truthy in JS but `if "<string>"` does not compile in Go, so anything not
|
|
212
|
+
* known to yield a Go bool must fall back to null.
|
|
213
|
+
*/
|
|
214
|
+
export function lowerCtorCond(
|
|
215
|
+
ctx: GoEmitContext,
|
|
216
|
+
node: ParsedExpr,
|
|
217
|
+
env: CtorLowerEnv,
|
|
218
|
+
): string | null {
|
|
219
|
+
if (node.kind === 'literal' && node.literalType === 'boolean') {
|
|
220
|
+
return String(node.value)
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// `!<cond>`
|
|
224
|
+
if (node.kind === 'unary' && node.op === '!') {
|
|
225
|
+
const inner = lowerCtorCond(ctx, node.argument, env)
|
|
226
|
+
return inner === null ? null : `!(${inner})`
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// `<a> && <b>` / `<a> || <b>` — both operands must themselves be boolean.
|
|
230
|
+
if (node.kind === 'logical' && (node.op === '&&' || node.op === '||')) {
|
|
231
|
+
const op = node.op
|
|
232
|
+
const l = lowerCtorCond(ctx, node.left, env)
|
|
233
|
+
const r = lowerCtorCond(ctx, node.right, env)
|
|
234
|
+
return l !== null && r !== null ? `(${l} ${op} ${r})` : null
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// `<arr>.includes(<x>)` is the one value-shape `lowerCtorExpr` lowers to a
|
|
238
|
+
// Go bool (`bf.Includes(...)`); reuse it for that case only. The collapsed
|
|
239
|
+
// `parseExpression` folds `.includes` into an `array-method` node.
|
|
240
|
+
if (node.kind === 'array-method' && node.method === 'includes') {
|
|
241
|
+
return lowerCtorExpr(ctx, node, env)
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
return null
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Resolve a string-array expression (a `['a','b']` literal, or a module const
|
|
249
|
+
* bound to one) to a Go `[]string{…}` literal, or null when it isn't a pure
|
|
250
|
+
* string-array. Used by `lowerCtorExpr` for `<arr>.includes(<x>)`.
|
|
251
|
+
*/
|
|
252
|
+
export function lowerCtorStringArray(ctx: GoEmitContext, node: ParsedExpr): string | null {
|
|
253
|
+
let arr: ParsedExpr | null = node
|
|
254
|
+
if (node.kind === 'identifier') {
|
|
255
|
+
const c = ctx.state.localConstants.find(lc => lc.name === node.name && lc.isModule)
|
|
256
|
+
if (!c?.value) return null
|
|
257
|
+
arr = c.parsed ?? null
|
|
258
|
+
}
|
|
259
|
+
if (!arr || arr.kind !== 'array-literal') return null
|
|
260
|
+
const elems: string[] = []
|
|
261
|
+
for (const el of arr.elements) {
|
|
262
|
+
if (el.kind === 'literal' && el.literalType === 'string') {
|
|
263
|
+
elems.push(JSON.stringify(el.value))
|
|
264
|
+
} else return null
|
|
265
|
+
}
|
|
266
|
+
return `[]string{${elems.join(', ')}}`
|
|
267
|
+
}
|