import type { Child } from "hono/jsx" import { type Context, Hono } from "hono" import { renderToString } from "hono/jsx/dom/server" import { join, dirname } from "path" import { readdirSync, watch } from "fs" import { NOSE_WWW } from "./config" import { expectDir, isFile } from "./utils" export type Handler = (r: Context) => string | Child | Response | Promise export type App = Hono | Handler export async function serveApp(c: Context, subdomain: string): Promise { const app = await findApp(subdomain) if (!app) return c.text(`App Not Found: ${subdomain}`, 404) if (app instanceof Hono) return app.fetch(c.req.raw) else return toResponse(await app(c)) } export function apps(): string[] { const apps: string[] = [] for (const entry of readdirSync(NOSE_WWW)) apps.push(entry.replace(/\.tsx?/, "")) return apps.sort() } export function appPath(name: string): string | undefined { const path = [ `${name}.ts`, `${name}.tsx`, join(name, "index.ts"), join(name, "index.tsx") ] .map(path => join(NOSE_WWW, path)) .flat() .filter(path => isFile(path))[0] if (!path) return return dirname(path) } async function findApp(name: string): Promise { const paths = [ `${name}.ts`, `${name}.tsx`, join(name, "index.ts"), join(name, "index.tsx") ] let app for (const path of paths) { app = await loadApp(join(NOSE_WWW, path)) if (app) return app } console.error("can't find app:", name) } async function loadApp(path: string): Promise { if (!await Bun.file(path).exists()) return const mod = await import(path + `?t=${Date.now()}`) if (mod?.default) return mod.default as App } export function toResponse(source: string | Child | Response): Response { if (source instanceof Response) return source else if (typeof source === "string") return new Response(source) else return new Response(renderToString(source), { headers: { "Content-Type": "text/html; charset=utf-8" } }) } // // dns nonsense // const dnsEntries: Record = {} const { stdout: ipRaw } = await Bun.$`hostname -I | awk '{print $1}'`.quiet() const { stdout: hostRaw } = await Bun.$`hostname`.quiet() const ip = ipRaw.toString().trim() const host = hostRaw.toString().trim() export async function publishDNS() { apps().forEach(publishAppDNS) const signals = ["SIGINT", "SIGTERM"] signals.forEach(sig => process.on(sig, () => { for (const name in dnsEntries) dnsEntries[name].kill("SIGTERM") process.exit(0) }) ) } function publishAppDNS(app: string) { if (process.env.BUN_HOT) return if (!dnsEntries[app]) dnsEntries[app] = Bun.spawn(["avahi-publish", "-a", `${app}.${host}.local`, "-R", ip]) return dnsEntries[app] } // exit process with error if no WWW dir expectDir(NOSE_WWW) const wwwWatcher = watch(NOSE_WWW, (event, filename) => { const www = apps() www.forEach(publishAppDNS) for (const name in dnsEntries) if (!www.includes(name)) { dnsEntries[name].kill("SIGTERM") delete dnsEntries[name] } })