From 9f6bf62efcac298cb01ec446d6c2cd28c491d05f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andre=CC=81=20Cruz?= Date: Wed, 22 May 2013 22:58:04 +0100 Subject: [PATCH] Some other tweaks to the API. --- README.md | 21 +++- bin/bower_new | 2 +- lib/core/Manager.js | 176 +++++++++++++++++++--------------- lib/core/PackageRepository.js | 2 +- lib/core/Project.js | 132 +++++++++++++------------ lib/core/ResolveCache.js | 8 ++ lib/core/resolverFactory.js | 2 +- 7 files changed, 190 insertions(+), 153 deletions(-) diff --git a/README.md b/README.md index b026bc7d..270d3955 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/bin/bower_new b/bin/bower_new index df0de948..da6c84b0 100755 --- a/bin/bower_new +++ b/bin/bower_new @@ -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'); diff --git a/lib/core/Manager.js b/lib/core/Manager.js index cd0a6942..5f6e7a22 100644 --- a/lib/core/Manager.js +++ b/lib/core/Manager.js @@ -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) { diff --git a/lib/core/PackageRepository.js b/lib/core/PackageRepository.js index 5abe9c83..294d8038 100644 --- a/lib/core/PackageRepository.js +++ b/lib/core/PackageRepository.js @@ -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)); diff --git a/lib/core/Project.js b/lib/core/Project.js index 69423fc7..fd0d1f9f 100644 --- a/lib/core/Project.js +++ b/lib/core/Project.js @@ -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); }; diff --git a/lib/core/ResolveCache.js b/lib/core/ResolveCache.js index 35056c56..26683b37 100644 --- a/lib/core/ResolveCache.js +++ b/lib/core/ResolveCache.js @@ -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) { diff --git a/lib/core/resolverFactory.js b/lib/core/resolverFactory.js index a676d3da..eb8aaeb5 100644 --- a/lib/core/resolverFactory.js +++ b/lib/core/resolverFactory.js @@ -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;