@barefootjs/jsx 0.27.0 → 0.28.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/index.js +620 -26
- package/dist/ir-to-client-js/control-flow/plan/branch-loop.d.ts +6 -0
- package/dist/ir-to-client-js/control-flow/plan/branch-loop.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/plan/build-branch-loop.d.ts +2 -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-insert.d.ts +7 -0
- package/dist/ir-to-client-js/control-flow/plan/build-insert.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/plan/build-lazy-row.d.ts +107 -0
- package/dist/ir-to-client-js/control-flow/plan/build-lazy-row.d.ts.map +1 -0
- package/dist/ir-to-client-js/control-flow/plan/build-loop.d.ts +9 -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/lazy-row-eligibility.d.ts +203 -0
- package/dist/ir-to-client-js/control-flow/plan/lazy-row-eligibility.d.ts.map +1 -0
- package/dist/ir-to-client-js/control-flow/plan/loop.d.ts +10 -0
- package/dist/ir-to-client-js/control-flow/plan/loop.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/stringify/branch-loop.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/stringify/lazy-row.d.ts +98 -0
- package/dist/ir-to-client-js/control-flow/stringify/lazy-row.d.ts.map +1 -0
- package/dist/ir-to-client-js/control-flow/stringify/loop-child-arm.d.ts +5 -0
- 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.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/package.json +2 -2
- package/src/__tests__/__snapshots__/doc-examples.test.ts.snap +162 -36
- package/src/__tests__/client-js-generation.test.ts +4 -1
- package/src/__tests__/composite-branch-loop.test.ts +12 -3
- package/src/__tests__/conditional-mapArray-key.test.ts +7 -1
- package/src/__tests__/create-selector.test.ts +21 -7
- package/src/__tests__/lazy-row-eligibility.test.ts +692 -0
- package/src/__tests__/loop-branch-bare-expression-reactive-text.test.ts +98 -0
- package/src/__tests__/loop-fallback-wrap.test.ts +31 -12
- package/src/__tests__/loop-hoisted-template.test.ts +10 -6
- package/src/__tests__/static-loop-csr-materialize.test.ts +6 -1
- package/src/ir-to-client-js/collect-elements.ts +48 -13
- package/src/ir-to-client-js/control-flow/plan/branch-loop.ts +6 -0
- package/src/ir-to-client-js/control-flow/plan/build-branch-loop.ts +35 -12
- package/src/ir-to-client-js/control-flow/plan/build-insert.ts +9 -2
- package/src/ir-to-client-js/control-flow/plan/build-lazy-row.ts +305 -0
- package/src/ir-to-client-js/control-flow/plan/build-loop.ts +44 -12
- package/src/ir-to-client-js/control-flow/plan/lazy-row-eligibility.ts +440 -0
- package/src/ir-to-client-js/control-flow/plan/loop.ts +10 -0
- package/src/ir-to-client-js/control-flow/stringify/branch-loop.ts +22 -0
- package/src/ir-to-client-js/control-flow/stringify/lazy-row.ts +478 -0
- package/src/ir-to-client-js/control-flow/stringify/loop-child-arm.ts +6 -1
- package/src/ir-to-client-js/control-flow/stringify/loop.ts +23 -0
- package/src/ir-to-client-js/control-flow.ts +7 -2
- package/src/ir-to-client-js/imports.ts +8 -2
- package/src/jsx-to-ir.ts +18 -1
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Emission pin for the loop-branch-stale-text fix.
|
|
3
|
+
*
|
|
4
|
+
* `summarizeLoopChildBranch` (ir-to-client-js/collect-elements.ts) used to
|
|
5
|
+
* collect NO `reactiveTexts` for ANY loop-row conditional branch whose whole
|
|
6
|
+
* content is a bare `expression` (no wrapping element) — including a plain
|
|
7
|
+
* property read like `row.label`, which can never construct a live DOM
|
|
8
|
+
* node. That blanket skip meant a keyed row's branch text silently froze at
|
|
9
|
+
* its mount-time value whenever the item changed without the conditional's
|
|
10
|
+
* OWN condition flipping (`insert()`, runtime/insert.ts, correctly no-ops
|
|
11
|
+
* when its condition is unchanged — branch-internal updates are the effect
|
|
12
|
+
* system's job).
|
|
13
|
+
*
|
|
14
|
+
* The fix narrows the skip to `node.hasFunctionCalls` (jsx-to-ir.ts's
|
|
15
|
+
* `exprHasFunctionCalls`, a recursive AST walk — catches a call nested
|
|
16
|
+
* anywhere, not just a top-level one) — a call can return a live DOM node
|
|
17
|
+
* (a hoisted `renderNode={(n) => <Pill/>}` callback lowered to a component
|
|
18
|
+
* call, #1211/#1213, pinned by `nested-loop-conditional.test.ts`) and the
|
|
19
|
+
* compiler cannot tell from syntax whether it does, so that shape must keep
|
|
20
|
+
* the skip. Everything call-free is provably incapable of yielding a Node
|
|
21
|
+
* and is now collected.
|
|
22
|
+
*
|
|
23
|
+
* The companion half of the fix lives in `transformConditionalBranch`
|
|
24
|
+
* (jsx-to-ir.ts): a bare loop-item read like `row.label` previously got NO
|
|
25
|
+
* `slotId` at all (`needsSlot` didn't consider a `render-item` free
|
|
26
|
+
* reference), so even with the collect-elements.ts skip narrowed there was
|
|
27
|
+
* nothing for the new effect — or the `<!--bf:sN-->` marker
|
|
28
|
+
* `irToHtmlTemplate` emits for it — to attach to. Both are exercised here
|
|
29
|
+
* together because neither half alone reproduces the fix.
|
|
30
|
+
*/
|
|
31
|
+
import { describe, test, expect } from 'bun:test'
|
|
32
|
+
import { compileJSX } from '../compiler'
|
|
33
|
+
import { TestAdapter } from '../adapters/test-adapter'
|
|
34
|
+
|
|
35
|
+
const adapter = new TestAdapter()
|
|
36
|
+
|
|
37
|
+
function clientJsFor(branchTrue: string): string {
|
|
38
|
+
const source = `
|
|
39
|
+
'use client'
|
|
40
|
+
import { createSignal } from '@barefootjs/client'
|
|
41
|
+
type Props = { format?: (s: string) => string }
|
|
42
|
+
type Row = { id: number; label: string; done: boolean }
|
|
43
|
+
export function C(_p: Props) {
|
|
44
|
+
const [rows] = createSignal<Row[]>([])
|
|
45
|
+
return <ul>{rows().map(row => <li key={row.id}>{row.done ? ${branchTrue} : 'pending'}</li>)}</ul>
|
|
46
|
+
}
|
|
47
|
+
`
|
|
48
|
+
const result = compileJSX(source, 'C.tsx', { adapter })
|
|
49
|
+
const errors = result.errors.filter(e => e.severity === 'error')
|
|
50
|
+
if (errors.length > 0) throw new Error(errors.map(e => e.message).join('\n'))
|
|
51
|
+
return result.files.find(f => f.type === 'clientJs')!.content
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
describe('loop-branch-stale-text — reactiveTexts narrowing (collect-elements.ts)', () => {
|
|
55
|
+
test('a bare property-read branch (no call anywhere) gets its own text effect', () => {
|
|
56
|
+
const js = clientJsFor('row.label')
|
|
57
|
+
expect(js).toContain("escapeTextOrNode(row().label)")
|
|
58
|
+
// And a slotId was actually allocated for it — `transformConditionalBranch`'s
|
|
59
|
+
// `refsLoopParam` half of the fix, without which the effect above would
|
|
60
|
+
// have nothing to `lazySlots(...)` claim.
|
|
61
|
+
expect(js).toMatch(/lazySlots\(__branchScope, \[\{ id: 's\d+', kind: 'markup', path: \[\] \}\]\)/)
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
test('a template-literal branch built from property reads only (no call) also gets a text effect', () => {
|
|
65
|
+
const js = clientJsFor('`[${row.label}]`')
|
|
66
|
+
expect(js).toContain('escapeTextOrNode(`[${row().label}]`)')
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
test('a branch with a call ANYWHERE (top-level) keeps the skip — no per-item text effect', () => {
|
|
70
|
+
const js = clientJsFor('_p.format(row.label)')
|
|
71
|
+
expect(js).not.toContain('escapeTextOrNode(_p.format(row().label))')
|
|
72
|
+
// bindEvents for that arm stays empty (mirrors the pre-fix shape exactly).
|
|
73
|
+
expect(js).toMatch(/bindEvents: \(__branchScope, \{ isFirstRun: __bfFirstRun = false \} = \{\}\) => \{\s*\}/m)
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
test('a branch with a call NESTED inside a template literal also keeps the skip', () => {
|
|
77
|
+
// The call is not at the top level of the expression — `exprHasFunctionCalls`
|
|
78
|
+
// is a full recursive AST walk, so this must be caught too.
|
|
79
|
+
const js = clientJsFor('`${_p.format(row.label)}!`')
|
|
80
|
+
expect(js).not.toContain("escapeTextOrNode(`${_p.format(row().label)}!`)")
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
test('a branch with a call nested inside a sub-ternary keeps the skip', () => {
|
|
84
|
+
const js = clientJsFor("row.id > 0 ? _p.format(row.label) : row.label")
|
|
85
|
+
// The outer branch (itself a nested conditional) is unaffected by this
|
|
86
|
+
// narrowing — nested conditionals already get their own `insert()` via
|
|
87
|
+
// `collectLoopChildConditionals`, never through `reactiveTexts`. This
|
|
88
|
+
// case exists to confirm compilation succeeds and the call-bearing leaf
|
|
89
|
+
// still doesn't get a bare reactiveTexts effect wrapping the whole
|
|
90
|
+
// nested-ternary expression text.
|
|
91
|
+
expect(js).not.toContain('escapeTextOrNode(row.id > 0 ? _p.format(row().label) : row().label)')
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
test('an inert literal branch (no loop-param ref, no call) stays exactly as before — no slotId, no effect', () => {
|
|
95
|
+
const js = clientJsFor("'yes'")
|
|
96
|
+
expect(js).not.toContain("escapeTextOrNode('yes')")
|
|
97
|
+
})
|
|
98
|
+
})
|
|
@@ -58,7 +58,7 @@ describe('Solid-style wrap-by-default fallback for loops (#943)', () => {
|
|
|
58
58
|
const clientJs = getClientJs(source, 'List.tsx')
|
|
59
59
|
// Dynamic loops emit a `mapArray(` call site. Tighter than
|
|
60
60
|
// `/mapArray|reconcile/` which would fire on any stray mention.
|
|
61
|
-
expect(clientJs).
|
|
61
|
+
expect(clientJs).toMatch(/\bmapArray(Lazy)?\(/)
|
|
62
62
|
expect(clientJs).toContain('items()')
|
|
63
63
|
})
|
|
64
64
|
|
|
@@ -83,7 +83,7 @@ describe('Solid-style wrap-by-default fallback for loops (#943)', () => {
|
|
|
83
83
|
`
|
|
84
84
|
|
|
85
85
|
const clientJs = getClientJs(source, 'List.tsx')
|
|
86
|
-
expect(clientJs).
|
|
86
|
+
expect(clientJs).toMatch(/\bmapArray(Lazy)?\(/)
|
|
87
87
|
expect(clientJs).toContain('getItems()')
|
|
88
88
|
})
|
|
89
89
|
|
|
@@ -124,7 +124,11 @@ describe('Solid-style wrap-by-default fallback for loops (#943)', () => {
|
|
|
124
124
|
`
|
|
125
125
|
|
|
126
126
|
const clientJs = getClientJs(source, 'List.tsx')
|
|
127
|
-
|
|
127
|
+
// `mapArrayLazy` counts: a keyed, single-root, conditional-free row over a
|
|
128
|
+
// PROP array is lazy-row-eligible (spec/slot-unification.md §9.4 — props
|
|
129
|
+
// are the canonical hydration-consistent source), so it takes the lazy
|
|
130
|
+
// reconciler. What #1586 pins is "reconciles at all", not which family.
|
|
131
|
+
expect(clientJs).toMatch(/\bmapArray(Lazy)?\s*\(/)
|
|
128
132
|
})
|
|
129
133
|
|
|
130
134
|
test('unrecognised-call chain with filter now reconciles', () => {
|
|
@@ -149,7 +153,7 @@ describe('Solid-style wrap-by-default fallback for loops (#943)', () => {
|
|
|
149
153
|
`
|
|
150
154
|
|
|
151
155
|
const clientJs = getClientJs(source, 'DoneList.tsx')
|
|
152
|
-
expect(clientJs).
|
|
156
|
+
expect(clientJs).toMatch(/\bmapArray(Lazy)?\(/)
|
|
153
157
|
expect(clientJs).toContain('computeItems()')
|
|
154
158
|
})
|
|
155
159
|
|
|
@@ -181,7 +185,7 @@ describe('Solid-style wrap-by-default fallback for loops (#943)', () => {
|
|
|
181
185
|
// Outer reconciles via mapArray. Also assert the inner loop's array
|
|
182
186
|
// expression (`o.children`) appears in the emitted template — without
|
|
183
187
|
// this, the test would pass even if the inner loop were dropped.
|
|
184
|
-
expect(clientJs).
|
|
188
|
+
expect(clientJs).toMatch(/\bmapArray(Lazy)?\(/)
|
|
185
189
|
expect(clientJs).toContain('outer()')
|
|
186
190
|
expect(clientJs).toContain('o.children')
|
|
187
191
|
})
|
|
@@ -219,8 +223,13 @@ describe('Solid-style wrap-by-default fallback for loops (#943)', () => {
|
|
|
219
223
|
// mapArray is emitted (widening now applies). renderItem uses the
|
|
220
224
|
// synthetic `__bfItem` param; references to `cfg` are rewritten
|
|
221
225
|
// inline and no unwrap statement is emitted.
|
|
222
|
-
expect(clientJs).
|
|
223
|
-
|
|
226
|
+
expect(clientJs).toMatch(/\bmapArray(Lazy)?\(/)
|
|
227
|
+
// The synthetic accessor param, in whichever reconciler shape applies:
|
|
228
|
+
// `mapArray`'s renderItem head `(__bfItem, __idx, __existing) =>`, or a
|
|
229
|
+
// lazy row plan's `const __bfItem = () => __e.item` binding
|
|
230
|
+
// (spec/slot-unification.md §9). Either way the destructured references
|
|
231
|
+
// below must read through the accessor, which is what this pins.
|
|
232
|
+
expect(clientJs).toMatch(/\(__bfItem, |const __bfItem = \(\) => __e\.item/)
|
|
224
233
|
expect(clientJs).not.toContain('const [, cfg] = __bfItem();')
|
|
225
234
|
expect(clientJs).toContain('__bfItem()[1].color')
|
|
226
235
|
})
|
|
@@ -247,7 +256,7 @@ describe('Solid-style wrap-by-default fallback for loops (#943)', () => {
|
|
|
247
256
|
`
|
|
248
257
|
|
|
249
258
|
const clientJs = getClientJs(source, 'Shelf.tsx')
|
|
250
|
-
expect(clientJs).
|
|
259
|
+
expect(clientJs).toMatch(/\bmapArray(Lazy)?\(/)
|
|
251
260
|
expect(clientJs).toContain('listOf()')
|
|
252
261
|
})
|
|
253
262
|
|
|
@@ -278,8 +287,13 @@ describe('Solid-style wrap-by-default fallback for loops (#943)', () => {
|
|
|
278
287
|
`
|
|
279
288
|
|
|
280
289
|
const clientJs = getClientJs(source, 'TypedLegend.tsx')
|
|
281
|
-
expect(clientJs).
|
|
282
|
-
|
|
290
|
+
expect(clientJs).toMatch(/\bmapArray(Lazy)?\(/)
|
|
291
|
+
// The synthetic accessor param, in whichever reconciler shape applies:
|
|
292
|
+
// `mapArray`'s renderItem head `(__bfItem, __idx, __existing) =>`, or a
|
|
293
|
+
// lazy row plan's `const __bfItem = () => __e.item` binding
|
|
294
|
+
// (spec/slot-unification.md §9). Either way the destructured references
|
|
295
|
+
// below must read through the accessor, which is what this pins.
|
|
296
|
+
expect(clientJs).toMatch(/\(__bfItem, |const __bfItem = \(\) => __e\.item/)
|
|
283
297
|
expect(clientJs).not.toContain('const [, cfg] = __bfItem();')
|
|
284
298
|
expect(clientJs).toContain('__bfItem()[1].color')
|
|
285
299
|
})
|
|
@@ -309,8 +323,13 @@ describe('Solid-style wrap-by-default fallback for loops (#943)', () => {
|
|
|
309
323
|
`
|
|
310
324
|
|
|
311
325
|
const clientJs = getClientJs(source, 'Fields.tsx')
|
|
312
|
-
expect(clientJs).
|
|
313
|
-
|
|
326
|
+
expect(clientJs).toMatch(/\bmapArray(Lazy)?\(/)
|
|
327
|
+
// The synthetic accessor param, in whichever reconciler shape applies:
|
|
328
|
+
// `mapArray`'s renderItem head `(__bfItem, __idx, __existing) =>`, or a
|
|
329
|
+
// lazy row plan's `const __bfItem = () => __e.item` binding
|
|
330
|
+
// (spec/slot-unification.md §9). Either way the destructured references
|
|
331
|
+
// below must read through the accessor, which is what this pins.
|
|
332
|
+
expect(clientJs).toMatch(/\(__bfItem, |const __bfItem = \(\) => __e\.item/)
|
|
314
333
|
expect(clientJs).not.toContain('const { label, value } = __bfItem();')
|
|
315
334
|
expect(clientJs).toContain('__bfItem().label')
|
|
316
335
|
expect(clientJs).toContain('__bfItem().value')
|
|
@@ -72,16 +72,20 @@ describe('hoisted loop-body skeleton template (perf)', () => {
|
|
|
72
72
|
`
|
|
73
73
|
const { clientJs, template } = compile(source, 'Bench.tsx')
|
|
74
74
|
|
|
75
|
-
// Hoisted declaration present, before the
|
|
75
|
+
// Hoisted declaration present, before the reconciler call. This row shape
|
|
76
|
+
// is also lazy-row-eligible (spec/slot-unification.md §9.4), so the
|
|
77
|
+
// reconciler is `mapArrayLazy` and the clone happens in the row plan's
|
|
78
|
+
// `createRow` rather than in a `mapArray` renderItem — the hoisting
|
|
79
|
+
// question this suite pins is orthogonal to which reconciler consumes it.
|
|
76
80
|
const declIdx = clientJs.indexOf('.innerHTML = `<tr')
|
|
77
|
-
const mapArrayIdx = clientJs.
|
|
81
|
+
const mapArrayIdx = clientJs.search(/\bmapArray(Lazy)?\(/)
|
|
78
82
|
expect(declIdx).toBeGreaterThan(-1)
|
|
79
83
|
expect(mapArrayIdx).toBeGreaterThan(-1)
|
|
80
84
|
expect(declIdx).toBeLessThan(mapArrayIdx)
|
|
81
85
|
|
|
82
|
-
//
|
|
86
|
+
// The row is built by cloning the hoisted template instead of re-parsing
|
|
83
87
|
// innerHTML per row.
|
|
84
|
-
expect(clientJs).toMatch(/const __el =
|
|
88
|
+
expect(clientJs).toMatch(/const __el = __tpl_\w+\.content\.firstElementChild\.cloneNode\(true\)/)
|
|
85
89
|
|
|
86
90
|
// The hoisted template itself carries no interpolations and no dynamic
|
|
87
91
|
// `class` attribute (that's covered by the reactive-attr createEffect).
|
|
@@ -172,7 +176,7 @@ describe('hoisted loop-body skeleton template (perf)', () => {
|
|
|
172
176
|
}
|
|
173
177
|
`
|
|
174
178
|
const { clientJs } = compile(source, 'SpreadList.tsx')
|
|
175
|
-
expect(clientJs).
|
|
179
|
+
expect(clientJs).toMatch(/\bmapArray(Lazy)?\(/)
|
|
176
180
|
expect(clientJs).not.toMatch(/__tpl_\w+ = document\.createElement\('template'\)/)
|
|
177
181
|
})
|
|
178
182
|
|
|
@@ -195,7 +199,7 @@ describe('hoisted loop-body skeleton template (perf)', () => {
|
|
|
195
199
|
}
|
|
196
200
|
`
|
|
197
201
|
const { clientJs } = compile(source, 'Dots.tsx')
|
|
198
|
-
expect(clientJs).
|
|
202
|
+
expect(clientJs).toMatch(/\bmapArray(Lazy)?\(/)
|
|
199
203
|
// A `<circle>` root with only dynamic attrs (all covered by reactive-attr
|
|
200
204
|
// createEffects) and a literal `r="4"` is exactly the shape the fast path
|
|
201
205
|
// proves safe — this DOES hoist, and it must wrap in a synthetic `<svg>`
|
|
@@ -95,7 +95,12 @@ describe('#1247 — static-loop CSR self-heal', () => {
|
|
|
95
95
|
}
|
|
96
96
|
`
|
|
97
97
|
const clientJs = getClientJs(source, 'PropList.tsx')
|
|
98
|
-
|
|
98
|
+
// Either reconciler shape satisfies the point of this test — the loop must
|
|
99
|
+
// go through the keyed `mapArray` family, not the static `forEach` path.
|
|
100
|
+
// A keyed, single-root, conditional-free row over a prop array is
|
|
101
|
+
// lazy-row-eligible (spec/slot-unification.md §9.4), so it emits
|
|
102
|
+
// `mapArrayLazy`; widen (don't narrow) the assertion.
|
|
103
|
+
expect(clientJs).toMatch(/\bmapArray(Lazy)?\s*\(/)
|
|
99
104
|
expect(clientJs).not.toMatch(/\.forEach\s*\(/)
|
|
100
105
|
})
|
|
101
106
|
|
|
@@ -1413,19 +1413,54 @@ function summarizeLoopChildBranch(
|
|
|
1413
1413
|
// on the branch root yields exactly this branch's direct bindings
|
|
1414
1414
|
// without re-collecting what a nested arm already owns (#2347).
|
|
1415
1415
|
reactiveAttrs: collectLoopChildReactiveAttrs(node, ctx, loopParam, loopParamBindings, true),
|
|
1416
|
-
// Skip when the branch's
|
|
1417
|
-
// (no wrapping element)
|
|
1418
|
-
//
|
|
1419
|
-
//
|
|
1420
|
-
//
|
|
1421
|
-
//
|
|
1422
|
-
//
|
|
1423
|
-
// and the
|
|
1424
|
-
//
|
|
1425
|
-
//
|
|
1426
|
-
//
|
|
1427
|
-
//
|
|
1428
|
-
|
|
1416
|
+
// Skip ONLY when the branch's entire content is a single bare
|
|
1417
|
+
// `expression` (no wrapping element) that MAY yield a live DOM node —
|
|
1418
|
+
// i.e. it contains a call anywhere (`node.hasFunctionCalls`, computed
|
|
1419
|
+
// by the AST walk in `exprHasFunctionCalls`/jsx-to-ir.ts, recursively —
|
|
1420
|
+
// catches a call nested in a template literal or a nested ternary, not
|
|
1421
|
+
// just a top-level call). A call can return an Element (a hoisted
|
|
1422
|
+
// `renderNode={(n) => <Pill/>}` callback lowered to a component call,
|
|
1423
|
+
// #1211/#1213) and the compiler cannot tell from syntax whether it does
|
|
1424
|
+
// — both `renderChild(...)` and `_p.renderCell(...)` are a
|
|
1425
|
+
// `CallExpression` — so this stays conservative for ANY call, not just
|
|
1426
|
+
// ones known to return JSX. Re-evaluating a non-idempotent call inside
|
|
1427
|
+
// an *additional* nested createEffect (on top of the eval already done
|
|
1428
|
+
// for the `__bfSlot`-wrapped splice whenever `insert()` (re-)mounts this
|
|
1429
|
+
// branch) would call it again on every unrelated tick and discard the
|
|
1430
|
+
// previous element's listeners/state.
|
|
1431
|
+
//
|
|
1432
|
+
// Everything else — a property access (`row.label`), an identifier, a
|
|
1433
|
+
// literal, a template literal, string concatenation, or a nested
|
|
1434
|
+
// ternary of those — cannot itself construct a DOM node (only a JSX
|
|
1435
|
+
// literal or a call can, and a JSX literal branch is never `type:
|
|
1436
|
+
// 'expression'` in the first place — see `transformJsxExpression`), so
|
|
1437
|
+
// it is safe to collect. This is the fix for the loop-branch-stale-text
|
|
1438
|
+
// defect: a keyed loop row whose branch is e.g. `row.done ? row.label :
|
|
1439
|
+
// 'pending'` previously had NO update path at all when `row.label`
|
|
1440
|
+
// changed without `row.done` flipping — `insert()` (runtime/insert.ts)
|
|
1441
|
+
// correctly no-ops when its condition is unchanged (branch-internal
|
|
1442
|
+
// updates are deliberately the effect system's job, not DOM
|
|
1443
|
+
// replacement's), but with reactiveTexts always `[]` for this shape,
|
|
1444
|
+
// no effect existed either, so the branch was frozen at its mount-time
|
|
1445
|
+
// value. The stale rationale in a prior version of this comment blamed
|
|
1446
|
+
// "the loop-child arm's `$t()`-based anchor lookup", but `$t()` no
|
|
1447
|
+
// longer exists — it was one of four content mechanisms deleted by the
|
|
1448
|
+
// slot-unification work. The current claim door
|
|
1449
|
+
// (`stringifyLoopChildArm` → `lazySlots(..., kind: 'markup')` →
|
|
1450
|
+
// `writeMarkup`, runtime/claim-slots.ts) clears the claimed range and
|
|
1451
|
+
// splices the Node by identity, so "lands beside the first" cannot
|
|
1452
|
+
// happen through it; only the call's non-idempotence still applies, and
|
|
1453
|
+
// that alone is why the skip narrows rather than disappears.
|
|
1454
|
+
//
|
|
1455
|
+
// Collecting here is necessary but not sufficient: `n.slotId` must also
|
|
1456
|
+
// be set for `collectLoopChildReactiveTexts` (ir-to-client-js/
|
|
1457
|
+
// reactivity.ts) to do anything — see `transformConditionalBranch`'s
|
|
1458
|
+
// `refsLoopParam` check (jsx-to-ir.ts) for the matching half of this fix
|
|
1459
|
+
// that gives a bare loop-item-reading branch a slotId at all, which also
|
|
1460
|
+
// makes `irToHtmlTemplate` emit its `<!--bf:sN-->…<!--/-->` marker (the
|
|
1461
|
+
// same call builds both the SSR and the CSR/hydration template, so the
|
|
1462
|
+
// two can't disagree on shape).
|
|
1463
|
+
reactiveTexts: node.type === 'expression' && node.hasFunctionCalls
|
|
1429
1464
|
? []
|
|
1430
1465
|
: collectLoopChildReactiveTexts(node, ctx, loopParam, loopParamBindings, true),
|
|
1431
1466
|
}
|
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
import type { CompositeLoopPlan, LoopChildRefBinding, PreambleRegionPlan } from './loop.ts'
|
|
19
19
|
import type { EventDelegationPlan } from './event-delegation.ts'
|
|
20
20
|
import type { ReactiveEffectsPlan } from './reactive-effects.ts'
|
|
21
|
+
import type { LazyRowPlanData } from './build-lazy-row.ts'
|
|
21
22
|
|
|
22
23
|
export interface BranchPlainLoopPlan {
|
|
23
24
|
/**
|
|
@@ -89,6 +90,11 @@ export interface BranchPlainLoopPlan {
|
|
|
89
90
|
* renderItem layout, mirroring the top-level plain plan.
|
|
90
91
|
*/
|
|
91
92
|
preambleRegions: readonly PreambleRegionPlan[]
|
|
93
|
+
/**
|
|
94
|
+
* Lazy row graph plan (`spec/slot-unification.md` §9, L3) — see
|
|
95
|
+
* `PlainLoopVariant.lazyRow`. Undefined ⇒ today's eager emission.
|
|
96
|
+
*/
|
|
97
|
+
lazyRow?: LazyRowPlanData
|
|
92
98
|
}
|
|
93
99
|
|
|
94
100
|
export interface BranchCompositeLoopPlan {
|
|
@@ -15,6 +15,8 @@ import { buildChainedArrayExpr, varSlotId, wrapLoopParamAsAccessor } from '../..
|
|
|
15
15
|
import { buildBranchCompositePlan } from './build-composite-loop.ts'
|
|
16
16
|
import { buildBranchLoopDelegationPlan } from './build-event-delegation.ts'
|
|
17
17
|
import { buildReactiveEffectsPlan } from './build-reactive-effects.ts'
|
|
18
|
+
import { buildLazyRowPlan } from './build-lazy-row.ts'
|
|
19
|
+
import type { LazyRowScopeInfo } from './lazy-row-eligibility.ts'
|
|
18
20
|
import { destructureLoopParam, loopKeyFn, buildChildRefBindings, buildPreambleRegionPlans } from '../shared.ts'
|
|
19
21
|
import { renderPreamble, irToHtmlTemplate } from '../../html-template.ts'
|
|
20
22
|
import type {
|
|
@@ -23,7 +25,11 @@ import type {
|
|
|
23
25
|
BranchPlainLoopPlan,
|
|
24
26
|
} from './branch-loop.ts'
|
|
25
27
|
|
|
26
|
-
export function buildBranchLoopPlan(
|
|
28
|
+
export function buildBranchLoopPlan(
|
|
29
|
+
loop: BranchLoop,
|
|
30
|
+
profileComponentName?: string,
|
|
31
|
+
lazyScope?: LazyRowScopeInfo,
|
|
32
|
+
): BranchLoopPlan {
|
|
27
33
|
const containerSlotId = loop.containerSlotId
|
|
28
34
|
const cv = varSlotId(containerSlotId)
|
|
29
35
|
const containerVar = `__loop_${cv}`
|
|
@@ -45,6 +51,17 @@ export function buildBranchLoopPlan(loop: BranchLoop, profileComponentName?: str
|
|
|
45
51
|
|
|
46
52
|
// flatMap descriptor mode — see buildPlainLoopPlan (build-loop.ts).
|
|
47
53
|
const fm = loop.flatMapClient
|
|
54
|
+
const arrayExpr = fm
|
|
55
|
+
? `(${buildChainedArrayExpr(loop)}).flatMap(${fm.params} => ${fm.body})`
|
|
56
|
+
: buildChainedArrayExpr(loop)
|
|
57
|
+
const indexParam = loop.index || '__idx'
|
|
58
|
+
const mapPreambleWrapped = loop.preamble
|
|
59
|
+
? renderPreamble(loop.preamble, {
|
|
60
|
+
transformJs: (t) => wrapLoopParamAsAccessor(t, loop.param, loop.paramBindings),
|
|
61
|
+
renderLeaf: (ir) => irToHtmlTemplate(ir, undefined, 1, [{ param: loop.param, bindings: loop.paramBindings }], undefined, true),
|
|
62
|
+
})
|
|
63
|
+
: ''
|
|
64
|
+
const preambleRegions = buildPreambleRegionPlans(loop.preambleRegions, loop.param, loop.paramBindings)
|
|
48
65
|
const plan: BranchPlainLoopPlan = {
|
|
49
66
|
kind: 'plain',
|
|
50
67
|
rowConstruction: 'string-template',
|
|
@@ -52,23 +69,29 @@ export function buildBranchLoopPlan(loop: BranchLoop, profileComponentName?: str
|
|
|
52
69
|
containerVar,
|
|
53
70
|
markerId: loop.markerId,
|
|
54
71
|
flatMapLeafItem: fm ? true : undefined,
|
|
55
|
-
arrayExpr
|
|
56
|
-
? `(${buildChainedArrayExpr(loop)}).flatMap(${fm.params} => ${fm.body})`
|
|
57
|
-
: buildChainedArrayExpr(loop),
|
|
72
|
+
arrayExpr,
|
|
58
73
|
keyFn: fm
|
|
59
74
|
? (fm.keyed ? '(__bfD, __bfI) => String(__bfD.k ?? __bfI)' : 'null')
|
|
60
75
|
: loopKeyFn(loop),
|
|
61
76
|
paramHead,
|
|
62
77
|
paramUnwrap,
|
|
63
|
-
indexParam
|
|
78
|
+
indexParam,
|
|
64
79
|
// Wrap loop-param references to signal-accessor form so the preamble
|
|
65
80
|
// matches the template literal's already-wrapped reads (#1065).
|
|
66
|
-
mapPreambleWrapped
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
81
|
+
mapPreambleWrapped,
|
|
82
|
+
// Lazy row graph (§9, L3) — undefined for every ineligible loop.
|
|
83
|
+
lazyRow: buildLazyRowPlan({
|
|
84
|
+
loop,
|
|
85
|
+
arrayExpr,
|
|
86
|
+
indexParam,
|
|
87
|
+
paramUnwrap,
|
|
88
|
+
mapPreambleWrapped,
|
|
89
|
+
preambleRegionCount: preambleRegions.length,
|
|
90
|
+
callSite: 'branch-plain',
|
|
91
|
+
flatMapLeafItem: Boolean(fm),
|
|
92
|
+
anchored: false,
|
|
93
|
+
scope: lazyScope,
|
|
94
|
+
}) ?? undefined,
|
|
72
95
|
template: loop.template,
|
|
73
96
|
reactiveEffects: hasReactiveEffects
|
|
74
97
|
? buildReactiveEffectsPlan({
|
|
@@ -82,7 +105,7 @@ export function buildBranchLoopPlan(loop: BranchLoop, profileComponentName?: str
|
|
|
82
105
|
: null,
|
|
83
106
|
eventDelegation: buildBranchLoopDelegationPlan(loop, cv, profileComponentName),
|
|
84
107
|
childRefs: buildChildRefBindings(loop.bindings.refs, loop.param, loop.paramBindings),
|
|
85
|
-
preambleRegions
|
|
108
|
+
preambleRegions,
|
|
86
109
|
bodyIsMultiRoot: loop.bodyIsMultiRoot ?? false,
|
|
87
110
|
profileLoopId: profileComponentName ? `${profileComponentName}#binding:${containerSlotId}` : undefined,
|
|
88
111
|
}
|
|
@@ -14,6 +14,7 @@ import type {
|
|
|
14
14
|
} from '../../types.ts'
|
|
15
15
|
import { addCondAttrToTemplate } from '../../html-template.ts'
|
|
16
16
|
import { buildBranchLoopPlan } from './build-branch-loop.ts'
|
|
17
|
+
import type { LazyRowScopeInfo } from './lazy-row-eligibility.ts'
|
|
17
18
|
import type {
|
|
18
19
|
InsertPlan,
|
|
19
20
|
InsertArm,
|
|
@@ -26,6 +27,12 @@ export interface BuildInsertOptions {
|
|
|
26
27
|
eventNameMode: 'dom' | 'raw'
|
|
27
28
|
/** Owning component name in profile mode (#1690, SR3) — else undefined. */
|
|
28
29
|
profileComponentName?: string
|
|
30
|
+
/**
|
|
31
|
+
* Component-scope name facts for the lazy row graph gate
|
|
32
|
+
* (`spec/slot-unification.md` §9.4). Threaded down to branch-scoped plain
|
|
33
|
+
* loops; omitted → those loops keep today's eager emission.
|
|
34
|
+
*/
|
|
35
|
+
lazyScope?: LazyRowScopeInfo
|
|
29
36
|
}
|
|
30
37
|
|
|
31
38
|
export function buildInsertPlan(
|
|
@@ -88,13 +95,13 @@ function buildArmBody(branch: BranchSummary, options: BuildInsertOptions): ArmBo
|
|
|
88
95
|
expression: t.expression,
|
|
89
96
|
})),
|
|
90
97
|
// Branch-scoped loops, fully Plan-built (Item 2 final migration).
|
|
91
|
-
loops: branch.loops.map(l => buildBranchLoopPlan(l, pc)),
|
|
98
|
+
loops: branch.loops.map(l => buildBranchLoopPlan(l, pc, options.lazyScope)),
|
|
92
99
|
// Nested conditionals are themselves InsertPlans — built recursively so
|
|
93
100
|
// the same stringifier handles arbitrary depth. Their scope is always
|
|
94
101
|
// `__branchScope` (the parent arm's bindEvents argument), regardless of
|
|
95
102
|
// the outer scope; only the eventNameMode is inherited.
|
|
96
103
|
conditionals: branch.conditionals.map(c =>
|
|
97
|
-
buildInsertPlan(c, { scope: { kind: 'branchScope' }, eventNameMode: options.eventNameMode, profileComponentName: pc }),
|
|
104
|
+
buildInsertPlan(c, { scope: { kind: 'branchScope' }, eventNameMode: options.eventNameMode, profileComponentName: pc, lazyScope: options.lazyScope }),
|
|
98
105
|
),
|
|
99
106
|
}
|
|
100
107
|
}
|