@pyreon/runtime-dom 0.24.5 → 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/mount.ts DELETED
@@ -1,597 +0,0 @@
1
- import type {
2
- ComponentFn,
3
- ForProps,
4
- NativeItem,
5
- PortalProps,
6
- RefProp,
7
- VNode,
8
- VNodeChild,
9
- } from '@pyreon/core'
10
- import {
11
- dispatchToErrorBoundary,
12
- EMPTY_PROPS,
13
- ForSymbol,
14
- Fragment,
15
- makeReactiveProps,
16
- PortalSymbol,
17
- propagateError,
18
- reportError,
19
- runWithHooks,
20
- } from '@pyreon/core'
21
- import { effectScope, renderEffect, runUntracked, setCurrentScope } from '@pyreon/reactivity'
22
- import { registerComponent, unregisterComponent } from './devtools'
23
- import { mountFor, mountKeyedList, mountReactive } from './nodes'
24
- import { applyProps } from './props'
25
-
26
- // Dev-mode gate: see `pyreon/no-process-dev-gate` lint rule for why this
27
- // uses `import.meta.env.DEV` instead of `typeof process !== 'undefined'`.
28
- const __DEV__ = process.env.NODE_ENV !== 'production'
29
-
30
- // Dev-time counter sink — see packages/internals/perf-harness for contract.
31
- const _countSink = globalThis as { __pyreon_count__?: (name: string, n?: number) => void }
32
-
33
- type Cleanup = () => void
34
- const noop: Cleanup = () => {
35
- /* noop */
36
- }
37
-
38
- // When > 0, we're mounting children inside an element — child cleanups can skip
39
- // DOM removal (parent element removal handles it). This avoids allocating a
40
- // removeChild closure for every nested element that has no reactive work.
41
- let _elementDepth = 0
42
-
43
- // Stack tracking which component is currently being mounted (depth-first order).
44
- // Used to infer parent/child relationships for DevTools.
45
- // Only allocated in dev — production mounts skip devtools entirely.
46
- let _mountingStack: string[] | undefined
47
- if (__DEV__) _mountingStack = []
48
-
49
- /**
50
- * Mount a single child into `parent`, inserting before `anchor` (null = append).
51
- * Returns a cleanup that removes the node(s) and disposes all reactive effects.
52
- *
53
- * This function is the hot path — all child types are handled inline to avoid
54
- * function call overhead in tight render loops (1000+ calls per list render).
55
- */
56
- export function mountChild(
57
- child: VNodeChild | VNodeChild[] | (() => VNodeChild | VNodeChild[]),
58
- parent: Node,
59
- anchor: Node | null = null,
60
- ): Cleanup {
61
- if (__DEV__) _countSink.__pyreon_count__?.('runtime.mountChild')
62
- // Reactive accessor — function that reads signals
63
- if (typeof child === 'function') {
64
- const sample = runUntracked(() => (child as () => VNodeChild | VNodeChild[])())
65
- if (isKeyedArray(sample)) {
66
- const prevDepth = _elementDepth
67
- _elementDepth = 0
68
- const cleanup = mountKeyedList(child as () => VNode[], parent, anchor, (v, p, a) =>
69
- mountChild(v, p, a),
70
- )
71
- _elementDepth = prevDepth
72
- return cleanup
73
- }
74
- // Text fast path: reactive string/number/boolean — update text.data in-place
75
- if (typeof sample === 'string' || typeof sample === 'number' || typeof sample === 'boolean') {
76
- const text = document.createTextNode(sample === false ? '' : String(sample))
77
- parent.insertBefore(text, anchor)
78
- const dispose = renderEffect(() => {
79
- const v = (child as () => unknown)()
80
- const next = v == null || v === false ? '' : String(v as string | number)
81
- if (next !== text.data) text.data = next
82
- })
83
- if (_elementDepth > 0) return dispose
84
- return () => {
85
- dispose()
86
- const p = text.parentNode
87
- if (p && (p as Element).isConnected !== false) p.removeChild(text)
88
- }
89
- }
90
- const prevDepth = _elementDepth
91
- _elementDepth = 0
92
- const cleanup = mountReactive(child as () => VNodeChild, parent, anchor, mountChild)
93
- _elementDepth = prevDepth
94
- return cleanup
95
- }
96
-
97
- // Array of children (e.g. from .map())
98
- if (Array.isArray(child)) return mountChildren(child, parent, anchor)
99
-
100
- // Nothing to render
101
- if (child == null || child === false) return noop
102
-
103
- // Primitive — text node (static, no reactive effects to tear down).
104
- if (typeof child !== 'object') {
105
- parent.insertBefore(document.createTextNode(String(child)), anchor)
106
- return noop
107
- }
108
-
109
- // NativeItem — pre-built DOM element from _tpl() or createTemplate().
110
- if ((child as unknown as NativeItem).__isNative) {
111
- const native = child as unknown as NativeItem
112
- parent.insertBefore(native.el, anchor)
113
- if (!native.cleanup) {
114
- if (_elementDepth > 0) return noop
115
- return () => {
116
- const p = native.el.parentNode
117
- if (p && (p as Element).isConnected !== false) p.removeChild(native.el)
118
- }
119
- }
120
- if (_elementDepth > 0) return native.cleanup
121
- return () => {
122
- native.cleanup?.()
123
- const p = native.el.parentNode
124
- if (p && (p as Element).isConnected !== false) p.removeChild(native.el)
125
- }
126
- }
127
-
128
- // VNode — element, component, fragment, For, Portal
129
- const vnode = child as VNode
130
-
131
- if (vnode.type === Fragment) return mountChildren(vnode.children ?? [], parent, anchor)
132
-
133
- if (vnode.type === (ForSymbol as unknown as string)) {
134
- // Compiler wraps `<For each={signal}>` in `_rp(() => signal())` →
135
- // `props.each` is a getter that returns the resolved array, not the
136
- // function. Destructuring eagerly captures the array and breaks
137
- // reactivity + crashes mountFor (which calls `source()`). Read each
138
- // lazily so the _rp getter fires inside mountFor's effect, preserving
139
- // signal tracking. User-written `each={() => fn()}` (already a
140
- // function, not _rp-wrapped) still works because props.each is the
141
- // function itself.
142
- const props = vnode.props as unknown as ForProps<unknown>
143
- const initialEach = props.each as unknown
144
- const source: () => unknown[] =
145
- typeof initialEach === 'function'
146
- ? (initialEach as () => unknown[])
147
- : (() => props.each as unknown as unknown[])
148
- const prevDepth = _elementDepth
149
- _elementDepth = 0
150
- const cleanup = mountFor(
151
- source as () => unknown[],
152
- props.by,
153
- props.children,
154
- parent,
155
- anchor,
156
- mountChild,
157
- )
158
- _elementDepth = prevDepth
159
- return cleanup
160
- }
161
-
162
- if (vnode.type === (PortalSymbol as unknown as string)) {
163
- const { target, children } = vnode.props as unknown as PortalProps
164
- if (__DEV__ && !target) {
165
- console.warn('[Pyreon] <Portal> received a falsy `target`. Provide a valid DOM element.')
166
- return noop
167
- }
168
- if (__DEV__ && !(target instanceof Node)) {
169
- console.warn(
170
- `[Pyreon] <Portal> target must be a DOM node. Received ${typeof target}. ` +
171
- 'Use document.getElementById() or a ref to get the target element.',
172
- )
173
- }
174
- return mountChild(children, target, null)
175
- }
176
-
177
- if (typeof vnode.type === 'function') {
178
- return mountComponent(vnode as VNode & { type: ComponentFn }, parent, anchor)
179
- }
180
-
181
- if (__DEV__ && typeof vnode.type !== 'string') {
182
- console.warn(
183
- `[Pyreon] Invalid VNode type: expected a string tag or component function, ` +
184
- `received ${typeof vnode.type} (${String(vnode.type)}). ` +
185
- `This usually means you passed an object or class instead of a component function.`,
186
- )
187
- return noop
188
- }
189
-
190
- return mountElement(vnode, parent, anchor)
191
- }
192
-
193
- // ─── Element ─────────────────────────────────────────────────────────────────
194
-
195
- // Void elements that cannot have children
196
- const VOID_ELEMENTS = new Set([
197
- 'area',
198
- 'base',
199
- 'br',
200
- 'col',
201
- 'embed',
202
- 'hr',
203
- 'img',
204
- 'input',
205
- 'link',
206
- 'meta',
207
- 'param',
208
- 'source',
209
- 'track',
210
- 'wbr',
211
- ])
212
-
213
- const SVG_NS = 'http://www.w3.org/2000/svg'
214
- const MATHML_NS = 'http://www.w3.org/1998/Math/MathML'
215
-
216
- // Tags that require namespace-aware creation
217
- const SVG_TAGS = new Set([
218
- 'svg',
219
- 'circle',
220
- 'ellipse',
221
- 'line',
222
- 'path',
223
- 'polygon',
224
- 'polyline',
225
- 'rect',
226
- 'g',
227
- 'defs',
228
- 'symbol',
229
- 'use',
230
- 'text',
231
- 'tspan',
232
- 'textPath',
233
- 'image',
234
- 'clipPath',
235
- 'mask',
236
- 'pattern',
237
- 'marker',
238
- 'linearGradient',
239
- 'radialGradient',
240
- 'stop',
241
- 'filter',
242
- 'feBlend',
243
- 'feColorMatrix',
244
- 'feComponentTransfer',
245
- 'feComposite',
246
- 'feConvolveMatrix',
247
- 'feDiffuseLighting',
248
- 'feDisplacementMap',
249
- 'feFlood',
250
- 'feGaussianBlur',
251
- 'feImage',
252
- 'feMerge',
253
- 'feMergeNode',
254
- 'feMorphology',
255
- 'feOffset',
256
- 'feSpecularLighting',
257
- 'feTile',
258
- 'feTurbulence',
259
- 'animate',
260
- 'animateMotion',
261
- 'animateTransform',
262
- 'set',
263
- 'desc',
264
- 'title',
265
- 'metadata',
266
- 'foreignObject',
267
- ])
268
-
269
- const MATHML_TAGS = new Set([
270
- 'math',
271
- 'mi',
272
- 'mo',
273
- 'mn',
274
- 'ms',
275
- 'mtext',
276
- 'mspace',
277
- 'mrow',
278
- 'mfrac',
279
- 'msqrt',
280
- 'mroot',
281
- 'msub',
282
- 'msup',
283
- 'msubsup',
284
- 'munder',
285
- 'mover',
286
- 'munderover',
287
- 'mtable',
288
- 'mtr',
289
- 'mtd',
290
- 'mpadded',
291
- 'mphantom',
292
- 'menclose',
293
- ])
294
-
295
- /** Track SVG context depth — children of <svg> inherit the SVG namespace. */
296
- let _svgDepth = 0
297
- let _mathmlDepth = 0
298
-
299
- function createElementWithNS(tag: string): Element {
300
- if (_svgDepth > 0 || SVG_TAGS.has(tag)) return document.createElementNS(SVG_NS, tag)
301
- if (_mathmlDepth > 0 || MATHML_TAGS.has(tag)) return document.createElementNS(MATHML_NS, tag)
302
- return document.createElement(tag)
303
- }
304
-
305
- function mountElement(vnode: VNode, parent: Node, anchor: Node | null): Cleanup {
306
- const tag = vnode.type as string
307
- const el = createElementWithNS(tag)
308
- const isSvg = tag === 'svg'
309
- const isMathml = tag === 'math'
310
- if (isSvg) _svgDepth++
311
- if (isMathml) _mathmlDepth++
312
-
313
- if (__DEV__ && (vnode.children?.length ?? 0) > 0 && VOID_ELEMENTS.has(vnode.type as string)) {
314
- console.warn(
315
- `[Pyreon] <${vnode.type as string}> is a void element and cannot have children. ` +
316
- 'Children passed to void elements will be ignored by the browser.',
317
- )
318
- }
319
-
320
- // Skip applyProps entirely when props is the shared empty sentinel (identity check — no allocation)
321
- const props = vnode.props
322
- const propCleanup: Cleanup | null = props !== EMPTY_PROPS ? applyProps(el, props) : null
323
-
324
- // Mount children inside element context — nested elements can skip DOM removal closures
325
- _elementDepth++
326
- const childCleanup = mountChildren(vnode.children ?? [], el, null)
327
- _elementDepth--
328
- if (isSvg) _svgDepth--
329
- if (isMathml) _mathmlDepth--
330
-
331
- parent.insertBefore(el, anchor)
332
-
333
- // Populate ref after the element is in the DOM
334
- const ref = props.ref as RefProp<Element> | null | undefined
335
- if (ref) {
336
- if (typeof ref === 'function') ref(el)
337
- else ref.current = el
338
- }
339
-
340
- if (!propCleanup && childCleanup === noop && !ref) {
341
- if (_elementDepth > 0) return noop
342
- return () => {
343
- const p = el.parentNode
344
- if (p && (p as Element).isConnected !== false) p.removeChild(el)
345
- }
346
- }
347
-
348
- if (_elementDepth > 0) {
349
- if (!ref && !propCleanup) return childCleanup
350
- if (!ref && propCleanup)
351
- return () => {
352
- propCleanup()
353
- childCleanup()
354
- }
355
- const refToClean = ref
356
- return () => {
357
- if (refToClean) {
358
- if (typeof refToClean === 'function') refToClean(null)
359
- else refToClean.current = null
360
- }
361
- if (propCleanup) propCleanup()
362
- childCleanup()
363
- }
364
- }
365
-
366
- return () => {
367
- if (ref) {
368
- if (typeof ref === 'function') ref(null)
369
- else ref.current = null
370
- }
371
- if (propCleanup) propCleanup()
372
- childCleanup()
373
- const p = el.parentNode
374
- if (p && (p as Element).isConnected !== false) p.removeChild(el)
375
- }
376
- }
377
-
378
- // ─── Component ───────────────────────────────────────────────────────────────
379
-
380
- function mountComponent(
381
- vnode: VNode & { type: ComponentFn },
382
- parent: Node,
383
- anchor: Node | null,
384
- ): Cleanup {
385
- const scope = effectScope()
386
- setCurrentScope(scope)
387
-
388
- let hooks: ReturnType<typeof runWithHooks>['hooks']
389
- let output: VNodeChild
390
-
391
- const componentName = (vnode.type.name || 'Anonymous') as string
392
-
393
- // Devtools: generate ID + track parent/child hierarchy (dev only).
394
- // In production, compId/devParentId are never assigned — Vite tree-shakes
395
- // all __DEV__ blocks + the devtools module import to zero bytes.
396
- let compId: string | undefined
397
- let devParentId: string | null | undefined
398
- if (__DEV__) {
399
- compId = `${componentName}-${Math.random().toString(36).slice(2, 9)}`
400
- devParentId = _mountingStack![_mountingStack!.length - 1] ?? null
401
- _mountingStack!.push(compId)
402
- }
403
-
404
- // Merge vnode.children into props.children if not already set
405
- const children = vnode.children ?? []
406
- const rawProps =
407
- children.length > 0 && (vnode.props as Record<string, unknown>).children === undefined
408
- ? {
409
- ...vnode.props,
410
- children: children.length === 1 ? children[0] : children,
411
- }
412
- : vnode.props
413
-
414
- // Convert compiler-emitted () => expr wrappers into getter properties.
415
- // This makes component props reactive — reading props.state inside an
416
- // effect/computed tracks the underlying signals.
417
- const mergedProps =
418
- rawProps === EMPTY_PROPS ? rawProps : makeReactiveProps(rawProps as Record<string, unknown>)
419
-
420
- try {
421
- const result = runWithHooks(vnode.type, mergedProps)
422
- hooks = result.hooks
423
- output = result.vnode
424
- } catch (err) {
425
- if (__DEV__) _mountingStack!.pop()
426
- setCurrentScope(null)
427
- scope.stop()
428
- reportError({
429
- component: componentName,
430
- phase: 'setup',
431
- error: err,
432
- timestamp: Date.now(),
433
- props: vnode.props as Record<string, unknown>,
434
- })
435
- const handled = dispatchToErrorBoundary(err)
436
- if (!handled) {
437
- console.error(`[Pyreon] <${componentName}> threw during setup:`, err)
438
- }
439
- if (__DEV__ && !handled) {
440
- const overlay = document.createElement('pre')
441
- overlay.style.cssText =
442
- 'color:#e53e3e;background:#fff5f5;padding:12px;border:2px solid #e53e3e;border-radius:6px;font-size:12px;margin:4px;font-family:monospace;white-space:pre-wrap;word-break:break-word'
443
- const e = err as Error
444
- overlay.textContent = `[${componentName}] ${e.message ?? err}\n${e.stack ?? ''}`
445
- parent.insertBefore(overlay, anchor)
446
- return () => overlay.remove()
447
- }
448
- return noop
449
- } finally {
450
- setCurrentScope(null)
451
- }
452
-
453
- if (__DEV__ && output != null && typeof output === 'object') {
454
- if (output instanceof Promise) {
455
- console.warn(
456
- `[Pyreon] Component <${componentName}> returned a Promise. ` +
457
- 'Components must be synchronous — use lazy() + Suspense for async loading, ' +
458
- 'or fetch data in onMount and store it in a signal.',
459
- )
460
- } else if (!('type' in output) && !Array.isArray(output) && !(output as any).__isNative) {
461
- // Objects without `type` that are NOT arrays (valid VNodeChild[])
462
- // and NOT NativeItems (from _tpl()) are invalid. Arrays come from
463
- // Fragment returns, NativeItems come from compiled templates.
464
- console.warn(
465
- `[Pyreon] Component <${componentName}> returned an invalid value. Components must return a VNode, string, null, function, or array.`,
466
- )
467
- }
468
- }
469
-
470
- if (hooks.update) {
471
- for (const fn of hooks.update) scope.addUpdateHook(fn)
472
- }
473
-
474
- let subtreeCleanup: Cleanup = noop
475
- try {
476
- subtreeCleanup = output != null ? mountChild(output, parent, anchor) : noop
477
- } catch (err) {
478
- if (__DEV__) _mountingStack!.pop()
479
- scope.stop()
480
- const handled = propagateError(err, hooks) || dispatchToErrorBoundary(err)
481
- if (!handled) {
482
- reportError({
483
- component: componentName,
484
- phase: 'render',
485
- error: err,
486
- timestamp: Date.now(),
487
- props: vnode.props as Record<string, unknown>,
488
- })
489
- console.error(`[Pyreon] <${componentName}> threw during render:`, err)
490
- }
491
- return noop
492
- }
493
-
494
- if (__DEV__) {
495
- _mountingStack!.pop()
496
- const firstEl = parent instanceof Element ? parent.firstElementChild : null
497
- registerComponent(compId!, componentName, firstEl, devParentId!)
498
- }
499
-
500
- // Fire onMount hooks inline — effects created inside are tracked by the scope.
501
- // Lazy-allocate mountCleanups only when an onMount callback returns a cleanup fn.
502
- let mountCleanups: Cleanup[] | null = null
503
- if (hooks.mount) {
504
- for (const fn of hooks.mount) {
505
- try {
506
- let cleanup: (() => void) | undefined
507
- scope.runInScope(() => {
508
- cleanup = fn() as (() => void) | undefined
509
- })
510
- if (cleanup) {
511
- if (mountCleanups === null) mountCleanups = []
512
- mountCleanups.push(cleanup)
513
- }
514
- } catch (err) {
515
- console.error(`[Pyreon] Error in onMount hook of <${componentName}>:`, err)
516
- reportError({ component: componentName, phase: 'mount', error: err, timestamp: Date.now() })
517
- }
518
- }
519
- }
520
-
521
- return () => {
522
- if (__DEV__) unregisterComponent(compId!)
523
- scope.stop()
524
- subtreeCleanup()
525
- if (hooks.unmount) {
526
- for (const fn of hooks.unmount) {
527
- try {
528
- fn()
529
- } catch (err) {
530
- console.error(`[Pyreon] Error in onUnmount hook of <${componentName}>:`, err)
531
- reportError({
532
- component: componentName,
533
- phase: 'unmount',
534
- error: err,
535
- timestamp: Date.now(),
536
- })
537
- }
538
- }
539
- }
540
- if (mountCleanups) for (const fn of mountCleanups) fn()
541
- }
542
- }
543
-
544
- // ─── Children ────────────────────────────────────────────────────────────────
545
-
546
- function mountChildren(children: VNodeChild[], parent: Node, anchor: Node | null): Cleanup {
547
- if (children.length === 0) return noop
548
-
549
- // 1-child fast path
550
- if (children.length === 1) {
551
- const c = children[0] as VNodeChild
552
- if (c !== undefined) {
553
- if (anchor === null && (typeof c === 'string' || typeof c === 'number')) {
554
- ;(parent as HTMLElement).textContent = String(c)
555
- return noop
556
- }
557
- return mountChild(c, parent, anchor)
558
- }
559
- }
560
-
561
- // 2-child fast path — avoids .map() allocation (covers <tr><td/><td/></tr>)
562
- if (children.length === 2) {
563
- const c0 = children[0] as VNodeChild
564
- const c1 = children[1] as VNodeChild
565
- if (c0 !== undefined && c1 !== undefined) {
566
- const d0 = mountChild(c0, parent, anchor)
567
- const d1 = mountChild(c1, parent, anchor)
568
- if (d0 === noop && d1 === noop) return noop
569
- if (d0 === noop) return d1
570
- if (d1 === noop) return d0
571
- return () => {
572
- d0()
573
- d1()
574
- }
575
- }
576
- }
577
-
578
- const cleanups = children.map((c) => mountChild(c, parent, anchor))
579
- return () => {
580
- for (const fn of cleanups) fn()
581
- }
582
- }
583
-
584
- // ─── Keyed array detection ────────────────────────────────────────────────────
585
-
586
- /** Returns true if value is a non-empty array of VNodes that all carry keys. */
587
- function isKeyedArray(value: unknown): value is VNode[] {
588
- if (!Array.isArray(value) || value.length === 0) return false
589
- return value.every(
590
- (v) =>
591
- v !== null &&
592
- typeof v === 'object' &&
593
- !Array.isArray(v) &&
594
- (v as VNode).key !== null &&
595
- (v as VNode).key !== undefined,
596
- )
597
- }