@barefootjs/go-template 0.29.0 → 0.30.2

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.
@@ -801,3 +801,128 @@ function propsAccessNameFromParsed(ctx: GoEmitContext, node: ParsedExpr): string
801
801
  if (!ctx.state.propsObjectName || node.object.name !== ctx.state.propsObjectName) return null
802
802
  return node.property
803
803
  }
804
+
805
+ /**
806
+ * #2448: every INPUT prop name a constructor-evaluated `parsed` expression
807
+ * reads, either via a `<propsObjectName>.<name>` member access (object-props
808
+ * signature, e.g. `props.n`) or a bare identifier bound to a destructured
809
+ * prop (no `propsObjectName`).
810
+ *
811
+ * Two kinds of expression reach here, because `New<Comp>Props` bakes BOTH
812
+ * into the struct at construction time: a `createMemo` body and a
813
+ * `createSignal` INITIAL VALUE (`const [dbl] = createSignal(props.n)` emits
814
+ * `Dbl: in.N`, exactly as `createMemo(() => props.n * 2)` emits
815
+ * `Dbl: in.N * 2`). Either one goes stale under a per-row override, so both
816
+ * feed the dependency map.
817
+ *
818
+ * Feeds `GoTemplateAdapter.childDerivedFieldDeps` (built by
819
+ * `recordDerivedFieldDeps`, `go-template-adapter.ts`) so a parent overriding
820
+ * one of THESE props per row (`bf_with_props`, #2445) can be refused loudly
821
+ * instead of silently leaving the derived field stale — see that map's
822
+ * docstring.
823
+ *
824
+ * Structural counterpart of {@link freeVarsInBody}: that walk reports a
825
+ * member's OBJECT identifier only (`props`), treating the property name as
826
+ * non-referential (fine for its own free-var-substitution purpose). Here the
827
+ * PROPERTY name is exactly what's wanted, so a `member` node contributes it
828
+ * once its object resolves to the props binding — walking stops at that
829
+ * first `propsObjectName.<name>` hop, so a deeper chain (`props.a.b`) still
830
+ * contributes only its BASE prop `a` (mirrors `collectPropRefs` in
831
+ * `ssr-defaults.ts`, the same first-level-only rule, for the same reason: an
832
+ * adapter that later re-derives a nested field still needs the base field
833
+ * seeded).
834
+ *
835
+ * This is a best-effort STRUCTURAL walk over the child's OWN analysis-time
836
+ * `parsed` tree — not a re-derivation of the Go initializer text, which
837
+ * isn't available yet at registration time (the cross-file shape pre-pass,
838
+ * #2131, runs before any component's codegen).
839
+ */
840
+ export function collectPropsReadByCtorInit(
841
+ body: ParsedExpr,
842
+ propsObjectName: string | null,
843
+ propNames: ReadonlySet<string>,
844
+ ): Set<string> {
845
+ const found = new Set<string>()
846
+ const visit = (e: ParsedExpr, bound: ReadonlySet<string>): void => {
847
+ switch (e.kind) {
848
+ case 'identifier':
849
+ // Destructured signature only: a bare identifier bound to a
850
+ // destructured prop param IS the reference (`(props) => props.n`
851
+ // has no bare `n` — that shape is the `member` branch below).
852
+ if (!propsObjectName && propNames.has(e.name) && !bound.has(e.name)) found.add(e.name)
853
+ return
854
+ case 'member':
855
+ if (
856
+ propsObjectName &&
857
+ !e.computed &&
858
+ e.object.kind === 'identifier' &&
859
+ e.object.name === propsObjectName
860
+ ) {
861
+ found.add(e.property)
862
+ return
863
+ }
864
+ visit(e.object, bound)
865
+ return
866
+ case 'index-access':
867
+ visit(e.object, bound)
868
+ visit(e.index, bound)
869
+ return
870
+ case 'binary':
871
+ case 'logical':
872
+ visit(e.left, bound)
873
+ visit(e.right, bound)
874
+ return
875
+ case 'unary':
876
+ visit(e.argument, bound)
877
+ return
878
+ case 'conditional':
879
+ visit(e.test, bound)
880
+ visit(e.consequent, bound)
881
+ visit(e.alternate, bound)
882
+ return
883
+ case 'call':
884
+ visit(e.callee, bound)
885
+ e.args.forEach(a => visit(a, bound))
886
+ return
887
+ case 'template-literal':
888
+ for (const p of e.parts) if (p.type === 'expression') visit(p.expr, bound)
889
+ return
890
+ case 'array-literal':
891
+ e.elements.forEach(el => visit(el, bound))
892
+ return
893
+ case 'object-literal':
894
+ for (const p of e.properties) visit(p.value, bound)
895
+ return
896
+ case 'array-method':
897
+ visit(e.object, bound)
898
+ e.args.forEach(a => visit(a, bound))
899
+ if (e.method === 'flat' && e.depthExpr) visit(e.depthExpr, bound)
900
+ return
901
+ case 'arrow': {
902
+ const inner = e.params.length === 0 ? bound : new Set([...bound, ...e.params])
903
+ visit(e.body, inner)
904
+ return
905
+ }
906
+ // Non-referential leaves — nothing to collect.
907
+ case 'literal':
908
+ case 'regex':
909
+ case 'unsupported':
910
+ return
911
+ default: {
912
+ // Exhaustiveness pin. A NEW `ParsedExpr` kind that lands without a
913
+ // case here would silently contribute no dependencies, and this
914
+ // walk's whole job is to answer "does this memo read that prop?" —
915
+ // a missed dependency is a MISSED REFUSAL, i.e. the silently-stale
916
+ // derived field #2448 exists to prevent. Fail the build instead:
917
+ // `never` makes the omission a compile error at the point the kind
918
+ // is added, the same drift defence `PARSED_EXPR_KINDS` gives the
919
+ // registry (`expression-parser.ts`).
920
+ const _exhaustive: never = e
921
+ void _exhaustive
922
+ return
923
+ }
924
+ }
925
+ }
926
+ visit(body, new Set())
927
+ return found
928
+ }
@@ -15,6 +15,48 @@ import type { TypeInfo } from '@barefootjs/jsx'
15
15
 
