feat: implement the Puppeteer CLI (#11344)

This commit is contained in:
Alex Rudenko 2023-11-23 09:51:37 +01:00 committed by GitHub
parent a7fcde9181
commit 53fb69bf7f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 226 additions and 26 deletions

View File

@ -10,13 +10,29 @@ Constructs a new instance of the `CLI` class
```typescript ```typescript
class CLI { class CLI {
constructor(cachePath?: string, rl?: readline.Interface); constructor(
opts?:
| string
| {
cachePath?: string;
scriptName?: string;
prefixCommand?: {
cmd: string;
description: string;
};
allowCachePathOverride?: boolean;
pinnedBrowsers?: Partial<{
[key in Browser]: string;
}>;
},
rl?: readline.Interface
);
} }
``` ```
## Parameters ## Parameters
| Parameter | Type | Description | | Parameter | Type | Description |
| --------- | ------------------ | ------------ | | --------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------ |
| cachePath | string | _(Optional)_ | | opts | string \| { cachePath?: string; scriptName?: string; prefixCommand?: { cmd: string; description: string; }; allowCachePathOverride?: boolean; pinnedBrowsers?: Partial&lt;{ \[key in [Browser](./browsers.browser.md)\]: string; }&gt;; } | _(Optional)_ |
| rl | readline.Interface | _(Optional)_ | | rl | readline.Interface | _(Optional)_ |

View File

@ -13,8 +13,8 @@ export declare class CLI
## Constructors ## Constructors
| Constructor | Modifiers | Description | | Constructor | Modifiers | Description |
| --------------------------------------------------------------- | --------- | ------------------------------------------------------- | | ---------------------------------------------------------- | --------- | ------------------------------------------------------- |
| [(constructor)(cachePath, rl)](./browsers.cli._constructor_.md) | | Constructs a new instance of the <code>CLI</code> class | | [(constructor)(opts, rl)](./browsers.cli._constructor_.md) | | Constructs a new instance of the <code>CLI</code> class |
## Methods ## Methods

View File

@ -32,7 +32,7 @@ again.
```bash ```bash
npm install npm install
# Or to download Firefox # Or to download Firefox by default
PUPPETEER_PRODUCT=firefox npm install PUPPETEER_PRODUCT=firefox npm install
``` ```

View File

@ -117,6 +117,12 @@ To fetch Firefox Nightly as part of Puppeteer installation:
PUPPETEER_PRODUCT=firefox npm i puppeteer PUPPETEER_PRODUCT=firefox npm i puppeteer
``` ```
To download Firefox Nightly into an existing Puppeteer project:
```bash
npx puppeteer browsers install firefox
```
#### Q: Whats considered a “Navigation”? #### Q: Whats considered a “Navigation”?
From Puppeteers standpoint, **“navigation” is anything that changes a pages From Puppeteers standpoint, **“navigation” is anything that changes a pages

3
package-lock.json generated
View File

@ -11986,6 +11986,9 @@
"cosmiconfig": "8.3.6", "cosmiconfig": "8.3.6",
"puppeteer-core": "21.5.2" "puppeteer-core": "21.5.2"
}, },
"bin": {
"puppeteer": "lib/esm/puppeteer/node/cli.js"
},
"devDependencies": { "devDependencies": {
"@types/node": "18.17.15" "@types/node": "18.17.15"
}, },

View File

