feat(FN-1073): add desktop CLI workflow and packaging support
- Add new `fn desktop` CLI command with argument handling and comprehensive command/bin tests - Implement desktop build and hot-reload dev scripts and wire package scripts/dependencies for Electron workflows - Add electron-builder configuration and desktop main-process/integration test coverage to stabilize packaging behavior - Document desktop development and usage in README files and include a changeset for the published CLI package
This commit is contained in:
@@ -4,20 +4,38 @@ Electron desktop shell for Fusion.
|
||||
|
||||
This package provides a native Electron wrapper around the existing Fusion dashboard web UI. The desktop shell connects to a running dashboard server and presents native desktop affordances including a system tray and application menu.
|
||||
|
||||
## Prerequisites
|
||||
## Running the Desktop Shell
|
||||
|
||||
Start the Fusion dashboard server first:
|
||||
### Hot-reload development workflow
|
||||
|
||||
```bash
|
||||
fn dashboard
|
||||
```
|
||||
|
||||
Then, in another terminal, start the desktop app:
|
||||
Run a single command from the workspace root:
|
||||
|
||||
```bash
|
||||
pnpm --filter @fusion/desktop dev
|
||||
```
|
||||
|
||||
This command now orchestrates the full desktop dev loop:
|
||||
|
||||
1. Bundles Electron `main.ts` and `preload.ts` to `packages/desktop/dist`
|
||||
2. Starts the dashboard Vite renderer dev server (`@fusion/dashboard dev:serve`)
|
||||
3. Waits for renderer readiness
|
||||
4. Launches Electron with `--dev` and live renderer reload
|
||||
|
||||
By default it uses `http://localhost:5173`. Override with `FUSION_DASHBOARD_URL`.
|
||||
|
||||
### Production-style desktop launch (from CLI)
|
||||
|
||||
```bash
|
||||
fn desktop
|
||||
```
|
||||
|
||||
`fn desktop` builds desktop artifacts, starts an embedded dashboard server on an ephemeral port, and launches Electron against that server.
|
||||
|
||||
Useful flags:
|
||||
|
||||
- `fn desktop --dev` — use dev renderer URL (`FUSION_DASHBOARD_URL` or `http://localhost:5173`)
|
||||
- `fn desktop --paused` — start with engine paused
|
||||
|
||||
## IPC Channel Reference
|
||||
|
||||
`src/ipc.ts` registers the renderer ↔ main process bridge used by `window.fusionAPI`.
|
||||
@@ -206,13 +224,24 @@ Tray icons are generated from `packages/dashboard/app/public/logo.svg`.
|
||||
|
||||
## Scripts
|
||||
|
||||
- `pnpm --filter @fusion/desktop dev` — run the Electron main process in development
|
||||
- `pnpm --filter @fusion/desktop build` — compile TypeScript sources
|
||||
- `pnpm --filter @fusion/desktop dev` — hot-reload workflow (main/preload bundle + dashboard Vite dev server + Electron)
|
||||
- `pnpm --filter @fusion/desktop build` — production desktop build (dashboard client build + main/preload bundle + asset copy)
|
||||
- `pnpm --filter @fusion/desktop test` — run Vitest suite
|
||||
- `pnpm --filter @fusion/desktop typecheck` — run TypeScript checks without emitting files
|
||||
- `pnpm --filter @fusion/desktop generate:icons` — regenerate tray icon PNG assets from the dashboard logo SVG
|
||||
- `pnpm --filter @fusion/desktop pack` — build distributable package via electron-builder
|
||||
- `pnpm --filter @fusion/desktop dist` — build distribution artifacts without publishing
|
||||
- `pnpm --filter @fusion/desktop pack` — generate unpacked artifacts via electron-builder (`--dir`)
|
||||
- `pnpm --filter @fusion/desktop dist` — generate installable desktop artifacts via electron-builder
|
||||
|
||||
## Packaging
|
||||
|
||||
Desktop packaging is configured in `electron-builder.yml`.
|
||||
|
||||
- Output directory: `packages/desktop/dist-electron`
|
||||
- Targets: macOS (`dmg`, `zip`), Windows (`nsis`, `portable`), Linux (`AppImage`, `deb`, `tar.gz`)
|
||||
- Deep link protocol: `fusion://`
|
||||
- Publish provider: GitHub (`gsxdsm/fusion`)
|
||||
|
||||
Run `pnpm --filter @fusion/desktop build` before `pack`/`dist` to ensure `dist/` assets are up to date.
|
||||
|
||||
## Environment
|
||||
|
||||
|
||||
62
packages/desktop/electron-builder.yml
Normal file
62
packages/desktop/electron-builder.yml
Normal file
@@ -0,0 +1,62 @@
|
||||
appId: com.gsxdsm.fusion.desktop
|
||||
productName: Fusion
|
||||
artifactName: "${productName}-${version}-${os}-${arch}.${ext}"
|
||||
|
||||
directories:
|
||||
output: dist-electron
|
||||
buildResources: src/icons
|
||||
|
||||
files:
|
||||
- dist/**/*
|
||||
- package.json
|
||||
|
||||
extraMetadata:
|
||||
main: dist/main.js
|
||||
|
||||
extraResources:
|
||||
- from: src/icons
|
||||
to: icons
|
||||
filter:
|
||||
- "tray-*.png"
|
||||
|
||||
protocols:
|
||||
- name: Fusion Deep Links
|
||||
schemes:
|
||||
- fusion
|
||||
|
||||
fileAssociations:
|
||||
- ext: fusion
|
||||
name: Fusion Task Link
|
||||
description: Fusion desktop deep link
|
||||
role: Viewer
|
||||
mimeType: x-scheme-handler/fusion
|
||||
|
||||
publish:
|
||||
provider: github
|
||||
owner: gsxdsm
|
||||
repo: fusion
|
||||
|
||||
mac:
|
||||
category: public.app-category.developer-tools
|
||||
minimumSystemVersion: "10.15"
|
||||
target:
|
||||
- target: dmg
|
||||
- target: zip
|
||||
|
||||
win:
|
||||
target:
|
||||
- target: nsis
|
||||
- target: portable
|
||||
|
||||
nsis:
|
||||
oneClick: false
|
||||
perMachine: false
|
||||
allowElevation: false
|
||||
allowToChangeInstallationDirectory: true
|
||||
|
||||
linux:
|
||||
category: Development
|
||||
target:
|
||||
- target: AppImage
|
||||
- target: deb
|
||||
- target: tar.gz
|
||||
@@ -3,41 +3,18 @@
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"main": "src/main.ts",
|
||||
"main": "dist/main.js",
|
||||
"engines": {
|
||||
"node": ">=22.5.0"
|
||||
},
|
||||
"scripts": {
|
||||
"dev": "tsx src/main.ts",
|
||||
"build": "tsc",
|
||||
"dev": "tsx scripts/dev.ts",
|
||||
"build": "tsx scripts/build.ts",
|
||||
"test": "vitest run",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"generate:icons": "tsx scripts/generate-icons.ts",
|
||||
"pack": "electron-builder",
|
||||
"dist": "electron-builder --publish never"
|
||||
},
|
||||
"build": {
|
||||
"appId": "com.fusion.desktop",
|
||||
"productName": "Fusion",
|
||||
"directories": {
|
||||
"output": "dist-electron"
|
||||
},
|
||||
"mac": {
|
||||
"category": "public.app-category.developer-tools",
|
||||
"target": [
|
||||
"dmg"
|
||||
]
|
||||
},
|
||||
"win": {
|
||||
"target": [
|
||||
"nsis"
|
||||
]
|
||||
},
|
||||
"linux": {
|
||||
"target": [
|
||||
"AppImage"
|
||||
]
|
||||
}
|
||||
"pack": "electron-builder --dir",
|
||||
"dist": "electron-builder"
|
||||
},
|
||||
"dependencies": {
|
||||
"electron-updater": "^6.6.0"
|
||||
@@ -50,6 +27,7 @@
|
||||
"@vitest/coverage-v8": "^3.1.0",
|
||||
"electron": "^35.0.0",
|
||||
"electron-builder": "^26.0.0",
|
||||
"esbuild": "^0.25.12",
|
||||
"jsdom": "^29.0.1",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
|
||||
98
packages/desktop/scripts/build.ts
Normal file
98
packages/desktop/scripts/build.ts
Normal file
@@ -0,0 +1,98 @@
|
||||
import { build } from "esbuild";
|
||||
import { cp, mkdir, rm, stat } from "node:fs/promises";
|
||||
import { dirname, join, resolve } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { spawn } from "node:child_process";
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
const packageRoot = resolve(__dirname, "..");
|
||||
const workspaceRoot = resolve(packageRoot, "..", "..");
|
||||
const dashboardClientDir = join(workspaceRoot, "packages", "dashboard", "dist", "client");
|
||||
const desktopDistDir = join(packageRoot, "dist");
|
||||
const desktopClientDistDir = join(desktopDistDir, "client");
|
||||
|
||||
function run(command: string, args: string[], cwd: string): Promise<void> {
|
||||
return new Promise((resolvePromise, rejectPromise) => {
|
||||
const child = spawn(command, args, {
|
||||
cwd,
|
||||
stdio: "inherit",
|
||||
env: process.env,
|
||||
});
|
||||
|
||||
child.on("error", (error) => {
|
||||
rejectPromise(error);
|
||||
});
|
||||
|
||||
child.on("exit", (code) => {
|
||||
if (code === 0) {
|
||||
resolvePromise();
|
||||
return;
|
||||
}
|
||||
|
||||
rejectPromise(new Error(`${command} ${args.join(" ")} exited with code ${code ?? "unknown"}`));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function ensureDashboardBuild(): Promise<void> {
|
||||
console.log("[desktop:build] Building dashboard client...");
|
||||
await run("pnpm", ["--filter", "@fusion/dashboard", "build:client"], workspaceRoot);
|
||||
|
||||
try {
|
||||
await stat(dashboardClientDir);
|
||||
} catch {
|
||||
throw new Error(`Dashboard client assets not found: ${dashboardClientDir}`);
|
||||
}
|
||||
}
|
||||
|
||||
async function buildElectronEntrypoints(): Promise<void> {
|
||||
console.log("[desktop:build] Bundling Electron main/preload with esbuild...");
|
||||
|
||||
await Promise.all([
|
||||
build({
|
||||
entryPoints: [join(packageRoot, "src", "main.ts")],
|
||||
outfile: join(desktopDistDir, "main.js"),
|
||||
bundle: true,
|
||||
format: "esm",
|
||||
platform: "node",
|
||||
target: "node22",
|
||||
sourcemap: true,
|
||||
packages: "external",
|
||||
external: ["electron"],
|
||||
logLevel: "info",
|
||||
}),
|
||||
build({
|
||||
entryPoints: [join(packageRoot, "src", "preload.ts")],
|
||||
outfile: join(desktopDistDir, "preload.js"),
|
||||
bundle: true,
|
||||
format: "esm",
|
||||
platform: "node",
|
||||
target: "node22",
|
||||
sourcemap: true,
|
||||
packages: "external",
|
||||
external: ["electron"],
|
||||
logLevel: "info",
|
||||
}),
|
||||
]);
|
||||
}
|
||||
|
||||
async function copyDashboardClient(): Promise<void> {
|
||||
console.log("[desktop:build] Copying dashboard client into desktop dist/client...");
|
||||
await cp(dashboardClientDir, desktopClientDistDir, { recursive: true });
|
||||
}
|
||||
|
||||
async function main(): Promise<void> {
|
||||
await rm(desktopDistDir, { recursive: true, force: true });
|
||||
await mkdir(desktopDistDir, { recursive: true });
|
||||
|
||||
await ensureDashboardBuild();
|
||||
await buildElectronEntrypoints();
|
||||
await copyDashboardClient();
|
||||
|
||||
console.log("[desktop:build] Desktop build complete");
|
||||
}
|
||||
|
||||
void main().catch((error) => {
|
||||
console.error("[desktop:build] Build failed", error);
|
||||
process.exitCode = 1;
|
||||
});
|
||||
175
packages/desktop/scripts/dev.ts
Normal file
175
packages/desktop/scripts/dev.ts
Normal file
@@ -0,0 +1,175 @@
|
||||
import { build } from "esbuild";
|
||||
import { spawn, type ChildProcess } from "node:child_process";
|
||||
import { mkdir } from "node:fs/promises";
|
||||
import { dirname, join, resolve } from "node:path";
|
||||
import { createRequire } from "node:module";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
const packageRoot = resolve(__dirname, "..");
|
||||
const workspaceRoot = resolve(packageRoot, "..", "..");
|
||||
const distDir = join(packageRoot, "dist");
|
||||
|
||||
const require = createRequire(import.meta.url);
|
||||
|
||||
const sleep = (ms: number): Promise<void> => new Promise((resolvePromise) => {
|
||||
setTimeout(resolvePromise, ms);
|
||||
});
|
||||
|
||||
function run(command: string, args: string[], cwd: string, env: NodeJS.ProcessEnv = process.env): ChildProcess {
|
||||
return spawn(command, args, {
|
||||
cwd,
|
||||
env,
|
||||
stdio: "inherit",
|
||||
});
|
||||
}
|
||||
|
||||
async function buildMainProcess(): Promise<void> {
|
||||
await mkdir(distDir, { recursive: true });
|
||||
|
||||
await Promise.all([
|
||||
build({
|
||||
entryPoints: [join(packageRoot, "src", "main.ts")],
|
||||
outfile: join(distDir, "main.js"),
|
||||
bundle: true,
|
||||
format: "esm",
|
||||
platform: "node",
|
||||
target: "node22",
|
||||
sourcemap: true,
|
||||
packages: "external",
|
||||
external: ["electron"],
|
||||
logLevel: "info",
|
||||
}),
|
||||
build({
|
||||
entryPoints: [join(packageRoot, "src", "preload.ts")],
|
||||
outfile: join(distDir, "preload.js"),
|
||||
bundle: true,
|
||||
format: "esm",
|
||||
platform: "node",
|
||||
target: "node22",
|
||||
sourcemap: true,
|
||||
packages: "external",
|
||||
external: ["electron"],
|
||||
logLevel: "info",
|
||||
}),
|
||||
]);
|
||||
}
|
||||
|
||||
function resolveDashboardUrl(): URL {
|
||||
const raw = process.env.FUSION_DASHBOARD_URL ?? "http://localhost:5173";
|
||||
const parsed = new URL(raw);
|
||||
|
||||
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
|
||||
throw new Error(`FUSION_DASHBOARD_URL must be http(s), received: ${raw}`);
|
||||
}
|
||||
|
||||
return parsed;
|
||||
}
|
||||
|
||||
async function waitForRenderer(url: string, timeoutMs: number = 60_000): Promise<void> {
|
||||
const startedAt = Date.now();
|
||||
|
||||
while (Date.now() - startedAt < timeoutMs) {
|
||||
try {
|
||||
const response = await fetch(url);
|
||||
if (response.ok) {
|
||||
return;
|
||||
}
|
||||
} catch {
|
||||
// Keep polling until timeout.
|
||||
}
|
||||
|
||||
await sleep(500);
|
||||
}
|
||||
|
||||
throw new Error(`Renderer dev server did not become ready at ${url} within ${timeoutMs}ms`);
|
||||
}
|
||||
|
||||
async function main(): Promise<void> {
|
||||
console.log("[desktop:dev] Building main/preload entrypoints...");
|
||||
await buildMainProcess();
|
||||
|
||||
const dashboardUrl = resolveDashboardUrl();
|
||||
const dashboardHost = dashboardUrl.hostname;
|
||||
const dashboardPort = dashboardUrl.port || (dashboardUrl.protocol === "https:" ? "443" : "80");
|
||||
|
||||
console.log(`[desktop:dev] Starting dashboard Vite dev server on ${dashboardUrl.origin}...`);
|
||||
const viteProcess = run(
|
||||
"pnpm",
|
||||
[
|
||||
"--filter",
|
||||
"@fusion/dashboard",
|
||||
"dev:serve",
|
||||
"--host",
|
||||
dashboardHost,
|
||||
"--port",
|
||||
dashboardPort,
|
||||
"--strictPort",
|
||||
],
|
||||
workspaceRoot,
|
||||
);
|
||||
|
||||
let isShuttingDown = false;
|
||||
let electronProcess: ChildProcess | null = null;
|
||||
|
||||
const shutdown = (code: number): void => {
|
||||
if (isShuttingDown) {
|
||||
return;
|
||||
}
|
||||
|
||||
isShuttingDown = true;
|
||||
|
||||
if (electronProcess && !electronProcess.killed) {
|
||||
electronProcess.kill("SIGTERM");
|
||||
}
|
||||
|
||||
if (!viteProcess.killed) {
|
||||
viteProcess.kill("SIGTERM");
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
if (electronProcess && !electronProcess.killed) {
|
||||
electronProcess.kill("SIGKILL");
|
||||
}
|
||||
if (!viteProcess.killed) {
|
||||
viteProcess.kill("SIGKILL");
|
||||
}
|
||||
process.exit(code);
|
||||
}, 200);
|
||||
};
|
||||
|
||||
process.on("SIGINT", () => shutdown(0));
|
||||
process.on("SIGTERM", () => shutdown(0));
|
||||
|
||||
viteProcess.on("exit", (code) => {
|
||||
if (!isShuttingDown) {
|
||||
const exitCode = code ?? 1;
|
||||
console.error(`[desktop:dev] Dashboard dev server exited early with code ${exitCode}`);
|
||||
shutdown(exitCode);
|
||||
}
|
||||
});
|
||||
|
||||
await waitForRenderer(dashboardUrl.toString());
|
||||
console.log("[desktop:dev] Renderer dev server ready. Launching Electron...");
|
||||
|
||||
const electronBinary = require("electron") as string;
|
||||
electronProcess = run(
|
||||
electronBinary,
|
||||
["--enable-source-maps", join(distDir, "main.js"), "--dev"],
|
||||
packageRoot,
|
||||
{
|
||||
...process.env,
|
||||
NODE_ENV: "development",
|
||||
FUSION_DASHBOARD_URL: dashboardUrl.toString(),
|
||||
},
|
||||
);
|
||||
|
||||
electronProcess.on("exit", (code) => {
|
||||
shutdown(code ?? 0);
|
||||
});
|
||||
}
|
||||
|
||||
void main().catch((error) => {
|
||||
console.error("[desktop:dev] Failed to start development workflow", error);
|
||||
process.exit(1);
|
||||
});
|
||||
100
packages/desktop/src/__tests__/main.integration.test.ts
Normal file
100
packages/desktop/src/__tests__/main.integration.test.ts
Normal file
@@ -0,0 +1,100 @@
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
const mocks = vi.hoisted(() => {
|
||||
const app = {
|
||||
whenReady: vi.fn(() => Promise.resolve()),
|
||||
on: vi.fn(),
|
||||
quit: vi.fn(),
|
||||
};
|
||||
|
||||
const browserWindow = {
|
||||
loadURL: vi.fn(),
|
||||
on: vi.fn(),
|
||||
hide: vi.fn(),
|
||||
show: vi.fn(),
|
||||
maximize: vi.fn(),
|
||||
};
|
||||
|
||||
return {
|
||||
app,
|
||||
BrowserWindow: vi.fn(() => browserWindow),
|
||||
Tray: vi.fn(() => ({
|
||||
destroy: vi.fn(),
|
||||
setImage: vi.fn(),
|
||||
setContextMenu: vi.fn(),
|
||||
setToolTip: vi.fn(),
|
||||
on: vi.fn(),
|
||||
})),
|
||||
nativeImage: {
|
||||
createEmpty: vi.fn(() => ({ id: "empty-image" })),
|
||||
},
|
||||
browserWindow,
|
||||
buildAppMenu: vi.fn(),
|
||||
setupTray: vi.fn(),
|
||||
registerIpcHandlers: vi.fn(),
|
||||
registerDeepLinkProtocol: vi.fn(),
|
||||
setupDeepLinkHandler: vi.fn(),
|
||||
loadWindowState: vi.fn(async () => null),
|
||||
saveWindowState: vi.fn(),
|
||||
setupAutoUpdater: vi.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock("electron", () => ({
|
||||
app: mocks.app,
|
||||
BrowserWindow: mocks.BrowserWindow,
|
||||
Tray: mocks.Tray,
|
||||
nativeImage: mocks.nativeImage,
|
||||
}));
|
||||
|
||||
vi.mock("../menu.js", () => ({
|
||||
buildAppMenu: mocks.buildAppMenu,
|
||||
}));
|
||||
|
||||
vi.mock("../tray.js", () => ({
|
||||
setupTray: mocks.setupTray,
|
||||
}));
|
||||
|
||||
vi.mock("../ipc.js", () => ({
|
||||
registerIpcHandlers: mocks.registerIpcHandlers,
|
||||
}));
|
||||
|
||||
vi.mock("../deep-link.js", () => ({
|
||||
registerDeepLinkProtocol: mocks.registerDeepLinkProtocol,
|
||||
setupDeepLinkHandler: mocks.setupDeepLinkHandler,
|
||||
}));
|
||||
|
||||
vi.mock("../native.js", () => ({
|
||||
DEFAULT_WINDOW_STATE: {
|
||||
width: 1280,
|
||||
height: 900,
|
||||
isMaximized: false,
|
||||
},
|
||||
loadWindowState: mocks.loadWindowState,
|
||||
saveWindowState: mocks.saveWindowState,
|
||||
setupAutoUpdater: mocks.setupAutoUpdater,
|
||||
}));
|
||||
|
||||
describe("main module integration", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
vi.resetModules();
|
||||
});
|
||||
|
||||
it("imports main module exports with a mocked electron runtime", async () => {
|
||||
const mainModule = await import("../main.ts");
|
||||
|
||||
expect(mainModule.run).toBeTypeOf("function");
|
||||
expect(mainModule.initializeApp).toBeTypeOf("function");
|
||||
expect(mainModule.createMainWindow).toBeTypeOf("function");
|
||||
});
|
||||
|
||||
it("initializes app lifecycle wiring without throwing", async () => {
|
||||
const { initializeApp } = await import("../main.ts");
|
||||
|
||||
await expect(initializeApp()).resolves.toBeUndefined();
|
||||
expect(mocks.BrowserWindow).toHaveBeenCalledTimes(1);
|
||||
expect(mocks.registerIpcHandlers).toHaveBeenCalledTimes(1);
|
||||
expect(mocks.setupTray).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
@@ -91,12 +91,13 @@ describe("main process", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("DASHBOARD_URL defaults to localhost:4040", async () => {
|
||||
it("DASHBOARD_URL defaults to local file URL in production mode", async () => {
|
||||
delete process.env.FUSION_DASHBOARD_URL;
|
||||
|
||||
const { DASHBOARD_URL } = await importMainModule();
|
||||
|
||||
expect(DASHBOARD_URL).toBe("http://localhost:4040");
|
||||
expect(DASHBOARD_URL.startsWith("file://")).toBe(true);
|
||||
expect(DASHBOARD_URL).toContain("/client/index.html");
|
||||
});
|
||||
|
||||
it("DASHBOARD_URL uses env override", async () => {
|
||||
@@ -125,7 +126,7 @@ describe("main process", () => {
|
||||
|
||||
expect(options.webPreferences.contextIsolation).toBe(true);
|
||||
expect(options.webPreferences.nodeIntegration).toBe(false);
|
||||
expect(options.webPreferences.preload).toContain("preload.ts");
|
||||
expect(options.webPreferences.preload).toContain("preload.js");
|
||||
});
|
||||
|
||||
it("createMainWindow loads the dashboard URL", async () => {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { app, BrowserWindow, nativeImage, Tray } from "electron";
|
||||
import { join, resolve } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { fileURLToPath, pathToFileURL } from "node:url";
|
||||
import { setupDeepLinkHandler, registerDeepLinkProtocol } from "./deep-link.js";
|
||||
import { registerIpcHandlers } from "./ipc.js";
|
||||
import { buildAppMenu } from "./menu.js";
|
||||
@@ -17,11 +17,33 @@ interface AppWithQuitFlag {
|
||||
isQuitting?: boolean;
|
||||
}
|
||||
|
||||
const DEFAULT_DEV_DASHBOARD_URL = "http://localhost:5173";
|
||||
|
||||
function isDevelopmentMode(): boolean {
|
||||
return process.env.NODE_ENV === "development" || process.argv.includes("--dev");
|
||||
}
|
||||
|
||||
const PRODUCTION_DASHBOARD_URL = pathToFileURL(
|
||||
join(import.meta.dirname, "client", "index.html"),
|
||||
).toString();
|
||||
|
||||
export const IS_DEVELOPMENT = isDevelopmentMode();
|
||||
export const DASHBOARD_URL = process.env.FUSION_DASHBOARD_URL ?? (
|
||||
IS_DEVELOPMENT ? DEFAULT_DEV_DASHBOARD_URL : PRODUCTION_DASHBOARD_URL
|
||||
);
|
||||
|
||||
function enableSourceMaps(): void {
|
||||
const processWithSourceMaps = process as NodeJS.Process & {
|
||||
setSourceMapsEnabled?: (enabled: boolean) => void;
|
||||
};
|
||||
processWithSourceMaps.setSourceMapsEnabled?.(true);
|
||||
}
|
||||
|
||||
enableSourceMaps();
|
||||
|
||||
let mainWindow: BrowserWindow | null = null;
|
||||
let tray: Tray | null = null;
|
||||
|
||||
export const DASHBOARD_URL = process.env.FUSION_DASHBOARD_URL || "http://localhost:4040";
|
||||
|
||||
function getAppWithQuitFlag(): Electron.App & AppWithQuitFlag {
|
||||
return app as Electron.App & AppWithQuitFlag;
|
||||
}
|
||||
@@ -35,7 +57,7 @@ export function createMainWindow(state?: WindowState): BrowserWindow {
|
||||
...(hasValidPosition ? { x: state.x, y: state.y } : {}),
|
||||
title: "Fusion",
|
||||
webPreferences: {
|
||||
preload: join(import.meta.dirname, "preload.ts"),
|
||||
preload: join(import.meta.dirname, "preload.js"),
|
||||
contextIsolation: true,
|
||||
nodeIntegration: false,
|
||||
},
|
||||
|
||||
@@ -6,9 +6,11 @@ export default defineConfig({
|
||||
test: {
|
||||
include: ["src/**/*.test.ts", "src/**/*.test.tsx"],
|
||||
environmentMatchGlobs: [["src/renderer/**", "jsdom"]],
|
||||
pool: "threads",
|
||||
testTimeout: 30_000,
|
||||
hookTimeout: 30_000,
|
||||
maxWorkers,
|
||||
fileParallelism: true,
|
||||
pool: "threads",
|
||||
passWithNoTests: true,
|
||||
},
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user