electron-updater-for-render 2.0.0-beta.1 → 2.0.0-beta.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.zh-CN.md +39 -76
- package/Readme.md +39 -77
- package/dist/main/index.cjs +7 -1
- package/dist/main/index.js +7 -1
- package/dist/preload/index.cjs +10 -4
- package/dist/preload/index.d.ts +4 -4
- package/dist/preload/index.js +10 -4
- package/dist/renderer/index.d.ts +4 -4
- package/package.json +1 -1
package/README.zh-CN.md
CHANGED
|
@@ -11,9 +11,9 @@
|
|
|
11
11
|
## 🚀 核心特性
|
|
12
12
|
|
|
13
13
|
- **双轨更新模式**:内置原生弹窗自动更新 + 暴露 IPC 接口支持前端自定义进度 UI,两种方案自由切换
|
|
14
|
-
- **三态检查系统**:`check()`
|
|
15
|
-
-
|
|
16
|
-
- **实时状态推送**:`onStatusChanged`
|
|
14
|
+
- **三态检查系统**:`check()` 明确返回 `idle / available / ready` 三种状态,帮助前端 UI 精确区分“无更新”、“全网首发下载”与“已就绪待重启”阶段,实现丝滑的状态流转
|
|
15
|
+
- **就绪状态持久化**:智能识别本地已下载的更新缓存。即使用户暂不安装或页面发生刷新,更新状态依然保持“待重启”就绪态,保障断点体验的连续性
|
|
16
|
+
- **实时状态推送**:`onStatusChanged` 钩子让主进程在更新就绪时主动通知渲染层完成 UI 刷新
|
|
17
17
|
- **强制更新控制**:云端下发 `forceUpdate: 'prompt'` 或 `'silent'`,应对 P0 级线上事故时一键锁死用户退路
|
|
18
18
|
- **从容退场钩子**:`onBeforeRestart` 让渲染进程在 `app.relaunch()` 之前有充足时间保存草稿、提交日志
|
|
19
19
|
- **并发互斥锁**:内置 `isDownloading` 单例锁,防止手动触发与开机自检同时争写同一 ASAR 文件
|
|
@@ -22,6 +22,10 @@
|
|
|
22
22
|
- **零配置版本探测**:默认自动读取 `process.cwd()/package.json` 中的版本号,无需任何额外配置
|
|
23
23
|
- **独立打包命令**:普通 `npm run build` 不受任何影响,只有 `npm run build:update` 才会触发 ASAR 打包
|
|
24
24
|
- **History 模式 SPA 支持**:内置自定义协议处理器(`app://`),支持 Vue/React History 路由模式,零配置零样板代码
|
|
25
|
+
- **三端隔离 SDK**:原生提供 `main`、`preload`、`renderer` 独立切入点,防止前端构建打包时发生 Node.js 模块(如 `fs`)污染,内置完美的 TypeScript 类型推导。
|
|
26
|
+
- **精准灰度分发**:支持基于 `deviceId` 的环境身份匹配,配合 `updater.config.ts` 中的 `rolloutRule` 可实现百分百定点静默空投。
|
|
27
|
+
- **Semver 内测通道**:基于 `allowPrerelease` 提供生产环境与内测环境的天然逻辑隔离,保障正式服用户免受 Beta 版干扰。
|
|
28
|
+
- **动态网关鉴权**:通过 `requestOptions` 可在 HTTP 请求中自定义 Headers(如 Oauth / Bearer Token)及 Query 参数。
|
|
25
29
|
|
|
26
30
|
---
|
|
27
31
|
|
|
@@ -85,6 +89,9 @@ import { defineConfig } from 'electron-updater-for-render/builder'
|
|
|
85
89
|
export default defineConfig({
|
|
86
90
|
outDir: './dist', // 必填:您的构建输出目录
|
|
87
91
|
updatesDir: './dist_updates', // 可选:更新包输出目录(默认 './dist_updates')
|
|
92
|
+
// rolloutRule: {
|
|
93
|
+
// deviceIds: ['YOUR_DEVICE_ID'] // 可选:灰度定向分发,仅指明白名单设备可更新
|
|
94
|
+
// },
|
|
88
95
|
// version: '1.2.3' // 可选:显式指定版本号(优先级高于 package.json)
|
|
89
96
|
// packageJsonPath: './package.json' // 可选:自定义 package.json 路径
|
|
90
97
|
// forceUpdate: 'prompt' // 可选:'prompt' | 'silent',仅 P0 紧急修复时使用
|
|
@@ -148,34 +155,35 @@ rsync -avz dist_updates/ user@your-server:/var/www/auto-updates/
|
|
|
148
155
|
|
|
149
156
|
```typescript
|
|
150
157
|
// src/main/index.ts
|
|
151
|
-
import { app, BrowserWindow
|
|
158
|
+
import { app, BrowserWindow } from 'electron'
|
|
152
159
|
import { join } from 'path'
|
|
153
|
-
import { RenderUpdater } from 'electron-updater-for-render'
|
|
160
|
+
import { RenderUpdater, setupUpdaterIPC } from 'electron-updater-for-render/main'
|
|
154
161
|
|
|
155
162
|
const updater = new RenderUpdater({
|
|
156
163
|
updateUrl: 'https://your-server.com/auto-updates', // 必填:更新服务器根地址
|
|
157
164
|
versionsDir: join(app.getPath('userData'), 'versions'), // 必填:本地版本存储目录
|
|
165
|
+
|
|
166
|
+
// 身份标识:配合 rolloutRule 实现灰度定向空投
|
|
167
|
+
identity: { deviceId: 'YOUR_DEVICE_UUID' },
|
|
168
|
+
// 频道管理:是否接收内测包(版本号包含 -beta/-rc 等后缀)
|
|
169
|
+
allowPrerelease: true,
|
|
170
|
+
// 网关定制:在请求中挂载自定义 Header(如鉴权)
|
|
171
|
+
requestOptions: {
|
|
172
|
+
headers: { 'Authorization': 'Bearer YOUR_TOKEN' }
|
|
173
|
+
},
|
|
174
|
+
|
|
158
175
|
maxVersionsToKeep: 2, // 可选:保留最近 2 个旧版本供回滚(默认 2)
|
|
159
176
|
|
|
160
|
-
//
|
|
177
|
+
// 可选:延缓重启时间。配合前端的 onBeforeRestart 钩子,给予 Vue 保存表单状态的时间
|
|
161
178
|
onBeforeRestart: async () => {
|
|
162
|
-
|
|
163
|
-
if (win) {
|
|
164
|
-
win.webContents.send('updater:before-restart')
|
|
165
|
-
await new Promise(resolve => setTimeout(resolve, 2000))
|
|
166
|
-
}
|
|
167
|
-
},
|
|
168
|
-
|
|
169
|
-
// 可选:当更新状态发生变化时(如后台下载完成),主动推送给渲染层
|
|
170
|
-
// 渲染层无需手动检查,按钮会自动切换到"立即重启"
|
|
171
|
-
onStatusChanged: (data) => {
|
|
172
|
-
const [win] = BrowserWindow.getAllWindows()
|
|
173
|
-
if (win) {
|
|
174
|
-
win.webContents.send('updater:status-changed', data)
|
|
175
|
-
}
|
|
179
|
+
await new Promise(resolve => setTimeout(resolve, 2000))
|
|
176
180
|
}
|
|
177
181
|
})
|
|
178
182
|
|
|
183
|
+
// 🎉 一键自动打通全链路 IPC 总线
|
|
184
|
+
// 它不仅暴露方法,还会自动将主进程的 progress, status, restart 生命周期广播给所有渲染层
|
|
185
|
+
setupUpdaterIPC(updater)
|
|
186
|
+
|
|
179
187
|
app.whenReady().then(async () => {
|
|
180
188
|
const mainWindow = new BrowserWindow({
|
|
181
189
|
webPreferences: { preload: join(__dirname, '../preload/index.js') }
|
|
@@ -188,22 +196,6 @@ app.whenReady().then(async () => {
|
|
|
188
196
|
// 延迟触发自动检测(支持拦截强制更新,autoPrompt 为 true 时会弹窗提示)
|
|
189
197
|
setTimeout(() => updater.checkForUpdatesAndNotify(), 3000)
|
|
190
198
|
})
|
|
191
|
-
|
|
192
|
-
// 暴露 IPC 接口,供前端界面主动控制更新流程
|
|
193
|
-
ipcMain.handle('updater:check', () => updater.check())
|
|
194
|
-
|
|
195
|
-
// ⚠️ 重要:第一个参数必须是前端传来的 UpdateInfo 对象,第二个才是进度回调
|
|
196
|
-
// 这样可以避免主进程重复请求 latest.json,也杜绝了参数错位导致的崩溃
|
|
197
|
-
ipcMain.handle('updater:download', async (event, info) =>
|
|
198
|
-
updater.download(info, (percent) => {
|
|
199
|
-
BrowserWindow.getAllWindows()[0]?.webContents.send('updater:progress', percent)
|
|
200
|
-
})
|
|
201
|
-
)
|
|
202
|
-
|
|
203
|
-
ipcMain.on('updater:install', () => {
|
|
204
|
-
app.relaunch()
|
|
205
|
-
app.quit()
|
|
206
|
-
})
|
|
207
199
|
```
|
|
208
200
|
|
|
209
201
|
---
|
|
@@ -213,36 +205,10 @@ ipcMain.on('updater:install', () => {
|
|
|
213
205
|
```typescript
|
|
214
206
|
// src/preload/index.ts
|
|
215
207
|
import { contextBridge, ipcRenderer } from 'electron'
|
|
208
|
+
import { exposeUpdaterPreload } from 'electron-updater-for-render/preload'
|
|
216
209
|
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
// 将 check() 返回的 UpdateInfo 对象透传给主进程
|
|
221
|
-
// 主进程凭此对象直接下载,无需重新请求 latest.json
|
|
222
|
-
download: (info?: any) => ipcRenderer.invoke('updater:download', info),
|
|
223
|
-
|
|
224
|
-
install: () => ipcRenderer.send('updater:install'),
|
|
225
|
-
|
|
226
|
-
onProgress: (callback: (percent: number) => void) => {
|
|
227
|
-
const fn = (_: any, p: number) => callback(p)
|
|
228
|
-
ipcRenderer.on('updater:progress', fn)
|
|
229
|
-
return () => ipcRenderer.removeListener('updater:progress', fn)
|
|
230
|
-
},
|
|
231
|
-
|
|
232
|
-
onBeforeRestart: (callback: () => void) => {
|
|
233
|
-
const fn = () => callback()
|
|
234
|
-
ipcRenderer.on('updater:before-restart', fn)
|
|
235
|
-
return () => ipcRenderer.removeListener('updater:before-restart', fn)
|
|
236
|
-
},
|
|
237
|
-
|
|
238
|
-
// 监听主进程的主动状态推送
|
|
239
|
-
// 当后台下载完成(如用户点"稍后安装"之后)时,UI 会自动更新到"立即重启"
|
|
240
|
-
onStatusChanged: (callback: (data: { status: string; version: string }) => void) => {
|
|
241
|
-
const fn = (_: any, data: any) => callback(data)
|
|
242
|
-
ipcRenderer.on('updater:status-changed', fn)
|
|
243
|
-
return () => ipcRenderer.removeListener('updater:status-changed', fn)
|
|
244
|
-
}
|
|
245
|
-
})
|
|
210
|
+
// 必须暴露至 window.updaterAPI 命名空间,因为这是 getUpdater() 的默认查找路径
|
|
211
|
+
contextBridge.exposeInMainWorld('updaterAPI', exposeUpdaterPreload(ipcRenderer))
|
|
246
212
|
```
|
|
247
213
|
|
|
248
214
|
---
|
|
@@ -257,6 +223,7 @@ contextBridge.exposeInMainWorld('updater', {
|
|
|
257
223
|
```vue
|
|
258
224
|
<script setup lang="ts">
|
|
259
225
|
import { ref, onMounted, onUnmounted } from 'vue'
|
|
226
|
+
import { getUpdater } from 'electron-updater-for-render/renderer'
|
|
260
227
|
|
|
261
228
|
// 状态:'idle' | 'checking' | 'available' | 'downloading' | 'ready' | 'no-update'
|
|
262
229
|
const status = ref<'idle' | 'checking' | 'available' | 'downloading' | 'ready' | 'no-update'>('idle')
|
|
@@ -269,15 +236,12 @@ let removeProgress: (() => void) | null = null
|
|
|
269
236
|
let removeRestart: (() => void) | null = null
|
|
270
237
|
let removeStatusChanged: (() => void) | null = null
|
|
271
238
|
|
|
272
|
-
const
|
|
239
|
+
const updater = getUpdater()
|
|
273
240
|
|
|
274
241
|
// ── 检查更新 ───────────────────────────────────────────────────────────────
|
|
275
242
|
// check() 返回 { status: 'idle' | 'available' | 'ready', version, info }
|
|
276
243
|
// 若状态为 'ready',说明更新已下载完毕只待重启,直接跳过下载流程
|
|
277
244
|
const checkUpdate = async () => {
|
|
278
|
-
const updater = getUpdater()
|
|
279
|
-
if (!updater) return
|
|
280
|
-
|
|
281
245
|
status.value = 'checking'
|
|
282
246
|
try {
|
|
283
247
|
const res = await updater.check()
|
|
@@ -298,13 +262,10 @@ const checkUpdate = async () => {
|
|
|
298
262
|
// ── 下载更新 ───────────────────────────────────────────────────────────────
|
|
299
263
|
// 将 check() 返回的 info 对象传给 download(),避免主进程重复请求 latest.json
|
|
300
264
|
const downloadUpdate = async () => {
|
|
301
|
-
const updater = getUpdater()
|
|
302
|
-
if (!updater) return
|
|
303
|
-
|
|
304
265
|
status.value = 'downloading'
|
|
305
266
|
progress.value = 0
|
|
306
267
|
|
|
307
|
-
removeProgress = updater.
|
|
268
|
+
removeProgress = updater.onDownloadProgress((percent: number) => {
|
|
308
269
|
progress.value = percent
|
|
309
270
|
})
|
|
310
271
|
|
|
@@ -321,14 +282,13 @@ const downloadUpdate = async () => {
|
|
|
321
282
|
|
|
322
283
|
// ── 安装并重启 ─────────────────────────────────────────────────────────────
|
|
323
284
|
const installUpdate = () => {
|
|
324
|
-
|
|
285
|
+
updater.installAndRestart()
|
|
325
286
|
}
|
|
326
287
|
|
|
327
288
|
// ── 监听主进程主动推送的状态变更 ───────────────────────────────────────────
|
|
328
289
|
// 场景:用户点击"稍后安装"后,日后主进程 onStatusChanged 触发时
|
|
329
290
|
// UI 无需用户做任何操作,按钮自动变为"立即重启"
|
|
330
291
|
const initStatusListener = () => {
|
|
331
|
-
const updater = getUpdater()
|
|
332
292
|
if (updater?.onStatusChanged) {
|
|
333
293
|
return updater.onStatusChanged((data: { status: string; version: string }) => {
|
|
334
294
|
if (data.status === 'ready') {
|
|
@@ -342,7 +302,6 @@ const initStatusListener = () => {
|
|
|
342
302
|
|
|
343
303
|
// ── 监听重启前通知 ─────────────────────────────────────────────────────────
|
|
344
304
|
const initRestartListener = () => {
|
|
345
|
-
const updater = getUpdater()
|
|
346
305
|
if (updater?.onBeforeRestart) {
|
|
347
306
|
return updater.onBeforeRestart(() => {
|
|
348
307
|
// 在此保存未提交的表单状态
|
|
@@ -443,6 +402,7 @@ onUnmounted(() => {
|
|
|
443
402
|
|---|---|---|---|
|
|
444
403
|
| `outDir` | `string` | ✅ | 构建输出目录 |
|
|
445
404
|
| `updatesDir` | `string` | — | 更新包输出目录,默认 `./dist_updates` |
|
|
405
|
+
| `rolloutRule` | `object` | — | 灰度分发规则字典。如 `{ deviceIds: ['UUID'] }` |
|
|
446
406
|
| `version` | `string` | — | 显式指定版本号,优先级高于 `packageJsonPath` |
|
|
447
407
|
| `packageJsonPath` | `string` | — | 自定义 `package.json` 路径,不填则自动读取 `process.cwd()/package.json` |
|
|
448
408
|
| `asarName` | `string` | — | ASAR 文件名,默认 `renderer.asar` |
|
|
@@ -456,6 +416,9 @@ onUnmounted(() => {
|
|
|
456
416
|
|---|---|---|---|
|
|
457
417
|
| `updateUrl` | `string` | ✅ | 更新服务器根地址 |
|
|
458
418
|
| `versionsDir` | `string` | ✅ | 本地 ASAR 版本存储目录 |
|
|
419
|
+
| `identity` | `object` | — | 客户端身份标识,包含 `deviceId`,用于请求与灰度匹配 |
|
|
420
|
+
| `allowPrerelease`| `boolean` | — | 是否允许检查预发布版本(遵循 semver 规则,如带 `-beta` 后缀) |
|
|
421
|
+
| `requestOptions` | `object` | — | 自定义 HTTP 请求配置,包含 `headers` 和 `query` 参数 |
|
|
459
422
|
| `publicKey` | `string` | — | RSA 公钥(PEM 格式),用于验证 ASAR 签名 |
|
|
460
423
|
| `autoDownload` | `boolean` | — | 发现更新后自动下载,不弹窗询问。默认 `false` |
|
|
461
424
|
| `autoPrompt` | `boolean` | — | 使用内置原生弹窗引导更新。默认 `true` |
|
package/Readme.md
CHANGED
|
@@ -11,9 +11,9 @@ Unlike `electron-updater` which re-downloads the entire app installer, this libr
|
|
|
11
11
|
## 🚀 Features
|
|
12
12
|
|
|
13
13
|
- **Dual-Track Update Mode**: Built-in native OS dialogs *or* a fully custom frontend progress UI via IPC — your choice
|
|
14
|
-
- **Three-State Check System**: `check()` returns `idle / available / ready
|
|
15
|
-
- **Ready-
|
|
16
|
-
- **Real-time Status Push**: `onStatusChanged` hook
|
|
14
|
+
- **Three-State Check System**: `check()` distinctly returns `idle / available / ready`, allowing the UI to accurately distinguish between "No Updates", "Download Required", and "Ready to Restart" for a seamless state transition flow
|
|
15
|
+
- **Ready-State Persistence**: Intelligently detects locally cached complete downloads. Even if the user delays installation or refreshes the page, the update state remains "Ready", ensuring an uninterrupted user journey
|
|
16
|
+
- **Real-time Status Push**: The `onStatusChanged` hook enables the main process to proactively notify the renderer layer to refresh the UI when updates are locally ready
|
|
17
17
|
- **Force Update**: Push `forceUpdate: 'prompt'` or `'silent'` from your server for P0 hotfixes that bypass the "Later" button entirely
|
|
18
18
|
- **Graceful Restart Hook**: `onBeforeRestart` async hook lets the renderer save unsaved state before `app.relaunch()`
|
|
19
19
|
- **Concurrency Lock**: Prevents race conditions between auto-check-on-boot and manual-trigger-on-click
|
|
@@ -22,6 +22,10 @@ Unlike `electron-updater` which re-downloads the entire app installer, this libr
|
|
|
22
22
|
- **Zero-config Version Detection**: Auto-reads version from `process.cwd()/package.json` — no extra config required
|
|
23
23
|
- **Separate Build Commands**: Normal `npm run build` is untouched. Only `npm run build:update` triggers ASAR packaging
|
|
24
24
|
- **History Mode SPA Support**: Built-in custom protocol handler (`app://`) for Vue/React History mode routing — zero boilerplate
|
|
25
|
+
- **Three-Tier SDK (Type-Safe)**: Dedicated `/main`, `/preload`, and `/renderer` exports. Built-in `setupUpdaterIPC` and `exposeUpdaterPreload` absolutely drops Node.js API pollution in the frontend build.
|
|
26
|
+
- **Precision Canary Releases**: Use `identity.deviceId` and `rolloutRule` to drop updates silently to a whitelist of device IDs.
|
|
27
|
+
- **Semver Beta Channels**: Built-in Beta/RC channel toggling via `allowPrerelease` guarantees Stable users are isolated from beta patches.
|
|
28
|
+
- **Dynamic Request Gateways**: Send custom Authenticated Headers (e.g., Oauth/Bearer tokens) and Queries via `requestOptions` when checking for updates.
|
|
25
29
|
|
|
26
30
|
---
|
|
27
31
|
|
|
@@ -84,6 +88,9 @@ import { defineConfig } from 'electron-updater-for-render/builder'
|
|
|
84
88
|
export default defineConfig({
|
|
85
89
|
outDir: './dist', // Required: Your build output directory
|
|
86
90
|
updatesDir: './dist_updates', // Optional: where to write update packages (default: './dist_updates')
|
|
91
|
+
// rolloutRule: {
|
|
92
|
+
// deviceIds: ['YOUR_DEVICE_ID'] // Optional: Whitelist of device IDs for Canary/Staged rollouts
|
|
93
|
+
// },
|
|
87
94
|
// version: '1.2.3' // Optional: explicit version (overrides package.json)
|
|
88
95
|
// packageJsonPath: './package.json' // Optional: custom package.json path
|
|
89
96
|
// forceUpdate: 'prompt' // Optional: 'prompt' | 'silent' — for P0 mandatory rollouts ONLY
|
|
@@ -146,38 +153,38 @@ The server must serve these files over HTTP(S). The client will fetch:
|
|
|
146
153
|
|
|
147
154
|
```typescript
|
|
148
155
|
// src/main/index.ts
|
|
149
|
-
import { app, BrowserWindow
|
|
156
|
+
import { app, BrowserWindow } from 'electron'
|
|
150
157
|
import { join } from 'path'
|
|
151
|
-
import { RenderUpdater } from 'electron-updater-for-render'
|
|
158
|
+
import { RenderUpdater, setupUpdaterIPC } from 'electron-updater-for-render/main'
|
|
152
159
|
|
|
153
160
|
const updater = new RenderUpdater({
|
|
154
161
|
updateUrl: 'https://your-server.com/auto-updates', // Required: base URL for updates
|
|
155
162
|
versionsDir: join(app.getPath('userData'), 'versions'), // Required: local version storage
|
|
163
|
+
|
|
164
|
+
// Cloud Rollout Rules Support
|
|
165
|
+
identity: { deviceId: 'USER_LOCAL_UUID' },
|
|
166
|
+
// Beta Channel Support
|
|
167
|
+
allowPrerelease: true,
|
|
168
|
+
// Custom Authenticated Gateway
|
|
169
|
+
requestOptions: {
|
|
170
|
+
headers: { 'Authorization': 'Bearer YOUR_TOKEN' }
|
|
171
|
+
},
|
|
172
|
+
|
|
156
173
|
maxVersionsToKeep: 2, // Optional: keep 2 old versions for rollback (default: 2)
|
|
157
174
|
|
|
158
|
-
// Optional: give the renderer time to save state
|
|
175
|
+
// Optional: Delay the restart by 2 seconds to give the Vue renderer time to save state
|
|
159
176
|
onBeforeRestart: async () => {
|
|
160
|
-
|
|
161
|
-
if (win) {
|
|
162
|
-
win.webContents.send('updater:before-restart')
|
|
163
|
-
await new Promise(resolve => setTimeout(resolve, 2000))
|
|
164
|
-
}
|
|
165
|
-
},
|
|
166
|
-
|
|
167
|
-
// Optional: proactively push status changes to the renderer UI
|
|
168
|
-
// Triggered when a pending update is recorded (e.g. after "Later" is clicked)
|
|
169
|
-
onStatusChanged: (data) => {
|
|
170
|
-
const [win] = BrowserWindow.getAllWindows()
|
|
171
|
-
if (win) {
|
|
172
|
-
win.webContents.send('updater:status-changed', data)
|
|
173
|
-
}
|
|
177
|
+
await new Promise(resolve => setTimeout(resolve, 2000))
|
|
174
178
|
}
|
|
175
179
|
})
|
|
176
180
|
|
|
181
|
+
// 🎉 Automatically sets up the entire IPC bus in one line
|
|
182
|
+
// It exposes API handles and automatically broadcasts progress, status, and restart events to all webContents
|
|
183
|
+
setupUpdaterIPC(updater)
|
|
184
|
+
|
|
177
185
|
app.whenReady().then(async () => {
|
|
178
186
|
const mainWindow = new BrowserWindow({
|
|
179
187
|
webPreferences: { preload: join(__dirname, '../preload/index.js') }
|
|
180
|
-
// ...
|
|
181
188
|
})
|
|
182
189
|
|
|
183
190
|
// Load the latest downloaded ASAR, fallback to dev server if no update downloaded yet
|
|
@@ -187,22 +194,6 @@ app.whenReady().then(async () => {
|
|
|
187
194
|
// Auto-check on startup (respects forceUpdate, shows dialogs if autoPrompt: true)
|
|
188
195
|
setTimeout(() => updater.checkForUpdatesAndNotify(), 3000)
|
|
189
196
|
})
|
|
190
|
-
|
|
191
|
-
// Expose manual update controls for custom frontend UI
|
|
192
|
-
ipcMain.handle('updater:check', () => updater.check())
|
|
193
|
-
|
|
194
|
-
// ⚠️ IMPORTANT: The first argument must be UpdateInfo (forwarded from the renderer),
|
|
195
|
-
// and the second argument is the progress callback.
|
|
196
|
-
ipcMain.handle('updater:download', async (event, info) =>
|
|
197
|
-
updater.download(info, (percent) => {
|
|
198
|
-
BrowserWindow.getAllWindows()[0]?.webContents.send('updater:progress', percent)
|
|
199
|
-
})
|
|
200
|
-
)
|
|
201
|
-
|
|
202
|
-
ipcMain.on('updater:install', () => {
|
|
203
|
-
app.relaunch()
|
|
204
|
-
app.quit()
|
|
205
|
-
})
|
|
206
197
|
```
|
|
207
198
|
|
|
208
199
|
---
|
|
@@ -212,36 +203,10 @@ ipcMain.on('updater:install', () => {
|
|
|
212
203
|
```typescript
|
|
213
204
|
// src/preload/index.ts
|
|
214
205
|
import { contextBridge, ipcRenderer } from 'electron'
|
|
206
|
+
import { exposeUpdaterPreload } from 'electron-updater-for-render/preload'
|
|
215
207
|
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
// Forward the UpdateInfo object returned by check() so the main process
|
|
220
|
-
// knows which version to download without re-fetching latest.json
|
|
221
|
-
download: (info?: any) => ipcRenderer.invoke('updater:download', info),
|
|
222
|
-
|
|
223
|
-
install: () => ipcRenderer.send('updater:install'),
|
|
224
|
-
|
|
225
|
-
onProgress: (callback: (percent: number) => void) => {
|
|
226
|
-
const fn = (_: any, p: number) => callback(p)
|
|
227
|
-
ipcRenderer.on('updater:progress', fn)
|
|
228
|
-
return () => ipcRenderer.removeListener('updater:progress', fn)
|
|
229
|
-
},
|
|
230
|
-
|
|
231
|
-
onBeforeRestart: (callback: () => void) => {
|
|
232
|
-
const fn = () => callback()
|
|
233
|
-
ipcRenderer.on('updater:before-restart', fn)
|
|
234
|
-
return () => ipcRenderer.removeListener('updater:before-restart', fn)
|
|
235
|
-
},
|
|
236
|
-
|
|
237
|
-
// Listen to proactive status pushes from the main process
|
|
238
|
-
// (e.g. when a background download finishes after user clicked "Later")
|
|
239
|
-
onStatusChanged: (callback: (data: { status: string; version: string }) => void) => {
|
|
240
|
-
const fn = (_: any, data: any) => callback(data)
|
|
241
|
-
ipcRenderer.on('updater:status-changed', fn)
|
|
242
|
-
return () => ipcRenderer.removeListener('updater:status-changed', fn)
|
|
243
|
-
}
|
|
244
|
-
})
|
|
208
|
+
// Expose the API under the 'updaterAPI' namespace, as expected by getUpdater()
|
|
209
|
+
contextBridge.exposeInMainWorld('updaterAPI', exposeUpdaterPreload(ipcRenderer))
|
|
245
210
|
```
|
|
246
211
|
|
|
247
212
|
---
|
|
@@ -256,6 +221,7 @@ The following example demonstrates the complete update flow with:
|
|
|
256
221
|
```vue
|
|
257
222
|
<script setup lang="ts">
|
|
258
223
|
import { ref, onMounted, onUnmounted } from 'vue'
|
|
224
|
+
import { getUpdater } from 'electron-updater-for-render/renderer'
|
|
259
225
|
|
|
260
226
|
// Status: 'idle' | 'checking' | 'available' | 'downloading' | 'ready' | 'no-update'
|
|
261
227
|
const status = ref<'idle' | 'checking' | 'available' | 'downloading' | 'ready' | 'no-update'>('idle')
|
|
@@ -268,15 +234,12 @@ let removeProgress: (() => void) | null = null
|
|
|
268
234
|
let removeRestart: (() => void) | null = null
|
|
269
235
|
let removeStatusChanged: (() => void) | null = null
|
|
270
236
|
|
|
271
|
-
const
|
|
237
|
+
const updater = getUpdater()
|
|
272
238
|
|
|
273
239
|
// ── Check for updates ──────────────────────────────────────────────────────
|
|
274
240
|
// check() returns { status: 'idle' | 'available' | 'ready', version, info }
|
|
275
241
|
// 'ready' means an update is already downloaded and waiting for restart.
|
|
276
242
|
const checkUpdate = async () => {
|
|
277
|
-
const updater = getUpdater()
|
|
278
|
-
if (!updater) return
|
|
279
|
-
|
|
280
243
|
status.value = 'checking'
|
|
281
244
|
try {
|
|
282
245
|
const res = await updater.check()
|
|
@@ -298,13 +261,10 @@ const checkUpdate = async () => {
|
|
|
298
261
|
// Pass the UpdateInfo object from check() so the main process doesn't need
|
|
299
262
|
// to re-fetch latest.json, preventing version mismatches.
|
|
300
263
|
const downloadUpdate = async () => {
|
|
301
|
-
const updater = getUpdater()
|
|
302
|
-
if (!updater) return
|
|
303
|
-
|
|
304
264
|
status.value = 'downloading'
|
|
305
265
|
progress.value = 0
|
|
306
266
|
|
|
307
|
-
removeProgress = updater.
|
|
267
|
+
removeProgress = updater.onDownloadProgress((percent: number) => {
|
|
308
268
|
progress.value = percent
|
|
309
269
|
})
|
|
310
270
|
|
|
@@ -321,14 +281,13 @@ const downloadUpdate = async () => {
|
|
|
321
281
|
|
|
322
282
|
// ── Install & restart ──────────────────────────────────────────────────────
|
|
323
283
|
const installUpdate = () => {
|
|
324
|
-
|
|
284
|
+
updater.installAndRestart()
|
|
325
285
|
}
|
|
326
286
|
|
|
327
287
|
// ── Real-time status push from main process ───────────────────────────────
|
|
328
288
|
// When a download completes in the background, the main process fires
|
|
329
289
|
// onStatusChanged. We update the UI immediately without any user action.
|
|
330
290
|
const initStatusListener = () => {
|
|
331
|
-
const updater = getUpdater()
|
|
332
291
|
if (updater?.onStatusChanged) {
|
|
333
292
|
return updater.onStatusChanged((data: { status: string; version: string }) => {
|
|
334
293
|
if (data.status === 'ready') {
|
|
@@ -342,7 +301,6 @@ const initStatusListener = () => {
|
|
|
342
301
|
|
|
343
302
|
// ── Graceful restart ───────────────────────────────────────────────────────
|
|
344
303
|
const initRestartListener = () => {
|
|
345
|
-
const updater = getUpdater()
|
|
346
304
|
if (updater?.onBeforeRestart) {
|
|
347
305
|
return updater.onBeforeRestart(() => {
|
|
348
306
|
// Save any unsaved state here before the app relaunches
|
|
@@ -443,6 +401,7 @@ Supports clean URLs and standard browser refresh behavior. Uses a custom protoco
|
|
|
443
401
|
|---|---|---|---|
|
|
444
402
|
| `outDir` | `string` | ✅ | Vite/Webpack build output directory |
|
|
445
403
|
| `updatesDir` | `string` | — | Output directory for update packages. Default: `./dist_updates` |
|
|
404
|
+
| `rolloutRule` | `object` | — | Target whitelists: `{ deviceIds: string[] }`. |
|
|
446
405
|
| `version` | `string` | — | Explicit version string, overrides `packageJsonPath` |
|
|
447
406
|
| `packageJsonPath` | `string` | — | Custom path to `package.json`. Defaults to `process.cwd()/package.json` |
|
|
448
407
|
| `asarName` | `string` | — | ASAR filename. Default: `renderer.asar` |
|
|
@@ -456,6 +415,9 @@ Supports clean URLs and standard browser refresh behavior. Uses a custom protoco
|
|
|
456
415
|
|---|---|---|---|
|
|
457
416
|
| `updateUrl` | `string` | ✅ | Base URL where update files are hosted |
|
|
458
417
|
| `versionsDir` | `string` | ✅ | Local directory to store downloaded ASAR versions |
|
|
418
|
+
| `identity` | `object` | — | Fingerprint for rollouts logic (must include `deviceId`). |
|
|
419
|
+
| `allowPrerelease` | `boolean` | — | Skips prerelease tags if false. |
|
|
420
|
+
| `requestOptions` | `object` | — | Custom HTTP headers and query params. |
|
|
459
421
|
| `publicKey` | `string` | — | RSA public key (PEM) for signature verification |
|
|
460
422
|
| `autoDownload` | `boolean` | — | Auto-download without asking. Default: `false` |
|
|
461
423
|
| `autoPrompt` | `boolean` | — | Show built-in native dialogs. Default: `true` |
|
package/dist/main/index.cjs
CHANGED
|
@@ -628,22 +628,28 @@ ${info.releaseNotes || "\u65E0\u8BE6\u7EC6\u8BB0\u5F55\u3002"}`,
|
|
|
628
628
|
};
|
|
629
629
|
function setupUpdaterIPC(updater) {
|
|
630
630
|
import_electron3.ipcMain.handle("updater:check", () => updater.check());
|
|
631
|
-
import_electron3.ipcMain.handle("updater:download", () => updater.download());
|
|
631
|
+
import_electron3.ipcMain.handle("updater:download", (_, info) => updater.download(info));
|
|
632
632
|
import_electron3.ipcMain.handle("updater:installAndRestart", () => updater.installAndRestart());
|
|
633
633
|
import_electron3.ipcMain.on("updater:useVersion", (_, version) => updater.useVersion(version));
|
|
634
|
+
const originalOnDownloadProgress = updater.onDownloadProgress;
|
|
634
635
|
updater.onDownloadProgress = (percent) => {
|
|
636
|
+
if (originalOnDownloadProgress) originalOnDownloadProgress(percent);
|
|
635
637
|
const { webContents } = require("electron");
|
|
636
638
|
webContents.getAllWebContents().forEach((wc) => {
|
|
637
639
|
wc.send("updater:onDownloadProgress", percent);
|
|
638
640
|
});
|
|
639
641
|
};
|
|
642
|
+
const originalOnStatusChanged = updater.onStatusChanged;
|
|
640
643
|
updater.onStatusChanged = (status) => {
|
|
644
|
+
if (originalOnStatusChanged) originalOnStatusChanged(status);
|
|
641
645
|
const { webContents } = require("electron");
|
|
642
646
|
webContents.getAllWebContents().forEach((wc) => {
|
|
643
647
|
wc.send("updater:onStatusChanged", status);
|
|
644
648
|
});
|
|
645
649
|
};
|
|
650
|
+
const originalOnBeforeRestart = updater.onBeforeRestart;
|
|
646
651
|
updater.onBeforeRestart = () => {
|
|
652
|
+
if (originalOnBeforeRestart) originalOnBeforeRestart();
|
|
647
653
|
const { webContents } = require("electron");
|
|
648
654
|
webContents.getAllWebContents().forEach((wc) => {
|
|
649
655
|
wc.send("updater:onBeforeRestart");
|
package/dist/main/index.js
CHANGED
|
@@ -600,22 +600,28 @@ ${info.releaseNotes || "\u65E0\u8BE6\u7EC6\u8BB0\u5F55\u3002"}`,
|
|
|
600
600
|
};
|
|
601
601
|
function setupUpdaterIPC(updater) {
|
|
602
602
|
ipcMain.handle("updater:check", () => updater.check());
|
|
603
|
-
ipcMain.handle("updater:download", () => updater.download());
|
|
603
|
+
ipcMain.handle("updater:download", (_, info) => updater.download(info));
|
|
604
604
|
ipcMain.handle("updater:installAndRestart", () => updater.installAndRestart());
|
|
605
605
|
ipcMain.on("updater:useVersion", (_, version) => updater.useVersion(version));
|
|
606
|
+
const originalOnDownloadProgress = updater.onDownloadProgress;
|
|
606
607
|
updater.onDownloadProgress = (percent) => {
|
|
608
|
+
if (originalOnDownloadProgress) originalOnDownloadProgress(percent);
|
|
607
609
|
const { webContents } = __require("electron");
|
|
608
610
|
webContents.getAllWebContents().forEach((wc) => {
|
|
609
611
|
wc.send("updater:onDownloadProgress", percent);
|
|
610
612
|
});
|
|
611
613
|
};
|
|
614
|
+
const originalOnStatusChanged = updater.onStatusChanged;
|
|
612
615
|
updater.onStatusChanged = (status) => {
|
|
616
|
+
if (originalOnStatusChanged) originalOnStatusChanged(status);
|
|
613
617
|
const { webContents } = __require("electron");
|
|
614
618
|
webContents.getAllWebContents().forEach((wc) => {
|
|
615
619
|
wc.send("updater:onStatusChanged", status);
|
|
616
620
|
});
|
|
617
621
|
};
|
|
622
|
+
const originalOnBeforeRestart = updater.onBeforeRestart;
|
|
618
623
|
updater.onBeforeRestart = () => {
|
|
624
|
+
if (originalOnBeforeRestart) originalOnBeforeRestart();
|
|
619
625
|
const { webContents } = __require("electron");
|
|
620
626
|
webContents.getAllWebContents().forEach((wc) => {
|
|
621
627
|
wc.send("updater:onBeforeRestart");
|
package/dist/preload/index.cjs
CHANGED
|
@@ -26,18 +26,24 @@ module.exports = __toCommonJS(preload_exports);
|
|
|
26
26
|
function exposeUpdaterPreload(ipcRenderer) {
|
|
27
27
|
return {
|
|
28
28
|
check: () => ipcRenderer.invoke("updater:check"),
|
|
29
|
-
download: () => ipcRenderer.invoke("updater:download"),
|
|
29
|
+
download: (info) => ipcRenderer.invoke("updater:download", info),
|
|
30
30
|
installAndRestart: () => ipcRenderer.invoke("updater:installAndRestart"),
|
|
31
31
|
useVersion: (version) => ipcRenderer.send("updater:useVersion", version),
|
|
32
32
|
// 监听事件
|
|
33
33
|
onDownloadProgress: (callback) => {
|
|
34
|
-
|
|
34
|
+
const fn = (_, percent) => callback(percent);
|
|
35
|
+
ipcRenderer.on("updater:onDownloadProgress", fn);
|
|
36
|
+
return () => ipcRenderer.removeListener("updater:onDownloadProgress", fn);
|
|
35
37
|
},
|
|
36
38
|
onStatusChanged: (callback) => {
|
|
37
|
-
|
|
39
|
+
const fn = (_, data) => callback(data);
|
|
40
|
+
ipcRenderer.on("updater:onStatusChanged", fn);
|
|
41
|
+
return () => ipcRenderer.removeListener("updater:onStatusChanged", fn);
|
|
38
42
|
},
|
|
39
43
|
onBeforeRestart: (callback) => {
|
|
40
|
-
|
|
44
|
+
const fn = () => callback();
|
|
45
|
+
ipcRenderer.on("updater:onBeforeRestart", fn);
|
|
46
|
+
return () => ipcRenderer.removeListener("updater:onBeforeRestart", fn);
|
|
41
47
|
}
|
|
42
48
|
};
|
|
43
49
|
}
|
package/dist/preload/index.d.ts
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import type { IpcRenderer } from 'electron';
|
|
2
2
|
export declare function exposeUpdaterPreload(ipcRenderer: IpcRenderer): {
|
|
3
3
|
check: () => Promise<any>;
|
|
4
|
-
download: () => Promise<any>;
|
|
4
|
+
download: (info?: any) => Promise<any>;
|
|
5
5
|
installAndRestart: () => Promise<any>;
|
|
6
6
|
useVersion: (version: string) => void;
|
|
7
|
-
onDownloadProgress: (callback: (percent: number) => void) =>
|
|
7
|
+
onDownloadProgress: (callback: (percent: number) => void) => () => Electron.IpcRenderer;
|
|
8
8
|
onStatusChanged: (callback: (data: {
|
|
9
9
|
status: "ready" | "available" | "idle";
|
|
10
10
|
version: string;
|
|
11
|
-
}) => void) =>
|
|
12
|
-
onBeforeRestart: (callback: () => void) =>
|
|
11
|
+
}) => void) => () => Electron.IpcRenderer;
|
|
12
|
+
onBeforeRestart: (callback: () => void) => () => Electron.IpcRenderer;
|
|
13
13
|
};
|
package/dist/preload/index.js
CHANGED
|
@@ -2,18 +2,24 @@
|
|
|
2
2
|
function exposeUpdaterPreload(ipcRenderer) {
|
|
3
3
|
return {
|
|
4
4
|
check: () => ipcRenderer.invoke("updater:check"),
|
|
5
|
-
download: () => ipcRenderer.invoke("updater:download"),
|
|
5
|
+
download: (info) => ipcRenderer.invoke("updater:download", info),
|
|
6
6
|
installAndRestart: () => ipcRenderer.invoke("updater:installAndRestart"),
|
|
7
7
|
useVersion: (version) => ipcRenderer.send("updater:useVersion", version),
|
|
8
8
|
// 监听事件
|
|
9
9
|
onDownloadProgress: (callback) => {
|
|
10
|
-
|
|
10
|
+
const fn = (_, percent) => callback(percent);
|
|
11
|
+
ipcRenderer.on("updater:onDownloadProgress", fn);
|
|
12
|
+
return () => ipcRenderer.removeListener("updater:onDownloadProgress", fn);
|
|
11
13
|
},
|
|
12
14
|
onStatusChanged: (callback) => {
|
|
13
|
-
|
|
15
|
+
const fn = (_, data) => callback(data);
|
|
16
|
+
ipcRenderer.on("updater:onStatusChanged", fn);
|
|
17
|
+
return () => ipcRenderer.removeListener("updater:onStatusChanged", fn);
|
|
14
18
|
},
|
|
15
19
|
onBeforeRestart: (callback) => {
|
|
16
|
-
|
|
20
|
+
const fn = () => callback();
|
|
21
|
+
ipcRenderer.on("updater:onBeforeRestart", fn);
|
|
22
|
+
return () => ipcRenderer.removeListener("updater:onBeforeRestart", fn);
|
|
17
23
|
}
|
|
18
24
|
};
|
|
19
25
|
}
|
package/dist/renderer/index.d.ts
CHANGED
|
@@ -6,15 +6,15 @@ export interface UpdaterClientAPI {
|
|
|
6
6
|
info?: UpdateInfo;
|
|
7
7
|
status: 'idle' | 'available' | 'ready';
|
|
8
8
|
}>;
|
|
9
|
-
download(): Promise<void>;
|
|
9
|
+
download(info?: UpdateInfo): Promise<void>;
|
|
10
10
|
installAndRestart(): Promise<void>;
|
|
11
11
|
useVersion(version: string): void;
|
|
12
|
-
onDownloadProgress(callback: (percent: number) => void): void;
|
|
12
|
+
onDownloadProgress(callback: (percent: number) => void): () => void;
|
|
13
13
|
onStatusChanged(callback: (data: {
|
|
14
14
|
status: 'ready' | 'available' | 'idle';
|
|
15
15
|
version: string;
|
|
16
|
-
}) => void): void;
|
|
17
|
-
onBeforeRestart(callback: () => void): void;
|
|
16
|
+
}) => void): () => void;
|
|
17
|
+
onBeforeRestart(callback: () => void): () => void;
|
|
18
18
|
}
|
|
19
19
|
/**
|
|
20
20
|
* Ensures the updater preload API is accessible and typed.
|