feat: vite dev server with HMR for dashboard

This commit is contained in:
Dustin Byrne
2026-03-25 19:06:14 -04:00
parent 98ddf0f3d3
commit 4a7a9041c1
4 changed files with 47 additions and 26 deletions

View File

@@ -25,7 +25,7 @@ export async function runDashboard(port: number, opts: { engine?: boolean; open?
});
// Start the web server with AI merge wired in
const app = createServer(store, { onMerge });
const app = await createServer(store, { onMerge, dev: true });
// Clean shutdown for file watcher when engine is not active
if (!opts.engine) {

View File

@@ -13,16 +13,16 @@
},
"dependencies": {
"@hai/core": "workspace:*",
"@vitejs/plugin-react": "^4.3.0",
"express": "^5.1.0",
"react": "^19.0.0",
"react-dom": "^19.0.0"
"react-dom": "^19.0.0",
"vite": "^6.0.0"
},
"devDependencies": {
"@types/express": "^5.0.0",
"@types/react": "^19.0.0",
"@types/react-dom": "^19.0.0",
"@vitejs/plugin-react": "^4.3.0",
"typescript": "^5.7.0",
"vite": "^6.0.0"
"typescript": "^5.7.0"
}
}

View File

@@ -1,6 +1,7 @@
import express from "express";
import { join, dirname } from "node:path";
import { existsSync } from "node:fs";
import { readFile } from "node:fs/promises";
import { fileURLToPath } from "node:url";
import type { TaskStore, MergeResult } from "@hai/core";
import { createApiRoutes } from "./routes.js";
@@ -13,31 +14,51 @@ export interface ServerOptions {
onMerge?: (taskId: string) => Promise<MergeResult>;
/** Maximum concurrent worktrees / execution slots (default 2) */
maxConcurrent?: number;
/** Enable Vite dev server with HMR (default: false) */
dev?: boolean;
}
export function createServer(store: TaskStore, options?: ServerOptions): ReturnType<typeof express> {
export async function createServer(store: TaskStore, options?: ServerOptions) {
const app = express();
app.use(express.json());
// Serve built React app — check both possible locations
const clientDir = existsSync(join(__dirname, "..", "dist", "client"))
? join(__dirname, "..", "dist", "client")
: existsSync(join(__dirname, "..", "client"))
? join(__dirname, "..", "client")
: join(__dirname, "..", "public"); // fallback to old public dir
app.use(express.static(clientDir));
// SSE endpoint
// SSE endpoint (before Vite middleware so it's not intercepted)
app.get("/api/events", createSSE(store));
// REST API
app.use("/api", createApiRoutes(store, options));
// SPA fallback
app.get("/{*splat}", (_req, res) => {
res.sendFile(join(clientDir, "index.html"));
});
if (options?.dev) {
// Dev mode: Vite middleware with HMR
const { createServer: createViteServer } = await import("vite");
const appDir = join(__dirname, "..", "app");
const vite = await createViteServer({
root: appDir,
server: { middlewareMode: true },
appType: "custom",
});
app.use(vite.middlewares);
// SPA fallback — serve index.html through Vite's transform pipeline
app.use(async (_req, res) => {
const htmlPath = join(appDir, "index.html");
let html = await readFile(htmlPath, "utf-8");
html = await vite.transformIndexHtml(_req.originalUrl, html);
res.status(200).set({ "Content-Type": "text/html" }).end(html);
});
} else {
// Production: serve pre-built static files
const clientDir = existsSync(join(__dirname, "..", "dist", "client"))
? join(__dirname, "..", "dist", "client")
: existsSync(join(__dirname, "..", "client"))
? join(__dirname, "..", "client")
: join(__dirname, "..", "public");
app.use(express.static(clientDir));
app.get("/{*splat}", (_req, res) => {
res.sendFile(join(clientDir, "index.html"));
});
}
return app;
}

12
pnpm-lock.yaml generated
View File

@@ -48,6 +48,9 @@ importers:
'@hai/core':
specifier: workspace:*
version: link:../core
'@vitejs/plugin-react':
specifier: ^4.3.0
version: 4.7.0(vite@6.4.1(@types/node@25.5.0)(tsx@4.21.0)(yaml@2.8.3))
express:
specifier: ^5.1.0
version: 5.2.1
@@ -57,6 +60,9 @@ importers:
react-dom:
specifier: ^19.0.0
version: 19.2.4(react@19.2.4)
vite:
specifier: ^6.0.0
version: 6.4.1(@types/node@25.5.0)(tsx@4.21.0)(yaml@2.8.3)
devDependencies:
'@types/express':
specifier: ^5.0.0
@@ -67,15 +73,9 @@ importers:
'@types/react-dom':
specifier: ^19.0.0
version: 19.2.3(@types/react@19.2.14)
'@vitejs/plugin-react':
specifier: ^4.3.0
version: 4.7.0(vite@6.4.1(@types/node@25.5.0)(tsx@4.21.0)(yaml@2.8.3))
typescript:
specifier: ^5.7.0
version: 5.9.3
vite:
specifier: ^6.0.0
version: 6.4.1(@types/node@25.5.0)(tsx@4.21.0)(yaml@2.8.3)
packages/engine:
dependencies: