@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.
package/dist/paths.js CHANGED
@@ -1,8 +1,50 @@
1
- import { homedir } from "node:os";
1
+ import { accessSync, constants } from "node:fs";
2
+ import { homedir, tmpdir } from "node:os";
2
3
  import { join } from "node:path";
3
- const isWindows = process.platform === "win32";
4
- const session = process.env.VITE_BROWSER_SESSION || "default";
5
- export const socketDir = join(homedir(), ".vite-browser");
4
+ export const isWindows = process.platform === "win32";
5
+ export const isLinux = process.platform === "linux";
6
+ /**
7
+ * Sanitize a session name for safe use in file paths and pipe names.
8
+ */
9
+ export function sanitizeSession(name) {
10
+ return name.replace(/[^a-zA-Z0-9_-]/g, "_");
11
+ }
12
+ const session = sanitizeSession(process.env.VITE_BROWSER_SESSION || "default");
13
+ /**
14
+ * Resolve the base directory for vite-browser runtime files.
15
+ *
16
+ * Uses `~/.vite-browser` on all platforms.
17
+ * Falls back to `$TMPDIR/vite-browser-<uid>` when the home directory
18
+ * is not writable (e.g. some CI/container environments).
19
+ */
20
+ export function resolveSocketDir() {
21
+ try {
22
+ const home = homedir();
23
+ if (home) {
24
+ accessSync(home, constants.W_OK);
25
+ return join(home, ".vite-browser");
26
+ }
27
+ }
28
+ catch {
29
+ // homedir() can throw, and sandboxed / CI environments may expose a
30
+ // home directory that is present but not writable.
31
+ }
32
+ // Fallback: use tmpdir scoped by uid to avoid collisions
33
+ const uid = process.getuid?.() ?? process.pid;
34
+ return join(tmpdir(), `vite-browser-${uid}`);
35
+ }
36
+ export const socketDir = resolveSocketDir();
37
+ /**
38
+ * Socket path for the daemon.
39
+ *
40
+ * - Windows: uses a named pipe `\\.\pipe\vite-browser-<session>`
41
+ * - Unix: uses a Unix domain socket file in socketDir
42
+ *
43
+ * Note: Unix socket paths have a ~104-char limit on macOS and ~108 on
44
+ * Linux. The `~/.vite-browser/<session>.sock` path is well within
45
+ * that range. The tmpdir fallback may produce longer paths; we keep
46
+ * them short by using numeric uid.
47
+ */
6
48
  export const socketPath = isWindows
7
49
  ? `\\\\.\\pipe\\vite-browser-${session}`
8
50
  : join(socketDir, `${session}.sock`);
@@ -12,6 +12,16 @@ export type ReactInspection = {
12
12
  };
13
13
  export declare function snapshot(page: Page): Promise<ReactNode[]>;
14
14
  export declare function inspect(page: Page, id: number): Promise<ReactInspection>;
15
+ export declare function getPrimaryRendererInterface(hook: {
16
+ rendererInterfaces?: Map<unknown, unknown> | {
17
+ values?: () => IterableIterator<unknown>;
18
+ };
19
+ } | null | undefined): unknown;
20
+ export declare function findRendererInterfaceForElement(hook: {
21
+ rendererInterfaces?: Map<unknown, unknown> | {
22
+ values?: () => IterableIterator<unknown>;
23
+ };
24
+ } | null | undefined, id: number): unknown;
15
25
  export declare function format(nodes: ReactNode[]): string;
16
26
  export declare function path(nodes: ReactNode[], id: number): string;
17
27
  export declare function typeName(type: number): string;
