@pyreon/runtime-dom 0.24.5 → 0.24.6

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.
Files changed (53) hide show
  1. package/package.json +5 -9
  2. package/src/delegate.ts +0 -98
  3. package/src/devtools.ts +0 -339
  4. package/src/env.d.ts +0 -6
  5. package/src/hydrate.ts +0 -450
  6. package/src/hydration-debug.ts +0 -129
  7. package/src/index.ts +0 -83
  8. package/src/keep-alive-entry.ts +0 -3
  9. package/src/keep-alive.ts +0 -83
  10. package/src/manifest.ts +0 -236
  11. package/src/mount.ts +0 -597
  12. package/src/nodes.ts +0 -896
  13. package/src/props.ts +0 -474
  14. package/src/template.ts +0 -523
  15. package/src/tests/callback-ref-unmount.browser.test.ts +0 -62
  16. package/src/tests/callback-ref-unmount.test.ts +0 -52
  17. package/src/tests/compiler-integration.test.tsx +0 -508
  18. package/src/tests/coverage-gaps.test.ts +0 -3183
  19. package/src/tests/coverage.test.ts +0 -1140
  20. package/src/tests/ctx-stack-growth-repro.test.tsx +0 -158
  21. package/src/tests/dev-gate-pattern.test.ts +0 -46
  22. package/src/tests/dev-gate-treeshake.test.ts +0 -256
  23. package/src/tests/error-boundary-stack-leak-repro.test.tsx +0 -133
  24. package/src/tests/fanout-repro.test.tsx +0 -219
  25. package/src/tests/hydration-integration.test.tsx +0 -540
  26. package/src/tests/keyed-array-in-for-batched-toggle.browser.test.ts +0 -140
  27. package/src/tests/lifecycle-integration.test.tsx +0 -342
  28. package/src/tests/lis-prepend.browser.test.ts +0 -99
  29. package/src/tests/manifest-snapshot.test.ts +0 -85
  30. package/src/tests/mount.test.ts +0 -3529
  31. package/src/tests/native-markers.test.ts +0 -19
  32. package/src/tests/props.test.ts +0 -581
  33. package/src/tests/reactive-props.test.ts +0 -270
  34. package/src/tests/real-world-integration.test.tsx +0 -714
  35. package/src/tests/rs-collapse-dyn-h.browser.test.ts +0 -303
  36. package/src/tests/rs-collapse-dyn.browser.test.ts +0 -316
  37. package/src/tests/rs-collapse-h.browser.test.ts +0 -152
  38. package/src/tests/rs-collapse-h.test.ts +0 -237
  39. package/src/tests/rs-collapse.browser.test.ts +0 -128
  40. package/src/tests/runtime-dom.browser.test.ts +0 -409
  41. package/src/tests/setup.ts +0 -3
  42. package/src/tests/show-context.test.ts +0 -270
  43. package/src/tests/show-of-for-batched-toggle.browser.test.ts +0 -122
  44. package/src/tests/ssr-xss-round-trip.browser.test.ts +0 -93
  45. package/src/tests/style-key-removal.browser.test.ts +0 -54
  46. package/src/tests/style-key-removal.test.ts +0 -88
  47. package/src/tests/template.test.ts +0 -383
  48. package/src/tests/transition-timeout-leak.test.ts +0 -126
  49. package/src/tests/transition.test.ts +0 -568
  50. package/src/tests/verified-correct-probes.test.ts +0 -56
  51. package/src/transition-entry.ts +0 -7
  52. package/src/transition-group.ts +0 -350
  53. package/src/transition.ts +0 -245
