feat!: deprecate createBrowserFetcher in favor of BrowserFetcher (#9079)

This PR deprecates the `createBrowserFetcher` API and requests users to
import the `BrowserFetcher` directly.

Fixed: #8999
This commit is contained in:
jrandolf 2022-10-10 17:51:18 +02:00 committed by GitHub
parent 1847704789
commit 7294dfe9c6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 207 additions and 168 deletions

View File

@ -0,0 +1,21 @@
---
sidebar_label: BrowserFetcher.(constructor)
---
# BrowserFetcher.(constructor)
Constructs a browser fetcher for the given options.
**Signature:**
```typescript
class BrowserFetcher {
constructor(options?: BrowserFetcherOptions);
}
```
## Parameters
| Parameter | Type | Description |
| --------- | ------------------------------------------------------------- | ----------------- |
| options | [BrowserFetcherOptions](./puppeteer.browserfetcheroptions.md) | <i>(Optional)</i> |

View File

@ -14,23 +14,25 @@ export declare class BrowserFetcher
## Remarks
BrowserFetcher operates on revision strings that specify a precise version of Chromium, e.g. `"533271"`. Revision strings can be obtained from [omahaproxy.appspot.com](http://omahaproxy.appspot.com/). In the Firefox case, BrowserFetcher downloads Firefox Nightly and operates on version numbers such as `"75"`.
The constructor for this class is marked as internal. Third-party code should not call the constructor directly or create subclasses that extend the `BrowserFetcher` class.
BrowserFetcher is not designed to work concurrently with other instances of BrowserFetcher that share the same downloads directory.
## Example
An example of using BrowserFetcher to download a specific version of Chromium and running Puppeteer against it:
```ts
const browserFetcher = puppeteer.createBrowserFetcher();
const browserFetcher = new BrowserFetcher();
const revisionInfo = await browserFetcher.download('533271');
const browser = await puppeteer.launch({
executablePath: revisionInfo.executablePath,
});
```
**NOTE** BrowserFetcher is not designed to work concurrently with other instances of BrowserFetcher that share the same downloads directory.
## Constructors
| Constructor | Modifiers | Description |
| --------------------------------------------------------------------- | --------- | --------------------------------------------------- |
| [(constructor)(options)](./puppeteer.browserfetcher._constructor_.md) | | Constructs a browser fetcher for the given options. |
## Methods

View File

@ -4,6 +4,8 @@ sidebar_label: BrowserFetcherOptions.host
# BrowserFetcherOptions.host property
Determines the host that will be used for downloading.
**Signature:**
```typescript

View File

@ -12,9 +12,9 @@ export interface BrowserFetcherOptions
## Properties
| Property | Modifiers | Type | Description |
| ---------------------------------------------------------- | --------- | ----------------------------------- | ----------------- |
| [host?](./puppeteer.browserfetcheroptions.host.md) | | string | <i>(Optional)</i> |
| [path?](./puppeteer.browserfetcheroptions.path.md) | | string | <i>(Optional)</i> |
| [platform?](./puppeteer.browserfetcheroptions.platform.md) | | [Platform](./puppeteer.platform.md) | <i>(Optional)</i> |
| [product?](./puppeteer.browserfetcheroptions.product.md) | | string | <i>(Optional)</i> |
| Property | Modifiers | Type | Description |
| ---------------------------------------------------------- | --------- | ----------------------------------- | ------------------------------------------------------------------------------------------------------ |
| [host?](./puppeteer.browserfetcheroptions.host.md) | | string | <i>(Optional)</i> Determines the host that will be used for downloading. |
| [path?](./puppeteer.browserfetcheroptions.path.md) | | string | <i>(Optional)</i> Determines the path to download browsers to. |
| [platform?](./puppeteer.browserfetcheroptions.platform.md) | | [Platform](./puppeteer.platform.md) | <i>(Optional)</i> Determines which platform the browser will be suited for. |
| [product?](./puppeteer.browserfetcheroptions.product.md) | | 'chrome' \| 'firefox' | <i>(Optional)</i> Determines which product the [BrowserFetcher](./puppeteer.browserfetcher.md) is for. |

View File

@ -4,6 +4,8 @@ sidebar_label: BrowserFetcherOptions.path
# BrowserFetcherOptions.path property
Determines the path to download browsers to.
**Signature:**
```typescript

View File

@ -4,6 +4,8 @@ sidebar_label: BrowserFetcherOptions.platform
# BrowserFetcherOptions.platform property
Determines which platform the browser will be suited for.
**Signature:**
```typescript

View File

@ -4,10 +4,12 @@ sidebar_label: BrowserFetcherOptions.product
# BrowserFetcherOptions.product property
Determines which product the [BrowserFetcher](./puppeteer.browserfetcher.md) is for.
**Signature:**
```typescript
interface BrowserFetcherOptions {
product?: string;
product?: 'chrome' | 'firefox';
}
```

View File

@ -4,6 +4,10 @@ sidebar_label: createBrowserFetcher
# createBrowserFetcher variable
> Warning: This API is now obsolete.
>
> Import [BrowserFetcher](./puppeteer.browserfetcher.md) directly and use the constructor.
**Signature:**
```typescript

View File

@ -4,6 +4,10 @@ sidebar_label: PuppeteerNode.createBrowserFetcher
# PuppeteerNode.createBrowserFetcher() method
> Warning: This API is now obsolete.
>
> Import [BrowserFetcher](./puppeteer.browserfetcher.md) directly and use the constructor.
**Signature:**
```typescript

View File

@ -14,9 +14,10 @@
* limitations under the License.
*/
import * as childProcess from 'child_process';
import {exec as execChildProcess} from 'child_process';
import extractZip from 'extract-zip';
import * as fs from 'fs';
import {createReadStream, createWriteStream, existsSync} from 'fs';
import {chmod, mkdir, readdir, unlink} from 'fs/promises';
import * as http from 'http';
import * as https from 'https';
import createHttpsProxyAgent, {
@ -69,6 +70,8 @@ const browserConfig = {
},
} as const;
const exec = promisify(execChildProcess);
/**
* Supported platforms.
*
@ -117,11 +120,11 @@ function downloadURL(
}
function handleArm64(): void {
let exists = fs.existsSync('/usr/bin/chromium-browser');
let exists = existsSync('/usr/bin/chromium-browser');
if (exists) {
return;
}
exists = fs.existsSync('/usr/bin/chromium');
exists = existsSync('/usr/bin/chromium');
if (exists) {
return;
}
@ -134,27 +137,32 @@ function handleArm64(): void {
throw new Error();
}
const readdirAsync = promisify(fs.readdir.bind(fs));
const mkdirAsync = promisify(fs.mkdir.bind(fs));
const unlinkAsync = promisify(fs.unlink.bind(fs));
const chmodAsync = promisify(fs.chmod.bind(fs));
function existsAsync(filePath: string): Promise<boolean> {
return new Promise(resolve => {
fs.access(filePath, err => {
return resolve(!err);
});
});
}
/**
* @public
*/
export interface BrowserFetcherOptions {
/**
* Determines which platform the browser will be suited for.
*/
platform?: Platform;
product?: string;
/**
* Determines which product the {@link BrowserFetcher} is for.
*
* @defaultValue `"chrome"`
*/
product?: 'chrome' | 'firefox';
/**
* Determines the path to download browsers to.
*/
path?: string;
/**
* Determines the host that will be used for downloading.
*/
host?: string;
/**
* @internal
*/
projectRoot?: string;
}
/**
@ -168,29 +176,38 @@ export interface BrowserFetcherRevisionInfo {
revision: string;
product: string;
}
/**
* BrowserFetcher can download and manage different versions of Chromium and Firefox.
* BrowserFetcher can download and manage different versions of Chromium and
* Firefox.
*
* @remarks
* BrowserFetcher operates on revision strings that specify a precise version of Chromium, e.g. `"533271"`. Revision strings can be obtained from {@link http://omahaproxy.appspot.com/ | omahaproxy.appspot.com}.
* In the Firefox case, BrowserFetcher downloads Firefox Nightly and
* operates on version numbers such as `"75"`.
* BrowserFetcher operates on revision strings that specify a precise version of
* Chromium, e.g. `"533271"`. Revision strings can be obtained from
* {@link http://omahaproxy.appspot.com/ | omahaproxy.appspot.com}. For Firefox,
* BrowserFetcher downloads Firefox Nightly and operates on version numbers such
* as `"75"`.
*
* @remarks
* The default constructed fetcher will always be for Chromium unless otherwise
* specified.
*
* @remarks
* BrowserFetcher is not designed to work concurrently with other instances of
* BrowserFetcher that share the same downloads directory.
*
* @example
* An example of using BrowserFetcher to download a specific version of Chromium
* and running Puppeteer against it:
*
* ```ts
* const browserFetcher = puppeteer.createBrowserFetcher();
* const browserFetcher = new BrowserFetcher();
* const revisionInfo = await browserFetcher.download('533271');
* const browser = await puppeteer.launch({
* executablePath: revisionInfo.executablePath,
* });
* ```
*
* **NOTE** BrowserFetcher is not designed to work concurrently with other
* instances of BrowserFetcher that share the same downloads directory.
*
* @public
*/
@ -201,9 +218,9 @@ export class BrowserFetcher {
#platform: Platform;
/**
* @internal
* Constructs a browser fetcher for the given options.
*/
constructor(projectRoot: string, options: BrowserFetcherOptions = {}) {
constructor(options: BrowserFetcherOptions = {}) {
this.#product = (options.product || 'chrome').toLowerCase() as Product;
assert(
this.#product === 'chrome' || this.#product === 'firefox',
@ -212,7 +229,7 @@ export class BrowserFetcher {
this.#downloadsFolder =
options.path ||
path.join(projectRoot, browserConfig[this.#product].destination);
path.join(options.projectRoot!, browserConfig[this.#product].destination);
this.#downloadHost = options.host || browserConfig[this.#product].host;
if (options.platform) {
@ -240,7 +257,7 @@ export class BrowserFetcher {
this.#platform =
os.arch() === 'x64' ||
// Windows 11 for ARM supports x64 emulation
(os.arch() === 'arm64' && _isWindows11(os.release()))
(os.arch() === 'arm64' && isWindows11(os.release()))
? 'win64'
: 'win32';
return;
@ -333,11 +350,11 @@ export class BrowserFetcher {
assert(fileName, `A malformed download URL was found: ${url}.`);
const archivePath = path.join(this.#downloadsFolder, fileName);
const outputPath = this.#getFolderPath(revision);
if (await existsAsync(outputPath)) {
if (existsSync(outputPath)) {
return this.revisionInfo(revision);
}
if (!(await existsAsync(this.#downloadsFolder))) {
await mkdirAsync(this.#downloadsFolder);
if (!existsSync(this.#downloadsFolder)) {
await mkdir(this.#downloadsFolder);
}
// Use system Chromium builds on Linux ARM devices
@ -349,13 +366,13 @@ export class BrowserFetcher {
await _downloadFile(url, archivePath, progressCallback);
await install(archivePath, outputPath);
} finally {
if (await existsAsync(archivePath)) {
await unlinkAsync(archivePath);
if (existsSync(archivePath)) {
await unlink(archivePath);
}
}
const revisionInfo = this.revisionInfo(revision);
if (revisionInfo) {
await chmodAsync(revisionInfo.executablePath, 0o755);
await chmod(revisionInfo.executablePath, 0o755);
}
return revisionInfo;
}
@ -367,10 +384,10 @@ export class BrowserFetcher {
* available locally on disk.
*/
async localRevisions(): Promise<string[]> {
if (!(await existsAsync(this.#downloadsFolder))) {
if (!existsSync(this.#downloadsFolder)) {
return [];
}
const fileNames = await readdirAsync(this.#downloadsFolder);
const fileNames = await readdir(this.#downloadsFolder);
return fileNames
.map(fileName => {
return parseFolderPath(this.#product, fileName);
@ -397,7 +414,7 @@ export class BrowserFetcher {
async remove(revision: string): Promise<void> {
const folderPath = this.#getFolderPath(revision);
assert(
await existsAsync(folderPath),
existsSync(folderPath),
`Failed to remove: revision ${revision} is not downloaded`
);
await new Promise(fulfill => {
@ -412,57 +429,66 @@ export class BrowserFetcher {
revisionInfo(revision: string): BrowserFetcherRevisionInfo {
const folderPath = this.#getFolderPath(revision);
let executablePath = '';
if (this.#product === 'chrome') {
if (this.#platform === 'mac' || this.#platform === 'mac_arm') {
executablePath = path.join(
folderPath,
archiveName(this.#product, this.#platform, revision),
'Chromium.app',
'Contents',
'MacOS',
'Chromium'
);
} else if (this.#platform === 'linux') {
executablePath = path.join(
folderPath,
archiveName(this.#product, this.#platform, revision),
'chrome'
);
} else if (this.#platform === 'win32' || this.#platform === 'win64') {
executablePath = path.join(
folderPath,
archiveName(this.#product, this.#platform, revision),
'chrome.exe'
);
} else {
throw new Error('Unsupported platform: ' + this.#platform);
}
} else if (this.#product === 'firefox') {
if (this.#platform === 'mac' || this.#platform === 'mac_arm') {
executablePath = path.join(
folderPath,
'Firefox Nightly.app',
'Contents',
'MacOS',
'firefox'
);
} else if (this.#platform === 'linux') {
executablePath = path.join(folderPath, 'firefox', 'firefox');
} else if (this.#platform === 'win32' || this.#platform === 'win64') {
executablePath = path.join(folderPath, 'firefox', 'firefox.exe');
} else {
throw new Error('Unsupported platform: ' + this.#platform);
}
} else {
throw new Error('Unsupported product: ' + this.#product);
switch (this.#product) {
case 'chrome':
switch (this.#platform) {
case 'mac':
case 'mac_arm':
executablePath = path.join(
folderPath,
archiveName(this.#product, this.#platform, revision),
'Chromium.app',
'Contents',
'MacOS',
'Chromium'
);
break;
case 'linux':
executablePath = path.join(
folderPath,
archiveName(this.#product, this.#platform, revision),
'chrome'
);
break;
case 'win32':
case 'win64':
executablePath = path.join(
folderPath,
archiveName(this.#product, this.#platform, revision),
'chrome.exe'
);
break;
}
break;
case 'firefox':
switch (this.#platform) {
case 'mac':
case 'mac_arm':
executablePath = path.join(
folderPath,
'Firefox Nightly.app',
'Contents',
'MacOS',
'firefox'
);
break;
case 'linux':
executablePath = path.join(folderPath, 'firefox', 'firefox');
break;
case 'win32':
case 'win64':
executablePath = path.join(folderPath, 'firefox', 'firefox.exe');
break;
}
}
const url = downloadURL(
this.#product,
this.#platform,
this.#downloadHost,
revision
);
const local = fs.existsSync(folderPath);
const local = existsSync(folderPath);
debugFetcher({
revision,
executablePath,
@ -506,7 +532,7 @@ function parseFolderPath(
* Windows 11 is identified by 10.0.22000 or greater
* @internal
*/
function _isWindows11(version: string): boolean {
function isWindows11(version: string): boolean {
const parts = version.split('.');
if (parts.length > 2) {
const major = parseInt(parts[0] as string, 10);
@ -550,7 +576,7 @@ function _downloadFile(
reject(error);
return;
}
const file = fs.createWriteStream(destinationPath);
const file = createWriteStream(destinationPath);
file.on('finish', () => {
return fulfill();
});
@ -574,16 +600,15 @@ function _downloadFile(
}
}
function install(archivePath: string, folderPath: string): Promise<unknown> {
async function install(archivePath: string, folderPath: string): Promise<void> {
debugFetcher(`Installing ${archivePath} to ${folderPath}`);
if (archivePath.endsWith('.zip')) {
return extractZip(archivePath, {dir: folderPath});
await extractZip(archivePath, {dir: folderPath});
} else if (archivePath.endsWith('.tar.bz2')) {
return _extractTar(archivePath, folderPath);
await extractTar(archivePath, folderPath);
} else if (archivePath.endsWith('.dmg')) {
return mkdirAsync(folderPath).then(() => {
return _installDMG(archivePath, folderPath);
});
await mkdir(folderPath);
await installDMG(archivePath, folderPath);
} else {
throw new Error(`Unsupported archive format: ${archivePath}`);
}
@ -592,12 +617,12 @@ function install(archivePath: string, folderPath: string): Promise<unknown> {
/**
* @internal
*/
function _extractTar(tarPath: string, folderPath: string): Promise<unknown> {
function extractTar(tarPath: string, folderPath: string): Promise<void> {
return new Promise((fulfill, reject) => {
const tarStream = tar.extract(folderPath);
tarStream.on('error', reject);
tarStream.on('finish', fulfill);
const readStream = fs.createReadStream(tarPath);
const readStream = createReadStream(tarPath);
readStream.pipe(bzip()).pipe(tarStream);
});
}
@ -605,56 +630,33 @@ function _extractTar(tarPath: string, folderPath: string): Promise<unknown> {
/**
* @internal
*/
function _installDMG(dmgPath: string, folderPath: string): Promise<void> {
let mountPath: string | undefined;
async function installDMG(dmgPath: string, folderPath: string): Promise<void> {
const {stdout} = await exec(
`hdiutil attach -nobrowse -noautoopen "${dmgPath}"`
);
return new Promise<void>((fulfill, reject): void => {
const mountCommand = `hdiutil attach -nobrowse -noautoopen "${dmgPath}"`;
childProcess.exec(mountCommand, (err, stdout) => {
if (err) {
return reject(err);
}
const volumes = stdout.match(/\/Volumes\/(.*)/m);
if (!volumes) {
return reject(new Error(`Could not find volume path in ${stdout}`));
}
mountPath = volumes[0]!;
readdirAsync(mountPath)
.then(fileNames => {
const appName = fileNames.find(item => {
return typeof item === 'string' && item.endsWith('.app');
});
if (!appName) {
return reject(new Error(`Cannot find app in ${mountPath}`));
}
const copyPath = path.join(mountPath!, appName);
debugFetcher(`Copying ${copyPath} to ${folderPath}`);
childProcess.exec(`cp -R "${copyPath}" "${folderPath}"`, err => {
if (err) {
reject(err);
} else {
fulfill();
}
});
})
.catch(reject);
});
})
.catch(error => {
console.error(error);
})
.finally((): void => {
if (!mountPath) {
return;
}
const unmountCommand = `hdiutil detach "${mountPath}" -quiet`;
debugFetcher(`Unmounting ${mountPath}`);
childProcess.exec(unmountCommand, err => {
if (err) {
console.error(`Error unmounting dmg: ${err}`);
}
});
const volumes = stdout.match(/\/Volumes\/(.*)/m);
if (!volumes) {
throw new Error(`Could not find volume path in ${stdout}`);
}
const mountPath = volumes[0]!;
try {
const fileNames = await readdir(mountPath);
const appName = fileNames.find(item => {
return typeof item === 'string' && item.endsWith('.app');
});
if (!appName) {
throw new Error(`Cannot find app in ${mountPath}`);
}
const mountedPath = path.join(mountPath!, appName);
debugFetcher(`Copying ${mountedPath} to ${folderPath}`);
await exec(`cp -R "${mountedPath}" "${folderPath}"`);
} finally {
debugFetcher(`Unmounting ${mountPath}`);
await exec(`hdiutil detach "${mountPath}" -quiet`);
}
}
function httpRequest(
@ -675,11 +677,7 @@ function httpRequest(
let options: Options = {
...urlParsed,
method,
headers: keepAlive
? {
Connection: 'keep-alive',
}
: undefined,
headers: keepAlive ? {Connection: 'keep-alive'} : undefined,
};
const proxyURL = getProxyForUrl(url);

View File

@ -221,7 +221,8 @@ export class FirefoxLauncher implements ProductLauncher {
'_projectRoot is undefined. Unable to create a BrowserFetcher.'
);
}
const browserFetcher = new BrowserFetcher(this._projectRoot, {
const browserFetcher = new BrowserFetcher({
projectRoot: this._projectRoot,
product: this.product,
});
const localRevisions = await browserFetcher.localRevisions();

View File

@ -160,7 +160,8 @@ export function resolveExecutablePath(
'_projectRoot is undefined. Unable to create a BrowserFetcher.'
);
}
const browserFetcher = new BrowserFetcher(_projectRoot, {
const browserFetcher = new BrowserFetcher({
projectRoot: _projectRoot,
product: product,
path: downloadPath,
});

View File

@ -234,16 +234,13 @@ export class PuppeteerNode extends Puppeteer {
}
/**
* @deprecated Import {@link BrowserFetcher} directly and use the constructor.
*
* @param options - Set of configurable options to specify the settings of the
* BrowserFetcher.
* @returns A new BrowserFetcher instance.
*/
createBrowserFetcher(options: BrowserFetcherOptions): BrowserFetcher {
if (!this.#projectRoot) {
throw new Error(
'_projectRoot is undefined. Unable to create a BrowserFetcher.'
);
}
return new BrowserFetcher(this.#projectRoot, options);
return new BrowserFetcher({...options, projectRoot: this.#projectRoot});
}
}

View File

@ -19,6 +19,7 @@ export * from './common/Device.js';
export * from './common/Errors.js';
export * from './common/PredefinedNetworkConditions.js';
export * from './common/Puppeteer.js';
export * from './node/BrowserFetcher.js';
/**
* @deprecated Use the query handler API defined on {@link Puppeteer}
@ -39,6 +40,7 @@ const puppeteer = new PuppeteerNode({
export const {
connect,
/** @deprecated Import {@link BrowserFetcher} directly and use the constructor. */
createBrowserFetcher,
defaultArgs,
executablePath,

View File

@ -59,7 +59,7 @@ export async function downloadBrowser(): Promise<void> {
process.env['PUPPETEER_DOWNLOAD_PATH'] ||
process.env['npm_config_puppeteer_download_path'] ||
process.env['npm_package_config_puppeteer_download_path'];
const browserFetcher = (puppeteer as PuppeteerNode).createBrowserFetcher({
const browserFetcher = puppeteer.createBrowserFetcher({
product,
host: downloadHost,
path: downloadPath,

View File

@ -19,11 +19,11 @@ export * from 'puppeteer-core/internal/common/Device.js';
export * from 'puppeteer-core/internal/common/Errors.js';
export * from 'puppeteer-core/internal/common/PredefinedNetworkConditions.js';
export * from 'puppeteer-core/internal/common/Puppeteer.js';
export * from 'puppeteer-core/internal/node/BrowserFetcher.js';
/**
* @deprecated Use the query handler API defined on {@link Puppeteer}
*/
export * from 'puppeteer-core/internal/common/QueryHandler.js';
export {BrowserFetcher} from 'puppeteer-core/internal/node/BrowserFetcher.js';
export {LaunchOptions} from 'puppeteer-core/internal/node/LaunchOptions.js';
import {Product} from 'puppeteer-core';
@ -57,6 +57,7 @@ const puppeteer = new PuppeteerNode({
export const {
connect,
/** @deprecated Import {@link BrowserFetcher} directly and use the constructor. */
createBrowserFetcher,
defaultArgs,
executablePath,