You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@cordova.apache.org by fi...@apache.org on 2013/04/29 09:33:31 UTC

[1/2] git commit: pretty much there for a config handler module. a few uninstall tests missing, and need to wire up to install/uninstall/prepare properly (and add tests for interaction between those modules)

Updated Branches:
  refs/heads/master 8c2e7ba18 -> 94be165cc


pretty much there for a config handler module. a few uninstall tests missing, and need to wire up to install/uninstall/prepare properly (and add tests for interaction between those modules)


Project: http://git-wip-us.apache.org/repos/asf/cordova-plugman/repo
Commit: http://git-wip-us.apache.org/repos/asf/cordova-plugman/commit/1395f63a
Tree: http://git-wip-us.apache.org/repos/asf/cordova-plugman/tree/1395f63a
Diff: http://git-wip-us.apache.org/repos/asf/cordova-plugman/diff/1395f63a

Branch: refs/heads/master
Commit: 1395f63a0c6f2db2f9d50a5b1fda4ab2fc0c10aa
Parents: 8c2e7ba
Author: Fil Maj <ma...@gmail.com>
Authored: Mon Apr 29 00:11:49 2013 -0700
Committer: Fil Maj <ma...@gmail.com>
Committed: Mon Apr 29 00:12:29 2013 -0700

----------------------------------------------------------------------
 README.md                                |    4 +-
 spec/install.spec.js                     |   10 +-
 spec/platforms/android.spec.js           |   74 ------
 spec/platforms/blackberry.spec.js        |   36 ---
 spec/platforms/ios.spec.js               |   48 ----
 spec/plugins/ConfigTestPlugin/plugin.xml |   37 +++
 spec/plugins/VariablePlugin/plugin.xml   |    2 +-
 spec/uninstall.spec.js                   |    7 +
 spec/util/config-changes.spec.js         |  303 +++++++++++++++++++++++++
 src/install.js                           |   41 +++-
 src/uninstall.js                         |   22 +-
 src/util/config-changes.js               |  269 +++++++++++++++++++++-
 src/util/xml-helpers.js                  |    8 +-
 13 files changed, 662 insertions(+), 199 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/cordova-plugman/blob/1395f63a/README.md
----------------------------------------------------------------------
diff --git a/README.md b/README.md
index 75e83a9..795d250 100644
--- a/README.md
+++ b/README.md
@@ -15,14 +15,14 @@ This document defines tool usage and the plugin format specification.
 ## Usage
 
     plugman --fetch --plugin <directory|git-url|name> [--plugins_dir <directory>]
-    plugman --install --platform <ios|android|bb10> --project <directory> --plugin <name> [--plugins_dir <directory>]
+    plugman --install --platform <ios|android|bb10> --project <directory> --plugin <name|url> [--plugins_dir <directory>]
     plugman --uninstall --platform <ios|android|bb10> --project <directory> --plugin <name> [--plugins_dir <directory>]
     plugman --remove --plugin <name> [--plugins_dir <directory>]
     plugman --list [--plugins_dir <directory>]
     plugman --prepare --platform <ios|android|bb10> --project <directory> [--plugins_dir <directory>]
 
 * `--fetch`: Retrieves and stores a plugin
-* `--install`: Installs an already-`--fetch`'ed plugin into a cordova project
+* `--install`: Installs a plugin into a cordova project. if `<name|url>` does not exist under the `plugins_dir`, will call `fetch` to retrieve it first.
 * `--uninstall`: Uninstalls an already-`--install`'ed plugin from a cordova project
 * `--remove`: Removes a `--fetch`'ed plugin
 * `--list`: Lists all `--fetch`'ed plugins

http://git-wip-us.apache.org/repos/asf/cordova-plugman/blob/1395f63a/spec/install.spec.js
----------------------------------------------------------------------
diff --git a/spec/install.spec.js b/spec/install.spec.js
index dd48d87..e993388 100644
--- a/spec/install.spec.js
+++ b/spec/install.spec.js
@@ -2,6 +2,7 @@ var install = require('../src/install'),
     android = require('../src/platforms/android'),
     ios     = require('../src/platforms/ios'),
     blackberry = require('../src/platforms/blackberry'),
+    config_changes = require('../src/util/config-changes'),
     plugman = require('../plugman'),
     fs      = require('fs'),
     os      = require('osenv'),
@@ -51,9 +52,15 @@ describe('install', function() {
             install('android', temp, 'DummyPlugin', plugins_dir, {});
             var transactions = android_installer.mostRecentCall.args[0];
 
-            expect(transactions.length).toEqual(6);
+            expect(transactions.length).toEqual(3);
             expect(transactions[0].tag).toBe('source-file');
         });
+        it('should call the config-changes module\'s add_installed_plugin_to_prepare_queue method', function() {
+            install('android', temp, 'DummyPlugin', plugins_dir, {});
+            var spy = spyOn(config_changes, 'add_installed_plugin_to_prepare_queue');
+            android_installer.mostRecentCall.args[5](null); // fake out handler install callback
+            expect(spy).toHaveBeenCalledWith(plugins_dir, 'DummyPlugin', 'android', {});
+        });
     });
 
     describe('failure', function() {
@@ -77,6 +84,7 @@ describe('install', function() {
             expect(executed_txs.length).toEqual(0);
         }); 
         it('should throw if plugin is already installed into project', function() {
+            // TODO: plugins and their version can be recognized using the platform.json file
             shell.cp('-rf', dummyplugin, plugins_dir);
             expect(function() {
                 install('android', temp, 'DummyPlugin', plugins_dir, {});

http://git-wip-us.apache.org/repos/asf/cordova-plugman/blob/1395f63a/spec/platforms/android.spec.js
----------------------------------------------------------------------
diff --git a/spec/platforms/android.spec.js b/spec/platforms/android.spec.js
index 198a4bd..beb7c30 100644
--- a/spec/platforms/android.spec.js
+++ b/spec/platforms/android.spec.js
@@ -94,42 +94,6 @@ describe('android project handler', function() {
                 }).toThrow('"' + target + '" already exists!');
             });
         });
-        describe('of <config-file> elements', function() {
-            it('should only target config.xml if that is applicable', function() {
-                var config = copyArray(configChanges);
-                shell.cp('-rf', android_two_project, temp);
-                var s = spyOn(xml_helpers, 'parseElementtreeSync').andCallThrough();
-                android.install(config, dummy_id, temp, dummyplugin, {});
-                expect(s).toHaveBeenCalledWith(path.join(temp, 'res', 'xml', 'config.xml'));
-                expect(s).not.toHaveBeenCalledWith(path.join(temp, 'res', 'xml', 'plugins.xml'));
-            });
-            it('should only target plugins.xml if that is applicable', function() {
-                shell.cp('-rf', android_one_project, temp);
-                var config = copyArray(configChanges);
-                var s = spyOn(xml_helpers, 'parseElementtreeSync').andCallThrough();
-                android.install(config, dummy_id, temp, dummyplugin, {});
-                expect(s).not.toHaveBeenCalledWith(path.join(temp, 'res', 'xml', 'config.xml'));
-                expect(s).toHaveBeenCalledWith(path.join(temp, 'res', 'xml', 'plugins.xml'));
-            });
-            it('should call into xml helper\'s graftXML', function() {
-                shell.cp('-rf', android_one_project, temp);
-                var config = copyArray(configChanges);
-                var s = spyOn(xml_helpers, 'graftXML').andReturn(true);
-                android.install(config, dummy_id, temp, dummyplugin, {});
-                expect(s).toHaveBeenCalled();
-            });
-        });
-        it('should call into plugins\'s searchAndReplace to interpolate variables properly', function() {
-            shell.cp('-rf', android_one_project, temp);
-            var config = copyArray(variable_configs);
-            var s = spyOn(plugins_module, 'searchAndReplace');
-            var vars = {
-                'API_KEY':'batcountry'
-            };
-            android.install(config, variable_id, temp, variableplugin, vars);
-            expect(s).toHaveBeenCalledWith(path.resolve(temp, 'AndroidManifest.xml'), vars);
-            expect(s).toHaveBeenCalledWith(path.resolve(temp, 'res', 'xml', 'plugins.xml'), vars);
-        });
     });
 
     describe('uninstallation', function() {
@@ -153,44 +117,6 @@ describe('android project handler', function() {
                 });
             });
         });
