Files
fusion/packages/desktop/scripts/dev.ts
gsxdsm ecf0eb6d84 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
2026-04-08 01:40:05 -07:00

176 lines
4.5 KiB
TypeScript

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);
});