diff --git a/bin/bower_new b/bin/bower_new index da6c84b0..a23b7229 100755 --- a/bin/bower_new +++ b/bin/bower_new @@ -1,19 +1,25 @@ #!/usr/bin/env node -process.title = 'bower'; - -var path = require('path'); -var nopt = require('nopt'); -var pkg = require(path.join(__dirname, '..', 'package.json')); require('colors'); +var path = require('path'); +var mout = require('mout'); +var pkg = require(path.join(__dirname, '..', 'package.json')); +var cli = require('../lib/util/cli'); +var commands = require('../lib/commands'); // -------- -var options = nopt({ - version: Boolean -}, { - 'v': ['--version'] -}, process.argv); +var options; +var command; +var renderer; +var levels; + +process.title = 'bower'; + +// Read CLI options +options = cli.readOptions(process.argv, { + version: { type: Boolean, shorthand: 'v' } +}); // Handle print of version if (options.version) { @@ -21,28 +27,56 @@ if (options.version) { process.exit(); } -// TODO: remove this later to appropriate command. -var Q = require('q'); -var Project = require('../lib/core/Project'); +// Parse log levels +levels = options['log-levels']; +if (levels) { + levels = levels + .split(',') + .map(function (level) { return level.trim(); }); +} -Q.longStackJumpLimit = 0; +// Get the command to execute +// TODO: abbreviations +command = options.argv.remain && options.argv.remain.shift(); +command = commands[command] || commands.help; -var test = new Project({ - offline: options.offline, - force: options.force -}); +// Get the renderer +renderer = cli.createRenderer(options); -test.install(/*['jquery-ui']*/) -.progress(function (notification) { - var id = notification.origin + '#' + notification.endpoint.target; - id = notification.type === 'warn' ? id.yellow : id.cyan; +// Execute the command +process.stdout.write(renderer.head()); +command.line(process.argv) +.on('data', function (data) { + var renderFuncName; + var renderFunc; + var str; - process.stdout.write('bower ' + id + ' ' + notification.data + '\n'); + // Check if this log level is allowed + if (levels && levels.indexOf(data.level) === -1) { + return; + } + + // Attempt to use renderer function specified by the tag + // Tags can be namespaced; this means that if the tag is + // "help.install" it will call "renderer.help.install()" + // Fallback to data if not found + renderFuncName = data.tag + .split('.') + .map(function (slice) { return mout.string.camelCase(slice); }) + .join('.'); + + renderFunc = mout.object.get(renderer, renderFuncName) || renderer.data; + str = renderFunc(data); + + // If type is warn, print to stderr instead + process[data.type === 'warn' ? 'stderr' : 'stdout'].write(str); }) -.then(function () { - process.exit(); -}, function (err) { - throw err; - //process.stderr.write(err.message + '\n'); - //process.exit(1); +.on('end', function (data) { + process.stdout.write(renderer.end(data)); + process.stdout.write(renderer.tail()); +}) +.on('error', function (err) { + process.stderr.write(renderer.error(err)); + process.stdout.write(renderer.tail()); + process.exit(1); }); \ No newline at end of file diff --git a/lib/commands/help.js b/lib/commands/help.js new file mode 100644 index 00000000..58212994 --- /dev/null +++ b/lib/commands/help.js @@ -0,0 +1,17 @@ +var Emitter = require('events').EventEmitter; + +function help(name) { + var emitter = new Emitter(); + + // TODO + return emitter; +} + +// ------------------- + +module.exports = help; + +module.exports.line = function (argv) { + // TODO + return help(); +}; \ No newline at end of file diff --git a/lib/commands/index.js b/lib/commands/index.js index 9d41f48e..bc3f0fee 100644 --- a/lib/commands/index.js +++ b/lib/commands/index.js @@ -1,3 +1,4 @@ module.exports = { - -}; \ No newline at end of file + 'help': require('./help'), + 'install': require('./install') +}; diff --git a/lib/commands/install.js b/lib/commands/install.js new file mode 100644 index 00000000..a246dae7 --- /dev/null +++ b/lib/commands/install.js @@ -0,0 +1,50 @@ +var Emitter = require('events').EventEmitter; +var Project = require('../core/Project'); +var cli = require('../util/cli'); +var help = require('./help'); + +function install(endpoints, options) { + var project; + var emitter = new Emitter(); + + options = options || {}; + + // Sanitize endpoints + if (endpoints && !endpoints.length) { + endpoints = null; + } + + project = new Project(options); + project.install(endpoints) + .then(function (installed) { + emitter.emit('end', installed); + }, function (error) { + emitter.emit('error', error); + }, function (notification) { + emitter.emit('data', notification); + }); + + return emitter; +} + +// ------------------- + +module.exports = install; + +module.exports.line = function (argv) { + var options = module.exports.options(argv); + + return options.help ? + help('install') : + install(options.argv.remain.slice(1), options); +}; + +module.exports.options = function (argv) { + return cli.readOptions(argv, { + 'save': { type: Boolean, shorthand: 'S' }, + 'save-dev': { type: Boolean, shorthand: 'D' }, + 'force': { type: Boolean, shorthand: 'f' }, + 'offline': { type: Boolean, shorthand: 'o' }, + 'production': { type: Boolean, shorthand: 'p' } + }); +}; diff --git a/lib/core/Manager.js b/lib/core/Manager.js index 5f6e7a22..6449b20a 100644 --- a/lib/core/Manager.js +++ b/lib/core/Manager.js @@ -100,8 +100,10 @@ Manager.prototype.install = function () { var release = decEndpoint.pkgMeta._release; deferred.notify({ - type: 'action', - data: 'Installing' + (release ? ' "' + release + '"' : ''), + level: 'action', + tag: 'install', + data: name + (release ? '#' + release : ''), + pkgMeta: decEndpoint.pkgMeta, origin: name, endpoint: decEndpoint }); @@ -181,6 +183,8 @@ Manager.prototype.areCompatible = function (first, second) { Manager.prototype._fetch = function (decEndpoint) { var name = decEndpoint.name; + var deferred = this._deferred; + var that = this; // Mark as being fetched this._fetching[name] = this._fetching[name] || []; @@ -192,24 +196,36 @@ Manager.prototype._fetch = function (decEndpoint) { // because it might be reused if a similar endpoint needs to be resolved decEndpoint.promise = this._repository.fetch(decEndpoint) // When done, call onFetch - .spread(this._onFetch.bind(this, decEndpoint)) + .spread(this._onFetch.bind(this, deferred, decEndpoint)) + // If it fails, we make the whole process to error out + .fail(function (err) { + err.origin = that._getOrigin(decEndpoint); + err.endpoint = decEndpoint; + deferred.reject(err); + }) // Listen to progress to proxy them to the resolve deferred // Note that we also mark where the notification is coming from .progress(function (notification) { + notification.origin = that._getOrigin(decEndpoint); notification.endpoint = decEndpoint; - notification.origin = name || decEndpoint.registryName || decEndpoint.resolverName; - this._deferred.notify(notification); - }.bind(this)); + deferred.notify(notification); + }); return decEndpoint.promise; }; -Manager.prototype._onFetch = function (decEndpoint, canonicalPkg, pkgMeta) { +Manager.prototype._onFetch = function (deferred, decEndpoint, canonicalPkg, pkgMeta) { var name; var resolved; var index; var initialName = decEndpoint.name; + // If the deferred associated with the process is already rejected, + // do not proceed. + if (deferred.promise.isRejected()) { + return; + } + // Remove from being fetched list mout.array.remove(this._fetching[initialName], decEndpoint); this._nrFetching--; @@ -373,6 +389,10 @@ Manager.prototype._dissect = function () { this._deferred.resolve(pkgMetas); }; +Manager.prototype._getOrigin = function (decEndpoint) { + return decEndpoint.name || decEndpoint.registryName || decEndpoint.resolverName; +}; + Manager.prototype._findHighestVersion = function (comparators) { var highest; var matches; @@ -403,4 +423,4 @@ Manager.prototype._findHighestVersion = function (comparators) { return highest; }; -module.exports = Manager; \ No newline at end of file +module.exports = Manager; diff --git a/lib/core/PackageRepository.js b/lib/core/PackageRepository.js index 294d8038..b6bdf796 100644 --- a/lib/core/PackageRepository.js +++ b/lib/core/PackageRepository.js @@ -25,7 +25,7 @@ var PackageRepository = function (options) { // ----------------- PackageRepository.prototype.fetch = function (decEndpoint) { - var resolver; + var res; var deferred = Q.defer(); var that = this; @@ -33,15 +33,20 @@ PackageRepository.prototype.fetch = function (decEndpoint) { resolverFactory(decEndpoint, this._options) // Decide if we retrieve from the cache or not // Also decide we if validate the cached entry or not - .then(function (res) { - resolver = res; + .then(function (resolver) { + res = resolver; // Set the resolver name in the decEndpoint - decEndpoint.resolverName = res.getName(); + decEndpoint.resolverName = resolver.getName(); // If force flag is used, bypass cache if (that._options.force) { - deferred.notify({ type: 'action', data: 'Resolving' }); + deferred.notify({ + level: 'action', + tag: 'resolve', + resolver: resolver, + data: 'Resolving ' + resolver.getSource(), + }); return that._resolve(resolver); } @@ -54,23 +59,50 @@ PackageRepository.prototype.fetch = function (decEndpoint) { if (!canonicalPkg) { // And the offline flag is passed, error out if (that._options.offline) { - throw createError('No cached version for ' + resolver.getTarget(), 'ENOCACHE'); + throw createError('No cached version for ' + resolver.getSource() + '#' + resolver.getTarget(), 'ENOCACHE', { + resolver: resolver + }); } // Otherwise, we have to resolve it - deferred.notify({ type: 'action', data: 'No cached version, resolving..' }); + deferred.notify({ + level: 'info', + tag: 'not-cached', + data: 'No cached version for ' + resolver.getSource() + '#' + resolver.getTarget(), + }); + deferred.notify({ + level: 'action', + tag: 'resolve', + resolver: resolver, + data: 'Resolving ' + resolver.getSource() + '#' + resolver.getTarget(), + }); + return that._resolve(resolver); } + deferred.notify({ + level: 'info', + tag: 'cached', + data: 'Got cached ' + resolver.getSource() + (pkgMeta._release ? '#' + pkgMeta._release: '') + ' entry', + canonicalPkg: canonicalPkg, + pkgMeta: pkgMeta + }); + // If offline flag is used, use directly the cached one if (that._options.offline) { - deferred.notify({ type: 'action', data: 'Got cached version' }); return [canonicalPkg, pkgMeta]; } // Otherwise check for new contents process.nextTick(function () { - deferred.notify({ type: 'action', data: 'Got cached version, validating..' }); + deferred.notify({ + level: 'action', + tag: 'check', + data: 'Checking ' + resolver.getSource() + '#' + resolver.getTarget() + ' for newer version', + canonicalPkg: canonicalPkg, + pkgMeta: pkgMeta, + resolver: resolver + }); }); return resolver.hasNew(canonicalPkg, pkgMeta) @@ -82,13 +114,28 @@ PackageRepository.prototype.fetch = function (decEndpoint) { } // Otherwise resolve to the newest one - deferred.notify({ type: 'action', data: 'There\'s a new version, resolving..' }); + deferred.notify({ + type: 'info', + data: 'There\'s a new version for ' + resolver.getSource() + '#' + resolver.getTarget(), + canonicalPkg: canonicalPkg, + pkgMeta: pkgMeta, + resolver: resolver + }); + deferred.notify({ + type: 'resolve', + resolver: resolver, + data: 'Resolving ' + resolver.getSource() + '#' + resolver.getTarget() + }); return that._resolve(resolver); }); }); }) - .then(deferred.resolve, deferred.reject, deferred.notify); + .then(deferred.resolve, deferred.reject, function (notification) { + // Store the resolver for each notification + notification.resolver = res; + deferred.notify(notification); + }); return deferred.promise; }; @@ -115,4 +162,4 @@ PackageRepository.prototype._resolve = function (resolver) { }.bind(this)); }; -module.exports = PackageRepository; \ No newline at end of file +module.exports = PackageRepository; diff --git a/lib/core/Project.js b/lib/core/Project.js index fd0d1f9f..396ce501 100644 --- a/lib/core/Project.js +++ b/lib/core/Project.js @@ -184,7 +184,9 @@ Project.prototype._readJson = function () { if (path.basename(filename) === 'component.json') { process.nextTick(function () { deferred.notify({ - type: 'warn', + level: 'warn', + tag: 'deprecated', + json: filename, data: 'You are using the deprecated component.json file' }); }); @@ -285,4 +287,4 @@ Project.prototype._restoreNode = function (node, flattened, jsonKey) { }, this); }; -module.exports = Project; \ No newline at end of file +module.exports = Project; diff --git a/lib/core/ResolveCache.js b/lib/core/ResolveCache.js index 26683b37..a3021ab8 100644 --- a/lib/core/ResolveCache.js +++ b/lib/core/ResolveCache.js @@ -208,4 +208,4 @@ ResolveCache.prototype._sortVersions = function (versions) { }); }; -module.exports = ResolveCache; \ No newline at end of file +module.exports = ResolveCache; diff --git a/lib/core/resolverFactory.js b/lib/core/resolverFactory.js index eb8aaeb5..d1d637bf 100644 --- a/lib/core/resolverFactory.js +++ b/lib/core/resolverFactory.js @@ -103,7 +103,7 @@ function createResolver(decEndpoint, options) { }); }; }) - // If we got the func, simply call and return + // If we got the function, simply call and return .then(function (func) { return func(); // Finally throw a meaningful error diff --git a/lib/core/resolvers/GitResolver.js b/lib/core/resolvers/GitResolver.js index e745bd7e..a2c366df 100644 --- a/lib/core/resolvers/GitResolver.js +++ b/lib/core/resolvers/GitResolver.js @@ -40,18 +40,30 @@ GitResolver.prototype._hasNew = function (canonicalPkg, pkgMeta) { GitResolver.prototype._resolve = function () { var deferred = Q.defer(); - deferred.notify({ type: 'action', data: 'Finding resolution' }); + deferred.notify({ + level: 'action', + tag: 'versions', + data: 'Finding resolution' + }); this._findResolution() .then(function (resolution) { - deferred.notify({ type: 'action', data: 'Checking out "' + (resolution.tag || resolution.branch || resolution.commit) + '"' }); + deferred.notify({ + level: 'action', + tag: 'checkout', + data: 'Checking out "' + (resolution.tag || resolution.branch || resolution.commit) + '"' + }); return this._checkout() // Always run cleanup after checkout to ensure that .git is removed! // If it's not removed, problems might arise when the "tmp" module attempts // to delete the temporary folder .fin(function () { - deferred.notify({ type: 'action', data: 'Cleaning up' }); + deferred.notify({ + level: 'action', + tag: 'cleanup', + data: 'Cleaning up git artifacts' + }); return this._cleanup(); }.bind(this)); }.bind(this)) @@ -179,7 +191,8 @@ GitResolver.prototype._savePkgMeta = function (meta) { if (typeof meta.version === 'string' && semver.neq(meta.version, version)) { process.nextTick(function (metaVersion) { deferred.notify({ - type: 'warn', + level: 'warn', + tag: 'mismatch', data: 'Version declared in the json (' + metaVersion + ') is different than the resolved one (' + version + ')' }); }.bind(this, meta.version)); @@ -194,7 +207,7 @@ GitResolver.prototype._savePkgMeta = function (meta) { } // Save version/commit/branch/tag in the release - meta._release = version || this._resolution.tag || this._resolution.commit; + meta._release = version || this._resolution.tag || this._resolution.commit.substr(0, 10); // Save resolution to be used in hasNew later meta._resolution = this._resolution; diff --git a/lib/core/resolvers/Resolver.js b/lib/core/resolvers/Resolver.js index ede8662b..2e814ced 100644 --- a/lib/core/resolvers/Resolver.js +++ b/lib/core/resolvers/Resolver.js @@ -155,7 +155,9 @@ Resolver.prototype._readJson = function (dir) { // If it is a component.json, warn about the deprecation if (path.basename(filename) === 'component.json') { deferred.notify({ - type: 'warn', + level: 'warn', + tag: 'deprecated', + json: filename, data: 'Package "' + this._name + '" is using the deprecated component.json file' }); } diff --git a/lib/core/resolvers/UrlResolver.js b/lib/core/resolvers/UrlResolver.js index 5a421fed..840705ab 100644 --- a/lib/core/resolvers/UrlResolver.js +++ b/lib/core/resolvers/UrlResolver.js @@ -217,7 +217,7 @@ UrlResolver.prototype._savePkgMeta = function (meta) { // Store ETAG under _release if (meta._cacheHeaders.ETag) { - meta._release = 'e-tag: ' + mout.string.trim(meta._cacheHeaders.ETag, '"'); + meta._release = 'e-tag:' + mout.string.trim(meta._cacheHeaders.ETag.substr(0, 10), '"'); } // Store main if is a single file diff --git a/lib/renderers/cli.js b/lib/renderers/cli.js new file mode 100644 index 00000000..a0b24278 --- /dev/null +++ b/lib/renderers/cli.js @@ -0,0 +1,94 @@ +var mout = require('mout'); + +var paddings = { + tag: 10, + tagPlusLabel: 30 +}; + +var tagColors = { + 'warn': 'yellow', + 'error': 'red', + '_default': 'cyan', +}; + +// ------------------------- + +function renderData(data) { + // Ensure data + data.data = data.data || ''; + + return 'bower ' + renderTagPlusLabel(data) + ' ' + data.data + '\n'; +} + +function renderError(err) { + var str; + + err.level = 'error'; + err.tag = err.code; + + str = 'bower ' + renderTagPlusLabel(err) + ' ' + err.message + '\n'; + + // Check if additional details were provided + if (err.details) { + str += err.details + '\n'; + } + + // Print trace + str += '\n' + err.stack + '\n'; + + return str; +} + +function renderEnd() { + return ''; +} + +// ------------------------- + +function empty() { + return ''; +} + +function uncolor(str) { + return str.replace(/\x1B\[\d+m/g, ''); +} + +function renderTagPlusLabel(data) { + var label; + var length; + var nrSpaces; + var tag = data.tag; + var tagColor = tagColors[data.level] || tagColors._default; + + // If there's not enough space, print only the tag + if (process.stdout.columns < 120) { + return mout.string.rpad(tag, paddings.tag)[tagColor]; + } + + label = data.origin + '#' + data.endpoint.target; + length = tag.length + label.length + 1; + nrSpaces = paddings.tagPlusLabel - length; + + // Make at least one space + if (nrSpaces < 1) { + nrSpaces = 1; + } + + return tag[tagColor] + mout.string.repeat(' ', nrSpaces) + label.green; +} + +module.exports.colorful = {}; +module.exports.colorful.head = empty; +module.exports.colorful.tail = empty; + +module.exports.colorful.data = renderData; +module.exports.colorful.error = renderError; +module.exports.colorful.end = renderEnd; + +// The colorless variant simply removes the colors from the colorful methods +module.exports.colorless = mout.object.map(module.exports.colorful, function (fn) { + return function () { + var str = fn.apply(fn, arguments); + return uncolor(str); + }; +}); diff --git a/lib/renderers/index.js b/lib/renderers/index.js new file mode 100644 index 00000000..11086987 --- /dev/null +++ b/lib/renderers/index.js @@ -0,0 +1,5 @@ +module.exports = { + cli: require('./cli'), + json: require('./json'), + mute: require('./mute') +}; \ No newline at end of file diff --git a/lib/renderers/json.js b/lib/renderers/json.js new file mode 100644 index 00000000..06a31af6 --- /dev/null +++ b/lib/renderers/json.js @@ -0,0 +1,38 @@ +var circularJson = require('circular-json'); + +function renderHead() { + return '['; +} + +function renderTail() { + return ']\n'; +} + +function renderData(data) { + return stringify(data) + ', '; +} + +function renderError(err) { + return stringify(err) + ', '; +} + +function renderEnd(data) { + return data ? stringify(data) : ''; +} + +// ------------------------- + +function uncolor(str) { + return str.replace(/\x1B\[\d+m/g, ''); +} + +function stringify(data) { + return uncolor(circularJson.stringify(data, null, ' ')); +} + +module.exports.head = renderHead; +module.exports.tail = renderTail; + +module.exports.data = renderData; +module.exports.error = renderError; +module.exports.end = renderEnd; \ No newline at end of file diff --git a/lib/renderers/mute.js b/lib/renderers/mute.js new file mode 100644 index 00000000..128b9990 --- /dev/null +++ b/lib/renderers/mute.js @@ -0,0 +1,7 @@ +function empty() { + return ''; +} + +module.exports.data = empty; +module.exports.error = empty; +module.exports.end = empty; \ No newline at end of file diff --git a/lib/util/cli.js b/lib/util/cli.js new file mode 100644 index 00000000..281d7972 --- /dev/null +++ b/lib/util/cli.js @@ -0,0 +1,41 @@ +var mout = require('mout'); +var nopt = require('nopt'); +var renderers = require('../renderers'); + +function readOptions(argv, options) { + var types; + var shorthands = {}; + + // Configure options that are common to all commands + options.help = { type: Boolean, shorthand: 'h' }; + options.color = { type: Boolean }; + options.silent = { type: Boolean, shorthand: 's' }; + options.json = { type: Boolean }; + options['log-levels'] = { type: String }; + + types = mout.object.map(options, function (option) { + return option.type; + }); + mout.object.forOwn(options, function (option, name) { + shorthands[option.shorthand] = '--' + name; + }); + + return nopt(types, shorthands, argv); +} + +function getRenderer(options) { + if (options.silent) { + return renderers.mute; + } + + if (options.json) { + return renderers.json; + } + + return options.color === false ? + renderers.cli.colorless : + renderers.cli.colorful; +} + +module.exports.readOptions = readOptions; +module.exports.createRenderer = getRenderer; \ No newline at end of file diff --git a/package.json b/package.json index 599cd957..c11cbfe6 100644 --- a/package.json +++ b/package.json @@ -34,7 +34,8 @@ "junk": "~0.2.0", "glob": "~3.2.1", "colors": "~0.6.0-1", - "ncp": "~0.4.2" + "ncp": "~0.4.2", + "circular-json": "~0.1.2" }, "devDependencies": { "mocha": "~1.8.2", diff --git a/test/core/resolverFactory.js b/test/core/resolverFactory.js index d0baac1a..a69684b7 100644 --- a/test/core/resolverFactory.js +++ b/test/core/resolverFactory.js @@ -17,7 +17,7 @@ describe('resolverFactory', function () { var tempSource; var options = {}; - options.registry = new RegistryClient(mout.object.fillIn({ + options.registryClient = new RegistryClient(mout.object.fillIn({ cache: defaultConfig._registry }, defaultConfig)); diff --git a/test/core/resolvers/gitResolver.js b/test/core/resolvers/gitResolver.js index ceb125b9..4f7d5b2b 100644 --- a/test/core/resolvers/gitResolver.js +++ b/test/core/resolvers/gitResolver.js @@ -735,7 +735,7 @@ describe('GitResolver', function () { .done(); }); - it('should save the release (under _release)', function (next) { + it('should save the release in the package meta', function (next) { var resolver = new GitResolver('foo'); var metaFile = path.join(tempDir, '.bower.json'); @@ -774,7 +774,7 @@ describe('GitResolver', function () { }) .then(function (contents) { var json = JSON.parse(contents.toString()); - expect(json._release).to.equal('aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'); + expect(json._release).to.equal('aaaaaaaaaa'); }) // Test with type 'commit' .then(function () { @@ -786,7 +786,7 @@ describe('GitResolver', function () { }) .then(function (contents) { var json = JSON.parse(contents.toString()); - expect(json._release).to.equal('aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'); + expect(json._release).to.equal('aaaaaaaaaa'); next(); }) .done(); diff --git a/test/core/resolvers/urlResolver.js b/test/core/resolvers/urlResolver.js index 4ff9a9df..240957bf 100644 --- a/test/core/resolvers/urlResolver.js +++ b/test/core/resolvers/urlResolver.js @@ -447,7 +447,9 @@ describe('UrlResolver', function () { .done(); }); - it('should store cache headers in the package meta', function (next) { + it.skip('should save the release if there\'s a E-Tag'); + + it('should save cache headers', function (next) { var resolver; nock('http://bower.io')