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
@@ -0,0 +1,46 @@
1
+ import { R as RenderReport, O as Options } from './types-DwNN5cWe.js';
2
+ import { C as Collector } from './notifiers-CqQW0Eum.js';
3
+
4
+ /** Environment variable carrying the path of the run's JSONL file from the reporter to the workers. */
5
+ declare const OUT_ENV = "RERENDER_LENS_OUT";
6
+ interface SetupOptions extends Omit<Options, 'notifier'> {
7
+ /** Also fail each test file that recorded avoidable re-renders (default false: the reporter decides). */
8
+ failFast?: boolean;
9
+ }
10
+ /** How the setup ties into the test runner (`{ afterAll }` from the runner's globals). */
11
+ interface SetupHooks {
12
+ afterAll?: (fn: () => void) => void;
13
+ /** Path of the current test file, for the reporter's output. */
14
+ testPath?: () => string | undefined;
15
+ }
16
+ interface LensSetup {
17
+ collector: Collector;
18
+ /** Append this file's reports to the shared output and clear the collector; registered with `afterAll` when given. */
19
+ flush(): void;
20
+ }
21
+ /**
22
+ * Call from a setup file. Creates the DevTools hook before react-dom loads, starts the library with
23
+ * a collector, and appends this file's reports to the shared output when it ends. The collector is
24
+ * returned for per-test assertions (`assertNoAvoidable`, `assertWithinBudget`).
25
+ */
26
+ declare function setupRerenderLens(options?: SetupOptions, hooks?: SetupHooks): LensSetup;
27
+ interface ReporterOptions {
28
+ /** Path to a budget JSON (`{ "*": 0, "Row": 2 }`). When set, the run fails on violations. */
29
+ budget?: string;
30
+ /** Print at most this many fixes. Default 10. */
31
+ limit?: number;
32
+ /** Also write the collected reports here as a panel-compatible export (`Sessions > Import`). */
33
+ exportTo?: string;
34
+ /** Where the lines go. Default `process.stdout`. */
35
+ write?: (line: string) => void;
36
+ }
37
+ interface RunResult {
38
+ /** Every report of the run, from every worker. */
39
+ reports: RenderReport[];
40
+ /** Set when a budget file was given and exceeded; the run must fail. */
41
+ error: Error | null;
42
+ }
43
+ /** Reports from every worker of a run. */
44
+ declare function readRun(file: string): RenderReport[];
45
+
46
+ export { type LensSetup as L, OUT_ENV as O, type ReporterOptions as R, type SetupHooks as S, type RunResult as a, type SetupOptions as b, readRun as r, setupRerenderLens as s };
@@ -0,0 +1,46 @@
1
+ import { R as RenderReport, O as Options } from './types-DwNN5cWe.cjs';
2
+ import { C as Collector } from './notifiers-B5poAEvE.cjs';
3
+
4
+ /** Environment variable carrying the path of the run's JSONL file from the reporter to the workers. */
5
+ declare const OUT_ENV = "RERENDER_LENS_OUT";
6
+ interface SetupOptions extends Omit<Options, 'notifier'> {
7
+ /** Also fail each test file that recorded avoidable re-renders (default false: the reporter decides). */
8
+ failFast?: boolean;
9
+ }
10
+ /** How the setup ties into the test runner (`{ afterAll }` from the runner's globals). */
11
+ interface SetupHooks {
12
+ afterAll?: (fn: () => void) => void;
13
+ /** Path of the current test file, for the reporter's output. */
14
+ testPath?: () => string | undefined;
15
+ }
16
+ interface LensSetup {
17
+ collector: Collector;
18
+ /** Append this file's reports to the shared output and clear the collector; registered with `afterAll` when given. */
19
+ flush(): void;
20
+ }
21
+ /**
22
+ * Call from a setup file. Creates the DevTools hook before react-dom loads, starts the library with
23
+ * a collector, and appends this file's reports to the shared output when it ends. The collector is
24
+ * returned for per-test assertions (`assertNoAvoidable`, `assertWithinBudget`).
25
+ */
26
+ declare function setupRerenderLens(options?: SetupOptions, hooks?: SetupHooks): LensSetup;
27
+ interface ReporterOptions {
28
+ /** Path to a budget JSON (`{ "*": 0, "Row": 2 }`). When set, the run fails on violations. */
29
+ budget?: string;
30
+ /** Print at most this many fixes. Default 10. */
31
+ limit?: number;
32
+ /** Also write the collected reports here as a panel-compatible export (`Sessions > Import`). */
33
+ exportTo?: string;
34
+ /** Where the lines go. Default `process.stdout`. */
35
+ write?: (line: string) => void;
36
+ }
37
+ interface RunResult {
38
+ /** Every report of the run, from every worker. */
39
+ reports: RenderReport[];
40
+ /** Set when a budget file was given and exceeded; the run must fail. */
41
+ error: Error | null;
42
+ }
43
+ /** Reports from every worker of a run. */
44
+ declare function readRun(file: string): RenderReport[];
45
+
46
+ export { type LensSetup as L, OUT_ENV as O, type ReporterOptions as R, type SetupHooks as S, type RunResult as a, type SetupOptions as b, readRun as r, setupRerenderLens as s };
package/dist/setup.cjs CHANGED
@@ -161,30 +161,30 @@ function diffRecords(prev, next, basePath = "") {
161
161
  if (Object.is(a, b)) continue;
162
162
  const kind = classify(a, b);
163
163
  if (kind === "different") {
164
- changes.push({ path: diffBudgetExhausted() ? path : firstDifferentPath(a, b, path), kind, prev: a, next: b });
164
+ changes.push({ path: diffBudgetExhausted() ? path : firstDifferentPath(a, b, path) ?? path, kind, prev: a, next: b });
165
165
  } else {
166
166
  changes.push({ path, kind, prev: a, next: b });
167
167
  }
168
168
  }
