@pyreon/runtime-dom 0.13.1 → 0.15.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.
package/lib/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { batch, effect, effectScope, renderEffect, runUntracked, setCurrentScope, signal } from "@pyreon/reactivity";
2
- import { EMPTY_PROPS, ForSymbol, Fragment, PortalSymbol, captureContextStack, createRef, cx, dispatchToErrorBoundary, h, makeReactiveProps, normalizeStyleValue, onMount, onUnmount, propagateError, reportError, restoreContextStack, runWithHooks, toKebabCase } from "@pyreon/core";
2
+ import { EMPTY_PROPS, ForSymbol, Fragment, PortalSymbol, captureContextStack, createRef, cx, dispatchToErrorBoundary, h, makeReactiveProps, nativeCompat, normalizeStyleValue, onMount, onUnmount, propagateError, reportError, restoreContextStack, runWithHooks, toKebabCase } from "@pyreon/core";
3
3
 
4
4
  //#region src/delegate.ts
5
5
  /**
@@ -66,6 +66,10 @@ function setupDelegation(container) {
66
66
  while (el && el !== container) {
67
67
  const handler = el[prop];
68
68
  if (typeof handler === "function") {
69
+ Object.defineProperty(e, "currentTarget", {
70
+ value: el,
71
+ configurable: true
72
+ });
69
73
  batch(() => handler(e));
70
74
  if (e.cancelBubble) break;
71
75
  }
@@ -77,13 +81,29 @@ function setupDelegation(container) {
77
81
 
78
82
  //#endregion
79
83
  //#region src/hydration-debug.ts
80
- let _enabled = import.meta.env?.DEV === true;
84
+ let _enabled = process.env.NODE_ENV !== "production";
81
85
  function enableHydrationWarnings() {
82
86
  _enabled = true;
83
87
  }
84
88
  function disableHydrationWarnings() {
85
89
  _enabled = false;
86
90
  }
91
+ let _handlers = [];
92
+ /**
93
+ * Register a hydration mismatch handler. Called on every mismatch in BOTH
94
+ * development and production, independent of the dev-mode warn toggle.
95
+ *
96
+ * Mirrors `@pyreon/core`'s `registerErrorHandler` pattern — multiple
97
+ * handlers can be registered; each is called in registration order;
98
+ * handler errors are swallowed so they don't propagate into the
99
+ * framework. Returns an unregister function.
100
+ */
101
+ function onHydrationMismatch(handler) {
102
+ _handlers.push(handler);
103
+ return () => {
104
+ _handlers = _handlers.filter((h) => h !== handler);
105
+ };
106
+ }
87
107
  /**
88
108
  * Emit a hydration mismatch warning.
89
109
  * @param type - Kind of mismatch
@@ -91,9 +111,20 @@ function disableHydrationWarnings() {
91
111
  * @param actual - What the DOM had
92
112
  * @param path - Human-readable path in the tree, e.g. "root > div > span"
93
113
  */
94
- function warnHydrationMismatch(_type, _expected, _actual, _path) {
95
- if (!_enabled) return;
96
- console.warn(`[Pyreon] Hydration mismatch (${_type}): expected ${String(_expected)}, got ${String(_actual)} at ${_path}`);
114
+ function warnHydrationMismatch(type, expected, actual, path) {
115
+ if (_enabled) console.warn(`[Pyreon] Hydration mismatch (${type}): expected ${String(expected)}, got ${String(actual)} at ${path}`);
116
+ if (_handlers.length > 0) {
117
+ const ctx = {
118
+ type,
119
+ expected,
120
+ actual,
121
+ path,
122
+ timestamp: Date.now()
123
+ };
124
+ for (const h of _handlers) try {
125
+ h(ctx);
126
+ } catch {}
127
+ }
97
128
  }
98
129
 
99
130
  //#endregion
@@ -279,13 +310,18 @@ function installDevTools() {
279
310
  });
280
311
  const win = window;
