rerender-lens 0.4.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. package/CHANGELOG.md +78 -0
  2. package/README.md +192 -362
  3. package/dist/{budget-COBu7jBU.d.cts → budget-BnJlYwrV.d.ts} +20 -10
  4. package/dist/{budget-LkNjGtRc.d.ts → budget-CRswn-nC.d.cts} +20 -10
  5. package/dist/cli.cjs +248 -31
  6. package/dist/cli.cjs.map +1 -1
  7. package/dist/cli.js +248 -31
  8. package/dist/cli.js.map +1 -1
  9. package/dist/{devtools-CZebzpF6.d.ts → devtools-BYWat7nQ.d.ts} +7 -1
  10. package/dist/{devtools-BkCct3cJ.d.cts → devtools-DJi_6AfJ.d.cts} +7 -1
  11. package/dist/index.cjs +439 -41
  12. package/dist/index.cjs.map +1 -1
  13. package/dist/index.d.cts +131 -8
  14. package/dist/index.d.ts +131 -8
  15. package/dist/index.js +424 -42
  16. package/dist/index.js.map +1 -1
  17. package/dist/jest-setup.cjs +1673 -0
  18. package/dist/jest-setup.cjs.map +1 -0
  19. package/dist/jest-setup.d.cts +8 -0
  20. package/dist/jest-setup.d.ts +8 -0
  21. package/dist/jest-setup.js +1671 -0
  22. package/dist/jest-setup.js.map +1 -0
  23. package/dist/jest.cjs +1760 -0
  24. package/dist/jest.cjs.map +1 -0
  25. package/dist/jest.d.cts +23 -0
  26. package/dist/jest.d.ts +23 -0
  27. package/dist/jest.js +1751 -0
  28. package/dist/jest.js.map +1 -0
  29. package/dist/{notifiers-BjjGSHpp.d.ts → notifiers-DwKlvv4H.d.cts} +8 -4
  30. package/dist/{notifiers-BGQtWKfX.d.cts → notifiers-FNzKHs0a.d.ts} +8 -4
  31. package/dist/playwright.cjs +230 -28
  32. package/dist/playwright.cjs.map +1 -1
  33. package/dist/playwright.d.cts +4 -4
  34. package/dist/playwright.d.ts +4 -4
  35. package/dist/playwright.js +230 -28
  36. package/dist/playwright.js.map +1 -1
  37. package/dist/relay.cjs +10 -2
  38. package/dist/relay.cjs.map +1 -1
  39. package/dist/relay.js +10 -2
  40. package/dist/relay.js.map +1 -1
  41. package/dist/rerender-lens.iife.js +356 -54
  42. package/dist/runner-report-B8Sc3FZO.d.cts +46 -0
  43. package/dist/runner-report-CW8X5RwK.d.ts +46 -0
  44. package/dist/setup.cjs +112 -27
  45. package/dist/setup.cjs.map +1 -1
  46. package/dist/setup.js +112 -27
  47. package/dist/setup.js.map +1 -1
  48. package/dist/{types-BzUEVkxJ.d.cts → types-DW5-N2XH.d.cts} +55 -2
  49. package/dist/{types-BzUEVkxJ.d.ts → types-DW5-N2XH.d.ts} +55 -2
  50. package/dist/vite.d.cts +1 -1
  51. package/dist/vite.d.ts +1 -1
  52. package/dist/vitest-setup.cjs +337 -41
  53. package/dist/vitest-setup.cjs.map +1 -1
  54. package/dist/vitest-setup.d.cts +4 -4
  55. package/dist/vitest-setup.d.ts +4 -4
  56. package/dist/vitest-setup.js +337 -41
  57. package/dist/vitest-setup.js.map +1 -1
  58. package/dist/vitest.cjs +400 -77
  59. package/dist/vitest.cjs.map +1 -1
  60. package/dist/vitest.d.cts +21 -39
  61. package/dist/vitest.d.ts +21 -39
  62. package/dist/vitest.js +401 -78
  63. package/dist/vitest.js.map +1 -1
  64. package/package.json +40 -4
  65. package/panel/panel.css +1 -0
  66. package/panel/panel.js +499 -309
package/dist/setup.js CHANGED
@@ -159,30 +159,30 @@ function diffRecords(prev, next, basePath = "") {
159
159
  if (Object.is(a, b)) continue;
160
160
  const kind = classify(a, b);
161
161
  if (kind === "different") {
162
- changes.push({ path: diffBudgetExhausted() ? 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 });
163
163
  } else {
164
164
  changes.push({ path, kind, prev: a, next: b });
165
165
  }
166
166
  }
