Normalize whitespace (#46)

diff --git a/.eslintignore b/.eslintignore
index 161d0c6..d606f61 100644
--- a/.eslintignore
+++ b/.eslintignore
@@ -1 +1 @@
-spec/fixtures/*
\ No newline at end of file
+spec/fixtures/*
diff --git a/RELEASENOTES.md b/RELEASENOTES.md
index 409fc6f..456f48b 100644
--- a/RELEASENOTES.md
+++ b/RELEASENOTES.md
@@ -35,7 +35,7 @@
 * [CB-13674](https://issues.apache.org/jira/browse/CB-13674): updated dependencies
 
 ### 2.2.0 (Nov 22, 2017)
-* [CB-13471](https://issues.apache.org/jira/browse/CB-13471) File Provider fix belongs in cordova-common 
+* [CB-13471](https://issues.apache.org/jira/browse/CB-13471) File Provider fix belongs in cordova-common
 * [CB-11244](https://issues.apache.org/jira/browse/CB-11244) Spot fix for upcoming `cordova-android@7` changes. https://github.com/apache/cordova-android/pull/389
 
 ### 2.1.1 (Oct 04, 2017)
diff --git a/spec/.eslintrc.yml b/spec/.eslintrc.yml
index 6afba65..043fd14 100644
--- a/spec/.eslintrc.yml
+++ b/spec/.eslintrc.yml
@@ -1,2 +1,2 @@
 env:
-    jasmine: true
\ No newline at end of file
+    jasmine: true
diff --git a/spec/PlatformJson.spec.js b/spec/PlatformJson.spec.js
index d2c3c7e..f6845bd 100644
--- a/spec/PlatformJson.spec.js
+++ b/spec/PlatformJson.spec.js
@@ -1,190 +1,190 @@
-/**

-    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 rewire = require('rewire');

-var PlatformJson = rewire('../src/PlatformJson');

-var ModuleMetadata = PlatformJson.__get__('ModuleMetadata');

-

-var FAKE_MODULE = {

-    name: 'fakeModule',

-    src: 'www/fakeModule.js',

-    clobbers: [{target: 'window.fakeClobber'}],

-    merges: [{target: 'window.fakeMerge'}],

-    runs: true

-};

-

-describe('PlatformJson class', function () {

-    it('Test 001 : should be constructable', function () {

-        expect(new PlatformJson()).toEqual(jasmine.any(PlatformJson));

-    });

-

-    describe('instance', function () {

-        var platformJson;

-        var fakePlugin;

-

-        beforeEach(function () {

-            platformJson = new PlatformJson('/fake/path', 'android');

-            fakePlugin = jasmine.createSpyObj('fakePlugin', ['getJsModules']);

-            fakePlugin.id = 'fakeId';

-            fakePlugin.version = '1.0.0';

-            fakePlugin.getJsModules.and.returnValue([FAKE_MODULE]);

-        });

-

-        describe('addPluginMetadata method', function () {

-            it('Test 002 : should not throw if root "modules" property is missing', function () {

-                expect(function () {

-                    platformJson.addPluginMetadata(fakePlugin);

-                }).not.toThrow();

-            });

-

-            it('Test 003 : should add each module to "root.modules" array', function () {

-                platformJson.addPluginMetadata(fakePlugin);

-                expect(platformJson.root.modules.length).toBe(1);

-                expect(platformJson.root.modules[0]).toEqual(jasmine.any(ModuleMetadata));

-            });

-

-            it('Test 004 : shouldn\'t add module if there is already module with the same file added', function () {

-                platformJson.root.modules = [{

-                    name: 'fakePlugin2',

-                    file: 'plugins/fakeId/www/fakeModule.js'

-                }];

-

-                platformJson.addPluginMetadata(fakePlugin);

-                expect(platformJson.root.modules.length).toBe(1);

-                expect(platformJson.root.modules[0].name).toBe('fakePlugin2');

-            });

-

-            it('Test 005 : should add entry to plugin_metadata with corresponding version', function () {

-                platformJson.addPluginMetadata(fakePlugin);

-                expect(platformJson.root.plugin_metadata[fakePlugin.id]).toBe(fakePlugin.version);

-            });

-        });

-

-        describe('removePluginMetadata method', function () {

-            it('Test 006 : should not throw if root "modules" property is missing', function () {

-                expect(function () {

-                    platformJson.removePluginMetadata(fakePlugin);

-                }).not.toThrow();

-            });

-

-            it('Test 007 : should remove plugin modules from "root.modules" array based on file path', function () {

-

-                var pluginPaths = [

-                    'plugins/fakeId/www/fakeModule.js',

-                    'plugins/otherPlugin/www/module1.js',

-                    'plugins/otherPlugin/www/module1.js'

-                ];

-

-                platformJson.root.modules = pluginPaths.map(function (p) { return {file: p}; });

-                platformJson.removePluginMetadata(fakePlugin);

-                var resultantPaths = platformJson.root.modules

-                    .map(function (p) { return p.file; })

-                    .filter(function (f) { return /fakeModule\.js$/.test(f); });

-

-                expect(resultantPaths.length).toBe(0);

-            });

-

-            it('Test 008 : should remove entry from plugin_metadata with corresponding version', function () {

-                platformJson.root.plugin_metadata = {};

-                platformJson.root.plugin_metadata[fakePlugin.id] = fakePlugin.version;

-                platformJson.removePluginMetadata(fakePlugin);

-                expect(platformJson.root.plugin_metadata[fakePlugin.id]).not.toBeDefined();

-            });

-        });

-

-        function evaluateCordovaDefineStatement (str) {

-            expect(typeof str).toBe('string');

-            const fnString = str.replace(/^\s*cordova\.define\('cordova\/plugin_list',\s*([\s\S]+)\);\s*$/, '($1)');

-            const mod = {exports: {}};

-            global.eval(fnString)(null, mod.exports, mod); // eslint-disable-line no-eval

-            return mod;

-        }

-

-        function expectedMetadata () {

-            // Create plain objects from ModuleMetadata instances

-            const modules = platformJson.root.modules.map(o => Object.assign({}, o));

-            modules.metadata = platformJson.root.plugin_metadata;

-            return modules;

-        }

-

-        describe('generateMetadata method', function () {

-            it('Test 009 : should generate text metadata containing list of installed modules', function () {

-                const meta = platformJson.addPluginMetadata(fakePlugin).generateMetadata();

-                const mod = evaluateCordovaDefineStatement(meta);

-

-                expect(mod.exports).toEqual(expectedMetadata());

-            });

-        });

-

-        describe('generateAndSaveMetadata method', function () {

-            it('should save generated metadata', function () {

-                // Needs to use graceful-fs, since that is used by fs-extra

-                const spy = spyOn(require('graceful-fs'), 'writeFileSync');

-

-                const dest = require('path').join(__dirname, 'test-destination');

-                platformJson.addPluginMetadata(fakePlugin).generateAndSaveMetadata(dest);

-

-                expect(spy).toHaveBeenCalledTimes(1);

-                const [file, data] = spy.calls.argsFor(0);

-                expect(file).toBe(dest);

-                const mod = evaluateCordovaDefineStatement(data);

-                expect(mod.exports).toEqual(expectedMetadata());

-            });

-        });

-    });

-});

-

-describe('ModuleMetadata class', function () {

-    it('Test 010 : should be constructable', function () {

-        var meta;

-        expect(function () {

-            meta = new ModuleMetadata('fakePlugin', {src: 'www/fakeModule.js'});

-        }).not.toThrow();

-        expect(meta instanceof ModuleMetadata).toBeTruthy();

-    });

-

-    it('Test 011 : should throw if either pluginId or jsModule argument isn\'t specified', function () {

-        expect(ModuleMetadata).toThrow();

-        expect(function () { new ModuleMetadata('fakePlugin', {}); }).toThrow(); /* eslint no-new : 0 */

-    });

-

-    it('Test 012 : should guess module id either from name property of from module src', function () {

-        expect(new ModuleMetadata('fakePlugin', {name: 'fakeModule'}).id).toMatch(/fakeModule$/);

-        expect(new ModuleMetadata('fakePlugin', {src: 'www/fakeModule.js'}).id).toMatch(/fakeModule$/);

-    });

-

-    it('Test 013 : should read "clobbers" property from module', function () {

-        expect(new ModuleMetadata('fakePlugin', {name: 'fakeModule'}).clobbers).not.toBeDefined();

-        var metadata = new ModuleMetadata('fakePlugin', FAKE_MODULE);

-        expect(metadata.clobbers).toEqual(jasmine.any(Array));

-        expect(metadata.clobbers[0]).toBe(FAKE_MODULE.clobbers[0].target);

-    });

-

-    it('Test 014 : should read "merges" property from module', function () {

-        expect(new ModuleMetadata('fakePlugin', {name: 'fakeModule'}).merges).not.toBeDefined();

-        var metadata = new ModuleMetadata('fakePlugin', FAKE_MODULE);

-        expect(metadata.merges).toEqual(jasmine.any(Array));

-        expect(metadata.merges[0]).toBe(FAKE_MODULE.merges[0].target);

-    });

-

-    it('Test 015 : should read "runs" property from module', function () {

-        expect(new ModuleMetadata('fakePlugin', {name: 'fakeModule'}).runs).not.toBeDefined();

-        expect(new ModuleMetadata('fakePlugin', FAKE_MODULE).runs).toBe(true);

-    });

-});

