@presto1314w/vite-devtools-browser 0.2.2 → 0.3.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/README.md CHANGED
@@ -7,6 +7,7 @@ It gives agents and developers structured access to:
7
7
  - Vue, React, and Svelte runtime state
8
8
  - Vite HMR activity and runtime health
9
9
  - event-window correlation between current errors and recent hot updates
10
+ - early propagation diagnostics from store/module updates into rerender paths
10
11
  - rule-based HMR diagnosis with confidence levels
11
12
  - module graph snapshots and diffs
12
13
  - mapped error output with optional source snippets
@@ -17,22 +18,19 @@ It ships in two forms:
17
18
  - Agent Skill: scenario-based debugging workflows for coding assistants
18
19
  - CLI Runtime (`@presto1314w/vite-devtools-browser`): structured shell commands for local Vite debugging
19
20
 
20
- Current documented baseline: `v0.2.2`.
21
+ Current documented baseline: `v0.3.0`.
21
22
 
22
- ## What's New In v0.2
23
+ ## What's New In v0.3
23
24
 
24
- `v0.2.x` moves `vite-browser` from snapshot-style inspection toward runtime diagnosis:
25
+ `v0.3.0` is the propagation-diagnostics release.
25
26
 
26
- - browser/runtime events are captured into a daemon-side event queue
27
- - `correlate errors` links the current error to recent HMR-updated modules
28
- - `diagnose hmr` turns runtime, trace, and error signals into structured findings
29
- - skills and CLI flows now route more directly to runtime triage instead of raw log inspection
27
+ It keeps the `v0.2.x` runtime diagnosis model, then adds a new layer of propagation-oriented reasoning:
30
28
 
31
- `v0.2.2` is the stabilization pass for this model:
32
-
33
- - tighter `diagnose hmr` wording around websocket evidence and runtime ambiguity
34
- - better test coverage for the four built-in diagnosis families
35
- - docs and release positioning aligned around the stable `v0.2.x` surface
29
+ - `correlate renders` summarizes recent render/update propagation evidence
30
+ - `diagnose propagation` turns store/module/render/error signals into structured guidance
31
+ - Vue-first store updates now include top-level `changedKeys`
32
+ - browser-side collection now captures render and store-update signals as first-class events
33
+ - propagation output stays conservative when evidence is incomplete
36
34
 
37
35
  ## Built For Agents
38
36
 
@@ -51,6 +49,7 @@ Most browser CLIs are optimized for automation. Most framework devtools are opti
51
49
  - it can inspect framework state like a devtools bridge
52
50
  - it can explain Vite-specific behavior like HMR updates and module graph changes
53
51
  - it can correlate recent updates with current failures
52
+ - it can surface high-confidence clues about how store/module changes propagate into rerender paths
54
53
  - it returns structured text that AI agents can consume directly in loops
55
54
 
56
55
  ## Positioning
@@ -90,6 +89,8 @@ vite-browser detect
90
89
  vite-browser vite runtime
91
90
  vite-browser errors --mapped --inline-source
92
91
  vite-browser correlate errors --mapped --window 5000
92
+ vite-browser correlate renders --window 5000
93
+ vite-browser diagnose propagation --window 5000
93
94
  vite-browser diagnose hmr --limit 50
94
95
  vite-browser vite hmr trace --limit 20
95
96
  vite-browser vite module-graph trace --limit 50
@@ -141,6 +142,26 @@ Confidence: high
141
142
  HMR update observed within 5000ms of the current error
142
143
  Matching modules: /src/App.tsx
143
144
 
145
+ $ vite-browser correlate renders --window 5000
146
+ # Render Correlation
147
+ Confidence: high
148
+ Recent source/store updates likely propagated through 1 render step(s).
149
+
150
+ ## Store Updates
151
+ - cart
152
+
153
+ ## Changed Keys
154
+ - items
155
+
156
+ ## Render Path
157
+ - AppShell > ShoppingCart > CartSummary
158
+
159
+ $ vite-browser diagnose propagation --window 5000
160
+ # Propagation Diagnosis
161
+ Status: fail
162
+ Confidence: high
163
+ A plausible store -> render -> error propagation path was found.
164
+
144
165
  $ vite-browser diagnose hmr --limit 50
