@barefootjs/jsx 0.34.0 → 0.35.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/analyzer.d.ts.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +344 -215
- package/dist/ir-to-client-js/collect-elements.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/plan/build-branch-loop.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/plan/build-component-loop.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/plan/build-composite-loop.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/plan/build-lazy-row.d.ts +11 -0
- 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.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/plan/build-plain-row.d.ts +52 -0
- package/dist/ir-to-client-js/control-flow/plan/build-plain-row.d.ts.map +1 -0
- package/dist/ir-to-client-js/control-flow/plan/build-reactive-effects.d.ts +2 -0
- package/dist/ir-to-client-js/control-flow/plan/build-reactive-effects.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/plan/lazy-conditional.d.ts +6 -7
- package/dist/ir-to-client-js/control-flow/plan/lazy-conditional.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/plan/lazy-preamble.d.ts +5 -7
- package/dist/ir-to-client-js/control-flow/plan/lazy-preamble.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/plan/lazy-row-eligibility.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/plan/loop.d.ts +10 -6
- 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 +11 -9
- package/dist/ir-to-client-js/control-flow/shared.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/stringify/composite-loop.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/stringify/lazy-row.d.ts.map +1 -1
- package/dist/ir-to-client-js/csr-substitute.d.ts +16 -0
- package/dist/ir-to-client-js/csr-substitute.d.ts.map +1 -1
- package/dist/ir-to-client-js/emit-reactive.d.ts.map +1 -1
- package/dist/ir-to-client-js/emit-registration.d.ts.map +1 -1
- package/dist/ir-to-client-js/html-template.d.ts +2 -2
- 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/index.d.ts.map +1 -1
- package/dist/ir-to-client-js/markup-slots.d.ts +21 -0
- package/dist/ir-to-client-js/markup-slots.d.ts.map +1 -0
- package/dist/ir-to-client-js/safe-html.d.ts +158 -0
- package/dist/ir-to-client-js/safe-html.d.ts.map +1 -0
- package/dist/ir-to-client-js/utils.d.ts +34 -1
- package/dist/ir-to-client-js/utils.d.ts.map +1 -1
- package/dist/jsx-to-ir.d.ts.map +1 -1
- package/dist/prop-rewrite.d.ts +8 -3
- package/dist/prop-rewrite.d.ts.map +1 -1
- package/dist/props-binding.d.ts +22 -1
- package/dist/props-binding.d.ts.map +1 -1
- package/dist/ssr-defaults.d.ts.map +1 -1
- package/dist/types.d.ts +5 -10
- package/dist/types.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/__tests__/__snapshots__/doc-examples.test.ts.snap +10 -12
- package/src/__tests__/build-plain-row-consolidation.test.ts +120 -0
- package/src/__tests__/client-template-escape-soundness.test.ts +538 -0
- package/src/__tests__/csr-template-loop-shadowing.test.ts +4 -2
- package/src/__tests__/flatmap-segments.test.ts +1 -1
- package/src/__tests__/lazy-conditional.test.ts +52 -12
- package/src/__tests__/lazy-preamble.test.ts +11 -11
- package/src/__tests__/lazy-row-eligibility.test.ts +8 -3
- package/src/__tests__/loop-child-reactive-attr-const-shadow.test.ts +25 -3
- package/src/__tests__/loop-item-conditional-codegen.test.ts +4 -2
- package/src/__tests__/map-arbitrary-body.test.ts +1 -1
- package/src/__tests__/nested-loop-index-param.test.ts +14 -3
- package/src/__tests__/nested-loop-reactive-attrs.test.ts +4 -2
- package/src/__tests__/reactive-attrs-in-map.test.ts +13 -10
- package/src/__tests__/rewrite-destructured-props.test.ts +37 -0
- package/src/__tests__/safe-html-splice-door.test.ts +61 -0
- package/src/__tests__/ssr-defaults.test.ts +59 -0
- package/src/analyzer.ts +4 -0
- package/src/index.ts +8 -2
- package/src/ir-to-client-js/collect-elements.ts +18 -2
- package/src/ir-to-client-js/control-flow/plan/build-branch-loop.ts +20 -28
- package/src/ir-to-client-js/control-flow/plan/build-component-loop.ts +9 -7
- package/src/ir-to-client-js/control-flow/plan/build-composite-loop.ts +10 -6
- package/src/ir-to-client-js/control-flow/plan/build-inner-loop.ts +8 -8
- package/src/ir-to-client-js/control-flow/plan/build-lazy-row.ts +18 -2
- package/src/ir-to-client-js/control-flow/plan/build-loop-child-arm.ts +3 -3
- package/src/ir-to-client-js/control-flow/plan/build-loop.ts +35 -35
- package/src/ir-to-client-js/control-flow/plan/build-plain-row.ts +110 -0
- package/src/ir-to-client-js/control-flow/plan/build-reactive-effects.ts +5 -2
- package/src/ir-to-client-js/control-flow/plan/lazy-conditional.ts +11 -10
- package/src/ir-to-client-js/control-flow/plan/lazy-preamble.ts +11 -13
- package/src/ir-to-client-js/control-flow/plan/lazy-row-eligibility.ts +13 -3
- package/src/ir-to-client-js/control-flow/plan/loop.ts +10 -6
- package/src/ir-to-client-js/control-flow/shared.ts +40 -17
- package/src/ir-to-client-js/control-flow/stringify/composite-loop.ts +2 -1
- package/src/ir-to-client-js/control-flow/stringify/lazy-row.ts +10 -0
- package/src/ir-to-client-js/csr-substitute.ts +29 -0
- package/src/ir-to-client-js/emit-reactive.ts +4 -1
- package/src/ir-to-client-js/emit-registration.ts +2 -4
- package/src/ir-to-client-js/html-template.ts +58 -244
- package/src/ir-to-client-js/imports.ts +3 -0
- package/src/ir-to-client-js/index.ts +4 -4
- package/src/ir-to-client-js/markup-slots.ts +25 -0
- package/src/ir-to-client-js/safe-html.ts +243 -0
- package/src/ir-to-client-js/utils.ts +58 -10
- package/src/jsx-to-ir.ts +115 -20
- package/src/prop-rewrite.ts +8 -4
- package/src/props-binding.ts +37 -1
- package/src/ssr-defaults.ts +45 -0
- package/src/types.ts +5 -10
- package/src/__tests__/text-slot-escaping.test.ts +0 -65
|
@@ -11,14 +11,13 @@
|
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
13
|
import type { BranchLoop } from '../../types.ts'
|
|
14
|
-
import { buildChainedArrayExpr, varSlotId
|
|
14
|
+
import { buildChainedArrayExpr, varSlotId } from '../../utils.ts'
|
|
15
15
|
import { buildBranchCompositePlan } from './build-composite-loop.ts'
|
|
16
16
|
import { buildBranchLoopDelegationPlan } from './build-event-delegation.ts'
|
|
17
17
|
import { buildReactiveEffectsPlan } from './build-reactive-effects.ts'
|
|
18
|
-
import {
|
|
18
|
+
import { buildPlainRowCore } from './build-plain-row.ts'
|
|
19
19
|
import type { LazyRowScopeInfo } from './lazy-row-eligibility.ts'
|
|
20
|
-
import {
|
|
21
|
-
import { renderPreamble, irToHtmlTemplate } from '../../html-template.ts'
|
|
20
|
+
import { loopKeyFn, buildChildRefBindings } from '../shared.ts'
|
|
22
21
|
import type {
|
|
23
22
|
BranchCompositeLoopPlan,
|
|
24
23
|
BranchLoopPlan,
|
|
@@ -44,7 +43,6 @@ export function buildBranchLoopPlan(
|
|
|
44
43
|
return composite
|
|
45
44
|
}
|
|
46
45
|
|
|
47
|
-
const { head: paramHead, unwrap: paramUnwrap } = destructureLoopParam(loop.param, loop.paramBindings)
|
|
48
46
|
const hasReactiveEffects = loop.bindings.reactiveAttrs.length > 0
|
|
49
47
|
|| loop.bindings.reactiveTexts.length > 0
|
|
50
48
|
|| loop.bindings.conditionals.length > 0
|
|
@@ -54,14 +52,18 @@ export function buildBranchLoopPlan(
|
|
|
54
52
|
const arrayExpr = fm
|
|
55
53
|
? `(${buildChainedArrayExpr(loop)}).flatMap(${fm.params} => ${fm.body})`
|
|
56
54
|
: buildChainedArrayExpr(loop)
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
: ''
|
|
64
|
-
|
|
55
|
+
|
|
56
|
+
// Shared "wrap item → decide lazy → wrap index" core (#2859 follow-up) —
|
|
57
|
+
// see `build-plain-row.ts`'s module docstring for the full rationale.
|
|
58
|
+
const core = buildPlainRowCore({
|
|
59
|
+
loop,
|
|
60
|
+
arrayExpr,
|
|
61
|
+
callSite: 'branch-plain',
|
|
62
|
+
flatMapLeafItem: Boolean(fm),
|
|
63
|
+
anchored: false,
|
|
64
|
+
scope: lazyScope,
|
|
65
|
+
})
|
|
66
|
+
const { paramHead, paramUnwrap, indexParam, preambleRegions, lazyRow } = core
|
|
65
67
|
const plan: BranchPlainLoopPlan = {
|
|
66
68
|
kind: 'plain',
|
|
67
69
|
rowConstruction: 'string-template',
|
|
@@ -78,21 +80,10 @@ export function buildBranchLoopPlan(
|
|
|
78
80
|
indexParam,
|
|
79
81
|
// Wrap loop-param references to signal-accessor form so the preamble
|
|
80
82
|
// matches the template literal's already-wrapped reads (#1065).
|
|
81
|
-
mapPreambleWrapped,
|
|
83
|
+
mapPreambleWrapped: core.mapPreambleWrapped,
|
|
82
84
|
// Lazy row graph (§9, L3) — undefined for every ineligible loop.
|
|
83
|
-
lazyRow
|
|
84
|
-
|
|
85
|
-
arrayExpr,
|
|
86
|
-
indexParam,
|
|
87
|
-
paramUnwrap,
|
|
88
|
-
mapPreambleWrapped,
|
|
89
|
-
preambleRegionCount: preambleRegions.length,
|
|
90
|
-
callSite: 'branch-plain',
|
|
91
|
-
flatMapLeafItem: Boolean(fm),
|
|
92
|
-
anchored: false,
|
|
93
|
-
scope: lazyScope,
|
|
94
|
-
}) ?? undefined,
|
|
95
|
-
template: loop.template,
|
|
85
|
+
lazyRow,
|
|
86
|
+
template: core.template,
|
|
96
87
|
reactiveEffects: hasReactiveEffects
|
|
97
88
|
? buildReactiveEffectsPlan({
|
|
98
89
|
attrs: loop.bindings.reactiveAttrs,
|
|
@@ -100,11 +91,12 @@ export function buildBranchLoopPlan(
|
|
|
100
91
|
conditionals: loop.bindings.conditionals,
|
|
101
92
|
loopParam: loop.param,
|
|
102
93
|
loopParamBindings: loop.paramBindings,
|
|
94
|
+
loopIndex: loop.index,
|
|
103
95
|
profileComponentName,
|
|
104
96
|
})
|
|
105
97
|
: null,
|
|
106
98
|
eventDelegation: buildBranchLoopDelegationPlan(loop, cv, profileComponentName),
|
|
107
|
-
childRefs: buildChildRefBindings(loop.bindings.refs, loop.param, loop.paramBindings),
|
|
99
|
+
childRefs: buildChildRefBindings(loop.bindings.refs, loop.param, loop.paramBindings, loop.index),
|
|
108
100
|
preambleRegions,
|
|
109
101
|
bodyIsMultiRoot: loop.bodyIsMultiRoot ?? false,
|
|
110
102
|
profileLoopId: profileComponentName ? `${profileComponentName}#binding:${containerSlotId}` : undefined,
|
|
@@ -34,8 +34,8 @@ import { internalInvariant } from '../../../errors.ts'
|
|
|
34
34
|
/** @internal — prefer `buildLoopPlan`. */
|
|
35
35
|
export function buildComponentLoopPlan(elem: TopLevelLoop, profileComponentName?: string): ComponentLoopPlan {
|
|
36
36
|
const { name } = elem.childComponent!
|
|
37
|
-
const propsExpr = buildComponentPropsExpr(elem.childComponent!, elem.param)
|
|
38
|
-
const keyExpr = wrapLoopParamAsAccessor(elem.key || '__idx', elem.param, elem.paramBindings)
|
|
37
|
+
const propsExpr = buildComponentPropsExpr(elem.childComponent!, elem.param, undefined, elem.index)
|
|
38
|
+
const keyExpr = wrapLoopParamAsAccessor(elem.key || '__idx', elem.param, elem.paramBindings, elem.index)
|
|
39
39
|
const { head: paramHead, unwrap: paramUnwrap } = destructureLoopParam(elem.param, elem.paramBindings)
|
|
40
40
|
|
|
41
41
|
// A component-root loop's preamble is JS-only by construction: Phase 1
|
|
@@ -47,7 +47,7 @@ export function buildComponentLoopPlan(elem: TopLevelLoop, profileComponentName?
|
|
|
47
47
|
// that let a JSX-bearing preamble through is missing, not this line.
|
|
48
48
|
const mapPreambleWrapped = elem.preamble
|
|
49
49
|
? renderPreamble(elem.preamble, {
|
|
50
|
-
transformJs: text => wrapLoopParamAsAccessor(text, elem.param, elem.paramBindings),
|
|
50
|
+
transformJs: text => wrapLoopParamAsAccessor(text, elem.param, elem.paramBindings, elem.index),
|
|
51
51
|
renderLeaf: () => {
|
|
52
52
|
internalInvariant(false, 'component-root loop received a JSX-bearing preamble — Phase 1 should have refused it')
|
|
53
53
|
},
|
|
@@ -62,13 +62,14 @@ export function buildComponentLoopPlan(elem: TopLevelLoop, profileComponentName?
|
|
|
62
62
|
: false
|
|
63
63
|
const rawChildrenExpr = isTextOnly ? irChildrenToJsExpr(comp.children!) : null
|
|
64
64
|
const childrenFreeIds = isTextOnly && comp.children ? irChildrenFreeIds(comp.children) : undefined
|
|
65
|
-
const childrenRefsLoop = rawChildrenExpr != null && childrenFreeIds != null
|
|
65
|
+
const childrenRefsLoop = rawChildrenExpr != null && childrenFreeIds != null
|
|
66
|
+
&& (childrenFreeIds.has(elem.param) || (!!elem.index && childrenFreeIds.has(elem.index)))
|
|
66
67
|
return {
|
|
67
68
|
componentName: comp.name,
|
|
68
69
|
selector: buildCompSelector(comp),
|
|
69
|
-
propsExpr: buildComponentPropsExpr(comp, elem.param),
|
|
70
|
+
propsExpr: buildComponentPropsExpr(comp, elem.param, undefined, elem.index),
|
|
70
71
|
childrenTextEffect: childrenRefsLoop
|
|
71
|
-
? { wrappedChildren: wrapLoopParamAsAccessor(rawChildrenExpr!, elem.param, elem.paramBindings) }
|
|
72
|
+
? { wrappedChildren: wrapLoopParamAsAccessor(rawChildrenExpr!, elem.param, elem.paramBindings, elem.index) }
|
|
72
73
|
: null,
|
|
73
74
|
}
|
|
74
75
|
})
|
|
@@ -98,7 +99,7 @@ export function buildComponentLoopPlan(elem: TopLevelLoop, profileComponentName?
|
|
|
98
99
|
// the per-item factory has no `__el` handle to invoke. Still required
|
|
99
100
|
// by the type so the structural invariant (every variant has a
|
|
100
101
|
// `childRefs`) is preserved; populated as empty.
|
|
101
|
-
childRefs: buildChildRefBindings(elem.bindings.refs, elem.param, elem.paramBindings),
|
|
102
|
+
childRefs: buildChildRefBindings(elem.bindings.refs, elem.param, elem.paramBindings, elem.index),
|
|
102
103
|
profileLoopId: profileComponentName ? `${profileComponentName}#binding:${elem.slotId}` : undefined,
|
|
103
104
|
childConditionalEffects: hasChildConds
|
|
104
105
|
? buildReactiveEffectsPlan({
|
|
@@ -107,6 +108,7 @@ export function buildComponentLoopPlan(elem: TopLevelLoop, profileComponentName?
|
|
|
107
108
|
conditionals: elem.bindings.conditionals,
|
|
108
109
|
loopParam: elem.param,
|
|
109
110
|
loopParamBindings: elem.paramBindings,
|
|
111
|
+
loopIndex: elem.index,
|
|
110
112
|
profileComponentName,
|
|
111
113
|
})
|
|
112
114
|
: null,
|
|
@@ -31,7 +31,7 @@ export function buildTopLevelCompositePlan(elem: TopLevelLoop, profileComponentN
|
|
|
31
31
|
const nestedComps = elem.nestedComponents!
|
|
32
32
|
const depthLevels = buildDepthLevels(elem.innerLoops ?? [], nestedComps, elem.bindings.events)
|
|
33
33
|
const { head: paramHead, unwrap: paramUnwrap } = destructureLoopParam(elem.param, elem.paramBindings)
|
|
34
|
-
const wrap = (expr: string) => wrapLoopParamAsAccessor(expr, elem.param, elem.paramBindings)
|
|
34
|
+
const wrap = (expr: string) => wrapLoopParamAsAccessor(expr, elem.param, elem.paramBindings, elem.index)
|
|
35
35
|
|
|
36
36
|
const outerCompsByDepth = nestedComps.filter(c => !c.loopDepth || c.loopDepth === 0)
|
|
37
37
|
|
|
@@ -48,13 +48,13 @@ export function buildTopLevelCompositePlan(elem: TopLevelLoop, profileComponentN
|
|
|
48
48
|
mapPreambleWrapped: elem.preamble
|
|
49
49
|
? renderPreamble(elem.preamble, {
|
|
50
50
|
transformJs: wrap,
|
|
51
|
-
renderLeaf: (ir) => irToHtmlTemplate(ir, undefined, 1, [{ param: elem.param, bindings: elem.paramBindings }], undefined),
|
|
51
|
+
renderLeaf: (ir) => irToHtmlTemplate(ir, undefined, 1, [{ param: elem.param, bindings: elem.paramBindings, index: elem.index }], undefined),
|
|
52
52
|
})
|
|
53
53
|
: '',
|
|
54
54
|
template: elem.template,
|
|
55
55
|
outerComps: filterCondCompsOut(outerCompsByDepth, elem.bindings.conditionals),
|
|
56
56
|
outerEvents: elem.bindings.events.filter(ev => ev.nestedLoops.length === 0),
|
|
57
|
-
childRefs: buildChildRefBindings(elem.bindings.refs, elem.param, elem.paramBindings),
|
|
57
|
+
childRefs: buildChildRefBindings(elem.bindings.refs, elem.param, elem.paramBindings, elem.index),
|
|
58
58
|
innerLoops: buildInnerLoopsPlan({
|
|
59
59
|
levels: depthLevels,
|
|
60
60
|
parentElVar: '__el',
|
|
@@ -63,6 +63,7 @@ export function buildTopLevelCompositePlan(elem: TopLevelLoop, profileComponentN
|
|
|
63
63
|
}),
|
|
64
64
|
loopParam: elem.param,
|
|
65
65
|
loopParamBindings: elem.paramBindings,
|
|
66
|
+
loopIndex: elem.index,
|
|
66
67
|
reactiveEffects: hasReactive(elem)
|
|
67
68
|
? buildReactiveEffectsPlan({
|
|
68
69
|
attrs: elem.bindings.reactiveAttrs,
|
|
@@ -70,6 +71,7 @@ export function buildTopLevelCompositePlan(elem: TopLevelLoop, profileComponentN
|
|
|
70
71
|
conditionals: elem.bindings.conditionals,
|
|
71
72
|
loopParam: elem.param,
|
|
72
73
|
loopParamBindings: elem.paramBindings,
|
|
74
|
+
loopIndex: elem.index,
|
|
73
75
|
profileComponentName,
|
|
74
76
|
})
|
|
75
77
|
: null,
|
|
@@ -88,7 +90,7 @@ export function buildBranchCompositePlan(loop: BranchLoop, cv: string, profileCo
|
|
|
88
90
|
const childEvents = loop.bindings.events
|
|
89
91
|
const depthLevels = buildDepthLevels(innerLoops, nestedComps, childEvents)
|
|
90
92
|
const { head: paramHead, unwrap: paramUnwrap } = destructureLoopParam(loop.param, loop.paramBindings)
|
|
91
|
-
const wrap = (expr: string) => wrapLoopParamAsAccessor(expr, loop.param, loop.paramBindings)
|
|
93
|
+
const wrap = (expr: string) => wrapLoopParamAsAccessor(expr, loop.param, loop.paramBindings, loop.index)
|
|
92
94
|
|
|
93
95
|
const outerCompsByDepth = nestedComps.filter(c => !c.loopDepth || c.loopDepth === 0)
|
|
94
96
|
|
|
@@ -108,13 +110,13 @@ export function buildBranchCompositePlan(loop: BranchLoop, cv: string, profileCo
|
|
|
108
110
|
mapPreambleWrapped: loop.preamble
|
|
109
111
|
? renderPreamble(loop.preamble, {
|
|
110
112
|
transformJs: wrap,
|
|
111
|
-
renderLeaf: (ir) => irToHtmlTemplate(ir, undefined, 1, [{ param: loop.param, bindings: loop.paramBindings }], undefined),
|
|
113
|
+
renderLeaf: (ir) => irToHtmlTemplate(ir, undefined, 1, [{ param: loop.param, bindings: loop.paramBindings, index: loop.index }], undefined),
|
|
112
114
|
})
|
|
113
115
|
: '',
|
|
114
116
|
template: loop.template,
|
|
115
117
|
outerComps: filterCondCompsOut(outerCompsByDepth, loop.bindings.conditionals),
|
|
116
118
|
outerEvents: childEvents.filter(ev => ev.nestedLoops.length === 0),
|
|
117
|
-
childRefs: buildChildRefBindings(loop.bindings.refs, loop.param, loop.paramBindings),
|
|
119
|
+
childRefs: buildChildRefBindings(loop.bindings.refs, loop.param, loop.paramBindings, loop.index),
|
|
118
120
|
innerLoops: buildInnerLoopsPlan({
|
|
119
121
|
levels: depthLevels,
|
|
120
122
|
parentElVar: '__el',
|
|
@@ -123,6 +125,7 @@ export function buildBranchCompositePlan(loop: BranchLoop, cv: string, profileCo
|
|
|
123
125
|
}),
|
|
124
126
|
loopParam: loop.param,
|
|
125
127
|
loopParamBindings: loop.paramBindings,
|
|
128
|
+
loopIndex: loop.index,
|
|
126
129
|
reactiveEffects: hasReactiveBranch(loop)
|
|
127
130
|
? buildReactiveEffectsPlan({
|
|
128
131
|
attrs: loop.bindings.reactiveAttrs,
|
|
@@ -130,6 +133,7 @@ export function buildBranchCompositePlan(loop: BranchLoop, cv: string, profileCo
|
|
|
130
133
|
conditionals: loop.bindings.conditionals,
|
|
131
134
|
loopParam: loop.param,
|
|
132
135
|
loopParamBindings: loop.paramBindings,
|
|
136
|
+
loopIndex: loop.index,
|
|
133
137
|
profileComponentName,
|
|
134
138
|
})
|
|
135
139
|
: null,
|
|
@@ -161,11 +161,11 @@ function buildReactiveEmit(
|
|
|
161
161
|
outerLoopParam?: string,
|
|
162
162
|
outerLoopParamBindings?: readonly LoopParamBinding[],
|
|
163
163
|
): InnerLoopReactiveEmit {
|
|
164
|
-
const wrapInner = (expr: string) => wrapLoopParamAsAccessor(expr, inner.param, inner.paramBindings)
|
|
165
|
-
const wrapBoth = (expr: string) => wrapLoopParamAsAccessor(wrapOuter(expr), inner.param, inner.paramBindings)
|
|
164
|
+
const wrapInner = (expr: string) => wrapLoopParamAsAccessor(expr, inner.param, inner.paramBindings, inner.index)
|
|
165
|
+
const wrapBoth = (expr: string) => wrapLoopParamAsAccessor(wrapOuter(expr), inner.param, inner.paramBindings, inner.index)
|
|
166
166
|
const { head: paramHead, unwrap: paramUnwrap } = destructureLoopParam(inner.param, inner.paramBindings)
|
|
167
167
|
const wrappedKey = inner.key
|
|
168
|
-
? wrapLoopParamAsAccessor(inner.key, inner.param, inner.paramBindings)
|
|
168
|
+
? wrapLoopParamAsAccessor(inner.key, inner.param, inner.paramBindings, inner.index)
|
|
169
169
|
: null
|
|
170
170
|
|
|
171
171
|
// Inner-wrap children IR recursively so nested component props (e.g.,
|
|
@@ -202,12 +202,12 @@ function buildReactiveEmit(
|
|
|
202
202
|
|
|
203
203
|
const reactiveTexts: InnerLoopText[] = inner.bindings.reactiveTexts.map(text => ({
|
|
204
204
|
slotId: text.slotId,
|
|
205
|
-
wrappedExpression: wrapLoopParamAsAccessor(wrapOuter(text.expression), inner.param, inner.paramBindings),
|
|
205
|
+
wrappedExpression: wrapLoopParamAsAccessor(wrapOuter(text.expression), inner.param, inner.paramBindings, inner.index),
|
|
206
206
|
insideConditional: !!text.insideConditional,
|
|
207
207
|
}))
|
|
208
208
|
|
|
209
209
|
const reactiveAttrs: InnerLoopReactiveAttr[] = inner.bindings.reactiveAttrs.map(attr => {
|
|
210
|
-
const wrapped = wrapLoopParamAsAccessor(wrapOuter(attr.expression), inner.param, inner.paramBindings)
|
|
210
|
+
const wrapped = wrapLoopParamAsAccessor(wrapOuter(attr.expression), inner.param, inner.paramBindings, inner.index)
|
|
211
211
|
return {
|
|
212
212
|
slotId: attr.childSlotId,
|
|
213
213
|
attrName: attr.attrName,
|
|
@@ -241,16 +241,16 @@ function buildReactiveEmit(
|
|
|
241
241
|
const leafLoopParams = outerLoopParam
|
|
242
242
|
? [
|
|
243
243
|
{ param: outerLoopParam, bindings: outerLoopParamBindings },
|
|
244
|
-
{ param: inner.param, bindings: inner.paramBindings },
|
|
244
|
+
{ param: inner.param, bindings: inner.paramBindings, index: inner.index },
|
|
245
245
|
]
|
|
246
|
-
: [{ param: inner.param, bindings: inner.paramBindings }]
|
|
246
|
+
: [{ param: inner.param, bindings: inner.paramBindings, index: inner.index }]
|
|
247
247
|
preludeStatements.push(renderPreamble(inner.preamble, {
|
|
248
248
|
transformJs: (t) => wrapInner(wrapOuter(t)),
|
|
249
249
|
renderLeaf: (ir) => irToHtmlTemplate(ir, undefined, 1, leafLoopParams, undefined),
|
|
250
250
|
}))
|
|
251
251
|
}
|
|
252
252
|
|
|
253
|
-
const childRefs = buildChildRefBindings(inner.bindings.refs, inner.param, inner.paramBindings)
|
|
253
|
+
const childRefs = buildChildRefBindings(inner.bindings.refs, inner.param, inner.paramBindings, inner.index)
|
|
254
254
|
|
|
255
255
|
// Per-item conditionals inside THIS loop's own row (#2706) — same
|
|
256
256
|
// insert()-parity treatment the top-level loop's row conditionals
|
|
@@ -156,6 +156,17 @@ export interface LazyRowPlanData {
|
|
|
156
156
|
* both parsed once per loop and clones on a flip.
|
|
157
157
|
*/
|
|
158
158
|
conditionals: readonly LazyRowConditionalBinding[]
|
|
159
|
+
/**
|
|
160
|
+
* At least one binding, condition, or preamble-substituted dependency reads
|
|
161
|
+
* the loop's index parameter (#2859 follow-up — `ClassifiedLazyBinding.
|
|
162
|
+
* referencesIndex`, which also forces `readsItem: true` for that binding).
|
|
163
|
+
* The stringifier binds `const <indexParam> = __e.index` at the top of
|
|
164
|
+
* `applyItem` and inside `applyOuter`'s per-entry loop only when this is
|
|
165
|
+
* true, and emits `indexDriven: true` on the plan object literal so the
|
|
166
|
+
* runtime (`mapArrayLazy`) also calls `applyItem` on a pure reorder — a
|
|
167
|
+
* position change with no item change — not just an item change.
|
|
168
|
+
*/
|
|
169
|
+
readsIndex: boolean
|
|
159
170
|
}
|
|
160
171
|
|
|
161
172
|
export interface BuildLazyRowArgs {
|
|
@@ -198,7 +209,7 @@ export function decideLazyRow(args: BuildLazyRowArgs): {
|
|
|
198
209
|
// bodies BEFORE classifying, because a binding that reads a declared local
|
|
199
210
|
// inherits the preamble's dependencies rather than its own literal names.
|
|
200
211
|
const primableNames = new Set<string>([...scope.signals.keys(), ...scope.memos])
|
|
201
|
-
const preambleAnalysis = analyzeLazyPreamble(loop.preamble,
|
|
212
|
+
const preambleAnalysis = analyzeLazyPreamble(loop.preamble, primableNames)
|
|
202
213
|
|
|
203
214
|
// §9.5 conditional widening: a row conditional whose arms are wiring-free
|
|
204
215
|
// static elements is driven from the apply bodies instead of a per-row
|
|
@@ -207,7 +218,7 @@ export function decideLazyRow(args: BuildLazyRowArgs): {
|
|
|
207
218
|
const condFacts: LazyConditionalFacts[] = []
|
|
208
219
|
let conditionalRefusal: string | null = null
|
|
209
220
|
for (const cond of rawConditionals) {
|
|
210
|
-
const verdict = analyzeLazyConditional(cond,
|
|
221
|
+
const verdict = analyzeLazyConditional(cond, {
|
|
211
222
|
whenTrueHtml: addCondAttrToTemplate(wrap(cond.whenTrueHtml), cond.slotId),
|
|
212
223
|
whenFalseHtml: addCondAttrToTemplate(wrap(cond.whenFalseHtml), cond.slotId),
|
|
213
224
|
})
|
|
@@ -374,6 +385,11 @@ export function decideLazyRow(args: BuildLazyRowArgs): {
|
|
|
374
385
|
itemNeedsPreamble: [...attrs, ...texts, ...conditionals].some(b => b.readsItem && b.readsPreamble),
|
|
375
386
|
outerNeedsPreamble: [...attrs, ...texts, ...conditionals].some(b => b.readsOuter && b.readsPreamble),
|
|
376
387
|
conditionals,
|
|
388
|
+
// From the RAW classification, not the final lists: a binding that
|
|
389
|
+
// reads the index is always `readsItem` (see `classifyLazyBinding`),
|
|
390
|
+
// so checking the final lists would say the same thing — raw is just
|
|
391
|
+
// the more direct source of truth.
|
|
392
|
+
readsIndex: classified.some(c => c.referencesIndex),
|
|
377
393
|
},
|
|
378
394
|
decision,
|
|
379
395
|
}
|
|
@@ -234,8 +234,8 @@ export function buildBranchInnerLoopsPlan(
|
|
|
234
234
|
const inner = innerLoops[i]
|
|
235
235
|
if (!inner.refsOuterParam || !inner.template) continue
|
|
236
236
|
|
|
237
|
-
const wrapInner = (expr: string) => wrapLoopParamAsAccessor(expr, inner.param, inner.paramBindings)
|
|
238
|
-
const wrapBoth = (expr: string) => wrapLoopParamAsAccessor(wrapOuter(expr), inner.param, inner.paramBindings)
|
|
237
|
+
const wrapInner = (expr: string) => wrapLoopParamAsAccessor(expr, inner.param, inner.paramBindings, inner.index)
|
|
238
|
+
const wrapBoth = (expr: string) => wrapLoopParamAsAccessor(wrapOuter(expr), inner.param, inner.paramBindings, inner.index)
|
|
239
239
|
|
|
240
240
|
const csl = inner.containerSlotId
|
|
241
241
|
// Inner loop's container: host-side `bf="<slot>"` slot marker first,
|
|
@@ -251,7 +251,7 @@ export function buildBranchInnerLoopsPlan(
|
|
|
251
251
|
|
|
252
252
|
const { head: paramHead, unwrap: paramUnwrap } = destructureLoopParam(inner.param, inner.paramBindings)
|
|
253
253
|
const wrappedKey = inner.key
|
|
254
|
-
? wrapLoopParamAsAccessor(inner.key, inner.param, inner.paramBindings)
|
|
254
|
+
? wrapLoopParamAsAccessor(inner.key, inner.param, inner.paramBindings, inner.index)
|
|
255
255
|
: null
|
|
256
256
|
|
|
257
257
|
// Inner-wrap children IR recursively so nested component props (e.g.
|
|
@@ -28,17 +28,14 @@ import {
|
|
|
28
28
|
buildLoopChildIndexExpr,
|
|
29
29
|
setIntersects,
|
|
30
30
|
varSlotId,
|
|
31
|
-
wrapLoopParamAsAccessor,
|
|
32
31
|
} from '../../utils.ts'
|
|
33
32
|
import {
|
|
34
33
|
loopKeyFn,
|
|
35
|
-
destructureLoopParam,
|
|
36
34
|
buildChildRefBindings,
|
|
37
35
|
buildStaticChildRefBindings,
|
|
38
|
-
buildPreambleRegionPlans,
|
|
39
36
|
} from '../shared.ts'
|
|
40
37
|
import { buildLoopReactiveEffectsPlan } from './build-reactive-effects.ts'
|
|
41
|
-
import {
|
|
38
|
+
import { buildPlainRowCore } from './build-plain-row.ts'
|
|
42
39
|
import type { LazyRowScopeInfo } from './lazy-row-eligibility.ts'
|
|
43
40
|
import { buildComponentLoopPlan } from './build-component-loop.ts'
|
|
44
41
|
import { buildTopLevelCompositePlan } from './build-composite-loop.ts'
|
|
@@ -99,8 +96,6 @@ export function buildPlainLoopPlan(
|
|
|
99
96
|
profileComponentName?: string,
|
|
100
97
|
lazyScope?: LazyRowScopeInfo,
|
|
101
98
|
): PlainLoopPlan {
|
|
102
|
-
const wrap = (expr: string) => wrapLoopParamAsAccessor(expr, elem.param, elem.paramBindings)
|
|
103
|
-
const { head: paramHead, unwrap: paramUnwrap } = destructureLoopParam(elem.param, elem.paramBindings)
|
|
104
99
|
const hasReactive = elem.bindings.reactiveAttrs.length > 0
|
|
105
100
|
|| elem.bindings.reactiveTexts.length > 0
|
|
106
101
|
|| elem.bindings.conditionals.length > 0
|
|
@@ -140,14 +135,26 @@ export function buildPlainLoopPlan(
|
|
|
140
135
|
}
|
|
141
136
|
|
|
142
137
|
const arrayExpr = buildChainedArrayExpr(elem)
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
138
|
+
|
|
139
|
+
// Shared "wrap item → decide lazy → wrap index" core (#2859 follow-up) —
|
|
140
|
+
// see `build-plain-row.ts`'s module docstring for why this single
|
|
141
|
+
// implementation matters: a lazy-eligible loop's `LazyRowPlanData` embeds
|
|
142
|
+
// `mapPreambleWrapped` and clones from `elem.template` verbatim, and
|
|
143
|
+
// `mapArrayLazy`'s `createRow(entry, index)` hands the row a plain index
|
|
144
|
+
// NUMBER, never an accessor — wrapping unconditionally would call a number
|
|
145
|
+
// there. Lazy-row eligibility only refuses a REACTIVE binding that
|
|
146
|
+
// references the index; a non-reactive use (e.g. a `data-key` built from
|
|
147
|
+
// the index) is not gated the same way, so "lazy-eligible" does not imply
|
|
148
|
+
// "no index in the template" either.
|
|
149
|
+
const core = buildPlainRowCore({
|
|
150
|
+
loop: elem,
|
|
151
|
+
arrayExpr,
|
|
152
|
+
callSite: 'plain',
|
|
153
|
+
flatMapLeafItem: false,
|
|
154
|
+
anchored: elem.bodyIsItemConditional ?? false,
|
|
155
|
+
scope: lazyScope,
|
|
156
|
+
})
|
|
157
|
+
const { paramHead, paramUnwrap, indexParam, preambleRegions, lazyRow, wrapItem: wrap } = core
|
|
151
158
|
|
|
152
159
|
return {
|
|
153
160
|
kind: 'plain',
|
|
@@ -160,29 +167,21 @@ export function buildPlainLoopPlan(
|
|
|
160
167
|
paramHead,
|
|
161
168
|
paramUnwrap,
|
|
162
169
|
indexParam,
|
|
163
|
-
|
|
164
|
-
// keeps the eager emission below byte-for-byte.
|
|
165
|
-
lazyRow: buildLazyRowPlan({
|
|
166
|
-
loop: elem,
|
|
167
|
-
arrayExpr,
|
|
168
|
-
indexParam,
|
|
169
|
-
paramUnwrap,
|
|
170
|
-
mapPreambleWrapped,
|
|
171
|
-
preambleRegionCount: preambleRegions.length,
|
|
172
|
-
callSite: 'plain',
|
|
173
|
-
flatMapLeafItem: false,
|
|
174
|
-
anchored: elem.bodyIsItemConditional ?? false,
|
|
175
|
-
scope: lazyScope,
|
|
176
|
-
}) ?? undefined,
|
|
170
|
+
lazyRow,
|
|
177
171
|
// Stage 3 / D4 — js segments get the loop-param accessor wrap; jsx leaves
|
|
178
172
|
// render as HTML-string templates under this loop's param context so a
|
|
179
173
|
// leaf that reads the item (`r`) becomes `r()`.
|
|
180
|
-
mapPreambleWrapped,
|
|
181
|
-
template:
|
|
174
|
+
mapPreambleWrapped: core.mapPreambleWrapped,
|
|
175
|
+
template: core.template,
|
|
182
176
|
skeletonTemplate: elem.skeletonTemplate,
|
|
183
177
|
skeletonPaths: elem.skeletonPaths,
|
|
184
178
|
reactiveEffects: hasReactive ? buildLoopReactiveEffectsPlan(elem, profileComponentName) : null,
|
|
185
|
-
|
|
179
|
+
// `elem.index` (#2859 follow-up): a ref callback closing over the loop
|
|
180
|
+
// index (`ref={el => refs[i] = el}`) must see it wrapped the same way
|
|
181
|
+
// every other reference in this row is — a row with any ref is always
|
|
182
|
+
// lazy-ineligible (`lazy-row-eligibility.ts`'s `childRefCount` gate), so
|
|
183
|
+
// there is no shared-string hazard here, unlike `mapPreambleWrapped`.
|
|
184
|
+
childRefs: buildChildRefBindings(elem.bindings.refs, elem.param, elem.paramBindings, elem.index),
|
|
186
185
|
preambleRegions,
|
|
187
186
|
bodyIsMultiRoot: elem.bodyIsMultiRoot ?? false,
|
|
188
187
|
anchored: elem.bodyIsItemConditional ?? false,
|
|
@@ -190,9 +189,10 @@ export function buildPlainLoopPlan(
|
|
|
190
189
|
// conditional without a key is a BF023 error, but the emitted client JS
|
|
191
190
|
// must still parse — an empty `anchorKeyExpr` would produce
|
|
192
191
|
// `createComment(`bf-loop-i:${}`)` (a SyntaxError that breaks the whole
|
|
193
|
-
// bundle). `
|
|
194
|
-
//
|
|
195
|
-
|
|
192
|
+
// bundle). `indexParam` matches the renderItem head built above, and
|
|
193
|
+
// (#2859) is bound to an INDEX ACCESSOR at runtime, not a plain number —
|
|
194
|
+
// call it, same as every other reference to it in this row's body.
|
|
195
|
+
anchorKeyExpr: elem.key ? wrap(elem.key) : `${indexParam}()`,
|
|
196
196
|
}
|
|
197
197
|
}
|
|
198
198
|
|
|
@@ -269,7 +269,7 @@ function buildStaticLoopMaterialize(
|
|
|
269
269
|
itemTemplate: elem.staticItemTemplate,
|
|
270
270
|
mapPreamble: elem.preamble
|
|
271
271
|
? renderPreamble(elem.preamble, {
|
|
272
|
-
renderLeaf: (ir) => irToHtmlTemplate(ir, undefined, 1, [{ param: elem.param, bindings: elem.paramBindings }], undefined),
|
|
272
|
+
renderLeaf: (ir) => irToHtmlTemplate(ir, undefined, 1, [{ param: elem.param, bindings: elem.paramBindings, index: elem.index }], undefined),
|
|
273
273
|
})
|
|
274
274
|
: '',
|
|
275
275
|
bodyIsMultiRoot: elem.bodyIsMultiRoot ?? false,
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared "wrap item → decide lazy → wrap index" core for a plain loop row.
|
|
3
|
+
*
|
|
4
|
+
* `buildPlainLoopPlan` (`build-loop.ts`, `TopLevelLoop`) and
|
|
5
|
+
* `buildBranchLoopPlan` (`build-branch-loop.ts`, `BranchLoop`) both build a
|
|
6
|
+
* row from a `.map()` callback the SAME way:
|
|
7
|
+
*
|
|
8
|
+
* 1. Wrap the row's JS with ITEM-ONLY accessor rewriting (no index) — this
|
|
9
|
+
* string must stay index-unwrapped because it is reused VERBATIM by the
|
|
10
|
+
* lazy row plan below, which reads the index differently (a live
|
|
11
|
+
* per-call value off `entry.index`/`createRow`'s parameter, never a
|
|
12
|
+
* `.map()`-body call).
|
|
13
|
+
* 2. Ask `buildLazyRowPlan` for a verdict.
|
|
14
|
+
* 3. Only once lazy is ruled out (`!lazyRow`), wrap index references too,
|
|
15
|
+
* for the eager path — `mapArrayLazy`'s `createRow` hands a row a plain
|
|
16
|
+
* index NUMBER, so wrapping unconditionally would call a number.
|
|
17
|
+
*
|
|
18
|
+
* Before this extraction, the sequence was duplicated verbatim (mod `elem`/`loop` naming)
|
|
19
|
+
* between the two builders — exactly the "one decision, two implementations"
|
|
20
|
+
* pattern this repo's CLAUDE.md warns against, and the shape of the original
|
|
21
|
+
* #2859 CI regression (the eager index-wrap leaking into the lazy-shared
|
|
22
|
+
* string). One implementation now, called from both sites, so the decision
|
|
23
|
+
* cannot silently diverge again.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
import type { BranchLoop, TopLevelLoop } from '../../types.ts'
|
|
27
|
+
import { wrapLoopParamAsAccessor, wrapIndexParamAsAccessor } from '../../utils.ts'
|
|
28
|
+
import { destructureLoopParam, buildPreambleRegionPlans } from '../shared.ts'
|
|
29
|
+
import { renderPreamble, irToHtmlTemplate } from '../../html-template.ts'
|
|
30
|
+
import { buildLazyRowPlan, type LazyRowPlanData } from './build-lazy-row.ts'
|
|
31
|
+
import type { LazyRowScopeInfo } from './lazy-row-eligibility.ts'
|
|
32
|
+
import type { PreambleRegionPlan } from './loop.ts'
|
|
33
|
+
|
|
34
|
+
export interface PlainRowInputs {
|
|
35
|
+
loop: TopLevelLoop | BranchLoop
|
|
36
|
+
/** Fully-chained array expression as the CALLER emits it (branch-loop's flatMap form differs from plain's). */
|
|
37
|
+
arrayExpr: string
|
|
38
|
+
callSite: 'plain' | 'branch-plain'
|
|
39
|
+
flatMapLeafItem: boolean
|
|
40
|
+
anchored: boolean
|
|
41
|
+
scope: LazyRowScopeInfo | undefined
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface PlainRowCore {
|
|
45
|
+
indexParam: string
|
|
46
|
+
paramHead: string
|
|
47
|
+
paramUnwrap: string
|
|
48
|
+
preambleRegions: readonly PreambleRegionPlan[]
|
|
49
|
+
lazyRow: LazyRowPlanData | undefined
|
|
50
|
+
/** Item-wrapped, and index-wrapped too IFF the row is NOT lazy-eligible. */
|
|
51
|
+
mapPreambleWrapped: string
|
|
52
|
+
/** Same rule as `mapPreambleWrapped`, applied to `loop.template`. */
|
|
53
|
+
template: string
|
|
54
|
+
/** Item-only wrap — exposed for the few expressions (e.g. `anchorKeyExpr`) built outside this core. */
|
|
55
|
+
wrapItem: (expr: string) => string
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function buildPlainRowCore(inputs: PlainRowInputs): PlainRowCore {
|
|
59
|
+
const { loop, arrayExpr, callSite, flatMapLeafItem, anchored, scope } = inputs
|
|
60
|
+
|
|
61
|
+
const wrapItem = (expr: string) => wrapLoopParamAsAccessor(expr, loop.param, loop.paramBindings)
|
|
62
|
+
const { head: paramHead, unwrap: paramUnwrap } = destructureLoopParam(loop.param, loop.paramBindings)
|
|
63
|
+
const indexParam = loop.index || '__idx'
|
|
64
|
+
|
|
65
|
+
const mapPreambleWrapped = loop.preamble
|
|
66
|
+
? renderPreamble(loop.preamble, {
|
|
67
|
+
transformJs: wrapItem,
|
|
68
|
+
renderLeaf: (ir) => irToHtmlTemplate(ir, undefined, 1, [{ param: loop.param, bindings: loop.paramBindings }], undefined),
|
|
69
|
+
})
|
|
70
|
+
: ''
|
|
71
|
+
const preambleRegions = buildPreambleRegionPlans(loop.preambleRegions, loop.param, loop.paramBindings, loop.index)
|
|
72
|
+
|
|
73
|
+
// Lazy row graph (§9, L3). `undefined` for every ineligible loop, which
|
|
74
|
+
// then keeps the eager emission below byte-for-byte. Decided from the
|
|
75
|
+
// item-only-wrapped `mapPreambleWrapped` above — eligibility itself reads
|
|
76
|
+
// `loop.preamble` (raw), not this string, so which wrap it carries doesn't
|
|
77
|
+
// change the verdict.
|
|
78
|
+
const lazyRow = buildLazyRowPlan({
|
|
79
|
+
loop,
|
|
80
|
+
arrayExpr,
|
|
81
|
+
indexParam,
|
|
82
|
+
paramUnwrap,
|
|
83
|
+
mapPreambleWrapped,
|
|
84
|
+
preambleRegionCount: preambleRegions.length,
|
|
85
|
+
callSite,
|
|
86
|
+
flatMapLeafItem,
|
|
87
|
+
anchored,
|
|
88
|
+
scope,
|
|
89
|
+
}) ?? undefined
|
|
90
|
+
|
|
91
|
+
// Only once lazy is ruled out is it safe to ALSO wrap index references
|
|
92
|
+
// (#2859 follow-up) — see the module docstring.
|
|
93
|
+
const mapPreambleWrappedFinal = !lazyRow && loop.index
|
|
94
|
+
? wrapIndexParamAsAccessor(mapPreambleWrapped, loop.index)
|
|
95
|
+
: mapPreambleWrapped
|
|
96
|
+
const templateFinal = !lazyRow && loop.index
|
|
97
|
+
? wrapIndexParamAsAccessor(loop.template, loop.index)
|
|
98
|
+
: loop.template
|
|
99
|
+
|
|
100
|
+
return {
|
|
101
|
+
indexParam,
|
|
102
|
+
paramHead,
|
|
103
|
+
paramUnwrap,
|
|
104
|
+
preambleRegions,
|
|
105
|
+
lazyRow,
|
|
106
|
+
mapPreambleWrapped: mapPreambleWrappedFinal,
|
|
107
|
+
template: templateFinal,
|
|
108
|
+
wrapItem,
|
|
109
|
+
}
|
|
110
|
+
}
|
|
@@ -52,6 +52,8 @@ export interface BuildReactiveEffectsArgs {
|
|
|
52
52
|
conditionals: readonly LoopChildConditional[] | undefined
|
|
53
53
|
loopParam: string
|
|
54
54
|
loopParamBindings?: readonly LoopParamBinding[]
|
|
55
|
+
/** This loop's index param name, when it declares one (#2859). */
|
|
56
|
+
loopIndex?: string | null
|
|
55
57
|
/** Owning component name in profile mode (#1690, SR3) — else undefined. */
|
|
56
58
|
profileComponentName?: string
|
|
57
59
|
}
|
|
@@ -60,8 +62,8 @@ export interface BuildReactiveEffectsArgs {
|
|
|
60
62
|
export function buildReactiveEffectsPlan(
|
|
61
63
|
args: BuildReactiveEffectsArgs,
|
|
62
64
|
): ReactiveEffectsPlan {
|
|
63
|
-
const { attrs, texts, conditionals, loopParam, loopParamBindings, profileComponentName } = args
|
|
64
|
-
const wrap = (expr: string) => wrapLoopParamAsAccessor(expr, loopParam, loopParamBindings)
|
|
65
|
+
const { attrs, texts, conditionals, loopParam, loopParamBindings, loopIndex, profileComponentName } = args
|
|
66
|
+
const wrap = (expr: string) => wrapLoopParamAsAccessor(expr, loopParam, loopParamBindings, loopIndex)
|
|
65
67
|
|
|
66
68
|
// 1. Group attrs by slot, preserving declaration order (Map insertion order
|
|
67
69
|
// matches the legacy iteration that produced byte-identical output).
|
|
@@ -175,6 +177,7 @@ export function buildLoopReactiveEffectsPlan(elem: TopLevelLoop, profileComponen
|
|
|
175
177
|
conditionals: elem.bindings.conditionals,
|
|
176
178
|
loopParam: elem.param,
|
|
177
179
|
loopParamBindings: elem.paramBindings,
|
|
180
|
+
loopIndex: elem.index,
|
|
178
181
|
profileComponentName,
|
|
179
182
|
})
|
|
180
183
|
}
|
|
@@ -47,9 +47,11 @@
|
|
|
47
47
|
* defeating the hoist. (A literal `${` in authored TEXT would also refuse
|
|
48
48
|
* here; that is the safe direction — a false refusal costs the eager
|
|
49
49
|
* fallback, a false accept would ship a frozen arm.)
|
|
50
|
-
* 4.
|
|
51
|
-
*
|
|
52
|
-
* `
|
|
50
|
+
* 4. (#2859 follow-up) A condition MAY read the loop index — it is
|
|
51
|
+
* classified like any other binding (`classifyLazyBinding` in
|
|
52
|
+
* `lazy-row-eligibility.ts`), which forces it into `applyItem` so a
|
|
53
|
+
* reorder (a position change the reconciler tracks on `entry.index`)
|
|
54
|
+
* re-evaluates it, not just an item change.
|
|
53
55
|
*
|
|
54
56
|
* Everything refused carries a specific reason, which `lazyRowEligibility`
|
|
55
57
|
* passes through unchanged.
|
|
@@ -99,13 +101,9 @@ function wiringOn(branch: LoopChildBranchSummary): string[] {
|
|
|
99
101
|
|
|
100
102
|
/**
|
|
101
103
|
* Decide whether `cond` can be driven from the loop-level apply bodies.
|
|
102
|
-
*
|
|
103
|
-
* `indexParam` is the loop's index parameter name as the emitter uses it
|
|
104
|
-
* (`elem.index || '__idx'`); a condition reading it is refused.
|
|
105
104
|
*/
|
|
106
105
|
export function analyzeLazyConditional(
|
|
107
106
|
cond: LoopChildConditional,
|
|
108
|
-
indexParam: string,
|
|
109
107
|
arms: PreparedArms,
|
|
110
108
|
): LazyConditionalAnalysis {
|
|
111
109
|
for (const [label, branch] of [['true', cond.whenTrue], ['false', cond.whenFalse]] as const) {
|
|
@@ -135,9 +133,12 @@ export function analyzeLazyConditional(
|
|
|
135
133
|
if (!cond.conditionFreeIdentifiers) {
|
|
136
134
|
return NO(`conditional on slot ${cond.slotId}: condition has no analyzable identifier set`)
|
|
137
135
|
}
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
136
|
+
// A condition reading the loop index (#2859 follow-up, widening from a hard
|
|
137
|
+
// refusal) is no longer refused here: the caller classifies the condition
|
|
138
|
+
// like any other binding (`classifyLazyBinding` in `lazy-row-eligibility.ts`),
|
|
139
|
+
// which forces `readsItem: true` for an index reference — the reconciler
|
|
140
|
+
// delivers the row's current position on `entry.index` the same way it
|
|
141
|
+
// delivers the item, so the condition is re-evaluated on a reorder too.
|
|
141
142
|
|
|
142
143
|
return {
|
|
143
144
|
lazySafe: true,
|