166 lines
8.2 KiB
JavaScript
166 lines
8.2 KiB
JavaScript
#!/usr/bin/env node
|
|
/**
|
|
* check-no-silent-engine-substitution — a profile must never fall back to a DIFFERENT map engine than
|
|
* the one its provider asked for.
|
|
*
|
|
* THE DEFECT, found by regions' C3 read and routed by dx-shepherd (END-SHEP-C3C, 2026-09-26):
|
|
*
|
|
* const gl = engineGlobal(provider) || window.mapboxgl
|
|
*
|
|
* That `||` looks like ordinary defensive coding and is not. engineGlobal() ALREADY resolves mapbox's own
|
|
* global for the mapbox case — useMapInstance.js:115 is
|
|
* `ENGINES[provider]?.global || ENGINES.mapbox.global`, which falls back to mapbox's NAME only for an
|
|
* UNKNOWN provider name, never for a missing engine. So the expression can fire in exactly ONE situation:
|
|
* the page asked for 'maplibre' and MapLibre failed to load. It then hands back MAPBOX — the token-gated
|
|
* engine — to a page that explicitly asked for the keyless one, breaking the keyless contract
|
|
* research-map.instances.json documents.
|
|
*
|
|
* ★ AND THE SUBSTITUTE IS BROKEN BY CONSTRUCTION, which is what makes this worth a gate rather than a
|
|
* one-line fix. These profiles gate `gl.accessToken = token` on `if (!isMaplibre)`, so a maplibre request
|
|
* that fell through to Mapbox got Mapbox WITHOUT a token: 401s on every tile instead of a legible failure.
|
|
* A fallback that cannot produce a working map even in principle is strictly worse than no fallback,
|
|
* because it converts a diagnosable error into a mysterious one.
|
|
*
|
|
* ⚠ WHY THE THREE EXISTING ENGINE GATES DID NOT CATCH IT, and why this is not a duplicate of them:
|
|
* check-leaf-engine-per-provider — a leaf loads the engine its provider asks for
|
|
* check-provider-switch — the page-level switch reaches a raw-built map
|
|
* check-auto-engine — 'auto' resolves to Mapbox with a token, MapLibre without
|
|
* All three assert the HAPPY PATH: what happens when engines load. This bug only appears when one does
|
|
* NOT load, so all three were green for as long as it existed. The property here is the negative one —
|
|
* "when the requested engine is absent, no other engine is substituted" — and nothing asserted it.
|
|
*
|
|
* ⚠ IT IS A SOURCE CHECK, ON PURPOSE. Reproducing it at the rendered layer means serving a leaf with
|
|
* MapLibre deliberately blocked, which is a browser run for a defect whose whole signature is one
|
|
* expression. The source is the authoritative place: the fallback either appears in the file or it does
|
|
* not. A browser gate here would cost a slot to measure something a grep decides.
|
|
*
|
|
* Usage: node internal/check-no-silent-engine-substitution.mjs
|
|
* node internal/check-no-silent-engine-substitution.mjs --self-test
|
|
*/
|
|
import { readFileSync, readdirSync, existsSync, statSync } from 'node:fs'
|
|
import { resolve, dirname, join } from 'node:path'
|
|
import { fileURLToPath } from 'node:url'
|
|
|
|
const HERE = dirname(fileURLToPath(import.meta.url))
|
|
const REPO = process.env.DOXA_REPO || resolve(HERE, '..')
|
|
const ROOT = join(REPO, 'plugin-designs')
|
|
|
|
// ⚠ FLAGS FILTERED FROM THE POSITIONAL READ. A bare `--self-test` was otherwise taken as a path, which
|
|
// has bitten this repo three times (--self-test read as a leaf, --emit read as a baseUrl, --poison read
|
|
// as a packet).
|
|
const SELF_TEST = process.argv.includes('--self-test')
|
|
|
|
// The substitution to refuse: engineGlobal(...) OR-ed with a specific engine's global. Any of these
|
|
// named globals on the right of a `||` after engineGlobal is a cross-engine fallback by definition —
|
|
// engineGlobal already returns the RIGHT global for the requested provider, so the only thing an `||`
|
|
// can add is a DIFFERENT engine.
|
|
const ENGINE_GLOBALS = ['mapboxgl', 'maplibregl']
|
|
const PATTERN = new RegExp(
|
|
String.raw`engineGlobal\s*\([^)]*\)\s*\|\|\s*(?:window\.)?(${ENGINE_GLOBALS.join('|')})\b`,
|
|
'g',
|
|
)
|
|
|
|
function scan(text) {
|
|
const hits = []
|
|
const lines = text.split('\n')
|
|
lines.forEach((line, i) => {
|
|
// Skip comment lines: this file's own documentation quotes the bad pattern, and so does every fix
|
|
// that explains what it removed. A gate that fails on its own explanation teaches people to delete
|
|
// the explanation.
|
|
const t = line.trim()
|
|
if (t.startsWith('//') || t.startsWith('*') || t.startsWith('/*')) return
|
|
PATTERN.lastIndex = 0
|
|
let m
|
|
while ((m = PATTERN.exec(line)) !== null) hits.push({ line: i + 1, engine: m[1], text: t.slice(0, 110) })
|
|
})
|
|
return hits
|
|
}
|
|
|
|
if (SELF_TEST) {
|
|
// ★ PROVE THE PATTERN FIRES BEFORE TRUSTING A GREEN RUN. A source gate that has never matched anything
|
|
// is indistinguishable from a broken regex, and this one's whole value is that it matches.
|
|
const should = [
|
|
'const gl = engineGlobal(upgProvider) || window.mapboxgl',
|
|
' const gl = engineGlobal(p)||maplibregl',
|
|
'let gl = engineGlobal( provider ) || window.maplibregl;',
|
|
]
|
|
const shouldNot = [
|
|
'const gl = engineGlobal(upgProvider)',
|
|
'// const gl = engineGlobal(p) || window.mapboxgl <- the removed fallback',
|
|
'const gl = engineGlobal(p) || null',
|
|
'const token = mapboxToken?.value || profileConfig?.value?.tk || \'\'',
|
|
]
|
|
let bad = 0
|
|
for (const s of should) {
|
|
const n = scan(s).length
|
|
console.log(` ${n ? 'MATCHED ' : 'MISSED '} ${s.trim().slice(0, 62)}`)
|
|
if (!n) bad++
|
|
}
|
|
for (const s of shouldNot) {
|
|
const n = scan(s).length
|
|
console.log(` ${n ? 'FALSE+ ' : 'ignored '} ${s.trim().slice(0, 62)}`)
|
|
if (n) bad++
|
|
}
|
|
if (bad) {
|
|
console.error(`\nNO-SILENT-ENGINE-SUBSTITUTION --self-test: FAIL — ${bad} case(s) wrong.`)
|
|
process.exit(1)
|
|
}
|
|
console.log('\nNO-SILENT-ENGINE-SUBSTITUTION --self-test: PASS — the pattern matches every real form '
|
|
+ 'and ignores comments, a null fallback and an unrelated token chain.')
|
|
process.exit(0)
|
|
}
|
|
|
|
if (!existsSync(ROOT)) {
|
|
console.log(`NO-SILENT-ENGINE-SUBSTITUTION: NOT MEASURED — no plugin-designs tree at ${ROOT}`)
|
|
process.exit(75)
|
|
}
|
|
|
|
const findings = []
|
|
let scanned = 0
|
|
const walk = (dir) => {
|
|
for (const e of readdirSync(dir, { withFileTypes: true })) {
|
|
const p = join(dir, e.name)
|
|
if (e.isDirectory()) {
|
|
if (e.name === 'node_modules' || e.name === 'dist' || e.name.startsWith('.')) continue
|
|
walk(p)
|
|
} else if (/\.(vue|js|mjs)$/.test(e.name)) {
|
|
let text
|
|
try { text = readFileSync(p, 'utf8') } catch { continue }
|
|
if (!text.includes('engineGlobal')) continue
|
|
scanned++
|
|
for (const h of scan(text)) findings.push({ file: p.slice(REPO.length + 1), ...h })
|
|
}
|
|
}
|
|
}
|
|
walk(ROOT)
|
|
|
|
console.log('NO-SILENT-ENGINE-SUBSTITUTION')
|
|
console.log(` ${scanned} file(s) call engineGlobal()`)
|
|
|
|
// ⚠ REFUSE TO JUDGE rather than pass vacuously. If nothing in the tree calls engineGlobal at all, the
|
|
// pattern had nothing to match and a green result would mean "I found no files", not "the code is clean".
|
|
if (!scanned) {
|
|
console.log('NO-SILENT-ENGINE-SUBSTITUTION: NOT MEASURED — no file calls engineGlobal(), so there was '
|
|
+ 'nothing to check. Exit 75 rather than 0: "found nothing" is not "found nothing wrong".')
|
|
process.exit(75)
|
|
}
|
|
|
|
if (findings.length) {
|
|
console.error(`\nNO-SILENT-ENGINE-SUBSTITUTION: FAIL — ${findings.length} silent engine substitution(s).`)
|
|
for (const f of findings) {
|
|
console.error(` ✗ ${f.file}:${f.line} falls back to ${f.engine}`)
|
|
console.error(` ${f.text}`)
|
|
}
|
|
console.error('\n engineGlobal() ALREADY returns the right global for the requested provider, so an `||`')
|
|
console.error(' after it can only supply a DIFFERENT engine — and it fires only when the page asked for')
|
|
console.error(' maplibre and maplibre failed to load. A page that asked for the KEYLESS engine then')
|
|
console.error(' receives the TOKEN-GATED one, and because accessToken is gated on !isMaplibre it')
|
|
console.error(' receives it WITHOUT a token: 401s instead of a legible error.')
|
|
console.error('\n FIX: delete the fallback. Keep the requested engine, and let the profile\'s existing')
|
|
console.error(' !gl branch fail visibly — console.warn plus the map\'s error/empty state, never a blank,')
|
|
console.error(' and never another engine. Name the engine in the message so a partner can tell an')
|
|
console.error(' honoured request from a silent downgrade.')
|
|
process.exit(1)
|
|
}
|
|
console.log('NO-SILENT-ENGINE-SUBSTITUTION: PASS — every engineGlobal() call keeps the engine its provider '
|
|
+ 'asked for.')
|