14 KiB
3. CLI commands + required installs (per deployment method)
Everything runs through bun and the scripts in package.json. The build loop
iterates every folder under plugin-designs/ (each folder = one bundle), so once a
map exists there, bun run build picks it up automatically — no per-map wiring.
Install once (all methods): Bun (
curl -fsSL https://bun.sh/install | bash) — Node ≥ 18 also works (npm run <script>; the scripts are identical). Then:bun install
The scripts these commands are grounded in (from this branch's package.json):
// TRANSCRIBED FROM package.json 2026-09-20. The previous copy had DRIFTED: it listed
// `check:drop` twice, omitted `check:template-literal` and `check:sri`, and still showed
// the old inline `build` pipeline — which has since moved behind a LOCK WRAPPER.
// ★ `build` is now `bash internal/build-lock.sh`; the real pipeline is `build:unlocked`.
// The lock SERIALIZES concurrent builds (a `clean` rm -rf's the shared output tree at the
// START of every build, so two builds racing give opposite answers seconds apart). Use
// BUILD_LOCK_WAIT=600 to QUEUE rather than collide. Note the lock serializes but does NOT
// dedupe: N queued builds = N full rebuilds.
// ★ THE PIPELINE RUNS IN PHASES, NOT AS ONE `&&` CHAIN (2026-09-23). Measured on the build
// box AS IT WAS THAT DAY — the numbers below are a DATED BENCHMARK, not a running total.
// The gate set has grown since (36 steps as of 2026-09-24: 7 pre + 29 post); the timings
// and the "31 in, 31 out" count are kept verbatim because they are the EVIDENCE that the
// phase change preserved the step set. Restating them as current would destroy that.
// Then: the 29 gates cost 84.5s chained, but 21 of them finish under 700ms — they were
// queueing behind check:mjs-parse (26.9s) and check:parse (18.7s), not working. The 22 vite
// builds cost 129.0s and were never ordered at all: vite.config.js gives each bundle its own
// outDir with emptyOutDir:false, so they only ever needed to not overwrite each other.
// Now: gates:pre ‖ → library-index → clean → token → build:bundles ‖ → tree → gates:post ‖.
// MEASURED end-to-end (both green, clean+token excluded as they are common to both):
// 165.7s → 65.3s mean of two rounds each. Concurrency: GATES_CONCURRENCY (default 6),
// BUILD_CONCURRENCY (default 4 — 6 buys only 3-9%, and ambient load on this shared box moves
// the same setting 40s→55s, so being a good neighbour costs nothing measurable).
// ★ NO GATE WAS REMOVED, WEAKENED, SAMPLED, OR MADE CONDITIONAL ON WHAT CHANGED. The step set
// is byte-identical (31 in, 31 out, asserted at edit time); only the start times moved. The
// `&&` BETWEEN phases is load-bearing and stays: a source gate still blocks the build, and
// the build still blocks every gate that reads its output. Within a phase the runner reports
// ALL failures instead of stopping at the first, so three broken gates cost one cycle, not three.
// ⚠ check:quoted-scripts IS ITSELF IN gates:post, so adding or removing ANY gate changes the
// very line this doc quotes — the update is self-referential and takes two passes: wire the
// gate, then re-transcribe. That is the mechanism working, not a bug.
// If you edit package.json, re-transcribe this block — a doc that QUOTES a config is
// claiming to be ground truth, and this one had silently stopped being it.
"dev": "bun x vite",
"build": "bash internal/build-lock.sh",
"build:unlocked":"bun run gates:pre && bun run library-index && bun run check:library-index && bun run clean && bun run token && bun run build:bundles && bun run tree && bun run gates:post",
"gates:pre":"node internal/run-parallel.mjs --label=pre -- check:template-literal check:mjs-parse check:sfc-parse check:tab-radius check:synced-css check:doc-scripts",
"gates:post":"node internal/run-parallel.mjs --label=post -- check:drop check:parse verify check:sri check:public-hygiene check:label-joins check:facet-cascade check:facet-one-field check:heartbeat check:kpi-unit check:composite-reg check:descriptions check:no-abs-paths check:no-codename check:dep-names check:composite-srcs check:embed-targets check:nested-token check:i18n-parity check:tabs-localized check:packets-all check:packet-strings-parity check:zone-census check:share-policy check:library-index check:csv-shape check:capability-citations check:catalog-identity check:no-orphan-gates check:quoted-scripts check:done-evidence check:script-flags check:backlog-status check:capability-exemplars check:packet-fixture check:packet-browser check:provider-switch check:locale-reachable check:url-resolver check:manifest-covers check:one-palette check:pin-dim-slider check:legend-source check:backlog-refs check:clustering-modes check:composite-embed-knobs check:composite-locale-leaks check:linked-vocabulary check:wp-embed",
"build:bundles": "node internal/run-parallel.mjs --bundles", // the 22 vite builds, 4 at a time
"preview": "bun x vite preview",
"serve": "node internal/serve-strict.mjs", // 404s a directory, like production
"cap:sync": "./node_modules/.bin/cap sync android",
"cap:verify": "test -f deployment/capacitor/android/app/src/main/assets/public/index.html && echo \"OK webDir copied\" || (echo \"FAIL: empty assets/public -> blank white screen. Check webDir.\" && exit 1)",
"apk:debug": "bun run build && bun run cap:sync && bun run app:start-page && bun run cap:verify && cd deployment/capacitor/android && ./gradlew assembleDebug",
"apk:install": "adb install -r deployment/capacitor/android/app/build/outputs/apk/debug/app-debug.apk",
"phone": "bun run apk:debug && bun run apk:install"
BUILD_ALLOW=1 bun run build writes the whole deployable tree to public/dx-maps/, including
manifest.json: the machine-readable index that carries a ready-to-paste iframe snippet and a
<script src> + SRI snippet for every plugin.
Summary table
| # | Deployment method | Install first | Command(s) | Status |
|---|---|---|---|---|
| 1 | iframe | Bun | bun install → BUILD_ALLOW=1 bun run build → drop public/dx-maps/ on any host |
✅ production |
| 2 | Mobile app (Android) | Bun for the check; + Android SDK · adb · JDK 17 for the APK |
bun run check:mobile (no toolchain) · bun run phone |
✅ built |
| 3 | JavaScript embed | Bun | BUILD_ALLOW=1 bun run build, then paste the embed.script snippet from manifest.json |
✅ production |
| 4 | WordPress / Disciple Tools | Bun (+ php for the check) |
paste a snippet into a Custom HTML block, or install deployment/wordpress/doxa-maps-embed/ · bun run check:wp-embed |
🧪 proof of concept |
| 5 | Desktop (Tauri) | Bun for the check; + Rust · Tauri CLI · webview libs for the binary | bun run check:tauri (no toolchain) · bun run tauri:build |
🧪 proof of concept |
1. iframe
Install: Bun.
bun install
BUILD_ALLOW=1 bun run build # builds every plugin-designs/* bundle -> public/dx-maps/
bun run serve # optional: serves public/ at http://localhost:4173
Each profile in manifest.json carries an embed.iframe snippet:
<iframe src="./all-people-groups/all-people-groups/index.html"
width="100%" height="600"
style="border:none;border-radius:12px;" allowfullscreen></iframe>
The folder is position-independent. Snippet URLs are relative to the tree root, so dx-maps/
works dropped on any static host. For absolute URLs, set embedBaseUrl in the build config.
2. Mobile app — Android / Capacitor
Install: Bun · Android SDK (sdkmanager, adb on your PATH) · JDK 17 · a device or
emulator. The @capacitor/* packages are already devDependencies.
bun run check:mobile # contract check — needs NO JDK, SDK or device
bun run phone # the full path: build → cap:sync → cap:verify → gradlew assembleDebug → adb install
bun run apk:debug # or the halves separately: build the APK only
bun run apk:install # push the built APK to a connected device
check:mobile runs anywhere. It copies the built tree into the Android project and asserts
deployment/capacitor/android/app/src/main/assets/public/index.html exists, which catches the one
silent failure: a wrong webDir launches to a blank white screen, not an error. Only Android is
scaffolded; iOS is not set up.
3. JavaScript embed
Install: Bun. Build, then paste the plugin's embed.script snippet from manifest.json: a
<script src> with a Subresource-Integrity hash, plus the custom element.
<script src="./all-people-groups/all-people-groups.js"
integrity="sha384-…" crossorigin="anonymous"></script>
<all-people-groups
profile-config='{"profile":"all-people-groups","tk":"YOUR_MAPBOX_TOKEN"}'>
</all-people-groups>
The hash pins the exact bytes. Whenever the bundle changes, the old hash stops matching and the
browser refuses to run the script, so re-copy the snippet from the current manifest.json after each
release. (An iframe embed updates automatically.) The element
takes props and emits events, so it can wire into the surrounding page.
On a partner's site, a JS embed needs two values an iframe does not. An iframe carries its own document base; a script runs in the host's document, so relative asset and API paths would resolve against the host and 404. The snippet already ships both:
| iframe | JavaScript embed | |
|---|---|---|
assetsBase |
not needed | required, in profile-config (YOUR_TREE_URL/assets until you set assetsBaseUrl) |
| API base | not needed | required, window.MAP_APP_API_URL (a host global) |
| Mapbox token | inherited by the leaf page | replace YOUR_MAPBOX_TOKEN |
4. WordPress / Disciple Tools — proof of concept
No plugin needed for the simple path: build, then paste the iframe or script snippet into a WordPress Custom HTML block.
The plugin path: deployment/wordpress/doxa-maps-embed/ adds a [doxa_map] shortcode and a
Gutenberg block, registering the bundle through WordPress's own dependency graph so the SRI hash and
crossorigin survive and the script loads once per page.
bun run check:wp-embed # renders the REAL shortcode with no WordPress installed
What is proven, and what is not. The check renders the shortcode against the real manifest.json
and asserts the element tag, valid profile-config JSON, the script src, the injected SRI hash and
crossorigin — and rejects a malformed hash. It does not prove that WordPress renders the page or a
browser runs it; that needs a live install (wp-env start, see
deployment/wordpress/doxa-maps-embed/HOW-TO-TEST.md).
5. Desktop — Tauri — proof of concept
Install: Bun for the check. For the binary: Rust (rustup), the Tauri CLI
(cargo install tauri-cli) and the platform's webview libraries.
bun run check:tauri # contract check — needs NO Rust, NO Tauri CLI
bun run tauri:build # the binary and a .deb — needs the toolchain above
The desktop app wraps the same tree. deployment/src-tauri/tauri.conf.json points
build.frontendDist at ../public, the desktop twin of Capacitor's webDir; the Rust crate carries
no application logic. check:tauri asserts that path resolves, every window url exists in the built
tree, the identifier matches Capacitor's appId, and the crate files are present.
Token caveat: a Tauri webview serves from tauri://localhost (or http://tauri.localhost on
Windows), so a tokenUrl endpoint must send CORS headers for that origin. The fix is a response header
on the token host, not a change in tauri.conf.json. Details:
deployment/src-tauri/README.md.
Notes and history
The measurements behind the sections above.
- Mobile —
check:mobileneeds no toolchain (verified 2026-09-20). Withjava,gradle,adbandsdkmanagerall absent, sync exited 0, the wholepublic/tree was staged into the native assets andcap:verifypassed; removingindex.htmlmade it exit 1. The doc once said "21 bundles staged"; that count named the wrong unit (cap synccopieswebDirwholesale) and went stale, so it says "the whole tree" now. - JavaScript embed — foreign-host run (2026-09-20). Both embed methods were run on a foreign host page
until they failed: the iframe needed nothing; the script embed needed
assetsBaseand the API base, which the snippet now emits. Same-root and foreign-host both then passed (map painted, attribution shown, 0 errors, 0 404s). - WordPress — corrected 2026-09-20. This section once said no WordPress build step existed; the
shortcode + block plugin and its contract check were added after. The check stubs the ~20 WordPress
functions the plugin calls (
deployment/wordpress/tests/). - Tauri — the build ran (2026-09-21). With the real toolchain (
tauri infoall ✔: webkit2gtk 2.50.6, rustc/cargo 1.96.1)bun run tauri:buildproduced a 65 MB ELF executable and a 58 MB.deb(Depends: libwebkit2gtk-4.1-0, libgtk-3-0); a later run measured 67 MB and 60 MB. The web tree is compiled into the binary, which is why the.debholds only 6 entries.bundle.targetsis["deb"]becauserpmbuildandlinuxdeploywere absent, so"all"would fail two formats at the end of a ~6-minute build. The window has not been opened: the build box is headless, and the binary stops with "Failed to initialize GTK" (no display, not a defect).check:tauriis negative-tested five ways (wrongfrontendDist, missing window url, identifier drift, empty windows array, missing crate file).