@@ -1,342 +0,0 @@
1
- import { defineComponent, For, h, onMount, onUnmount, Show } from '@pyreon/core'
2
- import { signal } from '@pyreon/reactivity'
3
- import { mount } from '../index'
4
-
5
- // ─── Helpers ────────────────────────────────────────────────────────────────
6
-
7
- function container(): HTMLElement {
8
- const el = document.createElement('div')
9
- document.body.appendChild(el)
10
- return el
11
- }
12
-
13
- afterEach(() => {
14
- document.body.innerHTML = ''
15
- })
16
-
17
- // ─── Show toggle ────────────────────────────────────────────────────────────
18
-
19
- describe('lifecycle — Show toggle', () => {
20
- test('show=true — child mounts, signal reactive', () => {
21
- const el = container()
22
- const text = signal('hello')
23
- mount(
24
- h(Show, { when: () => true }, h('div', null, () => text())),
25
- el,
26
- )
27
- expect(el.textContent).toBe('hello')
28
- text.set('world')
29
- expect(el.textContent).toBe('world')
30
- })
31
-
32
- test('show=false — child unmounts', () => {
33
- const el = container()
34
- const visible = signal(false)
35
- mount(
36
- h(Show, { when: visible }, h('div', { id: 'child' }, 'content')),
37
- el,
38
- )
39
- expect(el.querySelector('#child')).toBeNull()
40
- })
41
-
42
- test('show=true again — fresh mount, signal still reactive', () => {
43
- const el = container()
44
- const visible = signal(true)
45
- const text = signal('initial')
46
- mount(
47
- h(Show, { when: visible }, h('div', { id: 'child' }, () => text())),
48
- el,
49
- )
50
- expect(el.querySelector('#child')?.textContent).toBe('initial')
51
-
52
- // Hide
53
- visible.set(false)
54
- expect(el.querySelector('#child')).toBeNull()
55
-
56
- // Show again
57
- visible.set(true)
58
- expect(el.querySelector('#child')).not.toBeNull()
59
-
60
- // Signal should still be reactive in new mount
61
- text.set('updated')
62
- expect(el.querySelector('#child')?.textContent).toBe('updated')
63
- })
64
-
65
- test('rapid toggle (true -> false -> true) — no duplicate effects', () => {
66
- const el = container()
67
- const visible = signal(true)
68
- let mountCount = 0
69
-
70
- const Child = defineComponent(() => {
71
- mountCount++
72
- return h('div', null, 'child')
73
- })
74
-
75
- mount(h(Show, { when: visible }, h(Child, null)), el)
76
- expect(mountCount).toBe(1)
77
-
78
- // Rapid toggle
79
- visible.set(false)
80
- visible.set(true)
81
- // After rapid toggle, component should be mounted exactly once more
82
- expect(mountCount).toBe(2)
83
-
84
- visible.set(false)
85
- visible.set(true)
86
- expect(mountCount).toBe(3)
87
- })
88
- })
89
-
90
- // ─── For list ───────────────────────────────────────────────────────────────
91
-
92
- describe('lifecycle — For list', () => {
93
- type Item = { id: number; label: string }
94
-
95
- test('render list of 3 items — 3 DOM nodes', () => {
96
- const el = container()
97
- const items = signal<Item[]>([
98
- { id: 1, label: 'Alice' },
99
- { id: 2, label: 'Bob' },
100
- { id: 3, label: 'Charlie' },
101
- ])
102
- mount(
103
- h(
104
- 'ul',
105
- null,
106
- For({ each: items, by: (r) => r.id, children: (r) => h('li', { key: r.id }, r.label) }),
107
- ),
108
- el,
109
- )
110
- expect(el.querySelectorAll('li').length).toBe(3)
111
- expect(el.querySelectorAll('li')[0]?.textContent).toBe('Alice')
112
- expect(el.querySelectorAll('li')[1]?.textContent).toBe('Bob')
113
- expect(el.querySelectorAll('li')[2]?.textContent).toBe('Charlie')
114
- })
115
-
116
- test('add item — 4 DOM nodes, existing unchanged', () => {
117
- const el = container()
118
- const items = signal<Item[]>([
119
- { id: 1, label: 'Alice' },
120
- { id: 2, label: 'Bob' },
121
- { id: 3, label: 'Charlie' },
122
- ])
123
- mount(
124
- h(
125
- 'ul',
126
- null,
127
- For({ each: items, by: (r) => r.id, children: (r) => h('li', { key: r.id }, r.label) }),
128
- ),
129
- el,
130
- )
131
- const firstLi = el.querySelectorAll('li')[0]
132
-
133
- items.set([...items(), { id: 4, label: 'Dave' }])
134
- expect(el.querySelectorAll('li').length).toBe(4)
135
- expect(el.querySelectorAll('li')[3]?.textContent).toBe('Dave')
136
- // Existing nodes should be reused (same DOM identity)
137
- expect(el.querySelectorAll('li')[0]).toBe(firstLi)
138
- })
139
-
140
- test('remove item — 2 DOM nodes', () => {
141
- const el = container()
142
- const items = signal<Item[]>([
143
- { id: 1, label: 'Alice' },
144
- { id: 2, label: 'Bob' },
145
- { id: 3, label: 'Charlie' },
146
- ])
147
- mount(
148
- h(
149
- 'ul',
150
- null,
151
- For({ each: items, by: (r) => r.id, children: (r) => h('li', { key: r.id }, r.label) }),
152
- ),
153
- el,
154
- )
155
- items.set([{ id: 1, label: 'Alice' }, { id: 3, label: 'Charlie' }])
156
- expect(el.querySelectorAll('li').length).toBe(2)
157
- expect(el.querySelectorAll('li')[0]?.textContent).toBe('Alice')
158
- expect(el.querySelectorAll('li')[1]?.textContent).toBe('Charlie')
159
- })
160
-
161
- test('update item signal — only that item DOM changes', () => {
162
- const el = container()
163
- const items = signal([
164
- { id: 1, name: signal('Alice') },
165
- { id: 2, name: signal('Bob') },
166
- ])
167
- mount(
168
- h(
169
- 'ul',
170
- null,
171
- For({
172
- each: items,
173
- by: (r) => r.id,
174
- children: (r) => h('li', { key: r.id }, () => r.name()),
175
- }),
176
- ),
177
- el,
178
- )
179
- expect(el.querySelectorAll('li')[0]?.textContent).toBe('Alice')
180
-
181
- // Update only the first item's signal
182
- items()[0]!.name.set('Alicia')
183
- expect(el.querySelectorAll('li')[0]?.textContent).toBe('Alicia')
184
- // Second item unchanged
185
- expect(el.querySelectorAll('li')[1]?.textContent).toBe('Bob')
186
- })
187
-
188
- test('reorder items — DOM reordered without remount', () => {
189
- const el = container()
190
- const r1: Item = { id: 1, label: 'a' }
191
- const r2: Item = { id: 2, label: 'b' }
192
- const r3: Item = { id: 3, label: 'c' }
193
- const items = signal<Item[]>([r1, r2, r3])
194
- mount(
195
- h(
196
- 'ul',
197
- null,
198
- For({ each: items, by: (r) => r.id, children: (r) => h('li', { key: r.id }, r.label) }),
199
- ),
200
- el,
201
- )
202
- const origLi1 = el.querySelectorAll('li')[0]
203
- const origLi2 = el.querySelectorAll('li')[1]
204
- const origLi3 = el.querySelectorAll('li')[2]
205
-
206
- // Reverse order
207
- items.set([r3, r2, r1])
208
- const lis = el.querySelectorAll('li')
209
- expect(lis[0]?.textContent).toBe('c')
210
- expect(lis[1]?.textContent).toBe('b')
211
- expect(lis[2]?.textContent).toBe('a')
212
- // DOM nodes should be reused (moved, not recreated)
213
- expect(lis[0]).toBe(origLi3)
214
- expect(lis[1]).toBe(origLi2)
215
- expect(lis[2]).toBe(origLi1)
216
- })
217
- })
218
-
219
- // ─── Effect cleanup ─────────────────────────────────────────────────────────
220
-
221
- describe('lifecycle — effect cleanup', () => {
222
- test('effect inside component — runs on mount', () => {
223
- const el = container()
224
- let effectRan = false
225
- const Comp = defineComponent(() => {
226
- onMount(() => {
227
- effectRan = true
228
- })
229
- return h('div', null, 'mounted')
230
- })
231
- mount(h(Comp, null), el)
232
- expect(effectRan).toBe(true)
233
- })
234
-
235
- test('unmount component — effect cleaned up (verify via mock)', () => {
236
- const el = container()
237
- let cleanupCalled = false
238
- const Comp = defineComponent(() => {
239
- onMount(() => {
240
- return () => {
241
- cleanupCalled = true
242
- }
243
- })
244
- return h('div', null, 'with-cleanup')
245
- })
246
- const unmount = mount(h(Comp, null), el)
247
- expect(cleanupCalled).toBe(false)
248
-
249
- unmount()
250
- expect(cleanupCalled).toBe(true)
251
- })
252
-
253
- test('mount 10 components — unmount all — verify no lingering effects', () => {
254
- const el = container()
255
- let cleanupCount = 0
256
- const totalComponents = 10
257
-
258
- const Comp = defineComponent(() => {
259
- onUnmount(() => {
260
- cleanupCount++
261
- })
262
- return h('span', null, 'item')
263
- })
264
-
265
- const unmount = mount(
266
- h(
267
- 'div',
268
- null,
269
- ...Array.from({ length: totalComponents }, (_, i) => h(Comp, { key: i })),
270
- ),
271
- el,
272
- )
273
- expect(el.querySelectorAll('span').length).toBe(totalComponents)
274
-
275
- unmount()
276
- expect(cleanupCount).toBe(totalComponents)
277
- })
278
- })
279
-
280
- // ─── Deep nesting ───────────────────────────────────────────────────────────
281
-
282
- describe('lifecycle — deep nesting', () => {
283
- test('4 levels of components with signals — all reactive', () => {
284
- const el = container()
285
- const s1 = signal('L1')
286
- const s2 = signal('L2')
287
- const s3 = signal('L3')
288
- const s4 = signal('L4')
289
-
290
- const Level4 = () => h('span', { class: 'l4' }, () => s4())
291
- const Level3 = () => h('div', { class: 'l3' }, () => s3(), h(Level4, null))
292
- const Level2 = () => h('div', { class: 'l2' }, () => s2(), h(Level3, null))
293
- const Level1 = () => h('div', { class: 'l1' }, () => s1(), h(Level2, null))
294
-
295
- mount(h(Level1, null), el)
296
- expect(el.textContent).toContain('L1')
297
- expect(el.textContent).toContain('L2')
298
- expect(el.textContent).toContain('L3')
299
- expect(el.textContent).toContain('L4')
300
-
301
- s1.set('L1-updated')
302
- expect(el.textContent).toContain('L1-updated')
303
-
304
- s4.set('L4-updated')
305
- expect(el.textContent).toContain('L4-updated')
306
-
307
- // Middle levels also reactive
308
- s2.set('L2-updated')
309
- s3.set('L3-updated')
310
- expect(el.textContent).toContain('L2-updated')
311
- expect(el.textContent).toContain('L3-updated')
312
- })
313
-
314
- test('unmount root — all nested effects cleaned', () => {
315
- const el = container()
316
- let cleanupCount = 0
317
-
318
- const Level4 = defineComponent(() => {
319
- onUnmount(() => { cleanupCount++ })
320
- return h('span', null, 'l4')
321
- })
322
- const Level3 = defineComponent(() => {
323
- onUnmount(() => { cleanupCount++ })
324
- return h('div', null, 'l3', h(Level4, null))
325
- })
326
- const Level2 = defineComponent(() => {
327
- onUnmount(() => { cleanupCount++ })
328
- return h('div', null, 'l2', h(Level3, null))
329
- })
330
- const Level1 = defineComponent(() => {
331
- onUnmount(() => { cleanupCount++ })
332
- return h('div', null, 'l1', h(Level2, null))
333
- })
334
-
335
- const unmount = mount(h(Level1, null), el)
336
- expect(el.textContent).toContain('l1')
337
- expect(el.textContent).toContain('l4')
338
-
339
- unmount()
340
- expect(cleanupCount).toBe(4)
341
- })
342
- })
@@ -1,99 +0,0 @@
1
- import { For, h } from '@pyreon/core'
2
- import { signal } from '@pyreon/reactivity'
3
- import { flush, mountInBrowser } from '@pyreon/test-utils/browser'
4
- import { describe, expect, it } from 'vitest'
5
-
6
- // Real-Chromium coverage for the `computeForLis` known-slot fast path.
7
- // happy-dom is fine for op-counting the LIS probes but doesn't prove the
8
- // reconciler actually lays out the DOM in the right order after a prepend.
9
- // A bug in the fast path would silently corrupt the DOM under real CSS
10
- // layout — caught here, not in the vitest happy-dom suite.
11
-
12
- function buildRows(count: number, offset = 0): Array<{ id: number; label: string }> {
13
- return Array.from({ length: count }, (_, i) => ({
14
- id: i + offset,
15
- label: `row-${i + offset}`,
16
- }))
17
- }
18
-
19
- describe('runtime-dom LIS prepend fast path', () => {
20
- it('prepends 100 rows to a 100-row list, DOM matches the signal order', async () => {
21
- type Row = { id: number; label: string }
22
- const rows = signal<Row[]>(buildRows(100, 0))
23
- const { container, unmount } = mountInBrowser(
24
- h(
25
- 'ul',
26
- { id: 'list' },
27
- For({
28
- each: rows,
29
- by: (r: Row) => r.id,
30
- children: (r: Row) => h('li', { 'data-id': String(r.id) }, r.label),
31
- }),
32
- ),
33
- )
34
-
35
- let items = container.querySelectorAll<HTMLLIElement>('#list li')
36
- expect(items).toHaveLength(100)
37
- expect(items[0]?.dataset.id).toBe('0')
38
- expect(items[99]?.dataset.id).toBe('99')
39
-
40
- // Prepend 100 new rows with ids 100..199. Final list: [100..199, 0..99].
41
- const prepended = buildRows(100, 100)
42
- rows.set([...prepended, ...rows()])
43
- await flush()
44
-
45
- items = container.querySelectorAll<HTMLLIElement>('#list li')
46
- expect(items).toHaveLength(200)
47
- // First 100 items must be the prepended rows in order.
48
- expect(items[0]?.dataset.id).toBe('100')
49
- expect(items[99]?.dataset.id).toBe('199')
50
- // Next 100 must be the original rows in original order.
51
- expect(items[100]?.dataset.id).toBe('0')
52
- expect(items[199]?.dataset.id).toBe('99')
53
- unmount()
54
- })
55
-
56
- it('measured prepend wall-clock stays in the expected range', async () => {
57
- // HONEST framing: the LIS fast path saves ~50-100 µs on a 1k prepend.
58
- // The full prepend cost is dominated by DOM work (~5-20 ms in real
59
- // Chromium for 1k <li> nodes). This test measures the full wall-clock
60
- // to give a realistic upper bound — the LIS save is a single-digit
61
- // percent improvement, not a headline win.
62
- //
63
- // Assertion bound is intentionally loose (< 500 ms). The purpose is
64
- // to anchor a "is this catastrophically slow" ceiling, not to prove
65
- // the LIS fix is responsible for any particular chunk of time.
66
- type Row = { id: number; label: string }
67
- const rows = signal<Row[]>(buildRows(1000, 0))
68
- const { container, unmount } = mountInBrowser(
69
- h(
70
- 'ul',
71
- { id: 'perf-list' },
72
- For({
73
- each: rows,
74
- by: (r: Row) => r.id,
75
- children: (r: Row) => h('li', { 'data-id': String(r.id) }, r.label),
76
- }),
77
- ),
78
- )
79
-
80
- // Warm up — first mount allocates backing arrays.
81
- await flush()
82
- expect(container.querySelectorAll('#perf-list li')).toHaveLength(1000)
83
-
84
- const prepended = buildRows(1000, 1000)
85
- const t0 = performance.now()
86
- rows.set([...prepended, ...rows()])
87
- await flush()
88
- const elapsed = performance.now() - t0
89
-
90
- // oxlint-disable-next-line no-console
91
- console.log(`[lis-prepend] 1k→2k prepend wall-clock: ${elapsed.toFixed(2)}ms`)
92
-
93
- expect(container.querySelectorAll('#perf-list li')).toHaveLength(2000)
94
- // Loose ceiling. Real Chromium typically lands at 10-50ms. We're not
95
- // asserting the LIS win — we're asserting the whole path didn't break.
96
- expect(elapsed).toBeLessThan(500)
97
- unmount()
98
- })
99
- })
@@ -1,85 +0,0 @@
1
- import {
2
- renderApiReferenceEntries,
3
- renderLlmsFullSection,
4
- renderLlmsTxtLine,
5
- } from '@pyreon/manifest'
6
- import runtimeDomManifest from '../manifest'
7
-
8
- describe('gen-docs — runtime-dom snapshot', () => {
9
- it('renders @pyreon/runtime-dom to its expected llms.txt bullet', () => {
10
- expect(renderLlmsTxtLine(runtimeDomManifest)).toMatchInlineSnapshot(`"- @pyreon/runtime-dom — DOM renderer, mount, hydrateRoot, Transition, TransitionGroup, KeepAlive, SVG/MathML namespace, custom elements. SVG and MathML elements ALWAYS use \`setAttribute()\` for prop forwarding, never property assignment. Many SVG properties (\`markerWidth\`, \`refX\`, etc.) are read-only \`SVGAnimatedLength\` getters — \`el[key] = value\` crashes. Detected by \`el.namespaceURI !== "http://www.w3.org/1999/xhtml"\`."`)
11
- })
12
-
13
- it('renders @pyreon/runtime-dom to its expected llms-full.txt section — full body snapshot', () => {
14
- expect(renderLlmsFullSection(runtimeDomManifest)).toMatchInlineSnapshot(`
15
- "## @pyreon/runtime-dom — DOM Renderer
16
-
17
- Surgical signal-to-DOM renderer with zero virtual DOM overhead. The compiler emits \`_tpl()\` (cloneNode-based template instantiation) + \`_bind()\` (per-node reactive bindings) calls that mount directly to the DOM without VNode diffing. Reactive text uses \`TextNode.data\` assignment (not \`.textContent\`) for minimal DOM mutation. Supports SVG/MathML namespace auto-detection (67 tags), custom elements (props as properties), CSS transitions via \`<Transition>\` / \`<TransitionGroup>\`, and component caching via \`<KeepAlive>\`. Dev-mode warnings use \`import.meta.env.DEV\` (not \`typeof process\`) so they tree-shake to zero bytes in production Vite builds.
18
-
19
- \`\`\`typescript
20
- import { mount, hydrateRoot, Transition, TransitionGroup, KeepAlive } from "@pyreon/runtime-dom"
21
- import { signal } from "@pyreon/reactivity"
22
- import { Show, For } from "@pyreon/core"
23
-
24
- // Mount — clears container, returns unmount function
25
- const unmount = mount(<App />, document.getElementById("app")!)
26
-
27
- // Hydrate SSR-rendered HTML (preserves existing DOM)
28
- hydrateRoot(<App />, document.getElementById("app")!)
29
-
30
- // Transition — CSS-based enter/leave animations
31
- const visible = signal(true)
32
- const FadeExample = () => (
33
- <Transition name="fade" mode="out-in">
34
- <Show when={visible()}>
35
- <div>Content</div>
36
- </Show>
37
- </Transition>
38
- )
39
- // CSS: .fade-enter-active, .fade-leave-active { transition: opacity 0.3s }
40
- // .fade-enter-from, .fade-leave-to { opacity: 0 }
41
-
42
- // TransitionGroup — animate list items entering/leaving
43
- const items = signal([1, 2, 3])
44
- const ListExample = () => (
45
- <TransitionGroup name="list">
46
- <For each={items()} by={i => i}>
47
- {item => <div>{item}</div>}
48
- </For>
49
- </TransitionGroup>
50
- )
51
-
52
- // KeepAlive — cache component state across mount/unmount cycles
53
- const tab = signal<"a" | "b">("a")
54
- const TabExample = () => (
55
- <KeepAlive>
56
- <Show when={tab() === "a"}><ExpensiveA /></Show>
57
- <Show when={tab() === "b"}><ExpensiveB /></Show>
58
- </KeepAlive>
59
- )
60
- \`\`\`
61
-
62
- > **SVG/MathML uses setAttribute only**: SVG and MathML elements ALWAYS use \`setAttribute()\` for prop forwarding, never property assignment. Many SVG properties (\`markerWidth\`, \`refX\`, etc.) are read-only \`SVGAnimatedLength\` getters — \`el[key] = value\` crashes. Detected by \`el.namespaceURI !== "http://www.w3.org/1999/xhtml"\`.
63
- >
64
- > **Custom elements use property assignment**: Elements with a hyphen in their tag name (custom elements) get props set as JS properties, not HTML attributes. This matches the web components spec — attributes are strings, properties can be any type.
65
- >
66
- > **Transition 5s safety timeout**: If \`transitionend\` or \`animationend\` never fires (missing CSS, display:none, zero-duration), the transition completes automatically after 5 seconds to prevent stuck UI.
67
- >
68
- > **Dev warnings use import.meta.env.DEV**: All dev-mode warnings (\`mount()\` null container, duplicate keys, raw signal children) use \`import.meta.env.DEV\` — NOT \`typeof process\`. Vite/Rolldown literal-replaces it at build time; production bundles contain zero warning bytes. Tests run in vitest which sets DEV=true automatically.
69
- >
70
- > **Event delegation**: \`setupDelegation(container)\` is called by \`mount()\` — common events are delegated to the container root for performance. Direct event binding (non-delegated) is used for events that do not bubble (focus, blur, scroll, etc.).
71
- "
72
- `)
73
- })
74
-
75
- it('renders @pyreon/runtime-dom to MCP api-reference entries — one per api[] item', () => {
76
- const record = renderApiReferenceEntries(runtimeDomManifest)
77
- // +1: __PYREON_DEVTOOLS__ (reactive-devtools hook surface).
78
- expect(Object.keys(record).length).toBe(10)
79
- expect(Object.keys(record)).toContain('runtime-dom/mount')
80
- // Spot-check the flagship API — mount is the primary entry point
81
- const mount = record['runtime-dom/mount']!
82
- expect(mount.notes).toContain('container')
83
- expect(mount.mistakes?.split('\n').length).toBeGreaterThan(2)
84
- })
85
- })