-        describe('of <config-file> elements', function() {
-            it('should only target config.xml if that is applicable', function(done) {
-                var config = copyArray(configChanges);
-                var s = spyOn(xml_helpers, 'parseElementtreeSync').andCallThrough();
-                install('android', temp, 'DummyPlugin', plugins_dir, {}, function() {
-                    var config = copyArray(configChanges);
-                    android.uninstall(config, dummy_id, temp, path.join(plugins_dir, 'DummyPlugin'));
-                    expect(s).toHaveBeenCalledWith(path.join(temp, 'res', 'xml', 'config.xml'));
-                    expect(s).not.toHaveBeenCalledWith(path.join(temp, 'res', 'xml', 'plugins.xml'));
-                    done();
-                });
-            });
-            it('should only target plugins.xml if that is applicable', function(done) {
-                shell.rm('-rf', temp);
-                shell.mkdir('-p', temp);
-                shell.mkdir('-p', plugins_dir);
-                shell.cp('-rf', android_one_project, temp);
-                shell.cp('-rf', dummyplugin, plugins_dir);
-                var config = copyArray(configChanges);
-                var s = spyOn(xml_helpers, 'parseElementtreeSync').andCallThrough();
-                install('android', temp, 'DummyPlugin', plugins_dir, {}, function() {
-                    var config = copyArray(configChanges);
-                    android.uninstall(config, dummy_id, temp, path.join(plugins_dir, 'DummyPlugin'));
-                    expect(s).not.toHaveBeenCalledWith(path.join(temp, 'res', 'xml', 'config.xml'));
-                    expect(s).toHaveBeenCalledWith(path.join(temp, 'res', 'xml', 'plugins.xml'));
-                    done();
-                });
-            });
-            it('should call into xml helper\'s pruneXML', function(done) {
-                var config = copyArray(configChanges);
-                install('android', temp, 'DummyPlugin', plugins_dir, {}, function() {
-                    var s = spyOn(xml_helpers, 'pruneXML').andReturn(true);
-                    android.uninstall(config, dummy_id, temp, path.join(plugins_dir, 'DummyPlugin'));
-                    expect(s).toHaveBeenCalled();
-                    done();
-                });
-            });
-        });
         describe('of <asset> elements', function() {
             it('should remove www\'s plugins/<plugin-id> directory', function(done) {
                 var as = copyArray(assets);

http://git-wip-us.apache.org/repos/asf/cordova-plugman/blob/1395f63a/spec/platforms/blackberry.spec.js
----------------------------------------------------------------------
diff --git a/spec/platforms/blackberry.spec.js b/spec/platforms/blackberry.spec.js
index a4d7e63..049ec95 100644
--- a/spec/platforms/blackberry.spec.js
+++ b/spec/platforms/blackberry.spec.js
@@ -83,21 +83,6 @@ describe('blackberry project handler', function() {
                 }).toThrow('"' + target + '" already exists!');
             });
         });
-        describe('of <config-file> elements', function() {
-            it('should target config.xml', function() {
-                var config = copyArray(configChanges);
-                var s = spyOn(xml_helpers, 'parseElementtreeSync').andCallThrough();
-                blackberry.install(config, dummy_id, temp, dummyplugin, {});
-                expect(s).toHaveBeenCalledWith(path.join(temp, 'www', 'config.xml'));
-            });
-            it('should call into xml helper\'s graftXML', function() {
-                shell.cp('-rf', blackberry_project, temp);
-                var config = copyArray(configChanges);
-                var s = spyOn(xml_helpers, 'graftXML').andReturn(true);
-                blackberry.install(config, dummy_id, temp, dummyplugin, {});
-                expect(s).toHaveBeenCalled();
-            });
-        });
     });
 
     describe('uninstallation', function() {
@@ -123,27 +108,6 @@ describe('blackberry project handler', function() {
                 });
             });
         });
