react-debug-updates 0.1.7 → 0.1.10

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.
package/README.md CHANGED
@@ -1,18 +1,24 @@
1
1
  # react-debug-updates
2
2
 
3
- Visual debugging overlays and console logging for React re-renders. See exactly which components re-render, how often, how long they take, and *why* they re-rendered — all without modifying your components.
3
+ See exactly which React components re-render, how often, how long they take, and *why* — all without modifying your components.
4
4
 
5
5
  ![highlight overlays](https://img.shields.io/badge/overlays-visual%20highlights-61dafb) ![zero config](https://img.shields.io/badge/setup-zero%20config-green)
6
6
 
7
7
  <img src="demo.gif" alt="demo" width="852" height="476" />
8
8
 
9
+ ## Why?
10
+
11
+ I was working on an Electron app and spent hours trying to get the official React DevTools to work with it. DevTools' Electron integration is fragile, poorly documented, and breaks between versions. I just needed to see which components were re-rendering so I could fix performance issues.
12
+
13
+ So I wrote this — a plug-and-play one-liner that gives you visual highlight overlays and console logging for React re-renders. No browser extension, no Electron hacks, no configuration. Works in any React web environment — browsers, Electron, iframes.
14
+
9
15
  ## How it works
10
16
 
11
- Hooks into `__REACT_DEVTOOLS_GLOBAL_HOOK__` to intercept every React commit. Works in any React web environment browsers, Electron, iframes no React DevTools extension required. No wrappers, no HOCs, no code changes — just call `attachRenderLogger()` and you get:
17
+ Hooks into `__REACT_DEVTOOLS_GLOBAL_HOOK__` to intercept every React commit. Uses the same fiber tree diffing approach as React DevTools to detect which components actually re-rendered. No React DevTools extension required. No wrappers, no HOCs, no code changes — just call `startReactUpdatesMonitor()` and you get:
12
18
 
19
+ - **Visual highlights** — highlight boxes on re-rendered DOM nodes with a heat-map color scale (blue → red as render count increases)
13
20
  - **Console logging** — grouped, color-coded re-render reports with component tree paths and render durations
14
- - **Visual overlays** — highlight boxes on re-rendered DOM nodes with a heat-map color scale (bluered as render count increases)
15
- - **Cause detection** — pinpoint *which* `useState`, `useReducer`, `useSyncExternalStore`, or `useContext` hook triggered each re-render, with previous→next values
21
+ - **Update reasons** — pinpoint *which* `useState`, `useReducer`, `useSyncExternalStore`, or `useContext` hook triggered each re-render, with previousnext values
16
22
 
17
23
  ## Install
18
24
 
@@ -26,77 +32,61 @@ pnpm add react-debug-updates
26
32
 
27
33
  ## Quick start
28
34
 
29
- Import and call `attachRenderLogger` **before** React renders anything — ideally at the very top of your entry point. This ensures the hook is in place before the first commit.
35
+ Import and call `startReactUpdatesMonitor` **before** React renders anything — ideally at the very top of your entry point. This ensures the hook is in place before the first commit.
30
36
 
31
37
  ```ts
32
- import { attachRenderLogger } from "react-debug-updates";
38
+ import { startReactUpdatesMonitor } from "react-debug-updates";
33
39
 
34
- // Call before React renders top of your entry point
35
- const logger = attachRenderLogger({
36
- highlight: true,
37
- showCauses: true,
38
- });
40
+ // One-liner visual highlights out of the box
41
+ const stop = startReactUpdatesMonitor();
39
42
 
40
43
  // Later, to clean up:
41
- logger?.disconnect();
44
+ stop?.();
42
45
  ```
43
46
 
44
47
  ### Dev-only guard
45
48
 
46
49
  ```ts
47
50
  if (process.env.NODE_ENV === "development") {
48
- const { attachRenderLogger } = await import("react-debug-updates");
49
- attachRenderLogger({ highlight: true, showCauses: true });
51
+ const { startReactUpdatesMonitor } = await import("react-debug-updates");
52
+ startReactUpdatesMonitor();
50
53
  }
51
54
  ```
52
55
 
56
+ ### With options
57
+
58
+ ```ts
59
+ startReactUpdatesMonitor({
60
+ reasonOfUpdate: true,
61
+ logToConsole: true,
62
+ highlightOpacity: 0.5,
63
+ highlightShowLabels: false,
64
+ });
65
+ ```
66
+
53
67
  ## Requirements
54
68
 
55
69
  - A **React dev build** (which automatically creates `__REACT_DEVTOOLS_GLOBAL_HOOK__`) — no browser extension needed
56
- - For `showCauses` and render durations: React must be in **dev mode** (provides `_debugHookTypes` and `actualDuration` on fibers)
70
+ - For `reasonOfUpdate` and render durations: React must be in **dev mode** (provides `_debugHookTypes` and `actualDuration` on fibers)
57
71
 
58
72
  ## API
59
73
 
60
- ### `attachRenderLogger(options?): RenderLogger | null`
74
+ ### `startReactUpdatesMonitor(options?): (() => void) | null`
61
75
 
62
- Returns a `RenderLogger` handle, or `null` if the DevTools hook is not available.
76
+ Returns a `stop` function to unhook from React and remove all overlays, or `null` if the DevTools hook is not available.
63
77
 
64
78
  #### Options
65
79
 
66
80
  | Option | Type | Default | Description |
67
81
  | --- | --- | --- | --- |
68
- | `silent` | `boolean` | `false` | Suppress console output |
69
82
  | `mode` | `"self-triggered" \| "all"` | `"self-triggered"` | `"self-triggered"` tracks only components whose own state changed. `"all"` includes children swept by parent updates |
70
- | `bufferSize` | `number` | `500` | Max entries kept in the ring buffer |
71
- | `filter` | `(entry: RenderEntry) => boolean` | | Return `false` to skip an entry |
72
- | `highlight` | `boolean \| HighlightOptions` | `false` | Enable visual overlay highlights |
73
- | `showCauses` | `boolean` | `false` | Detect and display why each component re-rendered |
74
-
75
- #### `HighlightOptions`
76
-
77
- | Option | Type | Default | Description |
78
- | --- | --- | --- | --- |
79
- | `flushInterval` | `number` | `250` | Milliseconds between overlay flush cycles |
80
- | `animationDuration` | `number` | `1200` | Overlay fade-out animation duration (ms) |
81
- | `showLabels` | `boolean` | `true` | Show text labels (name, count, duration, cause) above overlays |
82
- | `opacity` | `number` | `0.3` | Peak opacity of overlay highlights (0–1) |
83
-
84
- ### `RenderLogger`
85
-
86
- | Property | Type | Description |
87
- | --- | --- | --- |
88
- | `entries` | `RenderEntry[]` | Ring buffer of recorded re-render entries |
89
- | `disconnect` | `() => void` | Unhook from React and remove all overlays |
90
-
91
- ### `RenderEntry`
92
-
93
- | Property | Type | Description |
94
- | --- | --- | --- |
95
- | `component` | `string` | Component display name |
96
- | `path` | `string` | Ancestor component path (e.g. `"App → Layout → Sidebar"`) |
97
- | `duration` | `number` | Render duration in ms (requires React dev mode) |
98
- | `timestamp` | `number` | `performance.now()` when the entry was recorded |
99
- | `causes` | `UpdateCause[]` | Why this component re-rendered (requires `showCauses`) |
83
+ | `reasonOfUpdate` | `boolean` | `false` | Detect and display why each component re-rendered |
84
+ | `logToConsole` | `boolean` | `false` | Log re-renders to the console |
85
+ | `highlight` | `boolean` | `true` | Enable visual highlight overlays |
86
+ | `highlightShowLabels` | `boolean` | `true` | Show text labels (name, count, duration, cause) above highlights |
87
+ | `highlightOpacity` | `number` | `0.3` | Peak opacity of highlight overlays (0–1) |
88
+ | `highlightFlushInterval` | `number` | `250` | Milliseconds between highlight flush cycles |
89
+ | `highlightAnimationDuration` | `number` | `1200` | Highlight fade-out animation duration (ms) |
100
90
 
101
91
  ### `UpdateCause`
102
92
 
@@ -120,11 +110,11 @@ Returns a `RenderLogger` handle, or `null` if the DevTools hook is not available
120
110
  ↳ useContext changed
121
111
  ```
122
112
 
123
- ## Visual overlays
113
+ ## Visual highlights
124
114
 
125
115
  Re-rendered components get a highlight box that fades out. The color shifts from blue to red as the same node re-renders repeatedly within a flush window — making "hot" components visually obvious.
126
116
 
127
- Each overlay label shows: `ComponentName ×count duration (cause)`
117
+ Each highlight label shows: `ComponentName ×count duration (cause)`
128
118
 
129
119
  ## License
130
120
 
package/dist/format.d.ts CHANGED
@@ -1,8 +1,8 @@
1
- import type { PendingEntry, UpdateCause } from "./types.js";
1
+ import type { HighlightEntry, UpdateCause } from "./types.js";
2
2
  export declare function formatValue(value: unknown, maxLength?: number): string;
3
3
  /** Compact summary for overlay labels. */
4
4
  export declare function formatCausesShort(causes: UpdateCause[]): string;
5
5
  /** Detailed lines for console output. */
6
6
  export declare function formatCausesConsole(causes: UpdateCause[]): string[];
7
7
  /** Log re-renders as a collapsed console group. */
8
- export declare function logRerendersToConsole(entries: PendingEntry[], showCauses: boolean): void;
8
+ export declare function logRerendersToConsole(entries: HighlightEntry[], showCauses: boolean): void;
@@ -0,0 +1,5 @@
1
+ import type { OverlayConfig, HighlightEntry } from "./types.js";
2
+ export declare function createHighlighter(config: OverlayConfig): {
3
+ push: (entry: HighlightEntry) => void;
4
+ dispose: () => void;
5
+ };
package/dist/index.d.ts CHANGED
@@ -1,2 +1,2 @@
1
- export { attachRenderLogger } from "./logger.js";
2
- export type { LoggerOptions, RenderLogger, RenderEntry, HighlightOptions, UpdateCause, } from "./types.js";
1
+ export { startReactUpdatesMonitor } from "./monitor.js";
2
+ export type { MonitorOptions, UpdateCause } from "./types.js";
@@ -0,0 +1,15 @@
1
+ import type { MonitorOptions } from "./types.js";
2
+ /**
3
+ * Start monitoring React re-renders.
4
+ *
5
+ * Hooks into `__REACT_DEVTOOLS_GLOBAL_HOOK__.onCommitFiberRoot` to intercept
6
+ * every React commit. Shows visual highlight overlays on re-rendered DOM nodes
7
+ * and optionally logs re-renders to the console.
8
+ *
9
+ * Call this **before** React renders anything — ideally at the very top of
10
+ * your entry point.
11
+ *
12
+ * Returns a `stop` function to unhook and clean up, or `null` if called
13
+ * in a non-browser environment (e.g. SSR).
14
+ */
15
+ export declare function startReactUpdatesMonitor({ logToConsole, highlight, mode, reasonOfUpdate, highlightFlushInterval, highlightAnimationDuration, highlightShowLabels, highlightOpacity, }?: MonitorOptions): (() => void) | null;
@@ -322,13 +322,7 @@ function heatColor(count, alpha) {
322
322
  const lightness = 55 - normalizedCount * 10;
323
323
  return `hsla(${hue}, ${saturation}%, ${lightness}%, ${alpha})`;
324
324
  }
325
- const HIGHLIGHT_DEFAULTS = {
326
- flushInterval: 250,
327
- animationDuration: 1200,
328
- showLabels: true,
329
- opacity: 0.3
330
- };
331
- function createBatcher(options) {
325
+ function createHighlighter(config) {
332
326
  let pending = [];
333
327
  let timer = null;
334
328
  function flush() {
@@ -381,10 +375,10 @@ function createBatcher(options) {
381
375
  style.height = `${rect.height}px`;
382
376
  style.backgroundColor = fillColor;
383
377
  style.border = `1.5px solid ${borderColor}`;
384
- style.setProperty("--rdu-opacity", String(options.opacity));
385
- style.animation = `${OVERLAY_ANIMATION_NAME} ${options.animationDuration}ms ease-out forwards`;
378
+ style.setProperty("--rdu-opacity", String(config.opacity));
379
+ style.animation = `${OVERLAY_ANIMATION_NAME} ${config.animationDuration}ms ease-out forwards`;
386
380
  const label = element.firstElementChild;
387
- if (options.showLabels) {
381
+ if (config.showLabels) {
388
382
  const countText = coalesced.count > 1 ? ` ×${coalesced.count}` : "";
389
383
  const durationText = coalesced.totalDuration > 0 ? ` ${coalesced.totalDuration.toFixed(1)}ms` : "";
390
384
  const causeText = coalesced.causeSummary ? ` (${coalesced.causeSummary})` : "";
@@ -399,7 +393,7 @@ function createBatcher(options) {
399
393
  function push(entry) {
400
394
  pending.push(entry);
401
395
  if (!timer) {
402
- timer = setInterval(flush, options.flushInterval);
396
+ timer = setInterval(flush, config.flushInterval);
403
397
  }
404
398
  }
405
399
  function dispose() {
@@ -411,75 +405,80 @@ function createBatcher(options) {
411
405
  }
412
406
  return { push, dispose };
413
407
  }
414
- function attachRenderLogger(options = {}) {
415
- const {
416
- silent = false,
417
- bufferSize = 500,
418
- filter,
419
- highlight = false,
420
- mode = "self-triggered",
421
- showCauses = false
422
- } = options;
423
- const hook = window.__REACT_DEVTOOLS_GLOBAL_HOOK__;
424
- if (!hook) {
425
- console.warn(
426
- "[react-debug-updates] __REACT_DEVTOOLS_GLOBAL_HOOK__ not found. Make sure React DevTools or a dev build of React is active."
427
- );
428
- return null;
408
+ function ensureDevToolsHook(win) {
409
+ const global = win;
410
+ if (!global.__REACT_DEVTOOLS_GLOBAL_HOOK__) {
411
+ global.__REACT_DEVTOOLS_GLOBAL_HOOK__ = {
412
+ supportsFiber: true,
413
+ inject() {
414
+ return 1;
415
+ },
416
+ onCommitFiberRoot() {
417
+ },
418
+ onCommitFiberUnmount() {
419
+ },
420
+ onPostCommitFiberRoot() {
421
+ },
422
+ checkDCE() {
423
+ }
424
+ };
429
425
  }
430
- const highlightOptions = highlight ? {
431
- ...HIGHLIGHT_DEFAULTS,
432
- ...typeof highlight === "object" ? highlight : {}
433
- } : null;
434
- const batcher = highlightOptions ? createBatcher(highlightOptions) : null;
435
- const entries = [];
426
+ return global.__REACT_DEVTOOLS_GLOBAL_HOOK__;
427
+ }
428
+ function startReactUpdatesMonitor({
429
+ logToConsole = false,
430
+ highlight = true,
431
+ mode = "self-triggered",
432
+ reasonOfUpdate = false,
433
+ highlightFlushInterval = 250,
434
+ highlightAnimationDuration = 1200,
435
+ highlightShowLabels = true,
436
+ highlightOpacity = 0.3
437
+ } = {}) {
438
+ if (typeof window === "undefined") return null;
439
+ const hook = ensureDevToolsHook(window);
440
+ const highlighter = highlight ? createHighlighter({
441
+ flushInterval: highlightFlushInterval,
442
+ animationDuration: highlightAnimationDuration,
443
+ showLabels: highlightShowLabels,
444
+ opacity: highlightOpacity
445
+ }) : null;
436
446
  const previousOnCommit = hook.onCommitFiberRoot.bind(hook);
437
447
  hook.onCommitFiberRoot = (rendererID, root, priorityLevel) => {
438
448
  previousOnCommit(rendererID, root, priorityLevel);
439
449
  const detectedRenders = detectRenders(root.current, mode);
440
450
  if (detectedRenders.length === 0) return;
441
- const pendingEntries = [];
451
+ const highlightEntries = [];
442
452
  for (let i = 0; i < detectedRenders.length; i++) {
443
453
  const { fiber, depth } = detectedRenders[i];
444
454
  const name = getComponentName(fiber);
445
455
  if (!name) continue;
446
- const pendingEntry = {
456
+ const entry = {
447
457
  component: name,
448
458
  path: getFiberPath(fiber),
449
459
  duration: fiber.actualDuration ?? 0,
450
460
  depth,
451
461
  domNode: findNearestDOMNode(fiber),
452
- causes: showCauses ? detectCauses(fiber) : []
453
- };
454
- const renderEntry = {
455
- component: pendingEntry.component,
456
- path: pendingEntry.path,
457
- duration: pendingEntry.duration,
458
- timestamp: performance.now(),
459
- causes: pendingEntry.causes
462
+ causes: reasonOfUpdate ? detectCauses(fiber) : []
460
463
  };
461
- if (filter && !filter(renderEntry)) continue;
462
- if (entries.length >= bufferSize) entries.shift();
463
- entries.push(renderEntry);
464
- pendingEntries.push(pendingEntry);
465
- batcher == null ? void 0 : batcher.push(pendingEntry);
464
+ highlightEntries.push(entry);
465
+ highlighter == null ? void 0 : highlighter.push(entry);
466
466
  }
467
- if (!silent) {
468
- logRerendersToConsole(pendingEntries, showCauses);
467
+ if (logToConsole) {
468
+ logRerendersToConsole(highlightEntries, reasonOfUpdate);
469
469
  }
470
470
  };
471
- const disconnect = () => {
472
- hook.onCommitFiberRoot = previousOnCommit;
473
- batcher == null ? void 0 : batcher.dispose();
474
- disposeAllOverlays();
475
- };
476
- if (!silent) {
471
+ if (logToConsole) {
477
472
  console.log(
478
473
  "%c⚛ react-debug-updates attached",
479
474
  "color: #61dafb; font-weight: bold"
480
475
  );
481
476
  }
482
- return { entries, disconnect };
477
+ return () => {
478
+ hook.onCommitFiberRoot = previousOnCommit;
479
+ highlighter == null ? void 0 : highlighter.dispose();
480
+ disposeAllOverlays();
481
+ };
483
482
  }
484
- exports.attachRenderLogger = attachRenderLogger;
483
+ exports.startReactUpdatesMonitor = startReactUpdatesMonitor;
485
484
  //# sourceMappingURL=react-debug-updates.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"react-debug-updates.cjs","sources":["../src/fiber.ts","../src/causes.ts","../src/format.ts","../src/overlay.ts","../src/batcher.ts","../src/logger.ts"],"sourcesContent":["import type { Fiber, DetectedRender } from \"./types.js\";\n\n// ────────────────────────────────────────────\n// Fiber tag constants\n// ────────────────────────────────────────────\n\nexport const FiberTag = {\n FunctionComponent: 0,\n ClassComponent: 1,\n HostComponent: 5,\n ForwardRef: 11,\n SimpleMemoComponent: 15,\n MemoComponent: 14,\n} as const;\n\n// Component tags we report as re-renders.\n// MemoComponent (14) is excluded to avoid double-reporting — it's a wrapper\n// fiber around the inner component. The inner component (FunctionComponent,\n// ForwardRef, or SimpleMemoComponent) has its own PerformedWork flag and\n// will be reported correctly on its own.\nconst COMPONENT_TAGS = new Set<number>([\n FiberTag.FunctionComponent,\n FiberTag.ClassComponent,\n FiberTag.ForwardRef,\n FiberTag.SimpleMemoComponent,\n]);\n\nconst PerformedWork = 0b0000001;\n\n// ────────────────────────────────────────────\n// Fiber tree helpers\n// ────────────────────────────────────────────\n\nexport function getComponentName(fiber: Fiber): string | null {\n const { type } = fiber;\n if (!type || typeof type === \"string\") return null;\n return type.displayName ?? type.name ?? null;\n}\n\nfunction isHTMLElement(value: unknown): value is HTMLElement {\n return (\n typeof value === \"object\" &&\n value !== null &&\n (value as Node).nodeType === 1 &&\n typeof (value as HTMLElement).getBoundingClientRect === \"function\"\n );\n}\n\nexport function findNearestDOMNode(fiber: Fiber): HTMLElement | null {\n if (fiber.tag === FiberTag.HostComponent && isHTMLElement(fiber.stateNode)) {\n return fiber.stateNode;\n }\n let child: Fiber | null = fiber.child;\n while (child) {\n const found = findNearestDOMNode(child);\n if (found) return found;\n child = child.sibling;\n }\n return null;\n}\n\nexport function getFiberPath(fiber: Fiber, maxDepth = 5): string {\n const parts: string[] = [];\n let current: Fiber | null = fiber.return;\n let depth = 0;\n while (current && depth < maxDepth) {\n const name = getComponentName(current);\n if (name) parts.unshift(name);\n current = current.return;\n depth++;\n }\n return parts.length ? parts.join(\" → \") : \"(root)\";\n}\n\n// ────────────────────────────────────────────\n// Self-triggered detection\n// ────────────────────────────────────────────\n\nfunction isSelfTriggered(fiber: Fiber): boolean {\n const alternate = fiber.alternate;\n if (!alternate) return false;\n return fiber.memoizedProps === alternate.memoizedProps;\n}\n\n// ────────────────────────────────────────────\n// didFiberRender — mirrors React DevTools\n// ────────────────────────────────────────────\n//\n// See: react-devtools-shared/src/backend/fiber/shared/DevToolsFiberChangeDetection.js\n//\n// For component fibers (function, class, memo, forwardRef), React sets the\n// PerformedWork flag (bit 0) only when user code actually executes.\n// createWorkInProgress resets flags to NoFlags, so PerformedWork on a\n// work-in-progress fiber is always fresh — never stale from a prior commit.\n//\n// This check must only be called AFTER confirming prevFiber !== nextFiber\n// (i.e. the fiber was actually processed, not a bailed-out subtree).\n\nfunction didFiberRender(nextFiber: Fiber): boolean {\n return (nextFiber.flags & PerformedWork) === PerformedWork;\n}\n\n// ────────────────────────────────────────────\n// Detect which components re-rendered in a commit\n// ────────────────────────────────────────────\n//\n// Mirrors React DevTools' updateFiberRecursively / updateChildrenRecursively.\n//\n// React double-buffers fibers. After a commit, root.current is the committed\n// tree and root.current.alternate is the previous tree. We walk both in\n// parallel. At each level, if prevChild and nextChild are the same object,\n// React bailed out that entire subtree (via cloneChildFibers) — we skip it.\n// Otherwise, we use nextChild.alternate as the previous fiber and check\n// didFiberRender (PerformedWork) to see if user code actually ran.\n//\n// Returns lightweight DetectedRender objects — just the fiber and its depth.\n// DOM lookup, cause detection, and formatting are the caller's responsibility.\n\nexport function detectRenders(\n root: Fiber,\n mode: \"self-triggered\" | \"all\",\n): DetectedRender[] {\n const results: DetectedRender[] = [];\n const selfTriggeredOnly = mode === \"self-triggered\";\n\n // The alternate of the committed root is the previous tree's root.\n // On initial mount this is null — nothing to report.\n const previousRoot = root.alternate;\n if (!previousRoot) return results;\n\n function walk(\n nextFiber: Fiber,\n previousFiber: Fiber | null,\n depth: number,\n ) {\n // ── Check this fiber ──\n if (\n COMPONENT_TAGS.has(nextFiber.tag) &&\n previousFiber !== null &&\n previousFiber !== nextFiber && // same object → bailed-out subtree\n didFiberRender(nextFiber) &&\n (!selfTriggeredOnly || isSelfTriggered(nextFiber))\n ) {\n results.push({ fiber: nextFiber, depth });\n }\n\n // ── Walk children, matching with previous tree ──\n let nextChild = nextFiber.child;\n let previousChildAtSameIndex = previousFiber?.child ?? null;\n\n while (nextChild) {\n let matchedPrevious: Fiber | null;\n\n if (previousChildAtSameIndex === nextChild) {\n // Same object identity — React shared this fiber via cloneChildFibers.\n // The entire subtree was bailed out; passing the same object as both\n // prev and next causes the prevFiber !== nextFiber guard to skip it.\n matchedPrevious = nextChild;\n } else {\n // Different object — this fiber was processed. The alternate is the\n // corresponding fiber from the previous tree.\n matchedPrevious = nextChild.alternate;\n }\n\n walk(nextChild, matchedPrevious, depth + 1);\n\n nextChild = nextChild.sibling;\n previousChildAtSameIndex = previousChildAtSameIndex?.sibling ?? null;\n }\n }\n\n walk(root, previousRoot, 0);\n return results;\n}\n","import type { Fiber, HookNode, UpdateCause } from \"./types.js\";\nimport { FiberTag } from \"./fiber.js\";\n\n// ────────────────────────────────────────────\n// Update cause detection\n// ────────────────────────────────────────────\n//\n// For function components, React stores hooks as a linked list on\n// fiber.memoizedState. In dev builds, _debugHookTypes lists hook names\n// in call order. We walk both in parallel, comparing memoizedState\n// on each node to find which hook's state actually changed.\n//\n// Caveat: useContext does NOT create a linked list node, so we skip it\n// when advancing the pointer, and detect it by elimination.\n\n/**\n * Hooks whose memoizedState changing can trigger a re-render.\n * We only report these — useEffect/useMemo/etc. don't cause re-renders.\n */\nconst STATE_HOOKS = new Set([\n \"useState\",\n \"useReducer\",\n \"useSyncExternalStore\",\n]);\n\n/**\n * Hooks that do NOT create a node in the memoizedState linked list.\n * Currently only useContext — every other hook allocates a node.\n */\nconst HOOKS_WITHOUT_NODE = new Set([\"useContext\"]);\n\nexport function detectCauses(fiber: Fiber): UpdateCause[] {\n const alternate = fiber.alternate;\n if (!alternate) return [];\n\n const causes: UpdateCause[] = [];\n\n // ── Props changed (parent re-rendered us with new props) ──\n if (fiber.memoizedProps !== alternate.memoizedProps) {\n causes.push({ kind: \"props\" });\n }\n\n // ── Class component ──\n if (fiber.tag === FiberTag.ClassComponent) {\n if (fiber.memoizedState !== alternate.memoizedState) {\n causes.push({ kind: \"class-state\" });\n }\n return causes;\n }\n\n // ── Function component hooks ──\n const hookTypes = fiber._debugHookTypes;\n\n if (!hookTypes) {\n // No debug info (prod build) — best effort\n if (fiber.memoizedState !== alternate.memoizedState) {\n causes.push({ kind: \"unknown\" });\n }\n return causes;\n }\n\n let currentNode = fiber.memoizedState as HookNode | null;\n let previousNode = alternate.memoizedState as HookNode | null;\n let hasContextHook = false;\n let anyStateHookChanged = false;\n\n for (let i = 0; i < hookTypes.length; i++) {\n const type = hookTypes[i];\n\n // useContext has no linked list node — skip pointer advance\n if (HOOKS_WITHOUT_NODE.has(type)) {\n if (type === \"useContext\") hasContextHook = true;\n continue;\n }\n\n if (STATE_HOOKS.has(type) && currentNode && previousNode) {\n if (!Object.is(currentNode.memoizedState, previousNode.memoizedState)) {\n anyStateHookChanged = true;\n causes.push({\n kind: \"hook\",\n hookIndex: i,\n hookType: type,\n previousValue: previousNode.memoizedState,\n nextValue: currentNode.memoizedState,\n });\n }\n }\n\n currentNode = currentNode?.next ?? null;\n previousNode = previousNode?.next ?? null;\n }\n\n // If no state hook changed but component is self-triggered and has\n // useContext → the context value must have changed.\n if (\n hasContextHook &&\n !anyStateHookChanged &&\n fiber.memoizedProps === alternate.memoizedProps\n ) {\n causes.push({ kind: \"hook\", hookType: \"useContext\" });\n }\n\n // If still nothing and self-triggered, mark unknown\n if (causes.length === 0 && fiber.memoizedProps === alternate.memoizedProps) {\n causes.push({ kind: \"unknown\" });\n }\n\n return causes;\n}\n","import type { PendingEntry, UpdateCause } from \"./types.js\";\n\n// ────────────────────────────────────────────\n// Value formatting\n// ────────────────────────────────────────────\n\nexport function formatValue(value: unknown, maxLength = 50): string {\n if (value === null) return \"null\";\n if (value === undefined) return \"undefined\";\n if (typeof value === \"boolean\") return String(value);\n if (typeof value === \"number\") return String(value);\n if (typeof value === \"function\")\n return `ƒ ${(value as { name?: string }).name || \"anonymous\"}`;\n\n if (typeof value === \"string\") {\n const truncated =\n value.length > maxLength ? value.slice(0, maxLength) + \"…\" : value;\n return JSON.stringify(truncated);\n }\n\n if (Array.isArray(value)) return `Array(${value.length})`;\n\n if (typeof value === \"object\") {\n const name = (value as object).constructor?.name;\n if (name && name !== \"Object\") return name;\n const keys = Object.keys(value as object);\n if (keys.length <= 3) return `{ ${keys.join(\", \")} }`;\n return `{ ${keys.slice(0, 3).join(\", \")}, … }`;\n }\n\n return String(value);\n}\n\n// ────────────────────────────────────────────\n// Cause formatting\n// ────────────────────────────────────────────\n\n/** Compact summary for overlay labels. */\nexport function formatCausesShort(causes: UpdateCause[]): string {\n if (causes.length === 0) return \"\";\n\n const parts: string[] = [];\n for (const cause of causes) {\n if (cause.kind === \"props\") {\n parts.push(\"props\");\n } else if (cause.kind === \"class-state\") {\n parts.push(\"state\");\n } else if (cause.kind === \"hook\" && cause.hookType) {\n const indexSuffix = cause.hookIndex != null ? `[${cause.hookIndex}]` : \"\";\n parts.push(`${cause.hookType}${indexSuffix}`);\n } else {\n parts.push(\"?\");\n }\n }\n\n return parts.join(\", \");\n}\n\n/** Detailed lines for console output. */\nexport function formatCausesConsole(causes: UpdateCause[]): string[] {\n const lines: string[] = [];\n\n for (const cause of causes) {\n if (cause.kind === \"props\") {\n lines.push(\" ↳ props changed (parent re-rendered)\");\n } else if (cause.kind === \"class-state\") {\n lines.push(\" ↳ class state changed\");\n } else if (cause.kind === \"hook\" && cause.hookType) {\n const indexSuffix =\n cause.hookIndex != null ? `[${cause.hookIndex}]` : \"\";\n const name = `${cause.hookType}${indexSuffix}`;\n\n if (cause.previousValue !== undefined && cause.nextValue !== undefined) {\n lines.push(\n ` ↳ ${name}: ${formatValue(cause.previousValue)} → ${formatValue(cause.nextValue)}`,\n );\n } else {\n lines.push(` ↳ ${name} changed`);\n }\n } else {\n lines.push(\" ↳ unknown cause\");\n }\n }\n\n return lines;\n}\n\n// ────────────────────────────────────────────\n// Console output\n// ────────────────────────────────────────────\n\n/** Log re-renders as a collapsed console group. */\nexport function logRerendersToConsole(\n entries: PendingEntry[],\n showCauses: boolean,\n) {\n if (entries.length === 0) return;\n\n console.groupCollapsed(\n `%c⚛ ${entries.length} re-render${entries.length > 1 ? \"s\" : \"\"}`,\n \"color: #61dafb; font-weight: bold\",\n );\n\n for (let i = 0; i < entries.length; i++) {\n const entry = entries[i];\n const durationText =\n entry.duration > 0 ? ` (${entry.duration.toFixed(2)}ms)` : \"\";\n console.log(\n `%c${entry.component}%c ${entry.path}${durationText}`,\n \"color: #e8e82e; font-weight: bold\",\n \"color: #888\",\n );\n\n if (showCauses && entry.causes.length > 0) {\n const lines = formatCausesConsole(entry.causes);\n for (const line of lines) {\n console.log(`%c${line}`, \"color: #aaa\");\n }\n }\n }\n\n console.groupEnd();\n}\n","// ────────────────────────────────────────────\n// Per-window overlay infrastructure\n// ────────────────────────────────────────────\n//\n// Performance:\n// - Overlay elements pooled per window, reused via animationend\n// - Single CSS @keyframes per window, no JS timers per element\n\nconst ANIMATION_NAME = \"__rdu-fade\";\n\nconst injectedWindows = new WeakSet<Window>();\n\nfunction ensureStylesheet(win: Window) {\n if (injectedWindows.has(win)) return;\n injectedWindows.add(win);\n\n const style = win.document.createElement(\"style\");\n style.textContent = `\n @keyframes ${ANIMATION_NAME} {\n 0% { opacity: 0; }\n 8% { opacity: var(--rdu-opacity, 1); }\n 40% { opacity: var(--rdu-opacity, 1); }\n 100% { opacity: 0; }\n }\n .${ANIMATION_NAME}-box {\n position: fixed;\n pointer-events: none;\n box-sizing: border-box;\n border-radius: 3px;\n opacity: 0;\n will-change: opacity;\n }\n .${ANIMATION_NAME}-label {\n position: absolute;\n top: -18px;\n left: -1px;\n font: 10px/16px ui-monospace, SFMono-Regular, \"SF Mono\", Menlo, Consolas, monospace;\n padding: 0 4px;\n color: #fff;\n border-radius: 2px 2px 0 0;\n white-space: nowrap;\n pointer-events: none;\n }\n `;\n win.document.head.appendChild(style);\n}\n\n// ────────────────────────────────────────────\n// Overlay root container\n// ────────────────────────────────────────────\n\nconst overlayRoots = new WeakMap<Window, HTMLDivElement>();\n\nfunction getOverlayRoot(win: Window): HTMLDivElement | null {\n if (win.closed) return null;\n\n const existing = overlayRoots.get(win);\n if (existing?.isConnected) return existing;\n\n ensureStylesheet(win);\n\n const root = win.document.createElement(\"div\");\n root.id = \"__react-debug-updates\";\n Object.assign(root.style, {\n position: \"fixed\",\n top: \"0\",\n left: \"0\",\n width: \"0\",\n height: \"0\",\n overflow: \"visible\",\n pointerEvents: \"none\",\n zIndex: \"2147483647\",\n } satisfies Partial<CSSStyleDeclaration>);\n win.document.body.appendChild(root);\n\n overlayRoots.set(win, root);\n return root;\n}\n\n// ────────────────────────────────────────────\n// Element pool\n// ────────────────────────────────────────────\n\nconst pools = new WeakMap<Window, HTMLDivElement[]>();\n\nexport function acquireOverlay(win: Window): HTMLDivElement | null {\n const root = getOverlayRoot(win);\n if (!root) return null;\n\n let pool = pools.get(win);\n if (!pool) {\n pool = [];\n pools.set(win, pool);\n }\n\n const document = win.document;\n let element = pool.pop();\n\n if (!element) {\n element = document.createElement(\"div\");\n element.className = `${ANIMATION_NAME}-box`;\n\n const label = document.createElement(\"span\");\n label.className = `${ANIMATION_NAME}-label`;\n element.appendChild(label);\n\n element.addEventListener(\"animationend\", () => {\n element!.style.animation = \"none\";\n element!.remove();\n pool!.push(element!);\n });\n }\n\n root.appendChild(element);\n return element;\n}\n\nexport function disposeAllOverlays() {\n const mainRoot = overlayRoots.get(window);\n mainRoot?.remove();\n overlayRoots.delete(window);\n pools.delete(window);\n}\n\nexport const OVERLAY_ANIMATION_NAME = ANIMATION_NAME;\n","import type { HighlightOptions, PendingEntry } from \"./types.js\";\nimport { formatCausesShort } from \"./format.js\";\nimport { acquireOverlay, OVERLAY_ANIMATION_NAME } from \"./overlay.js\";\n\n// ────────────────────────────────────────────\n// Heat color\n// ────────────────────────────────────────────\n\nfunction heatColor(count: number, alpha: number): string {\n const normalizedCount = Math.min((count - 1) / 7, 1);\n const hue = 200 - normalizedCount * 200;\n const saturation = 85 + normalizedCount * 15;\n const lightness = 55 - normalizedCount * 10;\n return `hsla(${hue}, ${saturation}%, ${lightness}%, ${alpha})`;\n}\n\n// ────────────────────────────────────────────\n// Batched flush\n// ────────────────────────────────────────────\n//\n// Commit path just pushes lightweight refs (no DOM reads, no formatting).\n// Flush (setInterval): batched rect reads → batched DOM writes.\n\nexport const HIGHLIGHT_DEFAULTS: Required<HighlightOptions> = {\n flushInterval: 250,\n animationDuration: 1200,\n showLabels: true,\n opacity: 0.3,\n};\n\ninterface CoalescedEntry {\n count: number;\n totalDuration: number;\n component: string;\n /** Shallowest fiber depth among coalesced entries (for z-ordering). */\n depth: number;\n domNode: HTMLElement;\n ownerWindow: Window;\n causeSummary: string;\n}\n\nexport function createBatcher(options: Required<HighlightOptions>) {\n let pending: PendingEntry[] = [];\n let timer: ReturnType<typeof setInterval> | null = null;\n\n function flush() {\n if (pending.length === 0) return;\n\n const batch = pending;\n pending = [];\n\n // Coalesce by DOM node identity\n const map = new Map<HTMLElement, CoalescedEntry>();\n\n for (let i = 0; i < batch.length; i++) {\n const entry = batch[i];\n if (!entry.domNode) continue;\n\n const existing = map.get(entry.domNode);\n if (existing) {\n existing.count++;\n existing.totalDuration += entry.duration;\n existing.depth = Math.min(existing.depth, entry.depth);\n } else {\n const win = entry.domNode.ownerDocument?.defaultView;\n if (!win || win.closed) continue;\n map.set(entry.domNode, {\n count: 1,\n totalDuration: entry.duration,\n component: entry.component,\n depth: entry.depth,\n domNode: entry.domNode,\n ownerWindow: win,\n causeSummary: formatCausesShort(entry.causes),\n });\n }\n }\n\n // Read phase: batch all rect reads\n const toShow: Array<{ coalesced: CoalescedEntry; rect: DOMRect }> = [];\n for (const coalesced of map.values()) {\n const rect = coalesced.domNode.getBoundingClientRect();\n if (rect.width > 0 || rect.height > 0) {\n toShow.push({ coalesced, rect });\n }\n }\n\n // Sort deepest first so parents are appended last (render on top)\n toShow.sort((a, b) => b.coalesced.depth - a.coalesced.depth);\n\n // Write phase: position overlays\n for (let i = 0; i < toShow.length; i++) {\n const { coalesced, rect } = toShow[i];\n const element = acquireOverlay(coalesced.ownerWindow);\n if (!element) continue;\n\n const fillColor = heatColor(coalesced.count, 0.18);\n const borderColor = heatColor(coalesced.count, 0.75);\n const labelBackground = heatColor(coalesced.count, 0.9);\n\n const style = element.style;\n style.top = `${rect.top}px`;\n style.left = `${rect.left}px`;\n style.width = `${rect.width}px`;\n style.height = `${rect.height}px`;\n style.backgroundColor = fillColor;\n style.border = `1.5px solid ${borderColor}`;\n style.setProperty(\"--rdu-opacity\", String(options.opacity));\n style.animation = `${OVERLAY_ANIMATION_NAME} ${options.animationDuration}ms ease-out forwards`;\n\n const label = element.firstElementChild as HTMLElement;\n if (options.showLabels) {\n const countText = coalesced.count > 1 ? ` ×${coalesced.count}` : \"\";\n const durationText =\n coalesced.totalDuration > 0\n ? ` ${coalesced.totalDuration.toFixed(1)}ms`\n : \"\";\n const causeText = coalesced.causeSummary\n ? ` (${coalesced.causeSummary})`\n : \"\";\n label.textContent = `${coalesced.component}${countText}${durationText}${causeText}`;\n label.style.backgroundColor = labelBackground;\n } else {\n label.textContent = \"\";\n label.style.backgroundColor = \"transparent\";\n }\n }\n }\n\n function push(entry: PendingEntry) {\n pending.push(entry);\n if (!timer) {\n timer = setInterval(flush, options.flushInterval);\n }\n }\n\n function dispose() {\n if (timer) {\n clearInterval(timer);\n timer = null;\n }\n pending = [];\n }\n\n return { push, dispose };\n}\n","import type {\n DevToolsHook,\n FiberRoot,\n LoggerOptions,\n PendingEntry,\n RenderEntry,\n RenderLogger,\n} from \"./types.js\";\nimport {\n detectRenders,\n findNearestDOMNode,\n getComponentName,\n getFiberPath,\n} from \"./fiber.js\";\nimport { detectCauses } from \"./causes.js\";\nimport { logRerendersToConsole } from \"./format.js\";\nimport { HIGHLIGHT_DEFAULTS, createBatcher } from \"./batcher.js\";\nimport { disposeAllOverlays } from \"./overlay.js\";\n\n/**\n * Attach a render logger to React's DevTools hook.\n *\n * Hooks into `__REACT_DEVTOOLS_GLOBAL_HOOK__.onCommitFiberRoot` to intercept\n * every React commit. Records re-render entries, optionally logs them to the\n * console, and optionally shows visual highlight overlays on re-rendered DOM nodes.\n *\n * Returns a `RenderLogger` handle, or `null` if the DevTools hook is not found.\n */\nexport function attachRenderLogger(\n options: LoggerOptions = {},\n): RenderLogger | null {\n const {\n silent = false,\n bufferSize = 500,\n filter,\n highlight = false,\n mode = \"self-triggered\",\n showCauses = false,\n } = options;\n\n const hook = (\n window as unknown as { __REACT_DEVTOOLS_GLOBAL_HOOK__?: DevToolsHook }\n ).__REACT_DEVTOOLS_GLOBAL_HOOK__;\n\n if (!hook) {\n console.warn(\n \"[react-debug-updates] __REACT_DEVTOOLS_GLOBAL_HOOK__ not found. \" +\n \"Make sure React DevTools or a dev build of React is active.\",\n );\n return null;\n }\n\n const highlightOptions = highlight\n ? {\n ...HIGHLIGHT_DEFAULTS,\n ...(typeof highlight === \"object\" ? highlight : {}),\n }\n : null;\n\n const batcher = highlightOptions ? createBatcher(highlightOptions) : null;\n\n const entries: RenderEntry[] = [];\n const previousOnCommit = hook.onCommitFiberRoot.bind(hook);\n\n hook.onCommitFiberRoot = (\n rendererID: number,\n root: FiberRoot,\n priorityLevel?: unknown,\n ) => {\n previousOnCommit(rendererID, root, priorityLevel);\n\n // 1. Detect which component fibers actually re-rendered (pure fiber analysis)\n const detectedRenders = detectRenders(root.current, mode);\n if (detectedRenders.length === 0) return;\n\n // 2. Build full entries: resolve names, DOM nodes, causes\n const pendingEntries: PendingEntry[] = [];\n\n for (let i = 0; i < detectedRenders.length; i++) {\n const { fiber, depth } = detectedRenders[i];\n const name = getComponentName(fiber);\n if (!name) continue;\n\n const pendingEntry: PendingEntry = {\n component: name,\n path: getFiberPath(fiber),\n duration: fiber.actualDuration ?? 0,\n depth,\n domNode: findNearestDOMNode(fiber),\n causes: showCauses ? detectCauses(fiber) : [],\n };\n\n const renderEntry: RenderEntry = {\n component: pendingEntry.component,\n path: pendingEntry.path,\n duration: pendingEntry.duration,\n timestamp: performance.now(),\n causes: pendingEntry.causes,\n };\n\n if (filter && !filter(renderEntry)) continue;\n\n if (entries.length >= bufferSize) entries.shift();\n entries.push(renderEntry);\n\n pendingEntries.push(pendingEntry);\n batcher?.push(pendingEntry);\n }\n\n // 3. Console output\n if (!silent) {\n logRerendersToConsole(pendingEntries, showCauses);\n }\n };\n\n const disconnect = () => {\n hook.onCommitFiberRoot = previousOnCommit;\n batcher?.dispose();\n disposeAllOverlays();\n };\n\n if (!silent) {\n console.log(\n \"%c⚛ react-debug-updates attached\",\n \"color: #61dafb; font-weight: bold\",\n );\n }\n\n return { entries, disconnect };\n}\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"react-debug-updates.cjs","sources":["../src/fiber.ts","../src/causes.ts","../src/format.ts","../src/overlay.ts","../src/highlights.ts","../src/monitor.ts"],"sourcesContent":["import type { Fiber, DetectedRender } from \"./types.js\";\n\n// ────────────────────────────────────────────\n// Fiber tag constants\n// ────────────────────────────────────────────\n\nexport const FiberTag = {\n FunctionComponent: 0,\n ClassComponent: 1,\n HostComponent: 5,\n ForwardRef: 11,\n SimpleMemoComponent: 15,\n MemoComponent: 14,\n} as const;\n\n// Component tags we report as re-renders.\n// MemoComponent (14) is excluded to avoid double-reporting — it's a wrapper\n// fiber around the inner component. The inner component (FunctionComponent,\n// ForwardRef, or SimpleMemoComponent) has its own PerformedWork flag and\n// will be reported correctly on its own.\nconst COMPONENT_TAGS = new Set<number>([\n FiberTag.FunctionComponent,\n FiberTag.ClassComponent,\n FiberTag.ForwardRef,\n FiberTag.SimpleMemoComponent,\n]);\n\nconst PerformedWork = 0b0000001;\n\n// ────────────────────────────────────────────\n// Fiber tree helpers\n// ────────────────────────────────────────────\n\nexport function getComponentName(fiber: Fiber): string | null {\n const { type } = fiber;\n if (!type || typeof type === \"string\") return null;\n return type.displayName ?? type.name ?? null;\n}\n\nfunction isHTMLElement(value: unknown): value is HTMLElement {\n return (\n typeof value === \"object\" &&\n value !== null &&\n (value as Node).nodeType === 1 &&\n typeof (value as HTMLElement).getBoundingClientRect === \"function\"\n );\n}\n\nexport function findNearestDOMNode(fiber: Fiber): HTMLElement | null {\n if (fiber.tag === FiberTag.HostComponent && isHTMLElement(fiber.stateNode)) {\n return fiber.stateNode;\n }\n let child: Fiber | null = fiber.child;\n while (child) {\n const found = findNearestDOMNode(child);\n if (found) return found;\n child = child.sibling;\n }\n return null;\n}\n\nexport function getFiberPath(fiber: Fiber, maxDepth = 5): string {\n const parts: string[] = [];\n let current: Fiber | null = fiber.return;\n let depth = 0;\n while (current && depth < maxDepth) {\n const name = getComponentName(current);\n if (name) parts.unshift(name);\n current = current.return;\n depth++;\n }\n return parts.length ? parts.join(\" → \") : \"(root)\";\n}\n\n// ────────────────────────────────────────────\n// Self-triggered detection\n// ────────────────────────────────────────────\n\nfunction isSelfTriggered(fiber: Fiber): boolean {\n const alternate = fiber.alternate;\n if (!alternate) return false;\n return fiber.memoizedProps === alternate.memoizedProps;\n}\n\n// ────────────────────────────────────────────\n// didFiberRender — mirrors React DevTools\n// ────────────────────────────────────────────\n//\n// See: react-devtools-shared/src/backend/fiber/shared/DevToolsFiberChangeDetection.js\n//\n// For component fibers (function, class, memo, forwardRef), React sets the\n// PerformedWork flag (bit 0) only when user code actually executes.\n// createWorkInProgress resets flags to NoFlags, so PerformedWork on a\n// work-in-progress fiber is always fresh — never stale from a prior commit.\n//\n// This check must only be called AFTER confirming prevFiber !== nextFiber\n// (i.e. the fiber was actually processed, not a bailed-out subtree).\n\nfunction didFiberRender(nextFiber: Fiber): boolean {\n return (nextFiber.flags & PerformedWork) === PerformedWork;\n}\n\n// ────────────────────────────────────────────\n// Detect which components re-rendered in a commit\n// ────────────────────────────────────────────\n//\n// Mirrors React DevTools' updateFiberRecursively / updateChildrenRecursively.\n//\n// React double-buffers fibers. After a commit, root.current is the committed\n// tree and root.current.alternate is the previous tree. We walk both in\n// parallel. At each level, if prevChild and nextChild are the same object,\n// React bailed out that entire subtree (via cloneChildFibers) — we skip it.\n// Otherwise, we use nextChild.alternate as the previous fiber and check\n// didFiberRender (PerformedWork) to see if user code actually ran.\n//\n// Returns lightweight DetectedRender objects — just the fiber and its depth.\n// DOM lookup, cause detection, and formatting are the caller's responsibility.\n\nexport function detectRenders(\n root: Fiber,\n mode: \"self-triggered\" | \"all\",\n): DetectedRender[] {\n const results: DetectedRender[] = [];\n const selfTriggeredOnly = mode === \"self-triggered\";\n\n // The alternate of the committed root is the previous tree's root.\n // On initial mount this is null — nothing to report.\n const previousRoot = root.alternate;\n if (!previousRoot) return results;\n\n function walk(\n nextFiber: Fiber,\n previousFiber: Fiber | null,\n depth: number,\n ) {\n // ── Check this fiber ──\n if (\n COMPONENT_TAGS.has(nextFiber.tag) &&\n previousFiber !== null &&\n previousFiber !== nextFiber && // same object → bailed-out subtree\n didFiberRender(nextFiber) &&\n (!selfTriggeredOnly || isSelfTriggered(nextFiber))\n ) {\n results.push({ fiber: nextFiber, depth });\n }\n\n // ── Walk children, matching with previous tree ──\n let nextChild = nextFiber.child;\n let previousChildAtSameIndex = previousFiber?.child ?? null;\n\n while (nextChild) {\n let matchedPrevious: Fiber | null;\n\n if (previousChildAtSameIndex === nextChild) {\n // Same object identity — React shared this fiber via cloneChildFibers.\n // The entire subtree was bailed out; passing the same object as both\n // prev and next causes the prevFiber !== nextFiber guard to skip it.\n matchedPrevious = nextChild;\n } else {\n // Different object — this fiber was processed. The alternate is the\n // corresponding fiber from the previous tree.\n matchedPrevious = nextChild.alternate;\n }\n\n walk(nextChild, matchedPrevious, depth + 1);\n\n nextChild = nextChild.sibling;\n previousChildAtSameIndex = previousChildAtSameIndex?.sibling ?? null;\n }\n }\n\n walk(root, previousRoot, 0);\n return results;\n}\n","import type { Fiber, HookNode, UpdateCause } from \"./types.js\";\nimport { FiberTag } from \"./fiber.js\";\n\n// ────────────────────────────────────────────\n// Update cause detection\n// ────────────────────────────────────────────\n//\n// For function components, React stores hooks as a linked list on\n// fiber.memoizedState. In dev builds, _debugHookTypes lists hook names\n// in call order. We walk both in parallel, comparing memoizedState\n// on each node to find which hook's state actually changed.\n//\n// Caveat: useContext does NOT create a linked list node, so we skip it\n// when advancing the pointer, and detect it by elimination.\n\n/**\n * Hooks whose memoizedState changing can trigger a re-render.\n * We only report these — useEffect/useMemo/etc. don't cause re-renders.\n */\nconst STATE_HOOKS = new Set([\n \"useState\",\n \"useReducer\",\n \"useSyncExternalStore\",\n]);\n\n/**\n * Hooks that do NOT create a node in the memoizedState linked list.\n * Currently only useContext — every other hook allocates a node.\n */\nconst HOOKS_WITHOUT_NODE = new Set([\"useContext\"]);\n\nexport function detectCauses(fiber: Fiber): UpdateCause[] {\n const alternate = fiber.alternate;\n if (!alternate) return [];\n\n const causes: UpdateCause[] = [];\n\n // ── Props changed (parent re-rendered us with new props) ──\n if (fiber.memoizedProps !== alternate.memoizedProps) {\n causes.push({ kind: \"props\" });\n }\n\n // ── Class component ──\n if (fiber.tag === FiberTag.ClassComponent) {\n if (fiber.memoizedState !== alternate.memoizedState) {\n causes.push({ kind: \"class-state\" });\n }\n return causes;\n }\n\n // ── Function component hooks ──\n const hookTypes = fiber._debugHookTypes;\n\n if (!hookTypes) {\n // No debug info (prod build) — best effort\n if (fiber.memoizedState !== alternate.memoizedState) {\n causes.push({ kind: \"unknown\" });\n }\n return causes;\n }\n\n let currentNode = fiber.memoizedState as HookNode | null;\n let previousNode = alternate.memoizedState as HookNode | null;\n let hasContextHook = false;\n let anyStateHookChanged = false;\n\n for (let i = 0; i < hookTypes.length; i++) {\n const type = hookTypes[i];\n\n // useContext has no linked list node — skip pointer advance\n if (HOOKS_WITHOUT_NODE.has(type)) {\n if (type === \"useContext\") hasContextHook = true;\n continue;\n }\n\n if (STATE_HOOKS.has(type) && currentNode && previousNode) {\n if (!Object.is(currentNode.memoizedState, previousNode.memoizedState)) {\n anyStateHookChanged = true;\n causes.push({\n kind: \"hook\",\n hookIndex: i,\n hookType: type,\n previousValue: previousNode.memoizedState,\n nextValue: currentNode.memoizedState,\n });\n }\n }\n\n currentNode = currentNode?.next ?? null;\n previousNode = previousNode?.next ?? null;\n }\n\n // If no state hook changed but component is self-triggered and has\n // useContext → the context value must have changed.\n if (\n hasContextHook &&\n !anyStateHookChanged &&\n fiber.memoizedProps === alternate.memoizedProps\n ) {\n causes.push({ kind: \"hook\", hookType: \"useContext\" });\n }\n\n // If still nothing and self-triggered, mark unknown\n if (causes.length === 0 && fiber.memoizedProps === alternate.memoizedProps) {\n causes.push({ kind: \"unknown\" });\n }\n\n return causes;\n}\n","import type { HighlightEntry, UpdateCause } from \"./types.js\";\n\n// ────────────────────────────────────────────\n// Value formatting\n// ────────────────────────────────────────────\n\nexport function formatValue(value: unknown, maxLength = 50): string {\n if (value === null) return \"null\";\n if (value === undefined) return \"undefined\";\n if (typeof value === \"boolean\") return String(value);\n if (typeof value === \"number\") return String(value);\n if (typeof value === \"function\")\n return `ƒ ${(value as { name?: string }).name || \"anonymous\"}`;\n\n if (typeof value === \"string\") {\n const truncated =\n value.length > maxLength ? value.slice(0, maxLength) + \"…\" : value;\n return JSON.stringify(truncated);\n }\n\n if (Array.isArray(value)) return `Array(${value.length})`;\n\n if (typeof value === \"object\") {\n const name = (value as object).constructor?.name;\n if (name && name !== \"Object\") return name;\n const keys = Object.keys(value as object);\n if (keys.length <= 3) return `{ ${keys.join(\", \")} }`;\n return `{ ${keys.slice(0, 3).join(\", \")}, … }`;\n }\n\n return String(value);\n}\n\n// ────────────────────────────────────────────\n// Cause formatting\n// ────────────────────────────────────────────\n\n/** Compact summary for overlay labels. */\nexport function formatCausesShort(causes: UpdateCause[]): string {\n if (causes.length === 0) return \"\";\n\n const parts: string[] = [];\n for (const cause of causes) {\n if (cause.kind === \"props\") {\n parts.push(\"props\");\n } else if (cause.kind === \"class-state\") {\n parts.push(\"state\");\n } else if (cause.kind === \"hook\" && cause.hookType) {\n const indexSuffix = cause.hookIndex != null ? `[${cause.hookIndex}]` : \"\";\n parts.push(`${cause.hookType}${indexSuffix}`);\n } else {\n parts.push(\"?\");\n }\n }\n\n return parts.join(\", \");\n}\n\n/** Detailed lines for console output. */\nexport function formatCausesConsole(causes: UpdateCause[]): string[] {\n const lines: string[] = [];\n\n for (const cause of causes) {\n if (cause.kind === \"props\") {\n lines.push(\" ↳ props changed (parent re-rendered)\");\n } else if (cause.kind === \"class-state\") {\n lines.push(\" ↳ class state changed\");\n } else if (cause.kind === \"hook\" && cause.hookType) {\n const indexSuffix =\n cause.hookIndex != null ? `[${cause.hookIndex}]` : \"\";\n const name = `${cause.hookType}${indexSuffix}`;\n\n if (cause.previousValue !== undefined && cause.nextValue !== undefined) {\n lines.push(\n ` ↳ ${name}: ${formatValue(cause.previousValue)} → ${formatValue(cause.nextValue)}`,\n );\n } else {\n lines.push(` ↳ ${name} changed`);\n }\n } else {\n lines.push(\" ↳ unknown cause\");\n }\n }\n\n return lines;\n}\n\n// ────────────────────────────────────────────\n// Console output\n// ────────────────────────────────────────────\n\n/** Log re-renders as a collapsed console group. */\nexport function logRerendersToConsole(\n entries: HighlightEntry[],\n showCauses: boolean,\n) {\n if (entries.length === 0) return;\n\n console.groupCollapsed(\n `%c⚛ ${entries.length} re-render${entries.length > 1 ? \"s\" : \"\"}`,\n \"color: #61dafb; font-weight: bold\",\n );\n\n for (let i = 0; i < entries.length; i++) {\n const entry = entries[i];\n const durationText =\n entry.duration > 0 ? ` (${entry.duration.toFixed(2)}ms)` : \"\";\n console.log(\n `%c${entry.component}%c ${entry.path}${durationText}`,\n \"color: #e8e82e; font-weight: bold\",\n \"color: #888\",\n );\n\n if (showCauses && entry.causes.length > 0) {\n const lines = formatCausesConsole(entry.causes);\n for (const line of lines) {\n console.log(`%c${line}`, \"color: #aaa\");\n }\n }\n }\n\n console.groupEnd();\n}\n","// ────────────────────────────────────────────\n// Per-window overlay infrastructure\n// ────────────────────────────────────────────\n//\n// Performance:\n// - Overlay elements pooled per window, reused via animationend\n// - Single CSS @keyframes per window, no JS timers per element\n\nconst ANIMATION_NAME = \"__rdu-fade\";\n\nconst injectedWindows = new WeakSet<Window>();\n\nfunction ensureStylesheet(win: Window) {\n if (injectedWindows.has(win)) return;\n injectedWindows.add(win);\n\n const style = win.document.createElement(\"style\");\n style.textContent = `\n @keyframes ${ANIMATION_NAME} {\n 0% { opacity: 0; }\n 8% { opacity: var(--rdu-opacity, 1); }\n 40% { opacity: var(--rdu-opacity, 1); }\n 100% { opacity: 0; }\n }\n .${ANIMATION_NAME}-box {\n position: fixed;\n pointer-events: none;\n box-sizing: border-box;\n border-radius: 3px;\n opacity: 0;\n will-change: opacity;\n }\n .${ANIMATION_NAME}-label {\n position: absolute;\n top: -18px;\n left: -1px;\n font: 10px/16px ui-monospace, SFMono-Regular, \"SF Mono\", Menlo, Consolas, monospace;\n padding: 0 4px;\n color: #fff;\n border-radius: 2px 2px 0 0;\n white-space: nowrap;\n pointer-events: none;\n }\n `;\n win.document.head.appendChild(style);\n}\n\n// ────────────────────────────────────────────\n// Overlay root container\n// ────────────────────────────────────────────\n\nconst overlayRoots = new WeakMap<Window, HTMLDivElement>();\n\nfunction getOverlayRoot(win: Window): HTMLDivElement | null {\n if (win.closed) return null;\n\n const existing = overlayRoots.get(win);\n if (existing?.isConnected) return existing;\n\n ensureStylesheet(win);\n\n const root = win.document.createElement(\"div\");\n root.id = \"__react-debug-updates\";\n Object.assign(root.style, {\n position: \"fixed\",\n top: \"0\",\n left: \"0\",\n width: \"0\",\n height: \"0\",\n overflow: \"visible\",\n pointerEvents: \"none\",\n zIndex: \"2147483647\",\n } satisfies Partial<CSSStyleDeclaration>);\n win.document.body.appendChild(root);\n\n overlayRoots.set(win, root);\n return root;\n}\n\n// ────────────────────────────────────────────\n// Element pool\n// ────────────────────────────────────────────\n\nconst pools = new WeakMap<Window, HTMLDivElement[]>();\n\nexport function acquireOverlay(win: Window): HTMLDivElement | null {\n const root = getOverlayRoot(win);\n if (!root) return null;\n\n let pool = pools.get(win);\n if (!pool) {\n pool = [];\n pools.set(win, pool);\n }\n\n const document = win.document;\n let element = pool.pop();\n\n if (!element) {\n element = document.createElement(\"div\");\n element.className = `${ANIMATION_NAME}-box`;\n\n const label = document.createElement(\"span\");\n label.className = `${ANIMATION_NAME}-label`;\n element.appendChild(label);\n\n element.addEventListener(\"animationend\", () => {\n element!.style.animation = \"none\";\n element!.remove();\n pool!.push(element!);\n });\n }\n\n root.appendChild(element);\n return element;\n}\n\nexport function disposeAllOverlays() {\n const mainRoot = overlayRoots.get(window);\n mainRoot?.remove();\n overlayRoots.delete(window);\n pools.delete(window);\n}\n\nexport const OVERLAY_ANIMATION_NAME = ANIMATION_NAME;\n","import type { OverlayConfig, HighlightEntry } from \"./types.js\";\nimport { formatCausesShort } from \"./format.js\";\nimport { acquireOverlay, OVERLAY_ANIMATION_NAME } from \"./overlay.js\";\n\n// ────────────────────────────────────────────\n// Heat color\n// ────────────────────────────────────────────\n\nfunction heatColor(count: number, alpha: number): string {\n const normalizedCount = Math.min((count - 1) / 7, 1);\n const hue = 200 - normalizedCount * 200;\n const saturation = 85 + normalizedCount * 15;\n const lightness = 55 - normalizedCount * 10;\n return `hsla(${hue}, ${saturation}%, ${lightness}%, ${alpha})`;\n}\n\n// ────────────────────────────────────────────\n// Highlight scheduler\n// ────────────────────────────────────────────\n//\n// Commit path just pushes lightweight entries (no DOM reads, no formatting).\n// Flush (setInterval): batched rect reads → batched DOM writes.\n\ninterface CoalescedEntry {\n count: number;\n totalDuration: number;\n component: string;\n /** Shallowest fiber depth among coalesced entries (for z-ordering). */\n depth: number;\n domNode: HTMLElement;\n ownerWindow: Window;\n causeSummary: string;\n}\n\nexport function createHighlighter(config: OverlayConfig) {\n let pending: HighlightEntry[] = [];\n let timer: ReturnType<typeof setInterval> | null = null;\n\n function flush() {\n if (pending.length === 0) return;\n\n const batch = pending;\n pending = [];\n\n // Coalesce by DOM node identity\n const map = new Map<HTMLElement, CoalescedEntry>();\n\n for (let i = 0; i < batch.length; i++) {\n const entry = batch[i];\n if (!entry.domNode) continue;\n\n const existing = map.get(entry.domNode);\n if (existing) {\n existing.count++;\n existing.totalDuration += entry.duration;\n existing.depth = Math.min(existing.depth, entry.depth);\n } else {\n const win = entry.domNode.ownerDocument?.defaultView;\n if (!win || win.closed) continue;\n map.set(entry.domNode, {\n count: 1,\n totalDuration: entry.duration,\n component: entry.component,\n depth: entry.depth,\n domNode: entry.domNode,\n ownerWindow: win,\n causeSummary: formatCausesShort(entry.causes),\n });\n }\n }\n\n // Read phase: batch all rect reads\n const toShow: Array<{ coalesced: CoalescedEntry; rect: DOMRect }> = [];\n for (const coalesced of map.values()) {\n const rect = coalesced.domNode.getBoundingClientRect();\n if (rect.width > 0 || rect.height > 0) {\n toShow.push({ coalesced, rect });\n }\n }\n\n // Sort deepest first so parents are appended last (render on top)\n toShow.sort((a, b) => b.coalesced.depth - a.coalesced.depth);\n\n // Write phase: position overlays\n for (let i = 0; i < toShow.length; i++) {\n const { coalesced, rect } = toShow[i];\n const element = acquireOverlay(coalesced.ownerWindow);\n if (!element) continue;\n\n const fillColor = heatColor(coalesced.count, 0.18);\n const borderColor = heatColor(coalesced.count, 0.75);\n const labelBackground = heatColor(coalesced.count, 0.9);\n\n const style = element.style;\n style.top = `${rect.top}px`;\n style.left = `${rect.left}px`;\n style.width = `${rect.width}px`;\n style.height = `${rect.height}px`;\n style.backgroundColor = fillColor;\n style.border = `1.5px solid ${borderColor}`;\n style.setProperty(\"--rdu-opacity\", String(config.opacity));\n style.animation = `${OVERLAY_ANIMATION_NAME} ${config.animationDuration}ms ease-out forwards`;\n\n const label = element.firstElementChild as HTMLElement;\n if (config.showLabels) {\n const countText = coalesced.count > 1 ? ` ×${coalesced.count}` : \"\";\n const durationText =\n coalesced.totalDuration > 0\n ? ` ${coalesced.totalDuration.toFixed(1)}ms`\n : \"\";\n const causeText = coalesced.causeSummary\n ? ` (${coalesced.causeSummary})`\n : \"\";\n label.textContent = `${coalesced.component}${countText}${durationText}${causeText}`;\n label.style.backgroundColor = labelBackground;\n } else {\n label.textContent = \"\";\n label.style.backgroundColor = \"transparent\";\n }\n }\n }\n\n function push(entry: HighlightEntry) {\n pending.push(entry);\n if (!timer) {\n timer = setInterval(flush, config.flushInterval);\n }\n }\n\n function dispose() {\n if (timer) {\n clearInterval(timer);\n timer = null;\n }\n pending = [];\n }\n\n return { push, dispose };\n}\n","import type {\n DevToolsHook,\n FiberRoot,\n HighlightEntry,\n MonitorOptions,\n} from \"./types.js\";\nimport {\n detectRenders,\n findNearestDOMNode,\n getComponentName,\n getFiberPath,\n} from \"./fiber.js\";\nimport { detectCauses } from \"./causes.js\";\nimport { logRerendersToConsole } from \"./format.js\";\nimport { createHighlighter } from \"./highlights.js\";\nimport { disposeAllOverlays } from \"./overlay.js\";\n\n/**\n * Ensure __REACT_DEVTOOLS_GLOBAL_HOOK__ exists on window.\n *\n * React reads this hook during initialization — if it's missing, React will\n * never connect. When this library is imported before React (the recommended\n * setup), we need to create a minimal hook so React can find and register with it.\n *\n * If a hook already exists (e.g. from the React DevTools extension), we leave\n * it untouched and hook into it.\n */\nfunction ensureDevToolsHook(win: Window): DevToolsHook {\n const global = win as unknown as {\n __REACT_DEVTOOLS_GLOBAL_HOOK__?: DevToolsHook;\n };\n\n if (!global.__REACT_DEVTOOLS_GLOBAL_HOOK__) {\n global.__REACT_DEVTOOLS_GLOBAL_HOOK__ = {\n supportsFiber: true,\n inject() {\n return 1;\n },\n onCommitFiberRoot() {},\n onCommitFiberUnmount() {},\n onPostCommitFiberRoot() {},\n checkDCE() {},\n } as DevToolsHook;\n }\n\n return global.__REACT_DEVTOOLS_GLOBAL_HOOK__;\n}\n\n/**\n * Start monitoring React re-renders.\n *\n * Hooks into `__REACT_DEVTOOLS_GLOBAL_HOOK__.onCommitFiberRoot` to intercept\n * every React commit. Shows visual highlight overlays on re-rendered DOM nodes\n * and optionally logs re-renders to the console.\n *\n * Call this **before** React renders anything — ideally at the very top of\n * your entry point.\n *\n * Returns a `stop` function to unhook and clean up, or `null` if called\n * in a non-browser environment (e.g. SSR).\n */\nexport function startReactUpdatesMonitor({\n logToConsole = false,\n highlight = true,\n mode = \"self-triggered\",\n reasonOfUpdate = false,\n highlightFlushInterval = 250,\n highlightAnimationDuration = 1200,\n highlightShowLabels = true,\n highlightOpacity = 0.3,\n}: MonitorOptions = {}): (() => void) | null {\n // SSR guard — nothing to do without a DOM\n if (typeof window === \"undefined\") return null;\n\n const hook = ensureDevToolsHook(window);\n\n const highlighter = highlight\n ? createHighlighter({\n flushInterval: highlightFlushInterval,\n animationDuration: highlightAnimationDuration,\n showLabels: highlightShowLabels,\n opacity: highlightOpacity,\n })\n : null;\n\n const previousOnCommit = hook.onCommitFiberRoot.bind(hook);\n\n hook.onCommitFiberRoot = (\n rendererID: number,\n root: FiberRoot,\n priorityLevel?: unknown,\n ) => {\n previousOnCommit(rendererID, root, priorityLevel);\n\n // 1. Detect which component fibers actually re-rendered (pure fiber analysis)\n const detectedRenders = detectRenders(root.current, mode);\n if (detectedRenders.length === 0) return;\n\n // 2. Build full entries: resolve names, DOM nodes, causes\n const highlightEntries: HighlightEntry[] = [];\n\n for (let i = 0; i < detectedRenders.length; i++) {\n const { fiber, depth } = detectedRenders[i];\n const name = getComponentName(fiber);\n if (!name) continue;\n\n const entry: HighlightEntry = {\n component: name,\n path: getFiberPath(fiber),\n duration: fiber.actualDuration ?? 0,\n depth,\n domNode: findNearestDOMNode(fiber),\n causes: reasonOfUpdate ? detectCauses(fiber) : [],\n };\n\n highlightEntries.push(entry);\n highlighter?.push(entry);\n }\n\n // 3. Console output\n if (logToConsole) {\n logRerendersToConsole(highlightEntries, reasonOfUpdate);\n }\n };\n\n if (logToConsole) {\n console.log(\n \"%c⚛ react-debug-updates attached\",\n \"color: #61dafb; font-weight: bold\",\n );\n }\n\n return () => {\n hook.onCommitFiberRoot = previousOnCommit;\n highlighter?.dispose();\n disposeAllOverlays();\n };\n}\n"],"names":[],"mappings":"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"}
