@pyreon/runtime-dom 0.21.0 → 0.23.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/README.md CHANGED
@@ -1,155 +1,179 @@
1
1
  # @pyreon/runtime-dom
2
2
 
3
- DOM renderer for Pyreon. Performs surgical signal-to-DOM updates with no virtual DOM diffing.
3
+ Surgical signal-to-DOM renderer no virtual DOM, no diff.
4
+
5
+ Mounts VNode trees and compiler-emitted `_tpl()` cloneNode templates directly into the DOM; per-node `_bind()` calls produce surgical signal-to-DOM updates without VDOM diffing. Reactive text uses `TextNode.data` (not `.textContent`) for minimal mutation; SVG / MathML namespaces are auto-detected (67 tags); custom elements receive props as properties. Ships CSS-transition support (`<Transition>` / `<TransitionGroup>`) and component caching (`<KeepAlive>`) — each also available as a subpath export so apps that don't use animations or caching can tree-shake them out.
4
6
 
5
7
  ## Install
6
8
 
7
9
  ```bash
8
- bun add @pyreon/runtime-dom
10
+ bun add @pyreon/runtime-dom @pyreon/core @pyreon/reactivity
9
11
  ```
10
12
 
11
- ## Quick Start
13
+ ## Quick start
12
14
 
13
15
  ```tsx
14
- import { mount } from '@pyreon/runtime-dom'
16
+ import { mount, hydrateRoot, Transition, KeepAlive } from '@pyreon/runtime-dom'
15
17
  import { signal } from '@pyreon/reactivity'
18
+ import { Show } from '@pyreon/core'
16
19
 
17
20
  const count = signal(0)
18
21
 
19
- const App = () => (
20
- <button onClick={() => count.update((n) => n + 1)}>Clicks: {() => count()}</button>
21
- )
22
+ function App() {
23
+ return (
24
+ <button onClick={() => count.update(n => n + 1)}>
25
+ Clicks: {() => count()}
26
+ </button>
27
+ )
28
+ }
22
29
 
23
30
  const unmount = mount(<App />, document.getElementById('app')!)
31
+ // Or hydrate SSR-rendered markup:
32
+ hydrateRoot(<App />, document.getElementById('app')!)
24
33
  ```
25
34
 
26
- ## Transition Examples
35
+ ## mount / render / unmount
27
36
 
28
- Animate elements on enter and leave:
37
+ ```ts
38
+ const unmount = mount(<App />, container)
39
+ unmount() // teardown effects, unmount subtree
40
+ ```
29
41
 
30
- ```tsx
31
- import { Transition } from '@pyreon/runtime-dom'
32
- import { signal } from '@pyreon/reactivity'
42
+ `render` is an alias for `mount` (Solid-parity). `mountChild(child, parent, anchor)` is the low-level form for advanced integrations.
43
+
44
+ ## Hydration
45
+
46
+ ```ts
47
+ import {
48
+ hydrateRoot, enableHydrationWarnings, disableHydrationWarnings, onHydrationMismatch,
49
+ } from '@pyreon/runtime-dom'
33
50
 
34
- const show = signal(true)
51
+ hydrateRoot(<App />, container)
35
52
 
36
- const App = () => (
37
- <div>
38
- <button onClick={() => show.set(!show())}>Toggle</button>
39
- <Transition name="fade">{() => show() && <p>Hello!</p>}</Transition>
40
- </div>
41
- )
53
+ enableHydrationWarnings() // dev console warnings on mismatch
54
+ onHydrationMismatch((ctx) => {
55
+ // ctx: { type, node, expected, received, path }
56
+ reportToTelemetry(ctx)
57
+ })
42
58
  ```
43
59
 
44
- Animate keyed lists with move support:
60
+ The `_tpl` hydration path uses a framework-wide correctness-first SWAP: when the SSR DOM doesn't match the freshly-built template, the SSR subtree is replaced with the rebuilt one (same final DOM byte-for-byte for matched cases; correct DOM for mismatches without a crash). Reactivity survives across the swap.
45
61
 
46
- ```tsx
47
- import { TransitionGroup } from '@pyreon/runtime-dom'
48
- import { For } from '@pyreon/core'
49
- import { signal } from '@pyreon/reactivity'
62
+ ## applyProp / applyProps
50
63
 
