import { promises as fs } from 'node:fs'; import path from 'node:path'; import sharp from 'sharp'; const ROOT = path.resolve('public'); // Files NOT to touch const SKIP = new Set(['favicon.ico', 'apple-touch-icon.png', 'icon.png', 'apple-icon.png']); const SKIP_EXACT_PATHS = new Set([ 'public/favicon.ico', 'public/apple-touch-icon.png', ]); const exts = new Set(['.jpg', '.jpeg', '.png', '.gif']); const results = { converted: 0, savedBytes: 0, skipped: 0, failed: 0 }; async function walk(dir) { const entries = await fs.readdir(dir, { withFileTypes: true }); for (const e of entries) { const p = path.join(dir, e.name); if (e.isDirectory()) { await walk(p); } else if (exts.has(path.extname(p).toLowerCase())) { await convert(p); } } } async function convert(input) { const ext = path.extname(input).toLowerCase(); const base = input.slice(0, -ext.length); const output = base + '.webp'; const relRoot = path.relative(process.cwd(), input).split(path.sep).join('/'); if (SKIP.has(path.basename(input)) || SKIP_EXACT_PATHS.has(relRoot)) { results.skipped++; console.log(`SKIP ${relRoot}`); return; } try { const stIn = await fs.stat(input); // Output options per type const isPng = ext === '.png'; const isGif = ext === '.gif'; const opts = isPng ? { quality: 86, alphaQuality: 90, effort: 5, nearLossless: false } : isGif ? { quality: 80, effort: 5, loop: 0 } // animated -> animated webp : { quality: 82, effort: 5 }; let pipeline = sharp(input, { animated: isGif }); await pipeline.webp(opts).toFile(output); const stOut = await fs.stat(output); if (stOut.size >= stIn.size) { // worse than original — keep original, remove webp await fs.unlink(output); results.skipped++; console.log(`KEEP ${relRoot} (webp larger: ${stOut.size} vs ${stIn.size})`); return; } await fs.unlink(input); results.converted++; results.savedBytes += stIn.size - stOut.size; const inKB = (stIn.size / 1024).toFixed(0); const outKB = (stOut.size / 1024).toFixed(0); console.log(`OK ${relRoot} ${inKB}KB -> ${outKB}KB`); } catch (err) { results.failed++; console.log(`FAIL ${relRoot}: ${err.message}`); } } await walk(ROOT); const savedMB = (results.savedBytes / 1024 / 1024).toFixed(2); console.log('\n---'); console.log(`converted: ${results.converted} skipped: ${results.skipped} failed: ${results.failed}`); console.log(`saved: ${savedMB} MB`);