Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: implement the Puppeteer CLI #11344

Merged
merged 3 commits into from
Nov 23, 2023
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
26 changes: 21 additions & 5 deletions docs/browsers-api/browsers.cli._constructor_.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,29 @@ Constructs a new instance of the `CLI` class

```typescript
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

| Parameter | Type | Description |
| --------- | ------------------ | ------------ |
| cachePath | string | _(Optional)_ |
| rl | readline.Interface | _(Optional)_ |
| Parameter | Type | Description |
| --------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------ |
| 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)_ |
6 changes: 3 additions & 3 deletions docs/browsers-api/browsers.cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,9 @@ export declare class CLI

## Constructors

| Constructor | Modifiers | Description |
| --------------------------------------------------------------- | --------- | ------------------------------------------------------- |
| [(constructor)(cachePath, rl)](./browsers.cli._constructor_.md) | | Constructs a new instance of the <code>CLI</code> class |
| Constructor | Modifiers | Description |
| ---------------------------------------------------------- | --------- | ------------------------------------------------------- |
| [(constructor)(opts, rl)](./browsers.cli._constructor_.md) | | Constructs a new instance of the <code>CLI</code> class |

## Methods

Expand Down
2 changes: 1 addition & 1 deletion docs/contributing.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ again.

```bash
npm install
# Or to download Firefox
# Or to download Firefox by default
PUPPETEER_PRODUCT=firefox npm install
```

Expand Down
6 changes: 6 additions & 0 deletions docs/faq.md
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,12 @@ To fetch Firefox Nightly as part of Puppeteer installation:
PUPPETEER_PRODUCT=firefox npm i puppeteer
```

To download Firefox Nightly into an existing Puppeteer project:

```bash
npx puppeteer browsers install firefox
```

#### Q: What’s considered a “Navigation”?

From Puppeteer’s standpoint, **“navigation” is anything that changes a page’s
Expand Down
3 changes: 3 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

93 changes: 79 additions & 14 deletions packages/browsers/src/CLI.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,10 +68,37 @@ interface ClearArgs {
export class CLI {
#cachePath;
#rl?: readline.Interface;
#scriptName = '';
#allowCachePathOverride = true;
#pinnedBrowsers?: Partial<{[key in Browser]: string}>;
#prefixCommand?: {cmd: string; description: string};

constructor(cachePath = process.cwd(), rl?: readline.Interface) {
this.#cachePath = cachePath;
constructor(
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.#scriptName = opts.scriptName ?? '@puppeteer/browsers';
this.#allowCachePathOverride = opts.allowCachePathOverride ?? true;
this.#pinnedBrowsers = opts.pinnedBrowsers;
this.#prefixCommand = opts.prefixCommand;
}

#defineBrowserParameter(yargs: Yargs.Argv<unknown>): void {
Expand All @@ -98,6 +125,9 @@ export class CLI {
}

#definePathParameter(yargs: Yargs.Argv<unknown>, required = false): void {
if (!this.#allowCachePathOverride) {
return;
}
yargs.option('path', {
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.',
Expand All @@ -111,8 +141,28 @@ export class CLI {

async run(argv: string[]): Promise<void> {
const yargsInstance = yargs(hideBin(argv));
await yargsInstance
.scriptName('@puppeteer/browsers')
let target = yargsInstance.scriptName(this.#scriptName);
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(
'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>).',
Expand All @@ -126,7 +176,7 @@ export class CLI {
});
yargs.example(
'$0 install chrome',
'Install the latest available build of the Chrome browser.'
`Install the ${latestOrPinned} available build of the Chrome browser.`
);
yargs.example(
'$0 install chrome@latest',
Expand Down Expand Up @@ -176,17 +226,28 @@ export class CLI {
'$0 install firefox --platform mac',
'Install the latest Mac (Intel) build of the Firefox browser.'
);
yargs.example(
'$0 install firefox --path /tmp/my-browser-cache',
'Install to the specified cache directory.'
);
if (this.#allowCachePathOverride) {
yargs.example(
'$0 install firefox --path /tmp/my-browser-cache',
'Install to the specified cache directory.'
);
}
},
async argv => {
const args = argv as unknown as InstallArgs;
args.platform ??= detectBrowserPlatform();
if (!args.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.name,
args.platform,
Expand Down Expand Up @@ -272,7 +333,9 @@ export class CLI {
)
.command(
'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 => {
this.#definePathParameter(yargs, true);
},
Expand All @@ -296,9 +359,7 @@ export class CLI {
}
)
.demandCommand(1)
.help()
.wrap(Math.min(120, yargsInstance.terminalWidth()))
.parse();
.help();
}

#parseBrowser(version: string): Browser {
Expand All @@ -307,7 +368,11 @@ export class CLI {

#parseBuildId(version: string): string {
const parts = version.split('@');
return parts.length === 2 ? parts[1]! : 'latest';
return parts.length === 2
? parts[1]!
: this.#pinnedBrowsers
? 'pinned'
: 'latest';
}
}

Expand Down
4 changes: 2 additions & 2 deletions packages/puppeteer-core/src/node/ProductLauncher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -423,14 +423,14 @@ export abstract class ProductLauncher {
case 'chrome':
throw new Error(
`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` +
'For (2), check out our guide on configuring puppeteer at https://pptr.dev/guides/configuration.'
);
case 'firefox':
throw new Error(
`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` +
'For (2), check out our guide on configuring puppeteer at https://pptr.dev/guides/configuration.'
);
Expand Down
3 changes: 2 additions & 1 deletion packages/puppeteer/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
"automation"
],
"type": "commonjs",
"bin": "./lib/esm/puppeteer/node/cli.js",
"main": "./lib/cjs/puppeteer/puppeteer.js",
"types": "./lib/types.d.ts",
"exports": {
Expand Down Expand Up @@ -77,7 +78,7 @@
]
},
"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",
"dependencies": [
"../puppeteer-core:build",
Expand Down
41 changes: 41 additions & 0 deletions packages/puppeteer/src/node/cli.ts
Original file line number Diff line number Diff line change
@@ -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);