|
| 1 | +import fs from "fs"; |
| 2 | +import path from "path"; |
| 3 | +import sharp from "sharp"; |
| 4 | + |
| 5 | +const TARGET_WIDTHS = [256, 384, 640, 1080, 1920]; |
| 6 | +const PUBLIC_DIR = path.join(process.cwd(), "public"); |
| 7 | +const OPTIMIZED_DIR = path.join(PUBLIC_DIR, "optimized"); |
| 8 | + |
| 9 | +// Directories to scan recursively for images |
| 10 | +const DIRS_TO_SCAN = ["team", "uploads"]; |
| 11 | +const IMAGE_EXTENSIONS = [".jpg", ".jpeg", ".png"]; |
| 12 | + |
| 13 | +function getFilesRecursively(dir: string, fileList: string[] = []): string[] { |
| 14 | + const absoluteDir = path.join(PUBLIC_DIR, dir); |
| 15 | + if (!fs.existsSync(absoluteDir)) return fileList; |
| 16 | + |
| 17 | + const files = fs.readdirSync(absoluteDir); |
| 18 | + for (const file of files) { |
| 19 | + const relativePath = path.join(dir, file); |
| 20 | + const absolutePath = path.join(PUBLIC_DIR, relativePath); |
| 21 | + if (fs.statSync(absolutePath).isDirectory()) { |
| 22 | + getFilesRecursively(relativePath, fileList); |
| 23 | + } else { |
| 24 | + const ext = path.extname(file).toLowerCase(); |
| 25 | + if (IMAGE_EXTENSIONS.includes(ext)) { |
| 26 | + fileList.push(relativePath); |
| 27 | + } |
| 28 | + } |
| 29 | + } |
| 30 | + return fileList; |
| 31 | +} |
| 32 | + |
| 33 | +async function optimizeImage(srcRelativePath: string) { |
| 34 | + const srcAbsolutePath = path.join(PUBLIC_DIR, srcRelativePath); |
| 35 | + const srcStats = fs.statSync(srcAbsolutePath); |
| 36 | + const srcMtime = srcStats.mtimeMs; |
| 37 | + |
| 38 | + for (const width of TARGET_WIDTHS) { |
| 39 | + const destRelativePath = path.join("optimized", String(width), srcRelativePath); |
| 40 | + const destAbsolutePath = path.join(PUBLIC_DIR, destRelativePath); |
| 41 | + |
| 42 | + // Incremental cache check: Skip if optimized file exists and is newer than source |
| 43 | + if (fs.existsSync(destAbsolutePath)) { |
| 44 | + const destStats = fs.statSync(destAbsolutePath); |
| 45 | + if (destStats.mtimeMs >= srcMtime) { |
| 46 | + continue; |
| 47 | + } |
| 48 | + } |
| 49 | + |
| 50 | + // Create target directory if it doesn't exist |
| 51 | + const destDir = path.dirname(destAbsolutePath); |
| 52 | + fs.mkdirSync(destDir, { recursive: true }); |
| 53 | + |
| 54 | + try { |
| 55 | + const ext = path.extname(srcRelativePath).toLowerCase(); |
| 56 | + const image = sharp(srcAbsolutePath); |
| 57 | + |
| 58 | + // Perform high-quality resizing |
| 59 | + // withoutEnlargement: true ensures we don't upscale small original images |
| 60 | + let transformer = image.resize({ width, withoutEnlargement: true }); |
| 61 | + |
| 62 | + if (ext === ".png") { |
| 63 | + transformer = transformer.png({ compressionLevel: 8, palette: true }); |
| 64 | + } else { |
| 65 | + transformer = transformer.jpeg({ quality: 80, progressive: true }); |
| 66 | + } |
| 67 | + |
| 68 | + await transformer.toFile(destAbsolutePath); |
| 69 | + console.log(`✅ Optimized: /${srcRelativePath} ➡️ /${destRelativePath}`); |
| 70 | + } catch (error) { |
| 71 | + console.error(`❌ Failed to optimize /${srcRelativePath} at width ${width}:`, error); |
| 72 | + } |
| 73 | + } |
| 74 | +} |
| 75 | + |
| 76 | +async function main() { |
| 77 | + console.log("🚀 Scanning public directories for image optimization..."); |
| 78 | + let allImages: string[] = []; |
| 79 | + for (const dir of DIRS_TO_SCAN) { |
| 80 | + allImages = allImages.concat(getFilesRecursively(dir)); |
| 81 | + } |
| 82 | + |
| 83 | + console.log(`🔍 Found ${allImages.length} images to optimize.`); |
| 84 | + |
| 85 | + const startTime = Date.now(); |
| 86 | + for (const relativePath of allImages) { |
| 87 | + await optimizeImage(relativePath); |
| 88 | + } |
| 89 | + const duration = ((Date.now() - startTime) / 1000).toFixed(2); |
| 90 | + console.log(`🎉 Image optimization pipeline finished in ${duration}s.`); |
| 91 | +} |
| 92 | + |
| 93 | +main().catch((err) => { |
| 94 | + console.error("FATAL: Image optimization script failed:", err); |
| 95 | + process.exit(1); |
| 96 | +}); |
0 commit comments