187 lines
6.5 KiB
TypeScript
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,
|
|
},
|
|
});
|