electron-updater-for-render 2.0.0-beta.2 → 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 CHANGED
@@ -21,8 +21,9 @@
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 模式 SPA 支持**:内置自定义协议处理器(`app://`),支持 Vue/React History 路由模式,零配置零样板代码
25
- - **三端隔离 SDK**:原生提供 `main`、`preload`、`renderer` 独立切入点,防止前端构建打包时发生 Node.js 模块(如 `fs`)污染,内置完美的 TypeScript 类型推导。
24
+ - **History 模式支持**:内置支持 Vue/React History 路由模式。
25
+ - **多页应用 (MPA) 支持**:支持通过 `getLoadUrl(entry)` 加载不同的入口文件。
26
+ - **极简 SDK 接入**:提供 `/main`, `/preload`, `/renderer` 导出,内置 `getRouterBase` 路由基准路径探测工具。
26
27
  - **精准灰度分发**:支持基于 `deviceId` 的环境身份匹配,配合 `updater.config.ts` 中的 `rolloutRule` 可实现百分百定点静默空投。
27
28
  - **Semver 内测通道**:基于 `allowPrerelease` 提供生产环境与内测环境的天然逻辑隔离,保障正式服用户免受 Beta 版干扰。
28
29
  - **动态网关鉴权**:通过 `requestOptions` 可在 HTTP 请求中自定义 Headers(如 Oauth / Bearer Token)及 Query 参数。
@@ -189,9 +190,10 @@ app.whenReady().then(async () => {
189
190
  webPreferences: { preload: join(__dirname, '../preload/index.js') }
190
191
  })
191
192
 
192
- // 优先加载已下载的热更新 ASAR,否则回退到开发服务器
193
- const loadUrl = updater.getLoadUrl()
194
- mainWindow.loadURL(loadUrl || 'http://localhost:5173')
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')}`)
195
197
 
196
198
  // 延迟触发自动检测(支持拦截强制更新,autoPrompt 为 true 时会弹窗提示)
197
199
  setTimeout(() => updater.checkForUpdatesAndNotify(), 3000)
@@ -223,7 +225,11 @@ contextBridge.exposeInMainWorld('updaterAPI', exposeUpdaterPreload(ipcRenderer))
223
225
  ```vue
224
226
  <script setup lang="ts">
225
227
  import { ref, onMounted, onUnmounted } from 'vue'
226
- import { getUpdater } from 'electron-updater-for-render/renderer'
228
+ import { getUpdater, getRouterBase } from 'electron-updater-for-render/renderer'
229
+
230
+ // 自动探测当前 Window 上下文对应的 History Base
231
+ const routerBase = getRouterBase()
232
+ const updater = getUpdater()
227
233
 
228
234
  // 状态:'idle' | 'checking' | 'available' | 'downloading' | 'ready' | 'no-update'
229
235
  const status = ref<'idle' | 'checking' | 'available' | 'downloading' | 'ready' | 'no-update'>('idle')
@@ -366,31 +372,49 @@ onUnmounted(() => {
366
372
 
367
373
  本库完美支持 Electron 常用的文件加载模式(Hash)和现代单页应用路由模式(History)。
368
374
 
369
- ### 1. Hash 模式(默认推荐)
370
- 这是最稳健的选择,使用 Electron 标准的 `file://` 协议。
375
+ ### 1. Hash 模式 (推荐方案:零配置、零侵入)
376
+ 这是 Electron 环境下的**工业级标准实践**。它对物理路径不敏感,不需要配置任何 Base,前端项目完全不需要引入插件的任何 JS 逻辑。
371
377
 
372
- - **主进程**:无需任何额外配置(`routerMode` 默认为 `'hash'`)。
378
+ - **主进程**:无需额外配置(`routerMode` 默认为 `'hash'`)。
373
379
  - **前端 (Vite)**:`vite.config.ts` 中的 `base` 设为 `'./'`(或不填)。
374
380
  - **前端 (Router)**:使用 `createWebHashHistory()`。
381
+ - **优点**:真正的“零感知”,前端代码与热更新逻辑完全解耦。
375
382
 
376
- ### 2. History 模式(企业级方案)
377
- 支持美观的 URL 和标准浏览器重载行为。由于 `file://` 不支持标准路由解析,本库会自动启用自定义协议(默认 `app://`)进行转发。
383
+ ### 2. History 模式 (高级方案)
384
+ 支持美观的 URL 和标准浏览器刷新行为。由于 MPA 存在物理路径认知偏差,需要对齐基准路径(Base)。
378
385
 
379
386
  - **主进程**:
