@rushstack/playwright-browser-tunnel 0.2.0 → 0.2.2

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.
Files changed (27) hide show
  1. package/.rush/temp/chunked-rush-logs/playwright-browser-tunnel._phase_build.chunks.jsonl +3 -3
  2. package/.rush/temp/operation/_phase_build/all.log +3 -3
  3. package/.rush/temp/operation/_phase_build/log-chunks.jsonl +3 -3
  4. package/.rush/temp/operation/_phase_build/state.json +1 -1
  5. package/.rush/temp/{rushstack+playwright-browser-tunnel-_phase_build-1937f4c0911bcb65c94b2b4decfde58532afb468.tar.log → rushstack+playwright-browser-tunnel-_phase_build-3f67426e34dc3685ce06e290853fe1361c9ed401.tar.log} +2 -2
  6. package/CHANGELOG.json +32 -0
  7. package/CHANGELOG.md +13 -1
  8. package/lib/PlaywrightBrowserTunnel.d.ts.map +1 -1
  9. package/lib/PlaywrightBrowserTunnel.js +54 -15
  10. package/lib/PlaywrightBrowserTunnel.js.map +1 -1
  11. package/lib/tsdoc-metadata.json +1 -1
  12. package/lib/tunneledBrowserConnection.d.ts.map +1 -1
  13. package/lib/tunneledBrowserConnection.js +25 -7
  14. package/lib/tunneledBrowserConnection.js.map +1 -1
  15. package/lib/utilities.d.ts +49 -0
  16. package/lib/utilities.d.ts.map +1 -1
  17. package/lib/utilities.js +63 -1
  18. package/lib/utilities.js.map +1 -1
  19. package/package.json +5 -5
  20. package/rush-logs/playwright-browser-tunnel._phase_build.cache.log +1 -1
  21. package/rush-logs/playwright-browser-tunnel._phase_build.log +3 -3
  22. package/src/PlaywrightBrowserTunnel.ts +76 -16
  23. package/src/tunneledBrowserConnection.ts +38 -7
  24. package/src/utilities.ts +93 -0
  25. package/temp/build/lint/_eslint-5eVG3S6w.json +3 -3
  26. package/temp/build/lint/lint.sarif +8 -8
  27. package/temp/build/typescript/ts_l9Fw4VUO.json +1 -1
@@ -1 +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 = 56767;\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"]}
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';\nimport {\n getNormalizedErrorString,\n getWebSocketCloseReason,\n getWebSocketReadyStateString,\n WebSocketCloseCode\n} from './utilities';\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 = 56767;\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(WebSocketCloseCode.PROTOCOL_ERROR, 'Invalid handshake ack');\n return;\n }\n } catch (e) {\n logger.writeErrorLine(`Failed parsing handshake ack: ${e}`);\n ws.close(WebSocketCloseCode.PROTOCOL_ERROR, 'Failed parsing handshake');\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', (code: number, reason: Buffer) => {\n const reasonStr: string = reason.toString() || 'no reason provided';\n const codeDescription: string = getWebSocketCloseReason(code);\n logger.writeDebugLine(\n `Remote websocket closed - code: ${code} (${codeDescription}), reason: ${reasonStr}`\n );\n logger.writeDebugLine(\n ` Connection state at close: handshakeSent=${handshakeSent}, handshakeAck=${handshakeAck}`\n );\n logger.writeDebugLine(` Buffered messages pending: ${bufferedLocalMessages.length}`);\n });\n ws.on('error', (err: Error) => {\n logger.writeErrorLine(`Remote websocket error: ${getNormalizedErrorString(err)}`);\n logger.writeErrorLine(` Socket readyState: ${getWebSocketReadyStateString(ws.readyState)}`);\n });\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(WebSocketCloseCode.PROTOCOL_ERROR, 'Missing browser param');\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', (code: number, reason: Buffer) => {\n const reasonStr: string = reason.toString() || 'no reason provided';\n const codeDescription: string = getWebSocketCloseReason(code);\n logger.writeDebugLine(\n `Local client websocket closed - code: ${code} (${codeDescription}), reason: ${reasonStr}`\n );\n logger.writeDebugLine(\n ` Remote socket state: ${remoteSocket ? getWebSocketReadyStateString(remoteSocket.readyState) : 'undefined'}`\n );\n logger.writeDebugLine(` handshakeAck: ${handshakeAck}`);\n });\n localWs.on('error', (err: Error) => {\n logger.writeErrorLine(`Local client websocket error: ${getNormalizedErrorString(err)}`);\n });\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"]}
@@ -14,4 +14,53 @@ export declare function isExtensionInstalledAsync(): Promise<boolean>;
14
14
  * @beta
15
15
  */
16
16
  export declare function getNormalizedErrorString(error: unknown): string;
