import express, { type Express } from "express"; import fs from "fs"; import path from "path"; import { createServer as createViteServer, createLogger } from "vite"; import { type Server } from "http"; import viteConfig from "../vite.config"; import { nanoid } from "nanoid"; const viteLogger = createLogger(); export function log(message: string, source = "express") { const formattedTime = new Date().toLocaleTimeString("en-US", { hour: "numeric", minute: "2-digit", second: "2-digit", hour12: true, }); console.log(`${formattedTime} [${source}] ${message}`); } export async function setupVite(app: Express, server: Server) { const serverOptions = { middlewareMode: true, hmr: { server }, allowedHosts: true as const, }; const vite = await createViteServer({ ...viteConfig, configFile: false, customLogger: { ...viteLogger, error: (msg, options) => { viteLogger.error(msg, options); process.exit(1); }, }, server: serverOptions, appType: "custom", }); app.use(vite.middlewares); app.use("*", async (req, res, next) => { const url = req.originalUrl; try { const clientTemplate = path.resolve( import.meta.dirname, "..", "client", "index.html", ); // always reload the index.html file from disk incase it changes let template = await fs.promises.readFile(clientTemplate, "utf-8"); template = template.replace( `src="/src/main.tsx"`, `src="/src/main.tsx?v=${nanoid()}"`, ); const page = await vite.transformIndexHtml(url, template); res.status(200).set({ "Content-Type": "text/html" }).end(page); } catch (e) { vite.ssrFixStacktrace(e as Error); next(e); } }); } export function serveStatic(app: Express) { const distPath = path.resolve(import.meta.dirname, "public"); if (!fs.existsSync(distPath)) { throw new Error( `Could not find the build directory: ${distPath}, make sure to build the client first`, ); } app.use( express.static(distPath, { setHeaders: (res, filePath) => { const relative = path.relative(distPath, filePath).replace(/\\/g, '/'); // Service Worker must never be cached by the browser, otherwise updates // are delayed by up to 24 hours and users see stale app versions. if (relative === 'sw.js') { res.setHeader('Cache-Control', 'no-store, no-cache, must-revalidate, proxy-revalidate'); res.setHeader('Pragma', 'no-cache'); res.setHeader('Expires', '0'); return; } // HTML entry and manifest must be revalidated on every load. if (relative === 'index.html' || relative === 'manifest.json') { res.setHeader('Cache-Control', 'no-cache, no-store, must-revalidate, proxy-revalidate'); res.setHeader('Pragma', 'no-cache'); res.setHeader('Expires', '0'); return; } // Vite hashed assets (e.g. index-AbC123Xy.js, FormEditor-fy2__RGH.js) // can be cached forever because the filename changes on every content change. // Vite uses a dash before the hash, so we match either `.hash.ext` or `-hash.ext`. if (/[-.][a-zA-Z0-9_-]{8,}\.(js|css|webp|png|jpg|jpeg|svg|gif|ico|woff|woff2|ttf|otf|eot)$/i.test(relative)) { res.setHeader('Cache-Control', 'public, max-age=31536000, immutable'); return; } }, }), ); // fall through to index.html if the file doesn't exist app.use("*", (_req, res) => { res.setHeader('Cache-Control', 'no-cache, no-store, must-revalidate, proxy-revalidate'); res.setHeader('Pragma', 'no-cache'); res.setHeader('Expires', '0'); res.sendFile(path.resolve(distPath, "index.html")); }); }