@barefootjs/go-template 0.35.0 → 0.35.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.
@@ -130,7 +130,7 @@ import { analyzeBakeableStaticChildLoop, scalarToGoLiteral, type BakedStaticChil
130
130
  import { analyzeBakeableStaticElementLoop } from "./analysis/static-element-loop-bake.ts"
131
131
  import type { GoEmitContext } from "./emit-context.ts"
132
132
  import { inlineLocalHelperCall } from "./expr/helper-inline.ts"
133
- import { lowerRegisteredAttrCall, lowerRegisteredCall, lowerRegisteredCallNode, lowerTernary } from "./expr/url-builder.ts"
133
+ import { lowerRegisteredAttrCall, lowerRegisteredCall, lowerRegisteredCallNode, lowerTemplateLiteralValue, lowerTernary, lowerValueOperand } from "./expr/url-builder.ts"
134
134
  import {
135
135
  convertInitialValue,
136
136
  jsLiteralToGo,
@@ -5180,12 +5180,35 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
5180
5180
  // arithmetic index lowers correctly. A multi-token operand
5181
5181
  // (`bf_add $i 1`) must be parenthesised or Go parses it as extra
5182
5182
  // `bf_get` arguments.
5183
- return `bf_get ${wrapIfMultiToken(emit(object))} ${wrapIfMultiToken(emit(index))}`
5183
+ return `bf_get ${wrapIfMultiToken(this.emitOperand(object, emit))} ${wrapIfMultiToken(this.emitOperand(index, emit))}`
5184
+ }
5185
+
5186
+ /**
5187
+ * A `binary`/`logical`/`unary` operand that feeds a prefix-call Go form
5188
+ * (`bf_mul a b`, `and a b`, `not a`, …), which cannot host a raw `{{…}}`
5189
+ * action the way the generic `emit` dispatcher's `templateLiteral()` would
5190
+ * produce for a template-literal operand (#2863). ONLY a template literal
5191
+ * is special-cased here, folded through `bf_concat_str` (the same door
5192
+ * `lowerValueOperand` uses for a ternary branch / helper-call arg /
5193
+ * query-guard value); every other kind still goes through the caller's own
5194
+ * `emit` closure unchanged. This matters: routing every operand through
5195
+ * `lowerValueOperand`'s `convertExpressionToGo` fallback (rather than only
5196
+ * the one broken kind) would ALSO pick up `convertExpressionToGo`'s own
5197
+ * string-keyed fast paths — e.g. inlining a bare identifier bound to a
5198
+ * local literal const — which `emit`'s AST-walk `identifier()` dispatch
5199
+ * does not do, a real behavioral divergence for that shape (caught by the
5200
+ * #2236 loop-param-shadowing regression pins).
5201
+ */
5202
+ private emitOperand(n: ParsedExpr, emit: (e: ParsedExpr) => string): string {
5203
+ if (n.kind === 'template-literal') {
5204
+ return wrapIfMultiToken(lowerTemplateLiteralValue(this.emitCtx, n.parts))
5205
+ }
5206
+ return emit(n)
5184
5207
  }
5185
5208
 
5186
5209
  binary(op: string, left: ParsedExpr, right: ParsedExpr, emit: (e: ParsedExpr) => string): string {
5187
- const l = emit(left)
5188
- const r = emit(right)
5210
+ const l = this.emitOperand(left, emit)
5211
+ const r = this.emitOperand(right, emit)
5189
5212
  // Every Go form below is a prefix function call (`bf_mul a b`, `gt a b`,
5190
5213
  // `eq a b`), so a COMPOUND operand must be parenthesised or the template
5191
5214
  // parser folds its tokens into the call's argument list — e.g.
@@ -5268,7 +5291,7 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
5268
5291
  }
5269
5292
 
5270
5293
  unary(op: string, argument: ParsedExpr, emit: (e: ParsedExpr) => string): string {
5271
- const arg = emit(argument)
5294
+ const arg = this.emitOperand(argument, emit)
5272
5295
  // `not` is a Go template prefix builtin like `and`/`or` (see `logical()`
5273
5296
  // below) — a multi-token argument (e.g. `or a b`) must be parenthesised
5274
5297
  // or it degrades into extra sibling args of `not` itself (#2758: `not
@@ -5291,8 +5314,8 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
5291
5314
  // `and`/`or`. This makes `searchParams().get(k) ?? d` lower to
5292
5315
  // `or (.SearchParams.Get "sort") "none"` instead of the broken
5293
5316
  // `or .SearchParams.Get "sort" "none"`.
5294
- const wrapLeft = wrapIfMultiToken(emit(left))
5295
- const wrapRight = wrapIfMultiToken(emit(right))
5317
+ const wrapLeft = wrapIfMultiToken(this.emitOperand(left, emit))
5318
+ const wrapRight = wrapIfMultiToken(this.emitOperand(right, emit))
5296
5319
  if (op === '&&') return `and ${wrapLeft} ${wrapRight}`
5297
5320
  // `??` on a nillable prop needs true JS nullish semantics (#2248): Go's
5298
5321
  // `or` is truthiness-based, so `{{or .Label "Default"}}` falls back on a
@@ -5600,8 +5623,16 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
5600
5623
  method: ArrayMethod,
5601
5624
  object: ParsedExpr,
5602
5625
  args: ParsedExpr[],
5603
- emit: (e: ParsedExpr) => string,
5626
+ rawEmit: (e: ParsedExpr) => string,
5604
5627
  ): string {
5628
+ // Every `emit(...)` call below lowers an ARGUMENT position (the
5629
+ // receiver or a `.method(...)` arg) that feeds a prefix-call Go form
5630
+ // (`bf_join (...) ...`, `bf_replace ... ... ...`, …) — shadow the
5631
+ // dispatcher's own `emit` with `emitOperand` (#2863) so a
5632
+ // template-literal receiver/argument (e.g. `items.join(\`${a}-${b}\`)`)
5633
+ // folds through `bf_concat_str` instead of leaking the TEXT-position
5634
+ // `templateLiteral()` form into another action's argument list.
5635
+ const emit = (e: ParsedExpr): string => this.emitOperand(e, rawEmit)
5605
5636
  // `bf_join` etc. are registered in the runtime FuncMap. The exhaustive
5606
5637
  // switch on `method` mirrors the IR-level discriminator — adding a new
5607
5638
  // `ArrayMethod` variant becomes a TS compile error until every adapter
@@ -6682,34 +6713,23 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
6682
6713
  }
6683
6714
  continue
6684
6715
  }
