electron-updater-for-render 2.0.0-beta.1 → 2.0.1
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 +88 -101
- package/Readme.md +82 -96
- package/dist/main/index.cjs +55 -11
- package/dist/main/index.d.ts +6 -1
- package/dist/main/index.js +55 -11
- 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.cjs +11 -0
- package/dist/renderer/index.d.ts +11 -4
- package/dist/renderer/index.js +10 -0
- 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 文件
|
|
@@ -21,7 +21,12 @@
|
|
|
21
21
|
- **Pipeline 流式下载**:Node.js `stream/promises pipeline` + `Transform` 管道,内存零泄漏,网络背压自动控制
|
|
22
22
|
- **零配置版本探测**:默认自动读取 `process.cwd()/package.json` 中的版本号,无需任何额外配置
|
|
23
23
|
- **独立打包命令**:普通 `npm run build` 不受任何影响,只有 `npm run build:update` 才会触发 ASAR 打包
|
|
24
|
-
- **History
|
|
24
|
+
- **History 模式支持**:内置支持 Vue/React 的 History 路由模式。
|
|
25
|
+
- **多页应用 (MPA) 支持**:支持通过 `getLoadUrl(entry)` 加载不同的入口文件。
|
|
26
|
+
- **极简 SDK 接入**:提供 `/main`, `/preload`, `/renderer` 导出,内置 `getRouterBase` 路由基准路径探测工具。
|
|
27
|
+
- **精准灰度分发**:支持基于 `deviceId` 的环境身份匹配,配合 `updater.config.ts` 中的 `rolloutRule` 可实现百分百定点静默空投。
|
|
28
|
+
- **Semver 内测通道**:基于 `allowPrerelease` 提供生产环境与内测环境的天然逻辑隔离,保障正式服用户免受 Beta 版干扰。
|
|
29
|
+
- **动态网关鉴权**:通过 `requestOptions` 可在 HTTP 请求中自定义 Headers(如 Oauth / Bearer Token)及 Query 参数。
|
|
25
30
|
|
|
26
31
|
---
|
|
27
32
|
|
|
@@ -85,6 +90,9 @@ import { defineConfig } from 'electron-updater-for-render/builder'
|
|
|
85
90
|
export default defineConfig({
|
|
86
91
|
outDir: './dist', // 必填:您的构建输出目录
|
|
87
92
|
updatesDir: './dist_updates', // 可选:更新包输出目录(默认 './dist_updates')
|
|
93
|
+
// rolloutRule: {
|
|
94
|
+
// deviceIds: ['YOUR_DEVICE_ID'] // 可选:灰度定向分发,仅指明白名单设备可更新
|
|
95
|
+
// },
|
|
88
96
|
// version: '1.2.3' // 可选:显式指定版本号(优先级高于 package.json)
|
|
89
97
|
// packageJsonPath: './package.json' // 可选:自定义 package.json 路径
|
|
90
98
|
// forceUpdate: 'prompt' // 可选:'prompt' | 'silent',仅 P0 紧急修复时使用
|
|
@@ -148,62 +156,48 @@ rsync -avz dist_updates/ user@your-server:/var/www/auto-updates/
|
|
|
148
156
|
|
|
149
157
|
```typescript
|
|
150
158
|
// src/main/index.ts
|
|
151
|
-
import { app, BrowserWindow
|
|
159
|
+
import { app, BrowserWindow } from 'electron'
|
|
152
160
|
import { join } from 'path'
|
|
153
|
-
import { RenderUpdater } from 'electron-updater-for-render'
|
|
161
|
+
import { RenderUpdater, setupUpdaterIPC } from 'electron-updater-for-render/main'
|
|
154
162
|
|
|
155
163
|
const updater = new RenderUpdater({
|
|
156
164
|
updateUrl: 'https://your-server.com/auto-updates', // 必填:更新服务器根地址
|
|
157
165
|
versionsDir: join(app.getPath('userData'), 'versions'), // 必填:本地版本存储目录
|
|
166
|
+
|
|
167
|
+
// 身份标识:配合 rolloutRule 实现灰度定向空投
|
|
168
|
+
identity: { deviceId: 'YOUR_DEVICE_UUID' },
|
|
169
|
+
// 频道管理:是否接收内测包(版本号包含 -beta/-rc 等后缀)
|
|
170
|
+
allowPrerelease: true,
|
|
171
|
+
// 网关定制:在请求中挂载自定义 Header(如鉴权)
|
|
172
|
+
requestOptions: {
|
|
173
|
+
headers: { 'Authorization': 'Bearer YOUR_TOKEN' }
|
|
174
|
+
},
|
|
175
|
+
|
|
158
176
|
maxVersionsToKeep: 2, // 可选:保留最近 2 个旧版本供回滚(默认 2)
|
|
159
177
|
|
|
160
|
-
//
|
|
178
|
+
// 可选:延缓重启时间。配合前端的 onBeforeRestart 钩子,给予 Vue 保存表单状态的时间
|
|
161
179
|
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
|
-
}
|
|
180
|
+
await new Promise(resolve => setTimeout(resolve, 2000))
|
|
176
181
|
}
|
|
177
182
|
})
|
|
178
183
|
|
|
184
|
+
// 🎉 一键自动打通全链路 IPC 总线
|
|
185
|
+
// 它不仅暴露方法,还会自动将主进程的 progress, status, restart 生命周期广播给所有渲染层
|
|
186
|
+
setupUpdaterIPC(updater)
|
|
187
|
+
|
|
179
188
|
app.whenReady().then(async () => {
|
|
180
189
|
const mainWindow = new BrowserWindow({
|
|
181
190
|
webPreferences: { preload: join(__dirname, '../preload/index.js') }
|
|
182
191
|
})
|
|
183
192
|
|
|
184
|
-
//
|
|
185
|
-
|
|
186
|
-
|
|
193
|
+
// 加载最新下载好的 ASAR,若无更新则回退至本地 dist。
|
|
194
|
+
// 支持多页应用入口传入 (默认为 'index.html')
|
|
195
|
+
const loadUrl = updater.getLoadUrl('login/index.html')
|
|
196
|
+
mainWindow.loadURL(loadUrl || `file://${join(__dirname, '../renderer/login/index.html')}`)
|
|
187
197
|
|
|
188
198
|
// 延迟触发自动检测(支持拦截强制更新,autoPrompt 为 true 时会弹窗提示)
|
|
189
199
|
setTimeout(() => updater.checkForUpdatesAndNotify(), 3000)
|
|
190
200
|
})
|
|
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
201
|
```
|
|
208
202
|
|
|
209
203
|
---
|
|
@@ -213,36 +207,10 @@ ipcMain.on('updater:install', () => {
|
|
|
213
207
|
```typescript
|
|
214
208
|
// src/preload/index.ts
|
|
215
209
|
import { contextBridge, ipcRenderer } from 'electron'
|
|
210
|
+
import { exposeUpdaterPreload } from 'electron-updater-for-render/preload'
|
|
216
211
|
|
|
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
|
-
})
|
|
212
|
+
// 必须暴露至 window.updaterAPI 命名空间,因为这是 getUpdater() 的默认查找路径
|
|
213
|
+
contextBridge.exposeInMainWorld('updaterAPI', exposeUpdaterPreload(ipcRenderer))
|
|
246
214
|
```
|
|
247
215
|
|
|
248
216
|
---
|
|
@@ -257,6 +225,11 @@ contextBridge.exposeInMainWorld('updater', {
|
|
|
257
225
|
```vue
|
|
258
226
|
<script setup lang="ts">
|
|
259
227
|
import { ref, onMounted, onUnmounted } from 'vue'
|
|
228
|
+
import { getUpdater, getRouterBase } from 'electron-updater-for-render/renderer'
|
|
229
|
+
|
|
230
|
+
// 自动探测当前 Window 上下文对应的 History Base
|
|
231
|
+
const routerBase = getRouterBase()
|
|
232
|
+
const updater = getUpdater()
|
|
260
233
|
|
|
261
234
|
// 状态:'idle' | 'checking' | 'available' | 'downloading' | 'ready' | 'no-update'
|
|
262
235
|
const status = ref<'idle' | 'checking' | 'available' | 'downloading' | 'ready' | 'no-update'>('idle')
|
|
@@ -269,15 +242,12 @@ let removeProgress: (() => void) | null = null
|
|
|
269
242
|
let removeRestart: (() => void) | null = null
|
|
270
243
|
let removeStatusChanged: (() => void) | null = null
|
|
271
244
|
|
|
272
|
-
const
|
|
245
|
+
const updater = getUpdater()
|
|
273
246
|
|
|
274
247
|
// ── 检查更新 ───────────────────────────────────────────────────────────────
|
|
275
248
|
// check() 返回 { status: 'idle' | 'available' | 'ready', version, info }
|
|
276
249
|
// 若状态为 'ready',说明更新已下载完毕只待重启,直接跳过下载流程
|
|
277
250
|
const checkUpdate = async () => {
|
|
278
|
-
const updater = getUpdater()
|
|
279
|
-
if (!updater) return
|
|
280
|
-
|
|
281
251
|
status.value = 'checking'
|
|
282
252
|
try {
|
|
283
253
|
const res = await updater.check()
|
|
@@ -298,13 +268,10 @@ const checkUpdate = async () => {
|
|
|
298
268
|
// ── 下载更新 ───────────────────────────────────────────────────────────────
|
|
299
269
|
// 将 check() 返回的 info 对象传给 download(),避免主进程重复请求 latest.json
|
|
300
270
|
const downloadUpdate = async () => {
|
|
301
|
-
const updater = getUpdater()
|
|
302
|
-
if (!updater) return
|
|
303
|
-
|
|
304
271
|
status.value = 'downloading'
|
|
305
272
|
progress.value = 0
|
|
306
273
|
|
|
307
|
-
removeProgress = updater.
|
|
274
|
+
removeProgress = updater.onDownloadProgress((percent: number) => {
|
|
308
275
|
progress.value = percent
|
|
309
276
|
})
|
|
310
277
|
|
|
@@ -321,14 +288,13 @@ const downloadUpdate = async () => {
|
|
|
321
288
|
|
|
322
289
|
// ── 安装并重启 ─────────────────────────────────────────────────────────────
|
|
323
290
|
const installUpdate = () => {
|
|
324
|
-
|
|
291
|
+
updater.installAndRestart()
|
|
325
292
|
}
|
|
326
293
|
|
|
327
294
|
// ── 监听主进程主动推送的状态变更 ───────────────────────────────────────────
|
|
328
295
|
// 场景:用户点击"稍后安装"后,日后主进程 onStatusChanged 触发时
|
|
329
296
|
// UI 无需用户做任何操作,按钮自动变为"立即重启"
|
|
330
297
|
const initStatusListener = () => {
|
|
331
|
-
const updater = getUpdater()
|
|
332
298
|
if (updater?.onStatusChanged) {
|
|
333
299
|
return updater.onStatusChanged((data: { status: string; version: string }) => {
|
|
334
300
|
if (data.status === 'ready') {
|
|
@@ -342,7 +308,6 @@ const initStatusListener = () => {
|
|
|
342
308
|
|
|
343
309
|
// ── 监听重启前通知 ─────────────────────────────────────────────────────────
|
|
344
310
|
const initRestartListener = () => {
|
|
345
|
-
const updater = getUpdater()
|
|
346
311
|
if (updater?.onBeforeRestart) {
|
|
347
312
|
return updater.onBeforeRestart(() => {
|
|
348
313
|
// 在此保存未提交的表单状态
|
|
@@ -407,31 +372,49 @@ onUnmounted(() => {
|
|
|
407
372
|
|
|
408
373
|
本库完美支持 Electron 常用的文件加载模式(Hash)和现代单页应用路由模式(History)。
|
|
409
374
|
|
|
410
|
-
### 1. Hash
|
|
411
|
-
|
|
375
|
+
### 1. Hash 模式 (推荐方案:零配置、零侵入)
|
|
376
|
+
这是 Electron 环境下的**工业级标准实践**。它对物理路径不敏感,不需要配置任何 Base,前端项目完全不需要引入插件的任何 JS 逻辑。
|
|
412
377
|
|
|
413
|
-
-
|
|
378
|
+
- **主进程**:无需额外配置(`routerMode` 默认为 `'hash'`)。
|
|
414
379
|
- **前端 (Vite)**:`vite.config.ts` 中的 `base` 设为 `'./'`(或不填)。
|
|
415
380
|
- **前端 (Router)**:使用 `createWebHashHistory()`。
|
|
381
|
+
- **优点**:真正的“零感知”,前端代码与热更新逻辑完全解耦。
|
|
416
382
|
|
|
417
|
-
### 2. History
|
|
418
|
-
支持美观的 URL
|
|
383
|
+
### 2. History 模式 (高级方案)
|
|
384
|
+
支持美观的 URL 和标准浏览器刷新行为。由于 MPA 存在物理路径认知偏差,需要对齐基准路径(Base)。
|
|
419
385
|
|
|
420
386
|
- **主进程**:
|
|
421
387
|
```typescript
|
|
422
388
|
new RenderUpdater({
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
389
|
+
// ...
|
|
390
|
+
routerMode: 'history',
|
|
391
|
+
protocol: 'my-app' // 可选:自定义协议名 (默认 'app')
|
|
392
|
+
})
|
|
393
|
+
```
|
|
394
|
+
|
|
395
|
+
- **渲染层 (Router) - 选项 A:使用自动化助手 (推荐)**
|
|
396
|
+
引入插件提供的轻量级工具函数,自动探测当前窗口的物理基准:
|
|
397
|
+
```typescript
|
|
398
|
+
import { getRouterBase } from 'electron-updater-for-render/renderer'
|
|
399
|
+
const router = createRouter({
|
|
400
|
+
history: createWebHistory(getRouterBase()), // 自动对齐 app:// 协议下的物理路径
|
|
401
|
+
routes: [...]
|
|
402
|
+
})
|
|
403
|
+
```
|
|
404
|
+
|
|
405
|
+
- **渲染层 (Router) - 选项 B:手动对齐 (零侵入)**
|
|
406
|
+
如果您不想在前端引入任何插件代码,可以手动指定与 `getLoadUrl()` 传入路径一致的字符串(需以 `.html/` 结尾):
|
|
407
|
+
```typescript
|
|
408
|
+
const router = createRouter({
|
|
409
|
+
history: createWebHistory('/index.html/'),
|
|
410
|
+
routes: [...]
|
|
427
411
|
})
|
|
428
412
|
```
|
|
429
|
-
> 💡 **全自动化**:协议注册和特权赋权逻辑已在库内部**全自动处理**,您无需编写任何 `registerSchemesAsPrivileged` 等繁琐代码。
|
|
430
413
|
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
-
|
|
414
|
+
### 3. 多页应用 (MPA) 支持
|
|
415
|
+
- **多窗口多入口**:支持为不同窗口指定不同的 HTML 入口。
|
|
416
|
+
- **History 模式刷新**:支持在 History 模式下进行页面刷新。
|
|
417
|
+
- **基准路径探测**:配合 `getRouterBase()` 自动识别当前的路由基准路径。
|
|
435
418
|
|
|
436
419
|
---
|
|
437
420
|
|
|
@@ -443,6 +426,7 @@ onUnmounted(() => {
|
|
|
443
426
|
|---|---|---|---|
|
|
444
427
|
| `outDir` | `string` | ✅ | 构建输出目录 |
|
|
445
428
|
| `updatesDir` | `string` | — | 更新包输出目录,默认 `./dist_updates` |
|
|
429
|
+
| `rolloutRule` | `object` | — | 灰度分发规则字典。如 `{ deviceIds: ['UUID'] }` |
|
|
446
430
|
| `version` | `string` | — | 显式指定版本号,优先级高于 `packageJsonPath` |
|
|
447
431
|
| `packageJsonPath` | `string` | — | 自定义 `package.json` 路径,不填则自动读取 `process.cwd()/package.json` |
|
|
448
432
|
| `asarName` | `string` | — | ASAR 文件名,默认 `renderer.asar` |
|
|
@@ -456,6 +440,9 @@ onUnmounted(() => {
|
|
|
456
440
|
|---|---|---|---|
|
|
457
441
|
| `updateUrl` | `string` | ✅ | 更新服务器根地址 |
|
|
458
442
|
| `versionsDir` | `string` | ✅ | 本地 ASAR 版本存储目录 |
|
|
443
|
+
| `identity` | `object` | — | 客户端身份标识,包含 `deviceId`,用于请求与灰度匹配 |
|
|
444
|
+
| `allowPrerelease`| `boolean` | — | 是否允许检查预发布版本(遵循 semver 规则,如带 `-beta` 后缀) |
|
|
445
|
+
| `requestOptions` | `object` | — | 自定义 HTTP 请求配置,包含 `headers` 和 `query` 参数 |
|
|
459
446
|
| `publicKey` | `string` | — | RSA 公钥(PEM 格式),用于验证 ASAR 签名 |
|
|
460
447
|
| `autoDownload` | `boolean` | — | 发现更新后自动下载,不弹窗询问。默认 `false` |
|
|
461
448
|
| `autoPrompt` | `boolean` | — | 使用内置原生弹窗引导更新。默认 `true` |
|
|
@@ -471,14 +458,14 @@ onUnmounted(() => {
|
|
|
471
458
|
|
|
472
459
|
### `RenderUpdater` 实例方法
|
|
473
460
|
|
|
474
|
-
| 方法 | 返回值 |
|
|
461
|
+
| 方法 | 返回值 | 描述 |
|
|
475
462
|
|---|---|---|
|
|
476
|
-
| `check()` | `Promise<CheckResult>` | 检查更新。返回 `{ updateAvailable, status, version, info }
|
|
477
|
-
| `download(info?, onProgress?)` | `Promise<void>` |
|
|
478
|
-
| `getLoadUrl()` | `string` |
|
|
479
|
-
| `installAndRestart()` | `Promise<void>` |
|
|
480
|
-
| `checkForUpdatesAndNotify()` | `Promise<void>` |
|
|
481
|
-
| `setUpdatePending(version)` | `void` |
|
|
463
|
+
| `check()` | `Promise<CheckResult>` | 检查更新。返回 `{ updateAvailable, status, version, info }`。状态为 `'idle' \| 'available' \| 'ready'` |
|
|
464
|
+
| `download(info?, onProgress?)` | `Promise<void>` | 下载更新。建议传入 `check()` 返回的 `info` 对象以防止版本竞争 |
|
|
465
|
+
| `getLoadUrl(entry?)` | `string` | 返回加载最新 ASAR 的 URL (`app://renderer/` 或 `file://...`)。`entry` 默认为 `index.html`。若无更新则返回空字符串 |
|
|
466
|
+
| `installAndRestart()` | `Promise<void>` | 应用更新并重启应用 |
|
|
467
|
+
| `checkForUpdatesAndNotify()` | `Promise<void>` | 一站式:检查 + 提示 + 下载 + 安装(使用内置原生对话框) |
|
|
468
|
+
| `setUpdatePending(version)` | `void` | 将某版本标记为已就绪并等待重启。会触发 `onStatusChanged` |
|
|
482
469
|
| `useVersion(version)` | `void` | 立即切换磁盘和内存双重版本指针 |
|
|
483
470
|
| `activeVersion` | `string`(getter) | 当前内存中正在运行的版本(本次会话) |
|
|
484
471
|
| `pendingVersion` | `string`(getter) | 磁盘上最新已下载版本 |
|
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
|
|
@@ -21,7 +21,12 @@ Unlike `electron-updater` which re-downloads the entire app installer, this libr
|
|
|
21
21
|
- **Stream Pipeline Download**: Node.js `stream/promises pipeline` + `Transform` — zero memory leaks, full backpressure support
|
|
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
|
-
- **History Mode
|
|
24
|
+
- **History Mode Support**: Built-in support for Vue/React History router mode.
|
|
25
|
+
- **Multi-Page Application (MPA)**: Support for loading multiple entry points via `getLoadUrl(entry)`.
|
|
26
|
+
- **Simplified SDK**: Unified exports for `/main`, `/preload`, and `/renderer`, including `getRouterBase`.
|
|
27
|
+
- **Precision Canary Releases**: Use `identity.deviceId` and `rolloutRule` to drop updates silently to a whitelist of device IDs.
|
|
28
|
+
- **Semver Beta Channels**: Built-in Beta/RC channel toggling via `allowPrerelease` guarantees Stable users are isolated from beta patches.
|
|
29
|
+
- **Dynamic Request Gateways**: Send custom Authenticated Headers (e.g., Oauth/Bearer tokens) and Queries via `requestOptions` when checking for updates.
|
|
25
30
|
|
|
26
31
|
---
|
|
27
32
|
|
|
@@ -84,6 +89,9 @@ import { defineConfig } from 'electron-updater-for-render/builder'
|
|
|
84
89
|
export default defineConfig({
|
|
85
90
|
outDir: './dist', // Required: Your build output directory
|
|
86
91
|
updatesDir: './dist_updates', // Optional: where to write update packages (default: './dist_updates')
|
|
92
|
+
// rolloutRule: {
|
|
93
|
+
// deviceIds: ['YOUR_DEVICE_ID'] // Optional: Whitelist of device IDs for Canary/Staged rollouts
|
|
94
|
+
// },
|
|
87
95
|
// version: '1.2.3' // Optional: explicit version (overrides package.json)
|
|
88
96
|
// packageJsonPath: './package.json' // Optional: custom package.json path
|
|
89
97
|
// forceUpdate: 'prompt' // Optional: 'prompt' | 'silent' — for P0 mandatory rollouts ONLY
|
|
@@ -146,63 +154,48 @@ The server must serve these files over HTTP(S). The client will fetch:
|
|
|
146
154
|
|
|
147
155
|
```typescript
|
|
148
156
|
// src/main/index.ts
|
|
149
|
-
import { app, BrowserWindow
|
|
157
|
+
import { app, BrowserWindow } from 'electron'
|
|
150
158
|
import { join } from 'path'
|
|
151
|
-
import { RenderUpdater } from 'electron-updater-for-render'
|
|
159
|
+
import { RenderUpdater, setupUpdaterIPC } from 'electron-updater-for-render/main'
|
|
152
160
|
|
|
153
161
|
const updater = new RenderUpdater({
|
|
154
162
|
updateUrl: 'https://your-server.com/auto-updates', // Required: base URL for updates
|
|
155
163
|
versionsDir: join(app.getPath('userData'), 'versions'), // Required: local version storage
|
|
164
|
+
|
|
165
|
+
// Cloud Rollout Rules Support
|
|
166
|
+
identity: { deviceId: 'USER_LOCAL_UUID' },
|
|
167
|
+
// Beta Channel Support
|
|
168
|
+
allowPrerelease: true,
|
|
169
|
+
// Custom Authenticated Gateway
|
|
170
|
+
requestOptions: {
|
|
171
|
+
headers: { 'Authorization': 'Bearer YOUR_TOKEN' }
|
|
172
|
+
},
|
|
173
|
+
|
|
156
174
|
maxVersionsToKeep: 2, // Optional: keep 2 old versions for rollback (default: 2)
|
|
157
175
|
|
|
158
|
-
// Optional: give the renderer time to save state
|
|
176
|
+
// Optional: Delay the restart by 2 seconds to give the Vue renderer time to save state
|
|
159
177
|
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
|
-
}
|
|
178
|
+
await new Promise(resolve => setTimeout(resolve, 2000))
|
|
174
179
|
}
|
|
175
180
|
})
|
|
176
181
|
|
|
182
|
+
// 🎉 Automatically sets up the entire IPC bus in one line
|
|
183
|
+
// It exposes API handles and automatically broadcasts progress, status, and restart events to all webContents
|
|
184
|
+
setupUpdaterIPC(updater)
|
|
185
|
+
|
|
177
186
|
app.whenReady().then(async () => {
|
|
178
187
|
const mainWindow = new BrowserWindow({
|
|
179
188
|
webPreferences: { preload: join(__dirname, '../preload/index.js') }
|
|
180
|
-
// ...
|
|
181
189
|
})
|
|
182
190
|
|
|
183
|
-
// Load the latest downloaded ASAR, fallback to
|
|
184
|
-
|
|
185
|
-
|
|
191
|
+
// Load the latest downloaded ASAR, fallback to local dist if no update is available
|
|
192
|
+
// Supports multi-page entries (default: 'index.html')
|
|
193
|
+
const loadUrl = updater.getLoadUrl('login/index.html')
|
|
194
|
+
mainWindow.loadURL(loadUrl || `file://${join(__dirname, '../renderer/login/index.html')}`)
|
|
186
195
|
|
|
187
196
|
// Auto-check on startup (respects forceUpdate, shows dialogs if autoPrompt: true)
|
|
188
197
|
setTimeout(() => updater.checkForUpdatesAndNotify(), 3000)
|
|
189
198
|
})
|
|
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
199
|
```
|
|
207
200
|
|
|
208
201
|
---
|
|
@@ -212,36 +205,10 @@ ipcMain.on('updater:install', () => {
|
|
|
212
205
|
```typescript
|
|
213
206
|
// src/preload/index.ts
|
|
214
207
|
import { contextBridge, ipcRenderer } from 'electron'
|
|
208
|
+
import { exposeUpdaterPreload } from 'electron-updater-for-render/preload'
|
|
215
209
|
|
|
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
|
-
})
|
|
210
|
+
// Expose the API under the 'updaterAPI' namespace, as expected by getUpdater()
|
|
211
|
+
contextBridge.exposeInMainWorld('updaterAPI', exposeUpdaterPreload(ipcRenderer))
|
|
245
212
|
```
|
|
246
213
|
|
|
247
214
|
---
|
|
@@ -256,6 +223,11 @@ The following example demonstrates the complete update flow with:
|
|
|
256
223
|
```vue
|
|
257
224
|
<script setup lang="ts">
|
|
258
225
|
import { ref, onMounted, onUnmounted } from 'vue'
|
|
226
|
+
import { getUpdater, getRouterBase } from 'electron-updater-for-render/renderer'
|
|
227
|
+
|
|
228
|
+
// Automatically detect the correct context-aware History base for MPA
|
|
229
|
+
const routerBase = getRouterBase()
|
|
230
|
+
const updater = getUpdater()
|
|
259
231
|
|
|
260
232
|
// Status: 'idle' | 'checking' | 'available' | 'downloading' | 'ready' | 'no-update'
|
|
261
233
|
const status = ref<'idle' | 'checking' | 'available' | 'downloading' | 'ready' | 'no-update'>('idle')
|
|
@@ -268,15 +240,12 @@ let removeProgress: (() => void) | null = null
|
|
|
268
240
|
let removeRestart: (() => void) | null = null
|
|
269
241
|
let removeStatusChanged: (() => void) | null = null
|
|
270
242
|
|
|
271
|
-
const
|
|
243
|
+
const updater = getUpdater()
|
|
272
244
|
|
|
273
245
|
// ── Check for updates ──────────────────────────────────────────────────────
|
|
274
246
|
// check() returns { status: 'idle' | 'available' | 'ready', version, info }
|
|
275
247
|
// 'ready' means an update is already downloaded and waiting for restart.
|
|
276
248
|
const checkUpdate = async () => {
|
|
277
|
-
const updater = getUpdater()
|
|
278
|
-
if (!updater) return
|
|
279
|
-
|
|
280
249
|
status.value = 'checking'
|
|
281
250
|
try {
|
|
282
251
|
const res = await updater.check()
|
|
@@ -298,13 +267,10 @@ const checkUpdate = async () => {
|
|
|
298
267
|
// Pass the UpdateInfo object from check() so the main process doesn't need
|
|
299
268
|
// to re-fetch latest.json, preventing version mismatches.
|
|
300
269
|
const downloadUpdate = async () => {
|
|
301
|
-
const updater = getUpdater()
|
|
302
|
-
if (!updater) return
|
|
303
|
-
|
|
304
270
|
status.value = 'downloading'
|
|
305
271
|
progress.value = 0
|
|
306
272
|
|
|
307
|
-
removeProgress = updater.
|
|
273
|
+
removeProgress = updater.onDownloadProgress((percent: number) => {
|
|
308
274
|
progress.value = percent
|
|
309
275
|
})
|
|
310
276
|
|
|
@@ -321,14 +287,13 @@ const downloadUpdate = async () => {
|
|
|
321
287
|
|
|
322
288
|
// ── Install & restart ──────────────────────────────────────────────────────
|
|
323
289
|
const installUpdate = () => {
|
|
324
|
-
|
|
290
|
+
updater.installAndRestart()
|
|
325
291
|
}
|
|
326
292
|
|
|
327
293
|
// ── Real-time status push from main process ───────────────────────────────
|
|
328
294
|
// When a download completes in the background, the main process fires
|
|
329
295
|
// onStatusChanged. We update the UI immediately without any user action.
|
|
330
296
|
const initStatusListener = () => {
|
|
331
|
-
const updater = getUpdater()
|
|
332
297
|
if (updater?.onStatusChanged) {
|
|
333
298
|
return updater.onStatusChanged((data: { status: string; version: string }) => {
|
|
334
299
|
if (data.status === 'ready') {
|
|
@@ -342,7 +307,6 @@ const initStatusListener = () => {
|
|
|
342
307
|
|
|
343
308
|
// ── Graceful restart ───────────────────────────────────────────────────────
|
|
344
309
|
const initRestartListener = () => {
|
|
345
|
-
const updater = getUpdater()
|
|
346
310
|
if (updater?.onBeforeRestart) {
|
|
347
311
|
return updater.onBeforeRestart(() => {
|
|
348
312
|
// Save any unsaved state here before the app relaunches
|
|
@@ -407,31 +371,49 @@ onUnmounted(() => {
|
|
|
407
371
|
|
|
408
372
|
The library supports both standard Electron file-loading (Hash) and modern SPA routing (History) via custom protocols.
|
|
409
373
|
|
|
410
|
-
### 1. Hash Mode (
|
|
411
|
-
|
|
374
|
+
### 1. Hash Mode (Recommended: Zero-Config, Zero-Intrusion)
|
|
375
|
+
The **industry standard** for Electron applications. It is resilient to physical path changes, requires no base configuration, and needs zero plugin-related JS in your frontend.
|
|
412
376
|
|
|
413
|
-
- **Main Process**: No extra config needed (defaults to `hash`).
|
|
377
|
+
- **Main Process**: No extra config needed (defaults to `'hash'`).
|
|
414
378
|
- **Renderer (Vite)**: Set `base: './'` (or omit) in `vite.config.ts`.
|
|
415
379
|
- **Renderer (Router)**: Use `createWebHashHistory()`.
|
|
380
|
+
- **Pros**: Pure decoupling; your frontend project remains completely unaware of the updater's existence.
|
|
416
381
|
|
|
417
|
-
### 2. History Mode (
|
|
418
|
-
Supports clean URLs and
|
|
382
|
+
### 2. History Mode (Advanced)
|
|
383
|
+
Supports clean URLs and native browser refresh behavior. Due to physical path context in MPAs, the router needs to be aligned with the base path.
|
|
419
384
|
|
|
420
385
|
- **Main Process**:
|
|
421
386
|
```typescript
|
|
422
387
|
new RenderUpdater({
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
388
|
+
// ...
|
|
389
|
+
routerMode: 'history',
|
|
390
|
+
protocol: 'my-app' // Optional: custom protocol name (default: 'app')
|
|
391
|
+
})
|
|
392
|
+
```
|
|
393
|
+
|
|
394
|
+
- **Renderer (Router) - Option A: Use Automated Helper (Recommended)**
|
|
395
|
+
Use our lightweight helper to automatically sense the physical base of the current window:
|
|
396
|
+
```typescript
|
|
397
|
+
import { getRouterBase } from 'electron-updater-for-render/renderer'
|
|
398
|
+
const router = createRouter({
|
|
399
|
+
history: createWebHistory(getRouterBase()), // Auto-aligns with the app:// protocol path
|
|
400
|
+
routes: [...]
|
|
401
|
+
})
|
|
402
|
+
```
|
|
403
|
+
|
|
404
|
+
- **Renderer (Router) - Option B: Manual Alignment (Zero-Intrusion)**
|
|
405
|
+
If you prefer not to import any plugin JS, you can manually provide a string that matches the path passed to `getLoadUrl()` (must end with `.html/`):
|
|
406
|
+
```typescript
|
|
407
|
+
const router = createRouter({
|
|
408
|
+
history: createWebHistory('/index.html/'),
|
|
409
|
+
routes: [...]
|
|
427
410
|
})
|
|
428
411
|
```
|
|
429
|
-
> 💡 **No Boilerplate**: Protocol registration and privileged scheme setup are **automated** internally. You don't need to call `registerSchemesAsPrivileged` manually.
|
|
430
412
|
|
|
431
|
-
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
- **
|
|
413
|
+
### 3. Multi-Page Application (MPA)
|
|
414
|
+
- **Multi-window Support**: Load different entry points for different windows.
|
|
415
|
+
- **History Refresh Support**: Supports browser refresh in History mode.
|
|
416
|
+
- **Base Path Detection**: Use `getRouterBase()` to automatically identify the current routing base.
|
|
435
417
|
|
|
436
418
|
---
|
|
437
419
|
|
|
@@ -443,6 +425,7 @@ Supports clean URLs and standard browser refresh behavior. Uses a custom protoco
|
|
|
443
425
|
|---|---|---|---|
|
|
444
426
|
| `outDir` | `string` | ✅ | Vite/Webpack build output directory |
|
|
445
427
|
| `updatesDir` | `string` | — | Output directory for update packages. Default: `./dist_updates` |
|
|
428
|
+
| `rolloutRule` | `object` | — | Target whitelists: `{ deviceIds: string[] }`. |
|
|
446
429
|
| `version` | `string` | — | Explicit version string, overrides `packageJsonPath` |
|
|
447
430
|
| `packageJsonPath` | `string` | — | Custom path to `package.json`. Defaults to `process.cwd()/package.json` |
|
|
448
431
|
| `asarName` | `string` | — | ASAR filename. Default: `renderer.asar` |
|
|
@@ -456,6 +439,9 @@ Supports clean URLs and standard browser refresh behavior. Uses a custom protoco
|
|
|
456
439
|
|---|---|---|---|
|
|
457
440
|
| `updateUrl` | `string` | ✅ | Base URL where update files are hosted |
|
|
458
441
|
| `versionsDir` | `string` | ✅ | Local directory to store downloaded ASAR versions |
|
|
442
|
+
| `identity` | `object` | — | Fingerprint for rollouts logic (must include `deviceId`). |
|
|
443
|
+
| `allowPrerelease` | `boolean` | — | Skips prerelease tags if false. |
|
|
444
|
+
| `requestOptions` | `object` | — | Custom HTTP headers and query params. |
|
|
459
445
|
| `publicKey` | `string` | — | RSA public key (PEM) for signature verification |
|
|
460
446
|
| `autoDownload` | `boolean` | — | Auto-download without asking. Default: `false` |
|
|
461
447
|
| `autoPrompt` | `boolean` | — | Show built-in native dialogs. Default: `true` |
|
|
@@ -475,7 +461,7 @@ Supports clean URLs and standard browser refresh behavior. Uses a custom protoco
|
|
|
475
461
|
|---|---|---|
|
|
476
462
|
| `check()` | `Promise<CheckResult>` | Check for updates. Returns `{ updateAvailable, status, version, info }`. Status is `'idle' \| 'available' \| 'ready'` |
|
|
477
463
|
| `download(info?, onProgress?)` | `Promise<void>` | Download the update. Pass the `info` object from `check()` to avoid redundant network requests |
|
|
478
|
-
| `getLoadUrl()` | `string` | Returns the URL to load the latest ASAR (`app://renderer/` or `file://...`).
|
|
464
|
+
| `getLoadUrl(entry?)` | `string` | Returns the URL to load the latest ASAR (`app://renderer/` or `file://...`). `entry` defaults to `index.html`. Returns empty string if no update downloaded |
|
|
479
465
|
| `installAndRestart()` | `Promise<void>` | Apply the pending update and relaunch the app |
|
|
480
466
|
| `checkForUpdatesAndNotify()` | `Promise<void>` | All-in-one: check + prompt + download + install with built-in native dialogs |
|
|
481
467
|
| `setUpdatePending(version)` | `void` | Mark a version as downloaded and pending restart. Also fires `onStatusChanged` |
|
package/dist/main/index.cjs
CHANGED
|
@@ -103,13 +103,42 @@ var RouterHandler = class _RouterHandler {
|
|
|
103
103
|
return new Response("ASAR Not Found", { status: 404 });
|
|
104
104
|
}
|
|
105
105
|
let targetFile = import_path.default.join(asarPath, relativePath);
|
|
106
|
-
const
|
|
107
|
-
if (!
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
106
|
+
const isFileExist = import_fs.default.existsSync(targetFile) && import_fs.default.statSync(targetFile).isFile();
|
|
107
|
+
if (!isFileExist) {
|
|
108
|
+
let rawPath = relativePath.endsWith("/") ? relativePath.slice(0, -1) : relativePath;
|
|
109
|
+
let ext = import_path.default.extname(rawPath).toLowerCase();
|
|
110
|
+
if (ext && ext !== ".html") {
|
|
111
|
+
return new Response("Not Found", { status: 404 });
|
|
112
|
+
}
|
|
113
|
+
let fallbackSearchPath = rawPath;
|
|
114
|
+
let foundFallback = false;
|
|
115
|
+
while (true) {
|
|
116
|
+
let checkFile = "";
|
|
117
|
+
if (fallbackSearchPath !== "") {
|
|
118
|
+
checkFile = import_path.default.join(asarPath, fallbackSearchPath.endsWith(".html") ? fallbackSearchPath : fallbackSearchPath + ".html");
|
|
119
|
+
if (import_fs.default.existsSync(checkFile) && import_fs.default.statSync(checkFile).isFile()) {
|
|
120
|
+
targetFile = checkFile;
|
|
121
|
+
foundFallback = true;
|
|
122
|
+
break;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
checkFile = import_path.default.join(asarPath, fallbackSearchPath, "index.html");
|
|
126
|
+
if (import_fs.default.existsSync(checkFile) && import_fs.default.statSync(checkFile).isFile()) {
|
|
127
|
+
targetFile = checkFile;
|
|
128
|
+
foundFallback = true;
|
|
129
|
+
break;
|
|
130
|
+
}
|
|
131
|
+
if (fallbackSearchPath === "") break;
|
|
132
|
+
const lastSlashIndex = fallbackSearchPath.lastIndexOf("/");
|
|
133
|
+
if (lastSlashIndex === -1) {
|
|
134
|
+
fallbackSearchPath = "";
|
|
135
|
+
} else {
|
|
136
|
+
fallbackSearchPath = fallbackSearchPath.substring(0, lastSlashIndex);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
if (!foundFallback) {
|
|
140
|
+
return new Response("Not Found", { status: 404 });
|
|
141
|
+
}
|
|
113
142
|
}
|
|
114
143
|
const response = await import_electron.net.fetch((0, import_url.pathToFileURL)(targetFile).toString());
|
|
115
144
|
const headers = new Headers(response.headers);
|
|
@@ -322,15 +351,24 @@ var RenderUpdater = class {
|
|
|
322
351
|
get isUpdatePending() {
|
|
323
352
|
return import_semver.default.gt(this._diskVersion || "0.0.0", this._activeVersion || "0.0.0");
|
|
324
353
|
}
|
|
325
|
-
|
|
354
|
+
/**
|
|
355
|
+
* 获取加载 URL。
|
|
356
|
+
* @param entry 入口文件路径,默认为 'index.html'。
|
|
357
|
+
* 对于多页应用 (MPA),可以传入 'login/index.html' 等。
|
|
358
|
+
*/
|
|
359
|
+
getLoadUrl(entry = "index.html") {
|
|
326
360
|
try {
|
|
327
361
|
if (this._activeVersion && this._activeVersion !== "0.0.0") {
|
|
328
362
|
const asarPath = import_path2.default.join(this.versionsDir, this._activeVersion, "renderer.asar");
|
|
329
363
|
if (import_original_fs.default.existsSync(asarPath)) {
|
|
330
364
|
if (this.routerMode === "history") {
|
|
331
|
-
|
|
365
|
+
if (entry === "index.html" || entry === "/index.html") {
|
|
366
|
+
return `${this.protocolName}://renderer/`;
|
|
367
|
+
}
|
|
368
|
+
const normalizedEntry = entry.startsWith("/") ? entry.slice(1) : entry;
|
|
369
|
+
return `${this.protocolName}://renderer/${normalizedEntry}/`;
|
|
332
370
|
}
|
|
333
|
-
return `file://${asarPath}
|
|
371
|
+
return `file://${asarPath}/${entry}`;
|
|
334
372
|
}
|
|
335
373
|
}
|
|
336
374
|
} catch (e) {
|
|
@@ -628,22 +666,28 @@ ${info.releaseNotes || "\u65E0\u8BE6\u7EC6\u8BB0\u5F55\u3002"}`,
|
|
|
628
666
|
};
|
|
629
667
|
function setupUpdaterIPC(updater) {
|
|
630
668
|
import_electron3.ipcMain.handle("updater:check", () => updater.check());
|
|
631
|
-
import_electron3.ipcMain.handle("updater:download", () => updater.download());
|
|
669
|
+
import_electron3.ipcMain.handle("updater:download", (_, info) => updater.download(info));
|
|
632
670
|
import_electron3.ipcMain.handle("updater:installAndRestart", () => updater.installAndRestart());
|
|
633
671
|
import_electron3.ipcMain.on("updater:useVersion", (_, version) => updater.useVersion(version));
|
|
672
|
+
const originalOnDownloadProgress = updater.onDownloadProgress;
|
|
634
673
|
updater.onDownloadProgress = (percent) => {
|
|
674
|
+
if (originalOnDownloadProgress) originalOnDownloadProgress(percent);
|
|
635
675
|
const { webContents } = require("electron");
|
|
636
676
|
webContents.getAllWebContents().forEach((wc) => {
|
|
637
677
|
wc.send("updater:onDownloadProgress", percent);
|
|
638
678
|
});
|
|
639
679
|
};
|
|
680
|
+
const originalOnStatusChanged = updater.onStatusChanged;
|
|
640
681
|
updater.onStatusChanged = (status) => {
|
|
682
|
+
if (originalOnStatusChanged) originalOnStatusChanged(status);
|
|
641
683
|
const { webContents } = require("electron");
|
|
642
684
|
webContents.getAllWebContents().forEach((wc) => {
|
|
643
685
|
wc.send("updater:onStatusChanged", status);
|
|
644
686
|
});
|
|
645
687
|
};
|
|
688
|
+
const originalOnBeforeRestart = updater.onBeforeRestart;
|
|
646
689
|
updater.onBeforeRestart = () => {
|
|
690
|
+
if (originalOnBeforeRestart) originalOnBeforeRestart();
|
|
647
691
|
const { webContents } = require("electron");
|
|
648
692
|
webContents.getAllWebContents().forEach((wc) => {
|
|
649
693
|
wc.send("updater:onBeforeRestart");
|
package/dist/main/index.d.ts
CHANGED
|
@@ -30,7 +30,12 @@ export declare class RenderUpdater {
|
|
|
30
30
|
get activeVersion(): string;
|
|
31
31
|
get pendingVersion(): string;
|
|
32
32
|
get isUpdatePending(): boolean;
|
|
33
|
-
|
|
33
|
+
/**
|
|
34
|
+
* 获取加载 URL。
|
|
35
|
+
* @param entry 入口文件路径,默认为 'index.html'。
|
|
36
|
+
* 对于多页应用 (MPA),可以传入 'login/index.html' 等。
|
|
37
|
+
*/
|
|
38
|
+
getLoadUrl(entry?: string): string;
|
|
34
39
|
check(): Promise<{
|
|
35
40
|
updateAvailable: boolean;
|
|
36
41
|
version?: string;
|
package/dist/main/index.js
CHANGED
|
@@ -75,13 +75,42 @@ var RouterHandler = class _RouterHandler {
|
|
|
75
75
|
return new Response("ASAR Not Found", { status: 404 });
|
|
76
76
|
}
|
|
77
77
|
let targetFile = path.join(asarPath, relativePath);
|
|
78
|
-
const
|
|
79
|
-
if (!
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
78
|
+
const isFileExist = fs.existsSync(targetFile) && fs.statSync(targetFile).isFile();
|
|
79
|
+
if (!isFileExist) {
|
|
80
|
+
let rawPath = relativePath.endsWith("/") ? relativePath.slice(0, -1) : relativePath;
|
|
81
|
+
let ext = path.extname(rawPath).toLowerCase();
|
|
82
|
+
if (ext && ext !== ".html") {
|
|
83
|
+
return new Response("Not Found", { status: 404 });
|
|
84
|
+
}
|
|
85
|
+
let fallbackSearchPath = rawPath;
|
|
86
|
+
let foundFallback = false;
|
|
87
|
+
while (true) {
|
|
88
|
+
let checkFile = "";
|
|
89
|
+
if (fallbackSearchPath !== "") {
|
|
90
|
+
checkFile = path.join(asarPath, fallbackSearchPath.endsWith(".html") ? fallbackSearchPath : fallbackSearchPath + ".html");
|
|
91
|
+
if (fs.existsSync(checkFile) && fs.statSync(checkFile).isFile()) {
|
|
92
|
+
targetFile = checkFile;
|
|
93
|
+
foundFallback = true;
|
|
94
|
+
break;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
checkFile = path.join(asarPath, fallbackSearchPath, "index.html");
|
|
98
|
+
if (fs.existsSync(checkFile) && fs.statSync(checkFile).isFile()) {
|
|
99
|
+
targetFile = checkFile;
|
|
100
|
+
foundFallback = true;
|
|
101
|
+
break;
|
|
102
|
+
}
|
|
103
|
+
if (fallbackSearchPath === "") break;
|
|
104
|
+
const lastSlashIndex = fallbackSearchPath.lastIndexOf("/");
|
|
105
|
+
if (lastSlashIndex === -1) {
|
|
106
|
+
fallbackSearchPath = "";
|
|
107
|
+
} else {
|
|
108
|
+
fallbackSearchPath = fallbackSearchPath.substring(0, lastSlashIndex);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
if (!foundFallback) {
|
|
112
|
+
return new Response("Not Found", { status: 404 });
|
|
113
|
+
}
|
|
85
114
|
}
|
|
86
115
|
const response = await net.fetch(pathToFileURL(targetFile).toString());
|
|
87
116
|
const headers = new Headers(response.headers);
|
|
@@ -294,15 +323,24 @@ var RenderUpdater = class {
|
|
|
294
323
|
get isUpdatePending() {
|
|
295
324
|
return semver.gt(this._diskVersion || "0.0.0", this._activeVersion || "0.0.0");
|
|
296
325
|
}
|
|
297
|
-
|
|
326
|
+
/**
|
|
327
|
+
* 获取加载 URL。
|
|
328
|
+
* @param entry 入口文件路径,默认为 'index.html'。
|
|
329
|
+
* 对于多页应用 (MPA),可以传入 'login/index.html' 等。
|
|
330
|
+
*/
|
|
331
|
+
getLoadUrl(entry = "index.html") {
|
|
298
332
|
try {
|
|
299
333
|
if (this._activeVersion && this._activeVersion !== "0.0.0") {
|
|
300
334
|
const asarPath = path2.join(this.versionsDir, this._activeVersion, "renderer.asar");
|
|
301
335
|
if (fs2.existsSync(asarPath)) {
|
|
302
336
|
if (this.routerMode === "history") {
|
|
303
|
-
|
|
337
|
+
if (entry === "index.html" || entry === "/index.html") {
|
|
338
|
+
return `${this.protocolName}://renderer/`;
|
|
339
|
+
}
|
|
340
|
+
const normalizedEntry = entry.startsWith("/") ? entry.slice(1) : entry;
|
|
341
|
+
return `${this.protocolName}://renderer/${normalizedEntry}/`;
|
|
304
342
|
}
|
|
305
|
-
return `file://${asarPath}
|
|
343
|
+
return `file://${asarPath}/${entry}`;
|
|
306
344
|
}
|
|
307
345
|
}
|
|
308
346
|
} catch (e) {
|
|
@@ -600,22 +638,28 @@ ${info.releaseNotes || "\u65E0\u8BE6\u7EC6\u8BB0\u5F55\u3002"}`,
|
|
|
600
638
|
};
|
|
601
639
|
function setupUpdaterIPC(updater) {
|
|
602
640
|
ipcMain.handle("updater:check", () => updater.check());
|
|
603
|
-
ipcMain.handle("updater:download", () => updater.download());
|
|
641
|
+
ipcMain.handle("updater:download", (_, info) => updater.download(info));
|
|
604
642
|
ipcMain.handle("updater:installAndRestart", () => updater.installAndRestart());
|
|
605
643
|
ipcMain.on("updater:useVersion", (_, version) => updater.useVersion(version));
|
|
644
|
+
const originalOnDownloadProgress = updater.onDownloadProgress;
|
|
606
645
|
updater.onDownloadProgress = (percent) => {
|
|
646
|
+
if (originalOnDownloadProgress) originalOnDownloadProgress(percent);
|
|
607
647
|
const { webContents } = __require("electron");
|
|
608
648
|
webContents.getAllWebContents().forEach((wc) => {
|
|
609
649
|
wc.send("updater:onDownloadProgress", percent);
|
|
610
650
|
});
|
|
611
651
|
};
|
|
652
|
+
const originalOnStatusChanged = updater.onStatusChanged;
|
|
612
653
|
updater.onStatusChanged = (status) => {
|
|
654
|
+
if (originalOnStatusChanged) originalOnStatusChanged(status);
|
|
613
655
|
const { webContents } = __require("electron");
|
|
614
656
|
webContents.getAllWebContents().forEach((wc) => {
|
|
615
657
|
wc.send("updater:onStatusChanged", status);
|
|
616
658
|
});
|
|
617
659
|
};
|
|
660
|
+
const originalOnBeforeRestart = updater.onBeforeRestart;
|
|
618
661
|
updater.onBeforeRestart = () => {
|
|
662
|
+
if (originalOnBeforeRestart) originalOnBeforeRestart();
|
|
619
663
|
const { webContents } = __require("electron");
|
|
620
664
|
webContents.getAllWebContents().forEach((wc) => {
|
|
621
665
|
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.cjs
CHANGED
|
@@ -20,6 +20,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
20
20
|
// src/renderer/index.ts
|
|
21
21
|
var renderer_exports = {};
|
|
22
22
|
__export(renderer_exports, {
|
|
23
|
+
getRouterBase: () => getRouterBase,
|
|
23
24
|
getUpdater: () => getUpdater
|
|
24
25
|
});
|
|
25
26
|
module.exports = __toCommonJS(renderer_exports);
|
|
@@ -33,7 +34,17 @@ function getUpdater(namespace = "updaterAPI") {
|
|
|
33
34
|
}
|
|
34
35
|
return api;
|
|
35
36
|
}
|
|
37
|
+
function getRouterBase() {
|
|
38
|
+
if (typeof window === "undefined") return "/";
|
|
39
|
+
const pathname = window.location.pathname;
|
|
40
|
+
const htmlIndex = pathname.toLowerCase().lastIndexOf(".html");
|
|
41
|
+
if (htmlIndex !== -1) {
|
|
42
|
+
return pathname.substring(0, htmlIndex + 6);
|
|
43
|
+
}
|
|
44
|
+
return "/";
|
|
45
|
+
}
|
|
36
46
|
// Annotate the CommonJS export names for ESM import in node:
|
|
37
47
|
0 && (module.exports = {
|
|
48
|
+
getRouterBase,
|
|
38
49
|
getUpdater
|
|
39
50
|
});
|
package/dist/renderer/index.d.ts
CHANGED
|
@@ -6,18 +6,25 @@ 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.
|
|
21
21
|
* This should only be used in the renderer process (e.g. standard Vue/React contexts).
|
|
22
22
|
*/
|
|
23
23
|
export declare function getUpdater(namespace?: string): UpdaterClientAPI;
|
|
24
|
+
/**
|
|
25
|
+
* 自动探测当前页面的路由基准路径 (Base URL)
|
|
26
|
+
* 专门用于解决多页面 (MPA) 在 History 模式下刷新后 404 的问题。
|
|
27
|
+
* 例如:当 Electron 加载的是 app://renderer/login/index.html/ 时,
|
|
28
|
+
* 本函数将返回 "/login/index.html/",您可以直接将其传给 createWebHistory()。
|
|
29
|
+
*/
|
|
30
|
+
export declare function getRouterBase(): string;
|
package/dist/renderer/index.js
CHANGED
|
@@ -9,6 +9,16 @@ function getUpdater(namespace = "updaterAPI") {
|
|
|
9
9
|
}
|
|
10
10
|
return api;
|
|
11
11
|
}
|
|
12
|
+
function getRouterBase() {
|
|
13
|
+
if (typeof window === "undefined") return "/";
|
|
14
|
+
const pathname = window.location.pathname;
|
|
15
|
+
const htmlIndex = pathname.toLowerCase().lastIndexOf(".html");
|
|
16
|
+
if (htmlIndex !== -1) {
|
|
17
|
+
return pathname.substring(0, htmlIndex + 6);
|
|
18
|
+
}
|
|
19
|
+
return "/";
|
|
20
|
+
}
|
|
12
21
|
export {
|
|
22
|
+
getRouterBase,
|
|
13
23
|
getUpdater
|
|
14
24
|
};
|