HB's Thoughts

I try to think really hard.

8/26/2024

Electron, TypeScript & Parcel

Documentation for Electron is entirely in JavaScript, but that doesn't stop you from using TypeScript to generate that JavaScript. A few simple rules must be followed, mainly in the file loading paths. I have also prepared a small addition for the front-end part. Instead of the standard Electron HTML page, I will make a small compilation with Parcel.

The Project

First, we will organize our project. It will have 2 subfolders - one for the Electron part, the other for the Browser part. We create a folder electron-typescript-parcel and open it in VSCode - or whichever editor you use. Open the built-in terminal in VSCode (or another if you don't use VSCode) and execute:

npm init -y

This will create a package.json file in the electron-typescript-parcel folder. Open the file and edit the author field. As a start, it is enough. Next, we need to add the Electron module to the project.

npm install --save-dev electron

If you are going to use GIT, now is a good time to execute:

git init

and add a .gitignore file. Add node_modules as a start.

Then add 2 folders - electron and browser in the project folder. As the names suggest - Electron will live in the first, and the front-end part for the browser will live in the second.

Electron

Through the terminal, enter the electron folder and execute:

npm init -y

and immediately after that add the TypeScript module:

npm install --save-dev typescript

Load package.json. In the script part, add "build": "tsc" and remove the "main" attribute.

// electron/package.json
{
  "name": "electron",
  "version": "1.0.0",
  "scripts": {
    "build": "tsc"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "description": "",
  "devDependencies": {
    "typescript": "^5.5.4"
  }
}

In the console execute:

npx tsc --init

This will create a tsconfig.json file. In it, you will need to find "outDir", uncomment the line, and set "outDir": "../dist".

From here, we follow the standard steps for creating a basic Electron application, skipping the part about creating the index.html file and renderer.js file, which we will add through Parcel.

Add a main.ts file to the electron folder and write the following code:

// electron/main.ts
import { app, BrowserWindow, ipcMain, nativeTheme } from "electron";
import path from "node:path";

/**
 * Creates a new window and loads an HTML file.
 */
const createWindow = (): void => {
  const mainWindow = new BrowserWindow({
    width: 800,
    height: 600,
    webPreferences: {
      preload: path.join(__dirname, "./preload.js"),
    },
  });

  mainWindow.loadFile("./dist/index.html");

  ipcMain.handle("dark-mode:toggle", () => {
    if (nativeTheme.shouldUseDarkColors) {
      nativeTheme.themeSource = "light";
    } else {
      nativeTheme.themeSource = "dark";
    }

    return nativeTheme.shouldUseDarkColors;
  });
};

app.whenReady().then(() => {
  createWindow();

  app.on("activate", () => {
    if (BrowserWindow.getAllWindows().length === 0) createWindow();
  });
});

app.on("window-all-closed", () => {
  if (process.platform !== "darwin") app.quit();
});

This is an example from the Electron documentation that changes the dark or light theme of the application.

Next, we add a preload.ts file to the electron folder and write the following code:

// electron/preload.ts
import { contextBridge, ipcRenderer } from "electron/renderer";

contextBridge.exposeInMainWorld("electronAPI", {
  toggle: () => ipcRenderer.invoke("dark-mode:toggle"),
});

Browser

Through the terminal, we navigate to the browser folder and execute:

npm init -y

After that, we install Parcel:

npm install --save-dev parcel

Loading package.json. In the script section, we add "build": "parcel build index.html --dist-dir ../dist --no-source-maps --public-url ./ --no-optimize" and remove the "main" attribute.

Create an index.html file in the browser folder and write the following code:

<!-- browser/index.html -->
<!doctype html>
<html lang="en">

<head>
  <meta charset="utf-8">
  <title>Hello World!</title>
  <meta http-equiv="Content-Security-Policy" content="script-src 'self' 'unsafe-inline';" />
  <link rel="stylesheet" type="text/css" href="./styles.css">
</head>

<body>
  <h1>Hello World!</h1>
  <p>Current theme source: <strong id="theme-source">System</strong></p>
  <p><button id="toggle-dark-mode">Toggle Theme Color</button></p>

  <script src="./render.ts"></script>
</body>

</html>

Creating a styles.css file in the browser folder and writing the following code in it:

/* browser/styles.css */
@media (prefers-color-scheme: dark) {
  body { background: #333; color: white; }
}

@media (prefers-color-scheme: light) {
  body { background: #ddd; color: black; }
}

Adding a render.ts file to the browser folder and writing the following code in it:

const toggleDarkMode = document.getElementById("toggle-dark-mode");
const themeSource = document.getElementById("theme-source");

if (themeSource && toggleDarkMode) {
  toggleDarkMode.addEventListener("click", async () => {
    // @ts-expect-error
    const isDarkMode = await window.electronAPI.toggle();

    themeSource.innerHTML = isDarkMode ? "Dark" : "Light";
    toggleDarkMode.innerHTML = `Toggle ${!isDarkMode ? "Dark" : "Light"} Mode`;
  });
}

To 'compile' the TypeScript file with Parcel, we will add a .parcelrc file in the folder and write the following:

// browser/.parcelrc
{
  "extends": "@parcel/config-default",
  "transformers": {
    "*.ts": ["@parcel/transformer-typescript-tsc"]
  }
}

Start

We go back to the project folder and edit the scripts and main fields in the package.json file:

// package.json
{
  "main": "./dist/main.js",
  "scripts": {
    "start": "npm run build --prefix ./electron && npm run build --prefix ./browser && electron ."
  }
}

In the .gitignore file, we add the dist and .parcel-cache folders, and in the command line, we execute:

npm start

After starting the application, a dist folder will appear in the project folder, containing all the code of the Electron application.

I have created repositories for this project on GitHub.


If you have an opinion or questions about the article, don't hesitate to share them.

Join the open discussion on GitHub.