├── .gitignore ├── bin ├── uninstall.js ├── shrinkwrap.js ├── install.js └── hook.js ├── lib ├── npm-install.js ├── get-current-pkg.js ├── get-previous-pkg.js ├── thenable.js ├── exec.js └── diff-deps.js ├── LICENSE ├── package.json └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | npm-debug.log 3 | -------------------------------------------------------------------------------- /bin/uninstall.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | var run = require("./install"); 3 | 4 | // hackish, just a quick bug fix for now 5 | process.argv.push("--uninstall"); 6 | 7 | run(); 8 | -------------------------------------------------------------------------------- /lib/npm-install.js: -------------------------------------------------------------------------------- 1 | var exec = require("./exec"); 2 | 3 | module.exports = runInstall; 4 | 5 | function runInstall(args) { 6 | console.log("Updating dependencies:\n"); 7 | console.log("npm " + args.join(" ")); 8 | return exec("npm", args)(); 9 | } 10 | -------------------------------------------------------------------------------- /lib/get-current-pkg.js: -------------------------------------------------------------------------------- 1 | var fs = require("fs"); 2 | 3 | module.exports = getCurrent; 4 | 5 | function getCurrent() { 6 | return new Promise(function(resolve, reject) { 7 | fs.readFile("package.json", {encoding: "utf8"}, function(err, data) { 8 | if (err) return reject(err); 9 | 10 | resolve(data); 11 | }) 12 | }); 13 | } 14 | 15 | -------------------------------------------------------------------------------- /lib/get-previous-pkg.js: -------------------------------------------------------------------------------- 1 | var exec = require("./exec"); 2 | 3 | module.exports = getPrevious; 4 | 5 | function getPrevious(packageFileChanged) { 6 | // get last package.json in the commit tree 7 | var treeish = (packageFileChanged[0] | 0) ? "HEAD" : "HEAD^"; 8 | return exec.raw("sh", ["-c", "git archive " + treeish + " package.json | tar xf - -O"], {isSilent: true})(); 9 | } 10 | -------------------------------------------------------------------------------- /lib/thenable.js: -------------------------------------------------------------------------------- 1 | module.exports = thenable; 2 | /** 3 | * @public 4 | * 5 | * haskell-ish currying 6 | **/ 7 | function thenable(fn, arr) { 8 | arr = arr || []; 9 | if (arr.length < fn.length) { 10 | return function() { 11 | var args = Array(arguments.length); 12 | for (var i = 0; i < args.length; i++) { 13 | args[i] = arguments[i]; 14 | } 15 | return thenable(fn, arr.concat(args)); 16 | }; 17 | } 18 | 19 | // create an extra function wrapper for .then() call 20 | return fn.apply.bind(fn, null, arr); 21 | } 22 | -------------------------------------------------------------------------------- /bin/shrinkwrap.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | var cmd = require("@th507/npm-shrinkwrap/bin/cli.js"); 4 | var parseArgs = require("minimist"); 5 | 6 | var exec = require("../lib/exec"); 7 | 8 | module.exports = cmd; 9 | 10 | if (require.main === module) { 11 | var args = parseArgs(process.argv.slice(2)); 12 | 13 | Promise.resolve() 14 | .then(exec("npm", ["prefix"])) 15 | .then(function(baseDir) { 16 | // find root directory for package.json 17 | args.dirname = args.dirname || baseDir; 18 | 19 | return args; 20 | }) 21 | .then(cmd); 22 | } 23 | -------------------------------------------------------------------------------- /lib/exec.js: -------------------------------------------------------------------------------- 1 | var child = require("child_process"); 2 | var thenable = require("./thenable"); 3 | 4 | module.exports = thenable(function(cmd, args) { return exec(cmd, args)}); 5 | module.exports.raw = thenable(exec); 6 | 7 | function exec(cmd, args, opts) { 8 | opts = opts || {}; 9 | var isSilent = opts.isSilent; 10 | return new Promise(function(resolve, reject) { 11 | var _child = child.spawn(cmd, args, opts); 12 | 13 | var out = ""; 14 | _child.stdout.on("data", function(data) { 15 | out += data; 16 | if (!isSilent) process.stdout.write(data); 17 | }); 18 | 19 | var err = ""; 20 | 21 | _child.stderr.on("data", function(data) { 22 | err += data; 23 | process.stderr.write(data); 24 | }); 25 | 26 | _child.on("close", function(code) { 27 | if (err) return reject(err); 28 | 29 | resolve(out.trim("\n")); 30 | }); 31 | }); 32 | } 33 | 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Jingwei Liu 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "npm-shrinkwrap-install", 3 | "version": "0.0.3", 4 | "description": "Automatic synchronise shrinkwrap file with package install/update/uninstall", 5 | "main": "index.js", 6 | "keywords": [ 7 | "npm-shrinkwrap", 8 | "shrinkwrap", 9 | "install", 10 | "dependencies", 11 | "dependency" 12 | ], 13 | "scripts": { 14 | "test": "echo \"Error: no test specified\" && exit 1" 15 | }, 16 | "bin": { 17 | "npm-shrinkwrap": "./bin/shrinkwrap.js", 18 | "npm-install": "./bin/install.js", 19 | "npm-i": "./bin/install.js", 20 | "npm-uninstall": "./bin/uninstall.js", 21 | "npm-un": "./bin/uninstall.js", 22 | "npm-remove": "./bin/uninstall.js", 23 | "npm-rm": "./bin/uninstall.js", 24 | "npm-r": "./bin/uninstall.js" 25 | }, 26 | "engine": { 27 | "node": ">= 0.11.x", 28 | "io": ">=1.0" 29 | }, 30 | "repository": { 31 | "type": "git", 32 | "url": "https://github.com/th507/npm-shrinkwrap-install" 33 | }, 34 | "homepage": "https://github.com/th507/npm-shrinkwrap-install", 35 | "author": { 36 | "name": "Jingwei \"John\" Liu", 37 | "email": "liujingwei@gmail.com" 38 | }, 39 | "license": "MIT", 40 | "dependencies": { 41 | "@th507/npm-shrinkwrap": "^5.4.0", 42 | "minimist": "^1.1.3" 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /lib/diff-deps.js: -------------------------------------------------------------------------------- 1 | module.exports = diff; 2 | 3 | function diff(previous, current) { 4 | var currentKeys = Object.keys(current); 5 | var previousKeys = Object.keys(previous); 6 | 7 | // recognize dependencies deletion/addition 8 | var dependenciesChanged = currentKeys.filter(function(k) { 9 | var bool = previousKeys.indexOf(k) === -1; 10 | 11 | if (bool) { 12 | console.log("Detected new dependency:", k, "N/A", " ==> ", current[k]); 13 | } 14 | 15 | return bool; 16 | }); 17 | 18 | currentKeys.forEach(function(k) { 19 | // version unchanged 20 | if (current[k] === previous[k]) return; 21 | 22 | if (dependenciesChanged.indexOf(k) !== -1) return; 23 | console.log("Detected version change:", k, previous[k], " ==> ", current[k]); 24 | dependenciesChanged.push(k); 25 | }); 26 | 27 | 28 | var args = process.argv.slice(2); 29 | 30 | var hasNewPkg = args.some(function(a) { 31 | return a[0] !== "-"; 32 | }); 33 | 34 | var hasNewPkg = false; 35 | var isUninstall = false; 36 | 37 | args = args.filter(function(a) { 38 | if (a[0] !== "-") hasNewPkg = true; 39 | 40 | if (a === "--uninstall") { 41 | isUninstall = true; 42 | return false; 43 | } 44 | 45 | return true; 46 | }); 47 | 48 | if (!dependenciesChanged.length && !hasNewPkg && !isUninstall) { 49 | // XXX: compare package.json with npm-shrinkwrap.json 50 | console.log("Dependencies unchanged"); 51 | return process.exit(); 52 | } 53 | 54 | if (isUninstall) { 55 | return ["uninstall"].concat(args); 56 | } 57 | 58 | 59 | return ["install"].concat(dependenciesChanged, args); 60 | } 61 | -------------------------------------------------------------------------------- /bin/install.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | /** 3 | * re-create shrinkwrap json if package.json dependencies have changed 4 | * requires Node >= 0.11 5 | * 6 | * @author Jingwei "John" Liu 7 | **/ 8 | 9 | var exec = require("../lib/exec"); 10 | var runInstall = require("../lib/npm-install"); 11 | var diffDeps = require("../lib/diff-deps"); 12 | var getPrevious = require("../lib/get-previous-pkg"); 13 | var getCurrent = require("../lib/get-current-pkg"); 14 | 15 | module.exports = run; 16 | function run() { 17 | Promise.resolve() 18 | // get npm/project root dir 19 | .then(exec.raw("npm", ["prefix"], {isSilent: true})) 20 | // chdir to npm/project root dir 21 | .then(process.chdir) 22 | // check if package.json has uncommitted change 23 | .then(exec.raw("sh", ["-c", "git status --porcelain -uno | grep package.json | wc -l | awk '{print $1}'"], {isSilent: true})) 24 | // read current and previous pkg file 25 | .then(function(packageFileChanged) { 26 | return Promise.all([getPrevious(packageFileChanged), getCurrent()]); 27 | }) 28 | // diff dependencies 29 | .then(function(arr) { 30 | return diffDeps.apply(null, arr.map(function(a) { 31 | var pkg = a ? JSON.parse(a) : {}; 32 | return pkg && pkg.dependencies; 33 | })); 34 | }) 35 | // install new package/version 36 | .then(runInstall) 37 | // maintenance: prune extraneous packages 38 | .then(exec("npm", ["prune"])) 39 | // update shrinkwrap file 40 | .then(exec(__dirname + "/shrinkwrap.js", [])) 41 | // log finish/error 42 | .then(function() { 43 | console.log("Updated shrinkwrap file"); 44 | }, function(e) { 45 | console.error(e.stack || e.toString()); 46 | }) 47 | } 48 | 49 | if (require.main === module) { 50 | run(); 51 | } 52 | -------------------------------------------------------------------------------- /bin/hook.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | /** 3 | * npm-shrinkwrap-install hook 4 | * adding hook for post-install and post-uninstall 5 | * 6 | * largely based on npm package: uber/npm-shrinkwrap/bin/install.js 7 | **/ 8 | var path = require('path'); 9 | var fs = require('fs'); 10 | var parseArgs = require("minimist"); 11 | 12 | var exec = require("../lib/exec"); 13 | 14 | // npm-shrinkwrap-install version 15 | var tool = require('../package.json'); 16 | 17 | module.exports = installHook; 18 | 19 | if (require.main === module) { 20 | var args = parseArgs(process.argv.slice(2)); 21 | 22 | Promise.resolve() 23 | .then(exec("npm", ["prefix"])) 24 | .then(function(baseDir) { 25 | // find root directory for package.json 26 | args.dirname = args.dirname || baseDir; 27 | 28 | return args; 29 | }) 30 | .then(installHook); 31 | } 32 | 33 | function installHook(opts, callback) { 34 | callback = callback || function noop() {}; 35 | 36 | var file = path.join(opts.dirname, 'package.json'); 37 | 38 | var pkg; 39 | try { 40 | pkg = JSON.parse(fs.readFileSync(file, "utf8")); 41 | } catch (e) { 42 | console.error("error reading package.json"); 43 | 44 | return callback(e); 45 | } 46 | 47 | pkg.scripts = pkg.scripts || {}; 48 | 49 | pkg.scripts.shrinkwrap = "npm-shrinkwrap"; 50 | pkg.scripts["install"] = "npm-install"; 51 | pkg.scripts["uninstall"] = "npm-install --uninstall"; 52 | 53 | // write to devDependencies 54 | opts.packageVersion = opts.packageVersion || '~' + tool.version; 55 | opts.moduleName = opts.moduleName || tool.name; 56 | if (!opts.onlyScripts) { 57 | pkg.devDependencies = pkg.devDependencies || {}; 58 | pkg.devDependencies[opts.moduleName] = opts.packageVersion; 59 | } 60 | else { 61 | console.log("skipping devDependencies"); 62 | } 63 | 64 | fs.writeFile(file, JSON.stringify(pkg, null, 2) + '\n', 65 | 'utf8', callback); 66 | 67 | console.log("wrote to package.json"); 68 | } 69 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # npm-shrinkwrap-install 2 | 3 | Automatic synchronise shrinkwrap file with package install/update/uninstall. 4 | 5 | 6 | # Install 7 | ```bash 8 | $ npm install npm-shrinkwrap-install -g 9 | ``` 10 | 11 | This is a CLI-only package (for now). 12 | 13 | Require Node.js >= 0.11, or iojs >= 1.0 14 | # Usage 15 | ## Install/Update a package 16 | 17 | ```bash 18 | $ npm-install babel@latest --save 19 | ``` 20 | 21 | You could manually alter the version in package.json and run `npm-install` as well. 22 | 23 | 24 | **Notice:** 25 | 26 | Before updating shrinkwrap, the script will try to verify node_modules content with your package.json. Which means: 27 | - Any package not included in `dependencies` will be pruned. So it is mandatory to use `--save` when installing new packages. 28 | - Any package with non-strict version description (^, ~, 0.2.x, \*, latest) in package.json will be updated. If you which to opt out this automatical update check, just use a strict version like 0.2.3. 29 | 30 | ## Uninstall a package 31 | 32 | ```bash 33 | $ npm-uninstall babel --save 34 | ``` 35 | 36 | Shrinkwrap file will be automatically in sync with your dependencies in package.json. 37 | 38 | **Notice:** 39 | 40 | If `--save` is omitted, the script will generate an error since node_modules content does not agree with your package.json. 41 | 42 | ## Create shrinkwrap file manually 43 | 44 | ```bash 45 | $ npm-shrinkwrap 46 | ``` 47 | 48 | # Commands and Aliases 49 | 50 | ## Install 51 | `npm-install` 52 | `npm-i` 53 | 54 | Accepts all arguments for `npm install` 55 | 56 | ## Uninstall 57 | `npm-uninstall` 58 | `npm-un` 59 | `npm-remove` 60 | `npm-rm` 61 | `npm-r` 62 | 63 | Accepts all arguments for `npm uninstall` 64 | 65 | ## Shrinkwrap 66 | `npm-shrinkwrap` 67 | 68 | Accepts all arguments for Uber's [npm-shrinkwrap](http://github.com/uber/npm-shrinkwrap) 69 | 70 | # Credit 71 | 72 | This script is largely based on Uber's [npm-shrinkwrap](http://github.com/uber/npm-shrinkwrap). At the time of the writing, their tool does not support [scoped package](https://docs.npmjs.com/misc/scope). A [pull-request](https://github.com/uber/npm-shrinkwrap/pull/80) is created to rectify this issue and yet to be accepted. 73 | 74 | As a temporary measure, I've forked their project and applied my patch to support scoped package: [@th507/npm-shrinkwrap](https://www.npmjs.com/package/@th507/npm-shrinkwrap). `npm-shrinkwrap-install` is currently dependent upon @th507/npm-shrinkwrap. 75 | 76 | # License 77 | 78 | Copyright (c) 2015 Jingwei "John" Liu 79 | 80 | Licensed under the MIT license. 81 | --------------------------------------------------------------------------------