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 +21 -0
- package/README.md +126 -0
- package/dist/batcher.d.ts +6 -0
- package/dist/causes.d.ts +2 -0
- package/dist/fiber.d.ts +13 -0
- package/dist/format.d.ts +6 -0
- package/dist/index.d.ts +2 -0
- package/dist/logger.d.ts +11 -0
- package/dist/overlay.d.ts +3 -0
- package/dist/react-debug-updates.cjs +457 -0
- package/dist/react-debug-updates.cjs.map +1 -0
- package/dist/react-debug-updates.js +457 -0
- package/dist/react-debug-updates.js.map +1 -0
- package/dist/types.d.ts +93 -0
- package/package.json +65 -0
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
|
+
 
|
|
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
|
+
};
|
package/dist/causes.d.ts
ADDED
package/dist/fiber.d.ts
ADDED
|
@@ -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[];
|
package/dist/format.d.ts
ADDED
|
@@ -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[];
|
package/dist/index.d.ts
ADDED
package/dist/logger.d.ts
ADDED
|
@@ -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,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
|