167
167
  return changes;
168
168
  }
169
- function firstDifferentPath(a, b, path, depth = 0) {
170
- if (depth > 8) return path;
169
+ function firstDifferentPath(a, b, path = "", depth = 0) {
170
+ if (depth > 8) return path || "(value)";
171
171
  if (Array.isArray(a) && Array.isArray(b)) {
172
- if (a.length !== b.length) return `${path}.length`;
172
+ if (a.length !== b.length) return path ? `${path}.length` : "length";
173
173
  for (let i = 0; i < a.length; i++) {
174
174
  if (!deepEqual(a[i], b[i])) return firstDifferentPath(a[i], b[i], joinPath(path, i), depth + 1);
175
175
  }
176
- return path;
176
+ return null;
177
177
  }
178
178
  if (isPlainObject(a) && isPlainObject(b)) {
179
179
  const keys = /* @__PURE__ */ new Set([...Object.keys(a), ...Object.keys(b)]);
180
180
  for (const k of keys) {
181
181
  if (!deepEqual(a[k], b[k])) return firstDifferentPath(a[k], b[k], joinPath(path, k), depth + 1);
182
182
  }
183
- return path;
183
+ return null;
184
184
  }
185
- return path;
185
+ return deepEqual(a, b) ? null : path || "(value)";
186
186
  }
187
187
 
188
188
  // src/report.ts
@@ -200,10 +200,12 @@ function storeAdvice(custom) {
200
200
  }
201
201
  var isGenuine = (c) => c.kind === "different" || c.kind === "added" || c.kind === "removed";
202
202
  var isChildren = (change) => change.path === "children";
203
+ var isRefObject = (change) => change.path === "ref" && change.kind === "deep-equal";
203
204
  function fixFor(change) {
204
205
  if (isChildren(change) && (change.kind === "element" || change.kind === "deep-equal")) {
205
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`;
206
207
  }
208
+ if (isRefObject(change)) return `create the ref once with useRef (or createRef outside the render) and pass the same object every time`;
207
209
  switch (change.kind) {
208
210
  case "function":
209
211
  return `wrap it in useCallback (or hoist it out of the parent's render)`;
@@ -219,6 +221,7 @@ function describe(change) {
219
221
  if (isChildren(change) && (change.kind === "element" || change.kind === "deep-equal")) {
220
222
  return `children are new React elements with the same types and props on every render of the parent`;
221
223
  }
224
+ if (isRefObject(change)) return `prop "ref" is a new ref object on every render (React re-attaches it each time)`;
222
225
  switch (change.kind) {
223
226
  case "deep-equal":
224
227
  return `prop "${change.path}" is a new reference but deep-equal to the previous value`;
@@ -246,11 +249,21 @@ function buildReport(input) {
246
249
  if (causes.length === 0) trigger = "parent";
247
250
  else if (causes.length === 1) trigger = causes[0];
248
251
  else trigger = "mixed";
249
- 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;
250
256
  const reasons = [];
251
- 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) {
252
265
  const who = input.parent ? `<${input.parent.name}> re-rendered (${describeTrigger(input.parent.trigger)})` : "its parent re-rendered";
253
- 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}.`);
254
267
  } else if (avoidable && input.parent) {
255
268
  reasons.push(`caused by <${input.parent.name}> re-rendering (${describeTrigger(input.parent.trigger)}).`);
256
269
  }
@@ -259,9 +272,9 @@ function buildReport(input) {
259
272
  reasons.push(fix ? `${describe(c)}: ${fix}.` : `${describe(c)}.`);
260
273
  }
261
274
  const memoized = input.memoized !== false;
262
- if (avoidable && !memoized && input.propChanges.length > 0) {
275
+ if (avoidable && !memoized && !input.compiled && input.propChanges.length > 0) {
263
276
  reasons.push(
264
- `"${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)"}.`
265
278
  );
266
279
  }
267
280
  for (const c of stateChanges) {
@@ -273,6 +286,8 @@ function buildReport(input) {
273
286
  const where = c.provider && c.provider.component ? ` (provided by <${c.provider.component}>)` : "";
274
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` : "";
275
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.`);
276
291
  } else if (isGenuine(c)) reasons.push(`${hookLabel(c)} changed.`);
277
292
  else if (isStateHook(c))
278
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.`);
@@ -303,6 +318,7 @@ function buildReport(input) {
303
318
  reasons,
304
319
  time: now()
305
320
  };
321
+ if (input.compiled) report.compiled = true;
306
322
  if (input.selfDuration !== void 0) report.selfDuration = input.selfDuration;
307
323
  if (input.treeDuration !== void 0) report.treeDuration = input.treeDuration;
308
324
  if (input.commitPriority) report.commitPriority = input.commitPriority;
@@ -340,10 +356,10 @@ var KIND_LABEL = {
340
356
  };
341
357
  function summarize(report) {
342
358
  const counts2 = /* @__PURE__ */ new Map();
343
- for (const c of [...report.propChanges, ...report.stateChanges, ...report.hookChanges]) {
359
+ for (const c of [...report.propChanges || [], ...report.stateChanges || [], ...report.hookChanges || []]) {
344
360
  counts2.set(c.kind, (counts2.get(c.kind) ?? 0) + 1);
345
361
  }
346
- const parts = [...counts2].map(([kind, n]) => `${n} ${KIND_LABEL[kind]}`);
362
+ const parts = [...counts2].map(([kind, n]) => `${n} ${KIND_LABEL[kind] || kind}`);
347
363
  if (parts.length === 0) parts.push("no changes");
348
364
  return parts.join(", ");
349
365
  }
@@ -406,6 +422,27 @@ function dispatch(report, override) {
406
422
  }
407
423
 
408
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"]);
409
446
  var noop = () => {
410
447
  };
411
448
  function customHooksFromStack(stack, componentName, marker) {
@@ -420,7 +457,7 @@ function customHooksFromStack(stack, componentName, marker) {
420
457
  continue;
421
458
  }
422
459
  if (name === componentName || name.endsWith("." + componentName)) break;
423
- 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);
424
461
  if (names.length > 8) break;
425
462
  }
426
463
  return names;
@@ -720,6 +757,11 @@ function isMemoizedFiber(fiber) {
720
757
  }
721
758
  return false;
722
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
+ }
723
765
  function durationsOf(fiber) {
724
766
  if (typeof fiber.actualDuration !== "number") return null;
725
767
  const tree = fiber.actualDuration;
@@ -898,7 +940,8 @@ function diffHooks(fiber, alt) {
898
940
  const labels = hookLabelsFor(fiber, b);
899
941
  let i = 0;
900
942
  while (a && b) {
901
- 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)) {
902
945
  const hook = labels?.[i] ?? hookLabel2(b);
903
946
  out.push({
904
947
  path: `${hook}#${i}`,
@@ -932,6 +975,10 @@ function diffContexts(fiber, alt) {
932
975
  let a = alt.dependencies?.firstContext ?? null;
933
976
  let b = fiber.dependencies?.firstContext ?? null;
934
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
+ }
935
982
  while (a && b) {
936
983
  if (a.context === b.context && !Object.is(a.memoizedValue, b.memoizedValue)) {
937
984
  const name = b.context.displayName ?? "Context";
@@ -964,10 +1011,26 @@ function diffContexts(fiber, alt) {
964
1011
  return out;
965
1012
  }
966
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
+ }
967
1030
  function analyze(fiber, alt, trackHooks) {
968
1031
  const prevProps = alt.memoizedProps ?? {};
969
1032
  const nextProps = fiber.memoizedProps ?? {};
970
- const propChanges = prevProps === nextProps ? [] : diffRecords(prevProps, nextProps);
1033
+ const propChanges = diffProps(prevProps, nextProps);
971
1034
  let stateChanges = [];
972
1035
  let hookChanges = [];
973
1036
  if (fiber.tag === ClassComponent) {
@@ -980,7 +1043,7 @@ function analyze(fiber, alt, trackHooks) {
980
1043
  } else if (trackHooks) {
981
1044
  hookChanges = [...diffHooks(fiber, alt), ...diffContexts(fiber, alt)];
982
1045
  }
983
- const isStateHook = (h) => h.hook !== "useContext" && h.hook !== "useSyncExternalStore";
1046
+ const isStateHook = (h) => h.hook !== "useContext" && h.hook !== "useSyncExternalStore" && h.hook !== "useDeferredValue";
984
1047
  const causes = [];
985
1048
  if (propChanges.some(isGenuine2)) causes.push("props");
986
1049
  if (stateChanges.some(isGenuine2) || hookChanges.some((h) => isStateHook(h) && isGenuine2(h))) causes.push("state");
@@ -1020,6 +1083,24 @@ var lastCommit = null;
1020
1083
  var EFFECT_LOOP_WINDOW_MS = 50;
1021
1084
  var MAX_REPORTS_PER_COMMIT = 200;
1022
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
+ }
1023
1104
  function onCommit(root, commitPriority) {
1024
1105
  const s = getState();
1025
1106
  if (!s.enabled) return;
@@ -1029,7 +1110,7 @@ function onCommit(root, commitPriority) {
1029
1110
  const rendered = [];
1030
1111
  const renderedNames = /* @__PURE__ */ new Set();
1031
1112
  let hot = false;
1032
- let suspenseResolved = false;
1113
+ let resolvedBoundaries = null;
1033
1114
  const stack = [root.current];
1034
1115
  while (stack.length) {
1035
1116
  const fiber = stack.pop();
@@ -1039,7 +1120,7 @@ function onCommit(root, commitPriority) {
1039
1120
  renderedNames.add(fiberName(fiber));
1040
1121
  if (shouldTrack(fiberType(fiber), o)) rendered.push(fiber);
1041
1122
  }
1042
- 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);
1043
1124
  if (!alt || fiber.child !== alt.child) {
1044
1125
  for (let c = fiber.child; c; c = c.sibling) stack.push(c);
1045
1126
  }
@@ -1051,10 +1132,10 @@ function onCommit(root, commitPriority) {
1051
1132
  let commitCause;
1052
1133
  let afterCommit;
1053
1134
  const inputDriven = commitPriority === "immediate" || commitPriority === "user-blocking";
1054
- if (!inputDriven && lastCommit && at - lastCommit.at < EFFECT_LOOP_WINDOW_MS && updaters.some((u) => lastCommit.rendered.has(u))) {
1135
+ if (!inputDriven && lastCommit && at - lastCommit.at < EFFECT_LOOP_WINDOW_MS && updaters.some((u) => lastCommit.rendered.has(u)) && !rendered.some(isDeferredCatchUp)) {
1055
1136
  commitCause = "effect-after-commit";
1056
1137
  afterCommit = lastCommit.id;
1057
- } else if (suspenseResolved) commitCause = "suspense-resolved";
1138
+ } else if (resolvedBoundaries) commitCause = "suspense-resolved";
1058
1139
  if (rendered.length === 0) {
1059
1140
  lastCommit = { id: s.nextCommitId, at, rendered: renderedNames };
1060
1141
  return;
@@ -1106,6 +1187,9 @@ function onCommit(root, commitPriority) {
1106
1187
  owner: ownerName(fiber),
1107
1188
  path: componentPath(fiber),
1108
1189
  memoized: isMemoizedFiber(fiber),
1190
+ compiled: isCompiledFiber(fiber),
1191
+ classComponent: fiber.tag === ClassComponent,
1192
+ revealed: resolvedBoundaries !== null && a.trigger === "parent" && isUnder(fiber, resolvedBoundaries),
1109
1193
  selfDuration: durations ? durations.self : void 0,
1110
1194
  treeDuration: durations ? durations.tree : void 0,
1111
1195
  commitId,
@@ -1288,7 +1372,7 @@ function flash(target, duration = 500) {
1288
1372
  }
1289
1373
 
1290
1374
  // src/version.ts
1291
- var VERSION = "0.4.0" ;
1375
+ var VERSION = "0.5.0" ;
1292
1376
 
1293
1377
  // src/devtools.ts
1294
1378
  var DEVTOOLS_MARKER = "__rerenderLens";
@@ -1621,15 +1705,16 @@ function createDevtoolsNotifier(options = {}) {
1621
1705
  }
1622
1706
 
1623
1707
  // src/setup.ts
1624
- var env = (key) => {
1708
+ var read = (get) => {
1625
1709
  try {
1626
- return typeof process !== "undefined" && process && process.env ? process.env[key] : void 0;
1710
+ return get();
1627
1711
  } catch {
1628
1712
  return void 0;
1629
1713
  }
1630
1714
  };
1631
- if (env("NODE_ENV") !== "production" && typeof window !== "undefined" && !window.__RERENDER_LENS_DEVTOOLS__) {
1632
- 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);
1633
1718
  init({ trackAllMemoized: true, notifier: createDevtoolsNotifier(relay ? { relay } : {}) });
1634
1719
  }
1635
1720
  //# sourceMappingURL=setup.js.map