@presto1314w/vite-devtools-browser 0.1.4 → 0.2.2

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
@@ -1,10 +1,66 @@
1
1
  # vite-browser
2
2
 
3
- `vite-browser` is a debugging toolchain for Vite apps:
3
+ `vite-browser` is a runtime diagnostics toolchain for Vite applications.
4
+
5
+ It gives agents and developers structured access to:
6
+
7
+ - Vue, React, and Svelte runtime state
8
+ - Vite HMR activity and runtime health
9
+ - event-window correlation between current errors and recent hot updates
10
+ - rule-based HMR diagnosis with confidence levels
11
+ - module graph snapshots and diffs
12
+ - mapped error output with optional source snippets
13
+ - network, logs, screenshots, and page evaluation
14
+
15
+ It ships in two forms:
16
+
4
17
  - Agent Skill: scenario-based debugging workflows for coding assistants
5
- - CLI Runtime (`@presto1314w/vite-devtools-browser`): structured inspection of Vue/React/Svelte runtime state
18
+ - CLI Runtime (`@presto1314w/vite-devtools-browser`): structured shell commands for local Vite debugging
19
+
20
+ Current documented baseline: `v0.2.2`.
21
+
22
+ ## What's New In v0.2
23
+
24
+ `v0.2.x` moves `vite-browser` from snapshot-style inspection toward runtime diagnosis:
25
+
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
30
+
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
36
+
37
+ ## Built For Agents
38
+
39
+ `vite-browser` is designed for agent workflows as much as local debugging.
40
+
41
+ Models do not work well with a DevTools panel that has to be visually inspected step by step. They work much better when runtime signals are exposed as structured commands that can be queried, compared, and chained in a loop. `vite-browser` turns framework state, Vite runtime status, HMR activity, module graph changes, mapped errors, logs, and network activity into terminal output that an agent can actually reason about.
42
+
43
+ Under the hood, each command is a one-shot request against a long-lived browser daemon. That keeps the CLI simple for users while letting agent loops inspect a running app repeatedly without having to manage browser lifecycle on every step.
44
+
45
+ ## Why vite-browser
46
+
47
+ Most browser CLIs are optimized for automation. Most framework devtools are optimized for humans in a GUI.
48
+
49
+ `vite-browser` is optimized for structured Vite runtime debugging:
6
50
 
7
- Current documented baseline: `v0.1.4`.
51
+ - it can inspect framework state like a devtools bridge
52
+ - it can explain Vite-specific behavior like HMR updates and module graph changes
53
+ - it can correlate recent updates with current failures
54
+ - it returns structured text that AI agents can consume directly in loops
55
+
56
+ ## Positioning
57
+
58
+ | Tool | Best for | Notable gap compared with `vite-browser` |
59
+ | --- | --- | --- |
60
+ | `agent-browser` | general browser automation | not focused on Vite runtime diagnostics |
61
+ | `next-browser` | Next.js + React debugging | not designed as a Vite runtime tool |
62
+ | `vite-plugin-vue-mcp` | Vue MCP integration inside Vite | plugin/MCP-first, not a standalone diagnostics CLI |
63
+ | `vite-browser` | Vite runtime diagnostics for agents and developers | browser lifecycle coverage still being expanded |
8
64
 
9
65
  ## Install
10
66
 
@@ -31,16 +87,69 @@ npm run dev
31
87
  # terminal B: inspect runtime
32
88
  vite-browser open http://localhost:5173
33
89
  vite-browser detect
34
- vite-browser vue tree
35
- vite-browser vue pinia
36
90
  vite-browser vite runtime
91
+ vite-browser errors --mapped --inline-source
92
+ vite-browser correlate errors --mapped --window 5000
93
+ vite-browser diagnose hmr --limit 50
37
94
  vite-browser vite hmr trace --limit 20
38
95
  vite-browser vite module-graph trace --limit 50
39
- vite-browser errors --mapped --inline-source
40
96
  vite-browser network
41
97
  vite-browser close
