Every plugin ships as a self-contained IIFE, so library/utils/embedScript.js is compiled separately into each one: eleven bundles on main each carry their own copy. Its cache was a module-scoped const, which made "load exactly once" true per BUNDLE rather than per page — the opposite of what the file's own header claimed. Two shells nesting the same child (the Research Gallery and the Plugin Studio both mounting doxa-research-map) each injected it. Nothing failed, which is why it survived. The second request came from the HTTP cache, so no failed request and no extra network row; the bundle then executed in full a second time — a second Vue, a second vue-i18n, a second pinia, ~375 KiB gzip of work — and its guarded customElements.define quietly declined at the very end. Where the two URLs are different versions of one plugin, whichever executed first owns the tag and the other is discarded in silence. The cache now lives on globalThis, the only scope that matches the claim, under a versioned key so a page mixing old and new bundles falls back to a cache each rather than misreading a shape it does not expect. Entries are keyed by the resolved href, because the callers do not pass the same string for one file. A <script> already in the document is adopted, which is what covers bundles built before this and host pages that write their own tag; that resolves immediately, and it is safe only because every caller here follows with a wait on the element itself — the note in the file says so, since a new caller must keep that pairing. claimTag is a diagnostic, not a guard: by the time a second bundle arrives the define is long since won, so the thing that was missing was anyone saying so. The check imports the module twice under different URLs — two module instances sharing one globalThis, the same shape as two IIFEs on one page. It asserts the second caller receives the FIRST one's promise, not merely that one script tag exists: mutation-testing showed the tag count alone is satisfied by the adoption path, so it would have passed with the original bug restored. |
||
|---|---|---|
| .. | ||
| api | ||
| charts | ||
| colors | ||
| components | ||
| composables | ||
| constants | ||
| data | ||
| elements | ||
| i18n | ||
| layouts | ||
| lists | ||
| maps | ||
| packet | ||
| packets | ||
| scripts | ||
| stores | ||
| styles | ||
| utils | ||
| vendor/cldr | ||
| index.js | ||
| ProfileLoader.vue | ||
| README.md | ||
library/ (@map) — the shared map template
The other half of the architecture (see ../plugin-designs/README.md):
every map in plugin-designs/<your-map>/ composes the reusable, parameterized pieces that live here.
Fix something here and every map benefits on the next build; break something here and every map
breaks — read components/WARNING.md before touching shared pieces.
| Folder | What lives there |
|---|---|
ProfileLoader.vue |
The root of every bundle: parses profile-config, loads the profile, provides the stores + config |
components/ |
Shared UI — map, legends, controls, popup (PeopleGroupDetail), tables, charts, hints (README) |
composables/ |
Shared logic — map engine (useMapInstance), provider switch, shadow-DOM styles, theming |
stores/ |
Per-instance Pinia stores (ui / map / data) — provided by ProfileLoader, injected by children |
api/ |
Data-source registry (sources.json) + fetch layer — the JAMstack runtime wiring |
colors/ |
Color sets maps opt into via profile-config.colorSet |
i18n/ |
The shared string catalogs (all maps speak the same languages) |
data/ |
Small shared datasets baked at build time (big served data lives in ../data/) |
constants/ |
Shared constants |
scripts/ |
Maintainer pipelines (posters, research) — not part of any bundle |
The rule of thumb (same as WARNING.md): custom code in your app-profile, reusable code here — and things only move from the first to the second once they've earned it.