@barefootjs/go-template 0.33.2 → 0.33.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.
@@ -80,7 +80,7 @@ import {
80
80
  BindingScope,
81
81
  } from '@barefootjs/jsx'
82
82
  import { findInterpolationEnd } from '@barefootjs/jsx/scanner'
83
- import { BF_REGION, escapeHtml } from '@barefootjs/shared'
83
+ import { BF_REGION, escapeHtml, resolveJsxChildrenProp } from '@barefootjs/shared'
84
84
 
85
85
  import {
86
86
  GO_IDENTIFIER,
@@ -118,7 +118,6 @@ import type {
118
118
  CtorLowerEnv,
119
119
  GoTemplateAdapterOptions,
120
120
  } from "./lib/types.ts"
121
- import { collectRootScopeNodes } from "./lib/ir-scope.ts"
122
121
  import { GO_TEMPLATE_PRIMITIVES } from "./lib/constants.ts"
123
122
  import { CompileState, resolveSignalParsedThroughSeedPlan } from "./lib/compile-state.ts"
124
123
  import { hasClientInteractivity, findNestedComponents } from "./analysis/component-tree.ts"
@@ -313,15 +312,6 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
313
312
  * everywhere except the accessor lookup itself.
314
313
  */
315
314
  private scope: BindingScope = BindingScope.EMPTY
316
- /**
317
- * Stack of `IRLoop.depth` values (innermost last), pushed/popped around
318
- * `renderChildren(loop.children)` in `renderLoop`. `renderAttributes`
319
- * reads the top of this stack to derive the `key` → `data-key`/
320
- * `data-key-N` suffix — the depth is IR-computed (jsx-to-ir.ts), not
321
- * re-derived here; this stack only threads that value down to the
322
- * loop body's root element (#2168 nested-loop-outer-binding).
323
- */
324
- private loopKeyDepthStack: number[] = []
325
315
  /**
326
316
  * Per-loop: true when the body renders the bare range value (scalar-item
327
317
  * inline-literal loop), so the `bf_tmpl` companion is fed `.BfLoopItem` (the
@@ -521,7 +511,6 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
521
511
  const isRootComponent = ir.root.type === 'component'
522
512
  const isIfStatement = ir.root.type === 'if-statement'
523
513
 
524
- this.state.rootScopeNodes = collectRootScopeNodes(ir.root)
525
514
  // Map each array memo backing a loop (`<memo>().map(...)`) to that loop's
526
515
  // handler-filled slice field, so `<memo>().length` lowers to the slice's
527
516
  // length. Built before rendering — `.length` can precede the loop in source.
@@ -2323,7 +2312,7 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
2323
2312
  }
2324
2313
  case 'jsx-children':
2325
2314
  // The RESERVED children slot (`comp.children.length > 0 ? comp.children
2326
- // : jsxChildrenPropNodes(...)`) is handled below via
2315
+ // : resolveJsxChildrenProp(...)`) is handled below via
2327
2316
  // `child.childrenText` / `child.childrenHtml` and must not be
2328
2317
  // re-emitted here. A JSX-valued prop under any OTHER name
2329
2318
  // (`header={<strong>Title</strong>}`, #2168 jsx-element-prop) is a
@@ -3173,17 +3162,6 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
3173
3162
  return out
3174
3163
  }
3175
3164
 
3176
- /**
3177
- * Pull the IR nodes out of a `children={<…/>}` attribute (a `jsx-children`
3178
- * prop value). Empty when the component takes no such prop.
3179
- */
3180
- private jsxChildrenPropNodes(props: IRProp[]): IRNode[] {
3181
- for (const p of props) {
3182
- if (p.value.kind === 'jsx-children') return p.value.children
3183
- }
3184
- return []
3185
- }
3186
-
3187
3165
  private extractHtmlChildren(children: IRNode[]): string | null {
3188
3166
  if (children.length === 0) return null
3189
3167
  if (children.every(c => c.type === 'text')) return null
@@ -3212,7 +3190,8 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
3212
3190
  *
3213
3191
  * `ctx.isRoot` leaks into a JSX-valued prop's subtree, so such a fragment
3214
3192
  * is already tagged `needsScopeComment` as if it were a component root —
3215
- * which is why `carriesDataKey` reaches this path at all.
3193
+ * which is why a relay `element.keyAttr` (#2753; `carriesDataKey` before
3194
+ * it) reaches this path at all.
3216
3195
  */
3217
3196
  private bakingStaticChildren = false
3218
3197
 
@@ -3274,7 +3253,7 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
3274
3253
  // paths below see them.
3275
3254
  const effectiveChildren = comp.children.length > 0
3276
3255
  ? comp.children
3277
- : this.jsxChildrenPropNodes(comp.props)
3256
+ : resolveJsxChildrenProp(comp.props)
3278
3257
  result.push({
3279
3258
  name: comp.name,
3280
3259
  slotId: comp.slotId,
@@ -4140,21 +4119,34 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
4140
4119
  if (element.needsScope) {
4141
4120
  hydrationAttrs += ` ${this.renderScopeMarker('.ScopeID')}`
4142
4121
  }
4143
- // A root scope element carries `data-key` for a keyed loop item the
4144
- // parent's loop init stamped `.BfDataKey`, so a non-keyed render emits
4145
- // nothing. Applies to early-return (if-statement) roots too, where every
4146
- // branch's top element qualifies.
4147
- // `carriesDataKey` is an INDEPENDENT reason to emit, not a refinement of
4148
- // the root-scope test above (#2732): a fragment root clears `needsScope` on
4149
- // its wrapped child and moves the other hydration markers onto the
4150
- // `<!--bf-scope:-->` comment, but `data-key` must stay a real attribute —
4151
- // the client's `mapArray` adopt loop reads `primaryEl.dataset.key`, never
4152
- // the comment. Mirrors the same branch in hono-adapter.ts.
4153
- if (
4154
- (this.state.rootScopeNodes.has(element) && element.needsScope) ||
4155
- (element.carriesDataKey && !this.bakingStaticChildren)
4156
- ) {
4157
- hydrationAttrs += `{{if .BfDataKey}} data-key="{{.BfDataKey}}"{{end}}`
4122
+ // #2753: `element.keyAttr` is the ONE IR-resolved decision for this
4123
+ // element's row-key attribute replacing both the `carriesDataKey`
4124
+ // boolean and this adapter's own `rootScopeNodes`/`needsScope` check
4125
+ // (mechanism 2: a render-root relay, including the #2732 fragment-root
4126
+ // case) and the separate `loopKeyDepthStack`-driven rewrite of a literal
4127
+ // `key` attribute this adapter used to do in `renderAttributes`
4128
+ // (mechanism 1: a `.map()` row root compiled inline here).
4129
+ if (element.keyAttr) {
4130
+ if (element.keyAttr.value !== undefined) {
4131
+ const lowered = emitAttrValue(
4132
+ { kind: 'expression', expr: element.keyAttr.value },
4133
+ this.elementAttrEmitter,
4134
+ element.keyAttr.name,
4135
+ )
4136
+ if (lowered) hydrationAttrs += ` ${lowered}`
4137
+ } else if (!this.bakingStaticChildren) {
4138
+ // Relay: this element is one of THIS component's own render roots,
4139
+ // carrying whatever key its OWN caller supplies at runtime (the
4140
+ // parent's loop init stamped `.BfDataKey`), when this component is
4141
+ // itself invoked as a caller's keyed loop row. Applies to
4142
+ // early-return (if-statement) roots too, where every branch's top
4143
+ // element qualifies, and to #2732's fragment-root case.
4144
+ // `bakingStaticChildren` (see its docstring) excludes a hoisted
4145
+ // children value baked into a static Go string at compile time —
4146
+ // by construction not a loop row, and a surviving `{{if}}` action
4147
+ // there gets silently dropped by the caller rather than rendered.
4148
+ hydrationAttrs += `{{if .BfDataKey}} data-key="{{.BfDataKey}}"{{end}}`
4149
+ }
4158
4150
  }
4159
4151
  if (element.slotId) {
4160
4152
  hydrationAttrs += ` ${this.renderSlotMarker(element.slotId)}`
@@ -7170,7 +7162,6 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
7170
7162
  this.scalarLiteralLoopGoType(loop.arrayParsed, loop.itemType) !== null,
7171
7163
  )
7172
7164
  this.loopWrapperStack.push(!!loop.childComponent)
7173
- this.loopKeyDepthStack.push(loop.depth)
7174
7165
  // Rendered inside the pushed loop scope so each initializer resolves
7175
7166
  // against the range item (`.Done`), in source order so a later
7176
7167
  // initializer sees an earlier `$` variable — same as the source block.
@@ -7178,7 +7169,6 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
7178
7169
  .map(d => `{{$${d.name} := ${this.renderParsedExpr(d.valueParsed)}}}`)
7179
7170
  .join('')
7180
7171
  const children = this.renderChildren(loop.children)
7181
- this.loopKeyDepthStack.pop()
7182
7172
  this.loopWrapperStack.pop()
7183
7173
  this.loopScalarItemStack.pop()
7184
7174
  // Build the per-item anchor marker while the row scope is still active,
@@ -7270,7 +7260,6 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
7270
7260
  const wasInLoopOuter = this.inLoop
7271
7261
  this.inLoop = true
7272
7262
  this.loopWrapperStack.push(false)
7273
- this.loopKeyDepthStack.push(loop.depth)
7274
7263
  this.loopScalarItemStack.push(this.scalarLiteralLoopGoType(loop.arrayParsed, loop.itemType) !== null)
7275
7264
  // The bake gate (`analyzeBakeableStaticElementLoop`) already refused a
7276
7265
  // destructured or `.keys()`-shape param, so `loop` binds via `enterLoopRow`
@@ -7303,7 +7292,6 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
7303
7292
 
7304
7293
  this.scope = prevScope
7305
7294
  this.loopScalarItemStack.pop()
7306
- this.loopKeyDepthStack.pop()
7307
7295
  this.loopWrapperStack.pop()
7308
7296
  this.inLoop = wasInLoopOuter
7309
7297
 
@@ -7339,7 +7327,7 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
7339
7327
  private queueDynamicChildrenDefine(comp: IRComponent): string | null {
7340
7328
  const effectiveChildren = comp.children.length > 0
7341
7329
  ? comp.children
7342
- : this.jsxChildrenPropNodes(comp.props)
7330
+ : resolveJsxChildrenProp(comp.props)
7343
7331
  if (effectiveChildren.length === 0) return null
7344
7332
  if (this.extractTextChildren(effectiveChildren) !== null) return null
7345
7333
  if (this.extractHtmlChildren(effectiveChildren) !== null) return null
@@ -7366,7 +7354,7 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
7366
7354
  private queueLoopBodyChildrenDefine(comp: IRComponent): string | null {
7367
7355
  const effectiveChildren = comp.children.length > 0
7368
7356
  ? comp.children
7369
- : this.jsxChildrenPropNodes(comp.props)
7357
+ : resolveJsxChildrenProp(comp.props)
7370
7358
  if (effectiveChildren.length === 0) return null
7371
7359
  if (this.extractTextChildren(effectiveChildren) !== null) return null
7372
7360
  if (this.extractHtmlChildren(effectiveChildren) !== null) return null
@@ -7749,19 +7737,15 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
7749
7737
  // literal never reaches the generic object-literal BF101 refusal
7750
7738
  // (which would double-report alongside the purpose-built one).
7751
7739
  if (isDangerousInnerHtmlAttr(attr)) continue
7752
- // Rewrite JSX special-prop names to their HTML-attribute counterparts. The
7753
- // Go template adapter has no JSX runtime to strip `key` / emit `data-key`,
7754
- // so the rewrite happens at attribute-emit time. Mirror of the `key`
7755
- // branch in `ir-to-client-js/html-template.ts`. The depth-suffix (plain
7756
- // `data-key` at the outermost loop, `data-key-N` N levels deep) comes
7757
- // from `IRLoop.depth` via `loopKeyDepthStack`, not re-derived here.
7758
- let attrName: string
7759
- if (attr.name === 'className') attrName = 'class'
7760
- else if (attr.name === 'key') {
7761
- const depth = this.loopKeyDepthStack.at(-1) ?? 0
7762
- attrName = depth > 0 ? `data-key-${depth}` : 'data-key'
7763
- }
7764
- else attrName = attr.name
7740
+ // `key` never renders as a literal HTML attribute #2753 resolves it
7741
+ // onto `element.keyAttr` (name AND value) at IR-build time, emitted
7742
+ // separately in `renderElement`. A stray `key` that jsx-to-ir.ts did
7743
+ // NOT recognize as a loop row root (so `keyAttr` was never set) has no
7744
+ // defined rendering outside a `.map()` anyway; drop it rather than
7745
+ // leak an invalid `key="..."` HTML attribute.
7746
+ if (attr.name === 'key') continue
7747
+ // Rewrite JSX special-prop names to their HTML-attribute counterparts.
7748
+ const attrName = attr.name === 'className' ? 'class' : attr.name
7765
7749
  const lowered = emitAttrValue(attr.value, this.elementAttrEmitter, attrName)
7766
7750
  if (lowered) parts.push(lowered)
7767
7751
  }
@@ -19,7 +19,6 @@ import type {
19
19
  ContextConsumer,
20
20
  EnvSignalReader,
21
21
  IRMetadata,
22
- IRNode,
23
22
  LoweringMatcher,
24
23
  MemoInfo,
25
24
  ParsedExpr,
@@ -205,10 +204,6 @@ export class CompileState {
205
204
  */
206
205
  stringValueNames: Set<string> = new Set()
207
206
 
208
- /** Component root scope element(s) — each carries `data-key` for a keyed loop
209
- * item. */
210
- rootScopeNodes: Set<IRNode> = new Set()
211
-
212
207
  /** Array-memo name → the handler-filled loop slice field its `.map()` feeds
213
208
  * (e.g. `visible` → `PostListItems`). Lets `<memo>().length` lower to the
214
209
  * slice's length instead of a nil/unset memo field. */
@@ -20,6 +20,10 @@ import type { RenderDivergences } from '@barefootjs/jsx'
20
20
  export const renderDivergences: RenderDivergences = {
21
21
  'jsx-element-prop-fragment-conditional':
22
22
  'Go-specific half of the fragment-wrapped-conditional prop shape, tracked as https://github.com/piconic-ai/barefootjs/issues/2703 (distinct from #2702, the cross-adapter hydrate-time bug). #2702 is a hydrate-time-only bug (SSR is correct on every adapter, confirmed for Go by direct `adapter.generate()` inspection of the raw template TEXT, which does contain `{{if .Cond}}<a bf-c="^s0">x</a>{{else}}...{{end}}`-shaped markup). But the REAL Go render of this exact fixture (`go-template-adapter.test.ts`, real `go run`) produces an EMPTY `<header>` — `<header bf="s1"><!--bf:s0--><!--/--></header>` instead of the reference `<a>x</a>` — i.e. the `header` prop\'s jsx-children payload never reaches the child (`Card`) component\'s slot construction (`.CardSlot1`) at all for this NAMED-prop shape, even though the same conditional-in-fragment payload renders correctly when passed via `children` instead (`jsx-element-prop-children-escape`). Root cause not yet isolated to a specific emitter function — only the reproducible symptom is pinned here. Every other adapter (Hono, ERB, Jinja, Mojolicious, MiniJinja, Twig, Xslate, Blade) renders this fixture correctly; verified by running each adapter\'s own JSX Conformance Tests for this fixture id.',
23
+ 'children-passthrough-renamed':
24
+ '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.',
23
25
  'signal-object-spread-init':
24
26
  'PRE-EXISTING, unrelated to the #2696 Step 2 spread work this fixture pins: a `derived`-classified signal/memo whose value is an OBJECT literal has no live-template-expression lowering on Go — unlike the other six template-stash backends (e.g. minijinja emits `{% set merged = dict(base, done=true) %}`), Go always bakes an object-typed signal/memo field into Go SOURCE at `NewXxxProps` constructor time (`convertInitialValue`/`parsedLiteralToGo`), and that baker is STATIC-only (identifier/call/member operands defer, `parsed-literal-to-go.ts`\'s own docstring) — it cannot reference a live prop at all. Reproduced identically with the spread REMOVED (`createSignal({ id: base.id, done: true })`), confirming the gap predates and is independent of spread: the signal seeds `nil` and every field read (`.Merged.ID`/`.Merged.Done`) reads the Go zero value regardless of `initialTodos`. Graduate by teaching the baker to emit prop-referencing Go expressions (https://github.com/piconic-ai/barefootjs/issues/2700).',
27
+ 'textarea-row-breakout':
28
+ 'A signal seeded from a bare identifier referencing a MODULE-LEVEL const (`const PAYLOAD = \'...\'; createSignal(PAYLOAD)`) bakes to `nil` in the generated `New<Component>Props` constructor instead of the const\'s literal value: `convertInitialValue` (`value-lowering.ts`) only resolves a direct prop reference or a literal expression for a bare identifier, and the analyzer types this signal `{ kind: \'unknown\' }`, so every typed branch falls through to the final `nil` fallback. `resolveModuleStringConst` exists on the adapter for exactly this resolution (used by `template-interp.ts` for live template expressions) but isn\'t wired into this signal-baking path. Unrelated to what this fixture exists to cover (#2765\'s loop-row textarea-escaping fix, verified correct here) — every other adapter renders the fixture\'s controlled `<textarea>` correctly. Tracked at https://github.com/piconic-ai/barefootjs/issues/2794; graduate by wiring `resolveModuleStringConst` into `convertInitialValue`\'s bare-identifier case.',
25
29
  }
@@ -1,13 +0,0 @@
1
- /**
2
- * IR traversal helpers for the Go html/template adapter. Pure functions over
3
- * the IR tree — no adapter instance state.
4
- */
5
- import type { IRNode } from '@barefootjs/jsx';
6
- /**
7
- * Collect the component's root scope element node(s) — the elements that become
8
- * the rendered root and so carry `data-key` for a keyed loop item. A plain
9
- * element root is itself; an `if-statement` (early-return) root contributes the
10
- * top element of each branch, since exactly one renders at runtime.
11
- */
12
- export declare function collectRootScopeNodes(node: IRNode): Set<IRNode>;
13
- //# sourceMappingURL=ir-scope.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"ir-scope.d.ts","sourceRoot":"","sources":["../../../src/adapter/lib/ir-scope.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAAK,EAAE,MAAM,EAA6B,MAAM,iBAAiB,CAAA;AAExE;;;;;GAKG;AACH,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,MAAM,GAAG,GAAG,CAAC,MAAM,CAAC,CAiB/D"}
@@ -1,31 +0,0 @@
1
- /**
2
- * IR traversal helpers for the Go html/template adapter. Pure functions over
3
- * the IR tree — no adapter instance state.
4
- */
5
-
6
- import type { IRNode, IRIfStatement, IRFragment } from '@barefootjs/jsx'
7
-
8
- /**
9
- * Collect the component's root scope element node(s) — the elements that become
10
- * the rendered root and so carry `data-key` for a keyed loop item. A plain
11
- * element root is itself; an `if-statement` (early-return) root contributes the
12
- * top element of each branch, since exactly one renders at runtime.
13
- */
14
- export function collectRootScopeNodes(node: IRNode): Set<IRNode> {
15
- const out = new Set<IRNode>()
16
- const visit = (n: IRNode | null): void => {
17
- if (!n) return
18
- if (n.type === 'element') { out.add(n); return }
19
- if (n.type === 'if-statement') {
20
- const s = n as IRIfStatement
21
- visit(s.consequent)
22
- visit(s.alternate)
23
- return
24
- }
25
- if (n.type === 'fragment') {
26
- for (const c of (n as IRFragment).children) visit(c)
27
- }
28
- }
29
- visit(node)
30
- return out
31
- }