380
387
  ```typescript
381
388
  new RenderUpdater({
382
- updateUrl: '...',
383
- versionsDir: '...',
384
- routerMode: 'history', // 开启 History 路由支持
385
- protocol: 'my-app' // 可选:自定义协议名 (默认 'app')
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: [...]
386
411
  })
387
412
  ```
388
- > 💡 **全自动化**:协议注册和特权赋权逻辑已在库内部**全自动处理**,您无需编写任何 `registerSchemesAsPrivileged` 等繁琐代码。
389
413
 
390
- - **前端构建工具(Vite、Webpack 等)**:**关键配置** —— 必须将 **公共路径 (Public Path / Base)** 设置为 `/`,确保资源引用为绝对路径,防止在深层路由下刷新出现 404。
391
- - **Vite**:`base: '/'`
392
- - **Webpack**:`output.publicPath: '/'`
393
- - **前端路由 (Router)**:使用 `createWebHistory()`。
414
+ ### 3. 多页应用 (MPA) 支持
415
+ - **多窗口多入口**:支持为不同窗口指定不同的 HTML 入口。
416
+ - **History 模式刷新**:支持在 History 模式下进行页面刷新。
417
+ - **基准路径探测**:配合 `getRouterBase()` 自动识别当前的路由基准路径。
394
418
 
395
419
  ---
396
420
 