@@ -320,13 +320,7 @@ function heatColor(count, alpha) {
320
320
  const lightness = 55 - normalizedCount * 10;
321
321
  return `hsla(${hue}, ${saturation}%, ${lightness}%, ${alpha})`;
322
322
  }
323
- const HIGHLIGHT_DEFAULTS = {
324
- flushInterval: 250,
325
- animationDuration: 1200,
326
- showLabels: true,
327
- opacity: 0.3
328
- };
329
- function createBatcher(options) {
323
+ function createHighlighter(config) {
330
324
  let pending = [];
331
325
  let timer = null;
332
326
  function flush() {
@@ -379,10 +373,10 @@ function createBatcher(options) {
379
373
  style.height = `${rect.height}px`;
380
374
  style.backgroundColor = fillColor;
381
375
  style.border = `1.5px solid ${borderColor}`;
382
- style.setProperty("--rdu-opacity", String(options.opacity));
383
- style.animation = `${OVERLAY_ANIMATION_NAME} ${options.animationDuration}ms ease-out forwards`;
376
+ style.setProperty("--rdu-opacity", String(config.opacity));
377
+ style.animation = `${OVERLAY_ANIMATION_NAME} ${config.animationDuration}ms ease-out forwards`;
384
378
  const label = element.firstElementChild;
385
- if (options.showLabels) {
379
+ if (config.showLabels) {
386
380
  const countText = coalesced.count > 1 ? ` ×${coalesced.count}` : "";
387
381
  const durationText = coalesced.totalDuration > 0 ? ` ${coalesced.totalDuration.toFixed(1)}ms` : "";
388
382
  const causeText = coalesced.causeSummary ? ` (${coalesced.causeSummary})` : "";
@@ -397,7 +391,7 @@ function createBatcher(options) {
397
391
  function push(entry) {
398
392
  pending.push(entry);
399
393
  if (!timer) {
400
- timer = setInterval(flush, options.flushInterval);
394
+ timer = setInterval(flush, config.flushInterval);
401
395
  }
402
396
  }
403
397
  function dispose() {
@@ -409,77 +403,82 @@ function createBatcher(options) {
409
403
  }
410
404
  return { push, dispose };
411
405
  }
412
- function attachRenderLogger(options = {}) {
413
- const {
414
- silent = false,
415
- bufferSize = 500,
416
- filter,
417
- highlight = false,
418
- mode = "self-triggered",
419
- showCauses = false
420
- } = options;
421
- const hook = window.__REACT_DEVTOOLS_GLOBAL_HOOK__;
422
- if (!hook) {
423
- console.warn(
424
- "[react-debug-updates] __REACT_DEVTOOLS_GLOBAL_HOOK__ not found. Make sure React DevTools or a dev build of React is active."
425
- );
426
- return null;
406
+ function ensureDevToolsHook(win) {
407
+ const global = win;
408
+ if (!global.__REACT_DEVTOOLS_GLOBAL_HOOK__) {
409
+ global.__REACT_DEVTOOLS_GLOBAL_HOOK__ = {
410
+ supportsFiber: true,
411
+ inject() {
412
+ return 1;
413
+ },
414
+ onCommitFiberRoot() {
415
+ },
416
+ onCommitFiberUnmount() {
417
+ },
418
+ onPostCommitFiberRoot() {
419
+ },
420
+ checkDCE() {
421
+ }
422
+ };
427
423
  }
428
- const highlightOptions = highlight ? {
429
- ...HIGHLIGHT_DEFAULTS,
430
- ...typeof highlight === "object" ? highlight : {}
431
- } : null;
432
- const batcher = highlightOptions ? createBatcher(highlightOptions) : null;
433
- const entries = [];
424
+ return global.__REACT_DEVTOOLS_GLOBAL_HOOK__;
425
+ }
426
+ function startReactUpdatesMonitor({
427
+ logToConsole = false,
428
+ highlight = true,
429
+ mode = "self-triggered",
430
+ reasonOfUpdate = false,
431
+ highlightFlushInterval = 250,
432
+ highlightAnimationDuration = 1200,
433
+ highlightShowLabels = true,
434
+ highlightOpacity = 0.3
435
+ } = {}) {
436
+ if (typeof window === "undefined") return null;
437
+ const hook = ensureDevToolsHook(window);
438
+ const highlighter = highlight ? createHighlighter({
439
+ flushInterval: highlightFlushInterval,
440
+ animationDuration: highlightAnimationDuration,
441
+ showLabels: highlightShowLabels,
442
+ opacity: highlightOpacity
443
+ }) : null;
434
444
  const previousOnCommit = hook.onCommitFiberRoot.bind(hook);
435
445
  hook.onCommitFiberRoot = (rendererID, root, priorityLevel) => {
436
446
  previousOnCommit(rendererID, root, priorityLevel);
437
447
  const detectedRenders = detectRenders(root.current, mode);
438
448
  if (detectedRenders.length === 0) return;
439
- const pendingEntries = [];
449
+ const highlightEntries = [];
440
450
  for (let i = 0; i < detectedRenders.length; i++) {
441
451
  const { fiber, depth } = detectedRenders[i];
442
452
  const name = getComponentName(fiber);
443
453
  if (!name) continue;
444
- const pendingEntry = {
454
+ const entry = {
445
455
  component: name,
446
456
  path: getFiberPath(fiber),
447
457
  duration: fiber.actualDuration ?? 0,
448
458
  depth,
449
459
  domNode: findNearestDOMNode(fiber),
450
- causes: showCauses ? detectCauses(fiber) : []
451
- };
452
- const renderEntry = {
453
- component: pendingEntry.component,
454
- path: pendingEntry.path,
455
- duration: pendingEntry.duration,
456
- timestamp: performance.now(),
457
- causes: pendingEntry.causes
460
+ causes: reasonOfUpdate ? detectCauses(fiber) : []
458
461
  };
459
- if (filter && !filter(renderEntry)) continue;
460
- if (entries.length >= bufferSize) entries.shift();
461
- entries.push(renderEntry);
462
- pendingEntries.push(pendingEntry);
463
- batcher == null ? void 0 : batcher.push(pendingEntry);
462
+ highlightEntries.push(entry);
463
+ highlighter == null ? void 0 : highlighter.push(entry);
464
464
  }
465
- if (!silent) {
466
- logRerendersToConsole(pendingEntries, showCauses);
465
+ if (logToConsole) {
466
+ logRerendersToConsole(highlightEntries, reasonOfUpdate);
467
467
  }
468
468
  };
469
- const disconnect = () => {
470
- hook.onCommitFiberRoot = previousOnCommit;
471
- batcher == null ? void 0 : batcher.dispose();
472
- disposeAllOverlays();
473
- };
474
- if (!silent) {
469
+ if (logToConsole) {
475
470
  console.log(
476
471
  "%c⚛ react-debug-updates attached",
477
472
  "color: #61dafb; font-weight: bold"
478
473
  );
479
474
  }
480
- return { entries, disconnect };
475
+ return () => {
476
+ hook.onCommitFiberRoot = previousOnCommit;
477
+ highlighter == null ? void 0 : highlighter.dispose();
478
+ disposeAllOverlays();
479
+ };
481
480
  }
482
481
  export {
483
- attachRenderLogger
482
+ startReactUpdatesMonitor
484
483
  };
485
484
  //# sourceMappingURL=react-debug-updates.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"react-debug-updates.js","sources":["../src/fiber.ts","../src/causes.ts","../src/format.ts","../src/overlay.ts","../src/batcher.ts","../src/logger.ts"],"sourcesContent":["import type { Fiber, DetectedRender } from \"./types.js\";\n\n// ────────────────────────────────────────────\n// Fiber tag constants\n// ────────────────────────────────────────────\n\nexport const FiberTag = {\n FunctionComponent: 0,\n ClassComponent: 1,\n HostComponent: 5,\n ForwardRef: 11,\n SimpleMemoComponent: 15,\n MemoComponent: 14,\n} as const;\n\n// Component tags we report as re-renders.\n// MemoComponent (14) is excluded to avoid double-reporting — it's a wrapper\n// fiber around the inner component. The inner component (FunctionComponent,\n// ForwardRef, or SimpleMemoComponent) has its own PerformedWork flag and\n// will be reported correctly on its own.\nconst COMPONENT_TAGS = new Set<number>([\n FiberTag.FunctionComponent,\n FiberTag.ClassComponent,\n FiberTag.ForwardRef,\n FiberTag.SimpleMemoComponent,\n]);\n\nconst PerformedWork = 0b0000001;\n\n// ────────────────────────────────────────────\n// Fiber tree helpers\n// ────────────────────────────────────────────\n\nexport function getComponentName(fiber: Fiber): string | null {\n const { type } = fiber;\n if (!type || typeof type === \"string\") return null;\n return type.displayName ?? type.name ?? null;\n}\n\nfunction isHTMLElement(value: unknown): value is HTMLElement {\n return (\n typeof value === \"object\" &&\n value !== null &&\n (value as Node).nodeType === 1 &&\n typeof (value as HTMLElement).getBoundingClientRect === \"function\"\n );\n}\n\nexport function findNearestDOMNode(fiber: Fiber): HTMLElement | null {\n if (fiber.tag === FiberTag.HostComponent && isHTMLElement(fiber.stateNode)) {\n return fiber.stateNode;\n }\n let child: Fiber | null = fiber.child;\n while (child) {\n const found = findNearestDOMNode(child);\n if (found) return found;\n child = child.sibling;\n }\n return null;\n}\n\nexport function getFiberPath(fiber: Fiber, maxDepth = 5): string {\n const parts: string[] = [];\n let current: Fiber | null = fiber.return;\n let depth = 0;\n while (current && depth < maxDepth) {\n const name = getComponentName(current);\n if (name) parts.unshift(name);\n current = current.return;\n depth++;\n }\n return parts.length ? parts.join(\" → \") : \"(root)\";\n}\n\n// ────────────────────────────────────────────\n// Self-triggered detection\n// ────────────────────────────────────────────\n\nfunction isSelfTriggered(fiber: Fiber): boolean {\n const alternate = fiber.alternate;\n if (!alternate) return false;\n return fiber.memoizedProps === alternate.memoizedProps;\n}\n\n// ────────────────────────────────────────────\n// didFiberRender — mirrors React DevTools\n// ────────────────────────────────────────────\n//\n// See: react-devtools-shared/src/backend/fiber/shared/DevToolsFiberChangeDetection.js\n//\n// For component fibers (function, class, memo, forwardRef), React sets the\n// PerformedWork flag (bit 0) only when user code actually executes.\n// createWorkInProgress resets flags to NoFlags, so PerformedWork on a\n// work-in-progress fiber is always fresh — never stale from a prior commit.\n//\n// This check must only be called AFTER confirming prevFiber !== nextFiber\n// (i.e. the fiber was actually processed, not a bailed-out subtree).\n\nfunction didFiberRender(nextFiber: Fiber): boolean {\n return (nextFiber.flags & PerformedWork) === PerformedWork;\n}\n\n// ────────────────────────────────────────────\n// Detect which components re-rendered in a commit\n// ────────────────────────────────────────────\n//\n// Mirrors React DevTools' updateFiberRecursively / updateChildrenRecursively.\n//\n// React double-buffers fibers. After a commit, root.current is the committed\n// tree and root.current.alternate is the previous tree. We walk both in\n// parallel. At each level, if prevChild and nextChild are the same object,\n// React bailed out that entire subtree (via cloneChildFibers) — we skip it.\n// Otherwise, we use nextChild.alternate as the previous fiber and check\n// didFiberRender (PerformedWork) to see if user code actually ran.\n//\n// Returns lightweight DetectedRender objects — just the fiber and its depth.\n// DOM lookup, cause detection, and formatting are the caller's responsibility.\n\nexport function detectRenders(\n root: Fiber,\n mode: \"self-triggered\" | \"all\",\n): DetectedRender[] {\n const results: DetectedRender[] = [];\n const selfTriggeredOnly = mode === \"self-triggered\";\n\n // The alternate of the committed root is the previous tree's root.\n // On initial mount this is null — nothing to report.\n const previousRoot = root.alternate;\n if (!previousRoot) return results;\n\n function walk(\n nextFiber: Fiber,\n previousFiber: Fiber | null,\n depth: number,\n ) {\n // ── Check this fiber ──\n if (\n COMPONENT_TAGS.has(nextFiber.tag) &&\n previousFiber !== null &&\n previousFiber !== nextFiber && // same object → bailed-out subtree\n didFiberRender(nextFiber) &&\n (!selfTriggeredOnly || isSelfTriggered(nextFiber))\n ) {\n results.push({ fiber: nextFiber, depth });\n }\n\n // ── Walk children, matching with previous tree ──\n let nextChild = nextFiber.child;\n let previousChildAtSameIndex = previousFiber?.child ?? null;\n\n while (nextChild) {\n let matchedPrevious: Fiber | null;\n\n if (previousChildAtSameIndex === nextChild) {\n // Same object identity — React shared this fiber via cloneChildFibers.\n // The entire subtree was bailed out; passing the same object as both\n // prev and next causes the prevFiber !== nextFiber guard to skip it.\n matchedPrevious = nextChild;\n } else {\n // Different object — this fiber was processed. The alternate is the\n // corresponding fiber from the previous tree.\n matchedPrevious = nextChild.alternate;\n }\n\n walk(nextChild, matchedPrevious, depth + 1);\n\n nextChild = nextChild.sibling;\n previousChildAtSameIndex = previousChildAtSameIndex?.sibling ?? null;\n }\n }\n\n walk(root, previousRoot, 0);\n return results;\n}\n","import type { Fiber, HookNode, UpdateCause } from \"./types.js\";\nimport { FiberTag } from \"./fiber.js\";\n\n// ────────────────────────────────────────────\n// Update cause detection\n// ────────────────────────────────────────────\n//\n// For function components, React stores hooks as a linked list on\n// fiber.memoizedState. In dev builds, _debugHookTypes lists hook names\n// in call order. We walk both in parallel, comparing memoizedState\n// on each node to find which hook's state actually changed.\n//\n// Caveat: useContext does NOT create a linked list node, so we skip it\n// when advancing the pointer, and detect it by elimination.\n\n/**\n * Hooks whose memoizedState changing can trigger a re-render.\n * We only report these — useEffect/useMemo/etc. don't cause re-renders.\n */\nconst STATE_HOOKS = new Set([\n \"useState\",\n \"useReducer\",\n \"useSyncExternalStore\",\n]);\n\n/**\n * Hooks that do NOT create a node in the memoizedState linked list.\n * Currently only useContext — every other hook allocates a node.\n */\nconst HOOKS_WITHOUT_NODE = new Set([\"useContext\"]);\n\nexport function detectCauses(fiber: Fiber): UpdateCause[] {\n const alternate = fiber.alternate;\n if (!alternate) return [];\n\n const causes: UpdateCause[] = [];\n\n // ── Props changed (parent re-rendered us with new props) ──\n if (fiber.memoizedProps !== alternate.memoizedProps) {\n causes.push({ kind: \"props\" });\n }\n\n // ── Class component ──\n if (fiber.tag === FiberTag.ClassComponent) {\n if (fiber.memoizedState !== alternate.memoizedState) {\n causes.push({ kind: \"class-state\" });\n }\n return causes;\n }\n\n // ── Function component hooks ──\n const hookTypes = fiber._debugHookTypes;\n\n if (!hookTypes) {\n // No debug info (prod build) — best effort\n if (fiber.memoizedState !== alternate.memoizedState) {\n causes.push({ kind: \"unknown\" });\n }\n return causes;\n }\n\n let currentNode = fiber.memoizedState as HookNode | null;\n let previousNode = alternate.memoizedState as HookNode | null;\n let hasContextHook = false;\n let anyStateHookChanged = false;\n\n for (let i = 0; i < hookTypes.length; i++) {\n const type = hookTypes[i];\n\n // useContext has no linked list node — skip pointer advance\n if (HOOKS_WITHOUT_NODE.has(type)) {\n if (type === \"useContext\") hasContextHook = true;\n continue;\n }\n\n if (STATE_HOOKS.has(type) && currentNode && previousNode) {\n if (!Object.is(currentNode.memoizedState, previousNode.memoizedState)) {\n anyStateHookChanged = true;\n causes.push({\n kind: \"hook\",\n hookIndex: i,\n hookType: type,\n previousValue: previousNode.memoizedState,\n nextValue: currentNode.memoizedState,\n });\n }\n }\n\n currentNode = currentNode?.next ?? null;\n previousNode = previousNode?.next ?? null;\n }\n\n // If no state hook changed but component is self-triggered and has\n // useContext → the context value must have changed.\n if (\n hasContextHook &&\n !anyStateHookChanged &&\n fiber.memoizedProps === alternate.memoizedProps\n ) {\n causes.push({ kind: \"hook\", hookType: \"useContext\" });\n }\n\n // If still nothing and self-triggered, mark unknown\n if (causes.length === 0 && fiber.memoizedProps === alternate.memoizedProps) {\n causes.push({ kind: \"unknown\" });\n }\n\n return causes;\n}\n","import type { PendingEntry, UpdateCause } from \"./types.js\";\n\n// ────────────────────────────────────────────\n// Value formatting\n// ────────────────────────────────────────────\n\nexport function formatValue(value: unknown, maxLength = 50): string {\n if (value === null) return \"null\";\n if (value === undefined) return \"undefined\";\n if (typeof value === \"boolean\") return String(value);\n if (typeof value === \"number\") return String(value);\n if (typeof value === \"function\")\n return `ƒ ${(value as { name?: string }).name || \"anonymous\"}`;\n\n if (typeof value === \"string\") {\n const truncated =\n value.length > maxLength ? value.slice(0, maxLength) + \"…\" : value;\n return JSON.stringify(truncated);\n }\n\n if (Array.isArray(value)) return `Array(${value.length})`;\n\n if (typeof value === \"object\") {\n const name = (value as object).constructor?.name;\n if (name && name !== \"Object\") return name;\n const keys = Object.keys(value as object);\n if (keys.length <= 3) return `{ ${keys.join(\", \")} }`;\n return `{ ${keys.slice(0, 3).join(\", \")}, … }`;\n }\n\n return String(value);\n}\n\n// ────────────────────────────────────────────\n// Cause formatting\n// ────────────────────────────────────────────\n\n/** Compact summary for overlay labels. */\nexport function formatCausesShort(causes: UpdateCause[]): string {\n if (causes.length === 0) return \"\";\n\n const parts: string[] = [];\n for (const cause of causes) {\n if (cause.kind === \"props\") {\n parts.push(\"props\");\n } else if (cause.kind === \"class-state\") {\n parts.push(\"state\");\n } else if (cause.kind === \"hook\" && cause.hookType) {\n const indexSuffix = cause.hookIndex != null ? `[${cause.hookIndex}]` : \"\";\n parts.push(`${cause.hookType}${indexSuffix}`);\n } else {\n parts.push(\"?\");\n }\n }\n\n return parts.join(\", \");\n}\n\n/** Detailed lines for console output. */\nexport function formatCausesConsole(causes: UpdateCause[]): string[] {\n const lines: string[] = [];\n\n for (const cause of causes) {\n if (cause.kind === \"props\") {\n lines.push(\" ↳ props changed (parent re-rendered)\");\n } else if (cause.kind === \"class-state\") {\n lines.push(\" ↳ class state changed\");\n } else if (cause.kind === \"hook\" && cause.hookType) {\n const indexSuffix =\n cause.hookIndex != null ? `[${cause.hookIndex}]` : \"\";\n const name = `${cause.hookType}${indexSuffix}`;\n\n if (cause.previousValue !== undefined && cause.nextValue !== undefined) {\n lines.push(\n ` ↳ ${name}: ${formatValue(cause.previousValue)} → ${formatValue(cause.nextValue)}`,\n );\n } else {\n lines.push(` ↳ ${name} changed`);\n }\n } else {\n lines.push(\" ↳ unknown cause\");\n }\n }\n\n return lines;\n}\n\n// ────────────────────────────────────────────\n// Console output\n// ────────────────────────────────────────────\n\n/** Log re-renders as a collapsed console group. */\nexport function logRerendersToConsole(\n entries: PendingEntry[],\n showCauses: boolean,\n) {\n if (entries.length === 0) return;\n\n console.groupCollapsed(\n `%c⚛ ${entries.length} re-render${entries.length > 1 ? \"s\" : \"\"}`,\n \"color: #61dafb; font-weight: bold\",\n );\n\n for (let i = 0; i < entries.length; i++) {\n const entry = entries[i];\n const durationText =\n entry.duration > 0 ? ` (${entry.duration.toFixed(2)}ms)` : \"\";\n console.log(\n `%c${entry.component}%c ${entry.path}${durationText}`,\n \"color: #e8e82e; font-weight: bold\",\n \"color: #888\",\n );\n\n if (showCauses && entry.causes.length > 0) {\n const lines = formatCausesConsole(entry.causes);\n for (const line of lines) {\n console.log(`%c${line}`, \"color: #aaa\");\n }\n }\n }\n\n console.groupEnd();\n}\n","// ────────────────────────────────────────────\n// Per-window overlay infrastructure\n// ────────────────────────────────────────────\n//\n// Performance:\n// - Overlay elements pooled per window, reused via animationend\n// - Single CSS @keyframes per window, no JS timers per element\n\nconst ANIMATION_NAME = \"__rdu-fade\";\n\nconst injectedWindows = new WeakSet<Window>();\n\nfunction ensureStylesheet(win: Window) {\n if (injectedWindows.has(win)) return;\n injectedWindows.add(win);\n\n const style = win.document.createElement(\"style\");\n style.textContent = `\n @keyframes ${ANIMATION_NAME} {\n 0% { opacity: 0; }\n 8% { opacity: var(--rdu-opacity, 1); }\n 40% { opacity: var(--rdu-opacity, 1); }\n 100% { opacity: 0; }\n }\n .${ANIMATION_NAME}-box {\n position: fixed;\n pointer-events: none;\n box-sizing: border-box;\n border-radius: 3px;\n opacity: 0;\n will-change: opacity;\n }\n .${ANIMATION_NAME}-label {\n position: absolute;\n top: -18px;\n left: -1px;\n font: 10px/16px ui-monospace, SFMono-Regular, \"SF Mono\", Menlo, Consolas, monospace;\n padding: 0 4px;\n color: #fff;\n border-radius: 2px 2px 0 0;\n white-space: nowrap;\n pointer-events: none;\n }\n `;\n win.document.head.appendChild(style);\n}\n\n// ────────────────────────────────────────────\n// Overlay root container\n// ────────────────────────────────────────────\n\nconst overlayRoots = new WeakMap<Window, HTMLDivElement>();\n\nfunction getOverlayRoot(win: Window): HTMLDivElement | null {\n if (win.closed) return null;\n\n const existing = overlayRoots.get(win);\n if (existing?.isConnected) return existing;\n\n ensureStylesheet(win);\n\n const root = win.document.createElement(\"div\");\n root.id = \"__react-debug-updates\";\n Object.assign(root.style, {\n position: \"fixed\",\n top: \"0\",\n left: \"0\",\n width: \"0\",\n height: \"0\",\n overflow: \"visible\",\n pointerEvents: \"none\",\n zIndex: \"2147483647\",\n } satisfies Partial<CSSStyleDeclaration>);\n win.document.body.appendChild(root);\n\n overlayRoots.set(win, root);\n return root;\n}\n\n// ────────────────────────────────────────────\n// Element pool\n// ────────────────────────────────────────────\n\nconst pools = new WeakMap<Window, HTMLDivElement[]>();\n\nexport function acquireOverlay(win: Window): HTMLDivElement | null {\n const root = getOverlayRoot(win);\n if (!root) return null;\n\n let pool = pools.get(win);\n if (!pool) {\n pool = [];\n pools.set(win, pool);\n }\n\n const document = win.document;\n let element = pool.pop();\n\n if (!element) {\n element = document.createElement(\"div\");\n element.className = `${ANIMATION_NAME}-box`;\n\n const label = document.createElement(\"span\");\n label.className = `${ANIMATION_NAME}-label`;\n element.appendChild(label);\n\n element.addEventListener(\"animationend\", () => {\n element!.style.animation = \"none\";\n element!.remove();\n pool!.push(element!);\n });\n }\n\n root.appendChild(element);\n return element;\n}\n\nexport function disposeAllOverlays() {\n const mainRoot = overlayRoots.get(window);\n mainRoot?.remove();\n overlayRoots.delete(window);\n pools.delete(window);\n}\n\nexport const OVERLAY_ANIMATION_NAME = ANIMATION_NAME;\n","import type { HighlightOptions, PendingEntry } from \"./types.js\";\nimport { formatCausesShort } from \"./format.js\";\nimport { acquireOverlay, OVERLAY_ANIMATION_NAME } from \"./overlay.js\";\n\n// ────────────────────────────────────────────\n// Heat color\n// ────────────────────────────────────────────\n\nfunction heatColor(count: number, alpha: number): string {\n const normalizedCount = Math.min((count - 1) / 7, 1);\n const hue = 200 - normalizedCount * 200;\n const saturation = 85 + normalizedCount * 15;\n const lightness = 55 - normalizedCount * 10;\n return `hsla(${hue}, ${saturation}%, ${lightness}%, ${alpha})`;\n}\n\n// ────────────────────────────────────────────\n// Batched flush\n// ────────────────────────────────────────────\n//\n// Commit path just pushes lightweight refs (no DOM reads, no formatting).\n// Flush (setInterval): batched rect reads → batched DOM writes.\n\nexport const HIGHLIGHT_DEFAULTS: Required<HighlightOptions> = {\n flushInterval: 250,\n animationDuration: 1200,\n showLabels: true,\n opacity: 0.3,\n};\n\ninterface CoalescedEntry {\n count: number;\n totalDuration: number;\n component: string;\n /** Shallowest fiber depth among coalesced entries (for z-ordering). */\n depth: number;\n domNode: HTMLElement;\n ownerWindow: Window;\n causeSummary: string;\n}\n\nexport function createBatcher(options: Required<HighlightOptions>) {\n let pending: PendingEntry[] = [];\n let timer: ReturnType<typeof setInterval> | null = null;\n\n function flush() {\n if (pending.length === 0) return;\n\n const batch = pending;\n pending = [];\n\n // Coalesce by DOM node identity\n const map = new Map<HTMLElement, CoalescedEntry>();\n\n for (let i = 0; i < batch.length; i++) {\n const entry = batch[i];\n if (!entry.domNode) continue;\n\n const existing = map.get(entry.domNode);\n if (existing) {\n existing.count++;\n existing.totalDuration += entry.duration;\n existing.depth = Math.min(existing.depth, entry.depth);\n } else {\n const win = entry.domNode.ownerDocument?.defaultView;\n if (!win || win.closed) continue;\n map.set(entry.domNode, {\n count: 1,\n totalDuration: entry.duration,\n component: entry.component,\n depth: entry.depth,\n domNode: entry.domNode,\n ownerWindow: win,\n causeSummary: formatCausesShort(entry.causes),\n });\n }\n }\n\n // Read phase: batch all rect reads\n const toShow: Array<{ coalesced: CoalescedEntry; rect: DOMRect }> = [];\n for (const coalesced of map.values()) {\n const rect = coalesced.domNode.getBoundingClientRect();\n if (rect.width > 0 || rect.height > 0) {\n toShow.push({ coalesced, rect });\n }\n }\n\n // Sort deepest first so parents are appended last (render on top)\n toShow.sort((a, b) => b.coalesced.depth - a.coalesced.depth);\n\n // Write phase: position overlays\n for (let i = 0; i < toShow.length; i++) {\n const { coalesced, rect } = toShow[i];\n const element = acquireOverlay(coalesced.ownerWindow);\n if (!element) continue;\n\n const fillColor = heatColor(coalesced.count, 0.18);\n const borderColor = heatColor(coalesced.count, 0.75);\n const labelBackground = heatColor(coalesced.count, 0.9);\n\n const style = element.style;\n style.top = `${rect.top}px`;\n style.left = `${rect.left}px`;\n style.width = `${rect.width}px`;\n style.height = `${rect.height}px`;\n style.backgroundColor = fillColor;\n style.border = `1.5px solid ${borderColor}`;\n style.setProperty(\"--rdu-opacity\", String(options.opacity));\n style.animation = `${OVERLAY_ANIMATION_NAME} ${options.animationDuration}ms ease-out forwards`;\n\n const label = element.firstElementChild as HTMLElement;\n if (options.showLabels) {\n const countText = coalesced.count > 1 ? ` ×${coalesced.count}` : \"\";\n const durationText =\n coalesced.totalDuration > 0\n ? ` ${coalesced.totalDuration.toFixed(1)}ms`\n : \"\";\n const causeText = coalesced.causeSummary\n ? ` (${coalesced.causeSummary})`\n : \"\";\n label.textContent = `${coalesced.component}${countText}${durationText}${causeText}`;\n label.style.backgroundColor = labelBackground;\n } else {\n label.textContent = \"\";\n label.style.backgroundColor = \"transparent\";\n }\n }\n }\n\n function push(entry: PendingEntry) {\n pending.push(entry);\n if (!timer) {\n timer = setInterval(flush, options.flushInterval);\n }\n }\n\n function dispose() {\n if (timer) {\n clearInterval(timer);\n timer = null;\n }\n pending = [];\n }\n\n return { push, dispose };\n}\n","import type {\n DevToolsHook,\n FiberRoot,\n LoggerOptions,\n PendingEntry,\n RenderEntry,\n RenderLogger,\n} from \"./types.js\";\nimport {\n detectRenders,\n findNearestDOMNode,\n getComponentName,\n getFiberPath,\n} from \"./fiber.js\";\nimport { detectCauses } from \"./causes.js\";\nimport { logRerendersToConsole } from \"./format.js\";\nimport { HIGHLIGHT_DEFAULTS, createBatcher } from \"./batcher.js\";\nimport { disposeAllOverlays } from \"./overlay.js\";\n\n/**\n * Attach a render logger to React's DevTools hook.\n *\n * Hooks into `__REACT_DEVTOOLS_GLOBAL_HOOK__.onCommitFiberRoot` to intercept\n * every React commit. Records re-render entries, optionally logs them to the\n * console, and optionally shows visual highlight overlays on re-rendered DOM nodes.\n *\n * Returns a `RenderLogger` handle, or `null` if the DevTools hook is not found.\n */\nexport function attachRenderLogger(\n options: LoggerOptions = {},\n): RenderLogger | null {\n const {\n silent = false,\n bufferSize = 500,\n filter,\n highlight = false,\n mode = \"self-triggered\",\n showCauses = false,\n } = options;\n\n const hook = (\n window as unknown as { __REACT_DEVTOOLS_GLOBAL_HOOK__?: DevToolsHook }\n ).__REACT_DEVTOOLS_GLOBAL_HOOK__;\n\n if (!hook) {\n console.warn(\n \"[react-debug-updates] __REACT_DEVTOOLS_GLOBAL_HOOK__ not found. \" +\n \"Make sure React DevTools or a dev build of React is active.\",\n );\n return null;\n }\n\n const highlightOptions = highlight\n ? {\n ...HIGHLIGHT_DEFAULTS,\n ...(typeof highlight === \"object\" ? highlight : {}),\n }\n : null;\n\n const batcher = highlightOptions ? createBatcher(highlightOptions) : null;\n\n const entries: RenderEntry[] = [];\n const previousOnCommit = hook.onCommitFiberRoot.bind(hook);\n\n hook.onCommitFiberRoot = (\n rendererID: number,\n root: FiberRoot,\n priorityLevel?: unknown,\n ) => {\n previousOnCommit(rendererID, root, priorityLevel);\n\n // 1. Detect which component fibers actually re-rendered (pure fiber analysis)\n const detectedRenders = detectRenders(root.current, mode);\n if (detectedRenders.length === 0) return;\n\n // 2. Build full entries: resolve names, DOM nodes, causes\n const pendingEntries: PendingEntry[] = [];\n\n for (let i = 0; i < detectedRenders.length; i++) {\n const { fiber, depth } = detectedRenders[i];\n const name = getComponentName(fiber);\n if (!name) continue;\n\n const pendingEntry: PendingEntry = {\n component: name,\n path: getFiberPath(fiber),\n duration: fiber.actualDuration ?? 0,\n depth,\n domNode: findNearestDOMNode(fiber),\n causes: showCauses ? detectCauses(fiber) : [],\n };\n\n const renderEntry: RenderEntry = {\n component: pendingEntry.component,\n path: pendingEntry.path,\n duration: pendingEntry.duration,\n timestamp: performance.now(),\n causes: pendingEntry.causes,\n };\n\n if (filter && !filter(renderEntry)) continue;\n\n if (entries.length >= bufferSize) entries.shift();\n entries.push(renderEntry);\n\n pendingEntries.push(pendingEntry);\n batcher?.push(pendingEntry);\n }\n\n // 3. Console output\n if (!silent) {\n logRerendersToConsole(pendingEntries, showCauses);\n }\n };\n\n const disconnect = () => {\n hook.onCommitFiberRoot = previousOnCommit;\n batcher?.dispose();\n disposeAllOverlays();\n };\n\n if (!silent) {\n console.log(\n \"%c⚛ react-debug-updates attached\",\n \"color: #61dafb; font-weight: bold\",\n );\n }\n\n return { entries, disconnect };\n}\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"react-debug-updates.js","sources":["../src/fiber.ts","../src/causes.ts","../src/format.ts","../src/overlay.ts","../src/highlights.ts","../src/monitor.ts"],"sourcesContent":["import type { Fiber, DetectedRender } from \"./types.js\";\n\n// ────────────────────────────────────────────\n// Fiber tag constants\n// ────────────────────────────────────────────\n\nexport const FiberTag = {\n FunctionComponent: 0,\n ClassComponent: 1,\n HostComponent: 5,\n ForwardRef: 11,\n SimpleMemoComponent: 15,\n MemoComponent: 14,\n} as const;\n\n// Component tags we report as re-renders.\n// MemoComponent (14) is excluded to avoid double-reporting — it's a wrapper\n// fiber around the inner component. The inner component (FunctionComponent,\n// ForwardRef, or SimpleMemoComponent) has its own PerformedWork flag and\n// will be reported correctly on its own.\nconst COMPONENT_TAGS = new Set<number>([\n FiberTag.FunctionComponent,\n FiberTag.ClassComponent,\n FiberTag.ForwardRef,\n FiberTag.SimpleMemoComponent,\n]);\n\nconst PerformedWork = 0b0000001;\n\n// ────────────────────────────────────────────\n// Fiber tree helpers\n// ────────────────────────────────────────────\n\nexport function getComponentName(fiber: Fiber): string | null {\n const { type } = fiber;\n if (!type || typeof type === \"string\") return null;\n return type.displayName ?? type.name ?? null;\n}\n\nfunction isHTMLElement(value: unknown): value is HTMLElement {\n return (\n typeof value === \"object\" &&\n value !== null &&\n (value as Node).nodeType === 1 &&\n typeof (value as HTMLElement).getBoundingClientRect === \"function\"\n );\n}\n\nexport function findNearestDOMNode(fiber: Fiber): HTMLElement | null {\n if (fiber.tag === FiberTag.HostComponent && isHTMLElement(fiber.stateNode)) {\n return fiber.stateNode;\n }\n let child: Fiber | null = fiber.child;\n while (child) {\n const found = findNearestDOMNode(child);\n if (found) return found;\n child = child.sibling;\n }\n return null;\n}\n\nexport function getFiberPath(fiber: Fiber, maxDepth = 5): string {\n const parts: string[] = [];\n let current: Fiber | null = fiber.return;\n let depth = 0;\n while (current && depth < maxDepth) {\n const name = getComponentName(current);\n if (name) parts.unshift(name);\n current = current.return;\n depth++;\n }\n return parts.length ? parts.join(\" → \") : \"(root)\";\n}\n\n// ────────────────────────────────────────────\n// Self-triggered detection\n// ────────────────────────────────────────────\n\nfunction isSelfTriggered(fiber: Fiber): boolean {\n const alternate = fiber.alternate;\n if (!alternate) return false;\n return fiber.memoizedProps === alternate.memoizedProps;\n}\n\n// ────────────────────────────────────────────\n// didFiberRender — mirrors React DevTools\n// ────────────────────────────────────────────\n//\n// See: react-devtools-shared/src/backend/fiber/shared/DevToolsFiberChangeDetection.js\n//\n// For component fibers (function, class, memo, forwardRef), React sets the\n// PerformedWork flag (bit 0) only when user code actually executes.\n// createWorkInProgress resets flags to NoFlags, so PerformedWork on a\n// work-in-progress fiber is always fresh — never stale from a prior commit.\n//\n// This check must only be called AFTER confirming prevFiber !== nextFiber\n// (i.e. the fiber was actually processed, not a bailed-out subtree).\n\nfunction didFiberRender(nextFiber: Fiber): boolean {\n return (nextFiber.flags & PerformedWork) === PerformedWork;\n}\n\n// ────────────────────────────────────────────\n// Detect which components re-rendered in a commit\n// ────────────────────────────────────────────\n//\n// Mirrors React DevTools' updateFiberRecursively / updateChildrenRecursively.\n//\n// React double-buffers fibers. After a commit, root.current is the committed\n// tree and root.current.alternate is the previous tree. We walk both in\n// parallel. At each level, if prevChild and nextChild are the same object,\n// React bailed out that entire subtree (via cloneChildFibers) — we skip it.\n// Otherwise, we use nextChild.alternate as the previous fiber and check\n// didFiberRender (PerformedWork) to see if user code actually ran.\n//\n// Returns lightweight DetectedRender objects — just the fiber and its depth.\n// DOM lookup, cause detection, and formatting are the caller's responsibility.\n\nexport function detectRenders(\n root: Fiber,\n mode: \"self-triggered\" | \"all\",\n): DetectedRender[] {\n const results: DetectedRender[] = [];\n const selfTriggeredOnly = mode === \"self-triggered\";\n\n // The alternate of the committed root is the previous tree's root.\n // On initial mount this is null — nothing to report.\n const previousRoot = root.alternate;\n if (!previousRoot) return results;\n\n function walk(\n nextFiber: Fiber,\n previousFiber: Fiber | null,\n depth: number,\n ) {\n // ── Check this fiber ──\n if (\n COMPONENT_TAGS.has(nextFiber.tag) &&\n previousFiber !== null &&\n previousFiber !== nextFiber && // same object → bailed-out subtree\n didFiberRender(nextFiber) &&\n (!selfTriggeredOnly || isSelfTriggered(nextFiber))\n ) {\n results.push({ fiber: nextFiber, depth });\n }\n\n // ── Walk children, matching with previous tree ──\n let nextChild = nextFiber.child;\n let previousChildAtSameIndex = previousFiber?.child ?? null;\n\n while (nextChild) {\n let matchedPrevious: Fiber | null;\n\n if (previousChildAtSameIndex === nextChild) {\n // Same object identity — React shared this fiber via cloneChildFibers.\n // The entire subtree was bailed out; passing the same object as both\n // prev and next causes the prevFiber !== nextFiber guard to skip it.\n matchedPrevious = nextChild;\n } else {\n // Different object — this fiber was processed. The alternate is the\n // corresponding fiber from the previous tree.\n matchedPrevious = nextChild.alternate;\n }\n\n walk(nextChild, matchedPrevious, depth + 1);\n\n nextChild = nextChild.sibling;\n previousChildAtSameIndex = previousChildAtSameIndex?.sibling ?? null;\n }\n }\n\n walk(root, previousRoot, 0);\n return results;\n}\n","import type { Fiber, HookNode, UpdateCause } from \"./types.js\";\nimport { FiberTag } from \"./fiber.js\";\n\n// ────────────────────────────────────────────\n// Update cause detection\n// ────────────────────────────────────────────\n//\n// For function components, React stores hooks as a linked list on\n// fiber.memoizedState. In dev builds, _debugHookTypes lists hook names\n// in call order. We walk both in parallel, comparing memoizedState\n// on each node to find which hook's state actually changed.\n//\n// Caveat: useContext does NOT create a linked list node, so we skip it\n// when advancing the pointer, and detect it by elimination.\n\n/**\n * Hooks whose memoizedState changing can trigger a re-render.\n * We only report these — useEffect/useMemo/etc. don't cause re-renders.\n */\nconst STATE_HOOKS = new Set([\n \"useState\",\n \"useReducer\",\n \"useSyncExternalStore\",\n]);\n\n/**\n * Hooks that do NOT create a node in the memoizedState linked list.\n * Currently only useContext — every other hook allocates a node.\n */\nconst HOOKS_WITHOUT_NODE = new Set([\"useContext\"]);\n\nexport function detectCauses(fiber: Fiber): UpdateCause[] {\n const alternate = fiber.alternate;\n if (!alternate) return [];\n\n const causes: UpdateCause[] = [];\n\n // ── Props changed (parent re-rendered us with new props) ──\n if (fiber.memoizedProps !== alternate.memoizedProps) {\n causes.push({ kind: \"props\" });\n }\n\n // ── Class component ──\n if (fiber.tag === FiberTag.ClassComponent) {\n if (fiber.memoizedState !== alternate.memoizedState) {\n causes.push({ kind: \"class-state\" });\n }\n return causes;\n }\n\n // ── Function component hooks ──\n const hookTypes = fiber._debugHookTypes;\n\n if (!hookTypes) {\n // No debug info (prod build) — best effort\n if (fiber.memoizedState !== alternate.memoizedState) {\n causes.push({ kind: \"unknown\" });\n }\n return causes;\n }\n\n let currentNode = fiber.memoizedState as HookNode | null;\n let previousNode = alternate.memoizedState as HookNode | null;\n let hasContextHook = false;\n let anyStateHookChanged = false;\n\n for (let i = 0; i < hookTypes.length; i++) {\n const type = hookTypes[i];\n\n // useContext has no linked list node — skip pointer advance\n if (HOOKS_WITHOUT_NODE.has(type)) {\n if (type === \"useContext\") hasContextHook = true;\n continue;\n }\n\n if (STATE_HOOKS.has(type) && currentNode && previousNode) {\n if (!Object.is(currentNode.memoizedState, previousNode.memoizedState)) {\n anyStateHookChanged = true;\n causes.push({\n kind: \"hook\",\n hookIndex: i,\n hookType: type,\n previousValue: previousNode.memoizedState,\n nextValue: currentNode.memoizedState,\n });\n }\n }\n\n currentNode = currentNode?.next ?? null;\n previousNode = previousNode?.next ?? null;\n }\n\n // If no state hook changed but component is self-triggered and has\n // useContext → the context value must have changed.\n if (\n hasContextHook &&\n !anyStateHookChanged &&\n fiber.memoizedProps === alternate.memoizedProps\n ) {\n causes.push({ kind: \"hook\", hookType: \"useContext\" });\n }\n\n // If still nothing and self-triggered, mark unknown\n if (causes.length === 0 && fiber.memoizedProps === alternate.memoizedProps) {\n causes.push({ kind: \"unknown\" });\n }\n\n return causes;\n}\n","import type { HighlightEntry, UpdateCause } from \"./types.js\";\n\n// ────────────────────────────────────────────\n// Value formatting\n// ────────────────────────────────────────────\n\nexport function formatValue(value: unknown, maxLength = 50): string {\n if (value === null) return \"null\";\n if (value === undefined) return \"undefined\";\n if (typeof value === \"boolean\") return String(value);\n if (typeof value === \"number\") return String(value);\n if (typeof value === \"function\")\n return `ƒ ${(value as { name?: string }).name || \"anonymous\"}`;\n\n if (typeof value === \"string\") {\n const truncated =\n value.length > maxLength ? value.slice(0, maxLength) + \"…\" : value;\n return JSON.stringify(truncated);\n }\n\n if (Array.isArray(value)) return `Array(${value.length})`;\n\n if (typeof value === \"object\") {\n const name = (value as object).constructor?.name;\n if (name && name !== \"Object\") return name;\n const keys = Object.keys(value as object);\n if (keys.length <= 3) return `{ ${keys.join(\", \")} }`;\n return `{ ${keys.slice(0, 3).join(\", \")}, … }`;\n }\n\n return String(value);\n}\n\n// ────────────────────────────────────────────\n// Cause formatting\n// ────────────────────────────────────────────\n\n/** Compact summary for overlay labels. */\nexport function formatCausesShort(causes: UpdateCause[]): string {\n if (causes.length === 0) return \"\";\n\n const parts: string[] = [];\n for (const cause of causes) {\n if (cause.kind === \"props\") {\n parts.push(\"props\");\n } else if (cause.kind === \"class-state\") {\n parts.push(\"state\");\n } else if (cause.kind === \"hook\" && cause.hookType) {\n const indexSuffix = cause.hookIndex != null ? `[${cause.hookIndex}]` : \"\";\n parts.push(`${cause.hookType}${indexSuffix}`);\n } else {\n parts.push(\"?\");\n }\n }\n\n return parts.join(\", \");\n}\n\n/** Detailed lines for console output. */\nexport function formatCausesConsole(causes: UpdateCause[]): string[] {\n const lines: string[] = [];\n\n for (const cause of causes) {\n if (cause.kind === \"props\") {\n lines.push(\" ↳ props changed (parent re-rendered)\");\n } else if (cause.kind === \"class-state\") {\n lines.push(\" ↳ class state changed\");\n } else if (cause.kind === \"hook\" && cause.hookType) {\n const indexSuffix =\n cause.hookIndex != null ? `[${cause.hookIndex}]` : \"\";\n const name = `${cause.hookType}${indexSuffix}`;\n\n if (cause.previousValue !== undefined && cause.nextValue !== undefined) {\n lines.push(\n ` ↳ ${name}: ${formatValue(cause.previousValue)} → ${formatValue(cause.nextValue)}`,\n );\n } else {\n lines.push(` ↳ ${name} changed`);\n }\n } else {\n lines.push(\" ↳ unknown cause\");\n }\n }\n\n return lines;\n}\n\n// ────────────────────────────────────────────\n// Console output\n// ────────────────────────────────────────────\n\n/** Log re-renders as a collapsed console group. */\nexport function logRerendersToConsole(\n entries: HighlightEntry[],\n showCauses: boolean,\n) {\n if (entries.length === 0) return;\n\n console.groupCollapsed(\n `%c⚛ ${entries.length} re-render${entries.length > 1 ? \"s\" : \"\"}`,\n \"color: #61dafb; font-weight: bold\",\n );\n\n for (let i = 0; i < entries.length; i++) {\n const entry = entries[i];\n const durationText =\n entry.duration > 0 ? ` (${entry.duration.toFixed(2)}ms)` : \"\";\n console.log(\n `%c${entry.component}%c ${entry.path}${durationText}`,\n \"color: #e8e82e; font-weight: bold\",\n \"color: #888\",\n );\n\n if (showCauses && entry.causes.length > 0) {\n const lines = formatCausesConsole(entry.causes);\n for (const line of lines) {\n console.log(`%c${line}`, \"color: #aaa\");\n }\n }\n }\n\n console.groupEnd();\n}\n","// ────────────────────────────────────────────\n// Per-window overlay infrastructure\n// ────────────────────────────────────────────\n//\n// Performance:\n// - Overlay elements pooled per window, reused via animationend\n// - Single CSS @keyframes per window, no JS timers per element\n\nconst ANIMATION_NAME = \"__rdu-fade\";\n\nconst injectedWindows = new WeakSet<Window>();\n\nfunction ensureStylesheet(win: Window) {\n if (injectedWindows.has(win)) return;\n injectedWindows.add(win);\n\n const style = win.document.createElement(\"style\");\n style.textContent = `\n @keyframes ${ANIMATION_NAME} {\n 0% { opacity: 0; }\n 8% { opacity: var(--rdu-opacity, 1); }\n 40% { opacity: var(--rdu-opacity, 1); }\n 100% { opacity: 0; }\n }\n .${ANIMATION_NAME}-box {\n position: fixed;\n pointer-events: none;\n box-sizing: border-box;\n border-radius: 3px;\n opacity: 0;\n will-change: opacity;\n }\n .${ANIMATION_NAME}-label {\n position: absolute;\n top: -18px;\n left: -1px;\n font: 10px/16px ui-monospace, SFMono-Regular, \"SF Mono\", Menlo, Consolas, monospace;\n padding: 0 4px;\n color: #fff;\n border-radius: 2px 2px 0 0;\n white-space: nowrap;\n pointer-events: none;\n }\n `;\n win.document.head.appendChild(style);\n}\n\n// ────────────────────────────────────────────\n// Overlay root container\n// ────────────────────────────────────────────\n\nconst overlayRoots = new WeakMap<Window, HTMLDivElement>();\n\nfunction getOverlayRoot(win: Window): HTMLDivElement | null {\n if (win.closed) return null;\n\n const existing = overlayRoots.get(win);\n if (existing?.isConnected) return existing;\n\n ensureStylesheet(win);\n\n const root = win.document.createElement(\"div\");\n root.id = \"__react-debug-updates\";\n Object.assign(root.style, {\n position: \"fixed\",\n top: \"0\",\n left: \"0\",\n width: \"0\",\n height: \"0\",\n overflow: \"visible\",\n pointerEvents: \"none\",\n zIndex: \"2147483647\",\n } satisfies Partial<CSSStyleDeclaration>);\n win.document.body.appendChild(root);\n\n overlayRoots.set(win, root);\n return root;\n}\n\n// ────────────────────────────────────────────\n// Element pool\n// ────────────────────────────────────────────\n\nconst pools = new WeakMap<Window, HTMLDivElement[]>();\n\nexport function acquireOverlay(win: Window): HTMLDivElement | null {\n const root = getOverlayRoot(win);\n if (!root) return null;\n\n let pool = pools.get(win);\n if (!pool) {\n pool = [];\n pools.set(win, pool);\n }\n\n const document = win.document;\n let element = pool.pop();\n\n if (!element) {\n element = document.createElement(\"div\");\n element.className = `${ANIMATION_NAME}-box`;\n\n const label = document.createElement(\"span\");\n label.className = `${ANIMATION_NAME}-label`;\n element.appendChild(label);\n\n element.addEventListener(\"animationend\", () => {\n element!.style.animation = \"none\";\n element!.remove();\n pool!.push(element!);\n });\n }\n\n root.appendChild(element);\n return element;\n}\n\nexport function disposeAllOverlays() {\n const mainRoot = overlayRoots.get(window);\n mainRoot?.remove();\n overlayRoots.delete(window);\n pools.delete(window);\n}\n\nexport const OVERLAY_ANIMATION_NAME = ANIMATION_NAME;\n","import type { OverlayConfig, HighlightEntry } from \"./types.js\";\nimport { formatCausesShort } from \"./format.js\";\nimport { acquireOverlay, OVERLAY_ANIMATION_NAME } from \"./overlay.js\";\n\n// ────────────────────────────────────────────\n// Heat color\n// ────────────────────────────────────────────\n\nfunction heatColor(count: number, alpha: number): string {\n const normalizedCount = Math.min((count - 1) / 7, 1);\n const hue = 200 - normalizedCount * 200;\n const saturation = 85 + normalizedCount * 15;\n const lightness = 55 - normalizedCount * 10;\n return `hsla(${hue}, ${saturation}%, ${lightness}%, ${alpha})`;\n}\n\n// ────────────────────────────────────────────\n// Highlight scheduler\n// ────────────────────────────────────────────\n//\n// Commit path just pushes lightweight entries (no DOM reads, no formatting).\n// Flush (setInterval): batched rect reads → batched DOM writes.\n\ninterface CoalescedEntry {\n count: number;\n totalDuration: number;\n component: string;\n /** Shallowest fiber depth among coalesced entries (for z-ordering). */\n depth: number;\n domNode: HTMLElement;\n ownerWindow: Window;\n causeSummary: string;\n}\n\nexport function createHighlighter(config: OverlayConfig) {\n let pending: HighlightEntry[] = [];\n let timer: ReturnType<typeof setInterval> | null = null;\n\n function flush() {\n if (pending.length === 0) return;\n\n const batch = pending;\n pending = [];\n\n // Coalesce by DOM node identity\n const map = new Map<HTMLElement, CoalescedEntry>();\n\n for (let i = 0; i < batch.length; i++) {\n const entry = batch[i];\n if (!entry.domNode) continue;\n\n const existing = map.get(entry.domNode);\n if (existing) {\n existing.count++;\n existing.totalDuration += entry.duration;\n existing.depth = Math.min(existing.depth, entry.depth);\n } else {\n const win = entry.domNode.ownerDocument?.defaultView;\n if (!win || win.closed) continue;\n map.set(entry.domNode, {\n count: 1,\n totalDuration: entry.duration,\n component: entry.component,\n depth: entry.depth,\n domNode: entry.domNode,\n ownerWindow: win,\n causeSummary: formatCausesShort(entry.causes),\n });\n }\n }\n\n // Read phase: batch all rect reads\n const toShow: Array<{ coalesced: CoalescedEntry; rect: DOMRect }> = [];\n for (const coalesced of map.values()) {\n const rect = coalesced.domNode.getBoundingClientRect();\n if (rect.width > 0 || rect.height > 0) {\n toShow.push({ coalesced, rect });\n }\n }\n\n // Sort deepest first so parents are appended last (render on top)\n toShow.sort((a, b) => b.coalesced.depth - a.coalesced.depth);\n\n // Write phase: position overlays\n for (let i = 0; i < toShow.length; i++) {\n const { coalesced, rect } = toShow[i];\n const element = acquireOverlay(coalesced.ownerWindow);\n if (!element) continue;\n\n const fillColor = heatColor(coalesced.count, 0.18);\n const borderColor = heatColor(coalesced.count, 0.75);\n const labelBackground = heatColor(coalesced.count, 0.9);\n\n const style = element.style;\n style.top = `${rect.top}px`;\n style.left = `${rect.left}px`;\n style.width = `${rect.width}px`;\n style.height = `${rect.height}px`;\n style.backgroundColor = fillColor;\n style.border = `1.5px solid ${borderColor}`;\n style.setProperty(\"--rdu-opacity\", String(config.opacity));\n style.animation = `${OVERLAY_ANIMATION_NAME} ${config.animationDuration}ms ease-out forwards`;\n\n const label = element.firstElementChild as HTMLElement;\n if (config.showLabels) {\n const countText = coalesced.count > 1 ? ` ×${coalesced.count}` : \"\";\n const durationText =\n coalesced.totalDuration > 0\n ? ` ${coalesced.totalDuration.toFixed(1)}ms`\n : \"\";\n const causeText = coalesced.causeSummary\n ? ` (${coalesced.causeSummary})`\n : \"\";\n label.textContent = `${coalesced.component}${countText}${durationText}${causeText}`;\n label.style.backgroundColor = labelBackground;\n } else {\n label.textContent = \"\";\n label.style.backgroundColor = \"transparent\";\n }\n }\n }\n\n function push(entry: HighlightEntry) {\n pending.push(entry);\n if (!timer) {\n timer = setInterval(flush, config.flushInterval);\n }\n }\n\n function dispose() {\n if (timer) {\n clearInterval(timer);\n timer = null;\n }\n pending = [];\n }\n\n return { push, dispose };\n}\n","import type {\n DevToolsHook,\n FiberRoot,\n HighlightEntry,\n MonitorOptions,\n} from \"./types.js\";\nimport {\n detectRenders,\n findNearestDOMNode,\n getComponentName,\n getFiberPath,\n} from \"./fiber.js\";\nimport { detectCauses } from \"./causes.js\";\nimport { logRerendersToConsole } from \"./format.js\";\nimport { createHighlighter } from \"./highlights.js\";\nimport { disposeAllOverlays } from \"./overlay.js\";\n\n/**\n * Ensure __REACT_DEVTOOLS_GLOBAL_HOOK__ exists on window.\n *\n * React reads this hook during initialization — if it's missing, React will\n * never connect. When this library is imported before React (the recommended\n * setup), we need to create a minimal hook so React can find and register with it.\n *\n * If a hook already exists (e.g. from the React DevTools extension), we leave\n * it untouched and hook into it.\n */\nfunction ensureDevToolsHook(win: Window): DevToolsHook {\n const global = win as unknown as {\n __REACT_DEVTOOLS_GLOBAL_HOOK__?: DevToolsHook;\n };\n\n if (!global.__REACT_DEVTOOLS_GLOBAL_HOOK__) {\n global.__REACT_DEVTOOLS_GLOBAL_HOOK__ = {\n supportsFiber: true,\n inject() {\n return 1;\n },\n onCommitFiberRoot() {},\n onCommitFiberUnmount() {},\n onPostCommitFiberRoot() {},\n checkDCE() {},\n } as DevToolsHook;\n }\n\n return global.__REACT_DEVTOOLS_GLOBAL_HOOK__;\n}\n\n/**\n * Start monitoring React re-renders.\n *\n * Hooks into `__REACT_DEVTOOLS_GLOBAL_HOOK__.onCommitFiberRoot` to intercept\n * every React commit. Shows visual highlight overlays on re-rendered DOM nodes\n * and optionally logs re-renders to the console.\n *\n * Call this **before** React renders anything — ideally at the very top of\n * your entry point.\n *\n * Returns a `stop` function to unhook and clean up, or `null` if called\n * in a non-browser environment (e.g. SSR).\n */\nexport function startReactUpdatesMonitor({\n logToConsole = false,\n highlight = true,\n mode = \"self-triggered\",\n reasonOfUpdate = false,\n highlightFlushInterval = 250,\n highlightAnimationDuration = 1200,\n highlightShowLabels = true,\n highlightOpacity = 0.3,\n}: MonitorOptions = {}): (() => void) | null {\n // SSR guard — nothing to do without a DOM\n if (typeof window === \"undefined\") return null;\n\n const hook = ensureDevToolsHook(window);\n\n const highlighter = highlight\n ? createHighlighter({\n flushInterval: highlightFlushInterval,\n animationDuration: highlightAnimationDuration,\n showLabels: highlightShowLabels,\n opacity: highlightOpacity,\n })\n : null;\n\n const previousOnCommit = hook.onCommitFiberRoot.bind(hook);\n\n hook.onCommitFiberRoot = (\n rendererID: number,\n root: FiberRoot,\n priorityLevel?: unknown,\n ) => {\n previousOnCommit(rendererID, root, priorityLevel);\n\n // 1. Detect which component fibers actually re-rendered (pure fiber analysis)\n const detectedRenders = detectRenders(root.current, mode);\n if (detectedRenders.length === 0) return;\n\n // 2. Build full entries: resolve names, DOM nodes, causes\n const highlightEntries: HighlightEntry[] = [];\n\n for (let i = 0; i < detectedRenders.length; i++) {\n const { fiber, depth } = detectedRenders[i];\n const name = getComponentName(fiber);\n if (!name) continue;\n\n const entry: HighlightEntry = {\n component: name,\n path: getFiberPath(fiber),\n duration: fiber.actualDuration ?? 0,\n depth,\n domNode: findNearestDOMNode(fiber),\n causes: reasonOfUpdate ? detectCauses(fiber) : [],\n };\n\n highlightEntries.push(entry);\n highlighter?.push(entry);\n }\n\n // 3. Console output\n if (logToConsole) {\n logRerendersToConsole(highlightEntries, reasonOfUpdate);\n }\n };\n\n if (logToConsole) {\n console.log(\n \"%c⚛ react-debug-updates attached\",\n \"color: #61dafb; font-weight: bold\",\n );\n }\n\n return () => {\n hook.onCommitFiberRoot = previousOnCommit;\n highlighter?.dispose();\n disposeAllOverlays();\n };\n}\n"],"names":[],"mappings":"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"}
package/dist/types.d.ts CHANGED
@@ -42,7 +42,7 @@ export interface DetectedRender {
42
42
  fiber: Fiber;
43
43
  depth: number;
44
44
  }
45
- export interface PendingEntry {
45
+ export interface HighlightEntry {
46
46
  component: string;
47
47
  path: string;
48
48
  duration: number;
@@ -50,26 +50,13 @@ export interface PendingEntry {
50
50
  domNode: HTMLElement | null;
51
51
  causes: UpdateCause[];
52
52
  }
53
- export interface HighlightOptions {
54
- /** Time between flushes (ms). Default: 250 */
55
- flushInterval?: number;
56
- /** Total animation duration (ms). Default: 1200 */
57
- animationDuration?: number;
58
- /** Show text labels (component name, count, duration, cause) above overlays. Default: true */
59
- showLabels?: boolean;
60
- /** Peak opacity of overlay highlights (0–1). Default: 0.3 */
61
- opacity?: number;
62
- }
63
- export interface RenderEntry {
64
- component: string;
65
- path: string;
66
- duration: number;
67
- timestamp: number;
68
- causes: UpdateCause[];
53
+ export interface OverlayConfig {
54
+ flushInterval: number;
55
+ animationDuration: number;
56
+ showLabels: boolean;
57
+ opacity: number;
69
58
  }
70
- export interface LoggerOptions {
71
- /** Suppress console output. Default: false */
72
- silent?: boolean;
59
+ export interface MonitorOptions {
73
60
  /**
74
61
  * Which re-renders to track.
75
62
  * - `"self-triggered"` — only components whose own state/hooks changed
@@ -78,12 +65,6 @@ export interface LoggerOptions {
78
65
  * Default: `"self-triggered"`
79
66
  */
80
67
  mode?: "self-triggered" | "all";
81
- /** Ring buffer size for stored entries. Default: 500 */
82
- bufferSize?: number;
83
- /** Filter — return `false` to skip an entry. */
84
- filter?: (entry: RenderEntry) => boolean;
85
- /** Enable visual highlight overlays. `true` for defaults, or pass options. */
86
- highlight?: boolean | HighlightOptions;
87
68
  /**
88
69
  * Detect and display *why* each component re-rendered.
89
70
  *
@@ -91,11 +72,17 @@ export interface LoggerOptions {
91
72
  * with previous→next values for state hooks.
92
73
  * Requires a React dev build (`_debugHookTypes`). Default: `false`
93
74
  */
94
- showCauses?: boolean;
95
- }
96
- export interface RenderLogger {
97
- /** Ring buffer of recorded re-render entries. */
98
- entries: RenderEntry[];
99
- /** Unhook from React and clean up overlays. */
100
- disconnect: () => void;
75
+ reasonOfUpdate?: boolean;
76
+ /** Log re-renders to the console. Default: false */
77
+ logToConsole?: boolean;
78
+ /** Enable visual highlight overlays. Default: true */
79
+ highlight?: boolean;
80
+ /** Show text labels (component name, count, duration, cause) above overlays. Default: true */
81
+ highlightShowLabels?: boolean;
82
+ /** Peak opacity of highlight overlays (0–1). Default: 0.3 */
83
+ highlightOpacity?: number;
84
+ /** Time between highlight flush cycles (ms). Default: 250 */
85
+ highlightFlushInterval?: number;
86
+ /** Highlight fade-out animation duration (ms). Default: 1200 */
87
+ highlightAnimationDuration?: number;
101
88
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-debug-updates",
3
- "version": "0.1.7",
3
+ "version": "0.1.10",
4
4
  "description": "See why React components re-render — visual overlays, console logging, and hook-level cause detection with zero code changes",
5
5
  "type": "module",
6
6
  "main": "./dist/react-debug-updates.cjs",
package/dist/batcher.d.ts DELETED
@@ -1,6 +0,0 @@
1
- import type { HighlightOptions, PendingEntry } from "./types.js";
2
- export declare const HIGHLIGHT_DEFAULTS: Required<HighlightOptions>;
3
- export declare function createBatcher(options: Required<HighlightOptions>): {
4
- push: (entry: PendingEntry) => void;
5
- dispose: () => void;
6
- };
package/dist/logger.d.ts DELETED
@@ -1,11 +0,0 @@
1
- import type { LoggerOptions, RenderLogger } from "./types.js";
2
- /**
3
- * Attach a render logger to React's DevTools hook.
4
- *
5
- * Hooks into `__REACT_DEVTOOLS_GLOBAL_HOOK__.onCommitFiberRoot` to intercept
6
- * every React commit. Records re-render entries, optionally logs them to the
7
- * console, and optionally shows visual highlight overlays on re-rendered DOM nodes.
8
- *
9
- * Returns a `RenderLogger` handle, or `null` if the DevTools hook is not found.
10
- */
11
- export declare function attachRenderLogger(options?: LoggerOptions): RenderLogger | null;