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)
This commit is contained in:
André Cruz
2013-06-24 23:19:59 +01:00
parent ee3941b86a
commit f9f8f7aebd
16 changed files with 259 additions and 76 deletions

View File

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

View File

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

View File

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

View File

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

View File

@@ -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'),

View File

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

125
lib/commands/link.js Normal file
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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