diff --git a/docs/api.md b/docs/api.md index 48d52c83..3168c5d1 100644 --- a/docs/api.md +++ b/docs/api.md @@ -454,7 +454,7 @@ When using `puppeteer-core`, remember to change the _include_ line: const puppeteer = require('puppeteer-core'); ``` -You will then need to call [`puppeteer.connect([options])`](#puppeteerconnectoptions) or [`puppeteer.launch([options])`](#puppeteerlaunchoptions) with an explicit `executablePath` option. +You will then need to call [`puppeteer.connect([options])`](#puppeteerconnectoptions) or [`puppeteer.launch([options])`](#puppeteerlaunchoptions) with an explicit `executablePath` or `channel` option. ### Environment Variables @@ -627,6 +627,7 @@ try { - `product` <[string]> Which browser to launch. At this time, this is either `chrome` or `firefox`. See also `PUPPETEER_PRODUCT`. - `ignoreHTTPSErrors` <[boolean]> Whether to ignore HTTPS errors during navigation. Defaults to `false`. - `headless` <[boolean]> Whether to run browser in [headless mode](https://developers.google.com/web/updates/2017/04/headless-chrome). Defaults to `true` unless the `devtools` option is `true`. + - `channel` <[string]> When specified, Puppeteer will search for the locally installed release channel of Google Chrome and use it to launch. Available values are `chrome`, `chrome-beta`, `chrome-canary`, `chrome-dev`. When channel is specified, `executablePath` cannot be specified. - `executablePath` <[string]> Path to a browser executable to run instead of the bundled Chromium. If `executablePath` is a relative path, then it is resolved relative to [current working directory](https://nodejs.org/api/process.html#process_process_cwd). **BEWARE**: Puppeteer is only [guaranteed to work](https://github.com/puppeteer/puppeteer/#q-why-doesnt-puppeteer-vxxx-work-with-chromium-vyyy) with the bundled Chromium, use at your own risk. - `slowMo` <[number]> Slows down Puppeteer operations by the specified amount of milliseconds. Useful so that you can see what is going on. - `defaultViewport` Sets a consistent viewport for each page. Defaults to an 800x600 viewport. `null` disables the default viewport. @@ -660,7 +661,7 @@ const browser = await puppeteer.launch({ }); ``` -> **NOTE** Puppeteer can also be used to control the Chrome browser, but it works best with the version of Chromium it is bundled with. There is no guarantee it will work with any other version. Use `executablePath` option with extreme caution. +> **NOTE** Puppeteer can also be used to control the Chrome browser, but it works best with the version of Chromium it is bundled with. There is no guarantee it will work with any other version. Use `executablePath` or `channel` option with extreme caution. > > If Google Chrome (rather than Chromium) is preferred, a [Chrome Canary](https://www.google.com/chrome/browser/canary.html) or [Dev Channel](https://www.chromium.org/getting-involved/dev-channel) build is suggested. > diff --git a/src/node/LaunchOptions.ts b/src/node/LaunchOptions.ts index c211c565..e132598c 100644 --- a/src/node/LaunchOptions.ts +++ b/src/node/LaunchOptions.ts @@ -46,11 +46,21 @@ export interface BrowserLaunchArgumentOptions { args?: string[]; } +export type ChromeReleaseChannel = + | 'chrome' + | 'chrome-beta' + | 'chrome-canary' + | 'chrome-dev'; + /** * Generic launch options that can be passed when launching any browser. * @public */ export interface LaunchOptions { + /** + * Chrome Release Channel + */ + channel?: ChromeReleaseChannel; /** * Path to a browser executable to use instead of the bundled Chromium. Note * that Puppeteer is only guaranteed to work with the bundled Chromium, so use diff --git a/src/node/Launcher.ts b/src/node/Launcher.ts index 0b934d5d..402ae0ad 100644 --- a/src/node/Launcher.ts +++ b/src/node/Launcher.ts @@ -17,6 +17,7 @@ import * as os from 'os'; import * as path from 'path'; import * as fs from 'fs'; +import { assert } from '../common/assert.js'; import { BrowserFetcher } from './BrowserFetcher.js'; import { Browser } from '../common/Browser.js'; import { BrowserRunner } from './BrowserRunner.js'; @@ -27,6 +28,7 @@ const writeFileAsync = promisify(fs.writeFile); import { BrowserLaunchArgumentOptions, + ChromeReleaseChannel, PuppeteerNodeLaunchOptions, } from './LaunchOptions.js'; import { Product } from '../common/Product.js'; @@ -37,7 +39,7 @@ import { Product } from '../common/Product.js'; */ export interface ProductLauncher { launch(object: PuppeteerNodeLaunchOptions); - executablePath: () => string; + executablePath: (string?) => string; defaultArgs(object: BrowserLaunchArgumentOptions); product: Product; } @@ -65,6 +67,7 @@ class ChromeLauncher implements ProductLauncher { ignoreDefaultArgs = false, args = [], dumpio = false, + channel = null, executablePath = null, pipe = false, env = process.env, @@ -105,7 +108,16 @@ class ChromeLauncher implements ProductLauncher { } let chromeExecutable = executablePath; - if (!executablePath) { + + if (channel) { + // executablePath is detected by channel, so it should not be specified by user. + assert( + !executablePath, + '`executablePath` must not be specified when `channel` is given.' + ); + + chromeExecutable = executablePathForChannel(channel); + } else if (!executablePath) { // Use Intel x86 builds on Apple M1 until native macOS arm64 // Chromium builds are available. if (os.platform() !== 'darwin' && os.arch() === 'arm64') { @@ -204,8 +216,12 @@ class ChromeLauncher implements ProductLauncher { return chromeArguments; } - executablePath(): string { - return resolveExecutablePath(this).executablePath; + executablePath(channel?: ChromeReleaseChannel): string { + if (channel) { + return executablePathForChannel(channel); + } else { + return resolveExecutablePath(this).executablePath; + } } get product(): Product { @@ -587,6 +603,80 @@ class FirefoxLauncher implements ProductLauncher { } } +function executablePathForChannel(channel: ChromeReleaseChannel): string { + const platform = os.platform(); + + let chromePath: string | undefined; + switch (platform) { + case 'win32': + switch (channel) { + case 'chrome': + chromePath = `${process.env.PROGRAMFILES}\\Google\\Chrome\\Application\\chrome.exe`; + break; + case 'chrome-beta': + chromePath = `${process.env.PROGRAMFILES}\\Google\\Chrome Beta\\Application\\chrome.exe`; + break; + case 'chrome-canary': + chromePath = `${process.env.PROGRAMFILES}\\Google\\Chrome SxS\\Application\\chrome.exe`; + break; + case 'chrome-dev': + chromePath = `${process.env.PROGRAMFILES}\\Google\\Chrome Dev\\Application\\chrome.exe`; + break; + } + break; + case 'darwin': + switch (channel) { + case 'chrome': + chromePath = + '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome'; + break; + case 'chrome-beta': + chromePath = + '/Applications/Google Chrome Beta.app/Contents/MacOS/Google Chrome Beta'; + break; + case 'chrome-canary': + chromePath = + '/Applications/Google Chrome Canary.app/Contents/MacOS/Google Chrome Canary'; + break; + case 'chrome-dev': + chromePath = + '/Applications/Google Chrome Dev.app/Contents/MacOS/Google Chrome Dev'; + break; + } + break; + case 'linux': + switch (channel) { + case 'chrome': + chromePath = '/opt/google/chrome/chrome'; + break; + case 'chrome-beta': + chromePath = '/opt/google/chrome-beta/chrome'; + break; + case 'chrome-dev': + chromePath = '/opt/google/chrome-unstable/chrome'; + break; + } + break; + } + + if (!chromePath) { + throw new Error( + `Unable to detect browser executable path for '${channel}' on ${platform}.` + ); + } + + // Check if Chrome exists and is accessible. + try { + fs.accessSync(chromePath); + } catch (error) { + throw new Error( + `Could not find Google Chrome executable for channel '${channel}' at '${chromePath}'.` + ); + } + + return chromePath; +} + function resolveExecutablePath(launcher: ChromeLauncher | FirefoxLauncher): { executablePath: string; missingText?: string; diff --git a/src/node/Puppeteer.ts b/src/node/Puppeteer.ts index fd4dbe1d..c71a3b1c 100644 --- a/src/node/Puppeteer.ts +++ b/src/node/Puppeteer.ts @@ -165,8 +165,8 @@ export class PuppeteerNode extends Puppeteer { * The browser binary might not be there if the download was skipped with * the `PUPPETEER_SKIP_DOWNLOAD` environment variable. */ - executablePath(): string { - return this._launcher.executablePath(); + executablePath(channel?: string): string { + return this._launcher.executablePath(channel); } /** diff --git a/test/launcher.spec.ts b/test/launcher.spec.ts index b4347929..5737d887 100644 --- a/test/launcher.spec.ts +++ b/test/launcher.spec.ts @@ -660,6 +660,12 @@ describe('Launcher specs', function () { expect(fs.existsSync(executablePath)).toBe(true); expect(fs.realpathSync(executablePath)).toBe(executablePath); }); + it('returns executablePath for channel', () => { + const { puppeteer } = getTestState(); + + const executablePath = puppeteer.executablePath('chrome'); + expect(executablePath).toBeTruthy(); + }); }); });