Skip to content

Commit 28c393e

Browse files
authored
refactor: type browser core (#218)
1 parent 8a4ea41 commit 28c393e

6 files changed

Lines changed: 133 additions & 75 deletions

File tree

src/browser/cdp.ts

Lines changed: 54 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
*/
1010

1111
import { WebSocket, type RawData } from 'ws';
12-
import type { IPage } from '../types.js';
12+
import type { BrowserCookie, IPage, ScreenshotOptions, SnapshotOptions, WaitOptions } from '../types.js';
1313
import { wrapForEval } from './utils.js';
1414
import { generateSnapshotJs, scrollToRefJs, getFormStateJs } from './dom-snapshot.js';
1515
import {
@@ -29,13 +29,24 @@ export interface CDPTarget {
2929
webSocketDebuggerUrl?: string;
3030
}
3131

32+
interface RuntimeEvaluateResult {
33+
result?: {
34+
value?: unknown;
35+
};
36+
exceptionDetails?: {
37+
exception?: {
38+
description?: string;
39+
};
40+
};
41+
}
42+
3243
const CDP_SEND_TIMEOUT = 30_000; // 30s per command
3344

3445
export class CDPBridge {
3546
private _ws: WebSocket | null = null;
3647
private _idCounter = 0;
37-
private _pending = new Map<number, { resolve: (val: any) => void; reject: (err: Error) => void; timer: ReturnType<typeof setTimeout> }>();
38-
private _eventListeners = new Map<string, Set<(params: any) => void>>();
48+
private _pending = new Map<number, { resolve: (val: unknown) => void; reject: (err: Error) => void; timer: ReturnType<typeof setTimeout> }>();
49+
private _eventListeners = new Map<string, Set<(params: unknown) => void>>();
3950

4051
async connect(opts?: { timeout?: number; workspace?: string }): Promise<IPage> {
4152
const endpoint = process.env.OPENCLI_CDP_ENDPOINT;
@@ -112,7 +123,7 @@ export class CDPBridge {
112123
}
113124

114125
/** Send a CDP command with timeout guard (P0 fix #4) */
115-
async send(method: string, params: any = {}, timeoutMs: number = CDP_SEND_TIMEOUT): Promise<any> {
126+
async send(method: string, params: Record<string, unknown> = {}, timeoutMs: number = CDP_SEND_TIMEOUT): Promise<unknown> {
116127
if (!this._ws || this._ws.readyState !== WebSocket.OPEN) {
117128
throw new Error('CDP connection is not open');
118129
}
@@ -128,25 +139,25 @@ export class CDPBridge {
128139
}
129140

130141
/** Listen for a CDP event */
131-
on(event: string, handler: (params: any) => void): void {
142+
on(event: string, handler: (params: unknown) => void): void {
132143
let set = this._eventListeners.get(event);
133144
if (!set) { set = new Set(); this._eventListeners.set(event, set); }
134145
set.add(handler);
135146
}
136147

137148
/** Remove a CDP event listener */
138-
off(event: string, handler: (params: any) => void): void {
149+
off(event: string, handler: (params: unknown) => void): void {
139150
this._eventListeners.get(event)?.delete(handler);
140151
}
141152

142153
/** Wait for a CDP event to fire (one-shot) */
143-
waitForEvent(event: string, timeoutMs: number = 15_000): Promise<any> {
154+
waitForEvent(event: string, timeoutMs: number = 15_000): Promise<unknown> {
144155
return new Promise((resolve, reject) => {
145156
const timer = setTimeout(() => {
146157
this.off(event, handler);
147158
reject(new Error(`Timed out waiting for CDP event '${event}'`));
148159
}, timeoutMs);
149-
const handler = (params: any) => {
160+
const handler = (params: unknown) => {
150161
clearTimeout(timer);
151162
this.off(event, handler);
152163
resolve(params);
@@ -173,28 +184,29 @@ class CDPPage implements IPage {
173184
}
174185
}
175186

176-
async evaluate(js: string): Promise<any> {
187+
async evaluate(js: string): Promise<unknown> {
177188
const expression = wrapForEval(js);
178189
const result = await this.bridge.send('Runtime.evaluate', {
179190
expression,
180191
returnByValue: true,
181192
awaitPromise: true
182-
});
193+
}) as RuntimeEvaluateResult;
183194
if (result.exceptionDetails) {
184195
throw new Error('Evaluate error: ' + (result.exceptionDetails.exception?.description || 'Unknown exception'));
185196
}
186197
return result.result?.value;
187198
}
188199

189-
async getCookies(opts: { domain?: string; url?: string } = {}): Promise<any[]> {
200+
async getCookies(opts: { domain?: string; url?: string } = {}): Promise<BrowserCookie[]> {
190201
const result = await this.bridge.send('Network.getCookies', opts.url ? { urls: [opts.url] } : {});
191-
const cookies = Array.isArray(result?.cookies) ? result.cookies : [];
192-
return opts.domain
193-
? cookies.filter((cookie: any) => typeof cookie.domain === 'string' && cookie.domain.includes(opts.domain!))
202+
const cookies = isRecord(result) && Array.isArray(result.cookies) ? result.cookies : [];
203+
const domain = opts.domain;
204+
return domain
205+
? cookies.filter((cookie): cookie is BrowserCookie => isCookie(cookie) && cookie.domain.includes(domain))
194206
: cookies;
195207
}
196208

197-
async snapshot(opts: { interactive?: boolean; compact?: boolean; maxDepth?: number; raw?: boolean; viewportExpand?: number; maxTextLength?: number } = {}): Promise<any> {
209+
async snapshot(opts: SnapshotOptions = {}): Promise<unknown> {
198210
const snapshotJs = generateSnapshotJs({
199211
viewportExpand: opts.viewportExpand ?? 800,
200212
maxDepth: Math.max(1, Math.min(Number(opts.maxDepth) || 50, 200)),
@@ -220,21 +232,22 @@ class CDPPage implements IPage {
220232
await this.evaluate(pressKeyJs(key));
221233
}
222234

223-
async scrollTo(ref: string): Promise<any> {
235+
async scrollTo(ref: string): Promise<unknown> {
224236
return this.evaluate(scrollToRefJs(ref));
225237
}
226238

227-
async getFormState(): Promise<any> {
228-
return this.evaluate(getFormStateJs());
239+
async getFormState(): Promise<Record<string, unknown>> {
240+
return (await this.evaluate(getFormStateJs())) as Record<string, unknown>;
229241
}
230242

231-
async wait(options: any): Promise<void> {
243+
async wait(options: number | WaitOptions): Promise<void> {
232244
if (typeof options === 'number') {
233245
await new Promise(resolve => setTimeout(resolve, options * 1000));
234246
return;
235247
}
236-
if (options.time) {
237-
await new Promise(resolve => setTimeout(resolve, options.time * 1000));
248+
if (typeof options.time === 'number') {
249+
const waitTime = options.time;
250+
await new Promise(resolve => setTimeout(resolve, waitTime * 1000));
238251
return;
239252
}
240253
if (options.text) {
@@ -255,13 +268,13 @@ class CDPPage implements IPage {
255268
await this.evaluate(autoScrollJs(times, delayMs));
256269
}
257270

258-
async screenshot(options: any = {}): Promise<string> {
271+
async screenshot(options: ScreenshotOptions = {}): Promise<string> {
259272
const result = await this.bridge.send('Page.captureScreenshot', {
260273
format: options.format ?? 'png',
261274
quality: options.format === 'jpeg' ? (options.quality ?? 80) : undefined,
262275
captureBeyondViewport: options.fullPage ?? false,
263276
});
264-
const base64 = result.data;
277+
const base64 = isRecord(result) && typeof result.data === 'string' ? result.data : '';
265278
if (options.path) {
266279
const fs = await import('node:fs');
267280
const path = await import('node:path');
@@ -272,11 +285,12 @@ class CDPPage implements IPage {
272285
return base64;
273286
}
274287

275-
async networkRequests(includeStatic: boolean = false): Promise<any> {
276-
return this.evaluate(networkRequestsJs(includeStatic));
288+
async networkRequests(includeStatic: boolean = false): Promise<unknown[]> {
289+
const result = await this.evaluate(networkRequestsJs(includeStatic));
290+
return Array.isArray(result) ? result : [];
277291
}
278292

279-
async tabs(): Promise<any> {
293+
async tabs(): Promise<unknown[]> {
280294
return [];
281295
}
282296

@@ -292,7 +306,7 @@ class CDPPage implements IPage {
292306
// Not supported in direct CDP mode
293307
}
294308

295-
async consoleMessages(_level?: string): Promise<any> {
309+
async consoleMessages(_level?: string): Promise<unknown[]> {
296310
return [];
297311
}
298312

@@ -304,13 +318,24 @@ class CDPPage implements IPage {
304318
}));
305319
}
306320

307-
async getInterceptedRequests(): Promise<any[]> {
321+
async getInterceptedRequests(): Promise<unknown[]> {
308322
const { generateReadInterceptedJs } = await import('../interceptor.js');
309323
const result = await this.evaluate(generateReadInterceptedJs('__opencli_xhr'));
310-
return (result as any[]) || [];
324+
return Array.isArray(result) ? result : [];
311325
}
312326
}
313327

328+
function isRecord(value: unknown): value is Record<string, unknown> {
329+
return typeof value === 'object' && value !== null && !Array.isArray(value);
330+
}
331+
332+
function isCookie(value: unknown): value is BrowserCookie {
333+
return isRecord(value)
334+
&& typeof value.name === 'string'
335+
&& typeof value.value === 'string'
336+
&& typeof value.domain === 'string';
337+
}
338+
314339
// ── CDP target selection (unchanged) ──
315340

316341
function selectCDPTarget(targets: CDPTarget[]): CDPTarget | undefined {

src/browser/daemon-client.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,8 @@ export async function sendCommand(
113113
throw new Error('sendCommand: max retries exhausted');
114114
}
115115

116-
export async function listSessions(): Promise<any[]> {
116+
export async function listSessions(): Promise<BrowserSessionInfo[]> {
117117
const result = await sendCommand('sessions');
118118
return Array.isArray(result) ? result : [];
119119
}
120+
import type { BrowserSessionInfo } from '../types.js';

src/browser/page.ts

Lines changed: 19 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
*/
1212

1313
import { formatSnapshot } from '../snapshotFormatter.js';
14-
import type { IPage } from '../types.js';
14+
import type { BrowserCookie, IPage, ScreenshotOptions, SnapshotOptions, WaitOptions } from '../types.js';
1515
import { sendCommand } from './daemon-client.js';
1616
import { wrapForEval } from './utils.js';
1717
import { generateSnapshotJs, scrollToRefJs, getFormStateJs } from './dom-snapshot.js';
@@ -70,17 +70,17 @@ export class Page implements IPage {
7070
}
7171
}
7272

73-
async evaluate(js: string): Promise<any> {
73+
async evaluate(js: string): Promise<unknown> {
7474
const code = wrapForEval(js);
7575
return sendCommand('exec', { code, ...this._workspaceOpt(), ...this._tabOpt() });
7676
}
7777

78-
async getCookies(opts: { domain?: string; url?: string } = {}): Promise<any[]> {
78+
async getCookies(opts: { domain?: string; url?: string } = {}): Promise<BrowserCookie[]> {
7979
const result = await sendCommand('cookies', { ...this._workspaceOpt(), ...opts });
8080
return Array.isArray(result) ? result : [];
8181
}
8282

83-
async snapshot(opts: { interactive?: boolean; compact?: boolean; maxDepth?: number; raw?: boolean; viewportExpand?: number; maxTextLength?: number } = {}): Promise<any> {
83+
async snapshot(opts: SnapshotOptions = {}): Promise<unknown> {
8484
// Primary: use the advanced DOM snapshot engine with multi-layer pruning
8585
const snapshotJs = generateSnapshotJs({
8686
viewportExpand: opts.viewportExpand ?? 800,
@@ -103,7 +103,7 @@ export class Page implements IPage {
103103
}
104104

105105
/** Fallback basic snapshot — original buildTree approach */
106-
private async _basicSnapshot(opts: { interactive?: boolean; compact?: boolean; maxDepth?: number; raw?: boolean } = {}): Promise<any> {
106+
private async _basicSnapshot(opts: Pick<SnapshotOptions, 'interactive' | 'compact' | 'maxDepth' | 'raw'> = {}): Promise<unknown> {
107107
const maxDepth = Math.max(1, Math.min(Number(opts.maxDepth) || 50, 200));
108108
const code = `
109109
(async () => {
@@ -153,17 +153,17 @@ export class Page implements IPage {
153153
await sendCommand('exec', { code, ...this._workspaceOpt(), ...this._tabOpt() });
154154
}
155155

156-
async scrollTo(ref: string): Promise<any> {
156+
async scrollTo(ref: string): Promise<unknown> {
157157
const code = scrollToRefJs(ref);
158158
return sendCommand('exec', { code, ...this._workspaceOpt(), ...this._tabOpt() });
159159
}
160160

161-
async getFormState(): Promise<any> {
161+
async getFormState(): Promise<Record<string, unknown>> {
162162
const code = getFormStateJs();
163-
return sendCommand('exec', { code, ...this._workspaceOpt(), ...this._tabOpt() });
163+
return (await sendCommand('exec', { code, ...this._workspaceOpt(), ...this._tabOpt() })) as Record<string, unknown>;
164164
}
165165

166-
async wait(options: number | { text?: string; time?: number; timeout?: number }): Promise<void> {
166+
async wait(options: number | WaitOptions): Promise<void> {
167167
if (typeof options === 'number') {
168168
await new Promise(resolve => setTimeout(resolve, options * 1000));
169169
return;
@@ -179,8 +179,9 @@ export class Page implements IPage {
179179
}
180180
}
181181

182-
async tabs(): Promise<any> {
183-
return sendCommand('tabs', { op: 'list', ...this._workspaceOpt() });
182+
async tabs(): Promise<unknown[]> {
183+
const result = await sendCommand('tabs', { op: 'list', ...this._workspaceOpt() });
184+
return Array.isArray(result) ? result : [];
184185
}
185186

186187
async closeTab(index?: number): Promise<void> {
@@ -195,17 +196,18 @@ export class Page implements IPage {
195196
await sendCommand('tabs', { op: 'select', index, ...this._workspaceOpt() });
196197
}
197198

198-
async networkRequests(includeStatic: boolean = false): Promise<any> {
199+
async networkRequests(includeStatic: boolean = false): Promise<unknown[]> {
199200
const code = networkRequestsJs(includeStatic);
200-
return sendCommand('exec', { code, ...this._workspaceOpt(), ...this._tabOpt() });
201+
const result = await sendCommand('exec', { code, ...this._workspaceOpt(), ...this._tabOpt() });
202+
return Array.isArray(result) ? result : [];
201203
}
202204

203205
/**
204206
* Console messages are not available in lightweight daemon mode.
205207
* Would require CDP Runtime.consoleAPICalled event listener.
206208
* @returns Always returns empty array.
207209
*/
208-
async consoleMessages(_level: string = 'info'): Promise<any> {
210+
async consoleMessages(_level: string = 'info'): Promise<unknown[]> {
209211
return [];
210212
}
211213

@@ -216,12 +218,7 @@ export class Page implements IPage {
216218
* @param options.fullPage - capture full scrollable page
217219
* @param options.path - save to file path (returns base64 if omitted)
218220
*/
219-
async screenshot(options: {
220-
format?: 'png' | 'jpeg';
221-
quality?: number;
222-
fullPage?: boolean;
223-
path?: string;
224-
} = {}): Promise<string> {
221+
async screenshot(options: ScreenshotOptions = {}): Promise<string> {
225222
const base64 = await sendCommand('screenshot', {
226223
...this._workspaceOpt(),
227224
format: options.format,
@@ -263,11 +260,11 @@ export class Page implements IPage {
263260
}));
264261
}
265262

266-
async getInterceptedRequests(): Promise<any[]> {
263+
async getInterceptedRequests(): Promise<unknown[]> {
267264
const { generateReadInterceptedJs } = await import('../interceptor.js');
268265
// Same as installInterceptor: must go through evaluate() for IIFE wrapping
269266
const result = await this.evaluate(generateReadInterceptedJs('__opencli_xhr'));
270-
return (result as any[]) || [];
267+
return Array.isArray(result) ? result : [];
271268
}
272269
}
273270

src/explore.ts

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -223,6 +223,10 @@ function flattenFields(obj: unknown, prefix: string, maxDepth: number): string[]
223223
return names;
224224
}
225225

226+
function isBooleanRecord(value: unknown): value is Record<string, boolean> {
227+
return typeof value === 'object' && value !== null && !Array.isArray(value);
228+
}
229+
226230
function scoreEndpoint(ep: { contentType: string; responseAnalysis: AnalyzedEndpoint['responseAnalysis']; pattern: string; status: number | null; hasSearchParam: boolean; hasPaginationParam: boolean; hasLimitParam: boolean }): number {
227231
let s = 0;
228232
if (ep.contentType.includes('json')) s += 10;
@@ -488,7 +492,10 @@ export async function exploreUrl(
488492

489493
// Step 6: Detect framework
490494
let framework: Record<string, boolean> = {};
491-
try { const fw = await page.evaluate(FRAMEWORK_DETECT_JS); if (fw && typeof fw === 'object') framework = fw; } catch {}
495+
try {
496+
const fw = await page.evaluate(FRAMEWORK_DETECT_JS);
497+
if (isBooleanRecord(fw)) framework = fw;
498+
} catch {}
492499

493500
// Step 6.5: Discover stores (Pinia / Vuex)
494501
let stores: DiscoveredStore[] = [];
@@ -551,7 +558,12 @@ export function renderExploreSummary(result: ExploreResult): string {
551558
async function readPageMetadata(page: IPage): Promise<{ url: string; title: string }> {
552559
try {
553560
const result = await page.evaluate(`() => ({ url: window.location.href, title: document.title || '' })`);
554-
if (result && typeof result === 'object') return { url: String(result.url ?? ''), title: String(result.title ?? '') };
561+
if (result && typeof result === 'object' && !Array.isArray(result)) {
562+
return {
563+
url: String((result as Record<string, unknown>).url ?? ''),
564+
title: String((result as Record<string, unknown>).title ?? ''),
565+
};
566+
}
555567
} catch {}
556568
return { url: '', title: '' };
557569
}

src/pipeline/steps/fetch.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ async function fetchBatchInBrowser(
7272
): Promise<unknown[]> {
7373
const headersJs = JSON.stringify(headers);
7474
const urlsJs = JSON.stringify(urls);
75-
return page.evaluate(`
75+
return (await page.evaluate(`
7676
async () => {
7777
const urls = ${urlsJs};
7878
const method = "${method}";
@@ -98,7 +98,7 @@ async function fetchBatchInBrowser(
9898
await Promise.all(workers);
9999
return results;
100100
}
101-
`);
101+
`)) as unknown[];
102102
}
103103

104104
export async function stepFetch(page: IPage | null, params: unknown, data: unknown, args: Record<string, unknown>): Promise<unknown> {

0 commit comments

Comments
 (0)