@presto1314w/vite-devtools-browser 0.3.3 → 0.3.6

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.
@@ -0,0 +1,195 @@
1
+ /**
2
+ * React commit tracking groundwork.
3
+ *
4
+ * This module records real commit metadata that can be observed from the
5
+ * React DevTools hook without pretending to expose a full Profiler surface.
6
+ */
7
+ export async function installRenderTracking(page) {
8
+ await page.evaluate(inPageInstallRenderTracking);
9
+ }
10
+ export async function getRecentRenders(page, limit = 50) {
11
+ return page.evaluate(inPageGetRecentRenders, limit);
12
+ }
13
+ export async function getRenderTriggers(page, limit = 50) {
14
+ return page.evaluate(inPageGetRenderTriggers, limit);
15
+ }
16
+ export async function clearRenderHistory(page) {
17
+ await page.evaluate(inPageClearRenderHistory);
18
+ }
19
+ export function formatDuration(duration) {
20
+ return duration == null ? "n/a" : `${duration.toFixed(2)}ms`;
21
+ }
22
+ export function formatRenderInfo(renders) {
23
+ if (renders.length === 0)
24
+ return "No renders recorded";
25
+ const lines = ["# React Commits\n"];
26
+ for (const render of renders) {
27
+ const phase = render.phase === "mount" ? "MOUNT" : render.phase === "update" ? "UPDATE" : "NESTED";
28
+ const duration = formatDuration(render.actualDuration);
29
+ const slow = render.actualDuration != null && render.actualDuration > 16 ? " ⚠️ SLOW" : "";
30
+ lines.push(`[${phase}] ${render.rootName} (${duration})${slow}`);
31
+ lines.push(` Fibers: ${render.fiberCount}`);
32
+ if (render.baseDuration != null) {
33
+ lines.push(` Base duration: ${render.baseDuration.toFixed(2)}ms`);
34
+ }
35
+ if (render.interactions.length > 0) {
36
+ const interactions = render.interactions.map((i) => i.name).join(", ");
37
+ lines.push(` Interactions: ${interactions}`);
38
+ }
39
+ }
40
+ return lines.join("\n");
41
+ }
42
+ export function formatRenderTriggers(triggers) {
43
+ if (triggers.length === 0)
44
+ return "No render triggers recorded";
45
+ const lines = ["# Render Triggers\n"];
46
+ for (const trigger of triggers) {
47
+ const reason = trigger.reason.toUpperCase();
48
+ const details = trigger.details ? ` - ${trigger.details}` : "";
49
+ lines.push(`[${reason}] ${trigger.rootName}${details}`);
50
+ }
51
+ return lines.join("\n");
52
+ }
53
+ export function analyzeSlowRenders(renders) {
54
+ const slowRenders = renders.filter((r) => r.actualDuration != null && r.actualDuration > 16);
55
+ if (slowRenders.length === 0) {
56
+ return "No slow renders detected with measurable duration (> 16ms)";
57
+ }
58
+ const lines = [
59
+ "# Slow Renders Analysis\n",
60
+ `Found ${slowRenders.length} slow render(s) (> 16ms)\n`,
61
+ ];
62
+ const byRoot = new Map();
63
+ for (const render of slowRenders) {
64
+ const list = byRoot.get(render.rootName) || [];
65
+ list.push(render);
66
+ byRoot.set(render.rootName, list);
67
+ }
68
+ const sorted = Array.from(byRoot.entries()).sort(([, a], [, b]) => {
69
+ const totalA = a.reduce((sum, r) => sum + (r.actualDuration ?? 0), 0);
70
+ const totalB = b.reduce((sum, r) => sum + (r.actualDuration ?? 0), 0);
71
+ return totalB - totalA;
72
+ });
73
+ for (const [name, commits] of sorted) {
74
+ const durations = commits.map((r) => r.actualDuration ?? 0);
75
+ const total = durations.reduce((sum, value) => sum + value, 0);
76
+ const count = commits.length;
77
+ const avg = total / count;
78
+ const max = Math.max(...durations);
79
+ lines.push(`${name}:`);
80
+ lines.push(` Count: ${count}`);
81
+ lines.push(` Total: ${total.toFixed(2)}ms`);
82
+ lines.push(` Average: ${avg.toFixed(2)}ms`);
83
+ lines.push(` Max: ${max.toFixed(2)}ms`);
84
+ lines.push("");
85
+ }
86
+ return lines.join("\n");
87
+ }
88
+ function inPageInstallRenderTracking() {
89
+ const win = window;
90
+ win.__REACT_RENDER_HISTORY__ = win.__REACT_RENDER_HISTORY__ || [];
91
+ win.__REACT_RENDER_TRIGGERS__ = win.__REACT_RENDER_TRIGGERS__ || [];
92
+ const hook = win.__REACT_DEVTOOLS_GLOBAL_HOOK__;
93
+ if (!hook) {
94
+ console.warn("React DevTools hook not found, render tracking limited");
95
+ return;
96
+ }
97
+ if (win.__REACT_RENDER_TRACKING_INSTALLED__)
98
+ return;
99
+ win.__REACT_RENDER_TRACKING_INSTALLED__ = true;
100
+ const originalOnCommitFiberRoot = hook.onCommitFiberRoot;
101
+ hook.onCommitFiberRoot = function (rendererId, root, priorityLevel) {
102
+ try {
103
+ const renderInfo = toRenderInfo(rendererId, root);
104
+ win.__REACT_RENDER_HISTORY__.push(renderInfo);
105
+ if (win.__REACT_RENDER_HISTORY__.length > 100) {
106
+ win.__REACT_RENDER_HISTORY__.shift();
107
+ }
108
+ }
109
+ catch {
110
+ // Ignore tracking errors to avoid breaking the inspected app.
111
+ }
112
+ if (originalOnCommitFiberRoot) {
113
+ return originalOnCommitFiberRoot.call(this, rendererId, root, priorityLevel);
114
+ }
115
+ };
116
+ }
117
+ function inPageGetRecentRenders(limit) {
118
+ const win = window;
119
+ const history = win.__REACT_RENDER_HISTORY__ || [];
120
+ return history.slice(-limit);
121
+ }
122
+ function inPageGetRenderTriggers(limit) {
123
+ const win = window;
124
+ const triggers = win.__REACT_RENDER_TRIGGERS__ || [];
125
+ return triggers.slice(-limit);
126
+ }
127
+ function inPageClearRenderHistory() {
128
+ const win = window;
129
+ win.__REACT_RENDER_HISTORY__ = [];
130
+ win.__REACT_RENDER_TRIGGERS__ = [];
131
+ }
132
+ function toRenderInfo(rendererId, root) {
133
+ const current = root?.current ?? null;
134
+ const rootChild = current?.child ?? null;
135
+ return {
136
+ rendererId,
137
+ rootName: getFiberName(rootChild || current),
138
+ phase: inferPhase(current),
139
+ actualDuration: numberOrNull(current?.actualDuration),
140
+ baseDuration: numberOrNull(current?.treeBaseDuration ?? current?.baseDuration),
141
+ startTime: numberOrNull(current?.actualStartTime),
142
+ commitTime: Date.now(),
143
+ fiberCount: countFibers(current),
144
+ interactions: normalizeInteractions(root?.memoizedInteractions),
145
+ };
146
+ }
147
+ function inferPhase(current) {
148
+ if (!current?.alternate)
149
+ return "mount";
150
+ return current.flags && (current.flags & 1024) !== 0 ? "nested-update" : "update";
151
+ }
152
+ function getFiberName(fiber) {
153
+ if (!fiber)
154
+ return "Root";
155
+ if (typeof fiber.type === "string")
156
+ return fiber.type;
157
+ if (fiber.type?.displayName)
158
+ return fiber.type.displayName;
159
+ if (fiber.type?.name)
160
+ return fiber.type.name;
161
+ if (fiber.elementType?.displayName)
162
+ return fiber.elementType.displayName;
163
+ if (fiber.elementType?.name)
164
+ return fiber.elementType.name;
165
+ return "Anonymous";
166
+ }
167
+ function countFibers(fiber) {
168
+ let count = 0;
169
+ const stack = fiber ? [fiber] : [];
170
+ while (stack.length > 0) {
171
+ const current = stack.pop();
172
+ if (!current)
173
+ continue;
174
+ count++;
175
+ if (current.sibling)
176
+ stack.push(current.sibling);
177
+ if (current.child)
178
+ stack.push(current.child);
179
+ }
180
+ return count;
181
+ }
182
+ function numberOrNull(value) {
183
+ return typeof value === "number" && Number.isFinite(value) ? value : null;
184
+ }
185
+ function normalizeInteractions(interactions) {
186
+ if (!(interactions instanceof Set))
187
+ return [];
188
+ return Array.from(interactions)
189
+ .map((interaction) => ({
190
+ id: typeof interaction?.id === "number" ? interaction.id : 0,
191
+ name: typeof interaction?.name === "string" ? interaction.name : "interaction",
192
+ timestamp: typeof interaction?.timestamp === "number" ? interaction.timestamp : 0,
193
+ }))
194
+ .filter((interaction) => interaction.name.length > 0);
195
+ }
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Zustand state management integration
3
+ *
4
+ * Detects Zustand stores and provides state inspection capabilities.
5
+ */
6
+ import type { Page } from "playwright";
7
+ export interface ZustandStore {
8
+ name: string;
9
+ state: Record<string, unknown>;
10
+ actions: string[];
11
+ }
12
+ /**
13
+ * Detect if Zustand is present in the page
14
+ */
15
+ export declare function detectZustand(page: Page): Promise<boolean>;
16
+ /**
17
+ * List all Zustand stores
18
+ */
19
+ export declare function listStores(page: Page): Promise<string[]>;
20
+ /**
21
+ * Inspect a specific Zustand store
22
+ */
23
+ export declare function inspectStore(page: Page, storeName: string): Promise<ZustandStore | null>;
24
+ /**
25
+ * Format Zustand store list for CLI output
26
+ */
27
+ export declare function formatStoreList(stores: string[]): string;
28
+ /**
29
+ * Format Zustand store inspection for CLI output
30
+ */
31
+ export declare function formatStoreInspection(store: ZustandStore): string;
@@ -0,0 +1,113 @@
1
+ /**
2
+ * Zustand state management integration
3
+ *
4
+ * Detects Zustand stores and provides state inspection capabilities.
5
+ */
6
+ /**
7
+ * Detect if Zustand is present in the page
8
+ */
9
+ export async function detectZustand(page) {
10
+ return page.evaluate(() => {
11
+ // Check for Zustand stores in window
12
+ const win = window;
13
+ return !!(win.__ZUSTAND_STORES__ || win.zustandStores);
14
+ });
15
+ }
16
+ /**
17
+ * List all Zustand stores
18
+ */
19
+ export async function listStores(page) {
20
+ return page.evaluate(inPageListStores);
21
+ }
22
+ /**
23
+ * Inspect a specific Zustand store
24
+ */
25
+ export async function inspectStore(page, storeName) {
26
+ return page.evaluate(inPageInspectStore, storeName);
27
+ }
28
+ /**
29
+ * Format Zustand store list for CLI output
30
+ */
31
+ export function formatStoreList(stores) {
32
+ if (stores.length === 0)
33
+ return "No Zustand stores found";
34
+ const lines = ["# Zustand Stores\n"];
35
+ stores.forEach((name) => lines.push(`- ${name}`));
36
+ lines.push("\nUse 'vite-browser react store inspect <name>' to view store details");
37
+ return lines.join("\n");
38
+ }
39
+ /**
40
+ * Format Zustand store inspection for CLI output
41
+ */
42
+ export function formatStoreInspection(store) {
43
+ const lines = [`# Zustand Store: ${store.name}\n`];
44
+ // State section
45
+ if (Object.keys(store.state).length > 0) {
46
+ lines.push("## State");
47
+ for (const [key, value] of Object.entries(store.state)) {
48
+ if (typeof value === "function")
49
+ continue;
50
+ lines.push(` ${key}: ${safeJson(value)}`);
51
+ }
52
+ lines.push("");
53
+ }
54
+ // Actions section
55
+ if (store.actions.length > 0) {
56
+ lines.push("## Actions");
57
+ for (const action of store.actions) {
58
+ lines.push(` ${action}()`);
59
+ }
60
+ lines.push("");
61
+ }
62
+ return lines.join("\n");
63
+ }
64
+ function safeJson(value) {
65
+ try {
66
+ const seen = new WeakSet();
67
+ return JSON.stringify(value, (_, v) => {
68
+ if (v && typeof v === "object") {
69
+ if (seen.has(v))
70
+ return "[Circular]";
71
+ seen.add(v);
72
+ }
73
+ return v;
74
+ });
75
+ }
76
+ catch {
77
+ return String(value);
78
+ }
79
+ }
80
+ // In-page functions
81
+ function inPageListStores() {
82
+ const win = window;
83
+ const stores = win.__ZUSTAND_STORES__ || win.zustandStores || {};
84
+ return Object.keys(stores);
85
+ }
86
+ function inPageInspectStore(storeName) {
87
+ const win = window;
88
+ const stores = win.__ZUSTAND_STORES__ || win.zustandStores || {};
89
+ const store = stores[storeName];
90
+ if (!store)
91
+ return null;
92
+ // Get current state
93
+ const state = store.getState?.() || store.getInitialState?.() || {};
94
+ // Extract actions (functions in the state)
95
+ const actions = [];
96
+ for (const [key, value] of Object.entries(state)) {
97
+ if (typeof value === "function") {
98
+ actions.push(key);
99
+ }
100
+ }
101
+ // Filter out functions from state display
102
+ const stateWithoutFunctions = {};
103
+ for (const [key, value] of Object.entries(state)) {
104
+ if (typeof value !== "function") {
105
+ stateWithoutFunctions[key] = value;
106
+ }
107
+ }
108
+ return {
109
+ name: storeName,
110
+ state: stateWithoutFunctions,
111
+ actions,
112
+ };
113
+ }
@@ -11,7 +11,7 @@ export interface VueComponent {
11
11
  file?: string;
12
12
  line?: number;
13
13
  }
