import path from "node:path"; import { fileURLToPath } from "node:url"; import { createRequire } from "node:module"; import { defineConfig } from "vitest/config"; import { loadEnv, type Plugin } from "vite"; import react from "@vitejs/plugin-react"; import tailwindcss from "@tailwindcss/vite"; const __dirname = path.dirname(fileURLToPath(import.meta.url)); const require = createRequire(import.meta.url); const streamBrowserifyPath = require.resolve("stream-browserify"); function createIamApiAdapter(apiBaseUrl: string): Plugin { const upstreamBaseUrl = `${apiBaseUrl.replace(/\/+$/, "")}/api`; return { name: "iam-api-adapter", configureServer(server) { server.middlewares.use("/um-api", async (req, res) => { const requestPath = req.url ?? "/"; const normalizedPath = requestPath.replace(/^\/+/, ""); const targetUrl = new URL(normalizedPath, `${upstreamBaseUrl}/`); try { const headers = new Headers(); for (const [key, value] of Object.entries(req.headers)) { if (!value || key.toLowerCase() === "host") { continue; } if (Array.isArray(value)) { for (const item of value) { headers.append(key, item); } continue; } headers.set(key, value); } const body = req.method === "GET" || req.method === "HEAD" ? undefined : await new Promise((resolve, reject) => { const chunks: Buffer[] = []; req.on("data", (chunk) => chunks.push( Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk), ), ); req.on("end", () => resolve(Buffer.concat(chunks))); req.on("error", reject); }); const upstreamResponse = await fetch(targetUrl, { method: req.method, headers, body, }); if (targetUrl.pathname.endsWith("/auth/me")) { const payload = await upstreamResponse.json(); const unwrappedPayload = payload && typeof payload === "object" && "success" in payload && "data" in payload ? payload.data : payload; res.statusCode = upstreamResponse.status; res.setHeader("content-type", "application/json; charset=utf-8"); res.end(JSON.stringify(unwrappedPayload)); return; } res.statusCode = upstreamResponse.status; upstreamResponse.headers.forEach((value, key) => { res.setHeader(key, value); }); res.end(Buffer.from(await upstreamResponse.arrayBuffer())); } catch (error) { server.ssrFixStacktrace(error as Error); res.statusCode = 502; res.setHeader("content-type", "application/json; charset=utf-8"); res.end( JSON.stringify({ message: "Failed to forward IAM request", }), ); } }); }, }; } export default defineConfig(({ mode }) => { const env = loadEnv(mode, __dirname, ""); const apiBaseUrl = env.VITE_BASE_API_URL?.trim() || "http://localhost:3000"; return { plugins: [react(), tailwindcss(), createIamApiAdapter(apiBaseUrl)], resolve: { alias: { "@": path.resolve(__dirname, "./src"), "node:buffer": "buffer", "node:stream": streamBrowserifyPath, // Resolve from TS source so Vite gets ESM named exports (dist is CommonJS). "@edr/types": path.resolve( __dirname, "../../../packages/types/src/index.ts", ), }, // Force a single copy of these singletons so MantineProvider context is // shared between the backoffice app and @edr/ui-common (which ships its // own node_modules copy). Without this, two separate @mantine/core // instances are bundled and the context lookup fails at runtime. dedupe: ["react", "react-dom", "@mantine/core", "@mantine/hooks"], }, server: { port: 5183, host: "0.0.0.0", }, test: { environment: "node", }, }; });