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
@@ -0,0 +1,46 @@
1
+ import { R as RenderReport, O as Options } from './types-DW5-N2XH.cjs';
2
+ import { C as Collector } from './notifiers-DwKlvv4H.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 };
@@ -0,0 +1,46 @@
1
+ import { R as RenderReport, O as Options } from './types-DW5-N2XH.js';
2
+ import { C as Collector } from './notifiers-FNzKHs0a.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 };
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
  }
@@ -408,6 +424,27 @@ function dispatch(report, override) {
408
424
  }
409
425
 
410
426
  // src/hookNames.ts
427
+ var NODE_COUNT = {
428
+ useState: 1,
429
+ useReducer: 1,
430
+ useRef: 1,
431
+ useMemo: 1,
432
+ useCallback: 1,
433
+ useEffect: 1,
434
+ useLayoutEffect: 1,
435
+ useInsertionEffect: 1,
436
+ useImperativeHandle: 1,
437
+ useSyncExternalStore: 2,
438
+ useTransition: 2,
439
+ useDeferredValue: 1,
440
+ useId: 1,
441
+ useOptimistic: 1,
442
+ useActionState: 3,
443
+ useFormState: 3,
444
+ useEffectEvent: 1,
445
+ useCacheRefresh: 1
446
+ };
447
+ var BUILTIN_HOOKS = /* @__PURE__ */ new Set([...Object.keys(NODE_COUNT), "useContext", "useDebugValue", "use"]);
411
448
  var noop = () => {
412
449
  };
413
450
  function customHooksFromStack(stack, componentName, marker) {
@@ -422,7 +459,7 @@ function customHooksFromStack(stack, componentName, marker) {
422
459
  continue;
423
460
  }
424
461
  if (name === componentName || name.endsWith("." + componentName)) break;
425
- if (/^use[A-Z0-9_]/.test(name)) names.push(name);
462
+ if (/^use[A-Z0-9_]/.test(name) && !BUILTIN_HOOKS.has(name)) names.push(name);
426
463
  if (names.length > 8) break;
427
464
  }
428
465
  return names;
@@ -722,6 +759,11 @@ function isMemoizedFiber(fiber) {
722
759
  }
723
760
  return false;
724
761
  }
