@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.
Files changed (55) hide show
  1. package/.rush/temp/chunked-rush-logs/playwright-browser-tunnel._phase_build.chunks.jsonl +8 -0
  2. package/.rush/temp/operation/_phase_build/all.log +8 -0
  3. package/.rush/temp/operation/_phase_build/log-chunks.jsonl +8 -0
  4. package/.rush/temp/operation/_phase_build/state.json +3 -0
  5. package/.rush/temp/rushstack+playwright-browser-tunnel-_phase_build-83a085f61cdc0a4bb81c20aa939830fc5b5cff2f.tar.log +42 -0
  6. package/.rush/temp/shrinkwrap-deps.json +104 -0
  7. package/CHANGELOG.json +17 -0
  8. package/CHANGELOG.md +11 -0
  9. package/README.md +128 -0
  10. package/config/api-extractor.json +19 -0
  11. package/config/rig.json +7 -0
  12. package/dist/playwright-browser-tunnel.d.ts +276 -0
  13. package/eslint.config.js +18 -0
  14. package/lib/HttpServer.d.ts +20 -0
  15. package/lib/HttpServer.d.ts.map +1 -0
  16. package/lib/HttpServer.js +77 -0
  17. package/lib/HttpServer.js.map +1 -0
  18. package/lib/LaunchOptionsValidator.d.ts +93 -0
  19. package/lib/LaunchOptionsValidator.d.ts.map +1 -0
  20. package/lib/LaunchOptionsValidator.js +163 -0
  21. package/lib/LaunchOptionsValidator.js.map +1 -0
  22. package/lib/PlaywrightBrowserTunnel.d.ts +92 -0
  23. package/lib/PlaywrightBrowserTunnel.d.ts.map +1 -0
  24. package/lib/PlaywrightBrowserTunnel.js +468 -0
  25. package/lib/PlaywrightBrowserTunnel.js.map +1 -0
  26. package/lib/index.d.ts +23 -0
  27. package/lib/index.d.ts.map +1 -0
  28. package/lib/index.js +33 -0
  29. package/lib/index.js.map +1 -0
  30. package/lib/tsdoc-metadata.json +11 -0
  31. package/lib/tunneledBrowserConnection.d.ts +48 -0
  32. package/lib/tunneledBrowserConnection.d.ts.map +1 -0
  33. package/lib/tunneledBrowserConnection.js +216 -0
  34. package/lib/tunneledBrowserConnection.js.map +1 -0
  35. package/lib/utilities.d.ts +17 -0
  36. package/lib/utilities.d.ts.map +1 -0
  37. package/lib/utilities.js +41 -0
  38. package/lib/utilities.js.map +1 -0
  39. package/package.json +44 -0
  40. package/playwright.config.ts +45 -0
  41. package/rush-logs/playwright-browser-tunnel._phase_build.cache.log +3 -0
  42. package/rush-logs/playwright-browser-tunnel._phase_build.log +8 -0
  43. package/src/HttpServer.ts +87 -0
  44. package/src/LaunchOptionsValidator.ts +234 -0
  45. package/src/PlaywrightBrowserTunnel.ts +590 -0
  46. package/src/index.ts +38 -0
  47. package/src/tunneledBrowserConnection.ts +284 -0
  48. package/src/utilities.ts +42 -0
  49. package/temp/build/lint/_eslint-5eVG3S6w.json +30 -0
  50. package/temp/build/lint/lint.sarif +233 -0
  51. package/temp/build/typescript/ts_l9Fw4VUO.json +1 -0
  52. package/temp/playwright-browser-tunnel.api.md +120 -0
  53. package/tests/demo.spec.ts +10 -0
  54. package/tests/testFixture.ts +22 -0
  55. package/tsconfig.json +6 -0
