@barefootjs/jsx 0.34.0 → 0.35.1
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/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 +1072 -903
- 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-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-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 +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-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-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 +5 -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/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-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/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/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/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 +54 -0
- 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/reactivity.d.ts +62 -18
- package/dist/ir-to-client-js/reactivity.d.ts.map +1 -1
- 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/types.d.ts +18 -2
- package/dist/ir-to-client-js/types.d.ts.map +1 -1
- 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/reactivity-checker.d.ts +18 -0
- package/dist/reactivity-checker.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 +138 -22
- package/src/__tests__/binding-scope-ratchet.test.ts +25 -2
- package/src/__tests__/build-plain-row-consolidation.test.ts +120 -0
- package/src/__tests__/child-prop-fallback-wrap.test.ts +4 -2
- package/src/__tests__/client-template-escape-soundness.test.ts +538 -0
- package/src/__tests__/compiler-stress-1244.test.ts +8 -8
- package/src/__tests__/csr-template-loop-shadowing.test.ts +4 -2
- package/src/__tests__/flatmap-segments.test.ts +1 -1
- 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__/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-index-reactivity.test.ts +208 -0
- package/src/__tests__/loop-item-conditional-codegen.test.ts +4 -2
- package/src/__tests__/map-arbitrary-body.test.ts +1 -1
- package/src/__tests__/map-body-brand-checker.test.ts +176 -0
- package/src/__tests__/nested-loop-index-param.test.ts +41 -8
- 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 +16 -6
- package/src/__tests__/preamble-branch-arm-rerun.test.ts +135 -0
- 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/__tests__/style-css-var-reactive.test.ts +4 -1
- package/src/analyzer-context.ts +12 -10
- package/src/analyzer.ts +4 -0
- package/src/index.ts +8 -2
- package/src/ir-to-client-js/collect-elements.ts +126 -32
- 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 +12 -6
- package/src/ir-to-client-js/control-flow/plan/build-inner-loop.ts +36 -59
- package/src/ir-to-client-js/control-flow/plan/build-lazy-row.ts +25 -4
- package/src/ir-to-client-js/control-flow/plan/build-loop-child-arm.ts +36 -9
- 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 +116 -0
- package/src/ir-to-client-js/control-flow/plan/build-reactive-effects.ts +23 -8
- package/src/ir-to-client-js/control-flow/plan/inner-loop.ts +24 -45
- 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-child-arm.ts +14 -0
- 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/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 +22 -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/csr-substitute.ts +29 -0
- package/src/ir-to-client-js/emit-reactive.ts +92 -6
- package/src/ir-to-client-js/emit-registration.ts +2 -4
- package/src/ir-to-client-js/html-template.ts +67 -245
- 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/reactivity.ts +73 -27
- package/src/ir-to-client-js/safe-html.ts +243 -0
- package/src/ir-to-client-js/types.ts +19 -3
- package/src/ir-to-client-js/utils.ts +58 -10
- package/src/jsx-to-ir.ts +136 -24
- package/src/prop-rewrite.ts +8 -4
- package/src/props-binding.ts +37 -1
- package/src/reactivity-checker.ts +60 -0
- package/src/ssr-defaults.ts +45 -0
- package/src/types.ts +5 -10
- package/src/__tests__/text-slot-escaping.test.ts +0 -65
|
@@ -7,8 +7,18 @@
|
|
|
7
7
|
* - Sibling levels at the same depth are flattened into the top-level
|
|
8
8
|
* list; deeper levels become `childLevels` of the immediately-preceding
|
|
9
9
|
* ancestor.
|
|
10
|
-
* -
|
|
11
|
-
*
|
|
10
|
+
* - Every level gets the full reactive `mapArray` emission (#2865 —
|
|
11
|
+
* the former "static forEach, setup-only" shortcut for a nested
|
|
12
|
+
* `.map()` whose array didn't textually mention the outer loop's item
|
|
13
|
+
* silently froze any OTHER reactivity the inner array or its rows
|
|
14
|
+
* depended on: a signal, a memo, a prop, an imported function call, a
|
|
15
|
+
* preamble local, a destructured outer param, an outer-index-derived
|
|
16
|
+
* array — none of those are "the outer item", so `refsParent` missed
|
|
17
|
+
* all of them, and the static emitter never wired a single
|
|
18
|
+
* `createEffect` for the row's own reactiveTexts/reactiveAttrs/
|
|
19
|
+
* conditionals either. A `mapArray` over a genuinely non-reactive
|
|
20
|
+
* array just subscribes to nothing and runs once — a strict superset
|
|
21
|
+
* of what the deleted static path did).
|
|
12
22
|
* - Recurse with `inner.param` as the new outerLoopParam so deeper
|
|
13
23
|
* levels see their immediate parent.
|
|
14
24
|
*/
|
|
@@ -28,7 +38,7 @@ import {
|
|
|
28
38
|
wrapLoopParamAsAccessor,
|
|
29
39
|
attrValueToString,
|
|
30
40
|
} from '../../utils.ts'
|
|
31
|
-
import { buildChildRefBindings
|
|
41
|
+
import { buildChildRefBindings } from '../shared.ts'
|
|
32
42
|
import { renderPreamble, irToHtmlTemplate } from '../../html-template.ts'
|
|
33
43
|
import { buildLoopChildConditionalsPlan } from './build-loop-child-arm.ts'
|
|
34
44
|
import type { LoopChildConditionalPlan } from './loop-child-arm.ts'
|
|
@@ -67,7 +77,6 @@ import type {
|
|
|
67
77
|
InnerLoopPlan,
|
|
68
78
|
InnerLoopReactiveAttr,
|
|
69
79
|
InnerLoopReactiveEmit,
|
|
70
|
-
InnerLoopStaticEmit,
|
|
71
80
|
InnerLoopText,
|
|
72
81
|
InnerLoopsPlan,
|
|
73
82
|
} from './inner-loop.ts'
|
|
@@ -77,6 +86,8 @@ export interface BuildInnerLoopsArgs {
|
|
|
77
86
|
parentElVar: string
|
|
78
87
|
outerLoopParam: string | undefined
|
|
79
88
|
outerLoopParamBindings?: readonly LoopParamBinding[]
|
|
89
|
+
/** Outer loop's index param name, when present (#2861). */
|
|
90
|
+
outerLoopIndex?: string | null
|
|
80
91
|
}
|
|
81
92
|
|
|
82
93
|
/**
|
|
@@ -85,9 +96,9 @@ export interface BuildInnerLoopsArgs {
|
|
|
85
96
|
* conditional branch are skipped (they're emitted by `stringifyBranchInnerLoops`).
|
|
86
97
|
*/
|
|
87
98
|
export function buildInnerLoopsPlan(args: BuildInnerLoopsArgs): InnerLoopsPlan {
|
|
88
|
-
const { levels, parentElVar, outerLoopParam, outerLoopParamBindings } = args
|
|
99
|
+
const { levels, parentElVar, outerLoopParam, outerLoopParamBindings, outerLoopIndex } = args
|
|
89
100
|
const wrapOuter = outerLoopParam
|
|
90
|
-
? (expr: string) => wrapLoopParamAsAccessor(expr, outerLoopParam, outerLoopParamBindings)
|
|
101
|
+
? (expr: string) => wrapLoopParamAsAccessor(expr, outerLoopParam, outerLoopParamBindings, outerLoopIndex)
|
|
91
102
|
: (expr: string) => expr
|
|
92
103
|
|
|
93
104
|
const plan: InnerLoopPlan[] = []
|
|
@@ -111,17 +122,13 @@ export function buildInnerLoopsPlan(args: BuildInnerLoopsArgs): InnerLoopsPlan {
|
|
|
111
122
|
? `qsa(${parentElVar}, '[bf="${inner.containerSlotId}"]')`
|
|
112
123
|
: parentElVar
|
|
113
124
|
|
|
114
|
-
//
|
|
115
|
-
//
|
|
116
|
-
//
|
|
117
|
-
|
|
118
|
-
const
|
|
125
|
+
// #2865: every nested loop gets the full reactive `mapArray` emission,
|
|
126
|
+
// regardless of whether its array references the outer loop's item —
|
|
127
|
+
// see the module docstring for why the old `refsParent`-gated static
|
|
128
|
+
// shortcut was unsound.
|
|
129
|
+
const emit: InnerLoopReactiveEmit = buildReactiveEmit(inner, level, wrapOuter, uidSuffix, outerLoopParam, outerLoopParamBindings, outerLoopIndex)
|
|
119
130
|
|
|
120
|
-
const
|
|
121
|
-
? buildReactiveEmit(inner, level, wrapOuter, uidSuffix, outerLoopParam, outerLoopParamBindings)
|
|
122
|
-
: buildStaticEmit(inner, level, uidSuffix)
|
|
123
|
-
|
|
124
|
-
const arrayExpr = useReactive ? wrapOuter(inner.array) : inner.array
|
|
131
|
+
const arrayExpr = wrapOuter(inner.array)
|
|
125
132
|
|
|
126
133
|
const childLevelsPlan = childLevels.length > 0
|
|
127
134
|
? buildInnerLoopsPlan({
|
|
@@ -129,6 +136,7 @@ export function buildInnerLoopsPlan(args: BuildInnerLoopsArgs): InnerLoopsPlan {
|
|
|
129
136
|
parentElVar: `__innerEl${uidSuffix}`,
|
|
130
137
|
outerLoopParam: inner.param,
|
|
131
138
|
outerLoopParamBindings: inner.paramBindings,
|
|
139
|
+
outerLoopIndex: inner.index,
|
|
132
140
|
})
|
|
133
141
|
: []
|
|
134
142
|
|
|
@@ -146,6 +154,7 @@ export function buildInnerLoopsPlan(args: BuildInnerLoopsArgs): InnerLoopsPlan {
|
|
|
146
154
|
childLevels: childLevelsPlan,
|
|
147
155
|
outerLoopParam,
|
|
148
156
|
outerLoopParamBindings,
|
|
157
|
+
outerLoopIndex,
|
|
149
158
|
})
|
|
150
159
|
|
|
151
160
|
i = j
|
|
@@ -160,12 +169,13 @@ function buildReactiveEmit(
|
|
|
160
169
|
uidSuffix: string,
|
|
161
170
|
outerLoopParam?: string,
|
|
162
171
|
outerLoopParamBindings?: readonly LoopParamBinding[],
|
|
172
|
+
outerLoopIndex?: string | null,
|
|
163
173
|
): InnerLoopReactiveEmit {
|
|
164
|
-
const wrapInner = (expr: string) => wrapLoopParamAsAccessor(expr, inner.param, inner.paramBindings)
|
|
165
|
-
const wrapBoth = (expr: string) => wrapLoopParamAsAccessor(wrapOuter(expr), inner.param, inner.paramBindings)
|
|
174
|
+
const wrapInner = (expr: string) => wrapLoopParamAsAccessor(expr, inner.param, inner.paramBindings, inner.index)
|
|
175
|
+
const wrapBoth = (expr: string) => wrapLoopParamAsAccessor(wrapOuter(expr), inner.param, inner.paramBindings, inner.index)
|
|
166
176
|
const { head: paramHead, unwrap: paramUnwrap } = destructureLoopParam(inner.param, inner.paramBindings)
|
|
167
177
|
const wrappedKey = inner.key
|
|
168
|
-
? wrapLoopParamAsAccessor(inner.key, inner.param, inner.paramBindings)
|
|
178
|
+
? wrapLoopParamAsAccessor(inner.key, inner.param, inner.paramBindings, inner.index)
|
|
169
179
|
: null
|
|
170
180
|
|
|
171
181
|
// Inner-wrap children IR recursively so nested component props (e.g.,
|
|
@@ -202,12 +212,12 @@ function buildReactiveEmit(
|
|
|
202
212
|
|
|
203
213
|
const reactiveTexts: InnerLoopText[] = inner.bindings.reactiveTexts.map(text => ({
|
|
204
214
|
slotId: text.slotId,
|
|
205
|
-
wrappedExpression: wrapLoopParamAsAccessor(wrapOuter(text.expression), inner.param, inner.paramBindings),
|
|
215
|
+
wrappedExpression: wrapLoopParamAsAccessor(wrapOuter(text.expression), inner.param, inner.paramBindings, inner.index),
|
|
206
216
|
insideConditional: !!text.insideConditional,
|
|
207
217
|
}))
|
|
208
218
|
|
|
209
219
|
const reactiveAttrs: InnerLoopReactiveAttr[] = inner.bindings.reactiveAttrs.map(attr => {
|
|
210
|
-
const wrapped = wrapLoopParamAsAccessor(wrapOuter(attr.expression), inner.param, inner.paramBindings)
|
|
220
|
+
const wrapped = wrapLoopParamAsAccessor(wrapOuter(attr.expression), inner.param, inner.paramBindings, inner.index)
|
|
211
221
|
return {
|
|
212
222
|
slotId: attr.childSlotId,
|
|
213
223
|
attrName: attr.attrName,
|
|
@@ -240,17 +250,17 @@ function buildReactiveEmit(
|
|
|
240
250
|
// wrapInner(wrapOuter(...)) applied to the js text.
|
|
241
251
|
const leafLoopParams = outerLoopParam
|
|
242
252
|
? [
|
|
243
|
-
{ param: outerLoopParam, bindings: outerLoopParamBindings },
|
|
244
|
-
{ param: inner.param, bindings: inner.paramBindings },
|
|
253
|
+
{ param: outerLoopParam, bindings: outerLoopParamBindings, index: outerLoopIndex },
|
|
254
|
+
{ param: inner.param, bindings: inner.paramBindings, index: inner.index },
|
|
245
255
|
]
|
|
246
|
-
: [{ param: inner.param, bindings: inner.paramBindings }]
|
|
256
|
+
: [{ param: inner.param, bindings: inner.paramBindings, index: inner.index }]
|
|
247
257
|
preludeStatements.push(renderPreamble(inner.preamble, {
|
|
248
258
|
transformJs: (t) => wrapInner(wrapOuter(t)),
|
|
249
259
|
renderLeaf: (ir) => irToHtmlTemplate(ir, undefined, 1, leafLoopParams, undefined),
|
|
250
260
|
}))
|
|
251
261
|
}
|
|
252
262
|
|
|
253
|
-
const childRefs = buildChildRefBindings(inner.bindings.refs, inner.param, inner.paramBindings)
|
|
263
|
+
const childRefs = buildChildRefBindings(inner.bindings.refs, inner.param, inner.paramBindings, inner.index)
|
|
254
264
|
|
|
255
265
|
// Per-item conditionals inside THIS loop's own row (#2706) — same
|
|
256
266
|
// insert()-parity treatment the top-level loop's row conditionals
|
|
@@ -265,6 +275,7 @@ function buildReactiveEmit(
|
|
|
265
275
|
wrap: wrapBoth,
|
|
266
276
|
loopParam: inner.param,
|
|
267
277
|
loopParamBindings: inner.paramBindings,
|
|
278
|
+
loopIndex: inner.index,
|
|
268
279
|
})
|
|
269
280
|
|
|
270
281
|
return {
|
|
@@ -283,37 +294,3 @@ function buildReactiveEmit(
|
|
|
283
294
|
childRefs,
|
|
284
295
|
}
|
|
285
296
|
}
|
|
286
|
-
|
|
287
|
-
function buildStaticEmit(inner: NestedLoop, level: DepthLevel, uidSuffix: string): InnerLoopStaticEmit {
|
|
288
|
-
// Static `forEach` iterates with the literal item as its first param, so
|
|
289
|
-
// no signal-accessor rewrite is needed — emit the preamble verbatim
|
|
290
|
-
// before the component/event setup so prop getters and event handlers
|
|
291
|
-
// can resolve the locals (#1064). Refs follow the same contract:
|
|
292
|
-
// wrapping the callback would rewrite `s.x` to `s().x` and throw at
|
|
293
|
-
// runtime when the callback closes over the static inner param (#1244,
|
|
294
|
-
// PR #1352 Copilot review).
|
|
295
|
-
//
|
|
296
|
-
// The index alias (#2218) lands first, same rationale as the reactive
|
|
297
|
-
// path: `forEach`'s second param is the synthetic `__innerIdx<uid>`, not
|
|
298
|
-
// the user's index name, so a preamble/prop/event that reads it needs the
|
|
299
|
-
// alias bound before anything else runs.
|
|
300
|
-
const preludeStatements: string[] = []
|
|
301
|
-
const indexAlias = nestedLoopIndexAlias(inner, `__innerIdx${uidSuffix}`, inner.param, level.comps, level.events)
|
|
302
|
-
if (indexAlias) preludeStatements.push(indexAlias)
|
|
303
|
-
// Static `forEach` receives the literal item, so neither the js text nor a
|
|
304
|
-
// leaf's param reads get accessor-wrapped (mirrors the verbatim emission
|
|
305
|
-
// of the js segments above).
|
|
306
|
-
if (inner.preamble) {
|
|
307
|
-
preludeStatements.push(renderPreamble(inner.preamble, {
|
|
308
|
-
renderLeaf: (ir) => irToHtmlTemplate(ir, undefined, 1, undefined, undefined),
|
|
309
|
-
}))
|
|
310
|
-
}
|
|
311
|
-
return {
|
|
312
|
-
mode: 'static',
|
|
313
|
-
rawKey: inner.key ?? null,
|
|
314
|
-
preludeStatements,
|
|
315
|
-
components: level.comps,
|
|
316
|
-
events: level.events,
|
|
317
|
-
childRefs: buildStaticChildRefBindings(inner.bindings.refs),
|
|
318
|
-
}
|
|
319
|
-
}
|
|
@@ -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,9 +218,14 @@ 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
|
-
|
|
211
|
-
|
|
212
|
-
|
|
221
|
+
// whenTrueHtml/whenFalseHtml are already loop-param-wrapped at IR
|
|
222
|
+
// render time (`irToHtmlTemplate`'s `loopParams`, collect-elements.ts)
|
|
223
|
+
// — do NOT re-wrap the rendered HTML string here (#2868: a
|
|
224
|
+
// word-boundary regex over assembled markup can match a bare tag name
|
|
225
|
+
// colliding with the param/index identifier, e.g. `<i>` -> `<i()>`).
|
|
226
|
+
const verdict = analyzeLazyConditional(cond, {
|
|
227
|
+
whenTrueHtml: addCondAttrToTemplate(cond.whenTrueHtml, cond.slotId),
|
|
228
|
+
whenFalseHtml: addCondAttrToTemplate(cond.whenFalseHtml, cond.slotId),
|
|
213
229
|
})
|
|
214
230
|
if (!verdict.lazySafe) { conditionalRefusal = verdict.reason; break }
|
|
215
231
|
condFacts.push(verdict.facts)
|
|
@@ -374,6 +390,11 @@ export function decideLazyRow(args: BuildLazyRowArgs): {
|
|
|
374
390
|
itemNeedsPreamble: [...attrs, ...texts, ...conditionals].some(b => b.readsItem && b.readsPreamble),
|
|
375
391
|
outerNeedsPreamble: [...attrs, ...texts, ...conditionals].some(b => b.readsOuter && b.readsPreamble),
|
|
376
392
|
conditionals,
|
|
393
|
+
// From the RAW classification, not the final lists: a binding that
|
|
394
|
+
// reads the index is always `readsItem` (see `classifyLazyBinding`),
|
|
395
|
+
// so checking the final lists would say the same thing — raw is just
|
|
396
|
+
// the more direct source of truth.
|
|
397
|
+
readsIndex: classified.some(c => c.referencesIndex),
|
|
377
398
|
},
|
|
378
399
|
decision,
|
|
379
400
|
}
|
|
@@ -202,6 +202,8 @@ export interface BuildBranchInnerLoopsArgs {
|
|
|
202
202
|
outerLoopParam: string
|
|
203
203
|
/** Outer loop param destructuring metadata. */
|
|
204
204
|
outerLoopParamBindings?: readonly LoopParamBinding[]
|
|
205
|
+
/** Outer loop's index param name, when present (#2861). */
|
|
206
|
+
outerLoopIndex?: string | null
|
|
205
207
|
/**
|
|
206
208
|
* Outer-wrap closure — defaults to wrapping with `outerLoopParam`. Overridden
|
|
207
209
|
* by `emitNestedLoopChildConditionals` recursion (which threads its own wrap).
|
|
@@ -225,6 +227,7 @@ export function buildBranchInnerLoopsPlan(
|
|
|
225
227
|
condSlotId,
|
|
226
228
|
outerLoopParam,
|
|
227
229
|
outerLoopParamBindings,
|
|
230
|
+
outerLoopIndex,
|
|
228
231
|
wrapOuter,
|
|
229
232
|
} = args
|
|
230
233
|
if (!innerLoops || innerLoops.length === 0) return []
|
|
@@ -232,10 +235,15 @@ export function buildBranchInnerLoopsPlan(
|
|
|
232
235
|
const plan: BranchInnerLoop[] = []
|
|
233
236
|
for (let i = 0; i < innerLoops.length; i++) {
|
|
234
237
|
const inner = innerLoops[i]
|
|
235
|
-
|
|
238
|
+
// #2865: an inner loop inside a conditional arm used to be dropped
|
|
239
|
+
// entirely (no `mapArray`, no hydration wiring at all) whenever its
|
|
240
|
+
// array didn't reference the outer loop's item — the same unsound
|
|
241
|
+
// gate `build-inner-loop.ts` used, just answered by skipping instead
|
|
242
|
+
// of falling back to a weaker static emission. Always build the plan.
|
|
243
|
+
if (!inner.template) continue
|
|
236
244
|
|
|
237
|
-
const wrapInner = (expr: string) => wrapLoopParamAsAccessor(expr, inner.param, inner.paramBindings)
|
|
238
|
-
const wrapBoth = (expr: string) => wrapLoopParamAsAccessor(wrapOuter(expr), inner.param, inner.paramBindings)
|
|
245
|
+
const wrapInner = (expr: string) => wrapLoopParamAsAccessor(expr, inner.param, inner.paramBindings, inner.index)
|
|
246
|
+
const wrapBoth = (expr: string) => wrapLoopParamAsAccessor(wrapOuter(expr), inner.param, inner.paramBindings, inner.index)
|
|
239
247
|
|
|
240
248
|
const csl = inner.containerSlotId
|
|
241
249
|
// Inner loop's container: host-side `bf="<slot>"` slot marker first,
|
|
@@ -251,7 +259,7 @@ export function buildBranchInnerLoopsPlan(
|
|
|
251
259
|
|
|
252
260
|
const { head: paramHead, unwrap: paramUnwrap } = destructureLoopParam(inner.param, inner.paramBindings)
|
|
253
261
|
const wrappedKey = inner.key
|
|
254
|
-
? wrapLoopParamAsAccessor(inner.key, inner.param, inner.paramBindings)
|
|
262
|
+
? wrapLoopParamAsAccessor(inner.key, inner.param, inner.paramBindings, inner.index)
|
|
255
263
|
: null
|
|
256
264
|
|
|
257
265
|
// Inner-wrap children IR recursively so nested component props (e.g.
|
|
@@ -327,11 +335,13 @@ export function buildBranchInnerLoopsPlan(
|
|
|
327
335
|
wrap: wrapBoth,
|
|
328
336
|
loopParam: inner.param,
|
|
329
337
|
loopParamBindings: inner.paramBindings,
|
|
338
|
+
loopIndex: inner.index,
|
|
330
339
|
}),
|
|
331
340
|
innerLoopParam: inner.param,
|
|
332
341
|
innerLoopParamBindings: inner.paramBindings,
|
|
333
342
|
outerLoopParam,
|
|
334
343
|
outerLoopParamBindings,
|
|
344
|
+
outerLoopIndex,
|
|
335
345
|
})
|
|
336
346
|
}
|
|
337
347
|
return plan
|
|
@@ -352,6 +362,8 @@ export interface BuildLoopChildConditionalsArgs {
|
|
|
352
362
|
loopParam: string
|
|
353
363
|
/** Loop param destructuring metadata. */
|
|
354
364
|
loopParamBindings?: readonly LoopParamBinding[]
|
|
365
|
+
/** Loop's index param name, when it declares one (#2861) — same role as `loopParam` above. */
|
|
366
|
+
loopIndex?: string | null
|
|
355
367
|
}
|
|
356
368
|
|
|
357
369
|
/**
|
|
@@ -360,7 +372,8 @@ export interface BuildLoopChildConditionalsArgs {
|
|
|
360
372
|
* recursion. Each conditional pre-builds:
|
|
361
373
|
*
|
|
362
374
|
* - the wrapped condition expression
|
|
363
|
-
* - the
|
|
375
|
+
* - the addCondAttr'd whenTrue / whenFalse template HTML (already
|
|
376
|
+
* loop-param-wrapped at IR render time, #2868)
|
|
364
377
|
* - per-arm `LoopChildArmPlan`: events / child components / inner
|
|
365
378
|
* loops / nested conditionals (recursion)
|
|
366
379
|
*
|
|
@@ -370,7 +383,7 @@ export interface BuildLoopChildConditionalsArgs {
|
|
|
370
383
|
export function buildLoopChildConditionalsPlan(
|
|
371
384
|
args: BuildLoopChildConditionalsArgs,
|
|
372
385
|
): LoopChildConditionalPlan[] {
|
|
373
|
-
const { conditionals, scopeVar, wrap, loopParam, loopParamBindings } = args
|
|
386
|
+
const { conditionals, scopeVar, wrap, loopParam, loopParamBindings, loopIndex } = args
|
|
374
387
|
if (!conditionals || conditionals.length === 0) return []
|
|
375
388
|
|
|
376
389
|
const plans: LoopChildConditionalPlan[] = []
|
|
@@ -379,13 +392,20 @@ export function buildLoopChildConditionalsPlan(
|
|
|
379
392
|
slotId: cond.slotId,
|
|
380
393
|
scopeVar,
|
|
381
394
|
wrappedCondition: wrap(cond.condition),
|
|
382
|
-
|
|
383
|
-
|
|
395
|
+
// whenTrueHtml/whenFalseHtml are already loop-param-wrapped for the
|
|
396
|
+
// full ancestor chain at IR render time (`irToHtmlTemplate`'s
|
|
397
|
+
// `loopParams`, collect-elements.ts) — do NOT re-wrap the rendered
|
|
398
|
+
// HTML string here (#2868: a word-boundary regex over assembled
|
|
399
|
+
// markup can match a bare tag name colliding with the param/index
|
|
400
|
+
// identifier, e.g. `<i>` -> `<i()>`).
|
|
401
|
+
whenTrueTemplateHtml: addCondAttrToTemplate(cond.whenTrueHtml, cond.slotId),
|
|
402
|
+
whenFalseTemplateHtml: addCondAttrToTemplate(cond.whenFalseHtml, cond.slotId),
|
|
384
403
|
whenTrueArm: buildLoopChildArmPlan({
|
|
385
404
|
branch: cond.whenTrue,
|
|
386
405
|
wrap,
|
|
387
406
|
loopParam,
|
|
388
407
|
loopParamBindings,
|
|
408
|
+
loopIndex,
|
|
389
409
|
condId: cond.slotId,
|
|
390
410
|
}),
|
|
391
411
|
whenFalseArm: buildLoopChildArmPlan({
|
|
@@ -393,8 +413,10 @@ export function buildLoopChildConditionalsPlan(
|
|
|
393
413
|
wrap,
|
|
394
414
|
loopParam,
|
|
395
415
|
loopParamBindings,
|
|
416
|
+
loopIndex,
|
|
396
417
|
condId: cond.slotId,
|
|
397
418
|
}),
|
|
419
|
+
...(cond.readsPreamble && { readsPreamble: true }),
|
|
398
420
|
})
|
|
399
421
|
}
|
|
400
422
|
return plans
|
|
@@ -429,6 +451,7 @@ export function buildArmAttrsPlan(
|
|
|
429
451
|
attrName: attr.attrName,
|
|
430
452
|
wrappedExpression: wrap(attr.expression),
|
|
431
453
|
meta: pickAttrMeta(attr),
|
|
454
|
+
...(attr.readsPreamble && { readsPreamble: true }),
|
|
432
455
|
})),
|
|
433
456
|
})
|
|
434
457
|
}
|
|
@@ -455,12 +478,14 @@ interface BuildLoopChildArmArgs {
|
|
|
455
478
|
wrap: (expr: string) => string
|
|
456
479
|
loopParam: string
|
|
457
480
|
loopParamBindings?: readonly LoopParamBinding[]
|
|
481
|
+
/** Loop's index param name, when it declares one (#2861). */
|
|
482
|
+
loopIndex?: string | null
|
|
458
483
|
/** The enclosing conditional's own slot id — threaded to `buildBranchInnerLoopsPlan`'s `condSlotId` (#2705). */
|
|
459
484
|
condId: string
|
|
460
485
|
}
|
|
461
486
|
|
|
462
487
|
function buildLoopChildArmPlan(args: BuildLoopChildArmArgs): LoopChildArmPlan {
|
|
463
|
-
const { branch, wrap, loopParam, loopParamBindings, condId } = args
|
|
488
|
+
const { branch, wrap, loopParam, loopParamBindings, loopIndex, condId } = args
|
|
464
489
|
return {
|
|
465
490
|
events: buildBranchEventBindingsPlan({
|
|
466
491
|
events: branch.events,
|
|
@@ -476,6 +501,7 @@ function buildLoopChildArmPlan(args: BuildLoopChildArmArgs): LoopChildArmPlan {
|
|
|
476
501
|
condSlotId: condId,
|
|
477
502
|
outerLoopParam: loopParam,
|
|
478
503
|
outerLoopParamBindings: loopParamBindings,
|
|
504
|
+
outerLoopIndex: loopIndex,
|
|
479
505
|
wrapOuter: wrap,
|
|
480
506
|
}),
|
|
481
507
|
nestedConditionals: buildLoopChildConditionalsPlan({
|
|
@@ -484,6 +510,7 @@ function buildLoopChildArmPlan(args: BuildLoopChildArmArgs): LoopChildArmPlan {
|
|
|
484
510
|
wrap,
|
|
485
511
|
loopParam,
|
|
486
512
|
loopParamBindings,
|
|
513
|
+
loopIndex,
|
|
487
514
|
}),
|
|
488
515
|
attrs: buildArmAttrsPlan(branch.reactiveAttrs, wrap),
|
|
489
516
|
texts: buildArmTextsPlan(branch.reactiveTexts, wrap),
|
|
@@ -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,116 @@
|
|
|
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
|
+
// `loop.templateIndexed` is a second structured render (index wrapped at
|
|
97
|
+
// IR time, `collect-elements.ts`) rather than a post-hoc regex pass over
|
|
98
|
+
// `loop.template` — a word-boundary regex over assembled HTML can match a
|
|
99
|
+
// bare tag name colliding with the index identifier (`<i>` -> `<i()>`,
|
|
100
|
+
// #2868). Falls back to `loop.template` for the rare shape that doesn't
|
|
101
|
+
// populate it (e.g. a flatMap projection loop).
|
|
102
|
+
const templateFinal = !lazyRow && loop.index
|
|
103
|
+
? (loop.templateIndexed ?? loop.template)
|
|
104
|
+
: loop.template
|
|
105
|
+
|
|
106
|
+
return {
|
|
107
|
+
indexParam,
|
|
108
|
+
paramHead,
|
|
109
|
+
paramUnwrap,
|
|
110
|
+
preambleRegions,
|
|
111
|
+
lazyRow,
|
|
112
|
+
mapPreambleWrapped: mapPreambleWrappedFinal,
|
|
113
|
+
template: templateFinal,
|
|
114
|
+
wrapItem,
|
|
115
|
+
}
|
|
116
|
+
}
|