nose-pluto/src/webapp.ts

114 lines
3.0 KiB
TypeScript

////
// Hosting for your NOSE webapps!
import type { Child } from "hono/jsx"
import { type Context, Hono } from "hono"
import { renderToString } from "hono/jsx/dom/server"
import { join } from "path"
import { readdirSync, watch } from "fs"
import { sendAll } from "./websocket"
import { expectDir } from "./utils"
import { NOSE_DIR } from "./config"
import { isFile, isDir } from "./utils"
import { importUrl } from "./shared/utils"
export type Handler = (r: Context) => string | Child | Response | Promise<Response>
export type App = Hono | Handler
export function initWebapps() {
startWatcher()
}
export async function serveApp(c: Context, subdomain: string): Promise<Response> {
const app = await findApp(subdomain)
const path = appDir(subdomain)
if (!path) return c.text(`App not found: ${subdomain}`, 404)
const staticPath = join(path, "pub", c.req.path === "/" ? "/index.html" : c.req.path)
if (isFile(staticPath))
return serveStatic(staticPath)
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_DIR))
if (isApp(entry))
apps.push(entry)
return apps.sort()
}
function isApp(name: string): boolean {
return isFile(join(NOSE_DIR, name, "index.ts"))
|| isFile(join(NOSE_DIR, name, "index.tsx"))
|| isDir(join(NOSE_DIR, name, "pub"))
}
export function appDir(name: string): string | undefined {
if (isApp(name))
return join(NOSE_DIR, name)
}
async function findApp(name: string): Promise<App | undefined> {
const paths = [
join(name, "index.ts"),
join(name, "index.tsx")
]
for (const path of paths) {
const app = await loadApp(join(NOSE_DIR, path))
if (app) return app
}
}
async function loadApp(path: string): Promise<App | undefined> {
if (!await Bun.file(path).exists()) return
const mod = await importUrl(path)
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"
}
})
}
function serveStatic(path: string): Response {
const file = Bun.file(path)
return new Response(file, {
headers: {
"Content-Type": file.type
}
})
}
let wwwWatcher
function startWatcher() {
if (!expectDir(NOSE_DIR)) return
wwwWatcher = watch(NOSE_DIR, { recursive: true }, async (event, filename) => {
if (!filename) return
if (/^.+\/index\.tsx?$/.test(filename))
sendAll({ type: "apps", data: apps() })
})
}