From 20ba998383cd925ae435e4ceae44c55846776cbb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andre=CC=81=20Cruz?= Date: Thu, 30 May 2013 21:57:05 +0100 Subject: [PATCH] Implement the uninstall command. Made also some tweaks to the render stuff. --- bin/bower_new | 35 +++--- lib/commands/index.js | 3 +- lib/commands/uninstall.js | 58 +++++++++ lib/config.js | 3 +- lib/core/Project.js | 187 ++++++++++++++++++++++++++---- lib/renderers/StandardRenderer.js | 27 ++++- lib/util/cli.js | 6 +- package.json | 1 + 8 files changed, 268 insertions(+), 52 deletions(-) create mode 100644 lib/commands/uninstall.js diff --git a/bin/bower_new b/bin/bower_new index 29888aac..6b39d09a 100755 --- a/bin/bower_new +++ b/bin/bower_new @@ -17,10 +17,11 @@ var command; var notifier; var levels = { 'error': 5, - 'warn': 4, - 'action': 3, - 'info': 2, - 'debug': 1 + 'conflict': 4, + 'warn': 3, + 'action': 2, + 'info': 1, + 'debug': 0 }; process.title = 'bower'; @@ -46,8 +47,17 @@ if (config.silent) { loglevel = levels[config.loglevel] || levels.info; } +config.interactive = loglevel <= levels.conflict; + +// Get the command to execute +// TODO: abbreviations +command = options.argv.remain && options.argv.remain.shift(); +if (!commands[command]) { + command = 'help'; +} + // Get the renderer -renderer = cli.getRenderer(config); +renderer = cli.getRenderer(command, config); // Check for newer version of Bower notifier = updateNotifier({ @@ -59,13 +69,6 @@ if (notifier.update && levels.info >= loglevel) { renderer.updateAvailable(notifier.update); } -// Get the command to execute -// TODO: abbreviations -command = options.argv.remain && options.argv.remain.shift(); -if (!commands[command]) { - command = 'help'; -} - // Execute the command commands[command].line(process.argv) .on('end', function (data) { @@ -73,13 +76,7 @@ commands[command].line(process.argv) return; } - // Execute the specific render method for the command - if (renderer[command]) { - renderer[command](data); - // Fallback to the generic end method - } else { - renderer.end(data); - } + renderer.end(data); }) .on('error', function (err) { if (levels.error >= loglevel) { diff --git a/lib/commands/index.js b/lib/commands/index.js index 4f8cbe42..a49e1241 100644 --- a/lib/commands/index.js +++ b/lib/commands/index.js @@ -1,5 +1,6 @@ module.exports = { 'help': require('./help'), 'install': require('./install'), - 'update': require('./update') + 'update': require('./update'), + 'uninstall': require('./uninstall') }; diff --git a/lib/commands/uninstall.js b/lib/commands/uninstall.js new file mode 100644 index 00000000..3e06148b --- /dev/null +++ b/lib/commands/uninstall.js @@ -0,0 +1,58 @@ +var Emitter = require('events').EventEmitter; +var mout = require('mout'); +var Project = require('../core/Project'); +var cli = require('../util/cli'); +var help = require('./help'); +var defaultConfig = require('../config'); + +function uninstall(endpoints, options, config) { + var project; + var emitter = new Emitter(); + + options = options || {}; + config = mout.object.deepMixIn(config, defaultConfig); + + // If endpoints are an empty array, null them + if (endpoints && !endpoints.length) { + endpoints = null; + } + + project = new Project(config); + project.uninstall(endpoints, options) + .then(function (uninstalled) { + emitter.emit('end', uninstalled); + }, function (error) { + emitter.emit('error', error); + }, function (notification) { + emitter.emit('notification', notification); + }); + + return emitter; +} + +// ------------------- + +uninstall.line = function (argv) { + var options = uninstall.options(argv); + var names = options.argv.remain.slice(1); + + if (options.help || !names.length) { + return help('uninstall'); + } + + return uninstall(names, options); +}; + +uninstall.options = function (argv) { + return cli.readOptions({ + 'help': { type: Boolean, shorthand: 'h' }, + 'save': { type: Boolean, shorthand: 'S' }, + 'save-dev': { type: Boolean, shorthand: 'D' } + }, argv); +}; + +uninstall.completion = function () { + // TODO: +}; + +module.exports = uninstall; diff --git a/lib/config.js b/lib/config.js index b9d447ef..778574e9 100644 --- a/lib/config.js +++ b/lib/config.js @@ -48,7 +48,8 @@ try { 'strict-ssl': true, 'user-agent': 'node/' + process.version + ' ' + process.platform + ' ' + process.arch, 'git': 'git', - 'color': true + 'color': true, + 'interactive': false }); } catch (e) { throw new Error('Unable to parse runtime configuration: ' + e.message); diff --git a/lib/core/Project.js b/lib/core/Project.js index 259aaf40..b8a7773f 100644 --- a/lib/core/Project.js +++ b/lib/core/Project.js @@ -3,6 +3,8 @@ var path = require('path'); var fs = require('fs'); var Q = require('q'); var mout = require('mout'); +var rimraf = require('rimraf'); +var promptly = require('promptly'); var bowerJson = require('bower-json'); var Manager = require('./Manager'); var defaultConfig = require('../config'); @@ -40,7 +42,7 @@ Project.prototype.install = function (endpoints, options) { // Start by repairing the project, installing only missing packages return this._repair() // Analyse the project - .then(that._analyse.bind(this)) + .then(that.analyse.bind(this)) .spread(function (json, tree, flattened) { var targets = {}; var resolved = {}; @@ -71,19 +73,19 @@ Project.prototype.install = function (endpoints, options) { return that._bootstrap(targets, resolved, installed) // Handle save and saveDev options .then(function () { - var key; + var jsonKey; if (!options.save && !options.saveDev) { return; } - key = options.save ? 'dependencies' : 'devDependencies'; - that._json[key] = that._json[key] || {}; + jsonKey = options.save ? 'dependencies' : 'devDependencies'; + that._json[jsonKey] = that._json[jsonKey] || {}; mout.object.forOwn(targets, function (decEndpoint) { var source = decEndpoint.registry ? '' : decEndpoint.source; var target = decEndpoint.pkgMeta.version ? '~' + decEndpoint.pkgMeta.version : decEndpoint.target; - that._json[key][decEndpoint.name] = mout.string.ltrim(source + '#' + target, ['#']); + that._json[jsonKey][decEndpoint.name] = mout.string.ltrim(source + '#' + target, ['#']); }); return that._saveJson() @@ -116,7 +118,7 @@ Project.prototype.update = function (names, options) { // If no names were specified, we update every package if (!names) { // Analyse the project - promise = this._analyse() + promise = this.analyse() .spread(function (json, tree, flattened) { // Mark each json entry as targets targets = mout.object.map(json.dependencies, function (value, key) { @@ -136,7 +138,7 @@ Project.prototype.update = function (names, options) { // Analyse the project .then(function (result) { repaired = result; - return that._analyse(); + return that.analyse(); }) .spread(function (json, tree, flattened) { targets = {}; @@ -192,16 +194,98 @@ Project.prototype.update = function (names, options) { }; Project.prototype.uninstall = function (names, options) { + var that = this; + var deferred = Q.defer(); + this.analyse() + .spread(function (json, tree, flattened) { + var promise = Q.resolve(); + var packages = []; + + names.forEach(function (name) { + var decEndpoint = flattened[name]; + + // Check if it is not installed + if (!decEndpoint || decEndpoint.missing) { + packages[name] = null; + return; + } + + // Decide if the package will be uninstalled or skipped + // This is done with a promise that resolves to a boolean + promise = promise + .then(function () { + var dependants; + var message; + var data; + + // Check if it has dependants + // Note that the root is filtered from the dependants + // as well as other dependants marked to be uninstalled + dependants = []; + mout.object.forOwn(decEndpoint.dependants, function (decEndpoint) { + if (!decEndpoint.root && names.indexOf(decEndpoint.name) === -1) { + dependants.push(decEndpoint); + } + }); + + // If the package has no dependants or the force config is enabled, + // mark it to be removed + if (!dependants.length || that._config.force) { + packages[name] = decEndpoint.dir; + // Otherwise we need to figure it out if the user really wants to remove it, + // even with dependants + } else { + message = dependants.map(function (dep) { return dep.name; }).join(', ') + ' depend on ' + decEndpoint.name; + data = { + package: decEndpoint.name, + dependants: dependants.map(function (decEndpoint) { + return decEndpoint.name; + }) + }; + + // If interactive is disabled, error out + if (!that._config.interactive) { + throw createError(message, 'ECONFLICT', { + skippable: true, + data: data + }); + // Question the user + } else { + deferred.notify({ + level: 'conflict', + id: 'mutual', + message: message, + data: data + }); + + return Q.nfcall(promptly.confirm, 'Continue anyway? (y/n)') + .then(function (confirmed) { + // If the user decided to skip it, remove from the array so that it won't + // influence subsequent dependants + if (!confirmed) { + mout.array.remove(names, name); + } else { + packages[name] = decEndpoint.dir; + } + }); + } + } + }); + }); + + return promise + .then(function () { + return that._removePackages(packages, options) + .progress(deferred.notify); + }); + }) + .then(deferred.resolve, deferred.reject); + + return deferred.promise; }; -Project.prototype.getTree = function () { - -}; - -// ----------------- - -Project.prototype._analyse = function () { +Project.prototype.analyse = function () { return F.all([ this._readJson(), this._readInstalled() @@ -215,10 +299,10 @@ Project.prototype._analyse = function () { source: this._config.cwd, target: json.version, dir: this._config.cwd, - pkgMeta: json + pkgMeta: json, + root: true }; - // Restore the original dependencies cross-references, // that is, the parent-child relationships this._restoreNode(root, flattened); @@ -238,6 +322,8 @@ Project.prototype._analyse = function () { }.bind(this)); }; +// ----------------- + Project.prototype._bootstrap = function (targets, resolved, installed) { // Configure the manager and kick in the resolve process return this._manager @@ -252,7 +338,7 @@ Project.prototype._bootstrap = function (targets, resolved, installed) { Project.prototype._repair = function (incompatible) { var that = this; - return this._analyse() + return this.analyse() .spread(function (json, tree, flattened) { var targets = []; var resolved = {}; @@ -332,7 +418,7 @@ Project.prototype._readJson = function () { return deferred.promise; }; -Project.prototype._saveJson = function (json) { +Project.prototype._saveJson = function () { var deferred = Q.defer(); if (!this._jsonFile) { @@ -345,9 +431,7 @@ Project.prototype._saveJson = function (json) { deferred.resolve(); }); } else { - json = json || this._json; - - Q.nfcall(fs.writeFile, this._jsonFile, JSON.stringify(json, null, ' ')) + Q.nfcall(fs.writeFile, this._jsonFile, JSON.stringify(this._json, null, ' ')) .then(deferred.resolve, deferred.reject, deferred.notify); } @@ -399,6 +483,65 @@ Project.prototype._readInstalled = function () { }); }; +Project.prototype._removePackages = function (packages, options) { + var promises = []; + var jsonKey = options.save ? 'dependencies' : (options.saveDev ? 'devDependencies' : null); + var deferred = Q.defer(); + + mout.object.forOwn(packages, function (dir, name) { + var promise; + + // Delete directory + if (!dir) { + process.nextTick(function () { + deferred.notify({ + level: 'info', + id: 'missing', + message: name, + data: { + package: name + } + }); + }); + promise = Q.resolve(); + } else { + process.nextTick(function () { + deferred.notify({ + level: 'action', + id: 'uninstall', + message: name, + data: { + package: name, + dir: dir + } + }); + }); + promise = Q.nfcall(rimraf, dir); + } + + // Remove from json only if successfully deleted + if (jsonKey && this._json[jsonKey]) { + promise = promise + .then(function () { + delete this._json[jsonKey][name]; + }.bind(this)); + } + + promises.push(promise); + }, this); + + Q.all(promises) + // Save json + .then(this._saveJson.bind(this)) + // Resolve with removed packages + .then(function () { + return packages; + }) + .then(deferred.resolve, deferred.reject, deferred.notify); + + return deferred.promise; +}; + Project.prototype._walkTree = function (node, fn) { var queue = [node]; var result; @@ -434,7 +577,7 @@ Project.prototype._restoreNode = function (node, flattened, jsonKey) { flattened[key] = local = json; local.missing = true; // Even if it is installed, check if it's compatible - } else if (!local.incompatible && !this._manager.areCompatible(local.pkgMeta.version || '*', json.target)) { + } else if (!local.incompatible && !local.missing && !this._manager.areCompatible(local.pkgMeta.version || '*', json.target)) { json.pkgMeta = local.pkgMeta; flattened[key] = local = json; local = json; diff --git a/lib/renderers/StandardRenderer.js b/lib/renderers/StandardRenderer.js index 00585331..622f9efd 100644 --- a/lib/renderers/StandardRenderer.js +++ b/lib/renderers/StandardRenderer.js @@ -1,6 +1,8 @@ var mout = require('mout'); -function StandardRenderer(colorful) { +var wideCommands = ['install', 'update']; + +function StandardRenderer(command, colorful) { this._sizes = { id: 10, // Id max chars label: 23, // Label max chars @@ -9,14 +11,20 @@ function StandardRenderer(colorful) { this._colors = { warn: 'yellow', error: 'red', + conflict: 'magenta', 'default': 'cyan' }; + this._command = command; this._colorful = colorful == null ? true : colorful; - this._compact = process.stdout.columns < 120; + this._compact = wideCommands.indexOf(command) === -1 ? true : process.stdout.columns < 120; } -StandardRenderer.prototype.end = function () {}; +StandardRenderer.prototype.end = function (data) { + if (this[this._command]) { + this[this._command](data); + } +}; StandardRenderer.prototype.error = function (err) { var str; @@ -31,8 +39,10 @@ StandardRenderer.prototype.error = function (err) { str += mout.string.trim(err.details) + '\n'; } - // Print stack - str += '\n' + err.stack + '\n'; + // Print stack if the error is not skippable + if (!err.skippable) { + str += '\n' + err.stack + '\n'; + } this._write(process.stderr, 'bower ' + str); }; @@ -78,6 +88,11 @@ StandardRenderer.prototype._genericNotification = function (notification) { this._write(stream, 'bower ' + str); }; +StandardRenderer.prototype._mutualNotification = function (notification) { + notification.id = 'conflict'; + this._genericNotification(notification); +}; + StandardRenderer.prototype._checkoutNotification = function (notification) { if (this._compact) { notification.message = notification.from + '#' + notification.message; @@ -102,7 +117,7 @@ StandardRenderer.prototype._prefixNotification = function (notification) { // Construct the label if (notification.from && notification.data.endpoint) { - label = notification.from ? notification.from + '#' + notification.data.endpoint.target : ''; + label = notification.from + '#' + notification.data.endpoint.target; // Make it empty if there's not enough information } else { label = ''; diff --git a/lib/util/cli.js b/lib/util/cli.js index ef77aa8c..0984b470 100644 --- a/lib/util/cli.js +++ b/lib/util/cli.js @@ -34,12 +34,12 @@ function readOptions(options, argv) { return parsedOptions; } -function getRenderer(config) { +function getRenderer(command, config) { if (config.json) { - return new renderers.Json(); + return new renderers.Json(command); } - return new renderers.Standard(config.color); + return new renderers.Standard(command, config.color); } module.exports.readOptions = readOptions; diff --git a/package.json b/package.json index 05a538a5..62f2e1a9 100644 --- a/package.json +++ b/package.json @@ -31,6 +31,7 @@ "mout": "~0.6.0", "ncp": "~0.4.2", "nopt": "~2.1.1", + "promptly": "~0.1.0", "q": "~0.9.2", "rc": "~0.1.0", "request": "~2.21.0",