+/**
+    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 rewire = require('rewire');
+var PlatformJson = rewire('../src/PlatformJson');
+var ModuleMetadata = PlatformJson.__get__('ModuleMetadata');
+
+var FAKE_MODULE = {
+    name: 'fakeModule',
+    src: 'www/fakeModule.js',
+    clobbers: [{target: 'window.fakeClobber'}],
+    merges: [{target: 'window.fakeMerge'}],
+    runs: true
+};
+
+describe('PlatformJson class', function () {
+    it('Test 001 : should be constructable', function () {
+        expect(new PlatformJson()).toEqual(jasmine.any(PlatformJson));
+    });
+
+    describe('instance', function () {
+        var platformJson;
+        var fakePlugin;
+
+        beforeEach(function () {
+            platformJson = new PlatformJson('/fake/path', 'android');
+            fakePlugin = jasmine.createSpyObj('fakePlugin', ['getJsModules']);
+            fakePlugin.id = 'fakeId';
+            fakePlugin.version = '1.0.0';
+            fakePlugin.getJsModules.and.returnValue([FAKE_MODULE]);
+        });
+
+        describe('addPluginMetadata method', function () {
+            it('Test 002 : should not throw if root "modules" property is missing', function () {
+                expect(function () {
+                    platformJson.addPluginMetadata(fakePlugin);
+                }).not.toThrow();
+            });
+
+            it('Test 003 : should add each module to "root.modules" array', function () {
+                platformJson.addPluginMetadata(fakePlugin);
+                expect(platformJson.root.modules.length).toBe(1);
+                expect(platformJson.root.modules[0]).toEqual(jasmine.any(ModuleMetadata));
+            });
+
+            it('Test 004 : shouldn\'t add module if there is already module with the same file added', function () {
+                platformJson.root.modules = [{
+                    name: 'fakePlugin2',
+                    file: 'plugins/fakeId/www/fakeModule.js'
+                }];
+
+                platformJson.addPluginMetadata(fakePlugin);
+                expect(platformJson.root.modules.length).toBe(1);
+                expect(platformJson.root.modules[0].name).toBe('fakePlugin2');
+            });
+
+            it('Test 005 : should add entry to plugin_metadata with corresponding version', function () {
+                platformJson.addPluginMetadata(fakePlugin);
+                expect(platformJson.root.plugin_metadata[fakePlugin.id]).toBe(fakePlugin.version);
+            });
+        });
+
+        describe('removePluginMetadata method', function () {
+            it('Test 006 : should not throw if root "modules" property is missing', function () {
+                expect(function () {
+                    platformJson.removePluginMetadata(fakePlugin);
+                }).not.toThrow();
+            });
+
+            it('Test 007 : should remove plugin modules from "root.modules" array based on file path', function () {
+
+                var pluginPaths = [
+                    'plugins/fakeId/www/fakeModule.js',
+                    'plugins/otherPlugin/www/module1.js',
+                    'plugins/otherPlugin/www/module1.js'
+                ];
+
+                platformJson.root.modules = pluginPaths.map(function (p) { return {file: p}; });
+                platformJson.removePluginMetadata(fakePlugin);
+                var resultantPaths = platformJson.root.modules
+                    .map(function (p) { return p.file; })
+                    .filter(function (f) { return /fakeModule\.js$/.test(f); });
+
+                expect(resultantPaths.length).toBe(0);
+            });
+
+            it('Test 008 : should remove entry from plugin_metadata with corresponding version', function () {
+                platformJson.root.plugin_metadata = {};
+                platformJson.root.plugin_metadata[fakePlugin.id] = fakePlugin.version;
+                platformJson.removePluginMetadata(fakePlugin);
+                expect(platformJson.root.plugin_metadata[fakePlugin.id]).not.toBeDefined();
+            });
+        });
+
+        function evaluateCordovaDefineStatement (str) {
+            expect(typeof str).toBe('string');
+            const fnString = str.replace(/^\s*cordova\.define\('cordova\/plugin_list',\s*([\s\S]+)\);\s*$/, '($1)');
+            const mod = {exports: {}};
+            global.eval(fnString)(null, mod.exports, mod); // eslint-disable-line no-eval
+            return mod;
+        }
+
+        function expectedMetadata () {
+            // Create plain objects from ModuleMetadata instances
+            const modules = platformJson.root.modules.map(o => Object.assign({}, o));
+            modules.metadata = platformJson.root.plugin_metadata;
+            return modules;
+        }
+
+        describe('generateMetadata method', function () {
+            it('Test 009 : should generate text metadata containing list of installed modules', function () {
+                const meta = platformJson.addPluginMetadata(fakePlugin).generateMetadata();
+                const mod = evaluateCordovaDefineStatement(meta);
+
+                expect(mod.exports).toEqual(expectedMetadata());
+            });
+        });
+
+        describe('generateAndSaveMetadata method', function () {
+            it('should save generated metadata', function () {
+                // Needs to use graceful-fs, since that is used by fs-extra
+                const spy = spyOn(require('graceful-fs'), 'writeFileSync');
+
+                const dest = require('path').join(__dirname, 'test-destination');
+                platformJson.addPluginMetadata(fakePlugin).generateAndSaveMetadata(dest);
+
+                expect(spy).toHaveBeenCalledTimes(1);
+                const [file, data] = spy.calls.argsFor(0);
+                expect(file).toBe(dest);
+                const mod = evaluateCordovaDefineStatement(data);
+                expect(mod.exports).toEqual(expectedMetadata());
+            });
+        });
+    });
+});
+
+describe('ModuleMetadata class', function () {
+    it('Test 010 : should be constructable', function () {
+        var meta;
+        expect(function () {
+            meta = new ModuleMetadata('fakePlugin', {src: 'www/fakeModule.js'});
+        }).not.toThrow();
+        expect(meta instanceof ModuleMetadata).toBeTruthy();
+    });
+
+    it('Test 011 : should throw if either pluginId or jsModule argument isn\'t specified', function () {
+        expect(ModuleMetadata).toThrow();
+        expect(function () { new ModuleMetadata('fakePlugin', {}); }).toThrow(); /* eslint no-new : 0 */
+    });
+
+    it('Test 012 : should guess module id either from name property of from module src', function () {
+        expect(new ModuleMetadata('fakePlugin', {name: 'fakeModule'}).id).toMatch(/fakeModule$/);
+        expect(new ModuleMetadata('fakePlugin', {src: 'www/fakeModule.js'}).id).toMatch(/fakeModule$/);
+    });
+
+    it('Test 013 : should read "clobbers" property from module', function () {
+        expect(new ModuleMetadata('fakePlugin', {name: 'fakeModule'}).clobbers).not.toBeDefined();
+        var metadata = new ModuleMetadata('fakePlugin', FAKE_MODULE);
+        expect(metadata.clobbers).toEqual(jasmine.any(Array));
+        expect(metadata.clobbers[0]).toBe(FAKE_MODULE.clobbers[0].target);
+    });
+
+    it('Test 014 : should read "merges" property from module', function () {
+        expect(new ModuleMetadata('fakePlugin', {name: 'fakeModule'}).merges).not.toBeDefined();
+        var metadata = new ModuleMetadata('fakePlugin', FAKE_MODULE);
+        expect(metadata.merges).toEqual(jasmine.any(Array));
+        expect(metadata.merges[0]).toBe(FAKE_MODULE.merges[0].target);
+    });
+
+    it('Test 015 : should read "runs" property from module', function () {
+        expect(new ModuleMetadata('fakePlugin', {name: 'fakeModule'}).runs).not.toBeDefined();
+        expect(new ModuleMetadata('fakePlugin', FAKE_MODULE).runs).toBe(true);
+    });
+});
diff --git a/spec/PluginManager.spec.js b/spec/PluginManager.spec.js
index adefa8c..b606286 100644
--- a/spec/PluginManager.spec.js
+++ b/spec/PluginManager.spec.js
@@ -1,137 +1,137 @@
-/**

-    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.

-*/

-

-// Promise-matchers do not work with jasmine 2.0.

-// require('promise-matchers');

-

-var Q = require('q');

-var fs = require('fs-extra');

-var path = require('path');

-var rewire = require('rewire');

-var PluginManager = rewire('../src/PluginManager');

-var PluginInfo = require('../src/PluginInfo/PluginInfo');

-var ConfigChanges = require('../src/ConfigChanges/ConfigChanges');

-

-var DUMMY_PLUGIN = path.join(__dirname, 'fixtures/plugins/org.test.plugins.dummyplugin');

-var FAKE_PLATFORM = 'cordova-atari';

-var FAKE_LOCATIONS = {

-    root: '/some/fake/path',

-    platformWww: '/some/fake/path/platform_www',

-    www: '/some/www/dir'

-};

-

