Read, use and update persistent resolutions.

Add --save-resolutions to deal with this.
Also:
  - Adjust logging when using compact
  - Minor code adjustments.
This commit is contained in:
André Cruz
2013-06-07 13:05:07 +01:00
parent 79522194f1
commit e704733743
15 changed files with 146 additions and 91 deletions

View File

@@ -30,8 +30,9 @@ Manager.prototype.getResolutions = function () {
return this._resolutions;
};
Manager.prototype.setResolutions = function (resolutions) {
Manager.prototype.setResolutions = function (resolutions, save) {
this._resolutions = resolutions || {};
this._saveResolutions = !!save;
return this;
};
@@ -74,6 +75,7 @@ Manager.prototype.resolve = function () {
this._fetching = {};
this._nrFetching = 0;
this._failed = {};
this._conflicted = {};
this._hasFailed = false;
this._deferred = Q.defer();
@@ -111,11 +113,7 @@ Manager.prototype.install = function () {
var promise;
var dest;
var release = decEndpoint.pkgMeta._release;
var data = {
endpoint: mout.object.pick(decEndpoint, ['name', 'source', 'target']),
canonicalPkg: decEndpoint.canonicalPkg,
pkgMeta: decEndpoint.pkgMeta
};
var data = that._toData(decEndpoint);
that._logger.action('install', name + (release ? '#' + release : ''), data);
@@ -134,11 +132,11 @@ Manager.prototype.install = function () {
return Q.all(promises);
})
.then(function () {
// Resolve with an object where keys are names and values
// are the package metas
return mout.object.map(that._dissected, function (decEndpoint) {
return decEndpoint.pkgMeta;
});
var data = this._toData(decEndpoint);
data.dependencies = mout.object.map(decEndpoint.dependencies, this._toData, this);
return data;
}, that);
})
.fin(function () {
this._working = false;
@@ -340,6 +338,8 @@ Manager.prototype._failFast = function () {
};
Manager.prototype._parseDependencies = function (decEndpoint, pkgMeta, jsonKey) {
decEndpoint.dependencies = decEndpoint.dependencies || {};
// Parse package dependencies
mout.object.forOwn(pkgMeta[jsonKey], function (value, key) {
var resolved;
@@ -347,6 +347,7 @@ Manager.prototype._parseDependencies = function (decEndpoint, pkgMeta, jsonKey)
var compatible;
var childDecEndpoint = endpointParser.json2decomposed(key, value);
decEndpoint.dependencies[key] = childDecEndpoint;
childDecEndpoint.dependants = childDecEndpoint.dependants || [];
childDecEndpoint.dependants.push(decEndpoint);
@@ -451,22 +452,40 @@ Manager.prototype._dissect = function () {
});
}, this);
// TODO: Remove resolutions that are not needed anymore (emitting a warn)
// TODO: Change the final promise result to an array of decEndpoints
// with dependencies included to be able to do a npm like tree
promise
.then(function () {
// Look for extraneous resolutions
mout.object.forOwn(this._resolutions, function (resolution, name) {
if (this._conflicted[name]) {
return;
}
if (this._saveResolutions) {
this._logger.info('resolution', 'Removed unnecessary ' + name + '#' + resolution + ' resolution', {
package: name,
resolution: resolution,
action: 'delete'
});
delete this._resolutions[name];
} else {
this._logger.warn('resolution', 'Unnecessary ' + name + '#' + resolution + ' resolution', {
package: name,
resolution: resolution
});
}
}, this);
// Filter only packages that need to be installed
this._dissected = mout.object.filter(suitables, function (decEndpoint, name) {
var installedMeta = this._installed[name];
return !installedMeta || installedMeta._release !== decEndpoint.pkgMeta._release;
}, this);
// Resolve with the package metas of the dissected object
return mout.object.map(this._dissected, function (decEndpoint) {
return decEndpoint.pkgMeta;
});
var data = this._toData(decEndpoint);
data.dependencies = mout.object.map(decEndpoint.dependencies, this._toData, this);
return data;
}, this);
}.bind(this))
.then(this._deferred.resolve, this._deferred.reject);
};
@@ -474,6 +493,7 @@ Manager.prototype._dissect = function () {
Manager.prototype._electSuitable = function (name, semvers, nonSemvers) {
var suitable;
var resolution;
var unresolvable;
var dataPicks;
var choices;
var picks = [];
@@ -507,6 +527,9 @@ Manager.prototype._electSuitable = function (name, semvers, nonSemvers) {
picks.push.apply(picks, semvers);
}
// At this point, there's a conflict
this._conflicted[name] = true;
// Sort picks by version/release
picks.sort(function (pick1, pick2) {
var version1 = pick1.pkgMeta.version;
@@ -543,23 +566,20 @@ Manager.prototype._electSuitable = function (name, semvers, nonSemvers) {
// Prepare data to be sent bellow
dataPicks = picks.map(function (pick) {
return {
endpoint: mout.object.pick(pick, ['name', 'source', 'target']),
pkgMeta: pick.pkgMeta,
canonicalPkg: pick.canonicalPkg,
dependants: pick.dependants.map(function (dependant) {
return {
endpoint: mout.object.pick(dependant, ['name', 'source', 'target']),
pkgMeta: dependant.pkgMeta,
canonicalPkg: dependant.canonicalPkg
};
})
};
});
var dataPick = this._toData(pick);
dataPick.dependants = pick.dependants.map(this._toData.bind(this), this);
return dataPick;
}, this);
// Check if there's a resolution that resolves the conflict
// Note that if a target is marked as unresolvable, the resolution has
// no effect
resolution = this._resolutions[name];
if (resolution) {
unresolvable = mout.object.find(this._targets, function (target) {
return target.name === name && target.unresolvable;
});
if (resolution && !unresolvable) {
if (semver.valid(resolution) != null || semver.validRange(resolution) != null) {
suitable = mout.array.findIndex(picks, function (pick) {
return semver.satisfies(pick.pkgMeta.version, resolution);
@@ -570,15 +590,21 @@ Manager.prototype._electSuitable = function (name, semvers, nonSemvers) {
});
}
if (suitable) {
this._logger.conflict('resolved', 'Unable to find suitable version for ' + name, {
if (!suitable) {
this._logger.conflict('warn', 'Unsuitable resolution for ' + name + ': ' + resolution, {
name: name,
picks: dataPicks,
resolution: resolution,
suitable: dataPicks[suitable]
resolution: resolution
});
return Q.resolve(picks[suitable]);
}
this._logger.conflict('solved', 'Unable to find suitable version for ' + name, {
name: name,
picks: dataPicks,
resolution: resolution,
suitable: dataPicks[suitable]
});
return Q.resolve(picks[suitable]);
}
// If interactive is disabled, error out
@@ -592,25 +618,44 @@ Manager.prototype._electSuitable = function (name, semvers, nonSemvers) {
// At this point the user needs to make a decision
this._logger.conflict('incompatible', 'Unable to find suitable version for ' + name, {
name: name,
picks: dataPicks
picks: dataPicks,
saveResolutions: this._saveResolutions
});
choices = picks.map(function (pick, index) { return index + 1; });
return Q.nfcall(promptly.choose, 'Choice:', choices)
.then(function (choice) {
var pick = picks[choice - 1];
var resolution;
// Store choice into resolutions
if (pick.target === '*') {
this._resolutions[name] = pick.pkgMeta._release || '*';
resolution = pick.pkgMeta._release || '*';
} else {
this._resolutions[name] = pick.target;
resolution = pick.target;
}
if (this._saveResolutions) {
this._logger.info('resolution', 'Saved ' + name + '#' + resolution + ' as the resolution', {
package: name,
resolution: resolution,
action: this._resolutions[name] ? 'edit' : 'add'
});
this._resolutions[name] = resolution;
}
return pick;
}.bind(this));
};
Manager.prototype._toData = function (decEndpoint) {
return {
endpoint: mout.object.pick(decEndpoint, ['name', 'source', 'target']),
canonicalPkg: decEndpoint.canonicalPkg,
pkgMeta: decEndpoint.pkgMeta
};
};
Manager.prototype._getCap = function (comparators, side) {
var matches;
var candidate;

View File

@@ -37,11 +37,15 @@ Project.prototype.install = function (endpoints, options) {
options = options || {};
this._production = !!options.production;
if (options.saveResolutions == null) {
this._saveResolutions = !!(options.save || options.saveDev);
} else {
this._saveResolutions = !!options.saveResolutions;
}
// Analyse the project
return this.analyse()
.spread(function (json, tree, flattened) {
var promise;
// Walk down the tree adding missing as targets
that._walkTree(tree, function (node, name) {
if (node.missing) {
@@ -55,7 +59,9 @@ Project.prototype.install = function (endpoints, options) {
if (endpoints) {
endpoints.forEach(function (endpoint) {
var decEndpoint = endpointParser.decompose(endpoint);
decEndpoint.specified = true;
// Mark as unresolvable so that a conflict for this target always require
// a choice
decEndpoint.unresolvable = true;
targets.push(decEndpoint);
});
}
@@ -70,25 +76,7 @@ Project.prototype.install = function (endpoints, options) {
}
// Bootstrap the process
promise = that._bootstrap(targets, resolved, flattened);
// If there are targets configured, listen to when they
// resolve in order to remove any associated resolution
// This can only be done at this step because endpoint names
// are not fully known before
if (endpoints) {
promise = promise.progress(function (decEndpoint) {
var resolutions;
if (decEndpoint.specified) {
resolutions = that._manager.getResolutions();
delete resolutions[decEndpoint.name];
delete decEndpoint.specified;
}
});
}
return promise;
return that._bootstrap(targets, resolved, flattened);
})
.then(function (installed) {
// Handle save and saveDev options
@@ -136,6 +124,7 @@ Project.prototype.update = function (names, options) {
options = options || {};
this._production = !!options.production;
this._saveResolutions = !!options.saveResolutions;
// Analyse the project
return this.analyse()
@@ -360,22 +349,22 @@ Project.prototype._bootstrap = function (targets, resolved, flattened) {
// Configure the manager and kick in the resolve process
return this._manager
.setProduction(this._production)
.setResolutions(this._json.resolutions)
.setResolutions(this._json.resolutions, this._saveResolutions)
.configure(targets, resolved, installed)
.resolve()
// Save resolutions into the json
.then(function () {
var resolutions = this._manager.getResolutions();
// Update resolutions
if (mout.object.size(resolutions)) {
this._json.resolutions = resolutions;
} else {
// If empty, delete key
if (!mout.object.size(resolutions)) {
delete this._json.resolutions;
} else {
this._json.resolutions = resolutions;
}
// Install resolved ones
return this._manager.install();
}.bind(this));
}.bind(this))
// Install resolved ones
.then(this._manager.install.bind(this._manager));
};
Project.prototype._readJson = function () {

View File

@@ -26,12 +26,12 @@ mout.object.mixIn(FsResolver, Resolver);
// -----------------
// TODO: should we store latest mtimes in the resolution and compare?
// this would be beneficial when copying big files/folders
// TODO: Should we store latest mtimes in the resolution and compare?
// This would be beneficial when copying big files/folders
// TODO: there's room for improvement by using streams if the source
// TODO: There's room for improvement by using streams if the source
// is an archive file, by piping read stream to the zip extractor
// this will likely increase the complexity of code but might worth it
// This will likely increase the complexity of code but might worth it
FsResolver.prototype._resolve = function () {
return this._readJson(this._source)
.then(this._copy.bind(this))

View File

@@ -58,7 +58,7 @@ GitRemoteResolver.prototype._checkout = function () {
GitRemoteResolver.fetchRefs = function (source) {
var cache;
// TODO: normalize source because of the various available protocols?
// TODO: Normalize source because of the various available protocols?
this._refs = this._refs || {};
cache = this._refs[source];

View File

@@ -82,9 +82,9 @@ UrlResolver.prototype._hasNew = function (canonicalPkg, pkgMeta) {
});
};
// TODO: there's room for improvement by using streams if the url
// TODO: There's room for improvement by using streams if the url
// is an archive file, by piping read stream to the zip extractor
// this will likely increase the complexity of code but might worth it
// This will likely increase the complexity of code but might worth it
UrlResolver.prototype._resolve = function () {
// Download