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/05/16 17:57:08 UTC

[09/20] git commit: updated/fixed install + uninstall specs. added specs to both for plugins with dependencies

updated/fixed install + uninstall specs. added specs to both for plugins with dependencies


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

Branch: refs/heads/master
Commit: bf56f3eddda3f88cb09bfd487a3c58d112c335e3
Parents: e05e51a
Author: Fil Maj <ma...@gmail.com>
Authored: Wed May 15 12:59:07 2013 -0700
Committer: Fil Maj <ma...@gmail.com>
Committed: Thu May 16 08:55:42 2013 -0700

----------------------------------------------------------------------
 main.js                                |    4 +-
 spec/install.spec.js                   |  127 ++++++++++++++-------------
 spec/plugins/dependencies/A/plugin.xml |   60 +++++++++++++
 spec/plugins/dependencies/B/plugin.xml |   60 +++++++++++++
 spec/plugins/dependencies/C/plugin.xml |   57 ++++++++++++
 spec/plugins/dependencies/D/plugin.xml |   57 ++++++++++++
 spec/plugins/dependencies/E/plugin.xml |   57 ++++++++++++
 spec/plugins/dependencies/README.md    |    5 +
 spec/uninstall.spec.js                 |   92 +++++++++-----------
 src/install.js                         |   46 ++++++----
 src/uninstall.js                       |   31 ++++---
 src/util/action-stack.js               |   24 +++--
 12 files changed, 462 insertions(+), 158 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/cordova-plugman/blob/bf56f3ed/main.js
----------------------------------------------------------------------
diff --git a/main.js b/main.js
index 414cf18..8339061 100755
--- a/main.js
+++ b/main.js
@@ -63,7 +63,7 @@ else if (!cli_opts.platform || !cli_opts.project || !cli_opts.plugin) {
     printUsage();
 }
 else if (cli_opts.uninstall) {
-    plugman.uninstall(cli_opts.platform, cli_opts.project, cli_opts.plugin, plugins_dir, {}, cli_opts.www, true /* is top level? */);
+    plugman.uninstall(cli_opts.platform, cli_opts.project, cli_opts.plugin, plugins_dir, {}, cli_opts.www);
 }
 else {
     var cli_variables = {}
@@ -74,7 +74,7 @@ else {
             if (/^[\w-_]+$/.test(key)) cli_variables[key] = tokens.join('=');
         });
     }
