import tailwindcss from "@tailwindcss/vite"; import fs from "fs"; import http from "http"; import { lookup as lookupMime } from "mrmime"; import path from "path"; import { fileURLToPath } from "url"; import { defineConfig, loadEnv, type Plugin } from "vite"; import { createHtmlPlugin } from "vite-plugin-html"; import { type AssetManifest, buildAssetUrl, rewriteAssetsForCdn, } from "./src/core/AssetUrls"; import { buildPublicAssetManifest, copyRootPublicFiles, createHashedPublicAssetFiles, getProprietaryDir, getResourcesDir, writePublicAssetManifest, } from "./src/server/PublicAssetManifest"; // Vite already handles these, but its good practice to define them explicitly const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); function serveProprietaryDir( proprietaryDir: string, resourcesDir: string, ): Plugin { return { name: "serve-proprietary-dir", configureServer(server) { // Must run before Vite's htmlFallback; skip when resources/ has the file // so publicDir keeps precedence. server.middlewares.use((req, res, next) => { if (!req.url) return next(); const rel = decodeURIComponent( new URL(req.url, "http://x").pathname, ).replace(/^\//, ""); if (rel.includes("..")) return next(); if (fs.existsSync(path.join(resourcesDir, rel))) return next(); const filePath = path.join(proprietaryDir, rel); if (!fs.existsSync(filePath) || !fs.statSync(filePath).isFile()) return next(); const mime = lookupMime(filePath); if (mime) res.setHeader("Content-Type", mime); res.setHeader("Cache-Control", "no-store"); fs.createReadStream(filePath).pipe(res); }); }, }; } // Dev-only stand-in for the nginx random-worker routing (the openfront_workers // upstream). Forwards these prefix-less POSTs to a randomly chosen worker port // so the worker can mint a self-owned id. Runs as direct middleware (before // vite's /api proxy). const RANDOM_WORKER_PATHS = ["/api/create_game", "/api/adminbot/create_game"]; function randomWorkerCreateProxy(numWorkers: number): Plugin { return { name: "random-worker-create-proxy", configureServer(server) { server.middlewares.use((req, res, next) => { if (req.method !== "POST") return next(); const path = (req.url ?? "").split("?")[0]; if (!RANDOM_WORKER_PATHS.includes(path)) return next(); const port = 3001 + Math.floor(Math.random() * numWorkers); const proxyReq = http.request( { host: "localhost", port, path, method: "POST", headers: req.headers, }, (proxyRes) => { res.writeHead(proxyRes.statusCode ?? 502, proxyRes.headers); proxyRes.pipe(res); }, ); proxyReq.on("error", (err) => { res.statusCode = 502; res.end(`create proxy error: ${err.message}`); }); req.pipe(proxyReq); }); }, }; } export default defineConfig(({ mode }) => { const env = loadEnv(mode, process.cwd(), ""); const isProduction = mode === "production"; const devNumWorkers = parseInt(env.NUM_WORKERS ?? "2", 10); const resourcesDir = getResourcesDir(__dirname); const proprietaryDir = getProprietaryDir(__dirname); const sourceDirs = [resourcesDir, proprietaryDir]; const assetManifest: AssetManifest = isProduction ? buildPublicAssetManifest(sourceDirs) : {}; const cdnBase = env.CDN_BASE ?? ""; const htmlAssetData = { assetManifest: JSON.stringify(assetManifest), cdnBase: JSON.stringify(cdnBase), gameEnv: JSON.stringify(env.GAME_ENV ?? "dev"), numWorkers: JSON.stringify(parseInt(env.NUM_WORKERS ?? "2", 10)), turnstileSiteKey: JSON.stringify( env.TURNSTILE_SITE_KEY ?? "1x00000000000000000000AA", ), jwtAudience: JSON.stringify(env.DOMAIN ?? "localhost"), instanceId: JSON.stringify(env.INSTANCE_ID ?? "DEV_ID"), manifestHref: buildAssetUrl("manifest.json", assetManifest, cdnBase), faviconHref: buildAssetUrl("images/Favicon.svg", assetManifest, cdnBase), gameplayScreenshotUrl: buildAssetUrl( "images/GameplayScreenshot.png", assetManifest, cdnBase, ), backgroundImageUrl: buildAssetUrl( "images/background.webp", assetManifest, cdnBase, ), desktopLogoImageUrl: buildAssetUrl( "images/OpenFront.png", assetManifest, cdnBase, ), mobileLogoImageUrl: buildAssetUrl("images/OF.png", assetManifest, cdnBase), }; // Vite's HTML transform replaces the source