@presto1314w/vite-devtools-browser 0.2.0 → 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,16 +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.0`.
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.0` 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:
28
+
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
30
34
 
31
35
  ## Built For Agents
32
36
 
@@ -45,6 +49,7 @@ Most browser CLIs are optimized for automation. Most framework devtools are opti
45
49
  - it can inspect framework state like a devtools bridge
46
50
  - it can explain Vite-specific behavior like HMR updates and module graph changes
47
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
48
53
  - it returns structured text that AI agents can consume directly in loops
49
54
 
50
55
  ## Positioning
@@ -84,6 +89,8 @@ vite-browser detect
84
89
  vite-browser vite runtime
85
90
  vite-browser errors --mapped --inline-source
86
91
  vite-browser correlate errors --mapped --window 5000
92
+ vite-browser correlate renders --window 5000
93
+ vite-browser diagnose propagation --window 5000
87
94
  vite-browser diagnose hmr --limit 50
88
95
  vite-browser vite hmr trace --limit 20
89
96
  vite-browser vite module-graph trace --limit 50
@@ -135,6 +142,26 @@ Confidence: high
135
142
  HMR update observed within 5000ms of the current error
136
143
  Matching modules: /src/App.tsx
137
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
+
138
165
  $ vite-browser diagnose hmr --limit 50
139
166
  # HMR Diagnosis
140
167
  ## missing-module
@@ -155,6 +182,8 @@ Suggestion: Verify the import path, file extension, alias configuration, and whe
155
182
  - HMR summary/timeline/clear
156
183
  - module-graph snapshot/diff/clear
157
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
158
187
  - rule-based HMR diagnosis with confidence levels
159
188
  - source-mapped errors with optional inline source snippet
160
189
  - Debug utilities: console logs, network tracing, screenshot, page `eval`
@@ -167,6 +196,8 @@ Suggestion: Verify the import path, file extension, alias configuration, and whe
167
196
  vite-browser vite runtime
168
197
  vite-browser errors --mapped --inline-source
169
198
  vite-browser correlate errors --mapped --window 5000
199
+ vite-browser correlate renders --window 5000
200
+ vite-browser diagnose propagation --window 5000
170
201
  vite-browser diagnose hmr --limit 50
171
202
  vite-browser vite hmr trace --limit 50
172
203
  vite-browser vite module-graph trace --limit 200
@@ -195,13 +226,14 @@ vite-browser svelte tree
195
226
 
196
227
  ## Current Boundaries
197
228
 
198
- `vite-browser` v0.2 is strong at:
229
+ `vite-browser` v0.3.0 is strong at:
199
230
 
200
231
  - surfacing runtime state as structured shell output
201
232
  - linking current errors to recent HMR/module activity
202
233
  - detecting several common HMR failure patterns quickly
234
+ - narrowing likely store/module -> render paths in Vue-first flows
203
235
 
204
- 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.
205
237
 
206
238
  ## Command Reference
207
239
 
@@ -241,8 +273,10 @@ vite-browser errors
241
273
  vite-browser errors --mapped
242
274
  vite-browser errors --mapped --inline-source
243
275
  vite-browser correlate errors [--window <ms>]
276
+ vite-browser correlate renders [--window <ms>]
244
277
  vite-browser correlate errors --mapped --inline-source
245
278
  vite-browser diagnose hmr [--window <ms>] [--limit <n>]
279
+ vite-browser diagnose propagation [--window <ms>]
246
280
  ```
247
281
 
248
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>;