UNPKG

@mements/serve

Version:

A lightweight, type-safe server framework for Bun with automatic performance tracking.

491 lines (430 loc) 15.1 kB
import { build, type BuildConfig } from "bun"; import plugin from "bun-plugin-tailwind"; import { existsSync } from "fs"; import { rm } from "fs/promises"; import path from "path"; import { randomUUID } from "crypto"; import { readdir } from "node:fs/promises"; export type ApiEndpoint = { method: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH'; path: string; body?: any; query?: Record<string, string | number | boolean>; response: any; }; export type GetHealthEndpoint = { method: 'GET'; path: '/api/health'; query?: undefined; response: { status: string }; }; export type ApiEndpoints = Record<string, ApiEndpoint>; type MeasureContext = { requestId?: string; level?: number; parentAction?: string; }; export type MeasureFn = <T>( fn: (measure: MeasureFn) => Promise<T>, action: string, context?: MeasureContext ) => Promise<T>; export async function measure<T>( fn: (measure: typeof measure) => Promise<T>, action: string, context: MeasureContext = {} ): Promise<T> { const start = performance.now(); const level = context.level || 0; let indent = "=".repeat(level); const requestId = context.requestId; let logPrefix = requestId ? `[${requestId}] ${indent}>` : `${indent}>`; try { indent = ">".repeat(level); logPrefix = requestId ? `[${requestId}] ${indent}$` : `${indent}$`; console.log(`${logPrefix} ${action}...`); const result = await fn((nestedFn, nestedAction) => measure(nestedFn, nestedAction, { requestId: requestId ? `${requestId}` : undefined, level: level + 1, parentAction: action }) ); const duration = performance.now() - start; indent = "<".repeat(level); logPrefix = requestId ? `[${requestId}] ${indent}$` : `${indent}$`; console.log(`${logPrefix} ${action}${duration.toFixed(2)}ms`); return result; } catch (error) { const duration = performance.now() - start; console.log('==========================='); console.log(`\n${logPrefix} ${action}${duration.toFixed(2)}ms`, error); console.log('==========================='); throw new Error(`${action} failed: ${error}`); } } interface PageConfig { route: string; target: string; handler: (ctx: { requestId: string; measure: MeasureFn }) => Promise<any>; } interface ImportConfig { name: string; version?: string; deps?: string[]; } export interface ServeOptions { pages: PageConfig[]; api?: Record<string, (req: Request) => Promise<Response>>; imports: ImportConfig[]; } type RouteMapping = Record<string, string>; type ImportMap = { imports: Record<string, string> }; type EntrypointConfig = { path: string; serverData?: (ctx: MiddlewareContext & { requestId: string; measure: MeasureFn }) => Promise<any>; }; const filePathCache: Record<string, string> = {}; const getHeaders = (ext: string) => { const contentTypes: Record<string, string> = { ".html": "text/html", ".js": "text/javascript", ".css": "text/css", ".png": "image/png", ".jpg": "image/jpeg", ".svg": "image/svg+xml", }; return { headers: { "Content-Type": contentTypes[ext] || "application/octet-stream", }, }; }; async function servePage( response: Response, importMap: ImportMap, serverData = {}, requestId: string ): Promise<Response> { return await measure( async (measure) => { const rewriter = new HTMLRewriter() .on("head", { element(element) { element.prepend( `<script type="importmap">${JSON.stringify(importMap)}</script>`, { html: true } ); }, }) .on("body", { element(element) { const data = { ...serverData, requestId }; element.append( `<script>window.serverData = ${JSON.stringify(data)}</script>`, { html: true } ); }, }); const transformedResponse = rewriter.transform(response); const transformedHtml = await transformedResponse.text(); return new Response(transformedHtml, getHeaders(".html")); }, "Transform page", { requestId, level: 2 } ); } export async function serve(config: ServeOptions) { const isDev = process.env.NODE_ENV !== "production"; const outdir = "./dist"; const assetsDir = "./assets"; const routeMap: RouteMapping = {}; const entrypoints: Record<string, EntrypointConfig> = {}; const pageHandlers: Record<string, (req: Request) => Promise<any>> = {}; await measure(async (measure) => { for (const page of config.pages) { let targetPath: string; if (page.target.startsWith("./")) { targetPath = page.target; } else { targetPath = `./pages/${page.target}/${page.target}.html`; } const relativePath = targetPath.startsWith("./") ? targetPath.substring(2) : targetPath; routeMap[page.route] = relativePath; entrypoints[page.route] = { path: targetPath, serverData: page.handler }; pageHandlers[page.route] = async (req) => { const url = new URL(req.url); const query = Object.fromEntries(url.searchParams); const requestId = req.headers.get("X-Request-ID") || randomUUID().split('-')[0]; const ctx: { requestId: string; measure: MeasureFn } = { request: req, method: req.method, path: url.pathname, query, body: req.method !== "GET" ? await req.json().catch(() => ({})) : undefined, headers: req.headers, requestId, measure, }; return await page.handler(ctx); }; } }, "Initialize routes"); const importMap: ImportMap = { imports: {} }; const versionMap: Record<string, string> = {}; for (const imp of config.imports) { if (imp.name.startsWith("@")) { versionMap[imp.name] = imp.version ?? "latest"; } else { const baseName = imp.name.split("/")[0]; versionMap[baseName] = imp.version ?? "latest"; } } for (const imp of config.imports) { let url: string; if (imp.name.startsWith("@")) { url = `https://esm.sh/${imp.name}@${versionMap[imp.name]}`; } else { const parts = imp.name.split("/"); const baseName = parts[0]; const subPaths = parts.slice(1); url = `https://esm.sh/${baseName}@${versionMap[baseName]}`; if (subPaths.length > 0) url += `/${subPaths.join("/")}`; } let queryParts: string[] = []; if (imp?.deps?.length) { const depsList = imp.deps .map((dep) => `${dep}@${versionMap[dep.split("/")[0]]}`) .join(","); queryParts.push(`deps=${depsList}`); } if (isDev) queryParts.push("dev"); if (queryParts.length) url += `?${queryParts.join("&")}`; importMap.imports[imp.name] = url; } let serverPort = -1; const buildConfig: BuildConfig = { entrypoints: Object.values(entrypoints).map((e) => e.path), outdir, plugins: [plugin], minify: !isDev, target: "browser", sourcemap: "linked", packages: "external", external: Object.keys(importMap.imports), define: { "process.env.NODE_ENV": JSON.stringify(isDev ? "development" : "production"), "process.env.HOST": isDev ? `http://localhost:${serverPort}` : "https://mements.ai", }, naming: { chunk: "[name].[hash].[ext]", entry: "[name].[hash].[ext]", }, }; if (existsSync(outdir)) { await rm(outdir, { recursive: true, force: true }); } async function rebuildPage(pagePath: string, requestId: string): Promise<any> { if (isDev) { const baseName = path.basename(pagePath).split(".")[0].toLowerCase(); const entrypoint = Object.values(entrypoints).find((e) => path.basename(e.path).split(".")[0].toLowerCase() === baseName ); if (!entrypoint) return null; return await measure( async (measure) => { try { const result = await build({ ...buildConfig, entrypoints: [entrypoint.path] }); if (result && result.outputs) { for (const output of result.outputs) { const outputBaseName = path.basename(output.path).split(".")[0].toLowerCase(); const ext = path.extname(output.path); filePathCache[`${outputBaseName}${ext}`] = output.path; } } return result; } catch (error) { console.error("Failed to rebuild page:", error); return null; } }, `Rebuild ${baseName}`, { requestId } ); } return null; } const server = Bun.serve({ port: process.env.BUN_PORT, development: isDev, async fetch(req) { const requestId = randomUUID().split("-")[0]; const newHeaders = new Headers(req.headers); if (!newHeaders.has("X-Request-ID")) { newHeaders.append("X-Request-ID", requestId); } const reqWithId = new Request(req, { headers: newHeaders }); return await measure( async (measure) => { const url = new URL(reqWithId.url); const pathname = url.pathname; const distPath = path.join(process.cwd(), outdir, pathname); if (await Bun.file(distPath).exists()) { return new Response(Bun.file(distPath), getHeaders(path.extname(pathname))); } const assetsPath = path.join(process.cwd(), assetsDir, pathname); if (await Bun.file(assetsPath).exists()) { return new Response(Bun.file(assetsPath), getHeaders(path.extname(pathname))); } if (pageHandlers[pathname]) { const routeFile = routeMap[pathname]; return await measure( async (measure) => { const ext = path.extname(routeFile); const baseName = path.basename(routeFile, ext); let htmlFile: any = null; if (isDev) { // In dev mode, always rebuild the page const buildResult = await rebuildPage(routeFile, requestId); if (buildResult) { const builtFile = buildResult.outputs.find((o: { path: string }) => o.path.endsWith(ext) ); if (builtFile) { htmlFile = Bun.file(builtFile.path); } } } else { // In production mode, check the cache first const cacheKey = `${baseName.toLowerCase()}${ext}`; if (filePathCache[cacheKey] && await Bun.file(filePathCache[cacheKey]).exists()) { htmlFile = Bun.file(filePathCache[cacheKey]); } // If not in cache or file doesn't exist, rebuild if (!htmlFile) { const buildResult = await rebuildPage(routeFile, requestId); if (buildResult) { const builtFile = buildResult.outputs.find((o: { path: string }) => o.path.endsWith(ext) ); if (builtFile) { htmlFile = Bun.file(builtFile.path); } } } } if (!htmlFile) { throw new Error(`Page not found: ${routeFile}`); } let serverData = {}; const handler = pageHandlers[pathname]; if (handler) { serverData = await measure( async (measure) => handler(reqWithId), `serverData ${pathname}` ); } return await servePage( new Response(htmlFile, getHeaders(ext)), importMap, serverData, requestId ); }, `page ${pathname}` ); } if (config.api && pathname in config.api) { return await measure( async (measure) => config.api![pathname](reqWithId), `endpoint ${pathname}` ); } return new Response("Route Not Found", { status: 404 }); }, `${req.method} ${req.url}`, { requestId } ); }, error(error) { console.error("Server Error:", error); return new Response( `<pre>${error}\n${error.stack}</pre>`, { headers: { "Content-Type": "text/html" }, status: 500 } ); }, }); serverPort = server.port; await measure(async () => { const result = await build(buildConfig); if (result && result.outputs) { for (const output of result.outputs) { const outputBaseName = path.basename(output.path).split(".")[0].toLowerCase(); const ext = path.extname(output.path); filePathCache[`${outputBaseName}${ext}`] = output.path; } } }, "Initial build"); return server; } if (require.main === module) { const exampleConfig: ServeOptions = { pages: [ { route: "/", target: "./pages/index.tsx", handler: async (ctx) => ({ message: `Hello from ${ctx.path}` }), }, ], api: { "/api/health": async (req) => new Response(JSON.stringify({ status: "ok" }), { headers: { "Content-Type": "application/json" }, }), }, imports: [ { name: "react", version: "18.2.0" }, { name: "react-dom/client", version: "18.2.0" }, ], }; serve(exampleConfig); } export function generateImports(packageJson: any) { const dependencies = { ...packageJson.dependencies || {}, ...packageJson.devDependencies || {} }; const knownDependencies: Record<string, string[]> = { "sonner": ["react", "react-dom"], "wouter": ["react"], "framer-motion": ["react"], }; const imports = Object.entries(dependencies) .filter(([name]) => { const backendDependencies = ["@types/", "typescript", "@mements/serve", "zod"]; return !backendDependencies.some(dep => name.startsWith(dep)); }) .map(([name, version]) => { if (name === "react-dom") { return [ { name, version: (version as string).replace(/^\^/, '') }, { name: "react-dom/client", version: (version as string).replace(/^\^/, ''), deps: [] }, { name: "react/jsx-dev-runtime", version: packageJson.dependencies.react.replace(/^\^/, ''), deps: [] }, { name: "react/jsx-runtime", version: packageJson.dependencies.react.replace(/^\^/, ''), deps: [] } ]; } if (name === "react") { return { name, version: (version as string).replace(/^\^/, ''), deps: [] }; } if (knownDependencies[name]) { return { name, version: (version as string).replace(/^\^/, ''), deps: knownDependencies[name] }; } return { name }; }) .flat(); return imports; }