You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@cordova.apache.org by st...@apache.org on 2014/08/05 00:53:15 UTC

[2/3] git commit: CB-7190: Add browserify support in cordova-lib/cordova-cli

CB-7190: Add browserify support in cordova-lib/cordova-cli


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

Branch: refs/heads/master
Commit: c2dd20cd2781500b5f2136b4b58f1a17c8d1da18
Parents: a6d23ed
Author: Suraj Pindoria <su...@yahoo.com>
Authored: Tue Jul 29 11:51:31 2014 -0700
Committer: Steven Gill <st...@gmail.com>
Committed: Mon Aug 4 15:52:12 2014 -0700

----------------------------------------------------------------------
 .../spec-plugman/install-browserify.spec.js     | 505 +++++++++++++++++++
 .../spec-plugman/uninstall-browserify.spec.js   | 309 ++++++++++++
 cordova-lib/src/cordova/plugin.js               |   1 +
 cordova-lib/src/plugman/install.js              |   6 +-
 cordova-lib/src/plugman/plugman.js              |   1 +
 cordova-lib/src/plugman/prepare-browserify.js   |   2 +-
 cordova-lib/src/plugman/uninstall.js            |  10 +-
 7 files changed, 829 insertions(+), 5 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/cordova-lib/blob/c2dd20cd/cordova-lib/spec-plugman/install-browserify.spec.js
