"the element gets the tk to the element itself and does not need to only gatekeep that to one component only — all components in a plugin can share that tk" (Driver, 2026-09-23). Inside ONE element that was already true: ProfileLoader provides 'mapboxToken' and eleven components inject it. The gate is the ELEMENT BOUNDARY — every custom element mounts its own app with its own ProfileLoader, which re-derives the token from its own profile-config, so a composite that nests a real map hands the child nothing. THE EXISTING FALLBACK COULD NOT FIRE. The ladder already read window.MAP_TOKEN. MEASURED: that global IS set on the composite leaf and the child still had no search bar — because the ladder lives in a computed() and window.MAP_TOKEN is a plain global, not a reactive source. The composite leaf FETCHES its token, so the computed resolved '' first, cached, and never re-ran. useSharedMapToken makes it a ref that updates on publish, and assignments to window.MAP_TOKEN are adopted through an accessor, so already-generated pages keep working untouched. Storage lives on window and changes travel as an event: each bundle is its own IIFE, so a module-level ref is NOT shared between a composite and the bundle it nests. THAT WAS NECESSARY AND NOT SUFFICIENT, which only measuring showed. With the token shared and reactive the bar was STILL absent. Real cause: GeocoderComponent's onMounted hit `typeof MapboxGeocoder === 'undefined'` and RETURNED SILENTLY — no throw, no console error, no DOM. MapboxGeocoder is "preloaded via index.html", true for a LEAF and false for a COMPOSITE leaf, which generate-tree classifies as token-free while it goes on to nest a real map. Measured, mapbox-gl-geocoder references per page: research-map leaf 2, research-map-plus-grid leaf 0. So the script is now fetched on demand when a host did not preload it, reusing the exact shape the maplibre path already had (one loader, two callers) and pinned to the same v5.0.0 the leaves preload. It resolves on error as well as load, so a blocked CDN leaves the map usable and the bar absent rather than hanging. This fixes every already-generated page without regenerating any of them. MEASURED on the built tree: leaf mapbox search@56 legend@104 leaf maplibre search@NONE legend@58 (unchanged: maplibre stays bar-free) composite search@56 legend@104 (was NONE) library/ reaches every bundle, so all 22 rebuilt: 22/22, 7/7 pre, 18/18 post, and a browser sweep of five StackLayout consumers plus all seven studio dashboards.
102 lines
5.3 KiB
JavaScript
102 lines
5.3 KiB
JavaScript
/**
|
|
* useSharedMapToken.js — ONE page, ONE map token, shared by every element on it.
|
|
*
|
|
* THE BUG THIS ENDS (Driver, 2026-09-23; the repo already names it
|
|
* BUG-COMPOSITE-LEAVES-ARE-TOKEN-FREE-BUT-NEST-REAL-MAPS):
|
|
* "the element gets the tk to the element itself and does not need to only gatekeep that to one
|
|
* component only — all components in a plugin can share that tk"
|
|
*
|
|
* Inside ONE element that was already true: ProfileLoader provides 'mapboxToken' and eleven
|
|
* components inject it. The gate is the ELEMENT BOUNDARY. Every custom element mounts its own Vue
|
|
* app with its own ProfileLoader, which re-derives the token from ITS OWN profile-config — so a
|
|
* composite that nests a real map hands the child no `tk` and the child starts from nothing.
|
|
*
|
|
* ★ AND THE FALLBACK THAT WAS SUPPOSED TO COVER THAT COULD NOT FIRE. ProfileLoader's ladder already
|
|
* read `window.MAP_TOKEN` after config.tk. MEASURED on the built tree: window.MAP_TOKEN IS set on
|
|
* the composite leaf — and the child STILL had no search bar. The reason is that the ladder lives
|
|
* in a computed() and window.MAP_TOKEN is a PLAIN GLOBAL, not a reactive source. The generated
|
|
* composite leaf FETCHES its token (one fetch( in the page), so the computed evaluates to '' long
|
|
* before the token lands, caches that, and never re-runs — nothing reactive ever changed.
|
|
* So the symptom looked like "no token" while the token was right there, seconds later. A grep
|
|
* for the fallback would have said the fallback existed; only reading WHEN it ran explains it.
|
|
*
|
|
* THE FIX: make the page-level token a REACTIVE, CROSS-ELEMENT fact.
|
|
* - publishMapToken(tk) stores it and announces it on `window`.
|
|
* - useSharedMapToken() returns a ref that is seeded from whatever is already known and UPDATES
|
|
* when a later publish happens — so an async token reaches elements that mounted before it.
|
|
* - Assignments to `window.MAP_TOKEN` are adopted automatically (see installBridge), so every
|
|
* ALREADY-GENERATED page keeps working untouched: the leaf pages assign that global today and
|
|
* now that assignment broadcasts instead of landing silently.
|
|
*
|
|
* ★ WHY AN EVENT AND NOT A SHARED MODULE REF. Each bundle is its own IIFE with its own copy of this
|
|
* module, so a module-level ref is NOT shared between <research-map-plus-grid> and the
|
|
* <doxa-research-map> it nests — they are different bundles. The storage therefore lives on
|
|
* `window` and changes travel as a DOM event, which is the only channel both copies can see.
|
|
*
|
|
* SECURITY: this shares a PUBLIC Mapbox token (pk.*) that is already in the page's HTML and in
|
|
* every tile request. It does not widen exposure — it stops a second element on the SAME page from
|
|
* being unable to use what that page was already given. Nothing is persisted or sent anywhere.
|
|
*/
|
|
import { ref, onBeforeUnmount } from 'vue'
|
|
|
|
const KEY = '__doxaMapToken' // the value, on window, so every bundle sees one copy
|
|
const EVENT = 'doxa:map-token' // fired when it changes
|
|
const FLAG = '__doxaMapTokenBridge'
|
|
|
|
const readStored = () => {
|
|
if (typeof window === 'undefined') return ''
|
|
const v = window[KEY]
|
|
return typeof v === 'string' ? v : ''
|
|
}
|
|
|
|
/** Announce a token to every element on the page. No-ops on an empty or unchanged value. */
|
|
export function publishMapToken(tk) {
|
|
if (typeof window === 'undefined') return ''
|
|
const next = typeof tk === 'string' ? tk.trim() : ''
|
|
if (!next || next === readStored()) return readStored()
|
|
window[KEY] = next
|
|
try { window.dispatchEvent(new CustomEvent(EVENT, { detail: next })) } catch { /* pre-CustomEvent host */ }
|
|
return next
|
|
}
|
|
|
|
/**
|
|
* Adopt `window.MAP_TOKEN` as a source, in BOTH directions and for the whole life of the page.
|
|
*
|
|
* Every generated leaf assigns `window.MAP_TOKEN`, some of them from an async fetch. Replacing that
|
|
* property with an accessor means such an assignment publishes to every element automatically —
|
|
* which is what makes this fix reach pages nobody regenerates. Installed once per bundle; guarded
|
|
* because N bundles on one page would otherwise stack N accessors on the same property.
|
|
*/
|
|
function installBridge() {
|
|
if (typeof window === 'undefined' || window[FLAG]) return
|
|
// Seed from whatever the page already set before this bundle loaded.
|
|
const existing = typeof window.MAP_TOKEN === 'string' ? window.MAP_TOKEN : ''
|
|
if (existing && !readStored()) window[KEY] = existing
|
|
try {
|
|
Object.defineProperty(window, 'MAP_TOKEN', {
|
|
configurable: true,
|
|
get() { return readStored() },
|
|
set(v) { publishMapToken(v) },
|
|
})
|
|
window[FLAG] = true
|
|
} catch {
|
|
// A host may have made MAP_TOKEN non-configurable. Reading it still works through the ladder
|
|
// below, so degrade to "no live adoption" rather than throwing during bundle init.
|
|
}
|
|
}
|
|
|
|
/**
|
|
* A reactive view of the page's shared map token.
|
|
* @returns {import('vue').Ref<string>} updates when any element publishes a token
|
|
*/
|
|
export function useSharedMapToken() {
|
|
installBridge()
|
|
const token = ref(readStored())
|
|
if (typeof window !== 'undefined') {
|
|
const onChange = () => { token.value = readStored() }
|
|
window.addEventListener(EVENT, onChange)
|
|
// A composable may be called outside a component (rare here); guard so it cannot throw.
|
|
try { onBeforeUnmount(() => window.removeEventListener(EVENT, onChange)) } catch { /* not in setup */ }
|
|
}
|
|
return token
|
|
}
|