| 1 | #!/usr/bin/env node |
| 2 | import { spawn } from "node:child_process"; |
| 3 | import { mkdir } from "node:fs/promises"; |
| 4 | import { dirname, resolve } from "node:path"; |
| 5 | import { fileURLToPath } from "node:url"; |
| 6 | |
| 7 | const desktopRoot = dirname(dirname(fileURLToPath(import.meta.url))); |
| 8 | const pnpm = process.platform === "win32" ? "pnpm.cmd" : "pnpm"; |
| 9 | const serviceName = process.platform === "win32" |
| 10 | ? "reasonix-desktop-service.exe" |
| 11 | : "reasonix-desktop-service"; |
| 12 | const servicePath = resolve(desktopRoot, "build/bin", serviceName); |
| 13 | const port = process.env.REASONIX_DESKTOP_VITE_PORT || "5173"; |
| 14 | const devUrl = process.env.REASONIX_ELECTRON_DEV_URL || `http://127.0.0.1:${port}`; |
| 15 | |
| 16 | if (process.argv.includes("--help")) { |
| 17 | console.log("Build the Reasonix service, start Vite, and launch the Electron development shell."); |
| 18 | console.log("Usage: pnpm dev:desktop"); |
| 19 | process.exit(0); |
| 20 | } |
| 21 | |
| 22 | function start(command, args, env = process.env) { |
| 23 | return spawn(command, args, { cwd: desktopRoot, env, stdio: "inherit" }); |
| 24 | } |
| 25 | |
| 26 | function waitForExit(child, label) { |
| 27 | return new Promise((resolveExit, rejectExit) => { |
| 28 | child.once("error", rejectExit); |
| 29 | child.once("exit", (code, signal) => { |
| 30 | if (code === 0) resolveExit(); |
| 31 | else rejectExit(new Error(`${label} exited with ${code ?? signal ?? "an unknown error"}`)); |
| 32 | }); |
| 33 | }); |
| 34 | } |
| 35 | |
| 36 | async function servesReasonix(url) { |
| 37 | try { |
| 38 | const response = await fetch(url); |
| 39 | if (!response.ok) return false; |
| 40 | const html = await response.text(); |
| 41 | return /<title>\s*Reasonix\s*<\/title>/i.test(html); |
| 42 | } catch { |
| 43 | return false; |
| 44 | } |
| 45 | } |
| 46 | |
| 47 | async function waitForVite(frontend) { |
| 48 | const deadline = Date.now() + 30_000; |
| 49 | while (Date.now() < deadline) { |
| 50 | if (frontend.exitCode !== null || frontend.signalCode) { |
| 51 | throw new Error(`Vite exited with ${frontend.exitCode ?? frontend.signalCode}`); |
| 52 | } |
| 53 | if (await servesReasonix(devUrl)) return; |
| 54 | await new Promise(resolveWait => setTimeout(resolveWait, 150)); |
| 55 | } |
| 56 | throw new Error(`Vite did not become ready at ${devUrl}`); |
| 57 | } |
| 58 | |
| 59 | let frontend; |
| 60 | let electron; |
| 61 | let exiting = false; |
| 62 | |
| 63 | function stopChildren() { |
| 64 | if (electron?.exitCode === null) electron.kill("SIGTERM"); |
| 65 | if (frontend?.exitCode === null) frontend.kill("SIGTERM"); |
| 66 | } |
| 67 | |
| 68 | function fail(error) { |
| 69 | console.error(error instanceof Error ? error.message : error); |
| 70 | stopChildren(); |
| 71 | process.exitCode = 1; |
| 72 | } |
| 73 | |
| 74 | for (const signal of ["SIGINT", "SIGTERM"]) { |
| 75 | process.once(signal, () => { |
| 76 | exiting = true; |
| 77 | stopChildren(); |
| 78 | }); |
| 79 | } |
| 80 | |
| 81 | try { |
| 82 | await mkdir(dirname(servicePath), { recursive: true }); |
| 83 | console.log("==> Building Reasonix desktop service"); |
| 84 | await waitForExit(start("go", ["build", "-o", servicePath, "."]), "Go build"); |
| 85 | |
| 86 | if (await servesReasonix(devUrl)) { |
| 87 | console.log(`==> Reusing Reasonix Vite server at ${devUrl}`); |
| 88 | } else { |
| 89 | console.log(`==> Starting Reasonix Vite server at ${devUrl}`); |
| 90 | frontend = start(pnpm, ["--dir", "frontend", "dev", "--host", "127.0.0.1", "--port", port, "--strictPort"]); |
| 91 | await waitForVite(frontend); |
| 92 | } |
| 93 | |
| 94 | console.log("==> Launching Reasonix Electron development shell"); |
| 95 | electron = start(pnpm, ["--dir", "electron", "dev"], { |
| 96 | ...process.env, |
| 97 | REASONIX_DESKTOP_SERVICE: servicePath, |
| 98 | REASONIX_ELECTRON_DEV_URL: devUrl, |
| 99 | }); |
| 100 | const electronExit = waitForExit(electron, "Electron"); |
| 101 | const frontendExit = frontend |
| 102 | ? waitForExit(frontend, "Vite").then(() => { throw new Error("Vite stopped before Electron"); }) |
| 103 | : new Promise(() => {}); |
| 104 | await Promise.race([electronExit, frontendExit]); |
| 105 | if (frontend?.exitCode === null) frontend.kill("SIGTERM"); |
| 106 | } catch (error) { |
| 107 | if (!exiting) fail(error); |
| 108 | } |
| 109 |