@ -68,10 +68,37 @@ interface ClearArgs {
export class CLI { export class CLI {
#cachePath; #cachePath;
#rl?: readline.Interface; #rl?: readline.Interface;
#scriptName = '';
#allowCachePathOverride = true;
#pinnedBrowsers?: Partial<{[key in Browser]: string}>;
#prefixCommand?: {cmd: string; description: string};
constructor(cachePath = process.cwd(), rl?: readline.Interface) { constructor(
this.#cachePath = cachePath; opts?:
| string
| {
cachePath?: string;
scriptName?: string;
prefixCommand?: {cmd: string; description: string};
allowCachePathOverride?: boolean;
pinnedBrowsers?: Partial<{[key in Browser]: string}>;
},
rl?: readline.Interface
) {
if (!opts) {
opts = {};
}
if (typeof opts === 'string') {
opts = {
cachePath: opts,
};
}
this.#cachePath = opts.cachePath ?? process.cwd();
this.#rl = rl; this.#rl = rl;
this.#scriptName = opts.scriptName ?? '@puppeteer/browsers';
this.#allowCachePathOverride = opts.allowCachePathOverride ?? true;
this.#pinnedBrowsers = opts.pinnedBrowsers;
this.#prefixCommand = opts.prefixCommand;
} }
#defineBrowserParameter(yargs: Yargs.Argv<unknown>): void { #defineBrowserParameter(yargs: Yargs.Argv<unknown>): void {
@ -98,6 +125,9 @@ export class CLI {
} }
#definePathParameter(yargs: Yargs.Argv<unknown>, required = false): void { #definePathParameter(yargs: Yargs.Argv<unknown>, required = false): void {
if (!this.#allowCachePathOverride) {
return;
}
yargs.option('path', { yargs.option('path', {
type: 'string', type: 'string',
desc: 'Path to the root folder for the browser downloads and installation. The installation folder structure is compatible with the cache structure used by Puppeteer.', desc: 'Path to the root folder for the browser downloads and installation. The installation folder structure is compatible with the cache structure used by Puppeteer.',
@ -111,8 +141,28 @@ export class CLI {
async run(argv: string[]): Promise<void> { async run(argv: string[]): Promise<void> {
const yargsInstance = yargs(hideBin(argv)); const yargsInstance = yargs(hideBin(argv));
await yargsInstance let target = yargsInstance.scriptName(this.#scriptName);
.scriptName('@puppeteer/browsers') if (this.#prefixCommand) {
target = target.command(
this.#prefixCommand.cmd,
this.#prefixCommand.description,
yargs => {
return this.#build(yargs);
}
);
} else {
target = this.#build(target);
}
await target
.demandCommand(1)
.help()
.wrap(Math.min(120, yargsInstance.terminalWidth()))
.parse();
}
#build(yargs: Yargs.Argv<unknown>): Yargs.Argv<unknown> {
const latestOrPinned = this.#pinnedBrowsers ? 'pinned' : 'latest';
return yargs
.command( .command(
'install <browser>', 'install <browser>',
'Download and install the specified browser. If successful, the command outputs the actual browser buildId that was installed and the absolute path to the browser executable (format: <browser>@<buildID> <path>).', 'Download and install the specified browser. If successful, the command outputs the actual browser buildId that was installed and the absolute path to the browser executable (format: <browser>@<buildID> <path>).',
@ -126,7 +176,7 @@ export class CLI {
}); });
yargs.example( yargs.example(
'$0 install chrome', '$0 install chrome',
'Install the latest available build of the Chrome browser.' `Install the ${latestOrPinned} available build of the Chrome browser.`
); );
yargs.example( yargs.example(
'$0 install chrome@latest', '$0 install chrome@latest',
@ -176,10 +226,12 @@ export class CLI {
'$0 install firefox --platform mac', '$0 install firefox --platform mac',
'Install the latest Mac (Intel) build of the Firefox browser.' 'Install the latest Mac (Intel) build of the Firefox browser.'
); );
if (this.#allowCachePathOverride) {
yargs.example( yargs.example(
'$0 install firefox --path /tmp/my-browser-cache', '$0 install firefox --path /tmp/my-browser-cache',
'Install to the specified cache directory.' 'Install to the specified cache directory.'
); );
}
}, },
async argv => { async argv => {
const args = argv as unknown as InstallArgs; const args = argv as unknown as InstallArgs;
@ -187,6 +239,15 @@ export class CLI {
if (!args.platform) { if (!args.platform) {
throw new Error(`Could not resolve the current platform`); throw new Error(`Could not resolve the current platform`);
} }
if (args.browser.buildId === 'pinned') {
const pinnedVersion = this.#pinnedBrowsers?.[args.browser.name];
if (!pinnedVersion) {
throw new Error(
`No pinned version found for ${args.browser.name}`
);
}
args.browser.buildId = pinnedVersion;
}
args.browser.buildId = await resolveBuildId( args.browser.buildId = await resolveBuildId(
args.browser.name, args.browser.name,
args.platform, args.platform,
@ -272,7 +333,9 @@ export class CLI {
) )
.command( .command(
'clear', 'clear',
'Removes all installed browsers from the specified cache directory', this.#allowCachePathOverride
? 'Removes all installed browsers from the specified cache directory'
: `Removes all installed browsers from ${this.#cachePath}`,
yargs => { yargs => {
this.#definePathParameter(yargs, true); this.#definePathParameter(yargs, true);
}, },
@ -296,9 +359,7 @@ export class CLI {
} }
) )
.demandCommand(1) .demandCommand(1)
.help() .help();
.wrap(Math.min(120, yargsInstance.terminalWidth()))
.parse();
} }
#parseBrowser(version: string): Browser { #parseBrowser(version: string): Browser {
@ -307,7 +368,11 @@ export class CLI {
#parseBuildId(version: string): string { #parseBuildId(version: string): string {
const parts = version.split('@'); const parts = version.split('@');
return parts.length === 2 ? parts[1]! : 'latest'; return parts.length === 2
? parts[1]!
: this.#pinnedBrowsers
? 'pinned'
: 'latest';
} }
} }

View File

@ -423,14 +423,14 @@ export abstract class ProductLauncher {
case 'chrome': case 'chrome':
throw new Error( throw new Error(
`Could not find Chrome (ver. ${this.puppeteer.browserRevision}). This can occur if either\n` + `Could not find Chrome (ver. ${this.puppeteer.browserRevision}). This can occur if either\n` +
' 1. you did not perform an installation before running the script (e.g. `npm install`) or\n' + ' 1. you did not perform an installation before running the script (e.g. `npx puppeteer browsers install chrome`) or\n' +
` 2. your cache path is incorrectly configured (which is: ${this.puppeteer.configuration.cacheDirectory}).\n` + ` 2. your cache path is incorrectly configured (which is: ${this.puppeteer.configuration.cacheDirectory}).\n` +
'For (2), check out our guide on configuring puppeteer at https://pptr.dev/guides/configuration.' 'For (2), check out our guide on configuring puppeteer at https://pptr.dev/guides/configuration.'
); );
case 'firefox': case 'firefox':
throw new Error( throw new Error(
`Could not find Firefox (rev. ${this.puppeteer.browserRevision}). This can occur if either\n` + `Could not find Firefox (rev. ${this.puppeteer.browserRevision}). This can occur if either\n` +
' 1. you did not perform an installation for Firefox before running the script (e.g. `PUPPETEER_PRODUCT=firefox npm install`) or\n' + ' 1. you did not perform an installation for Firefox before running the script (e.g. `npx puppeteer browsers install firefox`) or\n' +
` 2. your cache path is incorrectly configured (which is: ${this.puppeteer.configuration.cacheDirectory}).\n` + ` 2. your cache path is incorrectly configured (which is: ${this.puppeteer.configuration.cacheDirectory}).\n` +
'For (2), check out our guide on configuring puppeteer at https://pptr.dev/guides/configuration.' 'For (2), check out our guide on configuring puppeteer at https://pptr.dev/guides/configuration.'
); );

View File

@ -9,6 +9,7 @@
"automation" "automation"
], ],
"type": "commonjs", "type": "commonjs",
"bin": "./lib/esm/puppeteer/node/cli.js",
"main": "./lib/cjs/puppeteer/puppeteer.js", "main": "./lib/cjs/puppeteer/puppeteer.js",
"types": "./lib/types.d.ts", "types": "./lib/types.d.ts",
"exports": { "exports": {
@ -77,7 +78,7 @@
] ]
}, },
"build:tsc": { "build:tsc": {
"command": "tsc -b", "command": "tsc -b && tsx ../../tools/chmod.ts 755 lib/cjs/puppeteer/node/cli.js lib/esm/puppeteer/node/cli.js",
"clean": "if-file-deleted", "clean": "if-file-deleted",
"dependencies": [ "dependencies": [
"../puppeteer-core:build", "../puppeteer-core:build",

View File

@ -0,0 +1,41 @@
#!/usr/bin/env node
/**
* Copyright 2023 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 {CLI, Browser} from '@puppeteer/browsers';
import {PUPPETEER_REVISIONS} from 'puppeteer-core/internal/revisions.js';
import puppeteer from '../puppeteer.js';
// TODO: deprecate downloadPath in favour of cacheDirectory.
const cacheDir =
puppeteer.configuration.downloadPath ??
puppeteer.configuration.cacheDirectory!;
void new CLI({
cachePath: cacheDir,
scriptName: 'puppeteer',
prefixCommand: {
cmd: 'browsers',
description: 'Manage browsers of this Puppeteer installation',
},
allowCachePathOverride: false,
pinnedBrowsers: {
[Browser.CHROME]: PUPPETEER_REVISIONS.chrome,
[Browser.FIREFOX]: PUPPETEER_REVISIONS.firefox,
},
}).run(process.argv);

View File

@ -0,0 +1,68 @@
/**
* Copyright 2023 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 assert from 'assert';
import {spawnSync} from 'child_process';
import {existsSync} from 'fs';
import {readdir} from 'fs/promises';
import {join} from 'path';
import {configureSandbox} from './sandbox.js';
describe('Puppeteer CLI', () => {
configureSandbox({
dependencies: ['@puppeteer/browsers', 'puppeteer-core', 'puppeteer'],
env: cwd => {
return {
PUPPETEER_CACHE_DIR: join(cwd, '.cache', 'puppeteer'),
PUPPETEER_SKIP_DOWNLOAD: 'true',
};
},
});
it('can launch', async function () {
const result = spawnSync('npx', ['puppeteer', '--help'], {
// npx is not found without the shell flag on Windows.
shell: process.platform === 'win32',
cwd: this.sandbox,
});
assert.strictEqual(result.status, 0);
assert.ok(
result.stdout.toString('utf-8').startsWith('puppeteer <command>')
);
});
it('can download a browser', async function () {
assert.ok(!existsSync(join(this.sandbox, '.cache', 'puppeteer')));
const result = spawnSync(
'npx',
['puppeteer', 'browsers', 'install', 'chrome'],
{
// npx is not found without the shell flag on Windows.
shell: process.platform === 'win32',
cwd: this.sandbox,
env: {
...process.env,
PUPPETEER_CACHE_DIR: join(this.sandbox, '.cache', 'puppeteer'),
},
}
);
assert.strictEqual(result.status, 0);
const files = await readdir(join(this.sandbox, '.cache', 'puppeteer'));
assert.equal(files.length, 1);
assert.equal(files[0], 'chrome');
});
});