Ship every poster once, in its lightest printable form
With posterSources on, the tree shipped only the 28 zone poster sources, so exporting any of the 176 city or 174 country posters said "poster not found (404)": 350 of 378 posters could not be printed. Each poster now ships once: its .html source where one exists, else its .webp. Generating sources for the 350 was not an option: city posters need OSM street tiles to regenerate (publish-html.mjs), and a source is the heavy form (about 1 MB each against about 0.1 MB for a raster). Measured with check:tree-weight: 102.7 MB before, 137.9 MB after, of 160 MB. The 350 rasters are the tracked data/posters/*.webp: all 350 are existing git blobs, so the repository gains no new pack bytes. New gate check:posters-exportable (in gates:post). Every poster id, from data/posters and the wall's previews, must ship a real WebP or a real poster source (its class="stage" element), so a placeholder or a host's fallback page cannot pass. It skips when posters are off by config, which check:poster-degrades covers. Poison-tested: a missing poster and a fake image each fail.
This commit is contained in:
parent
25828469f3
commit
ccb159f56f
3 changed files with 91 additions and 9 deletions
69
internal/check-posters-exportable.mjs
Normal file
69
internal/check-posters-exportable.mjs
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
#!/usr/bin/env node
|
||||
// check-posters-exportable — every poster a reader can ask for ships in a printable form.
|
||||
//
|
||||
// ★ THE DEFECT (2026-09-29). With posterSources on, the tree shipped only the 28 zone sources.
|
||||
// The 176 city-* and 174 country-* posters have no source, so their export said "poster not
|
||||
// found (404)": 350 of 378 posters could not be printed. generate-tree now ships each poster
|
||||
// ONCE in its lightest printable form, the .html source where one exists, else the .webp.
|
||||
// This gate keeps it that way.
|
||||
//
|
||||
// With posterSources or bundlePosters on, every poster id (from data/posters, and every wall
|
||||
// card in the tree's previews/) must have, in <tree>/assets/posters:
|
||||
// <id>.webp that really is a WebP (RIFF....WEBP), or
|
||||
// <id>.html that really is a poster source (its `class="stage"` element),
|
||||
// so a placeholder or a host's fallback page cannot pass for one.
|
||||
// With both off, posters are not shipped on purpose and export refuses honestly: SKIP
|
||||
// (check:poster-degrades covers that case).
|
||||
//
|
||||
// Static: reads files, starts nothing. Exit 0 pass or skip, 1 fail.
|
||||
import { existsSync, openSync, readSync, closeSync, readFileSync, readdirSync } from 'node:fs'
|
||||
import { dirname, join, resolve } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { TREE_NAME } from './_tree-name.mjs'
|
||||
|
||||
const REPO = resolve(dirname(fileURLToPath(import.meta.url)), '..')
|
||||
const cfg = JSON.parse(readFileSync(join(REPO, 'vite.doxa-maps-build-config.json'), 'utf8'))
|
||||
if (cfg.posterSources !== true && cfg.bundlePosters !== true) {
|
||||
console.log('POSTERS-EXPORTABLE: SKIP — posterSources and bundlePosters are off, so posters are not shipped (export refuses honestly)')
|
||||
process.exit(0)
|
||||
}
|
||||
|
||||
const assets = join(REPO, 'public', TREE_NAME, 'assets')
|
||||
const posters = join(assets, 'posters')
|
||||
if (!existsSync(posters)) {
|
||||
console.error(`POSTERS-EXPORTABLE: FAIL — ${posters} is missing; run \`bun run tree\``)
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
const stem = (f) => f.replace(/\.(webp|html)$/, '')
|
||||
const ids = new Set(readdirSync(join(REPO, 'data', 'posters')).filter((f) => /\.(webp|html)$/.test(f)).map(stem))
|
||||
const previews = join(assets, 'previews')
|
||||
if (existsSync(previews)) for (const f of readdirSync(previews)) if (f.endsWith('.webp')) ids.add(stem(f))
|
||||
|
||||
const isWebp = (path) => {
|
||||
const fd = openSync(path, 'r')
|
||||
const b = Buffer.alloc(12)
|
||||
try { readSync(fd, b, 0, 12, 0) } finally { closeSync(fd) }
|
||||
return b.toString('latin1', 0, 4) === 'RIFF' && b.toString('latin1', 8, 12) === 'WEBP'
|
||||
}
|
||||
const isSource = (path) => readFileSync(path, 'utf8').includes('class="stage"')
|
||||
|
||||
const missing = []
|
||||
const bad = []
|
||||
let webp = 0
|
||||
let html = 0
|
||||
for (const id of [...ids].sort()) {
|
||||
const w = join(posters, `${id}.webp`)
|
||||
const h = join(posters, `${id}.html`)
|
||||
if (existsSync(w)) { if (isWebp(w)) { webp += 1; continue } bad.push(`${id}.webp is not a WebP image`) }
|
||||
if (existsSync(h)) { if (isSource(h)) { html += 1; continue } bad.push(`${id}.html is not a poster source (no class="stage")`) }
|
||||
if (!existsSync(w) && !existsSync(h)) missing.push(id)
|
||||
}
|
||||
|
||||
if (missing.length || bad.length) {
|
||||
console.error(`POSTERS-EXPORTABLE: FAIL — ${missing.length} poster(s) with nothing printable, ${bad.length} not what they claim, of ${ids.size}`)
|
||||
for (const id of missing.slice(0, 20)) console.error(` missing ${id} (neither .webp nor .html in ${TREE_NAME}/assets/posters)`)
|
||||
for (const b of bad.slice(0, 20)) console.error(` bad ${b}`)
|
||||
process.exit(1)
|
||||
}
|
||||
console.log(`POSTERS-EXPORTABLE: PASS — all ${ids.size} posters print: ${html} from their source (.html), ${webp} from a raster (.webp)`)
|
||||
|
|
@ -108,10 +108,11 @@ function readBuildConfig() {
|
|||
// from previews/, so the tree stays visually complete without them; bundling both pushed the
|
||||
// tree to 74 MB and the catalog server refused the pack. Turn on only for a fully offline drop.
|
||||
const bundlePosters = cfg.bundlePosters === true
|
||||
// Ship the poster SOURCES (<id>.html, what the browser rasterises) without the
|
||||
// 40.9 MB of rasters. bundlePosters is a single boolean over a directory holding
|
||||
// both, so it could not express this — and "neither" is what left export and print
|
||||
// preview 404ing on a default drop. Implied by bundlePosters:true.
|
||||
// Ship every poster ONCE, in its lightest printable form: its SOURCE (<id>.html, which the
|
||||
// browser rasterises) if it has one, else its raster (<id>.webp). bundlePosters is a single
|
||||
// boolean over a directory holding both, so it could not express this — and "neither" is
|
||||
// what left export and print preview 404ing on a default drop. Implied by bundlePosters:true
|
||||
// (which ships both forms of every poster).
|
||||
const posterSources = cfg.posterSources === true || bundlePosters
|
||||
return { name, paths, embedBaseUrl, tokenUrl, token, assetsBaseUrl, apiBaseUrl, gatedProfiles, qaWidget, bundlePosters, posterSources }
|
||||
} catch {
|
||||
|
|
@ -1833,9 +1834,16 @@ function main() {
|
|||
// which is the only combination the old boolean could not express.
|
||||
// The code default stays FALSE (real weight is a per-deployment decision); the dx-maps config
|
||||
// turns it ON, because the prayer packet's map page fetches these and printed blank without them.
|
||||
// The copies are byte-identical to the tracked data/posters/*.html, so git stores each once.
|
||||
// ★ SOURCES ALONE LEFT 350 OF 378 POSTERS UNPRINTABLE (2026-09-29). Only the 28 zone posters
|
||||
// have a source; the 176 city-* need OSM street tiles to regenerate (not reproducible offline,
|
||||
// see library/scripts/posters/publish-html.mjs) and the 174 country-* have none, so their
|
||||
// export said "poster not found (404)". So posterSources ships each poster ONCE: the source
|
||||
// where one exists (28, ~28 MB), the raster for the rest (350, ~37 MB). HTML is the heavy form
|
||||
// (~1 MB per source vs ~0.1 MB per raster), so generating sources for the 350 would have
|
||||
// multiplied the weight; and the rasters are the tracked data/posters/*.webp, byte-identical,
|
||||
// so git stores each once and the published tree adds ~0 new pack bytes.
|
||||
const posterExts = DEST_CONFIG.bundlePosters ? null // everything
|
||||
: (DEST_CONFIG.posterSources ? ['.html'] : []) // sources only / nothing
|
||||
: (DEST_CONFIG.posterSources ? ['.html', '.webp'] : []) // lightest form each / nothing
|
||||
const assetDirs = (posterExts === null || posterExts.length)
|
||||
? ['previews', 'posters', 'fonts', 'images']
|
||||
: ['previews', 'fonts', 'images']
|
||||
|
|
@ -1846,8 +1854,12 @@ function main() {
|
|||
// Selective copy: only the extensions this deployment asked for.
|
||||
mkdirSync(join(treeAssets, dirName), { recursive: true })
|
||||
let n = 0
|
||||
for (const f of readdirSync(src)) {
|
||||
const files = readdirSync(src)
|
||||
// A poster with a source never also ships its raster (lightest printable form).
|
||||
const hasSource = new Set(files.filter((f) => f.endsWith('.html')).map((f) => f.slice(0, -'.html'.length)))
|
||||
for (const f of files) {
|
||||
if (!posterExts.some((e) => f.endsWith(e))) continue
|
||||
if (f.endsWith('.webp') && hasSource.has(f.slice(0, -'.webp'.length))) continue
|
||||
cpSync(join(src, f), join(treeAssets, dirName, f))
|
||||
n += 1
|
||||
}
|
||||
|
|
@ -1860,7 +1872,7 @@ function main() {
|
|||
if (DEST_CONFIG.bundlePosters) {
|
||||
console.log('[generate-tree] posters/ bundled in full (bundlePosters:true) — rasters + sources')
|
||||
} else if (DEST_CONFIG.posterSources) {
|
||||
console.log('[generate-tree] posters/*.html bundled (posterSources:true) — no rasters; export and print RASTERISE in the browser')
|
||||
console.log('[generate-tree] posters/ bundled once each (posterSources:true) — the .html source where one exists (rasterised in the browser), else the .webp')
|
||||
} else {
|
||||
console.log('[generate-tree] posters/ NOT bundled — the wall renders from previews; export and print need posterSources:true, bundlePosters:true, or an absolute assetsBaseUrl')
|
||||
}
|
||||
|
|
|
|||
|
|
@ -233,6 +233,7 @@
|
|||
"check:poster-degrades": "node internal/check-poster-degrades-honestly.mjs",
|
||||
"check:poster-layout-optin": "node internal/check-poster-layout-optin.mjs",
|
||||
"check:poster-print-fallback": "node internal/check-poster-print-fallback.mjs",
|
||||
"check:posters-exportable": "node internal/check-posters-exportable.mjs",
|
||||
"check:poster-prints": "node internal/check-poster-prints.mjs",
|
||||
"check:poster-raster": "node internal/check-poster-raster.mjs",
|
||||
"check:poster-wall-localised": "node internal/check-poster-wall-localised.mjs",
|
||||
|
|
@ -316,7 +317,7 @@
|
|||
"dev": "bun x vite",
|
||||
"fix:sri": "node internal/fix-sri.mjs",
|
||||
"gallery": "node internal/generate-gallery.mjs",
|
||||
"gates:post": "node internal/run-parallel.mjs --label=post -- check:capability-derived check:drop check:parse verify check:sri check:public-hygiene check:label-joins check:facet-cascade check:facet-one-field check:heartbeat check:kpi-unit check:composite-reg check:descriptions check:no-abs-paths check:no-codename check:dep-names check:composite-srcs check:embed-targets check:nested-token check:i18n-parity check:tabs-localized check:packets-all check:packet-strings-parity check:zone-census check:zoning-declared check:share-policy check:library-index check:csv-shape check:capability-citations check:catalog-identity check:no-orphan-gates check:quoted-scripts check:done-evidence check:script-flags check:backlog-status check:capability-exemplars check:packet-fixture check:packet-browser check:provider-switch check:locale-reachable check:url-resolver check:manifest-covers check:one-palette check:pin-dim-slider check:legend-source check:backlog-refs check:clustering-modes check:composite-embed-knobs check:composite-locale-leaks check:linked-vocabulary check:wp-embed check:dashboard-geocoder-provider check:leaf-engine-per-provider check:map-props-unit check:composites-mount check:composite-interop check:no-free-identifiers check:map-fly-reduced-motion check:mst-clustering check:list-prop-unit check:profiles-mount check:cdn-pins check:no-eval check:popup-escaping check:i18n-treeshake check:pinned-umd check:one-palette:self-test check:no-cross-plugin-imports check:no-cross-plugin-imports:self-test check:map-attribution check:studio-rail check:approx-pin-note check:embedder-keys check:provider-for check:legend-logical-insets check:legend-logical-insets:self-test check:optional-t-equivalence check:text-highlight check:religion-label-not-value check:i18n-stopped-scope check:no-silent-engine check:regions-uses-library check:shadow-css-parses check:square-map-controls check:preset-config-merge check:instance-config-keys check:frozen-labels check:plural-forms check:region-fill-repaint check:poster-print-fallback check:country-names check:reduced-motion check:no-dead-library check:universal-props",
|
||||
"gates:post": "node internal/run-parallel.mjs --label=post -- check:capability-derived check:drop check:parse verify check:sri check:public-hygiene check:label-joins check:facet-cascade check:facet-one-field check:heartbeat check:kpi-unit check:composite-reg check:descriptions check:no-abs-paths check:no-codename check:dep-names check:composite-srcs check:embed-targets check:nested-token check:i18n-parity check:tabs-localized check:packets-all check:packet-strings-parity check:zone-census check:zoning-declared check:share-policy check:library-index check:csv-shape check:capability-citations check:catalog-identity check:no-orphan-gates check:quoted-scripts check:done-evidence check:script-flags check:backlog-status check:capability-exemplars check:packet-fixture check:packet-browser check:provider-switch check:locale-reachable check:url-resolver check:manifest-covers check:one-palette check:pin-dim-slider check:legend-source check:backlog-refs check:clustering-modes check:composite-embed-knobs check:composite-locale-leaks check:linked-vocabulary check:wp-embed check:dashboard-geocoder-provider check:leaf-engine-per-provider check:map-props-unit check:composites-mount check:composite-interop check:no-free-identifiers check:map-fly-reduced-motion check:mst-clustering check:list-prop-unit check:profiles-mount check:cdn-pins check:no-eval check:popup-escaping check:i18n-treeshake check:pinned-umd check:one-palette:self-test check:no-cross-plugin-imports check:no-cross-plugin-imports:self-test check:map-attribution check:studio-rail check:approx-pin-note check:embedder-keys check:provider-for check:legend-logical-insets check:legend-logical-insets:self-test check:optional-t-equivalence check:text-highlight check:religion-label-not-value check:i18n-stopped-scope check:no-silent-engine check:regions-uses-library check:shadow-css-parses check:square-map-controls check:preset-config-merge check:instance-config-keys check:frozen-labels check:plural-forms check:region-fill-repaint check:poster-print-fallback check:posters-exportable check:country-names check:reduced-motion check:no-dead-library check:universal-props",
|
||||
"gates:pre": "node internal/run-parallel.mjs --label=pre -- check:template-literal check:mjs-parse check:sfc-parse check:tab-radius check:synced-css check:doc-scripts check:setup-order check:package-json-unique-keys check:no-repo-pb-hooks check:no-credentials-omit check:no-credentials-omit:self-test check:shadow-reduced-motion check:poster-action-print check:targets check:targets:self-test check:browser-gates-guarded check:pin-selection check:facet-shape check:hardcoded-english:self-test check:list-selection check:selector-fields check:upg-zone-joins check:gospel-set-charts check:gospel-set-fit check:describe-group check:one-scroll-container check:packet-charts check:composite-token-url check:language-tree-layout check:i18n-keys-exist check:packet-variants check:profile-table check:element-bootstrap check:embed-url-absolute check:frame-target check:language-tree-model check:map-menu-anchor check:selection-kinds check:tiered-points check:whiteboard-projection check:mercator-slice check:ring-vocabulary check:map-props-unit check:mobile-gating check:provide-shadowing check:legend-top-offset check:cdn-cache check:plural-forms check:select-zone-race check:script-registry check:map-control-overlap check:poster-attribution check:gospel-whiteboard check:packet-map-missing-loud check:poster-layout-optin check:ported-globals-identical check:ring-bands check:packet-a1-structure check:zone-title-tier-a check:evangelical-share check:map-labels check:packet-offline check:help-reachable check:gated-not-in-tree check:window-globals-released check:legend-data-contract check:no-new-driver-references",
|
||||
"gen:iso3-to-alpha2": "node library/scripts/generate-iso3-to-alpha2.mjs",
|
||||
"internet:adm1": "node plugin-designs/cities-internet/scripts/build-adm1-internet.mjs",
|
||||
|
|
|
|||
Loading…
Reference in a new issue