/// /** * GoChat frontend Vite config — decoupled from Rails. * * Originally Chatwoot used vite-plugin-ruby which auto-discovers entrypoints * from app/javascript/entrypoints/ via config/vite.json. We replaced it with * explicit rollupOptions.input so the frontend builds as a standalone Vite app * that talks to the GoChat Go backend (no Ruby/Rails required). * * Entry points (mirrors app/javascript/entrypoints/): * dashboard — agent dashboard SPA (mounted at #app via /app/*) * widget — customer-facing web widget * portal — help-center portal * sdk — standalone SDK (library mode, built separately) * superadmin — super admin panel * survey — CSAT survey form * v3app — v3 login/signup flow */ import { defineConfig } from 'vite'; import path from 'path'; import vue from '@vitejs/plugin-vue'; import yaml from '@rollup/plugin-yaml'; const isLibraryMode = process.env.BUILD_MODE === 'library'; const isTestMode = process.env.TEST === 'true'; const vueOptions = { template: { compilerOptions: { isCustomElement: (tag: string) => ['ninja-keys'].includes(tag), }, }, }; const resolveAliases = { vue: 'vue/dist/vue.esm-bundler.js', components: path.resolve(__dirname, './app/javascript/dashboard/components'), next: path.resolve(__dirname, './app/javascript/dashboard/components-next'), v3: path.resolve(__dirname, './app/javascript/v3'), dashboard: path.resolve(__dirname, './app/javascript/dashboard'), helpers: path.resolve(__dirname, './app/javascript/shared/helpers'), shared: path.resolve(__dirname, './app/javascript/shared'), survey: path.resolve(__dirname, './app/javascript/survey'), widget: path.resolve(__dirname, './app/javascript/widget'), assets: path.resolve(__dirname, './app/javascript/dashboard/assets'), vuedraggable: path.resolve( __dirname, './node_modules/vuedraggable/src/vuedraggable.js' ), '@material/mwc-icon': path.resolve( __dirname, './app/javascript/shared/components/MaterialIconElement.js' ), }; // Entrypoints that vite-plugin-ruby used to auto-discover const entrypoints = { dashboard: path.resolve(__dirname, './index.html'), widget: path.resolve(__dirname, './widget.html'), portal: path.resolve(__dirname, './app/javascript/entrypoints/portal.js'), superadmin: path.resolve( __dirname, './app/javascript/entrypoints/superadmin.js' ), superadmin_pages: path.resolve( __dirname, './app/javascript/entrypoints/superadmin_pages.js' ), survey: path.resolve(__dirname, './app/javascript/entrypoints/survey.js'), v3app: path.resolve(__dirname, './app/javascript/entrypoints/v3app.js'), }; let plugins = [vue(vueOptions), yaml()]; if (isLibraryMode) { plugins = []; } else if (isTestMode) { plugins = [vue(vueOptions), yaml()]; } export default defineConfig({ plugins, define: { __INTLIFY_JIT_COMPILATION__: true, regeneratorRuntime: 'globalThis.regeneratorRuntime', }, css: { preprocessorOptions: { scss: { api: 'modern-compiler', quietDeps: true, silenceDeprecations: ['import', 'global-builtin'], }, }, }, // Dev server: SPA mode for dashboard + widget.html route server: { host: '0.0.0.0', port: 5000, strictPort: true, // port conflict → refuse to start instead of auto-incrementing // Serve widget.html for /widget path (widget iframe), index.html for SPA routes historyApiFallback: { rewrites: [ { from: /^\/widget/, to: '/widget.html' }, { from: /^\/super_admin/, to: '/super_admin.html' }, ], }, proxy: { '/api': { target: process.env.VITE_API_HOST || 'http://127.0.0.1:3000', changeOrigin: true, }, '/auth': { target: process.env.VITE_API_HOST || 'http://127.0.0.1:3000', changeOrigin: true, }, '/platform': { target: process.env.VITE_API_HOST || 'http://127.0.0.1:3000', changeOrigin: true, }, '/profile': { target: process.env.VITE_API_HOST || 'http://127.0.0.1:3000', changeOrigin: true, }, '/uploads': { target: process.env.VITE_API_HOST || 'http://127.0.0.1:3000', changeOrigin: true, }, '/cable': { target: process.env.VITE_API_HOST || 'http://127.0.0.1:3000', ws: true, changeOrigin: true, }, '/health': { target: process.env.VITE_API_HOST || 'http://127.0.0.1:3000', changeOrigin: true, }, '/runtime-config.js': { target: process.env.VITE_API_HOST || 'http://127.0.0.1:3000', changeOrigin: true, }, '/widget': { target: process.env.VITE_API_HOST || 'http://127.0.0.1:3000', changeOrigin: true, // Let Vite handle the /widget standalone HTML page (rewrite to /widget.html), // proxy everything else (API calls: /widget/init, /widget/conversations, etc.) bypass(req) { if (req.headers.accept?.includes('text/html')) { return '/widget.html'; } }, }, }, }, build: { outDir: 'dist', // Dashboard is a 3 MB entry; warn only when it grows beyond that budget. chunkSizeWarningLimit: 3000, rollupOptions: { input: isLibraryMode ? undefined : entrypoints, output: { // [NOTE] when not in library mode, no new keys will be added or overwritten ...(isLibraryMode ? { dir: 'dist/sdk', entryFileNames: (chunkInfo: { name: string }) => { if (chunkInfo.name === 'sdk') { return 'js/sdk.js'; } return '[name].js'; }, } : {}), inlineDynamicImports: isLibraryMode, // Disable code-splitting for SDK }, }, lib: isLibraryMode ? { entry: path.resolve(__dirname, './app/javascript/entrypoints/sdk.js'), formats: ['iife'] as const, // IIFE format for single file name: 'sdk', } : undefined, }, resolve: { alias: resolveAliases, }, test: { environment: 'jsdom', include: ['app/**/*.{test,spec}.?(c|m)[jt]s?(x)'], coverage: { reporter: ['lcov', 'text'], include: ['app/**/*.js', 'app/**/*.vue'], exclude: [ 'app/**/*.@(spec|stories|routes).js', '**/specs/**/*', '**/i18n/**/*', ], }, globals: true, outputFile: 'coverage/sonar-report.xml', pool: 'threads', poolOptions: { threads: { singleThread: false, }, }, server: { deps: { inline: ['tinykeys', '@material/mwc-icon'], }, }, setupFiles: ['fake-indexeddb/auto', 'vitest.setup.js'], mockReset: true, clearMocks: true, }, });