Building code for a utility process with Electron Forge and Webpack
February 20, 2023 - Tech
When developing an Electron app, you will often want to run
expensive or long-running operations in a different process. In the past, this
might have been done using the child_process
module. But,
Electron has recently introduced a new utilityProcess
that
uses the Chromium Services API. This article outlines how to compile the
code for your utilityProcess
using your existing Webpack config.
- The problem: how to compile the
utilityProcess
code with Webpack? - Electron Forge’s Webpack config
- The solution: write our own Electron Forge plugin
- Appendix: getting a first-party plugin
The problem: how to compile the utilityProcess
code with Webpack?
To initialize the utilityProcess
, you must provide a node module path, like so:
import { utilityProcess } from "electron";
this.child = utilityProcess
.fork("./utilityCode")
.on("spawn", () => console.log("spawned new utilityProcess"))
.on("exit", (code) => console.log("existing utilityProcess"));
This code assumes that you have a utilityCode.js
file that is a Node module.
But, this doesn’t work so well if you’re building all of your other code with
Webpack. You might be using Typescript, and integrating other compilation steps.
Naturally, you would want all of your utilityProcess
code to also run through
Webpack first.
Electron Forge’s Webpack config
When you initialize an Electron project with Electron Forge, it initializes a Webpack config for you. This configuration consists of the following files:
webpack.rules.config.ts
: which Webpack loaders to use for this project.webpack.main.config.ts
: the Webpack config for the main process.webpack.renderer.config.ts
: the Webpack config for the renderer process.webpack.plugins.ts
: contains plugins for the renderer process.forge.config.ts
: passes the main and renderer process configs to Forge.
The WebpackPlugin
is used by Electron Forge to run Webpack
for the code of each process, but there’s no API for adding a utilityProcess
.
The solution: write our own Electron Forge plugin
First, we create a new plugin called UtilityProcessPlugin
in a separate file called utility.plugin.ts
:
import { webpack, Configuration as RawWebpackConfiguration } from "webpack";
import { PluginBase } from "@electron-forge/plugin-base";
import { ForgeHookMap } from "@electron-forge/shared-types";
export default class UtilityProcessPlugin extends PluginBase<RawWebpackConfiguration> {
name = "utility-process";
getHooks(): ForgeHookMap {
return {
generateAssets: this.generateAssets.bind(this),
};
}
private async generateAssets(): Promise<void> {
return new Promise((resolve, reject) => {
webpack(this.config, (err, stats) => {
if (err) {
reject(err);
return;
}
if (stats?.hasErrors()) {
const json = stats.toJson();
for (const error of json.errors ?? []) {
reject(new Error(`${error.message}\n${error.stack}`));
return;
}
}
resolve();
});
});
}
}
This plugin takes a Webpack config and runs the Webpack compiler on it. Then, we create a Webpack configuration for the utilityProcess
:
import { Configuration } from "webpack";
import path from "path";
import { rules } from "./webpack.rules";
const modulePath = path.resolve(__dirname, "dist_utility/utility_process");
export const utilityConfig: Configuration = {
entry: "./src/utility.ts", // Change to your own entry point
target: "node",
module: {
rules,
},
output: {
path: modulePath,
filename: "index.js",
},
resolve: {
extensions: [".js", ".ts", ".jsx", ".tsx", ".css", ".json"],
},
// TODO: find a way to infer this based on whether we run electron-forge start
// or package.
mode: "development",
};
This configuration will look for a src/utility.ts
file as the entrypoint. Feel free to change this to whatever file is your entrypoint. Then, it will compile the code and output it to a dist_utility/utility_process
directory. Then, we need to tell Electron Forge about our plugin, by modifying forge.config.ts
:
// ... other imports
import { utilityConfig } from "./webpack.utility.config";
import UtilityProcessPlugin from "./utility.plugin";
const config: ForgeConfig = {
// ... other configuration
plugins: [
new UtilityProcessPlugin(utilityConfig),
// ... other plugins
],
};
export default config;
Next, to make sure our main process knows whether this code has been compiled
to, we use the Webpack DefinePlugin
to define a global
variable in the main process that contains the location of the compiled
utilityProcess
code. In the webpack.main.config.ts
file, we add:
import { Configuration, DefinePlugin } from "webpack";
// ... other imports
const modulePath = path.resolve(__dirname, "dist_utility/utility_process");
export const mainConfig: Configuration = {
// ... other configuration
plugins: [
new DefinePlugin({
UTILITY_PROCESS_MODULE_PATH: JSON.stringify(modulePath),
}),
],
};
Finally, we can modify our code in the main process to load our compiled
utilityProcess
:
import { utilityProcess } from "electron";
declare const UTILITY_PROCESS_MODULE_PATH: string;
this.child = utilityProcess
.fork(UTILITY_PROCESS_MODULE_PATH)
.on("spawn", () => console.log("spawned new utilityProcess"))
.on("exit", (code) => console.log("existing utilityProcess"));
Appendix: getting a first-party plugin
I have opened a feature request with the Electron Forge team to help push this
proposal into the WebpackPlugin
. Please see
https://github.com/electron/forge/issues/3169 for progress.
Let me know what you think about this article by leaving a comment below, reaching out to me on Twitter or sending me an email at pkukkapalli@gmail.com