diff --git a/packages/web/vite.config.ts b/packages/web/vite.config.ts index 1267586..1da63c8 100644 --- a/packages/web/vite.config.ts +++ b/packages/web/vite.config.ts @@ -1,10 +1,10 @@ import { readFileSync } from "node:fs"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; import tailwindcss from "@tailwindcss/vite"; import { tanstackRouter } from "@tanstack/router-plugin/vite"; import react from "@vitejs/plugin-react"; -import path from "path"; -import { fileURLToPath } from "url"; -import { defineConfig } from "vite"; +import { defineConfig, type Plugin } from "vite"; const __dirname = path.dirname(fileURLToPath(import.meta.url)); const host = process.env.TAURI_DEV_HOST; @@ -12,9 +12,52 @@ const { version } = JSON.parse( readFileSync(path.resolve(__dirname, "../../package.json"), "utf-8"), ) as { version: string }; +// Dev-mode mirror of the nginx /api reverse proxy: read X-Honcho-Upstream and +// forward /api/* there, so `make dev-web` behaves identically to the docker image +// (same-origin requests, no browser CORS). Connect strips the /api mount prefix, +// so req.url is already the upstream path (e.g. /v3/workspaces/list). +function honchoApiProxy(): Plugin { + const HEADER = "x-honcho-upstream"; + return { + name: "honcho-api-proxy", + configureServer(server) { + server.middlewares.use("/api", async (req, res) => { + const upstream = req.headers[HEADER]; + if (typeof upstream !== "string" || upstream.trim() === "") { + res.statusCode = 421; + res.setHeader("X-Honcho-Proxy-Reject", "no-upstream"); + res.end(); + return; + } + const target = upstream.replace(/\/+$/, "") + (req.url ?? ""); + const chunks: Buffer[] = []; + for await (const c of req) chunks.push(c as Buffer); + try { + const upstreamRes = await fetch(target, { + method: req.method, + headers: { + "content-type": req.headers["content-type"] ?? "application/json", + ...(req.headers.authorization ? { authorization: req.headers.authorization } : {}), + }, + body: ["GET", "HEAD"].includes(req.method ?? "") ? undefined : Buffer.concat(chunks), + }); + res.statusCode = upstreamRes.status; + upstreamRes.headers.forEach((v, k) => { + res.setHeader(k, v); + }); + res.end(Buffer.from(await upstreamRes.arrayBuffer())); + } catch (e) { + res.statusCode = 502; + res.end(`proxy error: ${e instanceof Error ? e.message : String(e)}`); + } + }); + }, + }; +} + export default defineConfig({ clearScreen: false, - plugins: [tanstackRouter({ autoCodeSplitting: true }), react(), tailwindcss()], + plugins: [tanstackRouter({ autoCodeSplitting: true }), react(), honchoApiProxy(), tailwindcss()], define: { __APP_VERSION__: JSON.stringify(version), },