@barefootjs/go-template 0.35.0 → 0.35.2

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,
@@ -262,8 +262,7 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
262
262
  extractPropFallback: (initialValue, preParsed) => this.extractPropFallback(initialValue, preParsed),
263
263
  extractCollisionDerivation: (parsed) => this.extractCollisionDerivation(parsed),
264
264
  resolveModuleStringConst: (name) => this.resolveModuleStringConst(name),
265
- resolveModuleNumericConst: (name) => this.resolveModuleNumericConst(name),
266
- resolveModuleBooleanConst: (name) => this.resolveModuleBooleanConst(name),
265
+ resolveModuleConstAsGo: (name, target) => this.resolveModuleConstAsGo(name, target),
267
266
  }
268
267
 
269
268
  /** Diagnostics from the current compile (backed by `CompileState`); `generate()` also merges these into `ir.errors`. */
@@ -4726,11 +4725,12 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
4726
4725
  }
4727
4726
  const inlined = this.resolveModuleStringConst(name)
4728
4727
  if (inlined !== null) return inlined
4729
- // Module numeric const (e.g. `const TRACK = 8` used in a width expression):
4730
- // inline the literal value rather than emit `{{.TRACK}}` against a Props
4731
- // field that never exists. Mirrors the string-const inlining above.
4732
- const inlinedNum = this.resolveModuleNumericConst(name)
4733
- if (inlinedNum !== null) return inlinedNum
4728
+ // Module scalar const (e.g. `const TRACK = 8` used in a width expression,
4729
+ // or `const OPEN = true`): inline the literal value rather than emit
4730
+ // `{{.TRACK}}` against a Props field that never exists. Mirrors the
4731
+ // string-const inlining above.
4732
+ const inlinedScalar = this.resolveModuleConstAsGo(name, { kind: 'template-action' })
4733
+ if (inlinedScalar !== null) return inlinedScalar
4734
4734
  if (this.isCurrentLoopItem(name)) return '.'
4735
4735
  // An *outer* loop's value variable (we're in a nested loop) is in scope as
4736
4736
  // the Go range variable `$name` declared by that loop's `{{range … := …}}`;
@@ -4866,19 +4866,41 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
4866
4866
  * rebinds. Outside any loop the root *is* the dot, so we emit `.Field`.
4867
4867
  */
4868
4868
  private rootFieldRef(name: string): string {
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
4869
+ const resolved = this.resolveRootFieldAlias(name)
4877
4870
  this.state.templateReadRootFields.add(resolved)
4878
4871
  const prefix = this.inLoop ? '$.' : '.'
4879
4872
  return `${prefix}${capitalizeFieldName(resolved)}`
4880
4873
  }
4881
4874
 
4875
+ /**
4876
+ * #2813 / #2788: `name` may be a bare local-const alias of a signal/memo
4877
+ * getter (`const items__alias = items`) or a bare-props-form body
4878
+ * destructure's renamed prop (`const { children: kids } = props`) —
4879
+ * resolve it to the ALIASED entity's own name, so a caller that needs to
4880
+ * build its own prefixed field reference (e.g. `renderFilterExprNode`,
4881
+ * which must hardcode `$.` regardless of `rootFieldRef`'s
4882
+ * `this.inLoop`-conditional prefix — #2857) still lands on the field that
4883
+ * entity itself seeds (`.Items`, `.Children`) instead of a never-populated
4884
+ * field under the local alias (`.Items__alias`, `.Kids`). `rootFieldRef`
4885
+ * itself is built on top of this so there is exactly one resolution.
4886
+ */
4887
+ private resolveRootFieldAlias(name: string): string {
4888
+ return this.state.getterAliases.get(name) ?? this.state.propDestructureAliases.get(name) ?? name
4889
+ }
4890
+
4891
+ /**
4892
+ * Root-scope field reference from INSIDE a `.filter()`/`.find()` predicate
4893
+ * (#2857, #2879). Same registration + alias resolution as `rootFieldRef`,
4894
+ * but the `$.` prefix is unconditional: a filter predicate's `{{if}}`
4895
+ * always sits inside its loop's own `{{range}}` (`renderLoop` restores
4896
+ * `this.inLoop` before rendering it), so a bare `.Field` would resolve
4897
+ * against the current row's type instead of root.
4898
+ */
4899
+ private filterRootFieldRef(name: string): string {
4900
+ this.rootFieldRef(name)
4901
+ return `$.${capitalizeFieldName(this.resolveRootFieldAlias(name))}`
4902
+ }
4903
+
4882
4904
  /**
4883
4905
  * When `name` is a local binding of the `searchParams()` env signal, resolve
4884
4906
  * it to the canonical `.SearchParams` field — not `.<Capitalized name>` — so
@@ -4926,13 +4948,10 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
4926
4948
  }
4927
4949
 
4928
4950
  /**
4929
- * The single module-const lookup shared by `resolveModuleNumericConst`
4930
- * and `resolveModuleBooleanConst` they differ only in which literal
4931
- * SHAPE they accept from the same "plain module-level const" search, not
4932
- * in how that search is performed. A second inline lookup per resolver
4933
- * would grow `binding-scope-ratchet.test.ts`'s shrink-only floor for this
4934
- * file (already at 5) for a shape variance the callers can express
4935
- * themselves instead.
4951
+ * The single module-const lookup shared by `resolveModuleConstAsGo`
4952
+ * kept as its own method (rather than inlined) so a second `.find(` isn't
4953
+ * added elsewhere, growing `binding-scope-ratchet.test.ts`'s shrink-only
4954
+ * floor for this file (already at 5).
4936
4955
  */
