rerender-lens 0.4.0 → 0.7.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 (68) hide show
  1. package/CHANGELOG.md +127 -0
  2. package/README.md +208 -359
  3. package/dist/{budget-LkNjGtRc.d.ts → budget-CA1MS7lB.d.cts} +20 -10
  4. package/dist/{budget-COBu7jBU.d.cts → budget-ok1VDJ4t.d.ts} +20 -10
  5. package/dist/cli.cjs +322 -53
  6. package/dist/cli.cjs.map +1 -1
  7. package/dist/cli.js +322 -53
  8. package/dist/cli.js.map +1 -1
  9. package/dist/{devtools-BkCct3cJ.d.cts → devtools-Cl5n_52v.d.ts} +17 -2
  10. package/dist/{devtools-CZebzpF6.d.ts → devtools-UwQx2VYS.d.cts} +17 -2
  11. package/dist/index.cjs +537 -49
  12. package/dist/index.cjs.map +1 -1
  13. package/dist/index.d.cts +137 -8
  14. package/dist/index.d.ts +137 -8
  15. package/dist/index.js +521 -50
  16. package/dist/index.js.map +1 -1
  17. package/dist/jest-setup.cjs +1690 -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 +1688 -0
  22. package/dist/jest-setup.js.map +1 -0
  23. package/dist/jest.cjs +1777 -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 +1768 -0
  28. package/dist/jest.js.map +1 -0
  29. package/dist/{notifiers-BjjGSHpp.d.ts → notifiers-B5poAEvE.d.cts} +8 -4
  30. package/dist/{notifiers-BGQtWKfX.d.cts → notifiers-CqQW0Eum.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 +71 -19
  38. package/dist/relay.cjs.map +1 -1
  39. package/dist/relay.d.cts +25 -3
  40. package/dist/relay.d.ts +25 -3
  41. package/dist/relay.js +71 -19
  42. package/dist/relay.js.map +1 -1
  43. package/dist/rerender-lens.iife.js +457 -62
  44. package/dist/runner-report-Bvqoe89M.d.ts +46 -0
  45. package/dist/runner-report-CvpDg2zt.d.cts +46 -0
  46. package/dist/setup.cjs +209 -35
  47. package/dist/setup.cjs.map +1 -1
  48. package/dist/setup.js +209 -35
  49. package/dist/setup.js.map +1 -1
  50. package/dist/{types-BzUEVkxJ.d.cts → types-DwNN5cWe.d.cts} +82 -2
  51. package/dist/{types-BzUEVkxJ.d.ts → types-DwNN5cWe.d.ts} +82 -2
  52. package/dist/vite.d.cts +1 -1
  53. package/dist/vite.d.ts +1 -1
  54. package/dist/vitest-setup.cjs +357 -44
  55. package/dist/vitest-setup.cjs.map +1 -1
  56. package/dist/vitest-setup.d.cts +4 -4
  57. package/dist/vitest-setup.d.ts +4 -4
  58. package/dist/vitest-setup.js +357 -44
  59. package/dist/vitest-setup.js.map +1 -1
  60. package/dist/vitest.cjs +420 -80
  61. package/dist/vitest.cjs.map +1 -1
  62. package/dist/vitest.d.cts +21 -39
  63. package/dist/vitest.d.ts +21 -39
  64. package/dist/vitest.js +421 -81
  65. package/dist/vitest.js.map +1 -1
  66. package/package.json +41 -4
  67. package/panel/panel.css +49 -0
  68. package/panel/panel.js +871 -353
@@ -1,4 +1,4 @@
1
- /* rerender-lens 0.4.0 — rerender-lens.core.js: types, version, diff, report, state, overlay. Built by scripts/build-vendor.mjs; do not edit. */
1
+ /* rerender-lens 0.7.0 — rerender-lens.core.js: types, version, diff, report, state, overlay. Built by scripts/build-vendor.mjs; do not edit. */
2
2
  (() => {
3
3
  var __defProp = Object.defineProperty;
4
4
  var __export = (target, all) => {
@@ -18,16 +18,18 @@
18
18
  __export(version_exports, {
19
19
  VERSION: () => VERSION
20
20
  });
21
- var VERSION = true ? "0.4.0" : "dev";
21
+ var VERSION = true ? "0.7.0" : "dev";
22
22
 
23
23
  // src/diff.ts
24
24
  var diff_exports = {};
25
25
  __export(diff_exports, {
26
+ AVOIDABLE_KINDS: () => AVOIDABLE_KINDS,
26
27
  DEFAULT_DIFF_BUDGET: () => DEFAULT_DIFF_BUDGET,
27
28
  beginDiffScope: () => beginDiffScope,
28
29
  classify: () => classify,
29
30
  deepEqual: () => deepEqual,
30
31
  diffBudgetExhausted: () => diffBudgetExhausted,
32
+ diffLeaves: () => diffLeaves,
31
33
  diffRecords: () => diffRecords,
32
34
  firstDifferentPath: () => firstDifferentPath,
33
35
  isReactElement: () => isReactElement
@@ -153,6 +155,7 @@
153
155
  }
154
156
  return false;
155
157
  }
158
+ var AVOIDABLE_KINDS = /* @__PURE__ */ new Set(["deep-equal", "function", "element"]);
156
159
  function classify(prev, next) {
157
160
  if (typeof prev === "function" && typeof next === "function") {
158
161
  return prev.name === next.name && prev.toString() === next.toString() ? "function" : "different";
@@ -189,35 +192,53 @@
189
192
  if (Object.is(a, b)) continue;
190
193
  const kind = classify(a, b);
191
194
  if (kind === "different") {
192
- changes.push({ path: diffBudgetExhausted() ? path : firstDifferentPath(a, b, path), kind, prev: a, next: b });
195
+ changes.push({ path: diffBudgetExhausted() ? path : firstDifferentPath(a, b, path) ?? path, kind, prev: a, next: b });
193
196
  } else {
194
197
  changes.push({ path, kind, prev: a, next: b });
195
198
  }
196
199
  }
197
200
  return changes;
198
201
  }
199
- function firstDifferentPath(a, b, path, depth = 0) {
200
- if (depth > 8) return path;
202
+ function firstDifferentPath(a, b, path = "", depth = 0) {
203
+ if (depth > 8) return path || "(value)";
201
204
  if (Array.isArray(a) && Array.isArray(b)) {
202
- if (a.length !== b.length) return `${path}.length`;
205
+ if (a.length !== b.length) return path ? `${path}.length` : "length";
203
206
  for (let i = 0; i < a.length; i++) {
204
207
  if (!deepEqual(a[i], b[i])) return firstDifferentPath(a[i], b[i], joinPath(path, i), depth + 1);
205
208
  }
206
- return path;
209
+ return null;
207
210
  }
208
211
  if (isPlainObject(a) && isPlainObject(b)) {
209
212
  const keys = /* @__PURE__ */ new Set([...Object.keys(a), ...Object.keys(b)]);
210
213
  for (const k of keys) {
211
214
  if (!deepEqual(a[k], b[k])) return firstDifferentPath(a[k], b[k], joinPath(path, k), depth + 1);
212
215
  }
213
- return path;
216
+ return null;
214
217
  }
215
- return path;
218
+ return deepEqual(a, b) ? null : path || "(value)";
219
+ }
220
+ function diffLeaves(a, b, limit = 20, path = "", out = []) {
221
+ if (out.length >= limit || Object.is(a, b)) return out;
222
+ if (Array.isArray(a) && Array.isArray(b)) {
223
+ const n = Math.max(a.length, b.length);
224
+ for (let i = 0; i < n && out.length < limit; i++) diffLeaves(a[i], b[i], limit, joinPath(path, i), out);
225
+ return out;
226
+ }
227
+ if (isPlainObject(a) && isPlainObject(b)) {
228
+ for (const k of /* @__PURE__ */ new Set([...Object.keys(a), ...Object.keys(b)])) {
229
+ if (out.length >= limit) break;
230
+ diffLeaves(a[k], b[k], limit, joinPath(path, k), out);
231
+ }
232
+ return out;
233
+ }
234
+ if (!deepEqual(a, b)) out.push({ path: path || "(value)", prev: a, next: b });
235
+ return out;
216
236
  }
217
237
 
218
238
  // src/report.ts
219
239
  var report_exports = {};
220
240
  __export(report_exports, {
241
+ KIND_LABEL: () => KIND_LABEL,
221
242
  buildReport: () => buildReport,
222
243
  fixFor: () => fixFor,
223
244
  hookLabel: () => hookLabel,
@@ -239,10 +260,12 @@
239
260
  }
240
261
  var isGenuine = (c) => c.kind === "different" || c.kind === "added" || c.kind === "removed";
241
262
  var isChildren = (change) => change.path === "children";
263
+ var isRefObject = (change) => change.path === "ref" && change.kind === "deep-equal";
242
264
  function fixFor(change) {
243
265
  if (isChildren(change) && (change.kind === "element" || change.kind === "deep-equal")) {
244
266
  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`;
245
267
  }
268
+ if (isRefObject(change)) return `create the ref once with useRef (or createRef outside the render) and pass the same object every time`;
246
269
  switch (change.kind) {
247
270
  case "function":
248
271
  return `wrap it in useCallback (or hoist it out of the parent's render)`;
@@ -258,6 +281,7 @@
258
281
  if (isChildren(change) && (change.kind === "element" || change.kind === "deep-equal")) {
259
282
  return `children are new React elements with the same types and props on every render of the parent`;
260
283
  }
284
+ if (isRefObject(change)) return `prop "ref" is a new ref object on every render (React re-attaches it each time)`;
261
285
  switch (change.kind) {
262
286
  case "deep-equal":
263
287
  return `prop "${change.path}" is a new reference but deep-equal to the previous value`;
@@ -285,11 +309,21 @@
285
309
  if (causes.length === 0) trigger = "parent";
286
310
  else if (causes.length === 1) trigger = causes[0];
287
311
  else trigger = "mixed";
288
- const avoidable = trigger === "parent";
312
+ const nothingChanged = input.propChanges.length === 0 && stateChanges.length === 0 && hookChanges.length === 0;
313
+ const cacheHit = input.compiled === true && trigger === "parent" && input.propChanges.length === 0;
314
+ const revealed = input.revealed === true && trigger === "parent";
315
+ const avoidable = trigger === "parent" && !cacheHit && !revealed;
289
316
  const reasons = [];
290
- if (avoidable && input.propChanges.length === 0 && stateChanges.length === 0 && hookChanges.length === 0) {
317
+ const memoFix = input.classComponent ? `Make "${input.component}" extend PureComponent (or implement shouldComponentUpdate)` : `Wrap "${input.component}" in React.memo (or extend PureComponent)`;
318
+ if (cacheHit) {
319
+ reasons.push(`compiled by React Compiler: its output is memoized and the inputs did not change, so this render is cheap; nothing to fix.`);
320
+ } else if (revealed) {
321
+ reasons.push(
322
+ `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.`
323
+ );
324
+ } else if (avoidable && nothingChanged) {
291
325
  const who = input.parent ? `<${input.parent.name}> re-rendered (${describeTrigger(input.parent.trigger)})` : "its parent re-rendered";
292
- reasons.push(`re-rendered with identical props because ${who}. Wrap "${input.component}" in React.memo (or extend PureComponent).`);
326
+ reasons.push(`re-rendered with identical props because ${who}. ${memoFix}.`);
293
327
  } else if (avoidable && input.parent) {
294
328
  reasons.push(`caused by <${input.parent.name}> re-rendering (${describeTrigger(input.parent.trigger)}).`);
295
329
  }
@@ -298,9 +332,9 @@
298
332
  reasons.push(fix ? `${describe(c)}: ${fix}.` : `${describe(c)}.`);
299
333
  }
300
334
  const memoized = input.memoized !== false;
301
- if (avoidable && !memoized && input.propChanges.length > 0) {
335
+ if (avoidable && !memoized && !input.compiled && input.propChanges.length > 0) {
302
336
  reasons.push(
303
- `"${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).`
337
+ `"${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)"}.`
304
338
  );
305
339
  }
306
340
  for (const c of stateChanges) {
@@ -312,6 +346,8 @@
312
346
  const where = c.provider && c.provider.component ? ` (provided by <${c.provider.component}>)` : "";
313
347
  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` : "";
314
348
  reasons.push(`${c.path} changed${where}${keys}.`);
349
+ } else if (c.hook === "useDeferredValue" && isGenuine(c)) {
350
+ 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.`);
315
351
  } else if (isGenuine(c)) reasons.push(`${hookLabel(c)} changed.`);
316
352
  else if (isStateHook(c))
317
353
  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.`);
@@ -342,6 +378,7 @@
342
378
  reasons,
343
379
  time: now()
344
380
  };
381
+ if (input.compiled) report.compiled = true;
345
382
  if (input.selfDuration !== void 0) report.selfDuration = input.selfDuration;
346
383
  if (input.treeDuration !== void 0) report.treeDuration = input.treeDuration;
347
384
  if (input.commitPriority) report.commitPriority = input.commitPriority;
@@ -379,10 +416,10 @@
379
416
  };
380
417
  function summarize(report) {
381
418
  const counts = /* @__PURE__ */ new Map();
382
- for (const c of [...report.propChanges, ...report.stateChanges, ...report.hookChanges]) {
419
+ for (const c of [...report.propChanges || [], ...report.stateChanges || [], ...report.hookChanges || []]) {
383
420
  counts.set(c.kind, (counts.get(c.kind) ?? 0) + 1);
384
421
  }
385
- const parts2 = [...counts].map(([kind, n]) => `${n} ${KIND_LABEL[kind]}`);
422
+ const parts2 = [...counts].map(([kind, n]) => `${n} ${KIND_LABEL[kind] || kind}`);
386
423
  if (parts2.length === 0) parts2.push("no changes");
387
424
  return parts2.join(", ");
388
425
  }
@@ -406,20 +443,33 @@
406
443
  // src/state.ts
407
444
  var state_exports = {};
408
445
  __export(state_exports, {
446
+ MAX_SEEN_COMPONENTS: () => MAX_SEEN_COMPONENTS,
409
447
  dispatch: () => dispatch,
410
448
  getState: () => getState,
449
+ noteRendered: () => noteRendered,
411
450
  warnOnce: () => warnOnce
412
451
  });
452
+ var MAX_SEEN_COMPONENTS = 500;
413
453
  var KEY = /* @__PURE__ */ Symbol.for("rerender-lens.state");
414
454
  function getState() {
415
455
  const g = globalThis;
416
456
  let s = g[KEY];
417
457
  if (!s) {
418
- 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 };
458
+ 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, seen: /* @__PURE__ */ new Set(), seenTracked: /* @__PURE__ */ new Set(), seenOverflow: false };
419
459
  g[KEY] = s;
420
460
  }
421
461
  return s;
422
462
  }
463
+ function noteRendered(s, name, tracked) {
464
+ if (!s.seen.has(name)) {
465
+ if (s.seen.size >= MAX_SEEN_COMPONENTS) {
466
+ s.seenOverflow = true;
467
+ return;
468
+ }
469
+ s.seen.add(name);
470
+ }
471
+ if (tracked) s.seenTracked.add(name);
472
+ }
423
473
  function warnOnce(key, message) {
424
474
  const s = getState();
425
475
  if (s.warnedOnce.has(key)) return;
@@ -575,7 +625,7 @@
575
625
  })();
576
626
 
577
627
  ;
578
- /* rerender-lens 0.4.0 — rerender-lens.engine.js: fiber, tracker, hookNames. Built by scripts/build-vendor.mjs; do not edit. */
628
+ /* rerender-lens 0.7.0 — rerender-lens.engine.js: fiber, tracker, hookNames. Built by scripts/build-vendor.mjs; do not edit. */
579
629
  (() => {
580
630
  var __create = Object.create;
581
631
  var __defProp = Object.defineProperty;
@@ -660,6 +710,7 @@
660
710
  getRenderers: () => getRenderers,
661
711
  hostNodesOf: () => hostNodesOf,
662
712
  instanceIdOf: () => instanceIdOf,
713
+ isCompiledFiber: () => isCompiledFiber,
663
714
  isMemoizedFiber: () => isMemoizedFiber,
664
715
  isProductionReact: () => isProductionReact,
665
716
  nearestComponent: () => nearestComponent,
@@ -679,6 +730,7 @@
679
730
  __export(tracker_exports, {
680
731
  configure: () => configure,
681
732
  disable: () => disable,
733
+ explainTracking: () => explainTracking,
682
734
  getDisplayName: () => getDisplayName,
683
735
  init: () => init,
684
736
  isEnabled: () => isEnabled,
@@ -726,10 +778,53 @@
726
778
  if (o.trackAllMemoized) return isMemo(type) || isPureClass(type);
727
779
  return false;
728
780
  }
781
+ var ANONYMOUS = "Anonymous";
782
+ function explainTracking(type, o) {
783
+ if (typeof type === "string")
784
+ return { tracked: false, name: type, memoized: false, reason: `<${type}> is a DOM element, not a React component; only components are tracked.` };
785
+ if (!isComponentLike(type))
786
+ return { tracked: false, name: ANONYMOUS, memoized: false, reason: "This is not a React component type, so it is never tracked." };
787
+ const name = getDisplayName(type);
788
+ const memo = isMemo(type);
789
+ const memoized = memo || isPureClass(type);
790
+ if (o.exclude?.some((m) => matches(m, name)))
791
+ return { tracked: false, name, memoized, reason: `<${name}> matches "exclude", which wins over every other rule.`, fix: `Remove "${name}" from the exclude list.` };
792
+ if (hasMarker(type)) return { tracked: true, name, memoized, reason: `<${name}> is marked with track() (the ${import_types.MARKER} static).` };
793
+ if (o.include?.some((m) => matches(m, name))) return { tracked: true, name, memoized, reason: `<${name}> matches "include".` };
794
+ if (o.trackAllComponents) return { tracked: true, name, memoized, reason: '"Track every component" is on, so every component is tracked.' };
795
+ if (o.trackAllMemoized && memoized)
796
+ return { tracked: true, name, memoized, reason: `<${name}> is ${memo ? "a React.memo component" : "a PureComponent"}, and "Track every React.memo and PureComponent" is on.` };
797
+ if (name === ANONYMOUS)
798
+ return {
799
+ tracked: false,
800
+ name,
801
+ memoized,
802
+ reason: 'This component has no display name, so "include" and "exclude" cannot address it.',
803
+ fix: "Give the function a name, set displayName, or mark it with track(fn, 'Name')."
804
+ };
805
+ if (o.trackAllMemoized)
806
+ return {
807
+ tracked: false,
808
+ name,
809
+ memoized,
810
+ reason: `"Track every React.memo and PureComponent" only covers React.memo components and PureComponent classes, and <${name}> is neither.`,
811
+ fix: `Add "${name}" to include, wrap it in React.memo, or turn on "Track every component".`
812
+ };
813
+ return {
814
+ tracked: false,
815
+ name,
816
+ memoized,
817
+ reason: `Nothing selects <${name}>: it is not marked with track(), no "include" pattern matches it, and neither "track every" option is on.`,
818
+ fix: `Add "${name}" to include, or turn on "Track every React.memo and PureComponent" / "Track every component".`
819
+ };
820
+ }
729
821
  function init(options = {}) {
730
822
  const s = (0, import_state.getState)();
731
823
  s.options = { ...options };
732
824
  s.printed.clear();
825
+ s.seen.clear();
826
+ s.seenTracked.clear();
827
+ s.seenOverflow = false;
733
828
  if (!s.enabled) {
734
829
  s.detach = attach();
735
830
  s.enabled = true;
@@ -785,6 +880,7 @@
785
880
  useEffectEvent: 1,
786
881
  useCacheRefresh: 1
787
882
  };
883
+ var BUILTIN_HOOKS = /* @__PURE__ */ new Set([...Object.keys(NODE_COUNT), "useContext", "useDebugValue", "use"]);
788
884
  var noop = () => {
789
885
  };
790
886
  function customHooksFromStack(stack, componentName, marker) {
@@ -799,7 +895,7 @@
799
895
  continue;
800
896
  }
801
897
  if (name === componentName || name.endsWith("." + componentName)) break;
802
- if (/^use[A-Z0-9_]/.test(name)) names.push(name);
898
+ if (/^use[A-Z0-9_]/.test(name) && !BUILTIN_HOOKS.has(name)) names.push(name);
803
899
  if (names.length > 8) break;
804
900
  }
805
901
  return names;
@@ -1101,6 +1197,11 @@
1101
1197
  }
1102
1198
  return false;
1103
1199
  }
1200
+ function isCompiledFiber(fiber) {
1201
+ if (fiber.tag === ClassComponent) return false;
1202
+ const cache2 = fiber.updateQueue?.memoCache;
1203
+ return !!cache2 && Array.isArray(cache2.data);
1204
+ }
1104
1205
  function durationsOf(fiber) {
1105
1206
  if (typeof fiber.actualDuration !== "number") return null;
1106
1207
  const tree = fiber.actualDuration;
@@ -1279,7 +1380,8 @@
1279
1380
  const labels = hookLabelsFor(fiber, b);
1280
1381
  let i = 0;
1281
1382
  while (a && b) {
1282
- if ((isStateNode(b) || isStoreNode(b)) && !Object.is(a.memoizedState, b.memoizedState)) {
1383
+ const deferred = labels?.[i] === "useDeferredValue";
1384
+ if ((deferred || isStateNode(b) || isStoreNode(b)) && !Object.is(a.memoizedState, b.memoizedState)) {
1283
1385
  const hook = labels?.[i] ?? hookLabel(b);
1284
1386
  out.push({
1285
1387
  path: `${hook}#${i}`,
@@ -1313,6 +1415,10 @@
1313
1415
  let a = alt.dependencies?.firstContext ?? null;
1314
1416
  let b = fiber.dependencies?.firstContext ?? null;
1315
1417
  let i = 0;
1418
+ if (b && !("memoizedValue" in b)) {
1419
+ (0, import_state2.warnOnce)("contextValues", "this React does not record context values on the fiber (React < 18): context changes are reported as plain parent re-renders.");
1420
+ return out;
1421
+ }
1316
1422
  while (a && b) {
1317
1423
  if (a.context === b.context && !Object.is(a.memoizedValue, b.memoizedValue)) {
1318
1424
  const name = b.context.displayName ?? "Context";
@@ -1345,10 +1451,28 @@
1345
1451
  return out;
1346
1452
  }
1347
1453
  var isGenuine = (c) => c.kind === "different" || c.kind === "added" || c.kind === "removed";
1454
+ var isRefObject = (v) => {
1455
+ if (typeof v !== "object" || v === null) return false;
1456
+ const keys = Object.keys(v);
1457
+ return keys.length === 1 && keys[0] === "current";
1458
+ };
1459
+ function diffProps(prev, next) {
1460
+ if (prev === next) return [];
1461
+ const pr = prev.ref;
1462
+ const nr = next.ref;
1463
+ if (pr !== nr && isRefObject(pr) && isRefObject(nr)) {
1464
+ const { ref: _p, ...restPrev } = prev;
1465
+ const { ref: _n, ...restNext } = next;
1466
+ void _p;
1467
+ void _n;
1468
+ return [{ path: "ref", kind: "deep-equal", prev: pr, next: nr }, ...(0, import_diff.diffRecords)(restPrev, restNext)];
1469
+ }
1470
+ return (0, import_diff.diffRecords)(prev, next);
1471
+ }
1348
1472
  function analyze(fiber, alt, trackHooks) {
1349
1473
  const prevProps = alt.memoizedProps ?? {};
1350
1474
  const nextProps = fiber.memoizedProps ?? {};
1351
- const propChanges = prevProps === nextProps ? [] : (0, import_diff.diffRecords)(prevProps, nextProps);
1475
+ const propChanges = diffProps(prevProps, nextProps);
1352
1476
  let stateChanges = [];
1353
1477
  let hookChanges = [];
1354
1478
  if (fiber.tag === ClassComponent) {
@@ -1361,7 +1485,7 @@
1361
1485
  } else if (trackHooks) {
1362
1486
  hookChanges = [...diffHooks(fiber, alt), ...diffContexts(fiber, alt)];
1363
1487
  }
1364
- const isStateHook = (h) => h.hook !== "useContext" && h.hook !== "useSyncExternalStore";
1488
+ const isStateHook = (h) => h.hook !== "useContext" && h.hook !== "useSyncExternalStore" && h.hook !== "useDeferredValue";
1365
1489
  const causes = [];
1366
1490
  if (propChanges.some(isGenuine)) causes.push("props");
1367
1491
  if (stateChanges.some(isGenuine) || hookChanges.some((h) => isStateHook(h) && isGenuine(h))) causes.push("state");
@@ -1401,6 +1525,24 @@
1401
1525
  var EFFECT_LOOP_WINDOW_MS = 50;
1402
1526
  var MAX_REPORTS_PER_COMMIT = 200;
1403
1527
  var COMMIT_TIME_BUDGET_MS = 25;
1528
+ function isDeferredCatchUp(fiber) {
1529
+ const types = fiber._debugHookTypes;
1530
+ if (!types || !types.includes("useDeferredValue") || !fiber.alternate) return false;
1531
+ const first = fiber.memoizedState;
1532
+ if (!isHookList(first)) return false;
1533
+ const labels = hookLabelsFor(fiber, first);
1534
+ if (!labels) return false;
1535
+ let a = fiber.alternate.memoizedState;
1536
+ let b = first;
1537
+ for (let i = 0; a && b; a = a.next, b = b.next, i++) {
1538
+ if (labels[i] === "useDeferredValue" && !Object.is(a.memoizedState, b.memoizedState)) return true;
1539
+ }
1540
+ return false;
1541
+ }
1542
+ function isUnder(fiber, boundaries) {
1543
+ for (let f = fiber.return; f && f.tag !== HostRoot; f = f.return) if (f.tag === SuspenseComponent && boundaries.includes(f)) return true;
1544
+ return false;
1545
+ }
1404
1546
  function onCommit(root, commitPriority) {
1405
1547
  const s = (0, import_state2.getState)();
1406
1548
  if (!s.enabled) return;
@@ -1410,17 +1552,20 @@
1410
1552
  const rendered = [];
1411
1553
  const renderedNames = /* @__PURE__ */ new Set();
1412
1554
  let hot = false;
1413
- let suspenseResolved = false;
1555
+ let resolvedBoundaries = null;
1414
1556
  const stack = [root.current];
1415
1557
  while (stack.length) {
1416
1558
  const fiber = stack.pop();
1417
1559
  const alt = fiber.alternate;
1418
1560
  if (alt && isComponentTag(fiber.tag) && didRender(fiber)) {
1419
1561
  if (isHotSwapped(fiber, alt)) hot = true;
1420
- renderedNames.add(fiberName(fiber));
1421
- if (shouldTrack(fiberType(fiber), o)) rendered.push(fiber);
1562
+ const name = fiberName(fiber);
1563
+ renderedNames.add(name);
1564
+ const tracked = shouldTrack(fiberType(fiber), o);
1565
+ (0, import_state2.noteRendered)(s, name, tracked);
1566
+ if (tracked) rendered.push(fiber);
1422
1567
  }
1423
- if (fiber.tag === SuspenseComponent && alt && alt.memoizedState !== null && fiber.memoizedState === null) suspenseResolved = true;
1568
+ if (fiber.tag === SuspenseComponent && alt && alt.memoizedState !== null && fiber.memoizedState === null) (resolvedBoundaries ?? (resolvedBoundaries = [])).push(fiber);
1424
1569
  if (!alt || fiber.child !== alt.child) {
1425
1570
  for (let c = fiber.child; c; c = c.sibling) stack.push(c);
1426
1571
  }
@@ -1432,10 +1577,10 @@
1432
1577
  let commitCause;
1433
1578
  let afterCommit;
1434
1579
  const inputDriven = commitPriority === "immediate" || commitPriority === "user-blocking";
1435
- if (!inputDriven && lastCommit && at - lastCommit.at < EFFECT_LOOP_WINDOW_MS && updaters.some((u) => lastCommit.rendered.has(u))) {
1580
+ if (!inputDriven && lastCommit && at - lastCommit.at < EFFECT_LOOP_WINDOW_MS && updaters.some((u) => lastCommit.rendered.has(u)) && !rendered.some(isDeferredCatchUp)) {
1436
1581
  commitCause = "effect-after-commit";
1437
1582
  afterCommit = lastCommit.id;
1438
- } else if (suspenseResolved) commitCause = "suspense-resolved";
1583
+ } else if (resolvedBoundaries) commitCause = "suspense-resolved";
1439
1584
  if (rendered.length === 0) {
1440
1585
  lastCommit = { id: s.nextCommitId, at, rendered: renderedNames };
1441
1586
  return;
@@ -1487,6 +1632,9 @@
1487
1632
  owner: ownerName(fiber),
1488
1633
  path: componentPath(fiber),
1489
1634
  memoized: isMemoizedFiber(fiber),
1635
+ compiled: isCompiledFiber(fiber),
1636
+ classComponent: fiber.tag === ClassComponent,
1637
+ revealed: resolvedBoundaries !== null && a.trigger === "parent" && isUnder(fiber, resolvedBoundaries),
1490
1638
  selfDuration: durations ? durations.self : void 0,
1491
1639
  treeDuration: durations ? durations.tree : void 0,
1492
1640
  commitId,
@@ -1508,7 +1656,7 @@
1508
1656
  })();
1509
1657
 
1510
1658
  ;
1511
- /* rerender-lens 0.4.0 — rerender-lens.js: devtools bridge + notifiers (window.RerenderLens). Built by scripts/build-vendor.mjs; do not edit. */
1659
+ /* rerender-lens 0.7.0 — rerender-lens.js: devtools bridge + notifiers (window.RerenderLens). Built by scripts/build-vendor.mjs; do not edit. */
1512
1660
  "use strict";
1513
1661
  var RerenderLens = (() => {
1514
1662
  var __create = Object.create;
@@ -1621,66 +1769,235 @@ var RerenderLens = (() => {
1621
1769
  MARKER: () => import_types.MARKER,
1622
1770
  PROTOCOL_VERSION: () => PROTOCOL_VERSION,
1623
1771
  VERSION: () => import_version2.VERSION,
1624
- buildReport: () => import_report2.buildReport,
1625
- classify: () => import_diff2.classify,
1772
+ buildReport: () => import_report.buildReport,
1773
+ classify: () => import_diff4.classify,
1626
1774
  combineNotifiers: () => combineNotifiers,
1627
1775
  configure: () => import_tracker2.configure,
1628
1776
  createCollector: () => createCollector,
1629
1777
  createDevtoolsNotifier: () => createDevtoolsNotifier,
1630
- deepEqual: () => import_diff2.deepEqual,
1631
- diffRecords: () => import_diff2.diffRecords,
1778
+ deepEqual: () => import_diff4.deepEqual,
1779
+ diffRecords: () => import_diff4.diffRecords,
1632
1780
  disable: () => import_tracker2.disable,
1633
1781
  ensureDevtoolsHook: () => import_fiber2.ensureDevtoolsHook,
1782
+ explainTracking: () => import_tracker2.explainTracking,
1634
1783
  getDisplayName: () => import_tracker2.getDisplayName,
1635
1784
  getRenderers: () => import_fiber2.getRenderers,
1636
1785
  init: () => import_tracker2.init,
1637
1786
  isEnabled: () => import_tracker2.isEnabled,
1638
1787
  isProductionReact: () => import_fiber2.isProductionReact,
1639
- printReport: () => import_report2.printReport,
1788
+ printReport: () => import_report.printReport,
1640
1789
  serialize: () => serialize,
1641
1790
  shouldTrack: () => import_tracker2.shouldTrack,
1642
- summarize: () => import_report2.summarize,
1791
+ summarize: () => import_report.summarize,
1643
1792
  track: () => import_tracker2.track
1644
1793
  });
1645
1794
  var import_types = __toESM(require_types(), 1);
1646
1795
  var import_tracker2 = __toESM(require_tracker(), 1);
1647
1796
  var import_fiber2 = __toESM(require_fiber(), 1);
1797
+ var import_diff4 = __toESM(require_diff(), 1);
1798
+ var import_report = __toESM(require_report(), 1);
1799
+
1800
+ // src/fixes.ts
1648
1801
  var import_diff2 = __toESM(require_diff(), 1);
1649
- var import_report2 = __toESM(require_report(), 1);
1802
+
1803
+ // src/causes.ts
1804
+ var import_diff = __toESM(require_diff(), 1);
1805
+ 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);
1806
+ var bump = (m, key, n = 1) => void m.set(key, (m.get(key) || 0) + n);
1807
+ function indexByComponent(reports) {
1808
+ const index = /* @__PURE__ */ new Map();
1809
+ for (const r of reports) {
1810
+ const list = index.get(r.component);
1811
+ if (list) list.push(r);
1812
+ else index.set(r.component, [r]);
1813
+ }
1814
+ return index;
1815
+ }
1816
+ function rootCauseOf(r, commitReports, index = indexByComponent(commitReports)) {
1817
+ let cur = r;
1818
+ const seen = /* @__PURE__ */ new Set([r]);
1819
+ while (cur.trigger === "parent" && cur.parent) {
1820
+ const parent = cur.parent;
1821
+ const candidates = index.get(parent.name);
1822
+ const p = candidates && candidates.find((x) => isAncestorReport(x, cur));
1823
+ if (!p || seen.has(p)) return { name: parent.name, trigger: parent.trigger, report: null };
1824
+ seen.add(p);
1825
+ cur = p;
1826
+ }
1827
+ return cur === r ? null : { name: cur.component, trigger: cur.trigger, report: cur };
1828
+ }
1829
+ function rootCausesOf(reports) {
1830
+ const roots = /* @__PURE__ */ new Map();
1831
+ const rootByReport = /* @__PURE__ */ new Map();
1832
+ const index = indexByComponent(reports);
1833
+ let avoidable = 0;
1834
+ let wasted = 0;
1835
+ for (const r of reports) {
1836
+ if (!r.avoidable) continue;
1837
+ avoidable++;
1838
+ if (typeof r.selfDuration === "number") wasted += r.selfDuration;
1839
+ const root = rootCauseOf(r, reports, index);
1840
+ const name = root ? root.name : r.parent && r.parent.name || "(unknown)";
1841
+ const trigger = root ? root.trigger : r.parent && r.parent.trigger || "parent";
1842
+ rootByReport.set(r, name);
1843
+ let agg = roots.get(name);
1844
+ if (!agg) {
1845
+ agg = { name, trigger, count: 0, components: /* @__PURE__ */ new Map() };
1846
+ roots.set(name, agg);
1847
+ }
1848
+ agg.count++;
1849
+ bump(agg.components, r.component);
1850
+ }
1851
+ return { roots: [...roots.values()].sort((a, b) => b.count - a.count), rootByReport, avoidable, wasted };
1852
+ }
1853
+ function groupByCommit(reports) {
1854
+ const out = /* @__PURE__ */ new Map();
1855
+ for (const r of reports) {
1856
+ if (typeof r.commitId !== "number" || r.commitId <= 0) continue;
1857
+ const list = out.get(r.commitId);
1858
+ if (list) list.push(r);
1859
+ else out.set(r.commitId, [r]);
1860
+ }
1861
+ return out;
1862
+ }
1863
+ function rankRootCauses(reports) {
1864
+ const byName = /* @__PURE__ */ new Map();
1865
+ for (const commit of groupByCommit(reports).values()) {
1866
+ for (const root of rootCausesOf(commit).roots) {
1867
+ let agg = byName.get(root.name);
1868
+ if (!agg) {
1869
+ agg = { name: root.name, trigger: root.trigger, commits: 0, count: 0, components: /* @__PURE__ */ new Map() };
1870
+ byName.set(root.name, agg);
1871
+ }
1872
+ agg.commits++;
1873
+ agg.count += root.count;
1874
+ for (const [c, n] of root.components) bump(agg.components, c, n);
1875
+ }
1876
+ }
1877
+ return [...byName.values()].sort((a, b) => b.count - a.count || b.commits - a.commits || a.name.localeCompare(b.name));
1878
+ }
1879
+ var plural = (n, word) => `${n} ${word}${n === 1 ? "" : "s"}`;
1880
+ function formatRootCauses(reports, limit = 5) {
1881
+ const ranked = rankRootCauses(reports);
1882
+ 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(", ")})`);
1883
+ if (ranked.length > limit) lines.push(` \u2026 ${ranked.length - limit} more`);
1884
+ return lines.join("\n");
1885
+ }
1650
1886
 
1651
1887
  // src/fixes.ts
1652
- var import_report = __toESM(require_report(), 1);
1653
- var AVOIDABLE = /* @__PURE__ */ new Set(["deep-equal", "function", "element"]);
1888
+ var MAX_FIX_REPORTS = 50;
1654
1889
  var rootOf = (path) => path.split(/[.[]/)[0] || path;
1890
+ var identifier = (name) => /^[A-Za-z_$][\w$]*$/.test(name) ? name : "value";
1891
+ function shortValue(v, max = 60) {
1892
+ let s;
1893
+ try {
1894
+ s = JSON.stringify(v);
1895
+ } catch {
1896
+ s = String(v);
1897
+ }
1898
+ if (s === void 0) s = String(v);
1899
+ return s.length > max ? s.slice(0, max - 3) + "..." : s;
1900
+ }
1655
1901
  function fixesFor(r) {
1656
1902
  const out = [];
1657
- const owner = r.owner || r.parent && r.parent.name || "?";
1658
- const changes = [...r.propChanges, ...r.stateChanges, ...r.hookChanges];
1903
+ const ownerName = r.owner || r.parent && r.parent.name || null;
1904
+ const propChanges = r.propChanges || [];
1905
+ const changes = [...propChanges, ...r.stateChanges || [], ...r.hookChanges || []];
1659
1906
  if (r.avoidable && (changes.length === 0 || r.memoized === false)) {
1907
+ const identical = changes.length === 0;
1660
1908
  out.push({
1661
1909
  kind: "memo",
1662
1910
  owner: r.component,
1663
1911
  target: r.component,
1664
1912
  prop: null,
1665
1913
  label: `Wrap <${r.component}> in React.memo`,
1666
- 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.`
1914
+ 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.`,
1915
+ snippet: `// ${r.component}
1916
+ import { memo } from 'react';
1917
+
1918
+ export const ${r.component} = memo(function ${r.component}(props) {
1919
+ // ...
1920
+ });
1921
+ // class components: extend PureComponent instead`
1667
1922
  });
1668
1923
  }
1669
- for (const c of r.propChanges) {
1670
- if (!AVOIDABLE.has(c.kind)) continue;
1924
+ for (const c of propChanges) {
1925
+ if (!import_diff2.AVOIDABLE_KINDS.has(c.kind)) continue;
1926
+ const owner = ownerName || "?";
1671
1927
  const root = rootOf(c.path);
1928
+ const id = identifier(root);
1672
1929
  if (root === "children" && (c.kind === "element" || c.kind === "deep-equal")) {
1673
- 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.` });
1930
+ out.push({
1931
+ kind: "children",
1932
+ owner,
1933
+ target: r.component,
1934
+ prop: "children",
1935
+ label: `memoize children of <${r.component}> in <${owner}>`,
1936
+ detail: `<${owner}> re-creates the children of <${r.component}> on every render; they have the same types and props each time.`,
1937
+ snippet: `// ${owner}
1938
+ import { useMemo } from 'react';
1939
+
1940
+ const children = useMemo(() => (
1941
+ <>{/* the same elements */}</>
1942
+ ), [/* deps */]);
1943
+
1944
+ <${r.component}>{children}</${r.component}>
1945
+
1946
+ // static children: hoist them to module scope
1947
+ const STATIC = <em>hi</em>;`
1948
+ });
1674
1949
  } else if (c.kind === "function") {
1675
- 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}>.` });
1950
+ out.push({
1951
+ kind: "useCallback",
1952
+ owner,
1953
+ target: r.component,
1954
+ prop: root,
1955
+ label: `useCallback(${root}) in <${owner}>`,
1956
+ detail: `prop "${c.path}" of <${r.component}> is a new function on every render of <${owner}>.`,
1957
+ snippet: `// ${owner}
1958
+ import { useCallback } from 'react';
1959
+
1960
+ const ${id} = useCallback((/* args */) => {
1961
+ // ...
1962
+ }, [/* deps */]);
1963
+
1964
+ <${r.component} ${root}={${id}} />`
1965
+ });
1676
1966
  } else if (c.kind === "element") {
1677
- 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}>.` });
1967
+ out.push({
1968
+ kind: "useMemoElement",
1969
+ owner,
1970
+ target: r.component,
1971
+ prop: root,
1972
+ label: `memoize element prop ${root} in <${owner}>`,
1973
+ detail: `prop "${c.path}" of <${r.component}> is a new element with the same type and props on every render of <${owner}>.`,
1974
+ snippet: `// ${owner}
1975
+ import { useMemo } from 'react';
1976
+
1977
+ const ${id} = useMemo(() => ${shortValue(c.next, 40)}, [/* deps */]);
1978
+ // or pass it as children from a component that does not re-render`
1979
+ });
1678
1980
  } else {
1679
- 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}>.` });
1981
+ const isArray = Array.isArray(c.next);
1982
+ out.push({
1983
+ kind: "useMemo",
1984
+ owner,
1985
+ target: r.component,
1986
+ prop: root,
1987
+ label: `useMemo(${root}) in <${owner}>`,
1988
+ detail: `prop "${c.path}" of <${r.component}> is a new ${isArray ? "array" : "object"} with the same contents on every render of <${owner}>.`,
1989
+ snippet: `// ${owner}
1990
+ import { useMemo } from 'react';
1991
+
1992
+ const ${id} = useMemo(() => (${shortValue(c.next, 80)}), [/* deps */]);
1993
+
1994
+ // or, when it never changes, hoist it to module scope:
1995
+ const ${id.toUpperCase()} = ${shortValue(c.next, 80)};`
1996
+ });
1680
1997
  }
1681
1998
  }
1682
- for (const c of [...r.stateChanges, ...r.hookChanges]) {
1683
- const isContext = c.hook === "useContext";
1999
+ for (const c of [...r.stateChanges || [], ...r.hookChanges || []]) {
2000
+ const isContext = c.hook === "useContext" || /^useContext/.test(c.path);
1684
2001
  const ctxName = isContext ? (/useContext\((.*)\)/.exec(c.path) || [])[1] || "Context" : "";
1685
2002
  const providerOwner = isContext && c.provider && c.provider.component ? c.provider.component : null;
1686
2003
  if (isContext && c.kind === "different" && c.changedKeys && typeof c.totalKeys === "number" && c.changedKeys.length > 0 && c.changedKeys.length < c.totalKeys) {
@@ -1690,20 +2007,63 @@ var RerenderLens = (() => {
1690
2007
  target: r.component,
1691
2008
  prop: ctxName,
1692
2009
  label: `split ${ctxName}${providerOwner ? ` in <${providerOwner}>` : ""}: only ${c.changedKeys.join(", ")} changed`,
1693
- detail: `${c.changedKeys.length} of ${c.totalKeys} keys changed in ${ctxName}, yet every consumer re-rendered.`
2010
+ 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.`,
2011
+ snippet: `// ${providerOwner || "Provider"}
2012
+ // one context per independently-changing slice
2013
+ const ${identifier(ctxName)}Static = createContext(...);
2014
+ const ${identifier(ctxName)}${c.changedKeys.map((k) => k[0].toUpperCase() + k.slice(1)).join("")} = createContext(...);
2015
+
2016
+ // or keep one context and let consumers select a slice:
2017
+ const ${c.changedKeys[0]} = useContextSelector(${ctxName}, (v) => v.${c.changedKeys[0]});`
1694
2018
  });
1695
2019
  continue;
1696
2020
  }
1697
- if (!AVOIDABLE.has(c.kind)) continue;
2021
+ if (!import_diff2.AVOIDABLE_KINDS.has(c.kind)) continue;
1698
2022
  if (isContext) {
1699
- 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.` });
2023
+ out.push({
2024
+ kind: "contextValue",
2025
+ owner: providerOwner || `${ctxName}.Provider`,
2026
+ target: r.component,
2027
+ prop: ctxName,
2028
+ label: `memoize the ${ctxName} provider value${providerOwner ? ` in <${providerOwner}>` : ""}`,
2029
+ detail: `<${r.component}> re-rendered because ${ctxName} produced a new value that is deep-equal to the previous one${providerOwner ? ` (Provider rendered by <${providerOwner}>)` : ""}.`,
2030
+ snippet: `// ${providerOwner || `where <${ctxName}.Provider> is rendered`}
2031
+ const value = useMemo(() => ({ /* ... */ }), [/* deps */]);
2032
+ <${ctxName}.Provider value={value}>`
2033
+ });
1700
2034
  } else if (c.hook === "useSyncExternalStore") {
1701
2035
  const chain = c.custom || [];
1702
2036
  const redux = chain.some((n) => /^use(App)?Selector$/.test(n));
1703
2037
  const zustand = !redux && chain.some((n) => /^use[A-Z]\w*Store$/.test(n) || n === "useStore" || n === "useBoundStore");
1704
- 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: (0, import_report.storeAdvice)(c.custom) });
2038
+ const via = chain.length ? ` via ${chain.join(" \u203A ")}` : "";
2039
+ out.push({
2040
+ kind: "storeSnapshot",
2041
+ owner: r.component,
2042
+ target: r.component,
2043
+ prop: c.path,
2044
+ label: redux ? `memoize the selector in <${r.component}>` : zustand ? `useShallow in <${r.component}>` : `stable getSnapshot in <${r.component}>`,
2045
+ 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.`,
2046
+ snippet: redux ? `// ${r.component}
2047
+ import { shallowEqual } from 'react-redux';
2048
+ const slice = useSelector(selectSlice, shallowEqual);
2049
+ // or memoize: const selectSlice = createSelector([selectA, selectB], (a, b) => ({ a, b }));` : zustand ? `// ${r.component}
2050
+ import { useShallow } from 'zustand/react/shallow';
2051
+ const { a, b } = useStore(useShallow((s) => ({ a: s.a, b: s.b })));
2052
+ // or select a primitive: const a = useStore((s) => s.a);` : `// ${r.component}
2053
+ // getSnapshot must return the same reference while the data is unchanged
2054
+ const snapshot = useSyncExternalStore(subscribe, store.getSnapshot /* cached */);`
2055
+ });
1705
2056
  } else {
1706
- 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).` });
2057
+ out.push({
2058
+ kind: "bailout",
2059
+ owner: r.component,
2060
+ target: r.component,
2061
+ prop: c.path,
2062
+ label: `bail out before setting ${c.path} in <${r.component}>`,
2063
+ detail: `${c.path} was set to a value deep-equal to the current one (new reference).`,
2064
+ snippet: `// ${r.component}
2065
+ setState((prev) => (deepEqual(prev, next) ? prev : next));`
2066
+ });
1707
2067
  }
1708
2068
  }
1709
2069
  return out;
@@ -1717,11 +2077,12 @@ var RerenderLens = (() => {
1717
2077
  const key = fixKey(f);
1718
2078
  let agg = byKey.get(key);
1719
2079
  if (!agg) {
1720
- agg = { ...f, key, count: 0, components: {} };
2080
+ agg = { ...f, key, count: 0, components: {}, reports: [] };
1721
2081
  byKey.set(key, agg);
1722
2082
  }
1723
2083
  agg.count++;
1724
2084
  agg.components[r.component] = (agg.components[r.component] || 0) + 1;
2085
+ if (agg.reports.length < MAX_FIX_REPORTS) agg.reports.push(r);
1725
2086
  }
1726
2087
  }
1727
2088
  return [...byKey.values()].sort((a, b) => b.count - a.count || a.label.localeCompare(b.label));
@@ -1731,6 +2092,8 @@ var RerenderLens = (() => {
1731
2092
  if (!ranked.length) return "No avoidable re-renders.";
1732
2093
  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(", ")})`);
1733
2094
  if (ranked.length > limit) lines.push(` \u2026 ${ranked.length - limit} more`);
2095
+ const roots = formatRootCauses(reports, 5);
2096
+ if (roots) lines.push("", "Root causes:", roots);
1734
2097
  return lines.join("\n");
1735
2098
  }
