electron-updater-for-render 1.1.2-beta.3 → 1.1.2-beta.5

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
@@ -275,6 +275,38 @@ const installUpdate = () => {
275
275
 
276
276
  ---
277
277
 
278
+ ## 🛣️ 路由模式配置 (Hash vs History)
279
+
280
+ 本库完美支持 Electron 常用的文件加载模式(Hash)和现代单页应用路由模式(History)。
281
+
282
+ ### 1. Hash 模式(默认推荐)
283
+ 这是最稳健的选择,使用 Electron 标准的 `file://` 协议。
284
+
285
+ - **主进程**:无需任何额外配置(`routerMode` 默认为 `'hash'`)。
286
+ - **前端 (Vite)**:`vite.config.ts` 中的 `base` 设为 `'./'`(或不填)。
287
+ - **前端 (Router)**:使用 `createWebHashHistory()`。
288
+
289
+ ### 2. History 模式(企业级方案)
290
+ 支持美观的 URL 和标准浏览器重载行为。由于 `file://` 不支持标准路解析,本库会自动启用自定义协议(默认 `app://`)进行转发。
291
+
292
+ - **主进程**:
293
+ ```typescript
294
+ new RenderUpdater({
295
+ updateUrl: '...',
296
+ versionsDir: '...',
297
+ routerMode: 'history', // 开启 History 路由支持
298
+ protocol: 'my-app' // 可选:自定义协议名 (默认 'app')
299
+ })
300
+ ```
301
+ > 💡 **全自动化:** 协议注册和特权赋权逻辑已在库内部**全自动处理**,您无需在顶层编写任何 `registerSchemesAsPrivileged` 等繁琐代码。
302
+
303
+ - **前端构建工具 (Vite, Webpack, 等)**:**关键配置** —— 您必须将构建工具的 **公共路径 (Public Path / Base)** 设置为 `/`。这是为了确保生成的资源引用(JS/CSS/图片)为绝对路径,防止在深层路由(如 `/home/settings`)下刷新页面时出现资源加载 404。
304
+ - **Vite**:`base: '/'`
305
+ - **Webpack**:`output.publicPath: '/'`
306
+ - **前端路由 (Router)**:使用 `createWebHistory()`。
307
+
308
+ ---
309
+
278
310
  ## ⚙️ API 参考
279
311
 
280
312
  ### `BuilderOptions`(CLI 配置参数)
@@ -305,6 +337,8 @@ const installUpdate = () => {
305
337
  | `onDownloadComplete` | `function` | — | 下载完成钩子:`(info, doInstall) => void` |
306
338
  | `onError` | `function` | — | 错误回调:`(error: Error) => void` |
307
339
  | `onBeforeRestart` | `async function` | — | `app.relaunch()` 前的异步钩子,可用于保存状态 |
340
+ | `routerMode` | `'hash' \| 'history'` | — | 路由模式选择。默认 `'hash'` |
341
+ | `protocol` | `string` | — | History 模式下的自定义协议名。默认 `'app'` |
308
342
 
309
343
  ---
310
344
 
package/Readme.md CHANGED
@@ -271,6 +271,38 @@ const installUpdate = () => {
271
271
 
272
272
  ---
273
273
 
274
+ ## 🛣️ Routing Modes (Hash vs History)
275
+
276
+ The library supports both standard Electron file-loading (Hash) and modern SPA routing (History) via custom protocols.
277
+
278
+ ### 1. Hash Mode (Default)
279
+ Recommended for simple apps. It uses standard `file://` protocol.
280
+
281
+ - **Main Process**: No extra config needed (defaults to `hash`).
282
+ - **Renderer (Vite)**: Set `base: './'` (or omit) in `vite.config.ts`.
283
+ - **Renderer (Router)**: Use `createWebHashHistory()`.
284
+
285
+ ### 2. History Mode (Modern)
286
+ Supports clean URLs and standard browser refresh behavior. It uses a custom protocol (default `app://`) to bypass `file://` limitations.
287
+
288
+ - **Main Process**:
289
+ ```typescript
290
+ new RenderUpdater({
291
+ updateUrl: '...',
292
+ versionsDir: '...',
293
+ routerMode: 'history', // Enable history mode
294
+ protocol: 'my-app' // Optional: custom protocol name (default: 'app')
295
+ })
296
+ ```
297
+ > 💡 **No Boilerplate**: Protocol registration and privileged scheme setup are **automated** internally. You don't need to call `registerSchemesAsPrivileged` manually.
298
+
299
+ - **Renderer (Build Tool: Vite, Webpack, etc.)**: **CRITICAL** — You must set your build tool's **Public Path** or **Base** to `/` to ensure absolute asset URLs. This prevents 404 errors for assets when refreshing the page on deep-nested routes.
300
+ - **Vite**: `base: '/'`
301
+ - **Webpack**: `output.publicPath: '/'`
302
+ - **Renderer (Router)**: Use `createWebHistory()`.
303
+
304
+ ---
305
+
274
306
  ## ⚙️ API Reference
275
307
 
276
308
  ### `BuilderOptions` (CLI Config)
@@ -301,6 +333,8 @@ const installUpdate = () => {
301
333
  | `onDownloadComplete` | `function` | — | Completion hook: `(info, doInstall) => void` |
302
334
  | `onError` | `function` | — | Error handler: `(error: Error) => void` |
303
335
  | `onBeforeRestart` | `async function` | — | Called before `app.relaunch()`. Await-able for graceful shutdown |
336
+ | `routerMode` | `'hash' \| 'history'` | — | Routing mode. Default: `'hash'` |
337
+ | `protocol` | `string` | — | Custom protocol name for history mode. Default: `'app'` |
304
338
 
305
339
  ---
306
340
 
package/dist/bin/cli.cjs CHANGED
@@ -39,8 +39,8 @@ var import_fs = __toESM(require("fs"), 1);
39
39
  var import_path = __toESM(require("path"), 1);
40
40
  var import_crypto = __toESM(require("crypto"), 1);
41
41
  async function createUpdatePackage(options) {
42
- const asar = await import("asar").catch(() => {
43
- throw new Error('Please install "asar" as a devDependency to use the builder.');
42
+ const asar = await import("@electron/asar").catch(() => {
43
+ throw new Error('Please install "@electron/asar" as a devDependency to use the builder.');
44
44
  });
45
45
  const {
46
46
  outDir,
package/dist/bin/cli.js CHANGED
@@ -12,8 +12,8 @@ import fs from "fs";
12
12
  import path from "path";
13
13
  import crypto from "crypto";
14
14
  async function createUpdatePackage(options) {
15
- const asar = await import("asar").catch(() => {
16
- throw new Error('Please install "asar" as a devDependency to use the builder.');
15
+ const asar = await import("@electron/asar").catch(() => {
16
+ throw new Error('Please install "@electron/asar" as a devDependency to use the builder.');
17
17
  });
18
18
  const {
19
19
  outDir,
@@ -41,8 +41,8 @@ function defineConfig(config) {
41
41
  return config;
42
42
  }
43
43
  async function createUpdatePackage(options) {
44
- const asar = await import("asar").catch(() => {
45
- throw new Error('Please install "asar" as a devDependency to use the builder.');
44
+ const asar = await import("@electron/asar").catch(() => {
45
+ throw new Error('Please install "@electron/asar" as a devDependency to use the builder.');
46
46
  });
47
47
  const {
48
48
  outDir,
@@ -6,8 +6,8 @@ function defineConfig(config) {
6
6
  return config;
7
7
  }
8
8
  async function createUpdatePackage(options) {
9
- const asar = await import("asar").catch(() => {
10
- throw new Error('Please install "asar" as a devDependency to use the builder.');
9
+ const asar = await import("@electron/asar").catch(() => {
10
+ throw new Error('Please install "@electron/asar" as a devDependency to use the builder.');
11
11
  });
12
12
  const {
13
13
  outDir,
@@ -33,12 +33,118 @@ __export(main_exports, {
33
33
  RenderUpdater: () => RenderUpdater
34
34
  });
35
35
  module.exports = __toCommonJS(main_exports);
36
- var import_path = __toESM(require("path"), 1);
37
- var import_original_fs = __toESM(require("original-fs"), 1);
36
+ var import_path2 = __toESM(require("path"), 1);
37
+ var import_original_fs2 = __toESM(require("original-fs"), 1);
38
38
  var import_crypto = __toESM(require("crypto"), 1);
39
- var import_electron = require("electron");
39
+ var import_semver = __toESM(require("semver"), 1);
40
+ var import_electron2 = require("electron");
40
41
  var import_promises = require("stream/promises");
41
42
  var import_stream = require("stream");
43
+
44
+ // src/main/router-handler.ts
45
+ var import_electron = require("electron");
46
+ var import_url = require("url");
47
+ var import_path = __toESM(require("path"), 1);
48
+ var import_original_fs = __toESM(require("original-fs"), 1);
49
+ var RouterHandler = class _RouterHandler {
50
+ static protocolName = "app";
51
+ static isSchemaRegistered = false;
52
+ static isHandlerInited = false;
53
+ versionsDir;
54
+ getActiveVersion;
55
+ constructor(options) {
56
+ this.versionsDir = options.versionsDir;
57
+ this.getActiveVersion = options.getActiveVersion;
58
+ this.init();
59
+ }
60
+ /**
61
+ * 零摩擦初始化入口
62
+ * 自动处理 Electron 生命周期:准备前注册特权,准备后挂载拦截器。
63
+ */
64
+ static setup(options) {
65
+ const { protocol: protocolName, versionsDir, getActiveVersion } = options;
66
+ if (!this.isSchemaRegistered) {
67
+ if (!import_electron.app.isReady()) {
68
+ this.protocolName = protocolName;
69
+ import_electron.protocol.registerSchemesAsPrivileged([
70
+ {
71
+ scheme: protocolName,
72
+ privileges: {
73
+ standard: true,
74
+ secure: true,
75
+ supportFetchAPI: true,
76
+ allowServiceWorkers: true,
77
+ corsEnabled: true
78
+ }
79
+ }
80
+ ]);
81
+ this.isSchemaRegistered = true;
82
+ } else {
83
+ console.warn(`[RouterHandler] App already ready. Cannot register scheme "${protocolName}".`);
84
+ }
85
+ }
86
+ if (!this.isHandlerInited) {
87
+ import_electron.app.whenReady().then(() => {
88
+ new _RouterHandler({ versionsDir, getActiveVersion });
89
+ this.isHandlerInited = true;
90
+ });
91
+ }
92
+ }
93
+ init() {
94
+ const protocolName = _RouterHandler.protocolName;
95
+ import_electron.protocol.handle(protocolName, async (request) => {
96
+ const url = new URL(request.url);
97
+ const { host, pathname } = url;
98
+ if (host === "renderer") {
99
+ const relativePath = pathname.startsWith("/") ? pathname.slice(1) : pathname;
100
+ const activeVersion = this.getActiveVersion();
101
+ const asarPath = import_path.default.join(this.versionsDir, activeVersion, "renderer.asar");
102
+ if (!import_original_fs.default.existsSync(asarPath)) {
103
+ return new Response("Not Found", { status: 404 });
104
+ }
105
+ const ext = import_path.default.extname(relativePath);
106
+ const isRequestingFile = ext !== "" && !relativePath.endsWith("/");
107
+ let targetFile = import_path.default.join(asarPath, relativePath);
108
+ if (!isRequestingFile || relativePath === "index.html" || relativePath === "") {
109
+ targetFile = import_path.default.join(asarPath, "index.html");
110
+ }
111
+ const response = await import_electron.net.fetch((0, import_url.pathToFileURL)(targetFile).toString());
112
+ const headers = new Headers(response.headers);
113
+ headers.set("Content-Type", this.getMimeType(targetFile));
114
+ headers.set("Access-Control-Allow-Origin", "*");
115
+ return new Response(response.body, {
116
+ status: response.status,
117
+ statusText: response.statusText,
118
+ headers
119
+ });
120
+ }
121
+ return new Response("Not Found", { status: 404 });
122
+ });
123
+ }
124
+ getMimeType(filePath) {
125
+ const ext = import_path.default.extname(filePath).toLowerCase();
126
+ const mimeMap = {
127
+ ".html": "text/html",
128
+ ".js": "application/javascript",
129
+ ".mjs": "application/javascript",
130
+ ".css": "text/css",
131
+ ".json": "application/json",
132
+ ".png": "image/png",
133
+ ".jpg": "image/jpeg",
134
+ ".jpeg": "image/jpeg",
135
+ ".gif": "image/gif",
136
+ ".svg": "image/svg+xml",
137
+ ".ico": "image/x-icon",
138
+ ".woff": "font/woff",
139
+ ".woff2": "font/woff2",
140
+ ".ttf": "font/ttf",
141
+ ".otf": "font/otf"
142
+ };
143
+ return mimeMap[ext] || "application/octet-stream";
144
+ }
145
+ };
146
+
147
+ // src/main/index.ts
42
148
  var RenderUpdater = class {
43
149
  versionsDir;
44
150
  currentVersionFile;
@@ -46,6 +152,8 @@ var RenderUpdater = class {
46
152
  activeVersion;
47
153
  publicKey;
48
154
  isDownloading = false;
155
+ routerMode;
156
+ protocolName;
49
157
  autoDownload;
50
158
  autoPrompt;
51
159
  maxVersionsToKeep;
@@ -56,7 +164,7 @@ var RenderUpdater = class {
56
164
  onBeforeRestart;
57
165
  constructor(options) {
58
166
  this.versionsDir = options.versionsDir;
59
- this.currentVersionFile = import_path.default.join(this.versionsDir, "current.json");
167
+ this.currentVersionFile = import_path2.default.join(this.versionsDir, "current.json");
60
168
  this.baseUrl = options.updateUrl;
61
169
  this.publicKey = options.publicKey;
62
170
  this.autoDownload = options.autoDownload ?? false;
@@ -67,25 +175,34 @@ var RenderUpdater = class {
67
175
  this.onDownloadComplete = options.onDownloadComplete;
68
176
  this.onError = options.onError;
69
177
  this.onBeforeRestart = options.onBeforeRestart;
70
- if (!import_original_fs.default.existsSync(this.versionsDir)) {
71
- import_original_fs.default.mkdirSync(this.versionsDir, { recursive: true });
178
+ this.routerMode = options.routerMode ?? "hash";
179
+ this.protocolName = options.protocol ?? "app";
180
+ if (!import_original_fs2.default.existsSync(this.versionsDir)) {
181
+ import_original_fs2.default.mkdirSync(this.versionsDir, { recursive: true });
72
182
  }
73
183
  this.activeVersion = this.readCurrentVersionFromFile();
74
184
  this.cleanOldVersions();
185
+ if (this.routerMode === "history") {
186
+ RouterHandler.setup({
187
+ protocol: this.protocolName,
188
+ versionsDir: this.versionsDir,
189
+ getActiveVersion: () => this.activeVersion
190
+ });
191
+ }
75
192
  }
76
193
  cleanOldVersions() {
77
194
  try {
78
- if (!import_original_fs.default.existsSync(this.versionsDir)) return;
79
- const items = import_original_fs.default.readdirSync(this.versionsDir);
195
+ if (!import_original_fs2.default.existsSync(this.versionsDir)) return;
196
+ const items = import_original_fs2.default.readdirSync(this.versionsDir);
80
197
  const versionDirs = [];
81
198
  for (const item of items) {
82
199
  if (item === "current.json") continue;
83
- const fullPath = import_path.default.join(this.versionsDir, item);
84
- if (import_original_fs.default.statSync(fullPath).isDirectory()) {
200
+ const fullPath = import_path2.default.join(this.versionsDir, item);
201
+ if (import_original_fs2.default.statSync(fullPath).isDirectory()) {
85
202
  versionDirs.push(item);
86
203
  }
87
204
  }
88
- versionDirs.sort((a, b) => this.compareVersions(b, a));
205
+ versionDirs.sort((a, b) => import_semver.default.compare(import_semver.default.coerce(b) ?? b, import_semver.default.coerce(a) ?? a));
89
206
  const activeIdx = versionDirs.indexOf(this.activeVersion);
90
207
  const safeVersions = /* @__PURE__ */ new Set();
91
208
  if (activeIdx !== -1) safeVersions.add(this.activeVersion);
@@ -100,7 +217,7 @@ var RenderUpdater = class {
100
217
  for (const v of versionDirs) {
101
218
  if (!safeVersions.has(v)) {
102
219
  console.info(`[RenderUpdater] Cleaning up old version: ${v}`);
103
- import_original_fs.default.rmSync(import_path.default.join(this.versionsDir, v), { recursive: true, force: true });
220
+ import_original_fs2.default.rmSync(import_path2.default.join(this.versionsDir, v), { recursive: true, force: true });
104
221
  }
105
222
  }
106
223
  } catch (e) {
@@ -108,9 +225,9 @@ var RenderUpdater = class {
108
225
  }
109
226
  }
110
227
  readCurrentVersionFromFile() {
111
- if (import_original_fs.default.existsSync(this.currentVersionFile)) {
228
+ if (import_original_fs2.default.existsSync(this.currentVersionFile)) {
112
229
  try {
113
- const current = JSON.parse(import_original_fs.default.readFileSync(this.currentVersionFile, "utf-8"));
230
+ const current = JSON.parse(import_original_fs2.default.readFileSync(this.currentVersionFile, "utf-8"));
114
231
  return current.version;
115
232
  } catch {
116
233
  return "0.0.0";
@@ -119,19 +236,23 @@ var RenderUpdater = class {
119
236
  return "0.0.0";
120
237
  }
121
238
  hasAnyVersion() {
122
- return import_original_fs.default.existsSync(this.currentVersionFile);
239
+ return import_original_fs2.default.existsSync(this.currentVersionFile);
123
240
  }
124
241
  /**
125
- * Returns the file:// URL to the latest renderer.asar index.html,
126
- * or empty string if no update is available.
242
+ * Returns the correct URL to load in the BrowserWindow.
243
+ * If history mode is enabled, it returns the custom protocol URL.
244
+ * Otherwise, it returns the local file URL with # hash.
127
245
  */
128
246
  getLoadUrl() {
129
247
  try {
130
- if (import_original_fs.default.existsSync(this.currentVersionFile)) {
131
- const current = JSON.parse(import_original_fs.default.readFileSync(this.currentVersionFile, "utf-8"));
248
+ if (import_original_fs2.default.existsSync(this.currentVersionFile)) {
249
+ const current = JSON.parse(import_original_fs2.default.readFileSync(this.currentVersionFile, "utf-8"));
132
250
  const version = current.version;
133
- const asarPath = import_path.default.join(this.versionsDir, version, "renderer.asar");
134
- if (import_original_fs.default.existsSync(asarPath)) {
251
+ const asarPath = import_path2.default.join(this.versionsDir, version, "renderer.asar");
252
+ if (import_original_fs2.default.existsSync(asarPath)) {
253
+ if (this.routerMode === "history") {
254
+ return `${this.protocolName}://renderer/`;
255
+ }
135
256
  return `file://${asarPath}/index.html`;
136
257
  }
137
258
  }
@@ -146,7 +267,7 @@ var RenderUpdater = class {
146
267
  if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
147
268
  const remoteInfo = await response.json();
148
269
  const currentVersion = this.activeVersion;
149
- if (this.compareVersions(remoteInfo.version, currentVersion) > 0) {
270
+ if (import_semver.default.gt(remoteInfo.version, currentVersion)) {
150
271
  return { updateAvailable: true, version: remoteInfo.version, info: remoteInfo };
151
272
  }
152
273
  return { updateAvailable: false };
@@ -165,19 +286,19 @@ var RenderUpdater = class {
165
286
  const response = await fetch(`${this.baseUrl}/latest.json?t=${Date.now()}`);
166
287
  if (!response.ok) throw new Error("[RenderUpdater] Cannot fetch latest.json for download");
167
288
  const info = await response.json();
168
- const versionDir = import_path.default.join(this.versionsDir, info.version);
169
- const asarPath = import_path.default.join(versionDir, "renderer.asar");
170
- if (import_original_fs.default.existsSync(asarPath) && this.verifyFile(asarPath, info)) {
289
+ const versionDir = import_path2.default.join(this.versionsDir, info.version);
290
+ const asarPath = import_path2.default.join(versionDir, "renderer.asar");
291
+ if (import_original_fs2.default.existsSync(asarPath) && this.verifyFile(asarPath, info)) {
171
292
  onProgress?.(100);
172
293
  this.useVersion(info.version);
173
294
  return;
174
295
  }
175
- if (!import_original_fs.default.existsSync(versionDir)) {
176
- import_original_fs.default.mkdirSync(versionDir, { recursive: true });
296
+ if (!import_original_fs2.default.existsSync(versionDir)) {
297
+ import_original_fs2.default.mkdirSync(versionDir, { recursive: true });
177
298
  }
178
299
  let downloadedBytes = 0;
179
- if (import_original_fs.default.existsSync(asarPath)) {
180
- downloadedBytes = import_original_fs.default.statSync(asarPath).size;
300
+ if (import_original_fs2.default.existsSync(asarPath)) {
301
+ downloadedBytes = import_original_fs2.default.statSync(asarPath).size;
181
302
  }
182
303
  const fetchOptions = {};
183
304
  if (downloadedBytes > 0) {
@@ -198,7 +319,7 @@ var RenderUpdater = class {
198
319
  const incomingTotal = contentLength ? parseInt(contentLength, 10) : 0;
199
320
  const total = downloadedBytes + incomingTotal;
200
321
  if (!downloadResponse.body) throw new Error("Response body is empty");
201
- const fileStream = import_original_fs.default.createWriteStream(asarPath, { flags: downloadResponse.status === 206 ? "a" : "w" });
322
+ const fileStream = import_original_fs2.default.createWriteStream(asarPath, { flags: downloadResponse.status === 206 ? "a" : "w" });
202
323
  const progressTransform = new import_stream.Transform({
203
324
  transform(chunk, _encoding, callback) {
204
325
  downloadedBytes += chunk.length;
@@ -214,7 +335,7 @@ var RenderUpdater = class {
214
335
  fileStream
215
336
  );
216
337
  if (!this.verifyFile(asarPath, info)) {
217
- import_original_fs.default.rmSync(versionDir, { recursive: true, force: true });
338
+ import_original_fs2.default.rmSync(versionDir, { recursive: true, force: true });
218
339
  throw new Error("[RenderUpdater] Verification failed after download (SHA256 or RSA Mismatch)");
219
340
  }
220
341
  this.useVersion(info.version);
@@ -224,7 +345,7 @@ var RenderUpdater = class {
224
345
  }
225
346
  }
226
347
  verifyFile(filePath, info) {
227
- const fileBuffer = import_original_fs.default.readFileSync(filePath);
348
+ const fileBuffer = import_original_fs2.default.readFileSync(filePath);
228
349
  const hashSum = import_crypto.default.createHash("sha256");
229
350
  hashSum.update(fileBuffer);
230
351
  const sha256 = hashSum.digest("hex");
@@ -250,23 +371,13 @@ var RenderUpdater = class {
250
371
  return true;
251
372
  }
252
373
  useVersion(version) {
253
- import_original_fs.default.writeFileSync(
374
+ import_original_fs2.default.writeFileSync(
254
375
  this.currentVersionFile,
255
376
  JSON.stringify({ version, date: (/* @__PURE__ */ new Date()).toISOString() })
256
377
  );
257
378
  this.activeVersion = version;
258
379
  }
259
- compareVersions(v1, v2) {
260
- const parts1 = v1.split(".").map(Number);
261
- const parts2 = v2.split(".").map(Number);
262
- for (let i = 0; i < Math.max(parts1.length, parts2.length); i++) {
263
- const n1 = parts1[i] || 0;
264
- const n2 = parts2[i] || 0;
265
- if (n1 > n2) return 1;
266
- if (n1 < n2) return -1;
267
- }
268
- return 0;
269
- }
380
+ // compareVersions 已由 semver 库替代,此方法已移除
270
381
  /**
271
382
  * One-stop method to check for updates, show dialogs (if autoPrompt=true),
272
383
  * download, and restart application.
@@ -285,8 +396,8 @@ var RenderUpdater = class {
285
396
  if (this.onBeforeRestart) {
286
397
  await this.onBeforeRestart();
287
398
  }
288
- import_electron.app.relaunch();
289
- import_electron.app.quit();
399
+ import_electron2.app.relaunch();
400
+ import_electron2.app.quit();
290
401
  };
291
402
  if (this.onDownloadComplete) {
292
403
  this.onDownloadComplete(info, () => {
@@ -295,7 +406,7 @@ var RenderUpdater = class {
295
406
  } else {
296
407
  if (info.forceUpdate) {
297
408
  if (info.forceUpdate === "prompt") {
298
- import_electron.dialog.showMessageBoxSync({
409
+ import_electron2.dialog.showMessageBoxSync({
299
410
  type: "warning",
300
411
  title: "\u4E0B\u8F7D\u5B8C\u6210",
301
412
  message: "\u6700\u65B0\u7248\u672C\u5DF2\u4E0B\u8F7D\u5B8C\u6BD5\u3002\u5373\u5C06\u91CD\u542F\u7A0B\u5E8F\u4EE5\u5E94\u7528\u66F4\u65B0\u3002",
@@ -304,7 +415,7 @@ var RenderUpdater = class {
304
415
  }
305
416
  doInstall().catch(console.error);
306
417
  } else if (this.autoPrompt) {
307
- import_electron.dialog.showMessageBoxSync({
418
+ import_electron2.dialog.showMessageBoxSync({
308
419
  type: "info",
309
420
  title: "\u4E0B\u8F7D\u5B8C\u6210",
310
421
  message: "\u6700\u65B0\u7248\u672C\u5DF2\u4E0B\u8F7D\u5B8C\u6BD5\u3002\u70B9\u51FB\u786E\u5B9A\u91CD\u542F\u7A0B\u5E8F\u4EE5\u5B89\u88C5\u66F4\u65B0\u3002",
@@ -327,7 +438,7 @@ var RenderUpdater = class {
327
438
  } else {
328
439
  if (info.forceUpdate) {
329
440
  if (info.forceUpdate === "prompt") {
330
- import_electron.dialog.showMessageBoxSync({
441
+ import_electron2.dialog.showMessageBoxSync({
331
442
  type: "warning",
332
443
  title: "\u53D1\u73B0\u65B0\u7248\u672C",
333
444
  message: `\u53D1\u73B0\u65B0\u7248\u672C (v${info.version})\u3002
@@ -341,7 +452,7 @@ ${info.releaseNotes || "\u672C\u6B21\u66F4\u65B0\u4E3A\u5F3A\u5236\u66F4\u65B0\u
341
452
  } else if (this.autoDownload) {
342
453
  await doDownload();
343
454
  } else if (this.autoPrompt) {
344
- const { response } = await import_electron.dialog.showMessageBox({
455
+ const { response } = await import_electron2.dialog.showMessageBox({
345
456
  type: "info",
346
457
  title: "\u53D1\u73B0\u65B0\u7248\u672C",
347
458
  message: `\u53D1\u73B0\u53EF\u7528\u66F4\u65B0 (v${info.version})\u3002\u662F\u5426\u7ACB\u5373\u4E0B\u8F7D\u66F4\u65B0\uFF1F
@@ -6,6 +6,8 @@ export declare class RenderUpdater {
6
6
  private activeVersion;
7
7
  private publicKey?;
8
8
  private isDownloading;
9
+ private routerMode;
10
+ private protocolName;
9
11
  private autoDownload;
10
12
  private autoPrompt;
11
13
  private maxVersionsToKeep;
@@ -19,8 +21,9 @@ export declare class RenderUpdater {
19
21
  private readCurrentVersionFromFile;
20
22
  hasAnyVersion(): boolean;
21
23
  /**
22
- * Returns the file:// URL to the latest renderer.asar index.html,
23
- * or empty string if no update is available.
24
+ * Returns the correct URL to load in the BrowserWindow.
25
+ * If history mode is enabled, it returns the custom protocol URL.
26
+ * Otherwise, it returns the local file URL with # hash.
24
27
  */
25
28
  getLoadUrl(): string;
26
29
  check(): Promise<{
@@ -34,7 +37,6 @@ export declare class RenderUpdater {
34
37
  download(onProgress?: (percent: number) => void): Promise<void>;
35
38
  private verifyFile;
36
39
  private useVersion;
37
- private compareVersions;
38
40
  /**
39
41
  * One-stop method to check for updates, show dialogs (if autoPrompt=true),
40
42
  * download, and restart application.
@@ -1,10 +1,116 @@
1
1
  // src/main/index.ts
2
- import path from "path";
3
- import fs from "original-fs";
2
+ import path2 from "path";
3
+ import fs2 from "original-fs";
4
4
  import crypto from "crypto";
5
- import { app, dialog } from "electron";
5
+ import semver from "semver";
6
+ import { app as app2, dialog } from "electron";
6
7
  import { pipeline } from "stream/promises";
7
8
  import { Readable, Transform } from "stream";
9
+
10
+ // src/main/router-handler.ts
11
+ import { protocol, net, app } from "electron";
12
+ import { pathToFileURL } from "url";
13
+ import path from "path";
14
+ import fs from "original-fs";
15
+ var RouterHandler = class _RouterHandler {
16
+ static protocolName = "app";
17
+ static isSchemaRegistered = false;
18
+ static isHandlerInited = false;
19
+ versionsDir;
20
+ getActiveVersion;
21
+ constructor(options) {
22
+ this.versionsDir = options.versionsDir;
23
+ this.getActiveVersion = options.getActiveVersion;
24
+ this.init();
25
+ }
26
+ /**
27
+ * 零摩擦初始化入口
28
+ * 自动处理 Electron 生命周期:准备前注册特权,准备后挂载拦截器。
29
+ */
30
+ static setup(options) {
31
+ const { protocol: protocolName, versionsDir, getActiveVersion } = options;
32
+ if (!this.isSchemaRegistered) {
33
+ if (!app.isReady()) {
34
+ this.protocolName = protocolName;
35
+ protocol.registerSchemesAsPrivileged([
36
+ {
37
+ scheme: protocolName,
38
+ privileges: {
39
+ standard: true,
40
+ secure: true,
41
+ supportFetchAPI: true,
42
+ allowServiceWorkers: true,
43
+ corsEnabled: true
44
+ }
45
+ }
46
+ ]);
47
+ this.isSchemaRegistered = true;
48
+ } else {
49
+ console.warn(`[RouterHandler] App already ready. Cannot register scheme "${protocolName}".`);
50
+ }
51
+ }
52
+ if (!this.isHandlerInited) {
53
+ app.whenReady().then(() => {
54
+ new _RouterHandler({ versionsDir, getActiveVersion });
55
+ this.isHandlerInited = true;
56
+ });
57
+ }
58
+ }
59
+ init() {
60
+ const protocolName = _RouterHandler.protocolName;
61
+ protocol.handle(protocolName, async (request) => {
62
+ const url = new URL(request.url);
63
+ const { host, pathname } = url;
64
+ if (host === "renderer") {
65
+ const relativePath = pathname.startsWith("/") ? pathname.slice(1) : pathname;
66
+ const activeVersion = this.getActiveVersion();
67
+ const asarPath = path.join(this.versionsDir, activeVersion, "renderer.asar");
68
+ if (!fs.existsSync(asarPath)) {
69
+ return new Response("Not Found", { status: 404 });
70
+ }
71
+ const ext = path.extname(relativePath);
72
+ const isRequestingFile = ext !== "" && !relativePath.endsWith("/");
73
+ let targetFile = path.join(asarPath, relativePath);
74
+ if (!isRequestingFile || relativePath === "index.html" || relativePath === "") {
75
+ targetFile = path.join(asarPath, "index.html");
76
+ }
77
+ const response = await net.fetch(pathToFileURL(targetFile).toString());
78
+ const headers = new Headers(response.headers);
79
+ headers.set("Content-Type", this.getMimeType(targetFile));
80
+ headers.set("Access-Control-Allow-Origin", "*");
81
+ return new Response(response.body, {
82
+ status: response.status,
83
+ statusText: response.statusText,
84
+ headers
85
+ });
86
+ }
87
+ return new Response("Not Found", { status: 404 });
88
+ });
89
+ }
90
+ getMimeType(filePath) {
91
+ const ext = path.extname(filePath).toLowerCase();
92
+ const mimeMap = {
93
+ ".html": "text/html",
94
+ ".js": "application/javascript",
95
+ ".mjs": "application/javascript",
96
+ ".css": "text/css",
97
+ ".json": "application/json",
98
+ ".png": "image/png",
99
+ ".jpg": "image/jpeg",
100
+ ".jpeg": "image/jpeg",
101
+ ".gif": "image/gif",
102
+ ".svg": "image/svg+xml",
103
+ ".ico": "image/x-icon",
104
+ ".woff": "font/woff",
105
+ ".woff2": "font/woff2",
106
+ ".ttf": "font/ttf",
107
+ ".otf": "font/otf"
108
+ };
109
+ return mimeMap[ext] || "application/octet-stream";
110
+ }
111
+ };
112
+
113
+ // src/main/index.ts
8
114
  var RenderUpdater = class {
9
115
  versionsDir;
10
116
  currentVersionFile;
@@ -12,6 +118,8 @@ var RenderUpdater = class {
12
118
  activeVersion;
13
119
  publicKey;
14
120
  isDownloading = false;
121
+ routerMode;
122
+ protocolName;
15
123
  autoDownload;
16
124
  autoPrompt;
17
125
  maxVersionsToKeep;
@@ -22,7 +130,7 @@ var RenderUpdater = class {
22
130
  onBeforeRestart;
23
131
  constructor(options) {
24
132
  this.versionsDir = options.versionsDir;
25
- this.currentVersionFile = path.join(this.versionsDir, "current.json");
133
+ this.currentVersionFile = path2.join(this.versionsDir, "current.json");
26
134
  this.baseUrl = options.updateUrl;
27
135
  this.publicKey = options.publicKey;
28
136
  this.autoDownload = options.autoDownload ?? false;
@@ -33,25 +141,34 @@ var RenderUpdater = class {
33
141
  this.onDownloadComplete = options.onDownloadComplete;
34
142
  this.onError = options.onError;
35
143
  this.onBeforeRestart = options.onBeforeRestart;
36
- if (!fs.existsSync(this.versionsDir)) {
37
- fs.mkdirSync(this.versionsDir, { recursive: true });
144
+ this.routerMode = options.routerMode ?? "hash";
145
+ this.protocolName = options.protocol ?? "app";
146
+ if (!fs2.existsSync(this.versionsDir)) {
147
+ fs2.mkdirSync(this.versionsDir, { recursive: true });
38
148
  }
39
149
  this.activeVersion = this.readCurrentVersionFromFile();
40
150
  this.cleanOldVersions();
151
+ if (this.routerMode === "history") {
152
+ RouterHandler.setup({
153
+ protocol: this.protocolName,
154
+ versionsDir: this.versionsDir,
155
+ getActiveVersion: () => this.activeVersion
156
+ });
157
+ }
41
158
  }
42
159
  cleanOldVersions() {
43
160
  try {
44
- if (!fs.existsSync(this.versionsDir)) return;
45
- const items = fs.readdirSync(this.versionsDir);
161
+ if (!fs2.existsSync(this.versionsDir)) return;
162
+ const items = fs2.readdirSync(this.versionsDir);
46
163
  const versionDirs = [];
47
164
  for (const item of items) {
48
165
  if (item === "current.json") continue;
49
- const fullPath = path.join(this.versionsDir, item);
50
- if (fs.statSync(fullPath).isDirectory()) {
166
+ const fullPath = path2.join(this.versionsDir, item);
167
+ if (fs2.statSync(fullPath).isDirectory()) {
51
168
  versionDirs.push(item);
52
169
  }
53
170
  }
54
- versionDirs.sort((a, b) => this.compareVersions(b, a));
171
+ versionDirs.sort((a, b) => semver.compare(semver.coerce(b) ?? b, semver.coerce(a) ?? a));
55
172
  const activeIdx = versionDirs.indexOf(this.activeVersion);
56
173
  const safeVersions = /* @__PURE__ */ new Set();
57
174
  if (activeIdx !== -1) safeVersions.add(this.activeVersion);
@@ -66,7 +183,7 @@ var RenderUpdater = class {
66
183
  for (const v of versionDirs) {
67
184
  if (!safeVersions.has(v)) {
68
185
  console.info(`[RenderUpdater] Cleaning up old version: ${v}`);
69
- fs.rmSync(path.join(this.versionsDir, v), { recursive: true, force: true });
186
+ fs2.rmSync(path2.join(this.versionsDir, v), { recursive: true, force: true });
70
187
  }
71
188
  }
72
189
  } catch (e) {
@@ -74,9 +191,9 @@ var RenderUpdater = class {
74
191
  }
75
192
  }
76
193
  readCurrentVersionFromFile() {
77
- if (fs.existsSync(this.currentVersionFile)) {
194
+ if (fs2.existsSync(this.currentVersionFile)) {
78
195
  try {
79
- const current = JSON.parse(fs.readFileSync(this.currentVersionFile, "utf-8"));
196
+ const current = JSON.parse(fs2.readFileSync(this.currentVersionFile, "utf-8"));
80
197
  return current.version;
81
198
  } catch {
82
199
  return "0.0.0";
@@ -85,19 +202,23 @@ var RenderUpdater = class {
85
202
  return "0.0.0";
86
203
  }
87
204
  hasAnyVersion() {
88
- return fs.existsSync(this.currentVersionFile);
205
+ return fs2.existsSync(this.currentVersionFile);
89
206
  }
90
207
  /**
91
- * Returns the file:// URL to the latest renderer.asar index.html,
92
- * or empty string if no update is available.
208
+ * Returns the correct URL to load in the BrowserWindow.
209
+ * If history mode is enabled, it returns the custom protocol URL.
210
+ * Otherwise, it returns the local file URL with # hash.
93
211
  */
94
212
  getLoadUrl() {
95
213
  try {
96
- if (fs.existsSync(this.currentVersionFile)) {
97
- const current = JSON.parse(fs.readFileSync(this.currentVersionFile, "utf-8"));
214
+ if (fs2.existsSync(this.currentVersionFile)) {
215
+ const current = JSON.parse(fs2.readFileSync(this.currentVersionFile, "utf-8"));
98
216
  const version = current.version;
99
- const asarPath = path.join(this.versionsDir, version, "renderer.asar");
100
- if (fs.existsSync(asarPath)) {
217
+ const asarPath = path2.join(this.versionsDir, version, "renderer.asar");
218
+ if (fs2.existsSync(asarPath)) {
219
+ if (this.routerMode === "history") {
220
+ return `${this.protocolName}://renderer/`;
221
+ }
101
222
  return `file://${asarPath}/index.html`;
102
223
  }
103
224
  }
@@ -112,7 +233,7 @@ var RenderUpdater = class {
112
233
  if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
113
234
  const remoteInfo = await response.json();
114
235
  const currentVersion = this.activeVersion;
115
- if (this.compareVersions(remoteInfo.version, currentVersion) > 0) {
236
+ if (semver.gt(remoteInfo.version, currentVersion)) {
116
237
  return { updateAvailable: true, version: remoteInfo.version, info: remoteInfo };
117
238
  }
118
239
  return { updateAvailable: false };
@@ -131,19 +252,19 @@ var RenderUpdater = class {
131
252
  const response = await fetch(`${this.baseUrl}/latest.json?t=${Date.now()}`);
132
253
  if (!response.ok) throw new Error("[RenderUpdater] Cannot fetch latest.json for download");
133
254
  const info = await response.json();
134
- const versionDir = path.join(this.versionsDir, info.version);
135
- const asarPath = path.join(versionDir, "renderer.asar");
136
- if (fs.existsSync(asarPath) && this.verifyFile(asarPath, info)) {
255
+ const versionDir = path2.join(this.versionsDir, info.version);
256
+ const asarPath = path2.join(versionDir, "renderer.asar");
257
+ if (fs2.existsSync(asarPath) && this.verifyFile(asarPath, info)) {
137
258
  onProgress?.(100);
138
259
  this.useVersion(info.version);
139
260
  return;
140
261
  }
141
- if (!fs.existsSync(versionDir)) {
142
- fs.mkdirSync(versionDir, { recursive: true });
262
+ if (!fs2.existsSync(versionDir)) {
263
+ fs2.mkdirSync(versionDir, { recursive: true });
143
264
  }
144
265
  let downloadedBytes = 0;
145
- if (fs.existsSync(asarPath)) {
146
- downloadedBytes = fs.statSync(asarPath).size;
266
+ if (fs2.existsSync(asarPath)) {
267
+ downloadedBytes = fs2.statSync(asarPath).size;
147
268
  }
148
269
  const fetchOptions = {};
149
270
  if (downloadedBytes > 0) {
@@ -164,7 +285,7 @@ var RenderUpdater = class {
164
285
  const incomingTotal = contentLength ? parseInt(contentLength, 10) : 0;
165
286
  const total = downloadedBytes + incomingTotal;
166
287
  if (!downloadResponse.body) throw new Error("Response body is empty");
167
- const fileStream = fs.createWriteStream(asarPath, { flags: downloadResponse.status === 206 ? "a" : "w" });
288
+ const fileStream = fs2.createWriteStream(asarPath, { flags: downloadResponse.status === 206 ? "a" : "w" });
168
289
  const progressTransform = new Transform({
169
290
  transform(chunk, _encoding, callback) {
170
291
  downloadedBytes += chunk.length;
@@ -180,7 +301,7 @@ var RenderUpdater = class {
180
301
  fileStream
181
302
  );
182
303
  if (!this.verifyFile(asarPath, info)) {
183
- fs.rmSync(versionDir, { recursive: true, force: true });
304
+ fs2.rmSync(versionDir, { recursive: true, force: true });
184
305
  throw new Error("[RenderUpdater] Verification failed after download (SHA256 or RSA Mismatch)");
185
306
  }
186
307
  this.useVersion(info.version);
@@ -190,7 +311,7 @@ var RenderUpdater = class {
190
311
  }
191
312
  }
192
313
  verifyFile(filePath, info) {
193
- const fileBuffer = fs.readFileSync(filePath);
314
+ const fileBuffer = fs2.readFileSync(filePath);
194
315
  const hashSum = crypto.createHash("sha256");
195
316
  hashSum.update(fileBuffer);
196
317
  const sha256 = hashSum.digest("hex");
@@ -216,23 +337,13 @@ var RenderUpdater = class {
216
337
  return true;
217
338
  }
218
339
  useVersion(version) {
219
- fs.writeFileSync(
340
+ fs2.writeFileSync(
220
341
  this.currentVersionFile,
221
342
  JSON.stringify({ version, date: (/* @__PURE__ */ new Date()).toISOString() })
222
343
  );
223
344
  this.activeVersion = version;
224
345
  }
225
- compareVersions(v1, v2) {
226
- const parts1 = v1.split(".").map(Number);
227
- const parts2 = v2.split(".").map(Number);
228
- for (let i = 0; i < Math.max(parts1.length, parts2.length); i++) {
229
- const n1 = parts1[i] || 0;
230
- const n2 = parts2[i] || 0;
231
- if (n1 > n2) return 1;
232
- if (n1 < n2) return -1;
233
- }
234
- return 0;
235
- }
346
+ // compareVersions 已由 semver 库替代,此方法已移除
236
347
  /**
237
348
  * One-stop method to check for updates, show dialogs (if autoPrompt=true),
238
349
  * download, and restart application.
@@ -251,8 +362,8 @@ var RenderUpdater = class {
251
362
  if (this.onBeforeRestart) {
252
363
  await this.onBeforeRestart();
253
364
  }
254
- app.relaunch();
255
- app.quit();
365
+ app2.relaunch();
366
+ app2.quit();
256
367
  };
257
368
  if (this.onDownloadComplete) {
258
369
  this.onDownloadComplete(info, () => {
@@ -0,0 +1,20 @@
1
+ export interface RouterHandlerOptions {
2
+ protocol: string;
3
+ versionsDir: string;
4
+ getActiveVersion: () => string;
5
+ }
6
+ export declare class RouterHandler {
7
+ private static protocolName;
8
+ private static isSchemaRegistered;
9
+ private static isHandlerInited;
10
+ private versionsDir;
11
+ private getActiveVersion;
12
+ private constructor();
13
+ /**
14
+ * 零摩擦初始化入口
15
+ * 自动处理 Electron 生命周期:准备前注册特权,准备后挂载拦截器。
16
+ */
17
+ static setup(options: RouterHandlerOptions): void;
18
+ private init;
19
+ private getMimeType;
20
+ }
package/dist/types.d.ts CHANGED
@@ -64,6 +64,18 @@ export interface UpdaterOptions {
64
64
  * Useful for telling the Vue renderer to save state and cleanup.
65
65
  */
66
66
  onBeforeRestart?: () => void | Promise<void>;
67
+ /**
68
+ * Defines the routing mode of the renderer project.
69
+ * 'hash': Use default file:// loading (Zero performance overhead).
70
+ * 'history': Use custom app:// protocol with try-files redirection to support History mode.
71
+ * Defaults to 'hash'.
72
+ */
73
+ routerMode?: 'hash' | 'history';
74
+ /**
75
+ * Custom protocol name for history mode or asset serving.
76
+ * Defaults to 'app'.
77
+ */
78
+ protocol?: string;
67
79
  }
68
80
  export interface BuilderOptions {
69
81
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "electron-updater-for-render",
3
- "version": "1.1.2-beta.3",
3
+ "version": "1.1.2-beta.5",
4
4
  "description": "A lightweight incremental updater for Electron renderer processes",
5
5
  "type": "module",
6
6
  "bin": {
@@ -27,20 +27,22 @@
27
27
  },
28
28
  "dependencies": {
29
29
  "jiti": "^2.6.1",
30
- "original-fs": "^1.2.0"
30
+ "original-fs": "^1.2.0",
31
+ "semver": "^7.7.4"
31
32
  },
32
33
  "peerDependencies": {
33
- "asar": "^3.2.0",
34
+ "@electron/asar": "^3.3.0",
34
35
  "electron": ">=20.0.0"
35
36
  },
36
37
  "peerDependenciesMeta": {
37
- "asar": {
38
+ "@electron/asar": {
38
39
  "optional": true
39
40
  }
40
41
  },
41
42
  "devDependencies": {
43
+ "@electron/asar": "^4.2.0",
42
44
  "@types/node": "^22.0.0",
43
- "asar": "^3.2.0",
45
+ "@types/semver": "^7.7.1",
44
46
  "tsup": "^8.0.2",
45
47
  "typescript": "^5.0.0"
46
48
  },