----------------------------------------------------------------------
diff --git a/cordova-lib/spec-plugman/install-browserify.spec.js b/cordova-lib/spec-plugman/install-browserify.spec.js
new file mode 100644
index 0000000..1731707
--- /dev/null
+++ b/cordova-lib/spec-plugman/install-browserify.spec.js
@@ -0,0 +1,505 @@
+// /**
+//     Licensed to the Apache Software Foundation (ASF) under one
+//     or more contributor license agreements.  See the NOTICE file
+//     distributed with this work for additional information
+//     regarding copyright ownership.  The ASF licenses this file
+//     to you under the Apache License, Version 2.0 (the
+//     "License"); you may not use this file except in compliance
+//     with the License.  You may obtain a copy of the License at
+
+//     http://www.apache.org/licenses/LICENSE-2.0
+
+//     Unless required by applicable law or agreed to in writing,
+//     software distributed under the License is distributed on an
+//     "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+//     KIND, either express or implied.  See the License for the
+//     specific language governing permissions and limitations
+//     under the License.
+// */
+var install = require('../src/plugman/install'),
+    actions = require('../src/plugman/util/action-stack'),
+    config_changes = require('../src/plugman/util/config-changes'),
+    xml_helpers = require('../src/util/xml-helpers'),
+    events  = require('../src/events'),
+    plugman = require('../src/plugman/plugman'),
+    browserify = require('../src/plugman/prepare-browserify'),
+    platforms = require('../src/plugman/platforms/common'),
+    common  = require('./common'),
+    fs      = require('fs'),
+    os      = require('os'),
+    path    = require('path'),
+    shell   = require('shelljs'),
+    child_process = require('child_process'),
+    semver  = require('semver'),
+    Q = require('q'),
+    spec    = __dirname,
+    done    = false,
+    srcProject = path.join(spec, 'projects', 'android_install'),
+    temp_dir = path.join(os.tmpdir(), 'plugman-test'),
+    project = path.join(temp_dir, 'android_install'),
+    plugins_dir = path.join(spec, 'plugins'),
+    plugins_install_dir = path.join(project, 'cordova', 'plugins'),
+    plugins = {
+        'DummyPlugin' : path.join(plugins_dir, 'DummyPlugin'),
+        'EnginePlugin' : path.join(plugins_dir, 'EnginePlugin'),
+        'EnginePluginAndroid' : path.join(plugins_dir, 'EnginePluginAndroid'),
+        'ChildBrowser' : path.join(plugins_dir, 'ChildBrowser'),
+        'VariablePlugin' : path.join(plugins_dir, 'VariablePlugin'),
+        'A' : path.join(plugins_dir, 'dependencies', 'A'),
+        'B' : path.join(plugins_dir, 'dependencies', 'B'),
+        'C' : path.join(plugins_dir, 'dependencies', 'C'),
+        'F' : path.join(plugins_dir, 'dependencies', 'F'),
+        'G' : path.join(plugins_dir, 'dependencies', 'G')
+    },
+    promise,
+    results = {},
+    dummy_id = 'com.phonegap.plugins.dummyplugin';
+
+
+// Pre-crete the temp dir, without it the test fails.
+shell.mkdir('-p', temp_dir);
+
+function installPromise(f) {
+  f.then(function(res) { done = true; }, function(err) { done = err; });
+}
+
+var existsSync = fs.existsSync;
+
+// Mocked functions for tests
+var fake = {
+    'existsSync' : {
+        'noPlugins' : function(path){
+            // fake installed plugin directories as 'not found'
+            if( path.slice(-5) !== '.json' && path.indexOf(plugins_install_dir) >= 0) {
+                return false;
+            }
+
+            return existsSync(path);
+        }
+    },
+    'fetch' : {
+        'dependencies' : function(id, dir) {
+            if(id == plugins['A'])
+                return Q(id); // full path to plugin
+
+            return Q( path.join(plugins_dir, 'dependencies', id) );
+        }
+    }
+}
+
+describe('start', function() {
+    var prepare, prepareBrowserify, config_queue_add, proc, actions_push, ca, emit, write;
+
+    beforeEach(function() {
+        prepare = spyOn(plugman, 'prepare');
+        prepareBrowserify = spyOn(plugman, 'prepareBrowserify');
+        config_queue_add = spyOn(config_changes, 'add_installed_plugin_to_prepare_queue');
+        proc = spyOn(actions.prototype, 'process').andReturn( Q(true) );
+        actions_push = spyOn(actions.prototype, 'push');
+        ca = spyOn(actions.prototype, 'createAction');
+    });
+    it('start', function() {
+        shell.rm('-rf', project);
+        shell.cp('-R', path.join(srcProject, '*'), project);
+
+        done = false;
+        promise = Q()
+         .then(
+            function(){ return install('android', project, plugins['DummyPlugin'], plugins_install_dir, { browserify: true }) }
+        ).then(
+            function(){
+                results['actions_callCount'] = actions_push.callCount;
+                results['actions_create'] = ca.argsForCall[0];
+                results['config_add'] = config_queue_add.argsForCall[0];
+
+                return Q();
+            }
+        ).then(
+            function(){ 
+                return install('android', project, plugins['EnginePlugin'], plugins_install_dir, { browserify: true }) }
+        ).then(
+            function(){
+                emit = spyOn(events, 'emit');
+                return install('android', project, plugins['ChildBrowser'], plugins_install_dir, { browserify: true })
+            }
+        ).then(
+            function(){
+                return install('android', project, plugins['VariablePlugin'], plugins_install_dir, { browserify: true, cli_variables:{API_KEY:'batman'} })
+            }
+        ).then(
+            function(){
+                done = true;
+                results['prepareCount'] = prepareBrowserify.callCount;
+                results['emit_results'] = [];
+
+                for(var i in emit.calls) {
+                    if(emit.calls[i].args[0] === 'results')
+                        results['emit_results'].push(emit.calls[i].args[1]);
+                }
+
+                events.emit("verbose", "***** DONE START *****");
+            }
+        ).fail(
+            function(error) {
+                expect(error).toBeUndefined();
+            }
+        );
+        waitsFor(function() { return done; }, 'promise never resolved', 500);
+    });
+});
+
+describe('install', function() {
+    var chmod, exec, proc, add_to_queue, prepare, actions_push, c_a, mkdir, cp, rm, fetchSpy, emit;
+
+    beforeEach(function() {
+        prepare = spyOn(plugman, 'prepare').andReturn( Q(true) );
+        prepareBrowserify = spyOn(plugman, 'prepareBrowserify');
+        exec = spyOn(child_process, 'exec').andCallFake(function(cmd, cb) {
+
+            cb(false, '', '');
+        });
+        spyOn(fs, 'mkdirSync').andReturn(true);
+        spyOn(shell, 'mkdir').andReturn(true);
+        spyOn(platforms, 'copyFile').andReturn(true);
+
+        fetchSpy = spyOn(plugman.raw, 'fetch').andReturn( Q( plugins['EnginePlugin'] ) );
+        chmod = spyOn(fs, 'chmodSync').andReturn(true);
+        fsWrite = spyOn(fs, 'writeFileSync').andReturn(true);
+        cp = spyOn(shell, 'cp').andReturn(true);
+        rm = spyOn(shell, 'rm').andReturn(true);
+        add_to_queue = spyOn(config_changes, 'add_installed_plugin_to_prepare_queue');
+        done = false;
+    });
+
+    describe('success', function() {
+        it('should call prepare after a successful install', function() {
+           expect(results['prepareCount']).toBe(4);
+        });
+
+        it('should emit a results event with platform-agnostic <info>', function() {
+            // ChildBrowser
+            expect(results['emit_results'][0]).toBe('No matter what platform you are installing to, this notice is very important.');
+        });
+        it('should emit a results event with platform-specific <info>', function() {
+            // ChildBrowser
+            expect(results['emit_results'][1]).toBe('Please make sure you read this because it is very important to complete the installation of your plugin.');
+        });
+        it('should interpolate variables into <info> tags', function() {
+            // VariableBrowser
+            expect(results['emit_results'][2]).toBe('Remember that your api key is batman!');
+        });
+
+        it('should call fetch if provided plugin cannot be resolved locally', function() {
+            fetchSpy.andReturn( Q( plugins['DummyPlugin'] ) );
+            spyOn(fs, 'existsSync').andCallFake( fake['existsSync']['noPlugins'] );
+
+            runs(function() {
+                installPromise(install('android', project, 'CLEANYOURSHORTS', plugins_dir, { browserify: true } ));
+            });
+            waitsFor(function() { return done; }, 'install promise never resolved', 200);
+            runs(function() {
+                expect(done).toBe(true);
+                expect(fetchSpy).toHaveBeenCalled();
+            });
+        });
+
+        it('should call the config-changes module\'s add_installed_plugin_to_prepare_queue method after processing an install', function() {
+           expect(results['config_add']).toEqual([plugins_install_dir, dummy_id, 'android', {}, true]);
+        });
+        it('should queue up actions as appropriate for that plugin and call process on the action stack',
+           function() {
+                expect(results['actions_callCount']).toEqual(3);
+                expect(results['actions_create']).toEqual([jasmine.any(Function), [jasmine.any(Object), path.join(plugins_install_dir, dummy_id), project, dummy_id], jasmine.any(Function), [jasmine.any(Object), project, dummy_id]]);
+        });
+
+        it('should check version if plugin has engine tag', function(){
+            var satisfies = spyOn(semver, 'satisfies').andReturn(true);
+            exec.andCallFake(function(cmd, cb) {
+                cb(null, '2.5.0\n');
+            });
+
+            runs(function() {
+                installPromise( install('android', project, plugins['EnginePlugin'], plugins_install_dir, { browserify: true }) );
+            });
+            waitsFor(function() { return done; }, 'install promise never resolved', 200);
+            runs(function() {
+                expect(satisfies).toHaveBeenCalledWith('2.5.0','>=2.3.0');
+            });
+        });
+        it('should check version and munge it a little if it has "rc" in it so it plays nice with semver (introduce a dash in it)', function() {
+            var satisfies = spyOn(semver, 'satisfies').andReturn(true);
+            exec.andCallFake(function(cmd, cb) {
+                cb(null, '3.0.0rc1\n');
+            });
+
+            runs(function() {
+                installPromise( install('android', project, plugins['EnginePlugin'], plugins_install_dir, { browserify: true }) );
+            });
+            waitsFor(function() { return done; }, 'install promise never resolved', 200);
+            runs(function() {
+                expect(satisfies).toHaveBeenCalledWith('3.0.0-rc1','>=2.3.0');
+            });
+        });
+        it('should check specific platform version over cordova version if specified', function() {
+            var spy = spyOn(semver, 'satisfies').andReturn(true);
+            exec.andCallFake(function(cmd, cb) {
+                cb(null, '3.1.0\n');
+            });
+            fetchSpy.andReturn( Q( plugins['EnginePluginAndroid'] ) );
+
+            runs(function() {
+                installPromise( install('android', project, plugins['EnginePluginAndroid'], plugins_install_dir, { browserify: true }) );
+            });
+            waitsFor(function() { return done; }, 'install promise never resolved', 200);
+            runs(function() {
+                expect(spy).toHaveBeenCalledWith('3.1.0','>=3.1.0');
+            });
+        });
+        it('should check platform sdk version if specified', function() {
+            var spy = spyOn(semver, 'satisfies').andReturn(true);
+            fetchSpy.andReturn( Q( plugins['EnginePluginAndroid'] ) );
+            exec.andCallFake(function(cmd, cb) {
+                cb(null, '18\n');
+            });
+
+            runs(function() {
+                installPromise( install('android', project, 'EnginePluginAndroid', plugins_install_dir, { browserify: true }) );
+            });
+            waitsFor(function() { return done; }, 'install promise never resolved', 200);
+            runs(function() {
+                // <engine name="cordova" VERSION=">=3.0.0"/>
+                // <engine name="cordova-android" VERSION=">=3.1.0"/>
+                // <engine name="android-sdk" VERSION=">=18"/>
+
+                expect(spy.calls.length).toBe(3);
+                expect(spy.calls[0].args).toEqual([ '18.0.0', '>=3.0.0' ]);
+                expect(spy.calls[1].args).toEqual([ '18.0.0', '>=3.1.0' ]);
+                expect(spy.calls[2].args).toEqual([ '18.0.0','>=18' ]);
+            });
+        });
+        it('should check engine versions', function() {
+            var spy = spyOn(semver, 'satisfies').andReturn(true);
+            fetchSpy.andReturn( Q( plugins['EnginePlugin'] ) );
+
+            runs(function() {
+                installPromise( install('android', project, plugins['EnginePlugin'], plugins_install_dir, { browserify: true }) );
+            });
+            waitsFor(function() { return done; }, 'install promise never resolved', 200);
+            runs(function() {
+                // <engine name="cordova" version=">=2.3.0"/>
+                // <engine name="cordova-plugman" version=">=0.10.0" />
+                // <engine name="mega-fun-plugin" version=">=1.0.0" scriptSrc="megaFunVersion" platform="*" />
+                // <engine name="mega-boring-plugin" version=">=3.0.0" scriptSrc="megaBoringVersion" platform="ios|android" />
+
+                var plugmanVersion = require('../package.json').version;
+
+                expect(spy.calls.length).toBe(4);
+                expect(spy.calls[0].args).toEqual([ null, '>=2.3.0' ]);
+                expect(spy.calls[1].args).toEqual([ plugmanVersion, '>=0.10.0' ]);
+                expect(spy.calls[2].args).toEqual([ null, '>=1.0.0' ]);
+                expect(spy.calls[3].args).toEqual([ null, '>=3.0.0' ]);
+            });
+        });
+        it('should not check custom engine version that is not supported for platform', function() {
+            var spy = spyOn(semver, 'satisfies').andReturn(true);
+            runs(function() {
+                installPromise( install('blackberry10', project, plugins['EnginePlugin'], plugins_install_dir, { browserify: true }) );
+            });
+            waitsFor(function() { return done; }, 'install promise never resolved', 200);
+            runs(function() {
+                expect(spy).not.toHaveBeenCalledWith('','>=3.0.0');
+            });
+        });
+
+        describe('with dependencies', function() {
+            var emit;
+            beforeEach(function() {
+                spyOn(fs, 'existsSync').andCallFake( fake['existsSync']['noPlugins'] );
+                fetchSpy.andCallFake( fake['fetch']['dependencies'] );
+                emit = spyOn(events, 'emit');
+                exec.andCallFake(function(cmd, cb) {
+                    cb(null, '9.0.0\n');
+                });
+            });
+
+            it('should install any dependent plugins if missing', function() {
+                runs(function() {
+                    installPromise( install('android', project, plugins['A'], plugins_install_dir, { browserify: true }) );
+                });
+                waitsFor(function() { return done; }, 'install promise never resolved', 200);
+                runs(function() {
+                    // Look for 'Installing plugin ...' in events
+                    var install = common.spy.getInstall(emit);
+
+                    expect(install).toEqual([
+                        'Install start for "C" on android.',
+                        'Install start for "D" on android.',
+                        'Install start for "A" on android.'
+                    ]);
+                });
+            });
+
+            it('should install any dependent plugins from registry when url is not defined', function() {
+                // Plugin A depends on C & D
+                runs(function() {
+                    installPromise( install('android', project, plugins['A'], plugins_install_dir, { browserify: true }) );
+                });
+                waitsFor(function() { return done; }, 'promise never resolved', 200);
+                runs(function() {
+                    // TODO: this is same test as above? Need test other dependency with url=?
+                    var install = common.spy.getInstall(emit);
+
+                    expect(install).toEqual([
+                        'Install start for "C" on android.',
+                        'Install start for "D" on android.',
+                        'Install start for "A" on android.'
+                    ]);;
+                });
+            });
+
+            it('should process all dependent plugins with alternate routes to the same plugin', function() {
+                // Plugin F depends on A, C, D and E
+                runs(function () {
+                    installPromise(install('android', project, plugins['F'], plugins_install_dir, { browserify: true }));
+                });
+                waitsFor(function () { return done; }, 'install promise never resolved', 200);
+                runs(function () {
+                    var install = common.spy.getInstall(emit);
+
+                    expect(install).toEqual([
+                        'Install start for "C" on android.',
+                        'Install start for "D" on android.',
+                        'Install start for "A" on android.',
+                        'Install start for "D" on android.',
+                        'Install start for "F" on android.'
+                    ]);
+                });
+            });
+
+            it('should throw if there is a cyclic dependency', function() {
+                runs(function () {
+                    installPromise( install('android', project, plugins['G'], plugins_install_dir, { browserify: true }) );
+                });
+                waitsFor(function () { return done; }, 'install promise never resolved', 200);
+                runs(function () {
+                    var install = common.spy.getInstall(emit);
+
+                    expect(done.message).toEqual('Cyclic dependency from G to H');
+                });
+            });
+
+            it('install subdir relative to top level plugin if no fetch meta', function() {
+                runs(function () {
+                    installPromise(install('android', project, plugins['B'], plugins_install_dir, { browserify: true }));
+                });
+                waitsFor(function () { return done; }, 'install promise never resolved', 200);
+                runs(function () {
+                    var install = common.spy.getInstall(emit);
+
+                    expect(install).toEqual([
+                        'Install start for "D" on android.',
+                        'Install start for "E" on android.',
+                        'Install start for "B" on android.'
+                    ]);
+                });
+            });
+
+            it('install uses meta data (if available) of top level plugin source', function() {
+                // Fake metadata so plugin 'B' appears from 'meta/B'
+                var meta = require('../src/plugman/util/metadata');
+                spyOn(meta, 'get_fetch_metadata').andCallFake(function(){
+                    return {
+                        source: {type: 'dir', url: path.join(plugins['B'], '..', 'meta')}
+                    };
+                });
+
+                runs(function () {
+                    installPromise(install('android', project, plugins['B'], plugins_install_dir, { browserify: true }));
+                });
+                waitsFor(function () { return done; }, 'install promise never resolved', 200);
+                runs(function () {
+                    var install = common.spy.getInstall(emit);
+
+                    expect(install).toEqual([
+                        'Install start for "D" on android.',
+                        'Install start for "E" on android.',
+                        'Install start for "B" on android.'
+                    ]);
+
+                    var copy = common.spy.startsWith(emit, "Copying from");
+                    expect(copy.length).toBe(3);
+                    expect(copy[0].indexOf(path.normalize('meta/D')) > 0).toBe(true);
+                    expect(copy[1].indexOf(path.normalize('meta/subdir/E')) > 0).toBe(true);
+                });
+            });
+        });
+
+    });
+
+    describe('failure', function() {
+        it('should throw if platform is unrecognized', function() {
+            runs(function() {
+                installPromise( install('atari', project, 'SomePlugin', plugins_install_dir, { browserify: true }) );
+            });
+            waitsFor(function() { return done; }, 'install promise never resolved', 200);
+            runs(function() {
+                expect(''+done).toContain('atari not supported.');
+            });
+        });
+        it('should throw if variables are missing', function() {
+            runs(function() {
+                installPromise( install('android', project, plugins['VariablePlugin'], plugins_install_dir, { browserify: true }) );
+            });
+            waitsFor(function(){ return done; }, 'install promise never resolved', 200);
+            runs(function() {
+                expect(''+done).toContain('Variable(s) missing: API_KEY');
+            });
+        });
+        it('should throw if git is not found on the path and a remote url is requested', function() {
+            spyOn(fs, 'existsSync').andCallFake( fake['existsSync']['noPlugins'] );
+            fetchSpy.andCallThrough();
+            var which_spy = spyOn(shell, 'which').andReturn(null);
+            runs(function() {
+                installPromise( install('android', project, 'https://git-wip-us.apache.org/repos/asf/cordova-plugin-camera.git', plugins_install_dir, { browserify: true }) );
+            });
+            waitsFor(function(){ return done; }, 'install promise never resolved', 200);
+            runs(function() {
+                expect(''+done).toContain('"git" command line tool is not installed: make sure it is accessible on your PATH.');
+            });
+        });
+        it('should throw if plugin version is less than the minimum requirement', function(){
+            var spy = spyOn(semver, 'satisfies').andReturn(false);
+            exec.andCallFake(function(cmd, cb) {
+                cb(null, '0.0.1\n');
+            });
+            runs(function() {
+                installPromise( install('android', project, plugins['EnginePlugin'], plugins_install_dir, { browserify: true }) );
+            });
+            waitsFor(function(){ return done; }, 'install promise never resolved', 200);
+            runs(function() {
+                expect(''+done).toContain('Plugin doesn\'t support this project\'s cordova version. cordova: 0.0.1, failed version requirement: >=2.3.0');
+            });
+        });
+    });
+
+});
+
+// When run using 'npm test', the removal of temp_dir is causing
+// tests in 'install.spec.js' to fail.
+
+// describe('end', function() {
+
+//     it('end', function() {
+//         done = false;
+
+//         promise.fin(function(err){
+//             if(err)
+//                 events.emit('error', err);
+
+//             shell.rm('-rf', temp_dir);
+//             done = true;
+//         });
+
+//         waitsFor(function() { return done; }, 'promise never resolved', 500);
+//     });
+// });

