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