51
- const items = signal([1, 2, 3])
64
+ ```ts
65
+ applyProp(el, 'class', { active: isActive() }) // cx-normalized
66
+ applyProp(el, 'style', { color: 'red' })
67
+ applyProp(el, 'onClick', handler)
68
+ applyProps(el, { class: 'btn', 'data-id': id })
69
+ ```
70
+
71
+ The `class` prop accepts strings, arrays, objects, or any nested mix — normalized via `cx()` from `@pyreon/core`. Event handlers may go through the delegation path (`DELEGATED_EVENTS` allowlist + `setupDelegation`) instead of `addEventListener` for common bubbling events.
72
+
73
+ ## HTML sanitization
74
+
75
+ ```ts
76
+ import { sanitizeHtml, setSanitizer } from '@pyreon/runtime-dom'
52
77
 
53
- const List = () => (
54
- <TransitionGroup name="list">
55
- <For each={items} by={(n) => n}>
56
- {(item) => <div>{() => item()}</div>}
57
- </For>
58
- </TransitionGroup>
59
- )
78
+ setSanitizer((html) => DOMPurify.sanitize(html))
79
+ sanitizeHtml('<script>…</script>') // routes through the active sanitizer
60
80
  ```
61
81
 
62
- ## KeepAlive Example
82
+ Used by `innerHTML` / `dangerouslySetInnerHTML` paths. Default sanitizer is a conservative pass-through — install `DOMPurify` or equivalent for production.
63
83
 
64
- Cache inactive component subtrees instead of destroying them:
84
+ ## SVG / MathML / custom elements
65
85
 
66
- ```tsx
67
- import { KeepAlive } from '@pyreon/runtime-dom'
68
- import { signal } from '@pyreon/reactivity'
86
+ - **SVG / MathML** tags (67 detected) are auto-created via `createElementNS` with the correct namespace URI.
87
+ - **SVG attribute application** ALWAYS uses `setAttribute()` (never property assignment) — many SVG properties (`SVGRectElement.x`, `SVGMarkerElement.refX`, etc.) are read-only `SVGAnimatedLength` getters and would crash on property write.
88
+ - **Custom elements** (tag name with hyphen) receive props as properties, not attributes — matches React/Vue/Solid convention.
69
89
 
70
- const tab = signal<'home' | 'settings'>('home')
90
+ ## Templates
71
91
 
72
- const App = () => (
73
- <div>
74
- <button onClick={() => tab.set('home')}>Home</button>
75
- <button onClick={() => tab.set('settings')}>Settings</button>
76
- <KeepAlive>{() => (tab() === 'home' ? <Home /> : <Settings />)}</KeepAlive>
77
- </div>
78
- )
92
+ ```ts
93
+ const template = createTemplate('<div class="card"><h1></h1><p></p></div>')
94
+ const el = template() // cloneNode under the hood
79
95
  ```
80
96
 
81
- ## API
97
+ `createTemplate(html)` is the user-facing reusable cloneNode factory. The compiler-emitted `_tpl(html)` is the same primitive — emitted automatically for any JSX element tree with ≥1 DOM tag.
82
98
 
83
- ### Mounting
99
+ ## Compiler-emitted runtime helpers
84
100
 
85
- - **`mount(root, container): () => void`** -- Clears the container and mounts the VNode tree. Returns an `unmount` function.
86
- - **`render`** -- Alias for `mount`.
87
- - **`mountChild(child, parent, anchor)`** -- Low-level mount of a single child node.
101
+ These symbols are emitted by `@pyreon/compiler`. Not for hand-written user code, but documented here as the contract:
88
102
 
89
- ### Hydration
103
+ | Symbol | Purpose |
104
+ |---|---|
105
+ | `_tpl(html)` | Parse + clone an HTML template once per literal |
106
+ | `_bindText(source, textNode)` | Reactive text — reads `source._v` directly on the fast path |
107
+ | `_bindDirect(source, el, key)` | Reactive attribute — fast path for primitive props |
108
+ | `_mountSlot(...)` | Mount a reactive child slot under a template anchor |
109
+ | `_applyProps(...)` | Spread props on a template element |
110
+ | `_rsCollapse(...)` / `_rsCollapseH(...)` | Rocketstyle compile-time-collapse mount paths |
90
111
 
91
- - **`hydrateRoot(root, container)`** -- Hydrates server-rendered HTML with client-side reactivity.
92
- - **`enableHydrationWarnings()` / `disableHydrationWarnings()`** -- Toggle console warnings for hydration mismatches.
112
+ ## Event delegation
93
113
 
94
- ### Props and Sanitization
114
+ ```ts
115
+ import { DELEGATED_EVENTS, delegatedPropName, setupDelegation } from '@pyreon/runtime-dom'
95
116
 
