@pyreon/runtime-dom 0.24.4 → 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/hydrate.ts DELETED
@@ -1,450 +0,0 @@
1
- /**
2
- * SSR Hydration — "walk-and-claim" strategy.
3
- *
4
- * The server renders plain HTML (no special markers needed). On the client,
5
- * hydrateRoot walks the VNode tree in parallel with the live DOM tree and:
6
- *
7
- * - Static elements → matched by tag position, props attached (events + reactive effects)
8
- * - Static text → existing text node reused
9
- * - Reactive text → existing text node found, reactive effect attached to .data
10
- * - Reactive blocks → comment marker inserted, mountReactive takes over
11
- * - Components → component fn called, output VNode matched against DOM subtree
12
- * - For lists → full remount (can't map keys to DOM without SSR markers)
13
- * - Fragment → transparent, children matched directly
14
- * - Portal → always remounts into target
15
- *
16
- * Falls back to mountChild() whenever DOM structure doesn't match the VNode.
17
- */
18
-
19
- import type { ComponentFn, RefProp, VNode, VNodeChild } from '@pyreon/core'
20
- import {
21
- dispatchToErrorBoundary,
22
- ForSymbol,
23
- Fragment,
24
- makeReactiveProps,
25
- PortalSymbol,
26
- reportError,
27
- runWithHooks,
28
- } from '@pyreon/core'
29
- import { effectScope, renderEffect, runUntracked, setCurrentScope } from '@pyreon/reactivity'
30
- import { setupDelegation } from './delegate'
31
- import { warnHydrationMismatch } from './hydration-debug'
32
- import { mountChild } from './mount'
33
- import { mountReactive } from './nodes'
34
- import { applyProps } from './props'
35
-
36
- type Cleanup = () => void
37
- const noop: Cleanup = () => {
38
- /* noop */
39
- }
40
-
41
- // ─── DOM cursor helpers ───────────────────────────────────────────────────────
42
-
43
- /** Skip comment and whitespace-only text nodes, return first "real" node */
44
- function firstReal(initialNode: ChildNode | null): ChildNode | null {
45
- let node = initialNode
46
- while (node) {
47
- if (node.nodeType === Node.COMMENT_NODE) {
48
- node = node.nextSibling
49
- continue
50
- }
51
- if (node.nodeType === Node.TEXT_NODE && isWhitespaceOnly((node as Text).data)) {
52
- node = node.nextSibling
53
- continue
54
- }
55
- return node
56
- }
57
- return null
58
- }
59
-
60
- /** Check if a string is whitespace-only without allocating a trimmed copy. */
61
- function isWhitespaceOnly(s: string): boolean {
62
- for (let i = 0; i < s.length; i++) {
63
- const c = s.charCodeAt(i)
64
- // space, tab, newline, carriage return, form feed
65
- if (c !== 32 && c !== 9 && c !== 10 && c !== 13 && c !== 12) return false
66
- }
67
- return true
68
- }
69
-
70
- /** Advance past a node, skipping whitespace-only text and comments */
71
- function nextReal(node: ChildNode): ChildNode | null {
72
- return firstReal(node.nextSibling)
73
- }
74
-
75
- // ─── Core recursive walker ────────────────────────────────────────────────────
76
-
77
- /**
78
- * Hydrate a single VNodeChild against the DOM subtree starting at `domNode`.
79
- * Returns [cleanup, nextDomSibling].
80
- */
81
- /** Insert a comment marker before domNode (or append if domNode is null). */
82
- function insertMarker(parent: Node, domNode: ChildNode | null, text: string): Comment {
83
- const marker = document.createComment(text)
84
- if (domNode) {
85
- parent.insertBefore(marker, domNode)
86
- } else {
87
- parent.appendChild(marker)
88
- }
89
- return marker
90
- }
91
-
92
- /** Hydrate a reactive accessor (function child). */
93
- function hydrateReactiveChild(
94
- child: () => VNodeChild,
95
- domNode: ChildNode | null,
96
- parent: Node,
97
- anchor: Node | null,
98
- path: string,
99
- ): [Cleanup, ChildNode | null] {
100
- const initial = runUntracked(child)
101
-
102
- if (initial == null || initial === false) {
103
- const marker = insertMarker(parent, domNode, 'pyreon')
104
- const cleanup = mountReactive(child, parent, marker, mountChild)
105
- return [cleanup, domNode]
106
- }
107
-
108
- if (typeof initial === 'string' || typeof initial === 'number' || typeof initial === 'boolean') {
109
- return hydrateReactiveText(
110
- child as () => string | number | boolean | null | undefined,
111
- domNode,
112
- parent,
113
- anchor,
114
- path,
115
- )
116
- }
117
-
118
- // Reactive accessor that produces a VNode/NativeItem subtree.
119
- const next = domNode ? nextReal(domNode) : null
120
- if (domNode && domNode.parentNode) {
121
- domNode.parentNode.removeChild(domNode)
122
- }
123
- const marker = insertMarker(parent, next, 'pyreon')
124
- const cleanup = mountReactive(child, parent, marker, mountChild)
125
- return [cleanup, next]
126
- }
127
-
128
- /** Hydrate a reactive text binding against an existing text node. */
129
- function hydrateReactiveText(
130
- child: () => string | number | boolean | null | undefined,
131
- domNode: ChildNode | null,
132
- parent: Node,
133
- anchor: Node | null,
134
- path: string,
135
- ): [Cleanup, ChildNode | null] {
136
- if (domNode?.nodeType === Node.TEXT_NODE) {
137
- const textNode = domNode as Text
138
- const dispose = renderEffect(() => {
139
- const v = child()
140
- textNode.data = v == null ? '' : String(v)
141
- })
142
- return [dispose, nextReal(domNode)]
143
- }
144
- warnHydrationMismatch('text', 'TextNode', domNode?.nodeType ?? 'null', `${path} > reactive`)
145
- const cleanup = mountChild(child, parent, anchor)
146
- return [cleanup, domNode]
147
- }
148
-
149
- /** Hydrate a VNode (fragment, For, Portal, component, element). */
150
- function hydrateVNode(
151
- vnode: VNode,
152
- domNode: ChildNode | null,
153
- parent: Node,
154
- anchor: Node | null,
155
- path: string,
156
- ): [Cleanup, ChildNode | null] {
157
- if (vnode.type === Fragment) {
158
- return hydrateChildren(vnode.children ?? [], domNode, parent, anchor, path)
159
- }
160
-
161
- if (vnode.type === ForSymbol) {
162
- const marker = insertMarker(parent, domNode, 'pyreon-for')
163
- const cleanup = mountChild(vnode, parent, marker)
164
- return [cleanup, null]
165
- }
166
-
167
- if (vnode.type === PortalSymbol) {
168
- const cleanup = mountChild(vnode, parent, anchor)
169
- return [cleanup, domNode]
170
- }
171
-
172
- if (typeof vnode.type === 'function') {
173
- return hydrateComponent(vnode, domNode, parent, anchor, path)
174
- }
175
-
176
- if (typeof vnode.type === 'string') {
177
- return hydrateElement(vnode, domNode, parent, anchor, path)
178
- }
179
-
180
- return [noop, domNode]
181
- }
182
-
183
- function hydrateChild(
184
- child: VNodeChild | VNodeChild[],
185
- domNode: ChildNode | null,
186
- parent: Node,
187
- anchor: Node | null,
188
- path = 'root',
189
- ): [Cleanup, ChildNode | null] {
190
- if (Array.isArray(child)) {
191
- const cleanups: Cleanup[] = []
192
- let cursor = domNode
193
- for (const c of child) {
194
- const [cleanup, next] = hydrateChild(c, cursor, parent, anchor, path)
195
- cleanups.push(cleanup)
196
- cursor = next
197
- }
198
- return [
199
- () => {
200
- for (const c of cleanups) c()
201
- },
202
- cursor,
203
- ]
204
- }
205
-
206
- if (child == null || child === false) return [noop, domNode]
207
-
208
- if (typeof child === 'function') {
209
- return hydrateReactiveChild(child as () => VNodeChild, domNode, parent, anchor, path)
210
- }
211
-
212
- if (typeof child === 'string' || typeof child === 'number') {
213
- if (domNode?.nodeType === Node.TEXT_NODE) {
214
- return [() => (domNode as Text).remove(), nextReal(domNode)]
215
- }
216
- warnHydrationMismatch('text', 'TextNode', domNode?.nodeType ?? 'null', `${path} > text`)
217
- const cleanup = mountChild(child, parent, anchor)
218
- return [cleanup, domNode]
219
- }
220
-
221
- // NativeItem — output of the compiler's `_tpl()` template fast path. The
222
- // client builds a fresh DOM subtree in memory (cloned + reactively bound).
223
- // We don't yet have a true hydration mode for `_tpl` (which would adopt
224
- // existing DOM nodes and rebind without remount). For now, swap the SSR
225
- // subtree at this position for the freshly-mounted one — same final DOM,
226
- // no duplication, reactivity intact. This is correctness-first; a true
227
- // adopting hydration is a separate compiler-side change.
228
- if ((child as unknown as { __isNative?: boolean })?.__isNative === true) {
229
- const native = child as unknown as { __isNative: true; el: Node; cleanup?: () => void }
230
- const next = domNode ? nextReal(domNode) : null
231
- if (domNode && domNode.parentNode) {
232
- domNode.parentNode.replaceChild(native.el, domNode)
233
- } else {
234
- parent.insertBefore(native.el, anchor)
235
- }
236
- const cleanup = () => {
237
- native.cleanup?.()
238
- const p = native.el.parentNode
239
- if (p && (p as Element).isConnected !== false) p.removeChild(native.el)
240
- }
241
- return [cleanup, next]
242
- }
243
-
244
- return hydrateVNode(child as VNode, domNode, parent, anchor, path)
245
- }
246
-
247
- // ─── Element hydration ────────────────────────────────────────────────────────
248
-
249
- function hydrateElement(
250
- vnode: VNode,
251
- domNode: ChildNode | null,
252
- parent: Node,
253
- anchor: Node | null,
254
- path = 'root',
255
- ): [Cleanup, ChildNode | null] {
256
- const elPath = `${path} > ${vnode.type as string}`
257
-
258
- // Check if existing DOM node matches
259
- if (
260
- domNode?.nodeType === Node.ELEMENT_NODE &&
261
- (domNode as Element).tagName.toLowerCase() === vnode.type
262
- ) {
263
- const el = domNode as Element
264
- const cleanups: Cleanup[] = []
265
-
266
- // Attach props (events + reactive effects) — don't set static attrs (SSR already did)
267
- const propCleanup = applyProps(el, vnode.props)
268
- if (propCleanup) cleanups.push(propCleanup)
269
-
270
- // Hydrate children
271
- const firstChild = firstReal(el.firstChild as ChildNode | null)
272
- const [childCleanup] = hydrateChildren(vnode.children ?? [], firstChild, el, null, elPath)
273
- cleanups.push(childCleanup)
274
-
275
- // Set ref
276
- const ref = vnode.props.ref as RefProp<Element> | undefined
277
- if (ref) {
278
- if (typeof ref === 'function') ref(el)
279
- else ref.current = el
280
- }
281
-
282
- const cleanup = () => {
283
- if (ref) {
284
- if (typeof ref === 'function') ref(null)
285
- else ref.current = null
286
- }
287
- for (const c of cleanups) c()
288
- el.remove()
289
- }
290
-
291
- return [cleanup, nextReal(domNode)]
292
- }
293
-
294
- // Mismatch — fall back to fresh mount
295
- const actual =
296
- domNode?.nodeType === Node.ELEMENT_NODE
297
- ? (domNode as Element).tagName.toLowerCase()
298
- : (domNode?.nodeType ?? 'null')
299
- warnHydrationMismatch('tag', vnode.type, actual, elPath)
300
- const cleanup = mountChild(vnode, parent, anchor)
301
- return [cleanup, domNode]
302
- }
303
-
304
- // ─── Children hydration ───────────────────────────────────────────────────────
305
-
306
- function hydrateChildren(
307
- children: VNodeChild[],
308
- domNode: ChildNode | null,
309
- parent: Node,
310
- anchor: Node | null,
311
- path = 'root',
312
- ): [Cleanup, ChildNode | null] {
313
- if (children.length === 0) return [noop, domNode]
314
-
315
- // Single-child fast path — avoids cleanups array allocation
316
- if (children.length === 1) {
317
- return hydrateChild(children[0] as VNodeChild, domNode, parent, anchor, path)
318
- }
319
-
320
- const cleanups: Cleanup[] = []
321
- let cursor = domNode
322
- for (const child of children) {
323
- const [cleanup, next] = hydrateChild(child, cursor, parent, anchor, path)
324
- cleanups.push(cleanup)
325
- cursor = next
326
- }
327
- return [
328
- () => {
329
- for (const c of cleanups) c()
330
- },
331
- cursor,
332
- ]
333
- }
334
-
335
- // ─── Component hydration ──────────────────────────────────────────────────────
336
-
337
- function hydrateComponent(
338
- vnode: VNode,
339
- domNode: ChildNode | null,
340
- parent: Node,
341
- anchor: Node | null,
342
- path = 'root',
343
- ): [Cleanup, ChildNode | null] {
344
- const scope = effectScope()
345
- setCurrentScope(scope)
346
-
347
- let subtreeCleanup: Cleanup = noop
348
- const mountCleanups: Cleanup[] = []
349
- let nextDom: ChildNode | null = domNode
350
-
351
- // Function.name is always a string per spec; || handles empty string, avoids uncoverable ?? branch
352
- const componentName = ((vnode.type as ComponentFn).name || 'Anonymous') as string
353
- const rawProps =
354
- (vnode.children ?? []).length > 0 &&
355
- (vnode.props as Record<string, unknown>).children === undefined
356
- ? {
357
- ...vnode.props,
358
- children:
359
- (vnode.children ?? []).length === 1
360
- ? (vnode.children ?? [])[0]
361
- : (vnode.children ?? []),
362
- }
363
- : (vnode.props as Record<string, unknown>)
364
- // Convert compiler-emitted `_rp(() => expr)` wrappers into getter properties —
365
- // mirrors mount.ts so component code can read `props.x` and get the resolved
366
- // value (not the raw `_rp` function). Without this, hydration set up reactive
367
- // bindings against the wrong values and any signal-driven re-render would
368
- // diverge from the SSR HTML.
369
- const mergedProps = makeReactiveProps(rawProps as Record<string, unknown>)
370
-
371
- let result: ReturnType<typeof runWithHooks>
372
- try {
373
- result = runWithHooks(vnode.type as ComponentFn, mergedProps)
374
- } catch (err) {
375
- setCurrentScope(null)
376
- scope.stop()
377
-
378
- console.error(`[Pyreon] Error hydrating component <${componentName}>:`, err)
379
- reportError({
380
- component: componentName,
381
- phase: 'setup',
382
- error: err,
383
- timestamp: Date.now(),
384
- props: vnode.props as Record<string, unknown>,
385
- })
386
- dispatchToErrorBoundary(err)
387
- return [noop, domNode]
388
- }
389
- setCurrentScope(null)
390
-
391
- const { vnode: output, hooks } = result
392
-
393
- // Register onUpdate hooks with the scope
394
- if (hooks.update) {
395
- for (const fn of hooks.update) scope.addUpdateHook(fn)
396
- }
397
-
398
- if (output != null) {
399
- const [childCleanup, next] = hydrateChild(output, domNode, parent, anchor, path)
400
- subtreeCleanup = childCleanup
401
- nextDom = next
402
- }
403
-
404
- // Fire onMount hooks; effects created inside are tracked by the scope via runInScope
405
- if (hooks.mount) {
406
- for (const fn of hooks.mount) {
407
- try {
408
- let c: (() => void) | undefined
409
- scope.runInScope(() => {
410
- c = fn() as (() => void) | undefined
411
- })
412
- if (c) mountCleanups.push(c)
413
- } catch (err) {
414
- reportError({ component: componentName, phase: 'mount', error: err, timestamp: Date.now() })
415
- }
416
- }
417
- }
418
-
419
- const cleanup: Cleanup = () => {
420
- scope.stop()
421
- subtreeCleanup()
422
- if (hooks.unmount) for (const fn of hooks.unmount) fn()
423
- for (const fn of mountCleanups) fn()
424
- }
425
-
426
- return [cleanup, nextDom]
427
- }
428
-
429
- // ─── Public API ───────────────────────────────────────────────────────────────
430
-
431
- /**
432
- * Hydrate a server-rendered container with a Pyreon VNode tree.
433
- *
434
- * Reuses existing DOM elements for static structure, attaches event listeners
435
- * and reactive effects without re-rendering. Falls back to fresh mount for
436
- * dynamic content (reactive conditionals, For lists).
437
- *
438
- * @example
439
- * // Server:
440
- * const html = await renderToString(h(App, null))
441
- *
442
- * // Client:
443
- * const unmount = hydrateRoot(document.getElementById("app")!, h(App, null))
444
- */
445
- export function hydrateRoot(container: Element, vnode: VNodeChild): () => void {
446
- setupDelegation(container)
447
- const firstChild = firstReal(container.firstChild as ChildNode | null)
448
- const [cleanup] = hydrateChild(vnode, firstChild, container, null)
449
- return cleanup
450
- }
@@ -1,129 +0,0 @@
1
- /**
2
- * Hydration mismatch warnings + telemetry hook.
3
- *
4
- * Two complementary surfaces:
5
- *
6
- * 1. **Dev-mode console.warn** — enabled automatically when
7
- * `NODE_ENV !== "production"` (and silent otherwise, matching React /
8
- * Vue / Solid). Toggle manually with `enableHydrationWarnings()` /
9
- * `disableHydrationWarnings()` if you need verbose production debugging.
10
- *
11
- * 2. **Telemetry callback** — register a handler with
12
- * `onHydrationMismatch(handler)` to forward every mismatch into your
13
- * error-tracking pipeline (Sentry, Datadog, etc.). Fires on EVERY
14
- * mismatch, in development AND production, regardless of the warn
15
- * toggle. Returns an unregister function.
16
- *
17
- * The dev warn and the telemetry callback are independent: a production
18
- * deployment can install Sentry forwarding via `onHydrationMismatch`
19
- * WITHOUT enabling the noisy console output.
20
- *
21
- * @example — dev console
22
- * import { enableHydrationWarnings } from "@pyreon/runtime-dom"
23
- * enableHydrationWarnings()
24
- *
25
- * @example — production telemetry
26
- * import { onHydrationMismatch } from "@pyreon/runtime-dom"
27
- * import * as Sentry from "@sentry/browser"
28
- *
29
- * onHydrationMismatch(ctx => {
30
- * Sentry.captureMessage(`Hydration mismatch (${ctx.type})`, {
31
- * extra: { expected: ctx.expected, actual: ctx.actual, path: ctx.path },
32
- * level: 'warning',
33
- * })
34
- * })
35
- */
36
-
37
- // Dev-mode gate: see `pyreon/no-process-dev-gate` lint rule for why this
38
- // uses `import.meta.env.DEV` instead of `typeof process !== 'undefined'`.
39
- const __DEV__ = process.env.NODE_ENV !== 'production'
40
-
41
- let _enabled = __DEV__
42
-
43
- export function enableHydrationWarnings(): void {
44
- _enabled = true
45
- }
46
-
47
- export function disableHydrationWarnings(): void {
48
- _enabled = false
49
- }
50
-
51
- // ─── Telemetry callback ─────────────────────────────────────────────────────
52
-
53
- export type HydrationMismatchType = 'tag' | 'text' | 'missing'
54
-
55
- export interface HydrationMismatchContext {
56
- /** Kind of mismatch */
57
- type: HydrationMismatchType
58
- /** What the VNode expected */
59
- expected: unknown
60
- /** What the DOM had */
61
- actual: unknown
62
- /** Human-readable path in the tree, e.g. "root > div > span" */
63
- path: string
64
- /** Unix timestamp (ms) */
65
- timestamp: number
66
- }
67
-
68
- export type HydrationMismatchHandler = (ctx: HydrationMismatchContext) => void
69
-
70
- let _handlers: HydrationMismatchHandler[] = []
71
-
72
- /**
73
- * Register a hydration mismatch handler. Called on every mismatch in BOTH
74
- * development and production, independent of the dev-mode warn toggle.
75
- *
76
- * Mirrors `@pyreon/core`'s `registerErrorHandler` pattern — multiple
77
- * handlers can be registered; each is called in registration order;
78
- * handler errors are swallowed so they don't propagate into the
79
- * framework. Returns an unregister function.
80
- */
81
- export function onHydrationMismatch(handler: HydrationMismatchHandler): () => void {
82
- _handlers.push(handler)
83
- return () => {
84
- _handlers = _handlers.filter((h) => h !== handler)
85
- }
86
- }
87
-
88
- /**
89
- * Emit a hydration mismatch warning.
90
- * @param type - Kind of mismatch
91
- * @param expected - What the VNode expected
92
- * @param actual - What the DOM had
93
- * @param path - Human-readable path in the tree, e.g. "root > div > span"
94
- */
95
- export function warnHydrationMismatch(
96
- type: HydrationMismatchType,
97
- expected: unknown,
98
- actual: unknown,
99
- path: string,
100
- ): void {
101
- // Dev-mode console.warn — gated on _enabled (default __DEV__).
102
- if (_enabled) {
103
- // oxlint-disable-next-line no-console
104
- console.warn(
105
- `[Pyreon] Hydration mismatch (${type}): expected ${String(expected)}, got ${String(actual)} at ${path}`,
106
- )
107
- }
108
-
109
- // Telemetry callbacks — fire in BOTH dev and prod, independent of the
110
- // warn toggle. This is the production observability hook (Sentry,
111
- // Datadog, etc.) that pre-fix was missing entirely.
112
- if (_handlers.length > 0) {
113
- const ctx: HydrationMismatchContext = {
114
- type,
115
- expected,
116
- actual,
117
- path,
118
- timestamp: Date.now(),
119
- }
120
- for (const h of _handlers) {
121
- try {
122
- h(ctx)
123
- } catch {
124
- // handler errors must never propagate back into the hydration
125
- // pipeline — a broken Sentry SDK shouldn't crash the app.
126
- }
127
- }
128
- }
129
- }
package/src/index.ts DELETED
@@ -1,83 +0,0 @@
1
- // @pyreon/runtime-dom — surgical signal-to-DOM renderer (no virtual DOM)
2
-
3
- export { DELEGATED_EVENTS, delegatedPropName, setupDelegation } from './delegate'
4
- export type { DevtoolsComponentEntry, PyreonDevtools } from './devtools'
5
- export { hydrateRoot } from './hydrate'
6
- export type {
7
- HydrationMismatchContext,
8
- HydrationMismatchHandler,
9
- HydrationMismatchType,
10
- } from './hydration-debug'
11
- export {
12
- disableHydrationWarnings,
13
- enableHydrationWarnings,
14
- onHydrationMismatch,
15
- } from './hydration-debug'
16
- export type { KeepAliveProps } from './keep-alive'
17
- export { KeepAlive } from './keep-alive'
18
- export { mountChild } from './mount'
19
- export type { SanitizeFn } from './props'
20
- export {
21
- applyProp,
22
- applyProps,
23
- applyProps as _applyProps,
24
- sanitizeHtml,
25
- setSanitizer,
26
- } from './props'
27
- export {
28
- _bindDirect,
29
- _bindText,
30
- _mountSlot,
31
- _rsCollapse,
32
- _rsCollapseDyn,
33
- _rsCollapseDynH,
34
- _rsCollapseH,
35
- _tpl,
36
- createTemplate,
37
- } from './template'
38
- export type { TransitionProps } from './transition'
39
- export { Transition } from './transition'
40
- export type { TransitionGroupProps } from './transition-group'
41
- export { TransitionGroup } from './transition-group'
42
-
43
- import type { VNodeChild } from '@pyreon/core'
44
- import { setupDelegation } from './delegate'
45
- import { installDevTools } from './devtools'
46
- import { mountChild } from './mount'
47
-
48
- // Dev-mode gate: see `pyreon/no-process-dev-gate` lint rule for why this
49
- // uses `import.meta.env.DEV` instead of `typeof process !== 'undefined'`.
50
- const __DEV__ = process.env.NODE_ENV !== 'production'
51
-
52
- // Dev-time counter sink — see packages/internals/perf-harness for contract.
53
- const _countSink = globalThis as { __pyreon_count__?: (name: string, n?: number) => void }
54
-
55
- /**
56
- * Mount a VNode tree into a container element.
57
- * Clears the container first, then mounts the given child.
58
- * Returns an `unmount` function that removes everything and disposes effects.
59
- *
60
- * @example
61
- * const unmount = mount(h("div", null, "Hello Pyreon"), document.getElementById("app")!)
62
- */
63
- export function mount(root: VNodeChild, container: Element): () => void {
64
- if (__DEV__ && container == null) {
65
- throw new Error(
66
- '[pyreon] mount() called with a null/undefined container. Make sure the element exists in the DOM, e.g. document.getElementById("app")',
67
- )
68
- }
69
- if (__DEV__) {
70
- _countSink.__pyreon_count__?.('runtime.mount')
71
- installDevTools()
72
- }
73
- setupDelegation(container)
74
- container.innerHTML = ''
75
- const unmount = mountChild(root, container, null)
76
- return () => {
77
- if (__DEV__) _countSink.__pyreon_count__?.('runtime.unmount')
78
- unmount()
79
- }
80
- }
81
-
82
- /** Alias for `mount` */
83
- export const render = mount
@@ -1,3 +0,0 @@
1
- // Subpath entry for @pyreon/runtime-dom/keep-alive
2
- export type { KeepAliveProps } from './keep-alive'
3
- export { KeepAlive } from './keep-alive'