/** * Рекурсивно обрабатывает .png, .jpg, .jpeg в указанной папке, создаёт рядом * `то же имя.webp` с тем же размером. Качество WebP высокое (90), без смены разрешения. * * Пропуск: если рядом уже есть .webp с временем изменения ≥ исходника — пропуск * (кроме --force). Для догенерации после правки картинки — удали webp или дай --force. * * Использование: node scripts/optimize-images.mjs <папка> [--force] */ import path from "node:path"; import fsp from "node:fs/promises"; import { fileURLToPath } from "node:url"; import sharp from "sharp"; const __dirname = path.dirname(fileURLToPath(import.meta.url)); const projectRoot = path.resolve(__dirname, ".."); const IMAGE_EXT = new Set([".png", ".jpg", ".jpeg"]); /** @type {import('sharp').WebpOptions} */ const WEBP = { quality: 90, alphaQuality: 100, effort: 4, }; /** * @param {string} dir * @returns {AsyncGenerator} */ async function* walkFiles(dir) { let entries; try { entries = await fsp.readdir(dir, { withFileTypes: true }); } catch (e) { console.error("Не открылась папка:", dir, e); return; } for (const e of entries) { const full = path.join(dir, e.name); if (e.isDirectory()) yield* walkFiles(full); else if (e.isFile()) yield full; } } /** * @param {string} p */ async function mtime(p) { const s = await fsp.stat(p); return s.mtimeMs; } /** * @param {string} inputPath * @param {string} outPath * @param {boolean} force */ async function shouldSkip(inputPath, outPath, force) { if (force) return false; try { const [tin, tout] = await Promise.all([mtime(inputPath), mtime(outPath)]); return tout >= tin; } catch { return false; } } /** * @param {string} inputPath * @param {string} outPath */ async function toWebp(inputPath, outPath) { await sharp(inputPath).webp(WEBP).toFile(outPath); } function parseArgs(argv) { const rest = argv.slice(2); const force = rest.includes("--force"); const folder = rest.filter((a) => a !== "--force")[0]; return { folder, force }; } const { folder, force } = parseArgs(process.argv); if (!folder) { console.error( "Укажи папку с картинками.\n" + " node scripts/optimize-images.mjs <папка> [--force]\n" + " npm run optimize-images -- <папка> [--force]", ); process.exit(1); } const root = path.isAbsolute(folder) ? folder : path.resolve(projectRoot, folder); try { const st = await fsp.stat(root); if (!st.isDirectory()) { console.error("Не папка:", root); process.exit(1); } } catch { console.error("Папка не найдена:", root); process.exit(1); } let done = 0; let skipped = 0; let err = 0; for await (const filePath of walkFiles(root)) { const ext = path.extname(filePath).toLowerCase(); if (!IMAGE_EXT.has(ext)) continue; const outPath = filePath.replace(/\.(png|jpe?g)$/i, ".webp"); if (outPath === filePath) continue; try { if (await shouldSkip(filePath, outPath, force)) { skipped += 1; console.log("пропуск (уже свежее):", path.relative(projectRoot, outPath)); continue; } await toWebp(filePath, outPath); done += 1; console.log("ok:", path.relative(projectRoot, outPath)); } catch (e) { err += 1; console.error("ошибка:", filePath, e); } } console.log(`\nготово: сгенерировано ${done}, пропущено ${skipped}, ошибок ${err}`); if (err > 0) process.exit(1);