http://git-wip-us.apache.org/repos/asf/cordova-lib/blob/c2dd20cd/cordova-lib/spec-plugman/uninstall-browserify.spec.js
----------------------------------------------------------------------
diff --git a/cordova-lib/spec-plugman/uninstall-browserify.spec.js b/cordova-lib/spec-plugman/uninstall-browserify.spec.js
new file mode 100644
index 0000000..4960dc8
--- /dev/null
+++ b/cordova-lib/spec-plugman/uninstall-browserify.spec.js
@@ -0,0 +1,309 @@
+/**
+    Licensed to the Apache Software Foundation (ASF) under one
+    or more contributor license agreements.  See the NOTICE file
+    distributed with this work for additional information
+    regarding copyright ownership.  The ASF licenses this file
+    to you under the Apache License, Version 2.0 (the
+    "License"); you may not use this file except in compliance
+    with the License.  You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing,
+    software distributed under the License is distributed on an
+    "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+    KIND, either express or implied.  See the License for the
+    specific language governing permissions and limitations
+    under the License.
+*/
+var uninstall = require('../src/plugman/uninstall'),
+    install = require('../src/plugman/install'),
+    actions = require('../src/plugman/util/action-stack'),
+    config_changes = require('../src/plugman/util/config-changes'),
+    events  = require('../src/events'),
+    plugman = require('../src/plugman/plugman'),
+    common  = require('./common'),
+    fs      = require('fs'),
+    path    = require('path'),
+    shell   = require('shelljs'),
+    Q       = require('q'),
+    spec    = __dirname,
+    done    = false,
+    srcProject = path.join(spec, 'projects', 'android_uninstall'),
+    project = path.join(spec, 'projects', 'android_uninstall.test'),
+    project2 = path.join(spec, 'projects', 'android_uninstall.test2'),
+
+    plugins_dir = path.join(spec, 'plugins'),
+    plugins_install_dir = path.join(project, 'cordova', 'plugins'),
+    plugins_install_dir2 = path.join(project2, 'cordova', 'plugins'),
+
+    plugins = {
+        'DummyPlugin' : path.join(plugins_dir, 'DummyPlugin'),
+        'A' : path.join(plugins_dir, 'dependencies', 'A'),
+        'C' : path.join(plugins_dir, 'dependencies', 'C')
+    },
+    promise,
+    dummy_id = 'com.phonegap.plugins.dummyplugin';
+
+function uninstallPromise(f) {
+    return f.then(function() { done = true; }, function(err) { done = err; });
+}
+
+describe('start', function() {
+
+    it('start', function() {
+        shell.rm('-rf', project);
+        shell.rm('-rf', project2);
+        shell.cp('-R', path.join(srcProject, '*'), project);
+        shell.cp('-R', path.join(srcProject, '*'), project2);
+
+        done = false;
+        promise = Q()
+        .then(
+            function(){ return install('android', project, plugins['DummyPlugin'], plugins_install_dir, { browserify: true }) }
+        ).then(
+            function(){ return install('android', project, plugins['A'], plugins_install_dir, { browserify: true }) }
+        ).then(
+            function(){ return install('android', project2, plugins['C'], plugins_install_dir2, { browserify: true }) }
+        ).then(
+            function(){ return install('android', project2, plugins['A'], plugins_install_dir2, { browserify: true }) }
+        ).then(
+            function(){ done = true; }
+        );
+        waitsFor(function() { return done; }, 'promise never resolved', 500);
+    });
+});
+
+describe('uninstallPlatform', function() {
+    var proc, prepare, prepareBrowserify, actions_push, add_to_queue, c_a, rm;
+    var fsWrite;
+
+    var plat_common = require('../src/plugman/platforms/common');
+
+    beforeEach(function() {
+        proc = spyOn(actions.prototype, 'process').andReturn(Q());
+        actions_push = spyOn(actions.prototype, 'push');
+        c_a = spyOn(actions.prototype, 'createAction');
+        prepare = spyOn(plugman, 'prepare');
+        prepareBrowserify = spyOn(plugman, 'prepareBrowserify');
+        fsWrite = spyOn(fs, 'writeFileSync').andReturn(true);
+        rm = spyOn(shell, 'rm').andReturn(true);
+        spyOn(shell, 'cp').andReturn(true);
+        add_to_queue = spyOn(config_changes, 'add_uninstalled_plugin_to_prepare_queue');
+        done = false;
+    });
+    describe('success', function() {
+        it('should call prepare after a successful uninstall', function() {
+            runs(function() {
+                uninstallPromise(uninstall.uninstallPlatform('android', project, dummy_id, plugins_install_dir, { browserify: true }));
+            });
+            waitsFor(function() { return done; }, 'promise never resolved', 200);
+            runs(function() {
+                expect(prepareBrowserify).toHaveBeenCalled();
+            });
+        });
+        it('should call the config-changes module\'s add_uninstalled_plugin_to_prepare_queue method after processing an install', function() {
+            runs(function() {
+                uninstallPromise(uninstall.uninstallPlatform('android', project, dummy_id, plugins_install_dir, { browserify: true }));
+            });
+            waitsFor(function() { return done; }, 'promise never resolved', 200);
+            runs(function() {
+                expect(add_to_queue).toHaveBeenCalledWith(plugins_install_dir, dummy_id, 'android', true);
+            });
+        });
+        it('should queue up actions as appropriate for that plugin and call process on the action stack', function() {
+            runs(function() {
+                uninstallPromise(uninstall.uninstallPlatform('android', project, dummy_id, plugins_install_dir, { browserify: true }));
+            });
+            waitsFor(function() { return done; }, 'promise never resolved', 200);
+            runs(function() {
+                expect(actions_push.calls.length).toEqual(5);
+                expect(proc).toHaveBeenCalled();
+            });
+        });
+
+        describe('with dependencies', function() {
+            var emit;
+            beforeEach(function() {
+                emit = spyOn(events, 'emit');
+            });
+            it('should uninstall "dangling" dependencies', function() {
+                runs(function() {
+                    uninstallPromise(uninstall.uninstallPlatform('android', project, 'A', plugins_install_dir, { browserify: true }));
+                });
+                waitsFor(function() { return done; }, 'promise never resolved', 200);
+                runs(function() {
+                    expect(emit).toHaveBeenCalledWith('log', 'Uninstalling 2 dependent plugins.');
+                });
+            });
+        });
+    });
+
+    describe('failure', function() {
+        it('should throw if platform is unrecognized', function() {
+            runs(function() {
+                uninstallPromise( uninstall.uninstallPlatform('atari', project, 'SomePlugin', plugins_install_dir, { browserify: true }) );
+            });
+            waitsFor(function() { return done; }, 'promise never resolved', 200);
+            runs(function() {
+                expect(''+done).toContain('atari not supported.');
+            });
+        });
+        it('should throw if plugin is missing', function() {
+            runs(function() {
+                uninstallPromise( uninstall.uninstallPlatform('android', project, 'SomePluginThatDoesntExist', plugins_install_dir, { browserify: true }) );
+            });
+            waitsFor(function() { return done; }, 'promise never resolved', 200);
+            runs(function() {
+                expect(''+done).toContain('Plugin "SomePluginThatDoesntExist" not found. Already uninstalled?');
+            });
+        });
+    });
+});
+
+describe('uninstallPlugin', function() {
+    var rm, fsWrite, rmstack = [], emit, prepareBrowserify;
+
+    beforeEach(function() {
+        fsWrite = spyOn(fs, 'writeFileSync').andReturn(true);
+        rm = spyOn(shell, 'rm').andCallFake(function(f,p) { rmstack.push(p); return true});
+        rmstack = [];
+        emit = spyOn(events, 'emit');
+        done = false;
+    });
+    describe('with dependencies', function() {
+
+        it('should delete all dependent plugins', function() {
+            runs(function() {
+                uninstallPromise( uninstall.uninstallPlugin('A', plugins_install_dir, {browserify: true}) );
+            });
+            waitsFor(function() { return done; }, 'promise never resolved', 200);
+            runs(function() {
+                var del = common.spy.getDeleted(emit);
+
+                expect(del).toEqual([
+                    'Deleted "C"',
+                    'Deleted "D"',
+                    'Deleted "A"'
+                ]);
+            });
+        });
+
+        it("should fail if plugin is a required dependency", function() {
+            runs(function() {
+                uninstallPromise( uninstall.uninstallPlugin('C', plugins_install_dir, {browserify: true}) );
+            });
+            waitsFor(function() { return done; }, 'promise never resolved', 200);
+            runs(function() {
+                expect(done.message).toBe('"C" is required by (A) and cannot be removed (hint: use -f or --force)');
+            });
+        });
+
+        it("allow forcefully removing a plugin", function() {
+            runs(function() {
+                uninstallPromise( uninstall.uninstallPlugin('C', plugins_install_dir, {browserify: true, force: true}) );
+            });
+            waitsFor(function() { return done; }, 'promise never resolved', 200);
+            runs(function() {
+                expect(done).toBe(true);
+                var del = common.spy.getDeleted(emit);
+                expect(del).toEqual(['Deleted "C"']);
+            });
+        });
+
+        it("never remove top level plugins if they are a dependency", function() {
+            runs(function() {
+                uninstallPromise( uninstall.uninstallPlugin('A', plugins_install_dir2, {browserify: true}) );
+            });
+            waitsFor(function() { return done; }, 'promise never resolved', 200);
+            runs(function() {
+                var del = common.spy.getDeleted(emit);
+
+                expect(del).toEqual([
+                    'Deleted "D"',
+                    'Deleted "A"'
+                ]);
+            });
+        });
+    });
+});
+
+describe('uninstall', function() {
+    var fsWrite, rm, add_to_queue;
+
+    beforeEach(function() {
+        fsWrite = spyOn(fs, 'writeFileSync').andReturn(true);
+        rm = spyOn(shell, 'rm').andReturn(true);
+        add_to_queue = spyOn(config_changes, 'add_uninstalled_plugin_to_prepare_queue');
+        done = false;
+    });
+    describe('success', function() {
+        it('should call the config-changes module\'s add_uninstalled_plugin_to_prepare_queue method after processing an install', function() {
+            runs(function() {
+                uninstallPromise( uninstall('android', project, plugins['DummyPlugin'], plugins_install_dir, { browserify: true }) );
+            });
+            waitsFor(function() { return done; }, 'promise never resolved', 200);
+            runs(function() {
+                expect(add_to_queue).toHaveBeenCalledWith(plugins_install_dir, dummy_id, 'android', true);
+            });
+        });
+    });
+
+    describe('failure', function() {
+        it('should throw if platform is unrecognized', function() {
+            runs(function() {
+                uninstallPromise(uninstall('atari', project, 'SomePlugin', plugins_install_dir, { browserify: true }));
+            });
+            waitsFor(function() { return done; }, 'promise never resolved', 200);
+            runs(function() {
+                expect(''+done).toContain('atari not supported.');
+            });
+        });
+        it('should throw if plugin is missing', function() {
+            runs(function() {
+                uninstallPromise(uninstall('android', project, 'SomePluginThatDoesntExist', plugins_install_dir, { browserify: true }));
+            });
+            waitsFor(function() { return done; }, 'promise never resolved', 200);
+            runs(function() {
+                expect(''+done).toContain('Plugin "SomePluginThatDoesntExist" not found. Already uninstalled?');
+            });
+        });
+    });
+});
+
+describe('end', function() {
+
+    it('end', function() {
+        done = false;
+
+        promise.then(
+            function(){
+                return uninstall('android', project, plugins['DummyPlugin'], plugins_install_dir, { browserify: true })
+            }
+        ).then(
+            function(){
+                // Fails... A depends on
+                return uninstall('android', project, plugins['C'], plugins_install_dir, { browserify: true })
+            }
+        ).fail(
+            function(err) {
+                expect(err.message).toBe("The plugin 'C' is required by (A), skipping uninstallation.");
+            }
+        ).then(
+            function(){
+                // dependencies on C,D ... should this only work with --recursive? prompt user..?
+                return uninstall('android', project, plugins['A'], plugins_install_dir, { browserify: true })
+            }
+        ).fin(function(err){
+            if(err)
+                plugman.emit('error', err);
+
+            shell.rm('-rf', project);
+            shell.rm('-rf', project2);
+            done = true;
+        });
+
+        waitsFor(function() { return done; }, 'promise never resolved', 500);
+    });
+});
+

