From b8d38cb05f7eedf554ed46f2f7428b621197d1cc Mon Sep 17 00:00:00 2001 From: Alex Rudenko Date: Thu, 16 Mar 2023 14:05:31 +0100 Subject: [PATCH] feat: implement a command to clear the cache (#9868) --- package-lock.json | 4 +- packages/browsers/src/CLI.ts | 44 +++++++++++++++-- .../src/{CacheStructure.ts => Cache.ts} | 8 +++- packages/browsers/src/fetch.ts | 4 +- packages/browsers/src/launcher.ts | 4 +- packages/browsers/src/main.ts | 1 + packages/browsers/test/src/cli.spec.ts | 47 +++++++++++++++++-- packages/browsers/test/src/fetch.spec.ts | 12 +++-- packages/browsers/test/src/launcher.spec.ts | 7 ++- 9 files changed, 109 insertions(+), 22 deletions(-) rename packages/browsers/src/{CacheStructure.ts => Cache.ts} (93%) diff --git a/package-lock.json b/package-lock.json index 4aa32b42961..d406bb663d1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9123,7 +9123,7 @@ }, "packages/browsers": { "name": "@puppeteer/browsers", - "version": "0.0.5", + "version": "0.1.1", "license": "Apache-2.0", "dependencies": { "debug": "4.3.4", @@ -9137,7 +9137,7 @@ "yargs": "17.7.1" }, "bin": { - "browsers": "lib/cjs/browsers.js" + "browsers": "lib/cjs/main-cli.js" }, "devDependencies": { "@types/node": "^14.15.0", diff --git a/packages/browsers/src/CLI.ts b/packages/browsers/src/CLI.ts index 01353f64c13..d6431c7fb81 100644 --- a/packages/browsers/src/CLI.ts +++ b/packages/browsers/src/CLI.ts @@ -14,6 +14,9 @@ * limitations under the License. */ +import {stdin as input, stdout as output} from 'process'; +import * as readline from 'readline'; + import ProgressBar from 'progress'; import yargs from 'yargs'; import {hideBin} from 'yargs/helpers'; @@ -24,6 +27,7 @@ import { BrowserPlatform, ChromeReleaseChannel, } from './browser-data/browser-data.js'; +import {Cache} from './Cache.js'; import {detectBrowserPlatform} from './detectPlatform.js'; import {fetch} from './fetch.js'; import { @@ -52,11 +56,17 @@ type LaunchArgs = { system: boolean; }; +type ClearArgs = { + path?: string; +}; + export class CLI { #cachePath; + #rl?: readline.Interface; - constructor(cachePath = process.cwd()) { + constructor(cachePath = process.cwd(), rl?: readline.Interface) { this.#cachePath = cachePath; + this.#rl = rl; } #defineBrowserParameter(yargs: yargs.Argv): void { @@ -82,13 +92,16 @@ export class CLI { }); } - #definePathParameter(yargs: yargs.Argv): void { + #definePathParameter(yargs: yargs.Argv, required = false): void { 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.', - default: process.cwd(), defaultDescription: 'Current working directory', + ...(required ? {} : {default: process.cwd()}), }); + if (required) { + yargs.demandOption('path'); + } } async run(argv: string[]): Promise { @@ -214,6 +227,31 @@ export class CLI { }); } ) + .command( + 'clear', + 'Removes all installed browsers from the specified cache directory', + 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() .wrap(Math.min(120, yargs.terminalWidth())) diff --git a/packages/browsers/src/CacheStructure.ts b/packages/browsers/src/Cache.ts similarity index 93% rename from packages/browsers/src/CacheStructure.ts rename to packages/browsers/src/Cache.ts index 998d0967370..457a7a8e6a7 100644 --- a/packages/browsers/src/CacheStructure.ts +++ b/packages/browsers/src/Cache.ts @@ -16,6 +16,8 @@ import path from 'path'; +import rimraf from 'rimraf'; + import {Browser, BrowserPlatform} from './browser-data/browser-data.js'; /** @@ -32,7 +34,7 @@ import {Browser, BrowserPlatform} from './browser-data/browser-data.js'; * ------ specific structure. * @internal */ -export class CacheStructure { +export class Cache { #rootDir: string; constructor(rootDir: string) { @@ -50,4 +52,8 @@ export class CacheStructure { ): string { return path.join(this.browserRoot(browser), `${platform}-${buildId}`); } + + clear(): void { + rimraf.sync(this.#rootDir); + } } diff --git a/packages/browsers/src/fetch.ts b/packages/browsers/src/fetch.ts index fd54b129bc9..25690b1662b 100644 --- a/packages/browsers/src/fetch.ts +++ b/packages/browsers/src/fetch.ts @@ -25,7 +25,7 @@ import { BrowserPlatform, downloadUrls, } from './browser-data/browser-data.js'; -import {CacheStructure} from './CacheStructure.js'; +import {Cache} from './Cache.js'; import {debug} from './debug.js'; import {detectBrowserPlatform} from './detectPlatform.js'; import {unpackArchive} from './fileUtil.js'; @@ -86,7 +86,7 @@ export async function fetch(options: Options): Promise { ); const fileName = url.toString().split('/').pop(); assert(fileName, `A malformed download URL was found: ${url}.`); - const structure = new CacheStructure(options.cacheDir); + const structure = new Cache(options.cacheDir); const browserRoot = structure.browserRoot(options.browser); const archivePath = path.join(browserRoot, fileName); if (!existsSync(browserRoot)) { diff --git a/packages/browsers/src/launcher.ts b/packages/browsers/src/launcher.ts index 6bac43e6c2e..077459254f7 100644 --- a/packages/browsers/src/launcher.ts +++ b/packages/browsers/src/launcher.ts @@ -27,7 +27,7 @@ import { resolveSystemExecutablePath, ChromeReleaseChannel, } from './browser-data/browser-data.js'; -import {CacheStructure} from './CacheStructure.js'; +import {Cache} from './Cache.js'; import {debug} from './debug.js'; import {detectBrowserPlatform} from './detectPlatform.js'; @@ -65,7 +65,7 @@ export function computeExecutablePath(options: Options): string { `Cannot download a binary for the provided platform: ${os.platform()} (${os.arch()})` ); } - const installationDir = new CacheStructure(options.cacheDir).installationDir( + const installationDir = new Cache(options.cacheDir).installationDir( options.browser, options.platform, options.buildId diff --git a/packages/browsers/src/main.ts b/packages/browsers/src/main.ts index e9c8524388f..fa24f7b96f0 100644 --- a/packages/browsers/src/main.ts +++ b/packages/browsers/src/main.ts @@ -29,3 +29,4 @@ export { ChromeReleaseChannel, } from './browser-data/browser-data.js'; export {CLI} from './CLI.js'; +export {Cache} from './Cache.js'; diff --git a/packages/browsers/test/src/cli.spec.ts b/packages/browsers/test/src/cli.spec.ts index 0fedd55d2dc..7482cd2523c 100644 --- a/packages/browsers/test/src/cli.spec.ts +++ b/packages/browsers/test/src/cli.spec.ts @@ -18,10 +18,11 @@ import assert from 'assert'; import fs from 'fs'; import os from 'os'; import path from 'path'; - -import rimraf from 'rimraf'; +import * as readline from 'readline'; +import {Writable, Readable} from 'stream'; import {CLI} from '../../lib/cjs/CLI.js'; +import {Cache} from '../../lib/cjs/main.js'; import {testChromeBuildId, testFirefoxBuildId} from './versions.js'; @@ -30,12 +31,33 @@ describe('CLI', function () { let tmpDir = '/tmp/puppeteer-browsers-test'; + function createMockedInterface(input: string) { + const readable = Readable.from([input]); + const writable = new Writable({ + write(_chunk, _encoding, callback) { + // Suppress the output to keep the test clean + callback(); + }, + }); + + return readline.createInterface({ + input: readable, + output: writable, + }); + } + beforeEach(() => { tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'puppeteer-browsers-test')); }); - afterEach(() => { - rimraf.sync(tmpDir); + afterEach(async () => { + new Cache(tmpDir).clear(); + await new CLI(tmpDir, createMockedInterface('yes')).run([ + 'npx', + '@puppeteer/browsers', + 'clear', + `--path=${tmpDir}`, + ]); }); it('should download Chromium binaries', async () => { @@ -57,6 +79,23 @@ describe('CLI', function () { ) ) ); + + await new CLI(tmpDir, createMockedInterface('no')).run([ + 'npx', + '@puppeteer/browsers', + 'clear', + `--path=${tmpDir}`, + ]); + assert.ok( + fs.existsSync( + path.join( + tmpDir, + 'chrome', + `linux-${testChromeBuildId}`, + 'chrome-linux' + ) + ) + ); }); it('should download Firefox binaries', async () => { diff --git a/packages/browsers/test/src/fetch.spec.ts b/packages/browsers/test/src/fetch.spec.ts index f08d05a832e..bab45e381e4 100644 --- a/packages/browsers/test/src/fetch.spec.ts +++ b/packages/browsers/test/src/fetch.spec.ts @@ -21,9 +21,13 @@ import https from 'https'; import os from 'os'; import path from 'path'; -import rimraf from 'rimraf'; - -import {fetch, canFetch, Browser, BrowserPlatform} from '../../lib/cjs/main.js'; +import { + fetch, + canFetch, + Browser, + BrowserPlatform, + Cache, +} from '../../lib/cjs/main.js'; import {testChromeBuildId, testFirefoxBuildId} from './versions.js'; @@ -39,7 +43,7 @@ describe('fetch', () => { }); afterEach(() => { - rimraf.sync(tmpDir); + new Cache(tmpDir).clear(); }); it('should check if a buildId can be downloaded', async () => { diff --git a/packages/browsers/test/src/launcher.spec.ts b/packages/browsers/test/src/launcher.spec.ts index 075d44021ce..3ff797659e9 100644 --- a/packages/browsers/test/src/launcher.spec.ts +++ b/packages/browsers/test/src/launcher.spec.ts @@ -19,8 +19,6 @@ import fs from 'fs'; import os from 'os'; import path from 'path'; -import rimraf from 'rimraf'; - import { CDP_WEBSOCKET_ENDPOINT_REGEX, computeExecutablePath, @@ -28,6 +26,7 @@ import { fetch, Browser, BrowserPlatform, + Cache, } from '../../lib/cjs/main.js'; import {testChromeBuildId, testFirefoxBuildId} from './versions.js'; @@ -86,7 +85,7 @@ describe('launcher', () => { }); afterEach(() => { - rimraf.sync(tmpDir); + new Cache(tmpDir).clear(); }); it('should launch a Chrome browser', async () => { @@ -146,7 +145,7 @@ describe('launcher', () => { }); afterEach(() => { - rimraf.sync(tmpDir); + new Cache(tmpDir).clear(); }); it('should launch a Firefox browser', async () => {