From 64d4c0c6bec1bf3f53c0644ce01cde20202f20af Mon Sep 17 00:00:00 2001 From: Nawaz Dhandala Date: Wed, 21 Jan 2026 11:29:29 +0000 Subject: [PATCH] feat: migrate from esbuild to Vite for build configuration across services --- Accounts/esbuild.config.js | 12 -- Accounts/package.json | 6 +- Accounts/vite.config.ts | 6 + AdminDashboard/esbuild.config.js | 12 -- AdminDashboard/package.json | 7 +- AdminDashboard/vite.config.ts | 6 + Common/UI/esbuild-config.js | 245 ------------------------------- Common/UI/vite.config.ts | 184 +++++++++++++++++++++++ Common/package.json | 2 + Dashboard/esbuild.config.js | 12 -- Dashboard/package.json | 7 +- Dashboard/vite.config.ts | 6 + StatusPage/esbuild.config.js | 12 -- StatusPage/package.json | 6 +- StatusPage/vite.config.ts | 6 + 15 files changed, 222 insertions(+), 307 deletions(-) delete mode 100644 Accounts/esbuild.config.js create mode 100644 Accounts/vite.config.ts delete mode 100644 AdminDashboard/esbuild.config.js create mode 100644 AdminDashboard/vite.config.ts delete mode 100644 Common/UI/esbuild-config.js create mode 100644 Common/UI/vite.config.ts delete mode 100644 Dashboard/esbuild.config.js create mode 100644 Dashboard/vite.config.ts delete mode 100644 StatusPage/esbuild.config.js create mode 100644 StatusPage/vite.config.ts diff --git a/Accounts/esbuild.config.js b/Accounts/esbuild.config.js deleted file mode 100644 index 5bd2a9a9d3..0000000000 --- a/Accounts/esbuild.config.js +++ /dev/null @@ -1,12 +0,0 @@ -const { createConfig, build, watch } = require('Common/UI/esbuild-config'); - -const config = createConfig({ - serviceName: 'Accounts', - publicPath: '/accounts/dist/', -}); - -if (process.argv.includes('--watch')) { - watch(config, 'Accounts'); -} else { - build(config, 'Accounts'); -} diff --git a/Accounts/package.json b/Accounts/package.json index 286d501eb7..d78e321b7c 100644 --- a/Accounts/package.json +++ b/Accounts/package.json @@ -7,10 +7,10 @@ "url": "https://github.com/OneUptime/oneuptime" }, "scripts": { - "dev-build": "NODE_ENV=development node esbuild.config.js", + "dev-build": "NODE_ENV=development npx vite build", "dev": "npx nodemon", - "build": "NODE_ENV=production node esbuild.config.js", - "analyze": "analyze=true NODE_ENV=production node esbuild.config.js", + "build": "NODE_ENV=production npx vite build", + "analyze": "analyze=true NODE_ENV=production npx vite build", "test": "", "compile": "tsc", "clear-modules": "rm -rf node_modules && rm package-lock.json && npm install", diff --git a/Accounts/vite.config.ts b/Accounts/vite.config.ts new file mode 100644 index 0000000000..57f35078a3 --- /dev/null +++ b/Accounts/vite.config.ts @@ -0,0 +1,6 @@ +import { createViteConfig } from 'Common/UI/vite.config'; + +export default createViteConfig({ + serviceName: 'Accounts', + publicPath: '/accounts/dist/', +}); diff --git a/AdminDashboard/esbuild.config.js b/AdminDashboard/esbuild.config.js deleted file mode 100644 index 39746e5533..0000000000 --- a/AdminDashboard/esbuild.config.js +++ /dev/null @@ -1,12 +0,0 @@ -const { createConfig, build, watch } = require('Common/UI/esbuild-config'); - -const config = createConfig({ - serviceName: 'AdminDashboard', - publicPath: '/admin/dist/', -}); - -if (process.argv.includes('--watch')) { - watch(config, 'AdminDashboard'); -} else { - build(config, 'AdminDashboard'); -} diff --git a/AdminDashboard/package.json b/AdminDashboard/package.json index a8aed6de37..23d662a1f4 100644 --- a/AdminDashboard/package.json +++ b/AdminDashboard/package.json @@ -15,12 +15,11 @@ "react-router-dom": "^6.30.2" }, "scripts": { - "dev-build": "NODE_ENV=development node esbuild.config.js", + "dev-build": "NODE_ENV=development npx vite build", "dev": "npx nodemon", - "build": "NODE_ENV=production node esbuild.config.js", - "analyze": "analyze=true NODE_ENV=production node esbuild.config.js", + "build": "NODE_ENV=production npx vite build", + "analyze": "analyze=true NODE_ENV=production npx vite build", "test": "react-app-rewired test", - "eject": "echo 'esbuild does not require eject'", "compile": "tsc", "clear-modules": "rm -rf node_modules && rm package-lock.json && npm install", "start": "node --require ts-node/register Serve.ts", diff --git a/AdminDashboard/vite.config.ts b/AdminDashboard/vite.config.ts new file mode 100644 index 0000000000..5d587bd9e6 --- /dev/null +++ b/AdminDashboard/vite.config.ts @@ -0,0 +1,6 @@ +import { createViteConfig } from 'Common/UI/vite.config'; + +export default createViteConfig({ + serviceName: 'AdminDashboard', + publicPath: '/admin/dist/', +}); diff --git a/Common/UI/esbuild-config.js b/Common/UI/esbuild-config.js deleted file mode 100644 index 54374a45d0..0000000000 --- a/Common/UI/esbuild-config.js +++ /dev/null @@ -1,245 +0,0 @@ -/** - * Shared esbuild configuration factory for OneUptime frontend services - * This creates consistent build configurations across all services - */ - -const esbuild = require('esbuild'); -const path = require('path'); -const fs = require('fs'); -const dotenv = require('dotenv'); - -function createRefractorCompatibilityPlugin() { - const candidateRoots = [ - path.resolve(__dirname, '../node_modules/refractor'), - path.resolve(__dirname, '../../node_modules/refractor'), - ]; - - const refractorRoot = candidateRoots.find((packagePath) => fs.existsSync(packagePath)); - - if (!refractorRoot) { - throw new Error('Unable to locate refractor package for esbuild compatibility plugin.'); - } - - return { - name: 'refractor-compatibility', - setup(build) { - build.onResolve({ filter: /^refractor\/lib\// }, (args) => { - const relativePath = args.path.replace(/^refractor\/lib\//, ''); - const candidatePath = path.join(refractorRoot, 'lib', `${relativePath}.js`); - return { path: candidatePath }; - }); - - build.onResolve({ filter: /^refractor\/lang\// }, (args) => { - const relativePath = args.path.replace(/^refractor\/lang\//, ''); - const filename = relativePath.endsWith('.js') ? relativePath : `${relativePath}.js`; - const candidatePath = path.join(refractorRoot, 'lang', filename); - return { path: candidatePath }; - }); - }, - }; -} - -// CSS Plugin to handle CSS/SCSS files -function createCSSPlugin() { - return { - name: 'css', - setup(build) { - build.onLoad({ filter: /\.s?css$/ }, async (args) => { - const sass = require('sass'); - const fs = require('fs'); - - let contents = fs.readFileSync(args.path, 'utf8'); - - // Compile SCSS to CSS if it's a SCSS file - if (args.path.endsWith('.scss') || args.path.endsWith('.sass')) { - try { - const result = sass.compile(args.path); - contents = result.css; - } catch (error) { - console.error(`SCSS compilation error in ${args.path}:`, error); - throw error; - } - } - - // Return CSS as a string that will be injected into the page - return { - contents: ` - const style = document.createElement('style'); - style.textContent = ${JSON.stringify(contents)}; - document.head.appendChild(style); - `, - loader: 'js', - }; - }); - }, - }; -} - -// File loader plugin for assets -function createFileLoaderPlugin() { - return { - name: 'file-loader', - setup(build) { - build.onLoad({ filter: /\.(png|jpe?g|gif|svg|woff|woff2|eot|ttf|otf)$/ }, async (args) => { - const fs = require('fs'); - const path = require('path'); - - const contents = fs.readFileSync(args.path); - const filename = path.basename(args.path); - const ext = path.extname(filename); - - // For development, we'll use data URLs for simplicity - // In production, you might want to copy files to the output directory - const mimeTypes = { - '.png': 'image/png', - '.jpg': 'image/jpeg', - '.jpeg': 'image/jpeg', - '.gif': 'image/gif', - '.svg': 'image/svg+xml', - '.woff': 'font/woff', - '.woff2': 'font/woff2', - '.eot': 'application/vnd.ms-fontobject', - '.ttf': 'font/ttf', - '.otf': 'font/otf', - }; - - const mimeType = mimeTypes[ext.toLowerCase()] || 'application/octet-stream'; - const dataUrl = `data:${mimeType};base64,${contents.toString('base64')}`; - - return { - contents: `export default ${JSON.stringify(dataUrl)};`, - loader: 'js', - }; - }); - }, - }; -} - -// Read environment variables from .env file -function readEnvFile(pathToFile) { - if (!fs.existsSync(pathToFile)) { - console.warn(`Environment file not found: ${pathToFile}`); - return {}; - } - - const parsed = dotenv.config({ path: pathToFile }).parsed || {}; - const env = {}; - - for (const key in parsed) { - env[`process.env.${key}`] = JSON.stringify(parsed[key]); - } - - return env; -} - -/** - * Create esbuild configuration for a service - * @param {Object} options - Configuration options - * @param {string} options.serviceName - Name of the service (dashboard, accounts, admin, status-page) - * @param {string} options.publicPath - Public path for assets - * @param {string} [options.entryPoint] - Entry point file (defaults to './src/Index.tsx') - * @param {string} [options.outdir] - Output directory (defaults to './public/dist') - * @param {Object} [options.additionalDefines] - Additional define variables - * @param {Array} [options.additionalExternal] - Additional external modules - * @param {Object} [options.additionalAlias] - Additional aliases - */ -function createConfig(options) { - const { - serviceName, - publicPath, - entryPoint = './src/Index.tsx', - outdir = './public/dist', - additionalDefines = {}, - additionalExternal = [], - additionalAlias = {} - } = options; - - const isDev = process.env.NODE_ENV !== 'production'; - const isAnalyze = process.env.analyze === 'true'; - - return { - entryPoints: [entryPoint], - bundle: true, - outdir, - format: 'esm', // Changed from 'iife' to 'esm' to support splitting - platform: 'browser', - target: 'es2017', - sourcemap: isDev ? 'inline' : false, - minify: false, - splitting: true, // Now supported with ESM format - publicPath, - define: { - 'process.env.NODE_ENV': JSON.stringify(isDev ? 'development' : 'production'), - ...additionalDefines, - }, - external: ['react-native-sqlite-storage', ...additionalExternal], - alias: { - 'react': path.resolve('./node_modules/react'), - ...additionalAlias, - }, - plugins: [createRefractorCompatibilityPlugin(), createCSSPlugin(), createFileLoaderPlugin()], - loader: { - '.tsx': 'tsx', - '.ts': 'ts', - '.jsx': 'jsx', - '.js': 'js', - '.json': 'json', - }, - resolveExtensions: ['.tsx', '.ts', '.jsx', '.js', '.json', '.css', '.scss'], - metafile: isAnalyze, - }; -} - -/** - * Build function that handles the build process - * @param {Object} config - esbuild configuration - * @param {string} serviceName - Name of the service for logging - */ -async function build(config, serviceName) { - const isAnalyze = process.env.analyze === 'true'; - - try { - const result = await esbuild.build(config); - - if (isAnalyze && result.metafile) { - const analyzeText = await esbuild.analyzeMetafile(result.metafile); - console.log(`\nšŸ“Š Bundle analysis for ${serviceName}:`); - console.log(analyzeText); - - // Write metafile for external analysis tools - const metafilePath = path.join(config.outdir, 'metafile.json'); - fs.writeFileSync(metafilePath, JSON.stringify(result.metafile, null, 2)); - console.log(`šŸ“ Metafile written to: ${metafilePath}`); - } - - console.log(`āœ… ${serviceName} build completed successfully`); - } catch (error) { - console.error(`āŒ ${serviceName} build failed:`, error); - process.exit(1); - } -} - -/** - * Watch function that handles the watch process - * @param {Object} config - esbuild configuration - * @param {string} serviceName - Name of the service for logging - */ -async function watch(config, serviceName) { - try { - const context = await esbuild.context(config); - await context.watch(); - console.log(`šŸ‘€ Watching ${serviceName} for changes...`); - } catch (error) { - console.error(`āŒ ${serviceName} watch failed:`, error); - process.exit(1); - } -} - -module.exports = { - createConfig, - build, - watch, - createCSSPlugin, - createFileLoaderPlugin, - readEnvFile, -}; diff --git a/Common/UI/vite.config.ts b/Common/UI/vite.config.ts new file mode 100644 index 0000000000..76ff1d172d --- /dev/null +++ b/Common/UI/vite.config.ts @@ -0,0 +1,184 @@ +/** + * Shared Vite configuration factory for OneUptime frontend services + * This creates consistent build configurations across all services with controlled code splitting + */ + +import { defineConfig, UserConfig, Plugin } from 'vite'; +import react from '@vitejs/plugin-react'; +import path from 'path'; +import fs from 'fs'; + +interface ServiceConfig { + serviceName: string; + publicPath: string; + outDir?: string; + entryPoint?: string; +} + +/** + * Create a Vite plugin for refractor compatibility + * Handles resolving refractor/lib/* and refractor/lang/* imports + */ +function createRefractorCompatibilityPlugin(): Plugin { + const candidateRoots = [ + path.resolve(__dirname, '../node_modules/refractor'), + path.resolve(__dirname, '../../node_modules/refractor'), + ]; + + const refractorRoot = candidateRoots.find((packagePath) => fs.existsSync(packagePath)); + + return { + name: 'refractor-compatibility', + resolveId(source: string) { + if (!refractorRoot) { + return null; + } + + if (source.startsWith('refractor/lib/')) { + const relativePath = source.replace(/^refractor\/lib\//, ''); + const candidatePath = path.join(refractorRoot, 'lib', `${relativePath}.js`); + if (fs.existsSync(candidatePath)) { + return candidatePath; + } + } + + if (source.startsWith('refractor/lang/')) { + const relativePath = source.replace(/^refractor\/lang\//, ''); + const filename = relativePath.endsWith('.js') ? relativePath : `${relativePath}.js`; + const candidatePath = path.join(refractorRoot, 'lang', filename); + if (fs.existsSync(candidatePath)) { + return candidatePath; + } + } + + return null; + }, + }; +} + +/** + * Create Vite configuration for a service + */ +export function createViteConfig(config: ServiceConfig): UserConfig { + const { + serviceName, + publicPath, + outDir = './public/dist', + entryPoint = './src/Index.tsx' + } = config; + + const isDev = process.env.NODE_ENV !== 'production'; + const isAnalyze = process.env.analyze === 'true'; + + return defineConfig({ + plugins: [ + react(), + createRefractorCompatibilityPlugin(), + ], + base: publicPath, + build: { + target: 'es2017', + outDir, + emptyOutDir: true, + sourcemap: isDev ? 'inline' : false, + minify: !isDev, + rollupOptions: { + input: entryPoint, + output: { + entryFileNames: 'Index.js', + chunkFileNames: '[name].js', + assetFileNames: '[name][extname]', + manualChunks: (id: string) => { + // Vendor chunk: React ecosystem + if (id.includes('node_modules/react/') || + id.includes('node_modules/react-dom/') || + id.includes('node_modules/react-router-dom/') || + id.includes('node_modules/react-router/') || + id.includes('node_modules/@remix-run/router/')) { + return 'vendor'; + } + + // UI Components chunk: Common UI library + if (id.includes('Common/UI/Components/') || + id.includes('Common/UI/Utils/')) { + return 'ui'; + } + + // Charting libraries chunk + if (id.includes('node_modules/recharts/') || + id.includes('node_modules/d3-') || + id.includes('node_modules/victory-')) { + return 'charts'; + } + + // Monaco editor chunk (large) + if (id.includes('node_modules/@monaco-editor/') || + id.includes('node_modules/monaco-editor/')) { + return 'monaco'; + } + + // Flow/diagram libraries + if (id.includes('node_modules/reactflow/') || + id.includes('node_modules/@reactflow/') || + id.includes('node_modules/elkjs/')) { + return 'flow'; + } + + // Syntax highlighting chunk + if (id.includes('node_modules/react-syntax-highlighter/') || + id.includes('node_modules/refractor/') || + id.includes('node_modules/prismjs/') || + id.includes('node_modules/highlight.js/')) { + return 'syntax'; + } + + // Date/time utilities + if (id.includes('node_modules/moment/') || + id.includes('node_modules/moment-timezone/')) { + return 'datetime'; + } + + // Markdown processing + if (id.includes('node_modules/react-markdown/') || + id.includes('node_modules/remark-') || + id.includes('node_modules/rehype-') || + id.includes('node_modules/unified/') || + id.includes('node_modules/marked/')) { + return 'markdown'; + } + + // Other large vendor libraries get grouped together + if (id.includes('node_modules/')) { + return 'vendor-misc'; + } + + // Let Vite handle code splitting for dynamic imports (lazy routes) + return undefined; + }, + }, + external: ['react-native-sqlite-storage'], + }, + // Report compressed sizes in analyze mode + reportCompressedSize: isAnalyze, + }, + resolve: { + alias: { + 'react': path.resolve('./node_modules/react'), + 'Common': path.resolve('../Common'), + }, + }, + css: { + preprocessorOptions: { + scss: {}, + }, + }, + // Define environment variables + define: { + 'process.env.NODE_ENV': JSON.stringify(isDev ? 'development' : 'production'), + }, + // Log level for build output + logLevel: isAnalyze ? 'info' : 'warn', + }); +} + +export default createViteConfig; diff --git a/Common/package.json b/Common/package.json index 3d31eab552..cf4071f1b1 100644 --- a/Common/package.json +++ b/Common/package.json @@ -20,6 +20,8 @@ "author": "OneUptime (https://oneuptime.com/)", "license": "Apache-2.0", "devDependencies": { + "@vitejs/plugin-react": "^4.3.0", + "vite": "^5.4.0", "@faker-js/faker": "^8.0.2", "@testing-library/jest-dom": "^5.16.5", "@testing-library/react": "^13.3.0", diff --git a/Dashboard/esbuild.config.js b/Dashboard/esbuild.config.js deleted file mode 100644 index f62105e2bc..0000000000 --- a/Dashboard/esbuild.config.js +++ /dev/null @@ -1,12 +0,0 @@ -const { createConfig, build, watch } = require('Common/UI/esbuild-config'); - -const config = createConfig({ - serviceName: 'Dashboard', - publicPath: '/dashboard/dist/', -}); - -if (process.argv.includes('--watch')) { - watch(config, 'Dashboard'); -} else { - build(config, 'Dashboard'); -} diff --git a/Dashboard/package.json b/Dashboard/package.json index 0781733c09..126c702f0c 100644 --- a/Dashboard/package.json +++ b/Dashboard/package.json @@ -8,12 +8,11 @@ }, "scripts": { "generate-sw": "node scripts/generate-sw.js", - "dev-build": "npm run generate-sw && NODE_ENV=development node esbuild.config.js", + "dev-build": "npm run generate-sw && NODE_ENV=development npx vite build", "dev": "npx nodemon", - "build": "npm run generate-sw && NODE_ENV=production node esbuild.config.js", - "analyze": "npm run generate-sw && analyze=true NODE_ENV=production node esbuild.config.js", + "build": "npm run generate-sw && NODE_ENV=production npx vite build", + "analyze": "npm run generate-sw && analyze=true NODE_ENV=production npx vite build", "test": "react-app-rewired test", - "eject": "echo 'esbuild does not require eject'", "compile": "npm run generate-sw && tsc", "clear-modules": "rm -rf node_modules && rm package-lock.json && npm install", "start": "node --require ts-node/register Serve.ts", diff --git a/Dashboard/vite.config.ts b/Dashboard/vite.config.ts new file mode 100644 index 0000000000..4aa73dcb6b --- /dev/null +++ b/Dashboard/vite.config.ts @@ -0,0 +1,6 @@ +import { createViteConfig } from 'Common/UI/vite.config'; + +export default createViteConfig({ + serviceName: 'Dashboard', + publicPath: '/dashboard/dist/', +}); diff --git a/StatusPage/esbuild.config.js b/StatusPage/esbuild.config.js deleted file mode 100644 index 68ca906bd7..0000000000 --- a/StatusPage/esbuild.config.js +++ /dev/null @@ -1,12 +0,0 @@ -const { createConfig, build, watch } = require('Common/UI/esbuild-config'); - -const config = createConfig({ - serviceName: 'StatusPage', - publicPath: '/status-page/dist/', -}); - -if (process.argv.includes('--watch')) { - watch(config, 'StatusPage'); -} else { - build(config, 'StatusPage'); -} diff --git a/StatusPage/package.json b/StatusPage/package.json index dfedefa584..4658cb2b39 100644 --- a/StatusPage/package.json +++ b/StatusPage/package.json @@ -8,9 +8,9 @@ }, "scripts": { "dev": "npx nodemon", - "build": "NODE_ENV=production node esbuild.config.js", - "dev-build": "NODE_ENV=development node esbuild.config.js", - "analyze": "analyze=true NODE_ENV=production node esbuild.config.js", + "build": "NODE_ENV=production npx vite build", + "dev-build": "NODE_ENV=development npx vite build", + "analyze": "analyze=true NODE_ENV=production npx vite build", "test": "", "compile": "tsc", "clear-modules": "rm -rf node_modules && rm package-lock.json && npm install", diff --git a/StatusPage/vite.config.ts b/StatusPage/vite.config.ts new file mode 100644 index 0000000000..b670e578f2 --- /dev/null +++ b/StatusPage/vite.config.ts @@ -0,0 +1,6 @@ +import { createViteConfig } from 'Common/UI/vite.config'; + +export default createViteConfig({ + serviceName: 'StatusPage', + publicPath: '/status-page/dist/', +});