import { readFileSync } from 'node:fs'; import { dirname, resolve } from 'node:path'; import { fileURLToPath } from 'node:url'; import { defineConfig } from 'vitest/config'; import type { Plugin } from 'vite'; import react from '@vitejs/plugin-react'; const PYODIDE_ASSETS = [ 'pyodide.asm.js', 'pyodide.asm.wasm', 'python_stdlib.zip', 'pyodide-lock.json', ] as const; const pyodideDirectory = resolve( dirname(fileURLToPath(import.meta.url)), '../node_modules/pyodide', ); /** 生产预览中长期缓存带内容哈希的资源,同时允许入口文件及时更新。 */ function previewCacheHeaders(): Plugin { return { name: 'preview-cache-headers', configurePreviewServer(server) { server.middlewares.use((request, response, next) => { const path = request.url?.split(/[?#]/, 1)[0] ?? ''; if (path.startsWith('/assets/')) response.setHeader('Cache-Control', 'public, max-age=31536000, immutable'); else if (path.startsWith('/pyodide/')) response.setHeader( 'Cache-Control', 'public, max-age=86400, stale-while-revalidate=604800', ); else response.setHeader('Cache-Control', 'no-cache'); next(); }); }, }; } /** 让开发服务器与生产构建都从本地 npm 包提供 Pyodide,保持平台可离线部署。 */ function localPyodideAssets(): Plugin { let isBuild = false; return { name: 'local-pyodide-assets', configResolved(config) { isBuild = config.command === 'build'; }, configureServer(server) { server.middlewares.use((request, response, next) => { const name = request.url?.split(/[?#]/, 1)[0].replace(/^\/pyodide\//, ''); if (!name || !PYODIDE_ASSETS.includes(name as (typeof PYODIDE_ASSETS)[number])) { next(); return; } response.setHeader( 'Content-Type', name.endsWith('.wasm') ? 'application/wasm' : name.endsWith('.zip') ? 'application/zip' : name.endsWith('.json') ? 'application/json' : 'text/javascript', ); response.end(readFileSync(resolve(pyodideDirectory, name))); }); }, buildStart() { if (isBuild) for (const name of PYODIDE_ASSETS) this.emitFile({ type: 'asset', fileName: `pyodide/${name}`, source: readFileSync(resolve(pyodideDirectory, name)), }); }, }; } export default defineConfig({ root: 'web_platform', base: './', plugins: [react(), localPyodideAssets(), previewCacheHeaders()], publicDir: 'public', build: { outDir: '../web-platform-dist', emptyOutDir: true, target: 'es2022', rollupOptions: { input: { main: resolve(dirname(fileURLToPath(import.meta.url)), 'index.html'), tuning: resolve(dirname(fileURLToPath(import.meta.url)), 'tuning.html'), }, }, modulePreload: { polyfill: false }, }, worker: { format: 'es' }, server: { open: true, fs: { allow: ['..'] } }, test: { globals: true, environment: 'jsdom', setupFiles: './src/test/setup.ts', include: ['src/**/*.test.ts', 'src/**/*.test.tsx'], coverage: { provider: 'v8', reporter: ['text', 'html', 'json-summary'], include: [ 'src/project/cachedFiles.ts', 'src/project/importer.ts', 'src/project/urdfToMjcf.ts', 'src/project/workspace.ts', 'src/simulation/geometry.ts', 'src/stores/useAppStore.ts', 'src/training/LocalTrainingClient.ts', 'src/viewer/interactionMath.ts', 'src/viewer/texturePixels.ts', 'src/viewer/visualizationMath.ts', ], thresholds: { lines: 85, functions: 75, branches: 70, statements: 80 }, }, }, });