playwriter 0.0.63 → 0.0.89
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/dist/a11y-client.js +18 -8
- package/dist/aria-snapshot.d.ts +41 -3
- package/dist/aria-snapshot.d.ts.map +1 -1
- package/dist/aria-snapshot.js +134 -55
- package/dist/aria-snapshot.js.map +1 -1
- package/dist/aria-snapshot.test.js +5 -2
- package/dist/aria-snapshot.test.js.map +1 -1
- package/dist/aria-snapshot.unit.test.js +83 -41
- package/dist/aria-snapshot.unit.test.js.map +1 -1
- package/dist/assets/cursors/screen-studio/pointer-macos-tahoe-data-url.d.ts +5 -0
- package/dist/assets/cursors/screen-studio/pointer-macos-tahoe-data-url.d.ts.map +1 -0
- package/dist/assets/cursors/screen-studio/pointer-macos-tahoe-data-url.js +5 -0
- package/dist/assets/cursors/screen-studio/pointer-macos-tahoe-data-url.js.map +1 -0
- package/dist/bippy.js +1 -1
- package/dist/cdp-log.d.ts +1 -1
- package/dist/cdp-log.d.ts.map +1 -1
- package/dist/cdp-log.js +1 -1
- package/dist/cdp-log.js.map +1 -1
- package/dist/cdp-relay.d.ts.map +1 -1
- package/dist/cdp-relay.js +492 -298
- package/dist/cdp-relay.js.map +1 -1
- package/dist/cdp-session.d.ts.map +1 -1
- package/dist/cdp-session.js.map +1 -1
- package/dist/cdp-types.d.ts.map +1 -1
- package/dist/cdp-types.js +7 -7
- package/dist/cdp-types.js.map +1 -1
- package/dist/clean-html.d.ts.map +1 -1
- package/dist/clean-html.js +4 -5
- package/dist/clean-html.js.map +1 -1
- package/dist/cli.js +45 -27
- package/dist/cli.js.map +1 -1
- package/dist/create-logger.d.ts.map +1 -1
- package/dist/create-logger.js +3 -1
- package/dist/create-logger.js.map +1 -1
- package/dist/debugger-examples-types.d.ts.map +1 -1
- package/dist/debugger.d.ts.map +1 -1
- package/dist/debugger.js +1 -3
- package/dist/debugger.js.map +1 -1
- package/dist/diff-utils.d.ts.map +1 -1
- package/dist/diff-utils.js +1 -4
- package/dist/diff-utils.js.map +1 -1
- package/dist/editor-api.md +12 -2
- package/dist/editor-examples.d.ts +1 -1
- package/dist/editor-examples.d.ts.map +1 -1
- package/dist/editor-examples.js +1 -1
- package/dist/editor-examples.js.map +1 -1
- package/dist/editor.d.ts +1 -1
- package/dist/editor.d.ts.map +1 -1
- package/dist/editor.js +1 -1
- package/dist/editor.js.map +1 -1
- package/dist/executor.d.ts +26 -3
- package/dist/executor.d.ts.map +1 -1
- package/dist/executor.js +297 -64
- package/dist/executor.js.map +1 -1
- package/dist/executor.unit.test.js +38 -1
- package/dist/executor.unit.test.js.map +1 -1
- package/dist/extension-connection.test.js +139 -36
- package/dist/extension-connection.test.js.map +1 -1
- package/dist/ffmpeg.d.ts +148 -0
- package/dist/ffmpeg.d.ts.map +1 -0
- package/dist/ffmpeg.js +523 -0
- package/dist/ffmpeg.js.map +1 -0
- package/dist/ghost-browser.d.ts.map +1 -1
- package/dist/ghost-browser.js.map +1 -1
- package/dist/ghost-cursor-client.js +287 -0
- package/dist/ghost-cursor.d.ts +27 -0
- package/dist/ghost-cursor.d.ts.map +1 -0
- package/dist/ghost-cursor.js +63 -0
- package/dist/ghost-cursor.js.map +1 -0
- package/dist/htmlrewrite.d.ts.map +1 -1
- package/dist/htmlrewrite.js +17 -55
- package/dist/htmlrewrite.js.map +1 -1
- package/dist/htmlrewrite.test.js.map +1 -1
- package/dist/kill-port.d.ts.map +1 -1
- package/dist/kill-port.js +1 -3
- package/dist/kill-port.js.map +1 -1
- package/dist/locator-selector.test.d.ts +2 -0
- package/dist/locator-selector.test.d.ts.map +1 -0
- package/dist/locator-selector.test.js +96 -0
- package/dist/locator-selector.test.js.map +1 -0
- package/dist/mcp-client.js.map +1 -1
- package/dist/mcp.d.ts.map +1 -1
- package/dist/mcp.js +8 -3
- package/dist/mcp.js.map +1 -1
- package/dist/on-mouse-action.test.d.ts +2 -0
- package/dist/on-mouse-action.test.d.ts.map +1 -0
- package/dist/on-mouse-action.test.js +155 -0
- package/dist/on-mouse-action.test.js.map +1 -0
- package/dist/page-markdown.js +4 -4
- package/dist/page-markdown.js.map +1 -1
- package/dist/prompt.md +450 -377
- package/dist/protocol.d.ts +4 -0
- package/dist/protocol.d.ts.map +1 -1
- package/dist/readability.js +16 -2
- package/dist/recording-ghost-cursor.d.ts +41 -0
- package/dist/recording-ghost-cursor.d.ts.map +1 -0
- package/dist/recording-ghost-cursor.js +79 -0
- package/dist/recording-ghost-cursor.js.map +1 -0
- package/dist/recording-relay.d.ts.map +1 -1
- package/dist/recording-relay.js +8 -8
- package/dist/recording-relay.js.map +1 -1
- package/dist/relay-client.d.ts +17 -4
- package/dist/relay-client.d.ts.map +1 -1
- package/dist/relay-client.js +45 -11
- package/dist/relay-client.js.map +1 -1
- package/dist/relay-core.test.d.ts.map +1 -1
- package/dist/relay-core.test.js +515 -26
- package/dist/relay-core.test.js.map +1 -1
- package/dist/relay-navigation.test.d.ts.map +1 -1
- package/dist/relay-navigation.test.js +169 -31
- package/dist/relay-navigation.test.js.map +1 -1
- package/dist/relay-session.test.d.ts.map +1 -1
- package/dist/relay-session.test.js +113 -65
- package/dist/relay-session.test.js.map +1 -1
- package/dist/relay-state.d.ts +158 -0
- package/dist/relay-state.d.ts.map +1 -0
- package/dist/relay-state.js +306 -0
- package/dist/relay-state.js.map +1 -0
- package/dist/relay-state.test.d.ts +2 -0
- package/dist/relay-state.test.d.ts.map +1 -0
- package/dist/relay-state.test.js +472 -0
- package/dist/relay-state.test.js.map +1 -0
- package/dist/scoped-fs.d.ts.map +1 -1
- package/dist/scoped-fs.js.map +1 -1
- package/dist/screen-recording.d.ts +66 -4
- package/dist/screen-recording.d.ts.map +1 -1
- package/dist/screen-recording.js +150 -13
- package/dist/screen-recording.js.map +1 -1
- package/dist/screen-recording.test.d.ts +2 -0
- package/dist/screen-recording.test.d.ts.map +1 -0
- package/dist/screen-recording.test.js +102 -0
- package/dist/screen-recording.test.js.map +1 -0
- package/dist/selector-generator.js +1 -1
- package/dist/snapshot-tools.test.js +71 -28
- package/dist/snapshot-tools.test.js.map +1 -1
- package/dist/start-relay-server.d.ts +1 -1
- package/dist/start-relay-server.d.ts.map +1 -1
- package/dist/start-relay-server.js +1 -1
- package/dist/start-relay-server.js.map +1 -1
- package/dist/styles-api.md +8 -1
- package/dist/styles-examples.d.ts +1 -1
- package/dist/styles-examples.d.ts.map +1 -1
- package/dist/styles-examples.js +1 -1
- package/dist/styles-examples.js.map +1 -1
- package/dist/styles.d.ts.map +1 -1
- package/dist/styles.js +1 -3
- package/dist/styles.js.map +1 -1
- package/dist/test-declarations.d.ts.map +1 -1
- package/dist/test-utils.d.ts +1 -1
- package/dist/test-utils.d.ts.map +1 -1
- package/dist/test-utils.js +7 -5
- package/dist/test-utils.js.map +1 -1
- package/dist/utils.d.ts.map +1 -1
- package/dist/utils.js.map +1 -1
- package/dist/wait-for-page-load.d.ts.map +1 -1
- package/dist/wait-for-page-load.js +1 -1
- package/dist/wait-for-page-load.js.map +1 -1
- package/package.json +4 -3
- package/src/a11y-client.ts +5 -4
- package/src/aria-snapshot.test.ts +5 -2
- package/src/aria-snapshot.ts +306 -117
- package/src/aria-snapshot.unit.test.ts +199 -141
- package/src/aria-snapshots/github-interactive.txt +2 -0
- package/src/aria-snapshots/github-raw.txt +5 -1
- package/src/aria-snapshots/hackernews-interactive.txt +238 -241
- package/src/aria-snapshots/hackernews-raw.txt +265 -269
- package/src/assets/aria-labels-example.png +0 -0
- package/src/assets/aria-labels-github.png +0 -0
- package/src/assets/aria-labels-hacker-news.png +0 -0
- package/src/assets/aria-labels-old-reddit.png +0 -0
- package/src/assets/cursors/screen-studio/pointer-macos-tahoe-data-url.ts +5 -0
- package/src/assets/cursors/screen-studio/pointer-macos-tahoe.svg +18 -0
- package/src/cdp-log.ts +4 -1
- package/src/cdp-relay.ts +1059 -737
- package/src/cdp-session.ts +12 -3
- package/src/cdp-types.ts +51 -51
- package/src/clean-html.ts +4 -5
- package/src/cli.ts +82 -55
- package/src/create-logger.ts +5 -3
- package/src/debugger-examples-types.ts +4 -1
- package/src/debugger.ts +1 -5
- package/src/diff-utils.ts +2 -5
- package/src/editor-examples.ts +11 -1
- package/src/editor.ts +10 -2
- package/src/executor.ts +374 -73
- package/src/executor.unit.test.ts +48 -1
- package/src/extension-connection.test.ts +612 -488
- package/src/ffmpeg.ts +769 -0
- package/src/ghost-browser.ts +4 -6
- package/src/ghost-cursor-client.ts +369 -0
- package/src/ghost-cursor.ts +110 -0
- package/src/htmlrewrite.test.ts +6 -2
- package/src/htmlrewrite.ts +348 -386
- package/src/kill-port.ts +1 -3
- package/src/locator-selector.test.ts +115 -0
- package/src/mcp-client.ts +1 -1
- package/src/mcp.ts +21 -15
- package/src/on-mouse-action.test.ts +196 -0
- package/src/page-markdown.ts +7 -7
- package/src/protocol.ts +73 -57
- package/src/recording-ghost-cursor.ts +113 -0
- package/src/recording-relay.ts +20 -12
- package/src/relay-client.ts +85 -18
- package/src/relay-core.test.ts +1117 -578
- package/src/relay-navigation.test.ts +648 -483
- package/src/relay-session.test.ts +984 -929
- package/src/relay-state.test.ts +570 -0
- package/src/relay-state.ts +497 -0
- package/src/resource.md +21 -49
- package/src/scoped-fs.ts +9 -3
- package/src/screen-recording.test.ts +111 -0
- package/src/screen-recording.ts +256 -31
- package/src/skill.md +476 -396
- package/src/snapshot-tools.test.ts +580 -528
- package/src/snapshots/shadcn-ui-accessibility-full.md +8 -8
- package/src/snapshots/shadcn-ui-accessibility-interactive.md +8 -8
- package/src/start-relay-server.ts +14 -11
- package/src/styles-examples.ts +8 -1
- package/src/styles.ts +20 -21
- package/src/test-declarations.ts +6 -6
- package/src/test-utils.ts +104 -91
- package/src/utils.ts +2 -1
- package/src/wait-for-page-load.ts +6 -1
package/dist/cdp-relay.js
CHANGED
|
@@ -13,6 +13,7 @@ import { EventEmitter } from 'node:events';
|
|
|
13
13
|
import { VERSION, EXTENSION_IDS } from './utils.js';
|
|
14
14
|
import { createCdpLogger } from './cdp-log.js';
|
|
15
15
|
import { RecordingRelay } from './recording-relay.js';
|
|
16
|
+
import * as relayState from './relay-state.js';
|
|
16
17
|
/**
|
|
17
18
|
* Checks if a target should be filtered out (not exposed to Playwright).
|
|
18
19
|
* Filters extension pages, service workers, and other restricted targets,
|
|
@@ -42,34 +43,64 @@ function isRestrictedTarget(targetInfo) {
|
|
|
42
43
|
}
|
|
43
44
|
export async function startPlayWriterCDPRelayServer({ port = 19988, host = '127.0.0.1', token, logger, cdpLogger, } = {}) {
|
|
44
45
|
const emitter = new EventEmitter();
|
|
45
|
-
const
|
|
46
|
-
const
|
|
46
|
+
const store = relayState.createRelayStore();
|
|
47
|
+
const extensionDownloadBehavior = new Map();
|
|
47
48
|
const resolvedCdpLogger = cdpLogger || createCdpLogger();
|
|
48
49
|
const logCdpJson = (entry) => {
|
|
49
50
|
resolvedCdpLogger.log(entry);
|
|
50
51
|
};
|
|
51
|
-
const playwrightClients = new Map();
|
|
52
52
|
const getDefaultExtensionId = () => {
|
|
53
|
-
return
|
|
53
|
+
return store.getState().extensions.keys().next().value || null;
|
|
54
54
|
};
|
|
55
|
+
/**
|
|
56
|
+
* Resolve an extension by ID, stableKey, or fallback.
|
|
57
|
+
* Returns the unified ExtensionEntry which includes both state and I/O.
|
|
58
|
+
*/
|
|
55
59
|
const getExtensionConnection = (extensionId, options = {}) => {
|
|
60
|
+
const currentRelayState = store.getState();
|
|
61
|
+
const { extensions } = currentRelayState;
|
|
56
62
|
if (extensionId) {
|
|
57
|
-
const direct =
|
|
58
|
-
if (direct) {
|
|
63
|
+
const direct = extensions.get(extensionId);
|
|
64
|
+
if (direct?.ws) {
|
|
59
65
|
return direct;
|
|
60
66
|
}
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
67
|
+
// Try stableKey lookup.
|
|
68
|
+
const byKey = relayState.findExtensionByStableKey(currentRelayState, extensionId);
|
|
69
|
+
if (byKey) {
|
|
70
|
+
const candidates = Array.from(extensions.values())
|
|
71
|
+
.filter((ext) => ext.stableKey === byKey.stableKey)
|
|
72
|
+
.reverse();
|
|
73
|
+
for (const candidate of candidates) {
|
|
74
|
+
if (candidate.ws) {
|
|
75
|
+
return candidate;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
64
78
|
}
|
|
65
79
|
return null;
|
|
66
80
|
}
|
|
67
81
|
if (!options.allowFallback) {
|
|
68
82
|
return null;
|
|
69
83
|
}
|
|
70
|
-
|
|
71
|
-
if (
|
|
72
|
-
|
|
84
|
+
// Single extension — use it directly
|
|
85
|
+
if (extensions.size === 1) {
|
|
86
|
+
const fallbackId = getDefaultExtensionId();
|
|
87
|
+
if (fallbackId) {
|
|
88
|
+
const ext = extensions.get(fallbackId);
|
|
89
|
+
if (ext?.ws) {
|
|
90
|
+
return ext;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
// Multiple extensions — auto-select if exactly one has active targets.
|
|
95
|
+
// This handles the common case of multiple Chrome profiles with the extension
|
|
96
|
+
// installed, where only one profile has playwriter-enabled tabs. (#52)
|
|
97
|
+
if (extensions.size > 1) {
|
|
98
|
+
const activeExtensions = Array.from(extensions.values()).filter((ext) => {
|
|
99
|
+
return ext.connectedTargets.size > 0;
|
|
100
|
+
});
|
|
101
|
+
if (activeExtensions.length === 1 && activeExtensions[0].ws) {
|
|
102
|
+
return activeExtensions[0];
|
|
103
|
+
}
|
|
73
104
|
}
|
|
74
105
|
return null;
|
|
75
106
|
};
|
|
@@ -92,39 +123,41 @@ export async function startPlayWriterCDPRelayServer({ port = 19988, host = '127.
|
|
|
92
123
|
const normalized = String(value);
|
|
93
124
|
return normalized ? normalized : null;
|
|
94
125
|
};
|
|
95
|
-
const getPageTargetForFrameId = ({
|
|
96
|
-
return Array.from(
|
|
126
|
+
const getPageTargetForFrameId = ({ extensionState, frameId, }) => {
|
|
127
|
+
return Array.from(extensionState.connectedTargets.values()).find((target) => {
|
|
97
128
|
return target.targetInfo.type === 'page' && target.frameIds.has(frameId);
|
|
98
129
|
});
|
|
99
130
|
};
|
|
100
131
|
const startExtensionPing = (extensionId) => {
|
|
101
|
-
const
|
|
102
|
-
if (!
|
|
132
|
+
const ext = store.getState().extensions.get(extensionId);
|
|
133
|
+
if (!ext) {
|
|
103
134
|
return;
|
|
104
135
|
}
|
|
105
|
-
if (
|
|
106
|
-
clearInterval(
|
|
136
|
+
if (ext.pingInterval) {
|
|
137
|
+
clearInterval(ext.pingInterval);
|
|
107
138
|
}
|
|
108
|
-
|
|
109
|
-
|
|
139
|
+
const pingInterval = setInterval(() => {
|
|
140
|
+
const latestExt = store.getState().extensions.get(extensionId);
|
|
141
|
+
latestExt?.ws?.send(JSON.stringify({ method: 'ping' }));
|
|
110
142
|
}, 5000);
|
|
143
|
+
store.setState((s) => relayState.updateExtensionIO(s, { extensionId, pingInterval }));
|
|
111
144
|
};
|
|
112
145
|
const stopExtensionPing = (extensionId) => {
|
|
113
|
-
const
|
|
114
|
-
if (!
|
|
146
|
+
const ext = store.getState().extensions.get(extensionId);
|
|
147
|
+
if (!ext || !ext.pingInterval) {
|
|
115
148
|
return;
|
|
116
149
|
}
|
|
117
|
-
clearInterval(
|
|
118
|
-
|
|
150
|
+
clearInterval(ext.pingInterval);
|
|
151
|
+
store.setState((s) => relayState.updateExtensionIO(s, { extensionId, pingInterval: null }));
|
|
119
152
|
};
|
|
120
|
-
function logCdpMessage({ direction, clientId, method, sessionId, params, id, source }) {
|
|
153
|
+
function logCdpMessage({ direction, clientId, method, sessionId, params, id, source, }) {
|
|
121
154
|
const noisyEvents = [
|
|
122
155
|
'Network.requestWillBeSentExtraInfo',
|
|
123
156
|
'Network.responseReceived',
|
|
124
157
|
'Network.responseReceivedExtraInfo',
|
|
125
158
|
'Network.dataReceived',
|
|
126
159
|
'Network.requestWillBeSent',
|
|
127
|
-
'Network.loadingFinished'
|
|
160
|
+
'Network.loadingFinished',
|
|
128
161
|
];
|
|
129
162
|
if (noisyEvents.includes(method)) {
|
|
130
163
|
return;
|
|
@@ -163,9 +196,7 @@ export async function startPlayWriterCDPRelayServer({ port = 19988, host = '127.
|
|
|
163
196
|
}
|
|
164
197
|
}
|
|
165
198
|
function sendToPlaywright({ message, clientId, source = 'extension', extensionId, }) {
|
|
166
|
-
const messageToSend = source === 'server' && 'method' in message
|
|
167
|
-
? { ...message, __serverGenerated: true }
|
|
168
|
-
: message;
|
|
199
|
+
const messageToSend = source === 'server' && 'method' in message ? { ...message, __serverGenerated: true } : message;
|
|
169
200
|
logCdpJson({
|
|
170
201
|
timestamp: new Date().toISOString(),
|
|
171
202
|
direction: 'to-playwright',
|
|
@@ -180,7 +211,7 @@ export async function startPlayWriterCDPRelayServer({ port = 19988, host = '127.
|
|
|
180
211
|
method: message.method,
|
|
181
212
|
sessionId: 'sessionId' in message ? message.sessionId : undefined,
|
|
182
213
|
params: 'params' in message ? message.params : undefined,
|
|
183
|
-
source
|
|
214
|
+
source,
|
|
184
215
|
});
|
|
185
216
|
}
|
|
186
217
|
const messageStr = JSON.stringify(messageToSend);
|
|
@@ -200,14 +231,14 @@ export async function startPlayWriterCDPRelayServer({ port = 19988, host = '127.
|
|
|
200
231
|
}
|
|
201
232
|
};
|
|
202
233
|
if (clientId) {
|
|
203
|
-
const client = playwrightClients.get(clientId);
|
|
234
|
+
const client = store.getState().playwrightClients.get(clientId);
|
|
204
235
|
if (client) {
|
|
205
236
|
safeSend(client);
|
|
206
237
|
}
|
|
207
238
|
}
|
|
208
239
|
else {
|
|
209
|
-
const
|
|
210
|
-
for (const client of
|
|
240
|
+
const { playwrightClients } = store.getState();
|
|
241
|
+
for (const client of playwrightClients.values()) {
|
|
211
242
|
if (extensionId && client.extensionId !== extensionId) {
|
|
212
243
|
continue;
|
|
213
244
|
}
|
|
@@ -227,11 +258,25 @@ export async function startPlayWriterCDPRelayServer({ port = 19988, host = '127.
|
|
|
227
258
|
return { method: record.method, sessionId, params: record.params };
|
|
228
259
|
}
|
|
229
260
|
async function sendToExtension({ extensionId, method, params, timeout = 30000, }) {
|
|
230
|
-
const
|
|
231
|
-
if (!
|
|
261
|
+
const conn = getExtensionConnection(extensionId);
|
|
262
|
+
if (!conn) {
|
|
263
|
+
throw new Error('Extension not connected');
|
|
264
|
+
}
|
|
265
|
+
const resolvedExtensionId = conn.id;
|
|
266
|
+
let id = 0;
|
|
267
|
+
store.setState((s) => {
|
|
268
|
+
const ext = s.extensions.get(resolvedExtensionId);
|
|
269
|
+
if (!ext) {
|
|
270
|
+
return s;
|
|
271
|
+
}
|
|
272
|
+
id = ext.messageId + 1;
|
|
273
|
+
const newExtensions = new Map(s.extensions);
|
|
274
|
+
newExtensions.set(resolvedExtensionId, { ...ext, messageId: id });
|
|
275
|
+
return { ...s, extensions: newExtensions };
|
|
276
|
+
});
|
|
277
|
+
if (!id) {
|
|
232
278
|
throw new Error('Extension not connected');
|
|
233
279
|
}
|
|
234
|
-
const id = ++connection.messageId;
|
|
235
280
|
const message = { id, method, params };
|
|
236
281
|
const forwardCdpParams = method === 'forwardCDPCommand' ? getForwardCdpParams(params) : undefined;
|
|
237
282
|
if (forwardCdpParams) {
|
|
@@ -245,13 +290,15 @@ export async function startPlayWriterCDPRelayServer({ port = 19988, host = '127.
|
|
|
245
290
|
},
|
|
246
291
|
});
|
|
247
292
|
}
|
|
248
|
-
connection.ws.send(JSON.stringify(message));
|
|
249
293
|
return new Promise((resolve, reject) => {
|
|
250
294
|
const timeoutId = setTimeout(() => {
|
|
251
|
-
|
|
295
|
+
store.setState((s) => relayState.removeExtensionPendingRequest(s, {
|
|
296
|
+
extensionId: resolvedExtensionId,
|
|
297
|
+
requestId: id,
|
|
298
|
+
}));
|
|
252
299
|
reject(new Error(`Extension request timeout after ${timeout}ms: ${method}`));
|
|
253
300
|
}, timeout);
|
|
254
|
-
|
|
301
|
+
const pendingRequest = {
|
|
255
302
|
resolve: (result) => {
|
|
256
303
|
clearTimeout(timeoutId);
|
|
257
304
|
resolve(result);
|
|
@@ -259,21 +306,63 @@ export async function startPlayWriterCDPRelayServer({ port = 19988, host = '127.
|
|
|
259
306
|
reject: (error) => {
|
|
260
307
|
clearTimeout(timeoutId);
|
|
261
308
|
reject(error);
|
|
262
|
-
}
|
|
263
|
-
}
|
|
309
|
+
},
|
|
310
|
+
};
|
|
311
|
+
store.setState((s) => relayState.addExtensionPendingRequest(s, {
|
|
312
|
+
extensionId: resolvedExtensionId,
|
|
313
|
+
requestId: id,
|
|
314
|
+
pendingRequest,
|
|
315
|
+
}));
|
|
316
|
+
const latestExt = store.getState().extensions.get(resolvedExtensionId);
|
|
317
|
+
if (!latestExt?.ws) {
|
|
318
|
+
clearTimeout(timeoutId);
|
|
319
|
+
store.setState((s) => relayState.removeExtensionPendingRequest(s, {
|
|
320
|
+
extensionId: resolvedExtensionId,
|
|
321
|
+
requestId: id,
|
|
322
|
+
}));
|
|
323
|
+
reject(new Error('Extension not connected'));
|
|
324
|
+
return;
|
|
325
|
+
}
|
|
326
|
+
try {
|
|
327
|
+
latestExt.ws.send(JSON.stringify(message));
|
|
328
|
+
}
|
|
329
|
+
catch (error) {
|
|
330
|
+
clearTimeout(timeoutId);
|
|
331
|
+
store.setState((s) => relayState.removeExtensionPendingRequest(s, {
|
|
332
|
+
extensionId: resolvedExtensionId,
|
|
333
|
+
requestId: id,
|
|
334
|
+
}));
|
|
335
|
+
const sendError = error instanceof Error ? error : new Error(String(error));
|
|
336
|
+
reject(new Error(`Extension send failed: ${method}`, { cause: sendError }));
|
|
337
|
+
}
|
|
264
338
|
});
|
|
265
339
|
}
|
|
266
340
|
const recordingRelays = new Map();
|
|
341
|
+
// Find which extension connection owns a CDP tab session ID (pw-tab-*).
|
|
342
|
+
// Used by recording routes where sessionId identifies the target tab.
|
|
343
|
+
// Delegates to the pure derivation function from relay-state.ts.
|
|
344
|
+
const findExtensionIdByCdpSession = (cdpSessionId) => {
|
|
345
|
+
return relayState.findExtensionIdByCdpSession(store.getState(), cdpSessionId);
|
|
346
|
+
};
|
|
347
|
+
// Resolve recording route session ID (CDP tab session) to extension connection.
|
|
348
|
+
const resolveRecordingRoute = async ({ sessionId, }) => {
|
|
349
|
+
if (!sessionId) {
|
|
350
|
+
return { extensionId: null, sessionId: null };
|
|
351
|
+
}
|
|
352
|
+
const extensionId = findExtensionIdByCdpSession(sessionId);
|
|
353
|
+
return { extensionId, sessionId };
|
|
354
|
+
};
|
|
267
355
|
const getRecordingRelay = (extensionId) => {
|
|
268
|
-
const allowDefault = !extensionId &&
|
|
269
|
-
const
|
|
270
|
-
if (!
|
|
356
|
+
const allowDefault = !extensionId && store.getState().extensions.size === 1;
|
|
357
|
+
const conn = getExtensionConnection(extensionId, { allowFallback: allowDefault });
|
|
358
|
+
if (!conn) {
|
|
271
359
|
return null;
|
|
272
360
|
}
|
|
273
|
-
|
|
274
|
-
|
|
361
|
+
const connId = conn.id;
|
|
362
|
+
if (!recordingRelays.has(connId)) {
|
|
363
|
+
recordingRelays.set(connId, new RecordingRelay((params) => sendToExtension({ extensionId: connId, ...params }), () => store.getState().extensions.has(connId), logger));
|
|
275
364
|
}
|
|
276
|
-
return recordingRelays.get(
|
|
365
|
+
return recordingRelays.get(connId) || null;
|
|
277
366
|
};
|
|
278
367
|
// Auto-create initial tab when PLAYWRITER_AUTO_ENABLE is set and no targets exist.
|
|
279
368
|
// This allows Playwright to connect and immediately have a page to work with.
|
|
@@ -281,33 +370,97 @@ export async function startPlayWriterCDPRelayServer({ port = 19988, host = '127.
|
|
|
281
370
|
if (!process.env.PLAYWRITER_AUTO_ENABLE) {
|
|
282
371
|
return;
|
|
283
372
|
}
|
|
284
|
-
const
|
|
285
|
-
if (!
|
|
373
|
+
const conn = getExtensionConnection(extensionId);
|
|
374
|
+
if (!conn) {
|
|
286
375
|
return;
|
|
287
376
|
}
|
|
288
|
-
if (
|
|
377
|
+
if (conn.connectedTargets.size > 0) {
|
|
289
378
|
return;
|
|
290
379
|
}
|
|
291
380
|
try {
|
|
292
381
|
logger?.log(pc.blue('Auto-creating initial tab for Playwright client'));
|
|
293
|
-
const result = await sendToExtension({ extensionId, method: 'createInitialTab', timeout: 10000 });
|
|
382
|
+
const result = (await sendToExtension({ extensionId, method: 'createInitialTab', timeout: 10000 }));
|
|
294
383
|
if (result.success && result.sessionId && result.targetInfo) {
|
|
295
|
-
|
|
384
|
+
store.setState((s) => relayState.addTarget(s, {
|
|
385
|
+
extensionId,
|
|
296
386
|
sessionId: result.sessionId,
|
|
297
387
|
targetId: result.targetInfo.targetId,
|
|
298
388
|
targetInfo: result.targetInfo,
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
logger?.log(pc.blue(`Auto-created tab, now have ${
|
|
389
|
+
}));
|
|
390
|
+
const updatedTargets = store.getState().extensions.get(extensionId)?.connectedTargets.size || 0;
|
|
391
|
+
logger?.log(pc.blue(`Auto-created tab, now have ${updatedTargets} targets, url: ${result.targetInfo.url}`));
|
|
302
392
|
}
|
|
303
393
|
}
|
|
304
394
|
catch (e) {
|
|
305
395
|
logger?.error('Failed to auto-create initial tab:', e);
|
|
306
396
|
}
|
|
307
397
|
}
|
|
398
|
+
function getPageTargetSessionIds({ extensionId }) {
|
|
399
|
+
const extensionState = store.getState().extensions.get(extensionId);
|
|
400
|
+
if (!extensionState) {
|
|
401
|
+
return [];
|
|
402
|
+
}
|
|
403
|
+
return Array.from(extensionState.connectedTargets.values())
|
|
404
|
+
.filter((target) => {
|
|
405
|
+
return target.targetInfo.type === 'page';
|
|
406
|
+
})
|
|
407
|
+
.map((target) => {
|
|
408
|
+
return target.sessionId;
|
|
409
|
+
});
|
|
410
|
+
}
|
|
411
|
+
function maybeEmitBrowserDownloadCompatEvent({ method, params, extensionId, }) {
|
|
412
|
+
const browserEventMethod = method === 'Page.downloadWillBegin'
|
|
413
|
+
? 'Browser.downloadWillBegin'
|
|
414
|
+
: method === 'Page.downloadProgress'
|
|
415
|
+
? 'Browser.downloadProgress'
|
|
416
|
+
: null;
|
|
417
|
+
if (!browserEventMethod) {
|
|
418
|
+
return;
|
|
419
|
+
}
|
|
420
|
+
sendToPlaywright({
|
|
421
|
+
message: {
|
|
422
|
+
method: browserEventMethod,
|
|
423
|
+
params,
|
|
424
|
+
},
|
|
425
|
+
source: 'server',
|
|
426
|
+
extensionId,
|
|
427
|
+
});
|
|
428
|
+
}
|
|
429
|
+
async function applyDownloadBehaviorToTargets({ extensionId, behavior, source, targetSessionIds, }) {
|
|
430
|
+
const pageBehavior = behavior.behavior === 'allowAndName' ? 'allow' : behavior.behavior;
|
|
431
|
+
const pageParams = (() => {
|
|
432
|
+
if (pageBehavior === 'allow' && behavior.downloadPath) {
|
|
433
|
+
return { behavior: pageBehavior, downloadPath: behavior.downloadPath };
|
|
434
|
+
}
|
|
435
|
+
return { behavior: pageBehavior };
|
|
436
|
+
})();
|
|
437
|
+
const sessions = targetSessionIds || getPageTargetSessionIds({ extensionId });
|
|
438
|
+
if (sessions.length === 0) {
|
|
439
|
+
return;
|
|
440
|
+
}
|
|
441
|
+
await Promise.all(sessions.map(async (targetSessionId) => {
|
|
442
|
+
try {
|
|
443
|
+
await sendToExtension({
|
|
444
|
+
extensionId,
|
|
445
|
+
method: 'forwardCDPCommand',
|
|
446
|
+
params: {
|
|
447
|
+
sessionId: targetSessionId,
|
|
448
|
+
method: 'Page.setDownloadBehavior',
|
|
449
|
+
params: pageParams,
|
|
450
|
+
source,
|
|
451
|
+
},
|
|
452
|
+
});
|
|
453
|
+
}
|
|
454
|
+
catch (error) {
|
|
455
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
456
|
+
logger?.log(pc.yellow(`[Server] Failed to apply Page.setDownloadBehavior to ${targetSessionId}: ${message}`));
|
|
457
|
+
}
|
|
458
|
+
}));
|
|
459
|
+
}
|
|
308
460
|
async function routeCdpCommand({ extensionId, method, params, sessionId, source, }) {
|
|
309
|
-
const
|
|
310
|
-
const connectedTargets =
|
|
461
|
+
const conn = getExtensionConnection(extensionId);
|
|
462
|
+
const connectedTargets = conn?.connectedTargets || new Map();
|
|
463
|
+
const resolvedExtensionId = conn?.id || extensionId;
|
|
311
464
|
switch (method) {
|
|
312
465
|
case 'Browser.getVersion': {
|
|
313
466
|
return {
|
|
@@ -315,10 +468,22 @@ export async function startPlayWriterCDPRelayServer({ port = 19988, host = '127.
|
|
|
315
468
|
product: 'Chrome/Extension-Bridge',
|
|
316
469
|
revision: '1.0.0',
|
|
317
470
|
userAgent: 'CDP-Bridge-Server/1.0.0',
|
|
318
|
-
jsVersion: 'V8'
|
|
471
|
+
jsVersion: 'V8',
|
|
319
472
|
};
|
|
320
473
|
}
|
|
321
474
|
case 'Browser.setDownloadBehavior': {
|
|
475
|
+
const downloadBehaviorParams = params;
|
|
476
|
+
if (!downloadBehaviorParams?.behavior) {
|
|
477
|
+
throw new Error('behavior is required for Browser.setDownloadBehavior');
|
|
478
|
+
}
|
|
479
|
+
if (resolvedExtensionId) {
|
|
480
|
+
extensionDownloadBehavior.set(resolvedExtensionId, downloadBehaviorParams);
|
|
481
|
+
await applyDownloadBehaviorToTargets({
|
|
482
|
+
extensionId: resolvedExtensionId,
|
|
483
|
+
behavior: downloadBehaviorParams,
|
|
484
|
+
source,
|
|
485
|
+
});
|
|
486
|
+
}
|
|
322
487
|
return {};
|
|
323
488
|
}
|
|
324
489
|
// Target.setAutoAttach is a CDP command Playwright sends on first connection.
|
|
@@ -328,15 +493,15 @@ export async function startPlayWriterCDPRelayServer({ port = 19988, host = '127.
|
|
|
328
493
|
if (sessionId) {
|
|
329
494
|
break;
|
|
330
495
|
}
|
|
331
|
-
if (
|
|
332
|
-
await maybeAutoCreateInitialTab(
|
|
496
|
+
if (conn) {
|
|
497
|
+
await maybeAutoCreateInitialTab(conn.id);
|
|
333
498
|
}
|
|
334
499
|
// Forward auto-attach so Chrome emits iframe Target.attachedToTarget events.
|
|
335
500
|
// Playwright relies on these (with parentFrameId) when reconnecting over CDP.
|
|
336
501
|
await sendToExtension({
|
|
337
|
-
extensionId:
|
|
502
|
+
extensionId: resolvedExtensionId,
|
|
338
503
|
method: 'forwardCDPCommand',
|
|
339
|
-
params: { method, params, source }
|
|
504
|
+
params: { method, params, source },
|
|
340
505
|
});
|
|
341
506
|
return {};
|
|
342
507
|
}
|
|
@@ -344,19 +509,20 @@ export async function startPlayWriterCDPRelayServer({ port = 19988, host = '127.
|
|
|
344
509
|
return {};
|
|
345
510
|
}
|
|
346
511
|
case 'Target.attachToTarget': {
|
|
347
|
-
const
|
|
348
|
-
if (!targetId) {
|
|
512
|
+
const attachParams = params;
|
|
513
|
+
if (!attachParams?.targetId) {
|
|
349
514
|
throw new Error('targetId is required for Target.attachToTarget');
|
|
350
515
|
}
|
|
351
516
|
for (const target of connectedTargets.values()) {
|
|
352
|
-
if (target.targetId === targetId) {
|
|
517
|
+
if (target.targetId === attachParams.targetId) {
|
|
353
518
|
return { sessionId: target.sessionId };
|
|
354
519
|
}
|
|
355
520
|
}
|
|
356
|
-
throw new Error(`Target ${targetId} not found in connected targets`);
|
|
521
|
+
throw new Error(`Target ${attachParams.targetId} not found in connected targets`);
|
|
357
522
|
}
|
|
358
523
|
case 'Target.getTargetInfo': {
|
|
359
|
-
const
|
|
524
|
+
const infoReqParams = params;
|
|
525
|
+
const targetId = infoReqParams?.targetId;
|
|
360
526
|
if (targetId) {
|
|
361
527
|
for (const target of connectedTargets.values()) {
|
|
362
528
|
if (target.targetId === targetId) {
|
|
@@ -379,30 +545,30 @@ export async function startPlayWriterCDPRelayServer({ port = 19988, host = '127.
|
|
|
379
545
|
.filter((t) => !isRestrictedTarget(t.targetInfo))
|
|
380
546
|
.map((t) => ({
|
|
381
547
|
...t.targetInfo,
|
|
382
|
-
attached: true
|
|
383
|
-
}))
|
|
548
|
+
attached: true,
|
|
549
|
+
})),
|
|
384
550
|
};
|
|
385
551
|
}
|
|
386
552
|
case 'Target.createTarget': {
|
|
387
553
|
return await sendToExtension({
|
|
388
|
-
extensionId:
|
|
554
|
+
extensionId: resolvedExtensionId,
|
|
389
555
|
method: 'forwardCDPCommand',
|
|
390
|
-
params: { method, params, source }
|
|
556
|
+
params: { method, params, source },
|
|
391
557
|
});
|
|
392
558
|
}
|
|
393
559
|
case 'Target.closeTarget': {
|
|
394
560
|
return await sendToExtension({
|
|
395
|
-
extensionId:
|
|
561
|
+
extensionId: resolvedExtensionId,
|
|
396
562
|
method: 'forwardCDPCommand',
|
|
397
|
-
params: { method, params, source }
|
|
563
|
+
params: { method, params, source },
|
|
398
564
|
});
|
|
399
565
|
}
|
|
400
566
|
// Ghost Browser API - forward to extension for chrome.ghostPublicAPI/ghostProxies/projects
|
|
401
567
|
case 'ghost-browser': {
|
|
402
568
|
return await sendToExtension({
|
|
403
|
-
extensionId:
|
|
569
|
+
extensionId: resolvedExtensionId,
|
|
404
570
|
method: 'ghost-browser',
|
|
405
|
-
params
|
|
571
|
+
params,
|
|
406
572
|
});
|
|
407
573
|
}
|
|
408
574
|
case 'Runtime.enable': {
|
|
@@ -428,18 +594,18 @@ export async function startPlayWriterCDPRelayServer({ port = 19988, host = '127.
|
|
|
428
594
|
emitter.on('cdp:event', handler);
|
|
429
595
|
});
|
|
430
596
|
const result = await sendToExtension({
|
|
431
|
-
extensionId:
|
|
597
|
+
extensionId: resolvedExtensionId,
|
|
432
598
|
method: 'forwardCDPCommand',
|
|
433
|
-
params: { sessionId, method, params, source }
|
|
599
|
+
params: { sessionId, method, params, source },
|
|
434
600
|
});
|
|
435
601
|
await contextCreatedPromise;
|
|
436
602
|
return result;
|
|
437
603
|
}
|
|
438
604
|
}
|
|
439
605
|
return await sendToExtension({
|
|
440
|
-
extensionId:
|
|
606
|
+
extensionId: resolvedExtensionId,
|
|
441
607
|
method: 'forwardCDPCommand',
|
|
442
|
-
params: { sessionId, method, params, source }
|
|
608
|
+
params: { sessionId, method, params, source },
|
|
443
609
|
});
|
|
444
610
|
}
|
|
445
611
|
const app = new Hono();
|
|
@@ -472,7 +638,7 @@ export async function startPlayWriterCDPRelayServer({ port = 19988, host = '127.
|
|
|
472
638
|
});
|
|
473
639
|
app.get('/extension/status', (c) => {
|
|
474
640
|
const defaultExtension = getExtensionConnection(null, { allowFallback: true });
|
|
475
|
-
const connected =
|
|
641
|
+
const connected = store.getState().extensions.size > 0;
|
|
476
642
|
const activeTargets = defaultExtension?.connectedTargets.size || 0;
|
|
477
643
|
const info = defaultExtension?.info;
|
|
478
644
|
return c.json({
|
|
@@ -484,14 +650,14 @@ export async function startPlayWriterCDPRelayServer({ port = 19988, host = '127.
|
|
|
484
650
|
});
|
|
485
651
|
});
|
|
486
652
|
app.get('/extensions/status', (c) => {
|
|
487
|
-
const extensions = Array.from(
|
|
653
|
+
const extensions = Array.from(store.getState().extensions.values()).map((ext) => {
|
|
488
654
|
return {
|
|
489
|
-
extensionId:
|
|
490
|
-
stableKey:
|
|
491
|
-
browser:
|
|
492
|
-
profile:
|
|
493
|
-
activeTargets:
|
|
494
|
-
playwriterVersion:
|
|
655
|
+
extensionId: ext.id,
|
|
656
|
+
stableKey: ext.stableKey,
|
|
657
|
+
browser: ext.info.browser || null,
|
|
658
|
+
profile: ext.info ? { email: ext.info.email || '', id: ext.info.id || '' } : null,
|
|
659
|
+
activeTargets: ext.connectedTargets.size,
|
|
660
|
+
playwriterVersion: ext.info?.version || null,
|
|
495
661
|
};
|
|
496
662
|
});
|
|
497
663
|
return c.json({ extensions });
|
|
@@ -502,68 +668,68 @@ export async function startPlayWriterCDPRelayServer({ port = 19988, host = '127.
|
|
|
502
668
|
app
|
|
503
669
|
.on(['GET', 'PUT'], '/json/version', (c) => {
|
|
504
670
|
return c.json({
|
|
505
|
-
|
|
671
|
+
Browser: `Playwriter/${VERSION}`,
|
|
506
672
|
'Protocol-Version': '1.3',
|
|
507
|
-
|
|
673
|
+
webSocketDebuggerUrl: getCdpWsUrl(c),
|
|
508
674
|
});
|
|
509
675
|
})
|
|
510
676
|
.on(['GET', 'PUT'], '/json/version/', (c) => {
|
|
511
677
|
return c.json({
|
|
512
|
-
|
|
678
|
+
Browser: `Playwriter/${VERSION}`,
|
|
513
679
|
'Protocol-Version': '1.3',
|
|
514
|
-
|
|
680
|
+
webSocketDebuggerUrl: getCdpWsUrl(c),
|
|
515
681
|
});
|
|
516
682
|
})
|
|
517
683
|
.on(['GET', 'PUT'], '/json/list', (c) => {
|
|
518
684
|
const wsUrl = getCdpWsUrl(c);
|
|
519
685
|
const defaultTargets = getExtensionConnection(null, { allowFallback: true })?.connectedTargets || new Map();
|
|
520
|
-
return c.json(Array.from(defaultTargets.values()).map(t => ({
|
|
686
|
+
return c.json(Array.from(defaultTargets.values()).map((t) => ({
|
|
521
687
|
id: t.targetId,
|
|
522
688
|
type: t.targetInfo.type,
|
|
523
689
|
title: t.targetInfo.title,
|
|
524
690
|
description: t.targetInfo.title,
|
|
525
691
|
url: t.targetInfo.url,
|
|
526
692
|
webSocketDebuggerUrl: wsUrl,
|
|
527
|
-
devtoolsFrontendUrl: `/devtools/inspector.html?ws=${wsUrl.replace('ws://', '')}
|
|
693
|
+
devtoolsFrontendUrl: `/devtools/inspector.html?ws=${wsUrl.replace('ws://', '')}`,
|
|
528
694
|
})));
|
|
529
695
|
})
|
|
530
696
|
.on(['GET', 'PUT'], '/json/list/', (c) => {
|
|
531
697
|
const wsUrl = getCdpWsUrl(c);
|
|
532
698
|
const defaultTargets = getExtensionConnection(null, { allowFallback: true })?.connectedTargets || new Map();
|
|
533
|
-
return c.json(Array.from(defaultTargets.values()).map(t => ({
|
|
699
|
+
return c.json(Array.from(defaultTargets.values()).map((t) => ({
|
|
534
700
|
id: t.targetId,
|
|
535
701
|
type: t.targetInfo.type,
|
|
536
702
|
title: t.targetInfo.title,
|
|
537
703
|
description: t.targetInfo.title,
|
|
538
704
|
url: t.targetInfo.url,
|
|
539
705
|
webSocketDebuggerUrl: wsUrl,
|
|
540
|
-
devtoolsFrontendUrl: `/devtools/inspector.html?ws=${wsUrl.replace('ws://', '')}
|
|
706
|
+
devtoolsFrontendUrl: `/devtools/inspector.html?ws=${wsUrl.replace('ws://', '')}`,
|
|
541
707
|
})));
|
|
542
708
|
})
|
|
543
709
|
.on(['GET', 'PUT'], '/json', (c) => {
|
|
544
710
|
const wsUrl = getCdpWsUrl(c);
|
|
545
711
|
const defaultTargets = getExtensionConnection(null, { allowFallback: true })?.connectedTargets || new Map();
|
|
546
|
-
return c.json(Array.from(defaultTargets.values()).map(t => ({
|
|
712
|
+
return c.json(Array.from(defaultTargets.values()).map((t) => ({
|
|
547
713
|
id: t.targetId,
|
|
548
714
|
type: t.targetInfo.type,
|
|
549
715
|
title: t.targetInfo.title,
|
|
550
716
|
description: t.targetInfo.title,
|
|
551
717
|
url: t.targetInfo.url,
|
|
552
718
|
webSocketDebuggerUrl: wsUrl,
|
|
553
|
-
devtoolsFrontendUrl: `/devtools/inspector.html?ws=${wsUrl.replace('ws://', '')}
|
|
719
|
+
devtoolsFrontendUrl: `/devtools/inspector.html?ws=${wsUrl.replace('ws://', '')}`,
|
|
554
720
|
})));
|
|
555
721
|
})
|
|
556
722
|
.on(['GET', 'PUT'], '/json/', (c) => {
|
|
557
723
|
const wsUrl = getCdpWsUrl(c);
|
|
558
724
|
const defaultTargets = getExtensionConnection(null, { allowFallback: true })?.connectedTargets || new Map();
|
|
559
|
-
return c.json(Array.from(defaultTargets.values()).map(t => ({
|
|
725
|
+
return c.json(Array.from(defaultTargets.values()).map((t) => ({
|
|
560
726
|
id: t.targetId,
|
|
561
727
|
type: t.targetInfo.type,
|
|
562
728
|
title: t.targetInfo.title,
|
|
563
729
|
description: t.targetInfo.title,
|
|
564
730
|
url: t.targetInfo.url,
|
|
565
731
|
webSocketDebuggerUrl: wsUrl,
|
|
566
|
-
devtoolsFrontendUrl: `/devtools/inspector.html?ws=${wsUrl.replace('ws://', '')}
|
|
732
|
+
devtoolsFrontendUrl: `/devtools/inspector.html?ws=${wsUrl.replace('ws://', '')}`,
|
|
567
733
|
})));
|
|
568
734
|
});
|
|
569
735
|
app.post('/mcp-log', async (c) => {
|
|
@@ -610,13 +776,19 @@ export async function startPlayWriterCDPRelayServer({ port = 19988, host = '127.
|
|
|
610
776
|
const clientId = c.req.param('clientId') || 'default';
|
|
611
777
|
const url = new URL(c.req.url, 'http://localhost');
|
|
612
778
|
const requestedExtensionId = url.searchParams.get('extensionId');
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
const
|
|
616
|
-
|
|
779
|
+
// When extensionId is explicit, resolve directly. Otherwise use fallback which
|
|
780
|
+
// handles single-extension and uniquely-active-extension cases (#52).
|
|
781
|
+
const resolvedExtension = requestedExtensionId
|
|
782
|
+
? getExtensionConnection(requestedExtensionId)
|
|
783
|
+
: getExtensionConnection(null, { allowFallback: true });
|
|
784
|
+
const clientExtensionId = resolvedExtension?.id || null;
|
|
785
|
+
const getBoundExtensionIdForClient = () => {
|
|
786
|
+
const client = store.getState().playwrightClients.get(clientId);
|
|
787
|
+
return client?.extensionId || null;
|
|
788
|
+
};
|
|
617
789
|
return {
|
|
618
790
|
async onOpen(_event, ws) {
|
|
619
|
-
if (playwrightClients.has(clientId)) {
|
|
791
|
+
if (store.getState().playwrightClients.has(clientId)) {
|
|
620
792
|
logger?.log(pc.yellow(`Rejecting duplicate Playwright clientId: ${clientId}`));
|
|
621
793
|
ws.close(4004, 'Duplicate Playwright clientId');
|
|
622
794
|
return;
|
|
@@ -630,10 +802,12 @@ export async function startPlayWriterCDPRelayServer({ port = 19988, host = '127.
|
|
|
630
802
|
return;
|
|
631
803
|
}
|
|
632
804
|
// Add client first so it can receive Target.attachedToTarget events
|
|
633
|
-
|
|
805
|
+
store.setState((s) => {
|
|
806
|
+
return relayState.addPlaywrightClient(s, { id: clientId, extensionId: clientExtensionId, ws });
|
|
807
|
+
});
|
|
634
808
|
const extensionConnection = getExtensionConnection(clientExtensionId);
|
|
635
809
|
const targetCount = extensionConnection?.connectedTargets.size || 0;
|
|
636
|
-
logger?.log(pc.green(`Playwright client connected: ${clientId} (${playwrightClients.size} total) (extension? ${!!extensionConnection}) (${targetCount} pages)`));
|
|
810
|
+
logger?.log(pc.green(`Playwright client connected: ${clientId} (${store.getState().playwrightClients.size} total) (extension? ${!!extensionConnection}) (${targetCount} pages)`));
|
|
637
811
|
},
|
|
638
812
|
async onMessage(event, ws) {
|
|
639
813
|
let message;
|
|
@@ -655,25 +829,35 @@ export async function startPlayWriterCDPRelayServer({ port = 19988, host = '127.
|
|
|
655
829
|
clientId,
|
|
656
830
|
method,
|
|
657
831
|
sessionId,
|
|
658
|
-
id
|
|
832
|
+
id,
|
|
659
833
|
});
|
|
660
834
|
emitter.emit('cdp:command', { clientId, command: message });
|
|
661
|
-
const
|
|
662
|
-
|
|
835
|
+
const boundExtensionId = getBoundExtensionIdForClient();
|
|
836
|
+
const extensionConn = getExtensionConnection(boundExtensionId);
|
|
837
|
+
if (!extensionConn) {
|
|
663
838
|
sendToPlaywright({
|
|
664
839
|
message: {
|
|
665
840
|
id,
|
|
666
841
|
sessionId,
|
|
667
|
-
error: { message: 'Extension not connected' }
|
|
842
|
+
error: { message: 'Extension not connected' },
|
|
668
843
|
},
|
|
669
|
-
clientId
|
|
844
|
+
clientId,
|
|
670
845
|
});
|
|
671
846
|
return;
|
|
672
847
|
}
|
|
673
848
|
try {
|
|
674
|
-
const result = await routeCdpCommand({
|
|
849
|
+
const result = await routeCdpCommand({
|
|
850
|
+
extensionId: extensionConn.id,
|
|
851
|
+
method,
|
|
852
|
+
params,
|
|
853
|
+
sessionId,
|
|
854
|
+
source,
|
|
855
|
+
});
|
|
675
856
|
if (method === 'Target.setAutoAttach' && !sessionId) {
|
|
676
|
-
|
|
857
|
+
// Re-read state after async routeCdpCommand — targets may have changed
|
|
858
|
+
const freshExt = store.getState().extensions.get(extensionConn.id);
|
|
859
|
+
const freshTargets = freshExt?.connectedTargets || new Map();
|
|
860
|
+
for (const target of freshTargets.values()) {
|
|
677
861
|
// Skip restricted targets (extensions, chrome:// URLs, non-page types)
|
|
678
862
|
if (isRestrictedTarget(target.targetInfo)) {
|
|
679
863
|
continue;
|
|
@@ -684,10 +868,10 @@ export async function startPlayWriterCDPRelayServer({ port = 19988, host = '127.
|
|
|
684
868
|
sessionId: target.sessionId,
|
|
685
869
|
targetInfo: {
|
|
686
870
|
...target.targetInfo,
|
|
687
|
-
attached: true
|
|
871
|
+
attached: true,
|
|
688
872
|
},
|
|
689
|
-
waitingForDebugger: false
|
|
690
|
-
}
|
|
873
|
+
waitingForDebugger: false,
|
|
874
|
+
},
|
|
691
875
|
};
|
|
692
876
|
if (!target.targetInfo.url) {
|
|
693
877
|
logger?.error(pc.red('[Server] WARNING: Target.attachedToTarget sent with empty URL!'), JSON.stringify(attachedPayload));
|
|
@@ -696,12 +880,14 @@ export async function startPlayWriterCDPRelayServer({ port = 19988, host = '127.
|
|
|
696
880
|
sendToPlaywright({
|
|
697
881
|
message: attachedPayload,
|
|
698
882
|
clientId,
|
|
699
|
-
source: 'server'
|
|
883
|
+
source: 'server',
|
|
700
884
|
});
|
|
701
885
|
}
|
|
702
886
|
}
|
|
703
887
|
if (method === 'Target.setDiscoverTargets' && params?.discover) {
|
|
704
|
-
|
|
888
|
+
const freshExt2 = store.getState().extensions.get(extensionConn.id);
|
|
889
|
+
const freshTargets2 = freshExt2?.connectedTargets || new Map();
|
|
890
|
+
for (const target of freshTargets2.values()) {
|
|
705
891
|
// Skip restricted targets (extensions, chrome:// URLs, non-page types)
|
|
706
892
|
if (isRestrictedTarget(target.targetInfo)) {
|
|
707
893
|
continue;
|
|
@@ -711,9 +897,9 @@ export async function startPlayWriterCDPRelayServer({ port = 19988, host = '127.
|
|
|
711
897
|
params: {
|
|
712
898
|
targetInfo: {
|
|
713
899
|
...target.targetInfo,
|
|
714
|
-
attached: true
|
|
715
|
-
}
|
|
716
|
-
}
|
|
900
|
+
attached: true,
|
|
901
|
+
},
|
|
902
|
+
},
|
|
717
903
|
};
|
|
718
904
|
if (!target.targetInfo.url) {
|
|
719
905
|
logger?.error(pc.red('[Server] WARNING: Target.targetCreated sent with empty URL!'), JSON.stringify(targetCreatedPayload));
|
|
@@ -722,34 +908,41 @@ export async function startPlayWriterCDPRelayServer({ port = 19988, host = '127.
|
|
|
722
908
|
sendToPlaywright({
|
|
723
909
|
message: targetCreatedPayload,
|
|
724
910
|
clientId,
|
|
725
|
-
source: 'server'
|
|
911
|
+
source: 'server',
|
|
726
912
|
});
|
|
727
913
|
}
|
|
728
914
|
}
|
|
729
|
-
if (method === 'Target.attachToTarget'
|
|
730
|
-
const
|
|
731
|
-
const
|
|
732
|
-
if (
|
|
733
|
-
const
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
915
|
+
if (method === 'Target.attachToTarget') {
|
|
916
|
+
const attachResponse = result;
|
|
917
|
+
const attachRequestParams = params;
|
|
918
|
+
if (attachResponse?.sessionId) {
|
|
919
|
+
const freshExt3 = store.getState().extensions.get(extensionConn.id);
|
|
920
|
+
const freshTargets3 = freshExt3?.connectedTargets || new Map();
|
|
921
|
+
const target = Array.from(freshTargets3.values()).find((t) => {
|
|
922
|
+
return t.targetId === attachRequestParams?.targetId;
|
|
923
|
+
});
|
|
924
|
+
if (target) {
|
|
925
|
+
const attachedPayload = {
|
|
926
|
+
method: 'Target.attachedToTarget',
|
|
927
|
+
params: {
|
|
928
|
+
sessionId: attachResponse.sessionId,
|
|
929
|
+
targetInfo: {
|
|
930
|
+
...target.targetInfo,
|
|
931
|
+
attached: true,
|
|
932
|
+
},
|
|
933
|
+
waitingForDebugger: false,
|
|
740
934
|
},
|
|
741
|
-
|
|
935
|
+
};
|
|
936
|
+
if (!target.targetInfo.url) {
|
|
937
|
+
logger?.error(pc.red('[Server] WARNING: Target.attachedToTarget (from attachToTarget) sent with empty URL!'), JSON.stringify(attachedPayload));
|
|
742
938
|
}
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
939
|
+
logger?.log(pc.magenta('[Server] Target.attachedToTarget (from attachToTarget) payload:'), JSON.stringify(attachedPayload));
|
|
940
|
+
sendToPlaywright({
|
|
941
|
+
message: attachedPayload,
|
|
942
|
+
clientId,
|
|
943
|
+
source: 'server',
|
|
944
|
+
});
|
|
746
945
|
}
|
|
747
|
-
logger?.log(pc.magenta('[Server] Target.attachedToTarget (from attachToTarget) payload:'), JSON.stringify(attachedPayload));
|
|
748
|
-
sendToPlaywright({
|
|
749
|
-
message: attachedPayload,
|
|
750
|
-
clientId,
|
|
751
|
-
source: 'server'
|
|
752
|
-
});
|
|
753
946
|
}
|
|
754
947
|
}
|
|
755
948
|
const response = { id, sessionId, result };
|
|
@@ -761,19 +954,19 @@ export async function startPlayWriterCDPRelayServer({ port = 19988, host = '127.
|
|
|
761
954
|
const errorResponse = {
|
|
762
955
|
id,
|
|
763
956
|
sessionId,
|
|
764
|
-
error: { message: e.message }
|
|
957
|
+
error: { message: e.message },
|
|
765
958
|
};
|
|
766
959
|
sendToPlaywright({ message: errorResponse, clientId });
|
|
767
960
|
emitter.emit('cdp:response', { clientId, response: errorResponse, command: message });
|
|
768
961
|
}
|
|
769
962
|
},
|
|
770
963
|
onClose() {
|
|
771
|
-
|
|
772
|
-
logger?.log(pc.yellow(`Playwright client disconnected: ${clientId} (${playwrightClients.size} remaining)`));
|
|
964
|
+
store.setState((s) => relayState.removePlaywrightClient(s, { clientId }));
|
|
965
|
+
logger?.log(pc.yellow(`Playwright client disconnected: ${clientId} (${store.getState().playwrightClients.size} remaining)`));
|
|
773
966
|
},
|
|
774
967
|
onError(event) {
|
|
775
968
|
logger?.error(`Playwright WebSocket error [${clientId}]:`, event);
|
|
776
|
-
}
|
|
969
|
+
},
|
|
777
970
|
};
|
|
778
971
|
}));
|
|
779
972
|
const getExtensionInfoFromRequest = (c) => {
|
|
@@ -819,32 +1012,25 @@ export async function startPlayWriterCDPRelayServer({ port = 19988, host = '127.
|
|
|
819
1012
|
return {
|
|
820
1013
|
onOpen(_event, ws) {
|
|
821
1014
|
const stableKey = buildStableExtensionKey(incomingExtensionInfo, connectionId);
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
if (
|
|
827
|
-
|
|
1015
|
+
// Check for existing connection with same stableKey and close it
|
|
1016
|
+
const existingExt = relayState.findExtensionByStableKey(store.getState(), stableKey);
|
|
1017
|
+
if (existingExt && existingExt.id !== connectionId) {
|
|
1018
|
+
logger?.log(pc.yellow(`Replacing extension connection for ${stableKey} (${existingExt.id} -> ${connectionId})`));
|
|
1019
|
+
if (existingExt.ws) {
|
|
1020
|
+
existingExt.ws.close(4001, 'Extension Replaced');
|
|
828
1021
|
}
|
|
829
1022
|
}
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
info: incomingExtensionInfo,
|
|
834
|
-
|
|
835
|
-
connectedTargets: new Map(),
|
|
836
|
-
pendingRequests: new Map(),
|
|
837
|
-
messageId: 0,
|
|
838
|
-
pingInterval: null,
|
|
839
|
-
};
|
|
840
|
-
extensionConnections.set(connectionId, connection);
|
|
841
|
-
extensionKeyIndex.set(stableKey, connectionId);
|
|
1023
|
+
// State transition: add extension with ws handle included.
|
|
1024
|
+
// Existing same-stableKey entry stays until old socket onClose.
|
|
1025
|
+
store.setState((s) => {
|
|
1026
|
+
return relayState.addExtension(s, { id: connectionId, info: incomingExtensionInfo, stableKey, ws });
|
|
1027
|
+
});
|
|
842
1028
|
startExtensionPing(connectionId);
|
|
843
1029
|
logger?.log(`Extension connected (${connectionId})`);
|
|
844
1030
|
},
|
|
845
1031
|
async onMessage(event, ws) {
|
|
846
|
-
const
|
|
847
|
-
if (!
|
|
1032
|
+
const ext = store.getState().extensions.get(connectionId);
|
|
1033
|
+
if (!ext) {
|
|
848
1034
|
ws.close(1000, 'Extension not registered');
|
|
849
1035
|
return;
|
|
850
1036
|
}
|
|
@@ -866,12 +1052,29 @@ export async function startPlayWriterCDPRelayServer({ port = 19988, host = '127.
|
|
|
866
1052
|
return;
|
|
867
1053
|
}
|
|
868
1054
|
if (message.id !== undefined) {
|
|
869
|
-
const pending =
|
|
1055
|
+
const pending = (() => {
|
|
1056
|
+
let pendingRequest = null;
|
|
1057
|
+
store.setState((s) => {
|
|
1058
|
+
const extensionEntry = s.extensions.get(connectionId);
|
|
1059
|
+
if (!extensionEntry) {
|
|
1060
|
+
return s;
|
|
1061
|
+
}
|
|
1062
|
+
const nextPendingRequest = extensionEntry.pendingRequests.get(message.id);
|
|
1063
|
+
if (!nextPendingRequest) {
|
|
1064
|
+
return s;
|
|
1065
|
+
}
|
|
1066
|
+
pendingRequest = nextPendingRequest;
|
|
1067
|
+
return relayState.removeExtensionPendingRequest(s, {
|
|
1068
|
+
extensionId: connectionId,
|
|
1069
|
+
requestId: message.id,
|
|
1070
|
+
});
|
|
1071
|
+
});
|
|
1072
|
+
return pendingRequest;
|
|
1073
|
+
})();
|
|
870
1074
|
if (!pending) {
|
|
871
1075
|
logger?.log('Unexpected response with id:', message.id);
|
|
872
1076
|
return;
|
|
873
1077
|
}
|
|
874
|
-
connection.pendingRequests.delete(message.id);
|
|
875
1078
|
if (message.error) {
|
|
876
1079
|
pending.reject(new Error(message.error));
|
|
877
1080
|
}
|
|
@@ -916,16 +1119,19 @@ export async function startPlayWriterCDPRelayServer({ port = 19988, host = '127.
|
|
|
916
1119
|
direction: 'from-extension',
|
|
917
1120
|
method,
|
|
918
1121
|
sessionId,
|
|
919
|
-
params
|
|
1122
|
+
params,
|
|
920
1123
|
});
|
|
921
1124
|
const cdpEvent = { method, sessionId, params };
|
|
922
1125
|
emitter.emit('cdp:event', { event: cdpEvent, sessionId });
|
|
1126
|
+
maybeEmitBrowserDownloadCompatEvent({ method, params, extensionId: connectionId });
|
|
923
1127
|
if (method === 'Target.attachedToTarget') {
|
|
924
1128
|
const targetParams = params;
|
|
925
1129
|
const incomingSessionId = sessionId;
|
|
926
1130
|
const iframeParentFrameId = targetParams.targetInfo.parentFrameId;
|
|
927
|
-
|
|
928
|
-
|
|
1131
|
+
// Read current extension state for iframe parent lookup
|
|
1132
|
+
const currentExtState = store.getState().extensions.get(connectionId);
|
|
1133
|
+
const iframeOwnerSessionId = targetParams.targetInfo.type === 'iframe' && iframeParentFrameId && currentExtState
|
|
1134
|
+
? getPageTargetForFrameId({ extensionState: currentExtState, frameId: iframeParentFrameId })?.sessionId
|
|
929
1135
|
: undefined;
|
|
930
1136
|
// Filter out restricted targets (unsupported types, extension pages, chrome:// URLs, etc.)
|
|
931
1137
|
if (isRestrictedTarget(targetParams.targetInfo)) {
|
|
@@ -940,8 +1146,8 @@ export async function startPlayWriterCDPRelayServer({ port = 19988, host = '127.
|
|
|
940
1146
|
source: 'server',
|
|
941
1147
|
},
|
|
942
1148
|
}).catch((error) => {
|
|
943
|
-
const
|
|
944
|
-
logger?.log(pc.yellow('[Server] Failed to resume restricted target:'),
|
|
1149
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
1150
|
+
logger?.log(pc.yellow('[Server] Failed to resume restricted target:'), msg);
|
|
945
1151
|
});
|
|
946
1152
|
}
|
|
947
1153
|
logger?.log(pc.gray(`[Server] Ignoring restricted target: ${targetParams.targetInfo.type} (${targetParams.targetInfo.url})`));
|
|
@@ -952,15 +1158,22 @@ export async function startPlayWriterCDPRelayServer({ port = 19988, host = '127.
|
|
|
952
1158
|
}
|
|
953
1159
|
logger?.log(pc.yellow('[Extension] Target.attachedToTarget full payload:'), JSON.stringify({ method, params: targetParams, sessionId }));
|
|
954
1160
|
// Check if we already sent this target to clients (e.g., from Target.setAutoAttach response)
|
|
955
|
-
const alreadyConnected =
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
1161
|
+
const alreadyConnected = currentExtState?.connectedTargets.has(targetParams.sessionId) ?? false;
|
|
1162
|
+
// State transition: add/update target
|
|
1163
|
+
store.setState((s) => relayState.addTarget(s, {
|
|
1164
|
+
extensionId: connectionId,
|
|
959
1165
|
sessionId: targetParams.sessionId,
|
|
960
1166
|
targetId: targetParams.targetInfo.targetId,
|
|
961
1167
|
targetInfo: targetParams.targetInfo,
|
|
962
|
-
|
|
963
|
-
|
|
1168
|
+
}));
|
|
1169
|
+
const cachedDownloadBehavior = extensionDownloadBehavior.get(connectionId);
|
|
1170
|
+
if (cachedDownloadBehavior && targetParams.targetInfo.type === 'page') {
|
|
1171
|
+
void applyDownloadBehaviorToTargets({
|
|
1172
|
+
extensionId: connectionId,
|
|
1173
|
+
behavior: cachedDownloadBehavior,
|
|
1174
|
+
targetSessionIds: [targetParams.sessionId],
|
|
1175
|
+
});
|
|
1176
|
+
}
|
|
964
1177
|
// Only forward to Playwright if this is a new target to avoid duplicates
|
|
965
1178
|
if (!alreadyConnected) {
|
|
966
1179
|
sendToPlaywright({
|
|
@@ -973,7 +1186,7 @@ export async function startPlayWriterCDPRelayServer({ port = 19988, host = '127.
|
|
|
973
1186
|
// session, detaches it, and the iframe stays paused (waitingForDebugger) which can hang navigations.
|
|
974
1187
|
sessionId: iframeOwnerSessionId ?? incomingSessionId,
|
|
975
1188
|
method: 'Target.attachedToTarget',
|
|
976
|
-
params: targetParams
|
|
1189
|
+
params: targetParams,
|
|
977
1190
|
},
|
|
978
1191
|
source: 'extension',
|
|
979
1192
|
extensionId: connectionId,
|
|
@@ -982,11 +1195,11 @@ export async function startPlayWriterCDPRelayServer({ port = 19988, host = '127.
|
|
|
982
1195
|
}
|
|
983
1196
|
else if (method === 'Target.detachedFromTarget') {
|
|
984
1197
|
const detachParams = params;
|
|
985
|
-
|
|
1198
|
+
store.setState((s) => relayState.removeTarget(s, { extensionId: connectionId, sessionId: detachParams.sessionId }));
|
|
986
1199
|
sendToPlaywright({
|
|
987
1200
|
message: {
|
|
988
1201
|
method: 'Target.detachedFromTarget',
|
|
989
|
-
params: detachParams
|
|
1202
|
+
params: detachParams,
|
|
990
1203
|
},
|
|
991
1204
|
source: 'extension',
|
|
992
1205
|
extensionId: connectionId,
|
|
@@ -994,17 +1207,12 @@ export async function startPlayWriterCDPRelayServer({ port = 19988, host = '127.
|
|
|
994
1207
|
}
|
|
995
1208
|
else if (method === 'Target.targetCrashed') {
|
|
996
1209
|
const crashParams = params;
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
connection.connectedTargets.delete(sid);
|
|
1000
|
-
logger?.log(pc.red('[Server] Target crashed, removing:'), crashParams.targetId);
|
|
1001
|
-
break;
|
|
1002
|
-
}
|
|
1003
|
-
}
|
|
1210
|
+
store.setState((s) => relayState.removeTargetByCrash(s, { extensionId: connectionId, targetId: crashParams.targetId }));
|
|
1211
|
+
logger?.log(pc.red('[Server] Target crashed, removing:'), crashParams.targetId);
|
|
1004
1212
|
sendToPlaywright({
|
|
1005
1213
|
message: {
|
|
1006
1214
|
method: 'Target.targetCrashed',
|
|
1007
|
-
params: crashParams
|
|
1215
|
+
params: crashParams,
|
|
1008
1216
|
},
|
|
1009
1217
|
source: 'extension',
|
|
1010
1218
|
extensionId: connectionId,
|
|
@@ -1012,16 +1220,11 @@ export async function startPlayWriterCDPRelayServer({ port = 19988, host = '127.
|
|
|
1012
1220
|
}
|
|
1013
1221
|
else if (method === 'Target.targetInfoChanged') {
|
|
1014
1222
|
const infoParams = params;
|
|
1015
|
-
|
|
1016
|
-
if (target.targetId === infoParams.targetInfo.targetId) {
|
|
1017
|
-
target.targetInfo = infoParams.targetInfo;
|
|
1018
|
-
break;
|
|
1019
|
-
}
|
|
1020
|
-
}
|
|
1223
|
+
store.setState((s) => relayState.updateTargetInfo(s, { extensionId: connectionId, targetInfo: infoParams.targetInfo }));
|
|
1021
1224
|
sendToPlaywright({
|
|
1022
1225
|
message: {
|
|
1023
1226
|
method: 'Target.targetInfoChanged',
|
|
1024
|
-
params: infoParams
|
|
1227
|
+
params: infoParams,
|
|
1025
1228
|
},
|
|
1026
1229
|
source: 'extension',
|
|
1027
1230
|
extensionId: connectionId,
|
|
@@ -1030,16 +1233,13 @@ export async function startPlayWriterCDPRelayServer({ port = 19988, host = '127.
|
|
|
1030
1233
|
else if (method === 'Page.frameAttached') {
|
|
1031
1234
|
const frameParams = params;
|
|
1032
1235
|
if (sessionId) {
|
|
1033
|
-
|
|
1034
|
-
if (target) {
|
|
1035
|
-
target.frameIds.add(frameParams.frameId);
|
|
1036
|
-
}
|
|
1236
|
+
store.setState((s) => relayState.addFrameId(s, { extensionId: connectionId, sessionId, frameId: frameParams.frameId }));
|
|
1037
1237
|
}
|
|
1038
1238
|
sendToPlaywright({
|
|
1039
1239
|
message: {
|
|
1040
1240
|
sessionId,
|
|
1041
1241
|
method,
|
|
1042
|
-
params
|
|
1242
|
+
params,
|
|
1043
1243
|
},
|
|
1044
1244
|
source: 'extension',
|
|
1045
1245
|
extensionId: connectionId,
|
|
@@ -1047,15 +1247,12 @@ export async function startPlayWriterCDPRelayServer({ port = 19988, host = '127.
|
|
|
1047
1247
|
}
|
|
1048
1248
|
else if (method === 'Page.frameDetached') {
|
|
1049
1249
|
const frameParams = params;
|
|
1050
|
-
|
|
1051
|
-
if (ownerTarget) {
|
|
1052
|
-
ownerTarget.frameIds.delete(frameParams.frameId);
|
|
1053
|
-
}
|
|
1250
|
+
store.setState((s) => relayState.removeFrameId(s, { extensionId: connectionId, frameId: frameParams.frameId }));
|
|
1054
1251
|
sendToPlaywright({
|
|
1055
1252
|
message: {
|
|
1056
1253
|
sessionId,
|
|
1057
1254
|
method,
|
|
1058
|
-
params
|
|
1255
|
+
params,
|
|
1059
1256
|
},
|
|
1060
1257
|
source: 'extension',
|
|
1061
1258
|
extensionId: connectionId,
|
|
@@ -1064,27 +1261,22 @@ export async function startPlayWriterCDPRelayServer({ port = 19988, host = '127.
|
|
|
1064
1261
|
else if (method === 'Page.frameNavigated') {
|
|
1065
1262
|
const frameParams = params;
|
|
1066
1263
|
if (sessionId) {
|
|
1067
|
-
|
|
1068
|
-
if (target) {
|
|
1069
|
-
target.frameIds.add(frameParams.frame.id);
|
|
1070
|
-
}
|
|
1264
|
+
store.setState((s) => relayState.addFrameId(s, { extensionId: connectionId, sessionId, frameId: frameParams.frame.id }));
|
|
1071
1265
|
}
|
|
1072
1266
|
if (!frameParams.frame.parentId && sessionId) {
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
logger?.log(pc.magenta('[Server] Updated target URL from Page.frameNavigated:'), frameParams.frame.url);
|
|
1081
|
-
}
|
|
1267
|
+
store.setState((s) => relayState.updateTargetUrl(s, {
|
|
1268
|
+
extensionId: connectionId,
|
|
1269
|
+
sessionId,
|
|
1270
|
+
url: frameParams.frame.url,
|
|
1271
|
+
title: frameParams.frame.name || undefined,
|
|
1272
|
+
}));
|
|
1273
|
+
logger?.log(pc.magenta('[Server] Updated target URL from Page.frameNavigated:'), frameParams.frame.url);
|
|
1082
1274
|
}
|
|
1083
1275
|
sendToPlaywright({
|
|
1084
1276
|
message: {
|
|
1085
1277
|
sessionId,
|
|
1086
1278
|
method,
|
|
1087
|
-
params
|
|
1279
|
+
params,
|
|
1088
1280
|
},
|
|
1089
1281
|
source: 'extension',
|
|
1090
1282
|
extensionId: connectionId,
|
|
@@ -1093,20 +1285,14 @@ export async function startPlayWriterCDPRelayServer({ port = 19988, host = '127.
|
|
|
1093
1285
|
else if (method === 'Page.navigatedWithinDocument') {
|
|
1094
1286
|
const navParams = params;
|
|
1095
1287
|
if (sessionId) {
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
target.targetInfo = {
|
|
1099
|
-
...target.targetInfo,
|
|
1100
|
-
url: navParams.url,
|
|
1101
|
-
};
|
|
1102
|
-
logger?.log(pc.magenta('[Server] Updated target URL from Page.navigatedWithinDocument:'), navParams.url);
|
|
1103
|
-
}
|
|
1288
|
+
store.setState((s) => relayState.updateTargetUrl(s, { extensionId: connectionId, sessionId, url: navParams.url }));
|
|
1289
|
+
logger?.log(pc.magenta('[Server] Updated target URL from Page.navigatedWithinDocument:'), navParams.url);
|
|
1104
1290
|
}
|
|
1105
1291
|
sendToPlaywright({
|
|
1106
1292
|
message: {
|
|
1107
1293
|
sessionId,
|
|
1108
1294
|
method,
|
|
1109
|
-
params
|
|
1295
|
+
params,
|
|
1110
1296
|
},
|
|
1111
1297
|
source: 'extension',
|
|
1112
1298
|
extensionId: connectionId,
|
|
@@ -1117,7 +1303,7 @@ export async function startPlayWriterCDPRelayServer({ port = 19988, host = '127.
|
|
|
1117
1303
|
message: {
|
|
1118
1304
|
sessionId,
|
|
1119
1305
|
method,
|
|
1120
|
-
params
|
|
1306
|
+
params,
|
|
1121
1307
|
},
|
|
1122
1308
|
source: 'extension',
|
|
1123
1309
|
extensionId: connectionId,
|
|
@@ -1125,10 +1311,9 @@ export async function startPlayWriterCDPRelayServer({ port = 19988, host = '127.
|
|
|
1125
1311
|
}
|
|
1126
1312
|
}
|
|
1127
1313
|
},
|
|
1128
|
-
onClose(event
|
|
1314
|
+
onClose(event) {
|
|
1129
1315
|
logger?.log(`Extension disconnected: code=${event.code} reason=${event.reason || 'none'} (${connectionId})`);
|
|
1130
|
-
|
|
1131
|
-
// Cancel any active recordings BEFORE removing connection (cancelRecording checks isExtensionConnected)
|
|
1316
|
+
// Cancel recordings BEFORE removing extension state (cancelRecording checks isExtensionConnected)
|
|
1132
1317
|
const recordingRelay = recordingRelays.get(connectionId);
|
|
1133
1318
|
if (recordingRelay) {
|
|
1134
1319
|
recordingRelay.cancelRecording({}).catch(() => {
|
|
@@ -1136,32 +1321,50 @@ export async function startPlayWriterCDPRelayServer({ port = 19988, host = '127.
|
|
|
1136
1321
|
});
|
|
1137
1322
|
}
|
|
1138
1323
|
recordingRelays.delete(connectionId);
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1324
|
+
// Reject all pending I/O requests (state cleanup happens in removeExtension below)
|
|
1325
|
+
const closingExt = store.getState().extensions.get(connectionId);
|
|
1326
|
+
if (closingExt) {
|
|
1327
|
+
stopExtensionPing(connectionId);
|
|
1328
|
+
for (const pending of closingExt.pendingRequests.values()) {
|
|
1142
1329
|
pending.reject(new Error('Extension connection closed'));
|
|
1143
1330
|
}
|
|
1144
|
-
connection.pendingRequests.clear();
|
|
1145
|
-
connection.connectedTargets.clear();
|
|
1146
1331
|
}
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1332
|
+
const currentRelayState = store.getState();
|
|
1333
|
+
const closingExtension = currentRelayState.extensions.get(connectionId);
|
|
1334
|
+
const successorCandidates = closingExtension
|
|
1335
|
+
? Array.from(currentRelayState.extensions.values())
|
|
1336
|
+
.reverse()
|
|
1337
|
+
.filter((ext) => {
|
|
1338
|
+
return ext.id !== connectionId && ext.stableKey === closingExtension.stableKey && Boolean(ext.ws);
|
|
1339
|
+
})
|
|
1340
|
+
: [];
|
|
1341
|
+
const successorExtension = closingExtension
|
|
1342
|
+
? successorCandidates[0]
|
|
1343
|
+
: undefined;
|
|
1344
|
+
if (successorExtension) {
|
|
1345
|
+
logger?.log(pc.yellow(`Rebinding clients from ${connectionId} to ${successorExtension.id} (stableKey: ${successorExtension.stableKey})`));
|
|
1346
|
+
store.setState((s) => {
|
|
1347
|
+
return relayState.rebindClientsToExtension(s, {
|
|
1348
|
+
fromExtensionId: connectionId,
|
|
1349
|
+
toExtensionId: successorExtension.id,
|
|
1350
|
+
});
|
|
1351
|
+
});
|
|
1152
1352
|
}
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1353
|
+
// Close playwright clients bound to this extension when no successor exists.
|
|
1354
|
+
if (!successorExtension) {
|
|
1355
|
+
const { playwrightClients } = store.getState();
|
|
1356
|
+
for (const client of playwrightClients.values()) {
|
|
1357
|
+
if (client.extensionId === connectionId) {
|
|
1358
|
+
client.ws.close(1000, 'Extension disconnected');
|
|
1359
|
+
}
|
|
1157
1360
|
}
|
|
1158
|
-
client.ws.close(1000, 'Extension disconnected');
|
|
1159
|
-
playwrightClients.delete(clientId);
|
|
1160
1361
|
}
|
|
1362
|
+
// State transition: remove extension + its bound clients atomically
|
|
1363
|
+
store.setState((s) => relayState.removeExtension(s, { extensionId: connectionId }));
|
|
1161
1364
|
},
|
|
1162
1365
|
onError(event) {
|
|
1163
1366
|
logger?.error('Extension WebSocket error:', event);
|
|
1164
|
-
}
|
|
1367
|
+
},
|
|
1165
1368
|
};
|
|
1166
1369
|
}));
|
|
1167
1370
|
// ============================================================================
|
|
@@ -1233,7 +1436,7 @@ export async function startPlayWriterCDPRelayServer({ port = 19988, host = '127.
|
|
|
1233
1436
|
app.use('/recording/*', privilegedRouteMiddleware);
|
|
1234
1437
|
app.post('/cli/execute', async (c) => {
|
|
1235
1438
|
try {
|
|
1236
|
-
const body = await c.req.json();
|
|
1439
|
+
const body = (await c.req.json());
|
|
1237
1440
|
const sessionId = normalizeSessionId(body.sessionId);
|
|
1238
1441
|
const { code, timeout = 10000 } = body;
|
|
1239
1442
|
if (!sessionId || !code) {
|
|
@@ -1254,7 +1457,7 @@ export async function startPlayWriterCDPRelayServer({ port = 19988, host = '127.
|
|
|
1254
1457
|
});
|
|
1255
1458
|
app.post('/cli/reset', async (c) => {
|
|
1256
1459
|
try {
|
|
1257
|
-
const body = await c.req.json();
|
|
1460
|
+
const body = (await c.req.json());
|
|
1258
1461
|
const sessionId = normalizeSessionId(body.sessionId);
|
|
1259
1462
|
if (!sessionId) {
|
|
1260
1463
|
return c.json({ error: 'sessionId is required' }, 400);
|
|
@@ -1284,13 +1487,13 @@ export async function startPlayWriterCDPRelayServer({ port = 19988, host = '127.
|
|
|
1284
1487
|
return c.json({ next: nextSessionNumber });
|
|
1285
1488
|
});
|
|
1286
1489
|
app.post('/cli/session/new', async (c) => {
|
|
1287
|
-
const body = await c.req.json().catch(() => ({}));
|
|
1490
|
+
const body = (await c.req.json().catch(() => ({})));
|
|
1288
1491
|
const sessionId = String(nextSessionNumber++);
|
|
1289
1492
|
const extensionId = body.extensionId || null;
|
|
1290
1493
|
const cwd = body.cwd;
|
|
1291
|
-
const allowDefault = !extensionId &&
|
|
1292
|
-
const
|
|
1293
|
-
if (!
|
|
1494
|
+
const allowDefault = !extensionId && store.getState().extensions.size === 1;
|
|
1495
|
+
const conn = getExtensionConnection(extensionId, { allowFallback: allowDefault });
|
|
1496
|
+
if (!conn) {
|
|
1294
1497
|
const error = extensionId
|
|
1295
1498
|
? `Extension not connected: ${extensionId}`
|
|
1296
1499
|
: 'Multiple extensions connected. Specify extensionId.';
|
|
@@ -1301,9 +1504,9 @@ export async function startPlayWriterCDPRelayServer({ port = 19988, host = '127.
|
|
|
1301
1504
|
sessionId,
|
|
1302
1505
|
cwd,
|
|
1303
1506
|
sessionMetadata: {
|
|
1304
|
-
extensionId:
|
|
1305
|
-
browser:
|
|
1306
|
-
profile:
|
|
1507
|
+
extensionId: conn.stableKey,
|
|
1508
|
+
browser: conn.info.browser || null,
|
|
1509
|
+
profile: conn.info ? { email: conn.info.email || '', id: conn.info.id || '' } : null,
|
|
1307
1510
|
},
|
|
1308
1511
|
});
|
|
1309
1512
|
const metadata = executor.getSessionMetadata();
|
|
@@ -1331,7 +1534,7 @@ export async function startPlayWriterCDPRelayServer({ port = 19988, host = '127.
|
|
|
1331
1534
|
});
|
|
1332
1535
|
app.post('/cli/session/delete', async (c) => {
|
|
1333
1536
|
try {
|
|
1334
|
-
const body = await c.req.json();
|
|
1537
|
+
const body = (await c.req.json());
|
|
1335
1538
|
const sessionId = normalizeSessionId(body.sessionId);
|
|
1336
1539
|
if (!sessionId) {
|
|
1337
1540
|
return c.json({ error: 'sessionId is required' }, 400);
|
|
@@ -1352,70 +1555,54 @@ export async function startPlayWriterCDPRelayServer({ port = 19988, host = '127.
|
|
|
1352
1555
|
// Recording Endpoints - For screen recording via chrome.tabCapture
|
|
1353
1556
|
// ============================================================================
|
|
1354
1557
|
app.post('/recording/start', async (c) => {
|
|
1355
|
-
const body = await c.req.json();
|
|
1558
|
+
const body = (await c.req.json());
|
|
1356
1559
|
const sessionId = normalizeSessionId(body.sessionId);
|
|
1357
1560
|
const { sessionId: _sessionId, ...recordingOptions } = body;
|
|
1358
|
-
const
|
|
1359
|
-
const executor = sessionId ? manager.getSession(sessionId) : null;
|
|
1360
|
-
if (sessionId && !executor) {
|
|
1361
|
-
return c.json({ success: false, error: `Session ${sessionId} not found` }, 404);
|
|
1362
|
-
}
|
|
1363
|
-
const extensionId = executor?.getSessionMetadata().extensionId || null;
|
|
1561
|
+
const { extensionId, sessionId: resolvedSessionId } = await resolveRecordingRoute({ sessionId });
|
|
1364
1562
|
const relay = getRecordingRelay(extensionId);
|
|
1365
1563
|
if (!relay) {
|
|
1366
1564
|
return c.json({ success: false, error: 'Extension not connected' }, 500);
|
|
1367
1565
|
}
|
|
1368
|
-
const recordingParams = (
|
|
1566
|
+
const recordingParams = (resolvedSessionId
|
|
1567
|
+
? { ...recordingOptions, sessionId: resolvedSessionId }
|
|
1568
|
+
: recordingOptions);
|
|
1369
1569
|
const result = await relay.startRecording(recordingParams);
|
|
1370
|
-
const status = result.success ? 200 :
|
|
1570
|
+
const status = result.success ? 200 : result.error?.includes('required') ? 400 : 500;
|
|
1371
1571
|
return c.json(result, status);
|
|
1372
1572
|
});
|
|
1373
1573
|
app.post('/recording/stop', async (c) => {
|
|
1374
|
-
const body = await c.req.json();
|
|
1574
|
+
const body = (await c.req.json());
|
|
1375
1575
|
const sessionId = normalizeSessionId(body.sessionId);
|
|
1376
|
-
const
|
|
1377
|
-
const executor = sessionId ? manager.getSession(sessionId) : null;
|
|
1378
|
-
if (sessionId && !executor) {
|
|
1379
|
-
return c.json({ success: false, error: `Session ${sessionId} not found` }, 404);
|
|
1380
|
-
}
|
|
1381
|
-
const extensionId = executor?.getSessionMetadata().extensionId || null;
|
|
1576
|
+
const { extensionId, sessionId: resolvedSessionId } = await resolveRecordingRoute({ sessionId });
|
|
1382
1577
|
const relay = getRecordingRelay(extensionId);
|
|
1383
1578
|
if (!relay) {
|
|
1384
1579
|
return c.json({ success: false, error: 'Extension not connected' }, 500);
|
|
1385
1580
|
}
|
|
1386
|
-
const stopParams =
|
|
1581
|
+
const stopParams = resolvedSessionId ? { sessionId: resolvedSessionId } : {};
|
|
1387
1582
|
const result = await relay.stopRecording(stopParams);
|
|
1388
|
-
const status = result.success ? 200 :
|
|
1583
|
+
const status = result.success ? 200 : result.error?.includes('not found') ? 404 : 500;
|
|
1389
1584
|
return c.json(result, status);
|
|
1390
1585
|
});
|
|
1391
1586
|
app.get('/recording/status', async (c) => {
|
|
1392
1587
|
const sessionId = normalizeSessionId(c.req.query('sessionId'));
|
|
1393
|
-
const
|
|
1394
|
-
const manager = await getExecutorManager();
|
|
1395
|
-
const executor = normalizedSessionId ? manager.getSession(normalizedSessionId) : null;
|
|
1396
|
-
const extensionId = executor?.getSessionMetadata().extensionId || null;
|
|
1588
|
+
const { extensionId, sessionId: resolvedSessionId } = await resolveRecordingRoute({ sessionId });
|
|
1397
1589
|
const relay = getRecordingRelay(extensionId);
|
|
1398
1590
|
if (!relay) {
|
|
1399
1591
|
return c.json({ isRecording: false });
|
|
1400
1592
|
}
|
|
1401
|
-
const isRecordingParams =
|
|
1593
|
+
const isRecordingParams = resolvedSessionId ? { sessionId: resolvedSessionId } : {};
|
|
1402
1594
|
const result = await relay.isRecording(isRecordingParams);
|
|
1403
1595
|
return c.json(result);
|
|
1404
1596
|
});
|
|
1405
1597
|
app.post('/recording/cancel', async (c) => {
|
|
1406
|
-
const body = await c.req.json();
|
|
1598
|
+
const body = (await c.req.json());
|
|
1407
1599
|
const sessionId = normalizeSessionId(body.sessionId);
|
|
1408
|
-
const
|
|
1409
|
-
const executor = sessionId ? manager.getSession(sessionId) : null;
|
|
1410
|
-
if (sessionId && !executor) {
|
|
1411
|
-
return c.json({ success: false, error: `Session ${sessionId} not found` }, 404);
|
|
1412
|
-
}
|
|
1413
|
-
const extensionId = executor?.getSessionMetadata().extensionId || null;
|
|
1600
|
+
const { extensionId, sessionId: resolvedSessionId } = await resolveRecordingRoute({ sessionId });
|
|
1414
1601
|
const relay = getRecordingRelay(extensionId);
|
|
1415
1602
|
if (!relay) {
|
|
1416
1603
|
return c.json({ success: false, error: 'Extension not connected' }, 500);
|
|
1417
1604
|
}
|
|
1418
|
-
const cancelParams =
|
|
1605
|
+
const cancelParams = resolvedSessionId ? { sessionId: resolvedSessionId } : {};
|
|
1419
1606
|
const result = await relay.cancelRecording(cancelParams);
|
|
1420
1607
|
return c.json(result);
|
|
1421
1608
|
});
|
|
@@ -1431,14 +1618,21 @@ export async function startPlayWriterCDPRelayServer({ port = 19988, host = '127.
|
|
|
1431
1618
|
logger?.log('CDP endpoint:', cdpEndpoint);
|
|
1432
1619
|
return {
|
|
1433
1620
|
close() {
|
|
1621
|
+
const { extensions, playwrightClients } = store.getState();
|
|
1434
1622
|
for (const client of playwrightClients.values()) {
|
|
1435
1623
|
client.ws.close(1000, 'Server stopped');
|
|
1436
1624
|
}
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1625
|
+
for (const ext of extensions.values()) {
|
|
1626
|
+
if (ext.pingInterval) {
|
|
1627
|
+
clearInterval(ext.pingInterval);
|
|
1628
|
+
}
|
|
1629
|
+
ext.ws?.close(1000, 'Server stopped');
|
|
1440
1630
|
}
|
|
1441
|
-
|
|
1631
|
+
// Reset store state
|
|
1632
|
+
store.setState({
|
|
1633
|
+
extensions: new Map(),
|
|
1634
|
+
playwrightClients: new Map(),
|
|
1635
|
+
});
|
|
1442
1636
|
server.close();
|
|
1443
1637
|
emitter.removeAllListeners();
|
|
1444
1638
|
},
|
|
@@ -1447,7 +1641,7 @@ export async function startPlayWriterCDPRelayServer({ port = 19988, host = '127.
|
|
|
1447
1641
|
},
|
|
1448
1642
|
off(event, listener) {
|
|
1449
1643
|
emitter.off(event, listener);
|
|
1450
|
-
}
|
|
1644
|
+
},
|
|
1451
1645
|
};
|
|
1452
1646
|
}
|
|
1453
1647
|
//# sourceMappingURL=cdp-relay.js.map
|