http://git-wip-us.apache.org/repos/asf/cordova-lib/blob/c2dd20cd/cordova-lib/src/cordova/plugin.js
----------------------------------------------------------------------
diff --git a/cordova-lib/src/cordova/plugin.js b/cordova-lib/src/cordova/plugin.js
index 8a792b9..5d4562f 100644
--- a/cordova-lib/src/cordova/plugin.js
+++ b/cordova-lib/src/cordova/plugin.js
@@ -124,6 +124,7 @@ module.exports = function plugin(command, targets, opts) {
                                 var platformRoot = path.join(projectRoot, 'platforms', platform),
                                     options = {
                                         cli_variables: opts.cli_variables || {},
+                                        browserify: opts.browserify || false,
                                         searchpath: searchPath,
                                         noregistry: opts.noregistry,
                                     },

http://git-wip-us.apache.org/repos/asf/cordova-lib/blob/c2dd20cd/cordova-lib/src/plugman/install.js
----------------------------------------------------------------------
diff --git a/cordova-lib/src/plugman/install.js b/cordova-lib/src/plugman/install.js
index a13c1e2..83834b4 100644
--- a/cordova-lib/src/plugman/install.js
+++ b/cordova-lib/src/plugman/install.js
@@ -569,7 +569,11 @@ function handleInstall(actions, pluginInfo, platform, project_dir, plugins_dir,
         // queue up the plugin so prepare knows what to do.
         config_changes.add_installed_plugin_to_prepare_queue(plugins_dir, pluginInfo.id, platform, filtered_variables, options.is_top_level);
         // call prepare after a successful install
-        plugman.prepare(project_dir, platform, plugins_dir, options.www_dir, options.is_top_level);
+        if (options.browserify) {
+            plugman.prepareBrowserify(project_dir, platform, plugins_dir, options.www_dir, options.is_top_level);
+        } else {
+            plugman.prepare(project_dir, platform, plugins_dir, options.www_dir, options.is_top_level);
+        }
 
         events.emit('verbose', 'Install complete for ' + pluginInfo.id + ' on ' + platform + '.');
         // WIN!

http://git-wip-us.apache.org/repos/asf/cordova-lib/blob/c2dd20cd/cordova-lib/src/plugman/plugman.js
----------------------------------------------------------------------
diff --git a/cordova-lib/src/plugman/plugman.js b/cordova-lib/src/plugman/plugman.js
index 6184503..5598628 100644
--- a/cordova-lib/src/plugman/plugman.js
+++ b/cordova-lib/src/plugman/plugman.js
@@ -70,6 +70,7 @@ addProperty(plugman, 'install', './install', true);
 addProperty(plugman, 'uninstall', './uninstall', true);
 addProperty(plugman, 'fetch', './fetch', true);
 addProperty(plugman, 'prepare', './prepare');
+addProperty(plugman, 'prepareBrowserify', './prepare-browserify');
 addProperty(plugman, 'config', './config', true);
 addProperty(plugman, 'owner', './owner', true);
 addProperty(plugman, 'adduser', './adduser', true);

http://git-wip-us.apache.org/repos/asf/cordova-lib/blob/c2dd20cd/cordova-lib/src/plugman/prepare-browserify.js
----------------------------------------------------------------------
diff --git a/cordova-lib/src/plugman/prepare-browserify.js b/cordova-lib/src/plugman/prepare-browserify.js
index 1a1cdf9..ed2bdf9 100644
--- a/cordova-lib/src/plugman/prepare-browserify.js
+++ b/cordova-lib/src/plugman/prepare-browserify.js
@@ -27,7 +27,7 @@ var platform_modules   = require('./platforms'),
     config_changes     = require('./util/config-changes'),
     xml_helpers        = require('../util/xml-helpers'),
     wp8                = require('./platforms/wp8'),
-    windows8           = require('./platforms/windows8'),
+    windows            = require('./platforms/windows'),
     common             = require('./platforms/common'),
     fs                 = require('fs'),
     shell              = require('shelljs'),

http://git-wip-us.apache.org/repos/asf/cordova-lib/blob/c2dd20cd/cordova-lib/src/plugman/uninstall.js
----------------------------------------------------------------------
diff --git a/cordova-lib/src/plugman/uninstall.js b/cordova-lib/src/plugman/uninstall.js
index c145da2..06bcb85 100644
--- a/cordova-lib/src/plugman/uninstall.js
+++ b/cordova-lib/src/plugman/uninstall.js
@@ -238,12 +238,12 @@ function runUninstallPlatform(actions, platform, project_dir, plugin_dir, plugin
     }
 
     return promise.then(function() {
-        return handleUninstall(actions, platform, plugin_id, plugin_et, project_dir, options.www_dir, plugins_dir, plugin_dir, options.is_top_level);
+        return handleUninstall(actions, platform, plugin_id, plugin_et, project_dir, options.www_dir, plugins_dir, plugin_dir, options.is_top_level, options);
     });
 }
 
 // Returns a promise.
-function handleUninstall(actions, platform, plugin_id, plugin_et, project_dir, www_dir, plugins_dir, plugin_dir, is_top_level) {
+function handleUninstall(actions, platform, plugin_id, plugin_et, project_dir, www_dir, plugins_dir, plugin_dir, is_top_level, options) {
     var platform_modules = require('./platforms');
     var handler = platform_modules[platform];
     var platformTag = plugin_et.find('./platform[@name="'+platform+'"]');
@@ -316,6 +316,10 @@ function handleUninstall(actions, platform, plugin_id, plugin_et, project_dir, w
         // queue up the plugin so prepare can remove the config changes
         config_changes.add_uninstalled_plugin_to_prepare_queue(plugins_dir, plugin_id, platform, is_top_level);
         // call prepare after a successful uninstall
-        plugman.prepare(project_dir, platform, plugins_dir, www_dir, is_top_level);
+        if (options.browserify) {
+            plugman.prepareBrowserify(project_dir, platform, plugins_dir, www_dir, is_top_level);
+        } else {
+            plugman.prepare(project_dir, platform, plugins_dir, www_dir, is_top_level);
+        }
     });
 }