15 KiB
Reference
Look-up tables and contracts for doxa-maps. For learning, see the tutorial; for tasks, how-to.md; for the why, why.md; for the short "what it is", explanation.md.
doxa-maps is a Vite bundler: one shared library, N independent embeddable bundles. Each
bundle is a single self-contained .js file that registers a custom HTML element
(<doxa-map>, <doxa-research-map>, …) you can drop on any web page.
The three zones
| Zone | Path | Alias | License | You edit it… |
|---|---|---|---|---|
| Shared library | library/ |
@map |
Apache-2.0 | …once; the fix reaches every bundle on next build |
| Your app-profile | app-profiles/<name>/ |
— | yours | …freely; nothing here leaks to other maps |
| The build machine | internal/ |
— | Apache-2.0 | …never — it's the bundler's own scripts |
Terminology. "Profile" is used at two levels: a bundle / app-profile is a folder under
app-profiles/; a profile screen is one .vue file sitting flat in that folder (no
profiles/ subfolder — the entry globs ./*.vue). A bundle can hold several screens; the host
picks which to render with the profile field of profile-config.
Three profile patterns (mix freely in one bundle): Individual (one .vue per bundle) ·
Parameterized (one screen driven by profile-config props at runtime) · Nested (one
screen that renders others via tabs, workbench-style).
Directory tree
doxa-maps/
├── library/ ← SHARED LIBRARY (alias @map, Apache-2.0)
│ ├── index.js ← thin barrel; prefer subpath imports for tree-shaking
│ ├── ProfileLoader.vue ← the ONE component that reads profile-config & loads a screen
│ ├── components/ ← map UI: legends/, map-controls/, poster/, dashboards/, drawers
│ ├── composables/ ← useMapInstance, useMapData, useMapLayers, … (+ legends/, poster/)
│ ├── api/ ← DataSourceManager (csv | api | rest-api) + sources.json
│ ├── colors/ ← one color strategy per file, auto-registered by _registry.js
│ ├── constants/ ← never-change values: mapDefaults.js, zoom.js, poster*.js
│ ├── i18n/locales/ ← 11 base locales: ar de en es fr hi it pt ro ru zh
│ ├── stores/ ← Pinia: dataStore, mapStore, uiStore (per-instance)
│ └── utils/ ← pure helpers (geoUtils, clustering)
│
├── app-profiles/ ← ALL bundles live here (each subfolder = 1 bundle)
│ ├── <bundle>/
│ │ ├── index.js ← bundle ENTRY — globs ./*.vue, registers the web component
│ │ ├── index.html ← Vite html entry = the dev STAGING page
│ │ ├── <profile>.vue ← 1..N FLAT profile screens (no profiles/ subfolder)
│ │ └── src/ ← everything else the map needs (its own colors/, api/, …)
│ └── template-bundle/ ← minimal copy-me reference bundle
│ (folders starting with _ or . are SKIPPED by the build)
│
├── internal/ ← the bundler's own build scripts — never edited by map builders
├── layouts/ ← the app shell: mounts several bundles as one single-page app
├── capacitor/ ← native projects (android/, ios/)
├── public/ ← THE SERVED WEB-ROOT, committed:
│ ├── index.html the app shell
│ ├── doxa-maps/<bundle>/ built bundles + manifest.json + staging HTML
│ └── assets/ posters & data the maps fetch at runtime
├── vite.config.js ← multi-entry: discovers app-profiles/<name>/ folders
└── vite.doxa-maps-build-config.json ← output tree name + copy destinations + token settings
The profile-config runtime contract
A bundle exposes ONE attribute, profile-config — a JSON string parsed by
ProfileLoader.vue at runtime. It is the entire wiring between host page and map, so the same
built bundle re-points to different data/colors/language with no rebuild.
<doxa-map profile-config='{
"profile": "doxa-simple-map", // which flat .vue screen to render (required)
"tk": "pk.eyJ...", // Mapbox token (required; host-supplied, never baked)
"instanceId": "my-map", // optional; isolates window events between embeds
"dataSource": "pray-tools", // or { "type":"rest-api","endpoint":"https://…" }
"colorSet": "default",
"locale": "en",
"provider": "mapbox", // map engine: "mapbox" (default) | "maplibre" (preview);
// also flippable per page via ?provider= or window.MAP_PROVIDER
"tabs": [ /* parameterized screens read this */ ]
}'></doxa-map>
profilemust match a flat.vuefilename in the bundle. Mismatch → an inline "Profile not found" error box listing the available names.tkis supplied at runtime so the token is never compiled in (dev uses.env'sVITE_MAPBOX_TOKEN).instanceIdscopes cross-instance window events so multiple embeds never interfere.dataSourceis handed toDataSourceManager(csv|api|rest-api).ProfileLoaderprovide()s to descendants:mapboxToken,dataSource(both computed refs —unrefbefore use),colorSet,profileConfig,instanceId,lang, and the instance-scoped Pinia storesuiStore/mapStore/dataStore. Descendants inject; never calluseXxxStore()directly (it would cross-bleed between embeds).
Each bundle registers a shadow-DOM web component via defineCustomElement, with its own
ShadowRoot, styles, Pinia, and i18n. customElements.define() is one-shot per tag name, so
each bundle registers a distinct tag.
Build scripts & output
| Script | What it does |
|---|---|
bun run dev |
Vite dev server + HMR (default http://localhost:5173). Serves every bundle at its html path. Writes nothing to public/ — no HMR across that boundary. |
bun run build |
clean → one vite build per app-profiles/*/ folder → tree → check:drop → check:parse. |
bun run build:bundle |
Builds ONE bundle (needs BUNDLE=<name>). |
bun run build:watch |
Rebuilds into public/ on every change (integration loop). |
bun run clean |
Wipes + recreates the public/<name>/ output tree. |
bun run tree |
Writes manifest.json + self-contained staging HTML, then copies the tree to every build-config paths destination. |
bun run verify |
Data-integrity + shipped-bundle + network-policy guards (fast, no browser). |
bun run verify:full |
+ rendered posters and live apps in Firefox. |
How a build works. vite.config.js is multi-entry by discovery: at build time it
registers every app-profiles/ subfolder that (a) doesn't start with _/. and (b) has
both index.html and index.js. One vite build builds exactly one bundle, selected
by BUNDLE=<name>, so the output is a self-contained IIFE (inlineDynamicImports: true).
bun run build loops every folder. Each bundle only bundles what its screens import, so sizes
differ (simple ≈ 420 KB, research ≈ 1.3 MB).
Where output goes. One bundle folder → one file: public/doxa-maps/<bundle>/<bundle>.js.
CSS is injected by JS (cssCodeSplit: false), so there is no separate .css. The tree root
name comes from vite.doxa-maps-build-config.json name (default doxa-maps).
emptyOutDir: false keeps sibling bundles intact when you rebuild one.
What a built bundle folder contains. Beyond the IIFE, bun run tree generates an
index.html per bundle (staging/showcase), per profile, and per instance —
each profile/instance page is a standalone, directly linkable/iframe-able map — plus
public/doxa-maps/manifest.json with ready-made embed.iframe/embed.script snippets for
every profile. Full design: architecture/embeddability.md.
Preset instances. A <profile>.instances.json beside a parameterized profile
({ profile, apiBaseUrl?, instances: [{ id, title?, props }] }) makes the build emit one
standalone page per instance at <bundle>/<id>/index.html. Recipe:
how-to.md.
Bun is required. bun.lock is the committed lockfile; bun x vite runs project-local
Vite. npm run … resolves the same scripts but Bun must be on PATH.
vite.doxa-maps-build-config.json fields
Edit this one file, then bun run build.
| Field | What it does |
|---|---|
name |
Output folder name (default doxa-maps). |
paths |
Extra folders to copy the whole built tree into. Point at a host repo's public/ → that host serves the maps and becomes the CDN. Empty (the default) ⇒ written to public/<name>/ only, since public/ is already the committed web-root. |
embedBaseUrl |
The public URL the tree is served from, used to generate absolute, paste-anywhere embed snippets. Empty ⇒ relative URLs (position-independent; keeps the drop-portability gate green; required for Capacitor, which loads from https://localhost). |
tokenUrl |
Endpoint each generated page fetches its Mapbox token from — a URL, not a secret. Set absolute (https://…/api/maps/token) so a CDN folder and the mobile app both work; endpoint must send CORS for the embedding origins. window.MAP_TOKEN_URL overrides at runtime. |
token |
Opt-in literal token baked into every page. Leave empty — prefer tokenUrl. |
assetsBaseUrl |
Where the shared asset tree (previews, posters, boundaries) is served from. Empty ⇒ a relative path per page depth (self-contained under one web-root). Set absolute for partner embeds and mobile, where there is no shared web-root to climb to. |
Token resolution ladder: per-map tk prop > ?tk= > baked token > window.MAP_TOKEN_URL > tokenUrl > same-origin /api/maps/token.
Data sources
Maps read data through @map/api/ driven by library/api/sources.json. Three source types:
| Type | What | Points at |
|---|---|---|
rest-api |
versioned REST endpoint (default) | the live people-group API; base URL resolves at runtime (window.MAP_APP_API_URL → VITE_API_BASE_URL → same-origin) |
csv |
a local CSV parsed at load | the Joshua Project dataset (+ photo fallbacks) |
api |
a single JSON URL | any plain JSON endpoint |
Country/region polygons are not a sources.json connection — they render from Mapbox's built-in
country-boundaries-v1 tileset plus a lazy-loaded WAGF-regions GeoJSON.
Adding or changing a source? Full contract + checklist: architecture/data-sources.md.
Colors
Reused by more than one map → shared library/colors/. Only your map uses it → your bundle's
src/colors/ (merges over the shared set, that bundle only). Filename derives the mode key
(language-family.js → languageFamily).
Writing or overriding a strategy? Full contract + checklist: architecture/color-strategies.md · two homes in depth: ../contributing/COLORS.md.
Importing from @map
Everything under library/ is importable via the @map/… alias (vite.config.js maps @map
→ library/). The import path mirrors the file path: a file at library/<sub>/<file> imports
as @map/<sub>/<file>. Examples:
import SemanticTreeLegend from '@map/components/legends/SemanticTreeLegend.vue'
import { useMapInstance } from '@map/composables/useMapInstance.js'
import { useMapData } from '@map/composables/useMapData.js'
import langFamilyByIso from '@map/data/langFamilyByIso.json'
Prefer subpath imports over the thin @map barrel (which exports MAP_CORE_VERSION only) so
tree-shaking works. The per-file catalog in architecture/file-map.md is the
authoritative menu — every library/ file listed is importable this way. A generated one-line import index can also be produced with
node internal/generate-library-index.mjs.
Going deeper — the architecture folder
Simple lookups live here; the full contracts, catalogs, and wiring detail live in
architecture/:
| Doc | What it holds |
|---|---|
| file-map.md | every file, one line each, with the gotcha each carries |
| data-sources.md | the api/ seam: connection contract + checklist |
| color-strategies.md | the strategy contract + legend wiring |
| components.md | shared component catalog + conventions |
| i18n.md | the locale contract + add-a-language checklist |
| embeddability.md | generated per-map pages + manifest — embeddable by default |
| app-wrappers.md | the SPA shell: nav.js + PROFILES contracts |
| poster-generation.md | the poster/preview generator pipeline |
| patterns-and-provenance.md | the two multi-map patterns + provenance |
The Semantic Tree Legend component (a reusable hierarchical legend) is documented in file-map.md.
Quick reference — "I want to…"
| I want to… | Do this |
|---|---|
| Add a new map | copy app-profiles/template-bundle/ → rename folder + tag in index.js |
| Add a screen to an existing bundle | drop a .vue flat in that bundle's folder, request it via profile-config.profile |
| Fix a legend / control for ALL maps | edit it once in library/ (@map); rebuild bundles |
| Add a color strategy | new file in @map/colors/ (shared) or the bundle's src/colors/ (map-private) |
| Build everything | bun run build → public/doxa-maps/<bundle>/<bundle>.js |
| Build just one | BUNDLE=<name> bun run build:bundle |
| Preview in isolation | bun run dev → http://localhost:5173/app-profiles/<name>/index.html |
| See a change on the served site | bun run build, then refresh (NOT dev) |
| Repoint the API without a rebuild | set window.MAP_APP_API_URL before the bundle loads |
| Repoint the Mapbox token | pass tk in profile-config, or set window.MAP_TOKEN_URL / build-config tokenUrl |