@bitstillery/mithril 3.0.0-AA

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 (47) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +185 -0
  3. package/api/mount-redraw.ts +173 -0
  4. package/api/router-server.ts +88 -0
  5. package/api/router.ts +670 -0
  6. package/browser.ts +10 -0
  7. package/htm.ts +7 -0
  8. package/hyperscript.ts +8 -0
  9. package/index.ts +134 -0
  10. package/jsx.d.ts +16 -0
  11. package/mount.ts +10 -0
  12. package/package.json +66 -0
  13. package/pathname/build.ts +40 -0
  14. package/pathname/compileTemplate.ts +49 -0
  15. package/pathname/parse.ts +19 -0
  16. package/querystring/build.ts +22 -0
  17. package/querystring/parse.ts +50 -0
  18. package/redraw.ts +10 -0
  19. package/render/cachedAttrsIsStaticMap.ts +10 -0
  20. package/render/delayedRemoval.ts +1 -0
  21. package/render/domFor.ts +23 -0
  22. package/render/emptyAttrs.ts +2 -0
  23. package/render/fragment.ts +11 -0
  24. package/render/hyperscript.ts +125 -0
  25. package/render/hyperscriptVnode.ts +21 -0
  26. package/render/render.ts +1307 -0
  27. package/render/renderToString.ts +494 -0
  28. package/render/ssrState.ts +366 -0
  29. package/render/trust.ts +6 -0
  30. package/render/vnode.ts +131 -0
  31. package/route.ts +11 -0
  32. package/server/logger.ts +223 -0
  33. package/server/session.ts +87 -0
  34. package/server/ssr.ts +205 -0
  35. package/server/ssrLogger.ts +13 -0
  36. package/server.ts +101 -0
  37. package/signal.ts +320 -0
  38. package/ssrContext.ts +87 -0
  39. package/state.ts +1026 -0
  40. package/store.ts +457 -0
  41. package/util/censor.ts +46 -0
  42. package/util/decodeURIComponentSafe.ts +34 -0
  43. package/util/hasOwn.ts +2 -0
  44. package/util/next_tick.ts +45 -0
  45. package/util/ssr.ts +385 -0
  46. package/util/uri.ts +126 -0
  47. package/version.ts +3 -0
