import { readdir, readFile, writeFile } from 'node:fs/promises'; import { extname, resolve } from 'node:path'; import sharp from 'sharp'; import { allowedUploadExtensions, maxBuiltImageBytes, maxBuiltImageDimension, } from './image-policy.mjs'; const key = process.env.SITE_KEY; if (!key || !/^[a-z0-9][a-z0-9-]{0,62}$/.test(key)) throw new Error('SITE_KEY is required to optimize built media.'); const uploadRoot = resolve(`dist/${key}/images/uploads`); const entries = await readdir(uploadRoot, { withFileTypes: true }); const qualitySteps = [82, 76, 70, 64, 58, 52, 46]; const dimensionSteps = [1920, 1600, 1440, 1280, 1024, 800]; function encode(pipeline, extension, quality) { if (extension === '.jpg' || extension === '.jpeg') { return pipeline.jpeg({ quality, progressive: true, chromaSubsampling: '4:2:0' }); } if (extension === '.png') { return pipeline.png({ compressionLevel: 9, effort: 7, palette: true, quality }); } if (extension === '.webp') return pipeline.webp({ quality, effort: 6 }); if (extension === '.avif') return pipeline.avif({ quality, effort: 5, chromaSubsampling: '4:2:0' }); throw new Error(`Unsupported CMS image extension: ${extension}`); } async function optimize(source, extension) { let smallest; for (const dimension of dimensionSteps) { for (const quality of qualitySteps) { const pipeline = sharp(source, { failOn: 'warning', limitInputPixels: 100_000_000 }) .rotate() .resize({ width: Math.min(dimension, maxBuiltImageDimension), height: Math.min(dimension, maxBuiltImageDimension), fit: 'inside', withoutEnlargement: true, }); const output = await encode(pipeline, extension, quality).toBuffer(); if (!smallest || output.length < smallest.length) smallest = output; if (output.length <= maxBuiltImageBytes) return output; } } throw new Error(`Unable to optimize an image below ${maxBuiltImageBytes} bytes; smallest result was ${smallest?.length ?? 0} bytes.`); } let inputBytes = 0; let outputBytes = 0; let optimizedCount = 0; for (const entry of entries) { if (!entry.isFile()) throw new Error(`Nested or special CMS media paths are not allowed: ${entry.name}`); const extension = extname(entry.name).toLowerCase(); if (!allowedUploadExtensions.has(extension)) throw new Error(`CMS media is not an allowed raster extension: ${entry.name}`); const path = resolve(uploadRoot, entry.name); const source = await readFile(path); const output = await optimize(source, extension); await writeFile(path, output); const metadata = await sharp(output).metadata(); if ((metadata.width ?? 0) > maxBuiltImageDimension || (metadata.height ?? 0) > maxBuiltImageDimension) { throw new Error(`Optimized image dimensions exceed ${maxBuiltImageDimension}px: ${entry.name}`); } if (metadata.exif || metadata.icc || metadata.iptc || metadata.xmp) { throw new Error(`Optimized image retained metadata: ${entry.name}`); } inputBytes += source.length; outputBytes += output.length; optimizedCount += 1; } console.log(`Optimized ${optimizedCount} built CMS images from ${inputBytes} to ${outputBytes} bytes; metadata removed.`);