96
- - **`applyProp(el, key, value)`** -- Applies a single prop to a DOM element. The `class` prop accepts strings, arrays, objects, or nested mix (processed via `cx()` from `@pyreon/core`).
97
- - **`applyProps(el, props)`** -- Applies all props to a DOM element.
98
- - **`sanitizeHtml(html): string`** -- Sanitizes an HTML string using the active sanitizer.
99
- - **`setSanitizer(fn: SanitizeFn)`** -- Replaces the default HTML sanitizer.
117
+ setupDelegation(rootElement)
118
+ ```
100
119
 
101
- ### Templates
120
+ Common bubbling events (`click`, `input`, `submit`, …) are attached once at the root via `setupDelegation` and dispatched per-node via the matching prop name. Tree-shakeable; only used by event names in `DELEGATED_EVENTS`.
102
121
 
103
- - **`createTemplate(html): () => Element`** -- Creates a reusable DOM template factory from an HTML string.
122
+ ## Transition
104
123
 
105
- ### Transitions
124
+ ```tsx
125
+ import { Transition } from '@pyreon/runtime-dom'
126
+ // or, for explicit tree-shake:
127
+ // import { Transition } from '@pyreon/runtime-dom/transition'
106
128
 
107
- - **`Transition`** -- Animates a single child on enter/leave with CSS classes or JS hooks.
108
- - **`TransitionGroup`** -- Animates a list of keyed children, including move transitions.
129
+ <Transition name="fade" mode="out-in">
130
+ <Show when={visible()}><div>Content</div></Show>
131
+ </Transition>
132
+ ```
109
133
 
110
- Also available as a separate subpath export for apps that don't use animations:
134
+ CSS-class enter/leave animations + JS hooks (`onBeforeEnter`, `onAfterLeave`, etc.). A 5-second timeout fires `transitionend` automatically if no real `transitionend` / `animationend` event arrives — animations never get stuck.
111
135
 
112
- ```ts
113
- import { Transition, TransitionGroup } from '@pyreon/runtime-dom/transition'
136
+ ```tsx
137
+ import { TransitionGroup } from '@pyreon/runtime-dom'
138
+
139
+ <TransitionGroup name="list" tag="ul">
140
+ <For each={items} by={i => i.id}>{(item) => <li>{item.name}</li>}</For>
141
+ </TransitionGroup>
114
142
  ```
115
143
 
116
- ### KeepAlive
144
+ `TransitionGroup` adds FLIP-style move animations for keyed lists.
117
145
 
118
- - **`KeepAlive`** -- Caches inactive component subtrees instead of destroying them.
146
+ ## KeepAlive
119
147
 
