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 CHANGED
@@ -11,9 +11,9 @@
11
11
  ## 🚀 核心特性
12
12
 
13
13
  - **双轨更新模式**:内置原生弹窗自动更新 + 暴露 IPC 接口支持前端自定义进度 UI,两种方案自由切换
14
- - **三态检查系统**:`check()` 返回 `idle / available / ready` 三种状态,UI 始终知道应该显示"下载"还是"重启",彻底消除重复下载弹窗
15
- - **就绪状态持久化**:用户点击"稍后安装"后,下载的更新会记录到磁盘。下次启动或刷新页面,UI 直接跳到"立即重启"——无需重新下载
16
- - **实时状态推送**:`onStatusChanged` 钩子让主进程在更新就绪时(如静默后台下载完成)主动通知渲染层,UI 即时刷新无需用户手动操作
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, ipcMain } from 'electron'
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
- const [win] = BrowserWindow.getAllWindows()
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
- contextBridge.exposeInMainWorld('updater', {
218
- check: () => ipcRenderer.invoke('updater:check'),
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 getUpdater = () => (window as any).updater
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.onProgress((percent: number) => {
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
- getUpdater()?.install()
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` the UI always knows whether to show "Download" or "Restart", eliminating redundant download prompts
15
- - **Ready-to-Install State**: When a user clicks "Later", the downloaded update is remembered on disk. On next launch/refresh, the UI automatically jumps to "Restart Now" no re-download needed
16
- - **Real-time Status Push**: `onStatusChanged` hook lets the main process proactively notify the renderer when an update becomes ready (e.g. after silent background download)
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, ipcMain } from 'electron'
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 before restart
175
+ // Optional: Delay the restart by 2 seconds to give the Vue renderer time to save state
159
176
  onBeforeRestart: async () => {
160
- const [win] = BrowserWindow.getAllWindows()
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
- contextBridge.exposeInMainWorld('updater', {
217
- check: () => ipcRenderer.invoke('updater:check'),
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 getUpdater = () => (window as any).updater
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.onProgress((percent: number) => {
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
- getUpdater()?.install()
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` |
@@ -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");
@@ -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");
@@ -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
- ipcRenderer.on("updater:onDownloadProgress", (_, percent) => callback(percent));
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
- ipcRenderer.on("updater:onStatusChanged", (_, data) => callback(data));
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
- ipcRenderer.on("updater:onBeforeRestart", () => callback());
44
+ const fn = () => callback();
45
+ ipcRenderer.on("updater:onBeforeRestart", fn);
46
+ return () => ipcRenderer.removeListener("updater:onBeforeRestart", fn);
41
47
  }
42
48
  };
43
49
  }
@@ -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) => 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) => void;
12
- onBeforeRestart: (callback: () => void) => void;
11
+ }) => void) => () => Electron.IpcRenderer;
12
+ onBeforeRestart: (callback: () => void) => () => Electron.IpcRenderer;
13
13
  };
@@ -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
- ipcRenderer.on("updater:onDownloadProgress", (_, percent) => callback(percent));
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
- ipcRenderer.on("updater:onStatusChanged", (_, data) => callback(data));
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
- ipcRenderer.on("updater:onBeforeRestart", () => callback());
20
+ const fn = () => callback();
21
+ ipcRenderer.on("updater:onBeforeRestart", fn);
22
+ return () => ipcRenderer.removeListener("updater:onBeforeRestart", fn);
17
23
  }
18
24
  };
19
25
  }
@@ -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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "electron-updater-for-render",
3
- "version": "2.0.0-beta.1",
3
+ "version": "2.0.0-beta.2",
4
4
  "description": "A lightweight incremental updater for Electron renderer processes",
5
5
  "type": "module",
6
6
  "bin": {