@barefootjs/go-template 0.18.4 → 0.18.7

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 (37) hide show
  1. package/dist/adapter/analysis/static-child-loop-bake.d.ts +61 -0
  2. package/dist/adapter/analysis/static-child-loop-bake.d.ts.map +1 -0
  3. package/dist/adapter/analysis/static-element-loop-bake.d.ts +83 -0
  4. package/dist/adapter/analysis/static-element-loop-bake.d.ts.map +1 -0
  5. package/dist/adapter/go-template-adapter.d.ts +151 -3
  6. package/dist/adapter/go-template-adapter.d.ts.map +1 -1
  7. package/dist/adapter/index.js +500 -52
  8. package/dist/adapter/lib/compile-state.d.ts +15 -0
  9. package/dist/adapter/lib/compile-state.d.ts.map +1 -1
  10. package/dist/adapter/lib/constants.d.ts.map +1 -1
  11. package/dist/adapter/memo/memo-compute.d.ts.map +1 -1
  12. package/dist/adapter/props/prop-classes.d.ts +40 -0
  13. package/dist/adapter/props/prop-classes.d.ts.map +1 -0
  14. package/dist/adapter/props/prop-types.d.ts.map +1 -1
  15. package/dist/adapter/type/type-codegen.d.ts +5 -1
  16. package/dist/adapter/type/type-codegen.d.ts.map +1 -1
  17. package/dist/adapter/value/value-lowering.d.ts.map +1 -1
  18. package/dist/build.js +500 -52
  19. package/dist/conformance-pins.d.ts.map +1 -1
  20. package/dist/index.js +503 -79
  21. package/dist/render-divergences.d.ts.map +1 -1
  22. package/dist/test-render.d.ts.map +1 -1
  23. package/package.json +3 -3
  24. package/src/__tests__/go-template-adapter.test.ts +708 -4
  25. package/src/adapter/analysis/static-child-loop-bake.ts +119 -0
  26. package/src/adapter/analysis/static-element-loop-bake.ts +211 -0
  27. package/src/adapter/go-template-adapter.ts +661 -34
  28. package/src/adapter/lib/compile-state.ts +17 -0
  29. package/src/adapter/lib/constants.ts +1 -0
  30. package/src/adapter/memo/memo-compute.ts +37 -9
  31. package/src/adapter/props/prop-classes.ts +70 -0
  32. package/src/adapter/props/prop-types.ts +69 -1
  33. package/src/adapter/type/type-codegen.ts +19 -2
  34. package/src/adapter/value/value-lowering.ts +27 -2
  35. package/src/conformance-pins.ts +30 -36
  36. package/src/render-divergences.ts +12 -30
  37. package/src/test-render.ts +131 -13
