@ordojs/cli
Version:
Command-line interface for OrdoJS framework
399 lines • 15.6 kB
JavaScript
/**
* @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