summaryrefslogtreecommitdiffhomepage
path: root/packages
diff options
context:
space:
mode:
Diffstat (limited to 'packages')
-rw-r--r--packages/frontend/package.json1
-rw-r--r--packages/frontend/serve.ts66
2 files changed, 67 insertions, 0 deletions
diff --git a/packages/frontend/package.json b/packages/frontend/package.json
index 453df07..1d11524 100644
--- a/packages/frontend/package.json
+++ b/packages/frontend/package.json
@@ -8,6 +8,7 @@
"dev": "vite",
"build": "vite build",
"preview": "vite preview",
+ "serve": "bun serve.ts",
"test": "vitest run",
"test:watch": "vitest",
"typecheck": "svelte-check --tsconfig ./tsconfig.json",
diff --git a/packages/frontend/serve.ts b/packages/frontend/serve.ts
new file mode 100644
index 0000000..75b0a9f
--- /dev/null
+++ b/packages/frontend/serve.ts
@@ -0,0 +1,66 @@
+// Static file server for the built frontend (packages/frontend/dist).
+// Uses Bun's built-in HTTP server — no extra runtime dependencies.
+//
+// Environment variables:
+// PORT — port to listen on (default: 18391)
+// HOST — interface to bind to (default: 0.0.0.0)
+// DIST_DIR — absolute path to the static assets directory
+// (default: <this-file's-dir>/dist)
+//
+// SPA fallback: requests that don't match a file on disk fall back to
+// index.html so client-side routing works.
+
+import { existsSync, statSync } from "node:fs";
+import { join, normalize, resolve } from "node:path";
+import { fileURLToPath } from "node:url";
+
+const moduleDir = (() => {
+ try {
+ // @ts-expect-error — Bun provides import.meta.dir
+ return import.meta.dir as string;
+ } catch {
+ return resolve(fileURLToPath(import.meta.url), "..");
+ }
+})();
+
+const port = Number(process.env.PORT) || 18391;
+const host = process.env.HOST || "0.0.0.0";
+const distDir = resolve(process.env.DIST_DIR || join(moduleDir, "dist"));
+
+if (!existsSync(distDir) || !statSync(distDir).isDirectory()) {
+ console.error(`[dispatch-frontend] dist directory not found: ${distDir}`);
+ console.error("[dispatch-frontend] Build the frontend first (bun run --cwd packages/frontend build)");
+ process.exit(1);
+}
+
+const indexPath = join(distDir, "index.html");
+
+function safeResolve(urlPath: string): string | null {
+ // Normalize and prevent path-traversal outside distDir.
+ const decoded = decodeURIComponent(urlPath);
+ const candidate = resolve(distDir, "." + normalize(decoded));
+ if (!candidate.startsWith(distDir)) return null;
+ return candidate;
+}
+
+Bun.serve({
+ port,
+ hostname: host,
+ async fetch(req) {
+ const url = new URL(req.url);
+ let pathname = url.pathname;
+ if (pathname === "/" || pathname === "") pathname = "/index.html";
+
+ const resolved = safeResolve(pathname);
+ if (resolved && existsSync(resolved) && statSync(resolved).isFile()) {
+ return new Response(Bun.file(resolved));
+ }
+
+ // SPA fallback — serve index.html for unknown routes
+ return new Response(Bun.file(indexPath), {
+ headers: { "content-type": "text/html; charset=utf-8" },
+ });
+ },
+});
+
+console.log(`[dispatch-frontend] serving ${distDir} on http://${host}:${port}`);