@barefootjs/jsx 0.29.0 → 0.30.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/adapters/child-scope.d.ts +17 -0
- package/dist/adapters/child-scope.d.ts.map +1 -0
- package/dist/adapters/loop-bound-names.d.ts.map +1 -1
- package/dist/analyzer-context.d.ts.map +1 -1
- package/dist/analyzer.d.ts.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +990 -367
- package/dist/ir-to-client-js/collect-elements.d.ts +5 -25
- package/dist/ir-to-client-js/collect-elements.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/plan/build-lazy-row.d.ts +66 -2
- package/dist/ir-to-client-js/control-flow/plan/build-lazy-row.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/plan/build-reactive-effects.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/plan/lazy-conditional.d.ts +92 -0
- package/dist/ir-to-client-js/control-flow/plan/lazy-conditional.d.ts.map +1 -0
- package/dist/ir-to-client-js/control-flow/plan/lazy-preamble.d.ts +134 -0
- package/dist/ir-to-client-js/control-flow/plan/lazy-preamble.d.ts.map +1 -0
- package/dist/ir-to-client-js/control-flow/plan/lazy-row-eligibility.d.ts +46 -11
- package/dist/ir-to-client-js/control-flow/plan/lazy-row-eligibility.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/plan/reactive-effects.d.ts +7 -0
- package/dist/ir-to-client-js/control-flow/plan/reactive-effects.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/stringify/lazy-row.d.ts.map +1 -1
- package/dist/ir-to-client-js/csr-substitute.d.ts.map +1 -1
- package/dist/ir-to-client-js/html-template.d.ts +2 -3
- package/dist/ir-to-client-js/html-template.d.ts.map +1 -1
- package/dist/ir-to-client-js/plan/declaration-emit.d.ts +6 -0
- package/dist/ir-to-client-js/plan/declaration-emit.d.ts.map +1 -1
- package/dist/ir-to-client-js/reactivity.d.ts +1 -10
- package/dist/ir-to-client-js/reactivity.d.ts.map +1 -1
- package/dist/ir-to-client-js/types.d.ts +12 -0
- package/dist/ir-to-client-js/types.d.ts.map +1 -1
- package/dist/jsx-to-ir.d.ts.map +1 -1
- package/dist/prop-rewrite.d.ts +24 -7
- package/dist/prop-rewrite.d.ts.map +1 -1
- package/dist/rich-type-evidence.d.ts.map +1 -1
- package/dist/ssr-defaults.d.ts.map +1 -1
- package/dist/to-locale-date-lowering.d.ts.map +1 -1
- package/dist/types.d.ts +78 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/__tests__/__snapshots__/doc-examples.test.ts.snap +6 -24
- package/src/__tests__/branch-loop-plain.test.ts +28 -45
- package/src/__tests__/csr-substitution-safety-divergence.test.ts +9 -3
- package/src/__tests__/csr-template-scope-soundness.test.ts +167 -0
- package/src/__tests__/early-return-branch-switch.test.ts +96 -0
- package/src/__tests__/form-control-value-ssr.test.ts +102 -0
- package/src/__tests__/lazy-conditional.test.ts +190 -0
- package/src/__tests__/lazy-preamble.test.ts +297 -0
- package/src/__tests__/lazy-row-eligibility.test.ts +97 -28
- package/src/__tests__/loop-item-root-scope.test.ts +213 -0
- package/src/__tests__/map-multi-return-body.test.ts +30 -3
- package/src/__tests__/nested-loop-conditional.test.ts +27 -6
- package/src/__tests__/preamble-attr-reactivity.test.ts +217 -0
- package/src/__tests__/preamble-declarations.test.ts +207 -0
- package/src/__tests__/ssr-defaults.test.ts +44 -0
- package/src/adapters/child-scope.ts +22 -0
- package/src/adapters/loop-bound-names.ts +6 -0
- package/src/analyzer-context.ts +43 -0
- package/src/analyzer.ts +33 -3
- package/src/index.ts +1 -0
- package/src/ir-to-client-js/collect-elements.ts +51 -10
- package/src/ir-to-client-js/control-flow/plan/build-branch-loop.ts +1 -1
- package/src/ir-to-client-js/control-flow/plan/build-composite-loop.ts +2 -2
- package/src/ir-to-client-js/control-flow/plan/build-event-delegation.ts +3 -3
- package/src/ir-to-client-js/control-flow/plan/build-inner-loop.ts +2 -2
- package/src/ir-to-client-js/control-flow/plan/build-lazy-row.ts +147 -5
- package/src/ir-to-client-js/control-flow/plan/build-loop.ts +2 -2
- package/src/ir-to-client-js/control-flow/plan/build-reactive-effects.ts +1 -0
- package/src/ir-to-client-js/control-flow/plan/lazy-conditional.ts +151 -0
- package/src/ir-to-client-js/control-flow/plan/lazy-preamble.ts +287 -0
- package/src/ir-to-client-js/control-flow/plan/lazy-row-eligibility.ts +85 -22
- package/src/ir-to-client-js/control-flow/plan/reactive-effects.ts +7 -0
- package/src/ir-to-client-js/control-flow/stringify/lazy-row.ts +133 -35
- package/src/ir-to-client-js/control-flow/stringify/reactive-effects.ts +24 -4
- package/src/ir-to-client-js/csr-substitute.ts +6 -1
- package/src/ir-to-client-js/emit-registration.ts +1 -1
- package/src/ir-to-client-js/html-template.ts +51 -40
- package/src/ir-to-client-js/index.ts +1 -1
- package/src/ir-to-client-js/plan/build-declaration-emit.ts +2 -0
- package/src/ir-to-client-js/plan/build-static-array-child-init.ts +1 -1
- package/src/ir-to-client-js/plan/declaration-emit.ts +6 -0
- package/src/ir-to-client-js/reactivity.ts +25 -0
- package/src/ir-to-client-js/stringify/declaration-emit.ts +9 -4
- package/src/ir-to-client-js/types.ts +12 -0
- package/src/jsx-to-ir.ts +409 -24
- package/src/prop-rewrite.ts +149 -19
- package/src/rich-type-evidence.ts +6 -7
- package/src/ssr-defaults.ts +7 -2
- package/src/to-locale-date-lowering.ts +8 -2
- package/src/types.ts +80 -0
|
@@ -20,8 +20,11 @@
|
|
|
20
20
|
import { freeIdentifiers, parseExpression } from '../../../expression-parser.ts'
|
|
21
21
|
import { pickAttrMeta, type AttrMeta } from '../../../types.ts'
|
|
22
22
|
import { extractFreeIdentifiersFromText } from '../../csr-substitute.ts'
|
|
23
|
+
import { addCondAttrToTemplate } from '../../html-template.ts'
|
|
23
24
|
import { wrapLoopParamAsAccessor, PROPS_PARAM } from '../../utils.ts'
|
|
24
25
|
import type { BranchLoop, ClientJsContext, TopLevelLoop } from '../../types.ts'
|
|
26
|
+
import { analyzeLazyConditional, type LazyConditionalFacts } from './lazy-conditional.ts'
|
|
27
|
+
import { analyzeLazyPreamble } from './lazy-preamble.ts'
|
|
25
28
|
import {
|
|
26
29
|
classifyLazyBinding,
|
|
27
30
|
lazyRowEligibility,
|
|
@@ -44,6 +47,13 @@ export interface LazyRowAttrBinding {
|
|
|
44
47
|
ordinal: number
|
|
45
48
|
readsItem: boolean
|
|
46
49
|
readsOuter: boolean
|
|
50
|
+
/**
|
|
51
|
+
* Reads a `.map()` preamble local, so every apply body this binding is
|
|
52
|
+
* emitted into must re-run the preamble first (#2447 follow-up). Per
|
|
53
|
+
* binding, not per loop, so `applyOuter` does not pay for a preamble only
|
|
54
|
+
* `applyItem`'s bindings read.
|
|
55
|
+
*/
|
|
56
|
+
readsPreamble: boolean
|
|
47
57
|
}
|
|
48
58
|
|
|
49
59
|
/** One reactive text (content slot) of a lazy row. */
|
|
@@ -58,6 +68,37 @@ export interface LazyRowTextBinding {
|
|
|
58
68
|
* (`LazyRowPlanData.textNeedsRead`).
|
|
59
69
|
*/
|
|
60
70
|
readsOuter: boolean
|
|
71
|
+
/**
|
|
72
|
+
* Reads a `.map()` preamble local, so every apply body this binding is
|
|
73
|
+
* emitted into must re-run the preamble first (#2447 follow-up).
|
|
74
|
+
*/
|
|
75
|
+
readsPreamble: boolean
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** One row conditional driven from the loop-level apply bodies (§9.5). */
|
|
79
|
+
export interface LazyRowConditionalBinding {
|
|
80
|
+
slotId: string
|
|
81
|
+
/** Already wrapped via `wrapLoopParamAsAccessor` — reads `<param>()`. */
|
|
82
|
+
wrappedCondition: string
|
|
83
|
+
whenTrueHtml: string
|
|
84
|
+
whenFalseHtml: string
|
|
85
|
+
/**
|
|
86
|
+
* Index into `entry.refs` holding the `[bf-c]` element. Always claimed ON
|
|
87
|
+
* DEMAND — `createRow` never seeds it, because the row it just cloned already
|
|
88
|
+
* rendered the correct arm and has no swap to perform.
|
|
89
|
+
*/
|
|
90
|
+
refIndex: number
|
|
91
|
+
/** Index into `entry.last` holding this conditional's dedup boolean. */
|
|
92
|
+
ordinal: number
|
|
93
|
+
readsItem: boolean
|
|
94
|
+
readsOuter: boolean
|
|
95
|
+
/**
|
|
96
|
+
* Reads a `.map()` preamble local, so every apply body this binding is
|
|
97
|
+
* emitted into must re-run the preamble first (#2447 follow-up). Per
|
|
98
|
+
* binding, not per loop, so `applyOuter` does not pay for a preamble only
|
|
99
|
+
* `applyItem`'s bindings read.
|
|
100
|
+
*/
|
|
101
|
+
readsPreamble: boolean
|
|
61
102
|
}
|
|
62
103
|
|
|
63
104
|
export interface LazyRowPlanData {
|
|
@@ -86,8 +127,35 @@ export interface LazyRowPlanData {
|
|
|
86
127
|
* nothing per row).
|
|
87
128
|
*/
|
|
88
129
|
outerPrimeGetters: readonly string[]
|
|
89
|
-
/**
|
|
90
|
-
|
|
130
|
+
/**
|
|
131
|
+
* The row's `.map()` callback preamble as already-wrapped JS statements
|
|
132
|
+
* (`mapPreambleWrapped`), or `''` when the row has none — proven safe to run
|
|
133
|
+
* by `analyzeLazyPreamble` (§9.5 widening).
|
|
134
|
+
*
|
|
135
|
+
* `createRow` always runs it, and specifically before the clone: the
|
|
136
|
+
* non-hoisted per-row template can interpolate values the preamble declares.
|
|
137
|
+
*
|
|
138
|
+
* `applyItem` / `applyOuter` run it only when a binding they own reads a
|
|
139
|
+
* declared local (`itemNeedsPreamble` / `outerNeedsPreamble`) — which is
|
|
140
|
+
* possible as of the #2447 follow-up, where an attribute reading one became
|
|
141
|
+
* a reactive binding instead of frozen template text. Re-running is sound
|
|
142
|
+
* because `analyzeLazyPreamble` proved the statements are `const`
|
|
143
|
+
* declarations whose initializers only read the item and zero-arg signal
|
|
144
|
+
* getters, and the binding's DEPENDENCIES are the preamble's own free
|
|
145
|
+
* identifiers (substituted in `classifyLazyBinding`), so `applyOuter`
|
|
146
|
+
* primes what the preamble reads rather than the opaque local name.
|
|
147
|
+
*/
|
|
148
|
+
preambleStatements: string
|
|
149
|
+
/** `applyItem` has a binding that reads a preamble local (#2447 follow-up). */
|
|
150
|
+
itemNeedsPreamble: boolean
|
|
151
|
+
/** `applyOuter` has one. Separate from the above so neither body pays for the other's. */
|
|
152
|
+
outerNeedsPreamble: boolean
|
|
153
|
+
/**
|
|
154
|
+
* Row conditionals driven from the apply bodies (§9.5, `lazy-conditional.ts`).
|
|
155
|
+
* Empty for most loops. Each arm is a static element, so the emitter hoists
|
|
156
|
+
* both parsed once per loop and clones on a flip.
|
|
157
|
+
*/
|
|
158
|
+
conditionals: readonly LazyRowConditionalBinding[]
|
|
91
159
|
}
|
|
92
160
|
|
|
93
161
|
export interface BuildLazyRowArgs {
|
|
@@ -126,6 +194,28 @@ export function decideLazyRow(args: BuildLazyRowArgs): {
|
|
|
126
194
|
const rowLocalNames = new Set<string>([loop.param])
|
|
127
195
|
for (const b of loop.paramBindings ?? []) rowLocalNames.add(b.name)
|
|
128
196
|
|
|
197
|
+
// §9.5 preamble widening: prove the preamble is safe to re-run in the apply
|
|
198
|
+
// bodies BEFORE classifying, because a binding that reads a declared local
|
|
199
|
+
// inherits the preamble's dependencies rather than its own literal names.
|
|
200
|
+
const primableNames = new Set<string>([...scope.signals.keys(), ...scope.memos])
|
|
201
|
+
const preambleAnalysis = analyzeLazyPreamble(loop.preamble, args.indexParam, primableNames)
|
|
202
|
+
|
|
203
|
+
// §9.5 conditional widening: a row conditional whose arms are wiring-free
|
|
204
|
+
// static elements is driven from the apply bodies instead of a per-row
|
|
205
|
+
// `insert()` effect. The FIRST refusal wins, so the gate names one shape.
|
|
206
|
+
const rawConditionals = loop.bindings.conditionals ?? []
|
|
207
|
+
const condFacts: LazyConditionalFacts[] = []
|
|
208
|
+
let conditionalRefusal: string | null = null
|
|
209
|
+
for (const cond of rawConditionals) {
|
|
210
|
+
const verdict = analyzeLazyConditional(cond, args.indexParam, {
|
|
211
|
+
whenTrueHtml: addCondAttrToTemplate(wrap(cond.whenTrueHtml), cond.slotId),
|
|
212
|
+
whenFalseHtml: addCondAttrToTemplate(wrap(cond.whenFalseHtml), cond.slotId),
|
|
213
|
+
})
|
|
214
|
+
if (!verdict.lazySafe) { conditionalRefusal = verdict.reason; break }
|
|
215
|
+
condFacts.push(verdict.facts)
|
|
216
|
+
}
|
|
217
|
+
const preambleFacts = preambleAnalysis.lazySafe ? preambleAnalysis.facts : undefined
|
|
218
|
+
|
|
129
219
|
// --- classify every binding -------------------------------------------
|
|
130
220
|
const classified: ClassifiedLazyBinding[] = []
|
|
131
221
|
const attrClass = new Map<number, ClassifiedLazyBinding>()
|
|
@@ -137,6 +227,7 @@ export function decideLazyRow(args: BuildLazyRowArgs): {
|
|
|
137
227
|
rowLocalNames,
|
|
138
228
|
indexParam: args.indexParam,
|
|
139
229
|
scope,
|
|
230
|
+
preamble: preambleFacts,
|
|
140
231
|
})
|
|
141
232
|
attrClass.set(i, c)
|
|
142
233
|
classified.push(c)
|
|
@@ -150,11 +241,30 @@ export function decideLazyRow(args: BuildLazyRowArgs): {
|
|
|
150
241
|
rowLocalNames,
|
|
151
242
|
indexParam: args.indexParam,
|
|
152
243
|
scope,
|
|
244
|
+
preamble: preambleFacts,
|
|
153
245
|
})
|
|
154
246
|
textClass.set(i, c)
|
|
155
247
|
classified.push(c)
|
|
156
248
|
})
|
|
157
249
|
|
|
250
|
+
const condClass = new Map<number, ClassifiedLazyBinding>()
|
|
251
|
+
condFacts.forEach((c, i) => {
|
|
252
|
+
// Classified like any other binding: a condition reading an outer signal
|
|
253
|
+
// must land in `applyOuter` AND get that signal onto the prime list, or the
|
|
254
|
+
// loop-level effect would never subscribe to it.
|
|
255
|
+
const k = classifyLazyBinding({
|
|
256
|
+
kind: 'attr',
|
|
257
|
+
slotId: c.slotId,
|
|
258
|
+
free: rawConditionals[i].conditionFreeIdentifiers ?? null,
|
|
259
|
+
rowLocalNames,
|
|
260
|
+
indexParam: args.indexParam,
|
|
261
|
+
scope,
|
|
262
|
+
preamble: preambleFacts,
|
|
263
|
+
})
|
|
264
|
+
condClass.set(i, k)
|
|
265
|
+
classified.push(k)
|
|
266
|
+
})
|
|
267
|
+
|
|
158
268
|
// --- §9.4 gate ---------------------------------------------------------
|
|
159
269
|
const shape: LazyRowShapeFacts = {
|
|
160
270
|
callSite: args.callSite,
|
|
@@ -162,12 +272,12 @@ export function decideLazyRow(args: BuildLazyRowArgs): {
|
|
|
162
272
|
anchored: args.anchored,
|
|
163
273
|
bodyIsMultiRoot: loop.bodyIsMultiRoot ?? false,
|
|
164
274
|
hasExplicitKey: loop.key != null,
|
|
165
|
-
|
|
275
|
+
conditionalRefusal,
|
|
166
276
|
childRefCount: loop.bindings.refs?.length ?? 0,
|
|
167
277
|
nestedComponentCount: loop.nestedComponents?.length ?? 0,
|
|
168
278
|
innerLoopCount: loop.innerLoops?.length ?? 0,
|
|
169
279
|
hasChildComponent: 'childComponent' in loop && loop.childComponent != null,
|
|
170
|
-
|
|
280
|
+
mapPreambleRefusal: preambleAnalysis.lazySafe ? null : preambleAnalysis.reason,
|
|
171
281
|
preambleRegionCount: args.preambleRegionCount,
|
|
172
282
|
hasParamUnwrap: args.paramUnwrap.length > 0,
|
|
173
283
|
}
|
|
@@ -198,6 +308,7 @@ export function decideLazyRow(args: BuildLazyRowArgs): {
|
|
|
198
308
|
ordinal: ordinal++,
|
|
199
309
|
readsItem: c.readsItem,
|
|
200
310
|
readsOuter: c.readsOuter,
|
|
311
|
+
readsPreamble: c.readsPreamble,
|
|
201
312
|
}
|
|
202
313
|
})
|
|
203
314
|
const texts: LazyRowTextBinding[] = loop.bindings.reactiveTexts.map((text, i) => {
|
|
@@ -212,6 +323,7 @@ export function decideLazyRow(args: BuildLazyRowArgs): {
|
|
|
212
323
|
// a list keeps exactly the classifier's answer.
|
|
213
324
|
readsItem: c.readsItem || !c.readsOuter,
|
|
214
325
|
readsOuter: c.readsOuter,
|
|
326
|
+
readsPreamble: c.readsPreamble,
|
|
215
327
|
}
|
|
216
328
|
})
|
|
217
329
|
|
|
@@ -222,6 +334,29 @@ export function decideLazyRow(args: BuildLazyRowArgs): {
|
|
|
222
334
|
}
|
|
223
335
|
}
|
|
224
336
|
|
|
337
|
+
// Conditional refs live PAST the door, and are claimed on demand — the row
|
|
338
|
+
// `createRow` just cloned already rendered the correct arm, so there is
|
|
339
|
+
// nothing for it to seed or swap (see `LazyRowConditionalBinding.refIndex`).
|
|
340
|
+
const condRefBase = attrSlotIds.length + (texts.length > 0 ? 1 : 0)
|
|
341
|
+
const conditionals: LazyRowConditionalBinding[] = condFacts.map((c, i) => {
|
|
342
|
+
const klass = condClass.get(i)!
|
|
343
|
+
return {
|
|
344
|
+
slotId: c.slotId,
|
|
345
|
+
wrappedCondition: wrap(c.condition),
|
|
346
|
+
whenTrueHtml: c.whenTrueHtml,
|
|
347
|
+
whenFalseHtml: c.whenFalseHtml,
|
|
348
|
+
refIndex: condRefBase + i,
|
|
349
|
+
ordinal: ordinal++,
|
|
350
|
+
// A condition reading neither the item nor a reactive outer name still
|
|
351
|
+
// has to be applied somewhere; `applyItem` is the harmless choice (the
|
|
352
|
+
// dedup makes a repeat a no-op), matching how a text that classified as
|
|
353
|
+
// neither is handled above.
|
|
354
|
+
readsItem: klass.readsItem || !klass.readsOuter,
|
|
355
|
+
readsOuter: klass.readsOuter,
|
|
356
|
+
readsPreamble: klass.readsPreamble,
|
|
357
|
+
}
|
|
358
|
+
})
|
|
359
|
+
|
|
225
360
|
return {
|
|
226
361
|
plan: {
|
|
227
362
|
attrSlotIds,
|
|
@@ -231,7 +366,14 @@ export function decideLazyRow(args: BuildLazyRowArgs): {
|
|
|
231
366
|
textNeedsRead: texts.some(t => t.readsOuter),
|
|
232
367
|
lastCount: ordinal,
|
|
233
368
|
outerPrimeGetters,
|
|
234
|
-
|
|
369
|
+
preambleStatements: args.mapPreambleWrapped,
|
|
370
|
+
// Which apply bodies must re-run the preamble — computed from the FINAL
|
|
371
|
+
// binding lists, not the raw classification, so a text/conditional that
|
|
372
|
+
// classified as neither item- nor outer-driven (and was therefore forced
|
|
373
|
+
// into `applyItem` above) is counted in the body it actually lands in.
|
|
374
|
+
itemNeedsPreamble: [...attrs, ...texts, ...conditionals].some(b => b.readsItem && b.readsPreamble),
|
|
375
|
+
outerNeedsPreamble: [...attrs, ...texts, ...conditionals].some(b => b.readsOuter && b.readsPreamble),
|
|
376
|
+
conditionals,
|
|
235
377
|
},
|
|
236
378
|
decision,
|
|
237
379
|
}
|
|
@@ -144,7 +144,7 @@ export function buildPlainLoopPlan(
|
|
|
144
144
|
const mapPreambleWrapped = elem.preamble
|
|
145
145
|
? renderPreamble(elem.preamble, {
|
|
146
146
|
transformJs: wrap,
|
|
147
|
-
renderLeaf: (ir) => irToHtmlTemplate(ir, undefined, 1, [{ param: elem.param, bindings: elem.paramBindings }], undefined
|
|
147
|
+
renderLeaf: (ir) => irToHtmlTemplate(ir, undefined, 1, [{ param: elem.param, bindings: elem.paramBindings }], undefined),
|
|
148
148
|
})
|
|
149
149
|
: ''
|
|
150
150
|
const preambleRegions = buildPreambleRegionPlans(elem.preambleRegions, elem.param, elem.paramBindings)
|
|
@@ -269,7 +269,7 @@ function buildStaticLoopMaterialize(
|
|
|
269
269
|
itemTemplate: elem.staticItemTemplate,
|
|
270
270
|
mapPreamble: elem.preamble
|
|
271
271
|
? renderPreamble(elem.preamble, {
|
|
272
|
-
renderLeaf: (ir) => irToHtmlTemplate(ir, undefined, 1, [{ param: elem.param, bindings: elem.paramBindings }], undefined
|
|
272
|
+
renderLeaf: (ir) => irToHtmlTemplate(ir, undefined, 1, [{ param: elem.param, bindings: elem.paramBindings }], undefined),
|
|
273
273
|
})
|
|
274
274
|
: '',
|
|
275
275
|
bodyIsMultiRoot: elem.bodyIsMultiRoot ?? false,
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Is a row's reactive conditional drivable from the LOOP-level apply bodies? —
|
|
3
|
+
* `spec/slot-unification.md` §9.5, the "row contains a reactive conditional"
|
|
4
|
+
* widening.
|
|
5
|
+
*
|
|
6
|
+
* ## Why it was refused wholesale
|
|
7
|
+
*
|
|
8
|
+
* The eager emission calls `insert(__el, 'sN', () => cond, trueArm, falseArm)`
|
|
9
|
+
* once per row. `insert` creates **one `createEffect` per call**, plus it probes
|
|
10
|
+
* both branch templates at runtime to decide element-vs-fragment form, resolves
|
|
11
|
+
* a search region, and manages per-branch `bindEvents` cleanup. Calling it from
|
|
12
|
+
* a lazy row would reinstate exactly the per-row reactive resource the lazy row
|
|
13
|
+
* graph exists to remove — so the gate refused any row with a conditional at
|
|
14
|
+
* all.
|
|
15
|
+
*
|
|
16
|
+
* ## What this accepts, and why that is the whole job
|
|
17
|
+
*
|
|
18
|
+
* For the narrow case where **both arms are wiring-free static elements**,
|
|
19
|
+
* everything `insert` does collapses to one operation: replace the
|
|
20
|
+
* `[bf-c="sN"]` element with the other arm's markup when the condition flips.
|
|
21
|
+
* No `bindEvents`, no branch cleanup, no auto-focus, no `__bfSlot` live-node
|
|
22
|
+
* splicing — those all exist for arms that own something, and these arms own
|
|
23
|
+
* nothing.
|
|
24
|
+
*
|
|
25
|
+
* That case needs no runtime helper. Both arms are compile-time constants, so
|
|
26
|
+
* each is parsed ONCE per loop into a hoisted `<template>` and cloned per swap,
|
|
27
|
+
* the same trick the row template already uses. What is left per row is a
|
|
28
|
+
* boolean, a dedup slot, and a `replaceWith` — which the existing
|
|
29
|
+
* `applyItem`/`applyOuter` bodies can carry directly.
|
|
30
|
+
*
|
|
31
|
+
* ## The four things that must be proven
|
|
32
|
+
*
|
|
33
|
+
* 1. **Both arms are wiring-free.** Every `LoopChildBranchSummary` collection
|
|
34
|
+
* must be empty. A single reactive text inside an arm means the arm's
|
|
35
|
+
* content has to track the item, which needs wiring this shape has no place
|
|
36
|
+
* for.
|
|
37
|
+
* 2. **Both arms are ELEMENT conditionals.** The caller passes each arm through
|
|
38
|
+
* `addCondAttrToTemplate` — the same door the eager emission uses — which
|
|
39
|
+
* injects `bf-c="<slotId>"` on a single root element and otherwise wraps the
|
|
40
|
+
* arm in `<!--bf-cond-start:id-->` markers. So the two forms are told apart
|
|
41
|
+
* by reading its output, not by re-deciding here. A FRAGMENT conditional
|
|
42
|
+
* spans a sibling range with no single node to replace, which is why
|
|
43
|
+
* `insert` carries a second code path for it.
|
|
44
|
+
* 3. **Both arms' HTML is static.** The arm strings are emitted inside a
|
|
45
|
+
* template literal, so an interpolation survives as `${…}`. Anything
|
|
46
|
+
* containing one is refused — it would have to be re-evaluated per row,
|
|
47
|
+
* defeating the hoist. (A literal `${` in authored TEXT would also refuse
|
|
48
|
+
* here; that is the safe direction — a false refusal costs the eager
|
|
49
|
+
* fallback, a false accept would ship a frozen arm.)
|
|
50
|
+
* 4. **The condition does not read the loop INDEX.** `applyItem` and
|
|
51
|
+
* `applyOuter` have no index parameter, the same reason
|
|
52
|
+
* `ClassifiedLazyBinding.referencesIndex` refuses a binding.
|
|
53
|
+
*
|
|
54
|
+
* Everything refused carries a specific reason, which `lazyRowEligibility`
|
|
55
|
+
* passes through unchanged.
|
|
56
|
+
*/
|
|
57
|
+
|
|
58
|
+
import type { LoopChildBranchSummary, LoopChildConditional } from '../../types.ts'
|
|
59
|
+
|
|
60
|
+
/** The prepared arm markup the caller hands in, and this module vets. */
|
|
61
|
+
export interface PreparedArms {
|
|
62
|
+
/**
|
|
63
|
+
* Loop-param wrapped and passed through `addCondAttrToTemplate`, i.e. exactly
|
|
64
|
+
* the strings the eager path would emit. Vetting the PREPARED form is the
|
|
65
|
+
* point: the raw `IRLoop` arm HTML carries no `bf-c` at all, so an
|
|
66
|
+
* element-vs-fragment decision made on it would be guesswork.
|
|
67
|
+
*/
|
|
68
|
+
whenTrueHtml: string
|
|
69
|
+
whenFalseHtml: string
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** A row conditional the loop-level apply bodies can drive. */
|
|
73
|
+
export interface LazyConditionalFacts {
|
|
74
|
+
slotId: string
|
|
75
|
+
/** Condition expression, NOT yet loop-param wrapped (the caller wraps). */
|
|
76
|
+
condition: string
|
|
77
|
+
/** Arm markup, static by construction — hoisted and cloned per swap. */
|
|
78
|
+
whenTrueHtml: string
|
|
79
|
+
whenFalseHtml: string
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export type LazyConditionalAnalysis =
|
|
83
|
+
| { lazySafe: true; facts: LazyConditionalFacts }
|
|
84
|
+
| { lazySafe: false; reason: string }
|
|
85
|
+
|
|
86
|
+
const NO = (reason: string): LazyConditionalAnalysis => ({ lazySafe: false, reason })
|
|
87
|
+
|
|
88
|
+
/** Names every non-empty collection on a branch summary, for the reason text. */
|
|
89
|
+
function wiringOn(branch: LoopChildBranchSummary): string[] {
|
|
90
|
+
const found: string[] = []
|
|
91
|
+
if (branch.childComponents.length > 0) found.push('child components')
|
|
92
|
+
if (branch.innerLoops && branch.innerLoops.length > 0) found.push('an inner loop')
|
|
93
|
+
if (branch.conditionals && branch.conditionals.length > 0) found.push('a nested conditional')
|
|
94
|
+
if (branch.events && branch.events.length > 0) found.push('events')
|
|
95
|
+
if (branch.reactiveAttrs && branch.reactiveAttrs.length > 0) found.push('reactive attrs')
|
|
96
|
+
if (branch.reactiveTexts && branch.reactiveTexts.length > 0) found.push('reactive text')
|
|
97
|
+
return found
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Decide whether `cond` can be driven from the loop-level apply bodies.
|
|
102
|
+
*
|
|
103
|
+
* `indexParam` is the loop's index parameter name as the emitter uses it
|
|
104
|
+
* (`elem.index || '__idx'`); a condition reading it is refused.
|
|
105
|
+
*/
|
|
106
|
+
export function analyzeLazyConditional(
|
|
107
|
+
cond: LoopChildConditional,
|
|
108
|
+
indexParam: string,
|
|
109
|
+
arms: PreparedArms,
|
|
110
|
+
): LazyConditionalAnalysis {
|
|
111
|
+
for (const [label, branch] of [['true', cond.whenTrue], ['false', cond.whenFalse]] as const) {
|
|
112
|
+
const wiring = wiringOn(branch)
|
|
113
|
+
if (wiring.length > 0) {
|
|
114
|
+
return NO(`conditional on slot ${cond.slotId}: its ${label} arm owns ${wiring.join(' + ')}`)
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
for (const [label, html] of [['true', arms.whenTrueHtml], ['false', arms.whenFalseHtml]] as const) {
|
|
119
|
+
if (html.includes('bf-cond-start:')) {
|
|
120
|
+
return NO(`conditional on slot ${cond.slotId}: its ${label} arm is a fragment conditional`)
|
|
121
|
+
}
|
|
122
|
+
if (!html.includes(`bf-c="${cond.slotId}"`)) {
|
|
123
|
+
return NO(`conditional on slot ${cond.slotId}: its ${label} arm has no single bf-c root`)
|
|
124
|
+
}
|
|
125
|
+
// The arm is emitted inside a template literal; a surviving `${` means the
|
|
126
|
+
// markup depends on the item and cannot be hoisted once per loop.
|
|
127
|
+
if (html.includes('${')) {
|
|
128
|
+
return NO(`conditional on slot ${cond.slotId}: its ${label} arm interpolates a value`)
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// `conditionFreeIdentifiers` is pre-computed by the analyzer (#1267). Absent
|
|
133
|
+
// means unprovable, which is a refusal rather than an assumption — the same
|
|
134
|
+
// stance the binding gate takes for a missing identifier set.
|
|
135
|
+
if (!cond.conditionFreeIdentifiers) {
|
|
136
|
+
return NO(`conditional on slot ${cond.slotId}: condition has no analyzable identifier set`)
|
|
137
|
+
}
|
|
138
|
+
if (cond.conditionFreeIdentifiers.has(indexParam)) {
|
|
139
|
+
return NO(`conditional on slot ${cond.slotId}: condition reads the loop index parameter '${indexParam}'`)
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return {
|
|
143
|
+
lazySafe: true,
|
|
144
|
+
facts: {
|
|
145
|
+
slotId: cond.slotId,
|
|
146
|
+
condition: cond.condition,
|
|
147
|
+
whenTrueHtml: arms.whenTrueHtml,
|
|
148
|
+
whenFalseHtml: arms.whenFalseHtml,
|
|
149
|
+
},
|
|
150
|
+
}
|
|
151
|
+
}
|