@barefootjs/go-template 0.35.1 → 0.35.3

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.
@@ -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 {
@@ -6212,12 +6241,28 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
6212
6241
  // prefix because a filter predicate must always escape back to
6213
6242
  // root regardless of loop nesting; call it only for its
6214
6243
  // registration side effect and keep the prefix here (pullfrog
6215
- // review, PR #2818).
6216
- this.rootFieldRef(signal)
6217
- 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)
6218
6249
  }
6219
- this.rootFieldRef(expr.name)
6220
- 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)
6221
6266
  }
6222
6267
 
6223
6268
  case 'literal':
@@ -6234,6 +6279,18 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
6234
6279
  if (expr.object.kind === 'identifier' && expr.object.name === param) {
6235
6280
  return `${paramPrefix}.${capitalizeFieldName(expr.property)}`
6236
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
+ }
6237
6294
  // `.length` on a higher-order filter result (e.g.
6238
6295
  // `x.tags.filter(t => t.active).length > 0`). Reuse
6239
6296
  // `renderFilterLengthExpr` so the inner filter lowers to
@@ -6262,11 +6319,11 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
6262
6319
  return `${paramPrefix}.${capitalizeFieldName(expr.callee.property)}`
6263
6320
  }
6264
6321
  // Signal calls: `filter()` -> `$.Filter`. Same registration-only
6265
- // `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
6266
6324
  // reason (#2700's BF101 gate; pullfrog review, PR #2818).
6267
6325
  if (expr.callee.kind === 'identifier' && expr.args.length === 0) {
6268
- this.rootFieldRef(expr.callee.name)
6269
- return `$.${capitalizeFieldName(expr.callee.name)}`
6326
+ return this.filterRootFieldRef(expr.callee.name)
6270
6327
  }
6271
6328
  // A nested callback method call (`other.some(r => …)`) reaching this
6272
6329
  // arm has no Go template form in filter context — the fallthrough
@@ -6302,30 +6359,50 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
6302
6359
  if (this.filterExprUnsupported) return 'false'
6303
6360
  const right = this.renderFilterExpr(expr.right, param, localVarMap, datumField)
6304
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)
6305
6375
 
