A real example of building Electron app: from scratch to release, and all those gotchas

Dadiorchen
CodeX
Published in
11 min readJul 24, 2021

I am not a fan of reading and writing tutorials for libraries, I prefer to read the official document, and I think this is the best way to learn stuff rather than read some secondary source of tutorial. But when I try to build my own product: the Midinote which is written by React and wrapped by the Electron. I read almost all those official docs, but it is still hard to do things right, the official document seems not well organized, especially for some sub-modules that it’s using underlying, like the Forge — the library to package and release the Electron app — it seems like you are doing a puzzle game. Things shouldn’t be like this, it’s JUST A TOOL, it should be as simple as possible for people, we need to use our limited energy to deal with real hard work in programming, right?

And another reason that building an app with Electron is hard is that it concerns some concepts of the app specification and the workflow of releasing app on different platforms like macOS, Windows, App Store, and some basic understanding of the structure of the app.

So I decided to write this article, it will cover the whole workflow from developing an app with Electron to how to release it, and all those troubles I have run into. I also will explain some basic concepts or structures along the way, to let you know how it works.

I can only cover the macOS part, cuz I still have no experience of building apps on Windows, I will write that part when I managed on it.

First, this is my real app running on macOS, Midinote, a note-taking app:

The Midinote running on macOS

And to keep things simple, I also created an example repo for demonstration, the code is here: https://github.com/dadiorchen/sandbox/tree/master/electron-react-app

Add Electron to an existing project

In this real case, I created a React app first, now I consider wrap it with Electron, and release it to a self-website or App Store. So we need to import Electron and the package tool Forge into an existing React app.

First, let's assume the React app has been created, (check React website for How-to), now install Electron-Forge:

npm install --save-dev @electron-forge/cli

and

npx electron-forge import

About npx check it here.

This command will add electron core, electron-forge into the project.

The above command will modify your package.json automatically, some of them aren’t what you want, it would override the command npm start to execute forge instant of React, change it back, the final form of the script section in package.json will look like:

"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject",
"start-forge": "electron-forge start",
"package": "electron-forge package",
"make": "electron-forge make"
},

And at this point, something remains missing, we need to set the entry point of the Electron app.

Add one line into package.json , under the root object:

"main": "./main.js",

Then add file main.js under the root directory of the project:

// main.js// Modules to control application life and create native browser window
const { app, BrowserWindow } = require('electron')
const path = require('path')
function createWindow () {
// Create the browser window.
const mainWindow = new BrowserWindow({
width: 800,
height: 600,
})
// and load the index.html of the app.
if(ENV === "dev"){
//for dev
mainWindow.loadURL('http://localhost:3000/');
}else if(ENV === "prod" || ENV === "production"){
//for prod
//win.loadFile('build/index.html')
const url = require('url').format({
protocol: 'file',
slashes: true,
pathname: require('path').join(__dirname, 'build/index.html')
})
mainWindow.loadURL(url)
}else{
throw new Error("wrong env");
}
// Open the DevTools.
// mainWindow.webContents.openDevTools()
}
// This method will be called when Electron has finished
// initialization and is ready to create browser windows.
// Some APIs can only be used after this event occurs.
app.whenReady().then(() => {
createWindow()
app.on('activate', function () {
// On macOS it's common to re-create a window in the app when the
// dock icon is clicked and there are no other windows open.
if (BrowserWindow.getAllWindows().length === 0) createWindow()
})
})
// Quit when all windows are closed, except on macOS. There, it's common
// for applications and their menu bar to stay active until the user quits
// explicitly with Cmd + Q.
app.on('window-all-closed', function () {
if (process.platform !== 'darwin') app.quit()
})
// In this file you can include the rest of your app's specific main process
// code. You can also put them in separate files and require them here.

For the file above, I copied the code from the tutorial from Electron but did some tweaks to fit our case.

Now you can run these commands to run the app with Electron:

npm start
npm run start-forge

You will see something like this:

The React app wrapped in Electron

Some explanations on this:

  • electron core is the real stuff to wrap the web app and run it in a desktop app.
  • electron-forge is the lib to package, release the app.

