@barefootjs/jsx 0.28.1 → 0.30.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/adapters/child-scope.d.ts +17 -0
- package/dist/adapters/child-scope.d.ts.map +1 -0
- package/dist/adapters/loop-bound-names.d.ts.map +1 -1
- package/dist/analyzer-context.d.ts.map +1 -1
- package/dist/analyzer.d.ts.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +890 -357
- package/dist/ir-to-client-js/collect-elements.d.ts +5 -25
- package/dist/ir-to-client-js/collect-elements.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/plan/build-lazy-row.d.ts +66 -2
- 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-reactive-effects.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/plan/lazy-conditional.d.ts +92 -0
- package/dist/ir-to-client-js/control-flow/plan/lazy-conditional.d.ts.map +1 -0
- package/dist/ir-to-client-js/control-flow/plan/lazy-preamble.d.ts +134 -0
- package/dist/ir-to-client-js/control-flow/plan/lazy-preamble.d.ts.map +1 -0
- package/dist/ir-to-client-js/control-flow/plan/lazy-row-eligibility.d.ts +46 -11
- 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/reactive-effects.d.ts +7 -0
- package/dist/ir-to-client-js/control-flow/plan/reactive-effects.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/stringify/composite-loop.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/stringify/lazy-row.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/stringify/template-parse.d.ts +14 -0
- package/dist/ir-to-client-js/control-flow/stringify/template-parse.d.ts.map +1 -1
- package/dist/ir-to-client-js/csr-substitute.d.ts.map +1 -1
- package/dist/ir-to-client-js/html-template.d.ts +2 -3
- 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/plan/declaration-emit.d.ts +6 -0
- package/dist/ir-to-client-js/plan/declaration-emit.d.ts.map +1 -1
- package/dist/ir-to-client-js/reactivity.d.ts +1 -10
- package/dist/ir-to-client-js/reactivity.d.ts.map +1 -1
- package/dist/ir-to-client-js/types.d.ts +12 -0
- package/dist/ir-to-client-js/types.d.ts.map +1 -1
- package/dist/jsx-to-ir.d.ts.map +1 -1
- package/dist/rich-type-evidence.d.ts.map +1 -1
- package/dist/ssr-defaults.d.ts.map +1 -1
- package/dist/to-locale-date-lowering.d.ts.map +1 -1
- package/dist/types.d.ts +78 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/value-references.d.ts +17 -4
- package/dist/value-references.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/__tests__/__snapshots__/doc-examples.test.ts.snap +6 -24
- package/src/__tests__/branch-loop-plain.test.ts +28 -45
- package/src/__tests__/csr-substitution-safety-divergence.test.ts +9 -3
- package/src/__tests__/csr-template-scope-soundness.test.ts +96 -0
- package/src/__tests__/early-return-branch-switch.test.ts +96 -0
- package/src/__tests__/form-control-value-ssr.test.ts +102 -0
- package/src/__tests__/ir-to-client-js/imports.test.ts +18 -0
- package/src/__tests__/lazy-conditional.test.ts +190 -0
- package/src/__tests__/lazy-preamble.test.ts +297 -0
- package/src/__tests__/lazy-row-eligibility.test.ts +97 -28
- package/src/__tests__/loop-item-root-scope.test.ts +213 -0
- package/src/__tests__/loop-row-creation-paths.test.ts +169 -0
- package/src/__tests__/map-multi-return-body.test.ts +30 -3
- package/src/__tests__/nested-loop-conditional.test.ts +27 -6
- package/src/__tests__/preamble-attr-reactivity.test.ts +217 -0
- package/src/__tests__/preamble-declarations.test.ts +207 -0
- package/src/__tests__/ssr-defaults.test.ts +44 -0
- package/src/__tests__/value-references.test.ts +88 -0
- package/src/adapters/child-scope.ts +22 -0
- package/src/adapters/loop-bound-names.ts +6 -0
- package/src/analyzer-context.ts +43 -0
- package/src/analyzer.ts +33 -3
- package/src/index.ts +1 -0
- package/src/ir-to-client-js/collect-elements.ts +51 -10
- package/src/ir-to-client-js/control-flow/plan/build-branch-loop.ts +1 -1
- package/src/ir-to-client-js/control-flow/plan/build-composite-loop.ts +2 -2
- package/src/ir-to-client-js/control-flow/plan/build-event-delegation.ts +3 -3
- package/src/ir-to-client-js/control-flow/plan/build-inner-loop.ts +2 -2
- package/src/ir-to-client-js/control-flow/plan/build-lazy-row.ts +147 -5
- package/src/ir-to-client-js/control-flow/plan/build-loop.ts +2 -2
- package/src/ir-to-client-js/control-flow/plan/build-reactive-effects.ts +1 -0
- package/src/ir-to-client-js/control-flow/plan/lazy-conditional.ts +151 -0
- package/src/ir-to-client-js/control-flow/plan/lazy-preamble.ts +287 -0
- package/src/ir-to-client-js/control-flow/plan/lazy-row-eligibility.ts +85 -22
- package/src/ir-to-client-js/control-flow/plan/reactive-effects.ts +7 -0
- package/src/ir-to-client-js/control-flow/stringify/composite-loop.ts +4 -0
- package/src/ir-to-client-js/control-flow/stringify/lazy-row.ts +133 -35
- package/src/ir-to-client-js/control-flow/stringify/reactive-effects.ts +24 -4
- package/src/ir-to-client-js/control-flow/stringify/template-parse.ts +23 -4
- package/src/ir-to-client-js/csr-substitute.ts +6 -1
- package/src/ir-to-client-js/emit-registration.ts +1 -1
- package/src/ir-to-client-js/html-template.ts +51 -40
- package/src/ir-to-client-js/imports.ts +5 -0
- package/src/ir-to-client-js/index.ts +1 -1
- package/src/ir-to-client-js/plan/build-declaration-emit.ts +2 -0
- package/src/ir-to-client-js/plan/build-static-array-child-init.ts +1 -1
- package/src/ir-to-client-js/plan/declaration-emit.ts +6 -0
- package/src/ir-to-client-js/reactivity.ts +25 -0
- package/src/ir-to-client-js/stringify/declaration-emit.ts +9 -4
- package/src/ir-to-client-js/types.ts +12 -0
- package/src/jsx-to-ir.ts +400 -24
- package/src/rich-type-evidence.ts +6 -7
- package/src/ssr-defaults.ts +7 -2
- package/src/to-locale-date-lowering.ts +8 -2
- package/src/types.ts +80 -0
- package/src/value-references.ts +26 -4
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `IRComponent.loopItemRoot` / `derivesScopeFromSlot()` (#2444): a
|
|
3
|
+
* component that is the DIRECT root of a loop row gets its own randomized
|
|
4
|
+
* scope id; a component nested BELOW the row root derives its scope id
|
|
5
|
+
* from parent scope + slot, like any other slotted child. Mirrors Hono's
|
|
6
|
+
* `renderChildrenInLoop` / `renderConditional` ctx-forwarding exactly.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { describe, test, expect } from 'bun:test'
|
|
10
|
+
import { analyzeComponent } from '../analyzer'
|
|
11
|
+
import { jsxToIR } from '../jsx-to-ir'
|
|
12
|
+
import { compileJSX } from '../compiler'
|
|
13
|
+
import { TestAdapter } from '../adapters/test-adapter'
|
|
14
|
+
import { derivesScopeFromSlot } from '../adapters/child-scope'
|
|
15
|
+
import type { IRComponent, IRElement, IRLoop, IRNode } from '../types'
|
|
16
|
+
|
|
17
|
+
const adapter = new TestAdapter()
|
|
18
|
+
|
|
19
|
+
function findLoop(node: IRNode): IRLoop {
|
|
20
|
+
if (node.type === 'loop') return node
|
|
21
|
+
if (node.type === 'element') {
|
|
22
|
+
for (const child of node.children) {
|
|
23
|
+
if (child.type === 'loop') return child
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
throw new Error('no loop found in IR')
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
describe('IRComponent.loopItemRoot (#2444)', () => {
|
|
30
|
+
test('a component nested below a plain-element loop row root is NOT a loop item root', () => {
|
|
31
|
+
const source = `
|
|
32
|
+
'use client'
|
|
33
|
+
import { createSignal } from '@barefootjs/client'
|
|
34
|
+
function Badge(props: { text: string }) {
|
|
35
|
+
return <span>{props.text}</span>
|
|
36
|
+
}
|
|
37
|
+
export function Rows(props: { items: { id: number; label: string }[] }) {
|
|
38
|
+
const [rows] = createSignal(props.items)
|
|
39
|
+
return (
|
|
40
|
+
<ul>
|
|
41
|
+
{rows().map(row => (
|
|
42
|
+
<li key={row.id}>
|
|
43
|
+
<Badge text={row.label} />
|
|
44
|
+
</li>
|
|
45
|
+
))}
|
|
46
|
+
</ul>
|
|
47
|
+
)
|
|
48
|
+
}
|
|
49
|
+
`
|
|
50
|
+
const ctx = analyzeComponent(source, 'Rows.tsx', 'Rows')
|
|
51
|
+
const ir = jsxToIR(ctx)
|
|
52
|
+
expect(ir).not.toBeNull()
|
|
53
|
+
|
|
54
|
+
const loop = findLoop(ir!)
|
|
55
|
+
expect(loop.children).toHaveLength(1)
|
|
56
|
+
const li = loop.children[0] as IRElement
|
|
57
|
+
expect(li.type).toBe('element')
|
|
58
|
+
const badge = li.children.find(c => c.type === 'component') as IRComponent
|
|
59
|
+
expect(badge).toBeDefined()
|
|
60
|
+
expect(badge.loopItemRoot).not.toBe(true)
|
|
61
|
+
expect(derivesScopeFromSlot(badge)).toBe(true)
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
test('a component that IS the loop row root IS a loop item root', () => {
|
|
65
|
+
const source = `
|
|
66
|
+
'use client'
|
|
67
|
+
import { createSignal } from '@barefootjs/client'
|
|
68
|
+
function Row(props: { label: string }) {
|
|
69
|
+
return <li>{props.label}</li>
|
|
70
|
+
}
|
|
71
|
+
export function Rows(props: { items: { id: number; label: string }[] }) {
|
|
72
|
+
const [rows] = createSignal(props.items)
|
|
73
|
+
return (
|
|
74
|
+
<ul>
|
|
75
|
+
{rows().map(row => (
|
|
76
|
+
<Row key={row.id} label={row.label} />
|
|
77
|
+
))}
|
|
78
|
+
</ul>
|
|
79
|
+
)
|
|
80
|
+
}
|
|
81
|
+
`
|
|
82
|
+
const ctx = analyzeComponent(source, 'Rows.tsx', 'Rows')
|
|
83
|
+
const ir = jsxToIR(ctx)
|
|
84
|
+
expect(ir).not.toBeNull()
|
|
85
|
+
|
|
86
|
+
const loop = findLoop(ir!)
|
|
87
|
+
expect(loop.children).toHaveLength(1)
|
|
88
|
+
const row = loop.children[0] as IRComponent
|
|
89
|
+
expect(row.type).toBe('component')
|
|
90
|
+
expect(row.loopItemRoot).toBe(true)
|
|
91
|
+
expect(derivesScopeFromSlot(row)).toBe(false)
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
test('a component in a whole-item conditional loop body IS a loop item root (matches Hono ctx-forwarding)', () => {
|
|
95
|
+
const source = `
|
|
96
|
+
'use client'
|
|
97
|
+
import { createSignal } from '@barefootjs/client'
|
|
98
|
+
function Row(props: { label: string }) {
|
|
99
|
+
return <li>{props.label}</li>
|
|
100
|
+
}
|
|
101
|
+
export function Rows(props: { items: { id: number; label: string; show: boolean }[] }) {
|
|
102
|
+
const [rows] = createSignal(props.items)
|
|
103
|
+
return (
|
|
104
|
+
<ul>
|
|
105
|
+
{rows().map(row => row.show && <Row key={row.id} label={row.label} />)}
|
|
106
|
+
</ul>
|
|
107
|
+
)
|
|
108
|
+
}
|
|
109
|
+
`
|
|
110
|
+
const ctx = analyzeComponent(source, 'Rows.tsx', 'Rows')
|
|
111
|
+
const ir = jsxToIR(ctx)
|
|
112
|
+
expect(ir).not.toBeNull()
|
|
113
|
+
|
|
114
|
+
const loop = findLoop(ir!)
|
|
115
|
+
expect(loop.children).toHaveLength(1)
|
|
116
|
+
const cond = loop.children[0]
|
|
117
|
+
expect(cond.type).toBe('conditional')
|
|
118
|
+
if (cond.type !== 'conditional') throw new Error('unreachable')
|
|
119
|
+
const row = cond.whenTrue as IRComponent
|
|
120
|
+
expect(row.type).toBe('component')
|
|
121
|
+
expect(row.loopItemRoot).toBe(true)
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
test('a component nested inside a fragment loop body is NOT a loop item root (Hono does not forward through fragments)', () => {
|
|
125
|
+
const source = `
|
|
126
|
+
'use client'
|
|
127
|
+
import { createSignal } from '@barefootjs/client'
|
|
128
|
+
function Badge(props: { text: string }) {
|
|
129
|
+
return <span>{props.text}</span>
|
|
130
|
+
}
|
|
131
|
+
export function Rows(props: { items: { id: number; label: string }[] }) {
|
|
132
|
+
const [rows] = createSignal(props.items)
|
|
133
|
+
return (
|
|
134
|
+
<ul>
|
|
135
|
+
{rows().map(row => (
|
|
136
|
+
<>
|
|
137
|
+
<Badge key={row.id} text={row.label} />
|
|
138
|
+
</>
|
|
139
|
+
))}
|
|
140
|
+
</ul>
|
|
141
|
+
)
|
|
142
|
+
}
|
|
143
|
+
`
|
|
144
|
+
const ctx = analyzeComponent(source, 'Rows.tsx', 'Rows')
|
|
145
|
+
const ir = jsxToIR(ctx)
|
|
146
|
+
expect(ir).not.toBeNull()
|
|
147
|
+
|
|
148
|
+
const loop = findLoop(ir!)
|
|
149
|
+
expect(loop.children).toHaveLength(1)
|
|
150
|
+
const fragment = loop.children[0]
|
|
151
|
+
expect(fragment.type).toBe('fragment')
|
|
152
|
+
if (fragment.type !== 'fragment') throw new Error('unreachable')
|
|
153
|
+
const badge = fragment.children.find(c => c.type === 'component') as IRComponent
|
|
154
|
+
expect(badge).toBeDefined()
|
|
155
|
+
expect(badge.loopItemRoot).not.toBe(true)
|
|
156
|
+
})
|
|
157
|
+
|
|
158
|
+
test('CSR template: a component nested below the row root passes its slot suffix to renderChild', () => {
|
|
159
|
+
const source = `
|
|
160
|
+
'use client'
|
|
161
|
+
import { createSignal } from '@barefootjs/client'
|
|
162
|
+
function Badge(props: { text: string }) {
|
|
163
|
+
return <span>{props.text}</span>
|
|
164
|
+
}
|
|
165
|
+
export function Rows(props: { items: { id: number; label: string }[] }) {
|
|
166
|
+
const [rows] = createSignal(props.items)
|
|
167
|
+
return (
|
|
168
|
+
<ul>
|
|
169
|
+
{rows().map(row => (
|
|
170
|
+
<li key={row.id}>
|
|
171
|
+
<Badge text={row.label} />
|
|
172
|
+
</li>
|
|
173
|
+
))}
|
|
174
|
+
</ul>
|
|
175
|
+
)
|
|
176
|
+
}
|
|
177
|
+
`
|
|
178
|
+
const result = compileJSX(source, 'Rows.tsx', { adapter })
|
|
179
|
+
expect(result.errors).toHaveLength(0)
|
|
180
|
+
const clientJs = result.files.find(f => f.type === 'clientJs')
|
|
181
|
+
expect(clientJs).toBeDefined()
|
|
182
|
+
// The nested Badge must receive a slot suffix (4th renderChild arg) so
|
|
183
|
+
// it derives its scope id — a bare `renderChild('Badge', {...})` with
|
|
184
|
+
// no suffix would mean the fix regressed.
|
|
185
|
+
expect(clientJs!.content).toMatch(/renderChild\('Badge[^']*',\s*\{text:\s*row\.label\},\s*undefined,\s*'s\d+'\)/)
|
|
186
|
+
})
|
|
187
|
+
|
|
188
|
+
test('CSR template: a component that IS the loop row root gets no slot suffix', () => {
|
|
189
|
+
const source = `
|
|
190
|
+
'use client'
|
|
191
|
+
import { createSignal } from '@barefootjs/client'
|
|
192
|
+
function Row(props: { label: string }) {
|
|
193
|
+
return <li>{props.label}</li>
|
|
194
|
+
}
|
|
195
|
+
export function Rows(props: { items: { id: number; label: string }[] }) {
|
|
196
|
+
const [rows] = createSignal(props.items)
|
|
197
|
+
return (
|
|
198
|
+
<ul>
|
|
199
|
+
{rows().map(row => (
|
|
200
|
+
<Row key={row.id} label={row.label} />
|
|
201
|
+
))}
|
|
202
|
+
</ul>
|
|
203
|
+
)
|
|
204
|
+
}
|
|
205
|
+
`
|
|
206
|
+
const result = compileJSX(source, 'Rows.tsx', { adapter })
|
|
207
|
+
expect(result.errors).toHaveLength(0)
|
|
208
|
+
const clientJs = result.files.find(f => f.type === 'clientJs')
|
|
209
|
+
expect(clientJs).toBeDefined()
|
|
210
|
+
// Static-array loop item root: no slot suffix — it owns its own random id.
|
|
211
|
+
expect(clientJs!.content).not.toMatch(/renderChild\('row',[^)]*'s\d+'\)/)
|
|
212
|
+
})
|
|
213
|
+
})
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which code creates a loop row's root element, and whether that row is
|
|
3
|
+
* connected before its body's tail runs.
|
|
4
|
+
*
|
|
5
|
+
* The "connect before init" invariant exists because `useContext` resolves by
|
|
6
|
+
* walking `parentElement`: a child that initialises inside a detached row finds
|
|
7
|
+
* no ancestors and falls through to the global last-writer-wins context store,
|
|
8
|
+
* silently reading whichever provider on the page wrote last. So the creation
|
|
9
|
+
* site has to know its destination in advance, and there are two ways to tell
|
|
10
|
+
* it — a PLACEHOLDER already in the tree (`mountAt`, child slots), or the
|
|
11
|
+
* AMBIENT row mount point.
|
|
12
|
+
*
|
|
13
|
+
* Both work because a runtime FUNCTION creates the element and can take an
|
|
14
|
+
* argument. A row whose root is a template clone written inline in the emitted
|
|
15
|
+
* body has no such function, so the compiler emits `mountRowRoot(clone)` to
|
|
16
|
+
* consume the same ambient point — but only for the variant that initialises
|
|
17
|
+
* anything inside the row. A row with no nested init has nothing that could
|
|
18
|
+
* resolve wrongly, and skipping it there keeps the high-volume `mapArrayLazy`
|
|
19
|
+
* emission untouched.
|
|
20
|
+
*
|
|
21
|
+
* This matrix pins that split against compiled output rather than
|
|
22
|
+
* recollection. `mountsRow` is the load-bearing column: if it ever flips to
|
|
23
|
+
* false for a shape that initialises a child, that shape has silently gone
|
|
24
|
+
* back to initialising detached, and nothing else in the suite would say so —
|
|
25
|
+
* the wrong value it produces is only wrong when a second provider of the same
|
|
26
|
+
* context happens to be on the page.
|
|
27
|
+
*/
|
|
28
|
+
import { describe, test, expect } from 'bun:test'
|
|
29
|
+
import { compileJSX } from '../compiler'
|
|
30
|
+
import { TestAdapter } from '../adapters/test-adapter'
|
|
31
|
+
|
|
32
|
+
interface RowShape {
|
|
33
|
+
/** How the row's root element comes into existence. */
|
|
34
|
+
rowRoot: 'createComponent' | 'template-clone'
|
|
35
|
+
/**
|
|
36
|
+
* Does a child inside the row reach its `init` through a placeholder slot
|
|
37
|
+
* (`upsertChild` / `upsertChildItem`)?
|
|
38
|
+
*
|
|
39
|
+
* Narrower than "something initialises in this row" — a conditional child
|
|
40
|
+
* arrives via `insert` + `initChild` and reads false here. Read it alongside
|
|
41
|
+
* `mountsRow`, never as a substitute.
|
|
42
|
+
*
|
|
43
|
+
* `initChild` on the row ROOT is deliberately excluded: that is the
|
|
44
|
+
* hydration branch adopting an SSR-rendered row, which is in the document by
|
|
45
|
+
* construction and never part of this problem.
|
|
46
|
+
*/
|
|
47
|
+
nestedChildInit: boolean
|
|
48
|
+
/** Multi-root (Fragment) body — the sibling-walk lookup path. */
|
|
49
|
+
multiRoot: boolean
|
|
50
|
+
/** Is the fresh row connected before the body's tail runs? */
|
|
51
|
+
mountsRow: boolean
|
|
52
|
+
/** Which list runtime the loop compiles to. */
|
|
53
|
+
loopRuntime: string
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function classify(loopBody: string): RowShape {
|
|
57
|
+
const source = `"use client"
|
|
58
|
+
import { createSignal } from '@barefootjs/client'
|
|
59
|
+
import { Chip } from './chip'
|
|
60
|
+
function T() {
|
|
61
|
+
const [items] = createSignal([{ id: '1', name: 'a' }])
|
|
62
|
+
return <ul>{items().map((it) => ${loopBody})}</ul>
|
|
63
|
+
}
|
|
64
|
+
export { T }`
|
|
65
|
+
const result = compileJSX(source, 'T.tsx', { adapter: new TestAdapter() })
|
|
66
|
+
const hard = result.errors.filter((e) => e.severity !== 'warning')
|
|
67
|
+
if (hard.length > 0) throw new Error(`unexpected compile error: ${hard.map((e) => e.code).join(',')}`)
|
|
68
|
+
const js = result.files.find((f) => f.type === 'clientJs')?.content ?? ''
|
|
69
|
+
|
|
70
|
+
// Slice from the list call to the end of the emitted init function.
|
|
71
|
+
// Everything the row does at setup time is in there, and the top-level
|
|
72
|
+
// `createComponent('T', …)` export sits after it, so it cannot be mistaken
|
|
73
|
+
// for a row root. Starting at the call also excludes the import line, so
|
|
74
|
+
// `mountsRow` reflects a real call site rather than the import of one.
|
|
75
|
+
const call = /mapArray(?:Lazy|Anchored)?\(/.exec(js)
|
|
76
|
+
if (!call) throw new Error('no list runtime call emitted')
|
|
77
|
+
const hydrateAt = js.indexOf('\nhydrate(')
|
|
78
|
+
const region = js.slice(call.index, hydrateAt === -1 ? undefined : hydrateAt)
|
|
79
|
+
|
|
80
|
+
return {
|
|
81
|
+
rowRoot: /cloneNode\(/.test(region) ? 'template-clone' : 'createComponent',
|
|
82
|
+
nestedChildInit: /upsertChild\(|upsertChildItem\(/.test(region),
|
|
83
|
+
multiRoot: /__bfExtras|upsertChildItem\(|qsaItem\(/.test(region),
|
|
84
|
+
mountsRow: /mountRowRoot\(/.test(region),
|
|
85
|
+
loopRuntime: /mapArray(?:Lazy|Anchored)?/.exec(region)?.[0] ?? 'unknown',
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
describe('loop row creation paths — who makes the row root, and when it is connected', () => {
|
|
90
|
+
test('component row root: the runtime creates it, so it takes the mount point itself', () => {
|
|
91
|
+
// `<Chip/>` IS the row. `createComponent` consumes the ambient point in its
|
|
92
|
+
// own step 7b, so no separate call is emitted.
|
|
93
|
+
expect(classify('<Chip key={it.id} id={it.id} />')).toEqual({
|
|
94
|
+
rowRoot: 'createComponent',
|
|
95
|
+
nestedChildInit: false,
|
|
96
|
+
multiRoot: false,
|
|
97
|
+
mountsRow: false,
|
|
98
|
+
loopRuntime: 'mapArray',
|
|
99
|
+
})
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
test('markup row root with no component inside: a clone, and deliberately not mounted', () => {
|
|
103
|
+
// Nothing initialises inside this row, so there is no `useContext` call
|
|
104
|
+
// during setup to resolve wrongly — and this is the high-volume path, so
|
|
105
|
+
// leaving it alone keeps the `mapArrayLazy` emission and the measurements
|
|
106
|
+
// taken against it untouched. `mountsRow: false` here is a decision.
|
|
107
|
+
expect(classify('<li key={it.id}>{it.name}</li>')).toEqual({
|
|
108
|
+
rowRoot: 'template-clone',
|
|
109
|
+
nestedChildInit: false,
|
|
110
|
+
multiRoot: false,
|
|
111
|
+
mountsRow: false,
|
|
112
|
+
loopRuntime: 'mapArrayLazy',
|
|
113
|
+
})
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
test('markup row root with a nested component: clone, mounted before the tail', () => {
|
|
117
|
+
// The row root is user markup, so it is not and cannot become a component;
|
|
118
|
+
// `mountRowRoot` is what gives it a destination. Flip `mountsRow` to false
|
|
119
|
+
// and `upsertChild` goes back to running the child's `init` against a
|
|
120
|
+
// detached element.
|
|
121
|
+
expect(classify('<li key={it.id}><Chip id={it.id} /></li>')).toEqual({
|
|
122
|
+
rowRoot: 'template-clone',
|
|
123
|
+
nestedChildInit: true,
|
|
124
|
+
multiRoot: false,
|
|
125
|
+
mountsRow: true,
|
|
126
|
+
loopRuntime: 'mapArray',
|
|
127
|
+
})
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
test('two nested components in one markup row: same case, one mount', () => {
|
|
131
|
+
expect(classify('<li key={it.id}><Chip id={it.id} /><Chip id={it.name} /></li>')).toEqual({
|
|
132
|
+
rowRoot: 'template-clone',
|
|
133
|
+
nestedChildInit: true,
|
|
134
|
+
multiRoot: false,
|
|
135
|
+
mountsRow: true,
|
|
136
|
+
loopRuntime: 'mapArray',
|
|
137
|
+
})
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
test('Fragment row with a nested component: mounted, and the multi-root lookup copes', () => {
|
|
141
|
+
// A Fragment root can never be a component, so this shape only became
|
|
142
|
+
// reachable for a mount point once the clone path got one. It is also the
|
|
143
|
+
// multi-root lookup, whose sibling walk in `qsa-item.ts` used to end with
|
|
144
|
+
// `return` and skip the `__bfExtras` stash the moment the primary was
|
|
145
|
+
// attached — a `break` now, so an attached primary still finds its extras.
|
|
146
|
+
expect(classify('<><Chip id={it.id} /><span>{it.name}</span></>')).toEqual({
|
|
147
|
+
rowRoot: 'template-clone',
|
|
148
|
+
nestedChildInit: true,
|
|
149
|
+
multiRoot: true,
|
|
150
|
+
mountsRow: true,
|
|
151
|
+
loopRuntime: 'mapArray',
|
|
152
|
+
})
|
|
153
|
+
})
|
|
154
|
+
|
|
155
|
+
test('conditional child in a markup row: mounted, though it inits through `insert`', () => {
|
|
156
|
+
// `nestedChildInit` is false because this child arrives via `insert` +
|
|
157
|
+
// `initChild` rather than `upsertChild` — but it is still a child
|
|
158
|
+
// initialising inside the row, and the emission is keyed on the loop
|
|
159
|
+
// variant (composite), not on which call the child arrives through. This
|
|
160
|
+
// row is why the two columns have to be read separately.
|
|
161
|
+
expect(classify('<li key={it.id}>{it.name ? <Chip id={it.id} /> : null}</li>')).toEqual({
|
|
162
|
+
rowRoot: 'template-clone',
|
|
163
|
+
nestedChildInit: false,
|
|
164
|
+
multiRoot: false,
|
|
165
|
+
mountsRow: true,
|
|
166
|
+
loopRuntime: 'mapArray',
|
|
167
|
+
})
|
|
168
|
+
})
|
|
169
|
+
})
|
|
@@ -133,13 +133,23 @@ describe('.map() multi-return body fold (Stage 2)', () => {
|
|
|
133
133
|
})
|
|
134
134
|
})
|
|
135
135
|
|
|
136
|
-
describe('leading-const preamble
|
|
136
|
+
describe('leading-const preamble — folded on JS, lowered on DSL, gated on shape', () => {
|
|
137
137
|
const preambleBody = `{
|
|
138
138
|
const label = it.kind.toUpperCase()
|
|
139
139
|
if (it.on) return <b key={it.id}>{label}</b>
|
|
140
140
|
return <span key={it.id}>{label}</span>
|
|
141
141
|
}`
|
|
142
142
|
|
|
143
|
+
// The same fold, but with a preamble no template language can declare: a
|
|
144
|
+
// destructuring binding takes several names off one initializer, and no
|
|
145
|
+
// adapter has a single per-row local form for that. Nothing lowers, so
|
|
146
|
+
// both branches would read an unassigned name.
|
|
147
|
+
const undeclarableBody = `{
|
|
148
|
+
const { kind } = it
|
|
149
|
+
if (it.on) return <b key={it.id}>{kind}</b>
|
|
150
|
+
return <span key={it.id}>{kind}</span>
|
|
151
|
+
}`
|
|
152
|
+
|
|
143
153
|
function compileWith(source: string, dsl: boolean) {
|
|
144
154
|
const a = new TestAdapter()
|
|
145
155
|
// Model a DSL adapter: its template runtime can't run a callback body
|
|
@@ -156,15 +166,32 @@ describe('.map() multi-return body fold (Stage 2)', () => {
|
|
|
156
166
|
expect(cj.content).not.toMatch(/return <(b|span)/)
|
|
157
167
|
})
|
|
158
168
|
|
|
159
|
-
test('a DSL adapter
|
|
169
|
+
test('a DSL adapter accepts it too — the preamble lowers to a per-row local (#2447)', () => {
|
|
170
|
+
// This used to be a BF021 refusal, on the premise that a loop-local
|
|
171
|
+
// can't reach a conditional branch template. The fold puts the
|
|
172
|
+
// conditional INSIDE the loop body, so once the preamble lowers to a
|
|
173
|
+
// per-row declaration the local is in scope in both arms — see the
|
|
174
|
+
// `map-preamble-branch-body` fixture, which now renders on all nine
|
|
175
|
+
// adapters.
|
|
160
176
|
const r = compileWith(wrap(preambleBody), true)
|
|
177
|
+
expect(r.errors.filter(e => e.severity !== 'warning')).toHaveLength(0)
|
|
178
|
+
})
|
|
179
|
+
|
|
180
|
+
test('a preamble that is NOT declarable still refuses with BF021 + /* @client */', () => {
|
|
181
|
+
const r = compileWith(wrap(undeclarableBody), true)
|
|
161
182
|
const bf021 = r.errors.filter(e => e.code === 'BF021')
|
|
162
183
|
expect(bf021).toHaveLength(1)
|
|
184
|
+
expect(bf021[0].message).toMatch(/preamble is not a sequence of value declarations/)
|
|
163
185
|
expect(bf021[0].suggestion?.message).toContain('@client')
|
|
164
186
|
})
|
|
165
187
|
|
|
188
|
+
test('a JS runtime runs the undeclarable preamble verbatim — no refusal', () => {
|
|
189
|
+
const r = compileWith(wrap(undeclarableBody), false)
|
|
190
|
+
expect(r.errors.filter(e => e.severity !== 'warning')).toHaveLength(0)
|
|
191
|
+
})
|
|
192
|
+
|
|
166
193
|
test('/* @client */ suppresses the DSL refusal', () => {
|
|
167
|
-
const clientSource = wrap(
|
|
194
|
+
const clientSource = wrap(undeclarableBody).replace('items.map', '/* @client */ items.map')
|
|
168
195
|
const r = compileWith(clientSource, true)
|
|
169
196
|
expect(r.errors.filter(e => e.code === 'BF021')).toHaveLength(0)
|
|
170
197
|
})
|
|
@@ -342,14 +342,35 @@ describe('nested loops/conditionals inside mapArray (#830, #839)', () => {
|
|
|
342
342
|
// Locate the nested mapArray callback body — the inner one is the
|
|
343
343
|
// `node.children.map(child => ...)` that uses `child.id` as its key.
|
|
344
344
|
// Note: pre-O-1 the inner mapArray was emitted twice (once per
|
|
345
|
-
// SSR/CSR side of the outer renderItem) so this
|
|
346
|
-
//
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
345
|
+
// SSR/CSR side of the outer renderItem) so this used to find 2+
|
|
346
|
+
// matches. After the O-1 dedup we only need at least 1.
|
|
347
|
+
//
|
|
348
|
+
// Extracted via a balanced-PAREN scan from each `mapArray(` call site
|
|
349
|
+
// (a call is itself a single balanced-paren expression) — NOT a lazy
|
|
350
|
+
// `[\s\S]*?...\}\)\s*\}` regex, which coincidentally matched the old
|
|
351
|
+
// output only because the FIRST `}) }` byte sequence anywhere in the
|
|
352
|
+
// file happened to fall right after the inner mapArray call. Any
|
|
353
|
+
// unrelated text elsewhere containing that same 3-character sequence
|
|
354
|
+
// (e.g. the SSR template's `renderChild(..., 's1')` slot argument,
|
|
355
|
+
// #2444) silently shifted or erased the match with no relation to
|
|
356
|
+
// mapArray's actual extent — a real balanced scan can't have that failure mode.
|
|
357
|
+
const mapCalls: string[] = []
|
|
358
|
+
const mapArrayCallRe = /mapArray\(/g
|
|
359
|
+
let callMatch: RegExpExecArray | null
|
|
360
|
+
while ((callMatch = mapArrayCallRe.exec(content)) !== null) {
|
|
361
|
+
let depth = 1
|
|
362
|
+
let i = callMatch.index + callMatch[0].length
|
|
363
|
+
while (i < content.length && depth > 0) {
|
|
364
|
+
if (content[i] === '(') depth++
|
|
365
|
+
else if (content[i] === ')') depth--
|
|
366
|
+
i++
|
|
367
|
+
}
|
|
368
|
+
mapCalls.push(content.slice(callMatch.index, i))
|
|
369
|
+
}
|
|
370
|
+
expect(mapCalls.length).toBeGreaterThanOrEqual(1)
|
|
350
371
|
|
|
351
372
|
// Pick the inner mapArray. It references `child.id` as the key function.
|
|
352
|
-
const innerMapArray = mapCalls
|
|
373
|
+
const innerMapArray = mapCalls.find(m => /\(child\)\s*=>\s*String\(child\.id\)/.test(m))
|
|
353
374
|
expect(innerMapArray).toBeDefined()
|
|
354
375
|
|
|
355
376
|
// Count initChild('Checkbox', ...) occurrences inside the inner callback,
|