@@ -0,0 +1,1307 @@
1
+ import {setCurrentComponent, clearCurrentComponent, clearComponentDependencies} from '../signal'
2
+ import {logHydrationError, resetHydrationErrorCount} from '../util/ssr'
3
+ import {logger} from '../server/logger'
4
+
5
+ import Vnode from './vnode'
6
+ import delayedRemoval from './delayedRemoval'
7
+ import domFor from './domFor'
8
+ import cachedAttrsIsStaticMap from './cachedAttrsIsStaticMap'
9
+
10
+ import type {Vnode as VnodeType, Children} from './vnode'
11
+
12
+ export default function renderFactory() {
13
+ const nameSpace: Record<string, string> = {
14
+ svg: 'http://www.w3.org/2000/svg',
15
+ math: 'http://www.w3.org/1998/Math/MathML',
16
+ }
17
+
18
+ let currentRedraw: (() => void) | undefined
19
+ let currentRender: any
20
+ // Track hydration mismatches for override mode
21
+ let hydrationMismatchCount = 0
22
+ const MAX_HYDRATION_MISMATCHES = 5
23
+
24
+ function getDocument(dom: Node): Document {
25
+ return dom.ownerDocument!
26
+ }
27
+
28
+ function getNameSpace(vnode: any): string | undefined {
29
+ return (vnode.attrs && vnode.attrs.xmlns) || nameSpace[vnode.tag]
30
+ }
31
+
32
+ // sanity check to discourage people from doing `vnode.state = ...`
33
+ function checkState(vnode: any, original: any) {
34
+ if (vnode.state !== original) throw new Error("'vnode.state' must not be modified.")
35
+ }
36
+
37
+ // Note: the hook is passed as the `this` argument to allow proxying the
38
+ // arguments without requiring a full array allocation to do so. It also
39
+ // takes advantage of the fact the current `vnode` is the first argument in
40
+ // all lifecycle methods.
41
+ function callHook(this: any, vnode: any, ...args: any[]) {
42
+ if (this == null || typeof this.apply !== 'function') {
43
+ const tagName = typeof vnode?.tag === 'function' ? vnode.tag?.name : vnode?.tag
44
+ throw new TypeError(
45
+ `callHook: expected a function with .apply (e.g. component.view), got ${tagName ?? vnode?.tag}. Check that the component has a view.`,
46
+ )
47
+ }
48
+ const original = vnode.state
49
+ try {
50
+ return this.apply(original, [vnode, ...args])
51
+ } finally {
52
+ checkState(vnode, original)
53
+ }
54
+ }
55
+
56
+ // IE11 (at least) throws an UnspecifiedError when accessing document.activeElement when
57
+ // inside an iframe. Catch and swallow this error, and heavy-handidly return null.
58
+ function activeElement(dom: Node): Element | null {
59
+ try {
60
+ return getDocument(dom).activeElement
61
+ } catch (_e) {
62
+ return null
63
+ }
64
+ }
65
+ // create
66
+ function createNodes(
67
+ parent: Element | DocumentFragment,
68
+ vnodes: (VnodeType | null)[],
69
+ start: number,
70
+ end: number,
71
+ hooks: Array<() => void>,
72
+ nextSibling: Node | null,
73
+ ns: string | undefined,
74
+ isHydrating: boolean = false,
75
+ matchedNodes: Set<Node> | null = null,
76
+ ) {
77
+ // Track which DOM nodes we've matched during hydration to avoid reusing the same node twice
78
+ // Create a new set if not provided and we're hydrating at the root level
79
+ const createdMatchedNodes = matchedNodes == null && isHydrating && nextSibling == null
80
+ if (createdMatchedNodes) {
81
+ matchedNodes = new Set<Node>()
82
+ }
83
+ for (let i = start; i < end; i++) {
84
+ const vnode = vnodes[i]
85
+ if (vnode != null) {
86
+ createNode(parent, vnode, hooks, ns, nextSibling, isHydrating, matchedNodes)
87
+ }
88
+ }
89
+ // After creating/matching all nodes, remove any unmatched nodes that remain
90
+ // Only do this at the root level to avoid removing nodes that are part of matched subtrees
91
+ if (createdMatchedNodes && matchedNodes && parent.firstChild && nextSibling == null) {
92
+ let node: Node | null = parent.firstChild
93
+ while (node) {
94
+ const next: Node | null = node.nextSibling
95
+ if (!matchedNodes.has(node)) {
96
+ try {
97
+ parent.removeChild(node)
98
+ } catch (e) {
99
+ const error = e as Error
100
+ logHydrationError(
101
+ 'removeChild (root level cleanup)',
102
+ null, // No vnode at root level
103
+ parent instanceof Element ? parent : null,
104
+ error,
105
+ {parent: parent instanceof Element ? parent : undefined, node, matchedNodes},
106
+ )
107
+ // Don't re-throw - we've already logged the error with all details
108
+ // Re-throwing causes the browser to log the DOMException stack trace
109
+ }
110
+ }
111
+ node = next
112
+ }
113
+ }
114
+ }
115
+ function createNode(
116
+ parent: Element | DocumentFragment,
117
+ vnode: any,
118
+ hooks: Array<() => void>,
119
+ ns: string | undefined,
120
+ nextSibling: Node | null,
121
+ isHydrating: boolean = false,
122
+ matchedNodes: Set<Node> | null = null,
123
+ ) {
124
+ const tag = vnode.tag
125
+ if (typeof tag === 'string') {
126
+ vnode.state = {}
127
+ if (vnode.attrs != null) initLifecycle(vnode.attrs, vnode, hooks, isHydrating)
128
+ switch (tag) {
129
+ case '#':
130
+ createText(parent, vnode, nextSibling, isHydrating, matchedNodes)
131
+ break
132
+ case '<':
133
+ createHTML(parent, vnode, ns, nextSibling)
134
+ break
135
+ case '[':
136
+ createFragment(parent, vnode, hooks, ns, nextSibling, isHydrating, matchedNodes)
137
+ break
138
+ default:
139
+ createElement(parent, vnode, hooks, ns, nextSibling, isHydrating, matchedNodes)
140
+ }
141
+ } else createComponent(parent, vnode, hooks, ns, nextSibling, isHydrating, matchedNodes)
142
+ }
143
+ function createText(
144
+ parent: Element | DocumentFragment,
145
+ vnode: any,
146
+ nextSibling: Node | null,
147
+ isHydrating: boolean = false,
148
+ matchedNodes: Set<Node> | null = null,
149
+ ) {
150
+ let textNode: Text
151
+ if (isHydrating && parent.firstChild && nextSibling == null && matchedNodes) {
152
+ // During hydration, try to reuse existing text node
153
+ // Normalize text for comparison (trim whitespace differences)
154
+ const expectedText = String(vnode.children || '').trim()
155
+ let candidate: Node | null = parent.firstChild
156
+ while (candidate) {
157
+ if (candidate.nodeType === 3 && !matchedNodes.has(candidate)) {
158
+ const candidateText = candidate as Text
159
+ const candidateValue = candidateText.nodeValue || ''
160
+ // Exact match preferred, but also accept trimmed match for whitespace differences
161
+ if (candidateValue === String(vnode.children) || (expectedText && candidateValue.trim() === expectedText)) {
162
+ textNode = candidateText
163
+ matchedNodes.add(textNode)
164
+ // Update text content if there's a minor difference (whitespace normalization)
165
+ if (candidateValue !== String(vnode.children)) {
166
+ textNode.nodeValue = String(vnode.children)
167
+ }
168
+ // Don't remove/reinsert - just reuse the existing node in place
169
+ break
170
+ }
171
+ }
172
+ candidate = candidate.nextSibling
173
+ }
174
+ // If no matching text node found, create new one
175
+ if (!textNode!) {
176
+ textNode = getDocument(parent as Element).createTextNode(vnode.children)
177
+ insertDOM(parent, textNode, nextSibling)
178
+ }
179
+ } else {
180
+ textNode = getDocument(parent as Element).createTextNode(vnode.children)
181
+ insertDOM(parent, textNode, nextSibling)
182
+ }
183
+ vnode.dom = textNode
184
+ }
185
+ const possibleParents: Record<string, string> = {
186
+ caption: 'table',
187
+ thead: 'table',
188
+ tbody: 'table',
189
+ tfoot: 'table',
190
+ tr: 'tbody',
191
+ th: 'tr',
192
+ td: 'tr',
193
+ colgroup: 'table',
194
+ col: 'colgroup',
195
+ }
196
+ function createHTML(parent: Element | DocumentFragment, vnode: any, ns: string | undefined, nextSibling: Node | null) {
197
+ const match = vnode.children.match(/^\s*?<(\w+)/im) || []
198
+ // not using the proper parent makes the child element(s) vanish.
199
+ // var div = document.createElement("div")
200
+ // div.innerHTML = "<td>i</td><td>j</td>"
201
+ // console.log(div.innerHTML)
202
+ // --> "ij", no <td> in sight.
203
+ let temp = getDocument(parent as Element).createElement(possibleParents[match[1]] || 'div')
204
+ if (ns === 'http://www.w3.org/2000/svg') {
205
+ temp.innerHTML = '<svg xmlns="http://www.w3.org/2000/svg">' + vnode.children + '</svg>'
206
+ temp = temp.firstChild as HTMLElement
207
+ } else {
208
+ temp.innerHTML = vnode.children
209
+ }
210
+ vnode.dom = temp.firstChild
211
+ vnode.domSize = temp.childNodes.length
212
+ const fragment = getDocument(parent as Element).createDocumentFragment()
213
+ let child: Node | null
214
+ while ((child = temp.firstChild) != null) {
215
+ fragment.appendChild(child)
216
+ }
217
+ insertDOM(parent, fragment, nextSibling)
218
+ }
219
+ function createFragment(
220
+ parent: Element | DocumentFragment,
221
+ vnode: any,
222
+ hooks: Array<() => void>,
223
+ ns: string | undefined,
224
+ nextSibling: Node | null,
225
+ isHydrating: boolean = false,
226
+ matchedNodes: Set<Node> | null = null,
227
+ ) {
228
+ const fragment = getDocument(parent as Element).createDocumentFragment()
229
+ if (vnode.children != null) {
230
+ const children = vnode.children
231
+ createNodes(fragment, children, 0, children.length, hooks, null, ns, isHydrating, matchedNodes)
232
+ }
233
+ vnode.dom = fragment.firstChild
234
+ vnode.domSize = fragment.childNodes.length
235
+ insertDOM(parent, fragment, nextSibling)
236
+ }
237
+ function createElement(
238
+ parent: Element | DocumentFragment,
239
+ vnode: any,
240
+ hooks: Array<() => void>,
241
+ ns: string | undefined,
242
+ nextSibling: Node | null,
243
+ isHydrating: boolean = false,
244
+ matchedNodes: Set<Node> | null = null,
245
+ ) {
246
+ const tag = vnode.tag
247
+ const attrs = vnode.attrs
248
+ const is = vnode.is
249
+
250
+ ns = getNameSpace(vnode) || ns
251
+
252
+ let element: Element
253
+ if (isHydrating && parent.firstChild && nextSibling == null && matchedNodes) {
254
+ // During hydration, try to reuse existing DOM node
255
+ // Only match if we're appending (nextSibling == null) to preserve order
256
+ // Find the first unmatched child element that matches the tag
257
+ // More lenient matching: skip text nodes and comments, allow tag name case differences
258
+ let candidate: Node | null = parent.firstChild
259
+ let fallbackCandidate: Element | null = null
260
+ while (candidate) {
261
+ // Skip text nodes, comments, and already matched nodes
262
+ if (candidate.nodeType === 1 && !matchedNodes.has(candidate)) {
263
+ const candidateEl = candidate as Element
264
+ // Case-insensitive tag matching (browsers normalize to uppercase for some tags)
265
+ // Use tagName if available, fallback to nodeName (for DOM mocks)
266
+ const candidateTag = (candidateEl as any).tagName || candidateEl.nodeName
267
+ if (candidateTag && candidateTag.toLowerCase() === tag.toLowerCase()) {
268
+ // Prefer exact match (is attribute matches if specified)
269
+ if (!is || candidateEl.getAttribute('is') === is) {
270
+ element = candidateEl
271
+ matchedNodes.add(element)
272
+ // Don't remove/reinsert - just reuse the existing node in place
273
+ break
274
+ }
275
+ // Keep track of first matching tag as fallback (even if is attribute doesn't match)
276
+ if (!fallbackCandidate) {
277
+ fallbackCandidate = candidateEl
278
+ }
279
+ }
280
+ }
281
+ candidate = candidate.nextSibling
282
+ }
283
+ // If no exact match found but we have a fallback, use it
284
+ // This handles cases where is attribute differs but tag matches
285
+ if (!element! && fallbackCandidate) {
286
+ element = fallbackCandidate
287
+ matchedNodes.add(element)
288
+ }
289
+ // If still no matching element found, create new one
290
+ if (!element!) {
291
+ element = ns
292
+ ? is
293
+ ? getDocument(parent as Element).createElementNS(ns, tag, {is: is} as any)
294
+ : getDocument(parent as Element).createElementNS(ns, tag)
295
+ : is
296
+ ? getDocument(parent as Element).createElement(tag, {is: is} as any)
297
+ : getDocument(parent as Element).createElement(tag)
298
+ insertDOM(parent, element, nextSibling)
299
+ }
300
+ } else {
301
+ // Normal creation path
302
+ element = ns
303
+ ? is
304
+ ? getDocument(parent as Element).createElementNS(ns, tag, {is: is} as any)
305
+ : getDocument(parent as Element).createElementNS(ns, tag)
306
+ : is
307
+ ? getDocument(parent as Element).createElement(tag, {is: is} as any)
308
+ : getDocument(parent as Element).createElement(tag)
309
+ insertDOM(parent, element, nextSibling)
310
+ }
311
+ vnode.dom = element
312
+
313
+ if (attrs != null) {
314
+ setAttrs(vnode, attrs, ns)
315
+ }
316
+
317
+ if (!maybeSetContentEditable(vnode)) {
318
+ if (vnode.children != null) {
319
+ const children = vnode.children
320
+ // During hydration, if we reused an element, it already has children
321
+ // Create a new matchedNodes set for this element's children to avoid duplicates
322
+ const childMatchedNodes = isHydrating && element.firstChild ? new Set<Node>() : null
323
+ createNodes(element, children, 0, children.length, hooks, null, ns, isHydrating, childMatchedNodes)
324
+ // After creating/matching children, remove any unmatched nodes that remain
325
+ // Only remove unmatched nodes if we actually matched some nodes (to avoid clearing everything)
326
+ if (isHydrating && childMatchedNodes && element.firstChild && childMatchedNodes.size > 0) {
327
+ let node: Node | null = element.firstChild
328
+ while (node) {
329
+ const next: Node | null = node.nextSibling
330
+ if (!childMatchedNodes.has(node)) {
331
+ // Verify node is still a child before attempting removal
332
+ if (element.contains && element.contains(node)) {
333
+ try {
334
+ element.removeChild(node)
335
+ hydrationMismatchCount++
336
+ } catch (e) {
337
+ const error = e as Error
338
+ // Check if node was already removed (not a child anymore)
339
+ if (!element.contains || !element.contains(node)) {
340
+ // Node already removed, skip silently
341
+ node = next
342
+ continue
343
+ }
344
+ hydrationMismatchCount++
345
+ logHydrationError('removeChild (element children cleanup)', vnode, element, error, {
346
+ parent: element,
347
+ node,
348
+ matchedNodes: childMatchedNodes,
349
+ })
350
+ // Don't re-throw - we've already logged the error with all details
351
+ // Re-throwing causes the browser to log the DOMException stack trace
352
+ }
353
+ }
354
+ // Node not in parent, already removed - skip silently
355
+ }
356
+ node = next
357
+ }
358
+ }
359
+ if (vnode.tag === 'select' && attrs != null) setLateSelectAttrs(vnode, attrs)
360
+ }
361
+ }
362
+ }
363
+ function initComponent(vnode: any, hooks: Array<() => void>, isHydrating: boolean = false) {
364
+ let sentinel: any
365
+ if (typeof vnode.tag.view === 'function') {
366
+ vnode.state = Object.create(vnode.tag)
367
+ sentinel = vnode.state.view
368
+ if (sentinel.$$reentrantLock$$ != null) return
369
+ sentinel.$$reentrantLock$$ = true
370
+ } else {
371
+ vnode.state = void 0
372
+ sentinel = vnode.tag
373
+ if (sentinel.$$reentrantLock$$ != null) return
374
+ sentinel.$$reentrantLock$$ = true
375
+ vnode.state =
376
+ vnode.tag.prototype != null && typeof vnode.tag.prototype.view === 'function'
377
+ ? new vnode.tag(vnode)
378
+ : vnode.tag(vnode)
379
+ }
380
+ initLifecycle(vnode.state, vnode, hooks, isHydrating)
381
+ if (vnode.attrs != null) initLifecycle(vnode.attrs, vnode, hooks, isHydrating)
382
+
383
+ // Track component for signal dependency tracking
384
+ // Store mapping from vnode.state to vnode.tag (component object) for redraw
385
+ if (vnode.state && vnode.tag && !isHydrating) {
386
+ ;(globalThis as any).__mithrilStateToComponent = (globalThis as any).__mithrilStateToComponent || new WeakMap()
387
+ ;(globalThis as any).__mithrilStateToComponent.set(vnode.state, vnode.tag)
388
+ }
389
+ // Always track component dependencies for signal tracking (even during hydration)
390
+ // This allows signals to know which components depend on them
391
+ // We only skip oninit during hydration, not signal tracking
392
+ // Only set currentComponent if vnode.state exists (it might be undefined for some component types)
393
+ if (vnode.state != null) {
394
+ setCurrentComponent(vnode.state)
395
+ }
396
+ try {
397
+ vnode.instance = Vnode.normalize(callHook.call(vnode.state.view, vnode))
398
+ } finally {
399
+ if (vnode.state != null) {
400
+ clearCurrentComponent()
401
+ }
402
+ }
403
+ if (vnode.instance === vnode) throw Error('A view cannot return the vnode it received as argument')
404
+ sentinel.$$reentrantLock$$ = null
405
+ }
406
+ function createComponent(
407
+ parent: Element | DocumentFragment,
408
+ vnode: any,
409
+ hooks: Array<() => void>,
410
+ ns: string | undefined,
411
+ nextSibling: Node | null,
412
+ isHydrating: boolean = false,
413
+ matchedNodes: Set<Node> | null = null,
414
+ ) {
415
+ initComponent(vnode, hooks, isHydrating)
416
+ if (vnode.instance != null) {
417
+ createNode(parent, vnode.instance, hooks, ns, nextSibling, isHydrating, matchedNodes)
418
+ vnode.dom = vnode.instance.dom
419
+ vnode.domSize = vnode.instance.domSize
420
+
421
+ // Store component's DOM element for fine-grained redraw (not during hydration)
422
+ if (vnode.state && vnode.dom && !isHydrating) {
423
+ ;(globalThis as any).__mithrilStateToDom = (globalThis as any).__mithrilStateToDom || new WeakMap()
424
+ ;(globalThis as any).__mithrilStateToDom.set(vnode.state, vnode.dom)
425
+ }
426
+ } else {
427
+ vnode.domSize = 0
428
+ }
429
+ }
430
+
431
+ // update
432
+ function updateNodes(
433
+ parent: Element | DocumentFragment,
434
+ old: (VnodeType | null)[] | null,
435
+ vnodes: (VnodeType | null)[] | null,
436
+ hooks: Array<() => void>,
437
+ nextSibling: Node | null,
438
+ ns: string | undefined,
439
+ isHydrating: boolean = false,
440
+ ) {
441
+ if (old === vnodes || (old == null && vnodes == null)) return
442
+ else if (old == null || old.length === 0)
443
+ createNodes(parent, vnodes!, 0, vnodes!.length, hooks, nextSibling, ns, isHydrating)
444
+ else if (vnodes == null || vnodes.length === 0) removeNodes(parent, old, 0, old.length)
445
+ else {
446
+ const isOldKeyed = old[0] != null && old[0]!.key != null
447
+ const isKeyed = vnodes[0] != null && vnodes[0]!.key != null
448
+ let start = 0,
449
+ oldStart = 0,
450
+ o: any,
451
+ v: any
452
+ if (isOldKeyed !== isKeyed) {
453
+ removeNodes(parent, old, 0, old.length)
454
+ createNodes(parent, vnodes, 0, vnodes.length, hooks, nextSibling, ns, isHydrating)
455
+ } else if (!isKeyed) {
456
+ // Don't index past the end of either list (causes deopts).
457
+ const commonLength = old.length < vnodes.length ? old.length : vnodes.length
458
+ // Rewind if necessary to the first non-null index on either side.
459
+ // We could alternatively either explicitly create or remove nodes when `start !== oldStart`
460
+ // but that would be optimizing for sparse lists which are more rare than dense ones.
461
+ while (oldStart < old.length && old[oldStart] == null) oldStart++
462
+ while (start < vnodes.length && vnodes[start] == null) start++
463
+ start = start < oldStart ? start : oldStart
464
+ for (; start < commonLength; start++) {
465
+ o = old[start]
466
+ v = vnodes[start]
467
+ if (o === v || (o == null && v == null)) continue
468
+ else if (o == null)
469
+ createNode(parent, v, hooks, ns, getNextSibling(old, start + 1, old.length, nextSibling), isHydrating)
470
+ else if (v == null) removeNode(parent, o)
471
+ else updateNode(parent, o, v, hooks, getNextSibling(old, start + 1, old.length, nextSibling), ns, isHydrating)
472
+ }
473
+ if (old.length > commonLength) removeNodes(parent, old, start, old.length)
474
+ if (vnodes.length > commonLength)
475
+ createNodes(parent, vnodes, start, vnodes.length, hooks, nextSibling, ns, isHydrating)
476
+ } else {
477
+ // keyed diff
478
+ let oldEnd = old.length - 1,
479
+ end = vnodes.length - 1,
480
+ oe: any,
481
+ ve: any,
482
+ topSibling: Node | null
483
+
484
+ // bottom-up
485
+ while (oldEnd >= oldStart && end >= start) {
486
+ oe = old[oldEnd]
487
+ ve = vnodes[end]
488
+ if (oe == null || ve == null || oe.key !== ve.key) break
489
+ if (oe !== ve) updateNode(parent, oe, ve, hooks, nextSibling, ns, isHydrating)
490
+ if (ve.dom != null) nextSibling = ve.dom
491
+ ;(oldEnd--, end--)
492
+ }
493
+ // top-down
494
+ while (oldEnd >= oldStart && end >= start) {
495
+ o = old[oldStart]
496
+ v = vnodes[start]
497
+ if (o == null || v == null || o.key !== v.key) break
498
+ ;(oldStart++, start++)
499
+ if (o !== v)
500
+ updateNode(parent, o, v, hooks, getNextSibling(old, oldStart, oldEnd + 1, nextSibling), ns, isHydrating)
501
+ }
502
+ // swaps and list reversals
503
+ while (oldEnd >= oldStart && end >= start) {
504
+ if (start === end) break
505
+ o = old[oldStart]
506
+ ve = vnodes[end]
507
+ oe = old[oldEnd]
508
+ v = vnodes[start]
509
+ if (o == null || ve == null || oe == null || v == null || o.key !== ve.key || oe.key !== v.key) break
510
+ topSibling = getNextSibling(old, oldStart, oldEnd, nextSibling)
511
+ moveDOM(parent, oe, topSibling)
512
+ if (oe !== v) updateNode(parent, oe, v, hooks, topSibling, ns, isHydrating)
513
+ if (++start <= --end) moveDOM(parent, o, nextSibling)
514
+ if (o !== ve) updateNode(parent, o, ve, hooks, nextSibling, ns, isHydrating)
515
+ if (ve.dom != null) nextSibling = ve.dom
516
+ oldStart++
517
+ oldEnd--
518
+ oe = old[oldEnd]
519
+ ve = vnodes[end]
520
+ o = old[oldStart]
521
+ v = vnodes[start]
522
+ }
523
+ // bottom up once again
524
+ while (oldEnd >= oldStart && end >= start) {
525
+ oe = old[oldEnd]
526
+ ve = vnodes[end]
527
+ if (oe == null || ve == null || oe.key !== ve.key) break
528
+ if (oe !== ve) updateNode(parent, oe, ve, hooks, nextSibling, ns, isHydrating)
529
+ if (ve.dom != null) nextSibling = ve.dom
530
+ ;(oldEnd--, end--)
531
+ oe = old[oldEnd]
532
+ ve = vnodes[end]
533
+ }
534
+ if (start > end) removeNodes(parent, old, oldStart, oldEnd + 1)
535
+ else if (oldStart > oldEnd) createNodes(parent, vnodes, start, end + 1, hooks, nextSibling, ns, isHydrating)
536
+ else {
537
+ // inspired by ivi https://github.com/ivijs/ivi/ by Boris Kaul
538
+ const originalNextSibling = nextSibling
539
+ let pos = 2147483647,
540
+ matched = 0
541
+ const oldIndices = Array.from({length: end - start + 1}, () => -1)
542
+ const map: Record<string, number> = Object.create(null)
543
+ for (let i = start; i <= end; i++) {
544
+ if (vnodes[i] != null) map[vnodes[i]!.key!] = i
545
+ }
546
+ for (let i = oldEnd; i >= oldStart; i--) {
547
+ oe = old[i]
548
+ if (oe == null) continue
549
+ const newIndex = map[oe.key!]
550
+ if (newIndex != null) {
551
+ pos = newIndex < pos ? newIndex : -1 // becomes -1 if nodes were re-ordered
552
+ oldIndices[newIndex - start] = i
553
+ ve = vnodes[newIndex]
554
+ old[i] = null
555
+ if (oe !== ve) updateNode(parent, oe, ve, hooks, nextSibling, ns, isHydrating)
556
+ if (ve != null && ve.dom != null) nextSibling = ve.dom
557
+ matched++
558
+ }
559
+ }
560
+ nextSibling = originalNextSibling
561
+ if (matched !== oldEnd - oldStart + 1) removeNodes(parent, old, oldStart, oldEnd + 1)
562
+ if (matched === 0) createNodes(parent, vnodes, start, end + 1, hooks, nextSibling, ns, isHydrating)
563
+ else {
564
+ if (pos === -1) {
565
+ // the indices of the indices of the items that are part of the
566
+ // longest increasing subsequence in the oldIndices list
567
+ const lisIndices = makeLisIndices(oldIndices)
568
+ let li = lisIndices.length - 1
569
+ for (let i = end; i >= start; i--) {
570
+ ve = vnodes[i]
571
+ if (ve == null) continue
572
+ if (oldIndices[i - start] === -1) createNode(parent, ve, hooks, ns, nextSibling, isHydrating)
573
+ else {
574
+ if (lisIndices[li] === i - start) li--
575
+ else moveDOM(parent, ve, nextSibling)
576
+ }
577
+ if (ve.dom != null) nextSibling = ve.dom
578
+ }
579
+ } else {
580
+ for (let i = end; i >= start; i--) {
581
+ ve = vnodes[i]
582
+ if (ve == null) continue
583
+ if (oldIndices[i - start] === -1) createNode(parent, ve, hooks, ns, nextSibling, isHydrating)
584
+ if (ve.dom != null) nextSibling = ve.dom
585
+ }
586
+ }
587
+ }
588
+ }
589
+ }
590
+ }
591
+ }
592
+ function updateNode(
593
+ parent: Element | DocumentFragment,
594
+ old: any,
595
+ vnode: any,
596
+ hooks: Array<() => void>,
597
+ nextSibling: Node | null,
598
+ ns: string | undefined,
599
+ isHydrating: boolean = false,
600
+ ) {
601
+ const oldTag = old.tag,
602
+ tag = vnode.tag
603
+ // Key change: different keys mean different logical nodes → remove old, create new.
604
+ // Critical when a component returns a single keyed child (e.g. router resolving to DocLoader).
605
+ if (oldTag === tag && old.is === vnode.is && old.key === vnode.key) {
606
+ vnode.state = old.state
607
+ vnode.events = old.events
608
+ if (shouldNotUpdate(vnode, old)) return
609
+ if (typeof oldTag === 'string') {
610
+ if (vnode.attrs != null) {
611
+ updateLifecycle(vnode.attrs, vnode, hooks)
612
+ }
613
+ switch (oldTag) {
614
+ case '#':
615
+ updateText(old, vnode)
616
+ break
617
+ case '<':
618
+ updateHTML(parent, old, vnode, ns, nextSibling)
619
+ break
620
+ case '[':
621
+ updateFragment(parent, old, vnode, hooks, nextSibling, ns, isHydrating)
622
+ break
623
+ default:
624
+ updateElement(old, vnode, hooks, ns, isHydrating)
625
+ }
626
+ } else updateComponent(parent, old, vnode, hooks, nextSibling, ns, isHydrating)
627
+ } else {
628
+ removeNode(parent, old, vnode) // Pass new vnode for context
629
+ createNode(parent, vnode, hooks, ns, nextSibling, isHydrating)
630
+ }
631
+ }
632
+ function updateText(old: any, vnode: any) {
633
+ if (old.children.toString() !== vnode.children.toString()) {
634
+ old.dom.nodeValue = vnode.children
635
+ }
636
+ vnode.dom = old.dom
637
+ }
638
+ function updateHTML(
639
+ parent: Element | DocumentFragment,
640
+ old: any,
641
+ vnode: any,
642
+ ns: string | undefined,
643
+ nextSibling: Node | null,
644
+ ) {
645
+ if (old.children !== vnode.children) {
646
+ removeDOM(parent, old)
647
+ createHTML(parent, vnode, ns, nextSibling)
648
+ } else {
649
+ vnode.dom = old.dom
650
+ vnode.domSize = old.domSize
651
+ }
652
+ }
653
+ function updateFragment(
654
+ parent: Element | DocumentFragment,
655
+ old: any,
656
+ vnode: any,
657
+ hooks: Array<() => void>,
658
+ nextSibling: Node | null,
659
+ ns: string | undefined,
660
+ isHydrating: boolean = false,
661
+ ) {
662
+ updateNodes(parent, old.children, vnode.children, hooks, nextSibling, ns, isHydrating)
663
+ let domSize = 0
664
+ const children = vnode.children
665
+ vnode.dom = null
666
+ if (children != null) {
667
+ for (let i = 0; i < children.length; i++) {
668
+ const child = children[i]
669
+ if (child != null && child.dom != null) {
670
+ if (vnode.dom == null) vnode.dom = child.dom
671
+ domSize += child.domSize || 1
672
+ }
673
+ }
674
+ }
675
+ vnode.domSize = domSize
676
+ }
677
+ function updateElement(old: any, vnode: any, hooks: Array<() => void>, ns: string | undefined, isHydrating: boolean = false) {
678
+ const element = (vnode.dom = old.dom)
679
+ ns = getNameSpace(vnode) || ns
680
+
681
+ if (old.attrs != vnode.attrs || (vnode.attrs != null && !cachedAttrsIsStaticMap.get(vnode.attrs))) {
682
+ updateAttrs(vnode, old.attrs, vnode.attrs, ns)
683
+ }
684
+ if (!maybeSetContentEditable(vnode)) {
685
+ updateNodes(element, old.children, vnode.children, hooks, null, ns, isHydrating)
686
+ }
687
+ }
688
+ function updateComponent(
689
+ parent: Element | DocumentFragment,
690
+ old: any,
691
+ vnode: any,
692
+ hooks: Array<() => void>,
693
+ nextSibling: Node | null,
694
+ ns: string | undefined,
695
+ isHydrating: boolean = false,
696
+ ) {
697
+ // Track component for signal dependency tracking
698
+ // Store mapping from vnode.state to vnode.tag (component object) for redraw
699
+ if (vnode.state && vnode.tag && !isHydrating) {
700
+ ;(globalThis as any).__mithrilStateToComponent = (globalThis as any).__mithrilStateToComponent || new WeakMap()
701
+ ;(globalThis as any).__mithrilStateToComponent.set(vnode.state, vnode.tag)
702
+ }
703
+ // Always track component dependencies for signal tracking (even during hydration)
704
+ // This allows signals to know which components depend on them
705
+ // We only skip oninit during hydration, not signal tracking
706
+ // Only set currentComponent if vnode.state exists (it might be undefined for some component types)
707
+ if (vnode.state != null) {
708
+ setCurrentComponent(vnode.state)
709
+ }
710
+ try {
711
+ vnode.instance = Vnode.normalize(callHook.call(vnode.state.view, vnode))
712
+ } finally {
713
+ if (vnode.state != null) {
714
+ clearCurrentComponent()
715
+ }
716
+ }
717
+ if (vnode.instance === vnode) throw Error('A view cannot return the vnode it received as argument')
718
+ updateLifecycle(vnode.state, vnode, hooks)
719
+ if (vnode.attrs != null) updateLifecycle(vnode.attrs, vnode, hooks)
720
+ if (vnode.instance != null) {
721
+ if (old.instance == null) createNode(parent, vnode.instance, hooks, ns, nextSibling, isHydrating)
722
+ else updateNode(parent, old.instance, vnode.instance, hooks, nextSibling, ns, isHydrating)
723
+ vnode.dom = vnode.instance.dom
724
+ vnode.domSize = vnode.instance.domSize
725
+
726
+ // Store component's DOM element for fine-grained redraw (not during hydration)
727
+ if (vnode.state && vnode.dom && !isHydrating) {
728
+ ;(globalThis as any).__mithrilStateToDom = (globalThis as any).__mithrilStateToDom || new WeakMap()
729
+ ;(globalThis as any).__mithrilStateToDom.set(vnode.state, vnode.dom)
730
+ }
731
+ } else {
732
+ if (old.instance != null) removeNode(parent, old.instance)
733
+ vnode.domSize = 0
734
+ }
735
+ }
736
+ // Lifted from ivi https://github.com/ivijs/ivi/
737
+ // takes a list of unique numbers (-1 is special and can
738
+ // occur multiple times) and returns an array with the indices
739
+ // of the items that are part of the longest increasing
740
+ // subsequence
741
+ const lisTemp: number[] = []
742
+ function makeLisIndices(a: number[]): number[] {
743
+ const result = [0]
744
+ let u = 0,
745
+ v = 0
746
+ const il = (lisTemp.length = a.length)
747
+ for (let i = 0; i < il; i++) lisTemp[i] = a[i]
748
+ for (let i = 0; i < il; ++i) {
749
+ if (a[i] === -1) continue
750
+ const j = result[result.length - 1]
751
+ if (a[j] < a[i]) {
752
+ lisTemp[i] = j
753
+ result.push(i)
754
+ continue
755
+ }
756
+ u = 0
757
+ v = result.length - 1
758
+ while (u < v) {
759
+ // Fast integer average without overflow.
760
+
761
+ const c = (u >>> 1) + (v >>> 1) + (u & v & 1)
762
+ if (a[result[c]] < a[i]) {
763
+ u = c + 1
764
+ } else {
765
+ v = c
766
+ }
767
+ }
768
+ if (a[i] < a[result[u]]) {
769
+ if (u > 0) lisTemp[i] = result[u - 1]
770
+ result[u] = i
771
+ }
772
+ }
773
+ u = result.length
774
+ v = result[u - 1]
775
+ while (u-- > 0) {
776
+ result[u] = v
777
+ v = lisTemp[v]
778
+ }
779
+ lisTemp.length = 0
780
+ return result
781
+ }
782
+
783
+ function getNextSibling(vnodes: (VnodeType | null)[], i: number, end: number, nextSibling: Node | null): Node | null {
784
+ for (; i < end; i++) {
785
+ if (vnodes[i] != null && vnodes[i]!.dom != null) return vnodes[i]!.dom!
786
+ }
787
+ return nextSibling
788
+ }
789
+
790
+ // This handles fragments with zombie children (removed from vdom, but persisted in DOM through onbeforeremove)
791
+ function moveDOM(parent: Element | DocumentFragment, vnode: any, nextSibling: Node | null) {
792
+ if (vnode.dom != null) {
793
+ let target: Node
794
+ if (vnode.domSize == null || vnode.domSize === 1) {
795
+ // don't allocate for the common case
796
+ target = vnode.dom
797
+ } else {
798
+ target = getDocument(parent as Element).createDocumentFragment()
799
+ for (const dom of domFor(vnode)) target.appendChild(dom)
800
+ }
801
+ insertDOM(parent, target, nextSibling)
802
+ }
803
+ }
804
+
805
+ function insertDOM(parent: Element | DocumentFragment, dom: Node, nextSibling: Node | null) {
806
+ if (nextSibling != null) parent.insertBefore(dom, nextSibling)
807
+ else parent.appendChild(dom)
808
+ }
809
+
810
+ function maybeSetContentEditable(vnode: any): boolean {
811
+ if (
812
+ vnode.attrs == null ||
813
+ (vnode.attrs.contenteditable == null && // attribute
814
+ vnode.attrs.contentEditable == null) // property
815
+ )
816
+ return false
817
+ const children = vnode.children
818
+ if (children != null && children.length === 1 && children[0].tag === '<') {
819
+ const content = children[0].children
820
+ if (vnode.dom.innerHTML !== content) vnode.dom.innerHTML = content
821
+ } else if (children != null && children.length !== 0) throw new Error('Child node of a contenteditable must be trusted.')
822
+ return true
823
+ }
824
+
825
+ // remove
826
+ function removeNodes(parent: Element | DocumentFragment, vnodes: (VnodeType | null)[], start: number, end: number) {
827
+ for (let i = start; i < end; i++) {
828
+ const vnode = vnodes[i]
829
+ if (vnode != null) removeNode(parent, vnode)
830
+ }
831
+ }
832
+ function tryBlockRemove(parent: Element | DocumentFragment, vnode: any, source: any, counter: {v: number}) {
833
+ const original = vnode.state
834
+ const result = callHook.call(source.onbeforeremove, vnode)
835
+ if (result == null) return
836
+
837
+ const generation = currentRender
838
+ for (const dom of domFor(vnode)) delayedRemoval.set(dom, generation)
839
+ counter.v++
840
+
841
+ Promise.resolve(result).finally(function () {
842
+ checkState(vnode, original)
843
+ tryResumeRemove(parent, vnode, counter)
844
+ })
845
+ }
846
+ function tryResumeRemove(parent: Element | DocumentFragment, vnode: any, counter: {v: number}, newVnode?: any) {
847
+ if (--counter.v === 0) {
848
+ onremove(vnode)
849
+ removeDOM(parent, vnode, newVnode)
850
+ }
851
+ }
852
+ function removeNode(parent: Element | DocumentFragment, vnode: any, newVnode?: any) {
853
+ const counter = {v: 1}
854
+ if (typeof vnode.tag !== 'string' && typeof vnode.state.onbeforeremove === 'function')
855
+ tryBlockRemove(parent, vnode, vnode.state, counter)
856
+ if (vnode.attrs && typeof vnode.attrs.onbeforeremove === 'function') tryBlockRemove(parent, vnode, vnode.attrs, counter)
857
+ tryResumeRemove(parent, vnode, counter, newVnode)
858
+ }
859
+ function removeDOM(parent: Element | DocumentFragment, vnode: any, newVnode?: any) {
860
+ if (vnode.dom == null) return
861
+ if (vnode.domSize == null || vnode.domSize === 1) {
862
+ // Check if node is still a child before attempting removal
863
+ const node = vnode.dom
864
+ if (parent.contains && parent.contains(node)) {
865
+ // Verify node is actually a direct or indirect child
866
+ try {
867
+ parent.removeChild(node)
868
+ } catch (e) {
869
+ const error = e as Error
870
+ // Check if node was already removed (not a child anymore)
871
+ if (!parent.contains || !parent.contains(node)) {
872
+ // Node already removed, skip silently
873
+ return
874
+ }
875
+ logHydrationError('removeDOM (single node)', vnode, parent instanceof Element ? parent : null, error, {
876
+ parent: parent instanceof Element ? parent : undefined,
877
+ node: vnode.dom,
878
+ oldVnode: vnode,
879
+ newVnode: newVnode,
880
+ })
881
+ // Don't re-throw - we've already logged the error with all details
882
+ // Re-throwing causes the browser to log the DOMException stack trace
883
+ }
884
+ }
885
+ // Node not in parent, already removed - skip silently
886
+ } else {
887
+ for (const dom of domFor(vnode)) {
888
+ // Check if node is still a child before attempting removal
889
+ if (parent.contains && parent.contains(dom)) {
890
+ try {
891
+ parent.removeChild(dom)
892
+ } catch (e) {
893
+ const error = e as Error
894
+ // Check if node was already removed (not a child anymore)
895
+ if (!parent.contains || !parent.contains(dom)) {
896
+ // Node already removed, skip silently
897
+ continue
898
+ }
899
+ logHydrationError('removeDOM (multiple nodes)', vnode, parent instanceof Element ? parent : null, error, {
900
+ parent: parent instanceof Element ? parent : undefined,
901
+ node: dom,
902
+ oldVnode: vnode,
903
+ newVnode: newVnode,
904
+ })
905
+ // Don't re-throw - we've already logged the error with all details
906
+ // Re-throwing causes the browser to log the DOMException stack trace
907
+ }
908
+ }
909
+ // Node not in parent, already removed - skip silently
910
+ }
911
+ }
912
+ }
913
+
914
+ function onremove(vnode: any) {
915
+ // Clean up signal dependencies when component is removed
916
+ if (typeof vnode.tag !== 'string' && vnode.state != null) {
917
+ clearComponentDependencies(vnode.state)
918
+ }
919
+ if (typeof vnode.tag !== 'string' && typeof vnode.state.onremove === 'function')
920
+ callHook.call(vnode.state.onremove, vnode)
921
+ if (vnode.attrs && typeof vnode.attrs.onremove === 'function') callHook.call(vnode.attrs.onremove, vnode)
922
+ if (typeof vnode.tag !== 'string') {
923
+ if (vnode.instance != null) onremove(vnode.instance)
924
+ } else {
925
+ if (vnode.events != null) vnode.events._ = null
926
+ const children = vnode.children
927
+ if (Array.isArray(children)) {
928
+ for (let i = 0; i < children.length; i++) {
929
+ const child = children[i]
930
+ if (child != null) onremove(child)
931
+ }
932
+ }
933
+ }
934
+ }
935
+
936
+ // attrs
937
+ function setAttrs(vnode: any, attrs: Record<string, any>, ns: string | undefined) {
938
+ for (const key in attrs) {
939
+ setAttr(vnode, key, null, attrs[key], ns)
940
+ }
941
+ }
942
+ function setAttr(vnode: any, key: string, old: any, value: any, ns: string | undefined) {
943
+ if (
944
+ key === 'key' ||
945
+ value == null ||
946
+ isLifecycleMethod(key) ||
947
+ (old === value && !isFormAttribute(vnode, key) && typeof value !== 'object')
948
+ )
949
+ return
950
+ if (key[0] === 'o' && key[1] === 'n') return updateEvent(vnode, key, value)
951
+ if (key.slice(0, 6) === 'xlink:') vnode.dom.setAttributeNS('http://www.w3.org/1999/xlink', key.slice(6), value)
952
+ else if (key === 'style') updateStyle(vnode.dom, old, value)
953
+ else if (hasPropertyKey(vnode, key, ns)) {
954
+ if (key === 'value') {
955
+ // Only do the coercion if we're actually going to check the value.
956
+ // setting input[value] to same value by typing on focused element moves cursor to end in Chrome
957
+ // setting input[type=file][value] to same value causes an error to be generated if it's non-empty
958
+ // minlength/maxlength validation isn't performed on script-set values(#2256)
959
+ if ((vnode.tag === 'input' || vnode.tag === 'textarea') && vnode.dom.value === '' + value) return
960
+ // setting select[value] to same value while having select open blinks select dropdown in Chrome
961
+ if (vnode.tag === 'select' && old !== null && vnode.dom.value === '' + value) return
962
+ // setting option[value] to same value while having select open blinks select dropdown in Chrome
963
+ if (vnode.tag === 'option' && old !== null && vnode.dom.value === '' + value) return
964
+ // setting input[type=file][value] to different value is an error if it's non-empty
965
+ // Not ideal, but it at least works around the most common source of uncaught exceptions for now.
966
+ if (vnode.tag === 'input' && vnode.attrs.type === 'file' && '' + value !== '') {
967
+ console.error('`value` is read-only on file inputs!')
968
+ return
969
+ }
970
+ }
971
+ // If you assign an input type that is not supported by IE 11 with an assignment expression, an error will occur.
972
+ if (vnode.tag === 'input' && key === 'type') vnode.dom.setAttribute(key, value)
973
+ else vnode.dom[key] = value
974
+ } else {
975
+ if (typeof value === 'boolean') {
976
+ if (value) vnode.dom.setAttribute(key, '')
977
+ else vnode.dom.removeAttribute(key)
978
+ } else vnode.dom.setAttribute(key === 'className' ? 'class' : key, value)
979
+ }
980
+ }
981
+ function removeAttr(vnode: any, key: string, old: any, ns: string | undefined) {
982
+ if (key === 'key' || old == null || isLifecycleMethod(key)) return
983
+ if (key[0] === 'o' && key[1] === 'n') updateEvent(vnode, key, undefined)
984
+ else if (key === 'style') updateStyle(vnode.dom, old, null)
985
+ else if (
986
+ hasPropertyKey(vnode, key, ns) &&
987
+ key !== 'className' &&
988
+ key !== 'title' && // creates "null" as title
989
+ !(
990
+ key === 'value' &&
991
+ (vnode.tag === 'option' ||
992
+ (vnode.tag === 'select' && vnode.dom.selectedIndex === -1 && vnode.dom === activeElement(vnode.dom)))
993
+ ) &&
994
+ !(vnode.tag === 'input' && key === 'type')
995
+ ) {
996
+ vnode.dom[key] = null
997
+ } else {
998
+ const nsLastIndex = key.indexOf(':')
999
+ if (nsLastIndex !== -1) key = key.slice(nsLastIndex + 1)
1000
+ if (old !== false) vnode.dom.removeAttribute(key === 'className' ? 'class' : key)
1001
+ }
1002
+ }
1003
+ function setLateSelectAttrs(vnode: any, attrs: Record<string, any>) {
1004
+ if ('value' in attrs) {
1005
+ if (attrs.value === null) {
1006
+ if (vnode.dom.selectedIndex !== -1) vnode.dom.value = null
1007
+ } else {
1008
+ const normalized = '' + attrs.value
1009
+ if (vnode.dom.value !== normalized || vnode.dom.selectedIndex === -1) {
1010
+ vnode.dom.value = normalized
1011
+ }
1012
+ }
1013
+ }
1014
+ if ('selectedIndex' in attrs) setAttr(vnode, 'selectedIndex', null, attrs.selectedIndex, undefined)
1015
+ }
1016
+ function updateAttrs(vnode: any, old: Record<string, any> | null, attrs: Record<string, any> | null, ns: string | undefined) {
1017
+ // Some attributes may NOT be case-sensitive (e.g. data-***),
1018
+ // so removal should be done first to prevent accidental removal for newly setting values.
1019
+ let val: any
1020
+ if (old != null) {
1021
+ if (old === attrs && !cachedAttrsIsStaticMap.has(attrs!)) {
1022
+ // Diagnostic: log element tag and attrs to identify the source
1023
+ console.warn(
1024
+ "Don't reuse attrs object, use new object for every redraw, this will throw in next major",
1025
+ 'element:',
1026
+ vnode.tag,
1027
+ 'attrs keys:',
1028
+ attrs != null ? Object.keys(attrs) : [],
1029
+ 'dom:',
1030
+ vnode.dom,
1031
+ )
1032
+ }
1033
+ for (const key in old) {
1034
+ if ((val = old[key]) != null && (attrs == null || attrs[key] == null)) {
1035
+ removeAttr(vnode, key, val, ns)
1036
+ }
1037
+ }
1038
+ }
1039
+ if (attrs != null) {
1040
+ for (const key in attrs) {
1041
+ setAttr(vnode, key, old && old[key], attrs[key], ns)
1042
+ }
1043
+ }
1044
+ }
1045
+ function isFormAttribute(vnode: any, attr: string): boolean {
1046
+ return (
1047
+ attr === 'value' ||
1048
+ attr === 'checked' ||
1049
+ attr === 'selectedIndex' ||
1050
+ (attr === 'selected' &&
1051
+ (vnode.dom === activeElement(vnode.dom) ||
1052
+ (vnode.tag === 'option' && vnode.dom.parentNode === activeElement(vnode.dom))))
1053
+ )
1054
+ }
1055
+ function isLifecycleMethod(attr: string): boolean {
1056
+ return (
1057
+ attr === 'oninit' ||
1058
+ attr === 'oncreate' ||
1059
+ attr === 'onupdate' ||
1060
+ attr === 'onremove' ||
1061
+ attr === 'onbeforeremove' ||
1062
+ attr === 'onbeforeupdate'
1063
+ )
1064
+ }
1065
+ function hasPropertyKey(vnode: any, key: string, ns: string | undefined): boolean {
1066
+ // Filter out namespaced keys
1067
+ return (
1068
+ ns === undefined &&
1069
+ // If it's a custom element, just keep it.
1070
+ (vnode.tag.indexOf('-') > -1 ||
1071
+ vnode.is ||
1072
+ // If it's a normal element, let's try to avoid a few browser bugs.
1073
+ (key !== 'href' && key !== 'list' && key !== 'form' && key !== 'width' && key !== 'height')) && // && key !== "type"
1074
+ // Defer the property check until *after* we check everything.
1075
+ key in vnode.dom
1076
+ )
1077
+ }
1078
+
1079
+ // style
1080
+ function updateStyle(element: HTMLElement, old: any, style: any) {
1081
+ if (old === style) {
1082
+ // Styles are equivalent, do nothing.
1083
+ } else if (style == null) {
1084
+ // New style is missing, just clear it.
1085
+ element.style.cssText = ''
1086
+ } else if (typeof style !== 'object') {
1087
+ // New style is a string, let engine deal with patching.
1088
+ element.style.cssText = style
1089
+ } else if (old == null || typeof old !== 'object') {
1090
+ // `old` is missing or a string, `style` is an object.
1091
+ element.style.cssText = ''
1092
+ // Add new style properties
1093
+ for (const key in style) {
1094
+ const value = style[key]
1095
+ if (value != null) {
1096
+ if (key.includes('-')) element.style.setProperty(key, String(value))
1097
+ else (element.style as any)[key] = String(value)
1098
+ }
1099
+ }
1100
+ } else {
1101
+ // Both old & new are (different) objects.
1102
+ // Remove style properties that no longer exist
1103
+ // Style properties may have two cases(dash-case and camelCase),
1104
+ // so removal should be done first to prevent accidental removal for newly setting values.
1105
+ for (const key in old) {
1106
+ if (old[key] != null && style[key] == null) {
1107
+ if (key.includes('-')) element.style.removeProperty(key)
1108
+ else (element.style as any)[key] = ''
1109
+ }
1110
+ }
1111
+ // Update style properties that have changed
1112
+ for (const key in style) {
1113
+ let value = style[key]
1114
+ if (value != null && (value = String(value)) !== String(old[key])) {
1115
+ if (key.includes('-')) element.style.setProperty(key, value)
1116
+ else (element.style as any)[key] = value
1117
+ }
1118
+ }
1119
+ }
1120
+ }
1121
+
1122
+ // Here's an explanation of how this works:
1123
+ // 1. The event names are always (by design) prefixed by `on`.
1124
+ // 2. The EventListener interface accepts either a function or an object
1125
+ // with a `handleEvent` method.
1126
+ // 3. The object does not inherit from `Object.prototype`, to avoid
1127
+ // any potential interference with that (e.g. setters).
1128
+ // 4. The event name is remapped to the handler before calling it.
1129
+ // 5. In function-based event handlers, `ev.target === this`. We replicate
1130
+ // that below.
1131
+ // 6. In function-based event handlers, `return false` prevents the default
1132
+ // action and stops event propagation. We replicate that below.
1133
+ function EventDict(this: any) {
1134
+ // Save this, so the current redraw is correctly tracked.
1135
+ this._ = currentRedraw
1136
+ }
1137
+ EventDict.prototype = Object.create(null)
1138
+ EventDict.prototype.handleEvent = function (ev: any) {
1139
+ const handler = this['on' + ev.type]
1140
+ let result: any
1141
+ if (typeof handler === 'function') result = handler.call(ev.currentTarget, ev)
1142
+ else if (typeof handler.handleEvent === 'function') handler.handleEvent(ev)
1143
+ const self = this
1144
+ if (self._ != null) {
1145
+ if (ev.redraw !== false) (0, self._)()
1146
+ if (result != null && typeof result.then === 'function') {
1147
+ Promise.resolve(result).then(function () {
1148
+ if (self._ != null && ev.redraw !== false) (0, self._)()
1149
+ })
1150
+ }
1151
+ }
1152
+ if (result === false) {
1153
+ ev.preventDefault()
1154
+ ev.stopPropagation()
1155
+ }
1156
+ }
1157
+
1158
+ // event
1159
+ function updateEvent(vnode: any, key: string, value: any) {
1160
+ if (vnode.events != null) {
1161
+ vnode.events._ = currentRedraw
1162
+ if (vnode.events[key] === value) return
1163
+ if (value != null && (typeof value === 'function' || typeof value === 'object')) {
1164
+ if (vnode.events[key] == null) vnode.dom.addEventListener(key.slice(2), vnode.events, false)
1165
+ vnode.events[key] = value
1166
+ } else {
1167
+ if (vnode.events[key] != null) vnode.dom.removeEventListener(key.slice(2), vnode.events, false)
1168
+ vnode.events[key] = undefined
1169
+ }
1170
+ } else if (value != null && (typeof value === 'function' || typeof value === 'object')) {
1171
+ vnode.events = new (EventDict as any)()
1172
+ vnode.dom.addEventListener(key.slice(2), vnode.events, false)
1173
+ vnode.events[key] = value
1174
+ }
1175
+ }
1176
+
1177
+ // lifecycle
1178
+ function initLifecycle(source: any, vnode: any, hooks: Array<() => void>, isHydrating: boolean = false) {
1179
+ // Always call oninit, but pass context so components can make intelligent decisions
1180
+ // Components can check context.isSSR or context.isHydrating to conditionally load data
1181
+ if (typeof source.oninit === 'function') {
1182
+ const context = {
1183
+ isSSR: false,
1184
+ isHydrating: isHydrating,
1185
+ }
1186
+ const result = callHook.call(source.oninit, vnode, context)
1187
+ // Auto-redraw when async oninit completes (client-side only)
1188
+ if (result != null && typeof result.then === 'function' && currentRedraw != null) {
1189
+ Promise.resolve(result).then(function () {
1190
+ if (currentRedraw != null) {
1191
+ // @ts-expect-error - Comma operator intentionally used to call without 'this' binding
1192
+ ;(0, currentRedraw)()
1193
+ }
1194
+ })
1195
+ }
1196
+ }
1197
+ if (typeof source.oncreate === 'function') hooks.push(callHook.bind(source.oncreate, vnode))
1198
+ }
1199
+ function updateLifecycle(source: any, vnode: any, hooks: Array<() => void>) {
1200
+ if (typeof source.onupdate === 'function') hooks.push(callHook.bind(source.onupdate, vnode))
1201
+ }
1202
+ function shouldNotUpdate(vnode: any, old: any): boolean {
1203
+ updateCheck: {
1204
+ if (vnode.attrs != null && typeof vnode.attrs.onbeforeupdate === 'function') {
1205
+ const force = callHook.call(vnode.attrs.onbeforeupdate, vnode, old)
1206
+ if (force !== undefined && !force) break updateCheck
1207
+ }
1208
+ if (typeof vnode.tag !== 'string' && typeof vnode.state.onbeforeupdate === 'function') {
1209
+ const force = callHook.call(vnode.state.onbeforeupdate, vnode, old)
1210
+ if (force !== undefined && !force) break updateCheck
1211
+ }
1212
+ return false
1213
+ }
1214
+ vnode.dom = old.dom
1215
+ vnode.domSize = old.domSize
1216
+ vnode.instance = old.instance
1217
+ // One would think having the actual latest attributes would be ideal,
1218
+ // but it doesn't let us properly diff based on our current internal
1219
+ // representation. We have to save not only the old DOM info, but also
1220
+ // the attributes used to create it, as we diff *that*, not against the
1221
+ // DOM directly (with a few exceptions in `setAttr`). And, of course, we
1222
+ // need to save the children and text as they are conceptually not
1223
+ // unlike special "attributes" internally.
1224
+ vnode.attrs = old.attrs
1225
+ vnode.children = old.children
1226
+ vnode.text = old.text
1227
+ return true
1228
+ }
1229
+
1230
+ let currentDOM: Element | null = null
1231
+
1232
+ return function (dom: Element, vnodes: Children | VnodeType | null, redraw?: () => void) {
1233
+ if (!dom) throw new TypeError('DOM element being rendered to does not exist.')
1234
+ if (currentDOM != null && dom.contains(currentDOM)) {
1235
+ throw new TypeError('Node is currently being rendered to and thus is locked.')
1236
+ }
1237
+ const prevRedraw = currentRedraw
1238
+ const prevDOM = currentDOM
1239
+ const hooks: Array<() => void> = []
1240
+ const active = activeElement(dom)
1241
+ const namespace = dom.namespaceURI
1242
+
1243
+ currentDOM = dom
1244
+ currentRedraw = typeof redraw === 'function' ? redraw : undefined
1245
+ currentRender = {}
1246
+ // Reset hydration error counter and mismatch count at start of each render cycle
1247
+ resetHydrationErrorCount()
1248
+ hydrationMismatchCount = 0
1249
+ try {
1250
+ // Detect hydration: DOM has children but no vnodes tracked
1251
+ // Only check children for Element nodes (DocumentFragment doesn't have children property)
1252
+ let isHydrating =
1253
+ (dom as any).vnodes == null &&
1254
+ dom.nodeType === 1 && // Element node
1255
+ 'children' in dom &&
1256
+ (dom as Element).children.length > 0
1257
+
1258
+ // First time rendering into a node clears it out (unless hydrating)
1259
+ if (!isHydrating && (dom as any).vnodes == null) dom.textContent = ''
1260
+ const normalized = (Vnode as any).normalizeChildren(Array.isArray(vnodes) ? vnodes : [vnodes])
1261
+ updateNodes(
1262
+ dom,
1263
+ (dom as any).vnodes,
1264
+ normalized,
1265
+ hooks,
1266
+ null,
1267
+ (namespace === 'http://www.w3.org/1999/xhtml' ? undefined : namespace) as string | undefined,
1268
+ isHydrating,
1269
+ )
1270
+
1271
+ // Check if we've exceeded mismatch threshold after processing nodes
1272
+ // If so, clear and re-render from scratch (client VDOM wins)
1273
+ if (isHydrating && hydrationMismatchCount > MAX_HYDRATION_MISMATCHES) {
1274
+ logger.warn('hydration mismatch threshold exceeded. clearing parent and re-rendering from client vdom.', {
1275
+ mismatchCount: hydrationMismatchCount,
1276
+ threshold: MAX_HYDRATION_MISMATCHES,
1277
+ })
1278
+ dom.textContent = ''
1279
+ hydrationMismatchCount = 0
1280
+ // Clear old vnodes and re-render without hydration flag
1281
+ ;(dom as any).vnodes = null
1282
+ // Re-render with fresh hooks array (hooks from first render are discarded)
1283
+ const overrideHooks: Array<() => void> = []
1284
+ updateNodes(
1285
+ dom,
1286
+ null,
1287
+ normalized,
1288
+ overrideHooks,
1289
+ null,
1290
+ (namespace === 'http://www.w3.org/1999/xhtml' ? undefined : namespace) as string | undefined,
1291
+ false,
1292
+ )
1293
+ // Execute hooks from override render
1294
+ for (let i = 0; i < overrideHooks.length; i++) overrideHooks[i]()
1295
+ }
1296
+
1297
+ ;(dom as any).vnodes = normalized
1298
+ // `document.activeElement` can return null: https://html.spec.whatwg.org/multipage/interaction.html#dom-document-activeelement
1299
+ if (active != null && activeElement(dom) !== active && typeof (active as any).focus === 'function')
1300
+ (active as any).focus()
1301
+ for (let i = 0; i < hooks.length; i++) hooks[i]()
1302
+ } finally {
1303
+ currentRedraw = prevRedraw
1304
+ currentDOM = prevDOM
1305
+ }
1306
+ }
1307
+ }