@presto1314w/vite-devtools-browser 0.3.0 → 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.
@@ -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() {
@@ -8,13 +8,23 @@ export type HmrEvent = {
8
8
  message: string;
9
9
  path?: string;
10
10
  };
11
+ export type RuntimeError = {
12
+ timestamp: number;
13
+ message: string;
14
+ stack?: string;
15
+ source?: string | null;
16
+ line?: number | null;
17
+ col?: number | null;
18
+ };
11
19
  export type BrowserSessionState = {
12
20
  context: BrowserContext | null;
13
21
  page: Page | null;
14
22
  framework: BrowserFramework;
15
23
  extensionModeDisabled: boolean;
24
+ collectorInstalled: boolean;
16
25
  consoleLogs: string[];
17
26
  hmrEvents: HmrEvent[];
27
+ runtimeErrors: RuntimeError[];
18
28
  lastReactSnapshot: ReactNode[];
19
29
  lastModuleGraphUrls: string[] | null;
20
30
  };
@@ -13,8 +13,10 @@ export function createBrowserSessionState() {
13
13
  page: null,
14
14
  framework: "unknown",
15
15
  extensionModeDisabled: false,
16
+ collectorInstalled: false,
16
17
  consoleLogs: [],
17
18
  hmrEvents: [],
19
+ runtimeErrors: [],
18
20
  lastReactSnapshot: [],
19
21
  lastModuleGraphUrls: null,
20
22
  };
@@ -30,8 +32,10 @@ export function resetBrowserSessionState(state) {
30
32
  state.context = null;
31
33
  state.page = null;
32
34
  state.framework = "unknown";
35
+ state.collectorInstalled = false;
33
36
  state.consoleLogs.length = 0;
34
37
  state.hmrEvents.length = 0;
38
+ state.runtimeErrors.length = 0;
35
39
  state.lastModuleGraphUrls = null;
36
40
  state.lastReactSnapshot = [];
37
41
  }
package/dist/browser.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import type { Page } from "playwright";
2
- import { type HmrEvent } from "./browser-session.js";
2
+ import { type HmrEvent, type RuntimeError } from "./browser-session.js";
3
3
  import { resolveViaSourceMap } from "./sourcemap.js";
4
4
  import { EventQueue } from "./event-queue.js";
5
5
  export { contextUsable, isClosedTargetError, type HmrEvent } from "./browser-session.js";
@@ -19,6 +19,7 @@ export declare function cookies(cookies: {
19
19
  }[], domain: string): Promise<number>;
20
20
  export declare function close(): Promise<void>;
21
21
  export declare function recordConsoleMessage(logs: string[], events: HmrEvent[], type: string, message: string, maxLogs?: number, maxEvents?: number): void;
22
+ export declare function recordRuntimeError(runtimeErrors: RuntimeError[], message: string, stack?: string, source?: string | null, line?: number | null, col?: number | null, logType?: string, maxErrors?: number): void;
22
23
  export declare function parseViteLog(message: string): HmrEvent;
23
24
  export declare function goto(url: string): Promise<string>;
24
25
  export declare function back(): Promise<void>;
package/dist/browser.js CHANGED
@@ -12,6 +12,7 @@ const session = createBrowserSessionState();
12
12
  let eventQueue = null;
13
13
  const MAX_LOGS = 200;
14
14
  const MAX_HMR_EVENTS = 500;
15
+ const MAX_RUNTIME_ERRORS = 50;
15
16
  export function setEventQueue(queue) {
16
17
  eventQueue = queue;
17
18
  }
@@ -25,12 +26,17 @@ export function getCurrentPage() {
25
26
  * Inject browser-side event collector into the page
26
27
  */
27
28
  async function injectEventCollector(currentPage) {
29
+ if (session.context && !session.collectorInstalled) {
30
+ await session.context.addInitScript(initBrowserEventCollector);
31
+ session.collectorInstalled = true;
32
+ }
28
33
  await currentPage.evaluate(initBrowserEventCollector);
29
34
  }
30
35
  /**
31
36
  * Flush browser events into daemon event queue
32
37
  */
33
38
  export async function flushBrowserEvents(currentPage, queue) {
39
+ await currentPage.evaluate(initBrowserEventCollector);
34
40
  const raw = await currentPage.evaluate(readBrowserEvents);
35
41
  for (const e of raw) {
36
42
  queue.push(e);
@@ -60,7 +66,15 @@ async function ensurePage() {
60
66
  }
61
67
  function attachListeners(currentPage) {
62
68
  currentPage.on("console", (msg) => {
63
- recordConsoleMessage(session.consoleLogs, session.hmrEvents, msg.type(), msg.text());
69
+ const type = msg.type();
70
+ const message = msg.text();
71
+ recordConsoleMessage(session.consoleLogs, session.hmrEvents, type, message);
72
+ if (type === "error" || isVueUnhandledWarning(type, message)) {
73
+ recordRuntimeError(session.runtimeErrors, message, undefined, undefined, undefined, undefined);
74
+ }
75
+ });
76
+ currentPage.on("pageerror", (error) => {
77
+ recordRuntimeError(session.runtimeErrors, error.message, error.stack, undefined, undefined, undefined, "pageerror");
64
78
  });
65
79
  }
66
80
  export function recordConsoleMessage(logs, events, type, message, maxLogs = MAX_LOGS, maxEvents = MAX_HMR_EVENTS) {
@@ -75,6 +89,34 @@ export function recordConsoleMessage(logs, events, type, message, maxLogs = MAX_
75
89
  if (events.length > maxEvents)
76
90
  events.shift();
77
91
  }
92
+ export function recordRuntimeError(runtimeErrors, message, stack, source, line, col, logType = "runtime-error", maxErrors = MAX_RUNTIME_ERRORS) {
93
+ const error = {
94
+ timestamp: Date.now(),
95
+ message,
96
+ stack,
97
+ source,
98
+ line,
99
+ col,
100
+ };
101
+ runtimeErrors.push(error);
102
+ if (runtimeErrors.length > maxErrors)
103
+ runtimeErrors.shift();
104
+ eventQueue?.push({
105
+ timestamp: error.timestamp,
106
+ type: "error",
107
+ payload: {
108
+ message,
109
+ stack,
110
+ source,
111
+ line,
112
+ col,
113
+ },
114
+ });
115
+ const details = stack ? `${message}\n${stack}` : message;
116
+ session.consoleLogs.push(`[${logType}] ${details}`);
117
+ if (session.consoleLogs.length > MAX_LOGS)
118
+ session.consoleLogs.shift();
119
+ }
78
120
  export function parseViteLog(message) {
79
121
  const lower = message.toLowerCase();
80
122
  const event = {
@@ -198,15 +240,29 @@ export async function viteModuleGraph(filter, limit = 200, mode = "snapshot") {
198
240
  export async function errors(mapped = false, inlineSource = false) {
199
241
  const currentPage = requireCurrentPage();
200
242
  const errorInfo = await readOverlayError(currentPage);
201
- if (!errorInfo)
243
+ const runtimeError = session.runtimeErrors[session.runtimeErrors.length - 1];
244
+ if (!errorInfo && !runtimeError)
202
245
  return "no errors";
203
- const raw = `${errorInfo.message ?? "Vite error"}\n\n${errorInfo.stack ?? ""}`.trim();
246
+ const raw = errorInfo
247
+ ? `${errorInfo.message ?? "Vite error"}\n\n${errorInfo.stack ?? ""}`.trim()
248
+ : formatRuntimeError(runtimeError);
204
249
  if (!mapped)
205
250
  return raw;
206
251
  const origin = new URL(currentPage.url()).origin;
207
252
  const mappedStack = await mapStackTrace(raw, origin, inlineSource);
208
253
  return mappedStack;
209
254
  }
255
+ function formatRuntimeError(error) {
256
+ const location = error.source && error.line != null && error.col != null
257
+ ? `\n\nat ${error.source}:${error.line}:${error.col}`
258
+ : "";
259
+ return `${error.message}${location}${error.stack ? `\n\n${error.stack}` : ""}`.trim();
260
+ }
261
+ function isVueUnhandledWarning(type, message) {
262
+ if (type !== "warning")
263
+ return false;
264
+ return /\[Vue warn\]: Unhandled error during execution/i.test(message);
265
+ }
210
266
  export async function logs() {
211
267
  if (session.consoleLogs.length === 0)
212
268
  return "no logs";
@@ -220,6 +276,7 @@ export async function screenshot() {
220
276
  }
221
277
  export async function evaluate(script) {
222
278
  const currentPage = requireCurrentPage();
279
+ await currentPage.evaluate(initBrowserEventCollector);
223
280
  const result = await currentPage.evaluate(script);
224
281
  return JSON.stringify(result, null, 2);
225
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;
@@ -195,71 +205,101 @@ export function exit(io, res, msg) {
195
205
  io.exit(0);
196
206
  }
197
207
  export function printUsage() {
198
- return `
199
- vite-browser - Programmatic access to Vue/React/Svelte DevTools and Vite dev server
200
-
201
- USAGE
202
- vite-browser <command> [options]
203
-
204
- BROWSER CONTROL
205
- open <url> [--cookies-json <file>] Launch browser and navigate
206
- close Close browser and daemon
207
- goto <url> Full-page navigation
208
- back Go back in history
209
- reload Reload current page
210
-
211
- FRAMEWORK DETECTION
212
- detect Detect framework (vue/react/svelte)
213
-
214
- VUE COMMANDS
215
- vue tree [id] Show Vue component tree or inspect component
216
- vue pinia [store] Show Pinia stores or inspect specific store
217
- vue router Show Vue Router information
218
-
219
- REACT COMMANDS
220
- react tree [id] Show React component tree or inspect component
221
-
222
- SVELTE COMMANDS
223
- svelte tree [id] Show Svelte component tree or inspect component
224
-
225
- VITE COMMANDS
226
- vite restart Restart Vite dev server
227
- vite hmr Show HMR summary
228
- vite hmr trace [--limit <n>] Show HMR timeline
229
- vite hmr clear Clear tracked HMR timeline
230
- vite runtime Show Vite runtime status
231
- vite module-graph [--filter <txt>] [--limit <n>]
232
- Show loaded Vite module resources
233
- vite module-graph trace [--filter <txt>] [--limit <n>]
234
- Show module additions/removals since baseline
235
- vite module-graph clear Clear module-graph baseline
236
- errors Show build/runtime errors
237
- errors --mapped Show errors with source-map mapping
238
- errors --mapped --inline-source Include mapped source snippets
239
- correlate errors [--window <ms>] Correlate current errors with recent HMR events
240
- correlate renders [--window <ms>] Summarize recent render/update propagation evidence
241
- correlate errors --mapped Correlate mapped errors with recent HMR events
242
- diagnose hmr [--window <ms>] Diagnose HMR failures from runtime, errors, and trace data
243
- diagnose hmr [--limit <n>] Control how many recent HMR trace entries are inspected
244
- diagnose propagation [--window <ms>]
245
- Diagnose likely update -> render -> error propagation
246
- logs Show dev server logs
247
-
248
- UTILITIES
249
- screenshot Save screenshot to temp file
250
- eval <script> Evaluate JavaScript in page context
251
- network [idx] List network requests or inspect one
252
-
253
- OPTIONS
254
- -h, --help Show this help message
208
+ return `
209
+ vite-browser - Programmatic access to Vue/React/Svelte DevTools and Vite dev server
210
+
211
+ USAGE
212
+ vite-browser <command> [options]
213
+
214
+ BROWSER CONTROL
215
+ open <url> [--cookies-json <file>] Launch browser and navigate
216
+ close Close browser and daemon
217
+ goto <url> Full-page navigation
218
+ back Go back in history
219
+ reload Reload current page
220
+
221
+ FRAMEWORK DETECTION
222
+ detect Detect framework (vue/react/svelte)
223
+
224
+ VUE COMMANDS
225
+ vue tree [id] Show Vue component tree or inspect component
226
+ vue pinia [store] Show Pinia stores or inspect specific store
227
+ vue router Show Vue Router information
228
+
229
+ REACT COMMANDS
230
+ react tree [id] Show React component tree or inspect component
231
+
232
+ SVELTE COMMANDS
233
+ svelte tree [id] Show Svelte component tree or inspect component
234
+
235
+ VITE COMMANDS
236
+ vite restart Restart Vite dev server
237
+ vite hmr Show HMR summary
238
+ vite hmr trace [--limit <n>] Show HMR timeline
239
+ vite hmr clear Clear tracked HMR timeline
240
+ vite runtime Show Vite runtime status
241
+ vite module-graph [--filter <txt>] [--limit <n>]
242
+ Show loaded Vite module resources
243
+ vite module-graph trace [--filter <txt>] [--limit <n>]
244
+ Show module additions/removals since baseline
245
+ vite module-graph clear Clear module-graph baseline
246
+ errors Show build/runtime errors
247
+ errors --mapped Show errors with source-map mapping
248
+ errors --mapped --inline-source Include mapped source snippets
249
+ correlate errors [--window <ms>] Correlate current errors with recent HMR events
250
+ correlate renders [--window <ms>] Summarize recent render/update propagation evidence
251
+ correlate errors --mapped Correlate mapped errors with recent HMR events
252
+ diagnose hmr [--window <ms>] Diagnose HMR failures from runtime, errors, and trace data
253
+ diagnose hmr [--limit <n>] Control how many recent HMR trace entries are inspected
254
+ diagnose propagation [--window <ms>]
255
+ Diagnose likely update -> render -> error propagation
256
+ logs Show dev server logs
257
+
258
+ UTILITIES
259
+ screenshot Save screenshot to temp file
260
+ eval <script> Evaluate JavaScript in page context
261
+ network [idx] List network requests or inspect one
262
+
263
+ OPTIONS
264
+ -h, --help Show this help message
255
265
  `;
256
266
  }
257
- if (process.argv[1] && import.meta.url.endsWith(process.argv[1].replaceAll("\\", "/"))) {
258
- await runCli(process.argv, {
259
- send,
260
- readFile: readFileSync,
261
- stdout: (text) => console.log(text),
262
- stderr: (text) => console.error(text),
263
- exit: (code) => process.exit(code),
267
+ function isEntrypoint(argv1) {
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
+ }
278
+ }
279
+ async function main() {
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
+ }
298
+ }
299
+ if (isEntrypoint(process.argv[1])) {
300
+ void main().catch((error) => {
301
+ const message = error instanceof Error ? error.message : String(error);
302
+ console.error(message);
303
+ process.exit(1);
264
304
  });
265
305
  }
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
  }