How to Package Electron Apps for distribution with Conveyor

Contributed by Godwin Kunale Nana Yaw

In this blog post, I’ll delve into Conveyor and discover how it simplifies the deployment of Electron apps. Whether you’re starting a new project or working with an existing one, I’ll guide you through the setup process of Conveyor. Additionally, I’ll provide insights into potential challenges when dealing with JavaScript package managers other than npm. Let’s dive in!

Background

Packaging Electron applications for distribution may initially seem straightforward until you encounter the challenges. Electron is widely embraced for its ability to create native desktop apps using familiar web technologies such as HTML, CSS, and JavaScript. However, the lack of robust packaging tools for Electron applications has been a persistent issue. For instance, generating distributable packages for various target systems like Windows, Linux, and Mac can be cumbersome without access to those specific operating systems or relying on automated CI/CD systems like GitHub Actions.

This complexity can be particularly overwhelming for web developers who are accustomed to a plethora of standardized tooling systems designed for web development. Within the Electron community, you’ll find vague instructions and numerous abandoned projects that partially solve certain problems while neglecting others.

This is where Conveyor steps in.

What is Conveyor & what does it do?

Conveyor is a command-line interface (CLI) tool designed to streamline the packaging process of applications for distribution. Similar to other familiar dev CLI tools, you can download and install Conveyor on your device and communicate with it via commands and configuration.

By utilizing Conveyor, you can eliminate several challenges:

  • Avoid the need to package applications separately for each native platform you want to support (Mac, Windows, and Linux). Instead, you can build for all these platforms simultaneously from a single device.
  • Simplify the task of signing your applications and handling notarization for macOS. Conveyor takes care of these steps for you by utilizing the provided keys, cloud signing service or self-signing if the keys are not specified.
  • Advanced well maintained update engines like Sparkle and the Windows Deployment engine (versus Squirrel which is nearly unmaintained). If wanted you can make your app check for updates as part of starting up, so your app is always up to date.

These benefits are just a glimpse of what Conveyor offers compared to other tools. For a comprehensive comparison, refer to this section of our documentation.

Set up for new projects

Let’s set up Conveyor on your local machine. To download the appropriate installer for your system visit this link.

The Conveyor CLI provides commands for scaffolding starter templates for Electron projects. In just three simple steps, we can create a fresh Electron project using Conveyor and modify our configuration to target both Mac and Windows systems.

STEP 1: Scaffold the project

To scaffold your project, you can run the following command: conveyor generate electron com.domain.new-project. This command will create a folder named new-project with the necessary project files. Inside this folder, you’ll find a conveyor.conf file that holds the essential configurations related to Conveyor.

STEP 2: Install project dependencies

Let’s run the command npm install to install our project dependencies. If not using npm you could run yarn install or whatever your package manager requires. This command will read the package.json file in your project and install all the required dependencies listed under the "dependencies" section.

STEP 3: Modify the conveyor.conf file to suit your needs.

The default configuration provided by Conveyor is sufficient to get you started. However, for the purposes of this post, we will modify it to build for both Windows and Mac platforms simultaneously. To do this, add the following line to the app {...} section of the configuration:

machines = [windows.amd64, mac]

This line instructs Conveyor to build distributable packages for the specified target machines, which include Windows and Mac.

Please note that the conveyor.conf file supports over 100 configuration keys that can be declared. Some of these are imported from your project’s package.json file, which is why you may see the following declaration in the configuration file:

package-json { 
    include required("package-lock.json") 
}

Additionally, please be aware that the syntax used inside the conveyor.conf file is HOCON, which is a superset of JSON. If you prefer, you can replace the HOCON syntax with valid JSON, and Conveyor should still function properly.

Adapting existing projects

Adapting your existing Electron applications for packaging with Conveyor is a straightforward process. Here are the steps to follow:

  • In the root directory of your Electron project, create a new file called conveyor.conf.
  • Copy and paste the following sample configuration into the conveyor.conf file:
include required("/stdlib/electron/electron.conf")

// This section imports metadata like the name, 
// app version and which version of Electron to use.
package-lock {
  include "package-lock.json"
}

app {
  display-name = Adapted App
  rdns-name = com.example.adapted-app
  site.base-url = "localhost:3000"
}

If you don’t have a package-lock.json file or if it doesn’t match the standard layout, you’d need to specify some more details by hand. See setting the Electron version below.

