CB-10822 Manage plugins/modules metadata using PlatformJson
authorVladimir Kotikov <v-vlkoti@microsoft.com>
Thu, 10 Mar 2016 11:03:11 +0000 (14:03 +0300)
committerVladimir Kotikov <v-vlkoti@microsoft.com>
Wed, 30 Mar 2016 12:33:40 +0000 (15:33 +0300)
package.json
spec/PlatformJson.spec.js [new file with mode: 0644]
src/PlatformJson.js

index 20c4916..6c5e1f2 100644 (file)
     "plist": "^1.2.0",
     "q": "^1.4.1",
     "semver": "^5.0.1",
-    "shelljs": "^0.5.1",
+    "shelljs": "^0.5.3",
     "underscore": "^1.8.3",
     "unorm": "^1.3.3"
   },
   "devDependencies": {
     "istanbul": "^0.3.17",
     "jasmine-node": "^1.14.5",
-    "jshint": "^2.8.0"
+    "jshint": "^2.8.0",
+    "rewire": "^2.5.1"
   },
   "contributors": []
 }
diff --git a/spec/PlatformJson.spec.js b/spec/PlatformJson.spec.js
new file mode 100644 (file)
index 0000000..c9310a2
--- /dev/null
@@ -0,0 +1,160 @@
+/**\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 rewire = require('rewire');\r
+var PlatformJson = rewire('../src/PlatformJson');\r
+var ModuleMetadata = PlatformJson.__get__('ModuleMetadata');\r
+\r
+var FAKE_MODULE = {\r
+    name: 'fakeModule',\r
+    src: 'www/fakeModule.js',\r
+    clobbers: [{target: 'window.fakeClobber'}],\r
+    merges: [{target: 'window.fakeMerge'}],\r
+    runs: true\r
+};\r
+\r
+describe('PlatformJson class', function() {\r
+    it('should be constructable', function () {\r
+        expect(new PlatformJson()).toEqual(jasmine.any(PlatformJson));\r
+    });\r
+\r
+    describe('instance', function () {\r
+        var platformJson;\r
+        var fakePlugin;\r
+        \r
+        beforeEach(function () {\r
+            platformJson = new PlatformJson('/fake/path', 'android');\r
+            fakePlugin = jasmine.createSpyObj('fakePlugin', ['getJsModules']);\r
+            fakePlugin.id = 'fakeId';\r
+            fakePlugin.version = '1.0.0';\r
+            fakePlugin.getJsModules.andReturn([FAKE_MODULE]);\r
+        });\r
+        \r
+        describe('addPluginMetadata method', function () {\r
+            it('should not throw if root "modules" property is missing', function () {\r
+                expect(function () {\r
+                    platformJson.addPluginMetadata(fakePlugin);\r
+                }).not.toThrow();\r
+            });\r
+    \r
+            it('should add each module to "root.modules" array', function () {\r
+                platformJson.addPluginMetadata(fakePlugin);\r
+                expect(platformJson.root.modules.length).toBe(1);\r
+                expect(platformJson.root.modules[0]).toEqual(jasmine.any(ModuleMetadata));\r
+            });\r
+            \r
+            it('shouldn\'t add module if there is already module with the same file added', function () {\r
+                platformJson.root.modules = [{\r
+                    name: 'fakePlugin2',\r
+                    file: 'plugins/fakeId/www/fakeModule.js'\r
+                }];\r
+                \r
+                platformJson.addPluginMetadata(fakePlugin);\r
+                expect(platformJson.root.modules.length).toBe(1);\r
+                expect(platformJson.root.modules[0].name).toBe('fakePlugin2');\r
+            });\r
+            \r
+            it('should add entry to plugin_metadata with corresponding version', function () {\r
+                platformJson.addPluginMetadata(fakePlugin);\r
+                expect(platformJson.root.plugin_metadata[fakePlugin.id]).toBe(fakePlugin.version);\r
+            });\r
+        });\r
+        \r
+        describe('removePluginMetadata method', function () {\r
+            it('should not throw if root "modules" property is missing', function () {\r
+                expect(function () {\r
+                    platformJson.removePluginMetadata(fakePlugin);\r
+                }).not.toThrow();\r
+            });\r
+    \r
+            it('should remove plugin modules from "root.modules" array based on file path', function () {\r
+                \r
+                var pluginPaths = [\r
+                    'plugins/fakeId/www/fakeModule.js',\r
+                    'plugins/otherPlugin/www/module1.js',\r
+                    'plugins/otherPlugin/www/module1.js'\r
+                ];\r
+                \r
+                platformJson.root.modules = pluginPaths.map(function (p) { return {file: p}; });\r
+                platformJson.removePluginMetadata(fakePlugin);\r
+                var resultantPaths = platformJson.root.modules\r
+                    .map(function (p) { return p.file; })\r
+                    .filter(function (f) { return /fakeModule\.js$/.test(f); });\r
+                   \r
+                expect(resultantPaths.length).toBe(0);\r
+            });\r
+            \r
+            it('should remove entry from plugin_metadata with corresponding version', function () {\r
+                platformJson.root.plugin_metadata = {};\r
+                platformJson.root.plugin_metadata[fakePlugin.id] = fakePlugin.version;\r
+                platformJson.removePluginMetadata(fakePlugin);\r
+                expect(platformJson.root.plugin_metadata[fakePlugin.id]).not.toBeDefined();\r
+            });\r
+        });\r
+        \r
+        describe('generateMetadata method', function () {\r
+            it('should generate text metadata containing list of installed modules', function () {\r
+                var meta = platformJson.addPluginMetadata(fakePlugin).generateMetadata();\r
+                expect(typeof meta).toBe('string');\r
+                expect(meta.indexOf(JSON.stringify(platformJson.root.modules, null, 4))).toBeGreaterThan(0);\r
+                // expect(meta).toMatch(JSON.stringify(platformJson.root.modules, null, 4));\r
+                expect(meta).toMatch(JSON.stringify(platformJson.root.plugin_metadata, null, 4));\r
+            });\r
+        });\r
+    });\r
+});\r
+\r
+describe('ModuleMetadata class', function () {\r
+    it('should be constructable', function () {\r
+        var meta;\r
+        expect(function name(params) {\r
+            meta = new ModuleMetadata('fakePlugin', {src: 'www/fakeModule.js'});\r
+        }).not.toThrow();\r
+        expect(meta instanceof ModuleMetadata).toBeTruthy();\r
+    });\r
+    \r
+    it('should throw if either pluginId or jsModule argument isn\'t specified', function () {\r
+        expect(ModuleMetadata).toThrow();\r
+        expect(function () { new ModuleMetadata('fakePlugin', {}); }).toThrow();\r
+    });\r
+    \r
+    it('should guess module id either from name property of from module src', function () {\r
+        expect(new ModuleMetadata('fakePlugin', {name: 'fakeModule'}).id).toMatch(/fakeModule$/);\r
+        expect(new ModuleMetadata('fakePlugin', {src: 'www/fakeModule.js'}).id).toMatch(/fakeModule$/);\r
+    });\r
+    \r
+    it('should read "clobbers" property from module', function () {\r
+        expect(new ModuleMetadata('fakePlugin', {name: 'fakeModule'}).clobbers).not.toBeDefined();\r
+        var metadata = new ModuleMetadata('fakePlugin', FAKE_MODULE);\r
+        expect(metadata.clobbers).toEqual(jasmine.any(Array));\r
+        expect(metadata.clobbers[0]).toBe(FAKE_MODULE.clobbers[0].target);\r
+    });\r
+    \r
+    it('should read "merges" property from module', function () {\r
+        expect(new ModuleMetadata('fakePlugin', {name: 'fakeModule'}).merges).not.toBeDefined();\r
+        var metadata = new ModuleMetadata('fakePlugin', FAKE_MODULE);\r
+        expect(metadata.merges).toEqual(jasmine.any(Array));\r
+        expect(metadata.merges[0]).toBe(FAKE_MODULE.merges[0].target);\r
+    });\r
+    \r
+    it('should read "runs" property from module', function () {\r
+        expect(new ModuleMetadata('fakePlugin', {name: 'fakeModule'}).runs).not.toBeDefined();\r
+        expect(new ModuleMetadata('fakePlugin', FAKE_MODULE).runs).toBe(true);\r
+    });\r
+});\r
index 793e976..4e2b287 100644 (file)
@@ -91,6 +91,38 @@ PlatformJson.prototype.addPlugin = function(pluginId, variables, isTopLevel) {
     return this;
 };
 
+/**
+ * @chaining
+ * Generates and adds metadata for provided plugin into associated <platform>.json file
+ *
+ * @param   {PluginInfo}  pluginInfo  A pluginInfo instance to add metadata from
+ * @returns {this} Current PlatformJson instance to allow calls chaining
+ */
+PlatformJson.prototype.addPluginMetadata = function (pluginInfo) {
+
+    var installedModules = this.root.modules || [];
+
+    var installedPaths = installedModules.map(function (installedModule) {
+        return installedModule.file;
+    });
+
+    var modulesToInstall = pluginInfo.getJsModules(this.platform)
+    .map(function (module) {
+        return new ModuleMetadata(pluginInfo.id, module);
+    })
+    .filter(function (metadata) {
+        // Filter out modules which are already added to metadata
+        return installedPaths.indexOf(metadata.file) === -1;
+    });
+
+    this.root.modules = installedModules.concat(modulesToInstall);
+
+    this.root.plugin_metadata = this.root.plugin_metadata || {};
+    this.root.plugin_metadata[pluginInfo.id] = pluginInfo.version;
+
+    return this;
+};
+
 PlatformJson.prototype.removePlugin = function(pluginId, isTopLevel) {
     var pluginsList = isTopLevel ?
         this.root.installed_plugins :
@@ -101,6 +133,35 @@ PlatformJson.prototype.removePlugin = function(pluginId, isTopLevel) {
     return this;
 };
 
+/**
+ * @chaining
+ * Removes metadata for provided plugin from associated file
+ *
+ * @param   {PluginInfo}  pluginInfo A PluginInfo instance to which modules' metadata
+ *   we need to remove
+ *
+ * @returns {this} Current PlatformJson instance to allow calls chaining
+ */
+PlatformJson.prototype.removePluginMetadata = function (pluginInfo) {
+    var modulesToRemove = pluginInfo.getJsModules(this.platform)
+    .map(function (jsModule) {
+        return  ['plugins', pluginInfo.id, jsModule.src].join('/');
+    });
+
+    var installedModules = this.root.modules || [];
+    this.root.modules = installedModules
+    .filter(function (installedModule) {
+        // Leave only those metadatas which 'file' is not in removed modules
+        return (modulesToRemove.indexOf(installedModule.file) === -1);
+    });
+
+    if (this.root.plugin_metadata) {
+        delete this.root.plugin_metadata[pluginInfo.id];
+    }
+
+    return this;
+};
+
 PlatformJson.prototype.addInstalledPluginToPrepareQueue = function(pluginDirName, vars, is_top_level) {
     this.root.prepare_queue.installed.push({'plugin':pluginDirName, 'vars':vars, 'topLevel':is_top_level});
 };
@@ -125,6 +186,39 @@ PlatformJson.prototype.makeTopLevel = function(pluginId) {
     return this;
 };
 
+/**
+ * Generates a metadata for all installed plugins and js modules. The resultant
+ *   string is ready to be written to 'cordova_plugins.js'
+ *
+ * @returns {String} cordova_plugins.js contents
+ */
+PlatformJson.prototype.generateMetadata = function () {
+    return [
+        'cordova.define(\'cordova/plugin_list\', function(require, exports, module) {',
+        'module.exports = ' + JSON.stringify(this.root.modules, null, 4) + ';',
+        'module.exports.metadata = ',
+        '// TOP OF METADATA',
+        JSON.stringify(this.root.plugin_metadata, null, 4) + ';',
+        '// BOTTOM OF METADATA',
+        '});' // Close cordova.define.
+    ].join('\n');
+};
+
+/**
+ * @chaining
+ * Generates and then saves metadata to specified file. Doesn't check if file exists.
+ *
+ * @param {String} destination  File metadata will be written to
+ * @return {PlatformJson} PlatformJson instance
+ */
+PlatformJson.prototype.generateAndSaveMetadata = function (destination) {
+    var meta = this.generateMetadata();
+    shelljs.mkdir('-p', path.dirname(destination));
+    fs.writeFileSync(destination, meta, 'utf-8');
+
+    return this;
+};
+
 // convert a munge from the old format ([file][parent][xml] = count) to the current one
 function fix_munge(root) {
     root.prepare_queue = root.prepare_queue || {installed:[], uninstalled:[]};
@@ -151,5 +245,35 @@ function fix_munge(root) {
     return root;
 }
 
+/**
+ * @constructor
+ * @class ModuleMetadata
+ *
+ * Creates a ModuleMetadata object that represents module entry in 'cordova_plugins.js'
+ *   file at run time
+ *
+ * @param {String}  pluginId  Plugin id where this module installed from
+ * @param (JsModule|Object)  jsModule  A js-module entry from PluginInfo class to generate metadata for
+ */
+function ModuleMetadata (pluginId, jsModule) {
+
+    if (!pluginId) throw new TypeError('pluginId argument must be a valid plugin id');
+    if (!jsModule.src && !jsModule.name) throw new TypeError('jsModule argument must contain src or/and name properties');
+
+    this.id  = pluginId + '.' + ( jsModule.name || jsModule.src.match(/([^\/]+)\.js/)[1] );
+    this.file = ['plugins', pluginId, jsModule.src].join('/');
+    this.pluginId = pluginId;
+
+    if (jsModule.clobbers && jsModule.clobbers.length > 0) {
+        this.clobbers = jsModule.clobbers.map(function(o) { return o.target; });
+    }
+    if (jsModule.merges && jsModule.merges.length > 0) {
+        this.merges = jsModule.merges.map(function(o) { return o.target; });
+    }
+    if (jsModule.runs) {
+        this.runs = true;
+    }
+}
+
 module.exports = PlatformJson;