145
166
  # HMR Diagnosis
146
167
  ## missing-module
@@ -161,6 +182,8 @@ Suggestion: Verify the import path, file extension, alias configuration, and whe
161
182
  - HMR summary/timeline/clear
162
183
  - module-graph snapshot/diff/clear
163
184
  - error/HMR correlation over recent event windows
185
+ - render/store propagation correlation over recent event windows
186
+ - early propagation diagnosis with store updates, changed keys, and render paths
164
187
  - rule-based HMR diagnosis with confidence levels
165
188
  - source-mapped errors with optional inline source snippet
166
189
  - Debug utilities: console logs, network tracing, screenshot, page `eval`
@@ -173,6 +196,8 @@ Suggestion: Verify the import path, file extension, alias configuration, and whe
173
196
  vite-browser vite runtime
174
197
  vite-browser errors --mapped --inline-source
175
198
  vite-browser correlate errors --mapped --window 5000
199
+ vite-browser correlate renders --window 5000
200
+ vite-browser diagnose propagation --window 5000
176
201
  vite-browser diagnose hmr --limit 50
177
202
  vite-browser vite hmr trace --limit 50
178
203
  vite-browser vite module-graph trace --limit 200
@@ -201,13 +226,14 @@ vite-browser svelte tree
201
226
 
202
227
  ## Current Boundaries
203
228
 
204
- `vite-browser` v0.2.2 is strong at:
229
+ `vite-browser` v0.3.0 is strong at:
205
230
 
206
231
  - surfacing runtime state as structured shell output
207
232
  - linking current errors to recent HMR/module activity
208
233
  - detecting several common HMR failure patterns quickly
234
+ - narrowing likely store/module -> render paths in Vue-first flows
209
235
 
210
- It is not yet a full propagation-trace engine. In particular, it does not reliably infer deep chains like `store -> component A -> component B -> error` across arbitrary component graphs.
236
+ `v0.3.0` is still not a full propagation-trace engine. Treat `correlate renders` and `diagnose propagation` as high-confidence propagation clues, not strict causal proof. In particular, they do not reliably infer deep chains like `store -> component A -> component B -> error` across arbitrary component graphs, and they intentionally fall back to conservative output when evidence is incomplete.
211
237
 
212
238
  ## Command Reference
213
239
 
@@ -247,8 +273,10 @@ vite-browser errors
247
273
  vite-browser errors --mapped
248
274
  vite-browser errors --mapped --inline-source
249
275
  vite-browser correlate errors [--window <ms>]
276
+ vite-browser correlate renders [--window <ms>]
250
277
  vite-browser correlate errors --mapped --inline-source
251
278
  vite-browser diagnose hmr [--window <ms>] [--limit <n>]
