Add conflict resolve, presenting choices to the user.

This commit also contains:
- Simplified Manager and Project
- Add warning when an extraneous package is found.
- Fix a lot of bugs in the overall resolve process
- Made templates rendering synchronous
-
This commit is contained in:
André Cruz
2013-06-05 00:50:23 +01:00
parent 4593b652c4
commit 68454492e9
13 changed files with 406 additions and 321 deletions

View File

@@ -49,8 +49,7 @@ install.options = function (argv) {
'help': { type: Boolean, shorthand: 'h' },
'save': { type: Boolean, shorthand: 'S' },
'save-dev': { type: Boolean, shorthand: 'D' },
'production': { type: Boolean, shorthand: 'p' },
'force-latest': { type: Boolean, shorthand: 'F'}
'production': { type: Boolean, shorthand: 'p' }
}, argv);
};

View File

@@ -4,6 +4,7 @@ var semver = require('semver');
var path = require('path');
var mkdirp = require('mkdirp');
var rimraf = require('rimraf');
var promptly = require('promptly');
var PackageRepository = require('./PackageRepository');
var copy = require('../util/copy');
var createError = require('../util/createError');
@@ -17,32 +18,31 @@ function Manager(config, logger) {
// -----------------
Manager.prototype.setProduction = function (production) {
this._production = production;
return this;
};
Manager.prototype.configure = function (targets, resolved, installed) {
// If working, error out
if (this._working) {
throw createError('Can\'t configure while working', 'EWORKING');
}
this._targets = {};
this._targets = targets;
this._resolved = {};
this._installed = {};
// Parse targets
targets.forEach(function (decEndpoint) {
this._targets[decEndpoint.name] = decEndpoint;
decEndpoint.dependants = mout.object.values(decEndpoint.dependants);
}, this);
// Parse resolved
mout.object.forOwn(resolved, function (value, name) {
this._resolved[name] = [{
name: name,
source: value._source,
target: value.version || '*',
pkgMeta: value,
initial: true
}];
this._installed[name] = value;
mout.object.forOwn(resolved, function (decEndpoint, name) {
decEndpoint.dependants = mout.object.values(decEndpoint.dependants);
this._resolved[name] = [decEndpoint];
this._installed[name] = decEndpoint.pkgMeta;
}, this);
// Parse installed
@@ -65,12 +65,12 @@ Manager.prototype.resolve = function () {
this._deferred = Q.defer();
// If there's nothing to resolve, simply dissect
if (mout.lang.isEmpty(this._targets)) {
if (!this._targets.length) {
process.nextTick(this._dissect.bind(this));
// Otherwise, fetch each target from the repository
// and let the process roll out
} else {
mout.object.forOwn(this._targets, this._fetch.bind(this));
this._targets.forEach(this._fetch.bind(this));
}
// Unset working flag when done
@@ -138,8 +138,8 @@ Manager.prototype.areCompatible = function (first, second) {
var validSecond = semver.valid(second) != null;
var validRangeFirst;
var validRangeSecond;
var highestSecond;
var highestFirst;
var rangeSecond;
var rangeFirst;
// Version -> Version
if (validFirst && validSecond) {
@@ -165,13 +165,23 @@ Manager.prototype.areCompatible = function (first, second) {
return true;
}
// Grab the highest version possible for both
highestSecond = this._findHighestVersion(semver.toComparators(second));
highestFirst = this._findHighestVersion(semver.toComparators(first));
rangeFirst = {};
rangeSecond = {};
// Check if the highest resolvable version for the
// second is the same as the first one
return semver.eq(highestSecond, highestFirst);
// Grab the highest possible for both
rangeFirst.max = this._getCap(semver.toComparators(first), 'highest');
rangeSecond.max = this._getCap(semver.toComparators(second), 'highest');
// Grab the lowest possible for both
rangeFirst.min = this._getCap(semver.toComparators(first), 'lowest');
rangeSecond.min = this._getCap(semver.toComparators(second), 'lowest');
// Check if the highest/lowest resolvable version for the second is the
// same as the first one
return semver.eq(rangeFirst.max.version, rangeSecond.max.version) &&
rangeFirst.max.comparator === rangeSecond.max.comparator &&
semver.eq(rangeFirst.min.version, rangeSecond.min.version) &&
rangeFirst.min.comparator === rangeSecond.min.comparator;
}
// As fallback, check if both are the equal
@@ -194,8 +204,9 @@ Manager.prototype._fetch = function (decEndpoint) {
this._fetching[name].push(decEndpoint);
this._nrFetching++;
// We create a new logger that pipes everything to ours
// and add the endpoint for each log
// Create a new logger that pipes everything to ours that will be
// used to fetch
// The endpoint is added for each log made
logger = this._logger.geminate().intercept(function (log) {
log.data = log.data || [];
log.data.endpoint = mout.object.pick(decEndpoint, ['name', 'source', 'target']);
@@ -246,7 +257,11 @@ Manager.prototype._onFetchSuccess = function (decEndpoint, canonicalPkg, pkgMeta
}
// Parse dependencies
this._parseDependencies(decEndpoint, pkgMeta);
this._parseDependencies(decEndpoint, pkgMeta, 'dependencies');
// Do the same for the dev dependencies
if (!this._production) {
this._parseDependencies(decEndpoint, pkgMeta, 'devDependencies');
}
// If the resolve process ended, parse the resolved packages
// to find the most suitable version for each package
@@ -281,7 +296,7 @@ Manager.prototype._onFetchError = function (decEndpoint, err) {
};
Manager.prototype._failFast = function () {
if (this._failFastTimeout) {
if (this._hasFailed) {
return;
}
@@ -295,27 +310,34 @@ Manager.prototype._failFast = function () {
}.bind(this), 20000);
};
Manager.prototype._parseDependencies = function (decEndpoint, pkgMeta) {
Manager.prototype._parseDependencies = function (decEndpoint, pkgMeta, jsonKey) {
// Parse package dependencies
mout.object.forOwn(pkgMeta.dependencies, function (value, key) {
mout.object.forOwn(pkgMeta[jsonKey], function (value, key) {
var resolved;
var beingFetched;
var compatible;
var childDecEndpoint = endpointParser.json2decomposed(key, value);
this._addDependant(childDecEndpoint, decEndpoint);
// Check if a compatible one is already resolved
// If there's one, we don't need to resolve it twice
resolved = this._resolved[key];
if (resolved) {
// Find compatible
compatible = mout.array.find(resolved, function (resolved) {
return this.areCompatible(resolved.target, childDecEndpoint.target);
}, this);
// Simply mark it as resolved
if (compatible) {
childDecEndpoint.canonicalPkg = compatible.canonicalPkg;
childDecEndpoint.pkgMeta = compatible.pkgMeta;
this._resolved[key].push(childDecEndpoint);
// If the compatible's target is equal, do not add to the resolved
if (compatible.target === childDecEndpoint.target) {
this._addDependant(compatible, decEndpoint);
} else {
childDecEndpoint.canonicalPkg = compatible.canonicalPkg;
childDecEndpoint.pkgMeta = compatible.pkgMeta;
this._resolved[key].push(childDecEndpoint);
}
return;
}
}
@@ -324,18 +346,23 @@ Manager.prototype._parseDependencies = function (decEndpoint, pkgMeta) {
// If there's one, we reuse it to avoid resolving it twice
beingFetched = this._fetching[key];
if (beingFetched) {
// Find compatible
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.canonicalPkg = compatible.canonicalPkg;
childDecEndpoint.pkgMeta = compatible.pkgMeta;
this._resolved[key].push(childDecEndpoint);
}.bind(this));
// If the compatible's target is equal, do not add to the resolved
if (compatible.target === childDecEndpoint.target) {
this._addDependant(compatible, decEndpoint);
} else {
compatible.promise.then(function () {
childDecEndpoint.canonicalPkg = compatible.canonicalPkg;
childDecEndpoint.pkgMeta = compatible.pkgMeta;
this._resolved[key].push(childDecEndpoint);
}.bind(this));
}
return;
}
}
@@ -345,9 +372,15 @@ Manager.prototype._parseDependencies = function (decEndpoint, pkgMeta) {
}, this);
};
Manager.prototype._addDependant = function (decEndpoint, parentDecEndpoint) {
decEndpoint.dependants = decEndpoint.dependants || [];
decEndpoint.dependants.push(parentDecEndpoint);
};
Manager.prototype._dissect = function () {
var err;
var suitables;
var promises = [];
var suitables = {};
// If something failed, reject the whole resolve promise
// with the first error
@@ -359,107 +392,162 @@ Manager.prototype._dissect = function () {
return;
}
suitables = {};
mout.object.forOwn(this._resolved, function (decEndpoints, name) {
var nonSemver;
var validSemver;
var suitable;
var target = mout.object.get(this._targets, name + '.target');
// If this was initially configured as a target without a valid semver target,
// it means the user wants it regardless of other ones
if (target && semver.valid(target) == null && semver.validRange(target) == null) {
suitables[name] = this._targets[name];
// TODO: issue warning
return;
}
// Filter non-semver ones
nonSemver = decEndpoints.filter(function (decEndpoint) {
return !decEndpoint.pkgMeta.version;
});
var promise;
var semvers;
var nonSemvers;
// Filter semver ones
validSemver = decEndpoints.filter(function (decEndpoint) {
semvers = decEndpoints.filter(function (decEndpoint) {
return !!decEndpoint.pkgMeta.version;
});
// Sort semver ones
validSemver.sort(function (first, second) {
semvers.sort(function (first, second) {
if (semver.gt(first, second)) {
return -1;
}
if (semver.lt(first, second)) {
return 1;
}
// If it gets here, they are equal but priority is given to
// installed ones
return first.initial ? -1 : (second.initial ? 1 : 0);
return 0;
});
// If there are no semver targets
if (!validSemver.length) {
// TODO: if various non-semver were found, resolve conflicts
suitable = nonSemver[0];
// Otherwise, find most suitable semver
// Filter non-semver ones
nonSemvers = decEndpoints.filter(function (decEndpoint) {
return !decEndpoint.pkgMeta.version;
});
promise = this._electSuitable(name, semvers, nonSemvers)
.then(function (suitable) {
suitables[name] = suitable;
});
promises.push(promise);
}, this);
return Q.all(promises)
.then(function () {
// 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;
});
}.bind(this))
.then(this._deferred.resolve, this._deferred.reject);
};
Manager.prototype._electSuitable = function (name, semvers, nonSemvers) {
var picks = [];
var dataPicks;
var choices;
var suitable;
// If there are no semver targets, there's a conflict if several exist
if (!semvers.length) {
if (nonSemvers.length > 1) {
picks.push.apply(nonSemvers);
} else {
// 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.pkgMeta.version, decEndpoint.target);
});
suitable = nonSemvers[0];
}
// Otherwise, find most suitable semver
} else {
suitable = mout.array.find(semvers, function (subject) {
return semvers.every(function (decEndpoint) {
return semver.satisfies(subject.pkgMeta.version, decEndpoint.target);
});
});
// Note that the user needs to pick one if there's no
// suitable version or if some one them were non-semver
if (!suitable || nonSemvers.length) {
picks.push.apply(picks, semvers);
}
picks.push.apply(picks, nonSemvers);
}
// If there are picks, the user needs to choose between them
if (picks.length) {
// If interactive is disabled, error out
if (!this._config.interactive) {
throw createError('Unable to find suitable version for ' + name, 'ECONFLICT', {
picks: picks
});
}
// TODO: handle case which there is a suitable version but there are no-semver ones too
if (suitable) {
suitables[name] = suitable;
} else {
throw new Error('No suitable version for "' + name + '"');
}
}, this);
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
};
})
};
});
// 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);
this._logger.conflict('incompatible', 'Unable to find suitable version for ' + name, {
picks: dataPicks,
name: name
});
// Resolve just with the package metas of the dissected object
this._deferred.resolve(mout.object.map(this._dissected, function (decEndpoint) {
return decEndpoint.pkgMeta;
}));
// Question the user
choices = picks.map(function (pick, index) {
return index + 1;
});
return Q.nfcall(promptly.choose, 'Choice:', choices)
.then(function (choice) {
return picks[choice - 1];
});
}
return Q.resolve(suitable);
};
Manager.prototype._findHighestVersion = function (comparators) {
var highest;
Manager.prototype._getCap = function (comparators, side) {
var matches;
var version;
var candidate;
var cap = {};
var compare = side === 'lowest' ? semver.lt : semver.gt;
comparators.forEach(function (comparator) {
// Get version of this comparator
// If it's an array, call recursively
if (Array.isArray(comparator)) {
version = this._findHighestVersion(comparator);
candidate = this._getCap(comparator, side);
// Compare with the current highest version
if (!cap.version || compare(candidate.version, cap.version)) {
cap = candidate;
}
// Otherwise extract the version from the comparator
// using a simple regexp
} else {
matches = comparator.match(/\d+\.\d+\.\d+.*$/);
matches = comparator.match(/(.*?)(\d+\.\d+\.\d+.*)$/);
if (!matches) {
return;
}
version = matches[0];
}
// Compare with the current highest version
if (!highest || semver.gt(version, highest)) {
highest = version;
// Compare with the current highest version
if (!cap.version || compare(matches[2], cap.version)) {
cap.version = matches[2];
cap.comparator = matches[1];
}
}
}, this);
return highest;
return cap;
};
module.exports = Manager;

View File

@@ -26,7 +26,7 @@ function Project(config, logger) {
Project.prototype.install = function (endpoints, options) {
var that = this;
var targets = {};
var targets = [];
var resolved = {};
var installed;
@@ -38,50 +38,46 @@ Project.prototype.install = function (endpoints, options) {
options = options || {};
this._production = !!options.production;
// If no endpoints were specified, simply repair the project
// Note that we also repair incompatible packages
if (!endpoints) {
return this._repair(true)
.fin(function () {
that._working = false;
});
}
// Start by repairing the project, installing only missing packages
return this._repair()
// Analyse the project
.then(that.analyse.bind(this))
return this.analyse()
.spread(function (json, tree, flattened) {
// Mark targets
endpoints.forEach(function (endpoint) {
var decEndpoint = endpointParser.decompose(endpoint);
targets[decEndpoint.name] = decEndpoint;
});
// Mark every package from the tree as resolved
// if it's not a target or a non-shared descendant of a target
// This is done by walking the tree (deep first) and abort traversal
// as soon as one target was found
// Walk down the tree adding missing and incompatible
// as targets
that._walkTree(tree, function (node, name) {
if (targets[name]) {
return false; // Abort traversal
if (node.walked) {
return;
}
resolved[name] = node.pkgMeta;
if (node.missing || node.incompatible) {
targets.push(node);
} else {
resolved[name] = node;
}
node.walked = true;
});
// Add endpoints as targets
if (endpoints) {
endpoints.forEach(function (endpoint) {
var decEndpoint = endpointParser.decompose(endpoint);
targets[decEndpoint.name] = decEndpoint;
});
}
// Mark installed
installed = mout.object.map(flattened, function (decEndpoint) {
return decEndpoint.pkgMeta;
});
})
// Bootstrap the process
.then(function () {
return that._bootstrap(targets, resolved, installed);
return that._bootstrap(mout.object.values(targets), resolved, installed);
})
// Handle save and saveDev options
.then(function (installed) {
if (!options.save && !options.saveDev) {
return;
return installed;
}
// Cycle through the initial targets and not the installed
@@ -114,11 +110,9 @@ Project.prototype.install = function (endpoints, options) {
Project.prototype.update = function (names, options) {
var that = this;
var targets;
var resolved;
var targets = [];
var resolved = {};
var installed;
var repaired;
var promise;
// If already working, error out
if (this._working) {
@@ -128,77 +122,71 @@ Project.prototype.update = function (names, options) {
options = options || {};
this._production = !!options.production;
// If no names were specified, we update every package
if (!names) {
// Analyse the project
promise = this.analyse()
.spread(function (json, tree, flattened) {
// Analyse the project
return this.analyse()
.spread(function (json, tree, flattened) {
// If no names were specified, update every package
if (!names) {
// Mark each json entry as targets
targets = mout.object.map(json.dependencies, function (value, key) {
return endpointParser.json2decomposed(key, value);
mout.object.forOwn(json.dependencies, function (value, key) {
var decEndpoint = endpointParser.json2decomposed(key, value);
decEndpoint.dependants = {};
decEndpoint.dependants[tree.name] = tree;
targets.push(decEndpoint);
});
// Mark installed
installed = mout.object.map(flattened, function (decEndpoint) {
return decEndpoint.pkgMeta;
});
});
// Otherwise we selectively update the specified ones
} else {
// Start by repairing the project
// Note that we also repair incompatible packages
promise = this._repair(true)
// Analyse the project
.then(function (result) {
repaired = result;
return that.analyse();
})
.spread(function (json, tree, flattened) {
targets = {};
resolved = {};
// Mark targets
names.forEach(function (name) {
var decEndpoint = flattened[name];
var jsonEntry;
if (!decEndpoint) {
throw createError('Package ' + name + ' is not installed', 'ENOTINSTALLED');
// Mark extraneous as targets
mout.object.forOwn(flattened, function (decEndpoint) {
if (decEndpoint.extraneous) {
targets.push(decEndpoint);
}
// If it was repaired, don't include in the targets
if (repaired[name]) {
});
// Otherwise, selectively update the specified ones
} else {
// Walk down the tree adding missing, incompatible
// and names as targets
that._walkTree(tree, function (node, name) {
if (node.walked) {
return;
}
// Use json entry if available, fallbacking to the installed one
jsonEntry = json.dependencies && json.dependencies[name];
if (jsonEntry) {
targets[name] = endpointParser.json2decomposed(name, jsonEntry);
if (node.missing || node.incompatible) {
targets.push(node);
} else if (names.indexOf(name) !== -1) {
targets.push(node);
} else {
targets[name] = decEndpoint;
}
});
// Mark every package from the tree as resolved
// if it's not a target or a non-shared descendant of a target
that._walkTree(tree, function (node, name) {
if (targets[name]) {
return false; // Abort traversal
resolved[name] = node;
}
resolved[name] = node.pkgMeta;
node.walked = true;
});
// Mark installed
installed = mout.object.map(flattened, function (decEndpoint) {
return decEndpoint.pkgMeta;
// Mark extraneous as targets only if
// it's not already a target
mout.object.forOwn(flattened, function (decEndpoint) {
var foundTarget;
var name = decEndpoint.name;
if (decEndpoint.extraneous && names.indexOf(name) !== -1) {
foundTarget = !!mout.array.find(targets, function (target) {
return target.name === name;
});
if (!foundTarget) {
targets.push(decEndpoint);
}
}
});
}
// Mark installed
installed = mout.object.map(flattened, function (decEndpoint) {
return decEndpoint.pkgMeta;
});
}
})
// Bootstrap the process
return promise.then(function () {
.then(function () {
return that._bootstrap(targets, resolved, installed);
})
.fin(function () {
@@ -303,12 +291,17 @@ Project.prototype.analyse = function () {
source: this._config.cwd,
target: json.version,
pkgMeta: json,
canonicalPkg: this._config.cwd,
root: true
};
if (json.version) {
root.pkgMeta._release = json.version;
}
// Restore the original dependencies cross-references,
// that is, the parent-child relationships
this._restoreNode(root, flattened);
this._restoreNode(root, flattened, 'dependencies');
// Do the same for the dev dependencies
if (!this._production) {
this._restoreNode(root, flattened, 'devDependencies');
@@ -316,22 +309,27 @@ Project.prototype.analyse = function () {
// Parse extraneous
mout.object.forOwn(flattened, function (decEndpoint) {
var release;
if (!decEndpoint.dependants) {
decEndpoint.extraneous = true;
// Restore it
this._restoreNode(decEndpoint, flattened);
this._restoreNode(decEndpoint, flattened, 'dependencies');
// Do the same for the dev dependencies
if (!this._production) {
this._restoreNode(decEndpoint, flattened, 'devDependencies');
}
release = decEndpoint.pkgMeta._release;
this._logger.log('warn', 'extraneous', decEndpoint.name + (release ? '#' + release : release), {
pkgMeta: decEndpoint.pkgMeta,
canonicalPkg: decEndpoint.canonicalPkg
});
}
}, this);
// The package meta set above is not really a package meta
// so we delete it from the root
// Also remove it from the flattened tree
delete root.pkgMeta;
// Remove root from the flattened tree
delete flattened[json.name];
return [json, root, flattened];
@@ -343,7 +341,8 @@ Project.prototype.analyse = function () {
Project.prototype._bootstrap = function (targets, resolved, installed) {
// Configure the manager and kick in the resolve process
return this._manager
.configure(mout.object.values(targets), resolved, installed)
.setProduction(this._production)
.configure(targets, resolved, installed)
.resolve()
// Install resolved ones
.then(function () {
@@ -351,39 +350,6 @@ Project.prototype._bootstrap = function (targets, resolved, installed) {
}.bind(this));
};
Project.prototype._repair = function (incompatible) {
var that = this;
return this.analyse()
.spread(function (json, tree, flattened) {
var targets = [];
var resolved = {};
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) {
targets.push(decEndpoint);
isBroken = true;
} else if (incompatible && decEndpoint.incompatible) {
targets.push(decEndpoint);
isBroken = true;
} else if (!decEndpoint.extraneous) {
resolved[name] = decEndpoint.pkgMeta;
}
});
// Do not proceed if the project does not need to be repaired
if (!isBroken) {
return {};
}
// Configure the manager and kick in the resolve process
return that._bootstrap(targets, resolved);
});
};
Project.prototype._readJson = function () {
var that = this;
@@ -550,20 +516,27 @@ Project.prototype._restoreNode = function (node, flattened, jsonKey) {
node.dependencies = {};
node.dependants = node.dependants || {};
mout.object.forOwn(node.pkgMeta[jsonKey || 'dependencies'], function (value, key) {
mout.object.forOwn(node.pkgMeta[jsonKey], function (value, key) {
var local = flattened[key];
var json = endpointParser.json2decomposed(key, value);
var compatible;
// Check if the dependency is not installed
if (!local) {
flattened[key] = local = json;
local.missing = true;
// Even if it is installed, check if it's compatible
} else if (!local.incompatible && !local.missing && !this._manager.areCompatible(local.pkgMeta.version || '*', json.target)) {
json.pkgMeta = local.pkgMeta;
flattened[key] = local = json;
local = json;
local.incompatible = true;
} else {
if (local.missing) {
compatible = this._manager.areCompatible(local.target, json.target);
} else {
compatible = this._manager.areCompatible(local.pkgMeta.version || '*', json.target);
}
if (!compatible) {
local = json;
local.incompatible = true;
}
}
// Cross reference
@@ -572,7 +545,7 @@ Project.prototype._restoreNode = function (node, flattened, jsonKey) {
local.dependants[node.name] = node;
// Call restore for this dependency
this._restoreNode(local, flattened);
this._restoreNode(local, flattened, jsonKey);
}, this);
};

View File

@@ -1,5 +1,6 @@
require('colors');
var mout = require('mout');
var semver = require('semver');
var template = require('../util/template');
var wideCommands = ['install', 'update'];
@@ -71,49 +72,31 @@ StandardRenderer.prototype.log = function (log) {
};
StandardRenderer.prototype.updateNotice = function (data) {
template('std/update-notice.std', data)
.then(function (str) {
this._write(process.stderr, str);
}.bind(this), this.error.bind(this));
var str = template.render('std/update-notice.std', data);
this._write(process.stderr, str);
};
// -------------------------
StandardRenderer.prototype._install = function (installed) {
// TODO: render tree of installed packages
};
StandardRenderer.prototype._update = function (updated) {
// TODO: render tree of updated packages
};
StandardRenderer.prototype._help = function (data) {
var str;
var that = this;
var specific;
if (!data.command) {
template('std/help.std', data)
.then(function (str) {
that._write(process.stdout, str);
}, this.error.bind(this));
str = template.render('std/help.std', data);
that._write(process.stdout, str);
} else {
// Try to render the help template for this command
template('std/help-' + data.command + '.std', data)
.then(function (str) {
that._write(process.stdout, str);
}, function (err) {
// If it failed with something else than ENOENT
// error out
if (err.code !== 'ENOENT') {
return err;
}
// Check if a specific template exists for the command
specific = 'std/help-' + data.command + '.std';
// Otherwise the template does not exist,
// so render the generic one
return template('std/help-generic.std', data)
.then(function (str) {
that._write(process.stdout, str);
}, that.error.bind(that));
});
if (template.exists(specific)) {
str = template.render(specific, data);
} else {
str = template.render('std/help-generic.std', data);
}
that._write(process.stdout, str);
}
};
@@ -138,6 +121,52 @@ StandardRenderer.prototype._mutualLog = function (log) {
this._genericLog(log);
};
StandardRenderer.prototype._incompatibleLog = function (log) {
var str;
// Generate dependants string for each pick
log.data.picks.forEach(function (pick) {
pick.dependants = pick.dependants.map(function (dependant) {
var release = dependant.pkgMeta._release;
return dependant.endpoint.name + (release ? '#' + release : '');
}).join(', ');
});
// Sort picks by version/release
log.data.picks.sort(function (pick1, pick2) {
var version1 = pick1.pkgMeta.version;
var version2 = pick2.pkgMeta.version;
// If both have versions, compare their versions using semver
if (version1 && version2) {
if (semver.gt(version1, version2)) {
return 1;
}
if (semver.lt(version1, version2)) {
return -1;
}
return 0;
}
// Give priority to the one that is a version
if (version1) {
return 1;
}
if (version2) {
return -1;
}
return 0;
});
str = template.render('std/incompatible.std', log.data);
this._write(process.stdout, '\n');
this._write(process.stdout, str);
this._write(process.stdout, '\n');
};
StandardRenderer.prototype._checkoutLog = function (log) {
if (this._compact) {
log.message = log.origin + '#' + log.message;

View File

@@ -14,32 +14,33 @@ mout.object.forOwn(helpers, function (register) {
register(Handlebars);
});
function template(name, data, escape) {
var compiled = cache[name];
var templatePath;
function render(name, data, escape) {
var contents;
// Check if already compiled
// Note that the cache might contain promises so we resolve
if (compiled) {
return Q.resolve(compiled)
.then(function (compiled) {
return compiled(data);
});
if (cache[name]) {
return cache[name](data);
}
// Otherwise, read the file, compile and cache
templatePath = path.join(templatesDir, name);
compiled = cache[name] = Q.nfcall(fs.readFile, templatePath)
.then(function (contents) {
return cache[name] = Handlebars.compile(contents.toString(), {
noEscape: !escape
});
contents = fs.readFileSync(path.join(templatesDir, name)).toString();
cache[name] = Handlebars.compile(contents, {
noEscape: !escape
});
// Call the function again
return compiled.then(function () {
return template(name, data);
});
return render(name, data, escape);
}
module.exports = template;
function exists(name) {
if (cache[name]) {
return true;
}
return fs.existsSync(path.join(templatesDir, name));
}
module.exports = {
render: render,
exists: exists
};