169
169
  return changes;
170
170
  }
171
- function firstDifferentPath(a, b, path, depth = 0) {
172
- if (depth > 8) return path;
171
+ function firstDifferentPath(a, b, path = "", depth = 0) {
172
+ if (depth > 8) return path || "(value)";
173
173
  if (Array.isArray(a) && Array.isArray(b)) {
174
- if (a.length !== b.length) return `${path}.length`;
174
+ if (a.length !== b.length) return path ? `${path}.length` : "length";
175
175
  for (let i = 0; i < a.length; i++) {
176
176
  if (!deepEqual(a[i], b[i])) return firstDifferentPath(a[i], b[i], joinPath(path, i), depth + 1);
177
177
  }
178
- return path;
178
+ return null;
179
179
  }
180
180
  if (isPlainObject(a) && isPlainObject(b)) {
181
181
  const keys = /* @__PURE__ */ new Set([...Object.keys(a), ...Object.keys(b)]);
182
182
  for (const k of keys) {
183
183
  if (!deepEqual(a[k], b[k])) return firstDifferentPath(a[k], b[k], joinPath(path, k), depth + 1);
184
184
  }
185
- return path;
185
+ return null;
186
186
  }
187
- return path;
187
+ return deepEqual(a, b) ? null : path || "(value)";
188
188
  }
189
189
 
190
190
  // src/report.ts
@@ -202,10 +202,12 @@ function storeAdvice(custom) {
202
202
  }
203
203
  var isGenuine = (c) => c.kind === "different" || c.kind === "added" || c.kind === "removed";
204
204
  var isChildren = (change) => change.path === "children";
205
+ var isRefObject = (change) => change.path === "ref" && change.kind === "deep-equal";
205
206
  function fixFor(change) {
206
207
  if (isChildren(change) && (change.kind === "element" || change.kind === "deep-equal")) {
207
208
  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`;
208
209
  }
210
+ if (isRefObject(change)) return `create the ref once with useRef (or createRef outside the render) and pass the same object every time`;
209
211
  switch (change.kind) {
210
212
  case "function":
211
213
  return `wrap it in useCallback (or hoist it out of the parent's render)`;
@@ -221,6 +223,7 @@ function describe(change) {
221
223
  if (isChildren(change) && (change.kind === "element" || change.kind === "deep-equal")) {
222
224
  return `children are new React elements with the same types and props on every render of the parent`;
223
225
  }
226
+ if (isRefObject(change)) return `prop "ref" is a new ref object on every render (React re-attaches it each time)`;
224
227
  switch (change.kind) {
225
228
  case "deep-equal":
226
229
  return `prop "${change.path}" is a new reference but deep-equal to the previous value`;
@@ -248,11 +251,21 @@ function buildReport(input) {
248
251
  if (causes.length === 0) trigger = "parent";
249
252
  else if (causes.length === 1) trigger = causes[0];
250
253
  else trigger = "mixed";
251
- const avoidable = trigger === "parent";
254
+ const nothingChanged = input.propChanges.length === 0 && stateChanges.length === 0 && hookChanges.length === 0;
255
+ const cacheHit = input.compiled === true && trigger === "parent" && input.propChanges.length === 0;
256
+ const revealed = input.revealed === true && trigger === "parent";
257
+ const avoidable = trigger === "parent" && !cacheHit && !revealed;
252
258
  const reasons = [];
253
- if (avoidable && input.propChanges.length === 0 && stateChanges.length === 0 && hookChanges.length === 0) {
259
+ const memoFix = input.classComponent ? `Make "${input.component}" extend PureComponent (or implement shouldComponentUpdate)` : `Wrap "${input.component}" in React.memo (or extend PureComponent)`;
260
+ if (cacheHit) {
261
+ reasons.push(`compiled by React Compiler: its output is memoized and the inputs did not change, so this render is cheap; nothing to fix.`);
262
+ } else if (revealed) {
263
+ reasons.push(
264
+ `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.`
265
+ );
266
+ } else if (avoidable && nothingChanged) {
254
267
  const who = input.parent ? `<${input.parent.name}> re-rendered (${describeTrigger(input.parent.trigger)})` : "its parent re-rendered";
255
- reasons.push(`re-rendered with identical props because ${who}. Wrap "${input.component}" in React.memo (or extend PureComponent).`);
268
+ reasons.push(`re-rendered with identical props because ${who}. ${memoFix}.`);
256
269
  } else if (avoidable && input.parent) {
257
270
  reasons.push(`caused by <${input.parent.name}> re-rendering (${describeTrigger(input.parent.trigger)}).`);
258
271
  }
@@ -261,9 +274,9 @@ function buildReport(input) {
261
274
  reasons.push(fix ? `${describe(c)}: ${fix}.` : `${describe(c)}.`);
262
275
  }
263
276
  const memoized = input.memoized !== false;
264
- if (avoidable && !memoized && input.propChanges.length > 0) {
277
+ if (avoidable && !memoized && !input.compiled && input.propChanges.length > 0) {
265
278
  reasons.push(
266
- `"${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).`
279
+ `"${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)"}.`
267
280
  );
268
281
  }
269
282
  for (const c of stateChanges) {
@@ -275,6 +288,8 @@ function buildReport(input) {
275
288
  const where = c.provider && c.provider.component ? ` (provided by <${c.provider.component}>)` : "";
276
289
  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` : "";
277
290
  reasons.push(`${c.path} changed${where}${keys}.`);
291
+ } else if (c.hook === "useDeferredValue" && isGenuine(c)) {
292
+ 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.`);
278
293
  } else if (isGenuine(c)) reasons.push(`${hookLabel(c)} changed.`);
279
294
  else if (isStateHook(c))
280
295
  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.`);
@@ -305,6 +320,7 @@ function buildReport(input) {
305
320
  reasons,
306
321
  time: now()
307
322
  };
323
+ if (input.compiled) report.compiled = true;
308
324
  if (input.selfDuration !== void 0) report.selfDuration = input.selfDuration;
309
325
  if (input.treeDuration !== void 0) report.treeDuration = input.treeDuration;
310
326
  if (input.commitPriority) report.commitPriority = input.commitPriority;
@@ -342,10 +358,10 @@ var KIND_LABEL = {
342
358
  };
343
359
  function summarize(report) {
344
360
  const counts2 = /* @__PURE__ */ new Map();
345
- for (const c of [...report.propChanges, ...report.stateChanges, ...report.hookChanges]) {
361
+ for (const c of [...report.propChanges || [], ...report.stateChanges || [], ...report.hookChanges || []]) {
346
362
  counts2.set(c.kind, (counts2.get(c.kind) ?? 0) + 1);
347
363
  }
348
- const parts = [...counts2].map(([kind, n]) => `${n} ${KIND_LABEL[kind]}`);
364
+ const parts = [...counts2].map(([kind, n]) => `${n} ${KIND_LABEL[kind] || kind}`);
349
365
  if (parts.length === 0) parts.push("no changes");
350
366
  return parts.join(", ");
351
367
  }
@@ -367,16 +383,27 @@ function printReport(report, options) {
367
383
  }
368
384
 
369
385
  // src/state.ts
386
+ var MAX_SEEN_COMPONENTS = 500;
370
387
  var KEY = /* @__PURE__ */ Symbol.for("rerender-lens.state");
371
388
  function getState() {
372
389
  const g = globalThis;
373
390
  let s = g[KEY];
374
391
  if (!s) {
375
- s = { options: {}, enabled: false, printed: /* @__PURE__ */ new Map(), detach: null, nextInstanceId: 1, nextCommitId: 1, warnedOnce: /* @__PURE__ */ new Set(), scheduled: 0, commits: 0, overheadMs: 0, maxCommitMs: 0, truncated: 0 };
392
+ 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 };
376
393
  g[KEY] = s;
377
394
  }
378
395
  return s;
379
396
  }
397
+ function noteRendered(s, name, tracked) {
398
+ if (!s.seen.has(name)) {
399
+ if (s.seen.size >= MAX_SEEN_COMPONENTS) {
400
+ s.seenOverflow = true;
401
+ return;
402
+ }
403
+ s.seen.add(name);
404
+ }
405
+ if (tracked) s.seenTracked.add(name);
406
+ }
380
407
  function warnOnce(key, message) {
381
408
  const s = getState();
382
409
  if (s.warnedOnce.has(key)) return;
@@ -408,6 +435,27 @@ function dispatch(report, override) {
408
435
  }
409
436
 
410
437
  // src/hookNames.ts
438
+ var NODE_COUNT = {
439
+ useState: 1,
440
+ useReducer: 1,
441
+ useRef: 1,
442
+ useMemo: 1,
443
+ useCallback: 1,
444
+ useEffect: 1,
445
+ useLayoutEffect: 1,
446
+ useInsertionEffect: 1,
447
+ useImperativeHandle: 1,
448
+ useSyncExternalStore: 2,
449
+ useTransition: 2,
450
+ useDeferredValue: 1,
451
+ useId: 1,
452
+ useOptimistic: 1,
453
+ useActionState: 3,
454
+ useFormState: 3,
455
+ useEffectEvent: 1,
456
+ useCacheRefresh: 1
457
+ };
458
+ var BUILTIN_HOOKS = /* @__PURE__ */ new Set([...Object.keys(NODE_COUNT), "useContext", "useDebugValue", "use"]);
411
459
  var noop = () => {
412
460
  };
413
461
  function customHooksFromStack(stack, componentName, marker) {
@@ -422,7 +470,7 @@ function customHooksFromStack(stack, componentName, marker) {
422
470
  continue;
423
471
  }
424
472
  if (name === componentName || name.endsWith("." + componentName)) break;
425
- if (/^use[A-Z0-9_]/.test(name)) names.push(name);
473
+ if (/^use[A-Z0-9_]/.test(name) && !BUILTIN_HOOKS.has(name)) names.push(name);
426
474
  if (names.length > 8) break;
427
475
  }
428
476
  return names;
@@ -722,6 +770,11 @@ function isMemoizedFiber(fiber) {
722
770
  }
723
771
  return false;
724
772
  }
773
+ function isCompiledFiber(fiber) {
774
+ if (fiber.tag === ClassComponent) return false;
775
+ const cache2 = fiber.updateQueue?.memoCache;
776
+ return !!cache2 && Array.isArray(cache2.data);
777
+ }
725
778
  function durationsOf(fiber) {
726
779
  if (typeof fiber.actualDuration !== "number") return null;
727
780
  const tree = fiber.actualDuration;
@@ -900,7 +953,8 @@ function diffHooks(fiber, alt) {
900
953
  const labels = hookLabelsFor(fiber, b);
901
954
  let i = 0;
902
955
  while (a && b) {
903
- if ((isStateNode(b) || isStoreNode(b)) && !Object.is(a.memoizedState, b.memoizedState)) {
956
+ const deferred = labels?.[i] === "useDeferredValue";
957
+ if ((deferred || isStateNode(b) || isStoreNode(b)) && !Object.is(a.memoizedState, b.memoizedState)) {
904
958
  const hook = labels?.[i] ?? hookLabel2(b);
905
959
  out.push({
906
960
  path: `${hook}#${i}`,
@@ -934,6 +988,10 @@ function diffContexts(fiber, alt) {
934
988
  let a = alt.dependencies?.firstContext ?? null;
935
989
  let b = fiber.dependencies?.firstContext ?? null;
936
990
  let i = 0;
991
+ if (b && !("memoizedValue" in b)) {
992
+ warnOnce("contextValues", "this React does not record context values on the fiber (React < 18): context changes are reported as plain parent re-renders.");
993
+ return out;
994
+ }
937
995
  while (a && b) {
938
996
  if (a.context === b.context && !Object.is(a.memoizedValue, b.memoizedValue)) {
939
997
  const name = b.context.displayName ?? "Context";
@@ -966,10 +1024,26 @@ function diffContexts(fiber, alt) {
966
1024
  return out;
967
1025
  }
968
1026
  var isGenuine2 = (c) => c.kind === "different" || c.kind === "added" || c.kind === "removed";
1027
+ var isRefObject2 = (v) => {
1028
+ if (typeof v !== "object" || v === null) return false;
1029
+ const keys = Object.keys(v);
1030
+ return keys.length === 1 && keys[0] === "current";
1031
+ };
1032
+ function diffProps(prev, next) {
1033
+ if (prev === next) return [];
1034
+ const pr = prev.ref;
1035
+ const nr = next.ref;
1036
+ if (pr !== nr && isRefObject2(pr) && isRefObject2(nr)) {
1037
+ const { ref: _p, ...restPrev } = prev;
1038
+ const { ref: _n, ...restNext } = next;
1039
+ return [{ path: "ref", kind: "deep-equal", prev: pr, next: nr }, ...diffRecords(restPrev, restNext)];
1040
+ }
1041
+ return diffRecords(prev, next);
1042
+ }
969
1043
  function analyze(fiber, alt, trackHooks) {
970
1044
  const prevProps = alt.memoizedProps ?? {};
971
1045
  const nextProps = fiber.memoizedProps ?? {};
972
- const propChanges = prevProps === nextProps ? [] : diffRecords(prevProps, nextProps);
1046
+ const propChanges = diffProps(prevProps, nextProps);
973
1047
  let stateChanges = [];
974
1048
  let hookChanges = [];
975
1049
  if (fiber.tag === ClassComponent) {
@@ -982,7 +1056,7 @@ function analyze(fiber, alt, trackHooks) {
982
1056
  } else if (trackHooks) {
983
1057
  hookChanges = [...diffHooks(fiber, alt), ...diffContexts(fiber, alt)];
984
1058
  }
985
- const isStateHook = (h) => h.hook !== "useContext" && h.hook !== "useSyncExternalStore";
1059
+ const isStateHook = (h) => h.hook !== "useContext" && h.hook !== "useSyncExternalStore" && h.hook !== "useDeferredValue";
986
1060
  const causes = [];
987
1061
  if (propChanges.some(isGenuine2)) causes.push("props");
988
1062
  if (stateChanges.some(isGenuine2) || hookChanges.some((h) => isStateHook(h) && isGenuine2(h))) causes.push("state");
@@ -1022,6 +1096,24 @@ var lastCommit = null;
1022
1096
  var EFFECT_LOOP_WINDOW_MS = 50;
1023
1097
  var MAX_REPORTS_PER_COMMIT = 200;
1024
1098
  var COMMIT_TIME_BUDGET_MS = 25;
1099
+ function isDeferredCatchUp(fiber) {
1100
+ const types = fiber._debugHookTypes;
1101
+ if (!types || !types.includes("useDeferredValue") || !fiber.alternate) return false;
1102
+ const first = fiber.memoizedState;
1103
+ if (!isHookList(first)) return false;
1104
+ const labels = hookLabelsFor(fiber, first);
1105
+ if (!labels) return false;
1106
+ let a = fiber.alternate.memoizedState;
1107
+ let b = first;
1108
+ for (let i = 0; a && b; a = a.next, b = b.next, i++) {
1109
+ if (labels[i] === "useDeferredValue" && !Object.is(a.memoizedState, b.memoizedState)) return true;
1110
+ }
1111
+ return false;
1112
+ }
1113
+ function isUnder(fiber, boundaries) {
1114
+ for (let f = fiber.return; f && f.tag !== HostRoot; f = f.return) if (f.tag === SuspenseComponent && boundaries.includes(f)) return true;
1115
+ return false;
1116
+ }
1025
1117
  function onCommit(root, commitPriority) {
1026
1118
  const s = getState();
1027
1119
  if (!s.enabled) return;
@@ -1031,17 +1123,20 @@ function onCommit(root, commitPriority) {
1031
1123
  const rendered = [];
1032
1124
  const renderedNames = /* @__PURE__ */ new Set();
1033
1125
  let hot = false;
1034
- let suspenseResolved = false;
1126
+ let resolvedBoundaries = null;
1035
1127
  const stack = [root.current];
1036
1128
  while (stack.length) {
1037
1129
  const fiber = stack.pop();
1038
1130
  const alt = fiber.alternate;
1039
1131
  if (alt && isComponentTag(fiber.tag) && didRender(fiber)) {
1040
1132
  if (isHotSwapped(fiber, alt)) hot = true;
1041
- renderedNames.add(fiberName(fiber));
1042
- if (shouldTrack(fiberType(fiber), o)) rendered.push(fiber);
1133
+ const name = fiberName(fiber);
1134
+ renderedNames.add(name);
1135
+ const tracked = shouldTrack(fiberType(fiber), o);
1136
+ noteRendered(s, name, tracked);
1137
+ if (tracked) rendered.push(fiber);
1043
1138
  }
1044
- if (fiber.tag === SuspenseComponent && alt && alt.memoizedState !== null && fiber.memoizedState === null) suspenseResolved = true;
1139
+ if (fiber.tag === SuspenseComponent && alt && alt.memoizedState !== null && fiber.memoizedState === null) (resolvedBoundaries ?? (resolvedBoundaries = [])).push(fiber);
1045
1140
  if (!alt || fiber.child !== alt.child) {
1046
1141
  for (let c = fiber.child; c; c = c.sibling) stack.push(c);
1047
1142
  }
@@ -1053,10 +1148,10 @@ function onCommit(root, commitPriority) {
1053
1148
  let commitCause;
1054
1149
  let afterCommit;
1055
1150
  const inputDriven = commitPriority === "immediate" || commitPriority === "user-blocking";
1056
- if (!inputDriven && lastCommit && at - lastCommit.at < EFFECT_LOOP_WINDOW_MS && updaters.some((u) => lastCommit.rendered.has(u))) {
1151
+ if (!inputDriven && lastCommit && at - lastCommit.at < EFFECT_LOOP_WINDOW_MS && updaters.some((u) => lastCommit.rendered.has(u)) && !rendered.some(isDeferredCatchUp)) {
1057
1152
  commitCause = "effect-after-commit";
1058
1153
  afterCommit = lastCommit.id;
1059
- } else if (suspenseResolved) commitCause = "suspense-resolved";
1154
+ } else if (resolvedBoundaries) commitCause = "suspense-resolved";
1060
1155
  if (rendered.length === 0) {
1061
1156
  lastCommit = { id: s.nextCommitId, at, rendered: renderedNames };
1062
1157
  return;
@@ -1108,6 +1203,9 @@ function onCommit(root, commitPriority) {
1108
1203
  owner: ownerName(fiber),
1109
1204
  path: componentPath(fiber),
1110
1205
  memoized: isMemoizedFiber(fiber),
1206
+ compiled: isCompiledFiber(fiber),
1207
+ classComponent: fiber.tag === ClassComponent,
1208
+ revealed: resolvedBoundaries !== null && a.trigger === "parent" && isUnder(fiber, resolvedBoundaries),
1111
1209
  selfDuration: durations ? durations.self : void 0,
1112
1210
  treeDuration: durations ? durations.tree : void 0,
1113
1211
  commitId,
@@ -1161,10 +1259,53 @@ function shouldTrack(type, o) {
1161
1259
  if (o.trackAllMemoized) return isMemo(type) || isPureClass(type);
1162
1260
  return false;
1163
1261
  }
1262
+ var ANONYMOUS = "Anonymous";
1263
+ function explainTracking(type, o) {
1264
+ if (typeof type === "string")
1265
+ return { tracked: false, name: type, memoized: false, reason: `<${type}> is a DOM element, not a React component; only components are tracked.` };
1266
+ if (!isComponentLike(type))
1267
+ return { tracked: false, name: ANONYMOUS, memoized: false, reason: "This is not a React component type, so it is never tracked." };
1268
+ const name = getDisplayName(type);
1269
+ const memo = isMemo(type);
1270
+ const memoized = memo || isPureClass(type);
1271
+ if (o.exclude?.some((m) => matches(m, name)))
1272
+ return { tracked: false, name, memoized, reason: `<${name}> matches "exclude", which wins over every other rule.`, fix: `Remove "${name}" from the exclude list.` };
1273
+ if (hasMarker(type)) return { tracked: true, name, memoized, reason: `<${name}> is marked with track() (the ${MARKER} static).` };
1274
+ if (o.include?.some((m) => matches(m, name))) return { tracked: true, name, memoized, reason: `<${name}> matches "include".` };
1275
+ if (o.trackAllComponents) return { tracked: true, name, memoized, reason: '"Track every component" is on, so every component is tracked.' };
1276
+ if (o.trackAllMemoized && memoized)
1277
+ return { tracked: true, name, memoized, reason: `<${name}> is ${memo ? "a React.memo component" : "a PureComponent"}, and "Track every React.memo and PureComponent" is on.` };
1278
+ if (name === ANONYMOUS)
1279
+ return {
1280
+ tracked: false,
1281
+ name,
1282
+ memoized,
1283
+ reason: 'This component has no display name, so "include" and "exclude" cannot address it.',
1284
+ fix: "Give the function a name, set displayName, or mark it with track(fn, 'Name')."
1285
+ };
1286
+ if (o.trackAllMemoized)
1287
+ return {
1288
+ tracked: false,
1289
+ name,
1290
+ memoized,
1291
+ reason: `"Track every React.memo and PureComponent" only covers React.memo components and PureComponent classes, and <${name}> is neither.`,
1292
+ fix: `Add "${name}" to include, wrap it in React.memo, or turn on "Track every component".`
1293
+ };
1294
+ return {
1295
+ tracked: false,
1296
+ name,
1297
+ memoized,
1298
+ reason: `Nothing selects <${name}>: it is not marked with track(), no "include" pattern matches it, and neither "track every" option is on.`,
1299
+ fix: `Add "${name}" to include, or turn on "Track every React.memo and PureComponent" / "Track every component".`
1300
+ };
1301
+ }
1164
1302
  function init(options = {}) {
1165
1303
  const s = getState();
1166
1304
  s.options = { ...options };
1167
1305
  s.printed.clear();
1306
+ s.seen.clear();
1307
+ s.seenTracked.clear();
1308
+ s.seenOverflow = false;
1168
1309
  if (!s.enabled) {
1169
1310
  s.detach = attach();
1170
1311
  s.enabled = true;
@@ -1290,7 +1431,7 @@ function flash(target, duration = 500) {
1290
1431
  }
1291
1432
 
1292
1433
  // src/version.ts
1293
- var VERSION = "0.4.0" ;
1434
+ var VERSION = "0.7.0" ;
1294
1435
 
1295
1436
  // src/devtools.ts
1296
1437
  var DEVTOOLS_MARKER = "__rerenderLens";
@@ -1416,6 +1557,18 @@ function createDevtoolsNotifier(options = {}) {
1416
1557
  target.postMessage(msg, "*");
1417
1558
  };
1418
1559
  const source = options.source ?? "page";
1560
+ const trackingSummary = () => {
1561
+ const s = getState();
1562
+ const o = s.options;
1563
+ return {
1564
+ mode: o.trackAllComponents ? "all" : o.trackAllMemoized ? "memoized" : "marked",
1565
+ include: (o.include ?? []).map(matcherToString).filter((x) => x !== null),
1566
+ exclude: (o.exclude ?? []).map(matcherToString).filter((x) => x !== null),
1567
+ renderedCount: s.seen.size,
1568
+ trackedCount: s.seenTracked.size,
1569
+ overflow: s.seenOverflow
1570
+ };
1571
+ };
1419
1572
  const info = () => ({
1420
1573
  count: buffer.length,
1421
1574
  library: VERSION,
@@ -1429,7 +1582,8 @@ function createDevtoolsNotifier(options = {}) {
1429
1582
  scheduled: getState().scheduled,
1430
1583
  commits: getState().commits,
1431
1584
  overhead: { totalMs: getState().overheadMs, maxCommitMs: getState().maxCommitMs },
1432
- truncated: getState().truncated
1585
+ truncated: getState().truncated,
1586
+ tracking: trackingSummary()
1433
1587
  });
1434
1588
  const bridge = {
1435
1589
  replay: () => {
@@ -1475,6 +1629,10 @@ function createDevtoolsNotifier(options = {}) {
1475
1629
  flashAvoidable: (on) => {
1476
1630
  flashOn = !!on;
1477
1631
  },
1632
+ explain: (target2) => {
1633
+ const comp = typeof target2 === "number" ? fiberById(target2) : nearestComponent(fiberForNode(target2));
1634
+ return comp ? explainTracking(fiberType(comp), getState().options) : null;
1635
+ },
1478
1636
  inspect: (node) => {
1479
1637
  const comp = nearestComponent(fiberForNode(node));
1480
1638
  if (!comp) return null;
@@ -1489,6 +1647,7 @@ function createDevtoolsNotifier(options = {}) {
1489
1647
  component: fiberName(comp),
1490
1648
  instanceId: id,
1491
1649
  tracked: shouldTrack(fiberType(comp), getState().options),
1650
+ tracking: explainTracking(fiberType(comp), getState().options),
1492
1651
  path,
1493
1652
  reports: id === null ? [] : buffer.filter((e) => e.instanceId === id).slice(-20).map((e) => e.payload)
1494
1653
  };
@@ -1523,6 +1682,9 @@ function createDevtoolsNotifier(options = {}) {
1523
1682
  bridge.flashAvoidable(!!data.arg);
1524
1683
  reply.result = true;
1525
1684
  break;
1685
+ case "explain":
1686
+ reply.result = bridge.explain(data.arg);
1687
+ break;
1526
1688
  default:
1527
1689
  reply.error = `unknown command ${String(data.cmd)}`;
1528
1690
  }
@@ -1531,6 +1693,7 @@ function createDevtoolsNotifier(options = {}) {
1531
1693
  }
1532
1694
  return reply;
1533
1695
  };
1696
+ const isRelayGreeting = (data) => !!data && typeof data === "object" && data.__rerenderLensRelay === true && typeof data.app === "string";
1534
1697
  const isCommand = (data) => !!data && typeof data === "object" && data.__rerenderLensCmd === true && typeof data.id === "string";
1535
1698
  const envelope = (type, payload) => ({ [DEVTOOLS_MARKER]: true, version: PROTOCOL_VERSION, type, payload });
1536
1699
  const sinks = [];
@@ -1553,14 +1716,20 @@ function createDevtoolsNotifier(options = {}) {
1553
1716
  const relayUrl = options.relay ?? (typeof window !== "undefined" ? window.__RERENDER_LENS_RELAY__ : void 0);
1554
1717
  const ES = options.eventSource ?? (typeof EventSource === "function" ? EventSource : null);
1555
1718
  if (relayUrl && ES && typeof fetch === "function") {
1556
- const base = relayUrl.replace(/\/$/, "");
1719
+ const [rawBase, relayQuery = ""] = relayUrl.split("?");
1720
+ const base = (rawBase ?? "").replace(/\/$/, "");
1721
+ const relayToken = new URLSearchParams(relayQuery).get("token");
1722
+ const auth = relayToken ? `token=${encodeURIComponent(relayToken)}` : "";
1557
1723
  let queue = [];
1558
1724
  let scheduled = false;
1725
+ let appId = null;
1559
1726
  const flush = () => {
1560
1727
  scheduled = false;
1561
1728
  const batch = queue;
1562
1729
  queue = [];
1563
- fetch(`${base}/message`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(batch), keepalive: true }).catch(() => {
1730
+ const params = [appId ? `app=${encodeURIComponent(appId)}` : "", auth].filter(Boolean).join("&");
1731
+ const to = `${base}/message${params ? `?${params}` : ""}`;
1732
+ fetch(to, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(batch), keepalive: true }).catch(() => {
1564
1733
  });
1565
1734
  };
1566
1735
  const relaySend = (message) => {
@@ -1571,7 +1740,8 @@ function createDevtoolsNotifier(options = {}) {
1571
1740
  }
1572
1741
  };
1573
1742
  try {
1574
- const stream = new ES(`${base}/events?role=app`);
1743
+ const label = typeof location !== "undefined" ? `${location.host}${location.pathname}`.slice(0, 120) : "";
1744
+ const stream = new ES(`${base}/events?role=app${label ? `&label=${encodeURIComponent(label)}` : ""}${auth ? `&${auth}` : ""}`);
1575
1745
  stream.onmessage = (event) => {
1576
1746
  let parsed;
1577
1747
  try {
@@ -1579,7 +1749,10 @@ function createDevtoolsNotifier(options = {}) {
1579
1749
  } catch {
1580
1750
  return;
1581
1751
  }
1582
- for (const m of Array.isArray(parsed) ? parsed : [parsed]) if (isCommand(m)) relaySend(runCommand(m));
1752
+ for (const m of Array.isArray(parsed) ? parsed : [parsed]) {
1753
+ if (isRelayGreeting(m)) appId = m.app;
1754
+ else if (isCommand(m)) relaySend(runCommand(m));
1755
+ }
1583
1756
  };
1584
1757
  stream.onerror = () => {
1585
1758
  };
@@ -1623,15 +1796,16 @@ function createDevtoolsNotifier(options = {}) {
1623
1796
  }
1624
1797
 
1625
1798
  // src/setup.ts
1626
- var env = (key) => {
1799
+ var read = (get) => {
1627
1800
  try {
1628
- return typeof process !== "undefined" && process && process.env ? process.env[key] : void 0;
1801
+ return get();
1629
1802
  } catch {
1630
1803
  return void 0;
1631
1804
  }
1632
1805
  };
1633
- if (env("NODE_ENV") !== "production" && typeof window !== "undefined" && !window.__RERENDER_LENS_DEVTOOLS__) {
1634
- const relay = env("RERENDER_LENS_RELAY") || env("NEXT_PUBLIC_RERENDER_LENS_RELAY");
1806
+ var production = read(() => process.env.NODE_ENV) === "production";
1807
+ if (!production && typeof window !== "undefined" && !window.__RERENDER_LENS_DEVTOOLS__) {
1808
+ const relay = read(() => process.env.RERENDER_LENS_RELAY) || read(() => process.env.NEXT_PUBLIC_RERENDER_LENS_RELAY);
1635
1809
  init({ trackAllMemoized: true, notifier: createDevtoolsNotifier(relay ? { relay } : {}) });
1636
1810
  }
1637
1811
  //# sourceMappingURL=setup.cjs.map