1
0

add initial marp implementation with sample content and build configuration

This commit is contained in:
2025-09-13 18:13:22 +02:00
parent dcacc9b409
commit e5f219507f
10319 changed files with 1402023 additions and 0 deletions

554
node_modules/@puppeteer/browsers/src/CLI.ts generated vendored Normal file
View File

@@ -0,0 +1,554 @@
/**
* @license
* Copyright 2023 Google Inc.
* SPDX-License-Identifier: Apache-2.0
*/
import {stdin as input, stdout as output} from 'node:process';
import * as readline from 'node:readline';
import type * as Yargs from 'yargs';
import {
Browser,
resolveBuildId,
BrowserPlatform,
type ChromeReleaseChannel,
} from './browser-data/browser-data.js';
import {Cache} from './Cache.js';
import {detectBrowserPlatform} from './detectPlatform.js';
import {packageVersion} from './generated/version.js';
import {install} from './install.js';
import {
computeExecutablePath,
computeSystemExecutablePath,
launch,
} from './launch.js';
interface InstallBrowser {
name: Browser;
buildId: string;
}
interface InstallArgs {
browser?: InstallBrowser;
path?: string;
platform?: BrowserPlatform;
baseUrl?: string;
installDeps?: boolean;
}
function isValidBrowser(browser: unknown): browser is Browser {
return Object.values(Browser).includes(browser as Browser);
}
function isValidPlatform(platform: unknown): platform is BrowserPlatform {
return Object.values(BrowserPlatform).includes(platform as BrowserPlatform);
}
/**
* @public
*/
export class CLI {
#cachePath: string;
#rl?: readline.Interface;
#scriptName: string;
#version: string;
#allowCachePathOverride: boolean;
#pinnedBrowsers?: Partial<
Record<
Browser,
{
buildId: string;
skipDownload: boolean;
}
>
>;
#prefixCommand?: {cmd: string; description: string};
constructor(
opts?:
| string
| {
cachePath?: string;
scriptName?: string;
version?: string;
prefixCommand?: {cmd: string; description: string};
allowCachePathOverride?: boolean;
pinnedBrowsers?: Partial<
Record<
Browser,
{
buildId: string;
skipDownload: boolean;
}
>
>;
},
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.#version = opts.version ?? packageVersion;
this.#allowCachePathOverride = opts.allowCachePathOverride ?? true;
this.#pinnedBrowsers = opts.pinnedBrowsers;
this.#prefixCommand = opts.prefixCommand;
}
#defineBrowserParameter<T>(
yargs: Yargs.Argv<T>,
required: true,
): Yargs.Argv<T & {browser: InstallBrowser}>;
#defineBrowserParameter<T>(
yargs: Yargs.Argv<T>,
required: boolean,
): Yargs.Argv<T & {browser: InstallBrowser | undefined}>;
#defineBrowserParameter<T>(yargs: Yargs.Argv<T>, required: boolean) {
return yargs.positional('browser', {
description:
'Which browser to install <browser>[@<buildId|latest>]. `latest` will try to find the latest available build. `buildId` is a browser-specific identifier such as a version or a revision.',
type: 'string',
coerce: (opt): InstallBrowser => {
const browser: InstallBrowser = {
name: this.#parseBrowser(opt),
buildId: this.#parseBuildId(opt),
};
if (!isValidBrowser(browser.name)) {
throw new Error(`Unsupported browser '${browser.name}'`);
}
return browser;
},
demandOption: required,
});
}
#definePlatformParameter<T>(yargs: Yargs.Argv<T>) {
return yargs.option('platform', {
type: 'string',
desc: 'Platform that the binary needs to be compatible with.',
choices: Object.values(BrowserPlatform),
default: detectBrowserPlatform(),
coerce: platform => {
if (!isValidPlatform(platform)) {
throw new Error(`Unsupported platform '${platform}'`);
}
return platform;
},
defaultDescription: 'Auto-detected',
});
}
#definePathParameter<T>(yargs: Yargs.Argv<T>, required = false) {
if (!this.#allowCachePathOverride) {
return yargs as Yargs.Argv<T & {path: undefined}>;
}
return yargs.option('path', {
type: 'string',
desc: 'Path to the root folder for the browser downloads and installation. If a relative path is provided, it will be resolved relative to the current working directory. The installation folder structure is compatible with the cache structure used by Puppeteer.',
defaultDescription: 'Current working directory',
...(required ? {} : {default: process.cwd()}),
demandOption: required,
});
}
async run(argv: string[]): Promise<void> {
const {default: yargs} = await import('yargs');
const {hideBin} = await import('yargs/helpers');
const yargsInstance = yargs(hideBin(argv));
let target = yargsInstance
.scriptName(this.#scriptName)
.version(this.#version);
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()))
.parseAsync();
}
#build(yargs: Yargs.Argv<unknown>) {
const latestOrPinned = this.#pinnedBrowsers ? 'pinned' : 'latest';
// If there are pinned browsers allow the positional arg to be optional
const browserArgType = this.#pinnedBrowsers ? '[browser]' : '<browser>';
return yargs
.command(
`install ${browserArgType}`,
'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>).',
yargs => {
if (this.#pinnedBrowsers) {
yargs.example('$0 install', 'Install all pinned browsers');
}
yargs
.example(
'$0 install chrome',
`Install the ${latestOrPinned} available build of the Chrome browser.`,
)
.example(
'$0 install chrome@latest',
'Install the latest available build for the Chrome browser.',
)
.example(
'$0 install chrome@stable',
'Install the latest available build for the Chrome browser from the stable channel.',
)
.example(
'$0 install chrome@beta',
'Install the latest available build for the Chrome browser from the beta channel.',
)
.example(
'$0 install chrome@dev',
'Install the latest available build for the Chrome browser from the dev channel.',
)
.example(
'$0 install chrome@canary',
'Install the latest available build for the Chrome Canary browser.',
)
.example(
'$0 install chrome@115',
'Install the latest available build for Chrome 115.',
)
.example(
'$0 install chromedriver@canary',
'Install the latest available build for ChromeDriver Canary.',
)
.example(
'$0 install chromedriver@115',
'Install the latest available build for ChromeDriver 115.',
)
.example(
'$0 install chromedriver@115.0.5790',
'Install the latest available patch (115.0.5790.X) build for ChromeDriver.',
)
.example(
'$0 install chrome-headless-shell',
'Install the latest available chrome-headless-shell build.',
)
.example(
'$0 install chrome-headless-shell@beta',
'Install the latest available chrome-headless-shell build corresponding to the Beta channel.',
)
.example(
'$0 install chrome-headless-shell@118',
'Install the latest available chrome-headless-shell 118 build.',
)
.example(
'$0 install chromium@1083080',
'Install the revision 1083080 of the Chromium browser.',
)
.example(
'$0 install firefox',
'Install the latest nightly available build of the Firefox browser.',
)
.example(
'$0 install firefox@stable',
'Install the latest stable build of the Firefox browser.',
)
.example(
'$0 install firefox@beta',
'Install the latest beta build of the Firefox browser.',
)
.example(
'$0 install firefox@devedition',
'Install the latest devedition build of the Firefox browser.',
)
.example(
'$0 install firefox@esr',
'Install the latest ESR build of the Firefox browser.',
)
.example(
'$0 install firefox@nightly',
'Install the latest nightly build of the Firefox browser.',
)
.example(
'$0 install firefox@stable_111.0.1',
'Install a specific version of the Firefox browser.',
)
.example(
'$0 install firefox --platform mac',
'Install the latest Mac (Intel) build of the Firefox browser.',
);
if (this.#allowCachePathOverride) {
yargs.example(
'$0 install firefox --path /tmp/my-browser-cache',
'Install to the specified cache directory.',
);
}
const yargsWithBrowserParam = this.#defineBrowserParameter(
yargs,
!Boolean(this.#pinnedBrowsers),
);
const yargsWithPlatformParam = this.#definePlatformParameter(
yargsWithBrowserParam,
);
return this.#definePathParameter(yargsWithPlatformParam, false)
.option('base-url', {
type: 'string',
desc: 'Base URL to download from',
})
.option('install-deps', {
type: 'boolean',
desc: 'Whether to attempt installing system dependencies (only supported on Linux, requires root privileges).',
default: false,
});
},
async args => {
if (this.#pinnedBrowsers && !args.browser) {
// Use allSettled to avoid scenarios that
// a browser may fail early and leave the other
// installation in a faulty state
const result = await Promise.allSettled(
Object.entries(this.#pinnedBrowsers).map(
async ([browser, options]) => {
if (options.skipDownload) {
return;
}
await this.#install({
...args,
browser: {
name: browser as Browser,
buildId: options.buildId,
},
});
},
),
);
for (const install of result) {
if (install.status === 'rejected') {
throw install.reason;
}
}
} else {
await this.#install(args);
}
},
)
.command(
'launch <browser>',
'Launch the specified browser',
yargs => {
yargs
.example(
'$0 launch chrome@115.0.5790.170',
'Launch Chrome 115.0.5790.170',
)
.example(
'$0 launch firefox@112.0a1',
'Launch the Firefox browser identified by the milestone 112.0a1.',
)
.example(
'$0 launch chrome@115.0.5790.170 --detached',
'Launch the browser but detach the sub-processes.',
)
.example(
'$0 launch chrome@canary --system',
'Try to locate the Canary build of Chrome installed on the system and launch it.',
)
.example(
'$0 launch chrome@115.0.5790.170 -- --version',
'Launch Chrome 115.0.5790.170 and pass custom argument to the binary.',
);
const yargsWithExtraAgs = yargs.parserConfiguration({
'populate--': true,
// Yargs does not have the correct overload for this.
}) as Yargs.Argv<{'--'?: Array<string | number>}>;
const yargsWithBrowserParam = this.#defineBrowserParameter(
yargsWithExtraAgs,
true,
);
const yargsWithPlatformParam = this.#definePlatformParameter(
yargsWithBrowserParam,
);
return this.#definePathParameter(yargsWithPlatformParam)
.option('detached', {
type: 'boolean',
desc: 'Detach the child process.',
default: false,
})
.option('system', {
type: 'boolean',
desc: 'Search for a browser installed on the system instead of the cache folder.',
default: false,
})
.option('dumpio', {
type: 'boolean',
desc: "Forwards the browser's process stdout and stderr",
default: false,
});
},
async args => {
const extraArgs = args['--']?.filter(arg => {
return typeof arg === 'string';
});
args.browser.buildId = this.#resolvePinnedBrowserIfNeeded(
args.browser.buildId,
args.browser.name,
);
const executablePath = args.system
? computeSystemExecutablePath({
browser: args.browser.name,
// TODO: throw an error if not a ChromeReleaseChannel is provided.
channel: args.browser.buildId as ChromeReleaseChannel,
platform: args.platform,
})
: computeExecutablePath({
browser: args.browser.name,
buildId: args.browser.buildId,
cacheDir: args.path ?? this.#cachePath,
platform: args.platform,
});
launch({
args: extraArgs,
executablePath,
dumpio: args.dumpio,
detached: args.detached,
});
},
)
.command(
'clear',
this.#allowCachePathOverride
? 'Removes all installed browsers from the specified cache directory'
: `Removes all installed browsers from ${this.#cachePath}`,
yargs => {
return this.#definePathParameter(yargs, true);
},
async args => {
const cacheDir = args.path ?? this.#cachePath;
const rl = this.#rl ?? readline.createInterface({input, output});
rl.question(
`Do you want to permanently and recursively delete the content of ${cacheDir} (yes/No)? `,
answer => {
rl.close();
if (!['y', 'yes'].includes(answer.toLowerCase().trim())) {
console.log('Cancelled.');
return;
}
const cache = new Cache(cacheDir);
cache.clear();
console.log(`${cacheDir} cleared.`);
},
);
},
)
.command(
'list',
'List all installed browsers in the cache directory',
yargs => {
yargs.example(
'$0 list',
'List all installed browsers in the cache directory',
);
if (this.#allowCachePathOverride) {
yargs.example(
'$0 list --path /tmp/my-browser-cache',
'List browsers installed in the specified cache directory',
);
}
return this.#definePathParameter(yargs);
},
async args => {
const cacheDir = args.path ?? this.#cachePath;
const cache = new Cache(cacheDir);
const browsers = cache.getInstalledBrowsers();
for (const browser of browsers) {
console.log(
`${browser.browser}@${browser.buildId} (${browser.platform}) ${browser.executablePath}`,
);
}
},
)
.demandCommand(1)
.help();
}
#parseBrowser(version: string): Browser {
return version.split('@').shift() as Browser;
}
#parseBuildId(version: string): string {
const parts = version.split('@');
return parts.length === 2
? parts[1]!
: this.#pinnedBrowsers
? 'pinned'
: 'latest';
}
#resolvePinnedBrowserIfNeeded(buildId: string, browserName: Browser): string {
if (buildId === 'pinned') {
const options = this.#pinnedBrowsers?.[browserName];
if (!options || !options.buildId) {
throw new Error(`No pinned version found for ${browserName}`);
}
return options.buildId;
}
return buildId;
}
async #install(args: InstallArgs) {
if (!args.browser) {
throw new Error(`No browser arg provided`);
}
if (!args.platform) {
throw new Error(`Could not resolve the current platform`);
}
args.browser.buildId = this.#resolvePinnedBrowserIfNeeded(
args.browser.buildId,
args.browser.name,
);
const originalBuildId = args.browser.buildId;
args.browser.buildId = await resolveBuildId(
args.browser.name,
args.platform,
args.browser.buildId,
);
await install({
browser: args.browser.name,
buildId: args.browser.buildId,
platform: args.platform,
cacheDir: args.path ?? this.#cachePath,
downloadProgressCallback: 'default',
baseUrl: args.baseUrl,
buildIdAlias:
originalBuildId !== args.browser.buildId ? originalBuildId : undefined,
installDeps: args.installDeps,
});
console.log(
`${args.browser.name}@${args.browser.buildId} ${computeExecutablePath({
browser: args.browser.name,
buildId: args.browser.buildId,
cacheDir: args.path ?? this.#cachePath,
platform: args.platform,
})}`,
);
}
}

