rerender-lens 0.4.0 → 0.5.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 (66) hide show
  1. package/CHANGELOG.md +78 -0
  2. package/README.md +192 -362
  3. package/dist/{budget-COBu7jBU.d.cts → budget-BnJlYwrV.d.ts} +20 -10
  4. package/dist/{budget-LkNjGtRc.d.ts → budget-CRswn-nC.d.cts} +20 -10
  5. package/dist/cli.cjs +248 -31
  6. package/dist/cli.cjs.map +1 -1
  7. package/dist/cli.js +248 -31
  8. package/dist/cli.js.map +1 -1
  9. package/dist/{devtools-CZebzpF6.d.ts → devtools-BYWat7nQ.d.ts} +7 -1
  10. package/dist/{devtools-BkCct3cJ.d.cts → devtools-DJi_6AfJ.d.cts} +7 -1
  11. package/dist/index.cjs +439 -41
  12. package/dist/index.cjs.map +1 -1
  13. package/dist/index.d.cts +131 -8
  14. package/dist/index.d.ts +131 -8
  15. package/dist/index.js +424 -42
  16. package/dist/index.js.map +1 -1
  17. package/dist/jest-setup.cjs +1673 -0
  18. package/dist/jest-setup.cjs.map +1 -0
  19. package/dist/jest-setup.d.cts +8 -0
  20. package/dist/jest-setup.d.ts +8 -0
  21. package/dist/jest-setup.js +1671 -0
  22. package/dist/jest-setup.js.map +1 -0
  23. package/dist/jest.cjs +1760 -0
  24. package/dist/jest.cjs.map +1 -0
  25. package/dist/jest.d.cts +23 -0
  26. package/dist/jest.d.ts +23 -0
  27. package/dist/jest.js +1751 -0
  28. package/dist/jest.js.map +1 -0
  29. package/dist/{notifiers-BjjGSHpp.d.ts → notifiers-DwKlvv4H.d.cts} +8 -4
  30. package/dist/{notifiers-BGQtWKfX.d.cts → notifiers-FNzKHs0a.d.ts} +8 -4
  31. package/dist/playwright.cjs +230 -28
  32. package/dist/playwright.cjs.map +1 -1
  33. package/dist/playwright.d.cts +4 -4
  34. package/dist/playwright.d.ts +4 -4
  35. package/dist/playwright.js +230 -28
  36. package/dist/playwright.js.map +1 -1
  37. package/dist/relay.cjs +10 -2
  38. package/dist/relay.cjs.map +1 -1
  39. package/dist/relay.js +10 -2
  40. package/dist/relay.js.map +1 -1
  41. package/dist/rerender-lens.iife.js +356 -54
  42. package/dist/runner-report-B8Sc3FZO.d.cts +46 -0
  43. package/dist/runner-report-CW8X5RwK.d.ts +46 -0
  44. package/dist/setup.cjs +112 -27
  45. package/dist/setup.cjs.map +1 -1
  46. package/dist/setup.js +112 -27
  47. package/dist/setup.js.map +1 -1
  48. package/dist/{types-BzUEVkxJ.d.cts → types-DW5-N2XH.d.cts} +55 -2
  49. package/dist/{types-BzUEVkxJ.d.ts → types-DW5-N2XH.d.ts} +55 -2
  50. package/dist/vite.d.cts +1 -1
  51. package/dist/vite.d.ts +1 -1
  52. package/dist/vitest-setup.cjs +337 -41
  53. package/dist/vitest-setup.cjs.map +1 -1
  54. package/dist/vitest-setup.d.cts +4 -4
  55. package/dist/vitest-setup.d.ts +4 -4
  56. package/dist/vitest-setup.js +337 -41
  57. package/dist/vitest-setup.js.map +1 -1
  58. package/dist/vitest.cjs +400 -77
  59. package/dist/vitest.cjs.map +1 -1
  60. package/dist/vitest.d.cts +21 -39
  61. package/dist/vitest.d.ts +21 -39
  62. package/dist/vitest.js +401 -78
  63. package/dist/vitest.js.map +1 -1
  64. package/package.json +40 -4
  65. package/panel/panel.css +1 -0
  66. package/panel/panel.js +499 -309
package/dist/index.cjs CHANGED
@@ -127,6 +127,7 @@ function deepEqualObjects(a, b, seen) {
127
127
  }
128
128
  return false;
129
129
  }