1736
2099
 
@@ -1836,7 +2199,7 @@ ${formatFixes(reports)}`);
1836
2199
  }
1837
2200
 
1838
2201
  // src/devtools.ts
1839
- var import_diff = __toESM(require_diff(), 1);
2202
+ var import_diff3 = __toESM(require_diff(), 1);
1840
2203
  var import_tracker = __toESM(require_tracker(), 1);
1841
2204
  var import_state = __toESM(require_state(), 1);
1842
2205
  var import_fiber = __toESM(require_fiber(), 1);
@@ -1862,7 +2225,7 @@ ${formatFixes(reports)}`);
1862
2225
  const next = (v) => serialize(v, maxDepth, seen, depth + 1, budget);
1863
2226
  seen.add(obj);
1864
2227
  try {
1865
- if ((0, import_diff.isReactElement)(obj)) {
2228
+ if ((0, import_diff3.isReactElement)(obj)) {
1866
2229
  const out2 = { $type: "element", name: (0, import_tracker.getDisplayName)(obj.type) };
1867
2230
  if (obj.key !== null && obj.key !== void 0) out2.key = String(obj.key);
1868
2231
  out2.props = next(obj.props);
@@ -1965,6 +2328,18 @@ ${formatFixes(reports)}`);
1965
2328
  target.postMessage(msg, "*");
1966
2329
  };
1967
2330
  const source = options.source ?? "page";
2331
+ const trackingSummary = () => {
2332
+ const s = (0, import_state.getState)();
2333
+ const o = s.options;
2334
+ return {
2335
+ mode: o.trackAllComponents ? "all" : o.trackAllMemoized ? "memoized" : "marked",
2336
+ include: (o.include ?? []).map(matcherToString).filter((x) => x !== null),
2337
+ exclude: (o.exclude ?? []).map(matcherToString).filter((x) => x !== null),
2338
+ renderedCount: s.seen.size,
2339
+ trackedCount: s.seenTracked.size,
2340
+ overflow: s.seenOverflow
2341
+ };
2342
+ };
1968
2343
  const info = () => ({
1969
2344
  count: buffer.length,
1970
2345
  library: import_version.VERSION,
@@ -1978,7 +2353,8 @@ ${formatFixes(reports)}`);
1978
2353
  scheduled: (0, import_state.getState)().scheduled,
1979
2354
  commits: (0, import_state.getState)().commits,
1980
2355
  overhead: { totalMs: (0, import_state.getState)().overheadMs, maxCommitMs: (0, import_state.getState)().maxCommitMs },
1981
- truncated: (0, import_state.getState)().truncated
2356
+ truncated: (0, import_state.getState)().truncated,
2357
+ tracking: trackingSummary()
1982
2358
  });
