@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
package/src/keep-alive.ts DELETED
@@ -1,83 +0,0 @@
1
- import type { Props, VNodeChild } from '@pyreon/core'
2
- import { createRef, h, nativeCompat, onMount } from '@pyreon/core'
3
- import { effect, runUntracked } from '@pyreon/reactivity'
4
- import { mountChild } from './mount'
5
-
6
- export interface KeepAliveProps extends Props {
7
- /**
8
- * Accessor that returns true when this slot's children should be visible.
9
- * When false, children are CSS-hidden but remain mounted — effects and
10
- * signals stay alive.
11
- * Defaults to true (always visible / always mounted).
12
- */
13
- active?: () => boolean
14
- children?: VNodeChild
15
- }
16
-
17
- /**
18
- * KeepAlive — mounts its children once and keeps them alive even when hidden.
19
- *
20
- * Unlike conditional rendering (which destroys and recreates component state),
21
- * KeepAlive CSS-hides the children while preserving all reactive state,
22
- * scroll position, form values, and in-flight async operations.
23
- *
24
- * Children are mounted imperatively on first activation and are never unmounted
25
- * while the KeepAlive itself is mounted.
26
- *
27
- * Multi-slot pattern (one KeepAlive per route):
28
- * @example
29
- * h(Fragment, null, [
30
- * h(KeepAlive, { active: () => route() === "/a" }, h(RouteA, null)),
31
- * h(KeepAlive, { active: () => route() === "/b" }, h(RouteB, null)),
32
- * ])
33
- *
34
- * With JSX:
35
- * @example
36
- * <>
37
- * <KeepAlive active={() => route() === "/a"}><RouteA /></KeepAlive>
38
- * <KeepAlive active={() => route() === "/b"}><RouteB /></KeepAlive>
39
- * </>
40
- */
41
- export function KeepAlive(props: KeepAliveProps): VNodeChild {
42
- const containerRef = createRef<HTMLElement>()
43
- let childCleanup: (() => void) | null = null
44
- let childMounted = false
45
-
46
- onMount(() => {
47
- const container = containerRef.current
48
- if (!container) return
49
-
50
- const e = effect(() => {
51
- const isActive = props.active?.() ?? true
52
-
53
- if (!childMounted) {
54
- // Mount children UNTRACKED — child component setup must not
55
- // subscribe this effect. Otherwise an unrelated signal flip in
56
- // the children would re-run KeepAlive's effect, runCleanup()
57
- // would dispose the children's inner effects (because they were
58
- // collected as inner effects of this run via _innerEffectCollector),
59
- // and the `if (!childMounted)` guard would skip re-mount → the
60
- // children become permanently un-reactive while still rendered.
61
- // Same shape as the mountFor / mountKeyedList fix in nodes.ts.
62
- childCleanup = runUntracked(() => mountChild(props.children ?? null, container, null))
63
- childMounted = true
64
- }
65
-
66
- // Show/hide without unmounting — state is fully preserved
67
- container.style.display = isActive ? '' : 'none'
68
- })
69
-
70
- return () => {
71
- e.dispose()
72
- childCleanup?.()
73
- }
74
- })
75
-
76
- // `display: contents` makes the wrapper transparent to layout
77
- // (children appear as if directly in the parent flow)
78
- return h('div', { ref: containerRef, style: 'display: contents' })
79
- }
80
-
81
- // Mark as native so compat-mode jsx() runtimes skip wrapCompatComponent —
82
- // KeepAlive uses onMount + effect + mountChild that need Pyreon's setup frame.
83
- nativeCompat(KeepAlive)
package/src/manifest.ts DELETED
@@ -1,236 +0,0 @@
1
- import { defineManifest } from '@pyreon/manifest'
2
-
3
- export default defineManifest({
4
- name: '@pyreon/runtime-dom',
5
- title: 'DOM Renderer',
6
- tagline:
7
- 'DOM renderer, mount, hydrateRoot, Transition, TransitionGroup, KeepAlive, SVG/MathML namespace, custom elements',
8
- description:
9
- '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.',
10
- category: 'browser',
11
- longExample: `import { mount, hydrateRoot, Transition, TransitionGroup, KeepAlive } from "@pyreon/runtime-dom"
12
- import { signal } from "@pyreon/reactivity"
13
- import { Show, For } from "@pyreon/core"
14
-
15
- // Mount — clears container, returns unmount function
16
- const unmount = mount(<App />, document.getElementById("app")!)
17
-
18
- // Hydrate SSR-rendered HTML (preserves existing DOM)
19
- hydrateRoot(<App />, document.getElementById("app")!)
20
-
21
- // Transition — CSS-based enter/leave animations
22
- const visible = signal(true)
23
- const FadeExample = () => (
24
- <Transition name="fade" mode="out-in">
25
- <Show when={visible()}>
26
- <div>Content</div>
27
- </Show>
28
- </Transition>
29
- )
30
- // CSS: .fade-enter-active, .fade-leave-active { transition: opacity 0.3s }
31
- // .fade-enter-from, .fade-leave-to { opacity: 0 }
32
-
33
- // TransitionGroup — animate list items entering/leaving
34
- const items = signal([1, 2, 3])
35
- const ListExample = () => (
36
- <TransitionGroup name="list">
37
- <For each={items()} by={i => i}>
38
- {item => <div>{item}</div>}
39
- </For>
40
- </TransitionGroup>
41
- )
42
-
43
- // KeepAlive — cache component state across mount/unmount cycles
44
- const tab = signal<"a" | "b">("a")
45
- const TabExample = () => (
46
- <KeepAlive>
47
- <Show when={tab() === "a"}><ExpensiveA /></Show>
48
- <Show when={tab() === "b"}><ExpensiveB /></Show>
49
- </KeepAlive>
50
- )`,
51
- features: [
52
- 'mount() — mount VNode tree into container, returns unmount function',
53
- 'hydrateRoot() — hydrate SSR-rendered HTML, preserving existing DOM',
54
- 'Transition — CSS-based enter/leave animations with mode support',
55
- 'TransitionGroup — animate list item additions and removals',
56
- 'KeepAlive — cache and restore component state across mount/unmount cycles',
57
- '_tpl() + _bind() — compiler-driven template instantiation with zero VNode overhead',
58
- 'SVG/MathML — 67 tags auto-detected, correct namespace URI, setAttribute-only',
59
- 'Custom elements — props set as properties on hyphenated tag names',
60
- 'Event delegation — synthetic event system for performance',
61
- 'Dev-mode warnings — container validation, output validation, duplicate keys',
62
- ],
63
- api: [
64
- {
65
- name: 'mount',
66
- kind: 'function',
67
- signature: 'mount(root: VNodeChild, container: Element): () => void',
68
- summary:
69
- 'Mount a VNode tree into a container element. Clears the container first, sets up event delegation, then mounts the given child. Returns an `unmount` function that removes everything and disposes all effects. In dev mode, throws if `container` is null/undefined with an actionable error message.',
70
- example: `import { mount } from "@pyreon/runtime-dom"
71
-
72
- const dispose = mount(<App />, document.getElementById("app")!)
73
-
74
- // To unmount:
75
- dispose()`,
76
- mistakes: [
77
- '`createRoot(container).render(<App />)` — Pyreon uses a single function call: `mount(<App />, container)`',
78
- '`mount(<App />, document.getElementById("app"))` without `!` — getElementById returns `Element | null`. The runtime throws in dev if null, but TypeScript needs the assertion',
79
- '`mount(<App />, document.body)` — mounting directly to body is discouraged; use a dedicated container element',
80
- 'Forgetting to call the returned unmount function — leaks event listeners and effects. Store and call it on cleanup',
81
- ],
82
- seeAlso: ['hydrateRoot', 'render'],
83
- },
84
- {
85
- name: 'render',
86
- kind: 'function',
87
- signature: 'render(root: VNodeChild, container: Element): () => void',
88
- summary:
89
- 'Alias for `mount`. Provided for API familiarity — both names point to the same function.',
90
- example: `import { render } from "@pyreon/runtime-dom"
91
- render(<App />, document.getElementById("app")!)`,
92
- seeAlso: ['mount'],
93
- },
94
- {
95
- name: 'hydrateRoot',
96
- kind: 'function',
97
- signature: 'hydrateRoot(root: VNodeChild, container: Element): () => void',
98
- summary:
99
- 'Hydrate server-rendered HTML. Walks the existing DOM and attaches reactive bindings without recreating elements. Expects the DOM to match the VNode tree structure — mismatches emit dev-mode warnings. Returns an unmount function.',
100
- example: `import { hydrateRoot } from "@pyreon/runtime-dom"
101
-
102
- // Hydrate SSR-rendered HTML:
103
- hydrateRoot(<App />, document.getElementById("app")!)`,
104
- seeAlso: ['mount', '@pyreon/runtime-server'],
105
- },
106
- {
107
- name: 'Transition',
108
- kind: 'component',
109
- signature: '<Transition name={name} mode={mode} onEnter={fn} onLeave={fn}>{children}</Transition>',
110
- summary:
111
- 'CSS-based enter/leave animation wrapper. Applies `{name}-enter-from`, `{name}-enter-active`, `{name}-enter-to` classes on enter and the corresponding `-leave-*` classes on leave. `mode` controls sequencing: `"out-in"` waits for leave to complete before entering, `"in-out"` enters first. Has a 5-second safety timeout — if `transitionend`/`animationend` never fires, the transition completes automatically.',
112
- example: `<Transition name="fade" mode="out-in">
113
- <Show when={visible()}>
114
- <div>Content</div>
115
- </Show>
116
- </Transition>
117
-
118
- /* CSS:
119
- .fade-enter-active, .fade-leave-active { transition: opacity 0.3s }
120
- .fade-enter-from, .fade-leave-to { opacity: 0 }
121
- */`,
122
- mistakes: [
123
- 'Missing CSS classes — `<Transition name="fade">` does nothing without `.fade-enter-active` / `.fade-leave-active` CSS',
124
- 'Wrapping multiple root elements — Transition expects a single child (or null). Multiple children cause undefined behavior',
125
- 'Using `mode="in-out"` when you want sequential — `"out-in"` is almost always what you want (old leaves, then new enters)',
126
- ],
127
- seeAlso: ['TransitionGroup', '@pyreon/kinetic'],
128
- },
129
- {
130
- name: 'TransitionGroup',
131
- kind: 'component',
132
- signature: '<TransitionGroup name={name} tag={tag}>{children}</TransitionGroup>',
133
- summary:
134
- 'Animate list item additions and removals with CSS transitions. Each item gets enter/leave classes on mount/unmount. The `tag` prop controls the wrapper element (defaults to a fragment). Works with `<For>` for reactive lists. Also applies `-move` classes for FLIP-animated reordering.',
135
- example: `<TransitionGroup name="list" tag="ul">
136
- <For each={items()} by={i => i.id}>
137
- {item => <li>{item.name}</li>}
138
- </For>
139
- </TransitionGroup>
140
-
141
- /* CSS:
142
- .list-enter-active, .list-leave-active { transition: all 0.3s }
143
- .list-enter-from, .list-leave-to { opacity: 0; transform: translateY(10px) }
144
- .list-move { transition: transform 0.3s }
145
- */`,
146
- seeAlso: ['Transition', 'For'],
147
- },
148
- {
149
- name: 'KeepAlive',
150
- kind: 'component',
151
- signature: '<KeepAlive include={pattern} exclude={pattern} max={number}>{children}</KeepAlive>',
152
- summary:
153
- 'Cache component instances across mount/unmount cycles so their state (signals, scroll position, form inputs) is preserved when they are toggled out and back in. `include`/`exclude` filter by component name. `max` limits cache size (LRU eviction). Useful for tab panels and multi-step forms.',
154
- example: `const tab = signal<"a" | "b">("a")
155
-
156
- <KeepAlive>
157
- <Show when={tab() === "a"}><ExpensiveFormA /></Show>
158
- <Show when={tab() === "b"}><ExpensiveFormB /></Show>
159
- </KeepAlive>`,
160
- seeAlso: ['Transition', 'Show'],
161
- },
162
- {
163
- name: '_tpl',
164
- kind: 'function',
165
- signature: '_tpl(html: string): () => DocumentFragment',
166
- summary:
167
- 'Compiler-internal: create a template factory from an HTML string. First call parses the HTML into a `<template>` element; subsequent calls use `cloneNode(true)` for zero-parse instantiation. Not intended for direct use — the JSX compiler emits `_tpl()` calls automatically.',
168
- example: `// Compiler output (not hand-written):
169
- const _$t0 = _tpl("<div class=\\"container\\"><span></span></div>")`,
170
- seeAlso: ['_bindText', '_bindDirect'],
171
- },
172
- {
173
- name: '_bindText',
174
- kind: 'function',
175
- signature: '_bindText(fn: () => string, node: Text): void',
176
- summary:
177
- 'Compiler-internal: bind a reactive expression to a text node via `TextNode.data` assignment. Creates a `renderEffect` that re-runs when tracked signals change. Each text node gets its own independent binding for fine-grained reactivity.',
178
- example: `// Compiler output for <div>{count()}</div>:
179
- const _$t = _tpl("<div> </div>")
180
- const _$n = _$t()
181
- _bindText(() => count(), _$n.firstChild)`,
182
- seeAlso: ['_tpl', '_bindDirect'],
183
- },
184
- {
185
- name: 'sanitizeHtml',
186
- kind: 'function',
187
- signature: 'sanitizeHtml(html: string): string',
188
- summary:
189
- 'Sanitize an HTML string using the registered sanitizer (set via `setSanitizer()`). Falls back to the identity function if no sanitizer is registered. Used by the runtime when setting `innerHTML` on elements.',
190
- example: `import { setSanitizer, sanitizeHtml } from "@pyreon/runtime-dom"
191
- setSanitizer(DOMPurify.sanitize)
192
- const clean = sanitizeHtml(userInput)`,
193
- seeAlso: ['setSanitizer'],
194
- },
195
- {
196
- name: '__PYREON_DEVTOOLS__',
197
- kind: 'constant',
198
- signature:
199
- 'window.__PYREON_DEVTOOLS__: { version; getComponentTree(); getAllComponents(); highlight(id); onComponentMount(cb); onComponentUnmount(cb); enableOverlay(); disableOverlay(); reactive: PyreonReactiveDevtools }',
200
- summary:
201
- 'Browser devtools hook, installed automatically on the first `mount()` (no-op on the server). Exposes the component tree + an element-picker overlay (also `Ctrl+Shift+P`) for the `@pyreon/devtools` Chrome extension, plus a `$p` console helper. The `reactive` namespace bridges `@pyreon/reactivity`’s opt-in graph: `reactive.activate()` / `deactivate()` start/stop tracking, `reactive.getGraph()` returns the live signal/computed/effect nodes + dependency edges, `reactive.getFires()` the bounded fire timeline — powering the extension’s Signals / Graph / Effects / Profiler / Console tabs. **Dev-only and tree-shaken from production builds**; `reactive` is zero-cost until `activate()` is called by an attached panel.',
202
- example: `// In the browser console (after the app has mounted):
203
- $p.tree() // root component entries
204
- window.__PYREON_DEVTOOLS__.reactive.activate()
205
- window.__PYREON_DEVTOOLS__.reactive.getGraph() // { nodes, edges }`,
206
- mistakes: [
207
- 'Reading it before the first `mount()` — it is installed by mount; it is `undefined` until then (and always `undefined` on the server / in production builds)',
208
- 'Expecting `reactive.getGraph()` to return data without calling `reactive.activate()` first — tracking is opt-in (zero-cost until a panel attaches)',
209
- 'Depending on it in app code — it is a dev-tooling hook, tree-shaken in production; never branch runtime behavior on its presence',
210
- ],
211
- seeAlso: ['mount'],
212
- },
213
- ],
214
- gotchas: [
215
- {
216
- label: 'SVG/MathML uses setAttribute only',
217
- note: '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"`.',
218
- },
219
- {
220
- label: 'Custom elements use property assignment',
221
- note: '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.',
222
- },
223
- {
224
- label: 'Transition 5s safety timeout',
225
- note: 'If `transitionend` or `animationend` never fires (missing CSS, display:none, zero-duration), the transition completes automatically after 5 seconds to prevent stuck UI.',
226
- },
227
- {
228
- label: 'Dev warnings use import.meta.env.DEV',
229
- note: '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.',
230
- },
231
- {
232
- label: 'Event delegation',
233
- note: '`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.).',
234
- },
235
- ],
236
- })