@barefootjs/jsx 0.26.2 → 0.26.4
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/interface.d.ts +29 -0
- package/dist/adapters/interface.d.ts.map +1 -1
- package/dist/adapters/jsx-adapter.d.ts +9 -0
- package/dist/adapters/jsx-adapter.d.ts.map +1 -1
- package/dist/adapters/loop-bound-names.d.ts.map +1 -1
- package/dist/adapters/parsed-expr-emitter.d.ts +0 -10
- package/dist/adapters/parsed-expr-emitter.d.ts.map +1 -1
- package/dist/adapters/test-adapter.d.ts.map +1 -1
- package/dist/analyzer-context.d.ts +11 -1
- package/dist/analyzer-context.d.ts.map +1 -1
- package/dist/analyzer.d.ts +40 -1
- package/dist/analyzer.d.ts.map +1 -1
- package/dist/expression-parser.d.ts.map +1 -1
- package/dist/index.js +1086 -269
- package/dist/ir-to-client-js/build-references.d.ts.map +1 -1
- 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 +14 -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-component-loop.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/plan/build-composite-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-inner-loop.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 +39 -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 +12 -1
- package/dist/ir-to-client-js/control-flow/shared.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/stringify/event-delegation.d.ts +1 -1
- 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/loop.d.ts +19 -0
- 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/html-template.d.ts +67 -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/plan/build-static-array-child-init.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 +29 -4
- package/dist/ir-to-client-js/types.d.ts.map +1 -1
- package/dist/jsx-to-ir.d.ts.map +1 -1
- package/dist/loop-destructure.d.ts.map +1 -1
- package/dist/strip-types.d.ts +18 -0
- package/dist/strip-types.d.ts.map +1 -1
- package/dist/types.d.ts +143 -32
- package/dist/types.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/__tests__/__snapshots__/doc-examples.test.ts.snap +277 -33
- package/src/__tests__/client-js-generation.test.ts +18 -5
- package/src/__tests__/compiler-runtime-contract.test.ts +4 -4
- package/src/__tests__/compiler-stress-1244.test.ts +12 -1
- package/src/__tests__/delegated-handler-preamble.test.ts +153 -0
- package/src/__tests__/event-delegation-scope-2367.test.ts +108 -0
- package/src/__tests__/flatmap-segments.test.ts +262 -0
- package/src/__tests__/map-arbitrary-body.test.ts +226 -0
- package/src/__tests__/map-body-no-silent-divergence.test.ts +405 -0
- package/src/__tests__/map-multi-return-body.test.ts +172 -0
- package/src/__tests__/preamble-region-patch.test.ts +150 -0
- package/src/__tests__/static-loop-csr-materialize.test.ts +3 -2
- package/src/__tests__/unsupported-expression.test.ts +20 -2
- package/src/adapters/interface.ts +40 -0
- package/src/adapters/jsx-adapter.ts +10 -0
- package/src/adapters/loop-bound-names.ts +6 -2
- package/src/adapters/parsed-expr-emitter.ts +5 -10
- package/src/adapters/test-adapter.ts +10 -0
- package/src/analyzer-context.ts +35 -1
- package/src/analyzer.ts +162 -24
- package/src/compiler.ts +2 -2
- package/src/expression-parser.ts +27 -19
- package/src/ir-to-client-js/build-references.ts +19 -2
- package/src/ir-to-client-js/collect-elements.ts +69 -13
- package/src/ir-to-client-js/control-flow/plan/branch-loop.ts +14 -1
- package/src/ir-to-client-js/control-flow/plan/build-branch-loop.ts +18 -5
- package/src/ir-to-client-js/control-flow/plan/build-component-loop.ts +4 -0
- package/src/ir-to-client-js/control-flow/plan/build-composite-loop.ts +15 -2
- package/src/ir-to-client-js/control-flow/plan/build-event-delegation.ts +32 -3
- package/src/ir-to-client-js/control-flow/plan/build-inner-loop.ts +26 -3
- package/src/ir-to-client-js/control-flow/plan/build-loop.ts +53 -2
- 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 +40 -0
- package/src/ir-to-client-js/control-flow/shared.ts +28 -2
- package/src/ir-to-client-js/control-flow/stringify/branch-loop.ts +25 -3
- package/src/ir-to-client-js/control-flow/stringify/event-delegation.ts +76 -21
- package/src/ir-to-client-js/control-flow/stringify/loop.ts +65 -1
- package/src/ir-to-client-js/control-flow.ts +11 -0
- package/src/ir-to-client-js/html-template.ts +244 -39
- package/src/ir-to-client-js/imports.ts +1 -1
- package/src/ir-to-client-js/plan/build-static-array-child-init.ts +22 -10
- package/src/ir-to-client-js/reactivity.ts +6 -0
- package/src/ir-to-client-js/types.ts +28 -3
- package/src/jsx-to-ir.ts +1059 -167
- package/src/loop-destructure.ts +9 -5
- package/src/rich-type-refusal.ts +6 -2
- package/src/strip-types.ts +47 -0
- package/src/types.ts +159 -35
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* if (<container>) <container>.addEventListener('<eventName>', (__bfEvt) => {
|
|
7
7
|
* const target = __bfEvt.target
|
|
8
8
|
* const <slot>El = target.closest('[bf="<slotId>"]')
|
|
9
|
-
* if (<slot>El) {
|
|
9
|
+
* if (<slot>El && <container>.contains(<slot>El)) {
|
|
10
10
|
* <item-lookup specific>
|
|
11
11
|
* <handlerCall>
|
|
12
12
|
* return
|
|
@@ -74,6 +74,26 @@ function indexBindingLine(handler: string, indexParam: string | null, indexExpr:
|
|
|
74
74
|
return `const ${indexParam} = ${indexExpr}`
|
|
75
75
|
}
|
|
76
76
|
|
|
77
|
+
/**
|
|
78
|
+
* Splice-only-when-referenced (#3, BUG-3 fix part 2): a preamble is dead
|
|
79
|
+
* weight for an event whose handler never reads one of the names it
|
|
80
|
+
* declares — the common case (an unused array builder like `cells` in the
|
|
81
|
+
* BUG-3 repro). Only emit `mapPreamble` when `handler`'s free identifiers
|
|
82
|
+
* intersect `declaredNames`; otherwise the delegated handler pays nothing
|
|
83
|
+
* for a preamble it never uses. `declaredNames` is empty whenever
|
|
84
|
+
* `mapPreamble` is `null`, so the `!mapPreamble` short-circuit is mostly
|
|
85
|
+
* redundant but keeps this correct-by-construction if that ever changes.
|
|
86
|
+
*/
|
|
87
|
+
function preambleLineForHandler(
|
|
88
|
+
mapPreamble: string | null,
|
|
89
|
+
declaredNames: readonly string[],
|
|
90
|
+
handler: string,
|
|
91
|
+
): string | null {
|
|
92
|
+
if (!mapPreamble || declaredNames.length === 0) return null
|
|
93
|
+
const free = extractFreeIdentifiersFromText(handler)
|
|
94
|
+
return declaredNames.some((name) => free.has(name)) ? mapPreamble : null
|
|
95
|
+
}
|
|
96
|
+
|
|
77
97
|
export function stringifyEventDelegation(lines: string[], plan: EventDelegationPlan): void {
|
|
78
98
|
const { containerVar, events, itemLookup, profileComponentName } = plan
|
|
79
99
|
const eventsByName = new Map<string, LoopChildEvent[]>()
|
|
@@ -94,8 +114,15 @@ export function stringifyEventDelegation(lines: string[], plan: EventDelegationP
|
|
|
94
114
|
lines.push(` const target = __bfEvt.target`)
|
|
95
115
|
for (const ev of evs) {
|
|
96
116
|
const childVar = varSlotId(ev.childSlotId)
|
|
117
|
+
// Bound the slot match to the delegating container (#2367). `bf` ids are
|
|
118
|
+
// unique only *within* a component, so an unscoped
|
|
119
|
+
// `target.closest('[bf="sN"]')` can match a same-id element in an ancestor
|
|
120
|
+
// component and silently take the wrong branch. The container holds every
|
|
121
|
+
// slot it delegates on, so a same-id ancestor is never a descendant of it —
|
|
122
|
+
// the `container.contains(...)` guard rejects the foreign match and the
|
|
123
|
+
// handler falls through to the correct branch.
|
|
97
124
|
lines.push(` const ${childVar}El = target.closest('[bf="${ev.childSlotId}"]')`)
|
|
98
|
-
lines.push(` if (${childVar}El) {`)
|
|
125
|
+
lines.push(` if (${childVar}El && ${containerVar}.contains(${childVar}El)) {`)
|
|
99
126
|
const handlerCall = withTurn(`(${ev.handler.trim()})(__bfEvt)`, profileComponentName, ev.childSlotId, ev.eventName)
|
|
100
127
|
switch (itemLookup.kind) {
|
|
101
128
|
case 'keyed':
|
|
@@ -126,7 +153,8 @@ function emitKeyedLookup(
|
|
|
126
153
|
handlerCall: string,
|
|
127
154
|
lookup: KeyedItemLookup,
|
|
128
155
|
): void {
|
|
129
|
-
const { arrayExpr, param, keyWithItem, mapPreamble, hasBindings, indexParam } = lookup
|
|
156
|
+
const { arrayExpr, param, keyWithItem, mapPreamble, mapPreambleDeclaredNames, hasBindings, indexParam } = lookup
|
|
157
|
+
const preambleLine = preambleLineForHandler(mapPreamble, mapPreambleDeclaredNames, ev.handler)
|
|
130
158
|
|
|
131
159
|
if (ev.nestedLoops.length === 0) {
|
|
132
160
|
// Single-level keyed lookup.
|
|
@@ -139,15 +167,28 @@ function emitKeyedLookup(
|
|
|
139
167
|
ls.push(` const __bfLoopItem = ${arrayExpr}.find(item => String(${keyWithItem}) === key)`)
|
|
140
168
|
ls.push(` if (__bfLoopItem) {`)
|
|
141
169
|
ls.push(` const ${param} = __bfLoopItem`)
|
|
142
|
-
if (
|
|
170
|
+
if (preambleLine) ls.push(` ${preambleLine}`)
|
|
143
171
|
if (idxLine) ls.push(` ${idxLine}`)
|
|
144
|
-
|
|
172
|
+
// Leading `;` (not just relying on the preceding line's own
|
|
173
|
+
// semicolon): `handlerCall` always starts with `(` — an ASI hazard
|
|
174
|
+
// pre-existing in this branch even with neither optional line above
|
|
175
|
+
// (`const ${param} = __bfLoopItem` has no trailing `;`), which glues
|
|
176
|
+
// the call onto it as `__bfLoopItem(...)` and throws
|
|
177
|
+
// `TypeError: __bfLoopItem is not a function`. Defend at the one
|
|
178
|
+
// emission point rather than chasing every preceding-line shape.
|
|
179
|
+
ls.push(` ;${handlerCall}`)
|
|
145
180
|
ls.push(` }`)
|
|
146
181
|
} else {
|
|
182
|
+
// The preamble (when referenced) and the handler call both run INSIDE
|
|
183
|
+
// the item null guard — a `.find()` miss (stale-DOM race, e.g. the
|
|
184
|
+
// clicked row's key no longer in the current array) must short-circuit
|
|
185
|
+
// before a preamble that dereferences the item ever runs (BUG-4).
|
|
147
186
|
ls.push(` const ${param} = ${arrayExpr}.find(item => String(${keyWithItem}) === key)`)
|
|
148
|
-
|
|
149
|
-
if (
|
|
150
|
-
|
|
187
|
+
ls.push(` if (${param}) {`)
|
|
188
|
+
if (preambleLine) ls.push(` ${preambleLine}`)
|
|
189
|
+
if (idxLine) ls.push(` ${idxLine}`)
|
|
190
|
+
ls.push(` ;${handlerCall}`)
|
|
191
|
+
ls.push(` }`)
|
|
151
192
|
}
|
|
152
193
|
ls.push(` }`)
|
|
153
194
|
return
|
|
@@ -180,10 +221,14 @@ function emitKeyedLookup(
|
|
|
180
221
|
}
|
|
181
222
|
const outerGuard = hasBindings ? '__bfLoopItem' : param
|
|
182
223
|
const allParams = [outerGuard, ...ev.nestedLoops.map(n => n.param)]
|
|
183
|
-
|
|
224
|
+
// Preamble and idx binding run INSIDE the combined item guard (BUG-4) —
|
|
225
|
+
// a nested `.find()` miss must short-circuit before the preamble runs.
|
|
184
226
|
const idxLine = indexBindingLine(ev.handler, indexParam, `${arrayExpr}.findIndex(item => String(${keyWithItem}) === outerKey)`)
|
|
185
|
-
|
|
186
|
-
|
|
227
|
+
ls.push(` if (${allParams.join(' && ')}) {`)
|
|
228
|
+
if (preambleLine) ls.push(` ${preambleLine}`)
|
|
229
|
+
if (idxLine) ls.push(` ${idxLine}`)
|
|
230
|
+
ls.push(` ;${handlerCall}`)
|
|
231
|
+
ls.push(` }`)
|
|
187
232
|
}
|
|
188
233
|
|
|
189
234
|
function emitDynamicIndexLookup(
|
|
@@ -192,7 +237,8 @@ function emitDynamicIndexLookup(
|
|
|
192
237
|
handlerCall: string,
|
|
193
238
|
lookup: DynamicIndexItemLookup,
|
|
194
239
|
): void {
|
|
195
|
-
const { arrayExpr, param, mapPreamble, hasBindings, indexParam } = lookup
|
|
240
|
+
const { arrayExpr, param, mapPreamble, mapPreambleDeclaredNames, hasBindings, indexParam } = lookup
|
|
241
|
+
const preambleLine = preambleLineForHandler(mapPreamble, mapPreambleDeclaredNames, ev.handler)
|
|
196
242
|
const idxLine = indexBindingLine(ev.handler, indexParam, 'idx')
|
|
197
243
|
ls.push(` const li = ${varSlotId(ev.childSlotId)}El.closest('li, [bf-i]')`)
|
|
198
244
|
ls.push(` if (li && li.parentElement) {`)
|
|
@@ -201,15 +247,19 @@ function emitDynamicIndexLookup(
|
|
|
201
247
|
ls.push(` const __bfLoopItem = ${arrayExpr}[idx]`)
|
|
202
248
|
ls.push(` if (__bfLoopItem) {`)
|
|
203
249
|
ls.push(` const ${param} = __bfLoopItem`)
|
|
204
|
-
if (
|
|
250
|
+
if (preambleLine) ls.push(` ${preambleLine}`)
|
|
205
251
|
if (idxLine) ls.push(` ${idxLine}`)
|
|
206
|
-
ls.push(`
|
|
252
|
+
ls.push(` ;${handlerCall}`)
|
|
207
253
|
ls.push(` }`)
|
|
208
254
|
} else {
|
|
255
|
+
// Preamble and idx binding run INSIDE the item null guard (BUG-4) — an
|
|
256
|
+
// out-of-range index (stale-DOM race) must short-circuit first.
|
|
209
257
|
ls.push(` const ${param} = ${arrayExpr}[idx]`)
|
|
210
|
-
|
|
211
|
-
if (
|
|
212
|
-
|
|
258
|
+
ls.push(` if (${param}) {`)
|
|
259
|
+
if (preambleLine) ls.push(` ${preambleLine}`)
|
|
260
|
+
if (idxLine) ls.push(` ${idxLine}`)
|
|
261
|
+
ls.push(` ;${handlerCall}`)
|
|
262
|
+
ls.push(` }`)
|
|
213
263
|
}
|
|
214
264
|
ls.push(` }`)
|
|
215
265
|
}
|
|
@@ -221,7 +271,8 @@ function emitStaticIndexLookup(
|
|
|
221
271
|
lookup: StaticIndexItemLookup,
|
|
222
272
|
containerVar: string,
|
|
223
273
|
): void {
|
|
224
|
-
const { arrayExpr, param, mapPreamble, offset, indexParam } = lookup
|
|
274
|
+
const { arrayExpr, param, mapPreamble, mapPreambleDeclaredNames, offset, indexParam } = lookup
|
|
275
|
+
const preambleLine = preambleLineForHandler(mapPreamble, mapPreambleDeclaredNames, ev.handler)
|
|
225
276
|
const idxLine = indexBindingLine(ev.handler, indexParam, '__idx')
|
|
226
277
|
ls.push(` let __el = ${varSlotId(ev.childSlotId)}El`)
|
|
227
278
|
ls.push(` while (__el.parentElement && __el.parentElement !== ${containerVar}) __el = __el.parentElement`)
|
|
@@ -229,8 +280,12 @@ function emitStaticIndexLookup(
|
|
|
229
280
|
const idxOffset = buildLoopChildIndexSubtraction(offset ?? undefined)
|
|
230
281
|
ls.push(` const __idx = Array.from(${containerVar}.children).indexOf(__el)${idxOffset}`)
|
|
231
282
|
ls.push(` const ${param} = ${arrayExpr}[__idx]`)
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
283
|
+
// Preamble and idx binding run INSIDE the item null guard (BUG-4) — an
|
|
284
|
+
// out-of-range index (stale-DOM race) must short-circuit first.
|
|
285
|
+
ls.push(` if (${param}) {`)
|
|
286
|
+
if (preambleLine) ls.push(` ${preambleLine}`)
|
|
287
|
+
if (idxLine) ls.push(` ${idxLine}`)
|
|
288
|
+
ls.push(` ;${handlerCall}`)
|
|
289
|
+
ls.push(` }`)
|
|
235
290
|
ls.push(` }`)
|
|
236
291
|
}
|
|
@@ -32,6 +32,7 @@ import { buildSkeletonPathPlan, type SkeletonPathPlan } from './skeleton-paths.t
|
|
|
32
32
|
import { stringifyComponentLoop } from './component-loop.ts'
|
|
33
33
|
import { stringifyCompositeLoop } from './composite-loop.ts'
|
|
34
34
|
import type { LoopChildRefBinding, LoopPlan, PlainLoopPlan, StaticLoopPlan } from '../plan/types.ts'
|
|
35
|
+
import type { PreambleRegionPlan } from '../plan/loop.ts'
|
|
35
36
|
|
|
36
37
|
/**
|
|
37
38
|
* Emit `(callback)(__rf)` for each ref on a per-item slot, looking up the
|
|
@@ -64,6 +65,40 @@ export function emitLoopChildRefs(
|
|
|
64
65
|
}
|
|
65
66
|
}
|
|
66
67
|
|
|
68
|
+
/**
|
|
69
|
+
* Emit the region-patch effect for each preamble-patched region (#2389 —
|
|
70
|
+
* `arr.map(t => { const cells = []; ...; return <tr>{cells}<td>{t.name}</td></tr> })`).
|
|
71
|
+
* The effect re-runs the (loop-param-accessor-wrapped) preamble on every
|
|
72
|
+
* reactive tick so it re-reads the current per-item signal, recomputes the
|
|
73
|
+
* region's value, and — past the FIRST run (which only records, trusting
|
|
74
|
+
* the SSR/CSR mount-time content already in the DOM) — patches the DOM
|
|
75
|
+
* range via `patchSlotRange(__el, 'sN', html)`. The marker lookup lives
|
|
76
|
+
* inside `patchSlotRange`, so a row that never changes pays zero lookup
|
|
77
|
+
* cost at mount/adoption.
|
|
78
|
+
*
|
|
79
|
+
* Non-empty `regions` forces the caller's multi-line renderItem layout (the
|
|
80
|
+
* effect needs `__el` as a query root), mirroring `emitLoopChildRefs`.
|
|
81
|
+
*/
|
|
82
|
+
export function emitPreambleRegionEffects(
|
|
83
|
+
lines: string[],
|
|
84
|
+
regions: readonly PreambleRegionPlan[],
|
|
85
|
+
mapPreambleWrapped: string,
|
|
86
|
+
opts: { indent: string; elVar: string },
|
|
87
|
+
): void {
|
|
88
|
+
if (regions.length === 0) return
|
|
89
|
+
const { indent, elVar } = opts
|
|
90
|
+
for (const region of regions) {
|
|
91
|
+
const v = varSlotId(region.slotId)
|
|
92
|
+
lines.push(`${indent}{ let __last_${v}`)
|
|
93
|
+
lines.push(`${indent}createEffect(() => {`)
|
|
94
|
+
if (mapPreambleWrapped) lines.push(`${indent} ${mapPreambleWrapped}`)
|
|
95
|
+
lines.push(`${indent} const __html_${v} = ${region.valueExpr}`)
|
|
96
|
+
lines.push(`${indent} if (__last_${v} === undefined) { __last_${v} = __html_${v}; return }`)
|
|
97
|
+
lines.push(`${indent} if (__html_${v} !== __last_${v}) { __last_${v} = __html_${v}; patchSlotRange(${elVar}, '${region.slotId}', __html_${v}) }`)
|
|
98
|
+
lines.push(`${indent}}) }`)
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
67
102
|
/**
|
|
68
103
|
* Single dispatch over `LoopPlan` (#1253). Narrows on `plan.kind` and
|
|
69
104
|
* delegates to the per-variant stringifier. Callers should consume this
|
|
@@ -128,8 +163,33 @@ export function stringifyPlainLoop(
|
|
|
128
163
|
bodyIsMultiRoot,
|
|
129
164
|
anchored,
|
|
130
165
|
anchorKeyExpr,
|
|
166
|
+
preambleRegions,
|
|
131
167
|
} = plan
|
|
132
168
|
|
|
169
|
+
// flatMap descriptor mode: the accessor flattens the source through the
|
|
170
|
+
// callback body (each leaf a `({ k, h })` descriptor), mapArray keys on
|
|
171
|
+
// `d.k`, and each item element is built from — and patched against — its
|
|
172
|
+
// rendered HTML. Hydration adopts the SSR leaf in place (`__last` seeds
|
|
173
|
+
// undefined so the first effect run records without patching — trust SSR,
|
|
174
|
+
// same contract as plain rows); a later same-key HTML change patches the
|
|
175
|
+
// element wholesale via `patchLeaf` (flatMap leaves carry no per-slot
|
|
176
|
+
// wiring by construction — the compiler refuses leaves that would).
|
|
177
|
+
if (plan.flatMapLeafItem) {
|
|
178
|
+
const loopBfIdArg = plan.profileLoopId ? `, ${JSON.stringify(plan.profileLoopId)}` : ''
|
|
179
|
+
lines.push(`${topIndent}mapArray(() => ${arrayExpr}, ${containerVar}, ${keyFn}, (__bfD, ${indexParam}, __existing) => {`)
|
|
180
|
+
lines.push(`${topIndent} let __el = __existing`)
|
|
181
|
+
lines.push(`${topIndent} if (!__el) { const __tpl = document.createElement('template'); __tpl.innerHTML = __bfD().h; __el = __tpl.content.firstElementChild }`)
|
|
182
|
+
lines.push(`${topIndent} let __last = __existing ? undefined : __bfD().h`)
|
|
183
|
+
lines.push(`${topIndent} createEffect(() => {`)
|
|
184
|
+
lines.push(`${topIndent} const __html = __bfD().h`)
|
|
185
|
+
lines.push(`${topIndent} if (__last === undefined) { __last = __html; return }`)
|
|
186
|
+
lines.push(`${topIndent} if (__html !== __last) { __last = __html; patchLeaf(__el, __html) }`)
|
|
187
|
+
lines.push(`${topIndent} })`)
|
|
188
|
+
lines.push(`${topIndent} return __el`)
|
|
189
|
+
lines.push(`${topIndent}}, '${markerId}'${loopBfIdArg})`)
|
|
190
|
+
return
|
|
191
|
+
}
|
|
192
|
+
|
|
133
193
|
// Whole-item conditional loops (#1665) render 0-or-1 element per item, so
|
|
134
194
|
// they route through `mapArrayAnchored`. The renderItem returns a fragment
|
|
135
195
|
// headed by a `<!--bf-loop-i:KEY-->` anchor and seeded with the
|
|
@@ -158,7 +218,10 @@ export function stringifyPlainLoop(
|
|
|
158
218
|
// the factory, so non-empty refs force the multi-line layout the same way
|
|
159
219
|
// reactive effects do (#1244).
|
|
160
220
|
const loopBfId = plan.profileLoopId ? `, ${JSON.stringify(plan.profileLoopId)}` : ''
|
|
161
|
-
|
|
221
|
+
// Preamble-patched regions (#2389) need `__el` as a query root for their
|
|
222
|
+
// effect, so non-empty regions force the multi-line layout — same
|
|
223
|
+
// precedent as `childRefs` above.
|
|
224
|
+
if (reactiveEffects === null && !bodyIsMultiRoot && childRefs.length === 0 && preambleRegions.length === 0) {
|
|
162
225
|
// Single-line renderItem (no reactive effects, single root, no refs).
|
|
163
226
|
const unwrapInline = paramUnwrap ? `${paramUnwrap} ` : ''
|
|
164
227
|
const preamble = mapPreambleWrapped ? `${mapPreambleWrapped}; ` : ''
|
|
@@ -217,6 +280,7 @@ export function stringifyPlainLoop(
|
|
|
217
280
|
})
|
|
218
281
|
}
|
|
219
282
|
emitLoopChildRefs(lines, childRefs, { indent: bodyIndent, elVar: '__el', bodyIsMultiRoot, elementIndexBySlot: pathPlan?.elementIndexBySlot })
|
|
283
|
+
emitPreambleRegionEffects(lines, preambleRegions, mapPreambleWrapped, { indent: bodyIndent, elVar: '__el' })
|
|
220
284
|
lines.push(`${bodyIndent}return __el`)
|
|
221
285
|
lines.push(`${topIndent}}, '${markerId}'${loopBfId})`)
|
|
222
286
|
}
|
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
*/
|
|
22
22
|
|
|
23
23
|
import type { ClientJsContext, TopLevelLoop } from './types.ts'
|
|
24
|
+
import { internalInvariant } from '../errors.ts'
|
|
24
25
|
import { buildInsertPlan } from './control-flow/plan/build-insert.ts'
|
|
25
26
|
import { stringifyInsert } from './control-flow/stringify/insert.ts'
|
|
26
27
|
import { buildLoopPlan } from './control-flow/plan/build-loop.ts'
|
|
@@ -70,6 +71,16 @@ export function emitLoopUpdates(lines: string[], ctx: ClientJsContext, unsafeLoc
|
|
|
70
71
|
unsafeLocalNames,
|
|
71
72
|
profileComponentName: ctx.profile ? ctx.componentName : undefined,
|
|
72
73
|
})
|
|
74
|
+
// Stage 3 root cure — a JSX-bearing preamble can only be spliced into a
|
|
75
|
+
// string-templated row (renderPreamble). Every shape that reaches a
|
|
76
|
+
// 'dom-ops' variant today is already refused in Phase 1 with a proper
|
|
77
|
+
// source location; this backstop exists for FUTURE variants, so a new
|
|
78
|
+
// plan kind that declares 'dom-ops' cannot silently drop the preamble —
|
|
79
|
+
// it fails the build here instead.
|
|
80
|
+
internalInvariant(
|
|
81
|
+
!(elem.preamble && elem.preamble.builderNames.length > 0 && plan.rowConstruction === 'dom-ops'),
|
|
82
|
+
`loop variant '${plan.kind}' declares dom-ops row construction but received a JSX-bearing preamble — add a Phase-1 refusal (or wire renderPreamble support) for this shape`,
|
|
83
|
+
)
|
|
73
84
|
stringifyLoop(lines, plan)
|
|
74
85
|
emitLoopEventDelegation(lines, elem, plan.kind, ctx.profile ? ctx.componentName : undefined)
|
|
75
86
|
}
|