You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@cordova.apache.org by st...@apache.org on 2016/05/09 21:18:36 UTC

[1/2] cordova-lib git commit: CB-9858 merging initial fetch work for plugin and platform fetching

Repository: cordova-lib
Updated Branches:
  refs/heads/master b21bcc390 -> 6025a5f23


http://git-wip-us.apache.org/repos/asf/cordova-lib/blob/6025a5f2/cordova-lib/src/cordova/plugin.js
----------------------------------------------------------------------
diff --git a/cordova-lib/src/cordova/plugin.js b/cordova-lib/src/cordova/plugin.js
index fd75458..74f1cca 100644
--- a/cordova-lib/src/cordova/plugin.js
+++ b/cordova-lib/src/cordova/plugin.js
@@ -1,828 +1,832 @@
-/**
-    Licensed to the Apache Software Foundation (ASF) under one
-    or more contributor license agreements.  See the NOTICE file
-    distributed with this work for additional information
-    regarding copyright ownership.  The ASF licenses this file
-    to you under the Apache License, Version 2.0 (the
-    "License"); you may not use this file except in compliance
-    with the License.  You may obtain a copy of the License at
-
-    http://www.apache.org/licenses/LICENSE-2.0
-
-    Unless required by applicable law or agreed to in writing,
-    software distributed under the License is distributed on an
-    "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
-    KIND, either express or implied.  See the License for the
-    specific language governing permissions and limitations
-    under the License.
-*/
-
-var cordova_util  = require('./util'),
-    path          = require('path'),
-    semver        = require('semver'),
-    config        = require('./config'),
-    Q             = require('q'),
-    CordovaError  = require('cordova-common').CordovaError,
-    ConfigParser  = require('cordova-common').ConfigParser,
-    fs            = require('fs'),
-    shell         = require('shelljs'),
-    PluginInfoProvider = require('cordova-common').PluginInfoProvider,
-    plugman       = require('../plugman/plugman'),
-    pluginMapper  = require('cordova-registry-mapper').newToOld,
-    pluginSpec    = require('./plugin_spec_parser'),
-    events        = require('cordova-common').events,
-    metadata      = require('../plugman/util/metadata'),
-    registry      = require('../plugman/registry/registry'),
-    chainMap      = require('../util/promise-util').Q_chainmap,
-    pkgJson       = require('../../package.json'),
-    opener        = require('opener');
-
-// For upper bounds in cordovaDependencies
-var UPPER_BOUND_REGEX = /^<\d+\.\d+\.\d+$/;
-
-// Returns a promise.
-module.exports = function plugin(command, targets, opts) {
-    // CB-10519 wrap function code into promise so throwing error
-    // would result in promise rejection instead of uncaught exception
-    return Q().then(function () {
-        var projectRoot = cordova_util.cdProjectRoot();
-
-        // Dance with all the possible call signatures we've come up over the time. They can be:
-        // 1. plugin() -> list the plugins
-        // 2. plugin(command, Array of targets, maybe opts object)
-        // 3. plugin(command, target1, target2, target3 ... )
-        // The targets are not really targets, they can be a mixture of plugins and options to be passed to plugman.
-
-        command = command || 'ls';
-        targets = targets || [];
-        opts = opts || {};
-        if ( opts.length ) {
-            // This is the case with multiple targets as separate arguments and opts is not opts but another target.
-            targets = Array.prototype.slice.call(arguments, 1);
-            opts = {};
-        }
-        if ( !Array.isArray(targets) ) {
-            // This means we had a single target given as string.
-            targets = [targets];
-        }
-        opts.options = opts.options || [];
-        opts.plugins = [];
-
-        // TODO: Otherwise HooksRunner will be Object instead of function when run from tests - investigate why
-        var HooksRunner = require('../hooks/HooksRunner');
-        var hooksRunner = new HooksRunner(projectRoot);
-        var config_json = config.read(projectRoot);
-        var platformList = cordova_util.listPlatforms(projectRoot);
-
-        // Massage plugin name(s) / path(s)
-        var pluginPath = path.join(projectRoot, 'plugins');
-        var plugins = cordova_util.findPlugins(pluginPath);
-        if (!targets || !targets.length) {
-            if (command == 'add' || command == 'rm') {
-                return Q.reject(new CordovaError('You need to qualify `'+cordova_util.binname+' plugin add` or `'+cordova_util.binname+' plugin remove` with one or more plugins!'));
-            } else {
-                targets = [];
-            }
-        }
-
-        //Split targets between plugins and options
-        //Assume everything after a token with a '-' is an option
-        var i;
-        for (i = 0; i < targets.length; i++) {
-            if (targets[i].match(/^-/)) {
-                opts.options = targets.slice(i);
-                break;
-            } else {
-                opts.plugins.push(targets[i]);
-            }
-        }
-
+/**
+    Licensed to the Apache Software Foundation (ASF) under one
+    or more contributor license agreements.  See the NOTICE file
+    distributed with this work for additional information
+    regarding copyright ownership.  The ASF licenses this file
+    to you under the Apache License, Version 2.0 (the
+    "License"); you may not use this file except in compliance
+    with the License.  You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing,
+    software distributed under the License is distributed on an
+    "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+    KIND, either express or implied.  See the License for the
+    specific language governing permissions and limitations
+    under the License.
+*/
+
+var cordova_util  = require('./util'),
+    path          = require('path'),
+    semver        = require('semver'),
+    config        = require('./config'),
+    Q             = require('q'),
+    CordovaError  = require('cordova-common').CordovaError,
+    ConfigParser  = require('cordova-common').ConfigParser,
+    fs            = require('fs'),
+    shell         = require('shelljs'),
+    PluginInfoProvider = require('cordova-common').PluginInfoProvider,
+    plugman       = require('../plugman/plugman'),
+    pluginMapper  = require('cordova-registry-mapper').newToOld,
+    pluginSpec    = require('./plugin_spec_parser'),
+    events        = require('cordova-common').events,
+    metadata      = require('../plugman/util/metadata'),
+    registry      = require('../plugman/registry/registry'),
+    chainMap      = require('../util/promise-util').Q_chainmap,
+    pkgJson       = require('../../package.json'),
+    opener        = require('opener');
+
+// For upper bounds in cordovaDependencies
+var UPPER_BOUND_REGEX = /^<\d+\.\d+\.\d+$/;
+
+// Returns a promise.
+module.exports = function plugin(command, targets, opts) {
+    // CB-10519 wrap function code into promise so throwing error
+    // would result in promise rejection instead of uncaught exception
+    return Q().then(function () {
+        var projectRoot = cordova_util.cdProjectRoot();
+
+        // Dance with all the possible call signatures we've come up over the time. They can be:
+        // 1. plugin() -> list the plugins
+        // 2. plugin(command, Array of targets, maybe opts object)
+        // 3. plugin(command, target1, target2, target3 ... )
+        // The targets are not really targets, they can be a mixture of plugins and options to be passed to plugman.
+
+        command = command || 'ls';
+        targets = targets || [];
+        opts = opts || {};
+        if ( opts.length ) {
+            // This is the case with multiple targets as separate arguments and opts is not opts but another target.
+            targets = Array.prototype.slice.call(arguments, 1);
+            opts = {};
+        }
+        if ( !Array.isArray(targets) ) {
+            // This means we had a single target given as string.
+            targets = [targets];
+        }
+        opts.options = opts.options || [];
+        opts.plugins = [];
+
+        // TODO: Otherwise HooksRunner will be Object instead of function when run from tests - investigate why
+        var HooksRunner = require('../hooks/HooksRunner');
+        var hooksRunner = new HooksRunner(projectRoot);
+        var config_json = config.read(projectRoot);
+        var platformList = cordova_util.listPlatforms(projectRoot);
+
+        // Massage plugin name(s) / path(s)
+        var pluginPath = path.join(projectRoot, 'plugins');
+        var plugins = cordova_util.findPlugins(pluginPath);
+        if (!targets || !targets.length) {
+            if (command == 'add' || command == 'rm') {
+                return Q.reject(new CordovaError('You need to qualify `'+cordova_util.binname+' plugin add` or `'+cordova_util.binname+' plugin remove` with one or more plugins!'));
+            } else {
+                targets = [];
+            }
+        }
+
+        //Split targets between plugins and options
+        //Assume everything after a token with a '-' is an option
+        var i;
+        for (i = 0; i < targets.length; i++) {
+            if (targets[i].match(/^-/)) {
+                opts.options = targets.slice(i);
+                break;
+            } else {
+                opts.plugins.push(targets[i]);
+            }
+        }
+
         // Assume we don't need to run prepare by default
         var shouldRunPrepare = false;
 
-        switch(command) {
-            case 'add':
-                if (!targets || !targets.length) {
-                    return Q.reject(new CordovaError('No plugin specified. Please specify a plugin to add. See `'+cordova_util.binname+' plugin search`.'));
-                }
-
-                var xml = cordova_util.projectConfig(projectRoot);
-                var cfg = new ConfigParser(xml);
-                var searchPath = config_json.plugin_search_path || [];
-                if (typeof opts.searchpath == 'string') {
-                    searchPath = opts.searchpath.split(path.delimiter).concat(searchPath);
-                } else if (opts.searchpath) {
-                    searchPath = opts.searchpath.concat(searchPath);
-                }
-                // Blank it out to appease unit tests.
-                if (searchPath.length === 0) {
-                    searchPath = undefined;
-                }
-
-                opts.cordova = { plugins: cordova_util.findPlugins(pluginPath) };
-                return hooksRunner.fire('before_plugin_add', opts)
-                .then(function() {
-                    var pluginInfoProvider = new PluginInfoProvider();
-                    return opts.plugins.reduce(function(soFar, target) {
-                        return soFar.then(function() {
-                            if (target[target.length - 1] == path.sep) {
-                                target = target.substring(0, target.length - 1);
-                            }
-
-                            // Fetch the plugin first.
-                            var fetchOptions = {
-                                searchpath: searchPath,
-                                noregistry: opts.noregistry,
-                                nohooks: opts.nohooks,
-                                link: opts.link,
-                                pluginInfoProvider: pluginInfoProvider,
-                                variables: opts.cli_variables,
-                                is_top_level: true
-                            };
-
-                            return determinePluginTarget(projectRoot, cfg, target, fetchOptions)
-                            .then(function(resolvedTarget) {
-                                target = resolvedTarget;
-                                events.emit('verbose', 'Calling plugman.fetch on plugin "' + target + '"');
-                                return plugman.raw.fetch(target, pluginPath, fetchOptions);
-                            })
-                            .then(function (directory) {
-                                return pluginInfoProvider.get(directory);
-                            });
-                        })
-                        .then(function(pluginInfo) {
-                            // Validate top-level required variables
-                            var pluginVariables = pluginInfo.getPreferences();
-                            var missingVariables = Object.keys(pluginVariables)
-                            .filter(function (variableName) {
-                                // discard variables with default value
-                                return !(pluginVariables[variableName] || opts.cli_variables[variableName]);
-                            });
-
-                            if (missingVariables.length) {
-                                events.emit('verbose', 'Removing ' + pluginInfo.dir + ' due to installation failure');
-                                shell.rm('-rf', pluginInfo.dir);
-                                var msg = 'Variable(s) missing (use: --variable ' + missingVariables.join('=value --variable ') + '=value).';
-                                return Q.reject(new CordovaError(msg));
-                            }
-
-                            // Iterate (in serial!) over all platforms in the project and install the plugin.
-                            return chainMap(platformList, function (platform) {
-                                var platformRoot = path.join(projectRoot, 'platforms', platform),
-                                options = {
-                                    cli_variables: opts.cli_variables || {},
-                                    browserify: opts.browserify || false,
-                                    searchpath: searchPath,
-                                    noregistry: opts.noregistry,
-                                    link: opts.link,
-                                    pluginInfoProvider: pluginInfoProvider,
-                                    // Set up platform to install asset files/js modules to <platform>/platform_www dir
-                                    // instead of <platform>/www. This is required since on each prepare platform's www dir is changed
-                                    // and files from 'platform_www' merged into 'www'. Thus we need to persist these
-                                    // files platform_www directory, so they'll be applied to www on each prepare.
-                                    usePlatformWww: true,
-                                    nohooks: opts.nohooks,
-                                    force: opts.force
-                                };
-
-                                events.emit('verbose', 'Calling plugman.install on plugin "' + pluginInfo.dir + '" for platform "' + platform);
+        switch(command) {
+            case 'add':
+                if (!targets || !targets.length) {
+                    return Q.reject(new CordovaError('No plugin specified. Please specify a plugin to add. See `'+cordova_util.binname+' plugin search`.'));
+                }
+
+                var xml = cordova_util.projectConfig(projectRoot);
+                var cfg = new ConfigParser(xml);
+                var searchPath = config_json.plugin_search_path || [];
+                if (typeof opts.searchpath == 'string') {
+                    searchPath = opts.searchpath.split(path.delimiter).concat(searchPath);
+                } else if (opts.searchpath) {
+                    searchPath = opts.searchpath.concat(searchPath);
+                }
+                // Blank it out to appease unit tests.
+                if (searchPath.length === 0) {
+                    searchPath = undefined;
+                }
+
+                opts.cordova = { plugins: cordova_util.findPlugins(pluginPath) };
+                return hooksRunner.fire('before_plugin_add', opts)
+                .then(function() {
+                    var pluginInfoProvider = new PluginInfoProvider();
+                    return opts.plugins.reduce(function(soFar, target) {
+                        return soFar.then(function() {
+                            if (target[target.length - 1] == path.sep) {
+                                target = target.substring(0, target.length - 1);
+                            }
+
+                            // Fetch the plugin first.
+                            var fetchOptions = {
+                                searchpath: searchPath,
+                                noregistry: opts.noregistry,
+                                fetch: opts.fetch || false,
+                                save: opts.save,
+                                nohooks: opts.nohooks,
+                                link: opts.link,
+                                pluginInfoProvider: pluginInfoProvider,
+                                variables: opts.cli_variables,
+                                is_top_level: true
+                            };
+
+                            return determinePluginTarget(projectRoot, cfg, target, fetchOptions)
+                            .then(function(resolvedTarget) {
+                                target = resolvedTarget;
+                                events.emit('verbose', 'Calling plugman.fetch on plugin "' + target + '"');
+                                return plugman.raw.fetch(target, pluginPath, fetchOptions);
+                            })
+                            .then(function (directory) {
+                                return pluginInfoProvider.get(directory);
+                            });
+                        })
+                        .then(function(pluginInfo) {
+                            // Validate top-level required variables
+                            var pluginVariables = pluginInfo.getPreferences();
+                            var missingVariables = Object.keys(pluginVariables)
+                            .filter(function (variableName) {
+                                // discard variables with default value
+                                return !(pluginVariables[variableName] || opts.cli_variables[variableName]);
+                            });
+
+                            if (missingVariables.length) {
+                                events.emit('verbose', 'Removing ' + pluginInfo.dir + ' due to installation failure');
+                                shell.rm('-rf', pluginInfo.dir);
+                                var msg = 'Variable(s) missing (use: --variable ' + missingVariables.join('=value --variable ') + '=value).';
+                                return Q.reject(new CordovaError(msg));
+                            }
+
+                            // Iterate (in serial!) over all platforms in the project and install the plugin.
+                            return chainMap(platformList, function (platform) {
+                                var platformRoot = path.join(projectRoot, 'platforms', platform),
+                                options = {
+                                    cli_variables: opts.cli_variables || {},
+                                    browserify: opts.browserify || false,
+                                    fetch: opts.fetch || false,
+                                    save: opts.save,
+                                    searchpath: searchPath,
+                                    noregistry: opts.noregistry,
+                                    link: opts.link,
+                                    pluginInfoProvider: pluginInfoProvider,
+                                    // Set up platform to install asset files/js modules to <platform>/platform_www dir
+                                    // instead of <platform>/www. This is required since on each prepare platform's www dir is changed
+                                    // and files from 'platform_www' merged into 'www'. Thus we need to persist these
+                                    // files platform_www directory, so they'll be applied to www on each prepare.
+                                    usePlatformWww: true,
+                                    nohooks: opts.nohooks,
+                                    force: opts.force
+                                };
+
+                                events.emit('verbose', 'Calling plugman.install on plugin "' + pluginInfo.dir + '" for platform "' + platform);
                                 return plugman.raw.install(platform, platformRoot, path.basename(pluginInfo.dir), pluginPath, options)
                                 .then(function (didPrepare) {
                                     // If platform does not returned anything we'll need
                                     // to trigger a prepare after all plugins installed
                                     if (!didPrepare) shouldRunPrepare = true;
                                 });
-                            })
-                            .thenResolve(pluginInfo);
-                        })
-                        .then(function(pluginInfo){
-                            // save to config.xml
-                            if(saveToConfigXmlOn(config_json, opts)){
-                                var src = parseSource(target, opts);
-                                var attributes = {
-                                    name: pluginInfo.id
-                                };
-
-                                if (src) {
-                                    attributes.spec = src;
-                                } else {
-                                    var ver = '~' + pluginInfo.version;
-                                    // Scoped packages need to have the package-spec along with the version
-                                    var parsedSpec = pluginSpec.parse(target);
-                                    if (parsedSpec.scope) {
-                                        attributes.spec = parsedSpec.package + '@' + ver;
-                                    } else {
-                                        attributes.spec = ver;
-                                    }
-                                }
-
-                                xml = cordova_util.projectConfig(projectRoot);
-                                cfg = new ConfigParser(xml);
-                                cfg.removePlugin(pluginInfo.id);
-                                cfg.addPlugin(attributes, opts.cli_variables);
-                                cfg.write();
-
-                                events.emit('results', 'Saved plugin info for "' + pluginInfo.id + '" to config.xml');
-                            }
-                        });
+                            })
+                            .thenResolve(pluginInfo);
+                        })
+                        .then(function(pluginInfo){
+                            // save to config.xml
+                            if(saveToConfigXmlOn(config_json, opts)){
+                                var src = parseSource(target, opts);
+                                var attributes = {
+                                    name: pluginInfo.id
+                                };
+
+                                if (src) {
+                                    attributes.spec = src;
+                                } else {
+                                    var ver = '~' + pluginInfo.version;
+                                    // Scoped packages need to have the package-spec along with the version
+                                    var parsedSpec = pluginSpec.parse(target);
+                                    if (parsedSpec.scope) {
+                                        attributes.spec = parsedSpec.package + '@' + ver;
+                                    } else {
+                                        attributes.spec = ver;
+                                    }
+                                }
+
+                                xml = cordova_util.projectConfig(projectRoot);
+                                cfg = new ConfigParser(xml);
+                                cfg.removePlugin(pluginInfo.id);
+                                cfg.addPlugin(attributes, opts.cli_variables);
+                                cfg.write();
+
+                                events.emit('results', 'Saved plugin info for "' + pluginInfo.id + '" to config.xml');
+                            }
+                        });
                     }, Q());
-                }).then(function() {
+                }).then(function() {
                     // CB-11022 We do not need to run prepare after plugin install until shouldRunPrepare flag is set to true
                     if (!shouldRunPrepare) {
                         return Q();
                     }
 
-                    // Need to require right here instead of doing this at the beginning of file
-                    // otherwise tests are failing without any real reason.
-                    return require('./prepare').preparePlatforms(platformList, projectRoot, opts);
-                }).then(function() {
-                    opts.cordova = { plugins: cordova_util.findPlugins(pluginPath) };
-                    return hooksRunner.fire('after_plugin_add', opts);
-                });
-            case 'rm':
-            case 'remove':
-                if (!targets || !targets.length) {
-                    return Q.reject(new CordovaError('No plugin specified. Please specify a plugin to remove. See `'+cordova_util.binname+' plugin list`.'));
-                }
-
-                opts.cordova = { plugins: cordova_util.findPlugins(pluginPath) };
-                return hooksRunner.fire('before_plugin_rm', opts)
-                .then(function() {
-                    return opts.plugins.reduce(function(soFar, target) {
-                        var validatedPluginId = validatePluginId(target, plugins);
-                        if (!validatedPluginId) {
-                            return Q.reject(new CordovaError('Plugin "' + target + '" is not present in the project. See `' + cordova_util.binname + ' plugin list`.'));
-                        }
-                        target = validatedPluginId;
-
-                        // Iterate over all installed platforms and uninstall.
-                        // If this is a web-only or dependency-only plugin, then
-                        // there may be nothing to do here except remove the
-                        // reference from the platform's plugin config JSON.
-                        return platformList.reduce(function(soFar, platform) {
-                            return soFar.then(function() {
-                                var platformRoot = path.join(projectRoot, 'platforms', platform);
-                                events.emit('verbose', 'Calling plugman.uninstall on plugin "' + target + '" for platform "' + platform + '"');
+                    // Need to require right here instead of doing this at the beginning of file
+                    // otherwise tests are failing without any real reason.
+                    return require('./prepare').preparePlatforms(platformList, projectRoot, opts);
+                }).then(function() {
+                    opts.cordova = { plugins: cordova_util.findPlugins(pluginPath) };
+                    return hooksRunner.fire('after_plugin_add', opts);
+                });
+            case 'rm':
+            case 'remove':
+                if (!targets || !targets.length) {
+                    return Q.reject(new CordovaError('No plugin specified. Please specify a plugin to remove. See `'+cordova_util.binname+' plugin list`.'));
+                }
+
+                opts.cordova = { plugins: cordova_util.findPlugins(pluginPath) };
+                return hooksRunner.fire('before_plugin_rm', opts)
+                .then(function() {
+                    return opts.plugins.reduce(function(soFar, target) {
+                        var validatedPluginId = validatePluginId(target, plugins);
+                        if (!validatedPluginId) {
+                            return Q.reject(new CordovaError('Plugin "' + target + '" is not present in the project. See `' + cordova_util.binname + ' plugin list`.'));
+                        }
+                        target = validatedPluginId;
+
+                        // Iterate over all installed platforms and uninstall.
+                        // If this is a web-only or dependency-only plugin, then
+                        // there may be nothing to do here except remove the
+                        // reference from the platform's plugin config JSON.
+                        return platformList.reduce(function(soFar, platform) {
+                            return soFar.then(function() {
+                                var platformRoot = path.join(projectRoot, 'platforms', platform);
+                                events.emit('verbose', 'Calling plugman.uninstall on plugin "' + target + '" for platform "' + platform + '"');
                                 return plugman.raw.uninstall.uninstallPlatform(platform, platformRoot, target, pluginPath)
                                 .then(function (didPrepare) {
                                     // If platform does not returned anything we'll need
                                     // to trigger a prepare after all plugins installed
                                     if (!didPrepare) shouldRunPrepare = true;
                                 });
-                            });
-                        }, Q())
-                        .then(function() {
-                            // TODO: Should only uninstallPlugin when no platforms have it.
-                            return plugman.raw.uninstall.uninstallPlugin(target, pluginPath);
-                        }).then(function(){
-                            //remove plugin from config.xml
-                            if(saveToConfigXmlOn(config_json, opts)){
-                                var configPath = cordova_util.projectConfig(projectRoot);
-                                if(fs.existsSync(configPath)){//should not happen with real life but needed for tests
-                                    var configXml = new ConfigParser(configPath);
-                                    configXml.removePlugin(target);
-                                    configXml.write();
-                                    events.emit('results', 'config.xml entry for ' +target+ ' is removed');
-                                }
-                            }
-                        })
-                        .then(function(){
-                            // Remove plugin from fetch.json
-                            events.emit('verbose', 'Removing plugin ' + target + ' from fetch.json');
-                            metadata.remove_fetch_metadata(pluginPath, target);
-                        });
-                    }, Q());
-                }).then(function () {
+                            });
+                        }, Q())
+                        .then(function() {
+                            // TODO: Should only uninstallPlugin when no platforms have it.
+                            return plugman.raw.uninstall.uninstallPlugin(target, pluginPath, opts);
+                        }).then(function(){
+                            //remove plugin from config.xml
+                            if(saveToConfigXmlOn(config_json, opts)){
+                                var configPath = cordova_util.projectConfig(projectRoot);
+                                if(fs.existsSync(configPath)){//should not happen with real life but needed for tests
+                                    var configXml = new ConfigParser(configPath);
+                                    configXml.removePlugin(target);
+                                    configXml.write();
+                                    events.emit('results', 'config.xml entry for ' +target+ ' is removed');
+                                }
+                            }
+                        })
+                        .then(function(){
+                            // Remove plugin from fetch.json
+                            events.emit('verbose', 'Removing plugin ' + target + ' from fetch.json');
+                            metadata.remove_fetch_metadata(pluginPath, target);
+                        });
+                    }, Q());
+                }).then(function () {
                     // CB-11022 We do not need to run prepare after plugin install until shouldRunPrepare flag is set to true
                     if (!shouldRunPrepare) {
                         return Q();
                     }
 
-                    return require('./prepare').preparePlatforms(platformList, projectRoot, opts);
-                }).then(function() {
-                    opts.cordova = { plugins: cordova_util.findPlugins(pluginPath) };
-                    return hooksRunner.fire('after_plugin_rm', opts);
-                });
-            case 'search':
-                return hooksRunner.fire('before_plugin_search', opts)
-                .then(function() {
-                    var link = 'http://cordova.apache.org/plugins/';
-                    if (opts.plugins.length > 0) {
-                        var keywords = (opts.plugins).join(' ');
-                        var query = link + '?q=' + encodeURI(keywords);
-                        opener(query);
-                    }
-                    else {
-                        opener(link);
-                    }
-
-                    return Q.resolve();
-                }).then(function() {
-                    return hooksRunner.fire('after_plugin_search', opts);
-                });
-            case 'save':
-                // save the versions/folders/git-urls of currently installed plugins into config.xml
-                return save(projectRoot, opts);
-            default:
-                return list(projectRoot, hooksRunner);
-        }
-    });
-};
-
-function determinePluginTarget(projectRoot, cfg, target, fetchOptions) {
-    var parsedSpec = pluginSpec.parse(target);
-
-    var id = parsedSpec.package || target;
-
-    // CB-10975 We need to resolve relative path to plugin dir from app's root before checking whether if it exists
-    var maybeDir = cordova_util.fixRelativePath(id);
-    if (parsedSpec.version || cordova_util.isUrl(id) || cordova_util.isDirectory(maybeDir)) {
-        return Q(target);
-    }
-
-    // If no version is specified, retrieve the version (or source) from config.xml
-    events.emit('verbose', 'No version specified, retrieving version from config.xml');
-    var ver = getVersionFromConfigFile(id, cfg);
-
-    if (cordova_util.isUrl(ver) || cordova_util.isDirectory(ver) || pluginSpec.parse(ver).scope) {
-        return Q(ver);
-    }
-
-    // If version exists in config.xml, use that
-    if (ver) {
-        return Q(id + '@' + ver);
-    }
-
-    // If no version is given at all and we are fetching from npm, we
-    // can attempt to use the Cordova dependencies the plugin lists in
-    // their package.json
-    var shouldUseNpmInfo = !fetchOptions.searchpath && !fetchOptions.noregistry;
-
-    if(shouldUseNpmInfo) {
-        events.emit('verbose', 'No version given in config.xml, attempting to use plugin engine info');
-    }
-
-    return (shouldUseNpmInfo ? registry.info([id]) : Q({}))
-    .then(function(pluginInfo) {
-        return getFetchVersion(projectRoot, pluginInfo, pkgJson.version);
-    })
-    .then(function(fetchVersion) {
-        return fetchVersion ? (id + '@' + fetchVersion) : target;
-    });
-}
-
-// Exporting for testing purposes
-module.exports.getFetchVersion = getFetchVersion;
-
-function validatePluginId(pluginId, installedPlugins) {
-    if (installedPlugins.indexOf(pluginId) >= 0) {
-        return pluginId;
-    }
-
-    var oldStylePluginId = pluginMapper[pluginId];
-    if (oldStylePluginId) {
-        events.emit('log', 'Plugin "' + pluginId + '" is not present in the project. Converting value to "' + oldStylePluginId + '" and trying again.');
-        return installedPlugins.indexOf(oldStylePluginId) >= 0 ? oldStylePluginId : null;
-    }
-
-    if (pluginId.indexOf('cordova-plugin-') < 0) {
-        return validatePluginId('cordova-plugin-' + pluginId, installedPlugins);
-    }
-}
-
-function save(projectRoot, opts){
-    var xml = cordova_util.projectConfig(projectRoot);
-    var cfg = new ConfigParser(xml);
-
-    // First, remove all pre-existing plugins from config.xml
-    cfg.getPluginIdList().forEach(function(plugin){
-        cfg.removePlugin(plugin);
-    });
-
-    // Then, save top-level plugins and their sources
-    var jsonFile = path.join(projectRoot, 'plugins', 'fetch.json');
-    var plugins;
-    try {
-        // It might be the case that fetch.json file is not yet existent.
-        // for example: when we have never ran the command 'cordova plugin add foo' on the project
-        // in that case, there's nothing to do except bubble up the error
-        plugins = JSON.parse(fs.readFileSync(jsonFile, 'utf-8'));
-    } catch (err) {
-        return Q.reject(err.message);
-    }
-
-    Object.keys(plugins).forEach(function(pluginName){
-        var plugin = plugins[pluginName];
-        var pluginSource = plugin.source;
-
-        // If not a top-level plugin, skip it, don't save it to config.xml
-        if(!plugin.is_top_level){
-            return;
-        }
-
-        var attribs = {name: pluginName};
-        var spec = getSpec(pluginSource, projectRoot, pluginName);
-        if (spec) {
-            attribs.spec = spec;
-        }
-
-        var variables = getPluginVariables(plugin.variables);
-        cfg.addPlugin(attribs, variables);
-    });
-    cfg.write();
-
-    return Q.resolve();
-}
-
-function getPluginVariables(variables){
-    var result = [];
-    if(!variables){
-        return result;
-    }
-
-    Object.keys(variables).forEach(function(pluginVar){
-        result.push({name: pluginVar, value: variables[pluginVar]});
-    });
-
-    return result;
-}
-
-function getVersionFromConfigFile(plugin, cfg){
-    var parsedSpec = pluginSpec.parse(plugin);
-    var pluginEntry = cfg.getPlugin(parsedSpec.id);
-    if (!pluginEntry && !parsedSpec.scope) {
-        // If the provided plugin id is in the new format (e.g. cordova-plugin-camera), it might be stored in config.xml
-        // under the old format (e.g. org.apache.cordova.camera), so check for that.
-        var oldStylePluginId = pluginMapper[parsedSpec.id];
-        if (oldStylePluginId) {
-            pluginEntry = cfg.getPlugin(oldStylePluginId);
-        }
-    }
-    return pluginEntry && pluginEntry.spec;
-}
-
-function list(projectRoot, hooksRunner, opts) {
-    var pluginsList = [];
-    return hooksRunner.fire('before_plugin_ls', opts)
-    .then(function() {
-        return getInstalledPlugins(projectRoot);
-    })
-    .then(function(plugins) {
-        if (plugins.length === 0) {
-            events.emit('results', 'No plugins added. Use `'+cordova_util.binname+' plugin add <plugin>`.');
-            return;
-        }
-        var pluginsDict = {};
-        var lines = [];
-        var txt, p;
-        for (var i=0; i<plugins.length; i++) {
-            p = plugins[i];
-            pluginsDict[p.id] = p;
-            pluginsList.push(p.id);
-            txt = p.id + ' ' + p.version + ' "' + (p.name || p.description) + '"';
-            lines.push(txt);
-        }
-        // Add warnings for deps with wrong versions.
-        for (var id in pluginsDict) {
-            p = pluginsDict[id];
-            for (var depId in p.deps) {
-                var dep = pluginsDict[depId];
-                //events.emit('results', p.deps[depId].version);
-                //events.emit('results', dep != null);
-                if (!dep) {
-                    txt = 'WARNING, missing dependency: plugin ' + id +
-                          ' depends on ' + depId +
-                          ' but it is not installed';
-                    lines.push(txt);
-                } else if (!semver.satisfies(dep.version, p.deps[depId].version)) {
-                    txt = 'WARNING, broken dependency: plugin ' + id +
-                          ' depends on ' + depId + ' ' + p.deps[depId].version +
-                          ' but installed version is ' + dep.version;
-                    lines.push(txt);
-                }
-            }
-        }
-        events.emit('results', lines.join('\n'));
-    })
-    .then(function() {
-        return hooksRunner.fire('after_plugin_ls', opts);
-    })
-    .then(function() {
-        return pluginsList;
-    });
-}
-
-function getInstalledPlugins(projectRoot) {
-    var pluginsDir = path.join(projectRoot, 'plugins');
-    // TODO: This should list based off of platform.json, not directories within plugins/
-    var pluginInfoProvider = new PluginInfoProvider();
-    return pluginInfoProvider.getAllWithinSearchPath(pluginsDir);
-}
-
-function saveToConfigXmlOn(config_json, options){
-    options = options || {};
-    var autosave =  config_json.auto_save_plugins || false;
-    return autosave || options.save;
-}
-
-function parseSource(target, opts) {
-    var url = require('url');
-    var uri = url.parse(target);
-    if (uri.protocol && uri.protocol != 'file:' && uri.protocol[1] != ':' && !target.match(/^\w+:\\/)) {
-        return target;
-    } else {
-        var plugin_dir = cordova_util.fixRelativePath(path.join(target, (opts.subdir || '.')));
-        if (fs.existsSync(plugin_dir)) {
-            return target;
-        }
-    }
-    return null;
-}
-
-function getSpec(pluginSource, projectRoot, pluginName) {
-    if (pluginSource.hasOwnProperty('url') || pluginSource.hasOwnProperty('path')) {
-        return pluginSource.url || pluginSource.path;
-    }
-
-    var version = null;
-    var scopedPackage = null;
-    if (pluginSource.hasOwnProperty('id')) {
-        // Note that currently version is only saved here if it was explicitly specified when the plugin was added.
-        var parsedSpec = pluginSpec.parse(pluginSource.id);
-        version = parsedSpec.version;
-        if (version) {
-            version = versionString(version);
-        }
-
-        if (parsedSpec.scope) {
-            scopedPackage = parsedSpec.package;
-        }
-    }
-
-    if (!version) {
-        // Fallback on getting version from the plugin folder, if it's there
-        var pluginInfoProvider = new PluginInfoProvider();
-        var dir = path.join(projectRoot, 'plugins', pluginName);
-
-        try {
-            // pluginInfoProvider.get() will throw if directory does not exist.
-            var pluginInfo = pluginInfoProvider.get(dir);
-            if (pluginInfo) {
-                version = versionString(pluginInfo.version);
-            }
-        } catch (err) {
-        }
-    }
-
-    if (scopedPackage) {
-        version = scopedPackage + '@' + version;
-    }
-
-    return version;
-}
-
-function versionString(version) {
-    var validVersion = semver.valid(version, true);
-    if (validVersion) {
-        return '~' + validVersion;
-    }
-
-    if (semver.validRange(version, true)) {
-        // Return what we were passed rather than the result of the validRange() call, as that call makes modifications
-        // we don't want, like converting '^1.2.3' to '>=1.2.3-0 <2.0.0-0'
-        return version;
-    }
-
-    return null;
-}
-
-/**
- * Gets the version of a plugin that should be fetched for a given project based
- * on the plugin's engine information from NPM and the platforms/plugins installed
- * in the project. The cordovaDependencies object in the package.json's engines
- * entry takes the form of an object that maps plugin versions to a series of
- * constraints and semver ranges. For example:
- *
- *     { plugin-version: { constraint: semver-range, ...}, ...}
- *
- * Constraint can be a plugin, platform, or cordova version. Plugin-version
- * can be either a single version (e.g. 3.0.0) or an upper bound (e.g. <3.0.0)
- *
- * @param {string}  projectRoot     The path to the root directory of the project
- * @param {object}  pluginInfo      The NPM info of the plugin to be fetched (e.g. the
- *                                  result of calling `registry.info()`)
- * @param {string}  cordovaVersion  The semver version of cordova-lib
- *
- * @return {Promise}                A promise that will resolve to either a string
- *                                  if there is a version of the plugin that this
- *                                  project satisfies or null if there is not
- */
-function getFetchVersion(projectRoot, pluginInfo, cordovaVersion) {
-    // Figure out the project requirements
-    if (pluginInfo.engines && pluginInfo.engines.cordovaDependencies) {
-        var pluginList = getInstalledPlugins(projectRoot);
-        var pluginMap = {};
-
-        pluginList.forEach(function(plugin) {
-            pluginMap[plugin.id] = plugin.version;
-        });
-
-        return cordova_util.getInstalledPlatformsWithVersions(projectRoot)
-        .then(function(platformVersions) {
-            return determinePluginVersionToFetch(
-                pluginInfo,
-                pluginMap,
-                platformVersions,
-                cordovaVersion);
-        });
-    } else {
-        // If we have no engine, we want to fall back to the default behavior
-        events.emit('verbose', 'No plugin engine info found or not using registry, falling back to latest version');
-        return Q(null);
-    }
-}
-
-function findVersion(versions, version) {
-    var cleanedVersion = semver.clean(version);
-    for(var i = 0; i < versions.length; i++) {
-        if(semver.clean(versions[i]) === cleanedVersion) {
-            return versions[i];
-        }
-    }
-    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' }
- *  }
- *
- * See cordova-spec/plugin_fetch.spec.js for test cases and examples
- */
-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;
-
-    for(var version in engine) {
-        if(semver.valid(semver.clean(version)) && !semver.gt(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(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(!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 && !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 ? 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 && 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 = getFailedRequirements(engine[versions[0]], pluginMap, platformMap, cordovaVersion);
-
-                // Warn the user that we are not fetching latest
-                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 ? 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 = 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);
-        });
-    }
-
-    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 getFailedRequirements(reqs, pluginMap, platformMap, cordovaVersion) {
-    var failed = [];
-
-    for (var req in reqs) {
-        if(reqs.hasOwnProperty(req) && typeof req === 'string' && semver.validRange(reqs[req])) {
-            var badInstalledVersion = null;
-            var trimmedReq = req.trim();
-
-            if(pluginMap[trimmedReq] && !semver.satisfies(pluginMap[trimmedReq], reqs[req])) {
-                badInstalledVersion = pluginMap[req];
-            } else if(trimmedReq === 'cordova' && !semver.satisfies(cordovaVersion, reqs[req])) {
-                badInstalledVersion = cordovaVersion;
-            } else if(trimmedReq.indexOf('cordova-') === 0) {
-                // Might be a platform constraint
-                var platform = trimmedReq.substring(8);
-                if(platformMap[platform] && !semver.satisfies(platformMap[platform], reqs[req])) {
-                    badInstalledVersion = platformMap[platform];
-                }
-            }
-
-            if(badInstalledVersion) {
-                failed.push({
-                    dependency: trimmedReq,
-                    installed: badInstalledVersion.trim(),
-                    required: reqs[req].trim()
-                });
-            }
-        } else {
-            events.emit('verbose', 'Ignoring invalid plugin dependency constraint ' + req + ':' + reqs[req]);
-        }
-    }
-
-    return failed;
-}
-
-function listUnmetRequirements(name, failedRequirements) {
-    events.emit('warn', 'Unmet project requirements for latest version of ' + name + ':');
-
-    failedRequirements.forEach(function(req) {
-        events.emit('warn', '    ' + req.dependency + ' (' + req.installed + ' installed, ' + req.required + ' required)');
-    });
-}
+                    return require('./prepare').preparePlatforms(platformList, projectRoot, opts);
+                }).then(function() {
+                    opts.cordova = { plugins: cordova_util.findPlugins(pluginPath) };
+                    return hooksRunner.fire('after_plugin_rm', opts);
+                });
+            case 'search':
+                return hooksRunner.fire('before_plugin_search', opts)
+                .then(function() {
+                    var link = 'http://cordova.apache.org/plugins/';
+                    if (opts.plugins.length > 0) {
+                        var keywords = (opts.plugins).join(' ');
+                        var query = link + '?q=' + encodeURI(keywords);
+                        opener(query);
+                    }
+                    else {
+                        opener(link);
+                    }
+
+                    return Q.resolve();
+                }).then(function() {
+                    return hooksRunner.fire('after_plugin_search', opts);
+                });
+            case 'save':
+                // save the versions/folders/git-urls of currently installed plugins into config.xml
+                return save(projectRoot, opts);
+            default:
+                return list(projectRoot, hooksRunner);
+        }
+    });
+};
+
+function determinePluginTarget(projectRoot, cfg, target, fetchOptions) {
+    var parsedSpec = pluginSpec.parse(target);
+
+    var id = parsedSpec.package || target;
+
+    // CB-10975 We need to resolve relative path to plugin dir from app's root before checking whether if it exists
+    var maybeDir = cordova_util.fixRelativePath(id);
+    if (parsedSpec.version || cordova_util.isUrl(id) || cordova_util.isDirectory(maybeDir)) {
+        return Q(target);
+    }
+
+    // If no version is specified, retrieve the version (or source) from config.xml
+    events.emit('verbose', 'No version specified, retrieving version from config.xml');
+    var ver = getVersionFromConfigFile(id, cfg);
+
+    if (cordova_util.isUrl(ver) || cordova_util.isDirectory(ver) || pluginSpec.parse(ver).scope) {
+        return Q(ver);
+    }
+
+    // If version exists in config.xml, use that
+    if (ver) {
+        return Q(id + '@' + ver);
+    }
+
+    // If no version is given at all and we are fetching from npm, we
+    // can attempt to use the Cordova dependencies the plugin lists in
+    // their package.json
+    var shouldUseNpmInfo = !fetchOptions.searchpath && !fetchOptions.noregistry;
+
+    if(shouldUseNpmInfo) {
+        events.emit('verbose', 'No version given in config.xml, attempting to use plugin engine info');
+    }
+
+    return (shouldUseNpmInfo ? registry.info([id]) : Q({}))
+    .then(function(pluginInfo) {
+        return getFetchVersion(projectRoot, pluginInfo, pkgJson.version);
+    })
+    .then(function(fetchVersion) {
+        return fetchVersion ? (id + '@' + fetchVersion) : target;
+    });
+}
+
+// Exporting for testing purposes
+module.exports.getFetchVersion = getFetchVersion;
+
+function validatePluginId(pluginId, installedPlugins) {
+    if (installedPlugins.indexOf(pluginId) >= 0) {
+        return pluginId;
+    }
+
+    var oldStylePluginId = pluginMapper[pluginId];
+    if (oldStylePluginId) {
+        events.emit('log', 'Plugin "' + pluginId + '" is not present in the project. Converting value to "' + oldStylePluginId + '" and trying again.');
+        return installedPlugins.indexOf(oldStylePluginId) >= 0 ? oldStylePluginId : null;
+    }
+
+    if (pluginId.indexOf('cordova-plugin-') < 0) {
+        return validatePluginId('cordova-plugin-' + pluginId, installedPlugins);
+    }
+}
+
+function save(projectRoot, opts){
+    var xml = cordova_util.projectConfig(projectRoot);
+    var cfg = new ConfigParser(xml);
+
+    // First, remove all pre-existing plugins from config.xml
+    cfg.getPluginIdList().forEach(function(plugin){
+        cfg.removePlugin(plugin);
+    });
+
+    // Then, save top-level plugins and their sources
+    var jsonFile = path.join(projectRoot, 'plugins', 'fetch.json');
+    var plugins;
+    try {
+        // It might be the case that fetch.json file is not yet existent.
+        // for example: when we have never ran the command 'cordova plugin add foo' on the project
+        // in that case, there's nothing to do except bubble up the error
+        plugins = JSON.parse(fs.readFileSync(jsonFile, 'utf-8'));
+    } catch (err) {
+        return Q.reject(err.message);
+    }
+
+    Object.keys(plugins).forEach(function(pluginName){
+        var plugin = plugins[pluginName];
+        var pluginSource = plugin.source;
+
+        // If not a top-level plugin, skip it, don't save it to config.xml
+        if(!plugin.is_top_level){
+            return;
+        }
+
+        var attribs = {name: pluginName};
+        var spec = getSpec(pluginSource, projectRoot, pluginName);
+        if (spec) {
+            attribs.spec = spec;
+        }
+
+        var variables = getPluginVariables(plugin.variables);
+        cfg.addPlugin(attribs, variables);
+    });
+    cfg.write();
+
+    return Q.resolve();
+}
+
+function getPluginVariables(variables){
+    var result = [];
+    if(!variables){
+        return result;
+    }
+
+    Object.keys(variables).forEach(function(pluginVar){
+        result.push({name: pluginVar, value: variables[pluginVar]});
+    });
+
+    return result;
+}
+
+function getVersionFromConfigFile(plugin, cfg){
+    var parsedSpec = pluginSpec.parse(plugin);
+    var pluginEntry = cfg.getPlugin(parsedSpec.id);
+    if (!pluginEntry && !parsedSpec.scope) {
+        // If the provided plugin id is in the new format (e.g. cordova-plugin-camera), it might be stored in config.xml
+        // under the old format (e.g. org.apache.cordova.camera), so check for that.
+        var oldStylePluginId = pluginMapper[parsedSpec.id];
+        if (oldStylePluginId) {
+            pluginEntry = cfg.getPlugin(oldStylePluginId);
+        }
+    }
+    return pluginEntry && pluginEntry.spec;
+}
+
+function list(projectRoot, hooksRunner, opts) {
+    var pluginsList = [];
+    return hooksRunner.fire('before_plugin_ls', opts)
+    .then(function() {
+        return getInstalledPlugins(projectRoot);
+    })
+    .then(function(plugins) {
+        if (plugins.length === 0) {
+            events.emit('results', 'No plugins added. Use `'+cordova_util.binname+' plugin add <plugin>`.');
+            return;
+        }
+        var pluginsDict = {};
+        var lines = [];
+        var txt, p;
+        for (var i=0; i<plugins.length; i++) {
+            p = plugins[i];
+            pluginsDict[p.id] = p;
+            pluginsList.push(p.id);
+            txt = p.id + ' ' + p.version + ' "' + (p.name || p.description) + '"';
+            lines.push(txt);
+        }
+        // Add warnings for deps with wrong versions.
+        for (var id in pluginsDict) {
+            p = pluginsDict[id];
+            for (var depId in p.deps) {
+                var dep = pluginsDict[depId];
+                //events.emit('results', p.deps[depId].version);
+                //events.emit('results', dep != null);
+                if (!dep) {
+                    txt = 'WARNING, missing dependency: plugin ' + id +
+                          ' depends on ' + depId +
+                          ' but it is not installed';
+                    lines.push(txt);
+                } else if (!semver.satisfies(dep.version, p.deps[depId].version)) {
+                    txt = 'WARNING, broken dependency: plugin ' + id +
+                          ' depends on ' + depId + ' ' + p.deps[depId].version +
+                          ' but installed version is ' + dep.version;
+                    lines.push(txt);
+                }
+            }
+        }
+        events.emit('results', lines.join('\n'));
+    })
+    .then(function() {
+        return hooksRunner.fire('after_plugin_ls', opts);
+    })
+    .then(function() {
+        return pluginsList;
+    });
+}
+
+function getInstalledPlugins(projectRoot) {
+    var pluginsDir = path.join(projectRoot, 'plugins');
+    // TODO: This should list based off of platform.json, not directories within plugins/
+    var pluginInfoProvider = new PluginInfoProvider();
+    return pluginInfoProvider.getAllWithinSearchPath(pluginsDir);
+}
+
+function saveToConfigXmlOn(config_json, options){
+    options = options || {};
+    var autosave =  config_json.auto_save_plugins || false;
+    return autosave || options.save;
+}
+
+function parseSource(target, opts) {
+    var url = require('url');
+    var uri = url.parse(target);
+    if (uri.protocol && uri.protocol != 'file:' && uri.protocol[1] != ':' && !target.match(/^\w+:\\/)) {
+        return target;
+    } else {
+        var plugin_dir = cordova_util.fixRelativePath(path.join(target, (opts.subdir || '.')));
+        if (fs.existsSync(plugin_dir)) {
+            return target;
+        }
+    }
+    return null;
+}
+
+function getSpec(pluginSource, projectRoot, pluginName) {
+    if (pluginSource.hasOwnProperty('url') || pluginSource.hasOwnProperty('path')) {
+        return pluginSource.url || pluginSource.path;
+    }
+
+    var version = null;
+    var scopedPackage = null;
+    if (pluginSource.hasOwnProperty('id')) {
+        // Note that currently version is only saved here if it was explicitly specified when the plugin was added.
+        var parsedSpec = pluginSpec.parse(pluginSource.id);
+        version = parsedSpec.version;
+        if (version) {
+            version = versionString(version);
+        }
+
+        if (parsedSpec.scope) {
+            scopedPackage = parsedSpec.package;
+        }
+    }
+
+    if (!version) {
+        // Fallback on getting version from the plugin folder, if it's there
+        var pluginInfoProvider = new PluginInfoProvider();
+        var dir = path.join(projectRoot, 'plugins', pluginName);
+
+        try {
+            // pluginInfoProvider.get() will throw if directory does not exist.
+            var pluginInfo = pluginInfoProvider.get(dir);
+            if (pluginInfo) {
+                version = versionString(pluginInfo.version);
+            }
+        } catch (err) {
+        }
+    }
+
+    if (scopedPackage) {
+        version = scopedPackage + '@' + version;
+    }
+
+    return version;
+}
+
+function versionString(version) {
+    var validVersion = semver.valid(version, true);
+    if (validVersion) {
+        return '~' + validVersion;
+    }
+
+    if (semver.validRange(version, true)) {
+        // Return what we were passed rather than the result of the validRange() call, as that call makes modifications
+        // we don't want, like converting '^1.2.3' to '>=1.2.3-0 <2.0.0-0'
+        return version;
+    }
+
+    return null;
+}
+
+/**
+ * Gets the version of a plugin that should be fetched for a given project based
+ * on the plugin's engine information from NPM and the platforms/plugins installed
+ * in the project. The cordovaDependencies object in the package.json's engines
+ * entry takes the form of an object that maps plugin versions to a series of
+ * constraints and semver ranges. For example:
+ *
+ *     { plugin-version: { constraint: semver-range, ...}, ...}
+ *
+ * Constraint can be a plugin, platform, or cordova version. Plugin-version
+ * can be either a single version (e.g. 3.0.0) or an upper bound (e.g. <3.0.0)
+ *
+ * @param {string}  projectRoot     The path to the root directory of the project
+ * @param {object}  pluginInfo      The NPM info of the plugin to be fetched (e.g. the
+ *                                  result of calling `registry.info()`)
+ * @param {string}  cordovaVersion  The semver version of cordova-lib
+ *
+ * @return {Promise}                A promise that will resolve to either a string
+ *                                  if there is a version of the plugin that this
+ *                                  project satisfies or null if there is not
+ */
+function getFetchVersion(projectRoot, pluginInfo, cordovaVersion) {
+    // Figure out the project requirements
+    if (pluginInfo.engines && pluginInfo.engines.cordovaDependencies) {
+        var pluginList = getInstalledPlugins(projectRoot);
+        var pluginMap = {};
+
+        pluginList.forEach(function(plugin) {
+            pluginMap[plugin.id] = plugin.version;
+        });
+
+        return cordova_util.getInstalledPlatformsWithVersions(projectRoot)
+        .then(function(platformVersions) {
+            return determinePluginVersionToFetch(
+                pluginInfo,
+                pluginMap,
+                platformVersions,
+                cordovaVersion);
+        });
+    } else {
+        // If we have no engine, we want to fall back to the default behavior
+        events.emit('verbose', 'No plugin engine info found or not using registry, falling back to latest version');
+        return Q(null);
+    }
+}
+
+function findVersion(versions, version) {
+    var cleanedVersion = semver.clean(version);
+    for(var i = 0; i < versions.length; i++) {
+        if(semver.clean(versions[i]) === cleanedVersion) {
+            return versions[i];
+        }
+    }
+    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' }
+ *  }
+ *
+ * See cordova-spec/plugin_fetch.spec.js for test cases and examples
+ */
+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;
+
+    for(var version in engine) {
+        if(semver.valid(semver.clean(version)) && !semver.gt(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(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(!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 && !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 ? 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 && 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 = getFailedRequirements(engine[versions[0]], pluginMap, platformMap, cordovaVersion);
+
+                // Warn the user that we are not fetching latest
+                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 ? 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 = 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);
+        });
+    }
+
+    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 getFailedRequirements(reqs, pluginMap, platformMap, cordovaVersion) {
+    var failed = [];
+
+    for (var req in reqs) {
+        if(reqs.hasOwnProperty(req) && typeof req === 'string' && semver.validRange(reqs[req])) {
+            var badInstalledVersion = null;
+            var trimmedReq = req.trim();
+
+            if(pluginMap[trimmedReq] && !semver.satisfies(pluginMap[trimmedReq], reqs[req])) {
+                badInstalledVersion = pluginMap[req];
+            } else if(trimmedReq === 'cordova' && !semver.satisfies(cordovaVersion, reqs[req])) {
+                badInstalledVersion = cordovaVersion;
+            } else if(trimmedReq.indexOf('cordova-') === 0) {
+                // Might be a platform constraint
+                var platform = trimmedReq.substring(8);
+                if(platformMap[platform] && !semver.satisfies(platformMap[platform], reqs[req])) {
+                    badInstalledVersion = platformMap[platform];
+                }
+            }
+
+            if(badInstalledVersion) {
+                failed.push({
+                    dependency: trimmedReq,
+                    installed: badInstalledVersion.trim(),
+                    required: reqs[req].trim()
+                });
+            }
+        } else {
+            events.emit('verbose', 'Ignoring invalid plugin dependency constraint ' + req + ':' + reqs[req]);
+        }
+    }
+
+    return failed;
+}
+
+function listUnmetRequirements(name, failedRequirements) {
+    events.emit('warn', 'Unmet project requirements for latest version of ' + name + ':');
+
+    failedRequirements.forEach(function(req) {
+        events.emit('warn', '    ' + req.dependency + ' (' + req.installed + ' installed, ' + req.required + ' required)');
+    });
+}

http://git-wip-us.apache.org/repos/asf/cordova-lib/blob/6025a5f2/cordova-lib/src/plugman/fetch.js
----------------------------------------------------------------------
diff --git a/cordova-lib/src/plugman/fetch.js b/cordova-lib/src/plugman/fetch.js
index 9583688..6db2723 100644
--- a/cordova-lib/src/plugman/fetch.js
+++ b/cordova-lib/src/plugman/fetch.js
@@ -32,8 +32,9 @@ var shell   = require('shelljs'),
     registry = require('./registry/registry'),
     pluginMappernto = require('cordova-registry-mapper').newToOld,
     pluginMapperotn = require('cordova-registry-mapper').oldToNew,
-    pluginSpec      = require('../cordova/plugin_spec_parser');
-var cordovaUtil = require('../cordova/util');
+    pluginSpec      = require('../cordova/plugin_spec_parser'),
+    fetch = require('cordova-fetch'),
+    cordovaUtil = require('../cordova/util');
 
 // Cache of PluginInfo objects for plugins in search path.
 var localPlugins = null;
@@ -44,7 +45,6 @@ module.exports = fetchPlugin;
 function fetchPlugin(plugin_src, plugins_dir, options) {
     // Ensure the containing directory exists.
     shell.mkdir('-p', plugins_dir);
-
     options = options || {};
     options.subdir = options.subdir || '.';
     options.searchpath = options.searchpath || [];
@@ -67,16 +67,26 @@ function fetchPlugin(plugin_src, plugins_dir, options) {
                 options.git_ref = result[1];
             if (result[2])
                 options.subdir = result[2];
+            //if --fetch was used, throw error for subdirectories
+            if (result[2] && options.fetch) {
+                return Q.reject(new CordovaError('--fetch does not support subdirectories'));
+            }
 
             // Recurse and exit with the new options and truncated URL.
             var new_dir = plugin_src.substring(0, plugin_src.indexOf('#'));
-            return fetchPlugin(new_dir, plugins_dir, options);
+
+            //skip the return if user asked for --fetch
+            //cordova-fetch doesn't need to strip out git-ref
+            if(!options.fetch) {
+                return fetchPlugin(new_dir, plugins_dir, options);
+            }
         }
     }
 
     return Q.when().then(function() {
-        // If it looks like a network URL, git clone it.
-        if ( uri.protocol && uri.protocol != 'file:' && uri.protocol[1] != ':' && !plugin_src.match(/^\w+:\\/)) {
+        // If it looks like a network URL, git clone it
+        // skip git cloning if user passed in --fetch flag
+        if ( uri.protocol && uri.protocol != 'file:' && uri.protocol[1] != ':' && !plugin_src.match(/^\w+:\\/) && !options.fetch) {
             events.emit('log', 'Fetching plugin "' + plugin_src + '" via git clone');
             if (options.link) {
                 events.emit('log', '--link is not supported for git URLs and will be ignored');
@@ -158,7 +168,18 @@ function fetchPlugin(plugin_src, plugins_dir, options) {
                     if (newID) {
                         events.emit('warn', 'Notice: ' + parsedSpec.id + ' has been automatically converted to ' + newID + ' to be fetched from npm. This is due to our old plugins registry shutting down.');
                     }
-                    P = registry.fetch([plugin_src]);
+                    //use cordova-fetch if --fetch was passed in
+                    if(options.fetch) {
+                        var projectRoot = path.join(plugins_dir, '..');
+                        //Plugman projects need to go up two directories to reach project root. 
+                        //Plugman projects have an options.projectRoot variable
+                        if(options.projectRoot) {
+                            projectRoot = options.projectRoot;
+                        }
+                        P = fetch(plugin_src, projectRoot, options); 
+                    } else {
+                        P = registry.fetch([plugin_src]);
+                    }
                     skipCopyingPlugin = false;
                 }
             }

http://git-wip-us.apache.org/repos/asf/cordova-lib/blob/6025a5f2/cordova-lib/src/plugman/plugman.js
----------------------------------------------------------------------
diff --git a/cordova-lib/src/plugman/plugman.js b/cordova-lib/src/plugman/plugman.js
index 2627b38..4265bc6 100644
--- a/cordova-lib/src/plugman/plugman.js
+++ b/cordova-lib/src/plugman/plugman.js
@@ -104,9 +104,12 @@ plugman.commands =  {
         var opts = {
             subdir: '.',
             cli_variables: cli_variables,
+            fetch: cli_opts.fetch || false,
+            save: cli_opts.save || false,
             www_dir: cli_opts.www,
             searchpath: cli_opts.searchpath,
-            link: cli_opts.link
+            link: cli_opts.link,
+            projectRoot: cli_opts.project
         };
         var p = Q();
         cli_opts.plugin.forEach(function (pluginSrc) {
@@ -128,8 +131,14 @@ plugman.commands =  {
 
         var p = Q();
         cli_opts.plugin.forEach(function (pluginSrc) {
+            var opts = {
+                www_dir: cli_opts.www,
+                save: cli_opts.save || false,
+                fetch: cli_opts.fetch || false,
+                projectRoot: cli_opts.project
+            };
             p = p.then(function () {
-                return plugman.raw.uninstall(cli_opts.platform, cli_opts.project, pluginSrc, cli_opts.plugins_dir, { www_dir: cli_opts.www });
+                return plugman.raw.uninstall(cli_opts.platform, cli_opts.project, pluginSrc, cli_opts.plugins_dir, opts);
             });
         });
 

http://git-wip-us.apache.org/repos/asf/cordova-lib/blob/6025a5f2/cordova-lib/src/plugman/uninstall.js
----------------------------------------------------------------------
diff --git a/cordova-lib/src/plugman/uninstall.js b/cordova-lib/src/plugman/uninstall.js
index 87bcdca..c987d49 100644
--- a/cordova-lib/src/plugman/uninstall.js
+++ b/cordova-lib/src/plugman/uninstall.js
@@ -34,6 +34,7 @@ var path = require('path'),
     HooksRunner = require('../hooks/HooksRunner'),
     cordovaUtil = require('../cordova/util'),
     pluginMapper = require('cordova-registry-mapper').oldToNew,
+    npmUninstall = require('cordova-fetch').uninstall,
     pluginSpec = require('../cordova/plugin_spec_parser');
 
 var superspawn = require('cordova-common').superspawn;
@@ -115,15 +116,31 @@ module.exports.uninstallPlugin = function(id, plugins_dir, options) {
         return Q();
     }
 
+    /*
+     * Deletes plugin from plugins directory and 
+     * node_modules directory if --fetch was supplied.
+     *
+     * @param {String} id   the id of the plugin being removed
+     *
+     * @return {Promise||Error} Returns a empty promise or a promise of doing the npm uninstall
+     */
     var doDelete = function(id) {
         var plugin_dir = path.join(plugins_dir, id);
         if ( !fs.existsSync(plugin_dir) ) {
             events.emit('verbose', 'Plugin "'+ id +'" already removed ('+ plugin_dir +')');
             return Q();
         }
-
+        
         shell.rm('-rf', plugin_dir);
         events.emit('verbose', 'Deleted "'+ id +'"');
+        
+        if(options.fetch) {
+            //remove plugin from node_modules directory
+            return npmUninstall(id, options.projectRoot, options); 
+        }
+        
+        return Q();
+
     };
 
     // We've now lost the metadata for the plugins that have been uninstalled, so we can't use that info.
@@ -201,7 +218,7 @@ module.exports.uninstallPlugin = function(id, plugins_dir, options) {
         });
     });
 
-    var i, plugin_id, msg;
+    var i, plugin_id, msg, delArray = [];
     for(i in toDelete) {
         plugin_id = toDelete[i];
 
@@ -221,11 +238,11 @@ module.exports.uninstallPlugin = function(id, plugins_dir, options) {
                 }
             }
         }
-
-        doDelete(plugin_id);
+        //create an array of promises
+        delArray.push(doDelete(plugin_id));
     }
-
-    return Q();
+    //return promise.all
+    return Q.all(delArray);
 };
 
 // possible options: cli_variables, www_dir, is_top_level


---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@cordova.apache.org
For additional commands, e-mail: commits-help@cordova.apache.org


[2/2] cordova-lib git commit: CB-9858 merging initial fetch work for plugin and platform fetching

Posted by st...@apache.org.
CB-9858 merging initial fetch work for plugin and platform fetching


Project: http://git-wip-us.apache.org/repos/asf/cordova-lib/repo
Commit: http://git-wip-us.apache.org/repos/asf/cordova-lib/commit/6025a5f2
Tree: http://git-wip-us.apache.org/repos/asf/cordova-lib/tree/6025a5f2
Diff: http://git-wip-us.apache.org/repos/asf/cordova-lib/diff/6025a5f2

Branch: refs/heads/master
Commit: 6025a5f23c00284fb3416d8042ff5d738bf03db3
Parents: b21bcc3
Author: Steve Gill <st...@gmail.com>
Authored: Mon May 9 14:18:21 2016 -0700
Committer: Steve Gill <st...@gmail.com>
Committed: Mon May 9 14:18:21 2016 -0700

----------------------------------------------------------------------
 .gitignore                                |    1 +
 .travis.yml                               |    2 +
 appveyor.yml                              |    2 +
 cordova-fetch/.jshintrc                   |   12 +
 cordova-fetch/README.md                   |   36 +
 cordova-fetch/RELEASENOTES.md             |   22 +
 cordova-fetch/index.js                    |  236 ++++
 cordova-fetch/package.json                |   43 +
 cordova-fetch/spec/fetch.spec.js          |  300 +++++
 cordova-fetch/spec/helpers.js             |   16 +
 cordova-fetch/spec/support/jasmine.json   |   11 +
 cordova-fetch/spec/testpkg.json           |   11 +
 cordova-lib/spec-cordova/helpers.js       |    3 +
 cordova-lib/spec-cordova/platform.spec.js |  108 ++
 cordova-lib/src/cordova/platform.js       |   31 +-
 cordova-lib/src/cordova/plugin.js         | 1608 ++++++++++++------------
 cordova-lib/src/plugman/fetch.js          |   35 +-
 cordova-lib/src/plugman/plugman.js        |   13 +-
 cordova-lib/src/plugman/uninstall.js      |   29 +-
 19 files changed, 1700 insertions(+), 819 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/cordova-lib/blob/6025a5f2/.gitignore
----------------------------------------------------------------------
diff --git a/.gitignore b/.gitignore
index ea05522..dae2aca 100644
--- a/.gitignore
+++ b/.gitignore
@@ -13,3 +13,4 @@ src/plugman/defaults.json
 cordova-lib/src/plugman/defaults.json
 cordova-lib/spec-plugman/plugins/recursivePlug/demo/fetch.json
 cordova-lib/spec-plugman/plugins/recursivePlug/demo/test-recursive
+cordova-fetch/test/temp

http://git-wip-us.apache.org/repos/asf/cordova-lib/blob/6025a5f2/.travis.yml
----------------------------------------------------------------------
diff --git a/.travis.yml b/.travis.yml
index bb404a1..e9f15ee 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -12,8 +12,10 @@ install:
   - npm link ../cordova-js
   - npm link ../cordova-common
   - npm link ../cordova-serve
+  - npm link ../cordova-fetch
   - npm install
 
 script:
   - "(cd ../cordova-common && npm test)"
+  - "(cd ../cordova-common && npm test)"
   - "npm run ci"

http://git-wip-us.apache.org/repos/asf/cordova-lib/blob/6025a5f2/appveyor.yml
----------------------------------------------------------------------
diff --git a/appveyor.yml b/appveyor.yml
index 36e2b26..dc26ed4 100644
--- a/appveyor.yml
+++ b/appveyor.yml
@@ -7,6 +7,7 @@ install:
   - npm link ../cordova-js
   - npm link ../cordova-common
   - npm link ../cordova-serve
+  - npm link ../cordova-fetch
   - npm install
 
 build: off
@@ -15,4 +16,5 @@ test_script:
   - node --version
   - npm --version
   - "cd ../cordova-common && npm test"
+  - "cd ../cordova-fetch && npm test"
   - "cd ../cordova-lib && npm test"

http://git-wip-us.apache.org/repos/asf/cordova-lib/blob/6025a5f2/cordova-fetch/.jshintrc
----------------------------------------------------------------------
diff --git a/cordova-fetch/.jshintrc b/cordova-fetch/.jshintrc
new file mode 100644
index 0000000..62edb5c
--- /dev/null
+++ b/cordova-fetch/.jshintrc
@@ -0,0 +1,12 @@
+{
+  "node": true,
+  "bitwise": true,
+  "undef": true,
+  "trailing": true,
+  "quotmark": true,
+  "indent": 4,
+  "unused": "vars",
+  "latedef": "nofunc",
+  "-W030": false,
+  "jasmine": true
+}

http://git-wip-us.apache.org/repos/asf/cordova-lib/blob/6025a5f2/cordova-fetch/README.md
----------------------------------------------------------------------
diff --git a/cordova-fetch/README.md b/cordova-fetch/README.md
new file mode 100644
index 0000000..5cc75e9
--- /dev/null
+++ b/cordova-fetch/README.md
@@ -0,0 +1,36 @@
+<!--
+#
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+#  KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+#
+-->
+
+# cordova-fetch
+
+This module is used for fetching modules from npm and gitURLs. It fetches the modules via `npm install`. 
+
+Usage:
+```
+var fetch = require('cordova-fetch');
+
+fetch(spec, dest, opts);
+```
+
+`spec` can be a string containg a npm `packageID` or a `git URL`. 
+`dest` is string of the directory location you wish to `npm install` these modules.
+`opts` is an Object of options cordova fetch handles. Currently, fetch only support the `save` option.
+    eg. `{'save':true}`

http://git-wip-us.apache.org/repos/asf/cordova-lib/blob/6025a5f2/cordova-fetch/RELEASENOTES.md
----------------------------------------------------------------------
diff --git a/cordova-fetch/RELEASENOTES.md b/cordova-fetch/RELEASENOTES.md
new file mode 100644
index 0000000..305cdc6
--- /dev/null
+++ b/cordova-fetch/RELEASENOTES.md
@@ -0,0 +1,22 @@
+<!--
+#
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+#  KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+#
+-->
+# Cordova-fetch Release Notes
+

http://git-wip-us.apache.org/repos/asf/cordova-lib/blob/6025a5f2/cordova-fetch/index.js
----------------------------------------------------------------------
diff --git a/cordova-fetch/index.js b/cordova-fetch/index.js
new file mode 100644
index 0000000..0e3c3fe
--- /dev/null
+++ b/cordova-fetch/index.js
@@ -0,0 +1,236 @@
+/**
+ Licensed to the Apache Software Foundation (ASF) under one
+ or more contributor license agreements.  See the NOTICE file
+ distributed with this work for additional information
+ regarding copyright ownership.  The ASF licenses this file
+ to you under the Apache License, Version 2.0 (the
+ 'License'); you may not use this file except in compliance
+ with the License.  You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing,
+ software distributed under the License is distributed on an
+ 'AS IS' BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ KIND, either express or implied.  See the License for the
+ specific language governing permissions and limitations
+ under the License.
+ */
+
+var Q = require('q');
+var shell = require('shelljs');
+var superspawn = require('cordova-common').superspawn;
+var events = require('cordova-common').events;
+var depls = require('dependency-ls');
+var path = require('path');
+var fs = require('fs');
+var CordovaError = require('cordova-common').CordovaError;
+var isUrl = require('is-url');
+
+/* 
+ * A function that npm installs a module from npm or a git url
+ *
+ * @param {String} target   the packageID or git url
+ * @param {String} dest     destination of where to install the module
+ * @param {Object} opts     [opts={save:true}] options to pass to fetch module
+ *
+ * @return {String|Promise}    Returns string of the absolute path to the installed module.
+ *
+ */
+module.exports = function(target, dest, opts) {
+    var fetchArgs = ['install'];
+    opts = opts || {};
+    var tree1;
+
+    //check if npm is installed
+    return isNpmInstalled()
+    .then(function() {
+        if(dest && target) {
+            //add target to fetchArgs Array
+            fetchArgs.push(target);
+        
+            //append node_modules to dest if it doesn't come included
+            if (path.basename(dest) !== 'node_modules') {
+            dest = path.resolve(path.join(dest, 'node_modules'));
+            }
+        
+            //create dest if it doesn't exist
+            if(!fs.existsSync(dest)) {
+                shell.mkdir('-p', dest);         
+            } 
+
+        } else return Q.reject(new CordovaError('Need to supply a target and destination'));
+
+        //set the directory where npm install will be run
+        opts.cwd = dest;
+
+        //if user added --save flag, pass it to npm install command
+        if(opts.save) {
+            events.emit('verbose', 'saving');
+            fetchArgs.push('--save'); 
+        } 
+    
+
+        //Grab json object of installed modules before npm install
+        return depls(dest);
+    })
+    .then(function(depTree) {
+        tree1 = depTree;
+
+        //install new module
+        return superspawn.spawn('npm', fetchArgs, opts);
+    })
+    .then(function(output) {
+        //Grab object of installed modules after npm install
+        return depls(dest);
+    })
+    .then(function(depTree2) {
+        var tree2 = depTree2;
+
+        //getJsonDiff will fail if the module already exists in node_modules.
+        //Need to use trimID in that case. 
+        //This could happen on a platform update.
+        var id = getJsonDiff(tree1, tree2) || trimID(target); 
+
+        return getPath(id, dest);
+    }) 
+    .fail(function(err){
+        return Q.reject(new CordovaError(err));
+    });
+};
+
+
+/*
+ * Takes two JSON objects and returns the key of the new property as a string.
+ * If a module already exists in node_modules, the diff will be blank. 
+ * cordova-fetch will use trimID in that case.
+ *
+ * @param {Object} obj1     json object representing installed modules before latest npm install
+ * @param {Object} obj2     json object representing installed modules after latest npm install
+ *
+ * @return {String}         String containing the key value of the difference between the two objects
+ *
+ */
+function getJsonDiff(obj1, obj2) {
+    var result = '';
+
+    //regex to filter out peer dependency warnings from result
+    var re = /UNMET PEER DEPENDENCY/;
+
+    for (var key in obj2) {
+        //if it isn't a unmet peer dependency, continue
+        if (key.search(re) === -1) {
+            if(obj2[key] != obj1[key]) result = key;
+        }
+    }
+    return result;
+}
+
+/*
+ * Takes the specified target and returns the moduleID
+ * If the git repoName is different than moduleID, then the 
+ * output from this function will be incorrect. This is the 
+ * backup way to get ID. getJsonDiff is the preferred way to 
+ * get the moduleID of the installed module.
+ *
+ * @param {String} target    target that was passed into cordova-fetch.
+ *                           can be moduleID, moduleID@version or gitURL
+ *
+ * @return {String} ID       moduleID without version.
+ */
+function trimID(target) {
+    var parts;
+
+    //If GITURL, set target to repo name
+    if (isUrl(target)) {
+        var re = /.*\/(.*).git/;
+        parts = target.match(re);
+        target = parts[1];
+    }
+    
+    //strip away everything after '@'
+    if(target.indexOf('@') != -1) {
+        parts = target.split('@');
+        target = parts[0];
+    }        
+    
+    return target;
+}
+
+/* 
+ * Takes the moduleID and destination and returns an absolute path to the module
+ *
+ * @param {String} id       the packageID
+ * @param {String} dest     destination of where to fetch the modules
+ *
+ * @return {String|Error}  Returns the absolute url for the module or throws a error
+ *
+ */
+
+function getPath(id, dest) {
+    var finalDest = path.resolve(path.join(dest, id));
+    
+    //Sanity check it exists
+    if(fs.existsSync(finalDest)){
+        return finalDest;
+    } else return Q.reject(new CordovaError('Failed to get absolute path to installed module'));
+}
+
+
+/*
+ * Checks to see if npm is installed on the users system
+ * @return {Promise|Error} Returns true or a cordova error.
+ */
+
+function isNpmInstalled() {
+    if(!shell.which('npm')) {
+        return Q.reject(new CordovaError('"npm" command line tool is not installed: make sure it is accessible on your PATH.'));
+    }
+    return Q();
+}
+
+/* 
+ * A function that deletes the target from node_modules and runs npm uninstall 
+ *
+ * @param {String} target   the packageID
+ * @param {String} dest     destination of where to uninstall the module from
+ * @param {Object} opts     [opts={save:true}] options to pass to npm uninstall
+ *
+ * @return {Promise|Error}    Returns a promise with the npm uninstall output or an error.
+ *
+ */
+module.exports.uninstall = function(target, dest, opts) {
+    var fetchArgs = ['uninstall'];
+    opts = opts || {};
+
+    //check if npm is installed on the system
+    return isNpmInstalled()
+    .then(function() {    
+        if(dest && target) {
+            //add target to fetchArgs Array
+            fetchArgs.push(target);  
+        } else return Q.reject(new CordovaError('Need to supply a target and destination'));
+
+        //set the directory where npm uninstall will be run
+        opts.cwd = dest;
+
+        //if user added --save flag, pass it to npm uninstall command
+        if(opts.save) {
+            fetchArgs.push('--save'); 
+        }
+
+        //run npm uninstall, this will remove dependency
+        //from package.json if --save was used.
+        return superspawn.spawn('npm', fetchArgs, opts);
+    })
+    .then(function(res) {
+        var pluginDest = path.join(dest, 'node_modules', target);
+        if(fs.existsSync(pluginDest)) {
+            shell.rm('-rf', pluginDest);
+        } 
+        return res;
+    })
+    .fail(function(err) {
+        return Q.reject(new CordovaError(err));
+    });
+};

http://git-wip-us.apache.org/repos/asf/cordova-lib/blob/6025a5f2/cordova-fetch/package.json
----------------------------------------------------------------------
diff --git a/cordova-fetch/package.json b/cordova-fetch/package.json
new file mode 100644
index 0000000..2e7e330
--- /dev/null
+++ b/cordova-fetch/package.json
@@ -0,0 +1,43 @@
+{
+  "name": "cordova-fetch",
+  "version": "1.0.0-dev",
+  "description": "Apache Cordova fetch module. Fetches from git and npm.",
+  "main": "index.js",
+  "repository": {
+    "type": "git",
+    "url": "git://git-wip-us.apache.org/repos/asf/cordova-lib.git"
+  },
+  "keywords": [
+    "cordova",
+    "fetch",
+    "apache",
+    "ecosystem:cordova",
+    "cordova:tool"
+  ],
+  "author": "Apache Software Foundation",
+  "license": "Apache-2.0",
+  "bugs": {
+    "url": "https://issues.apache.org/jira/browse/CB",
+    "email": "dev@cordova.apache.org"
+  },
+  "dependencies": {
+    "cordova-common": "^1.0.0",
+    "dependency-ls": "^1.0.0",
+    "is-url": "^1.2.1",
+    "q": "^1.4.1",
+    "shelljs": "^0.7.0"
+  },
+  "devDependencies": {
+    "jasmine": "^2.4.1",
+    "jshint": "^2.8.0"
+  },
+  "scripts": {
+    "test": "npm run jshint && npm run jasmine",
+    "jshint": "jshint index.js spec/fetch.spec.js",
+    "jasmine": "jasmine spec/fetch.spec.js"
+  },
+  "engines": {
+    "node": ">= 0.12.0",
+    "npm": ">= 2.5.1"
+  }
+}

http://git-wip-us.apache.org/repos/asf/cordova-lib/blob/6025a5f2/cordova-fetch/spec/fetch.spec.js
----------------------------------------------------------------------
diff --git a/cordova-fetch/spec/fetch.spec.js b/cordova-fetch/spec/fetch.spec.js
new file mode 100644
index 0000000..2f6513a
--- /dev/null
+++ b/cordova-fetch/spec/fetch.spec.js
@@ -0,0 +1,300 @@
+/**
+    Licensed to the Apache Software Foundation (ASF) under one
+    or more contributor license agreements.  See the NOTICE file
+    distributed with this work for additional information
+    regarding copyright ownership.  The ASF licenses this file
+    to you under the Apache License, Version 2.0 (the
+    "License"); you may not use this file except in compliance
+    with the License.  You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing,
+    software distributed under the License is distributed on an
+    "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+    KIND, either express or implied.  See the License for the
+    specific language governing permissions and limitations
+    under the License.
+*/
+
+var fetch = require('../index.js');
+var uninstall = require('../index.js').uninstall;
+var shell = require('shelljs');
+var path = require('path');
+var fs = require('fs');
+var helpers = require('./helpers.js');
+
+describe('platform fetch/uninstall tests via npm & git', function () {
+
+    var tmpDir = helpers.tmpDir('plat_fetch');
+    var opts = {};
+
+    beforeEach(function() {
+        process.chdir(tmpDir);
+    });
+    
+    afterEach(function() {
+        process.chdir(path.join(__dirname, '..'));  // Needed to rm the dir on Windows.
+        shell.rm('-rf', tmpDir);
+    });
+
+    it('should fetch and uninstall a cordova platform via npm & git', function(done) {
+        
+        fetch('cordova-android', tmpDir, opts)
+        .then(function(result) {
+            var pkgJSON = require(path.join(result,'package.json'));
+            expect(result).toBeDefined();
+            expect(fs.existsSync(result)).toBe(true);
+            expect(pkgJSON.name).toBe('cordova-android');
+            
+            return uninstall('cordova-android', tmpDir, opts);
+        })
+        .then(function() {
+            expect(fs.existsSync(path.join(tmpDir,'node_modules', 'cordova-android'))).toBe(false);
+            
+            return fetch('https://github.com/apache/cordova-ios.git', tmpDir, opts);       
+        })
+        .then(function(result) {
+            var pkgJSON = require(path.join(result,'package.json'));
+            expect(result).toBeDefined();
+            expect(fs.existsSync(result)).toBe(true);
+            expect(pkgJSON.name).toBe('cordova-ios');
+            
+            return uninstall('cordova-ios', tmpDir, opts);
+        })
+        .then(function() {
+            expect(fs.existsSync(path.join(tmpDir,'node_modules', 'cordova-ios'))).toBe(false);    
+        })
+        .fail(function(err) {
+            console.error(err);
+            expect(err).toBeUndefined();
+        })
+        .fin(done);
+    }, 60000);
+});
+
+describe('platform fetch/uninstall test via npm & git tags with --save', function () {
+
+    var tmpDir = helpers.tmpDir('plat_fetch_save');
+    var opts = {'save':true};
+    
+    beforeEach(function() {
+        //copy package.json from spec directory to tmpDir
+        shell.cp('spec/testpkg.json', path.join(tmpDir,'package.json'));
+        process.chdir(tmpDir);
+    });
+    
+    afterEach(function() {
+        process.chdir(path.join(__dirname, '..'));  // Needed to rm the dir on Windows.
+        shell.rm('-rf', tmpDir);
+    });
+
+    it('should fetch and uninstall a cordova platform via npm & git tags/branches', function(done) {
+        fetch('cordova-android@5.1.1', tmpDir, opts)
+        .then(function(result) {
+            var pkgJSON = require(path.join(result,'package.json'));
+            expect(result).toBeDefined();
+            expect(fs.existsSync(result)).toBe(true);
+            expect(pkgJSON.name).toBe('cordova-android');
+            expect(pkgJSON.version).toBe('5.1.1');
+
+            var rootPJ = require(path.join(tmpDir,'package.json'));
+            expect(rootPJ.dependencies['cordova-android']).toBe('^5.1.1');
+
+            return uninstall('cordova-android', tmpDir, opts);
+        })
+        .then(function() {
+            var rootPJ = JSON.parse(fs.readFileSync(path.join(tmpDir,'package.json'), 'utf8'));
+            expect(Object.keys(rootPJ.dependencies).length).toBe(0);
+            expect(fs.existsSync(path.join(tmpDir,'node_modules', 'cordova-android'))).toBe(false);
+
+            return fetch('https://github.com/apache/cordova-ios.git#rel/4.1.1', tmpDir, opts);       
+        })
+        .then(function(result) {
+            var pkgJSON = require(path.join(result,'package.json'));
+            expect(result).toBeDefined();
+            expect(fs.existsSync(result)).toBe(true);
+            expect(pkgJSON.name).toBe('cordova-ios');
+            expect(pkgJSON.version).toBe('4.1.1');
+
+            var rootPJ = JSON.parse(fs.readFileSync(path.join(tmpDir,'package.json'), 'utf8'));
+            expect(rootPJ.dependencies['cordova-ios']).toBe('git+https://github.com/apache/cordova-ios.git#rel/4.1.1');
+
+            return uninstall('cordova-ios', tmpDir, opts);
+        })
+        .then(function() {
+            var rootPJ = JSON.parse(fs.readFileSync(path.join(tmpDir,'package.json'), 'utf8'));
+            expect(Object.keys(rootPJ.dependencies).length).toBe(0);
+            expect(fs.existsSync(path.join(tmpDir,'node_modules', 'cordova-ios'))).toBe(false);
+
+            return fetch('https://github.com/apache/cordova-android.git#4.1.x', tmpDir, opts);
+        })
+        .then(function(result) {
+            var pkgJSON = JSON.parse(fs.readFileSync(path.join(result,'package.json'), 'utf8'));
+            expect(result).toBeDefined();
+            expect(fs.existsSync(result)).toBe(true);
+            expect(pkgJSON.name).toBe('cordova-android');
+            expect(pkgJSON.version).toBe('4.1.1');
+
+            var rootPJ = JSON.parse(fs.readFileSync(path.join(tmpDir,'package.json'), 'utf8'));
+            expect(rootPJ.dependencies['cordova-android']).toBe('git+https://github.com/apache/cordova-android.git#4.1.x');
+
+            return uninstall('cordova-android', tmpDir, opts);
+        })
+        .fail(function(err) {
+            console.error(err);
+            expect(err).toBeUndefined();
+        })
+        .fin(done);
+    }, 60000);
+});
+
+describe('plugin fetch/uninstall test with --save', function () {
+
+    var tmpDir = helpers.tmpDir('plug_fetch_save');
+    var opts = {'save':true};
+    
+    beforeEach(function() {
+        //copy package.json from spec directory to tmpDir
+        shell.cp('spec/testpkg.json', path.join(tmpDir,'package.json'));
+        process.chdir(tmpDir);
+    });
+    
+    afterEach(function() {
+        process.chdir(path.join(__dirname, '..'));  // Needed to rm the dir on Windows.
+        shell.rm('-rf', tmpDir);
+    });
+
+    it('should fetch and uninstall a cordova plugin via git commit sha', function(done) {
+        fetch('https://github.com/apache/cordova-plugin-contacts.git#7db612115755c2be73a98dda76ff4c5fd9d8a575', tmpDir, opts)
+        .then(function(result) {
+            var pkgJSON = require(path.join(result,'package.json'));
+            expect(result).toBeDefined();
+            expect(fs.existsSync(result)).toBe(true);
+            expect(pkgJSON.name).toBe('cordova-plugin-contacts');
+            expect(pkgJSON.version).toBe('2.0.2-dev');
+
+            var rootPJ = require(path.join(tmpDir,'package.json'));
+            expect(rootPJ.dependencies['cordova-plugin-contacts']).toBe('git+https://github.com/apache/cordova-plugin-contacts.git#7db612115755c2be73a98dda76ff4c5fd9d8a575');
+
+            return uninstall('cordova-plugin-contacts', tmpDir, opts);
+        })
+        .then(function() {
+            var rootPJ = JSON.parse(fs.readFileSync(path.join(tmpDir,'package.json'), 'utf8'));
+            expect(Object.keys(rootPJ.dependencies).length).toBe(0);
+            expect(fs.existsSync(path.join(tmpDir,'node_modules', 'cordova-plugin-contacts'))).toBe(false);
+        })
+        .fail(function(err) {
+            console.error(err);
+            expect(err).toBeUndefined();
+        })
+        .fin(done);
+    }, 30000);
+});
+
+describe('test trimID method for npm and git', function () {
+
+    var tmpDir = helpers.tmpDir('plug_trimID');
+    var opts = {};
+    
+    beforeEach(function() {
+        process.chdir(tmpDir);
+    });
+    
+    afterEach(function() {
+        process.chdir(path.join(__dirname, '..'));  // Needed to rm the dir on Windows.
+        shell.rm('-rf', tmpDir);
+    });
+
+    it('should fetch the same cordova plugin twice in a row', function(done) {
+        fetch('cordova-plugin-device', tmpDir, opts)
+        .then(function(result) {
+            var pkgJSON = require(path.join(result,'package.json'));
+            expect(result).toBeDefined();
+            expect(fs.existsSync(result)).toBe(true);
+            expect(pkgJSON.name).toBe('cordova-plugin-device');
+            
+            return fetch('https://github.com/apache/cordova-plugin-media.git', tmpDir, opts);
+        })
+        .then(function(result) {
+            var pkgJSON = require(path.join(result,'package.json'));
+            expect(result).toBeDefined();
+            expect(fs.existsSync(result)).toBe(true);
+            expect(pkgJSON.name).toBe('cordova-plugin-media');
+
+            //refetch to trigger trimID
+            return fetch('cordova-plugin-device', tmpDir, opts);
+            
+        })
+        .then(function(result) {
+            expect(result).toBeDefined();
+            expect(fs.existsSync(result)).toBe(true);
+
+            //refetch to trigger trimID
+            return fetch('https://github.com/apache/cordova-plugin-media.git', tmpDir, opts);
+        })
+        .then(function(result) {
+            expect(result).toBeDefined();
+            expect(fs.existsSync(result)).toBe(true);
+        })
+        .fail(function(err) {
+            console.error(err);
+            expect(err).toBeUndefined();
+        })
+        .fin(done);
+    }, 30000);
+});
+
+describe('fetch failure with unknown module', function () {
+
+    var tmpDir = helpers.tmpDir('fetch_fails_npm');
+    var opts = {};
+    
+    beforeEach(function() {
+        process.chdir(tmpDir);
+    });
+    
+    afterEach(function() {
+        process.chdir(path.join(__dirname, '..'));  // Needed to rm the dir on Windows.
+        shell.rm('-rf', tmpDir);
+    });
+
+    it('should fail fetching a module that does not exist on npm', function(done) {
+        fetch('NOTAMODULE', tmpDir, opts)
+        .then(function(result) {
+            console.log('This should fail and it should not be seen');
+        })
+        .fail(function(err) {
+            expect(err.message.code).toBe(1);
+            expect(err).toBeDefined();
+        })
+        .fin(done);
+    }, 30000);
+});
+
+describe('fetch failure with git subdirectory', function () {
+
+    var tmpDir = helpers.tmpDir('fetch_fails_subdirectory');
+    var opts = {};
+
+    beforeEach(function() {
+        process.chdir(tmpDir);
+    });
+    
+    afterEach(function() {
+        process.chdir(path.join(__dirname, '..'));  // Needed to rm the dir on Windows.
+        shell.rm('-rf', tmpDir);
+    });
+
+    it('should fail fetching a giturl which contains a subdirectory', function(done) {
+        fetch('https://github.com/apache/cordova-plugins.git#:keyboard', tmpDir, opts)
+        .then(function(result) {
+            console.log('This should fail and it should not be seen');
+        })
+        .fail(function(err) {
+            expect(err.message.code).toBe(1);
+            expect(err).toBeDefined();
+        })
+        .fin(done);
+    }, 30000);
+});

http://git-wip-us.apache.org/repos/asf/cordova-lib/blob/6025a5f2/cordova-fetch/spec/helpers.js
----------------------------------------------------------------------
diff --git a/cordova-fetch/spec/helpers.js b/cordova-fetch/spec/helpers.js
new file mode 100644
index 0000000..ee3f57b
--- /dev/null
+++ b/cordova-fetch/spec/helpers.js
@@ -0,0 +1,16 @@
+var path    = require('path'),
+    fs      = require('fs'),
+    shell   = require('shelljs'),
+    os      = require('os');
+
+module.exports.tmpDir = function (subdir) {
+    var dir = path.join(os.tmpdir(), 'e2e-test');
+    if (subdir) {
+        dir = path.join(dir, subdir);
+    }
+    if(fs.existsSync(dir)) {
+        shell.rm('-rf', dir);
+    }
+    shell.mkdir('-p', dir);
+    return dir;
+};

http://git-wip-us.apache.org/repos/asf/cordova-lib/blob/6025a5f2/cordova-fetch/spec/support/jasmine.json
----------------------------------------------------------------------
diff --git a/cordova-fetch/spec/support/jasmine.json b/cordova-fetch/spec/support/jasmine.json
new file mode 100644
index 0000000..3ea3166
--- /dev/null
+++ b/cordova-fetch/spec/support/jasmine.json
@@ -0,0 +1,11 @@
+{
+  "spec_dir": "spec",
+  "spec_files": [
+    "**/*[sS]pec.js"
+  ],
+  "helpers": [
+    "helpers/**/*.js"
+  ],
+  "stopSpecOnExpectationFailure": false,
+  "random": false
+}

http://git-wip-us.apache.org/repos/asf/cordova-lib/blob/6025a5f2/cordova-fetch/spec/testpkg.json
----------------------------------------------------------------------
diff --git a/cordova-fetch/spec/testpkg.json b/cordova-fetch/spec/testpkg.json
new file mode 100644
index 0000000..94e7f64
--- /dev/null
+++ b/cordova-fetch/spec/testpkg.json
@@ -0,0 +1,11 @@
+{
+  "name": "test",
+  "version": "1.0.0",
+  "description": "",
+  "main": "fetch.spec.js",
+  "scripts": {
+    "test": "echo \"Error: no test specified\" && exit 1"
+  },
+  "author": "",
+  "license": "ISC"
+}

http://git-wip-us.apache.org/repos/asf/cordova-lib/blob/6025a5f2/cordova-lib/spec-cordova/helpers.js
----------------------------------------------------------------------
diff --git a/cordova-lib/spec-cordova/helpers.js b/cordova-lib/spec-cordova/helpers.js
index a5d23fd..4cc1e2c 100644
--- a/cordova-lib/spec-cordova/helpers.js
+++ b/cordova-lib/spec-cordova/helpers.js
@@ -40,6 +40,9 @@ module.exports.tmpDir = function (subdir) {
     if (subdir) {
         dir = path.join(dir, subdir);
     }
+    if(fs.existsSync(dir)) {
+        shell.rm('-rf', dir);
+    }
     shell.mkdir('-p', dir);
     return dir;
 };

http://git-wip-us.apache.org/repos/asf/cordova-lib/blob/6025a5f2/cordova-lib/spec-cordova/platform.spec.js
----------------------------------------------------------------------
diff --git a/cordova-lib/spec-cordova/platform.spec.js b/cordova-lib/spec-cordova/platform.spec.js
index a35216f..a0bd104 100644
--- a/cordova-lib/spec-cordova/platform.spec.js
+++ b/cordova-lib/spec-cordova/platform.spec.js
@@ -248,3 +248,111 @@ describe('platform add plugin rm end-to-end', function () {
         .fin(done);
     }, 20000);
 });
+
+describe('platform add and remove --fetch', function () {
+
+    var tmpDir = helpers.tmpDir('plat_add_remove_fetch_test');
+    var project = path.join(tmpDir, 'helloFetch');
+    var platformsDir = path.join(project, 'platforms');
+    var nodeModulesDir = path.join(project, 'node_modules');
+    
+    beforeEach(function() {
+        process.chdir(tmpDir);
+    });
+    
+    afterEach(function() {
+        process.chdir(path.join(__dirname, '..'));  // Needed to rm the dir on Windows.
+        shell.rm('-rf', tmpDir);
+    });
+
+    it('should add and remove platform from node_modules directory', function(done) {
+        
+        cordova.raw.create('helloFetch')
+        .then(function() {
+            process.chdir(project);
+            return cordova.raw.platform('add', 'ios', {'fetch':true});
+        })
+        .then(function() {
+            expect(path.join(nodeModulesDir, 'cordova-ios')).toExist();
+            expect(path.join(platformsDir, 'ios')).toExist();
+            return cordova.raw.platform('add', 'android', {'fetch':true});
+        })
+        .then(function() {    
+            expect(path.join(nodeModulesDir, 'cordova-android')).toExist();
+            expect(path.join(platformsDir, 'android')).toExist();
+            //Tests finish before this command finishes resolving
+            //return cordova.raw.platform('rm', 'ios', {'fetch':true});
+        })
+        .then(function() {
+            //expect(path.join(nodeModulesDir, 'cordova-ios')).not.toExist();
+            //expect(path.join(platformsDir, 'ios')).not.toExist();
+            //Tests finish before this command finishes resolving
+            //return cordova.raw.platform('rm', 'android', {'fetch':true});
+        })
+        .then(function() {
+            //expect(path.join(nodeModulesDir, 'cordova-android')).not.toExist();
+            //expect(path.join(platformsDir, 'android')).not.toExist();
+        })
+        .fail(function(err) {
+            console.error(err);
+            expect(err).toBeUndefined();
+        })
+        .fin(done);
+    }, 40000);
+});
+
+describe('plugin add and rm end-to-end --fetch', function () {
+
+    var tmpDir = helpers.tmpDir('plugin_rm_fetch_test');
+    var project = path.join(tmpDir, 'hello3');
+    var pluginsDir = path.join(project, 'plugins');
+    
+    beforeEach(function() {
+        process.chdir(tmpDir);
+    });
+    
+    afterEach(function() {
+        process.chdir(path.join(__dirname, '..'));  // Needed to rm the dir on Windows.
+        shell.rm('-rf', tmpDir);
+    });
+
+    it('should remove dependency when removing parent plugin', function(done) {
+        
+        cordova.raw.create('hello3')
+        .then(function() {
+            process.chdir(project);
+            return cordova.raw.platform('add', 'ios', {'fetch': true});
+        })
+        .then(function() {
+            return cordova.raw.plugin('add', 'cordova-plugin-media', {'fetch': true});
+        })
+        .then(function() {
+            expect(path.join(pluginsDir, 'cordova-plugin-media')).toExist();
+            expect(path.join(pluginsDir, 'cordova-plugin-file')).toExist();
+            expect(path.join(pluginsDir, 'cordova-plugin-compat')).toExist();
+            expect(path.join(project, 'node_modules', 'cordova-plugin-media')).toExist();
+            expect(path.join(project, 'node_modules', 'cordova-plugin-file')).toExist();
+            expect(path.join(project, 'node_modules', 'cordova-plugin-compat')).toExist();
+            return cordova.raw.platform('add', 'android', {'fetch':true});
+        })
+        .then(function() {
+            expect(path.join(pluginsDir, 'cordova-plugin-media')).toExist();
+            expect(path.join(pluginsDir, 'cordova-plugin-file')).toExist();
+            return cordova.raw.plugin('rm', 'cordova-plugin-media', {'fetch':true});
+        })
+        .then(function() {
+            expect(path.join(pluginsDir, 'cordova-plugin-media')).not.toExist();
+            expect(path.join(pluginsDir, 'cordova-plugin-file')).not.toExist();
+            expect(path.join(pluginsDir, 'cordova-plugin-compat')).not.toExist();
+            //These don't work yet due to the tests finishing before the promise resolves.
+            //expect(path.join(project, 'node_modules', 'cordova-plugin-media')).not.toExist();
+            //expect(path.join(project, 'node_modules', 'cordova-plugin-file')).not.toExist();
+            //expect(path.join(project, 'node_modules', 'cordova-plugin-compat')).not.toExist();
+        })
+        .fail(function(err) {
+            console.error(err);
+            expect(err).toBeUndefined();
+        })
+        .fin(done);
+    }, 60000);
+});

http://git-wip-us.apache.org/repos/asf/cordova-lib/blob/6025a5f2/cordova-lib/src/cordova/platform.js
----------------------------------------------------------------------
diff --git a/cordova-lib/src/cordova/platform.js b/cordova-lib/src/cordova/platform.js
index db6f53c..cdfeb85 100644
--- a/cordova-lib/src/cordova/platform.js
+++ b/cordova-lib/src/cordova/platform.js
@@ -37,6 +37,8 @@ var config            = require('./config'),
     shell             = require('shelljs'),
     _                 = require('underscore'),
     PlatformJson      = require('cordova-common').PlatformJson,
+    fetch             = require('cordova-fetch'),
+    npmUninstall         = require('cordova-fetch').uninstall,
     platformMetadata  = require('./platform_metadata');
 
 // Expose the platform parsers on top of this command
@@ -257,6 +259,21 @@ function getSpecString(spec) {
 function downloadPlatform(projectRoot, platform, version, opts) {
     var target = version ? (platform + '@' + version) : platform;
     return Q().then(function() {
+        if (opts.fetch) {
+            //append cordova to platform
+            if(platform in platforms) {
+                target = 'cordova-'+target;
+            }
+
+            //gitURLs don't supply a platform, it equals null
+            if(!platform) {
+                target = version;
+            }
+
+            events.emit('log', 'Using cordova-fetch for '+ target);
+            return fetch(target, projectRoot, opts);
+        }
+
         if (cordova_util.isUrl(version)) {
             events.emit('log', 'git cloning: ' + version);
             var parts = version.split('#');
@@ -356,8 +373,8 @@ function remove(hooksRunner, projectRoot, targets, opts) {
                 events.emit('log', 'Removing ' + target + ' from config.xml file ...');
                 cfg.removeEngine(platformName);
                 cfg.write();
-        });
-    }
+            });
+        }
     }).then(function() {
         // Remove targets from platforms.json
         targets.forEach(function(target) {
@@ -365,6 +382,16 @@ function remove(hooksRunner, projectRoot, targets, opts) {
             platformMetadata.remove(projectRoot, target);
         });
     }).then(function() {
+        //Remove from node_modules if it exists and --fetch was used
+        if(opts.fetch) {
+            targets.forEach(function(target) {
+                if(target in platforms) {
+                    target = 'cordova-'+target;
+                }
+                return npmUninstall(target, projectRoot, opts);
+            });
+        }
+    }).then(function() {
         return hooksRunner.fire('after_platform_rm', opts);
     });
 }


---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@cordova.apache.org
For additional commands, e-mail: commits-help@cordova.apache.org