@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 +115 -91
- package/lib/_chunks/keep-alive-BM7bn3W9.js +1657 -0
- package/lib/analysis/index.js.html +1 -1
- package/lib/index.js +8 -1821
- package/lib/keep-alive-entry.js +2 -1380
- package/lib/transition-entry.js +1 -1
- package/package.json +6 -6
- package/src/tests/ctx-stack-growth-repro.test.tsx +158 -0
- package/src/tests/error-boundary-stack-leak-repro.test.tsx +133 -0
- package/lib/analysis/keep-alive-entry.js.html +0 -5406
- package/lib/analysis/transition-entry.js.html +0 -5406
package/README.md
CHANGED
|
@@ -1,155 +1,179 @@
|
|
|
1
1
|
# @pyreon/runtime-dom
|
|
2
2
|
|
|
3
|
-
|
|
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
|
|
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
|
-
|
|
20
|
-
|
|
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
|
-
##
|
|
35
|
+
## mount / render / unmount
|
|
27
36
|
|
|
28
|
-
|
|
37
|
+
```ts
|
|
38
|
+
const unmount = mount(<App />, container)
|
|
39
|
+
unmount() // teardown effects, unmount subtree
|
|
40
|
+
```
|
|
29
41
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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
|
-
|
|
51
|
+
hydrateRoot(<App />, container)
|
|
35
52
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
54
|
-
|
|
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
|
-
|
|
82
|
+
Used by `innerHTML` / `dangerouslySetInnerHTML` paths. Default sanitizer is a conservative pass-through — install `DOMPurify` or equivalent for production.
|
|
63
83
|
|
|
64
|
-
|
|
84
|
+
## SVG / MathML / custom elements
|
|
65
85
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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
|
-
|
|
90
|
+
## Templates
|
|
71
91
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
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
|
-
|
|
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
|
-
|
|
99
|
+
## Compiler-emitted runtime helpers
|
|
84
100
|
|
|
85
|
-
|
|
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
|
-
|
|
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
|
-
|
|
92
|
-
- **`enableHydrationWarnings()` / `disableHydrationWarnings()`** -- Toggle console warnings for hydration mismatches.
|
|
112
|
+
## Event delegation
|
|
93
113
|
|
|
94
|
-
|
|
114
|
+
```ts
|
|
115
|
+
import { DELEGATED_EVENTS, delegatedPropName, setupDelegation } from '@pyreon/runtime-dom'
|
|
95
116
|
|
|
96
|
-
|
|
97
|
-
|
|
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
|
-
|
|
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
|
-
|
|
122
|
+
## Transition
|
|
104
123
|
|
|
105
|
-
|
|
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
|
-
|
|
108
|
-
|
|
129
|
+
<Transition name="fade" mode="out-in">
|
|
130
|
+
<Show when={visible()}><div>Content</div></Show>
|
|
131
|
+
</Transition>
|
|
132
|
+
```
|
|
109
133
|
|
|
110
|
-
|
|
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
|
-
```
|
|
113
|
-
import {
|
|
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
|
-
|
|
144
|
+
`TransitionGroup` adds FLIP-style move animations for keyed lists.
|
|
117
145
|
|
|
118
|
-
|
|
146
|
+
## KeepAlive
|
|
119
147
|
|
|
120
|
-
|
|
148
|
+
```tsx
|
|
149
|
+
import { KeepAlive } from '@pyreon/runtime-dom'
|
|
150
|
+
// or: import { KeepAlive } from '@pyreon/runtime-dom/keep-alive'
|
|
121
151
|
|
|
122
|
-
|
|
123
|
-
|
|
152
|
+
<KeepAlive>
|
|
153
|
+
{() => tab() === 'home' ? <Home /> : <Settings />}
|
|
154
|
+
</KeepAlive>
|
|
124
155
|
```
|
|
125
156
|
|
|
126
|
-
|
|
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
|
-
##
|
|
159
|
+
## Dev-mode gates
|
|
131
160
|
|
|
132
|
-
|
|
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
|
-
-
|
|
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
|
-
##
|
|
165
|
+
## Mount-pipeline optimizations
|
|
142
166
|
|
|
143
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|