rerender-lens 0.3.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 +118 -0
  2. package/README.md +192 -353
  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-BHGUUo-p.d.ts → devtools-BYWat7nQ.d.ts} +17 -3
  10. package/dist/{devtools-DmhWiWcN.d.cts → devtools-DJi_6AfJ.d.cts} +17 -3
  11. package/dist/index.cjs +635 -109
  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 +620 -110
  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 +235 -30
  32. package/dist/playwright.cjs.map +1 -1
  33. package/dist/playwright.d.cts +8 -6
  34. package/dist/playwright.d.ts +8 -6
  35. package/dist/playwright.js +235 -31
  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 +1100 -337
  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 +308 -95
  45. package/dist/setup.cjs.map +1 -1
  46. package/dist/setup.js +308 -95
  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 +452 -87
  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 +452 -87
  57. package/dist/vitest-setup.js.map +1 -1
  58. package/dist/vitest.cjs +515 -123
  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 +516 -124
  63. package/dist/vitest.js.map +1 -1
  64. package/package.json +41 -5
  65. package/panel/panel.css +1 -34
  66. package/panel/panel.js +865 -713
package/dist/setup.js CHANGED
@@ -24,10 +24,49 @@ function remember(seen, a, b) {
24
24
  set.add(b);
25
25
  return false;
26
26
  }
