@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.
- package/dist/adapter/analysis/static-child-loop-bake.d.ts +61 -0
- package/dist/adapter/analysis/static-child-loop-bake.d.ts.map +1 -0
- package/dist/adapter/analysis/static-element-loop-bake.d.ts +83 -0
- package/dist/adapter/analysis/static-element-loop-bake.d.ts.map +1 -0
- package/dist/adapter/go-template-adapter.d.ts +151 -3
- package/dist/adapter/go-template-adapter.d.ts.map +1 -1
- package/dist/adapter/index.js +500 -52
- package/dist/adapter/lib/compile-state.d.ts +15 -0
- package/dist/adapter/lib/compile-state.d.ts.map +1 -1
- package/dist/adapter/lib/constants.d.ts.map +1 -1
- package/dist/adapter/memo/memo-compute.d.ts.map +1 -1
- package/dist/adapter/props/prop-classes.d.ts +40 -0
- package/dist/adapter/props/prop-classes.d.ts.map +1 -0
- package/dist/adapter/props/prop-types.d.ts.map +1 -1
- package/dist/adapter/type/type-codegen.d.ts +5 -1
- package/dist/adapter/type/type-codegen.d.ts.map +1 -1
- package/dist/adapter/value/value-lowering.d.ts.map +1 -1
- package/dist/build.js +500 -52
- package/dist/conformance-pins.d.ts.map +1 -1
- package/dist/index.js +503 -79
- package/dist/render-divergences.d.ts.map +1 -1
- package/dist/test-render.d.ts.map +1 -1
- package/package.json +3 -3
- package/src/__tests__/go-template-adapter.test.ts +708 -4
- package/src/adapter/analysis/static-child-loop-bake.ts +119 -0
- package/src/adapter/analysis/static-element-loop-bake.ts +211 -0
- package/src/adapter/go-template-adapter.ts +661 -34
- package/src/adapter/lib/compile-state.ts +17 -0
- package/src/adapter/lib/constants.ts +1 -0
- package/src/adapter/memo/memo-compute.ts +37 -9
- package/src/adapter/props/prop-classes.ts +70 -0
- package/src/adapter/props/prop-types.ts +69 -1
- package/src/adapter/type/type-codegen.ts +19 -2
- package/src/adapter/value/value-lowering.ts +27 -2
- package/src/conformance-pins.ts +30 -36
- package/src/render-divergences.ts +12 -30
- 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
|
+
}
|