6306
6376
  switch (expr.op) {
6307
6377
  case '===':
6308
6378
  case '==':
6309
- return `eq ${left} ${right}`
6379
+ return `eq ${wl} ${wr}`
6310
6380
  case '!==':
6311
6381
  case '!=':
6312
- return `ne ${left} ${right}`
6382
+ return `ne ${wl} ${wr}`
6313
6383
  case '>':
6314
- return `gt ${left} ${right}`
6384
+ return `gt ${wl} ${wr}`
6315
6385
  case '<':
6316
- return `lt ${left} ${right}`
6386
+ return `lt ${wl} ${wr}`
6317
6387
  case '>=':
6318
- return `ge ${left} ${right}`
6388
+ return `ge ${wl} ${wr}`
6319
6389
  case '<=':
6320
- return `le ${left} ${right}`
6390
+ return `le ${wl} ${wr}`
6321
6391
  case '+':
6322
- return this._emitPlus(expr.left, expr.right, left, right)
6392
+ return this._emitPlus(expr.left, expr.right, wl, wr)
6323
6393
  case '-':
6324
- return `bf_sub ${left} ${right}`
6394
+ return `bf_sub ${wl} ${wr}`
6325
6395
  case '*':
6326
- return `bf_mul ${left} ${right}`
6396
+ return `bf_mul ${wl} ${wr}`
6327
6397
  case '/':
6328
- 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}`
6329
6406
  default:
6330
6407
  return `${left} ${expr.op} ${right}`
6331
6408
  }
@@ -6571,7 +6648,7 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
6571
6648
  * `loopVarRefCount` — so both are still consulted. Shared by the
6572
6649
  * string-keyed fast paths (#2236, #2242 Copilot review) that resolve raw
6573
6650
  * `jsExpr` text before `identifier()`'s own guards can see it. Mirrors the
6574
- * checks in `resolveModuleStringConst` / `resolveModuleNumericConst`.
6651
+ * checks in `resolveModuleStringConst` / `resolveModuleConstAsGo`.
6575
6652
  */
6576
6653
  private isLoopShadowedName(name: string): boolean {
6577
6654
  return this.scope.isBound(name) || this.loopVarRefCount.has(name)
@@ -7127,6 +7204,15 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
7127
7204
  result = `bf_mul ${left} ${right}`; break
7128
7205
  case '/':
7129
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
7130
7216
  default:
7131
7217
  result = `${left} ${expr.op} ${right}`
7132
7218
  }
@@ -7,7 +7,7 @@
7
7
  * function over `ir.metadata`; no adapter instance state.
8
8
  */
9
9
 
10
- import { collectLoopBoundNames, type ComponentIR, type TypeInfo } from '@barefootjs/jsx'
10
+ import { collectLoopBoundNames, resolveBodyDestructuredPropAliases, type ComponentIR, type TypeInfo } from '@barefootjs/jsx'
11
11
 
12
12
  /** True when `type` is the `string` primitive. */
13
13
  function isStringTypeInfo(type: TypeInfo): boolean {
@@ -49,6 +49,19 @@ function isBareStringLiteral(initialValue: string | undefined): boolean {
49
49
  * string elsewhere in the component) but safe: the suppressed case just
50
50
  * falls back to today's numeric `bf_add` — the same, already-accepted
51
51
  * residual as an unresolvable operand — never silently-wrong output.
52
+ *
53
+ * A bare-props-form component's BODY-destructured, RENAMED prop (`const {
54
+ * fallbackLabel: skipLabel } = props`, the #2788 alias family) has no
55
+ * `propsParams` entry of its own — `propsParams` only carries the TYPE-level,
56
+ * caller-facing names, and the local alias never appears there. Without the
57
+ * alias resolved here too, a string-typed prop referenced only under its
58
+ * local rename in a `+` concat (`greeting + skipLabel`) is invisible to this
59
+ * witness, so `isStringConcatBinary` falls through to numeric `bf_add`
60
+ * instead of `bf_concat_str` (#2894, the go-template sibling of #2883's
61
+ * Mojolicious fix). `resolveBodyDestructuredPropAliases` already recognizes
62
+ * this exact destructuring shape; reusing it here keeps this one witness the
63
+ * single source of truth `_isStringValueName` reads, rather than growing a
64
+ * second, adapter-local alias lookup.
52
65
  */
53
66
  export function collectStringValueNames(ir: ComponentIR): Set<string> {
54
67
  const names = new Set<string>()
@@ -60,6 +73,12 @@ export function collectStringValueNames(ir: ComponentIR): Set<string> {
60
73
  for (const p of ir.metadata.propsParams) {
61
74
  if (isStringTypeInfo(p.type)) names.add(p.name)
62
75
  }
76
+ for (const [local, callerKey] of resolveBodyDestructuredPropAliases(
77
+ ir.metadata.localConstants ?? [],
78
+ ir.metadata.propsObjectName,
79
+ )) {
80
+ if (names.has(callerKey)) names.add(local)
81
+ }
63
82
  for (const c of ir.metadata.localConstants) {
64
83
  if ((c.type !== null && isStringTypeInfo(c.type)) || isBareStringLiteral(c.value)) {
65
84
  names.add(c.name)
@@ -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)
@@ -101,4 +101,19 @@ export const conformancePins: ConformancePins = {
101
101
  // resolves the primitive normally and never reaches this refusal (formerly
102
102
  // tracked as #2771, closed) — no open issue tracks further work.
103
103
  'namespace-import-primitive': [{ code: 'BF013', severity: 'error' }],
104
+ // #2893: `analyzeBakeableStaticElementLoop`'s static-array bake (Go's only
105
+ // path to bind a static/non-signal array as a loop source — `html/template`
106
+ // has no slice literal syntax) bails the WHOLE loop when the body contains
107
+ // a nested `loop` node (module docstring, `static-element-loop-bake.ts`).
108
+ // This fixture's outer static array's row has a nested `.map()` over
109
+ // `item.children`, so it falls through to the generic "computed loop
110
+ // array" BF101 refusal. Unrelated to #2798's own fix (a client-JS-only
111
+ // ref/reactive-binding gap) — this is a pre-existing Go-adapter SSR
112
+ // static-loop-baking scope boundary the new fixture happened to be the
113
+ // first to exercise.
114
+ 'static-nested-loop-ref': [{
115
+ code: 'BF101',
116
+ severity: 'error',
117
+ issue: 'https://github.com/piconic-ai/barefootjs/issues/2893',
118
+ }],
104
119
  }