import { Hono } from "hono" import { serveStatic, upgradeWebSocket, websocket } from "hono/bun" import { prettyJSON } from "hono/pretty-json" import color from "kleur" import type { Message } from "./shared/types" import { NOSE_ICON, NOSE_BIN, NOSE_WWW } from "./config" import { transpile, isFile, tilde } from "./utils" import { apps, serveApp, publishDNS } from "./webapp" import { commands } from "./commands" import { send, addWebsocket, removeWebsocket, closeWebsockets } from "./websocket" import { Layout } from "./components/layout" import { Terminal } from "./components/terminal" import { dispatchMessage } from "./dispatch" // // Hono setup // const app = new Hono() app.use("*", prettyJSON()) app.use('/img/*', serveStatic({ root: './public' })) app.use('/vendor/*', serveStatic({ root: './public' })) app.use('/css/*', serveStatic({ root: './src' })) app.use("*", async (c, next) => { const start = Date.now() await next() const end = Date.now() const fn = c.res.status < 300 ? color.green : c.res.status < 500 ? color.yellow : color.red console.log(fn(`${c.res.status} ${c.req.method} ${c.req.url} (${end - start}ms)`)) }) app.on("GET", ["/js/:path{.+}", "/shared/:path{.+}"], async c => { const path = "./src/" + c.req.path.replace("..", ".") // path must end in .js if (!path.endsWith(".js")) return c.text("File not found", 404) const ts = path.replace(".js", ".ts") if (isFile(ts)) { return new Response(await transpile(ts), { headers: { "Content-Type": "text/javascript" } }) } else if (isFile(path)) { return new Response(Bun.file(path), { headers: { "Content-Type": "text/javascript" } }) } else { return c.text("File not found", 404) } }) // // app routes // app.use("*", async (c, next) => { const url = new URL(c.req.url) const localhost = url.hostname.endsWith("localhost") const domains = url.hostname.split(".") let subdomain = "" if (domains.length > (localhost ? 1 : 2)) subdomain = domains[0]! if (subdomain) { const app = serveApp(c, subdomain) return await app } return next() }) app.get("/apps", c => { const url = new URL(c.req.url) const domain = url.hostname let port = url.port port = port && port !== "80" ? `:${port}` : "" return c.html(<>

apps

) }) app.get("/", c => c.html()) // // websocket // app.get("/ws", upgradeWebSocket(async c => { return { async onOpen(_e, ws) { addWebsocket(ws) send(ws, { type: "commands", data: await commands() }) }, async onMessage(event, ws) { let data: Message | undefined try { data = JSON.parse(event.data.toString()) } catch (e) { console.error("JSON parsing error", e) send(ws, { type: "error", data: "json parsing error" }) return } if (!data) return await dispatchMessage(ws, data) }, onClose: (event, ws) => removeWebsocket(ws) } })) // // hot reload mode cleanup // if (process.env.BUN_HOT) { // @ts-ignore globalThis.__hot_reload_cleanup?.() // @ts-ignore globalThis.__hot_reload_cleanup = () => { closeWebsockets() } for (const sig of ["SIGINT", "SIGTERM"] as const) { process.on(sig, () => { // @ts-ignore globalThis.__hot_reload_cleanup?.() process.exit(0) }) } } else { publishDNS() } // // server start // console.log(color.cyan(NOSE_ICON)) console.log(color.blue("NOSE_BIN:"), color.yellow(tilde(NOSE_BIN))) console.log(color.blue("NOSE_WWW:"), color.yellow(tilde(NOSE_WWW))) export default { port: process.env.PORT || 3000, hostname: "0.0.0.0", fetch: app.fetch, idleTimeout: 0, websocket }