42
98
  ```
43
99
 
100
+ For component/state debugging, then branch into framework-specific commands:
101
+
102
+ ```bash
103
+ vite-browser vue tree
104
+ vite-browser vue pinia
105
+ vite-browser vue router
106
+ vite-browser react tree
107
+ vite-browser svelte tree
108
+ ```
109
+
110
+ ## What It Looks Like
111
+
112
+ ```bash
113
+ $ vite-browser vite runtime
114
+ # Vite Runtime
115
+ URL: http://localhost:5173/
116
+ Framework: vue
117
+ Vite Client: loaded
118
+ HMR Socket: open
119
+ Error Overlay: none
120
+ Tracked HMR Events: 3
121
+
122
+ $ vite-browser vite hmr trace --limit 5
123
+ # HMR Trace
124
+ [12:34:10] connected [vite] connected.
125
+ [12:34:15] update /src/App.vue
126
+
127
+ $ vite-browser errors --mapped --inline-source
128
+ Failed to resolve import "./missing"
129
+
130
+ # Mapped Stack
131
+ - http://localhost:5173/src/main.ts:12:4 -> /src/main.ts:12:4
132
+ 12 | import "./missing"
133
+
134
+ $ vite-browser correlate errors --mapped --window 5000
135
+ # Error Correlation
136
+ ## Current Error
137
+ TypeError: boom at /src/App.tsx:4:2
138
+
139
+ ## Correlation
140
+ Confidence: high
141
+ HMR update observed within 5000ms of the current error
142
+ Matching modules: /src/App.tsx
143
+
144
+ $ vite-browser diagnose hmr --limit 50
145
+ # HMR Diagnosis
146
+ ## missing-module
147
+ Status: fail
148
+ Confidence: high
149
+ A module import failed to resolve during HMR.
150
+ Suggestion: Verify the import path, file extension, alias configuration, and whether the module exists on disk.
151
+ ```
152
+
44
153
  ## Core Capabilities
45
154
 
46
155
  - Framework detection: Vue/React/Svelte best-effort detection and version hinting
@@ -51,9 +160,55 @@ vite-browser close
51
160
  - runtime status summary
52
161
  - HMR summary/timeline/clear
53
162
  - module-graph snapshot/diff/clear
163
+ - error/HMR correlation over recent event windows
164
+ - rule-based HMR diagnosis with confidence levels
54
165
  - source-mapped errors with optional inline source snippet
55
166
  - Debug utilities: console logs, network tracing, screenshot, page `eval`
56
167
 
168
+ ## Recommended Workflows
169
+
170
+ ### Runtime/HMR triage
171
+
172
+ ```bash
173
+ vite-browser vite runtime
174
+ vite-browser errors --mapped --inline-source
175
+ vite-browser correlate errors --mapped --window 5000
176
+ vite-browser diagnose hmr --limit 50
177
+ vite-browser vite hmr trace --limit 50
178
+ vite-browser vite module-graph trace --limit 200
179
+ ```
180
+
181
+ ### Data/API triage
182
+
183
+ ```bash
184
+ vite-browser errors --mapped
185
+ vite-browser logs
186
+ vite-browser network
187
+ vite-browser network <idx>
188
+ vite-browser eval '<state probe>'
189
+ ```
190
+
191
+ ### Component/state triage
192
+
193
+ ```bash
194
+ vite-browser detect
195
+ vite-browser vue tree
196
+ vite-browser vue pinia
197
+ vite-browser vue router
198
+ vite-browser react tree
199
+ vite-browser svelte tree
200
+ ```
201
+
202
+ ## Current Boundaries
203
+
204
+ `vite-browser` v0.2.2 is strong at:
205
+
206
+ - surfacing runtime state as structured shell output
207
+ - linking current errors to recent HMR/module activity
208
+ - detecting several common HMR failure patterns quickly
209
+
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.
211
+
57
212
  ## Command Reference
58
213
 
59
214
  ### Browser
@@ -91,6 +246,9 @@ vite-browser vite module-graph clear
91
246
  vite-browser errors
92
247
  vite-browser errors --mapped
93
248
  vite-browser errors --mapped --inline-source
249
+ vite-browser correlate errors [--window <ms>]
250
+ vite-browser correlate errors --mapped --inline-source
251
+ vite-browser diagnose hmr [--window <ms>] [--limit <n>]
94
252
  ```
95
253
 
96
254
  ### Utilities
@@ -124,6 +282,10 @@ pnpm test:evals
124
282
  pnpm test:evals:e2e