277
node_modules/@puppeteer/browsers/src/Cache.ts generated vendored Normal file
View File

@@ -0,0 +1,277 @@
/**
* @license
* Copyright 2023 Google Inc.
* SPDX-License-Identifier: Apache-2.0
*/
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import debug from 'debug';
import {
Browser,
type BrowserPlatform,
executablePathByBrowser,
getVersionComparator,
} from './browser-data/browser-data.js';
import {detectBrowserPlatform} from './detectPlatform.js';
const debugCache = debug('puppeteer:browsers:cache');
/**
* @public
*/
export class InstalledBrowser {
browser: Browser;
buildId: string;
platform: BrowserPlatform;
readonly executablePath: string;
#cache: Cache;
/**
* @internal
*/
constructor(
cache: Cache,
browser: Browser,
buildId: string,
platform: BrowserPlatform,
) {
this.#cache = cache;
this.browser = browser;
this.buildId = buildId;
this.platform = platform;
this.executablePath = cache.computeExecutablePath({
browser,
buildId,
platform,
});
}
/**
* Path to the root of the installation folder. Use
* {@link computeExecutablePath} to get the path to the executable binary.
*/
get path(): string {
return this.#cache.installationDir(
this.browser,
this.platform,
this.buildId,
);
}
readMetadata(): Metadata {
return this.#cache.readMetadata(this.browser);
}
writeMetadata(metadata: Metadata): void {
this.#cache.writeMetadata(this.browser, metadata);
}
}
/**
* @internal
*/
export interface ComputeExecutablePathOptions {
/**
* Determines which platform the browser will be suited for.
*
* @defaultValue **Auto-detected.**
*/
platform?: BrowserPlatform;
/**
* Determines which browser to launch.
*/
browser: Browser;
/**
* Determines which buildId to download. BuildId should uniquely identify
* binaries and they are used for caching.
*/
buildId: string;
}
/**
* @public
*/
export interface Metadata {
// Maps an alias (canary/latest/dev/etc.) to a buildId.
aliases: Record<string, string>;
}
/**
* The cache used by Puppeteer relies on the following structure:
*
* - rootDir
* -- <browser1> | browserRoot(browser1)
* ---- <platform>-<buildId> | installationDir()
* ------ the browser-platform-buildId
* ------ specific structure.
* -- <browser2> | browserRoot(browser2)
* ---- <platform>-<buildId> | installationDir()
* ------ the browser-platform-buildId
* ------ specific structure.
* @internal
*/
export class Cache {
#rootDir: string;
constructor(rootDir: string) {
this.#rootDir = rootDir;
}
/**
* @internal
*/
get rootDir(): string {
return this.#rootDir;
}
browserRoot(browser: Browser): string {
return path.join(this.#rootDir, browser);
}
metadataFile(browser: Browser): string {
return path.join(this.browserRoot(browser), '.metadata');
}
readMetadata(browser: Browser): Metadata {
const metatadaPath = this.metadataFile(browser);
if (!fs.existsSync(metatadaPath)) {
return {aliases: {}};
}
// TODO: add type-safe parsing.
const data = JSON.parse(fs.readFileSync(metatadaPath, 'utf8'));
if (typeof data !== 'object') {
throw new Error('.metadata is not an object');
}
return data;
}
writeMetadata(browser: Browser, metadata: Metadata): void {
const metatadaPath = this.metadataFile(browser);
fs.mkdirSync(path.dirname(metatadaPath), {recursive: true});
fs.writeFileSync(metatadaPath, JSON.stringify(metadata, null, 2));
}
resolveAlias(browser: Browser, alias: string): string | undefined {
const metadata = this.readMetadata(browser);
if (alias === 'latest') {
return Object.values(metadata.aliases || {})
.sort(getVersionComparator(browser))
.at(-1);
}
return metadata.aliases[alias];
}
installationDir(
browser: Browser,
platform: BrowserPlatform,
buildId: string,
): string {
return path.join(this.browserRoot(browser), `${platform}-${buildId}`);
}
clear(): void {
fs.rmSync(this.#rootDir, {
force: true,
recursive: true,
maxRetries: 10,
retryDelay: 500,
});
}
uninstall(
browser: Browser,
platform: BrowserPlatform,
buildId: string,
): void {
const metadata = this.readMetadata(browser);
for (const alias of Object.keys(metadata.aliases)) {
if (metadata.aliases[alias] === buildId) {
delete metadata.aliases[alias];
}
}
fs.rmSync(this.installationDir(browser, platform, buildId), {
force: true,
recursive: true,
maxRetries: 10,
retryDelay: 500,
});
}
getInstalledBrowsers(): InstalledBrowser[] {
if (!fs.existsSync(this.#rootDir)) {
return [];
}
const types = fs.readdirSync(this.#rootDir);
const browsers = types.filter((t): t is Browser => {
return (Object.values(Browser) as string[]).includes(t);
});
return browsers.flatMap(browser => {
const files = fs.readdirSync(this.browserRoot(browser));
return files
.map(file => {
const result = parseFolderPath(
path.join(this.browserRoot(browser), file),
);
if (!result) {
return null;
}
return new InstalledBrowser(
this,
browser,
result.buildId,
result.platform as BrowserPlatform,
);
})
.filter((item: InstalledBrowser | null): item is InstalledBrowser => {
return item !== null;
});
});
}
computeExecutablePath(options: ComputeExecutablePathOptions): string {
options.platform ??= detectBrowserPlatform();
if (!options.platform) {
throw new Error(
`Cannot download a binary for the provided platform: ${os.platform()} (${os.arch()})`,
);
}
try {
options.buildId =
this.resolveAlias(options.browser, options.buildId) ?? options.buildId;
} catch {
debugCache('could not read .metadata file for the browser');
}
const installationDir = this.installationDir(
options.browser,
options.platform,
options.buildId,
);
return path.join(
installationDir,
executablePathByBrowser[options.browser](
options.platform,
options.buildId,
),
);
}
}
function parseFolderPath(
folderPath: string,
): {platform: string; buildId: string} | undefined {
const name = path.basename(folderPath);
const splits = name.split('-');
if (splits.length !== 2) {
return;
}
const [platform, buildId] = splits;
if (!buildId || !platform) {
return;
}
return {platform, buildId};
}

View File

@@ -0,0 +1,252 @@
/**
* @license
* Copyright 2023 Google Inc.
* SPDX-License-Identifier: Apache-2.0
*/
import * as chromeHeadlessShell from './chrome-headless-shell.js';
import * as chrome from './chrome.js';
import * as chromedriver from './chromedriver.js';
import * as chromium from './chromium.js';
import * as firefox from './firefox.js';
import {
Browser,
BrowserPlatform,
BrowserTag,
ChromeReleaseChannel,
type ProfileOptions,
} from './types.js';
export type {ProfileOptions};
export const downloadUrls = {
[Browser.CHROMEDRIVER]: chromedriver.resolveDownloadUrl,
[Browser.CHROMEHEADLESSSHELL]: chromeHeadlessShell.resolveDownloadUrl,
[Browser.CHROME]: chrome.resolveDownloadUrl,
[Browser.CHROMIUM]: chromium.resolveDownloadUrl,
[Browser.FIREFOX]: firefox.resolveDownloadUrl,
};
export const downloadPaths = {
[Browser.CHROMEDRIVER]: chromedriver.resolveDownloadPath,
[Browser.CHROMEHEADLESSSHELL]: chromeHeadlessShell.resolveDownloadPath,
[Browser.CHROME]: chrome.resolveDownloadPath,
[Browser.CHROMIUM]: chromium.resolveDownloadPath,
[Browser.FIREFOX]: firefox.resolveDownloadPath,
};
export const executablePathByBrowser = {
[Browser.CHROMEDRIVER]: chromedriver.relativeExecutablePath,
[Browser.CHROMEHEADLESSSHELL]: chromeHeadlessShell.relativeExecutablePath,
[Browser.CHROME]: chrome.relativeExecutablePath,
[Browser.CHROMIUM]: chromium.relativeExecutablePath,
[Browser.FIREFOX]: firefox.relativeExecutablePath,
};
export const versionComparators = {
[Browser.CHROMEDRIVER]: chromedriver.compareVersions,
[Browser.CHROMEHEADLESSSHELL]: chromeHeadlessShell.compareVersions,
[Browser.CHROME]: chrome.compareVersions,
[Browser.CHROMIUM]: chromium.compareVersions,
[Browser.FIREFOX]: firefox.compareVersions,
};
export {Browser, BrowserPlatform, ChromeReleaseChannel};
/**
* @internal
*/
async function resolveBuildIdForBrowserTag(
browser: Browser,
platform: BrowserPlatform,
tag: BrowserTag,
): Promise<string> {
switch (browser) {
case Browser.FIREFOX:
switch (tag) {
case BrowserTag.LATEST:
return await firefox.resolveBuildId(firefox.FirefoxChannel.NIGHTLY);
case BrowserTag.BETA:
return await firefox.resolveBuildId(firefox.FirefoxChannel.BETA);
case BrowserTag.NIGHTLY:
return await firefox.resolveBuildId(firefox.FirefoxChannel.NIGHTLY);
case BrowserTag.DEVEDITION:
return await firefox.resolveBuildId(
firefox.FirefoxChannel.DEVEDITION,
);
case BrowserTag.STABLE:
return await firefox.resolveBuildId(firefox.FirefoxChannel.STABLE);
case BrowserTag.ESR:
return await firefox.resolveBuildId(firefox.FirefoxChannel.ESR);
case BrowserTag.CANARY:
case BrowserTag.DEV:
throw new Error(`${tag.toUpperCase()} is not available for Firefox`);
}
case Browser.CHROME: {
switch (tag) {
case BrowserTag.LATEST:
return await chrome.resolveBuildId(ChromeReleaseChannel.CANARY);
case BrowserTag.BETA:
return await chrome.resolveBuildId(ChromeReleaseChannel.BETA);
case BrowserTag.CANARY:
return await chrome.resolveBuildId(ChromeReleaseChannel.CANARY);
case BrowserTag.DEV:
return await chrome.resolveBuildId(ChromeReleaseChannel.DEV);
case BrowserTag.STABLE:
return await chrome.resolveBuildId(ChromeReleaseChannel.STABLE);
case BrowserTag.NIGHTLY:
case BrowserTag.DEVEDITION:
case BrowserTag.ESR:
throw new Error(`${tag.toUpperCase()} is not available for Chrome`);
}
}
case Browser.CHROMEDRIVER: {
switch (tag) {
case BrowserTag.LATEST:
case BrowserTag.CANARY:
return await chromedriver.resolveBuildId(ChromeReleaseChannel.CANARY);
case BrowserTag.BETA:
return await chromedriver.resolveBuildId(ChromeReleaseChannel.BETA);
case BrowserTag.DEV:
return await chromedriver.resolveBuildId(ChromeReleaseChannel.DEV);
case BrowserTag.STABLE:
return await chromedriver.resolveBuildId(ChromeReleaseChannel.STABLE);
case BrowserTag.NIGHTLY:
case BrowserTag.DEVEDITION:
case BrowserTag.ESR:
throw new Error(
`${tag.toUpperCase()} is not available for ChromeDriver`,
);
}
}
case Browser.CHROMEHEADLESSSHELL: {
switch (tag) {
case BrowserTag.LATEST:
case BrowserTag.CANARY:
return await chromeHeadlessShell.resolveBuildId(
ChromeReleaseChannel.CANARY,
);
case BrowserTag.BETA:
return await chromeHeadlessShell.resolveBuildId(
ChromeReleaseChannel.BETA,
);
case BrowserTag.DEV:
return await chromeHeadlessShell.resolveBuildId(
ChromeReleaseChannel.DEV,
);
case BrowserTag.STABLE:
return await chromeHeadlessShell.resolveBuildId(
ChromeReleaseChannel.STABLE,
);
case BrowserTag.NIGHTLY:
case BrowserTag.DEVEDITION:
case BrowserTag.ESR:
throw new Error(`${tag} is not available for chrome-headless-shell`);
}
}
case Browser.CHROMIUM:
switch (tag) {
case BrowserTag.LATEST:
return await chromium.resolveBuildId(platform);
case BrowserTag.NIGHTLY:
case BrowserTag.CANARY:
case BrowserTag.DEV:
case BrowserTag.DEVEDITION:
case BrowserTag.BETA:
case BrowserTag.STABLE:
case BrowserTag.ESR:
throw new Error(
`${tag} is not supported for Chromium. Use 'latest' instead.`,
);
}
}
}
/**
* @public
*/
export async function resolveBuildId(
browser: Browser,
platform: BrowserPlatform,
tag: string | BrowserTag,
): Promise<string> {
const browserTag = tag as BrowserTag;
if (Object.values(BrowserTag).includes(browserTag)) {
return await resolveBuildIdForBrowserTag(browser, platform, browserTag);
}
switch (browser) {
case Browser.FIREFOX:
return tag;
case Browser.CHROME:
const chromeResult = await chrome.resolveBuildId(tag);
if (chromeResult) {
return chromeResult;
}
return tag;
case Browser.CHROMEDRIVER:
const chromeDriverResult = await chromedriver.resolveBuildId(tag);
if (chromeDriverResult) {
return chromeDriverResult;
}
return tag;
case Browser.CHROMEHEADLESSSHELL:
const chromeHeadlessShellResult =
await chromeHeadlessShell.resolveBuildId(tag);
if (chromeHeadlessShellResult) {
return chromeHeadlessShellResult;
}
return tag;
case Browser.CHROMIUM:
return tag;
}
}
/**
* @public
*/
export async function createProfile(
browser: Browser,
opts: ProfileOptions,
): Promise<void> {
switch (browser) {
case Browser.FIREFOX:
return await firefox.createProfile(opts);
case Browser.CHROME:
case Browser.CHROMIUM:
throw new Error(`Profile creation is not support for ${browser} yet`);
}
}
/**
* @public
*/
export function resolveSystemExecutablePath(
browser: Browser,
platform: BrowserPlatform,
channel: ChromeReleaseChannel,
): string {
switch (browser) {
case Browser.CHROMEDRIVER:
case Browser.CHROMEHEADLESSSHELL:
case Browser.FIREFOX:
case Browser.CHROMIUM:
throw new Error(
`System browser detection is not supported for ${browser} yet.`,
);
case Browser.CHROME:
return chrome.resolveSystemExecutablePath(platform, channel);
}
}
/**
* Returns a version comparator for the given browser that can be used to sort
* browser versions.
*
* @public
*/
export function getVersionComparator(
browser: Browser,
): (a: string, b: string) => number {
return versionComparators[browser];
}

View File

@@ -0,0 +1,71 @@
/**
* @license
* Copyright 2023 Google Inc.
* SPDX-License-Identifier: Apache-2.0
*/
import path from 'node:path';
import {BrowserPlatform} from './types.js';
function folder(platform: BrowserPlatform): string {
switch (platform) {
case BrowserPlatform.LINUX_ARM:
case BrowserPlatform.LINUX:
return 'linux64';
case BrowserPlatform.MAC_ARM:
return 'mac-arm64';
case BrowserPlatform.MAC:
return 'mac-x64';
case BrowserPlatform.WIN32:
return 'win32';
case BrowserPlatform.WIN64:
return 'win64';
}
}
export function resolveDownloadUrl(
platform: BrowserPlatform,
buildId: string,
baseUrl = 'https://storage.googleapis.com/chrome-for-testing-public',
): string {
return `${baseUrl}/${resolveDownloadPath(platform, buildId).join('/')}`;
}
export function resolveDownloadPath(
platform: BrowserPlatform,
buildId: string,
): string[] {
return [
buildId,
folder(platform),
`chrome-headless-shell-${folder(platform)}.zip`,
];
}
export function relativeExecutablePath(
platform: BrowserPlatform,
_buildId: string,
): string {
switch (platform) {
case BrowserPlatform.MAC:
case BrowserPlatform.MAC_ARM:
return path.join(
'chrome-headless-shell-' + folder(platform),
'chrome-headless-shell',
);
case BrowserPlatform.LINUX_ARM:
case BrowserPlatform.LINUX:
return path.join(
'chrome-headless-shell-linux64',
'chrome-headless-shell',
);
case BrowserPlatform.WIN32:
case BrowserPlatform.WIN64:
return path.join(
'chrome-headless-shell-' + folder(platform),
'chrome-headless-shell.exe',
);
}
}
export {resolveBuildId, compareVersions} from './chrome.js';

View File

@@ -0,0 +1,215 @@
/**
* @license
* Copyright 2023 Google Inc.
* SPDX-License-Identifier: Apache-2.0
*/
import path from 'node:path';
import semver from 'semver';
import {getJSON} from '../httpUtil.js';
import {BrowserPlatform, ChromeReleaseChannel} from './types.js';
function folder(platform: BrowserPlatform): string {
switch (platform) {
case BrowserPlatform.LINUX_ARM:
case BrowserPlatform.LINUX:
return 'linux64';
case BrowserPlatform.MAC_ARM:
return 'mac-arm64';
case BrowserPlatform.MAC:
return 'mac-x64';
case BrowserPlatform.WIN32:
return 'win32';
case BrowserPlatform.WIN64:
return 'win64';
}
}
export function resolveDownloadUrl(
platform: BrowserPlatform,
buildId: string,
baseUrl = 'https://storage.googleapis.com/chrome-for-testing-public',
): string {
return `${baseUrl}/${resolveDownloadPath(platform, buildId).join('/')}`;
}
export function resolveDownloadPath(
platform: BrowserPlatform,
buildId: string,
): string[] {
return [buildId, folder(platform), `chrome-${folder(platform)}.zip`];
}
export function relativeExecutablePath(
platform: BrowserPlatform,
_buildId: string,
): string {
switch (platform) {
case BrowserPlatform.MAC:
case BrowserPlatform.MAC_ARM:
return path.join(
'chrome-' + folder(platform),
'Google Chrome for Testing.app',
'Contents',
'MacOS',
'Google Chrome for Testing',
);
case BrowserPlatform.LINUX_ARM:
case BrowserPlatform.LINUX:
return path.join('chrome-linux64', 'chrome');
case BrowserPlatform.WIN32:
case BrowserPlatform.WIN64:
return path.join('chrome-' + folder(platform), 'chrome.exe');
}
}
export async function getLastKnownGoodReleaseForChannel(
channel: ChromeReleaseChannel,
): Promise<{version: string; revision: string}> {
const data = (await getJSON(
new URL(
'https://googlechromelabs.github.io/chrome-for-testing/last-known-good-versions.json',
),
)) as {
channels: Record<string, {version: string}>;
};
for (const channel of Object.keys(data.channels)) {
data.channels[channel.toLowerCase()] = data.channels[channel]!;
delete data.channels[channel];
}
return (
data as {
channels: Record<
ChromeReleaseChannel,
{version: string; revision: string}
>;
}
).channels[channel];
}
export async function getLastKnownGoodReleaseForMilestone(
milestone: string,
): Promise<{version: string; revision: string} | undefined> {
const data = (await getJSON(
new URL(
'https://googlechromelabs.github.io/chrome-for-testing/latest-versions-per-milestone.json',
),
)) as {
milestones: Record<string, {version: string; revision: string}>;
};
return data.milestones[milestone] as
| {version: string; revision: string}
| undefined;
}
export async function getLastKnownGoodReleaseForBuild(
/**
* @example `112.0.23`,
*/
buildPrefix: string,
): Promise<{version: string; revision: string} | undefined> {
const data = (await getJSON(
new URL(
'https://googlechromelabs.github.io/chrome-for-testing/latest-patch-versions-per-build.json',
),
)) as {
builds: Record<string, {version: string; revision: string}>;
};
return data.builds[buildPrefix] as
| {version: string; revision: string}
| undefined;
}
export async function resolveBuildId(
channel: ChromeReleaseChannel,
): Promise<string>;
export async function resolveBuildId(
channel: string,
): Promise<string | undefined>;
export async function resolveBuildId(
channel: ChromeReleaseChannel | string,
): Promise<string | undefined> {
if (
Object.values(ChromeReleaseChannel).includes(
channel as ChromeReleaseChannel,
)
) {
return (
await getLastKnownGoodReleaseForChannel(channel as ChromeReleaseChannel)
).version;
}
if (channel.match(/^\d+$/)) {
// Potentially a milestone.
return (await getLastKnownGoodReleaseForMilestone(channel))?.version;
}
if (channel.match(/^\d+\.\d+\.\d+$/)) {
// Potentially a build prefix without the patch version.
return (await getLastKnownGoodReleaseForBuild(channel))?.version;
}
return;
}
export function resolveSystemExecutablePath(
platform: BrowserPlatform,
channel: ChromeReleaseChannel,
): string {
switch (platform) {
case BrowserPlatform.WIN64:
case BrowserPlatform.WIN32:
switch (channel) {
case ChromeReleaseChannel.STABLE:
return `${process.env['PROGRAMFILES']}\\Google\\Chrome\\Application\\chrome.exe`;
case ChromeReleaseChannel.BETA:
return `${process.env['PROGRAMFILES']}\\Google\\Chrome Beta\\Application\\chrome.exe`;
case ChromeReleaseChannel.CANARY:
return `${process.env['PROGRAMFILES']}\\Google\\Chrome SxS\\Application\\chrome.exe`;
case ChromeReleaseChannel.DEV:
return `${process.env['PROGRAMFILES']}\\Google\\Chrome Dev\\Application\\chrome.exe`;
}
case BrowserPlatform.MAC_ARM:
case BrowserPlatform.MAC:
switch (channel) {
case ChromeReleaseChannel.STABLE:
return '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome';
case ChromeReleaseChannel.BETA:
return '/Applications/Google Chrome Beta.app/Contents/MacOS/Google Chrome Beta';
case ChromeReleaseChannel.CANARY:
return '/Applications/Google Chrome Canary.app/Contents/MacOS/Google Chrome Canary';
case ChromeReleaseChannel.DEV:
return '/Applications/Google Chrome Dev.app/Contents/MacOS/Google Chrome Dev';
}
case BrowserPlatform.LINUX_ARM:
case BrowserPlatform.LINUX:
switch (channel) {
case ChromeReleaseChannel.STABLE:
return '/opt/google/chrome/chrome';
case ChromeReleaseChannel.BETA:
return '/opt/google/chrome-beta/chrome';
case ChromeReleaseChannel.CANARY:
return '/opt/google/chrome-canary/chrome';
case ChromeReleaseChannel.DEV:
return '/opt/google/chrome-unstable/chrome';
}
}
}
export function compareVersions(a: string, b: string): number {
if (!semver.valid(a)) {
throw new Error(`Version ${a} is not a valid semver version`);
}
if (!semver.valid(b)) {
throw new Error(`Version ${b} is not a valid semver version`);
}
if (semver.gt(a, b)) {
return 1;
} else if (semver.lt(a, b)) {
return -1;
} else {
return 0;
}
}

View File

@@ -0,0 +1,58 @@
/**
* @license
* Copyright 2023 Google Inc.
* SPDX-License-Identifier: Apache-2.0
*/
import path from 'node:path';
import {BrowserPlatform} from './types.js';
function folder(platform: BrowserPlatform): string {
switch (platform) {
case BrowserPlatform.LINUX_ARM:
case BrowserPlatform.LINUX:
return 'linux64';
case BrowserPlatform.MAC_ARM:
return 'mac-arm64';
case BrowserPlatform.MAC:
return 'mac-x64';
case BrowserPlatform.WIN32:
return 'win32';
case BrowserPlatform.WIN64:
return 'win64';
}
}
export function resolveDownloadUrl(
platform: BrowserPlatform,
buildId: string,
baseUrl = 'https://storage.googleapis.com/chrome-for-testing-public',
): string {
return `${baseUrl}/${resolveDownloadPath(platform, buildId).join('/')}`;
}
export function resolveDownloadPath(
platform: BrowserPlatform,
buildId: string,
): string[] {
return [buildId, folder(platform), `chromedriver-${folder(platform)}.zip`];
}
export function relativeExecutablePath(
platform: BrowserPlatform,
_buildId: string,
): string {
switch (platform) {
case BrowserPlatform.MAC:
case BrowserPlatform.MAC_ARM:
return path.join('chromedriver-' + folder(platform), 'chromedriver');
case BrowserPlatform.LINUX_ARM:
case BrowserPlatform.LINUX:
return path.join('chromedriver-linux64', 'chromedriver');
case BrowserPlatform.WIN32:
case BrowserPlatform.WIN64:
return path.join('chromedriver-' + folder(platform), 'chromedriver.exe');
}
}
export {resolveBuildId, compareVersions} from './chrome.js';

View File

@@ -0,0 +1,95 @@
/**
* @license
* Copyright 2023 Google Inc.
* SPDX-License-Identifier: Apache-2.0
*/
import path from 'node:path';
import {getText} from '../httpUtil.js';
import {BrowserPlatform} from './types.js';
function archive(platform: BrowserPlatform, buildId: string): string {
switch (platform) {
case BrowserPlatform.LINUX_ARM:
case BrowserPlatform.LINUX:
return 'chrome-linux';
case BrowserPlatform.MAC_ARM:
case BrowserPlatform.MAC:
return 'chrome-mac';
case BrowserPlatform.WIN32:
case BrowserPlatform.WIN64:
// Windows archive name changed at r591479.
return parseInt(buildId, 10) > 591479 ? 'chrome-win' : 'chrome-win32';
}
}
function folder(platform: BrowserPlatform): string {
switch (platform) {
case BrowserPlatform.LINUX_ARM:
case BrowserPlatform.LINUX:
return 'Linux_x64';
case BrowserPlatform.MAC_ARM:
return 'Mac_Arm';
case BrowserPlatform.MAC:
return 'Mac';
case BrowserPlatform.WIN32:
return 'Win';
case BrowserPlatform.WIN64:
return 'Win_x64';
}
}
export function resolveDownloadUrl(
platform: BrowserPlatform,
buildId: string,
baseUrl = 'https://storage.googleapis.com/chromium-browser-snapshots',
): string {
return `${baseUrl}/${resolveDownloadPath(platform, buildId).join('/')}`;
}
export function resolveDownloadPath(
platform: BrowserPlatform,
buildId: string,
): string[] {
return [folder(platform), buildId, `${archive(platform, buildId)}.zip`];
}
export function relativeExecutablePath(
platform: BrowserPlatform,
_buildId: string,
): string {
switch (platform) {
case BrowserPlatform.MAC:
case BrowserPlatform.MAC_ARM:
return path.join(
'chrome-mac',
'Chromium.app',
'Contents',
'MacOS',
'Chromium',
);
case BrowserPlatform.LINUX_ARM:
case BrowserPlatform.LINUX:
return path.join('chrome-linux', 'chrome');
case BrowserPlatform.WIN32:
case BrowserPlatform.WIN64:
return path.join('chrome-win', 'chrome.exe');
}
}
export async function resolveBuildId(
platform: BrowserPlatform,
): Promise<string> {
return await getText(
new URL(
`https://storage.googleapis.com/chromium-browser-snapshots/${folder(
platform,
)}/LAST_CHANGE`,
),
);
}
export function compareVersions(a: string, b: string): number {
return Number(a) - Number(b);
}

View File

@@ -0,0 +1,455 @@
/**
* @license
* Copyright 2023 Google Inc.
* SPDX-License-Identifier: Apache-2.0
*/
import fs from 'node:fs';
import path from 'node:path';
import {getJSON} from '../httpUtil.js';
import {BrowserPlatform, type ProfileOptions} from './types.js';
function getFormat(buildId: string): string {
const majorVersion = Number(buildId.split('.').shift()!);
return majorVersion >= 135 ? 'xz' : 'bz2';
}
function archiveNightly(platform: BrowserPlatform, buildId: string): string {
switch (platform) {
case BrowserPlatform.LINUX:
return `firefox-${buildId}.en-US.linux-x86_64.tar.${getFormat(buildId)}`;
case BrowserPlatform.LINUX_ARM:
return `firefox-${buildId}.en-US.linux-aarch64.tar.${getFormat(buildId)}`;
case BrowserPlatform.MAC_ARM:
case BrowserPlatform.MAC:
return `firefox-${buildId}.en-US.mac.dmg`;
case BrowserPlatform.WIN32:
case BrowserPlatform.WIN64:
return `firefox-${buildId}.en-US.${platform}.zip`;
}
}
function archive(platform: BrowserPlatform, buildId: string): string {
switch (platform) {
case BrowserPlatform.LINUX_ARM:
case BrowserPlatform.LINUX:
return `firefox-${buildId}.tar.${getFormat(buildId)}`;
case BrowserPlatform.MAC_ARM:
case BrowserPlatform.MAC:
return `Firefox ${buildId}.dmg`;
case BrowserPlatform.WIN32:
case BrowserPlatform.WIN64:
return `Firefox Setup ${buildId}.exe`;
}
}
function platformName(platform: BrowserPlatform): string {
switch (platform) {
case BrowserPlatform.LINUX:
return `linux-x86_64`;
case BrowserPlatform.LINUX_ARM:
return `linux-aarch64`;
case BrowserPlatform.MAC_ARM:
case BrowserPlatform.MAC:
return `mac`;
case BrowserPlatform.WIN32:
case BrowserPlatform.WIN64:
return platform;
}
}
function parseBuildId(buildId: string): [FirefoxChannel, string] {
for (const value of Object.values(FirefoxChannel)) {
if (buildId.startsWith(value + '_')) {
buildId = buildId.substring(value.length + 1);
return [value, buildId];
}
}
// Older versions do not have channel as the prefix.«
return [FirefoxChannel.NIGHTLY, buildId];
}
export function resolveDownloadUrl(
platform: BrowserPlatform,
buildId: string,
baseUrl?: string,
): string {
const [channel] = parseBuildId(buildId);
switch (channel) {
case FirefoxChannel.NIGHTLY:
baseUrl ??=
'https://archive.mozilla.org/pub/firefox/nightly/latest-mozilla-central';
break;
case FirefoxChannel.DEVEDITION:
baseUrl ??= 'https://archive.mozilla.org/pub/devedition/releases';
break;
case FirefoxChannel.BETA:
case FirefoxChannel.STABLE:
case FirefoxChannel.ESR:
baseUrl ??= 'https://archive.mozilla.org/pub/firefox/releases';
break;
}
return `${baseUrl}/${resolveDownloadPath(platform, buildId).join('/')}`;
}
export function resolveDownloadPath(
platform: BrowserPlatform,
buildId: string,
): string[] {
const [channel, resolvedBuildId] = parseBuildId(buildId);
switch (channel) {
case FirefoxChannel.NIGHTLY:
return [archiveNightly(platform, resolvedBuildId)];
case FirefoxChannel.DEVEDITION:
case FirefoxChannel.BETA:
case FirefoxChannel.STABLE:
case FirefoxChannel.ESR:
return [
resolvedBuildId,
platformName(platform),
'en-US',
archive(platform, resolvedBuildId),
];
}
}
export function relativeExecutablePath(
platform: BrowserPlatform,
buildId: string,
): string {
const [channel] = parseBuildId(buildId);
switch (channel) {
case FirefoxChannel.NIGHTLY:
switch (platform) {
case BrowserPlatform.MAC_ARM:
case BrowserPlatform.MAC:
return path.join(
'Firefox Nightly.app',
'Contents',
'MacOS',
'firefox',
);
case BrowserPlatform.LINUX_ARM:
case BrowserPlatform.LINUX:
return path.join('firefox', 'firefox');
case BrowserPlatform.WIN32:
case BrowserPlatform.WIN64:
return path.join('firefox', 'firefox.exe');
}
case FirefoxChannel.BETA:
case FirefoxChannel.DEVEDITION:
case FirefoxChannel.ESR:
case FirefoxChannel.STABLE:
switch (platform) {
case BrowserPlatform.MAC_ARM:
case BrowserPlatform.MAC:
return path.join('Firefox.app', 'Contents', 'MacOS', 'firefox');
case BrowserPlatform.LINUX_ARM:
case BrowserPlatform.LINUX:
return path.join('firefox', 'firefox');
case BrowserPlatform.WIN32:
case BrowserPlatform.WIN64:
return path.join('core', 'firefox.exe');
}
}
}
export enum FirefoxChannel {
STABLE = 'stable',
ESR = 'esr',
DEVEDITION = 'devedition',
BETA = 'beta',
NIGHTLY = 'nightly',
}
export async function resolveBuildId(
channel: FirefoxChannel = FirefoxChannel.NIGHTLY,
): Promise<string> {
const channelToVersionKey = {
[FirefoxChannel.ESR]: 'FIREFOX_ESR',
[FirefoxChannel.STABLE]: 'LATEST_FIREFOX_VERSION',
[FirefoxChannel.DEVEDITION]: 'FIREFOX_DEVEDITION',
[FirefoxChannel.BETA]: 'FIREFOX_DEVEDITION',
[FirefoxChannel.NIGHTLY]: 'FIREFOX_NIGHTLY',
};
const versions = (await getJSON(
new URL('https://product-details.mozilla.org/1.0/firefox_versions.json'),
)) as Record<string, string>;
const version = versions[channelToVersionKey[channel]];
if (!version) {
throw new Error(`Channel ${channel} is not found.`);
}
return channel + '_' + version;
}
export async function createProfile(options: ProfileOptions): Promise<void> {
if (!fs.existsSync(options.path)) {
await fs.promises.mkdir(options.path, {
recursive: true,
});
}
await syncPreferences({
preferences: {
...defaultProfilePreferences(options.preferences),
...options.preferences,
},
path: options.path,
});
}
function defaultProfilePreferences(
extraPrefs: Record<string, unknown>,
): Record<string, unknown> {
const server = 'dummy.test';
const defaultPrefs = {
// Make sure Shield doesn't hit the network.
'app.normandy.api_url': '',
// Disable Firefox old build background check
'app.update.checkInstallTime': false,
// Disable automatically upgrading Firefox
'app.update.disabledForTesting': true,
// Increase the APZ content response timeout to 1 minute
'apz.content_response_timeout': 60000,
// Prevent various error message on the console
// jest-puppeteer asserts that no error message is emitted by the console
'browser.contentblocking.features.standard':
'-tp,tpPrivate,cookieBehavior0,-cryptoTP,-fp',
// Enable the dump function: which sends messages to the system
// console
// https://bugzilla.mozilla.org/show_bug.cgi?id=1543115
'browser.dom.window.dump.enabled': true,
// Disable topstories
'browser.newtabpage.activity-stream.feeds.system.topstories': false,
// Always display a blank page
'browser.newtabpage.enabled': false,
// Background thumbnails in particular cause grief: and disabling
// thumbnails in general cannot hurt
'browser.pagethumbnails.capturing_disabled': true,
// Disable safebrowsing components.
'browser.safebrowsing.blockedURIs.enabled': false,
'browser.safebrowsing.downloads.enabled': false,
'browser.safebrowsing.malware.enabled': false,
'browser.safebrowsing.phishing.enabled': false,
// Disable updates to search engines.
'browser.search.update': false,
// Do not restore the last open set of tabs if the browser has crashed
'browser.sessionstore.resume_from_crash': false,
// Skip check for default browser on startup
'browser.shell.checkDefaultBrowser': false,
// Disable newtabpage
'browser.startup.homepage': 'about:blank',
// Do not redirect user when a milstone upgrade of Firefox is detected
'browser.startup.homepage_override.mstone': 'ignore',
// Start with a blank page about:blank
'browser.startup.page': 0,
// Do not allow background tabs to be zombified on Android: otherwise for
// tests that open additional tabs: the test harness tab itself might get
// unloaded
'browser.tabs.disableBackgroundZombification': false,
// Do not warn when closing all other open tabs
'browser.tabs.warnOnCloseOtherTabs': false,
// Do not warn when multiple tabs will be opened
'browser.tabs.warnOnOpen': false,
// Do not automatically offer translations, as tests do not expect this.
'browser.translations.automaticallyPopup': false,
// Disable the UI tour.
'browser.uitour.enabled': false,
// Turn off search suggestions in the location bar so as not to trigger
// network connections.
'browser.urlbar.suggest.searches': false,
// Disable first run splash page on Windows 10
'browser.usedOnWindows10.introURL': '',
// Do not warn on quitting Firefox
'browser.warnOnQuit': false,
// Defensively disable data reporting systems
'datareporting.healthreport.documentServerURI': `http://${server}/dummy/healthreport/`,
'datareporting.healthreport.logging.consoleEnabled': false,
'datareporting.healthreport.service.enabled': false,
'datareporting.healthreport.service.firstRun': false,
'datareporting.healthreport.uploadEnabled': false,
// Do not show datareporting policy notifications which can interfere with tests
'datareporting.policy.dataSubmissionEnabled': false,
'datareporting.policy.dataSubmissionPolicyBypassNotification': true,
// DevTools JSONViewer sometimes fails to load dependencies with its require.js.
// This doesn't affect Puppeteer but spams console (Bug 1424372)
'devtools.jsonview.enabled': false,
// Disable popup-blocker
'dom.disable_open_during_load': false,
// Enable the support for File object creation in the content process
// Required for |Page.setFileInputFiles| protocol method.
'dom.file.createInChild': true,
// Disable the ProcessHangMonitor
'dom.ipc.reportProcessHangs': false,
// Disable slow script dialogues
'dom.max_chrome_script_run_time': 0,
'dom.max_script_run_time': 0,
// Only load extensions from the application and user profile
// AddonManager.SCOPE_PROFILE + AddonManager.SCOPE_APPLICATION
'extensions.autoDisableScopes': 0,
'extensions.enabledScopes': 5,
// Disable metadata caching for installed add-ons by default
'extensions.getAddons.cache.enabled': false,
// Disable installing any distribution extensions or add-ons.
'extensions.installDistroAddons': false,
// Turn off extension updates so they do not bother tests
'extensions.update.enabled': false,
// Turn off extension updates so they do not bother tests
'extensions.update.notifyUser': false,
// Make sure opening about:addons will not hit the network
'extensions.webservice.discoverURL': `http://${server}/dummy/discoveryURL`,
// Allow the application to have focus even it runs in the background
'focusmanager.testmode': true,
// Disable useragent updates
'general.useragent.updates.enabled': false,
// Always use network provider for geolocation tests so we bypass the
// macOS dialog raised by the corelocation provider
'geo.provider.testing': true,
// Do not scan Wifi
'geo.wifi.scan': false,
// No hang monitor
'hangmonitor.timeout': 0,
// Show chrome errors and warnings in the error console
'javascript.options.showInConsole': true,
// Disable download and usage of OpenH264: and Widevine plugins
'media.gmp-manager.updateEnabled': false,
// Disable the GFX sanity window
'media.sanity-test.disabled': true,
// Disable experimental feature that is only available in Nightly
'network.cookie.sameSite.laxByDefault': false,
// Do not prompt for temporary redirects
'network.http.prompt-temp-redirect': false,
// Disable speculative connections so they are not reported as leaking
// when they are hanging around
'network.http.speculative-parallel-limit': 0,
// Do not automatically switch between offline and online
'network.manage-offline-status': false,
// Make sure SNTP requests do not hit the network
'network.sntp.pools': server,
// Disable Flash.
'plugin.state.flash': 0,
'privacy.trackingprotection.enabled': false,
// Can be removed once Firefox 89 is no longer supported
// https://bugzilla.mozilla.org/show_bug.cgi?id=1710839
'remote.enabled': true,
// Disabled screenshots component
'screenshots.browser.component.enabled': false,
// Don't do network connections for mitm priming
'security.certerrors.mitm.priming.enabled': false,
// Local documents have access to all other local documents,
// including directory listings
'security.fileuri.strict_origin_policy': false,
// Do not wait for the notification button security delay
'security.notification_enable_delay': 0,
// Ensure blocklist updates do not hit the network
'services.settings.server': `http://${server}/dummy/blocklist/`,
// Do not automatically fill sign-in forms with known usernames and
// passwords
'signon.autofillForms': false,
// Disable password capture, so that tests that include forms are not
// influenced by the presence of the persistent doorhanger notification
'signon.rememberSignons': false,
// Disable first-run welcome page
'startup.homepage_welcome_url': 'about:blank',
// Disable first-run welcome page
'startup.homepage_welcome_url.additional': '',
// Disable browser animations (tabs, fullscreen, sliding alerts)
'toolkit.cosmeticAnimations.enabled': false,
// Prevent starting into safe mode after application crashes
'toolkit.startup.max_resumed_crashes': -1,
};
return Object.assign(defaultPrefs, extraPrefs);
}
async function backupFile(input: string): Promise<void> {
if (!fs.existsSync(input)) {
return;
}
await fs.promises.copyFile(input, input + '.puppeteer');
}
/**
* Populates the user.js file with custom preferences as needed to allow
* Firefox's support to properly function. These preferences will be
* automatically copied over to prefs.js during startup of Firefox. To be
* able to restore the original values of preferences a backup of prefs.js
* will be created.
*/
async function syncPreferences(options: ProfileOptions): Promise<void> {
const prefsPath = path.join(options.path, 'prefs.js');
const userPath = path.join(options.path, 'user.js');
const lines = Object.entries(options.preferences).map(([key, value]) => {
return `user_pref(${JSON.stringify(key)}, ${JSON.stringify(value)});`;
});
// Use allSettled to prevent corruption.
const result = await Promise.allSettled([
backupFile(userPath).then(async () => {
await fs.promises.writeFile(userPath, lines.join('\n'));
}),
backupFile(prefsPath),
]);
for (const command of result) {
if (command.status === 'rejected') {
throw command.reason;
}
}
}
export function compareVersions(a: string, b: string): number {
// TODO: this is a not very reliable check.
return parseInt(a.replace('.', ''), 16) - parseInt(b.replace('.', ''), 16);
}

View File

@@ -0,0 +1,70 @@
/**
* @license
* Copyright 2023 Google Inc.
* SPDX-License-Identifier: Apache-2.0
*/
/**
* Supported browsers.
*
* @public
*/
export enum Browser {
CHROME = 'chrome',
CHROMEHEADLESSSHELL = 'chrome-headless-shell',
CHROMIUM = 'chromium',
FIREFOX = 'firefox',
CHROMEDRIVER = 'chromedriver',
}
/**
* Platform names used to identify a OS platform x architecture combination in the way
* that is relevant for the browser download.
*
* @public
*/
export enum BrowserPlatform {
LINUX = 'linux',
LINUX_ARM = 'linux_arm',
MAC = 'mac',
MAC_ARM = 'mac_arm',
WIN32 = 'win32',
WIN64 = 'win64',
}
/**
* Enum describing a release channel for a browser.
*
* You can use this in combination with {@link resolveBuildId} to resolve
* a build ID based on a release channel.
*
* @public
*/
export enum BrowserTag {
CANARY = 'canary',
NIGHTLY = 'nightly',
BETA = 'beta',
DEV = 'dev',
DEVEDITION = 'devedition',
STABLE = 'stable',
ESR = 'esr',
LATEST = 'latest',
}
/**
* @public
*/
export interface ProfileOptions {
preferences: Record<string, unknown>;
path: string;
}
/**
* @public
*/
export enum ChromeReleaseChannel {
STABLE = 'stable',
DEV = 'dev',
CANARY = 'canary',
BETA = 'beta',
}

9
node_modules/@puppeteer/browsers/src/debug.ts generated vendored Normal file
View File

@@ -0,0 +1,9 @@
/**
* @license
* Copyright 2023 Google Inc.
* SPDX-License-Identifier: Apache-2.0
*/
import debug from 'debug';
export {debug};

52
node_modules/@puppeteer/browsers/src/detectPlatform.ts generated vendored Normal file
View File

@@ -0,0 +1,52 @@
/**
* @license
* Copyright 2023 Google Inc.
* SPDX-License-Identifier: Apache-2.0
*/
import os from 'node:os';
import {BrowserPlatform} from './browser-data/browser-data.js';
/**
* @public
*/
export function detectBrowserPlatform(): BrowserPlatform | undefined {
const platform = os.platform();
const arch = os.arch();
switch (platform) {
case 'darwin':
return arch === 'arm64' ? BrowserPlatform.MAC_ARM : BrowserPlatform.MAC;
case 'linux':
return arch === 'arm64'
? BrowserPlatform.LINUX_ARM
: BrowserPlatform.LINUX;
case 'win32':
return arch === 'x64' ||
// Windows 11 for ARM supports x64 emulation
(arch === 'arm64' && isWindows11(os.release()))
? BrowserPlatform.WIN64
: BrowserPlatform.WIN32;
default:
return undefined;
}
}
/**
* Windows 11 is identified by the version 10.0.22000 or greater
* @internal
*/
function isWindows11(version: string): boolean {
const parts = version.split('.');
if (parts.length > 2) {
const major = parseInt(parts[0] as string, 10);
const minor = parseInt(parts[1] as string, 10);
const patch = parseInt(parts[2] as string, 10);
return (
major > 10 ||
(major === 10 && minor > 0) ||
(major === 10 && minor === 0 && patch >= 22000)
);
}
return false;
}

183
node_modules/@puppeteer/browsers/src/fileUtil.ts generated vendored Normal file
View File

@@ -0,0 +1,183 @@
/**
* @license
* Copyright 2023 Google Inc.
* SPDX-License-Identifier: Apache-2.0
*/
import type {ChildProcessByStdio} from 'node:child_process';
import {spawnSync, spawn} from 'node:child_process';
import {createReadStream} from 'node:fs';
import {mkdir, readdir} from 'node:fs/promises';
import * as path from 'node:path';
import type {Readable, Transform, Writable} from 'node:stream';
import {Stream} from 'node:stream';
import debug from 'debug';
const debugFileUtil = debug('puppeteer:browsers:fileUtil');
/**
* @internal
*/
export async function unpackArchive(
archivePath: string,
folderPath: string,
): Promise<void> {
if (!path.isAbsolute(folderPath)) {
folderPath = path.resolve(process.cwd(), folderPath);
}
if (archivePath.endsWith('.zip')) {
const extractZip = await import('extract-zip');
await extractZip.default(archivePath, {dir: folderPath});
} else if (archivePath.endsWith('.tar.bz2')) {
await extractTar(archivePath, folderPath, 'bzip2');
} else if (archivePath.endsWith('.dmg')) {
await mkdir(folderPath);
await installDMG(archivePath, folderPath);
} else if (archivePath.endsWith('.exe')) {
// Firefox on Windows.
const result = spawnSync(archivePath, [`/ExtractDir=${folderPath}`], {
env: {
__compat_layer: 'RunAsInvoker',
},
});
if (result.status !== 0) {
throw new Error(
`Failed to extract ${archivePath} to ${folderPath}: ${result.output}`,
);
}
} else if (archivePath.endsWith('.tar.xz')) {
await extractTar(archivePath, folderPath, 'xz');
} else {
throw new Error(`Unsupported archive format: ${archivePath}`);
}
}
function createTransformStream(
child: ChildProcessByStdio<Writable, Readable, null>,
): Transform {
const stream = new Stream.Transform({
transform(chunk, encoding, callback) {
if (!child.stdin.write(chunk, encoding)) {
child.stdin.once('drain', callback);
} else {
callback();
}
},
flush(callback) {
if (child.stdout.destroyed) {
callback();
} else {
child.stdin.end();
child.stdout.on('close', callback);
}
},
});
child.stdin.on('error', e => {
if ('code' in e && e.code === 'EPIPE') {
// finished before reading the file finished (i.e. head)
stream.emit('end');
} else {
stream.destroy(e);
}
});
child.stdout
.on('data', data => {
return stream.push(data);
})
.on('error', e => {
return stream.destroy(e);
});
child.once('close', () => {
return stream.end();
});
return stream;
}
/**
* @internal
*/
export const internalConstantsForTesting = {
xz: 'xz',
bzip2: 'bzip2',
};
/**
* @internal
*/
async function extractTar(
tarPath: string,
folderPath: string,
decompressUtilityName: keyof typeof internalConstantsForTesting,
): Promise<void> {
const tarFs = await import('tar-fs');
return await new Promise<void>((fulfill, reject) => {
function handleError(utilityName: string) {
return (error: Error) => {
if ('code' in error && error.code === 'ENOENT') {
error = new Error(
`\`${utilityName}\` utility is required to unpack this archive`,
{
cause: error,
},
);
}
reject(error);
};
}
const unpack = spawn(
internalConstantsForTesting[decompressUtilityName],
['-d'],
{
stdio: ['pipe', 'pipe', 'inherit'],
},
)
.once('error', handleError(decompressUtilityName))
.once('exit', code => {
debugFileUtil(`${decompressUtilityName} exited, code=${code}`);
});
const tar = tarFs.extract(folderPath);
tar.once('error', handleError('tar'));
tar.once('finish', fulfill);
createReadStream(tarPath).pipe(createTransformStream(unpack)).pipe(tar);
});
}
/**
* @internal
*/
async function installDMG(dmgPath: string, folderPath: string): Promise<void> {
const {stdout} = spawnSync(`hdiutil`, [
'attach',
'-nobrowse',
'-noautoopen',
dmgPath,
]);
const volumes = stdout.toString('utf8').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);
spawnSync('cp', ['-R', mountedPath, folderPath]);
} finally {
spawnSync('hdiutil', ['detach', mountPath, '-quiet']);
}
}