120
- Also available as a separate subpath export:
148
+ ```tsx
149
+ import { KeepAlive } from '@pyreon/runtime-dom'
150
+ // or: import { KeepAlive } from '@pyreon/runtime-dom/keep-alive'
121
151
 
122
- ```ts
123
- import { KeepAlive } from '@pyreon/runtime-dom/keep-alive'
152
+ <KeepAlive>
153
+ {() => tab() === 'home' ? <Home /> : <Settings />}
154
+ </KeepAlive>
124
155
  ```
125
156
 
126
- ### Types
127
-
128
- `TransitionProps`, `TransitionGroupProps`, `KeepAliveProps`, `SanitizeFn`, `DevtoolsComponentEntry`, `PyreonDevtools`
157
+ Caches inactive subtrees instead of destroying them — preserves component state (form inputs, scroll positions, signals) across toggles. Pair with `<Show>` or a route guard for tab-style UIs.
129
158
 
130
- ## Production Performance
159
+ ## Dev-mode gates
131
160
 
132
- The mount pipeline is optimized for zero unnecessary allocations:
161
+ Dev-only warnings (e.g. duplicate `<For>` keys, mount of `null` container) are gated on `process.env.NODE_ENV !== 'production'` — the **bundler-agnostic library convention**. Every modern bundler (Vite, Webpack/Next.js, esbuild, Rollup, Parcel, Bun) auto-replaces `process.env.NODE_ENV` at consumer build time and tree-shakes the dev block to zero bytes in production. This is enforced repo-wide by the `pyreon/no-process-dev-gate` lint rule. Do NOT use `import.meta.env.DEV` (Vite/Rolldown-only) or `typeof process !== 'undefined' && …` (dead in real Vite browser bundles).
133
162
 
134
- - **Devtools gated on `__DEV__`** -- Component ID generation (`Math.random`), parent/child tracking (`_mountingStack`), and `registerComponent`/`unregisterComponent` are all behind `if (__DEV__)`. Vite tree-shakes the entire devtools module from production bundles.
135
- - **Lazy LifecycleHooks** -- `mount`/`unmount`/`update`/`error` arrays start as `null`, allocated on first hook registration. Components with no hooks (80%+) skip all hook iteration.
136
- - **Lazy mountCleanups** -- Only allocated when an `onMount` callback returns a cleanup function.
137
- - **makeReactiveProps scan-first** -- Scans for `_rp()` brands before allocating the getter-backed object. Static-only components return `raw` immediately.
138
- - **renderEffect first-run skip** -- Skips cleanup on first run since the deps array is empty.
139
- - **Text .data no-op writes** -- `_bindText` and `_bindDirect` skip DOM writes when the value hasn't changed.
163
+ A tree-shake regression test (`src/tests/dev-gate-treeshake.test.ts`) bundles `mount.ts` through Vite production and asserts warn strings are gone from the output.
140
164
 
141
- ## Dev-mode warnings — bundler tree-shake
165
+ ## Mount-pipeline optimizations
142
166
 
143
- Dev warnings are gated on `import.meta.env?.DEV`. Tree-shake behavior depends on both the source pattern and the consumer bundler:
167
+ - **Devtools gated on `__DEV__`** — component-ID generation (`Math.random`), `_mountingStack`, `registerComponent`/`unregisterComponent` all behind `if (__DEV__)`. Zero production cost.
168
+ - **Lazy `LifecycleHooks`** — `mount` / `unmount` / `update` / `error` arrays start as `null`; allocated on first hook. ~80% of components have no hooks → no allocation.
169
+ - **Lazy `mountCleanups`** — only allocated when an `onMount` callback returns a cleanup.
170
+ - **`makeReactiveProps` scan-first** — checks for `_rp` brands before allocating the result object. Static-only components (60%+) skip the object entirely.
171
+ - **`renderEffect` first-run skip** — empty deps on first run means no cleanup to invoke.
172
+ - **`TextNode.data` no-op writes** — `_bindText` / `_bindDirect` skip DOM writes when the value hasn't changed.
144
173
 
145
- | Source pattern | Vite prod | Raw esbuild prod | Test |
146
- | --- | --- | --- | --- |
147
- | `if (!import.meta.env?.DEV) return` (inline early-return) | tree-shaken | tree-shaken | `flow/src/tests/integration.test.ts` (esbuild) |
148
- | `const __DEV__ = ...; if (__DEV__) ...` | tree-shaken | mostly tree-shaken | `runtime-dom/src/tests/dev-gate-treeshake.test.ts` (Vite) |
149
- | `const __DEV__ = ...; __DEV__ && cond && warn(...)` (chained &&) | tree-shaken | runtime-gated only | `runtime-dom/.../dev-gate-treeshake.test.ts` (Vite + non-Vite runtime smoke) |
150
- | `typeof process !== 'undefined'` | dead in browser | dead in browser | `pyreon/no-process-dev-gate` lint rule |
174
+ ## Documentation
151
175
 
152
- Vite is Pyreon's primary supported bundler. Non-Vite consumers (webpack, bunchee, raw esbuild) using the chained `&&` form may retain warning strings as data, but the runtime gate evaluates to `false` when `import.meta.env.DEV` is undefined — warnings don't fire. Only a small bundle-size cost.
176
+ Full docs: [docs.pyreon.dev/docs/runtime-dom](https://docs.pyreon.dev/docs/runtime-dom) (or `docs/docs/runtime-dom.md` in this repo).
153
177
 
154
178
  ## License
155
179