-describe('PluginManager class', function () {

-

-    beforeEach(function () {

-        spyOn(ConfigChanges, 'PlatformMunger');

-        spyOn(fs, 'outputJsonSync');

-        spyOn(fs, 'writeFileSync');

-        spyOn(fs, 'ensureDirSync');

-    });

-

-    it('Test 001 : should be constructable', function () {

-        expect(new PluginManager(FAKE_PLATFORM, FAKE_LOCATIONS)).toEqual(jasmine.any(PluginManager));

-    });

-

-    it('Test 002 : should return new instance for every PluginManager.get call', function () {

-        expect(PluginManager.get(FAKE_PLATFORM, FAKE_LOCATIONS)).toEqual(jasmine.any(PluginManager));

-        expect(PluginManager.get(FAKE_PLATFORM, FAKE_LOCATIONS))

-            .not.toBe(PluginManager.get(FAKE_PLATFORM, FAKE_LOCATIONS));

-    });

-

-    describe('instance', function () {

-        var actions, manager;

-        var FAKE_PROJECT;

-        var fail = jasmine.createSpy('fail');

-        var ActionStackOrig = PluginManager.__get__('ActionStack');

-

-        beforeEach(function () {

-            FAKE_PROJECT = jasmine.createSpyObj('project', ['getInstaller', 'getUninstaller', 'write']);

-            manager = new PluginManager('windows', FAKE_LOCATIONS, FAKE_PROJECT);

-            actions = jasmine.createSpyObj('actions', ['createAction', 'push', 'process']);

-            actions.process.and.returnValue(Q.resolve());

-            PluginManager.__set__('ActionStack', function () { return actions; });

-        });

-

-        afterEach(function () {

-            PluginManager.__set__('ActionStack', ActionStackOrig);

-        });

-

-        describe('addPlugin method', function () {

-            it('should return a promise', function () {

-                expect(Q.isPromise(manager.addPlugin(null, {}))).toBe(true);

-            });

-            // Promise-matchers do not work with jasmine 2.0.

-            xit('Test 003 : should reject if "plugin" parameter is not specified or not a PluginInfo instance', function (done) {

-                expect(manager.addPlugin(null, {})).toHaveBeenRejected(done);

-                expect(manager.addPlugin({}, {})).toHaveBeenRejected(done);

-                expect(manager.addPlugin(new PluginInfo(DUMMY_PLUGIN), {})).not.toHaveBeenRejected(done);

-            });

-

-            it('Test 004 : should iterate through all plugin\'s files and frameworks', function (done) {

-                manager.addPlugin(new PluginInfo(DUMMY_PLUGIN), {})

-                    .then(function () {

-                        expect(FAKE_PROJECT.getInstaller.calls.count()).toBe(16);

-                        expect(FAKE_PROJECT.getUninstaller.calls.count()).toBe(16);

-

-                        expect(actions.push.calls.count()).toBe(16);

-                        expect(actions.process).toHaveBeenCalled();

-                        expect(FAKE_PROJECT.write).toHaveBeenCalled();

-                    })

-                    .fail(fail)

-                    .done(function () {

-                        expect(fail).not.toHaveBeenCalled();

-                        done();

-                    });

-            });

-

-            it('Test 005 : should save plugin metadata to www directory', function (done) {

-                var metadataPath = path.join(manager.locations.www, 'cordova_plugins.js');

-                var platformWwwMetadataPath = path.join(manager.locations.platformWww, 'cordova_plugins.js');

-

-                manager.addPlugin(new PluginInfo(DUMMY_PLUGIN), {})

-                    .then(function () {

-                        expect(fs.writeFileSync).toHaveBeenCalledWith(metadataPath, jasmine.any(String), 'utf-8');

-                        expect(fs.writeFileSync).not.toHaveBeenCalledWith(platformWwwMetadataPath, jasmine.any(String), 'utf-8');

-                    })

-                    .fail(fail)

-                    .done(function () {

-                        expect(fail).not.toHaveBeenCalled();

-                        done();

-                    });

-            });

-

-            it('Test 006 : should save plugin metadata to both www ans platform_www directories when options.usePlatformWww is specified', function (done) {

-                var metadataPath = path.join(manager.locations.www, 'cordova_plugins.js');

-                var platformWwwMetadataPath = path.join(manager.locations.platformWww, 'cordova_plugins.js');

-

-                manager.addPlugin(new PluginInfo(DUMMY_PLUGIN), {usePlatformWww: true})

-                    .then(function () {

-                        expect(fs.writeFileSync).toHaveBeenCalledWith(metadataPath, jasmine.any(String), 'utf-8');

-                        expect(fs.writeFileSync).toHaveBeenCalledWith(platformWwwMetadataPath, jasmine.any(String), 'utf-8');

-                    })

-                    .fail(fail)

-                    .done(function () {

-                        expect(fail).not.toHaveBeenCalled();

-                        done();

-                    });

-            });

-        });

-    });

-});

+/**
+    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.
+*/
+
+// Promise-matchers do not work with jasmine 2.0.
+// require('promise-matchers');
+
+var Q = require('q');
+var fs = require('fs-extra');
+var path = require('path');
+var rewire = require('rewire');
+var PluginManager = rewire('../src/PluginManager');
+var PluginInfo = require('../src/PluginInfo/PluginInfo');
+var ConfigChanges = require('../src/ConfigChanges/ConfigChanges');
+
+var DUMMY_PLUGIN = path.join(__dirname, 'fixtures/plugins/org.test.plugins.dummyplugin');
+var FAKE_PLATFORM = 'cordova-atari';
+var FAKE_LOCATIONS = {
+    root: '/some/fake/path',
+    platformWww: '/some/fake/path/platform_www',
+    www: '/some/www/dir'
+};
+
+describe('PluginManager class', function () {
+
+    beforeEach(function () {
+        spyOn(ConfigChanges, 'PlatformMunger');
+        spyOn(fs, 'outputJsonSync');
+        spyOn(fs, 'writeFileSync');
+        spyOn(fs, 'ensureDirSync');
+    });
+
+    it('Test 001 : should be constructable', function () {
+        expect(new PluginManager(FAKE_PLATFORM, FAKE_LOCATIONS)).toEqual(jasmine.any(PluginManager));
+    });
+
+    it('Test 002 : should return new instance for every PluginManager.get call', function () {
+        expect(PluginManager.get(FAKE_PLATFORM, FAKE_LOCATIONS)).toEqual(jasmine.any(PluginManager));
+        expect(PluginManager.get(FAKE_PLATFORM, FAKE_LOCATIONS))
+            .not.toBe(PluginManager.get(FAKE_PLATFORM, FAKE_LOCATIONS));
+    });
+
+    describe('instance', function () {
+        var actions, manager;
+        var FAKE_PROJECT;
+        var fail = jasmine.createSpy('fail');
+        var ActionStackOrig = PluginManager.__get__('ActionStack');
+
+        beforeEach(function () {
+            FAKE_PROJECT = jasmine.createSpyObj('project', ['getInstaller', 'getUninstaller', 'write']);
+            manager = new PluginManager('windows', FAKE_LOCATIONS, FAKE_PROJECT);
+            actions = jasmine.createSpyObj('actions', ['createAction', 'push', 'process']);
+            actions.process.and.returnValue(Q.resolve());
+            PluginManager.__set__('ActionStack', function () { return actions; });
+        });
+
+        afterEach(function () {
+            PluginManager.__set__('ActionStack', ActionStackOrig);
+        });
+
+        describe('addPlugin method', function () {
+            it('should return a promise', function () {
+                expect(Q.isPromise(manager.addPlugin(null, {}))).toBe(true);
+            });
+            // Promise-matchers do not work with jasmine 2.0.
+            xit('Test 003 : should reject if "plugin" parameter is not specified or not a PluginInfo instance', function (done) {
+                expect(manager.addPlugin(null, {})).toHaveBeenRejected(done);
+                expect(manager.addPlugin({}, {})).toHaveBeenRejected(done);
+                expect(manager.addPlugin(new PluginInfo(DUMMY_PLUGIN), {})).not.toHaveBeenRejected(done);
+            });
+
+            it('Test 004 : should iterate through all plugin\'s files and frameworks', function (done) {
+                manager.addPlugin(new PluginInfo(DUMMY_PLUGIN), {})
+                    .then(function () {
+                        expect(FAKE_PROJECT.getInstaller.calls.count()).toBe(16);
+                        expect(FAKE_PROJECT.getUninstaller.calls.count()).toBe(16);
+
+                        expect(actions.push.calls.count()).toBe(16);
+                        expect(actions.process).toHaveBeenCalled();
+                        expect(FAKE_PROJECT.write).toHaveBeenCalled();
+                    })
+                    .fail(fail)
+                    .done(function () {
+                        expect(fail).not.toHaveBeenCalled();
+                        done();
+                    });
+            });
+
+            it('Test 005 : should save plugin metadata to www directory', function (done) {
+                var metadataPath = path.join(manager.locations.www, 'cordova_plugins.js');
+                var platformWwwMetadataPath = path.join(manager.locations.platformWww, 'cordova_plugins.js');
+
+                manager.addPlugin(new PluginInfo(DUMMY_PLUGIN), {})
+                    .then(function () {
+                        expect(fs.writeFileSync).toHaveBeenCalledWith(metadataPath, jasmine.any(String), 'utf-8');
+                        expect(fs.writeFileSync).not.toHaveBeenCalledWith(platformWwwMetadataPath, jasmine.any(String), 'utf-8');
+                    })
+                    .fail(fail)
+                    .done(function () {
+                        expect(fail).not.toHaveBeenCalled();
+                        done();
+                    });
+            });
+
+            it('Test 006 : should save plugin metadata to both www ans platform_www directories when options.usePlatformWww is specified', function (done) {
+                var metadataPath = path.join(manager.locations.www, 'cordova_plugins.js');
+                var platformWwwMetadataPath = path.join(manager.locations.platformWww, 'cordova_plugins.js');
+
+                manager.addPlugin(new PluginInfo(DUMMY_PLUGIN), {usePlatformWww: true})
+                    .then(function () {
+                        expect(fs.writeFileSync).toHaveBeenCalledWith(metadataPath, jasmine.any(String), 'utf-8');
+                        expect(fs.writeFileSync).toHaveBeenCalledWith(platformWwwMetadataPath, jasmine.any(String), 'utf-8');
+                    })
+                    .fail(fail)
+                    .done(function () {
+                        expect(fail).not.toHaveBeenCalled();
+                        done();
+                    });
+            });
+        });
+    });
+});
diff --git a/spec/fixtures/plugins/ChildBrowser/plugin.xml b/spec/fixtures/plugins/ChildBrowser/plugin.xml
index 700ef7c..205d5e7 100644
--- a/spec/fixtures/plugins/ChildBrowser/plugin.xml
+++ b/spec/fixtures/plugins/ChildBrowser/plugin.xml
@@ -36,7 +36,7 @@
         <access origin="build.phonegap.com" />
         <access origin="s3.amazonaws.com" />
     </config-file>
