├── src ├── install.js ├── config.js ├── init.js ├── check.js ├── save.js ├── restore.js ├── helpers.js └── index.js ├── .babelrc ├── .gitignore ├── package.json ├── LICENSE └── README.md /src/install.js: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["nodejs-lts"] 3 | } 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | /node_modules/ 3 | 4 | /lib/ 5 | -------------------------------------------------------------------------------- /src/config.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | 3 | module.exports = { 4 | cacheDir: path.join(process.env.HOME, ".node_modules_cache") 5 | }; 6 | -------------------------------------------------------------------------------- /src/init.js: -------------------------------------------------------------------------------- 1 | const co = require("co"); 2 | 3 | const { cacheDir } = require("./config"); 4 | const { exists, mkdir } = require("./helpers"); 5 | 6 | 7 | module.exports = co.wrap(function*() { 8 | const cacheDirExists = yield exists(cacheDir); 9 | 10 | if (!cacheDirExists) { 11 | yield mkdir(cacheDir); 12 | } 13 | }); 14 | -------------------------------------------------------------------------------- /src/check.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | 3 | const co = require("co"); 4 | 5 | const { cacheDir } = require("./config"); 6 | const { exists, mkdir, exec } = require("./helpers"); 7 | 8 | 9 | module.exports = co.wrap(function*(hash) { 10 | const cachePath = path.join(cacheDir, hash); 11 | const cachePathExists = yield exists(cachePath); 12 | 13 | if (!cachePathExists) { 14 | throw new Error("Current node_modules is not cached."); 15 | } 16 | }); 17 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nm-cache", 3 | "version": "1.2.0", 4 | "description": "", 5 | "main": "lib/index.js", 6 | "bin": { 7 | "nm-cache": "lib/index.js" 8 | }, 9 | "files": [ 10 | "lib" 11 | ], 12 | "scripts": { 13 | "build": "babel -d lib/ src/ && chmod u+x lib/index.js", 14 | "prepublish": "npm run build" 15 | }, 16 | "author": "Dale Bustad ", 17 | "license": "MIT", 18 | "dependencies": { 19 | "co": "^4.6.0", 20 | "yargs": "^4.8.1" 21 | }, 22 | "devDependencies": { 23 | "babel-cli": "^6.11.4", 24 | "babel-preset-nodejs-lts": "^2.0.1" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/save.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | 3 | const co = require("co"); 4 | 5 | const { cacheDir } = require("./config"); 6 | const { exists, mkdir, exec } = require("./helpers"); 7 | 8 | 9 | module.exports = co.wrap(function*(basePath, hash, force) { 10 | const cachePath = path.join(cacheDir, hash); 11 | const cachePathExists = yield exists(cachePath); 12 | 13 | if (cachePathExists) { 14 | if (!force) { 15 | throw new Error("Cache conflict. Use --force to override."); 16 | } else { 17 | console.log("Removing existing cache..."); 18 | yield exec(`rm -fr "${cachePath}"`); 19 | } 20 | } 21 | yield mkdir(cachePath); 22 | 23 | const nmPath = path.join(basePath, "node_modules"); 24 | 25 | console.log(`Caching node_modules as ${hash}...`); 26 | yield exec(`pax -rwlpe ./* "${cachePath}/"`, nmPath); 27 | console.log("Done!"); 28 | }); 29 | -------------------------------------------------------------------------------- /src/restore.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | 3 | const co = require("co"); 4 | 5 | const { cacheDir } = require("./config"); 6 | const { exists, mkdir, exec } = require("./helpers"); 7 | 8 | 9 | module.exports = co.wrap(function*(basePath, hash) { 10 | const cachePath = path.join(cacheDir, hash); 11 | const cachePathExists = yield exists(cachePath); 12 | 13 | if (!cachePathExists) { 14 | throw new Error(`Non-existent cache for: ${hash}`); 15 | } 16 | 17 | const nmPath = path.join(basePath, "node_modules"); 18 | const nmPathExists = yield exists(nmPath); 19 | 20 | if (nmPathExists) { 21 | console.log("Removing existing node_modules..."); 22 | yield exec(`rm -fr "${nmPath}"`); 23 | } 24 | yield mkdir(nmPath); 25 | 26 | console.log(`Restoring node_modules from ${hash}...`); 27 | yield exec(`pax -rwlpe ./* "${nmPath}/"`, cachePath); 28 | console.log("Done!"); 29 | }); 30 | -------------------------------------------------------------------------------- /src/helpers.js: -------------------------------------------------------------------------------- 1 | const cp = require("child_process"); 2 | const fs = require("fs"); 3 | 4 | 5 | const fromCallback = fn => new Promise((resolve, reject) => { 6 | fn((error, result) => error 7 | ? reject(error) 8 | : resolve(result) 9 | ); 10 | }); 11 | 12 | const exists = fpath => fromCallback(cb => fs.access(fpath, cb)) 13 | .then(() => true) 14 | .catch(() => false); 15 | 16 | const mkdir = fpath => fromCallback(cb => fs.mkdir(fpath, cb)); 17 | 18 | const exec = (command, cwd) => new Promise((resolve, reject) => { 19 | const opts = cwd ? 20 | { cwd } : 21 | {}; 22 | 23 | cp.exec(command, opts, (err, stdout, stderr) => { 24 | return err ? 25 | reject(new Error(`${err} - ${stdout}`)) : 26 | resolve({ stdout, stderr }); 27 | }); 28 | }); 29 | 30 | const readFile = fpath => fromCallback(cb => fs.readfile(fpath, cb)); 31 | 32 | module.exports = { fromCallback, exists, mkdir, exec, readFile }; 33 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Dale Bustad 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 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const fs = require("fs"); 4 | const path = require("path"); 5 | 6 | const co = require("co"); 7 | const yargs = require("yargs"); 8 | 9 | const { exists, exec } = require("./helpers"); 10 | const init = require("./init"); 11 | const save = require("./save"); 12 | const check = require("./check"); 13 | const restore = require("./restore"); 14 | const install = require("./install"); 15 | 16 | 17 | const getPackageJson = fpath => fpath ? 18 | path.resolve(process.cwd(), fpath) : 19 | path.join(process.cwd(), "package.json"); 20 | 21 | const getHash = co.wrap(function* (fpath) { 22 | if (!(yield exists(fpath))) { 23 | throw new Error("The specified package.json file does not exist:", fpath); 24 | } 25 | 26 | const { stdout } = yield exec(`shasum -a 256 "${fpath}"`); 27 | return stdout.split(" ")[0]; 28 | }); 29 | 30 | const wrap = genFn => co(genFn).catch(err => { 31 | console.error(`\x1b[31mERROR: ${err}\x1b[0m\n`); 32 | process.exit(1); 33 | }); 34 | 35 | 36 | yargs 37 | .strict() 38 | .usage(`Usage: nm-cache [options] 39 | 40 | This utility helps you stash particular versions of your node_modules 41 | directories with minimal overhead. Later you can restore them. 42 | 43 | This can be useful when, for example, you switch between branches that have 44 | different dependencies or versions, or if you need to switch between Node 45 | run-time versions.`) 46 | .option("package-json", { 47 | describe: "`package.json` to use as node_modules anchor point (optional)." 48 | }) 49 | .option("force", { 50 | describe: "Overwrite pre-existing node_modules cache (optional)." 51 | }) 52 | .option("hash", { 53 | describe: "Indicates which cached directory to restore (optional)." 54 | }) 55 | .command("save", "Save a snapshot of your current node_modules.", ({ argv: { packageJson, force } }) => { 56 | wrap(function* () { 57 | yield init(); 58 | packageJson = getPackageJson(packageJson); 59 | const hash = yield getHash(packageJson); 60 | yield save(path.dirname(packageJson), hash, force); 61 | }); 62 | }) 63 | .command("restore", "Restore a saved snapshot, anchored to package.json", ({ argv: { packageJson } }) => { 64 | wrap(function* () { 65 | yield init(); 66 | packageJson = getPackageJson(packageJson); 67 | const hash = yield getHash(packageJson); 68 | yield restore(path.dirname(packageJson), hash); 69 | }); 70 | }) 71 | .command("check", "Exit with 0 if already cached, exit with 1 otherwise.", ({ argv: { packageJson } }) => { 72 | wrap(function* () { 73 | yield init(); 74 | packageJson = getPackageJson(packageJson); 75 | const hash = yield getHash(packageJson); 76 | yield check(hash); 77 | }); 78 | }) 79 | .demand(1, "You must provide a sub-command.") 80 | .epilogue("For more info or to report an issue, visit the following URL:\nhttp://github.com/divmain/nm-cache") 81 | .help("help") 82 | .parse(process.argv); 83 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # nm-cache 2 | 3 | Have you ever been in the situation where someone asks you to `git checkout` their branch, and you hesitate? 4 | 5 | If you have, and if it is because you're not interested in helping others, this package won't be of use to you. You're probably a jerk, and that's not a software problem. 6 | 7 | However, if you hesitate because your project's dependencies change often, and `npm install` takes forever, `nm-cache` might be able to help! 8 | 9 | The project may also be of use if you need to test a project between multiple versions of Node. If you use something like [nodenv](https://github.com/nodenv/nodenv) or [nvm](https://github.com/creationix/nvm), you'll need to globally install `nm-cache` for each Node version, but there should be no other constraints. 10 | 11 | 12 | ## Use 13 | 14 | - `npm install -g nm-cache` 15 | - Enjoy! 16 | 17 | **Note:** `nm-cache` requires a POSIX-compatible environment to work. OSX and most Linux flavors should work out-of-the-box. Windows 10 with its new Ubuntu mode might work too, although it hasn't been tested. 18 | 19 | 20 | ## Common workflow 21 | 22 | Let's say you want to checkout someone else's branch, but you don't want to wait forever. Here's what you would do: 23 | 24 | ```bash 25 | nm-cache save 26 | git checkout other-branch 27 | nm-cache restore || (npm install && nm-cache save) 28 | 29 | # Do all the things... 30 | 31 | git checkout original-branch 32 | nm-cache restore 33 | ``` 34 | 35 | Or maybe your `project.json` changes relatively often, but you don't want to check it manually every time you pull down the latest `master`: 36 | 37 | ```bash 38 | git pull origin master 39 | git diff-tree -r --name-only --no-commit-id HEAD@{1} HEAD | \ 40 | grep --quiet "package.json" && \ 41 | nm-cache restore || \ 42 | npm install && \ 43 | nm-cache save 44 | ``` 45 | 46 | If you want this to be automated, make it into a `post-merge` [git hook](https://git-scm.com/book/en/v2/Customizing-Git-Git-Hooks#Client-Side-Hooks). 47 | 48 | 49 | ## Options 50 | 51 | ``` 52 | Usage: nm-cache [options] 53 | 54 | This utility helps you stash particular versions of your node_modules 55 | directories with minimal overhead. Later you can restore them. 56 | 57 | This can be useful when, for example, you switch between branches that have 58 | different dependencies or versions, or if you need to switch between Node 59 | run-time versions. 60 | 61 | Commands: 62 | save Save a snapshot of your current node_modules. 63 | restore Restore a saved snapshot, anchored to package.json 64 | check Exit with 0 if already cached, exit with 1 otherwise. 65 | 66 | Options: 67 | --package-json `package.json` to use as node_modules anchor point (optional). 68 | --force Overwrite pre-existing node_modules cache (optional). 69 | --hash Indicates which cached directory to restore (optional). 70 | --help Show help [boolean] 71 | ``` 72 | 73 | 74 | ## Under the Hood 75 | 76 | You may notice that `nm-cache` is relatively quick about its business. This is because it does _not_ make copies of your `node_modules` subdirectories. Instead, it creates a directory structure that matches your existing `node_modules` and hard-links the files. 77 | 78 | One side benefit of this approach is that there will only ever be one physical copy of each snapshot stored on disk, including the `node_modules` directory itself. Of course, multiple snapshots with partially-identical contents will not be de-duplicated, as that would require some tight integration with `npm` or a heavy hash-every-file approach like `git`. 79 | 80 | You may also notice that `nm-cache` doesn't require that you specify any unique identifier for your snapshot. This is because, by default, it will create a hash of your `package.json` file and use this to uniquely identify the snapshot when they're saved. 81 | 82 | Similarly, when you're restoring, it'll hash your `package.json` and look to see whether a snapshot has already been made. If so, you won't have to `npm install` again! 83 | --------------------------------------------------------------------------------