Files
iistwin/server/vite.ts
Ильяс Султанов 5426c70f1f fix(deploy): vite.config импортируется по вычисляемому URL — esbuild без --splitting
esbuild без --splitting инлайнит статический import('../vite.config') в бандл
и поднимает его импорты (vite, @vitejs/plugin-react) на верхний уровень —
prod падал с ERR_MODULE_NOT_FOUND при старте. Динамический import по
вычисляемому URL esbuild не анализирует и оставляет рантайму (dev-only).

Проверено: dist/index.js содержит 0 статических импортов vite, бандл стартует,
подключается к БД и доходит до инициализации таблиц
2026-09-07 21:33:58 +03:00

125 lines
4.4 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import express, { type Express } from "express";
import fs from "fs";
import path from "path";
import { type Server } from "http";
import { nanoid } from "nanoid";
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) {
// vite и vite.config — devDependencies: импортируем динамически, чтобы
// production-бандл (npm ci --omit=dev) не резолвил их при старте.
// Путь вычисляется рантаймом — esbuild без --splitting инлайнит статический
// import("../vite.config") в бандл и поднимает его импорты (vite, plugin-react)
// на верхний уровень, что роняет прод при старте.
const { createServer: createViteServer, createLogger } = await import("vite");
const viteConfigUrl = new URL("../vite.config.ts", import.meta.url).href;
const { default: viteConfig } = await import(viteConfigUrl);
const viteLogger = createLogger();
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"));
});
}