├── .github └── workflows │ └── ci.yml ├── .gitignore ├── .npmrc ├── .vscode ├── launch.json └── settings.json ├── LICENSE ├── README.md ├── electron-builder.config.js ├── eslint.config.mjs ├── logo.png ├── package.json ├── pnpm-lock.yaml ├── renovate.json ├── src ├── common │ └── types.ts ├── main │ ├── app.controller.ts │ ├── app.service.ts │ ├── index.ts │ ├── main.window.ts │ └── tests │ │ └── unit.spec.ts ├── preload │ ├── index.d.ts │ └── index.ts └── render │ ├── App.vue │ ├── api │ └── index.ts │ ├── assets │ └── logo.png │ ├── components │ └── HelloWorld.vue │ ├── index.html │ ├── main.ts │ ├── plugins │ ├── index.ts │ └── ipc.ts │ ├── public │ └── favicon.ico │ ├── tests │ └── unit.spec.ts │ └── vite-env.d.ts ├── tests └── e2e.spec.ts ├── tsconfig.json ├── tsconfig.main.json └── vite.config.mts /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | pull_request: 9 | branches: 10 | - main 11 | 12 | jobs: 13 | lint: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v4 17 | 18 | - name: Install pnpm 19 | uses: pnpm/action-setup@v4.1.0 20 | 21 | - name: Set node 22 | uses: actions/setup-node@v4 23 | with: 24 | node-version: 22.x 25 | cache: pnpm 26 | 27 | - name: Install 28 | run: pnpm i 29 | 30 | - name: Lint 31 | run: pnpm run lint 32 | 33 | test: 34 | runs-on: ${{ matrix.os }} 35 | 36 | timeout-minutes: 10 37 | 38 | strategy: 39 | matrix: 40 | node_version: [18.x, 20.x] 41 | os: [ubuntu-latest, windows-latest] # , macos-latest] 42 | fail-fast: false 43 | 44 | steps: 45 | - uses: actions/checkout@v4 46 | 47 | - name: Install pnpm 48 | uses: pnpm/action-setup@v4.1.0 49 | 50 | - name: Set node version to ${{ matrix.node_version }} 51 | uses: actions/setup-node@v4 52 | with: 53 | node-version: ${{ matrix.node_version }} 54 | cache: pnpm 55 | 56 | - name: Install 57 | run: pnpm i 58 | 59 | - name: Build 60 | run: pnpm run build 61 | 62 | - name: Test 63 | run: pnpm run test 64 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store 3 | dist 4 | dist-ssr 5 | *.local 6 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | node-linker=hoisted -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "name": "Debug with npm", 6 | "type": "node", 7 | "request": "launch", 8 | "cwd": "${workspaceRoot}", 9 | "runtimeArgs": [ 10 | "run-script", 11 | "debug" 12 | ], 13 | "runtimeExecutable": "npm", 14 | "console": "integratedTerminal" 15 | } 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "cSpell.words": [ 3 | "anatine", 4 | "bytenode", 5 | "deine", 6 | "kolorist", 7 | "middlewares", 8 | "nsis", 9 | "outdir", 10 | "outfile", 11 | "postuninstall", 12 | "vitest" 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020-present, ArcherGu 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | logo 3 |

