Return-Path: X-Original-To: archive-asf-public-internal@cust-asf2.ponee.io Delivered-To: archive-asf-public-internal@cust-asf2.ponee.io Received: from cust-asf.ponee.io (cust-asf.ponee.io [163.172.22.183]) by cust-asf2.ponee.io (Postfix) with ESMTP id C1663200CAF for ; Thu, 22 Jun 2017 19:34:40 +0200 (CEST) Received: by cust-asf.ponee.io (Postfix) id BF68A160BD3; Thu, 22 Jun 2017 17:34:40 +0000 (UTC) Delivered-To: archive-asf-public@cust-asf.ponee.io Received: from mail.apache.org (hermes.apache.org [140.211.11.3]) by cust-asf.ponee.io (Postfix) with SMTP id 8D922160BFA for ; Thu, 22 Jun 2017 19:34:38 +0200 (CEST) Received: (qmail 59211 invoked by uid 500); 22 Jun 2017 17:34:37 -0000 Mailing-List: contact commits-help@cordova.apache.org; run by ezmlm Precedence: bulk List-Help: List-Unsubscribe: List-Post: List-Id: Delivered-To: mailing list commits@cordova.apache.org Received: (qmail 58780 invoked by uid 99); 22 Jun 2017 17:34:36 -0000 Received: from git1-us-west.apache.org (HELO git1-us-west.apache.org) (140.211.11.23) by apache.org (qpsmtpd/0.29) with ESMTP; Thu, 22 Jun 2017 17:34:36 +0000 Received: by git1-us-west.apache.org (ASF Mail Server at git1-us-west.apache.org, from userid 33) id D5C5AE967F; Thu, 22 Jun 2017 17:34:32 +0000 (UTC) Content-Type: text/plain; charset="us-ascii" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit From: filmaj@apache.org To: commits@cordova.apache.org Date: Thu, 22 Jun 2017 17:34:36 -0000 Message-Id: <5d2c60f8a0ff4506b56a26ac26efda56@git.apache.org> In-Reply-To: References: X-Mailer: ASF-Git Admin Mailer Subject: [05/14] cordova-lib git commit: first pass at plugin command refactor archived-at: Thu, 22 Jun 2017 17:34:40 -0000 first pass at plugin command refactor Project: http://git-wip-us.apache.org/repos/asf/cordova-lib/repo Commit: http://git-wip-us.apache.org/repos/asf/cordova-lib/commit/ce93923f Tree: http://git-wip-us.apache.org/repos/asf/cordova-lib/tree/ce93923f Diff: http://git-wip-us.apache.org/repos/asf/cordova-lib/diff/ce93923f Branch: refs/heads/master Commit: ce93923f1f74f0531e126eb49c62edb3a28c25e0 Parents: 080653f Author: filmaj Authored: Mon Jun 19 18:45:31 2017 -0500 Committer: filmaj Committed: Wed Jun 21 14:57:48 2017 -0500 ---------------------------------------------------------------------- integration-tests/save.spec.js | 20 - src/cordova/plugin.js | 914 -------------------------- src/cordova/plugin/add.js | 564 ++++++++++++++++ src/cordova/plugin/index.js | 96 +++ src/cordova/plugin/list.js | 75 +++ src/cordova/plugin/plugin_spec_parser.js | 61 ++ src/cordova/plugin/remove.js | 132 ++++ src/cordova/plugin/save.js | 146 ++++ src/cordova/plugin/search.js | 41 ++ src/cordova/plugin/util.js | 37 ++ src/cordova/plugin_spec_parser.js | 61 -- 11 files changed, 1152 insertions(+), 995 deletions(-) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/cordova-lib/blob/ce93923f/integration-tests/save.spec.js ---------------------------------------------------------------------- diff --git a/integration-tests/save.spec.js b/integration-tests/save.spec.js index 616d3db..e83b096 100644 --- a/integration-tests/save.spec.js +++ b/integration-tests/save.spec.js @@ -551,26 +551,6 @@ describe('(save flag)', function () { }); }, TIMEOUT); - it('Test 027 : spec.22 should update config with plugins: one with version, one with local folder and another one vith git url', function (done) { - cordova.plugin('add', pluginName + '@' + pluginVersion) - .then(function () { - return cordova.plugin('add', gitPluginUrl); - }).then(function () { - return cordova.plugin('add', localPluginPath); - }).then(function () { - return cordova.plugin('save'); - }).then(function () { - expect(helpers.getPluginSpec(appPath, pluginName)).toBe('~' + pluginVersion); - expect(helpers.getPluginSpec(appPath, gitPluginName)).toBe(gitPluginUrl); - expect(helpers.getPluginSpec(appPath, localPluginName)).toBe(localPluginPath); - done(); - }).catch(function (err) { - expect(true).toBe(false); - console.log(err.message); - done(); - }); - }, TIMEOUT); - it('Test 028 : spec.22.1 should update config with a spec that includes the scope for scoped plugins', function (done) { // Fetching globalization rather than console to avoid conflicts with earlier tests redirectRegistryCalls(pluginName2 + '@' + pluginVersion2); http://git-wip-us.apache.org/repos/asf/cordova-lib/blob/ce93923f/src/cordova/plugin.js ---------------------------------------------------------------------- diff --git a/src/cordova/plugin.js b/src/cordova/plugin.js deleted file mode 100644 index d631abc..0000000 --- a/src/cordova/plugin.js +++ /dev/null @@ -1,914 +0,0 @@ -/** - 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'), - 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'), - 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'), - semver = require('semver'), - 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, - 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.fetch(target, pluginPath, fetchOptions); - }) - .then(function (directory) { - return pluginInfoProvider.get(directory); - }); - }) - .then(function(pluginInfo) { - // Validate top-level required variables - var pluginVariables = pluginInfo.getPreferences(); - opts.cli_variables = opts.cli_variables || {}; - var pluginEntry = cfg.getPlugin(pluginInfo.id); - // Get variables from config.xml - var configVariables = pluginEntry ? pluginEntry.variables : {}; - // Add config variable if it's missing in cli_variables - Object.keys(configVariables).forEach(function(variable) { - opts.cli_variables[variable] = opts.cli_variables[variable] || configVariables[variable]; - }); - 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 + ' because mandatory plugin variables were missing.'); - 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_www dir - // instead of /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.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){ - var pkgJson; - var pkgJsonPath = path.join(projectRoot,'package.json'); - - // save to config.xml - if(saveToConfigXmlOn(config_json, opts)){ - // If statement to see if pkgJsonPath exists in the filesystem - if(fs.existsSync(pkgJsonPath)) { - // Delete any previous caches of require(package.json) - pkgJson = cordova_util.requireNoCache(pkgJsonPath); - } - // If package.json exists, the plugin object and plugin name - // will be added to package.json if not already there. - if(pkgJson) { - pkgJson.cordova = pkgJson.cordova || {}; - pkgJson.cordova.plugins = pkgJson.cordova.plugins || {}; - // Plugin and variables are added. - pkgJson.cordova.plugins[pluginInfo.id] = opts.cli_variables; - events.emit('log','Adding '+pluginInfo.id+ ' to package.json'); - - // Write to package.json - fs.writeFileSync(pkgJsonPath, JSON.stringify(pkgJson, null, 2), 'utf8'); - } - - 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(pkgJson && pkgJson.dependencies && pkgJson.dependencies[pluginInfo.id]){ - attributes.spec = pkgJson.dependencies[pluginInfo.id]; - } else { - 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() { - // 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 + '"'); - var options = { - force: opts.force || false - }; - return plugman.uninstall.uninstallPlatform(platform, platformRoot, target, 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; - }); - }); - }, Q()) - .then(function() { - // TODO: Should only uninstallPlugin when no platforms have it. - return plugman.uninstall.uninstallPlugin(target, pluginPath, opts); - }).then(function(){ - //remove plugin from config.xml - if(saveToConfigXmlOn(config_json, opts)){ - events.emit('log', 'Removing plugin ' + target + ' from config.xml file...'); - 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(); - } - var pkgJson; - var pkgJsonPath = path.join(projectRoot,'package.json'); - // If statement to see if pkgJsonPath exists in the filesystem - if(fs.existsSync(pkgJsonPath)) { - //delete any previous caches of require(package.json) - pkgJson = cordova_util.requireNoCache(pkgJsonPath); - } else { - // Create package.json in cordova@7 - } - // If package.json exists and contains a specified plugin in cordova['plugins'], it will be removed - if(pkgJson !== undefined && pkgJson.cordova !== undefined && pkgJson.cordova.plugins !== undefined) { - events.emit('log', 'Removing ' + target + ' from package.json'); - // Remove plugin from package.json - delete pkgJson.cordova.plugins[target]; - //Write out new package.json with plugin removed correctly. - fs.writeFileSync(pkgJsonPath, JSON.stringify(pkgJson, null, 2), 'utf8'); - } - } - - }).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); - } - // Require project pkgJson. - var pkgJsonPath = path.join(projectRoot, 'package.json'); - var cordovaVersion = pkgJson.version; - if(fs.existsSync(pkgJsonPath)) { - pkgJson = cordova_util.requireNoCache(pkgJsonPath); - } - - // If no parsedSpec.version, use the one from pkg.json or config.xml. - if (!parsedSpec.version) { - // Retrieve from pkg.json. - if(pkgJson && pkgJson.dependencies && pkgJson.dependencies[id]) { - events.emit('verbose', 'No version specified for ' + id + ', retrieving version from package.json'); - parsedSpec.version = pkgJson.dependencies[id]; - } else { - // If no version is specified, retrieve the version (or source) from config.xml. - events.emit('verbose', 'No version specified for ' + id + ', retrieving version from config.xml'); - parsedSpec.version = getVersionFromConfigFile(id, cfg); - } - } - - // If parsedSpec.version satisfies pkgJson version, no writing to pkg.json. Only write when - // it does not satisfy. - - if(parsedSpec.version) { - if(pkgJson && pkgJson.dependencies && pkgJson.dependencies[parsedSpec.package]) { - var noSymbolVersion = parsedSpec.version; - if (parsedSpec.version.charAt(0) === '^' || parsedSpec.version.charAt(0) === '~') { - noSymbolVersion = parsedSpec.version.slice(1); - } - - if(cordova_util.isUrl(parsedSpec.version) || cordova_util.isDirectory(parsedSpec.version)) { - if (pkgJson.dependencies[parsedSpec.package] !== parsedSpec.version) { - pkgJson.dependencies[parsedSpec.package] = parsedSpec.version; - } - if(fetchOptions.save === true) { - fs.writeFileSync(pkgJsonPath, JSON.stringify(pkgJson, null, 2), 'utf8'); - } - } else if (!semver.satisfies(noSymbolVersion, pkgJson.dependencies[parsedSpec.package])) { - pkgJson.dependencies[parsedSpec.package] = parsedSpec.version; - if (fetchOptions.save === true) { - fs.writeFileSync(pkgJsonPath, JSON.stringify(pkgJson, null, 2), 'utf8'); - } - } - } - } - - if (cordova_util.isUrl(parsedSpec.version) || cordova_util.isDirectory(parsedSpec.version) || pluginSpec.parse(parsedSpec.version).scope) { - return Q(parsedSpec.version); - } - - // If version exists in pkg.json or config.xml, use that. - if (parsedSpec.version) { - return Q(id + '@' + parsedSpec.version); - } - - // 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; - - events.emit('verbose', 'No version for ' + parsedSpec.package + ' saved in config.xml'); - if(shouldUseNpmInfo) { - events.emit('verbose', 'Attempting to use npm info for ' + parsedSpec.package + ' to choose a compatible release'); - } else { - events.emit('verbose', 'Not checking npm info for ' + parsedSpec.package + ' because searchpath or noregistry flag was given'); - } - - return (shouldUseNpmInfo ? registry.info([id]) - .then(function(pluginInfo) { - return getFetchVersion(projectRoot, pluginInfo, cordovaVersion); - }) : Q(null)) - .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; - } - - 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); - - 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 `.'); - return; - } - var pluginsDict = {}; - var lines = []; - var txt, p; - for (var i=0; i=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', 'npm info for ' + pluginInfo.name + ' did not contain any engine info. Fetching latest release'); - 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 = []; - var version = cordovaVersion; - if (semver.prerelease(version)) { - // semver.inc with 'patch' type removes prereleased tag from version - version = semver.inc(version, 'patch'); - } - - 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(version, 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 + ' in project, ' + req.required + ' required)'); - }); -} http://git-wip-us.apache.org/repos/asf/cordova-lib/blob/ce93923f/src/cordova/plugin/add.js ---------------------------------------------------------------------- diff --git a/src/cordova/plugin/add.js b/src/cordova/plugin/add.js new file mode 100644 index 0000000..346e688 --- /dev/null +++ b/src/cordova/plugin/add.js @@ -0,0 +1,564 @@ +/** + 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'); +var plugin_util = require('./util'); +var config = require('../config'); +var pkgJson = require('../../../package.json'); +var pluginSpec = require('./plugin_spec_parser'); +var plugman = require('../../plugman/plugman'); +var registry = require('../../plugman/registry/registry'); +var chainMap = require('../../util/promise-util').Q_chainmap; +var ConfigParser = require('cordova-common').ConfigParser; +var CordovaError = require('cordova-common').CordovaError; +var PluginInfoProvider = require('cordova-common').PluginInfoProvider; +var events = require('cordova-common').events; +var shell = require('shelljs'); +var Q = require('Q'); +var path = require('path'); +var fs = require('fs'); +var semver = require('semver'); + +module.exports = add; +module.exports.determinePluginTarget = determinePluginTarget; +module.exports.parseSource = parseSource; +module.exports.getVersionFromConfigFile = getVersionFromConfigFile; +module.exports.getFetchVersion = getFetchVersion; +module.exports.determinePluginVersionToFetch = determinePluginVersionToFetch; +module.exports.getFailedRequirements = getFailedRequirements; +module.exports.findVersion = findVersion; +module.exports.listUnmetRequirements = listUnmetRequirements; + +function add (projectRoot, targets, hooksRunner, opts) { + 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 shouldRunPrepare = false; + var pluginPath = path.join(projectRoot, 'plugins'); + var platformList = cordova_util.listPlatforms(projectRoot); + var config_json = config.read(projectRoot); + 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 module.exports.determinePluginTarget(projectRoot, cfg, target, fetchOptions).then(function (resolvedTarget) { + target = resolvedTarget; + events.emit('verbose', 'Calling plugman.fetch on plugin "' + target + '"'); + return plugman.fetch(target, pluginPath, fetchOptions); + }); + }).then(function (directory) { + return pluginInfoProvider.get(directory); + }).then(function (pluginInfo) { + // Validate top-level required variables + var pluginVariables = pluginInfo.getPreferences(); + opts.cli_variables = opts.cli_variables || {}; + var pluginEntry = cfg.getPlugin(pluginInfo.id); + // Get variables from config.xml + var configVariables = pluginEntry ? pluginEntry.variables : {}; + // Add config variable if it's missing in cli_variables + Object.keys(configVariables).forEach(function (variable) { + opts.cli_variables[variable] = opts.cli_variables[variable] || configVariables[variable]; + }); + 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 + ' because mandatory plugin variables were missing.'); + 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); + var 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_www dir + // instead of /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.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) { + var pkgJson; + var pkgJsonPath = path.join(projectRoot, 'package.json'); + + // save to config.xml + if (plugin_util.saveToConfigXmlOn(config_json, opts)) { + // If statement to see if pkgJsonPath exists in the filesystem + if (fs.existsSync(pkgJsonPath)) { + // Delete any previous caches of require(package.json) + pkgJson = cordova_util.requireNoCache(pkgJsonPath); + } + // If package.json exists, the plugin object and plugin name + // will be added to package.json if not already there. + if (pkgJson) { + pkgJson.cordova = pkgJson.cordova || {}; + pkgJson.cordova.plugins = pkgJson.cordova.plugins || {}; + // Plugin and variables are added. + pkgJson.cordova.plugins[pluginInfo.id] = opts.cli_variables; + events.emit('log', 'Adding ' + pluginInfo.id + ' to package.json'); + + // Write to package.json + fs.writeFileSync(pkgJsonPath, JSON.stringify(pkgJson, null, 2), 'utf8'); + } + + var src = module.exports.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 (pkgJson && pkgJson.dependencies && pkgJson.dependencies[pluginInfo.id]) { + attributes.spec = pkgJson.dependencies[pluginInfo.id]; + } else { + 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 () { + // 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. + // TODO: possible circular dependency? + return require('./prepare').preparePlatforms(platformList, projectRoot, opts); + }).then(function () { + opts.cordova = { plugins: cordova_util.findPlugins(pluginPath) }; + return hooksRunner.fire('after_plugin_add', opts); + }); +} + +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); + } + // Require project pkgJson. + var pkgJsonPath = path.join(projectRoot, 'package.json'); + var cordovaVersion = pkgJson.version; + if (fs.existsSync(pkgJsonPath)) { + pkgJson = cordova_util.requireNoCache(pkgJsonPath); + } + + // If no parsedSpec.version, use the one from pkg.json or config.xml. + if (!parsedSpec.version) { + // Retrieve from pkg.json. + if (pkgJson && pkgJson.dependencies && pkgJson.dependencies[id]) { + events.emit('verbose', 'No version specified for ' + id + ', retrieving version from package.json'); + parsedSpec.version = pkgJson.dependencies[id]; + } else { + // If no version is specified, retrieve the version (or source) from config.xml. + events.emit('verbose', 'No version specified for ' + id + ', retrieving version from config.xml'); + parsedSpec.version = module.exports.getVersionFromConfigFile(id, cfg); + } + } + + // If parsedSpec.version satisfies pkgJson version, no writing to pkg.json. Only write when + // it does not satisfy. + if (parsedSpec.version) { + if (pkgJson && pkgJson.dependencies && pkgJson.dependencies[parsedSpec.package]) { + var noSymbolVersion = parsedSpec.version; + if (parsedSpec.version.charAt(0) === '^' || parsedSpec.version.charAt(0) === '~') { + noSymbolVersion = parsedSpec.version.slice(1); + } + + if (cordova_util.isUrl(parsedSpec.version) || cordova_util.isDirectory(parsedSpec.version)) { + if (pkgJson.dependencies[parsedSpec.package] !== parsedSpec.version) { + pkgJson.dependencies[parsedSpec.package] = parsedSpec.version; + } + if (fetchOptions.save === true) { + fs.writeFileSync(pkgJsonPath, JSON.stringify(pkgJson, null, 2), 'utf8'); + } + } else if (!semver.satisfies(noSymbolVersion, pkgJson.dependencies[parsedSpec.package])) { + pkgJson.dependencies[parsedSpec.package] = parsedSpec.version; + if (fetchOptions.save === true) { + fs.writeFileSync(pkgJsonPath, JSON.stringify(pkgJson, null, 2), 'utf8'); + } + } + } + } + + if (cordova_util.isUrl(parsedSpec.version) || cordova_util.isDirectory(parsedSpec.version) || pluginSpec.parse(parsedSpec.version).scope) { + return Q(parsedSpec.version); + } + + // If version exists in pkg.json or config.xml, use that. + if (parsedSpec.version) { + return Q(id + '@' + parsedSpec.version); + } + + // 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; + + events.emit('verbose', 'No version for ' + parsedSpec.package + ' saved in config.xml'); + if (shouldUseNpmInfo) { + events.emit('verbose', 'Attempting to use npm info for ' + parsedSpec.package + ' to choose a compatible release'); + } else { + events.emit('verbose', 'Not checking npm info for ' + parsedSpec.package + ' because searchpath or noregistry flag was given'); + } + + // TODO: whoa wat + return (shouldUseNpmInfo ? registry.info([id]) + .then(function (pluginInfo) { + return getFetchVersion(projectRoot, pluginInfo, cordovaVersion); + }) : Q(null)) + .then(function (fetchVersion) { + return fetchVersion ? (id + '@' + fetchVersion) : target; + }); +} + +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 getVersionFromConfigFile (plugin, cfg) { + var parsedSpec = pluginSpec.parse(plugin); + var pluginEntry = cfg.getPlugin(parsedSpec.id); + + return pluginEntry && pluginEntry.spec; +} + +/** + * 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 = plugin_util.getInstalledPlugins(projectRoot); + var pluginMap = {}; + + pluginList.forEach(function (plugin) { + pluginMap[plugin.id] = plugin.version; + }); + + return cordova_util.getInstalledPlatformsWithVersions(projectRoot) + .then(function (platformVersions) { + return module.exports.determinePluginVersionToFetch( + pluginInfo, + pluginMap, + platformVersions, + cordovaVersion); + }); + } else { + // If we have no engine, we want to fall back to the default behavior + events.emit('verbose', 'npm info for ' + pluginInfo.name + ' did not contain any engine info. Fetching latest release'); + return Q(null); + } +} + +// For upper bounds in cordovaDependencies +var UPPER_BOUND_REGEX = /^<\d+\.\d+\.\d+$/; +/* + * 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 (module.exports.getFailedRequirements(engine[version], pluginMap, platformMap, cordovaVersion).length !== 0) { + var maxMatchingUpperBound = cleanedRange.substring(1); + if (maxMatchingUpperBound && (!upperBound || semver.gt(maxMatchingUpperBound, upperBound))) { + upperBound = maxMatchingUpperBound; + upperBoundRange = version; + } + } + } else { + events.emit('verbose', 'Ignoring invalid version in ' + name + ' cordovaDependencies: ' + version + ' (must be a single version <= latest or an upper bound)'); + } + } + } + + // If there were no valid requirements, we fall back to old behavior + if (!upperBoundExists && versions.length === 0) { + events.emit('verbose', 'Ignoring ' + name + ' cordovaDependencies entry because it did not contain any valid plugin version entries'); + return null; + } + + // Handle the lower end of versions by giving them a satisfied engine + if (!module.exports.findVersion(versions, '0.0.0')) { + versions.push('0.0.0'); + engine['0.0.0'] = {}; + } + + // Add an entry after the upper bound to handle the versions above the + // upper bound but below the next entry. For example: 0.0.0, <1.0.0, 2.0.0 + // needs a 1.0.0 entry that has the same engine as 0.0.0 + if (upperBound && !module.exports.findVersion(versions, upperBound) && !semver.gt(upperBound, latest)) { + versions.push(upperBound); + var below = semver.maxSatisfying(versions, upperBoundRange); + + // Get the original entry without trimmed whitespace + below = below ? module.exports.findVersion(versions, below) : null; + engine[upperBound] = below ? engine[below] : {}; + } + + // Sort in descending order; we want to start at latest and work back + versions.sort(semver.rcompare); + + for (var i = 0; i < versions.length; i++) { + if (upperBound && semver.lt(versions[i], upperBound)) { + // Because we sorted in desc. order, if the upper bound we found + // applies to this version (and thus the ones below) we can just + // quit + break; + } + + var range = i ? ('>=' + versions[i] + ' <' + versions[i - 1]) : ('>=' + versions[i]); + var maxMatchingVersion = semver.maxSatisfying(allVersions, range); + + if (maxMatchingVersion && module.exports.getFailedRequirements(engine[versions[i]], pluginMap, platformMap, cordovaVersion).length === 0) { + // Because we sorted in descending order, we can stop searching once + // we hit a satisfied constraint + if (maxMatchingVersion !== latest) { + var failedReqs = module.exports.getFailedRequirements(engine[versions[0]], pluginMap, platformMap, cordovaVersion); + + // Warn the user that we are not fetching latest + module.exports.listUnmetRequirements(name, failedReqs); + events.emit('warn', 'Fetching highest version of ' + name + ' that this project supports: ' + maxMatchingVersion + ' (latest is ' + latest + ')'); + } + return maxMatchingVersion; + } + } + + // No version of the plugin is satisfied. In this case, we fall back to + // fetching the latest version, but also output a warning + var latestFailedReqs = versions.length > 0 ? module.exports.getFailedRequirements(engine[versions[0]], pluginMap, platformMap, cordovaVersion) : []; + + // If the upper bound is greater than latest, we need to combine its engine + // requirements with latest to print out in the warning + if (upperBound && semver.satisfies(latest, upperBoundRange)) { + var upperFailedReqs = module.exports.getFailedRequirements(engine[upperBoundRange], pluginMap, platformMap, cordovaVersion); + upperFailedReqs.forEach(function (failedReq) { + for (var i = 0; i < latestFailedReqs.length; i++) { + if (latestFailedReqs[i].dependency === failedReq.dependency) { + // Not going to overcomplicate things and actually merge the ranges + latestFailedReqs[i].required += ' AND ' + failedReq.required; + return; + } + } + + // There is no req to merge it with + latestFailedReqs.push(failedReq); + }); + } + + module.exports.listUnmetRequirements(name, latestFailedReqs); + events.emit('warn', 'Current project does not satisfy the engine requirements specified by any version of ' + name + '. Fetching latest version of plugin anyway (may be incompatible)'); + + // No constraints were satisfied + return null; +} + +function getFailedRequirements (reqs, pluginMap, platformMap, cordovaVersion) { + var failed = []; + var version = cordovaVersion; + if (semver.prerelease(version)) { + // semver.inc with 'patch' type removes prereleased tag from version + version = semver.inc(version, 'patch'); + } + + 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(version, 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 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; +} + +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 + ' in project, ' + req.required + ' required)'); + }); +} http://git-wip-us.apache.org/repos/asf/cordova-lib/blob/ce93923f/src/cordova/plugin/index.js ---------------------------------------------------------------------- diff --git a/src/cordova/plugin/index.js b/src/cordova/plugin/index.js new file mode 100644 index 0000000..1099de1 --- /dev/null +++ b/src/cordova/plugin/index.js @@ -0,0 +1,96 @@ +/** + 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'); +var Q = require('q'); +var CordovaError = require('cordova-common').CordovaError; +var HooksRunner = require('../../hooks/HooksRunner'); + +module.exports = plugin; +module.exports.add = require('./add'); +module.exports.remove = require('./remove'); +module.exports.list = require('./list'); +module.exports.save = require('./save'); + +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 = []; + + var hooksRunner = new HooksRunner(projectRoot); + + // Massage plugin name(s) / path(s) + if (!targets || !targets.length) { + // TODO: what if command provided is 'remove' ? + 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 + for (var i = 0; i < targets.length; i++) { + if (targets[i].match(/^-/)) { + opts.options = targets.slice(i); + break; + } else { + opts.plugins.push(targets[i]); + } + } + + switch (command) { + case 'add': + return module.exports.add(projectRoot, targets, hooksRunner, opts); + case 'rm': + case 'remove': + return module.exports.remove(projectRoot, targets, hooksRunner, opts); + case 'search': + return module.exports.search(hooksRunner, opts); + case 'save': + // save the versions/folders/git-urls of currently installed plugins into config.xml + return module.exports.save(projectRoot, opts); + default: + return module.exports.list(projectRoot, hooksRunner); + } + }); +} http://git-wip-us.apache.org/repos/asf/cordova-lib/blob/ce93923f/src/cordova/plugin/list.js ---------------------------------------------------------------------- diff --git a/src/cordova/plugin/list.js b/src/cordova/plugin/list.js new file mode 100644 index 0000000..78d9521 --- /dev/null +++ b/src/cordova/plugin/list.js @@ -0,0 +1,75 @@ +/** + 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 semver = require('semver'); +var events = require('cordova-common').events; +var plugin_util = require('./util'); +var cordova_util = require('../util'); + +module.exports = list; + +function list (projectRoot, hooksRunner, opts) { + var pluginsList = []; + return hooksRunner.fire('before_plugin_ls', opts) + .then(function () { + return plugin_util.getInstalledPlugins(projectRoot); + }).then(function (plugins) { + if (plugins.length === 0) { + events.emit('results', 'No plugins added. Use `' + cordova_util.binname + ' plugin add `.'); + 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; + }); +} http://git-wip-us.apache.org/repos/asf/cordova-lib/blob/ce93923f/src/cordova/plugin/plugin_spec_parser.js ---------------------------------------------------------------------- diff --git a/src/cordova/plugin/plugin_spec_parser.js b/src/cordova/plugin/plugin_spec_parser.js new file mode 100644 index 0000000..7ad1bae --- /dev/null +++ b/src/cordova/plugin/plugin_spec_parser.js @@ -0,0 +1,61 @@ +/** + 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. +*/ + +// npm packages follow the pattern of (@scope/)?package(@spec)? where scope and tag are optional +var NPM_SPEC_REGEX = /^(@[^\/]+\/)?([^@\/]+)(?:@(.+))?$/; + +module.exports.parse = parse; + +/** + * Represents a parsed specification for a plugin + * @class + * @param {String} raw The raw specification (i.e. provided by the user) + * @param {String} scope The scope of the package if this is an npm package + * @param {String} id The id of the package if this is an npm package + * @param {String} version The version specified for the package if this is an npm package + */ +function PluginSpec (raw, scope, id, version) { + /** @member {String|null} The npm scope of the plugin spec or null if it does not have one */ + this.scope = scope || null; + + /** @member {String|null} The id of the plugin or the raw plugin spec if it is not an npm package */ + this.id = id || raw; + + /** @member {String|null} The specified version of the plugin or null if no version was specified */ + this.version = version || null; + + /** @member {String|null} The npm package of the plugin (with scope) or null if this is not a spec for an npm package */ + this.package = (scope ? scope + id : id) || null; +} + +/** + * Tries to parse the given string as an npm-style package specification of + * the form (@scope/)?package(@version)? and return the various parts. + * + * @param {String} raw The string to be parsed + * @return {PluginSpec} The parsed plugin spec + */ +function parse (raw) { + var split = NPM_SPEC_REGEX.exec(raw); + if (split) { + return new PluginSpec(raw, split[1], split[2], split[3]); + } + + return new PluginSpec(raw); +} --------------------------------------------------------------------- To unsubscribe, e-mail: commits-unsubscribe@cordova.apache.org For additional commands, e-mail: commits-help@cordova.apache.org