UNPKG

npm-update-outdated

Version:

Module for updating outdated NPM modules based on semver specs.

380 lines (317 loc) 8.29 kB
'use strict'; var exec = require('child_process').exec, events = require('events'), util = require('util'), when = require('when'), seq = require('when/sequence'), nodefn = require('when/node'), pExec = nodefn.lift(exec), commander = require('commander'), pkg = require('../package.json'); function NPMUpdateOutdated() { this.modules = {}; this.outdated = {}; this.missing = {}; this.saveOption = null; this.format = 'parseable'; this.update_to = 'wanted'; this.update_missing = false; this.auto_update = false; this.filter = null; this.args = []; // Private, but bound, methods _parseParseableInput = _parseParseableInput.bind(this); _updateModule = _updateModule.bind(this); } util.inherits(NPMUpdateOutdated, events.EventEmitter); NPMUpdateOutdated.prototype.Load = function(mods) { try { this.modules = _parseParseableInput(mods); } catch (e) { throw new Error('Failed to parse input data: ' + e.stack); } }; NPMUpdateOutdated.prototype.SetFilter = function(v) { this.filter = v; return this; }; NPMUpdateOutdated.prototype.SetAutoUpdate = function(v) { this.auto_update = !!v; // coerce to boolean return this; }; NPMUpdateOutdated.prototype.SetUpdateTo = function(v) { this.update_to = v; return this; } NPMUpdateOutdated.prototype.SetInstallMissing = function(v) { this.install_missing = v; return this; }; NPMUpdateOutdated.prototype.SetSaveOption = function(v) { this.saveOption = v; return this; } NPMUpdateOutdated.prototype.GetOutdated = function(filter, options) { var mods = this.modules, update_to, depth, pattern, filter = filter || this.filter; // Update this.update_to if options.update_to is either "wanted" or "latest" if (options && options.hasOwnProperty('update_to')) { if (['wanted', 'latest'].indexOf(options.update_to) !== -1) { this.update_to = options.update_to; } } if (filter) { pattern = new RegExp(filter); } for (var mod in mods) { if (!pattern || (pattern.test(mod))) { depth = mods[mod].location.split('/').length; if (depth === 2) { if (!mods[mod].current || mods[mod].current === 'MISSING') { this.missing[mod] = mods[mod]; } else if (mods[mod].current && (mods[mod].current !== mods[mod][this.update_to])) { this.outdated[mod] = mods[mod]; } } } } return this.outdated; }; NPMUpdateOutdated.prototype.InstallMissing = function(mods) { var mods = this.missing; if (this.install_missing === true) { return this.UpdateOutdated(mods, 'missing'); } return; }; NPMUpdateOutdated.prototype.UpdateOutdated = function(mods, type) { var that = this, type = type || 'outdated', current, wanted, latest, mods = mods || this.outdated, mod, modsLen, pChain = []; if (!mods || mods.constructor !== Object ) { console.log('Invalid or empty input: ' + JSON.stringify(mods)); return; } if ((modsLen = Object.keys(mods).length) === 0) { that.emit(type + '_end'); } else { _updateModules(mods, type) .then(function(resp) { that.emit(type + '_end', resp); }) .catch(function(err) { console.error(err); that.emit('error', err); }); } }; NPMUpdateOutdated.prototype.CheckOutdated = function(mods, type) { var m, printedHead = false, success = true; Object.keys(mods).forEach(function(v, k) { var obj = mods[v]; if (obj.current !== obj.wanted) { success = false; } if (!printedHead && !success) { printedHead = true; console.error(' '); console.error('# ======================================================'); console.error('# OH NOES!'); console.error('# The following top-level modules are outdated:'); } console.error("# \t" + obj.name + ': Current ' + obj.current + '; Wanted ' + obj.wanted); }); if (!success) { console.error('# '); console.error('# You can fix this by running the "npm-update-outdated" command. '); console.error('# '); console.error('# ======================================================'); console.error(' '); } return (success ? 0 : 1); }; /*********************************************************/ /* private methods /** * Parse this.parseableInput i.e. the format produced by running "npm outdated --parseable" * e.g. * /usr/local/lib/node_modules/istanbul/node_modules/escodegen:escodegen@1.3.3:escodegen@1.3.2:escodegen@1.4.1 * * */ function _parseParseableInput(input, isGlobal) { var lines = input.split("\n"), mods = {}, mod, modStr, split, splitVersions, global = isGlobal || false; lines.forEach(function(line) { if (line.length) { modStr = line.split(process.cwd() + '/node_modules/').pop(); // This version only supports modules in the top-level node_modules // directory, and not sub modules if (modStr.indexOf('node_modules') === -1) { mod = new Version().SetFromParseableString(modStr); mods[mod.name] = mod; } } }); return mods; } function _updateModules(mods, type) { var current, wanted, mod, pChain = when(function() { return mods; }); for (mod in mods) { current = mod + '@' + mods[mod].current; wanted = mod + '@' + mods[mod].wanted; pChain = pChain.then(function(arg) { return _updateModule(mods[arg], type); }.bind(null, mod)); } return pChain; } function _updateModule (mod, type) { var deferred = when.defer(), cmd; // TODO: Account for "--latest" argument being passed. if (type && type === 'missing') { process.stdout.write('Installing MISSING module ' + mod.name + '@' + mod.wanted + ' ... '); cmd = 'npm install ' + mod.name; } else { process.stdout.write('Updating ' + mod.name + ' from ' + mod.current + ' to ' + mod.wanted + ' ... '); cmd = 'npm update ' + mod.name; if (['--save', '--save-dev', 'save-optional', '--save-exact'].indexOf(this.saveOption) !== -1) { cmd += ' ' + this.saveOption; } } pExec(cmd) .then(function() { console.log('OK!'); deferred.resolve(true); }) .catch(function(err) { console.error('OH NOES!'); console.error(err); deferred.reject(err); }); return deferred.promise; } /*********************************************************/ function Version() { this.name = null; this.location = null; this.wanted = 0; this.current = 0; this.latest = 0; } Version.prototype.SetFromParseableString = function(str) { var split; if (str && str.length > 0 && str.indexOf(':') !== -1) { split = str.split(':'); this.SetName(split[0]) .SetLocation('node_modules/' + split[0]) .SetWanted(split[1].split('@').pop()) .SetCurrent(split[2].split('@').pop()) .SetLatest(split[3].split('@').pop()); } return this; }; Version.prototype.SetName = function(v) { this.name = v; return this; }; Version.prototype.SetLocation = function(v) { this.location = v; return this; }; Version.prototype.SetWanted = function(v) { if (!isNaN(v)) { throw new Error('SetWanted expects a number'); } this.wanted = v; return this; }; Version.prototype.SetCurrent = function(v) { if (!isNaN(v)) { throw new Error('SetCurrent expects a number'); } this.current = v; return this; }; Version.prototype.SetLatest = function(v) { if (!isNaN(v)) { throw new Error('SetLatest expects a number'); } this.latest = v; return this; }; module.exports = NPMUpdateOutdated;