refactor(web): move website into turbo app workspace
This commit is contained in:
parent
2664804e91
commit
b4e626a868
66 changed files with 318 additions and 298 deletions
25
apps/web/src/server/dev.test.ts
Normal file
25
apps/web/src/server/dev.test.ts
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
import { readFileSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import { describe, expect, test } from "vitest";
|
||||
import { createDevServerConfig } from "../../vite.config";
|
||||
|
||||
describe("local development runtime", () => {
|
||||
test("starts the Bun API server together with the Vite dev server", () => {
|
||||
const packageJson = JSON.parse(
|
||||
readFileSync(join(process.cwd(), "package.json"), "utf8"),
|
||||
) as { scripts?: Record<string, string> };
|
||||
|
||||
expect(packageJson.scripts?.dev).toBe("bun src/server/dev.ts");
|
||||
});
|
||||
|
||||
test("proxies dashboard API requests from Vite to the Bun API server", () => {
|
||||
const server = createDevServerConfig({
|
||||
DASHBOARD_DEV_API_TARGET: "http://127.0.0.1:5174",
|
||||
});
|
||||
|
||||
expect(server?.proxy?.["/api"]).toMatchObject({
|
||||
target: "http://127.0.0.1:5174",
|
||||
changeOrigin: true,
|
||||
});
|
||||
});
|
||||
});
|
||||
72
apps/web/src/server/dev.ts
Normal file
72
apps/web/src/server/dev.ts
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
const webHost = process.env.HOST || "0.0.0.0";
|
||||
const webPort = process.env.PORT || "5173";
|
||||
const apiHost = process.env.DASHBOARD_DEV_API_HOST || "127.0.0.1";
|
||||
const apiPort = process.env.DASHBOARD_DEV_API_PORT || "5174";
|
||||
const apiTarget = `http://${apiHost}:${apiPort}`;
|
||||
|
||||
if (import.meta.main) {
|
||||
runDevServers();
|
||||
}
|
||||
|
||||
export function runDevServers(): void {
|
||||
const children: Array<ReturnType<typeof Bun.spawn>> = [];
|
||||
let shuttingDown = false;
|
||||
|
||||
function spawn(
|
||||
label: string,
|
||||
command: string[],
|
||||
env: Record<string, string> = {},
|
||||
): void {
|
||||
const child = Bun.spawn(command, {
|
||||
env: {
|
||||
...process.env,
|
||||
...env,
|
||||
},
|
||||
stdin: "inherit",
|
||||
stdout: "inherit",
|
||||
stderr: "inherit",
|
||||
});
|
||||
children.push(child);
|
||||
|
||||
void child.exited.then((code) => {
|
||||
if (shuttingDown) return;
|
||||
console.error(`${label} exited with status ${code}`);
|
||||
shutdown(code || 1);
|
||||
});
|
||||
}
|
||||
|
||||
function shutdown(code = 0): void {
|
||||
if (shuttingDown) return;
|
||||
shuttingDown = true;
|
||||
|
||||
for (const child of children) {
|
||||
child.kill();
|
||||
}
|
||||
|
||||
void Promise.allSettled(children.map((child) => child.exited)).then(() => {
|
||||
process.exit(code);
|
||||
});
|
||||
}
|
||||
|
||||
process.on("SIGINT", () => shutdown(0));
|
||||
process.on("SIGTERM", () => shutdown(0));
|
||||
|
||||
spawn("api server", [process.execPath, "src/server/index.ts"], {
|
||||
HOST: apiHost,
|
||||
PORT: apiPort,
|
||||
});
|
||||
|
||||
spawn("vite dev server", [
|
||||
process.execPath,
|
||||
"x",
|
||||
"vite",
|
||||
"--host",
|
||||
webHost,
|
||||
"--port",
|
||||
webPort,
|
||||
], {
|
||||
DASHBOARD_DEV_API_TARGET: apiTarget,
|
||||
});
|
||||
|
||||
console.info(`Dashboard API proxy target: ${apiTarget}`);
|
||||
}
|
||||
84
apps/web/src/server/index.ts
Normal file
84
apps/web/src/server/index.ts
Normal file
|
|
@ -0,0 +1,84 @@
|
|||
import { extname, normalize } from "node:path";
|
||||
import { handleAgentDashboardRoute } from "./routes/agent-dashboard";
|
||||
import { handleDashboardRoute } from "./routes/dashboard";
|
||||
|
||||
const host = process.env.HOST || "0.0.0.0";
|
||||
const port = Number(process.env.PORT || 3000);
|
||||
const distRoot = `${process.cwd()}/dist`;
|
||||
|
||||
const contentTypes = new Map([
|
||||
[".css", "text/css; charset=utf-8"],
|
||||
[".html", "text/html; charset=utf-8"],
|
||||
[".js", "text/javascript; charset=utf-8"],
|
||||
[".json", "application/json; charset=utf-8"],
|
||||
[".svg", "image/svg+xml"],
|
||||
[".wasm", "application/wasm"],
|
||||
]);
|
||||
|
||||
export async function handleRequest(request: Request): Promise<Response> {
|
||||
const url = new URL(request.url);
|
||||
|
||||
if (url.pathname === "/api/dashboard") {
|
||||
if (request.method !== "GET") return methodNotAllowed(["GET"]);
|
||||
return handleDashboardRoute();
|
||||
}
|
||||
|
||||
if (url.pathname === "/api/agent/dashboard") {
|
||||
if (request.method !== "POST") return methodNotAllowed(["POST"]);
|
||||
return handleAgentDashboardRoute(request);
|
||||
}
|
||||
|
||||
if (url.pathname.startsWith("/api/")) {
|
||||
return Response.json({ ok: false, message: "Not found" }, { status: 404 });
|
||||
}
|
||||
|
||||
return serveStaticAsset(url.pathname);
|
||||
}
|
||||
|
||||
async function serveStaticAsset(pathname: string): Promise<Response> {
|
||||
const safePath = normalize(pathname).replace(/^(\.\.(\/|\\|$))+/, "");
|
||||
const assetPath = safePath === "/" || safePath === "." ? "/index.html" : safePath;
|
||||
const file = Bun.file(`${distRoot}${assetPath}`);
|
||||
|
||||
if (await file.exists()) {
|
||||
return new Response(file, {
|
||||
headers: contentTypeHeaders(assetPath),
|
||||
});
|
||||
}
|
||||
|
||||
const fallback = Bun.file(`${distRoot}/index.html`);
|
||||
if (await fallback.exists()) {
|
||||
return new Response(fallback, {
|
||||
headers: contentTypeHeaders(".html"),
|
||||
});
|
||||
}
|
||||
|
||||
return new Response("Build output not found", { status: 404 });
|
||||
}
|
||||
|
||||
function methodNotAllowed(allowedMethods: string[]): Response {
|
||||
return Response.json(
|
||||
{ ok: false, message: "Method not allowed" },
|
||||
{
|
||||
status: 405,
|
||||
headers: {
|
||||
Allow: allowedMethods.join(", "),
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
function contentTypeHeaders(pathname: string): HeadersInit {
|
||||
const contentType = contentTypes.get(extname(pathname));
|
||||
return contentType ? { "Content-Type": contentType } : {};
|
||||
}
|
||||
|
||||
if (import.meta.main) {
|
||||
Bun.serve({
|
||||
hostname: host,
|
||||
port,
|
||||
fetch: handleRequest,
|
||||
});
|
||||
|
||||
console.info(`Dimension Lab website listening on http://${host}:${port}`);
|
||||
}
|
||||
12
apps/web/src/server/routes/agent-dashboard.test.ts
Normal file
12
apps/web/src/server/routes/agent-dashboard.test.ts
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
import { describe, expect, test } from "vitest";
|
||||
import { handleAgentDashboardRoute } from "./agent-dashboard";
|
||||
|
||||
describe("agent dashboard API route", () => {
|
||||
test("delegates unauthorized requests to the existing agent handler", async () => {
|
||||
const response = await handleAgentDashboardRoute(
|
||||
new Request("http://localhost/api/agent/dashboard", { method: "POST" }),
|
||||
);
|
||||
|
||||
expect(response.status).toBe(401);
|
||||
});
|
||||
});
|
||||
5
apps/web/src/server/routes/agent-dashboard.ts
Normal file
5
apps/web/src/server/routes/agent-dashboard.ts
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
import { handleAgentDashboardRequest } from "$lib/server/agent-config";
|
||||
|
||||
export function handleAgentDashboardRoute(request: Request): Promise<Response> {
|
||||
return handleAgentDashboardRequest(request);
|
||||
}
|
||||
19
apps/web/src/server/routes/dashboard.test.ts
Normal file
19
apps/web/src/server/routes/dashboard.test.ts
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
import { describe, expect, test } from "vitest";
|
||||
import { dimensionLabDashboardFixture } from "$lib/model/fixtures/dimensionlab";
|
||||
import { loadDashboardResponse } from "./dashboard";
|
||||
|
||||
describe("dashboard API route", () => {
|
||||
test("returns ready dashboard runtime state from the existing model loader", async () => {
|
||||
const response = await loadDashboardResponse({
|
||||
disableLiveDatasources: true,
|
||||
refreshSeedDocument: true,
|
||||
seedIfEmpty: true,
|
||||
});
|
||||
|
||||
expect(response.state).toBe("ready");
|
||||
if (response.state !== "ready") throw new Error("expected ready dashboard");
|
||||
expect(response.document.metadata.title).toBe(
|
||||
dimensionLabDashboardFixture.metadata.title,
|
||||
);
|
||||
});
|
||||
});
|
||||
41
apps/web/src/server/routes/dashboard.ts
Normal file
41
apps/web/src/server/routes/dashboard.ts
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
import {
|
||||
loadDashboardRuntime,
|
||||
type DashboardRuntimeOptions,
|
||||
type DashboardRuntimeState,
|
||||
} from "$lib/server/dashboard";
|
||||
import { resolveDashboardDatasources } from "$lib/server/datasources";
|
||||
|
||||
export interface LoadDashboardResponseOptions
|
||||
extends Pick<
|
||||
DashboardRuntimeOptions,
|
||||
"refreshSeedDocument" | "seedIfEmpty" | "seedDocument"
|
||||
> {
|
||||
disableLiveDatasources?: boolean;
|
||||
}
|
||||
|
||||
export async function loadDashboardResponse(
|
||||
options: LoadDashboardResponseOptions = {},
|
||||
): Promise<DashboardRuntimeState> {
|
||||
const dashboard = loadDashboardRuntime(undefined, {
|
||||
refreshSeedDocument: options.refreshSeedDocument ?? true,
|
||||
seedIfEmpty: options.seedIfEmpty ?? true,
|
||||
seedDocument: options.seedDocument,
|
||||
});
|
||||
|
||||
if (dashboard.state !== "ready") {
|
||||
return dashboard;
|
||||
}
|
||||
|
||||
if (options.disableLiveDatasources || process.env.DISABLE_LIVE_DATASOURCES === "1") {
|
||||
return dashboard;
|
||||
}
|
||||
|
||||
return {
|
||||
...dashboard,
|
||||
document: await resolveDashboardDatasources(dashboard.document),
|
||||
};
|
||||
}
|
||||
|
||||
export async function handleDashboardRoute(): Promise<Response> {
|
||||
return Response.json(await loadDashboardResponse());
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue