rerender-lens 0.2.0 → 0.4.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.
Files changed (63) hide show
  1. package/CHANGELOG.md +110 -1
  2. package/README.md +159 -9
  3. package/dist/budget-COBu7jBU.d.cts +64 -0
  4. package/dist/budget-LkNjGtRc.d.ts +64 -0
  5. package/dist/cli.cjs +458 -0
  6. package/dist/cli.cjs.map +1 -0
  7. package/dist/cli.d.cts +4 -0
  8. package/dist/cli.d.ts +4 -0
  9. package/dist/cli.js +452 -0
  10. package/dist/cli.js.map +1 -0
  11. package/dist/devtools-BkCct3cJ.d.cts +252 -0
  12. package/dist/devtools-CZebzpF6.d.ts +252 -0
  13. package/dist/index.cjs +832 -75
  14. package/dist/index.cjs.map +1 -1
  15. package/dist/index.d.cts +40 -336
  16. package/dist/index.d.ts +40 -336
  17. package/dist/index.js +816 -76
  18. package/dist/index.js.map +1 -1
  19. package/dist/notifiers-BGQtWKfX.d.cts +90 -0
  20. package/dist/notifiers-BjjGSHpp.d.ts +90 -0
  21. package/dist/playwright.cjs +199 -0
  22. package/dist/playwright.cjs.map +1 -0
  23. package/dist/playwright.d.cts +37 -0
  24. package/dist/playwright.d.ts +37 -0
  25. package/dist/playwright.js +186 -0
  26. package/dist/playwright.js.map +1 -0
  27. package/dist/relay.cjs +166 -0
  28. package/dist/relay.cjs.map +1 -0
  29. package/dist/relay.d.cts +41 -0
  30. package/dist/relay.d.ts +41 -0
  31. package/dist/relay.js +161 -0
  32. package/dist/relay.js.map +1 -0
  33. package/dist/rerender-lens.iife.js +2178 -0
  34. package/dist/setup.cjs +1638 -0
  35. package/dist/setup.cjs.map +1 -0
  36. package/dist/setup.d.cts +2 -0
  37. package/dist/setup.d.ts +2 -0
  38. package/dist/setup.js +1636 -0
  39. package/dist/setup.js.map +1 -0
  40. package/dist/types-BzUEVkxJ.d.cts +177 -0
  41. package/dist/types-BzUEVkxJ.d.ts +177 -0
  42. package/dist/vite.cjs +113 -0
  43. package/dist/vite.cjs.map +1 -0
  44. package/dist/vite.d.cts +84 -0
  45. package/dist/vite.d.ts +84 -0
  46. package/dist/vite.js +103 -0
  47. package/dist/vite.js.map +1 -0
  48. package/dist/vitest-setup.cjs +1368 -0
  49. package/dist/vitest-setup.cjs.map +1 -0
  50. package/dist/vitest-setup.d.cts +8 -0
  51. package/dist/vitest-setup.d.ts +8 -0
  52. package/dist/vitest-setup.js +1366 -0
  53. package/dist/vitest-setup.js.map +1 -0
  54. package/dist/vitest.cjs +1429 -0
  55. package/dist/vitest.cjs.map +1 -0
  56. package/dist/vitest.d.cts +52 -0
  57. package/dist/vitest.d.ts +52 -0
  58. package/dist/vitest.js +1420 -0
  59. package/dist/vitest.js.map +1 -0
  60. package/package.json +84 -4
  61. package/panel/panel.css +384 -0
  62. package/panel/panel.html +12 -0
  63. package/panel/panel.js +3287 -0
package/dist/index.cjs CHANGED
@@ -28,10 +28,49 @@ function remember(seen, a, b) {
28
28
  set.add(b);
29
29
  return false;
30
30
  }