And electron-forge will use/install these libs under the hood:

  • electron-notarize the lib to deal with the Apple notarization process, which is required by Apple when you want to create an app.
  • electron-packager the lib to package the app to the form that the OS required.
  • electron-osx-sign the lib to deal with the code signing process, which is required by Apple if you want to release the app.

Build the app

Now, we can build the project into an app that can be running on macOS.

First, build the React:

npm run build

Build the Electron:

npm run make

Now you will find that a new folder called out shows up. And find the application built, in my case, it’s named: electron-react-app , double click to run it.

Package and sign the app

The above steps are just a way to let you try to build the app, actually, the built app in this way is incomplete, you can not use it to distribute/release your software on OSs. The full steps come below:

There are two ways to release the app on the macOS platform:

  1. The App Store
  2. The website of your own.

App Store is the place to release your app, and people can find it in the App Store Application on their macOS, and download it. From my understanding, App Store is more strict. Also, if you charge money for your app, you need to pay Apple at a certain rate.

Release to your own website is freer, and you do your own payment method, so Apple doesn’t charge money for your app.

Because my app will do some pretty heavy local operations like install a locally DB, I'm afraid I will get stuck if I release it to the App Store, by violating some rule from Apple, so I chose the latter, so I will just cover this part in this article, I might be able to write another blog to cover the workflow of releasing to App Store when I experienced it.

And to release the app, you are required two steps or say 3 steps, first, you need to package the app, a prerequisite for this is that your app must pass the so-called notarization step required by Apple. Then, you need to sign your code. Both of them (notarization and sign) need to connect to the cloud service by Apple and need to set something up with your Apple developer account, which means you need to enroll in the Apple Developer Membership and create something for the setting, I will cover this in below steps:

First, put settings below under the package.json ’s root object :

