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

@@ -126,15 +126,16 @@ If `config` is not passed, the default one will be used.
`Manager#setProduction(production)`: Manager
Enable/disable production (read of devDependencies).
Enable/disable `production` (read of devDependencies).
`Manager#getResolutions()`: Object
Get the current resolutions object.
`Manager#setResolutions(resolutions)`: Manager
`Manager#setResolutions(resolutions, save)`: Manager
Set the resolutions to be used on conflicts.
Set the `resolutions` to be used on conflicts.
If `save` is true, the `resolutions` object will be updated.
`Manager#configure(targets, resolved, installed)`: Manager
@@ -144,9 +145,7 @@ Configures the manager with `targets` and `installed`:
- `resolved`: object of resolved packages (keys are names and values the reconstructed decomposed endpoints)
- `installed`: object of currently installed packages (keys are names and values the package metas)
`targets` and `resolved` decomposed endpoints may contain a dependants key to specify dependants.
Those will be used when presenting choices to the user on conflicts.
`targets` and `resolved` decomposed endpoints should contain `dependency` and `dependants` keys correctly set.
If the Manager is already working, the promise is immediately rejected.
`Manager#resolve()`: Promise

View File

@@ -47,9 +47,10 @@ install.line = function (argv) {
install.options = function (argv) {
return cli.readOptions({
'help': { type: Boolean, shorthand: 'h' },
'production': { type: Boolean, shorthand: 'p' },
'save': { type: Boolean, shorthand: 'S' },
'save-dev': { type: Boolean, shorthand: 'D' },
'production': { type: Boolean, shorthand: 'p' }
'save-resolutions': { type: Boolean, shorthand: 'r' }
}, argv);
};

View File

@@ -48,7 +48,8 @@ update.line = function (argv) {
update.options = function (argv) {
return cli.readOptions({
'help': { type: Boolean, shorthand: 'h' },
'production': { type: Boolean, shorthand: 'p' }
'production': { type: Boolean, shorthand: 'p' },
'save-resolutions': { type: Boolean, shorthand: 'r' }
}, argv);
};

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

View File

@@ -140,7 +140,7 @@ StandardRenderer.prototype._incompatibleLog = function (log) {
this._write(process.stdout, '\n');
};
StandardRenderer.prototype._resolvedLog = function (log) {
StandardRenderer.prototype._solvedLog = function (log) {
this._incompatibleLog(log);
};
@@ -176,8 +176,13 @@ StandardRenderer.prototype._prefix = function (log) {
var id = log.id;
var idColor = this._colors[log.level] || this._colors['default'];
// If there's not enough space, print only the id
if (this._compact) {
// If there's not enough space for the id, adjust it
// for subsequent logs
if (id.length > this._sizes.id) {
this._sizes.id = id.length += this._sizes.sumup;
}
return mout.string.rpad(id, this._sizes.id)[idColor];
}
@@ -194,6 +199,7 @@ StandardRenderer.prototype._prefix = function (log) {
// Ensure at least one space between the label and the id
if (nrSpaces < 1) {
// Also adjust the label size for subsequent logs
this._sizes.label = label.length + this._sizes.sumup;
nrSpaces = this._sizes.id + this._sizes.label - length;
}

View File

@@ -20,6 +20,11 @@
"shorthand": "-D",
"flag": "--save-dev",
"description": "Save installed packages into the project's bower.json devDependencies"
},
{
"shorthand": "-r",
"flag": "--save-resolutions",
"description": "Save resolutions chosen to resolve conflicts"
}
]
}

View File

@@ -9,6 +9,11 @@
"shorthand": "-p",
"flag": "--production",
"description": "Do not install project devDependencies"
},
{
"shorthand": "-r",
"flag": "--save-resolutions",
"description": "Save resolutions chosen to resolve conflicts"
}
]
}

View File

@@ -1,8 +1,9 @@
{{#yellow}}Please note that,{{/yellow}}
{{#condense}}
{{#each picks}}
{{#white}}{{dependants}}{{/white}} depend on {{#cyan}}{{endpoint.name}}#{{endpoint.target}}{{/cyan}}{{#if pkgMeta._release}} which resolved to {{#white}}{{pkgMeta._release}}{{/white}}{{/if}}
{{#if dependants}}{{#white}}{{dependants}}{{/white}}{{else}}none{{/if}} depend on {{#cyan}}{{endpoint.name}}#{{endpoint.target}}{{/cyan}}{{#if pkgMeta._release}} which resolved to {{#white}}{{pkgMeta._release}}{{/white}}{{/if}}
{{/each}}
{{/condense}}
Resort to using {{#cyan}}{{suitable.endpoint.name}}#{{resolution}}{{/cyan}} which resolved to {{#white}}{{suitable.pkgMeta._release}}{{/white}}
Code incompatibilities may occur.

View File

@@ -4,3 +4,6 @@
{{#magenta}}{{sum @index 1}}){{/magenta}} {{#cyan}}{{endpoint.name}}#{{endpoint.target}}{{/cyan}}{{#if pkgMeta._release}} which resolved to {{#white}}{{pkgMeta._release}}{{/white}}{{/if}}{{#if dependants}} and has {{#white}}{{dependants}}{{/white}} as dependants{{/if}}
{{/each}}
{{/condense}}
{{#unless saveResolutions}}
Use --save-resolutions to persist your choice to the bower.json
{{/unless}}

View File

@@ -11,7 +11,7 @@ Options:
{{#condense}}
{{#each options}}
{{#yellow}}{{#rpad length="20"}}{{#if shorthand}}{{shorthand}}, {{/if}}{{flag}}{{/rpad}}{{/yellow}} {{description}}
{{#yellow}}{{#rpad length="23"}}{{#if shorthand}}{{shorthand}}, {{/if}}{{flag}}{{/rpad}}{{/yellow}} {{description}}
{{/each}}
{{/condense}}

View File

@@ -11,7 +11,7 @@ Commands:
{{#condense}}
{{#each commands}}
{{#rpad length="20"}}{{@key}}{{/rpad}} {{.}}
{{#rpad length="23"}}{{@key}}{{/rpad}} {{.}}
{{/each}}
{{/condense}}
@@ -19,7 +19,7 @@ Options:
{{#condense}}
{{#each options}}
{{#yellow}}{{#rpad length="20"}}{{#if shorthand}}{{shorthand}}, {{/if}}{{flag}}{{/rpad}}{{/yellow}} {{description}}
{{#yellow}}{{#rpad length="23"}}{{#if shorthand}}{{shorthand}}, {{/if}}{{flag}}{{/rpad}}{{/yellow}} {{description}}
{{/each}}
{{/condense}}