31
+ var scope = null;
32
+ var DEFAULT_DIFF_BUDGET = 5e4;
33
+ function beginDiffScope(budget = 2e5) {
34
+ const previous = scope;
35
+ scope = { cache: /* @__PURE__ */ new WeakMap(), remaining: budget, exhausted: false };
36
+ return () => {
37
+ scope = previous;
38
+ };
39
+ }
40
+ function diffBudgetExhausted() {
41
+ return scope?.exhausted === true;
42
+ }
31
43
  function deepEqual(a, b, seen = /* @__PURE__ */ new Map()) {
32
44
  if (Object.is(a, b)) return true;
33
45
  if (typeof a !== typeof b) return false;
34
46
  if (typeof a !== "object" || a === null || b === null) return false;
47
+ if (!scope) {
48
+ const end = beginDiffScope(DEFAULT_DIFF_BUDGET);
49
+ try {
50
+ return deepEqual(a, b, seen);
51
+ } finally {
52
+ end();
53
+ }
54
+ }
55
+ const sc = scope;
56
+ const objA = a;
57
+ const objB = b;
58
+ const hit = sc.cache.get(objA)?.get(objB);
59
+ if (hit !== void 0) return hit;
60
+ if (--sc.remaining < 0) {
61
+ sc.exhausted = true;
62
+ return false;
63
+ }
64
+ const result = deepEqualObjects(objA, objB, seen);
65
+ let m = sc.cache.get(objA);
66
+ if (!m) {
67
+ m = /* @__PURE__ */ new WeakMap();
68
+ sc.cache.set(objA, m);
69
+ }
70
+ m.set(objB, result);
71
+ return result;
72
+ }
73
+ function deepEqualObjects(a, b, seen) {
35
74
  const objA = a;
36
75
  const objB = b;
37
76
  if (remember(seen, objA, objB)) return true;
@@ -60,6 +99,10 @@ function deepEqual(a, b, seen = /* @__PURE__ */ new Map()) {
60
99
  }
61
100
  if (a instanceof Set) {
62
101
  if (!(b instanceof Set) || a.size !== b.size) return false;
102
+ if (a.size > 50) {
103
+ for (const v of a) if (!b.has(v)) return false;
104
+ return true;
105
+ }
63
106
  outer: for (const v of a) {
64
107
  if (b.has(v)) continue;
65
108
  for (const w of b) if (deepEqual(v, w, seen)) continue outer;
@@ -75,9 +118,10 @@ function deepEqual(a, b, seen = /* @__PURE__ */ new Map()) {
75
118
  const ka = Object.keys(a);
76
119
  const kb = Object.keys(b);
77
120
  if (ka.length !== kb.length) return false;
121
+ const rb = b;
78
122
  for (const k of ka) {
79
123
  if (!Object.prototype.hasOwnProperty.call(b, k)) return false;
80
- if (!deepEqual(a[k], b[k], seen)) return false;
124
+ if (!deepEqual(a[k], rb[k], seen)) return false;
81
125
  }
82
126
  return true;
83
127
  }
@@ -119,7 +163,7 @@ function diffRecords(prev, next, basePath = "") {
119
163
  if (Object.is(a, b)) continue;
120
164
  const kind = classify(a, b);
121
165
  if (kind === "different") {
122
- changes.push({ path: firstDifferentPath(a, b, path), kind, prev: a, next: b });
166
+ changes.push({ path: diffBudgetExhausted() ? path : firstDifferentPath(a, b, path), kind, prev: a, next: b });
123
167
  } else {
124
168
  changes.push({ path, kind, prev: a, next: b });
125
169
  }
@@ -147,6 +191,17 @@ function firstDifferentPath(a, b, path, depth = 0) {
147
191
 
148
192
  // src/report.ts
149
193
  var now = () => typeof performance !== "undefined" && typeof performance.now === "function" ? performance.now() : Date.now();
194
+ var hookLabel = (c) => c.custom && c.custom.length ? `${c.custom.join(" \u203A ")} \u203A ${c.hook}#${c.index}` : `${c.hook} #${c.index}`;
195
+ function storeAdvice(custom) {
196
+ const chain = custom ?? [];
197
+ if (chain.some((n) => /^useSelector$/.test(n)))
198
+ return "the Redux selector returns a new object on every call: return a stored slice, pass shallowEqual as the equality function, or memoize it with createSelector";
199
+ if (chain.some((n) => /^useAppSelector$/.test(n)))
200
+ return "the selector returns a new object on every call: return a stored slice, pass shallowEqual, or memoize it with createSelector";
201
+ if (chain.some((n) => /^use[A-Z]\w*Store$/.test(n) || n === "useStore" || n === "useBoundStore"))
202
+ return "the store selector returns a new object on every call: select a primitive, or wrap the selector with useShallow (Zustand) / an equality function";
203
+ return "getSnapshot returns a new reference with the same contents: cache the snapshot in the store and return the same object while the data is unchanged";
204
+ }
150
205
  var isGenuine = (c) => c.kind === "different" || c.kind === "added" || c.kind === "removed";
151
206
  var isChildren = (change) => change.path === "children";
152
207
  function fixFor(change) {
@@ -222,10 +277,17 @@ function buildReport(input) {
222
277
  const where = c.provider && c.provider.component ? ` (provided by <${c.provider.component}>)` : "";
223
278
  const keys = c.changedKeys && typeof c.totalKeys === "number" && c.changedKeys.length > 0 && c.changedKeys.length < c.totalKeys ? `: only ${c.changedKeys.map((k) => `"${k}"`).join(", ")} of ${c.totalKeys} keys changed, yet every consumer re-renders. Split the context or memoize the slices consumers read` : "";
224
279
  reasons.push(`${c.path} changed${where}${keys}.`);
225
- } else if (isGenuine(c)) reasons.push(`${c.hook} #${c.index} changed.`);
280
+ } else if (isGenuine(c)) reasons.push(`${hookLabel(c)} changed.`);
226
281
  else if (isStateHook(c))
227
- reasons.push(`${c.hook} #${c.index} was set to a value deep-equal to the current one (new reference, same contents): reuse the existing object or bail out before calling the setter.`);
228
- else reasons.push(`${c.hook} #${c.index} returned a new reference that is deep-equal to the previous value: memoize the context/store value where it is produced.`);
282
+ reasons.push(`${hookLabel(c)} was set to a value deep-equal to the current one (new reference, same contents): reuse the existing object or bail out before calling the setter.`);
283
+ else if (c.hook === "useSyncExternalStore") reasons.push(`${hookLabel(c)} returned a new reference that is deep-equal to the previous value: ${storeAdvice(c.custom)}.`);
284
+ else reasons.push(`${hookLabel(c)} returned a new reference that is deep-equal to the previous value: memoize the context/store value where it is produced.`);
285
+ }
286
+ if (input.commitCause === "effect-after-commit") {
287
+ const who = input.updaters && input.updaters.length ? input.updaters.map((u) => `<${u}>`).join(", ") : "a component that rendered in it";
288
+ reasons.push(`this commit was scheduled right after commit #${input.afterCommit ?? "?"} by ${who}: an effect there set state (effect \u2192 setState loop). Derive the value during render or compute it before setting state.`);
289
+ } else if (input.commitCause === "suspense-resolved") {
290
+ reasons.push("this commit shows content that was suspended (a Suspense boundary resolved).");
229
291
  }
230
292
  const report = {
231
293
  component: input.component,
@@ -249,6 +311,13 @@ function buildReport(input) {
249
311
  if (input.treeDuration !== void 0) report.treeDuration = input.treeDuration;
250
312
  if (input.commitPriority) report.commitPriority = input.commitPriority;
251
313
  if (input.source) report.source = input.source;
314
+ if (input.hookState) report.hookState = input.hookState;
315
+ if (input.contexts) report.contexts = input.contexts;
316
+ if (input.state) report.state = input.state;
317
+ if (input.updaters && input.updaters.length) report.updaters = input.updaters;
318
+ if (input.commitCause) report.commitCause = input.commitCause;
319
+ if (input.afterCommit !== void 0) report.afterCommit = input.afterCommit;
320
+ if (input.key !== void 0) report.key = input.key;
252
321
  return report;
253
322
  }
254
323
  function describeTrigger(t) {
@@ -293,6 +362,9 @@ function printReport(report, options) {
293
362
  c.log(`${ch.path} (${KIND_LABEL[ch.kind]})`, { prev: ch.prev, next: ch.next });
294
363
  }
295
364
  c.log("props", report.props);
365
+ if (report.hookState && report.hookState.length) c.log("hooks", Object.fromEntries(report.hookState.map((h) => [h.path, h.value])));
366
+ if (report.contexts && report.contexts.length) c.log("contexts", Object.fromEntries(report.contexts.map((x) => [x.name, x.value])));
367
+ if (report.state) c.log("state", report.state);
296
368
  c.groupEnd();
297
369
  }
298
370
 
@@ -302,7 +374,7 @@ function getState() {
302
374
  const g = globalThis;
303
375
  let s = g[KEY];
304
376
  if (!s) {
305
- s = { options: {}, enabled: false, printed: /* @__PURE__ */ new Map(), detach: null, nextInstanceId: 1, nextCommitId: 1, warnedOnce: /* @__PURE__ */ new Set(), scheduled: 0, commits: 0 };
377
+ s = { options: {}, enabled: false, printed: /* @__PURE__ */ new Map(), detach: null, nextInstanceId: 1, nextCommitId: 1, warnedOnce: /* @__PURE__ */ new Set(), scheduled: 0, commits: 0, overheadMs: 0, maxCommitMs: 0, truncated: 0 };
306
378
  g[KEY] = s;
307
379
  }
308
380
  return s;
@@ -332,11 +404,176 @@ function dispatch(report, override) {
332
404
  try {
333
405
  options.notifier(report);
334
406
  } catch (err) {
335
- (options.console ?? console).warn("[rerender-lens] notifier threw", err);
407
+ warnOnce("notifier", `notifier threw: ${String(err)}`);
336
408
  }
337
409
  }
338
410
  }
339
411
 
412
+ // src/hookNames.ts
413
+ var noop = () => {
414
+ };
415
+ function customHooksFromStack(stack, componentName, marker) {
416
+ const names = [];
417
+ let past = false;
418
+ for (const line of stack.split("\n")) {
419
+ const m = /^\s*at\s+(?:async\s+)?([^\s(]+)|^([^@\s]+)@/.exec(line);
420
+ const name = m ? (m[1] || m[2] || "").replace(/^Object\./, "").replace(/^Proxy\./, "") : "";
421
+ if (!name) continue;
422
+ if (!past) {
423
+ if (name.includes(marker)) past = true;
424
+ continue;
425
+ }
426
+ if (name === componentName || name.endsWith("." + componentName)) break;
427
+ if (/^use[A-Z0-9_]/.test(name)) names.push(name);
428
+ if (names.length > 8) break;
429
+ }
430
+ return names;
431
+ }
432
+ var cache = /* @__PURE__ */ new WeakMap();
433
+ function makeDispatcher(fiber, componentName, out, marker) {
434
+ let node = fiber.memoizedState;
435
+ let index = 0;
436
+ const take = (count) => {
437
+ const first = node;
438
+ for (let i = 0; i < count && node; i++) node = node.next;
439
+ index += count;
440
+ return first;
441
+ };
442
+ const record = (primitive, count) => {
443
+ const stack = new Error().stack || "";
444
+ const chain = customHooksFromStack(stack, componentName, marker);
445
+ const start = index;
446
+ const first = take(count);
447
+ if (chain.length) for (let i = 0; i < count; i++) out.set(start + i, chain);
448
+ return first;
449
+ };
450
+ const memoized = (n) => n ? n.memoizedState : void 0;
451
+ const dispatcher = {};
452
+ const define = (name, impl) => {
453
+ const fn = { [marker + name]: (...a) => impl(...a) }[marker + name];
454
+ dispatcher[name] = fn;
455
+ };
456
+ define("useState", () => {
457
+ const n = record("useState", 1);
458
+ return [memoized(n), noop];
459
+ });
460
+ define("useReducer", () => {
461
+ const n = record("useReducer", 1);
462
+ return [memoized(n), noop];
463
+ });
464
+ define("useRef", (init2) => {
465
+ const n = record("useRef", 1);
466
+ return n ? n.memoizedState : { current: init2 };
467
+ });
468
+ define("useMemo", (fn) => {
469
+ const n = record("useMemo", 1);
470
+ const s = memoized(n);
471
+ return Array.isArray(s) ? s[0] : typeof fn === "function" ? fn() : void 0;
472
+ });
473
+ define("useCallback", (fn) => {
474
+ const n = record("useCallback", 1);
475
+ const s = memoized(n);
476
+ return Array.isArray(s) ? s[0] : fn;
477
+ });
478
+ for (const effect of ["useEffect", "useLayoutEffect", "useInsertionEffect", "useImperativeHandle"]) define(effect, () => void record(effect, 1));
479
+ define("useContext", (ctx) => {
480
+ record("useContext", 0);
481
+ const c = ctx;
482
+ return c ? c._currentValue : void 0;
483
+ });
484
+ define("useDebugValue", () => void record("useDebugValue", 0));
485
+ define("useSyncExternalStore", (_subscribe, getSnapshot) => {
486
+ const n = record("useSyncExternalStore", 2);
487
+ return n ? n.memoizedState : typeof getSnapshot === "function" ? getSnapshot() : void 0;
488
+ });
489
+ define("useTransition", () => {
490
+ record("useTransition", 2);
491
+ return [false, noop];
492
+ });
493
+ define("useDeferredValue", (v) => {
494
+ const n = record("useDeferredValue", 1);
495
+ return n ? n.memoizedState : v;
496
+ });
497
+ define("useId", () => {
498
+ const n = record("useId", 1);
499
+ return n ? n.memoizedState : "id";
500
+ });
501
+ define("useOptimistic", (v) => {
502
+ const n = record("useOptimistic", 1);
503
+ return [n ? n.memoizedState : v, noop];
504
+ });
505
+ for (const action of ["useActionState", "useFormState"]) {
506
+ define(action, (_fn, init2) => {
507
+ const n = record(action, 3);
508
+ return [n ? n.memoizedState : init2, noop, false];
509
+ });
510
+ }
511
+ define("useEffectEvent", (fn) => {
512
+ record("useEffectEvent", 1);
513
+ return fn;
514
+ });
515
+ define("useCacheRefresh", () => {
516
+ record("useCacheRefresh", 1);
517
+ return noop;
518
+ });
519
+ define("useHostTransitionStatus", () => ({ pending: false, data: null, method: null, action: null }));
520
+ define("useMemoCache", (size) => new Array(Number(size) || 0).fill(/* @__PURE__ */ Symbol.for("react.memo_cache_sentinel")));
521
+ define("use", (usable) => {
522
+ record("use", 0);
523
+ const u = usable;
524
+ if (u && typeof u.then === "function") {
525
+ if (u.status === "fulfilled") return u.value;
526
+ throw new Error("rerender-lens: cannot replay use() on a pending promise");
527
+ }
528
+ return u ? u._currentValue : void 0;
529
+ });
530
+ define("readContext", (ctx) => ctx?._currentValue);
531
+ return { dispatcher, nodesConsumed: () => index };
532
+ }
533
+ function renderFunctionOf(type) {
534
+ let t = type;
535
+ while (t && typeof t === "object" && t.type) t = t.type;
536
+ if (t && typeof t === "object" && typeof t.render === "function") return { fn: t.render, name: t.render.name || t.displayName || "Component" };
537
+ if (typeof t === "function") return { fn: t, name: t.name || "Component" };
538
+ return { fn: null, name: "Component" };
539
+ }
540
+ function resolveHookNames(fiber, ref) {
541
+ const type = fiber.type;
542
+ if (!type || !ref) return /* @__PURE__ */ new Map();
543
+ const cached = cache.get(type);
544
+ if (cached) return cached.byNode;
545
+ const result = { byNode: /* @__PURE__ */ new Map(), ok: false };
546
+ cache.set(type, result);
547
+ const { fn, name } = renderFunctionOf(type);
548
+ if (!fn) return result.byNode;
549
+ const useH = "H" in ref;
550
+ const marker = "__rl_";
551
+ const previous = useH ? ref.H : ref.current;
552
+ const { dispatcher, nodesConsumed } = makeDispatcher(fiber, name, result.byNode, marker);
553
+ const proxy = new Proxy(dispatcher, {
554
+ get(target, key) {
555
+ if (key in target) return target[key];
556
+ return () => void 0;
557
+ }
558
+ });
559
+ let total = 0;
560
+ for (let n = fiber.memoizedState; n && typeof n === "object" && "next" in n; n = n.next) total++;
561
+ try {
562
+ if (useH) ref.H = proxy;
563
+ else ref.current = proxy;
564
+ const secondArg = fiber.tag === 11 ? fiber.stateNode?.ref ?? null : void 0;
565
+ fn(fiber.memoizedProps ?? {}, secondArg);
566
+ result.ok = nodesConsumed() === total;
567
+ if (!result.ok) result.byNode.clear();
568
+ } catch {
569
+ result.byNode.clear();
570
+ } finally {
571
+ if (useH) ref.H = previous;
572
+ else ref.current = previous;
573
+ }
574
+ return result.byNode;
575
+ }
576
+
340
577
  // src/fiber.ts
341
578
  var FunctionComponent = 0;
342
579
  var ClassComponent = 1;
@@ -344,10 +581,12 @@ var HostRoot = 3;
344
581
  var HostComponent = 5;
345
582
  var ContextProvider = 10;
346
583
  var ForwardRef = 11;
584
+ var SuspenseComponent = 13;
347
585
  var MemoComponent = 14;
348
586
  var SimpleMemoComponent = 15;
349
587
  var PerformedWork = 1;
350
588
  var HOOK = "__REACT_DEVTOOLS_GLOBAL_HOOK__";
589
+ var nowMs = () => typeof performance !== "undefined" && typeof performance.now === "function" ? performance.now() : Date.now();
351
590
  function ensureDevtoolsHook() {
352
591
  const g = globalThis;
353
592
  let hook = g[HOOK];
@@ -376,7 +615,9 @@ function ensureDevtoolsHook() {
376
615
  return hook;
377
616
  }
378
617
  var capturedRenderers = /* @__PURE__ */ new Map();
618
+ var dispatcherRefs = /* @__PURE__ */ new Map();
379
619
  var INJECT_WRAPPED = /* @__PURE__ */ Symbol.for("rerender-lens.injectWrapped");
620
+ var ATTACHED = /* @__PURE__ */ Symbol.for("rerender-lens.attached");
380
621
  function pickRenderer(r) {
381
622
  const info = r ?? {};
382
623
  return { version: info.version, bundleType: info.bundleType, rendererPackageName: info.rendererPackageName };
@@ -387,20 +628,45 @@ function wrapInject(hook) {
387
628
  const original = hook.inject;
388
629
  hook.inject = function(renderer) {
389
630
  const id = original.call(this, renderer);
390
- capturedRenderers.set(typeof id === "number" ? id : capturedRenderers.size + 1, pickRenderer(renderer));
631
+ const key = typeof id === "number" ? id : capturedRenderers.size + 1;
632
+ capturedRenderers.set(key, pickRenderer(renderer));
633
+ const ref = renderer?.currentDispatcherRef;
634
+ if (ref && typeof ref === "object") dispatcherRefs.set(key, ref);
391
635
  return id;
392
636
  };
393
637
  h[INJECT_WRAPPED] = true;
394
638
  }
639
+ function getDispatcherRef() {
640
+ for (const ref of dispatcherRefs.values()) return ref;
641
+ const g = globalThis;
642
+ const hook = g[HOOK];
643
+ let found = null;
644
+ if (hook && hook.renderers && typeof hook.renderers.forEach === "function") {
645
+ hook.renderers.forEach((r) => {
646
+ const ref = r?.currentDispatcherRef;
647
+ if (!found && ref && typeof ref === "object") found = ref;
648
+ });
649
+ }
650
+ return found;
651
+ }
395
652
  function attach() {
396
653
  const hook = ensureDevtoolsHook();
397
654
  wrapInject(hook);
655
+ if (hook[ATTACHED]) return () => {
656
+ };
657
+ hook[ATTACHED] = true;
398
658
  const previous = hook.onCommitFiberRoot;
399
659
  const patched = function(id, root, priority, didError) {
660
+ const started = nowMs();
400
661
  try {
401
662
  onCommit(root, priorityLabel(priority));
402
663
  } catch (err) {
403
664
  warnOnce("commit", `failed to inspect a commit: ${String(err)}`);
665
+ } finally {
666
+ const spent = nowMs() - started;
667
+ const s = getState();
668
+ s.overheadMs += spent;
669
+ if (spent > s.maxCommitMs) s.maxCommitMs = spent;
404
670
  }
405
671
  if (typeof previous === "function") return previous.call(this, id, root, priority, didError);
406
672
  };
@@ -412,6 +678,7 @@ function attach() {
412
678
  };
413
679
  hook.onScheduleFiberRoot = patchedSchedule;
414
680
  return () => {
681
+ hook[ATTACHED] = false;
415
682
  if (hook.onCommitFiberRoot === patched) hook.onCommitFiberRoot = previous;
416
683
  if (hook.onScheduleFiberRoot === patchedSchedule) hook.onScheduleFiberRoot = previousSchedule;
417
684
  };
@@ -480,11 +747,14 @@ var counts = /* @__PURE__ */ new WeakMap();
480
747
  var ids = /* @__PURE__ */ new WeakMap();
481
748
  var fibersById = /* @__PURE__ */ new Map();
482
749
  var hasWeakRef = typeof WeakRef === "function";
750
+ var pruneAt = 5e3;
483
751
  function remember2(id, fiber) {
484
752
  if (!hasWeakRef) return;
753
+ if (fibersById.get(id)?.deref() === fiber) return;
485
754
  fibersById.set(id, new WeakRef(fiber));
486
- if (fibersById.size > 5e3) {
755
+ if (fibersById.size > pruneAt) {
487
756
  for (const [k, ref] of fibersById) if (!ref.deref()) fibersById.delete(k);
757
+ pruneAt = Math.max(5e3, fibersById.size * 2);
488
758
  }
489
759
  }
490
760
  function instanceIdOf(fiber) {
@@ -539,7 +809,7 @@ function nearestComponent(fiber) {
539
809
  }
540
810
  var INTERNAL_FRAME = /react-dom|react_jsx|jsx-(dev-)?runtime|\/react\/|node_modules\/react|react-stack-bottom-frame|react_stack_bottom_frame|scheduler/;
541
811
  function parseStackLocation(stack) {
542
- for (const line of stack.split("\n")) {
812
+ for (const line of stack.split("\n", 24)) {
543
813
  const m = /(?:at\s+(?:.*?\s+)?\(?|@)?((?:https?|file|webpack|vite|blob):[^\s()]+?):(\d+):(\d+)\)?\s*$/.exec(line.trim());
544
814
  if (!m || !m[1] || INTERNAL_FRAME.test(m[1])) continue;
545
815
  return { fileName: m[1], lineNumber: Number(m[2]), columnNumber: Number(m[3]) };
@@ -555,9 +825,16 @@ function sourceOf(fiber) {
555
825
  return out;
556
826
  }
557
827
  const st = fiber._debugStack;
558
- const text = typeof st === "string" ? st : st && typeof st.stack === "string" ? st.stack : null;
559
- return text ? parseStackLocation(text) : void 0;
828
+ if (st && typeof st === "object") {
829
+ if (sourceCache.has(st)) return sourceCache.get(st);
830
+ const text = typeof st.stack === "string" ? st.stack : null;
831
+ const out = text ? parseStackLocation(text) : void 0;
832
+ sourceCache.set(st, out);
833
+ return out;
834
+ }
835
+ return typeof st === "string" ? parseStackLocation(st) : void 0;
560
836
  }
837
+ var sourceCache = /* @__PURE__ */ new WeakMap();
561
838
  function bumpCount(fiber) {
562
839
  const prev = counts.get(fiber) ?? (fiber.alternate ? counts.get(fiber.alternate) : void 0) ?? 0;
563
840
  const n = prev + 1;
@@ -581,7 +858,7 @@ function ownerName(fiber) {
581
858
  }
582
859
  var isStateNode = (n) => !!n.queue && typeof n.queue.lastRenderedReducer === "function";
583
860
  var isStoreNode = (n) => !!n.queue && typeof n.queue.getSnapshot === "function";
584
- function hookLabel(node) {
861
+ function hookLabel2(node) {
585
862
  if (isStoreNode(node)) return "useSyncExternalStore";
586
863
  if (isStateNode(node)) {
587
864
  const r = node.queue.lastRenderedReducer;
@@ -590,20 +867,43 @@ function hookLabel(node) {
590
867
  return "state";
591
868
  }
592
869
  var NODELESS_HOOKS = /* @__PURE__ */ new Set(["useContext", "useDebugValue", "use"]);
870
+ function hookLabelsFor(fiber, first) {
871
+ const types = fiber._debugHookTypes?.filter((t) => !NODELESS_HOOKS.has(t));
872
+ let nodeCount = 0;
873
+ for (let n = first; n; n = n.next) nodeCount++;
874
+ return types && types.length === nodeCount ? types : null;
875
+ }
876
+ var isHookList = (v) => !!v && typeof v === "object" && "next" in v;
877
+ function snapshotHooks(fiber) {
878
+ const out = [];
879
+ if (fiber.tag === ClassComponent) return out;
880
+ const first = fiber.memoizedState;
881
+ if (!isHookList(first)) return out;
882
+ const labels = hookLabelsFor(fiber, first);
883
+ let i = 0;
884
+ for (let n = first; n; n = n.next, i++) {
885
+ if (!isStateNode(n) && !isStoreNode(n)) continue;
886
+ const hook = labels?.[i] ?? hookLabel2(n);
887
+ out.push({ path: `${hook}#${i}`, hook, index: i, value: n.memoizedState });
888
+ }
889
+ return out;
890
+ }
891
+ function snapshotContexts(fiber) {
892
+ const out = [];
893
+ for (let d = fiber.dependencies?.firstContext ?? null; d; d = d.next) out.push({ name: d.context.displayName ?? "Context", value: d.memoizedValue });
894
+ return out;
895
+ }
593
896
  function diffHooks(fiber, alt) {
594
897
  const out = [];
595
898
  if (fiber.tag === ClassComponent) return out;
596
899
  let a = alt.memoizedState;
597
900
  let b = fiber.memoizedState;
598
901
  if (!a || !b || typeof b !== "object" || !("next" in b)) return out;
599
- const types = fiber._debugHookTypes?.filter((t) => !NODELESS_HOOKS.has(t));
600
- let nodeCount = 0;
601
- for (let n = b; n; n = n.next) nodeCount++;
602
- const labels = types && types.length === nodeCount ? types : null;
902
+ const labels = hookLabelsFor(fiber, b);
603
903
  let i = 0;
604
904
  while (a && b) {
605
905
  if ((isStateNode(b) || isStoreNode(b)) && !Object.is(a.memoizedState, b.memoizedState)) {
606
- const hook = labels?.[i] ?? hookLabel(b);
906
+ const hook = labels?.[i] ?? hookLabel2(b);
607
907
  out.push({
608
908
  path: `${hook}#${i}`,
609
909
  hook,
@@ -692,14 +992,14 @@ function analyze(fiber, alt, trackHooks) {
692
992
  const trigger = causes.length === 0 ? "parent" : causes.length === 1 ? causes[0] : "mixed";
693
993
  return { propChanges, stateChanges, hookChanges, trigger };
694
994
  }
695
- function nearestRenderedAncestor(fiber, cache, trackHooks) {
995
+ function nearestRenderedAncestor(fiber, cache2, trackHooks) {
696
996
  let f = fiber.return;
697
997
  while (f && f.tag !== HostRoot) {
698
998
  if (isComponentTag(f.tag) && f.alternate && didRender(f)) {
699
- let info = cache.get(f);
999
+ let info = cache2.get(f);
700
1000
  if (info === void 0) {
701
1001
  info = { name: fiberName(f), trigger: analyze(f, f.alternate, trackHooks).trigger };
702
- cache.set(f, info);
1002
+ cache2.set(f, info);
703
1003
  }
704
1004
  return info;
705
1005
  }
@@ -707,6 +1007,23 @@ function nearestRenderedAncestor(fiber, cache, trackHooks) {
707
1007
  }
708
1008
  return null;
709
1009
  }
1010
+ function updatersOf(root) {
1011
+ const set = root.memoizedUpdaters;
1012
+ if (!set || typeof set.forEach !== "function") return [];
1013
+ const names = [];
1014
+ set.forEach((f) => {
1015
+ const comp = nearestComponent(f);
1016
+ if (comp) {
1017
+ const name = fiberName(comp);
1018
+ if (!names.includes(name)) names.push(name);
1019
+ }
1020
+ });
1021
+ return names;
1022
+ }
1023
+ var lastCommit = null;
1024
+ var EFFECT_LOOP_WINDOW_MS = 50;
1025
+ var MAX_REPORTS_PER_COMMIT = 200;
1026
+ var COMMIT_TIME_BUDGET_MS = 25;
710
1027
  function onCommit(root, commitPriority) {
711
1028
  const s = getState();
712
1029
  if (!s.enabled) return;
@@ -714,48 +1031,95 @@ function onCommit(root, commitPriority) {
714
1031
  const o = s.options;
715
1032
  const trackHooks = o.trackHooks !== false;
716
1033
  const rendered = [];
1034
+ const renderedNames = /* @__PURE__ */ new Set();
717
1035
  let hot = false;
1036
+ let suspenseResolved = false;
718
1037
  const stack = [root.current];
719
1038
  while (stack.length) {
720
1039
  const fiber = stack.pop();
721
1040
  const alt = fiber.alternate;
722
1041
  if (alt && isComponentTag(fiber.tag) && didRender(fiber)) {
723
1042
  if (isHotSwapped(fiber, alt)) hot = true;
1043
+ renderedNames.add(fiberName(fiber));
724
1044
  if (shouldTrack(fiberType(fiber), o)) rendered.push(fiber);
725
1045
  }
1046
+ if (fiber.tag === SuspenseComponent && alt && alt.memoizedState !== null && fiber.memoizedState === null) suspenseResolved = true;
726
1047
  if (!alt || fiber.child !== alt.child) {
727
1048
  for (let c = fiber.child; c; c = c.sibling) stack.push(c);
728
1049
  }
729
1050
  }
730
1051
  if (hot && o.ignoreHotReload !== false) return;
731
1052
  rendered.reverse();
732
- if (rendered.length === 0) return;
1053
+ const at = nowMs();
1054
+ const updaters = updatersOf(root);
1055
+ let commitCause;
1056
+ let afterCommit;
1057
+ const inputDriven = commitPriority === "immediate" || commitPriority === "user-blocking";
1058
+ if (!inputDriven && lastCommit && at - lastCommit.at < EFFECT_LOOP_WINDOW_MS && updaters.some((u) => lastCommit.rendered.has(u))) {
1059
+ commitCause = "effect-after-commit";
1060
+ afterCommit = lastCommit.id;
1061
+ } else if (suspenseResolved) commitCause = "suspense-resolved";
1062
+ if (rendered.length === 0) {
1063
+ lastCommit = { id: s.nextCommitId, at, rendered: renderedNames };
1064
+ return;
1065
+ }
733
1066
  const commitId = s.nextCommitId++;
1067
+ lastCommit = { id: commitId, at, rendered: renderedNames };
1068
+ const dispatcherRef = o.resolveHookNames ? getDispatcherRef() : null;
734
1069
  const parentCache = /* @__PURE__ */ new Map();
735
- for (const fiber of rendered) {
736
- const alt = fiber.alternate;
737
- const a = analyze(fiber, alt, trackHooks);
738
- const durations = durationsOf(fiber);
739
- const report = buildReport({
740
- component: fiberName(fiber),
741
- instanceId: instanceId(fiber),
742
- renderCount: bumpCount(fiber),
743
- prevProps: alt.memoizedProps ?? {},
744
- nextProps: fiber.memoizedProps ?? {},
745
- propChanges: a.propChanges,
746
- stateChanges: a.stateChanges,
747
- hookChanges: a.hookChanges,
748
- parent: a.trigger === "parent" ? nearestRenderedAncestor(fiber, parentCache, trackHooks) : null,
749
- owner: ownerName(fiber),
750
- path: componentPath(fiber),
751
- memoized: isMemoizedFiber(fiber),
752
- selfDuration: durations ? durations.self : void 0,
753
- treeDuration: durations ? durations.tree : void 0,
754
- commitId,
755
- commitPriority,
756
- source: sourceOf(fiber)
757
- });
758
- dispatch(report);
1070
+ const endDiffScope = beginDiffScope();
1071
+ const deadline = at + COMMIT_TIME_BUDGET_MS;
1072
+ try {
1073
+ for (let i = 0; i < rendered.length; i++) {
1074
+ if (i >= MAX_REPORTS_PER_COMMIT || (i & 15) === 15 && nowMs() > deadline) {
1075
+ s.truncated += rendered.length - i;
1076
+ warnOnce("truncated", `a commit rendered ${rendered.length} tracked components; only ${i} were reported (see info().truncated). Narrow \`include\` or turn off includeState.`);
1077
+ break;
1078
+ }
1079
+ const fiber = rendered[i];
1080
+ const alt = fiber.alternate;
1081
+ const a = analyze(fiber, alt, trackHooks);
1082
+ const durations = durationsOf(fiber);
1083
+ let hookState = o.includeState !== false && trackHooks ? snapshotHooks(fiber) : void 0;
1084
+ if (dispatcherRef && fiber.tag !== ClassComponent && (a.hookChanges.length || hookState && hookState.length)) {
1085
+ const names = resolveHookNames(fiber, dispatcherRef);
1086
+ if (names.size) {
1087
+ for (const c of a.hookChanges) if (c.hook !== "useContext" && names.has(c.index)) c.custom = names.get(c.index);
1088
+ if (hookState) hookState = hookState.map((h) => names.has(h.index) ? { ...h, custom: names.get(h.index) } : h);
1089
+ }
1090
+ }
1091
+ const report = buildReport({
1092
+ component: fiberName(fiber),
1093
+ instanceId: instanceId(fiber),
1094
+ renderCount: bumpCount(fiber),
1095
+ prevProps: alt.memoizedProps ?? {},
1096
+ nextProps: fiber.memoizedProps ?? {},
1097
+ propChanges: a.propChanges,
1098
+ stateChanges: a.stateChanges,
1099
+ hookChanges: a.hookChanges,
1100
+ ...o.includeState !== false ? {
1101
+ hookState,
1102
+ contexts: trackHooks ? snapshotContexts(fiber) : void 0,
1103
+ state: fiber.tag === ClassComponent && fiber.memoizedState && typeof fiber.memoizedState === "object" ? fiber.memoizedState : void 0
1104
+ } : {},
1105
+ updaters,
1106
+ commitCause,
1107
+ afterCommit,
1108
+ key: fiber.key === null || fiber.key === void 0 ? null : String(fiber.key),
1109
+ parent: a.trigger === "parent" ? nearestRenderedAncestor(fiber, parentCache, trackHooks) : null,
1110
+ owner: ownerName(fiber),
1111
+ path: componentPath(fiber),
1112
+ memoized: isMemoizedFiber(fiber),
1113
+ selfDuration: durations ? durations.self : void 0,
1114
+ treeDuration: durations ? durations.tree : void 0,
1115
+ commitId,
1116
+ commitPriority,
1117
+ source: sourceOf(fiber)
1118
+ });
1119
+ dispatch(report);
1120
+ }
1121
+ } finally {
1122
+ endDiffScope();
759
1123
  }
760
1124
  }
761
1125
 
@@ -857,6 +1221,201 @@ function useWhyRerender(name, values, options) {
857
1221
  });
858
1222
  }
859
1223
 
1224
+ // src/fixes.ts
1225
+ var AVOIDABLE = /* @__PURE__ */ new Set(["deep-equal", "function", "element"]);
1226
+ var rootOf = (path) => path.split(/[.[]/)[0] || path;
1227
+ function fixesFor(r) {
1228
+ const out = [];
1229
+ const owner = r.owner || r.parent && r.parent.name || "?";
1230
+ const changes = [...r.propChanges, ...r.stateChanges, ...r.hookChanges];
1231
+ if (r.avoidable && (changes.length === 0 || r.memoized === false)) {
1232
+ out.push({
1233
+ kind: "memo",
1234
+ owner: r.component,
1235
+ target: r.component,
1236
+ prop: null,
1237
+ label: `Wrap <${r.component}> in React.memo`,
1238
+ detail: changes.length === 0 ? `<${r.component}> re-rendered with identical props because <${r.parent && r.parent.name || "its parent"}> re-rendered.` : `<${r.component}> is not memoized: fixing its props alone will not stop the re-render.`
1239
+ });
1240
+ }
1241
+ for (const c of r.propChanges) {
1242
+ if (!AVOIDABLE.has(c.kind)) continue;
1243
+ const root = rootOf(c.path);
1244
+ if (root === "children" && (c.kind === "element" || c.kind === "deep-equal")) {
1245
+ out.push({ kind: "children", owner, target: r.component, prop: "children", label: `memoize children of <${r.component}> in <${owner}>`, detail: `<${owner}> re-creates the children of <${r.component}> on every render.` });
1246
+ } else if (c.kind === "function") {
1247
+ out.push({ kind: "useCallback", owner, target: r.component, prop: root, label: `useCallback(${root}) in <${owner}>`, detail: `prop "${c.path}" of <${r.component}> is a new function on every render of <${owner}>.` });
1248
+ } else if (c.kind === "element") {
1249
+ out.push({ kind: "useMemoElement", owner, target: r.component, prop: root, label: `memoize element prop ${root} in <${owner}>`, detail: `prop "${c.path}" of <${r.component}> is a new element with the same type and props on every render of <${owner}>.` });
1250
+ } else {
1251
+ out.push({ kind: "useMemo", owner, target: r.component, prop: root, label: `useMemo(${root}) in <${owner}>`, detail: `prop "${c.path}" of <${r.component}> is a new ${Array.isArray(c.next) ? "array" : "object"} with the same contents on every render of <${owner}>.` });
1252
+ }
1253
+ }
1254
+ for (const c of [...r.stateChanges, ...r.hookChanges]) {
1255
+ const isContext = c.hook === "useContext";
1256
+ const ctxName = isContext ? (/useContext\((.*)\)/.exec(c.path) || [])[1] || "Context" : "";
1257
+ const providerOwner = isContext && c.provider && c.provider.component ? c.provider.component : null;
1258
+ if (isContext && c.kind === "different" && c.changedKeys && typeof c.totalKeys === "number" && c.changedKeys.length > 0 && c.changedKeys.length < c.totalKeys) {
1259
+ out.push({
1260
+ kind: "splitContext",
1261
+ owner: providerOwner || `${ctxName}.Provider`,
1262
+ target: r.component,
1263
+ prop: ctxName,
1264
+ label: `split ${ctxName}${providerOwner ? ` in <${providerOwner}>` : ""}: only ${c.changedKeys.join(", ")} changed`,
1265
+ detail: `${c.changedKeys.length} of ${c.totalKeys} keys changed in ${ctxName}, yet every consumer re-rendered.`
1266
+ });
1267
+ continue;
1268
+ }
1269
+ if (!AVOIDABLE.has(c.kind)) continue;
1270
+ if (isContext) {
1271
+ out.push({ kind: "contextValue", owner: providerOwner || `${ctxName}.Provider`, target: r.component, prop: ctxName, label: `memoize the ${ctxName} provider value${providerOwner ? ` in <${providerOwner}>` : ""}`, detail: `${ctxName} produced a new value that is deep-equal to the previous one.` });
1272
+ } else if (c.hook === "useSyncExternalStore") {
1273
+ const chain = c.custom || [];
1274
+ const redux = chain.some((n) => /^use(App)?Selector$/.test(n));
1275
+ const zustand = !redux && chain.some((n) => /^use[A-Z]\w*Store$/.test(n) || n === "useStore" || n === "useBoundStore");
1276
+ out.push({ kind: "storeSnapshot", owner: r.component, target: r.component, prop: c.path, label: redux ? `memoize the selector in <${r.component}>` : zustand ? `useShallow in <${r.component}>` : `stable getSnapshot in <${r.component}>`, detail: storeAdvice(c.custom) });
1277
+ } else {
1278
+ out.push({ kind: "bailout", owner: r.component, target: r.component, prop: c.path, label: `bail out before setting ${c.path} in <${r.component}>`, detail: `${c.path} was set to a value deep-equal to the current one (new reference).` });
1279
+ }
1280
+ }
1281
+ return out;
1282
+ }
1283
+ var fixKey = (f) => `${f.kind}|${f.owner}|${f.prop || f.target}`;
1284
+ function rankFixes(reports) {
1285
+ const byKey = /* @__PURE__ */ new Map();
1286
+ for (const r of reports) {
1287
+ if (!r.avoidable) continue;
1288
+ for (const f of fixesFor(r)) {
1289
+ const key = fixKey(f);
1290
+ let agg = byKey.get(key);
1291
+ if (!agg) {
1292
+ agg = { ...f, key, count: 0, components: {} };
1293
+ byKey.set(key, agg);
1294
+ }
1295
+ agg.count++;
1296
+ agg.components[r.component] = (agg.components[r.component] || 0) + 1;
1297
+ }
1298
+ }
1299
+ return [...byKey.values()].sort((a, b) => b.count - a.count || a.label.localeCompare(b.label));
1300
+ }
1301
+ function formatFixes(reports, limit = 10) {
1302
+ const ranked = rankFixes(reports);
1303
+ if (!ranked.length) return "No avoidable re-renders.";
1304
+ const lines = ranked.slice(0, limit).map((f, i) => `${String(i + 1).padStart(2)}. ${f.label} (removes ${f.count}: ${Object.entries(f.components).map(([c, n]) => `<${c}>${n > 1 ? " x" + n : ""}`).join(", ")})`);
1305
+ if (ranked.length > limit) lines.push(` \u2026 ${ranked.length - limit} more`);
1306
+ return lines.join("\n");
1307
+ }
1308
+
1309
+ // src/sessions.ts
1310
+ function summarizeReports(reports, meta = {}) {
1311
+ var _a;
1312
+ const byComponent = {};
1313
+ let avoidable = 0;
1314
+ let wasted = 0;
1315
+ for (const r of reports) {
1316
+ const c = byComponent[_a = r.component] || (byComponent[_a] = { total: 0, avoidable: 0, wasted: 0 });
1317
+ c.total++;
1318
+ if (r.avoidable) {
1319
+ c.avoidable++;
1320
+ avoidable++;
1321
+ if (typeof r.selfDuration === "number") {
1322
+ c.wasted += r.selfDuration;
1323
+ wasted += r.selfDuration;
1324
+ }
1325
+ }
1326
+ }
1327
+ return {
1328
+ id: meta.id ?? `s${Date.now().toString(36)}`,
1329
+ name: meta.name ?? "session",
1330
+ startedAt: meta.startedAt ?? (reports[0]?.time ?? 0),
1331
+ endedAt: meta.endedAt ?? (reports[reports.length - 1]?.time ?? null),
1332
+ total: reports.length,
1333
+ avoidable,
1334
+ wasted,
1335
+ byComponent,
1336
+ fixes: rankFixes(reports).map((f) => ({ key: f.key, label: f.label, count: f.count }))
1337
+ };
1338
+ }
1339
+ function compareSummaries(before, after) {
1340
+ const names = /* @__PURE__ */ new Set([...Object.keys(before.byComponent), ...Object.keys(after.byComponent)]);
1341
+ const rows = [];
1342
+ for (const component of names) {
1343
+ const b = before.byComponent[component]?.avoidable || 0;
1344
+ const a = after.byComponent[component]?.avoidable || 0;
1345
+ if (b || a) rows.push({ component, before: b, after: a, delta: a - b });
1346
+ }
1347
+ rows.sort((x, y) => x.delta - y.delta || y.before - x.before || x.component.localeCompare(y.component));
1348
+ const afterKeys = new Set(after.fixes.map((f) => f.key));
1349
+ const beforeKeys = new Set(before.fixes.map((f) => f.key));
1350
+ return {
1351
+ before,
1352
+ after,
1353
+ rows,
1354
+ total: { before: before.total, after: after.total, delta: after.total - before.total },
1355
+ avoidable: { before: before.avoidable, after: after.avoidable, delta: after.avoidable - before.avoidable },
1356
+ wasted: { before: before.wasted, after: after.wasted, delta: after.wasted - before.wasted },
1357
+ resolvedFixes: before.fixes.filter((f) => !afterKeys.has(f.key)),
1358
+ newFixes: after.fixes.filter((f) => !beforeKeys.has(f.key)),
1359
+ regressions: rows.filter((r) => r.delta > 0)
1360
+ };
1361
+ }
1362
+ function formatComparison(c) {
1363
+ const w = Math.max(14, ...c.rows.map((r) => r.component.length + 2));
1364
+ const line = (label, b, a, d) => `${label.padEnd(w)} ${String(b).padStart(8)} ${String(a).padStart(8)} ${d.padStart(8)}`;
1365
+ const delta = (n, suffix = "") => n === 0 ? "\xB10" : `${n > 0 ? "+" : ""}${Number.isInteger(n) ? n : n.toFixed(1)}${suffix}`;
1366
+ const out = [line("avoidable", c.before.name.slice(0, 8), c.after.name.slice(0, 8), "\u0394"), line("all components", c.avoidable.before, c.avoidable.after, delta(c.avoidable.delta))];
1367
+ if (c.wasted.before || c.wasted.after) out.push(line("wasted ms", c.wasted.before.toFixed(1), c.wasted.after.toFixed(1), delta(Number(c.wasted.delta.toFixed(1)))));
1368
+ for (const r of c.rows) out.push(line(`<${r.component}>`, r.before, r.after, delta(r.delta)));
1369
+ if (c.resolvedFixes.length) out.push("", "no longer needed:", ...c.resolvedFixes.map((f) => ` - ${f.label}`));
1370
+ if (c.newFixes.length) out.push("", "new:", ...c.newFixes.map((f) => ` - ${f.label}`));
1371
+ return out.join("\n");
1372
+ }
1373
+ function parseExport(data) {
1374
+ if (Array.isArray(data)) return { reports: data, sessions: [], summary: null };
1375
+ if (data && typeof data === "object") {
1376
+ const d = data;
1377
+ if (Array.isArray(d.reports)) return { reports: d.reports, sessions: Array.isArray(d.sessions) ? d.sessions : [], summary: null };
1378
+ if (d.byComponent && typeof d.byComponent === "object") return { reports: [], sessions: [], summary: data };
1379
+ }
1380
+ throw new Error("not a rerender-lens export or session summary");
1381
+ }
1382
+
1383
+ // src/budget.ts
1384
+ function avoidableCounts(reports) {
1385
+ const counts2 = {};
1386
+ for (const r of reports) if (r.avoidable) counts2[r.component] = (counts2[r.component] || 0) + 1;
1387
+ return counts2;
1388
+ }
1389
+ function checkBudget(reports, budget) {
1390
+ const b = typeof budget === "number" ? { "*": budget } : budget;
1391
+ const fallback = typeof b["*"] === "number" ? b["*"] : 0;
1392
+ const counts2 = avoidableCounts(reports);
1393
+ const violations = [];
1394
+ const slack = [];
1395
+ const names = /* @__PURE__ */ new Set([...Object.keys(counts2), ...Object.keys(b).filter((k) => k !== "*")]);
1396
+ for (const component of names) {
1397
+ const allowed = typeof b[component] === "number" ? b[component] : fallback;
1398
+ const avoidable = counts2[component] || 0;
1399
+ if (avoidable > allowed) violations.push({ component, avoidable, allowed });
1400
+ else if (avoidable < allowed) slack.push({ component, avoidable, allowed });
1401
+ }
1402
+ violations.sort((x, y) => y.avoidable - y.allowed - (x.avoidable - x.allowed));
1403
+ return { ok: violations.length === 0, violations, slack, counts: counts2 };
1404
+ }
1405
+ function toBudget(reports) {
1406
+ return { "*": 0, ...avoidableCounts(reports) };
1407
+ }
1408
+ function assertWithinBudget(reports, budget) {
1409
+ const result = checkBudget(reports, budget);
1410
+ if (result.ok) return result;
1411
+ const lines = result.violations.map((v) => ` <${v.component}>: ${v.avoidable} avoidable re-render${v.avoidable === 1 ? "" : "s"} (budget ${v.allowed})`);
1412
+ throw new Error(`Re-render budget exceeded:
1413
+ ${lines.join("\n")}
1414
+
1415
+ Fixes, most impact first:
1416
+ ${formatFixes(reports)}`);
1417
+ }
1418
+
860
1419
  // src/notifiers.ts
861
1420
  function createCollector() {
862
1421
  const reports = [];
@@ -877,8 +1436,14 @@ function createCollector() {
877
1436
  const lines = bad.map((r) => ` <${r.component}> render #${r.renderCount}:
878
1437
  ${r.reasons.map((x) => ` - ${x}`).join("\n")}`);
879
1438
  throw new Error(`${bad.length} avoidable re-render${bad.length === 1 ? "" : "s"} detected:
880
- ${lines.join("\n")}`);
881
- }
1439
+ ${lines.join("\n")}
1440
+
1441
+ Fixes, most impact first:
1442
+ ${formatFixes(reports)}`);
1443
+ },
1444
+ fixes: () => rankFixes(reports),
1445
+ summary: (name) => summarizeReports(reports, name ? { name } : {}),
1446
+ assertWithinBudget: (budget) => assertWithinBudget(reports, budget)
882
1447
  };
883
1448
  }
884
1449
  function combineNotifiers(...notifiers) {
@@ -948,13 +1513,24 @@ function highlight(target) {
948
1513
  for (const s of l.sticky) s.remove();
949
1514
  l.sticky = [];
950
1515
  if (!target) return;
951
- for (const node of target.nodes) {
952
- const rect = node.getBoundingClientRect();
953
- if (rect.width === 0 && rect.height === 0) continue;
1516
+ const frag = document.createDocumentFragment();
1517
+ for (const rect of measure(target.nodes)) {
954
1518
  const b = box(rect, "#1a73e8", "rgba(26, 115, 232, 0.12)", target.label);
955
- l.root.appendChild(b);
1519
+ frag.appendChild(b);
956
1520
  l.sticky.push(b);
957
1521
  }
1522
+ l.root.appendChild(frag);
1523
+ }
1524
+ var MAX_BOXES = 100;
1525
+ function measure(nodes) {
1526
+ const rects = [];
1527
+ for (const node of nodes) {
1528
+ if (rects.length >= MAX_BOXES) break;
1529
+ const rect = node.getBoundingClientRect();
1530
+ if (rect.width === 0 && rect.height === 0) continue;
1531
+ rects.push(rect);
1532
+ }
1533
+ return rects;
958
1534
  }
959
1535
  function clearHighlight() {
960
1536
  highlight(null);
@@ -962,26 +1538,34 @@ function clearHighlight() {
962
1538
  function flash(target, duration = 500) {
963
1539
  const l = ensureLayer();
964
1540
  if (!l) return;
965
- for (const node of target.nodes) {
966
- const rect = node.getBoundingClientRect();
967
- if (rect.width === 0 && rect.height === 0) continue;
1541
+ const frag = document.createDocumentFragment();
1542
+ const boxes = [];
1543
+ for (const rect of measure(target.nodes)) {
968
1544
  const b = box(rect, "#d93025", "rgba(217, 48, 37, 0.15)");
969
1545
  b.style.transition = `opacity ${duration}ms ease-out`;
970
- l.root.appendChild(b);
971
- requestAnimationFrame(() => {
972
- b.style.opacity = "0";
973
- });
974
- setTimeout(() => b.remove(), duration + 50);
1546
+ frag.appendChild(b);
1547
+ boxes.push(b);
975
1548
  }
1549
+ if (!boxes.length) return;
1550
+ l.root.appendChild(frag);
1551
+ requestAnimationFrame(() => {
1552
+ for (const b of boxes) b.style.opacity = "0";
1553
+ });
1554
+ setTimeout(() => {
1555
+ for (const b of boxes) b.remove();
1556
+ }, duration + 50);
976
1557
  }
977
1558
 
978
1559
  // src/version.ts
979
- var VERSION = "0.2.0" ;
1560
+ var VERSION = "0.4.0" ;
980
1561
 
981
1562
  // src/devtools.ts
982
1563
  var DEVTOOLS_MARKER = "__rerenderLens";
983
1564
  var PROTOCOL_VERSION = 2;
984
- function serialize(value, maxDepth = 6, seen = /* @__PURE__ */ new WeakSet(), depth = 0) {
1565
+ var DEFAULT_CHANNEL = "rerender-lens";
1566
+ var SERIALIZE_MAX_ENTRIES = 100;
1567
+ var SERIALIZE_MAX_NODES = 2e4;
1568
+ function serialize(value, maxDepth = 4, seen = /* @__PURE__ */ new WeakSet(), depth = 0, budget = { nodes: SERIALIZE_MAX_NODES }) {
985
1569
  if (value === null || value === void 0) return value;
986
1570
  const t = typeof value;
987
1571
  if (t === "string" || t === "boolean") return value;
@@ -992,24 +1576,56 @@ function serialize(value, maxDepth = 6, seen = /* @__PURE__ */ new WeakSet(), de
992
1576
  const obj = value;
993
1577
  if (seen.has(obj)) return "[Circular]";
994
1578
  if (depth >= maxDepth) return "[\u2026]";
1579
+ if (--budget.nodes < 0) return "[\u2026]";
1580
+ const next = (v) => serialize(v, maxDepth, seen, depth + 1, budget);
995
1581
  seen.add(obj);
996
1582
  try {
997
1583
  if (isReactElement(obj)) {
998
1584
  const out2 = { $type: "element", name: getDisplayName(obj.type) };
999
1585
  if (obj.key !== null && obj.key !== void 0) out2.key = String(obj.key);
1000
- out2.props = serialize(obj.props, maxDepth, seen, depth + 1);
1586
+ out2.props = next(obj.props);
1001
1587
  return out2;
1002
1588
  }
1003
1589
  if (obj instanceof Date) return { $type: "Date", value: obj.toISOString() };
1004
1590
  if (obj instanceof RegExp) return { $type: "RegExp", value: String(obj) };
1591
+ if (ArrayBuffer.isView(obj) || obj instanceof ArrayBuffer) {
1592
+ const bin = obj;
1593
+ return { $type: bin.constructor?.name || "ArrayBuffer", length: typeof bin.length === "number" ? bin.length : bin.byteLength };
1594
+ }
1595
+ if (obj instanceof Promise) return "[Promise]";
1596
+ if (typeof Node !== "undefined" && obj instanceof Node) return typeof Element !== "undefined" && obj instanceof Element ? `<${obj.tagName.toLowerCase()}>` : `[${obj.nodeName}]`;
1597
+ if (obj === globalThis) return "[Window]";
1005
1598
  if (obj instanceof Map) {
1006
- return { $type: "Map", entries: [...obj].map(([k, v]) => [serialize(k, maxDepth, seen, depth + 1), serialize(v, maxDepth, seen, depth + 1)]) };
1599
+ const entries = [];
1600
+ for (const [k, v] of obj) {
1601
+ if (entries.length >= SERIALIZE_MAX_ENTRIES) {
1602
+ entries.push(["\u2026", `+${obj.size - SERIALIZE_MAX_ENTRIES} more`]);
1603
+ break;
1604
+ }
1605
+ entries.push([next(k), next(v)]);
1606
+ }
1607
+ return { $type: "Map", entries };
1608
+ }
1609
+ if (obj instanceof Set) {
1610
+ const values = [];
1611
+ for (const v of obj) {
1612
+ if (values.length >= SERIALIZE_MAX_ENTRIES) {
1613
+ values.push(`\u2026+${obj.size - SERIALIZE_MAX_ENTRIES} more`);
1614
+ break;
1615
+ }
1616
+ values.push(next(v));
1617
+ }
1618
+ return { $type: "Set", values };
1619
+ }
1620
+ if (Array.isArray(obj)) {
1621
+ const out2 = obj.slice(0, SERIALIZE_MAX_ENTRIES).map(next);
1622
+ if (obj.length > SERIALIZE_MAX_ENTRIES) out2.push(`\u2026+${obj.length - SERIALIZE_MAX_ENTRIES} more`);
1623
+ return out2;
1007
1624
  }
1008
- if (obj instanceof Set) return { $type: "Set", values: [...obj].map((v) => serialize(v, maxDepth, seen, depth + 1)) };
1009
- if (Array.isArray(obj)) return obj.map((v) => serialize(v, maxDepth, seen, depth + 1));
1010
- if (typeof Element !== "undefined" && obj instanceof Element) return `<${obj.tagName.toLowerCase()}>`;
1011
1625
  const out = {};
1012
- for (const k of Object.keys(obj)) out[k] = serialize(obj[k], maxDepth, seen, depth + 1);
1626
+ const keys = Object.keys(obj);
1627
+ for (const k of keys.slice(0, SERIALIZE_MAX_ENTRIES)) out[k] = next(obj[k]);
1628
+ if (keys.length > SERIALIZE_MAX_ENTRIES) out["\u2026"] = `+${keys.length - SERIALIZE_MAX_ENTRIES} more`;
1013
1629
  const proto = Object.getPrototypeOf(obj);
1014
1630
  if (proto && proto !== Object.prototype && proto.constructor?.name) out.$type = proto.constructor.name;
1015
1631
  return out;
@@ -1031,7 +1647,7 @@ var stringToMatcher = (s) => {
1031
1647
  };
1032
1648
  function serializeOptions(o) {
1033
1649
  const out = {};
1034
- const bools = ["trackAllMemoized", "trackAllComponents", "trackHooks", "logAll", "silent", "collapse", "ignoreHotReload"];
1650
+ const bools = ["trackAllMemoized", "trackAllComponents", "trackHooks", "includeState", "resolveHookNames", "logAll", "silent", "collapse", "ignoreHotReload"];
1035
1651
  for (const k of bools) if (typeof o[k] === "boolean") out[k] = o[k];
1036
1652
  if (typeof o.maxReportsPerComponent === "number") out.maxReportsPerComponent = o.maxReportsPerComponent;
1037
1653
  if (o.include) out.include = o.include.map(matcherToString).filter((x) => x !== null);
@@ -1040,7 +1656,7 @@ function serializeOptions(o) {
1040
1656
  }
1041
1657
  function deserializeOptions(o) {
1042
1658
  const out = {};
1043
- const bools = ["trackAllMemoized", "trackAllComponents", "trackHooks", "logAll", "silent", "collapse", "ignoreHotReload"];
1659
+ const bools = ["trackAllMemoized", "trackAllComponents", "trackHooks", "includeState", "resolveHookNames", "logAll", "silent", "collapse", "ignoreHotReload"];
1044
1660
  for (const k of bools) if (typeof o[k] === "boolean") out[k] = o[k];
1045
1661
  if (typeof o.maxReportsPerComponent === "number") out.maxReportsPerComponent = o.maxReportsPerComponent;
1046
1662
  if (Array.isArray(o.include)) out.include = o.include.filter((s) => typeof s === "string" && s).map(stringToMatcher);
@@ -1049,13 +1665,20 @@ function deserializeOptions(o) {
1049
1665
  }
1050
1666
  function createDevtoolsNotifier(options = {}) {
1051
1667
  const bufferSize = options.bufferSize ?? 300;
1052
- const maxDepth = options.maxDepth ?? 6;
1668
+ const maxDepth = options.maxDepth ?? 4;
1053
1669
  const target = options.target ?? (typeof window !== "undefined" ? window : void 0);
1054
1670
  const buffer = [];
1055
1671
  let seq = 0;
1056
1672
  let flashOn = options.flashAvoidable ?? false;
1673
+ let live = !target || typeof target.addEventListener !== "function";
1674
+ if (!live) {
1675
+ target.addEventListener("message", (event) => {
1676
+ const data = event.data;
1677
+ if ((event.source === target || !event.source) && data && data.__rerenderLensReady === true) live = true;
1678
+ });
1679
+ }
1057
1680
  const post = (type, payload) => {
1058
- if (!target) return;
1681
+ if (!target || type === "report" && !live) return;
1059
1682
  const msg = { [DEVTOOLS_MARKER]: true, version: PROTOCOL_VERSION, type, payload };
1060
1683
  target.postMessage(msg, "*");
1061
1684
  };
@@ -1071,10 +1694,13 @@ function createDevtoolsNotifier(options = {}) {
1071
1694
  source,
1072
1695
  injected: typeof window !== "undefined" && typeof window.__RERENDER_LENS_INJECTED__ === "string",
1073
1696
  scheduled: getState().scheduled,
1074
- commits: getState().commits
1697
+ commits: getState().commits,
1698
+ overhead: { totalMs: getState().overheadMs, maxCommitMs: getState().maxCommitMs },
1699
+ truncated: getState().truncated
1075
1700
  });
1076
1701
  const bridge = {
1077
1702
  replay: () => {
1703
+ live = true;
1078
1704
  post("hello", info());
1079
1705
  for (const e of buffer) post("report", e.payload);
1080
1706
  },
@@ -1136,12 +1762,126 @@ function createDevtoolsNotifier(options = {}) {
1136
1762
  }
1137
1763
  };
1138
1764
  if (typeof window !== "undefined") window.__RERENDER_LENS_DEVTOOLS__ = bridge;
1139
- post("hello", info());
1765
+ const runCommand = (data) => {
1766
+ const reply = { __rerenderLensReply: true, id: data.id };
1767
+ try {
1768
+ switch (data.cmd) {
1769
+ case "info":
1770
+ reply.result = info();
1771
+ break;
1772
+ case "pull":
1773
+ reply.result = bridge.pull(typeof data.arg === "number" ? data.arg : 0);
1774
+ break;
1775
+ case "replay":
1776
+ bridge.replay();
1777
+ reply.result = true;
1778
+ break;
1779
+ case "clear":
1780
+ bridge.clear();
1781
+ reply.result = true;
1782
+ break;
1783
+ case "configure":
1784
+ reply.result = bridge.configure(data.arg ?? {});
1785
+ break;
1786
+ case "highlight":
1787
+ reply.result = bridge.highlight(typeof data.arg === "number" ? data.arg : null);
1788
+ break;
1789
+ case "flash":
1790
+ bridge.flashAvoidable(!!data.arg);
1791
+ reply.result = true;
1792
+ break;
1793
+ default:
1794
+ reply.error = `unknown command ${String(data.cmd)}`;
1795
+ }
1796
+ } catch (e) {
1797
+ reply.error = String(e.message || e);
1798
+ }
1799
+ return reply;
1800
+ };
1801
+ const isCommand = (data) => !!data && typeof data === "object" && data.__rerenderLensCmd === true && typeof data.id === "string";
1802
+ const envelope = (type, payload) => ({ [DEVTOOLS_MARKER]: true, version: PROTOCOL_VERSION, type, payload });
1803
+ const sinks = [];
1804
+ if (options.channel && typeof BroadcastChannel === "function") {
1805
+ const name = typeof options.channel === "string" ? options.channel : DEFAULT_CHANNEL;
1806
+ try {
1807
+ const channel = new BroadcastChannel(name);
1808
+ channel.onmessage = (event) => {
1809
+ if (isCommand(event.data)) channel.postMessage(runCommand(event.data));
1810
+ };
1811
+ sinks.push((message) => {
1812
+ try {
1813
+ channel.postMessage(message);
1814
+ } catch {
1815
+ }
1816
+ });
1817
+ } catch {
1818
+ }
1819
+ }
1820
+ const relayUrl = options.relay ?? (typeof window !== "undefined" ? window.__RERENDER_LENS_RELAY__ : void 0);
1821
+ const ES = options.eventSource ?? (typeof EventSource === "function" ? EventSource : null);
1822
+ if (relayUrl && ES && typeof fetch === "function") {
1823
+ const base = relayUrl.replace(/\/$/, "");
1824
+ let queue = [];
1825
+ let scheduled = false;
1826
+ const flush = () => {
1827
+ scheduled = false;
1828
+ const batch = queue;
1829
+ queue = [];
1830
+ fetch(`${base}/message`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(batch), keepalive: true }).catch(() => {
1831
+ });
1832
+ };
1833
+ const relaySend = (message) => {
1834
+ queue.push(message);
1835
+ if (!scheduled) {
1836
+ scheduled = true;
1837
+ setTimeout(flush, 0);
1838
+ }
1839
+ };
1840
+ try {
1841
+ const stream = new ES(`${base}/events?role=app`);
1842
+ stream.onmessage = (event) => {
1843
+ let parsed;
1844
+ try {
1845
+ parsed = JSON.parse(event.data);
1846
+ } catch {
1847
+ return;
1848
+ }
1849
+ for (const m of Array.isArray(parsed) ? parsed : [parsed]) if (isCommand(m)) relaySend(runCommand(m));
1850
+ };
1851
+ stream.onerror = () => {
1852
+ };
1853
+ sinks.push(relaySend);
1854
+ } catch {
1855
+ }
1856
+ }
1857
+ const postAll = (type, payload) => {
1858
+ post(type, payload);
1859
+ if (sinks.length) {
1860
+ const message = envelope(type, payload);
1861
+ for (const sink of sinks) sink(message);
1862
+ }
1863
+ };
1864
+ if (sinks.length) {
1865
+ const replay = bridge.replay;
1866
+ const clear = bridge.clear;
1867
+ bridge.replay = () => {
1868
+ for (const sink of sinks) {
1869
+ sink(envelope("hello", info()));
1870
+ for (const e of buffer) sink(envelope("report", e.payload));
1871
+ }
1872
+ replay();
1873
+ };
1874
+ bridge.clear = () => {
1875
+ clear();
1876
+ for (const sink of sinks) sink(envelope("clear"));
1877
+ };
1878
+ }
1879
+ postAll("hello", info());
1140
1880
  return (report) => {
1141
1881
  const payload = serialize(report, maxDepth);
1142
1882
  buffer.push({ seq: ++seq, instanceId: report.instanceId, payload });
1143
1883
  if (buffer.length > bufferSize) buffer.splice(0, buffer.length - bufferSize);
1144
- post("report", payload);
1884
+ postAll("report", payload);
1145
1885
  if (flashOn && report.avoidable && report.instanceId) {
1146
1886
  const fiber = fiberById(report.instanceId);
1147
1887
  if (fiber) flash({ nodes: hostNodesOf(fiber), label: report.component });
@@ -1149,31 +1889,48 @@ function createDevtoolsNotifier(options = {}) {
1149
1889
  };
1150
1890
  }
1151
1891
 
1892
+ exports.DEFAULT_CHANNEL = DEFAULT_CHANNEL;
1152
1893
  exports.DEVTOOLS_MARKER = DEVTOOLS_MARKER;
1153
1894
  exports.MARKER = MARKER;
1154
1895
  exports.PROTOCOL_VERSION = PROTOCOL_VERSION;
1155
1896
  exports.VERSION = VERSION;
1897
+ exports.assertWithinBudget = assertWithinBudget;
1898
+ exports.avoidableCounts = avoidableCounts;
1156
1899
  exports.buildReport = buildReport;
1900
+ exports.checkBudget = checkBudget;
1157
1901
  exports.classify = classify;
1158
1902
  exports.combineNotifiers = combineNotifiers;
1903
+ exports.compareSummaries = compareSummaries;
1159
1904
  exports.configure = configure;
1160
1905
  exports.createCollector = createCollector;
1161
1906
  exports.createDevtoolsNotifier = createDevtoolsNotifier;
1907
+ exports.customHooksFromStack = customHooksFromStack;
1162
1908
  exports.deepEqual = deepEqual;
1163
1909
  exports.deserializeOptions = deserializeOptions;
1164
1910
  exports.diffRecords = diffRecords;
1165
1911
  exports.disable = disable;
1166
1912
  exports.ensureDevtoolsHook = ensureDevtoolsHook;
1913
+ exports.fixKey = fixKey;
1914
+ exports.fixesFor = fixesFor;
1915
+ exports.formatComparison = formatComparison;
1916
+ exports.formatFixes = formatFixes;
1167
1917
  exports.getDisplayName = getDisplayName;
1168
1918
  exports.getRenderers = getRenderers;
1919
+ exports.hookLabel = hookLabel;
1169
1920
  exports.init = init;
1170
1921
  exports.isEnabled = isEnabled;
1171
1922
  exports.isProductionReact = isProductionReact;
1923
+ exports.parseExport = parseExport;
1172
1924
  exports.printReport = printReport;
1925
+ exports.rankFixes = rankFixes;
1926
+ exports.resolveHookNames = resolveHookNames;
1173
1927
  exports.serialize = serialize;
1174
1928
  exports.serializeOptions = serializeOptions;
1175
1929
  exports.shouldTrack = shouldTrack;
1930
+ exports.storeAdvice = storeAdvice;
1176
1931
  exports.summarize = summarize;
1932
+ exports.summarizeReports = summarizeReports;
1933
+ exports.toBudget = toBudget;
1177
1934
  exports.track = track;
1178
1935
  exports.useWhyRerender = useWhyRerender;
1179
1936
  //# sourceMappingURL=index.cjs.map