fix: properly bundle and cache frontend assets

- Build frontend assets natively with Bun to dist/public/ via `src/index.html` entrypoint
- Serve static assets in production with `Cache-Control: public, max-age=31536000, immutable` caching headers
- Preserve native `bun run dev` routing for seamless HMR by retaining development mode conditions in the server router
- Exclude `index.html` from long-term cache headers to allow for production updates
This commit is contained in:
ruinivist
2026-05-29 18:14:25 +00:00
parent 9b44356782
commit 17d01aff21
2 changed files with 39 additions and 3 deletions
+1 -1
View File
@@ -4,7 +4,7 @@
"private": true, "private": true,
"scripts": { "scripts": {
"dev": "bun --hot src/server.tsx", "dev": "bun --hot src/server.tsx",
"build": "rm -rf dist && bun build --target=bun ./src/server.tsx --outdir ./dist", "build": "rm -rf dist && bun build --minify --target=browser ./src/index.html --outdir ./dist/public && bun build --target=bun ./src/server.tsx --outdir ./dist",
"start": "cd dist && DATABASE_PATH=../data/excalidraw.sqlite bun ./server.js", "start": "cd dist && DATABASE_PATH=../data/excalidraw.sqlite bun ./server.js",
"test": "bun test", "test": "bun test",
"typecheck": "tsc --noEmit" "typecheck": "tsc --noEmit"
+38 -2
View File
@@ -14,7 +14,16 @@ export function createServer() {
const drawing = drawingStore.ensureInitialDrawing(); const drawing = drawingStore.ensureInitialDrawing();
return Response.redirect(new URL(`/d/${drawing.id}`, request.url), 302); return Response.redirect(new URL(`/d/${drawing.id}`, request.url), 302);
}, },
"/d/:id": index, ...(process.cwd().endsWith("/dist") ? {
"/d/:id": async (req: Request) => {
let file = Bun.file("./public/index.html");
return new Response(file, {
headers: { "Content-Type": "text/html; charset=utf-8" }
});
}
} : {
"/d/:id": index
}),
"/api/health": { "/api/health": {
GET: () => api.health(), GET: () => api.health(),
}, },
@@ -28,7 +37,34 @@ export function createServer() {
DELETE: (request: Request & { params: Record<string, string> }) => api.deleteDrawing(request), DELETE: (request: Request & { params: Record<string, string> }) => api.deleteDrawing(request),
}, },
}, },
fetch() { async fetch(request: Request) {
const url = new URL(request.url);
const isProd = process.cwd().endsWith("/dist");
if (!isProd) {
return Response.json({ ok: false, error: "Not found" }, { status: 404 });
}
// Remove any "/public" prefix from the request URL if it accidentally got included
let reqPath = url.pathname;
if (reqPath.startsWith("/public/")) {
reqPath = reqPath.substring(7);
}
// In dist, static files are inside public
const path = "./public" + reqPath;
const file = Bun.file(path);
if (await file.exists()) {
// Cache indefinitely except index.html
const headers: Record<string, string> = {};
if (!reqPath.endsWith("index.html")) {
headers["Cache-Control"] = "public, max-age=31536000, immutable";
} else {
headers["Content-Type"] = "text/html; charset=utf-8";
}
return new Response(file, { headers });
}
return Response.json({ ok: false, error: "Not found" }, { status: 404 }); return Response.json({ ok: false, error: "Not found" }, { status: 404 });
}, },
} satisfies Parameters<typeof Bun.serve>[0]; } satisfies Parameters<typeof Bun.serve>[0];