import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import tailwindcss from '@tailwindcss/vite'
import { viteStaticCopy } from 'vite-plugin-static-copy'
import { VitePWA } from 'vite-plugin-pwa'
import { fileURLToPath, URL } from 'node:url'
import { writeFileSync, readFileSync, readdirSync, statSync, existsSync, unlinkSync } from 'node:fs'
import { join, resolve } from 'node:path'
import { gzipSync } from 'node:zlib'
// Simple inline compression plugin to avoid extra dependencies
const compressionPlugin = () => ({
name: 'compression-plugin',
apply: 'build',
closeBundle: {
order: 'post',
handler: async () => {
const distDir = resolve(process.cwd(), 'dist')
if (!existsSync(distDir)) return
const compressDir = (dir: string) => {
const files = readdirSync(dir)
for (const file of files) {
const filePath = join(dir, file)
const stat = statSync(filePath)
if (stat.isDirectory()) {
compressDir(filePath)
} else if (file.match(/\.(js|css|html|svg|json|vs)$/) && !file.endsWith('.gz')) {
try {
const content = readFileSync(filePath)
if (content.length < 1024) continue // Don't compress very small files
const gzipped = gzipSync(content, { level: 9 })
writeFileSync(`${filePath}.gz`, gzipped)
// Remove original file after successful compression to save space in binary
unlinkSync(filePath)
} catch (e) {
console.error(`Compression failed for ${file}:`, e)
}
}
}
}
compressDir(distDir)
}
}
} as const)
// Release optimization plugin to offload fonts to CDN
const releaseOptimizePlugin = (isOptimize: boolean) => ({
name: 'release-optimize-plugin',
transformIndexHtml(html: string) {
if (isOptimize) {
return html.replace(
'',
` \n \n `
)
}
return html
},
transform(code: string, id: string) {
if (isOptimize && id.includes('index.css')) {
return {
code: code
.replace(/@import\s+["']@fontsource-variable\/inter\/index\.css["'];/g, '/* Removed for CDN */')
.replace(/@import\s+["']@fontsource-variable\/jetbrains-mono\/index\.css["'];/g, '/* Removed for CDN */'),
map: null
}
}
}
})
const isOptimize = process.env.VITE_RELEASE_OPTIMIZE === 'true'
export default defineConfig({
plugins: [
vue(),
tailwindcss(),
!isOptimize && viteStaticCopy({
targets: [
{
src: 'node_modules/monaco-editor/min/vs',
dest: 'assets'
}
]
}),
releaseOptimizePlugin(isOptimize),
compressionPlugin(),
VitePWA({
registerType: 'autoUpdate',
injectRegister: 'auto',
includeAssets: ['favicon.ico', 'logo.svg', 'pwa-icon-192.png', 'pwa-icon-512.png'],
manifest: {
name: 'Baihu Panel',
short_name: 'Baihu',
description: '白虎面板 - 现代化的服务器管理面板',
theme_color: '#ffffff',
background_color: '#ffffff',
display: 'standalone',
icons: [
{
src: 'logo.svg',
sizes: 'any',
type: 'image/svg+xml',
purpose: 'any maskable'
},
]
},
workbox: {
maximumFileSizeToCacheInBytes: 10 * 1024 * 1024 // 10MB to allow monaco-editor files
},
devOptions: {
enabled: true
}
})
],
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url))
}
},
server: {
proxy: {
'/api': {
target: 'http://localhost:8052',
changeOrigin: true,
ws: true
},
'/openapi': {
target: 'http://localhost:8052',
changeOrigin: true
}
}
},
// 使用相对路径,这样动态导入的模块也会使用相对路径
// 浏览器会根据当前页面 URL 解析相对路径
base: './',
build: {
reportCompressedSize: true,
sourcemap: false,
rollupOptions: {
output: {
// 防止生成以 _ 开头的文件,导致被 Cloudflare Pages 或 Github Pages 等静态托管平台拦截并降级返回 HTML
chunkFileNames: 'assets/[name]-[hash].js',
entryFileNames: 'assets/[name]-[hash].js',
assetFileNames: 'assets/[name]-[hash].[ext]',
sanitizeFileName(name) {
// 仿制 Rollup 的默认 sanitizeFileName,将特殊字符替换为 '-'
let safeName = name.replace(/[\0?*:|"<>\/\\&=$]/g, '-')
// 去除开头可能引起静态托管平台屏蔽的下划线 '_'
return safeName.replace(/^_/, '')
},
manualChunks(id) {
if (id.includes('node_modules')) {
// 编辑器相关
if (id.includes('monaco-editor') || id.includes('@guolao/vue-monaco-editor')) {
return 'vendor-monaco'
}
// 图表相关
if (id.includes('apexcharts') || id.includes('chart.js') || id.includes('vue3-apexcharts') || id.includes('vue-chartjs')) {
return 'vendor-charts'
}
// 终端相关
if (id.includes('@xterm/xterm') || id.includes('xterm') || id.includes('ansi-to-html') || id.includes('ansi-to-vue3')) {
return 'vendor-terminal'
}
// 基础 UI 库
if (id.includes('radix-vue') || id.includes('reka-ui') || id.includes('lucide-vue-next') || id.includes('date-fns')) {
return 'vendor-ui'
}
// 其他基础依赖
return 'vendor'
}
}
}
}
},
define: {
__MONACO_CDN__: isOptimize ? JSON.stringify('https://registry.npmmirror.com/monaco-editor/0.52.0/files/min/vs') : 'null'
}
})