├── .jshintignore ├── test ├── index.js ├── git-https-use-case.js └── npm-shrinkwrap.js ├── bin ├── formatters.js ├── help.js ├── install.js ├── usage.md ├── cli.js └── diff.js ├── walk-shrinkwrap.js ├── read-json.js ├── sync ├── install-module.js ├── read.js ├── index.js ├── purge-excess.js └── force-install.js ├── .jshintrc ├── LICENSE ├── .gitignore ├── docs.mli ├── trim-nested.js ├── verify-git.js ├── package.json ├── docs └── faq.md ├── errors.js ├── analyze-dependency.js ├── set-resolved.js ├── README.md ├── trim-and-sort-shrinkwrap.js └── index.js /.jshintignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | coverage/ 3 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | require('./npm-shrinkwrap.js'); 2 | require('./git-https-use-case.js'); 3 | -------------------------------------------------------------------------------- /bin/formatters.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | 'gitlink.tag.notsemver': notSemver, 3 | 'invalid.git.version': invalidVersion, 4 | 'default': printError 5 | }; 6 | 7 | function notSemver(err) { 8 | return 'WARN: ' + err.message; 9 | } 10 | 11 | function invalidVersion(err) { 12 | return 'ERROR: ' + err.message; 13 | } 14 | 15 | function printError(err) { 16 | return err.message; 17 | } 18 | -------------------------------------------------------------------------------- /walk-shrinkwrap.js: -------------------------------------------------------------------------------- 1 | module.exports = walkDeps; 2 | 3 | function walkDeps(package, fn, key, parent) { 4 | package._name = key || package.name; 5 | package._parent = parent || null; 6 | fn(package, package._name, package._parent); 7 | 8 | Object.keys(package.dependencies || {}) 9 | .forEach(function (key) { 10 | walkDeps(package.dependencies[key], 11 | fn, key, package); 12 | }); 13 | } 14 | -------------------------------------------------------------------------------- /read-json.js: -------------------------------------------------------------------------------- 1 | // From https://github.com/azer/read-json 2 | // Licensed under the BSD license 3 | // Adapted to use graceful-fs 4 | 5 | var fs = require("graceful-fs"); 6 | 7 | module.exports = readJSON; 8 | 9 | function readJSON(filename, options, callback){ 10 | if(callback === undefined){ 11 | callback = options; 12 | options = {}; 13 | } 14 | 15 | fs.readFile(filename, options, function(error, bf){ 16 | if(error) return callback(error); 17 | 18 | try { 19 | bf = JSON.parse(bf.toString().replace(/^\ufeff/g, '')); 20 | } catch (err) { 21 | callback(err); 22 | return; 23 | } 24 | 25 | callback(undefined, bf); 26 | }); 27 | } -------------------------------------------------------------------------------- /bin/help.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | var fs = require('graceful-fs'); 3 | var msee = require('msee'); 4 | var template = require('string-template'); 5 | 6 | function printHelp(opts) { 7 | opts = opts || {}; 8 | 9 | var loc = path.join(__dirname, 'usage.md'); 10 | var content = fs.readFileSync(loc, 'utf8'); 11 | 12 | content = template(content, { 13 | cmd: opts.cmd || 'npm-shrinkwrap' 14 | }); 15 | 16 | if (opts.h) { 17 | content = content.split('##')[0]; 18 | } 19 | 20 | var text = msee.parse(content, { 21 | paragraphStart: '\n' 22 | }); 23 | 24 | return console.log(text); 25 | } 26 | 27 | module.exports = printHelp; 28 | -------------------------------------------------------------------------------- /sync/install-module.js: -------------------------------------------------------------------------------- 1 | var exec = require('child_process').exec; 2 | 3 | var path = require('path'); 4 | 5 | module.exports = installModule; 6 | 7 | /* given a location of node_modules it will try and install the 8 | named dep into that location. 9 | 10 | Assumes npm.load() was called 11 | 12 | */ 13 | function installModule(nodeModules, dep, opts, cb) { 14 | var where = path.join(nodeModules, '..'); 15 | 16 | console.log('installing ', where, dep.resolved); 17 | var cmd = 'npm install ' + dep.resolved; 18 | 19 | if (opts.registry) { 20 | cmd += ' --registry=' + opts.registry; 21 | } 22 | 23 | exec(cmd, { 24 | cwd: where 25 | }, cb); 26 | } 27 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "bitwise": false, 3 | "camelcase": true, 4 | "curly": false, 5 | "eqeqeq": true, 6 | "forin": true, 7 | "immed": true, 8 | "indent": 4, 9 | "latedef": false, 10 | "newcap": false, 11 | "noarg": true, 12 | "nonew": true, 13 | "plusplus": false, 14 | "quotmark": false, 15 | "regexp": false, 16 | "undef": true, 17 | "unused": true, 18 | "strict": false, 19 | "trailing": true, 20 | "noempty": true, 21 | "maxdepth": 4, 22 | "maxparams": 4, 23 | "globals": { 24 | "console": true, 25 | "Buffer": true, 26 | "setTimeout": true, 27 | "clearTimeout": true, 28 | "setInterval": true, 29 | "clearInterval": true, 30 | "require": false, 31 | "module": false, 32 | "exports": true, 33 | "global": false, 34 | "process": true, 35 | "__dirname": false, 36 | "__filename": false 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Uber 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. -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled source # 2 | ################### 3 | *.com 4 | *.class 5 | *.dll 6 | *.exe 7 | *.a 8 | *.o 9 | *.so 10 | *.node 11 | 12 | # Node Waf Byproducts # 13 | ####################### 14 | .lock-wscript 15 | build/ 16 | autom4te.cache/ 17 | 18 | # Node Modules # 19 | ################ 20 | # Better to let npm install these from the package.json defintion 21 | # rather than maintain this manually 22 | node_modules/ 23 | 24 | # Packages # 25 | ############ 26 | # it's better to unpack these files and commit the raw source 27 | # git has its own built in compression methods 28 | *.7z 29 | *.dmg 30 | *.gz 31 | *.iso 32 | *.jar 33 | *.rar 34 | *.tar 35 | *.zip 36 | 37 | # Logs and databases # 38 | ###################### 39 | *.log 40 | dump.rdb 41 | *.js.tap 42 | *.coffee.tap 43 | 44 | # OS generated files # 45 | ###################### 46 | .DS_Store? 47 | .DS_Store 48 | ehthumbs.db 49 | Icon? 50 | Thumbs.db 51 | coverage 52 | 53 | # Text Editor Byproducts # 54 | ########################## 55 | *.swp 56 | *.swo 57 | .idea/ 58 | 59 | *.pyc 60 | 61 | # All translation files # 62 | ######################### 63 | static/translations-s3/ 64 | -------------------------------------------------------------------------------- /bin/install.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | var readJSON = require('../read-json'); 3 | var fs = require('graceful-fs'); 4 | var template = require('string-template'); 5 | 6 | var version = require('../package.json').version; 7 | 8 | var shrinkwrapCommand = '{cmd}'; 9 | 10 | module.exports = installModule; 11 | 12 | function installModule(opts, callback) { 13 | var file = path.join(opts.dirname, 'package.json'); 14 | 15 | opts.packageVersion = opts.packageVersion || '^' + version; 16 | opts.moduleName = opts.moduleName || 'npm-shrinkwrap'; 17 | 18 | readJSON(file, function (err, package) { 19 | if (err) { 20 | return callback(err); 21 | } 22 | 23 | package.scripts = package.scripts || {}; 24 | 25 | package.scripts.shrinkwrap = 26 | template(shrinkwrapCommand, opts); 27 | 28 | if (!opts.onlyScripts) { 29 | package.devDependencies = 30 | package.devDependencies || {}; 31 | package.devDependencies[opts.moduleName] = 32 | opts.packageVersion; 33 | } 34 | 35 | fs.writeFile(file, JSON.stringify(package, null, 2) + '\n', 36 | 'utf8', callback); 37 | }); 38 | } 39 | -------------------------------------------------------------------------------- /sync/read.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | var readJSON = require('../read-json'); 3 | var TypedError = require('error/typed'); 4 | 5 | var FileNotFound = TypedError({ 6 | type: 'file.not.found', 7 | message: 'Expected the npm-shrinkwrap.json file to exist exist\n' + 8 | 'filePath {filePath}.\n' + 9 | 'SUGGESTED FIX: run `npm run shrinkwrap` or `npm-shrinkwrap` to generate one.\n' 10 | }); 11 | 12 | module.exports = { 13 | shrinkwrap: readShrinkwrap, 14 | package: readPackage, 15 | devDependencies: readDevDependencies 16 | }; 17 | 18 | function readPackage(dirname, cb) { 19 | var filePath = path.join(dirname, 'package.json'); 20 | readJSON(filePath, cb); 21 | } 22 | 23 | function readShrinkwrap(dirname, cb) { 24 | var filePath = path.join(dirname, 'npm-shrinkwrap.json'); 25 | readJSON(filePath, function(err, json) { 26 | if (err && err.code === 'ENOENT') { 27 | return cb(FileNotFound({ 28 | filePath: filePath 29 | })); 30 | } else { 31 | cb(null, json); 32 | } 33 | }); 34 | } 35 | 36 | function readDevDependencies(dirname, cb) { 37 | readPackage(dirname, function (err, json) { 38 | if (err) { 39 | return cb(err); 40 | } 41 | 42 | cb(null, json.devDependencies); 43 | }); 44 | } 45 | -------------------------------------------------------------------------------- /docs.mli: -------------------------------------------------------------------------------- 1 | type ShrinkwrapOptions := { 2 | dirname: String, 3 | createUri: (name: String, version: String) => uri: String, 4 | registries: Array, 5 | rewriteResolved: (resolved: String) => resolved: String, 6 | warnOnNotSemver: Boolean, 7 | validators?: Array< 8 | (dep: Object, key: String) => Error | null 9 | > 10 | } 11 | 12 | npm-shrinkwrap := ( 13 | opts: ShrinkwrapOptions, 14 | cb: Callback> 15 | ) => void 16 | 17 | npm-shrinkwrap/analyze-dependency := ( 18 | name: String, 19 | gitLink: String, 20 | opts: ShrinkwrapOptions, 21 | cb: Callback 22 | ) => void 23 | 24 | npm-shrinkwrap/set-resolved := 25 | (ShrinkwrapOptions, Callback) => void 26 | 27 | npm-shrinkwrap/trim-and-sort-shrinkwrap := 28 | (ShrinkwrapOptions, Callback) => void 29 | 30 | npm-shrinkwrap/verify-git := 31 | (ShrinkwrapOptions, Callback) => void 32 | 33 | npm-shrinkwrap/bin/cli := (opts: ShrinkwrapOptions & { 34 | help: Boolean, 35 | install: Boolean, 36 | onwarn: Function>, 37 | onerror: Function, 38 | cmd: String, 39 | silent: Boolean, 40 | _: Array, 41 | packageVersion: String, 42 | moduleName: String, 43 | depth: Number, 44 | short: Boolean 45 | }) => void 46 | -------------------------------------------------------------------------------- /sync/index.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | var parallel = require('run-parallel'); 3 | var npm = require('npm'); 4 | 5 | var read = require('./read.js'); 6 | var forceInstall = require('./force-install.js'); 7 | 8 | /* sync shrinkwrap 9 | 10 | - read npm-shrinkwrap.json 11 | - walk it and write it into node_modules 12 | - remove any excess shit from node_modules 13 | 14 | */ 15 | 16 | module.exports = syncShrinkwrap; 17 | 18 | function syncShrinkwrap(opts, cb) { 19 | var dirname = opts.dirname || process.cwd(); 20 | 21 | var npmOpts = { 22 | prefix: opts.dirname, 23 | loglevel: 'error' 24 | }; 25 | 26 | if (opts.registry) { 27 | npmOpts.registry = opts.registry; 28 | } 29 | 30 | npm.load(npmOpts, function (err, npm) { 31 | if (err) { 32 | return cb(err); 33 | } 34 | 35 | opts.npm = npm; 36 | 37 | parallel({ 38 | shrinkwrap: read.shrinkwrap.bind(null, dirname), 39 | devDependencies: read.devDependencies.bind(null, dirname) 40 | }, function (err, tuple) { 41 | if (err) { 42 | return cb(err); 43 | } 44 | 45 | var nodeModules = path.join(dirname, 'node_modules'); 46 | var shrinkwrap = tuple.shrinkwrap; 47 | shrinkwrap.devDependencies = tuple.devDependencies; 48 | 49 | opts.dev = true; 50 | 51 | forceInstall(nodeModules, shrinkwrap, opts, cb); 52 | }); 53 | }); 54 | } 55 | 56 | -------------------------------------------------------------------------------- /trim-nested.js: -------------------------------------------------------------------------------- 1 | var jsonDiff = require('json-diff'); 2 | 3 | module.exports = trimNested; 4 | 5 | /* var patches = diff(current, previous) 6 | 7 | for each NESTED (depth >=1) patch, apply it to current. 8 | 9 | Write new current into disk at dirname/npm-shrinkwrap.json 10 | 11 | */ 12 | function trimNested(previous, current, opts) { 13 | // bail early if we want to keep nested dependencies 14 | if (opts.keepNested) { 15 | return current; 16 | } 17 | 18 | // purposes find patches from to 19 | // apply TO current FROM previous 20 | var patches = jsonDiff.diff(current, previous); 21 | 22 | if (!patches) { 23 | return current; 24 | } 25 | 26 | patches = removeTopLevelPatches(patches); 27 | 28 | if (patches.dependencies) { 29 | Object.keys(patches.dependencies) 30 | .forEach(function (key) { 31 | current.dependencies[key] = 32 | previous.dependencies[key]; 33 | }); 34 | } 35 | 36 | return current; 37 | } 38 | 39 | function removeTopLevelPatches(patches) { 40 | if (!patches.dependencies) { 41 | return patches; 42 | } 43 | 44 | patches.dependencies = Object.keys(patches.dependencies) 45 | .reduce(function (acc, key) { 46 | var patch = patches.dependencies[key]; 47 | 48 | if (typeof patch !== 'object' || patch === null) { 49 | return acc; 50 | } 51 | 52 | var patchKeys = Object.keys(patch); 53 | 54 | if (patchKeys.length === 1 && 55 | patchKeys[0] === 'dependencies' 56 | ) { 57 | acc[key] = patch; 58 | } 59 | return acc; 60 | }, {}); 61 | 62 | return patches; 63 | } 64 | -------------------------------------------------------------------------------- /verify-git.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | var readJSON = require('./read-json'); 3 | var parallel = require('run-parallel'); 4 | 5 | var analyzeDependency = require('./analyze-dependency.js'); 6 | 7 | module.exports = verifyGit; 8 | 9 | function verifyGit(opts, callback) { 10 | if (typeof opts === 'string') { 11 | opts = { dirname: opts }; 12 | } 13 | 14 | var packageFile = path.join(opts.dirname, 'package.json'); 15 | 16 | readJSON(packageFile, onpackage); 17 | 18 | function onpackage(err, package) { 19 | if (err) { 20 | return callback(err); 21 | } 22 | 23 | var deps = package.dependencies || {}; 24 | var devDeps = package.devDependencies || {}; 25 | 26 | parallel([ 27 | analyze.bind(null, deps, opts), 28 | opts.dev ? analyze.bind(null, devDeps, opts) : null 29 | ].filter(Boolean), function (err, values) { 30 | if (err) { 31 | return callback(err); 32 | } 33 | 34 | var errors = values[0].concat(values[1] || []); 35 | 36 | callback(null, errors); 37 | }); 38 | } 39 | } 40 | 41 | function analyze(deps, opts, callback) { 42 | var tasks = Object.keys(deps).map(function (key) { 43 | return analyzeDependency.bind(null, 44 | key, deps[key], opts); 45 | }); 46 | 47 | parallel(tasks, function (err, results){ 48 | if (err) { 49 | return callback(err); 50 | } 51 | 52 | var errors = Object.keys(results) 53 | .reduce(function (acc, key) { 54 | if (results[key]) { 55 | acc.push(results[key]); 56 | } 57 | return acc; 58 | }, []); 59 | 60 | callback(null, errors); 61 | }); 62 | } 63 | 64 | 65 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "npm-shrinkwrap", 3 | "version": "6.1.0", 4 | "description": "A consistent shrinkwrap tool", 5 | "keywords": [], 6 | "author": "Raynos ", 7 | "repository": "git://github.com/uber/npm-shrinkwrap.git", 8 | "main": "index", 9 | "homepage": "https://github.com/uber/npm-shrinkwrap", 10 | "bugs": { 11 | "url": "https://github.com/uber/npm-shrinkwrap/issues", 12 | "email": "raynos2@gmail.com" 13 | }, 14 | "dependencies": { 15 | "array-find": "^0.1.1", 16 | "array-flatten": "^2.1.0", 17 | "error": "^4.2.0", 18 | "graceful-fs": "^4.1.2", 19 | "json-diff": "^0.3.1", 20 | "minimist": "^1.1.0", 21 | "msee": "^0.1.1", 22 | "npm": "^2.15.10", 23 | "rimraf": "^2.2.8", 24 | "run-parallel": "^1.1.6", 25 | "run-series": "^1.0.2", 26 | "safe-json-parse": "^2.0.0", 27 | "semver": "^4.0.3", 28 | "sorted-object": "^1.0.0", 29 | "string-template": "^0.2.0" 30 | }, 31 | "devDependencies": { 32 | "fixtures-fs": "^2.0.0", 33 | "istanbul": "~0.3.2", 34 | "jshint": "2.5.6", 35 | "pre-commit": "0.0.9", 36 | "tap-spec": "~1.0.0", 37 | "tape": "^3.0.2" 38 | }, 39 | "scripts": { 40 | "test": "npm run jshint -s && NODE_ENV=test node test/index.js | tap-spec", 41 | "unit-test": "NODE_ENV=test node test/npm-shrinkwrap.js | tap-spec", 42 | "jshint-pre-commit": "jshint --verbose $(git diff --cached --name-only | grep '\\.js$')", 43 | "jshint": "jshint --verbose .", 44 | "cover": "istanbul cover --report none --print detail test/index.js", 45 | "view-cover": "istanbul report html && open ./coverage/index.html", 46 | "travis": "npm run cover -s && istanbul report lcov && ((cat coverage/lcov.info | coveralls) || exit 0)" 47 | }, 48 | "pre-commit": [ 49 | "jshint-pre-commit", 50 | "unit-test" 51 | ], 52 | "bin": { 53 | "npm-shrinkwrap": "./bin/cli.js" 54 | }, 55 | "license": "MIT", 56 | "engine": { 57 | "node": ">= 0.10.x" 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /test/git-https-use-case.js: -------------------------------------------------------------------------------- 1 | var test = require('tape'); 2 | var path = require('path'); 3 | var fs = require('graceful-fs'); 4 | var safeJsonParse = require('safe-json-parse'); 5 | var fixtures = require('fixtures-fs'); 6 | var exec = require('child_process').exec; 7 | 8 | var shrinkwrapCli = require('../bin/cli.js'); 9 | 10 | var PROJ = path.join(__dirname, 'proj'); 11 | 12 | test('npm-shrinkwrap --dev on git+https uri', fixtures(__dirname, { 13 | 'proj': { 14 | 'package.json': JSON.stringify({ 15 | name: 'foo', 16 | version: '0.1.0', 17 | dependencies: {}, 18 | devDependencies: { 19 | 'gulp-yuidoc': 'git://github.com/' + 20 | 'Netflix-Skunkworks/gulp-yuidoc.git' + 21 | '#4e9a896c28ddf2fec477eb81766f04b779b320a7' 22 | } 23 | }) 24 | } 25 | }, function (assert) { 26 | exec('npm install', { 27 | cwd: PROJ 28 | }, function (err, stdout, stderr) { 29 | assert.ifError(err); 30 | 31 | if (stderr) { 32 | console.error(stderr); 33 | } 34 | 35 | shrinkwrapCli({ 36 | dirname: PROJ, 37 | dev: true, 38 | _: [] 39 | }, function (err) { 40 | assert.ifError(err); 41 | 42 | var file = path.join(PROJ, 'npm-shrinkwrap.json'); 43 | fs.readFile(file, function (err, content) { 44 | assert.ifError(err); 45 | content = String(content); 46 | 47 | safeJsonParse(content, function (err, json) { 48 | assert.ifError(err); 49 | 50 | assert.equal(json.name, 'foo'); 51 | assert.equal(json.version, '0.1.0'); 52 | assert.ok(json.dependencies['gulp-yuidoc']); 53 | 54 | var dep = json.dependencies['gulp-yuidoc']; 55 | assert.equal(dep.version, '0.1.2'); 56 | 57 | assert.end(); 58 | }); 59 | }); 60 | }); 61 | }); 62 | })); 63 | 64 | -------------------------------------------------------------------------------- /docs/faq.md: -------------------------------------------------------------------------------- 1 | # FAQ 2 | 3 | ## How do I use validators 4 | 5 | Validators are an array of functions. 6 | 7 | Each one gets called with a package in the shrinkwrap. You 8 | can do a custom validation check for the package. 9 | 10 | Either return an error or null. 11 | 12 | ```js 13 | var npmShrinkwrap = require('npm-shrinkwrap/bin/cli'); 14 | var TypedError = require('error/typed'); 15 | 16 | var MissingResolved = TypedError({ 17 | type: 'missing.resolved.field', 18 | message: 'Expected dependency {name}@{version} to ' + 19 | 'have a resolved field.\n Instead found a ' + 20 | 'from field {from}.\n Invalid dependency is found ' + 21 | 'at path {path}' 22 | }); 23 | 24 | var InvalidGitDependency = TypedError({ 25 | type: 'invalid.git.dependency', 26 | message: 'Unexpected usage of invalid Git dependency.\n ' + 27 | 'Expected dependency {name}@{version} to not be ' + 28 | 'resolved to {resolved}.\n Please install again ' + 29 | 'from the gitolite mirror.\n Invalid dependency ' + 30 | 'is found at path {path}' 31 | }); 32 | 33 | npmShrinkwrap({ 34 | validators: [ 35 | assertResolved, 36 | assertNotGithub 37 | ] 38 | }); 39 | 40 | function assertResolved(package, name) { 41 | if (typeof package.from === 'string' && 42 | typeof package.resolved !== 'string' 43 | ) { 44 | return MissingResolved({ 45 | name: name, 46 | resolved: package.resolved, 47 | from: package.from, 48 | version: package.version, 49 | path: computePath(package) 50 | }); 51 | } 52 | 53 | return null; 54 | } 55 | 56 | function assertNotGithub(package, name) { 57 | if (package.resolved && 58 | package.resolved.indexOf('git@github.com') !== -1 59 | ) { 60 | return InvalidGitDependency({ 61 | name: name, 62 | version: package.version, 63 | resolved: package.resolved, 64 | from: package.from, 65 | path: computePath(package) 66 | }); 67 | } 68 | 69 | return null; 70 | } 71 | ``` 72 | -------------------------------------------------------------------------------- /bin/usage.md: -------------------------------------------------------------------------------- 1 | # `{cmd} [options]` 2 | 3 | Verifies your `package.json` and `node_modules` are in sync. 4 | Then runs `npm shrinkwrap` and cleans up the 5 | `npm-shrinkwrap.json` file to be consistent. 6 | 7 | Basically like `npm shrinkwrap` but better 8 | 9 | Options: 10 | --dirname sets the directory location of the package.json 11 | defaults to `process.cwd()`. 12 | --keep-nested If set, will not remove nested changes. 13 | --warnOnNotSemver If set, will downgrade invalid semver errors 14 | to warnings 15 | --dev If set, will shrinkwrap dev dependencies 16 | --silent If set, will be silent. 17 | 18 | ## `{cmd} --help` 19 | 20 | Prints this message 21 | 22 | ## `{cmd} sync` 23 | 24 | Syncs your `npm-shrinkwrap.json` file into the `node_modules` 25 | directory. 26 | 27 | This will ensure that your local `node_modules` matches the 28 | `npm-shrinkwrap.json` file verbatim. Any excess modules in 29 | your node_modules folder will be removed if they are not in 30 | the `npm-shrinkwrap.json` file. 31 | 32 | Options: 33 | --dirname sets the directory of the npm-shrinkwrap.json 34 | 35 | - `--dirname` defaults to `process.cwd()` 36 | 37 | ## `{cmd} install` 38 | 39 | Will write a `shrinkwrap` script to your `package.json` file. 40 | 41 | ```json 42 | { 43 | "scripts": { 44 | "shrinkwrap": "{cmd}" 45 | } 46 | } 47 | ``` 48 | 49 | Options: 50 | --dirname sets the directory location of the package.json 51 | 52 | ## `{cmd} diff [OldShaOrFile] [NewShaOrfile]` 53 | 54 | This will show a human readable for the shrinkwrap file. 55 | 56 | You can pass it either a path to a file or a git shaism. 57 | 58 | Example: 59 | 60 | `{cmd} diff HEAD npm-shrinkwrap.json` 61 | `{cmd} diff origin/master HEAD` 62 | 63 | Options: 64 | --depth configure the depth at which it prints 65 | --short when set it will print add/remove tersely 66 | --dirname configure which folder to run within 67 | 68 | - `--depth` defaults to `0` 69 | - `--short` defaults to `false` 70 | - `--dirname` defaults to `process.cwd()` 71 | -------------------------------------------------------------------------------- /errors.js: -------------------------------------------------------------------------------- 1 | var TypedError = require('error/typed'); 2 | 3 | var EmptyFile = TypedError({ 4 | message: 'npm-shrinkwrap must not be empty', 5 | type: 'npm-shrinkwrap.missing' 6 | }); 7 | 8 | var InvalidNPMVersion = TypedError({ 9 | type: 'npm-shrinkwrap.invalid_version', 10 | message: 'Using an older version of npm-shrinkwrap.\n' + 11 | 'Expected version {existing} but found {current}.\n' + 12 | 'To fix: please run `npm install npm-shrinkwrap@{existing}`\n' 13 | }); 14 | 15 | var NPMError = TypedError({ 16 | type: 'npm-shrinkwrap.npm-error', 17 | message: 'Problems were encountered\n' + 18 | 'Please correct and try again.\n' + 19 | '{problemsText}', 20 | pkginfo: null, 21 | problemsText: null 22 | }); 23 | 24 | var InvalidVersionsNPMError = TypedError({ 25 | type: 'npm-shrinkwrap.npm-error.invalid-version', 26 | message: 'Problems were encountered\n' + 27 | 'Please correct and try again\n' + 28 | 'invalid: {name}@{actual} {dirname}/node_modules/{name}', 29 | errors: null, 30 | name: null, 31 | actual: null, 32 | dirname: null 33 | }); 34 | 35 | var NoTagError = TypedError({ 36 | type: 'missing.gitlink.tag', 37 | message: 'Expected the git dependency {name} to have a ' + 38 | 'tag;\n instead I found {gitLink}' 39 | }); 40 | 41 | var NonSemverTag = TypedError({ 42 | type: 'gitlink.tag.notsemver', 43 | message: 'Expected the git dependency {name} to have a ' + 44 | 'valid version tag;\n instead I found {tag} for the ' + 45 | 'dependency {gitLink}' 46 | }); 47 | 48 | var InvalidPackage = TypedError({ 49 | type: 'invalid.packagejson', 50 | message: 'The package.json for module {name} in your ' + 51 | 'node_modules tree is malformed.\n Expected JSON with ' + 52 | 'a version field and instead got {json}' 53 | }); 54 | 55 | var InvalidVersion = TypedError({ 56 | type: 'invalid.git.version', 57 | message: 'The version of {name} installed is invalid.\n ' + 58 | 'Expected {expected} to be installed but instead ' + 59 | '{actual} is installed.' 60 | }); 61 | 62 | var MissingPackage = TypedError({ 63 | type: 'missing.package', 64 | message: 'The version of {name} installed is missing.\n' + 65 | 'Expected {expected} to be installed but instead ' + 66 | 'found nothing installed.\n' 67 | }); 68 | 69 | module.exports = { 70 | EmptyFile: EmptyFile, 71 | InvalidNPMVersion: InvalidNPMVersion, 72 | NPMError: NPMError, 73 | InvalidVersionsNPMError: InvalidVersionsNPMError, 74 | NoTagError: NoTagError, 75 | NonSemverTag: NonSemverTag, 76 | InvalidPackage: InvalidPackage, 77 | InvalidVersion: InvalidVersion, 78 | MissingPackage: MissingPackage 79 | }; 80 | -------------------------------------------------------------------------------- /sync/purge-excess.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | var fs = require('graceful-fs'); 3 | var parallel = require('run-parallel'); 4 | var rimraf = require('rimraf'); 5 | var flatten = require('array-flatten'); 6 | 7 | module.exports = purgeExcess; 8 | 9 | /* given the shrinkwrap & package.json, find all extra folders 10 | in top level node_modules directory and remove them 11 | 12 | Basically like `npm prune` except recursive 13 | */ 14 | function purgeExcess(dir, shrinkwrap, opts, cb) { 15 | if (typeof opts === 'function') { 16 | cb = opts; 17 | opts = {}; 18 | } 19 | 20 | findExcess(dir, shrinkwrap, opts, null, function (err, excessFiles) { 21 | if (err) { 22 | // if no node_modules then nothing to purge 23 | if (err.code === 'ENOENT') { 24 | return cb(null); 25 | } 26 | return cb(err); 27 | } 28 | 29 | var tasks = excessFiles.map(function (file) { 30 | var filePath = path.join(dir, file); 31 | console.log('removing', filePath); 32 | return rimraf.bind(null, filePath); 33 | }); 34 | 35 | parallel(tasks, cb); 36 | }); 37 | } 38 | 39 | /* find any excess folders in node_modules that are not in 40 | deps. 41 | */ 42 | function findExcess(dir, shrinkwrap, opts, scope, cb) { // jshint ignore:line 43 | fs.readdir(dir, function (err, files) { 44 | if (err) { 45 | return cb(err); 46 | } 47 | 48 | parallel(files.map(function (file) { 49 | return validateExcess.bind(null, dir, file, shrinkwrap, opts, 50 | scope); 51 | }), function (err, excessFiles) { 52 | if (err) { 53 | return cb(err); 54 | } 55 | return cb(null, flatten(excessFiles || []).filter(Boolean)); 56 | }); 57 | }); 58 | } 59 | 60 | /* find any excess folders in node_modules that are not in 61 | deps. 62 | */ 63 | function validateExcess(dir, file, shrinkwrap, opts, scope, cb) { // jshint ignore:line 64 | file = file.toLowerCase(); 65 | 66 | // don't consider node_modules/.bin 67 | if (file === '.bin') { 68 | return cb(); 69 | } 70 | 71 | // consider top-level scoped packages only; e.g. those nested at the level 72 | // node_modules/{*} 73 | var isScopedDir = file[0] === '@'; 74 | if (isScopedDir) { 75 | return findExcess(path.join(dir, scope + '/' + file), shrinkwrap, opts, 76 | file, cb); 77 | } 78 | 79 | // the file is in excess if it does not exist in the package.json's 80 | // dev dependencies; this step is skipped if we are not analyzing 81 | // dev dependencies 82 | if (opts.dev && shrinkwrap.devDependencies && 83 | lowercaseContains(Object.keys(shrinkwrap.devDependencies), file)) { 84 | return cb(); 85 | } 86 | 87 | // the file is in excess if it does not exist in the package.json's 88 | // regular dependencies 89 | if (shrinkwrap.dependencies && lowercaseContains(Object.keys(shrinkwrap.dependencies), file)) { 90 | return cb(); 91 | } 92 | 93 | // if all checks pass up until this point, the file is in excess 94 | return cb(null, [file]); 95 | } 96 | 97 | /* check if the element (as a string) is contained in the array of strings 98 | in a case-insensitive fashion. 99 | */ 100 | function lowercaseContains(arr, elem) { 101 | return arr.map(function (arrElem) { 102 | return arrElem.toLowerCase(); 103 | }).indexOf(elem) !== -1; 104 | } 105 | -------------------------------------------------------------------------------- /bin/cli.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | var parseArgs = require('minimist'); 4 | var path = require('path'); 5 | 6 | var installModule = require('./install.js'); 7 | var printHelp = require('./help.js'); 8 | var shrinkwrap = require('../index.js'); 9 | var formatters = require('./formatters.js'); 10 | var diffShrinkwrap = require('./diff.js'); 11 | var syncShrinkwrap = require('../sync/'); 12 | 13 | main.printWarnings = printWarnings; 14 | 15 | module.exports = main; 16 | 17 | if (require.main === module) { 18 | main(parseArgs(process.argv.slice(2))); 19 | } 20 | 21 | function main(opts, callback) { 22 | var command = opts._.shift(); 23 | 24 | if (opts.h || opts.help || command === 'help') { 25 | return printHelp(opts); 26 | } 27 | 28 | opts.dirname = opts.dirname ? 29 | path.resolve(opts.dirname) : process.cwd(); 30 | 31 | opts.keepNested = 'keep-nested' in opts ? 32 | !!opts['keep-nested'] : 'keepNested' in opts ? 33 | !!opts.keepNested : true; 34 | 35 | opts.warnOnNotSemver = opts.warnOnNotSemver ? 36 | opts.warnOnNotSemver : true; 37 | 38 | opts.cmd = opts.cmd || 'npm-shrinkwrap'; 39 | 40 | if (command === 'install') { 41 | return installModule(opts, function (err) { 42 | if (err) { 43 | throw err; 44 | } 45 | 46 | console.log('added %s to package.json', opts.cmd); 47 | }); 48 | } else if (command === 'diff') { 49 | return diffShrinkwrap(opts, function (err, diff) { 50 | if (callback) { 51 | return callback(err, diff); 52 | } 53 | 54 | if (err) { 55 | throw err; 56 | } 57 | 58 | console.log(diff); 59 | }); 60 | } else if (command === 'sync') { 61 | return syncShrinkwrap(opts, function (err) { 62 | if (callback) { 63 | return callback(err); 64 | } 65 | 66 | if (err) { 67 | console.log('error', err); 68 | console.error('stack', new Error().stack); 69 | // console.log('stack.length', err.stack.length); 70 | // return; 71 | throw err; 72 | } 73 | 74 | console.log('synced npm-shrinkwrap.json ' + 75 | 'into node_modules'); 76 | }); 77 | } 78 | 79 | shrinkwrap(opts, function (err, warnings) { 80 | if (err) { 81 | if (opts.onerror) { 82 | return opts.onerror(err); 83 | } 84 | 85 | if (callback) { 86 | return callback(err); 87 | } 88 | 89 | printWarnings(err, formatters); 90 | console.log('something went wrong. Did not write ' + 91 | 'npm-shrinkwrap.json'); 92 | return process.exit(1); 93 | } 94 | 95 | if (callback) { 96 | return callback(null, warnings); 97 | } 98 | 99 | if (warnings) { 100 | if (opts.onwarn) { 101 | opts.onwarn(warnings); 102 | } else { 103 | printWarnings({ errors: warnings }, formatters); 104 | } 105 | } 106 | 107 | if (!opts.silent) { 108 | console.log('wrote npm-shrinkwrap.json'); 109 | } 110 | }); 111 | } 112 | 113 | function printWarnings(err, formatters) { 114 | if (!err.errors) { 115 | return console.error(err.message); 116 | } 117 | 118 | err.errors.forEach(function (err) { 119 | var format = formatters[err.type] || formatters.default; 120 | 121 | console.error(format(err)); 122 | }); 123 | } 124 | -------------------------------------------------------------------------------- /analyze-dependency.js: -------------------------------------------------------------------------------- 1 | var url = require('url'); 2 | var validSemver = require('semver').valid; 3 | var path = require('path'); 4 | var readJSON = require('./read-json'); 5 | 6 | var errors = require('./errors.js'); 7 | 8 | module.exports = analyzeDependency; 9 | /* 10 | 11 | for each dependency in package.json 12 | 13 | - if not git then skip 14 | - if no # tag then throw 15 | - if # is not `v{version}` then throw 16 | - load up `require(name/package.json).version` 17 | - if version in node_modules not same in SHA then throw 18 | 19 | Support 20 | 21 | - git://github.com/user/project.git#commit-is h 22 | - git+ssh://user@hostname:project.git#commit-ish 23 | - git+ssh://user@hostname/project.git#commit-ish 24 | - git+http://user@hostname/project/blah.git#commit-ish 25 | - git+https://user@hostname/project/blah.git#commit-ish 26 | - user/name#commit-ish (github) 27 | */ 28 | 29 | function analyzeDependency(name, gitLink, opts, cb) { 30 | var parsed = parseTag(gitLink); 31 | 32 | if (!parsed) { 33 | return cb(null); 34 | } 35 | 36 | if (!parsed.tag) { 37 | return cb(null, errors.NoTagError({ 38 | name: name, 39 | gitLink: gitLink, 40 | dirname: opts.dirname 41 | })); 42 | } 43 | 44 | var version = parseVersion(parsed.tag); 45 | 46 | if (!version) { 47 | return cb(null, errors.NonSemverTag({ 48 | name: name, 49 | gitLink: gitLink, 50 | tag: parsed.tag, 51 | dirname: opts.dirname 52 | })); 53 | } 54 | 55 | var packageUri = path.join(opts.dirname, 'node_modules', 56 | name, 'package.json'); 57 | readJSON(packageUri, function (err, pkg) { 58 | if (err) { 59 | if (err.code === 'ENOENT') { 60 | return cb(null, errors.MissingPackage({ 61 | name: name, 62 | expected: version, 63 | dirname: opts.dirname, 64 | tag: parsed.tag 65 | })); 66 | } 67 | 68 | return cb(err); 69 | } 70 | 71 | if (!pkg || !pkg.version) { 72 | return cb(null, errors.InvalidPackage({ 73 | name: name, 74 | gitLink: gitLink, 75 | json: JSON.stringify(pkg), 76 | tag: parsed.tag, 77 | dirname: opts.dirname 78 | })); 79 | } 80 | 81 | if (pkg.version !== version) { 82 | return cb(null, errors.InvalidVersion({ 83 | name: name, 84 | expected: version, 85 | actual: pkg.version, 86 | gitLink: gitLink, 87 | tag: parsed.tag, 88 | dirname: opts.dirname 89 | })); 90 | } 91 | 92 | return cb(null); 93 | }); 94 | } 95 | 96 | function parseTag(value) { 97 | var uri = url.parse(value); 98 | 99 | if (isGitUrl(uri)) { 100 | return { 101 | tag: uri.hash ? uri.hash.substr(1) : null 102 | }; 103 | } 104 | 105 | // support github 106 | var parts = value.split('/'); 107 | if (parts.length === 2) { 108 | var tag = parts[1].split('#')[1]; 109 | 110 | return { tag: tag || null }; 111 | } 112 | 113 | return null; 114 | } 115 | 116 | function isGitUrl (url) { 117 | switch (url.protocol) { 118 | case "git:": 119 | case "git+http:": 120 | case "git+https:": 121 | case "git+rsync:": 122 | case "git+ftp:": 123 | case "git+ssh:": 124 | return true; 125 | } 126 | } 127 | 128 | function parseVersion(tag) { 129 | var char = tag[0]; 130 | 131 | if (char !== 'v') { 132 | return null; 133 | } 134 | 135 | var rest = tag.substr(1); 136 | var isValid = validSemver(rest); 137 | 138 | return isValid ? rest : null; 139 | } 140 | -------------------------------------------------------------------------------- /bin/diff.js: -------------------------------------------------------------------------------- 1 | var parallel = require('run-parallel'); 2 | var path = require('path'); 3 | var readJSON = require('../read-json'); 4 | var jsonDiff = require('json-diff'); 5 | var colorize = require('json-diff/lib/colorize'); 6 | var exec = require('child_process').exec; 7 | var jsonParse = require('safe-json-parse'); 8 | 9 | /*jshint camelcase: false*/ 10 | function purgeDeps(opts, diff, meta) { 11 | if (!diff) { 12 | return; 13 | } 14 | 15 | var depsKey = 'dependencies' in diff ? 16 | 'dependencies' : 'dependencies__deleted' in diff ? 17 | 'dependencies__deleted' : 'dependencies__added' in diff ? 18 | 'dependencies__added' : null; 19 | if (!depsKey) { 20 | return diff; 21 | } 22 | 23 | var deps = diff[depsKey]; 24 | 25 | diff[depsKey] = Object.keys(deps).reduce(function (acc, key) { 26 | var deleted = meta.deleted ? meta.deleted : 27 | (key.indexOf('__deleted') !== -1 || 28 | depsKey === 'dependencies__deleted'); 29 | var added = meta.added ? meta.added : 30 | (key.indexOf('__added') !== -1 || 31 | depsKey === 'dependencies__added'); 32 | if (deleted || added) { 33 | if (meta.depth >= opts.depth) { 34 | if (!opts.short) { 35 | deps[key].dependencies = '[NestedObject]'; 36 | acc[key] = deps[key]; 37 | } else { 38 | acc[key] = (deleted ? '[Deleted' : '[Added') + 39 | '@' + deps[key].version + ']'; 40 | } 41 | } else { 42 | acc[key] = purgeDeps(opts, deps[key], { 43 | depth: meta.depth + 1, 44 | added: added, 45 | deleted: deleted 46 | }); 47 | } 48 | } else { 49 | acc[key] = purgeDeps(opts, deps[key], { 50 | depth: meta.depth + 1 51 | }); 52 | } 53 | 54 | return acc; 55 | }, {}); 56 | 57 | return diff; 58 | } 59 | 60 | function diffContent(oldContent, newContent, opts) { 61 | var diff = jsonDiff.diff(oldContent, newContent); 62 | 63 | diff = purgeDeps(opts, diff, { 64 | depth: 0 65 | }); 66 | 67 | return colorize.colorize(diff, { 68 | color: opts.color 69 | }); 70 | } 71 | 72 | function gitShow(sha, cwd, callback) { 73 | function ongit(err, stdout, stderr) { 74 | if (stderr) { 75 | console.error(stderr); 76 | } 77 | 78 | if (err && err.message.indexOf('not in \'HEAD\'') !== -1) { 79 | return callback(null, {}); 80 | } 81 | 82 | if (err) { 83 | return callback(err); 84 | } 85 | 86 | jsonParse(stdout, callback); 87 | } 88 | 89 | exec('git show ' + sha + ':./npm-shrinkwrap.json', { 90 | cwd: cwd || process.cwd(), 91 | maxBuffer: 10000 * 1024 92 | }, ongit); 93 | } 94 | 95 | function isFile(fileName) { 96 | var index = fileName.indexOf('.json'); 97 | 98 | return index !== -1 && index === fileName.length - 5; 99 | } 100 | 101 | function main(opts, callback) { 102 | var fileA = opts._[0]; 103 | var fileB = opts._[1]; 104 | 105 | if (!fileB) { 106 | fileB = 'npm-shrinkwrap.json'; 107 | } 108 | 109 | if (!fileA) { 110 | fileA = 'HEAD'; 111 | } 112 | 113 | if (!("color" in opts)) { 114 | opts.color = process.stdout.isTTY; 115 | } else if (opts.color === "false") { 116 | opts.color = false; 117 | } 118 | 119 | if (!("short" in opts)) { 120 | opts.short = true; 121 | } 122 | 123 | opts.depth = 'depth' in opts ? opts.depth : 0; 124 | var cwd = opts.dirname || process.cwd(); 125 | 126 | parallel([ 127 | isFile(fileA) ? 128 | readJSON.bind(null, path.resolve(cwd, fileA)) : 129 | gitShow.bind(null, fileA, cwd), 130 | isFile(fileB) ? 131 | readJSON.bind(null, path.resolve(cwd, fileB)) : 132 | gitShow.bind(null, fileB, cwd) 133 | ], function (err, files) { 134 | if (err) { 135 | return callback(err); 136 | } 137 | 138 | callback(null, diffContent(files[0], files[1], opts)); 139 | }); 140 | } 141 | 142 | module.exports = main; 143 | -------------------------------------------------------------------------------- /set-resolved.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | var fs = require('graceful-fs'); 3 | var template = require('string-template'); 4 | var readJSON = require('./read-json'); 5 | var url = require('url'); 6 | var semver = require('semver'); 7 | 8 | var errors = require('./errors.js'); 9 | var version = require('./package.json').version; 10 | 11 | var NPM_URI = 'https://registry.npmjs.org/{name}/-/{name}-{version}.tgz'; 12 | 13 | module.exports = setResolved; 14 | 15 | function defaultCreateUri(name, version) { 16 | return template(NPM_URI, { 17 | name: name, 18 | version: version 19 | }); 20 | } 21 | 22 | /* from field is either: 23 | - {name}@{semverRange} 24 | - {name}@{gitUri} 25 | - {privateRegistryUri} 26 | 27 | */ 28 | function setResolved(opts, callback) { 29 | if (typeof opts === 'string') { 30 | opts = { dirname: opts }; 31 | } 32 | 33 | var shrinkwrapFile = path.join(opts.dirname, 'npm-shrinkwrap.json'); 34 | var createUri = opts.createUri || defaultCreateUri; 35 | var registries = opts.registries || ['registry.npmjs.org']; 36 | var rewriteResolved = opts.rewriteResolved || null; 37 | var rewriteFrom = opts.rewriteFrom || null; 38 | 39 | readJSON(shrinkwrapFile, onjson); 40 | 41 | function onjson(err, json) { 42 | if (err) { 43 | return callback(err); 44 | } 45 | 46 | var existingVersion = json['npm-shrinkwrap-version']; 47 | 48 | if (existingVersion && semver.gt(existingVersion, version)) { 49 | return callback(errors.InvalidNPMVersion({ 50 | existing: existingVersion, 51 | current: version 52 | })); 53 | } 54 | 55 | json['npm-shrinkwrap-version'] = version; 56 | 57 | json['node-version'] = process.version; 58 | 59 | json = fixResolved(json, null); 60 | 61 | // if top level shrinkwrap has a `from` or `resolved` 62 | // field then delete them 63 | if (json.from) { 64 | json.from = undefined; 65 | } 66 | if (json.resolved) { 67 | json.resolved = undefined; 68 | } 69 | 70 | fs.writeFile(shrinkwrapFile, 71 | JSON.stringify(json, null, 2) + '\n', callback); 72 | } 73 | 74 | function fixResolved(json, name) { 75 | if (json.from && !json.resolved) { 76 | computeResolved(json, name); 77 | } 78 | 79 | // handle the case of no resolved & no from 80 | if (json.version && name && !json.resolved) { 81 | json.resolved = createUri(name, json.version); 82 | } 83 | 84 | if (rewriteResolved && json.resolved) { 85 | json.resolved = rewriteResolved(json.resolved); 86 | } 87 | 88 | if (rewriteFrom && json.from) { 89 | json.from = rewriteFrom(json.from, json.resolved); 90 | } 91 | 92 | if (json.dependencies) { 93 | Object.keys(json.dependencies).forEach(function (dep) { 94 | fixResolved(json.dependencies[dep], dep); 95 | }); 96 | json.dependencies = json.dependencies; 97 | } 98 | 99 | return json; 100 | } 101 | 102 | /* look for `from` fields and set a `resolved` field next 103 | to it if the `resolved` does not exist. 104 | 105 | This normalizes `npm shrinkwrap` so a resolved field 106 | always get's set. 107 | 108 | */ 109 | function computeResolved(json, name) { 110 | var value = json.from; 111 | name = name || json.name; 112 | 113 | var uri = url.parse(value); 114 | 115 | // handle the case `from` is a privateRegistryURL 116 | if ((uri.protocol === 'http:' || uri.protocol === 'https:') && 117 | registries.indexOf(uri.host) !== -1 118 | ) { 119 | json.resolved = value; 120 | return; 121 | } 122 | 123 | // from is {name}@{semver | uri} 124 | var parts = value.split('@'); 125 | var rest = parts.slice(1).join('@'); 126 | 127 | var secondUri = url.parse(rest); 128 | 129 | // from is a {name}@{semver} 130 | if (!secondUri.protocol) { 131 | // call createUri to generate a tarball uri 132 | // for json module name & version 133 | json.resolved = createUri(name, json.version); 134 | return; 135 | } else { 136 | // from is a git link. 137 | // do not try to set resolved 138 | return; 139 | } 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /sync/force-install.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | var url = require('url'); 3 | var parallel = require('run-parallel'); 4 | var series = require('run-series'); 5 | var template = require('string-template'); 6 | 7 | var read = require('./read.js'); 8 | var purgeExcess = require('./purge-excess.js'); 9 | var installModule = require('./install-module.js'); 10 | 11 | var NPM_URI = 'https://registry.npmjs.org/{name}/-/{name}-{version}.tgz'; 12 | 13 | module.exports = forceInstall; 14 | 15 | function forceInstall(nodeModules, shrinkwrap, opts, cb) { 16 | if (typeof opts === 'function') { 17 | cb = opts; 18 | opts = {}; 19 | } 20 | 21 | // if no dependencies object then terminate recursion 22 | if (shrinkwrap.name && !shrinkwrap.dependencies) { 23 | return purgeExcess(nodeModules, shrinkwrap, opts, cb); 24 | } 25 | 26 | var deps = shrinkwrap.dependencies; 27 | // console.log('shrinkwrap', shrinkwrap); 28 | var tasks = Object.keys(deps).map(function (key) { 29 | var dep = deps[key]; 30 | if (!dep.name) { 31 | dep.name = key; 32 | } 33 | var filePath = path.join(nodeModules, key); 34 | 35 | return isCorrect.bind(null, filePath, dep, opts); 36 | }); 37 | 38 | tasks.push(purgeExcess.bind( 39 | null, nodeModules, shrinkwrap, opts)); 40 | 41 | parallel(tasks, function (err, results) { 42 | if (err) { 43 | return cb(err); 44 | } 45 | 46 | opts.dev = false; 47 | 48 | // remove purgeExcess result 49 | results.pop(); 50 | 51 | var incorrects = results.filter(function (dep) { 52 | return !dep.correct; 53 | }); 54 | var corrects = results.filter(function (dep) { 55 | return dep.correct; 56 | }); 57 | 58 | /* for each incorrect 59 | 60 | - install it 61 | - remove excess 62 | - force install all children 63 | 64 | 65 | */ 66 | var inCorrectTasks = incorrects.map(function (incorrect) { 67 | var name = incorrect.name; 68 | var folder = path.join(nodeModules, 69 | name, 'node_modules'); 70 | 71 | return series.bind(null, [ 72 | installModule.bind( 73 | null, nodeModules, incorrect, opts), 74 | forceInstall.bind(null, folder, incorrect, opts) 75 | ]); 76 | }); 77 | var correctTasks = corrects.map(function (correct) { 78 | var name = correct.name; 79 | var folder = path.join(nodeModules, name, 80 | 'node_modules'); 81 | 82 | return forceInstall.bind( 83 | null, folder, correct, opts); 84 | }); 85 | 86 | /* for each correct 87 | 88 | - force install all children 89 | */ 90 | 91 | var tasks = [].concat(inCorrectTasks, correctTasks); 92 | 93 | parallel(tasks, cb); 94 | }); 95 | } 96 | 97 | 98 | function isCorrect(filePath, dep, opts, cb) { 99 | var createUri = opts.createUri || defaultCreateUri; 100 | 101 | dep.resolved = dep.resolved || 102 | createUri(dep.name, dep.version); 103 | 104 | var resolvedUri = url.parse(dep.resolved); 105 | 106 | if (resolvedUri.protocol === 'http:' || 107 | resolvedUri.protocol === 'https:' 108 | ) { 109 | return isCorrectVersion(filePath, dep, cb); 110 | } else if (resolvedUri.protocol === 'git:' || 111 | resolvedUri.protocol === 'git+ssh:' || 112 | resolvedUri.protocol === 'git+http:' || 113 | resolvedUri.protocol === 'git+https:' 114 | ) { 115 | isCorrectSHA(filePath, dep, cb); 116 | } else { 117 | cb(new Error('unsupported protocol ' + 118 | resolvedUri.protocol)); 119 | } 120 | } 121 | 122 | function isCorrectVersion(filePath, dep, cb) { 123 | var expectedVersion = dep.version; 124 | 125 | read.package(filePath, function (err, json) { 126 | if (err) { 127 | if (err && err.code === 'ENOENT') { 128 | dep.correct = false; 129 | return cb(null, dep); 130 | } else if (err && !err.code) { 131 | dep.correct = false; 132 | return cb(null, dep); 133 | } 134 | 135 | return cb(err); 136 | } 137 | 138 | var actualVersion = json.version; 139 | 140 | dep.correct = actualVersion === expectedVersion; 141 | cb(null, dep); 142 | }); 143 | } 144 | 145 | function isCorrectSHA(filePath, dep, cb) { 146 | var expectedSha = getSha(dep.resolved); 147 | 148 | read.package(filePath, function (err, json) { 149 | if (err) { 150 | if (err && err.code === 'ENOENT') { 151 | dep.correct = false; 152 | return cb(null, dep); 153 | } 154 | 155 | return cb(err); 156 | } 157 | 158 | // gaurd against malformed node_modules by forcing 159 | // a re-install 160 | if (!json._resolved) { 161 | dep.correct = false; 162 | return cb(null, dep); 163 | } 164 | 165 | var actualSha = getSha(json._resolved); 166 | 167 | dep.correct = actualSha === expectedSha; 168 | 169 | cb(null, dep); 170 | }); 171 | } 172 | 173 | function getSha(uri) { 174 | var parts = url.parse(uri); 175 | return parts.hash && parts.hash.substr(1); 176 | } 177 | 178 | function defaultCreateUri(name, version) { 179 | return template(NPM_URI, { 180 | name: name, 181 | version: version 182 | }); 183 | } 184 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # npm-shrinkwrap 2 | 3 | (This project is deprecated and not maintained.) 4 | 5 | A consistent shrinkwrap tool **Note: npm >= 3 is currently not supported.** 6 | 7 | ## Usage 8 | 9 | `$ npm-shrinkwrap` 10 | 11 | This runs shrinkwrap, which verifies your package.json & 12 | node_modules tree are in sync. If they are it runs shrinkwrap 13 | then fixes the resolved fields and trims from fields 14 | 15 | When you run `npm-shrinkwrap` it will either: 16 | 17 | - fail because your package.json & node_modules disagree, i.e. 18 | you installed something without `--save` or hand edited your 19 | package.json 20 | - succeed, and add all top level dependencies to your 21 | npm-shrinkwrap.json file and then runs `npm-shrinkwrap sync` 22 | which writes the npm-shrinkwrap.json back into node_modules 23 | 24 | ## Motivation 25 | 26 | ### Verify local correctness 27 | 28 | We need to verify that `package.json`, `npm-shrinkwrap.json` and 29 | `node_modules` all have the same content. 30 | 31 | Currently npm verifies most things but doesn't verify git 32 | completely. 33 | 34 | The edge case npm doesn't handle is if you change the tag in 35 | your package.json. npm happily says that the dependency in 36 | your node_modules tree is valid regardless of what tag it is. 37 | 38 | ### Consistently set a `resolved` field. 39 | 40 | NPM shrinkwrap serializes your node_modules folder. Depending 41 | on whether you installed a module from cache or not it will 42 | either have or not have a resolved field. 43 | 44 | `npm-shrinkwrap` will put a `resolved` field in for everything 45 | in your shrinkwrap. 46 | 47 | ### Reduce diff churn 48 | 49 | There are a few tricks to ensuring there is no unneeded churn 50 | in the output of `npm shrinkwrap`. 51 | 52 | This first is to ensure you install with `npm cache clean` so 53 | that an `npm ls` output is going to consistently give you the 54 | `resolved` and `from` fields. 55 | 56 | The second is to just delete all `from` fields from the 57 | generated shrinkwrap file since they change a lot but are 58 | never used. However you can only delete some `from` fields, 59 | not all. 60 | 61 | ### Human readable `diff` 62 | 63 | When you run shrinkwrap and check it into git you have an 64 | unreadable git diff. 65 | 66 | `npm-shrinkwrap` comes with an `npm-shrinkwrap diff` command. 67 | 68 | ```sh 69 | npm-shrinkwrap diff master HEAD 70 | npm-shrinkwrap diff HEAD npm-shrinkwrap.json --short 71 | ``` 72 | 73 | You can use this command to print out a readable context 74 | specific diff of your shrinkwrap changes. 75 | 76 | ### Custom shrinkwrap validators 77 | 78 | `npm-shrinkwrap` can be programmatically configured with an 79 | array of `validators`. 80 | 81 | These `validators` run over every node in the shrinkwrap file 82 | and can do assertions. 83 | 84 | Useful assertions are things like assertion all dependencies 85 | point at your private registry instead of the public one. 86 | 87 | ## Example 88 | 89 | ```js 90 | var npmShrinkwrap = require("npm-shrinkwrap"); 91 | 92 | npmShrinkwrap({ 93 | dirname: process.cwd() 94 | }, function (err, optionalWarnings) { 95 | if (err) { 96 | throw err; 97 | } 98 | 99 | optionalWarnings.forEach(function (err) { 100 | console.warn(err.message) 101 | }) 102 | 103 | console.log("wrote npm-shrinkwrap.json") 104 | }) 105 | ``` 106 | 107 | ## Algorithm 108 | 109 | npm-shrinkwrap algorithm 110 | 111 | - run `npm ls` to verify that node_modules & package.json 112 | agree. 113 | 114 | - run `verifyGit()` which has a similar algorithm to 115 | `npm ls` and will verify that node_modules & package.json 116 | agree for all git links. 117 | 118 | - read the old `npm-shrinkwrap.json` into memory 119 | 120 | - run `npm shrinkwrap` 121 | 122 | - copy over excess non-standard keys from old shrinkwrap 123 | into new shrinkwrap and write new shrinkwrap with extra 124 | keys to disk. 125 | 126 | - run `setResolved()` which will ensure that the new 127 | npm-shrinkwrap.json has a `"resolved"` field for every 128 | package and writes it to disk. 129 | 130 | - run `trimFrom()` which normalizes or removes the `"from"` 131 | field from the new npm-shrinkwrap.json. It also sorts 132 | the new npm-shrinkwrap.json deterministically then 133 | writes that to disk 134 | 135 | - run `trimNested()` which will trim any changes in the 136 | npm-shrinkwrap.json to dependencies at depth >=1. i.e. 137 | any changes to nested dependencies without changes to 138 | the direct parent dependency just get deleted 139 | 140 | - run `sync()` to the new `npm-shrinkwrap.json` back into 141 | the `node_modules` folder 142 | 143 | 144 | npm-shrinkwrap NOTES: 145 | 146 | - `verifyGit()` only has a depth of 0, where as `npm ls` 147 | has depth infinity. 148 | 149 | - `verifyGit()` is only sound for git tags. This means that 150 | for non git tags it gives warnings / errors instead. 151 | 152 | - `trimFrom()` also sorts and rewrites the package.json 153 | for consistency 154 | 155 | - By default, the npm-shrinkwrap algorithm does not dedupe 156 | nested dependencies. This means that the shrinkwrap is 157 | closer to the installed dependencies by default. If this 158 | is not desired `--keepNested=false` can be passed to the 159 | shrinkwrap cli 160 | 161 | ## Cli Documentation 162 | 163 | ### `npm-shrinkwrap [options]` 164 | 165 | Verifies your `package.json` and `node_modules` are in sync. 166 | Then runs `npm shrinkwrap` and cleans up the 167 | `npm-shrinkwrap.json` file to be consistent. 168 | 169 | Basically like `npm shrinkwrap` but better 170 | 171 | ``` 172 | Options: 173 | --dirname sets the directory location of the package.json 174 | defaults to `process.cwd()`. 175 | --keep-nested If set, will not remove nested changes. 176 | --warnOnNotSemver If set, will downgrade invalid semver errors 177 | to warnings 178 | --dev If set, will shrinkwrap dev dependencies 179 | --silent If set, will be silent. 180 | ``` 181 | 182 | #### `npm-shrinkwrap --help` 183 | 184 | Prints this message 185 | 186 | #### `npm-shrinkwrap sync` 187 | 188 | Syncs your `npm-shrinkwrap.json` file into the `node_modules` 189 | directory. 190 | 191 | This will ensure that your local `node_modules` matches the 192 | `npm-shrinkwrap.json` file verbatim. Any excess modules in 193 | your node_modules folder will be removed if they are not in 194 | the `npm-shrinkwrap.json` file. 195 | 196 | Options: 197 | --dirname sets the directory of the npm-shrinkwrap.json 198 | 199 | - `--dirname` defaults to `process.cwd()` 200 | 201 | #### `npm-shrinkwrap install` 202 | 203 | Will write a `shrinkwrap` script to your `package.json` file. 204 | 205 | ```json 206 | { 207 | "scripts": { 208 | "shrinkwrap": "npm-shrinkwrap" 209 | } 210 | } 211 | ``` 212 | 213 | Options: 214 | --dirname sets the directory location of the package.json 215 | 216 | #### `npm-shrinkwrap diff [OldShaOrFile] [NewShaOrfile]` 217 | 218 | This will show a human readable for the shrinkwrap file. 219 | 220 | You can pass it either a path to a file or a git shaism. 221 | 222 | Example: 223 | 224 | ``` 225 | npm-shrinkwrap diff HEAD npm-shrinkwrap.json 226 | npm-shrinkwrap diff origin/master HEAD 227 | ``` 228 | 229 | ``` 230 | Options: 231 | --depth configure the depth at which it prints 232 | --short when set it will print add/remove tersely 233 | --dirname configure which folder to run within 234 | ``` 235 | 236 | - `--depth` defaults to `0` 237 | - `--short` defaults to `false` 238 | - `--dirname` defaults to `process.cwd()` 239 | 240 | ## Installation 241 | 242 | For usage with npm@2 243 | 244 | `npm install npm-shrinkwrap` 245 | 246 | For usage with npm@1 247 | 248 | `npm install npm-shrinkwrap@100.x` 249 | 250 | **Note: npm >= 3 is not supported.** 251 | 252 | ## Tests 253 | 254 | `npm test` 255 | 256 | ## Contributors 257 | 258 | - Raynos 259 | -------------------------------------------------------------------------------- /trim-and-sort-shrinkwrap.js: -------------------------------------------------------------------------------- 1 | var fs = require('graceful-fs'); 2 | var path = require('path'); 3 | var url = require('url'); 4 | var safeJsonParse = require('safe-json-parse'); 5 | var parallel = require('run-parallel'); 6 | var sortedObject = require('sorted-object'); 7 | var readJSON = require('./read-json'); 8 | 9 | var errors = require('./errors.js'); 10 | 11 | module.exports = trimFrom; 12 | 13 | // set keys in an order 14 | function sortedKeys(obj, orderedKeys) { 15 | var keys = Object.keys(obj).sort(); 16 | var fresh = {}; 17 | 18 | orderedKeys.forEach(function (key) { 19 | if (keys.indexOf(key) === -1) { 20 | return; 21 | } 22 | 23 | fresh[key] = obj[key]; 24 | }); 25 | 26 | keys.forEach(function (key) { 27 | if (orderedKeys.indexOf(key) !== -1) { 28 | return; 29 | } 30 | 31 | fresh[key] = obj[key]; 32 | }); 33 | 34 | return fresh; 35 | } 36 | 37 | function recursiveSorted(json) { 38 | if (!json) { 39 | return json; 40 | } 41 | 42 | var deps = json.dependencies; 43 | if (typeof deps === 'object' && deps !== null) { 44 | json.dependencies = Object.keys(deps) 45 | .reduce(function (acc, key) { 46 | acc[key] = recursiveSorted(deps[key]); 47 | return acc; 48 | }, {}); 49 | json.dependencies = sortedObject(json.dependencies); 50 | } 51 | 52 | return sortedKeys(json, [ 53 | 'name', 54 | 'version', 55 | 'from', 56 | 'resolved', 57 | 'npm-shrinkwrap-version', 58 | 'node-version', 59 | 'dependencies' 60 | ]); 61 | 62 | } 63 | 64 | function trimFrom(opts, callback) { 65 | if (typeof opts === 'string') { 66 | opts = { dirname: opts }; 67 | } 68 | 69 | var shrinkwrapFile = path.join(opts.dirname, 'npm-shrinkwrap.json'); 70 | var registries = opts.registries || ['registry.npmjs.org']; 71 | 72 | parallel([ 73 | fixShrinkwrap, 74 | fixPackage.bind(null, opts.dirname) 75 | ], callback); 76 | 77 | 78 | function fixShrinkwrap(callback) { 79 | fs.readFile(shrinkwrapFile, 'utf8', function (err, file) { 80 | if (err) { 81 | return callback(err); 82 | } 83 | 84 | if (file === '') { 85 | return callback(errors.EmptyFile()); 86 | } 87 | 88 | safeJsonParse(file, function (err, json) { 89 | if (err) { 90 | return callback(err); 91 | } 92 | 93 | json = recursiveSorted(json); 94 | 95 | json = replaceFields(json, replacer); 96 | 97 | fs.writeFile(shrinkwrapFile, 98 | JSON.stringify(json, null, 2) + '\n', callback); 99 | }); 100 | }); 101 | } 102 | 103 | function replaceFields(json, replacer, name) { 104 | name = name || 'root'; 105 | 106 | if (json.from) { 107 | json.from = replacer.call(json, 108 | 'from', json.from, name); 109 | } 110 | 111 | if (json.dependencies) { 112 | Object.keys(json.dependencies) 113 | .forEach(recurse); 114 | } 115 | 116 | return json; 117 | 118 | function recurse(name) { 119 | json.dependencies[name] = replaceFields( 120 | json.dependencies[name], 121 | replacer, 122 | name); 123 | } 124 | } 125 | 126 | function fixFromField(opts) { 127 | var shaIsm = opts.fromUri.hash && 128 | opts.fromUri.hash.slice(1); 129 | 130 | // from does not have shaIsm. bail early 131 | if (!shaIsm) { 132 | return opts.name + '@' + opts.fromValue; 133 | } 134 | 135 | var resolvedUri = url.parse(opts.resolvedValue); 136 | var resolveShaism = resolvedUri.hash && 137 | resolvedUri.hash.slice(1); 138 | 139 | // resolved does not have shaIsm. bail early 140 | if (!resolveShaism) { 141 | return opts.name + '@' + opts.fromValue; 142 | } 143 | 144 | // replace the from shaIsm with the resolved shaIsm 145 | if (shaIsm !== resolveShaism) { 146 | var pathname = opts.fromUri.pathname; 147 | // normalize git+ssh links with a ':' after the host instead of a '/' 148 | if (pathname[1] === ':') { 149 | pathname = pathname[0] + pathname.slice(2); 150 | } 151 | var newValue = url.format({ 152 | protocol: opts.fromUri.protocol, 153 | slashes: opts.fromUri.slashes, 154 | auth: opts.fromUri.auth, 155 | host: opts.fromUri.host, 156 | pathname: pathname, 157 | hash: resolveShaism 158 | }); 159 | return opts.name + '@' + newValue; 160 | } 161 | 162 | return opts.name + '@' + opts.fromValue; 163 | } 164 | 165 | /* trims the `from` field from `npm-shrinkwrap.json` files. 166 | 167 | The `from` field is likely to change because different npm 168 | clients do different things and general non determinism. 169 | 170 | The `from` field is not really important since the `resolved` 171 | and `version` fields are mostly used. 172 | 173 | The only situations in which `from` is used is non npm links 174 | (i.e. git, git+ssh and https tarbal links) and situations 175 | where there is no `resolved` field. 176 | */ 177 | function replacer(key, value, name) { 178 | if (key !== 'from') { 179 | return value; 180 | } 181 | 182 | var resolved = this.resolved; 183 | 184 | // if this dependency has no `resolved` field then it's not 185 | // safe to remove the `from` field since `npm install` will 186 | // use it. 187 | if (!resolved) { 188 | return value; 189 | } 190 | 191 | var uri = url.parse(value); 192 | 193 | // if it's a `http:` link to registry its safe 194 | // to remove as `from` is not really used 195 | if ((uri.protocol === 'http:' || uri.protocol === 'https:') && 196 | registries.indexOf(uri.host) !== -1 197 | ) { 198 | return undefined; 199 | // if it's any other link, like `git`, `git+ssh` or a http 200 | // link to an arbitrary tarball then we cant remove it 201 | } else if (uri.protocol) { 202 | // for resolve branches & shaisms to commit shas 203 | // we should always have `from` contain a git sha 204 | // because that's consistent 205 | 206 | return fixFromField({ 207 | fromUri: uri, 208 | name: name, 209 | fromValue: value, 210 | resolvedValue: resolved 211 | }); 212 | } 213 | 214 | // otherwise the `value` is in the format `name@semverish` 215 | 216 | var parts = value.split('@'); 217 | var rest = parts.slice(1).join('@'); 218 | 219 | // parse the `semverish` part of the `from` field value. 220 | var secondUri = url.parse(rest); 221 | 222 | // if it's an uri instead of a `semverish` then it's not 223 | // safe to remove the `from` field 224 | // However if it is NOT an uri then its safe to remove 225 | if (!secondUri.protocol) { 226 | return undefined; 227 | } 228 | 229 | return fixFromField({ 230 | fromUri: secondUri, 231 | fromValue: rest, 232 | name: name, 233 | resolvedValue: resolved 234 | }); 235 | } 236 | } 237 | 238 | function fixPackage(dirname, callback) { 239 | var packageJsonFile = path.join(dirname, 'package.json'); 240 | readJSON(packageJsonFile, function (err, json) { 241 | if (err) { 242 | return callback(err); 243 | } 244 | 245 | if (json.dependencies) { 246 | json.dependencies = sortedObject(json.dependencies); 247 | } 248 | if (json.devDependencies) { 249 | json.devDependencies = sortedObject(json.devDependencies); 250 | } 251 | 252 | var data = JSON.stringify(json, null, 2) + '\n'; 253 | fs.writeFile(packageJsonFile, data, callback); 254 | }); 255 | } 256 | 257 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | var ValidationError = require('error/validation'); 2 | var find = require('array-find'); 3 | var path = require('path'); 4 | var fs = require('graceful-fs'); 5 | var sortedObject = require('sorted-object'); 6 | var readJSON = require('./read-json'); 7 | 8 | var setResolved = require('./set-resolved.js'); 9 | var trimFrom = require('./trim-and-sort-shrinkwrap.js'); 10 | var verifyGit = require('./verify-git.js'); 11 | var walkDeps = require('./walk-shrinkwrap.js'); 12 | var trimNested = require('./trim-nested.js'); 13 | var sync = require('./sync/'); 14 | var ERRORS = require('./errors.js'); 15 | 16 | /* npm-shrinkwrap algorithm 17 | 18 | - run `npm ls` to verify that node_modules & package.json 19 | agree. 20 | 21 | - run `verifyGit()` which has a similar algorithm to 22 | `npm ls` and will verify that node_modules & package.json 23 | agree for all git links. 24 | 25 | - read the old `npm-shrinkwrap.json` into memory 26 | 27 | - run `npm shrinkwrap` 28 | 29 | - copy over excess non-standard keys from old shrinkwrap 30 | into new shrinkwrap and write new shrinkwrap with extra 31 | keys to disk. 32 | 33 | - run `setResolved()` which will ensure that the new 34 | npm-shrinkwrap.json has a `"resolved"` field for every 35 | package and writes it to disk. 36 | 37 | - run `trimFrom()` which normalizes or removes the `"from"` 38 | field from the new npm-shrinkwrap.json. It also sorts 39 | the new npm-shrinkwrap.json deterministically then 40 | writes that to disk 41 | 42 | - run `trimNested()` which will trim any changes in the 43 | npm-shrinkwrap.json to dependencies at depth >=1. i.e. 44 | any changes to nested dependencies without changes to 45 | the direct parent dependency just get deleted 46 | 47 | - run `sync()` to the new `npm-shrinkwrap.json` back into 48 | the `node_modules` folder 49 | 50 | 51 | npm-shrinkwrap NOTES: 52 | 53 | - `verifyGit()` only has a depth of 0, where as `npm ls` 54 | has depth infinity. 55 | 56 | - `verifyGit()` is only sound for git tags. This means that 57 | for non git tags it gives warnings / errors instead. 58 | 59 | - `trimFrom()` also sorts and rewrites the package.json 60 | for consistency 61 | 62 | */ 63 | 64 | function npmShrinkwrap(opts, callback) { 65 | if (typeof opts === 'string') { 66 | opts = { dirname: opts }; 67 | } 68 | 69 | var _warnings = null; 70 | var _oldShrinkwrap = null; 71 | 72 | getNPM().load({ 73 | prefix: opts.dirname, 74 | dev: opts.dev, 75 | loglevel: 'error' 76 | }, verifyTree); 77 | 78 | function verifyTree(err, npm) { 79 | if (err) { 80 | return callback(err); 81 | } 82 | 83 | // when running under `npm test` depth is set to 1 84 | // reset it to a high number like 100 85 | npm.config.set('depth', 100); 86 | 87 | npm.commands.ls([], true, onls); 88 | 89 | function onls(err, _, pkginfo) { 90 | if (err) { 91 | return callback(err); 92 | } 93 | 94 | if (pkginfo.problems) { 95 | var error = NPMError(pkginfo); 96 | return callback(error); 97 | } 98 | 99 | verifyGit(opts, onverify); 100 | } 101 | 102 | function onverify(err, errors) { 103 | if (err) { 104 | return callback(err); 105 | } 106 | 107 | if (errors.length === 0) { 108 | return onnpm(null, npm); 109 | } 110 | 111 | var error = ValidationError(errors); 112 | var invalid = find(errors, function (error) { 113 | return error.type === 'invalid.git.version'; 114 | }); 115 | 116 | if (invalid) { 117 | error = ERRORS.InvalidVersionsNPMError({ 118 | actual: invalid.actual, 119 | name: invalid.name, 120 | dirname: invalid.dirname, 121 | errors: error.errors 122 | }); 123 | } 124 | 125 | var types = errors.reduce(function (acc, e) { 126 | if (acc.indexOf(e.type) === -1) { 127 | acc.push(e.type); 128 | } 129 | 130 | return acc; 131 | }, []); 132 | 133 | if (opts.warnOnNotSemver && types.length === 1 && 134 | types[0] === 'gitlink.tag.notsemver' 135 | ) { 136 | _warnings = error.errors; 137 | return onnpm(null, npm); 138 | } 139 | 140 | callback(error); 141 | } 142 | } 143 | 144 | function onnpm(err, npm) { 145 | if (err) { 146 | return callback(err); 147 | } 148 | 149 | var fileName = path.join(opts.dirname, 'npm-shrinkwrap.json'); 150 | readJSON(fileName, onfile); 151 | 152 | function onfile(err, oldShrinkwrap) { 153 | if (err) { 154 | // if no npm-shrinkwrap.json exists then just 155 | // create one 156 | npm.commands.shrinkwrap({}, true, onshrinkwrap); 157 | return; 158 | } 159 | 160 | _oldShrinkwrap = oldShrinkwrap; 161 | 162 | /* npm.commands.shrinkwrap will blow away any 163 | extra keys that you set. 164 | 165 | We have to read extra keys & set them again 166 | after shrinkwrap is done 167 | */ 168 | var keys = Object.keys(oldShrinkwrap) 169 | .filter(function (k) { 170 | return [ 171 | 'name', 'version', 'dependencies' 172 | ].indexOf(k) === -1; 173 | }); 174 | 175 | npm.commands.shrinkwrap({}, true, onwrapped); 176 | 177 | function onwrapped(err) { 178 | if (err) { 179 | return callback(err); 180 | } 181 | 182 | readJSON(fileName, onnewfile); 183 | } 184 | 185 | function onnewfile(err, newShrinkwrap) { 186 | if (err) { 187 | return callback(err); 188 | } 189 | 190 | keys.forEach(function (k) { 191 | if (!newShrinkwrap[k]) { 192 | newShrinkwrap[k] = oldShrinkwrap[k]; 193 | } 194 | }); 195 | 196 | newShrinkwrap = sortedObject(newShrinkwrap); 197 | 198 | var buf = JSON.stringify(newShrinkwrap, null, 2) + '\n'; 199 | fs.writeFile(fileName, buf, 'utf8', onshrinkwrap); 200 | } 201 | } 202 | } 203 | 204 | function onshrinkwrap(err) { 205 | if (err) { 206 | return callback(err); 207 | } 208 | 209 | setResolved(opts, onResolved); 210 | } 211 | 212 | function onResolved(err) { 213 | if (err) { 214 | return callback(err); 215 | } 216 | 217 | trimFrom(opts, ontrim); 218 | } 219 | 220 | function ontrim(err) { 221 | if (err) { 222 | return callback(err); 223 | } 224 | 225 | var fileName = path.join(opts.dirname, 226 | 'npm-shrinkwrap.json'); 227 | readJSON(fileName, function (err, newShrinkwrap) { 228 | if (err) { 229 | return callback(err); 230 | } 231 | 232 | if (_oldShrinkwrap) { 233 | newShrinkwrap = trimNested(_oldShrinkwrap, 234 | newShrinkwrap, opts); 235 | } 236 | 237 | var buf = JSON.stringify(newShrinkwrap, null, 2) + '\n'; 238 | fs.writeFile(fileName, buf, 'utf8', function (err) { 239 | if (err) { 240 | return callback(err); 241 | } 242 | 243 | readJSON(fileName, onfinalwrap); 244 | }); 245 | }); 246 | } 247 | 248 | function onfinalwrap(err, shrinkwrap) { 249 | if (err) { 250 | return callback(err); 251 | } 252 | 253 | sync(opts, function (err) { 254 | if (err) { 255 | return callback(err); 256 | } 257 | 258 | onsync(null, shrinkwrap); 259 | }); 260 | } 261 | 262 | function onsync(err, shrinkwrap) { 263 | if (err) { 264 | return callback(err); 265 | } 266 | 267 | var warnings = _warnings ? _warnings : []; 268 | var errors = []; 269 | 270 | if (opts.validators && Array.isArray(opts.validators) && 271 | opts.validators.length !== 0 272 | ) { 273 | walkDeps(shrinkwrap, function (node, key, parent) { 274 | var errs = opts.validators.map(function (f) { 275 | return f(node, key, parent); 276 | }).filter(Boolean); 277 | 278 | if (errs.length) { 279 | errors = errors.concat(errs); 280 | } 281 | }); 282 | } 283 | 284 | if (errors.length) { 285 | return callback(ValidationError(errors), warnings); 286 | } 287 | 288 | callback(null, warnings); 289 | } 290 | } 291 | 292 | module.exports = npmShrinkwrap; 293 | 294 | /* you cannot call `npm.load()` twice with different prefixes. 295 | 296 | The only fix is to clear the entire node require cache and 297 | get a fresh duplicate copy of the entire npm library 298 | */ 299 | function getNPM() { 300 | Object.keys(require.cache).forEach(function (key) { 301 | delete require.cache[key]; 302 | }); 303 | var NPM = require('npm'); 304 | return NPM; 305 | } 306 | 307 | function NPMError(pkginfo) { 308 | var problemsText = pkginfo.problems.join('\n'); 309 | 310 | return ERRORS.NPMError({ 311 | pkginfo: pkginfo, 312 | problemsText: problemsText 313 | }); 314 | } 315 | -------------------------------------------------------------------------------- /test/npm-shrinkwrap.js: -------------------------------------------------------------------------------- 1 | var test = require('tape'); 2 | var fixtures = require('fixtures-fs'); 3 | var path = require('path'); 4 | var fs = require('graceful-fs'); 5 | 6 | var npmShrinkwrap = require('../index.js'); 7 | 8 | var PROJ = path.join(__dirname, 'proj'); 9 | var SHA = 'e8db5304e8e527aa17093cd9de66725118d9b589'; 10 | 11 | function moduleFixture(name, version, opts) { 12 | opts = opts || {}; 13 | 14 | var module = { 15 | 'package.json': JSON.stringify({ 16 | name: name, 17 | _id: name + '@' + version, 18 | _from: name + '@^' + version, 19 | version: version, 20 | dependencies: opts.dependencies ? 21 | opts.dependencies : undefined 22 | }) 23 | }; 24 | 25 | /*jshint camelcase: false*/ 26 | if (opts.node_modules) { 27 | module.node_modules = opts.node_modules; 28 | } 29 | 30 | return module; 31 | } 32 | 33 | function gitModuleFixture(name, version, opts) { 34 | opts = opts || {}; 35 | 36 | var module = { 37 | 'package.json': JSON.stringify({ 38 | name: name, 39 | _id: name + '@' + version, 40 | _from: 'git://github.com/uber/' + name + '#v' + version, 41 | _resolved: 'git://github.com/uber/' + name + '#' + SHA, 42 | version: version, 43 | dependencies: opts.dependencies ? 44 | opts.dependencies : undefined 45 | }) 46 | }; 47 | 48 | /*jshint camelcase: false*/ 49 | if (opts.node_modules) { 50 | module.node_modules = opts.node_modules; 51 | } 52 | 53 | return module; 54 | } 55 | 56 | function gitSSHModuleFixture(name, version, opts) { 57 | opts = opts || {}; 58 | 59 | var module = { 60 | 'package.json': JSON.stringify({ 61 | name : name, 62 | _id: name + '@' + version, 63 | _from: 'git+ssh://git@github.com:uber/' + name + '#v' + version, 64 | _resolved: 'git+ssh://git@github.com/uber/' + name + '#' + SHA, 65 | version: version, 66 | dependencies: opts.dependencies || undefined 67 | }) 68 | }; 69 | 70 | /*jshint camelcase: false*/ 71 | if (opts.node_modules) { 72 | module.node_modules = opts.node_modules; 73 | } 74 | 75 | return module; 76 | } 77 | 78 | test('npmShrinkwrap is a function', function (assert) { 79 | assert.strictEqual(typeof npmShrinkwrap, 'function'); 80 | assert.end(); 81 | }); 82 | 83 | test('creates simple shrinkwrap', fixtures(__dirname, { 84 | 'proj': moduleFixture('proj', '0.1.0', { 85 | dependencies: { 86 | foo: '2.0.0' 87 | }, 88 | 'node_modules': { 89 | 'foo': moduleFixture('foo', '2.0.0') 90 | } 91 | }) 92 | }, function (assert) { 93 | npmShrinkwrap(PROJ, function (err) { 94 | assert.ifError(err); 95 | 96 | var shrinkwrap = path.join(PROJ, 'npm-shrinkwrap.json'); 97 | fs.readFile(shrinkwrap, 'utf8', function (err, file) { 98 | assert.ifError(err); 99 | assert.notEqual(file, ''); 100 | 101 | var json = JSON.parse(file); 102 | 103 | assert.equal(json.name, 'proj'); 104 | assert.equal(json.version, '0.1.0'); 105 | assert.deepEqual(json.dependencies, { 106 | foo: { 107 | version: '2.0.0', 108 | resolved: 'https://registry.npmjs.org/foo/-/foo-2.0.0.tgz' 109 | } 110 | }); 111 | 112 | assert.end(); 113 | }); 114 | }); 115 | })); 116 | 117 | test('create shrinkwrap for git dep', fixtures(__dirname, { 118 | 'proj': moduleFixture('proj', '0.1.0', { 119 | dependencies: { 120 | bar: 'git+ssh://git@github.com:uber/bar#v2.0.0' 121 | }, 122 | 'node_modules': { 123 | 'bar': gitModuleFixture('bar', '2.0.0') 124 | } 125 | }) 126 | }, function (assert) { 127 | npmShrinkwrap(PROJ, function (err) { 128 | assert.ifError(err); 129 | 130 | var shrinkwrap = path.join(PROJ, 'npm-shrinkwrap.json'); 131 | fs.readFile(shrinkwrap, 'utf8', function (err, file) { 132 | assert.ifError(err); 133 | assert.notEqual(file, ''); 134 | 135 | var json = JSON.parse(file); 136 | 137 | assert.equal(json.name, 'proj'); 138 | assert.equal(json.version, '0.1.0'); 139 | assert.deepEqual(json.dependencies, { 140 | bar: { 141 | version: '2.0.0', 142 | from: 'bar@git://github.com/uber/bar#' + SHA, 143 | resolved: 'git://github.com/uber/bar#' + SHA 144 | } 145 | }); 146 | 147 | assert.end(); 148 | }); 149 | }); 150 | })); 151 | 152 | test('create shrinkwrap for git+ssh dep', fixtures(__dirname, { 153 | 'proj': moduleFixture('proj', '0.1.0', { 154 | dependencies: { 155 | baz: 'git+ssh://git@github.com:uber/baz#v2.0.0' 156 | }, 157 | 'node_modules': { 158 | 'baz': gitSSHModuleFixture('baz', '2.0.0') 159 | } 160 | }) 161 | }, function (assert) { 162 | npmShrinkwrap(PROJ, function (err) { 163 | assert.ifError(err); 164 | 165 | var shrinkwrap = path.join(PROJ, 'npm-shrinkwrap.json'); 166 | fs.readFile(shrinkwrap, 'utf8', function (err, file) { 167 | assert.ifError(err); 168 | assert.notEqual(file, ''); 169 | 170 | var json = JSON.parse(file); 171 | 172 | assert.equal(json.name, 'proj'); 173 | assert.equal(json.version, '0.1.0'); 174 | assert.deepEqual(json.dependencies, { 175 | baz: { 176 | version: '2.0.0', 177 | from: 'baz@git+ssh://git@github.com/uber/baz#' + SHA, 178 | resolved: 'git+ssh://git@github.com/uber/baz#' + SHA 179 | } 180 | }); 181 | 182 | assert.end(); 183 | }); 184 | }); 185 | })); 186 | 187 | test('error on removed module', fixtures(__dirname, { 188 | proj: moduleFixture('proj', '0.1.0', { 189 | dependencies: {}, 190 | 'node_modules': { 191 | 'foo': moduleFixture('foo', '1.0.0') 192 | } 193 | }) 194 | }, function (assert) { 195 | // debugger; 196 | npmShrinkwrap(PROJ, function (err) { 197 | assert.ok(err); 198 | 199 | assert.notEqual(err.message.indexOf( 200 | 'extraneous: foo@1.0.0'), -1); 201 | 202 | assert.end(); 203 | }); 204 | })); 205 | 206 | test('error on additional module', fixtures(__dirname, { 207 | proj: moduleFixture('proj', '0.1.0', { 208 | dependencies: { 'foo': '1.0.0' }, 209 | 'node_modules': {} 210 | }) 211 | }, function (assert) { 212 | npmShrinkwrap(PROJ, function (err) { 213 | assert.ok(err); 214 | 215 | assert.notEqual(err.message.indexOf( 216 | 'missing: foo@1.0.0'), -1); 217 | 218 | assert.end(); 219 | }); 220 | })); 221 | 222 | test('error on invalid module', fixtures(__dirname, { 223 | proj: moduleFixture('proj', '0.1.0', { 224 | dependencies: { 'foo': '1.0.1' }, 225 | 'node_modules': { 226 | 'foo': moduleFixture('foo', '1.0.0') 227 | } 228 | }) 229 | }, function (assert) { 230 | npmShrinkwrap(PROJ, function (err) { 231 | assert.ok(err); 232 | 233 | assert.notEqual(err.message.indexOf( 234 | 'invalid: foo@1.0.0'), -1); 235 | 236 | assert.end(); 237 | }); 238 | })); 239 | 240 | test('error on removed GIT module', fixtures(__dirname, { 241 | proj: moduleFixture('proj', '0.1.0', { 242 | dependencies: {}, 243 | 'node_modules': { 244 | 'foo': gitModuleFixture('foo', '1.0.0') 245 | } 246 | }) 247 | }, function (assert) { 248 | // debugger; 249 | npmShrinkwrap(PROJ, function (err) { 250 | assert.ok(err); 251 | 252 | assert.notEqual(err.message.indexOf( 253 | 'extraneous: foo@1.0.0'), -1); 254 | 255 | assert.end(); 256 | }); 257 | })); 258 | 259 | test('error on additional GIT module', fixtures(__dirname, { 260 | proj: moduleFixture('proj', '0.1.0', { 261 | dependencies: { 262 | 'foo': 'git://git@github.com:uber/foo#v1.0.0' 263 | }, 264 | 'node_modules': {} 265 | }) 266 | }, function (assert) { 267 | npmShrinkwrap(PROJ, function (err) { 268 | assert.ok(err); 269 | 270 | assert.notEqual(err.message.indexOf( 271 | 'missing: foo@git+ssh://git@github.com/uber/foo.git#v1.0.0'), -1); 272 | 273 | assert.end(); 274 | }); 275 | })); 276 | 277 | test('error on invalid GIT module', fixtures(__dirname, { 278 | proj: moduleFixture('proj', '0.1.0', { 279 | dependencies: { 280 | 'foo': 'git://git@github.com:uber/foo#v1.0.1' 281 | }, 282 | 'node_modules': { 283 | 'foo': gitModuleFixture('foo', '1.0.0') 284 | } 285 | }) 286 | }, function (assert) { 287 | npmShrinkwrap(PROJ, function (err) { 288 | assert.ok(err); 289 | 290 | assert.notEqual(err ? err.message.indexOf( 291 | 'invalid: foo@1.0.0') : -1, -1); 292 | 293 | assert.end(); 294 | }); 295 | })); 296 | 297 | test('create shrinkwrap for scoped package', fixtures(__dirname, { 298 | proj: moduleFixture('proj', '0.1.0', { 299 | dependencies: { 300 | '@uber/foo': '1.0.0' 301 | }, 302 | 'node_modules': { 303 | '@uber': { 304 | 'foo': moduleFixture('@uber/foo', '1.0.0') 305 | } 306 | } 307 | }) 308 | }, function (assert) { 309 | npmShrinkwrap(PROJ, function (err) { 310 | assert.ifError(err); 311 | 312 | var shrinkwrap = path.join(PROJ, 'npm-shrinkwrap.json'); 313 | fs.readFile(shrinkwrap, 'utf8', function (err, file) { 314 | assert.ifError(err); 315 | assert.notEqual(file, ''); 316 | 317 | var json = JSON.parse(file); 318 | 319 | assert.equal(json.name, 'proj'); 320 | assert.equal(json.version, '0.1.0'); 321 | assert.deepEqual(json.dependencies, { 322 | '@uber/foo': { 323 | version: '1.0.0', 324 | resolved: 'https://registry.npmjs.org/@uber/foo/-/@uber/foo-1.0.0.tgz' 325 | } 326 | }); 327 | 328 | assert.end(); 329 | }); 330 | }); 331 | })); 332 | --------------------------------------------------------------------------------