281
312
  win.$p = {
313
+ /** List all mounted components */
282
314
  components: () => devtools.getAllComponents(),
315
+ /** Component tree (roots only) */
283
316
  tree: () => devtools.getComponentTree(),
317
+ /** Highlight a component by id */
284
318
  highlight: (id) => devtools.highlight(id),
319
+ /** Toggle component inspector overlay */
285
320
  inspect: () => {
286
321
  if (_overlayActive) disableOverlay();
287
322
  else enableOverlay();
288
323
  },
324
+ /** Print component count */
289
325
  stats: () => {
290
326
  const all = devtools.getAllComponents();
291
327
  const roots = devtools.getComponentTree();
@@ -295,6 +331,7 @@ function installDevTools() {
295
331
  roots: roots.length
296
332
  };
297
333
  },
334
+ /** Quick help */
298
335
  help: () => {
299
336
  console.log("[Pyreon] $p commands:\n $p.components() — list all mounted components\n $p.tree() — component tree (roots only)\n $p.highlight(id)— outline a component\n $p.inspect() — toggle component inspector\n $p.stats() — print component count\n $p.help() — this message");
300
337
  }
@@ -303,7 +340,8 @@ function installDevTools() {
303
340
 
304
341
  //#endregion
305
342
  //#region src/nodes.ts
306
- const __DEV__$4 = import.meta.env?.DEV === true;
343
+ const __DEV__$5 = process.env.NODE_ENV !== "production";
344
+ const _countSink$3 = globalThis;
307
345
  /**
308
346
  * Move all nodes strictly between `start` and `end` into a throwaway
309
347
  * DocumentFragment, detaching them from the live DOM in O(n) top-level moves.
@@ -367,6 +405,7 @@ function growLisArrays(lis, n) {
367
405
  function computeKeyedLis(lis, n, newKeyOrder, curPos) {
368
406
  const { tails, tailIdx, pred } = lis;
369
407
  let lisLen = 0;
408
+ let ops = 0;
370
409
  for (let i = 0; i < n; i++) {
371
410
  const key = newKeyOrder[i];
372
411
  if (key === void 0) continue;
@@ -376,6 +415,7 @@ function computeKeyedLis(lis, n, newKeyOrder, curPos) {
376
415
  let hi = lisLen;
377
416
  while (lo < hi) {
378
417
  const mid = lo + hi >> 1;
418
+ ops++;
379
419
  if (tails[mid] < v) lo = mid + 1;
380
420
  else hi = mid;
381
421
  }
@@ -384,6 +424,7 @@ function computeKeyedLis(lis, n, newKeyOrder, curPos) {
384
424
  if (lo > 0) pred[i] = tailIdx[lo - 1];
385
425
  if (lo === lisLen) lisLen++;
386
426
  }
427
+ if (__DEV__$5 && ops > 0) _countSink$3.__pyreon_count__?.("runtime.mountFor.lisOps", ops);
387
428
  return lisLen;
388
429
  }
389
430
  function markStayingEntries(lis, lisLen) {
@@ -521,21 +562,36 @@ function trySmallKReorder(n, newKeys, currentKeys, cache, liveParent, tailMarker
521
562
  function computeForLis(lis, n, newKeys, cache) {
522
563
  const { tails, tailIdx, pred } = lis;
523
564
  let lisLen = 0;
565
+ let ops = 0;
566
+ let lastV = -1;
524
567
  for (let i = 0; i < n; i++) {
525
568
  const key = newKeys[i];
526
569
  const v = cache.get(key)?.pos ?? 0;
527
- let lo = 0;
528
- let hi = lisLen;
529
- while (lo < hi) {
530
- const mid = lo + hi >> 1;
531
- if (tails[mid] < v) lo = mid + 1;
532
- else hi = mid;
570
+ if (v > lastV) {
571
+ tails[lisLen] = v;
572
+ tailIdx[lisLen] = i;
573
+ if (lisLen > 0) pred[i] = tailIdx[lisLen - 1];
574
+ lisLen++;
575
+ lastV = v;
576
+ continue;
577
+ }
578
+ let lo;
579
+ if (v < lisLen && tails[v] === v) lo = v;
580
+ else {
581
+ lo = 0;
582
+ let hi = lisLen;
583
+ while (lo < hi) {
584
+ const mid = lo + hi >> 1;
585
+ ops++;
586
+ if (tails[mid] < v) lo = mid + 1;
587
+ else hi = mid;
588
+ }
533
589
  }
534
590
  tails[lo] = v;
535
591
  tailIdx[lo] = i;
536
592
  if (lo > 0) pred[i] = tailIdx[lo - 1];
537
- if (lo === lisLen) lisLen++;
538
593
  }
594
+ if (__DEV__$5 && ops > 0) _countSink$3.__pyreon_count__?.("runtime.mountFor.lisOps", ops);
539
595
  return lisLen;
540
596
  }
541
597
  function applyForMoves(n, newKeys, stay, cache, liveParent, tailMarker) {
@@ -576,6 +632,7 @@ function mountFor(source, getKey, renderItem, parent, anchor, mountChild) {
576
632
  parent.insertBefore(tailMarker, anchor);
577
633
  let cache = /* @__PURE__ */ new Map();
578
634
  let currentKeys = [];
635
+ const _reusableKeySet = /* @__PURE__ */ new Set();
579
636
  let cleanupCount = 0;
580
637
  let anchorsRegistered = false;
581
638
  let lis = {
@@ -586,9 +643,9 @@ function mountFor(source, getKey, renderItem, parent, anchor, mountChild) {
586
643
  };
587
644
  const warnForKey = (seen, key) => {
588
645
  if (!seen) return;
589
- if (__DEV__$4 && key == null) console.warn("[Pyreon] <For> `by` function returned null/undefined. Keys must be strings or numbers. Check your `by` prop.");
646
+ if (__DEV__$5 && key == null) console.warn("[Pyreon] <For> `by` function returned null/undefined. Keys must be strings or numbers. Check your `by` prop.");
590
647
  if (seen.has(key)) {
591
- if (__DEV__$4) console.warn(`[Pyreon] Duplicate key "${String(key)}" in <For> list. Keys must be unique.`);
648
+ if (__DEV__$5) console.warn(`[Pyreon] Duplicate key "${String(key)}" in <For> list. Keys must be unique.`);
592
649
  return true;
593
650
  }
594
651
  seen.add(key);
@@ -714,7 +771,9 @@ function mountFor(source, getKey, renderItem, parent, anchor, mountChild) {
714
771
  return false;
715
772
  };
716
773
  const handleIncrementalUpdate = (items, n, newKeys, liveParent) => {
717
- removeStaleForEntries(new Set(newKeys));
774
+ _reusableKeySet.clear();
775
+ for (let i = 0; i < newKeys.length; i++) _reusableKeySet.add(newKeys[i]);
776
+ removeStaleForEntries(_reusableKeySet);
718
777
  mountNewForEntries(items, n, newKeys, liveParent);
719
778
  if (!anchorsRegistered) {
720
779
  for (const entry of cache.values()) _forAnchors.add(entry.anchor);
@@ -813,7 +872,7 @@ function moveEntryBefore(parent, startNode, before) {
813
872
 
814
873
  //#endregion
815
874
  //#region src/props.ts
816
- const __DEV__$3 = import.meta.env?.DEV === true;
875
+ const __DEV__$4 = process.env.NODE_ENV !== "production";
817
876
  let _customSanitizer = null;
818
877
  /**
819
878
  * Set a custom HTML sanitizer used by `innerHTML` and `sanitizeHtml()`.
@@ -978,7 +1037,7 @@ function applyProps(el, props) {
978
1037
  */
979
1038
  function applyEventProp(el, key, value) {
980
1039
  if (typeof value !== "function") {
981
- if (__DEV__$3 && value != null) console.warn(`[Pyreon] Event handler "${key}" received a non-function value (${typeof value}). Expected a function. Did you mean ${key}={() => ...}?`);
1040
+ if (__DEV__$4 && value != null) console.warn(`[Pyreon] Event handler "${key}" received a non-function value (${typeof value}). Expected a function. Did you mean ${key}={() => ...}?`);
982
1041
  return null;
983
1042
  }
984
1043
  const eventName = (key[2]?.toLowerCase() + key.slice(3)).toLowerCase();
@@ -1009,7 +1068,7 @@ function applyEventProp(el, key, value) {
1009
1068
  * dispatch) eliminates the entire bug class.
1010
1069
  */
1011
1070
  function applyStaticProp(el, key, value) {
1012
- if (__DEV__$3 && 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().`);
1071
+ if (__DEV__$4 && 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().`);
1013
1072
  if (key === "innerHTML") {
1014
1073
  const html = String(value ?? "");
1015
1074
  if (typeof el.setHTML === "function") el.setHTML(html);
@@ -1076,7 +1135,7 @@ function applyClassProp(el, value) {
1076
1135
  }
1077
1136
  function setStaticProp(el, key, value) {
1078
1137
  if (URL_ATTRS.has(key) && typeof value === "string" && UNSAFE_URL_RE.test(value)) {
1079
- if (__DEV__$3) console.warn(`[Pyreon] Blocked unsafe URL in "${key}" attribute: ${value}`);
1138
+ if (__DEV__$4) console.warn(`[Pyreon] Blocked unsafe URL in "${key}" attribute: ${value}`);
1080
1139
  return;
1081
1140
  }
1082
1141
  if (key === "class" || key === "className") {
@@ -1113,10 +1172,12 @@ function setStaticProp(el, key, value) {
1113
1172
 
1114
1173
  //#endregion
1115
1174
  //#region src/mount.ts
1116
- const __DEV__$2 = import.meta.env?.DEV === true;
1175
+ const __DEV__$3 = process.env.NODE_ENV !== "production";
1176
+ const _countSink$2 = globalThis;
1117
1177
  const noop$1 = () => {};
1118
1178
  let _elementDepth = 0;
1119
- const _mountingStack = [];
1179
+ let _mountingStack;
1180
+ if (__DEV__$3) _mountingStack = [];
1120
1181
  /**
1121
1182
  * Mount a single child into `parent`, inserting before `anchor` (null = append).
1122
1183
  * Returns a cleanup that removes the node(s) and disposes all reactive effects.
@@ -1125,6 +1186,7 @@ const _mountingStack = [];
1125
1186
  * function call overhead in tight render loops (1000+ calls per list render).
1126
1187
  */
1127
1188
  function mountChild(child, parent, anchor = null) {
1189
+ if (__DEV__$3) _countSink$2.__pyreon_count__?.("runtime.mountChild");
1128
1190
  if (typeof child === "function") {
1129
1191
  const sample = runUntracked(() => child());
1130
1192
  if (isKeyedArray(sample)) {
@@ -1139,7 +1201,8 @@ function mountChild(child, parent, anchor = null) {
1139
1201
  parent.insertBefore(text, anchor);
1140
1202
  const dispose = renderEffect(() => {
1141
1203
  const v = child();
1142
- text.data = v == null || v === false ? "" : String(v);
1204
+ const next = v == null || v === false ? "" : String(v);
1205
+ if (next !== text.data) text.data = next;
1143
1206
  });
1144
1207
  if (_elementDepth > 0) return dispose;
1145
1208
  return () => {
@@ -1180,24 +1243,26 @@ function mountChild(child, parent, anchor = null) {
1180
1243
  const vnode = child;
1181
1244
  if (vnode.type === Fragment) return mountChildren(vnode.children ?? [], parent, anchor);
1182
1245
  if (vnode.type === ForSymbol) {
1183
- const { each, by, children } = vnode.props;
1246
+ const props = vnode.props;
1247
+ const initialEach = props.each;
1248
+ const source = typeof initialEach === "function" ? initialEach : (() => props.each);
1184
1249
  const prevDepth = _elementDepth;
1185
1250
  _elementDepth = 0;
1186
- const cleanup = mountFor(each, by, children, parent, anchor, mountChild);
1251
+ const cleanup = mountFor(source, props.by, props.children, parent, anchor, mountChild);
1187
1252
  _elementDepth = prevDepth;
1188
1253
  return cleanup;
1189
1254
  }
1190
1255
  if (vnode.type === PortalSymbol) {
1191
1256
  const { target, children } = vnode.props;
1192
- if (__DEV__$2 && !target) {
1257
+ if (__DEV__$3 && !target) {
1193
1258
  console.warn("[Pyreon] <Portal> received a falsy `target`. Provide a valid DOM element.");
1194
1259
  return noop$1;
1195
1260
  }
1196
- if (__DEV__$2 && !(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.`);
1261
+ if (__DEV__$3 && !(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.`);
1197
1262
  return mountChild(children, target, null);
1198
1263
  }
1199
1264
  if (typeof vnode.type === "function") return mountComponent(vnode, parent, anchor);
1200
- if (__DEV__$2 && typeof vnode.type !== "string") {
1265
+ if (__DEV__$3 && typeof vnode.type !== "string") {
1201
1266
  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.`);
1202
1267
  return noop$1;
1203
1268
  }
@@ -1312,7 +1377,7 @@ function mountElement(vnode, parent, anchor) {
1312
1377
  const isMathml = tag === "math";
1313
1378
  if (isSvg) _svgDepth++;
1314
1379
  if (isMathml) _mathmlDepth++;
1315
- if (__DEV__$2 && (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.`);
1380
+ if (__DEV__$3 && (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.`);
1316
1381
  const props = vnode.props;
1317
1382
  const propCleanup = props !== EMPTY_PROPS ? applyProps(el, props) : null;
1318
1383
  _elementDepth++;
@@ -1360,9 +1425,13 @@ function mountComponent(vnode, parent, anchor) {
1360
1425
  let hooks;
1361
1426
  let output;
1362
1427
  const componentName = vnode.type.name || "Anonymous";
1363
- const compId = `${componentName}-${Math.random().toString(36).slice(2, 9)}`;
1364
- const parentId = _mountingStack[_mountingStack.length - 1] ?? null;
1365
- _mountingStack.push(compId);
1428
+ let compId;
1429
+ let devParentId;
1430
+ if (__DEV__$3) {
1431
+ compId = `${componentName}-${Math.random().toString(36).slice(2, 9)}`;
1432
+ devParentId = _mountingStack[_mountingStack.length - 1] ?? null;
1433
+ _mountingStack.push(compId);
1434
+ }
1366
1435
  const children = vnode.children ?? [];
1367
1436
  const rawProps = children.length > 0 && vnode.props.children === void 0 ? {
1368
1437
  ...vnode.props,
@@ -1374,7 +1443,7 @@ function mountComponent(vnode, parent, anchor) {
1374
1443
  hooks = result.hooks;
1375
1444
  output = result.vnode;
1376
1445
  } catch (err) {
1377
- _mountingStack.pop();
1446
+ if (__DEV__$3) _mountingStack.pop();
1378
1447
  setCurrentScope(null);
1379
1448
  scope.stop();
1380
1449
  reportError({
@@ -1386,7 +1455,7 @@ function mountComponent(vnode, parent, anchor) {
1386
1455
  });
1387
1456
  const handled = dispatchToErrorBoundary(err);
1388
1457
  if (!handled) console.error(`[Pyreon] <${componentName}> threw during setup:`, err);
1389
- if (__DEV__$2 && !handled) {
1458
+ if (__DEV__$3 && !handled) {
1390
1459
  const overlay = document.createElement("pre");
1391
1460
  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";
1392
1461
  const e = err;
@@ -1398,16 +1467,16 @@ function mountComponent(vnode, parent, anchor) {
1398
1467
  } finally {
1399
1468
  setCurrentScope(null);
1400
1469
  }
1401
- if (__DEV__$2 && output != null && typeof output === "object") {
1470
+ if (__DEV__$3 && output != null && typeof output === "object") {
1402
1471
  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.`);
1403
1472
  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.`);
1404
1473
  }
1405
- for (const fn of hooks.update) scope.addUpdateHook(fn);
1474
+ if (hooks.update) for (const fn of hooks.update) scope.addUpdateHook(fn);
1406
1475
  let subtreeCleanup = noop$1;
1407
1476
  try {
1408
1477
  subtreeCleanup = output != null ? mountChild(output, parent, anchor) : noop$1;
1409
1478
  } catch (err) {
1410
- _mountingStack.pop();
1479
+ if (__DEV__$3) _mountingStack.pop();
1411
1480
  scope.stop();
1412
1481
  if (!(propagateError(err, hooks) || dispatchToErrorBoundary(err))) {
1413
1482
  reportError({
@@ -1421,15 +1490,21 @@ function mountComponent(vnode, parent, anchor) {
1421
1490
  }
1422
1491
  return noop$1;
1423
1492
  }
1424
- _mountingStack.pop();
1425
- registerComponent(compId, componentName, parent instanceof Element ? parent.firstElementChild : null, parentId);
1426
- const mountCleanups = [];
1427
- for (const fn of hooks.mount) try {
1493
+ if (__DEV__$3) {
1494
+ _mountingStack.pop();
1495
+ const firstEl = parent instanceof Element ? parent.firstElementChild : null;
1496
+ registerComponent(compId, componentName, firstEl, devParentId);
1497
+ }
1498
+ let mountCleanups = null;
1499
+ if (hooks.mount) for (const fn of hooks.mount) try {
1428
1500
  let cleanup;
1429
1501
  scope.runInScope(() => {
1430
1502
  cleanup = fn();
1431
1503
  });
1432
- if (cleanup) mountCleanups.push(cleanup);
1504
+ if (cleanup) {
1505
+ if (mountCleanups === null) mountCleanups = [];
1506
+ mountCleanups.push(cleanup);
1507
+ }
1433
1508
  } catch (err) {
1434
1509
  console.error(`[Pyreon] Error in onMount hook of <${componentName}>:`, err);
1435
1510
  reportError({
@@ -1440,10 +1515,10 @@ function mountComponent(vnode, parent, anchor) {
1440
1515
  });
1441
1516
  }
1442
1517
  return () => {
1443
- unregisterComponent(compId);
1518
+ if (__DEV__$3) unregisterComponent(compId);
1444
1519
  scope.stop();
1445
1520
  subtreeCleanup();
1446
- for (const fn of hooks.unmount) try {
1521
+ if (hooks.unmount) for (const fn of hooks.unmount) try {
1447
1522
  fn();
1448
1523
  } catch (err) {
1449
1524
  console.error(`[Pyreon] Error in onUnmount hook of <${componentName}>:`, err);
@@ -1454,7 +1529,7 @@ function mountComponent(vnode, parent, anchor) {
1454
1529
  timestamp: Date.now()
1455
1530
  });
1456
1531
  }
1457
- for (const fn of mountCleanups) fn();
1532
+ if (mountCleanups) for (const fn of mountCleanups) fn();
1458
1533
  };
1459
1534
  }
1460
1535
  function mountChildren(children, parent, anchor) {
@@ -1647,10 +1722,10 @@ function hydrateComponent(vnode, domNode, parent, anchor, path = "root") {
1647
1722
  const mountCleanups = [];
1648
1723
  let nextDom = domNode;
1649
1724
  const componentName = vnode.type.name || "Anonymous";
1650
- const mergedProps = (vnode.children ?? []).length > 0 && vnode.props.children === void 0 ? {
1725
+ const mergedProps = makeReactiveProps((vnode.children ?? []).length > 0 && vnode.props.children === void 0 ? {
1651
1726
  ...vnode.props,
1652
1727
  children: (vnode.children ?? []).length === 1 ? (vnode.children ?? [])[0] : vnode.children ?? []
1653
- } : vnode.props;
1728
+ } : vnode.props);
1654
1729
  let result;
1655
1730
  try {
1656
1731
  result = runWithHooks(vnode.type, mergedProps);
@@ -1670,13 +1745,13 @@ function hydrateComponent(vnode, domNode, parent, anchor, path = "root") {
1670
1745
  }
1671
1746
  setCurrentScope(null);
1672
1747
  const { vnode: output, hooks } = result;
1673
- for (const fn of hooks.update) scope.addUpdateHook(fn);
1748
+ if (hooks.update) for (const fn of hooks.update) scope.addUpdateHook(fn);
1674
1749
  if (output != null) {
1675
1750
  const [childCleanup, next] = hydrateChild(output, domNode, parent, anchor, path);
1676
1751
  subtreeCleanup = childCleanup;
1677
1752
  nextDom = next;
1678
1753
  }
1679
- for (const fn of hooks.mount) try {
1754
+ if (hooks.mount) for (const fn of hooks.mount) try {
1680
1755
  let c;
1681
1756
  scope.runInScope(() => {
1682
1757
  c = fn();
@@ -1693,7 +1768,7 @@ function hydrateComponent(vnode, domNode, parent, anchor, path = "root") {
1693
1768
  const cleanup = () => {
1694
1769
  scope.stop();
1695
1770
  subtreeCleanup();
1696
- for (const fn of hooks.unmount) fn();
1771
+ if (hooks.unmount) for (const fn of hooks.unmount) fn();
1697
1772
  for (const fn of mountCleanups) fn();
1698
1773
  };
1699
1774
  return [cleanup, nextDom];
@@ -1769,9 +1844,12 @@ function KeepAlive(props) {
1769
1844
  style: "display: contents"
1770
1845
  });
1771
1846
  }
1847
+ nativeCompat(KeepAlive);
1772
1848
 
1773
1849
  //#endregion
1774
1850
  //#region src/template.ts
1851
+ const __DEV__$2 = process.env.NODE_ENV !== "production";
1852
+ const _countSink$1 = globalThis;
1775
1853
  /**
1776
1854
  * Creates a row/item factory backed by HTML template cloning.
1777
1855
  *
@@ -1830,7 +1908,8 @@ function _bindText(source, node) {
1830
1908
  if (source.direct) {
1831
1909
  const textUpdate = () => {
1832
1910
  const v = source._v;
1833
- node.data = v == null || v === false ? "" : String(v);
1911
+ const next = v == null || v === false ? "" : String(v);
1912
+ if (next !== node.data) node.data = next;
1834
1913
  };
1835
1914
  textUpdate();
1836
1915
  return source.direct(textUpdate);
@@ -1838,7 +1917,8 @@ function _bindText(source, node) {
1838
1917
  const fn = source;
1839
1918
  return renderEffect(() => {
1840
1919
  const v = fn();
1841
- node.data = v == null || v === false ? "" : String(v);
1920
+ const next = v == null || v === false ? "" : String(v);
1921
+ if (next !== node.data) node.data = next;
1842
1922
  });
1843
1923
  }
1844
1924
  /**
@@ -1865,6 +1945,7 @@ function _bindDirect(source, updater) {
1865
1945
  const fn = source;
1866
1946
  return renderEffect(() => updater(fn()));
1867
1947
  }
1948
+ const TPL_CACHE_MAX = 1024;
1868
1949
  const _tplCache = /* @__PURE__ */ new Map();
1869
1950
  /**
1870
1951
  * Compiler-emitted template instantiation.
@@ -1890,10 +1971,18 @@ const _tplCache = /* @__PURE__ */ new Map();
1890
1971
  * })
1891
1972
  */
1892
1973
  function _tpl(html, bind) {
1974
+ if (__DEV__$2) _countSink$1.__pyreon_count__?.("runtime.tpl");
1893
1975
  let tpl = _tplCache.get(html);
1894
1976
  if (!tpl) {
1895
1977
  tpl = document.createElement("template");
1896
1978
  tpl.innerHTML = html;
1979
+ if (_tplCache.size >= TPL_CACHE_MAX) {
1980
+ const oldest = _tplCache.keys().next().value;
1981
+ if (oldest !== void 0) _tplCache.delete(oldest);
1982
+ }
1983
+ _tplCache.set(html, tpl);
1984
+ } else {
1985
+ _tplCache.delete(html);
1897
1986
  _tplCache.set(html, tpl);
1898
1987
  }
1899
1988
  const el = tpl.content.firstElementChild?.cloneNode(true);
@@ -1928,7 +2017,7 @@ function _mountSlot(children, parent, placeholder) {
1928
2017
 
1929
2018
  //#endregion
1930
2019
  //#region src/transition.ts
1931
- const __DEV__$1 = import.meta.env?.DEV === true;
2020
+ const __DEV__$1 = process.env.NODE_ENV !== "production";
1932
2021
  /**
1933
2022
  * Transition — adds CSS enter/leave animation classes to a single child element,
1934
2023
  * controlled by the reactive `show` prop.
@@ -2087,6 +2176,7 @@ function Transition(props) {
2087
2176
  };
2088
2177
  });
2089
2178
  }
2179
+ nativeCompat(Transition);
2090
2180
 
2091
2181
  //#endregion
2092
2182
  //#region src/transition-group.ts
@@ -2340,10 +2430,12 @@ function TransitionGroup(props) {
2340
2430
  });
2341
2431
  return h(tag, { ref: containerRef });
2342
2432
  }
2433
+ nativeCompat(TransitionGroup);
2343
2434
 
2344
2435
  //#endregion
2345
2436
  //#region src/index.ts
2346
- const __DEV__ = import.meta.env?.DEV === true;
2437
+ const __DEV__ = process.env.NODE_ENV !== "production";
2438
+ const _countSink = globalThis;
2347
2439
  /**
2348
2440
  * Mount a VNode tree into a container element.
2349
2441
  * Clears the container first, then mounts the given child.
@@ -2354,14 +2446,21 @@ const __DEV__ = import.meta.env?.DEV === true;
2354
2446
  */
2355
2447
  function mount(root, container) {
2356
2448
  if (__DEV__ && container == null) throw new Error("[pyreon] mount() called with a null/undefined container. Make sure the element exists in the DOM, e.g. document.getElementById(\"app\")");
2357
- installDevTools();
2449
+ if (__DEV__) {
2450
+ _countSink.__pyreon_count__?.("runtime.mount");
2451
+ installDevTools();
2452
+ }
2358
2453
  setupDelegation(container);
2359
2454
  container.innerHTML = "";
2360
- return mountChild(root, container, null);
2455
+ const unmount = mountChild(root, container, null);
2456
+ return () => {
2457
+ if (__DEV__) _countSink.__pyreon_count__?.("runtime.unmount");
2458
+ unmount();
2459
+ };
2361
2460
  }
2362
2461
  /** Alias for `mount` */
2363
2462
  const render = mount;
2364
2463
 
2365
2464
  //#endregion
2366
- export { DELEGATED_EVENTS, KeepAlive, Transition, TransitionGroup, applyProps as _applyProps, applyProps, _bindDirect, _bindText, _mountSlot, _tpl, applyProp, createTemplate, delegatedPropName, disableHydrationWarnings, enableHydrationWarnings, hydrateRoot, mount, mountChild, render, sanitizeHtml, setSanitizer, setupDelegation };
2465
+ export { DELEGATED_EVENTS, KeepAlive, Transition, TransitionGroup, applyProps as _applyProps, applyProps, _bindDirect, _bindText, _mountSlot, _tpl, applyProp, createTemplate, delegatedPropName, disableHydrationWarnings, enableHydrationWarnings, hydrateRoot, mount, mountChild, onHydrationMismatch, render, sanitizeHtml, setSanitizer, setupDelegation };
2367
2466
  //# sourceMappingURL=index.js.map