@presto1314w/vite-devtools-browser 0.1.4 → 0.2.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 +162 -6
- package/dist/browser.d.ts +9 -1
- package/dist/browser.js +92 -0
- package/dist/cli.js +19 -0
- package/dist/correlate.d.ts +20 -0
- package/dist/correlate.js +110 -0
- package/dist/daemon.d.ts +1 -0
- package/dist/daemon.js +36 -0
- package/dist/diagnose.d.ts +19 -0
- package/dist/diagnose.js +88 -0
- package/dist/event-queue.d.ts +22 -0
- package/dist/event-queue.js +32 -0
- package/dist/network.d.ts +2 -0
- package/dist/network.js +19 -1
- package/package.json +17 -3
package/README.md
CHANGED
|
@@ -1,10 +1,60 @@
|
|
|
1
1
|
# vite-browser
|
|
2
2
|
|
|
3
|
-
`vite-browser` is a
|
|
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
|
|
18
|
+
- CLI Runtime (`@presto1314w/vite-devtools-browser`): structured shell commands for local Vite debugging
|
|
19
|
+
|
|
20
|
+
Current documented baseline: `v0.2.0`.
|
|
21
|
+
|
|
22
|
+
## What's New In v0.2
|
|
23
|
+
|
|
24
|
+
`v0.2.0` 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
|
+
## Built For Agents
|
|
32
|
+
|
|
33
|
+
`vite-browser` is designed for agent workflows as much as local debugging.
|
|
34
|
+
|
|
35
|
+
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.
|
|
36
|
+
|
|
37
|
+
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.
|
|
38
|
+
|
|
39
|
+
## Why vite-browser
|
|
40
|
+
|
|
41
|
+
Most browser CLIs are optimized for automation. Most framework devtools are optimized for humans in a GUI.
|
|
42
|
+
|
|
43
|
+
`vite-browser` is optimized for structured Vite runtime debugging:
|
|
44
|
+
|
|
45
|
+
- it can inspect framework state like a devtools bridge
|
|
46
|
+
- it can explain Vite-specific behavior like HMR updates and module graph changes
|
|
47
|
+
- it can correlate recent updates with current failures
|
|
48
|
+
- it returns structured text that AI agents can consume directly in loops
|
|
49
|
+
|
|
50
|
+
## Positioning
|
|
6
51
|
|
|
7
|
-
|
|
52
|
+
| Tool | Best for | Notable gap compared with `vite-browser` |
|
|
53
|
+
| --- | --- | --- |
|
|
54
|
+
| `agent-browser` | general browser automation | not focused on Vite runtime diagnostics |
|
|
55
|
+
| `next-browser` | Next.js + React debugging | not designed as a Vite runtime tool |
|
|
56
|
+
| `vite-plugin-vue-mcp` | Vue MCP integration inside Vite | plugin/MCP-first, not a standalone diagnostics CLI |
|
|
57
|
+
| `vite-browser` | Vite runtime diagnostics for agents and developers | browser lifecycle coverage still being expanded |
|
|
8
58
|
|
|
9
59
|
## Install
|
|
10
60
|
|
|
@@ -31,16 +81,69 @@ npm run dev
|
|
|
31
81
|
# terminal B: inspect runtime
|
|
32
82
|
vite-browser open http://localhost:5173
|
|
33
83
|
vite-browser detect
|
|
34
|
-
vite-browser vue tree
|
|
35
|
-
vite-browser vue pinia
|
|
36
84
|
vite-browser vite runtime
|
|
85
|
+
vite-browser errors --mapped --inline-source
|
|
86
|
+
vite-browser correlate errors --mapped --window 5000
|
|
87
|
+
vite-browser diagnose hmr --limit 50
|
|
37
88
|
vite-browser vite hmr trace --limit 20
|
|
38
89
|
vite-browser vite module-graph trace --limit 50
|
|
39
|
-
vite-browser errors --mapped --inline-source
|
|
40
90
|
vite-browser network
|
|
41
91
|
vite-browser close
|
|
42
92
|
```
|
|
43
93
|
|
|
94
|
+
For component/state debugging, then branch into framework-specific commands:
|
|
95
|
+
|
|
96
|
+
```bash
|
|
97
|
+
vite-browser vue tree
|
|
98
|
+
vite-browser vue pinia
|
|
99
|
+
vite-browser vue router
|
|
100
|
+
vite-browser react tree
|
|
101
|
+
vite-browser svelte tree
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
## What It Looks Like
|
|
105
|
+
|
|
106
|
+
```bash
|
|
107
|
+
$ vite-browser vite runtime
|
|
108
|
+
# Vite Runtime
|
|
109
|
+
URL: http://localhost:5173/
|
|
110
|
+
Framework: vue
|
|
111
|
+
Vite Client: loaded
|
|
112
|
+
HMR Socket: open
|
|
113
|
+
Error Overlay: none
|
|
114
|
+
Tracked HMR Events: 3
|
|
115
|
+
|
|
116
|
+
$ vite-browser vite hmr trace --limit 5
|
|
117
|
+
# HMR Trace
|
|
118
|
+
[12:34:10] connected [vite] connected.
|
|
119
|
+
[12:34:15] update /src/App.vue
|
|
120
|
+
|
|
121
|
+
$ vite-browser errors --mapped --inline-source
|
|
122
|
+
Failed to resolve import "./missing"
|
|
123
|
+
|
|
124
|
+
# Mapped Stack
|
|
125
|
+
- http://localhost:5173/src/main.ts:12:4 -> /src/main.ts:12:4
|
|
126
|
+
12 | import "./missing"
|
|
127
|
+
|
|
128
|
+
$ vite-browser correlate errors --mapped --window 5000
|
|
129
|
+
# Error Correlation
|
|
130
|
+
## Current Error
|
|
131
|
+
TypeError: boom at /src/App.tsx:4:2
|
|
132
|
+
|
|
133
|
+
## Correlation
|
|
134
|
+
Confidence: high
|
|
135
|
+
HMR update observed within 5000ms of the current error
|
|
136
|
+
Matching modules: /src/App.tsx
|
|
137
|
+
|
|
138
|
+
$ vite-browser diagnose hmr --limit 50
|
|
139
|
+
# HMR Diagnosis
|
|
140
|
+
## missing-module
|
|
141
|
+
Status: fail
|
|
142
|
+
Confidence: high
|
|
143
|
+
A module import failed to resolve during HMR.
|
|
144
|
+
Suggestion: Verify the import path, file extension, alias configuration, and whether the module exists on disk.
|
|
145
|
+
```
|
|
146
|
+
|
|
44
147
|
## Core Capabilities
|
|
45
148
|
|
|
46
149
|
- Framework detection: Vue/React/Svelte best-effort detection and version hinting
|
|
@@ -51,9 +154,55 @@ vite-browser close
|
|
|
51
154
|
- runtime status summary
|
|
52
155
|
- HMR summary/timeline/clear
|
|
53
156
|
- module-graph snapshot/diff/clear
|
|
157
|
+
- error/HMR correlation over recent event windows
|
|
158
|
+
- rule-based HMR diagnosis with confidence levels
|
|
54
159
|
- source-mapped errors with optional inline source snippet
|
|
55
160
|
- Debug utilities: console logs, network tracing, screenshot, page `eval`
|
|
56
161
|
|
|
162
|
+
## Recommended Workflows
|
|
163
|
+
|
|
164
|
+
### Runtime/HMR triage
|
|
165
|
+
|
|
166
|
+
```bash
|
|
167
|
+
vite-browser vite runtime
|
|
168
|
+
vite-browser errors --mapped --inline-source
|
|
169
|
+
vite-browser correlate errors --mapped --window 5000
|
|
170
|
+
vite-browser diagnose hmr --limit 50
|
|
171
|
+
vite-browser vite hmr trace --limit 50
|
|
172
|
+
vite-browser vite module-graph trace --limit 200
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
### Data/API triage
|
|
176
|
+
|
|
177
|
+
```bash
|
|
178
|
+
vite-browser errors --mapped
|
|
179
|
+
vite-browser logs
|
|
180
|
+
vite-browser network
|
|
181
|
+
vite-browser network <idx>
|
|
182
|
+
vite-browser eval '<state probe>'
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
### Component/state triage
|
|
186
|
+
|
|
187
|
+
```bash
|
|
188
|
+
vite-browser detect
|
|
189
|
+
vite-browser vue tree
|
|
190
|
+
vite-browser vue pinia
|
|
191
|
+
vite-browser vue router
|
|
192
|
+
vite-browser react tree
|
|
193
|
+
vite-browser svelte tree
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
## Current Boundaries
|
|
197
|
+
|
|
198
|
+
`vite-browser` v0.2 is strong at:
|
|
199
|
+
|
|
200
|
+
- surfacing runtime state as structured shell output
|
|
201
|
+
- linking current errors to recent HMR/module activity
|
|
202
|
+
- detecting several common HMR failure patterns quickly
|
|
203
|
+
|
|
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.
|
|
205
|
+
|
|
57
206
|
## Command Reference
|
|
58
207
|
|
|
59
208
|
### Browser
|
|
@@ -91,6 +240,9 @@ vite-browser vite module-graph clear
|
|
|
91
240
|
vite-browser errors
|
|
92
241
|
vite-browser errors --mapped
|
|
93
242
|
vite-browser errors --mapped --inline-source
|
|
243
|
+
vite-browser correlate errors [--window <ms>]
|
|
244
|
+
vite-browser correlate errors --mapped --inline-source
|
|
245
|
+
vite-browser diagnose hmr [--window <ms>] [--limit <n>]
|
|
94
246
|
```
|
|
95
247
|
|
|
96
248
|
### Utilities
|
|
@@ -124,6 +276,10 @@ pnpm test:evals
|
|
|
124
276
|
pnpm test:evals:e2e
|
|
125
277
|
```
|
|
126
278
|
|
|
279
|
+
## Discovery
|
|
280
|
+
|
|
281
|
+
If you want to introduce the project to new users, start with the launch kit in [docs/launch-kit.md](./docs/launch-kit.md).
|
|
282
|
+
|
|
127
283
|
## Requirements
|
|
128
284
|
|
|
129
285
|
- 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
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;
|
package/dist/diagnose.js
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
export function diagnoseHMR(input) {
|
|
2
|
+
const results = [
|
|
3
|
+
detectCircularDependency(input),
|
|
4
|
+
detectMissingModule(input),
|
|
5
|
+
detectClosedWebsocket(input),
|
|
6
|
+
detectRepeatedFullReload(input),
|
|
7
|
+
].filter((result) => result !== null);
|
|
8
|
+
if (results.length > 0)
|
|
9
|
+
return results;
|
|
10
|
+
return [
|
|
11
|
+
{
|
|
12
|
+
code: "hmr-websocket-closed",
|
|
13
|
+
status: "pass",
|
|
14
|
+
confidence: "low",
|
|
15
|
+
summary: "No obvious HMR failure pattern detected",
|
|
16
|
+
detail: "Runtime, current error text, and HMR trace did not match any built-in failure rules.",
|
|
17
|
+
suggestion: "If symptoms persist, inspect `vite hmr trace`, `network`, and `correlate errors` output together.",
|
|
18
|
+
},
|
|
19
|
+
];
|
|
20
|
+
}
|
|
21
|
+
export function formatDiagnosisReport(results) {
|
|
22
|
+
const lines = ["# HMR Diagnosis", ""];
|
|
23
|
+
for (const result of results) {
|
|
24
|
+
lines.push(`## ${result.code}`, `Status: ${result.status}`, `Confidence: ${result.confidence}`, result.summary, result.detail, `Suggestion: ${result.suggestion}`, "");
|
|
25
|
+
}
|
|
26
|
+
return lines.join("\n").trimEnd();
|
|
27
|
+
}
|
|
28
|
+
function detectCircularDependency(input) {
|
|
29
|
+
const text = `${input.errorText}\n${input.hmrTraceText}`;
|
|
30
|
+
if (!/circular (dependency|import)|import cycle/i.test(text))
|
|
31
|
+
return null;
|
|
32
|
+
const moduleText = input.correlation?.matchingModules.length
|
|
33
|
+
? `Likely modules: ${input.correlation.matchingModules.join(", ")}.`
|
|
34
|
+
: "The error text points to a circular import/dependency chain.";
|
|
35
|
+
return {
|
|
36
|
+
code: "circular-dependency",
|
|
37
|
+
status: "fail",
|
|
38
|
+
confidence: input.correlation?.matchingModules.length ? "high" : "medium",
|
|
39
|
+
summary: "HMR is likely breaking because of a circular dependency.",
|
|
40
|
+
detail: `${moduleText} Circular imports often prevent safe hot replacement and force stale state or reload loops.`,
|
|
41
|
+
suggestion: "Break the import cycle by extracting shared code into a leaf module or switching one edge to a lazy import.",
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
function detectMissingModule(input) {
|
|
45
|
+
const text = input.errorText;
|
|
46
|
+
if (!/failed to resolve import|cannot find module|could not resolve|does the file exist/i.test(text)) {
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
return {
|
|
50
|
+
code: "missing-module",
|
|
51
|
+
status: "fail",
|
|
52
|
+
confidence: "high",
|
|
53
|
+
summary: "A module import failed to resolve during HMR.",
|
|
54
|
+
detail: input.correlation?.matchingModules.length
|
|
55
|
+
? `The current error overlaps with recent updates to ${input.correlation.matchingModules.join(", ")}.`
|
|
56
|
+
: "The current error text matches a missing or unresolved import pattern.",
|
|
57
|
+
suggestion: "Verify the import path, file extension, alias configuration, and whether the module exists on disk.",
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
function detectClosedWebsocket(input) {
|
|
61
|
+
const runtimeClosed = /HMR Socket:\s*(closed|closing|unknown)/i.test(input.runtimeText);
|
|
62
|
+
const traceClosed = /disconnected|failed to connect|connection lost|ws closed/i.test(input.hmrTraceText);
|
|
63
|
+
if (!runtimeClosed && !traceClosed)
|
|
64
|
+
return null;
|
|
65
|
+
return {
|
|
66
|
+
code: "hmr-websocket-closed",
|
|
67
|
+
status: "fail",
|
|
68
|
+
confidence: runtimeClosed && traceClosed ? "high" : "medium",
|
|
69
|
+
summary: "The HMR websocket is not healthy.",
|
|
70
|
+
detail: runtimeClosed
|
|
71
|
+
? "Runtime status reports the HMR socket as closed, closing, or unknown."
|
|
72
|
+
: "HMR trace contains disconnect or websocket failure messages.",
|
|
73
|
+
suggestion: "Check the dev server is running, the page is connected to the correct origin, and no proxy/firewall is blocking the websocket.",
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
function detectRepeatedFullReload(input) {
|
|
77
|
+
const matches = input.hmrTraceText.match(/full-reload|page reload/gi) ?? [];
|
|
78
|
+
if (matches.length < 2)
|
|
79
|
+
return null;
|
|
80
|
+
return {
|
|
81
|
+
code: "repeated-full-reload",
|
|
82
|
+
status: "warn",
|
|
83
|
+
confidence: matches.length >= 3 ? "high" : "medium",
|
|
84
|
+
summary: "Vite is repeatedly falling back to full page reloads.",
|
|
85
|
+
detail: `Observed ${matches.length} full-reload events in the recent HMR trace.`,
|
|
86
|
+
suggestion: "Check whether the changed module is outside HMR boundaries, introduces side effects, or triggers a circular dependency.",
|
|
87
|
+
};
|
|
88
|
+
}
|
|
@@ -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
|
-
|
|
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.
|
|
4
|
-
"description": "CLI for
|
|
5
|
-
"license": "MIT",
|
|
3
|
+
"version": "0.2.0",
|
|
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"
|