Merge pull request #92 from satazor/master

Unit of work implementation
This commit is contained in:
billy gates
2012-10-16 23:10:50 -07:00
19 changed files with 346 additions and 67 deletions

View File

@@ -37,4 +37,4 @@ bower.commands[bower.command || 'help'].line(input)
.on('error', function (err) {
if (options.verbose) throw err;
else template('error', { message: err.message }).on('data', function (d) { console.log (d); });
})
});

View File

@@ -23,10 +23,10 @@ module.exports = function (name) {
var templateName = name ? 'help-' + name : 'help';
if (!name) context = { commands: Object.keys(commands).join(', ') };
_.extend(context, config)
_.extend(context, config);
template(templateName, context).on('data', emitter.emit.bind(emitter, 'end'));
return emitter;
}
};
module.exports.line = function (argv) {
var options = nopt({}, {}, argv);

View File

@@ -25,7 +25,7 @@ var shorthand = { 'h': ['--help'], 'S': ['--save'] };
module.exports = function (paths, options) {
var emitter = new Emitter;
var manager = new Manager(paths)
var manager = new Manager(paths);
if (options && options.save) save(emitter, manager, paths);
@@ -44,4 +44,4 @@ module.exports.line = function (argv) {
if (options.help) return help('install');
return module.exports(paths, options);
};
};

View File

@@ -24,7 +24,7 @@ var shorthand = { 'h': ['--help'] };
var optionTypes = { help: Boolean, paths: Boolean, map: Boolean };
var getTree = function (packages, subPackages, result) {
var result = result || {};
result = result || {};
_.each(subPackages || packages, function (pkg, name) {
@@ -50,10 +50,10 @@ var generatePath = function (name, main) {
if (typeof main == 'string') {
return path.join(config.directory, name, main);
} else {
var main = main.map(function (main) { return generatePath(name, main); });
main = main.map(function (main) { return generatePath(name, main); });
return main.length == 1 ? main[0] : main;
}
}
};
var buildSource = function (pkg, shallow) {
var result = {};
@@ -72,7 +72,7 @@ var buildSource = function (pkg, shallow) {
}
return result;
}
};
var shallowTree = function (packages, tree) {
var result = {};
@@ -82,7 +82,7 @@ var shallowTree = function (packages, tree) {
});
return result;
}
};
var deepTree = function (packages, tree) {
@@ -91,7 +91,7 @@ var deepTree = function (packages, tree) {
Object.keys(tree).forEach(function (packageName) {
result[packageName] = {};
result[packageName].source = buildSource(packages[packageName])
result[packageName].source = buildSource(packages[packageName]);
if (Object.keys(tree[packageName]).length) {
result[packageName].dependencies = deepTree(packages, tree[packageName]);
@@ -100,7 +100,7 @@ var deepTree = function (packages, tree) {
});
return result;
}
};
var getNodes = function (packages, tree) {
return Object.keys(tree).map(function (key) {
@@ -125,7 +125,7 @@ var getNodes = function (packages, tree) {
return template('tree-branch', { package: key, version: version, upgrade: upgrade }, true);
}
});
}
};
var cliTree = function (emitter, packages, tree) {
emitter.emit('data', archy({

View File

@@ -77,5 +77,5 @@ module.exports.line = function (argv) {
if (options.help) return help('uninstall');
var names = options.argv.remain.slice(1);
return module.exports(names, options)
return module.exports(names, options);
};

View File

@@ -1,4 +1,4 @@
module.exports = {
directory: 'components',
directory: 'components',
json: 'component.json'
}
};

View File

@@ -13,14 +13,15 @@
// - end: fired when finished installing
// ==========================================
var Package = require('./package');
var config = require('./config');
var prune = require('../util/prune');
var events = require('events');
var async = require('async');
var path = require('path');
var glob = require('glob');
var fs = require('fs');
var Package = require('./package');
var UnitWork = require('./unit_work');
var config = require('./config');
var prune = require('../util/prune');
var events = require('events');
var async = require('async');
var path = require('path');
var glob = require('glob');
var fs = require('fs');
// read local dependencies (with versions)
// read json dependencies (resolving along the way into temp dir)
@@ -31,6 +32,7 @@ var Manager = function (endpoints) {
this.dependencies = {};
this.cwd = process.cwd();
this.endpoints = endpoints || [];
this.unitWork = new UnitWork;
};
Manager.prototype = Object.create(events.EventEmitter.prototype);
@@ -82,7 +84,6 @@ Manager.prototype.resolveEndpoints = function () {
Manager.prototype.loadJSON = function () {
var json = path.join(this.cwd, config.json);
fs.exists(json, function (exists) {
if (!exists) return this.emit('error', new Error('Could not find local ' + config.json));
fs.readFile(json, 'utf8', function (err, json) {

View File

@@ -31,6 +31,7 @@ var config = require('./config');
var source = require('./source');
var template = require('../util/template');
var readJSON = require('../util/read-json');
var UnitWork = require('./unit_work');
var temp = process.env.TMPDIR
|| process.env.TMP
@@ -43,13 +44,14 @@ var home = (process.platform === "win32"
var cache = process.platform === "win32"
? path.resolve(process.env.APPDATA || home || temp, "bower-cache")
: path.resolve(home || temp, ".bower")
: path.resolve(home || temp, ".bower");
var Package = function (name, endpoint, manager) {
this.dependencies = {};
this.json = {};
this.name = name;
this.manager = manager;
this.dependencies = {};
this.json = {};
this.name = name;
this.manager = manager;
this.unitWork = manager && manager.unitWork ? manager.unitWork : new UnitWork;
if (endpoint) {
@@ -69,6 +71,7 @@ var Package = function (name, endpoint, manager) {
this.tag = endpoint;
} else if (/^[\.\/~]\.?[^.]*\.(js|css)/.test(endpoint) && fs.statSync(endpoint).isFile()) {
this.path = path.resolve(endpoint);
this.assetType = path.extname(endpoint);
this.name = this.name.replace(this.assetType, '');
@@ -82,15 +85,35 @@ var Package = function (name, endpoint, manager) {
this.name = this.name.replace(this.assetType, '');
} else {
this.tag = endpoint.split('#', 2)[1];
try {
fs.statSync(endpoint);
this.path = path.resolve(endpoint);
} catch (e) {
this.tag = endpoint.split('#', 2)[1];
}
}
// Store a reference to the original tag
// This is because the tag gets rewriten later and the original tag
// must be used by the manager later on
this.originalTag = this.tag;
// Generate an id and a resourceId
// The id is an unique id that describes this package
// The resourceId is an unique id that describes the resource of this package
this.id = new Buffer(this.name + '%' + this.tag + '%' + this.gitUrl + '%' + this.path + '%' + this.assetUrl).toString('base64');
this.resourceId = new Buffer(this.gitUrl + '%' + this.path + '%' + this.assetUrl).toString('base64');
}
if (this.manager) {
this.on('data', this.manager.emit.bind(this.manager, 'data'));
this.on('error', this.manager.emit.bind(this.manager, 'error'));
}
// Cache a self bound function
this.waitUnlock = this.waitUnlock.bind(this);
this.setMaxListeners(30); // Increase the number of listeners because a package can have more than the default 10 dependencies
};
Package.prototype = Object.create(events.EventEmitter.prototype);
@@ -98,6 +121,29 @@ Package.prototype = Object.create(events.EventEmitter.prototype);
Package.prototype.constructor = Package;
Package.prototype.resolve = function () {
// Ensure that nobody is resolving the same dep at the same time
// If there is, we wait for the unlock event
if (this.unitWork.isLocked(this.name)) return this.unitWork.on('unlock', this.waitUnlock);
var data = this.unitWork.retrieve(this.name);
if (data) {
// Check if this exact package is the last resolved one
// If so, we copy the resolved result and we don't need to do anything else
if (data.id === this.id) {
this.unserialize(data);
return this.emit('resolve');
}
// Check if this exact package resource is the last resolved one
// This is to prevent it from being downloaded or copied over and over again in such case
if (data.resourceId === this.resourceId) {
this.path = data.path;
this.unitWork.lock(this.name, this);
return this.once('loadJSON', this.saveUnit).checkout();
}
}
// If not, we lock and resolve it
this.unitWork.lock(this.name, this);
if (this.assetUrl) {
this.download();
@@ -154,8 +200,8 @@ Package.prototype.generateAssetJSON = function () {
main: 'index' + this.assetType,
version: semverParser.exec(this.assetUrl) ? RegExp.$1 : "0.0.0",
repository: { type: "asset", url: this.assetUrl }
}
}
};
};
Package.prototype.uninstall = function () {
template('action', { name: 'uninstalling', shizzle: this.path })
@@ -176,15 +222,16 @@ Package.prototype.loadJSON = function (name) {
if (!name) return this.loadJSON('package.json');
return this.assetUrl ? this.emit('loadJSON') : this.path && this.on('describeTag', function (tag) {
this.version = this.tag = semver.clean(tag);
this.emit('loadJSON')
this.emit('loadJSON');
}.bind(this)).describeTag();
}
this.json = json;
this.name = this.json.name;
this.version = this.json.version;
this.emit('loadJSON');
}.bind(this), this);
}
};
Package.prototype.download = function () {
template('action', { name: 'downloading', shizzle: this.assetUrl })
@@ -209,7 +256,7 @@ Package.prototype.download = function () {
template('action', { name: 'redirect detected', shizzle: this.assetUrl })
.on('data', this.emit.bind(this, 'data'));
this.assetUrl = res.headers.location;
this.download();
return this.download();
}
res.on('data', function (data) {
@@ -218,13 +265,13 @@ Package.prototype.download = function () {
res.on('end', function () {
file.end();
this.once('loadJSON', this.addDependencies).loadJSON();
this.once('loadJSON', this.saveUnit).loadJSON();
}.bind(this));
}.bind(this)).on('error', this.emit.bind(this, 'error'));
}.bind(this));
}
};
Package.prototype.copy = function () {
template('action', { name: 'copying', shizzle: this.path }).on('data', this.emit.bind(this, 'data'));
@@ -238,7 +285,7 @@ Package.prototype.copy = function () {
if (this.assetType) {
return fs.readFile(this.path, function (err, data) {
fs.writeFile(path.join((this.path = tmpPath), 'index' + this.assetType), data, function () {
this.once('loadJSON', this.addDependencies).loadJSON();
this.once('loadJSON', this.saveUnit).loadJSON();
}.bind(this));
}.bind(this));
}
@@ -250,7 +297,7 @@ Package.prototype.copy = function () {
})
);
this.once('loadJSON', this.addDependencies);
this.once('loadJSON', this.saveUnit);
reader.on('error', this.emit.bind(this, 'error'));
reader.on('end', this.loadJSON.bind(this));
@@ -259,14 +306,20 @@ Package.prototype.copy = function () {
};
Package.prototype.getDeepDependencies = function (result) {
var result = result || [];
result = result || [];
for (var name in this.dependencies) {
result.push(this.dependencies[name])
result.push(this.dependencies[name]);
this.dependencies[name].getDeepDependencies(result);
}
return result;
};
Package.prototype.saveUnit = function () {
this.unitWork.store(this.name, this.serialize(), this);
this.unitWork.unlock(this.name, this);
this.addDependencies();
};
Package.prototype.addDependencies = function () {
var dependencies = this.json.dependencies || {};
var callbacks = Object.keys(dependencies).map(function (name) {
@@ -289,7 +342,7 @@ Package.prototype.clone = function () {
this.once('cache', function () {
this.once('loadJSON', this.copy.bind(this)).checkout();
}.bind(this)).cache();
}
};
Package.prototype.cache = function () {
mkdirp(cache, function (err) {
@@ -378,7 +431,7 @@ Package.prototype.describeTag = function () {
else if (code != 0) return this.emit('error', new Error('Git status: ' + code));
this.emit('describeTag', tag.replace(/\n$/, ''));
}.bind(this));
}
};
Package.prototype.versions = function () {
this.on('fetch', function () {
@@ -420,6 +473,36 @@ Package.prototype.fetchURL = function () {
}
};
Package.prototype.waitUnlock = function (name) {
if (this.name === name) {
this.unitWork.removeListener('unlock', this.waitUnlock);
this.resolve();
}
};
Package.prototype.serialize = function () {
return {
id: this.id,
resourceId: this.resourceId,
path: this.path,
tag: this.tag,
originalTag: this.originalTag,
assetUrl: this.assetUrl,
assetType: this.assetType,
json: this.json,
gitUrl: this.gitUrl,
dependencies: this.dependencies
};
};
Package.prototype.unserialize = function (obj) {
for (var key in obj) {
this[key] = obj[key];
}
this.version = this.tag;
};
Package.prototype.__defineGetter__('localPath', function () {
return path.join(process.cwd(), config.directory, this.name);
});

64
lib/core/unit_work.js Normal file
View File

@@ -0,0 +1,64 @@
// ==========================================
// BOWER: Package Object Definition
// ==========================================
// Copyright 2012 Twitter, Inc
// Licensed under The MIT License
// http://opensource.org/licenses/MIT
// ==========================================
// Events:
// - lock: fired when a lock write over a key is acquired
// - unlock: fired when an unlock write over a key is acquired
// ==========================================
var events = require('events');
var UnitWork = function () {
this.locks = [];
this.data = [];
this.setMaxListeners(100); // Increase the number of listeners because this is a central storage
};
UnitWork.prototype = Object.create(events.EventEmitter.prototype);
UnitWork.prototype.constructor = UnitWork;
UnitWork.prototype.lock = function (key, owner) {
if (this.locks[key]) throw new Error('A write lock for "' + key + '" was already acquired.');
if (!owner) throw new Error('A lock requires an owner.');
this.locks[key] = owner;
return this.emit('lock', key);
};
UnitWork.prototype.unlock = function (key, owner) {
if (!owner) throw new Error('A write lock requires an owner.');
if (this.locks[key]) {
if (this.locks[key] !== owner) throw new Error('Lock owner for "' + key + '" mismatch.');
delete this.locks[key];
this.emit('unlock', key);
}
return this;
};
UnitWork.prototype.isLocked = function (key) {
return !!this.locks[key];
};
UnitWork.prototype.store = function (key, data, owner) {
if (this.locks[key] && owner !== this.locks[key]) throw new Error('A write lock for "' + key + '" is acquired therefore only its owner can write to it.');
this.data[key] = data;
return this;
};
UnitWork.prototype.retrieve = function (key) {
return this.data[key];
};
UnitWork.prototype.keys = function () {
return Object.keys(this.data);
};
module.exports = UnitWork;

View File

@@ -12,11 +12,11 @@ var _ = require('lodash');
module.exports = hogan.Template.prototype.renderWithColors = function (context, partials, indent) {
context = _.extend({
yellow : function (s) { return s.yellow },
green : function (s) { return s.green },
cyan : function (s) { return s.cyan },
grey : function (s) { return s.grey },
red : function (s) { return s.red }
yellow : function (s) { return s.yellow; },
green : function (s) { return s.green; },
cyan : function (s) { return s.cyan; },
grey : function (s) { return s.grey; },
red : function (s) { return s.red; }
}, context);
return this.ri([context], partials || {}, indent);
};

View File

@@ -7,16 +7,15 @@
// ==========================================
var semver = require('semver');
var _ = require('lodash');
var versionRequirements = function (dependencyMap) {
var result = []
var result = {};
for (var name in dependencyMap) {
dependencyMap[name].forEach(function (pkg) {
for (var dep in pkg.json.dependencies) {
result[dep] = result[dep] || [];
result[dep].concat(pkg.json.dependencies[dep]);
result[name] = result[name] || [];
if (pkg.originalTag && result[name].indexOf(pkg.originalTag) === -1) {
result[name].push(pkg.originalTag);
}
});
}
@@ -27,12 +26,15 @@ var versionRequirements = function (dependencyMap) {
var validVersions = function (versions, dependency) {
if (!versions || !versions.length) return true;
// If a non resolved dependency is passed, we simply ignore it
if (!dependency.version) return false;
if (!semver.valid(dependency.version)) {
throw new Error('Invalid semver version "' + dependency.version + '" specified in ' + dependency.name);
throw new Error('Invalid semver version ' + dependency.version + ' specified in ' + dependency.name);
}
return _.find(versions, function (version) {
return !semver.satisfies(dependency.version, version)
return versions.every(function (version) {
return semver.satisfies(dependency.version, version);
});
};

View File

@@ -19,6 +19,6 @@ var read = module.exports = function (path, cb, obj) {
.on('data', obj.emit.bind(obj, 'data'));
}
}
}
};
readJSON(path, cb);
}
};

View File

@@ -40,7 +40,7 @@ function save (eventType, modifier, emitter, manager, paths) {
}).loadJSON.bind(manager));
};
}
function addDependency(pkg) {
var path = (pkg.gitUrl || pkg.assetUrl || pkg.path || '');

View File

@@ -0,0 +1,8 @@
{
"name": "myproject",
"version": "1.0.0",
"dependencies": {
"jquery": "1.6.0",
"jquery-pjax": "1.0.0"
}
}

View File

@@ -0,0 +1,8 @@
{
"name": "myproject",
"version": "1.0.0",
"dependencies": {
"jquery": "1.7.2",
"jquery-pjax": "1.0.0"
}
}

View File

@@ -3,6 +3,6 @@
"version": "1.0.0",
"dependencies": {
"package-bootstrap": "git://github.com/fat/package-bootstrap.git#~2.0.0",
"jquery": "git://github.com/maccman/package-jquery.git#v1.7.2"
"jquery": "~1.8.1"
}
}

View File

@@ -25,5 +25,5 @@ describe('help', function () {
next();
});
});
});

75
test/manager.js Normal file
View File

@@ -0,0 +1,75 @@
var assert = require('assert');
var Manager = require('../lib/core/manager');
var rimraf = require('rimraf');
var config = require('../lib/core/config');
var semver = require('semver');
describe('manager', function () {
beforeEach(function (done) {
var del = 0;
rimraf(config.directory, function (err) {
// Ignore the error if the local directory was not actually deleted
if (++del >= 2) done();
});
rimraf(config.cache, function (err) {
// Ignore the error if the cache directory was not actually deleted
if (++del >= 2) done();
});
});
it('Should resolve JSON dependencies', function (next) {
var manager = new Manager([]);
manager.cwd = __dirname + '/assets/project';
manager.on('resolve', function () {
assert.ok(semver.gte(manager.dependencies['jquery'][0].version, '1.8.1'));
assert.ok(semver.gte(manager.dependencies['package-bootstrap'][0].version, '2.0.0'));
assert.ok(semver.gte(manager.dependencies['jquery-ui'][0].version, '1.8.0'));
next();
});
manager.on('error', function (err) {
throw new Error(err);
});
manager.resolve();
});
it('Should resolve nested JSON dependencies', function (next) {
var manager = new Manager([]);
manager.cwd = __dirname + '/assets/project-nested';
manager.on('resolve', function () {
assert.deepEqual(manager.dependencies['jquery'][0].version, '1.7.2');
assert.deepEqual(manager.dependencies['jquery-pjax'][0].version, '1.0.0');
next();
});
manager.on('error', function (err) {
throw new Error(err);
});
manager.resolve();
});
it('Should detect unresolvable packages in nested JSON dependencies', function (next) {
var manager = new Manager([]);
manager.cwd = __dirname + '/assets/project-nested-conflict';
var detected = false;
manager.on('error', function (err) {
if (/no resolvable.* jquery$/i) detected = true;
});
manager.on('resolve', function () {
if (!detected) throw new Error('A conflict in jquery should have been detected.');
next();
});
manager.resolve();
});
});

View File

@@ -9,6 +9,21 @@ var config = require('../lib/core/config');
var Package = require('../lib/core/package');
describe('package', function () {
beforeEach(function (done) {
var del = 0;
rimraf(config.directory, function (err) {
// Ignore the error if the local directory was not actually deleted
if (++del >= 2) done();
});
rimraf(config.cache, function (err) {
// Ignore the error if the cache directory was not actually deleted
if (++del >= 2) done();
});
});
it('Should resolve git URLs properly', function () {
var pkg = new Package('jquery', 'git://github.com/jquery/jquery.git');
assert.equal(pkg.gitUrl, 'git://github.com/jquery/jquery.git');
@@ -35,7 +50,7 @@ describe('package', function () {
assert.equal(pkg.gitUrl, 'git@github.com:twitter/flight.git');
});
it('Should resolve url when we got redirected', function() {
it('Should resolve url when we got redirected', function (next) {
var redirecting_url = 'http://redirecting-url.com';
var redirecting_to_url = 'http://redirected-to-url.com';
@@ -53,6 +68,11 @@ describe('package', function () {
pkg.on('resolve', function () {
assert(pkg.assetUrl);
assert.equal(pkg.assetUrl, redirecting_to_url + '/jquery.zip');
next();
});
pkg.on('error', function (err) {
throw new Error(err);
});
pkg.download();
@@ -110,6 +130,10 @@ describe('package', function () {
next();
});
pkg.on('error', function (err) {
throw new Error(err);
});
pkg.loadJSON();
});
@@ -122,6 +146,10 @@ describe('package', function () {
next();
});
pkg.on('error', function (err) {
throw new Error(err);
});
pkg.resolve();
});
@@ -142,12 +170,18 @@ describe('package', function () {
pkg.on('resolve', function () {
pkg.install();
});
pkg.on('error', function (err) {
throw new Error(err);
});
pkg.on('install',function () {
assert(fs.existsSync(pkg.localPath));
rimraf(config.directory, function(err){
next();
});
});
pkg.clone();
});
@@ -158,16 +192,20 @@ describe('package', function () {
pkg.on('cache', function() {
cachePath = pkg.path;
});
pkg.on('resolve', function () {
pkg.install();
});
pkg.on('error', function (err) {
throw new Error(err);
});
pkg.on('install',function () {
async.map([pkg.localPath, cachePath], fs.stat, function (err, results) {
if (err) throw new Error(err);
assert.equal(results[0].mode, results[1].mode)
rimraf(config.directory, function(err){
next();
});
assert.equal(results[0].mode, results[1].mode);
next();
});
});