| #!/usr/bin/env node |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| const fs = require('fs'); |
| const path = require('path'); |
| const zlib = require('zlib'); |
|
|
| |
| function minifyCSS(css) { |
| return css |
| |
| .replace(/\/\*[\s\S]*?\*\//g, '') |
| |
| .replace(/\s+/g, ' ') |
| |
| .replace(/\s*([{}:;,>+~])\s*/g, '$1') |
| |
| .replace(/;}/g, '}') |
| |
| .replace(/([:\s])0+\.(\d+)/g, '$1.$2') |
| |
| .replace(/([:\s])0(px|em|%|rem|vh|vw)/g, '$10') |
| |
| .replace(/#([a-f0-9])\1([a-f0-9])\2([a-f0-9])\3/gi, '#$1$2$3') |
| .trim(); |
| } |
|
|
| function minifyJS(js) { |
| let minified = js; |
| |
| |
| minified = minified.replace(/([^:\/\*])(\/\/[^\n]*)/g, '$1'); |
| |
| |
| minified = minified.replace(/\/\*(?!\!)[\s\S]*?\*\//g, ''); |
| |
| |
| minified = minified.replace(/console\.log\([^;]*\);?/g, ''); |
| |
| |
| |
| |
| |
| minified = minified.replace(/\s+/g, ' '); |
| |
| |
| minified = minified.replace(/\s*([{}()[\]:;,<>!=+\-*/%&|?])\s*/g, '$1'); |
| |
| |
| minified = minified.replace(/\b(return|const|let|var|if|else|for|while|function|class|new|typeof|delete)\s+/g, '$1 '); |
| |
| |
| minified = minified.replace(/;}/g, '}'); |
| |
| return minified.trim(); |
| } |
|
|
| function minifyHTML(html) { |
| return html |
| |
| .replace(/<!--[\s\S]*?-->/g, '') |
| |
| .replace(/\s+/g, ' ') |
| |
| .replace(/>\s+</g, '><') |
| |
| .replace(/\s+(\w+)="([^"]*?)"/g, (match, attr, value) => { |
| if (/^[a-zA-Z0-9_-]+$/.test(value)) { |
| return ` ${attr}=${value}`; |
| } |
| return match; |
| }) |
| .trim(); |
| } |
|
|
| function gzipFile(content) { |
| return zlib.gzipSync(content, { level: 9 }); |
| } |
|
|
| function formatSize(bytes) { |
| return (bytes / 1024).toFixed(2) + ' KB'; |
| } |
|
|
| function getCompressionRatio(original, compressed) { |
| return ((1 - compressed / original) * 100).toFixed(1) + '%'; |
| } |
|
|
| console.log('🚀 Production Build Starting...\n'); |
| console.log('='.repeat(60)); |
|
|
| const publicDir = path.join(__dirname, '..', 'public'); |
| const distDir = path.join(__dirname, '..', 'dist'); |
|
|
| |
| if (!fs.existsSync(distDir)) { |
| fs.mkdirSync(distDir, { recursive: true }); |
| } |
|
|
| const files = [ |
| { |
| name: 'JavaScript', |
| input: path.join(publicDir, 'app.js'), |
| output: path.join(distDir, 'app.min.js'), |
| minify: minifyJS |
| }, |
| { |
| name: 'CSS', |
| input: path.join(publicDir, 'styles.css'), |
| output: path.join(distDir, 'styles.min.css'), |
| minify: minifyCSS |
| }, |
| { |
| name: 'HTML', |
| input: path.join(publicDir, 'index.html'), |
| output: path.join(distDir, 'index.html'), |
| minify: minifyHTML |
| }, |
| { |
| name: 'Service Worker', |
| input: path.join(publicDir, 'sw.js'), |
| output: path.join(distDir, 'sw.min.js'), |
| minify: minifyJS |
| } |
| ]; |
|
|
| let totalOriginalSize = 0; |
| let totalMinifiedSize = 0; |
| let totalGzipSize = 0; |
|
|
| const results = []; |
|
|
| for (const file of files) { |
| try { |
| console.log(`\n📦 Processing ${file.name}...`); |
| console.log('-'.repeat(60)); |
| |
| const content = fs.readFileSync(file.input, 'utf8'); |
| const minified = file.minify(content); |
| const gzipped = gzipFile(minified); |
| |
| |
| fs.writeFileSync(file.output, minified); |
| |
| |
| fs.writeFileSync(file.output + '.gz', gzipped); |
| |
| const originalSize = Buffer.byteLength(content, 'utf8'); |
| const minifiedSize = Buffer.byteLength(minified, 'utf8'); |
| const gzipSize = gzipped.length; |
| |
| totalOriginalSize += originalSize; |
| totalMinifiedSize += minifiedSize; |
| totalGzipSize += gzipSize; |
| |
| const result = { |
| name: file.name, |
| originalSize, |
| minifiedSize, |
| gzipSize, |
| minSavings: getCompressionRatio(originalSize, minifiedSize), |
| gzipSavings: getCompressionRatio(originalSize, gzipSize) |
| }; |
| |
| results.push(result); |
| |
| console.log(` Original: ${formatSize(originalSize)}`); |
| console.log(` Minified: ${formatSize(minifiedSize)} (${result.minSavings} savings)`); |
| console.log(` Gzipped: ${formatSize(gzipSize)} (${result.gzipSavings} savings)`); |
| console.log(` ✅ Saved to: ${path.basename(file.output)}`); |
| |
| } catch (error) { |
| console.error(` ❌ Error processing ${file.name}:`, error.message); |
| } |
| } |
|
|
| |
| console.log('\n\n📊 Build Summary'); |
| console.log('='.repeat(60)); |
| console.log(` Total Original Size: ${formatSize(totalOriginalSize)}`); |
| console.log(` Total Minified Size: ${formatSize(totalMinifiedSize)}`); |
| console.log(` Total Gzipped Size: ${formatSize(totalGzipSize)}`); |
| console.log('-'.repeat(60)); |
| console.log(` Minification Savings: ${getCompressionRatio(totalOriginalSize, totalMinifiedSize)}`); |
| console.log(` Gzip Savings: ${getCompressionRatio(totalOriginalSize, totalGzipSize)}`); |
|
|
| |
| console.log('\n\n💡 Performance Insights'); |
| console.log('='.repeat(60)); |
|
|
| |
| const speeds = { |
| '3G Slow': 400, |
| '3G': 750, |
| '4G': 5000, |
| '5G': 20000, |
| 'WiFi': 50000 |
| }; |
|
|
| Object.entries(speeds).forEach(([name, speed]) => { |
| const loadTime = (totalGzipSize / 1024 / speed) * 8; |
| const emoji = loadTime < 0.5 ? '🚀' : loadTime < 2 ? '✅' : '⚠️'; |
| console.log(` ${emoji} ${name.padEnd(10)} ${loadTime.toFixed(2)}s`); |
| }); |
|
|
| |
| const sizeKB = totalGzipSize / 1024; |
| const loadTimes = {}; |
| Object.entries({ |
| '3G Slow': 400, |
| '3G': 750, |
| '4G': 5000, |
| '5G': 20000, |
| 'WiFi': 50000 |
| }).forEach(([name, speed]) => { |
| loadTimes[name] = ((totalGzipSize / 1024 / speed) * 8).toFixed(2) + 's'; |
| }); |
|
|
| |
| let grade = 'NEEDS_OPTIMIZATION'; |
| let score = 2; |
| if (sizeKB < 50) { grade = 'EXCELLENT'; score = 5; } |
| else if (sizeKB < 100) { grade = 'VERY_GOOD'; score = 4; } |
| else if (sizeKB < 200) { grade = 'GOOD'; score = 3; } |
|
|
| |
| console.log('\n 📈 Performance Grade:'); |
| if (sizeKB < 50) { |
| console.log(' ⭐⭐⭐⭐⭐ EXCELLENT (< 50 KB)'); |
| console.log(' Your bundle is highly optimized!'); |
| } else if (sizeKB < 100) { |
| console.log(' ⭐⭐⭐⭐☆ VERY GOOD (50-100 KB)'); |
| console.log(' Good optimization. Consider code splitting for further improvements.'); |
| } else if (sizeKB < 200) { |
| console.log(' ⭐⭐⭐☆☆ GOOD (100-200 KB)'); |
| console.log(' ⚠️ Consider:'); |
| console.log(' • Code splitting by route/feature'); |
| console.log(' • Lazy loading non-critical components'); |
| console.log(' • Tree shaking with Rollup/Vite'); |
| } else { |
| console.log(' ⭐⭐☆☆☆ NEEDS OPTIMIZATION (> 200 KB)'); |
| console.log(' ⚠️ Bundle too large! Recommended actions:'); |
| console.log(' • Implement code splitting immediately'); |
| console.log(' • Remove unused dependencies'); |
| console.log(' • Use dynamic imports for routes'); |
| console.log(' • Consider migrating to Vite/Rollup for tree shaking'); |
| } |
|
|
| |
| const report = { |
| buildDate: new Date().toISOString(), |
| version: '3.10.2', |
| files: results, |
| totals: { |
| originalSize: totalOriginalSize, |
| minifiedSize: totalMinifiedSize, |
| gzipSize: totalGzipSize, |
| sizeKB: sizeKB.toFixed(2), |
| minSavings: getCompressionRatio(totalOriginalSize, totalMinifiedSize), |
| gzipSavings: getCompressionRatio(totalOriginalSize, totalGzipSize) |
| }, |
| performance: { |
| loadTimes, |
| grade, |
| score, |
| recommendations: score < 4 ? [ |
| 'Implement code splitting', |
| 'Lazy load non-critical features', |
| 'Use dynamic imports', |
| 'Consider modern bundler (Vite/Rollup)' |
| ] : [ |
| 'Excellent optimization!', |
| 'Monitor bundle size on updates', |
| 'Consider lazy loading for future features' |
| ] |
| }, |
| optimizations: { |
| consoleLogs: 'removed', |
| whitespace: 'minified', |
| gzip: 'level 9', |
| comments: 'removed' |
| } |
| }; |
|
|
| fs.writeFileSync( |
| path.join(distDir, 'build-report.json'), |
| JSON.stringify(report, null, 2) |
| ); |
|
|
| console.log('\n\n✅ Production build complete!'); |
| console.log(`📁 Output directory: ${distDir}`); |
| console.log(`📄 Build report: ${path.join(distDir, 'build-report.json')}\n`); |
|
|
|
|