17
+ /**
18
+ * WebSocket close codes as defined by RFC 6455.
19
+ * @see {@link https://datatracker.ietf.org/doc/html/rfc6455#section-11.7}
20
+ * @beta
21
+ */
22
+ export declare const WebSocketCloseCode: {
23
+ /** Normal closure; the connection successfully completed. */
24
+ readonly NORMAL_CLOSURE: 1000;
25
+ /** Endpoint is going away (e.g., server shutting down, browser navigating away). */
26
+ readonly GOING_AWAY: 1001;
27
+ /** Protocol error encountered. */
28
+ readonly PROTOCOL_ERROR: 1002;
29
+ /** Received data type that cannot be accepted (e.g., text-only endpoint received binary). */
30
+ readonly UNSUPPORTED_DATA: 1003;
31
+ /** No status code was provided even though one was expected. */
32
+ readonly NO_STATUS_RECEIVED: 1005;
33
+ /** Connection was closed abnormally (e.g., without sending a close frame). */
34
+ readonly ABNORMAL_CLOSURE: 1006;
35
+ /** Received message data inconsistent with the message type. */
36
+ readonly INVALID_PAYLOAD: 1007;
37
+ /** Received a message that violates policy. */
38
+ readonly POLICY_VIOLATION: 1008;
39
+ /** Received a message that is too big to process. */
40
+ readonly MESSAGE_TOO_BIG: 1009;
41
+ /** Server encountered an unexpected condition that prevented it from fulfilling the request. */
42
+ readonly INTERNAL_ERROR: 1011;
43
+ /** Connection was closed due to TLS handshake failure. */
44
+ readonly TLS_HANDSHAKE_FAILED: 1015;
45
+ };
46
+ /**
47
+ * Type for WebSocket close code values.
48
+ * @beta
49
+ */
50
+ export type WebSocketCloseCodeValue = (typeof WebSocketCloseCode)[keyof typeof WebSocketCloseCode];
51
+ /**
52
+ * Human-readable descriptions for WebSocket close codes.
53
+ * @beta
54
+ */
55
+ export declare const WebSocketCloseCodeDescriptions: Record<WebSocketCloseCodeValue, string>;
56
+ /**
57
+ * Returns a human-readable description for a WebSocket close code.
58
+ * @beta
59
+ */
60
+ export declare function getWebSocketCloseReason(code: number): string;
61
+ /**
62
+ * Returns a human-readable string for a WebSocket ready state.
63
+ * @beta
64
+ */
65
+ export declare function getWebSocketReadyStateString(readyState: number): string;
17
66
  //# sourceMappingURL=utilities.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"utilities.d.ts","sourceRoot":"","sources":["../src/utilities.ts"],"names":[],"mappings":"AAOA;;;GAGG;AACH,eAAO,MAAM,4BAA4B,EAAE,MACiB,CAAC;AAE7D;;;;GAIG;AACH,wBAAsB,yBAAyB,IAAI,OAAO,CAAC,OAAO,CAAC,CASlE;AAED;;;GAGG;AACH,wBAAgB,wBAAwB,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CAQ/D"}
1
+ {"version":3,"file":"utilities.d.ts","sourceRoot":"","sources":["../src/utilities.ts"],"names":[],"mappings":"AAOA;;;GAGG;AACH,eAAO,MAAM,4BAA4B,EAAE,MACiB,CAAC;AAE7D;;;;GAIG;AACH,wBAAsB,yBAAyB,IAAI,OAAO,CAAC,OAAO,CAAC,CASlE;AAED;;;GAGG;AACH,wBAAgB,wBAAwB,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CAQ/D;AAED;;;;GAIG;AACH,eAAO,MAAM,kBAAkB,EAAE;IAC/B,6DAA6D;IAC7D,QAAQ,CAAC,cAAc,EAAE,IAAI,CAAC;IAC9B,oFAAoF;IACpF,QAAQ,CAAC,UAAU,EAAE,IAAI,CAAC;IAC1B,kCAAkC;IAClC,QAAQ,CAAC,cAAc,EAAE,IAAI,CAAC;IAC9B,6FAA6F;IAC7F,QAAQ,CAAC,gBAAgB,EAAE,IAAI,CAAC;IAChC,gEAAgE;IAChE,QAAQ,CAAC,kBAAkB,EAAE,IAAI,CAAC;IAClC,8EAA8E;IAC9E,QAAQ,CAAC,gBAAgB,EAAE,IAAI,CAAC;IAChC,gEAAgE;IAChE,QAAQ,CAAC,eAAe,EAAE,IAAI,CAAC;IAC/B,+CAA+C;IAC/C,QAAQ,CAAC,gBAAgB,EAAE,IAAI,CAAC;IAChC,qDAAqD;IACrD,QAAQ,CAAC,eAAe,EAAE,IAAI,CAAC;IAC/B,gGAAgG;IAChG,QAAQ,CAAC,cAAc,EAAE,IAAI,CAAC;IAC9B,0DAA0D;IAC1D,QAAQ,CAAC,oBAAoB,EAAE,IAAI,CAAC;CAarC,CAAC;AAEF;;;GAGG;AACH,MAAM,MAAM,uBAAuB,GAAG,CAAC,OAAO,kBAAkB,CAAC,CAAC,MAAM,OAAO,kBAAkB,CAAC,CAAC;AAEnG;;;GAGG;AACH,eAAO,MAAM,8BAA8B,EAAE,MAAM,CAAC,uBAAuB,EAAE,MAAM,CAYlF,CAAC;AAEF;;;GAGG;AACH,wBAAgB,uBAAuB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAE5D;AAED;;;GAGG;AACH,wBAAgB,4BAA4B,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,CAavE"}
package/lib/utilities.js CHANGED
@@ -2,9 +2,11 @@
2
2
  // Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
3
3
  // See LICENSE in the project root for license information.
4
4
  Object.defineProperty(exports, "__esModule", { value: true });
5
- exports.EXTENSION_INSTALLED_FILENAME = void 0;
5
+ exports.WebSocketCloseCodeDescriptions = exports.WebSocketCloseCode = exports.EXTENSION_INSTALLED_FILENAME = void 0;
6
6
  exports.isExtensionInstalledAsync = isExtensionInstalledAsync;
7
7
  exports.getNormalizedErrorString = getNormalizedErrorString;
8
+ exports.getWebSocketCloseReason = getWebSocketCloseReason;
9
+ exports.getWebSocketReadyStateString = getWebSocketReadyStateString;
8
10
  const node_os_1 = require("node:os");
