@barefootjs/jsx 0.26.3 → 0.27.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.
- package/dist/compiler.d.ts.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +735 -140
- package/dist/ir-to-client-js/build-references.d.ts.map +1 -1
- package/dist/ir-to-client-js/client-only-elision.d.ts +103 -0
- package/dist/ir-to-client-js/client-only-elision.d.ts.map +1 -0
- package/dist/ir-to-client-js/collect-elements.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/plan/branch-loop.d.ts +12 -1
- package/dist/ir-to-client-js/control-flow/plan/branch-loop.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/plan/build-branch-loop.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/plan/build-event-delegation.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/plan/build-loop.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/plan/event-delegation.d.ts +16 -1
- package/dist/ir-to-client-js/control-flow/plan/event-delegation.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/plan/loop.d.ts +27 -0
- package/dist/ir-to-client-js/control-flow/plan/loop.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/shared.d.ts +13 -2
- package/dist/ir-to-client-js/control-flow/shared.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/stringify/claim-plan.d.ts +54 -0
- package/dist/ir-to-client-js/control-flow/stringify/claim-plan.d.ts.map +1 -0
- package/dist/ir-to-client-js/control-flow/stringify/event-delegation.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/stringify/inner-loop.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/stringify/insert.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/stringify/loop-child-arm.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/stringify/loop.d.ts +19 -3
- package/dist/ir-to-client-js/control-flow/stringify/loop.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/stringify/reactive-effects.d.ts +63 -4
- package/dist/ir-to-client-js/control-flow/stringify/reactive-effects.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow.d.ts +1 -1
- package/dist/ir-to-client-js/element-refs.d.ts +8 -3
- package/dist/ir-to-client-js/element-refs.d.ts.map +1 -1
- package/dist/ir-to-client-js/emit-reactive.d.ts +8 -1
- package/dist/ir-to-client-js/emit-reactive.d.ts.map +1 -1
- package/dist/ir-to-client-js/html-template.d.ts +73 -1
- package/dist/ir-to-client-js/html-template.d.ts.map +1 -1
- package/dist/ir-to-client-js/imports.d.ts +2 -2
- package/dist/ir-to-client-js/imports.d.ts.map +1 -1
- package/dist/ir-to-client-js/reactivity.d.ts.map +1 -1
- package/dist/ir-to-client-js/types.d.ts +35 -2
- package/dist/ir-to-client-js/types.d.ts.map +1 -1
- package/dist/ir-to-client-js/utils.d.ts +1 -1
- package/dist/jsx-to-ir.d.ts.map +1 -1
- package/dist/types.d.ts +70 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/__tests__/__snapshots__/doc-examples.test.ts.snap +555 -678
- package/src/__tests__/child-components-in-map.test.ts +4 -4
- package/src/__tests__/client-js-generation.test.ts +26 -12
- package/src/__tests__/compiler-runtime-contract.test.ts +4 -4
- package/src/__tests__/composite-branch-loop.test.ts +2 -2
- package/src/__tests__/cross-file-client-signal.test.ts +5 -1
- package/src/__tests__/delegated-handler-preamble.test.ts +153 -0
- package/src/__tests__/early-return-scope-var-ref.test.ts +10 -11
- package/src/__tests__/flatmap-segments.test.ts +182 -0
- package/src/__tests__/inline-jsx-callback.test.ts +6 -4
- package/src/__tests__/ir-jsx-props.test.ts +6 -5
- package/src/__tests__/loop-fallback-wrap.test.ts +3 -4
- package/src/__tests__/map-body-no-silent-divergence.test.ts +45 -0
- package/src/__tests__/module-level-signal.audit.test.ts +7 -2
- package/src/__tests__/nested-loop-conditional.test.ts +9 -7
- package/src/__tests__/nested-loop-reactive-attrs.test.ts +2 -2
- package/src/__tests__/preamble-region-patch.test.ts +156 -0
- package/src/__tests__/profile-cond-binding-ids.test.ts +3 -2
- package/src/__tests__/profile-loop-binding-ids.test.ts +4 -3
- package/src/__tests__/profile-nested-binding-ids.test.ts +11 -6
- package/src/__tests__/reactive-attrs-in-map.test.ts +6 -4
- package/src/__tests__/static-loop-csr-materialize.test.ts +3 -2
- package/src/__tests__/template-closure.test.ts +1 -1
- package/src/compiler.ts +11 -0
- package/src/index.ts +1 -0
- package/src/ir-to-client-js/build-references.ts +16 -0
- package/src/ir-to-client-js/client-only-elision.ts +273 -0
- package/src/ir-to-client-js/collect-elements.ts +74 -14
- package/src/ir-to-client-js/control-flow/plan/branch-loop.ts +12 -1
- package/src/ir-to-client-js/control-flow/plan/build-branch-loop.ts +11 -3
- package/src/ir-to-client-js/control-flow/plan/build-event-delegation.ts +19 -3
- package/src/ir-to-client-js/control-flow/plan/build-loop.ts +36 -0
- package/src/ir-to-client-js/control-flow/plan/event-delegation.ts +16 -1
- package/src/ir-to-client-js/control-flow/plan/loop.ts +28 -0
- package/src/ir-to-client-js/control-flow/shared.ts +27 -2
- package/src/ir-to-client-js/control-flow/stringify/branch-loop.ts +34 -4
- package/src/ir-to-client-js/control-flow/stringify/claim-plan.ts +68 -0
- package/src/ir-to-client-js/control-flow/stringify/event-delegation.ts +67 -19
- package/src/ir-to-client-js/control-flow/stringify/inner-loop.ts +18 -9
- package/src/ir-to-client-js/control-flow/stringify/insert.ts +17 -11
- package/src/ir-to-client-js/control-flow/stringify/loop-child-arm.ts +35 -16
- package/src/ir-to-client-js/control-flow/stringify/loop.ts +80 -14
- package/src/ir-to-client-js/control-flow/stringify/reactive-effects.ts +241 -31
- package/src/ir-to-client-js/control-flow.ts +1 -1
- package/src/ir-to-client-js/element-refs.ts +9 -11
- package/src/ir-to-client-js/emit-reactive.ts +50 -14
- package/src/ir-to-client-js/html-template.ts +144 -15
- package/src/ir-to-client-js/imports.ts +7 -4
- package/src/ir-to-client-js/reactivity.ts +7 -0
- package/src/ir-to-client-js/types.ts +34 -2
- package/src/ir-to-client-js/utils.ts +1 -1
- package/src/jsx-to-ir.ts +382 -15
- package/src/types.ts +72 -0
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Slot unification Step B (`spec/slot-unification.md` §3(b), §5 "Step B —
|
|
3
|
+
* marker elision"): decide, EXACTLY ONCE and BEFORE either `adapter.generate`
|
|
4
|
+
* (SSR) or `generateClientJs` (CSR) run, which `/* @client *\/` text slots
|
|
5
|
+
* can drop their `<!--bf:sN-->…<!--/-->` marker pair entirely from both
|
|
6
|
+
* outputs. Every consumer (all nine SSR adapters' `renderExpression`, and
|
|
7
|
+
* the CSR emitters in `html-template.ts`) reads the single
|
|
8
|
+
* `IRExpression.markerless` flag this pass writes — nobody re-derives the
|
|
9
|
+
* decision, per CLAUDE.md's "Never add compiler options/hooks for
|
|
10
|
+
* tool-specific output rewriting" spirit: one door in, everyone reads it.
|
|
11
|
+
*
|
|
12
|
+
* Scope — deliberately the NARROWEST slice of §3(b)'s elision rule that is
|
|
13
|
+
* fully sound today, not the general case:
|
|
14
|
+
*
|
|
15
|
+
* Only `expr.clientOnly && expr.slotId` expressions OUTSIDE any loop or
|
|
16
|
+
* conditional branch (i.e. reachable from the component's own render tree
|
|
17
|
+
* by walking only `element`/`text`/`expression`/`fragment` nodes).
|
|
18
|
+
*
|
|
19
|
+
* Why THIS slice and not ordinary reactive text slots (loop rows,
|
|
20
|
+
* conditional branches): every other kind of slot's SSR-rendered width is
|
|
21
|
+
* DATA-DEPENDENT — an ordinary `{item.name}` may render empty or non-empty
|
|
22
|
+
* per request, so a LATER sibling's absolute child-index path would only be
|
|
23
|
+
* valid for the specific width THIS request happened to produce, not for
|
|
24
|
+
* every request the compiled function ever serves. `/* @client *\/`
|
|
25
|
+
* expressions are the one case free of that problem: SSR can never evaluate
|
|
26
|
+
* client-only JS (the whole point of the escape valve), so its rendered
|
|
27
|
+
* width is deterministically ZERO on every request, for every adapter, with
|
|
28
|
+
* no data dependence at all. That determinism is what makes computing a
|
|
29
|
+
* real, reusable, hydration-safe path sound here without also solving the
|
|
30
|
+
* general "does an earlier sibling's width vary by request" problem.
|
|
31
|
+
*
|
|
32
|
+
* Path safety within that scope — the part that generalizes A3's
|
|
33
|
+
* `computeSkeletonSlotPaths` (`html-template.ts`) rather than reinventing
|
|
34
|
+
* it: paths are root-relative child-index chains, exactly like that
|
|
35
|
+
* function's, walked in document order with the SAME "once we hit something
|
|
36
|
+
* whose contributed width isn't a compile-time constant, every remaining
|
|
37
|
+
* sibling AND everything nested inside it loses eligibility" rule (nested
|
|
38
|
+
* content reached only through an uncertain-index sibling would resolve an
|
|
39
|
+
* absolute path through that uncertain index — silently wrong, not loud, so
|
|
40
|
+
* it must never be attempted). Concretely, walking a children list left to
|
|
41
|
+
* right:
|
|
42
|
+
* - a static `element` always contributes exactly one node — recurse into
|
|
43
|
+
* its own children as an independent, freshly-indexed scope (its own
|
|
44
|
+
* internal accounting can't leak out, and nothing external can poison
|
|
45
|
+
* it either, since the element itself is a fixed anchor regardless of
|
|
46
|
+
* what's inside it);
|
|
47
|
+
* - static `text` contributes a node per HTML-parser text-run-merging
|
|
48
|
+
* rules (adjacent text/expression children collapse into ONE Text
|
|
49
|
+
* node — mirrors `computeSkeletonSlotPaths`'s `pendingText` tracking);
|
|
50
|
+
* - a `clientOnly` expression with a slotId is the one ELIGIBLE case,
|
|
51
|
+
* PROVIDED it is not itself part of a merged text run (rule (i) — the
|
|
52
|
+
* `/* @client *\/` slot must not be immediately adjacent, once markers
|
|
53
|
+
* are gone, to loose text or another expression, or the HTML parser
|
|
54
|
+
* would merge them into one Text node with no way to tell which
|
|
55
|
+
* content is which). Once one is elided, this level (and everything
|
|
56
|
+
* nested inside any later sibling) is marked ineligible for the
|
|
57
|
+
* remainder of the walk at this level — see the "freeze" note below;
|
|
58
|
+
* - any other `expression` (a real reactive/static text or markup slot),
|
|
59
|
+
* any `conditional`/`loop`/`component`/`async`/`provider`/
|
|
60
|
+
* `if-statement`/`slot` node, is data-dependent or opaque width and
|
|
61
|
+
* FREEZES the rest of this children list (see below) — this is
|
|
62
|
+
* strictly conservative: it also means a `/* @client *\/` expression
|
|
63
|
+
* is never eligible once any of these appears earlier at the same
|
|
64
|
+
* level, even though `/* @client *\/`'s own width is always zero,
|
|
65
|
+
* because SIBLINGS reached only via nested recursion past that point
|
|
66
|
+
* would otherwise resolve an absolute path through the frozen node's
|
|
67
|
+
* uncertain contributed width;
|
|
68
|
+
* - the same hazard-tag / force-close-group / void-element / `<tr>`
|
|
69
|
+
* foster-parenting guards `computeSkeletonSlotPaths` uses (imported,
|
|
70
|
+
* not re-derived) abort the ENTIRE walk (a global bail, exactly like
|
|
71
|
+
* that function's `state.bailed`) — a parser hazard means every index
|
|
72
|
+
* computed anywhere in this tree is suspect, not just the ones near
|
|
73
|
+
* the hazard, so "don't guess" means don't guess about the blast
|
|
74
|
+
* radius either.
|
|
75
|
+
*
|
|
76
|
+
* "Freeze" (the one simplification versus a maximally-precise per-branch
|
|
77
|
+
* eligibility tracker): once ANY `/* @client *\/` slot at a given level is
|
|
78
|
+
* elided — OR once anything data-dependent/opaque is seen at that level —
|
|
79
|
+
* that level's remaining siblings (and anything nested only through them)
|
|
80
|
+
* stop being assigned real paths for the rest of THIS walk, even though a
|
|
81
|
+
* maximally precise analysis could sometimes still find one. This caps
|
|
82
|
+
* elision at (at most) one `/* @client *\/` slot per static subtree —
|
|
83
|
+
* conservative by design, matching the elision rule's own framing, and it
|
|
84
|
+
* guarantees two elided slots can never end up needing an insertion whose
|
|
85
|
+
* reference node depends on the other's still-unresolved width. Freezing is
|
|
86
|
+
* PER CHILDREN-LIST (an element's own children start a fresh, unfrozen
|
|
87
|
+
* scope) — global-bail is reserved for hazard tags alone.
|
|
88
|
+
*
|
|
89
|
+
* Deferred (loudly, not silently): ordinary reactive text slots in loop
|
|
90
|
+
* rows / conditional branches keep their markers — see above for why
|
|
91
|
+
* widening this to data-dependent-width content needs a materially
|
|
92
|
+
* different (and per-adapter-verified) safety argument that this PR does
|
|
93
|
+
* not attempt.
|
|
94
|
+
*/
|
|
95
|
+
|
|
96
|
+
import type { IRNode, IRExpression } from '../types.ts'
|
|
97
|
+
import {
|
|
98
|
+
VOID_ELEMENTS,
|
|
99
|
+
SKELETON_PATH_HAZARD_TAGS,
|
|
100
|
+
skeletonForceCloseGroup,
|
|
101
|
+
hasForeignTableRowContent,
|
|
102
|
+
flattenSkeletonChildren,
|
|
103
|
+
} from './html-template.ts'
|
|
104
|
+
|
|
105
|
+
/** Global bail, mirroring `computeSkeletonSlotPaths`'s `state.bailed`: once
|
|
106
|
+
* a parser hazard is seen anywhere, every remaining step of this ONE
|
|
107
|
+
* `decideClientOnlyElision` call becomes a no-op. Slots already elided
|
|
108
|
+
* before the hazard was reached keep their `markerless`/`elidedPath` —
|
|
109
|
+
* those paths were computed from safe ground and remain valid; only
|
|
110
|
+
* FURTHER assignment stops. */
|
|
111
|
+
interface ElisionState {
|
|
112
|
+
bailed: boolean
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Entry point: mutates `root`'s `expression` nodes in place, setting
|
|
117
|
+
* `markerless`/`elidedPath` on every eligible `/* @client *\/` slot found.
|
|
118
|
+
* Safe to call on any component's render tree; a tree with none simply
|
|
119
|
+
* mutates nothing.
|
|
120
|
+
*/
|
|
121
|
+
export function decideClientOnlyElision(root: IRNode): void {
|
|
122
|
+
walkNode(root, [], new Set(), { bailed: false })
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** Walk one node that occupies a known position (`path`) in its parent's
|
|
126
|
+
* children. Only `element`/`fragment` are ever recursed into by the
|
|
127
|
+
* children-list walker below — this wrapper exists so both the top-level
|
|
128
|
+
* call and `walkChildren`'s per-element recursion can share one path. */
|
|
129
|
+
function walkNode(
|
|
130
|
+
node: IRNode,
|
|
131
|
+
path: readonly number[],
|
|
132
|
+
forceCloseAncestors: ReadonlySet<number>,
|
|
133
|
+
state: ElisionState,
|
|
134
|
+
): void {
|
|
135
|
+
if (state.bailed) return
|
|
136
|
+
if (node.type === 'element') {
|
|
137
|
+
if (!elementIsPathSafe(node.tag, flattenSkeletonChildren(node.children))) {
|
|
138
|
+
state.bailed = true
|
|
139
|
+
return
|
|
140
|
+
}
|
|
141
|
+
const groupIdx = skeletonForceCloseGroup(node.tag)
|
|
142
|
+
if (groupIdx >= 0 && forceCloseAncestors.has(groupIdx)) {
|
|
143
|
+
state.bailed = true
|
|
144
|
+
return
|
|
145
|
+
}
|
|
146
|
+
const nextAncestors = groupIdx >= 0 ? new Set([...forceCloseAncestors, groupIdx]) : forceCloseAncestors
|
|
147
|
+
walkChildren(flattenSkeletonChildren(node.children), path, nextAncestors, state)
|
|
148
|
+
} else if (node.type === 'fragment') {
|
|
149
|
+
walkChildren(flattenSkeletonChildren(node.children), path, forceCloseAncestors, state)
|
|
150
|
+
}
|
|
151
|
+
// Any other root shape (a bare conditional/loop/component return, etc.)
|
|
152
|
+
// has no static children list to walk — nothing to do.
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Walk one parent's (already-fragment-flattened) children left to right,
|
|
157
|
+
* assigning paths to eligible `/* @client *\/` slots. `parentPath` is the
|
|
158
|
+
* path to the PARENT of this children list (the element/root whose
|
|
159
|
+
* `childNodes` these indices are relative to). `forceCloseAncestors`
|
|
160
|
+
* mirrors `computeSkeletonSlotPaths`'s ancestor-threaded force-close-group
|
|
161
|
+
* tracking (`<h2>` inside an open `<h1>`, `<li>` inside an open `<li>`, …) —
|
|
162
|
+
* threaded down through element recursion, exactly like that function.
|
|
163
|
+
* `frozen` is local to THIS call (this one children list) — an element's
|
|
164
|
+
* own children get a fresh, unfrozen scope regardless of what froze here.
|
|
165
|
+
*/
|
|
166
|
+
function walkChildren(
|
|
167
|
+
children: readonly IRNode[],
|
|
168
|
+
parentPath: readonly number[],
|
|
169
|
+
forceCloseAncestors: ReadonlySet<number>,
|
|
170
|
+
state: ElisionState,
|
|
171
|
+
): void {
|
|
172
|
+
let frozen = false
|
|
173
|
+
let idx = 0
|
|
174
|
+
let pendingText = false
|
|
175
|
+
|
|
176
|
+
for (let i = 0; i < children.length; i++) {
|
|
177
|
+
if (state.bailed) return
|
|
178
|
+
const child = children[i]
|
|
179
|
+
|
|
180
|
+
switch (child.type) {
|
|
181
|
+
case 'text': {
|
|
182
|
+
if (child.value === '') continue
|
|
183
|
+
if (!pendingText) idx += 1
|
|
184
|
+
pendingText = true
|
|
185
|
+
continue
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
case 'expression': {
|
|
189
|
+
if (child.expr === 'null' || child.expr === 'undefined') continue
|
|
190
|
+
if (child.clientOnly && child.slotId) {
|
|
191
|
+
const adjacent = isTextLike(children[i - 1]) || isTextLike(children[i + 1])
|
|
192
|
+
if (!frozen && !adjacent) {
|
|
193
|
+
markElided(child, [...parentPath, idx])
|
|
194
|
+
frozen = true // one elision per level — see module docstring
|
|
195
|
+
} else {
|
|
196
|
+
frozen = true // still freeze: an un-elided expression here is
|
|
197
|
+
// no different from any other data-dependent-width sibling.
|
|
198
|
+
}
|
|
199
|
+
idx += 1
|
|
200
|
+
pendingText = false
|
|
201
|
+
continue
|
|
202
|
+
}
|
|
203
|
+
// Any other expression — data-dependent width. Freeze the rest of
|
|
204
|
+
// this level; still consume ONE index (best-effort bookkeeping —
|
|
205
|
+
// nothing downstream reads `idx` past this point since `frozen`
|
|
206
|
+
// blocks any further assignment).
|
|
207
|
+
frozen = true
|
|
208
|
+
idx += 1
|
|
209
|
+
pendingText = false
|
|
210
|
+
continue
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
case 'element': {
|
|
214
|
+
if (!elementIsPathSafe(child.tag, flattenSkeletonChildren(child.children))) {
|
|
215
|
+
// Hazard shape: bail the WHOLE walk (matches
|
|
216
|
+
// `computeSkeletonSlotPaths`'s "don't guess" stance), not just
|
|
217
|
+
// this level — a parser hazard means indices anywhere in this
|
|
218
|
+
// tree are untrustworthy, not only the ones near it.
|
|
219
|
+
state.bailed = true
|
|
220
|
+
return
|
|
221
|
+
}
|
|
222
|
+
if (!frozen) {
|
|
223
|
+
walkNode(child, [...parentPath, idx], forceCloseAncestors, state)
|
|
224
|
+
}
|
|
225
|
+
idx += 1
|
|
226
|
+
pendingText = false
|
|
227
|
+
continue
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
case 'fragment':
|
|
231
|
+
continue // already flattened
|
|
232
|
+
|
|
233
|
+
default:
|
|
234
|
+
// conditional / loop / component / async / provider / if-statement /
|
|
235
|
+
// slot: opaque or variable width. Freeze the rest of this level.
|
|
236
|
+
frozen = true
|
|
237
|
+
idx += 1
|
|
238
|
+
pendingText = false
|
|
239
|
+
continue
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/** True for a node that would merge (per HTML text-node-merging rules) with
|
|
245
|
+
* an adjacent bare-text-position slot if that slot's markers were removed —
|
|
246
|
+
* rule (i) of §3(b). `undefined` (no sibling — start/end of the children
|
|
247
|
+
* list) is never adjacent. */
|
|
248
|
+
function isTextLike(node: IRNode | undefined): boolean {
|
|
249
|
+
if (!node) return false
|
|
250
|
+
if (node.type === 'text') return node.value !== ''
|
|
251
|
+
if (node.type === 'expression') return node.expr !== 'null' && node.expr !== 'undefined'
|
|
252
|
+
return false
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function markElided(expr: IRExpression, path: readonly number[]): void {
|
|
256
|
+
expr.markerless = true
|
|
257
|
+
expr.elidedPath = path
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Mirrors `computeSkeletonSlotPaths`'s per-element safety checks (hazard
|
|
262
|
+
* tags, void elements, `<tr>` foster-parenting) without re-deriving the
|
|
263
|
+
* underlying tag sets — imported from `html-template.ts` so the two walks
|
|
264
|
+
* can never silently diverge on which shapes are hazardous. Force-close
|
|
265
|
+
* groups are checked separately by the caller (`walkNode`), which has the
|
|
266
|
+
* ancestor set this check does not.
|
|
267
|
+
*/
|
|
268
|
+
function elementIsPathSafe(tag: string, flatChildren: readonly IRNode[]): boolean {
|
|
269
|
+
if (SKELETON_PATH_HAZARD_TAGS.has(tag)) return false
|
|
270
|
+
if (VOID_ELEMENTS.has(tag) && flatChildren.length > 0) return false
|
|
271
|
+
if (tag === 'tr' && hasForeignTableRowContent(flatChildren)) return false
|
|
272
|
+
return true
|
|
273
|
+
}
|
|
@@ -6,7 +6,7 @@ import { type IRNode, type IRElement, type IRComponent, type IRLoop, type IRProp
|
|
|
6
6
|
import type { ClientJsContext, ConditionalBranchChildComponent, ConditionalBranchReactiveAttr, BranchLoop, ConditionalBranchTextEffect, ConditionalElement, LoopChildBindings, LoopChildBranchSummary, LoopChildConditional, LoopOffset, NestedLoop } from './types.ts'
|
|
7
7
|
import { attrValueToString, freeIdsFromRefs, quotePropName, PROPS_PARAM } from './utils.ts'
|
|
8
8
|
import { classifyReactivity, decideWrapForAttr, decideWrapForChildProp, decideWrapFromAstFlags, collectEventHandlersFromIR, collectConditionalBranchEvents, collectConditionalBranchRefs, collectConditionalBranchChildComponents, collectLoopChildEventsWithNesting, collectLoopChildReactiveAttrs, collectLoopChildReactiveTexts, collectLoopChildRefs, emptyLoopChildBindings } from './reactivity.ts'
|
|
9
|
-
import { irToHtmlTemplate, irToPlaceholderTemplate, irChildrenToJsExpr, buildLoopSkeletonTemplate, computeSkeletonSlotPaths, type SkeletonSlotPaths } from './html-template.ts'
|
|
9
|
+
import { irToHtmlTemplate, irToPlaceholderTemplate, irChildrenToJsExpr, buildLoopSkeletonTemplate, computeSkeletonSlotPaths, renderFlatMapClientBody, renderFlatMapProjectionClientBody, flatMapCallbackHasKeyedLeaf, type SkeletonSlotPaths } from './html-template.ts'
|
|
10
10
|
import { templateRootIsSvg } from './control-flow/stringify/template-parse.ts'
|
|
11
11
|
import { expandDynamicPropValue, expandConstantForReactivity } from './prop-handling.ts'
|
|
12
12
|
import { walkIR, stopAt } from './walker.ts'
|
|
@@ -414,8 +414,8 @@ export function collectInnerLoops(
|
|
|
414
414
|
|
|
415
415
|
/**
|
|
416
416
|
* Decide whether a loop's runtime rendering needs element reconciliation
|
|
417
|
-
* (
|
|
418
|
-
* template-per-item path, and collect inner-loop metadata for its body.
|
|
417
|
+
* (mapArray/mapArrayAnchored + composite item rendering) rather than the
|
|
418
|
+
* simple template-per-item path, and collect inner-loop metadata for its body.
|
|
419
419
|
*
|
|
420
420
|
* Used by both the top-level `case 'loop'` in `collectElements` and the
|
|
421
421
|
* branch-loop collector in `collectBranchLoops`. Each call site applies
|
|
@@ -595,7 +595,11 @@ export function collectElements(
|
|
|
595
595
|
},
|
|
596
596
|
expression: ({ node: ex, scope: inCond }) => {
|
|
597
597
|
if (ex.clientOnly && ex.slotId) {
|
|
598
|
-
ctx.clientOnlyElements.push({
|
|
598
|
+
ctx.clientOnlyElements.push({
|
|
599
|
+
slotId: ex.slotId,
|
|
600
|
+
expression: ex.expr,
|
|
601
|
+
elidedPath: ex.markerless ? ex.elidedPath : undefined,
|
|
602
|
+
})
|
|
599
603
|
return
|
|
600
604
|
}
|
|
601
605
|
if (!ex.slotId || inCond) return
|
|
@@ -650,10 +654,26 @@ export function collectElements(
|
|
|
650
654
|
// are extracted explicitly below for the closure capture set.
|
|
651
655
|
if (!l.slotId || inCond) return
|
|
652
656
|
|
|
657
|
+
// flatMap PROJECTION loop: the only child is a nested IRLoop lowered
|
|
658
|
+
// from `flatMap(it => it.tags.map(...))`. The children exist for SSR
|
|
659
|
+
// templatization (nested {{range}} on DSL adapters); the CLIENT
|
|
660
|
+
// reconciles the flattened leaves through the descriptor mapArray
|
|
661
|
+
// path synthesized from the same inner loop, so none of the
|
|
662
|
+
// children-derived client machinery (bindings, inner-loop plans,
|
|
663
|
+
// per-row template) applies — leaf wiring was refused at IR build.
|
|
664
|
+
const projectionInner =
|
|
665
|
+
l.method === 'flatMap' && l.children.length === 1 && l.children[0].type === 'loop'
|
|
666
|
+
? l.children[0]
|
|
667
|
+
: undefined
|
|
668
|
+
|
|
653
669
|
const childHandlers: string[] = []
|
|
654
|
-
const bindings =
|
|
655
|
-
|
|
656
|
-
|
|
670
|
+
const bindings = projectionInner
|
|
671
|
+
? emptyLoopChildBindings()
|
|
672
|
+
: collectLoopChildBindings(l.children, ctx, siblingOffsets, l.param, l.paramBindings)
|
|
673
|
+
if (!projectionInner) {
|
|
674
|
+
for (const child of l.children) {
|
|
675
|
+
childHandlers.push(...collectEventHandlersFromIR(child))
|
|
676
|
+
}
|
|
657
677
|
}
|
|
658
678
|
|
|
659
679
|
if (l.childComponent) {
|
|
@@ -668,7 +688,9 @@ export function collectElements(
|
|
|
668
688
|
// Determine rendering strategy for dynamic arrays:
|
|
669
689
|
// Use element reconciliation when the loop body has nested components,
|
|
670
690
|
// or when inner loops need their own mapArray for events/reactive text.
|
|
671
|
-
const { useElementReconciliation, innerLoops } =
|
|
691
|
+
const { useElementReconciliation, innerLoops } = projectionInner
|
|
692
|
+
? { useElementReconciliation: false, innerLoops: undefined }
|
|
693
|
+
: decideLoopRendering(l, siblingOffsets, ctx)
|
|
672
694
|
|
|
673
695
|
let template = ''
|
|
674
696
|
let staticItemTemplate: string | undefined
|
|
@@ -691,7 +713,7 @@ export function collectElements(
|
|
|
691
713
|
// `data-key`, mirroring the SSR template's renderChild emit.
|
|
692
714
|
staticItemTemplate = irToHtmlTemplate(l.children[0], buildRestSpreadNames(ctx), 0, undefined, undefined, /* insideLoop */ true)
|
|
693
715
|
}
|
|
694
|
-
} else if (l.children[0]) {
|
|
716
|
+
} else if (l.children[0] && !projectionInner) {
|
|
695
717
|
// Pass loopParams so expressions are wrapped at generation time,
|
|
696
718
|
// avoiding post-hoc regex wrapping that corrupts literal attribute values.
|
|
697
719
|
// Forward destructured bindings (#951) so references like `cfg.color`
|
|
@@ -777,6 +799,20 @@ export function collectElements(
|
|
|
777
799
|
} : undefined,
|
|
778
800
|
chainOrder: l.chainOrder,
|
|
779
801
|
preamble: l.preamble,
|
|
802
|
+
preambleRegions: l.preambleRegions,
|
|
803
|
+
flatMapClient: projectionInner
|
|
804
|
+
? {
|
|
805
|
+
params: l.index ? `(${l.param}, ${l.index})` : `(${l.param})`,
|
|
806
|
+
body: renderFlatMapProjectionClientBody(projectionInner, buildRestSpreadNames(ctx)),
|
|
807
|
+
keyed: projectionInner.key !== null,
|
|
808
|
+
}
|
|
809
|
+
: l.flatMapCallback
|
|
810
|
+
? {
|
|
811
|
+
params: l.flatMapCallback.params,
|
|
812
|
+
body: renderFlatMapClientBody(l.flatMapCallback, buildRestSpreadNames(ctx)),
|
|
813
|
+
keyed: flatMapCallbackHasKeyedLeaf(l.flatMapCallback),
|
|
814
|
+
}
|
|
815
|
+
: undefined,
|
|
780
816
|
})
|
|
781
817
|
// Don't descend — loop-scoped variables are only available inside the iteration.
|
|
782
818
|
},
|
|
@@ -1069,18 +1105,28 @@ function collectBranchLoops(
|
|
|
1069
1105
|
// reactive-text collection on inner loops reached from this call path
|
|
1070
1106
|
// is handled at the enclosing branch-loop level below (`if (ctx)` block
|
|
1071
1107
|
// around `collectLoopChildReactiveTexts`), not per inner loop.
|
|
1072
|
-
|
|
1073
|
-
|
|
1108
|
+
// flatMap PROJECTION loop — see the top-level handler's note: children
|
|
1109
|
+
// are SSR-only; the client rides the descriptor mapArray path.
|
|
1110
|
+
const projectionInner =
|
|
1111
|
+
n.method === 'flatMap' && n.children.length === 1 && n.children[0].type === 'loop'
|
|
1112
|
+
? n.children[0]
|
|
1113
|
+
: undefined
|
|
1114
|
+
|
|
1115
|
+
const { useElementReconciliation, innerLoops: innerLoopsCollected } = projectionInner
|
|
1116
|
+
? { useElementReconciliation: false, innerLoops: undefined }
|
|
1117
|
+
: decideLoopRendering(n, siblingOffsets, undefined)
|
|
1074
1118
|
|
|
1075
1119
|
// Build the item template from loop children.
|
|
1076
|
-
// Use loopDepth=0: this loop gets its own
|
|
1120
|
+
// Use loopDepth=0: this loop gets its own mapArray/mapArrayAnchored call (independent
|
|
1077
1121
|
// from the conditional's template), so items use data-key (not data-key-1).
|
|
1078
1122
|
// Pass loopParams so expressions reference the per-item signal accessor,
|
|
1079
1123
|
// keeping the template consistent with reactive effect expressions that
|
|
1080
1124
|
// use `param()` to read the current item value.
|
|
1081
1125
|
let childTemplate: string
|
|
1082
1126
|
const branchLoopParamSpec = [{ param: n.param, bindings: n.paramBindings }]
|
|
1083
|
-
if (
|
|
1127
|
+
if (projectionInner) {
|
|
1128
|
+
childTemplate = '' // descriptor renderItem builds from d.h, not a row template
|
|
1129
|
+
} else if (useElementReconciliation && n.children[0]) {
|
|
1084
1130
|
childTemplate = irToPlaceholderTemplate(n.children[0], restNames, 0, branchLoopParamSpec)
|
|
1085
1131
|
} else {
|
|
1086
1132
|
childTemplate = n.children.map(c => irToHtmlTemplate(c, undefined, 0, branchLoopParamSpec)).join('')
|
|
@@ -1092,7 +1138,7 @@ function collectBranchLoops(
|
|
|
1092
1138
|
// memos). Previously these were only collected for composite loops,
|
|
1093
1139
|
// which caused reactive reads inside simple loop bodies to silently
|
|
1094
1140
|
// no-op for existing items.
|
|
1095
|
-
const branchBindings = ctx
|
|
1141
|
+
const branchBindings = ctx && !projectionInner
|
|
1096
1142
|
? collectLoopChildBindings(n.children, ctx, siblingOffsets, n.param, n.paramBindings)
|
|
1097
1143
|
: emptyLoopChildBindings()
|
|
1098
1144
|
|
|
@@ -1112,6 +1158,7 @@ function collectBranchLoops(
|
|
|
1112
1158
|
template: childTemplate,
|
|
1113
1159
|
containerSlotId: containerSlot,
|
|
1114
1160
|
preamble: n.preamble,
|
|
1161
|
+
preambleRegions: n.preambleRegions,
|
|
1115
1162
|
nestedComponents: useElementReconciliation ? n.nestedComponents : undefined,
|
|
1116
1163
|
bindings: branchBindings,
|
|
1117
1164
|
innerLoops: useElementReconciliation ? innerLoopsCollected : undefined,
|
|
@@ -1126,6 +1173,19 @@ function collectBranchLoops(
|
|
|
1126
1173
|
raw: n.sortComparator.raw,
|
|
1127
1174
|
} : undefined,
|
|
1128
1175
|
chainOrder: n.chainOrder,
|
|
1176
|
+
flatMapClient: projectionInner
|
|
1177
|
+
? {
|
|
1178
|
+
params: n.index ? `(${n.param}, ${n.index})` : `(${n.param})`,
|
|
1179
|
+
body: renderFlatMapProjectionClientBody(projectionInner, restNames),
|
|
1180
|
+
keyed: projectionInner.key !== null,
|
|
1181
|
+
}
|
|
1182
|
+
: n.flatMapCallback
|
|
1183
|
+
? {
|
|
1184
|
+
params: n.flatMapCallback.params,
|
|
1185
|
+
body: renderFlatMapClientBody(n.flatMapCallback, restNames),
|
|
1186
|
+
keyed: flatMapCallbackHasKeyedLeaf(n.flatMapCallback),
|
|
1187
|
+
}
|
|
1188
|
+
: undefined,
|
|
1129
1189
|
})
|
|
1130
1190
|
// Don't recurse into the loop — nested loops are handled by the loop's own reconciliation.
|
|
1131
1191
|
},
|
|
@@ -15,11 +15,17 @@
|
|
|
15
15
|
* pair.
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
|
-
import type { CompositeLoopPlan, LoopChildRefBinding } from './loop.ts'
|
|
18
|
+
import type { CompositeLoopPlan, LoopChildRefBinding, PreambleRegionPlan } from './loop.ts'
|
|
19
19
|
import type { EventDelegationPlan } from './event-delegation.ts'
|
|
20
20
|
import type { ReactiveEffectsPlan } from './reactive-effects.ts'
|
|
21
21
|
|
|
22
22
|
export interface BranchPlainLoopPlan {
|
|
23
|
+
/**
|
|
24
|
+
* flatMap descriptor mode — see `PlainLoopVariant.flatMapLeafItem`
|
|
25
|
+
* (plan/loop.ts): `arrayExpr` already flattens through the descriptor
|
|
26
|
+
* body and the stringifier emits the build-or-patch renderItem.
|
|
27
|
+
*/
|
|
28
|
+
flatMapLeafItem?: boolean
|
|
23
29
|
kind: 'plain'
|
|
24
30
|
/** See LoopPlanCommon.rowConstruction — required on every variant. */
|
|
25
31
|
rowConstruction: 'string-template' | 'dom-ops'
|
|
@@ -78,6 +84,11 @@ export interface BranchPlainLoopPlan {
|
|
|
78
84
|
* Undefined off → byte-identical (SR8).
|
|
79
85
|
*/
|
|
80
86
|
profileLoopId?: string
|
|
87
|
+
/**
|
|
88
|
+
* Preamble-patched regions (#2389) — non-empty forces the multi-line
|
|
89
|
+
* renderItem layout, mirroring the top-level plain plan.
|
|
90
|
+
*/
|
|
91
|
+
preambleRegions: readonly PreambleRegionPlan[]
|
|
81
92
|
}
|
|
82
93
|
|
|
83
94
|
export interface BranchCompositeLoopPlan {
|
|
@@ -15,7 +15,7 @@ import { buildChainedArrayExpr, varSlotId, wrapLoopParamAsAccessor } from '../..
|
|
|
15
15
|
import { buildBranchCompositePlan } from './build-composite-loop.ts'
|
|
16
16
|
import { buildBranchLoopDelegationPlan } from './build-event-delegation.ts'
|
|
17
17
|
import { buildReactiveEffectsPlan } from './build-reactive-effects.ts'
|
|
18
|
-
import { destructureLoopParam, loopKeyFn, buildChildRefBindings } from '../shared.ts'
|
|
18
|
+
import { destructureLoopParam, loopKeyFn, buildChildRefBindings, buildPreambleRegionPlans } from '../shared.ts'
|
|
19
19
|
import { renderPreamble, irToHtmlTemplate } from '../../html-template.ts'
|
|
20
20
|
import type {
|
|
21
21
|
BranchCompositeLoopPlan,
|
|
@@ -43,14 +43,21 @@ export function buildBranchLoopPlan(loop: BranchLoop, profileComponentName?: str
|
|
|
43
43
|
|| loop.bindings.reactiveTexts.length > 0
|
|
44
44
|
|| loop.bindings.conditionals.length > 0
|
|
45
45
|
|
|
46
|
+
// flatMap descriptor mode — see buildPlainLoopPlan (build-loop.ts).
|
|
47
|
+
const fm = loop.flatMapClient
|
|
46
48
|
const plan: BranchPlainLoopPlan = {
|
|
47
49
|
kind: 'plain',
|
|
48
50
|
rowConstruction: 'string-template',
|
|
49
51
|
containerSlotId,
|
|
50
52
|
containerVar,
|
|
51
53
|
markerId: loop.markerId,
|
|
52
|
-
|
|
53
|
-
|
|
54
|
+
flatMapLeafItem: fm ? true : undefined,
|
|
55
|
+
arrayExpr: fm
|
|
56
|
+
? `(${buildChainedArrayExpr(loop)}).flatMap(${fm.params} => ${fm.body})`
|
|
57
|
+
: buildChainedArrayExpr(loop),
|
|
58
|
+
keyFn: fm
|
|
59
|
+
? (fm.keyed ? '(__bfD, __bfI) => String(__bfD.k ?? __bfI)' : 'null')
|
|
60
|
+
: loopKeyFn(loop),
|
|
54
61
|
paramHead,
|
|
55
62
|
paramUnwrap,
|
|
56
63
|
indexParam: loop.index || '__idx',
|
|
@@ -75,6 +82,7 @@ export function buildBranchLoopPlan(loop: BranchLoop, profileComponentName?: str
|
|
|
75
82
|
: null,
|
|
76
83
|
eventDelegation: buildBranchLoopDelegationPlan(loop, cv, profileComponentName),
|
|
77
84
|
childRefs: buildChildRefBindings(loop.bindings.refs, loop.param, loop.paramBindings),
|
|
85
|
+
preambleRegions: buildPreambleRegionPlans(loop.preambleRegions, loop.param, loop.paramBindings),
|
|
78
86
|
bodyIsMultiRoot: loop.bodyIsMultiRoot ?? false,
|
|
79
87
|
profileLoopId: profileComponentName ? `${profileComponentName}#binding:${containerSlotId}` : undefined,
|
|
80
88
|
}
|
|
@@ -36,11 +36,18 @@ export function buildDynamicLoopDelegationPlan(
|
|
|
36
36
|
paramBindings: elem.paramBindings,
|
|
37
37
|
key: elem.key,
|
|
38
38
|
index: elem.index,
|
|
39
|
+
// No loopParams spec here (unlike the row-render context) — in the
|
|
40
|
+
// delegated handler `elem.param` is bound to the plain `.find()`/
|
|
41
|
+
// indexed result, not a signal accessor, so leaf refs must stay in
|
|
42
|
+
// their literal (`t.name`) form. Passing a loopParams spec here was
|
|
43
|
+
// BUG-3: it rewrote leaf refs to accessor-call form (`t().name`),
|
|
44
|
+
// which throws since `t` is a plain object in this scope.
|
|
39
45
|
mapPreamble: elem.preamble
|
|
40
46
|
? renderPreamble(elem.preamble, {
|
|
41
|
-
renderLeaf: (ir) => irToHtmlTemplate(ir, undefined, 1,
|
|
47
|
+
renderLeaf: (ir) => irToHtmlTemplate(ir, undefined, 1, undefined, undefined, true),
|
|
42
48
|
})
|
|
43
49
|
: null,
|
|
50
|
+
mapPreambleDeclaredNames: elem.preamble?.declaredNames ?? [],
|
|
44
51
|
}),
|
|
45
52
|
}
|
|
46
53
|
}
|
|
@@ -65,11 +72,14 @@ export function buildBranchLoopDelegationPlan(
|
|
|
65
72
|
paramBindings: loop.paramBindings,
|
|
66
73
|
key: loop.key,
|
|
67
74
|
index: loop.index,
|
|
75
|
+
// See note in `buildDynamicLoopDelegationPlan` above (BUG-3): no
|
|
76
|
+
// loopParams spec — leaf refs must stay in plain-object form here.
|
|
68
77
|
mapPreamble: loop.preamble
|
|
69
78
|
? renderPreamble(loop.preamble, {
|
|
70
|
-
renderLeaf: (ir) => irToHtmlTemplate(ir, undefined, 1,
|
|
79
|
+
renderLeaf: (ir) => irToHtmlTemplate(ir, undefined, 1, undefined, undefined, true),
|
|
71
80
|
})
|
|
72
81
|
: null,
|
|
82
|
+
mapPreambleDeclaredNames: loop.preamble?.declaredNames ?? [],
|
|
73
83
|
}),
|
|
74
84
|
}
|
|
75
85
|
}
|
|
@@ -95,11 +105,14 @@ export function buildStaticArrayDelegationPlan(
|
|
|
95
105
|
// array too (#1434).
|
|
96
106
|
arrayExpr: buildChainedArrayExpr(elem),
|
|
97
107
|
param: elem.param,
|
|
108
|
+
// See note in `buildDynamicLoopDelegationPlan` above (BUG-3): no
|
|
109
|
+
// loopParams spec — leaf refs must stay in plain-object form here.
|
|
98
110
|
mapPreamble: elem.preamble
|
|
99
111
|
? renderPreamble(elem.preamble, {
|
|
100
|
-
renderLeaf: (ir) => irToHtmlTemplate(ir, undefined, 1,
|
|
112
|
+
renderLeaf: (ir) => irToHtmlTemplate(ir, undefined, 1, undefined, undefined, true),
|
|
101
113
|
})
|
|
102
114
|
: null,
|
|
115
|
+
mapPreambleDeclaredNames: elem.preamble?.declaredNames ?? [],
|
|
103
116
|
offset: elem.offset ?? null,
|
|
104
117
|
indexParam: elem.index ?? null,
|
|
105
118
|
},
|
|
@@ -117,6 +130,7 @@ function buildKeyedOrIndexLookup(args: {
|
|
|
117
130
|
key: string | null
|
|
118
131
|
index: string | null
|
|
119
132
|
mapPreamble: string | null
|
|
133
|
+
mapPreambleDeclaredNames: readonly string[]
|
|
120
134
|
}): ItemLookup {
|
|
121
135
|
const hasBindings = (args.paramBindings?.length ?? 0) > 0
|
|
122
136
|
if (args.key !== null) {
|
|
@@ -132,6 +146,7 @@ function buildKeyedOrIndexLookup(args: {
|
|
|
132
146
|
paramBindings: args.paramBindings,
|
|
133
147
|
keyWithItem,
|
|
134
148
|
mapPreamble: args.mapPreamble,
|
|
149
|
+
mapPreambleDeclaredNames: args.mapPreambleDeclaredNames,
|
|
135
150
|
hasBindings,
|
|
136
151
|
indexParam: args.index,
|
|
137
152
|
}
|
|
@@ -141,6 +156,7 @@ function buildKeyedOrIndexLookup(args: {
|
|
|
141
156
|
arrayExpr: args.array,
|
|
142
157
|
param: args.param,
|
|
143
158
|
mapPreamble: args.mapPreamble,
|
|
159
|
+
mapPreambleDeclaredNames: args.mapPreambleDeclaredNames,
|
|
144
160
|
hasBindings,
|
|
145
161
|
indexParam: args.index,
|
|
146
162
|
}
|
|
@@ -35,6 +35,7 @@ import {
|
|
|
35
35
|
destructureLoopParam,
|
|
36
36
|
buildChildRefBindings,
|
|
37
37
|
buildStaticChildRefBindings,
|
|
38
|
+
buildPreambleRegionPlans,
|
|
38
39
|
} from '../shared.ts'
|
|
39
40
|
import { buildLoopReactiveEffectsPlan } from './build-reactive-effects.ts'
|
|
40
41
|
import { buildComponentLoopPlan } from './build-component-loop.ts'
|
|
@@ -91,6 +92,40 @@ export function buildPlainLoopPlan(elem: TopLevelLoop, profileComponentName?: st
|
|
|
91
92
|
|| elem.bindings.reactiveTexts.length > 0
|
|
92
93
|
|| elem.bindings.conditionals.length > 0
|
|
93
94
|
|
|
95
|
+
// flatMap descriptor mode: reconcile the FLATTENED leaves. The source
|
|
96
|
+
// accessor runs the flatMap body (plain items — per-item signals don't
|
|
97
|
+
// exist yet) producing `({ k, h })` descriptors; the keyFn reads the
|
|
98
|
+
// leaf-declared key with an index fallback. Everything row-template-
|
|
99
|
+
// related is bypassed — the stringifier emits the build-or-patch
|
|
100
|
+
// renderItem instead. Pre-fix this fell into the generic plain shape
|
|
101
|
+
// with the UN-flattened source, a null keyFn, and an EMPTY template
|
|
102
|
+
// (leaf loss at hydration; cloneNode(null) crash on adds).
|
|
103
|
+
if (elem.flatMapClient) {
|
|
104
|
+
return {
|
|
105
|
+
kind: 'plain',
|
|
106
|
+
rowConstruction: 'string-template',
|
|
107
|
+
containerVar: `_${varSlotId(elem.slotId)}`,
|
|
108
|
+
markerId: elem.markerId,
|
|
109
|
+
profileLoopId: profileComponentName ? `${profileComponentName}#binding:${elem.slotId}` : undefined,
|
|
110
|
+
arrayExpr: `(${buildChainedArrayExpr(elem)}).flatMap(${elem.flatMapClient.params} => ${elem.flatMapClient.body})`,
|
|
111
|
+
keyFn: elem.flatMapClient.keyed ? '(__bfD, __bfI) => String(__bfD.k ?? __bfI)' : 'null',
|
|
112
|
+
paramHead: '__bfD',
|
|
113
|
+
paramUnwrap: '',
|
|
114
|
+
indexParam: '__idx',
|
|
115
|
+
mapPreambleWrapped: '',
|
|
116
|
+
template: '',
|
|
117
|
+
reactiveEffects: null,
|
|
118
|
+
childRefs: [],
|
|
119
|
+
bodyIsMultiRoot: false,
|
|
120
|
+
anchored: false,
|
|
121
|
+
anchorKeyExpr: '__idx',
|
|
122
|
+
flatMapLeafItem: true,
|
|
123
|
+
// flatMap loops carry no `MapCallbackPreamble` (jsx-to-ir.ts drops it
|
|
124
|
+
// for flatMapCallback), so there is nothing to patch here.
|
|
125
|
+
preambleRegions: [],
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
94
129
|
return {
|
|
95
130
|
kind: 'plain',
|
|
96
131
|
rowConstruction: 'string-template',
|
|
@@ -116,6 +151,7 @@ export function buildPlainLoopPlan(elem: TopLevelLoop, profileComponentName?: st
|
|
|
116
151
|
skeletonPaths: elem.skeletonPaths,
|
|
117
152
|
reactiveEffects: hasReactive ? buildLoopReactiveEffectsPlan(elem, profileComponentName) : null,
|
|
118
153
|
childRefs: buildChildRefBindings(elem.bindings.refs, elem.param, elem.paramBindings),
|
|
154
|
+
preambleRegions: buildPreambleRegionPlans(elem.preambleRegions, elem.param, elem.paramBindings),
|
|
119
155
|
bodyIsMultiRoot: elem.bodyIsMultiRoot ?? false,
|
|
120
156
|
anchored: elem.bodyIsItemConditional ?? false,
|
|
121
157
|
// Fall back to the iteration index when the loop has no key. A whole-item
|