@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.
- package/package.json +5 -9
- package/src/delegate.ts +0 -98
- package/src/devtools.ts +0 -339
- package/src/env.d.ts +0 -6
- package/src/hydrate.ts +0 -450
- package/src/hydration-debug.ts +0 -129
- package/src/index.ts +0 -83
- package/src/keep-alive-entry.ts +0 -3
- package/src/keep-alive.ts +0 -83
- package/src/manifest.ts +0 -236
- package/src/mount.ts +0 -597
- package/src/nodes.ts +0 -896
- package/src/props.ts +0 -474
- package/src/template.ts +0 -523
- package/src/tests/callback-ref-unmount.browser.test.ts +0 -62
- package/src/tests/callback-ref-unmount.test.ts +0 -52
- package/src/tests/compiler-integration.test.tsx +0 -508
- package/src/tests/coverage-gaps.test.ts +0 -3183
- package/src/tests/coverage.test.ts +0 -1140
- package/src/tests/ctx-stack-growth-repro.test.tsx +0 -158
- package/src/tests/dev-gate-pattern.test.ts +0 -46
- package/src/tests/dev-gate-treeshake.test.ts +0 -256
- package/src/tests/error-boundary-stack-leak-repro.test.tsx +0 -133
- package/src/tests/fanout-repro.test.tsx +0 -219
- package/src/tests/hydration-integration.test.tsx +0 -540
- package/src/tests/keyed-array-in-for-batched-toggle.browser.test.ts +0 -140
- package/src/tests/lifecycle-integration.test.tsx +0 -342
- package/src/tests/lis-prepend.browser.test.ts +0 -99
- package/src/tests/manifest-snapshot.test.ts +0 -85
- package/src/tests/mount.test.ts +0 -3529
- package/src/tests/native-markers.test.ts +0 -19
- package/src/tests/props.test.ts +0 -581
- package/src/tests/reactive-props.test.ts +0 -270
- package/src/tests/real-world-integration.test.tsx +0 -714
- package/src/tests/rs-collapse-dyn-h.browser.test.ts +0 -303
- package/src/tests/rs-collapse-dyn.browser.test.ts +0 -316
- package/src/tests/rs-collapse-h.browser.test.ts +0 -152
- package/src/tests/rs-collapse-h.test.ts +0 -237
- package/src/tests/rs-collapse.browser.test.ts +0 -128
- package/src/tests/runtime-dom.browser.test.ts +0 -409
- package/src/tests/setup.ts +0 -3
- package/src/tests/show-context.test.ts +0 -270
- package/src/tests/show-of-for-batched-toggle.browser.test.ts +0 -122
- package/src/tests/ssr-xss-round-trip.browser.test.ts +0 -93
- package/src/tests/style-key-removal.browser.test.ts +0 -54
- package/src/tests/style-key-removal.test.ts +0 -88
- package/src/tests/template.test.ts +0 -383
- package/src/tests/transition-timeout-leak.test.ts +0 -126
- package/src/tests/transition.test.ts +0 -568
- package/src/tests/verified-correct-probes.test.ts +0 -56
- package/src/transition-entry.ts +0 -7
- package/src/transition-group.ts +0 -350
- 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
|
-
}
|
package/src/hydration-debug.ts
DELETED
|
@@ -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
|
package/src/keep-alive-entry.ts
DELETED