/// import fs from 'node:fs' import path from 'node:path' import { env } from './lib/env' const isProduction = env.NODE_ENV === 'production' // ─── Route Classification ────────────────────────────── const API_PREFIXES = ['/api/', '/webhook/', '/ws/', '/health', '/docs'] function isApiRoute(pathname: string): boolean { return API_PREFIXES.some((p) => pathname.startsWith(p)) || pathname === '/health' } // ─── Vite Dev Server (dev only) ──────────────────────── let vite: Awaited> | null = null if (!isProduction) { const { createVite } = await import('./vite') vite = await createVite() } // ─── Frontend Serving ────────────────────────────────── async function serveFrontend(request: Request): Promise { const url = new URL(request.url) const pathname = url.pathname if (!isProduction && vite) { // === DEVELOPMENT: Vite Middleware Mode === // SPA route → serve index.html via Vite transform if ( pathname === '/' || (!pathname.includes('.') && !pathname.startsWith('/@') && !pathname.startsWith('/__open-stack-frame-in-editor')) ) { const htmlPath = path.resolve('index.html') let htmlContent = fs.readFileSync(htmlPath, 'utf-8') htmlContent = await vite.transformIndexHtml(pathname, htmlContent) // Dedupe: Vite 8 middlewareMode injects react-refresh preamble twice const preamble = '' const firstIdx = htmlContent.indexOf(preamble) if (firstIdx !== -1) { const secondIdx = htmlContent.indexOf(preamble, firstIdx + preamble.length) if (secondIdx !== -1) { htmlContent = htmlContent.slice(0, secondIdx) + htmlContent.slice(secondIdx + preamble.length) } } return new Response(htmlContent, { headers: { 'Content-Type': 'text/html' }, }) } // Asset/module requests → proxy ke Vite middleware // Bridge: Bun Request → Node.js IncomingMessage/ServerResponse return new Promise((resolve) => { const req = new Proxy(request, { get(target, prop) { if (prop === 'url') return pathname + url.search if (prop === 'method') return request.method if (prop === 'headers') return Object.fromEntries(request.headers as any) return (target as any)[prop] }, }) as any const chunks: (Buffer | Uint8Array)[] = [] const res = { statusCode: 200, headers: {} as Record, setHeader(name: string, value: string) { this.headers[name.toLowerCase()] = value; return this }, getHeader(name: string) { return this.headers[name.toLowerCase()] }, removeHeader(name: string) { delete this.headers[name.toLowerCase()] }, writeHead(code: number, reasonOrHeaders?: string | Record, maybeHeaders?: Record) { this.statusCode = code const hdrs = typeof reasonOrHeaders === 'object' ? reasonOrHeaders : maybeHeaders if (hdrs) for (const [k, v] of Object.entries(hdrs)) this.headers[k.toLowerCase()] = String(v) return this }, write(chunk: any) { if (chunk) chunks.push(typeof chunk === 'string' ? Buffer.from(chunk) : chunk) return true }, end(data?: any) { if (data) { if (typeof data === 'string') chunks.push(Buffer.from(data)) else if (data instanceof Uint8Array || Buffer.isBuffer(data)) chunks.push(data) } resolve(new Response( chunks.length > 0 ? Buffer.concat(chunks) : null, { status: this.statusCode, headers: this.headers }, )) }, once() { return this }, on() { return this }, emit() { return this }, removeListener() { return this }, } as any vite.middlewares(req, res, (err: any) => { if (err) { resolve(new Response(err.stack || err.toString(), { status: 500 })) return } resolve(new Response('Not Found', { status: 404 })) }) }) } // === PRODUCTION: Static Files + SPA Fallback === const filePath = path.join('dist', pathname === '/' ? 'index.html' : pathname) if (fs.existsSync(filePath) && fs.statSync(filePath).isFile()) { const ext = path.extname(filePath) const contentType: Record = { '.js': 'application/javascript', '.css': 'text/css', '.html': 'text/html; charset=utf-8', '.json': 'application/json', '.svg': 'image/svg+xml', '.png': 'image/png', '.ico': 'image/x-icon', } const isHashed = pathname.startsWith('/assets/') return new Response(Bun.file(filePath), { headers: { 'Content-Type': contentType[ext] ?? 'application/octet-stream', 'Cache-Control': isHashed ? 'public, max-age=31536000, immutable' : 'public, max-age=3600', }, }) } // SPA fallback — semua route yang tidak match file → index.html const indexHtml = path.join('dist', 'index.html') if (fs.existsSync(indexHtml)) { return new Response(Bun.file(indexHtml), { headers: { 'Content-Type': 'text/html; charset=utf-8', 'Cache-Control': 'no-cache' }, }) } return new Response('Not Found', { status: 404 }) } // ─── Elysia App ──────────────────────────────────────── import { createApp } from './app' const app = createApp() // Frontend intercept — onRequest jalan SEBELUM route matching .onRequest(async ({ request }) => { const pathname = new URL(request.url).pathname // Dev inspector: open file di editor if (!isProduction && pathname === '/__open-in-editor' && request.method === 'POST') { const { relativePath, lineNumber, columnNumber } = (await request.json()) as { relativePath: string; lineNumber: string; columnNumber: string } const file = `${process.cwd()}/${relativePath}` const editor = env.REACT_EDITOR const loc = `${file}:${lineNumber}:${columnNumber}` // zed & subl: editor file:line:col — code & cursor: editor --goto file:line:col const noGotoEditors = ['subl', 'zed'] const args = noGotoEditors.includes(editor) ? [loc] : ['--goto', loc] const editorPath = Bun.which(editor) if (editorPath) Bun.spawn([editor, ...args], { stdio: ['ignore', 'ignore', 'ignore'] }) return new Response('ok') } // Non-API route → serve frontend if (!isApiRoute(pathname)) { return serveFrontend(request) } // undefined → lanjut ke Elysia route matching }) .listen(env.PORT) console.log(`Server running at http://localhost:${app.server!.port}`)