playwriter 0.0.2 → 0.0.4
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/bin.js +1 -1
- package/dist/browser-config.js +1 -3
- package/dist/browser-config.js.map +1 -1
- package/dist/cdp-types.d.ts +25 -0
- package/dist/cdp-types.d.ts.map +1 -0
- package/dist/cdp-types.js +91 -0
- package/dist/cdp-types.js.map +1 -0
- package/dist/extension/cdp-relay.d.ts +12 -0
- package/dist/extension/cdp-relay.d.ts.map +1 -0
- package/dist/extension/cdp-relay.js +378 -0
- package/dist/extension/cdp-relay.js.map +1 -0
- package/dist/extension/protocol.d.ts +29 -0
- package/dist/extension/protocol.d.ts.map +1 -0
- package/dist/extension/protocol.js +2 -0
- package/dist/extension/protocol.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -0
- package/dist/mcp-client.d.ts.map +1 -1
- package/dist/mcp-client.js +1 -1
- package/dist/mcp-client.js.map +1 -1
- package/dist/mcp.js +74 -464
- package/dist/mcp.js.map +1 -1
- package/dist/mcp.test.js +101 -142
- package/dist/mcp.test.js.map +1 -1
- package/dist/prompt.md +41 -487
- package/dist/resource.md +436 -0
- package/dist/start-relay-server.d.ts +8 -0
- package/dist/start-relay-server.d.ts.map +1 -0
- package/dist/start-relay-server.js +33 -0
- package/dist/start-relay-server.js.map +1 -0
- package/package.json +42 -36
- package/src/browser-config.ts +48 -50
- package/src/cdp-types.ts +124 -0
- package/src/extension/cdp-relay.ts +480 -0
- package/src/extension/protocol.ts +34 -0
- package/src/index.ts +1 -0
- package/src/mcp-client.ts +46 -46
- package/src/mcp.test.ts +109 -165
- package/src/mcp.ts +202 -694
- package/src/prompt.md +41 -487
- package/src/resource.md +436 -0
- package/src/snapshots/hacker-news-initial-accessibility.md +243 -127
- package/src/snapshots/shadcn-ui-accessibility.md +300 -510
- package/src/start-relay-server.ts +43 -0
package/src/cdp-types.ts
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import type { Protocol } from 'devtools-protocol';
|
|
2
|
+
import type { ProtocolMapping } from 'devtools-protocol/types/protocol-mapping.js';
|
|
3
|
+
|
|
4
|
+
export type CDPCommand<T extends keyof ProtocolMapping.Commands = keyof ProtocolMapping.Commands> = {
|
|
5
|
+
id: number;
|
|
6
|
+
sessionId?: string;
|
|
7
|
+
method: T;
|
|
8
|
+
params?: ProtocolMapping.Commands[T]['paramsType'][0];
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export type CDPResponse<T extends keyof ProtocolMapping.Commands = keyof ProtocolMapping.Commands> = {
|
|
12
|
+
id: number;
|
|
13
|
+
sessionId?: string;
|
|
14
|
+
result?: ProtocolMapping.Commands[T]['returnType'];
|
|
15
|
+
error?: { code?: number; message: string };
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export type CDPEvent<T extends keyof ProtocolMapping.Events = keyof ProtocolMapping.Events> = {
|
|
19
|
+
method: T;
|
|
20
|
+
sessionId?: string;
|
|
21
|
+
params?: ProtocolMapping.Events[T][0];
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export type CDPMessage = CDPCommand | CDPResponse | CDPEvent;
|
|
25
|
+
|
|
26
|
+
export { Protocol, ProtocolMapping };
|
|
27
|
+
|
|
28
|
+
// types tests. to see if types are right with some simple examples
|
|
29
|
+
if (false as any) {
|
|
30
|
+
const browserVersionCommand = {
|
|
31
|
+
id: 1,
|
|
32
|
+
method: 'Browser.getVersion',
|
|
33
|
+
} satisfies CDPCommand;
|
|
34
|
+
|
|
35
|
+
const browserVersionResponse = {
|
|
36
|
+
id: 1,
|
|
37
|
+
result: {
|
|
38
|
+
protocolVersion: '1.3',
|
|
39
|
+
product: 'Chrome',
|
|
40
|
+
revision: '123',
|
|
41
|
+
userAgent: 'Mozilla/5.0',
|
|
42
|
+
jsVersion: 'V8',
|
|
43
|
+
}
|
|
44
|
+
} satisfies CDPResponse;
|
|
45
|
+
|
|
46
|
+
const targetAttachCommand = {
|
|
47
|
+
id: 2,
|
|
48
|
+
method: 'Target.setAutoAttach',
|
|
49
|
+
params: {
|
|
50
|
+
autoAttach: true,
|
|
51
|
+
waitForDebuggerOnStart: false,
|
|
52
|
+
}
|
|
53
|
+
} satisfies CDPCommand;
|
|
54
|
+
|
|
55
|
+
const targetAttachResponse = {
|
|
56
|
+
id: 2,
|
|
57
|
+
result: undefined,
|
|
58
|
+
} satisfies CDPResponse;
|
|
59
|
+
|
|
60
|
+
const attachedToTargetEvent = {
|
|
61
|
+
method: 'Target.attachedToTarget',
|
|
62
|
+
params: {
|
|
63
|
+
sessionId: 'session-1',
|
|
64
|
+
targetInfo: {
|
|
65
|
+
targetId: 'target-1',
|
|
66
|
+
type: 'page',
|
|
67
|
+
title: 'Example',
|
|
68
|
+
url: 'https://example.com',
|
|
69
|
+
attached: true,
|
|
70
|
+
canAccessOpener: false,
|
|
71
|
+
},
|
|
72
|
+
waitingForDebugger: false,
|
|
73
|
+
}
|
|
74
|
+
} satisfies CDPEvent;
|
|
75
|
+
|
|
76
|
+
const consoleMessageEvent = {
|
|
77
|
+
method: 'Runtime.consoleAPICalled',
|
|
78
|
+
params: {
|
|
79
|
+
type: 'log',
|
|
80
|
+
args: [],
|
|
81
|
+
executionContextId: 1,
|
|
82
|
+
timestamp: 123456789,
|
|
83
|
+
}
|
|
84
|
+
} satisfies CDPEvent;
|
|
85
|
+
|
|
86
|
+
const pageNavigateCommand = {
|
|
87
|
+
id: 3,
|
|
88
|
+
method: 'Page.navigate',
|
|
89
|
+
params: {
|
|
90
|
+
url: 'https://example.com',
|
|
91
|
+
}
|
|
92
|
+
} satisfies CDPCommand;
|
|
93
|
+
|
|
94
|
+
const pageNavigateResponse = {
|
|
95
|
+
id: 3,
|
|
96
|
+
result: {
|
|
97
|
+
frameId: 'frame-1',
|
|
98
|
+
}
|
|
99
|
+
} satisfies CDPResponse;
|
|
100
|
+
|
|
101
|
+
const networkRequestEvent = {
|
|
102
|
+
method: 'Network.requestWillBeSent',
|
|
103
|
+
sessionId: 'session-1',
|
|
104
|
+
params: {
|
|
105
|
+
requestId: 'req-1',
|
|
106
|
+
loaderId: 'loader-1',
|
|
107
|
+
documentURL: 'https://example.com',
|
|
108
|
+
request: {
|
|
109
|
+
url: 'https://example.com/api',
|
|
110
|
+
method: 'GET',
|
|
111
|
+
headers: {},
|
|
112
|
+
initialPriority: 'High',
|
|
113
|
+
referrerPolicy: 'no-referrer',
|
|
114
|
+
},
|
|
115
|
+
timestamp: 123456789,
|
|
116
|
+
wallTime: 123456789,
|
|
117
|
+
initiator: {
|
|
118
|
+
type: 'other',
|
|
119
|
+
},
|
|
120
|
+
redirectHasExtraInfo: false,
|
|
121
|
+
type: 'XHR',
|
|
122
|
+
}
|
|
123
|
+
} satisfies CDPEvent;
|
|
124
|
+
}
|
|
@@ -0,0 +1,480 @@
|
|
|
1
|
+
import { Hono } from 'hono'
|
|
2
|
+
import { serve } from '@hono/node-server'
|
|
3
|
+
import { createNodeWebSocket } from '@hono/node-ws'
|
|
4
|
+
import type { WSContext } from 'hono/ws'
|
|
5
|
+
import type { Protocol } from '../cdp-types.js'
|
|
6
|
+
import type { CDPCommand, CDPResponse, CDPEvent } from '../cdp-types.js'
|
|
7
|
+
import type { ExtensionMessage, ExtensionEventMessage } from './protocol.js'
|
|
8
|
+
import chalk from 'chalk'
|
|
9
|
+
|
|
10
|
+
type ConnectedTarget = {
|
|
11
|
+
sessionId: string
|
|
12
|
+
targetId: string
|
|
13
|
+
targetInfo: Protocol.Target.TargetInfo
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
type PlaywrightClient = {
|
|
19
|
+
id: string
|
|
20
|
+
ws: WSContext
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export async function startPlayWriterCDPRelayServer({ port = 19988, logger = console }: { port?: number; logger?: { log(...args: any[]): void; error(...args: any[]): void } } = {}) {
|
|
24
|
+
const connectedTargets = new Map<string, ConnectedTarget>()
|
|
25
|
+
|
|
26
|
+
const playwrightClients = new Map<string, PlaywrightClient>()
|
|
27
|
+
let extensionWs: WSContext | null = null
|
|
28
|
+
|
|
29
|
+
const extensionPendingRequests = new Map<number, {
|
|
30
|
+
resolve: (result: any) => void
|
|
31
|
+
reject: (error: Error) => void
|
|
32
|
+
}>()
|
|
33
|
+
let extensionMessageId = 0
|
|
34
|
+
|
|
35
|
+
function logCdpMessage({
|
|
36
|
+
direction,
|
|
37
|
+
clientId,
|
|
38
|
+
method,
|
|
39
|
+
sessionId,
|
|
40
|
+
params,
|
|
41
|
+
id,
|
|
42
|
+
source
|
|
43
|
+
}: {
|
|
44
|
+
direction: 'to-playwright' | 'from-playwright' | 'from-extension'
|
|
45
|
+
clientId?: string
|
|
46
|
+
method: string
|
|
47
|
+
sessionId?: string
|
|
48
|
+
params?: any
|
|
49
|
+
id?: number
|
|
50
|
+
source?: 'extension' | 'server'
|
|
51
|
+
}) {
|
|
52
|
+
const noisyEvents = [
|
|
53
|
+
'Network.requestWillBeSentExtraInfo',
|
|
54
|
+
'Network.responseReceived',
|
|
55
|
+
'Network.responseReceivedExtraInfo',
|
|
56
|
+
'Network.dataReceived',
|
|
57
|
+
'Network.requestWillBeSent',
|
|
58
|
+
'Network.loadingFinished'
|
|
59
|
+
]
|
|
60
|
+
|
|
61
|
+
if (noisyEvents.includes(method)) {
|
|
62
|
+
return
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const details: string[] = []
|
|
66
|
+
|
|
67
|
+
if (id !== undefined) {
|
|
68
|
+
details.push(`id=${id}`)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (sessionId) {
|
|
72
|
+
details.push(`sessionId=${sessionId}`)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (params) {
|
|
76
|
+
if (params.targetId) {
|
|
77
|
+
details.push(`targetId=${params.targetId}`)
|
|
78
|
+
}
|
|
79
|
+
if (params.targetInfo?.targetId) {
|
|
80
|
+
details.push(`targetId=${params.targetInfo.targetId}`)
|
|
81
|
+
}
|
|
82
|
+
if (params.sessionId && params.sessionId !== sessionId) {
|
|
83
|
+
details.push(`sessionId=${params.sessionId}`)
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const detailsStr = details.length > 0 ? ` ${chalk.gray(details.join(', '))}` : ''
|
|
88
|
+
|
|
89
|
+
if (direction === 'from-playwright') {
|
|
90
|
+
const clientLabel = clientId ? chalk.blue(`[${clientId}]`) : ''
|
|
91
|
+
logger.log(chalk.cyan('← Playwright'), clientLabel + ':', method + detailsStr)
|
|
92
|
+
} else if (direction === 'from-extension') {
|
|
93
|
+
logger.log(chalk.yellow('← Extension:'), method + detailsStr)
|
|
94
|
+
} else if (direction === 'to-playwright') {
|
|
95
|
+
const color = source === 'server' ? chalk.magenta : chalk.green
|
|
96
|
+
const sourceLabel = source === 'server' ? chalk.gray(' (server-generated)') : ''
|
|
97
|
+
const clientLabel = clientId ? chalk.blue(`[${clientId}]`) : chalk.blue('[ALL]')
|
|
98
|
+
logger.log(color('→ Playwright'), clientLabel + ':', method + detailsStr + sourceLabel)
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function sendToPlaywright({
|
|
103
|
+
message,
|
|
104
|
+
clientId,
|
|
105
|
+
source = 'extension'
|
|
106
|
+
}: {
|
|
107
|
+
message: CDPResponse | CDPEvent
|
|
108
|
+
clientId?: string
|
|
109
|
+
source?: 'extension' | 'server'
|
|
110
|
+
}) {
|
|
111
|
+
const messageToSend = source === 'server' && 'method' in message
|
|
112
|
+
? { ...message, __serverGenerated: true }
|
|
113
|
+
: message
|
|
114
|
+
|
|
115
|
+
if ('method' in message) {
|
|
116
|
+
logCdpMessage({
|
|
117
|
+
direction: 'to-playwright',
|
|
118
|
+
clientId,
|
|
119
|
+
method: message.method,
|
|
120
|
+
sessionId: 'sessionId' in message ? message.sessionId : undefined,
|
|
121
|
+
params: 'params' in message ? message.params : undefined,
|
|
122
|
+
source
|
|
123
|
+
})
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const messageStr = JSON.stringify(messageToSend)
|
|
127
|
+
|
|
128
|
+
if (clientId) {
|
|
129
|
+
const client = playwrightClients.get(clientId)
|
|
130
|
+
if (client) {
|
|
131
|
+
client.ws.send(messageStr)
|
|
132
|
+
}
|
|
133
|
+
} else {
|
|
134
|
+
for (const client of playwrightClients.values()) {
|
|
135
|
+
client.ws.send(messageStr)
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
async function sendToExtension({ method, params }: { method: string; params?: any }) {
|
|
141
|
+
if (!extensionWs) {
|
|
142
|
+
throw new Error('Extension not connected')
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const id = ++extensionMessageId
|
|
146
|
+
const message = { id, method, params }
|
|
147
|
+
|
|
148
|
+
extensionWs.send(JSON.stringify(message))
|
|
149
|
+
|
|
150
|
+
return new Promise((resolve, reject) => {
|
|
151
|
+
extensionPendingRequests.set(id, { resolve, reject })
|
|
152
|
+
})
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
async function routeCdpCommand({ method, params, sessionId }: { method: string; params: any; sessionId?: string }) {
|
|
156
|
+
switch (method) {
|
|
157
|
+
case 'Browser.getVersion': {
|
|
158
|
+
return {
|
|
159
|
+
protocolVersion: '1.3',
|
|
160
|
+
product: 'Chrome/Extension-Bridge',
|
|
161
|
+
revision: '1.0.0',
|
|
162
|
+
userAgent: 'CDP-Bridge-Server/1.0.0',
|
|
163
|
+
jsVersion: 'V8'
|
|
164
|
+
} satisfies Protocol.Browser.GetVersionResponse
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
case 'Browser.setDownloadBehavior': {
|
|
168
|
+
return {}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
case 'Target.setAutoAttach': {
|
|
172
|
+
if (sessionId) {
|
|
173
|
+
break
|
|
174
|
+
}
|
|
175
|
+
return {}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
case 'Target.getTargetInfo': {
|
|
179
|
+
const targetId = params?.targetId
|
|
180
|
+
|
|
181
|
+
if (targetId) {
|
|
182
|
+
for (const target of connectedTargets.values()) {
|
|
183
|
+
if (target.targetId === targetId) {
|
|
184
|
+
return { targetInfo: target.targetInfo }
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
if (sessionId) {
|
|
190
|
+
const target = connectedTargets.get(sessionId)
|
|
191
|
+
if (target) {
|
|
192
|
+
return { targetInfo: target.targetInfo }
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const firstTarget = Array.from(connectedTargets.values())[0]
|
|
197
|
+
return { targetInfo: firstTarget?.targetInfo }
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
case 'Target.getTargets': {
|
|
201
|
+
return {
|
|
202
|
+
targetInfos: Array.from(connectedTargets.values()).map((t) => ({
|
|
203
|
+
...t.targetInfo,
|
|
204
|
+
attached: true
|
|
205
|
+
}))
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
case 'Target.createTarget': {
|
|
210
|
+
return await sendToExtension({
|
|
211
|
+
method: 'forwardCDPCommand',
|
|
212
|
+
params: { method, params }
|
|
213
|
+
})
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
case 'Target.closeTarget': {
|
|
217
|
+
return await sendToExtension({
|
|
218
|
+
method: 'forwardCDPCommand',
|
|
219
|
+
params: { method, params }
|
|
220
|
+
})
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
return await sendToExtension({
|
|
225
|
+
method: 'forwardCDPCommand',
|
|
226
|
+
params: { sessionId, method, params }
|
|
227
|
+
})
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const app = new Hono()
|
|
231
|
+
const { injectWebSocket, upgradeWebSocket } = createNodeWebSocket({ app })
|
|
232
|
+
|
|
233
|
+
app.get('/', (c) => {
|
|
234
|
+
return c.text('OK')
|
|
235
|
+
})
|
|
236
|
+
|
|
237
|
+
app.get('/cdp/:clientId?', upgradeWebSocket((c) => {
|
|
238
|
+
const clientId = c.req.param('clientId') || 'default'
|
|
239
|
+
|
|
240
|
+
return {
|
|
241
|
+
onOpen(_event, ws) {
|
|
242
|
+
if (playwrightClients.has(clientId)) {
|
|
243
|
+
logger.log(chalk.red(`Rejecting duplicate client ID: ${clientId}`))
|
|
244
|
+
ws.close(1000, 'Client ID already connected')
|
|
245
|
+
return
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
playwrightClients.set(clientId, { id: clientId, ws })
|
|
249
|
+
logger.log(chalk.green(`Playwright client connected: ${clientId} (${playwrightClients.size} total)`))
|
|
250
|
+
},
|
|
251
|
+
|
|
252
|
+
async onMessage(event, ws) {
|
|
253
|
+
let message: CDPCommand
|
|
254
|
+
|
|
255
|
+
try {
|
|
256
|
+
message = JSON.parse(event.data.toString())
|
|
257
|
+
} catch {
|
|
258
|
+
return
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
const { id, sessionId, method, params } = message
|
|
262
|
+
|
|
263
|
+
logCdpMessage({
|
|
264
|
+
direction: 'from-playwright',
|
|
265
|
+
clientId,
|
|
266
|
+
method,
|
|
267
|
+
sessionId,
|
|
268
|
+
id
|
|
269
|
+
})
|
|
270
|
+
|
|
271
|
+
if (!extensionWs) {
|
|
272
|
+
sendToPlaywright({
|
|
273
|
+
message: {
|
|
274
|
+
id,
|
|
275
|
+
sessionId,
|
|
276
|
+
error: { message: 'Extension not connected' }
|
|
277
|
+
},
|
|
278
|
+
clientId
|
|
279
|
+
})
|
|
280
|
+
return
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
try {
|
|
284
|
+
const result: any = await routeCdpCommand({ method, params, sessionId })
|
|
285
|
+
|
|
286
|
+
if (method === 'Target.setAutoAttach' && !sessionId) {
|
|
287
|
+
for (const target of connectedTargets.values()) {
|
|
288
|
+
sendToPlaywright({
|
|
289
|
+
message: {
|
|
290
|
+
method: 'Target.attachedToTarget',
|
|
291
|
+
params: {
|
|
292
|
+
sessionId: target.sessionId,
|
|
293
|
+
targetInfo: {
|
|
294
|
+
...target.targetInfo,
|
|
295
|
+
attached: true
|
|
296
|
+
},
|
|
297
|
+
waitingForDebugger: false
|
|
298
|
+
}
|
|
299
|
+
} satisfies CDPEvent,
|
|
300
|
+
clientId,
|
|
301
|
+
source: 'server'
|
|
302
|
+
})
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
sendToPlaywright({
|
|
307
|
+
message: { id, sessionId, result },
|
|
308
|
+
clientId
|
|
309
|
+
})
|
|
310
|
+
} catch (e) {
|
|
311
|
+
logger.error('Error handling CDP command:', method, params, e)
|
|
312
|
+
sendToPlaywright({
|
|
313
|
+
message: {
|
|
314
|
+
id,
|
|
315
|
+
sessionId,
|
|
316
|
+
error: { message: (e as Error).message }
|
|
317
|
+
},
|
|
318
|
+
clientId
|
|
319
|
+
})
|
|
320
|
+
}
|
|
321
|
+
},
|
|
322
|
+
|
|
323
|
+
onClose() {
|
|
324
|
+
playwrightClients.delete(clientId)
|
|
325
|
+
logger.log(chalk.yellow(`Playwright client disconnected: ${clientId} (${playwrightClients.size} remaining)`))
|
|
326
|
+
},
|
|
327
|
+
|
|
328
|
+
onError(event) {
|
|
329
|
+
logger.error(`Playwright WebSocket error [${clientId}]:`, event)
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
}))
|
|
333
|
+
|
|
334
|
+
app.get('/extension', upgradeWebSocket(() => {
|
|
335
|
+
return {
|
|
336
|
+
onOpen(_event, ws) {
|
|
337
|
+
if (extensionWs) {
|
|
338
|
+
logger.log('Rejecting second extension connection')
|
|
339
|
+
ws.close(1000, 'Another extension connection already established')
|
|
340
|
+
return
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
extensionWs = ws
|
|
344
|
+
logger.log('Extension connected')
|
|
345
|
+
},
|
|
346
|
+
|
|
347
|
+
async onMessage(event, ws) {
|
|
348
|
+
let message: ExtensionMessage
|
|
349
|
+
|
|
350
|
+
try {
|
|
351
|
+
message = JSON.parse(event.data.toString())
|
|
352
|
+
} catch {
|
|
353
|
+
ws.close(1000, 'Invalid JSON')
|
|
354
|
+
return
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
if ('id' in message) {
|
|
358
|
+
const pending = extensionPendingRequests.get(message.id)
|
|
359
|
+
if (!pending) {
|
|
360
|
+
logger.log('Unexpected response with id:', message.id)
|
|
361
|
+
return
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
extensionPendingRequests.delete(message.id)
|
|
365
|
+
|
|
366
|
+
if (message.error) {
|
|
367
|
+
pending.reject(new Error(message.error))
|
|
368
|
+
} else {
|
|
369
|
+
pending.resolve(message.result)
|
|
370
|
+
}
|
|
371
|
+
} else {
|
|
372
|
+
const extensionEvent = message as ExtensionEventMessage
|
|
373
|
+
|
|
374
|
+
if (extensionEvent.method !== 'forwardCDPEvent') {
|
|
375
|
+
return
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
const { method, params, sessionId } = extensionEvent.params
|
|
379
|
+
|
|
380
|
+
logCdpMessage({
|
|
381
|
+
direction: 'from-extension',
|
|
382
|
+
method,
|
|
383
|
+
sessionId,
|
|
384
|
+
params
|
|
385
|
+
})
|
|
386
|
+
|
|
387
|
+
if (method === 'Target.attachedToTarget') {
|
|
388
|
+
const targetParams = params as Protocol.Target.AttachedToTargetEvent
|
|
389
|
+
|
|
390
|
+
// Check if we already sent this target to clients (e.g., from Target.setAutoAttach response)
|
|
391
|
+
const alreadyConnected = connectedTargets.has(targetParams.sessionId)
|
|
392
|
+
|
|
393
|
+
// Always update our local state with latest target info
|
|
394
|
+
connectedTargets.set(targetParams.sessionId, {
|
|
395
|
+
sessionId: targetParams.sessionId,
|
|
396
|
+
targetId: targetParams.targetInfo.targetId,
|
|
397
|
+
targetInfo: targetParams.targetInfo
|
|
398
|
+
})
|
|
399
|
+
|
|
400
|
+
// Only forward to Playwright if this is a new target to avoid duplicates
|
|
401
|
+
if (!alreadyConnected) {
|
|
402
|
+
sendToPlaywright({
|
|
403
|
+
message: {
|
|
404
|
+
method: 'Target.attachedToTarget',
|
|
405
|
+
params: targetParams
|
|
406
|
+
} as CDPEvent,
|
|
407
|
+
source: 'extension'
|
|
408
|
+
})
|
|
409
|
+
}
|
|
410
|
+
} else if (method === 'Target.detachedFromTarget') {
|
|
411
|
+
const detachParams = params as Protocol.Target.DetachedFromTargetEvent
|
|
412
|
+
connectedTargets.delete(detachParams.sessionId)
|
|
413
|
+
|
|
414
|
+
sendToPlaywright({
|
|
415
|
+
message: {
|
|
416
|
+
method: 'Target.detachedFromTarget',
|
|
417
|
+
params: detachParams
|
|
418
|
+
} as CDPEvent,
|
|
419
|
+
source: 'extension'
|
|
420
|
+
})
|
|
421
|
+
} else {
|
|
422
|
+
sendToPlaywright({
|
|
423
|
+
message: {
|
|
424
|
+
sessionId,
|
|
425
|
+
method,
|
|
426
|
+
params
|
|
427
|
+
} as CDPEvent,
|
|
428
|
+
source: 'extension'
|
|
429
|
+
})
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
},
|
|
433
|
+
|
|
434
|
+
onClose() {
|
|
435
|
+
logger.log('Extension disconnected')
|
|
436
|
+
|
|
437
|
+
for (const pending of extensionPendingRequests.values()) {
|
|
438
|
+
pending.reject(new Error('Extension connection closed'))
|
|
439
|
+
}
|
|
440
|
+
extensionPendingRequests.clear()
|
|
441
|
+
|
|
442
|
+
extensionWs = null
|
|
443
|
+
connectedTargets.clear()
|
|
444
|
+
|
|
445
|
+
for (const client of playwrightClients.values()) {
|
|
446
|
+
client.ws.close(1000, 'Extension disconnected')
|
|
447
|
+
}
|
|
448
|
+
playwrightClients.clear()
|
|
449
|
+
},
|
|
450
|
+
|
|
451
|
+
onError(event) {
|
|
452
|
+
logger.error('Extension WebSocket error:', event)
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
}))
|
|
456
|
+
|
|
457
|
+
const server = serve({ fetch: app.fetch, port })
|
|
458
|
+
injectWebSocket(server)
|
|
459
|
+
|
|
460
|
+
const wsHost = `ws://localhost:${port}`
|
|
461
|
+
const cdpEndpoint = `${wsHost}/cdp`
|
|
462
|
+
const extensionEndpoint = `${wsHost}/extension`
|
|
463
|
+
|
|
464
|
+
logger.log('CDP relay server started')
|
|
465
|
+
logger.log('Extension endpoint:', extensionEndpoint)
|
|
466
|
+
logger.log('CDP endpoint:', cdpEndpoint)
|
|
467
|
+
|
|
468
|
+
return {
|
|
469
|
+
cdpEndpoint,
|
|
470
|
+
extensionEndpoint,
|
|
471
|
+
close() {
|
|
472
|
+
for (const client of playwrightClients.values()) {
|
|
473
|
+
client.ws.close(1000, 'Server stopped')
|
|
474
|
+
}
|
|
475
|
+
playwrightClients.clear()
|
|
476
|
+
extensionWs?.close(1000, 'Server stopped')
|
|
477
|
+
server.close()
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
export const VERSION = 1
|
|
2
|
+
|
|
3
|
+
export type ExtensionCommandMessage =
|
|
4
|
+
| {
|
|
5
|
+
id: number
|
|
6
|
+
method: 'attachToTab'
|
|
7
|
+
params?: object
|
|
8
|
+
}
|
|
9
|
+
| {
|
|
10
|
+
id: number
|
|
11
|
+
method: 'forwardCDPCommand'
|
|
12
|
+
params: {
|
|
13
|
+
method: string
|
|
14
|
+
sessionId?: string
|
|
15
|
+
params?: any
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export type ExtensionResponseMessage = {
|
|
20
|
+
id: number
|
|
21
|
+
result?: any
|
|
22
|
+
error?: string
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export type ExtensionEventMessage = {
|
|
26
|
+
method: 'forwardCDPEvent'
|
|
27
|
+
params: {
|
|
28
|
+
method: string
|
|
29
|
+
sessionId?: string
|
|
30
|
+
params?: any
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export type ExtensionMessage = ExtensionResponseMessage | ExtensionEventMessage
|
package/src/index.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './extension/cdp-relay.js'
|