react-debug-updates 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025-present
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,126 @@
1
+ # react-debug-updates
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.
4
+
5
+ ![highlight overlays](https://img.shields.io/badge/overlays-visual%20highlights-61dafb) ![zero config](https://img.shields.io/badge/setup-zero%20config-green)
6
+
7
+ ## How it works
8
+
9
+ Hooks into `__REACT_DEVTOOLS_GLOBAL_HOOK__` to intercept every React commit. No wrappers, no HOCs, no code changes — just call `attachRenderLogger()` and you get:
10
+
11
+ - **Console logging** — grouped, color-coded re-render reports with component tree paths and render durations
12
+ - **Visual overlays** — highlight boxes on re-rendered DOM nodes with a heat-map color scale (blue → red as render count increases)
13
+ - **Cause detection** — pinpoint *which* `useState`, `useReducer`, `useSyncExternalStore`, or `useContext` hook triggered each re-render, with previous→next values
14
+
15
+ ## Install
16
+
17
+ ```bash
18
+ npm install react-debug-updates
19
+ # or
20
+ yarn add react-debug-updates
21
+ # or
22
+ pnpm add react-debug-updates
23
+ ```
24
+
25
+ ## Quick start
26
+
27
+ ```ts
28
+ import { attachRenderLogger } from "react-debug-updates";
29
+
30
+ // Call early in your app's entry point (dev only)
31
+ const logger = attachRenderLogger({
32
+ highlight: true,
33
+ showCauses: true,
34
+ });
35
+
36
+ // Later, to clean up:
37
+ logger?.disconnect();
38
+ ```
39
+
40
+ ### Dev-only guard
41
+
42
+ ```ts
43
+ if (process.env.NODE_ENV === "development") {
44
+ const { attachRenderLogger } = await import("react-debug-updates");
45
+ attachRenderLogger({ highlight: true, showCauses: true });
46
+ }
47
+ ```
48
+
49
+ ## Requirements
50
+
51
+ - React **DevTools extension** installed, OR a **React dev build** (the library needs `__REACT_DEVTOOLS_GLOBAL_HOOK__`)
52
+ - For `showCauses` and render durations: React must be in **dev mode** (provides `_debugHookTypes` and `actualDuration` on fibers)
53
+
54
+ ## API
55
+
56
+ ### `attachRenderLogger(options?): RenderLogger | null`
57
+
58
+ Returns a `RenderLogger` handle, or `null` if the DevTools hook is not available.
59
+
60
+ #### Options
61
+
62
+ | Option | Type | Default | Description |
63
+ | --- | --- | --- | --- |
64
+ | `silent` | `boolean` | `false` | Suppress console output |
65
+ | `mode` | `"self-triggered" \| "all"` | `"self-triggered"` | `"self-triggered"` tracks only components whose own state changed. `"all"` includes children swept by parent updates |
66
+ | `bufferSize` | `number` | `500` | Max entries kept in the ring buffer |
67
+ | `filter` | `(entry: RenderEntry) => boolean` | — | Return `false` to skip an entry |
68
+ | `highlight` | `boolean \| HighlightOptions` | `false` | Enable visual overlay highlights |
69
+ | `showCauses` | `boolean` | `false` | Detect and display why each component re-rendered |
70
+
71
+ #### `HighlightOptions`
72
+
73
+ | Option | Type | Default | Description |
74
+ | --- | --- | --- | --- |
75
+ | `flushInterval` | `number` | `250` | Milliseconds between overlay flush cycles |
76
+ | `animationDuration` | `number` | `1200` | Overlay fade-out animation duration (ms) |
77
+ | `showLabels` | `boolean` | `true` | Show text labels (name, count, duration, cause) above overlays |
78
+
79
+ ### `RenderLogger`
80
+
81
+ | Property | Type | Description |
82
+ | --- | --- | --- |
83
+ | `entries` | `RenderEntry[]` | Ring buffer of recorded re-render entries |
84
+ | `disconnect` | `() => void` | Unhook from React and remove all overlays |
85
+
86
+ ### `RenderEntry`
87
+
88
+ | Property | Type | Description |
89
+ | --- | --- | --- |
90
+ | `component` | `string` | Component display name |
91
+ | `path` | `string` | Ancestor component path (e.g. `"App → Layout → Sidebar"`) |
92
+ | `duration` | `number` | Render duration in ms (requires React dev mode) |
93
+ | `timestamp` | `number` | `performance.now()` when the entry was recorded |
94
+ | `causes` | `UpdateCause[]` | Why this component re-rendered (requires `showCauses`) |
95
+
96
+ ### `UpdateCause`
97
+
98
+ | Property | Type | Description |
99
+ | --- | --- | --- |
100
+ | `kind` | `"hook" \| "props" \| "class-state" \| "unknown"` | Category of the cause |
101
+ | `hookIndex` | `number?` | Source-order index of the hook (0-based) |
102
+ | `hookType` | `string?` | e.g. `"useState"`, `"useReducer"`, `"useContext"` |
103
+ | `previousValue` | `unknown?` | Previous hook state value |
104
+ | `nextValue` | `unknown?` | New hook state value |
105
+
106
+ ## Console output
107
+
108
+ ```
109
+ ⚛ 3 re-renders
110
+ Counter App → Dashboard (0.42ms)
111
+ ↳ useState[0]: 5 → 6
112
+ TodoList App → Dashboard (1.03ms)
113
+ ↳ props changed (parent re-rendered)
114
+ Sidebar App (0.15ms)
115
+ ↳ useContext changed
116
+ ```
117
+
118
+ ## Visual overlays
119
+
120
+ 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.
121
+
122
+ Each overlay label shows: `ComponentName ×count duration (cause)`
123
+
124
+ ## License
125
+
126
+ MIT
@@ -0,0 +1,6 @@
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
+ };
@@ -0,0 +1,2 @@
1
+ import type { Fiber, UpdateCause } from "./types.js";
2
+ export declare function detectCauses(fiber: Fiber): UpdateCause[];
@@ -0,0 +1,13 @@
1
+ import type { Fiber, PendingEntry } from "./types.js";
2
+ export declare const FiberTag: {
3
+ readonly FunctionComponent: 0;
4
+ readonly ClassComponent: 1;
5
+ readonly HostComponent: 5;
6
+ readonly ForwardRef: 11;
7
+ readonly SimpleMemoComponent: 15;
8
+ readonly MemoComponent: 14;
9
+ };
10
+ export declare function getComponentName(fiber: Fiber): string | null;
11
+ export declare function findNearestDOMNode(fiber: Fiber): HTMLElement | null;
12
+ export declare function getFiberPath(fiber: Fiber, maxDepth?: number): string;
13
+ export declare function collectPending(root: Fiber, mode: "self-triggered" | "all", trackCauses: boolean): PendingEntry[];
@@ -0,0 +1,6 @@
1
+ import type { UpdateCause } from "./types.js";
2
+ export declare function formatValue(value: unknown, maxLength?: number): string;
3
+ /** Compact summary for overlay labels. */
4
+ export declare function formatCausesShort(causes: UpdateCause[]): string;
5
+ /** Detailed lines for console output. */
6
+ export declare function formatCausesConsole(causes: UpdateCause[]): string[];
@@ -0,0 +1,2 @@
1
+ export { attachRenderLogger } from "./logger.js";
2
+ export type { LoggerOptions, RenderLogger, RenderEntry, HighlightOptions, UpdateCause, } from "./types.js";
@@ -0,0 +1,11 @@
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;
@@ -0,0 +1,3 @@
1
+ export declare function acquireOverlay(win: Window): HTMLDivElement | null;
2
+ export declare function disposeAllOverlays(): void;
3
+ export declare const OVERLAY_ANIMATION_NAME = "__rdu-fade";
@@ -0,0 +1,457 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
3
+ const STATE_HOOKS = /* @__PURE__ */ new Set([
4
+ "useState",
5
+ "useReducer",
6
+ "useSyncExternalStore"
7
+ ]);
8
+ const HOOKS_WITHOUT_NODE = /* @__PURE__ */ new Set(["useContext"]);
9
+ function detectCauses(fiber) {
10
+ const alternate = fiber.alternate;
11
+ if (!alternate) return [];
12
+ const causes = [];
13
+ if (fiber.memoizedProps !== alternate.memoizedProps) {
14
+ causes.push({ kind: "props" });
15
+ }
16
+ if (fiber.tag === FiberTag.ClassComponent) {
17
+ if (fiber.memoizedState !== alternate.memoizedState) {
18
+ causes.push({ kind: "class-state" });
19
+ }
20
+ return causes;
21
+ }
22
+ const hookTypes = fiber._debugHookTypes;
23
+ if (!hookTypes) {
24
+ if (fiber.memoizedState !== alternate.memoizedState) {
25
+ causes.push({ kind: "unknown" });
26
+ }
27
+ return causes;
28
+ }
29
+ let currentNode = fiber.memoizedState;
30
+ let previousNode = alternate.memoizedState;
31
+ let hasContextHook = false;
32
+ let anyStateHookChanged = false;
33
+ for (let i = 0; i < hookTypes.length; i++) {
34
+ const type = hookTypes[i];
35
+ if (HOOKS_WITHOUT_NODE.has(type)) {
36
+ if (type === "useContext") hasContextHook = true;
37
+ continue;
38
+ }
39
+ if (STATE_HOOKS.has(type) && currentNode && previousNode) {
40
+ if (!Object.is(currentNode.memoizedState, previousNode.memoizedState)) {
41
+ anyStateHookChanged = true;
42
+ causes.push({
43
+ kind: "hook",
44
+ hookIndex: i,
45
+ hookType: type,
46
+ previousValue: previousNode.memoizedState,
47
+ nextValue: currentNode.memoizedState
48
+ });
49
+ }
50
+ }
51
+ currentNode = (currentNode == null ? void 0 : currentNode.next) ?? null;
52
+ previousNode = (previousNode == null ? void 0 : previousNode.next) ?? null;
53
+ }
54
+ if (hasContextHook && !anyStateHookChanged && fiber.memoizedProps === alternate.memoizedProps) {
55
+ causes.push({ kind: "hook", hookType: "useContext" });
56
+ }
57
+ if (causes.length === 0 && fiber.memoizedProps === alternate.memoizedProps) {
58
+ causes.push({ kind: "unknown" });
59
+ }
60
+ return causes;
61
+ }
62
+ const FiberTag = {
63
+ FunctionComponent: 0,
64
+ ClassComponent: 1,
65
+ HostComponent: 5,
66
+ ForwardRef: 11,
67
+ SimpleMemoComponent: 15,
68
+ MemoComponent: 14
69
+ };
70
+ const COMPONENT_TAGS = /* @__PURE__ */ new Set([
71
+ FiberTag.FunctionComponent,
72
+ FiberTag.ClassComponent,
73
+ FiberTag.ForwardRef,
74
+ FiberTag.SimpleMemoComponent,
75
+ FiberTag.MemoComponent
76
+ ]);
77
+ const PerformedWork = 1;
78
+ function getComponentName(fiber) {
79
+ const { type } = fiber;
80
+ if (!type || typeof type === "string") return null;
81
+ return type.displayName ?? type.name ?? null;
82
+ }
83
+ function isHTMLElement(value) {
84
+ return typeof value === "object" && value !== null && value.nodeType === 1 && typeof value.getBoundingClientRect === "function";
85
+ }
86
+ function findNearestDOMNode(fiber) {
87
+ if (fiber.tag === FiberTag.HostComponent && isHTMLElement(fiber.stateNode)) {
88
+ return fiber.stateNode;
89
+ }
90
+ let child = fiber.child;
91
+ while (child) {
92
+ const found = findNearestDOMNode(child);
93
+ if (found) return found;
94
+ child = child.sibling;
95
+ }
96
+ return null;
97
+ }
98
+ function getFiberPath(fiber, maxDepth = 5) {
99
+ const parts = [];
100
+ let current = fiber.return;
101
+ let depth = 0;
102
+ while (current && depth < maxDepth) {
103
+ const name = getComponentName(current);
104
+ if (name) parts.unshift(name);
105
+ current = current.return;
106
+ depth++;
107
+ }
108
+ return parts.length ? parts.join(" → ") : "(root)";
109
+ }
110
+ function isSelfTriggered(fiber) {
111
+ const alternate = fiber.alternate;
112
+ if (!alternate) return false;
113
+ return fiber.memoizedProps === alternate.memoizedProps;
114
+ }
115
+ function collectPending(root, mode, trackCauses) {
116
+ const entries = [];
117
+ const selfTriggeredOnly = mode === "self-triggered";
118
+ function walk(fiber) {
119
+ if (COMPONENT_TAGS.has(fiber.tag) && fiber.flags & PerformedWork && fiber.alternate !== null && (!selfTriggeredOnly || isSelfTriggered(fiber))) {
120
+ const name = getComponentName(fiber);
121
+ if (name) {
122
+ entries.push({
123
+ component: name,
124
+ path: getFiberPath(fiber),
125
+ duration: fiber.actualDuration ?? 0,
126
+ domNode: findNearestDOMNode(fiber),
127
+ causes: trackCauses ? detectCauses(fiber) : []
128
+ });
129
+ }
130
+ }
131
+ if (fiber.child) walk(fiber.child);
132
+ if (fiber.sibling) walk(fiber.sibling);
133
+ }
134
+ walk(root);
135
+ return entries;
136
+ }
137
+ function formatValue(value, maxLength = 50) {
138
+ var _a;
139
+ if (value === null) return "null";
140
+ if (value === void 0) return "undefined";
141
+ if (typeof value === "boolean") return String(value);
142
+ if (typeof value === "number") return String(value);
143
+ if (typeof value === "function")
144
+ return `ƒ ${value.name || "anonymous"}`;
145
+ if (typeof value === "string") {
146
+ const truncated = value.length > maxLength ? value.slice(0, maxLength) + "…" : value;
147
+ return JSON.stringify(truncated);
148
+ }
149
+ if (Array.isArray(value)) return `Array(${value.length})`;
150
+ if (typeof value === "object") {
151
+ const name = (_a = value.constructor) == null ? void 0 : _a.name;
152
+ if (name && name !== "Object") return name;
153
+ const keys = Object.keys(value);
154
+ if (keys.length <= 3) return `{ ${keys.join(", ")} }`;
155
+ return `{ ${keys.slice(0, 3).join(", ")}, … }`;
156
+ }
157
+ return String(value);
158
+ }
159
+ function formatCausesShort(causes) {
160
+ if (causes.length === 0) return "";
161
+ const parts = [];
162
+ for (const cause of causes) {
163
+ if (cause.kind === "props") {
164
+ parts.push("props");
165
+ } else if (cause.kind === "class-state") {
166
+ parts.push("state");
167
+ } else if (cause.kind === "hook" && cause.hookType) {
168
+ const indexSuffix = cause.hookIndex != null ? `[${cause.hookIndex}]` : "";
169
+ parts.push(`${cause.hookType}${indexSuffix}`);
170
+ } else {
171
+ parts.push("?");
172
+ }
173
+ }
174
+ return parts.join(", ");
175
+ }
176
+ function formatCausesConsole(causes) {
177
+ const lines = [];
178
+ for (const cause of causes) {
179
+ if (cause.kind === "props") {
180
+ lines.push(" ↳ props changed (parent re-rendered)");
181
+ } else if (cause.kind === "class-state") {
182
+ lines.push(" ↳ class state changed");
183
+ } else if (cause.kind === "hook" && cause.hookType) {
184
+ const indexSuffix = cause.hookIndex != null ? `[${cause.hookIndex}]` : "";
185
+ const name = `${cause.hookType}${indexSuffix}`;
186
+ if (cause.previousValue !== void 0 && cause.nextValue !== void 0) {
187
+ lines.push(
188
+ ` ↳ ${name}: ${formatValue(cause.previousValue)} → ${formatValue(cause.nextValue)}`
189
+ );
190
+ } else {
191
+ lines.push(` ↳ ${name} changed`);
192
+ }
193
+ } else {
194
+ lines.push(" ↳ unknown cause");
195
+ }
196
+ }
197
+ return lines;
198
+ }
199
+ const ANIMATION_NAME = "__rdu-fade";
200
+ const injectedWindows = /* @__PURE__ */ new WeakSet();
201
+ function ensureStylesheet(win) {
202
+ if (injectedWindows.has(win)) return;
203
+ injectedWindows.add(win);
204
+ const style = win.document.createElement("style");
205
+ style.textContent = `
206
+ @keyframes ${ANIMATION_NAME} {
207
+ 0% { opacity: 0; }
208
+ 8% { opacity: 1; }
209
+ 40% { opacity: 1; }
210
+ 100% { opacity: 0; }
211
+ }
212
+ .${ANIMATION_NAME}-box {
213
+ position: fixed;
214
+ pointer-events: none;
215
+ box-sizing: border-box;
216
+ border-radius: 3px;
217
+ opacity: 0;
218
+ will-change: opacity;
219
+ }
220
+ .${ANIMATION_NAME}-label {
221
+ position: absolute;
222
+ top: -18px;
223
+ left: -1px;
224
+ font: 10px/16px ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;
225
+ padding: 0 4px;
226
+ color: #fff;
227
+ border-radius: 2px 2px 0 0;
228
+ white-space: nowrap;
229
+ pointer-events: none;
230
+ }
231
+ `;
232
+ win.document.head.appendChild(style);
233
+ }
234
+ const overlayRoots = /* @__PURE__ */ new WeakMap();
235
+ function getOverlayRoot(win) {
236
+ if (win.closed) return null;
237
+ const existing = overlayRoots.get(win);
238
+ if (existing == null ? void 0 : existing.isConnected) return existing;
239
+ ensureStylesheet(win);
240
+ const root = win.document.createElement("div");
241
+ root.id = "__react-debug-updates";
242
+ Object.assign(root.style, {
243
+ position: "fixed",
244
+ top: "0",
245
+ left: "0",
246
+ width: "0",
247
+ height: "0",
248
+ overflow: "visible",
249
+ pointerEvents: "none",
250
+ zIndex: "2147483647"
251
+ });
252
+ win.document.body.appendChild(root);
253
+ overlayRoots.set(win, root);
254
+ return root;
255
+ }
256
+ const pools = /* @__PURE__ */ new WeakMap();
257
+ function acquireOverlay(win) {
258
+ const root = getOverlayRoot(win);
259
+ if (!root) return null;
260
+ let pool = pools.get(win);
261
+ if (!pool) {
262
+ pool = [];
263
+ pools.set(win, pool);
264
+ }
265
+ const document = win.document;
266
+ let element = pool.pop();
267
+ if (!element) {
268
+ element = document.createElement("div");
269
+ element.className = `${ANIMATION_NAME}-box`;
270
+ const label = document.createElement("span");
271
+ label.className = `${ANIMATION_NAME}-label`;
272
+ element.appendChild(label);
273
+ element.addEventListener("animationend", () => {
274
+ element.style.animation = "none";
275
+ element.remove();
276
+ pool.push(element);
277
+ });
278
+ }
279
+ root.appendChild(element);
280
+ return element;
281
+ }
282
+ function disposeAllOverlays() {
283
+ const mainRoot = overlayRoots.get(window);
284
+ mainRoot == null ? void 0 : mainRoot.remove();
285
+ overlayRoots.delete(window);
286
+ pools.delete(window);
287
+ }
288
+ const OVERLAY_ANIMATION_NAME = ANIMATION_NAME;
289
+ function heatColor(count, alpha) {
290
+ const normalizedCount = Math.min((count - 1) / 7, 1);
291
+ const hue = 200 - normalizedCount * 200;
292
+ const saturation = 85 + normalizedCount * 15;
293
+ const lightness = 55 - normalizedCount * 10;
294
+ return `hsla(${hue}, ${saturation}%, ${lightness}%, ${alpha})`;
295
+ }
296
+ const HIGHLIGHT_DEFAULTS = {
297
+ flushInterval: 250,
298
+ animationDuration: 1200,
299
+ showLabels: true
300
+ };
301
+ function createBatcher(options) {
302
+ let pending = [];
303
+ let timer = null;
304
+ function flush() {
305
+ var _a;
306
+ if (pending.length === 0) return;
307
+ const batch = pending;
308
+ pending = [];
309
+ const map = /* @__PURE__ */ new Map();
310
+ for (let i = 0; i < batch.length; i++) {
311
+ const entry = batch[i];
312
+ if (!entry.domNode) continue;
313
+ const existing = map.get(entry.domNode);
314
+ if (existing) {
315
+ existing.count++;
316
+ existing.totalDuration += entry.duration;
317
+ } else {
318
+ const win = (_a = entry.domNode.ownerDocument) == null ? void 0 : _a.defaultView;
319
+ if (!win || win.closed) continue;
320
+ map.set(entry.domNode, {
321
+ count: 1,
322
+ totalDuration: entry.duration,
323
+ component: entry.component,
324
+ domNode: entry.domNode,
325
+ ownerWindow: win,
326
+ causeSummary: formatCausesShort(entry.causes)
327
+ });
328
+ }
329
+ }
330
+ const toShow = [];
331
+ for (const coalesced of map.values()) {
332
+ const rect = coalesced.domNode.getBoundingClientRect();
333
+ if (rect.width > 0 || rect.height > 0) {
334
+ toShow.push({ coalesced, rect });
335
+ }
336
+ }
337
+ for (let i = 0; i < toShow.length; i++) {
338
+ const { coalesced, rect } = toShow[i];
339
+ const element = acquireOverlay(coalesced.ownerWindow);
340
+ if (!element) continue;
341
+ const fillColor = heatColor(coalesced.count, 0.18);
342
+ const borderColor = heatColor(coalesced.count, 0.75);
343
+ const labelBackground = heatColor(coalesced.count, 0.9);
344
+ const style = element.style;
345
+ style.top = `${rect.top}px`;
346
+ style.left = `${rect.left}px`;
347
+ style.width = `${rect.width}px`;
348
+ style.height = `${rect.height}px`;
349
+ style.backgroundColor = fillColor;
350
+ style.border = `1.5px solid ${borderColor}`;
351
+ style.animation = `${OVERLAY_ANIMATION_NAME} ${options.animationDuration}ms ease-out forwards`;
352
+ const label = element.firstElementChild;
353
+ if (options.showLabels) {
354
+ const countText = coalesced.count > 1 ? ` ×${coalesced.count}` : "";
355
+ const durationText = coalesced.totalDuration > 0 ? ` ${coalesced.totalDuration.toFixed(1)}ms` : "";
356
+ const causeText = coalesced.causeSummary ? ` (${coalesced.causeSummary})` : "";
357
+ label.textContent = `${coalesced.component}${countText}${durationText}${causeText}`;
358
+ label.style.backgroundColor = labelBackground;
359
+ } else {
360
+ label.textContent = "";
361
+ label.style.backgroundColor = "transparent";
362
+ }
363
+ }
364
+ }
365
+ function push(entry) {
366
+ pending.push(entry);
367
+ if (!timer) {
368
+ timer = setInterval(flush, options.flushInterval);
369
+ }
370
+ }
371
+ function dispose() {
372
+ if (timer) {
373
+ clearInterval(timer);
374
+ timer = null;
375
+ }
376
+ pending = [];
377
+ }
378
+ return { push, dispose };
379
+ }
380
+ function attachRenderLogger(options = {}) {
381
+ const {
382
+ silent = false,
383
+ bufferSize = 500,
384
+ filter,
385
+ highlight = false,
386
+ mode = "self-triggered",
387
+ showCauses = false
388
+ } = options;
389
+ const hook = window.__REACT_DEVTOOLS_GLOBAL_HOOK__;
390
+ if (!hook) {
391
+ console.warn(
392
+ "[react-debug-updates] __REACT_DEVTOOLS_GLOBAL_HOOK__ not found. Make sure React DevTools or a dev build of React is active."
393
+ );
394
+ return null;
395
+ }
396
+ const highlightOptions = highlight ? {
397
+ ...HIGHLIGHT_DEFAULTS,
398
+ ...typeof highlight === "object" ? highlight : {}
399
+ } : null;
400
+ const batcher = highlightOptions ? createBatcher(highlightOptions) : null;
401
+ const entries = [];
402
+ const previousOnCommit = hook.onCommitFiberRoot.bind(hook);
403
+ hook.onCommitFiberRoot = (rendererID, root, priorityLevel) => {
404
+ previousOnCommit(rendererID, root, priorityLevel);
405
+ const pendingEntries = collectPending(root.current, mode, showCauses);
406
+ for (let i = 0; i < pendingEntries.length; i++) {
407
+ const pendingEntry = pendingEntries[i];
408
+ const entry = {
409
+ component: pendingEntry.component,
410
+ path: pendingEntry.path,
411
+ duration: pendingEntry.duration,
412
+ timestamp: performance.now(),
413
+ causes: pendingEntry.causes
414
+ };
415
+ if (filter && !filter(entry)) continue;
416
+ if (entries.length >= bufferSize) entries.shift();
417
+ entries.push(entry);
418
+ batcher == null ? void 0 : batcher.push(pendingEntry);
419
+ }
420
+ if (!silent && pendingEntries.length > 0) {
421
+ console.groupCollapsed(
422
+ `%c⚛ ${pendingEntries.length} re-render${pendingEntries.length > 1 ? "s" : ""}`,
423
+ "color: #61dafb; font-weight: bold"
424
+ );
425
+ for (let i = 0; i < pendingEntries.length; i++) {
426
+ const pendingEntry = pendingEntries[i];
427
+ const durationText = pendingEntry.duration > 0 ? ` (${pendingEntry.duration.toFixed(2)}ms)` : "";
428
+ console.log(
429
+ `%c${pendingEntry.component}%c ${pendingEntry.path}${durationText}`,
430
+ "color: #e8e82e; font-weight: bold",
431
+ "color: #888"
432
+ );
433
+ if (showCauses && pendingEntry.causes.length > 0) {
434
+ const lines = formatCausesConsole(pendingEntry.causes);
435
+ for (const line of lines) {
436
+ console.log(`%c${line}`, "color: #aaa");
437
+ }
438
+ }
439
+ }
440
+ console.groupEnd();
441
+ }
442
+ };
443
+ const disconnect = () => {
444
+ hook.onCommitFiberRoot = previousOnCommit;
445
+ batcher == null ? void 0 : batcher.dispose();
446
+ disposeAllOverlays();
447
+ };
448
+ if (!silent) {
449
+ console.log(
450
+ "%c⚛ react-debug-updates attached",
451
+ "color: #61dafb; font-weight: bold"
452
+ );
453
+ }
454
+ return { entries, disconnect };
455
+ }
456
+ exports.attachRenderLogger = attachRenderLogger;
457
+ //# sourceMappingURL=react-debug-updates.cjs.map