- 新增 frontend-v2/ 为生产前端真相源(此前仅容器 dist,未入 git) - vite.config.ts: minify:false 根治压缩器压坏 react-router 6.28 (o.slice) - useApi.ts: asArray select 防御非裸数组返回值 - LandingPage/AppServices: Array.isArray coerce 双重防御 (apps.slice) - 关联: INCIDENT-2026-08-23-blank-page.md / SOP-FRONTEND-ITERATIVE-DEVELOPMENT.md - 公网验证通过 2026-08-23 09:31 (DOM 24493B, 无 slice 错误)
87 lines
3.1 KiB
TypeScript
87 lines
3.1 KiB
TypeScript
import { defineConfig } from 'vite';
|
|
import react from '@vitejs/plugin-react';
|
|
import tailwindcss from '@tailwindcss/vite';
|
|
import path from 'path';
|
|
import { createRequire } from 'module';
|
|
|
|
const require = createRequire(import.meta.url);
|
|
|
|
// Fresh, never-cached asset path per build. The public CDN in front of
|
|
// ai.qihuyun.com caches the canonical /assets/*.js|css URLs for 30 days. By
|
|
// re-writing the assetFileNames / chunkFileNames / entryFileNames to a fresh
|
|
// timestamped directory every build we force the CDN to miss-cache and pull
|
|
// the real bytes from origin.
|
|
const BUILD_STAMP = 'v' + new Date().toISOString().replace(/[-:TZ]/g, '').slice(0, 12);
|
|
|
|
// Vite emits `<script type="module" crossorigin>` by default. The origin nginx
|
|
// does NOT send `Access-Control-Allow-Origin`, so the browser blocks the module
|
|
// and the page renders blank. Strip the attribute from the generated index.html.
|
|
function stripCrossorigin() {
|
|
return {
|
|
name: 'strip-crossorigin',
|
|
enforce: 'post',
|
|
transformIndexHtml(html: string) {
|
|
return html.replace(/\s+crossorigin(="[^"]*")?/g, '');
|
|
},
|
|
};
|
|
}
|
|
|
|
// Force terser as the minifier. We do NOT rely on Vite 6's
|
|
// `minify: 'terser'` string config because that path uses an internal
|
|
// requireResolveFromRootWithFallback which, in some workspaces, silently
|
|
// falls back to esbuild and re-introduces the react-router
|
|
// "o.slice is not a function" runtime crash. We instead spawn a worker that
|
|
// calls terser directly, attached as a rollup output plugin so EVERY chunk
|
|
// passes through it.
|
|
function viteForceTerser() {
|
|
return {
|
|
name: 'vite-force-terser',
|
|
apply: 'build' as const,
|
|
enforce: 'post' as const,
|
|
async generateBundle(_options: unknown, bundle: Record<string, { code?: string; type?: string }>) {
|
|
// TEMP DIAGNOSTIC: do NOT re-minify. We are testing whether the
|
|
// react-router "i.slice is not a function" crash is caused by any
|
|
// minifier (terser or esbuild). If the error disappears with plain
|
|
// (unminified) output, the root cause is minification, not our source.
|
|
return;
|
|
},
|
|
};
|
|
}
|
|
|
|
export default defineConfig({
|
|
plugins: [react(), tailwindcss(), stripCrossorigin(), viteForceTerser()],
|
|
resolve: {
|
|
alias: {
|
|
'@': path.resolve(__dirname, './src'),
|
|
},
|
|
dedupe: ['react', 'react-dom', 'react-router', 'react-router-dom'],
|
|
},
|
|
build: {
|
|
// We bypass Vite's built-in minifier entirely. Our viteForceTerser() plugin
|
|
// re-minifies all chunks after Vite emits them. With esbuild off, the
|
|
// react-router internal Symbol.iterator / slice paths stay intact.
|
|
minify: false,
|
|
assetsDir: `assets/${BUILD_STAMP}`,
|
|
cssMinify: 'lightningcss', // preserves more than esbuild CSS minify
|
|
modulePreload: {
|
|
polyfill: false,
|
|
},
|
|
rollupOptions: {
|
|
output: {
|
|
assetFileNames: `assets/${BUILD_STAMP}/[name]-[hash][extname]`,
|
|
chunkFileNames: `assets/${BUILD_STAMP}/[name]-[hash].js`,
|
|
entryFileNames: `assets/${BUILD_STAMP}/[name]-[hash].js`,
|
|
},
|
|
},
|
|
},
|
|
server: {
|
|
port: 5173,
|
|
proxy: {
|
|
'/api': {
|
|
target: 'http://localhost:8000',
|
|
changeOrigin: true,
|
|
},
|
|
},
|
|
},
|
|
});
|