Search code examples
webpackelectronnode-modulesspawnelectron-forge

Electron app using Webpack runs binary node module in dev mode but not in production mode


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 onErrorNT (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.

Solution

  • 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