refactor(web): convert server to API-only mode

- Remove build process spawning for frontend bundler
- Remove SPA fallback and static file serving
- Return 404 for unknown routes instead of serving index.html
- Keep all REST API endpoints and WebSocket functionality
This commit is contained in:
syntaxbullet
2026-02-08 16:41:47 +01:00
parent 46e95ce7b3
commit 36f9c76fa9

View File

@@ -1,10 +1,10 @@
/**
* Web server factory module.
* Exports a function to create and start the web server.
* API server factory module.
* Exports a function to create and start the API server.
* This allows the server to be started in-process from the main application.
*/
import { serve, spawn, type Subprocess } from "bun";
import { serve } from "bun";
import { join, resolve, dirname } from "path";
import { logger } from "@shared/lib/logger";
@@ -20,37 +20,13 @@ export interface WebServerInstance {
}
/**
* Creates and starts the web server.
*
* Automatically handles building the frontend:
* - In development: Spawns 'bun run build.ts --watch'
* - In production: Assumes 'dist' is already built (or builds once)
* Creates and starts the API server.
*/
export async function createWebServer(config: WebServerConfig = {}): Promise<WebServerInstance> {
const { port = 3000, hostname = "localhost" } = config;
// Resolve directories
// server.ts is in web/src/, so we go up one level to get web/
// Resolve directories for asset serving
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" && process.env.NODE_ENV !== "test";
if (isDev) {
logger.info("web", "Starting Web Bundler in Watch Mode...");
try {
buildProcess = spawn(["bun", "run", "build.ts", "--watch"], {
cwd: webRoot,
stdout: "inherit",
stderr: "inherit",
});
} catch (error) {
logger.error("web", "Failed to start build process", error);
}
}
// Configuration constants
const MAX_CONNECTIONS = 10;
@@ -737,52 +713,8 @@ export async function createWebServer(config: WebServerConfig = {}): Promise<Web
return new Response("Not found", { status: 404 });
}
// Static File Serving
let pathName = url.pathname;
if (pathName === "/") pathName = "/index.html";
// 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()) {
// If serving index.html, inject env vars for frontend
if (pathName === "/index.html") {
const html = await fileRef.text();
return new Response(html, { headers: { "Content-Type": "text/html" } });
}
return new Response(fileRef);
}
// SPA Fallback: Serve index.html for unknown non-file routes
const parts = pathName.split("/");
const lastPart = parts[parts.length - 1];
// If it's a direct request for a missing file (has dot), return 404
// EXCEPT for index.html which is our fallback entry point
if (lastPart?.includes(".") && lastPart !== "index.html") {
// No frontend - return 404 for unknown routes
return new Response("Not Found", { status: 404 });
}
const indexFile = Bun.file(join(distDir, "index.html"));
if (!(await indexFile.exists())) {
if (isDev) {
return new Response("<html><body><h1>🛠️ Dashboard is building...</h1><p>Please refresh in a few seconds. The bundler is currently generating the static assets.</p><script>setTimeout(() => location.reload(), 2000);</script></body></html>", {
status: 503,
headers: { "Content-Type": "text/html" }
});
}
return new Response("Dashboard Not Found", { status: 404 });
}
const indexHtml = await indexFile.text();
return new Response(indexHtml, { headers: { "Content-Type": "text/html" } });
},
websocket: {
@@ -847,7 +779,6 @@ export async function createWebServer(config: WebServerConfig = {}): Promise<Web
idleTimeout: IDLE_TIMEOUT_SECONDS,
},
development: isDev,
});
/**
@@ -947,9 +878,6 @@ export async function createWebServer(config: WebServerConfig = {}): Promise<Web
server,
url,
stop: async () => {
if (buildProcess) {
buildProcess.kill();
}
if (statsBroadcastInterval) {
clearInterval(statsBroadcastInterval);
}