@pyreon/runtime-dom 0.13.1 → 0.14.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
@@ -303,7 +303,8 @@ function installDevTools() {
303
303
 
304
304
  //#endregion
305
305
  //#region src/nodes.ts
306
- const __DEV__$4 = import.meta.env?.DEV === true;
306
+ const __DEV__$5 = import.meta.env?.DEV === true;
307
+ const _countSink$3 = globalThis;
307
308
  /**
308
309
  * Move all nodes strictly between `start` and `end` into a throwaway
309
310
  * DocumentFragment, detaching them from the live DOM in O(n) top-level moves.
@@ -367,6 +368,7 @@ function growLisArrays(lis, n) {
367
368
  function computeKeyedLis(lis, n, newKeyOrder, curPos) {
368
369
  const { tails, tailIdx, pred } = lis;
369
370
  let lisLen = 0;
371
+ let ops = 0;
370
372
  for (let i = 0; i < n; i++) {
371
373
  const key = newKeyOrder[i];
372
374
  if (key === void 0) continue;
@@ -376,6 +378,7 @@ function computeKeyedLis(lis, n, newKeyOrder, curPos) {
376
378
  let hi = lisLen;
377
379
  while (lo < hi) {
378
380
  const mid = lo + hi >> 1;
381
+ ops++;
379
382
  if (tails[mid] < v) lo = mid + 1;
380
383
  else hi = mid;
381
384
  }
@@ -384,6 +387,7 @@ function computeKeyedLis(lis, n, newKeyOrder, curPos) {
384
387
  if (lo > 0) pred[i] = tailIdx[lo - 1];
385
388
  if (lo === lisLen) lisLen++;
386
389
  }
390
+ if (__DEV__$5 && ops > 0) _countSink$3.__pyreon_count__?.("runtime.mountFor.lisOps", ops);
387
391
  return lisLen;
388
392
  }
389
393
  function markStayingEntries(lis, lisLen) {
@@ -521,21 +525,36 @@ function trySmallKReorder(n, newKeys, currentKeys, cache, liveParent, tailMarker
521
525
  function computeForLis(lis, n, newKeys, cache) {
522
526
  const { tails, tailIdx, pred } = lis;
523
527
  let lisLen = 0;
528
+ let ops = 0;
529
+ let lastV = -1;
524
530
  for (let i = 0; i < n; i++) {
525
531
  const key = newKeys[i];
526
532
  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;
533
+ if (v > lastV) {
534
+ tails[lisLen] = v;
535
+ tailIdx[lisLen] = i;
536
+ if (lisLen > 0) pred[i] = tailIdx[lisLen - 1];
537
+ lisLen++;
538
+ lastV = v;
539
+ continue;
540
+ }
541
+ let lo;
542
+ if (v < lisLen && tails[v] === v) lo = v;
543
+ else {
544
+ lo = 0;
545
+ let hi = lisLen;
546
+ while (lo < hi) {
547
+ const mid = lo + hi >> 1;
548
+ ops++;
549
+ if (tails[mid] < v) lo = mid + 1;
550
+ else hi = mid;
551
+ }
533
552
  }
534
553
  tails[lo] = v;
535
554
  tailIdx[lo] = i;
536
555
  if (lo > 0) pred[i] = tailIdx[lo - 1];
537
- if (lo === lisLen) lisLen++;
538
556
  }
557
+ if (__DEV__$5 && ops > 0) _countSink$3.__pyreon_count__?.("runtime.mountFor.lisOps", ops);
539
558
  return lisLen;
540
559
  }
541
560
  function applyForMoves(n, newKeys, stay, cache, liveParent, tailMarker) {
@@ -576,6 +595,7 @@ function mountFor(source, getKey, renderItem, parent, anchor, mountChild) {
576
595
  parent.insertBefore(tailMarker, anchor);
577
596
  let cache = /* @__PURE__ */ new Map();
578
597
  let currentKeys = [];
598
+ const _reusableKeySet = /* @__PURE__ */ new Set();
579
599
  let cleanupCount = 0;
580
600
  let anchorsRegistered = false;
581
601
  let lis = {
@@ -586,9 +606,9 @@ function mountFor(source, getKey, renderItem, parent, anchor, mountChild) {
586
606
  };
587
607
  const warnForKey = (seen, key) => {
588
608
  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.");
609
+ 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
610
  if (seen.has(key)) {
591
- if (__DEV__$4) console.warn(`[Pyreon] Duplicate key "${String(key)}" in <For> list. Keys must be unique.`);
611
+ if (__DEV__$5) console.warn(`[Pyreon] Duplicate key "${String(key)}" in <For> list. Keys must be unique.`);
592
612
  return true;
593
613
  }
594
614
  seen.add(key);
@@ -714,7 +734,9 @@ function mountFor(source, getKey, renderItem, parent, anchor, mountChild) {
714
734
  return false;
715
735
  };
716
736
  const handleIncrementalUpdate = (items, n, newKeys, liveParent) => {
717
- removeStaleForEntries(new Set(newKeys));
737
+ _reusableKeySet.clear();
738
+ for (let i = 0; i < newKeys.length; i++) _reusableKeySet.add(newKeys[i]);
739
+ removeStaleForEntries(_reusableKeySet);
718
740
  mountNewForEntries(items, n, newKeys, liveParent);
719
741
  if (!anchorsRegistered) {
720
742
  for (const entry of cache.values()) _forAnchors.add(entry.anchor);
@@ -813,7 +835,7 @@ function moveEntryBefore(parent, startNode, before) {
813
835
 
814
836
  //#endregion
815
837
  //#region src/props.ts
816
- const __DEV__$3 = import.meta.env?.DEV === true;
838
+ const __DEV__$4 = import.meta.env?.DEV === true;
817
839
  let _customSanitizer = null;
818
840
  /**
819
841
  * Set a custom HTML sanitizer used by `innerHTML` and `sanitizeHtml()`.
@@ -978,7 +1000,7 @@ function applyProps(el, props) {
978
1000
  */
979
1001
  function applyEventProp(el, key, value) {
980
1002
  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}={() => ...}?`);
1003
+ 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
1004
  return null;
983
1005
  }
984
1006
  const eventName = (key[2]?.toLowerCase() + key.slice(3)).toLowerCase();
@@ -1009,7 +1031,7 @@ function applyEventProp(el, key, value) {
1009
1031
  * dispatch) eliminates the entire bug class.
1010
1032
  */
1011
1033
  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().`);
1034
+ 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
1035
  if (key === "innerHTML") {
1014
1036
  const html = String(value ?? "");
1015
1037
  if (typeof el.setHTML === "function") el.setHTML(html);
@@ -1076,7 +1098,7 @@ function applyClassProp(el, value) {
1076
1098
  }
1077
1099
  function setStaticProp(el, key, value) {
1078
1100
  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}`);
1101
+ if (__DEV__$4) console.warn(`[Pyreon] Blocked unsafe URL in "${key}" attribute: ${value}`);
1080
1102
  return;
1081
1103
  }
1082
1104
  if (key === "class" || key === "className") {
@@ -1113,10 +1135,12 @@ function setStaticProp(el, key, value) {
1113
1135
 
1114
1136
  //#endregion
1115
1137
  //#region src/mount.ts
1116
- const __DEV__$2 = import.meta.env?.DEV === true;
1138
+ const __DEV__$3 = import.meta.env?.DEV === true;
1139
+ const _countSink$2 = globalThis;
1117
1140
  const noop$1 = () => {};
1118
1141
  let _elementDepth = 0;
1119
- const _mountingStack = [];
1142
+ let _mountingStack;
1143
+ if (__DEV__$3) _mountingStack = [];
1120
1144
  /**
1121
1145
  * Mount a single child into `parent`, inserting before `anchor` (null = append).
1122
1146
  * Returns a cleanup that removes the node(s) and disposes all reactive effects.
@@ -1125,6 +1149,7 @@ const _mountingStack = [];
1125
1149
  * function call overhead in tight render loops (1000+ calls per list render).
1126
1150
  */
1127
1151
  function mountChild(child, parent, anchor = null) {
1152
+ if (__DEV__$3) _countSink$2.__pyreon_count__?.("runtime.mountChild");
1128
1153
  if (typeof child === "function") {
1129
1154
  const sample = runUntracked(() => child());
1130
1155
  if (isKeyedArray(sample)) {
@@ -1139,7 +1164,8 @@ function mountChild(child, parent, anchor = null) {
1139
1164
  parent.insertBefore(text, anchor);
1140
1165
  const dispose = renderEffect(() => {
1141
1166
  const v = child();
1142
- text.data = v == null || v === false ? "" : String(v);
1167
+ const next = v == null || v === false ? "" : String(v);
1168
+ if (next !== text.data) text.data = next;
1143
1169
  });
1144
1170
  if (_elementDepth > 0) return dispose;
1145
1171
  return () => {
@@ -1180,24 +1206,26 @@ function mountChild(child, parent, anchor = null) {
1180
1206
  const vnode = child;
1181
1207
  if (vnode.type === Fragment) return mountChildren(vnode.children ?? [], parent, anchor);
1182
1208
  if (vnode.type === ForSymbol) {
1183
- const { each, by, children } = vnode.props;
1209
+ const props = vnode.props;
1210
+ const initialEach = props.each;
1211
+ const source = typeof initialEach === "function" ? initialEach : (() => props.each);
1184
1212
  const prevDepth = _elementDepth;
1185
1213
  _elementDepth = 0;
1186
- const cleanup = mountFor(each, by, children, parent, anchor, mountChild);
1214
+ const cleanup = mountFor(source, props.by, props.children, parent, anchor, mountChild);
1187
1215
  _elementDepth = prevDepth;
1188
1216
  return cleanup;
1189
1217
  }
1190
1218
  if (vnode.type === PortalSymbol) {
1191
1219
  const { target, children } = vnode.props;
1192
- if (__DEV__$2 && !target) {
1220
+ if (__DEV__$3 && !target) {
1193
1221
  console.warn("[Pyreon] <Portal> received a falsy `target`. Provide a valid DOM element.");
1194
1222
  return noop$1;
1195
1223
  }
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.`);
1224
+ 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
1225
  return mountChild(children, target, null);
1198
1226
  }
1199
1227
  if (typeof vnode.type === "function") return mountComponent(vnode, parent, anchor);
1200
- if (__DEV__$2 && typeof vnode.type !== "string") {
1228
+ if (__DEV__$3 && typeof vnode.type !== "string") {
1201
1229
  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
1230
  return noop$1;
1203
1231
  }
@@ -1312,7 +1340,7 @@ function mountElement(vnode, parent, anchor) {
1312
1340
  const isMathml = tag === "math";
1313
1341
  if (isSvg) _svgDepth++;
1314
1342
  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.`);
1343
+ 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
1344
  const props = vnode.props;
1317
1345
  const propCleanup = props !== EMPTY_PROPS ? applyProps(el, props) : null;
1318
1346
  _elementDepth++;
@@ -1360,9 +1388,13 @@ function mountComponent(vnode, parent, anchor) {
1360
1388
  let hooks;
1361
1389
  let output;
1362
1390
  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);
1391
+ let compId;
1392
+ let devParentId;
1393
+ if (__DEV__$3) {
1394
+ compId = `${componentName}-${Math.random().toString(36).slice(2, 9)}`;
1395
+ devParentId = _mountingStack[_mountingStack.length - 1] ?? null;
1396
+ _mountingStack.push(compId);
1397
+ }
1366
1398
  const children = vnode.children ?? [];
1367
1399
  const rawProps = children.length > 0 && vnode.props.children === void 0 ? {
1368
1400
  ...vnode.props,
@@ -1374,7 +1406,7 @@ function mountComponent(vnode, parent, anchor) {
1374
1406
  hooks = result.hooks;
1375
1407
  output = result.vnode;
1376
1408
  } catch (err) {
1377
- _mountingStack.pop();
1409
+ if (__DEV__$3) _mountingStack.pop();
1378
1410
  setCurrentScope(null);
1379
1411
  scope.stop();
1380
1412
  reportError({
@@ -1386,7 +1418,7 @@ function mountComponent(vnode, parent, anchor) {
1386
1418
  });
1387
1419
  const handled = dispatchToErrorBoundary(err);
1388
1420
  if (!handled) console.error(`[Pyreon] <${componentName}> threw during setup:`, err);
1389
- if (__DEV__$2 && !handled) {
1421
+ if (__DEV__$3 && !handled) {
1390
1422
  const overlay = document.createElement("pre");
1391
1423
  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
1424
  const e = err;
@@ -1398,16 +1430,16 @@ function mountComponent(vnode, parent, anchor) {
1398
1430
  } finally {
1399
1431
  setCurrentScope(null);
1400
1432
  }
1401
- if (__DEV__$2 && output != null && typeof output === "object") {
1433
+ if (__DEV__$3 && output != null && typeof output === "object") {
1402
1434
  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
1435
  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
1436
  }
1405
- for (const fn of hooks.update) scope.addUpdateHook(fn);
1437
+ if (hooks.update) for (const fn of hooks.update) scope.addUpdateHook(fn);
1406
1438
  let subtreeCleanup = noop$1;
1407
1439
  try {
1408
1440
  subtreeCleanup = output != null ? mountChild(output, parent, anchor) : noop$1;
1409
1441
  } catch (err) {
1410
- _mountingStack.pop();
1442
+ if (__DEV__$3) _mountingStack.pop();
1411
1443
  scope.stop();
1412
1444
  if (!(propagateError(err, hooks) || dispatchToErrorBoundary(err))) {
1413
1445
  reportError({
@@ -1421,15 +1453,21 @@ function mountComponent(vnode, parent, anchor) {
1421
1453
  }
1422
1454
  return noop$1;
1423
1455
  }
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 {
1456
+ if (__DEV__$3) {
1457
+ _mountingStack.pop();
1458
+ const firstEl = parent instanceof Element ? parent.firstElementChild : null;
1459
+ registerComponent(compId, componentName, firstEl, devParentId);
1460
+ }
1461
+ let mountCleanups = null;
1462
+ if (hooks.mount) for (const fn of hooks.mount) try {
1428
1463
  let cleanup;
1429
1464
  scope.runInScope(() => {
1430
1465
  cleanup = fn();
1431
1466
  });
1432
- if (cleanup) mountCleanups.push(cleanup);
1467
+ if (cleanup) {
1468
+ if (mountCleanups === null) mountCleanups = [];
1469
+ mountCleanups.push(cleanup);
1470
+ }
1433
1471
  } catch (err) {
1434
1472
  console.error(`[Pyreon] Error in onMount hook of <${componentName}>:`, err);
1435
1473
  reportError({
@@ -1440,10 +1478,10 @@ function mountComponent(vnode, parent, anchor) {
1440
1478
  });
1441
1479
  }
1442
1480
  return () => {
1443
- unregisterComponent(compId);
1481
+ if (__DEV__$3) unregisterComponent(compId);
1444
1482
  scope.stop();
1445
1483
  subtreeCleanup();
1446
- for (const fn of hooks.unmount) try {
1484
+ if (hooks.unmount) for (const fn of hooks.unmount) try {
1447
1485
  fn();
1448
1486
  } catch (err) {
1449
1487
  console.error(`[Pyreon] Error in onUnmount hook of <${componentName}>:`, err);
@@ -1454,7 +1492,7 @@ function mountComponent(vnode, parent, anchor) {
1454
1492
  timestamp: Date.now()
1455
1493
  });
1456
1494
  }
1457
- for (const fn of mountCleanups) fn();
1495
+ if (mountCleanups) for (const fn of mountCleanups) fn();
1458
1496
  };
1459
1497
  }
1460
1498
  function mountChildren(children, parent, anchor) {
@@ -1670,13 +1708,13 @@ function hydrateComponent(vnode, domNode, parent, anchor, path = "root") {
1670
1708
  }
1671
1709
  setCurrentScope(null);
1672
1710
  const { vnode: output, hooks } = result;
1673
- for (const fn of hooks.update) scope.addUpdateHook(fn);
1711
+ if (hooks.update) for (const fn of hooks.update) scope.addUpdateHook(fn);
1674
1712
  if (output != null) {
1675
1713
  const [childCleanup, next] = hydrateChild(output, domNode, parent, anchor, path);
1676
1714
  subtreeCleanup = childCleanup;
1677
1715
  nextDom = next;
1678
1716
  }
1679
- for (const fn of hooks.mount) try {
1717
+ if (hooks.mount) for (const fn of hooks.mount) try {
1680
1718
  let c;
1681
1719
  scope.runInScope(() => {
1682
1720
  c = fn();
@@ -1693,7 +1731,7 @@ function hydrateComponent(vnode, domNode, parent, anchor, path = "root") {
1693
1731
  const cleanup = () => {
1694
1732
  scope.stop();
1695
1733
  subtreeCleanup();
1696
- for (const fn of hooks.unmount) fn();
1734
+ if (hooks.unmount) for (const fn of hooks.unmount) fn();
1697
1735
  for (const fn of mountCleanups) fn();
1698
1736
  };
1699
1737
  return [cleanup, nextDom];
@@ -1772,6 +1810,8 @@ function KeepAlive(props) {
1772
1810
 
1773
1811
  //#endregion
1774
1812
  //#region src/template.ts
1813
+ const __DEV__$2 = import.meta.env?.DEV === true;
1814
+ const _countSink$1 = globalThis;
1775
1815
  /**
1776
1816
  * Creates a row/item factory backed by HTML template cloning.
1777
1817
  *
@@ -1830,7 +1870,8 @@ function _bindText(source, node) {
1830
1870
  if (source.direct) {
1831
1871
  const textUpdate = () => {
1832
1872
  const v = source._v;
1833
- node.data = v == null || v === false ? "" : String(v);
1873
+ const next = v == null || v === false ? "" : String(v);
1874
+ if (next !== node.data) node.data = next;
1834
1875
  };
1835
1876
  textUpdate();
1836
1877
  return source.direct(textUpdate);
@@ -1838,7 +1879,8 @@ function _bindText(source, node) {
1838
1879
  const fn = source;
1839
1880
  return renderEffect(() => {
1840
1881
  const v = fn();
1841
- node.data = v == null || v === false ? "" : String(v);
1882
+ const next = v == null || v === false ? "" : String(v);
1883
+ if (next !== node.data) node.data = next;
1842
1884
  });
1843
1885
  }
1844
1886
  /**
@@ -1890,6 +1932,7 @@ const _tplCache = /* @__PURE__ */ new Map();
1890
1932
  * })
1891
1933
  */
1892
1934
  function _tpl(html, bind) {
1935
+ if (__DEV__$2) _countSink$1.__pyreon_count__?.("runtime.tpl");
1893
1936
  let tpl = _tplCache.get(html);
1894
1937
  if (!tpl) {
1895
1938
  tpl = document.createElement("template");
@@ -2344,6 +2387,7 @@ function TransitionGroup(props) {
2344
2387
  //#endregion
2345
2388
  //#region src/index.ts
2346
2389
  const __DEV__ = import.meta.env?.DEV === true;
2390
+ const _countSink = globalThis;
2347
2391
  /**
2348
2392
  * Mount a VNode tree into a container element.
2349
2393
  * Clears the container first, then mounts the given child.
@@ -2354,10 +2398,17 @@ const __DEV__ = import.meta.env?.DEV === true;
2354
2398
  */
2355
2399
  function mount(root, container) {
2356
2400
  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();
2401
+ if (__DEV__) {
2402
+ _countSink.__pyreon_count__?.("runtime.mount");
2403
+ installDevTools();
2404
+ }
2358
2405
  setupDelegation(container);
2359
2406
  container.innerHTML = "";
2360
- return mountChild(root, container, null);
2407
+ const unmount = mountChild(root, container, null);
2408
+ return () => {
2409
+ if (__DEV__) _countSink.__pyreon_count__?.("runtime.unmount");
2410
+ unmount();
2411
+ };
2361
2412
  }
2362
2413
  /** Alias for `mount` */
2363
2414
  const render = mount;