development was RED on gates:post (verify bundles-shipped): country-posters.js had grown to 1.66 MB, over the 1.5 MB ceiling, when the in-browser sheet generator landed. Measured by building without it: the generator alone was 64.9 KB of the bundle (single-file builds inline any import), and Natural Earth's places another ~170 KB (moved to a fetch in 8167415c5). library/packet/poster-render.js is a pure module with no imports, so the tree now serves it verbatim at assets/lib/poster-render.js (one generate-tree line) and posterSheets imports it BY URL at the first print — resolved against the page, not the bundle, and kept out of the build with @vite-ignore. Every map that can print a sheet sheds it; it is downloaded once, by a reader who prints, and cached across maps. Measured on real builds: country-posters 1,662,962 -> 1,472,015 bytes (ceiling 1,500,000); my-upg-100-list 1,231,789; verify-bundles-shipped passes; the bundle holds the runtime import and no generator code. check:poster-browser-equiv now also asserts the served copy is byte-identical to its source, and still passes byte-identical through makeZoneSheet and generateCountrySheet, which import the served copy. gates:pre 73/73.
96 lines
5.9 KiB
JavaScript
96 lines
5.9 KiB
JavaScript
#!/usr/bin/env bun
|
|
/**
|
|
* check-poster-browser-equiv — a map sheet GENERATED IN THE BROWSER is byte-identical to the one the Node
|
|
* generator writes, given the same records.
|
|
*
|
|
* WHY. The print standard generates sheets at the click with the SAME drawing code the Node script runs
|
|
* (library/packet/poster-render.js). What can still drift is the FEED: the boundaries fetched from the
|
|
* served tree, the zone/pin assignment, the name table, the series facts, the date. A difference there
|
|
* prints a different poster depending on where it was made, and nothing else would notice.
|
|
*
|
|
* ★ IT CALLS WHAT THE BROWSER CALLS. The first version rebuilt the feed itself and so could not see a
|
|
* defect in the real entry points: generateCountrySheet passed no province boundaries, and a country
|
|
* beside India or China printed without them — while this gate, feeding them by hand, stayed green. So:
|
|
* · a UPG-100 zone through makeZoneSheet (data/zone-sheet-print.js), the map-only packet sheet, an India
|
|
* zone so the ADM1 path runs;
|
|
* · a country through generateCountrySheet, NEPAL, which sits between India and China;
|
|
* both fetching the served boundaries exactly as the page does (file:// here, the asset URL there).
|
|
*
|
|
* Node side: the target file is deleted first (a stale sheet from an earlier run is never compared), and
|
|
* the generator runs with a CLEAN environment (only what this check sets). If the offline records cache
|
|
* is absent the check cannot run: NOT MEASURED (75). If it is present and the Node generator fails or
|
|
* writes nothing, that is a FAILURE — a broken driver must not read as "not measured".
|
|
*/
|
|
import { readFileSync, existsSync, rmSync } from 'node:fs'
|
|
import { spawnSync } from 'node:child_process'
|
|
import { join } from 'node:path'
|
|
import { pathToFileURL } from 'node:url'
|
|
|
|
const REPO = new URL('..', import.meta.url).pathname
|
|
const P = join(REPO, 'library/scripts/posters')
|
|
const CACHE = join(P, 'out/_records.json')
|
|
const ASSETS = join(REPO, 'public/dx-maps/assets')
|
|
const DATE = '2026-09-29'
|
|
if (!existsSync(CACHE)) {
|
|
console.log('POSTER-BROWSER-EQUIV: NOT MEASURED (exit 75) — no offline records cache at library/scripts/posters/out/_records.json;')
|
|
console.log(' run `node generate.mjs` once with an API configured to write it.')
|
|
process.exit(75)
|
|
}
|
|
if (!existsSync(join(ASSETS, 'boundaries/world-adm0.geojson'))) {
|
|
console.log('POSTER-BROWSER-EQUIV: NOT MEASURED (exit 75) — the served boundaries are not in the tree (public/dx-maps/assets/boundaries); build the tree first.')
|
|
process.exit(75)
|
|
}
|
|
|
|
let failed = false
|
|
// ★ THE SERVED GENERATOR IS THE SOURCE. Browsers import the copy at <assets>/lib/poster-render.js (the tree
|
|
// serves it, so no bundle carries it); Node runs library/packet/poster-render.js. If they differ, "the same
|
|
// generator" is no longer true and the byte comparison below would be between two programs.
|
|
{
|
|
const served = join(ASSETS, 'lib/poster-render.js')
|
|
const same = existsSync(served) && readFileSync(served, 'utf8') === readFileSync(join(REPO, 'library/packet/poster-render.js'), 'utf8')
|
|
console.log(` ${same ? 'ok ' : '✗ '} the served generator (assets/lib/poster-render.js) is byte-identical to its source`)
|
|
if (!same) failed = true
|
|
}
|
|
function nodeSheet(env, file) {
|
|
const out = join(P, 'out/posters', file)
|
|
rmSync(out, { force: true })
|
|
const r = spawnSync('node', ['generate.mjs'], { cwd: P, encoding: 'utf8', timeout: 240000,
|
|
env: { PATH: process.env.PATH, HOME: process.env.HOME, OFFLINE: '1', VITE_API_BASE_URL: 'http://offline.invalid', POSTER_DATE: DATE, ...env } })
|
|
if (r.status !== 0 || !existsSync(out)) {
|
|
console.log(` ✗ the Node generator did not write ${file} (exit ${r.status})\n${(r.stderr || r.stdout || '').slice(-400)}`)
|
|
failed = true
|
|
return null
|
|
}
|
|
return readFileSync(out, 'utf8')
|
|
}
|
|
const json = (p) => JSON.parse(readFileSync(p, 'utf8'))
|
|
const records = json(CACHE)
|
|
const assetsBase = pathToFileURL(ASSETS).href
|
|
const results = []
|
|
|
|
{ // a UPG-100 zone, through the helper every UPG-100 surface prints with
|
|
const { makeZoneSheet } = await import(join(REPO, 'plugin-designs/my-upg-100-list/data/zone-sheet-print.js'))
|
|
const zoneSheet = makeZoneSheet({ allRecords: async () => records, assetsBase: () => assetsBase, profileConfig: null, date: DATE })
|
|
const id = 'asia-india-gujarat'
|
|
results.push({ what: `${id} (packet sheet, makeZoneSheet)`, browser: await zoneSheet({ id }, 'packet'), node: nodeSheet({ FORMAT: 'packet', ONLY: id }, `${id}.packet.html`) })
|
|
}
|
|
{ // a country, through the function the country wall calls — Nepal, between India and China
|
|
const { generateCountrySheet } = await import(join(REPO, 'library/packet/posterSheets.js'))
|
|
const { ISO3_NAME } = await import(join(REPO, 'library/data/iso3-name.js'))
|
|
const tier = json(join(REPO, 'library/data/country-posters.json')).countries.filter((c) => c.sheet === '3x3')
|
|
const c = tier.find((x) => x.id === 'country-npl')
|
|
const recs = records.filter((r) => String(r.country_code).toUpperCase() === c.iso3)
|
|
const series = { totalGroups: tier.reduce((a, x) => a + (x.count || 0), 0), totalLists: tier.length, index: tier.indexOf(c) + 1 }
|
|
results.push({ what: `${c.id} (3x3 poster, generateCountrySheet)`,
|
|
browser: await generateCountrySheet({ country: c, records: recs, assetsBase, ISO3_NAME, format: '3x3', series, date: DATE }),
|
|
node: nodeSheet({ COUNTRIES: '1', FORMAT: '3x3', SHEET: '3x3', ONLY: c.id }, `${c.id}.html`) })
|
|
}
|
|
|
|
for (const r of results) {
|
|
if (r.node == null) continue
|
|
const same = r.browser === r.node && r.browser.length > 1000
|
|
if (!same) failed = true
|
|
console.log(` ${same ? 'ok ' : '✗ '} ${r.what.padEnd(46)} browser ${r.browser.length} bytes · node ${r.node.length} bytes`)
|
|
}
|
|
if (failed) { console.log('POSTER-BROWSER-EQUIV: FAIL — the browser feed and the Node generator do not draw the same sheet'); process.exit(1) }
|
|
console.log('POSTER-BROWSER-EQUIV: PASS — the sheets the browser generates (makeZoneSheet, generateCountrySheet) are byte-identical to the Node generator\'s')
|