CB-10679: New version choosing logic for plugin add Adds support for plugins specifying their cordova related dependencies in their package.json to guide cordova-lib in choosing the correct version of a plugin to fetch for the current project
This closes #363 Project: http://git-wip-us.apache.org/repos/asf/cordova-lib/repo Commit: http://git-wip-us.apache.org/repos/asf/cordova-lib/commit/45a235fa Tree: http://git-wip-us.apache.org/repos/asf/cordova-lib/tree/45a235fa Diff: http://git-wip-us.apache.org/repos/asf/cordova-lib/diff/45a235fa Branch: refs/heads/common-1.1.x Commit: 45a235fa73e5b23ed9fa44734449503976b999e3 Parents: 114de6c Author: riknoll <richard.b.kn...@gmail.com> Authored: Fri Jan 15 13:35:26 2016 -0800 Committer: riknoll <richard.b.kn...@gmail.com> Committed: Mon Mar 7 13:37:17 2016 -0800 ---------------------------------------------------------------------- cordova-lib/spec-cordova/plugin.spec.js | 157 +++++-- cordova-lib/spec-cordova/plugin_fetch.spec.js | 522 +++++++++++++++++++++ cordova-lib/spec-cordova/util.spec.js | 19 + cordova-lib/src/cordova/platform.js | 21 +- cordova-lib/src/cordova/plugin.js | 324 +++++++++++-- cordova-lib/src/cordova/util.js | 20 +- cordova-lib/src/plugman/registry/registry.js | 26 +- 7 files changed, 985 insertions(+), 104 deletions(-) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/cordova-lib/blob/45a235fa/cordova-lib/spec-cordova/plugin.spec.js ---------------------------------------------------------------------- diff --git a/cordova-lib/spec-cordova/plugin.spec.js b/cordova-lib/spec-cordova/plugin.spec.js index b09fbff..11a1b16 100644 --- a/cordova-lib/spec-cordova/plugin.spec.js +++ b/cordova-lib/spec-cordova/plugin.spec.js @@ -19,9 +19,12 @@ var helpers = require('./helpers'), path = require('path'), + Q = require('q'), shell = require('shelljs'), events = require('cordova-common').events, - cordova = require('../src/cordova/cordova'); + cordova = require('../src/cordova/cordova'), + plugman = require('../src/plugman/plugman'), + registry = require('../src/plugman/registry/registry'); var tmpDir = helpers.tmpDir('plugin_test'); var project = path.join(tmpDir, 'project'); @@ -29,21 +32,66 @@ var pluginsDir = path.join(__dirname, 'fixtures', 'plugins'); var pluginId = 'org.apache.cordova.fakeplugin1'; var org_test_defaultvariables = 'org.test.defaultvariables'; +var results; + + +// Runs: list, add, list +function addPlugin(target, id, options) { + // Check there are no plugins yet. + return cordova.raw.plugin('list').then(function() { + expect(results).toMatch(/No plugins added/gi); + }).then(function() { + // Add a fake plugin from fixtures. + return cordova.raw.plugin('add', target, options); + }).then(function() { + expect(path.join(project, 'plugins', id, 'plugin.xml')).toExist(); + }).then(function() { + return cordova.raw.plugin('ls'); + }).then(function() { + expect(results).toContain(id); + }); +} + +// Runs: remove, list +function removePlugin(id) { + return cordova.raw.plugin('rm', id) + .then(function() { + // The whole dir should be gone. + expect(path.join(project, 'plugins', id)).not.toExist(); + }).then(function() { + return cordova.raw.plugin('ls'); + }).then(function() { + expect(results).toMatch(/No plugins added/gi); + }); +} + +var errorHandler = { + errorCallback: function(error) { + // We want the error to be printed by jasmine + expect(error).toBeUndefined(); + } +}; + +// We can't call add with a searchpath or else we will conflict with other tests +// that use a searchpath. See loadLocalPlugins() in plugman/fetch.js for details. +// The searchpath behavior gets tested in the plugman spec +function mockPluginFetch(id, dir) { + spyOn(plugman.raw, 'fetch').andCallFake(function(target, pluginPath, fetchOptions) { + var dest = path.join(project, 'plugins', id); + var src = path.join(dir, 'plugin.xml'); + + shell.mkdir(dest); + shell.cp(src, dest); + return Q(dest); + }); +} + describe('plugin end-to-end', function() { - var results; + events.on('results', function(res) { results = res; }); beforeEach(function() { shell.rm('-rf', project); - }); - afterEach(function() { - process.chdir(path.join(__dirname, '..')); // Needed to rm the dir on Windows. - shell.rm('-rf', tmpDir); - }); - // The flow tested is: ls, add, ls, rm, ls. - // Plugin dependencies are not tested as that should be corvered in plugman tests. - // TODO (kamrik): Test the 'plugin search' command. - it('should successfully run', function(done) { // cp then mv because we need to copy everything, but that means it'll copy the whole directory. // Using /* doesn't work because of hidden files. shell.cp('-R', path.join(__dirname, 'fixtures', 'base'), tmpDir); @@ -52,40 +100,57 @@ describe('plugin end-to-end', function() { shell.cp('-R', path.join(__dirname, 'fixtures', 'platforms', helpers.testPlatform), path.join(project, 'platforms')); process.chdir(project); - events.on('results', function(res) { results = res; }); - - // Check there are no plugins yet. - cordova.raw.plugin('list').then(function() { - expect(results).toMatch(/No plugins added/gi); - }).then(function() { - // Add a fake plugin from fixtures. - return cordova.raw.plugin('add', path.join(pluginsDir, 'fake1')); - }).then(function() { - expect(path.join(project, 'plugins', pluginId, 'plugin.xml')).toExist(); - }).then(function() { - return cordova.raw.plugin('ls'); - }).then(function() { - expect(results).toContain(pluginId); - }).then(function() { - // And now remove it. - return cordova.raw.plugin('rm', pluginId); - }).then(function() { - // The whole dir should be gone. - expect(path.join(project, 'plugins', pluginId)).not.toExist(); - }).then(function() { - return cordova.raw.plugin('ls'); - }).then(function() { - expect(results).toMatch(/No plugins added/gi); - }).then(function() { - // Testing Default Variables plugin - return cordova.raw.plugin('add', path.join(pluginsDir, org_test_defaultvariables),{cli_variables: { REQUIRED:'yes', REQUIRED_ANDROID:'yes'}}); - }).then(function() { - return cordova.raw.plugin('ls'); - }).then(function() { - expect(results).toContain(org_test_defaultvariables); - }).fail(function(err) { - console.log(err.stack); - expect(err).toBeUndefined(); - }).fin(done); + spyOn(errorHandler, 'errorCallback').andCallThrough(); + }); + + afterEach(function() { + process.chdir(path.join(__dirname, '..')); // Needed to rm the dir on Windows. + shell.rm('-rf', tmpDir); + expect(errorHandler.errorCallback).not.toHaveBeenCalled(); + }); + + it('should successfully add and remove a plugin with no options', function(done) { + addPlugin(path.join(pluginsDir, 'fake1'), pluginId, {}, done) + .then(function() { + return removePlugin(pluginId); + }) + .fail(errorHandler.errorCallback) + .fin(done); + }); + + it('should successfully add a plugin when specifying CLI variables', function(done) { + addPlugin(path.join(pluginsDir, org_test_defaultvariables), org_test_defaultvariables, {cli_variables: { REQUIRED:'yes', REQUIRED_ANDROID:'yes'}}, done) + .fail(errorHandler.errorCallback) + .fin(done); + }); + + it('should not check npm info when using the searchpath flag', function(done) { + mockPluginFetch(pluginId, path.join(pluginsDir, 'fake1')); + + spyOn(registry, 'info'); + addPlugin(pluginId, pluginId, {searchpath: pluginsDir}, done) + .then(function() { + expect(registry.info).not.toHaveBeenCalled(); + + var fetchOptions = plugman.raw.fetch.mostRecentCall.args[2]; + expect(fetchOptions.searchpath).toBeDefined(); + }) + .fail(errorHandler.errorCallback) + .fin(done); + }); + + it('should not check npm info when using the noregistry flag', function(done) { + mockPluginFetch(pluginId, path.join(pluginsDir, 'fake1')); + + spyOn(registry, 'info'); + addPlugin(pluginId, pluginId, {noregistry:true}, done) + .then(function() { + expect(registry.info).not.toHaveBeenCalled(); + + var fetchOptions = plugman.raw.fetch.mostRecentCall.args[2]; + expect(fetchOptions.noregistry).toBeTruthy(); + }) + .fail(errorHandler.errorCallback) + .fin(done); }); }); http://git-wip-us.apache.org/repos/asf/cordova-lib/blob/45a235fa/cordova-lib/spec-cordova/plugin_fetch.spec.js ---------------------------------------------------------------------- diff --git a/cordova-lib/spec-cordova/plugin_fetch.spec.js b/cordova-lib/spec-cordova/plugin_fetch.spec.js new file mode 100644 index 0000000..107b539 --- /dev/null +++ b/cordova-lib/spec-cordova/plugin_fetch.spec.js @@ -0,0 +1,522 @@ +/** + 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 plugin = require('../src/cordova/plugin'), + helpers = require('./helpers'), + path = require('path'), + events = require('cordova-common').events, + shell = require('shelljs'); + +var testPluginVersions = [ + '0.0.2', + '0.7.0', + '1.0.0', + '1.1.0', + '1.1.3', + '1.3.0', + '1.7.0', + '1.7.1', + '2.0.0-rc.1', + '2.0.0-rc.2', + '2.0.0', + '2.3.0' +]; + +var cordovaVersion = '3.4.2'; + +var tempDir = helpers.tmpDir('plugin_fetch_spec'); +var project = path.join(tempDir, 'project'); + +var getVersionErrorCallback; +var warnings = []; + +// Used to extract the constraint, the installed version, and the required +// semver range from a warning message +var UNMET_REQ_REGEX = /\s+([^\s]+)[^\d]+(\d+\.\d+\.\d+) installed, (.+) required\)/; + +// We generate warnings when we don't fetch latest. Collect them to make sure we +// are making the correct warnings +events.on('warn', function(warning) { + warnings.push(warning); +}); + +// Tests a sample engine against the installed platforms/plugins in our test +// project +function testEngineWithProject(done, testEngine, testResult) { + plugin.getFetchVersion(project, + { + 'version': '2.3.0', + 'name': 'test-plugin', + 'engines': { 'cordovaDependencies': testEngine }, + 'versions': testPluginVersions + }, cordovaVersion) + .then(function(toFetch) { + expect(toFetch).toBe(testResult); + }) + .fail(getVersionErrorCallback) + .fin(done); +} + +// Checks the warnings that were printed by the CLI to ensure that the code is +// listing the correct reasons for failure. Checks against the global warnings +// object which is reset before each test +function checkUnmetRequirements(requirements) { + var reqWarnings = []; + + warnings.forEach(function(warning) { + var extracted = UNMET_REQ_REGEX.exec(warning); + if (extracted) { + reqWarnings.push({ + dependency: extracted[1], + installed: extracted[2], + required: extracted[3] + }); + } + }); + + expect(reqWarnings.length).toEqual(requirements.length); + + requirements.forEach(function(requirement) { + expect(reqWarnings).toContain(function(extractedWarning) { + return extractedWarning.dependency === requirement.dependency.trim() && + extractedWarning.installed === requirement.installed.trim() && + extractedWarning.required === requirement.required.trim(); + }, requirement); + }); +} + +// Helper functions for creating the requirements objects taken by +// checkUnmetRequirements() +function getPlatformRequirement(requirement) { + return { + dependency: 'cordova-android', + installed: '3.1.0', + required: requirement + }; +} + +function getCordovaRequirement(requirement) { + return { + dependency: 'cordova', + installed: cordovaVersion, + required: requirement + }; +} + +function getPluginRequirement(requirement) { + return { + dependency: 'ca.filmaj.AndroidPlugin', + installed: '4.2.0', + required: requirement + }; +} + +// Generates a callback that checks warning messages after the test is complete +function getWarningCheckCallback(done, requirements) { + return function() { + checkUnmetRequirements(requirements); + expect(getVersionErrorCallback).not.toHaveBeenCalled(); + done(); + }; +} + +function createTestProject() { + // Get the base project + shell.cp('-R', path.join(__dirname, 'fixtures', 'base'), tempDir); + shell.mv(path.join(tempDir, 'base'), project); + + // Copy a platform and a plugin to our sample project + shell.cp('-R', + path.join(__dirname, 'fixtures', 'platforms', helpers.testPlatform), + path.join(project, 'platforms')); + shell.cp('-R', + path.join(__dirname, 'fixtures', 'plugins', 'android'), + path.join(project, 'plugins')); +} + +function removeTestProject() { + shell.rm('-rf', tempDir); +} + +describe('plugin fetching version selection', function(done) { + createTestProject(); + + beforeEach(function() { + // Adding a matcher for checking the array of warning messages so that + // we can have meanigful error messages. Expected is passed because + // Jasmine will print it out if the matcher fails + this.addMatchers({ + toContain: function(check, expected) { + for(var i = 0; i < this.actual.length; i++) { + if (check(this.actual[i])) { + return true; + } + } + return false; + } + }); + + warnings = []; + getVersionErrorCallback = jasmine.createSpy('unexpectedPluginFetchErrorCallback'); + }); + + it('should handle a mix of upper bounds and single versions', function(done) { + var testEngine = { + '0.0.0' : { 'cordova-android': '1.0.0' }, + '0.0.2' : { 'cordova-android': '>1.0.0' }, + '<1.0.0': { 'cordova-android': '<2.0.0' }, + '1.0.0' : { 'cordova-android': '>2.0.0' }, + '1.7.0' : { 'cordova-android': '>4.0.0' }, + '<2.3.0': { 'cordova-android': '<6.0.0' }, + '2.3.0' : { 'cordova-android': '6.0.0' } + }; + + var after = getWarningCheckCallback(done, [ + getPlatformRequirement('6.0.0') + ]); + + testEngineWithProject(after, testEngine, '1.3.0'); + }); + + it('should apply upper bound engine constraints when there are no unspecified constraints above the upper bound', function(done) { + var testEngine = { + '1.0.0' : { 'cordova-android': '>2.0.0' }, + '1.7.0' : { 'cordova-android': '>4.0.0' }, + '<2.3.0': { + 'cordova-android': '<6.0.0', + 'ca.filmaj.AndroidPlugin': '<1.0.0' + }, + '2.3.0' : { 'cordova-android': '6.0.0' } + }; + + var after = getWarningCheckCallback(done, [ + getPlatformRequirement('6.0.0') + ]); + + testEngineWithProject(after, testEngine, null); + }); + + it('should apply upper bound engine constraints when there are unspecified constraints above the upper bound', function(done) { + var testEngine = { + '0.0.0' : {}, + '2.0.0' : { 'cordova-android': '~5.0.0' }, + '<1.0.0': { 'cordova-android': '>5.0.0' } + }; + + var after = getWarningCheckCallback(done, [ + getPlatformRequirement('~5.0.0') + ]); + + testEngineWithProject(after, testEngine, '1.7.1'); + }); + + it('should handle the case where there are no constraints for earliest releases', function(done) { + var testEngine = { + '1.0.0' : { 'cordova-android': '~5.0.0' } + }; + + var after = getWarningCheckCallback(done, [ + getPlatformRequirement('~5.0.0') + ]); + + testEngineWithProject(after, testEngine, '0.7.0'); + }); + + it('should handle the case where the lowest version is unsatisfied', function(done) { + var testEngine = { + '0.0.2' : { 'cordova-android': '~5.0.0' } + }; + + var after = getWarningCheckCallback(done, [ + getPlatformRequirement('~5.0.0') + ]); + + testEngineWithProject(after, testEngine, null); + }); + + it('should handle upperbounds if no single version constraints are given', function(done) { + var testEngine = { + '<1.0.0': { 'cordova-android': '<2.0.0' } + }; + + var after = getWarningCheckCallback(done, []); + + testEngineWithProject(after, testEngine, '2.3.0'); + }); + + it('should apply upper bounds greater than highest version', function(done) { + var testEngine = { + '0.0.0' : {}, + '<5.0.0': { 'cordova-android': '<2.0.0' } + }; + + var after = getWarningCheckCallback(done, [ + getPlatformRequirement('<2.0.0') + ]); + + testEngineWithProject(after, testEngine, null); + }); + + it('should treat empty constraints as satisfied', function(done) { + var testEngine = { + '1.0.0' : {}, + '1.1.0' : { 'cordova-android': '>5.0.0' } + }; + + var after = getWarningCheckCallback(done, [ + getPlatformRequirement('>5.0.0') + ]); + + testEngineWithProject(after, testEngine, '1.0.0'); + }); + + it('should ignore an empty cordovaDependencies entry', function(done) { + var testEngine = {}; + + var after = getWarningCheckCallback(done, []); + + testEngineWithProject(after, testEngine, null); + }); + + it('should ignore a badly formatted semver range', function(done) { + var testEngine = { + '1.1.3' : { 'cordova-android': 'badSemverRange' } + }; + + var after = getWarningCheckCallback(done, []); + + testEngineWithProject(after, testEngine, '2.3.0'); + }); + + it('should respect unreleased versions in constraints', function(done) { + var testEngine = { + '1.0.0' : { 'cordova-android': '3.1.0' }, + '1.1.2' : { 'cordova-android': '6.0.0' }, + '1.3.0' : { 'cordova-android': '6.0.0' } + }; + + var after = getWarningCheckCallback(done, [ + getPlatformRequirement('6.0.0') + ]); + + testEngineWithProject(after, testEngine, '1.1.0'); + }); + + it('should respect plugin constraints', function(done) { + var testEngine = { + '0.0.0' : { 'ca.filmaj.AndroidPlugin': '1.2.0' }, + '1.1.3' : { 'ca.filmaj.AndroidPlugin': '<5.0.0 || >2.3.0' }, + '2.3.0' : { 'ca.filmaj.AndroidPlugin': '6.0.0' } + }; + + var after = getWarningCheckCallback(done, [ + getPluginRequirement('6.0.0') + ]); + + testEngineWithProject(after, testEngine, '2.0.0'); + }); + + it('should respect cordova constraints', function(done) { + var testEngine = { + '0.0.0' : { 'cordova': '>1.0.0' }, + '1.1.3' : { 'cordova': '<3.0.0 || >4.0.0' }, + '2.3.0' : { 'cordova': '6.0.0' } + }; + + var after = getWarningCheckCallback(done, [ + getCordovaRequirement('6.0.0') + ]); + + testEngineWithProject(after, testEngine, '1.1.0'); + }); + + it('should not include pre-release versions', function(done) { + var testEngine = { + '0.0.0' : {}, + '2.0.0' : { 'cordova-android': '>5.0.0' } + }; + + var after = getWarningCheckCallback(done, [ + getPlatformRequirement('>5.0.0') + ]); + + // Should not return 2.0.0-rc.2 + testEngineWithProject(after, testEngine, '1.7.1'); + }); + + it('should not fail if there is no engine in the npm info', function(done) { + plugin.getFetchVersion(project, { + version: '2.3.0', + name: 'test-plugin', + versions: testPluginVersions + }, cordovaVersion) + .then(function(toFetch) { + expect(toFetch).toBe(null); + }) + .fail(getVersionErrorCallback).fin(done); + }); + + it('should not fail if there is no cordovaDependencies in the engines', function(done) { + var after = getWarningCheckCallback(done, []); + + plugin.getFetchVersion(project, { + version: '2.3.0', + name: 'test-plugin', + versions: testPluginVersions, + engines: { + 'node': '>7.0.0', + 'npm': '~2.0.0' + } + }, cordovaVersion) + .then(function(toFetch) { + expect(toFetch).toBe(null); + }) + .fail(getVersionErrorCallback).fin(after); + }); + + it('should handle extra whitespace', function(done) { + var testEngine = { + ' 1.0.0 ' : {}, + '2.0.0 ' : { ' cordova-android': '~5.0.0 ' }, + ' < 1.0.0\t' : { ' cordova-android ': ' > 5.0.0' } + }; + + var after = getWarningCheckCallback(done, [ + getPlatformRequirement('~5.0.0') + ]); + + testEngineWithProject(after, testEngine, '1.7.1'); + }); + + it('should ignore badly typed version requirement entries', function(done) { + var testEngine = { + '1.1.0' : ['cordova', '5.0.0'], + '1.3.0' : undefined, + '1.7.0' : null + }; + + var after = getWarningCheckCallback(done, []); + + testEngineWithProject(after, testEngine, '2.3.0'); + }); + + it('should ignore badly typed constraint entries', function(done) { + var testEngine = { + '0.0.2' : { 'cordova': 1 }, + '0.7.0' : { 'cordova': {}}, + '1.0.0' : { 'cordova': undefined}, + '1.1.3' : { 8 : '5.0.0'}, + '1.3.0' : { 'cordova': [] }, + '1.7.1' : { 'cordova': null } + }; + + var after = getWarningCheckCallback(done, []); + + testEngineWithProject(after, testEngine, '2.3.0'); + }); + + it('should ignore bad semver versions', function(done) { + var testEngine = { + '0.0.0' : { 'cordova-android': '5.0.0' }, + 'notAVersion' : { 'cordova-android': '3.1.0' }, + '^1.1.2' : { 'cordova-android': '3.1.0' }, + '<=1.3.0' : { 'cordova-android': '3.1.0' }, + '1.0' : { 'cordova-android': '3.1.0' }, + 2 : { 'cordova-android': '3.1.0' } + }; + + var after = getWarningCheckCallback(done, [ + getPlatformRequirement('5.0.0') + ]); + + testEngineWithProject(after, testEngine, null); + }); + + it('should not fail if there are bad semver versions', function(done) { + var testEngine = { + 'notAVersion' : { 'cordova-android': '3.1.0' }, + '^1.1.2' : { 'cordova-android': '3.1.0' }, + '<=1.3.0' : { 'cordova-android': '3.1.0' }, + '1.0.0' : { 'cordova-android': '~3' }, // Good semver + '2.0.0' : { 'cordova-android': '5.1.0' }, // Good semver + '1.0' : { 'cordova-android': '3.1.0' }, + 2 : { 'cordova-android': '3.1.0' } + }; + + var after = getWarningCheckCallback(done, [ + getPlatformRequirement('5.1.0') + ]); + + testEngineWithProject(after, testEngine, '1.7.1'); + }); + + it('should properly warn about multiple unmet requirements', function(done) { + var testEngine = { + '1.7.0' : { + 'cordova-android' : '>5.1.0', + 'ca.filmaj.AndroidPlugin' : '3.1.0', + 'cordova' : '3.4.2' + } + }; + + var after = getWarningCheckCallback(done, [ + getPlatformRequirement('>5.1.0'), + getPluginRequirement('3.1.0') + ]); + + testEngineWithProject(after, testEngine, '1.3.0'); + }); + + it('should properly warn about both unmet latest and upper bound requirements', function(done) { + var testEngine = { + '1.7.0' : { 'cordova-android': '>5.1.0' }, + '<5.0.0': { + 'cordova-android' : '>7.1.0', + 'ca.filmaj.AndroidPlugin' : '3.1.0' + } + }; + + var after = getWarningCheckCallback(done, [ + getPlatformRequirement('>5.1.0 AND >7.1.0'), + getPluginRequirement('3.1.0') + ]); + + testEngineWithProject(after, testEngine, null); + }); + + it('should not warn about versions past latest', function(done) { + var testEngine = { + '1.7.0' : { 'cordova-android': '>5.1.0' }, + '7.0.0': { + 'cordova-android' : '>7.1.0', + 'ca.filmaj.AndroidPlugin' : '3.1.0' + } + }; + + var after = getWarningCheckCallback(done, [ + getPlatformRequirement('>5.1.0') + ]); + + testEngineWithProject(after, testEngine, '1.3.0'); + }); + + it('clean up after plugin fetch spec', function() { + removeTestProject(); + }); +}); http://git-wip-us.apache.org/repos/asf/cordova-lib/blob/45a235fa/cordova-lib/spec-cordova/util.spec.js ---------------------------------------------------------------------- diff --git a/cordova-lib/spec-cordova/util.spec.js b/cordova-lib/spec-cordova/util.spec.js index 8a678c8..314de3b 100644 --- a/cordova-lib/spec-cordova/util.spec.js +++ b/cordova-lib/spec-cordova/util.spec.js @@ -24,6 +24,7 @@ var shell = require('shelljs'), fs = require('fs'), util = require('../src/cordova/util'), events = require('../cordova-lib').events, + helpers = require('./helpers'), temp = path.join(__dirname, '..', 'temp'); var cwd = process.cwd(); @@ -143,6 +144,24 @@ describe('util module', function() { expect(res.indexOf('atari')).toEqual(-1); }); }); + describe('getInstalledPlatformsWithVersions method', function() { + afterEach(function() { + shell.rm('-rf', temp); + }); + it('should get the supported platforms in the cordova project dir along with their reported versions', function(done) { + var platforms = path.join(temp, 'platforms'); + var android = path.join(platforms, 'android'); + + shell.mkdir('-p', android); + + shell.cp('-R', + path.join(__dirname, 'fixtures', 'platforms', helpers.testPlatform), platforms); + util.getInstalledPlatformsWithVersions(temp) + .then(function(platformMap) { + expect(platformMap['android']).toBe('3.1.0'); + }).fin(done); + }); + }); describe('findPlugins method', function() { afterEach(function() { shell.rm('-rf', temp); http://git-wip-us.apache.org/repos/asf/cordova-lib/blob/45a235fa/cordova-lib/src/cordova/platform.js ---------------------------------------------------------------------- diff --git a/cordova-lib/src/cordova/platform.js b/cordova-lib/src/cordova/platform.js index d130772..1b745be 100644 --- a/cordova-lib/src/cordova/platform.js +++ b/cordova-lib/src/cordova/platform.js @@ -492,26 +492,21 @@ function check(hooksRunner, projectRoot) { } function list(hooksRunner, projectRoot, opts) { - var platforms_on_fs = cordova_util.listPlatforms(projectRoot); return hooksRunner.fire('before_platform_ls', opts) .then(function() { - // Acquire the version number of each platform we have installed, and output that too. - return Q.all(platforms_on_fs.map(function(p) { - return superspawn.maybeSpawn(path.join(projectRoot, 'platforms', p, 'cordova', 'version'), [], { chmod: true }) - .then(function(v) { - if (!v) return p; - return p + ' ' + v; - }, function(v) { - return p + ' broken'; - }); - })); - }).then(function(platformsText) { + return cordova_util.getInstalledPlatformsWithVersions(projectRoot); + }).then(function(platformMap) { + var platformsText = []; + for (var plat in platformMap) { + platformsText.push(platformMap[plat] ? plat + ' ' + platformMap[plat] : plat); + } + platformsText = addDeprecatedInformationToPlatforms(platformsText); var results = 'Installed platforms:\n ' + platformsText.sort().join('\n ') + '\n'; var available = Object.keys(platforms).filter(hostSupports); available = available.filter(function(p) { - return platforms_on_fs.indexOf(p) < 0; // Only those not already installed. + return !platformMap[p]; // Only those not already installed. }); available = available.map(function (p){ http://git-wip-us.apache.org/repos/asf/cordova-lib/blob/45a235fa/cordova-lib/src/cordova/plugin.js ---------------------------------------------------------------------- diff --git a/cordova-lib/src/cordova/plugin.js b/cordova-lib/src/cordova/plugin.js index 35819c3..c5ead29 100644 --- a/cordova-lib/src/cordova/plugin.js +++ b/cordova-lib/src/cordova/plugin.js @@ -31,10 +31,14 @@ var cordova_util = require('./util'), pluginMapper = require('cordova-registry-mapper').newToOld, 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 @@ -121,32 +125,7 @@ module.exports = function plugin(command, targets, opts) { target = target.substring(0, target.length - 1); } - var parts = target.split('@'); - var id = parts[0]; - var version = parts[1]; - - // If no version is specified, retrieve the version (or source) from config.xml - if (!version && !cordova_util.isUrl(id) && !cordova_util.isDirectory(id)) { - 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)) { - target = ver; - } else { - //if version exists from config.xml, use that - if(ver) { - target = ver ? (id + '@' + ver) : target; - } else { - //fetch pinned version from cordova-lib - var pinnedVer = pkgJson.cordovaPlugins[id]; - target = pinnedVer ? (id + '@' + pinnedVer) : target; - } - } - } - // Fetch the plugin first. - events.emit('verbose', 'Calling plugman.fetch on plugin "' + target + '"'); - var fetchOptions = { searchpath: searchPath, noregistry: opts.noregistry, @@ -157,7 +136,12 @@ module.exports = function plugin(command, targets, opts) { is_top_level: true }; - return plugman.raw.fetch(target, pluginPath, fetchOptions) + 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); }); @@ -305,6 +289,51 @@ module.exports = function plugin(command, targets, opts) { }); }; +function determinePluginTarget(projectRoot, cfg, target, fetchOptions) { + var parts = target.split('@'); + var id = parts[0]; + var version = parts[1]; + + if (version || cordova_util.isUrl(id) || cordova_util.isDirectory(id)) { + 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)) { + 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) { + // Fallback to pinned version if available + fetchVersion = fetchVersion || pkgJson.cordovaPlugins[id]; + return fetchVersion ? (id + '@' + fetchVersion) : target; + }); +} + +// Exporting for testing purposes +module.exports.getFetchVersion = getFetchVersion; + function validatePluginId(pluginId, installedPlugins) { if (installedPlugins.indexOf(pluginId) >= 0) { return pluginId; @@ -395,10 +424,7 @@ function list(projectRoot, hooksRunner, opts) { var pluginsList = []; return hooksRunner.fire('before_plugin_ls', opts) .then(function() { - 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); + return getInstalledPlugins(projectRoot); }) .then(function(plugins) { if (plugins.length === 0) { @@ -445,6 +471,13 @@ function list(projectRoot, hooksRunner, opts) { }); } +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; @@ -512,3 +545,234 @@ function versionString(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 or pinned 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 latest or pinned versions, 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 or pinned 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/45a235fa/cordova-lib/src/cordova/util.js ---------------------------------------------------------------------- diff --git a/cordova-lib/src/cordova/util.js b/cordova-lib/src/cordova/util.js index 2cba201..0f533f5 100644 --- a/cordova-lib/src/cordova/util.js +++ b/cordova-lib/src/cordova/util.js @@ -27,7 +27,8 @@ var fs = require('fs'), npm = require('npm'), nopt = require('nopt'), Q = require('q'), - semver = require('semver'); + semver = require('semver'), + superspawn = require('cordova-common').superspawn; // Global configuration paths var global_config_path = process.env['CORDOVA_HOME']; @@ -63,6 +64,7 @@ exports.isDirectory = isDirectory; exports.isUrl = isUrl; exports.getLatestMatchingNpmVersion = getLatestMatchingNpmVersion; exports.getAvailableNpmVersions = getAvailableNpmVersions; +exports.getInstalledPlatformsWithVersions = getInstalledPlatformsWithVersions; function isUrl(value) { var u = value && url.parse(value); @@ -185,6 +187,22 @@ function listPlatforms(project_dir) { }); } +function getInstalledPlatformsWithVersions(project_dir) { + var result = {}; + var platforms_on_fs = listPlatforms(project_dir); + + return Q.all(platforms_on_fs.map(function(p) { + return superspawn.maybeSpawn(path.join(project_dir, 'platforms', p, 'cordova', 'version'), [], { chmod: true }) + .then(function(v) { + result[p] = v || null; + }, function(v) { + result[p] = 'broken'; + }); + })).then(function() { + return result; + }); +} + // list the directories in the path, ignoring any files function findPlugins(pluginPath) { var plugins = [], http://git-wip-us.apache.org/repos/asf/cordova-lib/blob/45a235fa/cordova-lib/src/plugman/registry/registry.js ---------------------------------------------------------------------- diff --git a/cordova-lib/src/plugman/registry/registry.js b/cordova-lib/src/plugman/registry/registry.js index fd633ab..08562d8 100644 --- a/cordova-lib/src/plugman/registry/registry.js +++ b/cordova-lib/src/plugman/registry/registry.js @@ -89,20 +89,18 @@ module.exports = { */ info: function(plugin) { plugin = plugin.shift(); - return (Q.nbind(npm.load, npm)) - .then(function() { - // Set cache timout limits to 0 to force npm to call the registry - // even when it has a recent .cache.json file. - npm.config.set('cache-min', 0); - npm.config.set('cache-max', 0); - return Q.ninvoke(npm.commands, 'view', [plugin], /* silent = */ true ); - }) - .then(function(info) { - // Plugin info should be accessed as info[version]. If a version - // specifier like >=x.y.z was used when calling npm view, info - // can contain several versions, but we take the first one here. - var version = Object.keys(info)[0]; - return info[version]; + return npmhelper.loadWithSettingsThenRestore({ + 'cache-min': 0, + 'cache-max': 0 + }, function() { + return Q.ninvoke(npm.commands, 'view', [plugin], /* silent = */ true ) + .then(function(info) { + // Plugin info should be accessed as info[version]. If a version + // specifier like >=x.y.z was used when calling npm view, info + // can contain several versions, but we take the first one here. + var version = Object.keys(info)[0]; + return info[version]; + }); }); } }; --------------------------------------------------------------------- To unsubscribe, e-mail: commits-unsubscr...@cordova.apache.org For additional commands, e-mail: commits-h...@cordova.apache.org