4937
4956
  private findModuleConst(name: string): ConstantInfo | undefined {
4938
4957
  return this.state.localConstants.find(
@@ -4941,40 +4960,50 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
4941
4960
  }
4942
4961
 
4943
4962
  /**
4944
- * Inline a module-level numeric const (`const TRACK = 8`) as its literal
4945
- * value. Only a plain numeric initializer qualifies anything computed or
4946
- * non-numeric falls through to the normal field/ident resolution. Scoped to
4947
- * module consts (like the string variant) and guarded against loop vars so a
4948
- * range variable that shadows a const name still wins.
4949
- */
4950
- private resolveModuleNumericConst(name: string): string | null {
4951
- if (this.isCurrentLoopItem(name)) return null
4952
- if (this.loopVarRefCount.has(name)) return null
4953
- if (this.isOuterLoopParam(name)) return null
4954
- const c = this.findModuleConst(name)
4955
- if (!c || c.value === undefined) return null
4956
- // `value` is reconstructed from source text, so a valid TS literal may carry
4957
- // numeric separators (`100_000`). Strip them between digits, then accept a
4958
- // plain decimal / float; Go template numeric literals don't allow `_`.
4959
- const v = c.value.trim().replace(/(?<=\d)_(?=\d)/g, '')
4960
- return /^-?\d+(\.\d+)?$/.test(v) ? v : null
4961
- }
4962
-
4963
- /**
4964
- * Inline a module-level boolean const (`const OPEN = true`) as its Go
4965
- * literal (`true`/`false`). Only a plain `true`/`false` initializer
4966
- * qualifies (#2815) mirrors `resolveModuleNumericConst`'s shape, sharing
4967
- * its lookup rather than adding a second `.find(` (see
4968
- * `findModuleConst`'s docstring).
4963
+ * Resolve a bare identifier naming a plain module-level const (`const
4964
+ * TRACK = 8`, `const OPEN = true`, `const INITIAL: Row[] = [...]`, #2794 /
4965
+ * #2815 / #2862) to its value's Go literal dispatched STRUCTURALLY off
4966
+ * `ConstantInfo.parsed`, through the same `parsedLiteralToGo` door an
4967
+ * inline `createSignal([...])`/`createSignal({...})` seed already takes,
4968
+ * rather than a separate per-type text-matching resolver for each literal
4969
+ * shape (the numeric/boolean resolvers this replaces, and the array/
4970
+ * object-literal gap #2862 tracked, were exactly that repeated pattern).
4971
+ * `resolveModuleStringConst` stays separate: its fixed-point text
4972
+ * resolution also covers a COMPOSED template-literal const (`` `${A}-${B}`
4973
+ * ``) that has no single structural literal to bake.
4974
+ *
4975
+ * `target` names the emission position, which constrains what can resolve
4976
+ * there:
4977
+ * - `go-source` (the `New<Component>Props` constructor) may bake a
4978
+ * composite (array/object) literal against `bakeType` — falling back
4979
+ * to the const's OWN declared type when the consumer's type is
4980
+ * `unknown` (the analyzer's inference is text-shaped and never chases
4981
+ * a bare `createSignal(INITIAL)` seed to its declaration).
4982
+ * - `template-action` (a bare `{{...}}` splice, e.g. a className
4983
+ * expression) has no valid Go template spelling for a composite
4984
+ * literal, so only a scalar (or unary-minus number) resolves there —
4985
+ * the same restriction the numeric/boolean resolvers already had.
4986
+ *
4987
+ * Scoped to module consts and guarded against loop vars so a range
4988
+ * variable that shadows a const name still wins (same guards the
4989
+ * resolvers this replaces already had).
4969
4990
  */
4970
- private resolveModuleBooleanConst(name: string): string | null {
4991
+ private resolveModuleConstAsGo(
4992
+ name: string,
4993
+ target: { kind: 'template-action' } | { kind: 'go-source'; bakeType: TypeInfo },
4994
+ ): string | null {
4971
4995
  if (this.isCurrentLoopItem(name)) return null
4972
4996
  if (this.loopVarRefCount.has(name)) return null
4973
4997
  if (this.isOuterLoopParam(name)) return null
4974
4998
  const c = this.findModuleConst(name)
4975
- if (!c || c.value === undefined) return null
4976
- const v = c.value.trim()
4977
- return v === 'true' || v === 'false' ? v : null
4999
+ if (!c?.parsed) return null
5000
+ if (target.kind === 'template-action') {
5001
+ return c.parsed.kind === 'literal' || c.parsed.kind === 'unary'
5002
+ ? parsedLiteralToGo(this.emitCtx, c.parsed)
5003
+ : null
5004
+ }
5005
+ const bakeType = target.bakeType.kind !== 'unknown' ? target.bakeType : (c.type ?? undefined)
5006
+ return parsedLiteralToGo(this.emitCtx, c.parsed, bakeType)
4978
5007
  }
4979
5008
 
4980
5009
  literal(value: string | number | boolean | null, literalType: LiteralType): string {
@@ -5180,12 +5209,35 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
5180
5209
  // arithmetic index lowers correctly. A multi-token operand
5181
5210
  // (`bf_add $i 1`) must be parenthesised or Go parses it as extra
5182
5211
  // `bf_get` arguments.
5183
- return `bf_get ${wrapIfMultiToken(emit(object))} ${wrapIfMultiToken(emit(index))}`
5212
+ return `bf_get ${wrapIfMultiToken(this.emitOperand(object, emit))} ${wrapIfMultiToken(this.emitOperand(index, emit))}`
5213
+ }
5214
+
5215
+ /**
5216
+ * A `binary`/`logical`/`unary` operand that feeds a prefix-call Go form
5217
+ * (`bf_mul a b`, `and a b`, `not a`, …), which cannot host a raw `{{…}}`
5218
+ * action the way the generic `emit` dispatcher's `templateLiteral()` would
5219
+ * produce for a template-literal operand (#2863). ONLY a template literal
5220
+ * is special-cased here, folded through `bf_concat_str` (the same door
5221
+ * `lowerValueOperand` uses for a ternary branch / helper-call arg /
5222
+ * query-guard value); every other kind still goes through the caller's own
5223
+ * `emit` closure unchanged. This matters: routing every operand through
5224
+ * `lowerValueOperand`'s `convertExpressionToGo` fallback (rather than only
5225
+ * the one broken kind) would ALSO pick up `convertExpressionToGo`'s own
5226
+ * string-keyed fast paths — e.g. inlining a bare identifier bound to a
5227
+ * local literal const — which `emit`'s AST-walk `identifier()` dispatch
5228
+ * does not do, a real behavioral divergence for that shape (caught by the
5229
+ * #2236 loop-param-shadowing regression pins).
5230
+ */
5231
+ private emitOperand(n: ParsedExpr, emit: (e: ParsedExpr) => string): string {
5232
+ if (n.kind === 'template-literal') {
5233
+ return wrapIfMultiToken(lowerTemplateLiteralValue(this.emitCtx, n.parts))
5234
+ }
5235
+ return emit(n)
5184
5236
  }
5185
5237
 
5186
5238
  binary(op: string, left: ParsedExpr, right: ParsedExpr, emit: (e: ParsedExpr) => string): string {
5187
- const l = emit(left)
5188
- const r = emit(right)
5239
+ const l = this.emitOperand(left, emit)
5240
+ const r = this.emitOperand(right, emit)
5189
5241
  // Every Go form below is a prefix function call (`bf_mul a b`, `gt a b`,
5190
5242
  // `eq a b`), so a COMPOUND operand must be parenthesised or the template
5191
5243
  // parser folds its tokens into the call's argument list — e.g.
@@ -5268,7 +5320,7 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
5268
5320
  }
5269
5321
 
5270
5322
  unary(op: string, argument: ParsedExpr, emit: (e: ParsedExpr) => string): string {
5271
- const arg = emit(argument)
5323
+ const arg = this.emitOperand(argument, emit)
5272
5324
  // `not` is a Go template prefix builtin like `and`/`or` (see `logical()`
5273
5325
  // below) — a multi-token argument (e.g. `or a b`) must be parenthesised
5274
5326
  // or it degrades into extra sibling args of `not` itself (#2758: `not
@@ -5291,8 +5343,8 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
5291
5343
  // `and`/`or`. This makes `searchParams().get(k) ?? d` lower to
5292
5344
  // `or (.SearchParams.Get "sort") "none"` instead of the broken
5293
5345
  // `or .SearchParams.Get "sort" "none"`.
5294
- const wrapLeft = wrapIfMultiToken(emit(left))
5295
- const wrapRight = wrapIfMultiToken(emit(right))
5346
+ const wrapLeft = wrapIfMultiToken(this.emitOperand(left, emit))
5347
+ const wrapRight = wrapIfMultiToken(this.emitOperand(right, emit))
5296
5348
  if (op === '&&') return `and ${wrapLeft} ${wrapRight}`
5297
5349
  // `??` on a nillable prop needs true JS nullish semantics (#2248): Go's
5298
5350
  // `or` is truthiness-based, so `{{or .Label "Default"}}` falls back on a
@@ -5600,8 +5652,16 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
5600
5652
  method: ArrayMethod,
5601
5653
  object: ParsedExpr,
5602
5654
  args: ParsedExpr[],
5603
- emit: (e: ParsedExpr) => string,
5655
+ rawEmit: (e: ParsedExpr) => string,
5604
5656
  ): string {
5657
+ // Every `emit(...)` call below lowers an ARGUMENT position (the
5658
+ // receiver or a `.method(...)` arg) that feeds a prefix-call Go form
5659
+ // (`bf_join (...) ...`, `bf_replace ... ... ...`, …) — shadow the
5660
+ // dispatcher's own `emit` with `emitOperand` (#2863) so a
5661
+ // template-literal receiver/argument (e.g. `items.join(\`${a}-${b}\`)`)
5662
+ // folds through `bf_concat_str` instead of leaking the TEXT-position
5663
+ // `templateLiteral()` form into another action's argument list.
5664
+ const emit = (e: ParsedExpr): string => this.emitOperand(e, rawEmit)
5605
5665
  // `bf_join` etc. are registered in the runtime FuncMap. The exhaustive
5606
5666
  // switch on `method` mirrors the IR-level discriminator — adding a new
5607
5667
  // `ArrayMethod` variant becomes a TS compile error until every adapter
@@ -6181,12 +6241,28 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
6181
6241
  // prefix because a filter predicate must always escape back to
6182
6242
  // root regardless of loop nesting; call it only for its
6183
6243
  // registration side effect and keep the prefix here (pullfrog
6184
- // review, PR #2818).
6185
- this.rootFieldRef(signal)
6186
- return `$.${capitalizeFieldName(signal)}`
6244
+ // review, PR #2818). The field name itself DOES go through
6245
+ // `resolveRootFieldAlias` (#2857) so a getter-alias local
6246
+ // (`const items__alias = items`) still emits the field the
6247
+ // signal itself seeds instead of a phantom `.Items__alias`.
6248
+ return this.filterRootFieldRef(signal)
6187
6249
  }
6188
- this.rootFieldRef(expr.name)
6189
- return `.${capitalizeFieldName(expr.name)}`
6250
+ // Any other identifier reaching here is ALSO a root-scope
6251
+ // reference (a memo, a plain derived const, or — #2857 — a
6252
+ // bare-props-form destructured/renamed prop) rather than the
6253
+ // loop's own param, so it needs the same unconditional `$.`
6254
+ // escape as the signal branch above, for the same reason: a
6255
+ // `.filter().map()` JSX loop's `{{if}}` gate (built from
6256
+ // `loop.filterPredicate` a few hundred lines below, via
6257
+ // `renderPredicateCondition` → this method) always sits inside
6258
+ // that loop's own `{{range}}`, never at the template's un-rebound
6259
+ // root, so `rootFieldRef`'s own `this.inLoop`-conditional prefix
6260
+ // would be wrong here even without the alias bug — a bare
6261
+ // `.Field` resolves against the CURRENT LOOP ITEM's type, not
6262
+ // root, and `html/template` panics at execute time (`can't
6263
+ // evaluate field ... in type ...`) the first time such a
6264
+ // reference is exercised.
6265
+ return this.filterRootFieldRef(expr.name)
6190
6266
  }
6191
6267
 
6192
6268
  case 'literal':
@@ -6203,6 +6279,18 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
6203
6279
  if (expr.object.kind === 'identifier' && expr.object.name === param) {
6204
6280
  return `${paramPrefix}.${capitalizeFieldName(expr.property)}`
6205
6281
  }
6282
+ // props.x on a bare-props-form component (#2879): props are
6283
+ // flattened onto the root struct, so this is a root-scope read
6284
+ // (`$.X`), not `.Props.X`. Mirrors `renderConditionExpr`'s own
6285
+ // `propsObjectName` branch; `props.a.b` still reaches root through
6286
+ // the generic recursion below.
6287
+ if (
6288
+ expr.object.kind === 'identifier' &&
6289
+ this.state.propsObjectName &&
6290
+ expr.object.name === this.state.propsObjectName
6291
+ ) {
6292
+ return this.filterRootFieldRef(expr.property)
6293
+ }
6206
6294
  // `.length` on a higher-order filter result (e.g.
6207
6295
  // `x.tags.filter(t => t.active).length > 0`). Reuse
6208
6296
  // `renderFilterLengthExpr` so the inner filter lowers to
@@ -6231,11 +6319,11 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
6231
6319
  return `${paramPrefix}.${capitalizeFieldName(expr.callee.property)}`
6232
6320
  }
