import { promises as fs } from 'node:fs'; import path from 'node:path'; import sharp from 'sharp'; const ROOT = path.resolve('public'); const SKIP = new Set(['favicon.ico']); const MAX_W = 1920; // cap any image wider than this const exts = new Set(['.jpg', '.jpeg', '.png', '.gif', '.webp']); let convertedCount = 0; let savedBytes = 0; let skippedCount = 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 compress(p); } } function qualityFor(bytes) { if (bytes > 700 * 1024) return 70; if (bytes > 300 * 1024) return 75; if (bytes > 120 * 1024) return 78; return 82; } async function compress(input) { const base = path.basename(input); if (SKIP.has(base)) return; const ext = path.extname(input).toLowerCase(); const isGif = ext === '.gif'; const relRoot = path.relative(process.cwd(), input).split(path.sep).join('/'); try { const stIn = await fs.stat(input); if (stIn.size < 8 * 1024) { // tiny: leave return; } const target = input.slice(0, -ext.length) + '.webp'; // Read into memory first, then close handle const srcBuf = await fs.readFile(input); const meta = await sharp(srcBuf, { animated: isGif }).metadata(); let pipeline = sharp(srcBuf, { animated: isGif }); if (meta.width && meta.width > MAX_W) { pipeline = pipeline.resize({ width: MAX_W, withoutEnlargement: true }); } const q = qualityFor(stIn.size); pipeline = pipeline.webp({ quality: q, alphaQuality: 88, effort: 6, smartSubsample: true, ...(isGif ? { loop: 0 } : {}), }); const buf = await pipeline.toBuffer(); if (buf.length >= stIn.size && ext === '.webp') { skippedCount++; return; } // write via temp file then rename for atomicity const tmp = target + '.tmp'; await fs.writeFile(tmp, buf); if (target !== input) { try { await fs.unlink(input); } catch {} } await fs.rename(tmp, target); convertedCount++; savedBytes += stIn.size - buf.length; const inKB = (stIn.size / 1024).toFixed(0); const outKB = (buf.length / 1024).toFixed(0); console.log(`OK q=${q} ${relRoot} ${inKB}KB -> ${outKB}KB${meta.width && meta.width > MAX_W ? ' (resized)' : ''}`); } catch (err) { console.log(`FAIL ${relRoot}: ${err.message}`); } } await walk(ROOT); const savedMB = (savedBytes / 1024 / 1024).toFixed(2); console.log('\n---'); console.log(`compressed: ${convertedCount} skipped: ${skippedCount}`); console.log(`saved: ${savedMB} MB`);