DEDUP-AUDIT.md (measured today: 253 files, 31 cross-plugin duplicate clusters, 622 duplicated lines) ranks this tenth by raw cost and first by suitability. It is the only cluster where the copies are identical in code, close over nothing, and sit in two plugins with one owner between them. loadScriptOnce (13 lines) and whenTagDefined (11) were byte-identical between doxa-maps-spa and plugin-studio. deriveTreeBase (9) differed by ONE COMMENT LINE and not one character of code -- drift in the documentation of a shared function, which is its own argument for having one copy. They are one concern, not three: load a bundle's script once, wait for its custom element to register, and work out the tree base to load it from. That is what a capability-5 app shell does to nest another plugin, and both plugins here ARE the capability-5 shells. Any future shell needs the same three. Verbatim move: no parameterisation, no behaviour change, callers import instead of declaring. The comments travel with the code because each records a measurement -- particularly the one explaining why onload is not a usable success signal for a dev-server ES module. ONE BEHAVIOUR CHANGE, AND IT IS AN IMPROVEMENT: the _loaded cache is now shared. Two shells on one page previously kept separate caches and each loaded the same bundle; the second now reuses the first's promise. deriveTreeBase reads document.currentScript, so it is only valid AT MODULE EVALUATION TIME. The call sites did not move -- both entries still call it at module top level -- and the library file documents the constraint, because a later call silently returns '' and every nested bundle URL would break. Verified: both bundles build; each built file contains all three functions exactly once and still derives currentScript; check:sri, check:series-isolation, check:studio-rail and check:capability-derived all PASS. library/utils/embedScript.js is the build owner's under rule 5B. The plugin-designs and public/ changes are in the same commit because they import it -- landing them apart breaks the build in between. |
||
|---|---|---|
| .. | ||
| api | ||
| colors | ||
| components | ||
| composables | ||
| constants | ||
| data | ||
| i18n | ||
| layouts | ||
| lists | ||
| maps | ||
| packet | ||
| 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.