Files

76 lines
3.1 KiB
JavaScript

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.`);