130
+ var AVOIDABLE_KINDS = /* @__PURE__ */ new Set(["deep-equal", "function", "element"]);
130
131
  function classify(prev, next) {
131
132
  if (typeof prev === "function" && typeof next === "function") {
132
133
  return prev.name === next.name && prev.toString() === next.toString() ? "function" : "different";
@@ -163,30 +164,47 @@ function diffRecords(prev, next, basePath = "") {
163
164
  if (Object.is(a, b)) continue;
164
165
  const kind = classify(a, b);
165
166
  if (kind === "different") {
166
- changes.push({ path: diffBudgetExhausted() ? path : firstDifferentPath(a, b, path), kind, prev: a, next: b });
167
+ changes.push({ path: diffBudgetExhausted() ? path : firstDifferentPath(a, b, path) ?? path, kind, prev: a, next: b });
167
168
  } else {
168
169
  changes.push({ path, kind, prev: a, next: b });
169
170
  }
170
171
  }
171
172
  return changes;
172
173
  }
173
- function firstDifferentPath(a, b, path, depth = 0) {
174
- if (depth > 8) return path;
174
+ function firstDifferentPath(a, b, path = "", depth = 0) {
175
+ if (depth > 8) return path || "(value)";
175
176
  if (Array.isArray(a) && Array.isArray(b)) {
176
- if (a.length !== b.length) return `${path}.length`;
177
+ if (a.length !== b.length) return path ? `${path}.length` : "length";
177
178
  for (let i = 0; i < a.length; i++) {
178
179
  if (!deepEqual(a[i], b[i])) return firstDifferentPath(a[i], b[i], joinPath(path, i), depth + 1);
179
180
  }
180
- return path;
181
+ return null;
181
182
  }
182
183
  if (isPlainObject(a) && isPlainObject(b)) {
183
184
  const keys = /* @__PURE__ */ new Set([...Object.keys(a), ...Object.keys(b)]);
184
185
  for (const k of keys) {
185
186
  if (!deepEqual(a[k], b[k])) return firstDifferentPath(a[k], b[k], joinPath(path, k), depth + 1);
186
187
  }
187
- return path;
188
+ return null;
188
189
  }
189
- return path;
190
+ return deepEqual(a, b) ? null : path || "(value)";
191
+ }
192
+ function diffLeaves(a, b, limit = 20, path = "", out = []) {
193
+ if (out.length >= limit || Object.is(a, b)) return out;
194
+ if (Array.isArray(a) && Array.isArray(b)) {
195
+ const n = Math.max(a.length, b.length);
196
+ for (let i = 0; i < n && out.length < limit; i++) diffLeaves(a[i], b[i], limit, joinPath(path, i), out);
197
+ return out;
198
+ }
199
+ if (isPlainObject(a) && isPlainObject(b)) {
200
+ for (const k of /* @__PURE__ */ new Set([...Object.keys(a), ...Object.keys(b)])) {
201
+ if (out.length >= limit) break;
202
+ diffLeaves(a[k], b[k], limit, joinPath(path, k), out);
203
+ }
204
+ return out;
205
+ }
206
+ if (!deepEqual(a, b)) out.push({ path: path || "(value)", prev: a, next: b });
207
+ return out;
190
208
  }
191
209
 
192
210
  // src/report.ts
@@ -204,10 +222,12 @@ function storeAdvice(custom) {
204
222
  }
205
223
  var isGenuine = (c) => c.kind === "different" || c.kind === "added" || c.kind === "removed";
206
224
  var isChildren = (change) => change.path === "children";
225
+ var isRefObject = (change) => change.path === "ref" && change.kind === "deep-equal";
207
226
  function fixFor(change) {
208
227
  if (isChildren(change) && (change.kind === "element" || change.kind === "deep-equal")) {
209
228
  return `lift the children out of the parent's render: memoize them with useMemo, hoist static elements to module scope, or render them from a component that does not re-render`;
210
229
  }
230
+ if (isRefObject(change)) return `create the ref once with useRef (or createRef outside the render) and pass the same object every time`;
211
231
  switch (change.kind) {
212
232
  case "function":
213
233
  return `wrap it in useCallback (or hoist it out of the parent's render)`;
@@ -223,6 +243,7 @@ function describe(change) {
223
243
  if (isChildren(change) && (change.kind === "element" || change.kind === "deep-equal")) {
224
244
  return `children are new React elements with the same types and props on every render of the parent`;
225
245
  }
246
+ if (isRefObject(change)) return `prop "ref" is a new ref object on every render (React re-attaches it each time)`;
226
247
  switch (change.kind) {
227
248
  case "deep-equal":
228
249
  return `prop "${change.path}" is a new reference but deep-equal to the previous value`;
@@ -250,11 +271,21 @@ function buildReport(input) {
250
271
  if (causes.length === 0) trigger = "parent";
251
272
  else if (causes.length === 1) trigger = causes[0];
252
273
  else trigger = "mixed";
253
- const avoidable = trigger === "parent";
274
+ const nothingChanged = input.propChanges.length === 0 && stateChanges.length === 0 && hookChanges.length === 0;
275
+ const cacheHit = input.compiled === true && trigger === "parent" && input.propChanges.length === 0;
276
+ const revealed = input.revealed === true && trigger === "parent";
277
+ const avoidable = trigger === "parent" && !cacheHit && !revealed;
254
278
  const reasons = [];
255
- if (avoidable && input.propChanges.length === 0 && stateChanges.length === 0 && hookChanges.length === 0) {
279
+ const memoFix = input.classComponent ? `Make "${input.component}" extend PureComponent (or implement shouldComponentUpdate)` : `Wrap "${input.component}" in React.memo (or extend PureComponent)`;
280
+ if (cacheHit) {
281
+ reasons.push(`compiled by React Compiler: its output is memoized and the inputs did not change, so this render is cheap; nothing to fix.`);
282
+ } else if (revealed) {
283
+ reasons.push(
284
+ `re-rendered because the Suspense boundary above it switched from its fallback to content: React re-renders the content it kept hidden while the fallback was shown. A one-off; nothing to fix.`
285
+ );
286
+ } else if (avoidable && nothingChanged) {
256
287
  const who = input.parent ? `<${input.parent.name}> re-rendered (${describeTrigger(input.parent.trigger)})` : "its parent re-rendered";
257
- reasons.push(`re-rendered with identical props because ${who}. Wrap "${input.component}" in React.memo (or extend PureComponent).`);
288
+ reasons.push(`re-rendered with identical props because ${who}. ${memoFix}.`);
258
289
  } else if (avoidable && input.parent) {
259
290
  reasons.push(`caused by <${input.parent.name}> re-rendering (${describeTrigger(input.parent.trigger)}).`);
260
291
  }
@@ -263,9 +294,9 @@ function buildReport(input) {
263
294
  reasons.push(fix ? `${describe(c)}: ${fix}.` : `${describe(c)}.`);
264
295
  }
265
296
  const memoized = input.memoized !== false;
266
- if (avoidable && !memoized && input.propChanges.length > 0) {
297
+ if (avoidable && !memoized && !input.compiled && input.propChanges.length > 0) {
267
298
  reasons.push(
268
- `"${input.component}" is not memoized, so fixing the props alone will not stop this re-render: also wrap it in React.memo (or extend PureComponent).`
299
+ `"${input.component}" is not memoized, so fixing the props alone will not stop this re-render: also ${input.classComponent ? "extend PureComponent (or implement shouldComponentUpdate)" : "wrap it in React.memo (or extend PureComponent)"}.`
269
300
  );
270
301
  }
271
302
  for (const c of stateChanges) {
@@ -277,6 +308,8 @@ function buildReport(input) {
277
308
  const where = c.provider && c.provider.component ? ` (provided by <${c.provider.component}>)` : "";
278
309
  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` : "";
279
310
  reasons.push(`${c.path} changed${where}${keys}.`);
311
+ } else if (c.hook === "useDeferredValue" && isGenuine(c)) {
312
+ reasons.push(`${hookLabel(c)} caught up with its latest value: this is the deferred second render React schedules after the urgent one; nothing to fix.`);
280
313
  } else if (isGenuine(c)) reasons.push(`${hookLabel(c)} changed.`);
281
314
  else if (isStateHook(c))
282
315
  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.`);
@@ -307,6 +340,7 @@ function buildReport(input) {
307
340
  reasons,
308
341
  time: now()
309
342
  };
343
+ if (input.compiled) report.compiled = true;
310
344
  if (input.selfDuration !== void 0) report.selfDuration = input.selfDuration;
311
345
  if (input.treeDuration !== void 0) report.treeDuration = input.treeDuration;
312
346
  if (input.commitPriority) report.commitPriority = input.commitPriority;
@@ -344,10 +378,10 @@ var KIND_LABEL = {
344
378
  };
345
379
  function summarize(report) {
346
380
  const counts2 = /* @__PURE__ */ new Map();
347
- for (const c of [...report.propChanges, ...report.stateChanges, ...report.hookChanges]) {
381
+ for (const c of [...report.propChanges || [], ...report.stateChanges || [], ...report.hookChanges || []]) {
348
382
  counts2.set(c.kind, (counts2.get(c.kind) ?? 0) + 1);
349
383
  }
350
- const parts = [...counts2].map(([kind, n]) => `${n} ${KIND_LABEL[kind]}`);
384
+ const parts = [...counts2].map(([kind, n]) => `${n} ${KIND_LABEL[kind] || kind}`);
351
385
  if (parts.length === 0) parts.push("no changes");
352
386
  return parts.join(", ");
353
387
  }
@@ -410,6 +444,27 @@ function dispatch(report, override) {
410
444
  }
411
445
 
412
446
  // src/hookNames.ts
447
+ var NODE_COUNT = {
448
+ useState: 1,
449
+ useReducer: 1,
450
+ useRef: 1,
451
+ useMemo: 1,
452
+ useCallback: 1,
453
+ useEffect: 1,
454
+ useLayoutEffect: 1,
455
+ useInsertionEffect: 1,
456
+ useImperativeHandle: 1,
457
+ useSyncExternalStore: 2,
458
+ useTransition: 2,
459
+ useDeferredValue: 1,
460
+ useId: 1,
461
+ useOptimistic: 1,
462
+ useActionState: 3,
463
+ useFormState: 3,
464
+ useEffectEvent: 1,
465
+ useCacheRefresh: 1
466
+ };
467
+ var BUILTIN_HOOKS = /* @__PURE__ */ new Set([...Object.keys(NODE_COUNT), "useContext", "useDebugValue", "use"]);
413
468
  var noop = () => {
414
469
  };
415
470
  function customHooksFromStack(stack, componentName, marker) {
@@ -424,7 +479,7 @@ function customHooksFromStack(stack, componentName, marker) {
424
479
  continue;
425
480
  }
426
481
  if (name === componentName || name.endsWith("." + componentName)) break;
427
- if (/^use[A-Z0-9_]/.test(name)) names.push(name);
482
+ if (/^use[A-Z0-9_]/.test(name) && !BUILTIN_HOOKS.has(name)) names.push(name);
428
483
  if (names.length > 8) break;
429
484
  }
430
485
  return names;
@@ -724,6 +779,11 @@ function isMemoizedFiber(fiber) {
724
779
  }
725
780
  return false;
726
781
  }
782
+ function isCompiledFiber(fiber) {
783
+ if (fiber.tag === ClassComponent) return false;
784
+ const cache2 = fiber.updateQueue?.memoCache;
785
+ return !!cache2 && Array.isArray(cache2.data);
786
+ }
727
787
  function durationsOf(fiber) {
728
788
  if (typeof fiber.actualDuration !== "number") return null;
729
789
  const tree = fiber.actualDuration;
@@ -902,7 +962,8 @@ function diffHooks(fiber, alt) {
902
962
  const labels = hookLabelsFor(fiber, b);
903
963
  let i = 0;
904
964
  while (a && b) {
905
- if ((isStateNode(b) || isStoreNode(b)) && !Object.is(a.memoizedState, b.memoizedState)) {
965
+ const deferred = labels?.[i] === "useDeferredValue";
966
+ if ((deferred || isStateNode(b) || isStoreNode(b)) && !Object.is(a.memoizedState, b.memoizedState)) {
906
967
  const hook = labels?.[i] ?? hookLabel2(b);
907
968
  out.push({
908
969
  path: `${hook}#${i}`,
@@ -936,6 +997,10 @@ function diffContexts(fiber, alt) {
936
997
  let a = alt.dependencies?.firstContext ?? null;
937
998
  let b = fiber.dependencies?.firstContext ?? null;
938
999
  let i = 0;
1000
+ if (b && !("memoizedValue" in b)) {
1001
+ warnOnce("contextValues", "this React does not record context values on the fiber (React < 18): context changes are reported as plain parent re-renders.");
1002
+ return out;
1003
+ }
939
1004
  while (a && b) {
940
1005
  if (a.context === b.context && !Object.is(a.memoizedValue, b.memoizedValue)) {
941
1006
  const name = b.context.displayName ?? "Context";
@@ -968,10 +1033,26 @@ function diffContexts(fiber, alt) {
968
1033
  return out;
969
1034
  }
970
1035
  var isGenuine2 = (c) => c.kind === "different" || c.kind === "added" || c.kind === "removed";
1036
+ var isRefObject2 = (v) => {
1037
+ if (typeof v !== "object" || v === null) return false;
1038
+ const keys = Object.keys(v);
1039
+ return keys.length === 1 && keys[0] === "current";
1040
+ };
1041
+ function diffProps(prev, next) {
1042
+ if (prev === next) return [];
1043
+ const pr = prev.ref;
1044
+ const nr = next.ref;
1045
+ if (pr !== nr && isRefObject2(pr) && isRefObject2(nr)) {
1046
+ const { ref: _p, ...restPrev } = prev;
1047
+ const { ref: _n, ...restNext } = next;
1048
+ return [{ path: "ref", kind: "deep-equal", prev: pr, next: nr }, ...diffRecords(restPrev, restNext)];
1049
+ }
1050
+ return diffRecords(prev, next);
1051
+ }
971
1052
  function analyze(fiber, alt, trackHooks) {
972
1053
  const prevProps = alt.memoizedProps ?? {};
973
1054
  const nextProps = fiber.memoizedProps ?? {};
974
- const propChanges = prevProps === nextProps ? [] : diffRecords(prevProps, nextProps);
1055
+ const propChanges = diffProps(prevProps, nextProps);
975
1056
  let stateChanges = [];
976
1057
  let hookChanges = [];
977
1058
  if (fiber.tag === ClassComponent) {
@@ -984,7 +1065,7 @@ function analyze(fiber, alt, trackHooks) {
984
1065
  } else if (trackHooks) {
985
1066
  hookChanges = [...diffHooks(fiber, alt), ...diffContexts(fiber, alt)];
986
1067
  }
987
- const isStateHook = (h) => h.hook !== "useContext" && h.hook !== "useSyncExternalStore";
1068
+ const isStateHook = (h) => h.hook !== "useContext" && h.hook !== "useSyncExternalStore" && h.hook !== "useDeferredValue";
988
1069
  const causes = [];
989
1070
  if (propChanges.some(isGenuine2)) causes.push("props");
990
1071
  if (stateChanges.some(isGenuine2) || hookChanges.some((h) => isStateHook(h) && isGenuine2(h))) causes.push("state");
@@ -1024,6 +1105,24 @@ var lastCommit = null;
1024
1105
  var EFFECT_LOOP_WINDOW_MS = 50;
1025
1106
  var MAX_REPORTS_PER_COMMIT = 200;
1026
1107
  var COMMIT_TIME_BUDGET_MS = 25;
1108
+ function isDeferredCatchUp(fiber) {
1109
+ const types = fiber._debugHookTypes;
1110
+ if (!types || !types.includes("useDeferredValue") || !fiber.alternate) return false;
1111
+ const first = fiber.memoizedState;
1112
+ if (!isHookList(first)) return false;
1113
+ const labels = hookLabelsFor(fiber, first);
1114
+ if (!labels) return false;
1115
+ let a = fiber.alternate.memoizedState;
1116
+ let b = first;
1117
+ for (let i = 0; a && b; a = a.next, b = b.next, i++) {
1118
+ if (labels[i] === "useDeferredValue" && !Object.is(a.memoizedState, b.memoizedState)) return true;
1119
+ }
1120
+ return false;
1121
+ }
1122
+ function isUnder(fiber, boundaries) {
1123
+ for (let f = fiber.return; f && f.tag !== HostRoot; f = f.return) if (f.tag === SuspenseComponent && boundaries.includes(f)) return true;
1124
+ return false;
1125
+ }
1027
1126
  function onCommit(root, commitPriority) {
1028
1127
  const s = getState();
1029
1128
  if (!s.enabled) return;
@@ -1033,7 +1132,7 @@ function onCommit(root, commitPriority) {
1033
1132
  const rendered = [];
1034
1133
  const renderedNames = /* @__PURE__ */ new Set();
1035
1134
  let hot = false;
1036
- let suspenseResolved = false;
1135
+ let resolvedBoundaries = null;
1037
1136
  const stack = [root.current];
1038
1137
  while (stack.length) {
1039
1138
  const fiber = stack.pop();
@@ -1043,7 +1142,7 @@ function onCommit(root, commitPriority) {
1043
1142
  renderedNames.add(fiberName(fiber));
1044
1143
  if (shouldTrack(fiberType(fiber), o)) rendered.push(fiber);
1045
1144
  }
1046
- if (fiber.tag === SuspenseComponent && alt && alt.memoizedState !== null && fiber.memoizedState === null) suspenseResolved = true;
1145
+ if (fiber.tag === SuspenseComponent && alt && alt.memoizedState !== null && fiber.memoizedState === null) (resolvedBoundaries ?? (resolvedBoundaries = [])).push(fiber);
1047
1146
  if (!alt || fiber.child !== alt.child) {
1048
1147
  for (let c = fiber.child; c; c = c.sibling) stack.push(c);
1049
1148
  }
@@ -1055,10 +1154,10 @@ function onCommit(root, commitPriority) {
1055
1154
  let commitCause;
1056
1155
  let afterCommit;
1057
1156
  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))) {
1157
+ if (!inputDriven && lastCommit && at - lastCommit.at < EFFECT_LOOP_WINDOW_MS && updaters.some((u) => lastCommit.rendered.has(u)) && !rendered.some(isDeferredCatchUp)) {
1059
1158
  commitCause = "effect-after-commit";
1060
1159
  afterCommit = lastCommit.id;
1061
- } else if (suspenseResolved) commitCause = "suspense-resolved";
1160
+ } else if (resolvedBoundaries) commitCause = "suspense-resolved";
1062
1161
  if (rendered.length === 0) {
1063
1162
  lastCommit = { id: s.nextCommitId, at, rendered: renderedNames };
1064
1163
  return;
@@ -1110,6 +1209,9 @@ function onCommit(root, commitPriority) {
1110
1209
  owner: ownerName(fiber),
1111
1210
  path: componentPath(fiber),
1112
1211
  memoized: isMemoizedFiber(fiber),
1212
+ compiled: isCompiledFiber(fiber),
1213
+ classComponent: fiber.tag === ClassComponent,
1214
+ revealed: resolvedBoundaries !== null && a.trigger === "parent" && isUnder(fiber, resolvedBoundaries),
1113
1215
  selfDuration: durations ? durations.self : void 0,
1114
1216
  treeDuration: durations ? durations.tree : void 0,
1115
1217
  commitId,
@@ -1221,38 +1323,270 @@ function useWhyRerender(name, values, options) {
1221
1323
  });
1222
1324
  }
1223
1325
 
1326
+ // src/causes.ts
1327
+ var isAncestorReport = (anc, r) => anc.path.length < r.path.length && r.path[anc.path.length] === anc.component && anc.path.every((p, i) => r.path[i] === p);
1328
+ var bump = (m, key, n = 1) => void m.set(key, (m.get(key) || 0) + n);
1329
+ function indexByComponent(reports) {
1330
+ const index = /* @__PURE__ */ new Map();
1331
+ for (const r of reports) {
1332
+ const list = index.get(r.component);
1333
+ if (list) list.push(r);
1334
+ else index.set(r.component, [r]);
1335
+ }
1336
+ return index;
1337
+ }
1338
+ function rootCauseOf(r, commitReports, index = indexByComponent(commitReports)) {
1339
+ let cur = r;
1340
+ const seen = /* @__PURE__ */ new Set([r]);
1341
+ while (cur.trigger === "parent" && cur.parent) {
1342
+ const parent = cur.parent;
1343
+ const candidates = index.get(parent.name);
1344
+ const p = candidates && candidates.find((x) => isAncestorReport(x, cur));
1345
+ if (!p || seen.has(p)) return { name: parent.name, trigger: parent.trigger, report: null };
1346
+ seen.add(p);
1347
+ cur = p;
1348
+ }
1349
+ return cur === r ? null : { name: cur.component, trigger: cur.trigger, report: cur };
1350
+ }
1351
+ function rootCausesOf(reports) {
1352
+ const roots = /* @__PURE__ */ new Map();
1353
+ const rootByReport = /* @__PURE__ */ new Map();
1354
+ const index = indexByComponent(reports);
1355
+ let avoidable = 0;
1356
+ let wasted = 0;
1357
+ for (const r of reports) {
1358
+ if (!r.avoidable) continue;
1359
+ avoidable++;
1360
+ if (typeof r.selfDuration === "number") wasted += r.selfDuration;
1361
+ const root = rootCauseOf(r, reports, index);
1362
+ const name = root ? root.name : r.parent && r.parent.name || "(unknown)";
1363
+ const trigger = root ? root.trigger : r.parent && r.parent.trigger || "parent";
1364
+ rootByReport.set(r, name);
1365
+ let agg = roots.get(name);
1366
+ if (!agg) {
1367
+ agg = { name, trigger, count: 0, components: /* @__PURE__ */ new Map() };
1368
+ roots.set(name, agg);
1369
+ }
1370
+ agg.count++;
1371
+ bump(agg.components, r.component);
1372
+ }
1373
+ return { roots: [...roots.values()].sort((a, b) => b.count - a.count), rootByReport, avoidable, wasted };
1374
+ }
1375
+ function analyzeCommit(reports) {
1376
+ const first = reports[0];
1377
+ return {
1378
+ id: first && typeof first.commitId === "number" ? first.commitId : 0,
1379
+ total: reports.length,
1380
+ ...rootCausesOf(reports),
1381
+ contexts: contextAttribution(reports),
1382
+ reports
1383
+ };
1384
+ }
1385
+ function contextAttribution(reports) {
1386
+ const byCtx = /* @__PURE__ */ new Map();
1387
+ for (const r of reports) {
1388
+ for (const c of r.hookChanges || []) {
1389
+ if (c.hook !== "useContext" && !/^useContext/.test(c.path)) continue;
1390
+ const m = /useContext\((.*)\)/.exec(c.path);
1391
+ const name = m && m[1] ? m[1] : c.path;
1392
+ let agg = byCtx.get(name);
1393
+ if (!agg) {
1394
+ agg = { name, consumers: 0, avoidable: 0, components: /* @__PURE__ */ new Map(), commits: /* @__PURE__ */ new Set(), providers: /* @__PURE__ */ new Map(), changedKeys: /* @__PURE__ */ new Set(), totalKeys: 0 };
1395
+ byCtx.set(name, agg);
1396
+ }
1397
+ agg.consumers++;
1398
+ if (AVOIDABLE_KINDS.has(c.kind)) agg.avoidable++;
1399
+ bump(agg.components, r.component);
1400
+ agg.commits.add(typeof r.commitId === "number" ? r.commitId : 0);
1401
+ if (c.provider && c.provider.component) bump(agg.providers, c.provider.component);
1402
+ if (c.changedKeys) for (const k of c.changedKeys) agg.changedKeys.add(k);
1403
+ if (typeof c.totalKeys === "number") agg.totalKeys = Math.max(agg.totalKeys, c.totalKeys);
1404
+ }
1405
+ }
1406
+ return [...byCtx.values()].sort((a, b) => b.consumers - a.consumers);
1407
+ }
1408
+ function cascadeTree(reports) {
1409
+ const root = { name: "", children: /* @__PURE__ */ new Map(), report: null };
1410
+ for (const r of reports) {
1411
+ let node = root;
1412
+ for (const seg of r.path.concat([r.component])) {
1413
+ let next = node.children.get(seg);
1414
+ if (!next) {
1415
+ next = { name: seg, children: /* @__PURE__ */ new Map(), report: null };
1416
+ node.children.set(seg, next);
1417
+ }
1418
+ node = next;
1419
+ }
1420
+ if (!node.report || r.avoidable) node.report = r;
1421
+ node.count = (node.count || 0) + 1;
1422
+ if (r.avoidable) node.avoidable = (node.avoidable || 0) + 1;
1423
+ }
1424
+ return root;
1425
+ }
1426
+ function rootCauseSummary(name, commits, analyze2) {
1427
+ const analyzeFn = analyze2 || ((_, reports) => analyzeCommit(reports));
1428
+ const out = { name, trigger: "parent", commits: [], total: 0, components: /* @__PURE__ */ new Map(), affected: [] };
1429
+ for (const [key, reports] of commits) {
1430
+ const analysis = analyzeFn(key, reports);
1431
+ const root = analysis.roots.find((x) => x.name === name);
1432
+ if (!root) continue;
1433
+ out.trigger = root.trigger;
1434
+ out.commits.push({ key, analysis, count: root.count, components: root.components });
1435
+ out.total += root.count;
1436
+ for (const [c, n] of root.components) bump(out.components, c, n);
1437
+ for (const r of reports) if (r.avoidable && analysis.rootByReport.get(r) === name) out.affected.push(r);
1438
+ }
1439
+ out.commits.reverse();
1440
+ return out;
1441
+ }
1442
+ function groupByCommit(reports) {
1443
+ const out = /* @__PURE__ */ new Map();
1444
+ for (const r of reports) {
1445
+ if (typeof r.commitId !== "number" || r.commitId <= 0) continue;
1446
+ const list = out.get(r.commitId);
1447
+ if (list) list.push(r);
1448
+ else out.set(r.commitId, [r]);
1449
+ }
1450
+ return out;
1451
+ }
1452
+ function rankRootCauses(reports) {
1453
+ const byName = /* @__PURE__ */ new Map();
1454
+ for (const commit of groupByCommit(reports).values()) {
1455
+ for (const root of rootCausesOf(commit).roots) {
1456
+ let agg = byName.get(root.name);
1457
+ if (!agg) {
1458
+ agg = { name: root.name, trigger: root.trigger, commits: 0, count: 0, components: /* @__PURE__ */ new Map() };
1459
+ byName.set(root.name, agg);
1460
+ }
1461
+ agg.commits++;
1462
+ agg.count += root.count;
1463
+ for (const [c, n] of root.components) bump(agg.components, c, n);
1464
+ }
1465
+ }
1466
+ return [...byName.values()].sort((a, b) => b.count - a.count || b.commits - a.commits || a.name.localeCompare(b.name));
1467
+ }
1468
+ var plural = (n, word) => `${n} ${word}${n === 1 ? "" : "s"}`;
1469
+ function formatRootCauses(reports, limit = 5) {
1470
+ const ranked = rankRootCauses(reports);
1471
+ const lines = ranked.slice(0, limit).map((root, i) => `${String(i + 1).padStart(2)}. <${root.name}> (${root.trigger}) started ${plural(root.commits, "commit")}, ${plural(root.count, "avoidable re-render")} (${[...root.components].map(([c, n]) => `<${c}>${n > 1 ? " x" + n : ""}`).join(", ")})`);
1472
+ if (ranked.length > limit) lines.push(` \u2026 ${ranked.length - limit} more`);
1473
+ return lines.join("\n");
1474
+ }
1475
+
1224
1476
  // src/fixes.ts
1225
- var AVOIDABLE = /* @__PURE__ */ new Set(["deep-equal", "function", "element"]);
1477
+ var MAX_FIX_REPORTS = 50;
1226
1478
  var rootOf = (path) => path.split(/[.[]/)[0] || path;
1479
+ var identifier = (name) => /^[A-Za-z_$][\w$]*$/.test(name) ? name : "value";
1480
+ function shortValue(v, max = 60) {
1481
+ let s;
1482
+ try {
1483
+ s = JSON.stringify(v);
1484
+ } catch {
1485
+ s = String(v);
1486
+ }
1487
+ if (s === void 0) s = String(v);
1488
+ return s.length > max ? s.slice(0, max - 3) + "..." : s;
1489
+ }
1227
1490
  function fixesFor(r) {
1228
1491
  const out = [];
1229
- const owner = r.owner || r.parent && r.parent.name || "?";
1230
- const changes = [...r.propChanges, ...r.stateChanges, ...r.hookChanges];
1492
+ const ownerName2 = r.owner || r.parent && r.parent.name || null;
1493
+ const propChanges = r.propChanges || [];
1494
+ const changes = [...propChanges, ...r.stateChanges || [], ...r.hookChanges || []];
1231
1495
  if (r.avoidable && (changes.length === 0 || r.memoized === false)) {
1496
+ const identical = changes.length === 0;
1232
1497
  out.push({
1233
1498
  kind: "memo",
1234
1499
  owner: r.component,
1235
1500
  target: r.component,
1236
1501
  prop: null,
1237
1502
  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.`
1503
+ detail: identical ? `<${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.`,
1504
+ snippet: `// ${r.component}
1505
+ import { memo } from 'react';
1506
+
1507
+ export const ${r.component} = memo(function ${r.component}(props) {
1508
+ // ...
1509
+ });
1510
+ // class components: extend PureComponent instead`
1239
1511
  });
1240
1512
  }
1241
- for (const c of r.propChanges) {
1242
- if (!AVOIDABLE.has(c.kind)) continue;
1513
+ for (const c of propChanges) {
1514
+ if (!AVOIDABLE_KINDS.has(c.kind)) continue;
1515
+ const owner = ownerName2 || "?";
1243
1516
  const root = rootOf(c.path);
1517
+ const id = identifier(root);
1244
1518
  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.` });
1519
+ out.push({
1520
+ kind: "children",
1521
+ owner,
1522
+ target: r.component,
1523
+ prop: "children",
1524
+ label: `memoize children of <${r.component}> in <${owner}>`,
1525
+ detail: `<${owner}> re-creates the children of <${r.component}> on every render; they have the same types and props each time.`,
1526
+ snippet: `// ${owner}
1527
+ import { useMemo } from 'react';
1528
+
1529
+ const children = useMemo(() => (
1530
+ <>{/* the same elements */}</>
1531
+ ), [/* deps */]);
1532
+
1533
+ <${r.component}>{children}</${r.component}>
1534
+
1535
+ // static children: hoist them to module scope
1536
+ const STATIC = <em>hi</em>;`
1537
+ });
1246
1538
  } 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}>.` });
1539
+ out.push({
1540
+ kind: "useCallback",
1541
+ owner,
1542
+ target: r.component,
1543
+ prop: root,
1544
+ label: `useCallback(${root}) in <${owner}>`,
1545
+ detail: `prop "${c.path}" of <${r.component}> is a new function on every render of <${owner}>.`,
1546
+ snippet: `// ${owner}
1547
+ import { useCallback } from 'react';
1548
+
1549
+ const ${id} = useCallback((/* args */) => {
1550
+ // ...
1551
+ }, [/* deps */]);
1552
+
1553
+ <${r.component} ${root}={${id}} />`
1554
+ });
1248
1555
  } 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}>.` });
1556
+ out.push({
1557
+ kind: "useMemoElement",
1558
+ owner,
1559
+ target: r.component,
1560
+ prop: root,
1561
+ label: `memoize element prop ${root} in <${owner}>`,
1562
+ detail: `prop "${c.path}" of <${r.component}> is a new element with the same type and props on every render of <${owner}>.`,
1563
+ snippet: `// ${owner}
1564
+ import { useMemo } from 'react';
1565
+
1566
+ const ${id} = useMemo(() => ${shortValue(c.next, 40)}, [/* deps */]);
1567
+ // or pass it as children from a component that does not re-render`
1568
+ });
1250
1569
  } 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}>.` });
1570
+ const isArray = Array.isArray(c.next);
1571
+ out.push({
1572
+ kind: "useMemo",
1573
+ owner,
1574
+ target: r.component,
1575
+ prop: root,
1576
+ label: `useMemo(${root}) in <${owner}>`,
1577
+ detail: `prop "${c.path}" of <${r.component}> is a new ${isArray ? "array" : "object"} with the same contents on every render of <${owner}>.`,
1578
+ snippet: `// ${owner}
1579
+ import { useMemo } from 'react';
1580
+
1581
+ const ${id} = useMemo(() => (${shortValue(c.next, 80)}), [/* deps */]);
1582
+
1583
+ // or, when it never changes, hoist it to module scope:
1584
+ const ${id.toUpperCase()} = ${shortValue(c.next, 80)};`
1585
+ });
1252
1586
  }
1253
1587
  }
1254
- for (const c of [...r.stateChanges, ...r.hookChanges]) {
1255
- const isContext = c.hook === "useContext";
1588
+ for (const c of [...r.stateChanges || [], ...r.hookChanges || []]) {
1589
+ const isContext = c.hook === "useContext" || /^useContext/.test(c.path);
1256
1590
  const ctxName = isContext ? (/useContext\((.*)\)/.exec(c.path) || [])[1] || "Context" : "";
1257
1591
  const providerOwner = isContext && c.provider && c.provider.component ? c.provider.component : null;
1258
1592
  if (isContext && c.kind === "different" && c.changedKeys && typeof c.totalKeys === "number" && c.changedKeys.length > 0 && c.changedKeys.length < c.totalKeys) {
@@ -1262,20 +1596,63 @@ function fixesFor(r) {
1262
1596
  target: r.component,
1263
1597
  prop: ctxName,
1264
1598
  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.`
1599
+ detail: `${c.changedKeys.map((k) => `"${k}"`).join(", ")} of ${c.totalKeys} keys changed in ${ctxName}, yet every consumer (like <${r.component}>) re-rendered. Consumers that read the other keys re-render for nothing.`,
1600
+ snippet: `// ${providerOwner || "Provider"}
1601
+ // one context per independently-changing slice
1602
+ const ${identifier(ctxName)}Static = createContext(...);
1603
+ const ${identifier(ctxName)}${c.changedKeys.map((k) => k[0].toUpperCase() + k.slice(1)).join("")} = createContext(...);
1604
+
1605
+ // or keep one context and let consumers select a slice:
1606
+ const ${c.changedKeys[0]} = useContextSelector(${ctxName}, (v) => v.${c.changedKeys[0]});`
1266
1607
  });
1267
1608
  continue;
1268
1609
  }
1269
- if (!AVOIDABLE.has(c.kind)) continue;
1610
+ if (!AVOIDABLE_KINDS.has(c.kind)) continue;
1270
1611
  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.` });
1612
+ out.push({
1613
+ kind: "contextValue",
1614
+ owner: providerOwner || `${ctxName}.Provider`,
1615
+ target: r.component,
1616
+ prop: ctxName,
1617
+ label: `memoize the ${ctxName} provider value${providerOwner ? ` in <${providerOwner}>` : ""}`,
1618
+ detail: `<${r.component}> re-rendered because ${ctxName} produced a new value that is deep-equal to the previous one${providerOwner ? ` (Provider rendered by <${providerOwner}>)` : ""}.`,
1619
+ snippet: `// ${providerOwner || `where <${ctxName}.Provider> is rendered`}
1620
+ const value = useMemo(() => ({ /* ... */ }), [/* deps */]);
1621
+ <${ctxName}.Provider value={value}>`
1622
+ });
1272
1623
  } else if (c.hook === "useSyncExternalStore") {
1273
1624
  const chain = c.custom || [];
1274
1625
  const redux = chain.some((n) => /^use(App)?Selector$/.test(n));
1275
1626
  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) });
1627
+ const via = chain.length ? ` via ${chain.join(" \u203A ")}` : "";
1628
+ out.push({
1629
+ kind: "storeSnapshot",
1630
+ owner: r.component,
1631
+ target: r.component,
1632
+ prop: c.path,
1633
+ label: redux ? `memoize the selector in <${r.component}>` : zustand ? `useShallow in <${r.component}>` : `stable getSnapshot in <${r.component}>`,
1634
+ detail: redux ? `the selector${via} returns a new object on every call, so the component re-renders on every store change.` : zustand ? `the store selector${via} returns a new object on every call, so the component re-renders on every store change.` : `${c.path}${via} returned a new reference with the same contents; getSnapshot must return a cached value.`,
1635
+ snippet: redux ? `// ${r.component}
1636
+ import { shallowEqual } from 'react-redux';
1637
+ const slice = useSelector(selectSlice, shallowEqual);
1638
+ // or memoize: const selectSlice = createSelector([selectA, selectB], (a, b) => ({ a, b }));` : zustand ? `// ${r.component}
1639
+ import { useShallow } from 'zustand/react/shallow';
1640
+ const { a, b } = useStore(useShallow((s) => ({ a: s.a, b: s.b })));
1641
+ // or select a primitive: const a = useStore((s) => s.a);` : `// ${r.component}
1642
+ // getSnapshot must return the same reference while the data is unchanged
1643
+ const snapshot = useSyncExternalStore(subscribe, store.getSnapshot /* cached */);`
1644
+ });
1277
1645
  } 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).` });
1646
+ out.push({
1647
+ kind: "bailout",
1648
+ owner: r.component,
1649
+ target: r.component,
1650
+ prop: c.path,
1651
+ label: `bail out before setting ${c.path} in <${r.component}>`,
1652
+ detail: `${c.path} was set to a value deep-equal to the current one (new reference).`,
1653
+ snippet: `// ${r.component}
1654
+ setState((prev) => (deepEqual(prev, next) ? prev : next));`
1655
+ });
1279
1656
  }
1280
1657
  }
1281
1658
  return out;
@@ -1289,11 +1666,12 @@ function rankFixes(reports) {
1289
1666
  const key = fixKey(f);
1290
1667
  let agg = byKey.get(key);
1291
1668
  if (!agg) {
1292
- agg = { ...f, key, count: 0, components: {} };
1669
+ agg = { ...f, key, count: 0, components: {}, reports: [] };
1293
1670
  byKey.set(key, agg);
1294
1671
  }
1295
1672
  agg.count++;
1296
1673
  agg.components[r.component] = (agg.components[r.component] || 0) + 1;
1674
+ if (agg.reports.length < MAX_FIX_REPORTS) agg.reports.push(r);
1297
1675
  }
1298
1676
  }
1299
1677
  return [...byKey.values()].sort((a, b) => b.count - a.count || a.label.localeCompare(b.label));
@@ -1303,6 +1681,8 @@ function formatFixes(reports, limit = 10) {
1303
1681
  if (!ranked.length) return "No avoidable re-renders.";
1304
1682
  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
1683
  if (ranked.length > limit) lines.push(` \u2026 ${ranked.length - limit} more`);
1684
+ const roots = formatRootCauses(reports, 5);
1685
+ if (roots) lines.push("", "Root causes:", roots);
1306
1686
  return lines.join("\n");
1307
1687
  }
1308
1688
 
@@ -1359,6 +1739,8 @@ function compareSummaries(before, after) {
1359
1739
  regressions: rows.filter((r) => r.delta > 0)
1360
1740
  };
1361
1741
  }
1742
+ var summarizeSession = (session, reports) => summarizeReports(reports, session);
1743
+ var compareSessions = compareSummaries;
1362
1744
  function formatComparison(c) {
1363
1745
  const w = Math.max(14, ...c.rows.map((r) => r.component.length + 2));
1364
1746
  const line = (label, b, a, d) => `${label.padEnd(w)} ${String(b).padStart(8)} ${String(a).padStart(8)} ${d.padStart(8)}`;
@@ -1557,7 +1939,7 @@ function flash(target, duration = 500) {
1557
1939
  }
1558
1940
 
1559
1941
  // src/version.ts
1560
- var VERSION = "0.4.0" ;
1942
+ var VERSION = "0.5.0" ;
1561
1943
 
1562
1944
  // src/devtools.ts
1563
1945
  var DEVTOOLS_MARKER = "__rerenderLens";
@@ -1889,47 +2271,63 @@ function createDevtoolsNotifier(options = {}) {
1889
2271
  };
1890
2272
  }
1891
2273
 
2274
+ exports.AVOIDABLE_KINDS = AVOIDABLE_KINDS;
1892
2275
  exports.DEFAULT_CHANNEL = DEFAULT_CHANNEL;
1893
2276
  exports.DEVTOOLS_MARKER = DEVTOOLS_MARKER;
2277
+ exports.KIND_LABEL = KIND_LABEL;
1894
2278
  exports.MARKER = MARKER;
1895
2279
  exports.PROTOCOL_VERSION = PROTOCOL_VERSION;
1896
2280
  exports.VERSION = VERSION;
2281
+ exports.analyzeCommit = analyzeCommit;
1897
2282
  exports.assertWithinBudget = assertWithinBudget;
1898
2283
  exports.avoidableCounts = avoidableCounts;
1899
2284
  exports.buildReport = buildReport;
2285
+ exports.cascadeTree = cascadeTree;
1900
2286
  exports.checkBudget = checkBudget;
1901
2287
  exports.classify = classify;
1902
2288
  exports.combineNotifiers = combineNotifiers;
2289
+ exports.compareSessions = compareSessions;
1903
2290
  exports.compareSummaries = compareSummaries;
1904
2291
  exports.configure = configure;
2292
+ exports.contextAttribution = contextAttribution;
1905
2293
  exports.createCollector = createCollector;
1906
2294
  exports.createDevtoolsNotifier = createDevtoolsNotifier;
1907
2295
  exports.customHooksFromStack = customHooksFromStack;
1908
2296
  exports.deepEqual = deepEqual;
1909
2297
  exports.deserializeOptions = deserializeOptions;
2298
+ exports.diffLeaves = diffLeaves;
1910
2299
  exports.diffRecords = diffRecords;
1911
2300
  exports.disable = disable;
1912
2301
  exports.ensureDevtoolsHook = ensureDevtoolsHook;
2302
+ exports.firstDifferentPath = firstDifferentPath;
1913
2303
  exports.fixKey = fixKey;
1914
2304
  exports.fixesFor = fixesFor;
1915
2305
  exports.formatComparison = formatComparison;
1916
2306
  exports.formatFixes = formatFixes;
2307
+ exports.formatRootCauses = formatRootCauses;
1917
2308
  exports.getDisplayName = getDisplayName;
1918
2309
  exports.getRenderers = getRenderers;
2310
+ exports.groupByCommit = groupByCommit;
1919
2311
  exports.hookLabel = hookLabel;
2312
+ exports.indexByComponent = indexByComponent;
1920
2313
  exports.init = init;
1921
2314
  exports.isEnabled = isEnabled;
1922
2315
  exports.isProductionReact = isProductionReact;
1923
2316
  exports.parseExport = parseExport;
1924
2317
  exports.printReport = printReport;
1925
2318
  exports.rankFixes = rankFixes;
2319
+ exports.rankRootCauses = rankRootCauses;
1926
2320
  exports.resolveHookNames = resolveHookNames;
2321
+ exports.rootCauseOf = rootCauseOf;
2322
+ exports.rootCauseSummary = rootCauseSummary;
1927
2323
  exports.serialize = serialize;
1928
2324
  exports.serializeOptions = serializeOptions;
2325
+ exports.shortValue = shortValue;
1929
2326
  exports.shouldTrack = shouldTrack;
1930
2327
  exports.storeAdvice = storeAdvice;
1931
2328
  exports.summarize = summarize;
1932
2329
  exports.summarizeReports = summarizeReports;
2330
+ exports.summarizeSession = summarizeSession;
1933
2331
  exports.toBudget = toBudget;
1934
2332
  exports.track = track;
1935
2333
  exports.useWhyRerender = useWhyRerender;