---
title: "Building and publishing an Electron application using electron-builder"
description:
  "Building and publishing an Electron application using electron-builder."
canonical_url: "https://www.bigbinary.com/blog/publish-electron-application"
markdown_url: "https://www.bigbinary.com/blog/publish-electron-application.md"
---

# Building and publishing an Electron application using electron-builder

Building and publishing an Electron application using electron-builder.

- Author: Farhan CK
- Published: October 22, 2024
- Categories: Electron

_Recently, we built [NeetoRecord](https://neetorecord.com/neetorecord/), a loom
alternative. The desktop application was built using Electron. In a series of
blogs, we capture how we built the desktop application and the challenges we ran
into. This blog is part 2 of the blog series. You can also read
[part 1](https://www.bigbinary.com/blog/sync-store-main-renderer-electron),
[part 3](https://www.bigbinary.com/blog/video-background-removal),
[part 4](https://www.bigbinary.com/blog/electron-multiple-browser-windows),
[part 5](https://www.bigbinary.com/blog/code-sign-notorize-mac-desktop-app),
[part 6](https://www.bigbinary.com/blog/deep-link-electron-app),
[part 7](https://www.bigbinary.com/blog/request-camera-micophone-permission-electron)
[part 8](https://www.bigbinary.com/blog/native-modules-electron) and
[part 9](https://www.bigbinary.com/blog/ev-code-sign-windows-application-ssl-com)
._

Building, packaging and publishing an app with the default Electron npm packages
can be quite challenging. It involves multiple packages and offers limited
customization. Additionally, setting up auto-updates requires significant
additional effort, often involving separate tools or services.

[electron-builder](https://www.electron.build/) is a complete solution for
building, packaging, and distributing [Electron](https://electronjs.org/)
applications for macOS, Windows, and Linux. It is a highly configurable
alternative to the default Electron packaging process that supports auto-update
out of the box.

In this blog, we look into how we can build, package and distribute Electron
applications using `electron-builder`.

### Electron processes

Electron has two types of processes: the `main` process and the `renderer`
process. The main process acts as the entry point to the application, where we
can create a browser window and load a webpage. This webpage runs in the
`renderer` process. The `main` process is written in `Node.js`, while the
renderer process can be developed using JavaScript or any JS framework like
`React`, `Vue`, or `Angular`.

### Project structure

When building an Electron application, it's best to keep the `main` and
`renderer` processes in separate folders since they are built separately.

```javascript {5, 7}
electron-app
├── assets
├── release
├── src
│   └── main
│     └── main.js
│   └── renderer
│     └── App.jsx
│     └── index.ejs
├── node_modules
├── package.json
```

### Browser window preload script

Since the `main` process is written in `Node.js`, it has access to `Node.js` and
Electron APIs, but the `renderer` process does not. To bridge this gap, Electron
supports a special script called a `preload` script, which we can specify when
creating a `BrowserWindow`. This script runs in a context that has access to
both the HTML DOM and a limited subset of Node.js and Electron APIs. An example
`preload` script looks like this:

```js
import { contextBridge, ipcRenderer } from "electron";

const electronHandler = {
  ipcRenderer: {
    sendMessage(channel, ...args) {
      ipcRenderer.send(channel, ...args);
    },
    on(channel, func) {
      const subscription = (_event, ...args) => func(...args);
      ipcRenderer.on(channel, subscription);

      return () => {
        ipcRenderer.removeListener(channel, subscription);
      };
    },
    once(channel, func) {
      ipcRenderer.once(channel, (_event, ...args) => func(...args));
    },
  },
};

contextBridge.exposeInMainWorld("electron", electronHandler);
```

This preload script exposes `send`, `on` and `once` methods of `ipcRenderer` to
the renderer process via the `contextBridge`. It allows the renderer to send
messages, listen for events, and handle one-time events from the main process
while maintaining security by controlling which APIs are accessible.

### Build

Since an Electron application has two processes, we need two separate Webpack
configurations—one for the `main` process and another for the `renderer`
process.

```js
// ./config/webpack/main.mjs

import path from "path";
import webpack from "webpack";
import { merge } from "webpack-merge";
import TerserPlugin from "terser-webpack-plugin";

const configuration = {
  target: "electron-main",
  entry: {
    main: "src/main/main.mjs",
    preload: "src/main/preload.mjs",
  },
  output: {
    path: "release/app/dist/main",
    filename: "[name].js",
    library: {
      type: "umd",
    },
  },
  optimization: {
    minimizer: [
      new TerserPlugin({
        parallel: true,
      }),
    ],
  },
  node: {
    __dirname: false,
    __filename: false,
  },
};

export default configuration;
```

The above is a basic Webpack configuration for the `main` process. Webpack
supports Electron out of the box, so by setting `target: "electron-main"`,
Webpack includes all the necessary Electron variables. Since we also have a
`preload` script, we added `preload.mjs` as an entry point as well. We will be
minifying the code using `TerserPlugin`.

Another important detail is that we've disabled `__dirname` and `__filename`.
This prevents Webpack's handling of these variables from interfering with
Node.js's native `__dirname` and `__filename`, ensuring they behave as expected
in our Electron app.

```js
// ./config/webpack/renderer.mjs

import path from "path";
import webpack from "webpack";
import HtmlWebpackPlugin from "html-webpack-plugin";
import MiniCssExtractPlugin from "mini-css-extract-plugin";
import { BundleAnalyzerPlugin } from "webpack-bundle-analyzer";
import CssMinimizerPlugin from "css-minimizer-webpack-plugin";
import { merge } from "webpack-merge";
import TerserPlugin from "terser-webpack-plugin";

const configuration = {
  target: ["web", "electron-renderer"],
  entry: "src/renderer/App.jsx",
  output: {
    path: "release/app/dist/renderer",
    publicPath: "./",
    filename: "renderer.js",
    library: {
      type: "umd",
    },
  },
  module: {
    rules: [
      {
        test: /\.s?(a|c)ss$/,
        use: [
          MiniCssExtractPlugin.loader,
          {
            loader: "css-loader",
            options: {
              modules: true,
              sourceMap: true,
              importLoaders: 1,
            },
          },
          "sass-loader",
        ],
        include: /\.module\.s?(c|a)ss$/,
      },
      {
        test: /\.s?(a|c)ss$/,
        use: [
          MiniCssExtractPlugin.loader,
          "css-loader",
          "sass-loader",
          "postcss-loader",
        ],
        exclude: /\.module\.s?(c|a)ss$/,
      },
      // Fonts
      {
        test: /\.(woff|woff2|eot|ttf|otf)$/i,
        type: "asset/resource",
      },
      // Images
      {
        test: /\.(png|jpg|jpeg|gif)$/i,
        type: "asset/resource",
      },
      // SVG
      {
        test: /\.svg$/,
        use: [
          {
            loader: "@svgr/webpack",
            options: {
              prettier: false,
              svgo: false,
              svgoConfig: {
                plugins: [{ removeViewBox: false }],
              },
              titleProp: true,
              ref: true,
            },
          },
          "file-loader",
        ],
      },
    ],
  },

  optimization: {
    minimize: true,
    minimizer: [new TerserPlugin(), new CssMinimizerPlugin()],
  },

  plugins: [
    new MiniCssExtractPlugin({
      filename: "style.css",
    }),
    new HtmlWebpackPlugin({
      filename: "index.html",
      template: "src/renderer/index.ejs",
      minify: {
        collapseWhitespace: true,
        removeAttributeQuotes: true,
        removeComments: true,
      },
      isBrowser: false,
      isDevelopment: false,
    }),
  ],
};

export default configuration;
```

In the `renderer` configuration, we set the target to
`target: ["web", "electron-renderer"]`, which provides both standard web and
Electron's renderer variables. Similar to a typical web application setup, we
load various plugins to handle fonts, images, and SVGs and to minify CSS and
JavaScript. Since JavaScript files are loaded locally in an Electron
application, we can bundle everything into a single file called `renderer.js`
instead of splitting it into multiple chunks as we would for a standard web
application.

Our build configuration is ready; add it to `scripts` in `package.json` for easy
execution.

```json
"scripts": {
    "build:main": "cross-env NODE_ENV=production webpack --config ./config/webpack/main.mjs",
    "build:renderer": "cross-env NODE_ENV=production webpack --config ./config/webpack/renderer.mjs",
    "build": "yarn build:main && yarn build:renderer",
}
```

Great! Now, we can build the entire Electron application using a single
`yarn build` command.

### Package

We can configure the `electron-builder` in `package.json` using the `build`
field. Below is an example configuration for an app named `MyApp`.

```json
 "build": {
    "productName": "MyApp",
    "appId": "com.neeto.MyApp",
    "directories": {
      "app": "release/app",
      "buildResources": "assets",
      "output": "release/build"
    },
     "mac": {
      "target": {
        "target": "default",
        "arch": [
          "arm64",
          "x64"
        ]
      }
    },
    "win": {
      "target": {
        "target": "nsis",
        "arch": [
          "x64"
        ]
      },
      "artifactName": "${productName}-Setup-${version}.${ext}"
    },
    "linux": {
      "category": "Utility",
      "target": [
        {
          "target": "rpm",
          "arch": [
            "x64"
          ]
        },
        {
          "target": "deb",
          "arch": [
            "x64"
          ]
        }
      ],
    },
 }
```

In the Webpack configuration, we specified `release/app` as the output directory
for the compiled JS code. This directory needs to be specified in
`electron-builder` so it knows where to find the compiled code during packaging.
Use the `directories.app` field to specify this path, and we can also define
where the packaged builds should be output using the `directories.output` field.

In addition to `directories`, the configuration includes settings for `appId`,
`productName` and platform-specific configurations for `mac`, `win`(Windows),
and `linux`. For each platform, we specify the installer target and
architecture. This configuration will produce builds for both Intel (`x64`) and
Apple Silicon (`arm64`) on **macOS**. For **Windows**, it generates an `NSIS`
installer targeting 64-bit architecture (`x64`). On **Linux**, it produces both
`RPM` and `DEB` installers for 64-bit architecture (`x64`).

With the `electron-builder` configuration set, we can proceed to package our
application using the `electron-builder build` command.

```json {5}
"scripts": {
    "build:main": "cross-env NODE_ENV=production webpack --config ./webpack/main.prod.mjs",
    "build:renderer": "cross-env NODE_ENV=production webpack --config ./webpack/renderer.prod.mjs",
    "build": "yarn build:main && yarn build:renderer",
    "package": "yarn build && electron-builder build",
}
```

We run `yarn build` before `electron-builder build` to ensure that the
JavaScript code is compiled before packaging. This enables us to handle both the
build and packaging processes in a single command.

### Publish

To publish the app to a server where users can download and use it, we can pass
the `--publish` flag to `electron-builder build` command. Before we can do that,
we need to update our `electron-builder` configuration with `publish` server
information.

Here is an example configuration to publish the builds to an S3 bucket named
`my-app-downloads`:

```json
 "build": {
  // other configs
  "publish": [
      {
        "provider": "s3",
        "bucket": "my-app-downloads",
        "path": "/electron/my-app/",
        "region": "us-east-1",
        "acl": null
      },
    ]
 }
```

The `publish` field accepts an array, allowing us to publish to multiple
locations. We need to specify a provider, such as `s3`, but `electron-builder`
also supports other providers like `github` and more by default. For a complete
list of all publishing options, check out the
[publish documentation](https://www.electron.build/configuration/publish).

To wrap things up, let's automate the deployment process by creating a GitHub
Actions workflow. Since building macOS apps is only supported on macOS, we'll
need to run the workflow from a macOS environment.

```yml
name: Publish

jobs:
  publish:
    runs-on: ${{ matrix.os }}

    strategy:
      matrix:
        os: [macos-latest]

    steps:
      - name: Setup Java
        uses: actions/setup-java@v3
        with:
          distribution: "adopt"
          java-version: "11"

      - name: Checkout git repo
        uses: actions/checkout@v3

      - name: Install Node and NPM
        uses: actions/setup-node@v3
        with:
          node-version: 20
          cache: npm

      - name: Install rpm using Homebrew
        run: brew install rpm

      - name: Install and build
        run: |
          yarn install
          yarn build

      - name: Publish releases
        env:
          AWS_ACCESS_KEY_ID: ${{secrets.AWS_ACCESS_KEY}}
          AWS_SECRET_ACCESS_KEY: ${{secrets.AWS_SECRET}}
        run: npm exec electron-builder -- --publish always -mwl
```

To successfully build for Windows and Linux from macOS, we'll need to set up a
Java version and install the `rpm` package. Additionally, configure our
`AWS_ACCESS_KEY_ID` and `AWS_SECRET_ACCESS_KEY` for the S3 bucket where we plan
to upload the builds. Once these steps are complete, we should be able to build
and publish our Electron app using the `electron-builder -- --publish` command.
The `-mwl` flag indicates that the build should target macOS, Windows, and
Linux.

### Auto-update

To enable auto-updating for our application, we first need to code-sign and
notarize it. We'll cover the code-signing and notarization details in upcoming
blog posts. Stay tuned!

## Links

- [Human page](https://www.bigbinary.com/blog/publish-electron-application)
