How to create an Electron app with Vite and minimal boilerplate
Rafael Beraldo
Posted on July 6, 2022
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
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
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
:
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
:
...
+ "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:
# 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:
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
:
...
"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. 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
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
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()]
})
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"
},
...
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
Posted on July 6, 2022
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.