You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@cordova.apache.org by ag...@apache.org on 2014/03/28 21:06:45 UTC

git commit: CB-6344: Specify after which sibling to add config-changes in plugin.xml

Repository: cordova-plugman
Updated Branches:
  refs/heads/master 2f18534b7 -> ac1160d47


CB-6344: Specify after which sibling to add config-changes in plugin.xml

* refactor munges - now they are in a more extensible form (munge.files[file].parents[parent][index] { xml, count, after })
* add an after parameter to graftXML which contains a list of possible predecessor elements separated by semicolon
* fix unit tests and add new ones

github: close #68


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

Branch: refs/heads/master
Commit: ac1160d470db03c2d2de2bbd7ad0ece336cf0394
Parents: 2f18534
Author: Martin Bektchiev <ma...@telerik.com>
Authored: Wed Mar 26 10:15:39 2014 +0200
Committer: Andrew Grieve <ag...@chromium.org>
Committed: Fri Mar 28 16:03:48 2014 -0400

----------------------------------------------------------------------
 spec/platforms/wp8.spec.js          |  21 +++
 spec/plugins/DummyPlugin/plugin.xml |  22 +++
 spec/util/config-changes.spec.js    |  70 +++++-----
 spec/util/xml-helpers.spec.js       |  27 ++++
 src/util/config-changes.js          | 228 +++++++++++++++++++++----------
 src/util/xml-helpers.js             |  24 +++-
 6 files changed, 285 insertions(+), 107 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/cordova-plugman/blob/ac1160d4/spec/platforms/wp8.spec.js
----------------------------------------------------------------------
diff --git a/spec/platforms/wp8.spec.js b/spec/platforms/wp8.spec.js
index 398e567..2b0f265 100644
--- a/spec/platforms/wp8.spec.js
+++ b/spec/platforms/wp8.spec.js
@@ -70,6 +70,11 @@ describe('wp8 project handler', function() {
     });
 
     describe('installation', function() {
+        var done;
+        function installPromise(f) {
+            done = false;
+            f.then(function() { done = true; }, function(err) { done = err; });
+        }
         beforeEach(function() {
             shell.mkdir('-p', temp);
         });
@@ -102,6 +107,22 @@ describe('wp8 project handler', function() {
                 }).toThrow('"' + target + '" already exists!');
             });
         });