Packaging for distribution

If you’ve reached this point it means you have either created a project using Conveyor’s Electron boilerplate or adapted your existing application for packaging with Conveyor. Now, let’s move on to the final step of building and packaging your project into auto-updating distributable packages using Conveyor.

From the root directory of your project, run the following command:

conveyor make site

This command triggers the build and packaging process of your Electron application using Conveyor. The resulting distributable packages will be placed inside an output folder, which is created in your project directory.

Navigate to the output directory to explore the generated distributable packages. Additionally, you’ll find a convenient download.html file within this directory. This download.html file can be served to the consumers of your application, allowing them to download platform-specific installers for their respective systems. Conveyor will also have made an icon for you.

Congratulations on reaching this stage! Your Electron application is now ready to be shared and installed on various platforms. All you have to do is serve the contents of the output directory from the URL specified as app.site.base-url in your config. You can also configure Conveyor to upload files for you, automating the entire release process.

Setting the Electron version

The default Electron config (/stdlib/electron/electron.conf) contains the following lines:

app {
    fsname = ${?package-json.name}
    display-name = ${?package-json.productName}
    version = ${?package-json.version}
    contact-email = ${?package-json.author.email}
    electron.version = ${?package-json.packages.node_modules/electron.version}
}

When you add this declaration to your own config:

package-json {
    include required("package-lock.json")
}

… it means the project will self-configure based on the details in your lockfile. It’s nice to avoid duplicating metadata. The most important is the last key: app.electron.version. If you don’t set this you may encounter an error message like “Windows App for Intel: No EXE files were found in the root of the Windows inputs” or something similar.

If you don’t have a package-lock.json file that’s OK, you can still read the Electron version from any other file. Just specify an exact version instead of a range in package.json and then import that instead:

package-json {
    include required("package.json")
}

app {
    electron.version = ${package-json.devDependencies.electron}
}

Alternatively:

  1. Set the Electron version by hand. For example, app { electron.version = 21 }.
  2. Import config from some other file. See below for how to import non-JSON files.
  3. Convert your yarn.lock to package-lock.json. You can use synp to do this.

Importing config from non-JSON files is easy because Conveyor can run programs and read their stdout as part of loading a config file, meaning you can compute config from scripts. This can be used to read the Electron version we’re using from the yarn.lock file.

Let’s put this into the file get-electron-version.sh (you’d have to translate it to PowerShell if building on Windows)

#!/usr/bin/env bash
yarn list --depth=0 --json -s --no-progress 2>/dev/null | jq -r '.data.trees[] | select(.name | startswith("electron@")) | .name | split("@")[1]'

Mark it executable (chmod +x get-electron-version.sh) and now it should print out the locked version of Electron you’re using. Now we can import that into our conveyor.conf:

include "#!=app.electron.version get-electron-version.sh"

Using build artifacts

In certain cases, your project may require a specific build process or have a custom flow. For example, projects like the Electron + Nuxt template repository or GitHub Desktop with Conveyor may have unique build requirements. In such situations, you can set up a build step using tools like electron-builder and configure Conveyor to use the generated build artifacts as inputs. This involves specifying the file paths or URLs to the generated artifacts for each supported operating system and CPU architecture.

To illustrate this, let’s modify the previous configuration example:

include required("/stdlib/electron/electron.conf")

package-lock {
  include "package-lock.json"
}

app {
  display-name = "Adapted App"
  rdns-name = "com.example.adapted-app"
  site.base-url = "localhost:3000"

  # This section specifies custom inputs for target operating systems and CPUs. 
  # The values for your inputs can be file paths or URLs to a file.

  windows.amd64.inputs = "build/${package-json.artifactName}-${package-json.version}.zip"
  mac.amd64.inputs = "build/${package-json.artifactName}-${package-json.version}.zip"
}

In this modified configuration, the inputs for each target operating system and CPU architecture point to raw files generated from a custom build step. You can perform this build step locally using tools like electron-builder or remotely using CI/CD pipelines such as GitHub Actions.

Remember, the inputs specified should correspond to the generated raw files based on your custom build process. This allows Conveyor to correctly include the necessary files when building and packaging your application for distribution. You can also specify directories or archives on remote URLs.

By following these approaches, you can handle edge cases and adapt Conveyor to work effectively with JavaScript package managers other than npm, ensuring a successful packaging process for your Electron application.

.