bunmicro 0.9.25 → 1.0.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,1161 @@
1
+ const CDP_DEFAULT_PORT = 9222;
2
+ const CDP_PROTOCOL_VERSION = "1.3";
3
+ const CDP_BROWSER_ID = "cdp-server";
4
+
5
+ let serverLogEnabled = 0//true;
6
+ function serverLog(...args) {
7
+ if (serverLogEnabled) console.log(...args);
8
+ }
9
+ function serverError(...args) {
10
+ if (serverLogEnabled) console.error(...args);
11
+ }
12
+
13
+ export class CdpServer {
14
+ #context;
15
+
16
+ constructor(context) {
17
+ this.#context = context;
18
+ }
19
+
20
+ static create(context) {
21
+ return new CdpServer(context);
22
+ }
23
+
24
+ listen(port = CDP_DEFAULT_PORT, hostname = "127.0.0.1") {
25
+ const context = this.#context;
26
+ const state = createCdpState();
27
+
28
+ const server = Bun.serve({
29
+ port,
30
+ hostname,
31
+ fetch(req, server) {
32
+ const url = new URL(req.url);
33
+ const pathname = url.pathname.replace(/\/+$/, "") || "/";
34
+ serverLog(`[CdpServer] HTTP ${req.method} ${pathname}`);
35
+ if (server.upgrade(req)) return;
36
+
37
+ const host = req.headers.get("host") ?? `127.0.0.1:${port}`;
38
+ const webSocketDebuggerUrl = `ws://${host}/devtools/browser/${CDP_BROWSER_ID}`;
39
+
40
+ if (pathname === "/json/version") {
41
+ return jsonResponse({
42
+ Browser: "CdpServer/1.0",
43
+ "Protocol-Version": CDP_PROTOCOL_VERSION,
44
+ "User-Agent": "CdpServer/1.0",
45
+ "V8-Version": Bun.version,
46
+ "WebKit-Version": "537.36",
47
+ webSocketDebuggerUrl,
48
+ });
49
+ }
50
+
51
+ if (pathname === "/json" || pathname === "/json/list") {
52
+ return jsonResponse(
53
+ [...state.targets.values()].map((target) => ({
54
+ id: target.targetId,
55
+ type: target.type,
56
+ title: target.title,
57
+ url: target.url,
58
+ webSocketDebuggerUrl,
59
+ }))
60
+ );
61
+ }
62
+
63
+ return jsonResponse({
64
+ server: "CdpServer",
65
+ port,
66
+ webSocketDebuggerUrl,
67
+ });
68
+ },
69
+ websocket: {
70
+ open(ws) {
71
+ serverLog("[CdpServer] client connected");
72
+ },
73
+ async message(ws, raw) {
74
+ let id;
75
+ let sessionId;
76
+ try {
77
+ const text = typeof raw === "string" ? raw : new TextDecoder().decode(raw);
78
+ const msg = JSON.parse(text);
79
+ id = msg.id;
80
+ sessionId = msg.sessionId;
81
+ const { method, params } = msg;
82
+ serverLog(`[CdpServer] -> ${method}`, params ?? "");
83
+
84
+ const emit = (eventMethod, eventParams) => {
85
+ ws.send(
86
+ JSON.stringify({
87
+ method: eventMethod,
88
+ params: eventParams,
89
+ ...(sessionId ? { sessionId } : {}),
90
+ })
91
+ );
92
+ };
93
+ const result = await dispatch(
94
+ context,
95
+ state,
96
+ method,
97
+ params ?? {},
98
+ sessionId,
99
+ emit
100
+ );
101
+ serverLog(`[CdpServer] <- ${method} OK`, result ?? "");
102
+ ws.send(
103
+ JSON.stringify({
104
+ id,
105
+ result: result ?? {},
106
+ ...(sessionId ? { sessionId } : {}),
107
+ })
108
+ );
109
+ } catch (err) {
110
+ serverError(`[CdpServer] <- ERROR ${err.message}`);
111
+ ws.send(
112
+ JSON.stringify({
113
+ id,
114
+ error: {
115
+ code: -32601,
116
+ message: err.message,
117
+ },
118
+ ...(sessionId ? { sessionId } : {}),
119
+ })
120
+ );
121
+ }
122
+ },
123
+ close(ws) {
124
+ serverLog("[CdpServer] client disconnected");
125
+ },
126
+ },
127
+ });
128
+
129
+ serverLog(`[CdpServer] listening on ws://${hostname}:${port}`);
130
+ return server;
131
+ }
132
+ }
133
+
134
+ function jsonResponse(value, status = 200) {
135
+ return new Response(JSON.stringify(value), {
136
+ status,
137
+ headers: { "Content-Type": "application/json" },
138
+ });
139
+ }
140
+
141
+ function createCdpState() {
142
+ return {
143
+ nextTargetId: 1,
144
+ nextSessionId: 1,
145
+ nextLoaderId: 1,
146
+ nextHistoryEntryId: 1,
147
+ nextObjectId: 1,
148
+ nextExecutionContextId: 1,
149
+ targets: new Map(),
150
+ sessions: new Map(),
151
+ objects: new Map(),
152
+ initialNavigations: new Map(),
153
+ implicitTarget: null,
154
+ autoAttach: false,
155
+ waitForDebuggerOnStart: false,
156
+ };
157
+ }
158
+
159
+ async function dispatch(ctx, state, method, params, sessionId, emit) {
160
+ switch (method) {
161
+ case "Browser.getVersion":
162
+ return {
163
+ protocolVersion: CDP_PROTOCOL_VERSION,
164
+ product: "Chrome/140.0.0.0",
165
+ revision: "cdp-server",
166
+ userAgent:
167
+ "Mozilla/5.0 AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.0.0 Safari/537.36",
168
+ jsVersion: Bun.version,
169
+ };
170
+ case "Browser.setDownloadBehavior":
171
+ return {};
172
+ case "Target.getBrowserContexts":
173
+ return {
174
+ browserContextIds: [],
175
+ defaultBrowserContextId: "default",
176
+ };
177
+ case "Target.setAutoAttach":
178
+ state.autoAttach = params.autoAttach;
179
+ state.waitForDebuggerOnStart = params.waitForDebuggerOnStart;
180
+ return {};
181
+ case "Target.setDiscoverTargets":
182
+ return {};
183
+ case "Target.createTarget": {
184
+ const targetId = `target-${state.nextTargetId++}`;
185
+ state.initialNavigations.set(targetId, {
186
+ navigated: createDeferred(),
187
+ settled: createDeferred(),
188
+ });
189
+ state.targets.set(targetId, {
190
+ targetId,
191
+ frameId: targetId,
192
+ loaderId: `loader-${state.nextLoaderId++}`,
193
+ type: "page",
194
+ browserContextId: params.browserContextId ?? "default",
195
+ title: "",
196
+ url: params.url || "about:blank",
197
+ attached: false,
198
+ elements: new Map(),
199
+ focusedSelector: null,
200
+ pendingSelector: null,
201
+ history: [
202
+ {
203
+ id: state.nextHistoryEntryId++,
204
+ url: params.url || "about:blank",
205
+ title: "",
206
+ },
207
+ ],
208
+ historyIndex: 0,
209
+ viewport: {
210
+ width: params.width ?? 800,
211
+ height: params.height ?? 600,
212
+ },
213
+ });
214
+
215
+ if (state.autoAttach) {
216
+ const attachedSessionId = `session-${state.nextSessionId++}`;
217
+ const target = state.targets.get(targetId);
218
+ target.attached = true;
219
+ state.sessions.set(attachedSessionId, targetId);
220
+ emit("Target.attachedToTarget", {
221
+ sessionId: attachedSessionId,
222
+ targetInfo: createTargetInfo(target),
223
+ waitingForDebugger: state.waitForDebuggerOnStart,
224
+ });
225
+ }
226
+
227
+ return { targetId };
228
+ }
229
+ case "Target.attachToTarget": {
230
+ const target = state.targets.get(params.targetId);
231
+ if (!target) throw new Error(`No target with given id: ${params.targetId}`);
232
+
233
+ const sessionId = `session-${state.nextSessionId++}`;
234
+ target.attached = true;
235
+ state.sessions.set(sessionId, params.targetId);
236
+ return { sessionId };
237
+ }
238
+ case "Target.getTargets":
239
+ return { targetInfos: [...state.targets.values()] };
240
+ case "Target.getTargetInfo": {
241
+ if (!params.targetId) {
242
+ return {
243
+ targetInfo: {
244
+ targetId: CDP_BROWSER_ID,
245
+ type: "browser",
246
+ title: "",
247
+ url: "",
248
+ attached: true,
249
+ },
250
+ };
251
+ }
252
+ const target = state.targets.get(params.targetId);
253
+ if (!target) throw new Error(`No target with given id: ${params.targetId}`);
254
+ return { targetInfo: target };
255
+ }
256
+ case "Target.closeTarget": {
257
+ const target = state.targets.get(params.targetId);
258
+ if (!target) return { success: false };
259
+
260
+ await ctx.close?.();
261
+ state.initialNavigations.get(params.targetId)?.navigated.resolve();
262
+ state.initialNavigations.get(params.targetId)?.settled.resolve();
263
+ state.initialNavigations.delete(params.targetId);
264
+
265
+ for (const [attachedSessionId, targetId] of state.sessions) {
266
+ if (targetId !== params.targetId) continue;
267
+ state.sessions.delete(attachedSessionId);
268
+ emit("Target.detachedFromTarget", {
269
+ sessionId: attachedSessionId,
270
+ targetId,
271
+ });
272
+ }
273
+ state.targets.delete(params.targetId);
274
+ emit("Target.targetDestroyed", { targetId: params.targetId });
275
+ return { success: true };
276
+ }
277
+ case "Target.detachFromTarget":
278
+ state.sessions.delete(params.sessionId);
279
+ return {};
280
+ case "Page.enable":
281
+ return {};
282
+ case "Page.getFrameTree": {
283
+ const target = getSessionTarget(state, sessionId);
284
+ return {
285
+ frameTree: {
286
+ frame: createFrame(target),
287
+ },
288
+ };
289
+ }
290
+ case "Page.setLifecycleEventsEnabled":
291
+ case "Log.enable":
292
+ case "Network.enable":
293
+ case "Emulation.setFocusEmulationEnabled":
294
+ case "Emulation.setEmulatedMedia":
295
+ case "Runtime.runIfWaitingForDebugger":
296
+ return {};
297
+ case "Page.addScriptToEvaluateOnNewDocument":
298
+ return {
299
+ identifier: `script-${state.nextObjectId++}`,
300
+ };
301
+ case "Page.createIsolatedWorld": {
302
+ const target = getSessionTarget(state, sessionId);
303
+ const executionContextId = state.nextExecutionContextId++;
304
+ target.utilityWorldName = params.worldName ?? "";
305
+ emit("Runtime.executionContextCreated", {
306
+ context: {
307
+ id: executionContextId,
308
+ origin: getOrigin(target.url),
309
+ name: target.utilityWorldName,
310
+ uniqueId: `context-${target.targetId}-${executionContextId}`,
311
+ auxData: {
312
+ isDefault: false,
313
+ type: "isolated",
314
+ frameId: target.frameId,
315
+ },
316
+ },
317
+ });
318
+ return { executionContextId };
319
+ }
320
+ case "Runtime.enable": {
321
+ const target = getSessionTarget(state, sessionId);
322
+ const executionContextId = state.nextExecutionContextId++;
323
+ target.mainExecutionContextId = executionContextId;
324
+ setTimeout(() => {
325
+ emit("Runtime.executionContextCreated", {
326
+ context: {
327
+ id: executionContextId,
328
+ origin: getOrigin(target.url),
329
+ name: "",
330
+ uniqueId: `context-${target.targetId}`,
331
+ auxData: {
332
+ isDefault: true,
333
+ type: "default",
334
+ frameId: target.frameId,
335
+ },
336
+ },
337
+ });
338
+ }, 0);
339
+ return {};
340
+ }
341
+ case "Page.navigate": {
342
+ const target = getSessionTarget(state, sessionId);
343
+ await ctx.navigate?.(params.url);
344
+ navigateTarget(state, target, params.url, emit, { addHistory: true });
345
+ state.initialNavigations.get(target.targetId)?.navigated.resolve();
346
+
347
+ return {
348
+ frameId: target.frameId,
349
+ loaderId: target.loaderId,
350
+ };
351
+ }
352
+ case "Page.getNavigationHistory": {
353
+ const target = getSessionTarget(state, sessionId);
354
+ return {
355
+ currentIndex: target.historyIndex,
356
+ entries: target.history.map((entry) => ({
357
+ id: entry.id,
358
+ url: entry.url,
359
+ userTypedURL: entry.url,
360
+ title: entry.title,
361
+ transitionType: "typed",
362
+ })),
363
+ };
364
+ }
365
+ case "Page.navigateToHistoryEntry": {
366
+ const target = getSessionTarget(state, sessionId);
367
+ const nextIndex = target.history.findIndex(
368
+ (entry) => entry.id === params.entryId
369
+ );
370
+ if (nextIndex < 0) {
371
+ throw new Error(`No history entry with given id: ${params.entryId}`);
372
+ }
373
+
374
+ if (nextIndex < target.historyIndex) {
375
+ await ctx.goBack?.();
376
+ } else if (nextIndex > target.historyIndex) {
377
+ await ctx.goForward?.();
378
+ }
379
+
380
+ target.historyIndex = nextIndex;
381
+ const entry = target.history[nextIndex];
382
+ navigateTarget(state, target, entry.url, emit);
383
+ return {};
384
+ }
385
+ case "Page.reload": {
386
+ const target = getSessionTarget(state, sessionId);
387
+ await ctx.reload?.();
388
+ navigateTarget(state, target, target.url, emit);
389
+ return {};
390
+ }
391
+ case "Emulation.setDeviceMetricsOverride": {
392
+ const target = getSessionTarget(state, sessionId);
393
+ target.viewport = {
394
+ width: params.width,
395
+ height: params.height,
396
+ };
397
+ await ctx.resize?.(params.width, params.height);
398
+ return {};
399
+ }
400
+ case "DOM.scrollIntoViewIfNeeded": {
401
+ const element = state.objects.get(params.objectId);
402
+ if (element?.kind !== "playwright-element") {
403
+ throw new Error(`Unknown element object: ${params.objectId}`);
404
+ }
405
+ await ctx.scrollTo?.(element.selector, {
406
+ block: "nearest",
407
+ });
408
+ return {};
409
+ }
410
+ case "DOM.getContentQuads": {
411
+ const target = getSessionTarget(state, sessionId);
412
+ const element = state.objects.get(params.objectId);
413
+ if (element?.kind !== "playwright-element") {
414
+ throw new Error(`Unknown element object: ${params.objectId}`);
415
+ }
416
+ target.pendingSelector = element.selector;
417
+ return {
418
+ quads: [[80, 40, 120, 40, 120, 60, 80, 60]],
419
+ };
420
+ }
421
+ case "Runtime.evaluate": {
422
+ let target;
423
+ if (sessionId) {
424
+ target = getSessionTarget(state, sessionId);
425
+ } else {
426
+ const pendingTarget = getPendingInitialNavigationTarget(state);
427
+ if (pendingTarget) {
428
+ await state.initialNavigations.get(pendingTarget.targetId).settled.promise;
429
+ target = state.targets.get(pendingTarget.targetId);
430
+ if (!target) throw new Error("WebView closed");
431
+ } else {
432
+ target = getImplicitTarget(state);
433
+ }
434
+ }
435
+ let value;
436
+
437
+ if (params.expression.trim() === "document.title") {
438
+ value = await getContextTitle(ctx, target);
439
+ const initialNavigation = state.initialNavigations.get(target.targetId);
440
+ if (initialNavigation) {
441
+ setTimeout(() => {
442
+ initialNavigation.settled.resolve();
443
+ state.initialNavigations.delete(target.targetId);
444
+ }, 0);
445
+ }
446
+ } else if (params.expression.includes("new (module.exports.InjectedScript())")) {
447
+ value = {
448
+ kind: "playwright-injected-script",
449
+ targetId: target.targetId,
450
+ };
451
+ } else if (params.expression.includes("new (module.exports.UtilityScript())")) {
452
+ value = {
453
+ kind: "playwright-utility-script",
454
+ targetId: target.targetId,
455
+ };
456
+ } else if (
457
+ params.expression.includes("timeout waiting for") &&
458
+ params.expression.includes("getBoundingClientRect")
459
+ ) {
460
+ target.pendingSelector = getActionabilitySelector(params.expression);
461
+ value = [100, 50];
462
+ } else if (
463
+ params.expression.includes("timeout waiting for") &&
464
+ params.expression.includes("scrollIntoView")
465
+ ) {
466
+ const scrollTo = getScrollToArguments(params.expression);
467
+ await ctx.scrollTo?.(scrollTo.selector, {
468
+ block: scrollTo.block,
469
+ timeout: scrollTo.timeout,
470
+ });
471
+ value = undefined;
472
+ } else {
473
+ if (shouldUseContextEvaluateResult(ctx)) {
474
+ value = await ctx.evaluate(params.expression);
475
+ } else {
476
+ value = await evaluateInTarget(target, params.expression);
477
+ await ctx.evaluate?.(params.expression);
478
+ }
479
+ }
480
+
481
+ return {
482
+ result: createRemoteObject(state, value, params.returnByValue),
483
+ };
484
+ }
485
+ case "Runtime.callFunctionOn": {
486
+ const target = getSessionTarget(state, sessionId);
487
+ const object = state.objects.get(params.objectId);
488
+ if (object?.kind !== "playwright-utility-script") {
489
+ throw new Error(`Unknown object: ${params.objectId}`);
490
+ }
491
+
492
+ if (!params.functionDeclaration.includes("utilityScript.evaluate")) {
493
+ throw new Error("Unsupported Runtime.callFunctionOn declaration");
494
+ }
495
+
496
+ const args = params.arguments ?? [];
497
+ const isFunction = args[1]?.value;
498
+ const returnByValue = args[2]?.value;
499
+ const expression = args[3]?.value;
500
+ const argCount = args[4]?.value ?? 0;
501
+ const handles = args
502
+ .slice(5 + argCount)
503
+ .map((arg) => state.objects.get(arg.objectId));
504
+ const callArgs = args
505
+ .slice(5, 5 + argCount)
506
+ .map((arg) => deserializePlaywrightValue(arg.value, handles));
507
+ const forwardEvaluate = shouldForwardPlaywrightEvaluate(
508
+ expression,
509
+ handles
510
+ );
511
+ let value;
512
+ if (forwardEvaluate && shouldUseContextEvaluateResult(ctx)) {
513
+ value = await ctx.evaluate(expression);
514
+ } else {
515
+ value = await evaluatePlaywrightCall(
516
+ ctx,
517
+ target,
518
+ expression,
519
+ isFunction,
520
+ callArgs
521
+ );
522
+ }
523
+ if (forwardEvaluate && !shouldUseContextEvaluateResult(ctx)) {
524
+ await ctx.evaluate?.(expression);
525
+ }
526
+
527
+ const serialized = returnByValue
528
+ ? serializePlaywrightValue(value)
529
+ : value;
530
+ return {
531
+ result: createRemoteObject(state, serialized, params.returnByValue),
532
+ };
533
+ }
534
+ case "Runtime.releaseObject":
535
+ case "Runtime.releaseObjectGroup":
536
+ if (params.objectId) state.objects.delete(params.objectId);
537
+ return {};
538
+ case "Input.dispatchMouseEvent": {
539
+ const target = getSessionTarget(state, sessionId);
540
+ if (params.type === "mouseWheel") {
541
+ await ctx.scroll?.(params.deltaX ?? 0, params.deltaY ?? 0);
542
+ return {};
543
+ }
544
+
545
+ if (params.type === "mouseReleased") {
546
+ target.focusedSelector = target.pendingSelector;
547
+ applyClick(target, target.pendingSelector);
548
+ await ctx.click?.(params.x, params.y, {
549
+ button: params.button,
550
+ clickCount: params.clickCount,
551
+ modifiers: params.modifiers,
552
+ });
553
+ }
554
+ return {};
555
+ }
556
+ case "Input.dispatchKeyEvent": {
557
+ if (params.type === "keyDown" || params.type === "char") {
558
+ await ctx.press?.(params.key ?? params.text, { modifiers: params.modifiers ?? 0 });
559
+ }
560
+ return {};
561
+ }
562
+ case "Input.insertText": {
563
+ const target = getSessionTarget(state, sessionId);
564
+ const element = target.elements.get(target.focusedSelector);
565
+ if (element) element.value += params.text;
566
+ await ctx.type?.(params.text);
567
+ return {};
568
+ }
569
+ case "navigate":
570
+ return ctx.navigate?.(params.url);
571
+ case "evaluate":
572
+ return ctx.evaluate?.(params.script);
573
+ case "click":
574
+ return typeof params.selector === "string"
575
+ ? ctx.click?.(params.selector, params.options)
576
+ : ctx.click?.(params.x, params.y, params.options);
577
+ case "type":
578
+ return ctx.type?.(params.text);
579
+ case "press":
580
+ return ctx.press?.(params.key, params.options);
581
+ case "scroll":
582
+ return ctx.scroll?.(params.dx, params.dy);
583
+ case "scrollTo":
584
+ return ctx.scrollTo?.(params.selector, params.options);
585
+ case "resize":
586
+ return ctx.resize?.(params.width, params.height);
587
+ case "goBack":
588
+ return ctx.goBack?.();
589
+ case "goForward":
590
+ return ctx.goForward?.();
591
+ case "reload":
592
+ return ctx.reload?.();
593
+ case "screenshot":
594
+ return ctx.screenshot?.({ ...params, encoding: "base64" });
595
+ case "cdp":
596
+ return ctx.cdp?.(params.method, params.params);
597
+ case "close":
598
+ return ctx.close?.();
599
+ default:
600
+ if (method.includes(".") && ctx.cdp) {
601
+ return ctx.cdp(method, params);
602
+ }
603
+ throw new Error(`Unknown method: ${method}`);
604
+ }
605
+ }
606
+
607
+ function createDeferred() {
608
+ let resolve;
609
+ const promise = new Promise((done) => {
610
+ resolve = done;
611
+ });
612
+ return { promise, resolve };
613
+ }
614
+
615
+ function getPendingInitialNavigationTarget(state) {
616
+ const targets = [...state.targets.values()];
617
+ for (let index = targets.length - 1; index >= 0; index--) {
618
+ const target = targets[index];
619
+ if (state.initialNavigations.has(target.targetId)) return target;
620
+ }
621
+ return null;
622
+ }
623
+
624
+ function navigateTarget(state, target, url, emit, options = {}) {
625
+ target.url = url;
626
+ loadDocument(target, url);
627
+ target.loaderId = `loader-${state.nextLoaderId++}`;
628
+ target.mainExecutionContextId = state.nextExecutionContextId++;
629
+ target.utilityExecutionContextId = target.utilityWorldName
630
+ ? state.nextExecutionContextId++
631
+ : null;
632
+
633
+ if (options.addHistory) {
634
+ target.history.splice(target.historyIndex + 1);
635
+ target.history.push({
636
+ id: state.nextHistoryEntryId++,
637
+ url,
638
+ title: target.title,
639
+ });
640
+ target.historyIndex = target.history.length - 1;
641
+ } else if (target.history[target.historyIndex]) {
642
+ target.history[target.historyIndex].title = target.title;
643
+ }
644
+
645
+ setTimeout(() => {
646
+ emit("Runtime.executionContextsCleared", {});
647
+ emit("Page.frameNavigated", {
648
+ frame: createFrame(target),
649
+ type: "Navigation",
650
+ });
651
+ emit("Runtime.executionContextCreated", {
652
+ context: {
653
+ id: target.mainExecutionContextId,
654
+ origin: getOrigin(target.url),
655
+ name: "",
656
+ uniqueId: `context-${target.targetId}-${target.loaderId}`,
657
+ auxData: {
658
+ isDefault: true,
659
+ type: "default",
660
+ frameId: target.frameId,
661
+ },
662
+ },
663
+ });
664
+ if (target.utilityExecutionContextId) {
665
+ emit("Runtime.executionContextCreated", {
666
+ context: {
667
+ id: target.utilityExecutionContextId,
668
+ origin: getOrigin(target.url),
669
+ name: target.utilityWorldName,
670
+ uniqueId: `context-${target.targetId}-${target.utilityExecutionContextId}`,
671
+ auxData: {
672
+ isDefault: false,
673
+ type: "isolated",
674
+ frameId: target.frameId,
675
+ },
676
+ },
677
+ });
678
+ }
679
+ const timestamp = performance.now() / 1000;
680
+ emit("Page.domContentEventFired", { timestamp });
681
+ emit("Page.loadEventFired", { timestamp });
682
+ emit("Page.lifecycleEvent", {
683
+ frameId: target.frameId,
684
+ loaderId: target.loaderId,
685
+ name: "DOMContentLoaded",
686
+ timestamp,
687
+ });
688
+ emit("Page.lifecycleEvent", {
689
+ frameId: target.frameId,
690
+ loaderId: target.loaderId,
691
+ name: "load",
692
+ timestamp,
693
+ });
694
+ }, 0);
695
+ }
696
+
697
+ function getSessionTarget(state, sessionId) {
698
+ const targetId = state.sessions.get(sessionId);
699
+ const target = state.targets.get(targetId);
700
+ if (!target) throw new Error(`No target for session: ${sessionId}`);
701
+ return target;
702
+ }
703
+
704
+ function getImplicitTarget(state) {
705
+ if (state.implicitTarget) return state.implicitTarget;
706
+
707
+ state.implicitTarget = {
708
+ targetId: "implicit-target",
709
+ frameId: "implicit-target",
710
+ loaderId: "implicit-loader",
711
+ type: "page",
712
+ browserContextId: "default",
713
+ title: "",
714
+ url: "about:blank",
715
+ attached: true,
716
+ elements: new Map(),
717
+ focusedSelector: null,
718
+ pendingSelector: null,
719
+ history: [
720
+ {
721
+ id: state.nextHistoryEntryId++,
722
+ url: "about:blank",
723
+ title: "",
724
+ },
725
+ ],
726
+ historyIndex: 0,
727
+ viewport: {
728
+ width: 800,
729
+ height: 600,
730
+ },
731
+ };
732
+ return state.implicitTarget;
733
+ }
734
+
735
+ function getOrigin(url) {
736
+ try {
737
+ return new URL(url).origin;
738
+ } catch {
739
+ return "null";
740
+ }
741
+ }
742
+
743
+ function createFrame(target) {
744
+ return {
745
+ id: target.frameId,
746
+ loaderId: target.loaderId,
747
+ url: target.url,
748
+ domainAndRegistry: "",
749
+ securityOrigin: getOrigin(target.url),
750
+ mimeType: "text/html",
751
+ };
752
+ }
753
+
754
+ function createTargetInfo(target) {
755
+ return {
756
+ targetId: target.targetId,
757
+ type: target.type,
758
+ title: target.title,
759
+ url: target.url,
760
+ attached: target.attached,
761
+ browserContextId: target.browserContextId,
762
+ };
763
+ }
764
+
765
+ function loadDocument(target, url) {
766
+ target.title = "";
767
+ target.elements = new Map();
768
+ target.focusedSelector = null;
769
+ target.pendingSelector = null;
770
+
771
+ if (!url.startsWith("data:text/html,")) return;
772
+
773
+ try {
774
+ const html = decodeURIComponent(url.slice("data:text/html,".length));
775
+ target.title = html.match(/<title[^>]*>([\s\S]*?)<\/title>/i)?.[1] ?? "";
776
+
777
+ for (const match of html.matchAll(
778
+ /<([a-z][\w-]*)([^>]*\sid=["']([^"']+)["'][^>]*)>([\s\S]*?)<\/\1>/gi
779
+ )) {
780
+ const tagName = match[1].toLowerCase();
781
+ const attributes = match[2] ?? "";
782
+ const id = match[3];
783
+ const textContent = match[4]?.replace(/<[^>]*>/g, "").trim() ?? "";
784
+ const value = attributes.match(/\svalue=["']([^"']*)["']/i)?.[1] ?? "";
785
+ target.elements.set(`#${id}`, { tagName, textContent, value });
786
+ }
787
+
788
+ for (const match of html.matchAll(
789
+ /<(input|textarea)([^>]*\sid=["']([^"']+)["'][^>]*)\/?>/gi
790
+ )) {
791
+ const tagName = match[1].toLowerCase();
792
+ const attributes = match[2] ?? "";
793
+ const id = match[3];
794
+ if (target.elements.has(`#${id}`)) continue;
795
+ const value = attributes.match(/\svalue=["']([^"']*)["']/i)?.[1] ?? "";
796
+ target.elements.set(`#${id}`, { tagName, textContent: "", value });
797
+ }
798
+ } catch {
799
+ target.title = "";
800
+ }
801
+ }
802
+
803
+ function getActionabilitySelector(expression) {
804
+ const match = expression.match(/\}\)\((["'])(.*?)\1,\d+\)\s*$/s);
805
+ return match?.[2] ?? null;
806
+ } // "
807
+
808
+ function getScrollToArguments(expression) {
809
+ const match = expression.match(/\}\)\((.*),(\d+),(.*)\)\s*$/s);
810
+ if (!match) throw new Error("Unable to parse scrollTo arguments");
811
+
812
+ return {
813
+ selector: JSON.parse(match[1]),
814
+ timeout: Number(match[2]),
815
+ block: JSON.parse(match[3]),
816
+ };
817
+ }
818
+
819
+ function applyClick(target, selector) {
820
+ if (selector !== "#test-button") return;
821
+
822
+ const result = target.elements.get("#result");
823
+ if (result) result.textContent = "clicked";
824
+ target.title = "Clicked";
825
+ }
826
+
827
+
828
+ async function evaluateInTarget(target, expression) {
829
+ const document = createDocument(target);
830
+ const AsyncFunction = Object.getPrototypeOf(async function () {}).constructor;
831
+ const evaluate = new AsyncFunction(
832
+ "document",
833
+ `return await (${expression});`
834
+ );
835
+ return evaluate(document);
836
+ }
837
+
838
+
839
+ function createDocument(target) {
840
+ return {
841
+ get title() {
842
+ return target.title;
843
+ },
844
+ set title(value) {
845
+ target.title = String(value);
846
+ },
847
+ querySelector(selector) {
848
+ return target.elements.get(selector) ?? null;
849
+ },
850
+ };
851
+ }
852
+
853
+ async function evaluatePlaywrightExpression(
854
+ target,
855
+ expression,
856
+ isFunction,
857
+ args
858
+ ) {
859
+ const document = createDocument(target);
860
+ const AsyncFunction = Object.getPrototypeOf(async function () {}).constructor;
861
+ const evaluate = new AsyncFunction(
862
+ "document",
863
+ "args",
864
+ isFunction
865
+ ? `return await (${expression})(...args);`
866
+ : `return await (${expression});`
867
+ );
868
+ return evaluate(document, args);
869
+ }
870
+
871
+ async function evaluatePlaywrightCall(ctx, target, expression, isFunction, args) {
872
+ if (expression.trim() === "() => document.title") {
873
+ return getContextTitle(ctx, target);
874
+ }
875
+ if (expression.includes("querySelectorAll(info.parsed")) {
876
+ return resolvePlaywrightLocator(target, args);
877
+ }
878
+ if (expression.includes("injected.previewNode")) {
879
+ const element = args.find((arg) => arg?.kind === "playwright-element");
880
+ return `JSHandle@${playwrightElementDescription(element)}`;
881
+ }
882
+ if (expression.includes("injected.checkElementStates")) {
883
+ return undefined;
884
+ }
885
+ if (expression.includes("injected.setupHitTargetInterceptor")) {
886
+ return {
887
+ kind: "playwright-hit-target-interceptor",
888
+ targetId: target.targetId,
889
+ };
890
+ }
891
+ if (expression.includes("=> h.stop()")) {
892
+ return "done";
893
+ }
894
+ if (
895
+ expression.includes("width: innerWidth") &&
896
+ expression.includes("height: innerHeight")
897
+ ) {
898
+ return {
899
+ width: target.viewport.width,
900
+ height: target.viewport.height,
901
+ };
902
+ }
903
+ return evaluatePlaywrightExpression(target, expression, isFunction, args);
904
+ }
905
+
906
+ async function getContextTitle(ctx, target) {
907
+ return (await ctx.title?.()) || target.title;
908
+ }
909
+
910
+ function shouldUseContextEvaluateResult(ctx) {
911
+ return (
912
+ typeof ctx.evaluate == "function"
913
+ );
914
+ }
915
+
916
+ function deserializePlaywrightValue(value, handles = [], refs = new Map()) {
917
+ if (value === undefined || value === null) return value;
918
+ if (typeof value !== "object") return value;
919
+ if ("ref" in value) return refs.get(value.ref);
920
+ if ("v" in value) {
921
+ if (value.v === "undefined") return undefined;
922
+ if (value.v === "null") return null;
923
+ if (value.v === "NaN") return NaN;
924
+ if (value.v === "Infinity") return Infinity;
925
+ if (value.v === "-Infinity") return -Infinity;
926
+ if (value.v === "-0") return -0;
927
+ }
928
+ if ("bi" in value) return BigInt(value.bi);
929
+ if ("h" in value) return handles[value.h];
930
+ if ("a" in value) {
931
+ const result = [];
932
+ refs.set(value.id, result);
933
+ for (const item of value.a) {
934
+ result.push(deserializePlaywrightValue(item, handles, refs));
935
+ }
936
+ return result;
937
+ }
938
+ if ("o" in value) {
939
+ const result = {};
940
+ refs.set(value.id, result);
941
+ for (const item of value.o) {
942
+ if (item.k === "__proto__") continue;
943
+ result[item.k] = deserializePlaywrightValue(item.v, handles, refs);
944
+ }
945
+ return result;
946
+ }
947
+ return value;
948
+ }
949
+
950
+ function resolvePlaywrightLocator(target, args) {
951
+ const [, options] = args;
952
+ const selector = getPlaywrightSelector(options?.info?.parsed);
953
+ const element = target.elements.get(selector);
954
+ if (!element) return { log: "", success: false, element: null };
955
+
956
+ return {
957
+ kind: "playwright-locator-result",
958
+ log: `locator resolved to ${selector}`,
959
+ success: true,
960
+ element: {
961
+ kind: "playwright-element",
962
+ targetId: target.targetId,
963
+ selector,
964
+ element,
965
+ },
966
+ };
967
+ }
968
+
969
+ function shouldForwardPlaywrightEvaluate(expression, handles) {
970
+ if (handles.some((handle) => handle?.kind?.startsWith("playwright-"))) {
971
+ return false;
972
+ }
973
+
974
+ const source = expression.trim();
975
+ if (source === "() => document.title") return false;
976
+ if (
977
+ source.includes("querySelectorAll(info.parsed") ||
978
+ source.includes("injected.previewNode") ||
979
+ source.includes("injected.checkElementStates") ||
980
+ source.includes("injected.setupHitTargetInterceptor") ||
981
+ (source.includes("width: innerWidth") &&
982
+ source.includes("height: innerHeight"))
983
+ ) {
984
+ return false;
985
+ }
986
+
987
+ return true;
988
+ }
989
+
990
+ function getPlaywrightSelector(parsed) {
991
+ if (typeof parsed === "string") return parsed;
992
+ if (!parsed || typeof parsed !== "object") return null;
993
+ if (typeof parsed.source === "string") return parsed.source;
994
+ if (typeof parsed.body === "string") return parsed.body;
995
+
996
+ for (const value of Object.values(parsed)) {
997
+ const selector = getPlaywrightSelector(value);
998
+ if (selector) return selector;
999
+ }
1000
+ return null;
1001
+ }
1002
+
1003
+ function serializePlaywrightValue(value, seen = new Map()) {
1004
+ if (value === undefined) return { v: "undefined" };
1005
+ if (value === null) return { v: "null" };
1006
+ if (
1007
+ typeof value === "string" ||
1008
+ typeof value === "number" ||
1009
+ typeof value === "boolean"
1010
+ ) {
1011
+ return value;
1012
+ }
1013
+ if (typeof value === "bigint") return { bi: value.toString() };
1014
+
1015
+ if (seen.has(value)) return { ref: seen.get(value) };
1016
+ const id = seen.size + 1;
1017
+ seen.set(value, id);
1018
+
1019
+ if (Array.isArray(value)) {
1020
+ return {
1021
+ a: value.map((item) => serializePlaywrightValue(item, seen)),
1022
+ id,
1023
+ };
1024
+ }
1025
+
1026
+ return {
1027
+ o: Object.entries(value).map(([key, item]) => ({
1028
+ k: key,
1029
+ v: serializePlaywrightValue(item, seen),
1030
+ })),
1031
+ id,
1032
+ };
1033
+ }
1034
+
1035
+ function createRemoteObject(state, value, returnByValue) {
1036
+ if (value === undefined) return { type: "undefined" };
1037
+ if (value === null) return { type: "object", subtype: "null", value: null };
1038
+ if (value?.kind === "playwright-element") {
1039
+ const objectId = `object-${state.nextObjectId++}`;
1040
+ state.objects.set(objectId, value);
1041
+ return {
1042
+ type: "object",
1043
+ subtype: "node",
1044
+ className: playwrightElementClassName(value.element?.tagName),
1045
+ description: playwrightElementDescription(value),
1046
+ objectId,
1047
+ };
1048
+ }
1049
+
1050
+ const type = typeof value;
1051
+ if (type === "string" || type === "boolean") {
1052
+ return { type, value };
1053
+ }
1054
+ if (type === "number") {
1055
+ return Number.isFinite(value)
1056
+ ? { type, value }
1057
+ : { type, unserializableValue: String(value) };
1058
+ }
1059
+ if (type === "bigint") {
1060
+ return { type, unserializableValue: `${value}n` };
1061
+ }
1062
+
1063
+ if (returnByValue) {
1064
+ return {
1065
+ type: "object",
1066
+ value,
1067
+ description: Array.isArray(value) ? "Array" : "Object",
1068
+ };
1069
+ }
1070
+
1071
+ const objectId = `object-${state.nextObjectId++}`;
1072
+ state.objects.set(objectId, value);
1073
+ return {
1074
+ type: "object",
1075
+ className: "Object",
1076
+ description: "Object",
1077
+ objectId,
1078
+ };
1079
+ }
1080
+
1081
+ function playwrightElementClassName(tagName) {
1082
+ const names = {
1083
+ button: "HTMLButtonElement",
1084
+ input: "HTMLInputElement",
1085
+ textarea: "HTMLTextAreaElement",
1086
+ output: "HTMLOutputElement",
1087
+ };
1088
+ return names[tagName] ?? "HTMLElement";
1089
+ }
1090
+
1091
+ function playwrightElementDescription(value) {
1092
+ const tagName = value.element?.tagName ?? "element";
1093
+ return `${tagName}#${value.selector.replace(/^#/, "")}`;
1094
+ }
1095
+
1096
+ function createLoggingContext() {
1097
+ const log = (method, payload = {}) => {
1098
+
1099
+ serverError(Bun.markdown.ansi(`## [CdpServer:context] ${method}`))
1100
+ serverError(payload);
1101
+ return { ok: true, method, payload };
1102
+ };
1103
+
1104
+ return {
1105
+ navigate(url) {
1106
+ return log("navigate", { url });
1107
+ },
1108
+ evaluate(script) {
1109
+ return log("evaluate", { script });
1110
+ },
1111
+ title() {
1112
+ log("title");
1113
+ return "";
1114
+ },
1115
+ click(selectorOrX, yOrOptions, options) {
1116
+ if (typeof selectorOrX === "string") {
1117
+ return log("click", { selector: selectorOrX, options: yOrOptions });
1118
+ }
1119
+
1120
+ return log("click", { x: selectorOrX, y: yOrOptions, options });
1121
+ },
1122
+ type(text) {
1123
+ return log("type", { text });
1124
+ },
1125
+ press(key, options) {
1126
+ return log("press", { key, options });
1127
+ },
1128
+ scroll(dx, dy) {
1129
+ return log("scroll", { dx, dy });
1130
+ },
1131
+ scrollTo(selector, options) {
1132
+ return log("scrollTo", { selector, options });
1133
+ },
1134
+ resize(width, height) {
1135
+ return log("resize", { width, height });
1136
+ },
1137
+ goBack() {
1138
+ return log("goBack");
1139
+ },
1140
+ goForward() {
1141
+ return log("goForward");
1142
+ },
1143
+ reload() {
1144
+ return log("reload");
1145
+ },
1146
+ screenshot(options) {
1147
+ return log("screenshot", { options });
1148
+ },
1149
+ cdp(method, params) {
1150
+ return log("cdp", { method, params });
1151
+ },
1152
+ close() {
1153
+ return log("close");
1154
+ },
1155
+ };
1156
+ }
1157
+
1158
+ if (import.meta.main) {
1159
+ const port = Number(Bun.argv[2] ?? CDP_DEFAULT_PORT);
1160
+ CdpServer.create(createLoggingContext()).listen(port);
1161
+ }