playwriter 0.0.63 → 0.0.80
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/aria-snapshot.d.ts +41 -3
- package/dist/aria-snapshot.d.ts.map +1 -1
- package/dist/aria-snapshot.js +131 -54
- 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 +408 -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 +295 -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 +281 -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 +594 -255
- package/dist/protocol.d.ts +4 -0
- package/dist/protocol.d.ts.map +1 -1
- package/dist/readability.js +1 -1
- 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 +44 -10
- package/dist/relay-client.js.map +1 -1
- package/dist/relay-core.test.d.ts.map +1 -1
- package/dist/relay-core.test.js +187 -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 +54 -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 +42 -4
- package/dist/screen-recording.d.ts.map +1 -1
- package/dist/screen-recording.js +88 -13
- package/dist/screen-recording.js.map +1 -1
- 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 +303 -116
- package/src/aria-snapshot.unit.test.ts +199 -141
- package/src/aria-snapshots/github-raw.txt +1 -1
- package/src/aria-snapshots/hackernews-interactive.txt +240 -240
- package/src/aria-snapshots/hackernews-raw.txt +270 -270
- 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 +949 -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 +372 -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 +368 -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 +107 -0
- package/src/recording-relay.ts +20 -12
- package/src/relay-client.ts +84 -17
- package/src/relay-core.test.ts +761 -583
- package/src/relay-navigation.test.ts +517 -484
- 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.ts +175 -31
- package/src/skill.md +619 -271
- package/src/snapshot-tools.test.ts +580 -528
- package/src/snapshots/shadcn-ui-accessibility-full.md +181 -183
- package/src/snapshots/shadcn-ui-accessibility-interactive.md +119 -121
- 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,63 @@ 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 extensionKeyIndex = new Map();
|
|
46
|
+
const store = relayState.createRelayStore();
|
|
47
47
|
const resolvedCdpLogger = cdpLogger || createCdpLogger();
|
|
48
48
|
const logCdpJson = (entry) => {
|
|
49
49
|
resolvedCdpLogger.log(entry);
|
|
50
50
|
};
|
|
51
|
-
const playwrightClients = new Map();
|
|
52
51
|
const getDefaultExtensionId = () => {
|
|
53
|
-
return
|
|
52
|
+
return store.getState().extensions.keys().next().value || null;
|
|
54
53
|
};
|
|
54
|
+
/**
|
|
55
|
+
* Resolve an extension by ID, stableKey, or fallback.
|
|
56
|
+
* Returns the unified ExtensionEntry which includes both state and I/O.
|
|
57
|
+
*/
|
|
55
58
|
const getExtensionConnection = (extensionId, options = {}) => {
|
|
59
|
+
const currentRelayState = store.getState();
|
|
60
|
+
const { extensions } = currentRelayState;
|
|
56
61
|
if (extensionId) {
|
|
57
|
-
const direct =
|
|
58
|
-
if (direct) {
|
|
62
|
+
const direct = extensions.get(extensionId);
|
|
63
|
+
if (direct?.ws) {
|
|
59
64
|
return direct;
|
|
60
65
|
}
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
66
|
+
// Try stableKey lookup.
|
|
67
|
+
const byKey = relayState.findExtensionByStableKey(currentRelayState, extensionId);
|
|
68
|
+
if (byKey) {
|
|
69
|
+
const candidates = Array.from(extensions.values())
|
|
70
|
+
.filter((ext) => ext.stableKey === byKey.stableKey)
|
|
71
|
+
.reverse();
|
|
72
|
+
for (const candidate of candidates) {
|
|
73
|
+
if (candidate.ws) {
|
|
74
|
+
return candidate;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
64
77
|
}
|
|
65
78
|
return null;
|
|
66
79
|
}
|
|
67
80
|
if (!options.allowFallback) {
|
|
68
81
|
return null;
|
|
69
82
|
}
|
|
70
|
-
|
|
71
|
-
if (
|
|
72
|
-
|
|
83
|
+
// Single extension — use it directly
|
|
84
|
+
if (extensions.size === 1) {
|
|
85
|
+
const fallbackId = getDefaultExtensionId();
|
|
86
|
+
if (fallbackId) {
|
|
87
|
+
const ext = extensions.get(fallbackId);
|
|
88
|
+
if (ext?.ws) {
|
|
89
|
+
return ext;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
// Multiple extensions — auto-select if exactly one has active targets.
|
|
94
|
+
// This handles the common case of multiple Chrome profiles with the extension
|
|
95
|
+
// installed, where only one profile has playwriter-enabled tabs. (#52)
|
|
96
|
+
if (extensions.size > 1) {
|
|
97
|
+
const activeExtensions = Array.from(extensions.values()).filter((ext) => {
|
|
98
|
+
return ext.connectedTargets.size > 0;
|
|
99
|
+
});
|
|
100
|
+
if (activeExtensions.length === 1 && activeExtensions[0].ws) {
|
|
101
|
+
return activeExtensions[0];
|
|
102
|
+
}
|
|
73
103
|
}
|
|
74
104
|
return null;
|
|
75
105
|
};
|
|
@@ -92,39 +122,41 @@ export async function startPlayWriterCDPRelayServer({ port = 19988, host = '127.
|
|
|
92
122
|
const normalized = String(value);
|
|
93
123
|
return normalized ? normalized : null;
|
|
94
124
|
};
|
|
95
|
-
const getPageTargetForFrameId = ({
|
|
96
|
-
return Array.from(
|
|
125
|
+
const getPageTargetForFrameId = ({ extensionState, frameId, }) => {
|
|
126
|
+
return Array.from(extensionState.connectedTargets.values()).find((target) => {
|
|
97
127
|
return target.targetInfo.type === 'page' && target.frameIds.has(frameId);
|
|
98
128
|
});
|
|
99
129
|
};
|
|
100
130
|
const startExtensionPing = (extensionId) => {
|
|
101
|
-
const
|
|
102
|
-
if (!
|
|
131
|
+
const ext = store.getState().extensions.get(extensionId);
|
|
132
|
+
if (!ext) {
|
|
103
133
|
return;
|
|
104
134
|
}
|
|
105
|
-
if (
|
|
106
|
-
clearInterval(
|
|
135
|
+
if (ext.pingInterval) {
|
|
136
|
+
clearInterval(ext.pingInterval);
|
|
107
137
|
}
|
|
108
|
-
|
|
109
|
-
|
|
138
|
+
const pingInterval = setInterval(() => {
|
|
139
|
+
const latestExt = store.getState().extensions.get(extensionId);
|
|
140
|
+
latestExt?.ws?.send(JSON.stringify({ method: 'ping' }));
|
|
110
141
|
}, 5000);
|
|
142
|
+
store.setState((s) => relayState.updateExtensionIO(s, { extensionId, pingInterval }));
|
|
111
143
|
};
|
|
112
144
|
const stopExtensionPing = (extensionId) => {
|
|
113
|
-
const
|
|
114
|
-
if (!
|
|
145
|
+
const ext = store.getState().extensions.get(extensionId);
|
|
146
|
+
if (!ext || !ext.pingInterval) {
|
|
115
147
|
return;
|
|
116
148
|
}
|
|
117
|
-
clearInterval(
|
|
118
|
-
|
|
149
|
+
clearInterval(ext.pingInterval);
|
|
150
|
+
store.setState((s) => relayState.updateExtensionIO(s, { extensionId, pingInterval: null }));
|
|
119
151
|
};
|
|
120
|
-
function logCdpMessage({ direction, clientId, method, sessionId, params, id, source }) {
|
|
152
|
+
function logCdpMessage({ direction, clientId, method, sessionId, params, id, source, }) {
|
|
121
153
|
const noisyEvents = [
|
|
122
154
|
'Network.requestWillBeSentExtraInfo',
|
|
123
155
|
'Network.responseReceived',
|
|
124
156
|
'Network.responseReceivedExtraInfo',
|
|
125
157
|
'Network.dataReceived',
|
|
126
158
|
'Network.requestWillBeSent',
|
|
127
|
-
'Network.loadingFinished'
|
|
159
|
+
'Network.loadingFinished',
|
|
128
160
|
];
|
|
129
161
|
if (noisyEvents.includes(method)) {
|
|
130
162
|
return;
|
|
@@ -163,9 +195,7 @@ export async function startPlayWriterCDPRelayServer({ port = 19988, host = '127.
|
|
|
163
195
|
}
|
|
164
196
|
}
|
|
165
197
|
function sendToPlaywright({ message, clientId, source = 'extension', extensionId, }) {
|
|
166
|
-
const messageToSend = source === 'server' && 'method' in message
|
|
167
|
-
? { ...message, __serverGenerated: true }
|
|
168
|
-
: message;
|
|
198
|
+
const messageToSend = source === 'server' && 'method' in message ? { ...message, __serverGenerated: true } : message;
|
|
169
199
|
logCdpJson({
|
|
170
200
|
timestamp: new Date().toISOString(),
|
|
171
201
|
direction: 'to-playwright',
|
|
@@ -180,7 +210,7 @@ export async function startPlayWriterCDPRelayServer({ port = 19988, host = '127.
|
|
|
180
210
|
method: message.method,
|
|
181
211
|
sessionId: 'sessionId' in message ? message.sessionId : undefined,
|
|
182
212
|
params: 'params' in message ? message.params : undefined,
|
|
183
|
-
source
|
|
213
|
+
source,
|
|
184
214
|
});
|
|
185
215
|
}
|
|
186
216
|
const messageStr = JSON.stringify(messageToSend);
|
|
@@ -200,14 +230,14 @@ export async function startPlayWriterCDPRelayServer({ port = 19988, host = '127.
|
|
|
200
230
|
}
|
|
201
231
|
};
|
|
202
232
|
if (clientId) {
|
|
203
|
-
const client = playwrightClients.get(clientId);
|
|
233
|
+
const client = store.getState().playwrightClients.get(clientId);
|
|
204
234
|
if (client) {
|
|
205
235
|
safeSend(client);
|
|
206
236
|
}
|
|
207
237
|
}
|
|
208
238
|
else {
|
|
209
|
-
const
|
|
210
|
-
for (const client of
|
|
239
|
+
const { playwrightClients } = store.getState();
|
|
240
|
+
for (const client of playwrightClients.values()) {
|
|
211
241
|
if (extensionId && client.extensionId !== extensionId) {
|
|
212
242
|
continue;
|
|
213
243
|
}
|
|
@@ -227,11 +257,25 @@ export async function startPlayWriterCDPRelayServer({ port = 19988, host = '127.
|
|
|
227
257
|
return { method: record.method, sessionId, params: record.params };
|
|
228
258
|
}
|
|
229
259
|
async function sendToExtension({ extensionId, method, params, timeout = 30000, }) {
|
|
230
|
-
const
|
|
231
|
-
if (!
|
|
260
|
+
const conn = getExtensionConnection(extensionId);
|
|
261
|
+
if (!conn) {
|
|
262
|
+
throw new Error('Extension not connected');
|
|
263
|
+
}
|
|
264
|
+
const resolvedExtensionId = conn.id;
|
|
265
|
+
let id = 0;
|
|
266
|
+
store.setState((s) => {
|
|
267
|
+
const ext = s.extensions.get(resolvedExtensionId);
|
|
268
|
+
if (!ext) {
|
|
269
|
+
return s;
|
|
270
|
+
}
|
|
271
|
+
id = ext.messageId + 1;
|
|
272
|
+
const newExtensions = new Map(s.extensions);
|
|
273
|
+
newExtensions.set(resolvedExtensionId, { ...ext, messageId: id });
|
|
274
|
+
return { ...s, extensions: newExtensions };
|
|
275
|
+
});
|
|
276
|
+
if (!id) {
|
|
232
277
|
throw new Error('Extension not connected');
|
|
233
278
|
}
|
|
234
|
-
const id = ++connection.messageId;
|
|
235
279
|
const message = { id, method, params };
|
|
236
280
|
const forwardCdpParams = method === 'forwardCDPCommand' ? getForwardCdpParams(params) : undefined;
|
|
237
281
|
if (forwardCdpParams) {
|
|
@@ -245,13 +289,15 @@ export async function startPlayWriterCDPRelayServer({ port = 19988, host = '127.
|
|
|
245
289
|
},
|
|
246
290
|
});
|
|
247
291
|
}
|
|
248
|
-
connection.ws.send(JSON.stringify(message));
|
|
249
292
|
return new Promise((resolve, reject) => {
|
|
250
293
|
const timeoutId = setTimeout(() => {
|
|
251
|
-
|
|
294
|
+
store.setState((s) => relayState.removeExtensionPendingRequest(s, {
|
|
295
|
+
extensionId: resolvedExtensionId,
|
|
296
|
+
requestId: id,
|
|
297
|
+
}));
|
|
252
298
|
reject(new Error(`Extension request timeout after ${timeout}ms: ${method}`));
|
|
253
299
|
}, timeout);
|
|
254
|
-
|
|
300
|
+
const pendingRequest = {
|
|
255
301
|
resolve: (result) => {
|
|
256
302
|
clearTimeout(timeoutId);
|
|
257
303
|
resolve(result);
|
|
@@ -259,21 +305,63 @@ export async function startPlayWriterCDPRelayServer({ port = 19988, host = '127.
|
|
|
259
305
|
reject: (error) => {
|
|
260
306
|
clearTimeout(timeoutId);
|
|
261
307
|
reject(error);
|
|
262
|
-
}
|
|
263
|
-
}
|
|
308
|
+
},
|
|
309
|
+
};
|
|
310
|
+
store.setState((s) => relayState.addExtensionPendingRequest(s, {
|
|
311
|
+
extensionId: resolvedExtensionId,
|
|
312
|
+
requestId: id,
|
|
313
|
+
pendingRequest,
|
|
314
|
+
}));
|
|
315
|
+
const latestExt = store.getState().extensions.get(resolvedExtensionId);
|
|
316
|
+
if (!latestExt?.ws) {
|
|
317
|
+
clearTimeout(timeoutId);
|
|
318
|
+
store.setState((s) => relayState.removeExtensionPendingRequest(s, {
|
|
319
|
+
extensionId: resolvedExtensionId,
|
|
320
|
+
requestId: id,
|
|
321
|
+
}));
|
|
322
|
+
reject(new Error('Extension not connected'));
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
try {
|
|
326
|
+
latestExt.ws.send(JSON.stringify(message));
|
|
327
|
+
}
|
|
328
|
+
catch (error) {
|
|
329
|
+
clearTimeout(timeoutId);
|
|
330
|
+
store.setState((s) => relayState.removeExtensionPendingRequest(s, {
|
|
331
|
+
extensionId: resolvedExtensionId,
|
|
332
|
+
requestId: id,
|
|
333
|
+
}));
|
|
334
|
+
const sendError = error instanceof Error ? error : new Error(String(error));
|
|
335
|
+
reject(new Error(`Extension send failed: ${method}`, { cause: sendError }));
|
|
336
|
+
}
|
|
264
337
|
});
|
|
265
338
|
}
|
|
266
339
|
const recordingRelays = new Map();
|
|
340
|
+
// Find which extension connection owns a CDP tab session ID (pw-tab-*).
|
|
341
|
+
// Used by recording routes where sessionId identifies the target tab.
|
|
342
|
+
// Delegates to the pure derivation function from relay-state.ts.
|
|
343
|
+
const findExtensionIdByCdpSession = (cdpSessionId) => {
|
|
344
|
+
return relayState.findExtensionIdByCdpSession(store.getState(), cdpSessionId);
|
|
345
|
+
};
|
|
346
|
+
// Resolve recording route session ID (CDP tab session) to extension connection.
|
|
347
|
+
const resolveRecordingRoute = async ({ sessionId, }) => {
|
|
348
|
+
if (!sessionId) {
|
|
349
|
+
return { extensionId: null, sessionId: null };
|
|
350
|
+
}
|
|
351
|
+
const extensionId = findExtensionIdByCdpSession(sessionId);
|
|
352
|
+
return { extensionId, sessionId };
|
|
353
|
+
};
|
|
267
354
|
const getRecordingRelay = (extensionId) => {
|
|
268
|
-
const allowDefault = !extensionId &&
|
|
269
|
-
const
|
|
270
|
-
if (!
|
|
355
|
+
const allowDefault = !extensionId && store.getState().extensions.size === 1;
|
|
356
|
+
const conn = getExtensionConnection(extensionId, { allowFallback: allowDefault });
|
|
357
|
+
if (!conn) {
|
|
271
358
|
return null;
|
|
272
359
|
}
|
|
273
|
-
|
|
274
|
-
|
|
360
|
+
const connId = conn.id;
|
|
361
|
+
if (!recordingRelays.has(connId)) {
|
|
362
|
+
recordingRelays.set(connId, new RecordingRelay((params) => sendToExtension({ extensionId: connId, ...params }), () => store.getState().extensions.has(connId), logger));
|
|
275
363
|
}
|
|
276
|
-
return recordingRelays.get(
|
|
364
|
+
return recordingRelays.get(connId) || null;
|
|
277
365
|
};
|
|
278
366
|
// Auto-create initial tab when PLAYWRITER_AUTO_ENABLE is set and no targets exist.
|
|
279
367
|
// This allows Playwright to connect and immediately have a page to work with.
|
|
@@ -281,24 +369,25 @@ export async function startPlayWriterCDPRelayServer({ port = 19988, host = '127.
|
|
|
281
369
|
if (!process.env.PLAYWRITER_AUTO_ENABLE) {
|
|
282
370
|
return;
|
|
283
371
|
}
|
|
284
|
-
const
|
|
285
|
-
if (!
|
|
372
|
+
const conn = getExtensionConnection(extensionId);
|
|
373
|
+
if (!conn) {
|
|
286
374
|
return;
|
|
287
375
|
}
|
|
288
|
-
if (
|
|
376
|
+
if (conn.connectedTargets.size > 0) {
|
|
289
377
|
return;
|
|
290
378
|
}
|
|
291
379
|
try {
|
|
292
380
|
logger?.log(pc.blue('Auto-creating initial tab for Playwright client'));
|
|
293
|
-
const result = await sendToExtension({ extensionId, method: 'createInitialTab', timeout: 10000 });
|
|
381
|
+
const result = (await sendToExtension({ extensionId, method: 'createInitialTab', timeout: 10000 }));
|
|
294
382
|
if (result.success && result.sessionId && result.targetInfo) {
|
|
295
|
-
|
|
383
|
+
store.setState((s) => relayState.addTarget(s, {
|
|
384
|
+
extensionId,
|
|
296
385
|
sessionId: result.sessionId,
|
|
297
386
|
targetId: result.targetInfo.targetId,
|
|
298
387
|
targetInfo: result.targetInfo,
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
logger?.log(pc.blue(`Auto-created tab, now have ${
|
|
388
|
+
}));
|
|
389
|
+
const updatedTargets = store.getState().extensions.get(extensionId)?.connectedTargets.size || 0;
|
|
390
|
+
logger?.log(pc.blue(`Auto-created tab, now have ${updatedTargets} targets, url: ${result.targetInfo.url}`));
|
|
302
391
|
}
|
|
303
392
|
}
|
|
304
393
|
catch (e) {
|
|
@@ -306,8 +395,9 @@ export async function startPlayWriterCDPRelayServer({ port = 19988, host = '127.
|
|
|
306
395
|
}
|
|
307
396
|
}
|
|
308
397
|
async function routeCdpCommand({ extensionId, method, params, sessionId, source, }) {
|
|
309
|
-
const
|
|
310
|
-
const connectedTargets =
|
|
398
|
+
const conn = getExtensionConnection(extensionId);
|
|
399
|
+
const connectedTargets = conn?.connectedTargets || new Map();
|
|
400
|
+
const resolvedExtensionId = conn?.id || extensionId;
|
|
311
401
|
switch (method) {
|
|
312
402
|
case 'Browser.getVersion': {
|
|
313
403
|
return {
|
|
@@ -315,7 +405,7 @@ export async function startPlayWriterCDPRelayServer({ port = 19988, host = '127.
|
|
|
315
405
|
product: 'Chrome/Extension-Bridge',
|
|
316
406
|
revision: '1.0.0',
|
|
317
407
|
userAgent: 'CDP-Bridge-Server/1.0.0',
|
|
318
|
-
jsVersion: 'V8'
|
|
408
|
+
jsVersion: 'V8',
|
|
319
409
|
};
|
|
320
410
|
}
|
|
321
411
|
case 'Browser.setDownloadBehavior': {
|
|
@@ -328,15 +418,15 @@ export async function startPlayWriterCDPRelayServer({ port = 19988, host = '127.
|
|
|
328
418
|
if (sessionId) {
|
|
329
419
|
break;
|
|
330
420
|
}
|
|
331
|
-
if (
|
|
332
|
-
await maybeAutoCreateInitialTab(
|
|
421
|
+
if (conn) {
|
|
422
|
+
await maybeAutoCreateInitialTab(conn.id);
|
|
333
423
|
}
|
|
334
424
|
// Forward auto-attach so Chrome emits iframe Target.attachedToTarget events.
|
|
335
425
|
// Playwright relies on these (with parentFrameId) when reconnecting over CDP.
|
|
336
426
|
await sendToExtension({
|
|
337
|
-
extensionId:
|
|
427
|
+
extensionId: resolvedExtensionId,
|
|
338
428
|
method: 'forwardCDPCommand',
|
|
339
|
-
params: { method, params, source }
|
|
429
|
+
params: { method, params, source },
|
|
340
430
|
});
|
|
341
431
|
return {};
|
|
342
432
|
}
|
|
@@ -344,19 +434,20 @@ export async function startPlayWriterCDPRelayServer({ port = 19988, host = '127.
|
|
|
344
434
|
return {};
|
|
345
435
|
}
|
|
346
436
|
case 'Target.attachToTarget': {
|
|
347
|
-
const
|
|
348
|
-
if (!targetId) {
|
|
437
|
+
const attachParams = params;
|
|
438
|
+
if (!attachParams?.targetId) {
|
|
349
439
|
throw new Error('targetId is required for Target.attachToTarget');
|
|
350
440
|
}
|
|
351
441
|
for (const target of connectedTargets.values()) {
|
|
352
|
-
if (target.targetId === targetId) {
|
|
442
|
+
if (target.targetId === attachParams.targetId) {
|
|
353
443
|
return { sessionId: target.sessionId };
|
|
354
444
|
}
|
|
355
445
|
}
|
|
356
|
-
throw new Error(`Target ${targetId} not found in connected targets`);
|
|
446
|
+
throw new Error(`Target ${attachParams.targetId} not found in connected targets`);
|
|
357
447
|
}
|
|
358
448
|
case 'Target.getTargetInfo': {
|
|
359
|
-
const
|
|
449
|
+
const infoReqParams = params;
|
|
450
|
+
const targetId = infoReqParams?.targetId;
|
|
360
451
|
if (targetId) {
|
|
361
452
|
for (const target of connectedTargets.values()) {
|
|
362
453
|
if (target.targetId === targetId) {
|
|
@@ -379,30 +470,30 @@ export async function startPlayWriterCDPRelayServer({ port = 19988, host = '127.
|
|
|
379
470
|
.filter((t) => !isRestrictedTarget(t.targetInfo))
|
|
380
471
|
.map((t) => ({
|
|
381
472
|
...t.targetInfo,
|
|
382
|
-
attached: true
|
|
383
|
-
}))
|
|
473
|
+
attached: true,
|
|
474
|
+
})),
|
|
384
475
|
};
|
|
385
476
|
}
|
|
386
477
|
case 'Target.createTarget': {
|
|
387
478
|
return await sendToExtension({
|
|
388
|
-
extensionId:
|
|
479
|
+
extensionId: resolvedExtensionId,
|
|
389
480
|
method: 'forwardCDPCommand',
|
|
390
|
-
params: { method, params, source }
|
|
481
|
+
params: { method, params, source },
|
|
391
482
|
});
|
|
392
483
|
}
|
|
393
484
|
case 'Target.closeTarget': {
|
|
394
485
|
return await sendToExtension({
|
|
395
|
-
extensionId:
|
|
486
|
+
extensionId: resolvedExtensionId,
|
|
396
487
|
method: 'forwardCDPCommand',
|
|
397
|
-
params: { method, params, source }
|
|
488
|
+
params: { method, params, source },
|
|
398
489
|
});
|
|
399
490
|
}
|
|
400
491
|
// Ghost Browser API - forward to extension for chrome.ghostPublicAPI/ghostProxies/projects
|
|
401
492
|
case 'ghost-browser': {
|
|
402
493
|
return await sendToExtension({
|
|
403
|
-
extensionId:
|
|
494
|
+
extensionId: resolvedExtensionId,
|
|
404
495
|
method: 'ghost-browser',
|
|
405
|
-
params
|
|
496
|
+
params,
|
|
406
497
|
});
|
|
407
498
|
}
|
|
408
499
|
case 'Runtime.enable': {
|
|
@@ -428,18 +519,18 @@ export async function startPlayWriterCDPRelayServer({ port = 19988, host = '127.
|
|
|
428
519
|
emitter.on('cdp:event', handler);
|
|
429
520
|
});
|
|
430
521
|
const result = await sendToExtension({
|
|
431
|
-
extensionId:
|
|
522
|
+
extensionId: resolvedExtensionId,
|
|
432
523
|
method: 'forwardCDPCommand',
|
|
433
|
-
params: { sessionId, method, params, source }
|
|
524
|
+
params: { sessionId, method, params, source },
|
|
434
525
|
});
|
|
435
526
|
await contextCreatedPromise;
|
|
436
527
|
return result;
|
|
437
528
|
}
|
|
438
529
|
}
|
|
439
530
|
return await sendToExtension({
|
|
440
|
-
extensionId:
|
|
531
|
+
extensionId: resolvedExtensionId,
|
|
441
532
|
method: 'forwardCDPCommand',
|
|
442
|
-
params: { sessionId, method, params, source }
|
|
533
|
+
params: { sessionId, method, params, source },
|
|
443
534
|
});
|
|
444
535
|
}
|
|
445
536
|
const app = new Hono();
|
|
@@ -472,7 +563,7 @@ export async function startPlayWriterCDPRelayServer({ port = 19988, host = '127.
|
|
|
472
563
|
});
|
|
473
564
|
app.get('/extension/status', (c) => {
|
|
474
565
|
const defaultExtension = getExtensionConnection(null, { allowFallback: true });
|
|
475
|
-
const connected =
|
|
566
|
+
const connected = store.getState().extensions.size > 0;
|
|
476
567
|
const activeTargets = defaultExtension?.connectedTargets.size || 0;
|
|
477
568
|
const info = defaultExtension?.info;
|
|
478
569
|
return c.json({
|
|
@@ -484,14 +575,14 @@ export async function startPlayWriterCDPRelayServer({ port = 19988, host = '127.
|
|
|
484
575
|
});
|
|
485
576
|
});
|
|
486
577
|
app.get('/extensions/status', (c) => {
|
|
487
|
-
const extensions = Array.from(
|
|
578
|
+
const extensions = Array.from(store.getState().extensions.values()).map((ext) => {
|
|
488
579
|
return {
|
|
489
|
-
extensionId:
|
|
490
|
-
stableKey:
|
|
491
|
-
browser:
|
|
492
|
-
profile:
|
|
493
|
-
activeTargets:
|
|
494
|
-
playwriterVersion:
|
|
580
|
+
extensionId: ext.id,
|
|
581
|
+
stableKey: ext.stableKey,
|
|
582
|
+
browser: ext.info.browser || null,
|
|
583
|
+
profile: ext.info ? { email: ext.info.email || '', id: ext.info.id || '' } : null,
|
|
584
|
+
activeTargets: ext.connectedTargets.size,
|
|
585
|
+
playwriterVersion: ext.info?.version || null,
|
|
495
586
|
};
|
|
496
587
|
});
|
|
497
588
|
return c.json({ extensions });
|
|
@@ -502,68 +593,68 @@ export async function startPlayWriterCDPRelayServer({ port = 19988, host = '127.
|
|
|
502
593
|
app
|
|
503
594
|
.on(['GET', 'PUT'], '/json/version', (c) => {
|
|
504
595
|
return c.json({
|
|
505
|
-
|
|
596
|
+
Browser: `Playwriter/${VERSION}`,
|
|
506
597
|
'Protocol-Version': '1.3',
|
|
507
|
-
|
|
598
|
+
webSocketDebuggerUrl: getCdpWsUrl(c),
|
|
508
599
|
});
|
|
509
600
|
})
|
|
510
601
|
.on(['GET', 'PUT'], '/json/version/', (c) => {
|
|
511
602
|
return c.json({
|
|
512
|
-
|
|
603
|
+
Browser: `Playwriter/${VERSION}`,
|
|
513
604
|
'Protocol-Version': '1.3',
|
|
514
|
-
|
|
605
|
+
webSocketDebuggerUrl: getCdpWsUrl(c),
|
|
515
606
|
});
|
|
516
607
|
})
|
|
517
608
|
.on(['GET', 'PUT'], '/json/list', (c) => {
|
|
518
609
|
const wsUrl = getCdpWsUrl(c);
|
|
519
610
|
const defaultTargets = getExtensionConnection(null, { allowFallback: true })?.connectedTargets || new Map();
|
|
520
|
-
return c.json(Array.from(defaultTargets.values()).map(t => ({
|
|
611
|
+
return c.json(Array.from(defaultTargets.values()).map((t) => ({
|
|
521
612
|
id: t.targetId,
|
|
522
613
|
type: t.targetInfo.type,
|
|
523
614
|
title: t.targetInfo.title,
|
|
524
615
|
description: t.targetInfo.title,
|
|
525
616
|
url: t.targetInfo.url,
|
|
526
617
|
webSocketDebuggerUrl: wsUrl,
|
|
527
|
-
devtoolsFrontendUrl: `/devtools/inspector.html?ws=${wsUrl.replace('ws://', '')}
|
|
618
|
+
devtoolsFrontendUrl: `/devtools/inspector.html?ws=${wsUrl.replace('ws://', '')}`,
|
|
528
619
|
})));
|
|
529
620
|
})
|
|
530
621
|
.on(['GET', 'PUT'], '/json/list/', (c) => {
|
|
531
622
|
const wsUrl = getCdpWsUrl(c);
|
|
532
623
|
const defaultTargets = getExtensionConnection(null, { allowFallback: true })?.connectedTargets || new Map();
|
|
533
|
-
return c.json(Array.from(defaultTargets.values()).map(t => ({
|
|
624
|
+
return c.json(Array.from(defaultTargets.values()).map((t) => ({
|
|
534
625
|
id: t.targetId,
|
|
535
626
|
type: t.targetInfo.type,
|
|
536
627
|
title: t.targetInfo.title,
|
|
537
628
|
description: t.targetInfo.title,
|
|
538
629
|
url: t.targetInfo.url,
|
|
539
630
|
webSocketDebuggerUrl: wsUrl,
|
|
540
|
-
devtoolsFrontendUrl: `/devtools/inspector.html?ws=${wsUrl.replace('ws://', '')}
|
|
631
|
+
devtoolsFrontendUrl: `/devtools/inspector.html?ws=${wsUrl.replace('ws://', '')}`,
|
|
541
632
|
})));
|
|
542
633
|
})
|
|
543
634
|
.on(['GET', 'PUT'], '/json', (c) => {
|
|
544
635
|
const wsUrl = getCdpWsUrl(c);
|
|
545
636
|
const defaultTargets = getExtensionConnection(null, { allowFallback: true })?.connectedTargets || new Map();
|
|
546
|
-
return c.json(Array.from(defaultTargets.values()).map(t => ({
|
|
637
|
+
return c.json(Array.from(defaultTargets.values()).map((t) => ({
|
|
547
638
|
id: t.targetId,
|
|
548
639
|
type: t.targetInfo.type,
|
|
549
640
|
title: t.targetInfo.title,
|
|
550
641
|
description: t.targetInfo.title,
|
|
551
642
|
url: t.targetInfo.url,
|
|
552
643
|
webSocketDebuggerUrl: wsUrl,
|
|
553
|
-
devtoolsFrontendUrl: `/devtools/inspector.html?ws=${wsUrl.replace('ws://', '')}
|
|
644
|
+
devtoolsFrontendUrl: `/devtools/inspector.html?ws=${wsUrl.replace('ws://', '')}`,
|
|
554
645
|
})));
|
|
555
646
|
})
|
|
556
647
|
.on(['GET', 'PUT'], '/json/', (c) => {
|
|
557
648
|
const wsUrl = getCdpWsUrl(c);
|
|
558
649
|
const defaultTargets = getExtensionConnection(null, { allowFallback: true })?.connectedTargets || new Map();
|
|
559
|
-
return c.json(Array.from(defaultTargets.values()).map(t => ({
|
|
650
|
+
return c.json(Array.from(defaultTargets.values()).map((t) => ({
|
|
560
651
|
id: t.targetId,
|
|
561
652
|
type: t.targetInfo.type,
|
|
562
653
|
title: t.targetInfo.title,
|
|
563
654
|
description: t.targetInfo.title,
|
|
564
655
|
url: t.targetInfo.url,
|
|
565
656
|
webSocketDebuggerUrl: wsUrl,
|
|
566
|
-
devtoolsFrontendUrl: `/devtools/inspector.html?ws=${wsUrl.replace('ws://', '')}
|
|
657
|
+
devtoolsFrontendUrl: `/devtools/inspector.html?ws=${wsUrl.replace('ws://', '')}`,
|
|
567
658
|
})));
|
|
568
659
|
});
|
|
569
660
|
app.post('/mcp-log', async (c) => {
|
|
@@ -610,13 +701,19 @@ export async function startPlayWriterCDPRelayServer({ port = 19988, host = '127.
|
|
|
610
701
|
const clientId = c.req.param('clientId') || 'default';
|
|
611
702
|
const url = new URL(c.req.url, 'http://localhost');
|
|
612
703
|
const requestedExtensionId = url.searchParams.get('extensionId');
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
const
|
|
616
|
-
|
|
704
|
+
// When extensionId is explicit, resolve directly. Otherwise use fallback which
|
|
705
|
+
// handles single-extension and uniquely-active-extension cases (#52).
|
|
706
|
+
const resolvedExtension = requestedExtensionId
|
|
707
|
+
? getExtensionConnection(requestedExtensionId)
|
|
708
|
+
: getExtensionConnection(null, { allowFallback: true });
|
|
709
|
+
const clientExtensionId = resolvedExtension?.id || null;
|
|
710
|
+
const getBoundExtensionIdForClient = () => {
|
|
711
|
+
const client = store.getState().playwrightClients.get(clientId);
|
|
712
|
+
return client?.extensionId || null;
|
|
713
|
+
};
|
|
617
714
|
return {
|
|
618
715
|
async onOpen(_event, ws) {
|
|
619
|
-
if (playwrightClients.has(clientId)) {
|
|
716
|
+
if (store.getState().playwrightClients.has(clientId)) {
|
|
620
717
|
logger?.log(pc.yellow(`Rejecting duplicate Playwright clientId: ${clientId}`));
|
|
621
718
|
ws.close(4004, 'Duplicate Playwright clientId');
|
|
622
719
|
return;
|
|
@@ -630,10 +727,12 @@ export async function startPlayWriterCDPRelayServer({ port = 19988, host = '127.
|
|
|
630
727
|
return;
|
|
631
728
|
}
|
|
632
729
|
// Add client first so it can receive Target.attachedToTarget events
|
|
633
|
-
|
|
730
|
+
store.setState((s) => {
|
|
731
|
+
return relayState.addPlaywrightClient(s, { id: clientId, extensionId: clientExtensionId, ws });
|
|
732
|
+
});
|
|
634
733
|
const extensionConnection = getExtensionConnection(clientExtensionId);
|
|
635
734
|
const targetCount = extensionConnection?.connectedTargets.size || 0;
|
|
636
|
-
logger?.log(pc.green(`Playwright client connected: ${clientId} (${playwrightClients.size} total) (extension? ${!!extensionConnection}) (${targetCount} pages)`));
|
|
735
|
+
logger?.log(pc.green(`Playwright client connected: ${clientId} (${store.getState().playwrightClients.size} total) (extension? ${!!extensionConnection}) (${targetCount} pages)`));
|
|
637
736
|
},
|
|
638
737
|
async onMessage(event, ws) {
|
|
639
738
|
let message;
|
|
@@ -655,25 +754,35 @@ export async function startPlayWriterCDPRelayServer({ port = 19988, host = '127.
|
|
|
655
754
|
clientId,
|
|
656
755
|
method,
|
|
657
756
|
sessionId,
|
|
658
|
-
id
|
|
757
|
+
id,
|
|
659
758
|
});
|
|
660
759
|
emitter.emit('cdp:command', { clientId, command: message });
|
|
661
|
-
const
|
|
662
|
-
|
|
760
|
+
const boundExtensionId = getBoundExtensionIdForClient();
|
|
761
|
+
const extensionConn = getExtensionConnection(boundExtensionId);
|
|
762
|
+
if (!extensionConn) {
|
|
663
763
|
sendToPlaywright({
|
|
664
764
|
message: {
|
|
665
765
|
id,
|
|
666
766
|
sessionId,
|
|
667
|
-
error: { message: 'Extension not connected' }
|
|
767
|
+
error: { message: 'Extension not connected' },
|
|
668
768
|
},
|
|
669
|
-
clientId
|
|
769
|
+
clientId,
|
|
670
770
|
});
|
|
671
771
|
return;
|
|
672
772
|
}
|
|
673
773
|
try {
|
|
674
|
-
const result = await routeCdpCommand({
|
|
774
|
+
const result = await routeCdpCommand({
|
|
775
|
+
extensionId: extensionConn.id,
|
|
776
|
+
method,
|
|
777
|
+
params,
|
|
778
|
+
sessionId,
|
|
779
|
+
source,
|
|
780
|
+
});
|
|
675
781
|
if (method === 'Target.setAutoAttach' && !sessionId) {
|
|
676
|
-
|
|
782
|
+
// Re-read state after async routeCdpCommand — targets may have changed
|
|
783
|
+
const freshExt = store.getState().extensions.get(extensionConn.id);
|
|
784
|
+
const freshTargets = freshExt?.connectedTargets || new Map();
|
|
785
|
+
for (const target of freshTargets.values()) {
|
|
677
786
|
// Skip restricted targets (extensions, chrome:// URLs, non-page types)
|
|
678
787
|
if (isRestrictedTarget(target.targetInfo)) {
|
|
679
788
|
continue;
|
|
@@ -684,10 +793,10 @@ export async function startPlayWriterCDPRelayServer({ port = 19988, host = '127.
|
|
|
684
793
|
sessionId: target.sessionId,
|
|
685
794
|
targetInfo: {
|
|
686
795
|
...target.targetInfo,
|
|
687
|
-
attached: true
|
|
796
|
+
attached: true,
|
|
688
797
|
},
|
|
689
|
-
waitingForDebugger: false
|
|
690
|
-
}
|
|
798
|
+
waitingForDebugger: false,
|
|
799
|
+
},
|
|
691
800
|
};
|
|
692
801
|
if (!target.targetInfo.url) {
|
|
693
802
|
logger?.error(pc.red('[Server] WARNING: Target.attachedToTarget sent with empty URL!'), JSON.stringify(attachedPayload));
|
|
@@ -696,12 +805,14 @@ export async function startPlayWriterCDPRelayServer({ port = 19988, host = '127.
|
|
|
696
805
|
sendToPlaywright({
|
|
697
806
|
message: attachedPayload,
|
|
698
807
|
clientId,
|
|
699
|
-
source: 'server'
|
|
808
|
+
source: 'server',
|
|
700
809
|
});
|
|
701
810
|
}
|
|
702
811
|
}
|
|
703
812
|
if (method === 'Target.setDiscoverTargets' && params?.discover) {
|
|
704
|
-
|
|
813
|
+
const freshExt2 = store.getState().extensions.get(extensionConn.id);
|
|
814
|
+
const freshTargets2 = freshExt2?.connectedTargets || new Map();
|
|
815
|
+
for (const target of freshTargets2.values()) {
|
|
705
816
|
// Skip restricted targets (extensions, chrome:// URLs, non-page types)
|
|
706
817
|
if (isRestrictedTarget(target.targetInfo)) {
|
|
707
818
|
continue;
|
|
@@ -711,9 +822,9 @@ export async function startPlayWriterCDPRelayServer({ port = 19988, host = '127.
|
|
|
711
822
|
params: {
|
|
712
823
|
targetInfo: {
|
|
713
824
|
...target.targetInfo,
|
|
714
|
-
attached: true
|
|
715
|
-
}
|
|
716
|
-
}
|
|
825
|
+
attached: true,
|
|
826
|
+
},
|
|
827
|
+
},
|
|
717
828
|
};
|
|
718
829
|
if (!target.targetInfo.url) {
|
|
719
830
|
logger?.error(pc.red('[Server] WARNING: Target.targetCreated sent with empty URL!'), JSON.stringify(targetCreatedPayload));
|
|
@@ -722,34 +833,41 @@ export async function startPlayWriterCDPRelayServer({ port = 19988, host = '127.
|
|
|
722
833
|
sendToPlaywright({
|
|
723
834
|
message: targetCreatedPayload,
|
|
724
835
|
clientId,
|
|
725
|
-
source: 'server'
|
|
836
|
+
source: 'server',
|
|
726
837
|
});
|
|
727
838
|
}
|
|
728
839
|
}
|
|
729
|
-
if (method === 'Target.attachToTarget'
|
|
730
|
-
const
|
|
731
|
-
const
|
|
732
|
-
if (
|
|
733
|
-
const
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
840
|
+
if (method === 'Target.attachToTarget') {
|
|
841
|
+
const attachResponse = result;
|
|
842
|
+
const attachRequestParams = params;
|
|
843
|
+
if (attachResponse?.sessionId) {
|
|
844
|
+
const freshExt3 = store.getState().extensions.get(extensionConn.id);
|
|
845
|
+
const freshTargets3 = freshExt3?.connectedTargets || new Map();
|
|
846
|
+
const target = Array.from(freshTargets3.values()).find((t) => {
|
|
847
|
+
return t.targetId === attachRequestParams?.targetId;
|
|
848
|
+
});
|
|
849
|
+
if (target) {
|
|
850
|
+
const attachedPayload = {
|
|
851
|
+
method: 'Target.attachedToTarget',
|
|
852
|
+
params: {
|
|
853
|
+
sessionId: attachResponse.sessionId,
|
|
854
|
+
targetInfo: {
|
|
855
|
+
...target.targetInfo,
|
|
856
|
+
attached: true,
|
|
857
|
+
},
|
|
858
|
+
waitingForDebugger: false,
|
|
740
859
|
},
|
|
741
|
-
|
|
860
|
+
};
|
|
861
|
+
if (!target.targetInfo.url) {
|
|
862
|
+
logger?.error(pc.red('[Server] WARNING: Target.attachedToTarget (from attachToTarget) sent with empty URL!'), JSON.stringify(attachedPayload));
|
|
742
863
|
}
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
864
|
+
logger?.log(pc.magenta('[Server] Target.attachedToTarget (from attachToTarget) payload:'), JSON.stringify(attachedPayload));
|
|
865
|
+
sendToPlaywright({
|
|
866
|
+
message: attachedPayload,
|
|
867
|
+
clientId,
|
|
868
|
+
source: 'server',
|
|
869
|
+
});
|
|
746
870
|
}
|
|
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
871
|
}
|
|
754
872
|
}
|
|
755
873
|
const response = { id, sessionId, result };
|
|
@@ -761,19 +879,19 @@ export async function startPlayWriterCDPRelayServer({ port = 19988, host = '127.
|
|
|
761
879
|
const errorResponse = {
|
|
762
880
|
id,
|
|
763
881
|
sessionId,
|
|
764
|
-
error: { message: e.message }
|
|
882
|
+
error: { message: e.message },
|
|
765
883
|
};
|
|
766
884
|
sendToPlaywright({ message: errorResponse, clientId });
|
|
767
885
|
emitter.emit('cdp:response', { clientId, response: errorResponse, command: message });
|
|
768
886
|
}
|
|
769
887
|
},
|
|
770
888
|
onClose() {
|
|
771
|
-
|
|
772
|
-
logger?.log(pc.yellow(`Playwright client disconnected: ${clientId} (${playwrightClients.size} remaining)`));
|
|
889
|
+
store.setState((s) => relayState.removePlaywrightClient(s, { clientId }));
|
|
890
|
+
logger?.log(pc.yellow(`Playwright client disconnected: ${clientId} (${store.getState().playwrightClients.size} remaining)`));
|
|
773
891
|
},
|
|
774
892
|
onError(event) {
|
|
775
893
|
logger?.error(`Playwright WebSocket error [${clientId}]:`, event);
|
|
776
|
-
}
|
|
894
|
+
},
|
|
777
895
|
};
|
|
778
896
|
}));
|
|
779
897
|
const getExtensionInfoFromRequest = (c) => {
|
|
@@ -819,32 +937,25 @@ export async function startPlayWriterCDPRelayServer({ port = 19988, host = '127.
|
|
|
819
937
|
return {
|
|
820
938
|
onOpen(_event, ws) {
|
|
821
939
|
const stableKey = buildStableExtensionKey(incomingExtensionInfo, connectionId);
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
if (
|
|
827
|
-
|
|
940
|
+
// Check for existing connection with same stableKey and close it
|
|
941
|
+
const existingExt = relayState.findExtensionByStableKey(store.getState(), stableKey);
|
|
942
|
+
if (existingExt && existingExt.id !== connectionId) {
|
|
943
|
+
logger?.log(pc.yellow(`Replacing extension connection for ${stableKey} (${existingExt.id} -> ${connectionId})`));
|
|
944
|
+
if (existingExt.ws) {
|
|
945
|
+
existingExt.ws.close(4001, 'Extension Replaced');
|
|
828
946
|
}
|
|
829
947
|
}
|
|
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);
|
|
948
|
+
// State transition: add extension with ws handle included.
|
|
949
|
+
// Existing same-stableKey entry stays until old socket onClose.
|
|
950
|
+
store.setState((s) => {
|
|
951
|
+
return relayState.addExtension(s, { id: connectionId, info: incomingExtensionInfo, stableKey, ws });
|
|
952
|
+
});
|
|
842
953
|
startExtensionPing(connectionId);
|
|
843
954
|
logger?.log(`Extension connected (${connectionId})`);
|
|
844
955
|
},
|
|
845
956
|
async onMessage(event, ws) {
|
|
846
|
-
const
|
|
847
|
-
if (!
|
|
957
|
+
const ext = store.getState().extensions.get(connectionId);
|
|
958
|
+
if (!ext) {
|
|
848
959
|
ws.close(1000, 'Extension not registered');
|
|
849
960
|
return;
|
|
850
961
|
}
|
|
@@ -866,12 +977,29 @@ export async function startPlayWriterCDPRelayServer({ port = 19988, host = '127.
|
|
|
866
977
|
return;
|
|
867
978
|
}
|
|
868
979
|
if (message.id !== undefined) {
|
|
869
|
-
const pending =
|
|
980
|
+
const pending = (() => {
|
|
981
|
+
let pendingRequest = null;
|
|
982
|
+
store.setState((s) => {
|
|
983
|
+
const extensionEntry = s.extensions.get(connectionId);
|
|
984
|
+
if (!extensionEntry) {
|
|
985
|
+
return s;
|
|
986
|
+
}
|
|
987
|
+
const nextPendingRequest = extensionEntry.pendingRequests.get(message.id);
|
|
988
|
+
if (!nextPendingRequest) {
|
|
989
|
+
return s;
|
|
990
|
+
}
|
|
991
|
+
pendingRequest = nextPendingRequest;
|
|
992
|
+
return relayState.removeExtensionPendingRequest(s, {
|
|
993
|
+
extensionId: connectionId,
|
|
994
|
+
requestId: message.id,
|
|
995
|
+
});
|
|
996
|
+
});
|
|
997
|
+
return pendingRequest;
|
|
998
|
+
})();
|
|
870
999
|
if (!pending) {
|
|
871
1000
|
logger?.log('Unexpected response with id:', message.id);
|
|
872
1001
|
return;
|
|
873
1002
|
}
|
|
874
|
-
connection.pendingRequests.delete(message.id);
|
|
875
1003
|
if (message.error) {
|
|
876
1004
|
pending.reject(new Error(message.error));
|
|
877
1005
|
}
|
|
@@ -916,7 +1044,7 @@ export async function startPlayWriterCDPRelayServer({ port = 19988, host = '127.
|
|
|
916
1044
|
direction: 'from-extension',
|
|
917
1045
|
method,
|
|
918
1046
|
sessionId,
|
|
919
|
-
params
|
|
1047
|
+
params,
|
|
920
1048
|
});
|
|
921
1049
|
const cdpEvent = { method, sessionId, params };
|
|
922
1050
|
emitter.emit('cdp:event', { event: cdpEvent, sessionId });
|
|
@@ -924,8 +1052,10 @@ export async function startPlayWriterCDPRelayServer({ port = 19988, host = '127.
|
|
|
924
1052
|
const targetParams = params;
|
|
925
1053
|
const incomingSessionId = sessionId;
|
|
926
1054
|
const iframeParentFrameId = targetParams.targetInfo.parentFrameId;
|
|
927
|
-
|
|
928
|
-
|
|
1055
|
+
// Read current extension state for iframe parent lookup
|
|
1056
|
+
const currentExtState = store.getState().extensions.get(connectionId);
|
|
1057
|
+
const iframeOwnerSessionId = targetParams.targetInfo.type === 'iframe' && iframeParentFrameId && currentExtState
|
|
1058
|
+
? getPageTargetForFrameId({ extensionState: currentExtState, frameId: iframeParentFrameId })?.sessionId
|
|
929
1059
|
: undefined;
|
|
930
1060
|
// Filter out restricted targets (unsupported types, extension pages, chrome:// URLs, etc.)
|
|
931
1061
|
if (isRestrictedTarget(targetParams.targetInfo)) {
|
|
@@ -940,8 +1070,8 @@ export async function startPlayWriterCDPRelayServer({ port = 19988, host = '127.
|
|
|
940
1070
|
source: 'server',
|
|
941
1071
|
},
|
|
942
1072
|
}).catch((error) => {
|
|
943
|
-
const
|
|
944
|
-
logger?.log(pc.yellow('[Server] Failed to resume restricted target:'),
|
|
1073
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
1074
|
+
logger?.log(pc.yellow('[Server] Failed to resume restricted target:'), msg);
|
|
945
1075
|
});
|
|
946
1076
|
}
|
|
947
1077
|
logger?.log(pc.gray(`[Server] Ignoring restricted target: ${targetParams.targetInfo.type} (${targetParams.targetInfo.url})`));
|
|
@@ -952,15 +1082,14 @@ export async function startPlayWriterCDPRelayServer({ port = 19988, host = '127.
|
|
|
952
1082
|
}
|
|
953
1083
|
logger?.log(pc.yellow('[Extension] Target.attachedToTarget full payload:'), JSON.stringify({ method, params: targetParams, sessionId }));
|
|
954
1084
|
// Check if we already sent this target to clients (e.g., from Target.setAutoAttach response)
|
|
955
|
-
const alreadyConnected =
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
1085
|
+
const alreadyConnected = currentExtState?.connectedTargets.has(targetParams.sessionId) ?? false;
|
|
1086
|
+
// State transition: add/update target
|
|
1087
|
+
store.setState((s) => relayState.addTarget(s, {
|
|
1088
|
+
extensionId: connectionId,
|
|
959
1089
|
sessionId: targetParams.sessionId,
|
|
960
1090
|
targetId: targetParams.targetInfo.targetId,
|
|
961
1091
|
targetInfo: targetParams.targetInfo,
|
|
962
|
-
|
|
963
|
-
});
|
|
1092
|
+
}));
|
|
964
1093
|
// Only forward to Playwright if this is a new target to avoid duplicates
|
|
965
1094
|
if (!alreadyConnected) {
|
|
966
1095
|
sendToPlaywright({
|
|
@@ -973,7 +1102,7 @@ export async function startPlayWriterCDPRelayServer({ port = 19988, host = '127.
|
|
|
973
1102
|
// session, detaches it, and the iframe stays paused (waitingForDebugger) which can hang navigations.
|
|
974
1103
|
sessionId: iframeOwnerSessionId ?? incomingSessionId,
|
|
975
1104
|
method: 'Target.attachedToTarget',
|
|
976
|
-
params: targetParams
|
|
1105
|
+
params: targetParams,
|
|
977
1106
|
},
|
|
978
1107
|
source: 'extension',
|
|
979
1108
|
extensionId: connectionId,
|
|
@@ -982,11 +1111,11 @@ export async function startPlayWriterCDPRelayServer({ port = 19988, host = '127.
|
|
|
982
1111
|
}
|
|
983
1112
|
else if (method === 'Target.detachedFromTarget') {
|
|
984
1113
|
const detachParams = params;
|
|
985
|
-
|
|
1114
|
+
store.setState((s) => relayState.removeTarget(s, { extensionId: connectionId, sessionId: detachParams.sessionId }));
|
|
986
1115
|
sendToPlaywright({
|
|
987
1116
|
message: {
|
|
988
1117
|
method: 'Target.detachedFromTarget',
|
|
989
|
-
params: detachParams
|
|
1118
|
+
params: detachParams,
|
|
990
1119
|
},
|
|
991
1120
|
source: 'extension',
|
|
992
1121
|
extensionId: connectionId,
|
|
@@ -994,17 +1123,12 @@ export async function startPlayWriterCDPRelayServer({ port = 19988, host = '127.
|
|
|
994
1123
|
}
|
|
995
1124
|
else if (method === 'Target.targetCrashed') {
|
|
996
1125
|
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
|
-
}
|
|
1126
|
+
store.setState((s) => relayState.removeTargetByCrash(s, { extensionId: connectionId, targetId: crashParams.targetId }));
|
|
1127
|
+
logger?.log(pc.red('[Server] Target crashed, removing:'), crashParams.targetId);
|
|
1004
1128
|
sendToPlaywright({
|
|
1005
1129
|
message: {
|
|
1006
1130
|
method: 'Target.targetCrashed',
|
|
1007
|
-
params: crashParams
|
|
1131
|
+
params: crashParams,
|
|
1008
1132
|
},
|
|
1009
1133
|
source: 'extension',
|
|
1010
1134
|
extensionId: connectionId,
|
|
@@ -1012,16 +1136,11 @@ export async function startPlayWriterCDPRelayServer({ port = 19988, host = '127.
|
|
|
1012
1136
|
}
|
|
1013
1137
|
else if (method === 'Target.targetInfoChanged') {
|
|
1014
1138
|
const infoParams = params;
|
|
1015
|
-
|
|
1016
|
-
if (target.targetId === infoParams.targetInfo.targetId) {
|
|
1017
|
-
target.targetInfo = infoParams.targetInfo;
|
|
1018
|
-
break;
|
|
1019
|
-
}
|
|
1020
|
-
}
|
|
1139
|
+
store.setState((s) => relayState.updateTargetInfo(s, { extensionId: connectionId, targetInfo: infoParams.targetInfo }));
|
|
1021
1140
|
sendToPlaywright({
|
|
1022
1141
|
message: {
|
|
1023
1142
|
method: 'Target.targetInfoChanged',
|
|
1024
|
-
params: infoParams
|
|
1143
|
+
params: infoParams,
|
|
1025
1144
|
},
|
|
1026
1145
|
source: 'extension',
|
|
1027
1146
|
extensionId: connectionId,
|
|
@@ -1030,16 +1149,13 @@ export async function startPlayWriterCDPRelayServer({ port = 19988, host = '127.
|
|
|
1030
1149
|
else if (method === 'Page.frameAttached') {
|
|
1031
1150
|
const frameParams = params;
|
|
1032
1151
|
if (sessionId) {
|
|
1033
|
-
|
|
1034
|
-
if (target) {
|
|
1035
|
-
target.frameIds.add(frameParams.frameId);
|
|
1036
|
-
}
|
|
1152
|
+
store.setState((s) => relayState.addFrameId(s, { extensionId: connectionId, sessionId, frameId: frameParams.frameId }));
|
|
1037
1153
|
}
|
|
1038
1154
|
sendToPlaywright({
|
|
1039
1155
|
message: {
|
|
1040
1156
|
sessionId,
|
|
1041
1157
|
method,
|
|
1042
|
-
params
|
|
1158
|
+
params,
|
|
1043
1159
|
},
|
|
1044
1160
|
source: 'extension',
|
|
1045
1161
|
extensionId: connectionId,
|
|
@@ -1047,15 +1163,12 @@ export async function startPlayWriterCDPRelayServer({ port = 19988, host = '127.
|
|
|
1047
1163
|
}
|
|
1048
1164
|
else if (method === 'Page.frameDetached') {
|
|
1049
1165
|
const frameParams = params;
|
|
1050
|
-
|
|
1051
|
-
if (ownerTarget) {
|
|
1052
|
-
ownerTarget.frameIds.delete(frameParams.frameId);
|
|
1053
|
-
}
|
|
1166
|
+
store.setState((s) => relayState.removeFrameId(s, { extensionId: connectionId, frameId: frameParams.frameId }));
|
|
1054
1167
|
sendToPlaywright({
|
|
1055
1168
|
message: {
|
|
1056
1169
|
sessionId,
|
|
1057
1170
|
method,
|
|
1058
|
-
params
|
|
1171
|
+
params,
|
|
1059
1172
|
},
|
|
1060
1173
|
source: 'extension',
|
|
1061
1174
|
extensionId: connectionId,
|
|
@@ -1064,27 +1177,22 @@ export async function startPlayWriterCDPRelayServer({ port = 19988, host = '127.
|
|
|
1064
1177
|
else if (method === 'Page.frameNavigated') {
|
|
1065
1178
|
const frameParams = params;
|
|
1066
1179
|
if (sessionId) {
|
|
1067
|
-
|
|
1068
|
-
if (target) {
|
|
1069
|
-
target.frameIds.add(frameParams.frame.id);
|
|
1070
|
-
}
|
|
1180
|
+
store.setState((s) => relayState.addFrameId(s, { extensionId: connectionId, sessionId, frameId: frameParams.frame.id }));
|
|
1071
1181
|
}
|
|
1072
1182
|
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
|
-
}
|
|
1183
|
+
store.setState((s) => relayState.updateTargetUrl(s, {
|
|
1184
|
+
extensionId: connectionId,
|
|
1185
|
+
sessionId,
|
|
1186
|
+
url: frameParams.frame.url,
|
|
1187
|
+
title: frameParams.frame.name || undefined,
|
|
1188
|
+
}));
|
|
1189
|
+
logger?.log(pc.magenta('[Server] Updated target URL from Page.frameNavigated:'), frameParams.frame.url);
|
|
1082
1190
|
}
|
|
1083
1191
|
sendToPlaywright({
|
|
1084
1192
|
message: {
|
|
1085
1193
|
sessionId,
|
|
1086
1194
|
method,
|
|
1087
|
-
params
|
|
1195
|
+
params,
|
|
1088
1196
|
},
|
|
1089
1197
|
source: 'extension',
|
|
1090
1198
|
extensionId: connectionId,
|
|
@@ -1093,20 +1201,14 @@ export async function startPlayWriterCDPRelayServer({ port = 19988, host = '127.
|
|
|
1093
1201
|
else if (method === 'Page.navigatedWithinDocument') {
|
|
1094
1202
|
const navParams = params;
|
|
1095
1203
|
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
|
-
}
|
|
1204
|
+
store.setState((s) => relayState.updateTargetUrl(s, { extensionId: connectionId, sessionId, url: navParams.url }));
|
|
1205
|
+
logger?.log(pc.magenta('[Server] Updated target URL from Page.navigatedWithinDocument:'), navParams.url);
|
|
1104
1206
|
}
|
|
1105
1207
|
sendToPlaywright({
|
|
1106
1208
|
message: {
|
|
1107
1209
|
sessionId,
|
|
1108
1210
|
method,
|
|
1109
|
-
params
|
|
1211
|
+
params,
|
|
1110
1212
|
},
|
|
1111
1213
|
source: 'extension',
|
|
1112
1214
|
extensionId: connectionId,
|
|
@@ -1117,7 +1219,7 @@ export async function startPlayWriterCDPRelayServer({ port = 19988, host = '127.
|
|
|
1117
1219
|
message: {
|
|
1118
1220
|
sessionId,
|
|
1119
1221
|
method,
|
|
1120
|
-
params
|
|
1222
|
+
params,
|
|
1121
1223
|
},
|
|
1122
1224
|
source: 'extension',
|
|
1123
1225
|
extensionId: connectionId,
|
|
@@ -1125,10 +1227,9 @@ export async function startPlayWriterCDPRelayServer({ port = 19988, host = '127.
|
|
|
1125
1227
|
}
|
|
1126
1228
|
}
|
|
1127
1229
|
},
|
|
1128
|
-
onClose(event
|
|
1230
|
+
onClose(event) {
|
|
1129
1231
|
logger?.log(`Extension disconnected: code=${event.code} reason=${event.reason || 'none'} (${connectionId})`);
|
|
1130
|
-
|
|
1131
|
-
// Cancel any active recordings BEFORE removing connection (cancelRecording checks isExtensionConnected)
|
|
1232
|
+
// Cancel recordings BEFORE removing extension state (cancelRecording checks isExtensionConnected)
|
|
1132
1233
|
const recordingRelay = recordingRelays.get(connectionId);
|
|
1133
1234
|
if (recordingRelay) {
|
|
1134
1235
|
recordingRelay.cancelRecording({}).catch(() => {
|
|
@@ -1136,32 +1237,50 @@ export async function startPlayWriterCDPRelayServer({ port = 19988, host = '127.
|
|
|
1136
1237
|
});
|
|
1137
1238
|
}
|
|
1138
1239
|
recordingRelays.delete(connectionId);
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1240
|
+
// Reject all pending I/O requests (state cleanup happens in removeExtension below)
|
|
1241
|
+
const closingExt = store.getState().extensions.get(connectionId);
|
|
1242
|
+
if (closingExt) {
|
|
1243
|
+
stopExtensionPing(connectionId);
|
|
1244
|
+
for (const pending of closingExt.pendingRequests.values()) {
|
|
1142
1245
|
pending.reject(new Error('Extension connection closed'));
|
|
1143
1246
|
}
|
|
1144
|
-
connection.pendingRequests.clear();
|
|
1145
|
-
connection.connectedTargets.clear();
|
|
1146
1247
|
}
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1248
|
+
const currentRelayState = store.getState();
|
|
1249
|
+
const closingExtension = currentRelayState.extensions.get(connectionId);
|
|
1250
|
+
const successorCandidates = closingExtension
|
|
1251
|
+
? Array.from(currentRelayState.extensions.values())
|
|
1252
|
+
.reverse()
|
|
1253
|
+
.filter((ext) => {
|
|
1254
|
+
return ext.id !== connectionId && ext.stableKey === closingExtension.stableKey && Boolean(ext.ws);
|
|
1255
|
+
})
|
|
1256
|
+
: [];
|
|
1257
|
+
const successorExtension = closingExtension
|
|
1258
|
+
? successorCandidates[0]
|
|
1259
|
+
: undefined;
|
|
1260
|
+
if (successorExtension) {
|
|
1261
|
+
logger?.log(pc.yellow(`Rebinding clients from ${connectionId} to ${successorExtension.id} (stableKey: ${successorExtension.stableKey})`));
|
|
1262
|
+
store.setState((s) => {
|
|
1263
|
+
return relayState.rebindClientsToExtension(s, {
|
|
1264
|
+
fromExtensionId: connectionId,
|
|
1265
|
+
toExtensionId: successorExtension.id,
|
|
1266
|
+
});
|
|
1267
|
+
});
|
|
1152
1268
|
}
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1269
|
+
// Close playwright clients bound to this extension when no successor exists.
|
|
1270
|
+
if (!successorExtension) {
|
|
1271
|
+
const { playwrightClients } = store.getState();
|
|
1272
|
+
for (const client of playwrightClients.values()) {
|
|
1273
|
+
if (client.extensionId === connectionId) {
|
|
1274
|
+
client.ws.close(1000, 'Extension disconnected');
|
|
1275
|
+
}
|
|
1157
1276
|
}
|
|
1158
|
-
client.ws.close(1000, 'Extension disconnected');
|
|
1159
|
-
playwrightClients.delete(clientId);
|
|
1160
1277
|
}
|
|
1278
|
+
// State transition: remove extension + its bound clients atomically
|
|
1279
|
+
store.setState((s) => relayState.removeExtension(s, { extensionId: connectionId }));
|
|
1161
1280
|
},
|
|
1162
1281
|
onError(event) {
|
|
1163
1282
|
logger?.error('Extension WebSocket error:', event);
|
|
1164
|
-
}
|
|
1283
|
+
},
|
|
1165
1284
|
};
|
|
1166
1285
|
}));
|
|
1167
1286
|
// ============================================================================
|
|
@@ -1233,7 +1352,7 @@ export async function startPlayWriterCDPRelayServer({ port = 19988, host = '127.
|
|
|
1233
1352
|
app.use('/recording/*', privilegedRouteMiddleware);
|
|
1234
1353
|
app.post('/cli/execute', async (c) => {
|
|
1235
1354
|
try {
|
|
1236
|
-
const body = await c.req.json();
|
|
1355
|
+
const body = (await c.req.json());
|
|
1237
1356
|
const sessionId = normalizeSessionId(body.sessionId);
|
|
1238
1357
|
const { code, timeout = 10000 } = body;
|
|
1239
1358
|
if (!sessionId || !code) {
|
|
@@ -1254,7 +1373,7 @@ export async function startPlayWriterCDPRelayServer({ port = 19988, host = '127.
|
|
|
1254
1373
|
});
|
|
1255
1374
|
app.post('/cli/reset', async (c) => {
|
|
1256
1375
|
try {
|
|
1257
|
-
const body = await c.req.json();
|
|
1376
|
+
const body = (await c.req.json());
|
|
1258
1377
|
const sessionId = normalizeSessionId(body.sessionId);
|
|
1259
1378
|
if (!sessionId) {
|
|
1260
1379
|
return c.json({ error: 'sessionId is required' }, 400);
|
|
@@ -1284,13 +1403,13 @@ export async function startPlayWriterCDPRelayServer({ port = 19988, host = '127.
|
|
|
1284
1403
|
return c.json({ next: nextSessionNumber });
|
|
1285
1404
|
});
|
|
1286
1405
|
app.post('/cli/session/new', async (c) => {
|
|
1287
|
-
const body = await c.req.json().catch(() => ({}));
|
|
1406
|
+
const body = (await c.req.json().catch(() => ({})));
|
|
1288
1407
|
const sessionId = String(nextSessionNumber++);
|
|
1289
1408
|
const extensionId = body.extensionId || null;
|
|
1290
1409
|
const cwd = body.cwd;
|
|
1291
|
-
const allowDefault = !extensionId &&
|
|
1292
|
-
const
|
|
1293
|
-
if (!
|
|
1410
|
+
const allowDefault = !extensionId && store.getState().extensions.size === 1;
|
|
1411
|
+
const conn = getExtensionConnection(extensionId, { allowFallback: allowDefault });
|
|
1412
|
+
if (!conn) {
|
|
1294
1413
|
const error = extensionId
|
|
1295
1414
|
? `Extension not connected: ${extensionId}`
|
|
1296
1415
|
: 'Multiple extensions connected. Specify extensionId.';
|
|
@@ -1301,9 +1420,9 @@ export async function startPlayWriterCDPRelayServer({ port = 19988, host = '127.
|
|
|
1301
1420
|
sessionId,
|
|
1302
1421
|
cwd,
|
|
1303
1422
|
sessionMetadata: {
|
|
1304
|
-
extensionId:
|
|
1305
|
-
browser:
|
|
1306
|
-
profile:
|
|
1423
|
+
extensionId: conn.stableKey,
|
|
1424
|
+
browser: conn.info.browser || null,
|
|
1425
|
+
profile: conn.info ? { email: conn.info.email || '', id: conn.info.id || '' } : null,
|
|
1307
1426
|
},
|
|
1308
1427
|
});
|
|
1309
1428
|
const metadata = executor.getSessionMetadata();
|
|
@@ -1331,7 +1450,7 @@ export async function startPlayWriterCDPRelayServer({ port = 19988, host = '127.
|
|
|
1331
1450
|
});
|
|
1332
1451
|
app.post('/cli/session/delete', async (c) => {
|
|
1333
1452
|
try {
|
|
1334
|
-
const body = await c.req.json();
|
|
1453
|
+
const body = (await c.req.json());
|
|
1335
1454
|
const sessionId = normalizeSessionId(body.sessionId);
|
|
1336
1455
|
if (!sessionId) {
|
|
1337
1456
|
return c.json({ error: 'sessionId is required' }, 400);
|
|
@@ -1352,70 +1471,54 @@ export async function startPlayWriterCDPRelayServer({ port = 19988, host = '127.
|
|
|
1352
1471
|
// Recording Endpoints - For screen recording via chrome.tabCapture
|
|
1353
1472
|
// ============================================================================
|
|
1354
1473
|
app.post('/recording/start', async (c) => {
|
|
1355
|
-
const body = await c.req.json();
|
|
1474
|
+
const body = (await c.req.json());
|
|
1356
1475
|
const sessionId = normalizeSessionId(body.sessionId);
|
|
1357
1476
|
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;
|
|
1477
|
+
const { extensionId, sessionId: resolvedSessionId } = await resolveRecordingRoute({ sessionId });
|
|
1364
1478
|
const relay = getRecordingRelay(extensionId);
|
|
1365
1479
|
if (!relay) {
|
|
1366
1480
|
return c.json({ success: false, error: 'Extension not connected' }, 500);
|
|
1367
1481
|
}
|
|
1368
|
-
const recordingParams = (
|
|
1482
|
+
const recordingParams = (resolvedSessionId
|
|
1483
|
+
? { ...recordingOptions, sessionId: resolvedSessionId }
|
|
1484
|
+
: recordingOptions);
|
|
1369
1485
|
const result = await relay.startRecording(recordingParams);
|
|
1370
|
-
const status = result.success ? 200 :
|
|
1486
|
+
const status = result.success ? 200 : result.error?.includes('required') ? 400 : 500;
|
|
1371
1487
|
return c.json(result, status);
|
|
1372
1488
|
});
|
|
1373
1489
|
app.post('/recording/stop', async (c) => {
|
|
1374
|
-
const body = await c.req.json();
|
|
1490
|
+
const body = (await c.req.json());
|
|
1375
1491
|
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;
|
|
1492
|
+
const { extensionId, sessionId: resolvedSessionId } = await resolveRecordingRoute({ sessionId });
|
|
1382
1493
|
const relay = getRecordingRelay(extensionId);
|
|
1383
1494
|
if (!relay) {
|
|
1384
1495
|
return c.json({ success: false, error: 'Extension not connected' }, 500);
|
|
1385
1496
|
}
|
|
1386
|
-
const stopParams =
|
|
1497
|
+
const stopParams = resolvedSessionId ? { sessionId: resolvedSessionId } : {};
|
|
1387
1498
|
const result = await relay.stopRecording(stopParams);
|
|
1388
|
-
const status = result.success ? 200 :
|
|
1499
|
+
const status = result.success ? 200 : result.error?.includes('not found') ? 404 : 500;
|
|
1389
1500
|
return c.json(result, status);
|
|
1390
1501
|
});
|
|
1391
1502
|
app.get('/recording/status', async (c) => {
|
|
1392
1503
|
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;
|
|
1504
|
+
const { extensionId, sessionId: resolvedSessionId } = await resolveRecordingRoute({ sessionId });
|
|
1397
1505
|
const relay = getRecordingRelay(extensionId);
|
|
1398
1506
|
if (!relay) {
|
|
1399
1507
|
return c.json({ isRecording: false });
|
|
1400
1508
|
}
|
|
1401
|
-
const isRecordingParams =
|
|
1509
|
+
const isRecordingParams = resolvedSessionId ? { sessionId: resolvedSessionId } : {};
|
|
1402
1510
|
const result = await relay.isRecording(isRecordingParams);
|
|
1403
1511
|
return c.json(result);
|
|
1404
1512
|
});
|
|
1405
1513
|
app.post('/recording/cancel', async (c) => {
|
|
1406
|
-
const body = await c.req.json();
|
|
1514
|
+
const body = (await c.req.json());
|
|
1407
1515
|
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;
|
|
1516
|
+
const { extensionId, sessionId: resolvedSessionId } = await resolveRecordingRoute({ sessionId });
|
|
1414
1517
|
const relay = getRecordingRelay(extensionId);
|
|
1415
1518
|
if (!relay) {
|
|
1416
1519
|
return c.json({ success: false, error: 'Extension not connected' }, 500);
|
|
1417
1520
|
}
|
|
1418
|
-
const cancelParams =
|
|
1521
|
+
const cancelParams = resolvedSessionId ? { sessionId: resolvedSessionId } : {};
|
|
1419
1522
|
const result = await relay.cancelRecording(cancelParams);
|
|
1420
1523
|
return c.json(result);
|
|
1421
1524
|
});
|
|
@@ -1431,14 +1534,21 @@ export async function startPlayWriterCDPRelayServer({ port = 19988, host = '127.
|
|
|
1431
1534
|
logger?.log('CDP endpoint:', cdpEndpoint);
|
|
1432
1535
|
return {
|
|
1433
1536
|
close() {
|
|
1537
|
+
const { extensions, playwrightClients } = store.getState();
|
|
1434
1538
|
for (const client of playwrightClients.values()) {
|
|
1435
1539
|
client.ws.close(1000, 'Server stopped');
|
|
1436
1540
|
}
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1541
|
+
for (const ext of extensions.values()) {
|
|
1542
|
+
if (ext.pingInterval) {
|
|
1543
|
+
clearInterval(ext.pingInterval);
|
|
1544
|
+
}
|
|
1545
|
+
ext.ws?.close(1000, 'Server stopped');
|
|
1440
1546
|
}
|
|
1441
|
-
|
|
1547
|
+
// Reset store state
|
|
1548
|
+
store.setState({
|
|
1549
|
+
extensions: new Map(),
|
|
1550
|
+
playwrightClients: new Map(),
|
|
1551
|
+
});
|
|
1442
1552
|
server.close();
|
|
1443
1553
|
emitter.removeAllListeners();
|
|
1444
1554
|
},
|
|
@@ -1447,7 +1557,7 @@ export async function startPlayWriterCDPRelayServer({ port = 19988, host = '127.
|
|
|
1447
1557
|
},
|
|
1448
1558
|
off(event, listener) {
|
|
1449
1559
|
emitter.off(event, listener);
|
|
1450
|
-
}
|
|
1560
|
+
},
|
|
1451
1561
|
};
|
|
1452
1562
|
}
|
|
1453
1563
|
//# sourceMappingURL=cdp-relay.js.map
|