Some other tweaks to the API.

This commit is contained in:
André Cruz
2013-05-22 22:58:04 +01:00
parent 60ecceafec
commit 9f6bf62efc
7 changed files with 190 additions and 153 deletions

View File

@@ -130,19 +130,30 @@ Note that `force` and `offline` are mutually exclusive.
##### Public methods
`Manager#configure(unresolved, resolved)`: Promise
`Manager#configure(targets, installed)`: Promise
Configures the manager with an array of unresolved `decomposed endpoint`s and
an array of resolved `decomposed endpoint`s (optional).
Configures the manager with `targets` and `installed`:
If the Manager is already resolving, the promise is immediately rejected.
- `targets`: array where keys are names and values the decomposed endpoints
- `installed`: object where keys are names and values the canonical package or the package metas
If the Manager is already working, the promise is immediately rejected.
`Manager#resolve()`: Promise
Starts the resolve promise, returning a promise of an object which keys are package names and
values the associated resolve info (decomposed endpoints plus package meta and other info).
If the Manager is already resolving, the promise is immediately rejected.
If the Manager is already working, the promise is immediately rejected.
`Manager#install()`: Promise
Installs packages that result from the dissection of the resolve process.
The promise is resolved with an object where keys are package names and values the package meta's.
If the Manager is already working, the promise is immediately rejected.
TODO
`Manager#areCompatible(source, subject)`: Boolean

View File

