├── .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 |
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 |
6 |
7 |
8 |
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 |
35 | {{ title }}
36 |
37 |
38 |
39 |
40 |
43 |
44 |
45 |
46 |
47 |
--------------------------------------------------------------------------------
/src/render/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Fast Vite Electron App
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/src/render/main.ts:
--------------------------------------------------------------------------------
1 | import { createApp } from 'vue'
2 | import App from './App.vue'
3 |
4 | createApp(App).mount('#app')
5 |
--------------------------------------------------------------------------------
/src/render/plugins/index.ts:
--------------------------------------------------------------------------------
1 | export * from './ipc'
2 |
--------------------------------------------------------------------------------
/src/render/plugins/ipc.ts:
--------------------------------------------------------------------------------
1 | import { getCurrentInstance, onUnmounted, toRaw } from 'vue'
2 |
3 | const { ipcRenderer } = window
4 |
5 | interface IpcInstance {
6 | send: (target: string, ...args: any[]) => Promise
7 | on: (event: string, callback: (...args: any[]) => void) => void
8 | }
9 |
10 | export const ipcInstance: IpcInstance = {
11 | send: (target, ...args) => {
12 | const payloads: any[] = args.map(e => toRaw(e))
13 | return ipcRenderer.invoke(target.toString(), ...payloads)
14 | },
15 | on: (event, callback) => {
16 | ipcRenderer.on(event.toString(), (e, ...args) => {
17 | callback(...args)
18 | })
19 |
20 | // Use tryOnUnmounted if use @vueuse https://vueuse.org/shared/tryOnUnmounted/
21 | if (getCurrentInstance()) {
22 | onUnmounted(() => {
23 | ipcRenderer.removeAllListeners(event.toString())
24 | })
25 | }
26 | },
27 | }
28 |
29 | export function useIpc() {
30 | return ipcInstance
31 | }
32 |
--------------------------------------------------------------------------------
/src/render/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ArcherGu/fast-vite-electron/85c1135eb94e1c448b91a7b9edd603f9db89027b/src/render/public/favicon.ico
--------------------------------------------------------------------------------
/src/render/tests/unit.spec.ts:
--------------------------------------------------------------------------------
1 | import { mount } from '@vue/test-utils'
2 | import { expect, it, vi } from 'vitest'
3 | import HelloWorld from '../components/HelloWorld.vue'
4 |
5 | vi.mock('../api', () => ({
6 | sendMsgToMainProcess: vi.fn(),
7 | }))
8 |
9 | vi.mock('../plugins/ipc', () => ({
10 | useIpc: vi.fn(() => ({
11 | on: vi.fn(),
12 | })),
13 | }))
14 |
15 | /**
16 | * @vitest-environment happy-dom
17 | */
18 | it('helloWorld component', async () => {
19 | expect(HelloWorld).toBeTruthy()
20 | const wrapper = mount(HelloWorld)
21 |
22 | const msgInput = wrapper.get('input')
23 |
24 | const msg = 'msg from unit test'
25 | await msgInput.setValue(msg)
26 | expect(msgInput.element.value).toBe(msg)
27 | })
28 |
--------------------------------------------------------------------------------
/src/render/vite-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
--------------------------------------------------------------------------------
/tests/e2e.spec.ts:
--------------------------------------------------------------------------------
1 | import type { ElectronApplication } from 'playwright'
2 | import { _electron as electron } from 'playwright'
3 | import { afterAll, beforeAll, expect, it } from 'vitest'
4 |
5 | let electronApp: ElectronApplication
6 |
7 | beforeAll(async () => {
8 | electronApp = await electron.launch({
9 | args: ['./dist/main/index.js'],
10 | executablePath: './dist/electron/win-unpacked/fast-vite-electron.exe',
11 | })
12 | })
13 |
14 | afterAll(async () => {
15 | await electronApp.close()
16 | })
17 |
18 | it('main window state', async () => {
19 | const windowState: { isVisible: boolean, isDevToolsOpened: boolean, isCrashed: boolean }
20 | = await electronApp.evaluate(({ BrowserWindow }) => {
21 | const mainWindow = BrowserWindow.getAllWindows()[0]
22 |
23 | const getState = () => ({
24 | isVisible: mainWindow.isVisible(),
25 | isDevToolsOpened: mainWindow.webContents.isDevToolsOpened(),
26 | isCrashed: mainWindow.webContents.isCrashed(),
27 | })
28 |
29 | return new Promise((resolve) => {
30 | if (mainWindow.isVisible())
31 | resolve(getState())
32 | else
33 | mainWindow.once('ready-to-show', () => setTimeout(() => resolve(getState()), 0))
34 | })
35 | })
36 |
37 | expect(windowState.isCrashed, 'App was crashed').toBeFalsy()
38 | expect(windowState.isVisible, 'Main window was not visible').toBeTruthy()
39 | expect(windowState.isDevToolsOpened, 'DevTools was opened').toBeFalsy()
40 | })
41 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ESNext",
4 | "jsx": "preserve",
5 | "lib": [
6 | "esnext",
7 | "dom"
8 | ],
9 | "emitDecoratorMetadata": true,
10 | "experimentalDecorators": true,
11 | "baseUrl": ".",
12 | "module": "ESNext",
13 | "moduleResolution": "node",
14 | "paths": {
15 | "@render/*": [
16 | "src/render/*"
17 | ],
18 | "@main/*": [
19 | "src/main/*"
20 | ],
21 | "@common/*": [
22 | "src/common/*"
23 | ]
24 | },
25 | "resolveJsonModule": true,
26 | "types": [
27 | "vite/client",
28 | "./src/preload"
29 | ],
30 | "strict": false,
31 | "noImplicitAny": false,
32 | "noEmit": true,
33 | "sourceMap": true,
34 | "esModuleInterop": true,
35 | "skipLibCheck": true
36 | },
37 | "include": [
38 | "src/**/*.ts",
39 | "src/**/*.d.ts",
40 | "src/**/*.tsx",
41 | "src/**/*.vue"
42 | ]
43 | }
44 |
--------------------------------------------------------------------------------
/tsconfig.main.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.json",
3 | "compilerOptions": {
4 | "module": "CommonJS"
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/vite.config.mts:
--------------------------------------------------------------------------------
1 | ///
2 | import { join } from 'node:path'
3 | import vue from '@vitejs/plugin-vue'
4 | import { defineConfig } from 'vite'
5 | import { VitePluginDoubleshot } from 'vite-plugin-doubleshot'
6 |
7 | // https://vitejs.dev/config/
8 | export default defineConfig({
9 | root: join(__dirname, 'src/render'),
10 | plugins: [
11 | vue(),
12 | VitePluginDoubleshot({
13 | type: 'electron',
14 | main: 'dist/main/index.js',
15 | entry: 'src/main/index.ts',
16 | outDir: 'dist/main',
17 | external: ['electron'],
18 | electron: {
19 | build: {
20 | config: './electron-builder.config.js',
21 | },
22 | preload: {
23 | entry: 'src/preload/index.ts',
24 | outDir: 'dist/preload',
25 | },
26 | },
27 | }),
28 | ],
29 | resolve: {
30 | alias: {
31 | '@render': join(__dirname, 'src/render'),
32 | '@main': join(__dirname, 'src/main'),
33 | '@common': join(__dirname, 'src/common'),
34 | },
35 | },
36 | base: './',
37 | build: {
38 | outDir: join(__dirname, 'dist/render'),
39 | emptyOutDir: true,
40 | },
41 | test: { // e2e tests
42 | include: ['./tests/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'],
43 | testTimeout: 30_000,
44 | hookTimeout: 30_000,
45 | },
46 | })
47 |
--------------------------------------------------------------------------------