@@ -0,0 +1,284 @@
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 playwright from 'playwright-core';
5
+ import type { Browser, LaunchOptions } from 'playwright-core';
6
+ import { WebSocketServer, WebSocket, type RawData } from 'ws';
7
+ import playwrightPackageJson from 'playwright-core/package.json';
8
+
9
+ import { type ITerminal, Terminal, ConsoleTerminalProvider } from '@rushstack/terminal';
10
+
11
+ import type { BrowserName } from './PlaywrightBrowserTunnel';
12
+ import { HttpServer } from './HttpServer';
13
+
14
+ const { version: playwrightVersion } = playwrightPackageJson;
15
+
16
+ const SUPPORTED_BROWSER_NAMES: Set<string> = new Set(['chromium', 'firefox', 'webkit']);
17
+
18
+ interface IHandshake {
19
+ action: 'handshake';
20
+ browserName: BrowserName;
21
+ launchOptions: LaunchOptions;
22
+ playwrightVersion: string;
23
+ }
24
+
25
+ interface IHandshakeAck {
26
+ action: 'handshakeAck';
27
+ }
28
+
29
+ const DEFAULT_LISTEN_PORT: number = 3000;
30
+
31
+ /**
32
+ * Disposable handle returned by {@link tunneledBrowserConnection}.
33
+ * @beta
34
+ */
35
+ export interface IDisposableTunneledBrowserConnection {
36
+ /**
37
+ * The WebSocket endpoint URL that the local Playwright client should connect to.
38
+ */
39
+ remoteEndpoint: string;
40
+ /**
41
+ * Dispose method that closes the WebSocket servers.
42
+ * Called automatically when using `using` syntax.
43
+ */
44
+ [Symbol.dispose]: () => void;
45
+ /**
46
+ * Promise that resolves when the remote WebSocket server closes.
47
+ */
48
+ closePromise: Promise<void>;
49
+ }
50
+
51
+ /**
52
+ * Creates a tunneled WebSocket endpoint that a local Playwright client can connect to.
53
+ * @beta
54
+ */
55
+ export async function tunneledBrowserConnection(
56
+ logger: ITerminal,
57
+ port: number = DEFAULT_LISTEN_PORT
58
+ ): Promise<IDisposableTunneledBrowserConnection> {
59
+ // Server that remote peer (actual browser host) connects to
60
+ const remoteWsServer: WebSocketServer = new WebSocketServer({ port });
61
+ // Local HTTP + WebSocket server where the playwright client will connect providing params
62
+ const httpServer: HttpServer = new HttpServer(logger);
63
+ await httpServer.listenAsync();
64
+ logger.writeLine(`Remote WebSocket server listening on ws://localhost:${port}`);
65
+
66
+ const localProxyWs: WebSocketServer = httpServer.wsServer;
67
+ const localProxyWsEndpoint: string = httpServer.endpoint;
68
+
69
+ let browserName: BrowserName | undefined;
70
+ let launchOptions: LaunchOptions | undefined;
71
+ let remoteSocket: WebSocket | undefined;
72
+ let handshakeAck: boolean = false;
73
+ let handshakeSent: boolean = false;
74
+
75
+ function maybeSendHandshake(): void {
76
+ if (!handshakeSent && remoteSocket && browserName && launchOptions) {
77
+ const handshake: IHandshake = {
78
+ action: 'handshake',
79
+ browserName,
80
+ launchOptions,
81
+ playwrightVersion
82
+ };
83
+ // Log handshake without 'headless' to avoid confusion (tunnel enforces headless: false)
84
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
85
+ const { headless, ...logOptions } = launchOptions;
86
+ const logHandshake: Omit<IHandshake, 'launchOptions'> & {
87
+ launchOptions: Omit<LaunchOptions, 'headless'>;
88
+ } = {
89
+ ...handshake,
90
+ launchOptions: logOptions
91
+ };
92
+ logger.writeLine(`Sending handshake to remote: ${JSON.stringify(logHandshake)}`);
93
+ handshakeSent = true;
94
+ remoteSocket.send(JSON.stringify(handshake));
95
+ }
96
+ }
97
+
98
+ return await new Promise((resolve) => {
99
+ remoteWsServer.on('error', (error) => {
100
+ logger.writeErrorLine(`Remote WebSocket server error: ${error}`);
101
+ });
102
+
103
+ remoteWsServer.on('close', () => {
104
+ logger.writeLine('Remote WebSocket server closed');
105
+ });
106
+
107
+ const bufferedLocalMessages: Array<RawData> = [];
108
+
109
+ remoteWsServer.on('connection', (ws) => {
110
+ logger.writeLine('Remote websocket connected');
111
+ remoteSocket = ws;
112
+ handshakeAck = false;
113
+ maybeSendHandshake();
114
+
115
+ ws.on('message', (message) => {
116
+ if (!handshakeAck) {
117
+ try {
118
+ const receivedHandshake: IHandshakeAck = JSON.parse(message.toString());
119
+ if (receivedHandshake.action === 'handshakeAck') {
120
+ handshakeAck = true;
121
+ logger.writeLine('Received handshakeAck from remote');
122
+ } else {
123
+ logger.writeErrorLine('Invalid handshake ack message');
124
+ ws.close();
125
+ return;
126
+ }
127
+ } catch (e) {
128
+ logger.writeErrorLine(`Failed parsing handshake ack: ${e}`);
129
+ ws.close();
130
+ return;
131
+ }
132
+ // Resolve only once local proxy available and handshake acknowledged
133
+ if (handshakeAck) {
134
+ // Flush any buffered local messages now that tunnel is active
135
+ const activeRemote: WebSocket | undefined = remoteSocket;
136
+ if (activeRemote && activeRemote.readyState === WebSocket.OPEN) {
137
+ while (bufferedLocalMessages.length > 0) {
138
+ const m: Buffer | ArrayBuffer | Buffer[] | string | undefined = bufferedLocalMessages.shift();
139
+ if (m !== undefined) {
140
+ logger.writeLine(`Flushing buffered local message to remote: ${m}`);
141
+ activeRemote.send(m);
142
+ }
143
+ }
144
+ }
145
+ }
146
+ } else {
147
+ // Forward from remote to all local clients
148
+ localProxyWs.clients.forEach((client) => {
149
+ if (client.readyState === WebSocket.OPEN) {
150
+ client.send(message);
151
+ }
152
+ });
153
+ }
154
+ });
155
+
156
+ ws.on('close', () => logger.writeLine('Remote websocket closed'));
157
+ ws.on('error', (err) => logger.writeErrorLine(`Remote websocket error: ${err}`));
158
+ });
159
+
160
+ localProxyWs.on('connection', (localWs, request) => {
161
+ try {
162
+ const urlString: string | undefined = request?.url;
163
+ if (urlString) {
164
+ const parsed: URL = new URL(urlString, 'http://localhost');
165
+ logger.writeLine(`Local client connected with query params: ${parsed.searchParams.toString()}`);
166
+ const bName: string | null = parsed.searchParams.get('browser');
167
+ if (bName && SUPPORTED_BROWSER_NAMES.has(bName)) {
168
+ browserName = bName as BrowserName;
169
+ }
170
+ const launchOptionsParam: string | null = parsed.searchParams.get('launchOptions');
171
+ if (launchOptionsParam) {
172
+ try {
173
+ launchOptions = JSON.parse(launchOptionsParam);
174
+ } catch (e) {
175
+ logger.writeErrorLine('Invalid launchOptions JSON provided');
176
+ }
177
+ }
178
+ }
179
+ } catch (e) {
180
+ logger.writeErrorLine(`Error parsing local connection query params: ${e}`);
181
+ }
182
+
183
+ if (!browserName) {
184
+ const supportedBrowsersString: string = Array.from(SUPPORTED_BROWSER_NAMES).join('|');
185
+ logger.writeErrorLine(`browser query param required (${supportedBrowsersString})`);
186
+ localWs.close();
187
+ return;
188
+ }
189
+ if (!launchOptions) {
190
+ launchOptions = {} as LaunchOptions; // default empty if not provided
191
+ }
192
+
193
+ maybeSendHandshake();
194
+
195
+ localWs.on('message', (message) => {
196
+ if (handshakeAck && remoteSocket?.readyState === WebSocket.OPEN) {
197
+ remoteSocket.send(message);
198
+ } else {
199
+ // Buffer until handshakeAck to avoid losing early protocol messages from Playwright
200
+ bufferedLocalMessages.push(message);
201
+ }
202
+ });
203
+ localWs.on('close', () => logger.writeLine('Local client websocket closed'));
204
+ localWs.on('error', (err) => logger.writeErrorLine(`Local client websocket error: ${err}`));
205
+ });
206
+
207
+ // Resolve immediately so caller can initiate local connection with query params (handshake completes later)
208
+ resolve({
209
+ remoteEndpoint: localProxyWsEndpoint,
210
+ [Symbol.dispose]() {
211
+ try {
212
+ remoteWsServer.close();
213
+ } catch {
214
+ // ignore errors during remote WebSocket server shutdown
215
+ }
216
+ try {
217
+ httpServer[Symbol.dispose]();
218
+ } catch {
219
+ // ignore errors during HTTP/WebSocket server shutdown
220
+ }
221
+ },
222
+ // eslint-disable-next-line promise/param-names
223
+ closePromise: new Promise<void>((resolve2) => {
224
+ remoteWsServer.once('close', () => {
225
+ resolve2();
226
+ });
227
+ })
228
+ });
229
+ });
230
+ }
231
+
232
+ /**
233
+ * Disposable handle returned by {@link createTunneledBrowserAsync}.
234
+ * @beta
235
+ */
236
+ export interface IDisposableTunneledBrowser {
237
+ /**
238
+ * The connected Playwright Browser instance.
239
+ */
240
+ browser: Browser;
241
+ /**
242
+ * Async dispose method that closes the browser connection.
243
+ * Called automatically when using `await using` syntax.
244
+ */
245
+ [Symbol.asyncDispose]: () => Promise<void>;
246
+ }
247
+
248
+ /**
249
+ * Creates a Playwright Browser instance connected via a tunneled WebSocket connection.
250
+ * @beta
251
+ */
252
+ export async function createTunneledBrowserAsync(
253
+ browserName: BrowserName,
254
+ launchOptions: LaunchOptions,
255
+ logger?: ITerminal,
256
+ port: number = DEFAULT_LISTEN_PORT
257
+ ): Promise<IDisposableTunneledBrowser> {
258
+ // Establish the tunnel first (remoteEndpoint here refers to local proxy endpoint for connect())
259
+
260
+ if (!logger) {
261
+ const terminalProvider: ConsoleTerminalProvider = new ConsoleTerminalProvider();
262
+ logger = new Terminal(terminalProvider);
263
+ }
264
+
265
+ const connection: IDisposableTunneledBrowserConnection = await tunneledBrowserConnection(logger, port);
266
+ const { remoteEndpoint } = connection;
267
+ // Append query params for browser and launchOptions
268
+ const urlObj: URL = new URL(remoteEndpoint);
269
+ urlObj.searchParams.set('browser', browserName);
270
+ urlObj.searchParams.set('launchOptions', JSON.stringify(launchOptions || {}));
271
+ const connectEndpoint: string = urlObj.toString();
272
+ const browser: Browser = await playwright[browserName].connect(connectEndpoint);
273
+ logger.writeLine(`Connected to remote browser at ${connectEndpoint}`);
274
+
275
+ return {
276
+ browser,
277
+ async [Symbol.asyncDispose]() {
278
+ logger.writeLine('Disposing browser');
279
+ await browser.close();
280
+ // Dispose the tunnel connection after browser is closed
281
+ connection[Symbol.dispose]();
282
+ }
283
+ };
284
+ }
@@ -0,0 +1,42 @@
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 { tmpdir } from 'node:os';
5
+
6
+ import { FileSystem } from '@rushstack/node-core-library';
7
+
8
+ /**
9
+ * The filename used to indicate that the Playwright on Codespaces extension is installed.
10
+ * @beta
11
+ */
12
+ export const EXTENSION_INSTALLED_FILENAME: string = '.playwright-codespaces-extension-installed.txt';
13
+
14
+ /**
15
+ * Helper to determine if the Playwright on Codespaces extension is installed. This check's for the
16
+ * existence of a well-known file in the OS temp directory.
17
+ * @beta
18
+ */
19
+ export async function isExtensionInstalledAsync(): Promise<boolean> {
20
+ // Read file from os.tempdir() + '/.playwright-codespaces-extension-installed'
21
+ const tempDir: string = tmpdir();
22
+
23
+ const extensionInstalledFilePath: string = `${tempDir}/${EXTENSION_INSTALLED_FILENAME}`;
24
+ const doesExist: boolean = FileSystem.exists(extensionInstalledFilePath);
25
+
26
+ // check if file exists
27
+ return doesExist;
28
+ }
29
+
30
+ /**
31
+ * Normalizes an error to a string for logging purposes.
32
+ * @beta
33
+ */
34
+ export function getNormalizedErrorString(error: unknown): string {
35
+ if (error instanceof Error) {
36
+ if (error.stack) {
37
+ return error.stack;
38
+ }
39
+ return error.message;
40
+ }
41
+ return String(error);
42
+ }
@@ -0,0 +1,30 @@
1
+ {
2
+ "cacheVersion": "9.37.0_v22.21.1",
3
+ "fileVersions": [
4
+ [
5
+ "HttpServer.ts",
6
+ "eb5d3c70d2744adfbd44448fe2fae2b0a6413b15bb338fd31af7ecade290c973_Q57tkjZtaaMkmlD1DBUjScEtgSo="
7
+ ],
8
+ [
9
+ "LaunchOptionsValidator.ts",
10
+ "36f10ef5bba6f95e0f332c1aab5a344c6e1475f8d35abb30a74143cdfd42a537_Q57tkjZtaaMkmlD1DBUjScEtgSo="
11
+ ],
12
+ [
13
+ "utilities.ts",
14
+ "8e9d962306505f79f305b1c7b85af30fc535fbe1de3fc31b795ad0f0ffa16bb0_Q57tkjZtaaMkmlD1DBUjScEtgSo="
15
+ ],
16
+ [
17
+ "PlaywrightBrowserTunnel.ts",
18
+ "c012ad7961971e4397e70333754dbc3e42cfc40a0df2bbe12cb097dcfa9cc962_Q57tkjZtaaMkmlD1DBUjScEtgSo="
19
+ ],
20
+ [
21
+ "tunneledBrowserConnection.ts",
22
+ "de7c574ca049c6265814d6f14c63f30cfea09bb73f467e1dfaa0e5555f8b5302_Q57tkjZtaaMkmlD1DBUjScEtgSo="
23
+ ],
24
+ [
25
+ "index.ts",
26
+ "24a883b07dbd0c309fa5f83653a48f3ac849200808fbe3872f34b11c99ea1668_Q57tkjZtaaMkmlD1DBUjScEtgSo="
27
+ ]
28
+ ],
29
+ "filesHash": "veFIt5t1iWa1vWcPeyJW4Q"
30
+ }
@@ -0,0 +1,233 @@
1
+ {
2
+ "version": "2.1.0",
3
+ "$schema": "http://json.schemastore.org/sarif-2.1.0-rtm.5",
4
+ "runs": [
5
+ {
6
+ "tool": {
7
+ "driver": {
8
+ "name": "ESLint",
9
+ "informationUri": "https://eslint.org",
10
+ "version": "9.37.0",
11
+ "rules": [
12
+ {
13
+ "id": "@typescript-eslint/no-explicit-any",
14
+ "helpUri": "https://typescript-eslint.io/rules/no-explicit-any",
15
+ "properties": {},
16
+ "shortDescription": {
17
+ "text": "Disallow the `any` type"
18
+ }
19
+ },
20
+ {
21
+ "id": "@typescript-eslint/naming-convention",
22
+ "helpUri": "https://typescript-eslint.io/rules/naming-convention",
23
+ "properties": {},
24
+ "shortDescription": {
25
+ "text": "Enforce naming conventions for everything across a codebase"
26
+ }
27
+ },
28
+ {
29
+ "id": "@typescript-eslint/no-unused-vars",
30
+ "helpUri": "https://typescript-eslint.io/rules/no-unused-vars",
31
+ "properties": {},
32
+ "shortDescription": {
33
+ "text": "Disallow unused variables"
34
+ }
35
+ },
36
+ {
37
+ "id": "promise/param-names",
38
+ "helpUri": "https://github.com/eslint-community/eslint-plugin-promise/blob/main/docs/rules/param-names.md",
39
+ "properties": {},
40
+ "shortDescription": {
41
+ "text": "Enforce consistent param names and ordering when creating new promises."
42
+ }
43
+ }
44
+ ]
45
+ }
46
+ },
47
+ "artifacts": [
48
+ {
49
+ "location": {
50
+ "uri": "src/HttpServer.ts"
51
+ }
52
+ },
53
+ {
54
+ "location": {
55
+ "uri": "src/LaunchOptionsValidator.ts"
56
+ }
57
+ },
58
+ {
59
+ "location": {
60
+ "uri": "src/utilities.ts"
61
+ }
62
+ },
63
+ {
64
+ "location": {
65
+ "uri": "src/PlaywrightBrowserTunnel.ts"
66
+ }
67
+ },
68
+ {
69
+ "location": {
70
+ "uri": "src/tunneledBrowserConnection.ts"
71
+ }
72
+ },
73
+ {
74
+ "location": {
75
+ "uri": "src/index.ts"
76
+ }
77
+ }
78
+ ],
79
+ "results": [
80
+ {
81
+ "level": "warning",
82
+ "message": {
83
+ "text": "Unexpected any. Specify a different type."
84
+ },
85
+ "locations": [
86
+ {
87
+ "physicalLocation": {
88
+ "artifactLocation": {
89
+ "uri": "src/LaunchOptionsValidator.ts",
90
+ "index": 1
91
+ },
92
+ "region": {
93
+ "startLine": 162,
94
+ "startColumn": 29,
95
+ "endLine": 162,
96
+ "endColumn": 32
97
+ }
98
+ }
99
+ }
100
+ ],
101
+ "ruleId": "@typescript-eslint/no-explicit-any",
102
+ "ruleIndex": 0,
103
+ "suppressions": [
104
+ {
105
+ "kind": "inSource",
106
+ "justification": ""
107
+ }
108
+ ]
109
+ },
110
+ {
111
+ "level": "warning",
112
+ "message": {
113
+ "text": "Classic Accessor name `status` must have one leading underscore(s)."
114
+ },
115
+ "locations": [
116
+ {
117
+ "physicalLocation": {
118
+ "artifactLocation": {
119
+ "uri": "src/PlaywrightBrowserTunnel.ts",
120
+ "index": 3
121
+ },
122
+ "region": {
123
+ "startLine": 136,
124
+ "startColumn": 15,
125
+ "endLine": 136,
126
+ "endColumn": 21
127
+ }
128
+ }
129
+ }
130
+ ],
131
+ "ruleId": "@typescript-eslint/naming-convention",
132
+ "ruleIndex": 1,
133
+ "suppressions": [
134
+ {
135
+ "kind": "inSource",
136
+ "justification": ""
137
+ }
138
+ ]
139
+ },
140
+ {
141
+ "level": "warning",
142
+ "message": {
143
+ "text": "'headless' is assigned a value but never used."
144
+ },
145
+ "locations": [
146
+ {
147
+ "physicalLocation": {
148
+ "artifactLocation": {
149
+ "uri": "src/PlaywrightBrowserTunnel.ts",
150
+ "index": 3
151
+ },
152
+ "region": {
153
+ "startLine": 379,
154
+ "startColumn": 13,
155
+ "endLine": 379,
156
+ "endColumn": 21
157
+ }
158
+ }
159
+ }
160
+ ],
161
+ "ruleId": "@typescript-eslint/no-unused-vars",
162
+ "ruleIndex": 2,
163
+ "suppressions": [
164
+ {
165
+ "kind": "inSource",
166
+ "justification": ""
167
+ }
168
+ ]
169
+ },
170
+ {
171
+ "level": "warning",
172
+ "message": {
173
+ "text": "'headless' is assigned a value but never used."
174
+ },
175
+ "locations": [
176
+ {
177
+ "physicalLocation": {
178
+ "artifactLocation": {
179
+ "uri": "src/tunneledBrowserConnection.ts",
180
+ "index": 4
181
+ },
182
+ "region": {
183
+ "startLine": 85,
184
+ "startColumn": 15,
185
+ "endLine": 85,
186
+ "endColumn": 23
187
+ }
188
+ }
189
+ }
190
+ ],
191
+ "ruleId": "@typescript-eslint/no-unused-vars",
192
+ "ruleIndex": 2,
193
+ "suppressions": [
194
+ {
195
+ "kind": "inSource",
196
+ "justification": ""
197
+ }
198
+ ]
199
+ },
200
+ {
201
+ "level": "error",
202
+ "message": {
203
+ "text": "Promise constructor parameters must be named to match \"^_?resolve$\""
204
+ },
205
+ "locations": [
206
+ {
207
+ "physicalLocation": {
208
+ "artifactLocation": {
209
+ "uri": "src/tunneledBrowserConnection.ts",
210
+ "index": 4
211
+ },
212
+ "region": {
213
+ "startLine": 223,
214
+ "startColumn": 40,
215
+ "endLine": 223,
216
+ "endColumn": 48
217
+ }
218
+ }
219
+ }
220
+ ],
221
+ "ruleId": "promise/param-names",
222
+ "ruleIndex": 3,
223
+ "suppressions": [
224
+ {
225
+ "kind": "inSource",
226
+ "justification": ""
227
+ }
228
+ ]
229
+ }
230
+ ]
231
+ }
232
+ ]
233
+ }