//// // 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 export type App = Hono | Handler export function initWebapps() { startWatcher() } export async function serveApp(c: Context, subdomain: string): Promise { 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 { 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 { 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() }) }) }