Files
fullstack-starter/apps/desktop/src/main/index.ts

184 lines
4.1 KiB
TypeScript

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<typeof spawn> | null = null
let isQuitting = false
const getAvailablePort = (): Promise<number> =>
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<boolean> => {
try {
const response = await fetch(url, { method: 'HEAD' })
return response.ok
} catch {
return false
}
}
const waitForServer = async (
url: string,
timeoutMs = 15_000,
): Promise<boolean> => {
const start = Date.now()
while (Date.now() - start < timeoutMs && !isQuitting) {
if (await isServerReady(url)) return true
await new Promise<void>((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<string | null> => {
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()
})