diff --git a/README.md b/README.md index 3af10e89..1010c8ba 100644 --- a/README.md +++ b/README.md @@ -38,7 +38,7 @@ Main issues are: - **Canonical package:** A folder containing all the files that belong to a package. May include a `bower.json` file inside. (typically what gets installed) - **Source:** URL, git endpoint, etc. - **Target:** `semver` range, commit hash, branch (indicates a version). -- **Endpoint:** name|source#target +- **Endpoint:** name=source#target - **Decomposed endpoint:** An object containing the `name`, `source` and `target` keys. - **Components folder:** The folder in which components are installed (`bower_components` by default). - **Package meta:** A data structure similar to the one found in `bower.json`, which might also contain additional information. This is stored in a `.bower.json` file, inside a canonical package. @@ -124,12 +124,13 @@ If `config` is not passed, the default one will be used. ##### Public methods -`Manager#configure(targets, installed)`: Promise +`Manager#configure(targets, resolved, installed)`: Promise Configures the manager with `targets` and `installed`: - `targets`: array of decomposed endpoints that need to be installed -- `installed`: object where keys are names and values the canonical package or the package metas +- `resolved`: object of resolved packages (keys are names and values the canonical package or the package metas) +- `installed`: object of currently installed packages (keys are names and values the canonical package or the package metas) If the Manager is already working, the promise is immediately rejected. diff --git a/lib/commands/index.js b/lib/commands/index.js index bc3f0fee..4f8cbe42 100644 --- a/lib/commands/index.js +++ b/lib/commands/index.js @@ -1,4 +1,5 @@ module.exports = { 'help': require('./help'), - 'install': require('./install') + 'install': require('./install'), + 'update': require('./update') }; diff --git a/lib/commands/update.js b/lib/commands/update.js new file mode 100644 index 00000000..f4e90c5c --- /dev/null +++ b/lib/commands/update.js @@ -0,0 +1,56 @@ +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 update(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.update(endpoints, options) + .then(function (installed) { + emitter.emit('end', installed); + }, function (error) { + emitter.emit('error', error); + }, function (notification) { + emitter.emit('notification', notification); + }); + + return emitter; +} + +// ------------------- + +update.line = function (argv) { + var options = update.options(argv); + + if (options.help) { + return help('update'); + } + + return update(options.argv.remain.slice(1), options); +}; + +update.options = function (argv) { + return cli.readOptions({ + 'help': { type: Boolean, shorthand: 'h' }, + 'production': { type: Boolean, shorthand: 'p' } + }, argv); +}; + +update.completion = function () { + // TODO: +}; + +module.exports = update; diff --git a/lib/core/Manager.js b/lib/core/Manager.js index 528b1a61..837868e8 100644 --- a/lib/core/Manager.js +++ b/lib/core/Manager.js @@ -18,7 +18,7 @@ function Manager(config) { // ----------------- -Manager.prototype.configure = function (targets, installed) { +Manager.prototype.configure = function (targets, resolved, installed) { // If working, error out if (this._working) { throw createError('Can\'t configure while working', 'EWORKING'); @@ -26,25 +26,31 @@ Manager.prototype.configure = function (targets, installed) { this._targets = {}; this._resolved = {}; + this._installed = {}; // Parse targets targets.forEach(function (decEndpoint) { this._targets[decEndpoint.name] = decEndpoint; }, this); - // Parse installed - mout.object.forOwn(installed, function (value, name) { + // Parse resolved + mout.object.forOwn(resolved, function (value, name) { // TODO: If value is a string, read package meta // If is not a string, than it's already the package meta this._resolved[name] = [{ name: name, - source: null, + source: value._source, target: value.version || '*', pkgMeta: value, - installed: true + initial: true }]; + + this._installed[name] = value; }, this); + // Parse installed + mout.object.mixIn(this._installed, installed); + return this; }; @@ -110,7 +116,10 @@ Manager.prototype.install = function () { // Remove existent and copy canonical package dest = path.join(destDir, name); promise = Q.nfcall(rimraf, dest) - .then(copy.copyDir.bind(copy, decEndpoint.dir, dest)); + .then(copy.copyDir.bind(copy, decEndpoint.dir, dest)) + .fail(function (err) { + throw that._extendNotification(err, decEndpoint); + }); promises.push(promise); }); @@ -241,7 +250,7 @@ Manager.prototype._onFetch = function (deferred, decEndpoint, canonicalPkg, pkgM // we need to remove the initially resolved one that match the new name if (!initialName) { index = mout.array.findIndex(resolved, function (decEndpoint) { - return decEndpoint.installed; + return decEndpoint.initial; }); if (index !== -1) { @@ -310,19 +319,18 @@ Manager.prototype._parseDependencies = function (decEndpoint, pkgMeta) { }; Manager.prototype._dissect = function () { - var pkgMetas; - var dissected = {}; + var suitables = {}; mout.object.forOwn(this._resolved, function (decEndpoints, name) { - var target = this._targets[name]; var nonSemver; var validSemver; var suitable; + var target = mout.object.get(this._targets, name + '.target'); // If this was initially configured as a target without a valid semver target, // it means the user wants it regardless of other ones - if (target && target.target && !semver.valid(target.target)) { - dissected[name] = this._targets[name]; + if (target && semver.valid(target) == null && semver.validRange(target) == null) { + suitables[name] = this._targets[name]; // TODO: issue warning return; } @@ -348,7 +356,7 @@ Manager.prototype._dissect = function () { // If it gets here, they are equal but priority is given to // installed ones - return first.installed ? -1 : (second.installed ? 1 : 0); + return first.initial ? -1 : (second.initial ? 1 : 0); }); // If there are no semver targets @@ -366,24 +374,23 @@ Manager.prototype._dissect = function () { } // TODO: handle case which there is a suitable version but there are no-semver ones too - if (suitable) { - dissected[name] = suitable; + suitables[name] = suitable; } else { throw new Error('No suitable version for "' + name + '"'); } }, this); // Filter only packages that need to be installed - this._dissected = mout.object.filter(dissected, function (decEndpoint) { - return !decEndpoint.installed; - }); + this._dissected = mout.object.filter(suitables, function (decEndpoint, name) { + var installedMeta = this._installed[name]; + return !installedMeta || installedMeta._release !== decEndpoint.pkgMeta._release; + }, this); // Resolve just with the package metas of the dissected object - pkgMetas = mout.object.map(this._dissected, function (decEndpoint) { + this._deferred.resolve(mout.object.map(this._dissected, function (decEndpoint) { return decEndpoint.pkgMeta; - }); - this._deferred.resolve(pkgMetas); + })); }; Manager.prototype._extendNotification = function (notification, decEndpoint) { diff --git a/lib/core/Project.js b/lib/core/Project.js index 868ee138..58f3ab99 100644 --- a/lib/core/Project.js +++ b/lib/core/Project.js @@ -18,7 +18,6 @@ function Project(config) { // ----------------- Project.prototype.install = function (endpoints, options) { - var repairResult; var that = this; // If already working, error out @@ -26,6 +25,9 @@ Project.prototype.install = function (endpoints, options) { return Q.reject(createError('Already working', 'EWORKING')); } + options = options || {}; + this._production = !!options.production; + // If no endpoints were specified, simply repair the project // Note that we also repair incompatible packages if (!endpoints) { @@ -38,58 +40,155 @@ Project.prototype.install = function (endpoints, options) { // Start by repairing the project, installing only missing packages return this._repair() // Analyse the project - .then(function (result) { - repairResult = result; - return that._analyse(); - }) + .then(that._analyse.bind(this)) .spread(function (json, tree, flattened) { - var targetNames = {}; - var targets = []; - var installed = {}; + var targets = {}; + var resolved = {}; + var installed; // Mark targets - endpoints.forEach(function (target) { - var decEndpoint = endpointParser.decompose(target); - targetNames[decEndpoint.name] = true; - targets.push(decEndpoint); + endpoints.forEach(function (endpoint) { + var decEndpoint = endpointParser.decompose(endpoint); + targets[decEndpoint.name] = decEndpoint; }); - // Mark every package from the tree as installed - // if they are not a target or a non-shared descendant of a target - // TODO: We should traverse the tree (deep first) and - // add each leaf to the resolved - // If a leaf is a target, we abort traversal of it - mout.object.forOwn(flattened, function (decEndpoint, name) { - if (targetNames[name]) { + // Mark every package from the tree as resolved + // if it's not a target or a non-shared descendant of a target + // This is done by walking the tree (deep first) and abort traversal + // as soon as one target was found + that._walkTree(tree, function (node, name) { + if (targets[name]) { + return false; // Abort traversal + } + resolved[name] = node.pkgMeta; + }); + + installed = mout.object.map(flattened, function (decEndpoint) { + return decEndpoint.pkgMeta; + }); + + // Bootstrap the process + return that._bootstrap(targets, resolved, installed) + // Handle save and saveDev options + .then(function () { + var key; + + if (!options.save && !options.saveDev) { return; } - installed[name] = decEndpoint.pkgMeta; - }); + key = options.save ? 'dependencies' : 'devDependencies'; + that._json[key] = that._json[key] || {}; - // Configure the manager and kick in the resolve process - return that._manager - .configure(targets, installed) - .resolve() - // Install resolved ones - .then(function () { - return that._manager.install(); - }) - // Resolve the promise with the repair and install results, - // by merging them together - .then(function (result) { - return mout.object.fillIn(result, repairResult); + mout.object.forOwn(targets, function (decEndpoint) { + var source = decEndpoint.source === decEndpoint.registryName ? '' : decEndpoint.source; + var target = decEndpoint.pkgMeta.version ? '~' + decEndpoint.pkgMeta.version : decEndpoint.target; + that._json[key][decEndpoint.name] = mout.string.ltrim(source + '#' + target, ['#']); + }); + + return that._saveJson() + .progress(function (notification) { + return notification; + }); }); }) .fin(function () { that._working = false; }); - - // TODO: handle save saveDev production }; -Project.prototype.update = function (names) { +Project.prototype.update = function (names, options) { + var that = this; + var targets; + var resolved; + var installed; + var repaired; + var promise; + // If already working, error out + if (this._working) { + return Q.reject(createError('Already working', 'EWORKING')); + } + + options = options || {}; + this._production = !!options.production; + + // If no names were specified, we update every package + if (!names) { + // Analyse the project + promise = this._analyse() + .spread(function (json, tree, flattened) { + // Mark each json entry as targets + targets = mout.object.map(json.dependencies, function (value, key) { + return endpointParser.json2decomposed(key, value); + }); + + // Mark installed + installed = mout.object.map(flattened, function (decEndpoint) { + return decEndpoint.pkgMeta; + }); + }); + // Otherwise we selectively update the specified ones + } else { + // Start by repairing the project + // Note that we also repair incompatible packages + promise = this._repair(true) + // Analyse the project + .then(function (result) { + repaired = result; + return that._analyse(); + }) + .spread(function (json, tree, flattened) { + targets = {}; + resolved = {}; + + // Mark targets + names.forEach(function (name) { + var decEndpoint = flattened[name]; + var jsonEntry; + + if (!decEndpoint) { + throw createError('Package ' + name + ' is not installed', 'ENOTINSTALLED'); + } + + // If it was repaired, don't include in the targets + if (repaired[name]) { + return; + } + + // Use json entry if available, + // fallbacking to the installed one + jsonEntry = json.dependencies && json.dependencies[name]; + if (jsonEntry) { + targets[name] = endpointParser.json2decomposed(name, jsonEntry); + } else { + targets[name] = decEndpoint; + } + }); + + // Mark every package from the tree as resolved + // if it's not a target or a non-shared descendant of a target + that._walkTree(tree, function (node, name) { + if (targets[name]) { + return false; // Abort traversal + } + resolved[name] = node.pkgMeta; + }); + + // Mark installed + installed = mout.object.map(flattened, function (decEndpoint) { + return decEndpoint.pkgMeta; + }); + }); + } + + // Bootstrap the process + return promise.then(function () { + return that._bootstrap(targets, resolved, installed); + }) + .fin(function () { + that._working = false; + }); }; Project.prototype.uninstall = function (names, options) { @@ -100,10 +199,6 @@ Project.prototype.getTree = function () { }; -Project.prototype.getFlatTree = function () { - -}; - // ----------------- Project.prototype._analyse = function () { @@ -117,28 +212,50 @@ Project.prototype._analyse = function () { root = { name: json.name, + source: this._config.cwd, + target: json.version, + dir: this._config.cwd, pkgMeta: json }; + // Restore the original dependencies cross-references, // that is, the parent-child relationships this._restoreNode(root, flattened); // Do the same for the dev dependencies - if (!this._config.production) { + if (!this._production) { this._restoreNode(root, flattened, 'devDependencies'); } + // Parse extraneous + mout.object.forOwn(flattened, function (decEndpoint) { + if (!decEndpoint.dependants) { + decEndpoint.extraneous = true; + } + }); + return [json, root, flattened]; }.bind(this)); }; +Project.prototype._bootstrap = function (targets, resolved, installed) { + // Configure the manager and kick in the resolve process + return this._manager + .configure(mout.object.values(targets), resolved, installed) + .resolve() + // Install resolved ones + .then(function () { + return this._manager.install(); + }.bind(this)); +}; + Project.prototype._repair = function (incompatible) { var that = this; return this._analyse() .spread(function (json, tree, flattened) { var targets = []; - var installed = {}; + var resolved = {}; var isBroken = false; // Figure out which are the missing/incompatible ones @@ -150,8 +267,8 @@ Project.prototype._repair = function (incompatible) { } else if (incompatible && decEndpoint.incompatible) { targets.push(decEndpoint); isBroken = true; - } else { - installed[name] = decEndpoint.pkgMeta; + } else if (!decEndpoint.extraneous) { + resolved[name] = decEndpoint.pkgMeta; } }); @@ -161,23 +278,22 @@ Project.prototype._repair = function (incompatible) { } // Configure the manager and kick in the resolve process - return that._manager - .configure(targets, installed) - .resolve() - // Install after resolve - .then(function () { - return that._manager.install(); - }); + return that._bootstrap(targets, resolved); }); }; Project.prototype._readJson = function () { - var deferred = Q.defer(); + var that = this; + var deferred; - // TODO: refactor! + if (this._json) { + return Q.resolve(this._json); + } + + deferred = Q.defer(); // Read local json - Q.nfcall(bowerJson.find, this._config.cwd) + this._json = Q.nfcall(bowerJson.find, this._config.cwd) .then(function (filename) { // If it is a component.json, warn about the deprecation if (path.basename(filename) === 'component.json') { @@ -193,6 +309,8 @@ Project.prototype._readJson = function () { }); } + that._jsonFile = filename; + // Read it return Q.nfcall(bowerJson.read, filename) .fail(function (err) { @@ -202,9 +320,36 @@ Project.prototype._readJson = function () { }); }, function () { // No json file was found, assume one - return Q.nfcall(bowerJson.parse, { name: path.basename(this._config.cwd) }); - }.bind(this)) - .then(deferred.resolve, deferred.reject, deferred.notify); + return Q.nfcall(bowerJson.parse, { + name: path.basename(that._config.cwd) + }); + }) + .then(function (json) { + that._json = json; + deferred.resolve(json); + }, deferred.reject, deferred.notify); + + return deferred.promise; +}; + +Project.prototype._saveJson = function (json) { + var deferred = Q.defer(); + + if (!this._jsonFile) { + process.nextTick(function () { + deferred.notify({ + level: 'warn', + id: 'no-json', + message: 'No bower.json file to save to' + }); + deferred.resolve(); + }); + } else { + json = json || this._json; + + Q.nfcall(fs.writeFile, this._jsonFile, JSON.stringify(json, null, ' ')) + .then(deferred.resolve, deferred.reject, deferred.notify); + } return deferred.promise; }; @@ -212,7 +357,6 @@ Project.prototype._readJson = function () { Project.prototype._readInstalled = function () { var componentsDir = path.join(this._config.cwd, this._config.directory); - // TODO: refactor // Gather all folders that are actual packages by // looking for the package metadata file return Q.nfcall(glob, '*/.bower.json', { @@ -227,14 +371,18 @@ Project.prototype._readInstalled = function () { filenames.forEach(function (filename) { var promise; var name = path.dirname(filename); + var jsonFile = path.join(componentsDir, filename); // Read package metadata - promise = Q.nfcall(fs.readFile, path.join(componentsDir, filename)) + promise = Q.nfcall(fs.readFile, jsonFile) .then(function (contents) { var pkgMeta = JSON.parse(contents.toString()); decEndpoints[name] = { name: name, + source: pkgMeta._source, + target: pkgMeta.version, + dir: path.dirname(jsonFile), pkgMeta: pkgMeta }; }); @@ -251,6 +399,22 @@ Project.prototype._readInstalled = function () { }); }; +Project.prototype._walkTree = function (node, fn) { + var queue = [node]; + var result; + + while (queue.length) { + node = queue.shift(); + result = fn(node, node.name); + + if (result === false) { + continue; + } + + queue.unshift.apply(queue, mout.object.values(node.dependencies)); + } +}; + Project.prototype._restoreNode = function (node, flattened, jsonKey) { // Do not restore if already processed or if the node is // missing or incompatible @@ -267,15 +431,14 @@ Project.prototype._restoreNode = function (node, flattened, jsonKey) { // Check if the dependency is not installed if (!local) { - local = json; + flattened[key] = local = json; local.missing = true; - flattened[key] = local; // Even if it is installed, check if it's compatible } else if (!local.incompatible && !this._manager.areCompatible(local.pkgMeta.version || '*', json.target)) { json.pkgMeta = local.pkgMeta; + flattened[key] = local = json; local = json; local.incompatible = true; - flattened[key] = local; } // Cross reference diff --git a/lib/core/resolvers/Resolver.js b/lib/core/resolvers/Resolver.js index 56d4915b..ca1ee4f4 100644 --- a/lib/core/resolvers/Resolver.js +++ b/lib/core/resolvers/Resolver.js @@ -174,7 +174,9 @@ Resolver.prototype._readJson = function (dir) { }); }.bind(this), function () { // No json file was found, assume one - return Q.nfcall(bowerJson.parse, { name: this._name }); + return Q.nfcall(bowerJson.parse, { + name: this._name + }); }.bind(this)) .then(deferred.resolve, deferred.reject, deferred.notify); diff --git a/package.json b/package.json index 84ca2067..05a538a5 100644 --- a/package.json +++ b/package.json @@ -33,7 +33,7 @@ "nopt": "~2.1.1", "q": "~0.9.2", "rc": "~0.1.0", - "request": "~2.20.0", + "request": "~2.21.0", "rimraf": "~2.1.4", "semver": "~1.1.4", "tar": "~0.1.17",