@pyreon/runtime-dom 0.21.0 → 0.23.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,1381 +1,3 @@
1
- import { EMPTY_PROPS, ForSymbol, Fragment, PortalSymbol, captureContextStack, createRef, cx, dispatchToErrorBoundary, h, makeReactiveProps, nativeCompat, normalizeStyleValue, onMount, propagateError, reportError, restoreContextStack, runWithHooks, toKebabCase } from "@pyreon/core";
2
- import { batch, effect, effectScope, renderEffect, runUntracked, setCurrentScope } from "@pyreon/reactivity";
1
+ import { t as KeepAlive } from "./_chunks/keep-alive-BM7bn3W9.js";
3
2
 
4
- //#region src/devtools.ts
5
- const _components = /* @__PURE__ */ new Map();
6
- const _mountListeners = [];
7
- const _unmountListeners = [];
8
- function registerComponent(id, name, el, parentId) {
9
- const entry = {
10
- id,
11
- name,
12
- el,
13
- parentId,
14
- childIds: []
15
- };
16
- _components.set(id, entry);
17
- if (parentId) {
18
- const parent = _components.get(parentId);
19
- if (parent) parent.childIds.push(id);
20
- }
21
- for (const cb of _mountListeners) cb(entry);
22
- }
23
- function unregisterComponent(id) {
24
- const entry = _components.get(id);
25
- if (!entry) return;
26
- if (entry.parentId) {
27
- const parent = _components.get(entry.parentId);
28
- if (parent) parent.childIds = parent.childIds.filter((c) => c !== id);
29
- }
30
- _components.delete(id);
31
- for (const cb of _unmountListeners) cb(id);
32
- }
33
-
34
- //#endregion
35
- //#region src/nodes.ts
36
- const __DEV__$2 = process.env.NODE_ENV !== "production";
37
- const _countSink$2 = globalThis;
38
- /**
39
- * Move all nodes strictly between `start` and `end` into a throwaway
40
- * DocumentFragment, detaching them from the live DOM in O(n) top-level moves.
41
- *
42
- * This is dramatically faster than Range.deleteContents() in JS-based DOMs
43
- * (happy-dom, jsdom) where deleting connected nodes with deep subtrees is O(n²).
44
- * In real browsers both approaches are similar, but the fragment approach is
45
- * never slower and avoids the pathological case.
46
- *
47
- * After this call every moved node has isConnected=false, so cleanup functions
48
- * that guard removeChild with `isConnected !== false` become no-ops.
49
- */
50
- function clearBetween(start, end) {
51
- const frag = document.createDocumentFragment();
52
- let cur = start.nextSibling;
53
- while (cur && cur !== end) {
54
- const next = cur.nextSibling;
55
- frag.appendChild(cur);
56
- cur = next;
57
- }
58
- }
59
- /** Emit `runtime.cleanup` once per registered mount cleanup that actually runs. */
60
- function _emitCleanup() {
61
- if (__DEV__$2) _countSink$2.__pyreon_count__?.("runtime.cleanup");
62
- }
63
- /**
64
- * Mount a reactive node whose content changes over time.
65
- *
66
- * A comment node is used as a stable anchor point in the DOM.
67
- * On each change: old nodes are removed, new ones inserted before the anchor.
68
- */
69
- function mountReactive(accessor, parent, anchor, mount) {
70
- if (__DEV__$2) _countSink$2.__pyreon_count__?.("runtime.mountReactive");
71
- const marker = document.createComment("pyreon");
72
- parent.insertBefore(marker, anchor);
73
- const contextSnapshot = captureContextStack();
74
- let currentCleanup = () => {};
75
- let hasCleanup = false;
76
- let generation = 0;
77
- const e = effect(() => {
78
- const myGen = ++generation;
79
- if (hasCleanup) _emitCleanup();
80
- runUntracked(() => currentCleanup());
81
- currentCleanup = () => {};
82
- hasCleanup = false;
83
- const value = accessor();
84
- if (value != null && value !== false) {
85
- const cleanup = runUntracked(() => restoreContextStack(contextSnapshot, () => mount(value, parent, marker)));
86
- if (myGen === generation) {
87
- currentCleanup = cleanup;
88
- hasCleanup = true;
89
- } else {
90
- _emitCleanup();
91
- cleanup();
92
- }
93
- }
94
- });
95
- return () => {
96
- e.dispose();
97
- if (hasCleanup) _emitCleanup();
98
- currentCleanup();
99
- marker.parentNode?.removeChild(marker);
100
- };
101
- }
102
- const _keyedAnchors = /* @__PURE__ */ new WeakSet();
103
- function growLisArrays(lis, n) {
104
- if (n <= lis.pred.length) return lis;
105
- return {
106
- tails: new Int32Array(n + 16),
107
- tailIdx: new Int32Array(n + 16),
108
- pred: new Int32Array(n + 16),
109
- stay: new Uint8Array(n + 16)
110
- };
111
- }
112
- function computeKeyedLis(lis, n, newKeyOrder, curPos) {
113
- const { tails, tailIdx, pred } = lis;
114
- let lisLen = 0;
115
- let ops = 0;
116
- for (let i = 0; i < n; i++) {
117
- const key = newKeyOrder[i];
118
- if (key === void 0) continue;
119
- const v = curPos.get(key) ?? -1;
120
- if (v < 0) continue;
121
- let lo = 0;
122
- let hi = lisLen;
123
- while (lo < hi) {
124
- const mid = lo + hi >> 1;
125
- ops++;
126
- if (tails[mid] < v) lo = mid + 1;
127
- else hi = mid;
128
- }
129
- tails[lo] = v;
130
- tailIdx[lo] = i;
131
- if (lo > 0) pred[i] = tailIdx[lo - 1];
132
- if (lo === lisLen) lisLen++;
133
- }
134
- if (__DEV__$2 && ops > 0) _countSink$2.__pyreon_count__?.("runtime.mountFor.lisOps", ops);
135
- return lisLen;
136
- }
137
- function markStayingEntries(lis, lisLen) {
138
- const { tailIdx, pred, stay } = lis;
139
- let cur = lisLen > 0 ? tailIdx[lisLen - 1] : -1;
140
- while (cur !== -1) {
141
- stay[cur] = 1;
142
- cur = pred[cur];
143
- }
144
- }
145
- function applyKeyedMoves(n, newKeyOrder, stay, cache, parent, tailMarker) {
146
- let cursor = tailMarker;
147
- for (let i = n - 1; i >= 0; i--) {
148
- const key = newKeyOrder[i];
149
- if (key === void 0) continue;
150
- const entry = cache.get(key);
151
- if (!entry) continue;
152
- if (!stay[i]) moveEntryBefore(parent, entry.anchor, cursor);
153
- cursor = entry.anchor;
154
- }
155
- }
156
- /** Grow LIS typed arrays if needed, then compute and apply reorder. */
157
- function keyedListReorder(lis, n, newKeyOrder, curPos, cache, parent, tailMarker) {
158
- const grown = growLisArrays(lis, n);
159
- grown.pred.fill(-1, 0, n);
160
- grown.stay.fill(0, 0, n);
161
- markStayingEntries(grown, computeKeyedLis(grown, n, newKeyOrder, curPos));
162
- applyKeyedMoves(n, newKeyOrder, grown.stay, cache, parent, tailMarker);
163
- return grown;
164
- }
165
- function mountKeyedList(accessor, parent, listAnchor, mountVNode) {
166
- const startMarker = document.createComment("");
167
- const tailMarker = document.createComment("");
168
- parent.insertBefore(startMarker, listAnchor);
169
- parent.insertBefore(tailMarker, listAnchor);
170
- const cache = /* @__PURE__ */ new Map();
171
- const curPos = /* @__PURE__ */ new Map();
172
- let currentKeyOrder = [];
173
- let lis = {
174
- tails: new Int32Array(16),
175
- tailIdx: new Int32Array(16),
176
- pred: new Int32Array(16),
177
- stay: new Uint8Array(16)
178
- };
179
- const collectKeyOrder = (newList) => {
180
- const newKeyOrder = [];
181
- const newKeySet = /* @__PURE__ */ new Set();
182
- for (const vnode of newList) {
183
- const key = vnode.key;
184
- if (key !== null && key !== void 0) {
185
- newKeyOrder.push(key);
186
- newKeySet.add(key);
187
- }
188
- }
189
- return {
190
- newKeyOrder,
191
- newKeySet
192
- };
193
- };
194
- const removeStaleEntries = (newKeySet) => {
195
- for (const [key, entry] of cache) {
196
- if (newKeySet.has(key)) continue;
197
- _emitCleanup();
198
- entry.cleanup();
199
- entry.anchor.parentNode?.removeChild(entry.anchor);
200
- cache.delete(key);
201
- curPos.delete(key);
202
- }
203
- };
204
- const mountNewEntries = (newList) => {
205
- for (const vnode of newList) {
206
- const key = vnode.key;
207
- if (key === null || key === void 0) continue;
208
- if (cache.has(key)) continue;
209
- const anchor = document.createComment("");
210
- _keyedAnchors.add(anchor);
211
- parent.insertBefore(anchor, tailMarker);
212
- const cleanup = mountVNode(vnode, parent, tailMarker);
213
- cache.set(key, {
214
- anchor,
215
- cleanup
216
- });
217
- }
218
- };
219
- const e = effect(() => {
220
- const newList = accessor();
221
- const n = newList.length;
222
- runUntracked(() => {
223
- if (n === 0 && cache.size > 0) {
224
- for (const entry of cache.values()) {
225
- _emitCleanup();
226
- entry.cleanup();
227
- }
228
- cache.clear();
229
- curPos.clear();
230
- currentKeyOrder = [];
231
- clearBetween(startMarker, tailMarker);
232
- return;
233
- }
234
- const { newKeyOrder, newKeySet } = collectKeyOrder(newList);
235
- removeStaleEntries(newKeySet);
236
- mountNewEntries(newList);
237
- if (currentKeyOrder.length > 0 && n > 0) lis = keyedListReorder(lis, n, newKeyOrder, curPos, cache, parent, tailMarker);
238
- curPos.clear();
239
- for (let i = 0; i < newKeyOrder.length; i++) {
240
- const k = newKeyOrder[i];
241
- if (k !== void 0) curPos.set(k, i);
242
- }
243
- currentKeyOrder = newKeyOrder;
244
- });
245
- });
246
- return () => {
247
- e.dispose();
248
- for (const entry of cache.values()) {
249
- _emitCleanup();
250
- entry.cleanup();
251
- entry.anchor.parentNode?.removeChild(entry.anchor);
252
- }
253
- cache.clear();
254
- startMarker.parentNode?.removeChild(startMarker);
255
- tailMarker.parentNode?.removeChild(tailMarker);
256
- };
257
- }
258
- /** Maximum number of displaced positions before falling back to full LIS. */
259
- const SMALL_K = 8;
260
- const _forAnchors = /* @__PURE__ */ new WeakSet();
261
- /** Try small-k reorder; returns true if handled, false if LIS fallback needed. */
262
- function trySmallKReorder(n, newKeys, currentKeys, cache, liveParent, tailMarker) {
263
- if (n !== currentKeys.length) return false;
264
- const diffs = [];
265
- for (let i = 0; i < n; i++) if (newKeys[i] !== currentKeys[i]) {
266
- diffs.push(i);
267
- if (diffs.length > SMALL_K) return false;
268
- }
269
- if (diffs.length > 0) smallKPlace(liveParent, diffs, newKeys, cache, tailMarker);
270
- for (const i of diffs) {
271
- const cached = cache.get(newKeys[i]);
272
- if (cached) cached.pos = i;
273
- }
274
- return true;
275
- }
276
- function computeForLis(lis, n, newKeys, cache) {
277
- const { tails, tailIdx, pred } = lis;
278
- let lisLen = 0;
279
- let ops = 0;
280
- let lastV = -1;
281
- for (let i = 0; i < n; i++) {
282
- const key = newKeys[i];
283
- const v = cache.get(key)?.pos ?? 0;
284
- if (v > lastV) {
285
- tails[lisLen] = v;
286
- tailIdx[lisLen] = i;
287
- if (lisLen > 0) pred[i] = tailIdx[lisLen - 1];
288
- lisLen++;
289
- lastV = v;
290
- continue;
291
- }
292
- let lo;
293
- if (v < lisLen && tails[v] === v) lo = v;
294
- else {
295
- lo = 0;
296
- let hi = lisLen;
297
- while (lo < hi) {
298
- const mid = lo + hi >> 1;
299
- ops++;
300
- if (tails[mid] < v) lo = mid + 1;
301
- else hi = mid;
302
- }
303
- }
304
- tails[lo] = v;
305
- tailIdx[lo] = i;
306
- if (lo > 0) pred[i] = tailIdx[lo - 1];
307
- }
308
- if (__DEV__$2 && ops > 0) _countSink$2.__pyreon_count__?.("runtime.mountFor.lisOps", ops);
309
- return lisLen;
310
- }
311
- function applyForMoves(n, newKeys, stay, cache, liveParent, tailMarker) {
312
- let cursor = tailMarker;
313
- for (let i = n - 1; i >= 0; i--) {
314
- const entry = cache.get(newKeys[i]);
315
- if (!entry) continue;
316
- if (!stay[i]) moveEntryBefore(liveParent, entry.anchor, cursor);
317
- cursor = entry.anchor;
318
- }
319
- }
320
- /** LIS-based reorder for mountFor. */
321
- function forLisReorder(lis, n, newKeys, cache, liveParent, tailMarker) {
322
- const grown = growLisArrays(lis, n);
323
- grown.pred.fill(-1, 0, n);
324
- grown.stay.fill(0, 0, n);
325
- markStayingEntries(grown, computeForLis(grown, n, newKeys, cache));
326
- applyForMoves(n, newKeys, grown.stay, cache, liveParent, tailMarker);
327
- for (let i = 0; i < n; i++) {
328
- const cached = cache.get(newKeys[i]);
329
- if (cached) cached.pos = i;
330
- }
331
- return grown;
332
- }
333
- /**
334
- * Keyed reconciler that works directly on the source item array.
335
- *
336
- * Optimizations:
337
- * - Calls renderItem() only for NEW keys — 0 VNode allocations for reorders
338
- * - Small-k fast path: if <= SMALL_K positions changed, skips LIS
339
- * - Fast clear path: moves nodes to DocumentFragment for O(n) bulk detach
340
- * - Fresh render fast path: skips stale-check and reorder on first render
341
- */
342
- function mountFor(source, getKey, renderItem, parent, anchor, mountChild) {
343
- const startMarker = document.createComment("");
344
- const tailMarker = document.createComment("");
345
- parent.insertBefore(startMarker, anchor);
346
- parent.insertBefore(tailMarker, anchor);
347
- let cache = /* @__PURE__ */ new Map();
348
- let currentKeys = [];
349
- const _reusableKeySet = /* @__PURE__ */ new Set();
350
- let cleanupCount = 0;
351
- let anchorsRegistered = false;
352
- let lis = {
353
- tails: new Int32Array(16),
354
- tailIdx: new Int32Array(16),
355
- pred: new Int32Array(16),
356
- stay: new Uint8Array(16)
357
- };
358
- const warnForKey = (seen, key) => {
359
- if (!seen) return;
360
- if (__DEV__$2 && key == null) console.warn("[Pyreon] <For> `by` function returned null/undefined. Keys must be strings or numbers. Check your `by` prop.");
361
- if (seen.has(key)) {
362
- if (__DEV__$2) console.warn(`[Pyreon] Duplicate key "${String(key)}" in <For> list. Keys must be unique.`);
363
- return true;
364
- }
365
- seen.add(key);
366
- return false;
367
- };
368
- /** Render item into container, update cache+cleanupCount. No anchor registration. */
369
- const renderInto = (item, key, pos, container, before) => {
370
- const result = renderItem(item);
371
- if (result.__isNative) {
372
- const native = result;
373
- container.insertBefore(native.el, before);
374
- cache.set(key, {
375
- anchor: native.el,
376
- cleanup: native.cleanup,
377
- pos
378
- });
379
- if (native.cleanup) cleanupCount++;
380
- return;
381
- }
382
- const priorLast = before ? before.previousSibling : container.lastChild;
383
- const cl = mountChild(result, container, before);
384
- const firstMounted = priorLast ? priorLast.nextSibling : container.firstChild;
385
- if (!firstMounted || firstMounted === before) {
386
- const ph = document.createComment("");
387
- container.insertBefore(ph, before);
388
- cache.set(key, {
389
- anchor: ph,
390
- cleanup: cl,
391
- pos
392
- });
393
- } else cache.set(key, {
394
- anchor: firstMounted,
395
- cleanup: cl,
396
- pos
397
- });
398
- cleanupCount++;
399
- };
400
- const handleFreshRender = (items, n, liveParent) => {
401
- const frag = document.createDocumentFragment();
402
- const keys = new Array(n);
403
- const _seenKeys = /* @__PURE__ */ new Set();
404
- for (let i = 0; i < n; i++) {
405
- const item = items[i];
406
- const key = getKey(item);
407
- if (warnForKey(_seenKeys, key)) continue;
408
- keys[i] = key;
409
- renderInto(item, key, i, frag, null);
410
- }
411
- liveParent.insertBefore(frag, tailMarker);
412
- anchorsRegistered = false;
413
- currentKeys = keys;
414
- };
415
- const collectNewKeys = (items, n) => {
416
- const newKeys = new Array(n);
417
- const _seenUpdate = /* @__PURE__ */ new Set();
418
- for (let i = 0; i < n; i++) {
419
- newKeys[i] = getKey(items[i]);
420
- warnForKey(_seenUpdate, newKeys[i]);
421
- }
422
- return newKeys;
423
- };
424
- const handleReplaceAll = (items, n, newKeys, liveParent) => {
425
- if (cleanupCount > 0) {
426
- for (const entry of cache.values()) if (entry.cleanup) {
427
- _emitCleanup();
428
- entry.cleanup();
429
- }
430
- }
431
- cache = /* @__PURE__ */ new Map();
432
- cleanupCount = 0;
433
- const parentParent = liveParent.parentNode;
434
- const canSwap = parentParent && liveParent.firstChild === startMarker && liveParent.lastChild === tailMarker;
435
- const frag = document.createDocumentFragment();
436
- for (let i = 0; i < n; i++) renderInto(items[i], newKeys[i], i, frag, null);
437
- anchorsRegistered = false;
438
- if (canSwap) {
439
- const fresh = liveParent.cloneNode(false);
440
- fresh.appendChild(startMarker);
441
- fresh.appendChild(frag);
442
- fresh.appendChild(tailMarker);
443
- parentParent.replaceChild(fresh, liveParent);
444
- } else {
445
- clearBetween(startMarker, tailMarker);
446
- liveParent.insertBefore(frag, tailMarker);
447
- }
448
- currentKeys = newKeys;
449
- };
450
- const removeStaleForEntries = (newKeySet) => {
451
- for (const [key, entry] of cache) {
452
- if (newKeySet.has(key)) continue;
453
- if (entry.cleanup) {
454
- _emitCleanup();
455
- entry.cleanup();
456
- cleanupCount--;
457
- }
458
- entry.anchor.parentNode?.removeChild(entry.anchor);
459
- cache.delete(key);
460
- }
461
- };
462
- const mountNewForEntries = (items, n, newKeys, liveParent) => {
463
- for (let i = 0; i < n; i++) {
464
- const key = newKeys[i];
465
- if (cache.has(key)) continue;
466
- renderInto(items[i], key, i, liveParent, tailMarker);
467
- const entry = cache.get(key);
468
- if (entry) _forAnchors.add(entry.anchor);
469
- }
470
- };
471
- const handleFastClear = (liveParent) => {
472
- if (cache.size === 0) return;
473
- if (cleanupCount > 0) {
474
- for (const entry of cache.values()) if (entry.cleanup) {
475
- _emitCleanup();
476
- entry.cleanup();
477
- }
478
- }
479
- const pp = liveParent.parentNode;
480
- if (pp && liveParent.firstChild === startMarker && liveParent.lastChild === tailMarker) {
481
- const fresh = liveParent.cloneNode(false);
482
- fresh.appendChild(startMarker);
483
- fresh.appendChild(tailMarker);
484
- pp.replaceChild(fresh, liveParent);
485
- } else clearBetween(startMarker, tailMarker);
486
- cache = /* @__PURE__ */ new Map();
487
- cleanupCount = 0;
488
- currentKeys = [];
489
- };
490
- const hasAnyKeptKey = (n, newKeys) => {
491
- for (let i = 0; i < n; i++) if (cache.has(newKeys[i])) return true;
492
- return false;
493
- };
494
- const handleIncrementalUpdate = (items, n, newKeys, liveParent) => {
495
- _reusableKeySet.clear();
496
- for (let i = 0; i < newKeys.length; i++) _reusableKeySet.add(newKeys[i]);
497
- removeStaleForEntries(_reusableKeySet);
498
- mountNewForEntries(items, n, newKeys, liveParent);
499
- if (!anchorsRegistered) {
500
- for (const entry of cache.values()) _forAnchors.add(entry.anchor);
501
- anchorsRegistered = true;
502
- }
503
- if (trySmallKReorder(n, newKeys, currentKeys, cache, liveParent, tailMarker)) {
504
- currentKeys = newKeys;
505
- return;
506
- }
507
- lis = forLisReorder(lis, n, newKeys, cache, liveParent, tailMarker);
508
- currentKeys = newKeys;
509
- };
510
- const e = effect(() => {
511
- const liveParent = startMarker.parentNode;
512
- if (!liveParent) return;
513
- const items = source();
514
- const n = items.length;
515
- runUntracked(() => {
516
- if (n === 0) {
517
- handleFastClear(liveParent);
518
- return;
519
- }
520
- if (currentKeys.length === 0) {
521
- handleFreshRender(items, n, liveParent);
522
- return;
523
- }
524
- const newKeys = collectNewKeys(items, n);
525
- if (!hasAnyKeptKey(n, newKeys)) {
526
- handleReplaceAll(items, n, newKeys, liveParent);
527
- return;
528
- }
529
- handleIncrementalUpdate(items, n, newKeys, liveParent);
530
- });
531
- });
532
- return () => {
533
- e.dispose();
534
- for (const entry of cache.values()) {
535
- if (cleanupCount > 0 && entry.cleanup) {
536
- _emitCleanup();
537
- entry.cleanup();
538
- }
539
- entry.anchor.parentNode?.removeChild(entry.anchor);
540
- }
541
- cache = /* @__PURE__ */ new Map();
542
- cleanupCount = 0;
543
- startMarker.parentNode?.removeChild(startMarker);
544
- tailMarker.parentNode?.removeChild(tailMarker);
545
- };
546
- }
547
- /**
548
- * Small-k reorder: directly place the k displaced entries without LIS.
549
- */
550
- function smallKPlace(parent, diffs, newKeys, cache, tailMarker) {
551
- const diffSet = new Set(diffs);
552
- let cursor = tailMarker;
553
- let prevDiffIdx = newKeys.length;
554
- for (let d = diffs.length - 1; d >= 0; d--) {
555
- const i = diffs[d];
556
- let nextNonDiff = -1;
557
- for (let j = i + 1; j < prevDiffIdx; j++) if (!diffSet.has(j)) {
558
- nextNonDiff = j;
559
- break;
560
- }
561
- if (nextNonDiff >= 0) {
562
- const nc = cache.get(newKeys[nextNonDiff])?.anchor;
563
- if (nc) cursor = nc;
564
- }
565
- const entry = cache.get(newKeys[i]);
566
- if (!entry) {
567
- prevDiffIdx = i;
568
- continue;
569
- }
570
- moveEntryBefore(parent, entry.anchor, cursor);
571
- cursor = entry.anchor;
572
- prevDiffIdx = i;
573
- }
574
- }
575
- /**
576
- * Move startNode and all siblings belonging to this entry to just before `before`.
577
- * Stops at the next entry anchor (identified via WeakSet) or the tail marker.
578
- *
579
- * Fast path: if the next sibling is already a boundary (another entry or tail),
580
- * this entry is a single node — skip the toMove array entirely.
581
- */
582
- function moveEntryBefore(parent, startNode, before) {
583
- const next = startNode.nextSibling;
584
- if (!next || next === before || next.parentNode === parent && (_forAnchors.has(next) || _keyedAnchors.has(next))) {
585
- parent.insertBefore(startNode, before);
586
- return;
587
- }
588
- const toMove = [startNode];
589
- let cur = next;
590
- while (cur && cur !== before) {
591
- const nextNode = cur.nextSibling;
592
- toMove.push(cur);
593
- cur = nextNode;
594
- if (cur && cur.parentNode === parent && (cur === before || _forAnchors.has(cur) || _keyedAnchors.has(cur))) break;
595
- }
596
- for (const node of toMove) parent.insertBefore(node, before);
597
- }
598
-
599
- //#endregion
600
- //#region src/delegate.ts
601
- /**
602
- * Events that are delegated (common bubbling events).
603
- * Non-bubbling events (focus, blur, mouseenter, mouseleave, load, error, scroll)
604
- * are NOT delegated — they must use addEventListener.
605
- */
606
- const DELEGATED_EVENTS = new Set([
607
- "click",
608
- "dblclick",
609
- "contextmenu",
610
- "focusin",
611
- "focusout",
612
- "input",
613
- "change",
614
- "keydown",
615
- "keyup",
616
- "mousedown",
617
- "mouseup",
618
- "mousemove",
619
- "mouseover",
620
- "mouseout",
621
- "pointerdown",
622
- "pointerup",
623
- "pointermove",
624
- "pointerover",
625
- "pointerout",
626
- "touchstart",
627
- "touchend",
628
- "touchmove",
629
- "submit"
630
- ]);
631
- /**
632
- * Property name used on DOM elements to store delegated event handlers.
633
- * Format: `__ev_{eventName}` e.g. `__ev_click`, `__ev_input`
634
- */
635
- function delegatedPropName(eventName) {
636
- return `__ev_${eventName}`;
637
- }
638
-
639
- //#endregion
640
- //#region src/props.ts
641
- const __DEV__$1 = process.env.NODE_ENV !== "production";
642
- const _countSink$1 = globalThis;
643
- let _customSanitizer = null;
644
- const SAFE_TAGS = new Set([
645
- "a",
646
- "abbr",
647
- "address",
648
- "article",
649
- "aside",
650
- "b",
651
- "bdi",
652
- "bdo",
653
- "blockquote",
654
- "br",
655
- "caption",
656
- "cite",
657
- "code",
658
- "col",
659
- "colgroup",
660
- "dd",
661
- "del",
662
- "details",
663
- "dfn",
664
- "div",
665
- "dl",
666
- "dt",
667
- "em",
668
- "figcaption",
669
- "figure",
670
- "footer",
671
- "h1",
672
- "h2",
673
- "h3",
674
- "h4",
675
- "h5",
676
- "h6",
677
- "header",
678
- "hr",
679
- "i",
680
- "ins",
681
- "kbd",
682
- "li",
683
- "main",
684
- "mark",
685
- "nav",
686
- "ol",
687
- "p",
688
- "pre",
689
- "q",
690
- "rp",
691
- "rt",
692
- "ruby",
693
- "s",
694
- "samp",
695
- "section",
696
- "small",
697
- "span",
698
- "strong",
699
- "sub",
700
- "summary",
701
- "sup",
702
- "table",
703
- "tbody",
704
- "td",
705
- "tfoot",
706
- "th",
707
- "thead",
708
- "time",
709
- "tr",
710
- "u",
711
- "ul",
712
- "var",
713
- "wbr"
714
- ]);
715
- const UNSAFE_ATTR_RE = /^on/i;
716
- /**
717
- * Fallback tag-stripping sanitizer for environments without the Sanitizer API.
718
- * Removes all tags not in SAFE_TAGS, strips event handler attributes,
719
- * and blocks javascript:/data: URLs in href/src/action attributes.
720
- */
721
- function fallbackSanitize(html) {
722
- const doc = new DOMParser().parseFromString(html, "text/html");
723
- sanitizeNode(doc.body);
724
- return doc.body.innerHTML;
725
- }
726
- /** Strip unsafe attributes from a single element. */
727
- function stripUnsafeAttrs(el) {
728
- const attrs = Array.from(el.attributes);
729
- for (const attr of attrs) if (UNSAFE_ATTR_RE.test(attr.name)) el.removeAttribute(attr.name);
730
- else if (URL_ATTRS.has(attr.name) && UNSAFE_URL_RE.test(attr.value)) el.removeAttribute(attr.name);
731
- }
732
- function sanitizeNode(node) {
733
- const children = Array.from(node.childNodes);
734
- for (const child of children) {
735
- if (child.nodeType !== 1) continue;
736
- const el = child;
737
- const tag = el.tagName.toLowerCase();
738
- if (!SAFE_TAGS.has(tag)) {
739
- const text = document.createTextNode(el.textContent);
740
- node.replaceChild(text, el);
741
- continue;
742
- }
743
- stripUnsafeAttrs(el);
744
- sanitizeNode(el);
745
- }
746
- }
747
- /**
748
- * Sanitize an HTML string using the browser Sanitizer API (Chrome 105+).
749
- * Falls back to a tag-allowlist sanitizer that strips unsafe elements and attributes.
750
- */
751
- function sanitizeHtml(html) {
752
- if (_customSanitizer) return _customSanitizer(html);
753
- return fallbackSanitize(html);
754
- }
755
- const EVENT_RE = /^on[A-Z]/;
756
- /**
757
- * Apply all props to a DOM element.
758
- * Returns a single chained cleanup (or null if no props need teardown).
759
- * Uses for-in instead of Object.keys() to avoid allocating a keys array.
760
- */
761
- function applyProps(el, props) {
762
- let first = null;
763
- let cleanups = null;
764
- for (const key in props) {
765
- if (key === "key" || key === "ref" || key === "children") continue;
766
- const descriptor = Object.getOwnPropertyDescriptor(props, key);
767
- let c;
768
- if (descriptor?.get) c = renderEffect(() => applyStaticProp(el, key, props[key]));
769
- else c = applyProp(el, key, props[key]);
770
- if (c) if (!first) first = c;
771
- else if (!cleanups) cleanups = [first, c];
772
- else cleanups.push(c);
773
- }
774
- if (cleanups) return () => {
775
- for (const c of cleanups) c();
776
- };
777
- return first;
778
- }
779
- /**
780
- * Apply a single prop.
781
- *
782
- * - `onXxx` → addEventListener
783
- * - `() => value` (non-event function) → reactive via effect
784
- * - anything else → static attribute / DOM property
785
- */
786
- /**
787
- * Bind an event handler (onClick → "click") with batching + delegation support.
788
- */
789
- function applyEventProp(el, key, value) {
790
- if (__DEV__$1) _countSink$1.__pyreon_count__?.("runtime.applyEvent");
791
- if (typeof value !== "function") {
792
- if (__DEV__$1 && value != null) console.warn(`[Pyreon] Event handler "${key}" received a non-function value (${typeof value}). Expected a function. Did you mean ${key}={() => ...}?`);
793
- return null;
794
- }
795
- const eventName = (key[2]?.toLowerCase() + key.slice(3)).toLowerCase();
796
- const handler = value;
797
- if (DELEGATED_EVENTS.has(eventName)) {
798
- const prop = delegatedPropName(eventName);
799
- el[prop] = (e) => batch(() => handler(e));
800
- return () => {
801
- el[prop] = void 0;
802
- };
803
- }
804
- const batched = (e) => batch(() => handler(e));
805
- el.addEventListener(eventName, batched);
806
- return () => el.removeEventListener(eventName, batched);
807
- }
808
- /**
809
- * Sink for a single prop's CALLED value (always a primitive / object /
810
- * `null` — never a function). Called both directly for static values and
811
- * from the reactive `renderEffect` for accessor-bound values.
812
- *
813
- * NOTE on architecture: extracting the special-cased sinks
814
- * (`innerHTML` / `dangerouslySetInnerHTML`) into this single dispatch
815
- * function ensures every prop kind goes through the same reactive
816
- * wrapping at `applyProp`'s entry. Previously each special case had its
817
- * own early-return branch that needed to remember to handle function
818
- * values; missing the dance once meant the closure was stringified and
819
- * set as literal text. The structural fix (one reactive-wrap, then
820
- * dispatch) eliminates the entire bug class.
821
- */
822
- function applyStaticProp(el, key, value) {
823
- if (__DEV__$1 && typeof value === "function") console.warn(`[Pyreon] applyStaticProp received a function for "${key}". This likely means a new special-cased prop sink in applyProp() bypassed the reactive-wrap path. The closure would be stringified and set as a literal value. Verify the dispatch in applyProp().`);
824
- if (key === "innerHTML") {
825
- const html = String(value ?? "");
826
- if (typeof el.setHTML === "function") el.setHTML(html);
827
- else el.innerHTML = sanitizeHtml(html);
828
- return;
829
- }
830
- if (key === "dangerouslySetInnerHTML") {
831
- el.innerHTML = value?.__html ?? "";
832
- return;
833
- }
834
- setStaticProp(el, key, value);
835
- }
836
- function applyProp(el, key, value) {
837
- if (__DEV__$1) _countSink$1.__pyreon_count__?.("runtime.applyProp");
838
- if (EVENT_RE.test(key)) return applyEventProp(el, key, value);
839
- if (typeof value === "function") return renderEffect(() => applyStaticProp(el, key, value()));
840
- applyStaticProp(el, key, value);
841
- return null;
842
- }
843
- const URL_ATTRS = new Set([
844
- "href",
845
- "src",
846
- "action",
847
- "formaction",
848
- "poster",
849
- "cite",
850
- "data"
851
- ]);
852
- const UNSAFE_URL_RE = /^\s*(?:javascript|data):/i;
853
- const _prevStyleKeys = /* @__PURE__ */ new WeakMap();
854
- /** Apply a style prop (string or object). */
855
- function applyStyleProp(el, value) {
856
- if (typeof value === "string") {
857
- el.style.cssText = value;
858
- _prevStyleKeys.delete(el);
859
- return;
860
- }
861
- const prev = _prevStyleKeys.get(el);
862
- if (value == null) {
863
- if (prev) {
864
- for (const propName of prev) el.style.removeProperty(propName);
865
- _prevStyleKeys.delete(el);
866
- }
867
- return;
868
- }
869
- if (typeof value === "object") {
870
- const obj = value;
871
- const next = /* @__PURE__ */ new Set();
872
- for (const k in obj) {
873
- const propName = k.startsWith("--") ? k : toKebabCase(k);
874
- next.add(propName);
875
- const css = normalizeStyleValue(k, obj[k]);
876
- el.style.setProperty(propName, css);
877
- }
878
- if (prev) {
879
- for (const propName of prev) if (!next.has(propName)) el.style.removeProperty(propName);
880
- }
881
- if (next.size === 0) _prevStyleKeys.delete(el);
882
- else _prevStyleKeys.set(el, next);
883
- }
884
- }
885
- function applyClassProp(el, value) {
886
- const resolved = typeof value === "string" ? value : cx(value);
887
- el.setAttribute("class", resolved || "");
888
- }
889
- function setStaticProp(el, key, value) {
890
- if (URL_ATTRS.has(key) && typeof value === "string" && UNSAFE_URL_RE.test(value)) {
891
- if (__DEV__$1) console.warn(`[Pyreon] Blocked unsafe URL in "${key}" attribute: ${value}`);
892
- return;
893
- }
894
- if (key === "class" || key === "className") {
895
- applyClassProp(el, value);
896
- return;
897
- }
898
- if (key === "style") {
899
- applyStyleProp(el, value);
900
- return;
901
- }
902
- if (value == null) {
903
- el.removeAttribute(key);
904
- return;
905
- }
906
- if (typeof value === "boolean") {
907
- if (value) el.setAttribute(key, "");
908
- else el.removeAttribute(key);
909
- return;
910
- }
911
- if (el.namespaceURI && el.namespaceURI !== "http://www.w3.org/1999/xhtml") {
912
- el.setAttribute(key, String(value));
913
- return;
914
- }
915
- if (key in el) {
916
- el[key] = value;
917
- return;
918
- }
919
- if (el.tagName.includes("-")) {
920
- el[key] = value;
921
- return;
922
- }
923
- el.setAttribute(key, String(value));
924
- }
925
-
926
- //#endregion
927
- //#region src/mount.ts
928
- const __DEV__ = process.env.NODE_ENV !== "production";
929
- const _countSink = globalThis;
930
- const noop = () => {};
931
- let _elementDepth = 0;
932
- let _mountingStack;
933
- if (__DEV__) _mountingStack = [];
934
- /**
935
- * Mount a single child into `parent`, inserting before `anchor` (null = append).
936
- * Returns a cleanup that removes the node(s) and disposes all reactive effects.
937
- *
938
- * This function is the hot path — all child types are handled inline to avoid
939
- * function call overhead in tight render loops (1000+ calls per list render).
940
- */
941
- function mountChild(child, parent, anchor = null) {
942
- if (__DEV__) _countSink.__pyreon_count__?.("runtime.mountChild");
943
- if (typeof child === "function") {
944
- const sample = runUntracked(() => child());
945
- if (isKeyedArray(sample)) {
946
- const prevDepth = _elementDepth;
947
- _elementDepth = 0;
948
- const cleanup = mountKeyedList(child, parent, anchor, (v, p, a) => mountChild(v, p, a));
949
- _elementDepth = prevDepth;
950
- return cleanup;
951
- }
952
- if (typeof sample === "string" || typeof sample === "number" || typeof sample === "boolean") {
953
- const text = document.createTextNode(sample === false ? "" : String(sample));
954
- parent.insertBefore(text, anchor);
955
- const dispose = renderEffect(() => {
956
- const v = child();
957
- const next = v == null || v === false ? "" : String(v);
958
- if (next !== text.data) text.data = next;
959
- });
960
- if (_elementDepth > 0) return dispose;
961
- return () => {
962
- dispose();
963
- const p = text.parentNode;
964
- if (p && p.isConnected !== false) p.removeChild(text);
965
- };
966
- }
967
- const prevDepth = _elementDepth;
968
- _elementDepth = 0;
969
- const cleanup = mountReactive(child, parent, anchor, mountChild);
970
- _elementDepth = prevDepth;
971
- return cleanup;
972
- }
973
- if (Array.isArray(child)) return mountChildren(child, parent, anchor);
974
- if (child == null || child === false) return noop;
975
- if (typeof child !== "object") {
976
- parent.insertBefore(document.createTextNode(String(child)), anchor);
977
- return noop;
978
- }
979
- if (child.__isNative) {
980
- const native = child;
981
- parent.insertBefore(native.el, anchor);
982
- if (!native.cleanup) {
983
- if (_elementDepth > 0) return noop;
984
- return () => {
985
- const p = native.el.parentNode;
986
- if (p && p.isConnected !== false) p.removeChild(native.el);
987
- };
988
- }
989
- if (_elementDepth > 0) return native.cleanup;
990
- return () => {
991
- native.cleanup?.();
992
- const p = native.el.parentNode;
993
- if (p && p.isConnected !== false) p.removeChild(native.el);
994
- };
995
- }
996
- const vnode = child;
997
- if (vnode.type === Fragment) return mountChildren(vnode.children ?? [], parent, anchor);
998
- if (vnode.type === ForSymbol) {
999
- const props = vnode.props;
1000
- const initialEach = props.each;
1001
- const source = typeof initialEach === "function" ? initialEach : (() => props.each);
1002
- const prevDepth = _elementDepth;
1003
- _elementDepth = 0;
1004
- const cleanup = mountFor(source, props.by, props.children, parent, anchor, mountChild);
1005
- _elementDepth = prevDepth;
1006
- return cleanup;
1007
- }
1008
- if (vnode.type === PortalSymbol) {
1009
- const { target, children } = vnode.props;
1010
- if (__DEV__ && !target) {
1011
- console.warn("[Pyreon] <Portal> received a falsy `target`. Provide a valid DOM element.");
1012
- return noop;
1013
- }
1014
- if (__DEV__ && !(target instanceof Node)) console.warn(`[Pyreon] <Portal> target must be a DOM node. Received ${typeof target}. Use document.getElementById() or a ref to get the target element.`);
1015
- return mountChild(children, target, null);
1016
- }
1017
- if (typeof vnode.type === "function") return mountComponent(vnode, parent, anchor);
1018
- if (__DEV__ && typeof vnode.type !== "string") {
1019
- console.warn(`[Pyreon] Invalid VNode type: expected a string tag or component function, received ${typeof vnode.type} (${String(vnode.type)}). This usually means you passed an object or class instead of a component function.`);
1020
- return noop;
1021
- }
1022
- return mountElement(vnode, parent, anchor);
1023
- }
1024
- const VOID_ELEMENTS = new Set([
1025
- "area",
1026
- "base",
1027
- "br",
1028
- "col",
1029
- "embed",
1030
- "hr",
1031
- "img",
1032
- "input",
1033
- "link",
1034
- "meta",
1035
- "param",
1036
- "source",
1037
- "track",
1038
- "wbr"
1039
- ]);
1040
- const SVG_NS = "http://www.w3.org/2000/svg";
1041
- const MATHML_NS = "http://www.w3.org/1998/Math/MathML";
1042
- const SVG_TAGS = new Set([
1043
- "svg",
1044
- "circle",
1045
- "ellipse",
1046
- "line",
1047
- "path",
1048
- "polygon",
1049
- "polyline",
1050
- "rect",
1051
- "g",
1052
- "defs",
1053
- "symbol",
1054
- "use",
1055
- "text",
1056
- "tspan",
1057
- "textPath",
1058
- "image",
1059
- "clipPath",
1060
- "mask",
1061
- "pattern",
1062
- "marker",
1063
- "linearGradient",
1064
- "radialGradient",
1065
- "stop",
1066
- "filter",
1067
- "feBlend",
1068
- "feColorMatrix",
1069
- "feComponentTransfer",
1070
- "feComposite",
1071
- "feConvolveMatrix",
1072
- "feDiffuseLighting",
1073
- "feDisplacementMap",
1074
- "feFlood",
1075
- "feGaussianBlur",
1076
- "feImage",
1077
- "feMerge",
1078
- "feMergeNode",
1079
- "feMorphology",
1080
- "feOffset",
1081
- "feSpecularLighting",
1082
- "feTile",
1083
- "feTurbulence",
1084
- "animate",
1085
- "animateMotion",
1086
- "animateTransform",
1087
- "set",
1088
- "desc",
1089
- "title",
1090
- "metadata",
1091
- "foreignObject"
1092
- ]);
1093
- const MATHML_TAGS = new Set([
1094
- "math",
1095
- "mi",
1096
- "mo",
1097
- "mn",
1098
- "ms",
1099
- "mtext",
1100
- "mspace",
1101
- "mrow",
1102
- "mfrac",
1103
- "msqrt",
1104
- "mroot",
1105
- "msub",
1106
- "msup",
1107
- "msubsup",
1108
- "munder",
1109
- "mover",
1110
- "munderover",
1111
- "mtable",
1112
- "mtr",
1113
- "mtd",
1114
- "mpadded",
1115
- "mphantom",
1116
- "menclose"
1117
- ]);
1118
- /** Track SVG context depth — children of <svg> inherit the SVG namespace. */
1119
- let _svgDepth = 0;
1120
- let _mathmlDepth = 0;
1121
- function createElementWithNS(tag) {
1122
- if (_svgDepth > 0 || SVG_TAGS.has(tag)) return document.createElementNS(SVG_NS, tag);
1123
- if (_mathmlDepth > 0 || MATHML_TAGS.has(tag)) return document.createElementNS(MATHML_NS, tag);
1124
- return document.createElement(tag);
1125
- }
1126
- function mountElement(vnode, parent, anchor) {
1127
- const tag = vnode.type;
1128
- const el = createElementWithNS(tag);
1129
- const isSvg = tag === "svg";
1130
- const isMathml = tag === "math";
1131
- if (isSvg) _svgDepth++;
1132
- if (isMathml) _mathmlDepth++;
1133
- if (__DEV__ && (vnode.children?.length ?? 0) > 0 && VOID_ELEMENTS.has(vnode.type)) console.warn(`[Pyreon] <${vnode.type}> is a void element and cannot have children. Children passed to void elements will be ignored by the browser.`);
1134
- const props = vnode.props;
1135
- const propCleanup = props !== EMPTY_PROPS ? applyProps(el, props) : null;
1136
- _elementDepth++;
1137
- const childCleanup = mountChildren(vnode.children ?? [], el, null);
1138
- _elementDepth--;
1139
- if (isSvg) _svgDepth--;
1140
- if (isMathml) _mathmlDepth--;
1141
- parent.insertBefore(el, anchor);
1142
- const ref = props.ref;
1143
- if (ref) if (typeof ref === "function") ref(el);
1144
- else ref.current = el;
1145
- if (!propCleanup && childCleanup === noop && !ref) {
1146
- if (_elementDepth > 0) return noop;
1147
- return () => {
1148
- const p = el.parentNode;
1149
- if (p && p.isConnected !== false) p.removeChild(el);
1150
- };
1151
- }
1152
- if (_elementDepth > 0) {
1153
- if (!ref && !propCleanup) return childCleanup;
1154
- if (!ref && propCleanup) return () => {
1155
- propCleanup();
1156
- childCleanup();
1157
- };
1158
- const refToClean = ref;
1159
- return () => {
1160
- if (refToClean) if (typeof refToClean === "function") refToClean(null);
1161
- else refToClean.current = null;
1162
- if (propCleanup) propCleanup();
1163
- childCleanup();
1164
- };
1165
- }
1166
- return () => {
1167
- if (ref) if (typeof ref === "function") ref(null);
1168
- else ref.current = null;
1169
- if (propCleanup) propCleanup();
1170
- childCleanup();
1171
- const p = el.parentNode;
1172
- if (p && p.isConnected !== false) p.removeChild(el);
1173
- };
1174
- }
1175
- function mountComponent(vnode, parent, anchor) {
1176
- const scope = effectScope();
1177
- setCurrentScope(scope);
1178
- let hooks;
1179
- let output;
1180
- const componentName = vnode.type.name || "Anonymous";
1181
- let compId;
1182
- let devParentId;
1183
- if (__DEV__) {
1184
- compId = `${componentName}-${Math.random().toString(36).slice(2, 9)}`;
1185
- devParentId = _mountingStack[_mountingStack.length - 1] ?? null;
1186
- _mountingStack.push(compId);
1187
- }
1188
- const children = vnode.children ?? [];
1189
- const rawProps = children.length > 0 && vnode.props.children === void 0 ? {
1190
- ...vnode.props,
1191
- children: children.length === 1 ? children[0] : children
1192
- } : vnode.props;
1193
- const mergedProps = rawProps === EMPTY_PROPS ? rawProps : makeReactiveProps(rawProps);
1194
- try {
1195
- const result = runWithHooks(vnode.type, mergedProps);
1196
- hooks = result.hooks;
1197
- output = result.vnode;
1198
- } catch (err) {
1199
- if (__DEV__) _mountingStack.pop();
1200
- setCurrentScope(null);
1201
- scope.stop();
1202
- reportError({
1203
- component: componentName,
1204
- phase: "setup",
1205
- error: err,
1206
- timestamp: Date.now(),
1207
- props: vnode.props
1208
- });
1209
- const handled = dispatchToErrorBoundary(err);
1210
- if (!handled) console.error(`[Pyreon] <${componentName}> threw during setup:`, err);
1211
- if (__DEV__ && !handled) {
1212
- const overlay = document.createElement("pre");
1213
- overlay.style.cssText = "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";
1214
- const e = err;
1215
- overlay.textContent = `[${componentName}] ${e.message ?? err}\n${e.stack ?? ""}`;
1216
- parent.insertBefore(overlay, anchor);
1217
- return () => overlay.remove();
1218
- }
1219
- return noop;
1220
- } finally {
1221
- setCurrentScope(null);
1222
- }
1223
- if (__DEV__ && output != null && typeof output === "object") {
1224
- if (output instanceof Promise) console.warn(`[Pyreon] Component <${componentName}> returned a Promise. Components must be synchronous — use lazy() + Suspense for async loading, or fetch data in onMount and store it in a signal.`);
1225
- else if (!("type" in output) && !Array.isArray(output) && !output.__isNative) console.warn(`[Pyreon] Component <${componentName}> returned an invalid value. Components must return a VNode, string, null, function, or array.`);
1226
- }
1227
- if (hooks.update) for (const fn of hooks.update) scope.addUpdateHook(fn);
1228
- let subtreeCleanup = noop;
1229
- try {
1230
- subtreeCleanup = output != null ? mountChild(output, parent, anchor) : noop;
1231
- } catch (err) {
1232
- if (__DEV__) _mountingStack.pop();
1233
- scope.stop();
1234
- if (!(propagateError(err, hooks) || dispatchToErrorBoundary(err))) {
1235
- reportError({
1236
- component: componentName,
1237
- phase: "render",
1238
- error: err,
1239
- timestamp: Date.now(),
1240
- props: vnode.props
1241
- });
1242
- console.error(`[Pyreon] <${componentName}> threw during render:`, err);
1243
- }
1244
- return noop;
1245
- }
1246
- if (__DEV__) {
1247
- _mountingStack.pop();
1248
- const firstEl = parent instanceof Element ? parent.firstElementChild : null;
1249
- registerComponent(compId, componentName, firstEl, devParentId);
1250
- }
1251
- let mountCleanups = null;
1252
- if (hooks.mount) for (const fn of hooks.mount) try {
1253
- let cleanup;
1254
- scope.runInScope(() => {
1255
- cleanup = fn();
1256
- });
1257
- if (cleanup) {
1258
- if (mountCleanups === null) mountCleanups = [];
1259
- mountCleanups.push(cleanup);
1260
- }
1261
- } catch (err) {
1262
- console.error(`[Pyreon] Error in onMount hook of <${componentName}>:`, err);
1263
- reportError({
1264
- component: componentName,
1265
- phase: "mount",
1266
- error: err,
1267
- timestamp: Date.now()
1268
- });
1269
- }
1270
- return () => {
1271
- if (__DEV__) unregisterComponent(compId);
1272
- scope.stop();
1273
- subtreeCleanup();
1274
- if (hooks.unmount) for (const fn of hooks.unmount) try {
1275
- fn();
1276
- } catch (err) {
1277
- console.error(`[Pyreon] Error in onUnmount hook of <${componentName}>:`, err);
1278
- reportError({
1279
- component: componentName,
1280
- phase: "unmount",
1281
- error: err,
1282
- timestamp: Date.now()
1283
- });
1284
- }
1285
- if (mountCleanups) for (const fn of mountCleanups) fn();
1286
- };
1287
- }
1288
- function mountChildren(children, parent, anchor) {
1289
- if (children.length === 0) return noop;
1290
- if (children.length === 1) {
1291
- const c = children[0];
1292
- if (c !== void 0) {
1293
- if (anchor === null && (typeof c === "string" || typeof c === "number")) {
1294
- parent.textContent = String(c);
1295
- return noop;
1296
- }
1297
- return mountChild(c, parent, anchor);
1298
- }
1299
- }
1300
- if (children.length === 2) {
1301
- const c0 = children[0];
1302
- const c1 = children[1];
1303
- if (c0 !== void 0 && c1 !== void 0) {
1304
- const d0 = mountChild(c0, parent, anchor);
1305
- const d1 = mountChild(c1, parent, anchor);
1306
- if (d0 === noop && d1 === noop) return noop;
1307
- if (d0 === noop) return d1;
1308
- if (d1 === noop) return d0;
1309
- return () => {
1310
- d0();
1311
- d1();
1312
- };
1313
- }
1314
- }
1315
- const cleanups = children.map((c) => mountChild(c, parent, anchor));
1316
- return () => {
1317
- for (const fn of cleanups) fn();
1318
- };
1319
- }
1320
- /** Returns true if value is a non-empty array of VNodes that all carry keys. */
1321
- function isKeyedArray(value) {
1322
- if (!Array.isArray(value) || value.length === 0) return false;
1323
- return value.every((v) => v !== null && typeof v === "object" && !Array.isArray(v) && v.key !== null && v.key !== void 0);
1324
- }
1325
-
1326
- //#endregion
1327
- //#region src/keep-alive.ts
1328
- /**
1329
- * KeepAlive — mounts its children once and keeps them alive even when hidden.
1330
- *
1331
- * Unlike conditional rendering (which destroys and recreates component state),
1332
- * KeepAlive CSS-hides the children while preserving all reactive state,
1333
- * scroll position, form values, and in-flight async operations.
1334
- *
1335
- * Children are mounted imperatively on first activation and are never unmounted
1336
- * while the KeepAlive itself is mounted.
1337
- *
1338
- * Multi-slot pattern (one KeepAlive per route):
1339
- * @example
1340
- * h(Fragment, null, [
1341
- * h(KeepAlive, { active: () => route() === "/a" }, h(RouteA, null)),
1342
- * h(KeepAlive, { active: () => route() === "/b" }, h(RouteB, null)),
1343
- * ])
1344
- *
1345
- * With JSX:
1346
- * @example
1347
- * <>
1348
- * <KeepAlive active={() => route() === "/a"}><RouteA /></KeepAlive>
1349
- * <KeepAlive active={() => route() === "/b"}><RouteB /></KeepAlive>
1350
- * </>
1351
- */
1352
- function KeepAlive(props) {
1353
- const containerRef = createRef();
1354
- let childCleanup = null;
1355
- let childMounted = false;
1356
- onMount(() => {
1357
- const container = containerRef.current;
1358
- if (!container) return;
1359
- const e = effect(() => {
1360
- const isActive = props.active?.() ?? true;
1361
- if (!childMounted) {
1362
- childCleanup = runUntracked(() => mountChild(props.children ?? null, container, null));
1363
- childMounted = true;
1364
- }
1365
- container.style.display = isActive ? "" : "none";
1366
- });
1367
- return () => {
1368
- e.dispose();
1369
- childCleanup?.();
1370
- };
1371
- });
1372
- return h("div", {
1373
- ref: containerRef,
1374
- style: "display: contents"
1375
- });
1376
- }
1377
- nativeCompat(KeepAlive);
1378
-
1379
- //#endregion
1380
- export { KeepAlive };
1381
- //# sourceMappingURL=keep-alive-entry.js.map
3
+ export { KeepAlive };