6233
6321
  // Signal calls: `filter()` -> `$.Filter`. Same registration-only
6234
- // `rootFieldRef` call as the `identifier` case above, for the same
6322
+ // `rootFieldRef` call and the same `resolveRootFieldAlias` field
6323
+ // resolution (#2857) — as the `identifier` case above, for the same
6235
6324
  // reason (#2700's BF101 gate; pullfrog review, PR #2818).
6236
6325
  if (expr.callee.kind === 'identifier' && expr.args.length === 0) {
6237
- this.rootFieldRef(expr.callee.name)
6238
- return `$.${capitalizeFieldName(expr.callee.name)}`
6326
+ return this.filterRootFieldRef(expr.callee.name)
6239
6327
  }
6240
6328
  // A nested callback method call (`other.some(r => …)`) reaching this
6241
6329
  // arm has no Go template form in filter context — the fallthrough
@@ -6271,30 +6359,50 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
6271
6359
  if (this.filterExprUnsupported) return 'false'
6272
6360
  const right = this.renderFilterExpr(expr.right, param, localVarMap, datumField)
6273
6361
  if (this.filterExprUnsupported) return 'false'
6362
+ // #2873 (pullfrog review, PR #2882): every case below is a prefix
6363
+ // function call (`bf_mod a b`, `eq a b`, …), so a COMPOUND operand —
6364
+ // itself a nested binary result like `bf_mod .N 2` — must be
6365
+ // parenthesised or the template parser folds its tokens into the
6366
+ // outer call's argument list (`eq bf_mod .N 2 0` hands `eq` four
6367
+ // args, and a bare func-name argument like `bf_mod` there is called
6368
+ // with ZERO args instead of nested: `wrong number of args for
6369
+ // bf_mod: want 2 got 0`). Mirrors the general (non-filter) binary
6370
+ // emitter's `wl`/`wr` wrapping (`wrapIfMultiToken`, above) — this
6371
+ // switch had never applied it, so `-`/`*`/`/` had the identical
6372
+ // pre-existing gap for a nested compound operand.
6373
+ const wl = wrapIfMultiToken(left)
6374
+ const wr = wrapIfMultiToken(right)
6274
6375
 
