fix(desktop): app icon + harden auto-updater loading
Generate a 1024x1024 macOS app icon from the dashboard logo so packaged builds carry the Fusion brand instead of the default Electron icon, and load electron-updater dynamically inside a try/catch so packaged builds tolerate CJS/ESM interop quirks and missing transitive deps. Widen the electron-builder files whitelist to include electron-updater's transitive dep tree so they are actually bundled into the asar. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@@ -12,8 +12,23 @@ files:
|
||||
- node_modules/@fusion/core/**/*
|
||||
- node_modules/@fusion/dashboard/**/*
|
||||
- node_modules/better-sqlite3/**/*
|
||||
- node_modules/ms/**/*
|
||||
- node_modules/electron-updater/**/*
|
||||
- node_modules/builder-util-runtime/**/*
|
||||
- node_modules/fs-extra/**/*
|
||||
- node_modules/jsonfile/**/*
|
||||
- node_modules/graceful-fs/**/*
|
||||
- node_modules/universalify/**/*
|
||||
- node_modules/js-yaml/**/*
|
||||
- node_modules/argparse/**/*
|
||||
- node_modules/sprintf-js/**/*
|
||||
- node_modules/lazy-val/**/*
|
||||
- node_modules/lodash.escaperegexp/**/*
|
||||
- node_modules/lodash.isequal/**/*
|
||||
- node_modules/semver/**/*
|
||||
- node_modules/tiny-typed-emitter/**/*
|
||||
- node_modules/debug/**/*
|
||||
- node_modules/ms/**/*
|
||||
- node_modules/sax/**/*
|
||||
|
||||
asarUnpack:
|
||||
- node_modules/better-sqlite3/**/*
|
||||
|
||||
@@ -12,23 +12,47 @@ const outputDir = resolve(packageDir, "src/icons");
|
||||
|
||||
const iconSizes = [16, 32, 48] as const;
|
||||
|
||||
const APP_ICON_SIZE = 1024;
|
||||
const APP_ICON_PADDING = 128;
|
||||
const APP_ICON_BG = "#0d1117";
|
||||
const APP_ICON_FG = "#58a6ff";
|
||||
|
||||
async function main(): Promise<void> {
|
||||
const sourceSvg = await readFile(sourceSvgPath, "utf8");
|
||||
const tintedSvg = sourceSvg.replaceAll("currentColor", "#333333");
|
||||
const trayTintedSvg = sourceSvg.replaceAll("currentColor", "#333333");
|
||||
const appTintedSvg = sourceSvg.replaceAll("currentColor", APP_ICON_FG);
|
||||
|
||||
await mkdir(outputDir, { recursive: true });
|
||||
|
||||
await Promise.all(
|
||||
iconSizes.map(async (size) => {
|
||||
const outputPath = resolve(outputDir, `tray-${size}.png`);
|
||||
await sharp(Buffer.from(tintedSvg), { density: 1024 })
|
||||
await sharp(Buffer.from(trayTintedSvg), { density: 1024 })
|
||||
.resize(size, size, { fit: "contain" })
|
||||
.png({ compressionLevel: 9 })
|
||||
.toFile(outputPath);
|
||||
}),
|
||||
);
|
||||
|
||||
console.log(`Generated ${iconSizes.length} tray icons in ${outputDir}`);
|
||||
const markSize = APP_ICON_SIZE - APP_ICON_PADDING * 2;
|
||||
const mark = await sharp(Buffer.from(appTintedSvg), { density: 2048 })
|
||||
.resize(markSize, markSize, { fit: "contain", background: { r: 0, g: 0, b: 0, alpha: 0 } })
|
||||
.png()
|
||||
.toBuffer();
|
||||
|
||||
await sharp({
|
||||
create: {
|
||||
width: APP_ICON_SIZE,
|
||||
height: APP_ICON_SIZE,
|
||||
channels: 4,
|
||||
background: APP_ICON_BG,
|
||||
},
|
||||
})
|
||||
.composite([{ input: mark, top: APP_ICON_PADDING, left: APP_ICON_PADDING }])
|
||||
.png({ compressionLevel: 9 })
|
||||
.toFile(resolve(outputDir, "icon.png"));
|
||||
|
||||
console.log(`Generated ${iconSizes.length} tray icons and app icon in ${outputDir}`);
|
||||
}
|
||||
|
||||
void main();
|
||||
|
||||
@@ -96,6 +96,7 @@ vi.mock("electron", () => ({
|
||||
}));
|
||||
|
||||
vi.mock("electron-updater", () => ({
|
||||
default: { autoUpdater: mocks.autoUpdater },
|
||||
autoUpdater: mocks.autoUpdater,
|
||||
}));
|
||||
|
||||
|
||||
3
packages/desktop/src/electron-updater.d.ts
vendored
@@ -14,4 +14,7 @@ declare module "electron-updater" {
|
||||
}
|
||||
|
||||
export const autoUpdater: AutoUpdater;
|
||||
|
||||
const electronUpdater: { autoUpdater: AutoUpdater };
|
||||
export default electronUpdater;
|
||||
}
|
||||
|
||||
BIN
packages/desktop/src/icons/icon.png
Normal file
|
After Width: | Height: | Size: 33 KiB |
|
Before Width: | Height: | Size: 160 B After Width: | Height: | Size: 394 B |
|
Before Width: | Height: | Size: 231 B After Width: | Height: | Size: 816 B |
|
Before Width: | Height: | Size: 344 B After Width: | Height: | Size: 1.2 KiB |
@@ -8,7 +8,6 @@ import {
|
||||
type OpenDialogOptions,
|
||||
type SaveDialogOptions,
|
||||
} from "electron";
|
||||
import { autoUpdater } from "electron-updater";
|
||||
|
||||
export interface WindowState {
|
||||
x?: number;
|
||||
@@ -167,34 +166,54 @@ export function showDesktopNotification(
|
||||
}
|
||||
|
||||
export function setupAutoUpdater(mainWindow?: BrowserWindow): void {
|
||||
try {
|
||||
autoUpdater.autoDownload = true;
|
||||
autoUpdater.autoInstallOnAppQuit = true;
|
||||
void (async () => {
|
||||
try {
|
||||
const mod = (await import("electron-updater")) as {
|
||||
default?: { autoUpdater?: unknown };
|
||||
autoUpdater?: unknown;
|
||||
};
|
||||
const autoUpdater = (mod.default?.autoUpdater ?? mod.autoUpdater) as
|
||||
| {
|
||||
autoDownload: boolean;
|
||||
autoInstallOnAppQuit: boolean;
|
||||
on: (event: string, handler: (...args: unknown[]) => void) => unknown;
|
||||
checkForUpdates: () => Promise<unknown>;
|
||||
}
|
||||
| undefined;
|
||||
|
||||
autoUpdater.on("update-available", (info) => {
|
||||
showDesktopNotification("Fusion Update Available", "Update available — downloading in background", {
|
||||
silent: true,
|
||||
if (!autoUpdater) {
|
||||
console.warn("[desktop/native] Auto-updater module loaded without autoUpdater export");
|
||||
return;
|
||||
}
|
||||
|
||||
autoUpdater.autoDownload = true;
|
||||
autoUpdater.autoInstallOnAppQuit = true;
|
||||
|
||||
autoUpdater.on("update-available", (info) => {
|
||||
showDesktopNotification("Fusion Update Available", "Update available — downloading in background", {
|
||||
silent: true,
|
||||
});
|
||||
mainWindow?.webContents.send("update-available", info);
|
||||
});
|
||||
mainWindow?.webContents.send("update-available", info);
|
||||
});
|
||||
|
||||
autoUpdater.on("update-downloaded", (info) => {
|
||||
showDesktopNotification("Fusion Update Ready", "Update ready — will install on quit", {
|
||||
silent: true,
|
||||
autoUpdater.on("update-downloaded", (info) => {
|
||||
showDesktopNotification("Fusion Update Ready", "Update ready — will install on quit", {
|
||||
silent: true,
|
||||
});
|
||||
mainWindow?.webContents.send("update-downloaded", info);
|
||||
});
|
||||
mainWindow?.webContents.send("update-downloaded", info);
|
||||
});
|
||||
|
||||
autoUpdater.on("error", (error) => {
|
||||
console.error("[desktop/native] Auto-updater error", error);
|
||||
});
|
||||
autoUpdater.on("error", (error) => {
|
||||
console.error("[desktop/native] Auto-updater error", error);
|
||||
});
|
||||
|
||||
void autoUpdater.checkForUpdates().catch((error) => {
|
||||
console.error("[desktop/native] Auto-updater check failed", error);
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("[desktop/native] Auto-updater unavailable", error);
|
||||
}
|
||||
await autoUpdater.checkForUpdates().catch((error: unknown) => {
|
||||
console.error("[desktop/native] Auto-updater check failed", error);
|
||||
});
|
||||
} catch (error) {
|
||||
console.warn("[desktop/native] Auto-updater unavailable", error);
|
||||
}
|
||||
})();
|
||||
}
|
||||
|
||||
function normalizeServerBaseUrl(serverUrl: string): string | null {
|
||||
|
||||