diff --git a/apps/desktop/electrobun.config.ts b/apps/desktop/electrobun.config.ts index a9af3b8..96573a5 100644 --- a/apps/desktop/electrobun.config.ts +++ b/apps/desktop/electrobun.config.ts @@ -10,6 +10,15 @@ export default { bun: { entrypoint: 'src/bun/index.ts', }, + copy: { + '../server/.output': 'server-output', + }, + mac: { + bundleCEF: true, + }, + win: { + bundleCEF: true, + }, linux: { bundleCEF: true, }, diff --git a/apps/desktop/src/bun/index.ts b/apps/desktop/src/bun/index.ts index 51d07e2..63c2b7a 100644 --- a/apps/desktop/src/bun/index.ts +++ b/apps/desktop/src/bun/index.ts @@ -1,37 +1,136 @@ -import Electrobun, { BrowserWindow } from 'electrobun/bun' +import { dirname, join } from 'node:path' +import { BrowserWindow, PATHS } from 'electrobun/bun' const DEV_SERVER_URL = 'http://localhost:3000' +const SERVER_READY_TIMEOUT_MS = 30_000 +const PORT_PATTERN = /Listening on:?\s*https?:\/\/[^\s:]+:(\d+)/ -async function waitForServer(url: string, timeoutMs = 30000): Promise { +const isDev = (): boolean => { + const env = process.env.ELECTROBUN_BUILD_ENV + return !env || env === 'dev' +} + +const getServerEntryPath = (): string => { + return join(PATHS.VIEWS_FOLDER, '..', 'server-output', 'server', 'index.mjs') +} + +const waitForServer = async ( + url: string, + timeoutMs = SERVER_READY_TIMEOUT_MS, +): Promise => { const start = Date.now() while (Date.now() - start < timeoutMs) { try { const response = await fetch(url, { method: 'HEAD' }) if (response.ok) return true - } catch { - await Bun.sleep(100) + } catch (_) { + // Server not up yet, retry after sleep } + await Bun.sleep(100) } return false } -async function main() { +const spawnServer = async (): Promise<{ + process: ReturnType + url: string +}> => { + const serverEntryPath = getServerEntryPath() + const serverDir = dirname(serverEntryPath) + + const serverProc = Bun.spawn([process.execPath, serverEntryPath], { + cwd: serverDir, + env: { + ...process.env, + PORT: '0', + HOST: '127.0.0.1', + }, + stdout: 'pipe', + stderr: 'pipe', + }) + + const port = await new Promise((resolve, reject) => { + const timeout = setTimeout(() => { + serverProc.kill() + reject( + new Error(`Server did not start within ${SERVER_READY_TIMEOUT_MS}ms`), + ) + }, SERVER_READY_TIMEOUT_MS) + + const reader = serverProc.stdout.getReader() + let buffer = '' + + const read = async () => { + try { + while (true) { + const { done, value } = await reader.read() + if (done) { + clearTimeout(timeout) + reject(new Error('Server process exited before reporting port')) + return + } + + const text = new TextDecoder().decode(value) + buffer += text + process.stdout.write(text) + + const match = PORT_PATTERN.exec(buffer) + if (match?.[1]) { + clearTimeout(timeout) + resolve(match[1]) + return + } + } + } catch (err) { + clearTimeout(timeout) + reject(err) + } + } + + serverProc.exited.then((code) => { + clearTimeout(timeout) + reject(new Error(`Server exited with code ${code} before becoming ready`)) + }) + + read() + }) + + return { process: serverProc, url: `http://127.0.0.1:${port}` } +} + +const main = async () => { console.log('Starting Furtherverse Desktop...') - console.log('Waiting for dev server at', DEV_SERVER_URL) - const ready = await waitForServer(DEV_SERVER_URL) - if (!ready) { - console.error( - 'Dev server not responding. Make sure to run: cd apps/server && bun dev', - ) - process.exit(1) + let serverUrl: string + let serverProcess: ReturnType | null = null + + if (isDev()) { + console.log('Dev mode: waiting for external server at', DEV_SERVER_URL) + const ready = await waitForServer(DEV_SERVER_URL) + if (!ready) { + console.error( + 'Dev server not responding. Make sure to run: bun dev in apps/server', + ) + process.exit(1) + } + console.log('Dev server ready!') + serverUrl = DEV_SERVER_URL + } else { + console.log('Production mode: starting embedded server...') + try { + const server = await spawnServer() + serverProcess = server.process + serverUrl = server.url + console.log('Server ready at', serverUrl) + } catch (err) { + console.error('Failed to start embedded server:', err) + process.exit(1) + } } - console.log('Dev server ready!') - new BrowserWindow({ title: 'Furtherverse', - url: DEV_SERVER_URL, + url: serverUrl, frame: { x: 100, y: 100, @@ -41,7 +140,31 @@ async function main() { renderer: 'cef', }) - Electrobun.events.on('will-quit', () => console.log('Quitting...')) + if (serverProcess) { + const cleanup = () => { + if (serverProcess) { + serverProcess.kill() + serverProcess = null + } + } + + process.on('exit', cleanup) + process.on('SIGTERM', () => { + cleanup() + process.exit(0) + }) + process.on('SIGINT', () => { + cleanup() + process.exit(0) + }) + + serverProcess.exited.then((code) => { + if (serverProcess) { + console.error(`Server exited unexpectedly with code ${code}`) + process.exit(1) + } + }) + } } main().catch((error) => { diff --git a/apps/desktop/turbo.json b/apps/desktop/turbo.json index 054a5fb..ab50186 100644 --- a/apps/desktop/turbo.json +++ b/apps/desktop/turbo.json @@ -3,6 +3,7 @@ "extends": ["//"], "tasks": { "build": { + "dependsOn": ["@furtherverse/server#build"], "outputs": ["build/**", "artifacts/**"] } }