Files
QihuAI Design 8f57956904 #978 safe-align: 固化 frontend-v2 空白页修复真相源
- 新增 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 错误)
2026-08-23 09:36:35 +08:00

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,
},
},
},
});