const fs = require('node:fs'); const path = require('node:path'); const sharp = require('sharp'); const sourceRoot = path.join(process.cwd(), 'src', 'assets', 'unoptimized'); const targetRoot = path.join(process.cwd(), 'src', 'assets', 'optimized'); const rasterExtensions = new Set([ '.jpg', '.jpeg', '.png', '.webp', '.avif', '.gif', '.tif', '.tiff', ]); const passthroughExtensions = new Set(['.svg']); function collectFilesRecursively(directoryPath) { if (!fs.existsSync(directoryPath)) { return []; } const entries = fs.readdirSync(directoryPath, { withFileTypes: true }); const files = []; for (const entry of entries) { const fullPath = path.join(directoryPath, entry.name); if (entry.isDirectory()) { files.push(...collectFilesRecursively(fullPath)); continue; } if (entry.isFile()) { files.push(fullPath); } } return files; } function ensureCleanDirectory(directoryPath) { fs.rmSync(directoryPath, { recursive: true, force: true }); fs.mkdirSync(directoryPath, { recursive: true }); } function getProfile(relativePath) { const normalizedRelativePath = relativePath.split(path.sep).join('/').toLowerCase(); if (normalizedRelativePath === 'background.jpg' || normalizedRelativePath === 'background.jpeg') { return { maxWidth: 1920, quality: 82, alphaQuality: 100 }; } if (normalizedRelativePath.startsWith('logos/')) { return { maxWidth: 216, quality: 78, alphaQuality: 88, variants: [64, 128, 216] }; } if (normalizedRelativePath.startsWith('portrait/')) { return { maxWidth: 640, quality: 82, alphaQuality: 90, variants: [128, 320, 384, 480] }; } return { maxWidth: 1600, quality: 82, alphaQuality: 92 }; } function getTargetRelativePath(relativePath, width) { const parsedPath = path.parse(relativePath); const fileName = width === undefined ? `${parsedPath.name}.webp` : `${parsedPath.name}-${width}.webp`; return path.join(parsedPath.dir, fileName); } async function createWebpBuffer(inputBuffer, profile, width) { const image = sharp(inputBuffer, { animated: false }).rotate(); const metadata = await image.metadata(); const targetWidth = width ?? profile.maxWidth; let pipeline = image; if ((metadata.width ?? 0) > targetWidth) { pipeline = pipeline.resize({ width: targetWidth, fit: 'inside', withoutEnlargement: true, }); } return pipeline .webp({ quality: profile.quality, alphaQuality: profile.alphaQuality, effort: 6, smartSubsample: true, nearLossless: profile.nearLossless ?? false, }) .toBuffer(); } async function writeOptimizedAsset(sourceFilePath) { const relativePath = path.relative(sourceRoot, sourceFilePath); const extension = path.extname(sourceFilePath).toLowerCase(); const targetRelativePath = passthroughExtensions.has(extension) ? relativePath : getTargetRelativePath(relativePath); const targetFilePath = path.join(targetRoot, targetRelativePath); fs.mkdirSync(path.dirname(targetFilePath), { recursive: true }); if (passthroughExtensions.has(extension)) { fs.copyFileSync(sourceFilePath, targetFilePath); return { sourceRelativePath: relativePath, targetRelativePath, beforeBytes: fs.statSync(sourceFilePath).size, afterBytes: fs.statSync(targetFilePath).size, mode: 'copied', }; } const profile = getProfile(relativePath); const inputBuffer = fs.readFileSync(sourceFilePath); const inputBytes = inputBuffer.byteLength; const outputBuffer = await createWebpBuffer(inputBuffer, profile); fs.writeFileSync(targetFilePath, outputBuffer); for (const variantWidth of profile.variants ?? []) { const variantRelativePath = getTargetRelativePath(relativePath, variantWidth); const variantFilePath = path.join(targetRoot, variantRelativePath); const variantBuffer = await createWebpBuffer(inputBuffer, profile, variantWidth); fs.writeFileSync(variantFilePath, variantBuffer); } return { sourceRelativePath: relativePath, targetRelativePath, beforeBytes: inputBytes, afterBytes: outputBuffer.byteLength, mode: 'converted', }; } async function run() { if (!fs.existsSync(sourceRoot)) { console.log('Optimized asset generation skipped: no unoptimized asset directory found.'); return; } ensureCleanDirectory(targetRoot); const assetFiles = collectFilesRecursively(sourceRoot).filter((filePath) => { const extension = path.extname(filePath).toLowerCase(); return rasterExtensions.has(extension) || passthroughExtensions.has(extension); }); let convertedCount = 0; let copiedCount = 0; let totalSavedBytes = 0; for (const assetFile of assetFiles) { const result = await writeOptimizedAsset(assetFile); const savedBytes = result.beforeBytes - result.afterBytes; totalSavedBytes += savedBytes; if (result.mode === 'copied') { copiedCount += 1; console.log( `Copied: ${result.sourceRelativePath} -> ${result.targetRelativePath} (${result.afterBytes} bytes)`, ); continue; } convertedCount += 1; console.log( `Optimized: ${result.sourceRelativePath} -> ${result.targetRelativePath} (${savedBytes >= 0 ? `-${savedBytes}` : `+${Math.abs(savedBytes)}`} bytes)`, ); } console.log( `Optimized asset generation complete. Converted: ${convertedCount}, copied: ${copiedCount}, net saved: ${totalSavedBytes} bytes.`, ); } run() .then(() => { process.exit(0); }) .catch((error) => { const message = error instanceof Error ? (error.stack ?? error.message) : String(error); console.error(`Optimized asset generation failed: ${message}`); process.exit(1); });