@presto1314w/vite-devtools-browser 0.3.5 → 0.3.6
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/dist/browser-collector.js +7 -6
- package/dist/browser-frameworks.d.ts +10 -0
- package/dist/browser-frameworks.js +63 -18
- package/dist/browser-session.d.ts +2 -0
- package/dist/browser-session.js +29 -1
- package/dist/browser.d.ts +6 -0
- package/dist/browser.js +39 -0
- package/dist/cli.js +47 -0
- package/dist/daemon.js +24 -0
- package/dist/paths.js +6 -2
- package/dist/react/devtools.d.ts +10 -0
- package/dist/react/devtools.js +18 -2
- package/dist/react/hook-manager.d.ts +2 -2
- package/dist/react/hook-manager.js +2 -2
- package/dist/react/hook.js +44 -16
- package/dist/react/profiler.d.ts +18 -35
- package/dist/react/profiler.js +113 -84
- package/package.json +1 -1
|
@@ -10,8 +10,11 @@ export function initBrowserEventCollector() {
|
|
|
10
10
|
q.shift();
|
|
11
11
|
};
|
|
12
12
|
}
|
|
13
|
+
const getVueApp = () => document.querySelector("#app")?.__vue_app__ ||
|
|
14
|
+
document.querySelector("[data-v-app]")?.__vue_app__ ||
|
|
15
|
+
null;
|
|
13
16
|
const inferFramework = () => {
|
|
14
|
-
if (window.__VUE__ || window.__VUE_DEVTOOLS_GLOBAL_HOOK__)
|
|
17
|
+
if (window.__VUE__ || window.__VUE_DEVTOOLS_GLOBAL_HOOK__ || getVueApp())
|
|
15
18
|
return "vue";
|
|
16
19
|
if (window.__REACT_DEVTOOLS_GLOBAL_HOOK__ || window.React)
|
|
17
20
|
return "react";
|
|
@@ -30,10 +33,8 @@ export function initBrowserEventCollector() {
|
|
|
30
33
|
const inferVueRenderDetails = () => {
|
|
31
34
|
const hook = window.__VUE_DEVTOOLS_GLOBAL_HOOK__;
|
|
32
35
|
const apps = hook?.apps;
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
const app = apps[0];
|
|
36
|
-
const rootInstance = app?._instance || app?._container?._vnode?.component;
|
|
36
|
+
const app = getVueApp() || (Array.isArray(apps) ? apps[0] : null);
|
|
37
|
+
const rootInstance = app?._instance || app?._container?._vnode?.component || app?._component?.subTree?.component;
|
|
37
38
|
if (!rootInstance)
|
|
38
39
|
return null;
|
|
39
40
|
const names = [];
|
|
@@ -120,7 +121,7 @@ export function initBrowserEventCollector() {
|
|
|
120
121
|
};
|
|
121
122
|
const attachPiniaSubscriptions = () => {
|
|
122
123
|
const hook = window.__VUE_DEVTOOLS_GLOBAL_HOOK__;
|
|
123
|
-
const app = Array.isArray(hook?.apps) ? hook.apps[0] : null;
|
|
124
|
+
const app = getVueApp() || (Array.isArray(hook?.apps) ? hook.apps[0] : null);
|
|
124
125
|
const pinia = window.__PINIA__ || window.pinia || app?.config?.globalProperties?.$pinia;
|
|
125
126
|
const registry = pinia?._s;
|
|
126
127
|
if (!(registry instanceof Map) || registry.size === 0)
|
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
import type { Page } from "playwright";
|
|
2
2
|
import type { BrowserFramework, BrowserSessionState } from "./browser-session.js";
|
|
3
|
+
export declare function detectFrameworkFromGlobals(globals: {
|
|
4
|
+
vueVersion?: string | null;
|
|
5
|
+
hasVueHook?: boolean;
|
|
6
|
+
hasVueAppMarker?: boolean;
|
|
7
|
+
hasReactGlobal?: boolean;
|
|
8
|
+
hasReactRootMarker?: boolean;
|
|
9
|
+
reactRendererVersion?: string | null;
|
|
10
|
+
svelteVersion?: string | null;
|
|
11
|
+
hasSvelteHook?: boolean;
|
|
12
|
+
}): string;
|
|
3
13
|
export declare function detectBrowserFramework(page: Page): Promise<{
|
|
4
14
|
detected: string;
|
|
5
15
|
framework: BrowserFramework;
|
|
@@ -1,26 +1,51 @@
|
|
|
1
1
|
import * as vueDevtools from "./vue/devtools.js";
|
|
2
2
|
import * as reactDevtools from "./react/devtools.js";
|
|
3
|
+
import { checkHookHealth, injectHook } from "./react/hook-manager.js";
|
|
3
4
|
import * as svelteDevtools from "./svelte/devtools.js";
|
|
5
|
+
export function detectFrameworkFromGlobals(globals) {
|
|
6
|
+
if (globals.vueVersion || globals.hasVueHook || globals.hasVueAppMarker) {
|
|
7
|
+
return `vue@${globals.vueVersion || "unknown"}`;
|
|
8
|
+
}
|
|
9
|
+
if (globals.reactRendererVersion || globals.hasReactGlobal || globals.hasReactRootMarker) {
|
|
10
|
+
return `react@${globals.reactRendererVersion || "unknown"}`;
|
|
11
|
+
}
|
|
12
|
+
if (globals.svelteVersion || globals.hasSvelteHook) {
|
|
13
|
+
return `svelte@${globals.svelteVersion || "unknown"}`;
|
|
14
|
+
}
|
|
15
|
+
return "unknown";
|
|
16
|
+
}
|
|
4
17
|
export async function detectBrowserFramework(page) {
|
|
18
|
+
await page.waitForFunction?.(() => Boolean(window.__VUE__ ||
|
|
19
|
+
window.__VUE_DEVTOOLS_GLOBAL_HOOK__ ||
|
|
20
|
+
window.React ||
|
|
21
|
+
window.__SVELTE__ ||
|
|
22
|
+
window.__svelte ||
|
|
23
|
+
document.querySelector("[data-v-app]") ||
|
|
24
|
+
document.querySelector("#app")?.__vue_app__ ||
|
|
25
|
+
document.querySelector("[data-reactroot]")), undefined, { timeout: 1_000 }).catch(() => { });
|
|
5
26
|
const detected = await page.evaluate(() => {
|
|
6
|
-
if (window.__VUE__ || window.__VUE_DEVTOOLS_GLOBAL_HOOK__) {
|
|
7
|
-
const version = window.__VUE__?.version || "unknown";
|
|
8
|
-
return `vue@${version}`;
|
|
9
|
-
}
|
|
10
27
|
const reactHook = window.__REACT_DEVTOOLS_GLOBAL_HOOK__;
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
28
|
+
const firstRenderer = reactHook?.renderers?.values?.().next?.().value ?? null;
|
|
29
|
+
const globals = {
|
|
30
|
+
vueVersion: window.__VUE__?.version ?? null,
|
|
31
|
+
hasVueHook: Boolean(window.__VUE_DEVTOOLS_GLOBAL_HOOK__),
|
|
32
|
+
hasVueAppMarker: Boolean(document.querySelector("#app")?.__vue_app__ ||
|
|
33
|
+
document.querySelector("[data-v-app]")?.__vue_app__ ||
|
|
34
|
+
document.querySelector("[data-v-app]")),
|
|
35
|
+
hasReactGlobal: Boolean(window.React),
|
|
36
|
+
hasReactRootMarker: Boolean(document.querySelector("[data-reactroot]")),
|
|
37
|
+
reactRendererVersion: firstRenderer?.version ?? window.React?.version ?? null,
|
|
38
|
+
svelteVersion: window.__SVELTE__?.VERSION || window.__svelte?.version || null,
|
|
39
|
+
hasSvelteHook: Boolean(window.__SVELTE_DEVTOOLS_GLOBAL_HOOK__),
|
|
40
|
+
};
|
|
41
|
+
if (globals.vueVersion || globals.hasVueHook || globals.hasVueAppMarker) {
|
|
42
|
+
return `vue@${globals.vueVersion || "unknown"}`;
|
|
16
43
|
}
|
|
17
|
-
if (
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
"unknown";
|
|
23
|
-
return `svelte@${version}`;
|
|
44
|
+
if (globals.reactRendererVersion || globals.hasReactGlobal || globals.hasReactRootMarker) {
|
|
45
|
+
return `react@${globals.reactRendererVersion || "unknown"}`;
|
|
46
|
+
}
|
|
47
|
+
if (globals.svelteVersion || globals.hasSvelteHook) {
|
|
48
|
+
return `svelte@${globals.svelteVersion || "unknown"}`;
|
|
24
49
|
}
|
|
25
50
|
return "unknown";
|
|
26
51
|
});
|
|
@@ -40,13 +65,13 @@ export async function inspectVueRouter(page) {
|
|
|
40
65
|
}
|
|
41
66
|
export async function inspectReactTree(state, page, id) {
|
|
42
67
|
if (!id) {
|
|
43
|
-
state.lastReactSnapshot = await reactDevtools.snapshot(page);
|
|
68
|
+
state.lastReactSnapshot = await withReactInspectorRecovery(page, () => reactDevtools.snapshot(page));
|
|
44
69
|
return reactDevtools.format(state.lastReactSnapshot);
|
|
45
70
|
}
|
|
46
71
|
const parsed = Number.parseInt(id, 10);
|
|
47
72
|
if (!Number.isFinite(parsed))
|
|
48
73
|
throw new Error("react component id must be a number");
|
|
49
|
-
const inspected = await reactDevtools.inspect(page, parsed);
|
|
74
|
+
const inspected = await withReactInspectorRecovery(page, () => reactDevtools.inspect(page, parsed));
|
|
50
75
|
const lines = [];
|
|
51
76
|
const componentPath = reactDevtools.path(state.lastReactSnapshot, parsed);
|
|
52
77
|
if (componentPath)
|
|
@@ -61,6 +86,26 @@ export async function inspectReactTree(state, page, id) {
|
|
|
61
86
|
export async function inspectSvelteTree(page, id) {
|
|
62
87
|
return id ? svelteDevtools.getComponentDetails(page, id) : svelteDevtools.getComponentTree(page);
|
|
63
88
|
}
|
|
89
|
+
async function withReactInspectorRecovery(page, read) {
|
|
90
|
+
try {
|
|
91
|
+
return await read();
|
|
92
|
+
}
|
|
93
|
+
catch (error) {
|
|
94
|
+
if (!isRecoverableReactInspectorError(error))
|
|
95
|
+
throw error;
|
|
96
|
+
const health = await checkHookHealth(page);
|
|
97
|
+
if (!health.installed) {
|
|
98
|
+
await injectHook(page);
|
|
99
|
+
}
|
|
100
|
+
await page.waitForTimeout(60).catch(() => { });
|
|
101
|
+
return read();
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
function isRecoverableReactInspectorError(error) {
|
|
105
|
+
if (!(error instanceof Error))
|
|
106
|
+
return false;
|
|
107
|
+
return /hook not installed|no React renderer attached/i.test(error.message);
|
|
108
|
+
}
|
|
64
109
|
function toBrowserFramework(detected) {
|
|
65
110
|
if (detected.startsWith("vue"))
|
|
66
111
|
return "vue";
|
|
@@ -45,3 +45,5 @@ export declare function isClosedTargetError(error: unknown): boolean;
|
|
|
45
45
|
* process crashes on older drivers.
|
|
46
46
|
*/
|
|
47
47
|
export declare function platformChromiumArgs(extra?: string[]): string[];
|
|
48
|
+
export declare function resolveChromiumExecutablePath(env?: NodeJS.ProcessEnv): string | undefined;
|
|
49
|
+
export declare function resolveBrowserHeadless(env?: NodeJS.ProcessEnv): boolean;
|
package/dist/browser-session.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
1
2
|
import { chromium } from "playwright";
|
|
2
3
|
import { getHookSource } from "./react/hook-manager.js";
|
|
3
4
|
import { isWindows, isLinux } from "./paths.js";
|
|
@@ -94,9 +95,36 @@ export function platformChromiumArgs(extra = []) {
|
|
|
94
95
|
}
|
|
95
96
|
return args;
|
|
96
97
|
}
|
|
98
|
+
export function resolveChromiumExecutablePath(env = process.env) {
|
|
99
|
+
const explicit = env.VITE_BROWSER_EXECUTABLE_PATH || env.PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH;
|
|
100
|
+
if (explicit && existsSync(explicit))
|
|
101
|
+
return explicit;
|
|
102
|
+
const candidates = isWindows
|
|
103
|
+
? [
|
|
104
|
+
`${env.PROGRAMFILES ?? "C:\\Program Files"}\\Google\\Chrome\\Application\\chrome.exe`,
|
|
105
|
+
`${env["PROGRAMFILES(X86)"] ?? "C:\\Program Files (x86)"}\\Google\\Chrome\\Application\\chrome.exe`,
|
|
106
|
+
`${env.LOCALAPPDATA ?? ""}\\Google\\Chrome\\Application\\chrome.exe`,
|
|
107
|
+
]
|
|
108
|
+
: isLinux
|
|
109
|
+
? [
|
|
110
|
+
"/usr/bin/google-chrome",
|
|
111
|
+
"/usr/bin/chromium",
|
|
112
|
+
"/usr/bin/chromium-browser",
|
|
113
|
+
"/snap/bin/chromium",
|
|
114
|
+
]
|
|
115
|
+
: [
|
|
116
|
+
"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
|
|
117
|
+
"/Applications/Chromium.app/Contents/MacOS/Chromium",
|
|
118
|
+
];
|
|
119
|
+
return candidates.find((candidate) => Boolean(candidate) && existsSync(candidate));
|
|
120
|
+
}
|
|
121
|
+
export function resolveBrowserHeadless(env = process.env) {
|
|
122
|
+
return /^(1|true|yes)$/i.test(env.VITE_BROWSER_HEADLESS || "");
|
|
123
|
+
}
|
|
97
124
|
async function launchBrowserContext() {
|
|
98
125
|
const browser = await chromium.launch({
|
|
99
|
-
headless:
|
|
126
|
+
headless: resolveBrowserHeadless(),
|
|
127
|
+
executablePath: resolveChromiumExecutablePath(),
|
|
100
128
|
args: platformChromiumArgs(),
|
|
101
129
|
});
|
|
102
130
|
const context = await browser.newContext({ viewport: { width: 1280, height: 720 } });
|
package/dist/browser.d.ts
CHANGED
|
@@ -40,6 +40,12 @@ export declare function vueTree(id?: string): Promise<string>;
|
|
|
40
40
|
export declare function vuePinia(store?: string): Promise<string>;
|
|
41
41
|
export declare function vueRouter(): Promise<string>;
|
|
42
42
|
export declare function reactTree(id?: string): Promise<string>;
|
|
43
|
+
export declare function reactStoreList(): Promise<string>;
|
|
44
|
+
export declare function reactStoreInspect(store: string): Promise<string>;
|
|
45
|
+
export declare function reactHookHealth(): Promise<string>;
|
|
46
|
+
export declare function reactHookInject(): Promise<string>;
|
|
47
|
+
export declare function reactCommits(limit?: number): Promise<string>;
|
|
48
|
+
export declare function reactCommitsClear(): Promise<string>;
|
|
43
49
|
export declare function svelteTree(id?: string): Promise<string>;
|
|
44
50
|
export declare function viteRestart(): Promise<string>;
|
|
45
51
|
export declare function viteHMR(): Promise<string>;
|
package/dist/browser.js
CHANGED
|
@@ -5,6 +5,9 @@ import { detectBrowserFramework, inspectReactTree, inspectSvelteTree, inspectVue
|
|
|
5
5
|
import { closeBrowserSession, createBrowserSessionState, ensureBrowserPage, getCurrentPage as getSessionPage, } from "./browser-session.js";
|
|
6
6
|
import { collectModuleRows, formatHmrTrace, formatModuleGraphSnapshot, formatModuleGraphTrace, formatRuntimeStatus, readOverlayError, readRuntimeSnapshot, } from "./browser-vite.js";
|
|
7
7
|
import * as networkLog from "./network.js";
|
|
8
|
+
import { checkHookHealth, formatHookHealth, injectHook } from "./react/hook-manager.js";
|
|
9
|
+
import { clearRenderHistory, formatRenderInfo, getRecentRenders, installRenderTracking } from "./react/profiler.js";
|
|
10
|
+
import { formatStoreInspection, formatStoreList, inspectStore, listStores } from "./react/zustand.js";
|
|
8
11
|
import { resolveViaSourceMap } from "./sourcemap.js";
|
|
9
12
|
export { contextUsable, isClosedTargetError } from "./browser-session.js";
|
|
10
13
|
export { formatHmrTrace, formatModuleGraphSnapshot, formatModuleGraphTrace, formatRuntimeStatus, normalizeLimit, } from "./browser-vite.js";
|
|
@@ -189,6 +192,36 @@ export async function vueRouter() {
|
|
|
189
192
|
export async function reactTree(id) {
|
|
190
193
|
return inspectReactTree(session, requireCurrentPage(), id);
|
|
191
194
|
}
|
|
195
|
+
export async function reactStoreList() {
|
|
196
|
+
return formatStoreList(await listStores(requireCurrentPage()));
|
|
197
|
+
}
|
|
198
|
+
export async function reactStoreInspect(store) {
|
|
199
|
+
const result = await inspectStore(requireCurrentPage(), store);
|
|
200
|
+
if (!result) {
|
|
201
|
+
throw new Error(`zustand store not found: ${store}`);
|
|
202
|
+
}
|
|
203
|
+
return formatStoreInspection(result);
|
|
204
|
+
}
|
|
205
|
+
export async function reactHookHealth() {
|
|
206
|
+
return formatHookHealth(await checkHookHealth(requireCurrentPage()));
|
|
207
|
+
}
|
|
208
|
+
export async function reactHookInject() {
|
|
209
|
+
const currentPage = requireCurrentPage();
|
|
210
|
+
const injected = await injectHook(currentPage);
|
|
211
|
+
const status = await checkHookHealth(currentPage);
|
|
212
|
+
const prefix = injected ? "React hook injected.\n\n" : "React hook already installed.\n\n";
|
|
213
|
+
return `${prefix}${formatHookHealth(status)}`;
|
|
214
|
+
}
|
|
215
|
+
export async function reactCommits(limit = 20) {
|
|
216
|
+
const currentPage = requireCurrentPage();
|
|
217
|
+
await installRenderTracking(currentPage);
|
|
218
|
+
return formatRenderInfo(await getRecentRenders(currentPage, limit));
|
|
219
|
+
}
|
|
220
|
+
export async function reactCommitsClear() {
|
|
221
|
+
const currentPage = requireCurrentPage();
|
|
222
|
+
await clearRenderHistory(currentPage);
|
|
223
|
+
return "cleared React commit history";
|
|
224
|
+
}
|
|
192
225
|
export async function svelteTree(id) {
|
|
193
226
|
return inspectSvelteTree(requireCurrentPage(), id);
|
|
194
227
|
}
|
|
@@ -210,6 +243,10 @@ export async function viteHMR() {
|
|
|
210
243
|
}
|
|
211
244
|
export async function viteRuntimeStatus() {
|
|
212
245
|
const currentPage = requireCurrentPage();
|
|
246
|
+
if (session.framework === "unknown") {
|
|
247
|
+
const result = await detectBrowserFramework(currentPage);
|
|
248
|
+
session.framework = result.framework;
|
|
249
|
+
}
|
|
213
250
|
const runtime = await readRuntimeSnapshot(currentPage);
|
|
214
251
|
return formatRuntimeStatus(runtime, session.framework, session.hmrEvents);
|
|
215
252
|
}
|
|
@@ -334,6 +371,8 @@ async function navigateAndRefreshContext(currentPage, navigate, refreshFramework
|
|
|
334
371
|
clearRuntimeErrors();
|
|
335
372
|
await injectEventCollector(currentPage);
|
|
336
373
|
if (refreshFramework) {
|
|
374
|
+
await currentPage.waitForLoadState?.("networkidle", { timeout: 1_000 }).catch(() => { });
|
|
375
|
+
await currentPage.waitForTimeout?.(100).catch(() => { });
|
|
337
376
|
await detectFramework();
|
|
338
377
|
}
|
|
339
378
|
}
|
package/dist/cli.js
CHANGED
|
@@ -97,6 +97,47 @@ export async function runCli(argv, io) {
|
|
|
97
97
|
const res = await io.send("react-tree", { id });
|
|
98
98
|
exit(io, res, res.ok && res.data ? String(res.data) : "");
|
|
99
99
|
}
|
|
100
|
+
if (cmd === "react" && arg === "store") {
|
|
101
|
+
const sub = args[2];
|
|
102
|
+
if (sub === "list") {
|
|
103
|
+
const res = await io.send("react-store-list");
|
|
104
|
+
exit(io, res, res.ok && res.data ? String(res.data) : "");
|
|
105
|
+
}
|
|
106
|
+
if (sub === "inspect") {
|
|
107
|
+
const store = args[3];
|
|
108
|
+
if (!store) {
|
|
109
|
+
io.stderr("usage: vite-browser react store inspect <name>");
|
|
110
|
+
io.exit(1);
|
|
111
|
+
}
|
|
112
|
+
const res = await io.send("react-store-inspect", { store });
|
|
113
|
+
exit(io, res, res.ok && res.data ? String(res.data) : "");
|
|
114
|
+
}
|
|
115
|
+
io.stderr("usage: vite-browser react store <list|inspect> [name]");
|
|
116
|
+
io.exit(1);
|
|
117
|
+
}
|
|
118
|
+
if (cmd === "react" && arg === "hook") {
|
|
119
|
+
const sub = args[2];
|
|
120
|
+
if (sub === "health") {
|
|
121
|
+
const res = await io.send("react-hook-health");
|
|
122
|
+
exit(io, res, res.ok && res.data ? String(res.data) : "");
|
|
123
|
+
}
|
|
124
|
+
if (sub === "inject") {
|
|
125
|
+
const res = await io.send("react-hook-inject");
|
|
126
|
+
exit(io, res, res.ok && res.data ? String(res.data) : "");
|
|
127
|
+
}
|
|
128
|
+
io.stderr("usage: vite-browser react hook <health|inject>");
|
|
129
|
+
io.exit(1);
|
|
130
|
+
}
|
|
131
|
+
if (cmd === "react" && arg === "commits") {
|
|
132
|
+
const sub = args[2];
|
|
133
|
+
if (sub === "clear") {
|
|
134
|
+
const res = await io.send("react-commits-clear");
|
|
135
|
+
exit(io, res, res.ok && res.data ? String(res.data) : "cleared React commit history");
|
|
136
|
+
}
|
|
137
|
+
const limit = parseNumberFlag(args, "--limit", 20);
|
|
138
|
+
const res = await io.send("react-commits", { limit });
|
|
139
|
+
exit(io, res, res.ok && res.data ? String(res.data) : "");
|
|
140
|
+
}
|
|
100
141
|
if (cmd === "svelte" && arg === "tree") {
|
|
101
142
|
const id = args[2];
|
|
102
143
|
const res = await io.send("svelte-tree", { id });
|
|
@@ -228,6 +269,12 @@ VUE COMMANDS
|
|
|
228
269
|
|
|
229
270
|
REACT COMMANDS
|
|
230
271
|
react tree [id] Show React component tree or inspect component
|
|
272
|
+
react store list List detected Zustand stores
|
|
273
|
+
react store inspect <name> Show Zustand store state and actions
|
|
274
|
+
react hook health Show bundled React hook status
|
|
275
|
+
react hook inject Inject bundled React hook into current page
|
|
276
|
+
react commits [--limit <n>] Show recent React commit records
|
|
277
|
+
react commits clear Clear recorded React commit history
|
|
231
278
|
|
|
232
279
|
SVELTE COMMANDS
|
|
233
280
|
svelte tree [id] Show Svelte component tree or inspect component
|
package/dist/daemon.js
CHANGED
|
@@ -71,6 +71,30 @@ export function createRunner(api = browser) {
|
|
|
71
71
|
const data = await api.reactTree(cmd.id);
|
|
72
72
|
return { ok: true, data };
|
|
73
73
|
}
|
|
74
|
+
if (cmd.action === "react-store-list") {
|
|
75
|
+
const data = await api.reactStoreList();
|
|
76
|
+
return { ok: true, data };
|
|
77
|
+
}
|
|
78
|
+
if (cmd.action === "react-store-inspect") {
|
|
79
|
+
const data = await api.reactStoreInspect(cmd.store);
|
|
80
|
+
return { ok: true, data };
|
|
81
|
+
}
|
|
82
|
+
if (cmd.action === "react-hook-health") {
|
|
83
|
+
const data = await api.reactHookHealth();
|
|
84
|
+
return { ok: true, data };
|
|
85
|
+
}
|
|
86
|
+
if (cmd.action === "react-hook-inject") {
|
|
87
|
+
const data = await api.reactHookInject();
|
|
88
|
+
return { ok: true, data };
|
|
89
|
+
}
|
|
90
|
+
if (cmd.action === "react-commits") {
|
|
91
|
+
const data = await api.reactCommits(cmd.limit ?? 20);
|
|
92
|
+
return { ok: true, data };
|
|
93
|
+
}
|
|
94
|
+
if (cmd.action === "react-commits-clear") {
|
|
95
|
+
const data = await api.reactCommitsClear();
|
|
96
|
+
return { ok: true, data };
|
|
97
|
+
}
|
|
74
98
|
// Svelte commands
|
|
75
99
|
if (cmd.action === "svelte-tree") {
|
|
76
100
|
const data = await api.svelteTree(cmd.id);
|
package/dist/paths.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { accessSync, constants } from "node:fs";
|
|
1
2
|
import { homedir, tmpdir } from "node:os";
|
|
2
3
|
import { join } from "node:path";
|
|
3
4
|
export const isWindows = process.platform === "win32";
|
|
@@ -19,11 +20,14 @@ const session = sanitizeSession(process.env.VITE_BROWSER_SESSION || "default");
|
|
|
19
20
|
export function resolveSocketDir() {
|
|
20
21
|
try {
|
|
21
22
|
const home = homedir();
|
|
22
|
-
if (home)
|
|
23
|
+
if (home) {
|
|
24
|
+
accessSync(home, constants.W_OK);
|
|
23
25
|
return join(home, ".vite-browser");
|
|
26
|
+
}
|
|
24
27
|
}
|
|
25
28
|
catch {
|
|
26
|
-
// homedir() can throw
|
|
29
|
+
// homedir() can throw, and sandboxed / CI environments may expose a
|
|
30
|
+
// home directory that is present but not writable.
|
|
27
31
|
}
|
|
28
32
|
// Fallback: use tmpdir scoped by uid to avoid collisions
|
|
29
33
|
const uid = process.getuid?.() ?? process.pid;
|
package/dist/react/devtools.d.ts
CHANGED
|
@@ -12,6 +12,16 @@ export type ReactInspection = {
|
|
|
12
12
|
};
|
|
13
13
|
export declare function snapshot(page: Page): Promise<ReactNode[]>;
|
|
14
14
|
export declare function inspect(page: Page, id: number): Promise<ReactInspection>;
|
|
15
|
+
export declare function getPrimaryRendererInterface(hook: {
|
|
16
|
+
rendererInterfaces?: Map<unknown, unknown> | {
|
|
17
|
+
values?: () => IterableIterator<unknown>;
|
|
18
|
+
};
|
|
19
|
+
} | null | undefined): unknown;
|
|
20
|
+
export declare function findRendererInterfaceForElement(hook: {
|
|
21
|
+
rendererInterfaces?: Map<unknown, unknown> | {
|
|
22
|
+
values?: () => IterableIterator<unknown>;
|
|
23
|
+
};
|
|
24
|
+
} | null | undefined, id: number): unknown;
|
|
15
25
|
export declare function format(nodes: ReactNode[]): string;
|
|
16
26
|
export declare function path(nodes: ReactNode[], id: number): string;
|
|
17
27
|
export declare function typeName(type: number): string;
|
package/dist/react/devtools.js
CHANGED
|
@@ -4,6 +4,21 @@ export async function snapshot(page) {
|
|
|
4
4
|
export async function inspect(page, id) {
|
|
5
5
|
return page.evaluate(inPageInspect, id);
|
|
6
6
|
}
|
|
7
|
+
export function getPrimaryRendererInterface(hook) {
|
|
8
|
+
const values = hook?.rendererInterfaces?.values?.();
|
|
9
|
+
return values?.next().value ?? null;
|
|
10
|
+
}
|
|
11
|
+
export function findRendererInterfaceForElement(hook, id) {
|
|
12
|
+
const values = hook?.rendererInterfaces?.values?.();
|
|
13
|
+
if (!values)
|
|
14
|
+
return null;
|
|
15
|
+
for (const rendererInterface of values) {
|
|
16
|
+
if (rendererInterface?.hasElementWithId?.(id)) {
|
|
17
|
+
return rendererInterface;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
7
22
|
export function format(nodes) {
|
|
8
23
|
const children = new Map();
|
|
9
24
|
for (const n of nodes) {
|
|
@@ -182,7 +197,7 @@ async function inPageSnapshot() {
|
|
|
182
197
|
const hook = window.__REACT_DEVTOOLS_GLOBAL_HOOK__;
|
|
183
198
|
if (!hook)
|
|
184
199
|
throw new Error("React DevTools hook not installed");
|
|
185
|
-
const ri = hook
|
|
200
|
+
const ri = getPrimaryRendererInterface(hook);
|
|
186
201
|
if (!ri)
|
|
187
202
|
throw new Error("no React renderer attached");
|
|
188
203
|
const batches = await collect(ri);
|
|
@@ -207,7 +222,8 @@ async function inPageSnapshot() {
|
|
|
207
222
|
}
|
|
208
223
|
function inPageInspect(id) {
|
|
209
224
|
const hook = window.__REACT_DEVTOOLS_GLOBAL_HOOK__;
|
|
210
|
-
const ri = hook
|
|
225
|
+
const ri = findRendererInterfaceForElement(hook, id) ??
|
|
226
|
+
getPrimaryRendererInterface(hook);
|
|
211
227
|
if (!ri)
|
|
212
228
|
throw new Error("no React renderer attached");
|
|
213
229
|
if (!ri.hasElementWithId(id))
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* React DevTools Hook Management
|
|
3
3
|
*
|
|
4
|
-
* Provides health checks and
|
|
4
|
+
* Provides health checks and injection helpers for the bundled React DevTools hook.
|
|
5
5
|
* This removes the dependency on external browser extensions for React inspection.
|
|
6
6
|
*/
|
|
7
7
|
import type { Page } from "playwright";
|
|
@@ -20,7 +20,7 @@ export interface HookHealthStatus {
|
|
|
20
20
|
*/
|
|
21
21
|
export declare function checkHookHealth(page: Page): Promise<HookHealthStatus>;
|
|
22
22
|
/**
|
|
23
|
-
* Inject the React DevTools hook into a page if not already present
|
|
23
|
+
* Inject the React DevTools hook into a page if not already present.
|
|
24
24
|
*/
|
|
25
25
|
export declare function injectHook(page: Page): Promise<boolean>;
|
|
26
26
|
/**
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* React DevTools Hook Management
|
|
3
3
|
*
|
|
4
|
-
* Provides health checks and
|
|
4
|
+
* Provides health checks and injection helpers for the bundled React DevTools hook.
|
|
5
5
|
* This removes the dependency on external browser extensions for React inspection.
|
|
6
6
|
*/
|
|
7
7
|
import { readFileSync } from "node:fs";
|
|
@@ -26,7 +26,7 @@ export async function checkHookHealth(page) {
|
|
|
26
26
|
return page.evaluate(inPageCheckHookHealth);
|
|
27
27
|
}
|
|
28
28
|
/**
|
|
29
|
-
* Inject the React DevTools hook into a page if not already present
|
|
29
|
+
* Inject the React DevTools hook into a page if not already present.
|
|
30
30
|
*/
|
|
31
31
|
export async function injectHook(page) {
|
|
32
32
|
const alreadyInstalled = await page.evaluate(() => !!window.__REACT_DEVTOOLS_GLOBAL_HOOK__);
|
package/dist/react/hook.js
CHANGED
|
@@ -18,13 +18,14 @@
|
|
|
18
18
|
return;
|
|
19
19
|
if (window.__REACT_DEVTOOLS_GLOBAL_HOOK__)
|
|
20
20
|
return;
|
|
21
|
+
let nextRendererId = 1;
|
|
21
22
|
const hook = {
|
|
22
23
|
renderers: new Map(),
|
|
23
24
|
rendererInterfaces: new Map(),
|
|
24
25
|
supportsFiber: true,
|
|
25
26
|
// Called by React when a renderer is injected
|
|
26
27
|
inject(renderer) {
|
|
27
|
-
const id =
|
|
28
|
+
const id = nextRendererId++;
|
|
28
29
|
hook.renderers.set(id, renderer);
|
|
29
30
|
// Create renderer interface for component inspection
|
|
30
31
|
const rendererInterface = createRendererInterface(renderer);
|
|
@@ -33,11 +34,13 @@
|
|
|
33
34
|
},
|
|
34
35
|
// Called by React on fiber root commits
|
|
35
36
|
onCommitFiberRoot(id, root, priorityLevel) {
|
|
36
|
-
|
|
37
|
+
const rendererInterface = hook.rendererInterfaces.get(id);
|
|
38
|
+
rendererInterface?.trackFiberRoot(root);
|
|
37
39
|
},
|
|
38
40
|
// Called by React on fiber unmount
|
|
39
41
|
onCommitFiberUnmount(id, fiber) {
|
|
40
|
-
|
|
42
|
+
const rendererInterface = hook.rendererInterfaces.get(id);
|
|
43
|
+
rendererInterface?.handleCommitFiberUnmount(fiber);
|
|
41
44
|
},
|
|
42
45
|
// Sub-hooks for advanced features
|
|
43
46
|
sub(event, handler) {
|
|
@@ -58,12 +61,25 @@
|
|
|
58
61
|
const fiberRoots = new Set();
|
|
59
62
|
const fiberToId = new Map();
|
|
60
63
|
const idToFiber = new Map();
|
|
61
|
-
|
|
64
|
+
const fiberParents = new Map();
|
|
65
|
+
let nextId = 2;
|
|
62
66
|
return {
|
|
67
|
+
trackFiberRoot(root) {
|
|
68
|
+
if (root && root.current) {
|
|
69
|
+
fiberRoots.add(root);
|
|
70
|
+
}
|
|
71
|
+
},
|
|
72
|
+
handleCommitFiberUnmount(fiber) {
|
|
73
|
+
pruneFiber(fiber);
|
|
74
|
+
},
|
|
63
75
|
// Flush initial operations to populate component tree
|
|
64
76
|
flushInitialOperations() {
|
|
77
|
+
fiberToId.clear();
|
|
78
|
+
idToFiber.clear();
|
|
79
|
+
fiberParents.clear();
|
|
80
|
+
nextId = 2;
|
|
65
81
|
for (const root of fiberRoots) {
|
|
66
|
-
walkTree(root.current,
|
|
82
|
+
walkTree(root.current, 1);
|
|
67
83
|
}
|
|
68
84
|
// Send operations via message event
|
|
69
85
|
const operations = buildOperations();
|
|
@@ -113,6 +129,7 @@
|
|
|
113
129
|
const id = nextId++;
|
|
114
130
|
fiberToId.set(fiber, id);
|
|
115
131
|
idToFiber.set(id, fiber);
|
|
132
|
+
fiberParents.set(fiber, parentId);
|
|
116
133
|
// Walk children
|
|
117
134
|
let child = fiber.child;
|
|
118
135
|
while (child) {
|
|
@@ -121,29 +138,25 @@
|
|
|
121
138
|
}
|
|
122
139
|
}
|
|
123
140
|
function buildOperations() {
|
|
124
|
-
const ops = [2, 0]; // version, string table size
|
|
125
141
|
const strings = [];
|
|
126
|
-
|
|
127
|
-
ops.push(1, 1, 11, 0, 0, 0, 0); // ADD_ROOT
|
|
142
|
+
const nodeOps = [];
|
|
128
143
|
// Add fiber nodes
|
|
129
144
|
for (const [id, fiber] of idToFiber) {
|
|
130
|
-
if (id === 1)
|
|
131
|
-
continue; // Skip root
|
|
132
145
|
const name = getDisplayName(fiber);
|
|
133
146
|
const nameIdx = addString(name);
|
|
134
147
|
const keyIdx = fiber.key ? addString(String(fiber.key)) : 0;
|
|
135
|
-
const parentId =
|
|
136
|
-
|
|
148
|
+
const parentId = fiberParents.get(fiber) || 0;
|
|
149
|
+
nodeOps.push(1, id, 0, parentId, 0, nameIdx, keyIdx, 0); // ADD_NODE
|
|
137
150
|
}
|
|
138
|
-
// Update string table size
|
|
139
|
-
ops[1] = strings.length;
|
|
140
|
-
// Insert string table after version
|
|
141
151
|
const stringOps = [];
|
|
142
152
|
for (const str of strings) {
|
|
143
153
|
const codePoints = Array.from(str).map(c => c.codePointAt(0));
|
|
144
154
|
stringOps.push(codePoints.length, ...codePoints);
|
|
145
155
|
}
|
|
146
|
-
ops
|
|
156
|
+
const ops = [0, 0, stringOps.length, ...stringOps];
|
|
157
|
+
// Add root operation
|
|
158
|
+
ops.push(1, 1, 11, 0, 0, 0, 0); // ADD_ROOT
|
|
159
|
+
ops.push(...nodeOps);
|
|
147
160
|
return ops;
|
|
148
161
|
function addString(str) {
|
|
149
162
|
let idx = strings.indexOf(str);
|
|
@@ -215,6 +228,21 @@
|
|
|
215
228
|
const { fileName, lineNumber, columnNumber } = fiber._debugSource;
|
|
216
229
|
return [null, fileName, lineNumber, columnNumber];
|
|
217
230
|
}
|
|
231
|
+
function pruneFiber(fiber) {
|
|
232
|
+
if (!fiber)
|
|
233
|
+
return;
|
|
234
|
+
const mappedId = fiberToId.get(fiber);
|
|
235
|
+
if (mappedId != null) {
|
|
236
|
+
fiberToId.delete(fiber);
|
|
237
|
+
idToFiber.delete(mappedId);
|
|
238
|
+
fiberParents.delete(fiber);
|
|
239
|
+
}
|
|
240
|
+
let child = fiber.child;
|
|
241
|
+
while (child) {
|
|
242
|
+
pruneFiber(child);
|
|
243
|
+
child = child.sibling;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
218
246
|
}
|
|
219
247
|
// Install the hook
|
|
220
248
|
Object.defineProperty(window, "__REACT_DEVTOOLS_GLOBAL_HOOK__", {
|
package/dist/react/profiler.d.ts
CHANGED
|
@@ -1,55 +1,38 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* React
|
|
2
|
+
* React commit tracking groundwork.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* This module records real commit metadata that can be observed from the
|
|
5
|
+
* React DevTools hook without pretending to expose a full Profiler surface.
|
|
5
6
|
*/
|
|
6
7
|
import type { Page } from "playwright";
|
|
8
|
+
export interface RenderInteraction {
|
|
9
|
+
id: number;
|
|
10
|
+
name: string;
|
|
11
|
+
timestamp: number;
|
|
12
|
+
}
|
|
7
13
|
export interface RenderInfo {
|
|
8
|
-
|
|
9
|
-
|
|
14
|
+
rendererId: number;
|
|
15
|
+
rootName: string;
|
|
10
16
|
phase: "mount" | "update" | "nested-update";
|
|
11
|
-
actualDuration: number;
|
|
12
|
-
baseDuration: number;
|
|
13
|
-
startTime: number;
|
|
17
|
+
actualDuration: number | null;
|
|
18
|
+
baseDuration: number | null;
|
|
19
|
+
startTime: number | null;
|
|
14
20
|
commitTime: number;
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
name: string;
|
|
18
|
-
timestamp: number;
|
|
19
|
-
}>;
|
|
21
|
+
fiberCount: number;
|
|
22
|
+
interactions: RenderInteraction[];
|
|
20
23
|
}
|
|
21
24
|
export interface RenderTrigger {
|
|
22
|
-
|
|
23
|
-
|
|
25
|
+
rendererId: number;
|
|
26
|
+
rootName: string;
|
|
24
27
|
reason: "props" | "state" | "hooks" | "parent" | "context" | "unknown";
|
|
25
28
|
timestamp: number;
|
|
26
29
|
details?: string;
|
|
27
30
|
}
|
|
28
|
-
/**
|
|
29
|
-
* Install render tracking in the page
|
|
30
|
-
*/
|
|
31
31
|
export declare function installRenderTracking(page: Page): Promise<void>;
|
|
32
|
-
/**
|
|
33
|
-
* Get recent render events
|
|
34
|
-
*/
|
|
35
32
|
export declare function getRecentRenders(page: Page, limit?: number): Promise<RenderInfo[]>;
|
|
36
|
-
/**
|
|
37
|
-
* Get render triggers (why components re-rendered)
|
|
38
|
-
*/
|
|
39
33
|
export declare function getRenderTriggers(page: Page, limit?: number): Promise<RenderTrigger[]>;
|
|
40
|
-
/**
|
|
41
|
-
* Clear render history
|
|
42
|
-
*/
|
|
43
34
|
export declare function clearRenderHistory(page: Page): Promise<void>;
|
|
44
|
-
|
|
45
|
-
* Format render info for CLI output
|
|
46
|
-
*/
|
|
35
|
+
export declare function formatDuration(duration: number | null): string;
|
|
47
36
|
export declare function formatRenderInfo(renders: RenderInfo[]): string;
|
|
48
|
-
/**
|
|
49
|
-
* Format render triggers for CLI output
|
|
50
|
-
*/
|
|
51
37
|
export declare function formatRenderTriggers(triggers: RenderTrigger[]): string;
|
|
52
|
-
/**
|
|
53
|
-
* Analyze slow renders (> 16ms)
|
|
54
|
-
*/
|
|
55
38
|
export declare function analyzeSlowRenders(renders: RenderInfo[]): string;
|
package/dist/react/profiler.js
CHANGED
|
@@ -1,56 +1,44 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* React
|
|
2
|
+
* React commit tracking groundwork.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* Install render tracking in the page
|
|
4
|
+
* This module records real commit metadata that can be observed from the
|
|
5
|
+
* React DevTools hook without pretending to expose a full Profiler surface.
|
|
8
6
|
*/
|
|
9
7
|
export async function installRenderTracking(page) {
|
|
10
8
|
await page.evaluate(inPageInstallRenderTracking);
|
|
11
9
|
}
|
|
12
|
-
/**
|
|
13
|
-
* Get recent render events
|
|
14
|
-
*/
|
|
15
10
|
export async function getRecentRenders(page, limit = 50) {
|
|
16
11
|
return page.evaluate(inPageGetRecentRenders, limit);
|
|
17
12
|
}
|
|
18
|
-
/**
|
|
19
|
-
* Get render triggers (why components re-rendered)
|
|
20
|
-
*/
|
|
21
13
|
export async function getRenderTriggers(page, limit = 50) {
|
|
22
14
|
return page.evaluate(inPageGetRenderTriggers, limit);
|
|
23
15
|
}
|
|
24
|
-
/**
|
|
25
|
-
* Clear render history
|
|
26
|
-
*/
|
|
27
16
|
export async function clearRenderHistory(page) {
|
|
28
17
|
await page.evaluate(inPageClearRenderHistory);
|
|
29
18
|
}
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
19
|
+
export function formatDuration(duration) {
|
|
20
|
+
return duration == null ? "n/a" : `${duration.toFixed(2)}ms`;
|
|
21
|
+
}
|
|
33
22
|
export function formatRenderInfo(renders) {
|
|
34
23
|
if (renders.length === 0)
|
|
35
24
|
return "No renders recorded";
|
|
36
|
-
const lines = ["# React
|
|
25
|
+
const lines = ["# React Commits\n"];
|
|
37
26
|
for (const render of renders) {
|
|
38
27
|
const phase = render.phase === "mount" ? "MOUNT" : render.phase === "update" ? "UPDATE" : "NESTED";
|
|
39
|
-
const duration = render.actualDuration
|
|
40
|
-
const slow = render.actualDuration > 16 ? " ⚠️ SLOW" : "";
|
|
41
|
-
lines.push(`[${phase}] ${render.
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
28
|
+
const duration = formatDuration(render.actualDuration);
|
|
29
|
+
const slow = render.actualDuration != null && render.actualDuration > 16 ? " ⚠️ SLOW" : "";
|
|
30
|
+
lines.push(`[${phase}] ${render.rootName} (${duration})${slow}`);
|
|
31
|
+
lines.push(` Fibers: ${render.fiberCount}`);
|
|
32
|
+
if (render.baseDuration != null) {
|
|
33
|
+
lines.push(` Base duration: ${render.baseDuration.toFixed(2)}ms`);
|
|
34
|
+
}
|
|
35
|
+
if (render.interactions.length > 0) {
|
|
36
|
+
const interactions = render.interactions.map((i) => i.name).join(", ");
|
|
46
37
|
lines.push(` Interactions: ${interactions}`);
|
|
47
38
|
}
|
|
48
39
|
}
|
|
49
40
|
return lines.join("\n");
|
|
50
41
|
}
|
|
51
|
-
/**
|
|
52
|
-
* Format render triggers for CLI output
|
|
53
|
-
*/
|
|
54
42
|
export function formatRenderTriggers(triggers) {
|
|
55
43
|
if (triggers.length === 0)
|
|
56
44
|
return "No render triggers recorded";
|
|
@@ -58,40 +46,36 @@ export function formatRenderTriggers(triggers) {
|
|
|
58
46
|
for (const trigger of triggers) {
|
|
59
47
|
const reason = trigger.reason.toUpperCase();
|
|
60
48
|
const details = trigger.details ? ` - ${trigger.details}` : "";
|
|
61
|
-
lines.push(`[${reason}] ${trigger.
|
|
49
|
+
lines.push(`[${reason}] ${trigger.rootName}${details}`);
|
|
62
50
|
}
|
|
63
51
|
return lines.join("\n");
|
|
64
52
|
}
|
|
65
|
-
/**
|
|
66
|
-
* Analyze slow renders (> 16ms)
|
|
67
|
-
*/
|
|
68
53
|
export function analyzeSlowRenders(renders) {
|
|
69
|
-
const slowRenders = renders.filter((r) => r.actualDuration > 16);
|
|
54
|
+
const slowRenders = renders.filter((r) => r.actualDuration != null && r.actualDuration > 16);
|
|
70
55
|
if (slowRenders.length === 0) {
|
|
71
|
-
return "No slow renders detected
|
|
56
|
+
return "No slow renders detected with measurable duration (> 16ms)";
|
|
72
57
|
}
|
|
73
58
|
const lines = [
|
|
74
59
|
"# Slow Renders Analysis\n",
|
|
75
60
|
`Found ${slowRenders.length} slow render(s) (> 16ms)\n`,
|
|
76
61
|
];
|
|
77
|
-
|
|
78
|
-
const byComponent = new Map();
|
|
62
|
+
const byRoot = new Map();
|
|
79
63
|
for (const render of slowRenders) {
|
|
80
|
-
const list =
|
|
64
|
+
const list = byRoot.get(render.rootName) || [];
|
|
81
65
|
list.push(render);
|
|
82
|
-
|
|
66
|
+
byRoot.set(render.rootName, list);
|
|
83
67
|
}
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
const
|
|
87
|
-
const totalB = b.reduce((sum, r) => sum + r.actualDuration, 0);
|
|
68
|
+
const sorted = Array.from(byRoot.entries()).sort(([, a], [, b]) => {
|
|
69
|
+
const totalA = a.reduce((sum, r) => sum + (r.actualDuration ?? 0), 0);
|
|
70
|
+
const totalB = b.reduce((sum, r) => sum + (r.actualDuration ?? 0), 0);
|
|
88
71
|
return totalB - totalA;
|
|
89
72
|
});
|
|
90
|
-
for (const [name,
|
|
91
|
-
const
|
|
92
|
-
const total =
|
|
73
|
+
for (const [name, commits] of sorted) {
|
|
74
|
+
const durations = commits.map((r) => r.actualDuration ?? 0);
|
|
75
|
+
const total = durations.reduce((sum, value) => sum + value, 0);
|
|
76
|
+
const count = commits.length;
|
|
93
77
|
const avg = total / count;
|
|
94
|
-
const max = Math.max(...
|
|
78
|
+
const max = Math.max(...durations);
|
|
95
79
|
lines.push(`${name}:`);
|
|
96
80
|
lines.push(` Count: ${count}`);
|
|
97
81
|
lines.push(` Total: ${total.toFixed(2)}ms`);
|
|
@@ -101,58 +85,39 @@ export function analyzeSlowRenders(renders) {
|
|
|
101
85
|
}
|
|
102
86
|
return lines.join("\n");
|
|
103
87
|
}
|
|
104
|
-
// In-page functions
|
|
105
88
|
function inPageInstallRenderTracking() {
|
|
106
89
|
const win = window;
|
|
107
|
-
// Initialize storage
|
|
108
90
|
win.__REACT_RENDER_HISTORY__ = win.__REACT_RENDER_HISTORY__ || [];
|
|
109
91
|
win.__REACT_RENDER_TRIGGERS__ = win.__REACT_RENDER_TRIGGERS__ || [];
|
|
110
|
-
// Hook into React DevTools if available
|
|
111
92
|
const hook = win.__REACT_DEVTOOLS_GLOBAL_HOOK__;
|
|
112
93
|
if (!hook) {
|
|
113
94
|
console.warn("React DevTools hook not found, render tracking limited");
|
|
114
95
|
return;
|
|
115
96
|
}
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
componentName: root?.current?.type?.name || "Root",
|
|
127
|
-
phase: root?.current?.mode === 0 ? "mount" : "update",
|
|
128
|
-
actualDuration: 0,
|
|
129
|
-
baseDuration: 0,
|
|
130
|
-
startTime: Date.now(),
|
|
131
|
-
commitTime: Date.now(),
|
|
132
|
-
interactions: new Set(),
|
|
133
|
-
};
|
|
134
|
-
win.__REACT_RENDER_HISTORY__.push(renderInfo);
|
|
135
|
-
// Keep only last 100 renders
|
|
136
|
-
if (win.__REACT_RENDER_HISTORY__.length > 100) {
|
|
137
|
-
win.__REACT_RENDER_HISTORY__.shift();
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
catch (e) {
|
|
141
|
-
// Ignore errors in tracking
|
|
142
|
-
}
|
|
143
|
-
if (originalOnCommitFiberRoot) {
|
|
144
|
-
return originalOnCommitFiberRoot.call(this, id, root, priorityLevel);
|
|
97
|
+
if (win.__REACT_RENDER_TRACKING_INSTALLED__)
|
|
98
|
+
return;
|
|
99
|
+
win.__REACT_RENDER_TRACKING_INSTALLED__ = true;
|
|
100
|
+
const originalOnCommitFiberRoot = hook.onCommitFiberRoot;
|
|
101
|
+
hook.onCommitFiberRoot = function (rendererId, root, priorityLevel) {
|
|
102
|
+
try {
|
|
103
|
+
const renderInfo = toRenderInfo(rendererId, root);
|
|
104
|
+
win.__REACT_RENDER_HISTORY__.push(renderInfo);
|
|
105
|
+
if (win.__REACT_RENDER_HISTORY__.length > 100) {
|
|
106
|
+
win.__REACT_RENDER_HISTORY__.shift();
|
|
145
107
|
}
|
|
146
|
-
}
|
|
147
|
-
|
|
108
|
+
}
|
|
109
|
+
catch {
|
|
110
|
+
// Ignore tracking errors to avoid breaking the inspected app.
|
|
111
|
+
}
|
|
112
|
+
if (originalOnCommitFiberRoot) {
|
|
113
|
+
return originalOnCommitFiberRoot.call(this, rendererId, root, priorityLevel);
|
|
114
|
+
}
|
|
115
|
+
};
|
|
148
116
|
}
|
|
149
117
|
function inPageGetRecentRenders(limit) {
|
|
150
118
|
const win = window;
|
|
151
119
|
const history = win.__REACT_RENDER_HISTORY__ || [];
|
|
152
|
-
return history.slice(-limit)
|
|
153
|
-
...r,
|
|
154
|
-
interactions: Array.from(r.interactions || []),
|
|
155
|
-
}));
|
|
120
|
+
return history.slice(-limit);
|
|
156
121
|
}
|
|
157
122
|
function inPageGetRenderTriggers(limit) {
|
|
158
123
|
const win = window;
|
|
@@ -164,3 +129,67 @@ function inPageClearRenderHistory() {
|
|
|
164
129
|
win.__REACT_RENDER_HISTORY__ = [];
|
|
165
130
|
win.__REACT_RENDER_TRIGGERS__ = [];
|
|
166
131
|
}
|
|
132
|
+
function toRenderInfo(rendererId, root) {
|
|
133
|
+
const current = root?.current ?? null;
|
|
134
|
+
const rootChild = current?.child ?? null;
|
|
135
|
+
return {
|
|
136
|
+
rendererId,
|
|
137
|
+
rootName: getFiberName(rootChild || current),
|
|
138
|
+
phase: inferPhase(current),
|
|
139
|
+
actualDuration: numberOrNull(current?.actualDuration),
|
|
140
|
+
baseDuration: numberOrNull(current?.treeBaseDuration ?? current?.baseDuration),
|
|
141
|
+
startTime: numberOrNull(current?.actualStartTime),
|
|
142
|
+
commitTime: Date.now(),
|
|
143
|
+
fiberCount: countFibers(current),
|
|
144
|
+
interactions: normalizeInteractions(root?.memoizedInteractions),
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
function inferPhase(current) {
|
|
148
|
+
if (!current?.alternate)
|
|
149
|
+
return "mount";
|
|
150
|
+
return current.flags && (current.flags & 1024) !== 0 ? "nested-update" : "update";
|
|
151
|
+
}
|
|
152
|
+
function getFiberName(fiber) {
|
|
153
|
+
if (!fiber)
|
|
154
|
+
return "Root";
|
|
155
|
+
if (typeof fiber.type === "string")
|
|
156
|
+
return fiber.type;
|
|
157
|
+
if (fiber.type?.displayName)
|
|
158
|
+
return fiber.type.displayName;
|
|
159
|
+
if (fiber.type?.name)
|
|
160
|
+
return fiber.type.name;
|
|
161
|
+
if (fiber.elementType?.displayName)
|
|
162
|
+
return fiber.elementType.displayName;
|
|
163
|
+
if (fiber.elementType?.name)
|
|
164
|
+
return fiber.elementType.name;
|
|
165
|
+
return "Anonymous";
|
|
166
|
+
}
|
|
167
|
+
function countFibers(fiber) {
|
|
168
|
+
let count = 0;
|
|
169
|
+
const stack = fiber ? [fiber] : [];
|
|
170
|
+
while (stack.length > 0) {
|
|
171
|
+
const current = stack.pop();
|
|
172
|
+
if (!current)
|
|
173
|
+
continue;
|
|
174
|
+
count++;
|
|
175
|
+
if (current.sibling)
|
|
176
|
+
stack.push(current.sibling);
|
|
177
|
+
if (current.child)
|
|
178
|
+
stack.push(current.child);
|
|
179
|
+
}
|
|
180
|
+
return count;
|
|
181
|
+
}
|
|
182
|
+
function numberOrNull(value) {
|
|
183
|
+
return typeof value === "number" && Number.isFinite(value) ? value : null;
|
|
184
|
+
}
|
|
185
|
+
function normalizeInteractions(interactions) {
|
|
186
|
+
if (!(interactions instanceof Set))
|
|
187
|
+
return [];
|
|
188
|
+
return Array.from(interactions)
|
|
189
|
+
.map((interaction) => ({
|
|
190
|
+
id: typeof interaction?.id === "number" ? interaction.id : 0,
|
|
191
|
+
name: typeof interaction?.name === "string" ? interaction.name : "interaction",
|
|
192
|
+
timestamp: typeof interaction?.timestamp === "number" ? interaction.timestamp : 0,
|
|
193
|
+
}))
|
|
194
|
+
.filter((interaction) => interaction.name.length > 0);
|
|
195
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@presto1314w/vite-devtools-browser",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.6",
|
|
4
4
|
"description": "Runtime diagnostics CLI for Vite apps with event-stream correlation, HMR diagnosis, framework inspection, and mapped errors",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"keywords": [
|