@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.
Files changed (73) hide show
  1. package/dist/adapter/analysis/component-tree.d.ts +23 -0
  2. package/dist/adapter/analysis/component-tree.d.ts.map +1 -0
  3. package/dist/adapter/emit-context.d.ts +53 -0
  4. package/dist/adapter/emit-context.d.ts.map +1 -0
  5. package/dist/adapter/expr/helper-inline.d.ts +31 -0
  6. package/dist/adapter/expr/helper-inline.d.ts.map +1 -0
  7. package/dist/adapter/expr/url-builder.d.ts +23 -0
  8. package/dist/adapter/expr/url-builder.d.ts.map +1 -0
  9. package/dist/adapter/go-template-adapter.d.ts +291 -1070
  10. package/dist/adapter/go-template-adapter.d.ts.map +1 -1
  11. package/dist/adapter/index.js +2857 -2618
  12. package/dist/adapter/lib/compile-state.d.ts +116 -0
  13. package/dist/adapter/lib/compile-state.d.ts.map +1 -0
  14. package/dist/adapter/lib/constants.d.ts +11 -0
  15. package/dist/adapter/lib/constants.d.ts.map +1 -0
  16. package/dist/adapter/lib/go-emit.d.ts +117 -0
  17. package/dist/adapter/lib/go-emit.d.ts.map +1 -0
  18. package/dist/adapter/lib/go-naming.d.ts +39 -0
  19. package/dist/adapter/lib/go-naming.d.ts.map +1 -0
  20. package/dist/adapter/lib/ir-scope.d.ts +13 -0
  21. package/dist/adapter/lib/ir-scope.d.ts.map +1 -0
  22. package/dist/adapter/lib/types.d.ts +165 -0
  23. package/dist/adapter/lib/types.d.ts.map +1 -0
  24. package/dist/adapter/memo/ctor-lowering.d.ts +39 -0
  25. package/dist/adapter/memo/ctor-lowering.d.ts.map +1 -0
  26. package/dist/adapter/memo/memo-compute.d.ts +124 -0
  27. package/dist/adapter/memo/memo-compute.d.ts.map +1 -0
  28. package/dist/adapter/memo/memo-type.d.ts +38 -0
  29. package/dist/adapter/memo/memo-type.d.ts.map +1 -0
  30. package/dist/adapter/memo/memo-value.d.ts +64 -0
  31. package/dist/adapter/memo/memo-value.d.ts.map +1 -0
  32. package/dist/adapter/memo/template-interp.d.ts +43 -0
  33. package/dist/adapter/memo/template-interp.d.ts.map +1 -0
  34. package/dist/adapter/props/prop-types.d.ts +31 -0
  35. package/dist/adapter/props/prop-types.d.ts.map +1 -0
  36. package/dist/adapter/spread/spread-codegen.d.ts +40 -0
  37. package/dist/adapter/spread/spread-codegen.d.ts.map +1 -0
  38. package/dist/adapter/type/type-codegen.d.ts +25 -0
  39. package/dist/adapter/type/type-codegen.d.ts.map +1 -0
  40. package/dist/adapter/value/parsed-literal-to-go.d.ts +17 -0
  41. package/dist/adapter/value/parsed-literal-to-go.d.ts.map +1 -0
  42. package/dist/adapter/value/value-lowering.d.ts +46 -0
  43. package/dist/adapter/value/value-lowering.d.ts.map +1 -0
  44. package/dist/build.js +2856 -2617
  45. package/dist/index.js +2857 -2618
  46. package/dist/test-render.d.ts.map +1 -1
  47. package/package.json +3 -3
  48. package/src/__tests__/derived-state-memo.test.ts +155 -84
  49. package/src/__tests__/go-template-adapter.test.ts +118 -54
  50. package/src/__tests__/lowering-plugin.test.ts +109 -0
  51. package/src/__tests__/query-href.test.ts +179 -0
  52. package/src/adapter/analysis/component-tree.ts +174 -0
  53. package/src/adapter/emit-context.ts +59 -0
  54. package/src/adapter/expr/helper-inline.ts +274 -0
  55. package/src/adapter/expr/url-builder.ts +123 -0
  56. package/src/adapter/go-template-adapter.ts +2099 -5325
  57. package/src/adapter/lib/compile-state.ts +150 -0
  58. package/src/adapter/lib/constants.ts +24 -0
  59. package/src/adapter/lib/go-emit.ts +289 -0
  60. package/src/adapter/lib/go-naming.ts +84 -0
  61. package/src/adapter/lib/ir-scope.ts +31 -0
  62. package/src/adapter/lib/types.ts +182 -0
  63. package/src/adapter/memo/ctor-lowering.ts +267 -0
  64. package/src/adapter/memo/memo-compute.ts +451 -0
  65. package/src/adapter/memo/memo-type.ts +74 -0
  66. package/src/adapter/memo/memo-value.ts +197 -0
  67. package/src/adapter/memo/template-interp.ts +246 -0
  68. package/src/adapter/props/prop-types.ts +84 -0
  69. package/src/adapter/spread/spread-codegen.ts +458 -0
  70. package/src/adapter/type/type-codegen.ts +95 -0
  71. package/src/adapter/value/parsed-literal-to-go.ts +94 -0
  72. package/src/adapter/value/value-lowering.ts +162 -0
  73. package/src/test-render.ts +2 -14
@@ -0,0 +1,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
+ }