6685
- // A ternary / single-interpolation template literal with STRING-typed
6686
- // branches (`row.on ? "yes" : "no"`, `${row.x}` alone) parses to a
6687
- // ParsedExpr `template-literal` whose sole part is non-string
6688
- // `templateLiteral()` wraps that one dynamic part's already-bare
6689
- // pipeline value (e.g. `(bf_ternary ...)`, #2335) in a bare `{{...}}`
6690
- // shell meant for TEXT-position embedding. A multi-part template
6691
- // literal (mixed literal text and interpolation) has no such reduction
6692
- // and stays refused below. Unwrap the single-part case back to the
6693
- // bare pipeline value this call site (a function ARGUMENT position,
6694
- // not a text position) needs.
6695
- const singlePartTemplateLiteral =
6696
- exprOut.parsed?.kind === 'template-literal' &&
6697
- exprOut.parsed.parts.length === 1 &&
6698
- exprOut.parsed.parts[0].type !== 'string' &&
6699
- go.startsWith('{{') &&
6700
- go.endsWith('}}')
6701
- if (singlePartTemplateLiteral) {
6702
- go = go.slice(2, -2)
6703
- }
6704
- // Skip the fragment re-check for the just-unwrapped single-part case —
6705
- // `kind` is still (accurately) `template-literal`, which would
6706
- // otherwise re-trip `isTemplateFragment`'s `kind === 'template-literal'`
6707
- // branch on the ALREADY-unwrapped bare value.
6708
- if (!singlePartTemplateLiteral && this.isTemplateFragment(go, exprOut.parsed?.kind)) {
6709
- // A genuine multi-part `{{if}}...{{end}}`-shaped fragment can't be a
6710
- // bare pipeline argument — refuse loudly rather than silently
6711
- // dropping the prop (the #2445 bug was exactly a silent drop one
6712
- // level up).
6716
+ // A ternary or template literal (`row.on ? "yes" : "no"`, `` `#${row.id}
6717
+ // ${row.label}` ``) in this ARGUMENT position (a `bf_with_props`/
6718
+ // `bf_reprops` value, not a text position) must lower through the
6719
+ // shared value-operand door (#2863/#2335) `convertExpressionToGo`'s
6720
+ // generic path returns `templateLiteral()`'s TEXT-position mixed
6721
+ // literal-text-plus-`{{…}}`-actions form for a template literal, which
6722
+ // is not a legal bare pipeline argument (multi-part) or needed an
6723
+ // ad-hoc single-part unwrap here (this replaces both). Recompute from
6724
+ // the already-validated `exprOut.parsed` tree rather than trying to
6725
+ // unwrap or refuse the text-position string after the fact.
6726
+ if (exprOut.parsed?.kind === 'template-literal' || exprOut.parsed?.kind === 'conditional') {
6727
+ go = lowerValueOperand(this.emitCtx, exprOut.parsed)
6728
+ } else if (this.isTemplateFragment(go, exprOut.parsed?.kind)) {
6729
+ // A genuine `{{if}}...{{end}}`-shaped (or other `{{`-leading action)
6730
+ // fragment can't be a bare pipeline argument — refuse loudly rather
6731
+ // than silently dropping the prop (the #2445 bug was exactly a
6732
+ // silent drop one level up).
6713
6733
  this.state.errors.push({
6714
6734
  code: 'BF101',
6715
6735
  severity: 'error',
@@ -7164,7 +7184,12 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
7164
7184
  }
7165
7185
 
7166
7186
  case 'template-literal':
7167
- return plain(this.renderParsedExpr(expr))
7187
+ // #2863 follow-up: fold to one Go value (`bf_concat_str` chain) via
7188
+ // the same door as the `conditional` case just above, instead of
7189
+ // `renderParsedExpr`'s TEXT-position `templateLiteral()` form —
7190
+ // which would leak raw `{{…}}` into THIS condition-expression's own
7191
+ // pipeline position (e.g. `(a === \`${x}-${y}\`) ? … : …`).
7192
+ return plain(lowerTemplateLiteralValue(this.emitCtx, expr.parts))
7168
7193
 
7169
7194
  case 'arrow':
7170
7195
  // A standalone arrow has no Go condition form (callbacks reach the
@@ -52,13 +52,16 @@ export const conformancePins: ConformancePins = {
52
52
  'filter-nested-find-predicate': [{ code: 'BF101', severity: 'error', issue: 'https://github.com/piconic-ai/barefootjs/issues/2320' }],
53
53
  'date-method-uncatalogued': [{ code: 'BF021', severity: 'error', issue: 'https://github.com/piconic-ai/barefootjs/issues/2356' }],
54
54
  'rich-prop-client-read': [{ code: 'BF049', severity: 'error', issue: 'https://github.com/piconic-ai/barefootjs/issues/2648' }],
55
- // #2667: a ternary/array LITERALLY WRAPPING JSX at a non-children prop
55
+ // A ternary or array literal LITERALLY WRAPPING JSX at a non-children prop
56
56
  // position (e.g. `header={cond ? <a/> : <b/>}`) is refused ahead of
57
57
  // `adapter.generate()` in the shared jsx-to-ir.ts phase, so it is pinned
58
58
  // identically on every adapter (including Hono) — same reasoning as
59
- // `rich-prop-client-read` above.
60
- 'jsx-element-prop-ternary': [{ code: 'BF021', severity: 'error', issue: 'https://github.com/piconic-ai/barefootjs/issues/2667' }],
61
- 'jsx-element-prop-array': [{ code: 'BF021', severity: 'error', issue: 'https://github.com/piconic-ai/barefootjs/issues/2667' }],
59
+ // `rich-prop-client-read` above. This is the permanent, intended behavior
60
+ // (formerly tracked as #2667, closed): the issue's own acceptance criteria
61
+ // treated a loud refusal as fully resolving the silent-divergence bug, so
62
+ // no open issue tracks further work here.
63
+ 'jsx-element-prop-ternary': [{ code: 'BF021', severity: 'error' }],
64
+ 'jsx-element-prop-array': [{ code: 'BF021', severity: 'error' }],
62
65
  // `jsx-element-prop-fragment-conditional` (#2703) is NOT pinned here — it
63
66
  // renders correctly since `queueDynamicPropDefine` (go-template-adapter.ts)
64
67
  // extended the reserved `children` slot's `bf_with_children`/`bf_tmpl`
@@ -88,11 +91,14 @@ export const conformancePins: ConformancePins = {
88
91
  severity: 'error',
89
92
  issue: 'https://github.com/piconic-ai/barefootjs/issues/2700',
90
93
  }],
91
- // #2771: a reactive primitive invoked through a namespace import
94
+ // A reactive primitive invoked through a namespace import
92
95
  // (`import * as bf from '@barefootjs/client'`, `bf.createSignal(...)`)
93
96
  // that the analyzer's checker-less fast path cannot recognize refuses
94
97
  // loudly (BF013) instead of silently dropping the declaration — fired
95
98
  // in the shared analyzer pass ahead of any adapter's `adapter.generate()`,
96
- // so all nine adapters (including Hono) pin this identically.
97
- 'namespace-import-primitive': [{ code: 'BF013', severity: 'error', issue: 'https://github.com/piconic-ai/barefootjs/issues/2771' }],
99
+ // so all nine adapters (including Hono) pin this identically. A compile
100
+ // that supplies a shared `ts.Program` (e.g. via `@barefootjs/vite`)
101
+ // resolves the primitive normally and never reaches this refusal (formerly
102
+ // tracked as #2771, closed) — no open issue tracks further work.
103
+ 'namespace-import-primitive': [{ code: 'BF013', severity: 'error' }],
98
104
  }