View File

@@ -0,0 +1,4 @@
/**
* @internal
*/
export const packageVersion = '2.10.9';

162
node_modules/@puppeteer/browsers/src/httpUtil.ts generated vendored Normal file
View File

@@ -0,0 +1,162 @@
/**
* @license
* Copyright 2023 Google Inc.
* SPDX-License-Identifier: Apache-2.0
*/
import {createWriteStream} from 'node:fs';
import * as http from 'node:http';
import * as https from 'node:https';
import {URL, urlToHttpOptions} from 'node:url';
import {ProxyAgent} from 'proxy-agent';
export function headHttpRequest(url: URL): Promise<boolean> {
return new Promise(resolve => {
const request = httpRequest(
url,
'HEAD',
response => {
// consume response data free node process
response.resume();
resolve(response.statusCode === 200);
},
false,
);
request.on('error', () => {
resolve(false);
});
});
}
export function httpRequest(
url: URL,
method: string,
response: (x: http.IncomingMessage) => void,
keepAlive = true,
): http.ClientRequest {
const options: http.RequestOptions = {
protocol: url.protocol,
hostname: url.hostname,
port: url.port,
path: url.pathname + url.search,
method,
headers: keepAlive ? {Connection: 'keep-alive'} : undefined,
auth: urlToHttpOptions(url).auth,
agent: new ProxyAgent(),
};
const requestCallback = (res: http.IncomingMessage): void => {
if (
res.statusCode &&
res.statusCode >= 300 &&
res.statusCode < 400 &&
res.headers.location
) {
httpRequest(new URL(res.headers.location), method, response);
// consume response data to free up memory
// And prevents the connection from being kept alive
res.resume();
} else {
response(res);
}
};
const request =
options.protocol === 'https:'
? https.request(options, requestCallback)
: http.request(options, requestCallback);
request.end();
return request;
}
/**
* @internal
*/
export function downloadFile(
url: URL,
destinationPath: string,
progressCallback?: (downloadedBytes: number, totalBytes: number) => void,
): Promise<void> {
return new Promise<void>((resolve, reject) => {
let downloadedBytes = 0;
let totalBytes = 0;
function onData(chunk: string): void {
downloadedBytes += chunk.length;
progressCallback!(downloadedBytes, totalBytes);
}
const request = httpRequest(url, 'GET', response => {
if (response.statusCode !== 200) {
const error = new Error(
`Download failed: server returned code ${response.statusCode}. URL: ${url}`,
);
// consume response data to free up memory
response.resume();
reject(error);
return;
}
const file = createWriteStream(destinationPath);
file.on('close', () => {
// The 'close' event is emitted when the stream and any of its
// underlying resources (a file descriptor, for example) have been
// closed. The event indicates that no more events will be emitted, and
// no further computation will occur.
return resolve();
});
file.on('error', error => {
// The 'error' event may be emitted by a Readable implementation at any
// time. Typically, this may occur if the underlying stream is unable to
// generate data due to an underlying internal failure, or when a stream
// implementation attempts to push an invalid chunk of data.
return reject(error);
});
response.pipe(file);
totalBytes = parseInt(response.headers['content-length']!, 10);
if (progressCallback) {
response.on('data', onData);
}
});
request.on('error', error => {
return reject(error);
});
});
}
export async function getJSON(url: URL): Promise<unknown> {
const text = await getText(url);
try {
return JSON.parse(text);
} catch {
throw new Error('Could not parse JSON from ' + url.toString());
}
}
export function getText(url: URL): Promise<string> {
return new Promise((resolve, reject) => {
const request = httpRequest(
url,
'GET',
response => {
let data = '';
if (response.statusCode && response.statusCode >= 400) {
return reject(new Error(`Got status code ${response.statusCode}`));
}
response.on('data', chunk => {
data += chunk;
});
response.on('end', () => {
try {
return resolve(String(data));
} catch {
return reject(new Error('Chrome version not found'));
}
});
},
false,
);
request.on('error', err => {
reject(err);
});
});
}

