diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000..2bc0b86 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,11 @@ +--- + +name: 🐞 Bug report +about: Create a report to help us improve +title: "[Bug] the title of bug report" +labels: bug +assignees: '' + +--- + +#### Describe the bug diff --git a/.github/ISSUE_TEMPLATE/help_wanted.md b/.github/ISSUE_TEMPLATE/help_wanted.md new file mode 100644 index 0000000..6fba797 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/help_wanted.md @@ -0,0 +1,10 @@ +--- +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 diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..c533dfb --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,12 @@ + + +### Description + + + +### What is the purpose of this pull request? + +- [ ] Bug fix +- [ ] New Feature +- [ ] Documentation update +- [ ] Other diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..fe250eb --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,11 @@ +# 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" diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..29a1e2d --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,47 @@ +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 \ No newline at end of file diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..3d3b53e --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,81 @@ +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 \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c2c37f0 --- /dev/null +++ b/.gitignore @@ -0,0 +1,33 @@ +# 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/ diff --git a/.npmrc b/.npmrc new file mode 100644 index 0000000..45c8cd7 --- /dev/null +++ b/.npmrc @@ -0,0 +1,6 @@ +# 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/ diff --git a/.vscode/.debug.script.mjs b/.vscode/.debug.script.mjs new file mode 100644 index 0000000..9ca9336 --- /dev/null +++ b/.vscode/.debug.script.mjs @@ -0,0 +1,23 @@ +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' }), + }, +) \ No newline at end of file diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 0000000..fe78c45 --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,7 @@ +{ + // See http://go.microsoft.com/fwlink/?LinkId=827846 + // for the documentation about the extensions.json format + "recommendations": [ + "mrmlnc.vscode-json5" + ] +} diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..2177eb1 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,54 @@ +{ + // 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/**" + ] + }, + ] +} \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..1e3e2cd --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,13 @@ +{ + "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" + } + ] +} diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 0000000..85d09cd --- /dev/null +++ b/.vscode/tasks.json @@ -0,0 +1,31 @@ +{ + // 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.*$" + } + } + } + ] +} \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..22edc0e --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +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. diff --git a/README.md b/README.md new file mode 100644 index 0000000..0a643cf --- /dev/null +++ b/README.md @@ -0,0 +1,91 @@ +# 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) diff --git a/README.zh-CN.md b/README.zh-CN.md new file mode 100644 index 0000000..5965d1f --- /dev/null +++ b/README.zh-CN.md @@ -0,0 +1,91 @@ +# 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) + +## 🍵 🍰 🍣 🍟 + + diff --git a/build/icon.icns b/build/icon.icns new file mode 100644 index 0000000..703f65b Binary files /dev/null and b/build/icon.icns differ diff --git a/build/icon.ico b/build/icon.ico new file mode 100644 index 0000000..0fc503d Binary files /dev/null and b/build/icon.ico differ diff --git a/build/icon.png b/build/icon.png new file mode 100644 index 0000000..442129f Binary files /dev/null and b/build/icon.png differ diff --git a/e2e/example.spec.ts b/e2e/example.spec.ts new file mode 100644 index 0000000..59ff80f --- /dev/null +++ b/e2e/example.spec.ts @@ -0,0 +1,8 @@ +import { test, expect, _electron as electron } from "@playwright/test"; + +test("homepage has title and links to intro page", async () => { + const app = await electron.launch({ args: [".", "--no-sandbox"] }); + const page = await app.firstWindow(); + expect(await page.title()).toBe("Electron + Vite + React"); + await page.screenshot({ path: "e2e/screenshots/example.png" }); +}); diff --git a/e2e/screenshots/example.png b/e2e/screenshots/example.png new file mode 100644 index 0000000..dad308e Binary files /dev/null and b/e2e/screenshots/example.png differ diff --git a/electron-builder.json5 b/electron-builder.json5 new file mode 100644 index 0000000..6c82482 --- /dev/null +++ b/electron-builder.json5 @@ -0,0 +1,43 @@ +/** + * @see https://www.electron.build/configuration/configuration + */ +{ + "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/" + } +} diff --git a/electron-vite-react-debug.gif b/electron-vite-react-debug.gif new file mode 100644 index 0000000..4f87992 Binary files /dev/null and b/electron-vite-react-debug.gif differ diff --git a/electron-vite-react.gif b/electron-vite-react.gif new file mode 100644 index 0000000..a4b5da5 Binary files /dev/null and b/electron-vite-react.gif differ diff --git a/electron/electron-env.d.ts b/electron/electron-env.d.ts new file mode 100644 index 0000000..aa2de81 --- /dev/null +++ b/electron/electron-env.d.ts @@ -0,0 +1,23 @@ +/// + +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 + } +} diff --git a/electron/main/index.ts b/electron/main/index.ts new file mode 100644 index 0000000..cb29401 --- /dev/null +++ b/electron/main/index.ts @@ -0,0 +1,123 @@ +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 }) + } +}) diff --git a/electron/main/update.ts b/electron/main/update.ts new file mode 100644 index 0000000..f69bcd1 --- /dev/null +++ b/electron/main/update.ts @@ -0,0 +1,76 @@ +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() +} diff --git a/electron/preload/index.ts b/electron/preload/index.ts new file mode 100644 index 0000000..3ddef8b --- /dev/null +++ b/electron/preload/index.ts @@ -0,0 +1,118 @@ +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) \ No newline at end of file diff --git a/index.html b/index.html new file mode 100644 index 0000000..aeae7ef --- /dev/null +++ b/index.html @@ -0,0 +1,14 @@ + + + + + + + + Electron + Vite + React + + +
+ + + diff --git a/package.json b/package.json new file mode 100644 index 0000000..00b0947 --- /dev/null +++ b/package.json @@ -0,0 +1,43 @@ +{ + "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", + "pree2e": "vite build --mode=test", + "e2e": "playwright test" + }, + "dependencies": { + "electron-updater": "^6.1.8" + }, + "devDependencies": { + "@playwright/test": "^1.42.1", + "@types/react": "^18.2.64", + "@types/react-dom": "^18.2.21", + "@vitejs/plugin-react": "^4.2.1", + "autoprefixer": "^10.4.18", + "electron": "^29.1.1", + "electron-builder": "^24.13.3", + "postcss": "^8.4.35", + "postcss-import": "^16.0.1", + "react": "^18.2.0", + "react-dom": "^18.2.0", + "tailwindcss": "^3.4.1", + "typescript": "^5.4.2", + "vite": "^5.1.5", + "vite-plugin-electron": "^0.28.4", + "vite-plugin-electron-renderer": "^0.14.5" + } +} diff --git a/playwright.config.ts b/playwright.config.ts new file mode 100644 index 0000000..d323551 --- /dev/null +++ b/playwright.config.ts @@ -0,0 +1,54 @@ +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; diff --git a/postcss.config.cjs b/postcss.config.cjs new file mode 100644 index 0000000..825cb18 --- /dev/null +++ b/postcss.config.cjs @@ -0,0 +1,8 @@ +module.exports = { + plugins: { + 'postcss-import': {}, + 'tailwindcss/nesting': {}, + tailwindcss: {}, + autoprefixer: {}, + }, +} diff --git a/public/favicon.ico b/public/favicon.ico new file mode 100644 index 0000000..0fc503d Binary files /dev/null and b/public/favicon.ico differ diff --git a/public/node.svg b/public/node.svg new file mode 100644 index 0000000..38d4eaa --- /dev/null +++ b/public/node.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/App.css b/src/App.css new file mode 100644 index 0000000..d02bb4b --- /dev/null +++ b/src/App.css @@ -0,0 +1,52 @@ +#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; +} diff --git a/src/App.tsx b/src/App.tsx new file mode 100644 index 0000000..c5462d1 --- /dev/null +++ b/src/App.tsx @@ -0,0 +1,38 @@ +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 \ No newline at end of file diff --git a/src/assets/logo-electron.svg b/src/assets/logo-electron.svg new file mode 100644 index 0000000..aa35624 --- /dev/null +++ b/src/assets/logo-electron.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/src/assets/logo-v1.svg b/src/assets/logo-v1.svg new file mode 100644 index 0000000..bed108b --- /dev/null +++ b/src/assets/logo-v1.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/logo-vite.svg b/src/assets/logo-vite.svg new file mode 100644 index 0000000..5432b66 --- /dev/null +++ b/src/assets/logo-vite.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/src/components/update/Modal/index.tsx b/src/components/update/Modal/index.tsx new file mode 100644 index 0000000..934f1a2 --- /dev/null +++ b/src/components/update/Modal/index.tsx @@ -0,0 +1,67 @@ +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 diff --git a/src/components/update/Modal/modal.css b/src/components/update/Modal/modal.css new file mode 100644 index 0000000..d52cacb --- /dev/null +++ b/src/components/update/Modal/modal.css @@ -0,0 +1,87 @@ +.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); + } + } +} \ No newline at end of file diff --git a/src/components/update/Progress/index.tsx b/src/components/update/Progress/index.tsx new file mode 100644 index 0000000..1701dc5 --- /dev/null +++ b/src/components/update/Progress/index.tsx @@ -0,0 +1,22 @@ +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 diff --git a/src/components/update/Progress/progress.css b/src/components/update/Progress/progress.css new file mode 100644 index 0000000..2cef7c1 --- /dev/null +++ b/src/components/update/Progress/progress.css @@ -0,0 +1,21 @@ +.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; + } +} \ No newline at end of file diff --git a/src/components/update/README.md b/src/components/update/README.md new file mode 100644 index 0000000..d119a11 --- /dev/null +++ b/src/components/update/README.md @@ -0,0 +1,49 @@ +# 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) diff --git a/src/components/update/README.zh-CN.md b/src/components/update/README.zh-CN.md new file mode 100644 index 0000000..f2c65fc --- /dev/null +++ b/src/components/update/README.zh-CN.md @@ -0,0 +1,51 @@ +# 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) diff --git a/src/components/update/index.tsx b/src/components/update/index.tsx new file mode 100644 index 0000000..2f73940 --- /dev/null +++ b/src/components/update/index.tsx @@ -0,0 +1,132 @@ +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 diff --git a/src/components/update/update.css b/src/components/update/update.css new file mode 100644 index 0000000..e776968 --- /dev/null +++ b/src/components/update/update.css @@ -0,0 +1,24 @@ +.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; + } +} \ No newline at end of file diff --git a/src/demos/ipc.ts b/src/demos/ipc.ts new file mode 100644 index 0000000..ba4daa0 --- /dev/null +++ b/src/demos/ipc.ts @@ -0,0 +1,4 @@ + +window.ipcRenderer.on('main-process-message', (_event, ...args) => { + console.log('[Receive Main-process message]:', ...args) +}) diff --git a/src/demos/node.ts b/src/demos/node.ts new file mode 100644 index 0000000..277e6a3 --- /dev/null +++ b/src/demos/node.ts @@ -0,0 +1,8 @@ +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) +}) diff --git a/src/index.css b/src/index.css new file mode 100644 index 0000000..f6ec8ba --- /dev/null +++ b/src/index.css @@ -0,0 +1,94 @@ +@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; + } +} diff --git a/src/main.tsx b/src/main.tsx new file mode 100644 index 0000000..baaff39 --- /dev/null +++ b/src/main.tsx @@ -0,0 +1,17 @@ +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' }, '*') diff --git a/src/type/electron-updater.d.ts b/src/type/electron-updater.d.ts new file mode 100644 index 0000000..5b26aba --- /dev/null +++ b/src/type/electron-updater.d.ts @@ -0,0 +1,10 @@ +interface VersionInfo { + update: boolean + version: string + newVersion?: string +} + +interface ErrorType { + message: string + error: Error +} diff --git a/src/vite-env.d.ts b/src/vite-env.d.ts new file mode 100644 index 0000000..53361d3 --- /dev/null +++ b/src/vite-env.d.ts @@ -0,0 +1,6 @@ +/// + +interface Window { + // expose in the `electron/preload/index.ts` + ipcRenderer: import('electron').IpcRenderer +} diff --git a/tailwind.config.js b/tailwind.config.js new file mode 100644 index 0000000..42872c4 --- /dev/null +++ b/tailwind.config.js @@ -0,0 +1,14 @@ +/** @type {import('tailwindcss').Config} */ +export default { + content: [ + './index.html', + './src/**/*.{js,ts,jsx,tsx}', + ], + theme: { + extend: {}, + }, + corePlugins: { + preflight: false, + }, + plugins: [], +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..c9c4765 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,27 @@ +{ + "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" }] +} diff --git a/tsconfig.node.json b/tsconfig.node.json new file mode 100644 index 0000000..1e7e7d6 --- /dev/null +++ b/tsconfig.node.json @@ -0,0 +1,10 @@ +{ + "compilerOptions": { + "composite": true, + "module": "ESNext", + "moduleResolution": "Node", + "resolveJsonModule": true, + "allowSyntheticDefaultImports": true + }, + "include": ["vite.config.ts", "package.json"] +} diff --git a/vite.config.flat.txt b/vite.config.flat.txt new file mode 100644 index 0000000..66b0dcd --- /dev/null +++ b/vite.config.flat.txt @@ -0,0 +1,78 @@ +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, + } +}) diff --git a/vite.config.ts b/vite.config.ts new file mode 100644 index 0000000..b3f2502 --- /dev/null +++ b/vite.config.ts @@ -0,0 +1,76 @@ +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, + } +})