9
11
  const node_core_library_1 = require("@rushstack/node-core-library");
10
12
  /**
@@ -38,4 +40,64 @@ function getNormalizedErrorString(error) {
38
40
  }
39
41
  return String(error);
40
42
  }
43
+ /**
44
+ * WebSocket close codes as defined by RFC 6455.
45
+ * @see {@link https://datatracker.ietf.org/doc/html/rfc6455#section-11.7}
46
+ * @beta
47
+ */
48
+ exports.WebSocketCloseCode = {
49
+ NORMAL_CLOSURE: 1000,
50
+ GOING_AWAY: 1001,
51
+ PROTOCOL_ERROR: 1002,
52
+ UNSUPPORTED_DATA: 1003,
53
+ NO_STATUS_RECEIVED: 1005,
54
+ ABNORMAL_CLOSURE: 1006,
55
+ INVALID_PAYLOAD: 1007,
56
+ POLICY_VIOLATION: 1008,
57
+ MESSAGE_TOO_BIG: 1009,
58
+ INTERNAL_ERROR: 1011,
59
+ TLS_HANDSHAKE_FAILED: 1015
60
+ };
61
+ /**
62
+ * Human-readable descriptions for WebSocket close codes.
63
+ * @beta
64
+ */
65
+ exports.WebSocketCloseCodeDescriptions = {
66
+ [exports.WebSocketCloseCode.NORMAL_CLOSURE]: 'Normal Closure',
67
+ [exports.WebSocketCloseCode.GOING_AWAY]: 'Going Away',
68
+ [exports.WebSocketCloseCode.PROTOCOL_ERROR]: 'Protocol Error',
69
+ [exports.WebSocketCloseCode.UNSUPPORTED_DATA]: 'Unsupported Data',
70
+ [exports.WebSocketCloseCode.NO_STATUS_RECEIVED]: 'No Status Received',
71
+ [exports.WebSocketCloseCode.ABNORMAL_CLOSURE]: 'Abnormal Closure (connection lost)',
72
+ [exports.WebSocketCloseCode.INVALID_PAYLOAD]: 'Invalid Payload',
73
+ [exports.WebSocketCloseCode.POLICY_VIOLATION]: 'Policy Violation',
74
+ [exports.WebSocketCloseCode.MESSAGE_TOO_BIG]: 'Message Too Big',
75
+ [exports.WebSocketCloseCode.INTERNAL_ERROR]: 'Internal Error',
76
+ [exports.WebSocketCloseCode.TLS_HANDSHAKE_FAILED]: 'TLS Handshake Failed'
77
+ };
78
+ /**
79
+ * Returns a human-readable description for a WebSocket close code.
80
+ * @beta
81
+ */
82
+ function getWebSocketCloseReason(code) {
83
+ return exports.WebSocketCloseCodeDescriptions[code] || 'Unknown';
84
+ }
85
+ /**
86
+ * Returns a human-readable string for a WebSocket ready state.
87
+ * @beta
88
+ */
89
+ function getWebSocketReadyStateString(readyState) {
90
+ switch (readyState) {
91
+ case 0:
92
+ return 'CONNECTING';
93
+ case 1:
94
+ return 'OPEN';
95
+ case 2:
96
+ return 'CLOSING';
97
+ case 3:
98
+ return 'CLOSED';
99
+ default:
100
+ return `UNKNOWN(${readyState})`;
101
+ }
102
+ }
41
103
  //# sourceMappingURL=utilities.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"utilities.js","sourceRoot":"","sources":["../src/utilities.ts"],"names":[],"mappings":";AAAA,4FAA4F;AAC5F,2DAA2D;;;AAkB3D,8DASC;AAMD,4DAQC;AAvCD,qCAAiC;AAEjC,oEAA0D;AAE1D;;;GAGG;AACU,QAAA,4BAA4B,GACvC,0DAA0D,CAAC;AAE7D;;;;GAIG;AACI,KAAK,UAAU,yBAAyB;IAC7C,wFAAwF;IACxF,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 Local Browser Server extension is installed.\n * @beta\n */\nexport const EXTENSION_INSTALLED_FILENAME: string =\n '.playwright-local-browser-server-extension-installed.txt';\n\n/**\n * Helper to determine if the Playwright Local Browser Server extension is installed. This checks 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-local-browser-server-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"]}
