1/// <reference types="vitest" /> 2import { defineConfig, loadEnv } from 'vite'; 3import vue from '@vitejs/plugin-vue'; 4import basicSsl from '@vitejs/plugin-basic-ssl'; 5import svgLoader from 'vite-svg-loader'; 6import viteCompression from 'vite-plugin-compression'; 7import { fileURLToPath, URL } from 'node:url'; 8import path from 'node:path'; 9import fs from 'node:fs'; 10 11// Plugin to resolve directory imports to index.js (like Webpack does) 12function resolveDirectoryIndex() { 13 return { 14 name: 'resolve-directory-index', 15 resolveId(source, importer) { 16 if (!importer || source.startsWith('\0')) return null; 17 18 // Resolve the full path 19 let resolved; 20 if (source.startsWith('@/')) { 21 resolved = path.resolve(__dirname, 'src', source.slice(2)); 22 } else if (source.startsWith('./') || source.startsWith('../')) { 23 resolved = path.resolve(path.dirname(importer), source); 24 } else { 25 return null; 26 } 27 28 // Check if it's a directory with an index.js 29 try { 30 const stats = fs.statSync(resolved); 31 if (stats.isDirectory()) { 32 const indexPath = path.join(resolved, 'index.js'); 33 if (fs.existsSync(indexPath)) { 34 return indexPath; 35 } 36 } 37 } catch { 38 // Path doesn't exist, let Vite handle it 39 } 40 41 return null; 42 }, 43 }; 44} 45 46export default defineConfig(({ mode }) => { 47 // Load env file based on `mode` in the current working directory. 48 const env = loadEnv(mode, process.cwd(), ''); 49 50 const envName = env.VITE_ENV_NAME; 51 const hasCustomStyles = env.CUSTOM_STYLES === 'true'; 52 const hasCustomStore = env.CUSTOM_STORE === 'true'; 53 const hasCustomRouter = env.CUSTOM_ROUTER === 'true'; 54 const hasCustomAppNav = env.CUSTOM_APP_NAV === 'true'; 55 56 // Build SCSS additionalData for prepending imports 57 const scssAdditionalData = (() => { 58 if (hasCustomStyles && envName !== undefined) { 59 return ` 60 @import "@/assets/styles/bmc/helpers"; 61 @import "@/env/assets/styles/_${envName}"; 62 @import "@/assets/styles/bootstrap/_helpers"; 63 `; 64 } else { 65 return ` 66 @import "@/assets/styles/bmc/helpers"; 67 @import "@/assets/styles/bootstrap/_helpers"; 68 `; 69 } 70 })(); 71 72 // Build custom aliases for environment-specific overrides 73 const customAliases = {}; 74 if (envName !== undefined) { 75 if (hasCustomStore) { 76 // If env has custom store, resolve all store modules 77 customAliases['./store'] = path.resolve( 78 __dirname, 79 `src/env/store/${envName}.js`, 80 ); 81 customAliases['../store'] = path.resolve( 82 __dirname, 83 `src/env/store/${envName}.js`, 84 ); 85 } 86 if (hasCustomRouter) { 87 // If env has custom router, resolve routes 88 customAliases['./routes'] = path.resolve( 89 __dirname, 90 `src/env/router/${envName}.js`, 91 ); 92 } 93 if (hasCustomAppNav) { 94 // If env has custom AppNavigation 95 customAliases['./AppNavigationMixin'] = path.resolve( 96 __dirname, 97 `src/env/components/AppNavigation/${envName}.js`, 98 ); 99 } 100 } 101 102 // Helper to inject auth token from cookie 103 const injectAuthToken = (proxyReq, req) => { 104 const cookies = req.headers.cookie; 105 if (cookies) { 106 const match = cookies.match(/X-Auth-Token=([^;]+)/); 107 if (match) { 108 proxyReq.setHeader('X-Auth-Token', match[1]); 109 } 110 } 111 }; 112 113 // Helper to remove HSTS header 114 const removeHsts = (proxyRes) => { 115 delete proxyRes.headers['strict-transport-security']; 116 }; 117 118 // Check if HTTPS should be enabled (default: true) 119 const useHttps = env.DEV_HTTPS !== 'false'; 120 121 return { 122 plugins: [ 123 resolveDirectoryIndex(), 124 vue(), 125 svgLoader({ 126 defaultImport: 'component', 127 }), 128 // Enable HTTPS with auto-generated self-signed certificate 129 ...(useHttps ? [basicSsl()] : []), 130 // Compression for production builds 131 ...(mode === 'production' 132 ? [ 133 viteCompression({ 134 deleteOriginFile: true, 135 algorithm: 'gzip', 136 }), 137 ] 138 : []), 139 ], 140 141 resolve: { 142 alias: { 143 '@': fileURLToPath(new URL('./src', import.meta.url)), 144 ...customAliases, 145 }, 146 // Allow importing without extensions (like Vue CLI did) 147 extensions: ['.mjs', '.js', '.ts', '.jsx', '.tsx', '.json', '.vue'], 148 }, 149 150 css: { 151 preprocessorOptions: { 152 scss: { 153 additionalData: scssAdditionalData, 154 // Silence Sass deprecation warnings from Bootstrap and other dependencies. 155 // Bootstrap is working on a long-term fix for Dart Sass compatibility. 156 // See: https://getbootstrap.com/docs/5.3/customize/sass/#importing 157 silenceDeprecations: ['import'], 158 quietDeps: true, 159 }, 160 sass: { 161 additionalData: scssAdditionalData, 162 silenceDeprecations: ['import'], 163 quietDeps: true, 164 }, 165 }, 166 }, 167 168 define: { 169 // Vue 3 compile-time feature flags 170 __VUE_OPTIONS_API__: true, 171 __VUE_PROD_DEVTOOLS__: false, 172 __VUE_PROD_HYDRATION_MISMATCH_DETAILS__: false, 173 // Expose session storage toggle to client code 174 'import.meta.env.VITE_STORE_SESSION': JSON.stringify( 175 env.VITE_STORE_SESSION || env.STORE_SESSION || '', 176 ), 177 }, 178 179 server: { 180 port: 8000, 181 // HTTPS is enabled via basicSsl plugin above 182 // Disable HMR WebSocket to avoid conflicts with app websockets 183 hmr: { 184 path: '/ws_hmr', 185 }, 186 proxy: { 187 '/redfish': { 188 target: env.BASE_URL, 189 changeOrigin: true, 190 secure: false, 191 configure: (proxy) => { 192 proxy.on('proxyReq', (proxyReq, req) => { 193 injectAuthToken(proxyReq, req); 194 195 // Detect if this is a browser navigation vs an API call 196 const isApiCall = 197 req.headers['x-requested-with'] === 'XMLHttpRequest'; 198 199 if (!isApiCall) { 200 proxyReq.setHeader( 201 'Accept', 202 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8', 203 ); 204 proxyReq.removeHeader('accept-encoding'); 205 } 206 207 // Fix referer to match BMC host 208 if (req.headers.referer && env.BASE_URL) { 209 try { 210 const refererUrl = new URL(req.headers.referer); 211 const bmcUrl = new URL(env.BASE_URL); 212 refererUrl.protocol = bmcUrl.protocol; 213 refererUrl.hostname = bmcUrl.hostname; 214 refererUrl.port = bmcUrl.port; 215 proxyReq.setHeader('Referer', refererUrl.toString()); 216 } catch (e) { 217 // If URL parsing fails, leave referer unchanged 218 } 219 } 220 221 // Remove x-forwarded headers 222 proxyReq.removeHeader('x-forwarded-host'); 223 proxyReq.removeHeader('x-forwarded-proto'); 224 proxyReq.removeHeader('x-forwarded-port'); 225 proxyReq.removeHeader('x-forwarded-for'); 226 }); 227 proxy.on('proxyRes', (proxyRes) => { 228 removeHsts(proxyRes); 229 delete proxyRes.headers['content-encoding']; 230 }); 231 }, 232 }, 233 '/login': { 234 target: env.BASE_URL, 235 changeOrigin: true, 236 secure: false, 237 configure: (proxy) => { 238 proxy.on('proxyRes', removeHsts); 239 }, 240 }, 241 '/kvm': { 242 target: env.BASE_URL, 243 changeOrigin: true, 244 secure: false, 245 ws: true, 246 configure: (proxy) => { 247 proxy.on('proxyRes', removeHsts); 248 proxy.on('proxyReqWs', (proxyReq, req) => { 249 const cookies = req.headers.cookie; 250 if (cookies) { 251 const match = cookies.match(/X-Auth-Token=([^;]+)/); 252 if (match) { 253 proxyReq.setHeader('X-Auth-Token', match[1]); 254 } 255 } 256 }); 257 proxy.on('error', (err) => { 258 console.error('[vite] /kvm proxy error:', err.message); 259 }); 260 }, 261 }, 262 '/console': { 263 target: env.BASE_URL, 264 changeOrigin: true, 265 secure: false, 266 ws: true, 267 configure: (proxy) => { 268 proxy.on('proxyRes', removeHsts); 269 proxy.on('proxyReqWs', (proxyReq, req) => { 270 // Forward the auth token from cookies for WebSocket connections 271 const cookies = req.headers.cookie; 272 if (cookies) { 273 const match = cookies.match(/X-Auth-Token=([^;]+)/); 274 if (match) { 275 proxyReq.setHeader('X-Auth-Token', match[1]); 276 } 277 } 278 }); 279 proxy.on('error', (err) => { 280 console.error('[vite] /console proxy error:', err.message); 281 }); 282 }, 283 }, 284 '/vm': { 285 target: env.BASE_URL, 286 changeOrigin: true, 287 secure: false, 288 ws: true, 289 configure: (proxy) => { 290 proxy.on('proxyRes', removeHsts); 291 proxy.on('proxyReqWs', (proxyReq, req) => { 292 const cookies = req.headers.cookie; 293 if (cookies) { 294 const match = cookies.match(/X-Auth-Token=([^;]+)/); 295 if (match) { 296 proxyReq.setHeader('X-Auth-Token', match[1]); 297 } 298 } 299 }); 300 proxy.on('error', (err) => { 301 console.error('[vite] /vm proxy error:', err.message); 302 }); 303 }, 304 }, 305 '/styles/redfish.css': { 306 target: env.BASE_URL, 307 changeOrigin: true, 308 secure: false, 309 configure: (proxy) => { 310 proxy.on('proxyReq', injectAuthToken); 311 proxy.on('proxyRes', removeHsts); 312 }, 313 }, 314 '/images/DMTF_Redfish_logo_2017.svg': { 315 target: env.BASE_URL, 316 changeOrigin: true, 317 secure: false, 318 configure: (proxy) => { 319 proxy.on('proxyReq', injectAuthToken); 320 proxy.on('proxyRes', removeHsts); 321 }, 322 }, 323 }, 324 }, 325 326 build: { 327 // Generate hashed filenames 328 rollupOptions: { 329 output: { 330 // Single chunk output (like LimitChunkCountPlugin with maxChunks: 1) 331 manualChunks: undefined, 332 entryFileNames: 'js/[name].[hash].js', 333 chunkFileNames: 'js/[name].[hash].js', 334 assetFileNames: (assetInfo) => { 335 if (assetInfo.name?.endsWith('.css')) { 336 return 'css/[name].[hash][extname]'; 337 } 338 return 'assets/[name].[hash][extname]'; 339 }, 340 }, 341 }, 342 // Disable source maps in production 343 sourcemap: false, 344 // Performance hints 345 chunkSizeWarningLimit: 512, 346 }, 347 348 // Handle .ico files 349 assetsInclude: ['**/*.ico'], 350 351 // Vitest configuration 352 test: { 353 globals: true, 354 environment: 'happy-dom', 355 setupFiles: ['./tests/vitest.setup.js'], 356 include: ['tests/unit/**/*.spec.js'], 357 css: false, 358 snapshotSerializers: ['vue3-snapshot-serializer'], 359 server: { 360 deps: { 361 inline: ['@carbon/icons-vue'], 362 }, 363 }, 364 coverage: { 365 provider: 'v8', 366 reporter: ['text', 'json', 'html'], 367 }, 368 }, 369 }; 370}); 371