CLI.js 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258
  1. /**
  2. * @license
  3. * Copyright 2023 Google Inc.
  4. * SPDX-License-Identifier: Apache-2.0
  5. */
  6. import { stdin as input, stdout as output } from 'process';
  7. import * as readline from 'readline';
  8. import ProgressBar from 'progress';
  9. import { hideBin } from 'yargs/helpers';
  10. import yargs from 'yargs/yargs';
  11. import { resolveBuildId, BrowserPlatform, } from './browser-data/browser-data.js';
  12. import { Cache } from './Cache.js';
  13. import { detectBrowserPlatform } from './detectPlatform.js';
  14. import { install } from './install.js';
  15. import { computeExecutablePath, computeSystemExecutablePath, launch, } from './launch.js';
  16. /**
  17. * @public
  18. */
  19. export class CLI {
  20. #cachePath;
  21. #rl;
  22. #scriptName = '';
  23. #allowCachePathOverride = true;
  24. #pinnedBrowsers;
  25. #prefixCommand;
  26. constructor(opts, rl) {
  27. if (!opts) {
  28. opts = {};
  29. }
  30. if (typeof opts === 'string') {
  31. opts = {
  32. cachePath: opts,
  33. };
  34. }
  35. this.#cachePath = opts.cachePath ?? process.cwd();
  36. this.#rl = rl;
  37. this.#scriptName = opts.scriptName ?? '@puppeteer/browsers';
  38. this.#allowCachePathOverride = opts.allowCachePathOverride ?? true;
  39. this.#pinnedBrowsers = opts.pinnedBrowsers;
  40. this.#prefixCommand = opts.prefixCommand;
  41. }
  42. #defineBrowserParameter(yargs) {
  43. yargs.positional('browser', {
  44. 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.',
  45. type: 'string',
  46. coerce: (opt) => {
  47. return {
  48. name: this.#parseBrowser(opt),
  49. buildId: this.#parseBuildId(opt),
  50. };
  51. },
  52. });
  53. }
  54. #definePlatformParameter(yargs) {
  55. yargs.option('platform', {
  56. type: 'string',
  57. desc: 'Platform that the binary needs to be compatible with.',
  58. choices: Object.values(BrowserPlatform),
  59. defaultDescription: 'Auto-detected',
  60. });
  61. }
  62. #definePathParameter(yargs, required = false) {
  63. if (!this.#allowCachePathOverride) {
  64. return;
  65. }
  66. yargs.option('path', {
  67. type: 'string',
  68. 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.',
  69. defaultDescription: 'Current working directory',
  70. ...(required ? {} : { default: process.cwd() }),
  71. });
  72. if (required) {
  73. yargs.demandOption('path');
  74. }
  75. }
  76. async run(argv) {
  77. const yargsInstance = yargs(hideBin(argv));
  78. let target = yargsInstance.scriptName(this.#scriptName);
  79. if (this.#prefixCommand) {
  80. target = target.command(this.#prefixCommand.cmd, this.#prefixCommand.description, yargs => {
  81. return this.#build(yargs);
  82. });
  83. }
  84. else {
  85. target = this.#build(target);
  86. }
  87. await target
  88. .demandCommand(1)
  89. .help()
  90. .wrap(Math.min(120, yargsInstance.terminalWidth()))
  91. .parse();
  92. }
  93. #build(yargs) {
  94. const latestOrPinned = this.#pinnedBrowsers ? 'pinned' : 'latest';
  95. return yargs
  96. .command('install <browser>', '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 => {
  97. this.#defineBrowserParameter(yargs);
  98. this.#definePlatformParameter(yargs);
  99. this.#definePathParameter(yargs);
  100. yargs.option('base-url', {
  101. type: 'string',
  102. desc: 'Base URL to download from',
  103. });
  104. yargs.example('$0 install chrome', `Install the ${latestOrPinned} available build of the Chrome browser.`);
  105. yargs.example('$0 install chrome@latest', 'Install the latest available build for the Chrome browser.');
  106. yargs.example('$0 install chrome@stable', 'Install the latest available build for the Chrome browser from the stable channel.');
  107. yargs.example('$0 install chrome@beta', 'Install the latest available build for the Chrome browser from the beta channel.');
  108. yargs.example('$0 install chrome@dev', 'Install the latest available build for the Chrome browser from the dev channel.');
  109. yargs.example('$0 install chrome@canary', 'Install the latest available build for the Chrome Canary browser.');
  110. yargs.example('$0 install chrome@115', 'Install the latest available build for Chrome 115.');
  111. yargs.example('$0 install chromedriver@canary', 'Install the latest available build for ChromeDriver Canary.');
  112. yargs.example('$0 install chromedriver@115', 'Install the latest available build for ChromeDriver 115.');
  113. yargs.example('$0 install chromedriver@115.0.5790', 'Install the latest available patch (115.0.5790.X) build for ChromeDriver.');
  114. yargs.example('$0 install chrome-headless-shell', 'Install the latest available chrome-headless-shell build.');
  115. yargs.example('$0 install chrome-headless-shell@beta', 'Install the latest available chrome-headless-shell build corresponding to the Beta channel.');
  116. yargs.example('$0 install chrome-headless-shell@118', 'Install the latest available chrome-headless-shell 118 build.');
  117. yargs.example('$0 install chromium@1083080', 'Install the revision 1083080 of the Chromium browser.');
  118. yargs.example('$0 install firefox', 'Install the latest nightly available build of the Firefox browser.');
  119. yargs.example('$0 install firefox@stable', 'Install the latest stable build of the Firefox browser.');
  120. yargs.example('$0 install firefox@beta', 'Install the latest beta build of the Firefox browser.');
  121. yargs.example('$0 install firefox@devedition', 'Install the latest devedition build of the Firefox browser.');
  122. yargs.example('$0 install firefox@esr', 'Install the latest ESR build of the Firefox browser.');
  123. yargs.example('$0 install firefox@nightly', 'Install the latest nightly build of the Firefox browser.');
  124. yargs.example('$0 install firefox@stable_111.0.1', 'Install a specific version of the Firefox browser.');
  125. yargs.example('$0 install firefox --platform mac', 'Install the latest Mac (Intel) build of the Firefox browser.');
  126. if (this.#allowCachePathOverride) {
  127. yargs.example('$0 install firefox --path /tmp/my-browser-cache', 'Install to the specified cache directory.');
  128. }
  129. }, async (argv) => {
  130. const args = argv;
  131. args.platform ??= detectBrowserPlatform();
  132. if (!args.platform) {
  133. throw new Error(`Could not resolve the current platform`);
  134. }
  135. if (args.browser.buildId === 'pinned') {
  136. const pinnedVersion = this.#pinnedBrowsers?.[args.browser.name];
  137. if (!pinnedVersion) {
  138. throw new Error(`No pinned version found for ${args.browser.name}`);
  139. }
  140. args.browser.buildId = pinnedVersion;
  141. }
  142. const originalBuildId = args.browser.buildId;
  143. args.browser.buildId = await resolveBuildId(args.browser.name, args.platform, args.browser.buildId);
  144. await install({
  145. browser: args.browser.name,
  146. buildId: args.browser.buildId,
  147. platform: args.platform,
  148. cacheDir: args.path ?? this.#cachePath,
  149. downloadProgressCallback: makeProgressCallback(args.browser.name, args.browser.buildId),
  150. baseUrl: args.baseUrl,
  151. buildIdAlias: originalBuildId !== args.browser.buildId
  152. ? originalBuildId
  153. : undefined,
  154. });
  155. console.log(`${args.browser.name}@${args.browser.buildId} ${computeExecutablePath({
  156. browser: args.browser.name,
  157. buildId: args.browser.buildId,
  158. cacheDir: args.path ?? this.#cachePath,
  159. platform: args.platform,
  160. })}`);
  161. })
  162. .command('launch <browser>', 'Launch the specified browser', yargs => {
  163. this.#defineBrowserParameter(yargs);
  164. this.#definePlatformParameter(yargs);
  165. this.#definePathParameter(yargs);
  166. yargs.option('detached', {
  167. type: 'boolean',
  168. desc: 'Detach the child process.',
  169. default: false,
  170. });
  171. yargs.option('system', {
  172. type: 'boolean',
  173. desc: 'Search for a browser installed on the system instead of the cache folder.',
  174. default: false,
  175. });
  176. yargs.example('$0 launch chrome@115.0.5790.170', 'Launch Chrome 115.0.5790.170');
  177. yargs.example('$0 launch firefox@112.0a1', 'Launch the Firefox browser identified by the milestone 112.0a1.');
  178. yargs.example('$0 launch chrome@115.0.5790.170 --detached', 'Launch the browser but detach the sub-processes.');
  179. yargs.example('$0 launch chrome@canary --system', 'Try to locate the Canary build of Chrome installed on the system and launch it.');
  180. }, async (argv) => {
  181. const args = argv;
  182. const executablePath = args.system
  183. ? computeSystemExecutablePath({
  184. browser: args.browser.name,
  185. // TODO: throw an error if not a ChromeReleaseChannel is provided.
  186. channel: args.browser.buildId,
  187. platform: args.platform,
  188. })
  189. : computeExecutablePath({
  190. browser: args.browser.name,
  191. buildId: args.browser.buildId,
  192. cacheDir: args.path ?? this.#cachePath,
  193. platform: args.platform,
  194. });
  195. launch({
  196. executablePath,
  197. detached: args.detached,
  198. });
  199. })
  200. .command('clear', this.#allowCachePathOverride
  201. ? 'Removes all installed browsers from the specified cache directory'
  202. : `Removes all installed browsers from ${this.#cachePath}`, yargs => {
  203. this.#definePathParameter(yargs, true);
  204. }, async (argv) => {
  205. const args = argv;
  206. const cacheDir = args.path ?? this.#cachePath;
  207. const rl = this.#rl ?? readline.createInterface({ input, output });
  208. rl.question(`Do you want to permanently and recursively delete the content of ${cacheDir} (yes/No)? `, answer => {
  209. rl.close();
  210. if (!['y', 'yes'].includes(answer.toLowerCase().trim())) {
  211. console.log('Cancelled.');
  212. return;
  213. }
  214. const cache = new Cache(cacheDir);
  215. cache.clear();
  216. console.log(`${cacheDir} cleared.`);
  217. });
  218. })
  219. .demandCommand(1)
  220. .help();
  221. }
  222. #parseBrowser(version) {
  223. return version.split('@').shift();
  224. }
  225. #parseBuildId(version) {
  226. const parts = version.split('@');
  227. return parts.length === 2
  228. ? parts[1]
  229. : this.#pinnedBrowsers
  230. ? 'pinned'
  231. : 'latest';
  232. }
  233. }
  234. /**
  235. * @public
  236. */
  237. export function makeProgressCallback(browser, buildId) {
  238. let progressBar;
  239. let lastDownloadedBytes = 0;
  240. return (downloadedBytes, totalBytes) => {
  241. if (!progressBar) {
  242. progressBar = new ProgressBar(`Downloading ${browser} ${buildId} - ${toMegabytes(totalBytes)} [:bar] :percent :etas `, {
  243. complete: '=',
  244. incomplete: ' ',
  245. width: 20,
  246. total: totalBytes,
  247. });
  248. }
  249. const delta = downloadedBytes - lastDownloadedBytes;
  250. lastDownloadedBytes = downloadedBytes;
  251. progressBar.tick(delta);
  252. };
  253. }
  254. function toMegabytes(bytes) {
  255. const mb = bytes / 1000 / 1000;
  256. return `${Math.round(mb * 10) / 10} MB`;
  257. }
  258. //# sourceMappingURL=CLI.js.map