129 lines
3.5 KiB
TypeScript
129 lines
3.5 KiB
TypeScript
////
|
|
// Hosting for your NOSE webapps!
|
|
|
|
import type { Child } from "hono/jsx"
|
|
import { type Context, Hono } from "hono"
|
|
import { join } from "path"
|
|
import { watch } from "fs"
|
|
import { sendAll } from "../websocket"
|
|
import { expectDir } from "../utils"
|
|
import { NOSE_DIR, BUN_BIN } from "../config"
|
|
import { isFile } from "../utils"
|
|
import { apps, isApp, appDir, isStaticApp, webappLog } from "./utils"
|
|
|
|
export type Handler = (r: Context) => string | Child | Response | Promise<Response>
|
|
export type App = Hono | Handler
|
|
|
|
const processes = new Map<string, { port: string, proc: ReturnType<typeof Bun.spawn> }>()
|
|
const restarting = new Set<string>()
|
|
|
|
export async function initWebapps() {
|
|
await startSubprocs()
|
|
startWatcher()
|
|
}
|
|
|
|
export async function serveApp(c: Context, subdomain: string): Promise<Response> {
|
|
if (!isApp(subdomain)) return c.text(`App not found: ${subdomain}`, 404)
|
|
|
|
const staticPath = join(appDir(subdomain)!, "pub", c.req.path === "/" ? "/index.html" : c.req.path)
|
|
if (isFile(staticPath)) return serveStatic(staticPath)
|
|
|
|
if (isStaticApp(subdomain)) return c.text("File not found", 404)
|
|
|
|
const port = await startApp(subdomain)
|
|
if (!port) return c.text(`App not found: ${subdomain}`, 404)
|
|
|
|
try {
|
|
const res = await fetch(`http://localhost:${port}${c.req.path}`, {
|
|
method: c.req.method,
|
|
headers: c.req.raw.headers,
|
|
body: c.req.raw.body,
|
|
})
|
|
|
|
return new Response(res.body, { status: res.status, headers: res.headers })
|
|
} catch {
|
|
return c.text("File not found", 404)
|
|
}
|
|
}
|
|
|
|
async function startApp(name: string): Promise<string | undefined> {
|
|
if (isStaticApp(name)) return
|
|
|
|
const existing = processes.get(name)
|
|
if (existing) return existing.port
|
|
|
|
const port = String(4000 + processes.size)
|
|
const proc = Bun.spawn({
|
|
cmd: [BUN_BIN, "run", "src/webapp/worker.ts", name],
|
|
env: { PORT: port },
|
|
stdout: "inherit",
|
|
stderr: "inherit",
|
|
})
|
|
|
|
processes.set(name, { port, proc })
|
|
proc.exited.then(() => restartApp(name))
|
|
|
|
await Bun.sleep(100) // give it time before first request
|
|
|
|
return port
|
|
}
|
|
|
|
|
|
function serveStatic(path: string): Response {
|
|
const file = Bun.file(path)
|
|
return new Response(file, {
|
|
headers: {
|
|
"Content-Type": file.type
|
|
}
|
|
})
|
|
}
|
|
|
|
async function shutdown() {
|
|
for (const [name, { port, proc }] of processes) {
|
|
webappLog(name, "Shutting down")
|
|
try { proc.kill() } catch { }
|
|
}
|
|
process.exit(0)
|
|
}
|
|
|
|
async function restartApp(name: string) {
|
|
if (restarting.has(name)) return
|
|
restarting.add(name)
|
|
|
|
webappLog(name, "restarting")
|
|
const existing = processes.get(name)
|
|
if (existing) {
|
|
try { existing.proc.kill() } catch { }
|
|
processes.delete(name)
|
|
}
|
|
|
|
await Bun.sleep(200) // give bun time to release the port
|
|
restarting.delete(name)
|
|
await startApp(name)
|
|
}
|
|
|
|
async function startSubprocs() {
|
|
const list = apps()
|
|
await Promise.all(list.map(app => startApp(app)))
|
|
}
|
|
|
|
|
|
let wwwWatcher
|
|
function startWatcher() {
|
|
if (!expectDir(NOSE_DIR)) return
|
|
|
|
wwwWatcher = watch(NOSE_DIR, { recursive: true }, async (event, filename) => {
|
|
if (!filename) return
|
|
|
|
const [appName,] = filename.split("/")
|
|
if (appName && !filename.includes("/pub/"))
|
|
restartApp(appName)
|
|
|
|
if (/^.+\/index\.tsx?$/.test(filename))
|
|
sendAll({ type: "apps", data: apps() })
|
|
})
|
|
}
|
|
|
|
process.on("SIGINT", shutdown)
|
|
process.on("SIGTERM", shutdown)
|