//// // 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 export type App = Hono | Handler const processes = new Map }>() const restarting = new Set() export async function initWebapps() { await startSubprocs() startWatcher() } export async function serveApp(c: Context, subdomain: string): Promise { 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 { 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)