@presto1314w/vite-devtools-browser 0.2.0 → 0.3.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 +43 -9
- package/dist/browser-collector.d.ts +3 -0
- package/dist/browser-collector.js +256 -0
- package/dist/browser-frameworks.d.ts +11 -0
- package/dist/browser-frameworks.js +72 -0
- package/dist/browser-session.d.ts +27 -0
- package/dist/browser-session.js +113 -0
- package/dist/browser-vite.d.ts +25 -0
- package/dist/browser-vite.js +181 -0
- package/dist/browser.d.ts +4 -28
- package/dist/browser.js +70 -444
- package/dist/cli.js +13 -0
- package/dist/correlate.d.ts +2 -1
- package/dist/correlate.js +11 -42
- package/dist/daemon.js +12 -0
- package/dist/diagnose-propagation.d.ts +10 -0
- package/dist/diagnose-propagation.js +58 -0
- package/dist/diagnose.js +28 -5
- package/dist/event-analysis.d.ts +32 -0
- package/dist/event-analysis.js +75 -0
- package/dist/event-queue.d.ts +70 -5
- package/dist/trace.d.ts +15 -0
- package/dist/trace.js +75 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -7,6 +7,7 @@ It gives agents and developers structured access to:
|
|
|
7
7
|
- Vue, React, and Svelte runtime state
|
|
8
8
|
- Vite HMR activity and runtime health
|
|
9
9
|
- event-window correlation between current errors and recent hot updates
|
|
10
|
+
- early propagation diagnostics from store/module updates into rerender paths
|
|
10
11
|
- rule-based HMR diagnosis with confidence levels
|
|
11
12
|
- module graph snapshots and diffs
|
|
12
13
|
- mapped error output with optional source snippets
|
|
@@ -17,16 +18,19 @@ It ships in two forms:
|
|
|
17
18
|
- Agent Skill: scenario-based debugging workflows for coding assistants
|
|
18
19
|
- CLI Runtime (`@presto1314w/vite-devtools-browser`): structured shell commands for local Vite debugging
|
|
19
20
|
|
|
20
|
-
Current documented baseline: `v0.
|
|
21
|
+
Current documented baseline: `v0.3.0`.
|
|
21
22
|
|
|
22
|
-
## What's New In v0.
|
|
23
|
+
## What's New In v0.3
|
|
23
24
|
|
|
24
|
-
`v0.
|
|
25
|
+
`v0.3.0` is the propagation-diagnostics release.
|
|
25
26
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
- `
|
|
29
|
-
-
|
|
27
|
+
It keeps the `v0.2.x` runtime diagnosis model, then adds a new layer of propagation-oriented reasoning:
|
|
28
|
+
|
|
29
|
+
- `correlate renders` summarizes recent render/update propagation evidence
|
|
30
|
+
- `diagnose propagation` turns store/module/render/error signals into structured guidance
|
|
31
|
+
- Vue-first store updates now include top-level `changedKeys`
|
|
32
|
+
- browser-side collection now captures render and store-update signals as first-class events
|
|
33
|
+
- propagation output stays conservative when evidence is incomplete
|
|
30
34
|
|
|
31
35
|
## Built For Agents
|
|
32
36
|
|
|
@@ -45,6 +49,7 @@ Most browser CLIs are optimized for automation. Most framework devtools are opti
|
|
|
45
49
|
- it can inspect framework state like a devtools bridge
|
|
46
50
|
- it can explain Vite-specific behavior like HMR updates and module graph changes
|
|
47
51
|
- it can correlate recent updates with current failures
|
|
52
|
+
- it can surface high-confidence clues about how store/module changes propagate into rerender paths
|
|
48
53
|
- it returns structured text that AI agents can consume directly in loops
|
|
49
54
|
|
|
50
55
|
## Positioning
|
|
@@ -84,6 +89,8 @@ vite-browser detect
|
|
|
84
89
|
vite-browser vite runtime
|
|
85
90
|
vite-browser errors --mapped --inline-source
|
|
86
91
|
vite-browser correlate errors --mapped --window 5000
|
|
92
|
+
vite-browser correlate renders --window 5000
|
|
93
|
+
vite-browser diagnose propagation --window 5000
|
|
87
94
|
vite-browser diagnose hmr --limit 50
|
|
88
95
|
vite-browser vite hmr trace --limit 20
|
|
89
96
|
vite-browser vite module-graph trace --limit 50
|
|
@@ -135,6 +142,26 @@ Confidence: high
|
|
|
135
142
|
HMR update observed within 5000ms of the current error
|
|
136
143
|
Matching modules: /src/App.tsx
|
|
137
144
|
|
|
145
|
+
$ vite-browser correlate renders --window 5000
|
|
146
|
+
# Render Correlation
|
|
147
|
+
Confidence: high
|
|
148
|
+
Recent source/store updates likely propagated through 1 render step(s).
|
|
149
|
+
|
|
150
|
+
## Store Updates
|
|
151
|
+
- cart
|
|
152
|
+
|
|
153
|
+
## Changed Keys
|
|
154
|
+
- items
|
|
155
|
+
|
|
156
|
+
## Render Path
|
|
157
|
+
- AppShell > ShoppingCart > CartSummary
|
|
158
|
+
|
|
159
|
+
$ vite-browser diagnose propagation --window 5000
|
|
160
|
+
# Propagation Diagnosis
|
|
161
|
+
Status: fail
|
|
162
|
+
Confidence: high
|
|
163
|
+
A plausible store -> render -> error propagation path was found.
|
|
164
|
+
|
|
138
165
|
$ vite-browser diagnose hmr --limit 50
|
|
139
166
|
# HMR Diagnosis
|
|
140
167
|
## missing-module
|
|
@@ -155,6 +182,8 @@ Suggestion: Verify the import path, file extension, alias configuration, and whe
|
|
|
155
182
|
- HMR summary/timeline/clear
|
|
156
183
|
- module-graph snapshot/diff/clear
|
|
157
184
|
- error/HMR correlation over recent event windows
|
|
185
|
+
- render/store propagation correlation over recent event windows
|
|
186
|
+
- early propagation diagnosis with store updates, changed keys, and render paths
|
|
158
187
|
- rule-based HMR diagnosis with confidence levels
|
|
159
188
|
- source-mapped errors with optional inline source snippet
|
|
160
189
|
- Debug utilities: console logs, network tracing, screenshot, page `eval`
|
|
@@ -167,6 +196,8 @@ Suggestion: Verify the import path, file extension, alias configuration, and whe
|
|
|
167
196
|
vite-browser vite runtime
|
|
168
197
|
vite-browser errors --mapped --inline-source
|
|
169
198
|
vite-browser correlate errors --mapped --window 5000
|
|
199
|
+
vite-browser correlate renders --window 5000
|
|
200
|
+
vite-browser diagnose propagation --window 5000
|
|
170
201
|
vite-browser diagnose hmr --limit 50
|
|
171
202
|
vite-browser vite hmr trace --limit 50
|
|
172
203
|
vite-browser vite module-graph trace --limit 200
|
|
@@ -195,13 +226,14 @@ vite-browser svelte tree
|
|
|
195
226
|
|
|
196
227
|
## Current Boundaries
|
|
197
228
|
|
|
198
|
-
`vite-browser` v0.
|
|
229
|
+
`vite-browser` v0.3.0 is strong at:
|
|
199
230
|
|
|
200
231
|
- surfacing runtime state as structured shell output
|
|
201
232
|
- linking current errors to recent HMR/module activity
|
|
202
233
|
- detecting several common HMR failure patterns quickly
|
|
234
|
+
- narrowing likely store/module -> render paths in Vue-first flows
|
|
203
235
|
|
|
204
|
-
|
|
236
|
+
`v0.3.0` is still not a full propagation-trace engine. Treat `correlate renders` and `diagnose propagation` as high-confidence propagation clues, not strict causal proof. In particular, they do not reliably infer deep chains like `store -> component A -> component B -> error` across arbitrary component graphs, and they intentionally fall back to conservative output when evidence is incomplete.
|
|
205
237
|
|
|
206
238
|
## Command Reference
|
|
207
239
|
|
|
@@ -241,8 +273,10 @@ vite-browser errors
|
|
|
241
273
|
vite-browser errors --mapped
|
|
242
274
|
vite-browser errors --mapped --inline-source
|
|
243
275
|
vite-browser correlate errors [--window <ms>]
|
|
276
|
+
vite-browser correlate renders [--window <ms>]
|
|
244
277
|
vite-browser correlate errors --mapped --inline-source
|
|
245
278
|
vite-browser diagnose hmr [--window <ms>] [--limit <n>]
|
|
279
|
+
vite-browser diagnose propagation [--window <ms>]
|
|
246
280
|
```
|
|
247
281
|
|
|
248
282
|
### Utilities
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
export function initBrowserEventCollector() {
|
|
2
|
+
if (window.__vb_events)
|
|
3
|
+
return;
|
|
4
|
+
window.__vb_events = [];
|
|
5
|
+
window.__vb_push = (event) => {
|
|
6
|
+
const q = window.__vb_events;
|
|
7
|
+
q.push(event);
|
|
8
|
+
if (q.length > 1000)
|
|
9
|
+
q.shift();
|
|
10
|
+
};
|
|
11
|
+
const inferFramework = () => {
|
|
12
|
+
if (window.__VUE__ || window.__VUE_DEVTOOLS_GLOBAL_HOOK__)
|
|
13
|
+
return "vue";
|
|
14
|
+
if (window.__REACT_DEVTOOLS_GLOBAL_HOOK__ || window.React)
|
|
15
|
+
return "react";
|
|
16
|
+
if (window.__SVELTE__ || window.__svelte || window.__SVELTE_DEVTOOLS_GLOBAL_HOOK__) {
|
|
17
|
+
return "svelte";
|
|
18
|
+
}
|
|
19
|
+
return "unknown";
|
|
20
|
+
};
|
|
21
|
+
const inferRenderLabel = () => {
|
|
22
|
+
const heading = document.querySelector("main h1, [role='main'] h1, h1")?.textContent?.trim() ||
|
|
23
|
+
document.title ||
|
|
24
|
+
location.pathname ||
|
|
25
|
+
"/";
|
|
26
|
+
return heading.slice(0, 120);
|
|
27
|
+
};
|
|
28
|
+
const inferVueRenderDetails = () => {
|
|
29
|
+
const hook = window.__VUE_DEVTOOLS_GLOBAL_HOOK__;
|
|
30
|
+
const apps = hook?.apps;
|
|
31
|
+
if (!Array.isArray(apps) || apps.length === 0)
|
|
32
|
+
return null;
|
|
33
|
+
const app = apps[0];
|
|
34
|
+
const rootInstance = app?._instance || app?._container?._vnode?.component;
|
|
35
|
+
if (!rootInstance)
|
|
36
|
+
return null;
|
|
37
|
+
const names = [];
|
|
38
|
+
let current = rootInstance;
|
|
39
|
+
let guard = 0;
|
|
40
|
+
while (current && guard < 8) {
|
|
41
|
+
const name = current.type?.name ||
|
|
42
|
+
current.type?.__name ||
|
|
43
|
+
current.type?.__file?.split(/[\\/]/).pop()?.replace(/\.\w+$/, "") ||
|
|
44
|
+
"Anonymous";
|
|
45
|
+
names.push(String(name));
|
|
46
|
+
const nextFromSubtree = current.subTree?.component;
|
|
47
|
+
const nextFromChildren = Array.isArray(current.subTree?.children)
|
|
48
|
+
? current.subTree.children.find((child) => child?.component)?.component
|
|
49
|
+
: null;
|
|
50
|
+
current = nextFromSubtree || nextFromChildren || null;
|
|
51
|
+
guard++;
|
|
52
|
+
}
|
|
53
|
+
const pinia = window.__PINIA__ || window.pinia || app?.config?.globalProperties?.$pinia;
|
|
54
|
+
const registry = pinia?._s;
|
|
55
|
+
const storeIds = registry instanceof Map
|
|
56
|
+
? Array.from(registry.keys()).map(String)
|
|
57
|
+
: registry && typeof registry === "object"
|
|
58
|
+
? Object.keys(registry)
|
|
59
|
+
: [];
|
|
60
|
+
return {
|
|
61
|
+
component: names[names.length - 1] || inferRenderLabel(),
|
|
62
|
+
componentPath: names.join(" > "),
|
|
63
|
+
storeHints: storeIds.slice(0, 5),
|
|
64
|
+
};
|
|
65
|
+
};
|
|
66
|
+
const inferRenderDetails = () => {
|
|
67
|
+
const framework = inferFramework();
|
|
68
|
+
if (framework === "vue") {
|
|
69
|
+
const vue = inferVueRenderDetails();
|
|
70
|
+
if (vue) {
|
|
71
|
+
return {
|
|
72
|
+
framework,
|
|
73
|
+
component: vue.component,
|
|
74
|
+
path: vue.componentPath,
|
|
75
|
+
storeHints: vue.storeHints,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return {
|
|
80
|
+
framework,
|
|
81
|
+
component: inferRenderLabel(),
|
|
82
|
+
path: `${framework}:${location.pathname || "/"}`,
|
|
83
|
+
storeHints: [],
|
|
84
|
+
};
|
|
85
|
+
};
|
|
86
|
+
const renderState = window.__vb_render_state ||
|
|
87
|
+
(window.__vb_render_state = {
|
|
88
|
+
timer: null,
|
|
89
|
+
lastReason: "initial-load",
|
|
90
|
+
mutationCount: 0,
|
|
91
|
+
});
|
|
92
|
+
const scheduleRender = (reason, extra = {}) => {
|
|
93
|
+
renderState.lastReason = reason;
|
|
94
|
+
renderState.mutationCount += Number(extra.mutationCount ?? 0);
|
|
95
|
+
if (renderState.timer != null)
|
|
96
|
+
window.clearTimeout(renderState.timer);
|
|
97
|
+
renderState.timer = window.setTimeout(() => {
|
|
98
|
+
const details = inferRenderDetails();
|
|
99
|
+
const changedKeys = Array.isArray(extra.changedKeys)
|
|
100
|
+
? extra.changedKeys.filter((value) => typeof value === "string" && value.length > 0)
|
|
101
|
+
: [];
|
|
102
|
+
window.__vb_push({
|
|
103
|
+
timestamp: Date.now(),
|
|
104
|
+
type: "render",
|
|
105
|
+
payload: {
|
|
106
|
+
component: details.component,
|
|
107
|
+
path: details.path,
|
|
108
|
+
framework: details.framework,
|
|
109
|
+
reason: renderState.lastReason,
|
|
110
|
+
mutationCount: renderState.mutationCount,
|
|
111
|
+
storeHints: details.storeHints,
|
|
112
|
+
changedKeys,
|
|
113
|
+
},
|
|
114
|
+
});
|
|
115
|
+
renderState.timer = null;
|
|
116
|
+
renderState.mutationCount = 0;
|
|
117
|
+
}, 60);
|
|
118
|
+
};
|
|
119
|
+
const attachPiniaSubscriptions = () => {
|
|
120
|
+
if (window.__vb_pinia_attached)
|
|
121
|
+
return;
|
|
122
|
+
const hook = window.__VUE_DEVTOOLS_GLOBAL_HOOK__;
|
|
123
|
+
const app = Array.isArray(hook?.apps) ? hook.apps[0] : null;
|
|
124
|
+
const pinia = window.__PINIA__ || window.pinia || app?.config?.globalProperties?.$pinia;
|
|
125
|
+
const registry = pinia?._s;
|
|
126
|
+
if (!(registry instanceof Map) || registry.size === 0)
|
|
127
|
+
return;
|
|
128
|
+
const attached = (window.__vb_pinia_attached = new Set());
|
|
129
|
+
registry.forEach((store, storeId) => {
|
|
130
|
+
if (!store || typeof store.$subscribe !== "function" || attached.has(String(storeId)))
|
|
131
|
+
return;
|
|
132
|
+
attached.add(String(storeId));
|
|
133
|
+
const extractChangedKeys = (mutation) => {
|
|
134
|
+
const keys = new Set();
|
|
135
|
+
const events = mutation?.events;
|
|
136
|
+
const collect = (entry) => {
|
|
137
|
+
const key = entry?.key ?? entry?.path;
|
|
138
|
+
if (typeof key === "string" && key.length > 0)
|
|
139
|
+
keys.add(key.split(".")[0]);
|
|
140
|
+
};
|
|
141
|
+
if (Array.isArray(events))
|
|
142
|
+
events.forEach(collect);
|
|
143
|
+
else if (events && typeof events === "object")
|
|
144
|
+
collect(events);
|
|
145
|
+
const payload = mutation?.payload;
|
|
146
|
+
if (payload && typeof payload === "object" && !Array.isArray(payload)) {
|
|
147
|
+
Object.keys(payload).forEach((key) => keys.add(key));
|
|
148
|
+
}
|
|
149
|
+
return Array.from(keys).slice(0, 10);
|
|
150
|
+
};
|
|
151
|
+
store.$subscribe((mutation) => {
|
|
152
|
+
const changedKeys = extractChangedKeys(mutation);
|
|
153
|
+
window.__vb_push({
|
|
154
|
+
timestamp: Date.now(),
|
|
155
|
+
type: "store-update",
|
|
156
|
+
payload: {
|
|
157
|
+
store: String(storeId),
|
|
158
|
+
mutationType: typeof mutation?.type === "string" ? mutation.type : "unknown",
|
|
159
|
+
events: Array.isArray(mutation?.events) ? mutation.events.length : 0,
|
|
160
|
+
changedKeys,
|
|
161
|
+
},
|
|
162
|
+
});
|
|
163
|
+
scheduleRender("store-update", { changedKeys });
|
|
164
|
+
}, { detached: true });
|
|
165
|
+
});
|
|
166
|
+
};
|
|
167
|
+
function attachViteListener() {
|
|
168
|
+
const hot = window.__vite_hot;
|
|
169
|
+
if (hot?.ws) {
|
|
170
|
+
hot.ws.addEventListener("message", (e) => {
|
|
171
|
+
try {
|
|
172
|
+
const data = JSON.parse(e.data);
|
|
173
|
+
window.__vb_push({
|
|
174
|
+
timestamp: Date.now(),
|
|
175
|
+
type: data.type === "error" ? "hmr-error" : "hmr-update",
|
|
176
|
+
payload: data,
|
|
177
|
+
});
|
|
178
|
+
if (data.type === "update" || data.type === "full-reload") {
|
|
179
|
+
scheduleRender("hmr-message");
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
catch { }
|
|
183
|
+
});
|
|
184
|
+
return true;
|
|
185
|
+
}
|
|
186
|
+
return false;
|
|
187
|
+
}
|
|
188
|
+
if (!attachViteListener()) {
|
|
189
|
+
let attempts = 0;
|
|
190
|
+
const timer = setInterval(() => {
|
|
191
|
+
attempts++;
|
|
192
|
+
if (attachViteListener() || attempts >= 50) {
|
|
193
|
+
clearInterval(timer);
|
|
194
|
+
}
|
|
195
|
+
}, 100);
|
|
196
|
+
}
|
|
197
|
+
const origOnError = window.onerror;
|
|
198
|
+
window.onerror = (msg, src, line, col, err) => {
|
|
199
|
+
window.__vb_push({
|
|
200
|
+
timestamp: Date.now(),
|
|
201
|
+
type: "error",
|
|
202
|
+
payload: { message: String(msg), source: src, line, col, stack: err?.stack },
|
|
203
|
+
});
|
|
204
|
+
return origOnError ? origOnError(msg, src, line, col, err) : false;
|
|
205
|
+
};
|
|
206
|
+
window.addEventListener("unhandledrejection", (e) => {
|
|
207
|
+
window.__vb_push({
|
|
208
|
+
timestamp: Date.now(),
|
|
209
|
+
type: "error",
|
|
210
|
+
payload: { message: e.reason?.message, stack: e.reason?.stack },
|
|
211
|
+
});
|
|
212
|
+
});
|
|
213
|
+
const observeDom = () => {
|
|
214
|
+
const root = document.body || document.documentElement;
|
|
215
|
+
if (!root || window.__vb_render_observer)
|
|
216
|
+
return;
|
|
217
|
+
const observer = new MutationObserver((mutations) => {
|
|
218
|
+
scheduleRender("dom-mutation", { mutationCount: mutations.length });
|
|
219
|
+
});
|
|
220
|
+
observer.observe(root, {
|
|
221
|
+
childList: true,
|
|
222
|
+
subtree: true,
|
|
223
|
+
attributes: true,
|
|
224
|
+
characterData: true,
|
|
225
|
+
});
|
|
226
|
+
window.__vb_render_observer = observer;
|
|
227
|
+
};
|
|
228
|
+
const patchHistory = () => {
|
|
229
|
+
if (window.__vb_history_patched)
|
|
230
|
+
return;
|
|
231
|
+
const wrap = (method) => {
|
|
232
|
+
const original = history[method];
|
|
233
|
+
const wrapped = ((...args) => {
|
|
234
|
+
const result = Reflect.apply(original, history, args);
|
|
235
|
+
scheduleRender(`history-${method}`);
|
|
236
|
+
return result;
|
|
237
|
+
});
|
|
238
|
+
history[method] = wrapped;
|
|
239
|
+
};
|
|
240
|
+
wrap("pushState");
|
|
241
|
+
wrap("replaceState");
|
|
242
|
+
window.addEventListener("popstate", () => scheduleRender("history-popstate"));
|
|
243
|
+
window.addEventListener("hashchange", () => scheduleRender("hashchange"));
|
|
244
|
+
window.__vb_history_patched = true;
|
|
245
|
+
};
|
|
246
|
+
observeDom();
|
|
247
|
+
patchHistory();
|
|
248
|
+
attachPiniaSubscriptions();
|
|
249
|
+
window.setInterval(attachPiniaSubscriptions, 1000);
|
|
250
|
+
scheduleRender("initial-load");
|
|
251
|
+
}
|
|
252
|
+
export function readBrowserEvents() {
|
|
253
|
+
const events = (window.__vb_events ?? []);
|
|
254
|
+
window.__vb_events = [];
|
|
255
|
+
return events;
|
|
256
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { Page } from "playwright";
|
|
2
|
+
import type { BrowserFramework, BrowserSessionState } from "./browser-session.js";
|
|
3
|
+
export declare function detectBrowserFramework(page: Page): Promise<{
|
|
4
|
+
detected: string;
|
|
5
|
+
framework: BrowserFramework;
|
|
6
|
+
}>;
|
|
7
|
+
export declare function inspectVueTree(page: Page, id?: string): Promise<string>;
|
|
8
|
+
export declare function inspectVuePinia(page: Page, store?: string): Promise<string>;
|
|
9
|
+
export declare function inspectVueRouter(page: Page): Promise<string>;
|
|
10
|
+
export declare function inspectReactTree(state: BrowserSessionState, page: Page, id?: string): Promise<string>;
|
|
11
|
+
export declare function inspectSvelteTree(page: Page, id?: string): Promise<string>;
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import * as vueDevtools from "./vue/devtools.js";
|
|
2
|
+
import * as reactDevtools from "./react/devtools.js";
|
|
3
|
+
import * as svelteDevtools from "./svelte/devtools.js";
|
|
4
|
+
export async function detectBrowserFramework(page) {
|
|
5
|
+
const detected = await page.evaluate(() => {
|
|
6
|
+
if (window.__VUE__ || window.__VUE_DEVTOOLS_GLOBAL_HOOK__) {
|
|
7
|
+
const version = window.__VUE__?.version || "unknown";
|
|
8
|
+
return `vue@${version}`;
|
|
9
|
+
}
|
|
10
|
+
const reactHook = window.__REACT_DEVTOOLS_GLOBAL_HOOK__;
|
|
11
|
+
if (reactHook || window.React || document.querySelector("[data-reactroot]")) {
|
|
12
|
+
const renderers = reactHook?.renderers;
|
|
13
|
+
const firstRenderer = renderers ? renderers.values().next().value : null;
|
|
14
|
+
const version = firstRenderer?.version || window.React?.version || "unknown";
|
|
15
|
+
return `react@${version}`;
|
|
16
|
+
}
|
|
17
|
+
if (window.__SVELTE__ ||
|
|
18
|
+
window.__svelte ||
|
|
19
|
+
window.__SVELTE_DEVTOOLS_GLOBAL_HOOK__) {
|
|
20
|
+
const version = window.__SVELTE__?.VERSION ||
|
|
21
|
+
window.__svelte?.version ||
|
|
22
|
+
"unknown";
|
|
23
|
+
return `svelte@${version}`;
|
|
24
|
+
}
|
|
25
|
+
return "unknown";
|
|
26
|
+
});
|
|
27
|
+
return {
|
|
28
|
+
detected,
|
|
29
|
+
framework: toBrowserFramework(detected),
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
export async function inspectVueTree(page, id) {
|
|
33
|
+
return id ? vueDevtools.getComponentDetails(page, id) : vueDevtools.getComponentTree(page);
|
|
34
|
+
}
|
|
35
|
+
export async function inspectVuePinia(page, store) {
|
|
36
|
+
return vueDevtools.getPiniaStores(page, store);
|
|
37
|
+
}
|
|
38
|
+
export async function inspectVueRouter(page) {
|
|
39
|
+
return vueDevtools.getRouterInfo(page);
|
|
40
|
+
}
|
|
41
|
+
export async function inspectReactTree(state, page, id) {
|
|
42
|
+
if (!id) {
|
|
43
|
+
state.lastReactSnapshot = await reactDevtools.snapshot(page);
|
|
44
|
+
return reactDevtools.format(state.lastReactSnapshot);
|
|
45
|
+
}
|
|
46
|
+
const parsed = Number.parseInt(id, 10);
|
|
47
|
+
if (!Number.isFinite(parsed))
|
|
48
|
+
throw new Error("react component id must be a number");
|
|
49
|
+
const inspected = await reactDevtools.inspect(page, parsed);
|
|
50
|
+
const lines = [];
|
|
51
|
+
const componentPath = reactDevtools.path(state.lastReactSnapshot, parsed);
|
|
52
|
+
if (componentPath)
|
|
53
|
+
lines.push(`path: ${componentPath}`);
|
|
54
|
+
lines.push(inspected.text);
|
|
55
|
+
if (inspected.source) {
|
|
56
|
+
const [file, line, column] = inspected.source;
|
|
57
|
+
lines.push(`source: ${file}:${line}:${column}`);
|
|
58
|
+
}
|
|
59
|
+
return lines.join("\n");
|
|
60
|
+
}
|
|
61
|
+
export async function inspectSvelteTree(page, id) {
|
|
62
|
+
return id ? svelteDevtools.getComponentDetails(page, id) : svelteDevtools.getComponentTree(page);
|
|
63
|
+
}
|
|
64
|
+
function toBrowserFramework(detected) {
|
|
65
|
+
if (detected.startsWith("vue"))
|
|
66
|
+
return "vue";
|
|
67
|
+
if (detected.startsWith("react"))
|
|
68
|
+
return "react";
|
|
69
|
+
if (detected.startsWith("svelte"))
|
|
70
|
+
return "svelte";
|
|
71
|
+
return "unknown";
|
|
72
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { type BrowserContext, type Page } from "playwright";
|
|
2
|
+
import type { ReactNode } from "./react/devtools.js";
|
|
3
|
+
export type BrowserFramework = "vue" | "react" | "svelte" | "unknown";
|
|
4
|
+
export type HmrEventType = "connecting" | "connected" | "update" | "full-reload" | "error" | "log";
|
|
5
|
+
export type HmrEvent = {
|
|
6
|
+
timestamp: number;
|
|
7
|
+
type: HmrEventType;
|
|
8
|
+
message: string;
|
|
9
|
+
path?: string;
|
|
10
|
+
};
|
|
11
|
+
export type BrowserSessionState = {
|
|
12
|
+
context: BrowserContext | null;
|
|
13
|
+
page: Page | null;
|
|
14
|
+
framework: BrowserFramework;
|
|
15
|
+
extensionModeDisabled: boolean;
|
|
16
|
+
consoleLogs: string[];
|
|
17
|
+
hmrEvents: HmrEvent[];
|
|
18
|
+
lastReactSnapshot: ReactNode[];
|
|
19
|
+
lastModuleGraphUrls: string[] | null;
|
|
20
|
+
};
|
|
21
|
+
export declare function createBrowserSessionState(): BrowserSessionState;
|
|
22
|
+
export declare function getCurrentPage(state: BrowserSessionState): Page | null;
|
|
23
|
+
export declare function resetBrowserSessionState(state: BrowserSessionState): void;
|
|
24
|
+
export declare function ensureBrowserPage(state: BrowserSessionState, onPageReady: (page: Page) => void): Promise<Page>;
|
|
25
|
+
export declare function closeBrowserSession(state: BrowserSessionState): Promise<void>;
|
|
26
|
+
export declare function contextUsable(current: Pick<BrowserContext, "pages"> | null): current is Pick<BrowserContext, "pages">;
|
|
27
|
+
export declare function isClosedTargetError(error: unknown): boolean;
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import { join, resolve } from "node:path";
|
|
3
|
+
import { chromium } from "playwright";
|
|
4
|
+
const extensionPath = process.env.REACT_DEVTOOLS_EXTENSION ??
|
|
5
|
+
resolve(import.meta.dirname, "../../next-browser/extensions/react-devtools-chrome");
|
|
6
|
+
const hasReactExtension = existsSync(join(extensionPath, "manifest.json"));
|
|
7
|
+
const installHook = hasReactExtension
|
|
8
|
+
? readFileSync(join(extensionPath, "build", "installHook.js"), "utf-8")
|
|
9
|
+
: null;
|
|
10
|
+
export function createBrowserSessionState() {
|
|
11
|
+
return {
|
|
12
|
+
context: null,
|
|
13
|
+
page: null,
|
|
14
|
+
framework: "unknown",
|
|
15
|
+
extensionModeDisabled: false,
|
|
16
|
+
consoleLogs: [],
|
|
17
|
+
hmrEvents: [],
|
|
18
|
+
lastReactSnapshot: [],
|
|
19
|
+
lastModuleGraphUrls: null,
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
export function getCurrentPage(state) {
|
|
23
|
+
if (!contextUsable(state.context))
|
|
24
|
+
return null;
|
|
25
|
+
if (!state.page || state.page.isClosed())
|
|
26
|
+
return null;
|
|
27
|
+
return state.page;
|
|
28
|
+
}
|
|
29
|
+
export function resetBrowserSessionState(state) {
|
|
30
|
+
state.context = null;
|
|
31
|
+
state.page = null;
|
|
32
|
+
state.framework = "unknown";
|
|
33
|
+
state.consoleLogs.length = 0;
|
|
34
|
+
state.hmrEvents.length = 0;
|
|
35
|
+
state.lastModuleGraphUrls = null;
|
|
36
|
+
state.lastReactSnapshot = [];
|
|
37
|
+
}
|
|
38
|
+
export async function ensureBrowserPage(state, onPageReady) {
|
|
39
|
+
if (!contextUsable(state.context)) {
|
|
40
|
+
await closeBrowserSession(state);
|
|
41
|
+
const launched = await launchBrowserContext(state.extensionModeDisabled);
|
|
42
|
+
state.context = launched.context;
|
|
43
|
+
state.extensionModeDisabled = launched.extensionModeDisabled;
|
|
44
|
+
}
|
|
45
|
+
if (!state.context)
|
|
46
|
+
throw new Error("browser not open");
|
|
47
|
+
if (!state.page || state.page.isClosed()) {
|
|
48
|
+
try {
|
|
49
|
+
state.page = state.context.pages()[0] ?? (await state.context.newPage());
|
|
50
|
+
}
|
|
51
|
+
catch (error) {
|
|
52
|
+
if (!isClosedTargetError(error))
|
|
53
|
+
throw error;
|
|
54
|
+
await closeBrowserSession(state);
|
|
55
|
+
state.extensionModeDisabled = true;
|
|
56
|
+
const launched = await launchBrowserContext(state.extensionModeDisabled);
|
|
57
|
+
state.context = launched.context;
|
|
58
|
+
state.extensionModeDisabled = launched.extensionModeDisabled;
|
|
59
|
+
state.page = state.context.pages()[0] ?? (await state.context.newPage());
|
|
60
|
+
}
|
|
61
|
+
onPageReady(state.page);
|
|
62
|
+
}
|
|
63
|
+
return state.page;
|
|
64
|
+
}
|
|
65
|
+
export async function closeBrowserSession(state) {
|
|
66
|
+
await state.context?.close();
|
|
67
|
+
resetBrowserSessionState(state);
|
|
68
|
+
}
|
|
69
|
+
export function contextUsable(current) {
|
|
70
|
+
if (!current)
|
|
71
|
+
return false;
|
|
72
|
+
try {
|
|
73
|
+
current.pages();
|
|
74
|
+
return true;
|
|
75
|
+
}
|
|
76
|
+
catch {
|
|
77
|
+
return false;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
export function isClosedTargetError(error) {
|
|
81
|
+
if (!(error instanceof Error))
|
|
82
|
+
return false;
|
|
83
|
+
return /Target page, context or browser has been closed/i.test(error.message);
|
|
84
|
+
}
|
|
85
|
+
async function launchBrowserContext(extensionModeDisabled) {
|
|
86
|
+
if (hasReactExtension && installHook && !extensionModeDisabled) {
|
|
87
|
+
try {
|
|
88
|
+
const context = await chromium.launchPersistentContext("", {
|
|
89
|
+
headless: false,
|
|
90
|
+
viewport: { width: 1280, height: 720 },
|
|
91
|
+
args: [
|
|
92
|
+
`--disable-extensions-except=${extensionPath}`,
|
|
93
|
+
`--load-extension=${extensionPath}`,
|
|
94
|
+
"--auto-open-devtools-for-tabs",
|
|
95
|
+
],
|
|
96
|
+
});
|
|
97
|
+
await context.waitForEvent("serviceworker").catch(() => { });
|
|
98
|
+
await context.addInitScript(installHook);
|
|
99
|
+
return { context, extensionModeDisabled };
|
|
100
|
+
}
|
|
101
|
+
catch {
|
|
102
|
+
extensionModeDisabled = true;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
const browser = await chromium.launch({
|
|
106
|
+
headless: false,
|
|
107
|
+
args: ["--auto-open-devtools-for-tabs"],
|
|
108
|
+
});
|
|
109
|
+
return {
|
|
110
|
+
context: await browser.newContext({ viewport: { width: 1280, height: 720 } }),
|
|
111
|
+
extensionModeDisabled,
|
|
112
|
+
};
|
|
113
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { Page } from "playwright";
|
|
2
|
+
import type { HmrEvent } from "./browser-session.js";
|
|
3
|
+
export type RuntimeSnapshot = {
|
|
4
|
+
url: string;
|
|
5
|
+
hasViteClient: boolean;
|
|
6
|
+
wsState: string;
|
|
7
|
+
hasErrorOverlay: boolean;
|
|
8
|
+
timestamp: number;
|
|
9
|
+
};
|
|
10
|
+
export type ModuleRow = {
|
|
11
|
+
url: string;
|
|
12
|
+
initiator: string;
|
|
13
|
+
durationMs: number;
|
|
14
|
+
};
|
|
15
|
+
export declare function normalizeLimit(limit: number, fallback: number, max: number): number;
|
|
16
|
+
export declare function formatRuntimeStatus(runtime: RuntimeSnapshot, currentFramework: string, events: HmrEvent[]): string;
|
|
17
|
+
export declare function formatHmrTrace(mode: "summary" | "trace", events: HmrEvent[], limit: number): string;
|
|
18
|
+
export declare function formatModuleGraphSnapshot(rows: ModuleRow[], filter?: string, limit?: number): string;
|
|
19
|
+
export declare function formatModuleGraphTrace(currentUrls: string[], previousUrls: Set<string> | null, filter?: string, limit?: number): string;
|
|
20
|
+
export declare function readRuntimeSnapshot(page: Page): Promise<RuntimeSnapshot>;
|
|
21
|
+
export declare function collectModuleRows(page: Page): Promise<ModuleRow[]>;
|
|
22
|
+
export declare function readOverlayError(page: Page): Promise<{
|
|
23
|
+
message?: string;
|
|
24
|
+
stack?: string;
|
|
25
|
+
} | null>;
|