@barefootjs/jsx 0.35.0 → 0.35.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/analyzer-context.d.ts.map +1 -1
- package/dist/index.js +731 -691
- package/dist/ir-to-client-js/collect-elements.d.ts +22 -2
- package/dist/ir-to-client-js/collect-elements.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/plan/build-composite-loop.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/plan/build-inner-loop.d.ts +14 -2
- package/dist/ir-to-client-js/control-flow/plan/build-inner-loop.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/plan/build-lazy-row.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/plan/build-loop-child-arm.d.ts +6 -1
- package/dist/ir-to-client-js/control-flow/plan/build-loop-child-arm.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/plan/build-plain-row.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/plan/build-reactive-effects.d.ts +3 -2
- package/dist/ir-to-client-js/control-flow/plan/build-reactive-effects.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/plan/inner-loop.d.ts +24 -44
- package/dist/ir-to-client-js/control-flow/plan/inner-loop.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/plan/loop-child-arm.d.ts +14 -0
- package/dist/ir-to-client-js/control-flow/plan/loop-child-arm.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/stringify/inner-loop.d.ts +6 -14
- package/dist/ir-to-client-js/control-flow/stringify/inner-loop.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/stringify/lazy-row.d.ts +1 -3
- package/dist/ir-to-client-js/control-flow/stringify/lazy-row.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/stringify/loop-child-arm.d.ts +19 -3
- package/dist/ir-to-client-js/control-flow/stringify/loop-child-arm.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/stringify/loop.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/stringify/reactive-effects.d.ts +5 -1
- package/dist/ir-to-client-js/control-flow/stringify/reactive-effects.d.ts.map +1 -1
- package/dist/ir-to-client-js/emit-reactive.d.ts +54 -0
- package/dist/ir-to-client-js/emit-reactive.d.ts.map +1 -1
- package/dist/ir-to-client-js/html-template.d.ts.map +1 -1
- package/dist/ir-to-client-js/reactivity.d.ts +62 -18
- package/dist/ir-to-client-js/reactivity.d.ts.map +1 -1
- package/dist/ir-to-client-js/types.d.ts +18 -2
- package/dist/ir-to-client-js/types.d.ts.map +1 -1
- package/dist/reactivity-checker.d.ts +18 -0
- package/dist/reactivity-checker.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/__tests__/__snapshots__/doc-examples.test.ts.snap +128 -10
- package/src/__tests__/binding-scope-ratchet.test.ts +26 -3
- package/src/__tests__/child-prop-fallback-wrap.test.ts +4 -2
- package/src/__tests__/compiler-stress-1244.test.ts +8 -8
- package/src/__tests__/issue-2868-cond-arm-tag-collision.test.ts +185 -0
- package/src/__tests__/issue-2869-attr-dedup-guard.test.ts +141 -0
- package/src/__tests__/loop-index-reactivity.test.ts +208 -0
- package/src/__tests__/map-body-brand-checker.test.ts +176 -0
- package/src/__tests__/nested-loop-index-param.test.ts +27 -5
- package/src/__tests__/nested-loop-non-outer-reactivity.test.ts +177 -0
- package/src/__tests__/nested-loop-plain.test.ts +39 -25
- package/src/__tests__/nested-loop-reactive-attrs.test.ts +12 -4
- package/src/__tests__/preamble-branch-arm-rerun.test.ts +135 -0
- package/src/__tests__/style-css-var-reactive.test.ts +4 -1
- package/src/analyzer-context.ts +12 -10
- package/src/ir-to-client-js/collect-elements.ts +110 -32
- package/src/ir-to-client-js/control-flow/plan/build-composite-loop.ts +2 -0
- package/src/ir-to-client-js/control-flow/plan/build-inner-loop.ts +28 -51
- package/src/ir-to-client-js/control-flow/plan/build-lazy-row.ts +7 -2
- package/src/ir-to-client-js/control-flow/plan/build-loop-child-arm.ts +33 -6
- package/src/ir-to-client-js/control-flow/plan/build-plain-row.ts +7 -1
- package/src/ir-to-client-js/control-flow/plan/build-reactive-effects.ts +18 -6
- package/src/ir-to-client-js/control-flow/plan/inner-loop.ts +24 -45
- package/src/ir-to-client-js/control-flow/plan/loop-child-arm.ts +14 -0
- package/src/ir-to-client-js/control-flow/stringify/inner-loop.ts +20 -66
- package/src/ir-to-client-js/control-flow/stringify/insert.ts +4 -2
- package/src/ir-to-client-js/control-flow/stringify/lazy-row.ts +12 -15
- package/src/ir-to-client-js/control-flow/stringify/loop-child-arm.ts +53 -11
- package/src/ir-to-client-js/control-flow/stringify/loop.ts +4 -2
- package/src/ir-to-client-js/control-flow/stringify/reactive-effects.ts +29 -18
- package/src/ir-to-client-js/emit-reactive.ts +88 -5
- package/src/ir-to-client-js/html-template.ts +9 -1
- package/src/ir-to-client-js/reactivity.ts +73 -27
- package/src/ir-to-client-js/types.ts +19 -3
- package/src/jsx-to-ir.ts +21 -4
- package/src/reactivity-checker.ts +60 -0
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Codegen-shape pins for the loop-row branch-arm preamble gap (#2596/#2447
|
|
3
|
+
* follow-up, found while discussing #2869's aftermath). Companion to the
|
|
4
|
+
* behavioral test in
|
|
5
|
+
* `packages/client/__tests__/runtime/preamble-branch-attr-staleness.test.ts`,
|
|
6
|
+
* which is the one that actually proves the fix works end to end — this
|
|
7
|
+
* file pins the exact emitted shape so a future refactor of
|
|
8
|
+
* `stringifyLoopChildArm`/`stringifyBranchReactiveAttrs`/
|
|
9
|
+
* `stringifyLoopChildConditional` can't silently drop the preamble re-run
|
|
10
|
+
* again.
|
|
11
|
+
*
|
|
12
|
+
* Two sites were fixed together: a reactive ATTRIBUTE inside a loop row's
|
|
13
|
+
* branch conditional that reads a `.map()` preamble local (case a), and a
|
|
14
|
+
* NESTED conditional's own CONDITION expression doing the same (case b,
|
|
15
|
+
* #2596 one nesting level deeper than the already-fixed outer-conditional
|
|
16
|
+
* case pinned by `preamble-conditional-reactivity.test.ts`). Case c is the
|
|
17
|
+
* negative control: no preamble local in play, so nothing should be
|
|
18
|
+
* injected.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { describe, test, expect } from 'bun:test'
|
|
22
|
+
import { compileJSX } from '../compiler'
|
|
23
|
+
import { TestAdapter } from '../adapters/test-adapter'
|
|
24
|
+
|
|
25
|
+
const adapter = new TestAdapter()
|
|
26
|
+
|
|
27
|
+
function clientJsFor(source: string, filename: string): string {
|
|
28
|
+
const result = compileJSX(source, filename, { adapter })
|
|
29
|
+
expect(result.errors).toHaveLength(0)
|
|
30
|
+
const clientJs = result.files.find(f => f.type === 'clientJs')?.content
|
|
31
|
+
if (!clientJs) throw new Error('No client JS emitted')
|
|
32
|
+
return clientJs
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
describe('#2596/#2447 follow-up — preamble re-run inside a loop-row branch arm', () => {
|
|
36
|
+
test('(a) an attribute inside a branch arm re-runs the preamble before reading it', () => {
|
|
37
|
+
const source = `'use client'
|
|
38
|
+
import { createSignal } from '@barefootjs/client'
|
|
39
|
+
type Row = { id: number; tier: string }
|
|
40
|
+
export function List() {
|
|
41
|
+
const [visible] = createSignal(true)
|
|
42
|
+
const [rows] = createSignal<Row[]>([{ id: 1, tier: 'gold' }])
|
|
43
|
+
return (
|
|
44
|
+
<ul>
|
|
45
|
+
{rows().map(row => {
|
|
46
|
+
const cls = 'tier-' + row.tier
|
|
47
|
+
return (
|
|
48
|
+
<li key={row.id}>
|
|
49
|
+
{visible() ? <span class={cls} id="target">{row.tier}</span> : <span>x</span>}
|
|
50
|
+
</li>
|
|
51
|
+
)
|
|
52
|
+
})}
|
|
53
|
+
</ul>
|
|
54
|
+
)
|
|
55
|
+
}
|
|
56
|
+
`
|
|
57
|
+
const js = clientJsFor(source, 'List.tsx')
|
|
58
|
+
|
|
59
|
+
// Pin the eager per-row mapArray path (not lazy-row, not relevant here
|
|
60
|
+
// but keeps this fixture stable if eligibility ever widens).
|
|
61
|
+
expect(js).toContain('mapArray(')
|
|
62
|
+
|
|
63
|
+
// The outer conditional's own getter is NOT preamble-guarded (`visible`
|
|
64
|
+
// is a plain signal, no preamble read) — stays bare.
|
|
65
|
+
expect(js).toContain("insert(__el, 's0', () => visible(), {")
|
|
66
|
+
|
|
67
|
+
// The arm's attr effect re-runs the preamble before reading `cls`.
|
|
68
|
+
const disposerStart = js.indexOf('const __disposers = []')
|
|
69
|
+
const disposerEnd = js.indexOf('return () => __disposers.forEach')
|
|
70
|
+
const arm = js.slice(disposerStart, disposerEnd)
|
|
71
|
+
const preambleAt = arm.indexOf("const cls = 'tier-' + row().tier;")
|
|
72
|
+
const readAt = arm.indexOf('const __x = cls')
|
|
73
|
+
expect(preambleAt).toBeGreaterThanOrEqual(0)
|
|
74
|
+
expect(readAt).toBeGreaterThan(preambleAt)
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
test('(b) a nested conditional inside a branch arm re-runs the preamble in its own condition getter', () => {
|
|
78
|
+
const source = `'use client'
|
|
79
|
+
import { createSignal } from '@barefootjs/client'
|
|
80
|
+
type Row = { id: number; tier: string }
|
|
81
|
+
export function List() {
|
|
82
|
+
const [visible] = createSignal(true)
|
|
83
|
+
const [selected] = createSignal(1)
|
|
84
|
+
const [rows] = createSignal<Row[]>([{ id: 1, tier: 'gold' }])
|
|
85
|
+
return (
|
|
86
|
+
<ul>
|
|
87
|
+
{rows().map(row => {
|
|
88
|
+
const cls = 'tier-' + row.tier
|
|
89
|
+
const on = selected() === row.id
|
|
90
|
+
return (
|
|
91
|
+
<li key={row.id}>
|
|
92
|
+
{visible() ? <div><span>{cls}</span>{on ? <em id="yes">yes</em> : <em id="no">no</em>}</div> : <span>x</span>}
|
|
93
|
+
</li>
|
|
94
|
+
)
|
|
95
|
+
})}
|
|
96
|
+
</ul>
|
|
97
|
+
)
|
|
98
|
+
}
|
|
99
|
+
`
|
|
100
|
+
const js = clientJsFor(source, 'List2.tsx')
|
|
101
|
+
|
|
102
|
+
expect(js).toMatch(
|
|
103
|
+
/insert\(__branchScope, 's\d+', \(\) => \{ const cls = 'tier-' \+ row\(\)\.tier; const on = selected\(\) === row\(\)\.id;; return \(on\) \}, \{/,
|
|
104
|
+
)
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
test('(c) negative control — no preamble local in play, nothing injected', () => {
|
|
108
|
+
const source = `'use client'
|
|
109
|
+
import { createSignal } from '@barefootjs/client'
|
|
110
|
+
type Row = { id: number; tier: string }
|
|
111
|
+
export function List() {
|
|
112
|
+
const [visible] = createSignal(true)
|
|
113
|
+
const [rows] = createSignal<Row[]>([{ id: 1, tier: 'gold' }])
|
|
114
|
+
return (
|
|
115
|
+
<ul>
|
|
116
|
+
{rows().map(row => (
|
|
117
|
+
<li key={row.id}>
|
|
118
|
+
{visible() ? <span class={row.tier} id="target">{row.tier}</span> : <span>x</span>}
|
|
119
|
+
</li>
|
|
120
|
+
))}
|
|
121
|
+
</ul>
|
|
122
|
+
)
|
|
123
|
+
}
|
|
124
|
+
`
|
|
125
|
+
const js = clientJsFor(source, 'List3.tsx')
|
|
126
|
+
|
|
127
|
+
expect(js).not.toContain('const cls =')
|
|
128
|
+
// The attr effect still exists and still writes — just with no
|
|
129
|
+
// preamble re-run ahead of it, since there is nothing to re-run.
|
|
130
|
+
const disposerStart = js.indexOf('const __disposers = []')
|
|
131
|
+
const disposerEnd = js.indexOf('return () => __disposers.forEach')
|
|
132
|
+
const arm = js.slice(disposerStart, disposerEnd)
|
|
133
|
+
expect(arm).toContain("setAttribute('class'")
|
|
134
|
+
})
|
|
135
|
+
})
|
|
@@ -113,7 +113,10 @@ describe('reactive CSS custom properties (#135)', () => {
|
|
|
113
113
|
|
|
114
114
|
// Both reactive sinks live in the init body — one assigning the DOM
|
|
115
115
|
// `disabled` property, the other writing the `aria-busy` attribute.
|
|
116
|
-
|
|
116
|
+
// The write reads a dedup-guarded temp (`__x`, #2869), not `busy()`
|
|
117
|
+
// inline — the computed value is still `busy()`'s result either way.
|
|
118
|
+
expect(content).toMatch(/const __x = busy\(\)/)
|
|
119
|
+
expect(content).toMatch(/\.disabled\s*=\s*!!\(__x\)/)
|
|
117
120
|
expect(content).toContain("setAttribute('aria-busy'")
|
|
118
121
|
})
|
|
119
122
|
})
|
package/src/analyzer-context.ts
CHANGED
|
@@ -26,6 +26,7 @@ import type {
|
|
|
26
26
|
} from './types.ts'
|
|
27
27
|
import { type ExcludeRange, collectAllTypeRanges, reconstructWithoutTypes } from './strip-types.ts'
|
|
28
28
|
import type { CallbackBodyAcceptor } from './adapters/interface.ts'
|
|
29
|
+
import { nodeContainsJsx } from './reactivity-checker.ts'
|
|
29
30
|
|
|
30
31
|
/**
|
|
31
32
|
* Deferred info for BF043 (props destructuring warning).
|
|
@@ -318,10 +319,17 @@ export function createAnalyzerContext(
|
|
|
318
319
|
// travel as structured segments, never as raw text. Every getJS call
|
|
319
320
|
// site shares this contract; the env gate keeps the subtree walk off
|
|
320
321
|
// the production hot path (the trichotomy harness enables it). Scoped
|
|
321
|
-
// to error
|
|
322
|
-
// take degraded fallback paths whose
|
|
323
|
-
// the invariant this trips on is
|
|
324
|
-
|
|
322
|
+
// to compiles with no `error`-severity diagnostic: a compile that
|
|
323
|
+
// already refused loudly may take degraded fallback paths whose
|
|
324
|
+
// artifacts are gated by the error — the invariant this trips on is
|
|
325
|
+
// the SILENT leak. A `warning` (e.g. BF043 props-destructuring) does
|
|
326
|
+
// not gate anything, so it must not silently disarm this assertion too
|
|
327
|
+
// (#2867) — only `error` severity does.
|
|
328
|
+
if (
|
|
329
|
+
process.env.BF_ASSERT_NO_JSX_IN_GETJS === '1' &&
|
|
330
|
+
!this.errors.some((e) => e.severity === 'error') &&
|
|
331
|
+
nodeContainsJsx(node)
|
|
332
|
+
) {
|
|
325
333
|
throw new Error(
|
|
326
334
|
'getJS() called on a JSX-bearing node — raw JSX must never be spliced ' +
|
|
327
335
|
'into emitted output. Carry mixed content as structured segments ' +
|
|
@@ -336,12 +344,6 @@ export function createAnalyzerContext(
|
|
|
336
344
|
}
|
|
337
345
|
}
|
|
338
346
|
|
|
339
|
-
/** Subtree JSX check for the test-gated getJS assertion above. */
|
|
340
|
-
function nodeContainsJsx(node: ts.Node): boolean {
|
|
341
|
-
if (ts.isJsxElement(node) || ts.isJsxSelfClosingElement(node) || ts.isJsxFragment(node)) return true
|
|
342
|
-
return ts.forEachChild(node, nodeContainsJsx) ?? false
|
|
343
|
-
}
|
|
344
|
-
|
|
345
347
|
// =============================================================================
|
|
346
348
|
// Source Location Helper
|
|
347
349
|
// =============================================================================
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
import { type IRNode, type IRElement, type IRComponent, type IRLoop, type IRProp, pickAttrMetaFromIR } from '../types.ts'
|
|
6
6
|
import type { ClientJsContext, ConditionalBranchChildComponent, ConditionalBranchReactiveAttr, BranchLoop, ConditionalBranchTextEffect, ConditionalElement, LoopChildBindings, LoopChildBranchSummary, LoopChildConditional, LoopOffset, NestedLoop } from './types.ts'
|
|
7
|
-
import { attrValueToString, freeIdsFromRefs, quotePropName, PROPS_PARAM } from './utils.ts'
|
|
7
|
+
import { attrValueToString, freeIdsFromRefs, quotePropName, PROPS_PARAM, type LoopParamSpec } from './utils.ts'
|
|
8
8
|
import { classifyReactivity, decideWrapForAttr, decideWrapForChildProp, decideWrapFromAstFlags, collectEventHandlersFromIR, collectConditionalBranchEvents, collectConditionalBranchRefs, collectConditionalBranchChildComponents, collectLoopChildEventsWithNesting, collectLoopChildReactiveAttrs, collectLoopChildReactiveTexts, collectLoopChildRefs, emptyLoopChildBindings, buildLoopRowScope, anyNameIn } from './reactivity.ts'
|
|
9
9
|
import { irToHtmlTemplate, irToPlaceholderTemplate, irChildrenToJsExpr, jsxChildrenPropGetterExpr, buildLoopSkeletonTemplate, computeSkeletonSlotPaths, renderFlatMapClientBody, renderFlatMapProjectionClientBody, flatMapCallbackHasKeyedLeaf, type SkeletonSlotPaths } from './html-template.ts'
|
|
10
10
|
import { detectRootNamespaceWrapTag } from './control-flow/stringify/template-parse.ts'
|
|
@@ -12,8 +12,8 @@ import { expandDynamicPropValue, expandConstantForReactivity, resolveRestSpreadO
|
|
|
12
12
|
import { extractFreeIdentifiersFromText } from './csr-substitute.ts'
|
|
13
13
|
import { walkIR, stopAt } from './walker.ts'
|
|
14
14
|
import { buildLoopChainExpr } from '../loop-chain.ts'
|
|
15
|
-
import { identifierPattern } from '../identifier-pattern.ts'
|
|
16
15
|
import { classifyDOMProp } from '@barefootjs/shared'
|
|
16
|
+
import type { BindingScope } from '../scope/binding-scope.ts'
|
|
17
17
|
|
|
18
18
|
/** Expressions that render nothing (0 DOM nodes) — `&&` / `?:` empty branches. */
|
|
19
19
|
const EMPTY_RENDER_EXPRS = new Set(['null', 'undefined', 'false', "''", '""', '``'])
|
|
@@ -272,7 +272,7 @@ export const branchInnerLoopOptions: CollectInnerLoopsOptions = {
|
|
|
272
272
|
export function collectInnerLoops(
|
|
273
273
|
nodes: IRNode[],
|
|
274
274
|
siblingOffsets: Map<IRLoop, IRNode[]>,
|
|
275
|
-
outerLoopParam?: string,
|
|
275
|
+
outerLoopParam?: string | LoopParamSpec,
|
|
276
276
|
ctx?: ClientJsContext,
|
|
277
277
|
options?: CollectInnerLoopsOptions,
|
|
278
278
|
): NestedLoop[] {
|
|
@@ -280,12 +280,29 @@ export function collectInnerLoops(
|
|
|
280
280
|
parentSlotId: string | null
|
|
281
281
|
depth: number
|
|
282
282
|
insideCond: boolean
|
|
283
|
+
/**
|
|
284
|
+
* Every ancestor loop's own item/index binding, innermost-first
|
|
285
|
+
* (#2861) — `undefined` above the outermost loop. Threaded (not just
|
|
286
|
+
* captured once from `outerLoopParam`) so a THIRD-level-deep loop's row
|
|
287
|
+
* sees BOTH its grandparent's and its immediate parent's bindings, not
|
|
288
|
+
* only the outermost one: each `loop:` visit below pushes its own
|
|
289
|
+
* frame via `buildLoopRowScope(..., scope.bindingScope)` before
|
|
290
|
+
* descending into that loop's own children.
|
|
291
|
+
*/
|
|
292
|
+
bindingScope: BindingScope | undefined
|
|
283
293
|
}
|
|
284
294
|
const result: NestedLoop[] = []
|
|
285
295
|
const flat = options?.flatBranchMode === true
|
|
286
296
|
const fixedDepth = options?.templateDepth
|
|
287
297
|
const collectBindings = options?.collectItemBindings === true
|
|
288
|
-
|
|
298
|
+
// Normalize to a spec regardless of which shape the caller passed (#2868
|
|
299
|
+
// callers always pass a full spec; a bare string carries no bindings/index
|
|
300
|
+
// of its own) — one shape feeds both the reactivity scope below and
|
|
301
|
+
// `loopParamsForTemplate` inside the `loop:` visitor.
|
|
302
|
+
const outerSpec: LoopParamSpec | undefined =
|
|
303
|
+
typeof outerLoopParam === 'string' ? { param: outerLoopParam } : outerLoopParam
|
|
304
|
+
const outerScope = buildLoopRowScope(outerSpec?.param, outerSpec?.bindings, undefined, outerSpec?.index)
|
|
305
|
+
const initialScope: Scope = { parentSlotId: null, depth: 0, insideCond: false, bindingScope: outerScope }
|
|
289
306
|
|
|
290
307
|
for (const root of nodes) {
|
|
291
308
|
walkIR<Scope>(root, initialScope, {
|
|
@@ -308,14 +325,10 @@ export function collectInnerLoops(
|
|
|
308
325
|
// Pass loopParams so expressions are wrapped at generation time (not post-hoc regex).
|
|
309
326
|
// Forward destructured bindings (#951) so inner-loop template
|
|
310
327
|
// references to the destructured locals are rewritten.
|
|
311
|
-
const loopParamsForTemplate =
|
|
312
|
-
? [
|
|
328
|
+
const loopParamsForTemplate = outerSpec
|
|
329
|
+
? [outerSpec, { param: n.param, bindings: n.paramBindings, index: n.index }]
|
|
313
330
|
: undefined
|
|
314
331
|
const template = n.children.map(c => irToPlaceholderTemplate(c, undefined, emitDepth, loopParamsForTemplate)).join('')
|
|
315
|
-
// Check if array expression references the outer loop param
|
|
316
|
-
const refsOuter = outerLoopParam
|
|
317
|
-
? identifierPattern(outerLoopParam).test(n.array)
|
|
318
|
-
: false
|
|
319
332
|
// Per-item bindings for inner loop body, collected uniformly when
|
|
320
333
|
// ctx is available: reactiveTexts / reactiveAttrs / refs are each
|
|
321
334
|
// classified against the loop's OWN param via `classifyReactivity`,
|
|
@@ -349,8 +362,8 @@ export function collectInnerLoops(
|
|
|
349
362
|
// guarantees the branch's marker is mounted the moment this
|
|
350
363
|
// effect first runs (issue-2706-nested-loop-conditional-slot
|
|
351
364
|
// .test.ts's original repro).
|
|
352
|
-
bindings.reactiveTexts.push(...collectLoopChildReactiveTexts(child, ctx, n.param, n.paramBindings, true, innerPreambleNames, n.index))
|
|
353
|
-
bindings.reactiveAttrs.push(...collectLoopChildReactiveAttrs(child, ctx, n.param, n.paramBindings, true, innerPreambleNames, n.index))
|
|
365
|
+
bindings.reactiveTexts.push(...collectLoopChildReactiveTexts(child, ctx, n.param, n.paramBindings, true, innerPreambleNames, n.index, scope.bindingScope))
|
|
366
|
+
bindings.reactiveAttrs.push(...collectLoopChildReactiveAttrs(child, ctx, n.param, n.paramBindings, true, innerPreambleNames, n.index, scope.bindingScope))
|
|
354
367
|
bindings.refs.push(...collectLoopChildRefs(child))
|
|
355
368
|
}
|
|
356
369
|
bindings.conditionals.push(...collectLoopChildConditionals(
|
|
@@ -361,6 +374,12 @@ export function collectInnerLoops(
|
|
|
361
374
|
n.paramBindings,
|
|
362
375
|
innerPreambleNames,
|
|
363
376
|
n.index,
|
|
377
|
+
// Render a branch's HTML against the FULL ancestor chain (outer
|
|
378
|
+
// loop(s) + this loop's own param), not just this loop's own
|
|
379
|
+
// param — an inner-loop conditional arm can read an outer
|
|
380
|
+
// loop's item/index too (#2868).
|
|
381
|
+
loopParamsForTemplate,
|
|
382
|
+
scope.bindingScope,
|
|
364
383
|
))
|
|
365
384
|
}
|
|
366
385
|
|
|
@@ -412,7 +431,6 @@ export function collectInnerLoops(
|
|
|
412
431
|
containerSlotId: scope.parentSlotId,
|
|
413
432
|
template,
|
|
414
433
|
preamble: n.preamble,
|
|
415
|
-
refsOuterParam: refsOuter,
|
|
416
434
|
childComponents,
|
|
417
435
|
insideConditional: !flat && scope.insideCond ? true : undefined,
|
|
418
436
|
offset: flat ? undefined : resolveLoopOffset(siblingOffsets.get(n)),
|
|
@@ -420,7 +438,14 @@ export function collectInnerLoops(
|
|
|
420
438
|
})
|
|
421
439
|
// Branch-mode callers handle deeper nesting via their own collection paths.
|
|
422
440
|
if (!flat) {
|
|
423
|
-
|
|
441
|
+
// #2861: push THIS loop's own item/index frame onto the scope
|
|
442
|
+
// stack before descending, so a loop found deeper still (depth 2+)
|
|
443
|
+
// sees every ancestor's bindings, not just the outermost one.
|
|
444
|
+
descend({
|
|
445
|
+
...scope,
|
|
446
|
+
depth: scope.depth + 1,
|
|
447
|
+
bindingScope: buildLoopRowScope(n.param, n.paramBindings, innerPreambleNames, n.index, scope.bindingScope),
|
|
448
|
+
})
|
|
424
449
|
}
|
|
425
450
|
},
|
|
426
451
|
// fragment / provider / async auto-descend with the same scope.
|
|
@@ -455,7 +480,12 @@ function decideLoopRendering(
|
|
|
455
480
|
// loop. These only surface for static arrays (gated at the call site via
|
|
456
481
|
// `isStaticArray && innerLoops.length`) so dynamic child-component loops —
|
|
457
482
|
// which render through `createComponent` — are unaffected.
|
|
458
|
-
const innerLoops = collectInnerLoops(
|
|
483
|
+
const innerLoops = collectInnerLoops(
|
|
484
|
+
loop.children,
|
|
485
|
+
siblingOffsets,
|
|
486
|
+
{ param: loop.param, bindings: loop.paramBindings, index: loop.index },
|
|
487
|
+
ctx,
|
|
488
|
+
)
|
|
459
489
|
const hasInnerLoops = (innerLoops?.length ?? 0) > 0
|
|
460
490
|
const useElementReconciliation =
|
|
461
491
|
!loop.childComponent && !loop.isStaticArray && (hasNestedComps || hasInnerLoops)
|
|
@@ -727,6 +757,7 @@ export function collectElements(
|
|
|
727
757
|
: decideLoopRendering(l, siblingOffsets, ctx)
|
|
728
758
|
|
|
729
759
|
let template = ''
|
|
760
|
+
let templateIndexed: string | undefined
|
|
730
761
|
let staticItemTemplate: string | undefined
|
|
731
762
|
let skeletonTemplate: string | undefined
|
|
732
763
|
let skeletonPaths: SkeletonSlotPaths | undefined
|
|
@@ -771,6 +802,18 @@ export function collectElements(
|
|
|
771
802
|
template = useElementReconciliation
|
|
772
803
|
? irToPlaceholderTemplate(l.children[0], resolveRestSpreadNames(ctx), 0, loopParamSpec)
|
|
773
804
|
: irToHtmlTemplate(l.children[0], resolveRestSpreadNames(ctx), 0, loopParamSpec)
|
|
805
|
+
// Second structured render, WITH the index wrapped too — used by
|
|
806
|
+
// `build-plain-row.ts` once lazy eligibility rules out the lazy row
|
|
807
|
+
// plan (which needs `template` above, index-unwrapped, verbatim).
|
|
808
|
+
// Rendered here (IR level) rather than via a post-hoc regex pass
|
|
809
|
+
// over `template`, which can corrupt a tag name colliding with the
|
|
810
|
+
// index identifier (`<i>` -> `<i()>`, #2868).
|
|
811
|
+
if (l.index) {
|
|
812
|
+
const loopParamSpecIndexed = [{ param: l.param, bindings: l.paramBindings, index: l.index }]
|
|
813
|
+
templateIndexed = useElementReconciliation
|
|
814
|
+
? irToPlaceholderTemplate(l.children[0], resolveRestSpreadNames(ctx), 0, loopParamSpecIndexed)
|
|
815
|
+
: irToHtmlTemplate(l.children[0], resolveRestSpreadNames(ctx), 0, loopParamSpecIndexed)
|
|
816
|
+
}
|
|
774
817
|
// Static-array loops emit a `forEach((param, idx) => ...)` whose body
|
|
775
818
|
// references the destructured param directly — `__bfItem()` is not in
|
|
776
819
|
// scope there. Build a second template that skips the loop-param
|
|
@@ -827,6 +870,7 @@ export function collectElements(
|
|
|
827
870
|
iterationShape: l.iterationShape,
|
|
828
871
|
objectIteration: l.objectIteration,
|
|
829
872
|
template,
|
|
873
|
+
templateIndexed,
|
|
830
874
|
staticItemTemplate,
|
|
831
875
|
skeletonTemplate,
|
|
832
876
|
skeletonPaths,
|
|
@@ -1177,6 +1221,7 @@ function collectBranchLoops(
|
|
|
1177
1221
|
// keeping the template consistent with reactive effect expressions that
|
|
1178
1222
|
// use `param()` to read the current item value.
|
|
1179
1223
|
let childTemplate: string
|
|
1224
|
+
let childTemplateIndexed: string | undefined
|
|
1180
1225
|
// NOT `index: n.index` here — see the identical note on `loopParamSpec`
|
|
1181
1226
|
// above; `BranchLoop.template` is reused verbatim by the lazy row plan
|
|
1182
1227
|
// too (`callSite: 'branch-plain'`), and `buildBranchLoopPlan` applies
|
|
@@ -1189,6 +1234,15 @@ function collectBranchLoops(
|
|
|
1189
1234
|
} else {
|
|
1190
1235
|
childTemplate = n.children.map(c => irToHtmlTemplate(c, undefined, 0, branchLoopParamSpec)).join('')
|
|
1191
1236
|
}
|
|
1237
|
+
// Second structured render, WITH the index wrapped too — see the
|
|
1238
|
+
// identical `templateIndexed` note on the top-level loop path above
|
|
1239
|
+
// (#2868).
|
|
1240
|
+
if (n.index && !projectionInner) {
|
|
1241
|
+
const branchLoopParamSpecIndexed = [{ param: n.param, bindings: n.paramBindings, index: n.index }]
|
|
1242
|
+
childTemplateIndexed = useElementReconciliation && n.children[0]
|
|
1243
|
+
? irToPlaceholderTemplate(n.children[0], restNames, 0, branchLoopParamSpecIndexed)
|
|
1244
|
+
: n.children.map(c => irToHtmlTemplate(c, undefined, 0, branchLoopParamSpecIndexed)).join('')
|
|
1245
|
+
}
|
|
1192
1246
|
|
|
1193
1247
|
// Collect per-item bindings (events, reactive attrs/texts, refs,
|
|
1194
1248
|
// conditionals) for ALL branch loops — simple loops also need
|
|
@@ -1214,6 +1268,7 @@ function collectBranchLoops(
|
|
|
1214
1268
|
iterationShape: n.iterationShape,
|
|
1215
1269
|
objectIteration: n.objectIteration,
|
|
1216
1270
|
template: childTemplate,
|
|
1271
|
+
templateIndexed: childTemplateIndexed,
|
|
1217
1272
|
containerSlotId: containerSlot,
|
|
1218
1273
|
preamble: n.preamble,
|
|
1219
1274
|
preambleRegions: n.preambleRegions,
|
|
@@ -1441,24 +1496,41 @@ export function collectLoopChildConditionals(
|
|
|
1441
1496
|
* `collectLoopChildBindings`'s doc comment (Copilot review on #2595).
|
|
1442
1497
|
*/
|
|
1443
1498
|
loopIndex?: string | null,
|
|
1499
|
+
/**
|
|
1500
|
+
* The full ancestor loop-param chain to render branch HTML against
|
|
1501
|
+
* (outermost first), when known — e.g. `[outerSpec, innerSpec]` for a
|
|
1502
|
+
* conditional inside an inner `.map()`. Defaults to just
|
|
1503
|
+
* `[{ param: loopParam, bindings: loopParamBindings, index: loopIndex }]`
|
|
1504
|
+
* (this function's own single-loop scope) when omitted, which is correct
|
|
1505
|
+
* for every caller except `collectInnerLoops`'s inner-loop handler, which
|
|
1506
|
+
* passes the full chain so an inner loop's conditional arm can also
|
|
1507
|
+
* reference an OUTER loop's item/index (#2868). Rendering the branch's
|
|
1508
|
+
* HTML against the full chain at IR time — rather than re-wrapping the
|
|
1509
|
+
* assembled HTML string with a word-boundary regex afterward — is also
|
|
1510
|
+
* what makes it safe for a branch root tag name to collide with a
|
|
1511
|
+
* param/index identifier (`<i>`, `<b>`, …): `irToHtmlTemplate` only wraps
|
|
1512
|
+
* JS expression positions, never tag names.
|
|
1513
|
+
*/
|
|
1514
|
+
loopParams?: ReadonlyArray<string | LoopParamSpec>,
|
|
1515
|
+
/** Enclosing loop's own scope, for a NESTED loop (#2861) — see `buildLoopRowScope`. */
|
|
1516
|
+
parentScope?: BindingScope,
|
|
1444
1517
|
): LoopChildConditional[] {
|
|
1445
1518
|
const conditionals: LoopChildConditional[] = []
|
|
1446
|
-
const scope = buildLoopRowScope(loopParam, loopParamBindings, preambleNames, loopIndex)
|
|
1519
|
+
const scope = buildLoopRowScope(loopParam, loopParamBindings, preambleNames, loopIndex, parentScope)
|
|
1447
1520
|
|
|
1448
1521
|
// Widen the source-level "references loop param" check so destructured
|
|
1449
1522
|
// callbacks fire too — the pattern text `[, cfg]` never word-matches on
|
|
1450
1523
|
// bare `cfg` but individual binding names do (#951). Consumes a
|
|
1451
1524
|
// pre-computed `Set<string>` of free identifiers (#1267) rather than
|
|
1452
1525
|
// running word-boundary regex on the expression text.
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
}
|
|
1526
|
+
//
|
|
1527
|
+
// #2861: also checks the loop's INDEX binding, not just item/destructure
|
|
1528
|
+
// — `scope.valueBoundNames()` already unions all three (the same query
|
|
1529
|
+
// Phase 1's `referencesLoopParam` reads to grant a slot at all), so a
|
|
1530
|
+
// condition referencing only the index (e.g. `{i % 2 === 0 ? … : null}`)
|
|
1531
|
+
// no longer dies at this pre-gate before `classifyReactivity` even runs.
|
|
1532
|
+
const refsAnyBindingViaFreeIds = (freeIds: ReadonlySet<string>): boolean =>
|
|
1533
|
+
scope !== undefined && anyNameIn(scope.valueBoundNames(), freeIds)
|
|
1462
1534
|
|
|
1463
1535
|
walkIR(node, null, {
|
|
1464
1536
|
// element / fragment / component / provider auto-descend with same scope.
|
|
@@ -1489,13 +1561,13 @@ export function collectLoopChildConditionals(
|
|
|
1489
1561
|
preambleNames !== undefined &&
|
|
1490
1562
|
preambleNames.size > 0 &&
|
|
1491
1563
|
anyNameIn(expanded.freeIds ?? extractFreeIdentifiersFromText(expanded.expr), preambleNames)
|
|
1492
|
-
// Loop-param conditionals are reactive via per-item signal
|
|
1493
|
-
// classifyReactivity sees
|
|
1494
|
-
|
|
1564
|
+
// Loop-param/loop-index conditionals are reactive via per-item signal
|
|
1565
|
+
// accessors; classifyReactivity sees all three paths (signal/memo/prop
|
|
1566
|
+
// + loop-param + loop-index, #2861).
|
|
1567
|
+
if (!readsPreamble && classifyReactivity(expanded.expr, ctx, scope, expanded.freeIds).kind === 'none') return
|
|
1495
1568
|
|
|
1496
|
-
const loopParamsForCond =
|
|
1497
|
-
? [{ param: loopParam, bindings: loopParamBindings, index: loopIndex }]
|
|
1498
|
-
: undefined
|
|
1569
|
+
const loopParamsForCond =
|
|
1570
|
+
loopParams ?? (loopParam ? [{ param: loopParam, bindings: loopParamBindings, index: loopIndex }] : undefined)
|
|
1499
1571
|
// `__slots` matches the closure variable emitted by
|
|
1500
1572
|
// `stringifyLoopChildConditional` — Child-position interpolations
|
|
1501
1573
|
// get wrapped in `__bfSlot(EXPR, __slots)` so live `Node` returns
|
|
@@ -1536,7 +1608,13 @@ function summarizeLoopChildBranch(
|
|
|
1536
1608
|
/** Enclosing loop's index param name — see `collectLoopChildConditionals` (Copilot review on #2595). */
|
|
1537
1609
|
loopIndex?: string | null,
|
|
1538
1610
|
): LoopChildBranchSummary {
|
|
1539
|
-
const inner = collectInnerLoops(
|
|
1611
|
+
const inner = collectInnerLoops(
|
|
1612
|
+
[node],
|
|
1613
|
+
siblingOffsets,
|
|
1614
|
+
loopParam ? { param: loopParam, bindings: loopParamBindings, index: loopIndex } : undefined,
|
|
1615
|
+
ctx,
|
|
1616
|
+
branchInnerLoopOptions,
|
|
1617
|
+
)
|
|
1540
1618
|
return {
|
|
1541
1619
|
childComponents: collectConditionalBranchChildComponents(node),
|
|
1542
1620
|
innerLoops: inner.length > 0 ? inner : undefined,
|
|
@@ -60,6 +60,7 @@ export function buildTopLevelCompositePlan(elem: TopLevelLoop, profileComponentN
|
|
|
60
60
|
parentElVar: '__el',
|
|
61
61
|
outerLoopParam: elem.param,
|
|
62
62
|
outerLoopParamBindings: elem.paramBindings,
|
|
63
|
+
outerLoopIndex: elem.index,
|
|
63
64
|
}),
|
|
64
65
|
loopParam: elem.param,
|
|
65
66
|
loopParamBindings: elem.paramBindings,
|
|
@@ -122,6 +123,7 @@ export function buildBranchCompositePlan(loop: BranchLoop, cv: string, profileCo
|
|
|
122
123
|
parentElVar: '__el',
|
|
123
124
|
outerLoopParam: loop.param,
|
|
124
125
|
outerLoopParamBindings: loop.paramBindings,
|
|
126
|
+
outerLoopIndex: loop.index,
|
|
125
127
|
}),
|
|
126
128
|
loopParam: loop.param,
|
|
127
129
|
loopParamBindings: loop.paramBindings,
|
|
@@ -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,6 +169,7 @@ function buildReactiveEmit(
|
|
|
160
169
|
uidSuffix: string,
|
|
161
170
|
outerLoopParam?: string,
|
|
162
171
|
outerLoopParamBindings?: readonly LoopParamBinding[],
|
|
172
|
+
outerLoopIndex?: string | null,
|
|
163
173
|
): InnerLoopReactiveEmit {
|
|
164
174
|
const wrapInner = (expr: string) => wrapLoopParamAsAccessor(expr, inner.param, inner.paramBindings, inner.index)
|
|
165
175
|
const wrapBoth = (expr: string) => wrapLoopParamAsAccessor(wrapOuter(expr), inner.param, inner.paramBindings, inner.index)
|
|
@@ -240,7 +250,7 @@ function buildReactiveEmit(
|
|
|
240
250
|
// wrapInner(wrapOuter(...)) applied to the js text.
|
|
241
251
|
const leafLoopParams = outerLoopParam
|
|
242
252
|
? [
|
|
243
|
-
{ param: outerLoopParam, bindings: outerLoopParamBindings },
|
|
253
|
+
{ param: outerLoopParam, bindings: outerLoopParamBindings, index: outerLoopIndex },
|
|
244
254
|
{ param: inner.param, bindings: inner.paramBindings, index: inner.index },
|
|
245
255
|
]
|
|
246
256
|
: [{ param: inner.param, bindings: inner.paramBindings, index: inner.index }]
|
|
@@ -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
|
-
}
|