279
+ vite-browser diagnose propagation [--window <ms>]
252
280
  ```
253
281
 
254
282
  ### Utilities
@@ -0,0 +1,3 @@
1
+ import type { VBEvent } from "./event-queue.js";
2
+ export declare function initBrowserEventCollector(): void;
3
+ export declare function readBrowserEvents(): VBEvent[];
@@ -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,27 @@
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 BrowserSessionState = {
12
+ context: BrowserContext | null;
13
+ page: Page | null;
14
+ framework: BrowserFramework;
15
+ extensionModeDisabled: boolean;
16
+ consoleLogs: string[];
17
+ hmrEvents: HmrEvent[];
18
+ lastReactSnapshot: ReactNode[];
19
+ lastModuleGraphUrls: string[] | null;
20
+ };
21
+ export declare function createBrowserSessionState(): BrowserSessionState;
22
+ export declare function getCurrentPage(state: BrowserSessionState): Page | null;
23
+ export declare function resetBrowserSessionState(state: BrowserSessionState): void;
24
+ export declare function ensureBrowserPage(state: BrowserSessionState, onPageReady: (page: Page) => void): Promise<Page>;
25
+ export declare function closeBrowserSession(state: BrowserSessionState): Promise<void>;
26
+ export declare function contextUsable(current: Pick<BrowserContext, "pages"> | null): current is Pick<BrowserContext, "pages">;
27
+ export declare function isClosedTargetError(error: unknown): boolean;
@@ -0,0 +1,113 @@
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
+ consoleLogs: [],
17
+ hmrEvents: [],
18
+ lastReactSnapshot: [],
19
+ lastModuleGraphUrls: null,
20
+ };
21
+ }
22
+ export function getCurrentPage(state) {
23
+ if (!contextUsable(state.context))
24
+ return null;
25
+ if (!state.page || state.page.isClosed())
26
+ return null;
27
+ return state.page;
28
+ }
29
+ export function resetBrowserSessionState(state) {
30
+ state.context = null;
31
+ state.page = null;
32
+ state.framework = "unknown";
33
+ state.consoleLogs.length = 0;
34
+ state.hmrEvents.length = 0;
35
+ state.lastModuleGraphUrls = null;
36
+ state.lastReactSnapshot = [];
37
+ }
38
+ export async function ensureBrowserPage(state, onPageReady) {
39
+ if (!contextUsable(state.context)) {
40
+ await closeBrowserSession(state);
41
+ const launched = await launchBrowserContext(state.extensionModeDisabled);
42
+ state.context = launched.context;
43
+ state.extensionModeDisabled = launched.extensionModeDisabled;
44
+ }
45
+ if (!state.context)
46
+ throw new Error("browser not open");
47
+ if (!state.page || state.page.isClosed()) {
48
+ try {
49
+ state.page = state.context.pages()[0] ?? (await state.context.newPage());
50
+ }
51
+ catch (error) {
52
+ if (!isClosedTargetError(error))
53
+ throw error;
54
+ await closeBrowserSession(state);
55
+ state.extensionModeDisabled = true;
56
+ const launched = await launchBrowserContext(state.extensionModeDisabled);
57
+ state.context = launched.context;
58
+ state.extensionModeDisabled = launched.extensionModeDisabled;
59
+ state.page = state.context.pages()[0] ?? (await state.context.newPage());
60
+ }
61
+ onPageReady(state.page);
62
+ }
63
+ return state.page;
64
+ }
65
+ export async function closeBrowserSession(state) {
66
+ await state.context?.close();
67
+ resetBrowserSessionState(state);
68
+ }
69
+ export function contextUsable(current) {
70
+ if (!current)
71
+ return false;
72
+ try {
73
+ current.pages();
74
+ return true;
75
+ }
76
+ catch {
77
+ return false;
78
+ }
79
+ }
80
+ export function isClosedTargetError(error) {
81
+ if (!(error instanceof Error))
82
+ return false;
83
+ return /Target page, context or browser has been closed/i.test(error.message);
84
+ }
85
+ async function launchBrowserContext(extensionModeDisabled) {
86
+ if (hasReactExtension && installHook && !extensionModeDisabled) {
87
+ try {
88
+ const context = await chromium.launchPersistentContext("", {
89
+ headless: false,
90
+ viewport: { width: 1280, height: 720 },
91
+ args: [
92
+ `--disable-extensions-except=${extensionPath}`,
93
+ `--load-extension=${extensionPath}`,
94
+ "--auto-open-devtools-for-tabs",
95
+ ],
96
+ });
97
+ await context.waitForEvent("serviceworker").catch(() => { });
98
+ await context.addInitScript(installHook);
99
+ return { context, extensionModeDisabled };
100
+ }
101
+ catch {
102
+ extensionModeDisabled = true;
103
+ }
104
+ }
105
+ const browser = await chromium.launch({
106
+ headless: false,
107
+ args: ["--auto-open-devtools-for-tabs"],
108
+ });
109
+ return {
110
+ context: await browser.newContext({ viewport: { width: 1280, height: 720 } }),
111
+ extensionModeDisabled,
112
+ };
113
+ }
@@ -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>;