Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Using Node.js addons in Electron's renderer with Webpack

I have the following renderer:

import SerialPort from "serialport";

new SerialPort("/dev/tty-usbserial1", { baudRate: 57600 });

It's built by Webpack, with the following config (trimmed for brevity):

const config = {
  entry: { renderer: ["./src/renderer"] }
  output: {
    path: `${__dirname}/dist`,
    filename: "[name].js",
  },
  target: "electron-renderer",
  node: false, // Disables __dirname mocking and such
};

It's served by a development server, along with an index.html, and is loaded by the main process as a web page (this is needed for hot module replacement during development).

The main process is built by Webpack and emitted to dist too. A Webpack plugin also generates the following dist/package.json:

{
  "name": "my-app",
  "main": "main.js"
}

When I run electron dist, the renderer process crashes with the following error:

Uncaught TypeError: Path must be a string. Received undefined
    at assertPath (path.js:28)
    at dirname (path.js:1364)
    at Function.getRoot (bindings.js?dfc1:151)
    at bindings (bindings.js?dfc1:60)
    at eval (linux.js?d488:2)
    at Object../node_modules/serialport/lib/bindings/linux.js (renderer.js:12686)
    at __webpack_require__ (renderer.js:712)
    at fn (renderer.js:95)
    at eval (auto-detect.js?3cc7:16)
    at Object../node_modules/serialport/lib/bindings/auto-detect.js (renderer.js:12638)

How do I fix this?

like image 730
Alec Mev Avatar asked May 26 '18 22:05

Alec Mev


People also ask

Should I use Webpack with Electron?

There is no reason to use Webpack in Electron, check out electron-compile to use Babel and LESS in Electron. What if your webapp is using webpack and you don't want to rewrite everything? What if your are using vue. js components to structure your code?

Can you use node modules in Electron?

The node-pre-gyp tool provides a way to deploy native Node modules with prebuilt binaries, and many popular modules are using it. Sometimes those modules work fine under Electron, but when there are no Electron-specific binaries available, you'll need to build from source.

Can you use Webpack for node?

Webpack provides a Node. js API which can be used directly in Node. js runtime.

How do I add a Webpack to a node project?

Installing WebpackUse npm init command to create package. json file in the project folder to get started with Nodejs project. Then we can install Webpack as development dependency with npm i --save-dev webpack webpack-cli .


2 Answers

Problem

The first problem is that node-bindings, which node-serialport relies on to resolve the path to its Node.js addon, simply doesn't work in Electron. There's an open issue for this, and I don't think the associated PR is even a complete fix, since I've done some debugging, and it appears that fileName remains undefined throughout the whole getFileName.

The second problem: even if it somehow found a serialport.node somewhere, it wouldn't work after packaging the application for distribution, since the addon itself isn't in the dist directory, and Webpack can't just bundle it together with the main JS file.

One could attempt to solve this with node-loader, given a correctly working node-bindings, but that wouldn't help either, since node-bindings uses elaborate heuristics, which Webpack simply can't extrapolate from, when trying to understand what files could be required by its require. The only safe thing Webpack could do is include the whole project, "just in case", and that's a certain no-go, obviously, so node-loader just doesn't copy anything.

So, we need to replace node-bindings and copy serialport.node manually.

Solution

First, we must grab the addon and put it in dist. This needs to be done in main's Webpack build, since the renderer is served as web page, potentially from an in-memory file system (so the *.node file may not be emitted to disk, and Electron will never see it). Here's how:

import CopyWebpackPlugin from "copy-webpack-plugin";

const config = {
  // ...
  plugins: [
    new CopyWebpackPlugin([
      "node_modules/serialport/build/Release/serialport.node",
    ]),
  ],
  // ...
};

Hardcoded, unfortunately, but easy to fix if something changes.

Second, we must substitute node-bindings with our own shim, src/bindings.js:

module.exports = x =>
  __non_webpack_require__(
    `${require("electron").remote.app.getAppPath()}/${x}`
  );

__non_webpack_require__ is self-explanatory (yes, plain require won't work, without some trickery, as it's handled by Webpack), and the require("electron").remote.app.getAppPath() is necessary because __dirname doesn't actually resolve to what one would expect - an absolute path to dist - but rather to some directory buried deep inside Electron.

And here's how the replacement is done, in renderer's Webpack config:

import { NormalModuleReplacementPlugin } from "webpack";

const config = {
  // ...
  plugins: [
    new NormalModuleReplacementPlugin(
      /^bindings$/,
      `${__dirname}/src/bindings`
    ),
  ],
  // ...
};

And that's it! Once the above is done, and index.html + renderer.js are being served by some server (or whatever your approach is), and the dist looks something like this:

dist/
  main.js
  package.json
  serialport.node

electron dist should "just work".

Alternatives

Could potentially get away with adding node-serialport as a dependency to the generated dist/package.json and just npm installing it in there, and marking serialport as an external in Webpack, but that feels even dirtier (package version mismatches, etc.).

Another way is to just declare everything as externals, and have electron-packager just copy the whole production part of node_modules to dist for you, but that's a whole lot of megabytes for basically nothing.

like image 121
Alec Mev Avatar answered Oct 13 '22 01:10

Alec Mev


I wanna thank @Alec Mev for practically the only answer on the entire internet that truly works and has real knowledge inside. I've spent days troubleshooting issues with webpack and native modules for electron until I tried Alec's answer and it perfectly works.

Just a small addition, that if the native code is being used within the main process and not the renderer (most common case when app security is stronger), the shim needs to be adjusted as following:

module.exports = x =>
  __non_webpack_require__(
    `${require('electron').app.getAppPath()}/${x}`
  );
like image 20
Abdessattar Avatar answered Oct 13 '22 00:10

Abdessattar