feat: serve workflow console production build
This commit is contained in:
@@ -0,0 +1,102 @@
|
|||||||
|
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
||||||
|
import * as fs from "node:fs";
|
||||||
|
import * as path from "node:path";
|
||||||
|
import * as os from "node:os";
|
||||||
|
import { Hono } from "hono";
|
||||||
|
import { addStaticRoutes, validateConsoleRoot } from "./static.js";
|
||||||
|
|
||||||
|
let tmpDir: string;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "static-test-"));
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
fs.rmSync(tmpDir, { recursive: true, force: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
const createTestApp = (consoleRoot: string) => {
|
||||||
|
const app = new Hono();
|
||||||
|
addStaticRoutes(app, { consoleRoot });
|
||||||
|
return app;
|
||||||
|
};
|
||||||
|
|
||||||
|
describe("validateConsoleRoot", () => {
|
||||||
|
it("throws when directory does not exist", () => {
|
||||||
|
expect(() => validateConsoleRoot("/nonexistent")).toThrow(
|
||||||
|
"Console root not found",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("throws when index.html is missing", () => {
|
||||||
|
expect(() => validateConsoleRoot(tmpDir)).toThrow(
|
||||||
|
"Console root missing index.html",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not throw for valid root", () => {
|
||||||
|
fs.writeFileSync(path.join(tmpDir, "index.html"), "<!DOCTYPE html>");
|
||||||
|
expect(() => validateConsoleRoot(tmpDir)).not.toThrow();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("addStaticRoutes", () => {
|
||||||
|
it("serves index.html from root", async () => {
|
||||||
|
fs.writeFileSync(
|
||||||
|
path.join(tmpDir, "index.html"),
|
||||||
|
"<!DOCTYPE html><html><body>test</body></html>",
|
||||||
|
);
|
||||||
|
const app = createTestApp(tmpDir);
|
||||||
|
const res = await app.request("/");
|
||||||
|
expect(res.status).toBe(200);
|
||||||
|
const text = await res.text();
|
||||||
|
expect(text).toContain("test");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("serves static assets", async () => {
|
||||||
|
fs.mkdirSync(path.join(tmpDir, "assets"), { recursive: true });
|
||||||
|
fs.writeFileSync(
|
||||||
|
path.join(tmpDir, "assets", "app.js"),
|
||||||
|
"console.log('hello')",
|
||||||
|
);
|
||||||
|
const app = createTestApp(tmpDir);
|
||||||
|
const res = await app.request("/assets/app.js");
|
||||||
|
expect(res.status).toBe(200);
|
||||||
|
const text = await res.text();
|
||||||
|
expect(text).toContain("console.log");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("SPA fallback serves index.html for unknown routes", async () => {
|
||||||
|
fs.writeFileSync(
|
||||||
|
path.join(tmpDir, "index.html"),
|
||||||
|
"<!DOCTYPE html><html><body>spa</body></html>",
|
||||||
|
);
|
||||||
|
const app = createTestApp(tmpDir);
|
||||||
|
const res = await app.request("/some/client/route");
|
||||||
|
expect(res.status).toBe(200);
|
||||||
|
const text = await res.text();
|
||||||
|
expect(text).toContain("spa");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns JSON 404 for unknown API routes", async () => {
|
||||||
|
fs.writeFileSync(
|
||||||
|
path.join(tmpDir, "index.html"),
|
||||||
|
"<!DOCTYPE html><html><body>spa</body></html>",
|
||||||
|
);
|
||||||
|
const app = createTestApp(tmpDir);
|
||||||
|
const res = await app.request("/api/unknown", { method: "GET" });
|
||||||
|
expect(res.status).toBe(404);
|
||||||
|
const body = await res.json();
|
||||||
|
expect(body.error).toBe("not found");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns JSON 404 for POST to non-API routes", async () => {
|
||||||
|
fs.writeFileSync(
|
||||||
|
path.join(tmpDir, "index.html"),
|
||||||
|
"<!DOCTYPE html><html><body>spa</body></html>",
|
||||||
|
);
|
||||||
|
const app = createTestApp(tmpDir);
|
||||||
|
const res = await app.request("/nonexistent", { method: "POST" });
|
||||||
|
expect(res.status).toBe(404);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
import { Hono } from "hono";
|
||||||
|
import { serveStatic } from "@hono/node-server/serve-static";
|
||||||
|
import * as fs from "node:fs";
|
||||||
|
import * as path from "node:path";
|
||||||
|
|
||||||
|
export type StaticOptions = {
|
||||||
|
readonly consoleRoot: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function validateConsoleRoot(consoleRoot: string): void {
|
||||||
|
if (!fs.existsSync(consoleRoot)) {
|
||||||
|
throw new Error(`Console root not found: ${consoleRoot}`);
|
||||||
|
}
|
||||||
|
const indexPath = path.join(consoleRoot, "index.html");
|
||||||
|
if (!fs.existsSync(indexPath)) {
|
||||||
|
throw new Error(`Console root missing index.html: ${consoleRoot}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function addStaticRoutes(
|
||||||
|
app: Hono,
|
||||||
|
options: StaticOptions,
|
||||||
|
): void {
|
||||||
|
const { consoleRoot } = options;
|
||||||
|
|
||||||
|
app.use("/assets/*", serveStatic({ root: consoleRoot }));
|
||||||
|
|
||||||
|
app.get("*", (c) => {
|
||||||
|
if (c.req.path.startsWith("/api/")) {
|
||||||
|
return c.json({ error: "not found" }, 404);
|
||||||
|
}
|
||||||
|
return serveStatic({ root: consoleRoot, path: "index.html" })(c);
|
||||||
|
});
|
||||||
|
|
||||||
|
app.all("*", (c) => {
|
||||||
|
if (c.req.path.startsWith("/api/")) {
|
||||||
|
return c.json({ error: "not found" }, 404);
|
||||||
|
}
|
||||||
|
return c.json({ error: "not found" }, 404);
|
||||||
|
});
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user