var Q = require('q'); var mout = require('mout'); var path = require('path'); var mkdirp = require('mkdirp'); var rimraf = require('../util/rimraf'); var fs = require('../util/fs'); var endpointParser = require('bower-endpoint-parser'); var PackageRepository = require('./PackageRepository'); var semver = require('../util/semver'); var copy = require('../util/copy'); var createError = require('../util/createError'); var scripts = require('./scripts'); var relativeToBaseDir = require('../util/relativeToBaseDir'); function Manager(config, logger) { this._config = config; this._logger = logger; this._repository = new PackageRepository(this._config, this._logger); this.configure({}); } // ----------------- Manager.prototype.configure = function(setup) { var targetsHash = {}; this._conflicted = {}; // Targets - ignore those specified in ignoredDependencies this._targets = mout.array.reject( setup.targets || [], function(target) { return mout.array.contains( this._config.ignoredDependencies, target.name ); }, this ); this._targets.forEach(function(decEndpoint) { decEndpoint.initialName = decEndpoint.name; decEndpoint.dependants = mout.object.values(decEndpoint.dependants); targetsHash[decEndpoint.name] = true; // If the endpoint is marked as newly, make it unresolvable decEndpoint.unresolvable = !!decEndpoint.newly; }); // Resolved & installed this._resolved = {}; this._installed = {}; mout.object.forOwn( setup.resolved, function(decEndpoint, name) { decEndpoint.dependants = mout.object.values(decEndpoint.dependants); this._resolved[name] = [decEndpoint]; this._installed[name] = decEndpoint.pkgMeta; }, this ); // Installed mout.object.mixIn(this._installed, setup.installed); // Incompatibles this._incompatibles = {}; setup.incompatibles = this._uniquify(setup.incompatibles || []); setup.incompatibles.forEach(function(decEndpoint) { var name = decEndpoint.name; this._incompatibles[name] = this._incompatibles[name] || []; this._incompatibles[name].push(decEndpoint); decEndpoint.dependants = mout.object.values(decEndpoint.dependants); // Mark as conflicted so that the resolution is not removed this._conflicted[name] = true; // If not a target/resolved, add as target if (!targetsHash[name] && !this._resolved[name]) { this._targets.push(decEndpoint); } }, this); // Resolutions this._resolutions = setup.resolutions || {}; // Uniquify targets this._targets = this._uniquify(this._targets); // Force-latest this._forceLatest = !!setup.forceLatest; return this; }; /** * It starts recursive fetching of targets. The flow is as follows: * * 1. this._fetch() that calls: * 2. this._onFetchError() or this._onFetchSuccess() that calls: * 3. this._parseDependencies() that calls for all dependencies: * 4. this._fetch() again. * * Fetching and parsing of dependencies continues until all dependencies are fetched. * */ Manager.prototype.resolve = function() { // If already resolving, error out if (this._working) { return Q.reject(createError('Already working', 'EWORKING')); } // Reset stuff this._fetching = {}; this._nrFetching = 0; this._failed = {}; this._hasFailed = false; this._deferred = Q.defer(); // If there's nothing to resolve, simply dissect if (!this._targets.length) { process.nextTick(this._dissect.bind(this)); // Otherwise, fetch each target from the repository // and let the process roll out } else { this._targets.forEach(this._fetch.bind(this)); } // Unset working flag when done return this._deferred.promise.fin( function() { this._working = false; }.bind(this) ); }; Manager.prototype.preinstall = function(json) { var that = this; var componentsDir = relativeToBaseDir(this._config.cwd)( this._config.directory ); // If nothing to install, skip the code bellow if (mout.lang.isEmpty(that._dissected)) { return Q.resolve({}); } return Q.nfcall(mkdirp, componentsDir).then(function() { return scripts.preinstall( that._config, that._logger, that._dissected, that._installed, json ); }); }; Manager.prototype.postinstall = function(json) { var that = this; var componentsDir = relativeToBaseDir(this._config.cwd)( this._config.directory ); // If nothing to install, skip the code bellow if (mout.lang.isEmpty(that._dissected)) { return Q.resolve({}); } return Q.nfcall(mkdirp, componentsDir).then(function() { return scripts.postinstall( that._config, that._logger, that._dissected, that._installed, json ); }); }; Manager.prototype.install = function(json) { var componentsDir; var componentsDirContents; var that = this; // If already resolving, error out if (this._working) { return Q.reject(createError('Already working', 'EWORKING')); } // If nothing to install, skip the code bellow if (mout.lang.isEmpty(that._dissected)) { return Q.resolve({}); } componentsDir = relativeToBaseDir(this._config.cwd)(this._config.directory); return Q.nfcall(mkdirp, componentsDir) .then(function() { var promises = []; componentsDirContents = fs.readdirSync(componentsDir); mout.object.forOwn(that._dissected, function(decEndpoint, name) { var promise; var dst; var release = decEndpoint.pkgMeta._release; var exists = mout.array.contains(componentsDirContents, name); that._logger.action( 'install', name + (release ? '#' + release : ''), that.toData(decEndpoint) ); dst = path.join(componentsDir, name); if (exists && !that._installed[name] && !that._config.force) { // There is a folder in the components directory that has // this name, but it was not installed by bower. that._logger.warn( 'skipped', name + ' was not installed because there is already a non-bower directory with that name in the components directory (' + path.relative(that._config.cwd, dst) + '). You can force installation with --force.' ); return; } // Remove existent and copy canonical dir promise = Q.nfcall(rimraf, dst) .then( copy.copyDir.bind(copy, decEndpoint.canonicalDir, dst) ) .then(function() { var metaFile = path.join(dst, '.bower.json'); decEndpoint.canonicalDir = dst; // Store additional metadata in bower.json return Q.nfcall(fs.readFile, metaFile).then(function( contents ) { var json = JSON.parse(contents.toString()); json._target = decEndpoint.target; json._originalSource = decEndpoint.source; if (decEndpoint.newly) { json._direct = true; } json = JSON.stringify(json, null, ' '); return Q.nfcall(fs.writeFile, metaFile, json); }); }); promises.push(promise); }); return Q.all(promises); }) .then(function() { // Sync up dissected dependencies and dependants // See: https://github.com/bower/bower/issues/879 mout.object.forOwn( that._dissected, function(pkg) { // Sync dependencies mout.object.forOwn( pkg.dependencies, function(dependency, name) { var dissected = this._dissected[name] || (this._resolved[name] ? this._resolved[name][0] : dependency); pkg.dependencies[name] = dissected; }, this ); // Sync dependants pkg.dependants = pkg.dependants.map(function(dependant) { var name = dependant.name; var dissected = this._dissected[name] || (this._resolved[name] ? this._resolved[name][0] : dependant); return dissected; }, this); }, that ); // Resolve with meaningful data return mout.object.map( that._dissected, function(decEndpoint) { return this.toData(decEndpoint); }, that ); }) .fin( function() { this._working = false; }.bind(this) ); }; Manager.prototype.toData = function(decEndpoint, extraKeys, upperDeps) { var names; var extra; var data = {}; upperDeps = upperDeps || []; data.endpoint = mout.object.pick(decEndpoint, ['name', 'source', 'target']); if (decEndpoint.canonicalDir) { data.canonicalDir = decEndpoint.canonicalDir; data.pkgMeta = decEndpoint.pkgMeta; } if (extraKeys) { extra = mout.object.pick(decEndpoint, extraKeys); extra = mout.object.filter(extra, function(value) { return !!value; }); mout.object.mixIn(data, extra); } if (decEndpoint.dependencies) { data.dependencies = {}; // Call recursively for each dependency but ordered // by dependency names names = Object.keys(decEndpoint.dependencies).sort(); names.forEach(function(name) { // Prevent from infinite recursion when installing cyclic // dependencies if (!mout.array.contains(upperDeps, name)) { data.dependencies[name] = this.toData( decEndpoint.dependencies[name], extraKeys, upperDeps.concat(decEndpoint.name) ); } }, this); } data.nrDependants = mout.object.size(decEndpoint.dependants); return data; }; Manager.prototype.getPackageRepository = function() { return this._repository; }; // ----------------- Manager.prototype._fetch = function(decEndpoint) { var name = decEndpoint.name; // Check if the whole process started to fail fast if (this._hasFailed) { return; } // Mark as being fetched this._fetching[name] = this._fetching[name] || []; this._fetching[name].push(decEndpoint); this._nrFetching++; // Fetch it from the repository // Note that the promise is stored in the decomposed endpoint // because it might be reused if a similar endpoint needs to be resolved return (decEndpoint.promise = this._repository .fetch(decEndpoint) // When done, call onFetchSuccess .spread(this._onFetchSuccess.bind(this, decEndpoint)) // If it fails, call onFetchFailure .fail(this._onFetchError.bind(this, decEndpoint))); }; Manager.prototype._onFetchSuccess = function( decEndpoint, canonicalDir, pkgMeta, isTargetable ) { var name; var resolved; var index; var incompatibles; var initialName = decEndpoint.initialName != null ? decEndpoint.initialName : decEndpoint.name; var fetching = this._fetching[initialName]; // Remove from being fetched list mout.array.remove(fetching, decEndpoint); this._nrFetching--; // Store some needed stuff decEndpoint.name = name = decEndpoint.name || pkgMeta.name; decEndpoint.canonicalDir = canonicalDir; decEndpoint.pkgMeta = pkgMeta; delete decEndpoint.promise; // Add to the resolved list // If there's an exact equal endpoint, replace instead of adding // This can happen because the name might not be known from the start resolved = this._resolved[name] = this._resolved[name] || []; index = mout.array.findIndex(resolved, function(resolved) { return resolved.target === decEndpoint.target; }); if (index !== -1) { // Merge dependants decEndpoint.dependants.push.apply( decEndpoint.dependants, resolved[index.dependants] ); decEndpoint.dependants = this._uniquify(decEndpoint.dependants); resolved.splice(index, 1); } resolved.push(decEndpoint); // Parse dependencies this._parseDependencies(decEndpoint, pkgMeta); // Check if there are incompatibilities for this package name // If there are, we need to fetch them incompatibles = this._incompatibles[name]; if (incompatibles) { // Filter already resolved incompatibles = incompatibles.filter(function(incompatible) { return !resolved.some(function(decEndpoint) { return incompatible.target === decEndpoint.target; }); }, this); // Filter being resolved incompatibles = incompatibles.filter(function(incompatible) { return !fetching.some(function(decEndpoint) { return incompatible.target === decEndpoint.target; }); }, this); incompatibles.forEach(this._fetch.bind(this)); delete this._incompatibles[name]; } // If the package is not targetable, flag it // It will be needed later so that untargetable endpoints // will not get * converted to ^version if (!isTargetable) { decEndpoint.untargetable = true; } // If there are no more packages being fetched, // finish the resolve process by dissecting all resolved packages if (this._nrFetching <= 0) { process.nextTick(this._dissect.bind(this)); } }; Manager.prototype._onFetchError = function(decEndpoint, err) { var name = decEndpoint.name; err.data = err.data || {}; err.data.endpoint = mout.object.pick(decEndpoint, [ 'name', 'source', 'target' ]); // Remove from being fetched list mout.array.remove(this._fetching[name], decEndpoint); this._nrFetching--; // Add to the failed list this._failed[name] = this._failed[name] || []; this._failed[name].push(err); delete decEndpoint.promise; // Make the whole process to fail fast this._failFast(); // If there are no more packages being fetched, // finish the resolve process (with an error) if (this._nrFetching <= 0) { process.nextTick(this._dissect.bind(this)); } }; Manager.prototype._failFast = function() { if (this._hasFailed) { return; } this._hasFailed = true; // If after some amount of time all pending tasks haven't finished, // we force the process to end this._failFastTimeout = setTimeout( function() { this._nrFetching = Infinity; this._dissect(); }.bind(this), 20000 ); }; /** * Parses decEndpoint dependencies and fetches missing ones. * * It fetches all non-yet-fetching dependencies with _fetch. * * * @param {string} pkgMeta Metadata of package to resolve dependencies. */ Manager.prototype._parseDependencies = function(decEndpoint, pkgMeta) { var pending = []; decEndpoint.dependencies = decEndpoint.dependencies || {}; // Parse package dependencies mout.object.forOwn( pkgMeta.dependencies, function(value, key) { var resolved; var fetching; var compatible; var childDecEndpoint = endpointParser.json2decomposed(key, value); // Check if this dependency should be skipped. if ( mout.array.contains( this._config.ignoredDependencies, childDecEndpoint.name ) ) { this._logger.action( 'skipped', childDecEndpoint.name, this.toData(decEndpoint) ); return; } // 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 if there's one with the exact same target compatible = mout.array.find( resolved, function(resolved) { return childDecEndpoint.target === resolved.target; }, this ); // If we found one, merge stuff instead of adding as resolved if (compatible) { decEndpoint.dependencies[key] = compatible; compatible.dependants.push(decEndpoint); compatible.dependants = this._uniquify( compatible.dependants ); return; } // Find one that is compatible compatible = mout.array.find( resolved, function(resolved) { return this._areCompatible(childDecEndpoint, resolved); }, this ); // If we found one, add as resolved // and copy resolved properties from the compatible one if (compatible) { decEndpoint.dependencies[key] = compatible; childDecEndpoint.canonicalDir = compatible.canonicalDir; childDecEndpoint.pkgMeta = compatible.pkgMeta; childDecEndpoint.dependencies = compatible.dependencies; childDecEndpoint.dependants = [decEndpoint]; this._resolved[key].push(childDecEndpoint); return; } } // Check if a compatible one is being fetched // If there's one, we wait and reuse it to avoid resolving it twice fetching = this._fetching[key]; if (fetching) { compatible = mout.array.find( fetching, function(fetching) { return this._areCompatible(childDecEndpoint, fetching); }, this ); if (compatible) { pending.push(compatible.promise); return; } } // Mark endpoint as unresolvable if the parent is also unresolvable childDecEndpoint.unresolvable = !!decEndpoint.unresolvable; // Otherwise, just fetch it from the repository decEndpoint.dependencies[key] = childDecEndpoint; childDecEndpoint.dependants = [decEndpoint]; this._fetch(childDecEndpoint); }, this ); if (pending.length > 0) { Q.all(pending).then( function() { this._parseDependencies(decEndpoint, pkgMeta); }.bind(this) ); } }; /** * After batch of packages has been fetched, the results are stored in _resolved hash. * * { * packageName: [array, of, decEndpoints] * } * * This method takes all possible decEndpoints and selects only one of them per packageName. * * It stores selected packages in the this._dissected array. * * The _dissected array is used later on by install method to move packages to config.directory. */ Manager.prototype._dissect = function() { var err; var componentsDir; var promise = Q.resolve(); var suitables = {}; var that = this; // If something failed, reject the whole resolve promise // with the first error if (this._hasFailed) { clearTimeout(this._failFastTimeout); // Cancel fail fast timeout err = mout.object.values(this._failed)[0][0]; this._deferred.reject(err); return; } // Find a suitable version for each package name mout.object.forOwn( this._resolved, function(decEndpoints, name) { var semvers; var nonSemvers; // Filter out non-semver ones semvers = decEndpoints.filter(function(decEndpoint) { return !!decEndpoint.pkgMeta.version; }); // Sort semver ones DESC semvers.sort(function(first, second) { var result = semver.rcompare( first.pkgMeta.version, second.pkgMeta.version ); // If they are equal and one of them is a wildcard target, // give lower priority if (!result) { if (first.target === '*') { return 1; } if (second.target === '*') { return -1; } } // If they are equal and one of them is an exact target, // give higher priority if (!result) { if (first.target === first.pkgMeta.version) { return -1; } if (second.target === second.pkgMeta.version) { return 1; } } return result; }); // Convert wildcard targets to semver range targets if they are newly // Note that this can only be made if they can be targetable // If they are not, the resolver is incapable of handling targets semvers.forEach(function(decEndpoint) { if ( decEndpoint.newly && decEndpoint.target === '*' && !decEndpoint.untargetable ) { decEndpoint.target = '^' + decEndpoint.pkgMeta.version; decEndpoint.originalTarget = '*'; } }); // Filter non-semver ones nonSemvers = decEndpoints.filter(function(decEndpoint) { return !decEndpoint.pkgMeta.version; }); promise = promise.then(function() { return that ._electSuitable(name, semvers, nonSemvers) .then(function(suitable) { suitables[name] = suitable; }); }); }, this ); // After a suitable version has been elected for every package promise .then( function() { // Look for extraneous resolutions mout.object.forOwn( this._resolutions, function(resolution, name) { if (this._conflicted[name]) { return; } // Packages that didn't have any conflicts in resolution yet have // "resolution" entry in bower.json are deemed unnecessary // // Probably in future we want to use them for force given resolution, but for now... // See: https://github.com/bower/bower/issues/1362 this._logger.warn( 'extra-resolution', 'Unnecessary resolution: ' + name + '#' + resolution, { name: name, resolution: resolution, action: 'delete' } ); }, this ); // Filter only packages that need to be installed componentsDir = relativeToBaseDir(this._config.cwd)( this._config.directory ); this._dissected = mout.object.filter( suitables, function(decEndpoint, name) { var installedMeta = this._installed[name]; var dst; // Skip linked dependencies if (decEndpoint.linked) { return false; } // Skip if source is the same as dest // FIXME: Shoudn't we force installation if force flag is used here? dst = path.join(componentsDir, name); if (dst === decEndpoint.canonicalDir) { return false; } // We skip installing decEndpoint, if: // 1. We have installed package with the same name // 2. Packages have matching meta fields (needs explanation) // 3. We didn't force the installation if ( installedMeta && installedMeta._target === decEndpoint.target && installedMeta._originalSource === decEndpoint.source && installedMeta._release === decEndpoint.pkgMeta._release ) { return this._config.force; } return true; }, this ); }.bind(this) ) .then(this._deferred.resolve, this._deferred.reject); }; Manager.prototype._electSuitable = function(name, semvers, nonSemvers) { var suitable; var resolution; var unresolvable; var dataPicks; var save; var picks = []; var versionRegex = /(?:[\d\w]\.){2}[\d\w](?:.)*/; var picksReleases; // If there are both semver and non-semver, there's no way // to figure out the suitable one if (semvers.length && nonSemvers.length) { picks.push.apply(picks, semvers); picks.push.apply(picks, nonSemvers); // If there are only non-semver ones, the suitable is elected // only if there's one } else if (nonSemvers.length) { if (nonSemvers.length === 1) { return Q.resolve(nonSemvers[0]); } picks.push.apply(picks, nonSemvers); // If there are only semver ones, figure out which one is // compatible with every requirement } else { suitable = mout.array.find(semvers, function(subject) { return semvers.every(function(decEndpoint) { return ( subject === decEndpoint || semver.satisfies( subject.pkgMeta.version, decEndpoint.target ) ); }); }); if (suitable) { return Q.resolve(suitable); } picks.push.apply(picks, semvers); } // At this point, there's a conflict this._conflicted[name] = true; // Prepare data to be sent bellow // 1 - Sort picks by version/release picks.sort(function(pick1, pick2) { var version1 = pick1.pkgMeta.version; var version2 = pick2.pkgMeta.version; var comp; // If both have versions, compare their versions using semver if (version1 && version2) { comp = semver.compare(version1, version2); if (comp) { return comp; } } else { // If one of them has a version, it's considered higher if (version1) { return 1; } if (version2) { return -1; } } // Give priority to the one with most dependants if (pick1.dependants.length > pick2.dependants.length) { return -1; } if (pick1.dependants.length < pick2.dependants.length) { return 1; } return 0; }); // 2 - Transform data dataPicks = picks.map(function(pick) { var dataPick = this.toData(pick); dataPick.dependants = pick.dependants.map(this.toData, this); dataPick.dependants.sort(function(dependant1, dependant2) { return dependant1.endpoint.name.localeCompare( dependant2.endpoint.name ); }); return dataPick; }, this); // Check if there's a resolution that resolves the conflict // Note that if one of them is marked as unresolvable, // the resolution has no effect resolution = this._resolutions[name]; unresolvable = mout.object.find(picks, function(pick) { return pick.unresolvable; }); if (resolution && !unresolvable) { suitable = -1; // Range resolution if (semver.validRange(resolution)) { suitable = mout.array.findIndex(picks, function(pick) { return ( pick.pkgMeta.version && semver.satisfies(pick.pkgMeta.version, resolution) ); }); } // Exact match resolution (e.g. branches/tags) if (suitable === -1) { suitable = mout.array.findIndex(picks, function(pick) { return ( pick.target === resolution || pick.pkgMeta._release === resolution ); }); } if (suitable === -1) { this._logger.warn( 'resolution', 'Unsuitable resolution declared for ' + name + ': ' + resolution, { name: name, picks: dataPicks, resolution: resolution } ); } else { 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 force latest is enabled, resolve to the highest semver version // or whatever non-semver if none available if (this._forceLatest) { suitable = picks.length - 1; this._logger.conflict( 'solved', 'Unable to find suitable version for ' + name, { name: name, picks: dataPicks, suitable: dataPicks[suitable], forced: true } ); // Save resolution if (this._config.argv.cooked.includes('--save')) { this._storeResolution(picks[suitable]); } return Q.resolve(picks[suitable]); } // If interactive is disabled, error out if (!this._config.interactive) { throw createError( 'Unable to find suitable version for ' + name, 'ECONFLICT', { name: name, picks: dataPicks } ); } // 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 } ); picksReleases = picks.map(function(pick) { return pick.pkgMeta._release; }); return Q.nfcall(this._logger.prompt.bind(this._logger), { type: 'input', message: 'Answer', validate: function(choice) { var invalidChoice = 'Invalid choice'; if (choice.match(versionRegex)) { return picksReleases.indexOf(choice) != -1 ? true : invalidChoice; } choice = Number(mout.string.trim(choice.trim(), '!')); if (!choice || choice < 1 || choice > picks.length) { return invalidChoice; } return true; } }).then( function(choice) { var pick; // Sanitize choice choice = choice.trim(); save = /^!/.test(choice) || /!$/.test(choice); // Save if prefixed or suffixed with ! if (choice.match(versionRegex)) { pick = picks[picksReleases.indexOf(choice)]; } else { choice = Number(mout.string.trim(choice, '!')); pick = picks[choice - 1]; } // Save resolution if (save) { this._storeResolution(pick); } return pick; }.bind(this) ); }; Manager.prototype._storeResolution = function(pick) { var resolution; var name = pick.name; if (pick.target === '*') { resolution = pick.pkgMeta._release || '*'; } else { resolution = pick.target; } this._logger.info( 'resolution', 'Saved ' + name + '#' + resolution + ' as resolution', { name: name, resolution: resolution, action: this._resolutions[name] ? 'edit' : 'add' } ); this._resolutions[name] = resolution; }; /** * Checks if some endpoint is compatible with already resolved target. * * It is used in two situations: * * checks if resolved component matches dependency constraint * * checks if not resolved component matches already fetched component * * If candidate matches already resolved component, it won't be downloaded. * * @param {Endpoint} candidate endpoint * @param {Endpoint} resolved endpoint * * @return {Boolean} */ Manager.prototype._areCompatible = function(candidate, resolved) { var resolvedVersion; var highestCandidate; var highestResolved; var candidateIsRange = semver.validRange(candidate.target); var resolvedIsRange = semver.validRange(resolved.target); var candidateIsVersion = semver.valid(candidate.target); var resolvedIsVersion = semver.valid(resolved.target); // Check if targets are equal if (candidate.target === resolved.target) { return true; } resolvedVersion = resolved.pkgMeta && resolved.pkgMeta.version; // If there is no pkgMeta, resolvedVersion is downloading now // Check based on target requirements if (!resolvedVersion) { // If one of the targets is range and other is version, // check version against the range if (candidateIsVersion && resolvedIsRange) { return semver.satisfies(candidate.target, resolved.target); } if (resolvedIsVersion && candidateIsRange) { return semver.satisfies(resolved.target, candidate.target); } if (resolvedIsVersion && candidateIsVersion) { return semver.eq(resolved.target, candidate.target); } // If both targets are range, check that both have same // higher cap if (resolvedIsRange && candidateIsRange) { highestCandidate = this._getCap( semver.toComparators(candidate.target), 'highest' ); highestResolved = this._getCap( semver.toComparators(resolved.target), 'highest' ); // This never happens, but you can't be sure without tests if (!highestResolved.version || !highestCandidate.version) { return false; } return ( semver.eq(highestCandidate.version, highestResolved.version) && highestCandidate.comparator === highestResolved.comparator ); } return false; } // If target is a version, compare against the resolved version if (candidateIsVersion) { return semver.eq(candidate.target, resolvedVersion); } // If target is a range, check if resolved version satisfies it if (candidateIsRange) { return semver.satisfies(resolvedVersion, candidate.target); } return false; }; /** * Gets highest/lowest version from set of comparators. * * The only thing that matters for this function is version number. * Returned comparator is splitted to comparator and version parts. * * It is used to receive lowest / highest bound of toComparators result: * semver.toComparators('~0.1.1') // => [ [ '>=0.1.1-0', '<0.2.0-0' ] ] * * Examples: * * _getCap([['>=2.1.1-0', '<2.2.0-0'], '<3.2.0'], 'highest') * // => { comparator: '<', version: '3.2.0' } * * _getCap([['>=2.1.1-0', '<2.2.0-0'], '<3.2.0'], 'lowest') * // => { comparator: '>=', version: '2.1.1-0' } * * @param {Array.} comparators * @param {string} side, 'highest' (default) or 'lowest' * * @return {{ comparator: string, version: string }} */ Manager.prototype._getCap = function(comparators, side) { var matches; 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)) { 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+.*)$/); if (!matches) { return; } // Compare with the current highest version if (!cap.version || compare(matches[2], cap.version)) { cap.version = matches[2]; cap.comparator = matches[1]; } } }, this); return cap; }; /** * Filters out unique endpoints, comparing by name and then source. * * It leaves last matching endpoint. * * Examples: * * manager._uniquify([ * { name: 'foo', source: 'google.com' }, * { name: 'foo', source: 'facebook.com' } * ]); * // => { name: 'foo', source: 'facebook.com' } * * @param {Array.} decEndpoints * @return {Array.} Filtered elements of decEndpoints * */ Manager.prototype._uniquify = function(decEndpoints) { var length = decEndpoints.length; return decEndpoints.filter(function(decEndpoint, index) { var x; var current; for (x = index + 1; x < length; ++x) { current = decEndpoints[x]; if (current === decEndpoint) { return false; } // Compare name if both set // Fallback to compare sources if (!current.name && !decEndpoint.name) { if (current.source !== decEndpoint.source) { continue; } } else if (current.name !== decEndpoint.name) { continue; } // Compare targets if name/sources are equal if (current.target === decEndpoint.target) { return false; } } return true; }); }; module.exports = Manager;