-    
+
     <info>No matter what platform you are installing to, this notice is very important.</info>
 
     <!-- android -->
@@ -82,7 +82,7 @@
         <config-file target="*-Info.plist" parent="AppId">
             <string>$APP_ID</string>
         </config-file>
-        
+
         <config-file target="*-Info.plist" parent="CFBundleURLTypes">
             <array>
               <dict>
@@ -119,8 +119,8 @@
                      target-dir="Plugins\" />
 
         <!-- modify the project file to include the added files -->
-        <config-file target=".csproj" parent=".">  
-        </config-file> 
+        <config-file target=".csproj" parent=".">
+        </config-file>
 
     </platform>
 </plugin>
diff --git a/spec/fixtures/plugins/ChildBrowser/src/android/ChildBrowser.java b/spec/fixtures/plugins/ChildBrowser/src/android/ChildBrowser.java
index 36113b6..bbc11dd 100644
--- a/spec/fixtures/plugins/ChildBrowser/src/android/ChildBrowser.java
+++ b/spec/fixtures/plugins/ChildBrowser/src/android/ChildBrowser.java
@@ -14,4 +14,4 @@
     KIND, either express or implied.  See the License for the
     specific language governing permissions and limitations
     under the License.
-*/
\ No newline at end of file
+*/
diff --git a/spec/fixtures/plugins/com.adobe.vars/plugin.xml b/spec/fixtures/plugins/com.adobe.vars/plugin.xml
index d1e1bff..9238747 100644
--- a/spec/fixtures/plugins/com.adobe.vars/plugin.xml
+++ b/spec/fixtures/plugins/com.adobe.vars/plugin.xml
@@ -34,16 +34,16 @@
             <poop name="GoogleMapsApiKey" value="$API_KEY" />
             <package>$PACKAGE_NAME</package>
 		</config-file>
-		
+
     </platform>
-    
+
     <!-- amazon fireos -->
     <platform name="amazon-fireos">
 		<config-file target="AndroidManifest.xml" parent="/manifest">
             <poop name="GoogleMapsApiKey" value="$API_KEY" />
             <package>$PACKAGE_NAME</package>
 		</config-file>
-		
+
     </platform>
 
     <!-- ios -->
diff --git a/spec/fixtures/plugins/org.test.multiple-children/plugin.xml b/spec/fixtures/plugins/org.test.multiple-children/plugin.xml
index 1e30814..53db8f6 100644
--- a/spec/fixtures/plugins/org.test.multiple-children/plugin.xml
+++ b/spec/fixtures/plugins/org.test.multiple-children/plugin.xml
@@ -29,22 +29,22 @@
     <platform name="android">
 		<config-file target="AndroidManifest.xml" parent="/manifest">
 			<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
-			
+
 			<!--library-->
 			<uses-permission android:name="android.permission.READ_PHONE_STATE"/>
-			
+
 			<!-- GCM connects to Google Services. -->
 			<uses-permission android:name="android.permission.INTERNET"/>
-			
+
 			<!-- GCM requires a Google account. -->
 			<uses-permission android:name="android.permission.GET_ACCOUNTS"/>
-			
+
 			<!-- Keeps the processor from sleeping when a message is received. -->
 			<uses-permission android:name="android.permission.WAKE_LOCK"/>
-			
+
 			<!--
 			 Creates a custom permission so only this app can receive its messages.
-			 
+
 			 NOTE: the permission *must* be called PACKAGE.permission.C2D_MESSAGE,
 			 where PACKAGE is the application's package name.
 			 -->
@@ -53,12 +53,12 @@
 			android:protectionLevel="signature"/>
 			<uses-permission
 			android:name="$PACKAGE_NAME.permission.C2D_MESSAGE"/>
-			
+
 			<!-- This app has permission to register and receive data message. -->
 			<uses-permission
 			android:name="com.google.android.c2dm.permission.RECEIVE"/>
 		</config-file>
-		
+
 		<config-file target="AndroidManifest.xml" parent="/manifest/application/activity">
 			<intent-filter>
 				<action android:name="$PACKAGE_NAME.MESSAGE"/>
@@ -68,15 +68,15 @@
 
 		<config-file target="AndroidManifest.xml" parent="/manifest/application">
 			<activity android:name="com.arellomobile.android.push.PushWebview"/>
-			
+
 			<activity android:name="com.arellomobile.android.push.MessageActivity"/>
-			
+
 			<activity android:name="com.arellomobile.android.push.PushHandlerActivity"/>
-			
+
 			<!--
 			 BroadcastReceiver that will receive intents from GCM
 			 services and handle them to the custom IntentService.
-			 
+
 			 The com.google.android.c2dm.permission.SEND permission is necessary
 			 so only GCM services can send data messages for the app.
 			 -->
@@ -91,15 +91,15 @@
 					<category android:name="$PACKAGE_NAME"/>
 				</intent-filter>
 			</receiver>
-			
+
 			<!--
 			 Application-specific subclass of PushGCMIntentService that will
 			 handle received messages.
 			 -->
-			<service android:name="com.arellomobile.android.push.PushGCMIntentService"/>        					
-			
+			<service android:name="com.arellomobile.android.push.PushGCMIntentService"/>
+
 		</config-file>
-		
+
 		<config-file target="res/xml/plugins.xml" parent="/plugins">
             <plugin name="PushNotification"
 			value="com.pushwoosh.test.plugin.pushnotifications.PushNotifications" onload="true"/>
diff --git a/spec/fixtures/plugins/org.test.src/plugin.xml b/spec/fixtures/plugins/org.test.src/plugin.xml
index 6d1aa91..78f60f1 100644
--- a/spec/fixtures/plugins/org.test.src/plugin.xml
+++ b/spec/fixtures/plugins/org.test.src/plugin.xml
@@ -99,4 +99,4 @@
       <preference name="WindowsToastCapable" value="true"/>
     </config-file>
   </platform>
-</plugin>
\ No newline at end of file
+</plugin>
diff --git a/spec/fixtures/projects/android/AndroidManifest.xml b/spec/fixtures/projects/android/AndroidManifest.xml
index a97674e..f10faa3 100644
--- a/spec/fixtures/projects/android/AndroidManifest.xml
+++ b/spec/fixtures/projects/android/AndroidManifest.xml
@@ -40,8 +40,8 @@
     <uses-permission android:name="android.permission.RECORD_VIDEO"/>
     <uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
     <uses-permission android:name="android.permission.READ_CONTACTS" />
-    <uses-permission android:name="android.permission.WRITE_CONTACTS" />   
-    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />   
+    <uses-permission android:name="android.permission.WRITE_CONTACTS" />
+    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
     <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
     <uses-permission android:name="android.permission.GET_ACCOUNTS" />
     <uses-permission android:name="android.permission.BROADCAST_STICKY" />
@@ -51,14 +51,14 @@
 
     <application android:icon="@drawable/icon" android:label="@string/app_name"
     	android:debuggable="true">
-		<activity android:name="ChildApp" android:label="@string/app_name" 
+		<activity android:name="ChildApp" android:label="@string/app_name"
 				  android:configChanges="orientation|keyboardHidden">
 			<intent-filter>
 				<action android:name="android.intent.action.MAIN" />
 				<category android:name="android.intent.category.LAUNCHER" />
 			</intent-filter>
         </activity>
-        <activity android:name="com.phonegap.DroidGap" android:label="@string/app_name" 
+        <activity android:name="com.phonegap.DroidGap" android:label="@string/app_name"
             	  android:configChanges="orientation|keyboardHidden">
         	<intent-filter>
         	</intent-filter>
@@ -66,4 +66,4 @@
     </application>
 
 	<uses-sdk android:minSdkVersion="5" />
-</manifest> 
+</manifest>
diff --git a/spec/fixtures/projects/android_two/AndroidManifest.xml b/spec/fixtures/projects/android_two/AndroidManifest.xml
index 019caae..2761208 100644
--- a/spec/fixtures/projects/android_two/AndroidManifest.xml
+++ b/spec/fixtures/projects/android_two/AndroidManifest.xml
@@ -40,8 +40,8 @@
     <uses-permission android:name="android.permission.RECORD_VIDEO"/>
     <uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
     <uses-permission android:name="android.permission.READ_CONTACTS" />
-    <uses-permission android:name="android.permission.WRITE_CONTACTS" />   
-    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />   
+    <uses-permission android:name="android.permission.WRITE_CONTACTS" />
+    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
     <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
     <uses-permission android:name="android.permission.GET_ACCOUNTS" />
     <uses-permission android:name="android.permission.BROADCAST_STICKY" />
@@ -51,14 +51,14 @@
 
     <application android:icon="@drawable/icon" android:label="@string/app_name"
     	android:debuggable="true">
-		<activity android:name="ChildApp" android:label="@string/app_name" 
+		<activity android:name="ChildApp" android:label="@string/app_name"
 				  android:configChanges="orientation|keyboardHidden">
 			<intent-filter>
 				<action android:name="android.intent.action.MAIN" />
 				<category android:name="android.intent.category.LAUNCHER" />
 			</intent-filter>
         </activity>
-        <activity android:name="org.test.DroidGap" android:label="@string/app_name" 
+        <activity android:name="org.test.DroidGap" android:label="@string/app_name"
             	  android:configChanges="orientation|keyboardHidden">
         	<intent-filter>
         	</intent-filter>
@@ -66,4 +66,4 @@
     </application>
 
 	<uses-sdk android:minSdkVersion="5" />