1983
2359
  const bridge = {
1984
2360
  replay: () => {
@@ -2024,6 +2400,10 @@ ${formatFixes(reports)}`);
2024
2400
  flashAvoidable: (on) => {
2025
2401
  flashOn = !!on;
2026
2402
  },
2403
+ explain: (target2) => {
2404
+ const comp = typeof target2 === "number" ? (0, import_fiber.fiberById)(target2) : (0, import_fiber.nearestComponent)((0, import_fiber.fiberForNode)(target2));
2405
+ return comp ? (0, import_tracker.explainTracking)((0, import_fiber.fiberType)(comp), (0, import_state.getState)().options) : null;
2406
+ },
2027
2407
  inspect: (node) => {
2028
2408
  const comp = (0, import_fiber.nearestComponent)((0, import_fiber.fiberForNode)(node));
2029
2409
  if (!comp) return null;
@@ -2038,6 +2418,7 @@ ${formatFixes(reports)}`);
2038
2418
  component: (0, import_fiber.fiberName)(comp),
2039
2419
  instanceId: id,
2040
2420
  tracked: (0, import_tracker.shouldTrack)((0, import_fiber.fiberType)(comp), (0, import_state.getState)().options),
2421
+ tracking: (0, import_tracker.explainTracking)((0, import_fiber.fiberType)(comp), (0, import_state.getState)().options),
2041
2422
  path,
2042
2423
  reports: id === null ? [] : buffer.filter((e) => e.instanceId === id).slice(-20).map((e) => e.payload)
2043
2424
  };
