Merge branch 'main' of https://git.nuxtbase.app/doxa-life/doxa-maps into bundler-1/work
This commit is contained in:
commit
7fd75a0965
7 changed files with 193 additions and 37 deletions
|
|
@ -0,0 +1,134 @@
|
|||
#!/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.`)
|
||||
|
|
@ -32,6 +32,7 @@ import { inject, ref, computed, unref, onMounted } from 'vue'
|
|||
import GeocoderComponent from '@map/components/map-controls/GeocoderComponent.vue'
|
||||
import { useGeocoderSearch } from '@map/composables/useGeocoderSearch.js'
|
||||
import { useShadowStyles } from '@map/composables/useShadowStyles.js'
|
||||
import { resolveMapProvider } from '@map/composables/useMapProvider.js'
|
||||
|
||||
const bus = inject('selectionBus', null)
|
||||
const dash = inject('dashboardData', null)
|
||||
|
|
@ -43,6 +44,15 @@ const mapboxToken = inject('mapboxToken', ref(''))
|
|||
// map profiles wire :data-source-id="dataSource" (DASH-FIX-2).
|
||||
const searchSourceId = inject('searchSourceId', '')
|
||||
|
||||
// ★ WHICH ENGINE THIS DASHBOARD IS RUNNING ON, because the search bar has to agree with it.
|
||||
// The same ladder every map panel uses: profile-config.provider -> ?provider= -> window.MAP_PROVIDER
|
||||
// -> mapbox.
|
||||
// inject() at setup top level, NOT inside the computed: Vue only resolves an injection during
|
||||
// setup, and a call from inside a computed returns undefined with a warning — which would have
|
||||
// silently resolved every host to the default engine, i.e. exactly the bug being fixed.
|
||||
const profileConfig = inject('profileConfig', null)
|
||||
const provider = computed(() => resolveMapProvider(profileConfig))
|
||||
|
||||
const token = computed(() => unref(mapboxToken) || '')
|
||||
|
||||
// ── Host-independent geocoder-plugin loader ────────────────────────────────────
|
||||
|
|
@ -52,10 +62,21 @@ const token = computed(() => unref(mapboxToken) || '')
|
|||
// host remembering to add that script. So load the plugin from the Mapbox CDN
|
||||
// ourselves (mirrors how GeocoderComponent injects the geocoder CSS at runtime)
|
||||
// and only mount the bars once it's ready — works in ANY host with zero wiring.
|
||||
// ★ AND UNDER MapLibre IT MUST NOT BE THE MAPBOX ONE. This preloader fetched
|
||||
// api.mapbox.com/.../mapbox-gl-geocoder.min.js unconditionally, so a partner who chose MapLibre
|
||||
// precisely to avoid Mapbox still sent every visitor's IP to api.mapbox.com the moment this
|
||||
// dashboard mounted — the keyless embed's whole reason for existing, cancelled by one hardcoded
|
||||
// URL. (Found by studio's Mapbox-globals audit.)
|
||||
//
|
||||
// GeocoderComponent already loads whichever plugin its `provider` says (ensureMapboxGeocoder /
|
||||
// ensureMaplibreGeocoder), so under MapLibre the right thing to do here is NOTHING: mark ready and
|
||||
// let the component fetch its own from jsDelivr. The mapbox preload stays for the mapbox path,
|
||||
// where it is what keeps a host from having to remember a <script> tag.
|
||||
const GEOCODER_JS = 'https://api.mapbox.com/mapbox-gl-js/plugins/mapbox-gl-geocoder/v5.0.0/mapbox-gl-geocoder.min.js'
|
||||
const pluginReady = ref(false)
|
||||
function ensureGeocoderPlugin() {
|
||||
if (typeof window === 'undefined') return
|
||||
if (provider.value === 'maplibre') { pluginReady.value = true; return }
|
||||
if (typeof window.MapboxGeocoder !== 'undefined') { pluginReady.value = true; return }
|
||||
let s = document.querySelector(`script[src="${GEOCODER_JS}"]`)
|
||||
if (!s) {
|
||||
|
|
@ -200,6 +221,7 @@ useShadowStyles(`
|
|||
v-if="pluginReady"
|
||||
:ref="(el) => { bar.geocoderRef.value = el }"
|
||||
standalone
|
||||
:provider="provider"
|
||||
:access-token="token"
|
||||
:data-source-id="searchSourceId"
|
||||
:placeholder="bar.entry.label"
|
||||
|
|
|
|||
|
|
@ -945,7 +945,7 @@
|
|||
"bundle": "research-dashboard",
|
||||
"scriptUrl": "./research-dashboard/research-dashboard.js",
|
||||
"output": "dx-maps/research-dashboard/research-dashboard.js",
|
||||
"integrity": "sha384-GsBGCy2M1ZnE7Xximx1HpGih2povZWx98661bjzLdaH6pnrTEhZj/PoPDuQ2gfJ5",
|
||||
"integrity": "sha384-QtpHA6LFYa0Ig1rB2KZtX32+djysHFL251q7PYRCpkuyCts2Zn4JA3KQtqNS2Gak",
|
||||
"elements": [
|
||||
"research-dashboard-map"
|
||||
],
|
||||
|
|
@ -961,7 +961,7 @@
|
|||
"scriptUrl": "./research-dashboard/research-dashboard.js",
|
||||
"embed": {
|
||||
"iframe": "<iframe src=\"./research-dashboard/research-dashboard/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
|
||||
"script": "<!-- Mapbox engine, fetched early; remove this line if you set \"provider\":\"maplibre\" -->\n<link rel=\"preload\" as=\"script\" href=\"https://api.mapbox.com/mapbox-gl-js/v3.24.0/mapbox-gl.js\">\n<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./research-dashboard/research-dashboard.js\" integrity=\"sha384-GsBGCy2M1ZnE7Xximx1HpGih2povZWx98661bjzLdaH6pnrTEhZj/PoPDuQ2gfJ5\" crossorigin=\"anonymous\"></script>\n<research-dashboard-map profile-config='{\"profile\":\"research-dashboard\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></research-dashboard-map>"
|
||||
"script": "<!-- Mapbox engine, fetched early; remove this line if you set \"provider\":\"maplibre\" -->\n<link rel=\"preload\" as=\"script\" href=\"https://api.mapbox.com/mapbox-gl-js/v3.24.0/mapbox-gl.js\">\n<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./research-dashboard/research-dashboard.js\" integrity=\"sha384-QtpHA6LFYa0Ig1rB2KZtX32+djysHFL251q7PYRCpkuyCts2Zn4JA3KQtqNS2Gak\" crossorigin=\"anonymous\"></script>\n<research-dashboard-map profile-config='{\"profile\":\"research-dashboard\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></research-dashboard-map>"
|
||||
},
|
||||
"bundle": "research-dashboard"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,6 +14,6 @@
|
|||
</head>
|
||||
<body>
|
||||
<plugin-studio tree-base="."></plugin-studio>
|
||||
<script src="./plugin-studio/plugin-studio.js" integrity="sha384-lYyH0iKEqJ0Ps32Bbzbe3XVdfJZbYAZ6SaeP7DURDmt6yVbxMY1bYrJNUdLDcf4d" crossorigin="anonymous"></script>
|
||||
<script src="./plugin-studio/plugin-studio.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
|||
|
|
@ -945,7 +945,7 @@
|
|||
"bundle": "research-dashboard",
|
||||
"scriptUrl": "./research-dashboard/research-dashboard.js",
|
||||
"output": "dx-maps/research-dashboard/research-dashboard.js",
|
||||
"integrity": "sha384-GsBGCy2M1ZnE7Xximx1HpGih2povZWx98661bjzLdaH6pnrTEhZj/PoPDuQ2gfJ5",
|
||||
"integrity": "sha384-QtpHA6LFYa0Ig1rB2KZtX32+djysHFL251q7PYRCpkuyCts2Zn4JA3KQtqNS2Gak",
|
||||
"elements": [
|
||||
"research-dashboard-map"
|
||||
],
|
||||
|
|
@ -961,7 +961,7 @@
|
|||
"scriptUrl": "./research-dashboard/research-dashboard.js",
|
||||
"embed": {
|
||||
"iframe": "<iframe src=\"./research-dashboard/research-dashboard/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
|
||||
"script": "<!-- Mapbox engine, fetched early; remove this line if you set \"provider\":\"maplibre\" -->\n<link rel=\"preload\" as=\"script\" href=\"https://api.mapbox.com/mapbox-gl-js/v3.24.0/mapbox-gl.js\">\n<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./research-dashboard/research-dashboard.js\" integrity=\"sha384-GsBGCy2M1ZnE7Xximx1HpGih2povZWx98661bjzLdaH6pnrTEhZj/PoPDuQ2gfJ5\" crossorigin=\"anonymous\"></script>\n<research-dashboard-map profile-config='{\"profile\":\"research-dashboard\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></research-dashboard-map>"
|
||||
"script": "<!-- Mapbox engine, fetched early; remove this line if you set \"provider\":\"maplibre\" -->\n<link rel=\"preload\" as=\"script\" href=\"https://api.mapbox.com/mapbox-gl-js/v3.24.0/mapbox-gl.js\">\n<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./research-dashboard/research-dashboard.js\" integrity=\"sha384-QtpHA6LFYa0Ig1rB2KZtX32+djysHFL251q7PYRCpkuyCts2Zn4JA3KQtqNS2Gak\" crossorigin=\"anonymous\"></script>\n<research-dashboard-map profile-config='{\"profile\":\"research-dashboard\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></research-dashboard-map>"
|
||||
},
|
||||
"bundle": "research-dashboard"
|
||||
}
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
|
@ -29,7 +29,7 @@
|
|||
|
||||
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
|
||||
<!-- Bundle IIFE — registers the <research-dashboard-map> custom element. -->
|
||||
<script src="../research-dashboard.js" integrity="sha384-GsBGCy2M1ZnE7Xximx1HpGih2povZWx98661bjzLdaH6pnrTEhZj/PoPDuQ2gfJ5" crossorigin="anonymous"></script>
|
||||
<script src="../research-dashboard.js" integrity="sha384-QtpHA6LFYa0Ig1rB2KZtX32+djysHFL251q7PYRCpkuyCts2Zn4JA3KQtqNS2Gak" crossorigin="anonymous"></script>
|
||||
|
||||
<script>
|
||||
// Mapbox token resolution — the A3 ladder, in precedence order:
|
||||
|
|
|
|||
Loading…
Reference in a new issue