Files
deerflow-code/frontend-web/vite.config.ts
T

187 lines
6.5 KiB
TypeScript

import { existsSync, readFileSync } from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
import react from "@vitejs/plugin-react";
import { defineConfig, type Plugin } from "vite";
import sourceIdentifierPlugin from "vite-plugin-source-identifier";
const rootDir = fileURLToPath(new URL(".", import.meta.url));
const JSX_RUNTIME_NAMED_EXPORTS = [
"jsx",
"jsxs",
"jsxDEV",
"createElement",
"Fragment",
] as const;
const JSX_RUNTIME_DEP_FILES = new Set([
"react_jsx-runtime.js",
"react_jsx-dev-runtime.js",
]);
function hasNamedJsxExport(code: string): boolean {
return (
/\bexport\s+const\s+jsx\b/.test(code) ||
/\bexport\s+function\s+jsx\b/.test(code) ||
/\bexport\s*\{[^}]*\bjsx\b/.test(code)
);
}
// Vite prebundles CJS `react/jsx-runtime` as `export default require_jsx_runtime()`.
// App source is rewritten to a default import, but a stale Chrome 102 tab may still
// execute `import { jsx } from '.../react_jsx-runtime.js'`. Named re-exports from a
// default-only module throw SyntaxError at link time, so the page never reloads.
function withNamedJsxExports(code: string): string {
if (hasNamedJsxExport(code)) return code;
return code.replace(
/export\s+default\s+([^;]+);/,
(_match, expr: string) =>
[
`const __jsxRuntime = ${String(expr).trim()};`,
"export default __jsxRuntime;",
...JSX_RUNTIME_NAMED_EXPORTS.map(
(name) => `export const ${name} = __jsxRuntime.${name};`,
),
].join("\n"),
);
}
function staleDepsModuleSource(): string {
return [
"export function jsx(){return null}",
"export function jsxs(){return null}",
"export function jsxDEV(){return null}",
"export const Fragment = Symbol.for('react.fragment');",
"export default {jsx, jsxs, jsxDEV, Fragment};",
"try{const k='__vite_stale_chunk_reloaded__';const n=Number(sessionStorage.getItem(k)||'0');" +
"if(n<3){sessionStorage.setItem(k,String(n+1));location.reload();}}catch(e){}",
].join("\n");
}
// 依赖预构建缓存被重建后(pnpm install / 重复启动 vite / --force),
// 残留的旧浏览器标签页仍会按旧版本号(?v=<hash>)请求已删除的 chunk。
// Chrome 在链接 `import { jsx } from ...` 时若没有本地 named export,会直接
// SyntaxError,模块体根本不会执行,页面就白屏卡死。缺失 chunk 一律补本地
// jsx 导出再刷新;真实 jsx-runtime 也补上 named export,兼容旧模块图。
const staleDepsChunkSelfReload = (): Plugin => ({
name: "stale-deps-chunk-self-reload",
configureServer(server) {
const depsDir = path.join(rootDir, "node_modules", ".vite", "deps");
server.middlewares.use((req, res, next) => {
const match = /^\/node_modules\/\.vite\/deps\/([^?]+)/.exec(req.url ?? "");
if (!match) {
next();
return;
}
const fileName = path.basename(decodeURIComponent(match[1]));
if (fileName !== decodeURIComponent(match[1])) {
next();
return;
}
const filePath = path.join(depsDir, fileName);
if (existsSync(filePath)) {
if (!JSX_RUNTIME_DEP_FILES.has(fileName)) {
next();
return;
}
res.setHeader("Content-Type", "application/javascript");
res.end(withNamedJsxExports(readFileSync(filePath, "utf8")));
return;
}
if (!/^chunk-[A-Za-z0-9_-]+\.js$/.test(fileName)) {
next();
return;
}
res.setHeader("Content-Type", "application/javascript");
res.setHeader("Cache-Control", "no-store");
res.end(staleDepsModuleSource());
});
},
});
// Tailwind 4 emits CSS cascade layers, which require Chrome 99+. Chrome 100
// is therefore the practical lower bound for this Win7 legacy build.
const LEGACY_CHROME = 100 << 16;
export default defineConfig({
plugins: [staleDepsChunkSelfReload(), sourceIdentifierPlugin(), react()],
css: {
// Also used by the production CSS minifier. Development CSS is handled
// by the matching PostCSS compatibility pass in postcss.config.js.
lightningcss: {
targets: {
chrome: LEGACY_CHROME,
},
},
},
resolve: {
alias: [
{
// react-syntax-highlighter@16.1.1 exposes an async `sqlMore`
// loader, but its own highlight.js@10.x dependency has no
// `sql_more` module. Reuse the bundled SQL grammar so Rollup can
// resolve every generated language loader without leaving a broken
// browser-time external import.
find: /^highlight\.js\/lib\/languages\/sql_more$/,
replacement: "highlight.js/lib/languages/sql",
},
{
find: "@/env",
replacement: path.resolve(rootDir, "src/env.ts"),
},
{
find: "next/navigation",
replacement: path.resolve(rootDir, "src/shims/next-navigation.ts"),
},
{
find: "next/link",
replacement: path.resolve(rootDir, "src/shims/next-link.tsx"),
},
{
find: "@vite",
replacement: path.resolve(rootDir, "src"),
},
{
find: "@",
replacement: path.resolve(rootDir, "src"),
},
],
},
// 预打包语法高亮核心依赖:refractor/react-syntax-highlighter 只被
// react-syntax-highlighter/dist/esm/* 的二次 import 触达,属「迟发现依赖」——
// 运行中途才触发 Vite 重新预打包会重排全部 chunk 名,浏览器在同一个 ?v=hash
// 下混用新旧缓存 chunk,报 "does not provide an export named 'refractor'"
// 并整树白屏(清 node_modules/.vite + 重启 + 硬刷新才恢复)。启动即预打包可避免。
optimizeDeps: {
include: [
"react",
"react/jsx-runtime",
"react/jsx-dev-runtime",
"react-dom",
"react-dom/client",
"react-syntax-highlighter",
"refractor",
],
esbuildOptions: {
target: "chrome100",
},
},
esbuild: {
target: "chrome100",
},
// Relative URLs make the same build work at the domain root and at any
// Nginx path prefix. A fixed prefix can still be supplied at release time.
base: process.env.VITE_ASSET_BASE ?? "./",
build: {
cssMinify: "lightningcss",
target: "chrome100",
},
server: {
// 与 scripts/env.sh FRONTEND_PORT=5174、圆桌 wujie 嵌入文档一致。
port: 5173,
// 无界宿主跨域 fetch 子应用静态资源时,Vite 需回显请求的 Origin。
cors: true,
},
});