@@ -2072,6 +2453,9 @@ ${formatFixes(reports)}`);
2072
2453
  bridge.flashAvoidable(!!data.arg);
2073
2454
  reply.result = true;
2074
2455
  break;
2456
+ case "explain":
2457
+ reply.result = bridge.explain(data.arg);
2458
+ break;
2075
2459
  default:
2076
2460
  reply.error = `unknown command ${String(data.cmd)}`;
2077
2461
  }
@@ -2080,6 +2464,7 @@ ${formatFixes(reports)}`);
2080
2464
  }
2081
2465
  return reply;
2082
2466
  };
2467
+ const isRelayGreeting = (data) => !!data && typeof data === "object" && data.__rerenderLensRelay === true && typeof data.app === "string";
2083
2468
  const isCommand = (data) => !!data && typeof data === "object" && data.__rerenderLensCmd === true && typeof data.id === "string";
2084
2469
  const envelope = (type, payload) => ({ [DEVTOOLS_MARKER]: true, version: PROTOCOL_VERSION, type, payload });
2085
2470
  const sinks = [];
@@ -2102,14 +2487,20 @@ ${formatFixes(reports)}`);
2102
2487
  const relayUrl = options.relay ?? (typeof window !== "undefined" ? window.__RERENDER_LENS_RELAY__ : void 0);
2103
2488
  const ES = options.eventSource ?? (typeof EventSource === "function" ? EventSource : null);
2104
2489
  if (relayUrl && ES && typeof fetch === "function") {
2105
- const base = relayUrl.replace(/\/$/, "");
2490
+ const [rawBase, relayQuery = ""] = relayUrl.split("?");
2491
+ const base = (rawBase ?? "").replace(/\/$/, "");
2492
+ const relayToken = new URLSearchParams(relayQuery).get("token");
2493
+ const auth = relayToken ? `token=${encodeURIComponent(relayToken)}` : "";
2106
2494
  let queue = [];
2107
2495
  let scheduled = false;
2496
+ let appId = null;
2108
2497
  const flush = () => {
2109
2498
  scheduled = false;
2110
2499
  const batch = queue;
2111
2500
  queue = [];
2112
- fetch(`${base}/message`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(batch), keepalive: true }).catch(() => {
2501
+ const params = [appId ? `app=${encodeURIComponent(appId)}` : "", auth].filter(Boolean).join("&");
2502
+ const to = `${base}/message${params ? `?${params}` : ""}`;
2503
+ fetch(to, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(batch), keepalive: true }).catch(() => {
2113
2504
  });
2114
2505
  };
2115
2506
  const relaySend = (message) => {
@@ -2120,7 +2511,8 @@ ${formatFixes(reports)}`);
2120
2511
  }
2121
2512
  };
2122
2513
  try {
2123
- const stream = new ES(`${base}/events?role=app`);
2514
+ const label = typeof location !== "undefined" ? `${location.host}${location.pathname}`.slice(0, 120) : "";
2515
+ const stream = new ES(`${base}/events?role=app${label ? `&label=${encodeURIComponent(label)}` : ""}${auth ? `&${auth}` : ""}`);
2124
2516
  stream.onmessage = (event) => {
2125
2517
  let parsed;
2126
2518
  try {
@@ -2128,7 +2520,10 @@ ${formatFixes(reports)}`);
2128
2520
  } catch {
2129
2521
  return;
2130
2522
  }
2131
- for (const m of Array.isArray(parsed) ? parsed : [parsed]) if (isCommand(m)) relaySend(runCommand(m));
2523
+ for (const m of Array.isArray(parsed) ? parsed : [parsed]) {
2524
+ if (isRelayGreeting(m)) appId = m.app;
2525
+ else if (isCommand(m)) relaySend(runCommand(m));
2526
+ }
2132
2527
  };
2133
2528
  stream.onerror = () => {
2134
2529
  };