Add API to WebpackPlugin for compiling utilityProcess code
Pre-flight checklist
- [X] I have read the contribution documentation for this project.
- [X] I agree to follow the code of conduct that this project uses.
- [X] I have searched the issue tracker for a feature request that matches the one I want to file, without success.
Problem description
Currently, the WebpackPlugin only takes Webpack configs for the main and renderer process. If I want to create a utilityProcess in Electron, there's no way to compile a separate bundle for that code.
Proposed solution
I propose adding an option to the WebpackPlugin constructor to pass a Webpack config for utilityProcess code, so that it is compiled alongside the main and renderer process code. Similar to the renderer process, the utilityProcess bundle would also have a global variable exposed in the main process indicating from where to load the compiled module.
Alternatives considered
For now, I have written a custom Forge plugin to mimic this behavior, but I do not think this is a stable solution. To do this, I have created a plugin called UtilityProcessPlugin:
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();
});
});
}
}
Then, I have created a separate config for the utilityProcess, like so:
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",
};
Finally, to expose the compiled location, I use the DefinePlugin from Webpack to manually set the global variable in the main process config:
new DefinePlugin({
UTILITY_PROCESS_MODULE_PATH: JSON.stringify(modulePath),
}),
As you can see in my TODO for the utilityProcess config, I believe it is currently not possible to tell whether we are running electron-forge start or electron-forge make, which makes it hard to pass the correct mode value to Webpack. This also feels like a less stable approach than having something natively supported by the WebpackPlugin.
Additional information
The main use case for this feature is that you want to share code across your main process, renderer process, and utility process. If you are using Typescript for the common code, for example, then sharing without using a Webpack build step is impossible.
I propose adding an option to the
WebpackPluginconstructor to pass a Webpack config forutilityProcesscode, so that it is compiled alongside the main and renderer process code. Similar to the renderer process, theutilityProcessbundle would also have a global variable exposed in the main process indicating from where to load the compiled module.
I think this would be a great addition. Just one remark: Ideally the configuration should allow for any number of entry points (multiple utility processes) - similar to the renderer config.
@rikublock I haven't tried this yet, but I believe the vite plugin can essentially allow arbitrary entrypoints now that https://github.com/electron/forge/issues/3208 is incorporated into the latest release.
I tried a similar solution for the my project, but seems electron is unable to find the location of the file after packaging. Also after archiving the project to asar file, there is not clean way to get exact location for the path.
@AniketDha have you found any solution? it seems I have the same problem after packaging.
// forge.config.ts
plugins: [
new VitePlugin({
build: [
{
entry: 'src/preload.ts',
config: 'vite.preload.config.ts',
},
{
entry: 'src/utility.ts',
config: 'vite.preload.config.ts',
},
],
],
}),
]
// main.ts
childProcess = utilityProcess.fork(path.join(__dirname, 'utility.js'));
This code works in development, but not after packaging.
This helped me to run worker via utilityProcess: https://github.com/electron/electron/issues/42978#issuecomment-2241786882
If you're using webpack with a custom config you can do something like
// webpack main config
entry: {
index: './src/main.ts',
utility: './src/utility.process.ts',
},
output: {
filename: '[name].js',
}
// main.js
//
// __dirname will relsolve to .webpack/main
// utility.js is the filename generate by webpack from the entry key(name) 'utility' above
// and the output filename template '[name].js'
childProcess = utilityProcess.fork(path.join(__dirname, 'utility.js'));
It would be nice if the webpack plugin didn't try to do so much 'magic' configuration templating for you. I'd personally prefer just to have a list of entrypoints with targets like 'electron-main', 'electonr-renderer', 'electron-utility' (alias of main), 'electron-preload'. built in assumption about a preload for every renderer is a bit too clever. I understand a bit from reading the code that it impacts defines and hmr, but I think the same result could be achieved with a more flat configuration.