@barefootjs/go-template 0.32.0 → 0.33.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.
@@ -58,6 +58,7 @@ import {
58
58
  asCallbackMethodCall,
59
59
  sortComparatorFromArrow,
60
60
  emitParsedExpr,
61
+ groupObjectLiteralSegments,
61
62
  emitIRNode,
62
63
  emitAttrValue,
63
64
  augmentInheritedPropAccesses,
@@ -1370,6 +1371,10 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
1370
1371
  if (el.kind !== 'object-literal') return null
1371
1372
  const seen = new Set<string>()
1372
1373
  for (const prop of el.properties) {
1374
+ // A spread (`{ ...t, a: 1 }`, #2696 Step 2) isn't a plain per-key
1375
+ // scalar shape this fast path bakes — fall back (the caller's
1376
+ // `ts.createSourceFile` path, or refusal) rather than mis-baking.
1377
+ if (prop.kind === 'spread') return null
1373
1378
  if (prop.shorthand) return null
1374
1379
  const key = prop.key
1375
1380
  if (!GO_IDENTIFIER.test(key)) return null
@@ -3401,6 +3406,10 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
3401
3406
  if (parsed.kind !== 'object-literal') return null
3402
3407
  const entries: string[] = []
3403
3408
  for (const prop of parsed.properties) {
3409
+ // A spread (`{ ...t }`, #2696 Step 2) isn't a per-key member this Go
3410
+ // struct-literal lowering can express — fail the whole value, same as
3411
+ // any other unsupported member (the consumer keeps its default).
3412
+ if (prop.kind === 'spread') return null
3404
3413
  if (prop.shorthand) return null
3405
3414
  const goVal = this.lowerProviderMapMemberValue(prop.value, propsParams)
3406
3415
  if (goVal === null) return null
@@ -4991,12 +5000,33 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
4991
5000
  return `bf_arr ${parts.join(' ')}`
4992
5001
  }
4993
5002
 
4994
- objectLiteral(_properties: ObjectLiteralProperty[], raw: string, _emit: (e: ParsedExpr) => string): string {
4995
- // The shared `isSupported` gate now admits an EMPTY object literal
4996
- // (`?? {}`) as `??`'s right operand (expression-parser.ts, `logical`
4997
- // case) every other adapter has a native `{}` dict/hashref literal to
4998
- // emit here, but `text/template` has none, so this dispatcher is the
4999
- // one place left to refuse the shape. Unlike the sibling adapters, Go
5003
+ objectLiteral(properties: ObjectLiteralProperty[], raw: string, emit: (e: ParsedExpr) => string): string {
5004
+ // A POPULATED literal is reachable here only in VALUE position (a
5005
+ // `.map()` receiver/callback body, an array-literal element, …) —
5006
+ // `isSupportedValue` admits it when every property value is itself
5007
+ // supported (expression-parser.ts, `checkSupport`'s `pos` parameter).
5008
+ // `text/template` has no map-literal syntax, so lower through the
5009
+ // variadic `bf_map` runtime helper (the object counterpart of
5010
+ // `arrayLiteral`'s `bf_arr`) instead of trying to spell a literal.
5011
+ if (properties.length > 0) {
5012
+ const wrap = (rendered: string) => (rendered.includes(' ') ? `(${rendered})` : rendered)
5013
+ const literalOf = (run: readonly Extract<ObjectLiteralProperty, { kind: 'prop' }>[]) =>
5014
+ `bf_map ${run.map(p => `"${escapeGoString(p.key)}" ${wrap(emit(p.value))}`).join(' ')}`
5015
+ if (!properties.some(p => p.kind === 'spread')) {
5016
+ return literalOf(properties as Extract<ObjectLiteralProperty, { kind: 'prop' }>[])
5017
+ }
5018
+ // Spread (`{ ...t, editing: false }`, #2696 Step 2): `bf_merge`
5019
+ // (runtime/eval.go, a `bf_map` sibling) is a variadic, null-safe map
5020
+ // merge — a non-map argument (nil included) is skipped rather than
5021
+ // panicking, matching JS's null/undefined-spread no-op — folding
5022
+ // every segment (a `bf_map` run, or a spread's own emitted value) in
5023
+ // ONE call, later segments winning, exactly JS spread's semantics.
5024
+ const segments = groupObjectLiteralSegments(properties, literalOf, e => wrap(emit(e)))
5025
+ return `bf_merge ${segments.join(' ')}`
5026
+ }
5027
+ // The EMPTY object literal (`?? {}`) reaches here as `??`'s right
5028
+ // operand (expression-parser.ts, `logical` case) — a RENDERED-position
5029
+ // admission, not a value-position one. Unlike the sibling adapters, Go
5000
5030
  // can't silently fall back to a safe sentinel text: `this.unsupported`'s
5001
5031
  // `[UNSUPPORTED: …]` marker would be spliced into a Go template action
5002
5032
  // (e.g. as an `or`/`and` operand) and break template parsing, and the
@@ -6306,7 +6336,12 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
6306
6336
  // (icon registries, variant/size class maps), so this fully covers them.
6307
6337
  const carried = constInfo.parsed
6308
6338
  if (carried?.kind === 'object-literal') {
6309
- const hit = carried.properties.find(prop => prop.key === key)
6339
+ // A spread entry (`{ ...t }`, #2696 Step 2) has no static key to match
6340
+ // against — skip it, same as before this kind existed (a record const
6341
+ // containing spread already couldn't reach here as a whole).
6342
+ const hit = carried.properties.find(
6343
+ (prop): prop is Extract<ObjectLiteralProperty, { kind: 'prop' }> => prop.kind === 'prop' && prop.key === key,
6344
+ )
6310
6345
  if (hit && hit.value.kind === 'literal') {
6311
6346
  if (hit.value.literalType === 'string') return JSON.stringify(hit.value.value)
6312
6347
  if (hit.value.literalType === 'number') return hit.value.raw ?? String(hit.value.value)
@@ -592,6 +592,17 @@ export function memoInitialFromParsedBody(
592
592
  if (concatGo !== null) return concatGo
593
593
  }
594
594
 
595
+ // () => <object>.map(cb).join(sep) as the memo's WHOLE body (no `+`
596
+ // concatenation) — `map-object-literal-body`'s `ids` memo: `rows().map(t
597
+ // => ({ id: t.id, done: false })).map(r => r.id).join(',')`. The `+`-chain
598
+ // arm above only reaches `matchMapJoinChain` as ONE leaf of a
599
+ // concatenation; a bare chain memo never entered it (#2696 review).
600
+ const bareChain = matchMapJoinChain(body)
601
+ if (bareChain) {
602
+ const chainGo = mapJoinChainToGo(ctx, bareChain, signals, propsParams, propFallbackVars)
603
+ if (chainGo !== null) return chainGo
604
+ }
605
+
595
606
  return null
596
607
  }
597
608
 
@@ -954,7 +965,7 @@ export function collectPropsReadByCtorInit(
954
965
  e.elements.forEach(el => visit(el, bound))
955
966
  return
956
967
  case 'object-literal':
957
- for (const p of e.properties) visit(p.value, bound)
968
+ for (const p of e.properties) visit(p.kind === 'spread' ? p.expr : p.value, bound)
958
969
  return
959
970
  case 'array-method':
960
971
  visit(e.object, bound)
@@ -181,6 +181,9 @@ export function computeObjectMemoInitialValue(
181
181
  const env: CtorLowerEnv = { searchParamsVars: new Set(), params: new Map() }
182
182
  const entries: string[] = []
183
183
  for (const prop of retObj.properties) {
184
+ // A spread (`{ ...t }`, #2696 Step 2) has no per-key `.Params.<Field>`
185
+ // accessor to match against — bail, same as any other unsupported shape.
186
+ if (prop.kind === 'spread') return null
184
187
  // Bail on a shorthand property (`return { tag }`): its value is a bare
185
188
  // identifier whose name need not match a `.Params.<Field>` accessor.
186
189
  if (prop.shorthand) return null
@@ -200,6 +200,11 @@ function recordIndexInterpolationToGo(
200
200
 
201
201
  const entries: { key: string; value: { kind: 'number' | 'string'; text: string } }[] = []
202
202
  for (const prop of parsedConst.properties) {
203
+ // A spread (`{ ...t }`, #2696 Step 2) has no static key/value pair this
204
+ // record-index lowering can bake — bail, same as any other unsupported
205
+ // member (a record const containing spread already couldn't reach here
206
+ // as a whole before this kind existed).
207
+ if (prop.kind === 'spread') return null
203
208
  const v = prop.value
204
209
  if (v.kind === 'literal' && v.literalType === 'number') {
205
210
  entries.push({ key: prop.key, value: { kind: 'number', text: v.raw ?? String(v.value) } })
@@ -143,6 +143,10 @@ function parsedObjectLiteralToGoMap(parsed: ParsedExpr | undefined): string | nu
143
143
  if (!parsed || parsed.kind !== 'object-literal') return null
144
144
  const entries: string[] = []
145
145
  for (const prop of parsed.properties) {
146
+ // A spread (`{ ...t }`, #2696 Step 2) has no static key/value pair this
147
+ // conservative Go-map bake can express — bail, same as any other
148
+ // unsupported shape.
149
+ if (prop.kind === 'spread') return null
146
150
  // Reject a numeric key (`{ 1: 'a' }`); `keyKind` distinguishes it from a
147
151
  // string `'1'` key.
148
152
  if (prop.keyKind === 'numeric') return null
@@ -386,6 +390,10 @@ function objectLiteralToGoSpreadMap(
386
390
  ): string | null {
387
391
  const entries: string[] = []
388
392
  for (const prop of obj.properties) {
393
+ // A spread (`{ ...t }`, #2696 Step 2) isn't a per-key entry this
394
+ // conditional-spread inline lowering can express — bail (the docstring
395
+ // already declares spread out-of-scope for this function).
396
+ if (prop.kind === 'spread') return null
389
397
  // Shorthand (`{ describedBy }`) is unsupported.
390
398
  if (prop.shorthand) return null
391
399
  // Reject a numeric key (`{ 1: x }`); `keyKind` distinguishes it from a
@@ -63,6 +63,9 @@ function bakeInlineObjectAsGoMap(ctx: GoEmitContext, expr: ParsedExpr): string |
63
63
  if (expr.kind !== 'object-literal') return null
64
64
  const entries: string[] = []
65
65
  for (const prop of expr.properties) {
66
+ // A spread (`{ ...t }`, #2696 Step 2) has no static key to bake as a Go
67
+ // map entry — bail, same as any other unsupported shape.
68
+ if (prop.kind === 'spread') return null
66
69
  if (prop.shorthand) return null
67
70
  const go =
68
71
  prop.value.kind === 'object-literal'
@@ -150,6 +153,10 @@ export function parsedLiteralToGo(
150
153
  if (!structFields) return null
151
154
  const entries: string[] = []
152
155
  for (const prop of expr.properties) {
156
+ // A spread (`{ ...t }`, #2696 Step 2) has no static key to match a
157
+ // struct field against — defer the whole object, same as any other
158
+ // unsupported shape.
159
+ if (prop.kind === 'spread') return null
153
160
  // A shorthand `{ a }` carries an identifier value → lowers to null below
154
161
  // and defers the whole object.
155
162
  const goField = structFields.get(prop.key)
@@ -232,6 +232,9 @@ export function objectLiteralToGoMap(ctx: GoEmitContext, expr: ParsedExpr): stri
232
232
  if (expr.kind !== 'object-literal') return null
233
233
  const entries: string[] = []
234
234
  for (const prop of expr.properties) {
235
+ // A spread (`{ ...t }`, #2696 Step 2) has no static key to bake as a Go
236
+ // map entry — bail, same as any other unsupported shape.
237
+ if (prop.kind === 'spread') return null
235
238
  if (prop.shorthand) return null
236
239
  const val = parsedLiteralToGo(ctx, prop.value)
237
240
  if (val === null) return null
@@ -390,40 +393,43 @@ function resolveMapJoinBaseAsGo(
390
393
  return null
391
394
  }
392
395
 
396
+ /** `.map`/`.filter` → the runtime evaluator function that composes it. Both
397
+ * take `items any` and return `[]any` (`eval.go`), so nesting one call's
398
+ * result straight into another's `items` argument is exactly `.a().b()`'s
399
+ * JS semantics — no expression-tree fusion needed (#2696 review). */
400
+ const CALLBACK_EVAL_FUNC: Record<string, string> = { map: 'bf.MapEval', filter: 'bf.FilterEval' }
401
+
393
402
  /**
394
- * Lower a `.map(cb).join(sep)` chain (matched by {@link matchMapJoinChain})
395
- * to a `bf.Join(bf.MapEval(<items>, "<projJSON>", "<param>", <envMap>),
396
- * <sep>)` Go expression the constructor-source analogue of
397
- * `matchFilterArmMemo`'s `bf.FilterEval` emit (`memo-compute.ts`), reusing
398
- * the SAME two runtime evaluator functions (`MapEval`, `eval.go`; `Join`,
399
- * `bf.go`) the template-position `bf_map_eval`/`bf_join` lowering already
400
- * calls. Shared by a memo's derived value (`memoInitialFromParsedBody`'s
401
- * concatenation-chain arm) and a SIGNAL's own initializer
402
- * (`convertInitialValue`'s `string` branch, #2492).
403
+ * Lower ONE `.map(cb)`/`.filter(cb)` step to a runtime-evaluator call over an
404
+ * already-resolved `itemsGo` Go expression: `bf.MapEval(<itemsGo>,
405
+ * "<projJSON>", "<param>", <envMap>)` (or `bf.FilterEval` for `.filter`).
406
+ * Shared by {@link resolveMapChainAsGo} (an intermediate step feeding the
407
+ * NEXT step's `items`) and {@link mapJoinChainToGo} (the outermost step,
408
+ * whose result feeds `bf.Join`).
403
409
  *
404
- * @returns the Go expression, or null when the receiver doesn't resolve
405
- * ({@link resolveMapJoinBaseAsGo}), the projection body isn't
406
- * representable to the runtime evaluator (`serializeParsedExpr` refusal),
407
- * a captured free variable doesn't resolve, or the separator isn't a
408
- * string literal (a dynamic separator — out of scope for this arm).
410
+ * @returns the Go expression, or null when the callback body isn't
411
+ * representable to the runtime evaluator (`serializeParsedExpr` refusal)
412
+ * or a captured free variable doesn't resolve.
409
413
  */
410
- export function mapJoinChainToGo(
414
+ function callbackStepToGo(
411
415
  ctx: GoEmitContext,
412
- chain: { object: ParsedExpr; arrow: Extract<ParsedExpr, { kind: 'arrow' }>; sepArg?: ParsedExpr },
416
+ method: string,
417
+ itemsGo: string,
418
+ arrow: Extract<ParsedExpr, { kind: 'arrow' }>,
413
419
  signals: { getter: string; initialValue: string; type?: TypeInfo; parsed?: ParsedExpr }[],
414
420
  propsParams: { name: string; sourceName?: string }[],
415
421
  propFallbackVars: ReadonlyMap<string, PropFallbackVar>,
416
422
  ): string | null {
417
- const itemsGo = resolveMapJoinBaseAsGo(ctx, chain.object, signals, propsParams)
418
- if (itemsGo === null) return null
423
+ const evalFunc = CALLBACK_EVAL_FUNC[method]
424
+ if (!evalFunc) return null
419
425
 
420
426
  const knownGetterNames = new Set(signals.map(s => s.getter))
421
- const materialized = materializeGetterCalls(chain.arrow.body, knownGetterNames)
427
+ const materialized = materializeGetterCalls(arrow.body, knownGetterNames)
422
428
  const projJSON = serializeParsedExpr(materialized)
423
429
  if (projJSON === null) return null
424
430
 
425
- const paramName = chain.arrow.params[0] ?? '_'
426
- const freeVars = freeVarsInBody(materialized, new Set(chain.arrow.params))
431
+ const paramName = arrow.params[0] ?? '_'
432
+ const freeVars = freeVarsInBody(materialized, new Set(arrow.params))
427
433
  const envEntries: string[] = []
428
434
  for (const name of freeVars) {
429
435
  const sig = signals.find(s => s.getter === name)
@@ -440,6 +446,74 @@ export function mapJoinChainToGo(
440
446
  }
441
447
  const envMap = `map[string]any{${envEntries.join(', ')}}`
442
448
 
449
+ return `${evalFunc}(${itemsGo}, "${escapeGoString(projJSON)}", ${JSON.stringify(paramName)}, ${envMap})`
450
+ }
451
+
452
+ /**
453
+ * Resolve a `.map()`/`.filter()` CHAIN of any depth to a Go expression:
454
+ * peel off callback-method layers one at a time (`asCallbackMethodCall`),
455
+ * composing a `bf.MapEval`/`bf.FilterEval` call around the PREVIOUS layer's
456
+ * result for each one, down to the base receiver
457
+ * ({@link resolveMapJoinBaseAsGo} — a signal call, prop field, or array
458
+ * literal). `rows().map(t => ({ id: t.id, … })).map(r => r.id)` composes as
459
+ * `bf.MapEval(bf.MapEval(<rows>, "{id:t.id,…}", "t", {}), "r.id", "r", {})`
460
+ * — before this, only a chain whose FIRST layer already matched a base
461
+ * shape resolved; a nested map/filter layer silently fell to `null` (#2696
462
+ * review: `map-object-literal-body`'s two chained `.map()`s).
463
+ *
464
+ * @returns the Go expression, or null when the base receiver doesn't
465
+ * resolve or any layer's callback isn't representable
466
+ * ({@link callbackStepToGo}).
467
+ */
468
+ function resolveMapChainAsGo(
469
+ ctx: GoEmitContext,
470
+ expr: ParsedExpr,
471
+ signals: { getter: string; initialValue: string; type?: TypeInfo; parsed?: ParsedExpr }[],
472
+ propsParams: { name: string; sourceName?: string }[],
473
+ propFallbackVars: ReadonlyMap<string, PropFallbackVar>,
474
+ ): string | null {
475
+ const cb = asCallbackMethodCall(expr)
476
+ if (cb && cb.method in CALLBACK_EVAL_FUNC) {
477
+ const innerGo = resolveMapChainAsGo(ctx, cb.object, signals, propsParams, propFallbackVars)
478
+ if (innerGo === null) return null
479
+ return callbackStepToGo(ctx, cb.method, innerGo, cb.arrow, signals, propsParams, propFallbackVars)
480
+ }
481
+ return resolveMapJoinBaseAsGo(ctx, expr, signals, propsParams)
482
+ }
483
+
484
+ /**
485
+ * Lower a `.map(cb).join(sep)` chain (matched by {@link matchMapJoinChain})
486
+ * to a `bf.Join(<chain>, <sep>)` Go expression, where `<chain>` is the
487
+ * chain's OWN `.map()` step composed by {@link callbackStepToGo} over
488
+ * whatever {@link resolveMapChainAsGo} resolves its receiver to (a base
489
+ * shape, or one or more nested `.map()`/`.filter()` layers) — the
490
+ * constructor-source analogue of `matchFilterArmMemo`'s `bf.FilterEval`
491
+ * emit (`memo-compute.ts`), reusing the SAME runtime evaluator functions
492
+ * (`MapEval`/`FilterEval`, `eval.go`; `Join`, `bf.go`) the template-position
493
+ * `bf_map_eval`/`bf_join` lowering already calls. Shared by a memo's
494
+ * derived value (`memoInitialFromParsedBody`'s concatenation-chain arm and
495
+ * bare-chain arm) and a SIGNAL's own initializer (`convertInitialValue`'s
496
+ * `string` branch, #2492).
497
+ *
498
+ * @returns the Go expression, or null when the receiver doesn't resolve
499
+ * ({@link resolveMapChainAsGo}), the projection body isn't representable
500
+ * to the runtime evaluator (`serializeParsedExpr` refusal), a captured
501
+ * free variable doesn't resolve, or the separator isn't a string literal
502
+ * (a dynamic separator — out of scope for this arm).
503
+ */
504
+ export function mapJoinChainToGo(
505
+ ctx: GoEmitContext,
506
+ chain: { object: ParsedExpr; arrow: Extract<ParsedExpr, { kind: 'arrow' }>; sepArg?: ParsedExpr },
507
+ signals: { getter: string; initialValue: string; type?: TypeInfo; parsed?: ParsedExpr }[],
508
+ propsParams: { name: string; sourceName?: string }[],
509
+ propFallbackVars: ReadonlyMap<string, PropFallbackVar>,
510
+ ): string | null {
511
+ const itemsGo = resolveMapChainAsGo(ctx, chain.object, signals, propsParams, propFallbackVars)
512
+ if (itemsGo === null) return null
513
+
514
+ const mapGo = callbackStepToGo(ctx, 'map', itemsGo, chain.arrow, signals, propsParams, propFallbackVars)
515
+ if (mapGo === null) return null
516
+
443
517
  let sepGo: string
444
518
  if (!chain.sepArg) {
445
519
  sepGo = JSON.stringify(',')
@@ -449,5 +523,5 @@ export function mapJoinChainToGo(
449
523
  return null
450
524
  }
451
525
 
452
- return `bf.Join(bf.MapEval(${itemsGo}, "${escapeGoString(projJSON)}", ${JSON.stringify(paramName)}, ${envMap}), ${sepGo})`
526
+ return `bf.Join(${mapGo}, ${sepGo})`
453
527
  }
@@ -52,4 +52,11 @@ 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
56
+ // position (e.g. `header={cond ? <a/> : <b/>}`) is refused ahead of
57
+ // `adapter.generate()` in the shared jsx-to-ir.ts phase, so it is pinned
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' }],
55
62
  }
@@ -17,4 +17,9 @@
17
17
 
18
18
  import type { RenderDivergences } from '@barefootjs/jsx'
19
19
 
20
- export const renderDivergences: RenderDivergences = {}
20
+ export const renderDivergences: RenderDivergences = {
21
+ 'jsx-element-prop-fragment-conditional':
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
+ 'signal-object-spread-init':
24
+ '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).',
25
+ }