Skip to content

Commit 059ab14

Browse files
authored
Clean playwright-server.js (#17)
extract utils and handlers to reduce code duplication and improve maintanability
1 parent e746a92 commit 059ab14

File tree

3 files changed

+784
-1057
lines changed

3 files changed

+784
-1057
lines changed

bin/lib/core.js

Lines changed: 247 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,247 @@
1+
const fs = require('fs');
2+
const path = require('path');
3+
4+
const DEBUG = process.env.PLAYWRIGHT_DEBUG === 'true';
5+
const DEBUG_LEVEL = process.env.PLAYWRIGHT_DEBUG_LEVEL || 'info';
6+
const DEBUG_LOG_FILE = process.env.PLAYWRIGHT_DEBUG_LOG || 'debug-dispatch.log';
7+
const DEBUG_LOG_DIR = process.env.PLAYWRIGHT_DEBUG_DIR || process.cwd();
8+
9+
class Logger {
10+
constructor() {
11+
this.logPath = path.join(DEBUG_LOG_DIR, DEBUG_LOG_FILE);
12+
this.enabled = DEBUG;
13+
this.level = DEBUG_LEVEL.toLowerCase();
14+
this.levels = { 'error': 0, 'warn': 1, 'info': 2, 'debug': 3 };
15+
this.currentLevelPriority = this.levels[this.level] ?? this.levels['info'];
16+
this.logStream = null;
17+
this.initConsoleLogging();
18+
}
19+
20+
initConsoleLogging() {
21+
if (DEBUG) {
22+
this.logStream = fs.createWriteStream('playwright-server.log', {flags: 'a'});
23+
const originalConsoleLog = console.log;
24+
console.log = (d) => {
25+
this.logStream.write(new Date().toISOString() + ' - ' + d + '\n');
26+
originalConsoleLog(d);
27+
};
28+
}
29+
}
30+
31+
shouldLog(level) {
32+
if (!this.enabled) return false;
33+
const levelPriority = this.levels[level] ?? this.levels['info'];
34+
return levelPriority <= this.currentLevelPriority;
35+
}
36+
37+
log(message, data = null, level = 'info') {
38+
if (!this.shouldLog(level)) return;
39+
const timestamp = new Date().toISOString();
40+
let logMessage = `[${timestamp}] [${level.toUpperCase()}] ${message}`;
41+
if (data) {
42+
if (typeof data === 'object') {
43+
try {
44+
logMessage += `\n${JSON.stringify(data, null, 2)}`;
45+
} catch (e) {
46+
logMessage += ` [Object: ${String(data)}]`;
47+
}
48+
} else {
49+
logMessage += ` ${data}`;
50+
}
51+
}
52+
logMessage += '\n';
53+
try {
54+
fs.appendFileSync(this.logPath, logMessage);
55+
} catch (error) {}
56+
}
57+
58+
error(message, data = null) { this.log(message, data, 'error'); }
59+
warn(message, data = null) { this.log(message, data, 'warn'); }
60+
info(message, data = null) { this.log(message, data, 'info'); }
61+
debug(message, data = null) { this.log(message, data, 'debug'); }
62+
separator(title) { this.log(`=== ${title} ===`, null, 'info'); }
63+
close() { if (this.logStream) this.logStream.end(); }
64+
}
65+
66+
class ErrorHandler {
67+
static formatError(error, command, requestId) {
68+
const message = error instanceof Error ? error.message : String(error);
69+
logger.error('Command execution failed', {
70+
command: command?.action, error: message, stack: error.stack, requestId, pageId: command?.pageId
71+
});
72+
return {
73+
requestId, error: message,
74+
stack: process.env.PLAYWRIGHT_DEBUG === 'true' ? error.stack : undefined,
75+
command: command?.action
76+
};
77+
}
78+
79+
static async safeExecute(fn, context = {}) {
80+
try {
81+
return await fn();
82+
} catch (error) {
83+
logger.error('Safe execution failed', { error: error.message, stack: error.stack, context });
84+
throw error;
85+
}
86+
}
87+
88+
static wrapHandler(handler) {
89+
return async (...args) => {
90+
try {
91+
return await handler(...args);
92+
} catch (error) {
93+
logger.error('Handler error', { error: error.message, stack: error.stack, handler: handler.name });
94+
throw error;
95+
}
96+
};
97+
}
98+
}
99+
100+
class LspFraming {
101+
static encode(content) {
102+
const contentLength = Buffer.byteLength(content, 'utf8');
103+
return `Content-Length: ${contentLength}\r\n\r\n${content}`;
104+
}
105+
106+
static decode(buffer) {
107+
const messages = [];
108+
let remaining = buffer;
109+
while (true) {
110+
const result = this.extractOneMessage(remaining);
111+
if (!result) break;
112+
const [message, newRemaining] = result;
113+
messages.push(message);
114+
remaining = newRemaining;
115+
}
116+
return { messages, remainingBuffer: remaining };
117+
}
118+
119+
static extractOneMessage(buffer) {
120+
const headerEndPos = buffer.indexOf('\r\n\r\n');
121+
if (headerEndPos === -1) return null;
122+
const headers = buffer.slice(0, headerEndPos).toString('utf8');
123+
const contentStart = headerEndPos + 4;
124+
const contentLength = this.parseContentLength(headers);
125+
if (contentLength === null) throw new Error('Missing or invalid Content-Length header');
126+
if (buffer.length < contentStart + contentLength) return null;
127+
const content = buffer.slice(contentStart, contentStart + contentLength).toString('utf8');
128+
const remaining = buffer.slice(contentStart + contentLength);
129+
return [content, remaining];
130+
}
131+
132+
static parseContentLength(headers) {
133+
const lines = headers.split('\r\n');
134+
for (const line of lines) {
135+
const match = line.trim().match(/^Content-Length:\s*(\d+)$/i);
136+
if (match) return parseInt(match[1], 10);
137+
}
138+
return null;
139+
}
140+
}
141+
142+
const sendFramedResponse = (data) => {
143+
const json = JSON.stringify(data);
144+
const framed = LspFraming.encode(json);
145+
process.stdout.write(framed);
146+
};
147+
148+
class CommandRegistry {
149+
constructor() { this.handlers = new Map(); }
150+
register(name, handler) { this.handlers.set(name, handler); return this; }
151+
async execute(name, ...args) {
152+
const handler = this.handlers.get(name);
153+
if (!handler) throw new Error(`Unknown action: ${name}`);
154+
return await handler(...args);
155+
}
156+
has(name) { return this.handlers.has(name); }
157+
static create(handlerMap) {
158+
const registry = new CommandRegistry();
159+
Object.entries(handlerMap).forEach(([name, handler]) => registry.register(name, handler));
160+
return registry;
161+
}
162+
}
163+
164+
class BaseHandler {
165+
constructor(deps = {}) { Object.assign(this, deps); }
166+
validateResource(resourceMap, resourceId, resourceType) {
167+
const resource = resourceMap.get(resourceId);
168+
if (!resource) throw new Error(`${resourceType} not found: ${resourceId}`);
169+
return resource;
170+
}
171+
wrapResult(value) { return value === undefined || value === null ? { success: true } : value; }
172+
createValueResult(value) { return { value }; }
173+
async executeWithRegistry(registry, method, ...args) { return await registry.execute(method, ...args); }
174+
}
175+
176+
class PromiseUtils {
177+
static wrapValue(promise) { return promise.then(value => ({ value })); }
178+
static wrapValues(promise) { return promise.then(values => ({ values })); }
179+
static wrapBinary(promise) { return promise.then(buffer => ({ binary: buffer.toString('base64') })); }
180+
}
181+
182+
class FrameUtils {
183+
static resolve(page, chain) {
184+
if (!chain || chain === ':root') return page;
185+
const parts = String(chain).split(' >> ').filter(Boolean);
186+
let fl = page;
187+
for (const part of parts) fl = fl.frameLocator(part);
188+
return fl;
189+
}
190+
191+
static async evaluateInFrame(page, frameLocator, isMainFrame, expression) {
192+
if (isMainFrame) return await page.evaluate(expression);
193+
const loc = frameLocator.locator('html');
194+
const count = await loc.count();
195+
if (count === 0) return null;
196+
return await loc.evaluate(expression);
197+
}
198+
199+
static async waitForReadyState(evalInFrame, state, timeoutMs) {
200+
const deadline = Date.now() + timeoutMs;
201+
const target = state || 'load';
202+
const predicate = async () => {
203+
const readyState = await evalInFrame(() => document.readyState);
204+
if (readyState === null) return false;
205+
if (target === 'load') return readyState === 'complete';
206+
if (target === 'domcontentloaded') return readyState === 'interactive' || readyState === 'complete';
207+
if (target === 'networkidle') return readyState === 'complete';
208+
return readyState === 'complete';
209+
};
210+
while (Date.now() < deadline) {
211+
try { if (await predicate()) return; } catch {}
212+
await new Promise(r => setTimeout(r, 50));
213+
}
214+
throw new Error(`Timeout waiting for load state: ${state || 'load'}`);
215+
}
216+
}
217+
218+
class RouteUtils {
219+
static async setupContextRoute(context, command, generateId, routes, extractRequestData, sendFramedResponse) {
220+
await context.route(command.url, async (route) => {
221+
const routeId = generateId('route');
222+
routes.set(routeId, { route, contextId: command.contextId });
223+
const req = route.request();
224+
const requestData = extractRequestData(req);
225+
logger.info('ROUTE SETUP', { url: requestData.url, method: requestData.method, type: 'context' });
226+
sendFramedResponse({ objectId: command.contextId, event: 'route', params: { routeId, request: requestData } });
227+
});
228+
return { success: true };
229+
}
230+
231+
static async setupPageRoute(page, command, generateId, routes, extractRequestData, sendFramedResponse, routeCounter) {
232+
await page.route(command.url, async (route) => {
233+
const routeId = `route_${++routeCounter.value}`;
234+
routes.set(routeId, { route, contextId: command.pageId });
235+
const req = route.request();
236+
logger.info('ROUTE SETUP', { url: req.url(), method: req.method(), type: 'page' });
237+
sendFramedResponse({ objectId: command.pageId, event: 'route', params: { routeId, request: extractRequestData(req) } });
238+
});
239+
}
240+
}
241+
242+
const logger = new Logger();
243+
244+
module.exports = {
245+
logger, ErrorHandler, LspFraming, sendFramedResponse,
246+
CommandRegistry, BaseHandler, PromiseUtils, FrameUtils, RouteUtils
247+
};

0 commit comments

Comments
 (0)