@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
|
@@ -0,0 +1,440 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lazy row graph eligibility — `spec/slot-unification.md` §9.4 (L3).
|
|
3
|
+
*
|
|
4
|
+
* ONE explicit decision function (`lazyRowEligibility`) answering a single
|
|
5
|
+
* question: may this plain loop row be emitted through `mapArrayLazy`
|
|
6
|
+
* (a compiler-emitted `LazyRowPlan`, no per-row reactive resources) instead
|
|
7
|
+
* of today's A3b-consolidated eager `mapArray` emission?
|
|
8
|
+
*
|
|
9
|
+
* Sound-or-loud (§9.3): the answer is always either `{ eligible: true }` or
|
|
10
|
+
* `{ eligible: false, reason }`. There is no silent third path — an
|
|
11
|
+
* ineligible loop keeps byte-for-byte today's emission, and `reason` is a
|
|
12
|
+
* human-readable string so a regression is diagnosable from a unit test
|
|
13
|
+
* rather than by diffing generated JS.
|
|
14
|
+
*
|
|
15
|
+
* The gate is deliberately conservative. Every restriction below is a
|
|
16
|
+
* "widen later" decision, never a correctness compromise:
|
|
17
|
+
*
|
|
18
|
+
* 1. **Shape** (§9.4 "plain loop-row shape"): non-anchored, non-flatMap,
|
|
19
|
+
* single-root, keyed, conditional-free, ref-free, no nested components
|
|
20
|
+
* or inner loops.
|
|
21
|
+
* 2. **No preamble.** A `.map()` callback preamble declares row-local
|
|
22
|
+
* bindings (and can declare row-local signals/memos, which need per-row
|
|
23
|
+
* reactivity by definition). Proving "declares no signal/memo" is not
|
|
24
|
+
* cheap here, and every preamble-declared local would land in a
|
|
25
|
+
* binding's free-identifier set as an unresolvable name anyway — so the
|
|
26
|
+
* rule is the cheap conservative one: **any** `mapPreambleWrapped` or
|
|
27
|
+
* any `preambleRegions` entry makes the loop ineligible.
|
|
28
|
+
* 3. **Every reactive outer dependency must be primable.** `mapArrayLazy`
|
|
29
|
+
* creates ONE loop-level effect for `applyOuter`; that effect subscribes
|
|
30
|
+
* only to what its body reads, and its body must read the outer signals
|
|
31
|
+
* even when the entry list is momentarily EMPTY (otherwise the effect
|
|
32
|
+
* never subscribes and the loop goes permanently dead). The emitter
|
|
33
|
+
* primes those reads with `getter()` statements, which is only possible
|
|
34
|
+
* for names it can prove are zero-arg component signal / memo getters.
|
|
35
|
+
* Any other name that could change reactively — a prop accessor (props
|
|
36
|
+
* may be defined as getters over the parent's signals), or a name the
|
|
37
|
+
* compiler cannot classify at all — is `opaque` and makes the loop
|
|
38
|
+
* ineligible. Imports and component-local functions are opaque TOO
|
|
39
|
+
* (`const isSelected = createSelector(selected)` is an ordinary local
|
|
40
|
+
* whose CALL is the reactive read), so they refuse the loop as well —
|
|
41
|
+
* the `inert` set names them only so the refusal can say which one.
|
|
42
|
+
* Genuinely ignorable are the names that cannot carry a reactive read
|
|
43
|
+
* at all: literal-derived local / module constants and pure globals.
|
|
44
|
+
* 4. **Outer-involving TEXT bindings are ACCEPTED** (§9.5, lifted).
|
|
45
|
+
* §9.3(1) requires read-compare-write seeding on `applyOuter`'s first
|
|
46
|
+
* run: compute the value, READ the current DOM, write only on
|
|
47
|
+
* difference. For an attribute that read is `getAttribute` / a DOM
|
|
48
|
+
* property on the held element ref. For a content slot the read door is
|
|
49
|
+
* `lazyClaimSlots`' `read(id)` (`claim-slots.ts`), the read-capable twin
|
|
50
|
+
* of `lazySlots` over the SAME claim. The emitter picks the door per
|
|
51
|
+
* LOOP — a loop with no outer-involving text keeps the single-closure
|
|
52
|
+
* writer — so this widening costs nothing for loops that do not use it
|
|
53
|
+
* (see `refParts` in `stringify/lazy-row.ts`).
|
|
54
|
+
* 5. **Loop-source hydration consistency** (§9.3(2)): item-driven bindings
|
|
55
|
+
* are never evaluated at hydration, so the SSR rows and the first client
|
|
56
|
+
* `items()` read are TRUSTED to agree. That trust is only sound when
|
|
57
|
+
* both derive from the same data — props (identical by construction via
|
|
58
|
+
* the `bf-p` protocol), literals, and derivations thereof. Any name in
|
|
59
|
+
* the loop source that reaches an import, an environment read, or
|
|
60
|
+
* anything the compiler cannot resolve → ineligible.
|
|
61
|
+
* 6. **Profile mode is never lazy** (§9.4, same policy as A3b): a merged
|
|
62
|
+
* loop-level effect would make every binding on every row share one
|
|
63
|
+
* `#binding:<slotId>` id.
|
|
64
|
+
*/
|
|
65
|
+
|
|
66
|
+
/** Facts about one component signal needed by the §9.3(2) source gate. */
|
|
67
|
+
export interface LazyRowSignalFacts {
|
|
68
|
+
/**
|
|
69
|
+
* Free identifiers of the signal's initializer, derived from its
|
|
70
|
+
* structured `SignalInfo.parsed` tree. `null` when `parsed` was absent or
|
|
71
|
+
* the tree contained an `unsupported` node — "unprovable", which the gate
|
|
72
|
+
* treats as a hard stop rather than an assumption.
|
|
73
|
+
*/
|
|
74
|
+
initializerFreeIdentifiers: ReadonlySet<string> | null
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** Component-scope facts the gate resolves names against. */
|
|
78
|
+
export interface LazyRowScopeInfo {
|
|
79
|
+
/** Signal getter name → initializer facts. */
|
|
80
|
+
signals: ReadonlyMap<string, LazyRowSignalFacts>
|
|
81
|
+
/** Memo getter names — reactive and primable via `name()`. */
|
|
82
|
+
memos: ReadonlySet<string>
|
|
83
|
+
/** Props emit name (`_p`), the source props object name, and every destructured prop param. */
|
|
84
|
+
props: ReadonlySet<string>
|
|
85
|
+
/**
|
|
86
|
+
* Constant name → free identifiers of its value (`ConstantInfo.freeIdentifiers`).
|
|
87
|
+
* An entry with an EMPTY set is literal-derived. Absent free-id data is
|
|
88
|
+
* modelled as `null` → unprovable for the source gate (but still inert for
|
|
89
|
+
* the binding gate — a `const` never changes reactively).
|
|
90
|
+
*/
|
|
91
|
+
constants: ReadonlyMap<string, ReadonlySet<string> | null>
|
|
92
|
+
/**
|
|
93
|
+
* Import local names and component-local function names. Used ONLY by the
|
|
94
|
+
* §9.3(2) source gate (to name the failure precisely); binding
|
|
95
|
+
* classification treats them as OPAQUE, not inert — see
|
|
96
|
+
* {@link classifyLazyBinding}.
|
|
97
|
+
*/
|
|
98
|
+
inert: ReadonlySet<string>
|
|
99
|
+
/** Profile mode (#1690) — never lazy. */
|
|
100
|
+
profile: boolean
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Globals a loop source may legitimately reach without breaking
|
|
105
|
+
* hydration consistency: pure, deterministic, environment-free. Anything
|
|
106
|
+
* NOT in this list (`Date`, `window`, `document`, `localStorage`,
|
|
107
|
+
* `navigator`, `performance`, `Math.random` via `Math`… ) is rejected by
|
|
108
|
+
* the source gate. `Math` is deliberately absent: `Math.random()` is an
|
|
109
|
+
* environment read in every way that matters here.
|
|
110
|
+
*/
|
|
111
|
+
const PURE_SOURCE_GLOBALS: ReadonlySet<string> = new Set([
|
|
112
|
+
'Object', 'Array', 'JSON', 'Number', 'String', 'Boolean',
|
|
113
|
+
])
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Globals that are inert for BINDING classification (they cannot change
|
|
117
|
+
* reactively, so a binding reading one needs no subscription). Broader than
|
|
118
|
+
* {@link PURE_SOURCE_GLOBALS} — a binding may read `Date`/`Intl` and still be
|
|
119
|
+
* lazy, because the binding is re-evaluated on every apply; only the LOOP
|
|
120
|
+
* SOURCE has a hydration-trust obligation.
|
|
121
|
+
*/
|
|
122
|
+
const INERT_BINDING_GLOBALS: ReadonlySet<string> = new Set([
|
|
123
|
+
'Object', 'Array', 'JSON', 'Number', 'String', 'Boolean', 'Math', 'Date',
|
|
124
|
+
'Intl', 'Symbol', 'Map', 'Set', 'WeakMap', 'WeakSet', 'Promise', 'RegExp',
|
|
125
|
+
'Error', 'BigInt', 'console', 'undefined', 'NaN', 'Infinity', 'globalThis',
|
|
126
|
+
'parseInt', 'parseFloat', 'isNaN', 'isFinite',
|
|
127
|
+
'encodeURIComponent', 'decodeURIComponent', 'encodeURI', 'decodeURI',
|
|
128
|
+
])
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Sentinel `opaqueOuterNames` entry for "free-identifier analysis produced
|
|
132
|
+
* nothing". Distinct from a real opaque NAME: a name means the identifier
|
|
133
|
+
* set is known and only its primability is not, which the re-subscribe seam
|
|
134
|
+
* handles; this means the set itself is unknown, which nothing can handle.
|
|
135
|
+
*/
|
|
136
|
+
export const UNKNOWN_IDENTIFIERS = '<unknown>'
|
|
137
|
+
|
|
138
|
+
/** One reactive binding of the row, already classified at build time. */
|
|
139
|
+
export interface ClassifiedLazyBinding {
|
|
140
|
+
kind: 'attr' | 'text'
|
|
141
|
+
slotId: string
|
|
142
|
+
/** Reads the loop param / a destructured param binding. */
|
|
143
|
+
readsItem: boolean
|
|
144
|
+
/** Reads at least one reactive outer name (or is `opaque`). */
|
|
145
|
+
readsOuter: boolean
|
|
146
|
+
/**
|
|
147
|
+
* Reactive outer names this binding depends on — signal / memo getters the
|
|
148
|
+
* emitter primes at the top of `applyOuter` so the loop-level effect
|
|
149
|
+
* subscribes even with zero rows.
|
|
150
|
+
*/
|
|
151
|
+
reactiveOuterNames: readonly string[]
|
|
152
|
+
/**
|
|
153
|
+
* Outer names that are neither primable (signal/memo) nor provably inert
|
|
154
|
+
* — including the FAIL-SAFE case where free identifiers were unavailable
|
|
155
|
+
* (then `readsItem`/`readsOuter` are both forced true and this carries the
|
|
156
|
+
* `'<unknown>'` sentinel). Non-empty ⇒ ineligible.
|
|
157
|
+
*/
|
|
158
|
+
opaqueOuterNames: readonly string[]
|
|
159
|
+
/** References the loop's index parameter — `applyItem`/`applyOuter` have no index. */
|
|
160
|
+
referencesIndex: boolean
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/** §9.4 shape facts, read off the loop plan / IR by the caller. */
|
|
164
|
+
export interface LazyRowShapeFacts {
|
|
165
|
+
/** Which emission site is asking. Both plain-loop-row sites are in scope. */
|
|
166
|
+
callSite: 'plain' | 'branch-plain'
|
|
167
|
+
flatMapLeafItem: boolean
|
|
168
|
+
anchored: boolean
|
|
169
|
+
bodyIsMultiRoot: boolean
|
|
170
|
+
/** `LoopCore.key != null` — index-keyed loops are ineligible in v1 (§9.4). */
|
|
171
|
+
hasExplicitKey: boolean
|
|
172
|
+
conditionalCount: number
|
|
173
|
+
childRefCount: number
|
|
174
|
+
nestedComponentCount: number
|
|
175
|
+
innerLoopCount: number
|
|
176
|
+
hasChildComponent: boolean
|
|
177
|
+
hasMapPreamble: boolean
|
|
178
|
+
preambleRegionCount: number
|
|
179
|
+
/** A destructured loop param with no `paramBindings` (snapshot unwrap). */
|
|
180
|
+
hasParamUnwrap: boolean
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
export interface LazyRowEligibilityArgs {
|
|
184
|
+
shape: LazyRowShapeFacts
|
|
185
|
+
bindings: readonly ClassifiedLazyBinding[]
|
|
186
|
+
/**
|
|
187
|
+
* Free identifiers of the loop's SOURCE (`LoopCore.arrayFreeIdentifiers`
|
|
188
|
+
* unioned with the free identifiers of the fully-chained array expression,
|
|
189
|
+
* so `.filter(...)`/`.sort(...)` dependencies are covered). `null` when the
|
|
190
|
+
* IR carried none — ineligible, never assumed empty.
|
|
191
|
+
*/
|
|
192
|
+
arraySourceIdentifiers: ReadonlySet<string> | null
|
|
193
|
+
scope: LazyRowScopeInfo
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
export type LazyRowEligibility =
|
|
197
|
+
| { eligible: true }
|
|
198
|
+
| { eligible: false; reason: string }
|
|
199
|
+
|
|
200
|
+
const NO: (reason: string) => LazyRowEligibility = (reason) => ({ eligible: false, reason })
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Decide whether one plain loop row may use the lazy row graph (§9.4).
|
|
204
|
+
* Pure — every input is pre-resolved data, so this is directly unit-testable
|
|
205
|
+
* without building a component.
|
|
206
|
+
*/
|
|
207
|
+
export function lazyRowEligibility(args: LazyRowEligibilityArgs): LazyRowEligibility {
|
|
208
|
+
const { shape, bindings, arraySourceIdentifiers, scope } = args
|
|
209
|
+
|
|
210
|
+
// (8) Profile mode keeps the granular eager emission so `#binding:<slotId>`
|
|
211
|
+
// attribution stays truthful — same policy as A3b.
|
|
212
|
+
if (scope.profile) return NO('profile mode keeps the granular eager emission')
|
|
213
|
+
|
|
214
|
+
// (1) Plain loop-row shape.
|
|
215
|
+
if (shape.callSite !== 'plain' && shape.callSite !== 'branch-plain') {
|
|
216
|
+
return NO(`call site '${shape.callSite}' is not a plain loop row`)
|
|
217
|
+
}
|
|
218
|
+
if (shape.flatMapLeafItem) return NO('flatMap descriptor loop (build-or-patch renderItem)')
|
|
219
|
+
if (shape.anchored) return NO('anchored whole-item-conditional loop')
|
|
220
|
+
|
|
221
|
+
// (2) Single-root rows — multi-root needs startMarker/extras bookkeeping
|
|
222
|
+
// `mapArrayLazy` deliberately does not carry.
|
|
223
|
+
if (shape.bodyIsMultiRoot) return NO('multi-root (Fragment) row')
|
|
224
|
+
|
|
225
|
+
// (3) Keyed via an explicit `key`.
|
|
226
|
+
//
|
|
227
|
+
// This is a FAIL-SAFE, not a widening opportunity — do not plan work
|
|
228
|
+
// around it. A `.map()` whose row element has no valid `key` is already
|
|
229
|
+
// a compile ERROR upstream (BF023 / BF024, `checkLoopKey` in
|
|
230
|
+
// `jsx-to-ir.ts`), so no program that compiles cleanly can reach this
|
|
231
|
+
// branch through the plain / branch-plain call sites. It stays because
|
|
232
|
+
// the gate must not depend on another pass having rejected the input,
|
|
233
|
+
// and because a `null` keyFn would silently switch `mapArrayLazy` to
|
|
234
|
+
// positional keying — correct in itself, but a decision this function
|
|
235
|
+
// should make explicitly rather than inherit.
|
|
236
|
+
//
|
|
237
|
+
// `lazy-row-eligibility.test.ts` pins the BF023 half so that if unkeyed
|
|
238
|
+
// loops ever become legal, the test naming this fails and points here.
|
|
239
|
+
if (!shape.hasExplicitKey) return NO('index-keyed loop (no explicit key)')
|
|
240
|
+
|
|
241
|
+
// (4) No conditionals in the row.
|
|
242
|
+
if (shape.conditionalCount > 0) return NO('row contains a reactive conditional')
|
|
243
|
+
|
|
244
|
+
// (5) No refs / child components / inner loops.
|
|
245
|
+
if (shape.childRefCount > 0) return NO('row has imperative child refs')
|
|
246
|
+
if (shape.hasChildComponent) return NO('row body is a child component')
|
|
247
|
+
if (shape.nestedComponentCount > 0) return NO('row contains nested child components')
|
|
248
|
+
if (shape.innerLoopCount > 0) return NO('row contains an inner loop')
|
|
249
|
+
|
|
250
|
+
// (6) No row-local declarations — see the module docstring for why the
|
|
251
|
+
// rule is "any preamble at all", not "a preamble declaring signals".
|
|
252
|
+
if (shape.hasMapPreamble) return NO('row has a map-callback preamble (may declare row-local reactivity)')
|
|
253
|
+
if (shape.preambleRegionCount > 0) return NO('row has preamble-patched regions')
|
|
254
|
+
if (shape.hasParamUnwrap) return NO('destructured loop param without param bindings')
|
|
255
|
+
|
|
256
|
+
// Per-binding gates.
|
|
257
|
+
for (const b of bindings) {
|
|
258
|
+
if (b.referencesIndex) {
|
|
259
|
+
return NO(`binding on slot ${b.slotId} references the loop index parameter`)
|
|
260
|
+
}
|
|
261
|
+
// An identifier set we could not compute at all is different in kind
|
|
262
|
+
// from a name we CAN see but cannot prime. With `free === null` the
|
|
263
|
+
// classifier knows nothing — not the outer reads, and not whether the
|
|
264
|
+
// binding reads the loop INDEX (which it reports as `false` by
|
|
265
|
+
// assumption, since there is nothing to look at). The seam covers
|
|
266
|
+
// stranded subscriptions; it cannot conjure an index parameter that
|
|
267
|
+
// `applyItem`/`applyOuter` do not have. So this one still refuses.
|
|
268
|
+
if (b.opaqueOuterNames.includes(UNKNOWN_IDENTIFIERS)) {
|
|
269
|
+
return NO(`binding on slot ${b.slotId} has no analyzable identifier set`)
|
|
270
|
+
}
|
|
271
|
+
// An outer name the emitter cannot prime used to refuse the loop: the
|
|
272
|
+
// loop-level effect must subscribe on its FIRST run, and with an empty
|
|
273
|
+
// entry list the per-entry reads never execute, so it would subscribe to
|
|
274
|
+
// nothing and go permanently dead. The runtime's re-subscribe seam
|
|
275
|
+
// in `mapArrayLazy` removes that obligation from the compiler (§9.3a):
|
|
276
|
+
// every loop-level outer effect re-runs after a reconcile that created a
|
|
277
|
+
// row or changed an item, so its subscription set is rebuilt against the
|
|
278
|
+
// current entries no matter what the reads turn out to be. Nothing to
|
|
279
|
+
// check here any more — and nothing to signal either, since the seam is
|
|
280
|
+
// unconditional rather than something the compiler opts a loop into.
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// (7) Hydration-consistency gate on the loop source (§9.3(2)).
|
|
284
|
+
if (!arraySourceIdentifiers) return NO('loop source free identifiers unavailable')
|
|
285
|
+
const sourceGate = checkSourceConsistency(arraySourceIdentifiers, scope)
|
|
286
|
+
if (sourceGate) return NO(`loop source is not provably hydration-consistent: ${sourceGate}`)
|
|
287
|
+
|
|
288
|
+
return { eligible: true }
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* §9.3(2): walk each name in the loop source down to props / literals.
|
|
293
|
+
* Returns `null` when every name resolves, otherwise the first failure
|
|
294
|
+
* reason. Cycle-safe via `seen`.
|
|
295
|
+
*/
|
|
296
|
+
function checkSourceConsistency(
|
|
297
|
+
names: ReadonlySet<string>,
|
|
298
|
+
scope: LazyRowScopeInfo,
|
|
299
|
+
): string | null {
|
|
300
|
+
const seen = new Set<string>()
|
|
301
|
+
|
|
302
|
+
const resolve = (name: string): string | null => {
|
|
303
|
+
if (seen.has(name)) return null // already proven (or in-progress) — cycles cannot add a new source
|
|
304
|
+
seen.add(name)
|
|
305
|
+
|
|
306
|
+
if (scope.props.has(name)) return null // prop accessor — identical to SSR by the bf-p protocol
|
|
307
|
+
if (PURE_SOURCE_GLOBALS.has(name)) return null
|
|
308
|
+
|
|
309
|
+
const constFree = scope.constants.get(name)
|
|
310
|
+
if (constFree !== undefined) {
|
|
311
|
+
if (constFree === null) return `constant '${name}' has no analyzable value`
|
|
312
|
+
for (const inner of constFree) {
|
|
313
|
+
const failure = resolve(inner)
|
|
314
|
+
if (failure) return failure
|
|
315
|
+
}
|
|
316
|
+
return null
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
const signal = scope.signals.get(name)
|
|
320
|
+
if (signal !== undefined) {
|
|
321
|
+
if (signal.initializerFreeIdentifiers === null) {
|
|
322
|
+
return `signal '${name}' has no structured initializer to prove props/literal derivation`
|
|
323
|
+
}
|
|
324
|
+
for (const inner of signal.initializerFreeIdentifiers) {
|
|
325
|
+
const failure = resolve(inner)
|
|
326
|
+
if (failure) return failure
|
|
327
|
+
}
|
|
328
|
+
return null
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
if (scope.memos.has(name)) return `memo '${name}' initializer is not analyzed by the v1 gate`
|
|
332
|
+
if (scope.inert.has(name)) return `'${name}' is an import or local function`
|
|
333
|
+
return `'${name}' does not resolve to a prop, literal-derived const, or props/literal-derived signal`
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
for (const name of names) {
|
|
337
|
+
const failure = resolve(name)
|
|
338
|
+
if (failure) return failure
|
|
339
|
+
}
|
|
340
|
+
return null
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* Classify one binding's free identifiers into the row-local / reactive-outer
|
|
345
|
+
* / opaque-outer partition {@link lazyRowEligibility} consumes.
|
|
346
|
+
*
|
|
347
|
+
* **FAIL-SAFE (mandatory, §9.2):** when `free` is `null` — the IR carried no
|
|
348
|
+
* pre-computed free identifiers, or `freeIdentifiers()` refused the parsed
|
|
349
|
+
* tree (an `unsupported` node) — BOTH `readsItem` and `readsOuter` are forced
|
|
350
|
+
* true. A binding with `readsItem` is emitted into `applyItem`; a binding with
|
|
351
|
+
* `readsOuter` into `applyOuter`; a binding in both is emitted in BOTH. Double
|
|
352
|
+
* application is idempotent because every emitted write is dedup-guarded
|
|
353
|
+
* against `entry.last`, so "both" is always the safe answer and never a
|
|
354
|
+
* correctness risk.
|
|
355
|
+
*
|
|
356
|
+
* The unknown case ALSO records an `'<unknown>'` opaque name, which makes the
|
|
357
|
+
* loop ineligible: an outer dependency the compiler cannot name is an outer
|
|
358
|
+
* dependency it cannot PRIME, and an unprimed `applyOuter` effect would never
|
|
359
|
+
* subscribe (see module docstring, restriction 3). So the fail-safe direction
|
|
360
|
+
* is preserved (maximally conservative classification) while the emission
|
|
361
|
+
* refuses loudly instead of shipping a dead effect.
|
|
362
|
+
*/
|
|
363
|
+
export function classifyLazyBinding(args: {
|
|
364
|
+
kind: 'attr' | 'text'
|
|
365
|
+
slotId: string
|
|
366
|
+
free: ReadonlySet<string> | null
|
|
367
|
+
/** Loop param name plus every destructured `paramBindings` name. */
|
|
368
|
+
rowLocalNames: ReadonlySet<string>
|
|
369
|
+
indexParam: string
|
|
370
|
+
scope: LazyRowScopeInfo
|
|
371
|
+
}): ClassifiedLazyBinding {
|
|
372
|
+
const { kind, slotId, free, rowLocalNames, indexParam, scope } = args
|
|
373
|
+
|
|
374
|
+
if (free === null) {
|
|
375
|
+
return {
|
|
376
|
+
kind,
|
|
377
|
+
slotId,
|
|
378
|
+
readsItem: true,
|
|
379
|
+
readsOuter: true,
|
|
380
|
+
reactiveOuterNames: [],
|
|
381
|
+
opaqueOuterNames: [UNKNOWN_IDENTIFIERS],
|
|
382
|
+
// An ASSUMPTION, not knowledge — which is precisely why
|
|
383
|
+
// `UNKNOWN_IDENTIFIERS` must keep refusing the loop: with no
|
|
384
|
+
// identifier set we cannot rule out an index read either, and
|
|
385
|
+
// `applyItem` / `applyOuter` have no index parameter to give it.
|
|
386
|
+
referencesIndex: false,
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
let readsItem = false
|
|
391
|
+
let referencesIndex = false
|
|
392
|
+
const reactiveOuterNames: string[] = []
|
|
393
|
+
const opaqueOuterNames: string[] = []
|
|
394
|
+
|
|
395
|
+
for (const name of free) {
|
|
396
|
+
if (rowLocalNames.has(name)) {
|
|
397
|
+
readsItem = true
|
|
398
|
+
continue
|
|
399
|
+
}
|
|
400
|
+
if (name === indexParam) {
|
|
401
|
+
referencesIndex = true
|
|
402
|
+
continue
|
|
403
|
+
}
|
|
404
|
+
if (INERT_BINDING_GLOBALS.has(name)) continue
|
|
405
|
+
if (scope.signals.has(name) || scope.memos.has(name)) {
|
|
406
|
+
if (!reactiveOuterNames.includes(name)) reactiveOuterNames.push(name)
|
|
407
|
+
continue
|
|
408
|
+
}
|
|
409
|
+
// A LITERAL-DERIVED constant (empty free-identifier set) is the only
|
|
410
|
+
// non-signal local name provably incapable of reactivity: with nothing
|
|
411
|
+
// free in its initializer, it cannot close over a signal, a selector, or
|
|
412
|
+
// any other reactive accessor.
|
|
413
|
+
//
|
|
414
|
+
// Everything else is OPAQUE, and deliberately so — this is where the
|
|
415
|
+
// conservative line has to sit, because a reactive accessor can hide
|
|
416
|
+
// behind an ordinary-looking name:
|
|
417
|
+
// - `const isSelected = createSelector(selected)` is a local `const`
|
|
418
|
+
// whose CALL is reactive (`create-selector.test.ts`),
|
|
419
|
+
// - a local function's body can read a signal,
|
|
420
|
+
// - a prop may be defined as a getter over the parent's signals,
|
|
421
|
+
// - an imported name may be another module's `@client` module signal.
|
|
422
|
+
// None of those can be primed by this emitter, and an unprimed
|
|
423
|
+
// `applyOuter` effect would never subscribe. Refusing the loop (eager
|
|
424
|
+
// fallback) is the sound answer; guessing "inert" would be a silent
|
|
425
|
+
// never-updates bug.
|
|
426
|
+
const constFree = scope.constants.get(name)
|
|
427
|
+
if (constFree !== undefined && constFree !== null && constFree.size === 0) continue
|
|
428
|
+
opaqueOuterNames.push(name)
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
return {
|
|
432
|
+
kind,
|
|
433
|
+
slotId,
|
|
434
|
+
readsItem,
|
|
435
|
+
readsOuter: reactiveOuterNames.length > 0 || opaqueOuterNames.length > 0,
|
|
436
|
+
reactiveOuterNames,
|
|
437
|
+
opaqueOuterNames,
|
|
438
|
+
referencesIndex,
|
|
439
|
+
}
|
|
440
|
+
}
|
|
@@ -26,6 +26,7 @@ import type { IRLoopChildComponent } from '../../../types.ts'
|
|
|
26
26
|
import type { SkeletonSlotPaths } from '../../html-template.ts'
|
|
27
27
|
import type { ReactiveEffectsPlan } from './reactive-effects.ts'
|
|
28
28
|
import type { InnerLoopsPlan } from './inner-loop.ts'
|
|
29
|
+
import type { LazyRowPlanData } from './build-lazy-row.ts'
|
|
29
30
|
|
|
30
31
|
/** Fields shared by every `LoopPlan` variant. */
|
|
31
32
|
interface LoopPlanCommon {
|
|
@@ -174,6 +175,15 @@ interface PlainLoopVariant extends DynamicLoopCommon {
|
|
|
174
175
|
* `__el` to query against.
|
|
175
176
|
*/
|
|
176
177
|
preambleRegions: readonly PreambleRegionPlan[]
|
|
178
|
+
/**
|
|
179
|
+
* Lazy row graph plan (`spec/slot-unification.md` §9, L3). Non-null when
|
|
180
|
+
* `lazyRowEligibility` accepted this loop: the stringifier emits
|
|
181
|
+
* `mapArrayLazy(...)` with this row plan instead of `mapArray(...)` +
|
|
182
|
+
* renderItem, and the row carries NO per-row reactive resources.
|
|
183
|
+
* Undefined for every ineligible loop — those keep today's emission
|
|
184
|
+
* byte-for-byte (sound-or-loud, `plan/lazy-row-eligibility.ts`).
|
|
185
|
+
*/
|
|
186
|
+
lazyRow?: LazyRowPlanData
|
|
177
187
|
}
|
|
178
188
|
|
|
179
189
|
/**
|
|
@@ -29,7 +29,7 @@ import {
|
|
|
29
29
|
} from '../csr-substitute.ts'
|
|
30
30
|
|
|
31
31
|
/**
|
|
32
|
-
* Build the `keyFn` argument for mapArray /
|
|
32
|
+
* Build the `keyFn` argument for mapArray / mapArrayAnchored. `null` when
|
|
33
33
|
* the loop has no key expression. Every `CollectedLoop` variant (top-level /
|
|
34
34
|
* branch / nested) carries an `index: string | null` field (#2218 threaded
|
|
35
35
|
* it onto `NestedLoop` too), so the index param — when present — is always
|
|
@@ -14,7 +14,8 @@ import { stringifyCompositeLoop } from './composite-loop.ts'
|
|
|
14
14
|
import { stringifyEventDelegation } from './event-delegation.ts'
|
|
15
15
|
import { stringifyReactiveEffects } from './reactive-effects.ts'
|
|
16
16
|
import { emitTemplateCloneInline, emitLoopItemElementSetup } from './template-parse.ts'
|
|
17
|
-
import { emitLoopChildRefs
|
|
17
|
+
import { emitLoopChildRefs } from './loop.ts'
|
|
18
|
+
import { stringifyLazyRowLoop } from './lazy-row.ts'
|
|
18
19
|
import type {
|
|
19
20
|
BranchLoopPlan,
|
|
20
21
|
BranchPlainLoopPlan,
|
|
@@ -89,6 +90,27 @@ function emitPlain(lines: string[], plan: BranchPlainLoopPlan): void {
|
|
|
89
90
|
return
|
|
90
91
|
}
|
|
91
92
|
|
|
93
|
+
// Lazy row graph (spec/slot-unification.md §9, L3) — same gate and same
|
|
94
|
+
// emitter as the top-level plain loop; only the container guard and indent
|
|
95
|
+
// differ. Ineligible loops fall through to the eager emission below.
|
|
96
|
+
if (plan.lazyRow) {
|
|
97
|
+
stringifyLazyRowLoop(lines, {
|
|
98
|
+
indent: ' ',
|
|
99
|
+
containerVar,
|
|
100
|
+
guardContainer: true,
|
|
101
|
+
markerId,
|
|
102
|
+
arrayExpr,
|
|
103
|
+
keyFn,
|
|
104
|
+
paramHead,
|
|
105
|
+
indexParam,
|
|
106
|
+
template,
|
|
107
|
+
lazyRow: plan.lazyRow,
|
|
108
|
+
})
|
|
109
|
+
lines.push(` }))`)
|
|
110
|
+
stringifyEventDelegation(lines, eventDelegation)
|
|
111
|
+
return
|
|
112
|
+
}
|
|
113
|
+
|
|
92
114
|
// Non-empty `childRefs` need `__el` as a handle inside the factory body,
|
|
93
115
|
// so force the multi-line layout (#1244) — preamble-patched regions
|
|
94
116
|
// (#2389) need the same handle for their own effect.
|
|
@@ -115,11 +137,19 @@ function emitPlain(lines: string[], plan: BranchPlainLoopPlan): void {
|
|
|
115
137
|
indent: ' ',
|
|
116
138
|
singleRootLayout: 'inline',
|
|
117
139
|
})
|
|
118
|
-
if (reactiveEffects !== null) {
|
|
119
|
-
|
|
140
|
+
if (reactiveEffects !== null || preambleRegions.length > 0) {
|
|
141
|
+
// Row-granularity effects (§3(c)): mirrors `stringifyPlainLoop`'s
|
|
142
|
+
// top-level call — attrs, outer texts, and preamble regions merge
|
|
143
|
+
// into ONE row effect (see `stringifyReactiveEffects`'s docstring).
|
|
144
|
+
stringifyReactiveEffects(lines, reactiveEffects, {
|
|
145
|
+
indent: ' ',
|
|
146
|
+
elVar: '__el',
|
|
147
|
+
bodyIsMultiRoot,
|
|
148
|
+
preambleRegions,
|
|
149
|
+
mapPreambleWrapped,
|
|
150
|
+
})
|
|
120
151
|
}
|
|
121
152
|
emitLoopChildRefs(lines, childRefs, { indent: ' ', elVar: '__el', bodyIsMultiRoot })
|
|
122
|
-
emitPreambleRegionEffects(lines, preambleRegions, mapPreambleWrapped, { indent: ' ', elVar: '__el' })
|
|
123
153
|
lines.push(` return __el`)
|
|
124
154
|
lines.push(` }, '${markerId}'${loopBfId})`)
|
|
125
155
|
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Claim-plan literal rendering — the compiler-side half of slot unification
|
|
3
|
+
* A3 (`spec/slot-unification.md` §4/§5-A3). The runtime's `SlotSpec`/
|
|
4
|
+
* `ClaimPlan` (`@barefootjs/client/runtime/claim-slots.ts`, A2) are pure
|
|
5
|
+
* data: `{ id, kind, path }` triples. This module is the single place that
|
|
6
|
+
* turns compile-time slot descriptors into that data's source-text form —
|
|
7
|
+
* every content-slot emission site (loop rows, preamble regions, dynamic
|
|
8
|
+
* text/JSX slots, `@client` expressions) renders its plan through
|
|
9
|
+
* `claimPlanLiteral`, never by hand-splicing strings, so the literal shape
|
|
10
|
+
* can only ever match `SlotSpec`.
|
|
11
|
+
*
|
|
12
|
+
* Paths are real root-relative child-index arrays wherever the caller
|
|
13
|
+
* already has them (the hoisted single-root loop skeleton, #2143's
|
|
14
|
+
* `SkeletonSlotPaths`); everywhere else a slot gets `path: []` and A2's
|
|
15
|
+
* `lazySlots`/`claimSlots` marker-scan fallback resolves it — sound, just a
|
|
16
|
+
* per-claim scan instead of an O(depth) walk. This is the explicit
|
|
17
|
+
* "cannot be statically pathed" escape valve `spec/slot-unification.md`
|
|
18
|
+
* §5-A3 sanctions, not a shortcut invented here.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
export interface ClaimSlotSpec {
|
|
22
|
+
readonly id: string
|
|
23
|
+
readonly kind: 'text' | 'markup'
|
|
24
|
+
/** Root-relative child-index path, used when the path is unconditionally
|
|
25
|
+
* valid at claim time. Ignored when `pathExpr` is set. */
|
|
26
|
+
readonly path: readonly number[]
|
|
27
|
+
/**
|
|
28
|
+
* Raw JS expression (already rendered source text) evaluating to a
|
|
29
|
+
* `number[]` at runtime, for the one case a plain literal can't express:
|
|
30
|
+
* a hoisted-skeleton path that's valid ONLY on the fresh-CSR-clone branch,
|
|
31
|
+
* never on the hydration-adopts-SSR branch (#2143 precedent — `__p` is
|
|
32
|
+
* nulled when `__existing` is truthy because the skeleton's simplified
|
|
33
|
+
* markup doesn't describe the real SSR-rendered tree). Typically
|
|
34
|
+
* `__existing ? [] : [1, 0]`. Overrides `path` when present.
|
|
35
|
+
*/
|
|
36
|
+
readonly pathExpr?: string
|
|
37
|
+
/**
|
|
38
|
+
* Slot unification Step B (`spec/slot-unification.md` §3(b), §5 Step B):
|
|
39
|
+
* true when NO marker was emitted for this slot — `path`'s last index is
|
|
40
|
+
* the slot's own position, not an anchor comment. Only ever set by
|
|
41
|
+
* `client-only-elision.ts`-derived callers; every other emission site
|
|
42
|
+
* omits it (falsy = today's marker-based behavior, unchanged).
|
|
43
|
+
*/
|
|
44
|
+
readonly markerless?: boolean
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Render one `SlotSpec` as a source-text object literal. */
|
|
48
|
+
function slotSpecLiteral(slot: ClaimSlotSpec): string {
|
|
49
|
+
const pathSrc = slot.pathExpr ?? `[${slot.path.join(', ')}]`
|
|
50
|
+
const markerlessSrc = slot.markerless ? ', markerless: true' : ''
|
|
51
|
+
return `{ id: '${slot.id}', kind: '${slot.kind}', path: ${pathSrc}${markerlessSrc} }`
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Render a `ClaimPlan` (an array of `SlotSpec`) as a source-text array literal. */
|
|
55
|
+
export function claimPlanLiteral(slots: readonly ClaimSlotSpec[]): string {
|
|
56
|
+
return `[${slots.map(slotSpecLiteral).join(', ')}]`
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* A stable JS identifier for the claimed-slot writer covering `slots`,
|
|
61
|
+
* derived from the first slot's id. Slot ids are unique per component (the
|
|
62
|
+
* `bf="sN"` marker namespace), so this can never collide across two
|
|
63
|
+
* distinct claim-plan call sites within the same emitted component.
|
|
64
|
+
*/
|
|
65
|
+
export function claimWriterVarName(slots: readonly ClaimSlotSpec[], sanitize: (id: string) => string): string {
|
|
66
|
+
const first = slots[0]?.id ?? '0'
|
|
67
|
+
return `__bfw_${sanitize(first)}`
|
|
68
|
+
}
|
|
@@ -30,11 +30,12 @@
|
|
|
30
30
|
* <indent>}) }
|
|
31
31
|
*/
|
|
32
32
|
|
|
33
|
-
import { keyAttrName, profileBindingId } from '../../utils.ts'
|
|
33
|
+
import { keyAttrName, profileBindingId, varSlotId } from '../../utils.ts'
|
|
34
34
|
import { emitComponentAndEventSetup } from '../shared.ts'
|
|
35
35
|
import { emitAttrUpdate } from '../../emit-reactive.ts'
|
|
36
36
|
import { emitMultiRootTemplateCloneLines, templateRootIsSvg } from './template-parse.ts'
|
|
37
37
|
import { emitLoopChildRefs } from './loop.ts'
|
|
38
|
+
import { claimPlanLiteral, claimWriterVarName, type ClaimSlotSpec } from './claim-plan.ts'
|
|
38
39
|
import type {
|
|
39
40
|
InnerLoopPlan,
|
|
40
41
|
InnerLoopsPlan,
|
|
@@ -110,15 +111,23 @@ function emitReactive(lines: string[], inner: InnerLoopPlan, indent: string, pc:
|
|
|
110
111
|
if (inner.childLevels.length > 0) {
|
|
111
112
|
stringifyInnerLoops(lines, inner.childLevels, `${indent} `, pc)
|
|
112
113
|
}
|
|
113
|
-
|
|
114
|
+
const conditionalTexts = emit.reactiveTexts.filter(t => t.insideConditional)
|
|
115
|
+
const plainTexts = emit.reactiveTexts.filter(t => !t.insideConditional)
|
|
116
|
+
for (const text of conditionalTexts) {
|
|
114
117
|
const bf = profileBindingId(pc, text.slotId)
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
118
|
+
// A fresh `claimSlots` claim on every run (not the cached `lazySlots`
|
|
119
|
+
// door): insert() may swap the branch's DOM between runs, and 'text'
|
|
120
|
+
// writes are a plain, idempotent `nodeValue` assignment with no
|
|
121
|
+
// dedup/trust-first-run state to go stale — so re-claiming here is exactly
|
|
122
|
+
// as safe as, and replaces, the old re-query-$t-on-every-run discipline.
|
|
123
|
+
lines.push(`${indent} createEffect(() => { claimSlots(__innerEl${uid}, [{ id: '${text.slotId}', kind: 'text', path: [] }]).write('${text.slotId}', String(${text.wrappedExpression})) }${bf})`)
|
|
124
|
+
}
|
|
125
|
+
if (plainTexts.length > 0) {
|
|
126
|
+
const slots: ClaimSlotSpec[] = plainTexts.map(t => ({ id: t.slotId, kind: 'text', path: [] }))
|
|
127
|
+
const writer = claimWriterVarName(slots, varSlotId)
|
|
128
|
+
lines.push(`${indent} const ${writer} = lazySlots(__innerEl${uid}, ${claimPlanLiteral(slots)})`)
|
|
129
|
+
for (const text of plainTexts) {
|
|
130
|
+
lines.push(`${indent} createEffect(() => { ${writer}('${text.slotId}', String(${text.wrappedExpression})) }${profileBindingId(pc, text.slotId)})`)
|
|
122
131
|
}
|
|
123
132
|
}
|
|
124
133
|
for (const attr of emit.reactiveAttrs) {
|