+        describe('of <config-changes> elements', function() {
+            beforeEach(function() {
+                shell.cp('-rf', path.join(wp8_project, '*'), temp);
+            });
+            it('should process and pass the after parameter to graftXML', function () {
+                var graftXML = spyOn(xml_helpers, 'graftXML').andCallThrough();
+
+                runs(function () { installPromise(install('wp8', temp, dummyplugin, plugins_dir, {})); });
+                waitsFor(function () { return done; }, 'install promise never resolved', 500);
+                runs(function () {
+                    expect(graftXML).toHaveBeenCalledWith(jasmine.any(Object), jasmine.any(Array), "/Deployment/App", "Tokens");
+                    expect(graftXML).toHaveBeenCalledWith(jasmine.any(Object), jasmine.any(Array), "/Deployment/App/Extensions", "Extension");
+                    expect(graftXML).toHaveBeenCalledWith(jasmine.any(Object), jasmine.any(Array), "/Deployment/App/Extensions", "FileTypeAssociation;Extension");
+                });
+            });
+        });
     });
 
     describe('uninstallation', function() {

http://git-wip-us.apache.org/repos/asf/cordova-plugman/blob/ac1160d4/spec/plugins/DummyPlugin/plugin.xml
----------------------------------------------------------------------
diff --git a/spec/plugins/DummyPlugin/plugin.xml b/spec/plugins/DummyPlugin/plugin.xml
index bc71119..dcec3bf 100644
--- a/spec/plugins/DummyPlugin/plugin.xml
+++ b/spec/plugins/DummyPlugin/plugin.xml
@@ -155,6 +155,28 @@
             <feature id="dummyPlugin" required="true" version="1.0.0.0"/>
         </config-file>
 
+        <config-file target="Properties/WMAppManifest.xml" parent="/Deployment/App" after="Tokens">
+            <Extensions />
+        </config-file>
+
+        <config-file target="Properties/WMAppManifest.xml" parent="/Deployment/App/Extensions" after="Extension">
+            <Extension ExtensionName="DummyExtension1" ConsumerID="{5B04B775-356B-4AA0-AAF8-6491FFEA5661}" TaskID="_default" ExtraFile="Extensions\\Extras.xml" />
+            <Extension ExtensionName="DummyExtension2" ConsumerID="{5B04B775-356B-4AA0-AAF8-6491FFEA5661}" TaskID="_default" ExtraFile="Extensions\\Extras.xml" />
+        </config-file>
+
+        <config-file target="Properties/WMAppManifest.xml" parent="/Deployment/App/Extensions" after="FileTypeAssociation;Extension">
+            <FileTypeAssociation TaskID="_default" Name="DummyFileType1" NavUriFragment="fileToken=%s">
+                <SupportedFileTypes>
+                    <FileType ContentType="application/dummy1">.dummy1</FileType>
+                </SupportedFileTypes>
+            </FileTypeAssociation>
+            <FileTypeAssociation TaskID="_default" Name="DummyFileType2" NavUriFragment="fileToken=%s">
+                <SupportedFileTypes>
+                    <FileType ContentType="application/dummy2">.dummy2</FileType>
+                </SupportedFileTypes>
+            </FileTypeAssociation>
+        </config-file>
+
         <source-file src="src/wp8/DummyPlugin.cs"/>
         <js-module src="www/dummyplugin.js" name="Dummy">
             <clobbers target="dummy" />

http://git-wip-us.apache.org/repos/asf/cordova-plugman/blob/ac1160d4/spec/util/config-changes.spec.js
----------------------------------------------------------------------
diff --git a/spec/util/config-changes.spec.js b/spec/util/config-changes.spec.js
index 3c3c918..2efae84 100644
--- a/spec/util/config-changes.spec.js
+++ b/spec/util/config-changes.spec.js
@@ -118,7 +118,10 @@ describe('config-changes module', function() {
         });
         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:{}};
+            var json = {
+                prepare_queue: {installed: [], uninstalled: []},
+                config_munge: {files: {"some_file": {parents: {"some_parent": [{"xml": "some_change", "count": 1}]}}}},
+                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));
@@ -144,64 +147,65 @@ describe('config-changes module', function() {
                 var xml;
                 var munger = new configChanges.PlatformMunger('android', temp, 'unused');
                 var munge = munger.generate_plugin_config_munge(dummyplugin, {});
-                expect(munge['AndroidManifest.xml']).toBeDefined();
-                expect(munge['AndroidManifest.xml']['/manifest/application']).toBeDefined();
+                expect(munge.files['AndroidManifest.xml']).toBeDefined();
+                expect(munge.files['AndroidManifest.xml'].parents['/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();
+                expect(configChanges.get_munge_change(munge, 'AndroidManifest.xml', '/manifest/application', xml).count).toEqual(1);
+                expect(munge.files['res/xml/plugins.xml']).toBeDefined();
+                expect(munge.files['res/xml/plugins.xml'].parents['/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();
+                expect(configChanges.get_munge_change(munge, 'res/xml/plugins.xml', '/plugins', xml).count).toEqual(1);
+                expect(munge.files['res/xml/config.xml']).toBeDefined();
+                expect(munge.files['res/xml/config.xml'].parents['/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);
+                expect(configChanges.get_munge_change(munge, 'res/xml/config.xml', '/cordova/plugins', xml).count).toEqual(1);
             });
             it('should split out multiple children of config-file elements into individual leaves', function() {
                 var munger = new configChanges.PlatformMunger('android', temp, 'unused');
                 var munge = munger.generate_plugin_config_munge(childrenplugin, {});
-                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="com.alunny.childapp.permission.C2D_MESSAGE" android:protectionLevel="signature" />']).toBeDefined();
-                expect(munge['AndroidManifest.xml']['/manifest']['<uses-permission android:name="com.alunny.childapp.permission.C2D_MESSAGE" />']).toBeDefined();
-                expect(munge['AndroidManifest.xml']['/manifest']['<uses-permission android:name="com.google.android.c2dm.permission.RECEIVE" />']).toBeDefined();
+                expect(munge.files['AndroidManifest.xml']).toBeDefined();
+                expect(munge.files['AndroidManifest.xml'].parents['/manifest']).toBeDefined();
+                expect(configChanges.get_munge_change(munge, 'AndroidManifest.xml', '/manifest', '<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />')).toBeDefined();
+                expect(configChanges.get_munge_change(munge, 'AndroidManifest.xml', '/manifest', '<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />')).toBeDefined();
+                expect(configChanges.get_munge_change(munge, 'AndroidManifest.xml', '/manifest', '<uses-permission android:name="android.permission.READ_PHONE_STATE" />')).toBeDefined();
+                expect(configChanges.get_munge_change(munge, 'AndroidManifest.xml', '/manifest', '<uses-permission android:name="android.permission.INTERNET" />')).toBeDefined();
+                expect(configChanges.get_munge_change(munge, 'AndroidManifest.xml', '/manifest', '<uses-permission android:name="android.permission.GET_ACCOUNTS" />')).toBeDefined();
+                expect(configChanges.get_munge_change(munge, 'AndroidManifest.xml', '/manifest', '<uses-permission android:name="android.permission.WAKE_LOCK" />')).toBeDefined();
+                expect(configChanges.get_munge_change(munge, 'AndroidManifest.xml', '/manifest', '<permission android:name="com.alunny.childapp.permission.C2D_MESSAGE" android:protectionLevel="signature" />')).toBeDefined();
+                expect(configChanges.get_munge_change(munge, 'AndroidManifest.xml', '/manifest', '<uses-permission android:name="com.alunny.childapp.permission.C2D_MESSAGE" />')).toBeDefined();
+                expect(configChanges.get_munge_change(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 munger = new configChanges.PlatformMunger('android', temp, 'unused');
                 var munge = munger.generate_plugin_config_munge(childrenplugin, {});
-                expect(munge['AndroidManifest.xml']['/manifest']['<!--library-->']).not.toBeDefined();
-                expect(munge['AndroidManifest.xml']['/manifest']['<!-- GCM connects to Google Services. -->']).not.toBeDefined();
+                expect(configChanges.get_munge_change(munge, 'AndroidManifest.xml', '/manifest', '<!--library-->')).not.toBeDefined();
+                expect(configChanges.get_munge_change(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 munger = new configChanges.PlatformMunger('android', temp, 'unused');
                 var munge = munger.generate_plugin_config_munge(configplugin, {});
-                expect(munge['res/xml/config.xml']['/widget']['<poop />']).toEqual(2);
+                expect(configChanges.get_munge_change(munge, 'res/xml/config.xml', '/widget', '<poop />').count).toEqual(2);
             });
             it('should take into account interpolation variables', function() {
                 var munger = new configChanges.PlatformMunger('android', temp, 'unused');
                 var munge = munger.generate_plugin_config_munge(childrenplugin, {PACKAGE_NAME:'ca.filmaj.plugins'});
-                expect(munge['AndroidManifest.xml']['/manifest']['<uses-permission android:name="ca.filmaj.plugins.permission.C2D_MESSAGE" />']).toBeDefined();
+                expect(configChanges.get_munge_change(munge, 'AndroidManifest.xml', '/manifest', '<uses-permission android:name="ca.filmaj.plugins.permission.C2D_MESSAGE" />')).toBeDefined();
             });
             it('should create munges for platform-agnostic config.xml changes', function() {
                 var munger = new configChanges.PlatformMunger('android', temp, 'unused');
                 var munge = munger.generate_plugin_config_munge(dummyplugin, {});
-                expect(munge['config.xml']['/*']['<access origin="build.phonegap.com" />']).toBeDefined();
-                expect(munge['config.xml']['/*']['<access origin="s3.amazonaws.com" />']).toBeDefined();
+                expect(configChanges.get_munge_change(munge, 'config.xml', '/*', '<access origin="build.phonegap.com" />')).toBeDefined();
+                expect(configChanges.get_munge_change(munge, 'config.xml', '/*', '<access origin="s3.amazonaws.com" />')).toBeDefined();
             });
             it('should automatically add on app java identifier as PACKAGE_NAME variable for android config munges', function() {
                 shell.cp('-rf', android_two_project, temp);
                 var munger = new configChanges.PlatformMunger('android', temp, 'unused');
                 var munge = munger.generate_plugin_config_munge(varplugin, {});
                 var expected_xml = '<package>com.alunny.childapp</package>';
-                expect(munge['AndroidManifest.xml']['/manifest'][expected_xml]).toBeDefined();
+                expect(configChanges.get_munge_change(munge, 'AndroidManifest.xml', '/manifest', expected_xml)).toBeDefined();
             });
         });
 
@@ -213,16 +217,16 @@ describe('config-changes module', function() {
                 var munger = new configChanges.PlatformMunger('ios', temp, 'unused');
                 var munge = munger.generate_plugin_config_munge(varplugin, {});
                 var expected_xml = '<cfbundleid>com.example.friendstring</cfbundleid>';
-                expect(munge['config.xml']['/widget'][expected_xml]).toBeDefined();
+                expect(configChanges.get_munge_change(munge, 'config.xml', '/widget', expected_xml)).toBeDefined();
             });
             it('should special case framework elements for ios', function() {
                 var munger = new configChanges.PlatformMunger('ios', temp, 'unused');
                 var munge = munger.generate_plugin_config_munge(cbplugin, {});
-                expect(munge['framework']).toBeDefined();
-                expect(munge['framework']['libsqlite3.dylib']['false']).toBeDefined();
-                expect(munge['framework']['social.framework']['true']).toBeDefined();
-                expect(munge['framework']['music.framework']['false']).toBeDefined();
-                expect(munge['framework']['Custom.framework']).not.toBeDefined();
+                expect(munge.files['framework']).toBeDefined();
+                expect(configChanges.get_munge_change(munge, 'framework', 'libsqlite3.dylib', 'false')).toBeDefined();
+                expect(configChanges.get_munge_change(munge, 'framework', 'social.framework', 'true')).toBeDefined();
+                expect(configChanges.get_munge_change(munge, 'framework', 'music.framework', 'false')).toBeDefined();
+                expect(munge.files['framework'].parents['Custom.framework']).not.toBeDefined();
             });
         });
     });

http://git-wip-us.apache.org/repos/asf/cordova-plugman/blob/ac1160d4/spec/util/xml-helpers.spec.js
----------------------------------------------------------------------
diff --git a/spec/util/xml-helpers.spec.js b/spec/util/xml-helpers.spec.js
index 5def06e..edcdedb 100644
--- a/spec/util/xml-helpers.spec.js
+++ b/spec/util/xml-helpers.spec.js
@@ -139,5 +139,32 @@ describe('xml-helpers', function(){
                 selector= "/bookstore/book[price>35]/title";
             expect(xml_helpers.graftXML(doc, children, selector)).toBe(false);
         });
+
+        it('appends children after the specified sibling', function () {
+            var doc = new et.ElementTree(et.XML('<widget><A/><B/><C/></widget>')),
+                children = [et.XML('<B id="new"/>'), et.XML('<B id="new2"/>')],
+                selector= "/widget",
+                after= "B;A";
+            expect(xml_helpers.graftXML(doc, children, selector, after)).toBe(true);
+            expect(et.tostring(doc.getroot())).toContain('<B /><B id="new" /><B id="new2" />');
+        });
+
+        it('appends children after the 2nd priority sibling if the 1st one is missing', function () {
+            var doc = new et.ElementTree(et.XML('<widget><A/><C/></widget>')),
+                children = [et.XML('<B id="new"/>'), et.XML('<B id="new2"/>')],
+                selector= "/widget",
+                after= "B;A";
+            expect(xml_helpers.graftXML(doc, children, selector, after)).toBe(true);
+            expect(et.tostring(doc.getroot())).toContain('<A /><B id="new" /><B id="new2" />');
+        });
+
+        it('inserts children at the beginning if specified sibling is missing', function () {
+            var doc = new et.ElementTree(et.XML('<widget><B/><C/></widget>')),
+                children = [et.XML('<A id="new"/>'), et.XML('<A id="new2"/>')],
+                selector= "/widget",
+                after= "A";
+            expect(xml_helpers.graftXML(doc, children, selector, after)).toBe(true);
+            expect(et.tostring(doc.getroot())).toContain('<widget><A id="new" /><A id="new2" />');
+        });
     });
 });

