src/cordova/plugin/add.js
function determinePluginVersionToFetch (pluginInfo, pluginMap, platformMap, cordovaVersion) {
    var allVersions = pluginInfo.versions;
    var engine = pluginInfo.engines.cordovaDependencies;
    var name = pluginInfo.name;

    // Filters out pre-release versions
    var latest = semver.maxSatisfying(allVersions, '>=0.0.0');

    var versions = [];
    var upperBound = null;
    var upperBoundRange = null;
    var upperBoundExists = false;

    // TODO: lots of 'versions' being thrown around in this function: cordova version,
    // platform version, plugin version. The below for loop: what version is it
    // iterating over? plugin version? please clarify the variable name.
    for (var version in engine) {
        // if a single version && less than latest
        if (semver.valid(semver.clean(version)) && semver.lte(version, latest)) {
            versions.push(version);
        } else {
            // Check if this is an upperbound; validRange() handles whitespace
            var cleanedRange = semver.validRange(version);
            if (cleanedRange && UPPER_BOUND_REGEX.exec(cleanedRange)) {
                upperBoundExists = true;
                // We only care about the highest upper bound that our project does not support
                if (module.exports.getFailedRequirements(engine[version], pluginMap, platformMap, cordovaVersion).length !== 0) {
                    var maxMatchingUpperBound = cleanedRange.substring(1);
                    if (maxMatchingUpperBound && (!upperBound || semver.gt(maxMatchingUpperBound, upperBound))) {
                        upperBound = maxMatchingUpperBound;
                        upperBoundRange = version;
                    }
                }
            } else {
                events.emit('verbose', 'Ignoring invalid version in ' + name + ' cordovaDependencies: ' + version + ' (must be a single version <= latest or an upper bound)');
            }
        }
    }
    // If there were no valid requirements, we fall back to old behavior
    if (!upperBoundExists && versions.length === 0) {
        events.emit('verbose', 'Ignoring ' + name + ' cordovaDependencies entry because it did not contain any valid plugin version entries');
        return null;
    }

    // Handle the lower end of versions by giving them a satisfied engine
    if (!module.exports.findVersion(versions, '0.0.0')) {
        versions.push('0.0.0');
        engine['0.0.0'] = {};
    }

    // Add an entry after the upper bound to handle the versions above the
    // upper bound but below the next entry. For example: 0.0.0, <1.0.0, 2.0.0
    // needs a 1.0.0 entry that has the same engine as 0.0.0
    if (upperBound && !module.exports.findVersion(versions, upperBound) && !semver.gt(upperBound, latest)) {
        versions.push(upperBound);
        var below = semver.maxSatisfying(versions, upperBoundRange);

        // Get the original entry without trimmed whitespace
        below = below ? module.exports.findVersion(versions, below) : null;
        engine[upperBound] = below ? engine[below] : {};
    }

    // Sort in descending order; we want to start at latest and work back
    versions.sort(semver.rcompare);

    for (var i = 0; i < versions.length; i++) {
        if (upperBound && semver.lt(versions[i], upperBound)) {
            // Because we sorted in desc. order, if the upper bound we found
            // applies to this version (and thus the ones below) we can just
            // quit
            break;
        }

        var range = i ? ('>=' + versions[i] + ' <' + versions[i - 1]) : ('>=' + versions[i]);
        var maxMatchingVersion = semver.maxSatisfying(allVersions, range);

        if (maxMatchingVersion && module.exports.getFailedRequirements(engine[versions[i]], pluginMap, platformMap, cordovaVersion).length === 0) {
            // Because we sorted in descending order, we can stop searching once
            // we hit a satisfied constraint
            if (maxMatchingVersion !== latest) {
                var failedReqs = module.exports.getFailedRequirements(engine[versions[0]], pluginMap, platformMap, cordovaVersion);

                // Warn the user that we are not fetching latest
                module.exports.listUnmetRequirements(name, failedReqs);
                events.emit('warn', 'Fetching highest version of ' + name + ' that this project supports: ' + maxMatchingVersion + ' (latest is ' + latest + ')');
            }
            return maxMatchingVersion;
        }
    }

    // No version of the plugin is satisfied. In this case, we fall back to
    // fetching the latest version, but also output a warning
    var latestFailedReqs = versions.length > 0 ? module.exports.getFailedRequirements(engine[versions[0]], pluginMap, platformMap, cordovaVersion) : [];

    // If the upper bound is greater than latest, we need to combine its engine
    // requirements with latest to print out in the warning
    if (upperBound && semver.satisfies(latest, upperBoundRange)) {
        var upperFailedReqs = module.exports.getFailedRequirements(engine[upperBoundRange], pluginMap, platformMap, cordovaVersion);
        upperFailedReqs.forEach(function (failedReq) {
            for (var i = 0; i < latestFailedReqs.length; i++) {
                if (latestFailedReqs[i].dependency === failedReq.dependency) {
                    // Not going to overcomplicate things and actually merge the ranges
                    latestFailedReqs[i].required += ' AND ' + failedReq.required;
                    return;
                }
            }

            // There is no req to merge it with
            latestFailedReqs.push(failedReq);
        });
    }

    module.exports.listUnmetRequirements(name, latestFailedReqs);
    events.emit('warn', 'Current project does not satisfy the engine requirements specified by any version of ' + name + '. Fetching latest version of plugin anyway (may be incompatible)');

    // No constraints were satisfied
    return null;
}
function determinePluginVersionToFetch (pluginInfo, pluginMap, platformMap, cordovaVersion) {
    var allVersions = pluginInfo.versions;
    var engine = pluginInfo.engines.cordovaDependencies;
    var name = pluginInfo.name;

    // Filters out pre-release versions
    var latest = semver.maxSatisfying(allVersions, '>=0.0.0');

    var versions = [];
    var upperBound = null;
    var upperBoundRange = null;
    var upperBoundExists = false;

    // TODO: lots of 'versions' being thrown around in this function: cordova version,
    // platform version, plugin version. The below for loop: what version is it
    // iterating over? plugin version? please clarify the variable name.
    for (var version in engine) {
        // if a single version && less than latest
        if (semver.valid(semver.clean(version)) && semver.lte(version, latest)) {
            versions.push(version);
        } else {
            // Check if this is an upperbound; validRange() handles whitespace
            var cleanedRange = semver.validRange(version);
            if (cleanedRange && UPPER_BOUND_REGEX.exec(cleanedRange)) {
                upperBoundExists = true;
                // We only care about the highest upper bound that our project does not support
                if (module.exports.getFailedRequirements(engine[version], pluginMap, platformMap, cordovaVersion).length !== 0) {
                    var maxMatchingUpperBound = cleanedRange.substring(1);
                    if (maxMatchingUpperBound && (!upperBound || semver.gt(maxMatchingUpperBound, upperBound))) {
                        upperBound = maxMatchingUpperBound;
                        upperBoundRange = version;
                    }
                }
            } else {
                events.emit('verbose', 'Ignoring invalid version in ' + name + ' cordovaDependencies: ' + version + ' (must be a single version <= latest or an upper bound)');
            }
        }
    }
    // If there were no valid requirements, we fall back to old behavior
    if (!upperBoundExists && versions.length === 0) {
        events.emit('verbose', 'Ignoring ' + name + ' cordovaDependencies entry because it did not contain any valid plugin version entries');
        return null;
    }

    // Handle the lower end of versions by giving them a satisfied engine
    if (!module.exports.findVersion(versions, '0.0.0')) {
        versions.push('0.0.0');
        engine['0.0.0'] = {};
    }

    // Add an entry after the upper bound to handle the versions above the
    // upper bound but below the next entry. For example: 0.0.0, <1.0.0, 2.0.0
    // needs a 1.0.0 entry that has the same engine as 0.0.0
    if (upperBound && !module.exports.findVersion(versions, upperBound) && !semver.gt(upperBound, latest)) {
        versions.push(upperBound);
        var below = semver.maxSatisfying(versions, upperBoundRange);

        // Get the original entry without trimmed whitespace
        below = below ? module.exports.findVersion(versions, below) : null;
        engine[upperBound] = below ? engine[below] : {};
    }

    // Sort in descending order; we want to start at latest and work back
    versions.sort(semver.rcompare);

    for (var i = 0; i < versions.length; i++) {
        if (upperBound && semver.lt(versions[i], upperBound)) {
            // Because we sorted in desc. order, if the upper bound we found
            // applies to this version (and thus the ones below) we can just
            // quit
            break;
        }

        var range = i ? ('>=' + versions[i] + ' <' + versions[i - 1]) : ('>=' + versions[i]);
        var maxMatchingVersion = semver.maxSatisfying(allVersions, range);

        if (maxMatchingVersion && module.exports.getFailedRequirements(engine[versions[i]], pluginMap, platformMap, cordovaVersion).length === 0) {
            // Because we sorted in descending order, we can stop searching once
            // we hit a satisfied constraint
            if (maxMatchingVersion !== latest) {
                var failedReqs = module.exports.getFailedRequirements(engine[versions[0]], pluginMap, platformMap, cordovaVersion);

                // Warn the user that we are not fetching latest
                module.exports.listUnmetRequirements(name, failedReqs);
                events.emit('warn', 'Fetching highest version of ' + name + ' that this project supports: ' + maxMatchingVersion + ' (latest is ' + latest + ')');
            }
            return maxMatchingVersion;
        }
    }

    // No version of the plugin is satisfied. In this case, we fall back to
    // fetching the latest version, but also output a warning
    var latestFailedReqs = versions.length > 0 ? module.exports.getFailedRequirements(engine[versions[0]], pluginMap, platformMap, cordovaVersion) : [];

    // If the upper bound is greater than latest, we need to combine its engine
    // requirements with latest to print out in the warning
    if (upperBound && semver.satisfies(latest, upperBoundRange)) {
        var upperFailedReqs = module.exports.getFailedRequirements(engine[upperBoundRange], pluginMap, platformMap, cordovaVersion);
        upperFailedReqs.forEach(function (failedReq) {
            for (var i = 0; i < latestFailedReqs.length; i++) {
                if (latestFailedReqs[i].dependency === failedReq.dependency) {
                    // Not going to overcomplicate things and actually merge the ranges
                    latestFailedReqs[i].required += ' AND ' + failedReq.required;
                    return;
                }
            }

            // There is no req to merge it with
            latestFailedReqs.push(failedReq);
        });
    }

    module.exports.listUnmetRequirements(name, latestFailedReqs);
    events.emit('warn', 'Current project does not satisfy the engine requirements specified by any version of ' + name + '. Fetching latest version of plugin anyway (may be incompatible)');

    // No constraints were satisfied
    return null;
}
function determinePluginVersionToFetch ( pluginInfo , pluginMap , platformMap , cordovaVersion ) { var allVersions = pluginInfo . versions ; var engine = pluginInfo . engines . cordovaDependencies ; var name = pluginInfo . name ; // Filters out pre-release versions var latest = semver . maxSatisfying ( allVersions , '>=0.0.0' ) ; var versions = [ ] ; var upperBound = null ; var upperBoundRange = null ; var upperBoundExists = false ; // TODO: lots of 'versions' being thrown around in this function: cordova version, // platform version, plugin version. The below for loop: what version is it // iterating over? plugin version? please clarify the variable name. for ( var version in engine ) { // if a single version && less than latest if ( semver . valid ( semver . clean ( version ) ) && semver . lte ( version , latest ) ) { versions . push ( version ) ; } else { // Check if this is an upperbound; validRange() handles whitespace var cleanedRange = semver . validRange ( version ) ; if ( cleanedRange && UPPER_BOUND_REGEX . exec ( cleanedRange ) ) { upperBoundExists = true ; // We only care about the highest upper bound that our project does not support if ( module . exports . getFailedRequirements ( engine [ version ] , pluginMap , platformMap , cordovaVersion ) . length !== 0 ) { var maxMatchingUpperBound = cleanedRange . substring ( 1 ) ; if ( maxMatchingUpperBound && ( ! upperBound || semver . gt ( maxMatchingUpperBound , upperBound ) ) ) { upperBound = maxMatchingUpperBound ; upperBoundRange = version ; } } } else { events . emit ( 'verbose' , 'Ignoring invalid version in ' + name + ' cordovaDependencies: ' + version + ' (must be a single version <= latest or an upper bound)' ) ; } } } // If there were no valid requirements, we fall back to old behavior if ( ! upperBoundExists && versions . length === 0 ) { events . emit ( 'verbose' , 'Ignoring ' + name + ' cordovaDependencies entry because it did not contain any valid plugin version entries' ) ; return null ; } // Handle the lower end of versions by giving them a satisfied engine if ( ! module . exports . findVersion ( versions , '0.0.0' ) ) { versions . push ( '0.0.0' ) ; engine [ '0.0.0' ] = { } ; } // Add an entry after the upper bound to handle the versions above the // upper bound but below the next entry. For example: 0.0.0, <1.0.0, 2.0.0 // needs a 1.0.0 entry that has the same engine as 0.0.0 if ( upperBound && ! module . exports . findVersion ( versions , upperBound ) && ! semver . gt ( upperBound , latest ) ) { versions . push ( upperBound ) ; var below = semver . maxSatisfying ( versions , upperBoundRange ) ; // Get the original entry without trimmed whitespace below = below ? module . exports . findVersion ( versions , below ) : null ; engine [ upperBound ] = below ? engine [ below ] : { } ; } // Sort in descending order; we want to start at latest and work back versions . sort ( semver . rcompare ) ; for ( var i = 0 ; i < versions . length ; i ++ ) { if ( upperBound && semver . lt ( versions [ i ] , upperBound ) ) { // Because we sorted in desc. order, if the upper bound we found // applies to this version (and thus the ones below) we can just // quit break ; } var range = i ? ( '>=' + versions [ i ] + ' <' + versions [ i - 1 ] ) : ( '>=' + versions [ i ] ) ; var maxMatchingVersion = semver . maxSatisfying ( allVersions , range ) ; if ( maxMatchingVersion && module . exports . getFailedRequirements ( engine [ versions [ i ] ] , pluginMap , platformMap , cordovaVersion ) . length === 0 ) { // Because we sorted in descending order, we can stop searching once // we hit a satisfied constraint if ( maxMatchingVersion !== latest ) { var failedReqs = module . exports . getFailedRequirements ( engine [ versions [ 0 ] ] , pluginMap , platformMap , cordovaVersion ) ; // Warn the user that we are not fetching latest module . exports . listUnmetRequirements ( name , failedReqs ) ; events . emit ( 'warn' , 'Fetching highest version of ' + name + ' that this project supports: ' + maxMatchingVersion + ' (latest is ' + latest + ')' ) ; } return maxMatchingVersion ; } } // No version of the plugin is satisfied. In this case, we fall back to // fetching the latest version, but also output a warning var latestFailedReqs = versions . length > 0 ? module . exports . getFailedRequirements ( engine [ versions [ 0 ] ] , pluginMap , platformMap , cordovaVersion ) : [ ] ; // If the upper bound is greater than latest, we need to combine its engine // requirements with latest to print out in the warning if ( upperBound && semver . satisfies ( latest , upperBoundRange ) ) { var upperFailedReqs = module . exports . getFailedRequirements ( engine [ upperBoundRange ] , pluginMap , platformMap , cordovaVersion ) ; upperFailedReqs . forEach ( function ( failedReq ) { for ( var i = 0 ; i < latestFailedReqs . length ; i ++ ) { if ( latestFailedReqs [ i ] . dependency === failedReq . dependency ) { // Not going to overcomplicate things and actually merge the ranges latestFailedReqs [ i ] . required += ' AND ' + failedReq . required ; return ; } } // There is no req to merge it with latestFailedReqs . push ( failedReq ) ; } ) ; } module . exports . listUnmetRequirements ( name , latestFailedReqs ) ; events . emit ( 'warn' , 'Current project does not satisfy the engine requirements specified by any version of ' + name + '. Fetching latest version of plugin anyway (may be incompatible)' ) ; // No constraints were satisfied return null ; }
/*
The engine entry maps plugin versions to constraints like so:
{
'1.0.0' : { 'cordova': '<5.0.0' },
'<2.0.0': {
'cordova': '>=5.0.0',
'cordova-ios': '~5.0.0',
'cordova-plugin-camera': '~5.0.0'
},
'3.0.0' : { 'cordova-ios': '>5.0.0' }
}

TODO: provide a better function description once logic is groked
TODO: update comment below once tests are rewritten/moved around.
See cordova-spec/plugin_fetch.spec.js for test cases and examples
/ * The engine entry maps plugin versions to constraints like so : { 1 . 0 . 0 : { cordova : <5 . 0 . 0 } <2 . 0 . 0 : { cordova : > = 5 . 0 . 0 cordova - ios : ~5 . 0 . 0 cordova - plugin - camera : ~5 . 0 . 0 } 3 . 0 . 0 : { cordova - ios : > 5 . 0 . 0 } }