6275
6376
  switch (expr.op) {
6276
6377
  case '===':
6277
6378
  case '==':
6278
- return `eq ${left} ${right}`
6379
+ return `eq ${wl} ${wr}`
6279
6380
  case '!==':
6280
6381
  case '!=':
6281
- return `ne ${left} ${right}`
6382
+ return `ne ${wl} ${wr}`
6282
6383
  case '>':
6283
- return `gt ${left} ${right}`
6384
+ return `gt ${wl} ${wr}`
6284
6385
  case '<':
6285
- return `lt ${left} ${right}`
6386
+ return `lt ${wl} ${wr}`
6286
6387
  case '>=':
6287
- return `ge ${left} ${right}`
6388
+ return `ge ${wl} ${wr}`
6288
6389
  case '<=':
6289
- return `le ${left} ${right}`
6390
+ return `le ${wl} ${wr}`
6290
6391
  case '+':
6291
- return this._emitPlus(expr.left, expr.right, left, right)
6392
+ return this._emitPlus(expr.left, expr.right, wl, wr)
6292
6393
  case '-':
6293
- return `bf_sub ${left} ${right}`
6394
+ return `bf_sub ${wl} ${wr}`
6294
6395
  case '*':
6295
- return `bf_mul ${left} ${right}`
6396
+ return `bf_mul ${wl} ${wr}`
6296
6397
  case '/':
6297
- return `bf_div ${left} ${right}`
6398
+ return `bf_div ${wl} ${wr}`
6399
+ case '%':
6400
+ // #2873: mirrors the general binary emitter's `%` case (below,
6401
+ // `bf_mod`) — this switch previously had no `%` case, so the
6402
+ // `default` arm emitted a literal ` % ` into the template text,
6403
+ // which `html/template` can't parse (`unexpected "%" in
6404
+ // operand`).
6405
+ return `bf_mod ${wl} ${wr}`
6298
6406
  default:
6299
6407
  return `${left} ${expr.op} ${right}`
6300
6408
  }
