diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 29a1e2d1..2ea0fbd8 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1,6 +1,7 @@ name: Build on: + workflow_dispatch: push: branches: [main] paths-ignore: @@ -14,6 +15,9 @@ on: - ".github/**" - "!.github/workflows/build.yml" +permissions: + contents: read + jobs: build: runs-on: ${{ matrix.os }} @@ -24,24 +28,32 @@ jobs: steps: - name: Checkout Code - uses: actions/checkout@v3 + uses: actions/checkout@v6 + with: + persist-credentials: false + + - name: Setup pnpm + uses: pnpm/action-setup@v6 + with: + version: 10 - name: Setup Node.js - uses: actions/setup-node@v3 + uses: actions/setup-node@v6 with: - node-version: 18 + node-version: 22 + cache: pnpm - name: Install Dependencies - run: npm install + run: pnpm install --frozen-lockfile - name: Build Release Files - run: npm run build + run: pnpm build env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Upload Artifact - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v7 with: - name: release_on_${{ matrix. os }} + 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 index 3d3b53e1..f2f8e5c0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,81 +1,29 @@ name: CI on: - pull_request_target: + pull_request: branches: - main + paths: + - '*.md' + - '**/*.md' permissions: - pull-requests: write + contents: read -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: +concurrency: + group: ci-${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true - - 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: +jobs: + lint-markdown: 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 + uses: actions/checkout@v6 with: - ref: ${{ github.event.pull_request.head.sha }} + persist-credentials: false - name: Lint markdown - run: npx markdownlint-cli ${{ needs.job1.outputs.markdown_files }} --ignore node_modules \ No newline at end of file + run: npx --yes markdownlint-cli@0.48.0 '**/*.md' --ignore node_modules --disable MD013 diff --git a/.github/workflows/pr-guard.yml b/.github/workflows/pr-guard.yml new file mode 100644 index 00000000..607f7cf6 --- /dev/null +++ b/.github/workflows/pr-guard.yml @@ -0,0 +1,65 @@ +name: PR Guard + +on: + pull_request_target: + branches: + - main + +permissions: + contents: read + pull-requests: write + +concurrency: + group: pr-guard-${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +jobs: + validate: + name: Validate Pull Request + runs-on: ubuntu-latest + steps: + - name: Detect Changed Files + uses: dorny/paths-filter@v3 + id: changes + with: + list-files: shell + filters: | + lockfiles: + - 'package-lock.json' + - 'yarn.lock' + - 'pnpm-lock.yaml' + - 'bun.lock' + + # ref: https://github.com/github/docs/blob/main/.github/workflows/triage-unallowed-contributions.yml + - name: Comment About Changes We Can't Accept + if: ${{ steps.changes.outputs.lockfiles == 'true' }} + uses: actions/github-script@v7 + with: + script: | + try { + const badFilesArr = [ + 'package-lock.json', + 'yarn.lock', + 'pnpm-lock.yaml', + 'bun.lock', + ] + 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` + + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.payload.pull_request.number, + body: reviewMessage, + }) + } catch(err) { + core.warning(`Failed to create PR comment: ${err.message}`) + } + + - name: Fail on Restricted Lockfile Changes + if: ${{ steps.changes.outputs.lockfiles == 'true' }} + run: | + echo "Lockfiles are managed by maintainers and should not be changed in PRs." + echo "Changed lockfiles:" + echo "${{ steps.changes.outputs.lockfiles_files }}" + exit 1 diff --git a/.gitignore b/.gitignore index c2c37f0a..3cad0a49 100644 --- a/.gitignore +++ b/.gitignore @@ -31,3 +31,5 @@ yarn.lock /test-results/ /playwright-report/ /playwright/.cache/ + +test/screenshots diff --git a/.vscode/.debug.script.mjs b/.vscode/.debug.script.mjs deleted file mode 100644 index d42a9e1b..00000000 --- a/.vscode/.debug.script.mjs +++ /dev/null @@ -1,24 +0,0 @@ -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'], - { - shell: true, - stdio: 'inherit', - env: Object.assign(process.env, { VSCODE_DEBUG: 'true' }), - }, -) diff --git a/.vscode/extensions.json b/.vscode/extensions.json deleted file mode 100644 index fe78c45f..00000000 --- a/.vscode/extensions.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - // 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 index 2177eb13..80edc4fd 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -1,54 +1,39 @@ { - // 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", + "cwd": "${workspaceFolder}", + "runtimeExecutable": "${workspaceFolder}/node_modules/.bin/vite", "windows": { - "runtimeExecutable": "${workspaceRoot}/node_modules/.bin/electron.cmd" + "runtimeExecutable": "${workspaceFolder}/node_modules/.bin/vite.cmd" }, - "runtimeArgs": [ - "--no-sandbox", - "--remote-debugging-port=9229", - "." - ], - "envFile": "${workspaceFolder}/.vscode/.debug.env", - "console": "integratedTerminal" + "runtimeArgs": ["dev"], + "env": { + "REMOTE_DEBUGGING_PORT": "9222" + } }, { "name": "Debug Renderer Process", - "port": 9229, + "port": 9222, "request": "attach", "type": "chrome", + "webRoot": "${workspaceFolder}/src", "timeout": 60000, - "skipFiles": [ - "/**", - "${workspaceRoot}/node_modules/**", - "${workspaceRoot}/dist-electron/**", - // Skip files in host(VITE_DEV_SERVER_URL) - "http://127.0.0.1:7777/**" - ] - }, + "presentation": { + "hidden": true + } + } + ], + "compounds": [ + { + "name": "Debug All", + "configurations": ["Debug Main Process", "Debug Renderer Process"], + "presentation": { + "order": 1 + } + } ] } \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json index 1e3e2cde..ca8e3188 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,10 +1,7 @@ { - "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 index 85d09cde..e0851e7c 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -6,19 +6,26 @@ { "label": "Before Debug", "type": "shell", - "command": "node .vscode/.debug.script.mjs", + "command": "node --enable-source-maps .vscode/.debug.script.mjs", "isBackground": true, + "options": { + "cwd": "${workspaceFolder}" + }, + "presentation": { + "reveal": "always", + "panel": "shared", + "clear": false + }, "problemMatcher": { "owner": "typescript", "fileLocation": "relative", "pattern": { - // TODO: correct "regexp" - "regexp": "^([a-zA-Z]\\:\/?([\\w\\-]\/?)+\\.\\w+):(\\d+):(\\d+): (ERROR|WARNING)\\: (.*)$", + "regexp": "^(.*):(\\d+):(\\d+): (ERROR|WARNING): (.*)$", "file": 1, - "line": 3, - "column": 4, - "code": 5, - "message": 6 + "line": 2, + "column": 3, + "code": 4, + "message": 5 }, "background": { "activeOnStart": true, diff --git a/README.md b/README.md index 0a643cfa..2289a8c1 100644 --- a/README.md +++ b/README.md @@ -1,24 +1,23 @@ # 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) +![GitHub stars](https://img.shields.io/github/stars/electron-vite/electron-vite-react?color=fa6470) +![GitHub issues](https://img.shields.io/github/issues/electron-vite/electron-vite-react?color=d8b22d) +![GitHub license](https://img.shields.io/github/license/electron-vite/electron-vite-react) +[![Required Node.js >= 20.19.0 || >= 22.12.0](https://img.shields.io/static/v1?label=node&message=%3E=20.19.0%20||%20%3E=22.12.0&logo=node.js&color=3f893e)](https://nodejs.org/about/releases) English | [简体中文](README.zh-CN.md) -## 👀 Overview +## 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 +- 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). +- Supports Electron and Node.js APIs in the renderer process. +- Supports C/C++ native addons. +- Includes debugger configuration. +- Easy to extend to multiple windows. -## 🛫 Quick Setup +## Quick Start ```sh # clone the project @@ -27,65 +26,54 @@ git clone https://github.com/electron-vite/electron-vite-react.git # enter the project directory cd electron-vite-react -# install dependency -npm install +# install dependencies +pnpm install -# develop -npm run dev +# start development +pnpm dev ``` -## 🐞 Debug +## Available Scripts -![electron-vite-react-debug.gif](/electron-vite-react-debug.gif) +- `pnpm dev`: start the Vite dev server. +- `pnpm build`: build the renderer and package the app with electron-builder. +- `pnpm preview`: preview the production web build locally. +- `pnpm test`: run Vitest unit tests. +- `pnpm test:e2e`: build the test mode bundle and run Playwright tests. +- `pnpm typecheck`: run the TypeScript type checker. -## 📂 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`* +## Project Structure ```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 +├── build/ Packaging assets +├── dist-electron/ Compiled Electron output +├── electron/ Main-process and preload source +│ ├── main/ +│ └── preload/ +├── public/ Static assets +├── src/ Renderer source code +│ ├── components/ +│ │ └── update/ +│ ├── demos/ +│ └── type/ +└── test/ Unit and end-to-end tests + └── e2e/ ``` - +The `renderer: {}` preset in `vite.config.ts` is only a Vite adapter that polyfills Electron, Node.js APIs and native modules for the renderer process. It is not the same as enabling Node integration. If you want direct Node.js access in the renderer, enable `nodeIntegration` in the `BrowserWindow` webPreferences in the main process and review the security impact carefully. -## 🔧 Additional features +## Features -1. electron-updater 👉 [see docs](src/components/update/README.md) -1. playwright +1. Electron auto update with docs in [src/components/update/README.md](src/components/update/README.md). +2. Vitest unit tests and Playwright end-to-end tests. +3. TailwindCSS v4. -## ❔ FAQ +## Resources +- Auto-update docs: [English](src/components/update/README.md) | [简体中文](src/components/update/README.zh-CN.md) - [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 index 5965d1f0..23e5b5e0 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -1,91 +1,79 @@ -# vite-react-electron +# 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) +![GitHub stars](https://img.shields.io/github/stars/electron-vite/electron-vite-react?color=fa6470) +![GitHub issues](https://img.shields.io/github/issues/electron-vite/electron-vite-react?color=d8b22d) +![GitHub license](https://img.shields.io/github/license/electron-vite/electron-vite-react) +[![Required Node.js >= 20.19.0 || >= 22.12.0](https://img.shields.io/static/v1?label=node&message=%3E=20.19.0%20||%20%3E=22.12.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++ 模块 -🖥 很容易实现多窗口 +- 开箱即用。 +- 基于官方的 [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 +# 安装依赖 +pnpm install -# develop -npm run dev +# 启动开发环境 +pnpm dev ``` -## 调试 +## 可用脚本 -![electron-vite-react-debug.gif](/electron-vite-react-debug.gif) +- `pnpm dev`:启动 Vite 开发服务器。 +- `pnpm build`:构建渲染进程并使用 electron-builder 打包应用。 +- `pnpm preview`:本地预览生产构建结果。 +- `pnpm test`:运行 Vitest 单元测试。 +- `pnpm test:e2e`:构建测试模式产物并运行 Playwright 测试。 +- `pnpm typecheck`:运行 TypeScript 类型检查。 -## 目录 - -*🚨 默认情况下, `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代码 +├── build/ 打包资源 +├── dist-electron/ 编译后的 Electron 输出 +├── electron/ 主进程和 preload 源码 +│ ├── main/ +│ └── preload/ +├── public/ 静态资源 +├── src/ 渲染进程源码 +│ ├── components/ +│ │ └── update/ +│ ├── demos/ +│ └── type/ +└── test/ 单元测试和端到端测试 + └── e2e/ ``` - +`vite.config.ts` 里的 `renderer: {}` 只是给 Vite 用的适配器,用来在渲染进程中修复 Electron、Node.js API 和原生模块的使用,它本身并不等同于开启 Node integration。若确实需要在渲染进程中直接使用 Node.js,请在主进程创建 `BrowserWindow` 时开启 `nodeIntegration`,并谨慎评估安全影响。 -## 🔧 额外的功能 +## 功能 -1. Electron 自动更新 👉 [阅读文档](src/components/update/README.zh-CN.md) -2. Playwright 测试 +1. Electron 自动更新,文档见 [src/components/update/README.md](src/components/update/README.md)。 +2. Vitest 单元测试和 Playwright 端到端测试。 +3. TailwindCSS v4。 -## ❔ FAQ +## 资源 +- 自动更新文档:[English](src/components/update/README.md) | [简体中文](src/components/update/README.zh-CN.md) - [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/electron-builder.json b/electron-builder.json index 75654076..5ba7c980 100644 --- a/electron-builder.json +++ b/electron-builder.json @@ -34,8 +34,9 @@ "deleteAppDataOnUninstall": false }, "publish": { - "provider": "generic", - "channel": "latest", - "url": "https://github.com/electron-vite/electron-vite-react/releases/download/v0.9.9/" + "provider": "github", + "owner": "electron-vite", + "repo": "electron-vite-react", + "releaseType": "release" } } diff --git a/electron/main/update.ts b/electron/main/update.ts index 6a365d6a..e9536171 100644 --- a/electron/main/update.ts +++ b/electron/main/update.ts @@ -1,14 +1,15 @@ import { app, ipcMain } from 'electron' -import { createRequire } from 'node:module' import type { ProgressInfo, UpdateDownloadedEvent, UpdateInfo, - CancellationToken } from 'electron-updater' +// Pure cjs module does not support named exports, so we need to import the default export and access the autoUpdater property +import updater from 'electron-updater' -const { autoUpdater } = createRequire(import.meta.url)('electron-updater'); -let cancellationToken = new CancellationToken() +const autoUpdater = updater.autoUpdater +let cancellationToken = new updater.CancellationToken() +let isDownloading = false export function update(win: Electron.BrowserWindow) { @@ -36,17 +37,22 @@ export function update(win: Electron.BrowserWindow) { } try { - return await autoUpdater.checkForUpdatesAndNotify() + return await autoUpdater.checkForUpdates() } catch (error) { - return { message: 'Network error', error } + const resolvedError = error instanceof Error ? error : new Error('Network error') + return { message: resolvedError.message, error: resolvedError } } }) // Start downloading and feedback on progress ipcMain.handle('start-download', (event: Electron.IpcMainInvokeEvent) => { + if (isDownloading) return + + isDownloading = true startDownload( (error, progressInfo) => { if (error) { + isDownloading = false // feedback download error message event.sender.send('update-error', { message: error.message, error }) } else { @@ -55,6 +61,7 @@ export function update(win: Electron.BrowserWindow) { } }, () => { + isDownloading = false // feedback update downloaded message event.sender.send('update-downloaded') } @@ -64,7 +71,7 @@ export function update(win: Electron.BrowserWindow) { // Cancel downloading ipcMain.handle('cancel-download', () => { cancellationToken.cancel() - cancellationToken = new CancellationToken(); + cancellationToken = new updater.CancellationToken(); }) // Install now @@ -77,8 +84,24 @@ 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) + const onDownloadProgress = (info: ProgressInfo) => callback(null, info) + const onError = (error: Error) => { + cleanup() + callback(error, null) + } + const onDownloaded = (event: UpdateDownloadedEvent) => { + cleanup() + complete(event) + } + + const cleanup = () => { + autoUpdater.off('download-progress', onDownloadProgress) + autoUpdater.off('error', onError) + autoUpdater.off('update-downloaded', onDownloaded) + } + + autoUpdater.on('download-progress', onDownloadProgress) + autoUpdater.on('error', onError) + autoUpdater.once('update-downloaded', onDownloaded) autoUpdater.downloadUpdate(cancellationToken) } diff --git a/package.json b/package.json index 0e091645..e01072a2 100644 --- a/package.json +++ b/package.json @@ -1,44 +1,40 @@ { "name": "electron-vite-react", - "version": "2.2.0", - "main": "dist-electron/main/index.js", + "version": "2.3.0", + "private": true, "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/" - } - }, + "author": "草鞋没号 <308487730@qq.com>", "type": "module", + "main": "dist-electron/main/index.js", "scripts": { "dev": "vite", - "build": "tsc && vite build && electron-builder", + "build": "vite build && electron-builder", "preview": "vite preview", "pretest": "vite build --mode=test", - "test": "vitest run" + "test": "vitest run", + "test:e2e": "pnpm pretest && playwright test", + "typecheck": "tsc --noEmit" }, "dependencies": { - "electron-updater": "^6.3.9" + "electron-updater": "^6.8.3" }, "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" - } + "@playwright/test": "^1.60.0", + "@tailwindcss/vite": "^4.3.0", + "@types/react": "^19.2.15", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "^6.0.2", + "electron": "^42.1.0", + "electron-builder": "^26.8.1", + "react": "^19.2.6", + "react-dom": "^19.2.6", + "tailwindcss": "^4.3.0", + "typescript": "^6.0.3", + "vite": "^8.0.14", + "vite-plugin-electron": "^1.0.0", + "vite-plugin-electron-renderer": "^1.0.0", + "vitest": "^4.1.7" + }, + "packageManager": "pnpm@10.32.1" } diff --git a/playwright.config.ts b/playwright.config.ts new file mode 100644 index 00000000..40263e40 --- /dev/null +++ b/playwright.config.ts @@ -0,0 +1,11 @@ +import { defineConfig } from '@playwright/test' + +export default defineConfig({ + testDir: './test/e2e', + fullyParallel: false, + workers: 1, + reporter: 'list', + use: { + trace: 'on-first-retry', + }, +}) diff --git a/postcss.config.cjs b/postcss.config.cjs deleted file mode 100644 index 825cb186..00000000 --- a/postcss.config.cjs +++ /dev/null @@ -1,8 +0,0 @@ -module.exports = { - plugins: { - 'postcss-import': {}, - 'tailwindcss/nesting': {}, - tailwindcss: {}, - autoprefixer: {}, - }, -} diff --git a/src/App.css b/src/App.css deleted file mode 100644 index d02bb4ba..00000000 --- a/src/App.css +++ /dev/null @@ -1,52 +0,0 @@ -#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 index c5462d1d..0dab2c88 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -2,35 +2,106 @@ 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' +import logoTailwind from './assets/logo-tailwindcss.svg' function App() { const [count, setCount] = useState(0) return ( -
-
- - Electron + Vite logo - Electron + Vite logo - -
-

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 +
+
+
+
+
- +
+
+
+
+
+
+ Electron + Vite + React + Tailwind +
+
+

+ Modern starter, cleaner rhythm, unified visual language. +

+

+ Refined spacing, balanced contrast, and consistent cards make the page feel more polished while keeping all demo functionality intact. +

+
+
+ + + + Vite logo + Electron logo + + + Open project repository + + +
+ +
+
+
+
+
Counter demo
+ Tailwind CSS logo +
+
{count}
+ +

+ Edit src/App.tsx and save to test HMR. +

+
+
+
+
+ +
+
+
Public assets
+

+ Place static files into the /public folder. +

+
+ +
+
+ Tailwind CSS logo + Tailwind system +
+

+ Unified utility classes now drive layout, hierarchy, and component consistency across the app. +

+
+ +
+
Update panel
+

+ Built-in updater UI follows the same spacing and typography rules for a more harmonious experience. +

+
+
+ + +
) } diff --git a/src/assets/logo-tailwindcss.svg b/src/assets/logo-tailwindcss.svg new file mode 100644 index 00000000..d96fb836 --- /dev/null +++ b/src/assets/logo-tailwindcss.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/tailwlindcss.svg b/src/assets/tailwlindcss.svg new file mode 100644 index 00000000..83a13ff4 --- /dev/null +++ b/src/assets/tailwlindcss.svg @@ -0,0 +1 @@ + diff --git a/src/components/update/Modal/index.tsx b/src/components/update/Modal/index.tsx deleted file mode 100644 index 934f1a2b..00000000 --- a/src/components/update/Modal/index.tsx +++ /dev/null @@ -1,67 +0,0 @@ -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 deleted file mode 100644 index d52cacba..00000000 --- a/src/components/update/Modal/modal.css +++ /dev/null @@ -1,87 +0,0 @@ -.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 deleted file mode 100644 index 1701dc57..00000000 --- a/src/components/update/Progress/index.tsx +++ /dev/null @@ -1,22 +0,0 @@ -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 deleted file mode 100644 index 2cef7c1b..00000000 --- a/src/components/update/Progress/progress.css +++ /dev/null @@ -1,21 +0,0 @@ -.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 index d119a117..abd9dd2d 100644 --- a/src/components/update/README.md +++ b/src/components/update/README.md @@ -2,48 +2,54 @@ English | [简体中文](README.zh-CN.md) -> Use `electron-updater` to realize the update detection, download and installation of the electric program. +> Use `electron-updater` to detect, download, and install Electron app updates. ```sh -npm i electron-updater +pnpm add electron-updater ``` -### Main logic +## Main Flow -1. ##### Configuration of the update the service address and update information script: +### 1. Configure the update server - Add a `publish` field to `electron-builder.json5` for configuring the update address and which strategy to use as the update service. +Add a `publish` field to `electron-builder.json` to define the update source and provider. - ``` json5 - { - "publish": { - "provider": "generic", - "channel": "latest", - "url": "https://foo.com/" - } - } - ``` +```json5 +{ + publish: { + provider: 'generic', + channel: 'latest', + url: 'https://foo.com/', + }, +} +``` + +For more information, please refer to [electron-builder.json](https://github.com/electron-vite/electron-vite-react/blob/main/electron-builder.json) + +### 2. Main-process update logic + +This project keeps `autoDownload` disabled, so users start downloads manually. - For more information, please refer to : [electron-builder.json5...](https://github.com/electron-vite/electron-vite-react/blob/2f2880a9f19de50ff14a0785b32a4d5427477e55/electron-builder.json5#L38) +- `check-update` calls `autoUpdater.checkForUpdates()` after packaging. +- The main process emits `update-can-available` when it receives `update-available` or `update-not-available`. +- `start-download` begins downloading the update and forwards download progress to the renderer. +- `cancel-download` stops the current download and resets the cancellation token. +- `quit-and-install` installs the downloaded update and restarts the app. -2. ##### The update logic of Electron: +For more information, please refer to [update.ts](https://github.com/electron-vite/electron-vite-react/blob/main/electron/main/update.ts) - - 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; +### 3. Renderer UI - For more information, please refer to : [update...](https://github.com/electron-vite/electron-vite-react/blob/main/electron/main/update.ts) +The update page is the user-facing entry point for the flow above. Users click the page button to trigger update actions in Electron. -3. ##### Updating UI pages in Electron: +- Click **Check update** to trigger `check-update`. +- If an update is available, the page lets the user start or cancel the download. +- After the download completes, the page switches to **Install now**. - 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) +For more information, please refer to [components/update/index.tsx](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). +Updates should be triggered by user action. In this project, the flow starts after the user clicks **Check update**. -For more information on using `electron-updater` for Electron updates, please refer to the documentation : [auto-update](https://www.electron.build/.html) +For more information on using `electron-updater` for Electron updates, please refer to the [documentation](https://www.electron.build) diff --git a/src/components/update/README.zh-CN.md b/src/components/update/README.zh-CN.md index f2c65fcd..7a8b89e9 100644 --- a/src/components/update/README.zh-CN.md +++ b/src/components/update/README.zh-CN.md @@ -1,51 +1,55 @@ -# electron-auto-update +# electron-updater [English](README.md) | 简体中文 -使用`electron-updater`实现electron程序的更新检测、下载和安装等功能。 +使用 `electron-updater` 检测、下载并安装 Electron 应用更新。 ```sh -npm i electron-updater +pnpm add electron-updater ``` -### 主要逻辑 +## 主要流程 -1. ##### 更新地址、更新信息脚本的配置 +### 1. 配置更新服务 - 在`electron-builder.json5`添加`publish`字段,用来配置更新地址和使用哪种策略作为更新服务 +在 `electron-builder.json` 中添加 `publish` 字段,用来配置更新地址和 provider。 - ``` json5 - { - "publish": { - "provider": "generic", // 提供者、提供商 - "channel": "latest", // 生成yml文件的名称 - "url": "https://foo.com/" //更新地址 - } - } - ``` +```json5 +{ + publish: { + provider: 'generic', + channel: 'latest', + url: 'https://foo.com/', + }, +} +``` -更多见 : [electron-builder.json5...](xxx) +更多详情请见:[electron-builder.json](https://github.com/electron-vite/electron-vite-react/blob/main/electron-builder.json) -2. ##### Electron更新逻辑 +### 2. 主进程更新逻辑 - - 检测更新是否可用; +本项目保持 `autoDownload` 关闭,因此下载需要由用户手动触发。 - - 检测服务端的软件版本; +- `check-update` 会在打包后调用 `autoUpdater.checkForUpdates()`。 +- 主进程在收到 `update-available` 或 `update-not-available` 时,会向渲染进程发送 `update-can-available`。 +- `start-download` 会开始下载更新,并把下载进度转发到渲染进程。 +- `cancel-download` 会取消当前下载,并重置取消令牌。 +- `quit-and-install` 会安装已下载的更新并重启应用。 - - 检测更新是否可用; +更多详情请见:[update.ts](https://github.com/electron-vite/electron-vite-react/blob/main/electron/main/update.ts) - - 下载服务端新版软件(当更新可用); - - 安装方式; +### 3. 渲染进程 UI - 更多见 : [update...](https://github.com/electron-vite/electron-vite-react/blob/main/electron/main/update.ts) +更新页面是用户触发上述流程的入口,用户通过页面按钮触发 Electron 的更新动作。 -3. ##### Electron更新UI页面 +- 点击 **Check update** 触发 `check-update`。 +- 如果发现新版本,页面会允许用户开始或取消下载。 +- 下载完成后,页面会切换为 **Install now**。 - 主要功能是:用户触发上述(2.)更新逻辑的UI页面。用户可以通过点击页面触发electron更新的不同功能。 - 更多见 : [components/update.ts...](https://github.com/electron-vite/electron-vite-react/tree/main/src/components/update/index.tsx) +更多详情请见:[components/update/index.tsx](https://github.com/electron-vite/electron-vite-react/blob/main/src/components/update/index.tsx) --- -这里建议更新触发以用户操作触发(本项目的以用户点击 **更新检测** 后触发electron更新功能) +建议通过用户操作触发更新。在本项目中,流程从用户点击 **Check update** 开始。 -关于更多使用`electron-updater`进行electron更新,见文档:[auto-update](https://www.electron.build/.html) +更多使用 `electron-updater` 的信息请参考 [官方文档](https://www.electron.build) diff --git a/src/components/update/index.tsx b/src/components/update/index.tsx index dc2dee64..8b8f1ec3 100644 --- a/src/components/update/index.tsx +++ b/src/components/update/index.tsx @@ -1,8 +1,7 @@ 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' +import Modal from '@/components/update/modal' +import Progress from '@/components/update/progress' const Update = () => { const [checking, setChecking] = useState(false) @@ -36,35 +35,41 @@ const Update = () => { } } - 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 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 onDownloadProgress = useCallback( + (_event: Electron.IpcRendererEvent, arg1: ProgressInfo) => { + setProgressInfo(arg1) + }, + [], + ) const onUpdateDownloaded = useCallback((_event: Electron.IpcRendererEvent, ...args: any[]) => { setProgressInfo({ percent: 100 }) - setModalBtn(state => ({ + setModalBtn((state) => ({ ...state, cancelText: 'Later', okText: 'Install now', @@ -95,34 +100,42 @@ const Update = () => { okText={modalBtn?.okText} onCancel={modalBtn?.onCancel} onOk={modalBtn?.onOk} - footer={updateAvailable ? /* hide footer */null : undefined} + title="Updater" + footer={updateAvailable ? /* hide footer */ null : undefined} > -
- {updateError - ? ( -
-

Error downloading the latest version.

-

{updateError.message}

+
+ {updateError ? ( +
+

Error downloading the latest version.

+

{updateError.message}

+
+ ) : updateAvailable ? ( +
+
+ The latest version is v{versionInfo?.newVersion} +
+
+ v{versionInfo?.version} -> v{versionInfo?.newVersion}
- ) : updateAvailable - ? ( -
-
The last version is: v{versionInfo?.newVersion}
-
v{versionInfo?.version} -> v{versionInfo?.newVersion}
-
-
Update progress:
-
- -
-
+
+
Update progress:
+
+
- ) - : ( -
{JSON.stringify(versionInfo ?? {}, null, 2)}
- )} +
+
+ ) : ( +
+              {JSON.stringify(versionInfo ?? {}, null, 2)}
+            
+ )}
- diff --git a/src/components/update/modal.tsx b/src/components/update/modal.tsx new file mode 100644 index 00000000..381a4262 --- /dev/null +++ b/src/components/update/modal.tsx @@ -0,0 +1,88 @@ +import React, { type ReactNode } from 'react' +import { createPortal } from 'react-dom' + +const ModalTemplate: React.FC< + React.PropsWithChildren<{ + title?: ReactNode + footer?: ReactNode + cancelText?: string + okText?: string + onCancel?: () => void + onOk?: () => void + width?: number + }> +> = (props) => { + const { + title, + children, + footer, + cancelText = 'Cancel', + okText = 'OK', + onCancel, + onOk, + width = 460, + } = props + + return ( +
+
+
+
+
+
+
System
+
{title}
+
+ + + + + +
+
{children}
+ {footer !== undefined ? ( +
+ + +
+ ) : ( + footer + )} +
+
+
+ ) +} + +const Modal = (props: Parameters[0] & { open: boolean }) => { + const { open, ...omit } = props + + return createPortal(open ? : null, document.body) +} + +export default Modal diff --git a/src/components/update/progress.tsx b/src/components/update/progress.tsx new file mode 100644 index 00000000..b2eda4e1 --- /dev/null +++ b/src/components/update/progress.tsx @@ -0,0 +1,26 @@ +import React from 'react' + +const Progress: React.FC< + React.PropsWithChildren<{ + percent?: number + }> +> = (props) => { + const { percent = 0 } = props + const normalizedPercent = Math.min(Math.max(percent ?? 0, 0), 100) + + return ( +
+
+
+
+ + {normalizedPercent.toFixed(1)}% + +
+ ) +} + +export default Progress diff --git a/src/components/update/update.css b/src/components/update/update.css deleted file mode 100644 index e7769684..00000000 --- a/src/components/update/update.css +++ /dev/null @@ -1,24 +0,0 @@ -.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/index.css b/src/index.css index f6ec8bae..277965e0 100644 --- a/src/index.css +++ b/src/index.css @@ -1,94 +1,84 @@ -@tailwind base; -@tailwind components; -@tailwind utilities; +@import "tailwindcss"; -: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%; +@theme { + --font-sans: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; } -a { - font-weight: 500; - color: #646cff; - text-decoration: inherit; -} -a:hover { - color: #535bf2; -} +@layer base { + :root { + color-scheme: light; + font-family: var(--font-sans); + line-height: 1.5; + font-weight: 400; + font-synthesis: none; + text-rendering: optimizeLegibility; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + -webkit-text-size-adjust: 100%; + } -body { - margin: 0; - display: flex; - place-items: center; - min-width: 320px; - min-height: 100vh; -} + html { + min-width: 320px; + min-height: 100%; + background: #f8fafc; + } -h1 { - font-size: 3.2em; - line-height: 1.1; -} + body { + min-width: 320px; + min-height: 100vh; + margin: 0; + background: + radial-gradient(circle at top, rgba(56, 189, 248, 0.16), transparent 34%), + radial-gradient(circle at bottom right, rgba(14, 165, 233, 0.12), transparent 28%), + #f8fafc; + color: #0f172a; + } -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; -} + #root { + min-height: 100vh; + background: transparent; + } -code { - background-color: #1a1a1a; - padding: 2px 4px; - margin: 0 4px; - border-radius: 4px; -} + a { + color: inherit; + text-decoration: none; + } -.card { - padding: 2em; -} + button, + input, + textarea, + select { + font: inherit; + } -#app { - max-width: 1280px; - margin: 0 auto; - padding: 2rem; - text-align: center; -} + code { + border-radius: 0.375rem; + border: 1px solid rgba(148, 163, 184, 0.35); + background: #f1f5f9; + padding: 0.125rem 0.375rem; + color: #0f172a; + } -@media (prefers-color-scheme: light) { - :root { - color: #213547; - background-color: #ffffff; + * { + scrollbar-color: #94a3b8 #e2e8f0; } - a:hover { - color: #747bff; + + *::-webkit-scrollbar { + width: 10px; + height: 10px; } - button { - background-color: #f9f9f9; + + *::-webkit-scrollbar-track { + background: #e2e8f0; } - code { - background-color: #f9f9f9; + + *::-webkit-scrollbar-thumb { + border: 2px solid #e2e8f0; + border-radius: 9999px; + background: #94a3b8; + } + + *::-webkit-scrollbar-thumb:hover { + background: #64748b; } } diff --git a/tailwind.config.js b/tailwind.config.js deleted file mode 100644 index 42872c4f..00000000 --- a/tailwind.config.js +++ /dev/null @@ -1,14 +0,0 @@ -/** @type {import('tailwindcss').Config} */ -export default { - content: [ - './index.html', - './src/**/*.{js,ts,jsx,tsx}', - ], - theme: { - extend: {}, - }, - corePlugins: { - preflight: false, - }, - plugins: [], -} diff --git a/test/e2e.spec.ts b/test/e2e.spec.ts deleted file mode 100644 index b8200596..00000000 --- a/test/e2e.spec.ts +++ /dev/null @@ -1,64 +0,0 @@ -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') - }) - }) -} diff --git a/test/e2e/e2e.spec.ts b/test/e2e/e2e.spec.ts new file mode 100644 index 00000000..fa951803 --- /dev/null +++ b/test/e2e/e2e.spec.ts @@ -0,0 +1,95 @@ +import path from 'node:path' +import { spawn, type ChildProcess } from 'node:child_process' +import { + type ElectronApplication, + type Page, + type JSHandle, + expect, + test, + _electron as electron, +} from '@playwright/test' +import type { BrowserWindow } from 'electron' + +const root = path.resolve(import.meta.dirname, '..', '..') +let electronApp: ElectronApplication +let page: Page +let xvfbProcess: ChildProcess | undefined + +function startXvfbOnLinux(): Promise { + if (process.platform !== 'linux' || process.env.DISPLAY) { + return Promise.resolve() + } + + return new Promise((resolve, reject) => { + xvfbProcess = spawn('Xvfb', [':99', '-screen', '0', '1280x720x24', '-ac'], { + stdio: 'ignore', + detached: true, + }) + + xvfbProcess.once('error', reject) + + setTimeout(() => { + process.env.DISPLAY = ':99' + resolve() + }, 500) + }) +} + +test.beforeAll(async () => { + test.setTimeout(30000) + await startXvfbOnLinux() + + 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.")') + }) +}) + +test.afterAll(async () => { + if (page) { + await page.screenshot({ path: 'test/screenshots/e2e.png' }) + await page.close() + } + + if (electronApp) { + await electronApp.close() + } + + if (xvfbProcess?.pid) { + process.kill(-xvfbProcess.pid) + xvfbProcess = undefined + } +}) + +test.describe('[electron-vite-react] e2e tests', () => { + test('startup', async () => { + const title = await page.title() + expect(title).toBe('Electron + Vite + React') + }) + + test('should be home page is load correctly', async () => { + const h1 = await page.$('h1') + const title = await h1?.textContent() + expect(title).toBe('A sharp starter with Tailwind-first styling.') + }) + + test('should be count button can click', async () => { + const countButton = await page.$('button:has-text("Increment counter")') + const countValue = await page.$('div.text-5xl') + + const valueBeforeClick = await countValue?.textContent() + expect(valueBeforeClick).toBe('0') + + await countButton?.click() + + const valueAfterClick = await countValue?.textContent() + expect(valueAfterClick).toBe('1') + }) +}) diff --git a/test/index.test.ts b/test/index.test.ts new file mode 100644 index 00000000..bc7601db --- /dev/null +++ b/test/index.test.ts @@ -0,0 +1,11 @@ +import { describe, it, expect } from 'vitest' + +describe('vitest smoke', () => { + it('runs a normal unit test', () => { + expect(1 + 1).toBe(2) + }) + + it('has test mode enabled while running tests', () => { + expect(process.env.NODE_ENV).toBe('test') + }) +}) diff --git a/tsconfig.json b/tsconfig.json index c9c47659..6211d12f 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -3,25 +3,27 @@ "target": "ESNext", "useDefineForClassFields": true, "lib": ["DOM", "DOM.Iterable", "ESNext"], + "types": ["vite/client"], "allowJs": false, "skipLibCheck": true, - "esModuleInterop": false, + "esModuleInterop": true, "allowSyntheticDefaultImports": true, "strict": true, + "verbatimModuleSyntax": true, + "moduleDetection": "force", "forceConsistentCasingInFileNames": true, "module": "ESNext", - "moduleResolution": "Node", + "moduleResolution": "bundler", "resolveJsonModule": true, "isolatedModules": true, "noEmit": true, "jsx": "react-jsx", - "baseUrl": "./", "paths": { "@/*": [ - "src/*" + "./src/*" ] - }, + } }, - "include": ["src", "electron"], + "include": ["src"], "references": [{ "path": "./tsconfig.node.json" }] } diff --git a/tsconfig.node.json b/tsconfig.node.json index 1e7e7d60..01f1ac80 100644 --- a/tsconfig.node.json +++ b/tsconfig.node.json @@ -2,9 +2,13 @@ "compilerOptions": { "composite": true, "module": "ESNext", - "moduleResolution": "Node", + "moduleResolution": "bundler", + "moduleDetection": "force", + "verbatimModuleSyntax": true, + "types": ["node"], + "skipLibCheck": true, "resolveJsonModule": true, "allowSyntheticDefaultImports": true }, - "include": ["vite.config.ts", "package.json"] + "include": ["electron", "vite.config.ts", "vitest.config.ts", "package.json"] } diff --git a/vite.config.ts b/vite.config.ts index 54e68a01..977969f6 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -2,9 +2,15 @@ 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 tailwindcss from '@tailwindcss/vite' +import { electronSimple } from 'vite-plugin-electron/multi-env' +import { notBundle } from 'vite-plugin-electron/plugin' import pkg from './package.json' +const external = Object.keys( + 'dependencies' in pkg ? (pkg.dependencies as Record) : {}, +) + // https://vitejs.dev/config/ export default defineConfig(({ command }) => { rmSync('dist-electron', { recursive: true, force: true }) @@ -16,44 +22,37 @@ export default defineConfig(({ command }) => { return { resolve: { alias: { - '@': path.join(__dirname, 'src') + '@': path.join(__dirname, 'src'), }, }, plugins: [ react(), - electron({ + tailwindcss(), + electronSimple({ 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: { + input: 'electron/main/index.ts', + plugins: [notBundle()], + options: { build: { sourcemap, minify: isBuild, outDir: 'dist-electron/main', - rollupOptions: { - external: Object.keys('dependencies' in pkg ? pkg.dependencies : {}), + rolldownOptions: { + external, }, }, }, }, 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: { + plugins: [notBundle()], + options: { build: { sourcemap: sourcemap ? 'inline' : undefined, // #332 minify: isBuild, outDir: 'dist-electron/preload', - rollupOptions: { - external: Object.keys('dependencies' in pkg ? pkg.dependencies : {}), + rolldownOptions: { + external, }, }, }, @@ -61,16 +60,9 @@ export default defineConfig(({ command }) => { // Polyfill 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: {}, + // 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/vitest.config.ts b/vitest.config.ts index 068f6fd0..6439980c 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -4,6 +4,8 @@ export default defineConfig({ test: { root: __dirname, include: ['test/**/*.{test,spec}.?(c|m)[jt]s?(x)'], + exclude: ['test/e2e.spec.ts'], + passWithNoTests: true, testTimeout: 1000 * 29, }, })