From the fleet census: 620 function bodies indexed across 29 plugin trees, 16 identical in two or more. These are the four cheapest — none closes over anything beyond its own parameters, so each is a file move plus an import rather than a new dependency to thread. hostElement resolves the custom element a component is mounted inside, one hop out through the shadow root. It was identical in three plugins, and it is the piece worth sharing for a reason other than line count: every plugin ships through defineCustomElement, a composite nests two shadow roots, and code that forgets the hop operates on the wrong node while looking correct. A document-level query for a node inside a shadow root returns null on a working build, which reads as a broken feature rather than a wrong query. Three copies were three chances to write that. findScrollportAncestor walks up to the nearest ancestor that actually scrolls, crossing shadow boundaries on the way — parentElement stops at a root, so a plugin in a custom element would otherwise find no scrollport and conclude the page does not scroll. findNodeInTree and buildFlatLegendMatchExpr join useLegendMapBridge, which already holds four functions from the same seam. They sit at module level rather than inside the factory because they close over nothing: a pure function hidden in a closure can only be reached by constructing the whole bridge. Call sites keep their old names through a one-line local binding, so no call changed and the diff is the declaration plus an import. Rebuild-and-compare, all five affected bundles, before -> after: regions-dashboard 1,098,469 -> 1,098,479 +10 country-posters 1,484,053 -> 1,484,028 -25 doxa-research-map 1,354,051 -> 1,354,061 +10 dashboard-plus-grid 915,660 -> 915,644 -16 big-picture 366,907 -> 366,719 -188 net -209, and country-posters gains 25 bytes against the 1.5 MB ceiling. Every behaviour-bearing token counted, not set-compared, and unchanged in all three map bundles: regions-fill, peoplePraying, engagementStatus, adoptionStatus, overflowY, scrollHeight, getRootNode, _flat_filter, religion. Owners cleared their own files first: regions for regions-list.vue, upg100 for countries-map.vue. check:selection-kinds, check:legend-matches-source, check:legend-data-contract, check:packet-variants, check:no-dead-library and check:no-cross-plugin-imports all pass. |
||
|---|---|---|
| .. | ||
| api | ||
| charts/whiteboard | ||
| colors | ||
| components | ||
| composables | ||
| constants | ||
| dashboards | ||
| 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.