From f9f8f7aebda72256481f0ec3dd10f18b52e207c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andre=CC=81=20Cruz?= Date: Mon, 24 Jun 2013 23:19:59 +0100 Subject: [PATCH] Implement link command. Also: - CS fixes - Remove options argument from commands that do not have them - Use console.trace instead of err.stack (more reliable) --- README.md | 3 +- bin/bower_new | 2 +- lib/commands/cache/clean.js | 6 +- lib/commands/help.js | 4 +- lib/commands/index.js | 1 + lib/commands/info.js | 4 +- lib/commands/link.js | 125 +++++++++++++++++++++++++++++ lib/commands/lookup.js | 7 +- lib/commands/search.js | 5 +- lib/commands/update.js | 1 - lib/core/Manager.js | 14 ++-- lib/core/Project.js | 70 ++++++++++++---- lib/renderers/StandardRenderer.js | 33 ++++++-- lib/util/copy.js | 8 +- lib/util/extract.js | 42 +++++----- test/core/resolvers/gitResolver.js | 10 +-- 16 files changed, 259 insertions(+), 76 deletions(-) create mode 100644 lib/commands/link.js diff --git a/README.md b/README.md index 73b2953a..1e095f52 100644 --- a/README.md +++ b/README.md @@ -214,8 +214,7 @@ function createResolver(decEndpoint, registryClient, config) -> Promise ``` The function is async to allow querying the Bower registry, etc. -The `registryClient` is an instance of [`RegistryClient`](https://github.com/bower/registry-client) to be used. If null, the registry won't be queried. -If `config` is not passed, the default config will be used. +The `registryClient` is an instance of [RegistryClient](https://github.com/bower/registry-client) to be used. If null, the registry won't be queried. #### ResolveCache diff --git a/bin/bower_new b/bin/bower_new index b8a64847..d0a5d9e6 100755 --- a/bin/bower_new +++ b/bin/bower_new @@ -74,7 +74,7 @@ if (!commandFunc) { command = 'help'; // If the user requested help, show the command's help // Do the same if the actual command is a group of other commands (e.g.: cache) -} else if (options.help || typeof commandFunc === 'object') { +} else if (options.help || !commandFunc.line) { emitter = commands.help(command); command = 'help'; // Call the line method diff --git a/lib/commands/cache/clean.js b/lib/commands/cache/clean.js index 73168565..35e3a314 100644 --- a/lib/commands/cache/clean.js +++ b/lib/commands/cache/clean.js @@ -128,9 +128,9 @@ function clearCompletion(config, logger) { file: dir }); }); - }, function (err) { - if (err.code !== 'ENOENT') { - throw err; + }, function (error) { + if (error.code !== 'ENOENT') { + throw error; } }); } diff --git a/lib/commands/help.js b/lib/commands/help.js index caac63cf..44398fd9 100644 --- a/lib/commands/help.js +++ b/lib/commands/help.js @@ -23,8 +23,8 @@ function help(name) { try { json = require(json); - } catch (err) { - return emitter.emit('error', err); + } catch (error) { + return emitter.emit('error', error); } emitter.emit('end', json); diff --git a/lib/commands/index.js b/lib/commands/index.js index a3b1929b..83a939b4 100644 --- a/lib/commands/index.js +++ b/lib/commands/index.js @@ -3,6 +3,7 @@ module.exports = { 'info': require('./info'), 'install': require('./install'), 'help': require('./help'), + 'link': require('./link'), 'list': require('./list'), 'lookup': require('./lookup'), 'register': require('./register'), diff --git a/lib/commands/info.js b/lib/commands/info.js index 1e8f04b5..130c5288 100644 --- a/lib/commands/info.js +++ b/lib/commands/info.js @@ -5,7 +5,7 @@ var Logger = require('../core/Logger'); var cli = require('../util/cli'); var defaultConfig = require('../config'); -function info(pkg, property, options, config) { +function info(pkg, property, config) { var repository; var emitter = new EventEmitter(); var logger = new Logger(); @@ -64,7 +64,7 @@ info.line = function (argv) { return null; } - return info(pkg, property, options); + return info(pkg, property); }; info.options = function (argv) { diff --git a/lib/commands/link.js b/lib/commands/link.js new file mode 100644 index 00000000..e10d48f4 --- /dev/null +++ b/lib/commands/link.js @@ -0,0 +1,125 @@ +var EventEmitter = require('events').EventEmitter; +var fs = require('fs'); +var path = require('path'); +var mkdirp = require('mkdirp'); +var rimraf = require('rimraf'); +var mout = require('mout'); +var Q = require('q'); +var Project = require('../core/Project'); +var Logger = require('../core/Logger'); +var createError = require('../util/createError'); +var cli = require('../util/cli'); +var defaultConfig = require('../config'); + +function linkSelf(config) { + var project; + var emitter = new EventEmitter(); + var logger = new Logger(); + + config = mout.object.deepMixIn(config || {}, defaultConfig); + project = new Project(config, logger); + + project.getJson() + .then(function (json) { + var src = config.cwd; + var dst = path.join(config.storage.links, json.name); + + // Delete previous link if any + return Q.nfcall(rimraf, dst) + // Link globally + .then(function () { + return createLink(src, dst); + }) + .then(function () { + emitter.emit('end', { + src: src, + dst: dst + }); + }); + }) + .fail(function (error) { + emitter.emit('error', error); + }); + + return logger.pipe(emitter); +} + +function linkTo(name, localName, config) { + var src; + var dst; + var emitter = new EventEmitter(); + + config = mout.object.deepMixIn(config || {}, defaultConfig); + + localName = localName || name; + src = path.join(config.storage.links, name); + dst = path.join(process.cwd(), config.directory, name); + + // Delete destination folder if any + Q.nfcall(rimraf, dst) + // Link locally + .then(function () { + return createLink(src, dst); + }) + .then(function () { + emitter.emit('end', { + src: src, + dst: dst + }); + }) + .fail(function (error) { + emitter.emit('error', error); + }); + + return emitter; +} + +function createLink(src, dst) { + var dstDir = path.dirname(dst); + + // Create directory + return Q.nfcall(mkdirp, dstDir) + // Check if source exists + .then(function () { + return Q.nfcall(fs.lstat, src) + .fail(function (error) { + if (error.code === 'ENOENT') { + throw createError('Failed to create link to ' + path.basename(src), 'ENOENT', { + details: src + ' doest not exists or points to a non-existent package' + }); + } + + throw error; + }); + }) + // Create symlink + .then(function () { + return Q.nfcall(fs.symlink, src, dst, 'dir'); + }); +} + +// ------------------- + +var link = {}; + +link.line = function (argv) { + var options = link.options(argv); + var name = options.argv.remain[1]; + var localName = options.argv.remain[2]; + + if (name) { + return linkTo(name, localName); + } + + return linkSelf(); +}; + +link.options = function (argv) { + return cli.readOptions(argv); +}; + +link.completion = function () { + // TODO: +}; + +module.exports = link; diff --git a/lib/commands/lookup.js b/lib/commands/lookup.js index d6313a5b..5c126e64 100644 --- a/lib/commands/lookup.js +++ b/lib/commands/lookup.js @@ -4,17 +4,16 @@ var RegistryClient = require('bower-registry-client'); var cli = require('../util/cli'); var defaultConfig = require('../config'); -function lookup(name, options, config) { +function lookup(name, config) { var registryClient; var emitter = new EventEmitter(); - options = options || {}; config = mout.object.deepMixIn(config || {}, defaultConfig); config.cache = config.storage.registry; registryClient = new RegistryClient(config); registryClient.lookup(name, function (error, entry) { - if (err) { + if (error) { return emitter.emit('error', error); } @@ -39,7 +38,7 @@ lookup.line = function (argv) { return null; } - return lookup(name, options); + return lookup(name); }; lookup.options = function (argv) { diff --git a/lib/commands/search.js b/lib/commands/search.js index 061d4bee..7df07635 100644 --- a/lib/commands/search.js +++ b/lib/commands/search.js @@ -4,11 +4,10 @@ var RegistryClient = require('bower-registry-client'); var cli = require('../util/cli'); var defaultConfig = require('../config'); -function search(name, options, config) { +function search(name, config) { var registryClient; var emitter = new EventEmitter(); - options = options || {}; config = mout.object.deepMixIn(config || {}, defaultConfig); config.cache = config.storage.registry; @@ -26,7 +25,7 @@ function search(name, options, config) { } function onResults(emitter, error, results) { - if (err) { + if (error) { return emitter.emit('error', error); } diff --git a/lib/commands/update.js b/lib/commands/update.js index 8ee29723..a923a7e1 100644 --- a/lib/commands/update.js +++ b/lib/commands/update.js @@ -38,7 +38,6 @@ update.line = function (argv) { update.options = function (argv) { return cli.readOptions({ - 'help': { type: Boolean, shorthand: 'h' }, 'production': { type: Boolean, shorthand: 'p' } }, argv); }; diff --git a/lib/core/Manager.js b/lib/core/Manager.js index aa913feb..c3a73ec8 100644 --- a/lib/core/Manager.js +++ b/lib/core/Manager.js @@ -112,19 +112,19 @@ Manager.prototype.install = function () { mout.object.forOwn(that._dissected, function (decEndpoint, name) { var promise; - var dest; + var dst; var release = decEndpoint.pkgMeta._release; that._logger.action('install', name + (release ? '#' + release : ''), that.toData(decEndpoint)); // Remove existent and copy canonical dir - dest = path.join(componentsDir, name); - decEndpoint.dest = dest; + dst = path.join(componentsDir, name); + decEndpoint.dst = dst; - promise = Q.nfcall(rimraf, dest) - .then(copy.copyDir.bind(copy, decEndpoint.canonicalDir, dest)) + promise = Q.nfcall(rimraf, dst) + .then(copy.copyDir.bind(copy, decEndpoint.canonicalDir, dst)) .then(function () { - var jsonFile = path.join(dest, '.bower.json'); + var jsonFile = path.join(dst, '.bower.json'); // Store _target in bower.json return Q.nfcall(fs.readFile, jsonFile) @@ -146,7 +146,7 @@ Manager.prototype.install = function () { // Resolve with meaningful data return mout.object.map(that._dissected, function (decEndpoint) { var pkg = this.toData(decEndpoint); - pkg.canonicalDir = decEndpoint.dest; + pkg.canonicalDir = decEndpoint.dst; return pkg; }, that); }) diff --git a/lib/core/Project.js b/lib/core/Project.js index b1e08d60..c4103309 100644 --- a/lib/core/Project.js +++ b/lib/core/Project.js @@ -45,7 +45,7 @@ Project.prototype.install = function (endpoints, options) { .spread(function (json, tree) { // Walk down the tree adding targets, resolved and incompatibles that._walkTree(tree, function (node, name) { - if (node.missing) { + if (node.missing || node.linked) { targets.push(node); } else if (node.incompatible) { incompatibles.push(node); @@ -147,7 +147,7 @@ Project.prototype.update = function (names, options) { // Walk down the tree adding missing, incompatible // and resolved that._walkTree(tree, function (node, name) { - if (node.missing) { + if (node.missing || node.linked) { targets.push(node); } else if (node.incompatible) { incompatibles.push(node); @@ -158,7 +158,7 @@ Project.prototype.update = function (names, options) { // Add root packages whose names are specified to be updated that._walkTree(tree, function (node, name) { - if (!node.missing && names.indexOf(name) !== -1) { + if (!node.missing && !node.linked && names.indexOf(name) !== -1) { targets.push(node); } }, true); @@ -301,7 +301,7 @@ Project.prototype.getTree = function () { .spread(function (json, tree, flattened) { var extraneous = []; - tree = this._manager.toData(tree, ['missing', 'incompatible']); + tree = this._manager.toData(tree, ['missing', 'incompatible', 'linked']); tree.root = true; mout.object.forOwn(flattened, function (pkg) { @@ -314,16 +314,21 @@ Project.prototype.getTree = function () { }.bind(this)); }; +Project.prototype.getJson = function () { + return this._readJson(); +}; + // ----------------- Project.prototype._analyse = function () { return Q.all([ this._readJson(), - this._readInstalled() + this._readInstalled(), + this._readLinks() ]) - .spread(function (json, installed) { + .spread(function (json, installed, links) { var root; - var flattened = mout.object.mixIn({}, installed); + var flattened = mout.object.mixIn({}, installed, links); root = { name: json.name, @@ -478,18 +483,17 @@ Project.prototype._readInstalled = function () { dot: true }) .then(function (filenames) { - var promises = []; + var promises; var decEndpoints = {}; // Foreach bower.json found - filenames.forEach(function (filename) { - var promise; + promises = filenames.map(function (filename) { var name = path.dirname(filename); filename = path.join(componentsDir, filename); // Read package metadata - promise = Q.nfcall(fs.readFile, filename) + return Q.nfcall(fs.readFile, filename) .then(function (contents) { return JSON.parse(contents.toString()); }) @@ -503,8 +507,6 @@ Project.prototype._readInstalled = function () { }; // Ignore if failed to read file }, function () {}); - - promises.push(promise); }); // Wait until all files have been read @@ -512,7 +514,47 @@ Project.prototype._readInstalled = function () { return Q.all(promises) .then(function () { return that._installed = decEndpoints; - }.bind(this)); + }); + }); +}; + +Project.prototype._readLinks = function () { + var componentsDir; + + // Read directory, looking for links + componentsDir = path.join(this._config.cwd, this._config.directory); + return Q.nfcall(fs.readdir, componentsDir) + .then(function (filenames) { + var promises; + var decEndpoints = {}; + + // Filter only those that are links + promises = filenames.map(function (filename) { + var dir = path.join(componentsDir, filename); + + return Q.nfcall(fs.lstat, dir) + .then(function (stat) { + if (stat.isSymbolicLink()) { + decEndpoints[filename] = { + name: filename, + source: dir, + target: '*', + canonicalDir: dir, + pkgMeta: { + name: filename + }, + linked: true + }; + } + }); + }); + + // Wait until all links have been read + // and resolve with the decomposed endpoints + return Q.all(promises) + .then(function () { + return decEndpoints; + }); }); }; diff --git a/lib/renderers/StandardRenderer.js b/lib/renderers/StandardRenderer.js index 3691c4d8..3dbb1a91 100644 --- a/lib/renderers/StandardRenderer.js +++ b/lib/renderers/StandardRenderer.js @@ -45,19 +45,25 @@ StandardRenderer.prototype.error = function (err) { err.level = 'error'; str = this._prefix(err) + ' ' + err.message + '\n'; + this._write(process.stderr, 'bower ' + str); // Check if additional details were provided if (err.details) { - str += mout.string.trim(err.details) + '\n'; + str = '\nAdditional error details:\n'.yellow + mout.string.trim(err.details) + '\n'; + this._write(process.stderr, str); } - // Print stack if verbose or the error has no code - // In some cases there's no stack (Maximum call stack exceeded errors) - if (err.stack && (this._config.verbose || !err.code)) { - str += '\n' + err.stack + '\n'; - } - this._write(process.stderr, 'bower ' + str); + // Print trace if verbose or the error has no code + if (this._config.verbose || !err.code) { + str = '\nStack trace:\n'.yellow; + this._write(process.stderr, str); + + // Use console.trace instead of err.stack because it was meaningless + // in some situations + // Worth investigating other solutions + console.trace(); + } }; StandardRenderer.prototype.log = function (log) { @@ -166,6 +172,16 @@ StandardRenderer.prototype._lookup = function (data) { this._write(process.stdout, str); }; +StandardRenderer.prototype._link = function (data) { + this._sizes.id = 4; + + this.log({ + id: 'link', + level: 'info', + message: data.src + ' > ' + data.dst + }); +}; + StandardRenderer.prototype._cacheList = function (entries) { entries.forEach(function (entry) { var pkgMeta = entry.pkgMeta; @@ -319,6 +335,9 @@ StandardRenderer.prototype._tree2archy = function (node) { label += ' missing'.red; return label; } + if (node.linked) { + label += ' linked'.magenta; + } if (node.incompatible) { label += ' incompatible'.yellow; diff --git a/lib/util/copy.js b/lib/util/copy.js index 23a307ab..a8ca905a 100644 --- a/lib/util/copy.js +++ b/lib/util/copy.js @@ -49,8 +49,8 @@ function parseOptions(opts) { // --------------------- // Available options: -// - mode: force final mode of dest (defaults to null) -// - copyMode: copy mode of src to dest, only if mode is not specified (defaults to true) +// - mode: force final mode of dst (defaults to null) +// - copyMode: copy mode of src to dst, only if mode is not specified (defaults to true) function copyFile(src, dst, opts) { var promise; @@ -74,8 +74,8 @@ function copyFile(src, dst, opts) { // Available options: // - ignore: array of patterns to be ignored (defaults to null) -// - mode: force final mode of dest (defaults to null) -// - copyMode: copy mode of src to dest, only if mode is not specified (defaults to true) +// - mode: force final mode of dst (defaults to null) +// - copyMode: copy mode of src to dst, only if mode is not specified (defaults to true) function copyDir(src, dst, opts) { var promise; diff --git a/lib/util/extract.js b/lib/util/extract.js index a7feee54..0b24bdad 100644 --- a/lib/util/extract.js +++ b/lib/util/extract.js @@ -29,54 +29,54 @@ extractors = { extractorTypes = Object.keys(extractors); -function extractZip(archive, dest) { +function extractZip(archive, dst) { var deferred = Q.defer(); fs.createReadStream(archive) .on('error', deferred.reject) - .pipe(unzip.Extract({ path: dest })) + .pipe(unzip.Extract({ path: dst })) .on('error', deferred.reject) - .on('close', deferred.resolve.bind(deferred, dest)); + .on('close', deferred.resolve.bind(deferred, dst)); return deferred.promise; } -function extractTar(archive, dest) { +function extractTar(archive, dst) { var deferred = Q.defer(); fs.createReadStream(archive) .on('error', deferred.reject) - .pipe(tar.Extract({ path: dest })) + .pipe(tar.Extract({ path: dst })) .on('error', deferred.reject) - .on('close', deferred.resolve.bind(deferred, dest)); + .on('close', deferred.resolve.bind(deferred, dst)); return deferred.promise; } -function extractTarGz(archive, dest) { +function extractTarGz(archive, dst) { var deferred = Q.defer(); fs.createReadStream(archive) .on('error', deferred.reject) .pipe(zlib.createGunzip()) .on('error', deferred.reject) - .pipe(tar.Extract({ path: dest })) + .pipe(tar.Extract({ path: dst })) .on('error', deferred.reject) - .on('close', deferred.resolve.bind(deferred, dest)); + .on('close', deferred.resolve.bind(deferred, dst)); return deferred.promise; } -function extractGz(archive, dest) { +function extractGz(archive, dst) { var deferred = Q.defer(); fs.createReadStream(archive) .on('error', deferred.reject) .pipe(zlib.createGunzip()) .on('error', deferred.reject) - .pipe(fs.createWriteStream(dest)) + .pipe(fs.createWriteStream(dst)) .on('error', deferred.reject) - .on('close', deferred.resolve.bind(deferred, dest)); + .on('close', deferred.resolve.bind(deferred, dst)); return deferred.promise; } @@ -124,9 +124,9 @@ function moveSingleDirContents(dir) { promises = files.map(function (file) { var src = path.join(dir, file); - var dest = path.join(destDir, file); + var dst = path.join(destDir, file); - return Q.nfcall(fs.rename, src, dest); + return Q.nfcall(fs.rename, src, dst); }); return Q.all(promises); @@ -145,7 +145,7 @@ function canExtract(target) { // Available options: // - keepArchive: true to keep the archive afterwards (defaults to false) // - keepStructure: true to keep the extracted structure unchanged (defaults to false) -function extract(src, dest, opts) { +function extract(src, dst, opts) { var extractor; var promise; @@ -158,11 +158,11 @@ function extract(src, dest, opts) { } // Extract archive - promise = extractor(src, dest); + promise = extractor(src, dst); - // TODO: There's an issue here if the src and dest are the same and + // TODO: There's an issue here if the src and dst are the same and // The zip name is the same as some of the zip file contents - // Maybe create a temp directory inside dest, unzip it there, + // Maybe create a temp directory inside dst, unzip it there, // unlink zip and then move contents // Remove archive @@ -177,16 +177,16 @@ function extract(src, dest, opts) { if (!opts.keepStructure) { promise = promise .then(function () { - return isSingleDir(dest); + return isSingleDir(dst); }) .then(function (singleDir) { return singleDir ? moveSingleDirContents(singleDir) : null; }); } - // Resolve promise to the dest dir + // Resolve promise to the dst dir return promise.then(function () { - return dest; + return dst; }); } diff --git a/test/core/resolvers/gitResolver.js b/test/core/resolvers/gitResolver.js index 9e3c3dc4..1be13c24 100644 --- a/test/core/resolvers/gitResolver.js +++ b/test/core/resolvers/gitResolver.js @@ -655,12 +655,12 @@ describe('GitResolver', function () { it('should remove the .git folder from the temp dir', function (next) { var resolver = create('foo'); - var dest = path.join(tempDir, '.git'); + var dst = path.join(tempDir, '.git'); this.timeout(15000); // Give some time to copy // Copy .git folder to the tempDir - copy.copyDir(path.resolve(__dirname, '../../../.git'), dest, { + copy.copyDir(path.resolve(__dirname, '../../../.git'), dst, { mode: 0777 }) .then(function () { @@ -668,7 +668,7 @@ describe('GitResolver', function () { return resolver._cleanup() .then(function () { - expect(fs.existsSync(dest)).to.be(false); + expect(fs.existsSync(dst)).to.be(false); next(); }); }) @@ -677,13 +677,13 @@ describe('GitResolver', function () { it('should not fail if .git does not exist for some reason', function (next) { var resolver = create('foo'); - var dest = path.join(tempDir, '.git'); + var dst = path.join(tempDir, '.git'); resolver._tempDir = tempDir; resolver._cleanup() .then(function () { - expect(fs.existsSync(dest)).to.be(false); + expect(fs.existsSync(dst)).to.be(false); next(); }) .done();