The gate proves providerFor(map) answers the same as the raw window.maplibregl sniffs it replaced (C3), on both engines and on the ambiguous inputs where the originals fell through to mapbox. It also proves the obvious isMapboxMap() rewrite would have inverted that fall-through. Registered in scripts (alphabetical, with its //scripts note) and gates:post; source-only. From regions-plugin 332dcf74; package.json edited by hand, not taken whole.
111 lines
8 KiB
JavaScript
111 lines
8 KiB
JavaScript
#!/usr/bin/env node
|
|
/* check-provider-for.mjs — run as `bun run check:provider-for`. Source-only, headless, ~0.1s.
|
|
Placed in internal/ with studio's explicit authorisation for this one gate (END-STU-C3AB): it spans
|
|
library/ plus three separate plugins, so no single plugin's scripts/ folder owns it. */
|
|
// C3: do the library helpers behave like the raw-global code they replace, on BOTH engine shapes AND
|
|
// on the ambiguous case that is neither?
|
|
//
|
|
// Same condition the shepherd set for C2: an equivalence gate, old pattern vs new, both engines. The
|
|
// three migrated sites, verbatim in shape from main 1a761741:
|
|
// A1 pplrData.js:115 window.maplibregl && window.maplibregl.Map && map instanceof window.maplibregl.Map ? 'maplibre' : 'mapbox'
|
|
// A2 research-map.vue:1322 !!(window.maplibregl && map.value instanceof window.maplibregl.Map)
|
|
// B all-people-groups:298 mapProvider === 'maplibre' ? window.maplibregl : window.mapboxgl
|
|
//
|
|
// ★ THE AMBIGUOUS CASE IS THE POINT OF THIS GATE, and it nearly cost a silent behaviour change.
|
|
// My first plan was to rewrite A1 and A2 in terms of the EXISTING isMapboxMap(). That is wrong in a
|
|
// way no per-engine test would have caught: both originals test for MAPLIBRE and fall through to
|
|
// mapbox/false, so a stub, a destroyed map, or a page where neither global loaded reads as 'mapbox'.
|
|
// An isMapboxMap() rewrite inverts the fall-through to 'maplibre'/true — changing what a diagnostic
|
|
// field reports and which attribution a poster PRINTS. Hence providerFor() returns undefined and each
|
|
// call site keeps its own default visible. Changing a default is not a curation migration.
|
|
//
|
|
// ★ AND THE PREMISE I HAD WRONG BEFORE THAT: I recorded in SPEC-LIBRARY-CURATION that "engineFor
|
|
// returns a NAME". It does not — it returns the engine NAMESPACE OBJECT (`return ml` / `return mb`).
|
|
// Two of my four bucket-D "must not migrate" conclusions rested on that false premise. Corrected in
|
|
// the spec; the lesson is that a helper's RETURN TYPE is a fact to read, not to infer from its name.
|
|
import { readFileSync } from 'node:fs'
|
|
import { providerFor, engineFor, isMapboxMap } from '../library/composables/useMapProvider.js'
|
|
|
|
// ★ engineGlobal CANNOT BE IMPORTED HERE, and the workaround matters. It lives in useMapInstance.js,
|
|
// whose import chain reaches library/i18n/index.js, which calls `import.meta.glob` -- a VITE
|
|
// transform, so plain Node throws "(intermediate value).glob is not a function". C2's gate never hit
|
|
// this because useMapProvider.js has no such chain.
|
|
// So this EXTRACTS AND EXECUTES engineGlobal's REAL SOURCE LINES rather than a paraphrase of them.
|
|
// Re-typing a three-line function into this gate would test my copy, and a copy silently drifts from
|
|
// the thing it stands for -- the same trap as a tool copied to a new filename that no-ops and exits 0.
|
|
// CDN is stubbed because engineGlobal reads only `.global` from the ENGINES table; the urls are
|
|
// irrelevant to it, which is checkable in the extracted text printed below on failure.
|
|
const umiSrc = readFileSync(new URL('../library/composables/useMapInstance.js', import.meta.url), 'utf8')
|
|
const enginesBlock = /const ENGINES = \{[\s\S]*?\n\}/.exec(umiSrc)
|
|
const engineGlobalSrc = /export function engineGlobal\(provider\) \{[\s\S]*?\n\}/.exec(umiSrc)
|
|
if (!enginesBlock || !engineGlobalSrc) {
|
|
console.error(' FAIL - could not extract ENGINES / engineGlobal from useMapInstance.js -- the gate is NOT MEASURED, not passing')
|
|
process.exit(2)
|
|
}
|
|
const engineGlobal = new Function(
|
|
'const CDN = { mapboxGlJs:{url:""}, mapboxGlCss:{url:""}, maplibreGlJs:{url:""}, maplibreGlCss:{url:""} };\n'
|
|
+ enginesBlock[0] + ';\n'
|
|
+ engineGlobalSrc[0].replace(/^export /, '') + ';\n'
|
|
+ 'return engineGlobal;'
|
|
)()
|
|
|
|
let fails = 0
|
|
const ok = (c, m) => { console.log(` ${c ? 'ok ' : 'FAIL'} - ${m}`); if (!c) fails++ }
|
|
|
|
class MapboxMap {}
|
|
class MaplibreMap {}
|
|
globalThis.window = { mapboxgl: { Map: MapboxMap }, maplibregl: { Map: MaplibreMap } }
|
|
|
|
// the THREE old patterns, as they stood
|
|
const oldA1 = (map) => (window.maplibregl && window.maplibregl.Map && map instanceof window.maplibregl.Map ? 'maplibre' : 'mapbox')
|
|
const oldA2 = (map) => !!(window.maplibregl && map instanceof window.maplibregl.Map)
|
|
const oldB = (provider) => (provider === 'maplibre' ? window.maplibregl : window.mapboxgl)
|
|
// the NEW expressions, exactly as they now read at the call sites
|
|
const newA1 = (map) => providerFor(map) || 'mapbox'
|
|
const newA2 = (map) => providerFor(map) === 'maplibre'
|
|
const newB = (provider) => engineGlobal(provider)
|
|
|
|
// ── 1. A1 and A2 across every map shape, INCLUDING neither ──────────────────────────────────────
|
|
const CASES = [
|
|
['a Mapbox map', new MapboxMap()],
|
|
['a MapLibre map', new MaplibreMap()],
|
|
['AMBIGUOUS: a plain object that is neither', {}],
|
|
['AMBIGUOUS: null', null],
|
|
['AMBIGUOUS: undefined', undefined]
|
|
]
|
|
for (const [label, map] of CASES) {
|
|
ok(oldA1(map) === newA1(map), `A1 ${label}: old "${oldA1(map)}" === new "${newA1(map)}"`)
|
|
ok(oldA2(map) === newA2(map), `A2 ${label}: old ${oldA2(map)} === new ${newA2(map)}`)
|
|
}
|
|
|
|
// ── 2. B across every provider name, including the unrecognised one ─────────────────────────────
|
|
for (const p of ['maplibre', 'mapbox', 'auto', '', undefined, 'nonsense']) {
|
|
ok(oldB(p) === newB(p), `B provider=${JSON.stringify(p)}: both resolve to the same namespace (${oldB(p) === window.maplibregl ? 'maplibregl' : 'mapboxgl'})`)
|
|
}
|
|
|
|
// ── 3. THE INVERSION THIS GATE EXISTS TO PREVENT ────────────────────────────────────────────────
|
|
// Documented as an executable claim, not as prose: isMapboxMap() is NOT a drop-in for these sites.
|
|
const wouldHaveBeen = (map) => (isMapboxMap(map) ? 'mapbox' : 'maplibre')
|
|
ok(wouldHaveBeen({}) !== oldA1({}),
|
|
`PROOF: the isMapboxMap() rewrite I nearly shipped disagrees with the original on the ambiguous case (it says "${wouldHaveBeen({})}", the original says "${oldA1({})}")`)
|
|
ok(wouldHaveBeen(new MapboxMap()) === oldA1(new MapboxMap()) && wouldHaveBeen(new MaplibreMap()) === oldA1(new MaplibreMap()),
|
|
'and it agrees on BOTH real engines -- which is exactly why a per-engine-only gate would have passed it')
|
|
|
|
// ── 4. return TYPES, since inferring one from a name is what went wrong ─────────────────────────
|
|
ok(typeof providerFor(new MaplibreMap()) === 'string', 'providerFor returns a STRING name')
|
|
ok(providerFor(new MaplibreMap()) === 'maplibre' && providerFor(new MapboxMap()) === 'mapbox', 'and the right one per engine')
|
|
ok(providerFor({}) === undefined, 'and undefined for neither -- never a guessed default')
|
|
ok(engineFor(new MaplibreMap()) === window.maplibregl, 'engineFor returns the NAMESPACE OBJECT, not a name (the premise I had backwards)')
|
|
ok(typeof engineGlobal('maplibre') === 'object', 'engineGlobal returns a namespace OBJECT from a name')
|
|
ok(/ENGINES\[provider\]\?\.global \|\| ENGINES\.mapbox\.global/.test(engineGlobalSrc[0]),
|
|
'CONTROL: the EXTRACTED source really is the mapbox-defaulting version -- this is what makes the bucket-C fallback finding true, and it is read from the file, not remembered')
|
|
|
|
// ── 5. NEG CONTROLS: the comparisons above must be capable of failing ───────────────────────────
|
|
ok(oldA1(new MaplibreMap()) !== oldA1(new MapboxMap()), 'NEG CONTROL: A1 discriminates the two engines at all')
|
|
ok(oldB('maplibre') !== oldB('mapbox'), 'NEG CONTROL: B discriminates the two namespaces at all')
|
|
const brokenNewA1 = () => 'mapbox' // a helper that always says mapbox
|
|
ok(!CASES.every(([, m]) => oldA1(m) === brokenNewA1(m)),
|
|
'NEG CONTROL: a helper that always answered "mapbox" would FAIL this gate -- so passing means something')
|
|
|
|
console.log(fails ? `\nC3-EQUIVALENCE: ${fails} FAIL(s)` : '\nC3-EQUIVALENCE: all three migrated sites match the raw-global code they replace, on both engines and on every ambiguous input')
|
|
process.exit(fails ? 1 : 0)
|