import tailwindcss from "@tailwindcss/vite"; import fs from "fs"; import path from "path"; import { fileURLToPath } from "url"; import { defineConfig, loadEnv, type Plugin } from "vite"; import { createHtmlPlugin } from "vite-plugin-html"; import tsconfigPaths from "vite-tsconfig-paths"; import { type AssetManifest, buildAssetUrl } from "./src/core/AssetUrls"; import { buildPublicAssetManifest, copyRootPublicFiles, createHashedPublicAssetFiles, getProprietaryDir, getResourcesDir, writePublicAssetManifestModule, } 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(dir: string): Plugin { const resolvedDir = path.resolve(dir) + path.sep; return { name: "serve-proprietary-dir", configureServer(server) { // Return a function so the middleware is registered after Vite's internal // static-file handler (publicDir). This makes proprietary/ a fallback // rather than taking precedence over resources/. return () => { server.middlewares.use((req, res, next) => { if (!req.url) return next(); const urlPath = new URL(req.url, "http://localhost").pathname; const filePath = path.resolve( dir, decodeURIComponent(urlPath).replace(/^\//, ""), ); if (!filePath.startsWith(resolvedDir)) return next(); if (fs.existsSync(filePath) && fs.statSync(filePath).isFile()) { res.setHeader("Cache-Control", "no-cache"); fs.createReadStream(filePath).pipe(res); } else { next(); } }); }; }, }; } export default defineConfig(({ mode }) => { const env = loadEnv(mode, process.cwd(), ""); const isProduction = mode === "production"; const resourcesDir = getResourcesDir(__dirname); const proprietaryDir = getProprietaryDir(__dirname); const sourceDirs = [resourcesDir, proprietaryDir]; const assetManifest: AssetManifest = isProduction ? buildPublicAssetManifest(sourceDirs) : {}; const htmlAssetData = { assetManifest: JSON.stringify(assetManifest), gameEnv: JSON.stringify(env.GAME_ENV ?? "dev"), manifestHref: buildAssetUrl("manifest.json", assetManifest), faviconHref: buildAssetUrl("images/Favicon.svg", assetManifest), gameplayScreenshotUrl: buildAssetUrl( "images/GameplayScreenshot.png", assetManifest, ), backgroundImageUrl: buildAssetUrl("images/background.webp", assetManifest), desktopLogoImageUrl: buildAssetUrl("images/OpenFront.png", assetManifest), mobileLogoImageUrl: buildAssetUrl("images/OF.png", assetManifest), }; const syncHashedPublicAssets = () => ({ name: "sync-hashed-public-assets", apply: "build" as const, closeBundle() { const outDir = path.join(__dirname, "static"); copyRootPublicFiles(resourcesDir, outDir); createHashedPublicAssetFiles(sourceDirs, outDir, assetManifest); writePublicAssetManifestModule(outDir, assetManifest); }, }); // In dev, redirect visits to /w*/game/* to "/" so Vite serves the index.html. const devGameHtmlBypass = (req?: { url?: string; method?: string; headers?: { accept?: string | string[] }; }) => { if (req?.method !== "GET") return undefined; const accept = req.headers?.accept; const acceptValue = Array.isArray(accept) ? accept.join(",") : (accept ?? ""); if (!acceptValue.includes("text/html")) return undefined; if (!req.url) return undefined; if (/^\/w\d+\/game\/[^/]+/.test(req.url)) { return "/"; } return undefined; }; return { test: { globals: true, environment: "jsdom", setupFiles: "./tests/setup.ts", }, root: "./", base: "/", publicDir: isProduction ? false : "resources", resolve: { alias: { "protobufjs/minimal": path.resolve( __dirname, "node_modules/protobufjs/minimal.js", ), resources: path.resolve(__dirname, "resources"), }, }, plugins: [ tsconfigPaths(), ...(!isProduction ? [serveProprietaryDir(proprietaryDir)] : []), ...(isProduction ? [] : [ createHtmlPlugin({ minify: false, entry: "/src/client/Main.ts", template: "index.html", inject: { data: { gitCommit: JSON.stringify("DEV"), ...htmlAssetData, }, }, }), ]), ...(isProduction ? [syncHashedPublicAssets()] : []), tailwindcss(), ], define: { __ASSET_MANIFEST__: JSON.stringify(assetManifest), "process.env.WEBSOCKET_URL": JSON.stringify( isProduction ? "" : "localhost:3000", ), "process.env.GAME_ENV": JSON.stringify(isProduction ? "prod" : "dev"), "process.env.STRIPE_PUBLISHABLE_KEY": JSON.stringify( env.STRIPE_PUBLISHABLE_KEY, ), "process.env.API_DOMAIN": JSON.stringify(env.API_DOMAIN), // Add other process.env variables if needed, OR migrate code to import.meta.env }, build: { outDir: "static", // Webpack outputs to 'static', assuming we want to keep this. emptyOutDir: true, assetsDir: "assets", // Sub-directory for assets rollupOptions: { output: { manualChunks: { vendor: ["pixi.js", "howler", "zod", "protobufjs"], }, }, }, }, server: { port: 9000, // Automatically open the browser when the server starts open: process.env.SKIP_BROWSER_OPEN !== "true", proxy: { "/lobbies": { target: "ws://localhost:3000", ws: true, changeOrigin: true, }, // Worker proxies "/w0": { target: "ws://localhost:3001", ws: true, secure: false, changeOrigin: true, bypass: (req) => devGameHtmlBypass(req), rewrite: (path) => path.replace(/^\/w0/, ""), }, "/w1": { target: "ws://localhost:3002", ws: true, secure: false, changeOrigin: true, bypass: (req) => devGameHtmlBypass(req), rewrite: (path) => path.replace(/^\/w1/, ""), }, // API proxies "/api": { target: "http://localhost:3000", changeOrigin: true, secure: false, }, }, }, }; });