Files
Rogeeandrogee a2e4f9a1e8 HH-540: scope iframe policy to Widget page (#123)
* fix(HH-540): scope iframe policy to widget page

* fix(HH-540): reject non-string allowed domains

---------

Co-authored-by: Rogee <rogee@ipao.vip>
2026-08-23 18:37:08 +08:00

219 lines
6.7 KiB
TypeScript

/// <reference types="vitest" />
/**
* 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,
},
});