@presto1314w/vite-devtools-browser 0.3.1 → 0.3.2

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 CHANGED
@@ -271,12 +271,13 @@ vite-browser eval <script>
271
271
 
272
272
  ## Current Boundaries
273
273
 
274
- `v0.3.1` is strong at:
274
+ `v0.3.2` is strong at:
275
275
  - surfacing runtime state as structured shell output
276
276
  - linking current errors to recent HMR and module activity
277
277
  - detecting common HMR failure patterns with confidence levels
278
278
  - narrowing likely store/module -> render paths in Vue-first flows
279
279
  - capturing browser-side runtime errors even when the Vite overlay is absent
280
+ - turning sparse live Vue/Pinia repro signals into actionable `store -> render -> error` guidance more reliably
280
281
 
281
282
  `correlate renders` and `diagnose propagation` are **high-confidence propagation clues**, not strict causal proof. They do not reliably trace deep chains like `store -> component A -> component B -> error` across arbitrary graphs, and intentionally fall back to conservative output when evidence is incomplete.
282
283
 
@@ -1,13 +1,15 @@
1
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
- };
2
+ if (!window.__vb_events) {
3
+ window.__vb_events = [];
4
+ }
5
+ if (!window.__vb_push) {
6
+ window.__vb_push = (event) => {
7
+ const q = window.__vb_events;
8
+ q.push(event);
9
+ if (q.length > 1000)
10
+ q.shift();
11
+ };
12
+ }
11
13
  const inferFramework = () => {
12
14
  if (window.__VUE__ || window.__VUE_DEVTOOLS_GLOBAL_HOOK__)
13
15
  return "vue";
@@ -117,15 +119,16 @@ export function initBrowserEventCollector() {
117
119
  }, 60);
118
120
  };
119
121
  const attachPiniaSubscriptions = () => {
120
- if (window.__vb_pinia_attached)
121
- return;
122
122
  const hook = window.__VUE_DEVTOOLS_GLOBAL_HOOK__;
123
123
  const app = Array.isArray(hook?.apps) ? hook.apps[0] : null;
124
124
  const pinia = window.__PINIA__ || window.pinia || app?.config?.globalProperties?.$pinia;
125
125
  const registry = pinia?._s;
126
126
  if (!(registry instanceof Map) || registry.size === 0)
127
127
  return;
128
- const attached = (window.__vb_pinia_attached = new Set());
128
+ const attached = window.__vb_pinia_attached ||
129
+ (window.__vb_pinia_attached = new Set());
130
+ const actionAttached = window.__vb_pinia_action_attached ||
131
+ (window.__vb_pinia_action_attached = new Set());
129
132
  registry.forEach((store, storeId) => {
130
133
  if (!store || typeof store.$subscribe !== "function" || attached.has(String(storeId)))
131
134
  return;
@@ -161,10 +164,33 @@ export function initBrowserEventCollector() {
161
164
  },
162
165
  });
163
166
  scheduleRender("store-update", { changedKeys });
164
- }, { detached: true });
167
+ },
168
+ // Flush synchronously so the store event is recorded before a render-time failure
169
+ // can short-circuit the rest of Vue's update cycle.
170
+ { detached: true, flush: "sync" });
171
+ if (typeof store.$onAction === "function" && !actionAttached.has(String(storeId))) {
172
+ actionAttached.add(String(storeId));
173
+ store.$onAction(({ name, after }) => {
174
+ after(() => {
175
+ window.__vb_push({
176
+ timestamp: Date.now(),
177
+ type: "store-update",
178
+ payload: {
179
+ store: String(storeId),
180
+ mutationType: typeof name === "string" && name.length > 0 ? `action:${name}` : "action",
181
+ events: 0,
182
+ changedKeys: [],
183
+ },
184
+ });
185
+ scheduleRender("store-update");
186
+ });
187
+ }, true);
188
+ }
165
189
  });
166
190
  };
