@presto1314w/vite-devtools-browser 0.1.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/LICENSE +21 -0
- package/README.md +112 -0
- package/dist/browser.d.ts +22 -0
- package/dist/browser.js +238 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +168 -0
- package/dist/client.d.ts +8 -0
- package/dist/client.js +72 -0
- package/dist/daemon.d.ts +1 -0
- package/dist/daemon.js +132 -0
- package/dist/network.d.ts +5 -0
- package/dist/network.js +97 -0
- package/dist/paths.d.ts +3 -0
- package/dist/paths.js +9 -0
- package/dist/react/devtools.d.ts +16 -0
- package/dist/react/devtools.js +218 -0
- package/dist/svelte/devtools.d.ts +3 -0
- package/dist/svelte/devtools.js +177 -0
- package/dist/vue/devtools.d.ts +29 -0
- package/dist/vue/devtools.js +277 -0
- package/package.json +38 -0
package/dist/daemon.js
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { createServer } from "node:net";
|
|
2
|
+
import { mkdirSync, writeFileSync, rmSync } from "node:fs";
|
|
3
|
+
import * as browser from "./browser.js";
|
|
4
|
+
import { socketDir, socketPath, pidFile } from "./paths.js";
|
|
5
|
+
mkdirSync(socketDir, { recursive: true, mode: 0o700 });
|
|
6
|
+
rmSync(socketPath, { force: true });
|
|
7
|
+
rmSync(pidFile, { force: true });
|
|
8
|
+
writeFileSync(pidFile, String(process.pid));
|
|
9
|
+
const server = createServer((socket) => {
|
|
10
|
+
let buffer = "";
|
|
11
|
+
socket.on("data", (chunk) => {
|
|
12
|
+
buffer += chunk;
|
|
13
|
+
let newline;
|
|
14
|
+
while ((newline = buffer.indexOf("\n")) >= 0) {
|
|
15
|
+
const line = buffer.slice(0, newline);
|
|
16
|
+
buffer = buffer.slice(newline + 1);
|
|
17
|
+
if (line)
|
|
18
|
+
dispatch(line, socket);
|
|
19
|
+
}
|
|
20
|
+
});
|
|
21
|
+
socket.on("error", () => { });
|
|
22
|
+
});
|
|
23
|
+
server.listen(socketPath);
|
|
24
|
+
process.on("SIGINT", shutdown);
|
|
25
|
+
process.on("SIGTERM", shutdown);
|
|
26
|
+
process.on("exit", cleanup);
|
|
27
|
+
async function dispatch(line, socket) {
|
|
28
|
+
const cmd = JSON.parse(line);
|
|
29
|
+
const result = await run(cmd).catch((err) => ({ ok: false, error: cleanError(err) }));
|
|
30
|
+
socket.write(JSON.stringify({ id: cmd.id, ...result }) + "\n");
|
|
31
|
+
if (cmd.action === "close")
|
|
32
|
+
setImmediate(shutdown);
|
|
33
|
+
}
|
|
34
|
+
function cleanError(err) {
|
|
35
|
+
const msg = err.message;
|
|
36
|
+
const m = msg.match(/^page\.\w+: (?:Error: )?(.+?)(?:\n|$)/);
|
|
37
|
+
return m ? m[1] : msg;
|
|
38
|
+
}
|
|
39
|
+
async function run(cmd) {
|
|
40
|
+
// Browser control
|
|
41
|
+
if (cmd.action === "open") {
|
|
42
|
+
await browser.open(cmd.url);
|
|
43
|
+
return { ok: true };
|
|
44
|
+
}
|
|
45
|
+
if (cmd.action === "cookies") {
|
|
46
|
+
const data = await browser.cookies(cmd.cookies, cmd.domain);
|
|
47
|
+
return { ok: true, data };
|
|
48
|
+
}
|
|
49
|
+
if (cmd.action === "close") {
|
|
50
|
+
await browser.close();
|
|
51
|
+
return { ok: true };
|
|
52
|
+
}
|
|
53
|
+
if (cmd.action === "goto") {
|
|
54
|
+
const data = await browser.goto(cmd.url);
|
|
55
|
+
return { ok: true, data };
|
|
56
|
+
}
|
|
57
|
+
if (cmd.action === "back") {
|
|
58
|
+
await browser.back();
|
|
59
|
+
return { ok: true };
|
|
60
|
+
}
|
|
61
|
+
if (cmd.action === "reload") {
|
|
62
|
+
const data = await browser.reload();
|
|
63
|
+
return { ok: true, data };
|
|
64
|
+
}
|
|
65
|
+
// Framework detection
|
|
66
|
+
if (cmd.action === "detect") {
|
|
67
|
+
const data = await browser.detectFramework();
|
|
68
|
+
return { ok: true, data };
|
|
69
|
+
}
|
|
70
|
+
// Vue commands
|
|
71
|
+
if (cmd.action === "vue-tree") {
|
|
72
|
+
const data = await browser.vueTree(cmd.id);
|
|
73
|
+
return { ok: true, data };
|
|
74
|
+
}
|
|
75
|
+
if (cmd.action === "vue-pinia") {
|
|
76
|
+
const data = await browser.vuePinia(cmd.store);
|
|
77
|
+
return { ok: true, data };
|
|
78
|
+
}
|
|
79
|
+
if (cmd.action === "vue-router") {
|
|
80
|
+
const data = await browser.vueRouter();
|
|
81
|
+
return { ok: true, data };
|
|
82
|
+
}
|
|
83
|
+
// React commands
|
|
84
|
+
if (cmd.action === "react-tree") {
|
|
85
|
+
const data = await browser.reactTree(cmd.id);
|
|
86
|
+
return { ok: true, data };
|
|
87
|
+
}
|
|
88
|
+
// Svelte commands
|
|
89
|
+
if (cmd.action === "svelte-tree") {
|
|
90
|
+
const data = await browser.svelteTree(cmd.id);
|
|
91
|
+
return { ok: true, data };
|
|
92
|
+
}
|
|
93
|
+
// Vite commands
|
|
94
|
+
if (cmd.action === "vite-restart") {
|
|
95
|
+
const data = await browser.viteRestart();
|
|
96
|
+
return { ok: true, data };
|
|
97
|
+
}
|
|
98
|
+
if (cmd.action === "vite-hmr") {
|
|
99
|
+
const data = await browser.viteHMR();
|
|
100
|
+
return { ok: true, data };
|
|
101
|
+
}
|
|
102
|
+
if (cmd.action === "errors") {
|
|
103
|
+
const data = await browser.errors();
|
|
104
|
+
return { ok: true, data };
|
|
105
|
+
}
|
|
106
|
+
if (cmd.action === "logs") {
|
|
107
|
+
const data = await browser.logs();
|
|
108
|
+
return { ok: true, data };
|
|
109
|
+
}
|
|
110
|
+
// Utilities
|
|
111
|
+
if (cmd.action === "screenshot") {
|
|
112
|
+
const data = await browser.screenshot();
|
|
113
|
+
return { ok: true, data };
|
|
114
|
+
}
|
|
115
|
+
if (cmd.action === "eval") {
|
|
116
|
+
const data = await browser.evaluate(cmd.script);
|
|
117
|
+
return { ok: true, data };
|
|
118
|
+
}
|
|
119
|
+
if (cmd.action === "network") {
|
|
120
|
+
const data = await browser.network(cmd.idx);
|
|
121
|
+
return { ok: true, data };
|
|
122
|
+
}
|
|
123
|
+
return { ok: false, error: `unknown action: ${cmd.action}` };
|
|
124
|
+
}
|
|
125
|
+
function shutdown() {
|
|
126
|
+
cleanup();
|
|
127
|
+
process.exit(0);
|
|
128
|
+
}
|
|
129
|
+
function cleanup() {
|
|
130
|
+
rmSync(socketPath, { force: true });
|
|
131
|
+
rmSync(pidFile, { force: true });
|
|
132
|
+
}
|
package/dist/network.js
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { writeFileSync } from "node:fs";
|
|
2
|
+
import { tmpdir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
const BODY_INLINE_LIMIT = 4000;
|
|
5
|
+
let entries = [];
|
|
6
|
+
let startTime = new Map();
|
|
7
|
+
export function attach(page) {
|
|
8
|
+
page.on("request", (req) => {
|
|
9
|
+
if (req.resourceType() === "document" && req.frame() === page.mainFrame()) {
|
|
10
|
+
clear();
|
|
11
|
+
}
|
|
12
|
+
startTime.set(req, Date.now());
|
|
13
|
+
});
|
|
14
|
+
page.on("response", (res) => {
|
|
15
|
+
const req = res.request();
|
|
16
|
+
const t0 = startTime.get(req);
|
|
17
|
+
if (t0 == null)
|
|
18
|
+
return;
|
|
19
|
+
entries.push({ req, res, ms: Date.now() - t0 });
|
|
20
|
+
});
|
|
21
|
+
page.on("requestfailed", (req) => {
|
|
22
|
+
if (!startTime.has(req))
|
|
23
|
+
return;
|
|
24
|
+
entries.push({ req, res: null, ms: null });
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
export function clear() {
|
|
28
|
+
entries = [];
|
|
29
|
+
startTime = new Map();
|
|
30
|
+
}
|
|
31
|
+
export async function detail(idx) {
|
|
32
|
+
const e = entries[idx];
|
|
33
|
+
if (!e)
|
|
34
|
+
throw new Error(`no request at index ${idx}`);
|
|
35
|
+
const { req, res, ms } = e;
|
|
36
|
+
const lines = [
|
|
37
|
+
`${req.method()} ${req.url()}`,
|
|
38
|
+
`type: ${req.resourceType()}${ms != null ? ` ${ms}ms` : ""}`,
|
|
39
|
+
"",
|
|
40
|
+
"request headers:",
|
|
41
|
+
...indent(await req.allHeaders()),
|
|
42
|
+
];
|
|
43
|
+
const postData = req.postData();
|
|
44
|
+
if (postData) {
|
|
45
|
+
lines.push("", "request body:", truncate(postData, 2000));
|
|
46
|
+
}
|
|
47
|
+
if (!res) {
|
|
48
|
+
lines.push("", `FAILED: ${req.failure()?.errorText ?? "unknown"}`);
|
|
49
|
+
return lines.join("\n");
|
|
50
|
+
}
|
|
51
|
+
lines.push("", `response: ${res.status()} ${res.statusText()}`, "response headers:", ...indent(await res.allHeaders()));
|
|
52
|
+
const body = await res.text().catch((err) => `(body unavailable: ${err.message})`);
|
|
53
|
+
lines.push("", "response body:", spillIfLong(body, idx, res));
|
|
54
|
+
return lines.join("\n");
|
|
55
|
+
}
|
|
56
|
+
function indent(headers) {
|
|
57
|
+
return Object.entries(headers).map(([k, v]) => ` ${k}: ${v}`);
|
|
58
|
+
}
|
|
59
|
+
function truncate(s, max) {
|
|
60
|
+
return s.length > max ? s.slice(0, max) + `\n... (${s.length - max} more bytes)` : s;
|
|
61
|
+
}
|
|
62
|
+
function spillIfLong(body, idx, res) {
|
|
63
|
+
if (body.length <= BODY_INLINE_LIMIT)
|
|
64
|
+
return body;
|
|
65
|
+
const ext = extFor(res.headers()["content-type"]);
|
|
66
|
+
const path = join(tmpdir(), `vite-browser-${process.pid}-${idx}${ext}`);
|
|
67
|
+
writeFileSync(path, body);
|
|
68
|
+
return `(${body.length} bytes written to ${path})`;
|
|
69
|
+
}
|
|
70
|
+
function extFor(contentType) {
|
|
71
|
+
if (!contentType)
|
|
72
|
+
return ".txt";
|
|
73
|
+
if (contentType.includes("json"))
|
|
74
|
+
return ".json";
|
|
75
|
+
if (contentType.includes("html"))
|
|
76
|
+
return ".html";
|
|
77
|
+
if (contentType.includes("javascript"))
|
|
78
|
+
return ".js";
|
|
79
|
+
return ".txt";
|
|
80
|
+
}
|
|
81
|
+
export function format() {
|
|
82
|
+
if (entries.length === 0)
|
|
83
|
+
return "(no requests)";
|
|
84
|
+
const lines = [
|
|
85
|
+
"# Network requests since last navigation",
|
|
86
|
+
"# Columns: idx status method type ms url",
|
|
87
|
+
"# Use `network <idx>` for headers and body.",
|
|
88
|
+
"",
|
|
89
|
+
];
|
|
90
|
+
entries.forEach((e, i) => {
|
|
91
|
+
const { req, res, ms } = e;
|
|
92
|
+
const status = res?.status() ?? "FAIL";
|
|
93
|
+
const time = ms != null ? `${ms}ms` : "-";
|
|
94
|
+
lines.push(`${i} ${status} ${req.method()} ${req.resourceType()} ${time} ${req.url()}`);
|
|
95
|
+
});
|
|
96
|
+
return lines.join("\n");
|
|
97
|
+
}
|
package/dist/paths.d.ts
ADDED
package/dist/paths.js
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { homedir } from "node:os";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
const isWindows = process.platform === "win32";
|
|
4
|
+
const session = process.env.VITE_BROWSER_SESSION || "default";
|
|
5
|
+
export const socketDir = join(homedir(), ".vite-browser");
|
|
6
|
+
export const socketPath = isWindows
|
|
7
|
+
? `\\\\.\\pipe\\vite-browser-${session}`
|
|
8
|
+
: join(socketDir, `${session}.sock`);
|
|
9
|
+
export const pidFile = join(socketDir, `${session}.pid`);
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { Page } from "playwright";
|
|
2
|
+
export type ReactNode = {
|
|
3
|
+
id: number;
|
|
4
|
+
type: number;
|
|
5
|
+
name: string | null;
|
|
6
|
+
key: string | null;
|
|
7
|
+
parent: number;
|
|
8
|
+
};
|
|
9
|
+
export type ReactInspection = {
|
|
10
|
+
text: string;
|
|
11
|
+
source: [file: string, line: number, column: number] | null;
|
|
12
|
+
};
|
|
13
|
+
export declare function snapshot(page: Page): Promise<ReactNode[]>;
|
|
14
|
+
export declare function inspect(page: Page, id: number): Promise<ReactInspection>;
|
|
15
|
+
export declare function format(nodes: ReactNode[]): string;
|
|
16
|
+
export declare function path(nodes: ReactNode[], id: number): string;
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
export async function snapshot(page) {
|
|
2
|
+
return page.evaluate(inPageSnapshot);
|
|
3
|
+
}
|
|
4
|
+
export async function inspect(page, id) {
|
|
5
|
+
return page.evaluate(inPageInspect, id);
|
|
6
|
+
}
|
|
7
|
+
export function format(nodes) {
|
|
8
|
+
const children = new Map();
|
|
9
|
+
for (const n of nodes) {
|
|
10
|
+
const list = children.get(n.parent) ?? [];
|
|
11
|
+
list.push(n);
|
|
12
|
+
children.set(n.parent, list);
|
|
13
|
+
}
|
|
14
|
+
const lines = [
|
|
15
|
+
"# React component tree",
|
|
16
|
+
"# Columns: depth id parent name [key=...]",
|
|
17
|
+
"# Use `react tree <id>` for props/hooks/state.",
|
|
18
|
+
"",
|
|
19
|
+
];
|
|
20
|
+
for (const root of children.get(0) ?? [])
|
|
21
|
+
walk(root, 0);
|
|
22
|
+
return lines.join("\n");
|
|
23
|
+
function walk(node, depth) {
|
|
24
|
+
const name = node.name ?? typeName(node.type);
|
|
25
|
+
const key = node.key ? ` key=${JSON.stringify(node.key)}` : "";
|
|
26
|
+
const parent = node.parent || "-";
|
|
27
|
+
lines.push(`${depth} ${node.id} ${parent} ${name}${key}`);
|
|
28
|
+
for (const c of children.get(node.id) ?? [])
|
|
29
|
+
walk(c, depth + 1);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
export function path(nodes, id) {
|
|
33
|
+
const byId = new Map(nodes.map((n) => [n.id, n]));
|
|
34
|
+
const names = [];
|
|
35
|
+
for (let n = byId.get(id); n; n = byId.get(n.parent)) {
|
|
36
|
+
names.push(n.name ?? typeName(n.type));
|
|
37
|
+
}
|
|
38
|
+
return names.reverse().join(" > ");
|
|
39
|
+
}
|
|
40
|
+
function typeName(type) {
|
|
41
|
+
const names = {
|
|
42
|
+
11: "Root",
|
|
43
|
+
12: "Suspense",
|
|
44
|
+
13: "SuspenseList",
|
|
45
|
+
};
|
|
46
|
+
return names[type] ?? `(${type})`;
|
|
47
|
+
}
|
|
48
|
+
async function inPageSnapshot() {
|
|
49
|
+
const hook = window.__REACT_DEVTOOLS_GLOBAL_HOOK__;
|
|
50
|
+
if (!hook)
|
|
51
|
+
throw new Error("React DevTools hook not installed");
|
|
52
|
+
const ri = hook.rendererInterfaces?.get?.(1);
|
|
53
|
+
if (!ri)
|
|
54
|
+
throw new Error("no React renderer attached");
|
|
55
|
+
const batches = await collect(ri);
|
|
56
|
+
return batches.flatMap(decode);
|
|
57
|
+
function collect(ri) {
|
|
58
|
+
return new Promise((resolve) => {
|
|
59
|
+
const out = [];
|
|
60
|
+
const listener = (e) => {
|
|
61
|
+
const payload = e.data?.payload;
|
|
62
|
+
if (e.data?.source === "react-devtools-bridge" && payload?.event === "operations") {
|
|
63
|
+
out.push(payload.payload);
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
window.addEventListener("message", listener);
|
|
67
|
+
ri.flushInitialOperations();
|
|
68
|
+
setTimeout(() => {
|
|
69
|
+
window.removeEventListener("message", listener);
|
|
70
|
+
resolve(out);
|
|
71
|
+
}, 80);
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
function decode(ops) {
|
|
75
|
+
let i = 2;
|
|
76
|
+
const strings = [null];
|
|
77
|
+
const tableEnd = ++i + ops[i - 1];
|
|
78
|
+
while (i < tableEnd) {
|
|
79
|
+
const len = ops[i++];
|
|
80
|
+
strings.push(String.fromCodePoint(...ops.slice(i, i + len)));
|
|
81
|
+
i += len;
|
|
82
|
+
}
|
|
83
|
+
const nodes = [];
|
|
84
|
+
while (i < ops.length) {
|
|
85
|
+
const op = ops[i];
|
|
86
|
+
if (op === 1) {
|
|
87
|
+
const id = ops[i + 1];
|
|
88
|
+
const type = ops[i + 2];
|
|
89
|
+
i += 3;
|
|
90
|
+
if (type === 11) {
|
|
91
|
+
nodes.push({ id, type, name: null, key: null, parent: 0 });
|
|
92
|
+
i += 4;
|
|
93
|
+
}
|
|
94
|
+
else {
|
|
95
|
+
nodes.push({
|
|
96
|
+
id,
|
|
97
|
+
type,
|
|
98
|
+
name: strings[ops[i + 2]] ?? null,
|
|
99
|
+
key: strings[ops[i + 3]] ?? null,
|
|
100
|
+
parent: ops[i],
|
|
101
|
+
});
|
|
102
|
+
i += 5;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
else {
|
|
106
|
+
i += skip(op, ops, i);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return nodes;
|
|
110
|
+
}
|
|
111
|
+
function skip(op, ops, i) {
|
|
112
|
+
if (op === 2)
|
|
113
|
+
return 2 + ops[i + 1];
|
|
114
|
+
if (op === 3)
|
|
115
|
+
return 3 + ops[i + 2];
|
|
116
|
+
if (op === 4)
|
|
117
|
+
return 3;
|
|
118
|
+
if (op === 5)
|
|
119
|
+
return 4;
|
|
120
|
+
if (op === 6)
|
|
121
|
+
return 1;
|
|
122
|
+
if (op === 7)
|
|
123
|
+
return 3;
|
|
124
|
+
if (op === 8)
|
|
125
|
+
return 6 + rects(ops[i + 5]);
|
|
126
|
+
if (op === 9)
|
|
127
|
+
return 2 + ops[i + 1];
|
|
128
|
+
if (op === 10)
|
|
129
|
+
return 3 + ops[i + 2];
|
|
130
|
+
if (op === 11)
|
|
131
|
+
return 3 + rects(ops[i + 2]);
|
|
132
|
+
if (op === 12)
|
|
133
|
+
return suspenders(ops, i);
|
|
134
|
+
if (op === 13)
|
|
135
|
+
return 2;
|
|
136
|
+
return 1;
|
|
137
|
+
}
|
|
138
|
+
function rects(n) {
|
|
139
|
+
return n === -1 ? 0 : n * 4;
|
|
140
|
+
}
|
|
141
|
+
function suspenders(ops, i) {
|
|
142
|
+
let j = i + 2;
|
|
143
|
+
for (let c = 0; c < ops[i + 1]; c++)
|
|
144
|
+
j += 5 + ops[j + 4];
|
|
145
|
+
return j - i;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
function inPageInspect(id) {
|
|
149
|
+
const hook = window.__REACT_DEVTOOLS_GLOBAL_HOOK__;
|
|
150
|
+
const ri = hook?.rendererInterfaces?.get?.(1);
|
|
151
|
+
if (!ri)
|
|
152
|
+
throw new Error("no React renderer attached");
|
|
153
|
+
if (!ri.hasElementWithId(id))
|
|
154
|
+
throw new Error(`element ${id} not found (page reloaded?)`);
|
|
155
|
+
const result = ri.inspectElement(1, id, null, true);
|
|
156
|
+
if (result?.type !== "full-data")
|
|
157
|
+
throw new Error(`inspect failed: ${result?.type}`);
|
|
158
|
+
const value = result.value;
|
|
159
|
+
const name = ri.getDisplayNameForElementID(id);
|
|
160
|
+
const lines = [`${name} #${id}`];
|
|
161
|
+
if (value.key != null)
|
|
162
|
+
lines.push(`key: ${JSON.stringify(value.key)}`);
|
|
163
|
+
section("props", value.props);
|
|
164
|
+
section("hooks", value.hooks);
|
|
165
|
+
section("state", value.state);
|
|
166
|
+
section("context", value.context);
|
|
167
|
+
if (value.owners?.length) {
|
|
168
|
+
const chain = value.owners.map((o) => o.displayName).join(" > ");
|
|
169
|
+
lines.push(`rendered by: ${chain}`);
|
|
170
|
+
}
|
|
171
|
+
const source = Array.isArray(value.source)
|
|
172
|
+
? [value.source[1], value.source[2], value.source[3]]
|
|
173
|
+
: null;
|
|
174
|
+
return { text: lines.join("\n"), source };
|
|
175
|
+
function section(label, payload) {
|
|
176
|
+
const data = payload?.data ?? payload;
|
|
177
|
+
if (data == null)
|
|
178
|
+
return;
|
|
179
|
+
if (Array.isArray(data)) {
|
|
180
|
+
if (data.length === 0)
|
|
181
|
+
return;
|
|
182
|
+
lines.push(`${label}:`);
|
|
183
|
+
for (const item of data)
|
|
184
|
+
lines.push(` ${hookLine(item)}`);
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
if (typeof data === "object") {
|
|
188
|
+
const entries = Object.entries(data);
|
|
189
|
+
if (entries.length === 0)
|
|
190
|
+
return;
|
|
191
|
+
lines.push(`${label}:`);
|
|
192
|
+
for (const [k, v] of entries)
|
|
193
|
+
lines.push(` ${k}: ${preview(v)}`);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
function hookLine(h) {
|
|
197
|
+
const idx = h.id != null ? `[${h.id}] ` : "";
|
|
198
|
+
const sub = h.subHooks?.length ? ` (${h.subHooks.length} sub)` : "";
|
|
199
|
+
return `${idx}${h.name}: ${preview(h.value)}${sub}`;
|
|
200
|
+
}
|
|
201
|
+
function preview(v) {
|
|
202
|
+
if (v == null)
|
|
203
|
+
return String(v);
|
|
204
|
+
if (typeof v !== "object")
|
|
205
|
+
return JSON.stringify(v);
|
|
206
|
+
const d = v;
|
|
207
|
+
if (d.type === "undefined")
|
|
208
|
+
return "undefined";
|
|
209
|
+
if (d.preview_long)
|
|
210
|
+
return d.preview_long;
|
|
211
|
+
if (d.preview_short)
|
|
212
|
+
return d.preview_short;
|
|
213
|
+
if (Array.isArray(v))
|
|
214
|
+
return `[${v.map(preview).join(", ")}]`;
|
|
215
|
+
const entries = Object.entries(v).map(([k, val]) => `${k}: ${preview(val)}`);
|
|
216
|
+
return `{${entries.join(", ")}}`;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
export async function getComponentTree(page) {
|
|
2
|
+
return page.evaluate(() => {
|
|
3
|
+
const output = [
|
|
4
|
+
"# Svelte component tree",
|
|
5
|
+
"# Use `svelte tree <id>` to inspect component details.",
|
|
6
|
+
"",
|
|
7
|
+
];
|
|
8
|
+
const nodes = collectSvelteNodes();
|
|
9
|
+
if (nodes.length === 0) {
|
|
10
|
+
output.push("Svelte app detected, but no DevTools component graph is available.");
|
|
11
|
+
output.push("Tip: install/enable Svelte DevTools, then run `svelte tree` again.");
|
|
12
|
+
return output.join("\n");
|
|
13
|
+
}
|
|
14
|
+
const children = new Map();
|
|
15
|
+
for (const n of nodes) {
|
|
16
|
+
const list = children.get(n.parent) ?? [];
|
|
17
|
+
list.push(n);
|
|
18
|
+
children.set(n.parent, list);
|
|
19
|
+
}
|
|
20
|
+
for (const root of children.get(0) ?? [])
|
|
21
|
+
walk(root, 0);
|
|
22
|
+
return output.join("\n");
|
|
23
|
+
function walk(node, depth) {
|
|
24
|
+
const indent = " ".repeat(depth);
|
|
25
|
+
output.push(`${indent}[${node.id}] ${node.name}`);
|
|
26
|
+
for (const c of children.get(node.id) ?? [])
|
|
27
|
+
walk(c, depth + 1);
|
|
28
|
+
}
|
|
29
|
+
function collectSvelteNodes() {
|
|
30
|
+
const out = [];
|
|
31
|
+
const seen = new WeakSet();
|
|
32
|
+
let seq = 1;
|
|
33
|
+
const globalCandidates = [
|
|
34
|
+
window.__SVELTE_DEVTOOLS_GLOBAL_HOOK__,
|
|
35
|
+
window.__SVELTE_DEVTOOLS__,
|
|
36
|
+
window.__svelte,
|
|
37
|
+
].filter(Boolean);
|
|
38
|
+
for (const candidate of globalCandidates) {
|
|
39
|
+
visit(candidate, 0, 0);
|
|
40
|
+
}
|
|
41
|
+
return out;
|
|
42
|
+
function visit(value, parent, depth) {
|
|
43
|
+
if (!value || typeof value !== "object")
|
|
44
|
+
return;
|
|
45
|
+
if (seen.has(value))
|
|
46
|
+
return;
|
|
47
|
+
if (depth > 6)
|
|
48
|
+
return;
|
|
49
|
+
seen.add(value);
|
|
50
|
+
const maybeComponent = !!value.$$ ||
|
|
51
|
+
typeof value.$set === "function" ||
|
|
52
|
+
typeof value.$destroy === "function" ||
|
|
53
|
+
value.type === "component";
|
|
54
|
+
let currentParent = parent;
|
|
55
|
+
if (maybeComponent) {
|
|
56
|
+
const id = Number.isFinite(Number(value.id)) ? Number(value.id) : seq++;
|
|
57
|
+
const name = value.name ||
|
|
58
|
+
value.$$?.component?.name ||
|
|
59
|
+
value.constructor?.name ||
|
|
60
|
+
value.$$?.tag ||
|
|
61
|
+
"AnonymousSvelteComponent";
|
|
62
|
+
const props = value.$capture_state?.() ?? value.props ?? value.$$?.props;
|
|
63
|
+
out.push({ id, parent, name: String(name), props });
|
|
64
|
+
currentParent = id;
|
|
65
|
+
}
|
|
66
|
+
const children = [
|
|
67
|
+
value.children,
|
|
68
|
+
value.$$?.children,
|
|
69
|
+
value.$$.fragment?.children,
|
|
70
|
+
value.components,
|
|
71
|
+
value.apps,
|
|
72
|
+
value.instances,
|
|
73
|
+
value.roots,
|
|
74
|
+
].filter(Boolean);
|
|
75
|
+
for (const child of children) {
|
|
76
|
+
if (Array.isArray(child)) {
|
|
77
|
+
for (const c of child)
|
|
78
|
+
visit(c, currentParent, depth + 1);
|
|
79
|
+
}
|
|
80
|
+
else if (typeof child === "object") {
|
|
81
|
+
for (const c of Object.values(child))
|
|
82
|
+
visit(c, currentParent, depth + 1);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
export async function getComponentDetails(page, id) {
|
|
90
|
+
return page.evaluate((targetId) => {
|
|
91
|
+
const nodes = collectSvelteNodes();
|
|
92
|
+
const match = nodes.find((n) => String(n.id) === targetId);
|
|
93
|
+
if (!match) {
|
|
94
|
+
return `Component ${targetId} not found. Run \'svelte tree\' to get fresh IDs.`;
|
|
95
|
+
}
|
|
96
|
+
const output = [`# Svelte component`, `# ID: ${match.id}`, `# Name: ${match.name}`, ""];
|
|
97
|
+
if (match.props && typeof match.props === "object") {
|
|
98
|
+
output.push("## Props/State");
|
|
99
|
+
for (const [k, v] of Object.entries(match.props)) {
|
|
100
|
+
output.push(` ${k}: ${safe(v)}`);
|
|
101
|
+
}
|
|
102
|
+
output.push("");
|
|
103
|
+
}
|
|
104
|
+
if (match.file) {
|
|
105
|
+
output.push("## Source");
|
|
106
|
+
output.push(` ${match.file}`);
|
|
107
|
+
}
|
|
108
|
+
return output.join("\n");
|
|
109
|
+
function safe(v) {
|
|
110
|
+
try {
|
|
111
|
+
return JSON.stringify(v);
|
|
112
|
+
}
|
|
113
|
+
catch {
|
|
114
|
+
return String(v);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
function collectSvelteNodes() {
|
|
118
|
+
const out = [];
|
|
119
|
+
const seen = new WeakSet();
|
|
120
|
+
let seq = 1;
|
|
121
|
+
const globalCandidates = [
|
|
122
|
+
window.__SVELTE_DEVTOOLS_GLOBAL_HOOK__,
|
|
123
|
+
window.__SVELTE_DEVTOOLS__,
|
|
124
|
+
window.__svelte,
|
|
125
|
+
].filter(Boolean);
|
|
126
|
+
for (const candidate of globalCandidates) {
|
|
127
|
+
visit(candidate, 0, 0);
|
|
128
|
+
}
|
|
129
|
+
return out;
|
|
130
|
+
function visit(value, parent, depth) {
|
|
131
|
+
if (!value || typeof value !== "object")
|
|
132
|
+
return;
|
|
133
|
+
if (seen.has(value))
|
|
134
|
+
return;
|
|
135
|
+
if (depth > 6)
|
|
136
|
+
return;
|
|
137
|
+
seen.add(value);
|
|
138
|
+
const maybeComponent = !!value.$$ ||
|
|
139
|
+
typeof value.$set === "function" ||
|
|
140
|
+
typeof value.$destroy === "function" ||
|
|
141
|
+
value.type === "component";
|
|
142
|
+
let currentParent = parent;
|
|
143
|
+
if (maybeComponent) {
|
|
144
|
+
const id = Number.isFinite(Number(value.id)) ? Number(value.id) : seq++;
|
|
145
|
+
const name = value.name ||
|
|
146
|
+
value.$$?.component?.name ||
|
|
147
|
+
value.constructor?.name ||
|
|
148
|
+
value.$$?.tag ||
|
|
149
|
+
"AnonymousSvelteComponent";
|
|
150
|
+
const props = value.$capture_state?.() ?? value.props ?? value.$$?.props;
|
|
151
|
+
const file = value.$$?.component?.__file || value.file;
|
|
152
|
+
out.push({ id, parent, name: String(name), props, file });
|
|
153
|
+
currentParent = id;
|
|
154
|
+
}
|
|
155
|
+
const children = [
|
|
156
|
+
value.children,
|
|
157
|
+
value.$$?.children,
|
|
158
|
+
value.$$.fragment?.children,
|
|
159
|
+
value.components,
|
|
160
|
+
value.apps,
|
|
161
|
+
value.instances,
|
|
162
|
+
value.roots,
|
|
163
|
+
].filter(Boolean);
|
|
164
|
+
for (const child of children) {
|
|
165
|
+
if (Array.isArray(child)) {
|
|
166
|
+
for (const c of child)
|
|
167
|
+
visit(c, currentParent, depth + 1);
|
|
168
|
+
}
|
|
169
|
+
else if (typeof child === "object") {
|
|
170
|
+
for (const c of Object.values(child))
|
|
171
|
+
visit(c, currentParent, depth + 1);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}, id);
|
|
177
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Vue DevTools integration
|
|
3
|
+
*
|
|
4
|
+
* Uses @vue/devtools-kit to access Vue component tree and state
|
|
5
|
+
*/
|
|
6
|
+
import type { Page } from "playwright";
|
|
7
|
+
export interface VueComponent {
|
|
8
|
+
id: string;
|
|
9
|
+
name: string;
|
|
10
|
+
type: string;
|
|
11
|
+
file?: string;
|
|
12
|
+
line?: number;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Get Vue component tree from the page
|
|
16
|
+
*/
|
|
17
|
+
export declare function getComponentTree(page: Page): Promise<string>;
|
|
18
|
+
/**
|
|
19
|
+
* Get component details by ID
|
|
20
|
+
*/
|
|
21
|
+
export declare function getComponentDetails(page: Page, id: string): Promise<string>;
|
|
22
|
+
/**
|
|
23
|
+
* Get Pinia stores
|
|
24
|
+
*/
|
|
25
|
+
export declare function getPiniaStores(page: Page, storeName?: string): Promise<string>;
|
|
26
|
+
/**
|
|
27
|
+
* Get Vue Router information
|
|
28
|
+
*/
|
|
29
|
+
export declare function getRouterInfo(page: Page): Promise<string>;
|