@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.
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 +302 -1071
  10. package/dist/adapter/go-template-adapter.d.ts.map +1 -1
  11. package/dist/adapter/index.js +3116 -2692
  12. package/dist/adapter/lib/compile-state.d.ts +142 -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 +126 -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 +173 -0
  27. package/dist/adapter/memo/memo-compute.d.ts.map +1 -0
  28. package/dist/adapter/memo/memo-type.d.ts +48 -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 +3114 -2690
  45. package/dist/index.js +3116 -2692
  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 +398 -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 +2189 -5320
  57. package/src/adapter/lib/compile-state.ts +181 -0
  58. package/src/adapter/lib/constants.ts +24 -0
  59. package/src/adapter/lib/go-emit.ts +309 -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 +661 -0
  65. package/src/adapter/memo/memo-type.ts +93 -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 +48 -22
@@ -0,0 +1,274 @@
1
+ /**
2
+ * Inlining of component-scope arrow-const helpers at a call site.
3
+ *
4
+ * When a JSX expression calls a local `const f = (a) => …` helper, the Go
5
+ * adapter has no function to emit — it inlines the helper body with the call
6
+ * args substituted for the params, so the result lowers like any inline
7
+ * expression. The substitution is scope-blind, so the guards here reject bodies
8
+ * where a param replacement could be wrong.
9
+ */
10
+
11
+ import { type ParsedExpr } from '@barefootjs/jsx'
12
+
13
+ import type { GoEmitContext } from '../emit-context.ts'
14
+
15
+ /**
16
+ * Inline a call to a local arrow-const helper.
17
+ *
18
+ * @param callParsed the call's already-built `ParsedExpr`. Must be a `call`
19
+ * node; inlining is declined without it.
20
+ * @returns the substituted body tree, or null when the expression isn't such a
21
+ * call or the body isn't substitution-safe (nested functions, helper-calling
22
+ * helpers, spread args, …)
23
+ */
24
+ export function inlineLocalHelperCall(
25
+ ctx: GoEmitContext,
26
+ jsExpr: string,
27
+ callParsed?: ParsedExpr,
28
+ ): ParsedExpr | null {
29
+ if (ctx.state.localHelperNames.size === 0) return null
30
+ // Fast path: skip unless the expression begins with a known helper name
31
+ // applied as a call (`<helper>(`); the real shape check is the tree
32
+ // inspection below.
33
+ const head = /^\s*([A-Za-z_$][\w$]*)\s*\(/.exec(jsExpr)
34
+ if (!head || !ctx.state.localHelperNames.has(head[1])) return null
35
+
36
+ if (!callParsed || callParsed.kind !== 'call' || callParsed.callee.kind !== 'identifier') {
37
+ return null
38
+ }
39
+ const calleeName = callParsed.callee.name
40
+ if (!ctx.state.localHelperNames.has(calleeName)) return null
41
+
42
+ const fnConst = ctx.state.localConstants.find(
43
+ c => c.name === calleeName && !c.isModule && c.parsed,
44
+ )
45
+ const arrow = fnConst?.parsed
46
+ if (!arrow || arrow.kind !== 'arrow') return null
47
+ if (arrow.params.length !== callParsed.args.length) return null
48
+
49
+ // The body must be a lowerable `ParsedExpr` — an `unsupported` body (e.g. a
50
+ // nested function / regex outside the narrow surface) refuses here.
51
+ const body = arrow.body
52
+ if (!body || body.kind === 'unsupported') return null
53
+
54
+ // Don't half-inline a helper that calls another local helper.
55
+ if (bodyCallsLocalHelper(ctx, body)) return null
56
+
57
+ // Refuse a body containing a method call (`x.foo(…)`). The structural body
58
+ // tree models it as a generic `call`, but `parseExpression` (which the rest
59
+ // of the lowering is keyed to) folds the string / array method family
60
+ // (`.replace`, `.includes`, `.filter`, …) into specialised `array-method` /
61
+ // `higher-order` nodes — so lowering this tree directly would diverge.
62
+ // (`x()` with an identifier callee — `params()` — is fine.)
63
+ if (bodyHasMethodCall(body)) return null
64
+
65
+ const subs = new Map<string, ParsedExpr>()
66
+ for (let i = 0; i < arrow.params.length; i++) {
67
+ subs.set(arrow.params[i], callParsed.args[i])
68
+ }
69
+ // Reject bodies where a param name appears as an object shorthand key
70
+ // (`{ k }`, which can't be rewritten to `{ (arg) }`).
71
+ if (!isSubstituteSafeBody(body, new Set(subs.keys()))) return null
72
+ return substituteHelperParams(body, subs)
73
+ }
74
+
75
+ /**
76
+ * Guard for `substituteHelperParams`: an `object-literal` lowers opaquely from
77
+ * its `raw` source string, so a param referenced ANYWHERE inside it survives
78
+ * un-substituted — a shorthand key (`{ p }`) or a value position (`{ x: p }`,
79
+ * `{ x: f(p) }`) — and would emit the param's original name instead of the call
80
+ * arg. A body is substitute-safe only when no object literal it contains
81
+ * references any param.
82
+ */
83
+ function isSubstituteSafeBody(body: ParsedExpr, paramNames: ReadonlySet<string>): boolean {
84
+ // True when `n`'s subtree references any param (handling object-literal
85
+ // shorthand keys, which carry the param name on the key rather than a value).
86
+ const referencesParam = (n: ParsedExpr): boolean => {
87
+ if (n.kind === 'identifier') return paramNames.has(n.name)
88
+ if (n.kind === 'object-literal') {
89
+ return n.properties.some(
90
+ p => (p.shorthand && paramNames.has(p.key)) || referencesParam(p.value),
91
+ )
92
+ }
93
+ let hit = false
94
+ forEachValueChild(n, c => {
95
+ if (referencesParam(c)) hit = true
96
+ })
97
+ return hit
98
+ }
99
+
100
+ let safe = true
101
+ const visit = (n: ParsedExpr): void => {
102
+ if (!safe) return
103
+ if (n.kind === 'object-literal') {
104
+ // The whole object literal lowers from `raw`; a param mentioned anywhere
105
+ // inside can't be substituted — decline the body.
106
+ if (referencesParam(n)) safe = false
107
+ return
108
+ }
109
+ forEachValueChild(n, visit)
110
+ }
111
+ visit(body)
112
+ return safe
113
+ }
114
+
115
+ /**
116
+ * True when `body` contains a call to a *local* (component-scope) arrow helper
117
+ * const — the signal that inlining `body` here would only push the problem to
118
+ * another un-lowered helper (e.g. `sortHref`'s body calls `hrefFor`).
119
+ */
120
+ function bodyCallsLocalHelper(ctx: GoEmitContext, body: ParsedExpr): boolean {
121
+ let found = false
122
+ const visit = (n: ParsedExpr): void => {
123
+ if (found) return
124
+ if (n.kind === 'call' && n.callee.kind === 'identifier') {
125
+ const name = n.callee.name
126
+ if (ctx.state.localConstants.some(c => c.name === name && !c.isModule && c.containsArrow)) {
127
+ found = true
128
+ return
129
+ }
130
+ }
131
+ forEachValueChild(n, visit)
132
+ }
133
+ visit(body)
134
+ return found
135
+ }
136
+
137
+ /**
138
+ * True when `body` contains a method call (`x.foo(…)` — a `call` whose callee
139
+ * is a `member`); such a body must not be lowered from this tree. See the guard
140
+ * in `inlineLocalHelperCall` for why.
141
+ */
142
+ function bodyHasMethodCall(body: ParsedExpr): boolean {
143
+ let found = false
144
+ const visit = (n: ParsedExpr): void => {
145
+ if (found) return
146
+ if (n.kind === 'call' && n.callee.kind === 'member') {
147
+ found = true
148
+ return
149
+ }
150
+ forEachValueChild(n, visit)
151
+ }
152
+ visit(body)
153
+ return found
154
+ }
155
+
156
+ /**
157
+ * Re-build `body` with each identifier named in `subs` replaced by the
158
+ * substitution's `ParsedExpr` subtree. The substitution is the arg subtree
159
+ * itself, so operator precedence survives without parenthesisation (the tree
160
+ * *is* the precedence). Non-value identifier positions — the property NAME in
161
+ * `a.b`, a plain `{ k: … }` key — are not visited, so a param sharing a name
162
+ * with a member or key is left untouched.
163
+ */
164
+ export function substituteHelperParams(
165
+ body: ParsedExpr,
166
+ subs: ReadonlyMap<string, ParsedExpr>,
167
+ ): ParsedExpr {
168
+ const go = (n: ParsedExpr): ParsedExpr => {
169
+ switch (n.kind) {
170
+ case 'identifier': {
171
+ const sub = subs.get(n.name)
172
+ return sub !== undefined ? sub : n
173
+ }
174
+ // `object-literal` / `unsupported` lower from their `raw` string, and a
175
+ // `regex` carries its exact source text — re-lowering structured children
176
+ // has no effect, so leave as-is (a param inside an object literal is
177
+ // rejected by the shorthand guard upstream).
178
+ case 'literal':
179
+ case 'object-literal':
180
+ case 'unsupported':
181
+ case 'regex':
182
+ return n
183
+ case 'member':
184
+ return { ...n, object: go(n.object) }
185
+ case 'index-access':
186
+ return { ...n, object: go(n.object), index: go(n.index) }
187
+ case 'call':
188
+ return { ...n, callee: go(n.callee), args: n.args.map(go) }
189
+ case 'binary':
190
+ return { ...n, left: go(n.left), right: go(n.right) }
191
+ case 'logical':
192
+ return { ...n, left: go(n.left), right: go(n.right) }
193
+ case 'unary':
194
+ return { ...n, argument: go(n.argument) }
195
+ case 'conditional':
196
+ return { ...n, test: go(n.test), consequent: go(n.consequent), alternate: go(n.alternate) }
197
+ case 'template-literal':
198
+ return {
199
+ ...n,
200
+ parts: n.parts.map(p =>
201
+ p.type === 'expression' ? { type: 'expression', expr: go(p.expr) } : p,
202
+ ),
203
+ }
204
+ case 'array-literal':
205
+ return { ...n, elements: n.elements.map(go) }
206
+ case 'arrow':
207
+ // A nested arrow's params shadow any same-named outer helper param, but
208
+ // the inline body-method guard (`bodyHasMethodCall`) already declined
209
+ // bodies whose substitution could be wrong; re-lower the body verbatim.
210
+ return { ...n, body: go(n.body) }
211
+ case 'array-method':
212
+ // `.flat` carries no `args`; the generic variant does. Re-lower both
213
+ // `object` and any `args`.
214
+ return n.method === 'flat'
215
+ ? { ...n, object: go(n.object) }
216
+ : { ...n, object: go(n.object), args: n.args.map(go) }
217
+ }
218
+ }
219
+ return go(body)
220
+ }
221
+
222
+ /**
223
+ * Visit the value-position `ParsedExpr` children of `n` (the property name in
224
+ * `a.b` and a plain `{ k: v }` key are skipped — they aren't value positions;
225
+ * `object-literal` is handled by the caller because its shorthand keys carry a
226
+ * guard).
227
+ */
228
+ function forEachValueChild(n: ParsedExpr, visit: (c: ParsedExpr) => void): void {
229
+ switch (n.kind) {
230
+ case 'identifier':
231
+ case 'literal':
232
+ case 'unsupported':
233
+ case 'object-literal':
234
+ case 'regex':
235
+ return
236
+ case 'member':
237
+ visit(n.object)
238
+ return
239
+ case 'index-access':
240
+ visit(n.object)
241
+ visit(n.index)
242
+ return
243
+ case 'call':
244
+ visit(n.callee)
245
+ n.args.forEach(visit)
246
+ return
247
+ case 'binary':
248
+ case 'logical':
249
+ visit(n.left)
250
+ visit(n.right)
251
+ return
252
+ case 'unary':
253
+ visit(n.argument)
254
+ return
255
+ case 'conditional':
256
+ visit(n.test)
257
+ visit(n.consequent)
258
+ visit(n.alternate)
259
+ return
260
+ case 'template-literal':
261
+ for (const p of n.parts) if (p.type === 'expression') visit(p.expr)
262
+ return
263
+ case 'array-literal':
264
+ n.elements.forEach(visit)
265
+ return
266
+ case 'arrow':
267
+ visit(n.body)
268
+ return
269
+ case 'array-method':
270
+ visit(n.object)
271
+ if (n.method !== 'flat') n.args.forEach(visit)
272
+ return
273
+ }
274
+ }
@@ -0,0 +1,123 @@
1
+ /**
2
+ * Go lowering of helper calls to template expressions. Renders backend-neutral
3
+ * `LoweringNode`s produced by registered lowering plugins (#2057) — one uniform
4
+ * path for both userland plugins and built-ins like `queryHref` (a default plugin
5
+ * that lowers to `bf_query`, #2042). Every node funnels through
6
+ * `renderLoweringNode`, which maps a logical helper id to its Go helper; the
7
+ * adapter carries no per-API recognition branch of its own.
8
+ */
9
+
10
+ import {
11
+ type LoweringNode,
12
+ type ParsedExpr,
13
+ parseExpression,
14
+ stringifyParsedExpr,
15
+ } from '@barefootjs/jsx'
16
+
17
+ import type { GoEmitContext } from '../emit-context.ts'
18
+ import { wrapIfMultiToken } from '../lib/go-emit.ts'
19
+
20
+ /** Logical helper id → Go template helper name. */
21
+ const GO_HELPER_NAMES: Record<string, string> = { query: 'bf_query' }
22
+
23
+ const BOOL_COMPARISON_OPS: ReadonlySet<string> = new Set([
24
+ '==', '===', '!=', '!==', '<', '>', '<=', '>=',
25
+ ])
26
+
27
+ /**
28
+ * Lower an expression to a Go *bool* for `bf_query`'s `include` argument. A
29
+ * comparison / negation / bool-literal already yields a bool
30
+ * (`convertConditionToGo`); anything else is JS string-truthiness, lowered to
31
+ * `ne <value> ""`. The arg must be a real bool — `bf_query` type-asserts it, so
32
+ * Go-template truthiness (`{{if x}}`) is not enough.
33
+ *
34
+ * The `ne <value> ""` path models *string* truthiness, which is the query
35
+ * domain: `queryHref` values are strings (`QueryParamValue`). A guard on a
36
+ * non-string value is outside the supported surface; the type-assert on
37
+ * `bf_query`'s include arg surfaces it as a build error rather than silently
38
+ * mis-including. (#2041 review.)
39
+ */
40
+ function lowerUrlGuard(ctx: GoEmitContext, g: ParsedExpr): string {
41
+ // Comparisons, `!x`, and bool literals lower to a Go bool via the condition
42
+ // lowering. `&&` / `||` do NOT qualify: Go's `and`/`or` return one of their
43
+ // operands (a string for a truthiness guard like `tag && other`), not a
44
+ // bool — so they take the truthiness-wrap path below, yielding
45
+ // `ne (and …) ""`, an actual bool.
46
+ const isBoolShape =
47
+ (g.kind === 'binary' && BOOL_COMPARISON_OPS.has(g.op)) ||
48
+ (g.kind === 'unary' && g.op === '!') ||
49
+ (g.kind === 'literal' && g.literalType === 'boolean')
50
+ if (isBoolShape) {
51
+ return ctx.convertConditionToGo(stringifyParsedExpr(g), g).condition
52
+ }
53
+ const valueGo = wrapIfMultiToken(ctx.convertExpressionToGo(stringifyParsedExpr(g), undefined, g))
54
+ return `ne ${valueGo} ""`
55
+ }
56
+
57
+ /**
58
+ * Lower a helper call to a Go template expression, or null when no registered
59
+ * plugin recognises it (→ the generic lowering). Matchers — including the
60
+ * built-in `queryHref` plugin (#2042) — are bound to the component metadata at
61
+ * init (`ctx.state.loweringMatchers`), so this is one uniform loop with no
62
+ * per-API branch.
63
+ *
64
+ * Cheap-out: no active matchers → null; else reuse `preParsed` when it's already
65
+ * a call, otherwise a regex pre-filter gates the parse so non-call expressions
66
+ * never pay for `parseExpression`.
67
+ */
68
+ export function lowerRegisteredCall(
69
+ ctx: GoEmitContext,
70
+ jsExpr: string,
71
+ preParsed?: ParsedExpr,
72
+ ): string | null {
73
+ const matchers = ctx.state.loweringMatchers
74
+ if (matchers.length === 0) return null
75
+
76
+ let call: ParsedExpr | undefined = preParsed?.kind === 'call' ? preParsed : undefined
77
+ if (!call) {
78
+ if (!/^\s*[A-Za-z_$][\w$]*\s*\(/.test(jsExpr)) return null
79
+ const parsed = parseExpression(jsExpr)
80
+ if (parsed.kind !== 'call') return null
81
+ call = parsed
82
+ }
83
+
84
+ for (const matcher of matchers) {
85
+ const node = matcher(call.callee, call.args)
86
+ if (!node) continue
87
+ const rendered = renderLoweringNode(ctx, node)
88
+ if (rendered !== null) return rendered
89
+ }
90
+ return null
91
+ }
92
+
93
+ /**
94
+ * Render a backend-neutral lowering node to a Go template expression, or null
95
+ * when the node's `helper` has no Go mapping — the caller then declines
96
+ * (generic lowering → BF101), rather than emitting the raw helper id, which
97
+ * would be invalid Go. Adapters must switch on `helper`; an unknown one is not
98
+ * rendered.
99
+ */
100
+ function renderLoweringNode(ctx: GoEmitContext, node: LoweringNode): string | null {
101
+ const helper = GO_HELPER_NAMES[node.helper]
102
+ if (!helper) return null
103
+ const lowerExpr = (n: ParsedExpr): string =>
104
+ ctx.convertExpressionToGo(stringifyParsedExpr(n), undefined, n)
105
+ if (node.kind === 'helper-call') {
106
+ const args = node.args.map(a => wrapIfMultiToken(lowerExpr(a)))
107
+ return [helper, ...args].join(' ')
108
+ }
109
+ // guard-list — `queryHref`-shaped. Inclusion mirrors the client exactly, where
110
+ // every param value is a STRING (`QueryParamValue`): bf_query drops an
111
+ // included-but-empty value and appends array members.
112
+ // - plain `key: v` (guard null) → `(true) "key" v`
113
+ // - conditional `key: cond ? a : <omit>` → `(<cond>) "key" a`, where the
114
+ // non-empty check is done by bf_query, not folded into the guard.
115
+ const parts: string[] = [wrapIfMultiToken(lowerExpr(node.base))]
116
+ for (const t of node.triples) {
117
+ const includeGo = t.guard === null ? 'true' : lowerUrlGuard(ctx, t.guard)
118
+ parts.push(`(${includeGo})`)
119
+ parts.push(JSON.stringify(t.key))
120
+ parts.push(wrapIfMultiToken(lowerExpr(t.value)))
121
+ }
122
+ return `${helper} ${parts.join(' ')}`
123
+ }