--- layout: post title: "Integrate native Node.js modules into an Electron app (1/2)" date: 2018-02-22 21:12 comments: true categories: [javascript] cover: /images/cover/avatar.png keywords: description: --- ### tl;dr - when you probably really need Electron - how to integrate Webpack with Electron - develop (and test) browser and Electron app - Electron vs. system-wide node.js ### OMG, another Electron freak! Yeah yeah, I hear you saying *Why don't you learn Swift/C#/C++/…*, *Electron is so much memory intensive* etc. I know you're there, [Electron](https://electronjs.org/) haters. Well, in my opinion it's super convenient to develop web application and have a possibility to run it inside Electron (with a great advantage—access to underlying node.js). Personally, it's been thrilling to communicate with node.js from Electron (I'm kind of passionate developer). Sure, you can feel no Electron app is native, but that's what trade-off is about. # When you may need Electron A simple rule: if your project has a node.js dependency (meaning, the dependency is working only in node.js environment, not in a browser), you need Electron. What Electron basically does is running your Javascript in Chromium with node.js environment. Imagine you can do your usual front-end stuff and also you can control for example serial port (or USB port) peripherals in the same fashion, in the same project, even in the same file. # Goal I'll show you how to communicate with serialport device from your Electron app. First, we'll create a project and install dependencies. Then [Webpack](https://github.com/webpack/webpack) needs to be configured. After this step we can finally run the app. The demo project will be fully functional in Electron and will gracefully degrade in a browser. <!-- more --> # Prerequisites and project structure Install [node.js](https://nodejs.org) with npm and [Electron](https://electronjs.org/). MacOS and Linux is OK, Windows should be supported too. ``` ├── app.js ├── app.test.js ├── build │ └── ... ├── index.electron.js ├── index.html ├── node_modules │ └── ... ├── package.json ├── release-builds │ └── ... └── webpack.config.js ``` Actual app code is inside `app.js`; we will write some tests in `app.test.js`; built bundle inside `build` directory; Electron main file is `index.electron.js`; our webpage index file is `index.html`; `package.json` contains list of dependencies and scripts; into `release-builds` we will pack whole Electron ready for distribution (in the next article); and finally `webpack.config.js` serves as Webpack config file. Paste this into `package.json` and run `npm install`: {% codeblock package.json lang:json %} { "name": "electron-tutorial", "main": "index.electron.js", "scripts": { "build": "webpack", "electron": "electron .", "test": "jest" }, "dependencies": { "electron": "^1.8.2", "electron-rebuild": "^1.7.3", "jest": "^22.4.0", "serialport": "^6.0.5", "webpack": "^3.11.0" } } {% endcodeblock %} # Let's start with Webpack Paste this into `webpack.config.js`: {% codeblock webpack.config.js lang:javascript %} const webpack = require('webpack'); const path = require('path'); const platform = process.env.PLATFORM; module.exports = { entry: { app: path.join(__dirname, 'app.js') }, output: { path: path.join(__dirname, 'build'), filename: '[name].js', publicPath: 'build/', }, plugins: [ new webpack.DefinePlugin({ 'process.env.IS_ELECTRON': JSON.stringify(platform === 'electron'), }) ], target: platform === 'electron' ? 'electron-renderer' : 'web', externals: { bindings: 'require("bindings")' // fixes warnings during build } }; {% endcodeblock %} You probably already know Webpack. What's critical in configuration below is `target` option. You need to set `target` to `electron-renderer` if you're building code for Electron. Long story short—this setting will enable node.js and browser environment in your bundle. Also notice `process.env.IS_ELECTRON` definition. This way we can easily execute parts of javascript in Electron only. # Develop `app.js`, `index.html` and `index.electron.js` code looks like this: {% codeblock app.js lang:javascript %} if (process.env.IS_ELECTRON) { const serialport = require('serialport'); // will show connected serialport devices if any serialport.list().then(list => console.log(list)); alert('Electron detected'); } else { alert('Browser detected'); } {% endcodeblock %} Notice the `if` clause; the code inside the clause will be executed only if the bundle was built for Electron environment. {% codeblock index.html lang:html %}{% raw %} <!doctype html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Node.js native modules</title> <script src="build/app.js"></script> </head> <body> Running. </body> </html> {% endraw %}{% endcodeblock %} {% codeblock index.electron.js lang:javascript %} const { app, BrowserWindow } = require('electron'); let mainWindow; function createWindow() { mainWindow = new BrowserWindow(); mainWindow.loadURL(`file:///${__dirname}/index.html`); mainWindow.webContents.openDevTools(); mainWindow.on('closed', () => { mainWindow = null; }); } app.on('ready', createWindow); app.on('window-all-closed', () => { app.quit(); }); app.on('activate', () => { if (mainWindow === null) { createWindow(); } }); {% endcodeblock %} There's nothing special in `index.html`. `index.electron.js` is (almost) a vanilla Electron configuration. All right, run Webpack with `npm run build` and open `index.html` in your browser (e.g. `file:///Users/joedoe/electron/index.html`). You should see something like on the screenshot below. {% imgcap /images/electron1.png Browser screenshot %} Now run Webpack in Electron mode: `PLATFORM=electron npm run build`. Open the `index.html` file with Electron: `npm run electron`. {% imgcap /images/electron3.png Cannot find native module %} Most native node.js modules are loaded via [node-bindings](https://github.com/TooTallNate/node-bindings) library. The library tries to find native module in `build` directory. We need to copy `serialport.node` to `build/`. If you already have a prebuilt module in `node_modules/serialport/build/Release/serialport.node` just copy it: `cp node_modules/serialport/build/Release/serialport.node build/`. If the module is missing build it with `./node_modules/.bin/electron-rebuild -e node_modules/electron` and copy to `build/` directory. Try Electron again with previous npm command. Now you're actually scanned your serial ports thanks to node.js native module! {% imgcap /images/electron2.png Native module in action %} # Living with two node.js environments There's a chance you'll eventually see an error message `The module ... was compiled against a different Node.js version using NODE_MODULE_VERSION X. This version of Node.js requires NODE_MODULE_VERSION Y.` In that case just run the `./node_modules/.bin/electron-rebuild -e node_modules/electron` command. The reason why it might happen is Electron node.js usually slightly differs from the system-wide node.js version. In other words, Electron environment doesn't depend on system-wide node.js. My strategy is to keep native modules built against Electron in `build/` directory and (if needed, because of tests e.g.) modules built against system-wide node.js in `node_modules` (use `npm rebuild` command). # Testing Let's write a simple test which actually `require()` serialport dependency: ```js test('i can require serialport', () => { const serialport = require('serialport'); expect(serialport).toBeDefined(); }); ``` This will load native module in `node_modules/serialport/build/Release/serialport.node` and the test will pass. Run tests with `npm run test`. # What's next We can develop the javascript app (although very primitive) for both browser and Electron. In the next article we'll pack Electron environment with javascript bundle into a ready-for-distribution executable package.