├── .gitattributes ├── .gitignore ├── .travis.yml ├── README.MD ├── cache.js ├── config.js ├── index.d.ts ├── index.js ├── install.js ├── package-lock.json ├── package.json ├── test ├── _utils.js ├── test-cache.js ├── test-config.js ├── test-install.js └── test-utils.js └── utils.js /.gitattributes: -------------------------------------------------------------------------------- 1 | *.js text eol=lf 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (http://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # Typescript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | # dotenv environment variables file 58 | .env 59 | 60 | # IDEA folders 61 | .idea/ 62 | 63 | # Chromium binaries output folder 64 | /lib/ 65 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: node_js 3 | os: 4 | - linux 5 | - osx 6 | node_js: 7 | - "lts/*" 8 | - "node" 9 | -------------------------------------------------------------------------------- /README.MD: -------------------------------------------------------------------------------- 1 | # node-chromium [![Build Status](https://travis-ci.org/dtolstyi/node-chromium.svg?branch=master)](https://travis-ci.org/dtolstyi/node-chromium) [![XO code style](https://img.shields.io/badge/code_style-XO-blue.svg)](https://github.com/sindresorhus/xo) [![npm version](https://badge.fury.io/js/chromium.svg)](https://badge.fury.io/js/chromium) [![Platforms](https://img.shields.io/badge/platforms-Win/Linux/Mac-lightgrey.svg)](https://github.com/dtolstyi/node-chromium) 2 | > Chromium binaries for your NodeJS project 3 | 4 | **node-chromium** allows you to easily add [Chromium](https://www.chromium.org/) binaries to your project and use it for automation, testing, web scraping or just for fun. 5 | 6 | ## Why Chromium? 7 | [Chromium](https://www.chromium.org/) is an open-source web browser developed and maintained by The Chromium Project. Google Chrome, also released in 2008, is a proprietary web browser developed and maintained by Google. The reason why Chrome and Chromium are tied to each other is that Chrome borrows Chromium’s source code. 8 | The main benefit of using Chromium is that it **doesn't** include all the proprietary modifications made by Google, thus it's more lightweight and more suitable for automation purposes. 9 | You can see full list of differences in [Fossbytes article](https://fossbytes.com/difference-google-chrome-vs-chromium-browser/). 10 | 11 | ## Requirements 12 | 13 | Starting from version `2.2.0` `node-chromium` is tested against and supports Node.js LTS and latest stable releases 14 | Versions `2.0.0` - `2.1.2` support Node.js 7+ 15 | If you need to use older versions of Node.js try `node-chromium 1.x.x` releases. 16 | 17 | ## Usage 18 | Depending on your needs, you can install module into **devDependencies** (`--save-dev`) or production **dependencies** (`--save`) 19 | 20 | ``` 21 | npm install --save chromium 22 | ``` 23 | 24 | During the installation process **node-chromium** will find the latest suitable build for your platform, download it and extract into libraries folder. As soon as installation is finished, you are ready to use Chromium in your project: 25 | 26 | ```js 27 | const chromium = require('chromium'); 28 | const {execFile} = require('child_process'); 29 | 30 | execFile(chromium.path, ['https://google.com'], err => { 31 | console.log('Hello Google!'); 32 | }); 33 | ``` 34 | 35 | ### Proxy Configuration 36 | When downloading the chromium binary **node-chromium** will use the proxy configured for `npm` to establish HTTP(S) connections. The proxy used by `npm` can be configured using 37 | ``` 38 | npm config set proxy http://:@: 39 | npm config set https-proxy http://:@:///.zip?alt=media` for example see the taobao mirror [chromium-browser-snapshots](https://npm.taobao.org/mirrors/chromium-browser-snapshots/). 72 | * May also be set in .npmrc like so: 73 | 74 | ```ini 75 | node_chromium_download_host=https://npm.taobao.org/mirrors/chromium-browser-snapshots/ 76 | node_chromium_revision=737027 77 | ``` 78 | 79 | ## Selenium WebDriver Headless (without UI) tests 80 | It's extremely easy to use **node-chromium** with **selenium-webdriver** to perform e2e tests without spawning browser UI. 81 | First, install all dependencies 82 | 83 | ``` 84 | npm install --save chromium chromedriver selenium-webdriver 85 | ``` 86 | 87 | After the installation is finished, create simple script that opens Google Search home page and takes it's screenshot in headless mode. 88 | 89 | ```js 90 | const fs = require('fs'); 91 | const webdriver = require('selenium-webdriver'); 92 | const chrome = require('selenium-webdriver/chrome'); 93 | const chromium = require('chromium'); 94 | require('chromedriver'); 95 | 96 | async function start() { 97 | let options = new chrome.Options(); 98 | options.setChromeBinaryPath(chromium.path); 99 | options.addArguments('--headless'); 100 | options.addArguments('--disable-gpu'); 101 | options.addArguments('--window-size=1280,960'); 102 | 103 | const driver = await new webdriver.Builder() 104 | .forBrowser('chrome') 105 | .setChromeOptions(options) 106 | .build(); 107 | 108 | await driver.get('http://google.com'); 109 | console.log('Hello Google!'); 110 | await takeScreenshot(driver, 'google-start-page'); 111 | 112 | await driver.quit(); 113 | } 114 | 115 | async function takeScreenshot(driver, name) { 116 | await driver.takeScreenshot().then((data) => { 117 | fs.writeFileSync(name + '.png', data, 'base64'); 118 | console.log('Screenshot is saved'); 119 | }); 120 | } 121 | 122 | start(); 123 | ``` 124 | 125 | ### Cache Downloaded Binaries 126 | By default downloaded chromium binaries are cached in the appropriate cache directory for your operating system. 127 | 128 | You may override the cache path by setting the `NODE_CHROMIUM_CACHE_PATH` environment variable to a directory path, for example: 129 | 130 | ```bash 131 | export NODE_CHROMIUM_CACHE_PATH=/path/to/cache/dir/ 132 | 133 | # or in .npmrc like so: 134 | # node_chromium_cache_path=/path/to/cache/dir/ 135 | ``` 136 | 137 | You may disable caching by setting `NODE_CHROMIUM_CACHE_DISABLE` to `true`: 138 | 139 | ```bash 140 | export NODE_CHROMIUM_CACHE_DISABLE=true 141 | 142 | # or in .npmrc like so: 143 | # node_chromium_cache_disable=true 144 | ``` 145 | 146 | ### Skip Automatic Chromium Install 147 | 148 | Chromium will ordinarily be installed when you exectute `npm install` however you may wish to skip this step if you are going to defer installation and perform it programatically at a later stage. Below is an example of how to do so. 149 | 150 | ```bash 151 | export NODE_CHROMIUM_SKIP_INSTALL=true 152 | 153 | # or in .npmrc like so: 154 | # node_chromium_skip_install=true 155 | ``` 156 | 157 | Then install it programatically when you need it: 158 | 159 | ```js 160 | chromium.install().then(function() { 161 | // do stuff... 162 | }); 163 | ``` 164 | ## Contributors 165 | 166 | 167 | 168 | 169 |

Rick Brown
170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 |

Alex Schlosser

psociety

Daniel Hernández Alcojor

Ryan Cooney

Amila Welihinda

Timon Kurmann

Jakub Wąsik
181 | 182 | ## License 183 | MIT 184 | -------------------------------------------------------------------------------- /cache.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const path = require('path'); 3 | const fs = require('fs'); 4 | const cachedir = require('cachedir'); 5 | const config = require('./config'); 6 | 7 | /** 8 | * Retrieve a Chromium archive from filesystem cache. 9 | * @param {string} revision The Chromium revision to retrieve. 10 | * @returns {string} The path to the cached Chromium archive. Falsy if not found. 11 | */ 12 | function get(revision) { 13 | const cachePath = buildCachePath(revision); 14 | if (fs.existsSync(cachePath)) { 15 | return cachePath; 16 | } 17 | 18 | return ''; 19 | } 20 | 21 | /** 22 | * Store a Chromium archive in filesystem cache for future use. 23 | * Has no effect if the user has not configured a cache location. 24 | * @param {string} revision The Chromium revision in the archive file. 25 | * @param {string} filePath The path to the Chromium archive file to store in cache. 26 | */ 27 | function put(revision, filePath) { 28 | const cachePath = buildCachePath(revision); 29 | if (cachePath && filePath) { 30 | try { 31 | fs.mkdirSync(path.dirname(cachePath), {recursive: true}); 32 | fs.copyFileSync(filePath, cachePath); 33 | } catch (error) { 34 | // Don't die on cache fail 35 | console.error('Could not cache file', cachePath, error); 36 | } 37 | } 38 | } 39 | 40 | /** 41 | * Get the unique cache path for this revision, on this platform and architecture. 42 | * @param {string} revision The revision of this Chromium binary, essentially a unique cache key. 43 | * @returns {string} The cache path, or falsy if caching is not enabled. 44 | */ 45 | function buildCachePath(revision) { 46 | if (!revision || config.getEnvVar('NODE_CHROMIUM_CACHE_DISABLE').toLowerCase() === 'true') { 47 | return ''; 48 | } 49 | 50 | const cachePath = config.getEnvVar('NODE_CHROMIUM_CACHE_PATH') || cachedir('node-chromium'); 51 | return path.join(cachePath, `chromium-${revision}-${process.platform}-${process.arch}.zip`); 52 | } 53 | 54 | module.exports = { 55 | get, 56 | put 57 | }; 58 | -------------------------------------------------------------------------------- /config.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const path = require('path'); 4 | 5 | module.exports = { 6 | /** 7 | * The default CDN URL, used if not overridden by user 8 | */ 9 | CDN_URL: 'https://www.googleapis.com/download/storage/v1/b/chromium-browser-snapshots/o/', 10 | /** 11 | * The default filesystem path where chromium will be installed. 12 | */ 13 | BIN_OUT_PATH: path.join(__dirname, 'lib', 'chromium'), 14 | /** 15 | * Gets a configuration parameter from the environment. 16 | * Will first check for a lowercase variant set via npm config in the format: `npm_config_${name.toLowerCase()}`. 17 | * If not set then will check the environment for the variable, as provided. 18 | * @param {string} name The name of the environment variable, case sensitive, conventionally uppercase. 19 | * @returns {string} The value of the environment variable. 20 | */ 21 | getEnvVar: name => { 22 | if (!name) { 23 | return ''; 24 | } 25 | 26 | return process.env[`npm_config_${name.toLowerCase()}`] || process.env[name] || ''; 27 | } 28 | }; 29 | -------------------------------------------------------------------------------- /index.d.ts: -------------------------------------------------------------------------------- 1 | 2 | declare const chromium: { 3 | readonly path: string; 4 | install(): Promise; 5 | }; 6 | 7 | export = chromium; 8 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const fs = require('fs'); 4 | 5 | const utils = require('./utils'); 6 | 7 | function getBinaryPath() { 8 | const path = utils.getOsChromiumBinPath(); 9 | 10 | if (fs.existsSync(path)) { 11 | return path; 12 | } 13 | 14 | return undefined; 15 | } 16 | 17 | module.exports = { 18 | /* 19 | * The path property needs to use a getter because the binaries may not be present for any number of reasons. 20 | * Using a getter allows this property to update itself as needed and reflect the current state of the filesystem. 21 | */ 22 | get path() { 23 | return getBinaryPath(); 24 | }, 25 | install: require('./install') 26 | }; 27 | -------------------------------------------------------------------------------- /install.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const path = require('path'); 4 | const fs = require('fs'); 5 | const extractZip = require('extract-zip'); 6 | const got = require('got'); 7 | const tmp = require('tmp'); 8 | const debug = require('debug')('node-chromium'); 9 | const rimraf = require('rimraf'); 10 | const ProgressBar = require('progress'); 11 | 12 | const config = require('./config'); 13 | const utils = require('./utils'); 14 | const cache = require('./cache'); 15 | 16 | let progressBar = null; 17 | 18 | /* eslint unicorn/prevent-abbreviations: ["off"] */ 19 | 20 | function createTempFile() { 21 | return new Promise((resolve, reject) => { 22 | tmp.file((error, path) => { 23 | if (error) { 24 | console.log('An error occured while trying to create temporary file', error); 25 | reject(error); 26 | } else { 27 | resolve(path); 28 | } 29 | }); 30 | }); 31 | } 32 | 33 | /** 34 | * Downloads the Chromium archive from the default CDN or mirror if configured. 35 | * If the required archive is retrieved from the cache directory then the download will be skipped. 36 | * @param {string} revision The Chromium revision to download. 37 | */ 38 | async function downloadChromiumRevision(revision) { 39 | const cacheEntry = cache.get(revision); 40 | if (cacheEntry) { 41 | debug('Found Chromium archive in cache, skipping download'); 42 | 43 | return Promise.resolve(cacheEntry); 44 | } 45 | 46 | debug('Downloading Chromium archive from Google CDN'); 47 | const url = utils.getDownloadUrl(revision); 48 | const tmpPath = await createTempFile(); 49 | return _downloadFile(url, tmpPath).then(tmpPath => { 50 | cache.put(revision, tmpPath); 51 | return tmpPath; 52 | }); 53 | } 54 | 55 | function _downloadFile(url, destPath) { 56 | return new Promise((resolve, reject) => { 57 | got.stream(url, utils.getRequestOptions(url)) 58 | .on('downloadProgress', onProgress) 59 | .on('error', error => { 60 | console.error('An error occurred while trying to download file', error.message); 61 | reject(error); 62 | }) 63 | .pipe(fs.createWriteStream(destPath)) 64 | .on('error', error => { 65 | console.error('An error occurred while trying to save file to disk', error); 66 | reject(error); 67 | }) 68 | .on('finish', () => { 69 | resolve(destPath); 70 | }); 71 | }); 72 | } 73 | 74 | /** 75 | * Handles download progress events. 76 | * @param progress Information about progress so far. 77 | */ 78 | function onProgress(progress) { 79 | const fakeProgressBar = {tick: () => {}}; 80 | try { 81 | if (!progressBar) { 82 | const formatBytes = bytes => { 83 | const mb = bytes / 1024 / 1024; 84 | return `${Math.round(mb * 10) / 10} MB`; 85 | }; 86 | 87 | if (progress.total) { 88 | progressBar = new ProgressBar(`Downloading Chromium - ${formatBytes(progress.total)} [:bar] :percent :etas `, { 89 | width: 20, 90 | total: progress.total 91 | }); 92 | } else { 93 | progressBar = fakeProgressBar; 94 | console.info('\tPlease wait, this may take a while...'); 95 | } 96 | } 97 | 98 | progressBar.tick(progress.transferred - progressBar.curr); 99 | } catch (error) { 100 | // Don't die on progress bar failure, log it and stop progress 101 | console.error('Error displaying progress bar. Continuing anyway...', error); 102 | progressBar = fakeProgressBar; 103 | } 104 | } 105 | 106 | function unzipArchive(archivePath, outputFolder) { 107 | debug('Started extracting archive', archivePath); 108 | 109 | return new Promise((resolve, reject) => { 110 | const osOutputFolder = path.join(outputFolder, utils.getOsChromiumFolderName()); 111 | rimraf(osOutputFolder, () => { 112 | extractZip(archivePath, {dir: outputFolder}, error => { 113 | if (error) { 114 | console.error('An error occurred while trying to extract archive', error); 115 | reject(error); 116 | } else { 117 | debug('Archive was successfully extracted'); 118 | resolve(true); 119 | } 120 | }); 121 | }); 122 | }); 123 | } 124 | 125 | async function install() { 126 | const chromiumRevision = config.getEnvVar('NODE_CHROMIUM_REVISION'); 127 | try { 128 | console.info('Step 1. Retrieving Chromium revision number'); 129 | const revision = chromiumRevision || await utils.getLatestRevisionNumber(); 130 | 131 | console.info(`Step 2. Downloading Chromium revision ${revision}`); 132 | const archivePath = await downloadChromiumRevision(revision); 133 | 134 | console.info('Step 3. Setting up Chromium binaries'); 135 | await unzipArchive(archivePath, config.BIN_OUT_PATH); 136 | 137 | console.info('Process is successfully finished'); 138 | } catch (error) { 139 | console.error('An error occurred while trying to setup Chromium. Resolve all issues and restart the process', error); 140 | } 141 | } 142 | 143 | if (require.main === module) { 144 | // Module called directly, not via "require", so execute install... 145 | if (config.getEnvVar('NODE_CHROMIUM_SKIP_INSTALL').toLowerCase() === 'true') { 146 | console.info('Skipping chromium install'); 147 | } else { 148 | install(); 149 | } 150 | } 151 | 152 | tmp.setGracefulCleanup(); // Ensure temporary files are cleaned up when process exits 153 | 154 | module.exports = install; 155 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "chromium", 3 | "version": "3.0.3", 4 | "description": "Chromium binaries for Node.js projects", 5 | "repository": "dtolstyi/node-chromium", 6 | "main": "index.js", 7 | "os": [ 8 | "darwin", 9 | "linux", 10 | "win32" 11 | ], 12 | "scripts": { 13 | "postinstall": "node install.js", 14 | "test": "xo && ava --verbose", 15 | "test-only": "ava --verbose" 16 | }, 17 | "files": [ 18 | "cache.js", 19 | "config.js", 20 | "index.js", 21 | "install.js", 22 | "utils.js" 23 | ], 24 | "keywords": [ 25 | "chromium", 26 | "chrome", 27 | "browser" 28 | ], 29 | "author": "Dmytro TOLSTYI ", 30 | "license": "MIT", 31 | "ava": { 32 | "timeout": "15m" 33 | }, 34 | "xo": { 35 | "space": 4 36 | }, 37 | "dependencies": { 38 | "cachedir": "^2.3.0", 39 | "debug": "^4.1.0", 40 | "extract-zip": "^1.7.0", 41 | "got": "^11.5.1", 42 | "progress": "^2.0.3", 43 | "rimraf": "^2.7.1", 44 | "tmp": "0.0.33", 45 | "tunnel": "^0.0.6" 46 | }, 47 | "devDependencies": { 48 | "ava": "^3.11.0", 49 | "xo": "^0.32.1" 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /test/_utils.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Test utils 3 | */ 4 | const platform = process.platform; 5 | const arch = process.arch; 6 | 7 | /** 8 | * Sets an environment variable and the corresponding npm_config variant. 9 | * 10 | * @param {string} name The UPPER_CASE name of the environment variable. 11 | * @param {string} value The value to set - if falsy will be deleted. 12 | */ 13 | function setEnvVar(name, value) { 14 | const npmName = `npm_config_${name.toLowerCase()}`; 15 | process.env[name] = value; 16 | process.env[npmName] = value; 17 | } 18 | 19 | /** 20 | * Clear an environment variable and the corresponding npm_config variant. 21 | * 22 | * @param {string} name The UPPER_CASE name of the environment variable. 23 | */ 24 | function clearEnvVar(name) { 25 | const npmName = `npm_config_${name.toLowerCase()}`; 26 | delete process.env[name]; 27 | delete process.env[npmName]; 28 | } 29 | 30 | /** 31 | * Mocks out the platform value on the global process object. 32 | * @param {string} newPlatformValue The mock platform. 33 | */ 34 | function mockPlatform(newPlatformValue) { 35 | Object.defineProperty(process, 'platform', { 36 | value: newPlatformValue 37 | }); 38 | } 39 | 40 | /** 41 | * Mocks out the arch value on the global process object. 42 | * @param {string} newArchValue The mock architecture. 43 | */ 44 | function mockArch(newArchValue) { 45 | Object.defineProperty(process, 'arch', { 46 | value: newArchValue 47 | }); 48 | } 49 | 50 | /** 51 | * Resets all mocked properties. 52 | */ 53 | function clearMocks() { 54 | mockPlatform(platform); 55 | mockArch(arch); 56 | } 57 | 58 | module.exports = { 59 | mockPlatform, 60 | mockArch, 61 | clearMocks, 62 | setEnvVar, 63 | clearEnvVar 64 | }; 65 | -------------------------------------------------------------------------------- /test/test-cache.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const os = require('os'); 3 | const fs = require('fs'); 4 | const path = require('path'); 5 | const test = require('ava'); 6 | const testUtils = require('./_utils'); 7 | const cache = require('../cache'); 8 | let idx = 0; 9 | 10 | /* eslint camelcase: ["error", {properties: "never"}] */ 11 | 12 | test.beforeEach(t => { 13 | setCacheDir(); 14 | t.pass(); 15 | }); 16 | 17 | test.afterEach(t => { 18 | testUtils.clearMocks(); 19 | t.pass(); 20 | }); 21 | 22 | test.serial('get with null returns falsy', t => { 23 | t.falsy(cache.get(null)); 24 | }); 25 | 26 | test.serial('get with no hit returns falsy', t => { 27 | t.falsy(cache.get('foobar')); 28 | }); 29 | 30 | test.serial('put and retrieve cached file when disabled', t => { 31 | process.env.NODE_CHROMIUM_CACHE_DISABLE = 'true'; 32 | const revision = Date.now().toString(); 33 | const file = createDummyFile(); 34 | t.falsy(cache.get(revision), 'There should be no cached file before the test'); 35 | cache.put(revision, file); 36 | t.falsy(cache.get(revision), 'There should still be no cached file'); 37 | }); 38 | 39 | test.serial('put and retrieve cached file', t => { 40 | const revision = Date.now().toString(); 41 | const file = createDummyFile(); 42 | t.falsy(cache.get(revision), 'There should be no cached file at this point'); 43 | cache.put(revision, file); 44 | const fileContent = fs.readFileSync(file, 'utf8'); 45 | const actualContent = fs.readFileSync(cache.get(revision), 'utf8'); 46 | t.is(fileContent, actualContent, 'The cached file should match the source file'); 47 | }); 48 | 49 | test.serial('put and overwrite existing cached file', t => { 50 | const revision = Date.now().toString(); 51 | const file = createDummyFile(); 52 | t.falsy(cache.get(revision), 'There should be no cached file at this point'); 53 | cache.put(revision, file); 54 | t.truthy(cache.get(revision), 'There should be a cached file at this point'); 55 | cache.put(revision, file); // Nothing bad should happen 56 | }); 57 | 58 | test.serial('cache entries for different platforms do not collide', t => { 59 | const revision = Date.now().toString(); 60 | ['darwin', 'linux', 'windows'].forEach(platform => { 61 | testUtils.mockPlatform(platform); 62 | const file = createDummyFile(); 63 | t.falsy(cache.get(revision), 'There should be no cached file at this point'); 64 | cache.put(revision, file); 65 | const fileContent = fs.readFileSync(file, 'utf8'); 66 | const actualContent = fs.readFileSync(cache.get(revision), 'utf8'); 67 | t.is(fileContent, actualContent, 'The cached file should match the source file'); 68 | }); 69 | }); 70 | 71 | test.serial('cache entries for different architectures do not collide', t => { 72 | const revision = Date.now().toString(); 73 | ['x32', 'x64'].forEach(arch => { 74 | testUtils.mockArch(arch); 75 | const file = createDummyFile(); 76 | t.falsy(cache.get(revision), 'There should be no cached file at this point'); 77 | cache.put(revision, file); 78 | const fileContent = fs.readFileSync(file, 'utf8'); 79 | const actualContent = fs.readFileSync(cache.get(revision), 'utf8'); 80 | t.is(fileContent, actualContent, 'The cached file should match the source file'); 81 | }); 82 | }); 83 | 84 | /** 85 | * Configures node-chromium to use a filesystem cache. 86 | */ 87 | function setCacheDir() { 88 | const cacheDir = path.join(os.tmpdir(), 'chromium-cache'); 89 | fs.mkdirSync(cacheDir, {recursive: true}); 90 | testUtils.setEnvVar('NODE_CHROMIUM_CACHE_PATH', cacheDir); 91 | testUtils.clearEnvVar('NODE_CHROMIUM_CACHE_DISABLE'); 92 | } 93 | 94 | /** 95 | * Creates a text file which, for the purposes of the cache test, can be treated as a chromium binary, 96 | */ 97 | function createDummyFile() { 98 | const temporaryDir = os.tmpdir(); 99 | const uid = `${Date.now()}_${idx++}`; 100 | const name = `${uid}.txt`; 101 | const filePath = path.join(temporaryDir, name); 102 | fs.writeFileSync(filePath, `Hello ${uid}`); 103 | return filePath; 104 | } 105 | -------------------------------------------------------------------------------- /test/test-config.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const test = require('ava'); 4 | 5 | const testUtils = require('./_utils'); 6 | const config = require('../config'); 7 | 8 | /* eslint camelcase: ["error", {properties: "never"}] */ 9 | 10 | test.beforeEach(t => { 11 | testUtils.clearEnvVar('FOO_BAR'); 12 | t.pass(); 13 | }); 14 | 15 | test.serial('getEnvVar returns string always', t => { 16 | t.is('', config.getEnvVar('FOO_BAR')); 17 | }); 18 | 19 | test.serial('getEnvVar basic test', t => { 20 | const expected = Date.now().toString(); 21 | process.env.FOO_BAR = expected; 22 | t.is(expected, config.getEnvVar('FOO_BAR')); 23 | }); 24 | 25 | test.serial('getEnvVar looks for npm_config version', t => { 26 | const expected = Date.now().toString(); 27 | process.env.npm_config_foo_bar = expected; 28 | t.is(expected, config.getEnvVar('FOO_BAR')); 29 | }); 30 | 31 | test.serial('getEnvVar prefers npm_config version', t => { 32 | const expected = Date.now().toString(); 33 | process.env.FOO_BAR = 'foobar'; 34 | process.env.npm_config_foo_bar = expected; 35 | t.is(expected, config.getEnvVar('FOO_BAR'), 'npm_config_ variant should trump raw env var'); 36 | }); 37 | -------------------------------------------------------------------------------- /test/test-install.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const test = require('ava'); 4 | 5 | const fs = require('fs'); 6 | const rimraf = require('rimraf'); 7 | const got = require('got'); 8 | const debug = require('debug')('node-chromium'); 9 | 10 | const testUtils = require('./_utils'); 11 | const utils = require('../utils'); 12 | const config = require('../config'); 13 | const install = require('../install'); 14 | const chromium = require('..'); 15 | 16 | test.before(t => { 17 | // Deleting output folder 18 | const outPath = config.BIN_OUT_PATH; 19 | debug(`Deleting output folder: [${outPath}]`); 20 | 21 | if (fs.existsSync(outPath)) { 22 | rimraf.sync(outPath); 23 | } 24 | 25 | t.pass(); 26 | }); 27 | 28 | test.afterEach(t => { 29 | testUtils.clearMocks(); 30 | t.pass(); 31 | }); 32 | 33 | test.serial('Canary Test', t => { 34 | t.pass(); 35 | }); 36 | 37 | test.serial('Before Install Process', t => { 38 | const binPath = utils.getOsChromiumBinPath(); 39 | t.false(fs.existsSync(binPath), `Chromium binary is found in: [${binPath}]`); 40 | t.falsy(chromium.path, 'chromium.path should not be defined when there is no installation'); 41 | }); 42 | 43 | test.serial('Chromium Install', async t => { 44 | await install(); 45 | const binPath = utils.getOsChromiumBinPath(); 46 | const isExists = fs.existsSync(binPath); 47 | t.true(isExists, `Chromium binary is not found in: [${binPath}]`); 48 | t.true(fs.existsSync(chromium.path), 'chromium.path should be defined after installation'); 49 | }); 50 | 51 | test.serial('Different OS support', async t => { 52 | const supportedPlatforms = ['darwin', 'linux', 'win32']; 53 | const notSupportedPlatforms = ['aix', 'freebsd', 'openbsd', 'sunos']; 54 | 55 | /* eslint-disable no-await-in-loop */ 56 | for (const platform of supportedPlatforms) { 57 | testUtils.mockPlatform(platform); 58 | 59 | const revision = await utils.getLatestRevisionNumber(); 60 | 61 | const url = utils.getDownloadUrl(revision); 62 | t.true(await isUrlAccessible(url)); 63 | } 64 | /* eslint-enable no-await-in-loop */ 65 | 66 | for (const platform of notSupportedPlatforms) { 67 | testUtils.mockPlatform(platform); 68 | 69 | t.throws(() => { 70 | utils.getDownloadUrl(); 71 | }, {message: 'Unsupported platform'}); 72 | } 73 | 74 | t.pass(); 75 | }); 76 | 77 | async function isUrlAccessible(url) { 78 | try { 79 | const response = await got(url, {method: 'HEAD'}); 80 | return /4\d\d/.test(response.statusCode) === false; 81 | } catch (error) { 82 | console.warn(`An error [${error.message}] occurred while trying to check URL [${url}] accessibility`); 83 | return false; 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /test/test-utils.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const test = require('ava'); 4 | 5 | const config = require('../config'); 6 | const utils = require('../utils'); 7 | 8 | const OVERRIDE_URL = 'http://example.com/chromium-browser-snapshots/'; 9 | 10 | /* eslint camelcase: ["error", {properties: "never"}] */ 11 | 12 | test.beforeEach(t => { 13 | process.env = {}; // Prevent the real environment from interfering with these tests 14 | t.pass(); 15 | }); 16 | 17 | test.serial('getDownloadUrl uses default', t => { 18 | const url = utils.getDownloadUrl('737027'); 19 | t.true(url.indexOf(config.CDN_URL) === 0, `By default the URL should download from ${config.CDN_URL} but got ${url}`); 20 | }); 21 | 22 | test.serial('getDownloadUrl contains revision', t => { 23 | const revision = '737027'; 24 | const url = utils.getDownloadUrl(revision); 25 | t.true(url.indexOf(revision) > 0, `Expected revision ${revision} in ${url}`); 26 | }); 27 | 28 | test.serial('getDownloadUrl honors environment variable', t => { 29 | process.env.NODE_CHROMIUM_DOWNLOAD_HOST = OVERRIDE_URL; 30 | 31 | const url = utils.getDownloadUrl('737027'); 32 | t.true(url.indexOf(OVERRIDE_URL) === 0, `Download URL should honor environment variable ${OVERRIDE_URL} but got ${url}`); 33 | }); 34 | 35 | test.serial('getDownloadUrl honors npm config', t => { 36 | process.env.npm_config_node_chromium_download_host = OVERRIDE_URL; 37 | 38 | const url = utils.getDownloadUrl('737027'); 39 | t.true(url.indexOf(OVERRIDE_URL) === 0, `Download URL should honor npm config ${OVERRIDE_URL} but got ${url}`); 40 | }); 41 | -------------------------------------------------------------------------------- /utils.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const path = require('path'); 4 | const got = require('got'); 5 | const tunnel = require('tunnel'); 6 | 7 | const config = require('./config'); 8 | 9 | module.exports = { 10 | /** 11 | * Returns chromium output folder name for current OS 12 | * 13 | * @returns {string} 14 | */ 15 | getOsChromiumFolderName() { 16 | const platform = process.platform; 17 | 18 | let archivePlatformPrefix = platform; 19 | 20 | if (platform === 'darwin') { 21 | archivePlatformPrefix = 'mac'; 22 | } else if (platform === 'win32') { 23 | archivePlatformPrefix = 'win'; 24 | } 25 | 26 | return `chrome-${archivePlatformPrefix}`; 27 | }, 28 | 29 | /** 30 | * Returns path to Chromium executable binary where it's being downloaded 31 | * 32 | * @returns {string} 33 | */ 34 | getOsChromiumBinPath() { 35 | let binPath = path.join(config.BIN_OUT_PATH, this.getOsChromiumFolderName()); 36 | 37 | const platform = process.platform; 38 | 39 | if (platform === 'linux') { 40 | binPath = path.join(binPath, 'chrome'); 41 | } else if (platform === 'win32') { 42 | binPath = path.join(binPath, 'chrome.exe'); 43 | } else if (platform === 'darwin') { 44 | binPath = path.join(binPath, 'Chromium.app/Contents/MacOS/Chromium'); 45 | } else { 46 | throw new Error('Unsupported platform'); 47 | } 48 | 49 | return binPath; 50 | }, 51 | 52 | /** 53 | * Returns full URL where Chromium can be found for current OS 54 | * 55 | * @param revision - Chromium revision 56 | * 57 | * @returns {string} 58 | */ 59 | getDownloadUrl(revision) { 60 | const altUrl = config.getEnvVar('NODE_CHROMIUM_DOWNLOAD_HOST'); 61 | let revisionPath = `/${revision}/${this.getOsChromiumFolderName()}`; 62 | if (!altUrl) { 63 | revisionPath = encodeURIComponent(revisionPath); // Needed for googleapis.com 64 | } 65 | 66 | return `${this.getOsCdnUrl()}${revisionPath}.zip?alt=media`; 67 | }, 68 | 69 | /** 70 | * Returns download Url according to current OS 71 | * 72 | * @returns {string} 73 | */ 74 | getOsCdnUrl() { 75 | let url = config.getEnvVar('NODE_CHROMIUM_DOWNLOAD_HOST') || config.CDN_URL; 76 | 77 | const platform = process.platform; 78 | 79 | if (platform === 'linux') { 80 | url += 'Linux'; 81 | if (process.arch === 'x64') { 82 | url += '_x64'; 83 | } 84 | } else if (platform === 'win32') { 85 | url += 'Win'; 86 | if (process.arch === 'x64') { 87 | url += '_x64'; 88 | } 89 | } else if (platform === 'darwin') { 90 | url += 'Mac'; 91 | } else { 92 | throw new Error('Unsupported platform'); 93 | } 94 | 95 | return url; 96 | }, 97 | 98 | /** 99 | * Retrieves latest available Chromium revision number string for current OS 100 | * 101 | * @returns {Promise} 102 | */ 103 | async getLatestRevisionNumber() { 104 | const url = this.getOsCdnUrl() + '%2FLAST_CHANGE?alt=media'; 105 | return (await got(url, this.getRequestOptions(url))).body; 106 | }, 107 | 108 | /** 109 | * Computes necessary configuration options for use with *got*. For the time being this only considers proxy settings. 110 | * @param url the target URL 111 | * @returns {Object} 112 | */ 113 | getRequestOptions(url) { 114 | const requestOptions = {}; 115 | const proxy = url.startsWith('https://') ? (process.env.npm_config_https_proxy || process.env.HTTPS_PROXY) : 116 | (process.env.npm_config_proxy || process.env.npm_config_http_proxy || process.env.HTTP_PROXY); 117 | if (proxy) { 118 | const proxyUrl = new URL(proxy); 119 | const noProxy = (process.env.npm_config_no_proxy || process.env.NO_PROXY || '').split(','); 120 | if (noProxy.find(exc => proxyUrl.hostname.endsWith(exc)) !== undefined) { 121 | console.info('Using http(s) proxy server: ' + proxy); 122 | const tunnelOptions = { 123 | proxy: { 124 | host: proxyUrl.hostname, 125 | port: proxyUrl.port 126 | } 127 | }; 128 | if (proxyUrl.username && proxyUrl.password) { 129 | tunnelOptions.proxy.proxyAuth = `${proxyUrl.username}:${proxyUrl.password}`; 130 | } 131 | 132 | const agent = {}; 133 | if (url.startsWith('https://')) { 134 | if (proxy.startsWith('https://')) { 135 | agent.https = tunnel.httpsOverHttps(tunnelOptions); 136 | } else { 137 | agent.https = tunnel.httpsOverHttp(tunnelOptions); 138 | } 139 | } else if (proxy.startsWith('https://')) { 140 | agent.http = tunnel.httpOverHttps(tunnelOptions); 141 | } else { 142 | agent.http = tunnel.httpOverHttp(tunnelOptions); 143 | } 144 | 145 | requestOptions.agent = agent; 146 | } 147 | } 148 | 149 | return requestOptions; 150 | } 151 | }; 152 | --------------------------------------------------------------------------------