#!/usr/bin/env node /** * check-poster-attribution — the poster footer credits whoever actually served the tiles. * * The footer is a LICENCE statement, so it has to be true rather than merely present. * posterDefaults ships '© Mapbox © OpenStreetMap'; under the fleet's MapLibre default Mapbox serves * nothing, OpenFreeMap serves everything, and any poster carrying the default credited a company * with no involvement while omitting the three parties whose licences apply. * * ★ THE FIXTURE IS THE REAL THING, and it is the reason this check exists in this shape. Fetched * 2026-09-28: tiles.openfreemap.org/styles/liberty declares two sources and NEITHER has an * `attribution` field, so reading the style alone returns nothing. The credit lives in the TileJSON * at tiles.openfreemap.org/planet, as HTML with anchors and a © entity, which the engine merges * into the SOURCE object after it loads. A check written against a tidy invented style would have * passed while the real map printed the wrong credit. * * Headless, no network, no browser, no dependencies. Exit 0 pass · 1 fail. */ import { resolve, dirname } from 'node:path' import { fileURLToPath } from 'node:url' import { readFileSync } from 'node:fs' const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..') const { attributionTextFromMap, attributionToText } = await import(resolve(ROOT, 'library/maps/map-attribution.js')) const fails = [] const eq = (what, got, want) => { if (got !== want) fails.push(`${what}\n got: ${JSON.stringify(got)}\n want: ${JSON.stringify(want)}`) } const ok = (what, cond, detail) => { if (!cond) fails.push(what + (detail ? `\n ${detail}` : '')) } // The exact string tiles.openfreemap.org/planet returns. const OFM_TILEJSON_HTML = 'OpenFreeMap ' + '© OpenMapTiles ' + 'Data from OpenStreetMap' const OFM_TEXT = 'OpenFreeMap © OpenMapTiles Data from OpenStreetMap' const fakeMap = ({ sources, live }) => ({ getStyle: () => ({ sources }), getSource: (id) => (live && Object.prototype.hasOwnProperty.call(live, id) ? live[id] : undefined), }) // 1. the real shape: style declares nothing, the loaded source carries the TileJSON credit { const map = fakeMap({ sources: { ne2_shaded: { type: 'raster' }, openmaptiles: { type: 'vector', url: 'https://tiles.openfreemap.org/planet' } }, live: { ne2_shaded: {}, openmaptiles: { attribution: OFM_TILEJSON_HTML } }, }) eq('the OpenFreeMap credit is read from the LOADED source, not the style', attributionTextFromMap(map), OFM_TEXT) ok('and it no longer says Mapbox', !attributionTextFromMap(map).includes('Mapbox')) } // 2. a Mapbox map still credits Mapbox — this must not become "always OpenFreeMap" { const map = fakeMap({ sources: { composite: { type: 'vector' } }, live: { composite: { attribution: '© Mapbox © OpenStreetMap' } }, }) eq('a Mapbox-served map credits Mapbox', attributionTextFromMap(map), '© Mapbox © OpenStreetMap') } // 3. a style that DOES declare attribution is honoured (the source is preferred, not required) { const map = fakeMap({ sources: { s: { type: 'raster', attribution: 'Tiles by Someone' } }, live: {} }) eq('a style-declared attribution is used when there is no live source', attributionTextFromMap(map), 'Tiles by Someone') } { const map = fakeMap({ sources: { s: { type: 'raster', attribution: 'the stale request' } }, live: { s: { attribution: 'what actually came back' } }, }) eq('the LOADED source wins over the style declaration', attributionTextFromMap(map), 'what actually came back') } // 4. repeated credits collapse; several distinct ones all survive { const map = fakeMap({ sources: { a: {}, b: {}, c: {} }, live: { a: { attribution: '© OpenStreetMap' }, b: { attribution: '© OpenStreetMap' }, c: { attribution: 'geoBoundaries' } }, }) eq('the same credit twice is printed once', attributionTextFromMap(map), '© OpenStreetMap geoBoundaries') } // 5. it must never invent, and never throw — an empty answer hands the caller back its fallback { eq('no map -> empty', attributionTextFromMap(null), '') eq('a map with no getStyle -> empty', attributionTextFromMap({}), '') eq('sources with no attribution -> empty', attributionTextFromMap(fakeMap({ sources: { a: {} }, live: { a: {} } })), '') const throwing = { getStyle: () => { throw new Error('style not loaded') } } eq('getStyle throwing before load -> empty, not a crash', attributionTextFromMap(throwing), '') const badSource = { getStyle: () => ({ sources: { a: {} } }), getSource: () => { throw new Error('nope') } } eq('getSource throwing -> empty, not a crash', attributionTextFromMap(badSource), '') } // 6. flattening: a footer is text, and entities must not print raw { eq('anchors are removed', attributionToText('© OpenStreetMap'), '© OpenStreetMap') eq('© becomes ©', attributionToText('© OpenMapTiles'), '© OpenMapTiles') eq('whitespace collapses', attributionToText(' a \n b '), 'a b') ok('no markup survives', !/[<>]/.test(attributionToText(OFM_TILEJSON_HTML))) ok('no undecoded entity survives', !/&[a-z]+;/i.test(attributionToText(OFM_TILEJSON_HTML))) } // 7. the renderer must actually consult it, and must keep the spec's value as a fallback. { const SRC = readFileSync(resolve(ROOT, 'library/composables/poster/useMapPoster.js'), 'utf8') .replace(/\/\*[\s\S]*?\*\//g, '').replace(/^\s*\/\/.*$/gm, '') ok('the renderer imports the helper', /import\s*\{[^}]*attributionTextFromMap[^}]*\}\s*from/.test(SRC)) ok('and calls it with the live source map', /attributionTextFromMap\s*\(\s*sourceMap\s*\)/.test(SRC)) // The spread `...(spec.slots?.footer || {})` contains a brace, so a [^}] class stops short of the // property it is looking for and reports the opposite of the truth. ok('the derived credit reaches the footer slot', /footer\s*:\s*\{[\s\S]{0,160}?attribution\s*:\s*credit/.test(SRC)) ok('an empty credit falls back to the spec rather than printing nothing', /credit\s*\?[\s\S]{0,240}?:\s*spec/.test(SRC), 'a licence footer may never be blank') } if (fails.length) { console.error('POSTER-ATTRIBUTION: FAIL') for (const f of fails) console.error(` ✗ ${f}`) process.exit(1) } console.log('POSTER-ATTRIBUTION: PASS — the credit is read from the LOADED source (where the TileJSON lands) rather than the style or a constant, HTML and entities are flattened for print, duplicates collapse, a Mapbox map still says Mapbox, and an unreadable map falls back instead of printing a blank licence line.')