@barefootjs/jsx 0.26.4 → 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/compiler.d.ts.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +941 -130
- package/dist/ir-to-client-js/client-only-elision.d.ts +103 -0
- package/dist/ir-to-client-js/client-only-elision.d.ts.map +1 -0
- package/dist/ir-to-client-js/collect-elements.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/plan/branch-loop.d.ts +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/shared.d.ts +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/claim-plan.d.ts +54 -0
- package/dist/ir-to-client-js/control-flow/stringify/claim-plan.d.ts.map +1 -0
- package/dist/ir-to-client-js/control-flow/stringify/inner-loop.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/stringify/insert.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/stringify/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 +17 -20
- package/dist/ir-to-client-js/control-flow/stringify/loop.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/stringify/reactive-effects.d.ts +63 -4
- package/dist/ir-to-client-js/control-flow/stringify/reactive-effects.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow.d.ts +1 -1
- package/dist/ir-to-client-js/control-flow.d.ts.map +1 -1
- package/dist/ir-to-client-js/element-refs.d.ts +8 -3
- package/dist/ir-to-client-js/element-refs.d.ts.map +1 -1
- package/dist/ir-to-client-js/emit-reactive.d.ts +8 -1
- package/dist/ir-to-client-js/emit-reactive.d.ts.map +1 -1
- package/dist/ir-to-client-js/html-template.d.ts +24 -0
- package/dist/ir-to-client-js/html-template.d.ts.map +1 -1
- package/dist/ir-to-client-js/imports.d.ts +2 -2
- package/dist/ir-to-client-js/imports.d.ts.map +1 -1
- package/dist/ir-to-client-js/reactivity.d.ts.map +1 -1
- package/dist/ir-to-client-js/types.d.ts +9 -1
- package/dist/ir-to-client-js/types.d.ts.map +1 -1
- package/dist/ir-to-client-js/utils.d.ts +1 -1
- package/dist/types.d.ts +25 -2
- package/dist/types.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/__tests__/__snapshots__/doc-examples.test.ts.snap +542 -689
- package/src/__tests__/child-components-in-map.test.ts +4 -4
- package/src/__tests__/client-js-generation.test.ts +23 -12
- package/src/__tests__/composite-branch-loop.test.ts +14 -5
- package/src/__tests__/conditional-mapArray-key.test.ts +7 -1
- package/src/__tests__/create-selector.test.ts +21 -7
- package/src/__tests__/cross-file-client-signal.test.ts +5 -1
- package/src/__tests__/early-return-scope-var-ref.test.ts +10 -11
- package/src/__tests__/inline-jsx-callback.test.ts +6 -4
- package/src/__tests__/ir-jsx-props.test.ts +6 -5
- 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 +34 -16
- package/src/__tests__/loop-hoisted-template.test.ts +10 -6
- package/src/__tests__/module-level-signal.audit.test.ts +7 -2
- package/src/__tests__/nested-loop-conditional.test.ts +9 -7
- package/src/__tests__/nested-loop-reactive-attrs.test.ts +2 -2
- package/src/__tests__/preamble-region-patch.test.ts +18 -12
- package/src/__tests__/profile-cond-binding-ids.test.ts +3 -2
- package/src/__tests__/profile-loop-binding-ids.test.ts +4 -3
- package/src/__tests__/profile-nested-binding-ids.test.ts +11 -6
- package/src/__tests__/reactive-attrs-in-map.test.ts +6 -4
- package/src/__tests__/static-loop-csr-materialize.test.ts +6 -1
- package/src/__tests__/template-closure.test.ts +1 -1
- package/src/compiler.ts +11 -0
- package/src/index.ts +1 -0
- package/src/ir-to-client-js/client-only-elision.ts +273 -0
- package/src/ir-to-client-js/collect-elements.ts +56 -17
- 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/shared.ts +1 -1
- package/src/ir-to-client-js/control-flow/stringify/branch-loop.ts +34 -4
- package/src/ir-to-client-js/control-flow/stringify/claim-plan.ts +68 -0
- package/src/ir-to-client-js/control-flow/stringify/inner-loop.ts +18 -9
- package/src/ir-to-client-js/control-flow/stringify/insert.ts +17 -11
- package/src/ir-to-client-js/control-flow/stringify/lazy-row.ts +478 -0
- package/src/ir-to-client-js/control-flow/stringify/loop-child-arm.ts +41 -17
- package/src/ir-to-client-js/control-flow/stringify/loop.ts +71 -46
- package/src/ir-to-client-js/control-flow/stringify/reactive-effects.ts +241 -31
- package/src/ir-to-client-js/control-flow.ts +8 -3
- package/src/ir-to-client-js/element-refs.ts +9 -11
- package/src/ir-to-client-js/emit-reactive.ts +50 -14
- package/src/ir-to-client-js/html-template.ts +29 -9
- package/src/ir-to-client-js/imports.ts +13 -4
- package/src/ir-to-client-js/reactivity.ts +3 -2
- package/src/ir-to-client-js/types.ts +10 -2
- package/src/ir-to-client-js/utils.ts +1 -1
- package/src/jsx-to-ir.ts +26 -8
- package/src/types.ts +25 -2
|
@@ -34,6 +34,7 @@ import type { InsertPlan, InsertArm, ArmBody, ScopeRef } from '../plan/types.ts'
|
|
|
34
34
|
import { stringifyBranchLoops } from './branch-loop.ts'
|
|
35
35
|
import { emitListenerLine } from './event-listener.ts'
|
|
36
36
|
import { nameForRegistryRef } from '../../component-scope.ts'
|
|
37
|
+
import { claimPlanLiteral, claimWriterVarName, type ClaimSlotSpec } from './claim-plan.ts'
|
|
37
38
|
|
|
38
39
|
export interface StringifyInsertOptions {
|
|
39
40
|
/** Indent on the `insert(` line itself. */
|
|
@@ -173,18 +174,23 @@ function emitArmBody(
|
|
|
173
174
|
lines.push(`${indent}} }`)
|
|
174
175
|
}
|
|
175
176
|
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
// Route through `__bfText` so a JSX-valued expression (`{cond && logo(id)}`)
|
|
177
|
+
if (body.textEffects.length > 0) {
|
|
178
|
+
// 'markup' kind so a JSX-valued expression (`{cond && logo(id)}`)
|
|
179
179
|
// re-splices the live element by identity instead of stringifying it to
|
|
180
|
-
// "[object HTMLElement]" — the
|
|
181
|
-
//
|
|
182
|
-
//
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
lines.push(`${indent}}${
|
|
180
|
+
// "[object HTMLElement]" — the same contract `__bfText` used to provide
|
|
181
|
+
// (#1663). Built fresh here, inside this arm's `bindEvents` body, so
|
|
182
|
+
// every branch activation re-claims against that swap's own
|
|
183
|
+
// `__branchScope` DOM (spec/slot-unification.md §5-A3's "branch-internal
|
|
184
|
+
// slots re-claim per branch activation").
|
|
185
|
+
const slots: ClaimSlotSpec[] = body.textEffects.map(te => ({ id: te.slotId, kind: 'markup', path: [] }))
|
|
186
|
+
const writer = claimWriterVarName(slots, varSlotId)
|
|
187
|
+
lines.push(`${indent}const ${writer} = lazySlots(__branchScope, ${claimPlanLiteral(slots)})`)
|
|
188
|
+
for (const te of body.textEffects) {
|
|
189
|
+
// `escapeTextOrNode`: see emit-reactive.ts's identical wrap for why a
|
|
190
|
+
// string value must be escaped before a 'markup' writer's
|
|
191
|
+
// `innerHTML =` insertion, while a live Node passes through untouched.
|
|
192
|
+
lines.push(`${indent}__disposers.push(createDisposableEffect(() => { ${writer}('${te.slotId}', escapeTextOrNode(${te.expression})) }${bindingBfId(te.slotId)}))`)
|
|
193
|
+
}
|
|
188
194
|
}
|
|
189
195
|
|
|
190
196
|
// Branch loops, now fully Plan-built. The stringifier writes its own
|
|
@@ -0,0 +1,478 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Emit a lazy row graph loop — `mapArrayLazy(...)` + a compiler-built
|
|
3
|
+
* `LazyRowPlan` object literal (`spec/slot-unification.md` §9, L3).
|
|
4
|
+
*
|
|
5
|
+
* This is the alternative to `stringifyPlainLoop`'s `mapArray` + renderItem
|
|
6
|
+
* emission, taken only when `lazyRowEligibility` said yes (see
|
|
7
|
+
* `plan/lazy-row-eligibility.ts`). Every ineligible loop keeps today's
|
|
8
|
+
* emission byte-for-byte — sound-or-loud, no silent third path.
|
|
9
|
+
*
|
|
10
|
+
* Output shape:
|
|
11
|
+
*
|
|
12
|
+
* const __tpl_<mid> = document.createElement('template') // hoisted skeleton, when usable
|
|
13
|
+
* __tpl_<mid>.innerHTML = `…`
|
|
14
|
+
* const __lzp_<mid> = [...] // hoisted fresh-clone text paths, when usable
|
|
15
|
+
* const __lzs_<mid> = [{ id, kind: 'text', path: [] }, …] // hoisted claim plan, ADOPTED-row form
|
|
16
|
+
* const __lzsc_<mid> = [{ id, kind: 'text', path: __lzp_<mid>[i] }, …] // FRESH-CLONE form, only when it differs
|
|
17
|
+
* const __lzc_<mid> = (__e) => { … } // lazy ref claim for ADOPTED rows (door slot empty)
|
|
18
|
+
* mapArrayLazy(() => <arr>, <container>, <keyFn>, {
|
|
19
|
+
* createRow: (__e, <idx>) => { … writes every binding, seeds refs+last … },
|
|
20
|
+
* applyItem: (__e) => { … item-driven bindings, dedup against __e.last … },
|
|
21
|
+
* applyOuter: (__es, __seed) => { … one loop-level effect body … },
|
|
22
|
+
* }, '<markerId>')
|
|
23
|
+
*
|
|
24
|
+
* Three shapes carry the plan's obligations (runtime docstring,
|
|
25
|
+
* `packages/client/src/runtime/map-array-lazy.ts`):
|
|
26
|
+
*
|
|
27
|
+
* **`createRow`** — CSR creation. Clones the row, resolves refs from KNOWN
|
|
28
|
+
* clone paths (the hoisted skeleton's `__p`-style child-index chains, reused
|
|
29
|
+
* verbatim from #2143 via `pathExpr`) with no scan, writes ALL bindings
|
|
30
|
+
* (item-driven and outer-involving alike) and seeds `entry.last` so the
|
|
31
|
+
* loop-level outer effect's dedup is correct from the row's first tick.
|
|
32
|
+
*
|
|
33
|
+
* **`applyItem`** — called by the reconciler after `entry.item` changed.
|
|
34
|
+
* Claims refs lazily through `__lzc_<mid>` (a `qsa` scan inside that ONE row)
|
|
35
|
+
* when `entry.refs` is null, materializes the content door on demand
|
|
36
|
+
* (`doorAccess`), then writes each item-driven binding behind a per-binding
|
|
37
|
+
* dedup on `entry.last`.
|
|
38
|
+
*
|
|
39
|
+
* **`applyOuter`** — the ONE loop-level effect body, emitted only when some
|
|
40
|
+
* binding is outer-involving. Two details matter:
|
|
41
|
+
*
|
|
42
|
+
* - **Prime reads first.** The effect subscribes to whatever its body reads.
|
|
43
|
+
* With an empty entry list the per-row loop reads nothing, so the effect
|
|
44
|
+
* would never subscribe and the loop would go permanently dead. The plan
|
|
45
|
+
* therefore emits a bare `getter()` statement per reactive outer name
|
|
46
|
+
* BEFORE the row loop. The eligibility gate guarantees every reactive
|
|
47
|
+
* outer name is a primable zero-arg signal/memo getter.
|
|
48
|
+
* - **Read-compare-write seeding (§9.3(1)).** On the first run (`__seed`)
|
|
49
|
+
* each binding computes its value, READS the current DOM, and writes only
|
|
50
|
+
* on difference — sound even when the outer state is client-only and
|
|
51
|
+
* diverges from SSR, with no writes on the consistent path. The DOM read
|
|
52
|
+
* is per attribute KIND and pairs with `emitAttrUpdate`'s dispatch
|
|
53
|
+
* (`emit-reactive.ts`); any kind this module does not recognise falls back
|
|
54
|
+
* to `true` (always write on seed), which is conservative, never wrong.
|
|
55
|
+
* CONTENT slots seed the same way through the claim's `read(id)` door
|
|
56
|
+
* (§9.5, lifted) — see `refParts` for the per-loop door choice.
|
|
57
|
+
*
|
|
58
|
+
* A loop with NO outer binding emits no `applyOuter` at all — its rows do
|
|
59
|
+
* literally nothing at hydration.
|
|
60
|
+
*/
|
|
61
|
+
|
|
62
|
+
import { isBooleanAttr } from '../../../html-constants.ts'
|
|
63
|
+
import { emitAttrUpdate } from '../../emit-reactive.ts'
|
|
64
|
+
import { toHtmlAttrName } from '../../utils.ts'
|
|
65
|
+
import type { SkeletonSlotPaths } from '../../html-template.ts'
|
|
66
|
+
import { claimPlanLiteral, type ClaimSlotSpec } from './claim-plan.ts'
|
|
67
|
+
import { pathExpr } from './skeleton-paths.ts'
|
|
68
|
+
import {
|
|
69
|
+
emitHoistedTemplateDecl,
|
|
70
|
+
emitTemplateCloneInline,
|
|
71
|
+
hoistedCloneExpr,
|
|
72
|
+
} from './template-parse.ts'
|
|
73
|
+
import type { LazyRowAttrBinding, LazyRowPlanData, LazyRowTextBinding } from '../plan/build-lazy-row.ts'
|
|
74
|
+
|
|
75
|
+
export interface StringifyLazyRowOptions {
|
|
76
|
+
/** Indent of the `mapArrayLazy(` line itself. */
|
|
77
|
+
indent: string
|
|
78
|
+
containerVar: string
|
|
79
|
+
/** Wrap the call in `if (<containerVar>) …` — branch-scoped loops do. */
|
|
80
|
+
guardContainer: boolean
|
|
81
|
+
markerId: string
|
|
82
|
+
arrayExpr: string
|
|
83
|
+
keyFn: string
|
|
84
|
+
paramHead: string
|
|
85
|
+
indexParam: string
|
|
86
|
+
/** Per-row interpolated template (the non-hoisted clone source). */
|
|
87
|
+
template: string
|
|
88
|
+
skeletonTemplate?: string
|
|
89
|
+
skeletonPaths?: SkeletonSlotPaths
|
|
90
|
+
lazyRow: LazyRowPlanData
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function stringifyLazyRowLoop(lines: string[], o: StringifyLazyRowOptions): void {
|
|
94
|
+
const { indent, lazyRow, paramHead } = o
|
|
95
|
+
const mid = o.markerId.replace(/[^A-Za-z0-9_$]/g, '_')
|
|
96
|
+
const tplVar = `__tpl_${mid}`
|
|
97
|
+
const claimVar = `__lzc_${mid}`
|
|
98
|
+
const hasRefs = lazyRow.attrSlotIds.length > 0 || lazyRow.texts.length > 0
|
|
99
|
+
const hasBindings = lazyRow.attrs.length > 0 || lazyRow.texts.length > 0
|
|
100
|
+
// ONE per-loop door decision (see `refParts`): the read-capable claim, or
|
|
101
|
+
// today's write-only writer. Never decided per binding — the door is
|
|
102
|
+
// allocated once per ROW.
|
|
103
|
+
const rwDoor = lazyRow.textNeedsRead
|
|
104
|
+
|
|
105
|
+
// Hoisted once-per-loop skeleton (perf, #2143): clone an already-parsed
|
|
106
|
+
// node per row instead of re-running an `innerHTML` parse. The skeleton
|
|
107
|
+
// omits dynamic ATTRIBUTE VALUES and empties text markers but keeps every
|
|
108
|
+
// `bf="sN"` attribute and `<!--bf:sN-->` marker, so a slot with no
|
|
109
|
+
// compile-time path still resolves by scan against the clone — same
|
|
110
|
+
// per-slot fallback the eager `__p` path takes (`buildSkeletonPathPlan`
|
|
111
|
+
// simply omits pathless slots).
|
|
112
|
+
const paths = o.skeletonPaths
|
|
113
|
+
const useHoisted = Boolean(o.skeletonTemplate)
|
|
114
|
+
|
|
115
|
+
if (useHoisted) emitHoistedTemplateDecl(lines, indent, tplVar, o.skeletonTemplate!)
|
|
116
|
+
|
|
117
|
+
// Hoisted fresh-clone claim paths for this loop's text slots, REFERENCED
|
|
118
|
+
// (not inlined) by `createRow`'s claim plan. Two reasons, both load-bearing:
|
|
119
|
+
//
|
|
120
|
+
// 1. One array per loop instead of one per row — `createRow` runs per CSR
|
|
121
|
+
// row and would otherwise re-allocate every path literal.
|
|
122
|
+
// 2. These paths describe the SKELETON CLONE, not the server-rendered
|
|
123
|
+
// tree, and they are ROW-relative — resolving them from the component
|
|
124
|
+
// scope root (which is what an SSR-shape checker can see) is
|
|
125
|
+
// meaningless. That is the same situation `ClaimSlotSpec.pathExpr`
|
|
126
|
+
// exists for on the eager path (`__existing ? [] : […]`), and the
|
|
127
|
+
// claim-plan conformance harness (`adapter-tests/src/
|
|
128
|
+
// claim-plan-conformance.ts`) correctly verifies only literal
|
|
129
|
+
// `number[]` paths. Adopted (SSR) rows never use these — `__lzc_<mid>`
|
|
130
|
+
// claims with `path: []`, the sanctioned marker-scan case.
|
|
131
|
+
const textPathVar = useHoisted && paths && lazyRow.texts.length > 0 ? `__lzp_${mid}` : null
|
|
132
|
+
if (textPathVar) {
|
|
133
|
+
const arrays = lazyRow.texts.map(t => `[${(paths!.textMarkerPaths.get(t.slotId) ?? []).join(', ')}]`)
|
|
134
|
+
lines.push(`${indent}const ${textPathVar} = [${arrays.join(', ')}]`)
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Hoisted claim-plan literal(s) for this loop's text slots (perf): a
|
|
138
|
+
// `ClaimPlan` is `readonly SlotSpec[]` (`claim-slots.ts:145`) and
|
|
139
|
+
// `claimRefs` (line 601) only ever READS it — there is no `spec.<field> = `
|
|
140
|
+
// assignment anywhere in that module, and `claimSlots`/`claimRefs` build a
|
|
141
|
+
// fresh `Map` per call — so one shared plan object is safe across every
|
|
142
|
+
// row of the loop instead of a fresh `{ id, kind, path }` object (plus,
|
|
143
|
+
// when `textPathVar` is null, a fresh inner `path: []` array) per row.
|
|
144
|
+
// `__lzs_<mid>` is the ADOPTED-row form (`path: []`, resolved by A2's
|
|
145
|
+
// marker scan); `__lzsc_<mid>` is the FRESH-CLONE form (each slot's path
|
|
146
|
+
// is a `__lzp_<mid>` lookup) and is only emitted when it differs from the
|
|
147
|
+
// adopted form — i.e. when `textPathVar` is non-null.
|
|
148
|
+
let adoptedPlanVar: string | null = null
|
|
149
|
+
let freshPlanVar: string | null = null
|
|
150
|
+
if (lazyRow.texts.length > 0) {
|
|
151
|
+
adoptedPlanVar = `__lzs_${mid}`
|
|
152
|
+
const adoptedSlots: ClaimSlotSpec[] = lazyRow.texts.map(t => ({ id: t.slotId, kind: 'text', path: [] }))
|
|
153
|
+
lines.push(`${indent}const ${adoptedPlanVar} = ${claimPlanLiteral(adoptedSlots)}`)
|
|
154
|
+
if (textPathVar) {
|
|
155
|
+
freshPlanVar = `__lzsc_${mid}`
|
|
156
|
+
const freshSlots: ClaimSlotSpec[] = lazyRow.texts.map((t, i) => ({
|
|
157
|
+
id: t.slotId,
|
|
158
|
+
kind: 'text',
|
|
159
|
+
path: [],
|
|
160
|
+
pathExpr: `${textPathVar}[${i}]`,
|
|
161
|
+
}))
|
|
162
|
+
lines.push(`${indent}const ${freshPlanVar} = ${claimPlanLiteral(freshSlots)}`)
|
|
163
|
+
} else {
|
|
164
|
+
freshPlanVar = adoptedPlanVar
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Lazy ref claim for ADOPTED (SSR) rows: one scan inside that row, cached
|
|
169
|
+
// on `entry.refs`. Shared by applyItem and applyOuter so a row claims once.
|
|
170
|
+
//
|
|
171
|
+
// The text door slot is left EMPTY here and filled by the first content
|
|
172
|
+
// write (`doorAccess`). An adopted row is claimed by whichever of
|
|
173
|
+
// applyItem/applyOuter touches it first, and an `applyOuter` that only
|
|
174
|
+
// drives ATTRIBUTES never reads or writes a content slot — so building the
|
|
175
|
+
// door in this function spends one closure per row of the list on something
|
|
176
|
+
// that stays unused until (and unless) that row's item changes. Deferring
|
|
177
|
+
// is confined to the adopted path on purpose: `createRow` writes every text
|
|
178
|
+
// immediately, so its door is used on the same tick it is built and there
|
|
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}}`)
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const call = `mapArrayLazy(() => ${o.arrayExpr}, ${o.containerVar}, ${o.keyFn}, {`
|
|
193
|
+
lines.push(`${indent}${o.guardContainer ? `if (${o.containerVar}) ` : ''}${call}`)
|
|
194
|
+
|
|
195
|
+
// --- createRow ---------------------------------------------------------
|
|
196
|
+
const b1 = `${indent} `
|
|
197
|
+
const b2 = `${indent} `
|
|
198
|
+
lines.push(`${b1}createRow: (__e, ${o.indexParam}) => {`)
|
|
199
|
+
// Always bound, even with no reactive bindings: the non-hoisted clone
|
|
200
|
+
// interpolates the per-row template, which reads the param for at least
|
|
201
|
+
// the `data-key` attribute.
|
|
202
|
+
lines.push(`${b2}const ${paramHead} = () => __e.item`)
|
|
203
|
+
const cloneExpr = useHoisted
|
|
204
|
+
? hoistedCloneExpr(tplVar, o.skeletonTemplate!)
|
|
205
|
+
: `(() => { ${emitTemplateCloneInline(o.template)} })()`
|
|
206
|
+
lines.push(`${b2}const __el = ${cloneExpr}`)
|
|
207
|
+
if (hasRefs) {
|
|
208
|
+
lines.push(`${b2}const __r = __e.refs = [${refParts(lazyRow, '__el', useHoisted ? (paths ?? null) : null, freshPlanVar).join(', ')}]`)
|
|
209
|
+
}
|
|
210
|
+
if (hasBindings) {
|
|
211
|
+
lines.push(`${b2}const __l = __e.last = []`)
|
|
212
|
+
for (const a of lazyRow.attrs) emitAttrBinding(lines, b2, a, 'create')
|
|
213
|
+
const createDoor = `__r[${lazyRow.writerIndex}]`
|
|
214
|
+
for (const t of lazyRow.texts) emitTextBinding(lines, b2, t, createDoor, 'create', rwDoor)
|
|
215
|
+
}
|
|
216
|
+
lines.push(`${b2}return __el`)
|
|
217
|
+
lines.push(`${b1}},`)
|
|
218
|
+
|
|
219
|
+
// --- applyItem ---------------------------------------------------------
|
|
220
|
+
const itemAttrs = lazyRow.attrs.filter(a => a.readsItem)
|
|
221
|
+
const itemTexts = lazyRow.texts.filter(t => t.readsItem)
|
|
222
|
+
if (itemAttrs.length === 0 && itemTexts.length === 0) {
|
|
223
|
+
lines.push(`${b1}applyItem: () => {},`)
|
|
224
|
+
} else {
|
|
225
|
+
lines.push(`${b1}applyItem: (__e) => {`)
|
|
226
|
+
lines.push(`${b2}const ${paramHead} = () => __e.item`)
|
|
227
|
+
lines.push(`${b2}const __r = __e.refs ?? (__e.refs = ${claimVar}(__e))`)
|
|
228
|
+
lines.push(`${b2}const __l = __e.last ?? (__e.last = [])`)
|
|
229
|
+
for (const a of itemAttrs) emitAttrBinding(lines, b2, a, 'item')
|
|
230
|
+
if (itemTexts.length > 0) {
|
|
231
|
+
lines.push(`${b2}const __d = ${doorAccess(lazyRow, lazyRow.writerIndex, adoptedPlanVar)}`)
|
|
232
|
+
for (const t of itemTexts) emitTextBinding(lines, b2, t, '__d', 'item', rwDoor)
|
|
233
|
+
}
|
|
234
|
+
lines.push(`${b1}},`)
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// --- applyOuter (only when some binding is outer-involving) -------------
|
|
238
|
+
const outerAttrs = lazyRow.attrs.filter(a => a.readsOuter)
|
|
239
|
+
const outerTexts = lazyRow.texts.filter(t => t.readsOuter)
|
|
240
|
+
if (outerAttrs.length > 0 || outerTexts.length > 0) {
|
|
241
|
+
const b3 = `${indent} `
|
|
242
|
+
lines.push(`${b1}applyOuter: (__es, __seed) => {`)
|
|
243
|
+
// Prime the outer reads so this ONE loop-level effect subscribes even
|
|
244
|
+
// when the entry list is momentarily empty (see module docstring).
|
|
245
|
+
for (const g of lazyRow.outerPrimeGetters) lines.push(`${b2}${g}()`)
|
|
246
|
+
lines.push(`${b2}for (const __e of __es) {`)
|
|
247
|
+
lines.push(`${b3}const ${paramHead} = () => __e.item`)
|
|
248
|
+
lines.push(`${b3}const __r = __e.refs ?? (__e.refs = ${claimVar}(__e))`)
|
|
249
|
+
lines.push(`${b3}const __l = __e.last ?? (__e.last = [])`)
|
|
250
|
+
for (const a of outerAttrs) emitAttrBinding(lines, b3, a, 'outer')
|
|
251
|
+
if (outerTexts.length > 0) {
|
|
252
|
+
lines.push(`${b3}const __d = ${doorAccess(lazyRow, lazyRow.writerIndex, adoptedPlanVar)}`)
|
|
253
|
+
for (const t of outerTexts) emitTextBinding(lines, b3, t, '__d', 'outer', rwDoor)
|
|
254
|
+
}
|
|
255
|
+
lines.push(`${b2}}`)
|
|
256
|
+
lines.push(`${b1}},`)
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
lines.push(`${indent}}, '${o.markerId}')`)
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* The `entry.refs` array contents: one element ref per reactive-attr slot
|
|
264
|
+
* (in `attrSlotIds` order), then — when the row has text slots — the
|
|
265
|
+
* claimed-slot door at `writerIndex`.
|
|
266
|
+
*
|
|
267
|
+
* **Which door (per LOOP, never per binding).** `lazySlots` returns a bare
|
|
268
|
+
* write function; `lazyClaimSlots` returns the `{ write, read }` pair over
|
|
269
|
+
* the SAME claim, at the cost of an extra closure on EVERY row of the list
|
|
270
|
+
* (`claim-slots.ts` measured ~40-84KB/1k rows). So the RW door is taken only
|
|
271
|
+
* when this loop actually has an outer-involving text binding to seed by
|
|
272
|
+
* read-compare-write (`plan.textNeedsRead`, decided once in
|
|
273
|
+
* `build-lazy-row.ts`); every other loop keeps today's writer byte-for-byte.
|
|
274
|
+
*
|
|
275
|
+
* **`deferDoor`** (adopted-row claim only). Even the cheap write-only door is
|
|
276
|
+
* a closure per row, and an `applyOuter` that drives ATTRIBUTES only claims
|
|
277
|
+
* every row at seed without ever touching a content slot — so the door was
|
|
278
|
+
* being built 1,000 times for a list of 1,000 rows and used zero times until
|
|
279
|
+
* some row's item changed. With `deferDoor` the slot holds `null` and the
|
|
280
|
+
* first content write fills it (`doorAccess`). Measured on the SSR bench's
|
|
281
|
+
* 1,000-row table (item texts + one outer-signal class, the shape this
|
|
282
|
+
* describes): post-hydration heap 1630.6KB -> 1573.2KB, -57.4KB, reproduced
|
|
283
|
+
* twice, against a per-run stdev of 0.1-0.6KB and with react/solid unmoved.
|
|
284
|
+
*
|
|
285
|
+
* `createRow` does NOT defer: it writes every text on the tick it builds the
|
|
286
|
+
* row, so its door is used immediately and a `??=` would only add a branch.
|
|
287
|
+
*
|
|
288
|
+
* **Honest cost note.** Reading a text slot CLAIMS that row's whole plan
|
|
289
|
+
* (§2's claim-once rule — `read` and `write` share `claimRefs`), so a loop
|
|
290
|
+
* with an outer-involving TEXT still pays one claim per row at hydration
|
|
291
|
+
* instead of the row-pristine zero. That is inherent to read-compare-write
|
|
292
|
+
* for content: you cannot compare what you have not resolved. It is still
|
|
293
|
+
* far cheaper than the eager path this replaces, which pays a root + a
|
|
294
|
+
* signal + an effect per row. A loop whose outer bindings are all attributes
|
|
295
|
+
* now pays neither the claim nor the door.
|
|
296
|
+
*
|
|
297
|
+
* `skeletonPaths` non-null = fresh-clone context (`createRow`): resolve via
|
|
298
|
+
* compile-time child-index chains, no scan. Null = adopted-row context
|
|
299
|
+
* (`__lzc_<mid>`): `qsa` + an empty claim path, which A2's marker scan
|
|
300
|
+
* resolves — the sanctioned "cannot be statically pathed" case for a
|
|
301
|
+
* server-rendered tree the skeleton does not describe (§5-A3).
|
|
302
|
+
*
|
|
303
|
+
* `planVar` is the hoisted claim-plan variable for THIS context (`__lzs_<mid>`
|
|
304
|
+
* for the adopted-row call site, `__lzsc_<mid>` — or the same `__lzs_<mid>`
|
|
305
|
+
* when the two forms coincide — for the fresh-clone one), built once by
|
|
306
|
+
* `stringifyLazyRowLoop` and referenced here rather than rebuilt per row.
|
|
307
|
+
* `null` iff `lazyRow.texts` is empty, the only case that skips the text part
|
|
308
|
+
* below.
|
|
309
|
+
*/
|
|
310
|
+
function refParts(
|
|
311
|
+
lazyRow: LazyRowPlanData,
|
|
312
|
+
elVar: string,
|
|
313
|
+
skeletonPaths: SkeletonSlotPaths | null,
|
|
314
|
+
planVar: string | null,
|
|
315
|
+
deferDoor = false,
|
|
316
|
+
): string[] {
|
|
317
|
+
const parts: string[] = []
|
|
318
|
+
for (const slotId of lazyRow.attrSlotIds) {
|
|
319
|
+
const path = skeletonPaths?.elementPaths.get(slotId)
|
|
320
|
+
parts.push(path ? pathExpr(elVar, path) : `qsa(${elVar}, '[bf="${slotId}"]')`)
|
|
321
|
+
}
|
|
322
|
+
if (lazyRow.texts.length > 0) {
|
|
323
|
+
parts.push(deferDoor ? 'null' : `${doorCtor(lazyRow)}(${elVar}, ${planVar})`)
|
|
324
|
+
}
|
|
325
|
+
return parts
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/** The door constructor for this loop — see `refParts`'s "which door" note. */
|
|
329
|
+
function doorCtor(lazyRow: LazyRowPlanData): string {
|
|
330
|
+
return lazyRow.textNeedsRead ? 'lazyClaimSlots' : 'lazySlots'
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* How a content write reaches this row's door.
|
|
335
|
+
*
|
|
336
|
+
* `createRow` seeds the door itself (fresh-clone plan, used on the same
|
|
337
|
+
* tick), so there it is a plain slot read. An ADOPTED row's slot is `null`
|
|
338
|
+
* until the first content write, so applyItem/applyOuter materialize it
|
|
339
|
+
* on demand against the ADOPTED plan — the only plan an adopted row can
|
|
340
|
+
* use, and reachable here because a row whose refs `createRow` seeded
|
|
341
|
+
* always finds a door already in the slot and never evaluates the `??`
|
|
342
|
+
* right-hand side.
|
|
343
|
+
*
|
|
344
|
+
* Emitted ONCE per apply body rather than per binding, and only when that
|
|
345
|
+
* body actually has content bindings: an `applyOuter` driving attributes
|
|
346
|
+
* only never mentions the door, which is the whole point of deferring it.
|
|
347
|
+
*/
|
|
348
|
+
function doorAccess(
|
|
349
|
+
lazyRow: LazyRowPlanData,
|
|
350
|
+
writerIndex: number,
|
|
351
|
+
adoptedPlanVar: string | null,
|
|
352
|
+
): string {
|
|
353
|
+
const slot = `__r[${writerIndex}]`
|
|
354
|
+
return `${slot} ?? (${slot} = ${doorCtor(lazyRow)}(__e.primaryEl, ${adoptedPlanVar}))`
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
/** `entry.last`-backed dedup test. `in` (not a truthiness check) so a
|
|
358
|
+
* legitimately `undefined` value still records and still writes once. */
|
|
359
|
+
function dedupGuard(ordinal: number): string {
|
|
360
|
+
return `!(${ordinal} in __l) || !Object.is(__l[${ordinal}], __x)`
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
function emitAttrBinding(
|
|
364
|
+
lines: string[],
|
|
365
|
+
ind: string,
|
|
366
|
+
a: LazyRowAttrBinding,
|
|
367
|
+
mode: 'create' | 'item' | 'outer',
|
|
368
|
+
): void {
|
|
369
|
+
lines.push(`${ind}{ const __t = __r[${a.refIndex}]`)
|
|
370
|
+
lines.push(`${ind}if (__t) {`)
|
|
371
|
+
lines.push(`${ind} const __x = ${a.wrappedExpression}`)
|
|
372
|
+
const guard = mode === 'create'
|
|
373
|
+
? null
|
|
374
|
+
: mode === 'item'
|
|
375
|
+
? dedupGuard(a.ordinal)
|
|
376
|
+
: `__seed ? (${seedDiffersExpr('__t', a)}) : (${dedupGuard(a.ordinal)})`
|
|
377
|
+
const writeIndent = guard ? `${ind} ` : `${ind} `
|
|
378
|
+
if (guard) lines.push(`${ind} if (${guard}) {`)
|
|
379
|
+
for (const stmt of emitAttrUpdate('__t', a.attrName, '__x', a.meta)) {
|
|
380
|
+
lines.push(`${writeIndent}${stmt}`)
|
|
381
|
+
}
|
|
382
|
+
if (guard) lines.push(`${ind} }`)
|
|
383
|
+
lines.push(`${ind} __l[${a.ordinal}] = __x`)
|
|
384
|
+
lines.push(`${ind}} }`)
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
/**
|
|
388
|
+
* A content-slot write, in the same three modes as {@link emitAttrBinding}.
|
|
389
|
+
*
|
|
390
|
+
* `'outer'` seeds by read-compare-write (§9.3(1)) through the RW door's
|
|
391
|
+
* `read(id)`. `read` returns `null` when it cannot answer (slot never
|
|
392
|
+
* claimed, or not a 'text' slot) and `null !== <the string>` is always true,
|
|
393
|
+
* so the comparison already fails safe into "write it" — no explicit null
|
|
394
|
+
* handling is needed or wanted here.
|
|
395
|
+
*
|
|
396
|
+
* The `'outer'` mode emits a real `if`/`else if` rather than one ternary
|
|
397
|
+
* guard so the seed branch can bind `textOrNode(__x)` ONCE and use it for
|
|
398
|
+
* both the comparison and the write. A ternary would coerce twice on the
|
|
399
|
+
* seed path, which double-invokes a user value's `toString` — observable
|
|
400
|
+
* when it has side effects, wasted work when it doesn't. Hoisting it above
|
|
401
|
+
* the branch instead would fix that but would also coerce on the non-seed
|
|
402
|
+
* path even when dedup skips the write, i.e. on every later tick; the branch
|
|
403
|
+
* keeps exactly one coercion per path taken.
|
|
404
|
+
*
|
|
405
|
+
* `textOrNode` rather than a bare `String(...)` because a child-position
|
|
406
|
+
* interpolation can evaluate to a live Node (`props.renderRow(item)` handed
|
|
407
|
+
* an inline-JSX arrow), and `String(node)` destroys it. The helper passes a
|
|
408
|
+
* Node through so the claim door can promote the slot to 'markup' and splice
|
|
409
|
+
* it; see `claim-slots.ts`. Whether such a call yields a string or a Node is
|
|
410
|
+
* not decidable from the expression's syntax — both are `CallExpression` —
|
|
411
|
+
* so this stays a runtime decision on the value, not a compile-time
|
|
412
|
+
* classification.
|
|
413
|
+
*
|
|
414
|
+
* A Node also makes the seed comparison fail safe on its own: `read(id)`
|
|
415
|
+
* answers with a string or `null`, neither of which is ever `===` a Node, so
|
|
416
|
+
* the seed always writes. That is the correct direction — a Node is freshly
|
|
417
|
+
* built on this run and is never the SSR-rendered content by identity.
|
|
418
|
+
*/
|
|
419
|
+
function emitTextBinding(
|
|
420
|
+
lines: string[],
|
|
421
|
+
ind: string,
|
|
422
|
+
t: LazyRowTextBinding,
|
|
423
|
+
doorExpr: string,
|
|
424
|
+
mode: 'create' | 'item' | 'outer',
|
|
425
|
+
rwDoor: boolean,
|
|
426
|
+
): void {
|
|
427
|
+
lines.push(`${ind}{ const __x = ${t.wrappedExpression}`)
|
|
428
|
+
const writeOf = (valueExpr: string): string =>
|
|
429
|
+
rwDoor
|
|
430
|
+
? `${doorExpr}.write('${t.slotId}', ${valueExpr})`
|
|
431
|
+
: `${doorExpr}('${t.slotId}', ${valueExpr})`
|
|
432
|
+
|
|
433
|
+
if (mode === 'outer') {
|
|
434
|
+
lines.push(`${ind}if (__seed) {`)
|
|
435
|
+
lines.push(`${ind} const __s = textOrNode(__x)`)
|
|
436
|
+
lines.push(`${ind} if (${doorExpr}.read('${t.slotId}') !== __s) ${writeOf('__s')}`)
|
|
437
|
+
lines.push(`${ind}} else if (${dedupGuard(t.ordinal)}) ${writeOf('textOrNode(__x)')}`)
|
|
438
|
+
} else if (mode === 'item') {
|
|
439
|
+
lines.push(`${ind}if (${dedupGuard(t.ordinal)}) ${writeOf('textOrNode(__x)')}`)
|
|
440
|
+
} else {
|
|
441
|
+
lines.push(`${ind}${writeOf('textOrNode(__x)')}`)
|
|
442
|
+
}
|
|
443
|
+
lines.push(`${ind}__l[${t.ordinal}] = __x }`)
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
/**
|
|
447
|
+
* Read-compare-write seeding predicate (§9.3(1)): does the CURRENT DOM
|
|
448
|
+
* differ from the value `__x` this binding would write? Mirrors
|
|
449
|
+
* `emitAttrUpdate`'s per-kind dispatch (`emit-reactive.ts`) — the two must
|
|
450
|
+
* be read together. An unrecognised kind returns `true`, i.e. write on the
|
|
451
|
+
* seed run unconditionally: conservative, never unsound.
|
|
452
|
+
*/
|
|
453
|
+
function seedDiffersExpr(target: string, a: LazyRowAttrBinding): string {
|
|
454
|
+
const html = toHtmlAttrName(a.attrName)
|
|
455
|
+
if (a.attrName === 'dangerouslySetInnerHTML' || html === 'dangerouslySetInnerHTML') return 'true'
|
|
456
|
+
if (html === 'style') return `${target}.getAttribute('style') !== styleToCss(__x)`
|
|
457
|
+
if (html === 'class') return `${target}.getAttribute('class') !== (__x != null ? String(__x) : null)`
|
|
458
|
+
if (html === 'value') return `${target}.value !== String(__x)`
|
|
459
|
+
if (isBooleanAttr(html)) return `${target}.${html} !== !!(__x)`
|
|
460
|
+
if (a.meta.presenceOrUndefined) {
|
|
461
|
+
// Compare the VALUE the writer would produce, not just presence.
|
|
462
|
+
// `emitAttrUpdate` writes `'true'` for `aria-*` (WAI-ARIA requires an
|
|
463
|
+
// explicit value) and `''` for everything else, while SSR renders these
|
|
464
|
+
// as a BARE attribute name (`templateAttrExpr` in `html-template.ts`),
|
|
465
|
+
// which parses to the empty string. So for `aria-*` the SSR value and
|
|
466
|
+
// the client's value legitimately differ while presence agrees — a
|
|
467
|
+
// presence-only check would skip the seed write and leave
|
|
468
|
+
// `aria-pressed=""` where the eager path produces `aria-pressed="true"`.
|
|
469
|
+
// `getAttribute` returns null when absent, which is exactly the falsy
|
|
470
|
+
// expectation, so one comparison covers both directions.
|
|
471
|
+
const written = html.startsWith('aria-') ? 'true' : ''
|
|
472
|
+
return `${target}.getAttribute('${html}') !== (__x ? '${written}' : null)`
|
|
473
|
+
}
|
|
474
|
+
return `${target}.getAttribute('${html}') !== (__x != null ? String(__x) : null)`
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
/** Exported for the emission-shape unit tests. */
|
|
478
|
+
export const __lazyRowInternals = { seedDiffersExpr, dedupGuard }
|
|
@@ -17,6 +17,7 @@ import { emitAttrUpdate } from '../../emit-reactive.ts'
|
|
|
17
17
|
import { templateRootIsSvg } from './template-parse.ts'
|
|
18
18
|
import { emitListenerLine } from './event-listener.ts'
|
|
19
19
|
import { nameForRegistryRef } from '../../component-scope.ts'
|
|
20
|
+
import { claimPlanLiteral, claimWriterVarName, type ClaimSlotSpec } from './claim-plan.ts'
|
|
20
21
|
import type {
|
|
21
22
|
BranchChildComponentInitsPlan,
|
|
22
23
|
BranchEventBindingsPlan,
|
|
@@ -96,6 +97,11 @@ export function stringifyBranchEventBindings(
|
|
|
96
97
|
* SSR side: element has `bf-s` → qsa() finds it, initChild wires events.
|
|
97
98
|
* CSR side: element is a `data-bf-ph` placeholder → createComponent
|
|
98
99
|
* replaces it, then initChild runs against the new element.
|
|
100
|
+
*
|
|
101
|
+
* The placeholder is passed to `createComponent` as its `mountAt`
|
|
102
|
+
* argument so the component's own init runs connected — context resolves
|
|
103
|
+
* by DOM position, and a detached init falls back to the global,
|
|
104
|
+
* last-writer-wins context store.
|
|
99
105
|
*/
|
|
100
106
|
export function stringifyBranchChildComponentInits(
|
|
101
107
|
lines: string[],
|
|
@@ -103,7 +109,7 @@ export function stringifyBranchChildComponentInits(
|
|
|
103
109
|
indent: string,
|
|
104
110
|
): void {
|
|
105
111
|
for (const init of plan) {
|
|
106
|
-
lines.push(`${indent}{ let __c = qsa(__branchScope, ${init.selector}); if (!__c) { const __ph = __branchScope.querySelector('[${DATA_BF_PH}="${init.placeholderId}"]'); if (__ph) { __c = createComponent('${nameForRegistryRef(init.name)}', ${init.propsExpr}
|
|
112
|
+
lines.push(`${indent}{ let __c = qsa(__branchScope, ${init.selector}); if (!__c) { const __ph = __branchScope.querySelector('[${DATA_BF_PH}="${init.placeholderId}"]'); if (__ph) { __c = createComponent('${nameForRegistryRef(init.name)}', ${init.propsExpr}, undefined, undefined, __ph) } } if (__c) initChild('${nameForRegistryRef(init.name)}', __c, ${init.propsExpr}) }`)
|
|
107
113
|
}
|
|
108
114
|
}
|
|
109
115
|
|
|
@@ -153,15 +159,22 @@ export function stringifyBranchInnerLoops(
|
|
|
153
159
|
inner.outerLoopParamBindings,
|
|
154
160
|
)
|
|
155
161
|
}
|
|
156
|
-
|
|
162
|
+
const conditionalTexts = inner.reactiveTexts.filter(t => t.insideConditional)
|
|
163
|
+
const plainTexts = inner.reactiveTexts.filter(t => !t.insideConditional)
|
|
164
|
+
for (const text of conditionalTexts) {
|
|
157
165
|
const bf = profileBindingId(pc, text.slotId)
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
|
|
166
|
+
// Fresh `claimSlots` claim every run (not the cached `lazySlots` door):
|
|
167
|
+
// insert() may swap the branch's DOM between runs, and a 'text' write
|
|
168
|
+
// is a plain, idempotent `nodeValue` assignment with no dedup state to
|
|
169
|
+
// go stale — replaces the old re-query-$t-on-every-run discipline.
|
|
170
|
+
lines.push(`${indent} createEffect(() => { claimSlots(__bel${uid}, [{ id: '${text.slotId}', kind: 'text', path: [] }]).write('${text.slotId}', String(${text.wrappedExpression})) }${bf})`)
|
|
171
|
+
}
|
|
172
|
+
if (plainTexts.length > 0) {
|
|
173
|
+
const slots: ClaimSlotSpec[] = plainTexts.map(t => ({ id: t.slotId, kind: 'text', path: [] }))
|
|
174
|
+
const writer = claimWriterVarName(slots, varSlotId)
|
|
175
|
+
lines.push(`${indent} const ${writer} = lazySlots(__bel${uid}, ${claimPlanLiteral(slots)})`)
|
|
176
|
+
for (const text of plainTexts) {
|
|
177
|
+
lines.push(`${indent} createEffect(() => { ${writer}('${text.slotId}', String(${text.wrappedExpression})) }${profileBindingId(pc, text.slotId)})`)
|
|
165
178
|
}
|
|
166
179
|
}
|
|
167
180
|
if (inner.nestedConditionals.length > 0) {
|
|
@@ -254,14 +267,25 @@ export function stringifyLoopChildArm(
|
|
|
254
267
|
lines.push(`${armIndent}}))`)
|
|
255
268
|
}
|
|
256
269
|
|
|
257
|
-
|
|
258
|
-
//
|
|
259
|
-
//
|
|
260
|
-
//
|
|
261
|
-
// instead of
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
270
|
+
if (arm.texts.length > 0) {
|
|
271
|
+
// 'markup' kind (not 'text') because a Child-position expression's value
|
|
272
|
+
// may be a live Node (e.g. a hoisted `renderNode={(n) => <PillNode/>}`
|
|
273
|
+
// callback, #1213) — claim-slots' markup writer splices a Node in by
|
|
274
|
+
// identity instead of stringifying it to "[object HTMLElement]" (#2347),
|
|
275
|
+
// the same contract `__bfText` used to provide. The writer is built
|
|
276
|
+
// fresh HERE, inside this arm's `bindEvents` body, so every branch
|
|
277
|
+
// activation re-claims against that swap's own `__branchScope` DOM
|
|
278
|
+
// (spec/slot-unification.md §5-A3's "branch-internal slots re-claim per
|
|
279
|
+
// branch activation").
|
|
280
|
+
const slots: ClaimSlotSpec[] = arm.texts.map(t => ({ id: t.slotId, kind: 'markup', path: [] }))
|
|
281
|
+
const writer = claimWriterVarName(slots, varSlotId)
|
|
282
|
+
lines.push(`${armIndent}const ${writer} = lazySlots(__branchScope, ${claimPlanLiteral(slots)})`)
|
|
283
|
+
for (const text of arm.texts) {
|
|
284
|
+
// `escapeTextOrNode`: see emit-reactive.ts's identical wrap for why a
|
|
285
|
+
// string value must be escaped before a 'markup' writer's
|
|
286
|
+
// `innerHTML =` insertion, while a live Node passes through untouched.
|
|
287
|
+
lines.push(`${armIndent}__disposers.push(createDisposableEffect(() => { ${writer}('${text.slotId}', escapeTextOrNode(${text.wrappedExpression})) }${profileBindingId(pc, text.slotId)}))`)
|
|
288
|
+
}
|
|
265
289
|
}
|
|
266
290
|
|
|
267
291
|
lines.push(`${armIndent}return () => __disposers.forEach(d => d())`)
|