How to build An Electron App by esbuild with hot reload functionality?
Tags: electron, typescript, esbuild, react
Published: 2023-01-30 09:15:25

- sample repo: https://github.com/sprout2000/electron-react-esbuild

## A script to run esbuild

- `esbuild.ts`

```typescript
import type { BuildOptions } from 'esbuild';

import { build, context } from 'esbuild';
import { htmlPlugin } from '@craftamap/esbuild-plugin-html';

/**
 * Process according to the value of
 * the NODE_ENV environment variable
 */
const isDev = process.env.NODE_ENV === 'development';

// Common settings for build or watch
const common: BuildOptions = {
  outdir: 'dist', // output destination
  bundle: true, // bundle, of course.
  minify: !isDev,
  sourcemap: isDev,
  define: {
    // Requires a type declaration file (later mention)
    DEBUG: isDev ? 'true' : 'false',
  },
};

// Configuration for main process
const main: BuildOptions = {
  // Import common settings
  ...common,
  // main process and preload script
  entryPoints: ['src/main.ts', 'src/preload.ts'],
  // Build for Node.js environment
  platform: 'node',
  // you will get run-time error without the following:
  external: ['electron'],
};

// Configuration for renderer process
const renderer: BuildOptions = {
  ...common,
  // Entry file for React app
  entryPoints: ['src/web/index.tsx'],
  // Build for Web
  platform: 'browser',
  // Required for htmlPlugin
  metafile: true,
  plugins: [
    htmlPlugin({
      files: [
        {
          entryPoints: ['src/web/index.tsx'],
          filename: 'index.html',
          htmlTemplate: 'src/web/index.html',
        },
      ],
    }),
  ],
};

// Function to execute during development
const watch = async () => {
  await (await context({ ...main })).watch();
  await (await context({ ...renderer })).watch();
};

// Function to execute during production build
const prod = async () => {
  build({ ...main });
  build({ ...renderer });
};

// Executing scripts
isDev ? watch() : prod();
```

## Create a type definition file

- `src/@types/Debug.d.ts`

```typescript
declare const DEBUG: boolean;
```

## Update your `package.json`

Use [electronmon](https://github.com/catdad/electronmon) to restart the Electron app when the main process code changes, and reload WebView for the renderer process.

```jsonc
{
  // Specify the main process JavaScript file output by esbuild.
  "main": "dist/main.js",
  "scripts": {
    "dev": "rimraf dist && run-p dev:esbuild dev:electron",
    // Run `tsc` without output before build as esbuild does not type check.
    "build": "tsc && cross-env NODE_ENV=\"production\" ts-node ./esbuild.ts",
    "dev:esbuild": "cross-env NODE_ENV=\"development\" ts-node ./esbuild.ts",
    "dev:electron": "wait-on dist/main.js dist/index.html && electronmon ."
  }
}
```

_You will need to install [rimraf](https://www.npmjs.com/package/rimraf), [npm-run-all](https://www.npmjs.com/package/npm-run-all), [cross-env](https://www.npmjs.com/package/cross-env) and [wait-on](https://www.npmjs.com/package/wait-on)._

- A sample for `tsconfig.json`

```jsonc
{
  "compilerOptions": {
    "target": "ESNext",
    "module": "ESNext",
    "moduleResolution": "Node",
    "esModuleInterop": true,
    "isolatedModules": true, // <-- required
    "resolveJsonModule": true,
    "lib": ["DOM", "ESNext"],
    "jsx": "react-jsx",
    "strict": true,
    "noEmit": true // <-- required
  },
  "include": ["src"],
  "ts-node": {
    "compilerOptions": {
      "module": "CommonJS"
    }
  }
}
```

## Assumed directory structure

```shell
% tree
.
├── esbuild.ts
├── package.json
├── src
│   ├── @types
│   │   └── Debug.d.ts
│   ├── main.ts
│   ├── preload.ts
│   └── web
│       ├── App.css
│       ├── App.tsx
│       ├── index.html
│       └── index.tsx
└── tsconfig.json

4 directories, 10 files
```