134 lines
6.9 KiB
JavaScript
134 lines
6.9 KiB
JavaScript
#!/usr/bin/env node
|
|
/**
|
|
* check-dashboard-geocoder-provider.mjs — a dashboard running on MapLibre must not call Mapbox.
|
|
*
|
|
* THE CONTRACT: a partner who selects MapLibre has selected the keyless engine precisely so that no
|
|
* Mapbox key and no Mapbox traffic is involved. One request to api.mapbox.com sends that visitor's
|
|
* IP to Mapbox and breaks the promise, whatever the page looks like afterwards.
|
|
*
|
|
* ★ WHAT THIS MEASURED, AND IT IS NOT WHERE THE AUDIT POINTED. studio's Mapbox-globals audit named
|
|
* DashboardSearchHeader.vue, which did preload api.mapbox.com/.../mapbox-gl-geocoder.min.js
|
|
* unconditionally and never passed `provider` to the GeocoderComponent it mounts — both now fixed.
|
|
* But that component is NOT MOUNTED by this dashboard (research-dashboard.vue:23 says so in as many
|
|
* words), so fixing it changes nothing here. The four requests this leaf actually makes under
|
|
* ?provider=maplibre are all from the GENERATED LEAF PAGE's own tags:
|
|
*
|
|
* mapbox-gl.css · mapbox-gl.js · mapbox-gl-geocoder.css · mapbox-gl-geocoder.min.js
|
|
*
|
|
* i.e. the leaf ships Mapbox GL and the Mapbox geocoder to every visitor regardless of the engine
|
|
* it was asked for. That is the leaf GENERATOR's doing, not this plugin's, and it is why this gate
|
|
* is deliberately NOT wired into gates:post yet: it would be a red gate about somebody else's file.
|
|
* Run it by name; it names the exact URLs.
|
|
*
|
|
* ★ ZERO IS THE ASSERTION, not "it rendered". A build that quietly kept the Mapbox geocoder would
|
|
* look identical: the bars appear, suggestions work, nothing errors. Only the request log tells the
|
|
* truth, which is why this counts requests rather than looking at the page.
|
|
*
|
|
* ASSERTIONS, on the built tree:
|
|
* maplibre ZERO requests to api.mapbox.com.
|
|
* both whatever search bars this leaf renders still produce suggestions — a zero bought by
|
|
* breaking search is not a fix. MEASURED: this leaf renders NONE, because the search
|
|
* header is not mounted here, so that half reports rather than asserts. Point the gate
|
|
* at a leaf that mounts the bars and it starts asserting again.
|
|
*
|
|
* Exit: 0 pass or SKIP (no built tree, no browser, no data) · 1 fail.
|
|
*/
|
|
import { createServer } from 'node:http'
|
|
import { spawn } from 'node:child_process'
|
|
import { existsSync, readFileSync } from 'node:fs'
|
|
import { resolve, dirname, join } from 'node:path'
|
|
import { fileURLToPath } from 'node:url'
|
|
|
|
const TAG = 'DASHBOARD-GEOCODER-PROVIDER'
|
|
const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..', '..', '..')
|
|
const skip = (why) => { console.log(`${TAG}: SKIP — ${why}`); process.exit(0) }
|
|
|
|
const { TREE_NAME } = await import(join(ROOT, 'internal', '_tree-name.mjs'))
|
|
const MANIFEST = join(ROOT, 'public', TREE_NAME, 'manifest.json')
|
|
if (!existsSync(MANIFEST)) skip('no built manifest (run `bun run tree` first).')
|
|
let chromium
|
|
try { ({ chromium } = await import('playwright')) } catch { skip('playwright is not installed.') }
|
|
|
|
const manifest = JSON.parse(readFileSync(MANIFEST, 'utf8'))
|
|
const bundle = (manifest.bundles || []).find((b) => b.bundle === 'research-dashboard')
|
|
const profile = bundle && (bundle.profiles || []).find((p) => p.iframeUrl)
|
|
if (!profile) skip('the manifest declares no research-dashboard leaf to load.')
|
|
|
|
const freePort = () => new Promise((ok) => {
|
|
const s = createServer().listen(0, '127.0.0.1', () => { const p = s.address().port; s.close(() => ok(p)) })
|
|
})
|
|
const PORT = await freePort()
|
|
const TREE = `http://127.0.0.1:${PORT}/${TREE_NAME}`
|
|
const LEAF = `${TREE}/${String(profile.iframeUrl).replace(/^\.\//, '')}`
|
|
const tree = spawn(process.execPath, ['internal/serve-strict.mjs', '--port', String(PORT)], { cwd: ROOT, stdio: 'ignore' })
|
|
const cleanup = () => { try { tree.kill() } catch { /* already gone */ } }
|
|
await new Promise((r) => setTimeout(r, 900))
|
|
if (!await fetch(`${TREE}/manifest.json`, { signal: AbortSignal.timeout(4000) }).then((r) => r.ok).catch(() => false)) {
|
|
cleanup(); skip('could not serve the built tree locally.')
|
|
}
|
|
const { requireThisTree } = await import(join(ROOT, 'internal', '_serves-this-tree.mjs'))
|
|
await requireThisTree(TREE, ROOT, TAG)
|
|
|
|
const browser = await chromium.launch({ args: ['--enable-unsafe-swiftshader', '--use-gl=angle', '--use-angle=swiftshader'] })
|
|
const fails = []
|
|
const lines = []
|
|
try {
|
|
for (const engine of ['maplibre', 'mapbox']) {
|
|
const page = await browser.newPage({ viewport: { width: 1440, height: 1000 } })
|
|
const mapboxCalls = []
|
|
page.on('request', (r) => { const u = r.url(); if (u.includes('api.mapbox.com')) mapboxCalls.push(u) })
|
|
await page.goto(`${LEAF}?provider=${engine}`, { waitUntil: 'domcontentloaded', timeout: 60000 })
|
|
await page.waitForTimeout(20000)
|
|
|
|
const search = await page.evaluate(async () => {
|
|
const sleep = (ms) => new Promise((r) => setTimeout(r, ms))
|
|
const seen = new Set(); const hits = []
|
|
const walk = (root) => {
|
|
if (!root || seen.has(root)) return
|
|
seen.add(root)
|
|
for (const el of root.querySelectorAll('*')) {
|
|
if (el.matches && el.matches('input.mapboxgl-ctrl-geocoder--input, input.maplibregl-ctrl-geocoder--input')) hits.push(el)
|
|
if (el.shadowRoot) walk(el.shadowRoot)
|
|
}
|
|
}
|
|
walk(document)
|
|
if (!hits.length) return { bars: 0, suggestions: 0 }
|
|
const input = hits[0]
|
|
input.focus()
|
|
input.value = 'Afar'
|
|
input.dispatchEvent(new Event('input', { bubbles: true }))
|
|
for (let i = 0; i < 40; i++) {
|
|
await sleep(500)
|
|
const root = input.getRootNode()
|
|
const n = root.querySelectorAll('.mapboxgl-ctrl-geocoder .suggestions li, .maplibregl-ctrl-geocoder .suggestions li, .suggestions li').length
|
|
if (n) return { bars: hits.length, suggestions: n }
|
|
}
|
|
return { bars: hits.length, suggestions: 0 }
|
|
})
|
|
await page.close()
|
|
|
|
lines.push(` · ${engine.padEnd(9)} api.mapbox.com requests: ${mapboxCalls.length} · search bars: ${search.bars} · suggestions for "Afar": ${search.suggestions}`)
|
|
|
|
if (engine === 'maplibre' && mapboxCalls.length) {
|
|
fails.push(`under maplibre the dashboard made ${mapboxCalls.length} request(s) to api.mapbox.com:`)
|
|
for (const u of [...new Set(mapboxCalls)].slice(0, 8)) fails.push(` ${u.slice(0, 150)}`)
|
|
}
|
|
// Reported, not asserted, when the leaf mounts no bars at all — see the header note.
|
|
if (search.bars && !search.suggestions) {
|
|
fails.push(`under ${engine} the search bar rendered but typing produced no suggestions — zero Mapbox traffic bought by a broken search is not a fix.`)
|
|
}
|
|
}
|
|
} catch (err) {
|
|
fails.push(`the measurement itself failed: ${err && err.message}`)
|
|
} finally {
|
|
await browser.close().catch(() => {})
|
|
cleanup()
|
|
}
|
|
|
|
for (const l of lines) console.log(l)
|
|
if (fails.length) {
|
|
console.log(`\n${TAG}: FAIL`)
|
|
for (const f of fails) console.log(` ✗ ${f}`)
|
|
process.exit(1)
|
|
}
|
|
console.log(`${TAG}: PASS — MapLibre calls Mapbox zero times and still searches; Mapbox is unchanged.`)
|