@barefootjs/jsx 0.33.6 → 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/adapters/parsed-expr-emitter.d.ts +17 -0
- package/dist/adapters/parsed-expr-emitter.d.ts.map +1 -1
- package/dist/analyzer.d.ts.map +1 -1
- package/dist/expression-parser.d.ts +17 -4
- package/dist/expression-parser.d.ts.map +1 -1
- package/dist/index.d.ts +4 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +445 -282
- 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/lowering-registry.d.ts +12 -0
- package/dist/lowering-registry.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__/lowering-value-position.test.ts +232 -0
- 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/adapters/parsed-expr-emitter.ts +43 -0
- package/src/analyzer.ts +4 -0
- package/src/expression-parser.ts +63 -29
- package/src/index.ts +10 -3
- 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/lowering-registry.ts +21 -0
- 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
|
@@ -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,
|
|
@@ -68,9 +68,10 @@
|
|
|
68
68
|
* markup, which is `preambleRegions` territory (refused separately).
|
|
69
69
|
* - No `builderNames`. Same shape as above, pinned explicitly so the reason
|
|
70
70
|
* names it rather than surfacing as a confusing "contains a call".
|
|
71
|
-
* - The preamble
|
|
72
|
-
* `
|
|
73
|
-
*
|
|
71
|
+
* - (#2859 follow-up) The preamble MAY read the loop's index parameter — it
|
|
72
|
+
* survives into `freeNames` below like any other read, so a binding that
|
|
73
|
+
* names this preamble local substitutes through to it and is classified
|
|
74
|
+
* the same way a direct index reference is (forced into `applyItem`).
|
|
74
75
|
*
|
|
75
76
|
* Member access is deliberately NOT banned even though a getter could in
|
|
76
77
|
* principle run code: a binding expression like `class={row.cls}` is already
|
|
@@ -130,9 +131,6 @@ const NO = (reason: string): LazyPreambleAnalysis => ({ lazySafe: false, reason
|
|
|
130
131
|
* Decide whether `preamble` may be re-executed inside a lazy row's apply
|
|
131
132
|
* bodies, and if so what it declares and reads.
|
|
132
133
|
*
|
|
133
|
-
* `indexParam` is the loop's index parameter name as the emitter uses it
|
|
134
|
-
* (`elem.index || '__idx'`); a preamble reading it is refused.
|
|
135
|
-
*
|
|
136
134
|
* `primableNames` are the component's signal getters and memos — the only
|
|
137
135
|
* callees a preamble initializer may invoke (see the module docstring). Pass
|
|
138
136
|
* the same names `LazyRowScopeInfo` carries, so "primable here" and "primable
|
|
@@ -140,7 +138,6 @@ const NO = (reason: string): LazyPreambleAnalysis => ({ lazySafe: false, reason
|
|
|
140
138
|
*/
|
|
141
139
|
export function analyzeLazyPreamble(
|
|
142
140
|
preamble: MapCallbackPreamble | undefined,
|
|
143
|
-
indexParam: string,
|
|
144
141
|
primableNames: ReadonlySet<string>,
|
|
145
142
|
): LazyPreambleAnalysis {
|
|
146
143
|
if (!preamble) return NO_PREAMBLE
|
|
@@ -196,13 +193,14 @@ export function analyzeLazyPreamble(
|
|
|
196
193
|
}
|
|
197
194
|
}
|
|
198
195
|
|
|
199
|
-
//
|
|
200
|
-
//
|
|
201
|
-
//
|
|
196
|
+
// A preamble reading the loop index (#2859 follow-up, widening from a hard
|
|
197
|
+
// refusal) is no longer refused here: `indexParam` survives into
|
|
198
|
+
// `freeNames` below, and any binding that names this preamble local
|
|
199
|
+
// substitutes through to it (`classifyLazyBinding`'s preamble handling),
|
|
200
|
+
// which classifies a free `indexParam` the same way a direct reference
|
|
201
|
+
// would — forcing the binding into `applyItem` so a reorder re-evaluates
|
|
202
|
+
// it via `entry.index`.
|
|
202
203
|
const readNames = extractFreeIdentifiersFromStatementText(text)
|
|
203
|
-
if (readNames.has(indexParam) && !declaredNames.has(indexParam)) {
|
|
204
|
-
return NO(`map-callback preamble reads the loop index parameter '${indexParam}'`)
|
|
205
|
-
}
|
|
206
204
|
|
|
207
205
|
const freeNames = new Set(readNames)
|
|
208
206
|
for (const declared of declaredNames) freeNames.delete(declared)
|
|
@@ -284,9 +284,15 @@ export function lazyRowEligibility(args: LazyRowEligibilityArgs): LazyRowEligibi
|
|
|
284
284
|
|
|
285
285
|
// Per-binding gates.
|
|
286
286
|
for (const b of bindings) {
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
287
|
+
// A binding that references the loop index (#2859 follow-up, widening
|
|
288
|
+
// from a hard refusal): `applyItem`/`applyOuter` have no index PARAMETER,
|
|
289
|
+
// but the reconciler tracks each entry's current position on `entry.index`
|
|
290
|
+
// (`LazyRowEntry.index`, `map-array-lazy.ts`) exactly like it tracks
|
|
291
|
+
// `entry.item` — a reorder is a position change the reconciler already
|
|
292
|
+
// detects, the same way an item change is. `classifyLazyBinding` forces
|
|
293
|
+
// `readsItem: true` for any binding with `referencesIndex`, so it is
|
|
294
|
+
// ALWAYS emitted into `applyItem`, and the runtime calls `applyItem` on a
|
|
295
|
+
// pure reorder too when `plan.indexDriven` (`LazyRowPlanData.readsIndex`).
|
|
290
296
|
// An identifier set we could not compute at all is different in kind
|
|
291
297
|
// from a name we CAN see but cannot prime. With `free === null` the
|
|
292
298
|
// classifier knows nothing — not the outer reads, and not whether the
|
|
@@ -445,7 +451,11 @@ export function classifyLazyBinding(args: {
|
|
|
445
451
|
return
|
|
446
452
|
}
|
|
447
453
|
if (name === indexParam) {
|
|
454
|
+
// The row's current position (#2859 follow-up) — the reconciler
|
|
455
|
+
// delivers it on `entry.index` exactly like it delivers the item on
|
|
456
|
+
// `entry.item`, so a binding that reads it is item-driven too.
|
|
448
457
|
referencesIndex = true
|
|
458
|
+
readsItem = true
|
|
449
459
|
return
|
|
450
460
|
}
|
|
451
461
|
if (INERT_BINDING_GLOBALS.has(name)) return
|