1
+ {"version":3,"file":"utilities.js","sourceRoot":"","sources":["../src/utilities.ts"],"names":[],"mappings":";AAAA,4FAA4F;AAC5F,2DAA2D;;;AAkB3D,8DASC;AAMD,4DAQC;AAwED,0DAEC;AAMD,oEAaC;AApID,qCAAiC;AAEjC,oEAA0D;AAE1D;;;GAGG;AACU,QAAA,4BAA4B,GACvC,0DAA0D,CAAC;AAE7D;;;;GAIG;AACI,KAAK,UAAU,yBAAyB;IAC7C,wFAAwF;IACxF,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;AAED;;;;GAIG;AACU,QAAA,kBAAkB,GAuB3B;IACF,cAAc,EAAE,IAAI;IACpB,UAAU,EAAE,IAAI;IAChB,cAAc,EAAE,IAAI;IACpB,gBAAgB,EAAE,IAAI;IACtB,kBAAkB,EAAE,IAAI;IACxB,gBAAgB,EAAE,IAAI;IACtB,eAAe,EAAE,IAAI;IACrB,gBAAgB,EAAE,IAAI;IACtB,eAAe,EAAE,IAAI;IACrB,cAAc,EAAE,IAAI;IACpB,oBAAoB,EAAE,IAAI;CAC3B,CAAC;AAQF;;;GAGG;AACU,QAAA,8BAA8B,GAA4C;IACrF,CAAC,0BAAkB,CAAC,cAAc,CAAC,EAAE,gBAAgB;IACrD,CAAC,0BAAkB,CAAC,UAAU,CAAC,EAAE,YAAY;IAC7C,CAAC,0BAAkB,CAAC,cAAc,CAAC,EAAE,gBAAgB;IACrD,CAAC,0BAAkB,CAAC,gBAAgB,CAAC,EAAE,kBAAkB;IACzD,CAAC,0BAAkB,CAAC,kBAAkB,CAAC,EAAE,oBAAoB;IAC7D,CAAC,0BAAkB,CAAC,gBAAgB,CAAC,EAAE,oCAAoC;IAC3E,CAAC,0BAAkB,CAAC,eAAe,CAAC,EAAE,iBAAiB;IACvD,CAAC,0BAAkB,CAAC,gBAAgB,CAAC,EAAE,kBAAkB;IACzD,CAAC,0BAAkB,CAAC,eAAe,CAAC,EAAE,iBAAiB;IACvD,CAAC,0BAAkB,CAAC,cAAc,CAAC,EAAE,gBAAgB;IACrD,CAAC,0BAAkB,CAAC,oBAAoB,CAAC,EAAE,sBAAsB;CAClE,CAAC;AAEF;;;GAGG;AACH,SAAgB,uBAAuB,CAAC,IAAY;IAClD,OAAO,sCAA8B,CAAC,IAA+B,CAAC,IAAI,SAAS,CAAC;AACtF,CAAC;AAED;;;GAGG;AACH,SAAgB,4BAA4B,CAAC,UAAkB;IAC7D,QAAQ,UAAU,EAAE,CAAC;QACnB,KAAK,CAAC;YACJ,OAAO,YAAY,CAAC;QACtB,KAAK,CAAC;YACJ,OAAO,MAAM,CAAC;QAChB,KAAK,CAAC;YACJ,OAAO,SAAS,CAAC;QACnB,KAAK,CAAC;YACJ,OAAO,QAAQ,CAAC;QAClB;YACE,OAAO,WAAW,UAAU,GAAG,CAAC;IACpC,CAAC;AACH,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 Local Browser Server extension is installed.\n * @beta\n */\nexport const EXTENSION_INSTALLED_FILENAME: string =\n '.playwright-local-browser-server-extension-installed.txt';\n\n/**\n * Helper to determine if the Playwright Local Browser Server extension is installed. This checks 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-local-browser-server-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\n/**\n * WebSocket close codes as defined by RFC 6455.\n * @see {@link https://datatracker.ietf.org/doc/html/rfc6455#section-11.7}\n * @beta\n */\nexport const WebSocketCloseCode: {\n /** Normal closure; the connection successfully completed. */\n readonly NORMAL_CLOSURE: 1000;\n /** Endpoint is going away (e.g., server shutting down, browser navigating away). */\n readonly GOING_AWAY: 1001;\n /** Protocol error encountered. */\n readonly PROTOCOL_ERROR: 1002;\n /** Received data type that cannot be accepted (e.g., text-only endpoint received binary). */\n readonly UNSUPPORTED_DATA: 1003;\n /** No status code was provided even though one was expected. */\n readonly NO_STATUS_RECEIVED: 1005;\n /** Connection was closed abnormally (e.g., without sending a close frame). */\n readonly ABNORMAL_CLOSURE: 1006;\n /** Received message data inconsistent with the message type. */\n readonly INVALID_PAYLOAD: 1007;\n /** Received a message that violates policy. */\n readonly POLICY_VIOLATION: 1008;\n /** Received a message that is too big to process. */\n readonly MESSAGE_TOO_BIG: 1009;\n /** Server encountered an unexpected condition that prevented it from fulfilling the request. */\n readonly INTERNAL_ERROR: 1011;\n /** Connection was closed due to TLS handshake failure. */\n readonly TLS_HANDSHAKE_FAILED: 1015;\n} = {\n NORMAL_CLOSURE: 1000,\n GOING_AWAY: 1001,\n PROTOCOL_ERROR: 1002,\n UNSUPPORTED_DATA: 1003,\n NO_STATUS_RECEIVED: 1005,\n ABNORMAL_CLOSURE: 1006,\n INVALID_PAYLOAD: 1007,\n POLICY_VIOLATION: 1008,\n MESSAGE_TOO_BIG: 1009,\n INTERNAL_ERROR: 1011,\n TLS_HANDSHAKE_FAILED: 1015\n};\n\n/**\n * Type for WebSocket close code values.\n * @beta\n */\nexport type WebSocketCloseCodeValue = (typeof WebSocketCloseCode)[keyof typeof WebSocketCloseCode];\n\n/**\n * Human-readable descriptions for WebSocket close codes.\n * @beta\n */\nexport const WebSocketCloseCodeDescriptions: Record<WebSocketCloseCodeValue, string> = {\n [WebSocketCloseCode.NORMAL_CLOSURE]: 'Normal Closure',\n [WebSocketCloseCode.GOING_AWAY]: 'Going Away',\n [WebSocketCloseCode.PROTOCOL_ERROR]: 'Protocol Error',\n [WebSocketCloseCode.UNSUPPORTED_DATA]: 'Unsupported Data',\n [WebSocketCloseCode.NO_STATUS_RECEIVED]: 'No Status Received',\n [WebSocketCloseCode.ABNORMAL_CLOSURE]: 'Abnormal Closure (connection lost)',\n [WebSocketCloseCode.INVALID_PAYLOAD]: 'Invalid Payload',\n [WebSocketCloseCode.POLICY_VIOLATION]: 'Policy Violation',\n [WebSocketCloseCode.MESSAGE_TOO_BIG]: 'Message Too Big',\n [WebSocketCloseCode.INTERNAL_ERROR]: 'Internal Error',\n [WebSocketCloseCode.TLS_HANDSHAKE_FAILED]: 'TLS Handshake Failed'\n};\n\n/**\n * Returns a human-readable description for a WebSocket close code.\n * @beta\n */\nexport function getWebSocketCloseReason(code: number): string {\n return WebSocketCloseCodeDescriptions[code as WebSocketCloseCodeValue] || 'Unknown';\n}\n\n/**\n * Returns a human-readable string for a WebSocket ready state.\n * @beta\n */\nexport function getWebSocketReadyStateString(readyState: number): string {\n switch (readyState) {\n case 0:\n return 'CONNECTING';\n case 1:\n return 'OPEN';\n case 2:\n return 'CLOSING';\n case 3:\n return 'CLOSED';\n default:\n return `UNKNOWN(${readyState})`;\n }\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rushstack/playwright-browser-tunnel",
3
- "version": "0.2.0",
3
+ "version": "0.2.2",
4
4
  "description": "Run a remote Playwright Browser Tunnel. Useful in remote development environments.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -20,8 +20,8 @@
20
20
  "ws": "~8.14.1",
21
21
  "playwright": "1.56.1",
22
22
  "@rushstack/node-core-library": "5.19.1",
23
- "@rushstack/ts-command-line": "5.1.7",
24
- "@rushstack/terminal": "0.21.0"
23
+ "@rushstack/terminal": "0.21.0",
24
+ "@rushstack/ts-command-line": "5.2.0"
25
25
  },