14
- export declare function formatComponentTree(apps: any[]): string;
14
+ export declare function formatComponentTree(apps: any[], maxDepth?: number): string;
15
15
  export declare function formatComponentDetails(targetInstance: any, componentId: string): string;
16
16
  export declare function formatPiniaStores(pinia: any, storeName?: string, piniaFromWindow?: boolean): string;
17
17
  export declare function formatRouterInfo(actualRouter: any): string;
@@ -3,7 +3,7 @@
3
3
  *
4
4
  * Uses @vue/devtools-kit to access Vue component tree and state
5
5
  */
6
- export function formatComponentTree(apps) {
6
+ export function formatComponentTree(apps, maxDepth = 50) {
7
7
  if (apps.length === 0)
8
8
  return "No Vue apps found";
9
9
  const output = [];
@@ -23,11 +23,17 @@ export function formatComponentTree(apps) {
23
23
  return;
24
24
  if (seen.has(instance))
25
25
  return;
26
+ if (depth > maxDepth) {
27
+ output.push(`${" ".repeat(depth)}[max depth reached]`);
28
+ return;
29
+ }
26
30
  seen.add(instance);
27
31
  const indent = " ".repeat(depth);
28
32
  const name = instance.type?.name || instance.type?.__name || "Anonymous";
29
33
  const uid = instance.uid ?? "?";
30
- output.push(`${indent}[${uid}] ${name}`);
34
+ const file = instance.type?.__file;
35
+ const fileSuffix = file ? ` [${file}]` : "";
36
+ output.push(`${indent}[${uid}] ${name}${fileSuffix}`);
31
37
  const subTree = instance.subTree;
32
38
  if (subTree?.component)
33
39
  visit(subTree.component, depth + 1);
@@ -169,6 +175,7 @@ export function formatPiniaStores(pinia, storeName, piniaFromWindow = true) {
169
175
  getterNames.push(...Array.from(rawGetters).map(String));
170
176
  else if (rawGetters && typeof rawGetters === "object")
171
177
  getterNames.push(...Object.keys(rawGetters));
178
+ const getterSet = new Set(getterNames);
172
179
  if (getterNames.length > 0) {
173
180
  output.push("## Getters");
174
181
  for (const key of getterNames) {
@@ -181,6 +188,24 @@ export function formatPiniaStores(pinia, storeName, piniaFromWindow = true) {
181
188
  }
182
189
  output.push("");
183
190
  }
191
+ // List actions (own functions on the store, excluding $ prefixed internals)
192
+ const actionNames = [];
193
+ if (store && typeof store === "object") {
194
+ for (const key of Object.keys(store)) {
195
+ if (key.startsWith("$") || key.startsWith("_"))
196
+ continue;
197
+ if (typeof store[key] === "function" && !getterSet.has(key)) {
198
+ actionNames.push(key);
199
+ }
200
+ }
201
+ }
202
+ if (actionNames.length > 0) {
203
+ output.push("## Actions");
204
+ for (const key of actionNames) {
205
+ output.push(` ${key}()`);
206
+ }
207
+ output.push("");
208
+ }
184
209
  return output.join("\n");
185
210
  }
186
211
  export function formatRouterInfo(actualRouter) {
@@ -199,6 +224,16 @@ export function formatRouterInfo(actualRouter) {
199
224
  if (currentRoute.query && Object.keys(currentRoute.query).length > 0) {
200
225
  output.push(` Query: ${JSON.stringify(currentRoute.query)}`);
201
226
  }
227
+ if (currentRoute.hash) {
228
+ output.push(` Hash: ${currentRoute.hash}`);
229
+ }
230
+ if (currentRoute.meta && Object.keys(currentRoute.meta).length > 0) {
231
+ output.push(` Meta: ${JSON.stringify(currentRoute.meta)}`);
232
+ }
233
+ const matched = currentRoute.matched;
234
+ if (Array.isArray(matched) && matched.length > 1) {
235
+ output.push(` Matched: ${matched.map((r) => r.path || r.name).join(" > ")}`);
236
+ }
202
237
  output.push("");
203
238
  }
204
239
  const routes = actualRouter.getRoutes?.() || actualRouter.options?.routes || [];
@@ -206,7 +241,10 @@ export function formatRouterInfo(actualRouter) {
206
241
  output.push("## All Routes");
207
242
  routes.forEach((route) => {
208
243
  const routeName = route.name ? ` (${route.name})` : "";
209
- output.push(` ${route.path}${routeName}`);
244
+ const meta = route.meta && Object.keys(route.meta).length > 0
245
+ ? ` meta=${JSON.stringify(route.meta)}`
246
+ : "";
247
+ output.push(` ${route.path}${routeName}${meta}`);
210
248
  });
211
249
  }
212
250
  return output.join("\n");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@presto1314w/vite-devtools-browser",
3
- "version": "0.3.3",
3
+ "version": "0.3.6",
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": [
@@ -35,6 +35,22 @@
35
35
  "bin": {
36
36
  "vite-browser": "dist/cli.js"
37
37
  },
38
+ "scripts": {
39
+ "start": "node --import tsx src/cli.ts",
40
+ "dev": "tsx src/cli.ts",
41
+ "docs:dev": "vitepress dev docs",
42
+ "docs:build": "vitepress build docs",
43
+ "docs:preview": "vitepress preview docs",
44
+ "typecheck": "tsc --noEmit",
45
+ "build": "tsc -p tsconfig.build.json",
46
+ "prepack": "pnpm build",
47
+ "test": "vitest run",
48
+ "test:watch": "vitest",
49
+ "test:coverage": "vitest run --coverage",
50
+ "test:evals": "vitest run --dir test/evals",
51
+ "test:evals:ci": "vitest run --dir test/evals --coverage --reporter=default",
52
+ "test:evals:e2e": "pnpm build && vitest run --config vitest.e2e.config.ts"
53
+ },
38
54
  "dependencies": {
39
55
  "playwright": "^1.50.0",
40
56
  "source-map-js": "^1.2.1"
@@ -42,22 +58,12 @@
42
58
  "devDependencies": {
43
59
  "@types/node": "^22.0.0",
44
60
  "@vitest/coverage-v8": "^4.0.18",
45
- "@vue/devtools-kit": "^7.3.2",
46
61
  "@vue/devtools-api": "^7.3.2",
62
+ "@vue/devtools-kit": "^7.3.2",
47
63
  "tsx": "^4.20.6",
48
64
  "typescript": "^5.7.0",
65
+ "vitepress": "^1.6.4",
49
66
  "vitest": "^4.0.16"
50
67
  },
51
- "scripts": {
52
- "start": "node --import tsx src/cli.ts",
53
- "dev": "tsx src/cli.ts",
54
- "typecheck": "tsc --noEmit",
55
- "build": "tsc -p tsconfig.build.json",
56
- "test": "vitest run",
57
- "test:watch": "vitest",
58
- "test:coverage": "vitest run --coverage",
59
- "test:evals": "vitest run --dir test/evals",
60
- "test:evals:ci": "vitest run --dir test/evals --coverage --reporter=default",
61
- "test:evals:e2e": "pnpm build && vitest run --config vitest.e2e.config.ts"
62
- }
63
- }
68
+ "packageManager": "pnpm@10.29.2"
69
+ }