CB-10833 Deduplicate common logic for plugin installation/uninstallation
authorVladimir Kotikov <v-vlkoti@microsoft.com>
Wed, 9 Mar 2016 19:26:05 +0000 (22:26 +0300)
committerVladimir Kotikov <v-vlkoti@microsoft.com>
Wed, 30 Mar 2016 14:32:28 +0000 (17:32 +0300)
 This closes #414

cordova-common.js
package.json
spec/PluginManager.spec.js [new file with mode: 0644]
src/PluginManager.js [new file with mode: 0644]

index 22e90a7..1f97b66 100644 (file)
@@ -17,9 +17,6 @@
     under the License.
 */
 
-/* jshint node:true */
-
-// For now expose plugman and cordova just as they were in the old repos
 exports = module.exports = {
     events: require('./src/events'),
     superspawn: require('./src/superspawn'),
@@ -33,6 +30,8 @@ exports = module.exports = {
 
     PluginInfo: require('./src/PluginInfo/PluginInfo.js'),
     PluginInfoProvider: require('./src/PluginInfo/PluginInfoProvider.js'),
+    
+    PluginManager: require('./src/PluginManager'),
 
     ConfigChanges: require('./src/ConfigChanges/ConfigChanges.js'),
     ConfigKeeper: require('./src/ConfigChanges/ConfigKeeper.js'),
index 6c5e1f2..5e4b89a 100644 (file)
@@ -41,6 +41,7 @@
     "istanbul": "^0.3.17",
     "jasmine-node": "^1.14.5",
     "jshint": "^2.8.0",
+    "promise-matchers": "^0.9.6",
     "rewire": "^2.5.1"
   },
   "contributors": []
diff --git a/spec/PluginManager.spec.js b/spec/PluginManager.spec.js
new file mode 100644 (file)
index 0000000..e99ab05
--- /dev/null
@@ -0,0 +1,94 @@
+/**\r
+    Licensed to the Apache Software Foundation (ASF) under one\r
+    or more contributor license agreements.  See the NOTICE file\r
+    distributed with this work for additional information\r
+    regarding copyright ownership.  The ASF licenses this file\r
+    to you under the Apache License, Version 2.0 (the\r
+    "License"); you may not use this file except in compliance\r
+    with the License.  You may obtain a copy of the License at\r
+\r
+    http://www.apache.org/licenses/LICENSE-2.0\r
+\r
+    Unless required by applicable law or agreed to in writing,\r
+    software distributed under the License is distributed on an\r
+    "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\r
+    KIND, either express or implied.  See the License for the\r
+    specific language governing permissions and limitations\r
+    under the License.\r
+*/\r
+\r
+require ('promise-matchers');\r
+\r
+var Q = require('q');\r
+var path = require('path');\r
+var rewire = require('rewire');\r
+var PlatformJson = require('../src/PlatformJson');\r
+var PluginManager = rewire('../src/PluginManager');\r
+var PluginInfo = require('../src/PluginInfo/PluginInfo');\r
+var ConfigChanges = require('../src/ConfigChanges/ConfigChanges');\r
+\r
+var DUMMY_PLUGIN = path.join(__dirname, 'fixtures/plugins/org.test.plugins.dummyplugin');\r
+var FAKE_PLATFORM = 'cordova-atari';\r
+var FAKE_LOCATIONS = {\r
+    root: '/some/fake/path'\r
+};\r
+\r
+describe('PluginManager class', function() {\r
+\r
+    beforeEach(function () {\r
+        spyOn(PlatformJson, 'load');\r
+        spyOn(ConfigChanges, 'PlatformMunger');\r
+    });\r
+\r
+    it('should be constructable', function () {\r
+        expect(new PluginManager(FAKE_PLATFORM, FAKE_LOCATIONS)).toEqual(jasmine.any(PluginManager));\r
+    });\r
+\r
+    it('should return new instance for every PluginManager.get call', function () {\r
+        expect(PluginManager.get(FAKE_PLATFORM, FAKE_LOCATIONS)).toEqual(jasmine.any(PluginManager));\r
+        expect(PluginManager.get(FAKE_PLATFORM, FAKE_LOCATIONS))\r
+            .not.toBe(PluginManager.get(FAKE_PLATFORM, FAKE_LOCATIONS));\r
+    });\r
+\r
+    describe('instance', function () {\r
+        var actions, manager;\r
+        var FAKE_PROJECT;\r
+        var ActionStackOrig = PluginManager.__get__('ActionStack');\r
+\r
+        beforeEach(function () {\r
+            FAKE_PROJECT = jasmine.createSpyObj('project', ['getInstaller', 'getUninstaller', 'write']);\r
+            manager = new PluginManager('windows', FAKE_LOCATIONS, FAKE_PROJECT);\r
+            actions = jasmine.createSpyObj('actions', ['createAction', 'push', 'process']);\r
+            actions.process.andReturn(Q.resolve());\r
+            PluginManager.__set__('ActionStack', function () { return actions; });\r
+        });\r
+\r
+        afterEach(function () {\r
+            PluginManager.__set__('ActionStack', ActionStackOrig);\r
+        });\r
+\r
+        describe('addPlugin method', function () {\r
+            it('should return a promise', function () {\r
+                expect(Q.isPromise(manager.addPlugin(null, {}))).toBe(true);\r
+            });\r
+\r
+            it('should reject if "plugin" parameter is not specified or not a PluginInfo instance', function () {\r
+                expect(manager.addPlugin(null, {})).toHaveBeenRejected();\r
+                expect(manager.addPlugin({}, {})).toHaveBeenRejected();\r
+                expect(manager.addPlugin(new PluginInfo(DUMMY_PLUGIN), {})).not.toHaveBeenRejected();\r
+            });\r
+\r
+            it('should iterate through all plugin\'s files and frameworks', function () {\r
+                manager.addPlugin(new PluginInfo(DUMMY_PLUGIN), {})\r
+                .then(function () {\r
+                    expect(FAKE_PROJECT.getInstaller.calls.length).toBe(16);\r
+                    expect(FAKE_PROJECT.getUninstaller.calls.length).toBe(16);\r
+\r
+                    expect(actions.push.calls.length).toBe(16);\r
+                    expect(actions.process).toHaveBeenCalled();\r
+                    expect(FAKE_PROJECT.write).toHaveBeenCalled();\r
+                });\r
+            });\r
+        });\r
+    });\r
+});\r
diff --git a/src/PluginManager.js b/src/PluginManager.js
new file mode 100644 (file)
index 0000000..ea4d688
--- /dev/null
@@ -0,0 +1,148 @@
+/*\r
+       Licensed to the Apache Software Foundation (ASF) under one\r
+       or more contributor license agreements.  See the NOTICE file\r
+       distributed with this work for additional information\r
+       regarding copyright ownership.  The ASF licenses this file\r
+       to you under the Apache License, Version 2.0 (the\r
+       "License"); you may not use this file except in compliance\r
+       with the License.  You may obtain a copy of the License at\r
+\r
+         http://www.apache.org/licenses/LICENSE-2.0\r
+\r
+       Unless required by applicable law or agreed to in writing,\r
+       software distributed under the License is distributed on an\r
+       "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\r
+       KIND, either express or implied.  See the License for the\r
+       specific language governing permissions and limitations\r
+       under the License.\r
+*/\r
+\r
+var Q = require('q');\r
+var fs = require('fs');\r
+var path = require('path');\r
+\r
+var ActionStack = require('./ActionStack');\r
+var PlatformJson = require('./PlatformJson');\r
+var CordovaError = require('./CordovaError/CordovaError');\r
+var PlatformMunger = require('./ConfigChanges/ConfigChanges').PlatformMunger;\r
+var PluginInfoProvider = require('./PluginInfo/PluginInfoProvider');\r
+\r
+/**\r
+ * @constructor\r
+ * @class PluginManager\r
+ * Represents an entity for adding/removing plugins for platforms\r
+ *\r
+ * @param {String} platform Platform name\r
+ * @param {Object} locations - Platform files and directories\r
+ * @param {IDEProject} ideProject The IDE project to add/remove plugin changes to/from\r
+ */\r
+function PluginManager(platform, locations, ideProject) {\r
+    this.platform = platform;\r
+    this.locations = locations;\r
+    this.project = ideProject;\r
+\r
+    var platformJson = PlatformJson.load(locations.root, platform);\r
+    this.munger = new PlatformMunger(platform, locations.root, platformJson, new PluginInfoProvider());\r
+}\r
+\r
+\r
+/**\r
+ * @constructs PluginManager\r
+ * A convenience shortcut to new PluginManager(...)\r
+ *\r
+ * @param {String} platform Platform name\r
+ * @param {Object} locations - Platform files and directories\r
+ * @param {IDEProject} ideProject The IDE project to add/remove plugin changes to/from\r
+ * @returns new PluginManager instance\r
+ */\r
+PluginManager.get = function(platform, locations, ideProject) {\r
+    return new PluginManager(platform, locations, ideProject);\r
+};\r
+\r
+PluginManager.INSTALL = 'install';\r
+PluginManager.UNINSTALL = 'uninstall';\r
+\r
+module.exports = PluginManager;\r
+\r
+/**\r
+ * Describes and implements common plugin installation/uninstallation routine. The flow is the following:\r
+ *  * Validate and set defaults for options. Note that options are empty by default. Everything\r
+ *    needed for platform IDE project must be passed from outside. Plugin variables (which\r
+ *    are the part of the options) also must be already populated with 'PACKAGE_NAME' variable.\r
+ *  * Collect all plugin's native and web files, get installers/uninstallers and process\r
+ *    all these via ActionStack.\r
+ *  * Save the IDE project, so the changes made by installers are persisted.\r
+ *  * Generate config changes munge for plugin and apply it to all required files\r
+ *  * Generate metadata for plugin and plugin modules and save it to 'cordova_plugins.js'\r
+ *\r
+ * @param {PluginInfo} plugin A PluginInfo structure representing plugin to install\r
+ * @param {Object} [options={}] An installation options. It is expected but is not necessary\r
+ *   that options would contain 'variables' inner object with 'PACKAGE_NAME' field set by caller.\r
+ *\r
+ * @returns {Promise} Returns a Q promise, either resolved in case of success, rejected otherwise.\r
+ */\r
+PluginManager.prototype.doOperation = function (operation, plugin, options) {\r
+    if (operation !== PluginManager.INSTALL && operation !== PluginManager.UNINSTALL)\r
+        return Q.reject(new CordovaError('The parameter is incorrect. The opeation must be either "add" or "remove"'));\r
+\r
+    if (!plugin || plugin.constructor.name !== 'PluginInfo')\r
+        return Q.reject(new CordovaError('The parameter is incorrect. The first parameter should be a PluginInfo instance'));\r
+\r
+    // Set default to empty object to play safe when accesing properties\r
+    options = options || {};\r
+\r
+    var self = this;\r
+    var actions = new ActionStack();\r
+\r
+    // gather all files need to be handled during operation ...\r
+    plugin.getFilesAndFrameworks(this.platform)\r
+        .concat(plugin.getAssets(this.platform))\r
+        .concat(plugin.getJsModules(this.platform))\r
+    // ... put them into stack ...\r
+    .forEach(function(item) {\r
+        var installer = self.project.getInstaller(item.itemType);\r
+        var uninstaller = self.project.getUninstaller(item.itemType);\r
+        var actionArgs = [item, plugin, self.project, options];\r
+\r
+        var action;\r
+        if (operation === PluginManager.INSTALL) {\r
+            action = actions.createAction.apply(actions, [installer, actionArgs, uninstaller, actionArgs]);\r
+        } else /* op === PluginManager.UNINSTALL */{\r
+            action = actions.createAction.apply(actions, [uninstaller, actionArgs, installer, actionArgs]);\r
+        }\r
+        actions.push(action);\r
+    });\r
+\r
+    // ... and run through the action stack\r
+    return actions.process(this.platform)\r
+    .then(function () {\r
+        if (self.project.write) {\r
+            self.project.write();\r
+        }\r
+\r
+        if (operation === PluginManager.INSTALL) {\r
+            // Ignore passed `is_top_level` option since platform itself doesn't know\r
+            // anything about managing dependencies - it's responsibility of caller.\r
+            self.munger.add_plugin_changes(plugin, options.variables, /*is_top_level=*/true, /*should_increment=*/true);\r
+            self.munger.platformJson.addPluginMetadata(plugin);\r
+        } else {\r
+            self.munger.remove_plugin_changes(plugin, /*is_top_level=*/true);\r
+            self.munger.platformJson.removePluginMetadata(plugin);\r
+        }\r
+\r
+        // Save everything (munge and plugin/modules metadata)\r
+        self.munger.save_all();\r
+\r
+        var targetDir = options.usePlatformWww ? self.locations.platformWww : self.locations.www;\r
+        fs.writeFileSync(path.join(targetDir, 'cordova_plugins.js'),\r
+            self.munger.platformJson.generateMetadata(), 'utf-8');\r
+    });\r
+};\r
+\r
+PluginManager.prototype.addPlugin = function (plugin, installOptions) {\r
+    return this.doOperation(PluginManager.INSTALL, plugin, installOptions);\r
+};\r
+\r
+PluginManager.prototype.removePlugin = function (plugin, uninstallOptions) {\r
+    return this.doOperation(PluginManager.UNINSTALL, plugin, uninstallOptions);\r
+};\r