├── .github
└── workflows
│ └── ci.yml
├── .gitignore
├── .npmrc
├── LICENSE
├── README.md
├── electron-builder.config.js
├── eslint.config.mjs
├── logo.png
├── package.json
├── pnpm-lock.yaml
├── renovate.json
├── src
├── main
│ ├── app.controller.ts
│ ├── app.module.ts
│ ├── app.service.ts
│ └── index.ts
├── preload
│ ├── index.d.ts
│ └── index.ts
└── render
│ ├── App.css
│ ├── App.tsx
│ ├── assets
│ └── logo.png
│ ├── index.html
│ ├── main.tsx
│ ├── public
│ └── vite.svg
│ └── vite-env.d.ts
├── tsconfig.json
├── tsconfig.node.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 | build:
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 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | pnpm-debug.log*
8 | lerna-debug.log*
9 |
10 | node_modules
11 | dist
12 | dist-ssr
13 | *.local
14 |
15 | # Editor directories and files
16 | .vscode/*
17 | !.vscode/extensions.json
18 | .idea
19 | .DS_Store
20 | *.suo
21 | *.ntvs*
22 | *.njsproj
23 | *.sln
24 | *.sw?
25 |
--------------------------------------------------------------------------------
/.npmrc:
--------------------------------------------------------------------------------
1 | node-linker=hoisted
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2024-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 |
3 |
4 |
5 | # ⚡Vite + React + Electron + Nestjs Template
6 |
7 | This template is used to build [vite](https://vitejs.dev/) + [electron](https://www.electronjs.org/) + [nestjs](https://nestjs.com/) 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 is a react version of the template, you can also use:
12 |
13 | - [Vue.js template](https://github.com/ArcherGu/fast-vite-nestjs-electron)
14 | - [Svelte.js template](https://github.com/ArcherGu/vite-svelte-nestjs-electron)
15 |
16 | ## Introduce
17 |
18 | This is a template based on my repo: [fast-vite-electron](https://github.com/ArcherGu/fast-vite-electron). In the main process, I integrated nestjs. In the main process, you can build your code just as you would write a nestjs backend. Desktop clients built from this template can quickly split the electron when you need to switch to B/S.
19 |
20 | ## Features
21 |
22 | - 🔨 [vite-plugin-doubleshot](https://github.com/archergu/doubleshot/tree/main/packages/plugin-vite#readme) to run/build electron main process or node backend.
23 |
24 |
25 | - 🛻 An electron ipc transport for [nestjs](https://nestjs.com/) that provides simple ipc communication.
26 |
27 |
28 | - 🪟 An electron module for [nestjs](https://nestjs.com/) to launch electron windows.
29 |
30 |
31 | - ⏩ 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)
32 |
33 | ## How to use
34 |
35 | - Click the [Use this template](https://github.com/ArcherGu/fast-vite-electron/generate) button (you must be logged in) or just clone this repo.
36 | - In the project folder:
37 |
38 | ```bash
39 | # install dependencies
40 | yarn # npm install
41 |
42 | # run in developer mode
43 | yarn dev # npm run dev
44 |
45 | # build
46 | yarn build # npm run build
47 | ```
48 |
49 | ## Note for PNPM
50 |
51 | 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)):
52 |
53 | ```
54 | node-linker=hoisted
55 | ```
56 |
57 | ```
58 | public-hoist-pattern=*
59 | ```
60 |
61 | ```
62 | shamefully-hoist=true
63 | ```
64 |
65 | ## Relative
66 |
67 | My blog post:
68 |
69 | - [极速 DX Vite + Electron + esbuild](https://archergu.me/posts/vite-electron-esbuild)
70 | - [用装饰器给 Electron 提供一个基础 API 框架](https://archergu.me/posts/electron-decorators)
71 |
--------------------------------------------------------------------------------
/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 | react: true,
16 | },
17 | )
18 |
--------------------------------------------------------------------------------
/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ArcherGu/vite-react-nestjs-electron/0f492c46f18ff1d479eb08a716cabea3c2d8dc58/logo.png
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "vite-react-nestjs-electron",
3 | "version": "0.0.1",
4 | "private": true,
5 | "packageManager": "pnpm@10.12.1",
6 | "description": "Vite + React + Electron with Doubleshot, so fast! ⚡",
7 | "main": "dist/main/index.js",
8 | "scripts": {
9 | "dev": "rimraf dist && vite",
10 | "debug": "rimraf dist && vite -- --dsb-debug",
11 | "build": "rimraf dist && vite build",
12 | "lint": "eslint .",
13 | "lint:fix": "eslint . --fix",
14 | "postinstall": "electron-builder install-app-deps"
15 | },
16 | "dependencies": {
17 | "@doubleshot/nest-electron": "^0.2.6",
18 | "@nestjs/common": "^11.0.9",
19 | "@nestjs/core": "^11.0.9",
20 | "@nestjs/microservices": "^11.0.9",
21 | "react": "^19.0.0",
22 | "react-dom": "^19.0.0",
23 | "reflect-metadata": "^0.2.2",
24 | "rxjs": "^7.8.1"
25 | },
26 | "devDependencies": {
27 | "@lightwing/eslint-config": "^1.0.104",
28 | "@types/react": "^19.0.8",
29 | "@types/react-dom": "^19.0.3",
30 | "@vitejs/plugin-react": "^4.3.4",
31 | "electron": "^36.0.0",
32 | "electron-builder": "^26.0.0",
33 | "eslint": "^9.20.1",
34 | "lint-staged": "^16.0.0",
35 | "rimraf": "^6.0.1",
36 | "simple-git-hooks": "^2.11.1",
37 | "typescript": "^5.7.3",
38 | "vite": "^6.1.0",
39 | "vite-plugin-doubleshot": "^0.0.18"
40 | },
41 | "pnpm": {
42 | "onlyBuiltDependencies": [
43 | "@nestjs/core",
44 | "electron"
45 | ]
46 | },
47 | "simple-git-hooks": {
48 | "pre-commit": "npx lint-staged"
49 | },
50 | "lint-staged": {
51 | "*.{js,ts,tsx,vue,md,json,yml}": [
52 | "eslint --fix"
53 | ]
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/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/main/app.controller.ts:
--------------------------------------------------------------------------------
1 | import type { BrowserWindow } from 'electron'
2 | import type { Observable } from 'rxjs'
3 | import { IpcHandle, Window } from '@doubleshot/nest-electron'
4 | import { Controller } from '@nestjs/common'
5 | import { Payload } from '@nestjs/microservices'
6 | import { of } from 'rxjs'
7 | import { AppService } from './app.service'
8 |
9 | @Controller()
10 | export class AppController {
11 | constructor(
12 | private readonly appService: AppService,
13 | @Window() private readonly mainWin: BrowserWindow,
14 | ) { }
15 |
16 | @IpcHandle('msg')
17 | public handleSendMsg(@Payload() msg: string): Observable {
18 | const { webContents } = this.mainWin
19 | webContents.send('reply-msg', 'this is msg from webContents.send')
20 | return of(`The main process received your message: ${msg} at time: ${this.appService.getTime()}`)
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/src/main/app.module.ts:
--------------------------------------------------------------------------------
1 | import { join } from 'node:path'
2 | import { ElectronModule } from '@doubleshot/nest-electron'
3 | import { Module } from '@nestjs/common'
4 | import { app, BrowserWindow } from 'electron'
5 | import { AppController } from './app.controller'
6 | import { AppService } from './app.service'
7 |
8 | @Module({
9 | imports: [ElectronModule.registerAsync({
10 | useFactory: async () => {
11 | const isDev = !app.isPackaged
12 | const win = new BrowserWindow({
13 | width: 1024,
14 | height: 768,
15 | autoHideMenuBar: true,
16 | webPreferences: {
17 | contextIsolation: true,
18 | preload: join(__dirname, '../preload/index.js'),
19 | },
20 | })
21 |
22 | win.on('closed', () => {
23 | win.destroy()
24 | })
25 |
26 | const URL = isDev
27 | ? process.env.DS_RENDERER_URL
28 | : `file://${join(app.getAppPath(), 'dist/render/index.html')}`
29 |
30 | win.loadURL(URL)
31 |
32 | return { win }
33 | },
34 | })],
35 | controllers: [AppController],
36 | providers: [AppService],
37 | })
38 | export class AppModule { }
39 |
--------------------------------------------------------------------------------
/src/main/app.service.ts:
--------------------------------------------------------------------------------
1 | import { Injectable } from '@nestjs/common'
2 |
3 | @Injectable()
4 | export class AppService {
5 | public getTime(): number {
6 | return new Date().getTime()
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/src/main/index.ts:
--------------------------------------------------------------------------------
1 | import type { MicroserviceOptions } from '@nestjs/microservices'
2 | import { ElectronIpcTransport } from '@doubleshot/nest-electron'
3 | import { NestFactory } from '@nestjs/core'
4 | import { app } from 'electron'
5 | import { AppModule } from './app.module'
6 |
7 | process.env.ELECTRON_DISABLE_SECURITY_WARNINGS = 'true'
8 |
9 | async function electronAppInit() {
10 | const isDev = !app.isPackaged
11 | app.on('window-all-closed', () => {
12 | if (process.platform !== 'darwin')
13 | app.quit()
14 | })
15 |
16 | if (isDev) {
17 | if (process.platform === 'win32') {
18 | process.on('message', (data) => {
19 | if (data === 'graceful-exit')
20 | app.quit()
21 | })
22 | }
23 | else {
24 | process.on('SIGTERM', () => {
25 | app.quit()
26 | })
27 | }
28 | }
29 |
30 | await app.whenReady()
31 | }
32 |
33 | async function bootstrap() {
34 | try {
35 | await electronAppInit()
36 |
37 | const nestApp = await NestFactory.createMicroservice(
38 | AppModule,
39 | {
40 | strategy: new ElectronIpcTransport('IpcTransport'),
41 | },
42 | )
43 |
44 | await nestApp.listen()
45 | }
46 | catch (error) {
47 | // eslint-disable-next-line no-console
48 | console.log(error)
49 | app.quit()
50 | }
51 | }
52 |
53 | bootstrap()
54 |
--------------------------------------------------------------------------------
/src/preload/index.d.ts:
--------------------------------------------------------------------------------
1 | declare global {
2 | interface Window {
3 | electron: {
4 | sendMsg: (msg: string) => Promise
5 | onReplyMsg: (cb: (msg: string) => any) => void
6 | }
7 | }
8 | }
9 |
10 | export { }
11 |
--------------------------------------------------------------------------------
/src/preload/index.ts:
--------------------------------------------------------------------------------
1 | import { contextBridge, ipcRenderer } from 'electron'
2 |
3 | contextBridge.exposeInMainWorld(
4 | 'electron',
5 | {
6 | sendMsg: (msg: string): Promise => ipcRenderer.invoke('msg', msg),
7 | onReplyMsg: (cb: (msg: string) => any) => ipcRenderer.on('reply-msg', (e, msg: string) => {
8 | cb(msg)
9 | }),
10 | },
11 | )
12 |
--------------------------------------------------------------------------------
/src/render/App.css:
--------------------------------------------------------------------------------
1 | #root {
2 | font-family: Avenir, Helvetica, Arial, sans-serif;
3 | -webkit-font-smoothing: antialiased;
4 | -moz-osx-font-smoothing: grayscale;
5 | text-align: center;
6 | color: #2c3e50;
7 | margin-top: 60px;
8 | padding: 0px;
9 | }
10 |
11 | .logo {
12 | width: 400px;
13 | height: 2;
14 | border-radius: 1rem;
15 | box-shadow:
16 | 0 0 #0000,
17 | 0 25px 50px -12px rgba(0, 0, 0, 0.25);
18 | }
19 |
--------------------------------------------------------------------------------
/src/render/App.tsx:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from 'react'
2 | import logo from './assets/logo.png'
3 | import './App.css'
4 |
5 | const { sendMsg: sendMsgToMainProcess, onReplyMsg } = window.electron
6 |
7 | function App() {
8 | const [log, setLog] = useState('')
9 | const [msg, setMsg] = useState('')
10 |
11 | function handleInput(e: React.ChangeEvent) {
12 | setMsg(e.target.value)
13 | }
14 |
15 | async function sendMsg() {
16 | try {
17 | setLog(log => `${log}[render]: ${msg} \n`)
18 | const data = await sendMsgToMainProcess(msg)
19 | setLog(log => `${log}[main]: ${data} \n`)
20 | }
21 | catch (error) {
22 | console.error(error)
23 | }
24 | }
25 |
26 | useEffect(() => {
27 | onReplyMsg((msg: string) => {
28 | setLog(log => `${log}[main]: ${msg} \n`)
29 | })
30 | }, [])
31 |
32 | return (
33 | <>
34 |
35 |
36 | Vite + React + Electron & Esbuild
37 |
38 |
39 |
40 |
41 |
42 |
45 |
46 | >
47 | )
48 | }
49 |
50 | export default App
51 |
--------------------------------------------------------------------------------
/src/render/assets/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ArcherGu/vite-react-nestjs-electron/0f492c46f18ff1d479eb08a716cabea3c2d8dc58/src/render/assets/logo.png
--------------------------------------------------------------------------------
/src/render/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Fast Vite React Electron App
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/src/render/main.tsx:
--------------------------------------------------------------------------------
1 | import ReactDOM from 'react-dom/client'
2 | import App from './App.tsx'
3 |
4 | ReactDOM.createRoot(document.getElementById('root')!).render(
5 | ,
6 | )
7 |
--------------------------------------------------------------------------------
/src/render/public/vite.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/render/vite-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ES2020",
4 | "jsx": "react-jsx",
5 | "lib": [
6 | "ES2020",
7 | "DOM",
8 | "DOM.Iterable"
9 | ],
10 | "useDefineForClassFields": true,
11 | "emitDecoratorMetadata": true,
12 | "experimentalDecorators": true,
13 | "baseUrl": ".",
14 | "module": "ESNext",
15 | "moduleResolution": "bundler",
16 | "paths": {
17 | "@render/*": [
18 | "src/render/*"
19 | ],
20 | "@main/*": [
21 | "src/main/*"
22 | ]
23 | },
24 | "resolveJsonModule": true,
25 | "types": [
26 | "./src/preload"
27 | ],
28 | "allowImportingTsExtensions": true,
29 | "strict": false,
30 | "noFallthroughCasesInSwitch": true,
31 | "noUnusedLocals": true,
32 | "noUnusedParameters": true,
33 | "noEmit": true,
34 | "isolatedModules": true,
35 | "skipLibCheck": true
36 | },
37 | "references": [
38 | {
39 | "path": "./tsconfig.node.json"
40 | }
41 | ],
42 | "include": [
43 | "src"
44 | ]
45 | }
46 |
--------------------------------------------------------------------------------
/tsconfig.node.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "composite": true,
4 | "module": "ESNext",
5 | "moduleResolution": "bundler",
6 | "strict": true,
7 | "allowSyntheticDefaultImports": true,
8 | "skipLibCheck": true
9 | },
10 | "include": [
11 | "vite.config.mts"
12 | ]
13 | }
14 |
--------------------------------------------------------------------------------
/vite.config.mts:
--------------------------------------------------------------------------------
1 | import { join } from 'node:path'
2 | import react from '@vitejs/plugin-react'
3 | import { defineConfig } from 'vite'
4 | import { VitePluginDoubleshot } from 'vite-plugin-doubleshot'
5 |
6 | // https://vitejs.dev/config/
7 | export default defineConfig({
8 | root: join(__dirname, 'src/render'),
9 | plugins: [
10 | react(),
11 | VitePluginDoubleshot({
12 | type: 'electron',
13 | main: 'dist/main/index.js',
14 | entry: 'src/main/index.ts',
15 | outDir: 'dist/main',
16 | external: ['electron'],
17 | electron: {
18 | build: {
19 | config: './electron-builder.config.js',
20 | },
21 | preload: {
22 | entry: 'src/preload/index.ts',
23 | outDir: 'dist/preload',
24 | },
25 | },
26 | }),
27 | ],
28 | resolve: {
29 | alias: {
30 | '@render': join(__dirname, 'src/render'),
31 | '@main': join(__dirname, 'src/main'),
32 | },
33 | },
34 | base: './',
35 | build: {
36 | outDir: join(__dirname, 'dist/render'),
37 | emptyOutDir: true,
38 | },
39 | })
40 |
--------------------------------------------------------------------------------