doxa-maps/documentation/README-SECTION-3-cli-commands.md
Doxa Maps 694dad7bab
Some checks are pending
Deploy to GitHub Pages / build (push) Waiting to run
Deploy to GitHub Pages / deploy (push) Blocked by required conditions
Docs: an iframe updates itself; a script embed's integrity hash pins the bytes, so re-copy it after each release
From the critic's SRI check. how-to's embed section had no word on it; the CLI section said the
hash changes on every build, which is only true when the bundle's bytes change.
2026-09-26 02:07:14 +00:00

209 lines
14 KiB
Markdown

## 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](https://bun.sh) (`curl -fsSL https://bun.sh/install | bash`) — Node ≥ 18 also works (`npm run <script>`; the scripts are identical). Then:
>
> ```bash
> bun install
> ```
The scripts these commands are grounded in (from this branch's `package.json`):
```jsonc
// 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.
```bash
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:
```html
<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.
```bash
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.
```html
<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.
```bash
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.
```bash
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`](../deployment/src-tauri/README.md).
---
## Notes and history
The measurements behind the sections above.
- **Mobile — `check:mobile` needs no toolchain (verified 2026-09-20).** With `java`, `gradle`, `adb` and
`sdkmanager` all absent, sync exited 0, the whole `public/` tree was staged into the native assets and
`cap:verify` passed; removing `index.html` made it exit 1. The doc once said "21 bundles staged"; that
count named the wrong unit (`cap sync` copies `webDir` wholesale) 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 `assetsBase` and 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 info` all ✔: webkit2gtk 2.50.6,
rustc/cargo 1.96.1) `bun run tauri:build` produced 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 `.deb` holds only 6 entries. `bundle.targets` is `["deb"]`
because `rpmbuild` and `linuxdeploy` were 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:tauri` is negative-tested five ways
(wrong `frontendDist`, missing window url, identifier drift, empty windows array, missing crate file).