4 | 5 | # ⚡Vite + Electron & Doubleshot Template 6 | 7 | This template is used to build vite + electron projects. Build with [Doubleshot](https://github.com/Doubleshotjs/doubleshot), crazy fast! 8 | 9 | 🎉 [Doubleshot](https://github.com/Doubleshotjs/doubleshot) is a whole new set of tools to help you quickly build and start a node backend or electron main process. 10 | 11 | This template is based on a small framework [einf](https://github.com/ArcherGu/einf) that I wrote myself, which may not be complete, if you want to apply to production, you can use the templates with integrated nest.js: 12 | 13 | - [Vue.js template](https://github.com/ArcherGu/fast-vite-nestjs-electron) 14 | - [React template](https://github.com/ArcherGu/vite-react-nestjs-electron) 15 | - [Svelte.js template](https://github.com/ArcherGu/vite-svelte-nestjs-electron) 16 | 17 | ## Features 18 | 19 | - 🔨 [vite-plugin-doubleshot](https://github.com/archergu/doubleshot/tree/main/packages/plugin-vite#readme) to run/build electron main process. 20 |
21 | 22 | - 😎 Controllers/Services ipc communication, powered by Typescript [decorators](https://www.typescriptlang.org/docs/handbook/decorators.html). 23 |
24 | 25 | - ⚡ Rendering process is powered by [Vite](https://vite.io/). 26 |
27 | 28 | - ⏩ Quick start and build, powered by [tsup](https://tsup.egoist.sh/) and [electron-builder](https://www.electron.build/) integrated in [@doubleshot/builder](https://github.com/Doubleshotjs/doubleshot/tree/main/packages/builder) 29 | 30 | ## Motivation 31 | 32 | In the past, I've been building desktop clients with [vue](https://v3.vuejs.org/) + [vue-cli-plugin-electron-builder](https://github.com/nklayman/vue-cli-plugin-electron-builder), and they work very well. But as the project volume grows, webpack-based build patterns become slower and slower. 33 | 34 | The advent of [vite](https://vitejs.dev/) and [esbuild](https://esbuild.github.io/) greatly improved the development experience and made me feel lightning fast ⚡. 35 | 36 | It took me a little time to extract this template and thank you for using it. 37 | 38 | ## How to use 39 | 40 | - Click the [Use this template](https://github.com/ArcherGu/fast-vite-electron/generate) button (you must be logged in) or just clone this repo. 41 | - In the project folder: 42 | 43 | ```bash 44 | # install dependencies 45 | yarn # npm install 46 | 47 | # run in developer mode 48 | yarn dev # npm run dev 49 | 50 | # build 51 | yarn build # npm run build 52 | ``` 53 | 54 | ## Note for PNPM 55 | 56 | In order to use with `pnpm`, you'll need to adjust your `.npmrc` to use any one the following approaches in order for your dependencies to be bundled correctly (ref: [#6389](https://github.com/electron-userland/electron-builder/issues/6289#issuecomment-1042620422)): 57 | 58 | ``` 59 | node-linker=hoisted 60 | ``` 61 | 62 | ``` 63 | public-hoist-pattern=* 64 | ``` 65 | 66 | ``` 67 | shamefully-hoist=true 68 | ``` 69 | 70 | ## Relative 71 | 72 | My blog post: 73 | 74 | - [极速 DX Vite + Electron + esbuild](https://archergu.me/posts/vite-electron-esbuild) 75 | - [用装饰器给 Electron 提供一个基础 API 框架](https://archergu.me/posts/electron-decorators) 76 | -------------------------------------------------------------------------------- /electron-builder.config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @type {import('electron-builder').Configuration} 3 | * @see https://www.electron.build/configuration/configuration 4 | */ 5 | const config = { 6 | directories: { 7 | output: 'dist/electron', 8 | }, 9 | publish: null, 10 | npmRebuild: false, 11 | files: [ 12 | 'dist/main/**/*', 13 | 'dist/preload/**/*', 14 | 'dist/render/**/*', 15 | ], 16 | } 17 | 18 | module.exports = config 19 | -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import lightwing from '@lightwing/eslint-config' 2 | 3 | export default lightwing( 4 | { 5 | ignores: [ 6 | 'dist', 7 | 'node_modules', 8 | '*.svelte', 9 | '*.snap', 10 | '*.d.ts', 11 | 'coverage', 12 | 'js_test', 13 | 'local-data', 14 | ], 15 | }, 16 | { 17 | rules: { 18 | 'node/prefer-global/process': 'off', 19 | }, 20 | }, 21 | ) 22 | -------------------------------------------------------------------------------- /logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ArcherGu/fast-vite-electron/85c1135eb94e1c448b91a7b9edd603f9db89027b/logo.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "fast-vite-electron", 3 | "version": "0.0.1", 4 | "packageManager": "pnpm@10.11.0", 5 | "description": "Vite + Electron with esbuild, so fast! ⚡", 6 | "main": "dist/main/index.js", 7 | "scripts": { 8 | "dev": "rimraf dist && vite", 9 | "debug": "rimraf dist && vite -- --dsb-debug", 10 | "build": "rimraf dist && vite build", 11 | "lint": "eslint .", 12 | "lint:fix": "eslint . --fix", 13 | "test": "npm run test:main && npm run test:render", 14 | "test:render": "vitest run -r src/render --passWithNoTests", 15 | "test:main": "vitest run -r src/main --passWithNoTests", 16 | "test:e2e": "vitest run", 17 | "postinstall": "electron-builder install-app-deps", 18 | "postuninstall": "electron-builder install-app-deps" 19 | }, 20 | "dependencies": { 21 | "einf": "^1.5.3", 22 | "vue": "^3.5.13" 23 | }, 24 | "devDependencies": { 25 | "@lightwing/eslint-config": "1.0.117", 26 | "@vitejs/plugin-vue": "5.2.4", 27 | "@vue/compiler-sfc": "3.5.16", 28 | "@vue/test-utils": "2.4.6", 29 | "electron": "36.3.2", 30 | "electron-builder": "26.0.12", 31 | "eslint": "9.28.0", 32 | "happy-dom": "17.5.6", 33 | "lint-staged": "16.1.0", 34 | "playwright": "1.52.0", 35 | "rimraf": "6.0.1", 36 | "simple-git-hooks": "2.13.0", 37 | "tslib": "2.8.1", 38 | "typescript": "5.8.3", 39 | "vite": "6.3.5", 40 | "vite-plugin-doubleshot": "0.0.18", 41 | "vitest": "3.1.4", 42 | "vue-tsc": "2.2.10" 43 | }, 44 | "pnpm": { 45 | "onlyBuiltDependencies": [ 46 | "@swc/core", 47 | "electron", 48 | "esbuild", 49 | "simple-git-hooks" 50 | ] 51 | }, 52 | "simple-git-hooks": { 53 | "pre-commit": "npx lint-staged" 54 | }, 55 | "lint-staged": { 56 | "*.{js,ts,tsx,vue,md,json,yml}": [ 57 | "eslint --fix" 58 | ] 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": [ 4 | "config:base" 5 | ], 6 | "automerge": true, 7 | "automergeStrategy": "squash" 8 | } 9 | -------------------------------------------------------------------------------- /src/common/types.ts: -------------------------------------------------------------------------------- 1 | export type Nullable = T | null 2 | 3 | export type Voidable = T | null | undefined 4 | -------------------------------------------------------------------------------- /src/main/app.controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller, IpcHandle, IpcSend } from 'einf' 2 | import { AppService } from './app.service' 3 | 4 | @Controller() 5 | export class AppController { 6 | constructor( 7 | private appService: AppService, 8 | ) { } 9 | 10 | @IpcSend('reply-msg') 11 | public replyMsg(msg: string) { 12 | return `${this.appService.getDelayTime()} seconds later, the main process replies to your message: ${msg}` 13 | } 14 | 15 | @IpcHandle('send-msg') 16 | public async handleSendMsg(msg: string): Promise { 17 | setTimeout(() => { 18 | this.replyMsg(msg) 19 | }, this.appService.getDelayTime() * 1000) 20 | 21 | return `The main process received your message: ${msg}` 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/main/app.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from 'einf' 2 | 3 | @Injectable() 4 | export class AppService { 5 | public getDelayTime(): number { 6 | return 2 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/main/index.ts: -------------------------------------------------------------------------------- 1 | import { createEinf } from 'einf' 2 | import { app } from 'electron' 3 | import { AppController } from './app.controller' 4 | import { createWindow } from './main.window' 5 | 6 | process.env.ELECTRON_DISABLE_SECURITY_WARNINGS = 'true' 7 | 8 | async function electronAppInit() { 9 | const isDev = !app.isPackaged 10 | app.on('window-all-closed', () => { 11 | if (process.platform !== 'darwin') 12 | app.exit() 13 | }) 14 | 15 | if (isDev) { 16 | if (process.platform === 'win32') { 17 | process.on('message', (data) => { 18 | if (data === 'graceful-exit') 19 | app.exit() 20 | }) 21 | } 22 | else { 23 | process.on('SIGTERM', () => { 24 | app.exit() 25 | }) 26 | } 27 | } 28 | } 29 | 30 | async function bootstrap() { 31 | try { 32 | await electronAppInit() 33 | 34 | await createEinf({ 35 | window: createWindow, 36 | controllers: [AppController], 37 | injects: [{ 38 | name: 'IS_DEV', 39 | inject: !app.isPackaged, 40 | }], 41 | }) 42 | } 43 | catch (error) { 44 | console.error(error) 45 | app.quit() 46 | } 47 | } 48 | 49 | bootstrap() 50 | -------------------------------------------------------------------------------- /src/main/main.window.ts: -------------------------------------------------------------------------------- 1 | import { join } from 'node:path' 2 | import { app, BrowserWindow } from 'electron' 3 | 4 | const isDev = !app.isPackaged 5 | 6 | export async function createWindow() { 7 | const win = new BrowserWindow({ 8 | width: 1024, 9 | height: 768, 10 | webPreferences: { 11 | nodeIntegration: false, 12 | contextIsolation: true, 13 | preload: join(__dirname, '../preload/index.js'), 14 | devTools: isDev, 15 | }, 16 | autoHideMenuBar: !isDev, 17 | }) 18 | 19 | const URL = isDev 20 | ? process.env.DS_RENDERER_URL 21 | : `file://${join(app.getAppPath(), 'dist/render/index.html')}` 22 | 23 | win.loadURL(URL) 24 | 25 | if (isDev) 26 | win.webContents.openDevTools() 27 | 28 | else 29 | win.removeMenu() 30 | 31 | win.on('closed', () => { 32 | win.destroy() 33 | }) 34 | 35 | return win 36 | } 37 | 38 | export async function restoreOrCreateWindow() { 39 | let window = BrowserWindow.getAllWindows().find(w => !w.isDestroyed()) 40 | 41 | if (window === undefined) 42 | window = await createWindow() 43 | 44 | if (window.isMinimized()) 45 | window.restore() 46 | 47 | window.focus() 48 | } 49 | -------------------------------------------------------------------------------- /src/main/tests/unit.spec.ts: -------------------------------------------------------------------------------- 1 | import { BrowserWindow } from 'electron' 2 | import { beforeEach, expect, it, vi } from 'vitest' 3 | import { restoreOrCreateWindow } from '../main.window' 4 | 5 | vi.mock('electron', () => { 6 | const bw = vi.fn(); 7 | 8 | (bw as any).getAllWindows = vi.fn(() => bw.mock.instances) 9 | bw.prototype.loadURL = vi.fn() 10 | bw.prototype.on = vi.fn() 11 | bw.prototype.destroy = vi.fn() 12 | bw.prototype.isDestroyed = vi.fn() 13 | bw.prototype.isMinimized = vi.fn() 14 | bw.prototype.focus = vi.fn() 15 | bw.prototype.restore = vi.fn() 16 | bw.prototype.maximize = vi.fn() 17 | bw.prototype.removeMenu = vi.fn() 18 | 19 | return { 20 | BrowserWindow: bw, 21 | app: { 22 | isPackaged: true, 23 | getAppPath: vi.fn(() => 'path'), 24 | }, 25 | } 26 | }) 27 | 28 | vi.mock('../bootstrap', () => ({ 29 | bootstrap: vi.fn(), 30 | destroy: vi.fn(), 31 | })) 32 | 33 | beforeEach(() => { 34 | vi.clearAllMocks() 35 | }) 36 | 37 | it('should create new window', async () => { 38 | const { mock } = vi.mocked(BrowserWindow) 39 | expect(mock.instances).toHaveLength(0) 40 | 41 | await restoreOrCreateWindow() 42 | expect(mock.instances).toHaveLength(1) 43 | expect(mock.instances[0].loadURL).toHaveBeenCalledOnce() 44 | expect(mock.instances[0].loadURL).toHaveBeenCalledWith(expect.stringMatching(/index\.html$/)) 45 | }) 46 | 47 | it('should restore existing window', async () => { 48 | const { mock } = vi.mocked(BrowserWindow) 49 | 50 | // Create Window and minimize it 51 | await restoreOrCreateWindow() 52 | expect(mock.instances).toHaveLength(1) 53 | const appWindow = vi.mocked(mock.instances[0]) 54 | appWindow.isMinimized.mockReturnValueOnce(true) 55 | 56 | await restoreOrCreateWindow() 57 | expect(mock.instances).toHaveLength(1) 58 | expect(appWindow.restore).toHaveBeenCalledOnce() 59 | }) 60 | 61 | it('should create new window if previous was destroyed', async () => { 62 | const { mock } = vi.mocked(BrowserWindow) 63 | 64 | // Create Window and destroy it 65 | await restoreOrCreateWindow() 66 | expect(mock.instances).toHaveLength(1) 67 | const appWindow = vi.mocked(mock.instances[0]) 68 | appWindow.isDestroyed.mockReturnValueOnce(true) 69 | 70 | await restoreOrCreateWindow() 71 | expect(mock.instances).toHaveLength(2) 72 | }) 73 | -------------------------------------------------------------------------------- /src/preload/index.d.ts: -------------------------------------------------------------------------------- 1 | import type { IpcRenderer } from 'electron' 2 | 3 | declare global { 4 | interface Window { 5 | ipcRenderer: IpcRenderer 6 | } 7 | } 8 | 9 | export { } 10 | -------------------------------------------------------------------------------- /src/preload/index.ts: -------------------------------------------------------------------------------- 1 | import { contextBridge, ipcRenderer } from 'electron' 2 | 3 | contextBridge.exposeInMainWorld( 4 | 'ipcRenderer', 5 | { 6 | invoke: ipcRenderer.invoke.bind(ipcRenderer), 7 | on: ipcRenderer.on.bind(ipcRenderer), 8 | removeAllListeners: ipcRenderer.removeAllListeners.bind(ipcRenderer), 9 | }, 10 | ) 11 | -------------------------------------------------------------------------------- /src/render/App.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 9 | 10 | 27 | -------------------------------------------------------------------------------- /src/render/api/index.ts: -------------------------------------------------------------------------------- 1 | import { ipcInstance } from '@render/plugins' 2 | 3 | export function sendMsgToMainProcess(msg: string) { 4 | return ipcInstance.send('send-msg', msg) 5 | } 6 | -------------------------------------------------------------------------------- /src/render/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ArcherGu/fast-vite-electron/85c1135eb94e1c448b91a7b9edd603f9db89027b/src/render/assets/logo.png -------------------------------------------------------------------------------- /src/render/components/HelloWorld.vue: -------------------------------------------------------------------------------- 1 | 33 | 34 |