chore(agnostification): split up launcher class (#6484)

The `Launcher` class was serving two purposes:

1. Launch browsers
2. Connect to browsers

Number 1) only needs to be done in Node land, but 2) is agnostic; in a
browser version of Puppeteer we'll need the ability to connect over a
websocket to send commands back and forth.

As part of the agnostification work we needed to split the `Launcher` up
so that the connection part can be made agnostic. Additionally, I
removed dependencies on `https`, `http` and `URL` from Node, instead
leaning on fetch (via `node-fetch` if in Node land) and the browser
`URL` API (which was added to Node in Node 10).
This commit is contained in:
Jack Franklin 2020-10-12 10:08:57 +01:00 committed by GitHub
parent 1ed38af79a
commit 4846b8723c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 180 additions and 221 deletions

View File

@ -1,22 +0,0 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [puppeteer](./puppeteer.md) &gt; [ProductLauncher](./puppeteer.productlauncher.md) &gt; [connect](./puppeteer.productlauncher.connect.md)
## ProductLauncher.connect() method
<b>Signature:</b>
```typescript
connect(object: any): any;
```
## Parameters
| Parameter | Type | Description |
| --- | --- | --- |
| object | any | |
<b>Returns:</b>
any

View File

@ -17,13 +17,12 @@ export interface ProductLauncher
| Property | Type | Description | | Property | Type | Description |
| --- | --- | --- | | --- | --- | --- |
| [executablePath](./puppeteer.productlauncher.executablepath.md) | () =&gt; string | | | [executablePath](./puppeteer.productlauncher.executablepath.md) | () =&gt; string | |
| [product](./puppeteer.productlauncher.product.md) | string | | | [product](./puppeteer.productlauncher.product.md) | [Product](./puppeteer.product.md) | |
## Methods ## Methods
| Method | Description | | Method | Description |
| --- | --- | | --- | --- |
| [connect(object)](./puppeteer.productlauncher.connect.md) | |
| [defaultArgs(object)](./puppeteer.productlauncher.defaultargs.md) | | | [defaultArgs(object)](./puppeteer.productlauncher.defaultargs.md) | |
| [launch(object)](./puppeteer.productlauncher.launch.md) | | | [launch(object)](./puppeteer.productlauncher.launch.md) | |

View File

@ -7,5 +7,5 @@
<b>Signature:</b> <b>Signature:</b>
```typescript ```typescript
product: string; product: Product;
``` ```

View File

