import { serveStatic } from "hono/bun"; import type { ViteDevServer } from "vite"; import { createServer as createViteServer } from "vite"; import config from "./zosite.json"; import { Hono } from "hono"; // AI agents: read README.md for navigation and contribution guidance. type Mode = "development" | "production"; const app = new Hono(); const mode: Mode = process.env.NODE_ENV === "production" ? "production" : "development"; /** * Add any API routes here. */ app.get("/api/hello-zo", (c) => c.json({ msg: "Hello from Zo" })); if (mode === "production") { configureProduction(app); } else { await configureDevelopment(app); } /** * Determine port based on mode. In production, use the published_port if available. * In development, always use the local_port. * Ports are managed by the system and injected via the PORT environment variable. */ const port = process.env.PORT ? parseInt(process.env.PORT, 10) : mode === "production" ? (config.publish?.published_port ?? config.local_port) : config.local_port; export default { fetch: app.fetch, port, idleTimeout: 255 }; /** * Configure routing for production builds. * * - Streams prebuilt assets from `dist`. * - Static files from `public/` are copied to `dist/` by Vite and served at root paths. * - Falls back to `index.html` for any other GET so the SPA router can resolve the request. */ function configureProduction(app: Hono) { app.use("/assets/*", serveStatic({ root: "./dist" })); app.get("/favicon.ico", (c) => c.redirect("/favicon.svg", 302)); app.use(async (c, next) => { if (c.req.method !== "GET") return next(); const path = c.req.path; if (path.startsWith("/api/") || path.startsWith("/assets/")) return next(); const file = Bun.file(`./dist${path}`); if (await file.exists()) { const stat = await file.stat(); if (stat && !stat.isDirectory()) { return new Response(file); } } return serveStatic({ path: "./dist/index.html" })(c, next); }); } /** * Configure routing for development builds. * * - Boots Vite in middleware mode for transforms. * - Static files from `public/` are served at root paths (matching Vite convention). * - Mirrors production routing semantics so SPA routes behave consistently. */ async function configureDevelopment(app: Hono): Promise { const vite = await createViteServer({ server: { middlewareMode: true, hmr: false, ws: false }, appType: "custom", }); app.use("*", async (c, next) => { if (c.req.path.startsWith("/api/")) return next(); if (c.req.path === "/favicon.ico") return c.redirect("/favicon.svg", 302); const url = c.req.path; try { if (url === "/" || url === "/index.html") { let template = await Bun.file("./index.html").text(); template = await vite.transformIndexHtml(url, template); return c.html(template, { headers: { "Cache-Control": "no-store, must-revalidate" }, }); } const publicFile = Bun.file(`./public${url}`); if (await publicFile.exists()) { const stat = await publicFile.stat(); if (stat && !stat.isDirectory()) { return new Response(publicFile, { headers: { "Cache-Control": "no-store, must-revalidate" }, }); } } let result; try { result = await vite.transformRequest(url); } catch { result = null; } if (result) { return new Response(result.code, { headers: { "Content-Type": "application/javascript", "Cache-Control": "no-store, must-revalidate", }, }); } let template = await Bun.file("./index.html").text(); template = await vite.transformIndexHtml("/", template); return c.html(template, { headers: { "Cache-Control": "no-store, must-revalidate" }, }); } catch (error) { vite.ssrFixStacktrace(error as Error); console.error(error); return c.text("Internal Server Error", 500); } }); return vite; }