├── .gitignore ├── LICENSE ├── README.md ├── electron-builder.json ├── package-lock.json ├── package.json ├── scripts ├── build.js ├── dev-server.js └── private │ └── tsc.js ├── src ├── main │ ├── main.ts │ ├── preload.ts │ ├── static │ │ └── .gitkeep │ └── tsconfig.json └── renderer │ ├── App.vue │ ├── assets │ ├── vite.svg │ └── vue.svg │ ├── components │ └── HelloWorld.vue │ ├── index.html │ ├── main.ts │ ├── style.css │ ├── tsconfig.json │ └── typings │ ├── electron.d.ts │ └── shims-vue.d.ts └── vite.config.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | build 4 | 5 | .vscode 6 | .idea -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Deluze 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. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 | # Electron Vue Template 4 | 5 | image 6 | 7 | A simple starter template for a **Vue3** + **Electron** TypeScript based application, including **ViteJS** and **Electron Builder**. 8 |
9 | 10 | ## About 11 | 12 | This template utilizes [ViteJS](https://vitejs.dev) for building and serving your (Vue powered) front-end process, it provides Hot Reloads (HMR) to make development fast and easy ⚡ 13 | 14 | Building the Electron (main) process is done with [Electron Builder](https://www.electron.build/), which makes your application easily distributable and supports cross-platform compilation 😎 15 | 16 | ## Getting started 17 | 18 | Click the green **Use this template** button on top of the repository, and clone your own newly created repository. 19 | 20 | **Or..** 21 | 22 | Clone this repository: `git clone git@github.com:Deluze/electron-vue-template.git` 23 | 24 | 25 | ### Install dependencies ⏬ 26 | 27 | ```bash 28 | npm install 29 | ``` 30 | 31 | ### Start developing ⚒️ 32 | 33 | ```bash 34 | npm run dev 35 | ``` 36 | 37 | ## Additional Commands 38 | 39 | ```bash 40 | npm run dev # starts application with hot reload 41 | npm run build # builds application, distributable files can be found in "dist" folder 42 | 43 | # OR 44 | 45 | npm run build:win # uses windows as build target 46 | npm run build:mac # uses mac as build target 47 | npm run build:linux # uses linux as build target 48 | ``` 49 | 50 | Optional configuration options can be found in the [Electron Builder CLI docs](https://www.electron.build/cli.html). 51 | ## Project Structure 52 | 53 | ```bash 54 | - scripts/ # all the scripts used to build or serve your application, change as you like. 55 | - src/ 56 | - main/ # Main thread (Electron application source) 57 | - renderer/ # Renderer thread (VueJS application source) 58 | ``` 59 | 60 | ## Using static files 61 | 62 | If you have any files that you want to copy over to the app directory after installation, you will need to add those files in your `src/main/static` directory. 63 | 64 | Files in said directory are only accessible to the `main` process, similar to `src/renderer/assets` only being accessible to the `renderer` process. Besides that, the concept is the same as to what you're used to in your other front-end projects. 65 | 66 | #### Referencing static files from your main process 67 | 68 | ```ts 69 | /* Assumes src/main/static/myFile.txt exists */ 70 | 71 | import {app} from 'electron'; 72 | import {join} from 'path'; 73 | import {readFileSync} from 'fs'; 74 | 75 | const path = join(app.getAppPath(), 'static', 'myFile.txt'); 76 | const buffer = readFileSync(path); 77 | ``` 78 | -------------------------------------------------------------------------------- /electron-builder.json: -------------------------------------------------------------------------------- 1 | { 2 | "appId": "com.electron.app", 3 | "directories": { 4 | "output": "dist" 5 | }, 6 | 7 | "nsis": { 8 | "oneClick": false, 9 | "perMachine": false, 10 | "allowToChangeInstallationDirectory": true, 11 | "shortcutName": "Electron App" 12 | }, 13 | "win": { 14 | "target": "nsis" 15 | }, 16 | "linux": { 17 | "target": ["snap"] 18 | }, 19 | "files": [ 20 | { 21 | "from": "build/main", 22 | "to": "main", 23 | "filter": ["**/*"] 24 | }, 25 | { 26 | "from": "build/renderer", 27 | "to": "renderer", 28 | "filter": ["**/*"] 29 | }, 30 | { 31 | "from": "src/main/static", 32 | "to": "static", 33 | "filter": ["**/*"] 34 | }, 35 | "!build", 36 | "!dist", 37 | "!scripts" 38 | ] 39 | } 40 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "electron-vue-template", 3 | "version": "0.1.0", 4 | "description": "A minimal Electron + Vue application", 5 | "main": "main/main.js", 6 | "scripts": { 7 | "dev": "node scripts/dev-server.js", 8 | "build": "node scripts/build.js && electron-builder", 9 | "build:win": "node scripts/build.js && electron-builder --win", 10 | "build:mac": "node scripts/build.js && electron-builder --mac", 11 | "build:linux": "node scripts/build.js && electron-builder --linux" 12 | }, 13 | "repository": "https://github.com/deluze/electron-vue-template", 14 | "author": { 15 | "name": "Deluze", 16 | "url": "https://github.com/Deluze" 17 | }, 18 | "devDependencies": { 19 | "@vitejs/plugin-vue": "^4.4.1", 20 | "chalk": "^4.1.2", 21 | "chokidar": "^3.5.3", 22 | "electron": "^32.1.2", 23 | "electron-builder": "^25.1.6", 24 | "typescript": "^5.2.2", 25 | "vite": "^4.5.0" 26 | }, 27 | "dependencies": { 28 | "vue": "^3.3.8" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /scripts/build.js: -------------------------------------------------------------------------------- 1 | const Path = require('path'); 2 | const Chalk = require('chalk'); 3 | const FileSystem = require('fs'); 4 | const Vite = require('vite'); 5 | const compileTs = require('./private/tsc'); 6 | 7 | function buildRenderer() { 8 | return Vite.build({ 9 | configFile: Path.join(__dirname, '..', 'vite.config.js'), 10 | base: './', 11 | mode: 'production' 12 | }); 13 | } 14 | 15 | function buildMain() { 16 | const mainPath = Path.join(__dirname, '..', 'src', 'main'); 17 | return compileTs(mainPath); 18 | } 19 | 20 | FileSystem.rmSync(Path.join(__dirname, '..', 'build'), { 21 | recursive: true, 22 | force: true, 23 | }) 24 | 25 | console.log(Chalk.blueBright('Transpiling renderer & main...')); 26 | 27 | Promise.allSettled([ 28 | buildRenderer(), 29 | buildMain(), 30 | ]).then(() => { 31 | console.log(Chalk.greenBright('Renderer & main successfully transpiled! (ready to be built with electron-builder)')); 32 | }); 33 | -------------------------------------------------------------------------------- /scripts/dev-server.js: -------------------------------------------------------------------------------- 1 | process.env.NODE_ENV = 'development'; 2 | 3 | const Vite = require('vite'); 4 | const ChildProcess = require('child_process'); 5 | const Path = require('path'); 6 | const Chalk = require('chalk'); 7 | const Chokidar = require('chokidar'); 8 | const Electron = require('electron'); 9 | const compileTs = require('./private/tsc'); 10 | const FileSystem = require('fs'); 11 | const { EOL } = require('os'); 12 | 13 | let viteServer = null; 14 | let electronProcess = null; 15 | let electronProcessLocker = false; 16 | let rendererPort = 0; 17 | 18 | async function startRenderer() { 19 | viteServer = await Vite.createServer({ 20 | configFile: Path.join(__dirname, '..', 'vite.config.js'), 21 | mode: 'development', 22 | }); 23 | 24 | return viteServer.listen(); 25 | } 26 | 27 | async function startElectron() { 28 | if (electronProcess) { // single instance lock 29 | return; 30 | } 31 | 32 | try { 33 | await compileTs(Path.join(__dirname, '..', 'src', 'main')); 34 | } catch { 35 | console.log(Chalk.redBright('Could not start Electron because of the above typescript error(s).')); 36 | electronProcessLocker = false; 37 | return; 38 | } 39 | 40 | const args = [ 41 | Path.join(__dirname, '..', 'build', 'main', 'main.js'), 42 | rendererPort, 43 | ]; 44 | electronProcess = ChildProcess.spawn(Electron, args); 45 | electronProcessLocker = false; 46 | 47 | electronProcess.stdout.on('data', data => { 48 | if (data == EOL) { 49 | return; 50 | } 51 | 52 | process.stdout.write(Chalk.blueBright(`[electron] `) + Chalk.white(data.toString())) 53 | }); 54 | 55 | electronProcess.stderr.on('data', data => 56 | process.stderr.write(Chalk.blueBright(`[electron] `) + Chalk.white(data.toString())) 57 | ); 58 | 59 | electronProcess.on('exit', () => stop()); 60 | } 61 | 62 | function restartElectron() { 63 | if (electronProcess) { 64 | electronProcess.removeAllListeners('exit'); 65 | electronProcess.kill(); 66 | electronProcess = null; 67 | } 68 | 69 | if (!electronProcessLocker) { 70 | electronProcessLocker = true; 71 | startElectron(); 72 | } 73 | } 74 | 75 | function copyStaticFiles() { 76 | copy('static'); 77 | } 78 | 79 | /* 80 | The working dir of Electron is build/main instead of src/main because of TS. 81 | tsc does not copy static files, so copy them over manually for dev server. 82 | */ 83 | function copy(path) { 84 | FileSystem.cpSync( 85 | Path.join(__dirname, '..', 'src', 'main', path), 86 | Path.join(__dirname, '..', 'build', 'main', path), 87 | { recursive: true } 88 | ); 89 | } 90 | 91 | function stop() { 92 | viteServer.close(); 93 | process.exit(); 94 | } 95 | 96 | async function start() { 97 | console.log(`${Chalk.greenBright('=======================================')}`); 98 | console.log(`${Chalk.greenBright('Starting Electron + Vite Dev Server...')}`); 99 | console.log(`${Chalk.greenBright('=======================================')}`); 100 | 101 | const devServer = await startRenderer(); 102 | rendererPort = devServer.config.server.port; 103 | 104 | copyStaticFiles(); 105 | startElectron(); 106 | 107 | const path = Path.join(__dirname, '..', 'src', 'main'); 108 | Chokidar.watch(path, { 109 | cwd: path, 110 | }).on('change', (path) => { 111 | console.log(Chalk.blueBright(`[electron] `) + `Change in ${path}. reloading... 🚀`); 112 | 113 | if (path.startsWith(Path.join('static', '/'))) { 114 | copy(path); 115 | } 116 | 117 | restartElectron(); 118 | }); 119 | } 120 | 121 | start(); 122 | -------------------------------------------------------------------------------- /scripts/private/tsc.js: -------------------------------------------------------------------------------- 1 | const ChildProcess = require('child_process'); 2 | const Chalk = require('chalk'); 3 | 4 | function compile(directory) { 5 | return new Promise((resolve, reject) => { 6 | const tscProcess = ChildProcess.exec('tsc', { 7 | cwd: directory, 8 | }); 9 | 10 | tscProcess.stdout.on('data', data => 11 | process.stdout.write(Chalk.yellowBright(`[tsc] `) + Chalk.white(data.toString())) 12 | ); 13 | 14 | tscProcess.on('exit', exitCode => { 15 | if (exitCode > 0) { 16 | reject(exitCode); 17 | } else { 18 | resolve(); 19 | } 20 | }); 21 | }); 22 | } 23 | 24 | module.exports = compile; 25 | -------------------------------------------------------------------------------- /src/main/main.ts: -------------------------------------------------------------------------------- 1 | import {app, BrowserWindow, ipcMain, session} from 'electron'; 2 | import {join} from 'path'; 3 | 4 | function createWindow () { 5 | const mainWindow = new BrowserWindow({ 6 | width: 800, 7 | height: 600, 8 | webPreferences: { 9 | preload: join(__dirname, 'preload.js'), 10 | nodeIntegration: false, 11 | contextIsolation: true, 12 | } 13 | }); 14 | 15 | if (process.env.NODE_ENV === 'development') { 16 | const rendererPort = process.argv[2]; 17 | mainWindow.loadURL(`http://localhost:${rendererPort}`); 18 | } 19 | else { 20 | mainWindow.loadFile(join(app.getAppPath(), 'renderer', 'index.html')); 21 | } 22 | } 23 | 24 | app.whenReady().then(() => { 25 | createWindow(); 26 | 27 | session.defaultSession.webRequest.onHeadersReceived((details, callback) => { 28 | callback({ 29 | responseHeaders: { 30 | ...details.responseHeaders, 31 | 'Content-Security-Policy': ['script-src \'self\''] 32 | } 33 | }) 34 | }) 35 | 36 | app.on('activate', function () { 37 | // On macOS it's common to re-create a window in the app when the 38 | // dock icon is clicked and there are no other windows open. 39 | if (BrowserWindow.getAllWindows().length === 0) { 40 | createWindow(); 41 | } 42 | }); 43 | }); 44 | 45 | app.on('window-all-closed', function () { 46 | if (process.platform !== 'darwin') app.quit() 47 | }); 48 | 49 | ipcMain.on('message', (event, message) => { 50 | console.log(message); 51 | }) -------------------------------------------------------------------------------- /src/main/preload.ts: -------------------------------------------------------------------------------- 1 | import {contextBridge, ipcRenderer} from 'electron'; 2 | 3 | contextBridge.exposeInMainWorld('electronAPI', { 4 | sendMessage: (message: string) => ipcRenderer.send('message', message) 5 | }) 6 | -------------------------------------------------------------------------------- /src/main/static/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Deluze/electron-vue-template/fceca5ec8ce9ac5a2922dddc1b732b7ba043a63a/src/main/static/.gitkeep -------------------------------------------------------------------------------- /src/main/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2015", 4 | "module": "commonjs", 5 | "strict": true, 6 | "esModuleInterop": true, 7 | "skipLibCheck": true, 8 | "forceConsistentCasingInFileNames": true, 9 | "outDir": "../../build/main", 10 | "allowJs": true, 11 | "noImplicitAny": false, 12 | }, 13 | "exclude": ["static"] 14 | } 15 | -------------------------------------------------------------------------------- /src/renderer/App.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 21 | 22 | 36 | -------------------------------------------------------------------------------- /src/renderer/assets/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/renderer/assets/vue.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/renderer/components/HelloWorld.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 28 | 29 | 34 | -------------------------------------------------------------------------------- /src/renderer/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Vite + Vue template 6 | 7 | 8 |
9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/renderer/main.ts: -------------------------------------------------------------------------------- 1 | import { createApp } from 'vue' 2 | import './style.css'; 3 | import App from './App.vue' 4 | 5 | const app = createApp(App); 6 | 7 | app.mount('#app'); 8 | -------------------------------------------------------------------------------- /src/renderer/style.css: -------------------------------------------------------------------------------- 1 | :root { 2 | font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif; 3 | line-height: 1.5; 4 | font-weight: 400; 5 | 6 | color-scheme: light dark; 7 | color: rgba(255, 255, 255, 0.87); 8 | background-color: #242424; 9 | 10 | font-synthesis: none; 11 | text-rendering: optimizeLegibility; 12 | -webkit-font-smoothing: antialiased; 13 | -moz-osx-font-smoothing: grayscale; 14 | -webkit-text-size-adjust: 100%; 15 | } 16 | 17 | a { 18 | font-weight: 500; 19 | color: #646cff; 20 | text-decoration: inherit; 21 | } 22 | a:hover { 23 | color: #535bf2; 24 | } 25 | 26 | a { 27 | font-weight: 500; 28 | color: #646cff; 29 | text-decoration: inherit; 30 | } 31 | a:hover { 32 | color: #535bf2; 33 | } 34 | 35 | body { 36 | margin: 0; 37 | display: flex; 38 | place-items: center; 39 | min-width: 320px; 40 | min-height: 100vh; 41 | } 42 | 43 | h1 { 44 | font-size: 3.2em; 45 | line-height: 1.1; 46 | } 47 | 48 | button { 49 | border-radius: 8px; 50 | border: 1px solid transparent; 51 | padding: 0.6em 1.2em; 52 | font-size: 1em; 53 | font-weight: 500; 54 | font-family: inherit; 55 | background-color: #1a1a1a; 56 | cursor: pointer; 57 | transition: border-color 0.25s; 58 | } 59 | button:hover { 60 | border-color: #646cff; 61 | } 62 | button:focus, 63 | button:focus-visible { 64 | outline: 4px auto -webkit-focus-ring-color; 65 | } 66 | 67 | .card { 68 | padding: 2em; 69 | } 70 | 71 | #app { 72 | max-width: 1280px; 73 | margin: 0 auto; 74 | padding: 2rem; 75 | text-align: center; 76 | } 77 | 78 | @media (prefers-color-scheme: light) { 79 | :root { 80 | color: #213547; 81 | background-color: #ffffff; 82 | } 83 | a:hover { 84 | color: #747bff; 85 | } 86 | button { 87 | background-color: #f9f9f9; 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/renderer/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "useDefineForClassFields": true, 5 | "module": "esnext", 6 | "moduleResolution": "node", 7 | "strict": true, 8 | "jsx": "preserve", 9 | "sourceMap": true, 10 | "resolveJsonModule": true, 11 | "esModuleInterop": true, 12 | "lib": ["esnext", "dom"], 13 | "types": ["vite/client"] 14 | }, 15 | "include": ["./**/*.ts", "./**/*.d.ts", "./**/*.tsx", "./**/*.vue"], 16 | } 17 | 18 | -------------------------------------------------------------------------------- /src/renderer/typings/electron.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Should match main/preload.ts for typescript support in renderer 3 | */ 4 | export default interface ElectronApi { 5 | sendMessage: (message: string) => void 6 | } 7 | 8 | declare global { 9 | interface Window { 10 | electronAPI: ElectronApi, 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/renderer/typings/shims-vue.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.vue' { 2 | import type { DefineComponent } from 'vue' 3 | 4 | const component: DefineComponent<{}, {}, any> 5 | export default component 6 | } 7 | -------------------------------------------------------------------------------- /vite.config.js: -------------------------------------------------------------------------------- 1 | const Path = require('path'); 2 | const vuePlugin = require('@vitejs/plugin-vue') 3 | 4 | const { defineConfig } = require('vite'); 5 | 6 | /** 7 | * https://vitejs.dev/config 8 | */ 9 | const config = defineConfig({ 10 | root: Path.join(__dirname, 'src', 'renderer'), 11 | publicDir: 'public', 12 | server: { 13 | port: 8080, 14 | }, 15 | open: false, 16 | build: { 17 | outDir: Path.join(__dirname, 'build', 'renderer'), 18 | emptyOutDir: true, 19 | }, 20 | plugins: [vuePlugin()], 21 | }); 22 | 23 | module.exports = config; 24 | --------------------------------------------------------------------------------