@rushstack/playwright-browser-tunnel 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.rush/temp/chunked-rush-logs/playwright-browser-tunnel._phase_build.chunks.jsonl +8 -0
- package/.rush/temp/operation/_phase_build/all.log +8 -0
- package/.rush/temp/operation/_phase_build/log-chunks.jsonl +8 -0
- package/.rush/temp/operation/_phase_build/state.json +3 -0
- package/.rush/temp/rushstack+playwright-browser-tunnel-_phase_build-83a085f61cdc0a4bb81c20aa939830fc5b5cff2f.tar.log +42 -0
- package/.rush/temp/shrinkwrap-deps.json +104 -0
- package/CHANGELOG.json +17 -0
- package/CHANGELOG.md +11 -0
- package/README.md +128 -0
- package/config/api-extractor.json +19 -0
- package/config/rig.json +7 -0
- package/dist/playwright-browser-tunnel.d.ts +276 -0
- package/eslint.config.js +18 -0
- package/lib/HttpServer.d.ts +20 -0
- package/lib/HttpServer.d.ts.map +1 -0
- package/lib/HttpServer.js +77 -0
- package/lib/HttpServer.js.map +1 -0
- package/lib/LaunchOptionsValidator.d.ts +93 -0
- package/lib/LaunchOptionsValidator.d.ts.map +1 -0
- package/lib/LaunchOptionsValidator.js +163 -0
- package/lib/LaunchOptionsValidator.js.map +1 -0
- package/lib/PlaywrightBrowserTunnel.d.ts +92 -0
- package/lib/PlaywrightBrowserTunnel.d.ts.map +1 -0
- package/lib/PlaywrightBrowserTunnel.js +468 -0
- package/lib/PlaywrightBrowserTunnel.js.map +1 -0
- package/lib/index.d.ts +23 -0
- package/lib/index.d.ts.map +1 -0
- package/lib/index.js +33 -0
- package/lib/index.js.map +1 -0
- package/lib/tsdoc-metadata.json +11 -0
- package/lib/tunneledBrowserConnection.d.ts +48 -0
- package/lib/tunneledBrowserConnection.d.ts.map +1 -0
- package/lib/tunneledBrowserConnection.js +216 -0
- package/lib/tunneledBrowserConnection.js.map +1 -0
- package/lib/utilities.d.ts +17 -0
- package/lib/utilities.d.ts.map +1 -0
- package/lib/utilities.js +41 -0
- package/lib/utilities.js.map +1 -0
- package/package.json +44 -0
- package/playwright.config.ts +45 -0
- package/rush-logs/playwright-browser-tunnel._phase_build.cache.log +3 -0
- package/rush-logs/playwright-browser-tunnel._phase_build.log +8 -0
- package/src/HttpServer.ts +87 -0
- package/src/LaunchOptionsValidator.ts +234 -0
- package/src/PlaywrightBrowserTunnel.ts +590 -0
- package/src/index.ts +38 -0
- package/src/tunneledBrowserConnection.ts +284 -0
- package/src/utilities.ts +42 -0
- package/temp/build/lint/_eslint-5eVG3S6w.json +30 -0
- package/temp/build/lint/lint.sarif +233 -0
- package/temp/build/typescript/ts_l9Fw4VUO.json +1 -0
- package/temp/playwright-browser-tunnel.api.md +120 -0
- package/tests/demo.spec.ts +10 -0
- package/tests/testFixture.ts +22 -0
- package/tsconfig.json +6 -0
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type { Browser, LaunchOptions } from 'playwright-core';
|
|
2
|
+
import { type ITerminal } from '@rushstack/terminal';
|
|
3
|
+
import type { BrowserName } from './PlaywrightBrowserTunnel';
|
|
4
|
+
/**
|
|
5
|
+
* Disposable handle returned by {@link tunneledBrowserConnection}.
|
|
6
|
+
* @beta
|
|
7
|
+
*/
|
|
8
|
+
export interface IDisposableTunneledBrowserConnection {
|
|
9
|
+
/**
|
|
10
|
+
* The WebSocket endpoint URL that the local Playwright client should connect to.
|
|
11
|
+
*/
|
|
12
|
+
remoteEndpoint: string;
|
|
13
|
+
/**
|
|
14
|
+
* Dispose method that closes the WebSocket servers.
|
|
15
|
+
* Called automatically when using `using` syntax.
|
|
16
|
+
*/
|
|
17
|
+
[Symbol.dispose]: () => void;
|
|
18
|
+
/**
|
|
19
|
+
* Promise that resolves when the remote WebSocket server closes.
|
|
20
|
+
*/
|
|
21
|
+
closePromise: Promise<void>;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Creates a tunneled WebSocket endpoint that a local Playwright client can connect to.
|
|
25
|
+
* @beta
|
|
26
|
+
*/
|
|
27
|
+
export declare function tunneledBrowserConnection(logger: ITerminal, port?: number): Promise<IDisposableTunneledBrowserConnection>;
|
|
28
|
+
/**
|
|
29
|
+
* Disposable handle returned by {@link createTunneledBrowserAsync}.
|
|
30
|
+
* @beta
|
|
31
|
+
*/
|
|
32
|
+
export interface IDisposableTunneledBrowser {
|
|
33
|
+
/**
|
|
34
|
+
* The connected Playwright Browser instance.
|
|
35
|
+
*/
|
|
36
|
+
browser: Browser;
|
|
37
|
+
/**
|
|
38
|
+
* Async dispose method that closes the browser connection.
|
|
39
|
+
* Called automatically when using `await using` syntax.
|
|
40
|
+
*/
|
|
41
|
+
[Symbol.asyncDispose]: () => Promise<void>;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Creates a Playwright Browser instance connected via a tunneled WebSocket connection.
|
|
45
|
+
* @beta
|
|
46
|
+
*/
|
|
47
|
+
export declare function createTunneledBrowserAsync(browserName: BrowserName, launchOptions: LaunchOptions, logger?: ITerminal, port?: number): Promise<IDisposableTunneledBrowser>;
|
|
48
|
+
//# sourceMappingURL=tunneledBrowserConnection.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tunneledBrowserConnection.d.ts","sourceRoot":"","sources":["../src/tunneledBrowserConnection.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,OAAO,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAI9D,OAAO,EAAE,KAAK,SAAS,EAAqC,MAAM,qBAAqB,CAAC;AAExF,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,2BAA2B,CAAC;AAoB7D;;;GAGG;AACH,MAAM,WAAW,oCAAoC;IACnD;;OAEG;IACH,cAAc,EAAE,MAAM,CAAC;IACvB;;;OAGG;IACH,CAAC,MAAM,CAAC,OAAO,CAAC,EAAE,MAAM,IAAI,CAAC;IAC7B;;OAEG;IACH,YAAY,EAAE,OAAO,CAAC,IAAI,CAAC,CAAC;CAC7B;AAED;;;GAGG;AACH,wBAAsB,yBAAyB,CAC7C,MAAM,EAAE,SAAS,EACjB,IAAI,GAAE,MAA4B,GACjC,OAAO,CAAC,oCAAoC,CAAC,CA4K/C;AAED;;;GAGG;AACH,MAAM,WAAW,0BAA0B;IACzC;;OAEG;IACH,OAAO,EAAE,OAAO,CAAC;IACjB;;;OAGG;IACH,CAAC,MAAM,CAAC,YAAY,CAAC,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CAC5C;AAED;;;GAGG;AACH,wBAAsB,0BAA0B,CAC9C,WAAW,EAAE,WAAW,EACxB,aAAa,EAAE,aAAa,EAC5B,MAAM,CAAC,EAAE,SAAS,EAClB,IAAI,GAAE,MAA4B,GACjC,OAAO,CAAC,0BAA0B,CAAC,CA2BrC"}
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
|
|
3
|
+
// See LICENSE in the project root for license information.
|
|
4
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
5
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
6
|
+
};
|
|
7
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
8
|
+
exports.tunneledBrowserConnection = tunneledBrowserConnection;
|
|
9
|
+
exports.createTunneledBrowserAsync = createTunneledBrowserAsync;
|
|
10
|
+
const playwright_core_1 = __importDefault(require("playwright-core"));
|
|
11
|
+
const ws_1 = require("ws");
|
|
12
|
+
const package_json_1 = __importDefault(require("playwright-core/package.json"));
|
|
13
|
+
const terminal_1 = require("@rushstack/terminal");
|
|
14
|
+
const HttpServer_1 = require("./HttpServer");
|
|
15
|
+
const { version: playwrightVersion } = package_json_1.default;
|
|
16
|
+
const SUPPORTED_BROWSER_NAMES = new Set(['chromium', 'firefox', 'webkit']);
|
|
17
|
+
const DEFAULT_LISTEN_PORT = 3000;
|
|
18
|
+
/**
|
|
19
|
+
* Creates a tunneled WebSocket endpoint that a local Playwright client can connect to.
|
|
20
|
+
* @beta
|
|
21
|
+
*/
|
|
22
|
+
async function tunneledBrowserConnection(logger, port = DEFAULT_LISTEN_PORT) {
|
|
23
|
+
// Server that remote peer (actual browser host) connects to
|
|
24
|
+
const remoteWsServer = new ws_1.WebSocketServer({ port });
|
|
25
|
+
// Local HTTP + WebSocket server where the playwright client will connect providing params
|
|
26
|
+
const httpServer = new HttpServer_1.HttpServer(logger);
|
|
27
|
+
await httpServer.listenAsync();
|
|
28
|
+
logger.writeLine(`Remote WebSocket server listening on ws://localhost:${port}`);
|
|
29
|
+
const localProxyWs = httpServer.wsServer;
|
|
30
|
+
const localProxyWsEndpoint = httpServer.endpoint;
|
|
31
|
+
let browserName;
|
|
32
|
+
let launchOptions;
|
|
33
|
+
let remoteSocket;
|
|
34
|
+
let handshakeAck = false;
|
|
35
|
+
let handshakeSent = false;
|
|
36
|
+
function maybeSendHandshake() {
|
|
37
|
+
if (!handshakeSent && remoteSocket && browserName && launchOptions) {
|
|
38
|
+
const handshake = {
|
|
39
|
+
action: 'handshake',
|
|
40
|
+
browserName,
|
|
41
|
+
launchOptions,
|
|
42
|
+
playwrightVersion
|
|
43
|
+
};
|
|
44
|
+
// Log handshake without 'headless' to avoid confusion (tunnel enforces headless: false)
|
|
45
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
46
|
+
const { headless, ...logOptions } = launchOptions;
|
|
47
|
+
const logHandshake = {
|
|
48
|
+
...handshake,
|
|
49
|
+
launchOptions: logOptions
|
|
50
|
+
};
|
|
51
|
+
logger.writeLine(`Sending handshake to remote: ${JSON.stringify(logHandshake)}`);
|
|
52
|
+
handshakeSent = true;
|
|
53
|
+
remoteSocket.send(JSON.stringify(handshake));
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return await new Promise((resolve) => {
|
|
57
|
+
remoteWsServer.on('error', (error) => {
|
|
58
|
+
logger.writeErrorLine(`Remote WebSocket server error: ${error}`);
|
|
59
|
+
});
|
|
60
|
+
remoteWsServer.on('close', () => {
|
|
61
|
+
logger.writeLine('Remote WebSocket server closed');
|
|
62
|
+
});
|
|
63
|
+
const bufferedLocalMessages = [];
|
|
64
|
+
remoteWsServer.on('connection', (ws) => {
|
|
65
|
+
logger.writeLine('Remote websocket connected');
|
|
66
|
+
remoteSocket = ws;
|
|
67
|
+
handshakeAck = false;
|
|
68
|
+
maybeSendHandshake();
|
|
69
|
+
ws.on('message', (message) => {
|
|
70
|
+
if (!handshakeAck) {
|
|
71
|
+
try {
|
|
72
|
+
const receivedHandshake = JSON.parse(message.toString());
|
|
73
|
+
if (receivedHandshake.action === 'handshakeAck') {
|
|
74
|
+
handshakeAck = true;
|
|
75
|
+
logger.writeLine('Received handshakeAck from remote');
|
|
76
|
+
}
|
|
77
|
+
else {
|
|
78
|
+
logger.writeErrorLine('Invalid handshake ack message');
|
|
79
|
+
ws.close();
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
catch (e) {
|
|
84
|
+
logger.writeErrorLine(`Failed parsing handshake ack: ${e}`);
|
|
85
|
+
ws.close();
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
// Resolve only once local proxy available and handshake acknowledged
|
|
89
|
+
if (handshakeAck) {
|
|
90
|
+
// Flush any buffered local messages now that tunnel is active
|
|
91
|
+
const activeRemote = remoteSocket;
|
|
92
|
+
if (activeRemote && activeRemote.readyState === ws_1.WebSocket.OPEN) {
|
|
93
|
+
while (bufferedLocalMessages.length > 0) {
|
|
94
|
+
const m = bufferedLocalMessages.shift();
|
|
95
|
+
if (m !== undefined) {
|
|
96
|
+
logger.writeLine(`Flushing buffered local message to remote: ${m}`);
|
|
97
|
+
activeRemote.send(m);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
else {
|
|
104
|
+
// Forward from remote to all local clients
|
|
105
|
+
localProxyWs.clients.forEach((client) => {
|
|
106
|
+
if (client.readyState === ws_1.WebSocket.OPEN) {
|
|
107
|
+
client.send(message);
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
ws.on('close', () => logger.writeLine('Remote websocket closed'));
|
|
113
|
+
ws.on('error', (err) => logger.writeErrorLine(`Remote websocket error: ${err}`));
|
|
114
|
+
});
|
|
115
|
+
localProxyWs.on('connection', (localWs, request) => {
|
|
116
|
+
try {
|
|
117
|
+
const urlString = request === null || request === void 0 ? void 0 : request.url;
|
|
118
|
+
if (urlString) {
|
|
119
|
+
const parsed = new URL(urlString, 'http://localhost');
|
|
120
|
+
logger.writeLine(`Local client connected with query params: ${parsed.searchParams.toString()}`);
|
|
121
|
+
const bName = parsed.searchParams.get('browser');
|
|
122
|
+
if (bName && SUPPORTED_BROWSER_NAMES.has(bName)) {
|
|
123
|
+
browserName = bName;
|
|
124
|
+
}
|
|
125
|
+
const launchOptionsParam = parsed.searchParams.get('launchOptions');
|
|
126
|
+
if (launchOptionsParam) {
|
|
127
|
+
try {
|
|
128
|
+
launchOptions = JSON.parse(launchOptionsParam);
|
|
129
|
+
}
|
|
130
|
+
catch (e) {
|
|
131
|
+
logger.writeErrorLine('Invalid launchOptions JSON provided');
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
catch (e) {
|
|
137
|
+
logger.writeErrorLine(`Error parsing local connection query params: ${e}`);
|
|
138
|
+
}
|
|
139
|
+
if (!browserName) {
|
|
140
|
+
const supportedBrowsersString = Array.from(SUPPORTED_BROWSER_NAMES).join('|');
|
|
141
|
+
logger.writeErrorLine(`browser query param required (${supportedBrowsersString})`);
|
|
142
|
+
localWs.close();
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
if (!launchOptions) {
|
|
146
|
+
launchOptions = {}; // default empty if not provided
|
|
147
|
+
}
|
|
148
|
+
maybeSendHandshake();
|
|
149
|
+
localWs.on('message', (message) => {
|
|
150
|
+
if (handshakeAck && (remoteSocket === null || remoteSocket === void 0 ? void 0 : remoteSocket.readyState) === ws_1.WebSocket.OPEN) {
|
|
151
|
+
remoteSocket.send(message);
|
|
152
|
+
}
|
|
153
|
+
else {
|
|
154
|
+
// Buffer until handshakeAck to avoid losing early protocol messages from Playwright
|
|
155
|
+
bufferedLocalMessages.push(message);
|
|
156
|
+
}
|
|
157
|
+
});
|
|
158
|
+
localWs.on('close', () => logger.writeLine('Local client websocket closed'));
|
|
159
|
+
localWs.on('error', (err) => logger.writeErrorLine(`Local client websocket error: ${err}`));
|
|
160
|
+
});
|
|
161
|
+
// Resolve immediately so caller can initiate local connection with query params (handshake completes later)
|
|
162
|
+
resolve({
|
|
163
|
+
remoteEndpoint: localProxyWsEndpoint,
|
|
164
|
+
[Symbol.dispose]() {
|
|
165
|
+
try {
|
|
166
|
+
remoteWsServer.close();
|
|
167
|
+
}
|
|
168
|
+
catch (_a) {
|
|
169
|
+
// ignore errors during remote WebSocket server shutdown
|
|
170
|
+
}
|
|
171
|
+
try {
|
|
172
|
+
httpServer[Symbol.dispose]();
|
|
173
|
+
}
|
|
174
|
+
catch (_b) {
|
|
175
|
+
// ignore errors during HTTP/WebSocket server shutdown
|
|
176
|
+
}
|
|
177
|
+
},
|
|
178
|
+
// eslint-disable-next-line promise/param-names
|
|
179
|
+
closePromise: new Promise((resolve2) => {
|
|
180
|
+
remoteWsServer.once('close', () => {
|
|
181
|
+
resolve2();
|
|
182
|
+
});
|
|
183
|
+
})
|
|
184
|
+
});
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* Creates a Playwright Browser instance connected via a tunneled WebSocket connection.
|
|
189
|
+
* @beta
|
|
190
|
+
*/
|
|
191
|
+
async function createTunneledBrowserAsync(browserName, launchOptions, logger, port = DEFAULT_LISTEN_PORT) {
|
|
192
|
+
// Establish the tunnel first (remoteEndpoint here refers to local proxy endpoint for connect())
|
|
193
|
+
if (!logger) {
|
|
194
|
+
const terminalProvider = new terminal_1.ConsoleTerminalProvider();
|
|
195
|
+
logger = new terminal_1.Terminal(terminalProvider);
|
|
196
|
+
}
|
|
197
|
+
const connection = await tunneledBrowserConnection(logger, port);
|
|
198
|
+
const { remoteEndpoint } = connection;
|
|
199
|
+
// Append query params for browser and launchOptions
|
|
200
|
+
const urlObj = new URL(remoteEndpoint);
|
|
201
|
+
urlObj.searchParams.set('browser', browserName);
|
|
202
|
+
urlObj.searchParams.set('launchOptions', JSON.stringify(launchOptions || {}));
|
|
203
|
+
const connectEndpoint = urlObj.toString();
|
|
204
|
+
const browser = await playwright_core_1.default[browserName].connect(connectEndpoint);
|
|
205
|
+
logger.writeLine(`Connected to remote browser at ${connectEndpoint}`);
|
|
206
|
+
return {
|
|
207
|
+
browser,
|
|
208
|
+
async [Symbol.asyncDispose]() {
|
|
209
|
+
logger.writeLine('Disposing browser');
|
|
210
|
+
await browser.close();
|
|
211
|
+
// Dispose the tunnel connection after browser is closed
|
|
212
|
+
connection[Symbol.dispose]();
|
|
213
|
+
}
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
//# sourceMappingURL=tunneledBrowserConnection.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tunneledBrowserConnection.js","sourceRoot":"","sources":["../src/tunneledBrowserConnection.ts"],"names":[],"mappings":"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","sourcesContent":["// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.\n// See LICENSE in the project root for license information.\n\nimport playwright from 'playwright-core';\nimport type { Browser, LaunchOptions } from 'playwright-core';\nimport { WebSocketServer, WebSocket, type RawData } from 'ws';\nimport playwrightPackageJson from 'playwright-core/package.json';\n\nimport { type ITerminal, Terminal, ConsoleTerminalProvider } from '@rushstack/terminal';\n\nimport type { BrowserName } from './PlaywrightBrowserTunnel';\nimport { HttpServer } from './HttpServer';\n\nconst { version: playwrightVersion } = playwrightPackageJson;\n\nconst SUPPORTED_BROWSER_NAMES: Set<string> = new Set(['chromium', 'firefox', 'webkit']);\n\ninterface IHandshake {\n action: 'handshake';\n browserName: BrowserName;\n launchOptions: LaunchOptions;\n playwrightVersion: string;\n}\n\ninterface IHandshakeAck {\n action: 'handshakeAck';\n}\n\nconst DEFAULT_LISTEN_PORT: number = 3000;\n\n/**\n * Disposable handle returned by {@link tunneledBrowserConnection}.\n * @beta\n */\nexport interface IDisposableTunneledBrowserConnection {\n /**\n * The WebSocket endpoint URL that the local Playwright client should connect to.\n */\n remoteEndpoint: string;\n /**\n * Dispose method that closes the WebSocket servers.\n * Called automatically when using `using` syntax.\n */\n [Symbol.dispose]: () => void;\n /**\n * Promise that resolves when the remote WebSocket server closes.\n */\n closePromise: Promise<void>;\n}\n\n/**\n * Creates a tunneled WebSocket endpoint that a local Playwright client can connect to.\n * @beta\n */\nexport async function tunneledBrowserConnection(\n logger: ITerminal,\n port: number = DEFAULT_LISTEN_PORT\n): Promise<IDisposableTunneledBrowserConnection> {\n // Server that remote peer (actual browser host) connects to\n const remoteWsServer: WebSocketServer = new WebSocketServer({ port });\n // Local HTTP + WebSocket server where the playwright client will connect providing params\n const httpServer: HttpServer = new HttpServer(logger);\n await httpServer.listenAsync();\n logger.writeLine(`Remote WebSocket server listening on ws://localhost:${port}`);\n\n const localProxyWs: WebSocketServer = httpServer.wsServer;\n const localProxyWsEndpoint: string = httpServer.endpoint;\n\n let browserName: BrowserName | undefined;\n let launchOptions: LaunchOptions | undefined;\n let remoteSocket: WebSocket | undefined;\n let handshakeAck: boolean = false;\n let handshakeSent: boolean = false;\n\n function maybeSendHandshake(): void {\n if (!handshakeSent && remoteSocket && browserName && launchOptions) {\n const handshake: IHandshake = {\n action: 'handshake',\n browserName,\n launchOptions,\n playwrightVersion\n };\n // Log handshake without 'headless' to avoid confusion (tunnel enforces headless: false)\n // eslint-disable-next-line @typescript-eslint/no-unused-vars\n const { headless, ...logOptions } = launchOptions;\n const logHandshake: Omit<IHandshake, 'launchOptions'> & {\n launchOptions: Omit<LaunchOptions, 'headless'>;\n } = {\n ...handshake,\n launchOptions: logOptions\n };\n logger.writeLine(`Sending handshake to remote: ${JSON.stringify(logHandshake)}`);\n handshakeSent = true;\n remoteSocket.send(JSON.stringify(handshake));\n }\n }\n\n return await new Promise((resolve) => {\n remoteWsServer.on('error', (error) => {\n logger.writeErrorLine(`Remote WebSocket server error: ${error}`);\n });\n\n remoteWsServer.on('close', () => {\n logger.writeLine('Remote WebSocket server closed');\n });\n\n const bufferedLocalMessages: Array<RawData> = [];\n\n remoteWsServer.on('connection', (ws) => {\n logger.writeLine('Remote websocket connected');\n remoteSocket = ws;\n handshakeAck = false;\n maybeSendHandshake();\n\n ws.on('message', (message) => {\n if (!handshakeAck) {\n try {\n const receivedHandshake: IHandshakeAck = JSON.parse(message.toString());\n if (receivedHandshake.action === 'handshakeAck') {\n handshakeAck = true;\n logger.writeLine('Received handshakeAck from remote');\n } else {\n logger.writeErrorLine('Invalid handshake ack message');\n ws.close();\n return;\n }\n } catch (e) {\n logger.writeErrorLine(`Failed parsing handshake ack: ${e}`);\n ws.close();\n return;\n }\n // Resolve only once local proxy available and handshake acknowledged\n if (handshakeAck) {\n // Flush any buffered local messages now that tunnel is active\n const activeRemote: WebSocket | undefined = remoteSocket;\n if (activeRemote && activeRemote.readyState === WebSocket.OPEN) {\n while (bufferedLocalMessages.length > 0) {\n const m: Buffer | ArrayBuffer | Buffer[] | string | undefined = bufferedLocalMessages.shift();\n if (m !== undefined) {\n logger.writeLine(`Flushing buffered local message to remote: ${m}`);\n activeRemote.send(m);\n }\n }\n }\n }\n } else {\n // Forward from remote to all local clients\n localProxyWs.clients.forEach((client) => {\n if (client.readyState === WebSocket.OPEN) {\n client.send(message);\n }\n });\n }\n });\n\n ws.on('close', () => logger.writeLine('Remote websocket closed'));\n ws.on('error', (err) => logger.writeErrorLine(`Remote websocket error: ${err}`));\n });\n\n localProxyWs.on('connection', (localWs, request) => {\n try {\n const urlString: string | undefined = request?.url;\n if (urlString) {\n const parsed: URL = new URL(urlString, 'http://localhost');\n logger.writeLine(`Local client connected with query params: ${parsed.searchParams.toString()}`);\n const bName: string | null = parsed.searchParams.get('browser');\n if (bName && SUPPORTED_BROWSER_NAMES.has(bName)) {\n browserName = bName as BrowserName;\n }\n const launchOptionsParam: string | null = parsed.searchParams.get('launchOptions');\n if (launchOptionsParam) {\n try {\n launchOptions = JSON.parse(launchOptionsParam);\n } catch (e) {\n logger.writeErrorLine('Invalid launchOptions JSON provided');\n }\n }\n }\n } catch (e) {\n logger.writeErrorLine(`Error parsing local connection query params: ${e}`);\n }\n\n if (!browserName) {\n const supportedBrowsersString: string = Array.from(SUPPORTED_BROWSER_NAMES).join('|');\n logger.writeErrorLine(`browser query param required (${supportedBrowsersString})`);\n localWs.close();\n return;\n }\n if (!launchOptions) {\n launchOptions = {} as LaunchOptions; // default empty if not provided\n }\n\n maybeSendHandshake();\n\n localWs.on('message', (message) => {\n if (handshakeAck && remoteSocket?.readyState === WebSocket.OPEN) {\n remoteSocket.send(message);\n } else {\n // Buffer until handshakeAck to avoid losing early protocol messages from Playwright\n bufferedLocalMessages.push(message);\n }\n });\n localWs.on('close', () => logger.writeLine('Local client websocket closed'));\n localWs.on('error', (err) => logger.writeErrorLine(`Local client websocket error: ${err}`));\n });\n\n // Resolve immediately so caller can initiate local connection with query params (handshake completes later)\n resolve({\n remoteEndpoint: localProxyWsEndpoint,\n [Symbol.dispose]() {\n try {\n remoteWsServer.close();\n } catch {\n // ignore errors during remote WebSocket server shutdown\n }\n try {\n httpServer[Symbol.dispose]();\n } catch {\n // ignore errors during HTTP/WebSocket server shutdown\n }\n },\n // eslint-disable-next-line promise/param-names\n closePromise: new Promise<void>((resolve2) => {\n remoteWsServer.once('close', () => {\n resolve2();\n });\n })\n });\n });\n}\n\n/**\n * Disposable handle returned by {@link createTunneledBrowserAsync}.\n * @beta\n */\nexport interface IDisposableTunneledBrowser {\n /**\n * The connected Playwright Browser instance.\n */\n browser: Browser;\n /**\n * Async dispose method that closes the browser connection.\n * Called automatically when using `await using` syntax.\n */\n [Symbol.asyncDispose]: () => Promise<void>;\n}\n\n/**\n * Creates a Playwright Browser instance connected via a tunneled WebSocket connection.\n * @beta\n */\nexport async function createTunneledBrowserAsync(\n browserName: BrowserName,\n launchOptions: LaunchOptions,\n logger?: ITerminal,\n port: number = DEFAULT_LISTEN_PORT\n): Promise<IDisposableTunneledBrowser> {\n // Establish the tunnel first (remoteEndpoint here refers to local proxy endpoint for connect())\n\n if (!logger) {\n const terminalProvider: ConsoleTerminalProvider = new ConsoleTerminalProvider();\n logger = new Terminal(terminalProvider);\n }\n\n const connection: IDisposableTunneledBrowserConnection = await tunneledBrowserConnection(logger, port);\n const { remoteEndpoint } = connection;\n // Append query params for browser and launchOptions\n const urlObj: URL = new URL(remoteEndpoint);\n urlObj.searchParams.set('browser', browserName);\n urlObj.searchParams.set('launchOptions', JSON.stringify(launchOptions || {}));\n const connectEndpoint: string = urlObj.toString();\n const browser: Browser = await playwright[browserName].connect(connectEndpoint);\n logger.writeLine(`Connected to remote browser at ${connectEndpoint}`);\n\n return {\n browser,\n async [Symbol.asyncDispose]() {\n logger.writeLine('Disposing browser');\n await browser.close();\n // Dispose the tunnel connection after browser is closed\n connection[Symbol.dispose]();\n }\n };\n}\n"]}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The filename used to indicate that the Playwright on Codespaces extension is installed.
|
|
3
|
+
* @beta
|
|
4
|
+
*/
|
|
5
|
+
export declare const EXTENSION_INSTALLED_FILENAME: string;
|
|
6
|
+
/**
|
|
7
|
+
* Helper to determine if the Playwright on Codespaces extension is installed. This check's for the
|
|
8
|
+
* existence of a well-known file in the OS temp directory.
|
|
9
|
+
* @beta
|
|
10
|
+
*/
|
|
11
|
+
export declare function isExtensionInstalledAsync(): Promise<boolean>;
|
|
12
|
+
/**
|
|
13
|
+
* Normalizes an error to a string for logging purposes.
|
|
14
|
+
* @beta
|
|
15
|
+
*/
|
|
16
|
+
export declare function getNormalizedErrorString(error: unknown): string;
|
|
17
|
+
//# sourceMappingURL=utilities.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"utilities.d.ts","sourceRoot":"","sources":["../src/utilities.ts"],"names":[],"mappings":"AAOA;;;GAGG;AACH,eAAO,MAAM,4BAA4B,EAAE,MAAyD,CAAC;AAErG;;;;GAIG;AACH,wBAAsB,yBAAyB,IAAI,OAAO,CAAC,OAAO,CAAC,CASlE;AAED;;;GAGG;AACH,wBAAgB,wBAAwB,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CAQ/D"}
|
package/lib/utilities.js
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
|
|
3
|
+
// See LICENSE in the project root for license information.
|
|
4
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
5
|
+
exports.EXTENSION_INSTALLED_FILENAME = void 0;
|
|
6
|
+
exports.isExtensionInstalledAsync = isExtensionInstalledAsync;
|
|
7
|
+
exports.getNormalizedErrorString = getNormalizedErrorString;
|
|
8
|
+
const node_os_1 = require("node:os");
|
|
9
|
+
const node_core_library_1 = require("@rushstack/node-core-library");
|
|
10
|
+
/**
|
|
11
|
+
* The filename used to indicate that the Playwright on Codespaces extension is installed.
|
|
12
|
+
* @beta
|
|
13
|
+
*/
|
|
14
|
+
exports.EXTENSION_INSTALLED_FILENAME = '.playwright-codespaces-extension-installed.txt';
|
|
15
|
+
/**
|
|
16
|
+
* Helper to determine if the Playwright on Codespaces extension is installed. This check's for the
|
|
17
|
+
* existence of a well-known file in the OS temp directory.
|
|
18
|
+
* @beta
|
|
19
|
+
*/
|
|
20
|
+
async function isExtensionInstalledAsync() {
|
|
21
|
+
// Read file from os.tempdir() + '/.playwright-codespaces-extension-installed'
|
|
22
|
+
const tempDir = (0, node_os_1.tmpdir)();
|
|
23
|
+
const extensionInstalledFilePath = `${tempDir}/${exports.EXTENSION_INSTALLED_FILENAME}`;
|
|
24
|
+
const doesExist = node_core_library_1.FileSystem.exists(extensionInstalledFilePath);
|
|
25
|
+
// check if file exists
|
|
26
|
+
return doesExist;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Normalizes an error to a string for logging purposes.
|
|
30
|
+
* @beta
|
|
31
|
+
*/
|
|
32
|
+
function getNormalizedErrorString(error) {
|
|
33
|
+
if (error instanceof Error) {
|
|
34
|
+
if (error.stack) {
|
|
35
|
+
return error.stack;
|
|
36
|
+
}
|
|
37
|
+
return error.message;
|
|
38
|
+
}
|
|
39
|
+
return String(error);
|
|
40
|
+
}
|
|
41
|
+
//# sourceMappingURL=utilities.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"utilities.js","sourceRoot":"","sources":["../src/utilities.ts"],"names":[],"mappings":";AAAA,4FAA4F;AAC5F,2DAA2D;;;AAiB3D,8DASC;AAMD,4DAQC;AAtCD,qCAAiC;AAEjC,oEAA0D;AAE1D;;;GAGG;AACU,QAAA,4BAA4B,GAAW,gDAAgD,CAAC;AAErG;;;;GAIG;AACI,KAAK,UAAU,yBAAyB;IAC7C,8EAA8E;IAC9E,MAAM,OAAO,GAAW,IAAA,gBAAM,GAAE,CAAC;IAEjC,MAAM,0BAA0B,GAAW,GAAG,OAAO,IAAI,oCAA4B,EAAE,CAAC;IACxF,MAAM,SAAS,GAAY,8BAAU,CAAC,MAAM,CAAC,0BAA0B,CAAC,CAAC;IAEzE,uBAAuB;IACvB,OAAO,SAAS,CAAC;AACnB,CAAC;AAED;;;GAGG;AACH,SAAgB,wBAAwB,CAAC,KAAc;IACrD,IAAI,KAAK,YAAY,KAAK,EAAE,CAAC;QAC3B,IAAI,KAAK,CAAC,KAAK,EAAE,CAAC;YAChB,OAAO,KAAK,CAAC,KAAK,CAAC;QACrB,CAAC;QACD,OAAO,KAAK,CAAC,OAAO,CAAC;IACvB,CAAC;IACD,OAAO,MAAM,CAAC,KAAK,CAAC,CAAC;AACvB,CAAC","sourcesContent":["// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.\n// See LICENSE in the project root for license information.\n\nimport { tmpdir } from 'node:os';\n\nimport { FileSystem } from '@rushstack/node-core-library';\n\n/**\n * The filename used to indicate that the Playwright on Codespaces extension is installed.\n * @beta\n */\nexport const EXTENSION_INSTALLED_FILENAME: string = '.playwright-codespaces-extension-installed.txt';\n\n/**\n * Helper to determine if the Playwright on Codespaces extension is installed. This check's for the\n * existence of a well-known file in the OS temp directory.\n * @beta\n */\nexport async function isExtensionInstalledAsync(): Promise<boolean> {\n // Read file from os.tempdir() + '/.playwright-codespaces-extension-installed'\n const tempDir: string = tmpdir();\n\n const extensionInstalledFilePath: string = `${tempDir}/${EXTENSION_INSTALLED_FILENAME}`;\n const doesExist: boolean = FileSystem.exists(extensionInstalledFilePath);\n\n // check if file exists\n return doesExist;\n}\n\n/**\n * Normalizes an error to a string for logging purposes.\n * @beta\n */\nexport function getNormalizedErrorString(error: unknown): string {\n if (error instanceof Error) {\n if (error.stack) {\n return error.stack;\n }\n return error.message;\n }\n return String(error);\n}\n"]}
|
package/package.json
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@rushstack/playwright-browser-tunnel",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Run a remote Playwright Browser Tunnel. Useful in remote development environments.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "https://github.com/microsoft/rushstack.git",
|
|
9
|
+
"directory": "apps/playwright-browser-tunnel"
|
|
10
|
+
},
|
|
11
|
+
"main": "lib/index.js",
|
|
12
|
+
"engines": {
|
|
13
|
+
"node": ">=20.0.0"
|
|
14
|
+
},
|
|
15
|
+
"engineStrict": true,
|
|
16
|
+
"homepage": "https://rushstack.io",
|
|
17
|
+
"dependencies": {
|
|
18
|
+
"string-argv": "~0.3.1",
|
|
19
|
+
"semver": "~7.5.4",
|
|
20
|
+
"ws": "~8.14.1",
|
|
21
|
+
"playwright": "1.56.1",
|
|
22
|
+
"@rushstack/node-core-library": "5.19.1",
|
|
23
|
+
"@rushstack/terminal": "0.21.0",
|
|
24
|
+
"@rushstack/ts-command-line": "5.1.7"
|
|
25
|
+
},
|
|
26
|
+
"devDependencies": {
|
|
27
|
+
"eslint": "~9.37.0",
|
|
28
|
+
"@types/semver": "7.5.0",
|
|
29
|
+
"@types/ws": "8.5.5",
|
|
30
|
+
"playwright-core": "~1.56.1",
|
|
31
|
+
"@playwright/test": "~1.56.1",
|
|
32
|
+
"@types/node": "20.17.19",
|
|
33
|
+
"@rushstack/heft": "1.1.10",
|
|
34
|
+
"local-node-rig": "1.0.0"
|
|
35
|
+
},
|
|
36
|
+
"peerDependencies": {
|
|
37
|
+
"playwright-core": "~1.56.1"
|
|
38
|
+
},
|
|
39
|
+
"scripts": {
|
|
40
|
+
"build": "heft build --clean",
|
|
41
|
+
"_phase:build": "heft run --only build -- --clean",
|
|
42
|
+
"demo": "playwright test --config=playwright.config.ts"
|
|
43
|
+
}
|
|
44
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { defineConfig, devices } from '@playwright/test';
|
|
2
|
+
|
|
3
|
+
export default defineConfig({
|
|
4
|
+
testDir: './tests',
|
|
5
|
+
/* Run tests in files in parallel */
|
|
6
|
+
fullyParallel: true,
|
|
7
|
+
/* Retry on CI only */
|
|
8
|
+
retries: 0,
|
|
9
|
+
/* Opt out of parallel tests on CI. */
|
|
10
|
+
workers: 1,
|
|
11
|
+
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
|
|
12
|
+
reporter: 'html',
|
|
13
|
+
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
|
|
14
|
+
use: {
|
|
15
|
+
/* Base URL to use in actions like `await page.goto('/')`. */
|
|
16
|
+
// baseURL: 'http://localhost:3000',
|
|
17
|
+
|
|
18
|
+
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
|
|
19
|
+
trace: 'on'
|
|
20
|
+
},
|
|
21
|
+
|
|
22
|
+
/* Configure projects for major browsers */
|
|
23
|
+
projects: [
|
|
24
|
+
{
|
|
25
|
+
name: 'chromium',
|
|
26
|
+
use: { ...devices['Desktop Chrome'] }
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
name: 'firefox',
|
|
30
|
+
use: { ...devices['Desktop Firefox'] }
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
name: 'webkit',
|
|
34
|
+
use: { ...devices['Desktop Safari'] }
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
name: 'Google Chrome',
|
|
38
|
+
use: { ...devices['Desktop Chrome'], channel: 'chrome' } // or 'chrome-beta'
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
name: 'Microsoft Edge',
|
|
42
|
+
use: { ...devices['Desktop Edge'], channel: 'msedge' } // or "msedge-beta" or 'msedge-dev'
|
|
43
|
+
}
|
|
44
|
+
]
|
|
45
|
+
});
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
Invoking: heft run --only build -- --clean --production
|
|
2
|
+
---- build started ----
|
|
3
|
+
[build:typescript] Using TypeScript version 5.8.2
|
|
4
|
+
[build:lint] Using ESLint version 9.37.0
|
|
5
|
+
[build:api-extractor] Using API Extractor version 7.55.5
|
|
6
|
+
[build:api-extractor] Analysis will use the bundled TypeScript version 5.8.2
|
|
7
|
+
---- build finished (22.696s) ----
|
|
8
|
+
-------------------- Finished (22.704s) --------------------
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
|
|
2
|
+
// See LICENSE in the project root for license information.
|
|
3
|
+
|
|
4
|
+
import http from 'node:http';
|
|
5
|
+
import type { AddressInfo } from 'node:net';
|
|
6
|
+
import { URL } from 'node:url';
|
|
7
|
+
|
|
8
|
+
import { WebSocketServer, type WebSocket } from 'ws';
|
|
9
|
+
|
|
10
|
+
import type { ITerminal } from '@rushstack/terminal';
|
|
11
|
+
|
|
12
|
+
const LOCALHOST: string = 'localhost';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Formats an address info object into a WebSocket-compatible address string.
|
|
16
|
+
* IPv6 addresses are formatted with brackets: [address]:port
|
|
17
|
+
* IPv4 addresses are formatted as: address:port
|
|
18
|
+
*/
|
|
19
|
+
function formatAddress(addressInfo: AddressInfo): string {
|
|
20
|
+
return addressInfo.family === 'IPv6'
|
|
21
|
+
? `[${addressInfo.address}]:${addressInfo.port}`
|
|
22
|
+
: `${addressInfo.address}:${addressInfo.port}`;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* This HttpServer is used for the localProxyWs WebSocketServer.
|
|
27
|
+
* The purpose is to parse the query params and path for the websocket url to get the
|
|
28
|
+
* browserName and launchOptions.
|
|
29
|
+
*/
|
|
30
|
+
export class HttpServer {
|
|
31
|
+
private readonly _server: http.Server;
|
|
32
|
+
private readonly _wsServer: WebSocketServer; // local proxy websocket server accepting browser clients
|
|
33
|
+
private _listeningAddress: string | undefined;
|
|
34
|
+
private _logger: ITerminal;
|
|
35
|
+
|
|
36
|
+
public constructor(logger: ITerminal) {
|
|
37
|
+
this._logger = logger;
|
|
38
|
+
// We'll create an HTTP server and attach a WebSocketServer in noServer mode so we can
|
|
39
|
+
// manually parse the URL and extract query parameters before upgrading.
|
|
40
|
+
this._server = http.createServer();
|
|
41
|
+
this._wsServer = new WebSocketServer({ noServer: true });
|
|
42
|
+
|
|
43
|
+
this._server.on('upgrade', (request, socket, head) => {
|
|
44
|
+
// Accept all upgrades on the root path. We parse query string for browserName + launchOptions.
|
|
45
|
+
this._wsServer.handleUpgrade(request, socket, head, (ws: WebSocket) => {
|
|
46
|
+
this._wsServer.emit('connection', ws, request);
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
public async listenAsync(): Promise<URL> {
|
|
52
|
+
return await new Promise((resolve) => {
|
|
53
|
+
// Bind to 'localhost' which resolves to IPv4 (127.0.0.1) or IPv6 (::1)
|
|
54
|
+
// depending on system configuration and DNS resolution
|
|
55
|
+
this._server.listen(0, LOCALHOST, () => {
|
|
56
|
+
const addressInfo: AddressInfo | string | null = this._server.address();
|
|
57
|
+
if (!addressInfo) {
|
|
58
|
+
throw new Error('Server address is null - server may not be bound properly');
|
|
59
|
+
}
|
|
60
|
+
if (typeof addressInfo === 'string') {
|
|
61
|
+
throw new Error(`Server address is a pipe/socket path (${addressInfo}), expected an IP address`);
|
|
62
|
+
}
|
|
63
|
+
const formattedAddress: string = formatAddress(addressInfo);
|
|
64
|
+
this._listeningAddress = formattedAddress;
|
|
65
|
+
// This MUST be printed to terminal so VS Code can auto-port forward
|
|
66
|
+
this._logger.writeLine(`Local proxy HttpServer listening at ws://${formattedAddress}`);
|
|
67
|
+
resolve(new URL(`ws://${formattedAddress}`));
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
public get endpoint(): string {
|
|
73
|
+
if (this._listeningAddress === undefined) {
|
|
74
|
+
throw new Error('HttpServer not listening yet');
|
|
75
|
+
}
|
|
76
|
+
return `ws://${this._listeningAddress}`;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
public get wsServer(): WebSocketServer {
|
|
80
|
+
return this._wsServer;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
public [Symbol.dispose](): void {
|
|
84
|
+
this._wsServer.close();
|
|
85
|
+
this._server.close();
|
|
86
|
+
}
|
|
87
|
+
}
|