522
node_modules/@puppeteer/browsers/src/install.ts generated vendored Normal file
View File

@@ -0,0 +1,522 @@
/**
* @license
* Copyright 2017 Google Inc.
* SPDX-License-Identifier: Apache-2.0
*/
import assert from 'node:assert';
import {spawnSync} from 'node:child_process';
import {existsSync, readFileSync} from 'node:fs';
import {mkdir, unlink} from 'node:fs/promises';
import os from 'node:os';
import path from 'node:path';
import type * as ProgressBar from 'progress';
import ProgressBarClass from 'progress';
import {
Browser,
BrowserPlatform,
downloadUrls,
} from './browser-data/browser-data.js';
import {Cache, InstalledBrowser} from './Cache.js';
import {debug} from './debug.js';
import {detectBrowserPlatform} from './detectPlatform.js';
import {unpackArchive} from './fileUtil.js';
import {downloadFile, getJSON, headHttpRequest} from './httpUtil.js';
const debugInstall = debug('puppeteer:browsers:install');
const times = new Map<string, [number, number]>();
function debugTime(label: string) {
times.set(label, process.hrtime());
}
function debugTimeEnd(label: string) {
const end = process.hrtime();
const start = times.get(label);
if (!start) {
return;
}
const duration =
end[0] * 1000 + end[1] / 1e6 - (start[0] * 1000 + start[1] / 1e6); // calculate duration in milliseconds
debugInstall(`Duration for ${label}: ${duration}ms`);
}
/**
* @public
*/
export interface InstallOptions {
/**
* Determines the path to download browsers to.
*/
cacheDir: string;
/**
* Determines which platform the browser will be suited for.
*
* @defaultValue **Auto-detected.**
*/
platform?: BrowserPlatform;
/**
* Determines which browser to install.
*/
browser: Browser;
/**
* Determines which buildId to download. BuildId should uniquely identify
* binaries and they are used for caching.
*/
buildId: string;
/**
* An alias for the provided `buildId`. It will be used to maintain local
* metadata to support aliases in the `launch` command.
*
* @example 'canary'
*/
buildIdAlias?: string;
/**
* Provides information about the progress of the download. If set to
* 'default', the default callback implementing a progress bar will be
* used.
*/
downloadProgressCallback?:
| 'default'
| ((downloadedBytes: number, totalBytes: number) => void);
/**
* Determines the host that will be used for downloading.
*
* @defaultValue Either
*
* - https://storage.googleapis.com/chrome-for-testing-public or
* - https://archive.mozilla.org/pub/firefox/nightly/latest-mozilla-central
*
*/
baseUrl?: string;
/**
* Whether to unpack and install browser archives.
*
* @defaultValue `true`
*/
unpack?: boolean;
/**
* @internal
* @defaultValue `false`
*/
forceFallbackForTesting?: boolean;
/**
* Whether to attempt to install system-level dependencies required
* for the browser.
*
* Only supported for Chrome on Debian or Ubuntu.
* Requires system-level privileges to run `apt-get`.
*
* @defaultValue `false`
*/
installDeps?: boolean;
}
/**
* Downloads and unpacks the browser archive according to the
* {@link InstallOptions}.
*
* @returns a {@link InstalledBrowser} instance.
*
* @public
*/
export function install(
options: InstallOptions & {unpack?: true},
): Promise<InstalledBrowser>;
/**
* Downloads the browser archive according to the {@link InstallOptions} without
* unpacking.
*
* @returns the absolute path to the archive.
*
* @public
*/
export function install(
options: InstallOptions & {unpack: false},
): Promise<string>;
export async function install(
options: InstallOptions,
): Promise<InstalledBrowser | string> {
options.platform ??= detectBrowserPlatform();
options.unpack ??= true;
if (!options.platform) {
throw new Error(
`Cannot download a binary for the provided platform: ${os.platform()} (${os.arch()})`,
);
}
const url = getDownloadUrl(
options.browser,
options.platform,
options.buildId,
options.baseUrl,
);
try {
return await installUrl(url, options);
} catch (err) {
// If custom baseUrl is provided, do not fall back to CfT dashboard.
if (options.baseUrl && !options.forceFallbackForTesting) {
throw err;
}
debugInstall(`Error downloading from ${url}.`);
switch (options.browser) {
case Browser.CHROME:
case Browser.CHROMEDRIVER:
case Browser.CHROMEHEADLESSSHELL: {
debugInstall(
`Trying to find download URL via https://googlechromelabs.github.io/chrome-for-testing.`,
);
interface Version {
downloads: Record<string, Array<{platform: string; url: string}>>;
}
const version = (await getJSON(
new URL(
`https://googlechromelabs.github.io/chrome-for-testing/${options.buildId}.json`,
),
)) as Version;
let platform = '';
switch (options.platform) {
case BrowserPlatform.LINUX:
platform = 'linux64';
break;
case BrowserPlatform.MAC_ARM:
platform = 'mac-arm64';
break;
case BrowserPlatform.MAC:
platform = 'mac-x64';
break;
case BrowserPlatform.WIN32:
platform = 'win32';
break;
case BrowserPlatform.WIN64:
platform = 'win64';
break;
}
const backupUrl = version.downloads[options.browser]?.find(link => {
return link['platform'] === platform;
})?.url;
if (backupUrl) {
// If the URL is the same, skip the retry.
if (backupUrl === url.toString()) {
throw err;
}
debugInstall(`Falling back to downloading from ${backupUrl}.`);
return await installUrl(new URL(backupUrl), options);
}
throw err;
}
default:
throw err;
}
}
}
async function installDeps(installedBrowser: InstalledBrowser) {
if (
process.platform !== 'linux' ||
installedBrowser.platform !== BrowserPlatform.LINUX
) {
return;
}
// Currently, only Debian-like deps are supported.
const depsPath = path.join(
path.dirname(installedBrowser.executablePath),
'deb.deps',
);
if (!existsSync(depsPath)) {
debugInstall(`deb.deps file was not found at ${depsPath}`);
return;
}
const data = readFileSync(depsPath, 'utf-8').split('\n').join(',');
if (process.getuid?.() !== 0) {
throw new Error('Installing system dependencies requires root privileges');
}
let result = spawnSync('apt-get', ['-v']);
if (result.status !== 0) {
throw new Error(
'Failed to install system dependencies: apt-get does not seem to be available',
);
}
debugInstall(`Trying to install dependencies: ${data}`);
result = spawnSync('apt-get', [
'satisfy',
'-y',
data,
'--no-install-recommends',
]);
if (result.status !== 0) {
throw new Error(
`Failed to install system dependencies: status=${result.status},error=${result.error},stdout=${result.stdout.toString('utf8')},stderr=${result.stderr.toString('utf8')}`,
);
}
debugInstall(`Installed system dependencies ${data}`);
}
async function installUrl(
url: URL,
options: InstallOptions,
): Promise<InstalledBrowser | string> {
options.platform ??= detectBrowserPlatform();
if (!options.platform) {
throw new Error(
`Cannot download a binary for the provided platform: ${os.platform()} (${os.arch()})`,
);
}
let downloadProgressCallback = options.downloadProgressCallback;
if (downloadProgressCallback === 'default') {
downloadProgressCallback = await makeProgressCallback(
options.browser,
options.buildIdAlias ?? options.buildId,
);
}
const fileName = decodeURIComponent(url.toString()).split('/').pop();
assert(fileName, `A malformed download URL was found: ${url}.`);
const cache = new Cache(options.cacheDir);
const browserRoot = cache.browserRoot(options.browser);
const archivePath = path.join(browserRoot, `${options.buildId}-${fileName}`);
if (!existsSync(browserRoot)) {
await mkdir(browserRoot, {recursive: true});
}
if (!options.unpack) {
if (existsSync(archivePath)) {
return archivePath;
}
debugInstall(`Downloading binary from ${url}`);
debugTime('download');
await downloadFile(url, archivePath, downloadProgressCallback);
debugTimeEnd('download');
return archivePath;
}
const outputPath = cache.installationDir(
options.browser,
options.platform,
options.buildId,
);
try {
if (existsSync(outputPath)) {
const installedBrowser = new InstalledBrowser(
cache,
options.browser,
options.buildId,
options.platform,
);
if (!existsSync(installedBrowser.executablePath)) {
throw new Error(
`The browser folder (${outputPath}) exists but the executable (${installedBrowser.executablePath}) is missing`,
);
}
await runSetup(installedBrowser);
if (options.installDeps) {
await installDeps(installedBrowser);
}
return installedBrowser;
}
debugInstall(`Downloading binary from ${url}`);
try {
debugTime('download');
await downloadFile(url, archivePath, downloadProgressCallback);
} finally {
debugTimeEnd('download');
}
debugInstall(`Installing ${archivePath} to ${outputPath}`);
try {
debugTime('extract');
await unpackArchive(archivePath, outputPath);
} finally {
debugTimeEnd('extract');
}
const installedBrowser = new InstalledBrowser(
cache,
options.browser,
options.buildId,
options.platform,
);
if (options.buildIdAlias) {
const metadata = installedBrowser.readMetadata();
metadata.aliases[options.buildIdAlias] = options.buildId;
installedBrowser.writeMetadata(metadata);
}
await runSetup(installedBrowser);
if (options.installDeps) {
await installDeps(installedBrowser);
}
return installedBrowser;
} finally {
if (existsSync(archivePath)) {
await unlink(archivePath);
}
}
}
async function runSetup(installedBrowser: InstalledBrowser): Promise<void> {
// On Windows for Chrome invoke setup.exe to configure sandboxes.
if (
(installedBrowser.platform === BrowserPlatform.WIN32 ||
installedBrowser.platform === BrowserPlatform.WIN64) &&
installedBrowser.browser === Browser.CHROME &&
installedBrowser.platform === detectBrowserPlatform()
) {
try {
debugTime('permissions');
const browserDir = path.dirname(installedBrowser.executablePath);
const setupExePath = path.join(browserDir, 'setup.exe');
if (!existsSync(setupExePath)) {
return;
}
spawnSync(
path.join(browserDir, 'setup.exe'),
[`--configure-browser-in-directory=` + browserDir],
{
shell: true,
},
);
// TODO: Handle error here. Currently the setup.exe sometimes
// errors although it sets the permissions correctly.
} finally {
debugTimeEnd('permissions');
}
}
}
/**
* @public
*/
export interface UninstallOptions {
/**
* Determines the platform for the browser binary.
*
* @defaultValue **Auto-detected.**
*/
platform?: BrowserPlatform;
/**
* The path to the root of the cache directory.
*/
cacheDir: string;
/**
* Determines which browser to uninstall.
*/
browser: Browser;
/**
* The browser build to uninstall
*/
buildId: string;
}
/**
*
* @public
*/
export async function uninstall(options: UninstallOptions): Promise<void> {
options.platform ??= detectBrowserPlatform();
if (!options.platform) {
throw new Error(
`Cannot detect the browser platform for: ${os.platform()} (${os.arch()})`,
);
}
new Cache(options.cacheDir).uninstall(
options.browser,
options.platform,
options.buildId,
);
}
/**
* @public
*/
export interface GetInstalledBrowsersOptions {
/**
* The path to the root of the cache directory.
*/
cacheDir: string;
}
/**
* Returns metadata about browsers installed in the cache directory.
*
* @public
*/
export async function getInstalledBrowsers(
options: GetInstalledBrowsersOptions,
): Promise<InstalledBrowser[]> {
return new Cache(options.cacheDir).getInstalledBrowsers();
}
/**
* @public
*/
export async function canDownload(options: InstallOptions): Promise<boolean> {
options.platform ??= detectBrowserPlatform();
if (!options.platform) {
throw new Error(
`Cannot download a binary for the provided platform: ${os.platform()} (${os.arch()})`,
);
}
return await headHttpRequest(
getDownloadUrl(
options.browser,
options.platform,
options.buildId,
options.baseUrl,
),
);
}
/**
* Retrieves a URL for downloading the binary archive of a given browser.
*
* The archive is bound to the specific platform and build ID specified.
*
* @public
*/
export function getDownloadUrl(
browser: Browser,
platform: BrowserPlatform,
buildId: string,
baseUrl?: string,
): URL {
return new URL(downloadUrls[browser](platform, buildId, baseUrl));
}
/**
* @public
*/
export function makeProgressCallback(
browser: Browser,
buildId: string,
): (downloadedBytes: number, totalBytes: number) => void {
let progressBar: ProgressBar;
let lastDownloadedBytes = 0;
return (downloadedBytes: number, totalBytes: number) => {
if (!progressBar) {
progressBar = new ProgressBarClass(
`Downloading ${browser} ${buildId} - ${toMegabytes(
totalBytes,
)} [:bar] :percent :etas `,
{
complete: '=',
incomplete: ' ',
width: 20,
total: totalBytes,
},
);
}
const delta = downloadedBytes - lastDownloadedBytes;
lastDownloadedBytes = downloadedBytes;
progressBar.tick(delta);
};
}
function toMegabytes(bytes: number) {
const mb = bytes / 1000 / 1000;
return `${Math.round(mb * 10) / 10} MB`;
}

