CLI.js 8.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199
  1. /**
  2. * Copyright 2023 Google Inc. All rights reserved.
  3. *
  4. * Licensed under the Apache License, Version 2.0 (the "License");
  5. * you may not use this file except in compliance with the License.
  6. * You may obtain a copy of the License at
  7. *
  8. * http://www.apache.org/licenses/LICENSE-2.0
  9. *
  10. * Unless required by applicable law or agreed to in writing, software
  11. * distributed under the License is distributed on an "AS IS" BASIS,
  12. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13. * See the License for the specific language governing permissions and
  14. * limitations under the License.
  15. */
  16. import { stdin as input, stdout as output } from 'process';
  17. import * as readline from 'readline';
  18. import ProgressBar from 'progress';
  19. import { hideBin } from 'yargs/helpers';
  20. import yargs from 'yargs/yargs';
  21. import { resolveBuildId, BrowserPlatform, } from './browser-data/browser-data.js';
  22. import { Cache } from './Cache.js';
  23. import { detectBrowserPlatform } from './detectPlatform.js';
  24. import { install } from './install.js';
  25. import { computeExecutablePath, computeSystemExecutablePath, launch, } from './launch.js';
  26. /**
  27. * @public
  28. */
  29. export class CLI {
  30. #cachePath;
  31. #rl;
  32. constructor(cachePath = process.cwd(), rl) {
  33. this.#cachePath = cachePath;
  34. this.#rl = rl;
  35. }
  36. #defineBrowserParameter(yargs) {
  37. yargs.positional('browser', {
  38. 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.',
  39. type: 'string',
  40. coerce: (opt) => {
  41. return {
  42. name: this.#parseBrowser(opt),
  43. buildId: this.#parseBuildId(opt),
  44. };
  45. },
  46. });
  47. }
  48. #definePlatformParameter(yargs) {
  49. yargs.option('platform', {
  50. type: 'string',
  51. desc: 'Platform that the binary needs to be compatible with.',
  52. choices: Object.values(BrowserPlatform),
  53. defaultDescription: 'Auto-detected',
  54. });
  55. }
  56. #definePathParameter(yargs, required = false) {
  57. yargs.option('path', {
  58. type: 'string',
  59. 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.',
  60. defaultDescription: 'Current working directory',
  61. ...(required ? {} : { default: process.cwd() }),
  62. });
  63. if (required) {
  64. yargs.demandOption('path');
  65. }
  66. }
  67. async run(argv) {
  68. const yargsInstance = yargs(hideBin(argv));
  69. await yargsInstance
  70. .scriptName('@puppeteer/browsers')
  71. .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 => {
  72. this.#defineBrowserParameter(yargs);
  73. this.#definePlatformParameter(yargs);
  74. this.#definePathParameter(yargs);
  75. yargs.option('base-url', {
  76. type: 'string',
  77. desc: 'Base URL to download from',
  78. });
  79. yargs.example('$0 install chrome', 'Install the latest available build of the Chrome browser.');
  80. yargs.example('$0 install chrome@latest', 'Install the latest available build for the Chrome browser.');
  81. yargs.example('$0 install chromium@1083080', 'Install the revision 1083080 of the Chromium browser.');
  82. yargs.example('$0 install firefox', 'Install the latest available build of the Firefox browser.');
  83. yargs.example('$0 install firefox --platform mac', 'Install the latest Mac (Intel) build of the Firefox browser.');
  84. yargs.example('$0 install firefox --path /tmp/my-browser-cache', 'Install to the specified cache directory.');
  85. }, async (argv) => {
  86. const args = argv;
  87. args.platform ??= detectBrowserPlatform();
  88. if (!args.platform) {
  89. throw new Error(`Could not resolve the current platform`);
  90. }
  91. args.browser.buildId = await resolveBuildId(args.browser.name, args.platform, args.browser.buildId);
  92. await install({
  93. browser: args.browser.name,
  94. buildId: args.browser.buildId,
  95. platform: args.platform,
  96. cacheDir: args.path ?? this.#cachePath,
  97. downloadProgressCallback: makeProgressCallback(args.browser.name, args.browser.buildId),
  98. baseUrl: args.baseUrl,
  99. });
  100. console.log(`${args.browser.name}@${args.browser.buildId} ${computeExecutablePath({
  101. browser: args.browser.name,
  102. buildId: args.browser.buildId,
  103. cacheDir: args.path ?? this.#cachePath,
  104. platform: args.platform,
  105. })}`);
  106. })
  107. .command('launch <browser>', 'Launch the specified browser', yargs => {
  108. this.#defineBrowserParameter(yargs);
  109. this.#definePlatformParameter(yargs);
  110. this.#definePathParameter(yargs);
  111. yargs.option('detached', {
  112. type: 'boolean',
  113. desc: 'Detach the child process.',
  114. default: false,
  115. });
  116. yargs.option('system', {
  117. type: 'boolean',
  118. desc: 'Search for a browser installed on the system instead of the cache folder.',
  119. default: false,
  120. });
  121. yargs.example('$0 launch chrome@1083080', 'Launch the Chrome browser identified by the revision 1083080.');
  122. yargs.example('$0 launch firefox@112.0a1', 'Launch the Firefox browser identified by the milestone 112.0a1.');
  123. yargs.example('$0 launch chrome@1083080 --detached', 'Launch the browser but detach the sub-processes.');
  124. yargs.example('$0 launch chrome@canary --system', 'Try to locate the Canary build of Chrome installed on the system and launch it.');
  125. }, async (argv) => {
  126. const args = argv;
  127. const executablePath = args.system
  128. ? computeSystemExecutablePath({
  129. browser: args.browser.name,
  130. // TODO: throw an error if not a ChromeReleaseChannel is provided.
  131. channel: args.browser.buildId,
  132. platform: args.platform,
  133. })
  134. : computeExecutablePath({
  135. browser: args.browser.name,
  136. buildId: args.browser.buildId,
  137. cacheDir: args.path ?? this.#cachePath,
  138. platform: args.platform,
  139. });
  140. launch({
  141. executablePath,
  142. detached: args.detached,
  143. });
  144. })
  145. .command('clear', 'Removes all installed browsers from the specified cache directory', yargs => {
  146. this.#definePathParameter(yargs, true);
  147. }, async (argv) => {
  148. const args = argv;
  149. const cacheDir = args.path ?? this.#cachePath;
  150. const rl = this.#rl ?? readline.createInterface({ input, output });
  151. rl.question(`Do you want to permanently and recursively delete the content of ${cacheDir} (yes/No)? `, answer => {
  152. rl.close();
  153. if (!['y', 'yes'].includes(answer.toLowerCase().trim())) {
  154. console.log('Cancelled.');
  155. return;
  156. }
  157. const cache = new Cache(cacheDir);
  158. cache.clear();
  159. console.log(`${cacheDir} cleared.`);
  160. });
  161. })
  162. .demandCommand(1)
  163. .help()
  164. .wrap(Math.min(120, yargsInstance.terminalWidth()))
  165. .parse();
  166. }
  167. #parseBrowser(version) {
  168. return version.split('@').shift();
  169. }
  170. #parseBuildId(version) {
  171. const parts = version.split('@');
  172. return parts.length === 2 ? parts[1] : 'latest';
  173. }
  174. }
  175. /**
  176. * @public
  177. */
  178. export function makeProgressCallback(browser, buildId) {
  179. let progressBar;
  180. let lastDownloadedBytes = 0;
  181. return (downloadedBytes, totalBytes) => {
  182. if (!progressBar) {
  183. progressBar = new ProgressBar(`Downloading ${browser} r${buildId} - ${toMegabytes(totalBytes)} [:bar] :percent :etas `, {
  184. complete: '=',
  185. incomplete: ' ',
  186. width: 20,
  187. total: totalBytes,
  188. });
  189. }
  190. const delta = downloadedBytes - lastDownloadedBytes;
  191. lastDownloadedBytes = downloadedBytes;
  192. progressBar.tick(delta);
  193. };
  194. }
  195. function toMegabytes(bytes) {
  196. const mb = bytes / 1000 / 1000;
  197. return `${Math.round(mb * 10) / 10} MB`;
  198. }
  199. //# sourceMappingURL=CLI.js.map