@barefootjs/go-template 0.33.6 → 0.35.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/expr/url-builder.d.ts +53 -1
- package/dist/adapter/expr/url-builder.d.ts.map +1 -1
- package/dist/adapter/go-template-adapter.d.ts.map +1 -1
- package/dist/adapter/index.js +68 -18
- package/dist/adapter/lib/compile-state.d.ts +20 -0
- package/dist/adapter/lib/compile-state.d.ts.map +1 -1
- package/dist/index.js +69 -22
- package/dist/render-divergences.d.ts.map +1 -1
- package/dist/vite.js +213 -70
- package/package.json +5 -5
- package/src/__tests__/go-template-adapter.test.ts +0 -8
- package/src/__tests__/lowering-plugin.test.ts +38 -0
- package/src/__tests__/query-href.test.ts +126 -0
- package/src/adapter/expr/url-builder.ts +114 -8
- package/src/adapter/go-template-adapter.ts +81 -8
- package/src/adapter/lib/compile-state.ts +22 -0
- package/src/render-divergences.ts +1 -6
|
@@ -176,4 +176,130 @@ export function P(props: { base: string; q: Record<string, string> }) {
|
|
|
176
176
|
const { template } = generate(src)
|
|
177
177
|
expect(template).not.toContain('bf_query')
|
|
178
178
|
})
|
|
179
|
+
|
|
180
|
+
// #2743: a `queryHref` value in an ATTRIBUTE position emits the whole
|
|
181
|
+
// attribute via `bf_attr` (template.HTMLAttr) so html/template's
|
|
182
|
+
// contextual URL-context autoescape (keyed off the attribute NAME) never
|
|
183
|
+
// percent-encodes the base — see `lowerRegisteredAttrCall`.
|
|
184
|
+
test('an href-attribute queryHref value routes through bf_attr, not `href="{{...}}"`', () => {
|
|
185
|
+
const src = `
|
|
186
|
+
'use client'
|
|
187
|
+
import { queryHref } from '@barefootjs/client'
|
|
188
|
+
export function P(props: { base: string; tag: string }) {
|
|
189
|
+
return <a href={queryHref(props.base, { tag: props.tag })}>x</a>
|
|
190
|
+
}
|
|
191
|
+
`
|
|
192
|
+
const { template } = generate(src)
|
|
193
|
+
expect(template).toContain('{{bf_attr "href" (bf_query .Base (true) "tag" .Tag)}}')
|
|
194
|
+
expect(template).not.toContain('href="{{')
|
|
195
|
+
})
|
|
196
|
+
|
|
197
|
+
// The route is keyed on the neutral `helper === 'query'` fact, not on the
|
|
198
|
+
// attribute name `href` — any attribute (e.g. `title`) gets the same
|
|
199
|
+
// treatment, since `queryHref` returns a plain string with nothing
|
|
200
|
+
// href-specific about it.
|
|
201
|
+
test('a non-href attribute (title) with a queryHref value also routes through bf_attr', () => {
|
|
202
|
+
const src = `
|
|
203
|
+
'use client'
|
|
204
|
+
import { queryHref } from '@barefootjs/client'
|
|
205
|
+
export function P(props: { base: string; tag: string }) {
|
|
206
|
+
return <a title={queryHref(props.base, { tag: props.tag })}>x</a>
|
|
207
|
+
}
|
|
208
|
+
`
|
|
209
|
+
const { template } = generate(src)
|
|
210
|
+
expect(template).toContain('{{bf_attr "title" (bf_query .Base (true) "tag" .Tag)}}')
|
|
211
|
+
expect(template).not.toContain('title="{{')
|
|
212
|
+
})
|
|
213
|
+
|
|
214
|
+
// Text-position (non-attribute) use is unaffected — `bf_attr` only wraps
|
|
215
|
+
// the whole-attribute case; a queryHref value read as text still lowers
|
|
216
|
+
// to a bare `bf_query` pipeline.
|
|
217
|
+
test('a queryHref value in text position is not wrapped in bf_attr', () => {
|
|
218
|
+
const src = `
|
|
219
|
+
'use client'
|
|
220
|
+
import { queryHref } from '@barefootjs/client'
|
|
221
|
+
export function P(props: { base: string; tag: string }) {
|
|
222
|
+
return <span>{queryHref(props.base, { tag: props.tag })}</span>
|
|
223
|
+
}
|
|
224
|
+
`
|
|
225
|
+
const { template } = generate(src)
|
|
226
|
+
expect(template).toContain('bf_query .Base (true) "tag" .Tag')
|
|
227
|
+
expect(template).not.toContain('bf_attr')
|
|
228
|
+
})
|
|
229
|
+
|
|
230
|
+
// #2743 follow-up (pullfrog review on #2841): a ternary attribute value
|
|
231
|
+
// with a real (non-`undefined`) alternate is syntactically valid and
|
|
232
|
+
// already lowers both branches correctly via `bf_ternary` — but was still
|
|
233
|
+
// wrapped in the ordinary `name="{{...}}"` form, leaving it exposed to
|
|
234
|
+
// html/template's URL-context percent-encoding. This must route through
|
|
235
|
+
// `bf_attr` too, wrapping the whole `bf_ternary` pipeline.
|
|
236
|
+
test('a queryHref value in a ternary branch (non-undefined alternate) routes through bf_attr', () => {
|
|
237
|
+
const src = `
|
|
238
|
+
'use client'
|
|
239
|
+
import { queryHref } from '@barefootjs/client'
|
|
240
|
+
export function P(props: { ok: boolean; base: string; tag: string }) {
|
|
241
|
+
return <a href={props.ok ? queryHref(props.base, { tag: props.tag }) : '/fallback'}>x</a>
|
|
242
|
+
}
|
|
243
|
+
`
|
|
244
|
+
const { template } = generate(src)
|
|
245
|
+
expect(template).toContain(
|
|
246
|
+
'{{bf_attr "href" (bf_ternary (bf_truthy .Ok) (bf_query .Base (true) "tag" .Tag) "/fallback")}}',
|
|
247
|
+
)
|
|
248
|
+
expect(template).not.toContain('href="{{')
|
|
249
|
+
})
|
|
250
|
+
|
|
251
|
+
// #2842: the `undefined`-alternate omission shape used to render only the
|
|
252
|
+
// consequent via a registry-blind path, emitting invalid Go syntax
|
|
253
|
+
// (`.QueryHref .Base bf_map "tag" .Tag`) with no diagnostic. The consequent
|
|
254
|
+
// now routes through `lowerRegisteredAttrCall` (the same whole-attribute
|
|
255
|
+
// `bf_attr` bypass the direct-call and non-undefined-ternary shapes use),
|
|
256
|
+
// inside the `{{if}}` that implements the omission.
|
|
257
|
+
test('the undefined-alternate omission shape routes the consequent through bf_attr inside the {{if}} (#2842)', () => {
|
|
258
|
+
const src = `
|
|
259
|
+
'use client'
|
|
260
|
+
import { queryHref } from '@barefootjs/client'
|
|
261
|
+
export function P(props: { ok: boolean; base: string; tag: string }) {
|
|
262
|
+
return <a href={props.ok ? queryHref(props.base, { tag: props.tag }) : undefined}>x</a>
|
|
263
|
+
}
|
|
264
|
+
`
|
|
265
|
+
const { template } = generate(src)
|
|
266
|
+
expect(template).toContain('{{if .Ok}}{{bf_attr "href" (bf_query .Base (true) "tag" .Tag)}}{{end}}')
|
|
267
|
+
expect(template).not.toContain('.QueryHref')
|
|
268
|
+
expect(template).not.toContain('bf_map')
|
|
269
|
+
expect(template).not.toContain('href="{{')
|
|
270
|
+
})
|
|
271
|
+
|
|
272
|
+
// #2842: a registered call nested in a template-literal interpolation
|
|
273
|
+
// (not just a direct attribute value) is registry-lowered too — the fix
|
|
274
|
+
// lives in the shared ParsedExpr `call()` dispatcher, so any nested
|
|
275
|
+
// position benefits, not only the ternary/attribute cases above.
|
|
276
|
+
test('a queryHref call nested in a template-literal interpolation is registry-lowered (#2842)', () => {
|
|
277
|
+
const src = `
|
|
278
|
+
'use client'
|
|
279
|
+
import { queryHref } from '@barefootjs/client'
|
|
280
|
+
export function P(props: { base: string; tag: string }) {
|
|
281
|
+
return <a title={\`pre \${queryHref(props.base, { tag: props.tag })}\`}>x</a>
|
|
282
|
+
}
|
|
283
|
+
`
|
|
284
|
+
const { template } = generate(src)
|
|
285
|
+
expect(template).toContain('title="pre {{bf_query .Base (true) "tag" .Tag}}"')
|
|
286
|
+
expect(template).not.toContain('.QueryHref')
|
|
287
|
+
})
|
|
288
|
+
|
|
289
|
+
// #2842: a nested ternary inside the undef-alternate consequent still
|
|
290
|
+
// recurses correctly — `lowerRegisteredAttrCall`'s `conditional` arm
|
|
291
|
+
// right-folds, matching `lowerTernary`'s own recursion.
|
|
292
|
+
test('a nested ternary inside the undef-alternate consequent still routes through bf_attr (#2842)', () => {
|
|
293
|
+
const src = `
|
|
294
|
+
'use client'
|
|
295
|
+
import { queryHref } from '@barefootjs/client'
|
|
296
|
+
export function P(props: { a: boolean; b: boolean; base: string; tag: string }) {
|
|
297
|
+
return <a href={props.a ? (props.b ? queryHref(props.base, { tag: props.tag }) : '/x') : undefined}>x</a>
|
|
298
|
+
}
|
|
299
|
+
`
|
|
300
|
+
const { template } = generate(src)
|
|
301
|
+
expect(template).toContain(
|
|
302
|
+
'{{if .A}}{{bf_attr "href" (bf_ternary (bf_truthy .B) (bf_query .Base (true) "tag" .Tag) "/x")}}{{end}}',
|
|
303
|
+
)
|
|
304
|
+
})
|
|
179
305
|
})
|
|
@@ -128,6 +128,44 @@ function lowerTernaryTest(ctx: GoEmitContext, test: ParsedExpr): string {
|
|
|
128
128
|
return isBoolShape ? go : `(bf_truthy ${go})`
|
|
129
129
|
}
|
|
130
130
|
|
|
131
|
+
/**
|
|
132
|
+
* First registered matcher that recognises `callee(args)`, or null (#2842).
|
|
133
|
+
* The single registry consultation every Go lowering path shares: the
|
|
134
|
+
* top-level `lowerRegisteredCall` early return, the ParsedExpr `call()`
|
|
135
|
+
* dispatcher (`go-template-adapter.ts`, so a registered call is recognised no
|
|
136
|
+
* matter how deep it sits in an expression tree — a ternary branch, a
|
|
137
|
+
* template-literal interpolation, a binary operand — not only when the call
|
|
138
|
+
* IS the whole expression), and the attribute-position `bf_attr` route all
|
|
139
|
+
* ask this one question. First-match-wins: a malformed helper id from one
|
|
140
|
+
* plugin must not be silently masked by falling through to another.
|
|
141
|
+
*/
|
|
142
|
+
export function matchRegisteredCall(
|
|
143
|
+
ctx: GoEmitContext,
|
|
144
|
+
callee: ParsedExpr,
|
|
145
|
+
args: readonly ParsedExpr[],
|
|
146
|
+
): LoweringNode | null {
|
|
147
|
+
for (const matcher of ctx.state.loweringMatchers) {
|
|
148
|
+
const node = matcher(callee, args)
|
|
149
|
+
if (node) return node
|
|
150
|
+
}
|
|
151
|
+
return null
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Render a recognised call's neutral node to its Go pipeline (unparenthesised
|
|
156
|
+
* — same contract as `lowerRegisteredCall`), or null when no plugin matches
|
|
157
|
+
* or the matched helper has no Go mapping.
|
|
158
|
+
*/
|
|
159
|
+
export function lowerRegisteredCallNode(
|
|
160
|
+
ctx: GoEmitContext,
|
|
161
|
+
callee: ParsedExpr,
|
|
162
|
+
args: readonly ParsedExpr[],
|
|
163
|
+
): string | null {
|
|
164
|
+
if (ctx.state.loweringMatchers.length === 0) return null
|
|
165
|
+
const node = matchRegisteredCall(ctx, callee, args)
|
|
166
|
+
return node ? renderLoweringNode(ctx, node) : null
|
|
167
|
+
}
|
|
168
|
+
|
|
131
169
|
/**
|
|
132
170
|
* Lower a helper call to a Go template expression, or null when no registered
|
|
133
171
|
* plugin recognises it (→ the generic lowering). Matchers — including the
|
|
@@ -144,8 +182,7 @@ export function lowerRegisteredCall(
|
|
|
144
182
|
jsExpr: string,
|
|
145
183
|
preParsed?: ParsedExpr,
|
|
146
184
|
): string | null {
|
|
147
|
-
|
|
148
|
-
if (matchers.length === 0) return null
|
|
185
|
+
if (ctx.state.loweringMatchers.length === 0) return null
|
|
149
186
|
|
|
150
187
|
let call: ParsedExpr | undefined = preParsed?.kind === 'call' ? preParsed : undefined
|
|
151
188
|
if (!call) {
|
|
@@ -155,13 +192,82 @@ export function lowerRegisteredCall(
|
|
|
155
192
|
call = parsed
|
|
156
193
|
}
|
|
157
194
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
195
|
+
return lowerRegisteredCallNode(ctx, call.callee, call.args)
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Attribute-position twin of `lowerRegisteredCall` (#2743). When an intrinsic
|
|
200
|
+
* element attribute's value is a `query` guard-list (the neutral node the
|
|
201
|
+
* `queryHref` plugin — or any plugin reusing the `query` helper — produces),
|
|
202
|
+
* emit the WHOLE attribute as one `bf_attr` action returning
|
|
203
|
+
* `template.HTMLAttr`, instead of `name="{{bf_query …}}"`. html/template
|
|
204
|
+
* infers a URL context from the attribute NAME (`href`, `src`, `action`,
|
|
205
|
+
* `data-*`, anything containing `src`/`uri`/`url`, …) and percent-encodes the
|
|
206
|
+
* entire value there; the JS reference (Hono) only HTML-escapes. Keyed on the
|
|
207
|
+
* neutral `helper` id, not the attribute name (Go's URL-context table is a
|
|
208
|
+
* heuristic we must not re-implement) and not the JS API name. Returns null
|
|
209
|
+
* for a non-call, an unmatched call, or any other node kind/helper — the
|
|
210
|
+
* caller then takes its ordinary path.
|
|
211
|
+
*
|
|
212
|
+
* Also accepts a `conditional` node directly (#2743 follow-up, pullfrog
|
|
213
|
+
* review on #2841): a ternary with a real, non-`undefined` alternate
|
|
214
|
+
* (`cond ? queryHref(...) : '/fallback'`) is syntactically valid and already
|
|
215
|
+
* lowers its branches correctly via `lowerTernary` — but the CALLER used to
|
|
216
|
+
* still wrap the resulting `(bf_ternary …)` pipeline in the ordinary
|
|
217
|
+
* `name="{{...}}"` form, which leaves it exposed to the same URL-context
|
|
218
|
+
* percent-encoding this function exists to route around. Whenever EITHER
|
|
219
|
+
* branch (recursing through a right-folded nested ternary chain, the same
|
|
220
|
+
* shape `lowerTernary` itself right-folds) is a `query` guard-list call, the
|
|
221
|
+
* whole ternary is wrapped in `bf_attr` too — matching the reference, which
|
|
222
|
+
* HTML-escapes the picked branch's value the same way regardless of which
|
|
223
|
+
* branch won.
|
|
224
|
+
*
|
|
225
|
+
* The caller passes the CONSEQUENT ALONE (not the full ternary) for the
|
|
226
|
+
* `undefined`-alternate omission shape (`cond ? queryHref(...) : undefined`,
|
|
227
|
+
* #2842) — omission needs the `{{if}}` wrapper that shape's caller already
|
|
228
|
+
* builds; a `bf_ternary … ""` inside `bf_attr` can't express it (`Attr`
|
|
229
|
+
* renders `name=""` for an empty value, not attribute absence).
|
|
230
|
+
*/
|
|
231
|
+
export function lowerRegisteredAttrCall(
|
|
232
|
+
ctx: GoEmitContext,
|
|
233
|
+
attrName: string,
|
|
234
|
+
parsed: ParsedExpr,
|
|
235
|
+
): string | null {
|
|
236
|
+
if (parsed.kind === 'conditional') {
|
|
237
|
+
if (!ternaryHasQueryBranch(ctx, parsed.consequent, parsed.alternate)) return null
|
|
238
|
+
// `lowerTernary` already returns a complete parenthesised `(bf_ternary …)`
|
|
239
|
+
// pipeline — pass it straight through as `bf_attr`'s value argument
|
|
240
|
+
// rather than re-wrapping it in a redundant second set of parens.
|
|
241
|
+
const rendered = lowerTernary(ctx, parsed.test, parsed.consequent, parsed.alternate)
|
|
242
|
+
return `{{bf_attr ${JSON.stringify(attrName)} ${rendered}}}`
|
|
163
243
|
}
|
|
164
|
-
return null
|
|
244
|
+
if (parsed.kind !== 'call') return null
|
|
245
|
+
const node = matchRegisteredCall(ctx, parsed.callee, parsed.args)
|
|
246
|
+
if (!node || node.kind !== 'guard-list' || node.helper !== 'query') return null
|
|
247
|
+
const rendered = renderLoweringNode(ctx, node)
|
|
248
|
+
return rendered === null ? null : `{{bf_attr ${JSON.stringify(attrName)} (${rendered})}}`
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/** Whether a `call` node is a recognised `query` guard-list lowering. */
|
|
252
|
+
function isQueryGuardListCall(ctx: GoEmitContext, node: ParsedExpr): boolean {
|
|
253
|
+
if (node.kind !== 'call') return false
|
|
254
|
+
const lowered = matchRegisteredCall(ctx, node.callee, node.args)
|
|
255
|
+
return lowered?.kind === 'guard-list' && lowered.helper === 'query'
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Whether either branch of a ternary is (or, for a right-folded chain,
|
|
260
|
+
* eventually resolves to) a `query` guard-list call — the trigger for
|
|
261
|
+
* routing the whole ternary through `bf_attr` in `lowerRegisteredAttrCall`.
|
|
262
|
+
*/
|
|
263
|
+
function ternaryHasQueryBranch(
|
|
264
|
+
ctx: GoEmitContext,
|
|
265
|
+
consequent: ParsedExpr,
|
|
266
|
+
alternate: ParsedExpr,
|
|
267
|
+
): boolean {
|
|
268
|
+
const branchHasQuery = (n: ParsedExpr): boolean =>
|
|
269
|
+
n.kind === 'conditional' ? ternaryHasQueryBranch(ctx, n.consequent, n.alternate) : isQueryGuardListCall(ctx, n)
|
|
270
|
+
return branchHasQuery(consequent) || branchHasQuery(alternate)
|
|
165
271
|
}
|
|
166
272
|
|
|
167
273
|
/**
|
|
@@ -80,6 +80,9 @@ import {
|
|
|
80
80
|
evaluateStaticLiteral,
|
|
81
81
|
BindingScope,
|
|
82
82
|
buildImportAliasMap,
|
|
83
|
+
resolveGetterAliases,
|
|
84
|
+
collectAliasableGetterNames,
|
|
85
|
+
resolveBodyDestructuredPropAliases,
|
|
83
86
|
} from '@barefootjs/jsx'
|
|
84
87
|
import { findInterpolationEnd } from '@barefootjs/jsx/scanner'
|
|
85
88
|
import { BF_REGION, escapeHtml, resolveJsxChildrenProp } from '@barefootjs/shared'
|
|
@@ -127,7 +130,7 @@ import { analyzeBakeableStaticChildLoop, scalarToGoLiteral, type BakedStaticChil
|
|
|
127
130
|
import { analyzeBakeableStaticElementLoop } from "./analysis/static-element-loop-bake.ts"
|
|
128
131
|
import type { GoEmitContext } from "./emit-context.ts"
|
|
129
132
|
import { inlineLocalHelperCall } from "./expr/helper-inline.ts"
|
|
130
|
-
import { lowerRegisteredCall, lowerTernary } from "./expr/url-builder.ts"
|
|
133
|
+
import { lowerRegisteredAttrCall, lowerRegisteredCall, lowerRegisteredCallNode, lowerTernary } from "./expr/url-builder.ts"
|
|
131
134
|
import {
|
|
132
135
|
convertInitialValue,
|
|
133
136
|
jsLiteralToGo,
|
|
@@ -465,6 +468,23 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
465
468
|
)
|
|
466
469
|
this.state.moduleStringConsts = this.collectModuleStringConsts(ir.metadata.localConstants)
|
|
467
470
|
this.state.localConstants = ir.metadata.localConstants ?? []
|
|
471
|
+
// #2813: precompute which local consts are bare alias-hop chains onto a
|
|
472
|
+
// signal/memo getter, so `rootFieldRef` can route a call through the
|
|
473
|
+
// getter's OWN field instead of registering a separate, never-seeded
|
|
474
|
+
// field under the alias's name. Env-signal getters are excluded — they
|
|
475
|
+
// resolve through `searchParamsFieldRef`, not a seeded root field.
|
|
476
|
+
{
|
|
477
|
+
const getterNames = collectAliasableGetterNames(ir.metadata.signals ?? [], ir.metadata.memos ?? [])
|
|
478
|
+
this.state.getterAliases = resolveGetterAliases(ir.metadata.localConstants ?? [], (n) => getterNames.has(n))
|
|
479
|
+
}
|
|
480
|
+
// #2788: bare-props-form body destructure aliases (`const { children:
|
|
481
|
+
// kids } = props`) — same alias-resolution need as `getterAliases`
|
|
482
|
+
// above, different terminal set (a prop key here, a signal/memo
|
|
483
|
+
// getter there).
|
|
484
|
+
this.state.propDestructureAliases = resolveBodyDestructuredPropAliases(
|
|
485
|
+
ir.metadata.localConstants ?? [],
|
|
486
|
+
ir.metadata.propsObjectName,
|
|
487
|
+
)
|
|
468
488
|
// #2208 fable review: every name a `.map()`/`.filter()` loop callback
|
|
469
489
|
// binds as its item/index parameter anywhere in the component. Static
|
|
470
490
|
// loop-source resolution (`getBakedStaticChildLoop` /
|
|
@@ -4846,9 +4866,17 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
4846
4866
|
* rebinds. Outside any loop the root *is* the dot, so we emit `.Field`.
|
|
4847
4867
|
*/
|
|
4848
4868
|
private rootFieldRef(name: string): string {
|
|
4849
|
-
|
|
4869
|
+
// #2813 / #2788: `name` may be a bare local-const alias of a
|
|
4870
|
+
// signal/memo getter (`const items__alias = items`) or a bare-props-
|
|
4871
|
+
// form body destructure's renamed prop (`const { children: kids } =
|
|
4872
|
+
// props`) — resolve it to the ALIASED entity's own name FIRST, so the
|
|
4873
|
+
// emitted field matches what that entity itself seeds (`.Items`,
|
|
4874
|
+
// `.Children`) instead of registering a second, never-populated field
|
|
4875
|
+
// under the local alias (`.Items__alias`, `.Kids`).
|
|
4876
|
+
const resolved = this.state.getterAliases.get(name) ?? this.state.propDestructureAliases.get(name) ?? name
|
|
4877
|
+
this.state.templateReadRootFields.add(resolved)
|
|
4850
4878
|
const prefix = this.inLoop ? '$.' : '.'
|
|
4851
|
-
return `${prefix}${capitalizeFieldName(
|
|
4879
|
+
return `${prefix}${capitalizeFieldName(resolved)}`
|
|
4852
4880
|
}
|
|
4853
4881
|
|
|
4854
4882
|
/**
|
|
@@ -4956,6 +4984,15 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
4956
4984
|
}
|
|
4957
4985
|
|
|
4958
4986
|
call(callee: ParsedExpr, args: ParsedExpr[], emit: (e: ParsedExpr) => string): string {
|
|
4987
|
+
// #2842: a registered lowering (the built-in `queryHref`, or any userland
|
|
4988
|
+
// plugin) must win in EVERY position this shared dispatcher reaches — a
|
|
4989
|
+
// ternary branch, a template-literal interpolation, a binary operand —
|
|
4990
|
+
// not only when the call is the whole expression (`convertExpressionToGo`'s
|
|
4991
|
+
// own early return). Same precedence as that top-level path, where the
|
|
4992
|
+
// registry is consulted before signal-read resolution and template
|
|
4993
|
+
// primitives.
|
|
4994
|
+
const lowered = lowerRegisteredCallNode(this.emitCtx, callee, args)
|
|
4995
|
+
if (lowered !== null) return lowered
|
|
4959
4996
|
// Signal call: count() -> .Count (or $.Count inside a loop). An env-signal
|
|
4960
4997
|
// binding (`searchParams()`, or an aliased `sp()`) resolves to the canonical
|
|
4961
4998
|
// `.SearchParams` field regardless of the JS name.
|
|
@@ -5232,7 +5269,11 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
5232
5269
|
|
|
5233
5270
|
unary(op: string, argument: ParsedExpr, emit: (e: ParsedExpr) => string): string {
|
|
5234
5271
|
const arg = emit(argument)
|
|
5235
|
-
|
|
5272
|
+
// `not` is a Go template prefix builtin like `and`/`or` (see `logical()`
|
|
5273
|
+
// below) — a multi-token argument (e.g. `or a b`) must be parenthesised
|
|
5274
|
+
// or it degrades into extra sibling args of `not` itself (#2758: `not
|
|
5275
|
+
// or a b` parses as `not` applied to 3 args, not 1).
|
|
5276
|
+
if (op === '!') return `not ${wrapIfMultiToken(arg)}`
|
|
5236
5277
|
if (op === '-') return `bf_neg ${arg}`
|
|
5237
5278
|
return arg
|
|
5238
5279
|
}
|
|
@@ -7074,7 +7115,11 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
7074
7115
|
|
|
7075
7116
|
case 'unary': {
|
|
7076
7117
|
const arg = this.renderConditionExpr(expr.argument)
|
|
7077
|
-
|
|
7118
|
+
// Same prefix-builtin wrapping rule as the `logical` case just below
|
|
7119
|
+
// (and the main `unary()` emitter above): `not` needs its multi-token
|
|
7120
|
+
// argument parenthesised, or e.g. `not or a b` parses as `not`
|
|
7121
|
+
// applied to 3 sibling args instead of 1 (#2758).
|
|
7122
|
+
if (expr.op === '!') return { preamble: arg.preamble, expr: `not ${wrapIfMultiToken(arg.expr)}` }
|
|
7078
7123
|
if (expr.op === '-') return { preamble: arg.preamble, expr: `bf_neg ${arg.expr}` }
|
|
7079
7124
|
return arg
|
|
7080
7125
|
}
|
|
@@ -7687,7 +7732,9 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
7687
7732
|
// `bfComment` prepends `bf-`, so `printf "loop-i:%v"` yields
|
|
7688
7733
|
// `<!--bf-loop-i:KEY-->`. The key expression resolves against the current
|
|
7689
7734
|
// range item (`.` context), matching `data-key`'s emission.
|
|
7690
|
-
|
|
7735
|
+
// `bfEscapeCommentKey` (#2795 follow-up) neutralizes `-` so a key can't
|
|
7736
|
+
// spell `-->` and close the comment early — see its doc in `bf.go`.
|
|
7737
|
+
return `{{bfComment (printf "loop-i:%v" (bfEscapeCommentKey ${this.convertExpressionToGo(loop.key)}))}}`
|
|
7691
7738
|
}
|
|
7692
7739
|
return ''
|
|
7693
7740
|
}
|
|
@@ -8021,10 +8068,30 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
8021
8068
|
? value.expr
|
|
8022
8069
|
: value.expr.slice(0, value.expr.indexOf('?')).trim(),
|
|
8023
8070
|
)
|
|
8024
|
-
|
|
8025
|
-
|
|
8071
|
+
// #2842 / #2743: a `query` guard-list consequent (queryHref) takes
|
|
8072
|
+
// the whole-attribute `bf_attr` route INSIDE the `{{if}}`, so
|
|
8073
|
+
// html/template's URL-context inference never percent-encodes it —
|
|
8074
|
+
// the same route the direct-call and non-undefined-ternary shapes
|
|
8075
|
+
// take. The consequent is passed alone (not the ternary): omission
|
|
8076
|
+
// needs this `{{if}}` wrapper, which `bf_attr` can't express on its
|
|
8077
|
+
// own. Any other consequent (a `helper-call` plugin, an unmatched
|
|
8078
|
+
// call, a member/literal) keeps the ordinary `name="{{…}}"` form —
|
|
8079
|
+
// `call()` now consults the registry too, so a plugin call renders
|
|
8080
|
+
// its `bf_<helper>` pipeline there instead of invalid Go syntax.
|
|
8081
|
+
const attrConsequent = lowerRegisteredAttrCall(this.emitCtx, name, parsed.consequent)
|
|
8082
|
+
const body = attrConsequent !== null
|
|
8083
|
+
? attrConsequent
|
|
8084
|
+
: `${name}="{{${this.renderParsedExpr(parsed.consequent)}}}"`
|
|
8026
8085
|
return `${preamble}{{if ${goCond}}}${body}{{end}}`
|
|
8027
8086
|
}
|
|
8087
|
+
// #2743 follow-up (pullfrog review on #2841): a `query` guard-list
|
|
8088
|
+
// value (queryHref) reachable through EITHER branch of this ternary
|
|
8089
|
+
// still needs the whole-attribute `bf_attr` route — otherwise the
|
|
8090
|
+
// pipeline below lands inside the ordinary `name="{{...}}"` wrapper
|
|
8091
|
+
// and html/template's URL-context inference still percent-encodes
|
|
8092
|
+
// whichever branch wins at render time. See `lowerRegisteredAttrCall`.
|
|
8093
|
+
const attrTernary = lowerRegisteredAttrCall(this.emitCtx, name, parsed)
|
|
8094
|
+
if (attrTernary !== null) return attrTernary
|
|
8028
8095
|
// #2335: the ternary lowers to the pipeline-position `(bf_ternary …)`
|
|
8029
8096
|
// value (no longer a `{{if}}…{{end}}` fragment), so wrap it in a single
|
|
8030
8097
|
// `{{…}}` action inside the attribute string — `name="{{bf_ternary …}}"`.
|
|
@@ -8034,6 +8101,12 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
8034
8101
|
// Inline Go template syntax with embedded `{{...}}` actions.
|
|
8035
8102
|
return `${name}="${this.renderParsedExpr(parsed)}"`
|
|
8036
8103
|
}
|
|
8104
|
+
// #2743: a `query` guard-list value (queryHref) emits the WHOLE
|
|
8105
|
+
// attribute via `bf_attr` (template.HTMLAttr) so html/template's
|
|
8106
|
+
// URL-context inference on the attribute name never percent-encodes
|
|
8107
|
+
// the base. See `lowerRegisteredAttrCall`.
|
|
8108
|
+
const attrAction = lowerRegisteredAttrCall(this.emitCtx, name, parsed)
|
|
8109
|
+
if (attrAction !== null) return attrAction
|
|
8037
8110
|
// Nullish-attribute omission: when the attribute value is a BARE reference
|
|
8038
8111
|
// to a nillable (`interface{}`) prop field, guard emission on `ne .X nil`
|
|
8039
8112
|
// so an unset optional prop drops the attribute entirely instead of
|
|
@@ -88,6 +88,28 @@ export class CompileState {
|
|
|
88
88
|
*/
|
|
89
89
|
localConstants: IRMetadata['localConstants'] = []
|
|
90
90
|
|
|
91
|
+
/**
|
|
92
|
+
* Bare local-const alias-hop chains that ultimately name a signal/memo
|
|
93
|
+
* getter (`const items__alias = items` → `items__alias` -> `items`,
|
|
94
|
+
* #2813) — the SSR-side twin of #2778's CSR-template fix. `rootFieldRef`
|
|
95
|
+
* resolves a name through this map before capitalizing it into a struct
|
|
96
|
+
* field, so `items__alias()` reads `.Items` (the field the getter
|
|
97
|
+
* actually seeds) instead of a phantom, never-seeded `.Items__alias`.
|
|
98
|
+
*/
|
|
99
|
+
getterAliases: Map<string, string> = new Map()
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Local names that are BODY-level destructured aliases of a prop key
|
|
103
|
+
* for a bare-props-form component (`const { children: kids } =
|
|
104
|
+
* props`), #2788 — the SSR-side twin of `aliased-destructured-prop`'s
|
|
105
|
+
* PARAMETER-destructuring case (`{ n: count }`, already handled via
|
|
106
|
+
* `ParamInfo.sourceName`/`capitalizeFieldName(p.name)`). `rootFieldRef`
|
|
107
|
+
* resolves a name through this map too, so `kids` reads the same
|
|
108
|
+
* `.Children` field the prop itself seeds instead of a phantom,
|
|
109
|
+
* never-populated `.Kids`.
|
|
110
|
+
*/
|
|
111
|
+
propDestructureAliases: Map<string, string> = new Map()
|
|
112
|
+
|
|
91
113
|
/**
|
|
92
114
|
* Every name a `.map()`/`.filter()` loop callback binds as its item/index
|
|
93
115
|
* parameter anywhere in the component (#2208 fable review). Consulted by
|
|
@@ -37,9 +37,4 @@
|
|
|
37
37
|
|
|
38
38
|
import type { RenderDivergences } from '@barefootjs/jsx'
|
|
39
39
|
|
|
40
|
-
export const renderDivergences: RenderDivergences = {
|
|
41
|
-
'children-passthrough-renamed':
|
|
42
|
-
'A `children` prop destructured under a different name (`const { children: kids } = props`) does not reach the SSR template on Go, tracked as https://github.com/piconic-ai/barefootjs/issues/2788. The same fixture also fails on Mojolicious, where the mechanism IS isolated: the `.html.ep` interpolates the LOCAL alias (`$kids`) while the stash defines only the caller-facing `children`, so the Perl render dies inside `Mojo::Template::process`. Go\'s own failure mode has NOT been read — `go` is not reachable from the local test process (the conformance case prints "go command not found" and skips), so this entry is declared from the CI failure on #2787 alone, not from a local reproduction. Whoever graduates this should read Go\'s actual output first rather than assume it shares Mojo\'s mechanism. Same alias family as `aliased-destructured-prop` (`{ n: count }`), whose Go half graduated in #2525 — worth checking whether the reserved `children` slot bypasses that fix or never had it. `children-passthrough-renamed` asserts the CORRECT (Hono-generated) output, so deleting this entry is the graduation.',
|
|
43
|
-
'aliased-loop-source':
|
|
44
|
-
'A `.map()` loop whose source is a local const alias of a signal getter (`const items__alias = items`) fails template execution on real Go (`can\'t evaluate field Items__alias in type main.AliasedLoopSourceProps`) — the zero-arg-call-to-field lowering routes `items__alias()` to a `.Items__alias` struct field that was never seeded, since seeding only knows about `items`, the real signal name; the alias hop is never resolved. This is the SSR-side twin of #2778 (fixed for the CSR client-JS template in the same PR that added this fixture) — that fix only touches client-JS emission, not Go\'s field-routing/seeding. Tracked at https://github.com/piconic-ai/barefootjs/issues/2813; graduate by resolving the alias hop at field-routing time using the same `resolveAliasOrigin`/`resolveGetterAliases` mechanism #2778 introduced, rather than a third alias-hop walker.',
|
|
45
|
-
}
|
|
40
|
+
export const renderDivergences: RenderDivergences = {}
|