@presto1314w/vite-devtools-browser 0.2.2 → 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 +42 -14
- 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/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,22 +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
|
-
- `correlate errors` links the current error to recent HMR-updated modules
|
|
28
|
-
- `diagnose hmr` turns runtime, trace, and error signals into structured findings
|
|
29
|
-
- skills and CLI flows now route more directly to runtime triage instead of raw log inspection
|
|
27
|
+
It keeps the `v0.2.x` runtime diagnosis model, then adds a new layer of propagation-oriented reasoning:
|
|
30
28
|
|
|
31
|
-
`
|
|
32
|
-
|
|
33
|
-
-
|
|
34
|
-
-
|
|
35
|
-
-
|
|
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
|
|
36
34
|
|
|
37
35
|
## Built For Agents
|
|
38
36
|
|
|
@@ -51,6 +49,7 @@ Most browser CLIs are optimized for automation. Most framework devtools are opti
|
|
|
51
49
|
- it can inspect framework state like a devtools bridge
|
|
52
50
|
- it can explain Vite-specific behavior like HMR updates and module graph changes
|
|
53
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
|
|
54
53
|
- it returns structured text that AI agents can consume directly in loops
|
|
55
54
|
|
|
56
55
|
## Positioning
|
|
@@ -90,6 +89,8 @@ vite-browser detect
|
|
|
90
89
|
vite-browser vite runtime
|
|
91
90
|
vite-browser errors --mapped --inline-source
|
|
92
91
|
vite-browser correlate errors --mapped --window 5000
|
|
92
|
+
vite-browser correlate renders --window 5000
|
|
93
|
+
vite-browser diagnose propagation --window 5000
|
|
93
94
|
vite-browser diagnose hmr --limit 50
|
|
94
95
|
vite-browser vite hmr trace --limit 20
|
|
95
96
|
vite-browser vite module-graph trace --limit 50
|
|
@@ -141,6 +142,26 @@ Confidence: high
|
|
|
141
142
|
HMR update observed within 5000ms of the current error
|
|
142
143
|
Matching modules: /src/App.tsx
|
|
143
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
|
+
|
|
144
165
|
$ vite-browser diagnose hmr --limit 50
|
|
145
166
|
# HMR Diagnosis
|
|
146
167
|
## missing-module
|
|
@@ -161,6 +182,8 @@ Suggestion: Verify the import path, file extension, alias configuration, and whe
|
|
|
161
182
|
- HMR summary/timeline/clear
|
|
162
183
|
- module-graph snapshot/diff/clear
|
|
163
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
|
|
164
187
|
- rule-based HMR diagnosis with confidence levels
|
|
165
188
|
- source-mapped errors with optional inline source snippet
|
|
166
189
|
- Debug utilities: console logs, network tracing, screenshot, page `eval`
|
|
@@ -173,6 +196,8 @@ Suggestion: Verify the import path, file extension, alias configuration, and whe
|
|
|
173
196
|
vite-browser vite runtime
|
|
174
197
|
vite-browser errors --mapped --inline-source
|
|
175
198
|
vite-browser correlate errors --mapped --window 5000
|
|
199
|
+
vite-browser correlate renders --window 5000
|
|
200
|
+
vite-browser diagnose propagation --window 5000
|
|
176
201
|
vite-browser diagnose hmr --limit 50
|
|
177
202
|
vite-browser vite hmr trace --limit 50
|
|
178
203
|
vite-browser vite module-graph trace --limit 200
|
|
@@ -201,13 +226,14 @@ vite-browser svelte tree
|
|
|
201
226
|
|
|
202
227
|
## Current Boundaries
|
|
203
228
|
|
|
204
|
-
`vite-browser` v0.
|
|
229
|
+
`vite-browser` v0.3.0 is strong at:
|
|
205
230
|
|
|
206
231
|
- surfacing runtime state as structured shell output
|
|
207
232
|
- linking current errors to recent HMR/module activity
|
|
208
233
|
- detecting several common HMR failure patterns quickly
|
|
234
|
+
- narrowing likely store/module -> render paths in Vue-first flows
|
|
209
235
|
|
|
210
|
-
|
|
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.
|
|
211
237
|
|
|
212
238
|
## Command Reference
|
|
213
239
|
|
|
@@ -247,8 +273,10 @@ vite-browser errors
|
|
|
247
273
|
vite-browser errors --mapped
|
|
248
274
|
vite-browser errors --mapped --inline-source
|
|
249
275
|
vite-browser correlate errors [--window <ms>]
|
|
276
|
+
vite-browser correlate renders [--window <ms>]
|
|
250
277
|
vite-browser correlate errors --mapped --inline-source
|
|
251
278
|
vite-browser diagnose hmr [--window <ms>] [--limit <n>]
|
|
279
|
+
vite-browser diagnose propagation [--window <ms>]
|
|
252
280
|
```
|
|
253
281
|
|
|
254
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>;
|