diff --git a/new-docs/puppeteer.productlauncher.connect.md b/new-docs/puppeteer.productlauncher.connect.md deleted file mode 100644 index 6837409e..00000000 --- a/new-docs/puppeteer.productlauncher.connect.md +++ /dev/null @@ -1,22 +0,0 @@ - - -[Home](./index.md) > [puppeteer](./puppeteer.md) > [ProductLauncher](./puppeteer.productlauncher.md) > [connect](./puppeteer.productlauncher.connect.md) - -## ProductLauncher.connect() method - -Signature: - -```typescript -connect(object: any): any; -``` - -## Parameters - -| Parameter | Type | Description | -| --- | --- | --- | -| object | any | | - -Returns: - -any - diff --git a/new-docs/puppeteer.productlauncher.md b/new-docs/puppeteer.productlauncher.md index 4f3dc6fa..8f1595fc 100644 --- a/new-docs/puppeteer.productlauncher.md +++ b/new-docs/puppeteer.productlauncher.md @@ -17,13 +17,12 @@ export interface ProductLauncher | Property | Type | Description | | --- | --- | --- | | [executablePath](./puppeteer.productlauncher.executablepath.md) | () => string | | -| [product](./puppeteer.productlauncher.product.md) | string | | +| [product](./puppeteer.productlauncher.product.md) | [Product](./puppeteer.product.md) | | ## Methods | Method | Description | | --- | --- | -| [connect(object)](./puppeteer.productlauncher.connect.md) | | | [defaultArgs(object)](./puppeteer.productlauncher.defaultargs.md) | | | [launch(object)](./puppeteer.productlauncher.launch.md) | | diff --git a/new-docs/puppeteer.productlauncher.product.md b/new-docs/puppeteer.productlauncher.product.md index 6d906bad..a1d63d3c 100644 --- a/new-docs/puppeteer.productlauncher.product.md +++ b/new-docs/puppeteer.productlauncher.product.md @@ -7,5 +7,5 @@ Signature: ```typescript -product: string; +product: Product; ``` diff --git a/new-docs/puppeteer.puppeteer.connect.md b/new-docs/puppeteer.puppeteer.connect.md index 7e99b608..26512d0d 100644 --- a/new-docs/puppeteer.puppeteer.connect.md +++ b/new-docs/puppeteer.puppeteer.connect.md @@ -13,7 +13,7 @@ connect(options: BrowserOptions & { browserWSEndpoint?: string; browserURL?: string; transport?: ConnectionTransport; - product?: string; + product?: Product; }): Promise; ``` @@ -21,7 +21,7 @@ connect(options: BrowserOptions & { | Parameter | Type | Description | | --- | --- | --- | -| options | [BrowserOptions](./puppeteer.browseroptions.md) & { browserWSEndpoint?: string; browserURL?: string; transport?: ConnectionTransport; product?: string; } | Set of configurable options to set on the browser. | +| options | [BrowserOptions](./puppeteer.browseroptions.md) & { browserWSEndpoint?: string; browserURL?: string; transport?: ConnectionTransport; product?: [Product](./puppeteer.product.md); } | Set of configurable options to set on the browser. | Returns: diff --git a/package.json b/package.json index 6aa78977..5448ba19 100644 --- a/package.json +++ b/package.json @@ -50,6 +50,7 @@ "devtools-protocol": "0.0.809251", "extract-zip": "^2.0.0", "https-proxy-agent": "^4.0.0", + "node-fetch": "^2.6.1", "pkg-dir": "^4.2.0", "progress": "^2.0.1", "proxy-from-env": "^1.0.0", diff --git a/src/api-docs-entry.ts b/src/api-docs-entry.ts index 848a3436..c56819b2 100644 --- a/src/api-docs-entry.ts +++ b/src/api-docs-entry.ts @@ -41,9 +41,11 @@ export * from './common/FileChooser.js'; export * from './common/FrameManager.js'; export * from './common/Input.js'; export * from './common/Page.js'; +export * from './common/Product.js'; export * from './common/Puppeteer.js'; -export * from './node/LaunchOptions.js'; +export * from './common/BrowserConnector.js'; export * from './node/Launcher.js'; +export * from './node/LaunchOptions.js'; export * from './common/HTTPRequest.js'; export * from './common/HTTPResponse.js'; export * from './common/SecurityDetails.js'; diff --git a/src/common/BrowserConnector.ts b/src/common/BrowserConnector.ts new file mode 100644 index 00000000..48ec8a9c --- /dev/null +++ b/src/common/BrowserConnector.ts @@ -0,0 +1,109 @@ +/** + * Copyright 2020 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { ConnectionTransport } from './ConnectionTransport.js'; +import { Browser } from './Browser.js'; +import { assert } from './assert.js'; +import { debugError } from '../common/helper.js'; +import { Connection } from './Connection.js'; +import { WebSocketTransport } from './WebSocketTransport.js'; +import { getFetch } from './fetch.js'; +import { Viewport } from './PuppeteerViewport.js'; + +/** + * Generic browser options that can be passed when launching any browser. + * @public + */ +export interface BrowserOptions { + ignoreHTTPSErrors?: boolean; + defaultViewport?: Viewport; + slowMo?: number; +} + +/** + * Users should never call this directly; it's called when calling + * `puppeteer.connect`. + * @internal + */ +export const connectToBrowser = async ( + options: BrowserOptions & { + browserWSEndpoint?: string; + browserURL?: string; + transport?: ConnectionTransport; + } +) => { + const { + browserWSEndpoint, + browserURL, + ignoreHTTPSErrors = false, + defaultViewport = { width: 800, height: 600 }, + transport, + slowMo = 0, + } = options; + + assert( + Number(!!browserWSEndpoint) + Number(!!browserURL) + Number(!!transport) === + 1, + 'Exactly one of browserWSEndpoint, browserURL or transport must be passed to puppeteer.connect' + ); + + let connection = null; + if (transport) { + connection = new Connection('', transport, slowMo); + } else if (browserWSEndpoint) { + const connectionTransport = await WebSocketTransport.create( + browserWSEndpoint + ); + connection = new Connection(browserWSEndpoint, connectionTransport, slowMo); + } else if (browserURL) { + const connectionURL = await getWSEndpoint(browserURL); + const connectionTransport = await WebSocketTransport.create(connectionURL); + connection = new Connection(connectionURL, connectionTransport, slowMo); + } + + const { browserContextIds } = await connection.send( + 'Target.getBrowserContexts' + ); + return Browser.create( + connection, + browserContextIds, + ignoreHTTPSErrors, + defaultViewport, + null, + () => connection.send('Browser.close').catch(debugError) + ); +}; + +async function getWSEndpoint(browserURL: string): Promise { + const endpointURL = new URL('/json/version', browserURL); + + const fetch = await getFetch(); + try { + const result = await fetch(endpointURL.toString(), { + method: 'GET', + }); + if (!result.ok) { + throw new Error(`HTTP ${result.statusText}`); + } + const data = await result.json(); + return data.webSocketDebuggerUrl; + } catch (error) { + error.message = + `Failed to fetch browser webSocket URL from ${endpointURL}: ` + + error.message; + throw error; + } +} diff --git a/src/common/Product.ts b/src/common/Product.ts new file mode 100644 index 00000000..58a62fad --- /dev/null +++ b/src/common/Product.ts @@ -0,0 +1,21 @@ +/** + * Copyright 2020 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Supported products. + * @public + */ +export type Product = 'chrome' | 'firefox'; diff --git a/src/common/Puppeteer.ts b/src/common/Puppeteer.ts index d324a09d..7dc50e3d 100644 --- a/src/common/Puppeteer.ts +++ b/src/common/Puppeteer.ts @@ -14,11 +14,7 @@ * limitations under the License. */ import Launcher from '../node/Launcher.js'; -import { - LaunchOptions, - ChromeArgOptions, - BrowserOptions, -} from '../node/LaunchOptions.js'; +import { LaunchOptions, ChromeArgOptions } from '../node/LaunchOptions.js'; import { ProductLauncher } from '../node/Launcher.js'; import { BrowserFetcher, @@ -36,6 +32,8 @@ import { CustomQueryHandler, } from './QueryHandler.js'; import { PUPPETEER_REVISIONS } from '../revisions.js'; +import { Product } from './Product.js'; +import { connectToBrowser, BrowserOptions } from './BrowserConnector.js'; /** * The main Puppeteer class. Provides the {@link Puppeteer.launch | launch} @@ -141,11 +139,11 @@ export class Puppeteer { browserWSEndpoint?: string; browserURL?: string; transport?: ConnectionTransport; - product?: string; + product?: Product; } ): Promise { if (options.product) this._productName = options.product; - return this._launcher.connect(options); + return connectToBrowser(options); } /** diff --git a/src/common/fetch.ts b/src/common/fetch.ts new file mode 100644 index 00000000..ae4b65c4 --- /dev/null +++ b/src/common/fetch.ts @@ -0,0 +1,22 @@ +/** + * Copyright 2020 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { isNode } from '../environment.js'; + +/* Use the global version if we're in the browser, else load the node-fetch module. */ +export const getFetch = async (): Promise => { + return isNode ? await import('node-fetch') : globalThis.fetch; +}; diff --git a/src/node/BrowserFetcher.ts b/src/node/BrowserFetcher.ts index f5ecdba4..4653cd23 100644 --- a/src/node/BrowserFetcher.ts +++ b/src/node/BrowserFetcher.ts @@ -22,6 +22,7 @@ import * as childProcess from 'child_process'; import * as https from 'https'; import * as http from 'http'; +import { Product } from '../common/Product.js'; import extractZip from 'extract-zip'; import { debug } from '../common/Debug.js'; import { promisify } from 'util'; @@ -65,11 +66,6 @@ const browserConfig = { * @public */ export type Platform = 'linux' | 'mac' | 'win32' | 'win64'; -/** - * Supported products. - * @public - */ -export type Product = 'chrome' | 'firefox'; function archiveName( product: Product, diff --git a/src/node/LaunchOptions.ts b/src/node/LaunchOptions.ts index 0003ba87..0eb99b69 100644 --- a/src/node/LaunchOptions.ts +++ b/src/node/LaunchOptions.ts @@ -13,9 +13,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - -import { Viewport } from '../common/PuppeteerViewport.js'; - /** * Launcher options that only apply to Chrome. * @@ -43,13 +40,3 @@ export interface LaunchOptions { env?: Record; pipe?: boolean; } - -/** - * Generic browser options that can be passed when launching any browser. - * @public - */ -export interface BrowserOptions { - ignoreHTTPSErrors?: boolean; - defaultViewport?: Viewport; - slowMo?: number; -} diff --git a/src/node/Launcher.ts b/src/node/Launcher.ts index 4f92a88c..49a111e0 100644 --- a/src/node/Launcher.ts +++ b/src/node/Launcher.ts @@ -15,29 +15,19 @@ */ import * as os from 'os'; import * as path from 'path'; -import * as http from 'http'; -import * as https from 'https'; -import * as URL from 'url'; import * as fs from 'fs'; import { BrowserFetcher } from './BrowserFetcher.js'; -import { Connection } from '../common/Connection.js'; import { Browser } from '../common/Browser.js'; -import { assert } from '../common/assert.js'; -import { debugError } from '../common/helper.js'; -import { ConnectionTransport } from '../common/ConnectionTransport.js'; -import { WebSocketTransport } from '../common/WebSocketTransport.js'; import { BrowserRunner } from './BrowserRunner.js'; import { promisify } from 'util'; const mkdtempAsync = promisify(fs.mkdtemp); const writeFileAsync = promisify(fs.writeFile); -import { - ChromeArgOptions, - LaunchOptions, - BrowserOptions, -} from './LaunchOptions.js'; +import { ChromeArgOptions, LaunchOptions } from './LaunchOptions.js'; +import { BrowserOptions } from '../common/BrowserConnector.js'; +import { Product } from '../common/Product.js'; /** * Describes a launcher - a class that is able to create and launch a browser instance. @@ -45,10 +35,9 @@ import { */ export interface ProductLauncher { launch(object); - connect(object); executablePath: () => string; defaultArgs(object); - product: string; + product: Product; } /** @@ -215,66 +204,9 @@ class ChromeLauncher implements ProductLauncher { return resolveExecutablePath(this).executablePath; } - get product(): string { + get product(): Product { return 'chrome'; } - - async connect( - options: BrowserOptions & { - browserWSEndpoint?: string; - browserURL?: string; - transport?: ConnectionTransport; - } - ): Promise { - const { - browserWSEndpoint, - browserURL, - ignoreHTTPSErrors = false, - defaultViewport = { width: 800, height: 600 }, - transport, - slowMo = 0, - } = options; - - assert( - Number(!!browserWSEndpoint) + - Number(!!browserURL) + - Number(!!transport) === - 1, - 'Exactly one of browserWSEndpoint, browserURL or transport must be passed to puppeteer.connect' - ); - - let connection = null; - if (transport) { - connection = new Connection('', transport, slowMo); - } else if (browserWSEndpoint) { - const connectionTransport = await WebSocketTransport.create( - browserWSEndpoint - ); - connection = new Connection( - browserWSEndpoint, - connectionTransport, - slowMo - ); - } else if (browserURL) { - const connectionURL = await getWSEndpoint(browserURL); - const connectionTransport = await WebSocketTransport.create( - connectionURL - ); - connection = new Connection(connectionURL, connectionTransport, slowMo); - } - - const { browserContextIds } = await connection.send( - 'Target.getBrowserContexts' - ); - return Browser.create( - connection, - browserContextIds, - ignoreHTTPSErrors, - defaultViewport, - null, - () => connection.send('Browser.close').catch(debugError) - ); - } } /** @@ -392,63 +324,6 @@ class FirefoxLauncher implements ProductLauncher { } } - async connect( - options: BrowserOptions & { - browserWSEndpoint?: string; - browserURL?: string; - transport?: ConnectionTransport; - } - ): Promise { - const { - browserWSEndpoint, - browserURL, - ignoreHTTPSErrors = false, - defaultViewport = { width: 800, height: 600 }, - transport, - slowMo = 0, - } = options; - - assert( - Number(!!browserWSEndpoint) + - Number(!!browserURL) + - Number(!!transport) === - 1, - 'Exactly one of browserWSEndpoint, browserURL or transport must be passed to puppeteer.connect' - ); - - let connection = null; - if (transport) { - connection = new Connection('', transport, slowMo); - } else if (browserWSEndpoint) { - const connectionTransport = await WebSocketTransport.create( - browserWSEndpoint - ); - connection = new Connection( - browserWSEndpoint, - connectionTransport, - slowMo - ); - } else if (browserURL) { - const connectionURL = await getWSEndpoint(browserURL); - const connectionTransport = await WebSocketTransport.create( - connectionURL - ); - connection = new Connection(connectionURL, connectionTransport, slowMo); - } - - const { browserContextIds } = await connection.send( - 'Target.getBrowserContexts' - ); - return Browser.create( - connection, - browserContextIds, - ignoreHTTPSErrors, - defaultViewport, - null, - () => connection.send('Browser.close').catch(debugError) - ); - } - executablePath(): string { return resolveExecutablePath(this).executablePath; } @@ -464,7 +339,7 @@ class FirefoxLauncher implements ProductLauncher { } } - get product(): string { + get product(): Product { return 'firefox'; } @@ -705,42 +580,6 @@ class FirefoxLauncher implements ProductLauncher { } } -function getWSEndpoint(browserURL: string): Promise { - let resolve, reject; - const promise = new Promise((res, rej) => { - resolve = res; - reject = rej; - }); - - const endpointURL = URL.resolve(browserURL, '/json/version'); - const protocol = endpointURL.startsWith('https') ? https : http; - const requestOptions = Object.assign(URL.parse(endpointURL), { - method: 'GET', - }); - const request = protocol.request(requestOptions, (res) => { - let data = ''; - if (res.statusCode !== 200) { - // Consume response data to free up memory. - res.resume(); - reject(new Error('HTTP ' + res.statusCode)); - return; - } - res.setEncoding('utf8'); - res.on('data', (chunk) => (data += chunk)); - res.on('end', () => resolve(JSON.parse(data).webSocketDebuggerUrl)); - }); - - request.on('error', reject); - request.end(); - - return promise.catch((error) => { - error.message = - `Failed to fetch browser webSocket url from ${endpointURL}: ` + - error.message; - throw error; - }); -} - function resolveExecutablePath( launcher: ChromeLauncher | FirefoxLauncher ): { executablePath: string; missingText?: string } { diff --git a/test/chromiumonly.spec.ts b/test/chromiumonly.spec.ts index 65d1c820..7b64a70f 100644 --- a/test/chromiumonly.spec.ts +++ b/test/chromiumonly.spec.ts @@ -84,7 +84,7 @@ describeChromeOnly('Chromium-Specific Launcher tests', function () { .connect({ browserURL }) .catch((error_) => (error = error_)); expect(error.message).toContain( - 'Failed to fetch browser webSocket url from' + 'Failed to fetch browser webSocket URL from' ); originalBrowser.close(); }); diff --git a/utils/doclint/check_public_api/index.js b/utils/doclint/check_public_api/index.js index f91205ea..d3e8fb87 100644 --- a/utils/doclint/check_public_api/index.js +++ b/utils/doclint/check_public_api/index.js @@ -803,6 +803,13 @@ function compareDocumentations(actual, expected) { expectedName: 'Array', }, ], + [ + 'Method Puppeteer.connect() options.product', + { + actualName: 'string', + expectedName: 'Product', + }, + ], ]); const expectedForSource = expectedNamingMismatches.get(source);