@presto1314w/vite-devtools-browser 0.1.1 → 0.1.3
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/browser.d.ts +4 -1
- package/dist/browser.js +297 -23
- package/dist/cli.js +47 -3
- package/dist/daemon.js +12 -2
- package/dist/sourcemap.d.ts +8 -0
- package/dist/sourcemap.js +71 -0
- package/dist/vue/devtools.js +61 -11
- package/package.json +8 -3
package/dist/browser.d.ts
CHANGED
|
@@ -15,7 +15,10 @@ export declare function reactTree(id?: string): Promise<string>;
|
|
|
15
15
|
export declare function svelteTree(id?: string): Promise<string>;
|
|
16
16
|
export declare function viteRestart(): Promise<string>;
|
|
17
17
|
export declare function viteHMR(): Promise<string>;
|
|
18
|
-
export declare function
|
|
18
|
+
export declare function viteRuntimeStatus(): Promise<string>;
|
|
19
|
+
export declare function viteHMRTrace(mode: "summary" | "trace" | "clear", limit?: number): Promise<string>;
|
|
20
|
+
export declare function viteModuleGraph(filter?: string, limit?: number, mode?: "snapshot" | "trace" | "clear"): Promise<string>;
|
|
21
|
+
export declare function errors(mapped?: boolean, inlineSource?: boolean): Promise<string>;
|
|
19
22
|
export declare function logs(): Promise<string>;
|
|
20
23
|
export declare function screenshot(): Promise<string>;
|
|
21
24
|
export declare function evaluate(script: string): Promise<string>;
|
package/dist/browser.js
CHANGED
|
@@ -6,6 +6,7 @@ import * as vueDevtools from "./vue/devtools.js";
|
|
|
6
6
|
import * as reactDevtools from "./react/devtools.js";
|
|
7
7
|
import * as svelteDevtools from "./svelte/devtools.js";
|
|
8
8
|
import * as networkLog from "./network.js";
|
|
9
|
+
import { resolveViaSourceMap } from "./sourcemap.js";
|
|
9
10
|
const extensionPath = process.env.REACT_DEVTOOLS_EXTENSION ??
|
|
10
11
|
resolve(import.meta.dirname, "../../next-browser/extensions/react-devtools-chrome");
|
|
11
12
|
const hasReactExtension = existsSync(join(extensionPath, "manifest.json"));
|
|
@@ -15,9 +16,13 @@ const installHook = hasReactExtension
|
|
|
15
16
|
let context = null;
|
|
16
17
|
let page = null;
|
|
17
18
|
let framework = "unknown";
|
|
19
|
+
let extensionModeDisabled = false;
|
|
18
20
|
const consoleLogs = [];
|
|
19
21
|
const MAX_LOGS = 200;
|
|
22
|
+
const MAX_HMR_EVENTS = 500;
|
|
20
23
|
let lastReactSnapshot = [];
|
|
24
|
+
const hmrEvents = [];
|
|
25
|
+
let lastModuleGraphUrls = null;
|
|
21
26
|
export async function open(url) {
|
|
22
27
|
const currentPage = await ensurePage();
|
|
23
28
|
if (url) {
|
|
@@ -37,6 +42,8 @@ export async function close() {
|
|
|
37
42
|
page = null;
|
|
38
43
|
framework = "unknown";
|
|
39
44
|
consoleLogs.length = 0;
|
|
45
|
+
hmrEvents.length = 0;
|
|
46
|
+
lastModuleGraphUrls = null;
|
|
40
47
|
networkLog.clear();
|
|
41
48
|
lastReactSnapshot = [];
|
|
42
49
|
}
|
|
@@ -48,7 +55,17 @@ async function ensurePage() {
|
|
|
48
55
|
if (!context)
|
|
49
56
|
throw new Error("browser not open");
|
|
50
57
|
if (!page || page.isClosed()) {
|
|
51
|
-
|
|
58
|
+
try {
|
|
59
|
+
page = context.pages()[0] ?? (await context.newPage());
|
|
60
|
+
}
|
|
61
|
+
catch (error) {
|
|
62
|
+
if (!isClosedTargetError(error))
|
|
63
|
+
throw error;
|
|
64
|
+
await close();
|
|
65
|
+
extensionModeDisabled = true;
|
|
66
|
+
context = await launch();
|
|
67
|
+
page = context.pages()[0] ?? (await context.newPage());
|
|
68
|
+
}
|
|
52
69
|
attachListeners(page);
|
|
53
70
|
networkLog.attach(page);
|
|
54
71
|
}
|
|
@@ -65,20 +82,30 @@ function contextUsable(current) {
|
|
|
65
82
|
return false;
|
|
66
83
|
}
|
|
67
84
|
}
|
|
85
|
+
function isClosedTargetError(error) {
|
|
86
|
+
if (!(error instanceof Error))
|
|
87
|
+
return false;
|
|
88
|
+
return /Target page, context or browser has been closed/i.test(error.message);
|
|
89
|
+
}
|
|
68
90
|
async function launch() {
|
|
69
|
-
if (hasReactExtension && installHook) {
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
91
|
+
if (hasReactExtension && installHook && !extensionModeDisabled) {
|
|
92
|
+
try {
|
|
93
|
+
const ctx = await chromium.launchPersistentContext("", {
|
|
94
|
+
headless: false,
|
|
95
|
+
viewport: { width: 1280, height: 720 },
|
|
96
|
+
args: [
|
|
97
|
+
`--disable-extensions-except=${extensionPath}`,
|
|
98
|
+
`--load-extension=${extensionPath}`,
|
|
99
|
+
"--auto-open-devtools-for-tabs",
|
|
100
|
+
],
|
|
101
|
+
});
|
|
102
|
+
await ctx.waitForEvent("serviceworker").catch(() => { });
|
|
103
|
+
await ctx.addInitScript(installHook);
|
|
104
|
+
return ctx;
|
|
105
|
+
}
|
|
106
|
+
catch {
|
|
107
|
+
extensionModeDisabled = true;
|
|
108
|
+
}
|
|
82
109
|
}
|
|
83
110
|
const browser = await chromium.launch({
|
|
84
111
|
headless: false,
|
|
@@ -92,8 +119,41 @@ function attachListeners(currentPage) {
|
|
|
92
119
|
consoleLogs.push(text);
|
|
93
120
|
if (consoleLogs.length > MAX_LOGS)
|
|
94
121
|
consoleLogs.shift();
|
|
122
|
+
const viteMessage = msg.text();
|
|
123
|
+
if (!viteMessage.includes("[vite]"))
|
|
124
|
+
return;
|
|
125
|
+
const event = parseViteLog(viteMessage);
|
|
126
|
+
hmrEvents.push(event);
|
|
127
|
+
if (hmrEvents.length > MAX_HMR_EVENTS)
|
|
128
|
+
hmrEvents.shift();
|
|
95
129
|
});
|
|
96
130
|
}
|
|
131
|
+
function parseViteLog(message) {
|
|
132
|
+
const lower = message.toLowerCase();
|
|
133
|
+
const event = {
|
|
134
|
+
timestamp: Date.now(),
|
|
135
|
+
type: "log",
|
|
136
|
+
message,
|
|
137
|
+
};
|
|
138
|
+
if (lower.includes("connecting"))
|
|
139
|
+
event.type = "connecting";
|
|
140
|
+
else if (lower.includes("connected"))
|
|
141
|
+
event.type = "connected";
|
|
142
|
+
else if (lower.includes("hot updated"))
|
|
143
|
+
event.type = "update";
|
|
144
|
+
else if (lower.includes("page reload"))
|
|
145
|
+
event.type = "full-reload";
|
|
146
|
+
else if (lower.includes("disconnected") ||
|
|
147
|
+
lower.includes("failed to connect") ||
|
|
148
|
+
lower.includes("connection lost") ||
|
|
149
|
+
lower.includes("error")) {
|
|
150
|
+
event.type = "error";
|
|
151
|
+
}
|
|
152
|
+
const hotUpdateMatch = message.match(/hot updated:\s*(.+)$/i);
|
|
153
|
+
if (hotUpdateMatch?.[1])
|
|
154
|
+
event.path = hotUpdateMatch[1].trim();
|
|
155
|
+
return event;
|
|
156
|
+
}
|
|
97
157
|
export async function goto(url) {
|
|
98
158
|
const currentPage = await ensurePage();
|
|
99
159
|
await currentPage.goto(url, { waitUntil: "domcontentloaded" });
|
|
@@ -202,17 +262,202 @@ export async function viteRestart() {
|
|
|
202
262
|
export async function viteHMR() {
|
|
203
263
|
if (!page)
|
|
204
264
|
throw new Error("browser not open");
|
|
265
|
+
return viteHMRTrace("summary", 20);
|
|
266
|
+
}
|
|
267
|
+
export async function viteRuntimeStatus() {
|
|
268
|
+
if (!page)
|
|
269
|
+
throw new Error("browser not open");
|
|
270
|
+
const runtime = await page.evaluate(() => {
|
|
271
|
+
const findViteClient = () => {
|
|
272
|
+
const scripts = Array.from(document.querySelectorAll("script[src]"));
|
|
273
|
+
return scripts.some((script) => script.getAttribute("src")?.includes("/@vite/client"));
|
|
274
|
+
};
|
|
275
|
+
const wsStateName = (wsState) => {
|
|
276
|
+
if (wsState == null)
|
|
277
|
+
return "unknown";
|
|
278
|
+
if (wsState === 0)
|
|
279
|
+
return "connecting";
|
|
280
|
+
if (wsState === 1)
|
|
281
|
+
return "open";
|
|
282
|
+
if (wsState === 2)
|
|
283
|
+
return "closing";
|
|
284
|
+
if (wsState === 3)
|
|
285
|
+
return "closed";
|
|
286
|
+
return "unknown";
|
|
287
|
+
};
|
|
288
|
+
const hot = window.__vite_hot;
|
|
289
|
+
const ws = hot?.ws || hot?.socket;
|
|
290
|
+
return {
|
|
291
|
+
url: location.href,
|
|
292
|
+
hasViteClient: findViteClient(),
|
|
293
|
+
wsState: wsStateName(ws?.readyState),
|
|
294
|
+
hasErrorOverlay: Boolean(document.querySelector("vite-error-overlay")),
|
|
295
|
+
timestamp: Date.now(),
|
|
296
|
+
};
|
|
297
|
+
});
|
|
298
|
+
const output = [];
|
|
299
|
+
output.push("# Vite Runtime");
|
|
300
|
+
output.push(`URL: ${runtime.url}`);
|
|
301
|
+
output.push(`Framework: ${framework}`);
|
|
302
|
+
output.push(`Vite Client: ${runtime.hasViteClient ? "loaded" : "not detected"}`);
|
|
303
|
+
output.push(`HMR Socket: ${runtime.wsState}`);
|
|
304
|
+
output.push(`Error Overlay: ${runtime.hasErrorOverlay ? "present" : "none"}`);
|
|
305
|
+
output.push(`Tracked HMR Events: ${hmrEvents.length}`);
|
|
306
|
+
const last = hmrEvents[hmrEvents.length - 1];
|
|
307
|
+
if (last) {
|
|
308
|
+
output.push(`Last HMR Event: ${new Date(last.timestamp).toLocaleTimeString()} [${last.type}] ${last.message}`);
|
|
309
|
+
}
|
|
310
|
+
return output.join("\n");
|
|
311
|
+
}
|
|
312
|
+
export async function viteHMRTrace(mode, limit = 20) {
|
|
313
|
+
if (!page)
|
|
314
|
+
throw new Error("browser not open");
|
|
315
|
+
if (mode === "clear") {
|
|
316
|
+
hmrEvents.length = 0;
|
|
317
|
+
return "cleared HMR trace";
|
|
318
|
+
}
|
|
319
|
+
if (hmrEvents.length === 0) {
|
|
320
|
+
const fallback = await page.evaluate(() => {
|
|
321
|
+
const updates = window.__vite_hmr_updates || [];
|
|
322
|
+
return updates.slice(-20).map((u) => ({
|
|
323
|
+
timestamp: u.timestamp ?? Date.now(),
|
|
324
|
+
type: "update",
|
|
325
|
+
message: u.path ? `[vite] hot updated: ${u.path}` : "[vite] hot updated",
|
|
326
|
+
path: u.path,
|
|
327
|
+
}));
|
|
328
|
+
});
|
|
329
|
+
if (fallback.length > 0)
|
|
330
|
+
hmrEvents.push(...fallback);
|
|
331
|
+
}
|
|
332
|
+
if (hmrEvents.length === 0)
|
|
333
|
+
return "No HMR updates";
|
|
334
|
+
const safeLimit = Number.isFinite(limit) && limit > 0 ? Math.min(limit, 200) : 20;
|
|
335
|
+
const recent = hmrEvents.slice(-safeLimit);
|
|
336
|
+
if (mode === "summary") {
|
|
337
|
+
const counts = recent.reduce((acc, event) => {
|
|
338
|
+
acc[event.type] = (acc[event.type] ?? 0) + 1;
|
|
339
|
+
return acc;
|
|
340
|
+
}, {});
|
|
341
|
+
const lines = ["# HMR Summary"];
|
|
342
|
+
lines.push(`Events considered: ${recent.length}`);
|
|
343
|
+
lines.push(`Counts: ${Object.entries(counts)
|
|
344
|
+
.map(([k, v]) => `${k}=${v}`)
|
|
345
|
+
.join(", ")}`);
|
|
346
|
+
const last = recent[recent.length - 1];
|
|
347
|
+
lines.push(`Last: ${new Date(last.timestamp).toLocaleTimeString()} [${last.type}] ${last.path ?? last.message}`);
|
|
348
|
+
lines.push("\nUse `vite-browser vite hmr trace --limit <n>` for timeline details.");
|
|
349
|
+
return lines.join("\n");
|
|
350
|
+
}
|
|
351
|
+
return [
|
|
352
|
+
"# HMR Trace",
|
|
353
|
+
...recent.map((event) => {
|
|
354
|
+
const detail = event.path ? `${event.path}` : event.message;
|
|
355
|
+
return `[${new Date(event.timestamp).toLocaleTimeString()}] ${event.type} ${detail}`;
|
|
356
|
+
}),
|
|
357
|
+
].join("\n");
|
|
358
|
+
}
|
|
359
|
+
export async function viteModuleGraph(filter, limit = 200, mode = "snapshot") {
|
|
360
|
+
if (!page)
|
|
361
|
+
throw new Error("browser not open");
|
|
362
|
+
if (mode === "clear") {
|
|
363
|
+
lastModuleGraphUrls = null;
|
|
364
|
+
return "cleared module-graph baseline";
|
|
365
|
+
}
|
|
366
|
+
const moduleRows = await collectModuleRows(page);
|
|
367
|
+
const currentUrls = moduleRows.map((row) => row.url);
|
|
368
|
+
const previousUrls = lastModuleGraphUrls ? new Set(lastModuleGraphUrls) : null;
|
|
369
|
+
const currentSet = new Set(currentUrls);
|
|
370
|
+
lastModuleGraphUrls = [...currentUrls];
|
|
371
|
+
const normalizedFilter = filter?.trim().toLowerCase();
|
|
372
|
+
const safeLimit = Number.isFinite(limit) && limit > 0 ? Math.min(limit, 500) : 200;
|
|
373
|
+
if (mode === "trace") {
|
|
374
|
+
if (!previousUrls) {
|
|
375
|
+
return "No module-graph baseline. Captured current snapshot; run `vite module-graph trace` again.";
|
|
376
|
+
}
|
|
377
|
+
const added = currentUrls.filter((url) => !previousUrls.has(url));
|
|
378
|
+
const removed = [...previousUrls].filter((url) => !currentSet.has(url));
|
|
379
|
+
const addedFiltered = normalizedFilter
|
|
380
|
+
? added.filter((url) => url.toLowerCase().includes(normalizedFilter))
|
|
381
|
+
: added;
|
|
382
|
+
const removedFiltered = normalizedFilter
|
|
383
|
+
? removed.filter((url) => url.toLowerCase().includes(normalizedFilter))
|
|
384
|
+
: removed;
|
|
385
|
+
const lines = [];
|
|
386
|
+
lines.push("# Vite Module Graph Trace");
|
|
387
|
+
lines.push(`Added: ${addedFiltered.length}, Removed: ${removedFiltered.length}`);
|
|
388
|
+
lines.push("");
|
|
389
|
+
lines.push("## Added");
|
|
390
|
+
if (addedFiltered.length === 0)
|
|
391
|
+
lines.push("(none)");
|
|
392
|
+
else
|
|
393
|
+
addedFiltered.slice(0, safeLimit).forEach((url) => lines.push(`+ ${url}`));
|
|
394
|
+
lines.push("");
|
|
395
|
+
lines.push("## Removed");
|
|
396
|
+
if (removedFiltered.length === 0)
|
|
397
|
+
lines.push("(none)");
|
|
398
|
+
else
|
|
399
|
+
removedFiltered.slice(0, safeLimit).forEach((url) => lines.push(`- ${url}`));
|
|
400
|
+
return lines.join("\n");
|
|
401
|
+
}
|
|
402
|
+
const filtered = moduleRows.filter((row) => normalizedFilter ? row.url.toLowerCase().includes(normalizedFilter) : true);
|
|
403
|
+
const limited = filtered.slice(0, safeLimit);
|
|
404
|
+
if (limited.length === 0)
|
|
405
|
+
return "No module resources found";
|
|
406
|
+
const lines = [];
|
|
407
|
+
lines.push("# Vite Module Graph (loaded resources)");
|
|
408
|
+
lines.push(`Total: ${filtered.length}${filtered.length > limited.length ? ` (showing ${limited.length})` : ""}`);
|
|
409
|
+
lines.push("# idx initiator ms url");
|
|
410
|
+
lines.push("");
|
|
411
|
+
limited.forEach((row, idx) => {
|
|
412
|
+
lines.push(`${idx} ${row.initiator} ${row.durationMs}ms ${row.url}`);
|
|
413
|
+
});
|
|
414
|
+
return lines.join("\n");
|
|
415
|
+
}
|
|
416
|
+
async function collectModuleRows(page) {
|
|
205
417
|
return page.evaluate(() => {
|
|
206
|
-
const
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
418
|
+
const isLikelyModuleUrl = (url) => {
|
|
419
|
+
if (!url)
|
|
420
|
+
return false;
|
|
421
|
+
if (url.includes("/@vite/"))
|
|
422
|
+
return true;
|
|
423
|
+
if (url.includes("/@id/"))
|
|
424
|
+
return true;
|
|
425
|
+
if (url.includes("/src/"))
|
|
426
|
+
return true;
|
|
427
|
+
if (url.includes("/node_modules/"))
|
|
428
|
+
return true;
|
|
429
|
+
return /\.(mjs|cjs|js|jsx|ts|tsx|vue|css)(\?|$)/.test(url);
|
|
430
|
+
};
|
|
431
|
+
const scripts = Array.from(document.querySelectorAll("script[src]")).map((node) => node.src);
|
|
432
|
+
const resources = performance
|
|
433
|
+
.getEntriesByType("resource")
|
|
434
|
+
.map((entry) => {
|
|
435
|
+
const item = entry;
|
|
436
|
+
return {
|
|
437
|
+
url: item.name,
|
|
438
|
+
initiator: item.initiatorType || "unknown",
|
|
439
|
+
durationMs: Number(item.duration.toFixed(1)),
|
|
440
|
+
};
|
|
441
|
+
})
|
|
442
|
+
.filter((entry) => isLikelyModuleUrl(entry.url));
|
|
443
|
+
const all = [
|
|
444
|
+
...scripts
|
|
445
|
+
.filter((url) => isLikelyModuleUrl(url))
|
|
446
|
+
.map((url) => ({ url, initiator: "script-tag", durationMs: 0 })),
|
|
447
|
+
...resources,
|
|
448
|
+
];
|
|
449
|
+
const seen = new Set();
|
|
450
|
+
const unique = [];
|
|
451
|
+
for (const row of all) {
|
|
452
|
+
if (seen.has(row.url))
|
|
453
|
+
continue;
|
|
454
|
+
seen.add(row.url);
|
|
455
|
+
unique.push(row);
|
|
456
|
+
}
|
|
457
|
+
return unique;
|
|
213
458
|
});
|
|
214
459
|
}
|
|
215
|
-
export async function errors() {
|
|
460
|
+
export async function errors(mapped = false, inlineSource = false) {
|
|
216
461
|
if (!page)
|
|
217
462
|
throw new Error("browser not open");
|
|
218
463
|
const errorInfo = await page.evaluate(() => {
|
|
@@ -225,7 +470,12 @@ export async function errors() {
|
|
|
225
470
|
});
|
|
226
471
|
if (!errorInfo)
|
|
227
472
|
return "no errors";
|
|
228
|
-
|
|
473
|
+
const raw = `${errorInfo.message ?? "Vite error"}\n\n${errorInfo.stack ?? ""}`.trim();
|
|
474
|
+
if (!mapped)
|
|
475
|
+
return raw;
|
|
476
|
+
const origin = new URL(page.url()).origin;
|
|
477
|
+
const mappedStack = await mapStackTrace(raw, origin, inlineSource);
|
|
478
|
+
return mappedStack;
|
|
229
479
|
}
|
|
230
480
|
export async function logs() {
|
|
231
481
|
if (consoleLogs.length === 0)
|
|
@@ -250,3 +500,27 @@ export async function network(idx) {
|
|
|
250
500
|
return networkLog.format();
|
|
251
501
|
return networkLog.detail(idx);
|
|
252
502
|
}
|
|
503
|
+
async function mapStackTrace(stack, origin, inlineSource = false) {
|
|
504
|
+
const locationRegex = /(https?:\/\/[^\s)]+):(\d+):(\d+)/g;
|
|
505
|
+
const matches = Array.from(stack.matchAll(locationRegex));
|
|
506
|
+
if (matches.length === 0)
|
|
507
|
+
return stack;
|
|
508
|
+
const mappedLines = [];
|
|
509
|
+
for (const match of matches) {
|
|
510
|
+
const fileUrl = match[1];
|
|
511
|
+
const line = Number.parseInt(match[2], 10);
|
|
512
|
+
const column = Number.parseInt(match[3], 10);
|
|
513
|
+
if (!Number.isFinite(line) || !Number.isFinite(column))
|
|
514
|
+
continue;
|
|
515
|
+
const mapped = await resolveViaSourceMap(origin, fileUrl, line, column, inlineSource);
|
|
516
|
+
if (!mapped)
|
|
517
|
+
continue;
|
|
518
|
+
mappedLines.push(`- ${fileUrl}:${line}:${column} -> ${mapped.file}:${mapped.line}:${mapped.column}`);
|
|
519
|
+
if (inlineSource && mapped.snippet) {
|
|
520
|
+
mappedLines.push(` ${mapped.snippet}`);
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
if (mappedLines.length === 0)
|
|
524
|
+
return stack;
|
|
525
|
+
return `${stack}\n\n# Mapped Stack\n${mappedLines.join("\n")}`;
|
|
526
|
+
}
|
package/dist/cli.js
CHANGED
|
@@ -85,11 +85,45 @@ if (cmd === "vite" && arg === "restart") {
|
|
|
85
85
|
exit(res, res.ok && res.data ? String(res.data) : "restarted");
|
|
86
86
|
}
|
|
87
87
|
if (cmd === "vite" && arg === "hmr") {
|
|
88
|
-
const
|
|
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 });
|
|
89
121
|
exit(res, res.ok && res.data ? String(res.data) : "");
|
|
90
122
|
}
|
|
91
123
|
if (cmd === "errors") {
|
|
92
|
-
const
|
|
124
|
+
const mapped = args.includes("--mapped");
|
|
125
|
+
const inlineSource = args.includes("--inline-source");
|
|
126
|
+
const res = await send("errors", { mapped, inlineSource });
|
|
93
127
|
exit(res, res.ok && res.data ? String(res.data) : "no errors");
|
|
94
128
|
}
|
|
95
129
|
if (cmd === "logs") {
|
|
@@ -154,8 +188,18 @@ SVELTE COMMANDS
|
|
|
154
188
|
|
|
155
189
|
VITE COMMANDS
|
|
156
190
|
vite restart Restart Vite dev server
|
|
157
|
-
vite hmr Show HMR
|
|
191
|
+
vite hmr Show HMR summary
|
|
192
|
+
vite hmr trace [--limit <n>] Show HMR timeline
|
|
193
|
+
vite hmr clear Clear tracked HMR timeline
|
|
194
|
+
vite runtime Show Vite runtime status
|
|
195
|
+
vite module-graph [--filter <txt>] [--limit <n>]
|
|
196
|
+
Show loaded Vite module resources
|
|
197
|
+
vite module-graph trace [--filter <txt>] [--limit <n>]
|
|
198
|
+
Show module additions/removals since baseline
|
|
199
|
+
vite module-graph clear Clear module-graph baseline
|
|
158
200
|
errors Show build/runtime errors
|
|
201
|
+
errors --mapped Show errors with source-map mapping
|
|
202
|
+
errors --mapped --inline-source Include mapped source snippets
|
|
159
203
|
logs Show dev server logs
|
|
160
204
|
|
|
161
205
|
UTILITIES
|
package/dist/daemon.js
CHANGED
|
@@ -96,11 +96,21 @@ async function run(cmd) {
|
|
|
96
96
|
return { ok: true, data };
|
|
97
97
|
}
|
|
98
98
|
if (cmd.action === "vite-hmr") {
|
|
99
|
-
const
|
|
99
|
+
const hmrMode = cmd.mode === "trace" || cmd.mode === "clear" ? cmd.mode : "summary";
|
|
100
|
+
const data = await browser.viteHMRTrace(hmrMode, cmd.limit ?? 20);
|
|
101
|
+
return { ok: true, data };
|
|
102
|
+
}
|
|
103
|
+
if (cmd.action === "vite-runtime") {
|
|
104
|
+
const data = await browser.viteRuntimeStatus();
|
|
105
|
+
return { ok: true, data };
|
|
106
|
+
}
|
|
107
|
+
if (cmd.action === "vite-module-graph") {
|
|
108
|
+
const graphMode = cmd.mode === "trace" || cmd.mode === "clear" ? cmd.mode : "snapshot";
|
|
109
|
+
const data = await browser.viteModuleGraph(cmd.filter, cmd.limit ?? 200, graphMode);
|
|
100
110
|
return { ok: true, data };
|
|
101
111
|
}
|
|
102
112
|
if (cmd.action === "errors") {
|
|
103
|
-
const data = await browser.errors();
|
|
113
|
+
const data = await browser.errors(Boolean(cmd.mapped), Boolean(cmd.inlineSource));
|
|
104
114
|
return { ok: true, data };
|
|
105
115
|
}
|
|
106
116
|
if (cmd.action === "logs") {
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
type MappedLocation = {
|
|
2
|
+
file: string;
|
|
3
|
+
line: number;
|
|
4
|
+
column: number;
|
|
5
|
+
snippet?: string;
|
|
6
|
+
};
|
|
7
|
+
export declare function resolveViaSourceMap(origin: string, fileUrl: string, line: number, column: number, includeSnippet?: boolean): Promise<MappedLocation | null>;
|
|
8
|
+
export {};
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { SourceMapConsumer } from "source-map-js";
|
|
2
|
+
const consumers = new Map();
|
|
3
|
+
export async function resolveViaSourceMap(origin, fileUrl, line, column, includeSnippet = false) {
|
|
4
|
+
const consumer = await loadConsumer(origin, fileUrl);
|
|
5
|
+
if (!consumer)
|
|
6
|
+
return null;
|
|
7
|
+
const position = consumer.originalPositionFor({
|
|
8
|
+
line,
|
|
9
|
+
column: Math.max(0, column - 1),
|
|
10
|
+
});
|
|
11
|
+
if (!position.source || position.line == null || position.column == null)
|
|
12
|
+
return null;
|
|
13
|
+
const mapped = {
|
|
14
|
+
file: cleanSource(position.source),
|
|
15
|
+
line: position.line,
|
|
16
|
+
column: position.column + 1,
|
|
17
|
+
};
|
|
18
|
+
if (includeSnippet) {
|
|
19
|
+
mapped.snippet = snippetFor(consumer, position.source, position.line);
|
|
20
|
+
}
|
|
21
|
+
return mapped;
|
|
22
|
+
}
|
|
23
|
+
async function loadConsumer(origin, fileUrl) {
|
|
24
|
+
const candidates = buildMapCandidates(origin, fileUrl);
|
|
25
|
+
for (const url of candidates) {
|
|
26
|
+
if (consumers.has(url)) {
|
|
27
|
+
const cached = consumers.get(url);
|
|
28
|
+
if (cached)
|
|
29
|
+
return cached;
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
32
|
+
const response = await fetch(url, { signal: AbortSignal.timeout(5000) }).catch(() => null);
|
|
33
|
+
if (!response?.ok) {
|
|
34
|
+
consumers.set(url, null);
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
const map = await response.json().catch(() => null);
|
|
38
|
+
if (!map) {
|
|
39
|
+
consumers.set(url, null);
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
const consumer = new SourceMapConsumer(map);
|
|
43
|
+
consumers.set(url, consumer);
|
|
44
|
+
return consumer;
|
|
45
|
+
}
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
function buildMapCandidates(origin, fileUrl) {
|
|
49
|
+
const url = new URL(fileUrl, origin);
|
|
50
|
+
const basePath = `${url.pathname}.map`;
|
|
51
|
+
const withSearch = url.search ? `${basePath}${url.search}` : basePath;
|
|
52
|
+
// Try preserving query first (Vite often keeps cache-busting query params).
|
|
53
|
+
return [`${origin}${withSearch}`, `${origin}${basePath}`];
|
|
54
|
+
}
|
|
55
|
+
function cleanSource(source) {
|
|
56
|
+
const decoded = decodeURIComponent(source.replace(/^file:\/\//, ""));
|
|
57
|
+
const nodeModulesIndex = decoded.lastIndexOf("/node_modules/");
|
|
58
|
+
if (nodeModulesIndex >= 0)
|
|
59
|
+
return decoded.slice(nodeModulesIndex + 1);
|
|
60
|
+
return decoded;
|
|
61
|
+
}
|
|
62
|
+
function snippetFor(consumer, source, line) {
|
|
63
|
+
const content = consumer.sourceContentFor(source, true);
|
|
64
|
+
if (!content)
|
|
65
|
+
return undefined;
|
|
66
|
+
const lines = content.split(/\r?\n/);
|
|
67
|
+
const sourceLine = lines[line - 1];
|
|
68
|
+
if (!sourceLine)
|
|
69
|
+
return undefined;
|
|
70
|
+
return `${line} | ${sourceLine.trim()}`;
|
|
71
|
+
}
|
package/dist/vue/devtools.js
CHANGED
|
@@ -176,12 +176,51 @@ export async function getComponentDetails(page, id) {
|
|
|
176
176
|
*/
|
|
177
177
|
export async function getPiniaStores(page, storeName) {
|
|
178
178
|
const result = await page.evaluate((name) => {
|
|
179
|
+
const safeJson = (value) => {
|
|
180
|
+
if (typeof value === "function")
|
|
181
|
+
return "[Function]";
|
|
182
|
+
if (typeof value === "bigint")
|
|
183
|
+
return value.toString();
|
|
184
|
+
const seen = new WeakSet();
|
|
185
|
+
try {
|
|
186
|
+
return JSON.stringify(value, (_, v) => {
|
|
187
|
+
if (typeof v === "function")
|
|
188
|
+
return "[Function]";
|
|
189
|
+
if (typeof v === "bigint")
|
|
190
|
+
return v.toString();
|
|
191
|
+
if (v && typeof v === "object") {
|
|
192
|
+
if (seen.has(v))
|
|
193
|
+
return "[Circular]";
|
|
194
|
+
seen.add(v);
|
|
195
|
+
}
|
|
196
|
+
return v;
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
catch {
|
|
200
|
+
return String(value);
|
|
201
|
+
}
|
|
202
|
+
};
|
|
203
|
+
const hook = window.__VUE_DEVTOOLS_GLOBAL_HOOK__;
|
|
204
|
+
const piniaFromApp = hook?.apps?.[0]?.config?.globalProperties?.$pinia;
|
|
179
205
|
// Try to find Pinia instance
|
|
180
|
-
const pinia = window.__PINIA__ || window.pinia;
|
|
206
|
+
const pinia = window.__PINIA__ || window.pinia || piniaFromApp;
|
|
181
207
|
if (!pinia)
|
|
182
208
|
return "Pinia not found";
|
|
183
|
-
|
|
184
|
-
const
|
|
209
|
+
// Pinia v3 uses Map for _s, older integrations can expose plain objects.
|
|
210
|
+
const storesById = {};
|
|
211
|
+
const registry = pinia._s;
|
|
212
|
+
if (registry instanceof Map) {
|
|
213
|
+
registry.forEach((store, id) => {
|
|
214
|
+
storesById[String(id)] = store;
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
else if (registry && typeof registry === "object") {
|
|
218
|
+
for (const [id, store] of Object.entries(registry)) {
|
|
219
|
+
storesById[id] = store;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
const stateById = pinia.state?.value && typeof pinia.state.value === "object" ? pinia.state.value : {};
|
|
223
|
+
const storeKeys = Array.from(new Set([...Object.keys(storesById), ...Object.keys(stateById)]));
|
|
185
224
|
if (storeKeys.length === 0)
|
|
186
225
|
return "No Pinia stores found";
|
|
187
226
|
const output = [];
|
|
@@ -195,29 +234,40 @@ export async function getPiniaStores(page, storeName) {
|
|
|
195
234
|
return output.join("\n");
|
|
196
235
|
}
|
|
197
236
|
// Get specific store
|
|
198
|
-
const store =
|
|
199
|
-
|
|
237
|
+
const store = storesById[name] ?? null;
|
|
238
|
+
const stateOnly = stateById[name];
|
|
239
|
+
if (!store && !stateOnly)
|
|
200
240
|
return `Store '${name}' not found`;
|
|
201
241
|
output.push(`# Pinia Store: ${name}\n`);
|
|
202
242
|
// State
|
|
203
|
-
const state = store
|
|
243
|
+
const state = store?.$state || store?.state || stateOnly || store;
|
|
204
244
|
if (state && typeof state === 'object') {
|
|
205
245
|
output.push("## State");
|
|
206
246
|
for (const [key, value] of Object.entries(state)) {
|
|
207
247
|
if (key.startsWith('$'))
|
|
208
248
|
continue; // Skip Pinia internals
|
|
209
|
-
output.push(` ${key}: ${
|
|
249
|
+
output.push(` ${key}: ${safeJson(value)}`);
|
|
210
250
|
}
|
|
211
251
|
output.push("");
|
|
212
252
|
}
|
|
213
253
|
// Getters
|
|
214
|
-
const
|
|
215
|
-
|
|
254
|
+
const getterNames = [];
|
|
255
|
+
const rawGetters = store?._getters;
|
|
256
|
+
if (Array.isArray(rawGetters)) {
|
|
257
|
+
getterNames.push(...rawGetters.map((g) => String(g)));
|
|
258
|
+
}
|
|
259
|
+
else if (rawGetters instanceof Set) {
|
|
260
|
+
getterNames.push(...Array.from(rawGetters).map((g) => String(g)));
|
|
261
|
+
}
|
|
262
|
+
else if (rawGetters && typeof rawGetters === "object") {
|
|
263
|
+
getterNames.push(...Object.keys(rawGetters));
|
|
264
|
+
}
|
|
265
|
+
if (getterNames.length > 0) {
|
|
216
266
|
output.push("## Getters");
|
|
217
|
-
for (const key of
|
|
267
|
+
for (const key of getterNames) {
|
|
218
268
|
try {
|
|
219
269
|
const value = store[key];
|
|
220
|
-
output.push(` ${key}: ${
|
|
270
|
+
output.push(` ${key}: ${safeJson(value)}`);
|
|
221
271
|
}
|
|
222
272
|
catch {
|
|
223
273
|
output.push(` ${key}: [Error]`);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@presto1314w/vite-devtools-browser",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"description": "CLI for programmatic access to Vue/React DevTools in Vite applications",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
"dist"
|
|
20
20
|
],
|
|
21
21
|
"bin": {
|
|
22
|
-
"vite-browser": "
|
|
22
|
+
"vite-browser": "dist/cli.js"
|
|
23
23
|
},
|
|
24
24
|
"scripts": {
|
|
25
25
|
"start": "node --import tsx src/cli.ts",
|
|
@@ -28,7 +28,11 @@
|
|
|
28
28
|
"build": "tsc -p tsconfig.build.json",
|
|
29
29
|
"prepack": "pnpm build",
|
|
30
30
|
"test": "vitest run",
|
|
31
|
-
"test:watch": "vitest"
|
|
31
|
+
"test:watch": "vitest",
|
|
32
|
+
"test:coverage": "vitest run --coverage",
|
|
33
|
+
"test:evals": "vitest run --dir test/evals",
|
|
34
|
+
"test:evals:ci": "vitest run --dir test/evals --coverage --reporter=default",
|
|
35
|
+
"test:evals:e2e": "pnpm build && vitest run --config vitest.e2e.config.ts"
|
|
32
36
|
},
|
|
33
37
|
"dependencies": {
|
|
34
38
|
"playwright": "^1.50.0",
|
|
@@ -36,6 +40,7 @@
|
|
|
36
40
|
},
|
|
37
41
|
"devDependencies": {
|
|
38
42
|
"@types/node": "^22.0.0",
|
|
43
|
+
"@vitest/coverage-v8": "^4.0.18",
|
|
39
44
|
"@vue/devtools-kit": "^7.3.2",
|
|
40
45
|
"@vue/devtools-api": "^7.3.2",
|
|
41
46
|
"tsx": "^4.20.6",
|