├── .gitignore ├── .npmrc ├── .travis.yml ├── README.md ├── __snapshots__ └── utils-spec.js.snap-shot ├── bin ├── _have-it.js └── have-it.js ├── dist └── have-it.js ├── package.json ├── rollup.config.js ├── src ├── have-it-spec.js ├── index.js ├── utils-spec.js └── utils.js └── test ├── e2e.sh └── index.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .DS_Store 3 | npm-debug.log 4 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | registry=http://registry.npmjs.org/ 2 | save-exact=true 3 | progress=false 4 | 5 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | cache: 3 | directories: 4 | - node_modules 5 | notifications: 6 | email: true 7 | node_js: 8 | - '8' 9 | before_script: 10 | - npm prune 11 | script: 12 | - npm test 13 | - npm run build 14 | - npm run e2e 15 | after_success: 16 | - npm run semantic-release 17 | branches: 18 | except: 19 | - /^v\d+\.\d+\.\d+$/ 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # have-it 2 | 3 | > The fastest NPM install does nothing because you already have it 4 | 5 | [![NPM][npm-icon] ][npm-url] 6 | 7 | [![Build status][ci-image] ][ci-url] 8 | [![semantic-release][semantic-image] ][semantic-url] 9 | [![js-standard-style][standard-image]][standard-url] 10 | 11 | If you have lots of local projects each with its own `node_modules` folder 12 | you probably already have a huge number of installed NPM packages. If you 13 | are offline or hate waiting, you can "install" a module from another folder 14 | into the current one using dummy "proxy" module. The setup is almost instant! 15 | 16 | Watch in action: [NPM vs Yarn vs have-it](https://www.youtube.com/watch?v=A0o1kC3d_Co) 17 | 18 | Quick stats: installing lodash, debug and express takes NPM 5 seconds, 19 | yarn takes 3 seconds, and `have-it` takes 250ms (0.25 seconds) 20 | 21 | ## Example 22 | 23 | 1. Install `have-it` globally with `npm i -g have-it`. This tool will be 24 | available under `have` and `have-it` names. 25 | 26 | 2. Set root folder for top level search. For example my projects are usually 27 | in `$HOME/git` folder. Thus I set `export HAVE=$HOME/git`. By default it 28 | will use `$HOME` value as the root. 29 | 30 | ``` 31 | $HOME 32 | /git 33 | /projectA 34 | /node_modules 35 | /projectB 36 | /node_modules 37 | /projectC 38 | /node_modules 39 | ``` 40 | 41 | 3. Install something with `have `. For example 42 | 43 | ```sh 44 | $ time have lodash 45 | have-it lodash 46 | have 1 module(s) 47 | lodash@4.17.4 48 | 49 | real 0m0.240s 50 | ``` 51 | 52 | For comparison `$ time npm i lodash` prints `real 0m1.909s` - a speed up 53 | of 10 times! 54 | 55 | You can pass typical NPM flags to save the installed version 56 | `-S --save -D --save-dev`. 57 | 58 | ## Installing dependencies from package.json 59 | 60 | Just run `have` to install dependencies from the `package.json` file. 61 | 62 | ## Fallback 63 | 64 | If a module cannot be found locally, `have-it` falls back to using 65 | `npm install` command. 66 | 67 | ## How does it work? 68 | 69 | `have-it` finds the module already installed and then creates in the local 70 | `node_modules` folder a dummy file that has `main` pointing at the found 71 | `main` file. For example, if `lodash` was found in folder 72 | `/Users/gleb/projectX/node_modules/lodash` the local dummy package will be 73 | 74 | ``` 75 | cat node_modules/lodash/package.json 76 | { 77 | "name": "lodash", 78 | "main": "/Users/gleb/projectX/node_modules/lodash/lodash.js", 79 | "version": "4.17.4", 80 | "description": "fake module created by 'have-it' pointing at existing module" 81 | } 82 | ``` 83 | 84 | Having actual dummy module like above works nicely with Node and its 85 | module loader. 86 | 87 | ## Main features 88 | 89 | Seems all use cases are already implemented: installing a specific version, 90 | saving version in `package.json`, etc. 91 | 92 | * [x] [#8](https://github.com/bahmutov/have-it/issues/8) 93 | save installed name version in `package.json` 94 | * [x] [#9](https://github.com/bahmutov/have-it/issues/9) 95 | respect `package.json` versions when installing 96 | * [x] [#10](https://github.com/bahmutov/have-it/issues/10) 97 | allow installing specific version from CLI `have lodash@3.0.0` 98 | * [x] [#12](https://github.com/bahmutov/have-it/issues/12) 99 | make symbolic links for each "bin" entry 100 | 101 | ## Related projects 102 | 103 | * [copi](https://github.com/bahmutov/copi) - physically copies found package 104 | into this folder 105 | * [local-npm](https://github.com/nolanlawson/local-npm) - Local and 106 | offline-first npm mirror (unmaintained) 107 | 108 | ## FAQ 109 | 110 |
111 | Why not use `npm link` 112 |

