136 lines
3.7 KiB
JavaScript
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);
|