"config": {
"forge": {
"packagerConfig": {
"osxSign": {
"identity": "[your apple identity]",
"hardened-runtime": true,
"entitlements": "entitlements.plist",
"entitlements-inherit": "entitlements.plist",
"signature-flags": "library"
},
"osxNotarize": {
"appleId": "[your app ID]",
"appleIdPassword": "[your apple ID password"
}
},
"makers": [
{
"name": "@electron-forge/maker-squirrel",
"config": {
"name": "electron_react_app"
}
},
{
"name": "@electron-forge/maker-zip",
"platforms": [
"darwin"
]
},
{
"name": "@electron-forge/maker-dmg",
"config": {
"background": "./assets/b1-right.png",
"format": "ULFO"
}
}
]
}

Add a file named entitlements.plist , under the project root folder with content below:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.cs.allow-jit</key>
<true/>
<key>com.apple.security.cs.allow-unsigned-executable-memory</key>
<true/>
<key>com.apple.security.cs.debugger</key>
<true/>
</dict>
</plist>

Let me explain this:

  • About how does Forge’s make works

When you run command npm run make the Forge would read config from package.json ’s config → forge → packageConfig, the osxNotarize is for the Apple notarization, and oxsSign is for Apple code signing, there are other setting items available, actually forge would use these settings to invoke the underlying lib like electron-notarize electron-osx-sign electron-packager so you can find all those available settings from their repositories, respectively. The rule is, Forge will send all those setting items which are directly under the packageConfig to the electron-packager command (Forge use it by invoking their command directly, passing the settings to the command as options), Forge send settings under oxsSign to electron-osx-sign ’s command and send settings under oxsNotarize to electron-notarize .

  • About how to set the oxsNotarize section:

Please check this document from Apple.

  • About how to set the oxsSign

Please check this document from Electron.

When you trying to sign the code, Apple requires the certificates applied by yourself on the Apple Developer Console, and there are majorly two different types of certificates according to the way you are going to distribute, as I said, the App Store and your own website. The page to choose the certificates looks like this:

After generating the certificates, you need to download and install them on your Mac, another way to do it is using Apple’s Xcode.

Please check the doc above, for more details, you will figure out which option you should choose.

  • About the entitlement.plist file

This file is required by Apple, Apple uses it to define some aspects of this app, like what kind of OS resource this app will use.

  • About the device register

To finish the whole process, you also need to register your device on the Apple Developer Console:

  • About the makers

The last section in the config above is the makers , this is a feature, or say, some process steps supported by Forge. What would happen is that after packaging the app, Forge will do some extra work against the app packaged.

  1. Compress the package, in the example above, the @electron-forge/maker-zip will compress the app to .zip file.
  2. Build an installable file for macOS, mainly, we use an installer file with .dmg extension to install a software, the setting of @electron-forge/maker-dmg makes this happen. And you can also set a background image on the installer UI by setting config-->background

It is also possible to just give your user the app folder (the .app folder) instead of the .dmg file to use the app directly, but it is not good in some scenarios, there are some restrictions. For example, my app can not get updated by doing so, because macOS prohibit you to write/replace your original app file to let it get updated, except it’s in the Applications folder, the .dmg will solve this problem for you.

Note, you need to manually install the lib for .dmg maker:

npm install --save @electron-forge/maker-dmg
  • Wind up

Generally, when you run npm run make :

  1. npm run make
  2. Forge will use appropriate tools to do things:
  3. Upload your app to Apple’s service to do the notarization, using the appleId credentials.
  4. Trying to use the identity that was created in your Apple Developer Console to sign the code.
  5. After creating the package for the app, compress it to .zip and build a .dmg as an app installer.
  6. Done, check out the result in your out folder.

Now your app is ready for release.

Release the app

To release the app by using the self-maintained website, it’s easy, you just need to put the .dmg or .zip file onto your website’s download folder or some file service like s3 to let people download it.

In my case, I use Github’s release feature to publish the new version, because I need the auto-update feature to upgrade the app. The lib I’m using: https://github.com/electron/update.electronjs.org

Some gotchas and hints

List some problems I met, hope it’s helpful.

  • When you run into a problem, the log is always the best way to help you out.

For an Electron app, and the process of package, release, there are some different kinds of logs:

  1. Console

The traditional console.log() is invisible when you run the app. So you need this: electron-log to output the log, by default, the log of it would be output to ~/Library/Logs/{app name}/{process type}.log

2. Log for squirrel

Squirrel is a lib that Electron is using to update the app. The log would be output to ~/Library/Caches/{app name}.ShipIt

3. Browser

Using Chrome’s devtool to check the log output by code running in the browser.

  • A read-only cache file problem

When I package my app, I ran into a problem that in the packaged app, under the React build folder, which is the output folder by React build command, under the node-module/.cache folder (by default, the Electron would package the whole app folder), there is a read-only file generated, which would cause the failure of an app’s update. Finally, I found is this module: terser-webpack-plugin generated read-only file, my solution on this is before packaging the app, and after React building, insert a command ( rm ) to clean the .cache folder.

  • A virtual machine helps

If you are doing all these stuff on you own Mac, install a virtual machine and try to install and run your app in a virtual machine is a good idea, it isolates the environment, avoids unexpected result and keeps you host OS clean, the app would be installed on your applications folder, and store all kinds of files like cache, local DB file into your macOS’s system folders. I used VirtualBox to do the job, it’s pretty neat.

  • Avoid the way I’m doing

Actually, what I am doing isn’t a good practice, in terms of the way to import the forge, the thing is, I’m using React, and I imported the Forge to the same folder as the React located, this is problematic:

  1. The Forge would by default package the whole project, which in some way is unnecessary and risky. Like the case I mentioned above, the cache file problem.
  2. In this way, the Electron app (the main.js and other non-browser-parts) and the React app (the browser part) are using the same package.json and the same dependency definition. This is problematic, it’s possible to lead to libraries conflict.

So I am going to reorganize this project to isolate the app source and the package tool source, might be able to share new learning with you in the future.

Conclusion

There are some barriers to using Electron to build an app, comparing to using an original tool like Xcode, and what I’m doing is nowhere near perfect, and what I have written didn’t cover all those details, but I hope this article can give you a clear path of the whole task, to avoid disorientation in the jungle of configurations.

If you have suggestions, please comment below.

--

--