16
16
  import type { GoEmitContext } from '../emit-context.ts'
17
17
 
18
+ /**
19
+ * Collapse a homogeneous LITERAL union (`'a' | 'b'`, `1 | 2`, `true | false`)
20
+ * to the primitive that backs it, so a variant-typed signal or prop
21
+ * (`createSignal<'a' | 'b'>('a')`, the `{ variant?: 'a' | 'b' }` prop shape)
22
+ * gets a real Go type instead of `interface{}` — and, downstream in
23
+ * `convertInitialValue`, a real seed instead of `nil`. #2477's Go leg: the
24
+ * analyzer maps an explicit literal-union type argument to
25
+ * `{kind:'union'}` of literal members, and with no
26
+ * `union` arm here OR in `convertInitialValue` the field fell to
27
+ * `interface{}` and the seed to `nil` — which the child's `string` field
28
+ * then rejected at `go run` time (`cannot use nil as string value`).
29
+ *
30
+ * Only a union whose EVERY member is a literal of ONE primitive family
31
+ * (string / number / boolean; a same-family primitive keyword member like
32
+ * `'a' | string` also counts) collapses. Anything else — mixed families,
33
+ * `null` / `undefined` members, object members — returns the input
34
+ * unchanged and keeps today's `interface{}` fallback, so the collapse can
35
+ * never widen what Go accepts, only type what it already receives.
36
+ */
37
+ export function collapseLiteralUnion(typeInfo: TypeInfo): TypeInfo {
38
+ if (typeInfo.kind !== 'union' || !typeInfo.unionTypes || typeInfo.unionTypes.length === 0) {
39
+ return typeInfo
40
+ }
41
+ // Purely structural: `typeNodeToTypeInfo` lowers a literal member to
42
+ // `kind: 'primitive'` + `literalValue`, so the member's family is just
43
+ // its `primitive` — no re-parsing of `raw` (the first cut of this
44
+ // helper regexed the member text because the analyzer had no literal
45
+ // arm; that gap is closed at the source).
46
+ const familyOf = (m: TypeInfo): 'string' | 'number' | 'boolean' | null => {
47
+ if (m.kind !== 'primitive') return null
48
+ return m.primitive === 'string' || m.primitive === 'number' || m.primitive === 'boolean'
49
+ ? m.primitive
50
+ : null
51
+ }
52
+ const first = familyOf(typeInfo.unionTypes[0])
53
+ if (!first) return typeInfo
54
+ for (const m of typeInfo.unionTypes) {
55
+ if (familyOf(m) !== first) return typeInfo
56
+ }
57
+ return { kind: 'primitive', raw: typeInfo.raw, primitive: first }
58
+ }
59
+
18
60
  /**
19
61
  * Convert a `TypeInfo` to a Go type string.
20
62
  *
@@ -27,9 +69,10 @@ import type { GoEmitContext } from '../emit-context.ts'
27
69
  */
