import { spawn } from 'node:child_process' import { createServer } from 'node:net' import { join } from 'node:path' import { app, BrowserWindow, shell } from 'electron' import killProcessTree from 'tree-kill' const DEV_SERVER_URL = 'http://localhost:3000' let mainWindow: BrowserWindow | null = null let serverProcess: ReturnType | null = null let isQuitting = false const getAvailablePort = (): Promise => new Promise((resolve, reject) => { const server = createServer() server.listen(0, () => { const addr = server.address() if (!addr || typeof addr === 'string') { server.close() reject(new Error('Failed to resolve port')) return } server.close(() => resolve(addr.port)) }) server.on('error', reject) }) const isServerReady = async (url: string): Promise => { try { const response = await fetch(url, { method: 'HEAD' }) return response.ok } catch { return false } } const waitForServer = async ( url: string, timeoutMs = 15_000, ): Promise => { const start = Date.now() while (Date.now() - start < timeoutMs && !isQuitting) { if (await isServerReady(url)) return true await new Promise((resolve) => setTimeout(resolve, 200)) } return false } const stopServerProcess = () => { if (!serverProcess) { return } const runningServer = serverProcess serverProcess = null if (!runningServer.pid || runningServer.exitCode !== null) { return } killProcessTree(runningServer.pid, (error?: Error) => { if (error) { console.error('Failed to stop server process:', error) } }) } const spawnServer = (port: number): string => { const binaryName = process.platform === 'win32' ? 'server.exe' : 'server' const binaryPath = join(process.resourcesPath, binaryName) serverProcess = spawn(binaryPath, [], { env: { ...process.env, PORT: String(port), HOST: '127.0.0.1', }, stdio: 'ignore', }) serverProcess.unref() serverProcess.on('error', (err) => { console.error('Failed to start server:', err) }) serverProcess.on('exit', () => { serverProcess = null }) return `http://127.0.0.1:${port}` } const getServerUrl = async (): Promise => { if (!app.isPackaged) { return DEV_SERVER_URL } const port = await getAvailablePort() if (isQuitting) { return null } return spawnServer(port) } const createWindow = async () => { mainWindow = new BrowserWindow({ width: 1200, height: 800, show: false, webPreferences: { preload: join(__dirname, '../preload/index.mjs'), sandbox: true, }, }) mainWindow.webContents.setWindowOpenHandler(({ url }) => { shell.openExternal(url) return { action: 'deny' } }) mainWindow.on('closed', () => { mainWindow = null }) if (process.env.ELECTRON_RENDERER_URL) { mainWindow.loadURL(process.env.ELECTRON_RENDERER_URL) } else { mainWindow.loadFile(join(__dirname, '../renderer/index.html')) } mainWindow.show() const serverUrl = await getServerUrl() if (!serverUrl || isQuitting || !mainWindow || mainWindow.isDestroyed()) { stopServerProcess() return } console.log(`Waiting for server at ${serverUrl}...`) const ready = await waitForServer(serverUrl) if (isQuitting || !mainWindow || mainWindow.isDestroyed()) { stopServerProcess() return } if (!ready) { console.error( app.isPackaged ? 'Server binary did not start in time.' : 'Dev server not responding. Run `bun dev` in apps/server first.', ) app.quit() return } console.log(`Loading ${serverUrl}`) if (!mainWindow || mainWindow.isDestroyed()) { return } mainWindow.loadURL(serverUrl) } app.whenReady().then(createWindow) app.on('window-all-closed', () => { if (process.platform !== 'darwin') { isQuitting = true stopServerProcess() app.quit() } }) app.on('activate', () => { if (!isQuitting && BrowserWindow.getAllWindows().length === 0) { createWindow() } }) app.on('before-quit', () => { isQuitting = true stopServerProcess() })