@@ -0,0 +1,119 @@
1
+ /**
2
+ * Compile-time baking for a static-array `.map()` loop whose body is a
3
+ * single child component with a plain-value (non-JSX) prop set (#2208).
4
+ *
5
+ * `NewXxxProps`'s existing `staticWithoutBody` path (see
6
+ * `go-template-adapter.ts`'s `generateNewPropsFunction`) populates a static
7
+ * loop's child slices from `in.<Name>s` — data the CALLER (handler) must
8
+ * supply. When the loop's array source is itself a fully-static literal
9
+ * (`const items = [{ label: 'Alpha' }, ...]`, #2208), there is no caller
10
+ * input to wait for: every per-item prop value is already known at compile
11
+ * time. `analyzeBakeableStaticChildLoop` resolves that data — the resolved
12
+ * Go literal for each item's input fields, plus its `data-key` — so the
13
+ * constructor can emit `New<Name>Props(<Name>Input{ Field: "value", ... })`
14
+ * directly per item instead of ranging over `in.<Name>s`.
15
+ *
16
+ * Deliberately narrow: only scalar (string/number/boolean) prop values are
17
+ * baked. Anything else (an unresolvable expression, a destructured loop
18
+ * param, a JSX-valued prop) returns `null` so the caller keeps the existing
19
+ * `in.<Name>s`-driven path (or BF101 refusal) unchanged.
20
+ */
21
+
22
+ import { evaluateStaticLiteral, parseExpression, resolveStaticLoopSource, type ConstantInfo, type IRProp, type ParsedExpr } from '@barefootjs/jsx'
23
+ import { capitalizeFieldName } from '../lib/go-naming.ts'
24
+ import { escapeGoString } from '../lib/go-emit.ts'
25
+
26
+ export interface BakedStaticChildItem {
27
+ inputFields: Array<{ goField: string; goValue: string }>
28
+ dataKey: string | null
29
+ }
30
+
31
+ export interface BakedStaticChildLoop {
32
+ items: BakedStaticChildItem[]
33
+ }
34
+
35
+ /**
36
+ * Render a compile-time-known JS scalar as a Go template literal (a quoted
37
+ * string / bare number / `true`/`false`), or `null` for anything else
38
+ * (object, array, `null`, `undefined`) — those have no bare-literal Go
39
+ * template form at this layer. Exported for reuse by
40
+ * `static-element-loop-bake.ts` (#2224), which bakes item field values into
41
+ * a plain-element loop body's text/attr positions the same way this module
42
+ * bakes them into a child component's `Input{...}` constructor call.
43
+ */
44
+ export function scalarToGoLiteral(value: unknown): string | null {
45
+ if (typeof value === 'string') return `"${escapeGoString(value)}"`
46
+ if (typeof value === 'number') return String(value)
47
+ if (typeof value === 'boolean') return value ? 'true' : 'false'
48
+ return null
49
+ }
50
+
51
+ /**
52
+ * Analyze one nested static child-component loop for bakeability. `props`
53
+ * are the child's JSX attrs (`IRLoopChildComponent.props`); `loopArrayParsed`
54
+ * / `loopParam` / `loopKey` come from the same `NestedComponentInfo` the
55
+ * caller already carries. Returns `null` when the shape isn't (yet)
56
+ * bakeable — the caller falls back to its existing behavior unchanged.
57
+ */
58
+ export function analyzeBakeableStaticChildLoop(
59
+ nested: {
60
+ props: ReadonlyArray<{ name: string; value: IRProp['value']; isEventHandler: boolean }>
61
+ loopArrayParsed?: ParsedExpr
62
+ loopParam?: string
63
+ loopKey?: string
64
+ },
65
+ localConstants: ReadonlyArray<ConstantInfo>,
66
+ opts?: { isNameShadowed?: (name: string) => boolean },
67
+ ): BakedStaticChildLoop | null {
68
+ // A destructured loop param's raw pattern text starts with `{`/`[` (the
69
+ // synthesized single-identifier rewrite only applies to a SIMPLE param) —
70
+ // defer rather than mis-bind bindings under a pattern name.
71
+ if (!nested.loopParam || /^[{[]/.test(nested.loopParam)) return null
72
+
73
+ const staticItemsResult = resolveStaticLoopSource(nested.loopArrayParsed, localConstants, opts)
74
+ if (staticItemsResult === null) return null
75
+
76
+ const items: BakedStaticChildItem[] = []
77
+ for (const item of staticItemsResult) {
78
+ const bindings = new Map<string, unknown>([[nested.loopParam, item]])
79
+ const inputFields: Array<{ goField: string; goValue: string }> = []
80
+ for (const prop of nested.props) {
81
+ if (prop.isEventHandler) continue
82
+ if (prop.name.includes('-')) continue // no rest-bag at this compile-time-baked layer
83
+ const resolved = resolvePropValue(prop.value, bindings)
84
+ if (resolved === undefined) return null
85
+ const goValue = scalarToGoLiteral(resolved)
86
+ if (goValue === null) return null
87
+ inputFields.push({ goField: capitalizeFieldName(prop.name), goValue })
88
+ }
89
+ let dataKey: string | null = null
90
+ if (nested.loopKey) {
91
+ const keyExpr = parseExpression(nested.loopKey)
92
+ const keyResolved = evaluateStaticLiteral(keyExpr, bindings)
93
+ if (keyResolved === null) return null
94
+ dataKey = String(keyResolved.value)
95
+ }
96
+ items.push({ inputFields, dataKey })
97
+ }
98
+ return { items }
99
+ }
100
+
101
+ /** Resolves a prop's `AttrValue` to a plain JS value, or `undefined` if unresolvable. */
102
+ function resolvePropValue(value: IRProp['value'], bindings: ReadonlyMap<string, unknown>): unknown {
103
+ switch (value.kind) {
104
+ case 'literal':
105
+ return value.value
106
+ case 'boolean-shorthand':
107
+ case 'boolean-attr':
108
+ return true
109
+ case 'expression': {
110
+ if (!value.parsed) return undefined
111
+ const resolved = evaluateStaticLiteral(value.parsed, bindings)
112
+ return resolved === null ? undefined : resolved.value
113
+ }
114
+ default:
115
+ // `template` / `spread` / `jsx-children`: not evaluated at this
116
+ // compile-time-baked layer — defer to the existing runtime path.
117
+ return undefined
118
+ }
119
+ }
@@ -0,0 +1,211 @@
1
+ /**
2
+ * Compile-time UNROLL for a static-array `.map()` loop whose body is a
3
+ * PLAIN ELEMENT TREE (no child component) — go-only follow-up to #2208
4
+ * (#2224 shape 1). `html/template` has no slice/map literal syntax, so
5
+ * unlike the other 7 template adapters (which splice a serialized literal
6
+ * straight into the loop header — #2208), Go can't bind a compile-time-only
7
+ * array as a `{{range}}` source at all. Rather than synthesizing a Go struct
8
+ * type for the item shape (a materially bigger lift — see the #2224 issue
9
+ * body's "suggested fix direction"), this module verifies the loop body is
10
+ * fully foldable against every item and lets the caller
11
+ * (`go-template-adapter.ts`'s `renderLoop`) render the body once PER ITEM
12
+ * with every item-derived value substituted as a compile-time-known Go
13
+ * literal — no `{{range}}`, no struct, no field lookup, so no hidden runtime
14
+ * failure mode either (`html/template` resolves struct fields at EXECUTE
15
+ * time, not Go compile time).
16
+ *
17
+ * ACCEPTANCE CRITERIA — `analyzeBakeableStaticElementLoop` returns `null`
18
+ * (caller keeps today's BF101 refusal) unless ALL of the following hold:
19
+ *
20
+ * - The loop has no child component (that shape is #2208's own baking
21
+ * path — `analyzeBakeableStaticChildLoop`).
22
+ * - Not a `.flatMap()` (`method: 'flatMap'` / `flatMapCallback` set) — an
23
+ * item can fold to 0+ elements there, and a complex callback carries its
24
+ * body out-of-band rather than in `children`; out of scope.
25
+ * - The loop array resolves via `resolveStaticLoopSource` (a fully-static
26
+ * array literal, inline or a named function-scope const;
27
+ * `isNameShadowed`-checked — the SAME resolution #2208 already trusts
28
+ * for the child-component shape).
29
+ * - The callback param is a simple identifier (no array/object destructure
30
+ * pattern).
31
+ * - The callback does NOT bind an index parameter (`.map((item, i) =>
32
+ * ...)`, or `.entries()`/`.keys()`/`Object.entries()`-style pre-map
33
+ * iteration) — deliberately excluded from the evaluator surface even
34
+ * though the index value is technically knowable at unroll time, to
35
+ * keep the per-item binding set identical to #2208's (item-only).
36
+ * - No `.filter()` / `.sort()` chained onto the `.map()` — out of scope.
37
+ * - The body is neither multi-root (`bodyIsMultiRoot`) nor a whole-item
38
+ * conditional (`bodyIsItemConditional`) — those need anchor-marker
39
+ * machinery this pass doesn't attempt to reproduce per item.
40
+ * - Every node anywhere in the body's IR tree is an `element`, `text`, or
41
+ * `expression` — a nested `loop`, `conditional`, `component`, `slot`,
42
+ * `fragment`, `if-statement`, `provider`, or `async` bails the WHOLE
43
+ * loop (no partial unroll; a loud refusal beats silently wrong output).
44
+ * - Every element attribute is `literal` / `boolean-attr` /
45
+ * `boolean-shorthand`, or a plain `expression` whose parsed kind is one
46
+ * of `identifier` / `member` / `index-access` / `literal`. An attribute
47
+ * `template-literal` or `conditional` bypasses the Go adapter's normal
48
+ * `convertExpressionToGo` emission path in its own attribute emitter
49
+ * (it calls `renderParsedExpr` directly and splices the result assuming
50
+ * adapter-specific self-wrapping conventions) — baking those would need
51
+ * separate handling, deferred. `spread` / `jsx-children` attrs bail.
52
+ * - Every dynamic text `expression` node (any parsed kind, INCLUDING
53
+ * `template-literal` — text always funnels through
54
+ * `convertExpressionToGo` uniformly, no bypass) resolves via
55
+ * `evaluateStaticLiteral(expr.parsed, itemBindings)` to a scalar
56
+ * (string/number/boolean) for EVERY item. A signal/memo call, a
57
+ * reference to any non-item-static local (props, outer consts, an
58
+ * enclosing loop's own param), an unresolvable nested method chain, or
59
+ * a non-scalar (array/object) result bails the whole loop.
60
+ *
61
+ * Analysis only (mirrors `static-child-loop-bake.ts`): this module never
62
+ * emits Go syntax. `go-template-adapter.ts`'s `renderLoop` re-runs the SAME
63
+ * `evaluateStaticLiteral` call per item through its own
64
+ * `convertExpressionToGo` override once this analysis has cleared the whole
65
+ * loop, so the two passes can never disagree — this pass is pure validation,
66
+ * with no adapter-state side effects to roll back if it can't clear a loop.
67
+ */
68
+
69
+ import {
70
+ evaluateStaticLiteral,
71
+ resolveStaticLoopSource,
72
+ type ConstantInfo,
73
+ type IRElement,
74
+ type IRLoop,
75
+ type IRNode,
76
+ type ParsedExpr,
77
+ } from '@barefootjs/jsx'
78
+ import { scalarToGoLiteral } from './static-child-loop-bake.ts'
79
+
80
+ export interface BakedStaticElementLoop {
81
+ items: unknown[]
82
+ }
83
+
84
+ const ALLOWED_ATTR_EXPRESSION_KINDS: ReadonlySet<ParsedExpr['kind']> = new Set([
85
+ 'identifier',
86
+ 'member',
87
+ 'index-access',
88
+ 'literal',
89
+ ])
90
+
91
+ type LoopShape = Pick<
92
+ IRLoop,
93
+ | 'childComponent'
94
+ | 'param'
95
+ | 'index'
96
+ | 'arrayParsed'
97
+ | 'children'
98
+ | 'filterPredicate'
99
+ | 'sortComparator'
100
+ | 'bodyIsMultiRoot'
101
+ | 'bodyIsItemConditional'
102
+ | 'paramBindings'
103
+ | 'iterationShape'
104
+ | 'objectIteration'
105
+ | 'method'
106
+ | 'flatMapCallback'
107
+ >
108
+
109
+ /**
110
+ * Analyze a `.map()` loop with a plain-element (non-component) body for
111
+ * static unrolling. Returns the resolved item values (ready for the caller
112
+ * to render the body once per item) or `null` when the shape isn't (yet)
113
+ * bakeable this way — see the acceptance criteria in the module docstring.
114
+ */
115
+ export function analyzeBakeableStaticElementLoop(
116
+ loop: LoopShape,
117
+ localConstants: ReadonlyArray<ConstantInfo>,
118
+ opts?: { isNameShadowed?: (name: string) => boolean },
119
+ ): BakedStaticElementLoop | null {
120
+ if (loop.childComponent) return null // #2208's own path handles this shape.
121
+ // `.flatMap()`: an item can fold to 0+ elements, and a complex callback
122
+ // carries its body out-of-band (`flatMapCallback`, `children` left empty)
123
+ // rather than in `children` — either way this pass's per-item, single-
124
+ // element-tree model doesn't apply. Out of scope.
125
+ if (loop.method === 'flatMap' || loop.flatMapCallback) return null
126
+ if (!loop.param || /^[{[]/.test(loop.param)) return null
127
+ if (loop.index && loop.index !== '_') return null
128
+ if (loop.paramBindings && loop.paramBindings.length > 0) return null
129
+ if (loop.filterPredicate || loop.sortComparator) return null
130
+ if (loop.iterationShape || loop.objectIteration) return null
131
+ if (loop.bodyIsMultiRoot || loop.bodyIsItemConditional) return null
132
+ if (!isFoldableTree(loop.children)) return null
133
+
134
+ const items = resolveStaticLoopSource(loop.arrayParsed, localConstants, opts)
135
+ if (items === null) return null
136
+
137
+ for (const item of items) {
138
+ const bindings = new Map<string, unknown>([[loop.param, item]])
139
+ if (!allExpressionsFoldFor(loop.children, bindings)) return null
140
+ }
141
+ return { items }
142
+ }
143
+
144
+ /**
145
+ * Structural (item-independent) pass: every node kind in the subtree must be
146
+ * one this module knows how to fold, and every attribute value must be a
147
+ * shape `convertExpressionToGo`'s normal (non-bypassing) path handles.
148
+ */
149
+ function isFoldableTree(nodes: readonly IRNode[]): boolean {
150
+ for (const node of nodes) {
151
+ switch (node.type) {
152
+ case 'text':
153
+ case 'expression':
154
+ continue // resolvability is checked per-item in `allExpressionsFoldFor`.
155
+ case 'element':
156
+ if (!isFoldableAttrs(node)) return false
157
+ if (!isFoldableTree(node.children)) return false
158
+ continue
159
+ default:
160
+ // 'conditional' | 'loop' | 'component' | 'slot' | 'fragment' |
161
+ // 'if-statement' | 'provider' | 'async' — none foldable per-item.
162
+ return false
163
+ }
164
+ }
165
+ return true
166
+ }
167
+
168
+ function isFoldableAttrs(element: IRElement): boolean {
169
+ for (const attr of element.attrs) {
170
+ if (attr.clientOnly) continue // omitted from SSR emission entirely.
171
+ switch (attr.value.kind) {
172
+ case 'literal':
173
+ case 'boolean-attr':
174
+ case 'boolean-shorthand':
175
+ continue
176
+ case 'expression':
177
+ if (!attr.value.parsed || !ALLOWED_ATTR_EXPRESSION_KINDS.has(attr.value.parsed.kind)) return false
178
+ continue
179
+ default:
180
+ // 'template' / 'spread' / 'jsx-children'
181
+ return false
182
+ }
183
+ }
184
+ return true
185
+ }
186
+
187
+ /** Per-item pass: every expression actually resolves to a bakeable scalar. */
188
+ function allExpressionsFoldFor(nodes: readonly IRNode[], bindings: ReadonlyMap<string, unknown>): boolean {
189
+ for (const node of nodes) {
190
+ if (node.type === 'expression') {
191
+ if (node.clientOnly) continue // renders as an item-independent marker.
192
+ if (!node.parsed || !resolvesToScalar(node.parsed, bindings)) return false
193
+ continue
194
+ }
195
+ if (node.type === 'element') {
196
+ for (const attr of node.attrs) {
197
+ if (attr.clientOnly) continue
198
+ if (attr.value.kind !== 'expression') continue
199
+ if (!attr.value.parsed || !resolvesToScalar(attr.value.parsed, bindings)) return false
200
+ }
201
+ if (!allExpressionsFoldFor(node.children, bindings)) return false
202
+ }
203
+ }
204
+ return true
205
+ }
206
+
207
+ function resolvesToScalar(expr: ParsedExpr, bindings: ReadonlyMap<string, unknown>): boolean {
208
+ const resolved = evaluateStaticLiteral(expr, bindings)
209
+ if (resolved === null) return false
210
+ return scalarToGoLiteral(resolved.value) !== null
211
+ }