-        describe('of <config-file> elements', function() {
-            it('should target config.xml', function(done) {
-                var config = copyArray(configChanges);
-                var s = spyOn(xml_helpers, 'parseElementtreeSync').andCallThrough();
-                install('blackberry', temp, 'DummyPlugin', plugins_dir, {}, function() {
-                    var config = copyArray(configChanges);
-                    blackberry.uninstall(config, dummy_id, temp, path.join(plugins_dir, 'DummyPlugin'));
-                    expect(s).toHaveBeenCalledWith(path.join(temp, 'www', 'config.xml'));
-                    done();
-                });
-            });
-            it('should call into xml helper\'s pruneXML', function(done) {
-                var config = copyArray(configChanges);
-                install('blackberry', temp, 'DummyPlugin', plugins_dir, {}, function() {
-                    var s = spyOn(xml_helpers, 'pruneXML').andReturn(true);
-                    blackberry.uninstall(config, dummy_id, temp, path.join(plugins_dir, 'DummyPlugin'));
-                    expect(s).toHaveBeenCalled();
-                    done();
-                });
-            });
-        });
         describe('of <asset> elements', function() {
             it('should remove www\'s plugins/<plugin-id> directory', function(done) {
                 var as = copyArray(assets);

http://git-wip-us.apache.org/repos/asf/cordova-plugman/blob/1395f63a/spec/platforms/ios.spec.js
----------------------------------------------------------------------
diff --git a/spec/platforms/ios.spec.js b/spec/platforms/ios.spec.js
index dc3bee5..cb5521f 100644
--- a/spec/platforms/ios.spec.js
+++ b/spec/platforms/ios.spec.js
@@ -97,13 +97,6 @@ describe('ios project handler', function() {
                 ios.install([], 'someid', temp, plugins_dir, {});
             }).toThrow('could not find PhoneGap/Cordova plist file, or config.xml file.');
         });
-        it('should interpolate any variables correctly into pbx, plist and config files', function() {
-            shell.cp('-rf', ios_config_xml_project, temp);
-            ios.install(variable_configs, variable_id, temp, plugins_dir, {'API_KEY':'ruthless competency'});
-            var config_file = path.join(temp, 'SampleApp', 'config.xml');
-            var contents = fs.readFileSync(config_file, 'utf-8');
-            expect(contents).toMatch(/awesome value="ruthless competency"/gi);
-        });
 
         describe('of <source-file> elements', function() {
             beforeEach(function() {
@@ -171,47 +164,6 @@ describe('ios project handler', function() {
             });
         });
 
-        describe('of <plugins-plist> elements', function() {
-            it('should only be used in an applicably old cordova-ios projects', function() {
-                shell.cp('-rf', ios_plist_project, temp);
-                var pls = copyArray(plist_els);
-                var spy = spyOn(plist, 'parseFileSync').andReturn({Plugins:{}});
-                ios.install(pls, dummy_id, temp, dummyplugin, {});
-                expect(spy).toHaveBeenCalledWith(path.join(temp, 'SampleApp', 'PhoneGap.plist'));
-            });
-            it('should not be used in an applicably new cordova-ios projects', function() {
-                shell.cp('-rf', ios_config_xml_project, temp);
-                var pls = copyArray(plist_els);
-                var spy = spyOn(plist, 'parseFileSync').andReturn({Plugins:{}});
-                ios.install(pls, dummy_id, temp, dummyplugin, {});
-                expect(spy).not.toHaveBeenCalledWith(path.join(temp, 'SampleApp', 'config.xml'));
-            });
-            it('should add a <plugin> element in applicably new cordova-ios projects with old-style plugins using only <plugins-plist> elements', function() {
-                shell.cp('-rf', ios_config_xml_project, temp);
-                var pls = copyArray(plist_only_els);
-                ios.install(pls, plist_id, temp, plistplugin, {});
-                expect(fs.readFileSync(path.join(temp, 'SampleApp', 'config.xml'), 'utf-8')).toMatch(/<plugin name="OldSkewl"/gi);
-            });
-        });
-
-        describe('of <config-file> elements', function() {
-            beforeEach(function() {
-                shell.cp('-rf', ios_config_xml_project, temp);
-            });
-            it('should call xml_helpers\' graftXML', function() {
-                var config = copyArray(dummy_configs);
-                var spy = spyOn(xml_helpers, 'graftXML').andReturn(true);
-                ios.install(config, dummy_id, temp, dummyplugin, {});
-                expect(spy).toHaveBeenCalledWith(jasmine.any(Object), dummy_configs[0]._children, '/widget/plugins');
-            });
-            it('should write the new config file out after successfully grafting', function() {
-                var config = copyArray(dummy_configs);
-                var spy = spyOn(fs, 'writeFileSync');
-                ios.install(config, dummy_id, temp, dummyplugin, {});
-                expect(spy).toHaveBeenCalledWith(path.join(temp, 'SampleApp', 'config.xml'), jasmine.any(String));
-            });
-        });
-
         describe('of <header-file> elements', function() {
             beforeEach(function() {
                 shell.cp('-rf', ios_config_xml_project, temp);

http://git-wip-us.apache.org/repos/asf/cordova-plugman/blob/1395f63a/spec/plugins/ConfigTestPlugin/plugin.xml
----------------------------------------------------------------------
diff --git a/spec/plugins/ConfigTestPlugin/plugin.xml b/spec/plugins/ConfigTestPlugin/plugin.xml
new file mode 100644
index 0000000..57dd1a8
--- /dev/null
+++ b/spec/plugins/ConfigTestPlugin/plugin.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+
+ Copyright 2013 Anis Kadri
+
+ Licensed 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.
+
+-->
+
+<plugin xmlns="http://www.phonegap.com/ns/plugins/1.0"
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    id="com.phonegap.configtest"
+    version="3.0.0">
+
+    <name>Does Code Fil Write Even Work? Hopefully the Tests Will Tell Us</name>
+
+    <!-- android -->
+    <platform name="android">
+        <config-file target="res/xml/config.xml" parent="/widget">
+            <poop/>
+        </config-file>
+        <config-file target="res/xml/config.xml" parent="/widget">
+            <poop/>
+        </config-file>
+    </platform>
+</plugin>

http://git-wip-us.apache.org/repos/asf/cordova-plugman/blob/1395f63a/spec/plugins/VariablePlugin/plugin.xml
----------------------------------------------------------------------
diff --git a/spec/plugins/VariablePlugin/plugin.xml b/spec/plugins/VariablePlugin/plugin.xml
index 67bddd0..7249ae8 100644
--- a/spec/plugins/VariablePlugin/plugin.xml
+++ b/spec/plugins/VariablePlugin/plugin.xml
@@ -21,7 +21,7 @@
 <plugin xmlns="http://www.phonegap.com/ns/plugins/1.0"
     xmlns="http://www.phonegap.com/ns/plugins/1.0"
     xmlns:android="http://schemas.android.com/apk/res/android"
-    id="com.pushwoosh.plugins.pushwoosh"
+    id="com.adobe.vars"
     version="3.0.0">
 
     <name>Use Variables</name>

http://git-wip-us.apache.org/repos/asf/cordova-plugman/blob/1395f63a/spec/uninstall.spec.js
----------------------------------------------------------------------
diff --git a/spec/uninstall.spec.js b/spec/uninstall.spec.js
index e6b16db..6404b4f 100644
--- a/spec/uninstall.spec.js
+++ b/spec/uninstall.spec.js
@@ -4,6 +4,7 @@ var uninstall = require('../src/uninstall'),
     ios     = require('../src/platforms/ios'),
     blackberry = require('../src/platforms/blackberry'),
     xml_helpers = require('../src/util/xml-helpers'),
+    config_changes = require('../src/util/config-changes'),
     plugman = require('../plugman'),
     fs      = require('fs'),
     et      = require('elementtree'),
@@ -46,6 +47,12 @@ describe('uninstall', function() {
             expect(transactions.length).toEqual(6);
             expect(transactions[0].tag).toBe('source-file');
         });
+        it('should call the config-changes module\'s add_uninstalled_plugin_to_prepare_queue method', function() {
+            uninstall('android', temp, 'DummyPlugin', plugins_dir, {});
+            var spy = spyOn(config_changes, 'add_uninstalled_plugin_to_prepare_queue');
+            android_uninstaller.mostRecentCall.args[4](null); // fake out handler uninstall callback
+            expect(spy).toHaveBeenCalledWith(plugins_dir, 'DummyPlugin', 'android');
+        });
     });
 
     describe('failure', function() {

http://git-wip-us.apache.org/repos/asf/cordova-plugman/blob/1395f63a/spec/util/config-changes.spec.js
----------------------------------------------------------------------
diff --git a/spec/util/config-changes.spec.js b/spec/util/config-changes.spec.js
new file mode 100644
index 0000000..1d88bf7
--- /dev/null
+++ b/spec/util/config-changes.spec.js
@@ -0,0 +1,303 @@
+var configChanges = require('../../src/util/config-changes'),
+    xml_helpers = require('../../src/util/xml-helpers'),
+    fs      = require('fs'),
+    os      = require('osenv'),
+    et      = require('elementtree'),
+    path    = require('path'),
+    shell   = require('shelljs'),
+    temp    = path.join(os.tmpdir(), 'plugman'),
+    dummyplugin = path.join(__dirname, '..', 'plugins', 'DummyPlugin'),
+    childrenplugin = path.join(__dirname, '..', 'plugins', 'multiple-children'),
+    configplugin = path.join(__dirname, '..', 'plugins', 'ConfigTestPlugin'),
+    varplugin = path.join(__dirname, '..', 'plugins', 'VariablePlugin'),
+    android_two_project = path.join(__dirname, '..', 'projects', 'android_two', '*'),
+    ios_plist_project = path.join(__dirname, '..', 'projects', 'ios-plist', '*'),
+    plugins_dir = path.join(temp, 'cordova', 'plugins');
+
+var dummy_xml = new et.ElementTree(et.XML(fs.readFileSync(path.join(dummyplugin, 'plugin.xml'), 'utf-8')));
+
+function innerXML(xmltext) {
+    return xmltext.replace(/^<[\w\s\-=\/"\.]+>/, '').replace(/<\/[\w\s\-=\/"\.]+>$/,'');
+}
+
+describe('config-changes module', function() {
+    beforeEach(function() {
+        shell.mkdir('-p', temp);
+        shell.mkdir('-p', plugins_dir);
+    });
+    afterEach(function() {
+        shell.rm('-rf', temp);
+    });
+
+    it('should have queue methods', function() {
+        expect(configChanges.add_installed_plugin_to_prepare_queue).toBeDefined();
+        expect(configChanges.add_uninstalled_plugin_to_prepare_queue).toBeDefined();
+    });
+    it('should have a get_platform_json method', function() {
+        expect(configChanges.get_platform_json).toBeDefined();
+    });
+    it('should have a save_platform_json method', function() {
+        expect(configChanges.save_platform_json).toBeDefined();
+    });
+    it('should have a generate_plugin_config_munge method', function() {
+        expect(configChanges.generate_plugin_config_munge).toBeDefined();
+    });
+    it('should have a process method', function() {
+        expect(configChanges.process).toBeDefined();
+    });
+
+    describe('queue methods', function() {
+        describe('add_installed_plugin_to_prepare_queue', function() {
+            it('should call get_platform_json method', function() {
+                var spy = spyOn(configChanges, 'get_platform_json').andReturn({
+                    prepare_queue:{
+                        installed:[],
+                        uninstalled:[]
+                    }
+                });
+                configChanges.add_installed_plugin_to_prepare_queue(plugins_dir, 'PooPlugin', 'android', {});
+                expect(spy).toHaveBeenCalledWith(plugins_dir, 'android');
+            });
+            it('should append specified plugin to platform.json', function() {
+                configChanges.add_installed_plugin_to_prepare_queue(plugins_dir, 'PooPlugin', 'android', {});
+                var json = configChanges.get_platform_json(plugins_dir, 'android');
+                expect(json.prepare_queue.installed[0].plugin).toEqual('PooPlugin');
+                expect(json.prepare_queue.installed[0].vars).toEqual({});
+            });
+            it('should append specified plugin with any variables to platform.json', function() {
+                configChanges.add_installed_plugin_to_prepare_queue(plugins_dir, 'PooPlugin', 'android', {'dude':'man'});
+                var json = configChanges.get_platform_json(plugins_dir, 'android');
+                expect(json.prepare_queue.installed[0].plugin).toEqual('PooPlugin');
+                expect(json.prepare_queue.installed[0].vars).toEqual({'dude':'man'});
+            });
+            it('should call save_platform_json with updated config', function() {
+                var spy = spyOn(configChanges, 'save_platform_json');
+                configChanges.add_installed_plugin_to_prepare_queue(plugins_dir, 'PooPlugin', 'android', {});
+                var config = spy.mostRecentCall.args[0];
+                expect(config.prepare_queue.installed[0].plugin).toEqual('PooPlugin');
+            });
+        });
+        
+        describe('add_uninstalled_plugin_to_prepare_queue', function() {
+            beforeEach(function() {
+                shell.cp('-rf', dummyplugin, plugins_dir);
+            });
+
+            it('should call get_platform_json method', function() {
+                var spy = spyOn(configChanges, 'get_platform_json').andReturn({
+                    prepare_queue:{
+                        installed:[],
+                        uninstalled:[]
+                    }
+                });
+                configChanges.add_uninstalled_plugin_to_prepare_queue(plugins_dir, 'DummyPlugin', 'android');
+                expect(spy).toHaveBeenCalledWith(plugins_dir, 'android');
+            });
+            it('should append specified plugin to platform.json', function() {
+                configChanges.add_uninstalled_plugin_to_prepare_queue(plugins_dir, 'DummyPlugin', 'android');
+                var json = configChanges.get_platform_json(plugins_dir, 'android');
+                expect(json.prepare_queue.uninstalled[0].plugin).toEqual('DummyPlugin');
+                expect(json.prepare_queue.uninstalled[0].id).toEqual('com.phonegap.plugins.dummyplugin');
+            });
+            it('should call save_platform_json with updated config', function() {
+                var spy = spyOn(configChanges, 'save_platform_json');
+                configChanges.add_uninstalled_plugin_to_prepare_queue(plugins_dir, 'DummyPlugin', 'android', {});
+                var config = spy.mostRecentCall.args[0];
+                expect(config.prepare_queue.uninstalled[0].plugin).toEqual('DummyPlugin');
+                expect(config.prepare_queue.uninstalled[0].id).toEqual('com.phonegap.plugins.dummyplugin');
+            });
+        });
+    });
+
+    describe('get_platform_json method', function() {
+        it('should write out and return an empty config json file if it doesn\'t exist', function() {
+            var filepath = path.join(plugins_dir, 'android.json');
+            var cfg = configChanges.get_platform_json(plugins_dir, 'android');
+            expect(cfg).toBeDefined();
+            expect(cfg.prepare_queue).toBeDefined();
+            expect(cfg.config_munge).toBeDefined();
+            expect(cfg.installed_plugins).toBeDefined();
+            expect(fs.existsSync(filepath)).toBe(true);
+            expect(fs.readFileSync(filepath, 'utf-8')).toEqual(JSON.stringify(cfg));
+        });
+        it('should return the json file if it exists', function() {
+            var filepath = path.join(plugins_dir, 'android.json');
+            var json = {prepare_queue:{installed:[],uninstalled:[]},config_munge:{somechange:"blah"},installed_plugins:{}};
+            fs.writeFileSync(filepath, JSON.stringify(json), 'utf-8');
+            var cfg = configChanges.get_platform_json(plugins_dir, 'android');
+            expect(JSON.stringify(json)).toEqual(JSON.stringify(cfg));
+        });
+    });
+
+    xdescribe('save_platform_json method', function() {
+        it('should write out specified json', function() {
+            var filepath = path.join(plugins_dir, 'android.json');
+            var cfg = {poop:true};
+            configChanges.save_platform_json(cfg, plugins_dir, 'android');
+            expect(fs.existsSync(filepath)).toBe(true);
+            expect(fs.readFileSync(filepath, 'utf-8')).toEqual(JSON.stringify(cfg));
+        });
+    });
+
+    describe('generate_plugin_config_munge method', function() {
+        it('should return a flat config heirarchy for simple, one-off config changes', function() {
+            var xml;
+            var munge = configChanges.generate_plugin_config_munge(dummyplugin, 'android');
+            expect(munge['AndroidManifest.xml']).toBeDefined();
+            expect(munge['AndroidManifest.xml']['/manifest/application']).toBeDefined();
+            xml = (new et.ElementTree(dummy_xml.find('./platform[@name="android"]/config-file[@target="AndroidManifest.xml"]'))).write({xml_declaration:false});
+            xml = innerXML(xml);
+            expect(munge['AndroidManifest.xml']['/manifest/application'][xml]).toEqual(1);
+            expect(munge['res/xml/plugins.xml']).toBeDefined();
+            expect(munge['res/xml/plugins.xml']['/plugins']).toBeDefined();
+            xml = (new et.ElementTree(dummy_xml.find('./platform[@name="android"]/config-file[@target="res/xml/plugins.xml"]'))).write({xml_declaration:false});
+            xml = innerXML(xml);
+            expect(munge['res/xml/plugins.xml']['/plugins'][xml]).toEqual(1);
+            expect(munge['res/xml/config.xml']).toBeDefined();
+            expect(munge['res/xml/config.xml']['/cordova/plugins']).toBeDefined();
+            xml = (new et.ElementTree(dummy_xml.find('./platform[@name="android"]/config-file[@target="res/xml/config.xml"]'))).write({xml_declaration:false});
+            xml = innerXML(xml);
+            expect(munge['res/xml/config.xml']['/cordova/plugins'][xml]).toEqual(1);
+        });
+        it('should split out multiple children of config-file elements into individual leaves', function() {
+            var munge = configChanges.generate_plugin_config_munge(childrenplugin, 'android');
+            expect(munge['AndroidManifest.xml']).toBeDefined();
+            expect(munge['AndroidManifest.xml']['/manifest']).toBeDefined();
+            expect(munge['AndroidManifest.xml']['/manifest']['<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />']).toBeDefined();
+            expect(munge['AndroidManifest.xml']['/manifest']['<uses-permission android:name="android.permission.READ_PHONE_STATE" />']).toBeDefined();
+            expect(munge['AndroidManifest.xml']['/manifest']['<uses-permission android:name="android.permission.INTERNET" />']).toBeDefined();
+            expect(munge['AndroidManifest.xml']['/manifest']['<uses-permission android:name="android.permission.GET_ACCOUNTS" />']).toBeDefined();
+            expect(munge['AndroidManifest.xml']['/manifest']['<uses-permission android:name="android.permission.WAKE_LOCK" />']).toBeDefined();
+            expect(munge['AndroidManifest.xml']['/manifest']['<permission android:name="$PACKAGE_NAME.permission.C2D_MESSAGE" android:protectionLevel="signature" />']).toBeDefined();
+            expect(munge['AndroidManifest.xml']['/manifest']['<uses-permission android:name="$PACKAGE_NAME.permission.C2D_MESSAGE" />']).toBeDefined();
+            expect(munge['AndroidManifest.xml']['/manifest']['<uses-permission android:name="com.google.android.c2dm.permission.RECEIVE" />']).toBeDefined();
+        });
+        it('should not use xml comments as config munge leaves', function() {
+            var munge = configChanges.generate_plugin_config_munge(childrenplugin, 'android');
+            expect(munge['AndroidManifest.xml']['/manifest']['<!--library-->']).not.toBeDefined();
+            expect(munge['AndroidManifest.xml']['/manifest']['<!-- GCM connects to Google Services. -->']).not.toBeDefined();
+        });
+        it('should increment config heirarchy leaves if dfferent config-file elements target the same file + selector + xml', function() {
+            var munge = configChanges.generate_plugin_config_munge(configplugin, 'android');
+            expect(munge['res/xml/config.xml']['/widget']['<poop />']).toEqual(2);
+        });
+        it('should take into account interpolation variables', function() {
+            var munge = configChanges.generate_plugin_config_munge(childrenplugin, 'android', {PACKAGE_NAME:'ca.filmaj.plugins'});
+            expect(munge['AndroidManifest.xml']['/manifest']['<uses-permission android:name="ca.filmaj.plugins.permission.C2D_MESSAGE" />']).toBeDefined();
+        });
+        it('should special case plugins-plist elements into own property', function() {
+        });
+    });
+
+    describe('processing of plugins (via process method)', function() {
+        beforeEach(function() {
+            shell.cp('-rf', dummyplugin, plugins_dir);
+        });
+        it('should generate config munges for queued plugins', function() {
+            shell.cp('-rf', android_two_project, temp);
+            var cfg = configChanges.get_platform_json(plugins_dir, 'android');
+            cfg.prepare_queue.installed = [{'plugin':'DummyPlugin', 'vars':{}}];
+            configChanges.save_platform_json(cfg, plugins_dir, 'android');
+            var spy = spyOn(configChanges, 'generate_plugin_config_munge').andReturn({});
+            configChanges.process(plugins_dir, temp, 'android');
+            expect(spy).toHaveBeenCalledWith(path.join(plugins_dir, 'DummyPlugin'), 'android', {});
+        });
+        it('should get a reference to existing config munge by calling get_platform_json', function() {
+            shell.cp('-rf', android_two_project, temp);
+            var spy = spyOn(configChanges, 'get_platform_json').andReturn({
+                prepare_queue:{
+                    installed:[],
+                    uninstalled:[]
+                },
+                config_munge:{}
+            });
+            configChanges.process(plugins_dir, temp, 'android');
+            expect(spy).toHaveBeenCalledWith(plugins_dir, 'android');
+        });
+        describe(': installation', function() {
+
+            it('should call graftXML for every new config munge it introduces (every leaf in config munge that does not exist)', function() {
+                shell.cp('-rf', android_two_project, temp);
+                var cfg = configChanges.get_platform_json(plugins_dir, 'android');
+                cfg.prepare_queue.installed = [{'plugin':'DummyPlugin', 'vars':{}}];
+                configChanges.save_platform_json(cfg, plugins_dir, 'android');
+
+                var spy = spyOn(xml_helpers, 'graftXML').andReturn(true);
+
+                var manifest_doc = new et.ElementTree(et.XML(fs.readFileSync(path.join(temp, 'AndroidManifest.xml'), 'utf-8')));
+                var munge = dummy_xml.find('./platform[@name="android"]/config-file[@target="AndroidManifest.xml"]');
+                configChanges.process(plugins_dir, temp, 'android');
+                expect(spy.calls.length).toEqual(2);
+                expect(spy.argsForCall[0][2]).toEqual('/manifest/application');
+                expect(spy.argsForCall[1][2]).toEqual('/cordova/plugins');
+            });
+            it('should not call graftXML for a config munge that already exists from another plugin', function() {
+                shell.cp('-rf', android_two_project, temp);
+                shell.cp('-rf', configplugin, plugins_dir);
+                var cfg = configChanges.get_platform_json(plugins_dir, 'android');
+                cfg.prepare_queue.installed = [{'plugin':'ConfigTestPlugin', 'vars':{}}];
+                configChanges.save_platform_json(cfg, plugins_dir, 'android');
+
+                var spy = spyOn(xml_helpers, 'graftXML').andReturn(true);
+                configChanges.process(plugins_dir, temp, 'android');
+                expect(spy.calls.length).toEqual(1);
+            });
+            it('should not call graftXML for a config munge targeting a config file that does not exist', function() {
+                shell.cp('-rf', android_two_project, temp);
+                var cfg = configChanges.get_platform_json(plugins_dir, 'android');
+                cfg.prepare_queue.installed = [{'plugin':'DummyPlugin', 'vars':{}}];
+                configChanges.save_platform_json(cfg, plugins_dir, 'android');
+
+                var spy = spyOn(fs, 'readFileSync').andCallThrough();
+
+                configChanges.process(plugins_dir, temp, 'android');
+                expect(spy).not.toHaveBeenCalledWith(path.join(temp, 'res', 'xml', 'plugins.xml'), 'utf-8');
+            });
+            it('should move successfully installed plugins from queue to installed plugins section, and include/retain vars if applicable', function() {
+                shell.cp('-rf', varplugin, plugins_dir);
+                var cfg = configChanges.get_platform_json(plugins_dir, 'android');
+                cfg.prepare_queue.installed = [{'plugin':'VariablePlugin', 'vars':{"API_KEY":"hi"}}];
+                configChanges.save_platform_json(cfg, plugins_dir, 'android');
+
+                configChanges.process(plugins_dir, temp, 'android');
+
+                cfg = configChanges.get_platform_json(plugins_dir, 'android');
+                expect(cfg.prepare_queue.installed.length).toEqual(0);
+                expect(cfg.installed_plugins['com.adobe.vars']).toBeDefined();
+                expect(cfg.installed_plugins['com.adobe.vars']['API_KEY']).toEqual('hi');
+            });
+            it('should save changes to global config munge after completing an install', function() {
+                shell.cp('-rf', android_two_project, temp);
+                shell.cp('-rf', varplugin, plugins_dir);
+                var cfg = configChanges.get_platform_json(plugins_dir, 'android');
+                cfg.prepare_queue.installed = [{'plugin':'VariablePlugin', 'vars':{"API_KEY":"hi"}}];
+                configChanges.save_platform_json(cfg, plugins_dir, 'android');
+
+                var spy = spyOn(configChanges, 'save_platform_json');
+                configChanges.process(plugins_dir, temp, 'android');
+                expect(spy).toHaveBeenCalled();
+            });
+            describe('of <plugins-plist> elements', function() {
+                it('should only be used in an applicably old cordova-ios projects', function() {
+                    shell.cp('-rf', ios_plist_project, temp);
+                });
+                it('should not be used in an applicably new cordova-ios projects', function() {
+                    shell.cp('-rf', ios_config_xml_project, temp);
+                    var pls = copyArray(plist_els);
+                    var spy = spyOn(plist, 'parseFileSync').andReturn({Plugins:{}});
+                    ios.install(pls, dummy_id, temp, dummyplugin, {});
+                    expect(spy).not.toHaveBeenCalledWith(path.join(temp, 'SampleApp', 'config.xml'));
+                });
+            });
+        });
+
+        describe(': uninstallation', function() {
+            it('should call pruneXML for every config munge it completely removes from the app (every leaf that is decremented to 0)');
+            it('should call pruneXML with variables to interpolate if applicable');
+            it('should not call pruneXML for a config munge that another plugin depends on');
+            it('should not call pruneXML for a config munge targeting a config file that does not exist');
+            it('should remove uninstalled plugins from installed plugins list');
+            it('should save changes to global config munge after completing an uninstall');
+        });
+    });
+});

http://git-wip-us.apache.org/repos/asf/cordova-plugman/blob/1395f63a/src/install.js
----------------------------------------------------------------------
diff --git a/src/install.js b/src/install.js
index c71d2ea..001f5e6 100644
--- a/src/install.js
+++ b/src/install.js
@@ -1,6 +1,7 @@
 var path = require('path'),
     fs   = require('fs'),
     et   = require('elementtree'),
+    config_changes = require('./util/config-changes');
     platform_modules = require('./platforms');
 
 // TODO: is name necessary as a param ehre?
@@ -53,13 +54,35 @@ function runInstall(platform, project_dir, plugin_dir, plugins_dir, cli_variable
     });
     if (missing_vars.length > 0) {
         var err = new Error('Variable(s) missing: ' + missing_vars.join(", "));
-        if (callback) {
-            callback(err);
-            return;
+        if (callback) callback(err);
+        else throw err;
+        return;
+    }
+
+    // check if platform has plugin fully installed or queued already.
+    var platform_config = config_changes.get_platform_json(plugins_dir, platform);
+    var plugin_basename = path.basename(plugin_dir);
+    if (platform_config.prepare_queue.installed.indexOf(plugin_basename) > -1) {
+        var err = new Error('plugin "' + plugin_basename + '" is already installed (but needs to be prepared)');
+        if (callback) callback(err);
+        else throw err;
+        return;
+    }
+    var is_fully_installed = false;
+    Object.keys(platform_config.installed_plugins).forEach(function(installed_plugin_id) {
+        if (installed_plugin_id == plugin_id) {
+            is_fully_installed = true;
         }
+    });
+    if (is_fully_installed) {
+        var err = new Error('plugin "' + plugin_basename + '" (id: '+plugin_id+') is already installed');
+        if (callback) callback(err);
         else throw err;
+        return;
     }
+    // TODO: handle asset elements
 
+    // TODO: if plugin does not have platform tag but has platform-agnostic config changes, should we queue it up?
     var platformTag = plugin_et.find('./platform[@name="'+platform+'"]');
     if (!platformTag) {
         // Either this plugin doesn't support this platform, or it's a JS-only plugin.
@@ -69,23 +92,19 @@ function runInstall(platform, project_dir, plugin_dir, plugins_dir, cli_variable
         if (callback) callback();
         return;
     }
-    var handler = platform_modules[platform];
-
-    // TODO: check if platform has plugin installed already.
 
     // parse plugin.xml into transactions
+    var handler = platform_modules[platform];
     var txs = [];
     var sourceFiles = platformTag.findall('./source-file'),
         headerFiles = platformTag.findall('./header-file'),
         resourceFiles = platformTag.findall('./resource-file'),
         assets = platformTag.findall('./asset'),
-        frameworks = platformTag.findall('./framework'),
-        pluginsPlist = platformTag.findall('./plugins-plist'),
-        configChanges = platformTag.findall('./config-file');
+        frameworks = platformTag.findall('./framework');
 
     assets = assets.concat(plugin_et.findall('./asset'));
 
-    txs = txs.concat(sourceFiles, headerFiles, resourceFiles, frameworks, configChanges, assets, pluginsPlist);
+    txs = txs.concat(sourceFiles, headerFiles, resourceFiles, frameworks, assets);
 
     // pass platform-specific transactions into install
     handler.install(txs, plugin_id, project_dir, plugin_dir, filtered_variables, function(err) {
@@ -110,6 +129,8 @@ function runInstall(platform, project_dir, plugin_dir, plugins_dir, cli_variable
             }
         } else {
             // WIN!
+            // queue up the plugin so prepare knows what to do.
+            config_changes.add_installed_plugin_to_prepare_queue(plugins_dir, path.basename(plugin_dir), platform, filtered_variables);
             // call prepare after a successful install
             require('./../plugman').prepare(project_dir, platform, plugins_dir);
 

http://git-wip-us.apache.org/repos/asf/cordova-plugman/blob/1395f63a/src/uninstall.js
----------------------------------------------------------------------
diff --git a/src/uninstall.js b/src/uninstall.js
index aca3f1f..a740d4f 100644
--- a/src/uninstall.js
+++ b/src/uninstall.js
@@ -1,17 +1,15 @@
 var path = require('path'),
     fs   = require('fs'),
     et   = require('elementtree'),
-    platform_modules = require('./platforms');
-
+    platform_modules = require('./platforms'),
+    config_changes = require('./util/config-changes');
 
 module.exports = function uninstallPlugin(platform, project_dir, name, plugins_dir, cli_variables, callback) {
     if (!platform_modules[platform]) {
         var err = new Error(platform + " not supported.");
-        if (callback) {
-            callback(err);
-            return;
-        }
+        if (callback) callback(err);
         else throw err;
+        return;
     }
 
     // Check that the plugin has already been fetched.
@@ -19,11 +17,9 @@ module.exports = function uninstallPlugin(platform, project_dir, name, plugins_d
 
     if (!fs.existsSync(plugin_dir)) {
         var err = new Error('Plugin "' + name + '" not found. Already uninstalled?');
-        if (callback) {
-            callback(err);
-            return;
-        }
+        if (callback) callback(err);
         else throw err;
+        return;
     }
 
     runUninstall(platform, project_dir, plugin_dir, plugins_dir, cli_variables, callback);
@@ -36,6 +32,8 @@ function runUninstall(platform, project_dir, plugin_dir, plugins_dir, cli_variab
     var name         = plugin_et.findall('name').text;
     var plugin_id    = plugin_et._root.attrib['id'];
 
+    // TODO: remove any asset elements
+
     var platformTag = plugin_et.find('./platform[@name="'+platform+'"]');
     var platformTag = plugin_et.find('./platform[@name="'+platform+'"]');
     if (!platformTag) {
@@ -46,9 +44,9 @@ function runUninstall(platform, project_dir, plugin_dir, plugins_dir, cli_variab
         if (callback) callback();
         return;
     }
-    var handler = platform_modules[platform];
 
     // parse plugin.xml into transactions
+    var handler = platform_modules[platform];
     var txs = [];
     var sourceFiles = platformTag.findall('./source-file'),
         headerFiles = platformTag.findall('./header-file'),
@@ -84,6 +82,8 @@ function runUninstall(platform, project_dir, plugin_dir, plugins_dir, cli_variab
             }
         } else {
             // WIN!
+            // queue up the plugin so prepare can remove the config changes
+            config_changes.add_uninstalled_plugin_to_prepare_queue(plugins_dir, path.basename(plugin_dir), platform);
             // call prepare after a successful uninstall
             require('./../plugman').prepare(project_dir, platform, plugins_dir);
             if (callback) callback();

http://git-wip-us.apache.org/repos/asf/cordova-plugman/blob/1395f63a/src/util/config-changes.js
----------------------------------------------------------------------
diff --git a/src/util/config-changes.js b/src/util/config-changes.js
index d931039..83f4544 100644
--- a/src/util/config-changes.js
+++ b/src/util/config-changes.js
@@ -17,22 +17,267 @@
  *
 */
 
-// takes an xml list of config-file tags
-// returns a JS object with an easy to use structure
+var fs   = require('fs'),
+    path = require('path'),
+    glob = require('glob'),
+    plist = require('plist'),
+    bplist = require('bplist-parser'),
+    et   = require('elementtree'),
+    platforms = require('./../platforms'),
+    xml_helpers = require('./../util/xml-helpers');
 
-module.exports = function configChanges(baseTag) {
-    var tags = baseTag.findall('./config-file'),
-        files = {};
+function checkPlatform(platform) {
+    if (!(platform in platforms)) throw new Error('platform "' + platform + '" not recognized.');
+}
+
+module.exports = {
+    add_installed_plugin_to_prepare_queue:function(plugins_dir, plugin, platform, vars) {
+        checkPlatform(platform);
+
+        var config = module.exports.get_platform_json(plugins_dir, platform);
+        config.prepare_queue.installed.push({'plugin':plugin, 'vars':vars});
+        module.exports.save_platform_json(config, plugins_dir, platform);
+    },
+    add_uninstalled_plugin_to_prepare_queue:function(plugins_dir, plugin, platform) {
+        checkPlatform(platform);
 
-    tags.forEach(function (tag) {
-        var target = tag.attrib['target'];
+        var plugin_xml = new et.ElementTree(et.XML(fs.readFileSync(path.join(plugins_dir, plugin, 'plugin.xml'), 'utf-8')));
+        var config = module.exports.get_platform_json(plugins_dir, platform);
+        config.prepare_queue.uninstalled.push({'plugin':plugin, 'id':plugin_xml._root.attrib['id']});
+        module.exports.save_platform_json(config, plugins_dir, platform);
+    },
+    get_platform_json:function(plugins_dir, platform) {
+        checkPlatform(platform);
 
-        if (files[target]) {
-            files[target].push(tag);
+        var filepath = path.join(plugins_dir, platform + '.json'); 
+        if (fs.existsSync(filepath)) {
+            return JSON.parse(fs.readFileSync(filepath, 'utf-8'));
         } else {
-            files[target] = [tag];
+            var config = {
+                prepare_queue:{installed:[], uninstalled:[]},
+                config_munge:{},
+                installed_plugins:{}
+            };
+            fs.writeFileSync(filepath, JSON.stringify(config), 'utf-8');
+            return config;
         }
-    });
+    },
+    save_platform_json:function(config, plugins_dir, platform) {
+        checkPlatform(platform);
+
+        var filepath = path.join(plugins_dir, platform + '.json'); 
+        fs.writeFileSync(filepath, JSON.stringify(config), 'utf-8');
+    },
+    generate_plugin_config_munge:function(plugin_dir, platform, vars) {
+        checkPlatform(platform);
+
+        var munge = {};
+        var plugin_xml = new et.ElementTree(et.XML(fs.readFileSync(path.join(plugin_dir, 'plugin.xml'), 'utf-8')));
+
+        var platformTag = plugin_xml.find('platform[@name="' + platform + '"]');
+        var changes = platformTag.findall('config-file');
+        // note down plugins-plist munges in special section of munge obj
+        var plugins_plist = platformTag.findall('plugins-plist');
+        plugins_plist.forEach(function(pl) {
+            if (!munge['plugins-plist']) {
+                munge['plugins-plist'] = {};
+            }
+            var key = pl.attrib['key'];
+            var value = pl.attrib['string'];
+            if (!munge['plugins-plist'][key]) {
+                munge['plugins-plist'][key] = string;
+            }
+        });
+
+        changes.forEach(function(change) {
+            var target = change.attrib['target'];
+            if (!munge[target]) {
+                munge[target] = {};
+            }
+            if (!munge[target][change.attrib['parent']]) {
+                munge[target][change.attrib['parent']] = {};
+            }
+            var xmls = change.getchildren();
+            xmls.forEach(function(xml) {
+                // 1. stringify each xml
+                var stringified = (new et.ElementTree(xml)).write({xml_declaration:false});
+                // interp vars
+                vars && Object.keys(vars).forEach(function(key) {
+                    var regExp = new RegExp("\\$" + key, "g");
+                    stringified = stringified.replace(regExp, vars[key]);
+                });
+                // 2. add into munge
+                if (!munge[target][change.attrib['parent']][stringified]) {
+                    munge[target][change.attrib['parent']][stringified] = 0;
+                }
+                munge[target][change.attrib['parent']][stringified] += 1;
+            });
+        });
+        return munge;
+    },
+    process:function(plugins_dir, project_dir, platform) {
+        checkPlatform(platform);
+
+        var platform_config = module.exports.get_platform_json(plugins_dir, platform);
+        // Uninstallation first
+        platform_config.prepare_queue.uninstalled.forEach(function(u) {
+            var plugin_dir = path.join(plugins_dir, u.plugin);
+            var plugin_id = u.id;
+            var plugin_vars = platform_config.installed_plugins[plugin_id].vars;
+
+            // get config munge, aka how did this plugin change various config files
+            var config_munge = module.exports.generate_plugin_config_munge(plugin_dir, platform, plugin_vars);
+            // global munge looks at all plugins' changes to config files
+            var global_munge = platform_config.config_munge;
+            
+            // Traverse config munge and decrement global munge
+            Object.keys(config_munge).forEach(function(file) {
+                if (file == 'plugins-plist' && platform == 'ios') {
+                    if (global_munge[file]) {
+                        Object.keys(config_munge[file]).forEach(function(key) {
+                            if (global_munge[file][key]) {
+                                // prune from old plist if exists
+                                var plistfile = glob.sync(path.join(project_dir, '**', '{PhoneGap,Cordova}.plist'));
+                                if (plistfile.length > 0) {
+                                    plistfile = plistfile[0];
+                                    // determine if this is a binary or ascii plist and choose the parser
+                                    // this is temporary until binary support is added to node-plist
+                                    var pl = (isBinaryPlist(plistfile) ? bplist : plist);
+                                    var plistObj = pl.parseFileSync(plistfile);
+                                    delete plistObj.Plugins[key];
+                                    fs.writeFileSync(plistfile, plist.build(plistObj));
+                                }
+                                delete global_munge[file][key];
+                            }
+                        });
+                    }
+                } else if (global_munge[file]) {
+                    Object.keys(config_munge[file]).forEach(function(selector) {
+                        if (global_munge[file][selector]) {
+                            Object.keys(config_munge[file][selector]).forEach(function(xml_child) {
+                                if (global_munge[file][selector][xml_child]) {
+                                    global_munge[file][selector][xml_child] -= 1;
+                                    if (global_munge[file][selector][xml_child] === 0) {
+                                        // this xml child is no longer necessary, prune it
+                                        // config.xml referenced in ios config changes refer to the project's config.xml, which we need to glob for.
+                                        var filepath = resolveConfigFilePath(project_dir, platform, file);
+                                        if (fs.existsSync(filepath)) {
+                                            var doc = new et.ElementTree(et.XML(fs.readFileSync(filepath, 'utf-8')));
+                                            var xml_to_prune = [new et.ElementTree(et.XML(xml_child)).getroot()];
+                                            if (xml_helpers.pruneXML(doc, xml_to_prune, selector)) {
+                                                // were good, write out the file!
+                                                fs.writeFileSync(filepath, doc.write(), 'utf-8');
+                                            } else {
+                                                // uh oh
+                                                throw new Error('pruning xml during config uninstall went bad :(');
+                                            }
+                                        }
+                                        delete global_munge[file][selector][xml_child];
+                                    }
+                                }
+                            });
+                        }
+                    });
+                }
+            });
+            platform_config.config_munge = global_munge;
+
+            // Remove from installed_plugins
+            delete platform_config.installed_plugins[plugin_id]
+        });
+
+        // Empty out uninstalled queue.
+        platform_config.prepare_queue.uninstalled = [];
+
+        // Now handle instalation
+        platform_config.prepare_queue.installed.forEach(function(u) {
+            var plugin_dir = path.join(plugins_dir, u.plugin);
+            var plugin_vars = u.vars;
+            var plugin_id = (new et.ElementTree(et.XML(fs.readFileSync(path.join(plugin_dir, 'plugin.xml'), 'utf-8'))))._root.attrib['id'];
+
+            // get config munge, aka how should this plugin change various config files
+            var config_munge = module.exports.generate_plugin_config_munge(plugin_dir, platform, plugin_vars);
+            // global munge looks at all plugins' changes to config files
+            var global_munge = platform_config.config_munge;
+            
+            // Traverse config munge and decrement global munge
+            Object.keys(config_munge).forEach(function(file) {
+                if (!global_munge[file]) {
+                    global_munge[file] = {};
+                }
+                Object.keys(config_munge[file]).forEach(function(selector) {
+                    if (file == 'config.xml' && platform == 'ios') {
+                        var key = selector;
+                        if (!global_munge[file][key]) {
+                            // this key does not exist, so add it to plist
+                            global_munge[file][key] = config_munge[file][key];
+                            var plistfile = glob.sync(path.join(project_dir, '**', '{PhoneGap,Cordova}.plist'));
+                            if (plistfile.length > 0) {
+                                plistfile = plistfile[0];
+                                // determine if this is a binary or ascii plist and choose the parser
+                                // this is temporary until binary support is added to node-plist
+                                var pl = (isBinaryPlist(plistfile) ? bplist : plist);
+                                var plistObj = pl.parseFileSync(plistfile);
+                                plistObj.Plugins[key] = config_munge[file][key];
+                                fs.writeFileSync(plistfile, plist.build(plistObj));
+                            }
+                        }
+                    } else {
+                        if (!global_munge[file][selector]) {
+                            global_munge[file][selector] = {};
+                        }
+                        Object.keys(config_munge[file][selector]).forEach(function(xml_child) {
+                            if (!global_munge[file][selector][xml_child]) {
+                                global_munge[file][selector][xml_child] = 0;
+                            }
+                            global_munge[file][selector][xml_child] += 1;
+                            if (global_munge[file][selector][xml_child] == 1) {
+                                // this xml child is new, graft it (only if config file exists)
+                                // config file may be in a place not exactly specified in the target
+                                var filepath = resolveConfigFilePath(project_dir, platform, file);
+                                if (fs.existsSync(filepath)) {
+                                    var doc = new et.ElementTree(et.XML(fs.readFileSync(filepath, 'utf-8')));
+                                    var xml_to_graft = [new et.ElementTree(et.XML(xml_child)).getroot()];
+                                    if (xml_helpers.graftXML(doc, xml_to_graft, selector)) {
+                                        // were good, write out the file!
+                                        fs.writeFileSync(filepath, doc.write(), 'utf-8');
+                                    } else {
+                                        // uh oh
+                                        throw new Error('grafting xml during config install went bad :(');
+                                    }
+                                }
+                            }
+                        });
+                    }
+                });
+            });
+            platform_config.config_munge = global_munge;
+
+            // Move to installed_plugins
+            platform_config.installed_plugins[plugin_id] = plugin_vars;
+        });
+
+        // Empty out installed queue.
+        platform_config.prepare_queue.installed = [];
+
+        // save
+        module.exports.save_platform_json(platform_config, plugins_dir, platform);
+    }
+};
+
+// determine if a plist file is binary
+function isBinaryPlist(filename) {
+    // I wish there was a synchronous way to read only the first 6 bytes of a
+    // file. This is wasteful :/ 
+    var buf = '' + fs.readFileSync(filename, 'utf8');
+    // binary plists start with a magic header, "bplist"
+    return buf.substring(0, 6) === 'bplist';
+}
 
-    return files;
+function resolveConfigFilePath(project_dir, platform, file) {
+    var filepath = path.join(project_dir, file);
+    if (platform == 'ios' && file == 'config.xml') {
+        filepath = glob.sync(path.join(project_dir, '**', 'config.xml'))[0];
+    }
+    return filepath;
 }

http://git-wip-us.apache.org/repos/asf/cordova-plugman/blob/1395f63a/src/util/xml-helpers.js
----------------------------------------------------------------------
diff --git a/src/util/xml-helpers.js b/src/util/xml-helpers.js
index f270132..e96fc20 100644
--- a/src/util/xml-helpers.js
+++ b/src/util/xml-helpers.js
@@ -25,7 +25,7 @@ var fs = require('fs')
   , path = require('path')
   , et = require('elementtree');
 
-exports = module.exports = {
+module.exports = {
     moveProjFile: function(origFile, projPath, callback) {
         var src = path.resolve(projPath, origFile)
           , dest = src.replace('.orig', '');
@@ -63,7 +63,7 @@ exports = module.exports = {
         }
 
         for (i; i < one._children.length; i++) {
-            if (!exports.equalNodes(one._children[i], two._children[i])) {
+            if (!module.exports.equalNodes(one._children[i], two._children[i])) {
                 return false;
             }
         }
@@ -72,7 +72,7 @@ exports = module.exports = {
     },
 
     // adds node to doc at selector
-    graftXML: function (doc, nodes, selector) {
+    graftXML: function(doc, nodes, selector) {
         var ROOT = /^\/([^\/]*)/
           , ABSOLUTE = /^\/([^\/]*)\/(.*)/
           , parent, tagName, subSelector;
@@ -166,7 +166,7 @@ function uniqueChild(node, parent) {
         return true;
     } else  {
         for (i; i < matchingKids.length; i++) {
-            if (exports.equalNodes(node, matchingKids[i])) {
+            if (module.exports.equalNodes(node, matchingKids[i])) {
                 return false;
             }
         }


[2/2] git commit: a few more specs and a tweak to plugins-plist handling.

Posted by fi...@apache.org.
a few more specs and a tweak to plugins-plist handling.


Project: http://git-wip-us.apache.org/repos/asf/cordova-plugman/repo
Commit: http://git-wip-us.apache.org/repos/asf/cordova-plugman/commit/94be165c
Tree: http://git-wip-us.apache.org/repos/asf/cordova-plugman/tree/94be165c
Diff: http://git-wip-us.apache.org/repos/asf/cordova-plugman/diff/94be165c

Branch: refs/heads/master
Commit: 94be165cca2d9340cdbdd5153c93efe045bfa812
Parents: 1395f63
Author: Fil Maj <ma...@gmail.com>
Authored: Mon Apr 29 00:33:09 2013 -0700
Committer: Fil Maj <ma...@gmail.com>
Committed: Mon Apr 29 00:33:09 2013 -0700

----------------------------------------------------------------------
 spec/util/config-changes.spec.js |   38 ++++++++++++++++++++++----------
 src/util/config-changes.js       |    4 +-
 2 files changed, 28 insertions(+), 14 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/cordova-plugman/blob/94be165c/spec/util/config-changes.spec.js
----------------------------------------------------------------------
diff --git a/spec/util/config-changes.spec.js b/spec/util/config-changes.spec.js
index 1d88bf7..1c2b27a 100644
--- a/spec/util/config-changes.spec.js
+++ b/spec/util/config-changes.spec.js
@@ -4,6 +4,7 @@ var configChanges = require('../../src/util/config-changes'),
     os      = require('osenv'),
     et      = require('elementtree'),
     path    = require('path'),
+    plist = require('plist'),
     shell   = require('shelljs'),
     temp    = path.join(os.tmpdir(), 'plugman'),
     dummyplugin = path.join(__dirname, '..', 'plugins', 'DummyPlugin'),
@@ -12,6 +13,7 @@ var configChanges = require('../../src/util/config-changes'),
     varplugin = path.join(__dirname, '..', 'plugins', 'VariablePlugin'),
     android_two_project = path.join(__dirname, '..', 'projects', 'android_two', '*'),
     ios_plist_project = path.join(__dirname, '..', 'projects', 'ios-plist', '*'),
+    ios_config_xml = path.join(__dirname, '..', 'projects', 'ios-config-xml', '*'),
     plugins_dir = path.join(temp, 'cordova', 'plugins');
 
 var dummy_xml = new et.ElementTree(et.XML(fs.readFileSync(path.join(dummyplugin, 'plugin.xml'), 'utf-8')));
@@ -186,6 +188,9 @@ describe('config-changes module', function() {
             expect(munge['AndroidManifest.xml']['/manifest']['<uses-permission android:name="ca.filmaj.plugins.permission.C2D_MESSAGE" />']).toBeDefined();
         });
         it('should special case plugins-plist elements into own property', function() {
+            var munge = configChanges.generate_plugin_config_munge(dummyplugin, 'ios', {});
+            expect(munge['plugins-plist']).toBeDefined();
+            expect(munge['plugins-plist']['com.phonegap.plugins.dummyplugin']).toEqual('DummyPluginCommand');
         });
     });
 
@@ -280,24 +285,33 @@ describe('config-changes module', function() {
             describe('of <plugins-plist> elements', function() {
                 it('should only be used in an applicably old cordova-ios projects', function() {
                     shell.cp('-rf', ios_plist_project, temp);
-                });
-                it('should not be used in an applicably new cordova-ios projects', function() {
-                    shell.cp('-rf', ios_config_xml_project, temp);
-                    var pls = copyArray(plist_els);
+                    shell.cp('-rf', dummyplugin, plugins_dir);
+                    var cfg = configChanges.get_platform_json(plugins_dir, 'ios');
+                    cfg.prepare_queue.installed = [{'plugin':'DummyPlugin', 'vars':{}}];
+                    configChanges.save_platform_json(cfg, plugins_dir, 'ios');
+
                     var spy = spyOn(plist, 'parseFileSync').andReturn({Plugins:{}});
-                    ios.install(pls, dummy_id, temp, dummyplugin, {});
-                    expect(spy).not.toHaveBeenCalledWith(path.join(temp, 'SampleApp', 'config.xml'));
+                    configChanges.process(plugins_dir, temp, 'ios');
+                    expect(spy).toHaveBeenCalledWith(path.join(temp, 'SampleApp', 'PhoneGap.plist'));
                 });
             });
         });
 
         describe(': uninstallation', function() {
-            it('should call pruneXML for every config munge it completely removes from the app (every leaf that is decremented to 0)');
-            it('should call pruneXML with variables to interpolate if applicable');
-            it('should not call pruneXML for a config munge that another plugin depends on');
-            it('should not call pruneXML for a config munge targeting a config file that does not exist');
-            it('should remove uninstalled plugins from installed plugins list');
-            it('should save changes to global config munge after completing an uninstall');
+            it('should call pruneXML for every config munge it completely removes from the app (every leaf that is decremented to 0)', function() {
+            });
+            it('should call pruneXML with variables to interpolate if applicable', function() {
+            });
+            it('should not call pruneXML for a config munge that another plugin depends on', function() {
+            });
+            it('should not call pruneXML for a config munge targeting a config file that does not exist', function() {
+            });
+            it('should remove uninstalled plugins from installed plugins list', function() {
+            });
+            it('should only parse + remove plist plugin entries in applicably old ios projects', function() {
+            });
+            it('should save changes to global config munge after completing an uninstall', function() {
+            });
         });
     });
 });

http://git-wip-us.apache.org/repos/asf/cordova-plugman/blob/94be165c/src/util/config-changes.js
----------------------------------------------------------------------
diff --git a/src/util/config-changes.js b/src/util/config-changes.js
index 83f4544..534c8d3 100644
--- a/src/util/config-changes.js
+++ b/src/util/config-changes.js
@@ -85,7 +85,7 @@ module.exports = {
             var key = pl.attrib['key'];
             var value = pl.attrib['string'];
             if (!munge['plugins-plist'][key]) {
-                munge['plugins-plist'][key] = string;
+                munge['plugins-plist'][key] = value;
             }
         });
 
@@ -206,7 +206,7 @@ module.exports = {
                     global_munge[file] = {};
                 }
                 Object.keys(config_munge[file]).forEach(function(selector) {
-                    if (file == 'config.xml' && platform == 'ios') {
+                    if (file == 'plugins-plist' && platform == 'ios') {
                         var key = selector;
                         if (!global_munge[file][key]) {
                             // this key does not exist, so add it to plist