167
191
  function attachViteListener() {
192
+ if (window.__vb_vite_listener_attached)
193
+ return true;
168
194
  const hot = window.__vite_hot;
169
195
  if (hot?.ws) {
170
196
  hot.ws.addEventListener("message", (e) => {
@@ -181,6 +207,7 @@ export function initBrowserEventCollector() {
181
207
  }
182
208
  catch { }
183
209
  });
210
+ window.__vb_vite_listener_attached = true;
184
211
  return true;
185
212
  }
186
213
  return false;
@@ -194,22 +221,28 @@ export function initBrowserEventCollector() {
194
221
  }
195
222
  }, 100);
196
223
  }
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 },
224
+ if (!window.__vb_onerror_attached) {
225
+ const origOnError = window.onerror;
226
+ window.onerror = (msg, src, line, col, err) => {
227
+ window.__vb_push({
228
+ timestamp: Date.now(),
229
+ type: "error",
230
+ payload: { message: String(msg), source: src, line, col, stack: err?.stack },
231
+ });
232
+ return origOnError ? origOnError(msg, src, line, col, err) : false;
233
+ };
234
+ window.__vb_onerror_attached = true;
235
+ }
236
+ if (!window.__vb_unhandledrejection_attached) {
237
+ window.addEventListener("unhandledrejection", (e) => {
238
+ window.__vb_push({
239
+ timestamp: Date.now(),
240
+ type: "error",
241
+ payload: { message: e.reason?.message, stack: e.reason?.stack },
242
+ });
211
243
  });
212
- });
244
+ window.__vb_unhandledrejection_attached = true;
245
+ }
213
246
  const observeDom = () => {
214
247
  const root = document.body || document.documentElement;
215
248
  if (!root || window.__vb_render_observer)
@@ -246,7 +279,9 @@ export function initBrowserEventCollector() {
246
279
  observeDom();
247
280
  patchHistory();
248
281
  attachPiniaSubscriptions();
249
- window.setInterval(attachPiniaSubscriptions, 1000);
282
+ if (!window.__vb_pinia_retry_timer) {
283
+ window.__vb_pinia_retry_timer = window.setInterval(attachPiniaSubscriptions, 250);
284
+ }
250
285
  scheduleRender("initial-load");
251
286
  }
252
287
  export function readBrowserEvents() {
package/dist/browser.js CHANGED
@@ -36,6 +36,7 @@ async function injectEventCollector(currentPage) {
36
36
  * Flush browser events into daemon event queue
37
37
  */
38
38
  export async function flushBrowserEvents(currentPage, queue) {
39
+ await currentPage.evaluate(initBrowserEventCollector);
39
40
  const raw = await currentPage.evaluate(readBrowserEvents);
40
41
  for (const e of raw) {
41
42
  queue.push(e);
@@ -275,6 +276,7 @@ export async function screenshot() {
275
276
  }
276
277
  export async function evaluate(script) {
277
278
  const currentPage = requireCurrentPage();
279
+ await currentPage.evaluate(initBrowserEventCollector);
278
280
  const result = await currentPage.evaluate(script);
279
281
  return JSON.stringify(result, null, 2);
280
282
  }
package/dist/cli.js CHANGED
@@ -1,6 +1,16 @@
1
1
  #!/usr/bin/env node
2
2
  import { readFileSync } from "node:fs";
3
+ import { realpathSync } from "node:fs";
4
+ import { resolve } from "node:path";
5
+ import { fileURLToPath } from "node:url";
3
6
  import { send } from "./client.js";
7
+ class CliExit extends Error {
8
+ code;
9
+ constructor(code) {
10
+ super(`CLI_EXIT:${code}`);
11
+ this.code = code;
12
+ }
13
+ }
4
14
  export function normalizeUrl(value) {
5
15
  if (value.includes("://"))
6
16
  return value;
@@ -255,16 +265,36 @@ OPTIONS
255
265
  `;
256
266
  }
257
267
  function isEntrypoint(argv1) {
258
- return Boolean(argv1 && import.meta.url.endsWith(argv1.replaceAll("\\", "/")));
268
+ if (!argv1)
269
+ return false;
270
+ try {
271
+ const current = realpathSync(fileURLToPath(import.meta.url));
272
+ const target = realpathSync(resolve(argv1));
273
+ return current === target;
274
+ }
275
+ catch {
276
+ return Boolean(argv1 && import.meta.url.endsWith(argv1.replaceAll("\\", "/")));
277
+ }
259
278
  }
260
279
  async function main() {
261
- await runCli(process.argv, {
262
- send,
263
- readFile: readFileSync,
264
- stdout: (text) => console.log(text),
265
- stderr: (text) => console.error(text),
266
- exit: (code) => process.exit(code),
267
- });
280
+ try {
281
+ await runCli(process.argv, {
282
+ send,
283
+ readFile: readFileSync,
284
+ stdout: (text) => process.stdout.write(`${text}\n`),
285
+ stderr: (text) => process.stderr.write(`${text}\n`),
286
+ exit: ((code) => {
287
+ throw new CliExit(code);
288
+ }),
289
+ });
290
+ }
291
+ catch (error) {
292
+ if (error instanceof CliExit) {
293
+ process.exitCode = error.code;
294
+ return;
295
+ }
296
+ throw error;
297
+ }
268
298
  }
269
299
  if (isEntrypoint(process.argv[1])) {
270
300
  void main().catch((error) => {
package/dist/daemon.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import { createServer } from "node:net";
2
2
  import { mkdirSync, writeFileSync, rmSync } from "node:fs";
3
+ import { setTimeout as sleep } from "node:timers/promises";
3
4
  import { fileURLToPath } from "node:url";
4
5
  import * as browser from "./browser.js";
5
6
  import { correlateErrorWithHMR, formatErrorCorrelationReport } from "./correlate.js";
@@ -18,19 +19,8 @@ export function cleanError(err) {
18
19
  }
19
20
  export function createRunner(api = browser) {
20
21
  return async function run(cmd) {
21
- // Flush browser events to daemon queue before processing command
22
22
  const queue = api.getEventQueue();
23
- if (queue) {
24
- try {
25
- const currentPage = api.getCurrentPage();
26
- if (currentPage) {
27
- await api.flushBrowserEvents(currentPage, queue);
28
- }
29
- }
30
- catch {
31
- // Ignore flush errors (page might not be open yet)
32
- }
33
- }
23
+ await flushCurrentPageEvents(api, queue);
34
24
  // Browser control
35
25
  if (cmd.action === "open") {
36
26
  await api.open(cmd.url);
@@ -114,8 +104,8 @@ export function createRunner(api = browser) {
114
104
  return { ok: true, data };
115
105
  }
116
106
  if (cmd.action === "correlate-renders") {
117
- const events = queue ? queue.window(cmd.windowMs ?? 5000) : [];
118
- const data = formatPropagationTraceReport(correlateRenderPropagation(events));
107
+ const events = await getSettledEventWindow(api, queue, cmd.windowMs ?? 5000);
108
+ const data = formatPropagationTraceReport(await buildPropagationTrace(api, events));
119
109
  return { ok: true, data };
120
110
  }
121
111
  if (cmd.action === "diagnose-hmr") {
@@ -128,8 +118,8 @@ export function createRunner(api = browser) {
128
118
  return { ok: true, data };
129
119
  }
130
120
  if (cmd.action === "diagnose-propagation") {
131
- const events = queue ? queue.window(cmd.windowMs ?? 5000) : [];
132
- const data = formatPropagationDiagnosisReport(diagnosePropagation(correlateRenderPropagation(events)));
121
+ const events = await getSettledEventWindow(api, queue, cmd.windowMs ?? 5000);
122
+ const data = formatPropagationDiagnosisReport(diagnosePropagation(await buildPropagationTrace(api, events)));
133
123
  return { ok: true, data };
134
124
  }
135
125
  if (cmd.action === "logs") {
@@ -143,6 +133,10 @@ export function createRunner(api = browser) {
143
133
  }
144
134
  if (cmd.action === "eval") {
145
135
  const data = await api.evaluate(cmd.script);
136
+ await settleCurrentPage(api, 120);
137
+ await flushCurrentPageEvents(api, queue);
138
+ await settleCurrentPage(api, 180);
139
+ await flushCurrentPageEvents(api, queue);
146
140
  return { ok: true, data };
147
141
  }
148
142
  if (cmd.action === "network") {
@@ -152,6 +146,64 @@ export function createRunner(api = browser) {
152
146
  return { ok: false, error: `unknown action: ${cmd.action}` };
153
147
  };
154
148
  }
149
+ async function buildPropagationTrace(api, events) {
150
+ const trace = correlateRenderPropagation(events);
151
+ if (!trace)
152
+ return null;
153
+ if (trace.errorMessages.length > 0)
154
+ return trace;
155
+ const currentError = String(await api.errors(false, false));
156
+ if (!currentError || currentError === "no errors")
157
+ return trace;
158
+ return {
159
+ ...trace,
160
+ errorMessages: [currentError, ...trace.errorMessages],
161
+ };
162
+ }
163
+ async function flushCurrentPageEvents(api, queue) {
164
+ if (!queue)
165
+ return;
166
+ try {
167
+ const currentPage = api.getCurrentPage();
168
+ if (currentPage) {
169
+ await api.flushBrowserEvents(currentPage, queue);
170
+ }
171
+ }
172
+ catch {
173
+ // Ignore flush errors (page might not be open yet)
174
+ }
175
+ }
176
+ async function getSettledEventWindow(api, queue, windowMs) {
177
+ if (!queue)
178
+ return [];
179
+ let events = queue.window(windowMs);
180
+ if (hasPropagationSignals(events))
181
+ return events;
182
+ for (const delayMs of [120, 300]) {
183
+ await settleCurrentPage(api, delayMs);
184
+ await flushCurrentPageEvents(api, queue);
185
+ events = queue.window(windowMs);
186
+ if (hasPropagationSignals(events))
187
+ return events;
188
+ }
189
+ return events;
190
+ }
191
+ function hasPropagationSignals(events) {
192
+ return events.some((event) => event.type === "render" || event.type === "store-update");
193
+ }
194
+ async function settleCurrentPage(api, delayMs) {
195
+ const currentPage = api.getCurrentPage();
196
+ if (!currentPage) {
197
+ await sleep(delayMs);
198
+ return;
199
+ }
200
+ try {
201
+ await currentPage.waitForTimeout(delayMs);
202
+ }
203
+ catch {
204
+ await sleep(delayMs);
205
+ }
206
+ }
155
207
  export async function dispatchLine(line, socket, run = createRunner(), onClose) {
156
208
  let cmd;
157
209
  try {
@@ -27,6 +27,8 @@ export declare function getRenderLabel(payload: RenderEventPayload): string;
27
27
  export declare function getStoreName(payload: StoreUpdatePayload): string | null;
28
28
  export declare function getChangedKeys(payload: StoreUpdatePayload): string[];
29
29
  export declare function getStoreHints(payload: RenderEventPayload): string[];
30
+ export declare function getRenderReason(payload: RenderEventPayload): string | null;
31
+ export declare function getRenderChangedKeys(payload: RenderEventPayload): string[];
30
32
  export declare function getNetworkUrl(payload: NetworkEventPayload): string | null;
31
33
  export declare function getErrorMessage(payload: ErrorEventPayload): string | null;
32
34
  export declare function uniqueStrings(values: string[]): string[];
@@ -59,6 +59,12 @@ export function getChangedKeys(payload) {
59
59
  export function getStoreHints(payload) {
60
60
  return payload.storeHints.filter((value) => value.length > 0);
61
61
  }
62
+ export function getRenderReason(payload) {
63
+ return typeof payload.reason === "string" && payload.reason.length > 0 ? payload.reason : null;
64
+ }
65
+ export function getRenderChangedKeys(payload) {
66
+ return payload.changedKeys.filter((value) => value.length > 0);
67
+ }
62
68
  export function getNetworkUrl(payload) {
63
69
  return payload.url.length > 0 ? payload.url : null;
64
70
  }
package/dist/trace.js CHANGED
@@ -1,4 +1,4 @@
1
- import { extractModulesFromHmrEvent, getChangedKeys, getErrorEvents, getErrorMessage, getHmrEvents, getNetworkEvents, getNetworkUrl, getRenderEvents, getRenderLabel, getStoreHints, getStoreName, getStoreUpdateEvents, sortEventsChronologically, uniqueStrings, } from "./event-analysis.js";
1
+ import { extractModulesFromHmrEvent, getChangedKeys, getErrorEvents, getErrorMessage, getHmrEvents, getNetworkEvents, getNetworkUrl, getRenderChangedKeys, getRenderEvents, getRenderLabel, getRenderReason, getStoreHints, getStoreName, getStoreUpdateEvents, sortEventsChronologically, uniqueStrings, } from "./event-analysis.js";
2
2
  export function correlateRenderPropagation(events) {
3
3
  const recent = sortEventsChronologically(events);
4
4
  const renderEvents = getRenderEvents(recent);
@@ -9,10 +9,24 @@ export function correlateRenderPropagation(events) {
9
9
  const networkEvents = getNetworkEvents(recent);
10
10
  const errorEvents = getErrorEvents(recent);
11
11
  const sourceModules = uniqueStrings(hmrEvents.flatMap(extractModulesFromHmrEvent));
12
- const storeUpdates = uniqueStrings(storeEvents.map((event) => getStoreName(event.payload)).filter((value) => value != null));
13
- const changedKeys = uniqueStrings(storeEvents.flatMap((event) => getChangedKeys(event.payload)));
14
- const renderComponents = uniqueStrings(renderEvents.map((event) => getRenderLabel(event.payload)).filter(Boolean));
12
+ const explicitStoreUpdates = uniqueStrings(storeEvents.map((event) => getStoreName(event.payload)).filter((value) => value != null));
15
13
  const storeHints = uniqueStrings(renderEvents.flatMap((event) => getStoreHints(event.payload)));
14
+ const inferredStoreUpdates = explicitStoreUpdates.length > 0
15
+ ? explicitStoreUpdates
16
+ : uniqueStrings(renderEvents
17
+ .filter((event) => getRenderReason(event.payload) === "store-update")
18
+ .flatMap((event) => getStoreHints(event.payload)));
19
+ const storeUpdates = inferredStoreUpdates.length > 0
20
+ ? inferredStoreUpdates
21
+ : storeHints.length === 1
22
+ ? storeHints
23
+ : [];
24
+ const changedKeys = uniqueStrings(storeEvents.length > 0
25
+ ? storeEvents.flatMap((event) => getChangedKeys(event.payload))
26
+ : renderEvents
27
+ .filter((event) => getRenderReason(event.payload) === "store-update")
28
+ .flatMap((event) => getRenderChangedKeys(event.payload)));
29
+ const renderComponents = uniqueStrings(renderEvents.map((event) => getRenderLabel(event.payload)).filter(Boolean));
16
30
  const networkUrls = uniqueStrings(networkEvents.map((event) => getNetworkUrl(event.payload)).filter((value) => value != null));
17
31
  const errorMessages = uniqueStrings(errorEvents.map((event) => getErrorMessage(event.payload)).filter((value) => value != null));
18
32
  const confidence = inferConfidence(sourceModules, storeUpdates, renderComponents, errorMessages);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@presto1314w/vite-devtools-browser",
3
- "version": "0.3.1",
3
+ "version": "0.3.2",
4
4
  "description": "Runtime diagnostics CLI for Vite apps with event-stream correlation, HMR diagnosis, framework inspection, and mapped errors",
5
5
  "license": "MIT",
6
6
  "keywords": [