@barefootjs/go-template 0.33.3 → 0.33.6

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.
@@ -38,6 +38,24 @@ export class CompileState {
38
38
  * that's resolvable and non-colliding. */
39
39
  referencedDerivedConsts: Set<string> = new Set()
40
40
 
41
+ /**
42
+ * Root-scope field names (signal getters, memo names, props, derived
43
+ * consts) the SSR template actually READ while rendering — recorded by
44
+ * `rootFieldRef`, the single door every such read passes through
45
+ * (`identifier`/`call`/`member`'s zero-arg-getter and bare-name lowering,
46
+ * plus the typed-expression emitter). `generateTypes` (#2700) consults
47
+ * this AFTER `generate()` has rendered the template to decide whether a
48
+ * signal whose object-literal initializer the constructor-time baker
49
+ * can't reproduce (`convertInitialValue` returning null) needs a loud
50
+ * BF101 refusal: a deferred bake that is NEVER read by the template (e.g.
51
+ * only feeds a spread-attrs bag, which routes through its own
52
+ * `.Spread_<slot>` field, never this set) silently keeping its Go zero
53
+ * value is harmless, so refusing it would be a false positive — see
54
+ * `refuseUnbakeableDerivedObjectLiteral`'s docstring for why the check is
55
+ * call-site-aware instead of firing at the bake site itself.
56
+ */
57
+ templateReadRootFields: Set<string> = new Set()
58
+
41
59
  templateVarCounter: number = 0
42
60
 
43
61
  /**
@@ -28,13 +28,17 @@ import { goFieldNameForKey } from '../lib/go-naming.ts'
28
28
  import { typeInfoToGo } from '../type/type-codegen.ts'
29
29
 
30
30
  /**
31
- * Look up a struct property's declared `TypeInfo` by source key, from the
32
- * user's own `TypeDefinition` (not a synthesized struct those only arise
33
- * from an UNTYPED literal, which never carries nested object/array elements
34
- * because `synthesizeStructFromSignal` requires every property value to be a
35
- * scalar literal). Returns undefined when the struct name isn't a user type
36
- * or the key isn't declared callers treat that as "nested type unknown"
37
- * and fall back to the generic/inline lowering.
31
+ * Look up a struct property's declared `TypeInfo` by source key. Resolves
32
+ * against ANY registered `TypeDefinition` a user-written type, a #2674
33
+ * anonymous-object synthesis, or (#2800) an untyped-literal signal struct
34
+ * synthesized by `synthesizeStructFromSignal`, which pushes one
35
+ * `TypeDefinition` per struct (top-level and nested) via the same
36
+ * `registerSynthStruct` door #2674 uses, specifically so a nested
37
+ * array-of-objects field's declared element type is findable here exactly
38
+ * like a user-declared nested property's is. Returns undefined when the
39
+ * struct name isn't registered or the key isn't declared — callers treat
40
+ * that as "nested type unknown" and fall back to the generic/inline
41
+ * lowering.
38
42
  */
39
43
  function structPropertyType(ctx: GoEmitContext, structGoType: string, key: string): TypeInfo | undefined {
40
44
  const td = ctx.state.currentTypeDefinitions.find((t: TypeDefinition) => t.name === structGoType)
@@ -94,6 +94,23 @@ 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
100
+ // inference is text-shaped and never chases an identifier to its
101
+ // declaration — so none of the typed branches below ever see it and
102
+ // this used to fall through to the final `nil`. Checked AFTER the
103
+ // prop lookup so a destructured prop that happens to share the
104
+ // const's name still wins (shadowing). Both resolvers return null
105
+ // for anything that isn't a statically-inlinable module const
106
+ // (a call-initialized const, a loop var, a component-scope const),
107
+ // leaving that case on the pre-existing `nil` path unchanged.
108
+ const inlinedStr = ctx.resolveModuleStringConst(value)
109
+ 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
97
114
  }
98
115
 
99
116
  const propName = ctx.extractPropNameFromInitialValue(value, preParsed)
@@ -59,4 +59,40 @@ export const conformancePins: ConformancePins = {
59
59
  // `rich-prop-client-read` above.
60
60
  'jsx-element-prop-ternary': [{ code: 'BF021', severity: 'error', issue: 'https://github.com/piconic-ai/barefootjs/issues/2667' }],
61
61
  'jsx-element-prop-array': [{ code: 'BF021', severity: 'error', issue: 'https://github.com/piconic-ai/barefootjs/issues/2667' }],
62
+ // `jsx-element-prop-fragment-conditional` (#2703) is NOT pinned here — it
63
+ // renders correctly since `queueDynamicPropDefine` (go-template-adapter.ts)
64
+ // extended the reserved `children` slot's `bf_with_children`/`bf_tmpl`
65
+ // dynamic-delivery route to named jsx-children props.
66
+ // #2805: a named jsx-children prop routed into the child's rest bag (no
67
+ // declared param — only reachable via the child's `...rest` spread) has
68
+ // no named Go struct field for `bf_with_props`/`WithProps` to target at
69
+ // all. `queueDynamicPropDefine` refuses loudly with BF101 rather than
70
+ // silently no-op through `WithProps`'s unmatched-field passthrough.
71
+ // `unescapable`: the capability gap (a rest-bag delivery route) is #2805
72
+ // itself, not authored yet, so no `/* @client */` escape twin exists.
73
+ 'jsx-element-prop-rest-bag-dynamic': [{
74
+ code: 'BF101',
75
+ severity: 'error',
76
+ issue: 'https://github.com/piconic-ai/barefootjs/issues/2805',
77
+ unescapable: { issue: 'https://github.com/piconic-ai/barefootjs/issues/2805' },
78
+ }],
79
+ // #2700: a `derived` signal/memo (non-empty free set) seeded from an
80
+ // object literal the constructor-time baker can't reproduce (identifier/
81
+ // member/call operands defer, `parsed-literal-to-go.ts`) now refuses
82
+ // loudly instead of silently keeping the Go zero value — this fixture's
83
+ // `merged().id` / `merged().done` reads are exactly that shape. A working
84
+ // `/* @client */` escape twin exists (`signal-object-spread-init-client`),
85
+ // verified to render correctly, so no `unescapable`.
86
+ 'signal-object-spread-init': [{
87
+ code: 'BF101',
88
+ severity: 'error',
89
+ issue: 'https://github.com/piconic-ai/barefootjs/issues/2700',
90
+ }],
91
+ // #2771: a reactive primitive invoked through a namespace import
92
+ // (`import * as bf from '@barefootjs/client'`, `bf.createSignal(...)`)
93
+ // that the analyzer's checker-less fast path cannot recognize refuses
94
+ // loudly (BF013) instead of silently dropping the declaration — fired
95
+ // 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' }],
62
98
  }
@@ -13,17 +13,33 @@
13
13
  * the same way it already seeded a signal-backed dynamic one: the adapter's
14
14
  * own `emission` was never the bug, only this harness's route-handler
15
15
  * stand-in was missing the prop-derived case.)
