import path from 'node:path'; import { fileURLToPath, URL } from 'node:url'; import ui from '@nuxt/ui/vite'; import tailwindcss from '@tailwindcss/vite'; import vue from '@vitejs/plugin-vue'; import { defineConfig } from 'vite'; import removeConsole from 'vite-plugin-remove-console'; import scopeTailwindToUnapi from './postcss/scopeTailwindToUnapi'; import { serveStaticHtml } from './vite-plugin-serve-static'; const dropConsole = process.env.VITE_ALLOW_CONSOLE_LOGS !== 'true'; console.log(dropConsole ? 'WARN: Console logs are disabled' : 'INFO: Console logs are enabled'); const __dirname = path.dirname(fileURLToPath(import.meta.url)); const assetsDir = path.join(__dirname, '../api/dev/webGui/'); /** * Used to avoid redeclaring variables in the webgui codebase. */ function terserReservations(inputStr: string) { const combinations = ['ace']; // Add 1-character combinations for (let i = 0; i < inputStr.length; i++) { combinations.push(inputStr[i]); } // Add 2-character combinations for (let i = 0; i < inputStr.length; i++) { for (let j = 0; j < inputStr.length; j++) { combinations.push(inputStr[i] + inputStr[j]); } } return combinations; } const charsToReserve = '_$abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'; /** * Shared terser options for consistent minification */ const sharedTerserOptions = { mangle: { reserved: terserReservations(charsToReserve), toplevel: true, }, }; /** * Shared define configuration */ const sharedDefine = { 'globalThis.__DEV__': JSON.stringify(process.env.NODE_ENV === 'development'), __VUE_PROD_DEVTOOLS__: JSON.stringify(false), 'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV || 'production'), 'process.env': JSON.stringify({}), }; // https://vitejs.dev/config/ export default defineConfig({ plugins: [ tailwindcss(), vue({ template: { compilerOptions: { // Treat all unraid-* components as custom elements isCustomElement: (tag) => tag.startsWith('unraid-'), }, }, }), ui(), serveStaticHtml(), // Serve static test pages // Remove console logs in production ...(dropConsole ? [ removeConsole({ includes: ['log', 'info', 'debug'], }), ] : []), ], css: { postcss: { plugins: [scopeTailwindToUnapi()], }, }, resolve: { alias: { '@': fileURLToPath(new URL('./src', import.meta.url)), '~': fileURLToPath(new URL('./src', import.meta.url)), '~~': fileURLToPath(new URL('./', import.meta.url)), '~/': fileURLToPath(new URL('./src/', import.meta.url)), }, }, optimizeDeps: { include: ['ajv', 'ajv-errors'], esbuildOptions: { define: { global: 'globalThis', }, }, }, define: { ...sharedDefine, 'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV || 'production'), }, publicDir: false, // Don't copy public files to dist build: { outDir: 'dist', emptyOutDir: true, manifest: false, // Disable Vite's manifest since we generate our own lib: { entry: fileURLToPath(new URL('./src/components/Wrapper/auto-mount.ts', import.meta.url)), name: 'UnraidStandaloneApps', fileName: 'standalone-apps', formats: ['es'], }, rollupOptions: { output: { format: 'es', entryFileNames: 'standalone-apps-[hash].js', chunkFileNames: '[name]-[hash].js', assetFileNames: (assetInfo) => { if (assetInfo.name?.endsWith('.css')) { return 'standalone-apps-[hash][extname]'; } return '[name]-[hash][extname]'; }, inlineDynamicImports: false, }, }, cssCodeSplit: false, // Bundle all CSS together minify: 'terser', terserOptions: sharedTerserOptions, }, server: { port: 3000, proxy: { '/graphql': { target: 'http://localhost:3001', changeOrigin: true, ws: true, secure: false, // Important: preserve the host header configure: (proxy) => { proxy.on('proxyReq', (proxyReq) => { proxyReq.setHeader('X-Forwarded-Host', 'localhost:3000'); proxyReq.setHeader('X-Forwarded-Proto', 'http'); proxyReq.setHeader('X-Forwarded-For', '127.0.0.1'); }); // Handle connection errors gracefully proxy.on('error', (err: Error, _req: unknown, res: unknown) => { console.warn('[Vite] GraphQL proxy error (API server may not be running):', err.message); // Check if res has writeHead method (it's an HTTP response, not a socket) const httpRes = res as { writeHead?: (statusCode: number, headers: Record) => void; end?: (data: string) => void; }; if ( httpRes && typeof httpRes.writeHead === 'function' && typeof httpRes.end === 'function' ) { httpRes.writeHead(503, { 'Content-Type': 'application/json', }); httpRes.end( JSON.stringify({ error: 'GraphQL API server not available', message: 'Please start the API server on port 3001', }) ); } }); }, }, '/webGui': { target: `file://${assetsDir}`, changeOrigin: true, }, }, // Configure static file serving fs: { strict: false, allow: ['..'], // Allow serving files outside of root }, }, });