import type { Connect, Plugin } from "vite"; import { createReadStream, existsSync, readFileSync, statSync } from "node:fs"; import { extname, resolve } from "node:path"; const MIME_TYPES: Record = { ".css": "text/css", ".js": "application/javascript", ".html": "text/html", ".png": "image/png", ".jpg": "image/jpeg", ".svg": "image/svg+xml", ".woff": "font/woff", ".woff2": "font/woff2", ".ttf": "font/ttf", ".json": "application/json", }; /** WESP hub page routes → static HTML (same as wesp/app/routes/pages.py). */ const WESP_PAGE_ROUTES: Record = { "/login": "login.html", "/recipes": "recipes.html", "/recipes_content": "recipes.html", "/components": "components.html", "/feed_dispensers": "feed_dispensers.html", "/reports": "reports.html", "/feed_consumption": "consumption.html", "/consumption": "consumption.html", "/lab": "lab.html", "/lab-profiles": "lab-profiles.html", "/admin": "admin.html", }; function injectOrchestratorScripts(html: string, urlPath: string): string { const favicon = '\n' + ''; const scripts = urlPath === "/login" ? [ '', '', ] : [ '', '', ]; const block = [favicon, ...scripts].join("\n"); if (html.includes("wesp-orchestrator-auth-bridge.js")) { if (!html.includes('rel="icon"') && html.includes("")) { return html.replace("", `${favicon}\n`); } return html; } if (html.includes("")) { return html.replace("", `${block}\n`); } return `${block}\n${html}`; } function sendFile(res: Connect.ServerResponse, filePath: string, transform?: (body: string) => string): void { const ext = extname(filePath).toLowerCase(); if (transform && ext === ".html") { const body = transform(readFileSync(filePath, "utf-8")); res.setHeader("Content-Type", "text/html; charset=utf-8"); res.end(body); return; } res.setHeader("Content-Type", MIME_TYPES[ext] ?? "application/octet-stream"); createReadStream(filePath).pipe(res); } export function wespStaticPlugin(staticRoot: string): Plugin { const root = resolve(staticRoot); return { name: "kompton-wesp-static", configureServer(server) { server.middlewares.use(createWespMiddleware(root)); }, configurePreviewServer(server) { server.middlewares.use(createWespMiddleware(root)); }, }; } function createWespMiddleware(root: string): Connect.NextHandleFunction { return (req, res, next) => { const urlPath = decodeURIComponent((req.url ?? "").split("?")[0]); if (!urlPath || urlPath === "/") { next(); return; } if (urlPath === "/favicon.ico") { const pngPath = resolve(root, "favicon-32.png"); if (existsSync(pngPath) && statSync(pngPath).isFile()) { res.setHeader("Content-Type", "image/png"); sendFile(res, pngPath); return; } } if (urlPath === "/favicon.svg") { const svgPath = resolve(root, "favicon.svg"); if (existsSync(svgPath) && statSync(svgPath).isFile()) { res.setHeader("Content-Type", "image/svg+xml"); sendFile(res, svgPath); return; } } const pageFile = WESP_PAGE_ROUTES[urlPath]; if (pageFile) { const htmlPath = resolve(root, pageFile); if (existsSync(htmlPath) && statSync(htmlPath).isFile()) { sendFile(res, htmlPath, (body) => injectOrchestratorScripts(body, urlPath)); return; } } if (urlPath.startsWith("/static/")) { const relative = urlPath.replace(/^\/static\//, ""); const filePath = resolve(root, relative); if (filePath.startsWith(root) && existsSync(filePath) && statSync(filePath).isFile()) { sendFile(res, filePath); return; } } next(); }; } export { WESP_PAGE_ROUTES };