doxa-maps/library/scripts/posters/publish-html.mjs
Doxa Maps e19697673d
Some checks are pending
Deploy to GitHub Pages / build (push) Waiting to run
Deploy to GitHub Pages / deploy (push) Blocked by required conditions
Doxa Maps — the map bundler, 7 production plugins and the Experimental shelf
2026-09-25 20:07:59 +00:00

110 lines
6 KiB
JavaScript

// publish-html.mjs — put the poster HTML where the app can fetch it, so the
// browser can rasterise a poster instead of the repo shipping 42 MB of .webp.
//
// ★ WHY THIS SCRIPT EXISTS. PosterWall falls back to rendering posters/<id>.html
// in the browser when no pre-rendered image is present (library/packet/
// poster-raster.js). That fallback was shipped and verified against a locally
// served file — but NOTHING PUBLISHED THE HTML. out/ is gitignored, public/ had
// zero .html, so deleting the .webp would have taken poster download from working
// to 404. The rasteriser worked; the feature would not have. This closes that gap.
//
// SCOPE — ZONE POSTERS ONLY, and that is a real distinction, not laziness:
// zone posters token-free, drawn from bundled geoBoundaries. Reproducible
// offline by anyone with the repo -> safe to stop shipping as
// images, because the HTML regenerates the picture.
// city posters need CITIES=1 BASEMAP=1 and fetch OSM STREET TILES at generation
// time. NOT reproducible offline. Deleting those .webp would make
// them unrecoverable without network, so they stay as images.
// Treating "42 MB of posters" as one homogeneous pile would have deleted the
// irreproducible half.
//
// ⚠ RUNNING THIS DOES NOT, BY ITSELF, SHRINK THE REPO. Measured with real git
// packs, for the 24 zone posters that render:
// webp 5.0 MB raw -> ~5.0 MB packed (already compressed; git cannot pack it)
// HTML 21.4 MB raw -> 5.78 MB packed (text, and highly repetitive across posters)
// Swapping one for the other is about +0.8 MB PACKED — a net loss, not the 42 MB
// win the headline suggests. Do not ship that swap and call it a saving.
//
// WHERE THE REAL 22 MB IS: 176 city-* posters, which this script deliberately does
// not touch because they need OSM tiles to regenerate. The zone posters were never
// the expensive half.
//
// So what is this FOR? Two things that are true regardless of size:
// 1. It is the missing half of the browser-rasterise fallback. That fallback
// fetches posters/<id>.html and nothing published it, so a zone whose image is
// absent 404s today. Run this and the fallback has an input.
// 2. It makes the HTML the SOURCE a reader can rasterise at any resolution,
// instead of one fixed raster — useful for a host that would rather serve text
// and let the browser do the work.
// Use it when you are dropping the images deliberately, not as a size optimisation.
//
// node publish-html.mjs # all zone posters found in out/
// ONLY=laos node publish-html.mjs # one
import { readFileSync, writeFileSync, mkdirSync, existsSync, readdirSync } from 'node:fs';
import { fileURLToPath, pathToFileURL } from 'node:url';
import { dirname, join, resolve } from 'node:path';
const __dirname = dirname(fileURLToPath(import.meta.url));
const SRC = join(__dirname, 'out', 'posters');
const REPO = resolve(__dirname, '..', '..', '..');
// ⚠ data/posters, NOT public/assets/posters. public/assets/<name> is a GENERATED
// MIRROR: generate-tree.mjs rmSync's it and re-copies data/<name> over it on every
// `bun run tree`, so anything written there is deleted by the next build. This
// script wrote there and its output therefore never survived — which is a large
// part of why the browser-rasterise fallback still had no input months after it
// shipped. data/ is the tracked source the mirror is made from.
const DEST = join(REPO, 'data', 'posters');
const ONLY = process.env.ONLY || '';
if (!existsSync(SRC)) {
console.error(`[publish-html] no ${SRC} — run \`node generate.mjs\` first.`);
process.exit(1);
}
// ★ THE ZONE IDS THE APP CAN ACTUALLY REACH — ask the LEGEND, not clusters.json.
// Enumerating the raw cluster keys is what this used to do, and it is wrong:
// clusters.json carries 30 list definitions but the legend exposes 28 leaves.
// deaf-old-world and deaf-new-world-asia-pacific are SUPERSEDED by the unified
// `deaf` row (affinity bloc A017) — verified against live data, 63 + 60 = 123 =
// A017 exactly, with zero groups on either side of the difference. They are kept
// in clusters.json deliberately (three isolation-proof scripts assert
// deaf_lists.length === 2), but no user can select them, so a poster for them is
// unreachable. Enumerating them made this script warn that two zones were
// "MISSING" when nothing was missing at all.
const clusters = JSON.parse(readFileSync(
join(REPO, 'plugin-designs', 'my-upg-100-list', 'data', 'clusters.json'), 'utf8'));
const { buildLegendNodes, leafZones } = await import(
pathToFileURL(join(REPO, 'plugin-designs', 'my-upg-100-list', 'data', 'legend-tree.js')).href);
const zoneIds = new Set(leafZones(buildLegendNodes(clusters, { records: null })).map((z) => z.id));
mkdirSync(DEST, { recursive: true });
let published = 0;
let bytes = 0;
const skipped = [];
for (const file of readdirSync(SRC)) {
if (!file.endsWith('.html')) continue;
const id = file.slice(0, -5);
if (ONLY && id !== ONLY) continue;
if (!zoneIds.has(id)) { skipped.push(id); continue; }
const html = readFileSync(join(SRC, file), 'utf8');
writeFileSync(join(DEST, file), html);
published += 1;
bytes += Buffer.byteLength(html);
}
console.log(`[publish-html] ${published} poster HTML -> data/posters/`);
console.log(`[publish-html] ${(bytes / 1048576).toFixed(1)} MB raw `
+ `(gzips to roughly a third over the wire)`);
if (skipped.length) {
// Loud, not silent: a reader should know what was left behind and why.
console.log(`[publish-html] skipped ${skipped.length} non-zone poster(s) `
+ `(city-* posters belong to Key Cities and need OSM tiles to regenerate)`);
}
const missing = [...zoneIds].filter((z) => !existsSync(join(SRC, `${z}.html`)));
if (missing.length && !ONLY) {
console.warn(`[publish-html] ⚠ ${missing.length} zone(s) have NO poster HTML: `
+ `${missing.slice(0, 6).join(', ')}${missing.length > 6 ? '…' : ''}`);
console.warn('[publish-html] their download will 404 until generate.mjs covers them.');
}