alphamate / server.ts
kulia-moon's picture
Add React build step for alphamate AI OS
aed993d verified
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<ViteDevServer> {
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;
}