UNPKG

create-bun-stack

Version:

Rails-inspired fullstack application generator for Bun

323 lines (292 loc) 11.4 kB
import { env } from "../config/env"; import { requireAdmin } from "./middleware/auth"; import { applyCorsHeaders, handleCorsPreflightRequest } from "./middleware/cors"; import { applyCsrfProtection } from "./middleware/csrf"; import { applySecurityHeaders } from "./middleware/security-headers"; import * as routes from "./routes"; const PORT = Number(env.PORT); // For development, we'll serve the React app dynamically const isDevelopment = env.NODE_ENV !== "production" || process.env.DEV_MODE === "true"; // Track server start time for development hot reload detection const serverStartTime = Date.now(); const appServer = Bun.serve({ port: PORT, async fetch(req) { const url = new URL(req.url); const path = url.pathname; const origin = req.headers.get("Origin"); // Helper to apply all security middleware const wrapResponse = (response: Response): Response => { let finalResponse = response; // Apply CORS headers for API routes if (path.startsWith("/api")) { finalResponse = applyCorsHeaders(finalResponse, origin); } // In development, add server start time header for hot reload detection if (isDevelopment && path.startsWith("/api")) { finalResponse = new Response(finalResponse.body, { status: finalResponse.status, statusText: finalResponse.statusText, headers: new Headers(finalResponse.headers), }); finalResponse.headers.set("X-Server-Start-Time", serverStartTime.toString()); } // Apply security headers to all responses return applySecurityHeaders(finalResponse, req); }; // Handle CORS preflight requests for API routes if (path.startsWith("/api")) { const preflightResponse = handleCorsPreflightRequest(req); if (preflightResponse) return wrapResponse(preflightResponse); // Apply CSRF protection const csrfError = applyCsrfProtection(req); if (csrfError) return wrapResponse(csrfError); } // Serve static files if (path === "/" || !path.startsWith("/api")) { if (isDevelopment) { // In development, serve from public directory if (path === "/") { // Serve index.html const htmlFile = Bun.file("./index.html"); if (await htmlFile.exists()) { let html = await htmlFile.text(); // Inject HMR client script in development const hmrScript = ` <script> // HMR Client let lastTimestamp = null; async function checkForUpdates() { try { const file = await fetch('/.hmr-timestamp'); const timestamp = await file.text(); if (lastTimestamp && lastTimestamp !== timestamp) { console.log('🔄 Reloading due to changes...'); window.location.reload(); } lastTimestamp = timestamp; } catch (e) { // Ignore errors } } setInterval(checkForUpdates, 100); checkForUpdates(); </script> `; html = html.replace("</body>", `${hmrScript}</body>`); return wrapResponse( new Response(html, { headers: { "Content-Type": "text/html" }, }) ); } } // Serve HMR timestamp file if (path === "/.hmr-timestamp") { const timestampFile = Bun.file("./.hmr-timestamp"); if (await timestampFile.exists()) { return wrapResponse( new Response(timestampFile, { headers: { "Content-Type": "text/plain", "Cache-Control": "no-cache, no-store, must-revalidate", }, }) ); } } // Serve from public directory const publicPath = `./public${path}`; const file = Bun.file(publicPath); if (await file.exists()) { // Determine content type based on file extension let contentType = "text/plain"; const filePath = path === "/" ? "/index.html" : path; if (filePath.endsWith(".html")) contentType = "text/html"; else if (filePath.endsWith(".js")) contentType = "application/javascript"; else if (filePath.endsWith(".css")) contentType = "text/css"; else if (filePath.endsWith(".json")) contentType = "application/json"; else if (filePath.endsWith(".ico")) contentType = "image/x-icon"; else if (filePath.endsWith(".png")) contentType = "image/png"; else if (filePath.endsWith(".jpg") || filePath.endsWith(".jpeg")) contentType = "image/jpeg"; else if (filePath.endsWith(".svg")) contentType = "image/svg+xml"; return wrapResponse( new Response(file, { headers: { "Content-Type": contentType }, }) ); } } else { // In production, serve from public directory const publicPath = `./public${path === "/" ? "/index.html" : path}`; const file = Bun.file(publicPath); if (await file.exists()) { // Determine content type based on file extension let contentType = "text/plain"; const filePath = path === "/" ? "/index.html" : path; if (filePath.endsWith(".html")) contentType = "text/html"; else if (filePath.endsWith(".js")) contentType = "application/javascript"; else if (filePath.endsWith(".css")) contentType = "text/css"; else if (filePath.endsWith(".json")) contentType = "application/json"; else if (filePath.endsWith(".ico")) contentType = "image/x-icon"; else if (filePath.endsWith(".png")) contentType = "image/png"; else if (filePath.endsWith(".jpg") || filePath.endsWith(".jpeg")) contentType = "image/jpeg"; else if (filePath.endsWith(".svg")) contentType = "image/svg+xml"; return wrapResponse( new Response(file, { headers: { "Content-Type": contentType }, }) ); } } } // API Routes if (path === "/api/health") { let dbStatus = "unknown"; let dbResponseTime = 0; try { const start = Date.now(); // Simple query to check database connectivity const { executeSimpleQuery } = await import("../db/client"); await executeSimpleQuery("SELECT 1 as test"); dbResponseTime = Date.now() - start; dbStatus = "connected"; } catch (error) { dbStatus = "disconnected"; console.error("Database health check failed:", error); } const response = Response.json({ status: "ok", timestamp: new Date(), database: { status: dbStatus, responseTime: dbResponseTime, }, version: process.env.npm_package_version || "unknown", environment: env.NODE_ENV, }); return wrapResponse(response); } if (path.startsWith("/api/users")) { const handlers = routes.users; let response: Response | undefined; if (req.method === "GET" && path === "/api/users") { // Apply admin middleware for GET /api/users const adminCheck = requireAdmin(req); if (adminCheck) { response = adminCheck; } else { response = await handlers.GET(req); } } if (req.method === "POST" && path === "/api/users") { response = await handlers.POST(req); } const match = path.match(/^\/api\/users\/(.+)$/); if (match?.[1]) { const reqWithParams = Object.assign(req, { params: { id: match[1] }, }); if (req.method === "GET") { response = await handlers["/:id"].GET(reqWithParams); } if (req.method === "PUT") { // Apply admin middleware for user updates const adminCheck = requireAdmin(req); if (adminCheck) { response = adminCheck; } else { response = await handlers["/:id"].PUT(reqWithParams); } } if (req.method === "DELETE") { // Apply admin middleware for user deletion const adminCheck = requireAdmin(req); if (adminCheck) { response = adminCheck; } else { response = await handlers["/:id"].DELETE(reqWithParams); } } } if (response) { return wrapResponse(response); } } if (path.startsWith("/api/auth")) { const handlers = routes.auth; let response: Response | undefined; if (path === "/api/auth/login" && req.method === "POST") { response = await handlers["/login"].POST(req); } if (path === "/api/auth/register" && req.method === "POST") { response = await handlers["/register"].POST(req); } if (path === "/api/auth/logout" && req.method === "POST") { response = await handlers["/logout"].POST(req); } if (response) { return wrapResponse(response); } } // Catch-all for SPA - return index.html for client-side routing if (isDevelopment) { // In development, serve index.html from root const indexFile = Bun.file("./index.html"); if (await indexFile.exists()) { let html = await indexFile.text(); // Inject HMR client script const hmrScript = ` <script> // HMR Client let lastTimestamp = null; async function checkForUpdates() { try { const file = await fetch('/.hmr-timestamp'); const timestamp = await file.text(); if (lastTimestamp && lastTimestamp !== timestamp) { console.log('🔄 Reloading due to changes...'); window.location.reload(); } lastTimestamp = timestamp; } catch (e) { // Ignore errors } } setInterval(checkForUpdates, 100); checkForUpdates(); </script> `; html = html.replace("</body>", `${hmrScript}</body>`); return wrapResponse( new Response(html, { headers: { "Content-Type": "text/html" }, }) ); } } else { // In production, serve from public const publicFile = Bun.file("./public/index.html"); if (await publicFile.exists()) { const html = await publicFile.text(); return wrapResponse( new Response(html, { headers: { "Content-Type": "text/html" }, }) ); } } // If nothing matched, return 404 return wrapResponse(new Response("Not Found", { status: 404 })); }, error(error) { console.error(error); return new Response("Internal Server Error", { status: 500 }); }, }); console.log(`🚀 Server running at http://localhost:${PORT}`); console.log(`🔄 Server started at: ${new Date().toISOString()}`); if (isDevelopment) { console.log("💡 Development mode with hot module replacement enabled"); } export { appServer as server };