Home > Software design >  Electron app using Webpack runs binary node module in dev mode but not in production mode
Electron app using Webpack runs binary node module in dev mode but not in production mode

Time:12-01

I have a macOS electron app that is based on this electron-forge Webpack Typescript boilerplate, integrated with React, as documented here.

TL;DR

I'm able to spawn a binary node module in dev mode (yarn start) but not able to in production mode (yarn package).

A bit further

A binary node module that is being spawned but not imported is not being packed by webpack.

The Full Problem:

In my code, I use the NodeJS spawn module to run a child process in the background. This child process is an installed dependency node module (btw it's @loadmill/agent npm package, but the problem could be applied to any package that is being called by a binary file, instead of a js file).

spawn('loadmill-agent', ['start', '-t', token])

But I don't explicitly import this package into the code. (i.e there is no import '@loadmill/agent' line anywhere in the code)

It works well in development mode. When I run yarn start, the child process is spawned and I can communicate with it and all is well under the sun.

However, when I package the app and run the same line of code, I get an error.

Uncaught Exception:
Error: spawn loadmill-agent ENOENT
at Process.ChildProcess._handle.onexit (node:internal/child_process:282:19)
at one rrorNT (node:internal/child_process:477:16)
at processTicksAndRejections (node:internal/process/task_queues:83:21)

I searched a bit and found that I can debug spawned process errors in NodeJS like so:

  spawn('loadmill-agent', ['start', '-t', token], {
      env: { NODE_DEBUG: 'child_process', },
    }
  );

Now instead of the error popup dialog, I get the actual error output: /bin/sh: loadmill-agent: command not found Which means the command is either not installed, or not on the PATH, or not executable without a shell.

Furthermore, the @loadmill/agent node module was not even packed by webpack as a dependency. I know this because I don't see it in the dependencies of the packaged electron app contents/resources.

To recap:

  1. The loadmill-agent node module is not being packed by webpack.
  2. The spawned process outputs /bin/sh: loadmill-agent: command not found

My assumption of a solution:

  1. Get webpack to somehow package @loadmill/agent.
  2. figure out how to spawn @loadmill/agent with-the-right-path-to-binary-file. This issue can probably be resolved by configuring the PATH env var or by using the fix-path npm package.

CodePudding user response:

You can force the inclusion of files in the packaged app using the electron-forge/electron-packager option packagerConfig.extraResource.

There are alternatives to using extraResource if you want to get fancier, like manually scripting the copy of the files using one of Forge's hooks. Regardless of what method you use, you may need to include some logic in your app for where to find the binary depending on whether in development mode or packaged mode (e.g. using app.isPackaged and process.resourcesPath).

As to the larger question of why the binary is not being included in your app package, it must be a configuration error. Are you using .asar in your app package? Are you sure the loadmill-agent module is installed as a regular (non-dev) local dependency to your app? Meaning it's listed in your package.json under dependencies and located inside node_modules?

CodePudding user response:

To those who are interested, I solved my problem. Here is how:

Inspired by this answer to a similar question, I changed a few things in my code for it to work.

  1. I switched to using @loadmill/agent's programmatic api, instead of their cli script. I mean, I wrote a code that looks something like this:
const { start } = require('@loadmill/agent');
const stop = start({
  token: 'INSERT_TOKEN_HERE'
});

// Stop the agent at a later time

in a file I named loadmill-agent.ts.

  1. Instead of spawn, I used NodeJS's fork. fork requires a path to file, instead of a terminal command. So I gave fork the path to my new file. Something like this (in main process code):
const childProcess = fork('loadmill-agent', // args & options ...

This obviously still didn't work because of the relative problems in electron apps and in webpack bundled apps

The problem was that after bundling with webpack, I had only 1 index.js file and it was calling a non-existing loadmill-agent.js file in the fork statement above.

  1. To fix that, I added an entry just for the loadmill-agent.ts file, in webpack.main.config.js file:
module.exports = {
...
  entry: {
    index: './src/index.ts',
    'loadmill-agent': './src/loadmill-agent.ts',
  },
...
}

Now the output of webpack had 2 files, index.js and loadmill-agent.js. Nice.

But still the path was wrong for some reason

  1. I did some debugging, and found out that the path that works for BOTH DEV AND PROD is this:
app.getAppPath()   '/.webpack/main/'   'loadmill-agent'

WHY?

Well,

Returns string - The current application directory.

  • '/.webpack/main/' is the output path of this built-in webpack configuration.
  • 'loadmill-agent' is the file name (.js) is implicit here.
  1. The final result looks something like this:
const childProcess = fork(app.getAppPath()   '/.webpack/main/'   'loadmill-agent', // args & options ...

Hope this helps someone in the future, Cheers

  • Related