How to create an Electron app with Vite and minimal boilerplate
Tags: javascript, electron, tutorial, vite
Published: 2022-07-06 18:07:03

> Este artigo também está disponível em [português](https://dev.to/rafaelberaldo/como-criar-um-app-electron-usando-vite-52d6).

So you want to wrap your new [Vite](https://vitejs.dev/) application with [Electron](https://www.electronjs.org/), but you don't want to use other's boilerplate for it. Let's understand how it works and build our own.

## Create a Vite app

We're going to use [Vite](https://vitejs.dev/)'s preset as the base file structure of our project. Start with the command:

```bash
$ npm create vite@latest
```

Then follow Vite's instructions. In this example I've created the React ``react-ts`` preset. But it also works with Vuejs and probably any other.

## Add Electron

Now we add [Electron](https://www.electronjs.org/) to our project.

```bash
$ npm i -D electron@latest
```

Then we create an ``electron`` folder in project root, and two files ``main.js`` and ``preload.js``. Our folder structure is like this:

```
project-root/
├── electron/
│   ├── main.js 
│   └── preload.js
├── src/
│   └── ...
├── index.html
├── package.json
├── vite.config.ts
└── ...
```

## Electron entry file

Electron needs an entry file to work, let's edit our ``electron/main.js``:

```js
const { app, BrowserWindow, shell } = require('electron')
const { join } = require('path')

if (!app.requestSingleInstanceLock()) {
  app.quit()
  process.exit(0)
}

let win = null

async function createWindow () {
  win = new BrowserWindow({
    title: 'Main window',
    width: 1024,
    height: 768,
    webPreferences: {
      preload: join(__dirname, '../electron/preload.js'),
      nodeIntegration: true
    }
  })

  if (app.isPackaged) {
    // win.removeMenu()
    win.loadFile(join(__dirname, '../dist/index.html'))
  } else {
    // Vite's dev server
    win.loadURL('http://localhost:5173')
    win.webContents.openDevTools()
  }

app.whenReady().then(createWindow)

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

app.on('second-instance', () => {
  if (win) {
    // Focus on the main window if the user tried to open another
    if (win.isMinimized()) win.restore()
    win.focus()
  }
})

app.on('activate', () => {
  const allWindows = BrowserWindow.getAllWindows()
  if (allWindows.length) {
    allWindows[0].focus()
  } else {
    createWindow()
  }
})
```

The ``preload.js`` file will stay blank for this tutorial. But you probably gonna use it in a real world app.

Add it to the ``package.json``:

```diff
  ...
+ "main": "electron/main.js",
  "scripts": {
    "dev": "vite",
    "build": "tsc && vite build",
    "preview": "vite preview",
+   "electron:dev": "electron ."
  },
  ...
```

You can now test your app, run Vite's dev server in a terminal instance, and Electron in another:

```bash
# First instance
$ npm run dev

# Second instance
$ npm run electron:dev
```

You see that it's completely decoupled, HMR works just fine since we're opening the Vite's dev server URL.

> But I want it to run in a single instance/command!

For that we can create a custom script. Create a new file ``scripts/dev.mjs`` with:

```js
import { spawn } from 'child_process'
import { createServer } from 'vite'
import electron from 'electron'

const server = await createServer({ configFile: 'vite.config.ts' })

spawn(electron, ['.'], { stdio: 'inherit' }).once('exit', process.exit)

await server.listen()
```

And update dev script in ``package.json``:

```diff
...
"scripts": {
  "dev": "vite",
  "build": "tsc && vite build",
  "preview": "vite preview",
- "electron:dev": "electron ."
+ "electron:dev": "node scripts/dev.mjs"
},
...
```

You can now start dev server with a single ``npm run electron:dev`` command.

Note that in our example you won't have live reload nor TypeScript for the ``electron/main.js``. For that you could do something [like this](https://github.com/electron-vite/electron-vite-boilerplate/blob/v1.0.0/scripts/watch.mjs). IMO it's not necessary for most cases.

## Building the app

Our dev server is working just fine. Now we have to be able to build the app.

We're gonna use [electron-builder](https://www.electron.build/). Add it to the project:

```bash
$ npm i -D electron-builder
```

And we need a config file, let's create a file ``electron-builder.yaml`` in project root:

```yaml
# https://www.electron.build/configuration/configuration

appId: your.app.id
asar: true
directories:
  output: release/${version}

files:
- dist
- electron

mac:
  artifactName: "${productName}_${version}.${ext}"
  target:
  - dmg
win:
  target:
  - target: nsis
    arch:
    - x64
  artifactName: "${productName}_${version}.${ext}"
nsis:
  oneClick: false
  perMachine: false
  allowToChangeInstallationDirectory: true
  deleteAppDataOnUninstall: false
```

Add a ``base`` property to Vite's config file in ``vite.config.ts``:

```js
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'

// https://vitejs.dev/config/
export default defineConfig({
  base: './',
  plugins: [react()]
})
```

This adds a ``./`` prefix on all assets, necessary to work within Electron's ``file://`` protocol.

Now add a build script to ``package.json``:

```diff
...
"scripts": {
  "dev": "vite",
  "build": "tsc && vite build",
  "preview": "vite preview",
  "electron:dev": "node scripts/dev.mjs",
+ "electron:build": "npm run build && electron-builder"
},
...
```

As you can see, we're gonna run Vite's build first, then electron-builder.

Vite's build is located in ``dist`` directory, Electron's build is located in ``release`` directory. Make sure to add both to ``.gitignore``.

You can now build your app with the ``npm run electron:build`` command.

Aaaand voi là! You have your Electron app working with Vite!

Source code of this tutorial: https://github.com/rafaberaldo/vite-electron

## References

- https://dev.to/olyno/how-to-create-an-electron-application-with-vite-im
- https://github.com/electron-vite/electron-vite-boilerplate
- https://github.com/electron-vite/vite-plugin-electron
- https://vitejs.dev/
- https://www.electronjs.org/