import { mkdir, readFile, readdir, writeFile } from "node:fs/promises"; import path from "node:path"; import { fileURLToPath } from "node:url"; import sharp from "sharp"; const root = fileURLToPath(new URL("../../../", import.meta.url)); const sources = path.join(root, "tools/figures/assets"); const output = path.join(root, "apps/docs/public/figures"); const check = process.argv.includes("--check"); const policy = JSON.parse(await readFile(path.join(root, "content/image-policy.json"), "utf8")); async function files(directory) { const entries = await readdir(directory, { withFileTypes: true }); return (await Promise.all(entries.map((entry) => { const name = path.join(directory, entry.name); return entry.isDirectory() ? files(name) : [name]; }))).flat().sort(); } // Originals stay outside public; both inline and lightbox images use the WebP. for (const name of await files(path.join(root, "apps/docs/public"))) { if (/\.(?:png|jpe?g|tiff?|bmp)$/i.test(name)) { throw new Error(`Original raster exposed in public: ${path.relative(root, name)}. Move it to tools/figures/assets and run images.`); } } await mkdir(output, { recursive: true }); for (const source of await files(sources)) { if (!/\.(?:png|jpe?g|tiff?|bmp)$/i.test(source)) continue; const relative = path.relative(sources, source).replace(/\.[^.]+$/, ".webp"); const destination = path.join(output, relative); const original = await readFile(source); const encoded = await sharp(original) .rotate() .resize({ width: policy.maxWidth, withoutEnlargement: true }) .webp({ quality: policy.quality, effort: 6 }) .toBuffer(); if (encoded.length > policy.maxBytes) { throw new Error(`${relative}: ${encoded.length} bytes exceeds the ${policy.maxBytes}-byte image budget.`); } if (check) { const generated = await readFile(destination); if (!encoded.equals(generated)) throw new Error(`${relative} is stale. Regenerate browser images on the confirmed remote runner.`); } else { await mkdir(path.dirname(destination), { recursive: true }); await writeFile(destination, encoded); } console.log(`${check ? "Verified" : "Generated"} ${relative}: ${original.length} → ${encoded.length} bytes (${(100 * (1 - encoded.length / original.length)).toFixed(1)}% smaller)`); }