762
+ function isCompiledFiber(fiber) {
763
+ if (fiber.tag === ClassComponent) return false;
764
+ const cache2 = fiber.updateQueue?.memoCache;
765
+ return !!cache2 && Array.isArray(cache2.data);
766
+ }
725
767
  function durationsOf(fiber) {
726
768
  if (typeof fiber.actualDuration !== "number") return null;
727
769
  const tree = fiber.actualDuration;
@@ -900,7 +942,8 @@ function diffHooks(fiber, alt) {
900
942
  const labels = hookLabelsFor(fiber, b);
901
943
  let i = 0;
902
944
  while (a && b) {
903
- if ((isStateNode(b) || isStoreNode(b)) && !Object.is(a.memoizedState, b.memoizedState)) {
945
+ const deferred = labels?.[i] === "useDeferredValue";
946
+ if ((deferred || isStateNode(b) || isStoreNode(b)) && !Object.is(a.memoizedState, b.memoizedState)) {
904
947
  const hook = labels?.[i] ?? hookLabel2(b);
905
948
  out.push({
906
949
  path: `${hook}#${i}`,
@@ -934,6 +977,10 @@ function diffContexts(fiber, alt) {
934
977
  let a = alt.dependencies?.firstContext ?? null;
935
978
  let b = fiber.dependencies?.firstContext ?? null;
936
979
  let i = 0;
980
+ if (b && !("memoizedValue" in b)) {
981
+ warnOnce("contextValues", "this React does not record context values on the fiber (React < 18): context changes are reported as plain parent re-renders.");
982
+ return out;
983
+ }
937
984
  while (a && b) {
938
985
  if (a.context === b.context && !Object.is(a.memoizedValue, b.memoizedValue)) {
939
986
  const name = b.context.displayName ?? "Context";
@@ -966,10 +1013,26 @@ function diffContexts(fiber, alt) {
966
1013
  return out;
967
1014
  }
968
1015
  var isGenuine2 = (c) => c.kind === "different" || c.kind === "added" || c.kind === "removed";
1016
+ var isRefObject2 = (v) => {
1017
+ if (typeof v !== "object" || v === null) return false;
1018
+ const keys = Object.keys(v);
1019
+ return keys.length === 1 && keys[0] === "current";
1020
+ };
1021
+ function diffProps(prev, next) {
1022
+ if (prev === next) return [];
1023
+ const pr = prev.ref;
1024
+ const nr = next.ref;
1025
+ if (pr !== nr && isRefObject2(pr) && isRefObject2(nr)) {
1026
+ const { ref: _p, ...restPrev } = prev;
1027
+ const { ref: _n, ...restNext } = next;
1028
+ return [{ path: "ref", kind: "deep-equal", prev: pr, next: nr }, ...diffRecords(restPrev, restNext)];
1029
+ }
1030
+ return diffRecords(prev, next);
1031
+ }
969
1032
  function analyze(fiber, alt, trackHooks) {
970
1033
  const prevProps = alt.memoizedProps ?? {};
971
1034
  const nextProps = fiber.memoizedProps ?? {};
972
- const propChanges = prevProps === nextProps ? [] : diffRecords(prevProps, nextProps);
1035
+ const propChanges = diffProps(prevProps, nextProps);
973
1036
  let stateChanges = [];
974
1037
  let hookChanges = [];
975
1038
  if (fiber.tag === ClassComponent) {
@@ -982,7 +1045,7 @@ function analyze(fiber, alt, trackHooks) {
982
1045
  } else if (trackHooks) {
983
1046
  hookChanges = [...diffHooks(fiber, alt), ...diffContexts(fiber, alt)];
984
1047
  }
985
- const isStateHook = (h) => h.hook !== "useContext" && h.hook !== "useSyncExternalStore";
1048
+ const isStateHook = (h) => h.hook !== "useContext" && h.hook !== "useSyncExternalStore" && h.hook !== "useDeferredValue";
986
1049
  const causes = [];
987
1050
  if (propChanges.some(isGenuine2)) causes.push("props");
988
1051
  if (stateChanges.some(isGenuine2) || hookChanges.some((h) => isStateHook(h) && isGenuine2(h))) causes.push("state");
@@ -1022,6 +1085,24 @@ var lastCommit = null;
1022
1085
  var EFFECT_LOOP_WINDOW_MS = 50;
1023
1086
  var MAX_REPORTS_PER_COMMIT = 200;
1024
1087
  var COMMIT_TIME_BUDGET_MS = 25;
1088
+ function isDeferredCatchUp(fiber) {
1089
+ const types = fiber._debugHookTypes;
1090
+ if (!types || !types.includes("useDeferredValue") || !fiber.alternate) return false;
1091
+ const first = fiber.memoizedState;
1092
+ if (!isHookList(first)) return false;
1093
+ const labels = hookLabelsFor(fiber, first);
1094
+ if (!labels) return false;
1095
+ let a = fiber.alternate.memoizedState;
1096
+ let b = first;
1097
+ for (let i = 0; a && b; a = a.next, b = b.next, i++) {
1098
+ if (labels[i] === "useDeferredValue" && !Object.is(a.memoizedState, b.memoizedState)) return true;
1099
+ }
1100
+ return false;
1101
+ }
1102
+ function isUnder(fiber, boundaries) {
1103
+ for (let f = fiber.return; f && f.tag !== HostRoot; f = f.return) if (f.tag === SuspenseComponent && boundaries.includes(f)) return true;
1104
+ return false;
1105
+ }
1025
1106
  function onCommit(root, commitPriority) {
1026
1107
  const s = getState();
1027
1108
  if (!s.enabled) return;
@@ -1031,7 +1112,7 @@ function onCommit(root, commitPriority) {
1031
1112
  const rendered = [];
1032
1113
  const renderedNames = /* @__PURE__ */ new Set();
1033
1114
  let hot = false;
1034
- let suspenseResolved = false;
1115
+ let resolvedBoundaries = null;
1035
1116
  const stack = [root.current];
1036
1117
  while (stack.length) {
1037
1118
  const fiber = stack.pop();
@@ -1041,7 +1122,7 @@ function onCommit(root, commitPriority) {
1041
1122
  renderedNames.add(fiberName(fiber));
1042
1123
  if (shouldTrack(fiberType(fiber), o)) rendered.push(fiber);
1043
1124
  }
1044
- if (fiber.tag === SuspenseComponent && alt && alt.memoizedState !== null && fiber.memoizedState === null) suspenseResolved = true;
1125
+ if (fiber.tag === SuspenseComponent && alt && alt.memoizedState !== null && fiber.memoizedState === null) (resolvedBoundaries ?? (resolvedBoundaries = [])).push(fiber);
1045
1126
  if (!alt || fiber.child !== alt.child) {
1046
1127
  for (let c = fiber.child; c; c = c.sibling) stack.push(c);
1047
1128
  }
@@ -1053,10 +1134,10 @@ function onCommit(root, commitPriority) {
1053
1134
  let commitCause;
1054
1135
  let afterCommit;
1055
1136
  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))) {
1137
+ if (!inputDriven && lastCommit && at - lastCommit.at < EFFECT_LOOP_WINDOW_MS && updaters.some((u) => lastCommit.rendered.has(u)) && !rendered.some(isDeferredCatchUp)) {
1057
1138
  commitCause = "effect-after-commit";
1058
1139
  afterCommit = lastCommit.id;
1059
- } else if (suspenseResolved) commitCause = "suspense-resolved";
1140
+ } else if (resolvedBoundaries) commitCause = "suspense-resolved";
1060
1141
  if (rendered.length === 0) {
1061
1142
  lastCommit = { id: s.nextCommitId, at, rendered: renderedNames };
1062
1143
  return;
@@ -1108,6 +1189,9 @@ function onCommit(root, commitPriority) {
1108
1189
  owner: ownerName(fiber),
1109
1190
  path: componentPath(fiber),
1110
1191
  memoized: isMemoizedFiber(fiber),
1192
+ compiled: isCompiledFiber(fiber),
1193
+ classComponent: fiber.tag === ClassComponent,
1194
+ revealed: resolvedBoundaries !== null && a.trigger === "parent" && isUnder(fiber, resolvedBoundaries),
1111
1195
  selfDuration: durations ? durations.self : void 0,
1112
1196
  treeDuration: durations ? durations.tree : void 0,
1113
1197
  commitId,
@@ -1290,7 +1374,7 @@ function flash(target, duration = 500) {
1290
1374
  }
1291
1375
 
1292
1376
  // src/version.ts
1293
- var VERSION = "0.4.0" ;
1377
+ var VERSION = "0.5.0" ;
1294
1378
 
1295
1379
  // src/devtools.ts
1296
1380
  var DEVTOOLS_MARKER = "__rerenderLens";
@@ -1623,15 +1707,16 @@ function createDevtoolsNotifier(options = {}) {
1623
1707
  }
1624
1708
 
1625
1709
  // src/setup.ts
1626
- var env = (key) => {
1710
+ var read = (get) => {
1627
1711
  try {
1628
- return typeof process !== "undefined" && process && process.env ? process.env[key] : void 0;
1712
+ return get();
1629
1713
  } catch {
1630
1714
  return void 0;
1631
1715
  }
1632
1716
  };
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");
1717
+ var production = read(() => process.env.NODE_ENV) === "production";
1718
+ if (!production && typeof window !== "undefined" && !window.__RERENDER_LENS_DEVTOOLS__) {
1719
+ const relay = read(() => process.env.RERENDER_LENS_RELAY) || read(() => process.env.NEXT_PUBLIC_RERENDER_LENS_RELAY);
1635
1720
  init({ trackAllMemoized: true, notifier: createDevtoolsNotifier(relay ? { relay } : {}) });
1636
1721
  }
1637
1722
  //# sourceMappingURL=setup.cjs.map