16
+ *
17
+ * (#2703's `jsx-element-prop-fragment-conditional` divergence graduated by
18
+ * reclassification, not a lowering fix: the underlying gap — a named
19
+ * jsx-children prop whose value can't be baked into a static Go string
20
+ * silently got no field at all, no diagnostic — is now a loud `BF101`
21
+ * refusal (see `conformance-pins.ts`) instead of a silent wrong render.
22
+ * "Compiles clean but renders divergent" no longer describes this fixture on
23
+ * Go, so it moved off this table. Dynamic delivery for named jsx-children
24
+ * props (the actual capability gap) is tracked separately at
25
+ * https://github.com/piconic-ai/barefootjs/issues/2703.)
26
+ *
27
+ * (#2700's `signal-object-spread-init` divergence graduated the same way —
28
+ * by reclassification, not a lowering fix: a `derived` signal/memo's
29
+ * object-literal initializer referencing a live prop/signal has no
30
+ * live-template-expression lowering on Go, only a static constructor-time
31
+ * baker — now a loud `BF101` refusal (`conformance-pins.ts`) with a
32
+ * verified `/* @client *\/` escape twin (`signal-object-spread-init-client`),
33
+ * instead of a silent wrong render. Teaching the baker to emit
34
+ * prop-referencing Go expressions — the actual capability gap — stays
35
+ * tracked at https://github.com/piconic-ai/barefootjs/issues/2700.)
16
36
  */
17
37
 
18
38
  import type { RenderDivergences } from '@barefootjs/jsx'
19
39
 
20
40
  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
41
  'children-passthrough-renamed':
24
42
  '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.',
25
- 'signal-object-spread-init':
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.',
43
+ 'aliased-loop-source':
44
+ 'A `.map()` loop whose source is a local const alias of a signal getter (`const items__alias = items`) fails template execution on real Go (`can\'t evaluate field Items__alias in type main.AliasedLoopSourceProps`) the zero-arg-call-to-field lowering routes `items__alias()` to a `.Items__alias` struct field that was never seeded, since seeding only knows about `items`, the real signal name; the alias hop is never resolved. This is the SSR-side twin of #2778 (fixed for the CSR client-JS template in the same PR that added this fixture) that fix only touches client-JS emission, not Go\'s field-routing/seeding. Tracked at https://github.com/piconic-ai/barefootjs/issues/2813; graduate by resolving the alias hop at field-routing time using the same `resolveAliasOrigin`/`resolveGetterAliases` mechanism #2778 introduced, rather than a third alias-hop walker.',
29
45
  }
@@ -514,11 +514,10 @@ function collectImportedComponentNames(
514
514
  if (!imp.source.startsWith('.') && !childSpecifiers?.has(imp.source)) continue
515
515
  for (const spec of imp.specifiers ?? []) {
516
516
  if (spec.isNamespace) continue
517
- // Imported binding name (the local alias is what JSX references, but the
518
- // generated `{{define}}`/types are keyed by the exported component name
519
- // which equals the specifier name when un-aliased; aliased component
520
- // imports aren't exercised by the UI fixtures).
521
- names.push(spec.alias ?? spec.name)
517
+ // #2822: push `spec.name` (the child's own declared name), not
518
+ // `spec.alias ?? spec.name` `childArtifacts` is keyed by each
519
+ // child's declared name, not the caller-local JSX/import binding.
520
+ names.push(spec.name)
522
521
  }
523
522
  }
524
523
  return names
@@ -595,16 +594,34 @@ function findBaseSignalGetter(expr: ParsedExpr | undefined, signalGetters: Reado
595
594
  /**
596
595
  * (#2630) Resolve a `isPropDerived` loop's `arrayParsed` to the JS-level
597
596
  * prop name it reads. `isArrayExprDirectPropRef` (`jsx-to-ir.ts`) is what
598
- * sets `isPropDerivedArray` in the first place, and it only recognizes two
599
- * shapes a bare destructured-prop identifier (`entries.map(...)` inside
600
- * `function({ entries })`) or a direct `props.<name>` member access so
601
- * unlike `findBaseSignalGetter` there is no `call`/chain walking to do; the
602
- * compiler already restricted the shape. Returns the prop's LOCAL binding
603
- * name; the caller resolves it to the CALLER-FACING field
604
- * (`sourceName ?? name`) via `propsParams`, matching how
605
- * `generateInputStruct`/`generateNewPropsFunction` key the Go field and how
606
- * `buildGoPropsInit` keys the harness's own prop initializer (both by the
607
- * JS `props` object's key, not the local destructure binding).
597
+ * sets `isPropDerivedArray` in the first place, and even after #2724
598
+ * taught it to resolve a bare `const x = y` alias-hop chain (including one
599
+ * that reaches the WHOLE props object, e.g. `const p = props; p.items`)
600
+ * `arrayParsed` (this function's input) is `loop.array`'s own written
601
+ * structure, not the alias-resolved origin: a hop through a destructured
602
+ * prop's alias ends up here as a bare identifier (the LOCAL alias name, not
603
+ * the prop's own name — resolved below via `propsParams`), and a hop that
604
+ * lands on `props.<key>` OR `<aliasOfProps>.<key>` both end up here as the
605
+ * same member shape (`.property` is the source key either way; this
606
+ * function never inspects `.object.name`, so it doesn't matter whether the
607
+ * object identifier IS `props` or an alias of it).
608
+ *
609
+ * `renderLoop`'s own BF101 gate (`go-template-adapter.ts`, the
610
+ * `arrayName`/`localConstants` check) refuses a BARE-IDENTIFIER loop array
611
+ * that is a local computed value — including an alias-hop identifier —
612
+ * before this function would ever see one for that shape. But that gate's
613
+ * `arrayName` regex (`^[A-Za-z_$][\w$]*$`) never matches a MEMBER-ACCESS
614
+ * array text at all, so a `<propsAlias>.<key>` loop array (unlike a bare
615
+ * identifier one) is NOT screened by BF101 and can reach this function for
616
+ * real — which is fine: the member branch below resolves it correctly
617
+ * regardless, per the previous paragraph. Unlike `findBaseSignalGetter`
618
+ * there is no `call`/chain walking to do here either way — whatever hop
619
+ * resolution happened, already happened in `isArrayExprDirectPropRef`.
620
+ * Returns the prop's LOCAL binding name; the caller resolves it to the
621
+ * CALLER-FACING field (`sourceName ?? name`) via `propsParams`, matching
622
+ * how `generateInputStruct`/`generateNewPropsFunction` key the Go field and
623
+ * how `buildGoPropsInit` keys the harness's own prop initializer (both by
624
+ * the JS `props` object's key, not the local destructure binding).
608
625
  */
609
626
  function findLoopPropField(expr: ParsedExpr | undefined): string | null {
610
627
  if (!expr) return null