@@ -34,7 +34,7 @@ var test = new Project({
test.install(/*['jquery-ui']*/)
.progress(function (notification) {
var id = notification.from + '#' + notification.endpoint.target;
var id = notification.origin + '#' + notification.endpoint.target;
id = notification.type === 'warn' ? id.yellow : id.cyan;
process.stdout.write('bower ' + id + ' ' + notification.data + '\n');

View File

@@ -17,24 +17,33 @@ var Manager = function (options) {
this._repository = new PackageRepository(options);
};
Manager.prototype.configure = function (unresolved, resolved) {
// -----------------
Manager.prototype.configure = function (targets, installed) {
// If working, error out
if (this._working) {
throw createError('Can\'t configure while working', 'EWORKING');
}
// Store stuff
this._targets = unresolved;
this._targets = {};
this._resolved = {};
mout.object.forOwn(resolved, function (decEndpoint) {
// Only accept resolved endpoints with a name, dir and json properties
if (!decEndpoint.name || !decEndpoint.dir || !decEndpoint.json) {
throw createError('The properties "name", "dir" and "json" must be set when configuring resolved endpoints');
}
// Parse targets
targets.forEach(function (decEndpoint) {
this._targets[decEndpoint.name] = decEndpoint;
}, this);
this._resolved[decEndpoint.name] = [decEndpoint];
decEndpoint.initial = true; // Mark this endpoint
// Parse installed
mout.object.forOwn(installed, function (value, name) {
// TODO: If value is a string, read package meta
// If is not a string, than it's already the package meta
this._resolved[name] = [{
name: name,
source: null,
target: value.version || '*',
pkgMeta: value,
installed: true
}];
}, this);
return this;
@@ -85,36 +94,33 @@ Manager.prototype.install = function () {
.then(function () {
var promises = [];
mout.object.forOwn(that._dissected, function (decEndpoint) {
mout.object.forOwn(that._dissected, function (decEndpoint, name) {
var promise;
var dest;
var release = decEndpoint.json._release;
// Do not copy if it was initially configured as resolved
if (decEndpoint.initial) {
return;
}
var release = decEndpoint.pkgMeta._release;
deferred.notify({
type: 'action',
data: 'Installing' + (release ? ' "' + release + '"' : ''),
from: decEndpoint.name || decEndpoint.resolverName,
origin: name,
endpoint: decEndpoint
});
dest = path.join(destDir, decEndpoint.name);
// Remove existent
// Remove existent and copy canonical package
dest = path.join(destDir, name);
promise = Q.nfcall(rimraf, dest)
// Copy dir
.then(copy.copyDir.bind(copy, decEndpoint.dir, dest));
promises.push(promise);
});
return Q.all(promises)
.then(function () {
return that._dissected;
return Q.all(promises);
})
.then(function () {
// Resolve with an object where keys are names and values
// are the package metas
return mout.object.map(this._dissected, function (decEndpoint) {
return decEndpoint.pkgMeta;
});
})
.then(deferred.resolve, deferred.reject, deferred.notify);
@@ -126,48 +132,49 @@ Manager.prototype.install = function () {
}.bind(this));
};
Manager.prototype.areCompatible = function (source, subject) {
var validSource = semver.valid(source.target) != null;
var validSubject = semver.valid(subject.target) != null;
var validRangeSource = semver.validRange(source.target) != null;
var validRangeSubject = semver.validRange(subject.target) != null;
var highestSubject;
var highestSource;
Manager.prototype.areCompatible = function (first, second) {
var validFirst = semver.valid(first) != null;
var validSecond = semver.valid(second) != null;
var validRangeFirst;
var validRangeSecond;
var highestSecond;
var highestFirst;
// Version -> Version
if (validSource && validSubject) {
return semver.eq(source.target, subject.target);
if (validFirst && validSecond) {
return semver.eq(first, second);
}
// Range -> Version
if (validRangeSource && validSubject) {
return semver.satisfies(subject.target, source.target);
validRangeFirst = semver.validRange(first) != null;
if (validRangeFirst && validSecond) {
return semver.satisfies(second, first);
}
// Version -> Range
if (validSource && validRangeSubject) {
return semver.satisfies(source.target, subject.target);
validRangeSecond = semver.validRange(second) != null;
if (validFirst && validRangeSecond) {
return semver.satisfies(first, second);
}
// Range -> Range
if (validRangeSource && validRangeSubject) {
if (validRangeFirst && validRangeSecond) {
// Special case which both targets are *
if (source.target === '*' && subject.target === '*') {
if (first === '*' && second === '*') {
return true;
}
// Grab the highest version possible for both
highestSubject = this._findHighestVersion(semver.toComparators(subject.target));
highestSource = this._findHighestVersion(semver.toComparators(source.target));
highestSecond = this._findHighestVersion(semver.toComparators(second));
highestFirst = this._findHighestVersion(semver.toComparators(first));
// Check if the highest resolvable version for the
// subject is the same as the source one
return semver.eq(highestSubject, highestSource);
// second is the same as the first one
return semver.eq(highestSecond, highestFirst);
}
// Otherwise check if both targets are the same
return source.target === subject.target;
// As fallback, check if both are the equal
return first === second;
};
// -----------------
@@ -187,10 +194,10 @@ Manager.prototype._fetch = function (decEndpoint) {
// When done, call onFetch
.spread(this._onFetch.bind(this, decEndpoint))
// Listen to progress to proxy them to the resolve deferred
// Note that we mark where the notification is coming from
// Note that we also mark where the notification is coming from
.progress(function (notification) {
notification.endpoint = decEndpoint;
notification.from = decEndpoint.name || decEndpoint.registryName || decEndpoint.resolverName;
notification.origin = name || decEndpoint.registryName || decEndpoint.resolverName;
this._deferred.notify(notification);
}.bind(this));
@@ -198,7 +205,6 @@ Manager.prototype._fetch = function (decEndpoint) {
};
Manager.prototype._onFetch = function (decEndpoint, canonicalPkg, pkgMeta) {
var json;
var name;
var resolved;
var index;
@@ -208,10 +214,10 @@ Manager.prototype._onFetch = function (decEndpoint, canonicalPkg, pkgMeta) {
mout.array.remove(this._fetching[initialName], decEndpoint);
this._nrFetching--;
// Set the name, dir, json property in the decomposed endpoint
decEndpoint.dir = canonicalPkg;
// Store some needed stuff
decEndpoint.name = name = decEndpoint.name || pkgMeta.name;
decEndpoint.json = json = pkgMeta;
decEndpoint.dir = canonicalPkg;
decEndpoint.pkgMeta = pkgMeta;
// Add to the resolved list, marking it as resolved
resolved = this._resolved[name] = this._resolved[name] || [];
@@ -222,7 +228,7 @@ Manager.prototype._onFetch = function (decEndpoint, canonicalPkg, pkgMeta) {
// we need to remove the initially resolved one that match the new name
if (!initialName) {
index = mout.array.findIndex(resolved, function (decEndpoint) {
return decEndpoint.initial;
return decEndpoint.installed;
});
if (index !== -1) {
@@ -231,7 +237,7 @@ Manager.prototype._onFetch = function (decEndpoint, canonicalPkg, pkgMeta) {
}
// Parse dependencies
this._parseDependencies(decEndpoint, json);
this._parseDependencies(decEndpoint, pkgMeta);
// If the resolve process ended, parse the resolved packages
// to find the most suitable version for each package
@@ -240,25 +246,26 @@ Manager.prototype._onFetch = function (decEndpoint, canonicalPkg, pkgMeta) {
}
};
Manager.prototype._parseDependencies = function (decEndpoint, json) {
Manager.prototype._parseDependencies = function (decEndpoint, pkgMeta) {
// Parse package dependencies
mout.object.forOwn(json.dependencies, function (value, key) {
var decEndpoints;
mout.object.forOwn(pkgMeta.dependencies, function (value, key) {
var resolved;
var beingFetched;
var compatible;
var childDecEndpoint = endpointParser.json2decomposed(key, value);
// Check if a compatible one is already resolved
// If there's one, we don't need to resolve it twice
decEndpoints = this._resolved[key];
if (decEndpoints) {
compatible = mout.array.find(decEndpoints, function (resolved) {
return this.areCompatible(resolved, childDecEndpoint);
resolved = this._resolved[key];
if (resolved) {
compatible = mout.array.find(resolved, function (resolved) {
return this.areCompatible(resolved.target, childDecEndpoint.target);
}, this);
// Simply mark it as resolved
if (compatible) {
childDecEndpoint.dir = compatible.dir;
childDecEndpoint.json = compatible.json;
childDecEndpoint.pkgMeta = compatible.pkgMeta;
this._resolved[key].push(childDecEndpoint);
return;
}
@@ -266,17 +273,17 @@ Manager.prototype._parseDependencies = function (decEndpoint, json) {
// Check if a compatible one is being fetched
// If there's one, we reuse it to avoid resolving it twice
decEndpoints = this._fetching[key];
if (decEndpoints) {
compatible = mout.array.find(decEndpoints, function (beingFetched) {
return this.areCompatible(beingFetched, childDecEndpoint);
beingFetched = this._fetching[key];
if (beingFetched) {
compatible = mout.array.find(beingFetched, function (beingFetched) {
return this.areCompatible(beingFetched.target, childDecEndpoint.target);
}, this);
// Wait for it to resolve and then add it to the resolved packages
if (compatible) {
childDecEndpoint = compatible.promise.then(function () {
childDecEndpoint.dir = compatible.dir;
childDecEndpoint.json = compatible.json;
childDecEndpoint.pkgMeta = compatible.pkgMeta;
this._resolved[key].push(childDecEndpoint);
}.bind(this));
@@ -290,30 +297,31 @@ Manager.prototype._parseDependencies = function (decEndpoint, json) {
};
Manager.prototype._dissect = function () {
this._dissected = {};
var pkgMetas;
var dissected = {};
mout.object.forOwn(this._resolved, function (decEndpoints, name) {
var configured = this._targets[name];
var target = this._targets[name];
var nonSemver;
var validSemver;
var suitable;
// If this was initially configured as a target without a valid semver target,
// it means the user wants it regardless of other ones
if (configured && configured.target && !semver.valid(configured.target)) {
this._dissected[name] = this._targets[name];
if (target && target.target && !semver.valid(target.target)) {
dissected[name] = this._targets[name];
// TODO: issue warning
return;
}
// Filter non-semver ones
nonSemver = decEndpoints.filter(function (decEndpoint) {
return !decEndpoint.json.version;
return !decEndpoint.pkgMeta.version;
});
// Filter semver ones
validSemver = decEndpoints.filter(function (decEndpoint) {
return !!decEndpoint.json.version;
return !!decEndpoint.pkgMeta.version;
});
// Sort semver ones
@@ -324,7 +332,10 @@ Manager.prototype._dissect = function () {
if (semver.lt(first, second)) {
return 1;
}
return 0;
// If it gets here, they are equal but priority is given to
// installed ones
return first.installed ? -1 : (second.installed ? 1 : 0);
});
// If there are no semver targets
@@ -336,7 +347,7 @@ Manager.prototype._dissect = function () {
// TODO: handle conflicts if there is no suitable version
suitable = mout.array.find(validSemver, function (subject) {
return validSemver.every(function (decEndpoint) {
return semver.satisfies(subject.json.version, decEndpoint.target);
return semver.satisfies(subject.pkgMeta.version, decEndpoint.target);
});
});
}
@@ -344,13 +355,22 @@ Manager.prototype._dissect = function () {
// TODO: handle case which there is a suitable version but there are no-semver ones too
if (suitable) {
this._dissected[name] = suitable;
dissected[name] = suitable;
} else {
throw new Error('No suitable version for "' + name + '"');
}
}, this);
this._deferred.resolve(this._dissected);
// Filter only packages that need to be installed
this._dissected = mout.object.filter(dissected, function (decEndpoint) {
return !decEndpoint.installed;
});
// Resolve just with the package metas of the dissected object
pkgMetas = mout.object.map(this._dissected, function (decEndpoint) {
return decEndpoint.pkgMeta;
});
this._deferred.resolve(pkgMetas);
};
Manager.prototype._findHighestVersion = function (comparators) {

View File

@@ -15,7 +15,7 @@ var PackageRepository = function (options) {
// Instantiate the registry and store it in the options object
// because it will be passed to the resolver factory
this._options.registry = new RegistryClient(mout.object.fillIn({
this._options.registryClient = new RegistryClient(mout.object.fillIn({
cache: this._config.roaming.registry
}, this._config));

View File

@@ -17,68 +17,75 @@ var Project = function (options) {
this._manager = new Manager(options);
};
Project.prototype.install = function (targets) {
// -----------------
Project.prototype.install = function (endpoints) {
var repairResult;
var that = this;
var repairDissected;
// If already working, error out
if (this._working) {
return Q.reject(createError('Already working', 'EWORKING'));
}
// If no targets were specified, simply repair the project if necessary
// If no endpoints were specified, simply repair the project
// Note that we also repair incompatible packages
if (!targets) {
if (!endpoints) {
return this._repair(true)
.fin(function () {
that._working = false;
}.bind(this));
});
}
// Start by repairing the project, installing any missing packages
// Start by repairing the project, installing only missing packages
return this._repair()
// Analyse the project
.then(function (dissected) {
repairDissected = dissected;
.then(function (result) {
repairResult = result;
return that._analyse();
})
// Decide which dependencies should be fetched and the ones
// that are already resolved
.spread(function (json, tree, flattened) {
var unresolved = {};
var resolved = {};
var targetNames = {};
var targets = [];
var installed = {};
// Mark targets as unresolved
targets.forEach(function (target) {
unresolved[target.name] = endpointParser.decompose(target);
// Mark targets
endpoints.forEach(function (target) {
var decEndpoint = endpointParser.decompose(target);
targetNames[decEndpoint.name] = true;
targets.push(decEndpoint);
});
// Mark every package from the tree as resolved
// Mark every package from the tree as installed
// if they are not a target or a non-shared descendant of a target
// TODO: We should do traverse the tree (vertically) and
// TODO: We should traverse the tree (deep first) and
// add each leaf to the resolved
// If a leaf is a target, we abort traversal of it
resolved = mout.object.filter(flattened, function (decEndpoint, name) {
return !unresolved[name];
mout.object.forOwn(flattened, function (decEndpoint, name) {
if (targetNames[name]) {
return;
}
installed[name] = decEndpoint.pkgMeta;
});
// Configure the manager with the unresolved and resolved endpoints
// And kick in the resolve process
// Configure the manager and kick in the resolve process
return that._manager
.configure(unresolved, resolved)
.configure(targets, installed)
.resolve()
// Install resolved ones
.then(function () {
return that._manager.install();
})
// Resolve with the repair and install dissection
.then(function (dissected) {
return mout.object.fillIn(dissected, repairDissected);
// Resolve the promise with the repair and install results,
// by merging them together
.then(function (result) {
return mout.object.fillIn(result, repairResult);
});
})
.fin(function () {
that._working = false;
}.bind(this));
});
};
Project.prototype.update = function (names) {
@@ -107,23 +114,22 @@ Project.prototype._analyse = function () {
])
.spread(function (json, installed) {
var root;
var flattened = installed;
root = {
name: json.name,
source: this._config.cwd,
target: json.version || '*',
json: json,
dir: this._config.cwd
pkgMeta: json
};
// Restore the original dependencies cross-references,
// that is, the parent-child relationships
this._restoreNode(root, installed);
this._restoreNode(root, flattened);
// Do the same for the dev dependencies
if (!this._options.production) {
this._restoreNode(root, installed, 'devDependencies');
this._restoreNode(root, flattened, 'devDependencies');
}
return [json, root, installed];
return [json, root, flattened];
}.bind(this));
};
@@ -132,21 +138,21 @@ Project.prototype._repair = function (incompatible) {
return this._analyse()
.spread(function (json, tree, flattened) {
var unresolved = {};
var resolved = {};
var targets = [];
var installed = {};
var isBroken = false;
// Figure out which are the missing/incompatible ones
// by parsing the flattened tree
mout.object.forOwn(flattened, function (decEndpoint, name) {
if (decEndpoint.missing) {
unresolved[name] = decEndpoint;
targets.push(decEndpoint);
isBroken = true;
} else if (incompatible && decEndpoint.incompatible) {
unresolved[name] = decEndpoint;
targets.push(decEndpoint);
isBroken = true;
} else {
resolved[name] = decEndpoint;
installed[name] = decEndpoint.pkgMeta;
}
});
@@ -155,10 +161,9 @@ Project.prototype._repair = function (incompatible) {
return {};
}
// Configure the manager with the unresolved and resolved endpoints
// And kick in the resolve process
// Configure the manager and kick in the resolve process
return that._manager
.configure(unresolved, resolved)
.configure(targets, installed)
.resolve()
// Install after resolve
.then(function () {
@@ -213,6 +218,7 @@ Project.prototype._readInstalled = function () {
})
.then(function (filenames) {
var promises = [];
var decEndpoints = {};
// Foreach bower.json found
filenames.forEach(function (filename) {
@@ -222,16 +228,11 @@ Project.prototype._readInstalled = function () {
// Read package metadata
promise = Q.nfcall(fs.readFile, path.join(componentsDir, filename))
.then(function (contents) {
var json = JSON.parse(contents.toString());
var dir = path.join(componentsDir, name);
var pkgMeta = JSON.parse(contents.toString());
// Set decomposed endpoint manually
return {
decEndpoints[name] = {
name: name,
source: dir,
target: json.version || '*',
json: json,
dir: dir
pkgMeta: pkgMeta
};
});
@@ -239,21 +240,15 @@ Project.prototype._readInstalled = function () {
});
// Wait until all files have been read
// to form the final object of decomposed endpoints
// and resolve with the decomposed endpoints
return Q.all(promises)
.then(function (locals) {
var decEndpoints = {};
locals.forEach(function (decEndpoint) {
decEndpoints[decEndpoint.name] = decEndpoint;
});
.then(function () {
return decEndpoints;
});
});
};
Project.prototype._restoreNode = function (node, locals, jsonKey) {
Project.prototype._restoreNode = function (node, flattened, jsonKey) {
// Do not restore if already processed or if the node is
// missing or incompatible
if (node.dependencies || node.missing || node.incompatible) {
@@ -261,20 +256,23 @@ Project.prototype._restoreNode = function (node, locals, jsonKey) {
}
node.dependencies = {};
node.dependants = {};
mout.object.forOwn(node.json[jsonKey || 'dependencies'], function (value, key) {
var local = locals[key];
mout.object.forOwn(node.pkgMeta[jsonKey || 'dependencies'], function (value, key) {
var local = flattened[key];
var json = endpointParser.json2decomposed(key, value);
// Check if the dependency is installed
// Check if the dependency is not installed
if (!local) {
local = endpointParser.json2decomposed(key, value);
local = json;
local.missing = true;
locals[key] = local;
// If so, also check if it's compatible
} else if (!this._manager.areCompatible(local, json)) {
flattened[key] = local;
// Even if it is installed, check if it's compatible
} else if (!local.incompatible && !this._manager.areCompatible(local.pkgMeta.version || '*', json.target)) {
json.pkgMeta = local.pkgMeta;
local = json;
local.incompatible = true;
locals[key] = json;
flattened[key] = local;
}
// Cross reference
@@ -283,7 +281,7 @@ Project.prototype._restoreNode = function (node, locals, jsonKey) {
local.dependants[node.name] = node;
// Call restore for this dependency
this._restoreNode(local, locals);
this._restoreNode(local, flattened);
}, this);
};

View File

@@ -19,6 +19,8 @@ var ResolveCache = function (dir) {
mkdirp.sync(dir);
};
// -----------------
ResolveCache.prototype.retrieve = function (source, target) {
var sourceId = this._getSourceId(source);
var dir = path.join(this._dir, sourceId);
@@ -158,6 +160,12 @@ ResolveCache.prototype._getVersions = function (source) {
dir = path.join(this._dir, sourceId);
return Q.nfcall(fs.readdir, dir)
.then(function (versions) {
// If there are no versions there, do not cache in memory
if (!versions.length) {
return versions;
}
// Sort and cache in memory
this._sortVersions(versions);
return this._versions[sourceId] = versions;
}.bind(this), function (err) {

View File

@@ -87,7 +87,7 @@ function createResolver(decEndpoint, options) {
})
// As last resort, we try the registry
.fail(function (err) {
var registry = options.registry;
var registry = options.registryClient;
if (!registry) {
throw err;