@barefootjs/go-template 0.33.4 → 0.34.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.
@@ -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)
@@ -76,4 +76,23 @@ export const conformancePins: ConformancePins = {
76
76
  issue: 'https://github.com/piconic-ai/barefootjs/issues/2805',
77
77
  unescapable: { issue: 'https://github.com/piconic-ai/barefootjs/issues/2805' },
78
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' }],
79
98
  }
@@ -23,6 +23,16 @@
23
23
  * Go, so it moved off this table. Dynamic delivery for named jsx-children
24
24
  * props (the actual capability gap) is tracked separately at
25
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.)
26
36
  */
27
37
 
28
38
  import type { RenderDivergences } from '@barefootjs/jsx'
@@ -30,10 +40,6 @@ import type { RenderDivergences } from '@barefootjs/jsx'
30
40
  export const renderDivergences: RenderDivergences = {
31
41
  'children-passthrough-renamed':
32
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.',
33
- 'signal-object-spread-init':
34
- '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).',
35
- 'textarea-row-breakout':
36
- '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.',
37
- 'nested-loop-ref-const':
38
- 'A signal-backed object array whose elements have a NESTED array-of-objects field (`children: [{...}]`, producing this fixture\'s depth-2 `.map()`) bakes to `nil` in `New<Component>Props`, leaving the whole `{{range .Items}}` body empty on real Go — `synthesizeStructFromSignal` (`go-template-adapter.ts`) only synthesizes a struct when EVERY property value is a scalar literal (`scalarParsedGoType`), so a `children` array property aborts synthesis for the entire signal; `parsedLiteralToGo` then has no named struct to bake an object-literal element against and falls through to `nil`. Reproduced identically with #2750\'s fix reverted, confirming this predates and is independent of #2750 (which only touches client-JS reachability, never SSR baking) — every other adapter (confirmed: Hono) renders the nested loop correctly. Tracked at https://github.com/piconic-ai/barefootjs/issues/2800; graduate by teaching `synthesizeStructFromSignal` to recursively synthesize a nested struct for an array-valued property instead of bailing to `null`.',
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.',
39
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