@@ -4,6 +4,21 @@ export async function snapshot(page) {
4
4
  export async function inspect(page, id) {
5
5
  return page.evaluate(inPageInspect, id);
6
6
  }
7
+ export function getPrimaryRendererInterface(hook) {
8
+ const values = hook?.rendererInterfaces?.values?.();
9
+ return values?.next().value ?? null;
10
+ }
11
+ export function findRendererInterfaceForElement(hook, id) {
12
+ const values = hook?.rendererInterfaces?.values?.();
13
+ if (!values)
14
+ return null;
15
+ for (const rendererInterface of values) {
16
+ if (rendererInterface?.hasElementWithId?.(id)) {
17
+ return rendererInterface;
18
+ }
19
+ }
20
+ return null;
21
+ }
7
22
  export function format(nodes) {
8
23
  const children = new Map();
9
24
  for (const n of nodes) {
@@ -182,7 +197,7 @@ async function inPageSnapshot() {
182
197
  const hook = window.__REACT_DEVTOOLS_GLOBAL_HOOK__;
183
198
  if (!hook)
184
199
  throw new Error("React DevTools hook not installed");
185
- const ri = hook.rendererInterfaces?.get?.(1);
200
+ const ri = getPrimaryRendererInterface(hook);
186
201
  if (!ri)
187
202
  throw new Error("no React renderer attached");
188
203
  const batches = await collect(ri);
@@ -207,7 +222,8 @@ async function inPageSnapshot() {
207
222
  }
208
223
  function inPageInspect(id) {
209
224
  const hook = window.__REACT_DEVTOOLS_GLOBAL_HOOK__;
210
- const ri = hook?.rendererInterfaces?.get?.(1);
225
+ const ri = findRendererInterfaceForElement(hook, id) ??
226
+ getPrimaryRendererInterface(hook);
211
227
  if (!ri)
212
228
  throw new Error("no React renderer attached");
213
229
  if (!ri.hasElementWithId(id))
@@ -0,0 +1,29 @@
1
+ /**
2
+ * React DevTools Hook Management
3
+ *
4
+ * Provides health checks and injection helpers for the bundled React DevTools hook.
5
+ * This removes the dependency on external browser extensions for React inspection.
6
+ */
7
+ import type { Page } from "playwright";
8
+ /**
9
+ * Get the hook source code, lazily loaded and cached
10
+ */
11
+ export declare function getHookSource(): string;
12
+ export interface HookHealthStatus {
13
+ installed: boolean;
14
+ hasRenderers: boolean;
15
+ rendererCount: number;
16
+ hasFiberSupport: boolean;
17
+ }
18
+ /**
19
+ * Check the health of the React DevTools hook in the page
20
+ */
21
+ export declare function checkHookHealth(page: Page): Promise<HookHealthStatus>;
22
+ /**
23
+ * Inject the React DevTools hook into a page if not already present.
24
+ */
25
+ export declare function injectHook(page: Page): Promise<boolean>;
26
+ /**
27
+ * Format hook health status for CLI output
28
+ */
29
+ export declare function formatHookHealth(status: HookHealthStatus): string;
@@ -0,0 +1,75 @@
1
+ /**
2
+ * React DevTools Hook Management
3
+ *
4
+ * Provides health checks and injection helpers for the bundled React DevTools hook.
5
+ * This removes the dependency on external browser extensions for React inspection.
6
+ */
7
+ import { readFileSync } from "node:fs";
8
+ import { resolve } from "node:path";
9
+ /** Path to the bundled React DevTools hook */
10
+ const hookPath = resolve(import.meta.dirname, "./hook.js");
11
+ /** Cached hook source code */
12
+ let hookSource = null;
13
+ /**
14
+ * Get the hook source code, lazily loaded and cached
15
+ */
16
+ export function getHookSource() {
17
+ if (!hookSource) {
18
+ hookSource = readFileSync(hookPath, "utf-8");
19
+ }
20
+ return hookSource;
21
+ }
22
+ /**
23
+ * Check the health of the React DevTools hook in the page
24
+ */
25
+ export async function checkHookHealth(page) {
26
+ return page.evaluate(inPageCheckHookHealth);
27
+ }
28
+ /**
29
+ * Inject the React DevTools hook into a page if not already present.
30
+ */
31
+ export async function injectHook(page) {
32
+ const alreadyInstalled = await page.evaluate(() => !!window.__REACT_DEVTOOLS_GLOBAL_HOOK__);
33
+ if (alreadyInstalled)
34
+ return false;
35
+ await page.evaluate(getHookSource());
36
+ return true;
37
+ }
38
+ /**
39
+ * Format hook health status for CLI output
40
+ */
41
+ export function formatHookHealth(status) {
42
+ const lines = ["# React DevTools Hook Status\n"];
43
+ lines.push(`Installed: ${status.installed ? "✅ Yes" : "❌ No"}`);
44
+ lines.push(`Fiber support: ${status.hasFiberSupport ? "✅ Yes" : "❌ No"}`);
45
+ lines.push(`Renderers: ${status.rendererCount}`);
46
+ lines.push(`Has renderers: ${status.hasRenderers ? "✅ Yes" : "❌ No"}`);
47
+ if (!status.installed) {
48
+ lines.push("\n⚠️ Hook not installed. React DevTools features will not work.");
49
+ lines.push("The hook should be injected before React loads.");
50
+ }
51
+ else if (!status.hasRenderers) {
52
+ lines.push("\n⚠️ No React renderers detected.");
53
+ lines.push("This page may not be using React, or React hasn't mounted yet.");
54
+ }
55
+ return lines.join("\n");
56
+ }
57
+ // In-page functions
58
+ function inPageCheckHookHealth() {
59
+ const hook = window.__REACT_DEVTOOLS_GLOBAL_HOOK__;
60
+ if (!hook) {
61
+ return {
62
+ installed: false,
63
+ hasRenderers: false,
64
+ rendererCount: 0,
65
+ hasFiberSupport: false,
66
+ };
67
+ }
68
+ const rendererCount = hook.renderers?.size ?? 0;
69
+ return {
70
+ installed: true,
71
+ hasRenderers: rendererCount > 0,
72
+ rendererCount,
73
+ hasFiberSupport: !!hook.supportsFiber,
74
+ };
75
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,255 @@
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
+ let nextRendererId = 1;
22
+ const hook = {
23
+ renderers: new Map(),
24
+ rendererInterfaces: new Map(),
25
+ supportsFiber: true,
26
+ // Called by React when a renderer is injected
27
+ inject(renderer) {
28
+ const id = nextRendererId++;
29
+ hook.renderers.set(id, renderer);
30
+ // Create renderer interface for component inspection
31
+ const rendererInterface = createRendererInterface(renderer);
32
+ hook.rendererInterfaces.set(id, rendererInterface);
33
+ return id;
34
+ },
35
+ // Called by React on fiber root commits
36
+ onCommitFiberRoot(id, root, priorityLevel) {
37
+ const rendererInterface = hook.rendererInterfaces.get(id);
38
+ rendererInterface?.trackFiberRoot(root);
39
+ },
40
+ // Called by React on fiber unmount
41
+ onCommitFiberUnmount(id, fiber) {
42
+ const rendererInterface = hook.rendererInterfaces.get(id);
43
+ rendererInterface?.handleCommitFiberUnmount(fiber);
44
+ },
45
+ // Sub-hooks for advanced features
46
+ sub(event, handler) {
47
+ // Event subscription (not implemented in minimal version)
48
+ },
49
+ // Check if DevTools is installed
50
+ checkDCE(fn) {
51
+ // Dead code elimination check - always pass
52
+ try {
53
+ fn();
54
+ }
55
+ catch (e) {
56
+ // Ignore
57
+ }
58
+ },
59
+ };
60
+ function createRendererInterface(renderer) {
61
+ const fiberRoots = new Set();
62
+ const fiberToId = new Map();
63
+ const idToFiber = new Map();
64
+ const fiberParents = new Map();
65
+ let nextId = 2;
66
+ return {
67
+ trackFiberRoot(root) {
68
+ if (root && root.current) {
69
+ fiberRoots.add(root);
70
+ }
71
+ },
72
+ handleCommitFiberUnmount(fiber) {
73
+ pruneFiber(fiber);
74
+ },
75
+ // Flush initial operations to populate component tree
76
+ flushInitialOperations() {
77
+ fiberToId.clear();
78
+ idToFiber.clear();
79
+ fiberParents.clear();
80
+ nextId = 2;
81
+ for (const root of fiberRoots) {
82
+ walkTree(root.current, 1);
83
+ }
84
+ // Send operations via message event
85
+ const operations = buildOperations();
86
+ window.postMessage({
87
+ source: "react-devtools-bridge",
88
+ payload: {
89
+ event: "operations",
90
+ payload: operations,
91
+ },
92
+ }, "*");
93
+ },
94
+ // Check if element exists
95
+ hasElementWithId(id) {
96
+ return idToFiber.has(id);
97
+ },
98
+ // Get display name for element
99
+ getDisplayNameForElementID(id) {
100
+ const fiber = idToFiber.get(id);
101
+ if (!fiber)
102
+ return "Unknown";
103
+ return getDisplayName(fiber);
104
+ },
105
+ // Inspect element details
106
+ inspectElement(requestID, id, path, forceFullData) {
107
+ const fiber = idToFiber.get(id);
108
+ if (!fiber)
109
+ return { type: "not-found", id };
110
+ const data = {
111
+ id,
112
+ type: "full-data",
113
+ value: {
114
+ key: fiber.key,
115
+ props: extractProps(fiber),
116
+ state: extractState(fiber),
117
+ hooks: extractHooks(fiber),
118
+ context: null,
119
+ owners: extractOwners(fiber),
120
+ source: extractSource(fiber),
121
+ },
122
+ };
123
+ return data;
124
+ },
125
+ };
126
+ function walkTree(fiber, parentId) {
127
+ if (!fiber)
128
+ return;
129
+ const id = nextId++;
130
+ fiberToId.set(fiber, id);
131
+ idToFiber.set(id, fiber);
132
+ fiberParents.set(fiber, parentId);
133
+ // Walk children
134
+ let child = fiber.child;
135
+ while (child) {
136
+ walkTree(child, id);
137
+ child = child.sibling;
138
+ }
139
+ }
140
+ function buildOperations() {
141
+ const strings = [];
142
+ const nodeOps = [];
143
+ // Add fiber nodes
144
+ for (const [id, fiber] of idToFiber) {
145
+ const name = getDisplayName(fiber);
146
+ const nameIdx = addString(name);
147
+ const keyIdx = fiber.key ? addString(String(fiber.key)) : 0;
148
+ const parentId = fiberParents.get(fiber) || 0;
149
+ nodeOps.push(1, id, 0, parentId, 0, nameIdx, keyIdx, 0); // ADD_NODE
150
+ }
151
+ const stringOps = [];
152
+ for (const str of strings) {
153
+ const codePoints = Array.from(str).map(c => c.codePointAt(0));
154
+ stringOps.push(codePoints.length, ...codePoints);
155
+ }
156
+ const ops = [0, 0, stringOps.length, ...stringOps];
157
+ // Add root operation
158
+ ops.push(1, 1, 11, 0, 0, 0, 0); // ADD_ROOT
159
+ ops.push(...nodeOps);
160
+ return ops;
161
+ function addString(str) {
162
+ let idx = strings.indexOf(str);
163
+ if (idx === -1) {
164
+ idx = strings.length;
165
+ strings.push(str);
166
+ }
167
+ return idx + 1; // 1-indexed
168
+ }
169
+ }
170
+ function getDisplayName(fiber) {
171
+ if (!fiber)
172
+ return "Unknown";
173
+ if (fiber.type && fiber.type.displayName)
174
+ return fiber.type.displayName;
175
+ if (fiber.type && fiber.type.name)
176
+ return fiber.type.name;
177
+ if (typeof fiber.type === "string")
178
+ return fiber.type;
179
+ if (fiber.tag === 11)
180
+ return "Root";
181
+ return "Anonymous";
182
+ }
183
+ function extractProps(fiber) {
184
+ if (!fiber.memoizedProps)
185
+ return null;
186
+ const props = { ...fiber.memoizedProps };
187
+ delete props.children; // Don't include children in props
188
+ return { data: props };
189
+ }
190
+ function extractState(fiber) {
191
+ if (!fiber.memoizedState)
192
+ return null;
193
+ return { data: fiber.memoizedState };
194
+ }
195
+ function extractHooks(fiber) {
196
+ if (!fiber.memoizedState)
197
+ return null;
198
+ const hooks = [];
199
+ let hook = fiber.memoizedState;
200
+ let index = 0;
201
+ while (hook) {
202
+ hooks.push({
203
+ id: index++,
204
+ name: "Hook",
205
+ value: hook.memoizedState,
206
+ subHooks: [],
207
+ });
208
+ hook = hook.next;
209
+ }
210
+ return hooks.length > 0 ? { data: hooks } : null;
211
+ }
212
+ function extractOwners(fiber) {
213
+ const owners = [];
214
+ let current = fiber.return;
215
+ while (current) {
216
+ if (current.type) {
217
+ owners.push({
218
+ displayName: getDisplayName(current),
219
+ });
220
+ }
221
+ current = current.return;
222
+ }
223
+ return owners;
224
+ }
225
+ function extractSource(fiber) {
226
+ if (!fiber._debugSource)
227
+ return null;
228
+ const { fileName, lineNumber, columnNumber } = fiber._debugSource;
229
+ return [null, fileName, lineNumber, columnNumber];
230
+ }
231
+ function pruneFiber(fiber) {
232
+ if (!fiber)
233
+ return;
234
+ const mappedId = fiberToId.get(fiber);
235
+ if (mappedId != null) {
236
+ fiberToId.delete(fiber);
237
+ idToFiber.delete(mappedId);
238
+ fiberParents.delete(fiber);
239
+ }
240
+ let child = fiber.child;
241
+ while (child) {
242
+ pruneFiber(child);
243
+ child = child.sibling;
244
+ }
245
+ }
246
+ }
247
+ // Install the hook
248
+ Object.defineProperty(window, "__REACT_DEVTOOLS_GLOBAL_HOOK__", {
249
+ value: hook,
250
+ writable: false,
251
+ enumerable: false,
252
+ configurable: false,
253
+ });
254
+ })();
255
+ export {};
@@ -0,0 +1,38 @@
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
+ import type { Page } from "playwright";
8
+ export interface RenderInteraction {
9
+ id: number;
10
+ name: string;
11
+ timestamp: number;
12
+ }
13
+ export interface RenderInfo {
14
+ rendererId: number;
15
+ rootName: string;
16
+ phase: "mount" | "update" | "nested-update";
17
+ actualDuration: number | null;
18
+ baseDuration: number | null;
19
+ startTime: number | null;
20
+ commitTime: number;
21
+ fiberCount: number;
22
+ interactions: RenderInteraction[];
23
+ }
24
+ export interface RenderTrigger {
25
+ rendererId: number;
26
+ rootName: string;
27
+ reason: "props" | "state" | "hooks" | "parent" | "context" | "unknown";
28
+ timestamp: number;
29
+ details?: string;
30
+ }
31
+ export declare function installRenderTracking(page: Page): Promise<void>;
32
+ export declare function getRecentRenders(page: Page, limit?: number): Promise<RenderInfo[]>;
33
+ export declare function getRenderTriggers(page: Page, limit?: number): Promise<RenderTrigger[]>;
34
+ export declare function clearRenderHistory(page: Page): Promise<void>;
35
+ export declare function formatDuration(duration: number | null): string;
36
+ export declare function formatRenderInfo(renders: RenderInfo[]): string;
37
+ export declare function formatRenderTriggers(triggers: RenderTrigger[]): string;
38
+ export declare function analyzeSlowRenders(renders: RenderInfo[]): string;