133 lines
3.3 KiB
TypeScript
133 lines
3.3 KiB
TypeScript
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<Response>
|
|
export type App = Hono | Handler
|
|
|
|
export async function serveApp(c: Context, subdomain: string): Promise<Response> {
|
|
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<App | undefined> {
|
|
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<App | undefined> {
|
|
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<string, any> = {}
|
|
|
|
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]
|
|
}
|
|
}) |