@ -13,7 +13,7 @@ connect(options: BrowserOptions & {
browserWSEndpoint?: string; browserWSEndpoint?: string;
browserURL?: string; browserURL?: string;
transport?: ConnectionTransport; transport?: ConnectionTransport;
product?: string; product?: Product;
}): Promise<Browser>; }): Promise<Browser>;
``` ```
@ -21,7 +21,7 @@ connect(options: BrowserOptions & {
| Parameter | Type | Description | | Parameter | Type | Description |
| --- | --- | --- | | --- | --- | --- |
| options | [BrowserOptions](./puppeteer.browseroptions.md) &amp; { browserWSEndpoint?: string; browserURL?: string; transport?: ConnectionTransport; product?: string; } | Set of configurable options to set on the browser. | | options | [BrowserOptions](./puppeteer.browseroptions.md) &amp; { browserWSEndpoint?: string; browserURL?: string; transport?: ConnectionTransport; product?: [Product](./puppeteer.product.md)<!-- -->; } | Set of configurable options to set on the browser. |
<b>Returns:</b> <b>Returns:</b>

View File

@ -50,6 +50,7 @@
"devtools-protocol": "0.0.809251", "devtools-protocol": "0.0.809251",
"extract-zip": "^2.0.0", "extract-zip": "^2.0.0",
"https-proxy-agent": "^4.0.0", "https-proxy-agent": "^4.0.0",
"node-fetch": "^2.6.1",
"pkg-dir": "^4.2.0", "pkg-dir": "^4.2.0",
"progress": "^2.0.1", "progress": "^2.0.1",
"proxy-from-env": "^1.0.0", "proxy-from-env": "^1.0.0",

View File

@ -41,9 +41,11 @@ export * from './common/FileChooser.js';
export * from './common/FrameManager.js'; export * from './common/FrameManager.js';
export * from './common/Input.js'; export * from './common/Input.js';
export * from './common/Page.js'; export * from './common/Page.js';
export * from './common/Product.js';
export * from './common/Puppeteer.js'; export * from './common/Puppeteer.js';
export * from './node/LaunchOptions.js'; export * from './common/BrowserConnector.js';
export * from './node/Launcher.js'; export * from './node/Launcher.js';
export * from './node/LaunchOptions.js';
export * from './common/HTTPRequest.js'; export * from './common/HTTPRequest.js';
export * from './common/HTTPResponse.js'; export * from './common/HTTPResponse.js';
export * from './common/SecurityDetails.js'; export * from './common/SecurityDetails.js';

View File

@ -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<string> {
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;
}
}

21
src/common/Product.ts Normal file
View File

@ -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';

View File

@ -14,11 +14,7 @@
* limitations under the License. * limitations under the License.
*/ */
import Launcher from '../node/Launcher.js'; import Launcher from '../node/Launcher.js';
import { import { LaunchOptions, ChromeArgOptions } from '../node/LaunchOptions.js';
LaunchOptions,
ChromeArgOptions,
BrowserOptions,
} from '../node/LaunchOptions.js';
import { ProductLauncher } from '../node/Launcher.js'; import { ProductLauncher } from '../node/Launcher.js';
import { import {
BrowserFetcher, BrowserFetcher,
@ -36,6 +32,8 @@ import {
CustomQueryHandler, CustomQueryHandler,
} from './QueryHandler.js'; } from './QueryHandler.js';
import { PUPPETEER_REVISIONS } from '../revisions.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} * The main Puppeteer class. Provides the {@link Puppeteer.launch | launch}
@ -141,11 +139,11 @@ export class Puppeteer {
browserWSEndpoint?: string; browserWSEndpoint?: string;
browserURL?: string; browserURL?: string;
transport?: ConnectionTransport; transport?: ConnectionTransport;
product?: string; product?: Product;
} }
): Promise<Browser> { ): Promise<Browser> {
if (options.product) this._productName = options.product; if (options.product) this._productName = options.product;
return this._launcher.connect(options); return connectToBrowser(options);
} }
/** /**

22
src/common/fetch.ts Normal file
View File

@ -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<typeof fetch> => {
return isNode ? await import('node-fetch') : globalThis.fetch;
};

View File

@ -22,6 +22,7 @@ import * as childProcess from 'child_process';
import * as https from 'https'; import * as https from 'https';
import * as http from 'http'; import * as http from 'http';
import { Product } from '../common/Product.js';
import extractZip from 'extract-zip'; import extractZip from 'extract-zip';
import { debug } from '../common/Debug.js'; import { debug } from '../common/Debug.js';
import { promisify } from 'util'; import { promisify } from 'util';
@ -65,11 +66,6 @@ const browserConfig = {
* @public * @public
*/ */
export type Platform = 'linux' | 'mac' | 'win32' | 'win64'; export type Platform = 'linux' | 'mac' | 'win32' | 'win64';
/**
* Supported products.
* @public
*/
export type Product = 'chrome' | 'firefox';
function archiveName( function archiveName(
product: Product, product: Product,

View File

@ -13,9 +13,6 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
import { Viewport } from '../common/PuppeteerViewport.js';
/** /**
* Launcher options that only apply to Chrome. * Launcher options that only apply to Chrome.
* *
@ -43,13 +40,3 @@ export interface LaunchOptions {
env?: Record<string, string | undefined>; env?: Record<string, string | undefined>;
pipe?: boolean; pipe?: boolean;
} }
/**
* Generic browser options that can be passed when launching any browser.
* @public
*/
export interface BrowserOptions {
ignoreHTTPSErrors?: boolean;
defaultViewport?: Viewport;
slowMo?: number;
}

View File

@ -15,29 +15,19 @@
*/ */
import * as os from 'os'; import * as os from 'os';
import * as path from 'path'; 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 * as fs from 'fs';
import { BrowserFetcher } from './BrowserFetcher.js'; import { BrowserFetcher } from './BrowserFetcher.js';
import { Connection } from '../common/Connection.js';
import { Browser } from '../common/Browser.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 { BrowserRunner } from './BrowserRunner.js';
import { promisify } from 'util'; import { promisify } from 'util';
const mkdtempAsync = promisify(fs.mkdtemp); const mkdtempAsync = promisify(fs.mkdtemp);
const writeFileAsync = promisify(fs.writeFile); const writeFileAsync = promisify(fs.writeFile);
import { import { ChromeArgOptions, LaunchOptions } from './LaunchOptions.js';
ChromeArgOptions, import { BrowserOptions } from '../common/BrowserConnector.js';
LaunchOptions, import { Product } from '../common/Product.js';
BrowserOptions,
} from './LaunchOptions.js';
/** /**
* Describes a launcher - a class that is able to create and launch a browser instance. * Describes a launcher - a class that is able to create and launch a browser instance.
@ -45,10 +35,9 @@ import {
*/ */
export interface ProductLauncher { export interface ProductLauncher {
launch(object); launch(object);
connect(object);
executablePath: () => string; executablePath: () => string;
defaultArgs(object); defaultArgs(object);
product: string; product: Product;
} }
/** /**
@ -215,66 +204,9 @@ class ChromeLauncher implements ProductLauncher {
return resolveExecutablePath(this).executablePath; return resolveExecutablePath(this).executablePath;
} }
get product(): string { get product(): Product {
return 'chrome'; return 'chrome';
} }
async connect(
options: BrowserOptions & {
browserWSEndpoint?: string;
browserURL?: string;
transport?: ConnectionTransport;
}
): Promise<Browser> {
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<Browser> {
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 { executablePath(): string {
return resolveExecutablePath(this).executablePath; return resolveExecutablePath(this).executablePath;
} }
@ -464,7 +339,7 @@ class FirefoxLauncher implements ProductLauncher {
} }
} }
get product(): string { get product(): Product {
return 'firefox'; return 'firefox';
} }
@ -705,42 +580,6 @@ class FirefoxLauncher implements ProductLauncher {
} }
} }
function getWSEndpoint(browserURL: string): Promise<string> {
let resolve, reject;
const promise = new Promise<string>((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( function resolveExecutablePath(
launcher: ChromeLauncher | FirefoxLauncher launcher: ChromeLauncher | FirefoxLauncher
): { executablePath: string; missingText?: string } { ): { executablePath: string; missingText?: string } {

View File

@ -84,7 +84,7 @@ describeChromeOnly('Chromium-Specific Launcher tests', function () {
.connect({ browserURL }) .connect({ browserURL })
.catch((error_) => (error = error_)); .catch((error_) => (error = error_));
expect(error.message).toContain( expect(error.message).toContain(
'Failed to fetch browser webSocket url from' 'Failed to fetch browser webSocket URL from'
); );
originalBrowser.close(); originalBrowser.close();
}); });

View File

@ -803,6 +803,13 @@ function compareDocumentations(actual, expected) {
expectedName: 'Array<Object>', expectedName: 'Array<Object>',
}, },
], ],
[
'Method Puppeteer.connect() options.product',
{
actualName: 'string',
expectedName: 'Product',
},
],
]); ]);
const expectedForSource = expectedNamingMismatches.get(source); const expectedForSource = expectedNamingMismatches.get(source);