#!/usr/bin/env node /** * Scans ./img recursively and writes ./images.json — the manifest the * single-page app fetches at runtime. No backend required. * * Usage: * node scripts/generate-manifest.mjs * * Re-run this whenever you add, rename, or remove images. */ import { readdir, stat, writeFile } from "node:fs/promises"; import { join, relative, sep, extname } from "node:path"; import { fileURLToPath } from "node:url"; const ROOT = fileURLToPath(new URL("..", import.meta.url)); const IMG_DIR = join(ROOT, "img"); const OUT_FILE = join(ROOT, "images.json"); const IMAGE_EXTS = new Set([ ".png", ".jpg", ".jpeg", ".gif", ".webp", ".avif", ".svg", ".bmp", ".ico", ".tif", ".tiff", ]); const collator = new Intl.Collator(undefined, { numeric: true, sensitivity: "base" }); async function walk(dir) { const entries = await readdir(dir, { withFileTypes: true }); const files = []; for (const entry of entries) { if (entry.name.startsWith(".")) continue; // skip dotfiles/dirs const full = join(dir, entry.name); if (entry.isDirectory()) { files.push(...(await walk(full))); } else if (entry.isFile() && IMAGE_EXTS.has(extname(entry.name).toLowerCase())) { const info = await stat(full); files.push({ name: entry.name, path: relative(ROOT, full).split(sep).join("/"), // POSIX-style for URLs size: info.size, mtime: info.mtime.toISOString(), }); } } return files; } async function main() { let files; try { files = await walk(IMG_DIR); } catch (err) { if (err.code === "ENOENT") { console.error(`✗ Image folder not found: ${IMG_DIR}`); console.error(" Create it and add some images, then re-run."); process.exit(1); } throw err; } files.sort((a, b) => collator.compare(a.path, b.path)); const manifest = { generatedAt: new Date().toISOString(), count: files.length, images: files, }; await writeFile(OUT_FILE, JSON.stringify(manifest, null, 2) + "\n", "utf8"); console.log(`✓ Wrote ${files.length} image${files.length === 1 ? "" : "s"} to images.json`); } main().catch((err) => { console.error(err); process.exit(1); });