How to create an Electron app with Vite and minimal boilerplate

rafaelberaldo

Rafael Beraldo

Posted on July 6, 2022

How to create an Electron app with Vite and minimal boilerplate

Este artigo também está disponível em português.

So you want to wrap your new Vite application with Electron, 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's preset as the base file structure of our project. Start with the command:

$ npm create vite@latest
Enter fullscreen mode Exit fullscreen mode

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 to our project.

$ npm i -D electron@latest
Enter fullscreen mode Exit fullscreen mode

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
└── ...
Enter fullscreen mode Exit fullscreen mode

Electron entry file

Electron needs an entry file to work, let's edit our electron/main.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()
  }
})
Enter fullscreen mode Exit fullscreen mode

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:

  ...
+ "main": "electron/main.js",
  "scripts": {
    "dev": "vite",
    "build": "tsc && vite build",
    "preview": "vite preview",
+   "electron:dev": "electron ."
  },
  ...
Enter fullscreen mode Exit fullscreen mode

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

# First instance
$ npm run dev

# Second instance
$ npm run electron:dev
Enter fullscreen mode Exit fullscreen mode

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:

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()
Enter fullscreen mode Exit fullscreen mode

And update dev script in package.json:

...
"scripts": {
  "dev": "vite",
  "build": "tsc && vite build",
  "preview": "vite preview",
- "electron:dev": "electron ."
+ "electron:dev": "node scripts/dev.mjs"
},
...
Enter fullscreen mode Exit fullscreen mode

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. 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. Add it to the project:

$ npm i -D electron-builder
Enter fullscreen mode Exit fullscreen mode

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

# 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
Enter fullscreen mode Exit fullscreen mode

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

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

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

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

Now add a build script to package.json:

...
"scripts": {
  "dev": "vite",
  "build": "tsc && vite build",
  "preview": "vite preview",
  "electron:dev": "node scripts/dev.mjs",
+ "electron:build": "npm run build && electron-builder"
},
...
Enter fullscreen mode Exit fullscreen mode

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

💖 💪 🙅 🚩
rafaelberaldo
Rafael Beraldo

Posted on July 6, 2022

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related