-</manifest> 
+</manifest>
diff --git a/spec/fixtures/projects/android_two_no_perms/AndroidManifest.xml b/spec/fixtures/projects/android_two_no_perms/AndroidManifest.xml
index 74aeecc..85155f9 100644
--- a/spec/fixtures/projects/android_two_no_perms/AndroidManifest.xml
+++ b/spec/fixtures/projects/android_two_no_perms/AndroidManifest.xml
@@ -31,14 +31,14 @@
 
     <application android:icon="@drawable/icon" android:label="@string/app_name"
     	android:debuggable="true">
-		<activity android:name="ChildApp" android:label="@string/app_name" 
+		<activity android:name="ChildApp" android:label="@string/app_name"
 				  android:configChanges="orientation|keyboardHidden">
 			<intent-filter>
 				<action android:name="android.intent.action.MAIN" />
 				<category android:name="android.intent.category.LAUNCHER" />
 			</intent-filter>
         </activity>
-        <activity android:name="org.test.DroidGap" android:label="@string/app_name" 
+        <activity android:name="org.test.DroidGap" android:label="@string/app_name"
             	  android:configChanges="orientation|keyboardHidden">
         	<intent-filter>
         	</intent-filter>
@@ -46,4 +46,4 @@
     </application>
 
 	<uses-sdk android:minSdkVersion="5" />
-</manifest> 
+</manifest>
diff --git a/spec/fixtures/projects/ios-config-xml/SampleApp/SampleApp-Info.plist b/spec/fixtures/projects/ios-config-xml/SampleApp/SampleApp-Info.plist
index 1edf010..22edad6 100644
--- a/spec/fixtures/projects/ios-config-xml/SampleApp/SampleApp-Info.plist
+++ b/spec/fixtures/projects/ios-config-xml/SampleApp/SampleApp-Info.plist
@@ -9,9 +9,9 @@
 # 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
diff --git a/spec/fixtures/projects/ios-config-xml/SampleApp/config.xml b/spec/fixtures/projects/ios-config-xml/SampleApp/config.xml
index f0a823b..883c160 100644
--- a/spec/fixtures/projects/ios-config-xml/SampleApp/config.xml
+++ b/spec/fixtures/projects/ios-config-xml/SampleApp/config.xml
@@ -8,9 +8,9 @@
 # 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
diff --git a/spec/fixtures/projects/windows/TestApp.jsproj b/spec/fixtures/projects/windows/TestApp.jsproj
index ff6cf68..1e1964f 100644
--- a/spec/fixtures/projects/windows/TestApp.jsproj
+++ b/spec/fixtures/projects/windows/TestApp.jsproj
@@ -68,7 +68,7 @@
   </ItemGroup>
   <Import Project="$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)\$(WMSJSProjectDirectory)\Microsoft.VisualStudio.$(WMSJSProject).targets" />
   <!-- To modify your build process, add your task inside one of the targets below then uncomment
-       that target and the DisableFastUpToDateCheck PropertyGroup. 
+       that target and the DisableFastUpToDateCheck PropertyGroup.
        Other similar extension points exist, see Microsoft.Common.targets.
   <Target Name="BeforeBuild">
   </Target>
diff --git a/spec/fixtures/projects/windows/TestApp.projitems b/spec/fixtures/projects/windows/TestApp.projitems
index c408373..b97e851 100644
--- a/spec/fixtures/projects/windows/TestApp.projitems
+++ b/spec/fixtures/projects/windows/TestApp.projitems
@@ -31,4 +31,4 @@
   </ItemGroup>
   <Import Project="CordovaAppDebug.projitems" Condition="Exists('$(MSBuildThisFileDirectory)CordovaAppDebug.projitems') And '$(Configuration)'=='Debug'" />
   <Import Project="CordovaAppRelease.projitems" Condition="Exists('$(MSBuildThisFileDirectory)CordovaAppRelease.projitems') And '$(Configuration)'!='Debug'" />
-</Project>
\ No newline at end of file
+</Project>
diff --git a/spec/fixtures/projects/windows/bom_test.xml b/spec/fixtures/projects/windows/bom_test.xml
index 57cadf6..446869d 100644
--- a/spec/fixtures/projects/windows/bom_test.xml
+++ b/spec/fixtures/projects/windows/bom_test.xml
@@ -8,9 +8,9 @@
 # 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
diff --git a/spec/fixtures/projects/windows/www/cordova-2.6.0.js b/spec/fixtures/projects/windows/www/cordova-2.6.0.js
index f8c32b2..84d49c3 100644
--- a/spec/fixtures/projects/windows/www/cordova-2.6.0.js
+++ b/spec/fixtures/projects/windows/www/cordova-2.6.0.js
@@ -12,9 +12,9 @@
  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
@@ -8042,7 +8042,7 @@
     }
 
     // Try to XHR the cordova_plugins.json file asynchronously.
