Implement the uninstall command.

Made also some tweaks to the render stuff.
This commit is contained in:
André Cruz
2013-05-30 21:57:05 +01:00
parent 3cf56ff7d5
commit 20ba998383
8 changed files with 268 additions and 52 deletions

View File

@@ -1,5 +1,6 @@
module.exports = {
'help': require('./help'),
'install': require('./install'),
'update': require('./update')
'update': require('./update'),
'uninstall': require('./uninstall')
};

58
lib/commands/uninstall.js Normal file
View File

@@ -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;

View File

@@ -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);

View File

@@ -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;

View File

@@ -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 = '';

View File

@@ -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;