From 05f27ca604c2faa586c4ce01c3db19f1b5ee7313 Mon Sep 17 00:00:00 2001 From: syntaxbullet Date: Thu, 8 Jan 2026 17:01:36 +0100 Subject: [PATCH] refactor: fix frontend --- web/src/index.html | 1 + web/src/server.ts | 98 ++++++++++++++++++++++++++++++++-------------- 2 files changed, 69 insertions(+), 30 deletions(-) diff --git a/web/src/index.html b/web/src/index.html index 5b72de0..6f502de 100644 --- a/web/src/index.html +++ b/web/src/index.html @@ -10,6 +10,7 @@
+ \ No newline at end of file diff --git a/web/src/server.ts b/web/src/server.ts index 7071d46..43633dd 100644 --- a/web/src/server.ts +++ b/web/src/server.ts @@ -4,7 +4,8 @@ * This allows the server to be started in-process from the main application. */ -import { serve } from "bun"; +import { serve, spawn, type Subprocess } from "bun"; +import { join, resolve, dirname } from "path"; export interface WebServerConfig { port?: number; @@ -20,32 +21,78 @@ export interface WebServerInstance { /** * Creates and starts the web server. * - * IMPORTANT: This function must be called from within the web project directory - * or the bundler won't resolve paths correctly. Use `startWebServerFromRoot` - * if calling from the main application. + * Automatically handles building the frontend: + * - In development: Spawns 'bun run build.ts --watch' + * - In production: Assumes 'dist' is already built (or builds once) */ export async function createWebServer(config: WebServerConfig = {}): Promise { const { port = 3000, hostname = "localhost" } = config; - // Dynamic import of the HTML to ensure bundler context is correct - const index = await import("./index.html"); + // Resolve directories + // server.ts is in web/src/, so we go up one level to get web/ + const currentDir = dirname(new URL(import.meta.url).pathname); + const webRoot = resolve(currentDir, ".."); + const distDir = join(webRoot, "dist"); + + // Manage build process in development + let buildProcess: Subprocess | undefined; + const isDev = process.env.NODE_ENV !== "production"; + + if (isDev) { + console.log("🛠️ Starting Web Bundler in Watch Mode..."); + try { + buildProcess = spawn(["bun", "run", "build.ts", "--watch"], { + cwd: webRoot, + stdout: "inherit", + stderr: "inherit", + }); + } catch (error) { + console.error("Failed to start build process:", error); + } + } const server = serve({ port, hostname, - routes: { - // Serve index.html for all unmatched routes (SPA catch-all) - "/*": index.default, + async fetch(req) { + const url = new URL(req.url); + // API routes + if (url.pathname === "/api/health") { + return Response.json({ status: "ok", timestamp: Date.now() }); + } + // Static File Serving + let pathName = url.pathname; + if (pathName === "/") pathName = "/index.html"; - "/api/health": () => Response.json({ status: "ok", timestamp: Date.now() }), + // Construct safe file path + // Remove leading slash to join correctly + const safePath = join(distDir, pathName.replace(/^\//, "")); + + // Security check: ensure path is within distDir + if (!safePath.startsWith(distDir)) { + return new Response("Forbidden", { status: 403 }); + } + + const fileRef = Bun.file(safePath); + if (await fileRef.exists()) { + return new Response(fileRef); + } + + // SPA Fallback: Serve index.html for unknown non-file routes + // If the path looks like a file (has extension), return 404 + // Otherwise serve index.html + const parts = pathName.split("/"); + const lastPart = parts[parts.length - 1]; + if (lastPart?.includes(".")) { + return new Response("Not Found", { status: 404 }); + } + + return new Response(Bun.file(join(distDir, "index.html"))); }, - development: process.env.NODE_ENV !== "production" && { - hmr: true, - console: true, - }, + development: isDev, }); const url = `http://${hostname}:${port}`; @@ -54,6 +101,9 @@ export async function createWebServer(config: WebServerConfig = {}): Promise { + if (buildProcess) { + buildProcess.kill(); + } server.stop(true); }, }; @@ -61,25 +111,13 @@ export async function createWebServer(config: WebServerConfig = {}): Promise { - const originalCwd = process.cwd(); - - try { - // Change to web project directory for correct bundler resolution - process.chdir(webProjectPath); - - const instance = await createWebServer(config); - - console.log(`🌐 Web server running at ${instance.url}`); - - return instance; - } finally { - // Restore original working directory - process.chdir(originalCwd); - } + // Current implementation doesn't need CWD switching thanks to absolute path resolution + return createWebServer(config); }