├── .editorconfig ├── .gitattributes ├── .gitignore ├── .npmrc ├── .travis.yml ├── fixtures ├── gifsicle-darwin.tar.gz ├── gifsicle-linux.tar.gz └── gifsicle-win32.tar.gz ├── index.js ├── license ├── package.json ├── readme.md └── test.js /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = tab 5 | end_of_line = lf 6 | charset = utf-8 7 | trim_trailing_whitespace = true 8 | insert_final_newline = true 9 | 10 | [*.yml] 11 | indent_style = space 12 | indent_size = 2 13 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto eol=lf 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | yarn.lock 3 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - '10' 4 | - '8' 5 | - '6' 6 | -------------------------------------------------------------------------------- /fixtures/gifsicle-darwin.tar.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kevva/bin-wrapper/569681b2dbcac7d138c840044134edb06615cedf/fixtures/gifsicle-darwin.tar.gz -------------------------------------------------------------------------------- /fixtures/gifsicle-linux.tar.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kevva/bin-wrapper/569681b2dbcac7d138c840044134edb06615cedf/fixtures/gifsicle-linux.tar.gz -------------------------------------------------------------------------------- /fixtures/gifsicle-win32.tar.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kevva/bin-wrapper/569681b2dbcac7d138c840044134edb06615cedf/fixtures/gifsicle-win32.tar.gz -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const fs = require('fs'); 3 | const path = require('path'); 4 | const url = require('url'); 5 | const pify = require('pify'); 6 | const importLazy = require('import-lazy')(require); 7 | 8 | const binCheck = importLazy('bin-check'); 9 | const binVersionCheck = importLazy('bin-version-check'); 10 | const download = importLazy('download'); 11 | const osFilterObj = importLazy('os-filter-obj'); 12 | 13 | const statAsync = pify(fs.stat); 14 | const chmodAsync = pify(fs.chmod); 15 | 16 | /** 17 | * Initialize a new `BinWrapper` 18 | * 19 | * @param {Object} options 20 | * @api public 21 | */ 22 | module.exports = class BinWrapper { 23 | constructor(options = {}) { 24 | this.options = options; 25 | 26 | if (this.options.strip <= 0) { 27 | this.options.strip = 0; 28 | } else if (!this.options.strip) { 29 | this.options.strip = 1; 30 | } 31 | } 32 | 33 | /** 34 | * Get or set files to download 35 | * 36 | * @param {String} src 37 | * @param {String} os 38 | * @param {String} arch 39 | * @api public 40 | */ 41 | src(src, os, arch) { 42 | if (arguments.length === 0) { 43 | return this._src; 44 | } 45 | 46 | this._src = this._src || []; 47 | this._src.push({ 48 | url: src, 49 | os, 50 | arch 51 | }); 52 | 53 | return this; 54 | } 55 | 56 | /** 57 | * Get or set the destination 58 | * 59 | * @param {String} dest 60 | * @api public 61 | */ 62 | dest(dest) { 63 | if (arguments.length === 0) { 64 | return this._dest; 65 | } 66 | 67 | this._dest = dest; 68 | return this; 69 | } 70 | 71 | /** 72 | * Get or set the binary 73 | * 74 | * @param {String} bin 75 | * @api public 76 | */ 77 | use(bin) { 78 | if (arguments.length === 0) { 79 | return this._use; 80 | } 81 | 82 | this._use = bin; 83 | return this; 84 | } 85 | 86 | /** 87 | * Get or set a semver range to test the binary against 88 | * 89 | * @param {String} range 90 | * @api public 91 | */ 92 | version(range) { 93 | if (arguments.length === 0) { 94 | return this._version; 95 | } 96 | 97 | this._version = range; 98 | return this; 99 | } 100 | 101 | /** 102 | * Get path to the binary 103 | * 104 | * @api public 105 | */ 106 | path() { 107 | return path.join(this.dest(), this.use()); 108 | } 109 | 110 | /** 111 | * Run 112 | * 113 | * @param {Array} cmd 114 | * @api public 115 | */ 116 | run(cmd = ['--version']) { 117 | return this.findExisting().then(() => { 118 | if (this.options.skipCheck) { 119 | return; 120 | } 121 | 122 | return this.runCheck(cmd); 123 | }); 124 | } 125 | 126 | /** 127 | * Run binary check 128 | * 129 | * @param {Array} cmd 130 | * @api private 131 | */ 132 | runCheck(cmd) { 133 | return binCheck(this.path(), cmd).then(works => { 134 | if (!works) { 135 | throw new Error(`The \`${this.path()}\` binary doesn't seem to work correctly`); 136 | } 137 | 138 | if (this.version()) { 139 | return binVersionCheck(this.path(), this.version()); 140 | } 141 | 142 | return Promise.resolve(); 143 | }); 144 | } 145 | 146 | /** 147 | * Find existing files 148 | * 149 | * @api private 150 | */ 151 | findExisting() { 152 | return statAsync(this.path()).catch(error => { 153 | if (error && error.code === 'ENOENT') { 154 | return this.download(); 155 | } 156 | 157 | return Promise.reject(error); 158 | }); 159 | } 160 | 161 | /** 162 | * Download files 163 | * 164 | * @api private 165 | */ 166 | download() { 167 | const files = osFilterObj(this.src() || []); 168 | const urls = []; 169 | 170 | if (files.length === 0) { 171 | return Promise.reject(new Error('No binary found matching your system. It\'s probably not supported.')); 172 | } 173 | 174 | files.forEach(file => urls.push(file.url)); 175 | 176 | return Promise.all(urls.map(url => download(url, this.dest(), { 177 | extract: true, 178 | strip: this.options.strip 179 | }))).then(result => { 180 | const resultingFiles = flatten(result.map((item, index) => { 181 | if (Array.isArray(item)) { 182 | return item.map(file => file.path); 183 | } 184 | 185 | const parsedUrl = url.parse(files[index].url); 186 | const parsedPath = path.parse(parsedUrl.pathname); 187 | 188 | return parsedPath.base; 189 | })); 190 | 191 | return Promise.all(resultingFiles.map(fileName => { 192 | return chmodAsync(path.join(this.dest(), fileName), 0o755); 193 | })); 194 | }); 195 | } 196 | }; 197 | 198 | function flatten(arr) { 199 | return arr.reduce((acc, elem) => { 200 | if (Array.isArray(elem)) { 201 | acc.push(...elem); 202 | } else { 203 | acc.push(elem); 204 | } 205 | 206 | return acc; 207 | }, []); 208 | } 209 | -------------------------------------------------------------------------------- /license: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) Kevin Mårtensson 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "bin-wrapper", 3 | "version": "4.1.0", 4 | "description": "Binary wrapper that makes your programs seamlessly available as local dependencies", 5 | "license": "MIT", 6 | "repository": "kevva/bin-wrapper", 7 | "author": { 8 | "name": "Kevin Mårtensson", 9 | "email": "kevinmartensson@gmail.com", 10 | "url": "https://github.com/kevva" 11 | }, 12 | "engines": { 13 | "node": ">=6" 14 | }, 15 | "scripts": { 16 | "test": "xo && ava" 17 | }, 18 | "files": [ 19 | "index.js" 20 | ], 21 | "keywords": [ 22 | "bin", 23 | "check", 24 | "local", 25 | "wrapper" 26 | ], 27 | "dependencies": { 28 | "bin-check": "^4.1.0", 29 | "bin-version-check": "^4.0.0", 30 | "download": "^7.1.0", 31 | "import-lazy": "^3.1.0", 32 | "os-filter-obj": "^2.0.0", 33 | "pify": "^4.0.1" 34 | }, 35 | "devDependencies": { 36 | "ava": "*", 37 | "executable": "^4.1.1", 38 | "nock": "^10.0.2", 39 | "path-exists": "^3.0.0", 40 | "rimraf": "^2.6.2", 41 | "tempy": "^0.2.1", 42 | "xo": "*" 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # bin-wrapper [![Build Status](https://travis-ci.org/kevva/bin-wrapper.svg?branch=master)](https://travis-ci.org/kevva/bin-wrapper) 2 | 3 | > Binary wrapper that makes your programs seamlessly available as local dependencies 4 | 5 | 6 | ## Install 7 | 8 | ``` 9 | $ npm install bin-wrapper 10 | ``` 11 | 12 | 13 | ## Usage 14 | 15 | ```js 16 | const BinWrapper = require('bin-wrapper'); 17 | 18 | const base = 'https://github.com/imagemin/gifsicle-bin/raw/master/vendor'; 19 | const bin = new BinWrapper() 20 | .src(`${base}/macos/gifsicle`, 'darwin') 21 | .src(`${base}/linux/x64/gifsicle`, 'linux', 'x64') 22 | .src(`${base}/win/x64/gifsicle.exe`, 'win32', 'x64') 23 | .dest(path.join('vendor')) 24 | .use(process.platform === 'win32' ? 'gifsicle.exe' : 'gifsicle') 25 | .version('>=1.71'); 26 | 27 | (async () => { 28 | await bin.run(['--version']); 29 | console.log('gifsicle is working'); 30 | })(); 31 | ``` 32 | 33 | Get the path to your binary with `bin.path()`: 34 | 35 | ```js 36 | console.log(bin.path()); 37 | //=> 'path/to/vendor/gifsicle' 38 | ``` 39 | 40 | 41 | ## API 42 | 43 | ### `new BinWrapper(options)` 44 | 45 | Creates a new `BinWrapper` instance. 46 | 47 | #### options 48 | 49 | Type: `Object` 50 | 51 | ##### skipCheck 52 | 53 | Type: `boolean`
54 | Default: `false` 55 | 56 | Whether to skip the binary check or not. 57 | 58 | ##### strip 59 | 60 | Type: `number`
61 | Default: `1` 62 | 63 | Strip a number of leading paths from file names on extraction. 64 | 65 | ### .src(url, [os], [arch]) 66 | 67 | Adds a source to download. 68 | 69 | #### url 70 | 71 | Type: `string` 72 | 73 | Accepts a URL pointing to a file to download. 74 | 75 | #### os 76 | 77 | Type: `string` 78 | 79 | Tie the source to a specific OS. 80 | 81 | #### arch 82 | 83 | Type: `string` 84 | 85 | Tie the source to a specific arch. 86 | 87 | ### .dest(destination) 88 | 89 | #### destination 90 | 91 | Type: `string` 92 | 93 | Accepts a path which the files will be downloaded to. 94 | 95 | ### .use(binary) 96 | 97 | #### binary 98 | 99 | Type: `string` 100 | 101 | Define which file to use as the binary. 102 | 103 | ### .path() 104 | 105 | Returns the full path to your binary. 106 | 107 | ### .version(range) 108 | 109 | #### range 110 | 111 | Type: `string` 112 | 113 | Define a [semver range](https://github.com/isaacs/node-semver#ranges) to check 114 | the binary against. 115 | 116 | ### .run([arguments]) 117 | 118 | Runs the search for the binary. If no binary is found it will download the file 119 | using the URL provided in `.src()`. 120 | 121 | #### arguments 122 | 123 | Type: `Array`
124 | Default: `['--version']` 125 | 126 | Command to run the binary with. If it exits with code `0` it means that the 127 | binary is working. 128 | 129 | 130 | ## License 131 | 132 | MIT © [Kevin Mårtensson](http://kevinmartensson.com) 133 | -------------------------------------------------------------------------------- /test.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import path from 'path'; 3 | import nock from 'nock'; 4 | import pathExists from 'path-exists'; 5 | import pify from 'pify'; 6 | import rimraf from 'rimraf'; 7 | import test from 'ava'; 8 | import tempy from 'tempy'; 9 | import executable from 'executable'; 10 | import Fn from '.'; 11 | 12 | const rimrafP = pify(rimraf); 13 | const fixture = path.join.bind(path, __dirname, 'fixtures'); 14 | 15 | test.beforeEach(() => { 16 | nock('http://foo.com') 17 | .get('/gifsicle.tar.gz') 18 | .replyWithFile(200, fixture('gifsicle-' + process.platform + '.tar.gz')) 19 | .get('/gifsicle-darwin.tar.gz') 20 | .replyWithFile(200, fixture('gifsicle-darwin.tar.gz')) 21 | .get('/gifsicle-win32.tar.gz') 22 | .replyWithFile(200, fixture('gifsicle-win32.tar.gz')) 23 | .get('/test.js') 24 | .replyWithFile(200, __filename); 25 | }); 26 | 27 | test('expose a constructor', t => { 28 | t.is(typeof Fn, 'function'); 29 | }); 30 | 31 | test('add a source', t => { 32 | const bin = new Fn().src('http://foo.com/bar.tar.gz'); 33 | t.is(bin._src[0].url, 'http://foo.com/bar.tar.gz'); 34 | }); 35 | 36 | test('add a source to a specific os', t => { 37 | const bin = new Fn().src('http://foo.com', process.platform); 38 | t.is(bin._src[0].os, process.platform); 39 | }); 40 | 41 | test('set destination directory', t => { 42 | const bin = new Fn().dest(path.join(__dirname, 'foo')); 43 | t.is(bin._dest, path.join(__dirname, 'foo')); 44 | }); 45 | 46 | test('set which file to use as the binary', t => { 47 | const bin = new Fn().use('foo'); 48 | t.is(bin._use, 'foo'); 49 | }); 50 | 51 | test('set a version range to test against', t => { 52 | const bin = new Fn().version('1.0.0'); 53 | t.is(bin._version, '1.0.0'); 54 | }); 55 | 56 | test('get the binary path', t => { 57 | const bin = new Fn() 58 | .dest('tmp') 59 | .use('foo'); 60 | 61 | t.is(bin.path(), path.join('tmp', 'foo')); 62 | }); 63 | 64 | test('verify that a binary is working', async t => { 65 | const bin = new Fn() 66 | .src('http://foo.com/gifsicle.tar.gz') 67 | .dest(tempy.directory()) 68 | .use(process.platform === 'win32' ? 'gifsicle.exe' : 'gifsicle'); 69 | 70 | await bin.run(); 71 | t.true(await pathExists(bin.path())); 72 | await rimrafP(bin.dest()); 73 | }); 74 | 75 | test('meet the desired version', async t => { 76 | const bin = new Fn() 77 | .src('http://foo.com/gifsicle.tar.gz') 78 | .dest(tempy.directory()) 79 | .use(process.platform === 'win32' ? 'gifsicle.exe' : 'gifsicle') 80 | .version('>=1.71'); 81 | 82 | await bin.run(); 83 | t.true(await pathExists(bin.path())); 84 | await rimrafP(bin.dest()); 85 | }); 86 | 87 | test('download files even if they are not used', async t => { 88 | const bin = new Fn({strip: 0, skipCheck: true}) 89 | .src('http://foo.com/gifsicle-darwin.tar.gz') 90 | .src('http://foo.com/gifsicle-win32.tar.gz') 91 | .src('http://foo.com/test.js') 92 | .dest(tempy.directory()) 93 | .use(process.platform === 'win32' ? 'gifsicle.exe' : 'gifsicle'); 94 | 95 | await bin.run(); 96 | const files = fs.readdirSync(bin.dest()); 97 | 98 | t.is(files.length, 3); 99 | t.is(files[0], 'gifsicle'); 100 | t.is(files[1], 'gifsicle.exe'); 101 | t.is(files[2], 'test.js'); 102 | 103 | await rimrafP(bin.dest()); 104 | }); 105 | 106 | test('skip running binary check', async t => { 107 | const bin = new Fn({skipCheck: true}) 108 | .src('http://foo.com/gifsicle.tar.gz') 109 | .dest(tempy.directory()) 110 | .use(process.platform === 'win32' ? 'gifsicle.exe' : 'gifsicle'); 111 | 112 | await bin.run(['--shouldNotFailAnyway']); 113 | t.true(await pathExists(bin.path())); 114 | await rimrafP(bin.dest()); 115 | }); 116 | 117 | test('error if no binary is found and no source is provided', async t => { 118 | const bin = new Fn() 119 | .dest(tempy.directory()) 120 | .use(process.platform === 'win32' ? 'gifsicle.exe' : 'gifsicle'); 121 | 122 | await t.throws(bin.run(), 'No binary found matching your system. It\'s probably not supported.'); 123 | }); 124 | 125 | test('downloaded files are set to be executable', async t => { 126 | const bin = new Fn({strip: 0, skipCheck: true}) 127 | .src('http://foo.com/gifsicle-darwin.tar.gz') 128 | .src('http://foo.com/gifsicle-win32.tar.gz') 129 | .src('http://foo.com/test.js') 130 | .dest(tempy.directory()) 131 | .use(process.platform === 'win32' ? 'gifsicle.exe' : 'gifsicle'); 132 | 133 | await bin.run(); 134 | 135 | const files = fs.readdirSync(bin.dest()); 136 | 137 | files.forEach(fileName => { 138 | t.true(executable.sync(path.join(bin.dest(), fileName))); 139 | }); 140 | }); 141 | --------------------------------------------------------------------------------