You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@cordova.apache.org by er...@apache.org on 2020/12/26 06:31:35 UTC

[cordova-electron] branch master updated: breaking: add plugin support (#175)

This is an automated email from the ASF dual-hosted git repository.

erisu pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/cordova-electron.git


The following commit(s) were added to refs/heads/master by this push:
     new 1fa798e  breaking: add plugin support (#175)
1fa798e is described below

commit 1fa798e4c370a25c9267a2e2919a7b2afbdb9daf
Author: エリス <er...@users.noreply.github.com>
AuthorDate: Sat Dec 26 15:31:25 2020 +0900

    breaking: add plugin support (#175)
    
    * feat: add main icp handler for plugin communication
    * feat: pass plugin dir path to uninstall step for framework
    * feat: support framework un/install w/ package parser update
    * feat: do not overwrite platform installed node_modules and package.json
    * feat: update exec to support electron with browser fallback
    * feat: add render to main connection layer with preloader
    * feat: update cordova.js with exec changes
    * chore: various fixes & lint correction
    * test: updated use cases
    * test: plugin support coverage w/ cleanup & fix
    * fix: plugin uninstall & emit msg w/ coverage
    * chore: lowered successful delinking loglevel to verbose
---
 .eslintignore                                      |   1 +
 bin/templates/platform_www/cdv-electron-main.js    |  19 +-
 bin/templates/platform_www/cdv-electron-preload.js |  16 ++
 cordova-js-src/exec.js                             | 110 +++++----
 cordova-lib/cordova.js                             | 112 +++++----
 lib/Api.js                                         |   2 +-
 lib/PackageJsonParser.js                           |  74 ++----
 lib/handler.js                                     | 113 ++++++++-
 lib/parser.js                                      |   5 +-
 lib/prepare.js                                     |   4 +-
 .../test-app-with-electron-plugin/config.xml       |  18 ++
 .../test-app-with-electron-plugin/package.json     |  22 ++
 .../platforms/electron/config.xml                  |  18 ++
 .../platforms/electron/cordova/Api.js              |  24 ++
 .../platforms/electron/cordova/defaults.xml        |  21 ++
 .../platforms/electron/cordova/version             |  23 ++
 .../platforms/electron/cordova/version.bat         |  26 ++
 .../platforms/electron/electron.json               |  14 ++
 .../platforms/electron/platform_www/config.xml     |  22 ++
 .../platforms/electron/www/config.xml              |  18 ++
 .../platforms/electron/www/cordova_plugins.js      |   8 +
 .../platforms/electron/www/package.json            |  15 ++
 .../plugins/cordova-plugin-device/package.json     |  46 ++++
 .../plugins/cordova-plugin-device/plugin.xml       |  40 +++
 .../cordova-plugin-device/src/electron/index.js    |  22 ++
 .../src/electron/package.json                      |  20 ++
 .../plugins/cordova-plugin-device/www/device.js    |  85 +++++++
 .../test-app-with-electron-plugin/www/index.html   |  34 +++
 tests/spec/unit/lib/PackageJsonParser.spec.js      | 118 ++-------
 tests/spec/unit/lib/handler.spec.js                | 270 ++++++++++++++++++++-
 30 files changed, 1049 insertions(+), 271 deletions(-)

diff --git a/.eslintignore b/.eslintignore
index 170e18d..4c5b690 100644
--- a/.eslintignore
+++ b/.eslintignore
@@ -1,2 +1,3 @@
 cordova-lib/cordova.js
 bin/templates/platform_www/cordova.js
+tests/spec/fixtures/test-app-with-electron-plugin/
\ No newline at end of file
diff --git a/bin/templates/platform_www/cdv-electron-main.js b/bin/templates/platform_www/cdv-electron-main.js
index c8d4968..32f9258 100644
--- a/bin/templates/platform_www/cdv-electron-main.js
+++ b/bin/templates/platform_www/cdv-electron-main.js
@@ -19,11 +19,13 @@
 
 const fs = require('fs');
 const path = require('path');
+const { cordova } = require('./package.json');
 // Module to control application life, browser window and tray.
 const {
     app,
     BrowserWindow,
-    protocol
+    protocol,
+    ipcMain
 } = require('electron');
 // Electron settings from .json file.
 const cdvElectronSettings = require('./cdv-electron-settings.json');
@@ -70,6 +72,9 @@ function createWindow () {
     }
 
     const browserWindowOpts = Object.assign({}, cdvElectronSettings.browserWindow, { icon: appIcon });
+    browserWindowOpts.webPreferences.preload = path.join(app.getAppPath(), 'cdv-electron-preload.js');
+    browserWindowOpts.webPreferences.contextIsolation = true;
+
     mainWindow = new BrowserWindow(browserWindowOpts);
 
     // Load a local HTML file or a remote URL.
@@ -141,5 +146,17 @@ app.on('activate', () => {
     }
 });
 
+ipcMain.handle('cdv-plugin-exec', async (_, serviceName, action, ...args) => {
+    if (cordova && cordova.services && cordova.services[serviceName]) {
+        const plugin = require(cordova.services[serviceName]);
+
+        return plugin[action]
+            ? plugin[action](args)
+            : Promise.reject(new Error(`The action "${action}" for the requested plugin service "${serviceName}" does not exist.`));
+    } else {
+        return Promise.reject(new Error(`The requested plugin service "${serviceName}" does not exist have native support.`));
+    }
+});
+
 // In this file you can include the rest of your app's specific main process
 // code. You can also put them in separate files and require them here.
diff --git a/bin/templates/platform_www/cdv-electron-preload.js b/bin/templates/platform_www/cdv-electron-preload.js
new file mode 100644
index 0000000..704085e
--- /dev/null
+++ b/bin/templates/platform_www/cdv-electron-preload.js
@@ -0,0 +1,16 @@
+const { contextBridge, ipcRenderer } = require('electron');
+const { cordova } = require('./package.json');
+
+contextBridge.exposeInMainWorld('_cdvElectronIpc', {
+    exec: (success, error, serviceName, action, args) => {
+        return ipcRenderer.invoke('cdv-plugin-exec', serviceName, action, args)
+            .then(
+                success,
+                error
+            );
+    },
+
+    hasService: (serviceName) => cordova &&
+    cordova.services &&
+    cordova.services[serviceName]
+});
diff --git a/cordova-js-src/exec.js b/cordova-js-src/exec.js
index 6d21761..1a91e6c 100644
--- a/cordova-js-src/exec.js
+++ b/cordova-js-src/exec.js
@@ -39,65 +39,71 @@ const execProxy = require('cordova/exec/proxy');
  * @param {String[]} [args]     Zero or more arguments to pass to the method
  */
 module.exports = function (success, fail, service, action, args) {
-    var proxy = execProxy.get(service, action);
+    if (window._cdvElectronIpc.hasService(service)) {
+        // Electron based plugin support
+        window._cdvElectronIpc.exec(success, fail, service, action, args);
+    } else {
+        // Fall back for browser based plugin support...
+        const proxy = execProxy.get(service, action);
 
-    args = args || [];
+        args = args || [];
 
-    if (proxy) {
-        var callbackId = service + cordova.callbackId++;
+        if (proxy) {
+            var callbackId = service + cordova.callbackId++;
 
-        if (typeof success === 'function' || typeof fail === 'function') {
-            cordova.callbacks[callbackId] = { success: success, fail: fail };
-        }
-        try {
-            // callbackOptions param represents additional optional parameters command could pass back, like keepCallback or
-            // custom callbackId, for example {callbackId: id, keepCallback: true, status: cordova.callbackStatus.JSON_EXCEPTION }
-            var onSuccess = function (result, callbackOptions) {
-                callbackOptions = callbackOptions || {};
-                var callbackStatus;
-                // covering both undefined and null.
-                // strict null comparison was causing callbackStatus to be undefined
-                // and then no callback was called because of the check in cordova.callbackFromNative
-                // see CB-8996 Mobilespec app hang on windows
-                if (callbackOptions.status !== undefined && callbackOptions.status !== null) {
-                    callbackStatus = callbackOptions.status;
-                } else {
-                    callbackStatus = cordova.callbackStatus.OK;
-                }
-                cordova.callbackSuccess(callbackOptions.callbackId || callbackId,
-                    {
+            if (typeof success === 'function' || typeof fail === 'function') {
+                cordova.callbacks[callbackId] = { success: success, fail: fail };
+            }
+            try {
+                // callbackOptions param represents additional optional parameters command could pass back, like keepCallback or
+                // custom callbackId, for example {callbackId: id, keepCallback: true, status: cordova.callbackStatus.JSON_EXCEPTION }
+                var onSuccess = function (result, callbackOptions) {
+                    callbackOptions = callbackOptions || {};
+                    var callbackStatus;
+                    // covering both undefined and null.
+                    // strict null comparison was causing callbackStatus to be undefined
+                    // and then no callback was called because of the check in cordova.callbackFromNative
+                    // see CB-8996 Mobilespec app hang on windows
+                    if (callbackOptions.status !== undefined && callbackOptions.status !== null) {
+                        callbackStatus = callbackOptions.status;
+                    } else {
+                        callbackStatus = cordova.callbackStatus.OK;
+                    }
+                    cordova.callbackSuccess(callbackOptions.callbackId || callbackId,
+                        {
+                            status: callbackStatus,
+                            message: result,
+                            keepCallback: callbackOptions.keepCallback || false
+                        });
+                };
+                var onError = function (err, callbackOptions) {
+                    callbackOptions = callbackOptions || {};
+                    var callbackStatus;
+                    // covering both undefined and null.
+                    // strict null comparison was causing callbackStatus to be undefined
+                    // and then no callback was called because of the check in cordova.callbackFromNative
+                    // note: status can be 0
+                    if (callbackOptions.status !== undefined && callbackOptions.status !== null) {
+                        callbackStatus = callbackOptions.status;
+                    } else {
+                        callbackStatus = cordova.callbackStatus.OK;
+                    }
+                    cordova.callbackError(callbackOptions.callbackId || callbackId, {
                         status: callbackStatus,
-                        message: result,
+                        message: err,
                         keepCallback: callbackOptions.keepCallback || false
                     });
-            };
-            var onError = function (err, callbackOptions) {
-                callbackOptions = callbackOptions || {};
-                var callbackStatus;
-                // covering both undefined and null.
-                // strict null comparison was causing callbackStatus to be undefined
-                // and then no callback was called because of the check in cordova.callbackFromNative
-                // note: status can be 0
-                if (callbackOptions.status !== undefined && callbackOptions.status !== null) {
-                    callbackStatus = callbackOptions.status;
-                } else {
-                    callbackStatus = cordova.callbackStatus.OK;
-                }
-                cordova.callbackError(callbackOptions.callbackId || callbackId, {
-                    status: callbackStatus,
-                    message: err,
-                    keepCallback: callbackOptions.keepCallback || false
-                });
-            };
-            proxy(onSuccess, onError, args);
-        } catch (e) {
-            console.log('Exception calling native with command :: ' + service + ' :: ' + action + ' ::exception=' + e);
-        }
-    } else {
-        console.log('Error: exec proxy not found for :: ' + service + ' :: ' + action);
+                };
+                proxy(onSuccess, onError, args);
+            } catch (e) {
+                console.log('Exception calling native with command :: ' + service + ' :: ' + action + ' ::exception=' + e);
+            }
+        } else {
+            console.log('Error: exec proxy not found for :: ' + service + ' :: ' + action);
 
-        if (typeof fail === 'function') {
-            fail('Missing Command Error');
+            if (typeof fail === 'function') {
+                fail('Missing Command Error');
+            }
         }
     }
 };
diff --git a/cordova-lib/cordova.js b/cordova-lib/cordova.js
index 4a3a4c1..12474a5 100644
--- a/cordova-lib/cordova.js
+++ b/cordova-lib/cordova.js
@@ -1,5 +1,5 @@
 // Platform: electron
-// 538a985db128858c0a0eb4dd40fb9c8e5433fc94
+// d66ee158a971168ea9619a1bc854055582ba0e84
 /*
  Licensed to the Apache Software Foundation (ASF) under one
  or more contributor license agreements.  See the NOTICE file
@@ -962,65 +962,71 @@ const execProxy = require('cordova/exec/proxy');
  * @param {String[]} [args]     Zero or more arguments to pass to the method
  */
 module.exports = function (success, fail, service, action, args) {
-    var proxy = execProxy.get(service, action);
+    if (window._cdvElectronIpc.hasService(service)) {
+        // Electron based plugin support
+        window._cdvElectronIpc.exec(success, fail, service, action, args);
+    } else {
+        // Fall back for browser based plugin support...
+        const proxy = execProxy.get(service, action);
 
-    args = args || [];
+        args = args || [];
 
-    if (proxy) {
-        var callbackId = service + cordova.callbackId++;
+        if (proxy) {
+            var callbackId = service + cordova.callbackId++;
 
-        if (typeof success === 'function' || typeof fail === 'function') {
-            cordova.callbacks[callbackId] = { success: success, fail: fail };
-        }
-        try {
-            // callbackOptions param represents additional optional parameters command could pass back, like keepCallback or
-            // custom callbackId, for example {callbackId: id, keepCallback: true, status: cordova.callbackStatus.JSON_EXCEPTION }
-            var onSuccess = function (result, callbackOptions) {
-                callbackOptions = callbackOptions || {};
-                var callbackStatus;
-                // covering both undefined and null.
-                // strict null comparison was causing callbackStatus to be undefined
-                // and then no callback was called because of the check in cordova.callbackFromNative
-                // see CB-8996 Mobilespec app hang on windows
-                if (callbackOptions.status !== undefined && callbackOptions.status !== null) {
-                    callbackStatus = callbackOptions.status;
-                } else {
-                    callbackStatus = cordova.callbackStatus.OK;
-                }
-                cordova.callbackSuccess(callbackOptions.callbackId || callbackId,
-                    {
+            if (typeof success === 'function' || typeof fail === 'function') {
+                cordova.callbacks[callbackId] = { success: success, fail: fail };
+            }
+            try {
+                // callbackOptions param represents additional optional parameters command could pass back, like keepCallback or
+                // custom callbackId, for example {callbackId: id, keepCallback: true, status: cordova.callbackStatus.JSON_EXCEPTION }
+                var onSuccess = function (result, callbackOptions) {
+                    callbackOptions = callbackOptions || {};
+                    var callbackStatus;
+                    // covering both undefined and null.
+                    // strict null comparison was causing callbackStatus to be undefined
+                    // and then no callback was called because of the check in cordova.callbackFromNative
+                    // see CB-8996 Mobilespec app hang on windows
+                    if (callbackOptions.status !== undefined && callbackOptions.status !== null) {
+                        callbackStatus = callbackOptions.status;
+                    } else {
+                        callbackStatus = cordova.callbackStatus.OK;
+                    }
+                    cordova.callbackSuccess(callbackOptions.callbackId || callbackId,
+                        {
+                            status: callbackStatus,
+                            message: result,
+                            keepCallback: callbackOptions.keepCallback || false
+                        });
+                };
+                var onError = function (err, callbackOptions) {
+                    callbackOptions = callbackOptions || {};
+                    var callbackStatus;
+                    // covering both undefined and null.
+                    // strict null comparison was causing callbackStatus to be undefined
+                    // and then no callback was called because of the check in cordova.callbackFromNative
+                    // note: status can be 0
+                    if (callbackOptions.status !== undefined && callbackOptions.status !== null) {
+                        callbackStatus = callbackOptions.status;
+                    } else {
+                        callbackStatus = cordova.callbackStatus.OK;
+                    }
+                    cordova.callbackError(callbackOptions.callbackId || callbackId, {
                         status: callbackStatus,
-                        message: result,
+                        message: err,
                         keepCallback: callbackOptions.keepCallback || false
                     });
-            };
-            var onError = function (err, callbackOptions) {
-                callbackOptions = callbackOptions || {};
-                var callbackStatus;
-                // covering both undefined and null.
-                // strict null comparison was causing callbackStatus to be undefined
-                // and then no callback was called because of the check in cordova.callbackFromNative
-                // note: status can be 0
-                if (callbackOptions.status !== undefined && callbackOptions.status !== null) {
-                    callbackStatus = callbackOptions.status;
-                } else {
-                    callbackStatus = cordova.callbackStatus.OK;
-                }
-                cordova.callbackError(callbackOptions.callbackId || callbackId, {
-                    status: callbackStatus,
-                    message: err,
-                    keepCallback: callbackOptions.keepCallback || false
-                });
-            };
-            proxy(onSuccess, onError, args);
-        } catch (e) {
-            console.log('Exception calling native with command :: ' + service + ' :: ' + action + ' ::exception=' + e);
-        }
-    } else {
-        console.log('Error: exec proxy not found for :: ' + service + ' :: ' + action);
+                };
+                proxy(onSuccess, onError, args);
+            } catch (e) {
+                console.log('Exception calling native with command :: ' + service + ' :: ' + action + ' ::exception=' + e);
+            }
+        } else {
+            console.log('Error: exec proxy not found for :: ' + service + ' :: ' + action);
 
-        if (typeof fail === 'function') {
-            fail('Missing Command Error');
+            if (typeof fail === 'function') {
+                fail('Missing Command Error');
+            }
         }
     }
 };
diff --git a/lib/Api.js b/lib/Api.js
index 57e0d23..62e0a34 100644
--- a/lib/Api.js
+++ b/lib/Api.js
@@ -232,7 +232,7 @@ class Api {
                 if (['asset', 'js-module'].indexOf(type) > -1) {
                     return installer.uninstall(item, wwwDest, plugin_id);
                 } else {
-                    return installer.uninstall(item, this.root, plugin_id, options, project);
+                    return installer.uninstall(item, plugin_dir, this.root, plugin_id, options, project);
                 }
             }
         };
diff --git a/lib/PackageJsonParser.js b/lib/PackageJsonParser.js
index 9bebeff..ec866bb 100644
--- a/lib/PackageJsonParser.js
+++ b/lib/PackageJsonParser.js
@@ -19,67 +19,29 @@
 
 const fs = require('fs-extra');
 const path = require('path');
-const { events } = require('cordova-common');
 const { getPackageJson } = require('./util');
 
 class PackageJsonParser {
     constructor (wwwDir, projectRootDir) {
+        // Electron App Package
         this.path = path.join(wwwDir, 'package.json');
+        fs.ensureFileSync(this.path);
+        this.package = JSON.parse(fs.readFileSync(this.path, 'utf8') || '{}');
+
+        // Force settings that are not allowed to change.
+        this.package.main = 'cdv-electron-main.js';
+
         this.www = wwwDir;
         this.projectRootDir = projectRootDir;
-        this.package = {
-            main: 'cdv-electron-main.js'
-        };
     }
 
-    configure (config, projectPackageJson) {
+    configure (config) {
         if (config) {
             this.package.name = config.packageName() || 'io.cordova.hellocordova';
             this.package.displayName = config.name() || 'HelloCordova';
             this.package.version = config.version() || '1.0.0';
             this.package.description = config.description() || 'A sample Apache Cordova application that responds to the deviceready event.';
 
-            if (projectPackageJson.dependencies) {
-                const cordovaDependencies = [];
-                const droppedPackages = [];
-
-                for (const [npmPackage, npmPackageValue] of Object.entries(projectPackageJson.dependencies)) {
-                    if (/^cordova(?!-plugin)-/.test(npmPackage)) {
-                        cordovaDependencies.push(npmPackage);
-                    }
-
-                    // Format FilePath Based Dependencies
-                    if (npmPackageValue.startsWith('file:..')) {
-                        const relativePath = npmPackageValue.split('file:')[1];
-                        const absolutePath = path.resolve(this.projectRootDir, relativePath);
-
-                        if (fs.pathExistsSync(absolutePath)) {
-                            projectPackageJson.dependencies[npmPackage] = `file:${absolutePath}`;
-                        } else {
-                            droppedPackages.push(npmPackage);
-                        }
-                    }
-                }
-
-                // If Cordova dependencies are detected in "dependencies" of "package.json" warn for potential app package bloating
-                if (cordovaDependencies.length) {
-                    events.emit('warn', '[Cordova Electron] The built package size may be larger than necessary. Please run with --verbose for more details.');
-
-                    events.emit('verbose', `[Cordova Electron] The following Cordova package(s) were detected as "dependencies" in the projects "package.json" file.
-\t- ${cordovaDependencies.join('\n\t- ')}
-
-It is recommended that all Cordova packages are defined as "devDependencies" in the "package.json" file. It is safe to move them manually.
-Packages defined as a dependency will be bundled with the application and can increase the built application's size.
-`);
-                }
-
-                if (droppedPackages.length) {
-                    events.emit('warn', `[Cordova Electron] The following local npm dependencies could not be located and will not be deployed to the Electron app:\n\t${droppedPackages.join('\n\t- ')}`);
-                }
-
-                this.package.dependencies = projectPackageJson.dependencies;
-            }
-
             this.configureHomepage(config);
             this.configureLicense(config);
 
@@ -96,16 +58,30 @@ Packages defined as a dependency will be bundled with the application and can in
         return this;
     }
 
+    static _orderObject (obj) {
+        const ordered = {};
+        Object.keys(obj).sort().forEach(key => {
+            ordered[key] = obj[key];
+        });
+        return ordered;
+    }
+
     enableDevTools (enable = false) {
-        if (enable) {
-            const pkgJson = getPackageJson();
-            const devToolsDependency = 'electron-devtools-installer';
+        const pkgJson = getPackageJson();
+        const devToolsDependency = 'electron-devtools-installer';
 
+        if (enable) {
             if (!this.package.dependencies) {
                 this.package.dependencies = {};
             }
 
             this.package.dependencies[devToolsDependency] = pkgJson.dependencies[devToolsDependency];
+            this.package.dependencies = PackageJsonParser._orderObject(this.package.dependencies);
+        } else if (
+            this.package.dependencies &&
+            this.package.dependencies[devToolsDependency]
+        ) {
+            delete this.package.dependencies[devToolsDependency];
         }
 
         return this;
diff --git a/lib/handler.js b/lib/handler.js
index a1275c6..ffa9e80 100644
--- a/lib/handler.js
+++ b/lib/handler.js
@@ -19,14 +19,16 @@
 
 const path = require('path');
 const fs = require('fs-extra');
+const execa = require('execa');
 const { events } = require('cordova-common');
+const { _orderObject } = require('./PackageJsonParser');
+const { deepMerge } = require('./util');
 
 module.exports = {
     www_dir: (project_dir) => path.join(project_dir, 'www'),
     package_name: (project_dir) => {
         // this method should the id from root config.xml => <widget id=xxx
         // return common.package_name(project_dir, this.www_dir(project_dir));
-        // console.log('package_name called with ' + project_dir);
         let pkgName = 'io.cordova.hellocordova';
         const widget_id_regex = /(?:<widget\s+id=['"])(\S+)(?:['"])/;
         const configPath = path.join(project_dir, 'config.xml');
@@ -79,7 +81,7 @@ module.exports = {
             // common.copyFile(plugin_dir, obj.src, project_dir, dest);
             events.emit('verbose', 'source-file.install is not supported for electron');
         },
-        uninstall: (obj, project_dir, plugin_id, options) => {
+        uninstall: (obj, plugin_dir, project_dir, plugin_id, options) => {
             // var dest = path.join(obj.targetDir, path.basename(obj.src));
             // common.removeFile(project_dir, dest);
             events.emit('verbose', 'source-file.uninstall is not supported for electron');
@@ -89,7 +91,7 @@ module.exports = {
         install: (obj, plugin_dir, project_dir, plugin_id, options) => {
             events.emit('verbose', 'header-file.install is not supported for electron');
         },
-        uninstall: (obj, project_dir, plugin_id, options) => {
+        uninstall: (obj, plugin_dir, project_dir, plugin_id, options) => {
             events.emit('verbose', 'header-file.uninstall is not supported for electron');
         }
     },
@@ -97,23 +99,118 @@ module.exports = {
         install: (obj, plugin_dir, project_dir, plugin_id, options) => {
             events.emit('verbose', 'resource-file.install is not supported for electron');
         },
-        uninstall: (obj, project_dir, plugin_id, options) => {
+        uninstall: (obj, plugin_dir, project_dir, plugin_id, options) => {
             events.emit('verbose', 'resource-file.uninstall is not supported for electron');
         }
     },
     framework: {
         install: (obj, plugin_dir, project_dir, plugin_id, options) => {
-            events.emit('verbose', 'framework.install is not supported for electron');
+            const electronPluginSrc = path.resolve(plugin_dir, obj.src);
+
+            if (!fs.existsSync(electronPluginSrc)) {
+                events.emit(
+                    'warn',
+                    '[Cordova Electron] The defined "framework" source path does not exist and can not be installed.'
+                );
+                return;
+            }
+
+            const wwwDir = path.join(project_dir, 'www');
+
+            execa('npm', ['install', electronPluginSrc], {
+                cwd: wwwDir
+            });
+
+            const appPackageFile = path.join(wwwDir, 'package.json');
+            let appPackage = JSON.parse(fs.readFileSync(appPackageFile, 'utf8'));
+            const pluginPackage = JSON.parse(fs.readFileSync(path.join(electronPluginSrc, 'package.json'), 'utf8'));
+
+            if (!pluginPackage.cordova || !pluginPackage.cordova.serviceName) {
+                return;
+            }
+
+            const serviceName = pluginPackage.cordova.serviceName;
+
+            if (
+                appPackage.cordova &&
+                appPackage.cordova.services &&
+                appPackage.cordova.services[serviceName]
+            ) {
+                events.emit(
+                    'warn',
+                    `[Cordova Electron] The service name "${serviceName}" is already taken by "${appPackage.cordova.services[serviceName]}" and can not be redeclared.`
+                );
+                return;
+            }
+
+            const appendingData = {
+                cordova: {
+                    services: {
+                        [serviceName]: pluginPackage.name
+                    }
+                }
+            };
+
+            appPackage = deepMerge(appPackage, appendingData);
+
+            appPackage.cordova.services = _orderObject(appPackage.cordova.services);
+            fs.writeFileSync(
+                appPackageFile,
+                JSON.stringify(appPackage, null, 2),
+                'utf8'
+            );
         },
-        uninstall: (obj, project_dir, plugin_id, options) => {
-            events.emit('verbose', 'framework.uninstall is not supported for electron');
+        uninstall: (obj, plugin_dir, project_dir, plugin_id, options) => {
+            const electronPluginPackageFile = path.resolve(plugin_dir, obj.src, 'package.json');
+            const electronPluginPackage = JSON.parse(
+                fs.readFileSync(electronPluginPackageFile, 'utf8')
+            );
+
+            const electronPluginName = electronPluginPackage.name;
+            const wwwDir = path.join(project_dir, 'www');
+
+            console.log(electronPluginPackageFile);
+            console.log(electronPluginName);
+
+            execa('npm', ['uninstall', electronPluginName], {
+                cwd: wwwDir
+            });
+
+            const appPackageFile = path.join(wwwDir, 'package.json');
+            const appPackage = JSON.parse(fs.readFileSync(appPackageFile, 'utf8'));
+
+            if (
+                appPackage &&
+                appPackage.cordova &&
+                appPackage.cordova.services
+            ) {
+                let hasUpdatedPackage = false;
+                Object.keys(appPackage.cordova.services).forEach(serviceName => {
+                    if (appPackage.cordova.services[serviceName] === electronPluginName) {
+                        delete appPackage.cordova.services[serviceName];
+                        hasUpdatedPackage = true;
+                        events.emit(
+                            'verbose',
+                            `[Cordova Electron] The service name "${serviceName}" was delinked.`
+                        );
+                    }
+                });
+
+                if (hasUpdatedPackage) {
+                    fs.writeFileSync(
+                        appPackageFile,
+                        JSON.stringify(appPackage, null, 2),
+                        'utf8'
+                    );
+                }
+            }
         }
     },
     'lib-file': {
         install: (obj, plugin_dir, project_dir, plugin_id, options) => {
             events.emit('verbose', 'lib-file.install is not supported for electron');
         },
-        uninstall: (obj, project_dir, plugin_id, options) => {
+        uninstall: (obj, plugin_dir, project_dir, plugin_id, options) => {
             events.emit('verbose', 'lib-file.uninstall is not supported for electron');
         }
     },
diff --git a/lib/parser.js b/lib/parser.js
index 4ee2ba2..8905699 100644
--- a/lib/parser.js
+++ b/lib/parser.js
@@ -65,7 +65,10 @@ class Parser {
         // targetDir points to electron/www
         const targetDir = path.relative(cordovaProject.root, my_www);
         events.emit('verbose', `Merging and updating files from [${sourceDirs.join(', ')}] to ${targetDir}`);
-        FileUpdater.mergeAndUpdateDir(sourceDirs, targetDir, { rootDir: cordovaProject.root }, logFileOp);
+        FileUpdater.mergeAndUpdateDir(sourceDirs, targetDir, {
+            rootDir: cordovaProject.root,
+            exclude: ['node_modules', 'package.json']
+        }, logFileOp);
     }
 
     config_xml () {
diff --git a/lib/prepare.js b/lib/prepare.js
index db46af0..3eb499c 100644
--- a/lib/prepare.js
+++ b/lib/prepare.js
@@ -72,10 +72,8 @@ module.exports.prepare = function (cordovaProject, options) {
             .write();
     }
 
-    const projectPackageJson = JSON.parse(fs.readFileSync(path.join(cordovaProject.root, 'package.json'), 'utf8'));
-
     (new PackageJsonParser(this.locations.www, cordovaProject.root))
-        .configure(this.config, projectPackageJson)
+        .configure(this.config)
         .enableDevTools(options && options.options && !options.options.release)
         .write();
 
diff --git a/tests/spec/fixtures/test-app-with-electron-plugin/config.xml b/tests/spec/fixtures/test-app-with-electron-plugin/config.xml
new file mode 100644
index 0000000..ab176fd
--- /dev/null
+++ b/tests/spec/fixtures/test-app-with-electron-plugin/config.xml
@@ -0,0 +1,18 @@
+<?xml version='1.0' encoding='utf-8'?>
+<widget id="org.apache.cordovaTestApp" version="1.0.0" xmlns="http://www.w3.org/ns/widgets" xmlns:cdv="http://cordova.apache.org/ns/1.0">
+    <name>cordovaTestApp</name>
+    <description>
+        A sample Apache Cordova application that responds to the deviceready event.
+    </description>
+    <author email="dev@cordova.apache.org" href="http://cordova.io">
+        Apache Cordova Team
+    </author>
+    <content src="index.html" />
+    <access origin="*" />
+    <allow-intent href="http://*/*" />
+    <allow-intent href="https://*/*" />
+    <allow-intent href="tel:*" />
+    <allow-intent href="sms:*" />
+    <allow-intent href="mailto:*" />
+    <allow-intent href="geo:*" />
+</widget>
diff --git a/tests/spec/fixtures/test-app-with-electron-plugin/package.json b/tests/spec/fixtures/test-app-with-electron-plugin/package.json
new file mode 100644
index 0000000..db9b021
--- /dev/null
+++ b/tests/spec/fixtures/test-app-with-electron-plugin/package.json
@@ -0,0 +1,22 @@
+{
+  "name": "org.apache.cordovaTestApp",
+  "displayName": "cordovaTestApp",
+  "version": "1.0.0",
+  "description": "A sample Apache Cordova application that responds to the deviceready event.",
+  "main": "index.js",
+  "scripts": {
+    "test": "echo \"Error: no test specified\" && exit 1"
+  },
+  "keywords": [
+    "ecosystem:cordova"
+  ],
+  "author": "Apache Cordova Team",
+  "license": "Apache-2.0",
+  "devDependencies": {},
+  "cordova": {
+    "plugins": {},
+    "platforms": [
+      "electron"
+    ]
+  }
+}
\ No newline at end of file
diff --git a/tests/spec/fixtures/test-app-with-electron-plugin/platforms/electron/config.xml b/tests/spec/fixtures/test-app-with-electron-plugin/platforms/electron/config.xml
new file mode 100644
index 0000000..ab176fd
--- /dev/null
+++ b/tests/spec/fixtures/test-app-with-electron-plugin/platforms/electron/config.xml
@@ -0,0 +1,18 @@
+<?xml version='1.0' encoding='utf-8'?>
+<widget id="org.apache.cordovaTestApp" version="1.0.0" xmlns="http://www.w3.org/ns/widgets" xmlns:cdv="http://cordova.apache.org/ns/1.0">
+    <name>cordovaTestApp</name>
+    <description>
+        A sample Apache Cordova application that responds to the deviceready event.
+    </description>
+    <author email="dev@cordova.apache.org" href="http://cordova.io">
+        Apache Cordova Team
+    </author>
+    <content src="index.html" />
+    <access origin="*" />
+    <allow-intent href="http://*/*" />
+    <allow-intent href="https://*/*" />
+    <allow-intent href="tel:*" />
+    <allow-intent href="sms:*" />
+    <allow-intent href="mailto:*" />
+    <allow-intent href="geo:*" />
+</widget>
diff --git a/tests/spec/fixtures/test-app-with-electron-plugin/platforms/electron/cordova/Api.js b/tests/spec/fixtures/test-app-with-electron-plugin/platforms/electron/cordova/Api.js
new file mode 100644
index 0000000..175ff23
--- /dev/null
+++ b/tests/spec/fixtures/test-app-with-electron-plugin/platforms/electron/cordova/Api.js
@@ -0,0 +1,24 @@
+/*
+    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.
+*/
+
+try {
+    module.exports = require('cordova-electron');
+} catch (error) {
+    module.exports = require('../../../lib/Api');
+}
diff --git a/tests/spec/fixtures/test-app-with-electron-plugin/platforms/electron/cordova/defaults.xml b/tests/spec/fixtures/test-app-with-electron-plugin/platforms/electron/cordova/defaults.xml
new file mode 100644
index 0000000..2dee32a
--- /dev/null
+++ b/tests/spec/fixtures/test-app-with-electron-plugin/platforms/electron/cordova/defaults.xml
@@ -0,0 +1,21 @@
+<?xml version='1.0' encoding='utf-8'?>
+<!--
+    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.
+-->
+<widget id="io.cordova.hellocordova" version="0.0.1" xmlns="http://www.w3.org/ns/widgets" xmlns:cdv="http://cordova.apache.org/ns/1.0">
+</widget>
diff --git a/tests/spec/fixtures/test-app-with-electron-plugin/platforms/electron/cordova/version b/tests/spec/fixtures/test-app-with-electron-plugin/platforms/electron/cordova/version
new file mode 100755
index 0000000..bbb52c6
--- /dev/null
+++ b/tests/spec/fixtures/test-app-with-electron-plugin/platforms/electron/cordova/version
@@ -0,0 +1,23 @@
+#!/usr/bin/env node
+
+/*
+    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.
+*/
+
+const Api = require('./Api');
+console.log(Api.version());
diff --git a/tests/spec/fixtures/test-app-with-electron-plugin/platforms/electron/cordova/version.bat b/tests/spec/fixtures/test-app-with-electron-plugin/platforms/electron/cordova/version.bat
new file mode 100644
index 0000000..2e69f1c
--- /dev/null
+++ b/tests/spec/fixtures/test-app-with-electron-plugin/platforms/electron/cordova/version.bat
@@ -0,0 +1,26 @@
+:: 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.
+
+@ECHO OFF
+SET script_path="%~dp0version"
+IF EXIST %script_path% (
+        node %script_path% %*
+) ELSE (
+    ECHO.
+    ECHO ERROR: Could not find 'version' script in 'cordova' folder, aborting...>&2
+    EXIT /B 1
+)
diff --git a/tests/spec/fixtures/test-app-with-electron-plugin/platforms/electron/electron.json b/tests/spec/fixtures/test-app-with-electron-plugin/platforms/electron/electron.json
new file mode 100644
index 0000000..16b00cc
--- /dev/null
+++ b/tests/spec/fixtures/test-app-with-electron-plugin/platforms/electron/electron.json
@@ -0,0 +1,14 @@
+{
+  "prepare_queue": {
+    "installed": [],
+    "uninstalled": []
+  },
+  "config_munge": {
+    "files": {}
+  },
+  "installed_plugins": {},
+  "dependent_plugins": {},
+  "modules": [],
+  "plugin_metadata": {
+  }
+}
diff --git a/tests/spec/fixtures/test-app-with-electron-plugin/platforms/electron/platform_www/config.xml b/tests/spec/fixtures/test-app-with-electron-plugin/platforms/electron/platform_www/config.xml
new file mode 100644
index 0000000..63d4646
--- /dev/null
+++ b/tests/spec/fixtures/test-app-with-electron-plugin/platforms/electron/platform_www/config.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+    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.
+-->
+<widget xmlns="http://www.w3.org/ns/widgets">
+
+</widget>
\ No newline at end of file
diff --git a/tests/spec/fixtures/test-app-with-electron-plugin/platforms/electron/www/config.xml b/tests/spec/fixtures/test-app-with-electron-plugin/platforms/electron/www/config.xml
new file mode 100644
index 0000000..ab176fd
--- /dev/null
+++ b/tests/spec/fixtures/test-app-with-electron-plugin/platforms/electron/www/config.xml
@@ -0,0 +1,18 @@
+<?xml version='1.0' encoding='utf-8'?>
+<widget id="org.apache.cordovaTestApp" version="1.0.0" xmlns="http://www.w3.org/ns/widgets" xmlns:cdv="http://cordova.apache.org/ns/1.0">
+    <name>cordovaTestApp</name>
+    <description>
+        A sample Apache Cordova application that responds to the deviceready event.
+    </description>
+    <author email="dev@cordova.apache.org" href="http://cordova.io">
+        Apache Cordova Team
+    </author>
+    <content src="index.html" />
+    <access origin="*" />
+    <allow-intent href="http://*/*" />
+    <allow-intent href="https://*/*" />
+    <allow-intent href="tel:*" />
+    <allow-intent href="sms:*" />
+    <allow-intent href="mailto:*" />
+    <allow-intent href="geo:*" />
+</widget>
diff --git a/tests/spec/fixtures/test-app-with-electron-plugin/platforms/electron/www/cordova_plugins.js b/tests/spec/fixtures/test-app-with-electron-plugin/platforms/electron/www/cordova_plugins.js
new file mode 100644
index 0000000..46ddcee
--- /dev/null
+++ b/tests/spec/fixtures/test-app-with-electron-plugin/platforms/electron/www/cordova_plugins.js
@@ -0,0 +1,8 @@
+cordova.define('cordova/plugin_list', function (require, exports, module) {
+            module.exports = [];
+
+            module.exports.metadata =
+            // TOP OF METADATA
+            {}
+            // BOTTOM OF METADATA
+        });
\ No newline at end of file
diff --git a/tests/spec/fixtures/test-app-with-electron-plugin/platforms/electron/www/package.json b/tests/spec/fixtures/test-app-with-electron-plugin/platforms/electron/www/package.json
new file mode 100644
index 0000000..2e6d6b1
--- /dev/null
+++ b/tests/spec/fixtures/test-app-with-electron-plugin/platforms/electron/www/package.json
@@ -0,0 +1,15 @@
+{
+  "main": "cdv-electron-main.js",
+  "name": "org.apache.cordovaTestApp",
+  "displayName": "test-app-with-electron-plugin",
+  "version": "1.0.0",
+  "description": "A sample Apache Cordova application that responds to the deviceready event.",
+  "homepage": "http://cordova.io",
+  "license": "Apache-2.0",
+  "author": {
+    "name": "Apache Cordova Team",
+    "email": "dev@cordova.apache.org"
+  },
+  "dependencies": {},
+  "cordova": {}
+}
\ No newline at end of file
diff --git a/tests/spec/fixtures/test-app-with-electron-plugin/plugins/cordova-plugin-device/package.json b/tests/spec/fixtures/test-app-with-electron-plugin/plugins/cordova-plugin-device/package.json
new file mode 100644
index 0000000..0b59744
--- /dev/null
+++ b/tests/spec/fixtures/test-app-with-electron-plugin/plugins/cordova-plugin-device/package.json
@@ -0,0 +1,46 @@
+{
+  "name": "cordova-plugin-device",
+  "version": "2.0.4-dev",
+  "description": "Cordova Device Plugin",
+  "types": "./types/index.d.ts",
+  "cordova": {
+    "id": "cordova-plugin-device",
+    "platforms": [
+      "android",
+      "ios",
+      "windows",
+      "browser",
+      "osx"
+    ]
+  },
+  "repository": "github:apache/cordova-plugin-device",
+  "bugs": "https://github.com/apache/cordova-plugin-device/issues",
+  "keywords": [
+    "cordova",
+    "device",
+    "ecosystem:cordova",
+    "cordova-android",
+    "cordova-electron",
+    "cordova-ios",
+    "cordova-windows",
+    "cordova-browser",
+    "cordova-osx"
+  ],
+  "scripts": {
+    "test": "npm run lint",
+    "lint": "eslint ."
+  },
+  "author": "Apache Software Foundation",
+  "license": "Apache-2.0",
+  "engines": {
+    "cordovaDependencies": {
+      "3.0.0": {
+        "cordova": ">100",
+        "cordova-electron": ">=3.0.0"
+      }
+    }
+  },
+  "devDependencies": {
+    "@cordova/eslint-config": "^3.0.0"
+  }
+}
diff --git a/tests/spec/fixtures/test-app-with-electron-plugin/plugins/cordova-plugin-device/plugin.xml b/tests/spec/fixtures/test-app-with-electron-plugin/plugins/cordova-plugin-device/plugin.xml
new file mode 100644
index 0000000..2dcfb6f
--- /dev/null
+++ b/tests/spec/fixtures/test-app-with-electron-plugin/plugins/cordova-plugin-device/plugin.xml
@@ -0,0 +1,40 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  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.
+-->
+
+<plugin xmlns="http://apache.org/cordova/ns/plugins/1.0"
+    id="cordova-plugin-device"
+    version="2.0.4-dev">
+    <name>Device</name>
+    <description>Cordova Mock Device Plugin for Electron Testing</description>
+    <license>Apache 2.0</license>
+    <keywords>cordova,device</keywords>
+
+    <engines>
+        <engine name="cordova-electron" version=">=3.0.0" />
+    </engines>
+
+    <js-module src="www/device.js" name="device">
+        <clobbers target="device" />
+    </js-module>
+
+    <platform name="electron">
+        <framework src="src/electron" />
+    </platform>
+</plugin>
diff --git a/tests/spec/fixtures/test-app-with-electron-plugin/plugins/cordova-plugin-device/src/electron/index.js b/tests/spec/fixtures/test-app-with-electron-plugin/plugins/cordova-plugin-device/src/electron/index.js
new file mode 100644
index 0000000..4303399
--- /dev/null
+++ b/tests/spec/fixtures/test-app-with-electron-plugin/plugins/cordova-plugin-device/src/electron/index.js
@@ -0,0 +1,22 @@
+const { system, osInfo } = require('systeminformation');
+
+module.exports = {
+    getDeviceInfo: async () => {
+        try {
+            const { manufacturer, model, uuid } = await system();
+            const { platform, distro, codename, build } = await osInfo();
+
+            return {
+                manufacturer,
+                model,
+                platform: platform === 'darwin' ? codename : distro,
+                version: build,
+                uuid,
+                // cordova: ''
+                isVirtual: false
+            };
+        } catch (e) {
+            console.log(e);
+        }
+    }
+};
diff --git a/tests/spec/fixtures/test-app-with-electron-plugin/plugins/cordova-plugin-device/src/electron/package.json b/tests/spec/fixtures/test-app-with-electron-plugin/plugins/cordova-plugin-device/src/electron/package.json
new file mode 100644
index 0000000..d3b235f
--- /dev/null
+++ b/tests/spec/fixtures/test-app-with-electron-plugin/plugins/cordova-plugin-device/src/electron/package.json
@@ -0,0 +1,20 @@
+{
+  "name": "cordova-plugin-device-electron",
+  "version": "1.0.0",
+  "description": "Electron Native Supprot for Cordova Device Plugin",
+  "main": "index.js",
+  "keywords": [
+    "cordova",
+    "electron",
+    "device",
+    "native"
+  ],
+  "author": "Apache Software Foundation",
+  "license": "Apache-2.0",
+  "dependencies": {
+    "systeminformation": "^4.27.9"
+  },
+  "cordova": {
+    "serviceName": "Device"
+  }
+}
diff --git a/tests/spec/fixtures/test-app-with-electron-plugin/plugins/cordova-plugin-device/www/device.js b/tests/spec/fixtures/test-app-with-electron-plugin/plugins/cordova-plugin-device/www/device.js
new file mode 100644
index 0000000..8bb2aa6
--- /dev/null
+++ b/tests/spec/fixtures/test-app-with-electron-plugin/plugins/cordova-plugin-device/www/device.js
@@ -0,0 +1,85 @@
+/*
+ *
+ * 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 argscheck = require('cordova/argscheck');
+var channel = require('cordova/channel');
+var exec = require('cordova/exec');
+var cordova = require('cordova');
+
+channel.createSticky('onCordovaInfoReady');
+// Tell cordova channel to wait on the CordovaInfoReady event
+channel.waitForInitialization('onCordovaInfoReady');
+
+/**
+ * This represents the mobile device, and provides properties for inspecting the model, version, UUID of the
+ * phone, etc.
+ * @constructor
+ */
+function Device () {
+    this.available = false;
+    this.platform = null;
+    this.version = null;
+    this.uuid = null;
+    this.cordova = null;
+    this.model = null;
+    this.manufacturer = null;
+    this.isVirtual = null;
+    this.serial = null;
+
+    var me = this;
+
+    channel.onCordovaReady.subscribe(function () {
+        me.getInfo(
+            function (info) {
+                // ignoring info.cordova returning from native, we should use value from cordova.version defined in cordova.js
+                // TODO: CB-5105 native implementations should not return info.cordova
+                var buildLabel = cordova.version;
+                me.available = true;
+                me.platform = info.platform;
+                me.version = info.version;
+                me.uuid = info.uuid;
+                me.cordova = buildLabel;
+                me.model = info.model;
+                me.isVirtual = info.isVirtual;
+                me.manufacturer = info.manufacturer || 'unknown';
+                me.serial = info.serial || 'unknown';
+                channel.onCordovaInfoReady.fire();
+            },
+            function (e) {
+                me.available = false;
+                console.error('[ERROR] Error initializing cordova-plugin-device: ' + e);
+            }
+        );
+    });
+}
+
+/**
+ * Get device info
+ *
+ * @param {Function} successCallback The function to call when the heading data is available
+ * @param {Function} errorCallback The function to call when there is an error getting the heading data. (OPTIONAL)
+ */
+Device.prototype.getInfo = function (successCallback, errorCallback) {
+    argscheck.checkArgs('fF', 'Device.getInfo', arguments);
+    exec(successCallback, errorCallback, 'Device', 'getDeviceInfo', []);
+};
+
+module.exports = new Device();
diff --git a/tests/spec/fixtures/test-app-with-electron-plugin/www/index.html b/tests/spec/fixtures/test-app-with-electron-plugin/www/index.html
new file mode 100644
index 0000000..c144cf0
--- /dev/null
+++ b/tests/spec/fixtures/test-app-with-electron-plugin/www/index.html
@@ -0,0 +1,34 @@
+<!DOCTYPE html>
+<!--
+    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.
+-->
+<html>
+    <head>
+        <meta charset="utf-8">
+        <meta http-equiv="Content-Security-Policy" content="default-src 'self' data: 'unsafe-eval'; style-src 'self' 'unsafe-inline'; media-src *; img-src 'self' data: content:;">
+        <meta name="format-detection" content="telephone=no">
+        <meta name="msapplication-tap-highlight" content="no">
+        <meta name="viewport" content="initial-scale=1, width=device-width, viewport-fit=cover">
+        <meta name="color-scheme" content="light dark">
+        <title>Hello World</title>
+    </head>
+    <body>
+        <div class="app"></div>
+        <script src="cordova.js"></script>
+    </body>
+</html>
diff --git a/tests/spec/unit/lib/PackageJsonParser.spec.js b/tests/spec/unit/lib/PackageJsonParser.spec.js
index fb21b8f..8f02b4b 100644
--- a/tests/spec/unit/lib/PackageJsonParser.spec.js
+++ b/tests/spec/unit/lib/PackageJsonParser.spec.js
@@ -66,6 +66,10 @@ describe('PackageJsonParser class', () => {
         spyOn(events, 'emit');
     });
 
+    afterAll(() => {
+        fs.removeSync('mock');
+    });
+
     it('should have been constructed with initial values.', () => {
         expect(packageJsonParser).toBeDefined();
         expect(packageJsonParser.path).toEqual(path.join(locations.www, 'package.json'));
@@ -91,6 +95,18 @@ describe('PackageJsonParser class', () => {
         expect(packageJsonParser.package.dependencies).not.toBeDefined();
     });
 
+    it('should remove dev tools extension when enable argument = false.', () => {
+        packageJsonParser.package.dependencies = packageJsonParser.package.dependencies || {
+            'electron-devtools-installer': '1.0.0' // test
+        };
+        // Ensure mock was set, this is acting as if it was set before.
+        expect(packageJsonParser.package.dependencies['electron-devtools-installer']).toBeDefined();
+        // This should remove the mock
+        packageJsonParser.enableDevTools(false);
+        // the dependency should have been removed.
+        expect(packageJsonParser.package.dependencies['electron-devtools-installer']).not.toBeDefined();
+    });
+
     it('should not add dev tools extension when enable argument = undefined.', () => {
         packageJsonParser.enableDevTools();
         // the package object should be the same as it was initialized
@@ -121,10 +137,6 @@ describe('PackageJsonParser class', () => {
             displayName: 'HelloCordova',
             version: '1.0.0',
             description: 'A sample Apache Cordova application that responds to the deviceready event.',
-            dependencies: {
-                'cordova-electron': '^1.0.0',
-                'cordova-plugin-camera': '^1.0.0'
-            },
             homepage: 'https://cordova.io',
             license: 'Apache-2.0',
             author: 'Apache Cordova Team'
@@ -142,10 +154,6 @@ describe('PackageJsonParser class', () => {
             displayName: 'HelloWorld',
             version: '1.1.1',
             description: 'A sample Apache Cordova application.',
-            dependencies: {
-                'cordova-electron': '^1.0.0',
-                'cordova-plugin-camera': '^1.0.0'
-            },
             homepage: 'http://cordova.io',
             license: 'Apache 2.0 License',
             author: { name: 'Cordova Team', email: 'dev@cordova.com' }
@@ -154,30 +162,6 @@ describe('PackageJsonParser class', () => {
         expect(packageJsonParser.package).toEqual(packageJsonObj);
     });
 
-    it('should warn when any of the dependencies contain cordova-* is defined but not for cordova-plugin-*.', () => {
-        packageJsonParser.configure(cfg, defaultMockProjectPackageJson);
-
-        expect(events.emit).toHaveBeenCalledWith(
-            'warn',
-            jasmine.stringMatching(/^\[Cordova Electron\] The built package size/)
-        );
-
-        expect(events.emit).toHaveBeenCalledWith(
-            'verbose',
-            jasmine.stringMatching(/The following Cordova package\(s\) were detected/)
-        );
-
-        expect(events.emit).toHaveBeenCalledWith(
-            'verbose',
-            jasmine.stringMatching(/cordova-electron/)
-        );
-
-        expect(events.emit).not.toHaveBeenCalledWith(
-            'verbose',
-            jasmine.stringMatching(/cordova-plugin-camera/)
-        );
-    });
-
     it('should set default author when missing but author email is defined.', () => {
         packageJsonParser.configure(cfgNoAuthorCustomEmail, defaultMockProjectPackageJson);
         expect(packageJsonParser.package.author).toEqual({
@@ -186,76 +170,6 @@ describe('PackageJsonParser class', () => {
         });
     });
 
-    it('should not warn when any cordova-* packages are defined as devDependency.', () => {
-        // Fix defaultMockProjectPackageJson where cordova-* is devDependency
-        const mockProjectPackageJson = Object.assign({}, defaultMockProjectPackageJson);
-        mockProjectPackageJson.devDependencies = Object.assign({}, defaultMockProjectPackageJson.dependencies);
-        mockProjectPackageJson.dependencies = { foobar: '1.0.0' }; // setting a non "cordova-" dependency
-
-        packageJsonParser.configure(cfg, mockProjectPackageJson);
-
-        expect(events.emit).not.toHaveBeenCalled();
-    });
-
-    it('should skip configuring the Electron app\'s dependencies when the Cordova project\'s package.json dependencies are not set.', () => {
-        const mockProjectPackageJson = Object.assign({}, defaultMockProjectPackageJson);
-        mockProjectPackageJson.dependencies = {};
-
-        packageJsonParser.configure(cfg, mockProjectPackageJson);
-
-        expect(events.emit).not.toHaveBeenCalled();
-    });
-
-    it('should skip preparing npm packages that already contain absolute paths.', () => {
-        const mockProjectPackageJson = Object.assign({}, defaultMockProjectPackageJson);
-        mockProjectPackageJson.dependencies = { foobar: 'file:/tmp/foobar.tar.gz' };
-
-        packageJsonParser.configure(cfg, mockProjectPackageJson);
-
-        expect(events.emit).not.toHaveBeenCalled();
-    });
-
-    it('should convert npm packages that contain relative path to be absolute paths.', () => {
-        const mockProjectPackageJson = Object.assign({}, defaultMockProjectPackageJson);
-        mockProjectPackageJson.dependencies = { foobar: 'file:../tmp/foobar.tar.gz' };
-
-        spyOn(fs, 'pathExistsSync').and.returnValue(true);
-
-        packageJsonParser.configure(cfg, mockProjectPackageJson);
-
-        expect(events.emit).not.toHaveBeenCalled();
-    });
-
-    it('should warn that an npm packages will be dropped when the absolute path could not be found.', () => {
-        const mockProjectPackageJson = Object.assign({}, defaultMockProjectPackageJson);
-        mockProjectPackageJson.dependencies = { foobar: 'file:../tmp/foobar.tar.gz' };
-
-        spyOn(fs, 'pathExistsSync').and.returnValue(false);
-
-        packageJsonParser.configure(cfg, mockProjectPackageJson);
-
-        expect(events.emit).toHaveBeenCalledWith(
-            'warn',
-            jasmine.stringMatching(/^\[Cordova Electron\] The following local npm dependencies could not be located and will not be deployed/)
-        );
-
-        expect(events.emit).toHaveBeenCalledWith(
-            'warn',
-            jasmine.stringMatching(/foobar/)
-        );
-    });
-
-    it('should not set package dependencies when project dependencies is missing.', () => {
-        const mockProjectPackageJson = Object.assign({}, defaultMockProjectPackageJson);
-        mockProjectPackageJson.devDependencies = Object.assign({}, defaultMockProjectPackageJson.dependencies);
-        delete mockProjectPackageJson.dependencies;
-
-        packageJsonParser.configure(cfg, mockProjectPackageJson);
-
-        expect(events.emit).not.toHaveBeenCalled();
-        expect(packageJsonParser.package.dependencies).not.toBeDefined();
-    });
-
     it('should write something.', () => {
         spyOn(fs, 'writeFileSync').and.returnValue(true);
         packageJsonParser.write();
diff --git a/tests/spec/unit/lib/handler.spec.js b/tests/spec/unit/lib/handler.spec.js
index 8b08b7e..3ab405e 100644
--- a/tests/spec/unit/lib/handler.spec.js
+++ b/tests/spec/unit/lib/handler.spec.js
@@ -23,6 +23,9 @@ const { events } = require('cordova-common');
 const rewire = require('rewire');
 
 const rootDir = path.resolve(__dirname, '../../../..');
+const fixturesDir = path.join(rootDir, 'tests/spec/fixtures');
+const tmpDir = path.join(rootDir, 'temp');
+const testProjectDir = path.join(tmpDir, 'testapp');
 
 const handler = rewire(path.join(rootDir, 'lib/handler'));
 
@@ -170,6 +173,272 @@ describe('Handler export', () => {
         });
     });
 
+    describe('framework method', () => {
+        const frameworkInstallMockObject = {
+            itemType: 'framework',
+            type: undefined,
+            parent: undefined,
+            custom: false,
+            embed: false,
+            src: 'src/electron',
+            spec: undefined,
+            weak: false,
+            versions: undefined,
+            targetDir: undefined,
+            deviceTarget: undefined,
+            arch: undefined,
+            implementation: undefined
+        };
+
+        const frameworkInstallPluginId = 'cordova-plugin-device';
+        const frameworkInstallElectronPluginId = `${frameworkInstallPluginId}-electron`;
+        const frameworkInstallPluginDir = path.join(testProjectDir, `plugins/${frameworkInstallPluginId}`);
+        const frameworkInstallProjectDir = path.join(testProjectDir, 'platforms/electron');
+        const frameworkInstallProjectAppPackageFile = path.join(frameworkInstallProjectDir, 'www/package.json');
+        const frameworkInstallPluginPackageFile = path.join(frameworkInstallPluginDir, 'src/electron/package.json');
+
+        beforeEach(() => {
+            fs.ensureDirSync(tmpDir);
+            fs.copySync(path.resolve(fixturesDir, 'test-app-with-electron-plugin'), testProjectDir);
+
+            spyOn(events, 'emit');
+        });
+
+        afterEach(() => {
+            fs.removeSync(tmpDir);
+        });
+
+        describe('framework.install', () => {
+            it('should not install framework when the source path does not exist.', async () => {
+                const execaSpy = jasmine.createSpy('execa');
+                handler.__set__('execa', execaSpy);
+
+                spyOn(fs, 'existsSync').and.returnValue(false);
+
+                await handler.framework.install(
+                    frameworkInstallMockObject,
+                    frameworkInstallPluginDir,
+                    frameworkInstallProjectDir,
+                    frameworkInstallPluginId
+                );
+
+                expect(events.emit).toHaveBeenCalledWith(
+                    'warn',
+                    '[Cordova Electron] The defined "framework" source path does not exist and can not be installed.'
+                );
+                events.emit.calls.reset();
+            });
+
+            it('should update the electron app package when service is registered', async () => {
+                const execaSpy = jasmine.createSpy('execa');
+                handler.__set__('execa', execaSpy);
+
+                // Mock npm install by updating the App's package.json
+                const appPackage = JSON.parse(
+                    fs.readFileSync(frameworkInstallProjectAppPackageFile, 'utf8')
+                );
+
+                appPackage.dependencies[frameworkInstallElectronPluginId] = path.relative(
+                    frameworkInstallProjectAppPackageFile,
+                    path.join(frameworkInstallPluginDir, 'src/electron')
+                );
+
+                fs.writeFileSync(
+                    frameworkInstallProjectAppPackageFile,
+                    JSON.stringify(appPackage, null, 2),
+                    'utf8'
+                );
+
+                await handler.framework.install(
+                    frameworkInstallMockObject,
+                    frameworkInstallPluginDir,
+                    frameworkInstallProjectDir,
+                    frameworkInstallPluginId
+                );
+
+                // Validate that device plugin's service is registered.
+                const validateAppPackage = JSON.parse(fs.readFileSync(frameworkInstallProjectAppPackageFile, 'utf8'));
+                const test = validateAppPackage && validateAppPackage.cordova && validateAppPackage.cordova.services && validateAppPackage.cordova.services.Device;
+                expect(test).toBe(frameworkInstallElectronPluginId);
+            });
+
+            it('should not update the electron app package when there are no registered service', async () => {
+                const execaSpy = jasmine.createSpy('execa').and.returnValue({
+                    stdout: frameworkInstallElectronPluginId
+                });
+                handler.__set__('execa', execaSpy);
+
+                // Mock npm install by updating the App's package.json
+                const appPackage = JSON.parse(
+                    fs.readFileSync(frameworkInstallProjectAppPackageFile, 'utf8')
+                );
+
+                appPackage.dependencies[frameworkInstallElectronPluginId] = path.relative(
+                    frameworkInstallProjectAppPackageFile,
+                    path.join(frameworkInstallPluginDir, 'src/electron')
+                );
+
+                fs.writeFileSync(
+                    frameworkInstallProjectAppPackageFile,
+                    JSON.stringify(appPackage, null, 2),
+                    'utf8'
+                );
+
+                const pluginPackage = JSON.parse(fs.readFileSync(frameworkInstallPluginPackageFile, 'utf8'));
+                delete pluginPackage.cordova;
+
+                fs.writeFileSync(
+                    frameworkInstallPluginPackageFile,
+                    JSON.stringify(pluginPackage, null, 2),
+                    'utf8'
+                );
+
+                await handler.framework.install(
+                    frameworkInstallMockObject,
+                    frameworkInstallPluginDir,
+                    frameworkInstallProjectDir,
+                    frameworkInstallPluginId
+                );
+
+                // Validate that device plugin's service is registered.
+                const validateAppPackage = JSON.parse(fs.readFileSync(frameworkInstallProjectAppPackageFile, 'utf8'));
+                const test = validateAppPackage && validateAppPackage.cordova && validateAppPackage.cordova.services && validateAppPackage.cordova.services.Device;
+                expect(test).not.toBe(frameworkInstallElectronPluginId);
+            });
+
+            it('should warn when there are conflicting service name between more then one plugin', async () => {
+                const execaSpy = jasmine.createSpy('execa').and.returnValue({
+                    stdout: frameworkInstallElectronPluginId
+                });
+                handler.__set__('execa', execaSpy);
+
+                // Mock npm install by updating the App's package.json
+                const appPackage = JSON.parse(
+                    fs.readFileSync(frameworkInstallProjectAppPackageFile, 'utf8')
+                );
+
+                appPackage.dependencies[frameworkInstallElectronPluginId] = path.relative(
+                    frameworkInstallProjectAppPackageFile,
+                    path.join(frameworkInstallPluginDir, 'src/electron')
+                );
+
+                // fake some other device service already registered
+                appPackage.cordova = appPackage.cordova || {};
+                appPackage.cordova.services = appPackage.cordova.services || {
+                    Device: 'cordova-plugin-device-electron'
+                };
+
+                fs.writeFileSync(
+                    frameworkInstallProjectAppPackageFile,
+                    JSON.stringify(appPackage, null, 2),
+                    'utf8'
+                );
+
+                await handler.framework.install(
+                    frameworkInstallMockObject,
+                    frameworkInstallPluginDir,
+                    frameworkInstallProjectDir,
+                    frameworkInstallPluginId
+                );
+
+                expect(events.emit).toHaveBeenCalledWith(
+                    'warn',
+                    '[Cordova Electron] The service name "Device" is already taken by "cordova-plugin-device-electron" and can not be redeclared.'
+                );
+                events.emit.calls.reset();
+            });
+        });
+
+        describe('framework.uninstall', () => {
+            it('should delink service name if defined', async () => {
+                const execaSpy = jasmine.createSpy('execa');
+                handler.__set__('execa', execaSpy);
+
+                // Mock npm install by updating the App's package.json
+                const appPackage = JSON.parse(
+                    fs.readFileSync(frameworkInstallProjectAppPackageFile, 'utf8')
+                );
+
+                appPackage.dependencies[frameworkInstallElectronPluginId] = path.relative(
+                    frameworkInstallProjectAppPackageFile,
+                    path.join(frameworkInstallPluginDir, 'src/electron')
+                );
+
+                // fake some other device service already registered
+                appPackage.cordova = appPackage.cordova || {};
+                appPackage.cordova.services = appPackage.cordova.services || {
+                    Device: 'cordova-plugin-device-electron'
+                };
+
+                fs.writeFileSync(
+                    frameworkInstallProjectAppPackageFile,
+                    JSON.stringify(appPackage, null, 2),
+                    'utf8'
+                );
+
+                await handler.framework.uninstall(
+                    frameworkInstallMockObject,
+                    frameworkInstallPluginDir,
+                    frameworkInstallProjectDir
+                );
+
+                expect(events.emit).toHaveBeenCalledWith(
+                    'verbose',
+                    '[Cordova Electron] The service name "Device" was delinked.'
+                );
+                events.emit.calls.reset();
+            });
+
+            it('should not delink service name if defined by another plugin', async () => {
+                const execaSpy = jasmine.createSpy('execa');
+                handler.__set__('execa', execaSpy);
+
+                // Mock npm install by updating the App's package.json
+                const appPackage = JSON.parse(
+                    fs.readFileSync(frameworkInstallProjectAppPackageFile, 'utf8')
+                );
+
+                appPackage.dependencies[frameworkInstallElectronPluginId] = path.relative(
+                    frameworkInstallProjectAppPackageFile,
+                    path.join(frameworkInstallPluginDir, 'src/electron')
+                );
+
+                // fake some other device service already registered
+                appPackage.cordova = appPackage.cordova || {};
+                appPackage.cordova.services = appPackage.cordova.services || {
+                    Device: 'some-other-package'
+                };
+
+                fs.writeFileSync(
+                    frameworkInstallProjectAppPackageFile,
+                    JSON.stringify(appPackage, null, 2),
+                    'utf8'
+                );
+
+                await handler.framework.uninstall(
+                    frameworkInstallMockObject,
+                    frameworkInstallPluginDir,
+                    frameworkInstallProjectDir
+                );
+
+                expect(events.emit).not.toHaveBeenCalled();
+            });
+
+            it('should not delink service name when not defined', async () => {
+                const execaSpy = jasmine.createSpy('execa');
+                handler.__set__('execa', execaSpy);
+
+                await handler.framework.uninstall(
+                    frameworkInstallMockObject,
+                    frameworkInstallPluginDir,
+                    frameworkInstallProjectDir
+                );
+
+                expect(events.emit).not.toHaveBeenCalled();
+            });
+        });
+    });
+
     describe('Unsupported Handlers', () => {
         it('should emit that the install and uninstall methods are not supported for X types.', () => {
             spyOn(events, 'emit');
@@ -179,7 +448,6 @@ describe('Handler export', () => {
                 'source-file',
                 'header-file',
                 'resource-file',
-                'framework',
                 'lib-file'
             ].forEach(type => {
                 for (const method of ['install', 'uninstall']) {


---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@cordova.apache.org
For additional commands, e-mail: commits-help@cordova.apache.org