@@ -6540,7 +6648,7 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
6540
6648
  * `loopVarRefCount` — so both are still consulted. Shared by the
6541
6649
  * string-keyed fast paths (#2236, #2242 Copilot review) that resolve raw
6542
6650
  * `jsExpr` text before `identifier()`'s own guards can see it. Mirrors the
6543
- * checks in `resolveModuleStringConst` / `resolveModuleNumericConst`.
6651
+ * checks in `resolveModuleStringConst` / `resolveModuleConstAsGo`.
6544
6652
  */
6545
6653
  private isLoopShadowedName(name: string): boolean {
6546
6654
  return this.scope.isBound(name) || this.loopVarRefCount.has(name)
@@ -6682,34 +6790,23 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
6682
6790
  }
6683
6791
  continue
6684
6792
  }
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).
6793
+ // A ternary or template literal (`row.on ? "yes" : "no"`, `` `#${row.id}
6794
+ // ${row.label}` ``) in this ARGUMENT position (a `bf_with_props`/
6795
+ // `bf_reprops` value, not a text position) must lower through the
6796
+ // shared value-operand door (#2863/#2335) `convertExpressionToGo`'s
6797
+ // generic path returns `templateLiteral()`'s TEXT-position mixed
6798
+ // literal-text-plus-`{{…}}`-actions form for a template literal, which
6799
+ // is not a legal bare pipeline argument (multi-part) or needed an
6800
+ // ad-hoc single-part unwrap here (this replaces both). Recompute from
6801
+ // the already-validated `exprOut.parsed` tree rather than trying to
6802
+ // unwrap or refuse the text-position string after the fact.
6803
+ if (exprOut.parsed?.kind === 'template-literal' || exprOut.parsed?.kind === 'conditional') {
6804
+ go = lowerValueOperand(this.emitCtx, exprOut.parsed)
6805
+ } else if (this.isTemplateFragment(go, exprOut.parsed?.kind)) {
6806
+ // A genuine `{{if}}...{{end}}`-shaped (or other `{{`-leading action)
6807
+ // fragment can't be a bare pipeline argument — refuse loudly rather
6808
+ // than silently dropping the prop (the #2445 bug was exactly a
6809
+ // silent drop one level up).
6713
6810
  this.state.errors.push({
6714
6811
  code: 'BF101',
6715
6812
  severity: 'error',
@@ -7107,6 +7204,15 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
7107
7204
  result = `bf_mul ${left} ${right}`; break
7108
7205
  case '/':
7109
7206
  result = `bf_div ${left} ${right}`; break
7207
+ case '%':
7208
+ // #2873: this switch had no `%` case even though
7209
+ // `needsParensInGoTemplate` (above) already treats `%` as an
7210
+ // arithmetic operator needing parens — so the `default` arm
7211
+ // below emitted a literal ` % ` into a ternary/condition's
7212
+ // template text, which `html/template` can't parse
7213
+ // (`unexpected "%" in operand`). Mirrors the general binary
7214
+ // emitter's `%` case (`bf_mod`, outside condition position).
7215
+ result = `bf_mod ${left} ${right}`; break
7110
7216
  default:
7111
7217
  result = `${left} ${expr.op} ${right}`
7112
7218
  }
@@ -7164,7 +7270,12 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
7164
7270
  }
7165
7271
 
7166
7272
  case 'template-literal':
7167
- return plain(this.renderParsedExpr(expr))
7273
+ // #2863 follow-up: fold to one Go value (`bf_concat_str` chain) via
7274
+ // the same door as the `conditional` case just above, instead of
7275
+ // `renderParsedExpr`'s TEXT-position `templateLiteral()` form —
7276
+ // which would leak raw `{{…}}` into THIS condition-expression's own
7277
+ // pipeline position (e.g. `(a === \`${x}-${y}\`) ? … : …`).
7278
+ return plain(lowerTemplateLiteralValue(this.emitCtx, expr.parts))
7168
7279
 
7169
7280
  case 'arrow':
7170
7281
  // A standalone arrow has no Go condition form (callbacks reach the
@@ -94,9 +94,10 @@ export function convertInitialValue(
94
94
  if (param) {
95
95
  return propRef(param)
96
96
  }
97
- // Module-const seed (#2794): a signal seeded from a bare identifier
98
- // that refers to a module-level const (`const PAYLOAD = 'x';
99
- // createSignal(PAYLOAD)`) types `unknown` the analyzer's type
97
+ // Module-const seed (#2794/#2815/#2862): a signal seeded from a bare
98
+ // identifier that refers to a module-level const (`const PAYLOAD = 'x';
99
+ // createSignal(PAYLOAD)`, or `const INITIAL: Row[] = [...];
100
+ // createSignal(INITIAL)`) types `unknown` — the analyzer's type
100
101
  // inference is text-shaped and never chases an identifier to its
101
102
  // declaration — so none of the typed branches below ever see it and
102
103
  // this used to fall through to the final `nil`. Checked AFTER the
@@ -107,10 +108,8 @@ export function convertInitialValue(
107
108
  // leaving that case on the pre-existing `nil` path unchanged.
108
109
  const inlinedStr = ctx.resolveModuleStringConst(value)
109
110
  if (inlinedStr !== null) return inlinedStr
110
- const inlinedNum = ctx.resolveModuleNumericConst(value)
111
- if (inlinedNum !== null) return inlinedNum
112
- const inlinedBool = ctx.resolveModuleBooleanConst(value)
113
- if (inlinedBool !== null) return inlinedBool
111
+ const inlinedConst = ctx.resolveModuleConstAsGo(value, { kind: 'go-source', bakeType: typeInfo })
112
+ if (inlinedConst !== null) return inlinedConst
114
113
  }
115
114
 
116
115
  const propName = ctx.extractPropNameFromInitialValue(value, preParsed)
@@ -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
  }