初始化项目
This commit is contained in:
+487
@@ -0,0 +1,487 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2023 Google Inc.
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import {stdin as input, stdout as output} from 'process';
|
||||
import * as readline from 'readline';
|
||||
|
||||
import ProgressBar from 'progress';
|
||||
import type * as Yargs from 'yargs';
|
||||
import {hideBin} from 'yargs/helpers';
|
||||
import yargs from 'yargs/yargs';
|
||||
|
||||
import {
|
||||
resolveBuildId,
|
||||
type Browser,
|
||||
BrowserPlatform,
|
||||
type ChromeReleaseChannel,
|
||||
} from './browser-data/browser-data.js';
|
||||
import {Cache} from './Cache.js';
|
||||
import {detectBrowserPlatform} from './detectPlatform.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;
|
||||
}
|
||||
|
||||
interface LaunchArgs {
|
||||
browser: {
|
||||
name: Browser;
|
||||
buildId: string;
|
||||
};
|
||||
path?: string;
|
||||
platform?: BrowserPlatform;
|
||||
detached: boolean;
|
||||
system: boolean;
|
||||
}
|
||||
|
||||
interface ClearArgs {
|
||||
path?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export class CLI {
|
||||
#cachePath;
|
||||
#rl?: readline.Interface;
|
||||
#scriptName = '';
|
||||
#allowCachePathOverride = true;
|
||||
#pinnedBrowsers?: Partial<{
|
||||
[key in Browser]: {
|
||||
buildId: string;
|
||||
skipDownload: boolean;
|
||||
};
|
||||
}>;
|
||||
#prefixCommand?: {cmd: string; description: string};
|
||||
|
||||
constructor(
|
||||
opts?:
|
||||
| string
|
||||
| {
|
||||
cachePath?: string;
|
||||
scriptName?: string;
|
||||
prefixCommand?: {cmd: string; description: string};
|
||||
allowCachePathOverride?: boolean;
|
||||
pinnedBrowsers?: Partial<{
|
||||
[key in 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.#allowCachePathOverride = opts.allowCachePathOverride ?? true;
|
||||
this.#pinnedBrowsers = opts.pinnedBrowsers;
|
||||
this.#prefixCommand = opts.prefixCommand;
|
||||
}
|
||||
|
||||
#defineBrowserParameter(yargs: Yargs.Argv<unknown>): void {
|
||||
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): InstallArgs['browser'] => {
|
||||
return {
|
||||
name: this.#parseBrowser(opt),
|
||||
buildId: this.#parseBuildId(opt),
|
||||
};
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
#definePlatformParameter(yargs: Yargs.Argv<unknown>): void {
|
||||
yargs.option('platform', {
|
||||
type: 'string',
|
||||
desc: 'Platform that the binary needs to be compatible with.',
|
||||
choices: Object.values(BrowserPlatform),
|
||||
defaultDescription: 'Auto-detected',
|
||||
});
|
||||
}
|
||||
|
||||
#definePathParameter(yargs: Yargs.Argv<unknown>, required = false): void {
|
||||
if (!this.#allowCachePathOverride) {
|
||||
return;
|
||||
}
|
||||
yargs.option('path', {
|
||||
type: 'string',
|
||||
desc: 'Path to the root folder for the browser downloads and installation. The installation folder structure is compatible with the cache structure used by Puppeteer.',
|
||||
defaultDescription: 'Current working directory',
|
||||
...(required ? {} : {default: process.cwd()}),
|
||||
});
|
||||
if (required) {
|
||||
yargs.demandOption('path');
|
||||
}
|
||||
}
|
||||
|
||||
async run(argv: string[]): Promise<void> {
|
||||
const yargsInstance = yargs(hideBin(argv));
|
||||
let target = yargsInstance.scriptName(this.#scriptName);
|
||||
if (this.#prefixCommand) {
|
||||
target = target.command(
|
||||
this.#prefixCommand.cmd,
|
||||
this.#prefixCommand.description,
|
||||
yargs => {
|
||||
return this.#build(yargs);
|
||||
}
|
||||
);
|
||||
} else {
|
||||
target = this.#build(target);
|
||||
}
|
||||
await target
|
||||
.demandCommand(1)
|
||||
.help()
|
||||
.wrap(Math.min(120, yargsInstance.terminalWidth()))
|
||||
.parse();
|
||||
}
|
||||
|
||||
#build(yargs: Yargs.Argv<unknown>): Yargs.Argv<unknown> {
|
||||
const latestOrPinned = this.#pinnedBrowsers ? 'pinned' : 'latest';
|
||||
// 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 => {
|
||||
this.#defineBrowserParameter(yargs);
|
||||
this.#definePlatformParameter(yargs);
|
||||
this.#definePathParameter(yargs);
|
||||
yargs.option('base-url', {
|
||||
type: 'string',
|
||||
desc: 'Base URL to download from',
|
||||
});
|
||||
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.`
|
||||
);
|
||||
yargs.example(
|
||||
'$0 install chrome@latest',
|
||||
'Install the latest available build for the Chrome browser.'
|
||||
);
|
||||
yargs.example(
|
||||
'$0 install chrome@stable',
|
||||
'Install the latest available build for the Chrome browser from the stable channel.'
|
||||
);
|
||||
yargs.example(
|
||||
'$0 install chrome@beta',
|
||||
'Install the latest available build for the Chrome browser from the beta channel.'
|
||||
);
|
||||
yargs.example(
|
||||
'$0 install chrome@dev',
|
||||
'Install the latest available build for the Chrome browser from the dev channel.'
|
||||
);
|
||||
yargs.example(
|
||||
'$0 install chrome@canary',
|
||||
'Install the latest available build for the Chrome Canary browser.'
|
||||
);
|
||||
yargs.example(
|
||||
'$0 install chrome@115',
|
||||
'Install the latest available build for Chrome 115.'
|
||||
);
|
||||
yargs.example(
|
||||
'$0 install chromedriver@canary',
|
||||
'Install the latest available build for ChromeDriver Canary.'
|
||||
);
|
||||
yargs.example(
|
||||
'$0 install chromedriver@115',
|
||||
'Install the latest available build for ChromeDriver 115.'
|
||||
);
|
||||
yargs.example(
|
||||
'$0 install chromedriver@115.0.5790',
|
||||
'Install the latest available patch (115.0.5790.X) build for ChromeDriver.'
|
||||
);
|
||||
yargs.example(
|
||||
'$0 install chrome-headless-shell',
|
||||
'Install the latest available chrome-headless-shell build.'
|
||||
);
|
||||
yargs.example(
|
||||
'$0 install chrome-headless-shell@beta',
|
||||
'Install the latest available chrome-headless-shell build corresponding to the Beta channel.'
|
||||
);
|
||||
yargs.example(
|
||||
'$0 install chrome-headless-shell@118',
|
||||
'Install the latest available chrome-headless-shell 118 build.'
|
||||
);
|
||||
yargs.example(
|
||||
'$0 install chromium@1083080',
|
||||
'Install the revision 1083080 of the Chromium browser.'
|
||||
);
|
||||
yargs.example(
|
||||
'$0 install firefox',
|
||||
'Install the latest nightly available build of the Firefox browser.'
|
||||
);
|
||||
yargs.example(
|
||||
'$0 install firefox@stable',
|
||||
'Install the latest stable build of the Firefox browser.'
|
||||
);
|
||||
yargs.example(
|
||||
'$0 install firefox@beta',
|
||||
'Install the latest beta build of the Firefox browser.'
|
||||
);
|
||||
yargs.example(
|
||||
'$0 install firefox@devedition',
|
||||
'Install the latest devedition build of the Firefox browser.'
|
||||
);
|
||||
yargs.example(
|
||||
'$0 install firefox@esr',
|
||||
'Install the latest ESR build of the Firefox browser.'
|
||||
);
|
||||
yargs.example(
|
||||
'$0 install firefox@nightly',
|
||||
'Install the latest nightly build of the Firefox browser.'
|
||||
);
|
||||
yargs.example(
|
||||
'$0 install firefox@stable_111.0.1',
|
||||
'Install a specific version of the Firefox browser.'
|
||||
);
|
||||
yargs.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.'
|
||||
);
|
||||
}
|
||||
},
|
||||
async argv => {
|
||||
const args = argv as unknown as InstallArgs;
|
||||
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({
|
||||
...argv,
|
||||
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 => {
|
||||
this.#defineBrowserParameter(yargs);
|
||||
this.#definePlatformParameter(yargs);
|
||||
this.#definePathParameter(yargs);
|
||||
yargs.option('detached', {
|
||||
type: 'boolean',
|
||||
desc: 'Detach the child process.',
|
||||
default: false,
|
||||
});
|
||||
yargs.option('system', {
|
||||
type: 'boolean',
|
||||
desc: 'Search for a browser installed on the system instead of the cache folder.',
|
||||
default: false,
|
||||
});
|
||||
yargs.example(
|
||||
'$0 launch chrome@115.0.5790.170',
|
||||
'Launch Chrome 115.0.5790.170'
|
||||
);
|
||||
yargs.example(
|
||||
'$0 launch firefox@112.0a1',
|
||||
'Launch the Firefox browser identified by the milestone 112.0a1.'
|
||||
);
|
||||
yargs.example(
|
||||
'$0 launch chrome@115.0.5790.170 --detached',
|
||||
'Launch the browser but detach the sub-processes.'
|
||||
);
|
||||
yargs.example(
|
||||
'$0 launch chrome@canary --system',
|
||||
'Try to locate the Canary build of Chrome installed on the system and launch it.'
|
||||
);
|
||||
},
|
||||
async argv => {
|
||||
const args = argv as unknown as LaunchArgs;
|
||||
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({
|
||||
executablePath,
|
||||
detached: args.detached,
|
||||
});
|
||||
}
|
||||
)
|
||||
.command(
|
||||
'clear',
|
||||
this.#allowCachePathOverride
|
||||
? 'Removes all installed browsers from the specified cache directory'
|
||||
: `Removes all installed browsers from ${this.#cachePath}`,
|
||||
yargs => {
|
||||
this.#definePathParameter(yargs, true);
|
||||
},
|
||||
async argv => {
|
||||
const args = argv as unknown as ClearArgs;
|
||||
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.`);
|
||||
}
|
||||
);
|
||||
}
|
||||
)
|
||||
.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';
|
||||
}
|
||||
|
||||
async #install(args: InstallArgs) {
|
||||
args.platform ??= detectBrowserPlatform();
|
||||
if (!args.browser) {
|
||||
throw new Error(`No browser arg proveded`);
|
||||
}
|
||||
if (!args.platform) {
|
||||
throw new Error(`Could not resolve the current platform`);
|
||||
}
|
||||
if (args.browser.buildId === 'pinned') {
|
||||
const options = this.#pinnedBrowsers?.[args.browser.name];
|
||||
if (!options || !options.buildId) {
|
||||
throw new Error(`No pinned version found for ${args.browser.name}`);
|
||||
}
|
||||
args.browser.buildId = options.buildId;
|
||||
}
|
||||
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: makeProgressCallback(
|
||||
args.browser.name,
|
||||
args.browser.buildId
|
||||
),
|
||||
baseUrl: args.baseUrl,
|
||||
buildIdAlias:
|
||||
originalBuildId !== args.browser.buildId ? originalBuildId : undefined,
|
||||
});
|
||||
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,
|
||||
})}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @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 ProgressBar(
|
||||
`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`;
|
||||
}
|
||||
+274
@@ -0,0 +1,274 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2023 Google Inc.
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import fs from 'fs';
|
||||
import os from 'os';
|
||||
import path from '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;
|
||||
}
|
||||
|
||||
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};
|
||||
}
|
||||
+252
@@ -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
|
||||
): 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];
|
||||
}
|
||||
Generated
Vendored
+69
@@ -0,0 +1,69 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2023 Google Inc.
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
import path from 'path';
|
||||
|
||||
import {BrowserPlatform} from './types.js';
|
||||
|
||||
function folder(platform: BrowserPlatform): string {
|
||||
switch (platform) {
|
||||
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:
|
||||
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';
|
||||
+213
@@ -0,0 +1,213 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2023 Google Inc.
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import path from '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:
|
||||
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:
|
||||
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: {
|
||||
[channel in 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:
|
||||
switch (channel) {
|
||||
case ChromeReleaseChannel.STABLE:
|
||||
return '/opt/google/chrome/chrome';
|
||||
case ChromeReleaseChannel.BETA:
|
||||
return '/opt/google/chrome-beta/chrome';
|
||||
case ChromeReleaseChannel.DEV:
|
||||
return '/opt/google/chrome-unstable/chrome';
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error(
|
||||
`Unable to detect browser executable path for '${channel}' on ${platform}.`
|
||||
);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
+56
@@ -0,0 +1,56 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2023 Google Inc.
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
import path from 'path';
|
||||
|
||||
import {BrowserPlatform} from './types.js';
|
||||
|
||||
function folder(platform: BrowserPlatform): string {
|
||||
switch (platform) {
|
||||
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:
|
||||
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';
|
||||
+92
@@ -0,0 +1,92 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2023 Google Inc.
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import path from 'path';
|
||||
|
||||
import {getText} from '../httpUtil.js';
|
||||
|
||||
import {BrowserPlatform} from './types.js';
|
||||
|
||||
function archive(platform: BrowserPlatform, buildId: string): string {
|
||||
switch (platform) {
|
||||
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:
|
||||
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:
|
||||
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);
|
||||
}
|
||||
+437
@@ -0,0 +1,437 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2023 Google Inc.
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
|
||||
import {getJSON} from '../httpUtil.js';
|
||||
|
||||
import {BrowserPlatform, type ProfileOptions} from './types.js';
|
||||
|
||||
function archiveNightly(platform: BrowserPlatform, buildId: string): string {
|
||||
switch (platform) {
|
||||
case BrowserPlatform.LINUX:
|
||||
return `firefox-${buildId}.en-US.${platform}-x86_64.tar.bz2`;
|
||||
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:
|
||||
return `firefox-${buildId}.tar.bz2`;
|
||||
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.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, resolvedBuildId] = 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;
|
||||
}
|
||||
switch (channel) {
|
||||
case FirefoxChannel.NIGHTLY:
|
||||
return `${baseUrl}/${resolveDownloadPath(platform, resolvedBuildId).join('/')}`;
|
||||
case FirefoxChannel.DEVEDITION:
|
||||
case FirefoxChannel.BETA:
|
||||
case FirefoxChannel.STABLE:
|
||||
case FirefoxChannel.ESR:
|
||||
return `${baseUrl}/${resolvedBuildId}/${platformName(platform)}/en-US/${archive(platform, resolvedBuildId)}`;
|
||||
}
|
||||
}
|
||||
|
||||
export function resolveDownloadPath(
|
||||
platform: BrowserPlatform,
|
||||
buildId: string
|
||||
): string[] {
|
||||
return [archiveNightly(platform, buildId)];
|
||||
}
|
||||
|
||||
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:
|
||||
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:
|
||||
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,-cm,-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,
|
||||
|
||||
// Disabled screenshots extension
|
||||
'extensions.screenshots.disabled': true,
|
||||
|
||||
// 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,
|
||||
|
||||
// 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);
|
||||
}
|
||||
+64
@@ -0,0 +1,64 @@
|
||||
/**
|
||||
* @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',
|
||||
MAC = 'mac',
|
||||
MAC_ARM = 'mac_arm',
|
||||
WIN32 = 'win32',
|
||||
WIN64 = 'win64',
|
||||
}
|
||||
|
||||
/**
|
||||
* @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
@@ -0,0 +1,9 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2023 Google Inc.
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import debug from 'debug';
|
||||
|
||||
export {debug};
|
||||
+51
@@ -0,0 +1,51 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2023 Google Inc.
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import os from 'os';
|
||||
|
||||
import {BrowserPlatform} from './browser-data/browser-data.js';
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export function detectBrowserPlatform(): BrowserPlatform | undefined {
|
||||
const platform = os.platform();
|
||||
switch (platform) {
|
||||
case 'darwin':
|
||||
return os.arch() === 'arm64'
|
||||
? BrowserPlatform.MAC_ARM
|
||||
: BrowserPlatform.MAC;
|
||||
case 'linux':
|
||||
return BrowserPlatform.LINUX;
|
||||
case 'win32':
|
||||
return os.arch() === 'x64' ||
|
||||
// Windows 11 for ARM supports x64 emulation
|
||||
(os.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;
|
||||
}
|
||||
+91
@@ -0,0 +1,91 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2023 Google Inc.
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import {spawnSync} from 'child_process';
|
||||
import {createReadStream} from 'fs';
|
||||
import {mkdir, readdir} from 'fs/promises';
|
||||
import * as path from 'path';
|
||||
|
||||
import extractZip from 'extract-zip';
|
||||
import tar from 'tar-fs';
|
||||
import bzip from 'unbzip2-stream';
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
export async function unpackArchive(
|
||||
archivePath: string,
|
||||
folderPath: string
|
||||
): Promise<void> {
|
||||
if (archivePath.endsWith('.zip')) {
|
||||
await extractZip(archivePath, {dir: folderPath});
|
||||
} else if (archivePath.endsWith('.tar.bz2')) {
|
||||
await extractTar(archivePath, folderPath);
|
||||
} 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 {
|
||||
throw new Error(`Unsupported archive format: ${archivePath}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
function extractTar(tarPath: string, folderPath: string): Promise<void> {
|
||||
return new Promise((fulfill, reject) => {
|
||||
const tarStream = tar.extract(folderPath);
|
||||
tarStream.on('error', reject);
|
||||
tarStream.on('finish', fulfill);
|
||||
const readStream = createReadStream(tarPath);
|
||||
readStream.pipe(bzip()).pipe(tarStream);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @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']);
|
||||
}
|
||||
}
|
||||
+154
@@ -0,0 +1,154 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2023 Google Inc.
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import {createWriteStream} from 'fs';
|
||||
import * as http from 'http';
|
||||
import * as https from 'https';
|
||||
import {URL, urlToHttpOptions} from '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('finish', () => {
|
||||
return resolve();
|
||||
});
|
||||
file.on('error', error => {
|
||||
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);
|
||||
});
|
||||
});
|
||||
}
|
||||
+397
@@ -0,0 +1,397 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2017 Google Inc.
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import assert from 'assert';
|
||||
import {spawnSync} from 'child_process';
|
||||
import {existsSync} from 'fs';
|
||||
import {mkdir, unlink} from 'fs/promises';
|
||||
import os from 'os';
|
||||
import path from 'path';
|
||||
|
||||
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.
|
||||
*/
|
||||
downloadProgressCallback?: (
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export function install(
|
||||
options: InstallOptions & {unpack?: true}
|
||||
): Promise<InstalledBrowser>;
|
||||
/**
|
||||
* @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 url = version.downloads[options.browser]?.find(link => {
|
||||
return link['platform'] === platform;
|
||||
})?.url;
|
||||
if (url) {
|
||||
debugInstall(`Falling back to downloading from ${url}.`);
|
||||
return await installUrl(new URL(url), options);
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
default:
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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()})`
|
||||
);
|
||||
}
|
||||
const fileName = 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, options.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);
|
||||
return installedBrowser;
|
||||
}
|
||||
debugInstall(`Downloading binary from ${url}`);
|
||||
try {
|
||||
debugTime('download');
|
||||
await downloadFile(url, archivePath, options.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);
|
||||
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
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function getDownloadUrl(
|
||||
browser: Browser,
|
||||
platform: BrowserPlatform,
|
||||
buildId: string,
|
||||
baseUrl?: string
|
||||
): URL {
|
||||
return new URL(downloadUrls[browser](platform, buildId, baseUrl));
|
||||
}
|
||||
+532
@@ -0,0 +1,532 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2023 Google Inc.
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import childProcess from 'child_process';
|
||||
import {accessSync} from 'fs';
|
||||
import os from 'os';
|
||||
import readline from 'readline';
|
||||
|
||||
import {
|
||||
type Browser,
|
||||
type BrowserPlatform,
|
||||
resolveSystemExecutablePath,
|
||||
type ChromeReleaseChannel,
|
||||
} 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.
|
||||
*/
|
||||
cacheDir: string;
|
||||
/**
|
||||
* 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 {
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* @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 (error) {
|
||||
throw new Error(
|
||||
`Could not find Google Chrome executable for channel '${options.channel}' at '${path}'.`
|
||||
);
|
||||
}
|
||||
return path;
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export interface LaunchOptions {
|
||||
executablePath: string;
|
||||
pipe?: boolean;
|
||||
dumpio?: boolean;
|
||||
args?: string[];
|
||||
env?: Record<string, string | undefined>;
|
||||
handleSIGINT?: boolean;
|
||||
handleSIGTERM?: boolean;
|
||||
handleSIGHUP?: boolean;
|
||||
detached?: boolean;
|
||||
onExit?: () => Promise<void>;
|
||||
}
|
||||
|
||||
/**
|
||||
* @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 => {
|
||||
if (timeoutId) {
|
||||
clearTimeout(timeoutId);
|
||||
}
|
||||
rl.off('line', onLine);
|
||||
rl.off('close', onClose);
|
||||
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
@@ -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);
|
||||
+43
@@ -0,0 +1,43 @@
|
||||
/**
|
||||
* @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,
|
||||
getInstalledBrowsers,
|
||||
canDownload,
|
||||
uninstall,
|
||||
} 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, makeProgressCallback} from './CLI.js';
|
||||
export {Cache, InstalledBrowser} from './Cache.js';
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"extends": "../../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"module": "NodeNext",
|
||||
"moduleResolution": "NodeNext",
|
||||
"outDir": "../lib/cjs"
|
||||
}
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"extends": "../../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "../lib/esm"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user