-    plugman.install(cli_opts.platform, cli_opts.project, cli_opts.plugin, plugins_dir, '.', cli_variables, cli_opts.www, true /* is top level? */);
+    plugman.install(cli_opts.platform, cli_opts.project, cli_opts.plugin, plugins_dir, '.', cli_variables, cli_opts.www);
 }
 
 function printUsage() {

http://git-wip-us.apache.org/repos/asf/cordova-plugman/blob/bf56f3ed/spec/common.spec.js
----------------------------------------------------------------------
diff --git a/spec/common.spec.js b/spec/common.spec.js
deleted file mode 100644
index e69de29..0000000

http://git-wip-us.apache.org/repos/asf/cordova-plugman/blob/bf56f3ed/spec/install.spec.js
----------------------------------------------------------------------
diff --git a/spec/install.spec.js b/spec/install.spec.js
index c471747..34bc9e2 100644
--- a/spec/install.spec.js
+++ b/spec/install.spec.js
@@ -1,6 +1,6 @@
 var install = require('../src/install'),
-    android = require('../src/platforms/android'),
     common = require('../src/platforms/common'),
+    actions = require('../src/util/action-stack'),
     //ios     = require('../src/platforms/ios'),
     //blackberry = require('../src/platforms/blackberry'),
     config_changes = require('../src/util/config-changes'),
@@ -11,7 +11,13 @@ var install = require('../src/install'),
     shell   = require('shelljs'),
     temp    = path.join(os.tmpdir(), 'plugman'),
     childbrowser = path.join(__dirname, 'plugins', 'ChildBrowser'),
+    dep_a = path.join(__dirname, 'plugins', 'dependencies', 'A'),
+    dep_b = path.join(__dirname, 'plugins', 'dependencies', 'B'),
+    dep_c = path.join(__dirname, 'plugins', 'dependencies', 'C'),
+    dep_d = path.join(__dirname, 'plugins', 'dependencies', 'D'),
+    dep_e = path.join(__dirname, 'plugins', 'dependencies', 'E'),
     dummyplugin = path.join(__dirname, 'plugins', 'DummyPlugin'),
+    dummy_id = 'com.phonegap.plugins.dummyplugin',
     variableplugin = path.join(__dirname, 'plugins', 'VariablePlugin'),
     faultyplugin = path.join(__dirname, 'plugins', 'FaultyPlugin'),
     android_one_project = path.join(__dirname, 'projects', 'android_one', '*');
@@ -24,26 +30,19 @@ describe('install', function() {
 
     beforeEach(function() {
         shell.mkdir('-p', temp);
-        shell.mkdir('-p', plugins_dir);
         shell.cp('-rf', android_one_project, temp);
     });
     afterEach(function() {
         shell.rm('-rf', temp);
     });
 
-
     describe('success', function() {
-        var android_installer;
-        beforeEach(function() {
-            shell.cp('-rf', dummyplugin, plugins_dir);
-            android_installer = spyOn(android, 'install');
-        });
         it('should properly install assets', function() {
             var s = spyOn(common, 'copyFile').andCallThrough();
-            install('android', temp, 'DummyPlugin', plugins_dir, {});
+            install('android', temp, dummyplugin, plugins_dir, '.', {});
             // making sure the right methods were called
             expect(s).toHaveBeenCalled();
-            expect(s.calls.length).toEqual(2);
+            expect(s.calls.length).toEqual(3);
 
             expect(fs.existsSync(path.join(temp, 'assets', 'www', 'dummyplugin.js'))).toBe(true);
             expect(fs.existsSync(path.join(temp, 'assets', 'www', 'dummyplugin'))).toBe(true);
@@ -57,14 +56,16 @@ describe('install', function() {
             var sRemoveFile = spyOn(common, 'removeFile').andCallThrough();
             var sRemoveFileF = spyOn(common, 'removeFileF').andCallThrough();
             
-            // messing the plugin
-            shell.rm('-rf', path.join(plugins_dir, 'dummyplugin', 'www', 'dummyplugin')); 
+            // messing with the plugin
+            shell.mkdir('-p', plugins_dir);
+            shell.cp('-rf', dummyplugin, plugins_dir);
+            shell.rm('-rf', path.join(plugins_dir, 'DummyPlugin', 'www', 'dummyplugin')); 
             expect(function() {
-                install('android', temp, 'DummyPlugin', plugins_dir, {});
+                install('android', temp, 'DummyPlugin', plugins_dir, '.', {});
             }).toThrow();
             // making sure the right methods were called
             expect(sCopyFile).toHaveBeenCalled();
-            expect(sCopyFile.calls.length).toEqual(2);
+            expect(sCopyFile.calls.length).toEqual(3);
 
             expect(sRemoveFile).toHaveBeenCalled();
             expect(sRemoveFile.calls.length).toEqual(1);
@@ -77,10 +78,10 @@ describe('install', function() {
 
         it('should properly install assets into a custom www dir', function() {
             var s = spyOn(common, 'copyFile').andCallThrough();
-            install('android', temp, 'DummyPlugin', plugins_dir, {}, path.join(temp, 'staging'));
+            install('android', temp, dummyplugin, plugins_dir, '.', {}, path.join(temp, 'staging'));
             // making sure the right methods were called
             expect(s).toHaveBeenCalled();
-            expect(s.calls.length).toEqual(2);
+            expect(s.calls.length).toEqual(3);
 
             expect(fs.existsSync(path.join(temp, 'staging', 'dummyplugin.js'))).toBe(true);
             expect(fs.existsSync(path.join(temp, 'staging', 'dummyplugin'))).toBe(true);
@@ -96,13 +97,15 @@ describe('install', function() {
             var sRemoveFileF = spyOn(common, 'removeFileF').andCallThrough();
             
             // messing the plugin
+            shell.mkdir('-p', plugins_dir);
+            shell.cp('-rf', dummyplugin, plugins_dir);
             shell.rm('-rf', path.join(plugins_dir, 'dummyplugin', 'www', 'dummyplugin')); 
             expect(function() {
-                install('android', temp, 'DummyPlugin', plugins_dir, {}, path.join(temp, 'staging'));
+                install('android', temp, 'DummyPlugin', plugins_dir, '.', {}, path.join(temp, 'staging'));
             }).toThrow();
             // making sure the right methods were called
             expect(sCopyFile).toHaveBeenCalled();
-            expect(sCopyFile.calls.length).toEqual(2);
+            expect(sCopyFile.calls.length).toEqual(3);
 
             expect(sRemoveFile).toHaveBeenCalled();
             expect(sRemoveFile.calls.length).toEqual(1);
@@ -115,38 +118,56 @@ describe('install', function() {
 
         it('should call prepare after a successful install', function() {
             var s = spyOn(plugman, 'prepare');
-            install('android', temp, 'DummyPlugin', plugins_dir, {});
-            android_installer.mostRecentCall.args[5](); // fake the installer calling back successfully
+            install('android', temp, dummyplugin, plugins_dir, '.', {});
             expect(s).toHaveBeenCalled();
         });
 
         it('should call fetch if provided plugin cannot be resolved locally', function() {
             var s = spyOn(plugman, 'fetch');
-            install('android', temp, 'CLEANYOURSHORTS', plugins_dir, {});
+            install('android', temp, 'CLEANYOURSHORTS', plugins_dir, '.', {});
             expect(s).toHaveBeenCalled();
         });
-        it('should generate an array of transactions required to run an installation and pass into appropriate platform handler\'s install method', function() {
-            install('android', temp, 'DummyPlugin', plugins_dir, {});
-            var transactions = android_installer.mostRecentCall.args[0];
-
-            expect(transactions.length).toEqual(1);
-            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', {});
+            install('android', temp, dummyplugin, plugins_dir, '.', {});
+            expect(spy).toHaveBeenCalledWith(plugins_dir, dummy_id, 'android', {}, true);
+        });
+        it('should notify if plugin is already installed into project', function() {
+            expect(function() {
+                install('android', temp, dummyplugin, plugins_dir,'.',  {});
+            }).not.toThrow();
+            var spy = spyOn(console, 'log');
+            install('android', temp, dummyplugin, plugins_dir, '.', {});
+            expect(spy).toHaveBeenCalledWith('Plugin "com.phonegap.plugins.dummyplugin" already installed, \'sall good.');
+        });
+
+        describe('with dependencies', function() {
+            it('should process all dependent plugins', function() {
+                var spy = spyOn(actions.prototype, 'process').andCallThrough();
+                shell.mkdir('-p', plugins_dir);
+                shell.cp('-rf', dep_a, plugins_dir);
+                shell.cp('-rf', dep_d, plugins_dir);
+                shell.cp('-rf', dep_c, plugins_dir);
+                install('android', temp, 'A', plugins_dir, '.', {});
+                expect(spy.calls.length).toEqual(3);
+            });
+            it('should fetch any dependent plugins if missing', function() {
+                var spy = spyOn(plugman, 'fetch');
+                shell.mkdir('-p', plugins_dir);
+                shell.cp('-rf', dep_a, plugins_dir);
+                shell.cp('-rf', dep_c, plugins_dir);
+                install('android', temp, 'A', plugins_dir, '.', {});
+                expect(spy).toHaveBeenCalled();
+            });
         });
     });
 
     describe('failure', function() {
         it('should throw if asset target already exists', function() {
-            shell.cp('-rf', dummyplugin, plugins_dir);
             var target = path.join(temp, 'assets', 'www', 'dummyplugin.js');
             fs.writeFileSync(target, 'some bs', 'utf-8');
             expect(function() {
-                install('android', temp, 'DummyPlugin', plugins_dir, {});
+                install('android', temp, dummyplugin, plugins_dir, '.', {});
             }).toThrow();
         });
         it('should throw if platform is unrecognized', function() {
@@ -155,44 +176,28 @@ describe('install', function() {
             }).toThrow('atari not supported.');
         });
         it('should throw if variables are missing', function() {
-            shell.cp('-rf', variableplugin, plugins_dir);
             expect(function() {
-                install('android', temp, 'VariablePlugin', plugins_dir, {});
+                install('android', temp, variableplugin, plugins_dir, '.', {});
             }).toThrow('Variable(s) missing: API_KEY');
         });
-        it('should handle a failed install by passing completed transactions into appropriate handler\'s uninstall method', function() {
-            shell.cp('-rf', faultyplugin, plugins_dir);
-            var s = spyOn(android, 'uninstall');
-            install('android', temp, 'FaultyPlugin', plugins_dir, {});
-
-            var executed_txs = s.mostRecentCall.args[0];
-            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
+        it('should throw if a file required for installation cannot be found', function() {
+            shell.mkdir('-p', plugins_dir);
             shell.cp('-rf', dummyplugin, plugins_dir);
+            shell.rm(path.join(plugins_dir, 'DummyPlugin', 'src', 'android', 'DummyPlugin.java')); 
+            
             expect(function() {
-                install('android', temp, 'DummyPlugin', plugins_dir, {});
-            }).not.toThrow();
-            expect(function() {
-                install('android', temp, 'DummyPlugin', plugins_dir, {});
+                install('android', temp, 'DummyPlugin', plugins_dir, '.', {});
             }).toThrow();
         });
-        it('should revert web assets if an install error occurs', function() {
-            var sRemoveFile = spyOn(common, 'removeFile').andCallThrough();
-            var sRemoveFileF = spyOn(common, 'removeFileF').andCallThrough();
+        it('should pass error into specified callback if a file required for installation cannot be found', function(done) {
+            shell.mkdir('-p', plugins_dir);
             shell.cp('-rf', dummyplugin, plugins_dir);
             shell.rm(path.join(plugins_dir, 'DummyPlugin', 'src', 'android', 'DummyPlugin.java')); 
             
-            install('android', temp, 'DummyPlugin', plugins_dir, {}, undefined, function() {});
-            
-            expect(sRemoveFile).toHaveBeenCalled();
-            expect(sRemoveFile.calls.length).toEqual(2);
-            expect(sRemoveFileF).toHaveBeenCalled();
-            expect(sRemoveFileF.calls.length).toEqual(1);
-           
-            expect(fs.existsSync(path.join(temp, 'assets', 'www', 'dummyplugin.js'))).toBe(false);
-            expect(fs.existsSync(path.join(temp, 'assets', 'www', 'dummyplugin'))).toBe(false);
+            install('android', temp, 'DummyPlugin', plugins_dir, '.', {}, null, function(err) {
+                expect(err).toBeDefined();
+                done();
+            });
         });
     });
 });

http://git-wip-us.apache.org/repos/asf/cordova-plugman/blob/bf56f3ed/spec/plugins/dependencies/A/plugin.xml
----------------------------------------------------------------------
diff --git a/spec/plugins/dependencies/A/plugin.xml b/spec/plugins/dependencies/A/plugin.xml
new file mode 100644
index 0000000..604f191
--- /dev/null
+++ b/spec/plugins/dependencies/A/plugin.xml
@@ -0,0 +1,60 @@
+<?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="A"
+    version="0.6.0">
+
+    <name>Plugin A</name>
+
+    <dependency id="C" />
+    <dependency id="D" url="D" />
+
+    <asset src="www/plugin-a.js" target="plugin-a.js" />
+
+    <config-file target="config.xml" parent="/*">
+        <access origin="build.phonegap.com" />
+    </config-file>
+	
+    <!-- android -->
+    <platform name="android">
+        <config-file target="res/xml/config.xml" parent="plugins">
+            <plugin name="A"
+                value="com.phonegap.A.A"/>
+        </config-file>
+
+        <source-file src="src/android/A.java"
+                target-dir="src/com/phonegap/A" />
+    </platform>
+
+        
+    <!-- ios -->
+    <platform name="ios">
+        <!-- CDV 2.5+ -->
+        <config-file target="config.xml" parent="plugins">
+            <plugin name="A"
+                value="APluginCommand"/>
+        </config-file>
+
+        <header-file src="src/ios/APluginCommand.h" />
+        <source-file src="src/ios/APluginCommand.m"/>
+    </platform>
+</plugin>

http://git-wip-us.apache.org/repos/asf/cordova-plugman/blob/bf56f3ed/spec/plugins/dependencies/A/src/android/A.java
----------------------------------------------------------------------
diff --git a/spec/plugins/dependencies/A/src/android/A.java b/spec/plugins/dependencies/A/src/android/A.java
new file mode 100644
index 0000000..e69de29

http://git-wip-us.apache.org/repos/asf/cordova-plugman/blob/bf56f3ed/spec/plugins/dependencies/A/src/ios/APluginCommand.h
----------------------------------------------------------------------
diff --git a/spec/plugins/dependencies/A/src/ios/APluginCommand.h b/spec/plugins/dependencies/A/src/ios/APluginCommand.h
new file mode 100644
index 0000000..e69de29

http://git-wip-us.apache.org/repos/asf/cordova-plugman/blob/bf56f3ed/spec/plugins/dependencies/A/src/ios/APluginCommand.m
----------------------------------------------------------------------
diff --git a/spec/plugins/dependencies/A/src/ios/APluginCommand.m b/spec/plugins/dependencies/A/src/ios/APluginCommand.m
new file mode 100644
index 0000000..e69de29

http://git-wip-us.apache.org/repos/asf/cordova-plugman/blob/bf56f3ed/spec/plugins/dependencies/A/www/plugin-a.js
----------------------------------------------------------------------
diff --git a/spec/plugins/dependencies/A/www/plugin-a.js b/spec/plugins/dependencies/A/www/plugin-a.js
new file mode 100644
index 0000000..e69de29

http://git-wip-us.apache.org/repos/asf/cordova-plugman/blob/bf56f3ed/spec/plugins/dependencies/B/plugin.xml
----------------------------------------------------------------------
diff --git a/spec/plugins/dependencies/B/plugin.xml b/spec/plugins/dependencies/B/plugin.xml
new file mode 100644
index 0000000..6f92495
--- /dev/null
+++ b/spec/plugins/dependencies/B/plugin.xml
@@ -0,0 +1,60 @@
+<?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="B"
+    version="0.6.0">
+
+    <name>Plugin B</name>
+
+    <dependency id="D" />
+    <dependency id="E" />
+
+    <asset src="www/plugin-b.js" target="plugin-b.js" />
+
+    <config-file target="config.xml" parent="/*">
+        <access origin="build.phonegap.com" />
+    </config-file>
+	
+    <!-- android -->
+    <platform name="android">
+        <config-file target="res/xml/config.xml" parent="plugins">
+            <plugin name="B"
+                value="com.phonegap.B.B"/>
+        </config-file>
+
+        <source-file src="src/android/B.java"
+                target-dir="src/com/phonegap/B" />
+    </platform>
+
+        
+    <!-- ios -->
+    <platform name="ios">
+        <!-- CDV 2.5+ -->
+        <config-file target="config.xml" parent="plugins">
+            <plugin name="B"
+                value="BPluginCommand"/>
+        </config-file>
+
+        <header-file src="src/ios/BPluginCommand.h" />
+        <source-file src="src/ios/BPluginCommand.m"/>
+    </platform>
+</plugin>

http://git-wip-us.apache.org/repos/asf/cordova-plugman/blob/bf56f3ed/spec/plugins/dependencies/B/src/android/B.java
----------------------------------------------------------------------
diff --git a/spec/plugins/dependencies/B/src/android/B.java b/spec/plugins/dependencies/B/src/android/B.java
new file mode 100644
index 0000000..e69de29

http://git-wip-us.apache.org/repos/asf/cordova-plugman/blob/bf56f3ed/spec/plugins/dependencies/B/src/ios/BPluginCommand.h
----------------------------------------------------------------------
diff --git a/spec/plugins/dependencies/B/src/ios/BPluginCommand.h b/spec/plugins/dependencies/B/src/ios/BPluginCommand.h
new file mode 100644
index 0000000..e69de29

http://git-wip-us.apache.org/repos/asf/cordova-plugman/blob/bf56f3ed/spec/plugins/dependencies/B/src/ios/BPluginCommand.m
----------------------------------------------------------------------
diff --git a/spec/plugins/dependencies/B/src/ios/BPluginCommand.m b/spec/plugins/dependencies/B/src/ios/BPluginCommand.m
new file mode 100644
index 0000000..e69de29

http://git-wip-us.apache.org/repos/asf/cordova-plugman/blob/bf56f3ed/spec/plugins/dependencies/B/www/plugin-b.js
----------------------------------------------------------------------
diff --git a/spec/plugins/dependencies/B/www/plugin-b.js b/spec/plugins/dependencies/B/www/plugin-b.js
new file mode 100644
index 0000000..e69de29

http://git-wip-us.apache.org/repos/asf/cordova-plugman/blob/bf56f3ed/spec/plugins/dependencies/C/plugin.xml
----------------------------------------------------------------------
diff --git a/spec/plugins/dependencies/C/plugin.xml b/spec/plugins/dependencies/C/plugin.xml
new file mode 100644
index 0000000..5baf559
--- /dev/null
+++ b/spec/plugins/dependencies/C/plugin.xml
@@ -0,0 +1,57 @@
+<?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="C"
+    version="0.6.0">
+
+    <name>Plugin C</name>
+
+    <asset src="www/plugin-c.js" target="plugin-c.js" />
+
+    <config-file target="config.xml" parent="/*">
+        <access origin="build.phonegap.com" />
+    </config-file>
+	
+    <!-- android -->
+    <platform name="android">
+        <config-file target="res/xml/config.xml" parent="plugins">
+            <plugin name="C"
+                value="com.phonegap.C.C"/>
+        </config-file>
+
+        <source-file src="src/android/C.java"
+                target-dir="src/com/phonegap/C" />
+    </platform>
+
+        
+    <!-- ios -->
+    <platform name="ios">
+        <!-- CDV 2.5+ -->
+        <config-file target="config.xml" parent="plugins">
+            <plugin name="C"
+                value="CPluginCommand"/>
+        </config-file>
+
+        <header-file src="src/ios/CPluginCommand.h" />
+        <source-file src="src/ios/CPluginCommand.m"/>
+    </platform>
+</plugin>

http://git-wip-us.apache.org/repos/asf/cordova-plugman/blob/bf56f3ed/spec/plugins/dependencies/C/src/android/C.java
----------------------------------------------------------------------
diff --git a/spec/plugins/dependencies/C/src/android/C.java b/spec/plugins/dependencies/C/src/android/C.java
new file mode 100644
index 0000000..e69de29

http://git-wip-us.apache.org/repos/asf/cordova-plugman/blob/bf56f3ed/spec/plugins/dependencies/C/src/ios/CPluginCommand.h
----------------------------------------------------------------------
diff --git a/spec/plugins/dependencies/C/src/ios/CPluginCommand.h b/spec/plugins/dependencies/C/src/ios/CPluginCommand.h
new file mode 100644
index 0000000..e69de29

http://git-wip-us.apache.org/repos/asf/cordova-plugman/blob/bf56f3ed/spec/plugins/dependencies/C/src/ios/CPluginCommand.m
----------------------------------------------------------------------
diff --git a/spec/plugins/dependencies/C/src/ios/CPluginCommand.m b/spec/plugins/dependencies/C/src/ios/CPluginCommand.m
new file mode 100644
index 0000000..e69de29

http://git-wip-us.apache.org/repos/asf/cordova-plugman/blob/bf56f3ed/spec/plugins/dependencies/C/www/plugin-c.js
----------------------------------------------------------------------
diff --git a/spec/plugins/dependencies/C/www/plugin-c.js b/spec/plugins/dependencies/C/www/plugin-c.js
new file mode 100644
index 0000000..e69de29

http://git-wip-us.apache.org/repos/asf/cordova-plugman/blob/bf56f3ed/spec/plugins/dependencies/D/plugin.xml
----------------------------------------------------------------------
diff --git a/spec/plugins/dependencies/D/plugin.xml b/spec/plugins/dependencies/D/plugin.xml
new file mode 100644
index 0000000..94449ef
--- /dev/null
+++ b/spec/plugins/dependencies/D/plugin.xml
@@ -0,0 +1,57 @@
+<?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="D"
+    version="0.6.0">
+
+    <name>Plugin D</name>
+
+    <asset src="www/plugin-d.js" target="plugin-d.js" />
+
+    <config-file target="config.xml" parent="/*">
+        <access origin="build.phonegap.com" />
+    </config-file>
+	
+    <!-- android -->
+    <platform name="android">
+        <config-file target="res/xml/config.xml" parent="plugins">
+            <plugin name="D"
+                value="com.phonegap.D.D"/>
+        </config-file>
+
+        <source-file src="src/android/D.java"
+                target-dir="src/com/phonegap/D" />
+    </platform>
+
+        
+    <!-- ios -->
+    <platform name="ios">
+        <!-- CDV 2.5+ -->
+        <config-file target="config.xml" parent="plugins">
+            <plugin name="D"
+                value="DPluginCommand"/>
+        </config-file>
+
+        <header-file src="src/ios/DPluginCommand.h" />
+        <source-file src="src/ios/DPluginCommand.m"/>
+    </platform>
+</plugin>

http://git-wip-us.apache.org/repos/asf/cordova-plugman/blob/bf56f3ed/spec/plugins/dependencies/D/src/android/D.java
----------------------------------------------------------------------
diff --git a/spec/plugins/dependencies/D/src/android/D.java b/spec/plugins/dependencies/D/src/android/D.java
new file mode 100644
index 0000000..e69de29

http://git-wip-us.apache.org/repos/asf/cordova-plugman/blob/bf56f3ed/spec/plugins/dependencies/D/src/ios/DPluginCommand.h
----------------------------------------------------------------------
diff --git a/spec/plugins/dependencies/D/src/ios/DPluginCommand.h b/spec/plugins/dependencies/D/src/ios/DPluginCommand.h
new file mode 100644
index 0000000..e69de29

http://git-wip-us.apache.org/repos/asf/cordova-plugman/blob/bf56f3ed/spec/plugins/dependencies/D/src/ios/DPluginCommand.m
----------------------------------------------------------------------
diff --git a/spec/plugins/dependencies/D/src/ios/DPluginCommand.m b/spec/plugins/dependencies/D/src/ios/DPluginCommand.m
new file mode 100644
index 0000000..e69de29

http://git-wip-us.apache.org/repos/asf/cordova-plugman/blob/bf56f3ed/spec/plugins/dependencies/D/www/plugin-d.js
----------------------------------------------------------------------
diff --git a/spec/plugins/dependencies/D/www/plugin-d.js b/spec/plugins/dependencies/D/www/plugin-d.js
new file mode 100644
index 0000000..e69de29

http://git-wip-us.apache.org/repos/asf/cordova-plugman/blob/bf56f3ed/spec/plugins/dependencies/E/plugin.xml
----------------------------------------------------------------------
diff --git a/spec/plugins/dependencies/E/plugin.xml b/spec/plugins/dependencies/E/plugin.xml
new file mode 100644
index 0000000..2564e96
--- /dev/null
+++ b/spec/plugins/dependencies/E/plugin.xml
@@ -0,0 +1,57 @@
+<?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="E"
+    version="0.6.0">
+
+    <name>Plugin E</name>
+
+    <asset src="www/plugin-e.js" target="plugin-e.js" />
+
+    <config-file target="config.xml" parent="/*">
+        <access origin="build.phonegap.com" />
+    </config-file>
+	
+    <!-- android -->
+    <platform name="android">
+        <config-file target="res/xml/config.xml" parent="plugins">
+            <plugin name="E"
+                value="com.phonegap.E.E"/>
+        </config-file>
+
+        <source-file src="src/android/E.java"
+                target-dir="src/com/phonegap/E" />
+    </platform>
+
+        
+    <!-- ios -->
+    <platform name="ios">
+        <!-- CDV 2.5+ -->
+        <config-file target="config.xml" parent="plugins">
+            <plugin name="E"
+                value="EPluginCommand"/>
+        </config-file>
+
+        <header-file src="src/ios/EPluginCommand.h" />
+        <source-file src="src/ios/EPluginCommand.m"/>
+    </platform>
+</plugin>

http://git-wip-us.apache.org/repos/asf/cordova-plugman/blob/bf56f3ed/spec/plugins/dependencies/E/src/android/E.java
----------------------------------------------------------------------
diff --git a/spec/plugins/dependencies/E/src/android/E.java b/spec/plugins/dependencies/E/src/android/E.java
new file mode 100644
index 0000000..e69de29

http://git-wip-us.apache.org/repos/asf/cordova-plugman/blob/bf56f3ed/spec/plugins/dependencies/E/src/ios/EPluginCommand.h
----------------------------------------------------------------------
diff --git a/spec/plugins/dependencies/E/src/ios/EPluginCommand.h b/spec/plugins/dependencies/E/src/ios/EPluginCommand.h
new file mode 100644
index 0000000..e69de29

http://git-wip-us.apache.org/repos/asf/cordova-plugman/blob/bf56f3ed/spec/plugins/dependencies/E/src/ios/EPluginCommand.m
----------------------------------------------------------------------
diff --git a/spec/plugins/dependencies/E/src/ios/EPluginCommand.m b/spec/plugins/dependencies/E/src/ios/EPluginCommand.m
new file mode 100644
index 0000000..e69de29

http://git-wip-us.apache.org/repos/asf/cordova-plugman/blob/bf56f3ed/spec/plugins/dependencies/E/www/plugin-e.js
----------------------------------------------------------------------
diff --git a/spec/plugins/dependencies/E/www/plugin-e.js b/spec/plugins/dependencies/E/www/plugin-e.js
new file mode 100644
index 0000000..e69de29

http://git-wip-us.apache.org/repos/asf/cordova-plugman/blob/bf56f3ed/spec/plugins/dependencies/README.md
----------------------------------------------------------------------
diff --git a/spec/plugins/dependencies/README.md b/spec/plugins/dependencies/README.md
new file mode 100644
index 0000000..29cc70f
--- /dev/null
+++ b/spec/plugins/dependencies/README.md
@@ -0,0 +1,5 @@
+Here's a general overview of how the plugins in this directory are dependent on each other:
+
+        A          B
+       / \        / \
+      C   '---D--'   E

http://git-wip-us.apache.org/repos/asf/cordova-plugman/blob/bf56f3ed/spec/uninstall.spec.js
----------------------------------------------------------------------
diff --git a/spec/uninstall.spec.js b/spec/uninstall.spec.js
index c39812a..99d6026 100644
--- a/spec/uninstall.spec.js
+++ b/spec/uninstall.spec.js
@@ -1,6 +1,7 @@
 var uninstall = require('../src/uninstall'),
     install = require('../src/install'),
     common = require('../src/platforms/common'),
+    actions = require('../src/util/action-stack'),
     android = require('../src/platforms/android'),
     ios     = require('../src/platforms/ios'),
     blackberry = require('../src/platforms/blackberry'),
@@ -14,41 +15,44 @@ var uninstall = require('../src/uninstall'),
     shell   = require('shelljs'),
     temp    = path.join(os.tmpdir(), 'plugman'),
     dummyplugin = path.join(__dirname, 'plugins', 'DummyPlugin'),
+    dummy_id = 'com.phonegap.plugins.dummyplugin',
     faultyplugin = path.join(__dirname, 'plugins', 'FaultyPlugin'),
     childbrowserplugin = path.join(__dirname, 'plugins', 'ChildBrowser'),
+    dep_a = path.join(__dirname, 'plugins', 'dependencies', 'A'),
+    dep_b = path.join(__dirname, 'plugins', 'dependencies', 'B'),
+    dep_c = path.join(__dirname, 'plugins', 'dependencies', 'C'),
+    dep_d = path.join(__dirname, 'plugins', 'dependencies', 'D'),
+    dep_e = path.join(__dirname, 'plugins', 'dependencies', 'E'),
     android_one_project = path.join(__dirname, 'projects', 'android_one', '*'),
     ios_project = path.join(__dirname, 'projects', 'ios-config-xml', '*'),
     plugins_dir = path.join(temp, 'cordova', 'plugins');
 
 describe('uninstall', function() {
     var copied_plugin_path = path.join(temp,'ChildBrowser');
+    var dummy_plugin_path = path.join(plugins_dir, dummy_id);
 
     beforeEach(function() {
         shell.mkdir('-p', temp);
-        shell.mkdir('-p', plugins_dir);
         shell.cp('-rf', android_one_project, temp);
-        shell.cp('-rf', dummyplugin, plugins_dir);
     });
     afterEach(function() {
         shell.rm('-rf', temp);
     });
 
     describe('success', function() {
-        var android_uninstaller;
         beforeEach(function() {
-            install('android', temp, 'DummyPlugin', plugins_dir, {});
-            android_uninstaller = spyOn(android, 'uninstall');
+            install('android', temp, dummyplugin, plugins_dir, '.', {});
         });
         it('should properly uninstall assets', function() {
             var s = spyOn(common, 'removeFile').andCallThrough();
             var s2 = spyOn(common, 'removeFileF').andCallThrough();
             // making sure the right methods were called
-            uninstall('android', temp, 'DummyPlugin', plugins_dir, {});
+            uninstall('android', temp, dummy_id, plugins_dir, {});
             expect(s).toHaveBeenCalled();
             expect(s.calls.length).toEqual(2);
             
             expect(s2).toHaveBeenCalled();
-            expect(s2.calls.length).toEqual(1);
+            expect(s2.calls.length).toEqual(2);
 
             expect(fs.existsSync(path.join(temp, 'assets', 'www', 'dummyplugin.js'))).toBe(false);
             expect(fs.existsSync(path.join(temp, 'assets', 'www', 'dummyplugin'))).toBe(false);
@@ -61,28 +65,46 @@ describe('uninstall', function() {
             shell.rm('-rf', path.join(temp, 'assets', 'www', 'dummyplugin'));
             
             expect(function() {
-                uninstall('android', temp, 'DummyPlugin', plugins_dir, {});
+                uninstall('android', temp, dummy_id, plugins_dir, {});
             }).toThrow();
 
             expect(sRemoveFile).toHaveBeenCalled();
             expect(sRemoveFile.calls.length).toEqual(2);
             expect(sCopyFile).toHaveBeenCalled();
-            expect(sCopyFile.calls.length).toEqual(1);
+            expect(sCopyFile.calls.length).toEqual(2);
             
             expect(fs.existsSync(path.join(temp, 'assets', 'www', 'dummyplugin.js'))).toBe(true);
         });
-        it('should generate and pass uninstall transaction log to appropriate platform handler\'s uninstall', function() {
-            uninstall('android', temp, 'DummyPlugin', plugins_dir, {});
-            var transactions = android_uninstaller.mostRecentCall.args[0];
-
-            expect(transactions.length).toEqual(1);
-            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');
+            uninstall('android', temp, dummy_id, plugins_dir, {});
+            expect(spy).toHaveBeenCalledWith(plugins_dir, dummy_id, 'android', true);
+        });
+
+        describe('with dependencies', function() {
+            it('should uninstall any dependent plugins', function() {
+                shell.mkdir('-p', plugins_dir);
+                shell.cp('-rf', dep_a, plugins_dir);
+                shell.cp('-rf', dep_d, plugins_dir);
+                shell.cp('-rf', dep_c, plugins_dir);
+                install('android', temp, 'A', plugins_dir, '.', {});
+                var spy = spyOn(actions.prototype, 'process').andCallThrough();
+                uninstall('android', temp, 'A', plugins_dir, {});
+                expect(spy.calls.length).toEqual(3);
+            });
+            it('should not uninstall any dependent plugins that are required by other top-level plugins', function() {
+                shell.mkdir('-p', plugins_dir);
+                shell.cp('-rf', dep_a, plugins_dir);
+                shell.cp('-rf', dep_b, plugins_dir);
+                shell.cp('-rf', dep_d, plugins_dir);
+                shell.cp('-rf', dep_c, plugins_dir);
+                shell.cp('-rf', dep_e, plugins_dir);
+                install('android', temp, 'A', plugins_dir, '.', {});
+                install('android', temp, 'B', plugins_dir, '.', {});
+                var spy = spyOn(actions.prototype, 'process').andCallThrough();
+                uninstall('android', temp, 'A', plugins_dir, {});
+                expect(spy.calls.length).toEqual(2);
+            });
         });
     });
 
@@ -97,37 +119,5 @@ describe('uninstall', function() {
                 uninstall('android', temp, 'SomePlugin', plugins_dir, {});
             }).toThrow('Plugin "SomePlugin" not found. Already uninstalled?');
         });
-       // it('should handle a failed uninstall by passing completed transactions into appropriate handler\'s install method', function() {
-       //     shell.rm('-rf', path.join(temp, '*'));
-       //     shell.mkdir('-p', plugins_dir);
-       //     
-       //     shell.cp('-rf', ios_project, temp);
-       //     shell.cp('-rf', childbrowserplugin, plugins_dir);
-       //     install('ios', temp, 'ChildBrowser', plugins_dir, {});
-
-       //     // make uninstall fail by removing a js asset
-       //     shell.rm(path.join(temp, 'SampleApp', 'Plugins', 'ChildBrowserCommand.m'));
-       //     var s = spyOn(ios, 'install');
-       //     uninstall('ios', temp, 'ChildBrowser', plugins_dir, {});
-       //     var executed_txs = s.mostRecentCall.args[0];
-       //     expect(executed_txs.length).toEqual(1);
-       //     // It only ended up "uninstalling" one source file, so install reversion should pass in that source file to re-install
-       //     expect(executed_txs[0].tag).toEqual('source-file');
-       // });
-        it('should revert assets when uninstall fails', function() {
-            install('android', temp, 'DummyPlugin', plugins_dir, {});
-            
-            var s = spyOn(common, 'copyFile').andCallThrough();
-            
-            shell.rm('-rf', path.join(temp, 'src', 'com', 'phonegap', 'plugins', 'dummyplugin'));
-            expect(function() {
-                uninstall('android', temp, 'DummyPlugin', plugins_dir, {});
-            }).toThrow();
-            expect(s).toHaveBeenCalled();
-            expect(s.calls.length).toEqual(2);
-            
-            expect(fs.existsSync(path.join(temp, 'assets', 'www', 'dummyplugin.js'))).toBe(true);
-            expect(fs.existsSync(path.join(temp, 'assets', 'www', 'dummyplugin'))).toBe(true);
-        });
     });
 });

http://git-wip-us.apache.org/repos/asf/cordova-plugman/blob/bf56f3ed/src/install.js
----------------------------------------------------------------------
diff --git a/src/install.js b/src/install.js
index 37a6dff..9a85e2c 100644
--- a/src/install.js
+++ b/src/install.js
@@ -6,7 +6,7 @@ var path = require('path'),
     action_stack = require('./util/action-stack'),
     platform_modules = require('./platforms');
 
-module.exports = function installPlugin(platform, project_dir, id, plugins_dir, subdir, cli_variables, www_dir, is_top_level, callback) {
+module.exports = function installPlugin(platform, project_dir, id, plugins_dir, subdir, cli_variables, www_dir, callback) {
     if (!platform_modules[platform]) {
         var err = new Error(platform + " not supported.");
         if (callback) callback(err);
@@ -14,6 +14,12 @@ module.exports = function installPlugin(platform, project_dir, id, plugins_dir,
         return;
     }
 
+    var current_stack = new action_stack();
+
+    possiblyFetch(current_stack, platform, project_dir, id, plugins_dir, subdir, cli_variables, www_dir, true, callback);
+};
+
+function possiblyFetch(actions, platform, project_dir, id, plugins_dir, subdir, cli_variables, www_dir, is_top_level, callback) {
     var plugin_dir = path.join(plugins_dir, id);
 
     // Check that the plugin has already been fetched.
@@ -25,15 +31,15 @@ module.exports = function installPlugin(platform, project_dir, id, plugins_dir,
                 callback(err);
             } else {
                 // update ref to plugin_dir after successful fetch, via fetch callback
-                runInstall(platform, project_dir, plugin_dir, plugins_dir, cli_variables, www_dir, is_top_level, callback);
+                runInstall(actions, platform, project_dir, plugin_dir, plugins_dir, cli_variables, www_dir, is_top_level, callback);
             }
         });
     } else {
-        runInstall(platform, project_dir, plugin_dir, plugins_dir, cli_variables, www_dir, is_top_level, callback);
+        runInstall(actions, platform, project_dir, plugin_dir, plugins_dir, cli_variables, www_dir, is_top_level, callback);
     }
-};
+}
 
-function runInstall(platform, project_dir, plugin_dir, plugins_dir, cli_variables, www_dir, is_top_level, callback) {
+function runInstall(actions, platform, project_dir, plugin_dir, plugins_dir, cli_variables, www_dir, is_top_level, callback) {
     var xml_path     = path.join(plugin_dir, 'plugin.xml')
       , xml_text     = fs.readFileSync(xml_path, 'utf-8')
       , plugin_et    = new et.ElementTree(et.XML(xml_text))
@@ -83,7 +89,7 @@ function runInstall(platform, project_dir, plugin_dir, plugins_dir, cli_variable
     var dependencies = plugin_et.findall('dependency');
     if (dependencies && dependencies.length) {
         var end = n(dependencies.length, function() {
-            handleInstall(plugin_id, plugin_et, platform, project_dir, plugins_dir, plugin_basename, plugin_dir, filtered_variables, www_dir, is_top_level, callback);
+            handleInstall(actions, plugin_id, plugin_et, platform, project_dir, plugins_dir, plugin_basename, plugin_dir, filtered_variables, www_dir, is_top_level, callback);
         });
         dependencies.forEach(function(dep) {
             var dep_plugin_id = dep.attrib.id;
@@ -92,21 +98,21 @@ function runInstall(platform, project_dir, plugin_dir, plugins_dir, cli_variable
             if (dep_subdir) {
                 dep_subdir = path.join.apply(null, dep_subdir.split('/'));
             }
-
-            if (fs.existsSync(path.join(plugins_dir, dep_plugin_id))) {
-                console.log('Dependent plugin ' + dep.attrib.id + ' already fetched, using that version.');
-                module.exports(platform, project_dir, dep_plugin_id, plugins_dir, dep_subdir, filtered_variables, www_dir, false, end);
+            var dep_plugin_dir = path.join(plugins_dir, dep_plugin_id);
+            if (fs.existsSync(dep_plugin_dir)) {
+                console.log('Dependent plugin ' + dep_plugin_id + ' already fetched, using that version.');
+                runInstall(actions, platform, project_dir, dep_plugin_dir, plugins_dir, filtered_variables, www_dir, false, end);
             } else {
-                console.log('Dependent plugin ' + dep.attrib.id + ' not fetched, retrieving then installing.');
-                module.exports(platform, project_dir, dep_url, plugins_dir, dep_subdir, filtered_variables, www_dir, false, end);
+                console.log('Dependent plugin ' + dep_plugin_id + ' not fetched, retrieving then installing.');
+                possiblyFetch(actions, platform, project_dir, dep_url, plugins_dir, dep_subdir, filtered_variables, www_dir, false, end);
             }
         });
     } else {
-        handleInstall(plugin_id, plugin_et, platform, project_dir, plugins_dir, plugin_basename, plugin_dir, filtered_variables, www_dir, is_top_level, callback);
+        handleInstall(actions, plugin_id, plugin_et, platform, project_dir, plugins_dir, plugin_basename, plugin_dir, filtered_variables, www_dir, is_top_level, callback);
     }
 }
 
-function handleInstall(plugin_id, plugin_et, platform, project_dir, plugins_dir, plugin_basename, plugin_dir, filtered_variables, www_dir, is_top_level, callback) {
+function handleInstall(actions, plugin_id, plugin_et, platform, project_dir, plugins_dir, plugin_basename, plugin_dir, filtered_variables, www_dir, is_top_level, callback) {
     var handler = platform_modules[platform];
     www_dir = www_dir || handler.www_dir(project_dir);
 
@@ -121,30 +127,30 @@ function handleInstall(plugin_id, plugin_et, platform, project_dir, plugins_dir,
 
         // queue up native stuff
         sourceFiles && sourceFiles.forEach(function(source) {
-            action_stack.push(action_stack.createAction(handler["source-file"].install, [source, plugin_dir, project_dir], handler["source-file"].uninstall, [source, project_dir]));
+            actions.push(actions.createAction(handler["source-file"].install, [source, plugin_dir, project_dir], handler["source-file"].uninstall, [source, project_dir]));
         });
 
         headerFiles && headerFiles.forEach(function(header) {
-            action_stack.push(action_stack.createAction(handler["header-file"].install, [header, plugin_dir, project_dir], handler["header-file"].uninstall, [header, project_dir]));
+            actions.push(actions.createAction(handler["header-file"].install, [header, plugin_dir, project_dir], handler["header-file"].uninstall, [header, project_dir]));
         });
 
         resourceFiles && resourceFiles.forEach(function(resource) {
-            action_stack.push(action_stack.createAction(handler["resource-file"].install, [resource, plugin_dir, project_dir], handler["resource-file"].uninstall, [resource, project_dir]));
+            actions.push(actions.createAction(handler["resource-file"].install, [resource, plugin_dir, project_dir], handler["resource-file"].uninstall, [resource, project_dir]));
         });
 
         frameworks && frameworks.forEach(function(framework) {
-            action_stack.push(action_stack.createAction(handler["framework"].install, [framework, plugin_dir, project_dir], handler["framework"].uninstall, [framework, project_dir]));
+            actions.push(actions.createAction(handler["framework"].install, [framework, plugin_dir, project_dir], handler["framework"].uninstall, [framework, project_dir]));
         });
     }
 
     // queue up asset installation
     var common = require('./platforms/common');
     assets && assets.forEach(function(asset) {
-        action_stack.push(action_stack.createAction(common.asset.install, [asset, plugin_dir, www_dir], common.asset.uninstall, [asset, www_dir, plugin_id]));
+        actions.push(actions.createAction(common.asset.install, [asset, plugin_dir, www_dir], common.asset.uninstall, [asset, www_dir, plugin_id]));
     });
 
     // run through the action stack
-    action_stack.process(platform, project_dir, function(err) {
+    actions.process(platform, project_dir, function(err) {
         if (err) {
             if (callback) callback(err);
             else throw err;

http://git-wip-us.apache.org/repos/asf/cordova-plugman/blob/bf56f3ed/src/uninstall.js
----------------------------------------------------------------------
diff --git a/src/uninstall.js b/src/uninstall.js
index 986e7c1..7528876 100644
--- a/src/uninstall.js
+++ b/src/uninstall.js
@@ -9,7 +9,7 @@ var path = require('path'),
     underscore = require('underscore'),
     platform_modules = require('./platforms');
 
-module.exports = function uninstallPlugin(platform, project_dir, id, plugins_dir, cli_variables, www_dir, is_top_level, callback) {
+module.exports = function uninstallPlugin(platform, project_dir, id, plugins_dir, cli_variables, www_dir, callback) {
     if (!platform_modules[platform]) {
         var err = new Error(platform + " not supported.");
         if (callback) callback(err);
@@ -20,16 +20,18 @@ module.exports = function uninstallPlugin(platform, project_dir, id, plugins_dir
     var plugin_dir = path.join(plugins_dir, id);
 
     if (!fs.existsSync(plugin_dir)) {
-        var err = new Error('Plugin "' + name + '" not found. Already uninstalled?');
+        var err = new Error('Plugin "' + id + '" not found. Already uninstalled?');
         if (callback) callback(err);
         else throw err;
         return;
     }
 
-    runUninstall(platform, project_dir, plugin_dir, plugins_dir, cli_variables, www_dir, is_top_level, callback);
+    var current_stack = new action_stack();
+
+    runUninstall(current_stack, platform, project_dir, plugin_dir, plugins_dir, cli_variables, www_dir, true, callback);
 };
 
-function runUninstall(platform, project_dir, plugin_dir, plugins_dir, cli_variables, www_dir, is_top_level, callback) {
+function runUninstall(actions, platform, project_dir, plugin_dir, plugins_dir, cli_variables, www_dir, is_top_level, callback) {
     var xml_path     = path.join(plugin_dir, 'plugin.xml')
       , xml_text     = fs.readFileSync(xml_path, 'utf-8')
       , plugin_et    = new et.ElementTree(et.XML(xml_text))
@@ -60,18 +62,19 @@ function runUninstall(platform, project_dir, plugin_dir, plugins_dir, cli_variab
     var danglers = underscore.difference.apply(null, diff_arr);
     if (dependents.length && danglers && danglers.length) {
         var end = n(danglers.length, function() {
-            handleUninstall(platform, plugin_id, plugin_et, project_dir, www_dir, plugins_dir, plugin_dir, is_top_level, callback);
+            handleUninstall(actions, platform, plugin_id, plugin_et, project_dir, www_dir, plugins_dir, plugin_dir, is_top_level, callback);
         });
         danglers.forEach(function(dangler) {
-            module.exports(platform, project_dir, dangler, plugins_dir, cli_variables, www_dir, false /* TODO: should this "is_top_level" param be false for dependents? */, end);
+            var dependent_path = path.join(plugins_dir, dangler);
+            runUninstall(actions, platform, project_dir, dependent_path, plugins_dir, cli_variables, www_dir, false /* TODO: should this "is_top_level" param be false for dependents? */, end);
         });
     } else {
         // this plugin can get axed by itself, gogo!
-        handleUninstall(platform, plugin_id, plugin_et, project_dir, www_dir, plugins_dir, plugin_dir, is_top_level, callback);
+        handleUninstall(actions, platform, plugin_id, plugin_et, project_dir, www_dir, plugins_dir, plugin_dir, is_top_level, callback);
     }
 }
 
-function handleUninstall(platform, plugin_id, plugin_et, project_dir, www_dir, plugins_dir, plugin_dir, is_top_level, callback) {
+function handleUninstall(actions, platform, plugin_id, plugin_et, project_dir, www_dir, plugins_dir, plugin_dir, is_top_level, callback) {
     var platform_modules = require('./platforms');
     var handler = platform_modules[platform];
     var platformTag = plugin_et.find('./platform[@name="'+platform+'"]');
@@ -87,30 +90,30 @@ function handleUninstall(platform, plugin_id, plugin_et, project_dir, www_dir, p
 
         // queue up native stuff
         sourceFiles && sourceFiles.forEach(function(source) {
-            action_stack.push(action_stack.createAction(handler["source-file"].uninstall, [source, project_dir], handler["source-file"].install, [source, plugin_dir, project_dir]));
+            actions.push(actions.createAction(handler["source-file"].uninstall, [source, project_dir], handler["source-file"].install, [source, plugin_dir, project_dir]));
         });
 
         headerFiles && headerFiles.forEach(function(header) {
-            action_stack.push(action_stack.createAction(handler["header-file"].uninstall, [header, project_dir], handler["header-file"].install, [header, plugin_dir, project_dir]));
+            actions.push(actions.createAction(handler["header-file"].uninstall, [header, project_dir], handler["header-file"].install, [header, plugin_dir, project_dir]));
         });
 
         resourceFiles && resourceFiles.forEach(function(resource) {
-            action_stack.push(action_stack.createAction(handler["resource-file"].uninstall, [resource, project_dir], handler["resource-file"].install, [resource, plugin_dir, project_dir]));
+            actions.push(actions.createAction(handler["resource-file"].uninstall, [resource, project_dir], handler["resource-file"].install, [resource, plugin_dir, project_dir]));
         });
 
         frameworks && frameworks.forEach(function(framework) {
-            action_stack.push(action_stack.createAction(handler["framework"].uninstall, [framework, project_dir], handler["framework"].install, [framework, plugin_dir, project_dir]));
+            actions.push(actions.createAction(handler["framework"].uninstall, [framework, project_dir], handler["framework"].install, [framework, plugin_dir, project_dir]));
         });
     }
 
     // queue up asset installation
     var common = require('./platforms/common');
     assets && assets.forEach(function(asset) {
-        action_stack.push(action_stack.createAction(common.asset.uninstall, [asset, www_dir, plugin_id], common.asset.install, [asset, plugin_dir, www_dir]));
+        actions.push(actions.createAction(common.asset.uninstall, [asset, www_dir, plugin_id], common.asset.install, [asset, plugin_dir, www_dir]));
     });
 
     // run through the action stack
-    action_stack.process(platform, project_dir, function(err) {
+    actions.process(platform, project_dir, function(err) {
         if (err) {
             if (callback) callback(err);
             else throw err;

http://git-wip-us.apache.org/repos/asf/cordova-plugman/blob/bf56f3ed/src/util/action-stack.js
----------------------------------------------------------------------
diff --git a/src/util/action-stack.js b/src/util/action-stack.js
index a76f33c..115c6b7 100644
--- a/src/util/action-stack.js
+++ b/src/util/action-stack.js
@@ -1,10 +1,12 @@
 var ios = require('../platforms/ios'),
     fs = require('fs');
 
-var stack = [];
-var completed = [];
+function ActionStack() {
+    this.stack = [];
+    this.completed = [];
+}
 
-module.exports = {
+ActionStack.prototype = {
     createAction:function(handler, action_params, reverter, revert_params) {
         return {
             handler:{
@@ -18,26 +20,26 @@ module.exports = {
         };
     },
     push:function(tx) {
-        stack.push(tx);
+        this.stack.push(tx);
     },
     process:function(platform, project_dir, callback) {
         if (platform == 'ios') {
             // parse xcode project file once
             var project_files = ios.parseIOSProjectFiles(project_dir);
         }
-        while(stack.length) {
-            var action = stack.shift();
+        while(this.stack.length) {
+            var action = this.stack.shift();
             var handler = action.handler.run;
             var action_params = action.handler.params;
             if (platform == 'ios') action_params.push(project_files);
             try {
                 handler.apply(null, action_params);
             } catch(e) {
-                var incomplete = stack.unshift(action);
+                var incomplete = this.stack.unshift(action);
                 var issue = 'Uh oh!\n';
                 // revert completed tasks
-                while(completed.length) {
-                    var undo = completed.shift();
+                while(this.completed.length) {
+                    var undo = this.completed.shift();
                     var revert = undo.reverter.run;
                     var revert_params = undo.reverter.params;
                     if (platform == 'ios') revert_params.push(project_files);
@@ -52,7 +54,7 @@ module.exports = {
                 else throw e;
                 return;
             }
-            completed.push(action);
+            this.completed.push(action);
         }
         if (platform == 'ios') {
             // write out xcodeproj file
@@ -61,3 +63,5 @@ module.exports = {
         if (callback) callback();
     }
 };
+
+module.exports = ActionStack;