From 442301749a8ddf0b432f81d26dc5e4c17ca54709 Mon Sep 17 00:00:00 2001 From: Samuel K Date: Fri, 29 May 2026 19:54:04 -0500 Subject: [PATCH 1/3] fix(desktop): load autoUpdater via default export under ESM Main runs as ESM (`"type": "module"` in desktop/package.json). Node's cjs-module-lexer doesn't surface electron-updater's `autoUpdater` getter (defined via Object.defineProperty on exports) as a named export, so `const { autoUpdater } = await import("electron-updater")` was always undefined and the unsupported-build error fired on every packaged build. Reach the getter through `mod.default.autoUpdater` (CJS module.exports) via a small `loadAutoUpdater()` helper, used by all three call sites. --- desktop/src/main/updater.ts | 29 ++++++++++++++++++++++------- 1 file changed, 22 insertions(+), 7 deletions(-) diff --git a/desktop/src/main/updater.ts b/desktop/src/main/updater.ts index a8c72b544..830969edc 100644 --- a/desktop/src/main/updater.ts +++ b/desktop/src/main/updater.ts @@ -89,11 +89,15 @@ export function getLastStatus(): UpdateStatus { } function normalizeReleaseNotes( - notes: string | { note: string }[] | null | undefined, + notes: string | { note: string | null }[] | null | undefined, ): string | undefined { if (!notes) return undefined if (typeof notes === "string") return notes - if (Array.isArray(notes)) return notes.map((n) => n.note).join("\n") + if (Array.isArray(notes)) + return notes + .map((n) => n.note) + .filter((n): n is string => !!n) + .join("\n") return undefined } @@ -119,9 +123,9 @@ export function setAutoDownloadEnabled(enabled: boolean): void { // Update the live autoUpdater too so the change takes effect this session. // electron-updater reads autoDownload at the moment update-available fires. if (app.isPackaged) { - import("electron-updater") - .then(({ autoUpdater }) => { - if (autoUpdater && typeof autoUpdater === "object") { + loadAutoUpdater() + .then((autoUpdater) => { + if (autoUpdater) { autoUpdater.autoDownload = enabled } }) @@ -129,6 +133,17 @@ export function setAutoDownloadEnabled(enabled: boolean): void { } } +// electron-updater exposes `autoUpdater` via a CJS getter that Node's +// cjs-module-lexer doesn't surface as a named export under ESM. We have to +// reach it through `default` (the CJS module.exports), which invokes the +// getter and returns the platform-specific updater instance. +async function loadAutoUpdater(): Promise< + (typeof import("electron-updater"))["autoUpdater"] | null +> { + const mod = await import("electron-updater") + return mod.default?.autoUpdater ?? mod.autoUpdater ?? null +} + export function getAutoDownloadEnabled(): boolean { return autoDownloadEnabled } @@ -147,7 +162,7 @@ export async function initAutoUpdater( return } - const { autoUpdater } = await import("electron-updater") + const autoUpdater = await loadAutoUpdater() if (!autoUpdater || typeof autoUpdater.checkForUpdates !== "function") { setStatus({ @@ -230,7 +245,7 @@ async function getUpdater() { setStatus({ state: "not-available", code: "dev-mode" }) return null } - const { autoUpdater } = await import("electron-updater") + const autoUpdater = await loadAutoUpdater() if (!autoUpdater || typeof autoUpdater.checkForUpdates !== "function") { setStatus({ state: "error", From fa8e0d065be492ae939f9268a2236d3d9460e4b3 Mon Sep 17 00:00:00 2001 From: Samuel K Date: Fri, 29 May 2026 19:54:10 -0500 Subject: [PATCH 2/3] fix(desktop): register missing app_ready IPC handler The renderer invokes `app_ready` on mount (App.svelte) but no main-side handler existed, so the promise always rejected with "No handler registered for 'app_ready'". The error was silently swallowed until PR #459 added a console.warn surfacing it. Register the handler alongside the other update IPCs. It replays the current updater status to the renderer so a freshly-mounted (or reloaded) UI immediately reflects state main already knows. --- desktop/src/main/ipc.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/desktop/src/main/ipc.ts b/desktop/src/main/ipc.ts index a80092237..640c82aeb 100644 --- a/desktop/src/main/ipc.ts +++ b/desktop/src/main/ipc.ts @@ -18,6 +18,7 @@ import { checkForUpdatesWithChannel, downloadUpdate, getAutoDownloadEnabled, + getLastStatus, getReleaseChannel, installUpdate, setAutoDownloadEnabled, @@ -808,6 +809,16 @@ export function registerIpcHandlers(deps: IpcDependencies): { tunnelProcesses: M await checkForUpdates() }) + // Deferred so the renderer's update-status listener (registered after this + // call resolves) is attached before the replay arrives. + ipcMain.handle("app_ready", (event) => { + setImmediate(() => { + if (!event.sender.isDestroyed()) { + event.sender.send("update-status", getLastStatus()) + } + }) + }) + ipcMain.handle("install_update", async () => { installUpdate() }) From 185767173abaa38dce20fea60b5ef04c73561382 Mon Sep 17 00:00:00 2001 From: Samuel K Date: Fri, 29 May 2026 19:57:39 -0500 Subject: [PATCH 3/3] test(desktop): expose default export in electron-updater mock The updater now reads autoUpdater via mod.default?.autoUpdater (the CJS/ESM interop path required under "type": "module"). The test mock only defined the top-level autoUpdater export, so accessing mod.default threw a vitest strict-mock error. Make the mock mirror the real module shape. --- desktop/src/main/__tests__/updater.test.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/desktop/src/main/__tests__/updater.test.ts b/desktop/src/main/__tests__/updater.test.ts index 697c35e3a..ceb59a38f 100644 --- a/desktop/src/main/__tests__/updater.test.ts +++ b/desktop/src/main/__tests__/updater.test.ts @@ -20,7 +20,10 @@ const electronUpdaterMock = { }, } -vi.mock("electron-updater", () => electronUpdaterMock) +vi.mock("electron-updater", () => ({ + ...electronUpdaterMock, + default: electronUpdaterMock, +})) vi.mock("electron", () => ({ app: { isPackaged: true,