@barefootjs/jsx 0.35.0 → 0.35.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/analyzer-context.d.ts.map +1 -1
- package/dist/index.js +731 -691
- package/dist/ir-to-client-js/collect-elements.d.ts +22 -2
- package/dist/ir-to-client-js/collect-elements.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/plan/build-composite-loop.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/plan/build-inner-loop.d.ts +14 -2
- package/dist/ir-to-client-js/control-flow/plan/build-inner-loop.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/plan/build-lazy-row.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/plan/build-loop-child-arm.d.ts +6 -1
- package/dist/ir-to-client-js/control-flow/plan/build-loop-child-arm.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/plan/build-plain-row.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/plan/build-reactive-effects.d.ts +3 -2
- 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/inner-loop.d.ts +24 -44
- package/dist/ir-to-client-js/control-flow/plan/inner-loop.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/plan/loop-child-arm.d.ts +14 -0
- package/dist/ir-to-client-js/control-flow/plan/loop-child-arm.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/stringify/inner-loop.d.ts +6 -14
- 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/lazy-row.d.ts +1 -3
- 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/loop-child-arm.d.ts +19 -3
- 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.map +1 -1
- package/dist/ir-to-client-js/control-flow/stringify/reactive-effects.d.ts +5 -1
- package/dist/ir-to-client-js/control-flow/stringify/reactive-effects.d.ts.map +1 -1
- package/dist/ir-to-client-js/emit-reactive.d.ts +54 -0
- package/dist/ir-to-client-js/emit-reactive.d.ts.map +1 -1
- package/dist/ir-to-client-js/html-template.d.ts.map +1 -1
- package/dist/ir-to-client-js/reactivity.d.ts +62 -18
- package/dist/ir-to-client-js/reactivity.d.ts.map +1 -1
- package/dist/ir-to-client-js/types.d.ts +18 -2
- package/dist/ir-to-client-js/types.d.ts.map +1 -1
- package/dist/reactivity-checker.d.ts +18 -0
- package/dist/reactivity-checker.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/__tests__/__snapshots__/doc-examples.test.ts.snap +128 -10
- package/src/__tests__/binding-scope-ratchet.test.ts +26 -3
- package/src/__tests__/child-prop-fallback-wrap.test.ts +4 -2
- package/src/__tests__/compiler-stress-1244.test.ts +8 -8
- package/src/__tests__/issue-2868-cond-arm-tag-collision.test.ts +185 -0
- package/src/__tests__/issue-2869-attr-dedup-guard.test.ts +141 -0
- package/src/__tests__/loop-index-reactivity.test.ts +208 -0
- package/src/__tests__/map-body-brand-checker.test.ts +176 -0
- package/src/__tests__/nested-loop-index-param.test.ts +27 -5
- package/src/__tests__/nested-loop-non-outer-reactivity.test.ts +177 -0
- package/src/__tests__/nested-loop-plain.test.ts +39 -25
- package/src/__tests__/nested-loop-reactive-attrs.test.ts +12 -4
- package/src/__tests__/preamble-branch-arm-rerun.test.ts +135 -0
- package/src/__tests__/style-css-var-reactive.test.ts +4 -1
- package/src/analyzer-context.ts +12 -10
- package/src/ir-to-client-js/collect-elements.ts +110 -32
- package/src/ir-to-client-js/control-flow/plan/build-composite-loop.ts +2 -0
- package/src/ir-to-client-js/control-flow/plan/build-inner-loop.ts +28 -51
- package/src/ir-to-client-js/control-flow/plan/build-lazy-row.ts +7 -2
- package/src/ir-to-client-js/control-flow/plan/build-loop-child-arm.ts +33 -6
- package/src/ir-to-client-js/control-flow/plan/build-plain-row.ts +7 -1
- package/src/ir-to-client-js/control-flow/plan/build-reactive-effects.ts +18 -6
- package/src/ir-to-client-js/control-flow/plan/inner-loop.ts +24 -45
- package/src/ir-to-client-js/control-flow/plan/loop-child-arm.ts +14 -0
- package/src/ir-to-client-js/control-flow/stringify/inner-loop.ts +20 -66
- package/src/ir-to-client-js/control-flow/stringify/insert.ts +4 -2
- package/src/ir-to-client-js/control-flow/stringify/lazy-row.ts +12 -15
- package/src/ir-to-client-js/control-flow/stringify/loop-child-arm.ts +53 -11
- package/src/ir-to-client-js/control-flow/stringify/loop.ts +4 -2
- package/src/ir-to-client-js/control-flow/stringify/reactive-effects.ts +29 -18
- package/src/ir-to-client-js/emit-reactive.ts +88 -5
- package/src/ir-to-client-js/html-template.ts +9 -1
- package/src/ir-to-client-js/reactivity.ts +73 -27
- package/src/ir-to-client-js/types.ts +19 -3
- package/src/jsx-to-ir.ts +21 -4
- package/src/reactivity-checker.ts +60 -0
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
|
|
14
14
|
import { varSlotId, DATA_BF_PH, keyAttrName, mapArrayKeyArgs, profileBindingId } from '../../utils.ts'
|
|
15
15
|
import { emitComponentAndEventSetup } from '../shared.ts'
|
|
16
|
-
import {
|
|
16
|
+
import { emitDedupedAttrUpdate, DEDUP_STORE_DECL } from '../../emit-reactive.ts'
|
|
17
17
|
import { namespaceWrapForTemplate } from './template-parse.ts'
|
|
18
18
|
import { emitListenerLine } from './event-listener.ts'
|
|
19
19
|
import { nameForRegistryRef } from '../../component-scope.ts'
|
|
@@ -46,15 +46,25 @@ export function stringifyBranchReactiveAttrs(
|
|
|
46
46
|
lines: string[],
|
|
47
47
|
plan: readonly ReactiveAttrSlot[],
|
|
48
48
|
indent: string,
|
|
49
|
-
pc
|
|
49
|
+
pc: string | undefined,
|
|
50
|
+
/** The row's preamble re-run statement, when this scope has one (#2596/#2447) — see `conditionGetterExpr`'s docstring for the shared contract. */
|
|
51
|
+
mapPreambleWrapped: string | undefined,
|
|
50
52
|
): void {
|
|
51
53
|
for (const slot of plan) {
|
|
52
54
|
const varName = `__ra_${varSlotId(slot.slotId)}`
|
|
53
55
|
lines.push(`${indent}{ const ${varName} = qsa(__branchScope, '[bf="${slot.slotId}"]')`)
|
|
56
|
+
lines.push(`${indent}${DEDUP_STORE_DECL}`)
|
|
54
57
|
lines.push(`${indent}if (${varName}) {`)
|
|
58
|
+
let ordinal = 0
|
|
55
59
|
for (const attr of slot.attrs) {
|
|
56
60
|
lines.push(`${indent} __disposers.push(createDisposableEffect(() => {`)
|
|
57
|
-
|
|
61
|
+
// One effect per attr, so an attr reading a preamble local needs the
|
|
62
|
+
// declarations inside ITS OWN effect — same rule, same line, as
|
|
63
|
+
// `emitAttrSlotsGranular` (reactive-effects.ts, #2447 follow-up).
|
|
64
|
+
if (attr.readsPreamble && mapPreambleWrapped) {
|
|
65
|
+
lines.push(`${indent} ${mapPreambleWrapped}`)
|
|
66
|
+
}
|
|
67
|
+
for (const stmt of emitDedupedAttrUpdate(varName, attr.attrName, attr.wrappedExpression, attr.meta, ordinal++)) {
|
|
58
68
|
lines.push(`${indent} ${stmt}`)
|
|
59
69
|
}
|
|
60
70
|
lines.push(`${indent} }${profileBindingId(pc, slot.slotId)}))`)
|
|
@@ -156,6 +166,8 @@ export function stringifyBranchInnerLoops(
|
|
|
156
166
|
[...inner.legacyEvents],
|
|
157
167
|
inner.outerLoopParam,
|
|
158
168
|
inner.outerLoopParamBindings,
|
|
169
|
+
false,
|
|
170
|
+
inner.outerLoopIndex,
|
|
159
171
|
)
|
|
160
172
|
}
|
|
161
173
|
const conditionalTexts = inner.reactiveTexts.filter(t => t.insideConditional)
|
|
@@ -177,7 +189,13 @@ export function stringifyBranchInnerLoops(
|
|
|
177
189
|
}
|
|
178
190
|
}
|
|
179
191
|
if (inner.nestedConditionals.length > 0) {
|
|
180
|
-
|
|
192
|
+
// An inner loop's own conditionals are classified against the INNER
|
|
193
|
+
// loop's preamble names, not the outer row's — `BranchInnerLoop`
|
|
194
|
+
// carries no re-runnable preamble string for that scope, so there is
|
|
195
|
+
// nothing correct to inject here. The `readsPreamble && mapPreambleWrapped`
|
|
196
|
+
// guard downstream makes this `undefined` a no-op, exactly as before
|
|
197
|
+
// this fix (inner-loop preambles are out of scope — see PR notes).
|
|
198
|
+
stringifyLoopChildConditionals(lines, inner.nestedConditionals, `${indent} `, pc, undefined)
|
|
181
199
|
}
|
|
182
200
|
lines.push(`${indent} return __bel${uid}`)
|
|
183
201
|
// #2753 Shape B: see the identical comment in `inner-loop.ts` —
|
|
@@ -198,33 +216,55 @@ export function stringifyLoopChildConditionals(
|
|
|
198
216
|
lines: string[],
|
|
199
217
|
conditionals: readonly LoopChildConditionalPlan[],
|
|
200
218
|
indent: string,
|
|
201
|
-
pc
|
|
219
|
+
pc: string | undefined,
|
|
220
|
+
/** The row's preamble re-run statement, when this scope has one (#2596/#2447). */
|
|
221
|
+
mapPreambleWrapped: string | undefined,
|
|
202
222
|
): void {
|
|
203
223
|
for (const cond of conditionals) {
|
|
204
|
-
stringifyLoopChildConditional(lines, cond, indent, pc)
|
|
224
|
+
stringifyLoopChildConditional(lines, cond, indent, pc, mapPreambleWrapped)
|
|
205
225
|
}
|
|
206
226
|
}
|
|
207
227
|
|
|
228
|
+
/**
|
|
229
|
+
* The `() => cond` getter handed to `insert()`. A condition flagged
|
|
230
|
+
* `readsPreamble` (#2596) re-runs the row preamble INSIDE the getter —
|
|
231
|
+
* `insert()` re-invokes this closure on every dependency change to decide
|
|
232
|
+
* the branch, and a preamble local is a plain per-row `const`, not a signal
|
|
233
|
+
* `insert()` can see through on its own. ONE implementation for both the
|
|
234
|
+
* outer (`emitOuterConditional`, reactive-effects.ts) and nested
|
|
235
|
+
* (`stringifyLoopChildConditional`, here) levels so the two can't drift.
|
|
236
|
+
*/
|
|
237
|
+
export function conditionGetterExpr(
|
|
238
|
+
wrappedCondition: string,
|
|
239
|
+
readsPreamble: boolean | undefined,
|
|
240
|
+
mapPreambleWrapped: string | undefined,
|
|
241
|
+
): string {
|
|
242
|
+
return readsPreamble && mapPreambleWrapped
|
|
243
|
+
? `() => { ${mapPreambleWrapped}; return (${wrappedCondition}) }`
|
|
244
|
+
: `() => ${wrappedCondition}`
|
|
245
|
+
}
|
|
246
|
+
|
|
208
247
|
function stringifyLoopChildConditional(
|
|
209
248
|
lines: string[],
|
|
210
249
|
cond: LoopChildConditionalPlan,
|
|
211
250
|
indent: string,
|
|
212
251
|
pc: string | undefined,
|
|
252
|
+
mapPreambleWrapped: string | undefined,
|
|
213
253
|
): void {
|
|
214
254
|
const armIndent = `${indent} `
|
|
215
255
|
// Body-form arrows wire `__bfSlot` captures into the runtime so live
|
|
216
256
|
// `Node` returns from Child-position interpolations are spliced into
|
|
217
257
|
// the parsed fragment instead of being stringified by the surrounding
|
|
218
258
|
// template literal (#1213).
|
|
219
|
-
lines.push(`${indent}insert(${cond.scopeVar}, '${cond.slotId}',
|
|
259
|
+
lines.push(`${indent}insert(${cond.scopeVar}, '${cond.slotId}', ${conditionGetterExpr(cond.wrappedCondition, cond.readsPreamble, mapPreambleWrapped)}, {`)
|
|
220
260
|
lines.push(`${indent} template: () => { const __slots = []; return { html: \`${cond.whenTrueTemplateHtml}\`, slots: __slots } },`)
|
|
221
261
|
lines.push(`${indent} bindEvents: (__branchScope, { isFirstRun: __bfFirstRun = false } = {}) => {`)
|
|
222
|
-
stringifyLoopChildArm(lines, cond.whenTrueArm, armIndent, pc)
|
|
262
|
+
stringifyLoopChildArm(lines, cond.whenTrueArm, armIndent, pc, mapPreambleWrapped)
|
|
223
263
|
lines.push(`${indent} }`)
|
|
224
264
|
lines.push(`${indent}}, {`)
|
|
225
265
|
lines.push(`${indent} template: () => { const __slots = []; return { html: \`${cond.whenFalseTemplateHtml}\`, slots: __slots } },`)
|
|
226
266
|
lines.push(`${indent} bindEvents: (__branchScope, { isFirstRun: __bfFirstRun = false } = {}) => {`)
|
|
227
|
-
stringifyLoopChildArm(lines, cond.whenFalseArm, armIndent, pc)
|
|
267
|
+
stringifyLoopChildArm(lines, cond.whenFalseArm, armIndent, pc, mapPreambleWrapped)
|
|
228
268
|
lines.push(`${indent} }`)
|
|
229
269
|
lines.push(`${indent}}${profileBindingId(pc, cond.slotId)})`)
|
|
230
270
|
}
|
|
@@ -242,6 +282,8 @@ export function stringifyLoopChildArm(
|
|
|
242
282
|
arm: LoopChildArmPlan,
|
|
243
283
|
armIndent: string,
|
|
244
284
|
pc: string | undefined,
|
|
285
|
+
/** The row's preamble re-run statement, when this scope has one (#2596/#2447) — forwarded to this arm's own reactive attrs and nested conditionals. */
|
|
286
|
+
mapPreambleWrapped: string | undefined,
|
|
245
287
|
): void {
|
|
246
288
|
stringifyBranchEventBindings(lines, arm.events, armIndent)
|
|
247
289
|
stringifyBranchChildComponentInits(lines, arm.childComponents, armIndent)
|
|
@@ -257,7 +299,7 @@ export function stringifyLoopChildArm(
|
|
|
257
299
|
if (!hasDisposables) return
|
|
258
300
|
|
|
259
301
|
lines.push(`${armIndent}const __disposers = []`)
|
|
260
|
-
stringifyBranchReactiveAttrs(lines, arm.attrs, armIndent, pc)
|
|
302
|
+
stringifyBranchReactiveAttrs(lines, arm.attrs, armIndent, pc, mapPreambleWrapped)
|
|
261
303
|
|
|
262
304
|
// Nested conditionals: each inner `insert()` is wrapped in its own
|
|
263
305
|
// disposable effect so disposing THIS entry cascades to whatever
|
|
@@ -265,7 +307,7 @@ export function stringifyLoopChildArm(
|
|
|
265
307
|
// conditional's nested-conditional handling in stringify/insert.ts).
|
|
266
308
|
for (const cond of arm.nestedConditionals) {
|
|
267
309
|
lines.push(`${armIndent}__disposers.push(createDisposableEffect(() => {`)
|
|
268
|
-
stringifyLoopChildConditional(lines, cond, `${armIndent} `, pc)
|
|
310
|
+
stringifyLoopChildConditional(lines, cond, `${armIndent} `, pc, mapPreambleWrapped)
|
|
269
311
|
lines.push(`${armIndent}}))`)
|
|
270
312
|
}
|
|
271
313
|
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
*/
|
|
31
31
|
|
|
32
32
|
import { emitRefCall, varSlotId, profileBindingId } from '../../utils.ts'
|
|
33
|
-
import {
|
|
33
|
+
import { emitDedupedAttrUpdate, DEDUP_STORE_DECL } from '../../emit-reactive.ts'
|
|
34
34
|
import { stringifyReactiveEffects } from './reactive-effects.ts'
|
|
35
35
|
import { emitTemplateCloneInline, emitLoopItemElementSetup, emitHoistedTemplateDecl, hoistedCloneExpr, namespaceWrapForTemplate, multiRootNamespaceWrapForTemplate, wrapHtmlForNamespace } from './template-parse.ts'
|
|
36
36
|
import { buildSkeletonPathPlan, type SkeletonPathPlan } from './skeleton-paths.ts'
|
|
@@ -452,13 +452,15 @@ export function stringifyStaticLoop(lines: string[], plan: StaticLoopPlan): void
|
|
|
452
452
|
lines.push(` }`)
|
|
453
453
|
}
|
|
454
454
|
lines.push(` if (__iterEl) {`)
|
|
455
|
+
if (attrsBySlot.length > 0) lines.push(` ${DEDUP_STORE_DECL}`)
|
|
456
|
+
let ordinal = 0
|
|
455
457
|
for (const [slotId, attrs] of attrsBySlot) {
|
|
456
458
|
const varName = `__t_${varSlotId(slotId)}`
|
|
457
459
|
lines.push(` const ${varName} = qsa(__iterEl, '[bf="${slotId}"]')`)
|
|
458
460
|
lines.push(` if (${varName}) {`)
|
|
459
461
|
for (const attr of attrs) {
|
|
460
462
|
lines.push(` createEffect(() => {`)
|
|
461
|
-
for (const stmt of
|
|
463
|
+
for (const stmt of emitDedupedAttrUpdate(varName, attr.attrName, attr.expression, attr, ordinal++)) {
|
|
462
464
|
lines.push(` ${stmt}`)
|
|
463
465
|
}
|
|
464
466
|
lines.push(` }${profileBindingId(pc, slotId)})`)
|
|
@@ -32,8 +32,8 @@
|
|
|
32
32
|
*/
|
|
33
33
|
|
|
34
34
|
import { varSlotId, profileBindingId } from '../../utils.ts'
|
|
35
|
-
import {
|
|
36
|
-
import { stringifyLoopChildArm } from './loop-child-arm.ts'
|
|
35
|
+
import { emitDedupedAttrUpdate, DEDUP_STORE_DECL } from '../../emit-reactive.ts'
|
|
36
|
+
import { stringifyLoopChildArm, conditionGetterExpr } from './loop-child-arm.ts'
|
|
37
37
|
import { claimPlanLiteral, claimWriterVarName, type ClaimSlotSpec } from './claim-plan.ts'
|
|
38
38
|
import type {
|
|
39
39
|
NestedConditionalPlan,
|
|
@@ -103,7 +103,11 @@ export interface StringifyReactiveEffectsOptions {
|
|
|
103
103
|
* The loop callback's pre-return preamble (already wrapped with the loop
|
|
104
104
|
* param accessor), re-run once per row-effect tick — BEFORE any preamble
|
|
105
105
|
* region read — so `preambleRegions`' `valueExpr`s see freshly recomputed
|
|
106
|
-
* locals.
|
|
106
|
+
* locals. Also consumed by every OTHER `readsPreamble` site in this row's
|
|
107
|
+
* scope: row-level attrs (`emitAttrSlotsGranular`/`emitConsolidatedRowEffect`),
|
|
108
|
+
* outer condition getters (`emitOuterConditional`), and — forwarded
|
|
109
|
+
* through `stringifyLoopChildArm` (#2596 follow-up) — that row's branch
|
|
110
|
+
* arms' own reactive attrs and nested condition getters.
|
|
107
111
|
*/
|
|
108
112
|
mapPreambleWrapped?: string
|
|
109
113
|
}
|
|
@@ -160,7 +164,9 @@ function emitAttrSlotsGranular(
|
|
|
160
164
|
const varName = `__ra_${varSlotId(slot.slotId)}`
|
|
161
165
|
const lookupExpr = attrLookupExpr(slot.slotId, varName, elVar, lookup, elementIndexBySlot)
|
|
162
166
|
lines.push(`${indent}{ const ${varName} = ${lookupExpr}`)
|
|
167
|
+
lines.push(`${indent}${DEDUP_STORE_DECL}`)
|
|
163
168
|
lines.push(`${indent}if (${varName}) {`)
|
|
169
|
+
let ordinal = 0
|
|
164
170
|
for (const attr of slot.attrs) {
|
|
165
171
|
lines.push(`${indent} createEffect(() => {`)
|
|
166
172
|
// Profile mode keeps one effect per attr, so an attr reading a preamble
|
|
@@ -171,7 +177,7 @@ function emitAttrSlotsGranular(
|
|
|
171
177
|
if (attr.readsPreamble && mapPreambleWrapped) {
|
|
172
178
|
lines.push(`${indent} ${mapPreambleWrapped}`)
|
|
173
179
|
}
|
|
174
|
-
for (const stmt of
|
|
180
|
+
for (const stmt of emitDedupedAttrUpdate(varName, attr.attrName, attr.wrappedExpression, attr.meta, ordinal++)) {
|
|
175
181
|
lines.push(`${indent} ${stmt}`)
|
|
176
182
|
}
|
|
177
183
|
lines.push(`${indent} }${bindingBfId(slot.slotId)})`)
|
|
@@ -233,11 +239,14 @@ function attrLookupExpr(
|
|
|
233
239
|
* dispatches per-slot on `kind`), so a row pays for at most one `lazySlots`
|
|
234
240
|
* closure + one claim `Map` instead of one per category.
|
|
235
241
|
*
|
|
236
|
-
* Every attr
|
|
237
|
-
*
|
|
238
|
-
*
|
|
239
|
-
*
|
|
240
|
-
*
|
|
242
|
+
* Every attr write goes through `emitDedupedAttrUpdate` (#2869): a
|
|
243
|
+
* self-contained `{ }` block (so two 'value' attrs in the same row effect
|
|
244
|
+
* can't collide on `emitAttrUpdate`'s bare `const __val`) behind a
|
|
245
|
+
* previous-value guard on the row's ONE `__l` store, ordinals sequential
|
|
246
|
+
* across every slot in the row. Without the guard, fusing bindings into one
|
|
247
|
+
* effect meant a change to ANY binding rewrote ALL of them on every rerun —
|
|
248
|
+
* `untrack()` on one binding's read couldn't stop a sibling binding's
|
|
249
|
+
* legitimate change from re-triggering this effect and re-writing it anyway.
|
|
241
250
|
*/
|
|
242
251
|
function emitConsolidatedRowEffect(
|
|
243
252
|
lines: string[],
|
|
@@ -258,6 +267,11 @@ function emitConsolidatedRowEffect(
|
|
|
258
267
|
const varName = `__ra_${varSlotId(slot.slotId)}`
|
|
259
268
|
lines.push(`${indent}const ${varName} = ${attrLookupExpr(slot.slotId, varName, elVar, lookup, elementIndexBySlot)}`)
|
|
260
269
|
}
|
|
270
|
+
// Row-wide dedup store (#2869) — one `__l`, shared by every attr write
|
|
271
|
+
// below via a sequential ordinal, so a sibling binding's legitimate
|
|
272
|
+
// rerun of this shared effect can't force an unchanged binding to
|
|
273
|
+
// rewrite too.
|
|
274
|
+
if (attrSlots.length > 0) lines.push(`${indent}${DEDUP_STORE_DECL}`)
|
|
261
275
|
|
|
262
276
|
// 2. ONE claimed-slot writer covering outer texts (kind 'text') and
|
|
263
277
|
// preamble regions (kind 'markup') — mixed-kind plans are supported by
|
|
@@ -293,15 +307,14 @@ function emitConsolidatedRowEffect(
|
|
|
293
307
|
if (mapPreambleWrapped && (preambleRegions.length > 0 || attrsReadPreamble(attrSlots))) {
|
|
294
308
|
lines.push(`${indent} ${mapPreambleWrapped}`)
|
|
295
309
|
}
|
|
310
|
+
let ordinal = 0
|
|
296
311
|
for (const slot of attrSlots) {
|
|
297
312
|
const varName = `__ra_${varSlotId(slot.slotId)}`
|
|
298
313
|
lines.push(`${indent} if (${varName}) {`)
|
|
299
314
|
for (const attr of slot.attrs) {
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
lines.push(`${indent} ${stmt}`)
|
|
315
|
+
for (const stmt of emitDedupedAttrUpdate(varName, attr.attrName, attr.wrappedExpression, attr.meta, ordinal++)) {
|
|
316
|
+
lines.push(`${indent} ${stmt}`)
|
|
303
317
|
}
|
|
304
|
-
lines.push(`${indent} }`)
|
|
305
318
|
}
|
|
306
319
|
lines.push(`${indent} }`)
|
|
307
320
|
}
|
|
@@ -354,18 +367,16 @@ function emitOuterConditional(
|
|
|
354
367
|
// scope here (it's a plain per-row `const`, not a signal `insert()` can see
|
|
355
368
|
// through on its own). Same treatment as `readsPreamble` attrs
|
|
356
369
|
// (`emitAttrUpdate`'s callers) get ahead of their own write.
|
|
357
|
-
const conditionGetter = cond.readsPreamble
|
|
358
|
-
? `() => { ${mapPreambleWrapped}; return (${cond.wrappedCondition}) }`
|
|
359
|
-
: `() => ${cond.wrappedCondition}`
|
|
370
|
+
const conditionGetter = conditionGetterExpr(cond.wrappedCondition, cond.readsPreamble, mapPreambleWrapped)
|
|
360
371
|
lines.push(`${indent}insert(${elVar}, '${cond.slotId}', ${conditionGetter}, {`)
|
|
361
372
|
lines.push(`${indent} template: () => { const __slots = []; return { html: \`${cond.whenTrueTemplateHtml}\`, slots: __slots } },`)
|
|
362
373
|
lines.push(`${indent} bindEvents: (__branchScope, { isFirstRun: __bfFirstRun = false } = {}) => {`)
|
|
363
|
-
stringifyLoopChildArm(lines, cond.whenTrueArm, armIndent, pc)
|
|
374
|
+
stringifyLoopChildArm(lines, cond.whenTrueArm, armIndent, pc, mapPreambleWrapped)
|
|
364
375
|
lines.push(`${indent} }`)
|
|
365
376
|
lines.push(`${indent}}, {`)
|
|
366
377
|
lines.push(`${indent} template: () => { const __slots = []; return { html: \`${cond.whenFalseTemplateHtml}\`, slots: __slots } },`)
|
|
367
378
|
lines.push(`${indent} bindEvents: (__branchScope, { isFirstRun: __bfFirstRun = false } = {}) => {`)
|
|
368
|
-
stringifyLoopChildArm(lines, cond.whenFalseArm, armIndent, pc)
|
|
379
|
+
stringifyLoopChildArm(lines, cond.whenFalseArm, armIndent, pc, mapPreambleWrapped)
|
|
369
380
|
lines.push(`${indent} }`)
|
|
370
381
|
lines.push(`${indent}}${profileBindingId(pc, cond.slotId)})`)
|
|
371
382
|
}
|
|
@@ -85,6 +85,9 @@ function emitChildValueMirrorStatements(target: string, expression: string): str
|
|
|
85
85
|
* Generate JS statements to update a DOM attribute reactively.
|
|
86
86
|
* Centralizes the attribute-type dispatch (value, class, boolean, presence, generic)
|
|
87
87
|
* so that new AttrMeta flags are handled in one place.
|
|
88
|
+
*
|
|
89
|
+
* Per-kind WRITE dispatch only — an effect body must never call this
|
|
90
|
+
* directly, only through `emitDedupedAttrUpdate` below (#2869).
|
|
88
91
|
*/
|
|
89
92
|
export function emitAttrUpdate(target: string, attrName: string, expression: string, meta: AttrMeta): string[] {
|
|
90
93
|
const htmlName = toHtmlAttrName(attrName)
|
|
@@ -126,6 +129,81 @@ export function emitAttrUpdate(target: string, attrName: string, expression: str
|
|
|
126
129
|
]
|
|
127
130
|
}
|
|
128
131
|
|
|
132
|
+
/**
|
|
133
|
+
* Last-value store every deduped attribute write in one emitting block
|
|
134
|
+
* shares — `const __l = []`, declared ONCE in the block that creates the
|
|
135
|
+
* effect(s), so the effect closure (a stable object across its own reruns)
|
|
136
|
+
* captures it and no other effect can see it. Each binding owns a fixed
|
|
137
|
+
* numeric ordinal into it. The lazy row graph (`control-flow/stringify/
|
|
138
|
+
* lazy-row.ts`) declares the same `__l` from the reconciler's `entry.last`
|
|
139
|
+
* instead — `applyItem` / `applyOuter` are separate invocations with no
|
|
140
|
+
* closure of their own to own the array — but the guard, the temp name,
|
|
141
|
+
* the ordinal scheme and the record-after-write rule below are shared with
|
|
142
|
+
* it (#2869).
|
|
143
|
+
*/
|
|
144
|
+
export const DEDUP_STORE_DECL = 'const __l = []'
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* `__l`-backed dedup test. `in` (not a truthiness check) so a legitimately
|
|
148
|
+
* `undefined` value still records and still writes once; `!(N in __l)` on
|
|
149
|
+
* the first run is what makes the first write unconditional — the same
|
|
150
|
+
* "dedup, no trust-first-run" contract `claim-slots.ts` documents for
|
|
151
|
+
* 'markup' slots. Moved here from `control-flow/stringify/lazy-row.ts`
|
|
152
|
+
* so the eager paths below and the lazy row graph share one guard (#2869).
|
|
153
|
+
*/
|
|
154
|
+
export function dedupGuard(ordinal: number): string {
|
|
155
|
+
return `!(${ordinal} in __l) || !Object.is(__l[${ordinal}], __x)`
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* The ONLY way an effect body should write a reactive attribute (#2869).
|
|
160
|
+
* Wraps `emitAttrUpdate`'s per-kind write in a previous-value guard:
|
|
161
|
+
* compute once into `__x`, write only when `guard` holds, then record
|
|
162
|
+
* `__x` into `__l[ordinal]` whether or not the write happened, so the
|
|
163
|
+
* stored value always tracks the latest computation. Without this, every
|
|
164
|
+
* rerun of a shared effect — a keyed row's fused effect (#2869's report),
|
|
165
|
+
* or even a plain per-slot effect covering two attrs on one element — rewrote
|
|
166
|
+
* every attribute in it on every rerun, no matter which binding actually
|
|
167
|
+
* changed. `untrack()` cannot fix this on its own: it only suppresses
|
|
168
|
+
* dependency REGISTRATION for the read inside it, not re-execution of the
|
|
169
|
+
* surrounding effect when an unrelated sibling binding legitimately changes
|
|
170
|
+
* — so an `<iframe srcdoc={untrack(...)}>` kept reloading every time a
|
|
171
|
+
* sibling attribute in the same row effect changed.
|
|
172
|
+
*
|
|
173
|
+
* Self-contained block: `__x` (and `emitAttrUpdate`'s own `__v` / bare
|
|
174
|
+
* `const __val`) never leak into the caller's scope, so several bindings
|
|
175
|
+
* can sit in one effect body without a wrapper block of their own.
|
|
176
|
+
*
|
|
177
|
+
* Compares the RAW value, exactly like the lazy-row path always has: an
|
|
178
|
+
* object-valued `style` / `dangerouslySetInnerHTML` is a fresh object per
|
|
179
|
+
* run and simply keeps writing as before (no regression, no gain — a
|
|
180
|
+
* follow-up could compare the coerced value instead).
|
|
181
|
+
*
|
|
182
|
+
* `guard` is overridable for the lazy row graph's `__seed`-run DOM
|
|
183
|
+
* read-compare (`__seed ? <DOM read-compare> : <dedupGuard>`) and `null`
|
|
184
|
+
* for its `createRow` (fresh clone: write unconditionally, still record).
|
|
185
|
+
*/
|
|
186
|
+
export function emitDedupedAttrUpdate(
|
|
187
|
+
target: string,
|
|
188
|
+
attrName: string,
|
|
189
|
+
expression: string,
|
|
190
|
+
meta: AttrMeta,
|
|
191
|
+
ordinal: number,
|
|
192
|
+
guard: string | null = dedupGuard(ordinal),
|
|
193
|
+
): string[] {
|
|
194
|
+
const write = emitAttrUpdate(target, attrName, '__x', meta)
|
|
195
|
+
const lines = [`{ const __x = ${expression}`]
|
|
196
|
+
if (guard) {
|
|
197
|
+
lines.push(`if (${guard}) {`)
|
|
198
|
+
for (const stmt of write) lines.push(` ${stmt}`)
|
|
199
|
+
lines.push(`}`)
|
|
200
|
+
} else {
|
|
201
|
+
lines.push(...write)
|
|
202
|
+
}
|
|
203
|
+
lines.push(`__l[${ordinal}] = __x }`)
|
|
204
|
+
return lines
|
|
205
|
+
}
|
|
206
|
+
|
|
129
207
|
/**
|
|
130
208
|
* Rewrite destructured prop names in an expression to `(props.xxx ?? default)`.
|
|
131
209
|
* Only applies when the component uses destructured props (not props.xxx style).
|
|
@@ -494,20 +572,22 @@ export function emitReactiveAttributeUpdates(lines: string[], ctx: ClientJsConte
|
|
|
494
572
|
|
|
495
573
|
for (const [slotId, attrs] of attrsBySlot) {
|
|
496
574
|
const v = varSlotId(slotId)
|
|
575
|
+
lines.push(` { ${DEDUP_STORE_DECL}`)
|
|
497
576
|
lines.push(` createEffect(() => {`)
|
|
498
577
|
lines.push(` if (_${v}) {`)
|
|
578
|
+
let ordinal = 0
|
|
499
579
|
for (const attr of attrs) {
|
|
500
580
|
// Catalogued-lowering MUST run before the bare-prop-name rewrite:
|
|
501
581
|
// the matcher needs the source-form receiver (a bare identifier or
|
|
502
582
|
// `props.x`), the exact two shapes `resolveReceiverType` supports —
|
|
503
583
|
// same ordering `jsx-to-ir.ts`'s static-template path documents.
|
|
504
584
|
const expression = rewriteDestructuredPropsInExpr(lower(attr.expression), ctx)
|
|
505
|
-
for (const stmt of
|
|
585
|
+
for (const stmt of emitDedupedAttrUpdate(`_${v}`, attr.attrName, expression, attr, ordinal++)) {
|
|
506
586
|
lines.push(` ${stmt}`)
|
|
507
587
|
}
|
|
508
588
|
}
|
|
509
589
|
lines.push(` }`)
|
|
510
|
-
lines.push(` }${bindingIdArg(ctx, slotId)})`)
|
|
590
|
+
lines.push(` }${bindingIdArg(ctx, slotId)}) }`)
|
|
511
591
|
lines.push('')
|
|
512
592
|
}
|
|
513
593
|
}
|
|
@@ -580,6 +660,7 @@ export function emitReactiveChildProps(lines: string[], ctx: ClientJsContext): v
|
|
|
580
660
|
if (ctx.reactiveChildProps.length > 0) {
|
|
581
661
|
lines.push('')
|
|
582
662
|
lines.push(` // Reactive child component props`)
|
|
663
|
+
lines.push(` { ${DEDUP_STORE_DECL}`)
|
|
583
664
|
lines.push(` createEffect(() => {`)
|
|
584
665
|
|
|
585
666
|
const propsByComponent = new Map<string, typeof ctx.reactiveChildProps>()
|
|
@@ -591,6 +672,7 @@ export function emitReactiveChildProps(lines: string[], ctx: ClientJsContext): v
|
|
|
591
672
|
propsByComponent.get(key)!.push(prop)
|
|
592
673
|
}
|
|
593
674
|
|
|
675
|
+
let ordinal = 0
|
|
594
676
|
for (const [, props] of propsByComponent) {
|
|
595
677
|
const first = props[0]
|
|
596
678
|
// The component's own `comment: true` root child IS `__scope` — query
|
|
@@ -609,10 +691,11 @@ export function emitReactiveChildProps(lines: string[], ctx: ClientJsContext): v
|
|
|
609
691
|
// `value` is the CHILD-ROOT MIRROR case, not a developer-authored
|
|
610
692
|
// attribute — route it through the no-SSR-fallback helper instead
|
|
611
693
|
// of `emitAttrUpdate`'s generic (attribute-fallback) dispatch;
|
|
612
|
-
// see `emitChildValueMirrorStatements`'s docstring (#2716).
|
|
694
|
+
// see `emitChildValueMirrorStatements`'s docstring (#2716). It has
|
|
695
|
+
// its own DOM-compare and consumes no dedup ordinal (#2869).
|
|
613
696
|
const stmts = toHtmlAttrName(prop.attrName) === 'value'
|
|
614
697
|
? emitChildValueMirrorStatements(varName, prop.expression)
|
|
615
|
-
:
|
|
698
|
+
: emitDedupedAttrUpdate(varName, prop.attrName, prop.expression, prop, ordinal++)
|
|
616
699
|
for (const stmt of stmts) {
|
|
617
700
|
lines.push(` ${stmt}`)
|
|
618
701
|
}
|
|
@@ -620,6 +703,6 @@ export function emitReactiveChildProps(lines: string[], ctx: ClientJsContext): v
|
|
|
620
703
|
lines.push(` }`)
|
|
621
704
|
}
|
|
622
705
|
|
|
623
|
-
lines.push(` }${bindingIdArg(ctx, ctx.reactiveChildProps[0]?.slotId ?? undefined)})`)
|
|
706
|
+
lines.push(` }${bindingIdArg(ctx, ctx.reactiveChildProps[0]?.slotId ?? undefined)}) }`)
|
|
624
707
|
}
|
|
625
708
|
}
|
|
@@ -911,7 +911,15 @@ export function irToHtmlTemplate(node: IRNode, restSpreadNames?: ReadonlySet<str
|
|
|
911
911
|
// emit sites (`irToComponentTemplateWithOpts`,
|
|
912
912
|
// `generateCsrTemplateWithOpts`), which already pick the
|
|
913
913
|
// prop-rewritten variant.
|
|
914
|
-
|
|
914
|
+
// Also wrap through `wrapExpr` (#2868 follow-up): this
|
|
915
|
+
// `renderChild(...)` call can itself live inside a loop row's
|
|
916
|
+
// conditional-branch HTML (a child component rendered by
|
|
917
|
+
// `.map()`-row type), so a prop reading the row's own loop
|
|
918
|
+
// param/index must read the live accessor, same as every
|
|
919
|
+
// other expression position `irToHtmlTemplate` renders —
|
|
920
|
+
// previously masked by the post-hoc `wrapLoopParamAsAccessor`
|
|
921
|
+
// pass over the whole branch string, which #2868 removed.
|
|
922
|
+
const expr = wrapExpr(attrValueToString(p.value, { useTemplate: true }) ?? 'undefined')
|
|
915
923
|
return `${quotePropName(p.name)}: ${expr}`
|
|
916
924
|
}
|
|
917
925
|
}
|
|
@@ -38,15 +38,27 @@ import { identifierCallPattern } from '../identifier-pattern.ts'
|
|
|
38
38
|
* `index` field through before this fix (see each call site's own
|
|
39
39
|
* `undefined`/omitted-arg default), so `undefined` here still means "not
|
|
40
40
|
* guarded," not "no index param."
|
|
41
|
+
*
|
|
42
|
+
* `parentScope` (#2861): stacks this loop-row frame on top of an
|
|
43
|
+
* ENCLOSING loop's own scope, rather than always starting from
|
|
44
|
+
* `BindingScope.EMPTY` — for a nested `.map()` inside another `.map()`'s
|
|
45
|
+
* JSX body. `BindingScope.lookup`/`valueBoundNames` search innermost-first
|
|
46
|
+
* across every frame, so a row expression referencing the OUTER loop's
|
|
47
|
+
* item/index (with no dependency on the INNER loop's own item/index or any
|
|
48
|
+
* signal) is still granted a slot and classified reactive, instead of
|
|
49
|
+
* silently returning `'none'` because only the inner-most frame was ever
|
|
50
|
+
* visible. Omitted (or `undefined`) for a loop with no enclosing loop —
|
|
51
|
+
* identical to the pre-#2861 shape.
|
|
41
52
|
*/
|
|
42
53
|
export function buildLoopRowScope(
|
|
43
54
|
loopParam?: string,
|
|
44
55
|
loopParamBindings?: readonly LoopParamBinding[],
|
|
45
56
|
preambleNames?: ReadonlySet<string>,
|
|
46
57
|
loopIndex?: string | null,
|
|
58
|
+
parentScope?: BindingScope,
|
|
47
59
|
): BindingScope | undefined {
|
|
48
60
|
if (!loopParam) return undefined
|
|
49
|
-
return BindingScope.EMPTY.enterLoopRow({
|
|
61
|
+
return (parentScope ?? BindingScope.EMPTY).enterLoopRow({
|
|
50
62
|
param: loopParam,
|
|
51
63
|
paramBindings: loopParamBindings,
|
|
52
64
|
index: loopIndex,
|
|
@@ -274,14 +286,28 @@ function needsEffectWrapperCore(
|
|
|
274
286
|
/**
|
|
275
287
|
* Why a given expression should be treated as reactive inside a loop item.
|
|
276
288
|
*
|
|
277
|
-
* Surfaces the
|
|
289
|
+
* Surfaces the three distinct reasons loop-child collectors care about:
|
|
278
290
|
* - `signal-or-memo-or-prop` — the expression reads a signal getter, a memo,
|
|
279
291
|
* or a prop name (what `needsEffectWrapper` already classifies).
|
|
280
|
-
* - `loop-param` — the expression reads the loop
|
|
281
|
-
* per-item signal accessor at runtime;
|
|
282
|
-
* does not know about loop params.
|
|
292
|
+
* - `loop-param` — the expression reads the loop's item (or a destructured
|
|
293
|
+
* binding of it), which becomes a per-item signal accessor at runtime;
|
|
294
|
+
* the string-level `needsEffectWrapper` does not know about loop params.
|
|
295
|
+
* - `loop-index` (#2861) — the expression reads ONLY the loop's index
|
|
296
|
+
* parameter (`.map((item, i) => ...)`'s `i`), with no item/signal/memo/prop
|
|
297
|
+
* read anywhere else in it (e.g. bare `{i}`, `class={i % 2 === 0 ? …}`).
|
|
298
|
+
* `mapArray`/`mapArrayAnchored` hand `renderItem` an index ACCESSOR
|
|
299
|
+
* exactly like the item accessor (#2859/#2860), and `mapArrayLazy` tracks
|
|
300
|
+
* a row's position on `entry.index` exactly like `entry.item` — but
|
|
301
|
+
* before this classification existed, nothing ever put such an
|
|
302
|
+
* expression in front of that machinery in the first place: Phase 1
|
|
303
|
+
* (`jsx-to-ir.ts`'s `referencesLoopParam`) already grants it a patchable
|
|
304
|
+
* slot via `BindingScope.valueBoundNames()` (item/index/destructure
|
|
305
|
+
* together), while this function — Phase 2 — had no equivalent case for
|
|
306
|
+
* the index alone, so the slot was written once at row-creation time and
|
|
307
|
+
* never revisited.
|
|
283
308
|
*
|
|
284
|
-
* `none` means
|
|
309
|
+
* `none` means none of the three applies and the collector can skip the
|
|
310
|
+
* expression.
|
|
285
311
|
*
|
|
286
312
|
* Consolidates the duplicated reactive-classification check shared by
|
|
287
313
|
* `collectLoopChildReactiveTexts`, `collectLoopChildReactiveAttrs`, and
|
|
@@ -291,37 +317,53 @@ export type ReactivitySource =
|
|
|
291
317
|
| { kind: 'none' }
|
|
292
318
|
| { kind: 'signal-or-memo-or-prop' }
|
|
293
319
|
| { kind: 'loop-param'; param: string }
|
|
320
|
+
| { kind: 'loop-index'; param: string }
|
|
294
321
|
|
|
295
322
|
/**
|
|
296
323
|
* Classify a (constant-expanded) expression as reactive inside a loop item.
|
|
297
|
-
* See `ReactivitySource` for the
|
|
298
|
-
* matching
|
|
299
|
-
* expression also reads a signal — the `kind` is purely
|
|
300
|
-
* collectors only care about `kind !== 'none'`.
|
|
324
|
+
* See `ReactivitySource` for the three reasons we care about. Loop-param
|
|
325
|
+
* and loop-index matching take precedence so one of them is reported even
|
|
326
|
+
* when the expression also reads a signal — the `kind` is purely
|
|
327
|
+
* informational and collectors only care about `kind !== 'none'`.
|
|
328
|
+
*
|
|
329
|
+
* Takes the row's `BindingScope` (#2482) rather than a raw `(loopParam,
|
|
330
|
+
* loopParamBindings)` pair so this reads the EXACT SAME query Phase 1's
|
|
331
|
+
* `referencesLoopParam` reads (`scope.valueBoundNames()` — item/index/
|
|
332
|
+
* destructure together) to decide whether an expression gets a patchable
|
|
333
|
+
* slot at all. `undefined` means no enclosing loop.
|
|
334
|
+
*
|
|
335
|
+
* For destructured `.map()` callbacks (#951), a `'destructure'`-sourced
|
|
336
|
+
* binding is reported as `loop-param` — the pattern text itself (e.g.
|
|
337
|
+
* `{ id, label }`) never word-matches on a bare binding like `id`, so
|
|
338
|
+
* iterating `valueBoundNames()` (rather than testing the raw param text)
|
|
339
|
+
* is what catches a destructured reference at all.
|
|
301
340
|
*
|
|
302
|
-
*
|
|
303
|
-
*
|
|
304
|
-
*
|
|
305
|
-
*
|
|
306
|
-
*
|
|
341
|
+
* Item/destructure bindings are checked before the index so `loop-param`
|
|
342
|
+
* wins when an expression reads both (e.g. `` `${item.id}-${i}` ``) — this
|
|
343
|
+
* only affects which `kind` is reported, since every caller of this
|
|
344
|
+
* function besides the two `ReactivitySource`-typed union members simply
|
|
345
|
+
* tests `.kind !== 'none'`.
|
|
307
346
|
*/
|
|
308
347
|
export function classifyReactivity(
|
|
309
348
|
expr: string,
|
|
310
349
|
ctx: ClientJsContext,
|
|
311
|
-
|
|
312
|
-
loopParamBindings?: readonly LoopParamBinding[],
|
|
350
|
+
scope: BindingScope | undefined,
|
|
313
351
|
freeIdentifiers?: ReadonlySet<string>,
|
|
314
352
|
): ReactivitySource {
|
|
315
353
|
const has = (name: string): boolean =>
|
|
316
354
|
freeIdentifiers ? freeIdentifiers.has(name) : tokenContainsIdent(expr, name)
|
|
317
|
-
if (
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
355
|
+
if (scope) {
|
|
356
|
+
let indexHit: string | undefined
|
|
357
|
+
for (const name of scope.valueBoundNames()) {
|
|
358
|
+
if (!has(name)) continue
|
|
359
|
+
const source = scope.lookup(name)?.binding.source
|
|
360
|
+
if (source === 'index') {
|
|
361
|
+
indexHit ??= name
|
|
362
|
+
continue
|
|
321
363
|
}
|
|
364
|
+
return { kind: 'loop-param', param: name }
|
|
322
365
|
}
|
|
323
|
-
|
|
324
|
-
return { kind: 'loop-param', param: loopParam }
|
|
366
|
+
if (indexHit) return { kind: 'loop-index', param: indexHit }
|
|
325
367
|
}
|
|
326
368
|
if (needsEffectWrapper(expr, ctx, freeIdentifiers)) {
|
|
327
369
|
return { kind: 'signal-or-memo-or-prop' }
|
|
@@ -666,9 +708,11 @@ export function collectLoopChildReactiveTexts(
|
|
|
666
708
|
stopAtReactiveConditionals = false,
|
|
667
709
|
preambleNames?: ReadonlySet<string>,
|
|
668
710
|
loopIndex?: string | null,
|
|
711
|
+
/** Enclosing loop's own scope, for a NESTED loop (#2861) — see `buildLoopRowScope`. */
|
|
712
|
+
parentScope?: BindingScope,
|
|
669
713
|
): LoopChildReactiveText[] {
|
|
670
714
|
const texts: LoopChildReactiveText[] = []
|
|
671
|
-
const scope = buildLoopRowScope(loopParam, loopParamBindings, preambleNames, loopIndex)
|
|
715
|
+
const scope = buildLoopRowScope(loopParam, loopParamBindings, preambleNames, loopIndex, parentScope)
|
|
672
716
|
walkIR(node, false, {
|
|
673
717
|
// Skip loop/async/if-statement subtrees — the original walker omitted
|
|
674
718
|
// them; they have their own scopes (inner-loop reconciliation, async
|
|
@@ -696,7 +740,7 @@ export function collectLoopChildReactiveTexts(
|
|
|
696
740
|
// `classifyReactivity` can't see through) still gets an update
|
|
697
741
|
// effect instead of silently freezing at its SSR value (#2282).
|
|
698
742
|
const reactive =
|
|
699
|
-
classifyReactivity(expanded.expr, ctx,
|
|
743
|
+
classifyReactivity(expanded.expr, ctx, scope, expanded.freeIds).kind !== 'none'
|
|
700
744
|
|| decideWrapFromAstFlags(n).wrap
|
|
701
745
|
if (!reactive) return
|
|
702
746
|
texts.push({
|
|
@@ -746,9 +790,11 @@ export function collectLoopChildReactiveAttrs(
|
|
|
746
790
|
stopAtReactiveConditionals = false,
|
|
747
791
|
preambleNames?: ReadonlySet<string>,
|
|
748
792
|
loopIndex?: string | null,
|
|
793
|
+
/** Enclosing loop's own scope, for a NESTED loop (#2861) — see `buildLoopRowScope`. */
|
|
794
|
+
parentScope?: BindingScope,
|
|
749
795
|
): LoopChildReactiveAttr[] {
|
|
750
796
|
const attrs: LoopChildReactiveAttr[] = []
|
|
751
|
-
const scope = buildLoopRowScope(loopParam, loopParamBindings, preambleNames, loopIndex)
|
|
797
|
+
const scope = buildLoopRowScope(loopParam, loopParamBindings, preambleNames, loopIndex, parentScope)
|
|
752
798
|
traverseElements(node, (el) => {
|
|
753
799
|
if (el.slotId) {
|
|
754
800
|
for (const attr of el.attrs) {
|
|
@@ -799,7 +845,7 @@ export function collectLoopChildReactiveAttrs(
|
|
|
799
845
|
preambleNames.size > 0 &&
|
|
800
846
|
anyNameIn(expanded.freeIds ?? extractFreeIdentifiersFromText(expanded.expr), preambleNames)
|
|
801
847
|
const reactive =
|
|
802
|
-
classifyReactivity(expanded.expr, ctx,
|
|
848
|
+
classifyReactivity(expanded.expr, ctx, scope, expanded.freeIds).kind !== 'none'
|
|
803
849
|
|| readsPreamble
|
|
804
850
|
|| attr.callsReactiveGetters
|
|
805
851
|
|| attr.hasFunctionCalls
|