@barefootjs/jsx 0.27.0 → 0.28.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/index.js +620 -26
- package/dist/ir-to-client-js/control-flow/plan/branch-loop.d.ts +6 -0
- 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 +2 -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-insert.d.ts +7 -0
- package/dist/ir-to-client-js/control-flow/plan/build-insert.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/plan/build-lazy-row.d.ts +107 -0
- package/dist/ir-to-client-js/control-flow/plan/build-lazy-row.d.ts.map +1 -0
- package/dist/ir-to-client-js/control-flow/plan/build-loop.d.ts +9 -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/lazy-row-eligibility.d.ts +203 -0
- package/dist/ir-to-client-js/control-flow/plan/lazy-row-eligibility.d.ts.map +1 -0
- package/dist/ir-to-client-js/control-flow/plan/loop.d.ts +10 -0
- package/dist/ir-to-client-js/control-flow/plan/loop.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/stringify/branch-loop.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/stringify/lazy-row.d.ts +98 -0
- package/dist/ir-to-client-js/control-flow/stringify/lazy-row.d.ts.map +1 -0
- package/dist/ir-to-client-js/control-flow/stringify/loop-child-arm.d.ts +5 -0
- 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.map +1 -1
- package/dist/ir-to-client-js/control-flow.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/package.json +2 -2
- package/src/__tests__/__snapshots__/doc-examples.test.ts.snap +162 -36
- package/src/__tests__/client-js-generation.test.ts +4 -1
- package/src/__tests__/composite-branch-loop.test.ts +12 -3
- package/src/__tests__/conditional-mapArray-key.test.ts +7 -1
- package/src/__tests__/create-selector.test.ts +21 -7
- package/src/__tests__/lazy-row-eligibility.test.ts +692 -0
- package/src/__tests__/loop-branch-bare-expression-reactive-text.test.ts +98 -0
- package/src/__tests__/loop-fallback-wrap.test.ts +31 -12
- package/src/__tests__/loop-hoisted-template.test.ts +10 -6
- package/src/__tests__/static-loop-csr-materialize.test.ts +6 -1
- package/src/ir-to-client-js/collect-elements.ts +48 -13
- package/src/ir-to-client-js/control-flow/plan/branch-loop.ts +6 -0
- package/src/ir-to-client-js/control-flow/plan/build-branch-loop.ts +35 -12
- package/src/ir-to-client-js/control-flow/plan/build-insert.ts +9 -2
- package/src/ir-to-client-js/control-flow/plan/build-lazy-row.ts +305 -0
- package/src/ir-to-client-js/control-flow/plan/build-loop.ts +44 -12
- package/src/ir-to-client-js/control-flow/plan/lazy-row-eligibility.ts +440 -0
- package/src/ir-to-client-js/control-flow/plan/loop.ts +10 -0
- package/src/ir-to-client-js/control-flow/stringify/branch-loop.ts +22 -0
- package/src/ir-to-client-js/control-flow/stringify/lazy-row.ts +478 -0
- package/src/ir-to-client-js/control-flow/stringify/loop-child-arm.ts +6 -1
- package/src/ir-to-client-js/control-flow/stringify/loop.ts +23 -0
- package/src/ir-to-client-js/control-flow.ts +7 -2
- package/src/ir-to-client-js/imports.ts +8 -2
- package/src/jsx-to-ir.ts +18 -1
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Build the compiler-side `LazyRowPlan` payload (`spec/slot-unification.md`
|
|
3
|
+
* §9, L3) for one plain loop row — or `null` when the row is ineligible and
|
|
4
|
+
* must keep today's eager `mapArray` emission.
|
|
5
|
+
*
|
|
6
|
+
* Everything decided here is BUILD-TIME data: per-binding item/outer
|
|
7
|
+
* classification (`classifyLazyBinding`), ref-array and dedup-slot layout,
|
|
8
|
+
* and the outer-signal prime list. The stringifier
|
|
9
|
+
* (`stringify/lazy-row.ts`) is a deterministic walk over this plan — it
|
|
10
|
+
* makes no classification decision of its own.
|
|
11
|
+
*
|
|
12
|
+
* Free identifiers are never obtained by regex (repo rule). Texts carry
|
|
13
|
+
* `LoopChildReactiveText.freeIdentifiers` from the analyzer; attrs have no
|
|
14
|
+
* such field, so their expression is run through `parseExpression` +
|
|
15
|
+
* `freeIdentifiers` (`expression-parser.ts`) — both of which fail SAFE, the
|
|
16
|
+
* latter returning `null` on an `unsupported` node, which
|
|
17
|
+
* `classifyLazyBinding` turns into "reads both, and opaque".
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { freeIdentifiers, parseExpression } from '../../../expression-parser.ts'
|
|
21
|
+
import { pickAttrMeta, type AttrMeta } from '../../../types.ts'
|
|
22
|
+
import { extractFreeIdentifiersFromText } from '../../csr-substitute.ts'
|
|
23
|
+
import { wrapLoopParamAsAccessor, PROPS_PARAM } from '../../utils.ts'
|
|
24
|
+
import type { BranchLoop, ClientJsContext, TopLevelLoop } from '../../types.ts'
|
|
25
|
+
import {
|
|
26
|
+
classifyLazyBinding,
|
|
27
|
+
lazyRowEligibility,
|
|
28
|
+
type ClassifiedLazyBinding,
|
|
29
|
+
type LazyRowEligibility,
|
|
30
|
+
type LazyRowScopeInfo,
|
|
31
|
+
type LazyRowShapeFacts,
|
|
32
|
+
} from './lazy-row-eligibility.ts'
|
|
33
|
+
|
|
34
|
+
/** One reactive attribute of a lazy row. */
|
|
35
|
+
export interface LazyRowAttrBinding {
|
|
36
|
+
slotId: string
|
|
37
|
+
attrName: string
|
|
38
|
+
/** Already wrapped via `wrapLoopParamAsAccessor` — reads `<param>()`. */
|
|
39
|
+
wrappedExpression: string
|
|
40
|
+
meta: AttrMeta
|
|
41
|
+
/** Index into `entry.refs` holding this slot's element. */
|
|
42
|
+
refIndex: number
|
|
43
|
+
/** Index into `entry.last` holding this binding's dedup value. */
|
|
44
|
+
ordinal: number
|
|
45
|
+
readsItem: boolean
|
|
46
|
+
readsOuter: boolean
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** One reactive text (content slot) of a lazy row. */
|
|
50
|
+
export interface LazyRowTextBinding {
|
|
51
|
+
slotId: string
|
|
52
|
+
wrappedExpression: string
|
|
53
|
+
ordinal: number
|
|
54
|
+
readsItem: boolean
|
|
55
|
+
/**
|
|
56
|
+
* Reads at least one reactive outer name — emitted into `applyOuter` with
|
|
57
|
+
* §9.3(1) read-compare-write seeding, which needs the RW claim door
|
|
58
|
+
* (`LazyRowPlanData.textNeedsRead`).
|
|
59
|
+
*/
|
|
60
|
+
readsOuter: boolean
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export interface LazyRowPlanData {
|
|
64
|
+
/** Distinct attr slot ids, in declaration order — `entry.refs[0..n-1]`. */
|
|
65
|
+
attrSlotIds: readonly string[]
|
|
66
|
+
attrs: readonly LazyRowAttrBinding[]
|
|
67
|
+
texts: readonly LazyRowTextBinding[]
|
|
68
|
+
/** Index into `entry.refs` holding the claimed-slot door; -1 when no texts. */
|
|
69
|
+
writerIndex: number
|
|
70
|
+
/**
|
|
71
|
+
* PER-LOOP door selection (§9.3(1) content seeding): true when at least
|
|
72
|
+
* one text binding is outer-involving and therefore needs `read(id)` to
|
|
73
|
+
* seed by comparison. The emitter then claims through `lazyClaimSlots`
|
|
74
|
+
* (the `{ write, read }` door) and every text write becomes
|
|
75
|
+
* `__r[w].write(...)`; false keeps the single-closure `lazySlots` writer
|
|
76
|
+
* and the bare `__r[w](...)` call form. One decision for the whole loop —
|
|
77
|
+
* the door is a per-ROW allocation, so it must not be chosen per binding.
|
|
78
|
+
*/
|
|
79
|
+
textNeedsRead: boolean
|
|
80
|
+
/** Total `entry.last` slots. */
|
|
81
|
+
lastCount: number
|
|
82
|
+
/**
|
|
83
|
+
* Signal / memo getters read at the top of `applyOuter` so the ONE
|
|
84
|
+
* loop-level effect subscribes even while the entry list is empty. Empty
|
|
85
|
+
* ⇒ no `applyOuter` is emitted at all (and hydration does literally
|
|
86
|
+
* nothing per row).
|
|
87
|
+
*/
|
|
88
|
+
outerPrimeGetters: readonly string[]
|
|
89
|
+
/** True when at least one binding is outer-involving. */
|
|
90
|
+
hasOuter: boolean
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export interface BuildLazyRowArgs {
|
|
94
|
+
loop: TopLevelLoop | BranchLoop
|
|
95
|
+
/** Fully-chained array expression as emitted (`buildChainedArrayExpr`). */
|
|
96
|
+
arrayExpr: string
|
|
97
|
+
indexParam: string
|
|
98
|
+
/** `destructureLoopParam(...).unwrap` — non-empty ⇒ ineligible. */
|
|
99
|
+
paramUnwrap: string
|
|
100
|
+
mapPreambleWrapped: string
|
|
101
|
+
preambleRegionCount: number
|
|
102
|
+
callSite: 'plain' | 'branch-plain'
|
|
103
|
+
flatMapLeafItem: boolean
|
|
104
|
+
anchored: boolean
|
|
105
|
+
scope: LazyRowScopeInfo | undefined
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Returns the lazy row plan when eligible, otherwise `null` plus the
|
|
110
|
+
* decision (exposed for tests / diagnostics via {@link decideLazyRow}).
|
|
111
|
+
*/
|
|
112
|
+
export function buildLazyRowPlan(args: BuildLazyRowArgs): LazyRowPlanData | null {
|
|
113
|
+
return decideLazyRow(args).plan
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export function decideLazyRow(args: BuildLazyRowArgs): {
|
|
117
|
+
plan: LazyRowPlanData | null
|
|
118
|
+
decision: LazyRowEligibility
|
|
119
|
+
} {
|
|
120
|
+
const { loop, scope } = args
|
|
121
|
+
if (!scope) {
|
|
122
|
+
return { plan: null, decision: { eligible: false, reason: 'no component scope info supplied' } }
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const wrap = (expr: string) => wrapLoopParamAsAccessor(expr, loop.param, loop.paramBindings)
|
|
126
|
+
const rowLocalNames = new Set<string>([loop.param])
|
|
127
|
+
for (const b of loop.paramBindings ?? []) rowLocalNames.add(b.name)
|
|
128
|
+
|
|
129
|
+
// --- classify every binding -------------------------------------------
|
|
130
|
+
const classified: ClassifiedLazyBinding[] = []
|
|
131
|
+
const attrClass = new Map<number, ClassifiedLazyBinding>()
|
|
132
|
+
loop.bindings.reactiveAttrs.forEach((attr, i) => {
|
|
133
|
+
const c = classifyLazyBinding({
|
|
134
|
+
kind: 'attr',
|
|
135
|
+
slotId: attr.childSlotId,
|
|
136
|
+
free: attrFreeIdentifiers(attr.expression),
|
|
137
|
+
rowLocalNames,
|
|
138
|
+
indexParam: args.indexParam,
|
|
139
|
+
scope,
|
|
140
|
+
})
|
|
141
|
+
attrClass.set(i, c)
|
|
142
|
+
classified.push(c)
|
|
143
|
+
})
|
|
144
|
+
const textClass = new Map<number, ClassifiedLazyBinding>()
|
|
145
|
+
loop.bindings.reactiveTexts.forEach((text, i) => {
|
|
146
|
+
const c = classifyLazyBinding({
|
|
147
|
+
kind: 'text',
|
|
148
|
+
slotId: text.slotId,
|
|
149
|
+
free: text.freeIdentifiers ?? null,
|
|
150
|
+
rowLocalNames,
|
|
151
|
+
indexParam: args.indexParam,
|
|
152
|
+
scope,
|
|
153
|
+
})
|
|
154
|
+
textClass.set(i, c)
|
|
155
|
+
classified.push(c)
|
|
156
|
+
})
|
|
157
|
+
|
|
158
|
+
// --- §9.4 gate ---------------------------------------------------------
|
|
159
|
+
const shape: LazyRowShapeFacts = {
|
|
160
|
+
callSite: args.callSite,
|
|
161
|
+
flatMapLeafItem: args.flatMapLeafItem,
|
|
162
|
+
anchored: args.anchored,
|
|
163
|
+
bodyIsMultiRoot: loop.bodyIsMultiRoot ?? false,
|
|
164
|
+
hasExplicitKey: loop.key != null,
|
|
165
|
+
conditionalCount: loop.bindings.conditionals?.length ?? 0,
|
|
166
|
+
childRefCount: loop.bindings.refs?.length ?? 0,
|
|
167
|
+
nestedComponentCount: loop.nestedComponents?.length ?? 0,
|
|
168
|
+
innerLoopCount: loop.innerLoops?.length ?? 0,
|
|
169
|
+
hasChildComponent: 'childComponent' in loop && loop.childComponent != null,
|
|
170
|
+
hasMapPreamble: args.mapPreambleWrapped.length > 0,
|
|
171
|
+
preambleRegionCount: args.preambleRegionCount,
|
|
172
|
+
hasParamUnwrap: args.paramUnwrap.length > 0,
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const decision = lazyRowEligibility({
|
|
176
|
+
shape,
|
|
177
|
+
bindings: classified,
|
|
178
|
+
arraySourceIdentifiers: loopSourceIdentifiers(loop, args.arrayExpr),
|
|
179
|
+
scope,
|
|
180
|
+
})
|
|
181
|
+
if (!decision.eligible) return { plan: null, decision }
|
|
182
|
+
|
|
183
|
+
// --- layout ------------------------------------------------------------
|
|
184
|
+
const attrSlotIds: string[] = []
|
|
185
|
+
for (const attr of loop.bindings.reactiveAttrs) {
|
|
186
|
+
if (!attrSlotIds.includes(attr.childSlotId)) attrSlotIds.push(attr.childSlotId)
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
let ordinal = 0
|
|
190
|
+
const attrs: LazyRowAttrBinding[] = loop.bindings.reactiveAttrs.map((attr, i) => {
|
|
191
|
+
const c = attrClass.get(i)!
|
|
192
|
+
return {
|
|
193
|
+
slotId: attr.childSlotId,
|
|
194
|
+
attrName: attr.attrName,
|
|
195
|
+
wrappedExpression: wrap(attr.expression),
|
|
196
|
+
meta: pickAttrMeta(attr),
|
|
197
|
+
refIndex: attrSlotIds.indexOf(attr.childSlotId),
|
|
198
|
+
ordinal: ordinal++,
|
|
199
|
+
readsItem: c.readsItem,
|
|
200
|
+
readsOuter: c.readsOuter,
|
|
201
|
+
}
|
|
202
|
+
})
|
|
203
|
+
const texts: LazyRowTextBinding[] = loop.bindings.reactiveTexts.map((text, i) => {
|
|
204
|
+
const c = textClass.get(i)!
|
|
205
|
+
return {
|
|
206
|
+
slotId: text.slotId,
|
|
207
|
+
wrappedExpression: wrap(text.expression),
|
|
208
|
+
ordinal: ordinal++,
|
|
209
|
+
// A text that classified as NEITHER item- nor outer-driven still gets
|
|
210
|
+
// applied on item change (harmless, dedup-guarded) rather than
|
|
211
|
+
// silently never being written. Anything the classifier did place in
|
|
212
|
+
// a list keeps exactly the classifier's answer.
|
|
213
|
+
readsItem: c.readsItem || !c.readsOuter,
|
|
214
|
+
readsOuter: c.readsOuter,
|
|
215
|
+
}
|
|
216
|
+
})
|
|
217
|
+
|
|
218
|
+
const outerPrimeGetters: string[] = []
|
|
219
|
+
for (const c of classified) {
|
|
220
|
+
for (const name of c.reactiveOuterNames) {
|
|
221
|
+
if (!outerPrimeGetters.includes(name)) outerPrimeGetters.push(name)
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
return {
|
|
226
|
+
plan: {
|
|
227
|
+
attrSlotIds,
|
|
228
|
+
attrs,
|
|
229
|
+
texts,
|
|
230
|
+
writerIndex: texts.length > 0 ? attrSlotIds.length : -1,
|
|
231
|
+
textNeedsRead: texts.some(t => t.readsOuter),
|
|
232
|
+
lastCount: ordinal,
|
|
233
|
+
outerPrimeGetters,
|
|
234
|
+
hasOuter: attrs.some(a => a.readsOuter) || texts.some(t => t.readsOuter),
|
|
235
|
+
},
|
|
236
|
+
decision,
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Project a `ClientJsContext` down to the name facts the §9.4 gate resolves
|
|
242
|
+
* against. Built once per component and threaded into every loop plan
|
|
243
|
+
* builder, so the gate never reaches back into the whole context.
|
|
244
|
+
*/
|
|
245
|
+
export function buildLazyRowScopeInfo(ctx: ClientJsContext): LazyRowScopeInfo {
|
|
246
|
+
const signals = new Map<string, { initializerFreeIdentifiers: ReadonlySet<string> | null }>()
|
|
247
|
+
for (const s of ctx.signals) {
|
|
248
|
+
signals.set(s.getter, {
|
|
249
|
+
// `parsed` is the analyzer's structured initializer. Absent (or
|
|
250
|
+
// refused by `freeIdentifiers`) ⇒ unprovable, which the source gate
|
|
251
|
+
// treats as a hard stop rather than an assumption.
|
|
252
|
+
initializerFreeIdentifiers: s.parsed ? freeIdentifiers(s.parsed) : null,
|
|
253
|
+
})
|
|
254
|
+
}
|
|
255
|
+
const memos = new Set(ctx.memos.map(m => m.name))
|
|
256
|
+
const props = new Set<string>([PROPS_PARAM])
|
|
257
|
+
if (ctx.propsObjectName) props.add(ctx.propsObjectName)
|
|
258
|
+
for (const p of ctx.propsParams) props.add(p.name)
|
|
259
|
+
const constants = new Map<string, ReadonlySet<string> | null>()
|
|
260
|
+
for (const c of ctx.localConstants) {
|
|
261
|
+
constants.set(c.name, c.freeIdentifiers ?? null)
|
|
262
|
+
}
|
|
263
|
+
const inert = new Set<string>()
|
|
264
|
+
for (const f of ctx.localFunctions) inert.add(f.name)
|
|
265
|
+
for (const imp of ctx.imports) {
|
|
266
|
+
if (imp.isTypeOnly) continue
|
|
267
|
+
for (const spec of imp.specifiers) inert.add(spec.alias || spec.name)
|
|
268
|
+
}
|
|
269
|
+
return { signals, memos, props, constants, inert, profile: ctx.profile }
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Free identifiers of a reactive-attr expression. `LoopChildReactiveAttr`
|
|
274
|
+
* carries none (unlike `LoopChildReactiveText`), so parse it structurally —
|
|
275
|
+
* `freeIdentifiers` returns `null` on an `unsupported` node, which is the
|
|
276
|
+
* fail-safe signal `classifyLazyBinding` expects.
|
|
277
|
+
*/
|
|
278
|
+
function attrFreeIdentifiers(expression: string): ReadonlySet<string> | null {
|
|
279
|
+
if (!expression || expression.trim().length === 0) return new Set()
|
|
280
|
+
try {
|
|
281
|
+
return freeIdentifiers(parseExpression(expression))
|
|
282
|
+
} catch {
|
|
283
|
+
return null
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* §9.3(2) source names: the IR's pre-computed `arrayFreeIdentifiers` UNIONED
|
|
289
|
+
* with the free identifiers of the fully-chained expression actually emitted
|
|
290
|
+
* — `arrayFreeIdentifiers` describes `loop.array` alone, so a
|
|
291
|
+
* `.filter(t => t.done === showDone())` / `.sort(...)` chain would otherwise
|
|
292
|
+
* smuggle an unvetted dependency past the gate. `extractFreeIdentifiersFromNode`
|
|
293
|
+
* (via `...FromText`) scopes arrow parameters, so the predicate's own param
|
|
294
|
+
* never shows up as a free name. Returns `null` when the IR carried no
|
|
295
|
+
* pre-computed set — the gate refuses rather than assuming empty.
|
|
296
|
+
*/
|
|
297
|
+
function loopSourceIdentifiers(
|
|
298
|
+
loop: TopLevelLoop | BranchLoop,
|
|
299
|
+
arrayExpr: string,
|
|
300
|
+
): ReadonlySet<string> | null {
|
|
301
|
+
if (!loop.arrayFreeIdentifiers) return null
|
|
302
|
+
const names = new Set<string>(loop.arrayFreeIdentifiers)
|
|
303
|
+
for (const name of extractFreeIdentifiersFromText(arrayExpr)) names.add(name)
|
|
304
|
+
return names
|
|
305
|
+
}
|
|
@@ -38,6 +38,8 @@ import {
|
|
|
38
38
|
buildPreambleRegionPlans,
|
|
39
39
|
} from '../shared.ts'
|
|
40
40
|
import { buildLoopReactiveEffectsPlan } from './build-reactive-effects.ts'
|
|
41
|
+
import { buildLazyRowPlan } from './build-lazy-row.ts'
|
|
42
|
+
import type { LazyRowScopeInfo } from './lazy-row-eligibility.ts'
|
|
41
43
|
import { buildComponentLoopPlan } from './build-component-loop.ts'
|
|
42
44
|
import { buildTopLevelCompositePlan } from './build-composite-loop.ts'
|
|
43
45
|
import { renderPreamble, irToHtmlTemplate } from '../../html-template.ts'
|
|
@@ -54,6 +56,13 @@ export interface BuildLoopPlanOptions {
|
|
|
54
56
|
unsafeLocalNames: Set<string>
|
|
55
57
|
/** Owning component name when compiling in profile mode (#1690) — else undefined. */
|
|
56
58
|
profileComponentName?: string
|
|
59
|
+
/**
|
|
60
|
+
* Component-scope name facts for the lazy row graph gate
|
|
61
|
+
* (`spec/slot-unification.md` §9.4, `buildLazyRowScopeInfo`). Omitted →
|
|
62
|
+
* no loop is lazy-eligible, i.e. today's emission everywhere. Optional so
|
|
63
|
+
* hand-built plan fixtures keep type-checking.
|
|
64
|
+
*/
|
|
65
|
+
lazyScope?: LazyRowScopeInfo
|
|
57
66
|
}
|
|
58
67
|
|
|
59
68
|
/**
|
|
@@ -68,7 +77,7 @@ export function buildLoopPlan(elem: TopLevelLoop, opts: BuildLoopPlanOptions): L
|
|
|
68
77
|
// array's per-item conditional still toggles reactively instead of freezing
|
|
69
78
|
// in the SSR-time `forEach` (which has no conditional handling at all).
|
|
70
79
|
if (elem.bodyIsItemConditional) {
|
|
71
|
-
return buildPlainLoopPlan(elem, opts.profileComponentName)
|
|
80
|
+
return buildPlainLoopPlan(elem, opts.profileComponentName, opts.lazyScope)
|
|
72
81
|
}
|
|
73
82
|
if (elem.isStaticArray) {
|
|
74
83
|
return buildStaticLoopPlan(elem, opts.unsafeLocalNames, opts.profileComponentName)
|
|
@@ -81,11 +90,15 @@ export function buildLoopPlan(elem: TopLevelLoop, opts: BuildLoopPlanOptions): L
|
|
|
81
90
|
if (elem.childComponent) {
|
|
82
91
|
return buildComponentLoopPlan(elem, opts.profileComponentName)
|
|
83
92
|
}
|
|
84
|
-
return buildPlainLoopPlan(elem, opts.profileComponentName)
|
|
93
|
+
return buildPlainLoopPlan(elem, opts.profileComponentName, opts.lazyScope)
|
|
85
94
|
}
|
|
86
95
|
|
|
87
96
|
/** @internal — prefer `buildLoopPlan`. Exported for the branch-loop wrapper only. */
|
|
88
|
-
export function buildPlainLoopPlan(
|
|
97
|
+
export function buildPlainLoopPlan(
|
|
98
|
+
elem: TopLevelLoop,
|
|
99
|
+
profileComponentName?: string,
|
|
100
|
+
lazyScope?: LazyRowScopeInfo,
|
|
101
|
+
): PlainLoopPlan {
|
|
89
102
|
const wrap = (expr: string) => wrapLoopParamAsAccessor(expr, elem.param, elem.paramBindings)
|
|
90
103
|
const { head: paramHead, unwrap: paramUnwrap } = destructureLoopParam(elem.param, elem.paramBindings)
|
|
91
104
|
const hasReactive = elem.bindings.reactiveAttrs.length > 0
|
|
@@ -126,32 +139,51 @@ export function buildPlainLoopPlan(elem: TopLevelLoop, profileComponentName?: st
|
|
|
126
139
|
}
|
|
127
140
|
}
|
|
128
141
|
|
|
142
|
+
const arrayExpr = buildChainedArrayExpr(elem)
|
|
143
|
+
const indexParam = elem.index || '__idx'
|
|
144
|
+
const mapPreambleWrapped = elem.preamble
|
|
145
|
+
? renderPreamble(elem.preamble, {
|
|
146
|
+
transformJs: wrap,
|
|
147
|
+
renderLeaf: (ir) => irToHtmlTemplate(ir, undefined, 1, [{ param: elem.param, bindings: elem.paramBindings }], undefined, true),
|
|
148
|
+
})
|
|
149
|
+
: ''
|
|
150
|
+
const preambleRegions = buildPreambleRegionPlans(elem.preambleRegions, elem.param, elem.paramBindings)
|
|
151
|
+
|
|
129
152
|
return {
|
|
130
153
|
kind: 'plain',
|
|
131
154
|
rowConstruction: 'string-template',
|
|
132
155
|
containerVar: `_${varSlotId(elem.slotId)}`,
|
|
133
156
|
markerId: elem.markerId,
|
|
134
157
|
profileLoopId: profileComponentName ? `${profileComponentName}#binding:${elem.slotId}` : undefined,
|
|
135
|
-
arrayExpr
|
|
158
|
+
arrayExpr,
|
|
136
159
|
keyFn: loopKeyFn(elem),
|
|
137
160
|
paramHead,
|
|
138
161
|
paramUnwrap,
|
|
139
|
-
indexParam
|
|
162
|
+
indexParam,
|
|
163
|
+
// Lazy row graph (§9, L3). `null` for every ineligible loop, which then
|
|
164
|
+
// keeps the eager emission below byte-for-byte.
|
|
165
|
+
lazyRow: buildLazyRowPlan({
|
|
166
|
+
loop: elem,
|
|
167
|
+
arrayExpr,
|
|
168
|
+
indexParam,
|
|
169
|
+
paramUnwrap,
|
|
170
|
+
mapPreambleWrapped,
|
|
171
|
+
preambleRegionCount: preambleRegions.length,
|
|
172
|
+
callSite: 'plain',
|
|
173
|
+
flatMapLeafItem: false,
|
|
174
|
+
anchored: elem.bodyIsItemConditional ?? false,
|
|
175
|
+
scope: lazyScope,
|
|
176
|
+
}) ?? undefined,
|
|
140
177
|
// Stage 3 / D4 — js segments get the loop-param accessor wrap; jsx leaves
|
|
141
178
|
// render as HTML-string templates under this loop's param context so a
|
|
142
179
|
// leaf that reads the item (`r`) becomes `r()`.
|
|
143
|
-
mapPreambleWrapped
|
|
144
|
-
? renderPreamble(elem.preamble, {
|
|
145
|
-
transformJs: wrap,
|
|
146
|
-
renderLeaf: (ir) => irToHtmlTemplate(ir, undefined, 1, [{ param: elem.param, bindings: elem.paramBindings }], undefined, true),
|
|
147
|
-
})
|
|
148
|
-
: '',
|
|
180
|
+
mapPreambleWrapped,
|
|
149
181
|
template: elem.template,
|
|
150
182
|
skeletonTemplate: elem.skeletonTemplate,
|
|
151
183
|
skeletonPaths: elem.skeletonPaths,
|
|
152
184
|
reactiveEffects: hasReactive ? buildLoopReactiveEffectsPlan(elem, profileComponentName) : null,
|
|
153
185
|
childRefs: buildChildRefBindings(elem.bindings.refs, elem.param, elem.paramBindings),
|
|
154
|
-
preambleRegions
|
|
186
|
+
preambleRegions,
|
|
155
187
|
bodyIsMultiRoot: elem.bodyIsMultiRoot ?? false,
|
|
156
188
|
anchored: elem.bodyIsItemConditional ?? false,
|
|
157
189
|
// Fall back to the iteration index when the loop has no key. A whole-item
|