diff --git a/package-lock.json b/package-lock.json index 2dcdbb9499e..10c0619a61c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2708,7 +2708,6 @@ "version": "8.0.1", "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", - "dev": true, "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", @@ -8426,10 +8425,9 @@ } }, "node_modules/yargs": { - "version": "17.6.0", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.6.0.tgz", - "integrity": "sha512-8H/wTDqlSwoSnScvV2N/JHfLWOKuh5MVla9hqLjK3nsfyy6Y4kDSYSvkU5YCUEPOSnRXfIyx3Sq+B/IWudTo4g==", - "dev": true, + "version": "17.6.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.6.2.tgz", + "integrity": "sha512-1/9UrdHjDZc0eOU0HxOHoS78C69UD3JRMvzlJ7S79S2nTaWRA/whGCTV8o9e/N/1Va9YIV7Q4sOxD8VV4pCWOw==", "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", @@ -8437,7 +8435,7 @@ "require-directory": "^2.1.1", "string-width": "^4.2.3", "y18n": "^5.0.5", - "yargs-parser": "^21.0.0" + "yargs-parser": "^21.1.1" }, "engines": { "node": ">=12" @@ -8500,7 +8498,6 @@ "version": "21.1.1", "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", - "dev": true, "engines": { "node": ">=12" } @@ -8571,12 +8568,18 @@ "debug": "4.3.4", "extract-zip": "2.0.1", "https-proxy-agent": "5.0.1", + "progress": "2.0.3", "proxy-from-env": "1.1.0", "tar-fs": "2.1.1", - "unbzip2-stream": "1.4.3" + "unbzip2-stream": "1.4.3", + "yargs": "17.6.2" + }, + "bin": { + "browsers": "lib/cjs/browsers.js" }, "devDependencies": { - "@types/node": "^14.15.0" + "@types/node": "^14.15.0", + "@types/yargs": "17.0.22" }, "engines": { "node": ">=14.1.0" @@ -8596,6 +8599,15 @@ "integrity": "sha512-FXKWbsJ6a1hIrRxv+FoukuHnGTgEzKYGi7kilfMae96AL9UNkPFNWJEEYWzdRI9ooIkbr4AKldyuSTLql06vLQ==", "dev": true }, + "packages/browsers/node_modules/@types/yargs": { + "version": "17.0.22", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.22.tgz", + "integrity": "sha512-pet5WJ9U8yPVRhkwuEIp5ktAeAqRZOq4UdAyWLWzxbtpyXnzbtLdKiXAjJzi/KLmPGS9wk86lUFWZFN6sISo4g==", + "dev": true, + "dependencies": { + "@types/yargs-parser": "*" + } + }, "packages/ng-schematics": { "name": "@puppeteer/ng-schematics", "version": "0.1.0", @@ -9965,12 +9977,15 @@ "version": "file:packages/browsers", "requires": { "@types/node": "^14.15.0", + "@types/yargs": "17.0.22", "debug": "4.3.4", "extract-zip": "2.0.1", "https-proxy-agent": "5.0.1", + "progress": "2.0.3", "proxy-from-env": "1.1.0", "tar-fs": "2.1.1", - "unbzip2-stream": "1.4.3" + "unbzip2-stream": "1.4.3", + "yargs": "17.6.2" }, "dependencies": { "@types/node": { @@ -9978,6 +9993,15 @@ "resolved": "https://registry.npmjs.org/@types/node/-/node-14.18.36.tgz", "integrity": "sha512-FXKWbsJ6a1hIrRxv+FoukuHnGTgEzKYGi7kilfMae96AL9UNkPFNWJEEYWzdRI9ooIkbr4AKldyuSTLql06vLQ==", "dev": true + }, + "@types/yargs": { + "version": "17.0.22", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.22.tgz", + "integrity": "sha512-pet5WJ9U8yPVRhkwuEIp5ktAeAqRZOq4UdAyWLWzxbtpyXnzbtLdKiXAjJzi/KLmPGS9wk86lUFWZFN6sISo4g==", + "dev": true, + "requires": { + "@types/yargs-parser": "*" + } } } }, @@ -11023,7 +11047,6 @@ "version": "8.0.1", "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", - "dev": true, "requires": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", @@ -15216,10 +15239,9 @@ "dev": true }, "yargs": { - "version": "17.6.0", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.6.0.tgz", - "integrity": "sha512-8H/wTDqlSwoSnScvV2N/JHfLWOKuh5MVla9hqLjK3nsfyy6Y4kDSYSvkU5YCUEPOSnRXfIyx3Sq+B/IWudTo4g==", - "dev": true, + "version": "17.6.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.6.2.tgz", + "integrity": "sha512-1/9UrdHjDZc0eOU0HxOHoS78C69UD3JRMvzlJ7S79S2nTaWRA/whGCTV8o9e/N/1Va9YIV7Q4sOxD8VV4pCWOw==", "requires": { "cliui": "^8.0.1", "escalade": "^3.1.1", @@ -15227,14 +15249,13 @@ "require-directory": "^2.1.1", "string-width": "^4.2.3", "y18n": "^5.0.5", - "yargs-parser": "^21.0.0" + "yargs-parser": "^21.1.1" }, "dependencies": { "yargs-parser": { "version": "21.1.1", "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", - "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", - "dev": true + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==" } } }, diff --git a/package.json b/package.json index f87f9acd79d..cad7bbd16e9 100644 --- a/package.json +++ b/package.json @@ -40,6 +40,7 @@ "wireit": { "build": { "dependencies": [ + "./packages/browsers:build", "./packages/ng-schematics:build", "./packages/puppeteer-core:build", "./packages/puppeteer:build", diff --git a/packages/browsers/package.json b/packages/browsers/package.json index ad638765aeb..72a3fd39733 100644 --- a/packages/browsers/package.json +++ b/packages/browsers/package.json @@ -8,6 +8,9 @@ "clean": "tsc --build --clean && rimraf lib", "test": "wireit" }, + "bin": { + "@puppeteer/browsers": "lib/cjs/browsers.js" + }, "wireit": { "build": { "command": "tsc -b", @@ -63,12 +66,15 @@ "debug": "4.3.4", "extract-zip": "2.0.1", "https-proxy-agent": "5.0.1", + "progress": "2.0.3", "proxy-from-env": "1.1.0", "tar-fs": "2.1.1", - "unbzip2-stream": "1.4.3" + "unbzip2-stream": "1.4.3", + "yargs": "17.6.2" }, "devDependencies": { - "@types/node": "^14.15.0" + "@types/node": "^14.15.0", + "@types/yargs": "17.0.22" }, "peerDependencies": { "typescript": ">= 4.7.4" diff --git a/packages/browsers/src/CLI.ts b/packages/browsers/src/CLI.ts new file mode 100644 index 00000000000..683ff4faf94 --- /dev/null +++ b/packages/browsers/src/CLI.ts @@ -0,0 +1,107 @@ +import yargs from 'yargs'; +import ProgressBar from 'progress'; +import {hideBin} from 'yargs/helpers'; +import {Browser, BrowserPlatform} from './browsers/types.js'; +import {fetch} from './fetch.js'; +import path from 'path'; + +type Arguments = { + browser: { + name: Browser; + revision: string; + }; + path?: string; + platform?: BrowserPlatform; +}; + +export class CLI { + #cachePath; + + constructor(cachePath = process.cwd()) { + this.#cachePath = cachePath; + } + + async run(argv: string[]): Promise { + await yargs(hideBin(argv)) + .command( + '$0 install ', + 'run files', + yargs => { + yargs.positional('browser', { + description: 'The browser version', + type: 'string', + coerce: (opt): Arguments['browser'] => { + return { + name: this.#parseBrowser(opt), + revision: this.#parseRevision(opt), + }; + }, + }); + }, + async argv => { + const args = argv as unknown as Arguments; + await fetch({ + browser: args.browser.name, + revision: args.browser.revision, + platform: args.platform, + outputDir: path.join( + args.path ?? this.#cachePath, + args.browser.name + ), + progressCallback: this.#makeProgressBar( + args.browser.name, + args.browser.revision + ), + }); + } + ) + .option('path', { + type: 'string', + desc: 'Path where the browsers will be downloaded to and installed from', + default: process.cwd(), + }) + .option('platform', { + type: 'string', + desc: 'Platform that the binary needs to be compatible with.', + choices: Object.values(BrowserPlatform), + defaultDescription: 'Auto-detected by default.', + }) + .parse(); + } + + #parseBrowser(version: string): Browser { + return version.split('@').shift() as Browser; + } + + #parseRevision(version: string): string { + return version.split('@').pop() ?? 'latest'; + } + + #toMegabytes(bytes: number) { + const mb = bytes / 1024 / 1024; + return `${Math.round(mb * 10) / 10} Mb`; + } + + #makeProgressBar(browser: Browser, revision: string) { + let progressBar: ProgressBar | null = null; + let lastDownloadedBytes = 0; + return (downloadedBytes: number, totalBytes: number) => { + if (!progressBar) { + progressBar = new ProgressBar( + `Downloading ${browser} r${revision} - ${this.#toMegabytes( + totalBytes + )} [:bar] :percent :etas `, + { + complete: '=', + incomplete: ' ', + width: 20, + total: totalBytes, + } + ); + } + const delta = downloadedBytes - lastDownloadedBytes; + lastDownloadedBytes = downloadedBytes; + progressBar.tick(delta); + }; + } +} diff --git a/packages/browsers/src/browsers.ts b/packages/browsers/src/browsers.ts new file mode 100644 index 00000000000..21c69ba883d --- /dev/null +++ b/packages/browsers/src/browsers.ts @@ -0,0 +1,5 @@ +#!/usr/bin/env node + +import {CLI} from './CLI.js'; + +new CLI().run(process.argv); diff --git a/packages/browsers/test/src/cli.spec.ts b/packages/browsers/test/src/cli.spec.ts new file mode 100644 index 00000000000..01281b73143 --- /dev/null +++ b/packages/browsers/test/src/cli.spec.ts @@ -0,0 +1,67 @@ +/** + * Copyright 2023 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {CLI} from '../../lib/cjs/CLI.js'; +import fs from 'fs'; +import path from 'path'; +import os from 'os'; +import assert from 'assert'; + +describe('CLI', function () { + this.timeout(60000); + + let tmpDir = '/tmp/puppeteer-browsers-test'; + const testChromeRevision = '1083080'; + const testFirefoxRevision = '111.0a1'; + + beforeEach(() => { + tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'puppeteer-browsers-test')); + }); + + afterEach(() => { + fs.rmSync(tmpDir, {recursive: true}); + }); + + it('should download Chromium binaries', async () => { + await new CLI(tmpDir).run([ + 'npx', + '@puppeteer/browsers', + 'install', + `chrome@${testChromeRevision}`, + `--path=${tmpDir}`, + '--platform=linux', + ]); + assert.ok( + fs.existsSync(path.join(tmpDir, 'chrome', `linux-${testChromeRevision}`)) + ); + }); + + it('should download Firefox binaries', async () => { + await new CLI(tmpDir).run([ + 'npx', + '@puppeteer/browsers', + 'install', + `firefox@${testFirefoxRevision}`, + `--path=${tmpDir}`, + '--platform=linux', + ]); + assert.ok( + fs.existsSync( + path.join(tmpDir, 'firefox', `linux-${testFirefoxRevision}`) + ) + ); + }); +});