import path from 'path'; import { fileURLToPath } from 'url'; import HtmlWebpackPlugin from 'html-webpack-plugin'; import ReactRefreshWebpackPlugin from '@pmmmwh/react-refresh-webpack-plugin'; import MiniCssExtractPlugin from 'mini-css-extract-plugin'; import ESLintPlugin from 'eslint-webpack-plugin'; import { cpSync } from 'fs'; import { execSync } from 'child_process'; import webpack from 'webpack'; import fs from 'fs'; import { BundleAnalyzerPlugin } from 'webpack-bundle-analyzer'; // Get git commit hash const getGitCommitHash = () => { try { return execSync('git rev-parse HEAD').toString().trim(); } catch (e) { console.error('Failed to get git commit hash:', e); return 'unknown'; } }; const GIT_COMMIT_HASH = getGitCommitHash(); // Create a plugin to add the git commit hash directly to the HTML class GitCommitPlugin { apply(compiler) { compiler.hooks.compilation.tap('GitCommitPlugin', (compilation) => { HtmlWebpackPlugin.getHooks(compilation).beforeEmit.tapAsync( 'GitCommitPlugin', (data, cb) => { // Add a meta tag for the git commit hash const metaTag = ``; data.html = data.html.replace('', `${metaTag}\n`); cb(null, data); } ); }); } } // Plugin to generate currentHash.json file class GitHashJsonPlugin { apply(compiler) { compiler.hooks.afterEmit.tap('GitHashJsonPlugin', (_compilation) => { const outputPath = path.join(compiler.options.output.path, 'currentHash.json'); const content = JSON.stringify({ gitCommit: GIT_COMMIT_HASH, buildTime: new Date().toISOString() }, null, 2); try { fs.mkdirSync(path.dirname(outputPath), { recursive: true }); fs.writeFileSync(outputPath, content); console.log('Generated currentHash.json'); } catch (err) { console.error('Error creating currentHash.json:', err); } }); } } // Custom plugin to copy assets const CopyAssetsPlugin = { apply: (compiler) => { compiler.hooks.afterEmit.tap('CopyAssetsPlugin', () => { // Copy assets directory but exclude fonts (webpack handles fonts with hashed names) const assetsSrc = path.resolve(__dirname, 'public/assets'); const assetsDest = path.resolve(__dirname, 'dist/assets'); try { // Copy all assets except fonts const items = fs.readdirSync(assetsSrc); for (const item of items) { if (item !== 'fonts') { const srcPath = path.join(assetsSrc, item); const destPath = path.join(assetsDest, item); cpSync(srcPath, destPath, { recursive: true }); } } console.log('Assets copied successfully (fonts excluded - handled by webpack)'); } catch (err) { console.error('Error copying assets:', err); } // Copy favicon.ico const faviconSrc = path.resolve(__dirname, 'public/favicon.ico'); const faviconDest = path.resolve(__dirname, 'dist/favicon.ico'); try { cpSync(faviconSrc, faviconDest); console.log('Favicon copied successfully'); } catch (err) { console.error('Error copying favicon:', err); } // Copy index.html to payment/success file for callback const indexSrc = path.resolve(__dirname, 'dist/index.html'); const paymentDir = path.resolve(__dirname, 'dist/payment'); const paymentSuccessDest = path.resolve(__dirname, 'dist/payment/success'); try { // Create payment directory if it doesn't exist if (!fs.existsSync(paymentDir)) { fs.mkdirSync(paymentDir, { recursive: true }); } cpSync(indexSrc, paymentSuccessDest); console.log('Index.html copied to payment/success file successfully'); } catch (err) { console.error('Error copying index.html to payment/success file:', err); } }); }, }; // Custom plugin to inline CSS instead of loading externally class InlineCssPlugin { apply(compiler) { compiler.hooks.compilation.tap('InlineCssPlugin', (compilation) => { HtmlWebpackPlugin.getHooks(compilation).beforeEmit.tapAsync( 'InlineCssPlugin', (data, cb) => { // Only inline CSS in production mode if (isDevelopment) { cb(null, data); return; } // Find CSS assets and inline them let inlinedCss = ''; const cssAssets = []; // Get CSS assets from compilation Object.keys(compilation.assets).forEach(assetName => { if (assetName.endsWith('.css')) { const cssContent = compilation.assets[assetName].source(); inlinedCss += cssContent + '\n'; cssAssets.push(assetName); // Remove CSS asset from compilation to prevent external file generation delete compilation.assets[assetName]; } }); if (inlinedCss.trim()) { // Remove existing CSS link tags from HTML data.html = data.html.replace(/]*href="[^"]*\.css"[^>]*>/g, ''); // JavaScript file preloading removed // Extract font URLs from CSS for preloading - DISABLED // const fontUrls = []; // const fontMatches = inlinedCss.match(/url\(([^)]+\.ttf)\)/g); // if (fontMatches) { // fontMatches.forEach(match => { // const fontUrl = match.replace(/url\(([^)]+)\)/, '$1').replace(/['"]/g, ''); // if (!fontUrls.includes(fontUrl)) { // fontUrls.push(fontUrl); // } // }); // } // Add font preload links - DISABLED let fontPreloads = ''; // fontUrls.forEach(fontUrl => { // fontPreloads += `\n`; // }); // Add critical image preloads - DISABLED let imagePreloads = ''; // Get the output filename to determine page type const outputPath = data.outputName || ''; // Only preload on homepage - check if this is the main index.html const isHomePage = outputPath === 'index.html'; // Define critical images array - only preload these on homepage // const criticalImages = [ // '/assets/images/filiale1.jpg', // '/assets/images/filiale2.jpg', // '/assets/images/seeds.jpg', // '/assets/images/cutlings.jpg', // '/assets/images/presse.jpg', // '/assets/images/purpl.jpg' // ]; // Only preload navigation images for homepage - DISABLED let preloadCount = 0; // if (isHomePage) { // // Add the as="image" attribute to ensure proper preloading // criticalImages.forEach(imagePath => { // imagePreloads += `\n`; // preloadCount++; // }); // } // Add inlined CSS to head const styleTag = ``; // Add only the style tag to head (no JS preloads) data.html = data.html.replace('', `${styleTag}\n`); console.log(`✅ Inlined CSS assets: ${cssAssets.join(', ')} (${Math.round(inlinedCss.length / 1024)}KB)`); // if (fontUrls.length > 0) { // console.log(`✅ Added font preloads: ${fontUrls.length} fonts`); // } // Log whether images were preloaded console.log(`Page: ${outputPath} - Is homepage: ${isHomePage}`); // if (preloadCount > 0) { // console.log(`✅ Added image preloads: ${preloadCount} critical images`); // } else { // console.log(`✅ No image preloads added for ${outputPath}`); // } } cb(null, data); } ); }); } } const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); const isDevelopment = process.env.NODE_ENV !== 'production'; const isAnalyze = process.env.ANALYZE === 'true'; const proxyTarget = process.env.PROXY_TARGET || 'http://localhost:9303'; export default { mode: isDevelopment ? 'development' : 'production', entry: { main: './src/index.js' }, target: 'web', output: { path: path.resolve(__dirname, 'dist'), filename: isDevelopment ? 'js/[name].[contenthash].bundle.js' : 'js/[name].[contenthash].js', chunkFilename: isDevelopment ? 'js/[name].[contenthash].chunk.js' : 'js/[name].[contenthash].chunk.js', clean: isDevelopment ? true : false, publicPath: '/' }, devtool: isDevelopment ? 'source-map' : false, optimization: { runtimeChunk: 'single', moduleIds: 'deterministic', sideEffects: false, usedExports: true, minimize: !isDevelopment, minimizer: !isDevelopment ? [ // Use default minimizers (terser-webpack-plugin for JS) '...', ] : [], splitChunks: { chunks: 'all', maxInitialRequests: 30, maxAsyncRequests: 30, minSize: 20000, cacheGroups: { // Split React and React DOM into separate chunk react: { test: /[\\/]node_modules[\\/](react|react-dom|react-router-dom)[\\/]/, name: 'react', priority: 30, reuseExistingChunk: true, }, // Split commonly used MUI icons (used in main bundle and immediate-loading components) muiIconsCommon: { test: /[\\/]node_modules[\\/]@mui[\\/]icons-material[\\/].*(SmartToy|Palette|Search|Home|ShoppingCart|Close|ChevronLeft|ChevronRight|Person|Google|Add|Remove|Delete|KeyboardArrowUp|ZoomIn|Loupe|ExpandMore|ExpandLess|Mic|Stop|PhotoCamera|Menu|KeyboardReturn|ContentCopy|Cancel|CloudUpload|Star).*\.js$/, name: 'mui-icons-common', priority: 29, reuseExistingChunk: true, enforce: true, }, // Split remaining MUI icons into separate chunk (for lazy-loaded components only) muiIcons: { test: /[\\/]node_modules[\\/]@mui[\\/]icons-material[\\/].*(Article|LockReset|AdminPanelSettings|Group|BarChart).*\.js$/, name: 'mui-icons', priority: 28, reuseExistingChunk: true, chunks: 'async', // Only split icons used in lazy-loaded chunks enforce: true, // Ensure this rule is applied }, // Split MUI core (styles + components) muiCore: { test: /[\\/]node_modules[\\/]@mui[\\/]/, name: 'mui-core', priority: 26, reuseExistingChunk: true, }, // Split emotion (MUI's styling dependency) emotion: { test: /[\\/]node_modules[\\/]@emotion[\\/]/, name: 'emotion', priority: 24, reuseExistingChunk: true, }, // Split chart.js into separate chunk (only loaded when needed) charts: { test: /[\\/]node_modules[\\/](chart\.js|react-chartjs-2)[\\/]/, name: 'charts', priority: 20, reuseExistingChunk: true, }, // Other vendor libraries vendor: { test: /[\\/]node_modules[\\/]/, name: 'vendor', priority: 10, reuseExistingChunk: true, }, // Common modules used across the app common: { name: 'common', minChunks: 2, priority: 5, reuseExistingChunk: true, enforce: true, }, }, }, }, module: { rules: [ { test: /\.jsx?$/, exclude: /node_modules/, use: 'babel-loader', }, { test: /\.css$/, use: [ isDevelopment ? 'style-loader' : MiniCssExtractPlugin.loader, 'css-loader' ], }, { test: /\.(png|svg|jpg|jpeg|gif)$/i, type: 'asset/resource', }, { test: /\.(woff|woff2|eot|ttf|otf)$/i, type: 'asset/resource', }, ], }, resolve: { extensions: ['.js', '.jsx'], }, plugins: [ new HtmlWebpackPlugin({ template: './public/index.html', inject: true, scriptLoading: 'blocking', }), new GitCommitPlugin(), new GitHashJsonPlugin(), new webpack.DefinePlugin({ 'process.env.GIT_COMMIT_HASH': JSON.stringify(GIT_COMMIT_HASH) }), isDevelopment && new ReactRefreshWebpackPlugin({ overlay: false, // Disable React Refresh overlay to prevent conflicts }), !isDevelopment && new MiniCssExtractPlugin({ filename: 'css/[name].[contenthash].css', chunkFilename: 'css/[id].[contenthash].css', }), new ESLintPlugin({ extensions: ['js', 'jsx'], emitWarning: true, emitError: true, failOnError: false, failOnWarning: false, quiet: false, eslintPath: 'eslint/use-at-your-own-risk' }), !isDevelopment && CopyAssetsPlugin, isAnalyze && new BundleAnalyzerPlugin({ analyzerMode: 'static', reportFilename: 'bundle-report.html', openAnalyzer: true, generateStatsFile: true, statsFilename: 'bundle-stats.json', }), new InlineCssPlugin(), ].filter(Boolean), watchOptions: { // Only rebuild when file content actually changes, not just timestamps aggregateTimeout: 300, // Wait 300ms after a change before rebuilding poll: false, // Use native file watching instead of polling ignored: /node_modules/, // Ignore node_modules for performance followSymlinks: false, // Don't follow symlinks }, devServer: { allowedHosts: 'all', compress: true, headers: { 'Cache-Control': 'public, max-age=3600', }, // Add proxy configuration for socket.io and API proxy: [ { context: ['/socket.io'], target: proxyTarget, changeOrigin: true, ws: true, logLevel: 'debug', secure: proxyTarget.startsWith('https') }, { context: ['/api'], target: proxyTarget, changeOrigin: true, logLevel: 'debug', secure: proxyTarget.startsWith('https') } ], hot: true, port: 9500, open: false, historyApiFallback: { index: '/index.html' }, client: { logging: 'verbose', webSocketURL: 'wss://dev.seedheads.de/ws', overlay: { errors: true, warnings: true, // Disable warnings overlay to reduce noise runtimeErrors: true, }, }, }, };