// Copies the public popup captures (scripts/shots.mjs) into the frontpage's // static assets and writes the manifest the homepage renders from. // // The site serves the captures themselves rather than the composed store // tiles: the tilt, the ink border and the offset shadow are the site's own // CSS (.shot in web/src/styles/global.css), so the gallery follows the theme // into dark mode instead of carrying a baked-in paper background, and the // hero's lockup is not repeated a few hundred pixels below itself. // // The manifest carries each capture's real pixel size, read here rather than // guessed, so the page can reserve the right box before the image lands and // a re-capture at a different popup width cannot silently reflow the page. // // Usage: node scripts/sync-shots.mjs (also: npm run shots) import { existsSync, mkdirSync, readFileSync, readdirSync, rmSync, writeFileSync } from 'node:fs' import { join, resolve } from 'node:path' import { pathToFileURL } from 'node:url' import { pngSize } from './render-icons.mjs' import { MANIFEST, SHOTS, SITE_DIR, SITE_URL_PREFIX, capturePath } from './shots.mjs' const REPO_ROOT = resolve(import.meta.dirname, '..') /** * Sync the captures into the site and rewrite the manifest. * * @param {object} [options] * @param {string} [options.root] repo root to work against (tests use a copy) * @param {Shot[]} [options.shots] * @returns {{ manifest: object[], removed: string[] }} */ export function syncShots({ root = REPO_ROOT, shots = SHOTS } = {}) { const siteDir = join(root, SITE_DIR) mkdirSync(siteDir, { recursive: true }) const manifest = shots.map((shot) => { const capture = join(root, capturePath(shot)) if (!existsSync(capture)) { throw new Error( `${capturePath(shot)} does not exist — run \`node scripts/capture-status-docs.mjs\``, ) } const png = readFileSync(capture) const { width, height } = pngSize(png) writeFileSync(join(siteDir, `${shot.name}.png`), png) return { name: shot.name, src: `${SITE_URL_PREFIX}/${shot.name}.png`, width, height, alt: shot.alt, caption: shot.caption, } }) // A shot dropped from the list would otherwise stay on the site forever: // the page would stop rendering it, but the deploy would keep uploading it. const kept = new Set(manifest.map((entry) => `${entry.name}.png`)) const removed = readdirSync(siteDir).filter((file) => !kept.has(file)) for (const file of removed) rmSync(join(siteDir, file), { recursive: true, force: true }) writeFileSync(join(root, MANIFEST), `${JSON.stringify(manifest, null, 2)}\n`) return { manifest, removed } } function main() { const { manifest, removed } = syncShots() for (const entry of manifest) { console.log(`shots: ${entry.name}.png ${entry.width}x${entry.height} -> ${SITE_DIR}/`) } for (const file of removed) console.log(`shots: removed stale ${SITE_DIR}/${file}`) console.log(`shots: wrote ${manifest.length} entr${manifest.length === 1 ? 'y' : 'ies'} to ${MANIFEST}`) } if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) { main() }