28
70
  export function typeInfoToGo(
29
71
  ctx: GoEmitContext,
30
- typeInfo: TypeInfo,
72
+ _typeInfo: TypeInfo,
31
73
  defaultValue?: string,
32
74
  ): string {
75
+ const typeInfo = collapseLiteralUnion(_typeInfo)
33
76
  switch (typeInfo.kind) {
34
77
  case 'primitive':
35
78
  switch (typeInfo.primitive) {
@@ -11,6 +11,7 @@ import type { GoEmitContext } from '../emit-context.ts'
11
11
  import type { PropFallbackVar } from '../lib/types.ts'
12
12
  import { capitalizeFieldName } from '../lib/go-naming.ts'
13
13
  import { parsedLiteralToGo } from './parsed-literal-to-go.ts'
14
+ import { collapseLiteralUnion } from '../type/type-codegen.ts'
14
15
 
15
16
  /** Default for `getSignalInitialValueAsGo`'s optional fallback-var map. */
16
17
  const EMPTY_PROP_FALLBACK_VARS: ReadonlyMap<string, PropFallbackVar> = new Map()
@@ -61,10 +62,14 @@ function nillableAwarePropRef(ctx: GoEmitContext, propName: string, expectedType
61
62
  export function convertInitialValue(
62
63
  ctx: GoEmitContext,
63
64
  value: string,
64
- typeInfo: TypeInfo,
65
+ _typeInfo: TypeInfo,
65
66
  propsParams?: { name: string }[],
66
67
  preParsed?: ParsedExpr,
67
68
  ): string {
69
+ // Literal unions collapse to their backing primitive the same way
70
+ // `typeInfoToGo` collapses the field's type — the two MUST agree, or a
71
+ // `string` field gets a `nil` seed (#2477's `go run` failure).
72
+ const typeInfo = collapseLiteralUnion(_typeInfo)
68
73
  const propRef = (propName: string): string => nillableAwarePropRef(ctx, propName, typeInfo)
69
74
 
70
75
  if (/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(value)) {
@@ -15,11 +15,6 @@ export const conformancePins: ConformancePins = {
15
15
  // JS-runtime target runs it, a DSL adapter surfaces BF021 + `/* @client */`.
16
16
  // See spec/callback-fidelity.md.
17
17
  'filter-typeof-predicate': [{ code: 'BF021', severity: 'error' }],
18
- // A `.map()` body with a `const`/`let` preamble before its branches:
19
- // a JS runtime folds it, a DSL adapter can't carry the loop-local into a
20
- // conditional branch template, so it refuses with BF021 + `/* @client */`.
21
- // See spec/callback-fidelity.md.
22
- 'map-preamble-branch-body': [{ code: 'BF021', severity: 'error' }],
23
18
  'map-array-builder-body': [{ code: 'BF021', severity: 'error' }],
24
19
  'map-array-builder-escaping': [{ code: 'BF021', severity: 'error' }],
25
20
  // `.fill(value)` mutates the receiver in place — no template lowering
@@ -30,13 +30,28 @@ export const renderDivergences: RenderDivergences = {
30
30
  // `test-render.ts`) now replicates that documented contract for a
31
31
  // signal-backed dynamic child-component loop.
32
32
 
33
- // #2445: a child component nested inside a dynamic loop row whose root
34
- // is a plain element gets ONE hoisted `BadgeSlot0 BadgeProps` field on
35
- // the parent, built outside the loop with no per-row data, and the
36
- // template passes `$.BadgeSlot0` for every row so every row renders
37
- // the child with zero-value props. The sibling shape (row root IS the
38
- // child component) already emits a per-row slice; see the `todo-app-ssr`
39
- // note above on `.TodoItems []TodoItemProps`.
40
- 'composite-row-child-component':
41
- 'a child component nested inside a dynamic loop row receives one hoisted props value built outside `{{range}}`, so every row renders the child with zero-value props (https://github.com/piconic-ai/barefootjs/issues/2445)',
33
+ // Onboarding TSX-fidelity fixtures (PR #2461): `expectedHtml` is
34
+ // hand-authored to the CORRECT output because the emission bug lives in
35
+ // the shared compiler layer every adapter, including the Hono
36
+ // reference, currently emits the broken form (verified against this
37
+ // adapter's emitted template; see each fixture's docstring). Graduate
38
+ // by fixing the shared emission, regenerating `expectedHtml` from the
39
+ // fixed reference, and deleting these lines (and the matching hono
40
+ // `skipJsx` entries).
41
+ 'aliased-destructured-prop':
42
+ 'aliased destructured prop `{ n: count }` loses its rename — the Input struct field is Count `json:"count"`, so the caller-side struct literal keyed by the real prop name fails `go run` outright (unknown field N, exit 1) (https://github.com/piconic-ai/barefootjs/issues/2460)',
43
+
44
+ // #2482 audit follow-ups: loop-scope holes specific to this adapter's
45
+ // four-stack scope tracking and its SSR seeding. Graduate by applying
46
+ // the fix described in each issue and deleting the line.
47
+ 'loop-destructured-param-condition':
48
+ 'a destructured .map() param binding used as a row ternary CONDITION emits the root-scope `{{if $.Active}}` — `renderConditionExpr` omits `loopBindingStack`, unlike `identifierToGoRef` (text positions resolve the same binding correctly) (https://github.com/piconic-ai/barefootjs/issues/2486)',
49
+ 'nested-loop-tail-content':
50
+ 'outer-row content AFTER a nested inner loop renders through non-loop arms (spread lowers to the component-root `.Spread_0`) — `inLoop` is cleared, not restored, by the inner loop\'s exit; the same content BEFORE the inner loop emits correctly (https://github.com/piconic-ai/barefootjs/issues/2487)',
51
+ 'loop-param-shadows-spread-const':
52
+ 'spreading a loop row object mangles attribute names (`id` → `-i-d`, `title` → `-title`); the spread VALUE is correctly row-scoped, distinguishing this from the template-adapter const-shadow hole #2489 (https://github.com/piconic-ai/barefootjs/issues/2490)',
53
+ 'loop-param-shadows-record-template-span':
54
+ 'a dynamic-key element access on a loop row (`tone[k]`) renders empty at execute time — the emitted template contains no baked const (correct post-fix), but the row lookup resolves to nothing (https://github.com/piconic-ai/barefootjs/issues/2491)',
55
+ 'callback-param-shadows-prop':
56
+ 'JS-computed signal/memo initializers (`[…].map(…).join(…)`, memo over signal + prop) don\'t seed Go SSR — renders `[]` / empty where every other adapter renders the computed value; hydration snaps to correct (https://github.com/piconic-ai/barefootjs/issues/2492)',
42
57
  }