-    try { // we commented we were going to try, so let us actually try and catch 
+    try { // we commented we were going to try, so let us actually try and catch
         var xhr = new context.XMLHttpRequest();
         xhr.onreadystatechange = function() {
             if (this.readyState != 4) { // not DONE
diff --git a/spec/fixtures/projects/windows/www/css/index.css b/spec/fixtures/projects/windows/www/css/index.css
index 51daa79..583c001 100644
--- a/spec/fixtures/projects/windows/www/css/index.css
+++ b/spec/fixtures/projects/windows/www/css/index.css
@@ -102,13 +102,13 @@
     50% { opacity: 0.4; }
     to { opacity: 1.0; }
 }
- 
+
 @-webkit-keyframes fade {
     from { opacity: 1.0; }
     50% { opacity: 0.4; }
     to { opacity: 1.0; }
 }
- 
+
 .blink {
     animation:fade 3000ms infinite;
     -webkit-animation:fade 3000ms infinite;
diff --git a/spec/superspawn.spec.js b/spec/superspawn.spec.js
index 2a9729c..5a63564 100644
--- a/spec/superspawn.spec.js
+++ b/spec/superspawn.spec.js
@@ -1,91 +1,91 @@
-/**

-    Licensed to the Apache Software Foundation (ASF) under one

-    or more contributor license agreements.  See the NOTICE file

-    distributed with this work for additional information

-    regarding copyright ownership.  The ASF licenses this file

-    to you under the Apache License, Version 2.0 (the

-    "License"); you may not use this file except in compliance

-    with the License.  You may obtain a copy of the License at

-

-    http://www.apache.org/licenses/LICENSE-2.0

-

-    Unless required by applicable law or agreed to in writing,

-    software distributed under the License is distributed on an

-    "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY

-    KIND, either express or implied.  See the License for the

-    specific language governing permissions and limitations

-    under the License.

-*/

-

-var Q = require('q');

-var superspawn = require('../src/superspawn');

-

-var LS = process.platform === 'win32' ? 'dir' : 'ls';

-

-describe('spawn method', function () {

-    var progressSpy, failSpy;

-

-    beforeEach(function () {

-        progressSpy = jasmine.createSpy('progress');

-        failSpy = jasmine.createSpy('fail'); /* eslint no-unused-vars : 0 */

-    });

-

-    it('Test 001 : should return a promise', function () {

-        expect(Q.isPromise(superspawn.spawn(LS))).toBe(true);

-        expect(Q.isPromise(superspawn.spawn('invalid_command'))).toBe(true);

-    });

-

-    it('Test 002 : should notify about stdout "data" events', function (done) {

-        superspawn.spawn(LS, [], {stdio: 'pipe'})

-            .progress(progressSpy)

-            .fin(function () {

-                expect(progressSpy).toHaveBeenCalledWith({'stdout': jasmine.any(String)});

-                done();

-            });

-    });

-

-    it('Test 003 : should notify about stderr "data" events', function (done) {

-        superspawn.spawn(LS, ['doesnt-exist'], {stdio: 'pipe'})

-            .progress(progressSpy)

-            .fin(function () {

-                expect(progressSpy).toHaveBeenCalledWith({'stderr': jasmine.any(String)});

-                done();

-            });

-    });

-

-    it('Test 004 : reject handler should pass in Error object with stdout and stderr properties', function (done) {

-        var cp = require('child_process');

-        spyOn(cp, 'spawn').and.callFake(function (cmd, args, opts) {

-            return {

-                stdout: {

-                    setEncoding: function () {},

-                    on: function (evt, handler) {

-                        // some sample stdout output

-                        handler('business as usual');

-                    }

-                },

-                stderr: {

-                    setEncoding: function () {},

-                    on: function (evt, handler) {

-                        // some sample stderr output

-                        handler('mayday mayday');

-                    }

-                },

-                on: function (evt, handler) {

-                    // What's passed to handler here is the exit code, so we can control

-                    // resolve/reject flow via this argument.

-                    handler(1); // this will trigger error flow

-                },

-                removeListener: function () {}

-            };

-        });

-        superspawn.spawn('this aggression', ['will', 'not', 'stand', 'man'], {})

-            .catch(function (err) {

-                expect(err).toBeDefined();

-                expect(err.stdout).toContain('usual');

-                expect(err.stderr).toContain('mayday');

-                done();

-            });

-    });

-

-});

+/**
+    Licensed to the Apache Software Foundation (ASF) under one
+    or more contributor license agreements.  See the NOTICE file
+    distributed with this work for additional information
+    regarding copyright ownership.  The ASF licenses this file
+    to you under the Apache License, Version 2.0 (the
+    "License"); you may not use this file except in compliance
+    with the License.  You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing,
+    software distributed under the License is distributed on an
+    "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+    KIND, either express or implied.  See the License for the
+    specific language governing permissions and limitations
+    under the License.
+*/
+
+var Q = require('q');
+var superspawn = require('../src/superspawn');
+
+var LS = process.platform === 'win32' ? 'dir' : 'ls';
+
+describe('spawn method', function () {
+    var progressSpy, failSpy;
+
+    beforeEach(function () {
+        progressSpy = jasmine.createSpy('progress');
+        failSpy = jasmine.createSpy('fail'); /* eslint no-unused-vars : 0 */
+    });
+
+    it('Test 001 : should return a promise', function () {
+        expect(Q.isPromise(superspawn.spawn(LS))).toBe(true);
+        expect(Q.isPromise(superspawn.spawn('invalid_command'))).toBe(true);
+    });
+
+    it('Test 002 : should notify about stdout "data" events', function (done) {
+        superspawn.spawn(LS, [], {stdio: 'pipe'})
+            .progress(progressSpy)
+            .fin(function () {
+                expect(progressSpy).toHaveBeenCalledWith({'stdout': jasmine.any(String)});
+                done();
+            });
+    });
+
+    it('Test 003 : should notify about stderr "data" events', function (done) {
+        superspawn.spawn(LS, ['doesnt-exist'], {stdio: 'pipe'})
+            .progress(progressSpy)
+            .fin(function () {
+                expect(progressSpy).toHaveBeenCalledWith({'stderr': jasmine.any(String)});
+                done();
+            });
+    });
+
+    it('Test 004 : reject handler should pass in Error object with stdout and stderr properties', function (done) {
+        var cp = require('child_process');
+        spyOn(cp, 'spawn').and.callFake(function (cmd, args, opts) {
+            return {
+                stdout: {
+                    setEncoding: function () {},
+                    on: function (evt, handler) {
+                        // some sample stdout output
+                        handler('business as usual');
+                    }
+                },
+                stderr: {
+                    setEncoding: function () {},
+                    on: function (evt, handler) {
+                        // some sample stderr output
+                        handler('mayday mayday');
+                    }
+                },
+                on: function (evt, handler) {
+                    // What's passed to handler here is the exit code, so we can control
+                    // resolve/reject flow via this argument.
+                    handler(1); // this will trigger error flow
+                },
+                removeListener: function () {}
+            };
+        });
+        superspawn.spawn('this aggression', ['will', 'not', 'stand', 'man'], {})
+            .catch(function (err) {
+                expect(err).toBeDefined();
+                expect(err.stdout).toContain('usual');
+                expect(err.stderr).toContain('mayday');
+                done();
+            });
+    });
+
+});
diff --git a/src/CordovaLogger.js b/src/CordovaLogger.js
index b5c3564..b6564d2 100644
--- a/src/CordovaLogger.js
+++ b/src/CordovaLogger.js
@@ -1,220 +1,220 @@
-/*

- 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 ansi = require('ansi');

-var EventEmitter = require('events').EventEmitter;

-var CordovaError = require('./CordovaError/CordovaError');

-var EOL = require('os').EOL;

-

-var INSTANCE;

-

-/**

- * @class CordovaLogger

- *

- * Implements logging facility that anybody could use. Should not be

- *   instantiated directly, `CordovaLogger.get()` method should be used instead

- *   to acquire logger instance

- */

-function CordovaLogger () {

-    this.levels = {};

-    this.colors = {};

-    this.stdout = process.stdout;

-    this.stderr = process.stderr;

-

-    this.stdoutCursor = ansi(this.stdout);

-    this.stderrCursor = ansi(this.stderr);

-

-    this.addLevel('verbose', 1000, 'grey');

-    this.addLevel('normal', 2000);

-    this.addLevel('warn', 2000, 'yellow');

-    this.addLevel('info', 3000, 'blue');

-    this.addLevel('error', 5000, 'red');

-    this.addLevel('results', 10000);

-

-    this.setLevel('normal');

-}

-

-/**

- * Static method to create new or acquire existing instance.

- *

- * @return  {CordovaLogger}  Logger instance

- */

-CordovaLogger.get = function () {

-    return INSTANCE || (INSTANCE = new CordovaLogger());

-};

-

-CordovaLogger.VERBOSE = 'verbose';

-CordovaLogger.NORMAL = 'normal';

-CordovaLogger.WARN = 'warn';

-CordovaLogger.INFO = 'info';

-CordovaLogger.ERROR = 'error';

-CordovaLogger.RESULTS = 'results';

-

-/**

- * Emits log message to process' stdout/stderr depending on message's severity

- *   and current log level. If severity is less than current logger's level,

- *   then the message is ignored.

- *

- * @param   {String}  logLevel  The message's log level. The logger should have

- *   corresponding level added (via logger.addLevel), otherwise

- *   `CordovaLogger.NORMAL` level will be used.

- * @param   {String}  message   The message, that should be logged to process'

- *   stdio

- *

- * @return  {CordovaLogger}     Current instance, to allow calls chaining.

- */

-CordovaLogger.prototype.log = function (logLevel, message) {

-    // if there is no such logLevel defined, or provided level has

-    // less severity than active level, then just ignore this call and return

-    if (!this.levels[logLevel] || this.levels[logLevel] < this.levels[this.logLevel]) {

-        // return instance to allow to chain calls

-        return this;

-    }

-

-    var isVerbose = this.logLevel === 'verbose';

-    var cursor = this.stdoutCursor;

-

-    if (message instanceof Error || logLevel === CordovaLogger.ERROR) {

-        message = formatError(message, isVerbose);

-        cursor = this.stderrCursor;

-    }

-

-    var color = this.colors[logLevel];

-    if (color) {

-        cursor.bold().fg[color]();

-    }

-

-    cursor.write(message).reset().write(EOL);

-

-    return this;

-};

-

-/**

- * Adds a new level to logger instance. This method also creates a shortcut

- *   method to log events with the level provided (i.e. after adding new level

- *   'debug', the method `debug(message)`, equal to logger.log('debug', message),

- *   will be added to logger instance)

- *

- * @param  {String}  level     A log level name. The levels with the following

- *   names added by default to every instance: 'verbose', 'normal', 'warn',

- *   'info', 'error', 'results'

- * @param  {Number}  severity  A number that represents level's severity.

- * @param  {String}  color     A valid color name, that will be used to log

- *   messages with this level. Any CSS color code or RGB value is allowed

- *   (according to ansi documentation:

- *   https://github.com/TooTallNate/ansi.js#features)

- *

- * @return  {CordovaLogger}     Current instance, to allow calls chaining.

- */

-CordovaLogger.prototype.addLevel = function (level, severity, color) {

-

-    this.levels[level] = severity;

-

-    if (color) {

-        this.colors[level] = color;

-    }

-

-    // Define own method with corresponding name

-    if (!this[level]) {

-        this[level] = this.log.bind(this, level);

-    }

-

-    return this;

-};

-

-/**

- * Sets the current logger level to provided value. If logger doesn't have level

- *   with this name, `CordovaLogger.NORMAL` will be used.

- *

- * @param  {String}  logLevel  Level name. The level with this name should be

- *   added to logger before.

- *

- * @return  {CordovaLogger}     Current instance, to allow calls chaining.

- */

-CordovaLogger.prototype.setLevel = function (logLevel) {

-    this.logLevel = this.levels[logLevel] ? logLevel : CordovaLogger.NORMAL;

-

-    return this;

-};

-

-/**

- * Adjusts the current logger level according to the passed options.

- *

- * @param   {Object|Array}  opts  An object or args array with options

- *

- * @return  {CordovaLogger}     Current instance, to allow calls chaining.

- */

-CordovaLogger.prototype.adjustLevel = function (opts) {

-    if (opts.verbose || (Array.isArray(opts) && opts.includes('--verbose'))) {

-        this.setLevel('verbose');

-    } else if (opts.silent || (Array.isArray(opts) && opts.includes('--silent'))) {

-        this.setLevel('error');

-    }

-

-    return this;

-};

-

-/**

- * Attaches logger to EventEmitter instance provided.

- *

- * @param   {EventEmitter}  eventEmitter  An EventEmitter instance to attach

- *   logger to.

- *

- * @return  {CordovaLogger}     Current instance, to allow calls chaining.

- */

-CordovaLogger.prototype.subscribe = function (eventEmitter) {

-

-    if (!(eventEmitter instanceof EventEmitter)) { throw new Error('Subscribe method only accepts an EventEmitter instance as argument'); }

-

-    eventEmitter.on('verbose', this.verbose)

-        .on('log', this.normal)

-        .on('info', this.info)

-        .on('warn', this.warn)

-        .on('warning', this.warn)

-        // Set up event handlers for logging and results emitted as events.

-        .on('results', this.results);

-

-    return this;

-};

-

-function formatError (error, isVerbose) {

-    var message = '';

-

-    if (error instanceof CordovaError) {

-        message = error.toString(isVerbose);

-    } else if (error instanceof Error) {

-        if (isVerbose) {

-            message = error.stack;

-        } else {

-            message = error.message;

-        }

-    } else {

-        // Plain text error message

-        message = error;

-    }

-

-    if (typeof message === 'string' && !message.toUpperCase().startsWith('ERROR:')) {

-        // Needed for backward compatibility with external tools

-        message = 'Error: ' + message;

-    }

-

-    return message;

-}

-

-module.exports = CordovaLogger;

+/*
+ 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 ansi = require('ansi');
+var EventEmitter = require('events').EventEmitter;
+var CordovaError = require('./CordovaError/CordovaError');
+var EOL = require('os').EOL;
+
+var INSTANCE;
+
+/**
+ * @class CordovaLogger
+ *
+ * Implements logging facility that anybody could use. Should not be
+ *   instantiated directly, `CordovaLogger.get()` method should be used instead
+ *   to acquire logger instance
+ */
+function CordovaLogger () {
+    this.levels = {};
+    this.colors = {};
+    this.stdout = process.stdout;
+    this.stderr = process.stderr;
+
+    this.stdoutCursor = ansi(this.stdout);
+    this.stderrCursor = ansi(this.stderr);
+
+    this.addLevel('verbose', 1000, 'grey');
+    this.addLevel('normal', 2000);
+    this.addLevel('warn', 2000, 'yellow');
+    this.addLevel('info', 3000, 'blue');
+    this.addLevel('error', 5000, 'red');
+    this.addLevel('results', 10000);
+
+    this.setLevel('normal');
+}
+
+/**
+ * Static method to create new or acquire existing instance.
+ *
+ * @return  {CordovaLogger}  Logger instance
+ */
+CordovaLogger.get = function () {
+    return INSTANCE || (INSTANCE = new CordovaLogger());
+};
+
+CordovaLogger.VERBOSE = 'verbose';
+CordovaLogger.NORMAL = 'normal';
+CordovaLogger.WARN = 'warn';
+CordovaLogger.INFO = 'info';
+CordovaLogger.ERROR = 'error';
+CordovaLogger.RESULTS = 'results';
+
+/**
+ * Emits log message to process' stdout/stderr depending on message's severity
+ *   and current log level. If severity is less than current logger's level,
+ *   then the message is ignored.
+ *
+ * @param   {String}  logLevel  The message's log level. The logger should have
+ *   corresponding level added (via logger.addLevel), otherwise
+ *   `CordovaLogger.NORMAL` level will be used.
+ * @param   {String}  message   The message, that should be logged to process'
+ *   stdio
+ *
+ * @return  {CordovaLogger}     Current instance, to allow calls chaining.
+ */
+CordovaLogger.prototype.log = function (logLevel, message) {
+    // if there is no such logLevel defined, or provided level has
+    // less severity than active level, then just ignore this call and return
+    if (!this.levels[logLevel] || this.levels[logLevel] < this.levels[this.logLevel]) {
+        // return instance to allow to chain calls
+        return this;
+    }
+
+    var isVerbose = this.logLevel === 'verbose';
+    var cursor = this.stdoutCursor;
+
+    if (message instanceof Error || logLevel === CordovaLogger.ERROR) {
+        message = formatError(message, isVerbose);
+        cursor = this.stderrCursor;
+    }
+
+    var color = this.colors[logLevel];
+    if (color) {
+        cursor.bold().fg[color]();
+    }
+
+    cursor.write(message).reset().write(EOL);
+
+    return this;
+};
+
+/**
+ * Adds a new level to logger instance. This method also creates a shortcut
+ *   method to log events with the level provided (i.e. after adding new level
+ *   'debug', the method `debug(message)`, equal to logger.log('debug', message),
+ *   will be added to logger instance)
+ *
+ * @param  {String}  level     A log level name. The levels with the following
+ *   names added by default to every instance: 'verbose', 'normal', 'warn',
+ *   'info', 'error', 'results'
+ * @param  {Number}  severity  A number that represents level's severity.
+ * @param  {String}  color     A valid color name, that will be used to log
+ *   messages with this level. Any CSS color code or RGB value is allowed
+ *   (according to ansi documentation:
+ *   https://github.com/TooTallNate/ansi.js#features)
+ *
+ * @return  {CordovaLogger}     Current instance, to allow calls chaining.
+ */
+CordovaLogger.prototype.addLevel = function (level, severity, color) {
+
+    this.levels[level] = severity;
+
+    if (color) {
+        this.colors[level] = color;
+    }
+
+    // Define own method with corresponding name
+    if (!this[level]) {
+        this[level] = this.log.bind(this, level);
+    }
+
+    return this;
+};
+
+/**
+ * Sets the current logger level to provided value. If logger doesn't have level
+ *   with this name, `CordovaLogger.NORMAL` will be used.
+ *
+ * @param  {String}  logLevel  Level name. The level with this name should be
+ *   added to logger before.
+ *
+ * @return  {CordovaLogger}     Current instance, to allow calls chaining.
+ */
+CordovaLogger.prototype.setLevel = function (logLevel) {
+    this.logLevel = this.levels[logLevel] ? logLevel : CordovaLogger.NORMAL;
+
+    return this;
+};
+
+/**
+ * Adjusts the current logger level according to the passed options.
+ *
+ * @param   {Object|Array}  opts  An object or args array with options
+ *
+ * @return  {CordovaLogger}     Current instance, to allow calls chaining.
+ */
+CordovaLogger.prototype.adjustLevel = function (opts) {
+    if (opts.verbose || (Array.isArray(opts) && opts.includes('--verbose'))) {
+        this.setLevel('verbose');
+    } else if (opts.silent || (Array.isArray(opts) && opts.includes('--silent'))) {
+        this.setLevel('error');
+    }
+
+    return this;
+};
+
+/**
+ * Attaches logger to EventEmitter instance provided.
+ *
+ * @param   {EventEmitter}  eventEmitter  An EventEmitter instance to attach
+ *   logger to.
+ *
+ * @return  {CordovaLogger}     Current instance, to allow calls chaining.
+ */
+CordovaLogger.prototype.subscribe = function (eventEmitter) {
+
+    if (!(eventEmitter instanceof EventEmitter)) { throw new Error('Subscribe method only accepts an EventEmitter instance as argument'); }
+
+    eventEmitter.on('verbose', this.verbose)
+        .on('log', this.normal)
+        .on('info', this.info)
+        .on('warn', this.warn)
+        .on('warning', this.warn)
+        // Set up event handlers for logging and results emitted as events.
+        .on('results', this.results);
+
+    return this;
+};
+
+function formatError (error, isVerbose) {
+    var message = '';
+
+    if (error instanceof CordovaError) {
+        message = error.toString(isVerbose);
+    } else if (error instanceof Error) {
+        if (isVerbose) {
+            message = error.stack;
+        } else {
+            message = error.message;
+        }
+    } else {
+        // Plain text error message
+        message = error;
+    }
+
+    if (typeof message === 'string' && !message.toUpperCase().startsWith('ERROR:')) {
+        // Needed for backward compatibility with external tools
+        message = 'Error: ' + message;
+    }
+
+    return message;
+}
+
+module.exports = CordovaLogger;
diff --git a/src/PluginManager.js b/src/PluginManager.js
index 5a018de..c8f971a 100644
--- a/src/PluginManager.js
+++ b/src/PluginManager.js
@@ -1,149 +1,149 @@
-/*

-       Licensed to the Apache Software Foundation (ASF) under one

-       or more contributor license agreements.  See the NOTICE file

-       distributed with this work for additional information

-       regarding copyright ownership.  The ASF licenses this file

-       to you under the Apache License, Version 2.0 (the

-       "License"); you may not use this file except in compliance

-       with the License.  You may obtain a copy of the License at

-

-         http://www.apache.org/licenses/LICENSE-2.0

-

-       Unless required by applicable law or agreed to in writing,

-       software distributed under the License is distributed on an

-       "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY

-       KIND, either express or implied.  See the License for the

-       specific language governing permissions and limitations

-       under the License.

-*/

-

-var Q = require('q');

-var fs = require('fs-extra');

-var path = require('path');

-

-var ActionStack = require('./ActionStack');

-var PlatformJson = require('./PlatformJson');

-var CordovaError = require('./CordovaError/CordovaError');

-var PlatformMunger = require('./ConfigChanges/ConfigChanges').PlatformMunger;

-var PluginInfoProvider = require('./PluginInfo/PluginInfoProvider');

-

-/**

- * @constructor

- * @class PluginManager

- * Represents an entity for adding/removing plugins for platforms

- *

- * @param {String} platform Platform name

- * @param {Object} locations - Platform files and directories

- * @param {IDEProject} ideProject The IDE project to add/remove plugin changes to/from

- */

-function PluginManager (platform, locations, ideProject) {

-    this.platform = platform;

-    this.locations = locations;

-    this.project = ideProject;

-

-    var platformJson = PlatformJson.load(locations.root, platform);

-    this.munger = new PlatformMunger(platform, locations.root, platformJson, new PluginInfoProvider());

-}

-

-/**

- * @constructs PluginManager

- * A convenience shortcut to new PluginManager(...)

- *

- * @param {String} platform Platform name

- * @param {Object} locations - Platform files and directories

- * @param {IDEProject} ideProject The IDE project to add/remove plugin changes to/from

- * @returns new PluginManager instance

- */

-PluginManager.get = function (platform, locations, ideProject) {

-    return new PluginManager(platform, locations, ideProject);

-};

-

-PluginManager.INSTALL = 'install';

-PluginManager.UNINSTALL = 'uninstall';

-

-module.exports = PluginManager;

-

-/**

- * Describes and implements common plugin installation/uninstallation routine. The flow is the following:

- *  * Validate and set defaults for options. Note that options are empty by default. Everything

- *    needed for platform IDE project must be passed from outside. Plugin variables (which

- *    are the part of the options) also must be already populated with 'PACKAGE_NAME' variable.

- *  * Collect all plugin's native and web files, get installers/uninstallers and process

- *    all these via ActionStack.

- *  * Save the IDE project, so the changes made by installers are persisted.

- *  * Generate config changes munge for plugin and apply it to all required files

- *  * Generate metadata for plugin and plugin modules and save it to 'cordova_plugins.js'

- *

- * @param {PluginInfo} plugin A PluginInfo structure representing plugin to install

- * @param {Object} [options={}] An installation options. It is expected but is not necessary

- *   that options would contain 'variables' inner object with 'PACKAGE_NAME' field set by caller.

- *

- * @returns {Promise} Returns a Q promise, either resolved in case of success, rejected otherwise.

- */

-PluginManager.prototype.doOperation = function (operation, plugin, options) {

-    if (operation !== PluginManager.INSTALL && operation !== PluginManager.UNINSTALL) { return Q.reject(new CordovaError('The parameter is incorrect. The opeation must be either "add" or "remove"')); }

-

-    if (!plugin || plugin.constructor.name !== 'PluginInfo') { return Q.reject(new CordovaError('The parameter is incorrect. The first parameter should be a PluginInfo instance')); }

-

-    // Set default to empty object to play safe when accesing properties

-    options = options || {};

-

-    var self = this;

-    var actions = new ActionStack();

-

-    // gather all files need to be handled during operation ...

-    plugin.getFilesAndFrameworks(this.platform, options)

-        .concat(plugin.getAssets(this.platform))

-        .concat(plugin.getJsModules(this.platform))

-        // ... put them into stack ...

-        .forEach(function (item) {

-            var installer = self.project.getInstaller(item.itemType);

-            var uninstaller = self.project.getUninstaller(item.itemType);

-            var actionArgs = [item, plugin, self.project, options];

-

-            var action;

-            if (operation === PluginManager.INSTALL) {

-                action = actions.createAction.apply(actions, [installer, actionArgs, uninstaller, actionArgs]); /* eslint no-useless-call: 0 */

-            } else /* op === PluginManager.UNINSTALL */{

-                action = actions.createAction.apply(actions, [uninstaller, actionArgs, installer, actionArgs]); /* eslint no-useless-call: 0 */

-            }

-            actions.push(action);

-        });

-

-    // ... and run through the action stack

-    return actions.process(this.platform)

-        .then(function () {

-            if (self.project.write) {

-                self.project.write();

-            }

-

-            if (operation === PluginManager.INSTALL) {

-                // Ignore passed `is_top_level` option since platform itself doesn't know

-                // anything about managing dependencies - it's responsibility of caller.

-                self.munger.add_plugin_changes(plugin, options.variables, /* is_top_level= */true, /* should_increment= */true, options.force);

-                self.munger.platformJson.addPluginMetadata(plugin);

-            } else {

-                self.munger.remove_plugin_changes(plugin, /* is_top_level= */true);

-                self.munger.platformJson.removePluginMetadata(plugin);

-            }

-

-            // Save everything (munge and plugin/modules metadata)

-            self.munger.save_all();

-

-            var metadata = self.munger.platformJson.generateMetadata();

-            fs.writeFileSync(path.join(self.locations.www, 'cordova_plugins.js'), metadata, 'utf-8');

-

-            // CB-11022 save plugin metadata to both www and platform_www if options.usePlatformWww is specified

-            if (options.usePlatformWww) {

-                fs.writeFileSync(path.join(self.locations.platformWww, 'cordova_plugins.js'), metadata, 'utf-8');

-            }

-        });

-};

-

-PluginManager.prototype.addPlugin = function (plugin, installOptions) {

-    return this.doOperation(PluginManager.INSTALL, plugin, installOptions);

-};

-

-PluginManager.prototype.removePlugin = function (plugin, uninstallOptions) {

-    return this.doOperation(PluginManager.UNINSTALL, plugin, uninstallOptions);

-};

+/*
+       Licensed to the Apache Software Foundation (ASF) under one
+       or more contributor license agreements.  See the NOTICE file
+       distributed with this work for additional information
+       regarding copyright ownership.  The ASF licenses this file
+       to you under the Apache License, Version 2.0 (the
+       "License"); you may not use this file except in compliance
+       with the License.  You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+       Unless required by applicable law or agreed to in writing,
+       software distributed under the License is distributed on an
+       "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+       KIND, either express or implied.  See the License for the
+       specific language governing permissions and limitations
+       under the License.
+*/
+
+var Q = require('q');
+var fs = require('fs-extra');
+var path = require('path');
+
+var ActionStack = require('./ActionStack');
+var PlatformJson = require('./PlatformJson');
+var CordovaError = require('./CordovaError/CordovaError');
+var PlatformMunger = require('./ConfigChanges/ConfigChanges').PlatformMunger;
+var PluginInfoProvider = require('./PluginInfo/PluginInfoProvider');
+
+/**
+ * @constructor
+ * @class PluginManager
+ * Represents an entity for adding/removing plugins for platforms
+ *
+ * @param {String} platform Platform name
+ * @param {Object} locations - Platform files and directories
+ * @param {IDEProject} ideProject The IDE project to add/remove plugin changes to/from
+ */
+function PluginManager (platform, locations, ideProject) {
+    this.platform = platform;
+    this.locations = locations;
+    this.project = ideProject;
+
+    var platformJson = PlatformJson.load(locations.root, platform);
+    this.munger = new PlatformMunger(platform, locations.root, platformJson, new PluginInfoProvider());
+}
+
+/**
+ * @constructs PluginManager
+ * A convenience shortcut to new PluginManager(...)
+ *
+ * @param {String} platform Platform name
+ * @param {Object} locations - Platform files and directories
+ * @param {IDEProject} ideProject The IDE project to add/remove plugin changes to/from
+ * @returns new PluginManager instance
+ */
+PluginManager.get = function (platform, locations, ideProject) {
+    return new PluginManager(platform, locations, ideProject);
+};
+
+PluginManager.INSTALL = 'install';
+PluginManager.UNINSTALL = 'uninstall';
+
+module.exports = PluginManager;
+
+/**
+ * Describes and implements common plugin installation/uninstallation routine. The flow is the following:
+ *  * Validate and set defaults for options. Note that options are empty by default. Everything
+ *    needed for platform IDE project must be passed from outside. Plugin variables (which
+ *    are the part of the options) also must be already populated with 'PACKAGE_NAME' variable.
+ *  * Collect all plugin's native and web files, get installers/uninstallers and process
+ *    all these via ActionStack.
+ *  * Save the IDE project, so the changes made by installers are persisted.
+ *  * Generate config changes munge for plugin and apply it to all required files
+ *  * Generate metadata for plugin and plugin modules and save it to 'cordova_plugins.js'
+ *
+ * @param {PluginInfo} plugin A PluginInfo structure representing plugin to install
+ * @param {Object} [options={}] An installation options. It is expected but is not necessary
+ *   that options would contain 'variables' inner object with 'PACKAGE_NAME' field set by caller.
+ *
+ * @returns {Promise} Returns a Q promise, either resolved in case of success, rejected otherwise.
+ */
+PluginManager.prototype.doOperation = function (operation, plugin, options) {
+    if (operation !== PluginManager.INSTALL && operation !== PluginManager.UNINSTALL) { return Q.reject(new CordovaError('The parameter is incorrect. The opeation must be either "add" or "remove"')); }
+
+    if (!plugin || plugin.constructor.name !== 'PluginInfo') { return Q.reject(new CordovaError('The parameter is incorrect. The first parameter should be a PluginInfo instance')); }
+
+    // Set default to empty object to play safe when accesing properties
+    options = options || {};
+
+    var self = this;
+    var actions = new ActionStack();
+
+    // gather all files need to be handled during operation ...
+    plugin.getFilesAndFrameworks(this.platform, options)
+        .concat(plugin.getAssets(this.platform))
+        .concat(plugin.getJsModules(this.platform))
+        // ... put them into stack ...
+        .forEach(function (item) {
+            var installer = self.project.getInstaller(item.itemType);
+            var uninstaller = self.project.getUninstaller(item.itemType);
+            var actionArgs = [item, plugin, self.project, options];
+
+            var action;
+            if (operation === PluginManager.INSTALL) {
+                action = actions.createAction.apply(actions, [installer, actionArgs, uninstaller, actionArgs]); /* eslint no-useless-call: 0 */
+            } else /* op === PluginManager.UNINSTALL */{
+                action = actions.createAction.apply(actions, [uninstaller, actionArgs, installer, actionArgs]); /* eslint no-useless-call: 0 */
+            }
+            actions.push(action);
+        });
+
+    // ... and run through the action stack
+    return actions.process(this.platform)
+        .then(function () {
+            if (self.project.write) {
+                self.project.write();
+            }
+
+            if (operation === PluginManager.INSTALL) {
+                // Ignore passed `is_top_level` option since platform itself doesn't know
+                // anything about managing dependencies - it's responsibility of caller.
+                self.munger.add_plugin_changes(plugin, options.variables, /* is_top_level= */true, /* should_increment= */true, options.force);
+                self.munger.platformJson.addPluginMetadata(plugin);
+            } else {
+                self.munger.remove_plugin_changes(plugin, /* is_top_level= */true);
+                self.munger.platformJson.removePluginMetadata(plugin);
+            }
+
+            // Save everything (munge and plugin/modules metadata)
+            self.munger.save_all();
+
+            var metadata = self.munger.platformJson.generateMetadata();
+            fs.writeFileSync(path.join(self.locations.www, 'cordova_plugins.js'), metadata, 'utf-8');
+
+            // CB-11022 save plugin metadata to both www and platform_www if options.usePlatformWww is specified
+            if (options.usePlatformWww) {
+                fs.writeFileSync(path.join(self.locations.platformWww, 'cordova_plugins.js'), metadata, 'utf-8');
+            }
+        });
+};
+
+PluginManager.prototype.addPlugin = function (plugin, installOptions) {
+    return this.doOperation(PluginManager.INSTALL, plugin, installOptions);
+};
+
+PluginManager.prototype.removePlugin = function (plugin, uninstallOptions) {
+    return this.doOperation(PluginManager.UNINSTALL, plugin, uninstallOptions);
+};