603
node_modules/@puppeteer/browsers/src/launch.ts generated vendored Normal file
View File

@@ -0,0 +1,603 @@
/**
* @license
* Copyright 2023 Google Inc.
* SPDX-License-Identifier: Apache-2.0
*/
import childProcess from 'node:child_process';
import {accessSync} from 'node:fs';
import os from 'node:os';
import readline from 'node:readline';
import {
type Browser,
type BrowserPlatform,
resolveSystemExecutablePath,
type ChromeReleaseChannel,
executablePathByBrowser,
} from './browser-data/browser-data.js';
import {Cache} from './Cache.js';
import {debug} from './debug.js';
import {detectBrowserPlatform} from './detectPlatform.js';
const debugLaunch = debug('puppeteer:browsers:launcher');
/**
* @public
*/
export interface ComputeExecutablePathOptions {
/**
* Root path to the storage directory.
*
* Can be set to `null` if the executable path should be relative
* to the extracted download location. E.g. `./chrome-linux64/chrome`.
*/
cacheDir: string | null;
/**
* Determines which platform the browser will be suited for.
*
* @defaultValue **Auto-detected.**
*/
platform?: BrowserPlatform;
/**
* Determines which browser to launch.
*/
browser: Browser;
/**
* Determines which buildId to download. BuildId should uniquely identify
* binaries and they are used for caching.
*/
buildId: string;
}
/**
* @public
*/
export function computeExecutablePath(
options: ComputeExecutablePathOptions,
): string {
if (options.cacheDir === null) {
options.platform ??= detectBrowserPlatform();
if (options.platform === undefined) {
throw new Error(
`No platform specified. Couldn't auto-detect browser platform.`,
);
}
return executablePathByBrowser[options.browser](
options.platform,
options.buildId,
);
}
return new Cache(options.cacheDir).computeExecutablePath(options);
}
/**
* @public
*/
export interface SystemOptions {
/**
* Determines which platform the browser will be suited for.
*
* @defaultValue **Auto-detected.**
*/
platform?: BrowserPlatform;
/**
* Determines which browser to launch.
*/
browser: Browser;
/**
* Release channel to look for on the system.
*/
channel: ChromeReleaseChannel;
}
/**
* Returns a path to a system-wide Chrome installation given a release channel
* name by checking known installation locations (using
* https://pptr.dev/browsers-api/browsers.computesystemexecutablepath/). If
* Chrome instance is not found at the expected path, an error is thrown.
*
* @public
*/
export function computeSystemExecutablePath(options: SystemOptions): string {
options.platform ??= detectBrowserPlatform();
if (!options.platform) {
throw new Error(
`Cannot download a binary for the provided platform: ${os.platform()} (${os.arch()})`,
);
}
const path = resolveSystemExecutablePath(
options.browser,
options.platform,
options.channel,
);
try {
accessSync(path);
} catch {
throw new Error(
`Could not find Google Chrome executable for channel '${options.channel}' at '${path}'.`,
);
}
return path;
}
/**
* @public
*/
export interface LaunchOptions {
/**
* Absolute path to the browser's executable.
*/
executablePath: string;
/**
* Configures stdio streams to open two additional streams for automation over
* those streams instead of WebSocket.
*
* @defaultValue `false`.
*/
pipe?: boolean;
/**
* If true, forwards the browser's process stdout and stderr to the Node's
* process stdout and stderr.
*
* @defaultValue `false`.
*/
dumpio?: boolean;
/**
* Additional arguments to pass to the executable when launching.
*/
args?: string[];
/**
* Environment variables to set for the browser process.
*/
env?: Record<string, string | undefined>;
/**
* Handles SIGINT in the Node process and tries to kill the browser process.
*
* @defaultValue `true`.
*/
handleSIGINT?: boolean;
/**
* Handles SIGTERM in the Node process and tries to gracefully close the browser
* process.
*
* @defaultValue `true`.
*/
handleSIGTERM?: boolean;
/**
* Handles SIGHUP in the Node process and tries to gracefully close the browser process.
*
* @defaultValue `true`.
*/
handleSIGHUP?: boolean;
/**
* Whether to spawn process in the {@link https://nodejs.org/api/child_process.html#optionsdetached | detached}
* mode.
*
* @defaultValue `true` except on Windows.
*/
detached?: boolean;
/**
* A callback to run after the browser process exits or before the process
* will be closed via the {@link Process.close} call (including when handling
* signals). The callback is only run once.
*/
onExit?: () => Promise<void>;
}
/**
* Launches a browser process according to {@link LaunchOptions}.
*
* @public
*/
export function launch(opts: LaunchOptions): Process {
return new Process(opts);
}
/**
* @public
*/
export const CDP_WEBSOCKET_ENDPOINT_REGEX =
/^DevTools listening on (ws:\/\/.*)$/;
/**
* @public
*/
export const WEBDRIVER_BIDI_WEBSOCKET_ENDPOINT_REGEX =
/^WebDriver BiDi listening on (ws:\/\/.*)$/;
type EventHandler = (...args: any[]) => void;
const processListeners = new Map<string, EventHandler[]>();
const dispatchers = {
exit: (...args: any[]) => {
processListeners.get('exit')?.forEach(handler => {
return handler(...args);
});
},
SIGINT: (...args: any[]) => {
processListeners.get('SIGINT')?.forEach(handler => {
return handler(...args);
});
},
SIGHUP: (...args: any[]) => {
processListeners.get('SIGHUP')?.forEach(handler => {
return handler(...args);
});
},
SIGTERM: (...args: any[]) => {
processListeners.get('SIGTERM')?.forEach(handler => {
return handler(...args);
});
},
};
function subscribeToProcessEvent(
event: 'exit' | 'SIGINT' | 'SIGHUP' | 'SIGTERM',
handler: EventHandler,
): void {
const listeners = processListeners.get(event) || [];
if (listeners.length === 0) {
process.on(event, dispatchers[event]);
}
listeners.push(handler);
processListeners.set(event, listeners);
}
function unsubscribeFromProcessEvent(
event: 'exit' | 'SIGINT' | 'SIGHUP' | 'SIGTERM',
handler: EventHandler,
): void {
const listeners = processListeners.get(event) || [];
const existingListenerIdx = listeners.indexOf(handler);
if (existingListenerIdx === -1) {
return;
}
listeners.splice(existingListenerIdx, 1);
processListeners.set(event, listeners);
if (listeners.length === 0) {
process.off(event, dispatchers[event]);
}
}
/**
* @public
*/
export class Process {
#executablePath;
#args: string[];
#browserProcess: childProcess.ChildProcess;
#exited = false;
// The browser process can be closed externally or from the driver process. We
// need to invoke the hooks only once though but we don't know how many times
// we will be invoked.
#hooksRan = false;
#onExitHook = async () => {};
#browserProcessExiting: Promise<void>;
constructor(opts: LaunchOptions) {
this.#executablePath = opts.executablePath;
this.#args = opts.args ?? [];
opts.pipe ??= false;
opts.dumpio ??= false;
opts.handleSIGINT ??= true;
opts.handleSIGTERM ??= true;
opts.handleSIGHUP ??= true;
// On non-windows platforms, `detached: true` makes child process a
// leader of a new process group, making it possible to kill child
// process tree with `.kill(-pid)` command. @see
// https://nodejs.org/api/child_process.html#child_process_options_detached
opts.detached ??= process.platform !== 'win32';
const stdio = this.#configureStdio({
pipe: opts.pipe,
dumpio: opts.dumpio,
});
const env = opts.env || {};
debugLaunch(`Launching ${this.#executablePath} ${this.#args.join(' ')}`, {
detached: opts.detached,
env: Object.keys(env).reduce<Record<string, string | undefined>>(
(res, key) => {
if (key.toLowerCase().startsWith('puppeteer_')) {
res[key] = env[key];
}
return res;
},
{},
),
stdio,
});
this.#browserProcess = childProcess.spawn(
this.#executablePath,
this.#args,
{
detached: opts.detached,
env,
stdio,
},
);
debugLaunch(`Launched ${this.#browserProcess.pid}`);
if (opts.dumpio) {
this.#browserProcess.stderr?.pipe(process.stderr);
this.#browserProcess.stdout?.pipe(process.stdout);
}
subscribeToProcessEvent('exit', this.#onDriverProcessExit);
if (opts.handleSIGINT) {
subscribeToProcessEvent('SIGINT', this.#onDriverProcessSignal);
}
if (opts.handleSIGTERM) {
subscribeToProcessEvent('SIGTERM', this.#onDriverProcessSignal);
}
if (opts.handleSIGHUP) {
subscribeToProcessEvent('SIGHUP', this.#onDriverProcessSignal);
}
if (opts.onExit) {
this.#onExitHook = opts.onExit;
}
this.#browserProcessExiting = new Promise((resolve, reject) => {
this.#browserProcess.once('exit', async () => {
debugLaunch(`Browser process ${this.#browserProcess.pid} onExit`);
this.#clearListeners();
this.#exited = true;
try {
await this.#runHooks();
} catch (err) {
reject(err);
return;
}
resolve();
});
});
}
async #runHooks() {
if (this.#hooksRan) {
return;
}
this.#hooksRan = true;
await this.#onExitHook();
}
get nodeProcess(): childProcess.ChildProcess {
return this.#browserProcess;
}
#configureStdio(opts: {
pipe: boolean;
dumpio: boolean;
}): Array<'ignore' | 'pipe'> {
if (opts.pipe) {
if (opts.dumpio) {
return ['ignore', 'pipe', 'pipe', 'pipe', 'pipe'];
} else {
return ['ignore', 'ignore', 'ignore', 'pipe', 'pipe'];
}
} else {
if (opts.dumpio) {
return ['pipe', 'pipe', 'pipe'];
} else {
return ['pipe', 'ignore', 'pipe'];
}
}
}
#clearListeners(): void {
unsubscribeFromProcessEvent('exit', this.#onDriverProcessExit);
unsubscribeFromProcessEvent('SIGINT', this.#onDriverProcessSignal);
unsubscribeFromProcessEvent('SIGTERM', this.#onDriverProcessSignal);
unsubscribeFromProcessEvent('SIGHUP', this.#onDriverProcessSignal);
}
#onDriverProcessExit = (_code: number) => {
this.kill();
};
#onDriverProcessSignal = (signal: string): void => {
switch (signal) {
case 'SIGINT':
this.kill();
process.exit(130);
case 'SIGTERM':
case 'SIGHUP':
void this.close();
break;
}
};
async close(): Promise<void> {
await this.#runHooks();
if (!this.#exited) {
this.kill();
}
return await this.#browserProcessExiting;
}
hasClosed(): Promise<void> {
return this.#browserProcessExiting;
}
kill(): void {
debugLaunch(`Trying to kill ${this.#browserProcess.pid}`);
// If the process failed to launch (for example if the browser executable path
// is invalid), then the process does not get a pid assigned. A call to
// `proc.kill` would error, as the `pid` to-be-killed can not be found.
if (
this.#browserProcess &&
this.#browserProcess.pid &&
pidExists(this.#browserProcess.pid)
) {
try {
debugLaunch(`Browser process ${this.#browserProcess.pid} exists`);
if (process.platform === 'win32') {
try {
childProcess.execSync(
`taskkill /pid ${this.#browserProcess.pid} /T /F`,
);
} catch (error) {
debugLaunch(
`Killing ${this.#browserProcess.pid} using taskkill failed`,
error,
);
// taskkill can fail to kill the process e.g. due to missing permissions.
// Let's kill the process via Node API. This delays killing of all child
// processes of `this.proc` until the main Node.js process dies.
this.#browserProcess.kill();
}
} else {
// on linux the process group can be killed with the group id prefixed with
// a minus sign. The process group id is the group leader's pid.
const processGroupId = -this.#browserProcess.pid;
try {
process.kill(processGroupId, 'SIGKILL');
} catch (error) {
debugLaunch(
`Killing ${this.#browserProcess.pid} using process.kill failed`,
error,
);
// Killing the process group can fail due e.g. to missing permissions.
// Let's kill the process via Node API. This delays killing of all child
// processes of `this.proc` until the main Node.js process dies.
this.#browserProcess.kill('SIGKILL');
}
}
} catch (error) {
throw new Error(
`${PROCESS_ERROR_EXPLANATION}\nError cause: ${
isErrorLike(error) ? error.stack : error
}`,
);
}
}
this.#clearListeners();
}
waitForLineOutput(regex: RegExp, timeout = 0): Promise<string> {
if (!this.#browserProcess.stderr) {
throw new Error('`browserProcess` does not have stderr.');
}
const rl = readline.createInterface(this.#browserProcess.stderr);
let stderr = '';
return new Promise((resolve, reject) => {
rl.on('line', onLine);
rl.on('close', onClose);
this.#browserProcess.on('exit', onClose);
this.#browserProcess.on('error', onClose);
const timeoutId =
timeout > 0 ? setTimeout(onTimeout, timeout) : undefined;
const cleanup = (): void => {
clearTimeout(timeoutId);
rl.off('line', onLine);
rl.off('close', onClose);
rl.close();
this.#browserProcess.off('exit', onClose);
this.#browserProcess.off('error', onClose);
};
function onClose(error?: Error): void {
cleanup();
reject(
new Error(
[
`Failed to launch the browser process!${
error ? ' ' + error.message : ''
}`,
stderr,
'',
'TROUBLESHOOTING: https://pptr.dev/troubleshooting',
'',
].join('\n'),
),
);
}
function onTimeout(): void {
cleanup();
reject(
new TimeoutError(
`Timed out after ${timeout} ms while waiting for the WS endpoint URL to appear in stdout!`,
),
);
}
function onLine(line: string): void {
stderr += line + '\n';
const match = line.match(regex);
if (!match) {
return;
}
cleanup();
// The RegExp matches, so this will obviously exist.
resolve(match[1]!);
}
});
}
}
const PROCESS_ERROR_EXPLANATION = `Puppeteer was unable to kill the process which ran the browser binary.
This means that, on future Puppeteer launches, Puppeteer might not be able to launch the browser.
Please check your open processes and ensure that the browser processes that Puppeteer launched have been killed.
If you think this is a bug, please report it on the Puppeteer issue tracker.`;
/**
* @internal
*/
function pidExists(pid: number): boolean {
try {
return process.kill(pid, 0);
} catch (error) {
if (isErrnoException(error)) {
if (error.code && error.code === 'ESRCH') {
return false;
}
}
throw error;
}
}
/**
* @internal
*/
export interface ErrorLike extends Error {
name: string;
message: string;
}
/**
* @internal
*/
export function isErrorLike(obj: unknown): obj is ErrorLike {
return (
typeof obj === 'object' && obj !== null && 'name' in obj && 'message' in obj
);
}
/**
* @internal
*/
export function isErrnoException(obj: unknown): obj is NodeJS.ErrnoException {
return (
isErrorLike(obj) &&
('errno' in obj || 'code' in obj || 'path' in obj || 'syscall' in obj)
);
}
/**
* @public
*/
export class TimeoutError extends Error {
/**
* @internal
*/
constructor(message?: string) {
super(message);
this.name = this.constructor.name;
Error.captureStackTrace(this, this.constructor);
}
}

11
node_modules/@puppeteer/browsers/src/main-cli.ts generated vendored Normal file
View File

@@ -0,0 +1,11 @@
#!/usr/bin/env node
/**
* @license
* Copyright 2023 Google Inc.
* SPDX-License-Identifier: Apache-2.0
*/
import {CLI} from './CLI.js';
void new CLI().run(process.argv);

51
node_modules/@puppeteer/browsers/src/main.ts generated vendored Normal file
View File

@@ -0,0 +1,51 @@
/**
* @license
* Copyright 2023 Google Inc.
* SPDX-License-Identifier: Apache-2.0
*/
export type {
LaunchOptions,
ComputeExecutablePathOptions as Options,
SystemOptions,
} from './launch.js';
export {
launch,
computeExecutablePath,
computeSystemExecutablePath,
TimeoutError,
CDP_WEBSOCKET_ENDPOINT_REGEX,
WEBDRIVER_BIDI_WEBSOCKET_ENDPOINT_REGEX,
Process,
} from './launch.js';
export type {
InstallOptions,
GetInstalledBrowsersOptions,
UninstallOptions,
} from './install.js';
export {
install,
makeProgressCallback,
getInstalledBrowsers,
canDownload,
uninstall,
getDownloadUrl,
} from './install.js';
export {detectBrowserPlatform} from './detectPlatform.js';
export type {ProfileOptions} from './browser-data/browser-data.js';
export {
resolveBuildId,
Browser,
BrowserPlatform,
ChromeReleaseChannel,
createProfile,
getVersionComparator,
} from './browser-data/browser-data.js';
export {CLI} from './CLI.js';
export {
Cache,
InstalledBrowser,
type Metadata,
type ComputeExecutablePathOptions,
} from './Cache.js';
export {BrowserTag} from './browser-data/types.js';

View File

@@ -0,0 +1,4 @@
/**
* @internal
*/
export const packageVersion = 'PACKAGE_VERSION';

View File

@@ -0,0 +1,8 @@
{
"extends": "../../../tsconfig.base.json",
"compilerOptions": {
"module": "NodeNext",
"moduleResolution": "NodeNext",
"outDir": "../lib/cjs"
}
}

View File

@@ -0,0 +1,6 @@
{
"extends": "../../../tsconfig.base.json",
"compilerOptions": {
"outDir": "../lib/esm"
}
}