UNPKG

siteslimmer

Version:

A tool to optimize built files including HTML, CSS, JS, and fonts.

171 lines (145 loc) 4.91 kB
#!/usr/bin/env node import path from 'path' import yargs from 'yargs' import { hideBin } from 'yargs/helpers' import { minifyHTML } from './minifyHtml.js' import { minifyCSS } from './minifyCss.js' import { minifyJS } from './minifyJs.js' import { getUniqueCharactersFromFiles, subsetFontFile } from './subsetFont.js' import { promises as fs } from 'fs' import cliProgress from 'cli-progress' import chalk from 'chalk' import { createRequire } from 'module' const require = createRequire(import.meta.url) const subsetFont = require('subset-font') const SUPPORTED_EXTENSIONS = ['.html', '.htm', '.css', '.js', '.mjs', '.woff', '.woff2'] const argv = yargs(hideBin(process.argv)) .usage('Usage: $0 <path> [options]') .example('$0 /path/to/directory', 'Optimize all supported files in the specified directory') .example('$0 /path/to/file.html', 'Optimize a single HTML file') .help() .argv const getAllFiles = async (dirPath, arrayOfFiles = []) => { const files = await fs.readdir(dirPath) for (const file of files) { const filePath = path.join(dirPath, file) const stat = await fs.lstat(filePath) if (stat.isDirectory()) { await getAllFiles(filePath, arrayOfFiles) } else { const ext = path.extname(filePath).toLowerCase() if (SUPPORTED_EXTENSIONS.includes(ext)) { arrayOfFiles.push(filePath) } } } return arrayOfFiles } const processFile = async (filePath, uniqueChars) => { const ext = path.extname(filePath).toLowerCase() let result = null try { switch (ext) { case '.html': case '.htm': result = await minifyHTML(filePath) break case '.css': result = await minifyCSS(filePath) break case '.js': case '.mjs': result = await minifyJS(filePath) break case '.woff': case '.woff2': result = await subsetFontFile(filePath, uniqueChars) break } } catch (error) { console.error(chalk.red(`${error.message}`)) } return result } const formatBytes = (bytes) => { const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'] if (bytes === 0) return '0 Byte' const i = parseInt(Math.floor(Math.log(bytes) / Math.log(1024))) return `${Math.round(bytes / Math.pow(1024, i), 2)} ${sizes[i]}` } const calculatePercentageDecrease = (originalSize, newSize) => { const decrease = originalSize - newSize const percentage = (decrease / originalSize) * 100 return percentage.toFixed(2) } const main = async () => { const inputPath = argv._[0] ? path.resolve(argv._[0]) : process.cwd() const stat = await fs.lstat(inputPath) let filePaths = [] if (stat.isDirectory()) { filePaths = await getAllFiles(inputPath) } else if (stat.isFile()) { const ext = path.extname(inputPath).toLowerCase() if (SUPPORTED_EXTENSIONS.includes(ext)) { filePaths.push(inputPath) } else { console.log(chalk.yellow('Unsupported file type.')) return } } else { console.log(chalk.yellow('The provided path is not a valid file or directory.')) return } const htmlCssJsFiles = filePaths.filter(filePath => ['.html', '.htm', '.css', '.js', '.mjs'].includes(path.extname(filePath).toLowerCase())) let uniqueChars = '' if (htmlCssJsFiles.length > 0) { uniqueChars = await getUniqueCharactersFromFiles(htmlCssJsFiles) } const totalFiles = filePaths.length if (totalFiles === 0) { console.log(chalk.yellow('No supported files found.')) return } const progressBar = new cliProgress.SingleBar({ format: 'Optimization Progress | {bar} | {percentage}% || {value}/{total} Files', barCompleteChar: '\u2588', barIncompleteChar: '\u2591', hideCursor: true, }, cliProgress.Presets.shades_classic) progressBar.start(totalFiles, 0) let totalOriginalSize = 0 let totalNewSize = 0 let improvements = [] for (const filePath of filePaths) { const result = await processFile(filePath, uniqueChars) if (result && result.newSize < result.originalSize) { const percentageDecrease = calculatePercentageDecrease(result.originalSize, result.newSize) if (percentageDecrease >= 1) { totalOriginalSize += result.originalSize totalNewSize += result.newSize improvements.push({ fileName: path.basename(filePath), originalSize: result.originalSize, newSize: result.newSize, percentageDecrease, }) } } progressBar.increment() } progressBar.stop() if (improvements.length > 0) { console.log(chalk.green('\nOptimizations:')) improvements.forEach(({ fileName, originalSize, newSize, percentageDecrease }) => { console.log(chalk.yellow(`${percentageDecrease}%`) + ` saving on ${fileName} (${formatBytes(originalSize)} => ${formatBytes(newSize)})`) }) const totalSavings = totalOriginalSize - totalNewSize console.log(`\nTotal Saved: ` + chalk.green(formatBytes(totalSavings))) } else { console.log(chalk.yellow('\nNo improvements were made.')) } } main().catch(error => { console.error(chalk.red(`Error: ${error.message}`)) process.exit(1) })