@presto1314w/vite-devtools-browser 0.1.3 → 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/dist/cli.js CHANGED
@@ -1,165 +1,191 @@
1
1
  #!/usr/bin/env node
2
2
  import { readFileSync } from "node:fs";
3
3
  import { send } from "./client.js";
4
- const args = process.argv.slice(2);
5
- const cmd = args[0];
6
- const arg = args[1];
7
- if (cmd === "--help" || cmd === "-h" || !cmd) {
8
- printUsage();
9
- process.exit(0);
10
- }
11
- if (cmd === "open") {
12
- if (!arg) {
13
- console.error("usage: vite-browser open <url> [--cookies-json <file>]");
14
- process.exit(1);
15
- }
16
- const url = normalizeUrl(arg);
17
- const cookieIdx = args.indexOf("--cookies-json");
18
- const cookieFile = cookieIdx >= 0 ? args[cookieIdx + 1] : undefined;
19
- if (cookieFile) {
20
- const res = await send("open");
21
- if (!res.ok)
22
- exit(res, "");
23
- const raw = readFileSync(cookieFile, "utf-8");
24
- const cookies = JSON.parse(raw);
25
- const domain = new URL(url).hostname;
26
- const cRes = await send("cookies", { cookies, domain });
27
- if (!cRes.ok)
28
- exit(cRes, "");
29
- await send("goto", { url });
30
- exit(res, `opened -> ${url} (${cookies.length} cookies for ${domain})`);
31
- }
32
- const res = await send("open", { url });
33
- exit(res, `opened -> ${url}`);
34
- }
35
- if (cmd === "close") {
36
- const res = await send("close");
37
- exit(res, "closed");
4
+ export function normalizeUrl(value) {
5
+ if (value.includes("://"))
6
+ return value;
7
+ return `http://${value}`;
38
8
  }
39
- if (cmd === "goto") {
40
- if (!arg) {
41
- console.error("usage: vite-browser goto <url>");
42
- process.exit(1);
9
+ export function parseNumberFlag(args, name, fallback) {
10
+ const idx = args.indexOf(name);
11
+ if (idx < 0)
12
+ return fallback;
13
+ return Number.parseInt(args[idx + 1] ?? String(fallback), 10);
14
+ }
15
+ export async function runCli(argv, io) {
16
+ const args = argv.slice(2);
17
+ const cmd = args[0];
18
+ const arg = args[1];
19
+ if (cmd === "--help" || cmd === "-h" || !cmd) {
20
+ io.stdout(printUsage());
21
+ io.exit(0);
43
22
  }
44
- const res = await send("goto", { url: normalizeUrl(arg) });
45
- exit(res, res.ok ? `-> ${res.data}` : "");
46
- }
47
- if (cmd === "back") {
48
- const res = await send("back");
49
- exit(res, "back");
50
- }
51
- if (cmd === "reload") {
52
- const res = await send("reload");
53
- exit(res, res.ok ? `reloaded -> ${res.data}` : "");
54
- }
55
- if (cmd === "detect") {
56
- const res = await send("detect");
57
- exit(res, res.ok && res.data ? String(res.data) : "");
58
- }
59
- if (cmd === "vue" && arg === "tree") {
60
- const id = args[2];
61
- const res = await send("vue-tree", { id });
62
- exit(res, res.ok && res.data ? String(res.data) : "");
63
- }
64
- if (cmd === "vue" && arg === "pinia") {
65
- const store = args[2];
66
- const res = await send("vue-pinia", { store });
67
- exit(res, res.ok && res.data ? String(res.data) : "");
68
- }
69
- if (cmd === "vue" && arg === "router") {
70
- const res = await send("vue-router");
71
- exit(res, res.ok && res.data ? String(res.data) : "");
72
- }
73
- if (cmd === "react" && arg === "tree") {
74
- const id = args[2];
75
- const res = await send("react-tree", { id });
76
- exit(res, res.ok && res.data ? String(res.data) : "");
77
- }
78
- if (cmd === "svelte" && arg === "tree") {
79
- const id = args[2];
80
- const res = await send("svelte-tree", { id });
81
- exit(res, res.ok && res.data ? String(res.data) : "");
82
- }
83
- if (cmd === "vite" && arg === "restart") {
84
- const res = await send("vite-restart");
85
- exit(res, res.ok && res.data ? String(res.data) : "restarted");
86
- }
87
- if (cmd === "vite" && arg === "hmr") {
88
- const sub = args[2];
89
- if (sub === "clear") {
90
- const res = await send("vite-hmr", { mode: "clear" });
91
- exit(res, res.ok && res.data ? String(res.data) : "cleared HMR trace");
92
- }
93
- if (sub === "trace") {
94
- const limitIdx = args.indexOf("--limit");
95
- const limit = limitIdx >= 0 ? Number.parseInt(args[limitIdx + 1] ?? "20", 10) : 20;
96
- const res = await send("vite-hmr", { mode: "trace", limit });
97
- exit(res, res.ok && res.data ? String(res.data) : "");
98
- }
99
- const res = await send("vite-hmr", { mode: "summary", limit: 20 });
100
- exit(res, res.ok && res.data ? String(res.data) : "");
101
- }
102
- if (cmd === "vite" && arg === "runtime") {
103
- const res = await send("vite-runtime");
104
- exit(res, res.ok && res.data ? String(res.data) : "");
105
- }
106
- if (cmd === "vite" && arg === "module-graph") {
107
- const sub = args[2];
108
- const filterIdx = args.indexOf("--filter");
109
- const limitIdx = args.indexOf("--limit");
110
- const filter = filterIdx >= 0 ? args[filterIdx + 1] : undefined;
111
- const limit = limitIdx >= 0 ? Number.parseInt(args[limitIdx + 1] ?? "200", 10) : 200;
112
- if (sub === "clear") {
113
- const res = await send("vite-module-graph", { mode: "clear" });
114
- exit(res, res.ok && res.data ? String(res.data) : "cleared module-graph baseline");
115
- }
116
- if (sub === "trace") {
117
- const res = await send("vite-module-graph", { mode: "trace", filter, limit });
118
- exit(res, res.ok && res.data ? String(res.data) : "");
119
- }
120
- const res = await send("vite-module-graph", { mode: "snapshot", filter, limit });
121
- exit(res, res.ok && res.data ? String(res.data) : "");
122
- }
123
- if (cmd === "errors") {
124
- const mapped = args.includes("--mapped");
125
- const inlineSource = args.includes("--inline-source");
126
- const res = await send("errors", { mapped, inlineSource });
127
- exit(res, res.ok && res.data ? String(res.data) : "no errors");
128
- }
129
- if (cmd === "logs") {
130
- const res = await send("logs");
131
- exit(res, res.ok && res.data ? String(res.data) : "");
132
- }
133
- if (cmd === "screenshot") {
134
- const res = await send("screenshot");
135
- exit(res, res.ok && res.data ? String(res.data) : "");
136
- }
137
- if (cmd === "eval") {
138
- if (!arg) {
139
- console.error("usage: vite-browser eval <script>");
140
- process.exit(1);
23
+ if (cmd === "open") {
24
+ if (!arg) {
25
+ io.stderr("usage: vite-browser open <url> [--cookies-json <file>]");
26
+ io.exit(1);
27
+ }
28
+ const url = normalizeUrl(arg);
29
+ const cookieIdx = args.indexOf("--cookies-json");
30
+ const cookieFile = cookieIdx >= 0 ? args[cookieIdx + 1] : undefined;
31
+ if (cookieFile) {
32
+ const res = await io.send("open");
33
+ if (!res.ok)
34
+ exit(io, res, "");
35
+ const raw = io.readFile(cookieFile, "utf-8");
36
+ const cookies = JSON.parse(raw);
37
+ const domain = new URL(url).hostname;
38
+ const cRes = await io.send("cookies", { cookies, domain });
39
+ if (!cRes.ok)
40
+ exit(io, cRes, "");
41
+ await io.send("goto", { url });
42
+ exit(io, res, `opened -> ${url} (${cookies.length} cookies for ${domain})`);
43
+ }
44
+ const res = await io.send("open", { url });
45
+ exit(io, res, `opened -> ${url}`);
141
46
  }
142
- const res = await send("eval", { script: arg });
143
- exit(res, res.ok && res.data ? String(res.data) : "");
144
- }
145
- if (cmd === "network") {
146
- const idx = arg ? parseInt(arg, 10) : undefined;
147
- const res = await send("network", { idx });
148
- exit(res, res.ok && res.data ? String(res.data) : "");
47
+ if (cmd === "close") {
48
+ const res = await io.send("close");
49
+ exit(io, res, "closed");
50
+ }
51
+ if (cmd === "goto") {
52
+ if (!arg) {
53
+ io.stderr("usage: vite-browser goto <url>");
54
+ io.exit(1);
55
+ }
56
+ const res = await io.send("goto", { url: normalizeUrl(arg) });
57
+ exit(io, res, res.ok ? `-> ${res.data}` : "");
58
+ }
59
+ if (cmd === "back") {
60
+ const res = await io.send("back");
61
+ exit(io, res, "back");
62
+ }
63
+ if (cmd === "reload") {
64
+ const res = await io.send("reload");
65
+ exit(io, res, res.ok ? `reloaded -> ${res.data}` : "");
66
+ }
67
+ if (cmd === "detect") {
68
+ const res = await io.send("detect");
69
+ exit(io, res, res.ok && res.data ? String(res.data) : "");
70
+ }
71
+ if (cmd === "vue" && arg === "tree") {
72
+ const id = args[2];
73
+ const res = await io.send("vue-tree", { id });
74
+ exit(io, res, res.ok && res.data ? String(res.data) : "");
75
+ }
76
+ if (cmd === "vue" && arg === "pinia") {
77
+ const store = args[2];
78
+ const res = await io.send("vue-pinia", { store });
79
+ exit(io, res, res.ok && res.data ? String(res.data) : "");
80
+ }
81
+ if (cmd === "vue" && arg === "router") {
82
+ const res = await io.send("vue-router");
83
+ exit(io, res, res.ok && res.data ? String(res.data) : "");
84
+ }
85
+ if (cmd === "react" && arg === "tree") {
86
+ const id = args[2];
87
+ const res = await io.send("react-tree", { id });
88
+ exit(io, res, res.ok && res.data ? String(res.data) : "");
89
+ }
90
+ if (cmd === "svelte" && arg === "tree") {
91
+ const id = args[2];
92
+ const res = await io.send("svelte-tree", { id });
93
+ exit(io, res, res.ok && res.data ? String(res.data) : "");
94
+ }
95
+ if (cmd === "vite" && arg === "restart") {
96
+ const res = await io.send("vite-restart");
97
+ exit(io, res, res.ok && res.data ? String(res.data) : "restarted");
98
+ }
99
+ if (cmd === "vite" && arg === "hmr") {
100
+ const sub = args[2];
101
+ if (sub === "clear") {
102
+ const res = await io.send("vite-hmr", { mode: "clear" });
103
+ exit(io, res, res.ok && res.data ? String(res.data) : "cleared HMR trace");
104
+ }
105
+ if (sub === "trace") {
106
+ const limit = parseNumberFlag(args, "--limit", 20);
107
+ const res = await io.send("vite-hmr", { mode: "trace", limit });
108
+ exit(io, res, res.ok && res.data ? String(res.data) : "");
109
+ }
110
+ const res = await io.send("vite-hmr", { mode: "summary", limit: 20 });
111
+ exit(io, res, res.ok && res.data ? String(res.data) : "");
112
+ }
113
+ if (cmd === "vite" && arg === "runtime") {
114
+ const res = await io.send("vite-runtime");
115
+ exit(io, res, res.ok && res.data ? String(res.data) : "");
116
+ }
117
+ if (cmd === "vite" && arg === "module-graph") {
118
+ const sub = args[2];
119
+ const filterIdx = args.indexOf("--filter");
120
+ const filter = filterIdx >= 0 ? args[filterIdx + 1] : undefined;
121
+ const limit = parseNumberFlag(args, "--limit", 200);
122
+ if (sub === "clear") {
123
+ const res = await io.send("vite-module-graph", { mode: "clear" });
124
+ exit(io, res, res.ok && res.data ? String(res.data) : "cleared module-graph baseline");
125
+ }
126
+ if (sub === "trace") {
127
+ const res = await io.send("vite-module-graph", { mode: "trace", filter, limit });
128
+ exit(io, res, res.ok && res.data ? String(res.data) : "");
129
+ }
130
+ const res = await io.send("vite-module-graph", { mode: "snapshot", filter, limit });
131
+ exit(io, res, res.ok && res.data ? String(res.data) : "");
132
+ }
133
+ if (cmd === "errors") {
134
+ const mapped = args.includes("--mapped");
135
+ const inlineSource = args.includes("--inline-source");
136
+ const res = await io.send("errors", { mapped, inlineSource });
137
+ exit(io, res, res.ok && res.data ? String(res.data) : "no errors");
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
+ }
154
+ if (cmd === "logs") {
155
+ const res = await io.send("logs");
156
+ exit(io, res, res.ok && res.data ? String(res.data) : "");
157
+ }
158
+ if (cmd === "screenshot") {
159
+ const res = await io.send("screenshot");
160
+ exit(io, res, res.ok && res.data ? String(res.data) : "");
161
+ }
162
+ if (cmd === "eval") {
163
+ if (!arg) {
164
+ io.stderr("usage: vite-browser eval <script>");
165
+ io.exit(1);
166
+ }
167
+ const res = await io.send("eval", { script: arg });
168
+ exit(io, res, res.ok && res.data ? String(res.data) : "");
169
+ }
170
+ if (cmd === "network") {
171
+ const idx = arg ? parseInt(arg, 10) : undefined;
172
+ const res = await io.send("network", { idx });
173
+ exit(io, res, res.ok && res.data ? String(res.data) : "");
174
+ }
175
+ io.stderr(`unknown command: ${cmd}`);
176
+ io.exit(1);
149
177
  }
150
- console.error(`unknown command: ${cmd}`);
151
- process.exit(1);
152
- function exit(res, msg) {
178
+ export function exit(io, res, msg) {
153
179
  if (!res.ok) {
154
- console.error(res.error || "error");
155
- process.exit(1);
180
+ io.stderr(res.error || "error");
181
+ io.exit(1);
156
182
  }
157
183
  if (msg)
158
- console.log(msg);
159
- process.exit(0);
184
+ io.stdout(msg);
185
+ io.exit(0);
160
186
  }
161
- function printUsage() {
162
- console.log(`
187
+ export function printUsage() {
188
+ return `
163
189
  vite-browser - Programmatic access to Vue/React/Svelte DevTools and Vite dev server
164
190
 
165
191
  USAGE
@@ -200,6 +226,10 @@ VITE COMMANDS
200
226
  errors Show build/runtime errors
201
227
  errors --mapped Show errors with source-map mapping
202
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
203
233
  logs Show dev server logs
204
234
 
205
235
  UTILITIES
@@ -209,10 +239,14 @@ UTILITIES
209
239
 
210
240
  OPTIONS
211
241
  -h, --help Show this help message
212
- `);
213
- }
214
- function normalizeUrl(value) {
215
- if (value.includes("://"))
216
- return value;
217
- return `http://${value}`;
242
+ `;
243
+ }
244
+ if (process.argv[1] && import.meta.url.endsWith(process.argv[1].replaceAll("\\", "/"))) {
245
+ await runCli(process.argv, {
246
+ send,
247
+ readFile: readFileSync,
248
+ stdout: (text) => console.log(text),
249
+ stderr: (text) => console.error(text),
250
+ exit: (code) => process.exit(code),
251
+ });
218
252
  }
package/dist/client.d.ts CHANGED
@@ -1,3 +1,7 @@
1
+ import { type Socket } from "node:net";
2
+ import { readFileSync, existsSync, rmSync } from "node:fs";
3
+ import { spawn } from "node:child_process";
4
+ import { setTimeout as sleep } from "node:timers/promises";
1
5
  export type Response = {
2
6
  ok: true;
3
7
  data?: unknown;
@@ -5,4 +9,24 @@ export type Response = {
5
9
  ok: false;
6
10
  error: string;
7
11
  };
12
+ export type ClientDeps = {
13
+ socketPath: string;
14
+ pidFile: string;
15
+ existsSync: typeof existsSync;
16
+ readFileSync: typeof readFileSync;
17
+ rmSync: typeof rmSync;
18
+ processKill: typeof process.kill;
19
+ spawn: typeof spawn;
20
+ connect: () => Promise<Socket>;
21
+ sleep: typeof sleep;
22
+ daemonPath: string;
23
+ };
24
+ export declare function createClientDeps(): ClientDeps;
8
25
  export declare function send(action: string, payload?: Record<string, unknown>): Promise<Response>;
26
+ export declare function ensureDaemon(deps: ClientDeps): Promise<void>;
27
+ export declare function daemonAlive(deps: Pick<ClientDeps, "existsSync" | "pidFile" | "readFileSync" | "processKill" | "rmSync" | "socketPath">): boolean;
28
+ export declare function connect(): Promise<Socket>;
29
+ export declare function readLine(socket: Pick<Socket, "on">): Promise<string>;
30
+ export declare function ok(s: Pick<Socket, "destroy">): boolean;
31
+ export declare function no(): boolean;
32
+ export declare function removeSocketFile(path: string, removeFile?: typeof rmSync): void;
package/dist/client.js CHANGED
@@ -4,54 +4,69 @@ import { spawn } from "node:child_process";
4
4
  import { setTimeout as sleep } from "node:timers/promises";
5
5
  import { fileURLToPath } from "node:url";
6
6
  import { socketPath, pidFile } from "./paths.js";
7
+ export function createClientDeps() {
8
+ const ext = import.meta.url.endsWith(".ts") ? ".ts" : ".js";
9
+ const daemonPath = fileURLToPath(new URL(`./daemon${ext}`, import.meta.url));
10
+ return {
11
+ socketPath,
12
+ pidFile,
13
+ existsSync,
14
+ readFileSync,
15
+ rmSync,
16
+ processKill: process.kill.bind(process),
17
+ spawn,
18
+ connect,
19
+ sleep,
20
+ daemonPath,
21
+ };
22
+ }
7
23
  export async function send(action, payload = {}) {
8
- await ensureDaemon();
9
- const socket = await connect();
24
+ const deps = createClientDeps();
25
+ await ensureDaemon(deps);
26
+ const socket = await deps.connect();
10
27
  const id = String(Date.now());
11
28
  socket.write(JSON.stringify({ id, action, ...payload }) + "\n");
12
29
  const line = await readLine(socket);
13
30
  socket.end();
14
31
  return JSON.parse(line);
15
32
  }
16
- async function ensureDaemon() {
17
- if (daemonAlive() && (await connect().then(ok, no)))
33
+ export async function ensureDaemon(deps) {
34
+ if (daemonAlive(deps) && (await deps.connect().then(ok, no)))
18
35
  return;
19
- const ext = import.meta.url.endsWith(".ts") ? ".ts" : ".js";
20
- const daemon = fileURLToPath(new URL(`./daemon${ext}`, import.meta.url));
21
- const child = spawn(process.execPath, [daemon], {
36
+ const child = deps.spawn(process.execPath, [deps.daemonPath], {
22
37
  detached: true,
23
38
  stdio: "ignore",
24
39
  });
25
40
  child.unref();
26
41
  for (let i = 0; i < 50; i++) {
27
- if (await connect().then(ok, no))
42
+ if (await deps.connect().then(ok, no))
28
43
  return;
29
- await sleep(100);
44
+ await deps.sleep(100);
30
45
  }
31
- throw new Error(`daemon failed to start (${socketPath})`);
46
+ throw new Error(`daemon failed to start (${deps.socketPath})`);
32
47
  }
33
- function daemonAlive() {
34
- if (!existsSync(pidFile))
48
+ export function daemonAlive(deps) {
49
+ if (!deps.existsSync(deps.pidFile))
35
50
  return false;
36
- const pid = Number(readFileSync(pidFile, "utf-8"));
51
+ const pid = Number(deps.readFileSync(deps.pidFile, "utf-8"));
37
52
  try {
38
- process.kill(pid, 0);
53
+ deps.processKill(pid, 0);
39
54
  return true;
40
55
  }
41
56
  catch {
42
- rmSync(pidFile, { force: true });
43
- removeSocketFile();
57
+ deps.rmSync(deps.pidFile, { force: true });
58
+ removeSocketFile(deps.socketPath, deps.rmSync);
44
59
  return false;
45
60
  }
46
61
  }
47
- function connect() {
62
+ export function connect() {
48
63
  return new Promise((resolve, reject) => {
49
64
  const socket = netConnect(socketPath);
50
65
  socket.once("connect", () => resolve(socket));
51
66
  socket.once("error", reject);
52
67
  });
53
68
  }
54
- function readLine(socket) {
69
+ export function readLine(socket) {
55
70
  return new Promise((resolve, reject) => {
56
71
  let buffer = "";
57
72
  socket.on("data", (chunk) => {
@@ -63,15 +78,15 @@ function readLine(socket) {
63
78
  socket.on("error", reject);
64
79
  });
65
80
  }
66
- function ok(s) {
81
+ export function ok(s) {
67
82
  s.destroy();
68
83
  return true;
69
84
  }
70
- function no() {
85
+ export function no() {
71
86
  return false;
72
87
  }
73
- function removeSocketFile() {
88
+ export function removeSocketFile(path, removeFile = rmSync) {
74
89
  if (process.platform === "win32")
75
90
  return;
76
- rmSync(socketPath, { force: true });
91
+ removeFile(path, { force: true });
77
92
  }
@@ -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
+ }