Repository: caoxiemeihao/vite-react-electron Branch: main Commit: 190492b3f240 Files: 49 Total size: 53.9 KB Directory structure: gitextract_5rcbumob/ ├── .github/ │ ├── ISSUE_TEMPLATE/ │ │ ├── bug_report.md │ │ └── help_wanted.md │ ├── PULL_REQUEST_TEMPLATE.md │ ├── dependabot.yml │ └── workflows/ │ ├── build.yml │ └── ci.yml ├── .gitignore ├── .npmrc ├── .playwright.config.txt ├── .vite.config.flat.txt ├── .vscode/ │ ├── .debug.script.mjs │ ├── extensions.json │ ├── launch.json │ ├── settings.json │ └── tasks.json ├── LICENSE ├── README.md ├── README.zh-CN.md ├── build/ │ └── icon.icns ├── electron/ │ ├── electron-env.d.ts │ ├── main/ │ │ ├── index.ts │ │ └── update.ts │ └── preload/ │ └── index.ts ├── electron-builder.json ├── index.html ├── package.json ├── postcss.config.cjs ├── src/ │ ├── App.css │ ├── App.tsx │ ├── components/ │ │ └── update/ │ │ ├── Modal/ │ │ │ ├── index.tsx │ │ │ └── modal.css │ │ ├── Progress/ │ │ │ ├── index.tsx │ │ │ └── progress.css │ │ ├── README.md │ │ ├── README.zh-CN.md │ │ ├── index.tsx │ │ └── update.css │ ├── demos/ │ │ ├── ipc.ts │ │ └── node.ts │ ├── index.css │ ├── main.tsx │ ├── type/ │ │ └── electron-updater.d.ts │ └── vite-env.d.ts ├── tailwind.config.js ├── test/ │ └── e2e.spec.ts ├── tsconfig.json ├── tsconfig.node.json ├── vite.config.ts └── vitest.config.ts ================================================ FILE CONTENTS ================================================ ================================================ FILE: .github/ISSUE_TEMPLATE/bug_report.md ================================================ --- name: 🐞 Bug report about: Create a report to help us improve title: "[Bug] the title of bug report" labels: bug assignees: '' --- #### Describe the bug ================================================ FILE: .github/ISSUE_TEMPLATE/help_wanted.md ================================================ --- name: 🥺 Help wanted about: Confuse about the use of electron-vue-vite title: "[Help] the title of help wanted report" labels: help wanted assignees: '' --- #### Describe the problem you confuse ================================================ FILE: .github/PULL_REQUEST_TEMPLATE.md ================================================ ### Description ### What is the purpose of this pull request? - [ ] Bug fix - [ ] New Feature - [ ] Documentation update - [ ] Other ================================================ FILE: .github/dependabot.yml ================================================ # To get started with Dependabot version updates, you'll need to specify which # package ecosystems to update and where the package manifests are located. # Please see the documentation for all configuration options: # https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates version: 2 updates: - package-ecosystem: "npm" # See documentation for possible values directory: "/" # Location of package manifests schedule: interval: "monthly" ================================================ FILE: .github/workflows/build.yml ================================================ name: Build on: push: branches: [main] paths-ignore: - "**.md" - "**.spec.js" - ".idea" - ".vscode" - ".dockerignore" - "Dockerfile" - ".gitignore" - ".github/**" - "!.github/workflows/build.yml" jobs: build: runs-on: ${{ matrix.os }} strategy: matrix: os: [macos-latest, ubuntu-latest, windows-latest] steps: - name: Checkout Code uses: actions/checkout@v3 - name: Setup Node.js uses: actions/setup-node@v3 with: node-version: 18 - name: Install Dependencies run: npm install - name: Build Release Files run: npm run build env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Upload Artifact uses: actions/upload-artifact@v3 with: name: release_on_${{ matrix. os }} path: release/ retention-days: 5 ================================================ FILE: .github/workflows/ci.yml ================================================ name: CI on: pull_request_target: branches: - main permissions: pull-requests: write jobs: job1: name: Check Not Allowed File Changes runs-on: ubuntu-latest outputs: markdown_change: ${{ steps.filter_markdown.outputs.change }} markdown_files: ${{ steps.filter_markdown.outputs.change_files }} steps: - name: Check Not Allowed File Changes uses: dorny/paths-filter@v2 id: filter_not_allowed with: list-files: json filters: | change: - 'package-lock.json' - 'yarn.lock' - 'pnpm-lock.yaml' # ref: https://github.com/github/docs/blob/main/.github/workflows/triage-unallowed-contributions.yml - name: Comment About Changes We Can't Accept if: ${{ steps.filter_not_allowed.outputs.change == 'true' }} uses: actions/github-script@v6 with: script: | let workflowFailMessage = "It looks like you've modified some files that we can't accept as contributions." try { const badFilesArr = [ 'package-lock.json', 'yarn.lock', 'pnpm-lock.yaml', ] const badFiles = badFilesArr.join('\n- ') const reviewMessage = `👋 Hey there spelunker. It looks like you've modified some files that we can't accept as contributions. The complete list of files we can't accept are:\n- ${badFiles}\n\nYou'll need to revert all of the files you changed in that list using [GitHub Desktop](https://docs.github.com/en/free-pro-team@latest/desktop/contributing-and-collaborating-using-github-desktop/managing-commits/reverting-a-commit) or \`git checkout origin/main \`. Once you get those files reverted, we can continue with the review process. :octocat:\n\nMore discussion:\n- https://github.com/electron-vite/electron-vite-vue/issues/192` createdComment = await github.rest.issues.createComment({ owner: context.repo.owner, repo: context.repo.repo, issue_number: context.payload.number, body: reviewMessage, }) workflowFailMessage = `${workflowFailMessage} Please see ${createdComment.data.html_url} for details.` } catch(err) { console.log("Error creating comment.", err) } core.setFailed(workflowFailMessage) - name: Check Not Linted Markdown if: ${{ always() }} uses: dorny/paths-filter@v2 id: filter_markdown with: list-files: shell filters: | change: - added|modified: '*.md' job2: name: Lint Markdown runs-on: ubuntu-latest needs: job1 if: ${{ always() && needs.job1.outputs.markdown_change == 'true' }} steps: - name: Checkout Code uses: actions/checkout@v3 with: ref: ${{ github.event.pull_request.head.sha }} - name: Lint markdown run: npx markdownlint-cli ${{ needs.job1.outputs.markdown_files }} --ignore node_modules ================================================ FILE: .gitignore ================================================ # Logs logs *.log npm-debug.log* yarn-debug.log* yarn-error.log* pnpm-debug.log* lerna-debug.log* node_modules dist dist-ssr dist-electron release *.local # Editor directories and files .vscode/.debug.env .idea .DS_Store *.suo *.ntvs* *.njsproj *.sln *.sw? #lockfile package-lock.json pnpm-lock.yaml yarn.lock /test-results/ /playwright-report/ /playwright/.cache/ ================================================ FILE: .npmrc ================================================ # For electron-builder # https://github.com/electron-userland/electron-builder/issues/6289#issuecomment-1042620422 shamefully-hoist=true # For China 🇨🇳 developers # electron_mirror=https://npmmirror.com/mirrors/electron/ ================================================ FILE: .playwright.config.txt ================================================ import type { PlaywrightTestConfig } from "@playwright/test"; /** * Read environment variables from file. * https://github.com/motdotla/dotenv */ // require('dotenv').config(); /** * See https://playwright.dev/docs/test-configuration. */ const config: PlaywrightTestConfig = { testDir: "./e2e", /* Maximum time one test can run for. */ timeout: 30 * 1000, expect: { /** * Maximum time expect() should wait for the condition to be met. * For example in `await expect(locator).toHaveText();` */ timeout: 5000, }, /* Run tests in files in parallel */ fullyParallel: true, /* Fail the build on CI if you accidentally left test.only in the source code. */ forbidOnly: !!process.env.CI, /* Retry on CI only */ retries: process.env.CI ? 2 : 0, /* Opt out of parallel tests on CI. */ workers: process.env.CI ? 1 : undefined, /* Reporter to use. See https://playwright.dev/docs/test-reporters */ reporter: "html", /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ use: { /* Maximum time each action such as `click()` can take. Defaults to 0 (no limit). */ actionTimeout: 0, /* Base URL to use in actions like `await page.goto('/')`. */ // baseURL: 'http://localhost:3000', /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ trace: "on-first-retry", }, /* Folder for test artifacts such as screenshots, videos, traces, etc. */ // outputDir: 'test-results/', /* Run your local dev server before starting the tests */ // webServer: { // command: 'npm run start', // port: 3000, // }, }; export default config; ================================================ FILE: .vite.config.flat.txt ================================================ import { rmSync } from 'node:fs' import path from 'node:path' import { defineConfig } from 'vite' import react from '@vitejs/plugin-react' import electron from 'vite-plugin-electron' import renderer from 'vite-plugin-electron-renderer' import pkg from './package.json' // https://vitejs.dev/config/ export default defineConfig(({ command }) => { rmSync('dist-electron', { recursive: true, force: true }) const isServe = command === 'serve' const isBuild = command === 'build' const sourcemap = isServe || !!process.env.VSCODE_DEBUG return { resolve: { alias: { '@': path.join(__dirname, 'src') }, }, plugins: [ react(), electron([ { // Main-Process entry file of the Electron App. entry: 'electron/main/index.ts', onstart(options) { if (process.env.VSCODE_DEBUG) { console.log(/* For `.vscode/.debug.script.mjs` */'[startup] Electron App') } else { options.startup() } }, vite: { build: { sourcemap, minify: isBuild, outDir: 'dist-electron/main', rollupOptions: { external: Object.keys('dependencies' in pkg ? pkg.dependencies : {}), }, }, }, }, { entry: 'electron/preload/index.ts', onstart(options) { // Notify the Renderer-Process to reload the page when the Preload-Scripts build is complete, // instead of restarting the entire Electron App. options.reload() }, vite: { build: { sourcemap: sourcemap ? 'inline' : undefined, // #332 minify: isBuild, outDir: 'dist-electron/preload', rollupOptions: { external: Object.keys('dependencies' in pkg ? pkg.dependencies : {}), }, }, }, } ]), // Use Node.js API in the Renderer-process renderer(), ], server: process.env.VSCODE_DEBUG && (() => { const url = new URL(pkg.debug.env.VITE_DEV_SERVER_URL) return { host: url.hostname, port: +url.port, } })(), clearScreen: false, } }) ================================================ FILE: .vscode/.debug.script.mjs ================================================ import fs from 'node:fs' import path from 'node:path' import { fileURLToPath } from 'node:url' import { createRequire } from 'node:module' import { spawn } from 'node:child_process' const pkg = createRequire(import.meta.url)('../package.json') const __dirname = path.dirname(fileURLToPath(import.meta.url)) // write .debug.env const envContent = Object.entries(pkg.debug.env).map(([key, val]) => `${key}=${val}`) fs.writeFileSync(path.join(__dirname, '.debug.env'), envContent.join('\n')) // bootstrap spawn( // TODO: terminate `npm run dev` when Debug exits. process.platform === 'win32' ? 'npm.cmd' : 'npm', ['run', 'dev'], { stdio: 'inherit', env: Object.assign(process.env, { VSCODE_DEBUG: 'true' }), }, ) ================================================ FILE: .vscode/extensions.json ================================================ { // See http://go.microsoft.com/fwlink/?LinkId=827846 // for the documentation about the extensions.json format "recommendations": [ "mrmlnc.vscode-json5" ] } ================================================ FILE: .vscode/launch.json ================================================ { // Use IntelliSense to learn about possible attributes. // Hover to view descriptions of existing attributes. // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 "version": "0.2.0", "compounds": [ { "name": "Debug App", "preLaunchTask": "Before Debug", "configurations": [ "Debug Main Process", "Debug Renderer Process" ], "presentation": { "hidden": false, "group": "", "order": 1 }, "stopAll": true } ], "configurations": [ { "name": "Debug Main Process", "type": "node", "request": "launch", "runtimeExecutable": "${workspaceRoot}/node_modules/.bin/electron", "windows": { "runtimeExecutable": "${workspaceRoot}/node_modules/.bin/electron.cmd" }, "runtimeArgs": [ "--no-sandbox", "--remote-debugging-port=9229", "." ], "envFile": "${workspaceFolder}/.vscode/.debug.env", "console": "integratedTerminal" }, { "name": "Debug Renderer Process", "port": 9229, "request": "attach", "type": "chrome", "timeout": 60000, "skipFiles": [ "/**", "${workspaceRoot}/node_modules/**", "${workspaceRoot}/dist-electron/**", // Skip files in host(VITE_DEV_SERVER_URL) "http://127.0.0.1:7777/**" ] }, ] } ================================================ FILE: .vscode/settings.json ================================================ { "typescript.tsdk": "node_modules/typescript/lib", "typescript.tsc.autoDetect": "off", "json.schemas": [ { "fileMatch": [ "/*electron-builder.json5", "/*electron-builder.json" ], "url": "https://json.schemastore.org/electron-builder" } ] } ================================================ FILE: .vscode/tasks.json ================================================ { // See https://go.microsoft.com/fwlink/?LinkId=733558 // for the documentation about the tasks.json format "version": "2.0.0", "tasks": [ { "label": "Before Debug", "type": "shell", "command": "node .vscode/.debug.script.mjs", "isBackground": true, "problemMatcher": { "owner": "typescript", "fileLocation": "relative", "pattern": { // TODO: correct "regexp" "regexp": "^([a-zA-Z]\\:\/?([\\w\\-]\/?)+\\.\\w+):(\\d+):(\\d+): (ERROR|WARNING)\\: (.*)$", "file": 1, "line": 3, "column": 4, "code": 5, "message": 6 }, "background": { "activeOnStart": true, "beginsPattern": "^.*VITE v.* ready in \\d* ms.*$", "endsPattern": "^.*\\[startup\\] Electron App.*$" } } } ] } ================================================ FILE: LICENSE ================================================ MIT License Copyright (c) 2023 草鞋没号 Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ # electron-vite-react [![awesome-vite](https://awesome.re/mentioned-badge.svg)](https://github.com/vitejs/awesome-vite) ![GitHub stars](https://img.shields.io/github/stars/caoxiemeihao/vite-react-electron?color=fa6470) ![GitHub issues](https://img.shields.io/github/issues/caoxiemeihao/vite-react-electron?color=d8b22d) ![GitHub license](https://img.shields.io/github/license/caoxiemeihao/vite-react-electron) [![Required Node.JS >= 14.18.0 || >=16.0.0](https://img.shields.io/static/v1?label=node&message=14.18.0%20||%20%3E=16.0.0&logo=node.js&color=3f893e)](https://nodejs.org/about/releases) English | [简体中文](README.zh-CN.md) ## 👀 Overview 📦 Ready out of the box 🎯 Based on the official [template-react-ts](https://github.com/vitejs/vite/tree/main/packages/create-vite/template-react-ts), project structure will be familiar to you 🌱 Easily extendable and customizable 💪 Supports Node.js API in the renderer process 🔩 Supports C/C++ native addons 🐞 Debugger configuration included 🖥 Easy to implement multiple windows ## 🛫 Quick Setup ```sh # clone the project git clone https://github.com/electron-vite/electron-vite-react.git # enter the project directory cd electron-vite-react # install dependency npm install # develop npm run dev ``` ## 🐞 Debug ![electron-vite-react-debug.gif](/electron-vite-react-debug.gif) ## 📂 Directory structure Familiar React application structure, just with `electron` folder on the top :wink: *Files in this folder will be separated from your React application and built into `dist-electron`* ```tree ├── electron Electron-related code │ ├── main Main-process source code │ └── preload Preload-scripts source code │ ├── release Generated after production build, contains executables │ └── {version} │ ├── {os}-{os_arch} Contains unpacked application executable │ └── {app_name}_{version}.{ext} Installer for the application │ ├── public Static assets └── src Renderer source code, your React application ``` ## 🔧 Additional features 1. electron-updater 👉 [see docs](src/components/update/README.md) 1. playwright ## ❔ FAQ - [C/C++ addons, Node.js modules - Pre-Bundling](https://github.com/electron-vite/vite-plugin-electron-renderer#dependency-pre-bundling) - [dependencies vs devDependencies](https://github.com/electron-vite/vite-plugin-electron-renderer#dependencies-vs-devdependencies) ================================================ FILE: README.zh-CN.md ================================================ # vite-react-electron [![awesome-vite](https://awesome.re/mentioned-badge.svg)](https://github.com/vitejs/awesome-vite) ![GitHub stars](https://img.shields.io/github/stars/caoxiemeihao/vite-react-electron?color=fa6470) ![GitHub issues](https://img.shields.io/github/issues/caoxiemeihao/vite-react-electron?color=d8b22d) ![GitHub license](https://img.shields.io/github/license/caoxiemeihao/vite-react-electron) [![Required Node.JS >= 14.18.0 || >=16.0.0](https://img.shields.io/static/v1?label=node&message=14.18.0%20||%20%3E=16.0.0&logo=node.js&color=3f893e)](https://nodejs.org/about/releases) [English](README.md) | 简体中文 ## 概述 📦 开箱即用 🎯 基于官方的 [template-react-ts](https://github.com/vitejs/vite/tree/main/packages/create-vite/template-react-ts), 低侵入性 🌱 结构清晰,可塑性强 💪 支持在渲染进程中使用 Electron、Node.js API 🔩 支持 C/C++ 模块 🖥 很容易实现多窗口 ## 快速开始 ```sh # clone the project git clone https://github.com/electron-vite/electron-vite-react.git # enter the project directory cd electron-vite-react # install dependency npm install # develop npm run dev ``` ## 调试 ![electron-vite-react-debug.gif](/electron-vite-react-debug.gif) ## 目录 *🚨 默认情况下, `electron` 文件夹下的文件将会被构建到 `dist-electron`* ```tree ├── electron Electron 源码文件夹 │ ├── main Main-process 源码 │ └── preload Preload-scripts 源码 │ ├── release 构建后生成程序目录 │ └── {version} │ ├── {os}-{os_arch} 未打包的程序(绿色运行版) │ └── {app_name}_{version}.{ext} 应用安装文件 │ ├── public 同 Vite 模板的 public └── src 渲染进程源码、React代码 ``` ## 🔧 额外的功能 1. Electron 自动更新 👉 [阅读文档](src/components/update/README.zh-CN.md) 2. Playwright 测试 ## ❔ FAQ - [C/C++ addons, Node.js modules - Pre-Bundling](https://github.com/electron-vite/vite-plugin-electron-renderer#dependency-pre-bundling) - [dependencies vs devDependencies](https://github.com/electron-vite/vite-plugin-electron-renderer#dependencies-vs-devdependencies) ## 🍵 🍰 🍣 🍟 ================================================ FILE: electron/electron-env.d.ts ================================================ /// declare namespace NodeJS { interface ProcessEnv { VSCODE_DEBUG?: 'true' /** * The built directory structure * * ```tree * ├─┬ dist-electron * │ ├─┬ main * │ │ └── index.js > Electron-Main * │ └─┬ preload * │ └── index.mjs > Preload-Scripts * ├─┬ dist * │ └── index.html > Electron-Renderer * ``` */ APP_ROOT: string /** /dist/ or /public/ */ VITE_PUBLIC: string } } ================================================ FILE: electron/main/index.ts ================================================ import { app, BrowserWindow, shell, ipcMain } from 'electron' import { createRequire } from 'node:module' import { fileURLToPath } from 'node:url' import path from 'node:path' import os from 'node:os' import { update } from './update' const require = createRequire(import.meta.url) const __dirname = path.dirname(fileURLToPath(import.meta.url)) // The built directory structure // // ├─┬ dist-electron // │ ├─┬ main // │ │ └── index.js > Electron-Main // │ └─┬ preload // │ └── index.mjs > Preload-Scripts // ├─┬ dist // │ └── index.html > Electron-Renderer // process.env.APP_ROOT = path.join(__dirname, '../..') export const MAIN_DIST = path.join(process.env.APP_ROOT, 'dist-electron') export const RENDERER_DIST = path.join(process.env.APP_ROOT, 'dist') export const VITE_DEV_SERVER_URL = process.env.VITE_DEV_SERVER_URL process.env.VITE_PUBLIC = VITE_DEV_SERVER_URL ? path.join(process.env.APP_ROOT, 'public') : RENDERER_DIST // Disable GPU Acceleration for Windows 7 if (os.release().startsWith('6.1')) app.disableHardwareAcceleration() // Set application name for Windows 10+ notifications if (process.platform === 'win32') app.setAppUserModelId(app.getName()) if (!app.requestSingleInstanceLock()) { app.quit() process.exit(0) } let win: BrowserWindow | null = null const preload = path.join(__dirname, '../preload/index.mjs') const indexHtml = path.join(RENDERER_DIST, 'index.html') async function createWindow() { win = new BrowserWindow({ title: 'Main window', icon: path.join(process.env.VITE_PUBLIC, 'favicon.ico'), webPreferences: { preload, // Warning: Enable nodeIntegration and disable contextIsolation is not secure in production // nodeIntegration: true, // Consider using contextBridge.exposeInMainWorld // Read more on https://www.electronjs.org/docs/latest/tutorial/context-isolation // contextIsolation: false, }, }) if (VITE_DEV_SERVER_URL) { // #298 win.loadURL(VITE_DEV_SERVER_URL) // Open devTool if the app is not packaged win.webContents.openDevTools() } else { win.loadFile(indexHtml) } // Test actively push message to the Electron-Renderer win.webContents.on('did-finish-load', () => { win?.webContents.send('main-process-message', new Date().toLocaleString()) }) // Make all links open with the browser, not with the application win.webContents.setWindowOpenHandler(({ url }) => { if (url.startsWith('https:')) shell.openExternal(url) return { action: 'deny' } }) // Auto update update(win) } 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() } }) // New window example arg: new windows url ipcMain.handle('open-win', (_, arg) => { const childWindow = new BrowserWindow({ webPreferences: { preload, nodeIntegration: true, contextIsolation: false, }, }) if (VITE_DEV_SERVER_URL) { childWindow.loadURL(`${VITE_DEV_SERVER_URL}#${arg}`) } else { childWindow.loadFile(indexHtml, { hash: arg }) } }) ================================================ FILE: electron/main/update.ts ================================================ import { app, ipcMain } from 'electron' import { createRequire } from 'node:module' import type { ProgressInfo, UpdateDownloadedEvent, UpdateInfo, } from 'electron-updater' const { autoUpdater } = createRequire(import.meta.url)('electron-updater'); export function update(win: Electron.BrowserWindow) { // When set to false, the update download will be triggered through the API autoUpdater.autoDownload = false autoUpdater.disableWebInstaller = false autoUpdater.allowDowngrade = false // start check autoUpdater.on('checking-for-update', function () { }) // update available autoUpdater.on('update-available', (arg: UpdateInfo) => { win.webContents.send('update-can-available', { update: true, version: app.getVersion(), newVersion: arg?.version }) }) // update not available autoUpdater.on('update-not-available', (arg: UpdateInfo) => { win.webContents.send('update-can-available', { update: false, version: app.getVersion(), newVersion: arg?.version }) }) // Checking for updates ipcMain.handle('check-update', async () => { if (!app.isPackaged) { const error = new Error('The update feature is only available after the package.') return { message: error.message, error } } try { return await autoUpdater.checkForUpdatesAndNotify() } catch (error) { return { message: 'Network error', error } } }) // Start downloading and feedback on progress ipcMain.handle('start-download', (event: Electron.IpcMainInvokeEvent) => { startDownload( (error, progressInfo) => { if (error) { // feedback download error message event.sender.send('update-error', { message: error.message, error }) } else { // feedback update progress message event.sender.send('download-progress', progressInfo) } }, () => { // feedback update downloaded message event.sender.send('update-downloaded') } ) }) // Install now ipcMain.handle('quit-and-install', () => { autoUpdater.quitAndInstall(false, true) }) } function startDownload( callback: (error: Error | null, info: ProgressInfo | null) => void, complete: (event: UpdateDownloadedEvent) => void, ) { autoUpdater.on('download-progress', (info: ProgressInfo) => callback(null, info)) autoUpdater.on('error', (error: Error) => callback(error, null)) autoUpdater.on('update-downloaded', complete) autoUpdater.downloadUpdate() } ================================================ FILE: electron/preload/index.ts ================================================ import { ipcRenderer, contextBridge } from 'electron' // --------- Expose some API to the Renderer process --------- contextBridge.exposeInMainWorld('ipcRenderer', { on(...args: Parameters) { const [channel, listener] = args return ipcRenderer.on(channel, (event, ...args) => listener(event, ...args)) }, off(...args: Parameters) { const [channel, ...omit] = args return ipcRenderer.off(channel, ...omit) }, send(...args: Parameters) { const [channel, ...omit] = args return ipcRenderer.send(channel, ...omit) }, invoke(...args: Parameters) { const [channel, ...omit] = args return ipcRenderer.invoke(channel, ...omit) }, // You can expose other APTs you need here. // ... }) // --------- Preload scripts loading --------- function domReady(condition: DocumentReadyState[] = ['complete', 'interactive']) { return new Promise(resolve => { if (condition.includes(document.readyState)) { resolve(true) } else { document.addEventListener('readystatechange', () => { if (condition.includes(document.readyState)) { resolve(true) } }) } }) } const safeDOM = { append(parent: HTMLElement, child: HTMLElement) { if (!Array.from(parent.children).find(e => e === child)) { return parent.appendChild(child) } }, remove(parent: HTMLElement, child: HTMLElement) { if (Array.from(parent.children).find(e => e === child)) { return parent.removeChild(child) } }, } /** * https://tobiasahlin.com/spinkit * https://connoratherton.com/loaders * https://projects.lukehaas.me/css-loaders * https://matejkustec.github.io/SpinThatShit */ function useLoading() { const className = `loaders-css__square-spin` const styleContent = ` @keyframes square-spin { 25% { transform: perspective(100px) rotateX(180deg) rotateY(0); } 50% { transform: perspective(100px) rotateX(180deg) rotateY(180deg); } 75% { transform: perspective(100px) rotateX(0) rotateY(180deg); } 100% { transform: perspective(100px) rotateX(0) rotateY(0); } } .${className} > div { animation-fill-mode: both; width: 50px; height: 50px; background: #fff; animation: square-spin 3s 0s cubic-bezier(0.09, 0.57, 0.49, 0.9) infinite; } .app-loading-wrap { position: fixed; top: 0; left: 0; width: 100vw; height: 100vh; display: flex; align-items: center; justify-content: center; background: #282c34; z-index: 9; } ` const oStyle = document.createElement('style') const oDiv = document.createElement('div') oStyle.id = 'app-loading-style' oStyle.innerHTML = styleContent oDiv.className = 'app-loading-wrap' oDiv.innerHTML = `
` return { appendLoading() { safeDOM.append(document.head, oStyle) safeDOM.append(document.body, oDiv) }, removeLoading() { safeDOM.remove(document.head, oStyle) safeDOM.remove(document.body, oDiv) }, } } // ---------------------------------------------------------------------- const { appendLoading, removeLoading } = useLoading() domReady().then(appendLoading) window.onmessage = (ev) => { ev.data.payload === 'removeLoading' && removeLoading() } setTimeout(removeLoading, 4999) ================================================ FILE: electron-builder.json ================================================ { "$schema": "https://raw.githubusercontent.com/electron-userland/electron-builder/master/packages/app-builder-lib/scheme.json", "appId": "YourAppID", "asar": true, "directories": { "output": "release/${version}" }, "files": [ "dist-electron", "dist" ], "mac": { "artifactName": "${productName}_${version}.${ext}", "target": [ "dmg", "zip" ] }, "win": { "target": [ { "target": "nsis", "arch": [ "x64" ] } ], "artifactName": "${productName}_${version}.${ext}" }, "nsis": { "oneClick": false, "perMachine": false, "allowToChangeInstallationDirectory": true, "deleteAppDataOnUninstall": false }, "publish": { "provider": "generic", "channel": "latest", "url": "https://github.com/electron-vite/electron-vite-react/releases/download/v0.9.9/" } } ================================================ FILE: index.html ================================================ Electron + Vite + React
================================================ FILE: package.json ================================================ { "name": "electron-vite-react", "version": "2.2.0", "main": "dist-electron/main/index.js", "description": "Electron Vite React boilerplate.", "author": "草鞋没号 <308487730@qq.com>", "license": "MIT", "private": true, "debug": { "env": { "VITE_DEV_SERVER_URL": "http://127.0.0.1:7777/" } }, "type": "module", "scripts": { "dev": "vite", "build": "tsc && vite build && electron-builder", "preview": "vite preview", "pretest": "vite build --mode=test", "test": "vitest run" }, "dependencies": { "electron-updater": "^6.3.9" }, "devDependencies": { "@playwright/test": "^1.48.2", "@types/react": "^18.3.12", "@types/react-dom": "^18.3.1", "@vitejs/plugin-react": "^4.3.3", "autoprefixer": "^10.4.20", "electron": "^33.2.0", "electron-builder": "^24.13.3", "postcss": "^8.4.49", "postcss-import": "^16.1.0", "react": "^18.3.1", "react-dom": "^18.3.1", "tailwindcss": "^3.4.15", "typescript": "^5.4.2", "vite": "^5.4.11", "vite-plugin-electron": "^0.29.0", "vite-plugin-electron-renderer": "^0.14.6", "vitest": "^2.1.5" } } ================================================ FILE: postcss.config.cjs ================================================ module.exports = { plugins: { 'postcss-import': {}, 'tailwindcss/nesting': {}, tailwindcss: {}, autoprefixer: {}, }, } ================================================ FILE: src/App.css ================================================ #root { max-width: 1280px; margin: 0 auto; padding: 2rem; text-align: center; } .logo-box { position: relative; height: 9em; } .logo { position: absolute; left: calc(50% - 4.5em); height: 6em; padding: 1.5em; will-change: filter; transition: filter 300ms; } .logo:hover { filter: drop-shadow(0 0 2em #646cffaa); } @keyframes logo-spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } } @media (prefers-reduced-motion: no-preference) { .logo.electron { animation: logo-spin infinite 20s linear; } } .card { padding: 2em; } .read-the-docs { color: #888; } .flex-center { display: flex; align-items: center; justify-content: center; } ================================================ FILE: src/App.tsx ================================================ import { useState } from 'react' import UpdateElectron from '@/components/update' import logoVite from './assets/logo-vite.svg' import logoElectron from './assets/logo-electron.svg' import './App.css' function App() { const [count, setCount] = useState(0) return (

Electron + Vite + React

Edit src/App.tsx and save to test HMR

Click on the Electron + Vite logo to learn more

Place static files into the/public folder Node logo
) } export default App ================================================ FILE: src/components/update/Modal/index.tsx ================================================ import React, { ReactNode } from 'react' import { createPortal } from 'react-dom' import './modal.css' const ModalTemplate: React.FC void onOk?: () => void width?: number }>> = props => { const { title, children, footer, cancelText = 'Cancel', okText = 'OK', onCancel, onOk, width = 530, } = props return (
{title}
{children}
{typeof footer !== 'undefined' ? (
) : footer}
) } const Modal = (props: Parameters[0] & { open: boolean }) => { const { open, ...omit } = props return createPortal( open ? ModalTemplate(omit) : null, document.body, ) } export default Modal ================================================ FILE: src/components/update/Modal/modal.css ================================================ .update-modal { --primary-color: rgb(224, 30, 90); .update-modal__mask { width: 100vw; height: 100vh; position: fixed; left: 0; top: 0; z-index: 9; background: rgba(0, 0, 0, 0.45); } .update-modal__warp { position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); z-index: 19; } .update-modal__content { box-shadow: 0 0 10px -4px rgb(130, 86, 208); overflow: hidden; border-radius: 4px; .content__header { display: flex; line-height: 38px; background-color: var(--primary-color); .content__header-text { font-weight: bold; width: 0; flex-grow: 1; } } .update-modal--close { width: 30px; height: 30px; margin: 4px; line-height: 34px; text-align: center; cursor: pointer; svg { width: 17px; height: 17px; } } .content__body { padding: 10px; background-color: #fff; color: #333; } .content__footer { padding: 10px; background-color: #fff; display: flex; justify-content: flex-end; button { padding: 7px 11px; background-color: var(--primary-color); font-size: 14px; margin-left: 10px; &:first-child { margin-left: 0; } } } } .icon { padding: 0 15px; width: 20px; fill: currentColor; &:hover { color: rgba(0, 0, 0, 0.4); } } } ================================================ FILE: src/components/update/Progress/index.tsx ================================================ import React from 'react' import './progress.css' const Progress: React.FC> = props => { const { percent = 0 } = props return (
{(percent ?? 0).toString().substring(0, 4)}%
) } export default Progress ================================================ FILE: src/components/update/Progress/progress.css ================================================ .update-progress { display: flex; align-items: center; .update-progress-pr { border: 1px solid #000; border-radius: 3px; height: 6px; width: 300px; } .update-progress-rate { height: 6px; border-radius: 3px; background-image: linear-gradient(to right, rgb(130, 86, 208) 0%, var(--primary-color) 100%) } .update-progress-num { margin: 0 10px; } } ================================================ FILE: src/components/update/README.md ================================================ # electron-updater English | [简体中文](README.zh-CN.md) > Use `electron-updater` to realize the update detection, download and installation of the electric program. ```sh npm i electron-updater ``` ### Main logic 1. ##### Configuration of the update the service address and update information script: Add a `publish` field to `electron-builder.json5` for configuring the update address and which strategy to use as the update service. ``` json5 { "publish": { "provider": "generic", "channel": "latest", "url": "https://foo.com/" } } ``` For more information, please refer to : [electron-builder.json5...](https://github.com/electron-vite/electron-vite-react/blob/2f2880a9f19de50ff14a0785b32a4d5427477e55/electron-builder.json5#L38) 2. ##### The update logic of Electron: - Checking if an update is available; - Checking the version of the software on the server; - Checking if an update is available; - Downloading the new version of the software from the server (when an update is available); - Installation method; For more information, please refer to : [update...](https://github.com/electron-vite/electron-vite-react/blob/main/electron/main/update.ts) 3. ##### Updating UI pages in Electron: The main function is to provide a UI page for users to trigger the update logic mentioned in (2.) above. Users can click on the page to trigger different update functions in Electron. For more information, please refer to : [components/update...](https://github.com/electron-vite/electron-vite-react/blob/main/src/components/update/index.tsx) --- Here it is recommended to trigger updates through user actions (in this project, Electron update function is triggered after the user clicks on the "Check for updates" button). For more information on using `electron-updater` for Electron updates, please refer to the documentation : [auto-update](https://www.electron.build/.html) ================================================ FILE: src/components/update/README.zh-CN.md ================================================ # electron-auto-update [English](README.md) | 简体中文 使用`electron-updater`实现electron程序的更新检测、下载和安装等功能。 ```sh npm i electron-updater ``` ### 主要逻辑 1. ##### 更新地址、更新信息脚本的配置 在`electron-builder.json5`添加`publish`字段,用来配置更新地址和使用哪种策略作为更新服务 ``` json5 { "publish": { "provider": "generic", // 提供者、提供商 "channel": "latest", // 生成yml文件的名称 "url": "https://foo.com/" //更新地址 } } ``` 更多见 : [electron-builder.json5...](xxx) 2. ##### Electron更新逻辑 - 检测更新是否可用; - 检测服务端的软件版本; - 检测更新是否可用; - 下载服务端新版软件(当更新可用); - 安装方式; 更多见 : [update...](https://github.com/electron-vite/electron-vite-react/blob/main/electron/main/update.ts) 3. ##### Electron更新UI页面 主要功能是:用户触发上述(2.)更新逻辑的UI页面。用户可以通过点击页面触发electron更新的不同功能。 更多见 : [components/update.ts...](https://github.com/electron-vite/electron-vite-react/tree/main/src/components/update/index.tsx) --- 这里建议更新触发以用户操作触发(本项目的以用户点击 **更新检测** 后触发electron更新功能) 关于更多使用`electron-updater`进行electron更新,见文档:[auto-update](https://www.electron.build/.html) ================================================ FILE: src/components/update/index.tsx ================================================ import type { ProgressInfo } from 'electron-updater' import { useCallback, useEffect, useState } from 'react' import Modal from '@/components/update/Modal' import Progress from '@/components/update/Progress' import './update.css' const Update = () => { const [checking, setChecking] = useState(false) const [updateAvailable, setUpdateAvailable] = useState(false) const [versionInfo, setVersionInfo] = useState() const [updateError, setUpdateError] = useState() const [progressInfo, setProgressInfo] = useState>() const [modalOpen, setModalOpen] = useState(false) const [modalBtn, setModalBtn] = useState<{ cancelText?: string okText?: string onCancel?: () => void onOk?: () => void }>({ onCancel: () => setModalOpen(false), onOk: () => window.ipcRenderer.invoke('start-download'), }) const checkUpdate = async () => { setChecking(true) /** * @type {import('electron-updater').UpdateCheckResult | null | { message: string, error: Error }} */ const result = await window.ipcRenderer.invoke('check-update') setProgressInfo({ percent: 0 }) setChecking(false) setModalOpen(true) if (result?.error) { setUpdateAvailable(false) setUpdateError(result?.error) } } const onUpdateCanAvailable = useCallback((_event: Electron.IpcRendererEvent, arg1: VersionInfo) => { setVersionInfo(arg1) setUpdateError(undefined) // Can be update if (arg1.update) { setModalBtn(state => ({ ...state, cancelText: 'Cancel', okText: 'Update', onOk: () => window.ipcRenderer.invoke('start-download'), })) setUpdateAvailable(true) } else { setUpdateAvailable(false) } }, []) const onUpdateError = useCallback((_event: Electron.IpcRendererEvent, arg1: ErrorType) => { setUpdateAvailable(false) setUpdateError(arg1) }, []) const onDownloadProgress = useCallback((_event: Electron.IpcRendererEvent, arg1: ProgressInfo) => { setProgressInfo(arg1) }, []) const onUpdateDownloaded = useCallback((_event: Electron.IpcRendererEvent, ...args: any[]) => { setProgressInfo({ percent: 100 }) setModalBtn(state => ({ ...state, cancelText: 'Later', okText: 'Install now', onOk: () => window.ipcRenderer.invoke('quit-and-install'), })) }, []) useEffect(() => { // Get version information and whether to update window.ipcRenderer.on('update-can-available', onUpdateCanAvailable) window.ipcRenderer.on('update-error', onUpdateError) window.ipcRenderer.on('download-progress', onDownloadProgress) window.ipcRenderer.on('update-downloaded', onUpdateDownloaded) return () => { window.ipcRenderer.off('update-can-available', onUpdateCanAvailable) window.ipcRenderer.off('update-error', onUpdateError) window.ipcRenderer.off('download-progress', onDownloadProgress) window.ipcRenderer.off('update-downloaded', onUpdateDownloaded) } }, []) return ( <>
{updateError ? (

Error downloading the latest version.

{updateError.message}

) : updateAvailable ? (
The last version is: v{versionInfo?.newVersion}
v{versionInfo?.version} -> v{versionInfo?.newVersion}
Update progress:
) : (
{JSON.stringify(versionInfo ?? {}, null, 2)}
)}
) } export default Update ================================================ FILE: src/components/update/update.css ================================================ .modal-slot { .update-progress { display: flex; } .new-version__target, .update__progress { margin-left: 40px; } .progress__title { margin-right: 10px; } .progress__bar { width: 0; flex-grow: 1; } .can-not-available { padding: 20px; text-align: center; } } ================================================ FILE: src/demos/ipc.ts ================================================ window.ipcRenderer.on('main-process-message', (_event, ...args) => { console.log('[Receive Main-process message]:', ...args) }) ================================================ FILE: src/demos/node.ts ================================================ import { lstat } from 'node:fs/promises' import { cwd } from 'node:process' lstat(cwd()).then(stats => { console.log('[fs.lstat]', stats) }).catch(err => { console.error(err) }) ================================================ FILE: src/index.css ================================================ @tailwind base; @tailwind components; @tailwind utilities; :root { font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif; line-height: 1.5; font-weight: 400; color-scheme: light dark; color: rgba(255, 255, 255, 0.87); background-color: #242424; font-synthesis: none; text-rendering: optimizeLegibility; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; -webkit-text-size-adjust: 100%; } a { font-weight: 500; color: #646cff; text-decoration: inherit; } a:hover { color: #535bf2; } body { margin: 0; display: flex; place-items: center; min-width: 320px; min-height: 100vh; } h1 { font-size: 3.2em; line-height: 1.1; } button { border-radius: 8px; border: 1px solid transparent; padding: 0.6em 1.2em; font-size: 1em; font-weight: 500; font-family: inherit; background-color: #1a1a1a; cursor: pointer; transition: border-color 0.25s; } button:hover { border-color: #646cff; } button:focus, button:focus-visible { outline: 4px auto -webkit-focus-ring-color; } code { background-color: #1a1a1a; padding: 2px 4px; margin: 0 4px; border-radius: 4px; } .card { padding: 2em; } #app { max-width: 1280px; margin: 0 auto; padding: 2rem; text-align: center; } @media (prefers-color-scheme: light) { :root { color: #213547; background-color: #ffffff; } a:hover { color: #747bff; } button { background-color: #f9f9f9; } code { background-color: #f9f9f9; } } ================================================ FILE: src/main.tsx ================================================ import React from 'react' import ReactDOM from 'react-dom/client' import App from './App' import './index.css' import './demos/ipc' // If you want use Node.js, the`nodeIntegration` needs to be enabled in the Main process. // import './demos/node' ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render( , ) postMessage({ payload: 'removeLoading' }, '*') ================================================ FILE: src/type/electron-updater.d.ts ================================================ interface VersionInfo { update: boolean version: string newVersion?: string } interface ErrorType { message: string error: Error } ================================================ FILE: src/vite-env.d.ts ================================================ /// interface Window { // expose in the `electron/preload/index.ts` ipcRenderer: import('electron').IpcRenderer } ================================================ FILE: tailwind.config.js ================================================ /** @type {import('tailwindcss').Config} */ export default { content: [ './index.html', './src/**/*.{js,ts,jsx,tsx}', ], theme: { extend: {}, }, corePlugins: { preflight: false, }, plugins: [], } ================================================ FILE: test/e2e.spec.ts ================================================ import path from 'node:path' import { type ElectronApplication, type Page, type JSHandle, _electron as electron, } from 'playwright' import type { BrowserWindow } from 'electron' import { beforeAll, afterAll, describe, expect, test, } from 'vitest' const root = path.join(__dirname, '..') let electronApp: ElectronApplication let page: Page if (process.platform === 'linux') { // pass ubuntu test(() => expect(true).true) } else { beforeAll(async () => { electronApp = await electron.launch({ args: ['.', '--no-sandbox'], cwd: root, env: { ...process.env, NODE_ENV: 'development' }, }) page = await electronApp.firstWindow() const mainWin: JSHandle = await electronApp.browserWindow(page) await mainWin.evaluate(async (win) => { win.webContents.executeJavaScript('console.log("Execute JavaScript with e2e testing.")') }) }) afterAll(async () => { await page.screenshot({ path: 'test/screenshots/e2e.png' }) await page.close() await electronApp.close() }) describe('[electron-vite-react] e2e tests', async () => { test('startup', async () => { const title = await page.title() expect(title).eq('Electron + Vite + React') }) test('should be home page is load correctly', async () => { const h1 = await page.$('h1') const title = await h1?.textContent() expect(title).eq('Electron + Vite + React') }) test('should be count button can click', async () => { const countButton = await page.$('button') await countButton?.click() const countValue = await countButton?.textContent() expect(countValue).eq('count is 1') }) }) } ================================================ FILE: tsconfig.json ================================================ { "compilerOptions": { "target": "ESNext", "useDefineForClassFields": true, "lib": ["DOM", "DOM.Iterable", "ESNext"], "allowJs": false, "skipLibCheck": true, "esModuleInterop": false, "allowSyntheticDefaultImports": true, "strict": true, "forceConsistentCasingInFileNames": true, "module": "ESNext", "moduleResolution": "Node", "resolveJsonModule": true, "isolatedModules": true, "noEmit": true, "jsx": "react-jsx", "baseUrl": "./", "paths": { "@/*": [ "src/*" ] }, }, "include": ["src", "electron"], "references": [{ "path": "./tsconfig.node.json" }] } ================================================ FILE: tsconfig.node.json ================================================ { "compilerOptions": { "composite": true, "module": "ESNext", "moduleResolution": "Node", "resolveJsonModule": true, "allowSyntheticDefaultImports": true }, "include": ["vite.config.ts", "package.json"] } ================================================ FILE: vite.config.ts ================================================ import { rmSync } from 'node:fs' import path from 'node:path' import { defineConfig } from 'vite' import react from '@vitejs/plugin-react' import electron from 'vite-plugin-electron/simple' import pkg from './package.json' // https://vitejs.dev/config/ export default defineConfig(({ command }) => { rmSync('dist-electron', { recursive: true, force: true }) const isServe = command === 'serve' const isBuild = command === 'build' const sourcemap = isServe || !!process.env.VSCODE_DEBUG return { resolve: { alias: { '@': path.join(__dirname, 'src') }, }, plugins: [ react(), electron({ main: { // Shortcut of `build.lib.entry` entry: 'electron/main/index.ts', onstart(args) { if (process.env.VSCODE_DEBUG) { console.log(/* For `.vscode/.debug.script.mjs` */'[startup] Electron App') } else { args.startup() } }, vite: { build: { sourcemap, minify: isBuild, outDir: 'dist-electron/main', rollupOptions: { external: Object.keys('dependencies' in pkg ? pkg.dependencies : {}), }, }, }, }, preload: { // Shortcut of `build.rollupOptions.input`. // Preload scripts may contain Web assets, so use the `build.rollupOptions.input` instead `build.lib.entry`. input: 'electron/preload/index.ts', vite: { build: { sourcemap: sourcemap ? 'inline' : undefined, // #332 minify: isBuild, outDir: 'dist-electron/preload', rollupOptions: { external: Object.keys('dependencies' in pkg ? pkg.dependencies : {}), }, }, }, }, // Ployfill the Electron and Node.js API for Renderer process. // If you want use Node.js in Renderer process, the `nodeIntegration` needs to be enabled in the Main process. // See 👉 https://github.com/electron-vite/vite-plugin-electron-renderer renderer: {}, }), ], server: process.env.VSCODE_DEBUG && (() => { const url = new URL(pkg.debug.env.VITE_DEV_SERVER_URL) return { host: url.hostname, port: +url.port, } })(), clearScreen: false, } }) ================================================ FILE: vitest.config.ts ================================================ import { defineConfig } from 'vitest/config' export default defineConfig({ test: { root: __dirname, include: ['test/**/*.{test,spec}.?(c|m)[jt]s?(x)'], testTimeout: 1000 * 29, }, })