UNPKG

ordojs

Version:

A revolutionary web framework with compile-time optimizations and unified client-server development

399 lines 15.6 kB
/** * @fileoverview OrdoJS CLI - Asset optimization utilities for production builds */ import brotli from 'brotli'; import { filesize } from 'filesize'; import gzipSize from 'gzip-size'; import path from 'path'; import { Terser } from 'terser'; import { readFile, writeFile } from './fs.js'; import { logger } from './logger.js'; /** * Default optimization options */ const DEFAULT_OPTIONS = { minifyJs: true, minifyCss: true, brotli: true, gzip: true, sizeReport: true, terserOptions: { compress: { passes: 2, drop_console: true, drop_debugger: true }, mangle: true, format: { comments: false } } }; /** * Asset optimizer for production builds */ export class AssetOptimizer { options; /** * Create a new asset optimizer */ constructor(options = {}) { this.options = { ...DEFAULT_OPTIONS, ...options }; } /** * Optimize a JavaScript file */ async optimizeJavaScript(filePath) { const content = await readFile(filePath); const originalSize = content.length; const originalSizeHuman = filesize(originalSize); let minifiedContent = content; let minifiedSize = originalSize; let minified = false; // Minify JavaScript if enabled if (this.options.minifyJs) { try { const result = await Terser.minify(content, this.options.terserOptions); if (result.code) { minifiedContent = result.code; minifiedSize = minifiedContent.length; minified = true; // Write minified file await writeFile(filePath, minifiedContent); } } catch (error) { logger.warn(`Failed to minify ${filePath}: ${error instanceof Error ? error.message : String(error)}`); } } // Calculate compression ratio const compressionRatio = originalSize > 0 ? 1 - minifiedSize / originalSize : 0; // Generate size information const sizes = { originalSize, minifiedSize, compressionRatio, originalSizeHuman, minifiedSizeHuman: filesize(minifiedSize) }; // Generate Gzip compressed version if enabled let gzipped = false; if (this.options.gzip) { try { const gzipSizeValue = await gzipSize(minifiedContent); sizes.gzipSize = gzipSizeValue; sizes.gzipSizeHuman = filesize(gzipSizeValue); gzipped = true; // Write gzipped file const gzipFilePath = `${filePath}.gz`; // We don't actually write the gzipped file here as it's typically // handled by the web server or CDN, but we could if needed } catch (error) { logger.warn(`Failed to gzip ${filePath}: ${error instanceof Error ? error.message : String(error)}`); } } // Generate Brotli compressed version if enabled let brotlified = false; if (this.options.brotli) { try { const buffer = Buffer.from(minifiedContent); const compressed = brotli.compress(buffer, { mode: 1, // text mode quality: 11, // maximum compression lgwin: 24 // maximum window size }); if (compressed) { const brotliSizeValue = compressed.length; sizes.brotliSize = brotliSizeValue; sizes.brotliSizeHuman = filesize(brotliSizeValue); brotlified = true; // Write brotli file const brotliFilePath = `${filePath}.br`; await writeFile(brotliFilePath, compressed); } } catch (error) { logger.warn(`Failed to brotli compress ${filePath}: ${error instanceof Error ? error.message : String(error)}`); } } return { filePath, type: 'js', sizes, minified, gzipped, brotlified }; } /** * Optimize a CSS file * Note: Basic implementation - the actual CSS minification is handled by the CSS optimizer in the core package */ async optimizeCSS(filePath) { const content = await readFile(filePath); const originalSize = content.length; const originalSizeHuman = filesize(originalSize); // CSS is already minified by the CSS optimizer in the core package // This is just for generating size information and compression const minifiedSize = originalSize; const minifiedContent = content; const minified = true; // Calculate compression ratio const compressionRatio = 0; // Already minified // Generate size information const sizes = { originalSize, minifiedSize, compressionRatio, originalSizeHuman, minifiedSizeHuman: filesize(minifiedSize) }; // Generate Gzip compressed version if enabled let gzipped = false; if (this.options.gzip) { try { const gzipSizeValue = await gzipSize(minifiedContent); sizes.gzipSize = gzipSizeValue; sizes.gzipSizeHuman = filesize(gzipSizeValue); gzipped = true; } catch (error) { logger.warn(`Failed to gzip ${filePath}: ${error instanceof Error ? error.message : String(error)}`); } } // Generate Brotli compressed version if enabled let brotlified = false; if (this.options.brotli) { try { const buffer = Buffer.from(minifiedContent); const compressed = brotli.compress(buffer, { mode: 1, // text mode quality: 11, // maximum compression lgwin: 24 // maximum window size }); if (compressed) { const brotliSizeValue = compressed.length; sizes.brotliSize = brotliSizeValue; sizes.brotliSizeHuman = filesize(brotliSizeValue); brotlified = true; // Write brotli file const brotliFilePath = `${filePath}.br`; await writeFile(brotliFilePath, compressed); } } catch (error) { logger.warn(`Failed to brotli compress ${filePath}: ${error instanceof Error ? error.message : String(error)}`); } } return { filePath, type: 'css', sizes, minified, gzipped, brotlified }; } /** * Optimize an HTML file */ async optimizeHTML(filePath) { const content = await readFile(filePath); const originalSize = content.length; const originalSizeHuman = filesize(originalSize); let minifiedContent = content; let minifiedSize = originalSize; let minified = false; // Simple HTML minification if (this.options.minifyJs) { try { // Very basic HTML minification - in a real implementation, we'd use a proper HTML minifier minifiedContent = content .replace(/<!--[\s\S]*?-->/g, '') // Remove comments .replace(/\s{2,}/g, ' ') // Remove extra spaces .replace(/>\s+</g, '><') // Remove spaces between tags .trim(); minifiedSize = minifiedContent.length; minified = true; // Write minified file await writeFile(filePath, minifiedContent); } catch (error) { logger.warn(`Failed to minify ${filePath}: ${error instanceof Error ? error.message : String(error)}`); } } // Calculate compression ratio const compressionRatio = originalSize > 0 ? 1 - minifiedSize / originalSize : 0; // Generate size information const sizes = { originalSize, minifiedSize, compressionRatio, originalSizeHuman, minifiedSizeHuman: filesize(minifiedSize) }; // Generate Gzip compressed version if enabled let gzipped = false; if (this.options.gzip) { try { const gzipSizeValue = await gzipSize(minifiedContent); sizes.gzipSize = gzipSizeValue; sizes.gzipSizeHuman = filesize(gzipSizeValue); gzipped = true; } catch (error) { logger.warn(`Failed to gzip ${filePath}: ${error instanceof Error ? error.message : String(error)}`); } } // Generate Brotli compressed version if enabled let brotlified = false; if (this.options.brotli) { try { const buffer = Buffer.from(minifiedContent); const compressed = brotli.compress(buffer, { mode: 1, // text mode quality: 11, // maximum compression lgwin: 24 // maximum window size }); if (compressed) { const brotliSizeValue = compressed.length; sizes.brotliSize = brotliSizeValue; sizes.brotliSizeHuman = filesize(brotliSizeValue); brotlified = true; // Write brotli file const brotliFilePath = `${filePath}.br`; await writeFile(brotliFilePath, compressed); } } catch (error) { logger.warn(`Failed to brotli compress ${filePath}: ${error instanceof Error ? error.message : String(error)}`); } } return { filePath, type: 'html', sizes, minified, gzipped, brotlified }; } /** * Optimize a directory of assets */ async optimizeDirectory(dir, patterns = ['**/*.js', '**/*.css', '**/*.html']) { const results = []; let totalOriginalSize = 0; let totalMinifiedSize = 0; let totalGzipSize = 0; let totalBrotliSize = 0; // Process each file pattern for (const pattern of patterns) { const files = await glob(path.join(dir, pattern)); for (const file of files) { let result; // Determine file type and optimize accordingly if (file.endsWith('.js')) { result = await this.optimizeJavaScript(file); } else if (file.endsWith('.css')) { result = await this.optimizeCSS(file); } else if (file.endsWith('.html')) { result = await this.optimizeHTML(file); } else { // Skip other file types continue; } results.push(result); // Update totals totalOriginalSize += result.sizes.originalSize; totalMinifiedSize += result.sizes.minifiedSize || result.sizes.originalSize; totalGzipSize += result.sizes.gzipSize || 0; totalBrotliSize += result.sizes.brotliSize || 0; } } // Calculate overall compression ratio const overallCompressionRatio = totalOriginalSize > 0 ? 1 - totalMinifiedSize / totalOriginalSize : 0; return { assets: results, totalOriginalSize, totalMinifiedSize, totalGzipSize, totalBrotliSize, overallCompressionRatio, totalOriginalSizeHuman: filesize(totalOriginalSize), totalMinifiedSizeHuman: filesize(totalMinifiedSize), totalGzipSizeHuman: filesize(totalGzipSize), totalBrotliSizeHuman: filesize(totalBrotliSize) }; } /** * Generate a size report for the optimized assets */ generateSizeReport(results) { let report = '\n=== Asset Size Report ===\n\n'; // Add summary report += 'Summary:\n'; report += ` Total Original Size: ${results.totalOriginalSizeHuman}\n`; report += ` Total Minified Size: ${results.totalMinifiedSizeHuman} (${Math.round(results.overallCompressionRatio * 100)}% reduction)\n`; report += ` Total Gzip Size: ${results.totalGzipSizeHuman}\n`; report += ` Total Brotli Size: ${results.totalBrotliSizeHuman}\n\n`; // Group assets by type const jsAssets = results.assets.filter(asset => asset.type === 'js'); const cssAssets = results.assets.filter(asset => asset.type === 'css'); const htmlAssets = results.assets.filter(asset => asset.type === 'html'); // Add JavaScript assets if (jsAssets.length > 0) { report += 'JavaScript Assets:\n'; for (const asset of jsAssets) { report += ` ${path.basename(asset.filePath)}\n`; report += ` Original: ${asset.sizes.originalSizeHuman}\n`; report += ` Minified: ${asset.sizes.minifiedSizeHuman}\n`; if (asset.sizes.gzipSizeHuman) { report += ` Gzip: ${asset.sizes.gzipSizeHuman}\n`; } if (asset.sizes.brotliSizeHuman) { report += ` Brotli: ${asset.sizes.brotliSizeHuman}\n`; } } report += '\n'; } // Add CSS assets if (cssAssets.length > 0) { report += 'CSS Assets:\n'; for (const asset of cssAssets) { report += ` ${path.basename(asset.filePath)}\n`; report += ` Original: ${asset.sizes.originalSizeHuman}\n`; report += ` Minified: ${asset.sizes.minifiedSizeHuman}\n`; if (asset.sizes.gzipSizeHuman) { report += ` Gzip: ${asset.sizes.gzipSizeHuman}\n`; } if (asset.sizes.brotliSizeHuman) { report += ` Brotli: ${asset.sizes.brotliSizeHuman}\n`; } } report += '\n'; } // Add HTML assets if (htmlAssets.length > 0) { report += 'HTML Assets:\n'; for (const asset of htmlAssets) { report += ` ${path.basename(asset.filePath)}\n`; report += ` Original: ${asset.sizes.originalSizeHuman}\n`; report += ` Minified: ${asset.sizes.minifiedSizeHuman}\n`; if (asset.sizes.gzipSizeHuman) { report += ` Gzip: ${asset.sizes.gzipSizeHuman}\n`; } if (asset.sizes.brotliSizeHuman) { report += ` Brotli: ${asset.sizes.brotliSizeHuman}\n`; } } report += '\n'; } return report; } } //# sourceMappingURL=asset-optimizer.js.map