@presto1314w/vite-devtools-browser 0.3.3 → 0.3.5
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/LICENSE +21 -21
- package/README.md +135 -322
- package/dist/browser-session.d.ts +11 -1
- package/dist/browser-session.js +29 -40
- package/dist/cli.js +57 -57
- package/dist/client.js +2 -2
- package/dist/daemon.js +3 -8
- package/dist/event-queue.d.ts +10 -1
- package/dist/event-queue.js +52 -2
- package/dist/paths.d.ts +25 -0
- package/dist/paths.js +42 -4
- package/dist/react/hook-manager.d.ts +29 -0
- package/dist/react/hook-manager.js +75 -0
- package/dist/react/hook.d.ts +1 -0
- package/dist/react/hook.js +227 -0
- package/dist/react/profiler.d.ts +55 -0
- package/dist/react/profiler.js +166 -0
- package/dist/react/zustand.d.ts +31 -0
- package/dist/react/zustand.js +113 -0
- package/dist/vue/devtools.d.ts +1 -1
- package/dist/vue/devtools.js +41 -3
- package/package.json +21 -15
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimal React DevTools Global Hook
|
|
3
|
+
*
|
|
4
|
+
* This is a minimal implementation of the React DevTools global hook interface
|
|
5
|
+
* that allows vite-browser to inspect React component trees without requiring
|
|
6
|
+
* the full React DevTools browser extension.
|
|
7
|
+
*
|
|
8
|
+
* Based on the React DevTools architecture (MIT License)
|
|
9
|
+
* https://github.com/facebook/react/tree/main/packages/react-devtools
|
|
10
|
+
*
|
|
11
|
+
* This implementation provides just enough functionality for:
|
|
12
|
+
* - Component tree inspection
|
|
13
|
+
* - Fiber root tracking
|
|
14
|
+
* - Bridge communication for operations
|
|
15
|
+
*/
|
|
16
|
+
(function installReactDevToolsHook() {
|
|
17
|
+
if (typeof window === "undefined")
|
|
18
|
+
return;
|
|
19
|
+
if (window.__REACT_DEVTOOLS_GLOBAL_HOOK__)
|
|
20
|
+
return;
|
|
21
|
+
const hook = {
|
|
22
|
+
renderers: new Map(),
|
|
23
|
+
rendererInterfaces: new Map(),
|
|
24
|
+
supportsFiber: true,
|
|
25
|
+
// Called by React when a renderer is injected
|
|
26
|
+
inject(renderer) {
|
|
27
|
+
const id = Math.random();
|
|
28
|
+
hook.renderers.set(id, renderer);
|
|
29
|
+
// Create renderer interface for component inspection
|
|
30
|
+
const rendererInterface = createRendererInterface(renderer);
|
|
31
|
+
hook.rendererInterfaces.set(id, rendererInterface);
|
|
32
|
+
return id;
|
|
33
|
+
},
|
|
34
|
+
// Called by React on fiber root commits
|
|
35
|
+
onCommitFiberRoot(id, root, priorityLevel) {
|
|
36
|
+
// Hook for tracking renders - can be overridden by profiler
|
|
37
|
+
},
|
|
38
|
+
// Called by React on fiber unmount
|
|
39
|
+
onCommitFiberUnmount(id, fiber) {
|
|
40
|
+
// Hook for tracking unmounts
|
|
41
|
+
},
|
|
42
|
+
// Sub-hooks for advanced features
|
|
43
|
+
sub(event, handler) {
|
|
44
|
+
// Event subscription (not implemented in minimal version)
|
|
45
|
+
},
|
|
46
|
+
// Check if DevTools is installed
|
|
47
|
+
checkDCE(fn) {
|
|
48
|
+
// Dead code elimination check - always pass
|
|
49
|
+
try {
|
|
50
|
+
fn();
|
|
51
|
+
}
|
|
52
|
+
catch (e) {
|
|
53
|
+
// Ignore
|
|
54
|
+
}
|
|
55
|
+
},
|
|
56
|
+
};
|
|
57
|
+
function createRendererInterface(renderer) {
|
|
58
|
+
const fiberRoots = new Set();
|
|
59
|
+
const fiberToId = new Map();
|
|
60
|
+
const idToFiber = new Map();
|
|
61
|
+
let nextId = 1;
|
|
62
|
+
return {
|
|
63
|
+
// Flush initial operations to populate component tree
|
|
64
|
+
flushInitialOperations() {
|
|
65
|
+
for (const root of fiberRoots) {
|
|
66
|
+
walkTree(root.current, null);
|
|
67
|
+
}
|
|
68
|
+
// Send operations via message event
|
|
69
|
+
const operations = buildOperations();
|
|
70
|
+
window.postMessage({
|
|
71
|
+
source: "react-devtools-bridge",
|
|
72
|
+
payload: {
|
|
73
|
+
event: "operations",
|
|
74
|
+
payload: operations,
|
|
75
|
+
},
|
|
76
|
+
}, "*");
|
|
77
|
+
},
|
|
78
|
+
// Check if element exists
|
|
79
|
+
hasElementWithId(id) {
|
|
80
|
+
return idToFiber.has(id);
|
|
81
|
+
},
|
|
82
|
+
// Get display name for element
|
|
83
|
+
getDisplayNameForElementID(id) {
|
|
84
|
+
const fiber = idToFiber.get(id);
|
|
85
|
+
if (!fiber)
|
|
86
|
+
return "Unknown";
|
|
87
|
+
return getDisplayName(fiber);
|
|
88
|
+
},
|
|
89
|
+
// Inspect element details
|
|
90
|
+
inspectElement(requestID, id, path, forceFullData) {
|
|
91
|
+
const fiber = idToFiber.get(id);
|
|
92
|
+
if (!fiber)
|
|
93
|
+
return { type: "not-found", id };
|
|
94
|
+
const data = {
|
|
95
|
+
id,
|
|
96
|
+
type: "full-data",
|
|
97
|
+
value: {
|
|
98
|
+
key: fiber.key,
|
|
99
|
+
props: extractProps(fiber),
|
|
100
|
+
state: extractState(fiber),
|
|
101
|
+
hooks: extractHooks(fiber),
|
|
102
|
+
context: null,
|
|
103
|
+
owners: extractOwners(fiber),
|
|
104
|
+
source: extractSource(fiber),
|
|
105
|
+
},
|
|
106
|
+
};
|
|
107
|
+
return data;
|
|
108
|
+
},
|
|
109
|
+
};
|
|
110
|
+
function walkTree(fiber, parentId) {
|
|
111
|
+
if (!fiber)
|
|
112
|
+
return;
|
|
113
|
+
const id = nextId++;
|
|
114
|
+
fiberToId.set(fiber, id);
|
|
115
|
+
idToFiber.set(id, fiber);
|
|
116
|
+
// Walk children
|
|
117
|
+
let child = fiber.child;
|
|
118
|
+
while (child) {
|
|
119
|
+
walkTree(child, id);
|
|
120
|
+
child = child.sibling;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
function buildOperations() {
|
|
124
|
+
const ops = [2, 0]; // version, string table size
|
|
125
|
+
const strings = [];
|
|
126
|
+
// Add root operation
|
|
127
|
+
ops.push(1, 1, 11, 0, 0, 0, 0); // ADD_ROOT
|
|
128
|
+
// Add fiber nodes
|
|
129
|
+
for (const [id, fiber] of idToFiber) {
|
|
130
|
+
if (id === 1)
|
|
131
|
+
continue; // Skip root
|
|
132
|
+
const name = getDisplayName(fiber);
|
|
133
|
+
const nameIdx = addString(name);
|
|
134
|
+
const keyIdx = fiber.key ? addString(String(fiber.key)) : 0;
|
|
135
|
+
const parentId = fiberToId.get(fiber.return) || 0;
|
|
136
|
+
ops.push(1, id, 5, parentId, 0, nameIdx, keyIdx, 0); // ADD_NODE
|
|
137
|
+
}
|
|
138
|
+
// Update string table size
|
|
139
|
+
ops[1] = strings.length;
|
|
140
|
+
// Insert string table after version
|
|
141
|
+
const stringOps = [];
|
|
142
|
+
for (const str of strings) {
|
|
143
|
+
const codePoints = Array.from(str).map(c => c.codePointAt(0));
|
|
144
|
+
stringOps.push(codePoints.length, ...codePoints);
|
|
145
|
+
}
|
|
146
|
+
ops.splice(2, 0, ...stringOps);
|
|
147
|
+
return ops;
|
|
148
|
+
function addString(str) {
|
|
149
|
+
let idx = strings.indexOf(str);
|
|
150
|
+
if (idx === -1) {
|
|
151
|
+
idx = strings.length;
|
|
152
|
+
strings.push(str);
|
|
153
|
+
}
|
|
154
|
+
return idx + 1; // 1-indexed
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
function getDisplayName(fiber) {
|
|
158
|
+
if (!fiber)
|
|
159
|
+
return "Unknown";
|
|
160
|
+
if (fiber.type && fiber.type.displayName)
|
|
161
|
+
return fiber.type.displayName;
|
|
162
|
+
if (fiber.type && fiber.type.name)
|
|
163
|
+
return fiber.type.name;
|
|
164
|
+
if (typeof fiber.type === "string")
|
|
165
|
+
return fiber.type;
|
|
166
|
+
if (fiber.tag === 11)
|
|
167
|
+
return "Root";
|
|
168
|
+
return "Anonymous";
|
|
169
|
+
}
|
|
170
|
+
function extractProps(fiber) {
|
|
171
|
+
if (!fiber.memoizedProps)
|
|
172
|
+
return null;
|
|
173
|
+
const props = { ...fiber.memoizedProps };
|
|
174
|
+
delete props.children; // Don't include children in props
|
|
175
|
+
return { data: props };
|
|
176
|
+
}
|
|
177
|
+
function extractState(fiber) {
|
|
178
|
+
if (!fiber.memoizedState)
|
|
179
|
+
return null;
|
|
180
|
+
return { data: fiber.memoizedState };
|
|
181
|
+
}
|
|
182
|
+
function extractHooks(fiber) {
|
|
183
|
+
if (!fiber.memoizedState)
|
|
184
|
+
return null;
|
|
185
|
+
const hooks = [];
|
|
186
|
+
let hook = fiber.memoizedState;
|
|
187
|
+
let index = 0;
|
|
188
|
+
while (hook) {
|
|
189
|
+
hooks.push({
|
|
190
|
+
id: index++,
|
|
191
|
+
name: "Hook",
|
|
192
|
+
value: hook.memoizedState,
|
|
193
|
+
subHooks: [],
|
|
194
|
+
});
|
|
195
|
+
hook = hook.next;
|
|
196
|
+
}
|
|
197
|
+
return hooks.length > 0 ? { data: hooks } : null;
|
|
198
|
+
}
|
|
199
|
+
function extractOwners(fiber) {
|
|
200
|
+
const owners = [];
|
|
201
|
+
let current = fiber.return;
|
|
202
|
+
while (current) {
|
|
203
|
+
if (current.type) {
|
|
204
|
+
owners.push({
|
|
205
|
+
displayName: getDisplayName(current),
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
current = current.return;
|
|
209
|
+
}
|
|
210
|
+
return owners;
|
|
211
|
+
}
|
|
212
|
+
function extractSource(fiber) {
|
|
213
|
+
if (!fiber._debugSource)
|
|
214
|
+
return null;
|
|
215
|
+
const { fileName, lineNumber, columnNumber } = fiber._debugSource;
|
|
216
|
+
return [null, fileName, lineNumber, columnNumber];
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
// Install the hook
|
|
220
|
+
Object.defineProperty(window, "__REACT_DEVTOOLS_GLOBAL_HOOK__", {
|
|
221
|
+
value: hook,
|
|
222
|
+
writable: false,
|
|
223
|
+
enumerable: false,
|
|
224
|
+
configurable: false,
|
|
225
|
+
});
|
|
226
|
+
})();
|
|
227
|
+
export {};
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* React render tracking and profiling
|
|
3
|
+
*
|
|
4
|
+
* Tracks component renders, durations, and trigger reasons using React DevTools Profiler API.
|
|
5
|
+
*/
|
|
6
|
+
import type { Page } from "playwright";
|
|
7
|
+
export interface RenderInfo {
|
|
8
|
+
componentId: number;
|
|
9
|
+
componentName: string;
|
|
10
|
+
phase: "mount" | "update" | "nested-update";
|
|
11
|
+
actualDuration: number;
|
|
12
|
+
baseDuration: number;
|
|
13
|
+
startTime: number;
|
|
14
|
+
commitTime: number;
|
|
15
|
+
interactions: Set<{
|
|
16
|
+
id: number;
|
|
17
|
+
name: string;
|
|
18
|
+
timestamp: number;
|
|
19
|
+
}>;
|
|
20
|
+
}
|
|
21
|
+
export interface RenderTrigger {
|
|
22
|
+
componentId: number;
|
|
23
|
+
componentName: string;
|
|
24
|
+
reason: "props" | "state" | "hooks" | "parent" | "context" | "unknown";
|
|
25
|
+
timestamp: number;
|
|
26
|
+
details?: string;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Install render tracking in the page
|
|
30
|
+
*/
|
|
31
|
+
export declare function installRenderTracking(page: Page): Promise<void>;
|
|
32
|
+
/**
|
|
33
|
+
* Get recent render events
|
|
34
|
+
*/
|
|
35
|
+
export declare function getRecentRenders(page: Page, limit?: number): Promise<RenderInfo[]>;
|
|
36
|
+
/**
|
|
37
|
+
* Get render triggers (why components re-rendered)
|
|
38
|
+
*/
|
|
39
|
+
export declare function getRenderTriggers(page: Page, limit?: number): Promise<RenderTrigger[]>;
|
|
40
|
+
/**
|
|
41
|
+
* Clear render history
|
|
42
|
+
*/
|
|
43
|
+
export declare function clearRenderHistory(page: Page): Promise<void>;
|
|
44
|
+
/**
|
|
45
|
+
* Format render info for CLI output
|
|
46
|
+
*/
|
|
47
|
+
export declare function formatRenderInfo(renders: RenderInfo[]): string;
|
|
48
|
+
/**
|
|
49
|
+
* Format render triggers for CLI output
|
|
50
|
+
*/
|
|
51
|
+
export declare function formatRenderTriggers(triggers: RenderTrigger[]): string;
|
|
52
|
+
/**
|
|
53
|
+
* Analyze slow renders (> 16ms)
|
|
54
|
+
*/
|
|
55
|
+
export declare function analyzeSlowRenders(renders: RenderInfo[]): string;
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* React render tracking and profiling
|
|
3
|
+
*
|
|
4
|
+
* Tracks component renders, durations, and trigger reasons using React DevTools Profiler API.
|
|
5
|
+
*/
|
|
6
|
+
/**
|
|
7
|
+
* Install render tracking in the page
|
|
8
|
+
*/
|
|
9
|
+
export async function installRenderTracking(page) {
|
|
10
|
+
await page.evaluate(inPageInstallRenderTracking);
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Get recent render events
|
|
14
|
+
*/
|
|
15
|
+
export async function getRecentRenders(page, limit = 50) {
|
|
16
|
+
return page.evaluate(inPageGetRecentRenders, limit);
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Get render triggers (why components re-rendered)
|
|
20
|
+
*/
|
|
21
|
+
export async function getRenderTriggers(page, limit = 50) {
|
|
22
|
+
return page.evaluate(inPageGetRenderTriggers, limit);
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Clear render history
|
|
26
|
+
*/
|
|
27
|
+
export async function clearRenderHistory(page) {
|
|
28
|
+
await page.evaluate(inPageClearRenderHistory);
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Format render info for CLI output
|
|
32
|
+
*/
|
|
33
|
+
export function formatRenderInfo(renders) {
|
|
34
|
+
if (renders.length === 0)
|
|
35
|
+
return "No renders recorded";
|
|
36
|
+
const lines = ["# React Renders\n"];
|
|
37
|
+
for (const render of renders) {
|
|
38
|
+
const phase = render.phase === "mount" ? "MOUNT" : render.phase === "update" ? "UPDATE" : "NESTED";
|
|
39
|
+
const duration = render.actualDuration.toFixed(2);
|
|
40
|
+
const slow = render.actualDuration > 16 ? " ⚠️ SLOW" : "";
|
|
41
|
+
lines.push(`[${phase}] ${render.componentName} (${duration}ms)${slow}`);
|
|
42
|
+
if (render.interactions.size > 0) {
|
|
43
|
+
const interactions = Array.from(render.interactions)
|
|
44
|
+
.map((i) => i.name)
|
|
45
|
+
.join(", ");
|
|
46
|
+
lines.push(` Interactions: ${interactions}`);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return lines.join("\n");
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Format render triggers for CLI output
|
|
53
|
+
*/
|
|
54
|
+
export function formatRenderTriggers(triggers) {
|
|
55
|
+
if (triggers.length === 0)
|
|
56
|
+
return "No render triggers recorded";
|
|
57
|
+
const lines = ["# Render Triggers\n"];
|
|
58
|
+
for (const trigger of triggers) {
|
|
59
|
+
const reason = trigger.reason.toUpperCase();
|
|
60
|
+
const details = trigger.details ? ` - ${trigger.details}` : "";
|
|
61
|
+
lines.push(`[${reason}] ${trigger.componentName}${details}`);
|
|
62
|
+
}
|
|
63
|
+
return lines.join("\n");
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Analyze slow renders (> 16ms)
|
|
67
|
+
*/
|
|
68
|
+
export function analyzeSlowRenders(renders) {
|
|
69
|
+
const slowRenders = renders.filter((r) => r.actualDuration > 16);
|
|
70
|
+
if (slowRenders.length === 0) {
|
|
71
|
+
return "No slow renders detected (all renders < 16ms)";
|
|
72
|
+
}
|
|
73
|
+
const lines = [
|
|
74
|
+
"# Slow Renders Analysis\n",
|
|
75
|
+
`Found ${slowRenders.length} slow render(s) (> 16ms)\n`,
|
|
76
|
+
];
|
|
77
|
+
// Group by component
|
|
78
|
+
const byComponent = new Map();
|
|
79
|
+
for (const render of slowRenders) {
|
|
80
|
+
const list = byComponent.get(render.componentName) || [];
|
|
81
|
+
list.push(render);
|
|
82
|
+
byComponent.set(render.componentName, list);
|
|
83
|
+
}
|
|
84
|
+
// Sort by total time
|
|
85
|
+
const sorted = Array.from(byComponent.entries()).sort(([, a], [, b]) => {
|
|
86
|
+
const totalA = a.reduce((sum, r) => sum + r.actualDuration, 0);
|
|
87
|
+
const totalB = b.reduce((sum, r) => sum + r.actualDuration, 0);
|
|
88
|
+
return totalB - totalA;
|
|
89
|
+
});
|
|
90
|
+
for (const [name, renders] of sorted) {
|
|
91
|
+
const count = renders.length;
|
|
92
|
+
const total = renders.reduce((sum, r) => sum + r.actualDuration, 0);
|
|
93
|
+
const avg = total / count;
|
|
94
|
+
const max = Math.max(...renders.map((r) => r.actualDuration));
|
|
95
|
+
lines.push(`${name}:`);
|
|
96
|
+
lines.push(` Count: ${count}`);
|
|
97
|
+
lines.push(` Total: ${total.toFixed(2)}ms`);
|
|
98
|
+
lines.push(` Average: ${avg.toFixed(2)}ms`);
|
|
99
|
+
lines.push(` Max: ${max.toFixed(2)}ms`);
|
|
100
|
+
lines.push("");
|
|
101
|
+
}
|
|
102
|
+
return lines.join("\n");
|
|
103
|
+
}
|
|
104
|
+
// In-page functions
|
|
105
|
+
function inPageInstallRenderTracking() {
|
|
106
|
+
const win = window;
|
|
107
|
+
// Initialize storage
|
|
108
|
+
win.__REACT_RENDER_HISTORY__ = win.__REACT_RENDER_HISTORY__ || [];
|
|
109
|
+
win.__REACT_RENDER_TRIGGERS__ = win.__REACT_RENDER_TRIGGERS__ || [];
|
|
110
|
+
// Hook into React DevTools if available
|
|
111
|
+
const hook = win.__REACT_DEVTOOLS_GLOBAL_HOOK__;
|
|
112
|
+
if (!hook) {
|
|
113
|
+
console.warn("React DevTools hook not found, render tracking limited");
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
// Listen for commit events
|
|
117
|
+
if (!win.__REACT_RENDER_TRACKING_INSTALLED__) {
|
|
118
|
+
win.__REACT_RENDER_TRACKING_INSTALLED__ = true;
|
|
119
|
+
// Track renders via Profiler API
|
|
120
|
+
const originalOnCommitFiberRoot = hook.onCommitFiberRoot;
|
|
121
|
+
hook.onCommitFiberRoot = function (id, root, priorityLevel) {
|
|
122
|
+
try {
|
|
123
|
+
// Record render info
|
|
124
|
+
const renderInfo = {
|
|
125
|
+
componentId: id,
|
|
126
|
+
componentName: root?.current?.type?.name || "Root",
|
|
127
|
+
phase: root?.current?.mode === 0 ? "mount" : "update",
|
|
128
|
+
actualDuration: 0,
|
|
129
|
+
baseDuration: 0,
|
|
130
|
+
startTime: Date.now(),
|
|
131
|
+
commitTime: Date.now(),
|
|
132
|
+
interactions: new Set(),
|
|
133
|
+
};
|
|
134
|
+
win.__REACT_RENDER_HISTORY__.push(renderInfo);
|
|
135
|
+
// Keep only last 100 renders
|
|
136
|
+
if (win.__REACT_RENDER_HISTORY__.length > 100) {
|
|
137
|
+
win.__REACT_RENDER_HISTORY__.shift();
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
catch (e) {
|
|
141
|
+
// Ignore errors in tracking
|
|
142
|
+
}
|
|
143
|
+
if (originalOnCommitFiberRoot) {
|
|
144
|
+
return originalOnCommitFiberRoot.call(this, id, root, priorityLevel);
|
|
145
|
+
}
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
function inPageGetRecentRenders(limit) {
|
|
150
|
+
const win = window;
|
|
151
|
+
const history = win.__REACT_RENDER_HISTORY__ || [];
|
|
152
|
+
return history.slice(-limit).map((r) => ({
|
|
153
|
+
...r,
|
|
154
|
+
interactions: Array.from(r.interactions || []),
|
|
155
|
+
}));
|
|
156
|
+
}
|
|
157
|
+
function inPageGetRenderTriggers(limit) {
|
|
158
|
+
const win = window;
|
|
159
|
+
const triggers = win.__REACT_RENDER_TRIGGERS__ || [];
|
|
160
|
+
return triggers.slice(-limit);
|
|
161
|
+
}
|
|
162
|
+
function inPageClearRenderHistory() {
|
|
163
|
+
const win = window;
|
|
164
|
+
win.__REACT_RENDER_HISTORY__ = [];
|
|
165
|
+
win.__REACT_RENDER_TRIGGERS__ = [];
|
|
166
|
+
}
|
|
@@ -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
|
+
}
|
package/dist/vue/devtools.d.ts
CHANGED
|
@@ -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;
|
package/dist/vue/devtools.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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");
|