Files
mercadodevida/project/scripts/generate-thumbnails.mjs
2026-08-19 16:49:16 +02:00

95 lines
3.5 KiB
JavaScript
Executable File
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env node
// Generate cached thumbnails (40px and 200px) for every uploaded product image.
// Pre-generates thumbnails in each app's public/uploads/thumbs/<size>/ directory
// so Next.js serves them as static files.
import { readdir, stat, mkdir, writeFile, readFile } from 'node:fs/promises';
import { dirname, extname, join } from 'node:path';
import { createRequire } from 'node:module';
const require = createRequire(import.meta.url);
const sharp = require(process.env.SHARP_PATH || '/Users/chattie/git/mercadodevida/project/apps/admin/node_modules/sharp');
const PROJECT_DIR = join(import.meta.dirname, '..');
const SIZES = [40, 200];
/**
* Maximum height ratio (h/w) for cached thumbnails. Mirrors the resize
* contract used by the upload route and the dynamic `/uploads/[...path]`
* handler so that batch regeneration and on-demand generation produce the
* same shape. The thumbnail fits inside a `width × width * MAX_HEIGHT_RATIO`
* bounding box without cropping, preserving the source aspect ratio.
*/
const MAX_HEIGHT_RATIO = 1.4;
/** Set FORCE=1 to regenerate thumbnails even when the cached file exists. */
const FORCE = process.env.FORCE === '1' || process.env.FORCE === 'true';
const APP_DIRS = [
join(PROJECT_DIR, 'apps', 'admin'),
join(PROJECT_DIR, 'frontend'),
join(PROJECT_DIR, 'storefront'),
];
const SRC_DIR = join(PROJECT_DIR, 'apps', 'admin', 'public', 'uploads');
const VALID_EXTS = new Set(['.jpg', '.jpeg', '.png', '.webp', '.avif', '.gif']);
async function main() {
let files;
try {
files = await readdir(SRC_DIR);
} catch (err) {
if (err.code === 'ENOENT') {
console.log('[INFO] No uploads dir yet, nothing to do');
return;
}
throw err;
}
const images = files.filter((f) => VALID_EXTS.has(extname(f).toLowerCase()));
console.log(`[INFO] Found ${images.length} source images`);
let generated = 0;
for (const filename of images) {
const src = join(SRC_DIR, filename);
const srcStat = await stat(src);
if (!srcStat.isFile()) continue;
for (const appDir of APP_DIRS) {
// Ensure peer upload exists so the thumbnail is reachable
const peerPath = join(appDir, 'public', 'uploads', filename);
try {
await stat(peerPath);
} catch {
// Copy from source if missing (mirror admin's uploads)
await mkdir(dirname(peerPath), { recursive: true });
await writeFile(peerPath, await import('node:fs/promises').then((fs) => fs.readFile(src)));
}
for (const size of SIZES) {
// Store as <app>/public/uploads/<size>/<filename> so Next.js serves
// them as plain static files via URLs like /uploads/40/foo.jpg
const thumbDir = join(appDir, 'public', 'uploads', String(size));
const thumbPath = join(thumbDir, filename);
// Skip if already cached (unless FORCE=1 to regenerate)
if (!FORCE) {
try {
const s = await stat(thumbPath);
if (s.isFile() && s.size > 0) continue;
} catch {}
}
await mkdir(thumbDir, { recursive: true });
const maxHeight = Math.round(size * MAX_HEIGHT_RATIO);
await sharp(src)
.resize({ width: size, height: maxHeight, fit: 'inside', withoutEnlargement: true })
.jpeg({ quality: 80 })
.toFile(thumbPath);
generated += 1;
}
}
}
console.log(`[OK] Processed ${images.length} images, generated ${generated} new thumbnails`);
}
main().catch((err) => {
console.error('[FAIL]', err.message);
process.exit(1);
});