27
+ var scope = null;
28
+ var DEFAULT_DIFF_BUDGET = 5e4;
29
+ function beginDiffScope(budget = 2e5) {
30
+ const previous = scope;
31
+ scope = { cache: /* @__PURE__ */ new WeakMap(), remaining: budget, exhausted: false };
32
+ return () => {
33
+ scope = previous;
34
+ };
35
+ }
36
+ function diffBudgetExhausted() {
37
+ return scope?.exhausted === true;
38
+ }
27
39
  function deepEqual(a, b, seen = /* @__PURE__ */ new Map()) {
28
40
  if (Object.is(a, b)) return true;
29
41
  if (typeof a !== typeof b) return false;
30
42
  if (typeof a !== "object" || a === null || b === null) return false;
43
+ if (!scope) {
44
+ const end = beginDiffScope(DEFAULT_DIFF_BUDGET);
45
+ try {
46
+ return deepEqual(a, b, seen);
47
+ } finally {
48
+ end();
49
+ }
50
+ }
51
+ const sc = scope;
52
+ const objA = a;
53
+ const objB = b;
54
+ const hit = sc.cache.get(objA)?.get(objB);
55
+ if (hit !== void 0) return hit;
56
+ if (--sc.remaining < 0) {
57
+ sc.exhausted = true;
58
+ return false;
59
+ }
60
+ const result = deepEqualObjects(objA, objB, seen);
61
+ let m = sc.cache.get(objA);
62
+ if (!m) {
63
+ m = /* @__PURE__ */ new WeakMap();
64
+ sc.cache.set(objA, m);
65
+ }
66
+ m.set(objB, result);
67
+ return result;
68
+ }
69
+ function deepEqualObjects(a, b, seen) {
31
70
  const objA = a;
32
71
  const objB = b;
33
72
  if (remember(seen, objA, objB)) return true;
@@ -56,6 +95,10 @@ function deepEqual(a, b, seen = /* @__PURE__ */ new Map()) {
56
95
  }
57
96
  if (a instanceof Set) {
58
97
  if (!(b instanceof Set) || a.size !== b.size) return false;
98
+ if (a.size > 50) {
99
+ for (const v of a) if (!b.has(v)) return false;
100
+ return true;
101
+ }
59
102
  outer: for (const v of a) {
60
103
  if (b.has(v)) continue;
61
104
  for (const w of b) if (deepEqual(v, w, seen)) continue outer;
@@ -71,9 +114,10 @@ function deepEqual(a, b, seen = /* @__PURE__ */ new Map()) {
71
114
  const ka = Object.keys(a);
72
115
  const kb = Object.keys(b);
73
116
  if (ka.length !== kb.length) return false;
117
+ const rb = b;
74
118
  for (const k of ka) {
75
119
  if (!Object.prototype.hasOwnProperty.call(b, k)) return false;
76
- if (!deepEqual(a[k], b[k], seen)) return false;
120
+ if (!deepEqual(a[k], rb[k], seen)) return false;
77
121
  }
78
122
  return true;
79
123
  }
@@ -115,30 +159,30 @@ function diffRecords(prev, next, basePath = "") {
115
159
  if (Object.is(a, b)) continue;
116
160
  const kind = classify(a, b);
117
161
  if (kind === "different") {
118
- changes.push({ path: firstDifferentPath(a, b, path), kind, prev: a, next: b });
162
+ changes.push({ path: diffBudgetExhausted() ? path : firstDifferentPath(a, b, path) ?? path, kind, prev: a, next: b });
119
163
  } else {
120
164
  changes.push({ path, kind, prev: a, next: b });
121
165
  }
122
166
  }
123
167
  return changes;
124
168
  }
125
- function firstDifferentPath(a, b, path, depth = 0) {
126
- if (depth > 8) return path;
169
+ function firstDifferentPath(a, b, path = "", depth = 0) {
170
+ if (depth > 8) return path || "(value)";
127
171
  if (Array.isArray(a) && Array.isArray(b)) {
128
- if (a.length !== b.length) return `${path}.length`;
172
+ if (a.length !== b.length) return path ? `${path}.length` : "length";
129
173
  for (let i = 0; i < a.length; i++) {
130
174
  if (!deepEqual(a[i], b[i])) return firstDifferentPath(a[i], b[i], joinPath(path, i), depth + 1);
131
175
  }
132
- return path;
176
+ return null;
133
177
  }
134
178
  if (isPlainObject(a) && isPlainObject(b)) {
135
179
  const keys = /* @__PURE__ */ new Set([...Object.keys(a), ...Object.keys(b)]);
136
180
  for (const k of keys) {
137
181
  if (!deepEqual(a[k], b[k])) return firstDifferentPath(a[k], b[k], joinPath(path, k), depth + 1);
138
182
  }
139
- return path;
183
+ return null;
140
184
  }
141
- return path;
185
+ return deepEqual(a, b) ? null : path || "(value)";
142
186
  }
143
187
 
144
188
  // src/report.ts
@@ -156,10 +200,12 @@ function storeAdvice(custom) {
156
200
  }
157
201
  var isGenuine = (c) => c.kind === "different" || c.kind === "added" || c.kind === "removed";
158
202
  var isChildren = (change) => change.path === "children";
203
+ var isRefObject = (change) => change.path === "ref" && change.kind === "deep-equal";
159
204
  function fixFor(change) {
160
205
  if (isChildren(change) && (change.kind === "element" || change.kind === "deep-equal")) {
161
206
  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`;
162
207
  }
208
+ if (isRefObject(change)) return `create the ref once with useRef (or createRef outside the render) and pass the same object every time`;
163
209
  switch (change.kind) {
164
210
  case "function":
165
211
  return `wrap it in useCallback (or hoist it out of the parent's render)`;
@@ -175,6 +221,7 @@ function describe(change) {
175
221
  if (isChildren(change) && (change.kind === "element" || change.kind === "deep-equal")) {
176
222
  return `children are new React elements with the same types and props on every render of the parent`;
177
223
  }
224
+ if (isRefObject(change)) return `prop "ref" is a new ref object on every render (React re-attaches it each time)`;
178
225
  switch (change.kind) {
179
226
  case "deep-equal":
180
227
  return `prop "${change.path}" is a new reference but deep-equal to the previous value`;
@@ -202,11 +249,21 @@ function buildReport(input) {
202
249
  if (causes.length === 0) trigger = "parent";
203
250
  else if (causes.length === 1) trigger = causes[0];
204
251
  else trigger = "mixed";
205
- const avoidable = trigger === "parent";
252
+ const nothingChanged = input.propChanges.length === 0 && stateChanges.length === 0 && hookChanges.length === 0;
253
+ const cacheHit = input.compiled === true && trigger === "parent" && input.propChanges.length === 0;
254
+ const revealed = input.revealed === true && trigger === "parent";
255
+ const avoidable = trigger === "parent" && !cacheHit && !revealed;
206
256
  const reasons = [];
207
- if (avoidable && input.propChanges.length === 0 && stateChanges.length === 0 && hookChanges.length === 0) {
257
+ const memoFix = input.classComponent ? `Make "${input.component}" extend PureComponent (or implement shouldComponentUpdate)` : `Wrap "${input.component}" in React.memo (or extend PureComponent)`;
258
+ if (cacheHit) {
259
+ reasons.push(`compiled by React Compiler: its output is memoized and the inputs did not change, so this render is cheap; nothing to fix.`);
260
+ } else if (revealed) {
261
+ reasons.push(
262
+ `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.`
263
+ );
264
+ } else if (avoidable && nothingChanged) {
208
265
  const who = input.parent ? `<${input.parent.name}> re-rendered (${describeTrigger(input.parent.trigger)})` : "its parent re-rendered";
209
- reasons.push(`re-rendered with identical props because ${who}. Wrap "${input.component}" in React.memo (or extend PureComponent).`);
266
+ reasons.push(`re-rendered with identical props because ${who}. ${memoFix}.`);
210
267
  } else if (avoidable && input.parent) {
211
268
  reasons.push(`caused by <${input.parent.name}> re-rendering (${describeTrigger(input.parent.trigger)}).`);
212
269
  }
@@ -215,9 +272,9 @@ function buildReport(input) {
215
272
  reasons.push(fix ? `${describe(c)}: ${fix}.` : `${describe(c)}.`);
216
273
  }
217
274
  const memoized = input.memoized !== false;
218
- if (avoidable && !memoized && input.propChanges.length > 0) {
275
+ if (avoidable && !memoized && !input.compiled && input.propChanges.length > 0) {
219
276
  reasons.push(
220
- `"${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).`
277
+ `"${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)"}.`
221
278
  );
222
279
  }
223
280
  for (const c of stateChanges) {
@@ -229,6 +286,8 @@ function buildReport(input) {
229
286
  const where = c.provider && c.provider.component ? ` (provided by <${c.provider.component}>)` : "";
230
287
  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` : "";
231
288
  reasons.push(`${c.path} changed${where}${keys}.`);
289
+ } else if (c.hook === "useDeferredValue" && isGenuine(c)) {
290
+ 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.`);
232
291
  } else if (isGenuine(c)) reasons.push(`${hookLabel(c)} changed.`);
233
292
  else if (isStateHook(c))
234
293
  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.`);
@@ -259,6 +318,7 @@ function buildReport(input) {
259
318
  reasons,
260
319
  time: now()
261
320
  };
321
+ if (input.compiled) report.compiled = true;
262
322
  if (input.selfDuration !== void 0) report.selfDuration = input.selfDuration;
263
323
  if (input.treeDuration !== void 0) report.treeDuration = input.treeDuration;
264
324
  if (input.commitPriority) report.commitPriority = input.commitPriority;
@@ -296,10 +356,10 @@ var KIND_LABEL = {
296
356
  };
297
357
  function summarize(report) {
298
358
  const counts2 = /* @__PURE__ */ new Map();
299
- for (const c of [...report.propChanges, ...report.stateChanges, ...report.hookChanges]) {
359
+ for (const c of [...report.propChanges || [], ...report.stateChanges || [], ...report.hookChanges || []]) {
300
360
  counts2.set(c.kind, (counts2.get(c.kind) ?? 0) + 1);
301
361
  }
302
- const parts = [...counts2].map(([kind, n]) => `${n} ${KIND_LABEL[kind]}`);
362
+ const parts = [...counts2].map(([kind, n]) => `${n} ${KIND_LABEL[kind] || kind}`);
303
363
  if (parts.length === 0) parts.push("no changes");
304
364
  return parts.join(", ");
305
365
  }
@@ -326,7 +386,7 @@ function getState() {
326
386
  const g = globalThis;
327
387
  let s = g[KEY];
328
388
  if (!s) {
329
- 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 };
389
+ 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 };
330
390
  g[KEY] = s;
331
391
  }
332
392
  return s;
@@ -356,12 +416,33 @@ function dispatch(report, override) {
356
416
  try {
357
417
  options.notifier(report);
358
418
  } catch (err) {
359
- (options.console ?? console).warn("[rerender-lens] notifier threw", err);
419
+ warnOnce("notifier", `notifier threw: ${String(err)}`);
360
420
  }
361
421
  }
362
422
  }
363
423
 
364
424
  // src/hookNames.ts
425
+ var NODE_COUNT = {
426
+ useState: 1,
427
+ useReducer: 1,
428
+ useRef: 1,
429
+ useMemo: 1,
430
+ useCallback: 1,
431
+ useEffect: 1,
432
+ useLayoutEffect: 1,
433
+ useInsertionEffect: 1,
434
+ useImperativeHandle: 1,
435
+ useSyncExternalStore: 2,
436
+ useTransition: 2,
437
+ useDeferredValue: 1,
438
+ useId: 1,
439
+ useOptimistic: 1,
440
+ useActionState: 3,
441
+ useFormState: 3,
442
+ useEffectEvent: 1,
443
+ useCacheRefresh: 1
444
+ };
445
+ var BUILTIN_HOOKS = /* @__PURE__ */ new Set([...Object.keys(NODE_COUNT), "useContext", "useDebugValue", "use"]);
365
446
  var noop = () => {
366
447
  };
367
448
  function customHooksFromStack(stack, componentName, marker) {
@@ -376,7 +457,7 @@ function customHooksFromStack(stack, componentName, marker) {
376
457
  continue;
377
458
  }
378
459
  if (name === componentName || name.endsWith("." + componentName)) break;
379
- if (/^use[A-Z0-9_]/.test(name)) names.push(name);
460
+ if (/^use[A-Z0-9_]/.test(name) && !BUILTIN_HOOKS.has(name)) names.push(name);
380
461
  if (names.length > 8) break;
381
462
  }
382
463
  return names;
@@ -676,6 +757,11 @@ function isMemoizedFiber(fiber) {
676
757
  }
677
758
  return false;
678
759
  }
760
+ function isCompiledFiber(fiber) {
761
+ if (fiber.tag === ClassComponent) return false;
762
+ const cache2 = fiber.updateQueue?.memoCache;
763
+ return !!cache2 && Array.isArray(cache2.data);
764
+ }
679
765
  function durationsOf(fiber) {
680
766
  if (typeof fiber.actualDuration !== "number") return null;
681
767
  const tree = fiber.actualDuration;
@@ -699,11 +785,14 @@ var counts = /* @__PURE__ */ new WeakMap();
699
785
  var ids = /* @__PURE__ */ new WeakMap();
700
786
  var fibersById = /* @__PURE__ */ new Map();
701
787
  var hasWeakRef = typeof WeakRef === "function";
788
+ var pruneAt = 5e3;
702
789
  function remember2(id, fiber) {
703
790
  if (!hasWeakRef) return;
791
+ if (fibersById.get(id)?.deref() === fiber) return;
704
792
  fibersById.set(id, new WeakRef(fiber));
705
- if (fibersById.size > 5e3) {
793
+ if (fibersById.size > pruneAt) {
706
794
  for (const [k, ref] of fibersById) if (!ref.deref()) fibersById.delete(k);
795
+ pruneAt = Math.max(5e3, fibersById.size * 2);
707
796
  }
708
797
  }
709
798
  function instanceIdOf(fiber) {
@@ -758,7 +847,7 @@ function nearestComponent(fiber) {
758
847
  }
759
848
  var INTERNAL_FRAME = /react-dom|react_jsx|jsx-(dev-)?runtime|\/react\/|node_modules\/react|react-stack-bottom-frame|react_stack_bottom_frame|scheduler/;
760
849
  function parseStackLocation(stack) {
761
- for (const line of stack.split("\n")) {
850
+ for (const line of stack.split("\n", 24)) {
762
851
  const m = /(?:at\s+(?:.*?\s+)?\(?|@)?((?:https?|file|webpack|vite|blob):[^\s()]+?):(\d+):(\d+)\)?\s*$/.exec(line.trim());
763
852
  if (!m || !m[1] || INTERNAL_FRAME.test(m[1])) continue;
764
853
  return { fileName: m[1], lineNumber: Number(m[2]), columnNumber: Number(m[3]) };
@@ -774,9 +863,16 @@ function sourceOf(fiber) {
774
863
  return out;
775
864
  }
776
865
  const st = fiber._debugStack;
777
- const text = typeof st === "string" ? st : st && typeof st.stack === "string" ? st.stack : null;
778
- return text ? parseStackLocation(text) : void 0;
866
+ if (st && typeof st === "object") {
867
+ if (sourceCache.has(st)) return sourceCache.get(st);
868
+ const text = typeof st.stack === "string" ? st.stack : null;
869
+ const out = text ? parseStackLocation(text) : void 0;
870
+ sourceCache.set(st, out);
871
+ return out;
872
+ }
873
+ return typeof st === "string" ? parseStackLocation(st) : void 0;
779
874
  }
875
+ var sourceCache = /* @__PURE__ */ new WeakMap();
780
876
  function bumpCount(fiber) {
781
877
  const prev = counts.get(fiber) ?? (fiber.alternate ? counts.get(fiber.alternate) : void 0) ?? 0;
782
878
  const n = prev + 1;
@@ -844,7 +940,8 @@ function diffHooks(fiber, alt) {
844
940
  const labels = hookLabelsFor(fiber, b);
845
941
  let i = 0;
846
942
  while (a && b) {
847
- if ((isStateNode(b) || isStoreNode(b)) && !Object.is(a.memoizedState, b.memoizedState)) {
943
+ const deferred = labels?.[i] === "useDeferredValue";
944
+ if ((deferred || isStateNode(b) || isStoreNode(b)) && !Object.is(a.memoizedState, b.memoizedState)) {
848
945
  const hook = labels?.[i] ?? hookLabel2(b);
849
946
  out.push({
850
947
  path: `${hook}#${i}`,
@@ -878,6 +975,10 @@ function diffContexts(fiber, alt) {
878
975
  let a = alt.dependencies?.firstContext ?? null;
879
976
  let b = fiber.dependencies?.firstContext ?? null;
880
977
  let i = 0;
978
+ if (b && !("memoizedValue" in b)) {
979
+ warnOnce("contextValues", "this React does not record context values on the fiber (React < 18): context changes are reported as plain parent re-renders.");
980
+ return out;
981
+ }
881
982
  while (a && b) {
882
983
  if (a.context === b.context && !Object.is(a.memoizedValue, b.memoizedValue)) {
883
984
  const name = b.context.displayName ?? "Context";
@@ -910,10 +1011,26 @@ function diffContexts(fiber, alt) {
910
1011
  return out;
911
1012
  }
912
1013
  var isGenuine2 = (c) => c.kind === "different" || c.kind === "added" || c.kind === "removed";
1014
+ var isRefObject2 = (v) => {
1015
+ if (typeof v !== "object" || v === null) return false;
1016
+ const keys = Object.keys(v);
1017
+ return keys.length === 1 && keys[0] === "current";
1018
+ };
1019
+ function diffProps(prev, next) {
1020
+ if (prev === next) return [];
1021
+ const pr = prev.ref;
1022
+ const nr = next.ref;
1023
+ if (pr !== nr && isRefObject2(pr) && isRefObject2(nr)) {
1024
+ const { ref: _p, ...restPrev } = prev;
1025
+ const { ref: _n, ...restNext } = next;
1026
+ return [{ path: "ref", kind: "deep-equal", prev: pr, next: nr }, ...diffRecords(restPrev, restNext)];
1027
+ }
1028
+ return diffRecords(prev, next);
1029
+ }
913
1030
  function analyze(fiber, alt, trackHooks) {
914
1031
  const prevProps = alt.memoizedProps ?? {};
915
1032
  const nextProps = fiber.memoizedProps ?? {};
916
- const propChanges = prevProps === nextProps ? [] : diffRecords(prevProps, nextProps);
1033
+ const propChanges = diffProps(prevProps, nextProps);
917
1034
  let stateChanges = [];
918
1035
  let hookChanges = [];
919
1036
  if (fiber.tag === ClassComponent) {
@@ -926,7 +1043,7 @@ function analyze(fiber, alt, trackHooks) {
926
1043
  } else if (trackHooks) {
927
1044
  hookChanges = [...diffHooks(fiber, alt), ...diffContexts(fiber, alt)];
928
1045
  }
929
- const isStateHook = (h) => h.hook !== "useContext" && h.hook !== "useSyncExternalStore";
1046
+ const isStateHook = (h) => h.hook !== "useContext" && h.hook !== "useSyncExternalStore" && h.hook !== "useDeferredValue";
930
1047
  const causes = [];
931
1048
  if (propChanges.some(isGenuine2)) causes.push("props");
932
1049
  if (stateChanges.some(isGenuine2) || hookChanges.some((h) => isStateHook(h) && isGenuine2(h))) causes.push("state");
@@ -964,6 +1081,26 @@ function updatersOf(root) {
964
1081
  }
965
1082
  var lastCommit = null;
966
1083
  var EFFECT_LOOP_WINDOW_MS = 50;
1084
+ var MAX_REPORTS_PER_COMMIT = 200;
1085
+ var COMMIT_TIME_BUDGET_MS = 25;
1086
+ function isDeferredCatchUp(fiber) {
1087
+ const types = fiber._debugHookTypes;
1088
+ if (!types || !types.includes("useDeferredValue") || !fiber.alternate) return false;
1089
+ const first = fiber.memoizedState;
1090
+ if (!isHookList(first)) return false;
1091
+ const labels = hookLabelsFor(fiber, first);
1092
+ if (!labels) return false;
1093
+ let a = fiber.alternate.memoizedState;
1094
+ let b = first;
1095
+ for (let i = 0; a && b; a = a.next, b = b.next, i++) {
1096
+ if (labels[i] === "useDeferredValue" && !Object.is(a.memoizedState, b.memoizedState)) return true;
1097
+ }
1098
+ return false;
1099
+ }
1100
+ function isUnder(fiber, boundaries) {
1101
+ for (let f = fiber.return; f && f.tag !== HostRoot; f = f.return) if (f.tag === SuspenseComponent && boundaries.includes(f)) return true;
1102
+ return false;
1103
+ }
967
1104
  function onCommit(root, commitPriority) {
968
1105
  const s = getState();
969
1106
  if (!s.enabled) return;
@@ -973,7 +1110,7 @@ function onCommit(root, commitPriority) {
973
1110
  const rendered = [];
974
1111
  const renderedNames = /* @__PURE__ */ new Set();
975
1112
  let hot = false;
976
- let suspenseResolved = false;
1113
+ let resolvedBoundaries = null;
977
1114
  const stack = [root.current];
978
1115
  while (stack.length) {
979
1116
  const fiber = stack.pop();
@@ -983,7 +1120,7 @@ function onCommit(root, commitPriority) {
983
1120
  renderedNames.add(fiberName(fiber));
984
1121
  if (shouldTrack(fiberType(fiber), o)) rendered.push(fiber);
985
1122
  }
986
- if (fiber.tag === SuspenseComponent && alt && alt.memoizedState !== null && fiber.memoizedState === null) suspenseResolved = true;
1123
+ if (fiber.tag === SuspenseComponent && alt && alt.memoizedState !== null && fiber.memoizedState === null) (resolvedBoundaries ?? (resolvedBoundaries = [])).push(fiber);
987
1124
  if (!alt || fiber.child !== alt.child) {
988
1125
  for (let c = fiber.child; c; c = c.sibling) stack.push(c);
989
1126
  }
@@ -994,10 +1131,11 @@ function onCommit(root, commitPriority) {
994
1131
  const updaters = updatersOf(root);
995
1132
  let commitCause;
996
1133
  let afterCommit;
997
- if (lastCommit && at - lastCommit.at < EFFECT_LOOP_WINDOW_MS && updaters.some((u) => lastCommit.rendered.has(u))) {
1134
+ const inputDriven = commitPriority === "immediate" || commitPriority === "user-blocking";
1135
+ if (!inputDriven && lastCommit && at - lastCommit.at < EFFECT_LOOP_WINDOW_MS && updaters.some((u) => lastCommit.rendered.has(u)) && !rendered.some(isDeferredCatchUp)) {
998
1136
  commitCause = "effect-after-commit";
999
1137
  afterCommit = lastCommit.id;
1000
- } else if (suspenseResolved) commitCause = "suspense-resolved";
1138
+ } else if (resolvedBoundaries) commitCause = "suspense-resolved";
1001
1139
  if (rendered.length === 0) {
1002
1140
  lastCommit = { id: s.nextCommitId, at, rendered: renderedNames };
1003
1141
  return;
@@ -1006,47 +1144,62 @@ function onCommit(root, commitPriority) {
1006
1144
  lastCommit = { id: commitId, at, rendered: renderedNames };
1007
1145
  const dispatcherRef = o.resolveHookNames ? getDispatcherRef() : null;
1008
1146
  const parentCache = /* @__PURE__ */ new Map();
1009
- for (const fiber of rendered) {
1010
- const alt = fiber.alternate;
1011
- const a = analyze(fiber, alt, trackHooks);
1012
- const durations = durationsOf(fiber);
1013
- let hookState = o.includeState !== false && trackHooks ? snapshotHooks(fiber) : void 0;
1014
- if (dispatcherRef && fiber.tag !== ClassComponent && (a.hookChanges.length || hookState && hookState.length)) {
1015
- const names = resolveHookNames(fiber, dispatcherRef);
1016
- if (names.size) {
1017
- for (const c of a.hookChanges) if (c.hook !== "useContext" && names.has(c.index)) c.custom = names.get(c.index);
1018
- if (hookState) hookState = hookState.map((h) => names.has(h.index) ? { ...h, custom: names.get(h.index) } : h);
1147
+ const endDiffScope = beginDiffScope();
1148
+ const deadline = at + COMMIT_TIME_BUDGET_MS;
1149
+ try {
1150
+ for (let i = 0; i < rendered.length; i++) {
1151
+ if (i >= MAX_REPORTS_PER_COMMIT || (i & 15) === 15 && nowMs() > deadline) {
1152
+ s.truncated += rendered.length - i;
1153
+ warnOnce("truncated", `a commit rendered ${rendered.length} tracked components; only ${i} were reported (see info().truncated). Narrow \`include\` or turn off includeState.`);
1154
+ break;
1155
+ }
1156
+ const fiber = rendered[i];
1157
+ const alt = fiber.alternate;
1158
+ const a = analyze(fiber, alt, trackHooks);
1159
+ const durations = durationsOf(fiber);
1160
+ let hookState = o.includeState !== false && trackHooks ? snapshotHooks(fiber) : void 0;
1161
+ if (dispatcherRef && fiber.tag !== ClassComponent && (a.hookChanges.length || hookState && hookState.length)) {
1162
+ const names = resolveHookNames(fiber, dispatcherRef);
1163
+ if (names.size) {
1164
+ for (const c of a.hookChanges) if (c.hook !== "useContext" && names.has(c.index)) c.custom = names.get(c.index);
1165
+ if (hookState) hookState = hookState.map((h) => names.has(h.index) ? { ...h, custom: names.get(h.index) } : h);
1166
+ }
1019
1167
  }
1168
+ const report = buildReport({
1169
+ component: fiberName(fiber),
1170
+ instanceId: instanceId(fiber),
1171
+ renderCount: bumpCount(fiber),
1172
+ prevProps: alt.memoizedProps ?? {},
1173
+ nextProps: fiber.memoizedProps ?? {},
1174
+ propChanges: a.propChanges,
1175
+ stateChanges: a.stateChanges,
1176
+ hookChanges: a.hookChanges,
1177
+ ...o.includeState !== false ? {
1178
+ hookState,
1179
+ contexts: trackHooks ? snapshotContexts(fiber) : void 0,
1180
+ state: fiber.tag === ClassComponent && fiber.memoizedState && typeof fiber.memoizedState === "object" ? fiber.memoizedState : void 0
1181
+ } : {},
1182
+ updaters,
1183
+ commitCause,
1184
+ afterCommit,
1185
+ key: fiber.key === null || fiber.key === void 0 ? null : String(fiber.key),
1186
+ parent: a.trigger === "parent" ? nearestRenderedAncestor(fiber, parentCache, trackHooks) : null,
1187
+ owner: ownerName(fiber),
1188
+ path: componentPath(fiber),
1189
+ memoized: isMemoizedFiber(fiber),
1190
+ compiled: isCompiledFiber(fiber),
1191
+ classComponent: fiber.tag === ClassComponent,
1192
+ revealed: resolvedBoundaries !== null && a.trigger === "parent" && isUnder(fiber, resolvedBoundaries),
1193
+ selfDuration: durations ? durations.self : void 0,
1194
+ treeDuration: durations ? durations.tree : void 0,
1195
+ commitId,
1196
+ commitPriority,
1197
+ source: sourceOf(fiber)
1198
+ });
1199
+ dispatch(report);
1020
1200
  }
1021
- const report = buildReport({
1022
- component: fiberName(fiber),
1023
- instanceId: instanceId(fiber),
1024
- renderCount: bumpCount(fiber),
1025
- prevProps: alt.memoizedProps ?? {},
1026
- nextProps: fiber.memoizedProps ?? {},
1027
- propChanges: a.propChanges,
1028
- stateChanges: a.stateChanges,
1029
- hookChanges: a.hookChanges,
1030
- ...o.includeState !== false ? {
1031
- hookState,
1032
- contexts: trackHooks ? snapshotContexts(fiber) : void 0,
1033
- state: fiber.tag === ClassComponent && fiber.memoizedState && typeof fiber.memoizedState === "object" ? fiber.memoizedState : void 0
1034
- } : {},
1035
- updaters,
1036
- commitCause,
1037
- afterCommit,
1038
- key: fiber.key === null || fiber.key === void 0 ? null : String(fiber.key),
1039
- parent: a.trigger === "parent" ? nearestRenderedAncestor(fiber, parentCache, trackHooks) : null,
1040
- owner: ownerName(fiber),
1041
- path: componentPath(fiber),
1042
- memoized: isMemoizedFiber(fiber),
1043
- selfDuration: durations ? durations.self : void 0,
1044
- treeDuration: durations ? durations.tree : void 0,
1045
- commitId,
1046
- commitPriority,
1047
- source: sourceOf(fiber)
1048
- });
1049
- dispatch(report);
1201
+ } finally {
1202
+ endDiffScope();
1050
1203
  }
1051
1204
  }
1052
1205
 
@@ -1175,13 +1328,24 @@ function highlight(target) {
1175
1328
  for (const s of l.sticky) s.remove();
1176
1329
  l.sticky = [];
1177
1330
  if (!target) return;
1178
- for (const node of target.nodes) {
1179
- const rect = node.getBoundingClientRect();
1180
- if (rect.width === 0 && rect.height === 0) continue;
1331
+ const frag = document.createDocumentFragment();
1332
+ for (const rect of measure(target.nodes)) {
1181
1333
  const b = box(rect, "#1a73e8", "rgba(26, 115, 232, 0.12)", target.label);
1182
- l.root.appendChild(b);
1334
+ frag.appendChild(b);
1183
1335
  l.sticky.push(b);
1184
1336
  }
1337
+ l.root.appendChild(frag);
1338
+ }
1339
+ var MAX_BOXES = 100;
1340
+ function measure(nodes) {
1341
+ const rects = [];
1342
+ for (const node of nodes) {
1343
+ if (rects.length >= MAX_BOXES) break;
1344
+ const rect = node.getBoundingClientRect();
1345
+ if (rect.width === 0 && rect.height === 0) continue;
1346
+ rects.push(rect);
1347
+ }
1348
+ return rects;
1185
1349
  }
1186
1350
  function clearHighlight() {
1187
1351
  highlight(null);
@@ -1189,27 +1353,34 @@ function clearHighlight() {
1189
1353
  function flash(target, duration = 500) {
1190
1354
  const l = ensureLayer();
1191
1355
  if (!l) return;
1192
- for (const node of target.nodes) {
1193
- const rect = node.getBoundingClientRect();
1194
- if (rect.width === 0 && rect.height === 0) continue;
1356
+ const frag = document.createDocumentFragment();
1357
+ const boxes = [];
1358
+ for (const rect of measure(target.nodes)) {
1195
1359
  const b = box(rect, "#d93025", "rgba(217, 48, 37, 0.15)");
1196
1360
  b.style.transition = `opacity ${duration}ms ease-out`;
1197
- l.root.appendChild(b);
1198
- requestAnimationFrame(() => {
1199
- b.style.opacity = "0";
1200
- });
1201
- setTimeout(() => b.remove(), duration + 50);
1361
+ frag.appendChild(b);
1362
+ boxes.push(b);
1202
1363
  }
1364
+ if (!boxes.length) return;
1365
+ l.root.appendChild(frag);
1366
+ requestAnimationFrame(() => {
1367
+ for (const b of boxes) b.style.opacity = "0";
1368
+ });
1369
+ setTimeout(() => {
1370
+ for (const b of boxes) b.remove();
1371
+ }, duration + 50);
1203
1372
  }
1204
1373
 
1205
1374
  // src/version.ts
1206
- var VERSION = "0.3.0" ;
1375
+ var VERSION = "0.5.0" ;
1207
1376
 
1208
1377
  // src/devtools.ts
1209
1378
  var DEVTOOLS_MARKER = "__rerenderLens";
1210
1379
  var PROTOCOL_VERSION = 2;
1211
1380
  var DEFAULT_CHANNEL = "rerender-lens";
1212
- function serialize(value, maxDepth = 6, seen = /* @__PURE__ */ new WeakSet(), depth = 0) {
1381
+ var SERIALIZE_MAX_ENTRIES = 100;
1382
+ var SERIALIZE_MAX_NODES = 2e4;
1383
+ function serialize(value, maxDepth = 4, seen = /* @__PURE__ */ new WeakSet(), depth = 0, budget = { nodes: SERIALIZE_MAX_NODES }) {
1213
1384
  if (value === null || value === void 0) return value;
1214
1385
  const t = typeof value;
1215
1386
  if (t === "string" || t === "boolean") return value;
@@ -1220,24 +1391,56 @@ function serialize(value, maxDepth = 6, seen = /* @__PURE__ */ new WeakSet(), de
1220
1391
  const obj = value;
1221
1392
  if (seen.has(obj)) return "[Circular]";
1222
1393
  if (depth >= maxDepth) return "[\u2026]";
1394
+ if (--budget.nodes < 0) return "[\u2026]";
1395
+ const next = (v) => serialize(v, maxDepth, seen, depth + 1, budget);
1223
1396
  seen.add(obj);
1224
1397
  try {
1225
1398
  if (isReactElement(obj)) {
1226
1399
  const out2 = { $type: "element", name: getDisplayName(obj.type) };
1227
1400
  if (obj.key !== null && obj.key !== void 0) out2.key = String(obj.key);
1228
- out2.props = serialize(obj.props, maxDepth, seen, depth + 1);
1401
+ out2.props = next(obj.props);
1229
1402
  return out2;
1230
1403
  }
1231
1404
  if (obj instanceof Date) return { $type: "Date", value: obj.toISOString() };
1232
1405
  if (obj instanceof RegExp) return { $type: "RegExp", value: String(obj) };
1406
+ if (ArrayBuffer.isView(obj) || obj instanceof ArrayBuffer) {
1407
+ const bin = obj;
1408
+ return { $type: bin.constructor?.name || "ArrayBuffer", length: typeof bin.length === "number" ? bin.length : bin.byteLength };
1409
+ }
1410
+ if (obj instanceof Promise) return "[Promise]";
1411
+ if (typeof Node !== "undefined" && obj instanceof Node) return typeof Element !== "undefined" && obj instanceof Element ? `<${obj.tagName.toLowerCase()}>` : `[${obj.nodeName}]`;
1412
+ if (obj === globalThis) return "[Window]";
1233
1413
  if (obj instanceof Map) {
1234
- return { $type: "Map", entries: [...obj].map(([k, v]) => [serialize(k, maxDepth, seen, depth + 1), serialize(v, maxDepth, seen, depth + 1)]) };
1414
+ const entries = [];
1415
+ for (const [k, v] of obj) {
1416
+ if (entries.length >= SERIALIZE_MAX_ENTRIES) {
1417
+ entries.push(["\u2026", `+${obj.size - SERIALIZE_MAX_ENTRIES} more`]);
1418
+ break;
1419
+ }
1420
+ entries.push([next(k), next(v)]);
1421
+ }
1422
+ return { $type: "Map", entries };
1423
+ }
1424
+ if (obj instanceof Set) {
1425
+ const values = [];
1426
+ for (const v of obj) {
1427
+ if (values.length >= SERIALIZE_MAX_ENTRIES) {
1428
+ values.push(`\u2026+${obj.size - SERIALIZE_MAX_ENTRIES} more`);
1429
+ break;
1430
+ }
1431
+ values.push(next(v));
1432
+ }
1433
+ return { $type: "Set", values };
1434
+ }
1435
+ if (Array.isArray(obj)) {
1436
+ const out2 = obj.slice(0, SERIALIZE_MAX_ENTRIES).map(next);
1437
+ if (obj.length > SERIALIZE_MAX_ENTRIES) out2.push(`\u2026+${obj.length - SERIALIZE_MAX_ENTRIES} more`);
1438
+ return out2;
1235
1439
  }
1236
- if (obj instanceof Set) return { $type: "Set", values: [...obj].map((v) => serialize(v, maxDepth, seen, depth + 1)) };
1237
- if (Array.isArray(obj)) return obj.map((v) => serialize(v, maxDepth, seen, depth + 1));
1238
- if (typeof Element !== "undefined" && obj instanceof Element) return `<${obj.tagName.toLowerCase()}>`;
1239
1440
  const out = {};
1240
- for (const k of Object.keys(obj)) out[k] = serialize(obj[k], maxDepth, seen, depth + 1);
1441
+ const keys = Object.keys(obj);
1442
+ for (const k of keys.slice(0, SERIALIZE_MAX_ENTRIES)) out[k] = next(obj[k]);
1443
+ if (keys.length > SERIALIZE_MAX_ENTRIES) out["\u2026"] = `+${keys.length - SERIALIZE_MAX_ENTRIES} more`;
1241
1444
  const proto = Object.getPrototypeOf(obj);
1242
1445
  if (proto && proto !== Object.prototype && proto.constructor?.name) out.$type = proto.constructor.name;
1243
1446
  return out;
@@ -1277,13 +1480,20 @@ function deserializeOptions(o) {
1277
1480
  }
1278
1481
  function createDevtoolsNotifier(options = {}) {
1279
1482
  const bufferSize = options.bufferSize ?? 300;
1280
- const maxDepth = options.maxDepth ?? 6;
1483
+ const maxDepth = options.maxDepth ?? 4;
1281
1484
  const target = options.target ?? (typeof window !== "undefined" ? window : void 0);
1282
1485
  const buffer = [];
1283
1486
  let seq = 0;
1284
1487
  let flashOn = options.flashAvoidable ?? false;
1488
+ let live = !target || typeof target.addEventListener !== "function";
1489
+ if (!live) {
1490
+ target.addEventListener("message", (event) => {
1491
+ const data = event.data;
1492
+ if ((event.source === target || !event.source) && data && data.__rerenderLensReady === true) live = true;
1493
+ });
1494
+ }
1285
1495
  const post = (type, payload) => {
1286
- if (!target) return;
1496
+ if (!target || type === "report" && !live) return;
1287
1497
  const msg = { [DEVTOOLS_MARKER]: true, version: PROTOCOL_VERSION, type, payload };
1288
1498
  target.postMessage(msg, "*");
1289
1499
  };
@@ -1300,10 +1510,12 @@ function createDevtoolsNotifier(options = {}) {
1300
1510
  injected: typeof window !== "undefined" && typeof window.__RERENDER_LENS_INJECTED__ === "string",
1301
1511
  scheduled: getState().scheduled,
1302
1512
  commits: getState().commits,
1303
- overhead: { totalMs: getState().overheadMs, maxCommitMs: getState().maxCommitMs }
1513
+ overhead: { totalMs: getState().overheadMs, maxCommitMs: getState().maxCommitMs },
1514
+ truncated: getState().truncated
1304
1515
  });
1305
1516
  const bridge = {
1306
1517
  replay: () => {
1518
+ live = true;
1307
1519
  post("hello", info());
1308
1520
  for (const e of buffer) post("report", e.payload);
1309
1521
  },
@@ -1493,15 +1705,16 @@ function createDevtoolsNotifier(options = {}) {
1493
1705
  }
1494
1706
 
1495
1707
  // src/setup.ts
1496
- var env = (key) => {
1708
+ var read = (get) => {
1497
1709
  try {
1498
- return typeof process !== "undefined" && process && process.env ? process.env[key] : void 0;
1710
+ return get();
1499
1711
  } catch {
1500
1712
  return void 0;
1501
1713
  }
1502
1714
  };
1503
- if (env("NODE_ENV") !== "production" && typeof window !== "undefined" && !window.__RERENDER_LENS_DEVTOOLS__) {
1504
- const relay = env("RERENDER_LENS_RELAY") || env("NEXT_PUBLIC_RERENDER_LENS_RELAY");
1715
+ var production = read(() => process.env.NODE_ENV) === "production";
1716
+ if (!production && typeof window !== "undefined" && !window.__RERENDER_LENS_DEVTOOLS__) {
1717
+ const relay = read(() => process.env.RERENDER_LENS_RELAY) || read(() => process.env.NEXT_PUBLIC_RERENDER_LENS_RELAY);
1505
1718
  init({ trackAllMemoized: true, notifier: createDevtoolsNotifier(relay ? { relay } : {}) });
1506
1719
  }
1507
1720
  //# sourceMappingURL=setup.js.map