@barefootjs/jsx 0.28.1 → 0.30.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/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 +890 -357
- 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/composite-loop.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/control-flow/stringify/template-parse.d.ts +14 -0
- package/dist/ir-to-client-js/control-flow/stringify/template-parse.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/imports.d.ts +2 -2
- package/dist/ir-to-client-js/imports.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/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/dist/value-references.d.ts +17 -4
- package/dist/value-references.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 +96 -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__/ir-to-client-js/imports.test.ts +18 -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__/loop-row-creation-paths.test.ts +169 -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/__tests__/value-references.test.ts +88 -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/composite-loop.ts +4 -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/control-flow/stringify/template-parse.ts +23 -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/imports.ts +5 -0
- 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 +400 -24
- 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
- package/src/value-references.ts +26 -4
|
@@ -70,7 +70,12 @@ import {
|
|
|
70
70
|
emitTemplateCloneInline,
|
|
71
71
|
hoistedCloneExpr,
|
|
72
72
|
} from './template-parse.ts'
|
|
73
|
-
import type {
|
|
73
|
+
import type {
|
|
74
|
+
LazyRowAttrBinding,
|
|
75
|
+
LazyRowConditionalBinding,
|
|
76
|
+
LazyRowPlanData,
|
|
77
|
+
LazyRowTextBinding,
|
|
78
|
+
} from '../plan/build-lazy-row.ts'
|
|
74
79
|
|
|
75
80
|
export interface StringifyLazyRowOptions {
|
|
76
81
|
/** Indent of the `mapArrayLazy(` line itself. */
|
|
@@ -94,9 +99,11 @@ export function stringifyLazyRowLoop(lines: string[], o: StringifyLazyRowOptions
|
|
|
94
99
|
const { indent, lazyRow, paramHead } = o
|
|
95
100
|
const mid = o.markerId.replace(/[^A-Za-z0-9_$]/g, '_')
|
|
96
101
|
const tplVar = `__tpl_${mid}`
|
|
97
|
-
const claimVar = `__lzc_${mid}`
|
|
98
102
|
const hasRefs = lazyRow.attrSlotIds.length > 0 || lazyRow.texts.length > 0
|
|
99
|
-
|
|
103
|
+
// Conditionals count: `createRow` seeds their dedup boolean into `__l`, so a
|
|
104
|
+
// row whose ONLY reactive thing is a conditional still needs `__l` declared.
|
|
105
|
+
const hasBindings =
|
|
106
|
+
lazyRow.attrs.length > 0 || lazyRow.texts.length > 0 || lazyRow.conditionals.length > 0
|
|
100
107
|
// ONE per-loop door decision (see `refParts`): the read-capable claim, or
|
|
101
108
|
// today's write-only writer. Never decided per binding — the door is
|
|
102
109
|
// allocated once per ROW.
|
|
@@ -165,28 +172,17 @@ export function stringifyLazyRowLoop(lines: string[], o: StringifyLazyRowOptions
|
|
|
165
172
|
}
|
|
166
173
|
}
|
|
167
174
|
|
|
168
|
-
//
|
|
169
|
-
//
|
|
170
|
-
//
|
|
171
|
-
//
|
|
172
|
-
//
|
|
173
|
-
//
|
|
174
|
-
//
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
// is nothing to defer (see the `deferDoor` note on `refParts`).
|
|
180
|
-
if (hasRefs) {
|
|
181
|
-
// With the door deferred, the ELEMENT refs are the only parts that read the
|
|
182
|
-
// row root — so a text-only row (no reactive-attr slot) claims to a bare
|
|
183
|
-
// `[null]` and the `__el` binding would be dead. `parts` decides, rather
|
|
184
|
-
// than a second predicate that could drift from `refParts`.
|
|
185
|
-
const parts = refParts(lazyRow, '__el', null, adoptedPlanVar, true)
|
|
186
|
-
lines.push(`${indent}const ${claimVar} = (__e) => {`)
|
|
187
|
-
if (parts.some(p => p.includes('__el'))) lines.push(`${indent} const __el = __e.primaryEl`)
|
|
188
|
-
lines.push(`${indent} return [${parts.join(', ')}]`)
|
|
189
|
-
lines.push(`${indent}}`)
|
|
175
|
+
// Hoisted arm templates, one pair per conditional. Both arms are static by
|
|
176
|
+
// construction (`analyzeLazyConditional`), so each is parsed ONCE per loop and
|
|
177
|
+
// cloned on a flip — a per-row `innerHTML` parse would undo the point of
|
|
178
|
+
// driving the swap from the loop level. The comparison the seed run makes is
|
|
179
|
+
// against `content.firstElementChild.outerHTML`, i.e. the browser's OWN
|
|
180
|
+
// serialization of the same markup, so it cannot false-mismatch on attribute
|
|
181
|
+
// order or quoting the way comparing against the authored string would.
|
|
182
|
+
for (const c of lazyRow.conditionals) {
|
|
183
|
+
const v = condVars(mid, c.slotId)
|
|
184
|
+
emitHoistedTemplateDecl(lines, indent, v.trueTpl, c.whenTrueHtml)
|
|
185
|
+
emitHoistedTemplateDecl(lines, indent, v.falseTpl, c.whenFalseHtml)
|
|
190
186
|
}
|
|
191
187
|
|
|
192
188
|
const call = `mapArrayLazy(() => ${o.arrayExpr}, ${o.containerVar}, ${o.keyFn}, {`
|
|
@@ -200,6 +196,9 @@ export function stringifyLazyRowLoop(lines: string[], o: StringifyLazyRowOptions
|
|
|
200
196
|
// interpolates the per-row template, which reads the param for at least
|
|
201
197
|
// the `data-key` attribute.
|
|
202
198
|
lines.push(`${b2}const ${paramHead} = () => __e.item`)
|
|
199
|
+
// The row's `.map()` callback preamble, before the clone: a non-hoisted
|
|
200
|
+
// per-row template interpolates values the preamble declares.
|
|
201
|
+
if (lazyRow.preambleStatements) lines.push(`${b2}${lazyRow.preambleStatements}`)
|
|
203
202
|
const cloneExpr = useHoisted
|
|
204
203
|
? hoistedCloneExpr(tplVar, o.skeletonTemplate!)
|
|
205
204
|
: `(() => { ${emitTemplateCloneInline(o.template)} })()`
|
|
@@ -213,31 +212,42 @@ export function stringifyLazyRowLoop(lines: string[], o: StringifyLazyRowOptions
|
|
|
213
212
|
const createDoor = `__r[${lazyRow.writerIndex}]`
|
|
214
213
|
for (const t of lazyRow.texts) emitTextBinding(lines, b2, t, createDoor, 'create', rwDoor)
|
|
215
214
|
}
|
|
215
|
+
for (const c of lazyRow.conditionals) emitConditional(lines, b2, mid, c, 'create')
|
|
216
216
|
lines.push(`${b2}return __el`)
|
|
217
217
|
lines.push(`${b1}},`)
|
|
218
218
|
|
|
219
219
|
// --- applyItem ---------------------------------------------------------
|
|
220
220
|
const itemAttrs = lazyRow.attrs.filter(a => a.readsItem)
|
|
221
221
|
const itemTexts = lazyRow.texts.filter(t => t.readsItem)
|
|
222
|
-
|
|
222
|
+
const itemConds = lazyRow.conditionals.filter(c => c.readsItem)
|
|
223
|
+
if (itemAttrs.length === 0 && itemTexts.length === 0 && itemConds.length === 0) {
|
|
223
224
|
lines.push(`${b1}applyItem: () => {},`)
|
|
224
225
|
} else {
|
|
225
226
|
lines.push(`${b1}applyItem: (__e) => {`)
|
|
226
227
|
lines.push(`${b2}const ${paramHead} = () => __e.item`)
|
|
227
|
-
lines.push(`${b2}const __r = __e.refs ?? (__e.refs =
|
|
228
|
+
lines.push(`${b2}const __r = __e.refs ?? (__e.refs = [])`)
|
|
228
229
|
lines.push(`${b2}const __l = __e.last ?? (__e.last = [])`)
|
|
230
|
+
// Re-run the preamble only when a binding in THIS body reads a local it
|
|
231
|
+
// declares (#2447 follow-up). `mapArrayLazy` wraps `applyItem` in
|
|
232
|
+
// `untrack()`, so a signal read inside the preamble subscribes nothing
|
|
233
|
+
// here — the subscription is `applyOuter`'s job.
|
|
234
|
+
if (lazyRow.itemNeedsPreamble && lazyRow.preambleStatements) {
|
|
235
|
+
lines.push(`${b2}${lazyRow.preambleStatements}`)
|
|
236
|
+
}
|
|
229
237
|
for (const a of itemAttrs) emitAttrBinding(lines, b2, a, 'item')
|
|
230
238
|
if (itemTexts.length > 0) {
|
|
231
239
|
lines.push(`${b2}const __d = ${doorAccess(lazyRow, lazyRow.writerIndex, adoptedPlanVar)}`)
|
|
232
240
|
for (const t of itemTexts) emitTextBinding(lines, b2, t, '__d', 'item', rwDoor)
|
|
233
241
|
}
|
|
242
|
+
for (const c of itemConds) emitConditional(lines, b2, mid, c, 'item')
|
|
234
243
|
lines.push(`${b1}},`)
|
|
235
244
|
}
|
|
236
245
|
|
|
237
246
|
// --- applyOuter (only when some binding is outer-involving) -------------
|
|
238
247
|
const outerAttrs = lazyRow.attrs.filter(a => a.readsOuter)
|
|
239
248
|
const outerTexts = lazyRow.texts.filter(t => t.readsOuter)
|
|
240
|
-
|
|
249
|
+
const outerConds = lazyRow.conditionals.filter(c => c.readsOuter)
|
|
250
|
+
if (outerAttrs.length > 0 || outerTexts.length > 0 || outerConds.length > 0) {
|
|
241
251
|
const b3 = `${indent} `
|
|
242
252
|
lines.push(`${b1}applyOuter: (__es, __seed) => {`)
|
|
243
253
|
// Prime the outer reads so this ONE loop-level effect subscribes even
|
|
@@ -245,13 +255,20 @@ export function stringifyLazyRowLoop(lines: string[], o: StringifyLazyRowOptions
|
|
|
245
255
|
for (const g of lazyRow.outerPrimeGetters) lines.push(`${b2}${g}()`)
|
|
246
256
|
lines.push(`${b2}for (const __e of __es) {`)
|
|
247
257
|
lines.push(`${b3}const ${paramHead} = () => __e.item`)
|
|
248
|
-
lines.push(`${b3}const __r = __e.refs ?? (__e.refs =
|
|
258
|
+
lines.push(`${b3}const __r = __e.refs ?? (__e.refs = [])`)
|
|
249
259
|
lines.push(`${b3}const __l = __e.last ?? (__e.last = [])`)
|
|
260
|
+
// Per ENTRY, not once for the batch: the preamble reads the item, so its
|
|
261
|
+
// locals differ per row. The outer signals it reads are already primed
|
|
262
|
+
// above, so this effect subscribes even when `__es` is empty.
|
|
263
|
+
if (lazyRow.outerNeedsPreamble && lazyRow.preambleStatements) {
|
|
264
|
+
lines.push(`${b3}${lazyRow.preambleStatements}`)
|
|
265
|
+
}
|
|
250
266
|
for (const a of outerAttrs) emitAttrBinding(lines, b3, a, 'outer')
|
|
251
267
|
if (outerTexts.length > 0) {
|
|
252
268
|
lines.push(`${b3}const __d = ${doorAccess(lazyRow, lazyRow.writerIndex, adoptedPlanVar)}`)
|
|
253
269
|
for (const t of outerTexts) emitTextBinding(lines, b3, t, '__d', 'outer', rwDoor)
|
|
254
270
|
}
|
|
271
|
+
for (const c of outerConds) emitConditional(lines, b3, mid, c, 'outer')
|
|
255
272
|
lines.push(`${b2}}`)
|
|
256
273
|
lines.push(`${b1}},`)
|
|
257
274
|
}
|
|
@@ -260,9 +277,10 @@ export function stringifyLazyRowLoop(lines: string[], o: StringifyLazyRowOptions
|
|
|
260
277
|
}
|
|
261
278
|
|
|
262
279
|
/**
|
|
263
|
-
* The `entry.refs` array
|
|
264
|
-
* (in `attrSlotIds` order), then — when the row has text slots — the
|
|
265
|
-
* claimed-slot door at `writerIndex`.
|
|
280
|
+
* The `entry.refs` array `createRow` seeds: one element ref per reactive-attr
|
|
281
|
+
* slot (in `attrSlotIds` order), then — when the row has text slots — the
|
|
282
|
+
* claimed-slot door at `writerIndex`. Fresh-clone context only; an ADOPTED row
|
|
283
|
+
* starts empty and fills each slot on demand (`elementAccess` / `doorAccess`).
|
|
266
284
|
*
|
|
267
285
|
* **Which door (per LOOP, never per binding).** `lazySlots` returns a bare
|
|
268
286
|
* write function; `lazyClaimSlots` returns the `{ write, read }` pair over
|
|
@@ -312,7 +330,6 @@ function refParts(
|
|
|
312
330
|
elVar: string,
|
|
313
331
|
skeletonPaths: SkeletonSlotPaths | null,
|
|
314
332
|
planVar: string | null,
|
|
315
|
-
deferDoor = false,
|
|
316
333
|
): string[] {
|
|
317
334
|
const parts: string[] = []
|
|
318
335
|
for (const slotId of lazyRow.attrSlotIds) {
|
|
@@ -320,11 +337,31 @@ function refParts(
|
|
|
320
337
|
parts.push(path ? pathExpr(elVar, path) : `qsa(${elVar}, '[bf="${slotId}"]')`)
|
|
321
338
|
}
|
|
322
339
|
if (lazyRow.texts.length > 0) {
|
|
323
|
-
parts.push(
|
|
340
|
+
parts.push(`${doorCtor(lazyRow)}(${elVar}, ${planVar})`)
|
|
324
341
|
}
|
|
325
342
|
return parts
|
|
326
343
|
}
|
|
327
344
|
|
|
345
|
+
/**
|
|
346
|
+
* How an ADOPTED row's binding reaches its element ref: claim THIS slot, and
|
|
347
|
+
* only this slot, on first use.
|
|
348
|
+
*
|
|
349
|
+
* The previous shape claimed the whole row in one closure (`__lzc_<mid>`),
|
|
350
|
+
* which meant an `applyOuter` driving ONE attribute still ran a `qsa` scan for
|
|
351
|
+
* every OTHER reactive-attr slot in the row — on every row, at hydration. The
|
|
352
|
+
* `in` test (not `??`) is what makes the cache honest: `qsa` answers `null` for
|
|
353
|
+
* a slot it cannot find, and `null` is a real cached answer — the `in` test
|
|
354
|
+
* treats it as one and never scans again, where a `??`-guarded cache would
|
|
355
|
+
* re-scan that slot on every tick forever.
|
|
356
|
+
*
|
|
357
|
+
* This is the element-ref twin of `doorAccess`, which already deferred the
|
|
358
|
+
* content door for exactly the same reason.
|
|
359
|
+
*/
|
|
360
|
+
function elementAccess(a: LazyRowAttrBinding): string {
|
|
361
|
+
const slot = `__r[${a.refIndex}]`
|
|
362
|
+
return `${a.refIndex} in __r ? ${slot} : (${slot} = qsa(__e.primaryEl, '[bf="${a.slotId}"]'))`
|
|
363
|
+
}
|
|
364
|
+
|
|
328
365
|
/** The door constructor for this loop — see `refParts`'s "which door" note. */
|
|
329
366
|
function doorCtor(lazyRow: LazyRowPlanData): string {
|
|
330
367
|
return lazyRow.textNeedsRead ? 'lazyClaimSlots' : 'lazySlots'
|
|
@@ -354,6 +391,63 @@ function doorAccess(
|
|
|
354
391
|
return `${slot} ?? (${slot} = ${doorCtor(lazyRow)}(__e.primaryEl, ${adoptedPlanVar}))`
|
|
355
392
|
}
|
|
356
393
|
|
|
394
|
+
/** Hoisted arm-template variable names for one conditional of one loop. */
|
|
395
|
+
function condVars(mid: string, slotId: string): { trueTpl: string; falseTpl: string } {
|
|
396
|
+
const key = `${mid}_${slotId.replace(/[^A-Za-z0-9_$]/g, '_')}`
|
|
397
|
+
return { trueTpl: `__cbt_${key}`, falseTpl: `__cbf_${key}` }
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
/**
|
|
401
|
+
* A row conditional, in the same three modes as {@link emitAttrBinding}.
|
|
402
|
+
*
|
|
403
|
+
* The whole swap is `replaceWith` on the `[bf-c]` element, because
|
|
404
|
+
* `analyzeLazyConditional` has already established that the arms own nothing —
|
|
405
|
+
* no events, no children to init, no live nodes to splice. What `insert()` does
|
|
406
|
+
* per row with an effect of its own, this does with a boolean and a dedup slot.
|
|
407
|
+
*
|
|
408
|
+
* **`'create'` writes no DOM.** The row `createRow` just cloned came from the
|
|
409
|
+
* per-row template, which interpolated `cond ? trueHtml : falseHtml` itself — so
|
|
410
|
+
* the correct arm is already in place and the only thing missing is the dedup
|
|
411
|
+
* record. Swapping here would replace an element with an identical one.
|
|
412
|
+
*
|
|
413
|
+
* **`'outer'` seeds by read-compare-write (§9.3(1))** against
|
|
414
|
+
* `content.firstElementChild.outerHTML` — the browser's serialization of the arm
|
|
415
|
+
* we would install, compared with the serialization of what is there. Both sides
|
|
416
|
+
* go through the DOM, so a match means the server already rendered this arm and
|
|
417
|
+
* no write happens.
|
|
418
|
+
*
|
|
419
|
+
* The ref is REASSIGNED after a swap (`__r[i] = __n`). Without that, the next
|
|
420
|
+
* apply would hold the detached node and write into nothing.
|
|
421
|
+
*/
|
|
422
|
+
function emitConditional(
|
|
423
|
+
lines: string[],
|
|
424
|
+
ind: string,
|
|
425
|
+
mid: string,
|
|
426
|
+
c: LazyRowConditionalBinding,
|
|
427
|
+
mode: 'create' | 'item' | 'outer',
|
|
428
|
+
): void {
|
|
429
|
+
const v = condVars(mid, c.slotId)
|
|
430
|
+
if (mode === 'create') {
|
|
431
|
+
lines.push(`${ind}__l[${c.ordinal}] = !!(${c.wrappedCondition})`)
|
|
432
|
+
return
|
|
433
|
+
}
|
|
434
|
+
const slot = `__r[${c.refIndex}]`
|
|
435
|
+
lines.push(`${ind}{ const __c = ${c.refIndex} in __r ? ${slot} : (${slot} = qsa(__e.primaryEl, '[bf-c="${c.slotId}"]'))`)
|
|
436
|
+
lines.push(`${ind}if (__c) {`)
|
|
437
|
+
lines.push(`${ind} const __x = !!(${c.wrappedCondition})`)
|
|
438
|
+
lines.push(`${ind} const __w = (__x ? ${v.trueTpl} : ${v.falseTpl}).content.firstElementChild`)
|
|
439
|
+
const guard = mode === 'item'
|
|
440
|
+
? dedupGuard(c.ordinal)
|
|
441
|
+
: `__seed ? (__c.outerHTML !== __w.outerHTML) : (${dedupGuard(c.ordinal)})`
|
|
442
|
+
lines.push(`${ind} if (${guard}) {`)
|
|
443
|
+
lines.push(`${ind} const __n = __w.cloneNode(true)`)
|
|
444
|
+
lines.push(`${ind} __c.replaceWith(__n)`)
|
|
445
|
+
lines.push(`${ind} ${slot} = __n`)
|
|
446
|
+
lines.push(`${ind} }`)
|
|
447
|
+
lines.push(`${ind} __l[${c.ordinal}] = __x`)
|
|
448
|
+
lines.push(`${ind}} }`)
|
|
449
|
+
}
|
|
450
|
+
|
|
357
451
|
/** `entry.last`-backed dedup test. `in` (not a truthiness check) so a
|
|
358
452
|
* legitimately `undefined` value still records and still writes once. */
|
|
359
453
|
function dedupGuard(ordinal: number): string {
|
|
@@ -366,7 +460,11 @@ function emitAttrBinding(
|
|
|
366
460
|
a: LazyRowAttrBinding,
|
|
367
461
|
mode: 'create' | 'item' | 'outer',
|
|
368
462
|
): void {
|
|
369
|
-
|
|
463
|
+
// `createRow` seeded every ref from a known clone path, so there it is a
|
|
464
|
+
// plain read. An ADOPTED row starts with an EMPTY `refs` array and each
|
|
465
|
+
// binding claims its OWN slot on first use — see `elementAccess`.
|
|
466
|
+
const target = mode === 'create' ? `__r[${a.refIndex}]` : elementAccess(a)
|
|
467
|
+
lines.push(`${ind}{ const __t = ${target}`)
|
|
370
468
|
lines.push(`${ind}if (__t) {`)
|
|
371
469
|
lines.push(`${ind} const __x = ${a.wrappedExpression}`)
|
|
372
470
|
const guard = mode === 'create'
|
|
@@ -124,7 +124,7 @@ export function stringifyReactiveEffects(
|
|
|
124
124
|
if (pc) {
|
|
125
125
|
// Profile mode: preserve the granular per-slot/per-attr effect shape so
|
|
126
126
|
// every binding keeps its own bfId (see module docstring).
|
|
127
|
-
emitAttrSlotsGranular(lines, indent, elVar, lookup, attrSlots, elementIndexBySlot, bindingBfId)
|
|
127
|
+
emitAttrSlotsGranular(lines, indent, elVar, lookup, attrSlots, elementIndexBySlot, bindingBfId, mapPreambleWrapped)
|
|
128
128
|
emitOuterTexts(lines, indent, elVar, outerTexts, bindingBfId, textClaimPathExprs)
|
|
129
129
|
emitPreambleRegionsEffect(lines, indent, elVar, preambleRegions, mapPreambleWrapped)
|
|
130
130
|
} else {
|
|
@@ -154,6 +154,7 @@ function emitAttrSlotsGranular(
|
|
|
154
154
|
attrSlots: readonly ReactiveAttrSlot[],
|
|
155
155
|
elementIndexBySlot: ReadonlyMap<string, number> | undefined,
|
|
156
156
|
bindingBfId: (slotId: string) => string,
|
|
157
|
+
mapPreambleWrapped: string | undefined,
|
|
157
158
|
): void {
|
|
158
159
|
for (const slot of attrSlots) {
|
|
159
160
|
const varName = `__ra_${varSlotId(slot.slotId)}`
|
|
@@ -162,6 +163,14 @@ function emitAttrSlotsGranular(
|
|
|
162
163
|
lines.push(`${indent}if (${varName}) {`)
|
|
163
164
|
for (const attr of slot.attrs) {
|
|
164
165
|
lines.push(`${indent} createEffect(() => {`)
|
|
166
|
+
// Profile mode keeps one effect per attr, so an attr reading a preamble
|
|
167
|
+
// local needs the declarations inside ITS OWN effect — there is no
|
|
168
|
+
// shared row-effect scope to hoist them into here (#2447 follow-up).
|
|
169
|
+
// Re-running per attr is the price of the per-binding bfId this mode
|
|
170
|
+
// exists to produce; `bf debug profile` is off the build hot path.
|
|
171
|
+
if (attr.readsPreamble && mapPreambleWrapped) {
|
|
172
|
+
lines.push(`${indent} ${mapPreambleWrapped}`)
|
|
173
|
+
}
|
|
165
174
|
for (const stmt of emitAttrUpdate(varName, attr.attrName, attr.wrappedExpression, attr.meta)) {
|
|
166
175
|
lines.push(`${indent} ${stmt}`)
|
|
167
176
|
}
|
|
@@ -193,6 +202,11 @@ function emitPreambleRegionsEffect(
|
|
|
193
202
|
lines.push(`${indent}})`)
|
|
194
203
|
}
|
|
195
204
|
|
|
205
|
+
/** Does any attr in these slots read a `.map()` preamble local (#2447)? */
|
|
206
|
+
function attrsReadPreamble(attrSlots: readonly ReactiveAttrSlot[]): boolean {
|
|
207
|
+
return attrSlots.some(slot => slot.attrs.some(a => a.readsPreamble))
|
|
208
|
+
}
|
|
209
|
+
|
|
196
210
|
function attrLookupExpr(
|
|
197
211
|
slotId: string,
|
|
198
212
|
varName: string,
|
|
@@ -270,6 +284,15 @@ function emitConsolidatedRowEffect(
|
|
|
270
284
|
return
|
|
271
285
|
}
|
|
272
286
|
lines.push(`${indent}createEffect(() => {`)
|
|
287
|
+
// The preamble re-run leads the body, not trails it (#2447 follow-up).
|
|
288
|
+
// It used to sit between the attr writes and the region writes, because
|
|
289
|
+
// regions were its only readers. An attribute can read a preamble local
|
|
290
|
+
// now (`class={cls}`), and it reads it from THIS scope — so the
|
|
291
|
+
// declarations have to exist before the first write, whichever kind that
|
|
292
|
+
// is. Regions are unaffected by the move: they were already downstream.
|
|
293
|
+
if (mapPreambleWrapped && (preambleRegions.length > 0 || attrsReadPreamble(attrSlots))) {
|
|
294
|
+
lines.push(`${indent} ${mapPreambleWrapped}`)
|
|
295
|
+
}
|
|
273
296
|
for (const slot of attrSlots) {
|
|
274
297
|
const varName = `__ra_${varSlotId(slot.slotId)}`
|
|
275
298
|
lines.push(`${indent} if (${varName}) {`)
|
|
@@ -282,9 +305,6 @@ function emitConsolidatedRowEffect(
|
|
|
282
305
|
}
|
|
283
306
|
lines.push(`${indent} }`)
|
|
284
307
|
}
|
|
285
|
-
if (preambleRegions.length > 0 && mapPreambleWrapped) {
|
|
286
|
-
lines.push(`${indent} ${mapPreambleWrapped}`)
|
|
287
|
-
}
|
|
288
308
|
for (const text of outerTexts) {
|
|
289
309
|
lines.push(`${indent} ${writer}('${text.slotId}', String(${text.wrappedExpression}))`)
|
|
290
310
|
}
|
|
@@ -251,9 +251,23 @@ export function emitLoopItemElementSetup(
|
|
|
251
251
|
indent: string
|
|
252
252
|
/** Single-root layout: 'inline' (plain / branch-plain) or 'multiline' (composite). */
|
|
253
253
|
singleRootLayout: 'inline' | 'multiline'
|
|
254
|
+
/**
|
|
255
|
+
* Emit `mountRowRoot(__el)` on the FRESH branch, connecting the row at the
|
|
256
|
+
* mount point `mapArray` handed down before the body's tail runs.
|
|
257
|
+
*
|
|
258
|
+
* Only bodies that initialise something inside the row need it — the tail
|
|
259
|
+
* is where `useContext` would otherwise resolve against a detached element
|
|
260
|
+
* and fall through to the global last-writer-wins store. A row with no
|
|
261
|
+
* nested init has nothing to resolve, so plain loops leave this off and
|
|
262
|
+
* their emission (and the `mapArrayLazy` measurements) are untouched.
|
|
263
|
+
*
|
|
264
|
+
* Never on the hydration branch: that row came from SSR markup and is in
|
|
265
|
+
* the document already.
|
|
266
|
+
*/
|
|
267
|
+
mountRow?: boolean
|
|
254
268
|
},
|
|
255
269
|
): void {
|
|
256
|
-
const { template, bodyIsMultiRoot, indent, singleRootLayout } = opts
|
|
270
|
+
const { template, bodyIsMultiRoot, indent, singleRootLayout, mountRow } = opts
|
|
257
271
|
const innerIndent = indent + ' '
|
|
258
272
|
if (bodyIsMultiRoot) {
|
|
259
273
|
lines.push(`${indent}let __el, __extras`)
|
|
@@ -264,17 +278,22 @@ export function emitLoopItemElementSetup(
|
|
|
264
278
|
lines.push(ln)
|
|
265
279
|
}
|
|
266
280
|
lines.push(`${innerIndent}__el.__bfExtras = __extras`)
|
|
281
|
+
// After the stash: `mountRowRoot` attaches the primary, and an attached
|
|
282
|
+
// primary makes `itemRootElements`' sibling walk the first thing a lookup
|
|
283
|
+
// sees — it must find the stash already in place behind it.
|
|
284
|
+
if (mountRow) lines.push(`${innerIndent}mountRowRoot(__el)`)
|
|
267
285
|
lines.push(`${indent}}`)
|
|
268
286
|
return
|
|
269
287
|
}
|
|
270
288
|
if (singleRootLayout === 'inline') {
|
|
271
289
|
const cloneExpr = emitTemplateCloneInline(template)
|
|
272
|
-
|
|
290
|
+
const clone = `__existing ?? (() => { ${cloneExpr} })()`
|
|
291
|
+
lines.push(`${indent}const __el = ${mountRow ? `__existing ?? mountRowRoot((() => { ${cloneExpr} })())` : clone}`)
|
|
273
292
|
return
|
|
274
293
|
}
|
|
275
|
-
lines.push(`${indent}const __el = __existing ?? (() => {`)
|
|
294
|
+
lines.push(`${indent}const __el = __existing ?? ${mountRow ? 'mountRowRoot(' : ''}(() => {`)
|
|
276
295
|
for (const ln of emitTemplateCloneLines(template, innerIndent)) lines.push(ln)
|
|
277
|
-
lines.push(`${indent}})()`)
|
|
296
|
+
lines.push(`${indent}})()${mountRow ? ')' : ''}`)
|
|
278
297
|
}
|
|
279
298
|
|
|
280
299
|
/**
|
|
@@ -447,7 +447,12 @@ export function buildSignalMemoEnv(
|
|
|
447
447
|
for (const m of memos) {
|
|
448
448
|
substitutions.set(m.name, {
|
|
449
449
|
kind: 'call',
|
|
450
|
-
|
|
450
|
+
// Destructured mode (#2468): `templateComputation` (when present) has
|
|
451
|
+
// bare destructured prop refs already rewritten to `_p.X` — the
|
|
452
|
+
// spliced body lands in the module-scope template arrow, which is not
|
|
453
|
+
// a closure over init's `const value = _p.value` extraction. Mirrors
|
|
454
|
+
// the signal `templateInitialValue` handling above (#2265).
|
|
455
|
+
replacement: extractMemoBodyExpr(m.templateComputation ?? m.computation),
|
|
451
456
|
freeIdentifiers: m.computationFreeIdentifiers ?? new Set(),
|
|
452
457
|
})
|
|
453
458
|
}
|
|
@@ -170,7 +170,7 @@ export function emitRegistrationAndHydration(
|
|
|
170
170
|
// transformation runs at this layer (#1277).
|
|
171
171
|
const csrInlinableConstants = csrInlinableConstantsFromCtx(ctx)
|
|
172
172
|
const templateHtml = generateCsrTemplate(
|
|
173
|
-
_ir.root, csrInlinableConstants, ctx,
|
|
173
|
+
_ir.root, csrInlinableConstants, ctx, restSpreadNames, ctx.propsObjectName, unsafeLocalNames, ctx.deferredChildSlots
|
|
174
174
|
)
|
|
175
175
|
if (templateHtml) {
|
|
176
176
|
defParts.push(`template: (${PROPS_PARAM}) => \`${templateHtml}\``)
|