import fs from 'node:fs/promises'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; import sharp from 'sharp'; // ERROR: `__dirname` is not defined in ES module scope // https://iamwebwiz.medium.com/how-to-fix-dirname-is-not-defined-in-es-module-scope-34d94a86694d const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); const allowedFileExtensions = [ '.avif', '.gif', '.heif', '.jpeg', '.png', '.raw', '.tiff', '.webp', ] as const; type AllowedFileExtension = (typeof allowedFileExtensions)[number]; const publicDir = path.join(__dirname, '../public'); const cacheFile = path.join(__dirname, '/compressed-images.json'); const KB_IN_BYTES = 1024; const COMPRESS_CONFIG = { avif: { chromaSubsampling: '4:4:4', effort: 9.0, }, gif: { effort: 10.0, }, jpeg: { chromaSubsampling: '4:4:4', mozjpeg: true, trellisQuantisation: true, overshootDeringing: true, optimiseScans: true, }, png: { compressionLevel: 9.0, palette: true, }, raw: {}, tiff: { compression: 'lzw', }, webp: { effort: 6.0, }, }; (async () => { let cache: string[] = []; const isCacheFileExists = await fs .access(cacheFile) .then(() => true) .catch(() => false); if (isCacheFileExists) { const cacheFileContent = await fs.readFile(cacheFile, 'utf8'); cache = JSON.parse(cacheFileContent); } const images = await recursiveGetImages(publicDir); for (const image of images) { const extname = path.extname(image).toLowerCase() as AllowedFileExtension; if ( !allowedFileExtensions.includes(extname) || image.includes('node_modules') || image.includes('.astro') || image.includes('.vscode') || image.includes('.git') ) { continue; } const stats = await fs.stat(image); const relativeImagePath = path.relative(path.join(__dirname, '..'), image); if (cache.includes(relativeImagePath)) { continue; } const prevSize = stats.size / KB_IN_BYTES; let imageBuffer: Buffer | undefined; switch (extname) { case '.avif': imageBuffer = await sharp(image).avif(COMPRESS_CONFIG.avif).toBuffer(); break; case '.gif': imageBuffer = await sharp(image).gif(COMPRESS_CONFIG.gif).toBuffer(); break; case '.heif': imageBuffer = await sharp(image).heif().toBuffer(); break; case '.jpeg': imageBuffer = await sharp(image).jpeg(COMPRESS_CONFIG.jpeg).toBuffer(); break; case '.png': imageBuffer = await sharp(image).png(COMPRESS_CONFIG.png).toBuffer(); break; case '.raw': imageBuffer = await sharp(image).raw().toBuffer(); break; case '.tiff': imageBuffer = await sharp(image).tiff(COMPRESS_CONFIG.tiff).toBuffer(); break; case '.webp': imageBuffer = await sharp(image).webp(COMPRESS_CONFIG.webp).toBuffer(); break; } if (!imageBuffer) { console.error(`❌ ${image} Compressing failed!`); continue; } const newSize = imageBuffer.length / KB_IN_BYTES; const diff = prevSize - newSize; if (diff <= 0) { console.log(`📦 Skipped ${relativeImagePath}`); continue; } const diffPercent = ((diff / prevSize) * 100).toFixed(2); console.log( `📦 Reduced ${prevSize.toFixed(2)}KB → ${newSize.toFixed(2)}KB (${diff.toFixed(2)}KB, ${diffPercent}%) for ${relativeImagePath}`, ); await fs.writeFile(image, imageBuffer); cache.push(relativeImagePath); // So that we don't lose the cache if the script crashes await fs.writeFile(cacheFile, JSON.stringify(cache, null, 2), 'utf8'); } await fs.writeFile(cacheFile, JSON.stringify(cache, null, 2), 'utf8'); })(); async function recursiveGetImages(dir: string): Promise { const subdirs = await fs.readdir(dir, { withFileTypes: true }); const files = await Promise.all( subdirs.map((dirent) => { const res = path.resolve(dir, dirent.name); return dirent.isDirectory() ? recursiveGetImages(res) : res; }), ); return Array.prototype.concat(...files); }