26
26
  "devDependencies": {
27
27
  "eslint": "~9.37.0",
@@ -30,8 +30,8 @@
30
30
  "playwright-core": "~1.56.1",
31
31
  "@playwright/test": "~1.56.1",
32
32
  "@types/node": "20.17.19",
33
- "local-node-rig": "1.0.0",
34
- "@rushstack/heft": "1.1.11"
33
+ "@rushstack/heft": "1.1.13",
34
+ "local-node-rig": "1.0.0"
35
35
  },
36
36
  "peerDependencies": {
37
37
  "playwright-core": "~1.56.1"
@@ -1,3 +1,3 @@
1
1
  Caching build output folders: dist, lib, temp, .rush/temp/operation/_phase_build
2
2
  Successfully set cache entry.
3
- Cache key: rushstack+playwright-browser-tunnel-_phase_build-1937f4c0911bcb65c94b2b4decfde58532afb468
3
+ Cache key: rushstack+playwright-browser-tunnel-_phase_build-3f67426e34dc3685ce06e290853fe1361c9ed401
@@ -2,7 +2,7 @@ Invoking: heft run --only build -- --clean --production
2
2
  ---- build started ----
3
3
  [build:typescript] Using TypeScript version 5.8.2
4
4
  [build:lint] Using ESLint version 9.37.0
5
- [build:api-extractor] Using API Extractor version 7.56.0
5
+ [build:api-extractor] Using API Extractor version 7.56.1
6
6
  [build:api-extractor] Analysis will use the bundled TypeScript version 5.8.2
7
- ---- build finished (21.387s) ----
8
- -------------------- Finished (21.404s) --------------------
7
+ ---- build finished (21.890s) ----
8
+ -------------------- Finished (21.899s) --------------------
@@ -11,7 +11,12 @@ import semver from 'semver';
11
11
  import { TerminalProviderSeverity, TerminalStreamWritable, type ITerminal } from '@rushstack/terminal';
12
12
  import { Executable, FileSystem, Async } from '@rushstack/node-core-library';
13
13
 
14
- import { getNormalizedErrorString } from './utilities';
14
+ import {
15
+ getNormalizedErrorString,
16
+ getWebSocketCloseReason,
17
+ getWebSocketReadyStateString,
18
+ WebSocketCloseCode
19
+ } from './utilities';
15
20
  import { LaunchOptionsValidator, type ILaunchOptionsValidationResult } from './LaunchOptionsValidator';
16
21
 
17
22
  /**
@@ -170,7 +175,7 @@ export class PlaywrightTunnel {
170
175
  this._pollInterval = undefined;
171
176
  }
172
177
  await this._initWsPromise?.finally(() => {
173
- this._ws?.close();
178
+ this._ws?.close(WebSocketCloseCode.NORMAL_CLOSURE, 'Tunnel stopped');
174
179
  });
175
180
  }
176
181
 
@@ -448,37 +453,68 @@ export class PlaywrightTunnel {
448
453
  // ws1 is the tunnel websocket, ws2 is the browser server websocket
449
454
  private async _setupForwardingAsync(ws1: WebSocket, ws2: WebSocket): Promise<void> {
450
455
  this._terminal.writeLine('Setting up message forwarding between ws1 and ws2');
456
+ this._terminal.writeLine(` ws1 (tunnel) readyState: ${getWebSocketReadyStateString(ws1.readyState)}`);
457
+ this._terminal.writeLine(` ws2 (browser) readyState: ${getWebSocketReadyStateString(ws2.readyState)}`);
458
+
459
+ const messageCount: { ws1ToWs2: number; ws2ToWs1: number } = { ws1ToWs2: 0, ws2ToWs1: 0 };
460
+
451
461
  ws1.on('message', (data) => {
462
+ messageCount.ws1ToWs2++;
452
463
  if (ws2.readyState === WebSocket.OPEN) {
453
464
  ws2.send(data);
454
465
  } else {
455
- this._terminal.writeLine('ws2 is not open. Dropping message.');
466
+ this._terminal.writeLine(
467
+ `ws2 not open (state: ${getWebSocketReadyStateString(ws2.readyState)}). Dropping message #${messageCount.ws1ToWs2}`
468
+ );
456
469
  }
457
470
  });
458
471
  ws2.on('message', (data) => {
472
+ messageCount.ws2ToWs1++;
459
473
  if (ws1.readyState === WebSocket.OPEN) {
460
474
  ws1.send(data);
461
475
  } else {
462
- this._terminal.writeLine('ws1 is not open. Dropping message.');
476
+ this._terminal.writeLine(
477
+ `ws1 not open (state: ${getWebSocketReadyStateString(ws1.readyState)}). Dropping message #${messageCount.ws2ToWs1}`
478
+ );
463
479
  }
464
480
  });
465
481
 
466
- ws1.once('close', () => {
482
+ ws1.once('close', (code: number, reason: Buffer) => {
483
+ const reasonStr: string = reason.toString() || 'no reason provided';
484
+ const codeDescription: string = getWebSocketCloseReason(code);
485
+ this._terminal.writeLine(
486
+ `ws1 (tunnel) closed - code: ${code} (${codeDescription}), reason: ${reasonStr}`
487
+ );
488
+ this._terminal.writeLine(
489
+ ` Messages forwarded: ws1->ws2: ${messageCount.ws1ToWs2}, ws2->ws1: ${messageCount.ws2ToWs1}`
490
+ );
467
491
  if (ws2.readyState === WebSocket.OPEN) {
468
- ws2.close();
492
+ this._terminal.writeLine(' Closing ws2 (browser) in response');
493
+ ws2.close(WebSocketCloseCode.NORMAL_CLOSURE, 'Tunnel closed');
469
494
  }
470
495
  });
471
- ws2.once('close', () => {
496
+ ws2.once('close', (code: number, reason: Buffer) => {
497
+ const reasonStr: string = reason.toString() || 'no reason provided';
498
+ const codeDescription: string = getWebSocketCloseReason(code);
499
+ this._terminal.writeLine(
500
+ `ws2 (browser) closed - code: ${code} (${codeDescription}), reason: ${reasonStr}`
501
+ );
502
+ this._terminal.writeLine(
503
+ ` Messages forwarded: ws1->ws2: ${messageCount.ws1ToWs2}, ws2->ws1: ${messageCount.ws2ToWs1}`
504
+ );
472
505
  if (ws1.readyState === WebSocket.OPEN) {
473
- ws1.close();
506
+ this._terminal.writeLine(' Closing ws1 (tunnel) in response');
507
+ ws1.close(WebSocketCloseCode.NORMAL_CLOSURE, 'Browser closed');
474
508
  }
475
509
  });
476
510
 
477
511
  ws1.once('error', (error) => {
478
- this._terminal.writeLine(`WebSocket error: ${getNormalizedErrorString(error)}`);
512
+ this._terminal.writeErrorLine(`ws1 (tunnel) WebSocket error: ${getNormalizedErrorString(error)}`);
513
+ this._terminal.writeErrorLine(` ws1 readyState: ${getWebSocketReadyStateString(ws1.readyState)}`);
479
514
  });
480
515
  ws2.once('error', (error) => {
481
- this._terminal.writeLine(`WebSocket error: ${getNormalizedErrorString(error)}`);
516
+ this._terminal.writeErrorLine(`ws2 (browser) WebSocket error: ${getNormalizedErrorString(error)}`);
517
+ this._terminal.writeErrorLine(` ws2 readyState: ${getWebSocketReadyStateString(ws2.readyState)}`);
482
518
  });
483
519
  }
484
520
 
@@ -507,11 +543,21 @@ export class PlaywrightTunnel {
507
543
  this._terminal.writeLine(`WebSocket error occurred: ${getNormalizedErrorString(error)}`);
508
544
  });
509
545
 
510
- ws.on('close', async () => {
546
+ ws.on('close', async (code: number, reason: Buffer) => {
547
+ const reasonStr: string = reason.toString() || 'no reason provided';
548
+ const codeDescription: string = getWebSocketCloseReason(code);
511
549
  this._initWsPromise = undefined;
512
550
  this.status = 'stopped';
513
- this._terminal.writeLine('WebSocket connection closed');
514
- await browserServer?.close();
551
+ this._terminal.writeLine(
552
+ `WebSocket connection closed - code: ${code} (${codeDescription}), reason: ${reasonStr}`
553
+ );
554
+ this._terminal.writeLine(` handshake received: ${handshake !== undefined}`);
555
+ this._terminal.writeLine(` browserServer active: ${browserServer !== undefined}`);
556
+ if (browserServer) {
557
+ this._terminal.writeLine(' Closing browser server...');
558
+ await browserServer.close();
559
+ this._terminal.writeLine(' Browser server closed');
560
+ }
515
561
  });
516
562
 
517
563
  return await new Promise<WebSocket>((resolve, reject) => {
@@ -531,7 +577,7 @@ export class PlaywrightTunnel {
531
577
  if (!shouldProceed) {
532
578
  terminal.writeLine('Browser server launch cancelled by user.');
533
579
  ws.off('message', onMessageHandler);
534
- ws.close();
580
+ ws.close(WebSocketCloseCode.NORMAL_CLOSURE, 'Launch cancelled by user');
535
581
  reject(new Error('Browser server launch cancelled by user'));
536
582
  return;
537
583
  }
@@ -544,6 +590,20 @@ export class PlaywrightTunnel {
544
590
  client = browserServerProxy.client;
545
591
  browserServer = browserServerProxy.browserServer;
546
592
 
593
+ // Monitor browser server process for crashes
594
+ const browserProcess: ChildProcess | null = browserServer.process();
595
+ if (browserProcess) {
596
+ browserProcess.on('exit', (code: number | null, signal: string | null) => {
597
+ terminal.writeErrorLine(`Browser server process exited - code: ${code}, signal: ${signal}`);
598
+ });
599
+ browserProcess.on('error', (err: Error) => {
600
+ terminal.writeErrorLine(`Browser server process error: ${getNormalizedErrorString(err)}`);
601
+ });
602
+ terminal.writeDebugLine(`Browser server process started with PID: ${browserProcess.pid}`);
603
+ } else {
604
+ terminal.writeDebugLine('Warning: Browser server process handle not available for monitoring');
605
+ }
606
+
547
607
  this.status = 'browser-server-running';
548
608
 
549
609
  // Send ack so that the counterpart also knows to start forwarding messages.
@@ -571,7 +631,7 @@ export class PlaywrightTunnel {
571
631
 
572
632
  // Cleanup and close connection on error
573
633
  ws.off('message', onMessageHandler);
574
- ws.close();
634
+ ws.close(WebSocketCloseCode.INTERNAL_ERROR, 'Handshake error');
575
635
  reject(error);
576
636
  return;
577
637
  }
@@ -579,7 +639,7 @@ export class PlaywrightTunnel {
579
639
  if (!client) {
580
640
  terminal.writeLine('Browser WebSocket client is not initialized.');
581
641
  ws.off('message', onMessageHandler);
582
- ws.close();
642
+ ws.close(WebSocketCloseCode.INTERNAL_ERROR, 'Browser client not initialized');
583
643
  return;
584
644
  }
585
645
  }
@@ -10,6 +10,12 @@ import { type ITerminal, Terminal, ConsoleTerminalProvider } from '@rushstack/te
10
10
 
11
11
  import type { BrowserName } from './PlaywrightBrowserTunnel';
12
12
  import { HttpServer } from './HttpServer';
13
+ import {
14
+ getNormalizedErrorString,
15
+ getWebSocketCloseReason,
16
+ getWebSocketReadyStateString,
17
+ WebSocketCloseCode
18
+ } from './utilities';
13
19
 
14
20
  const { version: playwrightVersion } = playwrightPackageJson;
15
21
 
@@ -121,12 +127,12 @@ export async function tunneledBrowserConnection(
121
127
  logger.writeLine('Received handshakeAck from remote');
122
128
  } else {
123
129
  logger.writeErrorLine('Invalid handshake ack message');
124
- ws.close();
130
+ ws.close(WebSocketCloseCode.PROTOCOL_ERROR, 'Invalid handshake ack');
125
131
  return;
126
132
  }
127
133
  } catch (e) {
128
134
  logger.writeErrorLine(`Failed parsing handshake ack: ${e}`);
129
- ws.close();
135
+ ws.close(WebSocketCloseCode.PROTOCOL_ERROR, 'Failed parsing handshake');
130
136
  return;
131
137
  }
132
138
  // Resolve only once local proxy available and handshake acknowledged
@@ -153,8 +159,21 @@ export async function tunneledBrowserConnection(
153
159
  }
154
160
  });
155
161
 
156
- ws.on('close', () => logger.writeLine('Remote websocket closed'));
157
- ws.on('error', (err) => logger.writeErrorLine(`Remote websocket error: ${err}`));
162
+ ws.on('close', (code: number, reason: Buffer) => {
163
+ const reasonStr: string = reason.toString() || 'no reason provided';
164
+ const codeDescription: string = getWebSocketCloseReason(code);
165
+ logger.writeDebugLine(
166
+ `Remote websocket closed - code: ${code} (${codeDescription}), reason: ${reasonStr}`
167
+ );
168
+ logger.writeDebugLine(
169
+ ` Connection state at close: handshakeSent=${handshakeSent}, handshakeAck=${handshakeAck}`
170
+ );
171
+ logger.writeDebugLine(` Buffered messages pending: ${bufferedLocalMessages.length}`);
172
+ });
173
+ ws.on('error', (err: Error) => {
174
+ logger.writeErrorLine(`Remote websocket error: ${getNormalizedErrorString(err)}`);
175
+ logger.writeErrorLine(` Socket readyState: ${getWebSocketReadyStateString(ws.readyState)}`);
176
+ });
158
177
  });
159
178
 
160
179
  localProxyWs.on('connection', (localWs, request) => {
@@ -183,7 +202,7 @@ export async function tunneledBrowserConnection(
183
202
  if (!browserName) {
184
203
  const supportedBrowsersString: string = Array.from(SUPPORTED_BROWSER_NAMES).join('|');
185
204
  logger.writeErrorLine(`browser query param required (${supportedBrowsersString})`);
186
- localWs.close();
205
+ localWs.close(WebSocketCloseCode.PROTOCOL_ERROR, 'Missing browser param');
187
206
  return;
188
207
  }
189
208
  if (!launchOptions) {
@@ -200,8 +219,20 @@ export async function tunneledBrowserConnection(
200
219
  bufferedLocalMessages.push(message);
201
220
  }
202
221
  });
203
- localWs.on('close', () => logger.writeLine('Local client websocket closed'));
204
- localWs.on('error', (err) => logger.writeErrorLine(`Local client websocket error: ${err}`));
222
+ localWs.on('close', (code: number, reason: Buffer) => {
223
+ const reasonStr: string = reason.toString() || 'no reason provided';
224
+ const codeDescription: string = getWebSocketCloseReason(code);
225
+ logger.writeDebugLine(
226
+ `Local client websocket closed - code: ${code} (${codeDescription}), reason: ${reasonStr}`
227
+ );
228
+ logger.writeDebugLine(
229
+ ` Remote socket state: ${remoteSocket ? getWebSocketReadyStateString(remoteSocket.readyState) : 'undefined'}`
230
+ );
231
+ logger.writeDebugLine(` handshakeAck: ${handshakeAck}`);
232
+ });
233
+ localWs.on('error', (err: Error) => {
234
+ logger.writeErrorLine(`Local client websocket error: ${getNormalizedErrorString(err)}`);
235
+ });
205
236
  });
206
237
 
207
238
  // Resolve immediately so caller can initiate local connection with query params (handshake completes later)
package/src/utilities.ts CHANGED
@@ -41,3 +41,96 @@ export function getNormalizedErrorString(error: unknown): string {
41
41
  }
42
42
  return String(error);
43
43
  }
44
+
45
+ /**
46
+ * WebSocket close codes as defined by RFC 6455.
47
+ * @see {@link https://datatracker.ietf.org/doc/html/rfc6455#section-11.7}
48
+ * @beta
49
+ */
50
+ export const WebSocketCloseCode: {
51
+ /** Normal closure; the connection successfully completed. */
52
+ readonly NORMAL_CLOSURE: 1000;
53
+ /** Endpoint is going away (e.g., server shutting down, browser navigating away). */
54
+ readonly GOING_AWAY: 1001;
55
+ /** Protocol error encountered. */
56
+ readonly PROTOCOL_ERROR: 1002;
57
+ /** Received data type that cannot be accepted (e.g., text-only endpoint received binary). */
58
+ readonly UNSUPPORTED_DATA: 1003;
59
+ /** No status code was provided even though one was expected. */
60
+ readonly NO_STATUS_RECEIVED: 1005;
61
+ /** Connection was closed abnormally (e.g., without sending a close frame). */
62
+ readonly ABNORMAL_CLOSURE: 1006;
63
+ /** Received message data inconsistent with the message type. */
64
+ readonly INVALID_PAYLOAD: 1007;
65
+ /** Received a message that violates policy. */
66
+ readonly POLICY_VIOLATION: 1008;
67
+ /** Received a message that is too big to process. */
68
+ readonly MESSAGE_TOO_BIG: 1009;
69
+ /** Server encountered an unexpected condition that prevented it from fulfilling the request. */
70
+ readonly INTERNAL_ERROR: 1011;
71
+ /** Connection was closed due to TLS handshake failure. */
72
+ readonly TLS_HANDSHAKE_FAILED: 1015;
73
+ } = {
74
+ NORMAL_CLOSURE: 1000,
75
+ GOING_AWAY: 1001,
76
+ PROTOCOL_ERROR: 1002,
77
+ UNSUPPORTED_DATA: 1003,
78
+ NO_STATUS_RECEIVED: 1005,
79
+ ABNORMAL_CLOSURE: 1006,
80
+ INVALID_PAYLOAD: 1007,
81
+ POLICY_VIOLATION: 1008,
82
+ MESSAGE_TOO_BIG: 1009,
83
+ INTERNAL_ERROR: 1011,
84
+ TLS_HANDSHAKE_FAILED: 1015
85
+ };
86
+
87
+ /**
88
+ * Type for WebSocket close code values.
89
+ * @beta
90
+ */
91
+ export type WebSocketCloseCodeValue = (typeof WebSocketCloseCode)[keyof typeof WebSocketCloseCode];
92
+
93
+ /**
94
+ * Human-readable descriptions for WebSocket close codes.
95
+ * @beta
96
+ */
97
+ export const WebSocketCloseCodeDescriptions: Record<WebSocketCloseCodeValue, string> = {
98
+ [WebSocketCloseCode.NORMAL_CLOSURE]: 'Normal Closure',
99
+ [WebSocketCloseCode.GOING_AWAY]: 'Going Away',
100
+ [WebSocketCloseCode.PROTOCOL_ERROR]: 'Protocol Error',
101
+ [WebSocketCloseCode.UNSUPPORTED_DATA]: 'Unsupported Data',
102
+ [WebSocketCloseCode.NO_STATUS_RECEIVED]: 'No Status Received',
103
+ [WebSocketCloseCode.ABNORMAL_CLOSURE]: 'Abnormal Closure (connection lost)',
104
+ [WebSocketCloseCode.INVALID_PAYLOAD]: 'Invalid Payload',
105
+ [WebSocketCloseCode.POLICY_VIOLATION]: 'Policy Violation',
106
+ [WebSocketCloseCode.MESSAGE_TOO_BIG]: 'Message Too Big',
107
+ [WebSocketCloseCode.INTERNAL_ERROR]: 'Internal Error',
108
+ [WebSocketCloseCode.TLS_HANDSHAKE_FAILED]: 'TLS Handshake Failed'
109
+ };
110
+
111
+ /**
112
+ * Returns a human-readable description for a WebSocket close code.
113
+ * @beta
114
+ */
115
+ export function getWebSocketCloseReason(code: number): string {
116
+ return WebSocketCloseCodeDescriptions[code as WebSocketCloseCodeValue] || 'Unknown';
117
+ }
118
+
119
+ /**
120
+ * Returns a human-readable string for a WebSocket ready state.
121
+ * @beta
122
+ */
123
+ export function getWebSocketReadyStateString(readyState: number): string {
124
+ switch (readyState) {
125
+ case 0:
126
+ return 'CONNECTING';
127
+ case 1:
128
+ return 'OPEN';
129
+ case 2:
130
+ return 'CLOSING';
131
+ case 3:
132
+ return 'CLOSED';
133
+ default:
134
+ return `UNKNOWN(${readyState})`;
135
+ }
136
+ }