/* eslint-disable no-console */ import { lingui } from '@lingui/vite-plugin'; import { isNonEmptyString } from '@sniptt/guards'; import react from '@vitejs/plugin-react-swc'; import wyw from '@wyw-in-js/vite'; import fs from 'fs'; import path from 'path'; import { visualizer } from 'rollup-plugin-visualizer'; import { defineConfig, loadEnv, PluginOption, searchForWorkspaceRoot } from 'vite'; import checker from 'vite-plugin-checker'; import svgr from 'vite-plugin-svgr'; import tsconfigPaths from 'vite-tsconfig-paths'; type Checkers = Parameters[0]; export default defineConfig(({ command, mode }) => { const env = loadEnv(mode, __dirname, ''); const { REACT_APP_SERVER_BASE_URL, VITE_BUILD_SOURCEMAP, VITE_DISABLE_TYPESCRIPT_CHECKER, VITE_DISABLE_ESLINT_CHECKER, VITE_HOST, SSL_CERT_PATH, SSL_KEY_PATH, REACT_APP_PORT, IS_DEBUG_MODE, } = env; const port = isNonEmptyString(REACT_APP_PORT) ? parseInt(REACT_APP_PORT) : 3001; const isBuildCommand = command === 'build'; const tsConfigPath = isBuildCommand ? path.resolve(__dirname, './tsconfig.build.json') : path.resolve(__dirname, './tsconfig.dev.json'); const CHUNK_SIZE_WARNING_LIMIT = 1024 * 1024; // 1MB // Please don't increase this limit for main index chunk // If it gets too big then find modules in the code base // that can be loaded lazily, there are more! const MAIN_CHUNK_SIZE_LIMIT = 4.7 * 1024 * 1024; // 4.7MB for main index chunk const OTHER_CHUNK_SIZE_LIMIT = 5 * 1024 * 1024; // 5MB for other chunks const checkers: Checkers = { overlay: false, }; if (VITE_DISABLE_TYPESCRIPT_CHECKER === 'true') { console.log( `VITE_DISABLE_TYPESCRIPT_CHECKER: ${VITE_DISABLE_TYPESCRIPT_CHECKER}`, ); } if (VITE_DISABLE_ESLINT_CHECKER === 'true') { console.log(`VITE_DISABLE_ESLINT_CHECKER: ${VITE_DISABLE_ESLINT_CHECKER}`); } if (VITE_BUILD_SOURCEMAP === 'true') { console.log(`VITE_BUILD_SOURCEMAP: ${VITE_BUILD_SOURCEMAP}`); } if (VITE_DISABLE_TYPESCRIPT_CHECKER !== 'true') { checkers['typescript'] = { tsconfigPath: tsConfigPath, }; } if (VITE_DISABLE_ESLINT_CHECKER !== 'true') { checkers['eslint'] = { lintCommand: // Appended to packages/twenty-front/.eslintrc.cjs 'eslint ../../packages/twenty-front --report-unused-disable-directives --max-warnings 0 --config .eslintrc.cjs', }; } return { root: __dirname, cacheDir: '../../node_modules/.vite/packages/twenty-front', server: { port: port, ...(VITE_HOST ? { host: VITE_HOST } : {}), ...(SSL_KEY_PATH && SSL_CERT_PATH ? { protocol: 'https', https: { key: fs.readFileSync(env.SSL_KEY_PATH), cert: fs.readFileSync(env.SSL_CERT_PATH), }, } : { protocol: 'http', }), fs: { allow: [ searchForWorkspaceRoot(process.cwd()), '**/@blocknote/core/src/fonts/**', ], }, }, plugins: [ react({ jsxImportSource: '@emotion/react', plugins: [['@lingui/swc-plugin', {}]], }), tsconfigPaths({ projects: ['tsconfig.json'], }), svgr(), lingui({ configPath: path.resolve(__dirname, './lingui.config.ts'), }), checker(checkers), // TODO: fix this, we have to restrict the include to only the components that are using linaria // Otherwise the build will fail because wyw tries to include emotion styled components wyw({ include: [ '**/CurrencyDisplay.tsx', '**/EllipsisDisplay.tsx', '**/ContactLink.tsx', '**/BooleanDisplay.tsx', '**/LinksDisplay.tsx', '**/RoundedLink.tsx', '**/OverflowingTextWithTooltip.tsx', '**/Chip.tsx', '**/Tag.tsx', '**/MultiSelectFieldDisplay.tsx', '**/RatingInput.tsx', '**/RecordTableCellContainer.tsx', '**/RecordTableCellDisplayContainer.tsx', '**/Avatar.tsx', '**/RecordTableBodyDroppable.tsx', '**/RecordTableCellBaseContainer.tsx', '**/RecordTableCellTd.tsx', '**/RecordTableTd.tsx', '**/RecordTableHeaderDragDropColumn.tsx', '**/ActorDisplay.tsx', '**/BooleanDisplay.tsx', '**/CurrencyDisplay.tsx', '**/TextDisplay.tsx', '**/EllipsisDisplay.tsx', '**/AvatarChip.tsx', '**/URLDisplay.tsx', '**/EmailsDisplay.tsx', '**/PhonesDisplay.tsx', '**/MultiSelectDisplay.tsx', ], babelOptions: { presets: ['@babel/preset-typescript', '@babel/preset-react'], }, }), visualizer({ open: true, gzipSize: true, brotliSize: true, filename: 'dist/stats.html', }) as PluginOption, // https://github.com/btd/rollup-plugin-visualizer/issues/162#issuecomment-1538265997, ], optimizeDeps: { exclude: [ '../../node_modules/.vite', '../../node_modules/.cache', '../../node_modules/twenty-ui', ], }, build: { minify: 'esbuild', outDir: 'build', sourcemap: VITE_BUILD_SOURCEMAP === 'true', rollupOptions: { // Don't use manual chunks as it causes many issue // including this one we wasted a lot of time on: // https://github.com/rollup/rollup/issues/2793 output: { // Set chunk size warning limit (in bytes) - warns at 1MB chunkSizeWarningLimit: CHUNK_SIZE_WARNING_LIMIT, // Custom plugin to fail build if chunks exceed max size plugins: [ { name: 'chunk-size-limit', generateBundle(_options, bundle) { const oversizedChunks: string[] = []; Object.entries(bundle).forEach(([fileName, chunk]) => { if (chunk.type === 'chunk' && chunk.code) { const size = Buffer.byteLength(chunk.code, 'utf8'); const isMainChunk = fileName.includes('index') && chunk.isEntry; const sizeLimit = isMainChunk ? MAIN_CHUNK_SIZE_LIMIT : OTHER_CHUNK_SIZE_LIMIT; const limitType = isMainChunk ? 'main' : 'other'; if (size > sizeLimit) { oversizedChunks.push(`${fileName} (${limitType}): ${(size / 1024 / 1024).toFixed(2)}MB (limit: ${(sizeLimit / 1024 / 1024).toFixed(2)}MB)`); } } }); if (oversizedChunks.length > 0) { const errorMessage = `Build failed: The following chunks exceed their size limits:\n${oversizedChunks.map(chunk => ` - ${chunk}`).join('\n')}`; this.error(errorMessage); } } }, // TODO; later - think about prefetching modules such // as date time picker, phone input etc... /* { name: 'add-prefetched-modules', transformIndexHtml(html: string, ctx: { path: string; filename: string; server?: ViteDevServer; bundle?: import('rollup').OutputBundle; chunk?: import('rollup').OutputChunk; }) { const bundles = Object.keys(ctx.bundle ?? {}); let modernBundles = bundles.filter( (bundle) => bundle.endsWith('.map') === false ); // Remove existing files and concatenate them into link tags const prefechBundlesString = modernBundles .filter((bundle) => html.includes(bundle) === false) .map((bundle) => ``) .join(''); // Use regular expression to get the content within const headContent = html.match(/([\s\S]*)<\/head>/)?.[1] ?? ''; // Insert the content of prefetch into the head const newHeadContent = `${headContent}${prefechBundlesString}`; // Replace the original head html = html.replace( /([\s\S]*)<\/head>/, `${newHeadContent}` ); return html; }, }*/ ] } }, }, envPrefix: 'REACT_APP_', define: { _env_: { REACT_APP_SERVER_BASE_URL, }, 'process.env': { REACT_APP_SERVER_BASE_URL, IS_DEBUG_MODE, }, }, css: { modules: { localsConvention: 'camelCaseOnly', }, }, resolve: { alias: { path: 'rollup-plugin-node-polyfills/polyfills/path', // https://github.com/twentyhq/twenty/pull/10782/files // This will likely be migrated to twenty-ui package when built separately '@tabler/icons-react': '@tabler/icons-react/dist/esm/icons/index.mjs', }, }, }; });