@presto1314w/vite-devtools-browser 0.2.2 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +318 -296
- package/dist/browser-collector.d.ts +3 -0
- package/dist/browser-collector.js +256 -0
- package/dist/browser-frameworks.d.ts +11 -0
- package/dist/browser-frameworks.js +72 -0
- package/dist/browser-session.d.ts +37 -0
- package/dist/browser-session.js +117 -0
- package/dist/browser-vite.d.ts +25 -0
- package/dist/browser-vite.js +181 -0
- package/dist/browser.d.ts +5 -28
- package/dist/browser.js +127 -446
- package/dist/cli.js +78 -55
- package/dist/correlate.d.ts +2 -1
- package/dist/correlate.js +11 -42
- package/dist/daemon.js +12 -0
- package/dist/diagnose-propagation.d.ts +10 -0
- package/dist/diagnose-propagation.js +58 -0
- package/dist/event-analysis.d.ts +32 -0
- package/dist/event-analysis.js +75 -0
- package/dist/event-queue.d.ts +70 -5
- package/dist/trace.d.ts +15 -0
- package/dist/trace.js +75 -0
- package/package.json +17 -17
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
export function initBrowserEventCollector() {
|
|
2
|
+
if (window.__vb_events)
|
|
3
|
+
return;
|
|
4
|
+
window.__vb_events = [];
|
|
5
|
+
window.__vb_push = (event) => {
|
|
6
|
+
const q = window.__vb_events;
|
|
7
|
+
q.push(event);
|
|
8
|
+
if (q.length > 1000)
|
|
9
|
+
q.shift();
|
|
10
|
+
};
|
|
11
|
+
const inferFramework = () => {
|
|
12
|
+
if (window.__VUE__ || window.__VUE_DEVTOOLS_GLOBAL_HOOK__)
|
|
13
|
+
return "vue";
|
|
14
|
+
if (window.__REACT_DEVTOOLS_GLOBAL_HOOK__ || window.React)
|
|
15
|
+
return "react";
|
|
16
|
+
if (window.__SVELTE__ || window.__svelte || window.__SVELTE_DEVTOOLS_GLOBAL_HOOK__) {
|
|
17
|
+
return "svelte";
|
|
18
|
+
}
|
|
19
|
+
return "unknown";
|
|
20
|
+
};
|
|
21
|
+
const inferRenderLabel = () => {
|
|
22
|
+
const heading = document.querySelector("main h1, [role='main'] h1, h1")?.textContent?.trim() ||
|
|
23
|
+
document.title ||
|
|
24
|
+
location.pathname ||
|
|
25
|
+
"/";
|
|
26
|
+
return heading.slice(0, 120);
|
|
27
|
+
};
|
|
28
|
+
const inferVueRenderDetails = () => {
|
|
29
|
+
const hook = window.__VUE_DEVTOOLS_GLOBAL_HOOK__;
|
|
30
|
+
const apps = hook?.apps;
|
|
31
|
+
if (!Array.isArray(apps) || apps.length === 0)
|
|
32
|
+
return null;
|
|
33
|
+
const app = apps[0];
|
|
34
|
+
const rootInstance = app?._instance || app?._container?._vnode?.component;
|
|
35
|
+
if (!rootInstance)
|
|
36
|
+
return null;
|
|
37
|
+
const names = [];
|
|
38
|
+
let current = rootInstance;
|
|
39
|
+
let guard = 0;
|
|
40
|
+
while (current && guard < 8) {
|
|
41
|
+
const name = current.type?.name ||
|
|
42
|
+
current.type?.__name ||
|
|
43
|
+
current.type?.__file?.split(/[\\/]/).pop()?.replace(/\.\w+$/, "") ||
|
|
44
|
+
"Anonymous";
|
|
45
|
+
names.push(String(name));
|
|
46
|
+
const nextFromSubtree = current.subTree?.component;
|
|
47
|
+
const nextFromChildren = Array.isArray(current.subTree?.children)
|
|
48
|
+
? current.subTree.children.find((child) => child?.component)?.component
|
|
49
|
+
: null;
|
|
50
|
+
current = nextFromSubtree || nextFromChildren || null;
|
|
51
|
+
guard++;
|
|
52
|
+
}
|
|
53
|
+
const pinia = window.__PINIA__ || window.pinia || app?.config?.globalProperties?.$pinia;
|
|
54
|
+
const registry = pinia?._s;
|
|
55
|
+
const storeIds = registry instanceof Map
|
|
56
|
+
? Array.from(registry.keys()).map(String)
|
|
57
|
+
: registry && typeof registry === "object"
|
|
58
|
+
? Object.keys(registry)
|
|
59
|
+
: [];
|
|
60
|
+
return {
|
|
61
|
+
component: names[names.length - 1] || inferRenderLabel(),
|
|
62
|
+
componentPath: names.join(" > "),
|
|
63
|
+
storeHints: storeIds.slice(0, 5),
|
|
64
|
+
};
|
|
65
|
+
};
|
|
66
|
+
const inferRenderDetails = () => {
|
|
67
|
+
const framework = inferFramework();
|
|
68
|
+
if (framework === "vue") {
|
|
69
|
+
const vue = inferVueRenderDetails();
|
|
70
|
+
if (vue) {
|
|
71
|
+
return {
|
|
72
|
+
framework,
|
|
73
|
+
component: vue.component,
|
|
74
|
+
path: vue.componentPath,
|
|
75
|
+
storeHints: vue.storeHints,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return {
|
|
80
|
+
framework,
|
|
81
|
+
component: inferRenderLabel(),
|
|
82
|
+
path: `${framework}:${location.pathname || "/"}`,
|
|
83
|
+
storeHints: [],
|
|
84
|
+
};
|
|
85
|
+
};
|
|
86
|
+
const renderState = window.__vb_render_state ||
|
|
87
|
+
(window.__vb_render_state = {
|
|
88
|
+
timer: null,
|
|
89
|
+
lastReason: "initial-load",
|
|
90
|
+
mutationCount: 0,
|
|
91
|
+
});
|
|
92
|
+
const scheduleRender = (reason, extra = {}) => {
|
|
93
|
+
renderState.lastReason = reason;
|
|
94
|
+
renderState.mutationCount += Number(extra.mutationCount ?? 0);
|
|
95
|
+
if (renderState.timer != null)
|
|
96
|
+
window.clearTimeout(renderState.timer);
|
|
97
|
+
renderState.timer = window.setTimeout(() => {
|
|
98
|
+
const details = inferRenderDetails();
|
|
99
|
+
const changedKeys = Array.isArray(extra.changedKeys)
|
|
100
|
+
? extra.changedKeys.filter((value) => typeof value === "string" && value.length > 0)
|
|
101
|
+
: [];
|
|
102
|
+
window.__vb_push({
|
|
103
|
+
timestamp: Date.now(),
|
|
104
|
+
type: "render",
|
|
105
|
+
payload: {
|
|
106
|
+
component: details.component,
|
|
107
|
+
path: details.path,
|
|
108
|
+
framework: details.framework,
|
|
109
|
+
reason: renderState.lastReason,
|
|
110
|
+
mutationCount: renderState.mutationCount,
|
|
111
|
+
storeHints: details.storeHints,
|
|
112
|
+
changedKeys,
|
|
113
|
+
},
|
|
114
|
+
});
|
|
115
|
+
renderState.timer = null;
|
|
116
|
+
renderState.mutationCount = 0;
|
|
117
|
+
}, 60);
|
|
118
|
+
};
|
|
119
|
+
const attachPiniaSubscriptions = () => {
|
|
120
|
+
if (window.__vb_pinia_attached)
|
|
121
|
+
return;
|
|
122
|
+
const hook = window.__VUE_DEVTOOLS_GLOBAL_HOOK__;
|
|
123
|
+
const app = Array.isArray(hook?.apps) ? hook.apps[0] : null;
|
|
124
|
+
const pinia = window.__PINIA__ || window.pinia || app?.config?.globalProperties?.$pinia;
|
|
125
|
+
const registry = pinia?._s;
|
|
126
|
+
if (!(registry instanceof Map) || registry.size === 0)
|
|
127
|
+
return;
|
|
128
|
+
const attached = (window.__vb_pinia_attached = new Set());
|
|
129
|
+
registry.forEach((store, storeId) => {
|
|
130
|
+
if (!store || typeof store.$subscribe !== "function" || attached.has(String(storeId)))
|
|
131
|
+
return;
|
|
132
|
+
attached.add(String(storeId));
|
|
133
|
+
const extractChangedKeys = (mutation) => {
|
|
134
|
+
const keys = new Set();
|
|
135
|
+
const events = mutation?.events;
|
|
136
|
+
const collect = (entry) => {
|
|
137
|
+
const key = entry?.key ?? entry?.path;
|
|
138
|
+
if (typeof key === "string" && key.length > 0)
|
|
139
|
+
keys.add(key.split(".")[0]);
|
|
140
|
+
};
|
|
141
|
+
if (Array.isArray(events))
|
|
142
|
+
events.forEach(collect);
|
|
143
|
+
else if (events && typeof events === "object")
|
|
144
|
+
collect(events);
|
|
145
|
+
const payload = mutation?.payload;
|
|
146
|
+
if (payload && typeof payload === "object" && !Array.isArray(payload)) {
|
|
147
|
+
Object.keys(payload).forEach((key) => keys.add(key));
|
|
148
|
+
}
|
|
149
|
+
return Array.from(keys).slice(0, 10);
|
|
150
|
+
};
|
|
151
|
+
store.$subscribe((mutation) => {
|
|
152
|
+
const changedKeys = extractChangedKeys(mutation);
|
|
153
|
+
window.__vb_push({
|
|
154
|
+
timestamp: Date.now(),
|
|
155
|
+
type: "store-update",
|
|
156
|
+
payload: {
|
|
157
|
+
store: String(storeId),
|
|
158
|
+
mutationType: typeof mutation?.type === "string" ? mutation.type : "unknown",
|
|
159
|
+
events: Array.isArray(mutation?.events) ? mutation.events.length : 0,
|
|
160
|
+
changedKeys,
|
|
161
|
+
},
|
|
162
|
+
});
|
|
163
|
+
scheduleRender("store-update", { changedKeys });
|
|
164
|
+
}, { detached: true });
|
|
165
|
+
});
|
|
166
|
+
};
|
|
167
|
+
function attachViteListener() {
|
|
168
|
+
const hot = window.__vite_hot;
|
|
169
|
+
if (hot?.ws) {
|
|
170
|
+
hot.ws.addEventListener("message", (e) => {
|
|
171
|
+
try {
|
|
172
|
+
const data = JSON.parse(e.data);
|
|
173
|
+
window.__vb_push({
|
|
174
|
+
timestamp: Date.now(),
|
|
175
|
+
type: data.type === "error" ? "hmr-error" : "hmr-update",
|
|
176
|
+
payload: data,
|
|
177
|
+
});
|
|
178
|
+
if (data.type === "update" || data.type === "full-reload") {
|
|
179
|
+
scheduleRender("hmr-message");
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
catch { }
|
|
183
|
+
});
|
|
184
|
+
return true;
|
|
185
|
+
}
|
|
186
|
+
return false;
|
|
187
|
+
}
|
|
188
|
+
if (!attachViteListener()) {
|
|
189
|
+
let attempts = 0;
|
|
190
|
+
const timer = setInterval(() => {
|
|
191
|
+
attempts++;
|
|
192
|
+
if (attachViteListener() || attempts >= 50) {
|
|
193
|
+
clearInterval(timer);
|
|
194
|
+
}
|
|
195
|
+
}, 100);
|
|
196
|
+
}
|
|
197
|
+
const origOnError = window.onerror;
|
|
198
|
+
window.onerror = (msg, src, line, col, err) => {
|
|
199
|
+
window.__vb_push({
|
|
200
|
+
timestamp: Date.now(),
|
|
201
|
+
type: "error",
|
|
202
|
+
payload: { message: String(msg), source: src, line, col, stack: err?.stack },
|
|
203
|
+
});
|
|
204
|
+
return origOnError ? origOnError(msg, src, line, col, err) : false;
|
|
205
|
+
};
|
|
206
|
+
window.addEventListener("unhandledrejection", (e) => {
|
|
207
|
+
window.__vb_push({
|
|
208
|
+
timestamp: Date.now(),
|
|
209
|
+
type: "error",
|
|
210
|
+
payload: { message: e.reason?.message, stack: e.reason?.stack },
|
|
211
|
+
});
|
|
212
|
+
});
|
|
213
|
+
const observeDom = () => {
|
|
214
|
+
const root = document.body || document.documentElement;
|
|
215
|
+
if (!root || window.__vb_render_observer)
|
|
216
|
+
return;
|
|
217
|
+
const observer = new MutationObserver((mutations) => {
|
|
218
|
+
scheduleRender("dom-mutation", { mutationCount: mutations.length });
|
|
219
|
+
});
|
|
220
|
+
observer.observe(root, {
|
|
221
|
+
childList: true,
|
|
222
|
+
subtree: true,
|
|
223
|
+
attributes: true,
|
|
224
|
+
characterData: true,
|
|
225
|
+
});
|
|
226
|
+
window.__vb_render_observer = observer;
|
|
227
|
+
};
|
|
228
|
+
const patchHistory = () => {
|
|
229
|
+
if (window.__vb_history_patched)
|
|
230
|
+
return;
|
|
231
|
+
const wrap = (method) => {
|
|
232
|
+
const original = history[method];
|
|
233
|
+
const wrapped = ((...args) => {
|
|
234
|
+
const result = Reflect.apply(original, history, args);
|
|
235
|
+
scheduleRender(`history-${method}`);
|
|
236
|
+
return result;
|
|
237
|
+
});
|
|
238
|
+
history[method] = wrapped;
|
|
239
|
+
};
|
|
240
|
+
wrap("pushState");
|
|
241
|
+
wrap("replaceState");
|
|
242
|
+
window.addEventListener("popstate", () => scheduleRender("history-popstate"));
|
|
243
|
+
window.addEventListener("hashchange", () => scheduleRender("hashchange"));
|
|
244
|
+
window.__vb_history_patched = true;
|
|
245
|
+
};
|
|
246
|
+
observeDom();
|
|
247
|
+
patchHistory();
|
|
248
|
+
attachPiniaSubscriptions();
|
|
249
|
+
window.setInterval(attachPiniaSubscriptions, 1000);
|
|
250
|
+
scheduleRender("initial-load");
|
|
251
|
+
}
|
|
252
|
+
export function readBrowserEvents() {
|
|
253
|
+
const events = (window.__vb_events ?? []);
|
|
254
|
+
window.__vb_events = [];
|
|
255
|
+
return events;
|
|
256
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { Page } from "playwright";
|
|
2
|
+
import type { BrowserFramework, BrowserSessionState } from "./browser-session.js";
|
|
3
|
+
export declare function detectBrowserFramework(page: Page): Promise<{
|
|
4
|
+
detected: string;
|
|
5
|
+
framework: BrowserFramework;
|
|
6
|
+
}>;
|
|
7
|
+
export declare function inspectVueTree(page: Page, id?: string): Promise<string>;
|
|
8
|
+
export declare function inspectVuePinia(page: Page, store?: string): Promise<string>;
|
|
9
|
+
export declare function inspectVueRouter(page: Page): Promise<string>;
|
|
10
|
+
export declare function inspectReactTree(state: BrowserSessionState, page: Page, id?: string): Promise<string>;
|
|
11
|
+
export declare function inspectSvelteTree(page: Page, id?: string): Promise<string>;
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import * as vueDevtools from "./vue/devtools.js";
|
|
2
|
+
import * as reactDevtools from "./react/devtools.js";
|
|
3
|
+
import * as svelteDevtools from "./svelte/devtools.js";
|
|
4
|
+
export async function detectBrowserFramework(page) {
|
|
5
|
+
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
|
+
const reactHook = window.__REACT_DEVTOOLS_GLOBAL_HOOK__;
|
|
11
|
+
if (reactHook || window.React || document.querySelector("[data-reactroot]")) {
|
|
12
|
+
const renderers = reactHook?.renderers;
|
|
13
|
+
const firstRenderer = renderers ? renderers.values().next().value : null;
|
|
14
|
+
const version = firstRenderer?.version || window.React?.version || "unknown";
|
|
15
|
+
return `react@${version}`;
|
|
16
|
+
}
|
|
17
|
+
if (window.__SVELTE__ ||
|
|
18
|
+
window.__svelte ||
|
|
19
|
+
window.__SVELTE_DEVTOOLS_GLOBAL_HOOK__) {
|
|
20
|
+
const version = window.__SVELTE__?.VERSION ||
|
|
21
|
+
window.__svelte?.version ||
|
|
22
|
+
"unknown";
|
|
23
|
+
return `svelte@${version}`;
|
|
24
|
+
}
|
|
25
|
+
return "unknown";
|
|
26
|
+
});
|
|
27
|
+
return {
|
|
28
|
+
detected,
|
|
29
|
+
framework: toBrowserFramework(detected),
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
export async function inspectVueTree(page, id) {
|
|
33
|
+
return id ? vueDevtools.getComponentDetails(page, id) : vueDevtools.getComponentTree(page);
|
|
34
|
+
}
|
|
35
|
+
export async function inspectVuePinia(page, store) {
|
|
36
|
+
return vueDevtools.getPiniaStores(page, store);
|
|
37
|
+
}
|
|
38
|
+
export async function inspectVueRouter(page) {
|
|
39
|
+
return vueDevtools.getRouterInfo(page);
|
|
40
|
+
}
|
|
41
|
+
export async function inspectReactTree(state, page, id) {
|
|
42
|
+
if (!id) {
|
|
43
|
+
state.lastReactSnapshot = await reactDevtools.snapshot(page);
|
|
44
|
+
return reactDevtools.format(state.lastReactSnapshot);
|
|
45
|
+
}
|
|
46
|
+
const parsed = Number.parseInt(id, 10);
|
|
47
|
+
if (!Number.isFinite(parsed))
|
|
48
|
+
throw new Error("react component id must be a number");
|
|
49
|
+
const inspected = await reactDevtools.inspect(page, parsed);
|
|
50
|
+
const lines = [];
|
|
51
|
+
const componentPath = reactDevtools.path(state.lastReactSnapshot, parsed);
|
|
52
|
+
if (componentPath)
|
|
53
|
+
lines.push(`path: ${componentPath}`);
|
|
54
|
+
lines.push(inspected.text);
|
|
55
|
+
if (inspected.source) {
|
|
56
|
+
const [file, line, column] = inspected.source;
|
|
57
|
+
lines.push(`source: ${file}:${line}:${column}`);
|
|
58
|
+
}
|
|
59
|
+
return lines.join("\n");
|
|
60
|
+
}
|
|
61
|
+
export async function inspectSvelteTree(page, id) {
|
|
62
|
+
return id ? svelteDevtools.getComponentDetails(page, id) : svelteDevtools.getComponentTree(page);
|
|
63
|
+
}
|
|
64
|
+
function toBrowserFramework(detected) {
|
|
65
|
+
if (detected.startsWith("vue"))
|
|
66
|
+
return "vue";
|
|
67
|
+
if (detected.startsWith("react"))
|
|
68
|
+
return "react";
|
|
69
|
+
if (detected.startsWith("svelte"))
|
|
70
|
+
return "svelte";
|
|
71
|
+
return "unknown";
|
|
72
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { type BrowserContext, type Page } from "playwright";
|
|
2
|
+
import type { ReactNode } from "./react/devtools.js";
|
|
3
|
+
export type BrowserFramework = "vue" | "react" | "svelte" | "unknown";
|
|
4
|
+
export type HmrEventType = "connecting" | "connected" | "update" | "full-reload" | "error" | "log";
|
|
5
|
+
export type HmrEvent = {
|
|
6
|
+
timestamp: number;
|
|
7
|
+
type: HmrEventType;
|
|
8
|
+
message: string;
|
|
9
|
+
path?: string;
|
|
10
|
+
};
|
|
11
|
+
export type RuntimeError = {
|
|
12
|
+
timestamp: number;
|
|
13
|
+
message: string;
|
|
14
|
+
stack?: string;
|
|
15
|
+
source?: string | null;
|
|
16
|
+
line?: number | null;
|
|
17
|
+
col?: number | null;
|
|
18
|
+
};
|
|
19
|
+
export type BrowserSessionState = {
|
|
20
|
+
context: BrowserContext | null;
|
|
21
|
+
page: Page | null;
|
|
22
|
+
framework: BrowserFramework;
|
|
23
|
+
extensionModeDisabled: boolean;
|
|
24
|
+
collectorInstalled: boolean;
|
|
25
|
+
consoleLogs: string[];
|
|
26
|
+
hmrEvents: HmrEvent[];
|
|
27
|
+
runtimeErrors: RuntimeError[];
|
|
28
|
+
lastReactSnapshot: ReactNode[];
|
|
29
|
+
lastModuleGraphUrls: string[] | null;
|
|
30
|
+
};
|
|
31
|
+
export declare function createBrowserSessionState(): BrowserSessionState;
|
|
32
|
+
export declare function getCurrentPage(state: BrowserSessionState): Page | null;
|
|
33
|
+
export declare function resetBrowserSessionState(state: BrowserSessionState): void;
|
|
34
|
+
export declare function ensureBrowserPage(state: BrowserSessionState, onPageReady: (page: Page) => void): Promise<Page>;
|
|
35
|
+
export declare function closeBrowserSession(state: BrowserSessionState): Promise<void>;
|
|
36
|
+
export declare function contextUsable(current: Pick<BrowserContext, "pages"> | null): current is Pick<BrowserContext, "pages">;
|
|
37
|
+
export declare function isClosedTargetError(error: unknown): boolean;
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import { join, resolve } from "node:path";
|
|
3
|
+
import { chromium } from "playwright";
|
|
4
|
+
const extensionPath = process.env.REACT_DEVTOOLS_EXTENSION ??
|
|
5
|
+
resolve(import.meta.dirname, "../../next-browser/extensions/react-devtools-chrome");
|
|
6
|
+
const hasReactExtension = existsSync(join(extensionPath, "manifest.json"));
|
|
7
|
+
const installHook = hasReactExtension
|
|
8
|
+
? readFileSync(join(extensionPath, "build", "installHook.js"), "utf-8")
|
|
9
|
+
: null;
|
|
10
|
+
export function createBrowserSessionState() {
|
|
11
|
+
return {
|
|
12
|
+
context: null,
|
|
13
|
+
page: null,
|
|
14
|
+
framework: "unknown",
|
|
15
|
+
extensionModeDisabled: false,
|
|
16
|
+
collectorInstalled: false,
|
|
17
|
+
consoleLogs: [],
|
|
18
|
+
hmrEvents: [],
|
|
19
|
+
runtimeErrors: [],
|
|
20
|
+
lastReactSnapshot: [],
|
|
21
|
+
lastModuleGraphUrls: null,
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
export function getCurrentPage(state) {
|
|
25
|
+
if (!contextUsable(state.context))
|
|
26
|
+
return null;
|
|
27
|
+
if (!state.page || state.page.isClosed())
|
|
28
|
+
return null;
|
|
29
|
+
return state.page;
|
|
30
|
+
}
|
|
31
|
+
export function resetBrowserSessionState(state) {
|
|
32
|
+
state.context = null;
|
|
33
|
+
state.page = null;
|
|
34
|
+
state.framework = "unknown";
|
|
35
|
+
state.collectorInstalled = false;
|
|
36
|
+
state.consoleLogs.length = 0;
|
|
37
|
+
state.hmrEvents.length = 0;
|
|
38
|
+
state.runtimeErrors.length = 0;
|
|
39
|
+
state.lastModuleGraphUrls = null;
|
|
40
|
+
state.lastReactSnapshot = [];
|
|
41
|
+
}
|
|
42
|
+
export async function ensureBrowserPage(state, onPageReady) {
|
|
43
|
+
if (!contextUsable(state.context)) {
|
|
44
|
+
await closeBrowserSession(state);
|
|
45
|
+
const launched = await launchBrowserContext(state.extensionModeDisabled);
|
|
46
|
+
state.context = launched.context;
|
|
47
|
+
state.extensionModeDisabled = launched.extensionModeDisabled;
|
|
48
|
+
}
|
|
49
|
+
if (!state.context)
|
|
50
|
+
throw new Error("browser not open");
|
|
51
|
+
if (!state.page || state.page.isClosed()) {
|
|
52
|
+
try {
|
|
53
|
+
state.page = state.context.pages()[0] ?? (await state.context.newPage());
|
|
54
|
+
}
|
|
55
|
+
catch (error) {
|
|
56
|
+
if (!isClosedTargetError(error))
|
|
57
|
+
throw error;
|
|
58
|
+
await closeBrowserSession(state);
|
|
59
|
+
state.extensionModeDisabled = true;
|
|
60
|
+
const launched = await launchBrowserContext(state.extensionModeDisabled);
|
|
61
|
+
state.context = launched.context;
|
|
62
|
+
state.extensionModeDisabled = launched.extensionModeDisabled;
|
|
63
|
+
state.page = state.context.pages()[0] ?? (await state.context.newPage());
|
|
64
|
+
}
|
|
65
|
+
onPageReady(state.page);
|
|
66
|
+
}
|
|
67
|
+
return state.page;
|
|
68
|
+
}
|
|
69
|
+
export async function closeBrowserSession(state) {
|
|
70
|
+
await state.context?.close();
|
|
71
|
+
resetBrowserSessionState(state);
|
|
72
|
+
}
|
|
73
|
+
export function contextUsable(current) {
|
|
74
|
+
if (!current)
|
|
75
|
+
return false;
|
|
76
|
+
try {
|
|
77
|
+
current.pages();
|
|
78
|
+
return true;
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
return false;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
export function isClosedTargetError(error) {
|
|
85
|
+
if (!(error instanceof Error))
|
|
86
|
+
return false;
|
|
87
|
+
return /Target page, context or browser has been closed/i.test(error.message);
|
|
88
|
+
}
|
|
89
|
+
async function launchBrowserContext(extensionModeDisabled) {
|
|
90
|
+
if (hasReactExtension && installHook && !extensionModeDisabled) {
|
|
91
|
+
try {
|
|
92
|
+
const context = await chromium.launchPersistentContext("", {
|
|
93
|
+
headless: false,
|
|
94
|
+
viewport: { width: 1280, height: 720 },
|
|
95
|
+
args: [
|
|
96
|
+
`--disable-extensions-except=${extensionPath}`,
|
|
97
|
+
`--load-extension=${extensionPath}`,
|
|
98
|
+
"--auto-open-devtools-for-tabs",
|
|
99
|
+
],
|
|
100
|
+
});
|
|
101
|
+
await context.waitForEvent("serviceworker").catch(() => { });
|
|
102
|
+
await context.addInitScript(installHook);
|
|
103
|
+
return { context, extensionModeDisabled };
|
|
104
|
+
}
|
|
105
|
+
catch {
|
|
106
|
+
extensionModeDisabled = true;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
const browser = await chromium.launch({
|
|
110
|
+
headless: false,
|
|
111
|
+
args: ["--auto-open-devtools-for-tabs"],
|
|
112
|
+
});
|
|
113
|
+
return {
|
|
114
|
+
context: await browser.newContext({ viewport: { width: 1280, height: 720 } }),
|
|
115
|
+
extensionModeDisabled,
|
|
116
|
+
};
|
|
117
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { Page } from "playwright";
|
|
2
|
+
import type { HmrEvent } from "./browser-session.js";
|
|
3
|
+
export type RuntimeSnapshot = {
|
|
4
|
+
url: string;
|
|
5
|
+
hasViteClient: boolean;
|
|
6
|
+
wsState: string;
|
|
7
|
+
hasErrorOverlay: boolean;
|
|
8
|
+
timestamp: number;
|
|
9
|
+
};
|
|
10
|
+
export type ModuleRow = {
|
|
11
|
+
url: string;
|
|
12
|
+
initiator: string;
|
|
13
|
+
durationMs: number;
|
|
14
|
+
};
|
|
15
|
+
export declare function normalizeLimit(limit: number, fallback: number, max: number): number;
|
|
16
|
+
export declare function formatRuntimeStatus(runtime: RuntimeSnapshot, currentFramework: string, events: HmrEvent[]): string;
|
|
17
|
+
export declare function formatHmrTrace(mode: "summary" | "trace", events: HmrEvent[], limit: number): string;
|
|
18
|
+
export declare function formatModuleGraphSnapshot(rows: ModuleRow[], filter?: string, limit?: number): string;
|
|
19
|
+
export declare function formatModuleGraphTrace(currentUrls: string[], previousUrls: Set<string> | null, filter?: string, limit?: number): string;
|
|
20
|
+
export declare function readRuntimeSnapshot(page: Page): Promise<RuntimeSnapshot>;
|
|
21
|
+
export declare function collectModuleRows(page: Page): Promise<ModuleRow[]>;
|
|
22
|
+
export declare function readOverlayError(page: Page): Promise<{
|
|
23
|
+
message?: string;
|
|
24
|
+
stack?: string;
|
|
25
|
+
} | null>;
|