@@ -434,14 +458,14 @@ onUnmounted(() => {
434
458
 
435
459
  ### `RenderUpdater` 实例方法
436
460
 
437
- | 方法 | 返回值 | 说明 |
461
+ | 方法 | 返回值 | 描述 |
438
462
  |---|---|---|
439
- | `check()` | `Promise<CheckResult>` | 检查更新。返回 `{ updateAvailable, status, version, info }`,其中 `status` 为 `'idle' \| 'available' \| 'ready'` |
440
- | `download(info?, onProgress?)` | `Promise<void>` | 下载更新。将 `check()` 返回的 `info` 传入,可避免重复请求 latest.json |
441
- | `getLoadUrl()` | `string` | 返回最新 ASAR 的加载地址(`app://renderer/` 或 `file://...`),未安装任何更新时返回空字符串 |
442
- | `installAndRestart()` | `Promise<void>` | 应用待安装更新并重启应用 |
443
- | `checkForUpdatesAndNotify()` | `Promise<void>` | 一键式:检查 + 弹窗 + 下载 + 安装,使用内置原生弹窗 |
444
- | `setUpdatePending(version)` | `void` | 将某版本标记为已下载待重启,同时触发 `onStatusChanged` |
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` |
445
469
  | `useVersion(version)` | `void` | 立即切换磁盘和内存双重版本指针 |
446
470
  | `activeVersion` | `string`(getter) | 当前内存中正在运行的版本(本次会话) |
447
471
  | `pendingVersion` | `string`(getter) | 磁盘上最新已下载版本 |
package/Readme.md CHANGED
@@ -21,8 +21,9 @@ 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 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.
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`.
26
27
  - **Precision Canary Releases**: Use `identity.deviceId` and `rolloutRule` to drop updates silently to a whitelist of device IDs.
27
28
  - **Semver Beta Channels**: Built-in Beta/RC channel toggling via `allowPrerelease` guarantees Stable users are isolated from beta patches.
28
29
  - **Dynamic Request Gateways**: Send custom Authenticated Headers (e.g., Oauth/Bearer tokens) and Queries via `requestOptions` when checking for updates.
@@ -187,9 +188,10 @@ app.whenReady().then(async () => {
187
188
  webPreferences: { preload: join(__dirname, '../preload/index.js') }
188
189
  })
189
190
 
190
- // Load the latest downloaded ASAR, fallback to dev server if no update downloaded yet
191
- const loadUrl = updater.getLoadUrl()
192
- mainWindow.loadURL(loadUrl || 'http://localhost:5173')
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')}`)
193
195
 
194
196
  // Auto-check on startup (respects forceUpdate, shows dialogs if autoPrompt: true)
195
197
  setTimeout(() => updater.checkForUpdatesAndNotify(), 3000)
@@ -221,7 +223,11 @@ The following example demonstrates the complete update flow with:
221
223
  ```vue
222
224
  <script setup lang="ts">
223
225
  import { ref, onMounted, onUnmounted } from 'vue'
224
- import { getUpdater } from 'electron-updater-for-render/renderer'
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()
225
231
 
226
232
  // Status: 'idle' | 'checking' | 'available' | 'downloading' | 'ready' | 'no-update'
227
233
  const status = ref<'idle' | 'checking' | 'available' | 'downloading' | 'ready' | 'no-update'>('idle')
@@ -365,31 +371,49 @@ onUnmounted(() => {
365
371
 
366
372
  The library supports both standard Electron file-loading (Hash) and modern SPA routing (History) via custom protocols.
367
373
 
368
- ### 1. Hash Mode (Default)
369
- Recommended for simple apps. It uses standard `file://` protocol.
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.
370
376
 
371
- - **Main Process**: No extra config needed (defaults to `hash`).
377
+ - **Main Process**: No extra config needed (defaults to `'hash'`).
372
378
  - **Renderer (Vite)**: Set `base: './'` (or omit) in `vite.config.ts`.
373
379
  - **Renderer (Router)**: Use `createWebHashHistory()`.
380
+ - **Pros**: Pure decoupling; your frontend project remains completely unaware of the updater's existence.
374
381
 
375
- ### 2. History Mode (Modern)
376
- Supports clean URLs and standard browser refresh behavior. Uses a custom protocol (default `app://`) to bypass `file://` limitations.
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.
377
384
 
378
385
  - **Main Process**:
379
386
  ```typescript
380
387
  new RenderUpdater({
381
- updateUrl: '...',
382
- versionsDir: '...',
383
- routerMode: 'history', // Enable history mode
384
- protocol: 'my-app' // Optional: custom protocol name (default: 'app')
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: [...]
385
410
  })
386
411
  ```
387
- > 💡 **No Boilerplate**: Protocol registration and privileged scheme setup are **automated** internally. You don't need to call `registerSchemesAsPrivileged` manually.
388
412
 
389
- - **Renderer (Build Tool — Vite, Webpack, etc.)**: **CRITICAL** — set your build tool's public base path to `/` for absolute asset URLs. This prevents 404 errors when refreshing on deep-nested routes.
390
- - **Vite**: `base: '/'`
391
- - **Webpack**: `output.publicPath: '/'`
392
- - **Renderer (Router)**: Use `createWebHistory()`.
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.
393
417
 
394
418
  ---
395
419
 
@@ -437,7 +461,7 @@ Supports clean URLs and standard browser refresh behavior. Uses a custom protoco
437
461
  |---|---|---|
438
462
  | `check()` | `Promise<CheckResult>` | Check for updates. Returns `{ updateAvailable, status, version, info }`. Status is `'idle' \| 'available' \| 'ready'` |
439
463
  | `download(info?, onProgress?)` | `Promise<void>` | Download the update. Pass the `info` object from `check()` to avoid redundant network requests |
440
- | `getLoadUrl()` | `string` | Returns the URL to load the latest ASAR (`app://renderer/` or `file://...`). Empty string if no update downloaded |
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 |
441
465
  | `installAndRestart()` | `Promise<void>` | Apply the pending update and relaunch the app |
442
466
  | `checkForUpdatesAndNotify()` | `Promise<void>` | All-in-one: check + prompt + download + install with built-in native dialogs |
443
467
  | `setUpdatePending(version)` | `void` | Mark a version as downloaded and pending restart. Also fires `onStatusChanged` |
@@ -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 ext = import_path.default.extname(relativePath);
107
- if (!relativePath || relativePath === "/") {
108
- targetFile = import_path.default.join(asarPath, "index.html");
109
- } else if (ext === "") {
110
- targetFile = import_path.default.join(asarPath, "index.html");
111
- } else if (!import_fs.default.existsSync(targetFile)) {
112
- targetFile = import_path.default.join(asarPath, "index.html");
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
- getLoadUrl() {
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
- return `${this.protocolName}://renderer/`;
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}/index.html`;
371
+ return `file://${asarPath}/${entry}`;
334
372
  }
335
373
  }
336
374
  } catch (e) {
@@ -30,7 +30,12 @@ export declare class RenderUpdater {
30
30
  get activeVersion(): string;
31
31
  get pendingVersion(): string;
32
32
  get isUpdatePending(): boolean;
33
- getLoadUrl(): string;
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;
@@ -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 ext = path.extname(relativePath);
79
- if (!relativePath || relativePath === "/") {
80
- targetFile = path.join(asarPath, "index.html");
81
- } else if (ext === "") {
82
- targetFile = path.join(asarPath, "index.html");
83
- } else if (!fs.existsSync(targetFile)) {
84
- targetFile = path.join(asarPath, "index.html");
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
- getLoadUrl() {
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
- return `${this.protocolName}://renderer/`;
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}/index.html`;
343
+ return `file://${asarPath}/${entry}`;
306
344
  }
307
345
  }
308
346
  } catch (e) {
@@ -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
  });
@@ -21,3 +21,10 @@ export interface UpdaterClientAPI {
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;
@@ -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
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "electron-updater-for-render",
3
- "version": "2.0.0-beta.2",
3
+ "version": "2.0.1",
4
4
  "description": "A lightweight incremental updater for Electron renderer processes",
5
5
  "type": "module",
6
6
  "bin": {