siteslimmer
Version:
A tool to optimize built files including HTML, CSS, JS, and fonts.
171 lines (145 loc) • 4.91 kB
JavaScript
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)
})