125
283
  ```
126
284
 
285
+ ## Discovery
286
+
287
+ If you want to introduce the project to new users, start with the launch kit in [docs/launch-kit.md](./docs/launch-kit.md).
288
+
127
289
  ## Requirements
128
290
 
129
291
  - Node.js `>=20`
package/dist/browser.d.ts CHANGED
@@ -1,5 +1,6 @@
1
- import { type BrowserContext } from "playwright";
1
+ import { type BrowserContext, type Page } from "playwright";
2
2
  import { resolveViaSourceMap } from "./sourcemap.js";
3
+ import { EventQueue } from "./event-queue.js";
3
4
  type HmrEventType = "connecting" | "connected" | "update" | "full-reload" | "error" | "log";
4
5
  export type HmrEvent = {
5
6
  timestamp: number;
@@ -20,6 +21,13 @@ export type ModuleRow = {
20
21
  durationMs: number;
21
22
  };
22
23
  export type ModuleGraphMode = "snapshot" | "trace" | "clear";
24
+ export declare function setEventQueue(queue: EventQueue): void;
25
+ export declare function getEventQueue(): EventQueue | null;
26
+ export declare function getCurrentPage(): Page | null;
27
+ /**
28
+ * Flush browser events into daemon event queue
29
+ */
30
+ export declare function flushBrowserEvents(currentPage: Page, queue: EventQueue): Promise<void>;
23
31
  export declare function open(url: string | undefined): Promise<void>;
24
32
  export declare function cookies(cookies: {
25
33
  name: string;
package/dist/browser.js CHANGED
@@ -17,16 +17,107 @@ let context = null;
17
17
  let page = null;
18
18
  let framework = "unknown";
19
19
  let extensionModeDisabled = false;
20
+ let eventQueue = null;
20
21
  const consoleLogs = [];
21
22
  const MAX_LOGS = 200;
22
23
  const MAX_HMR_EVENTS = 500;
23
24
  let lastReactSnapshot = [];
24
25
  const hmrEvents = [];
25
26
  let lastModuleGraphUrls = null;
27
+ export function setEventQueue(queue) {
28
+ eventQueue = queue;
29
+ }
30
+ export function getEventQueue() {
31
+ return eventQueue;
32
+ }
33
+ export function getCurrentPage() {
34
+ if (!contextUsable(context))
35
+ return null;
36
+ if (!page || page.isClosed())
37
+ return null;
38
+ return page;
39
+ }
40
+ /**
41
+ * Inject browser-side event collector into the page
42
+ */
43
+ async function injectEventCollector(currentPage) {
44
+ await currentPage.evaluate(() => {
45
+ if (window.__vb_events)
46
+ return; // already injected
47
+ window.__vb_events = [];
48
+ window.__vb_push = (event) => {
49
+ const q = window.__vb_events;
50
+ q.push(event);
51
+ if (q.length > 1000)
52
+ q.shift();
53
+ };
54
+ // Subscribe to Vite HMR WebSocket
55
+ function attachViteListener() {
56
+ const hot = window.__vite_hot;
57
+ if (hot?.ws) {
58
+ hot.ws.addEventListener('message', (e) => {
59
+ try {
60
+ const data = JSON.parse(e.data);
61
+ window.__vb_push({
62
+ timestamp: Date.now(),
63
+ type: data.type === 'error' ? 'hmr-error' : 'hmr-update',
64
+ payload: data
65
+ });
66
+ }
67
+ catch { }
68
+ });
69
+ return true;
70
+ }
71
+ return false;
72
+ }
73
+ // Retry if __vite_hot not ready yet
74
+ if (!attachViteListener()) {
75
+ let attempts = 0;
76
+ const timer = setInterval(() => {
77
+ attempts++;
78
+ if (attachViteListener() || attempts >= 50) {
79
+ clearInterval(timer);
80
+ }
81
+ }, 100);
82
+ }
83
+ // Hook window.onerror for runtime errors
84
+ const origOnError = window.onerror;
85
+ window.onerror = (msg, src, line, col, err) => {
86
+ window.__vb_push({
87
+ timestamp: Date.now(),
88
+ type: 'error',
89
+ payload: { message: String(msg), source: src, line, col, stack: err?.stack }
90
+ });
91
+ return origOnError ? origOnError(msg, src, line, col, err) : false;
92
+ };
93
+ // Hook unhandledrejection
94
+ window.addEventListener('unhandledrejection', (e) => {
95
+ window.__vb_push({
96
+ timestamp: Date.now(),
97
+ type: 'error',
98
+ payload: { message: e.reason?.message, stack: e.reason?.stack }
99
+ });
100
+ });
101
+ });
102
+ }
103
+ /**
104
+ * Flush browser events into daemon event queue
105
+ */
106
+ export async function flushBrowserEvents(currentPage, queue) {
107
+ const raw = await currentPage.evaluate(() => {
108
+ const events = window.__vb_events ?? [];
109
+ window.__vb_events = []; // clear after flush
110
+ return events;
111
+ });
112
+ for (const e of raw) {
113
+ queue.push(e);
114
+ }
115
+ }
26
116
  export async function open(url) {
27
117
  const currentPage = await ensurePage();
28
118
  if (url) {
29
119
  await currentPage.goto(url, { waitUntil: "domcontentloaded" });
120
+ await injectEventCollector(currentPage);
30
121
  await detectFramework();
31
122
  }
32
123
  }
@@ -256,6 +347,7 @@ export function formatModuleGraphTrace(currentUrls, previousUrls, filter, limit
256
347
  export async function goto(url) {
257
348
  const currentPage = await ensurePage();
258
349
  await currentPage.goto(url, { waitUntil: "domcontentloaded" });
350
+ await injectEventCollector(currentPage);
259
351
  await detectFramework();
260
352
  return currentPage.url();
261
353
  }
package/dist/cli.js CHANGED
@@ -136,6 +136,21 @@ export async function runCli(argv, io) {
136
136
  const res = await io.send("errors", { mapped, inlineSource });
137
137
  exit(io, res, res.ok && res.data ? String(res.data) : "no errors");
138
138
  }
139
+ if (cmd === "correlate" && arg === "errors") {
140
+ const mapped = args.includes("--mapped");
141
+ const inlineSource = args.includes("--inline-source");
142
+ const windowMs = parseNumberFlag(args, "--window", 5000);
143
+ const res = await io.send("correlate-errors", { mapped, inlineSource, windowMs });
144
+ exit(io, res, res.ok && res.data ? String(res.data) : "");
145
+ }
146
+ if (cmd === "diagnose" && arg === "hmr") {
147
+ const mapped = args.includes("--mapped");
148
+ const inlineSource = args.includes("--inline-source");
149
+ const windowMs = parseNumberFlag(args, "--window", 5000);
150
+ const limit = parseNumberFlag(args, "--limit", 50);
151
+ const res = await io.send("diagnose-hmr", { mapped, inlineSource, windowMs, limit });
152
+ exit(io, res, res.ok && res.data ? String(res.data) : "");
153
+ }
139
154
  if (cmd === "logs") {
140
155
  const res = await io.send("logs");
141
156
  exit(io, res, res.ok && res.data ? String(res.data) : "");
@@ -211,6 +226,10 @@ VITE COMMANDS
211
226
  errors Show build/runtime errors
212
227
  errors --mapped Show errors with source-map mapping
213
228
  errors --mapped --inline-source Include mapped source snippets
229
+ correlate errors [--window <ms>] Correlate current errors with recent HMR events
230
+ correlate errors --mapped Correlate mapped errors with recent HMR events
231
+ diagnose hmr [--window <ms>] Diagnose HMR failures from runtime, errors, and trace data
232
+ diagnose hmr [--limit <n>] Control how many recent HMR trace entries are inspected
214
233
  logs Show dev server logs
215
234
 
216
235
  UTILITIES
@@ -0,0 +1,20 @@
1
+ import type { VBEvent } from "./event-queue.js";
2
+ export type CorrelationConfidence = "high" | "medium" | "low";
3
+ export type ErrorCorrelation = {
4
+ summary: string;
5
+ detail: string;
6
+ confidence: CorrelationConfidence;
7
+ windowMs: number;
8
+ matchingModules: string[];
9
+ relatedEvents: VBEvent[];
10
+ };
11
+ export type RenderNetworkCorrelation = {
12
+ summary: string;
13
+ detail: string;
14
+ confidence: CorrelationConfidence;
15
+ requestCount: number;
16
+ urls: string[];
17
+ };
18
+ export declare function correlateErrorWithHMR(errorText: string, events: VBEvent[], windowMs?: number): ErrorCorrelation | null;
19
+ export declare function correlateRenderWithNetwork(events: VBEvent[], requestThreshold?: number): RenderNetworkCorrelation | null;
20
+ export declare function formatErrorCorrelationReport(errorText: string, correlation: ErrorCorrelation | null): string;
@@ -0,0 +1,110 @@
1
+ const MODULE_PATTERNS = [
2
+ /\/src\/[^\s"'`):]+/g,
3
+ /\/@fs\/[^\s"'`):]+/g,
4
+ /[A-Za-z]:\\[^:\n]+/g,
5
+ ];
6
+ export function correlateErrorWithHMR(errorText, events, windowMs = 5000) {
7
+ const recentEvents = events.filter((event) => event.type === "hmr-update" || event.type === "hmr-error");
8
+ if (recentEvents.length === 0)
9
+ return null;
10
+ const errorModules = extractModules(errorText);
11
+ const matchedEvents = recentEvents.filter((event) => {
12
+ const modules = extractModulesFromEvent(event);
13
+ if (errorModules.length === 0)
14
+ return event.type === "hmr-error";
15
+ return modules.some((module) => errorModules.includes(module));
16
+ });
17
+ const relatedEvents = matchedEvents.length > 0 ? matchedEvents : recentEvents;
18
+ const matchingModules = unique(relatedEvents.flatMap((event) => extractModulesFromEvent(event)).filter((module) => errorModules.includes(module)));
19
+ const confidence = inferConfidence(errorModules, matchingModules, relatedEvents);
20
+ const eventKind = relatedEvents.some((event) => event.type === "hmr-error") ? "HMR error" : "HMR update";
21
+ const moduleText = matchingModules.length > 0
22
+ ? `Matching modules: ${matchingModules.join(", ")}`
23
+ : errorModules.length > 0
24
+ ? `Error modules: ${errorModules.join(", ")}`
25
+ : "No module path overlap; correlation is time-window based.";
26
+ return {
27
+ summary: `${eventKind} observed within ${windowMs}ms of the current error`,
28
+ detail: `${moduleText}\nRecent events considered: ${relatedEvents.length}`,
29
+ confidence,
30
+ windowMs,
31
+ matchingModules,
32
+ relatedEvents,
33
+ };
34
+ }
35
+ export function correlateRenderWithNetwork(events, requestThreshold = 3) {
36
+ const renderEvents = events.filter((event) => event.type === "render");
37
+ const networkEvents = events.filter((event) => event.type === "network");
38
+ if (renderEvents.length === 0 || networkEvents.length === 0)
39
+ return null;
40
+ const latestRender = renderEvents[renderEvents.length - 1];
41
+ const start = latestRender.timestamp - 1000;
42
+ const end = latestRender.timestamp + 1000;
43
+ const overlappingNetwork = networkEvents.filter((event) => event.timestamp >= start && event.timestamp <= end);
44
+ const urls = unique(overlappingNetwork
45
+ .map((event) => event.payload.url)
46
+ .filter((url) => typeof url === "string"));
47
+ if (overlappingNetwork.length < requestThreshold)
48
+ return null;
49
+ return {
50
+ summary: "Repeated network activity detected around a render event",
51
+ detail: `Observed ${overlappingNetwork.length} network requests near the latest render. URLs: ${urls.join(", ")}`,
52
+ confidence: overlappingNetwork.length >= requestThreshold + 2 ? "high" : "medium",
53
+ requestCount: overlappingNetwork.length,
54
+ urls,
55
+ };
56
+ }
57
+ export function formatErrorCorrelationReport(errorText, correlation) {
58
+ const lines = ["# Error Correlation", "", "## Current Error", errorText.trim() || "(empty error)"];
59
+ if (!correlation) {
60
+ lines.push("", "## Correlation", "No recent HMR events correlated with the current error.");
61
+ return lines.join("\n");
62
+ }
63
+ lines.push("", "## Correlation", `Confidence: ${correlation.confidence}`, correlation.summary, correlation.detail, "", "## Related Events", ...correlation.relatedEvents.map((event) => formatEventLine(event)));
64
+ return lines.join("\n");
65
+ }
66
+ function formatEventLine(event) {
67
+ const payload = event.payload;
68
+ const path = payload.path;
69
+ const message = payload.message;
70
+ if (typeof path === "string")
71
+ return `- ${event.type}: ${path}`;
72
+ if (typeof message === "string")
73
+ return `- ${event.type}: ${message}`;
74
+ return `- ${event.type}: ${JSON.stringify(payload)}`;
75
+ }
76
+ function inferConfidence(errorModules, matchingModules, events) {
77
+ if (matchingModules.length > 0)
78
+ return "high";
79
+ if (events.some((event) => event.type === "hmr-error"))
80
+ return "medium";
81
+ if (errorModules.length === 0 && events.length > 0)
82
+ return "low";
83
+ return "medium";
84
+ }
85
+ function extractModulesFromEvent(event) {
86
+ const payload = event.payload;
87
+ const candidates = [];
88
+ if (typeof payload.path === "string")
89
+ candidates.push(payload.path);
90
+ if (typeof payload.message === "string")
91
+ candidates.push(payload.message);
92
+ if (Array.isArray(payload.updates)) {
93
+ for (const update of payload.updates) {
94
+ if (update && typeof update === "object" && typeof update.path === "string") {
95
+ candidates.push(update.path);
96
+ }
97
+ }
98
+ }
99
+ return unique(candidates.flatMap((candidate) => extractModules(candidate)));
100
+ }
101
+ function extractModules(text) {
102
+ const matches = MODULE_PATTERNS.flatMap((pattern) => text.match(pattern) ?? []);
103
+ return unique(matches.map(normalizeModulePath).filter(Boolean));
104
+ }
105
+ function normalizeModulePath(value) {
106
+ return value.replace(/[),.:]+$/, "");
107
+ }
108
+ function unique(values) {
109
+ return [...new Set(values)];
110
+ }
package/dist/daemon.d.ts CHANGED
@@ -9,6 +9,7 @@ export type Cmd = {
9
9
  idx?: number;
10
10
  mode?: "summary" | "trace" | "clear" | "snapshot";
11
11
  limit?: number;
12
+ windowMs?: number;
12
13
  filter?: string;
13
14
  mapped?: boolean;
14
15
  inlineSource?: boolean;
package/dist/daemon.js CHANGED
@@ -2,7 +2,11 @@ import { createServer } from "node:net";
2
2
  import { mkdirSync, writeFileSync, rmSync } from "node:fs";
3
3
  import { fileURLToPath } from "node:url";
4
4
  import * as browser from "./browser.js";
5
+ import { correlateErrorWithHMR, formatErrorCorrelationReport } from "./correlate.js";
6
+ import { diagnoseHMR, formatDiagnosisReport } from "./diagnose.js";
5
7
  import { socketDir, socketPath, pidFile } from "./paths.js";
8
+ import { EventQueue } from "./event-queue.js";
9
+ import * as networkLog from "./network.js";
6
10
  export function cleanError(err) {
7
11
  if (!(err instanceof Error))
8
12
  return String(err);
@@ -12,6 +16,19 @@ export function cleanError(err) {
12
16
  }
13
17
  export function createRunner(api = browser) {
14
18
  return async function run(cmd) {
19
+ // Flush browser events to daemon queue before processing command
20
+ const queue = api.getEventQueue();
21
+ if (queue) {
22
+ try {
23
+ const currentPage = api.getCurrentPage();
24
+ if (currentPage) {
25
+ await api.flushBrowserEvents(currentPage, queue);
26
+ }
27
+ }
28
+ catch {
29
+ // Ignore flush errors (page might not be open yet)
30
+ }
31
+ }
15
32
  // Browser control
16
33
  if (cmd.action === "open") {
17
34
  await api.open(cmd.url);
@@ -88,6 +105,21 @@ export function createRunner(api = browser) {
88
105
  const data = await api.errors(Boolean(cmd.mapped), Boolean(cmd.inlineSource));
89
106
  return { ok: true, data };
90
107
  }
108
+ if (cmd.action === "correlate-errors") {
109
+ const errorText = String(await api.errors(Boolean(cmd.mapped), Boolean(cmd.inlineSource)));
110
+ const events = queue ? queue.window(cmd.windowMs ?? 5000) : [];
111
+ const data = formatErrorCorrelationReport(errorText, errorText === "no errors" ? null : correlateErrorWithHMR(errorText, events, cmd.windowMs ?? 5000));
112
+ return { ok: true, data };
113
+ }
114
+ if (cmd.action === "diagnose-hmr") {
115
+ const errorText = String(await api.errors(Boolean(cmd.mapped), Boolean(cmd.inlineSource)));
116
+ const runtimeText = String(await api.viteRuntimeStatus());
117
+ const hmrTraceText = String(await api.viteHMRTrace("trace", cmd.limit ?? 50));
118
+ const events = queue ? queue.window(cmd.windowMs ?? 5000) : [];
119
+ const correlation = errorText === "no errors" ? null : correlateErrorWithHMR(errorText, events, cmd.windowMs ?? 5000);
120
+ const data = formatDiagnosisReport(diagnoseHMR({ errorText, runtimeText, hmrTraceText, correlation }));
121
+ return { ok: true, data };
122
+ }
91
123
  if (cmd.action === "logs") {
92
124
  const data = await api.logs();
93
125
  return { ok: true, data };
@@ -123,6 +155,10 @@ export async function dispatchLine(line, socket, run = createRunner(), onClose)
123
155
  setImmediate(() => onClose?.());
124
156
  }
125
157
  export function startDaemon() {
158
+ // Initialize event queue
159
+ const eventQueue = new EventQueue(1000);
160
+ browser.setEventQueue(eventQueue);
161
+ networkLog.setEventQueue(eventQueue);
126
162
  const run = createRunner();
127
163
  mkdirSync(socketDir, { recursive: true, mode: 0o700 });
128
164
  removeSocketFile();
@@ -0,0 +1,19 @@
1
+ import type { ErrorCorrelation } from "./correlate.js";
2
+ export type DiagnosisStatus = "pass" | "warn" | "fail";
3
+ export type DiagnosisConfidence = "high" | "medium" | "low";
4
+ export type DiagnosisResult = {
5
+ code: "circular-dependency" | "missing-module" | "hmr-websocket-closed" | "repeated-full-reload";
6
+ status: DiagnosisStatus;
7
+ confidence: DiagnosisConfidence;
8
+ summary: string;
9
+ detail: string;
10
+ suggestion: string;
11
+ };
12
+ export type DiagnoseInput = {
13
+ errorText: string;
14
+ runtimeText: string;
15
+ hmrTraceText: string;
16
+ correlation: ErrorCorrelation | null;
17
+ };
18
+ export declare function diagnoseHMR(input: DiagnoseInput): DiagnosisResult[];
19
+ export declare function formatDiagnosisReport(results: DiagnosisResult[]): string;
@@ -0,0 +1,111 @@
1
+ export function diagnoseHMR(input) {
2
+ const results = [
3
+ detectCircularDependency(input),
4
+ detectMissingModule(input),
5
+ detectClosedWebsocket(input),
6
+ detectRepeatedFullReload(input),
7
+ ]
8
+ .filter((result) => result !== null)
9
+ .sort(compareDiagnosisResults);
10
+ if (results.length > 0)
11
+ return results;
12
+ return [
13
+ {
14
+ code: "hmr-websocket-closed",
15
+ status: "pass",
16
+ confidence: "low",
17
+ summary: "No obvious HMR failure pattern detected",
18
+ detail: "Runtime, current error text, and HMR trace did not match any built-in failure rules.",
19
+ suggestion: "If symptoms persist, inspect `vite hmr trace`, `network`, and `correlate errors` output together.",
20
+ },
21
+ ];
22
+ }
23
+ export function formatDiagnosisReport(results) {
24
+ const lines = ["# HMR Diagnosis", ""];
25
+ for (const result of results) {
26
+ lines.push(`## ${result.code}`, `Status: ${result.status}`, `Confidence: ${result.confidence}`, result.summary, result.detail, `Suggestion: ${result.suggestion}`, "");
27
+ }
28
+ return lines.join("\n").trimEnd();
29
+ }
30
+ function detectCircularDependency(input) {
31
+ const text = `${input.errorText}\n${input.hmrTraceText}`;
32
+ if (!/circular (dependency|import)|import cycle/i.test(text))
33
+ return null;
34
+ const moduleText = input.correlation?.matchingModules.length
35
+ ? `Likely modules: ${input.correlation.matchingModules.join(", ")}.`
36
+ : "The error text points to a circular import/dependency chain.";
37
+ return {
38
+ code: "circular-dependency",
39
+ status: "fail",
40
+ confidence: input.correlation?.matchingModules.length ? "high" : "medium",
41
+ summary: "HMR is likely breaking because of a circular dependency.",
42
+ detail: `${moduleText} Circular imports often prevent safe hot replacement and force stale state or reload loops.`,
43
+ suggestion: "Break the import cycle by extracting shared code into a leaf module or switching one edge to a lazy import.",
44
+ };
45
+ }
46
+ function detectMissingModule(input) {
47
+ const text = input.errorText;
48
+ if (!/failed to resolve import|cannot find module|could not resolve|does the file exist/i.test(text)) {
49
+ return null;
50
+ }
51
+ return {
52
+ code: "missing-module",
53
+ status: "fail",
54
+ confidence: "high",
55
+ summary: "A module import failed to resolve during HMR.",
56
+ detail: input.correlation?.matchingModules.length
57
+ ? `The current error overlaps with recent updates to ${input.correlation.matchingModules.join(", ")}.`
58
+ : "The current error text matches a missing or unresolved import pattern.",
59
+ suggestion: "Verify the import path, file extension, alias configuration, and whether the module exists on disk.",
60
+ };
61
+ }
62
+ function detectClosedWebsocket(input) {
63
+ const runtimeState = extractRuntimeSocketState(input.runtimeText);
64
+ const runtimeClosed = runtimeState === "closed" || runtimeState === "closing";
65
+ const traceClosed = /disconnected|failed to connect|connection lost|ws closed/i.test(input.hmrTraceText);
66
+ if (!runtimeClosed && !traceClosed)
67
+ return null;
68
+ const runtimeOnly = runtimeClosed && !traceClosed;
69
+ const traceOnly = traceClosed && !runtimeClosed;
70
+ return {
71
+ code: "hmr-websocket-closed",
72
+ status: "fail",
73
+ confidence: runtimeClosed && traceClosed ? "high" : "medium",
74
+ summary: "The HMR websocket is not healthy.",
75
+ detail: runtimeOnly
76
+ ? "Runtime status reports the HMR socket as closed or closing."
77
+ : traceOnly
78
+ ? "HMR trace contains disconnect or websocket failure messages."
79
+ : "Runtime status and HMR trace both indicate websocket instability.",
80
+ suggestion: "Check the dev server is running, the page is connected to the correct origin, and no proxy/firewall is blocking the websocket.",
81
+ };
82
+ }
83
+ function detectRepeatedFullReload(input) {
84
+ const matches = input.hmrTraceText.match(/full-reload|page reload/gi) ?? [];
85
+ if (matches.length < 2)
86
+ return null;
87
+ return {
88
+ code: "repeated-full-reload",
89
+ status: "warn",
90
+ confidence: matches.length >= 3 ? "high" : "medium",
91
+ summary: "Vite is repeatedly falling back to full page reloads.",
92
+ detail: `Observed ${matches.length} full-reload events in the recent HMR trace.`,
93
+ suggestion: "Check whether the changed module is outside HMR boundaries, introduces side effects, or triggers a circular dependency.",
94
+ };
95
+ }
96
+ function extractRuntimeSocketState(runtimeText) {
97
+ const match = runtimeText.match(/HMR Socket:\s*(open|closed|closing|connecting|unknown)/i);
98
+ const state = match?.[1]?.toLowerCase();
99
+ if (state === "open" || state === "closed" || state === "closing" || state === "connecting") {
100
+ return state;
101
+ }
102
+ return "unknown";
103
+ }
104
+ function compareDiagnosisResults(a, b) {
105
+ return scoreDiagnosis(b) - scoreDiagnosis(a);
106
+ }
107
+ function scoreDiagnosis(result) {
108
+ const statusScore = result.status === "fail" ? 30 : result.status === "warn" ? 20 : 10;
109
+ const confidenceScore = result.confidence === "high" ? 3 : result.confidence === "medium" ? 2 : 1;
110
+ return statusScore + confidenceScore;
111
+ }
@@ -0,0 +1,22 @@
1
+ export type VBEventType = 'hmr-update' | 'hmr-error' | 'module-change' | 'network' | 'error' | 'render';
2
+ export interface VBEvent {
3
+ timestamp: number;
4
+ type: VBEventType;
5
+ payload: unknown;
6
+ }
7
+ export declare class EventQueue {
8
+ private events;
9
+ private readonly maxSize;
10
+ constructor(maxSize?: number);
11
+ push(event: VBEvent): void;
12
+ /**
13
+ * Return all events within the last `ms` milliseconds before `before`
14
+ */
15
+ window(ms: number, before?: number): VBEvent[];
16
+ /**
17
+ * Return all events of a given type
18
+ */
19
+ ofType(type: VBEventType): VBEvent[];
20
+ all(): VBEvent[];
21
+ clear(): void;
22
+ }
@@ -0,0 +1,32 @@
1
+ export class EventQueue {
2
+ events = [];
3
+ maxSize;
4
+ constructor(maxSize = 1000) {
5
+ this.maxSize = maxSize;
6
+ }
7
+ push(event) {
8
+ this.events.push(event);
9
+ if (this.events.length > this.maxSize) {
10
+ this.events.shift();
11
+ }
12
+ }
13
+ /**
14
+ * Return all events within the last `ms` milliseconds before `before`
15
+ */
16
+ window(ms, before = Date.now()) {
17
+ const start = before - ms;
18
+ return this.events.filter((e) => e.timestamp >= start && e.timestamp <= before);
19
+ }
20
+ /**
21
+ * Return all events of a given type
22
+ */
23
+ ofType(type) {
24
+ return this.events.filter((e) => e.type === type);
25
+ }
26
+ all() {
27
+ return [...this.events];
28
+ }
29
+ clear() {
30
+ this.events = [];
31
+ }
32
+ }
package/dist/network.d.ts CHANGED
@@ -1,4 +1,6 @@
1
1
  import type { Page } from "playwright";
2
+ import type { EventQueue } from "./event-queue.js";
3
+ export declare function setEventQueue(queue: EventQueue): void;
2
4
  export declare function attach(page: Page): void;
3
5
  export declare function clear(): void;
4
6
  export declare function detail(idx: number): Promise<string>;
package/dist/network.js CHANGED
@@ -4,6 +4,10 @@ import { join } from "node:path";
4
4
  const BODY_INLINE_LIMIT = 4000;
5
5
  let entries = [];
6
6
  let startTime = new Map();
7
+ let eventQueue = null;
8
+ export function setEventQueue(queue) {
9
+ eventQueue = queue;
10
+ }
7
11
  export function attach(page) {
8
12
  page.on("request", (req) => {
9
13
  if (req.resourceType() === "document" && req.frame() === page.mainFrame()) {
@@ -16,7 +20,21 @@ export function attach(page) {
16
20
  const t0 = startTime.get(req);
17
21
  if (t0 == null)
18
22
  return;
19
- entries.push({ req, res, ms: Date.now() - t0 });
23
+ const ms = Date.now() - t0;
24
+ entries.push({ req, res, ms });
25
+ // Push to event queue if available
26
+ if (eventQueue) {
27
+ eventQueue.push({
28
+ timestamp: Date.now(),
29
+ type: 'network',
30
+ payload: {
31
+ url: req.url(),
32
+ method: req.method(),
33
+ status: res.status(),
34
+ ms
35
+ }
36
+ });
37
+ }
20
38
  });
21
39
  page.on("requestfailed", (req) => {
22
40
  if (!startTime.has(req))
package/package.json CHANGED
@@ -1,8 +1,22 @@
1
1
  {
2
2
  "name": "@presto1314w/vite-devtools-browser",
3
- "version": "0.1.4",
4
- "description": "CLI for programmatic access to Vue/React DevTools in Vite applications",
5
- "license": "MIT",
3
+ "version": "0.2.2",
4
+ "description": "Runtime diagnostics CLI for Vite apps with event-stream correlation, HMR diagnosis, framework inspection, and mapped errors",
5
+ "license": "MIT",
6
+ "keywords": [
7
+ "vite",
8
+ "devtools",
9
+ "debugging",
10
+ "runtime-diagnostics",
11
+ "hmr",
12
+ "module-graph",
13
+ "sourcemap",
14
+ "vue",
15
+ "react",
16
+ "svelte",
17
+ "cli",
18
+ "ai-agents"
19
+ ],
6
20
  "repository": {
7
21
  "type": "git",
8
22
  "url": "git+https://github.com/MapleCity1314/vite-browser.git"