diff --git a/NOTES.md b/NOTES.md index 582dd3da..e2dc4112 100644 --- a/NOTES.md +++ b/NOTES.md @@ -37,3 +37,4 @@ Not BC changes: - Shorthand resolver syntax {{{}}} to just {{}} - Removed json property from the config +- 'moment': 'git://repo' is no longer supported.. #* must be added to the end diff --git a/lib/commands/cache/clean.js b/lib/commands/cache/clean.js index ac5681ce..d26dfa22 100644 --- a/lib/commands/cache/clean.js +++ b/lib/commands/cache/clean.js @@ -42,7 +42,8 @@ function clean(packages, options, config) { ]) .spread(function (entries) { emitter.emit('end', entries); - }, function (error) { + }) + .fail(function (error) { emitter.emit('error', error); }); diff --git a/lib/commands/cache/list.js b/lib/commands/cache/list.js index 674dccdc..3699f9e0 100644 --- a/lib/commands/cache/list.js +++ b/lib/commands/cache/list.js @@ -30,7 +30,8 @@ function list(packages, options, config) { } emitter.emit('end', entries); - }, function (error) { + }) + .fail(function (error) { emitter.emit('error', error); }); diff --git a/lib/commands/info.js b/lib/commands/info.js index 130c5288..0bf5136f 100644 --- a/lib/commands/info.js +++ b/lib/commands/info.js @@ -27,6 +27,9 @@ function info(pkg, property, config) { name: pkg.name, versions: versions }); + }) + .fail(function (error) { + emitter.emit('error', error); }); // Otherwise fetch version and retrieve package meta } else { @@ -45,7 +48,8 @@ function info(pkg, property, config) { } emitter.emit('end', pkgMeta); - }, function (error) { + }) + .fail(function (error) { emitter.emit('error', error); }); } diff --git a/lib/commands/install.js b/lib/commands/install.js index 2ebbd155..251cc965 100644 --- a/lib/commands/install.js +++ b/lib/commands/install.js @@ -22,7 +22,8 @@ function install(endpoints, options, config) { project.install(endpoints, options) .then(function (installed) { emitter.emit('end', installed); - }, function (error) { + }) + .fail(function (error) { emitter.emit('error', error); }); diff --git a/lib/commands/list.js b/lib/commands/list.js index 7ed4fcb1..54701876 100644 --- a/lib/commands/list.js +++ b/lib/commands/list.js @@ -1,34 +1,41 @@ var EventEmitter = require('events').EventEmitter; var path = require('path'); var mout = require('mout'); +var semver = require('semver'); +var Q = require('q'); var Project = require('../core/Project'); +var PackageRepository = require('../core/PackageRepository'); var Logger = require('../core/Logger'); var cli = require('../util/cli'); var defaultConfig = require('../config'); function list(options, config) { var project; + var repository; var emitter = new EventEmitter(); var logger = new Logger(); options = options || {}; config = mout.object.deepMixIn(config || {}, defaultConfig); project = new Project(config, logger); - - // TODO: look for new versions + repository = new PackageRepository(config, logger); project.getTree() .spread(function (tree, flattened, extraneous) { - var ret; - if (options.paths) { - ret = paths(flattened); - } else { - ret = normal(tree, extraneous); + return emitter.emit('end', paths(flattened)); } - emitter.emit('end', ret); - }, function (error) { + if (config.offline) { + return emitter.emit('end', normal(tree, extraneous)); + } + + return checkVersions(project, tree, logger, config) + .then(function () { + emitter.emit('end', normal(tree, extraneous)); + }); + }) + .fail(function (error) { emitter.emit('error', error); }); @@ -37,6 +44,44 @@ function list(options, config) { return logger.pipe(emitter); } +function checkVersions(project, tree, logger, config) { + var promises; + var nodes = []; + var repository = new PackageRepository(config, logger); + + // Gather all nodes + project.walkTree(tree, function (node) { + nodes.push(node); + }, true); + + if (nodes.length) { + logger.info('check-new', 'Checking for new versions of the project dependencies..'); + } + + // Check for new versions for each node + promises = nodes.map(function (node) { + var target = node.endpoint.target; + + return repository.versions(node.endpoint.source) + .then(function (versions) { + node.versions = versions; + + // Do not check if node's target is not a valid semver one + if (versions.length && semver.validRange(target)) { + node.update = { + target: semver.maxSatisfying(versions, target), + latest: semver.maxSatisfying(versions, '*') + }; + } + }); + }); + + // Set the versions also for the root node + tree.versions = []; + + return Q.all(promises); +} + function paths(flattened) { var ret = {}; diff --git a/lib/commands/lookup.js b/lib/commands/lookup.js index 5c126e64..81f4a525 100644 --- a/lib/commands/lookup.js +++ b/lib/commands/lookup.js @@ -1,5 +1,6 @@ var EventEmitter = require('events').EventEmitter; var mout = require('mout'); +var Q = require('q'); var RegistryClient = require('bower-registry-client'); var cli = require('../util/cli'); var defaultConfig = require('../config'); @@ -12,17 +13,18 @@ function lookup(name, config) { config.cache = config.storage.registry; registryClient = new RegistryClient(config); - registryClient.lookup(name, function (error, entry) { - if (error) { - return emitter.emit('error', error); - } + Q.nfcall(registryClient.lookup.bind(registryClient), name) + .then(function (entry) { // TODO: Handle entry.type.. for now it's only 'alias' // When we got published packages, this needs to be adjusted emitter.emit('end', !entry ? null : { name: name, url: entry && entry.url }); + }) + .fail(function (error) { + emitter.emit('error', error); }); return emitter; diff --git a/lib/commands/search.js b/lib/commands/search.js index 7df07635..2de9e516 100644 --- a/lib/commands/search.js +++ b/lib/commands/search.js @@ -1,11 +1,13 @@ var EventEmitter = require('events').EventEmitter; var mout = require('mout'); +var Q = require('q'); var RegistryClient = require('bower-registry-client'); var cli = require('../util/cli'); var defaultConfig = require('../config'); function search(name, config) { var registryClient; + var promise; var emitter = new EventEmitter(); config = mout.object.deepMixIn(config || {}, defaultConfig); @@ -15,23 +17,23 @@ function search(name, config) { // If no name was specified, list all packages if (!name) { - registryClient.list(onResults.bind(onResults, emitter)); + promise = Q.nfcall(registryClient.list.bind(registryClient)); // Otherwise search it } else { - registryClient.search(name, onResults.bind(onResults, emitter)); + promise = Q.nfcall(registryClient.search.bind(registryClient), name); } + promise + .then(function (results) { + emitter.emit('end', results); + }) + .fail(function (error) { + emitter.emit('error', error); + }); + return emitter; } -function onResults(emitter, error, results) { - if (error) { - return emitter.emit('error', error); - } - - emitter.emit('end', results); -} - // ------------------- search.line = function (argv) { diff --git a/lib/commands/uninstall.js b/lib/commands/uninstall.js index a339a6f1..f7e638a6 100644 --- a/lib/commands/uninstall.js +++ b/lib/commands/uninstall.js @@ -24,7 +24,8 @@ function uninstall(names, options, config) { project.uninstall(names, options) .then(function (installed) { emitter.emit('end', installed); - }, function (error) { + }) + .fail(function (error) { emitter.emit('error', error); }); diff --git a/lib/commands/update.js b/lib/commands/update.js index a923a7e1..f1e67164 100644 --- a/lib/commands/update.js +++ b/lib/commands/update.js @@ -22,7 +22,8 @@ function update(names, options, config) { project.update(names, options) .then(function (installed) { emitter.emit('end', installed); - }, function (error) { + }) + .fail(function (error) { emitter.emit('error', error); }); diff --git a/lib/core/Manager.js b/lib/core/Manager.js index 26f093cc..1298aead 100644 --- a/lib/core/Manager.js +++ b/lib/core/Manager.js @@ -164,13 +164,13 @@ Manager.prototype.areCompatible = function (candidate, resolved) { } // If target is a version, compare against the resolved version - if (semver.valid(candidate.target) != null) { + if (semver.valid(candidate.target)) { return semver.eq(candidate.target, resolvedVersion); } // If target is a range, check if the max versions of the range are the same // and if the resolved version satisfies the candidate target - if (semver.validRange(candidate.target) != null) { + if (semver.validRange(candidate.target)) { highestCandidate = this._getCap(semver.toComparators(candidate.target), 'highest'); highestResolved = this._getCap(semver.toComparators(resolved.target), 'highest'); @@ -580,7 +580,7 @@ Manager.prototype._electSuitable = function (name, semvers, nonSemvers) { }); if (resolution && !unresolvable) { - if (semver.valid(resolution) != null || semver.validRange(resolution) != null) { + if (semver.validRange(resolution)) { suitable = mout.array.findIndex(picks, function (pick) { return semver.satisfies(pick.pkgMeta.version, resolution); }); diff --git a/lib/core/Project.js b/lib/core/Project.js index 2ab5ba0d..d2b8afdc 100644 --- a/lib/core/Project.js +++ b/lib/core/Project.js @@ -3,6 +3,7 @@ var path = require('path'); var fs = require('graceful-fs'); var crypto = require('crypto'); var Q = require('q'); +var semver = require('semver'); var mout = require('mout'); var rimraf = require('rimraf'); var promptly = require('promptly'); @@ -44,7 +45,7 @@ Project.prototype.install = function (endpoints, options) { return this._analyse() .spread(function (json, tree) { // Walk down the tree adding targets, resolved and incompatibles - that._walkTree(tree, function (node, name) { + that.walkTree(tree, function (node, name) { if (node.missing || node.linked) { targets.push(node); } else if (node.incompatible) { @@ -72,18 +73,14 @@ Project.prototype.install = function (endpoints, options) { // Handle save and saveDev options if (options.save || options.saveDev) { mout.object.forOwn(targets, function (decEndpoint) { - var source = decEndpoint.source === decEndpoint.name ? '' : decEndpoint.source; - var target = decEndpoint.target; - var endpoint = mout.string.ltrim(source + '#' + target, ['#']); + var jsonEndpoint = endpointParser.decomposed2json(decEndpoint); if (options.save) { - that._json.dependencies = that._json.dependencies || {}; - that._json.dependencies[decEndpoint.name] = endpoint; + that._json.dependencies = mout.object.mixIn(that._json.dependencies || {}, jsonEndpoint); } if (options.saveDev) { - that._json.devDependencies = that._json.devDependencies || {}; - that._json.devDependencies[decEndpoint.name] = endpoint; + that._json.devDependencies = mout.object.mixIn(that._json.devDependencies || {}, jsonEndpoint); } }); } @@ -121,7 +118,7 @@ Project.prototype.update = function (names, options) { // If no names were specified, update every package if (!names) { // Mark each root dependency as targets - that._walkTree(tree, function (node) { + that.walkTree(tree, function (node) { targets.push(node); return false; }, true); @@ -146,7 +143,7 @@ Project.prototype.update = function (names, options) { // Walk down the tree adding missing, incompatible // and resolved - that._walkTree(tree, function (node, name) { + that.walkTree(tree, function (node, name) { if (node.missing || node.linked) { targets.push(node); } else if (node.incompatible) { @@ -157,7 +154,7 @@ Project.prototype.update = function (names, options) { }, true); // Add root packages whose names are specified to be updated - that._walkTree(tree, function (node, name) { + that.walkTree(tree, function (node, name) { if (!node.missing && !node.linked && names.indexOf(name) !== -1) { targets.push(node); } @@ -221,7 +218,7 @@ Project.prototype.uninstall = function (names, options) { var dependants = []; // Walk the down the tree, gathering dependants of the package - that._walkTree(tree, function (node, nodeName) { + that.walkTree(tree, function (node, nodeName) { if (name === nodeName) { dependants.push.apply(dependants, mout.object.values(node.dependants)); } @@ -301,8 +298,22 @@ Project.prototype.getTree = function () { .spread(function (json, tree, flattened) { var extraneous = []; - tree = this._manager.toData(tree, ['missing', 'incompatible', 'linked']); + tree = this._manager.toData(tree, ['missing', 'linked']); + // Mark incompatibles + this.walkTree(tree, function (node) { + var version; + var target = node.endpoint.target; + + if (target !== '*' && semver.validRange(target)) { + version = node.pkgMeta.version; + if (!version || !semver.satisfies(version, target)) { + node.incompatible = true; + } + } + }, true); + + // Find extraneous mout.object.forOwn(flattened, function (pkg) { if (pkg.extraneous) { extraneous.push(this._manager.toData(pkg, ['linked'])); @@ -317,6 +328,47 @@ Project.prototype.getJson = function () { return this._readJson(); }; +Project.prototype.walkTree = function (node, fn, onlyOnce) { + var result; + var dependencies; + var queue = mout.object.values(node.dependencies); + + if (onlyOnce === true) { + onlyOnce = []; + } + + while (queue.length) { + node = queue.shift(); + result = fn(node, node.name); + + // Abort traversal if result is false + if (result === false) { + continue; + } + + // Add dependencies to the queue + dependencies = mout.object.values(node.dependencies); + // If onlyOnce was true, do not add if already traversed + if (onlyOnce) { + dependencies = dependencies.filter(function (dependency) { + return !mout.array.find(onlyOnce, function (stacked) { + if (dependency.endpoint) { + return mout.object.equals(dependency.endpoint, stacked.endpoint); + } + + return dependency.name === stacked.name && + dependency.source === stacked.source && + dependency.target === stacked.target; + }); + }); + + onlyOnce.push.apply(onlyOnce, dependencies); + } + + queue.unshift.apply(queue, dependencies); + } +}; + // ----------------- Project.prototype._analyse = function () { @@ -613,41 +665,6 @@ Project.prototype._removePackages = function (packages, options) { }); }; -Project.prototype._walkTree = function (node, fn, onlyOnce) { - var queue = mout.object.values(node.dependencies); - var result; - var dependencies; - - if (onlyOnce === true) { - onlyOnce = []; - } - - while (queue.length) { - node = queue.shift(); - result = fn(node, node.name); - - if (onlyOnce) { - onlyOnce.push(node); - } - - // Abort traversal if result is false - if (result === false) { - continue; - } - - // Add dependencies to the queue - dependencies = mout.object.values(node.dependencies); - // If onlyOnce was true, do not add if already traversed - if (onlyOnce) { - dependencies = dependencies.filter(function (dependency) { - return onlyOnce.indexOf(dependency) === -1; - }); - } - - queue.unshift.apply(queue, dependencies); - } -}; - Project.prototype._restoreNode = function (node, flattened, jsonKey) { var deps; diff --git a/lib/core/ResolveCache.js b/lib/core/ResolveCache.js index af2c6f17..eafea802 100644 --- a/lib/core/ResolveCache.js +++ b/lib/core/ResolveCache.js @@ -47,7 +47,7 @@ ResolveCache.prototype.retrieve = function (source, target) { var suitable; // If target is a semver, find a suitable version - if (semver.valid(target) != null || semver.validRange(target) != null) { + if (semver.validRange(target)) { suitable = mout.array.find(versions, function (version) { return semver.satisfies(version, target); }); @@ -100,7 +100,7 @@ ResolveCache.prototype.versions = function (source) { return this._getVersions(sourceId) .spread(function (versions) { return versions.filter(function (version) { - return semver.valid(version) != null; + return semver.valid(version); }); }); }; @@ -331,8 +331,8 @@ ResolveCache.prototype._getVersions = function (sourceId) { ResolveCache.prototype._sortVersions = function (versions) { // Sort DESC versions.sort(function (version1, version2) { - var validSemver1 = semver.valid(version1) != null; - var validSemver2 = semver.valid(version2) != null; + var validSemver1 = semver.valid(version1); + var validSemver2 = semver.valid(version2); // If both are semvers, compare them if (validSemver1 && validSemver2) { diff --git a/lib/renderers/StandardRenderer.js b/lib/renderers/StandardRenderer.js index 358e700a..5aa01182 100644 --- a/lib/renderers/StandardRenderer.js +++ b/lib/renderers/StandardRenderer.js @@ -44,7 +44,7 @@ StandardRenderer.prototype.error = function (err) { err.id = err.code || 'error'; err.level = 'error'; - str = this._prefix(err) + ' ' + err.message.trim() + '\n'; + str = this._prefix(err) + ' ' + err.message.replace(/\r?\n/g, ' ').trim() + '\n'; this._write(process.stderr, 'bower ' + str); // Check if additional details were provided @@ -59,7 +59,7 @@ StandardRenderer.prototype.error = function (err) { // TODO: In some situations, err.stack is meaningless // Investigate why and find alternatives str = '\nStack trace:\n'.yellow; - str += err.stack; + str += err.stack + '\n'; this._write(process.stderr, str); } }; @@ -323,11 +323,13 @@ StandardRenderer.prototype._tree2archy = function (node) { var dependencies = mout.object.values(node.dependencies); var version = !node.missing ? node.pkgMeta._release || node.pkgMeta.version : null; var label = node.endpoint.name + (version ? '#' + version : ''); + var update; if (node.root) { label += ' ' + node.canonicalDir; } + // State lables if (node.missing) { label += ' missing'.red; return label; @@ -343,6 +345,23 @@ StandardRenderer.prototype._tree2archy = function (node) { label += ' extraneous'.green; } + // New versions + if (node.update) { + update = ''; + + if (node.update.target && node.pkgMeta.version !== node.update.target) { + update += node.update.target + ' available'; + } + + if (update && node.update.latest !== node.update.target) { + update += ', latest is ' + node.update.latest; + } + + if (update) { + label += ' (' + update.cyan + ')'; + } + } + if (!dependencies.length) { return label; } diff --git a/lib/util/endpointParser.js b/lib/util/endpointParser.js index 471ba546..748d3d33 100644 --- a/lib/util/endpointParser.js +++ b/lib/util/endpointParser.js @@ -49,6 +49,23 @@ function json2decomposed(key, value) { return decompose(endpoint); } +function decomposed2json(decEndpoint) { + var key = decEndpoint.name; + var value = ''; + var ret = {}; + + if (decEndpoint.source !== decEndpoint.name) { + value += decEndpoint.source + '#'; + } + + value += decEndpoint.target; + + ret[key] = value; + + return ret; +} + module.exports.decompose = decompose; module.exports.compose = compose; module.exports.json2decomposed = json2decomposed; +module.exports.decomposed2json = decomposed2json; diff --git a/package.json b/package.json index b3a03259..feef849b 100644 --- a/package.json +++ b/package.json @@ -40,7 +40,7 @@ "rc": "~0.3.0", "request": "~2.21.0", "rimraf": "~2.1.4", - "semver": "~2.0.7", + "semver": "~2.0.8", "stringify-object": "~0.1.4", "tar": "~0.1.17", "tmp": "~0.0.20",