@narumitw/pi-chrome-devtools 0.13.0 → 0.14.0

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,219 @@
1
+ import {
2
+ ensureDevToolsEndpoint,
3
+ devToolsEndpoint,
4
+ fetchDevToolsJson,
5
+ formatPageListItem,
6
+ launchHint,
7
+ withEndpointRetry,
8
+ } from "./browser-manager.js";
9
+ import {
10
+ DEFAULT_ENDPOINT_WAIT_MS,
11
+ DEFAULT_TIMEOUT_MS,
12
+ state,
13
+ type DevToolsPage,
14
+ } from "./runtime.js";
15
+
16
+ interface CdpResponse<T = unknown> {
17
+ id: number;
18
+ result?: T;
19
+ error?: { code: number; message: string; data?: unknown };
20
+ }
21
+
22
+ export async function listPages(options: { waitMs?: number } = {}) {
23
+ const waitMs = options.waitMs ?? DEFAULT_ENDPOINT_WAIT_MS;
24
+ await ensureDevToolsEndpoint(waitMs);
25
+ return withEndpointRetry(async () => {
26
+ const pages = await fetchDevToolsJson<DevToolsPage[]>("/json/list");
27
+ return pages.filter((page) => page.type === "page" && page.webSocketDebuggerUrl);
28
+ }, waitMs);
29
+ }
30
+
31
+ export async function getPage(pageId: string) {
32
+ const pages = await listPages();
33
+ return requirePage(pageId, pages);
34
+ }
35
+
36
+ export async function resolvePage(pageId?: string) {
37
+ const pages = await listPages();
38
+ if (pageId) return requirePage(pageId, pages);
39
+
40
+ const page = resolveDefaultPage(pages);
41
+ if (!page) {
42
+ throw new Error(
43
+ [
44
+ `No Chrome pages found at ${devToolsEndpoint()}.`,
45
+ "Use chrome_devtools_navigate with a URL to create a page, or open a Chrome tab manually.",
46
+ launchHint(),
47
+ ].join("\n"),
48
+ );
49
+ }
50
+
51
+ return page;
52
+ }
53
+
54
+ export async function resolvePageForNavigation(pageId?: string) {
55
+ const pages = await listPages();
56
+ if (pageId) return { created: false, page: requirePage(pageId, pages) };
57
+
58
+ const page = resolveDefaultPage(pages);
59
+ if (page) return { created: false, page };
60
+
61
+ return { created: true, page: await createPage("about:blank") };
62
+ }
63
+
64
+ function resolveDefaultPage(pages: DevToolsPage[]) {
65
+ if (!state.activePageId) return pages[0];
66
+
67
+ const activePage = pages.find((candidate) => candidate.id === state.activePageId);
68
+ if (activePage) return activePage;
69
+
70
+ state.activePageId = undefined;
71
+ return pages[0];
72
+ }
73
+
74
+ function requirePage(pageId: string, pages: DevToolsPage[]) {
75
+ const page = pages.find((candidate) => candidate.id === pageId);
76
+ if (page) return page;
77
+
78
+ const availablePages = pages.map(formatPageListItem).join("\n");
79
+ throw new Error(
80
+ [
81
+ `Chrome DevTools page not found: ${pageId}.`,
82
+ availablePages
83
+ ? `Available pages:\n${availablePages}`
84
+ : "No inspectable Chrome pages are currently available.",
85
+ ].join("\n"),
86
+ );
87
+ }
88
+
89
+ export async function createPage(url: string, options: { waitMs?: number } = {}) {
90
+ const waitMs = options.waitMs ?? DEFAULT_ENDPOINT_WAIT_MS;
91
+ await ensureDevToolsEndpoint(waitMs);
92
+ const page = await withEndpointRetry(
93
+ () =>
94
+ fetchDevToolsJson<DevToolsPage>(`/json/new?${encodeURIComponent(url)}`, {
95
+ method: "PUT",
96
+ }),
97
+ waitMs,
98
+ );
99
+ if (page.type !== "page" || !page.webSocketDebuggerUrl) {
100
+ throw new Error("Chrome DevTools created a target that is not an inspectable page.");
101
+ }
102
+
103
+ return page;
104
+ }
105
+
106
+ export function formatPage(page: DevToolsPage) {
107
+ return {
108
+ id: page.id,
109
+ type: page.type,
110
+ title: page.title,
111
+ url: page.url,
112
+ };
113
+ }
114
+
115
+ export function textResult(text: string, details: unknown) {
116
+ return {
117
+ content: [{ type: "text" as const, text }],
118
+ details,
119
+ };
120
+ }
121
+
122
+ export async function withCdp<T>(page: DevToolsPage, callback: (client: CdpClient) => Promise<T>) {
123
+ if (!page.webSocketDebuggerUrl) throw new Error(`Page has no webSocketDebuggerUrl: ${page.id}`);
124
+
125
+ const client = await CdpClient.connect(page.webSocketDebuggerUrl);
126
+ try {
127
+ return await callback(client);
128
+ } finally {
129
+ client.close();
130
+ }
131
+ }
132
+
133
+ class CdpClient {
134
+ #nextId = 1;
135
+ #pending = new Map<
136
+ number,
137
+ {
138
+ resolve: (value: unknown) => void;
139
+ reject: (reason: unknown) => void;
140
+ timeout: NodeJS.Timeout;
141
+ }
142
+ >();
143
+ private readonly socket: WebSocket;
144
+
145
+ private constructor(socket: WebSocket) {
146
+ this.socket = socket;
147
+ socket.addEventListener("message", (event) => {
148
+ const response = JSON.parse(String(event.data)) as CdpResponse;
149
+ if (typeof response.id !== "number") return;
150
+
151
+ const pending = this.#pending.get(response.id);
152
+ if (!pending) return;
153
+
154
+ clearTimeout(pending.timeout);
155
+ this.#pending.delete(response.id);
156
+
157
+ if (response.error) {
158
+ pending.reject(new Error(`CDP error ${response.error.code}: ${response.error.message}`));
159
+ } else {
160
+ pending.resolve(response.result);
161
+ }
162
+ });
163
+
164
+ socket.addEventListener("close", () => {
165
+ this.rejectAll(new Error("Chrome DevTools WebSocket closed"));
166
+ });
167
+
168
+ socket.addEventListener("error", () => {
169
+ this.rejectAll(new Error("Chrome DevTools WebSocket error"));
170
+ });
171
+ }
172
+
173
+ static connect(url: string) {
174
+ return new Promise<CdpClient>((resolve, reject) => {
175
+ const socket = new WebSocket(url);
176
+ const timeout = setTimeout(() => {
177
+ socket.close();
178
+ reject(new Error(`Timed out connecting to Chrome DevTools WebSocket: ${url}`));
179
+ }, DEFAULT_TIMEOUT_MS);
180
+
181
+ socket.addEventListener("open", () => {
182
+ clearTimeout(timeout);
183
+ resolve(new CdpClient(socket));
184
+ });
185
+
186
+ socket.addEventListener("error", () => {
187
+ clearTimeout(timeout);
188
+ reject(new Error(`Failed to connect to Chrome DevTools WebSocket: ${url}`));
189
+ });
190
+ });
191
+ }
192
+
193
+ send<T = unknown>(method: string, params?: Record<string, unknown>) {
194
+ const id = this.#nextId;
195
+ this.#nextId += 1;
196
+
197
+ return new Promise<T>((resolve, reject) => {
198
+ const timeout = setTimeout(() => {
199
+ this.#pending.delete(id);
200
+ reject(new Error(`Timed out waiting for CDP response: ${method}`));
201
+ }, DEFAULT_TIMEOUT_MS);
202
+
203
+ this.#pending.set(id, { resolve: resolve as (value: unknown) => void, reject, timeout });
204
+ this.socket.send(JSON.stringify({ id, method, params: params ?? {} }));
205
+ });
206
+ }
207
+
208
+ close() {
209
+ this.socket.close();
210
+ }
211
+
212
+ private rejectAll(error: Error) {
213
+ for (const [id, pending] of this.#pending) {
214
+ clearTimeout(pending.timeout);
215
+ pending.reject(error);
216
+ this.#pending.delete(id);
217
+ }
218
+ }
219
+ }