`npm link` is cumbersome and links a single package *version* globally

113 |
114 | 115 |
116 | Why not use symbolic links? 117 |

Symbolic links do not work if the linked package needs to load another 118 | one of its own packages. For example `debug` requires `ms`. If we 119 | link to `debug` package folder, then Node module loader fails to 120 | find `ms`

121 |
122 | 123 |
124 | Why not use local NPM proxy? 125 |

Because it is (relatively) hard

126 |
127 | 128 |
129 | What happens in production / CI? 130 |

Nothing, you just use `npm install` there

131 |
132 | 133 | ## Debugging 134 | 135 | Run this tool with `DEBUG=have-it have ...` environment variable. 136 | 137 | To run [e2e test](test/e2e.sh) use `npm run e2e` 138 | 139 | To avoid building a single "dist" file during local development, add 140 | a new alias to the `package.json` 141 | 142 | ```json 143 | { 144 | "bin": { 145 | "have-it": "dist/have-it.js", 146 | "have": "dist/have-it.js", 147 | "_have": "bin/_have-it.js" 148 | } 149 | } 150 | ``` 151 | 152 | and use this alias for local work 153 | 154 | ```sh 155 | npm link 156 | _have lodash 157 | ``` 158 | 159 | ### Small print 160 | 161 | Author: Gleb Bahmutov <gleb.bahmutov@gmail.com> © 2017 162 | 163 | * [@bahmutov](https://twitter.com/bahmutov) 164 | * [glebbahmutov.com](https://glebbahmutov.com) 165 | * [blog](https://glebbahmutov.com/blog) 166 | 167 | License: MIT - do anything with the code, but don't blame me if it does not work. 168 | 169 | Support: if you find any problems with this module, email / tweet / 170 | [open issue](https://github.com/bahmutov/have-it/issues) on Github 171 | 172 | ## MIT License 173 | 174 | Copyright (c) 2017 Gleb Bahmutov <gleb.bahmutov@gmail.com> 175 | 176 | Permission is hereby granted, free of charge, to any person 177 | obtaining a copy of this software and associated documentation 178 | files (the "Software"), to deal in the Software without 179 | restriction, including without limitation the rights to use, 180 | copy, modify, merge, publish, distribute, sublicense, and/or sell 181 | copies of the Software, and to permit persons to whom the 182 | Software is furnished to do so, subject to the following 183 | conditions: 184 | 185 | The above copyright notice and this permission notice shall be 186 | included in all copies or substantial portions of the Software. 187 | 188 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 189 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 190 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 191 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 192 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 193 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 194 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 195 | OTHER DEALINGS IN THE SOFTWARE. 196 | 197 | [npm-icon]: https://nodei.co/npm/have-it.svg?downloads=true 198 | [npm-url]: https://npmjs.org/package/have-it 199 | [ci-image]: https://travis-ci.org/bahmutov/have-it.svg?branch=master 200 | [ci-url]: https://travis-ci.org/bahmutov/have-it 201 | [semantic-image]: https://img.shields.io/badge/%20%20%F0%9F%93%A6%F0%9F%9A%80-semantic--release-e10079.svg 202 | [semantic-url]: https://github.com/semantic-release/semantic-release 203 | [standard-image]: https://img.shields.io/badge/code%20style-standard-brightgreen.svg 204 | [standard-url]: http://standardjs.com/ 205 | -------------------------------------------------------------------------------- /__snapshots__/utils-spec.js.snap-shot: -------------------------------------------------------------------------------- 1 | exports['finds missing deps 1'] = [ 2 | { 3 | "name": "bar" 4 | } 5 | ] 6 | 7 | -------------------------------------------------------------------------------- /bin/_have-it.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | // useful for local testing without building 4 | try { 5 | require('./have-it.js') 6 | } catch (err) { 7 | console.error(err) 8 | console.error('Could not load ./have-it.js') 9 | console.error('_have alias is only meant for local development!') 10 | console.error('Use "have" or "have-it" please') 11 | process.exit(1) 12 | } 13 | -------------------------------------------------------------------------------- /bin/have-it.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const findAndInstall = require('..') 4 | const name = process.argv[2] 5 | const {toInstall} = require('../src/utils') 6 | const R = require('ramda') 7 | 8 | function onError (err) { 9 | console.error(err) 10 | process.exit(1) 11 | } 12 | 13 | if (!name) { 14 | // installing all packages from package.json 15 | // no CLI options because the list of names in the package.json already 16 | toInstall() 17 | .then(findAndInstall) 18 | .catch(onError) 19 | } else { 20 | const isOption = s => s.startsWith('-') 21 | const [options, names] = R.partition(isOption, process.argv.slice(2)) 22 | console.log('have-it %s', names.join(' ')) 23 | findAndInstall(names, options) 24 | .catch(onError) 25 | } 26 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "have-it", 3 | "description": "The fastest NPM install does nothing because you already have it", 4 | "version": "0.0.0-development", 5 | "author": "Gleb Bahmutov ", 6 | "bugs": "https://github.com/bahmutov/have-it/issues", 7 | "config": { 8 | "pre-git": { 9 | "commit-msg": "simple", 10 | "pre-commit": [ 11 | "npm prune", 12 | "npm run deps", 13 | "npm test", 14 | "npm run build", 15 | "npm run ban" 16 | ], 17 | "pre-push": [ 18 | "npm run e2e", 19 | "npm run secure", 20 | "npm run license", 21 | "npm run ban -- --all", 22 | "npm run size" 23 | ], 24 | "post-commit": [], 25 | "post-merge": [] 26 | } 27 | }, 28 | "engines": { 29 | "node": ">=6" 30 | }, 31 | "files": [ 32 | "dist", 33 | "bin/_have-it.js" 34 | ], 35 | "bin": { 36 | "have-it": "dist/have-it.js", 37 | "have": "dist/have-it.js", 38 | "_have": "bin/_have-it.js" 39 | }, 40 | "homepage": "https://github.com/bahmutov/have-it#readme", 41 | "keywords": [ 42 | "fast", 43 | "install", 44 | "installer", 45 | "npm", 46 | "yarn" 47 | ], 48 | "license": "MIT", 49 | "main": "src/", 50 | "publishConfig": { 51 | "registry": "http://registry.npmjs.org/" 52 | }, 53 | "repository": { 54 | "type": "git", 55 | "url": "https://github.com/bahmutov/have-it.git" 56 | }, 57 | "scripts": { 58 | "ban": "ban", 59 | "deps": "deps-ok && dependency-check .", 60 | "issues": "git-issues", 61 | "license": "license-checker --production --onlyunknown --csv", 62 | "lint": "standard --verbose --fix *.js src/*.js bin/*.js", 63 | "pretest": "npm run lint", 64 | "secure": "nsp check", 65 | "size": "t=\"$(npm pack .)\"; wc -c \"${t}\"; tar tvf \"${t}\"; rm \"${t}\";", 66 | "test": "npm run unit", 67 | "unit": "mocha src/*-spec.js", 68 | "semantic-release": "semantic-release pre && npm publish && semantic-release post", 69 | "postinstall": "echo \"have-it: set HAVE environment variable\nto the root folder with most packages\n\"", 70 | "build": "rollup -c rollup.config.js", 71 | "prebuild": "npm run lint", 72 | "e2e": "./test/e2e.sh" 73 | }, 74 | "devDependencies": { 75 | "ban-sensitive-files": "1.9.0", 76 | "check-more-types": "2.24.0", 77 | "debug": "3.1.0", 78 | "dependency-check": "2.9.1", 79 | "deps-ok": "1.2.1", 80 | "execa": "0.8.0", 81 | "git-issues": "1.3.1", 82 | "github-post-release": "1.13.1", 83 | "glob-all": "3.1.0", 84 | "lazy-ass": "1.6.0", 85 | "license-checker": "14.0.0", 86 | "mkdirp": "0.5.1", 87 | "mocha": "3.5.3", 88 | "nsp": "2.8.1", 89 | "pre-git": "3.15.3", 90 | "ramda": "0.24.1", 91 | "rollup": "0.41.4", 92 | "rollup-plugin-commonjs": "8.2.1", 93 | "rollup-plugin-node-resolve": "3.0.0", 94 | "semantic-release": "^8.0.3", 95 | "semver": "5.4.1", 96 | "simple-commit-message": "3.3.1", 97 | "snap-shot": "2.17.0", 98 | "spawn-sync": "1.0.15", 99 | "standard": "8.6.0", 100 | "try-thread-sleep": "1.0.0" 101 | }, 102 | "release": { 103 | "analyzeCommits": "simple-commit-message", 104 | "generateNotes": "github-post-release" 105 | }, 106 | "dependencies": { 107 | "parse-package-name": "0.1.0" 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import commonjs from 'rollup-plugin-commonjs' 2 | import nodeResolve from 'rollup-plugin-node-resolve' 3 | 4 | export default { 5 | plugins: [ 6 | nodeResolve({ 7 | jsnext: true, 8 | main: true 9 | }), 10 | 11 | commonjs({ 12 | // if false then skip sourceMap generation for CommonJS modules 13 | sourceMap: false // Default: true 14 | }) 15 | ], 16 | entry: 'bin/have-it.js', 17 | dest: 'dist/have-it.js', 18 | format: 'cjs', 19 | banner: '#!/usr/bin/env node', 20 | external: [ 21 | 'fs', 'path', 'assert', 'util', 'events', 'tty', 'net', 22 | 'stream', 'child_process', 'os' 23 | ] 24 | } 25 | -------------------------------------------------------------------------------- /src/have-it-spec.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | /* global describe, it */ 4 | describe('have-it', () => { 5 | const haveIt = require('.') 6 | it('write this test', () => { 7 | console.assert(haveIt, 'should export something') 8 | }) 9 | }) 10 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const rootFolder = process.env.HAVE || process.env.HOME 4 | 5 | const debug = require('debug')('have-it') 6 | const path = require('path') 7 | const fs = require('fs') 8 | const R = require('ramda') 9 | const semver = require('semver') 10 | const la = require('lazy-ass') 11 | const is = require('check-more-types') 12 | const glob = require('glob-all') 13 | const execa = require('execa') 14 | const parse = require('parse-package-name') 15 | 16 | const {mkdir, saveJSON, findMissing, saveVersions} = require('./utils') 17 | 18 | debug('using root folder %s', rootFolder) 19 | 20 | function getVersion (folder) { 21 | const packageFilename = path.join(folder, 'package.json') 22 | return new Promise((resolve, reject) => { 23 | try { 24 | fs.readFile(packageFilename, 'utf8', (err, s) => { 25 | if (err) { 26 | return reject(err) 27 | } 28 | const json = JSON.parse(s) 29 | const main = json.main || 'index.js' 30 | // debug('main %s', main) 31 | const withExtension = main.endsWith('index') ? main + '.js' : main 32 | 33 | let fullMain = path.isAbsolute(withExtension) 34 | ? withExtension : path.join(folder, withExtension) 35 | 36 | let resolvedBin 37 | if (is.object(json.bin) && is.not.empty(json.bin)) { 38 | debug('resolving bin aliases') 39 | debug(json.bin) 40 | debug('with respect to folder', folder) 41 | const toFullBin = (value, key) => 42 | path.join(folder, value) 43 | resolvedBin = R.mapObjIndexed(toFullBin, json.bin) 44 | } 45 | 46 | if (!fs.existsSync(fullMain)) { 47 | fullMain += '.js' 48 | } 49 | 50 | if (!fs.existsSync(fullMain)) { 51 | const notFound = new Error(`Cannot find main file ${fullMain}`) 52 | return reject(notFound) 53 | } 54 | 55 | const result = { 56 | folder: folder, 57 | filename: packageFilename, 58 | name: json.name, 59 | version: json.version, 60 | main: fullMain 61 | } 62 | if (resolvedBin) { 63 | result.bin = resolvedBin 64 | } 65 | return resolve(result) 66 | }) 67 | } catch (err) { 68 | reject() 69 | } 70 | }) 71 | } 72 | 73 | function getVersionSafe (folder) { 74 | return getVersion(folder) 75 | } 76 | 77 | function byVersion (a, b) { 78 | return semver.compare(a.version, b.version) 79 | } 80 | 81 | const latestVersion = R.pipe( 82 | R.sort(byVersion), 83 | R.last 84 | ) 85 | 86 | const pickFoundVersion = target => found => { 87 | la(is.array(found), 'expected list of found items', found) 88 | 89 | if (is.not.semver(target)) { 90 | return latestVersion(found) 91 | } 92 | debug('need to pick version %s', target) 93 | debug('from list with %d found items', found.length) 94 | const sameVersion = R.propEq('version', target) 95 | return R.find(sameVersion, found) 96 | } 97 | 98 | // TODO unit test this 99 | const pickFoundVersions = targets => found => { 100 | debug('only need the following targets', targets) 101 | 102 | const pickInstall = (found, name) => { 103 | const target = R.find(R.propEq('name', name), targets) 104 | const picked = pickFoundVersion(target.version)(found) 105 | return picked 106 | } 107 | 108 | return R.mapObjIndexed(pickInstall, found) 109 | } 110 | 111 | function findModules (searchNames) { 112 | la(is.strings(searchNames), 'expected names to find', searchNames) 113 | 114 | // names could potentially have version part 115 | const parsedNames = searchNames.map(parse) 116 | const names = R.pluck('name', parsedNames) 117 | debug('just names', names) 118 | 119 | const searches = names.map(name => `${rootFolder}/*/node_modules/${name}`) 120 | const folders = glob.sync(searches) 121 | return Promise.all(folders.map(getVersionSafe)) 122 | .then(R.filter(R.is(Object))) 123 | .then(R.groupBy(R.prop('name'))) 124 | .then(pickFoundVersions(parsedNames)) 125 | .then(results => { 126 | const found = R.pickBy(is.object, results) 127 | const foundNames = R.keys(found) 128 | const missing = findMissing(parsedNames, foundNames) 129 | if (is.not.empty(missing)) { 130 | console.log('You do not have %d module(s): %s', 131 | missing.length, missing.map(R.prop('name')).join(', ')) 132 | debug('all names to find', names) 133 | debug('found names', foundNames) 134 | debug('missing names', missing) 135 | } 136 | return { 137 | missing, 138 | found 139 | } 140 | }) 141 | } 142 | 143 | function print (modules) { 144 | const different = R.uniqBy(R.prop('version'))(modules) 145 | debug('%d different version(s)', different.length) 146 | debug(R.project(['version'], different)) 147 | } 148 | 149 | function installMain (p) { 150 | if (!p) { 151 | console.error('nothing to install') 152 | // TODO: use real NPM install in this case 153 | return 154 | } 155 | const nodeModulesFolder = path.join(process.cwd(), 'node_modules') 156 | const destination = path.join(nodeModulesFolder, p.name) 157 | debug('installing found module') 158 | debug(p) 159 | debug('as', destination) 160 | 161 | const linkAnyBin = () => { 162 | if (p.bin) { 163 | debug('linking bin') 164 | debug(p.bin) 165 | const binFolder = path.join(nodeModulesFolder, '.bin') 166 | if (!fs.existsSync(binFolder)) { 167 | debug('making .bin folder', binFolder) 168 | fs.mkdirSync(binFolder) 169 | } 170 | const linkBin = (aliasPath, alias) => { 171 | const binLink = path.join(binFolder, alias) 172 | debug(binLink, '->', aliasPath) 173 | fs.symlinkSync(aliasPath, binLink) 174 | } 175 | R.mapObjIndexed(linkBin, p.bin) 176 | } else { 177 | debug('nothing to link into .bin') 178 | } 179 | } 180 | 181 | return mkdir(destination) 182 | .then(() => { 183 | const pkg = { 184 | name: p.name, 185 | main: p.main, 186 | version: p.version, 187 | description: 'fake module created by \'have-it\' pointing at existing module' 188 | } 189 | const filename = path.join(destination, 'package.json') 190 | return saveJSON(filename, pkg) 191 | }) 192 | .then(linkAnyBin) 193 | .then(R.always(p)) 194 | } 195 | 196 | const saveDependencies = options => 197 | options.includes('-S') || options.includes('--save') 198 | 199 | const saveDevDependencies = options => 200 | options.includes('-D') || options.includes('--save-dev') 201 | 202 | function haveModules (list, options = []) { 203 | la(is.strings(options), 'expected list of options', options) 204 | 205 | const nameAndVersion = R.project(['name', 'version'])(list) 206 | 207 | return Promise.all(list.map(installMain)) 208 | .then(() => { 209 | list.forEach(p => { 210 | console.log(`have ${p.name}@${p.version}`) 211 | }) 212 | }) 213 | .then(() => { 214 | if (saveDependencies(options)) { 215 | debug('saving as dependencies in package.json') 216 | saveVersions(nameAndVersion) 217 | } else if (saveDevDependencies(options)) { 218 | debug('saving as devDependencies in package.json') 219 | saveVersions(nameAndVersion, true) 220 | } 221 | }) 222 | } 223 | 224 | const fullInstallName = parsed => 225 | parsed.version ? `${parsed.name}@${parsed.version}` : parsed.name 226 | 227 | function npmInstall (list, options) { 228 | la(is.array(list), 'expected list of modules to npm install', list) 229 | la(is.strings(options), 'expected list of CLI options', options) 230 | 231 | if (is.empty(list)) { 232 | return Promise.resolve() 233 | } 234 | const flags = options.join(' ') 235 | const names = list.map(fullInstallName).join(' ') 236 | const cmd = `npm install ${flags} ${names}` 237 | console.log(cmd) 238 | return execa.shell(cmd) 239 | } 240 | 241 | const installModules = (options = []) => ({found, missing}) => { 242 | la(is.object(found), 'expected found modules object', found) 243 | la(is.array(missing), 'expected list of missing names', missing) 244 | 245 | const list = R.values(found) 246 | 247 | return haveModules(list, options) 248 | .then(() => npmInstall(missing, options)) 249 | } 250 | 251 | function findAndInstall (names, options) { 252 | if (is.string(names)) { 253 | names = [names] 254 | } 255 | la(is.array(names), 'expected list of names to install', names) 256 | 257 | return findModules(names) 258 | .then(R.tap(print)) 259 | .then(installModules(options)) 260 | } 261 | 262 | module.exports = findAndInstall 263 | 264 | // findAndInstall(['lodash', 'debug']) 265 | // .then(console.log) 266 | -------------------------------------------------------------------------------- /src/utils-spec.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const la = require('lazy-ass') 4 | const is = require('check-more-types') 5 | const snapshot = require('snap-shot') 6 | 7 | /* global describe, it */ 8 | describe.only('findMissing', () => { 9 | const {findMissing} = require('./utils') 10 | 11 | it('is a function', () => { 12 | la(is.fn(findMissing)) 13 | }) 14 | 15 | it('finds missing deps', () => { 16 | const names = [{ 17 | name: 'foo' 18 | }, { 19 | name: 'bar' 20 | }] 21 | const found = ['foo'] 22 | const missing = findMissing(names, found) 23 | snapshot(missing) 24 | }) 25 | }) 26 | -------------------------------------------------------------------------------- /src/utils.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const mkdirp = require('mkdirp') 4 | const debug = require('debug')('have-it') 5 | const fs = require('fs') 6 | const path = require('path') 7 | const {concat, difference} = require('ramda') 8 | const la = require('lazy-ass') 9 | const is = require('check-more-types') 10 | const R = require('ramda') 11 | 12 | function mkdir (name) { 13 | return new Promise((resolve, reject) => { 14 | mkdirp(name, {}, (err) => { 15 | if (err) { 16 | console.error(err) 17 | return reject(err) 18 | } 19 | resolve() 20 | }) 21 | }) 22 | } 23 | 24 | function saveJSON (filename, json) { 25 | return new Promise((resolve, reject) => { 26 | const text = JSON.stringify(json, null, 2) + '\n\n' 27 | fs.writeFile(filename, text, 'utf8', (err) => { 28 | if (err) { 29 | return reject(err) 30 | } 31 | resolve() 32 | }) 33 | }) 34 | } 35 | 36 | function loadJSON (filename) { 37 | return new Promise((resolve, reject) => { 38 | fs.readFile(filename, 'utf8', (err, text) => { 39 | if (err) { 40 | return reject(err) 41 | } 42 | const json = JSON.parse(text) 43 | resolve(json) 44 | }) 45 | }) 46 | } 47 | 48 | function isProduction () { 49 | return process.env.NODE_ENV === 'production' 50 | } 51 | 52 | const packageFilename = path.join(process.cwd(), 'package.json') 53 | 54 | function withVersion (deps) { 55 | la(is.object(deps), 'expected dependencies object', deps) 56 | const at = ([name, version]) => `${name}@${version}` 57 | return R.toPairs(deps).map(at) 58 | } 59 | 60 | function toInstall () { 61 | return loadJSON(packageFilename).then(pkg => { 62 | const deps = withVersion(pkg.dependencies || {}) 63 | const devDeps = withVersion(pkg.devDependencies || {}) 64 | const selectedDeps = isProduction() ? deps : concat(deps, devDeps) 65 | debug('found deps to install in package.json') 66 | debug(selectedDeps) 67 | return selectedDeps 68 | }) 69 | } 70 | 71 | // returns just the list of missing objects 72 | function findMissing (names, found) { 73 | la(is.array(names), 'wrong names', names) 74 | la(is.strings(found), 'wrong installed', found) 75 | 76 | // each object in "names" is parsed object 77 | // {name, version} 78 | 79 | const missingNames = difference(R.pluck('name', names), found) 80 | return missingNames.map(name => R.find(R.propEq('name', name), names)) 81 | } 82 | 83 | function saveVersions (list, dev) { 84 | la(is.array(list), 'missing list to save') 85 | 86 | const key = dev ? 'devDependencies' : 'dependencies' 87 | return loadJSON(packageFilename).then(pkg => { 88 | const deps = pkg[key] || {} 89 | list.forEach(info => { 90 | deps[info.name] = info.version 91 | }) 92 | pkg[key] = deps 93 | return saveJSON(packageFilename, pkg) 94 | }) 95 | } 96 | 97 | module.exports = { 98 | mkdir, 99 | saveJSON, 100 | loadJSON, 101 | isProduction, 102 | toInstall, 103 | findMissing, 104 | saveVersions 105 | } 106 | -------------------------------------------------------------------------------- /test/e2e.sh: -------------------------------------------------------------------------------- 1 | set e+x 2 | 3 | echo "Linking current have-it" 4 | npm link 5 | 6 | testFolder=`dirname $0` 7 | echo "Test folder $testFolder" 8 | sourceFolder=$PWD 9 | echo "Current folder $sourceFolder" 10 | 11 | HAVE=$HOME/git 12 | echo "All existing projects live under $HAVE" 13 | 14 | echo "Creating test folder" 15 | folder=/tmp/test-have-it 16 | rm -rf $folder 17 | mkdir $folder 18 | echo "Created test folder $folder" 19 | cp $testFolder/index.js $folder 20 | 21 | cd $folder 22 | echo "Changed working dir to $folder" 23 | 24 | echo "Creating new package" 25 | npm init --yes 26 | 27 | echo "Installing lodash using have-it" 28 | DEBUG=have-it HAVE=$HAVE have lodash --save 29 | 30 | echo "Package file after installing" 31 | cat package.json 32 | 33 | echo "Top level packages" 34 | npm ls --depth=0 35 | 36 | echo "Trying to use the program" 37 | node index.js 38 | 39 | echo "Uninstalling lodash" 40 | npm uninstall --save lodash 41 | 42 | echo "Installing lodash again - NPM install fallback" 43 | DEBUG=have-it have lodash --save 44 | echo "Trying to use the program (expect latest version)" 45 | node index.js 46 | 47 | echo "Installing specific version of lodash" 48 | DEBUG=have-it HAVE=$HAVE have lodash@3.10.0 --save 49 | echo "Trying to use the program (expect lodash 3.10.0)" 50 | node index.js 51 | 52 | echo "Installing specific version from package.json" 53 | rm -rf node_modules 54 | DEBUG=have-it HAVE=$HAVE have 55 | echo "Trying to use the program (expect lodash 3.10.0)" 56 | node index.js 57 | 58 | echo "All done testing have-it in $folder" 59 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | const _ = require('lodash') 2 | console.log('_.VERSION', _.VERSION) 3 | --------------------------------------------------------------------------------