http://git-wip-us.apache.org/repos/asf/cordova-plugman/blob/ac1160d4/src/util/config-changes.js
----------------------------------------------------------------------
diff --git a/src/util/config-changes.js b/src/util/config-changes.js
index 989a582..d09fcf1 100644
--- a/src/util/config-changes.js
+++ b/src/util/config-changes.js
@@ -39,6 +39,7 @@ var fs   = require('fs'),
     bplist = require('bplist-parser'),
     xcode = require('xcode'),
     et   = require('elementtree'),
+    _ = require('underscore'), 
     xml_helpers = require('./../util/xml-helpers'),
     platforms = require('./../platforms'),
     events = require('./../events'),
@@ -77,6 +78,11 @@ exports.process = function(plugins_dir, project_dir, platform) {
     munger.process();
     munger.save_all();
 };
+
+exports.get_munge_change = function(munge, keys) {
+    return deep_find.apply(null, arguments);
+}
+
 /******************************************************************************/
 
 
@@ -126,19 +132,20 @@ PlatformMunger.prototype.apply_file_munge = PlatformMunger_apply_file_munge;
 function PlatformMunger_apply_file_munge(file, munge, remove) {
     var self = this;
     var xml_child;
-
+    
     if ( file === 'framework' && self.platform === 'ios' ) {
         // ios pbxproj file
         var pbxproj = self.config_keeper.get(self.project_dir, self.platform, 'framework');
-        for (var src in munge) {
-            for (xml_child in munge[src]) {
+        for (var src in munge.parents) {
+            for (xml_child in munge.parents[src]) {
+                var xml = munge.parents[src][xml_child].xml;
                 // Only add the framework if it's not a cordova-ios core framework
                 if (keep_these_frameworks.indexOf(src) == -1) {
                     // xml_child in this case is whether the framework should use weak or not
                     if (remove) {
                         pbxproj.data.removeFramework(src);
                     } else {
-                        pbxproj.data.addFramework(src, {weak: (xml_child === 'true')});
+                        pbxproj.data.addFramework(src, {weak: (xml === 'true')});
                     }
                     pbxproj.is_changed = true;
                 }
@@ -146,13 +153,13 @@ function PlatformMunger_apply_file_munge(file, munge, remove) {
         }
     } else {
         // all other types of files
-        for (var selector in munge) {
-            for (xml_child in munge[selector]) {
+        for (var selector in munge.parents) {
+            for (xml_child in munge.parents[selector]) {
                 // this xml child is new, graft it (only if config file exists)
                 var config_file = self.config_keeper.get(self.project_dir, self.platform, file);
                 if (config_file.exists) {
-                    if (remove) config_file.prune_child(selector, xml_child);
-                    else config_file.graft_child(selector, xml_child);
+                    if (remove) config_file.prune_child(selector, munge.parents[selector][xml_child]);
+                    else config_file.graft_child(selector, munge.parents[selector][xml_child]);
                 }
             }
         }
@@ -173,7 +180,7 @@ function remove_plugin_changes(plugin_name, plugin_id, is_top_level) {
     var global_munge = platform_config.config_munge;
     var munge = decrement_munge(global_munge, config_munge);
 
-    for (var file in munge) {
+    for (var file in munge.files) {
         if (file == 'plugins-plist' && self.platform == 'ios') {
             // TODO: remove this check and <plugins-plist> sections in spec/plugins/../plugin.xml files.
             events.emit(
@@ -183,7 +190,7 @@ function remove_plugin_changes(plugin_name, plugin_id, is_top_level) {
             );
             continue;
         }
-        self.apply_file_munge(file, munge[file], /* remove = */ true);
+        self.apply_file_munge(file, munge.files[file], /* remove = */ true);
     }
 
     // Remove from installed_plugins
@@ -223,7 +230,7 @@ function add_plugin_changes(plugin_id, plugin_vars, is_top_level, should_increme
         munge = config_munge;
     }
 
-    for (var file in munge) {
+    for (var file in munge.files) {
         // TODO: remove this warning some time after 3.4 is out.
         if (file == 'plugins-plist' && self.platform == 'ios') {
             events.emit(
@@ -233,7 +240,7 @@ function add_plugin_changes(plugin_id, plugin_vars, is_top_level, should_increme
             );
             continue;
         }
-        self.apply_file_munge(file, munge[file]);
+        self.apply_file_munge(file, munge.files[file]);
     }
 
     // Move to installed_plugins if it is a top-level plugin
@@ -257,7 +264,7 @@ function reapply_global_munge () {
 
     var platform_config = exports.get_platform_json(self.plugins_dir, self.platform);
     var global_munge = platform_config.config_munge;
-    for (var file in global_munge) {
+    for (var file in global_munge.files) {
         // TODO: remove this warning some time after 3.4 is out.
         if (file == 'plugins-plist' && self.platform == 'ios') {
             events.emit(
@@ -268,7 +275,7 @@ function reapply_global_munge () {
             continue;
         }
         // TODO: This is mostly file IO and can run in parallel since each file is independent.
-        self.apply_file_munge(file, global_munge[file]);
+        self.apply_file_munge(file, global_munge.files[file]);
     }
 }
 
@@ -285,7 +292,7 @@ function generate_plugin_config_munge(plugin_dir, vars) {
         vars['PACKAGE_NAME'] = self.platform_handler.package_name(self.project_dir);
     }
 
-    var munge = {};
+    var munge = { files: {} };
     var plugin_config = self.config_keeper.get(plugin_dir, '', 'plugin.xml');
     var plugin_xml = plugin_config.data;
 
@@ -303,18 +310,10 @@ function generate_plugin_config_munge(plugin_dir, vars) {
         frameworks.forEach(function(f) {
             var custom = f.attrib['custom'];
             if(!custom) {
-                if (!munge['framework']) {
-                    munge['framework'] = {};
-                }
                 var file = f.attrib['src'];
-                var weak = ('true' == f.attrib['weak']);
-                if (!munge['framework'][file]) {
-                    munge['framework'][file] = {};
-                }
-                if (!munge['framework'][file][weak]) {
-                    munge['framework'][file][weak] = 0;
-                }
-                munge['framework'][file][weak] += 1;
+                var weak = ('true' == f.attrib['weak']).toString();
+
+                deep_add(munge, 'framework', file, { xml: weak, count: 1 });
             }
         });
     }
@@ -322,14 +321,9 @@ function generate_plugin_config_munge(plugin_dir, vars) {
     changes.forEach(function(change) {
         var target = change.attrib['target'];
         var parent = change.attrib['parent'];
-        if (!munge[target]) {
-            munge[target] = {};
-        }
-        if (!munge[target][parent]) {
-            munge[target][parent] = {};
-        }
+        var after = change.attrib['after'];
         var xmls = change.getchildren();
-        xmls.forEach(function(xml) {
+		xmls.forEach(function(xml) {
             // 1. stringify each xml
             var stringified = (new et.ElementTree(xml)).write({xml_declaration:false});
             // interp vars
@@ -340,16 +334,12 @@ function generate_plugin_config_munge(plugin_dir, vars) {
                 });
             }
             // 2. add into munge
-            if (!munge[target][parent][stringified]) {
-                munge[target][parent][stringified] = 0;
-            }
-            munge[target][parent][stringified] += 1;
+            deep_add(munge, target, parent, { xml: stringified, count: 1, after: after });
         });
     });
     return munge;
 }
 
-
 // Go over the prepare queue an apply the config munges for each plugin
 // that has been (un)installed.
 PlatformMunger.prototype.process = PlatformMunger_process;
@@ -426,7 +416,7 @@ function get_platform_json(plugins_dir, platform) {
 
     var filepath = path.join(plugins_dir, platform + '.json');
     if (fs.existsSync(filepath)) {
-        return JSON.parse(fs.readFileSync(filepath, 'utf-8'));
+        return fix_munge(JSON.parse(fs.readFileSync(filepath, 'utf-8')));
     } else {
         var config = {
             prepare_queue:{installed:[], uninstalled:[]},
@@ -445,6 +435,28 @@ function save_platform_json(config, plugins_dir, platform) {
     fs.writeFileSync(filepath, JSON.stringify(config, null, 4), 'utf-8');
 }
 
+
+// convert a munge from the old format ([file][parent][xml] = count) to the current one
+function fix_munge(platform_config) {
+    var munge = platform_config.config_munge;
+    if (!munge.files) {
+        var new_munge = { files: {} };
+        for (var file in munge) {
+            for (var selector in munge[file]) {
+                for (var xml_child in munge[file][selector]) {
+                    var val = parseInt(munge[file][selector][xml_child]);
+                    for (var i = 0; i < val; i++) {
+                        deep_add(new_munge, [file, selector, { xml: xml_child, count: val }]);
+                    }
+                }
+            }
+        }
+        platform_config.config_munge = new_munge;
+    }
+
+    return platform_config;
+}
+
 /**** END of ConfigKeeper ****/
 
 
@@ -528,14 +540,14 @@ function ConfigFile_graft_child(selector, xml_child) {
     var filepath = self.filepath;
     var result;
     if (self.type === 'xml') {
-        var xml_to_graft = [et.XML(xml_child)];
-        result = xml_helpers.graftXML(self.data, xml_to_graft, selector);
+        var xml_to_graft = [et.XML(xml_child.xml)];
+        result = xml_helpers.graftXML(self.data, xml_to_graft, selector, xml_child.after);
         if ( !result) {
             throw new Error('grafting xml at selector "' + selector + '" from "' + filepath + '" during config install went bad :(');
         }
     } else {
         // plist file
-        result = plist_helpers.graftPLIST(self.data, xml_child, selector);
+        result = plist_helpers.graftPLIST(self.data, xml_child.xml, selector);
         if ( !result ) {
             throw new Error('grafting to plist "' + filepath + '" during config install went bad :(');
         }
@@ -550,11 +562,11 @@ function ConfigFile_prune_child(selector, xml_child) {
     var filepath = self.filepath;
     var result;
     if (self.type === 'xml') {
-        var xml_to_graft = [et.XML(xml_child)];
+        var xml_to_graft = [et.XML(xml_child.xml)];
         result = xml_helpers.pruneXML(self.data, xml_to_graft, selector);
     } else {
         // plist file
-        result = plist_helpers.prunePLIST(self.data, xml_child, selector);
+        result = plist_helpers.prunePLIST(self.data, xml_child.xml, selector);
     }
     if ( !result) {
         var err_msg = 'Pruning at selector "' + selector + '" from "' + filepath + '" went bad.';
@@ -650,21 +662,94 @@ function resolveConfigFilePath(project_dir, platform, file) {
 * Munge object manipulations functions
 ******************************************************************************/
 
-// Increment obj[key1][key2]...[keyN] by val. If it
-// didn't exist, set it to val.
-function deep_add(obj, val, keys /* or key1, key2 .... */ ) {
+// add the count of [key1][key2]...[keyN] to obj
+// return true if it didn't exist before
+function deep_add(obj, keys /* or key1, key2 .... */ ) {
     if ( !Array.isArray(keys) ) {
-        keys = Array.prototype.slice.call(arguments, 2);
+        keys = Array.prototype.slice.call(arguments, 1);
     }
-    var k = keys[0];
 
+    return process_munge(obj, true/*createParents*/, function (parentArray, k) {
+        var found = _.find(parentArray, function(element) {
+            return element.xml == k.xml;
+        });
+        if (found) {
+            found.after = found.after || k.after;
+            found.count += k.count;
+        } else {
+            parentArray.push(k);
+        }
+        return !found;
+    }, keys);
+}
+
+// decrement the count of [key1][key2]...[keyN] from obj and remove if it reaches 0
+// return true if it was removed or not found
+function deep_remove(obj, keys /* or key1, key2 .... */ ) {
+    if ( !Array.isArray(keys) ) {
+        keys = Array.prototype.slice.call(arguments, 1);
+    }
+
+    var result = process_munge(obj, false/*createParents*/, function (parentArray, k) {
+        var index = -1;
+        var found = _.find(parentArray, function (element) {
+            index++;
+            return element.xml == k.xml;
+        });
+        if (found) {
+            found.count -= k.count;
+            if (found.count > 0) {
+                return false;
+            }
+            else {
+                parentArray.splice(index, 1);
+            }
+        }
+        return undefined;
+    }, keys);
+
+    return typeof result === "undefined" ? true : result;
+}
+
+// search for [key1][key2]...[keyN]
+// return the object or undefined if not found
+function deep_find(obj, keys /* or key1, key2 .... */ ) {
+    if ( !Array.isArray(keys) ) {
+        keys = Array.prototype.slice.call(arguments, 1);
+    }
+
+    return process_munge(obj, false/*createParents?*/, function (parentArray, k) {
+        return _.find(parentArray, function (element) {
+            return element.xml == (k.xml || k);
+        });
+    }, keys);
+}
+
+// Execute func passing it the parent array and the xmlChild key.
+// When createParents is true, add the file and parent items  they are missing
+// When createParents is false, stop and return undefined if the file and/or parent items are missing
+
+function process_munge(obj, createParents, func, keys /* or key1, key2 .... */ ) {
+    if ( !Array.isArray(keys) ) {
+        keys = Array.prototype.slice.call(arguments, 1);
+    }
+    var k = keys[0];
     if (keys.length == 1) {
-        obj[k] = obj[k] || 0;
-        obj[k] += val;
-        return obj[k];
+        return func(obj, k);
+    } else if (keys.length == 2) {
+        if (!obj.parents[k] && !createParents) {
+            return undefined;
+        }
+        obj.parents[k] = obj.parents[k] || [];
+        return process_munge(obj.parents[k], createParents, func, keys.slice(1));
+    } else if (keys.length == 3){
+        if (!obj.files[k] && !createParents) {
+            return undefined;
+        }
+        obj.files[k] = obj.files[k] || { parents: {} };
+        return process_munge(obj.files[k], createParents, func, keys.slice(1));
     } else {
-        obj[k] = obj[k] || {};
-        return deep_add(obj[k], val, keys.slice(1));
+        throw new Error("Invalid key format. Must contain at most 3 elements (file, parent, xmlChild).");
     }
 }
 
@@ -673,17 +758,17 @@ function deep_add(obj, val, keys /* or key1, key2 .... */ ) {
 // Returns a munge object containing values that exist in munge
 // but not in base.
 function increment_munge(base, munge) {
-    var diff = {};
+    var diff = { files: {} };
 
-    for (var file in munge) {
-        for (var selector in munge[file]) {
-            for (var xml_child in munge[file][selector]) {
-                var val = munge[file][selector][xml_child];
+    for (var file in munge.files) {
+        for (var selector in munge.files[file].parents) {
+            for (var xml_child in munge.files[file].parents[selector]) {
+                var val = munge.files[file].parents[selector][xml_child];
                 // if node not in base, add it to diff and base
                 // else increment it's value in base without adding to diff
-                var new_val = deep_add(base, val, [file, selector, xml_child]);
-                if ( val == new_val ) {
-                    deep_add(diff, val, file, selector, xml_child);
+                var newlyAdded = deep_add(base, [file, selector, val]);
+                if (newlyAdded) {
+                    deep_add(diff, file, selector, val);
                 }
             }
         }
@@ -693,21 +778,20 @@ function increment_munge(base, munge) {
 
 // Update the base munge object as
 // base[file][selector][child] -= base[file][selector][child]
-// nodes that reached zero value are removed from base and the returned munge
+// nodes that reached zero value are removed from base and added to the returned munge
 // object.
 function decrement_munge(base, munge) {
-    var zeroed = {};
+    var zeroed = { files: {} };
 
-    for (var file in munge) {
-        for (var selector in munge[file]) {
-            for (var xml_child in munge[file][selector]) {
-                var val = munge[file][selector][xml_child];
+    for (var file in munge.files) {
+        for (var selector in munge.files[file].parents) {
+            for (var xml_child in munge.files[file].parents[selector]) {
+                var val = munge.files[file].parents[selector][xml_child];
                 // if node not in base, add it to diff and base
                 // else increment it's value in base without adding to diff
-                var new_val = deep_add(base, -val, [file, selector, xml_child]);
-                if ( new_val <= 0) {
-                    deep_add(zeroed, val, file, selector, xml_child);
-                    delete base[file][selector][xml_child];
+                var removed = deep_remove(base, [file, selector, val]);
+                if (removed) {
+                    deep_add(zeroed, file, selector, val);
                 }
             }
         }

http://git-wip-us.apache.org/repos/asf/cordova-plugman/blob/ac1160d4/src/util/xml-helpers.js
----------------------------------------------------------------------
diff --git a/src/util/xml-helpers.js b/src/util/xml-helpers.js
index cfb914c..265025e 100644
--- a/src/util/xml-helpers.js
+++ b/src/util/xml-helpers.js
@@ -23,6 +23,7 @@
 
 var fs = require('fs')
   , path = require('path')
+  , _ = require('underscore')
   , et = require('elementtree');
 
 module.exports = {
@@ -72,7 +73,7 @@ module.exports = {
     },
 
     // adds node to doc at selector, creating parent if it doesn't exist
-    graftXML: function(doc, nodes, selector) {
+    graftXML: function(doc, nodes, selector, after) {
         var parent = resolveParent(doc, selector);
         if (!parent) {
             //Try to create the parent recursively if necessary
@@ -91,7 +92,11 @@ module.exports = {
         nodes.forEach(function (node) {
             // check if child is unique first
             if (uniqueChild(node, parent)) {
-                parent.append(node);
+                var children = parent.getchildren();
+                var insertIdx = after ? findInsertIdx(children, after) : children.length;
+
+                //TODO: replace with parent.insert after the bug in ElementTree is fixed
+                parent.getchildren().splice(insertIdx, 0, node);
             }
         });
 
@@ -174,3 +179,18 @@ function resolveParent(doc, selector) {
     }
     return parent;
 }
+
+// Find the index at which to insert an entry. After is a ;-separated priority list 
+// of tags after which the insertion should be made. E.g. If we need to 
+// insert an element C, and the rule is that the order of children has to be 
+// As, Bs, Cs. After will be equal to "C;B;A".
+
+function findInsertIdx(children, after) {
+    var childrenTags = children.map(function(child) { return child.tag; });
+    var afters = after.split(";");
+    var afterIndexes = afters.map(function(current) { return childrenTags.lastIndexOf(current); });
+    var foundIndex = _.find(afterIndexes, function(index) { return index != -1; });
+
+    //add to the beginning if no matching nodes are found
+    return typeof foundIndex === 'undefined' ? 0 : foundIndex+1;
+}