UNPKG

browser-driver-manager

Version:

A cli for managing Chrome and Firefox browsers and drivers. Especially useful to keep Chrome and Chromedriver in-sync.

286 lines (257 loc) 9.75 kB
#!/usr/bin/env node const fsPromises = require('fs').promises; const os = require('os'); const path = require('path'); const readline = require('readline'); const puppeteerBrowsers = require('@puppeteer/browsers'); const puppeteerInstall = puppeteerBrowsers.install; const { resolveBuildId, detectBrowserPlatform, Browser, uninstall } = puppeteerBrowsers; class ErrorWithSuggestion extends Error { constructor(suggestion, originalError) { super(`${suggestion} The original error was: ${originalError.message}`); } } /** * Get the cache directory. * @returns {string} - Path to the cache directory. */ const getBDMCacheDir = () => path.resolve(os.homedir(), '.browser-driver-manager'); /** * Install a single browser. * @param {string} cacheDir - The path to the root of the cache directory. * @param {string} browser - Which browser to install. * @param {string} buildId - Which build ID to download. Build IDs uniquely identify binaries. * @param {object} options - Options for customizing how the browser is installed. * @param {string} options.verbose - Show download progress information. * @throws {Error} - Puppeteer's `install` must succeed. * @returns {string} - The installed browser. */ async function installBrowser(cacheDir, browser, buildId, options) { const downloadProgressCallback = (downloadedBytes, totalBytes) => { // closes over `browser` and `options` if (!options?.verbose) { return; } const browserTitle = browser[0].toUpperCase() + browser.slice(1); let progressMessage = `Downloading ${browserTitle}: `; if (downloadedBytes < totalBytes) { const cursorDisablingString = '\x1B[?25l'; progressMessage = `${cursorDisablingString}${progressMessage}`; progressMessage += `${Math.ceil((downloadedBytes * 100) / totalBytes)}%`; } else { const cursorEnablingString = '\r\n\x1B[?25h'; progressMessage += `Done!${cursorEnablingString}`; } readline.cursorTo(process.stdout, 0); process.stdout.write(progressMessage); }; const installedBrowser = await puppeteerInstall({ cacheDir, browser, buildId, downloadProgressCallback }); return installedBrowser; } /** * Write the environment file to the BDMCacheDir. * @param {object} data - The data to write. * @param {string} data.chromePath - The path to Chrome. * @param {string} data.chromedriverPath - The path to Chromedriver * @param {string} data.version - The version of Chrome and Chromedriver * @throws {Error} - Environment file must be writable. */ async function setEnv({ chromePath, chromedriverPath, version }) { console.log('Setting env CHROME/CHROMEDRIVER_TEST_PATH/VERSION'); const envPath = path.resolve(getBDMCacheDir(), '.env'); try { await fsPromises.writeFile( envPath, `CHROME_TEST_PATH="${chromePath}"${os.EOL}CHROMEDRIVER_TEST_PATH="${chromedriverPath}"${os.EOL}CHROME_TEST_VERSION="${version}"${os.EOL}` ); console.log( `CHROME_TEST_PATH="${chromePath}"${os.EOL}CHROMEDRIVER_TEST_PATH="${chromedriverPath}"${os.EOL}CHROME_TEST_VERSION="${version}"${os.EOL}` ); } catch (e) { throw new ErrorWithSuggestion( `Error setting CHROME/CHROMEDRIVER_TEST_PATH/VERSION. Ensure that the environment file at ${envPath} is writable.`, e ); } } /** * Read the environment file from the BDMCacheDir. * @returns {string|undefined} - The content of the environment file. */ async function getEnv() { try { const envPath = path.resolve(getBDMCacheDir(), '.env'); const env = await fsPromises.readFile(envPath, 'utf8'); return env; } catch (e) { return; } } /** * Log the environment file to the console. * @throws {Error} - Environment file must exist. */ async function which() { const env = await getEnv(); if (!env) { throw new Error('No environment file exists. Please install first'); } console.log(env); } /** * Read the installed version from the environment file. * @returns {string|null} - The version if one exists. Null if errors are suppressed and no valid version exists. * @throws {Error} - Environment file must exist. * @throws {Error} - Environment file must have valid version. */ async function getVersion(suppressErrors = false) { const pattern = /^CHROME_TEST_VERSION="([\d.]+)"$/m; const env = await getEnv(); if (!env) { if (suppressErrors) { return null; } throw new Error('No environment file exists. Please install first'); } // Search for the pattern in the file path const match = env.match(pattern); if (!match || match.length < 2) { if (suppressErrors) { return null; } throw new Error( `No version found in the environment file. Either remove the environment file and reinstall, or add a line 'CHROME_TEST_VERSION={YOUR_INSTALLED_VERSION} to it.` ); } const version = match[1]; return version; } /** * Log the version to the console. * @throws {Error} - Version must exist. */ async function version() { const version = await getVersion(); if (!version) { throw new Error('No installed version found. Please install first'); } console.log(version); } /** * Install a version of Chrome and Chromedriver. * If already-installed Chrome and Chromedriver match this version, skip installation. * If already-installed Chrome and Chromedriver are a different version, overwrite them. * @param {string} browserId - Browser name (chrome), with an optional `@` and version number (e.g. 116.0.5845.96) or channel (e.g. beta, dev, canary) * @param {object} options - Pass-through to installBrowser * @throws {Error} Browser must be chrome * @throws {Error} Browser platform must be detectable * @throws {Error} Build ID must resolve * @throws {Error} Browser must be uninstallable when one is already installed * @throws {Error} Version of chrome and chromedriver must be findable * @throws {Error} Environment file must be removable when one already exists * @returns */ async function install(browserId, options) { const [browser, version = 'stable'] = browserId.split(/@|=/); // Should support for other browsers be added, commander should handle this check. // With only one supported browser, this error message is more meaningful than commander's. if (!browser.includes('chrome')) { throw new Error( `The selected browser, ${browser}, could not be installed. Currently, only "chrome" is supported.` ); } const platform = detectBrowserPlatform(); if (!platform) { throw new Error( `Unable to detect a valid platform for ${process.platform} ${process.arch}. Darwin (MacOS, iOS, etc.), Windows, and Linux platforms are supported.` ); } // Get the version to install of both Chrome and Chromedriver const buildId = await resolveBuildId(Browser.CHROME, platform, version); // Get any currently installed version const currentVersion = await getVersion(true); const cacheDir = getBDMCacheDir(); if (currentVersion) { if (currentVersion === buildId) { console.log( `Chrome and Chromedriver versions ${currentVersion} are already installed. Skipping installation.` ); return; } console.log( `Chrome and Chromedriver versions ${currentVersion} are currently installed. Overwriting.` ); } // Create a cache directory if it does not exist on the user's home directory, or if it's been removed above // This will be where environment variables will be stored for the tests // since it is a consistent location across different platforms try { await fsPromises.mkdir(cacheDir, { recursive: true }); } catch (e) { throw new ErrorWithSuggestion( `Unable to create the cache directory ${cacheDir}. Ensure the directory can be created and try again.`, e ); } let installedBrowsers = {}; const browsers = ['CHROME', 'CHROMEDRIVER']; for (const installedBrowser of browsers) { try { installedBrowsers[installedBrowser] = await installBrowser( cacheDir, Browser[installedBrowser], buildId, options ); } catch (e) { const error = /status code 404/.test(e.message) ? new ErrorWithSuggestion( `Tried to install version ${buildId} of ${installedBrowser.toLowerCase()}, but it couldn't be found. Please refer to the Chrome for Testing availability dashboard for available versions: https://googlechromelabs.github.io/chrome-for-testing/.`, e ) : e; throw error; } } const envPath = path.resolve(cacheDir, '.env'); try { // Remove the existing .env, if it exists. setEnv creates it again later // Should execution stop beforehand, .env will not be in a bad (empty) state await fsPromises.rm(envPath, { force: true }); } catch (e) { throw new ErrorWithSuggestion( `Unable to remove .env from ${cacheDir}. Ensure the file can be removed and try again.`, e ); } await setEnv({ chromePath: installedBrowsers['CHROME'].executablePath, chromedriverPath: installedBrowsers['CHROMEDRIVER'].executablePath, version: buildId }); // Uninstall previous versions last so potential failure doesn't prevent installation if (currentVersion) { for (const browser of browsers) { try { await uninstall({ browser: Browser[browser], buildId: currentVersion, cacheDir }); } catch (e) { throw new ErrorWithSuggestion( `Unable to uninstall ${browser} version ${currentVersion}. Ensure that the executable in ${cacheDir} can be removed and try again.`, e ); } } } } module.exports = { install, version, which };