The following directories can be found in src/
and approximately represent the Electron processes of interest:
main/
: Code that runs Electron's main process.- has access to Node
- has direct access to Electron APIs
services/
: Code that is spawned and coordinated by Electron's main process, usually as autilityProcess
.- has access to Node
- does not have direct access to Electron APIs.
preload/
: Code that is injected as preload scripts into the renderer windows created by the main process.- has access to browser APIs and some Electron and Node APIs
renderer/
: Code that runs in Electron's renderer process.- has access to browser APIs
The following use cases should generally be defined and set up in a preload script:
- communication between renderer and main processes
- any usage of Electron's renderer modules
- usage of a Node API (either directly or through a module) that's supported by the preload script sandbox
For example, in src/preload/main-window.js
, we expose an API to interact with the main process on window.runtime
. This may change in the future but it's actually quite nice because it becomes more accessible for debugging purposes (e.g. testing the API using the devtools console).
Make sure you have the desired Node version installed. For this project we encourage using the version that's specified in the .nvmrc
(or .tool-versions
) file. We recommend using a proper version management tool to install and manage Node versions, such as nvm, fnm, asdf, or mise.
Create a copy of the .env.template
and call it .env
. Update the following variables:
ONLINE_STYLE_URL
: Full URL that points to a compatible map's StyleJSON.
Run the following commands to start the app:
npm install # Install dependencies
npm start # Build translations, then build the app in development mode and start the development server
- Changes in the
src/renderer/
should immediately automatically be reflected in the app - Changes in the
src/preload/
require the window to be refreshed to be reflected in the relevant window. Either go to theView > Reload
menu option or use the keyboard shortcut (e.g. CMD + R on macOS, CTRL + R on Linux, Windows) - Changes to
src/main/
orsrc/services/
require restarting the app. You can either:- Stop the process that is running
npm start
and rerun it. - Type R + S + Enter in the process that is running
npm start
, which tells Forge to restart the main process.
- Stop the process that is running
- In development, the
userData
directory is set to thedata/
directory by default. This provides the following benefits:- Avoids conflicting with the existing app if it's installed. Assuming the same app id is used, Electron will default to using the OS-specific user data directory, which means that if you have a production version of the app installed, starting the development version will read and write from the production user data directory. Most of the time this is not desired (you generally don't want to mix production data and settings with your development environment). If it is desired, comment out the line that calls
app.setPath('userData', ...)
insrc/main/index.js
- Easier to debug because you don't have to spend as much time to figure out which directory to look at (it changes based on operating system)
- Avoids conflicting with the existing app if it's installed. Assuming the same app id is used, Electron will default to using the OS-specific user data directory, which means that if you have a production version of the app installed, starting the development version will read and write from the production user data directory. Most of the time this is not desired (you generally don't want to mix production data and settings with your development environment). If it is desired, comment out the line that calls
- If you want to change the
userData
directory, define an environment variable calledUSER_DATA_PATH
that can be used when callingnpm start
. For example, runningUSER_DATA_PATH=./my_data npm start
will create amy_data
directory relative to the project root. This is useful for creating different "profiles" and isolating data for the purpose of testing features or reproducing bugs - If you are installing a package that is only going to be used by code the renderer (e.g. a React component library), you most likely should install it as a dev dependency instead of a direct dependency. This differs from typical development workflows you see elsewhere, but the reasoning is that during the packaging stage of the app,
@electron/packager
avoids copying dev dependencies found in thenode_modules
directory. Since we bundle our renderer code, we do not need to copy over these dependencies, which results in a significant decrease in disk space occupied by the app. - We use
debug
for much of our logging in the main process. In order to see them, you can specify theDEBUG
environment variable when running the app e.g.DEBUG=comapeo:* npm start
.
- The configuration for the renderer app is defined in the Vite configuration file that lives in the
src/renderer
. - The configuration for the Electron app is located in
forge.config.js
.
The renderer app (aka React code) can run unit test with Vitest (and integration tests with React Testing Library).
To run unit or integration tests run npm run vitest:run
or npm run vitest:watch
. See vitest run and vitest watch to understand the difference.
The messages/
directory contains the translation strings used by the app. Within this directory are directories for the main process (messages/main/
) and renderer process (messages/renderer/
). Messages found in messages/main/
are typically needed for translating text that lives in native UI (e.g. the menu bar), whereas messages in messages/renderer/
are needed for translating text that's used in the rendered web app.
In order to update translations, run npm run intl:translations
, which will extract messages and place them in the relevant messages/
directory and then compile those messages into translated strings and place them in the translations/
directory.
-
For the main process:
- Use
defineMessage
(ordefineMessages
) from@formatjs/intl
to create messages and use in the main process code. - Run npm run
intl:translations:main
(ornpm run intl:translations
).
- Use
-
For the renderer process:
- Use
defineMessage
(ordefineMessages
) fromreact-intl
to create messages and use in the renderer process code. - Run npm run
intl:translations:renderer
(ornpm run intl:translations
).
- Use
The Electron Forge docs are pretty informative (especially https://www.electronforge.io/core-concepts/build-lifecycle) but in a nutshell:
-
npm run forge:package
: generate an executable app bundle i.e. an executable that you can run in the command-line. -
npm run forge:make
: generate an distributable installer or archives that you can install by opening using your filesystem. -
npm run forge:publish
: upload the distributable to some cloud storage for distribution.
All commands place the built assets in the out/
directory.
Our build process requires a .env.production
file to exist at the project root in order for these commands to work. Usually this file will be generated as a prerequisite to packaging the app (e.g. in continuous deployment environments like GitHub Actions). If you are just debugging locally, you can create a copy of your .env
file and call it .env.production
in order to avoid errors at build time.
If you're running into an error with any of the Forge-related commands but not seeing any output in the console, you probably have to prefix the command with DEBUG=electron-forge
e.g. DEBUG=electron-forge npm run forge:package
.
By default, we package the app in the ASAR format. However, it can be helpful to avoid doing that for debugging purposes (e.g. building locally), in which case you can specify a ASAR=true
environment variable when running the relevant Forge command e.g. ASAR=true npm run forge:package
.