├── .gitignore ├── .npmignore ├── lib └── filter-package-deps.js ├── LICENSE.md ├── package.json ├── index.js ├── test.js └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | bower_components 2 | node_modules 3 | *.log 4 | .DS_Store 5 | bundle.js 6 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | bower_components 2 | node_modules 3 | *.log 4 | .DS_Store 5 | bundle.js 6 | test 7 | test.js 8 | demo/ 9 | .npmignore 10 | LICENSE.md -------------------------------------------------------------------------------- /lib/filter-package-deps.js: -------------------------------------------------------------------------------- 1 | var types = [ 2 | 'dependencies', 3 | 'devDependencies', 4 | 'optionalDependencies' 5 | ] 6 | 7 | // Given a package.json and list of deps, 8 | // will filter down to those that haven't yet 9 | // been installed 10 | module.exports = function (package, opt) { 11 | opt = opt || {} 12 | 13 | return types.reduce(function (dict, type) { 14 | var actualDeps = package[type] || {} 15 | var actualNames = Object.keys(actualDeps) 16 | var desired = [].concat(opt[type]).filter(Boolean) 17 | 18 | // only installs packages that aren't in package.json 19 | var needed = desired.filter(function (pkgName) { 20 | // split scoped package names 21 | var parts = pkgName.split(/(.+)@/).filter(Boolean) 22 | var name = parts[0] 23 | var exists = actualNames.indexOf(name) >= 0 24 | return !exists 25 | }) 26 | 27 | if (needed.length) 28 | dict[type] = needed 29 | return dict 30 | }, {}) 31 | } 32 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | Copyright (c) 2015 Matt DesLauriers 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in all 12 | copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 17 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 18 | DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR 19 | OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE 20 | OR OTHER DEALINGS IN THE SOFTWARE. 21 | 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "install-if-needed", 3 | "version": "1.0.4", 4 | "description": "installs the given modules if needed", 5 | "main": "index.js", 6 | "license": "MIT", 7 | "author": { 8 | "name": "Matt DesLauriers", 9 | "email": "dave.des@gmail.com", 10 | "url": "https://github.com/mattdesl" 11 | }, 12 | "dependencies": { 13 | "closest-package": "^1.0.0", 14 | "object-assign": "^2.0.0", 15 | "read-closest-package": "^1.0.0", 16 | "read-json": "^1.0.3", 17 | "run-series": "^1.1.0", 18 | "spawn-npm-install": "^1.3.0" 19 | }, 20 | "devDependencies": { 21 | "tape": "^4.0.0" 22 | }, 23 | "scripts": { 24 | "test": "node test.js" 25 | }, 26 | "keywords": [ 27 | "install", 28 | "npm", 29 | "programmatically", 30 | "spawn", 31 | "installation", 32 | "cli", 33 | "command", 34 | "module", 35 | "installs", 36 | "installing", 37 | "save", 38 | "--save", 39 | "--save-dev", 40 | "dep", 41 | "deps", 42 | "dependencies", 43 | "devDependencies" 44 | ], 45 | "repository": { 46 | "type": "git", 47 | "url": "git://github.com/mattdesl/install-if-needed.git" 48 | }, 49 | "homepage": "https://github.com/mattdesl/install-if-needed", 50 | "bugs": { 51 | "url": "https://github.com/mattdesl/install-if-needed/issues" 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | var install = require('spawn-npm-install') 2 | var each = require('run-series') 3 | var assign = require('object-assign') 4 | var filter = require('./lib/filter-package-deps') 5 | var readPackage = require('read-closest-package') 6 | 7 | var saveArg = { 8 | dependencies: 'save', 9 | devDependencies: 'saveDev', 10 | optionalDependencies: 'saveOptional' 11 | } 12 | 13 | module.exports = function (opt, cb) { 14 | if (typeof opt === 'string' || Array.isArray(opt)) 15 | opt = { dependencies: opt } 16 | opt = opt || {} 17 | 18 | if (opt.package) { 19 | process.nextTick(function () { 20 | run(null, opt.package) 21 | }) 22 | } else { 23 | readPackage({ cwd: opt.cwd || process.cwd() }, run) 24 | } 25 | 26 | function run (err, packageData) { 27 | if (err) 28 | return cb(err) 29 | 30 | // if we should --save / --save-dev / --save-optional 31 | var useSave = opt.save !== false 32 | 33 | // get needed dependencies 34 | var needed = filter(packageData, opt) 35 | 36 | // get install tasks 37 | var tasks = Object.keys(needed).map(function (key) { 38 | var installOpts = assign({}, opt) 39 | delete installOpts.save 40 | 41 | var save = saveArg[key] 42 | if (useSave && save) 43 | installOpts[save] = true 44 | 45 | return function (next) { 46 | var deps = needed[key] 47 | install(deps, installOpts, next) 48 | } 49 | }) 50 | 51 | // run each task in series 52 | each(tasks, cb) 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /test.js: -------------------------------------------------------------------------------- 1 | var install = require('./') 2 | var test = require('tape') 3 | var json = require('read-json') 4 | var uninstall = require('spawn-npm-install').uninstall 5 | var path = require('path') 6 | 7 | test('installs the given modules if needed', function (t) { 8 | t.plan(3) 9 | 10 | install({ 11 | devDependencies: ['tape'], 12 | optionalDependencies: ['through2', 'quote-stream'] 13 | }, function (err) { 14 | if (err) t.fail(err) 15 | includes(t, 'devDependencies', 'tape', true) 16 | includes(t, 'optionalDependencies', 'through2', true) 17 | includes(t, 'optionalDependencies', 'quote-stream', true) 18 | 19 | uninstall(['through2', 'quote-stream'], { saveOptional: true }, function (err) { 20 | if (err) t.fail(err) 21 | }) 22 | }) 23 | }) 24 | 25 | test('allows package data to be specified', function (t) { 26 | t.plan(1) 27 | 28 | install({ 29 | package: { 30 | devDependencies: { 31 | 'quote-stream': '*' 32 | } 33 | }, 34 | devDependencies: ['quote-stream'] 35 | }, function (err) { 36 | if (err) t.fail(err) 37 | includes(t, 'devDependencies', 'quote-stream', false) 38 | }) 39 | }) 40 | 41 | test('does not save deps when save: false', function (t) { 42 | t.plan(2) 43 | 44 | install({ 45 | save: false, 46 | optionalDependencies: ['zalgo'], 47 | devDependencies: ['quote-stream'] 48 | }, function (err) { 49 | if (err) t.fail(err) 50 | includes(t, 'devDependencies', 'quote-stream', false) 51 | includes(t, 'optionalDependencies', 'zalgo', false) 52 | }) 53 | }) 54 | 55 | test('saves to dependencies', function (t) { 56 | t.plan(2) 57 | 58 | install(['zalgo', 'quote-stream'], function (err) { 59 | if (err) t.fail(err) 60 | includes(t, 'dependencies', 'quote-stream', true) 61 | includes(t, 'dependencies', 'zalgo', true) 62 | 63 | uninstall(['zalgo', 'quote-stream'], { save: true }, function (err) { 64 | if (err) t.fail(err) 65 | }) 66 | }) 67 | }) 68 | 69 | function includes (t, key, name, exists) { 70 | json(path.join(__dirname, 'package.json'), function (err, data) { 71 | if (err) t.fail(err) 72 | 73 | var idx = Object.keys(data[key]).indexOf(name) 74 | if (exists) { 75 | t.notEqual(idx, -1, 'has dep') 76 | } else { 77 | t.equal(idx, -1, 'does not have dep') 78 | } 79 | }) 80 | } 81 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # install-if-needed 2 | 3 | [![stable](http://badges.github.io/stability-badges/dist/stable.svg)](http://github.com/badges/stability-badges) 4 | 5 | Installs the given list of modules and saves them into their respective fields in your nearest `package.json`. Dependencies that already exist in your `package.json` will be skipped. 6 | 7 | ```js 8 | var install = require('install-if-needed') 9 | 10 | install({ 11 | dependencies: ['through2'], 12 | devDependencies: ['tape@2.x', 'standard'] 13 | }, function(err) { 14 | if (err) 15 | console.error("There was an error installing.") 16 | }) 17 | ``` 18 | 19 | You can pass `{ stdio: 'inherit' }` to preserve logging and colors, acting like the usual `npm install` command. 20 | 21 | ## Usage 22 | 23 | [![NPM](https://nodei.co/npm/install-if-needed.png)](https://www.npmjs.com/package/install-if-needed) 24 | 25 | #### `install(opt[, cb])` 26 | 27 | Looks at `package` JSON and installs any of the specified dependencies that are not listed in their respective field. 28 | 29 | - `cwd` the directory for the [closest package.json](https://www.npmjs.com/package/closest-package), (default `process.cwd()`) 30 | - `package` optional package data, if not defined will search for closest `package.json` 31 | - `save` whether to `--save`, `--save-dev` and `--save-optional` (default true) 32 | - `dependencies` dependencies to install 33 | - `devDependencies` dev dependencies to install 34 | - `optionalDependencies` optional dependencies to install 35 | - `command` the command to spawn when installing, defaults to `'npm'` 36 | 37 | Other options are passed to [spawn-npm-install](https://www.npmjs.com/package/spawn-npm-install). 38 | 39 | On complete, `cb` is called with `(err)` status. All dependencies also accept a single string instead of an array. 40 | 41 | Alternatively, `opt` can be a string or array, which is the same as listing it in `dependencies`. 42 | 43 | ```js 44 | //e.g. 45 | // npm install tape --save 46 | install('tape', done) 47 | ``` 48 | 49 | ## Motivation 50 | 51 | This helps build CLI tooling that auto-installs modules as needed. For example, a tool which stubs out an empty test file for [tape](https://www.npmjs.com/package/tape): 52 | 53 | ```js 54 | #!/usr/bin/env node 55 | var install = require('install-if-needed') 56 | var fs = require('fs') 57 | var template = fs.readFileSync(__dirname + '/template.js') 58 | 59 | install({ 60 | devDependencies: 'tape' 61 | }, function(err) { 62 | if (err) throw err 63 | fs.writeFile(process.argv[2], template) 64 | }) 65 | ``` 66 | 67 | And the CLI might be as simple as: 68 | 69 | ```sh 70 | quick-tape tests/simple.js 71 | ``` 72 | 73 | ## License 74 | 75 | MIT, see [LICENSE.md](http://github.com/mattdesl/install-if-needed/blob/master/LICENSE.md) for details. 76 | --------------------------------------------------------------------------------