96 lines
2.5 KiB
TypeScript
96 lines
2.5 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 } from "fs"
|
|
|
|
import { NOSE_DIR } from "./config"
|
|
import { isFile, isDir } 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)
|
|
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 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"
|
|
}
|
|
})
|
|
}
|
|
|
|
function serveStatic(path: string): Response {
|
|
const file = Bun.file(path)
|
|
return new Response(file, {
|
|
headers: {
|
|
"Content-Type": file.type
|
|
}
|
|
})
|
|
} |