@barefootjs/go-template 0.31.10 → 0.33.0

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.
Files changed (33) hide show
  1. package/dist/adapter/emit-context.d.ts +16 -0
  2. package/dist/adapter/emit-context.d.ts.map +1 -1
  3. package/dist/adapter/expr/helper-inline.d.ts.map +1 -1
  4. package/dist/adapter/go-template-adapter.d.ts +75 -1
  5. package/dist/adapter/go-template-adapter.d.ts.map +1 -1
  6. package/dist/adapter/index.js +165 -15
  7. package/dist/adapter/lib/types.d.ts +13 -0
  8. package/dist/adapter/lib/types.d.ts.map +1 -1
  9. package/dist/adapter/memo/memo-compute.d.ts.map +1 -1
  10. package/dist/adapter/memo/memo-value.d.ts.map +1 -1
  11. package/dist/adapter/memo/template-interp.d.ts.map +1 -1
  12. package/dist/adapter/props/prop-types.d.ts.map +1 -1
  13. package/dist/adapter/spread/spread-codegen.d.ts.map +1 -1
  14. package/dist/adapter/value/parsed-literal-to-go.d.ts.map +1 -1
  15. package/dist/adapter/value/value-lowering.d.ts +15 -12
  16. package/dist/adapter/value/value-lowering.d.ts.map +1 -1
  17. package/dist/index.js +166 -17
  18. package/dist/render-divergences.d.ts.map +1 -1
  19. package/dist/vite.js +259 -52
  20. package/package.json +5 -5
  21. package/src/__tests__/go-template-adapter.test.ts +246 -6
  22. package/src/adapter/emit-context.ts +14 -0
  23. package/src/adapter/expr/helper-inline.ts +4 -2
  24. package/src/adapter/go-template-adapter.ts +296 -9
  25. package/src/adapter/lib/types.ts +10 -0
  26. package/src/adapter/memo/memo-compute.ts +12 -1
  27. package/src/adapter/memo/memo-value.ts +3 -0
  28. package/src/adapter/memo/template-interp.ts +5 -0
  29. package/src/adapter/props/prop-types.ts +26 -1
  30. package/src/adapter/spread/spread-codegen.ts +8 -0
  31. package/src/adapter/value/parsed-literal-to-go.ts +7 -0
  32. package/src/adapter/value/value-lowering.ts +96 -22
  33. package/src/render-divergences.ts +2 -23
@@ -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
  }
@@ -18,27 +18,6 @@
18
18
  import type { RenderDivergences } from '@barefootjs/jsx'
19
19
 
20
20
  export const renderDivergences: RenderDivergences = {
21
- // #2683: the props-struct emitter (`go-template-adapter.ts`) skips a
22
- // signal whose Go field name collides with a prop field, keyed purely on
23
- // the NAME collision — never on whether `extractPropFallback` actually
24
- // matched a supported `props.x ?? <default>` shape. For a non-idempotent
25
- // derivation (`createSignal((props.count ?? 1) * 2)`) the fallback
26
- // extractor correctly declines to fold `* 2` into the struct default, but
27
- // the `continue` fires anyway on the name match alone, so the emitted
28
- // struct field silently drops the `* 2` and the signal's initial value
29
- // renders as the raw prop instead of its derived value. Not a one-liner:
30
- // two Go struct fields can't share an identifier, so simply removing the
31
- // skip emits a duplicate field — the real fix needs its own PR.
32
- 'signal-prop-same-name-derived':
33
- 'self-derived signal collides with its prop field name in the generated Go props struct — the non-idempotent `* 2` derivation is dropped and the signal renders the raw prop value instead (https://github.com/piconic-ai/barefootjs/issues/2683)',
34
- // #2685 review: same #2683 bug, one hop of const indirection removed —
35
- // the props-struct field-name collision is keyed on the SIGNAL's name,
36
- // not on how its initializer reaches the prop, so
37
- // `const mid = props.count; createSignal((mid ?? 1) * 2)` collides
38
- // exactly like the direct-access form above. This is go's PRE-EXISTING
39
- // #2683 defect surfacing through a new fixture, not a regression from
40
- // the #2685 review fix (which lands correctly on every other
41
- // template-stash adapter — see those adapters' conformance runs).
42
- 'signal-prop-same-name-via-const-derived':
43
- 'self-derived signal (reached through a component-scope const) collides with its prop field name in the generated Go props struct — the non-idempotent `* 2` derivation is dropped and the signal renders the raw prop value instead (https://github.com/piconic-ai/barefootjs/issues/2683)',
21
+ 'signal-object-spread-init':
22
+ '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).',
44
23
  }