Files
site-portfolio/scripts/optimize-images.mjs

136 lines
3.7 KiB
JavaScript

/**
* Рекурсивно обрабатывает .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<string, void, void>}
*/
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);