forked from imbytecat/fullstack-starter
168 lines
3.8 KiB
TypeScript
168 lines
3.8 KiB
TypeScript
import { spawn } from 'node:child_process'
|
|
import { createServer } from 'node:net'
|
|
import { join } from 'node:path'
|
|
import { app, BrowserWindow, shell } from 'electron'
|
|
|
|
const DEV_SERVER_URL = 'http://localhost:3000'
|
|
|
|
let mainWindow: BrowserWindow | null = null
|
|
let serverProcess: ReturnType<typeof spawn> | null = null
|
|
|
|
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) {
|
|
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
|
|
}
|
|
|
|
runningServer.kill()
|
|
}
|
|
|
|
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: 'pipe',
|
|
})
|
|
|
|
serverProcess.stdout?.on('data', (data: Buffer) => {
|
|
console.log(`[server] ${data.toString().trim()}`)
|
|
})
|
|
|
|
serverProcess.stderr?.on('data', (data: Buffer) => {
|
|
console.error(`[server] ${data.toString().trim()}`)
|
|
})
|
|
|
|
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> => {
|
|
if (!app.isPackaged) {
|
|
return DEV_SERVER_URL
|
|
}
|
|
const port = await getAvailablePort()
|
|
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()
|
|
|
|
console.log(`Waiting for server at ${serverUrl}...`)
|
|
const ready = await waitForServer(serverUrl)
|
|
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') {
|
|
stopServerProcess()
|
|
app.quit()
|
|
}
|
|
})
|
|
|
|
app.on('activate', () => {
|
|
if (BrowserWindow.getAllWindows().length === 0) {
|
|
createWindow()
|
|
}
|
|
})
|
|
|
|
app.on('before-quit', () => {
|
|
stopServerProcess()
|
|
})
|