Some checks are pending
build-gate / build (push) Waiting to run
- Opens with what dx-maps is for: seven objectives, each paired with the gate or section that proves it is met. - New Research Gallery section: the cap-5 app shell and the tree of cap-4..1 plugins it nests, each still independently embeddable. - Plugin Studio: clone main (the old branch was 198 commits behind), a command table, and a fresh studio screenshot (the old one showed a retired nav and tofu icons). - Agent swarm: renamed to industry terms (trunk-based development, merge queue, maintainer), a laptop recipe (worktrees, strict ports, one builder, flock as the queue), and a redrawn diagram whose inner ring is an actual queue with one gate. SVG source committed next to the PNG. - MapLibre is now described as it is: a one-prop, token-free engine. - Removed internal credential/push instructions (AGENT-BRIEF.md covers agents). - documentation/: fixed 19 dead links left by the docs/ move, removed internal references, and listed the working docs in the index. - readme-shots: ONLY=<name> retakes a single image.
247 lines
16 KiB
Markdown
247 lines
16 KiB
Markdown
# Develop a plugin and see it live — the golden path
|
||
|
||
> **The one thing that matters:** author a map plugin, run one build, and see it live on the platform.
|
||
> This page THREADS the existing docs into that read-order — it does not duplicate them. Follow the six
|
||
> steps; each links to the doc that teaches that piece in depth. Brand-neutral: any team on any platform
|
||
> inherits this same path.
|
||
|
||
## The path at a glance
|
||
|
||
```
|
||
author build publish see it live
|
||
plugin-designs/ → bun run build → merge to catalog → https://<platform>/plugin-assets/…
|
||
(one folder) (one step) (the runbook) (a tab, an iframe, a <script>)
|
||
```
|
||
|
||
Everything you build is a **compiled Vue custom element** — one folder → one bundle → one `<tag>`. A static
|
||
HTML page is never a plugin (it can only be iframed). See [reference.md](reference.md) for the capability model.
|
||
|
||
## Step 1 — Understand the 5 capabilities (30 sec)
|
||
|
||
A plugin is one of five tiers, simplest → most composable:
|
||
**single-element → prop-table → monolithic → nested-element SPA → nested-SPA (app-shell)**.
|
||
You declare the tier in `<bundle>.meta.json` (`{"type": …}`) for the composite tiers. Read
|
||
[reference.md](reference.md) §capabilities; imitate the nearest reference plugin in `plugin-designs/`.
|
||
|
||
## Step 2 — Author the plugin (the 4-file pattern)
|
||
|
||
In **`plugin-designs/<your-plugin>/`** create these files (imitate the nearest reference plugin, never author
|
||
a plugin as a page). **Three are required; the rest are opt-in:**
|
||
|
||
| File | Required? | What it is |
|
||
|---|---|---|
|
||
| `<your-plugin>.vue` | **required** | the root Vue SFC — your plugin's UI (styles are shadow-scoped automatically) |
|
||
| `index.js` | **required** | `defineCustomElement(RootVue)` → `customElements.define('<your-tag>', El)` — register ONLY your own tag |
|
||
| `index.html` | **required** | the staging entry page that mounts `<your-tag>` — the build fails without it |
|
||
| `<your-plugin>.meta.json` | composite tiers only | `{"type": "<machine key>", "composes": […], "label": "…"}` |
|
||
| `<your-plugin>.instances.json` | optional | preset instances — supplying one makes the bundle **prop-table** (cap-2) |
|
||
| `src/` | optional | private components used only by this plugin |
|
||
|
||
> **Measured 2026-09-21, so you can calibrate.** Across the 18 shipped bundles the folder holds **3 to 8
|
||
> files** — not one has "exactly four." `all-people-groups` is the true minimum at **3** (root `.vue` +
|
||
> `index.js` + `index.html`) and is a fully working, manifest-listed plugin. **9 of 18 carry no `.meta.json`
|
||
> at all**, because single and prop-table tiers are inferred rather than declared. Start at three and add a
|
||
> file only when you need what it does.
|
||
>
|
||
> `index.html` really is load-bearing: moving it out of `demo-map/` made the build error and dropped the
|
||
> bundle from the buildable list (18 → 17). Restoring it returned a clean build.
|
||
|
||
**`plugin-designs/` is your FREE ZONE** — build freely here. Only contribute a component to the shared
|
||
`library/` (`@map`) if it is genuinely **reusable + parameterized** — a library edit reaches every plugin, so
|
||
it's a HIGH BAR (see [reference.md](reference.md) §governance). Full walkthrough: [tutorial.md](tutorial.md).
|
||
|
||
## Step 3 — Build (one step)
|
||
|
||
From `doxa-maps-plugin-studio/`:
|
||
|
||
```sh
|
||
bun run build # discovers plugin-designs/*, bundles each, writes public/doxa-maps/<plugin>/<plugin>.js
|
||
```
|
||
|
||
Exit 0 + all DX guards PASS + `inline-parse-check` PASS = a good build. Your plugin now exists as a real
|
||
custom-element bundle under `public/doxa-maps/<your-plugin>/`, with a standalone leaf page and a manifest
|
||
entry. Recipes: [how-to.md](how-to.md).
|
||
|
||
## Step 3a — The FAST loop: see it in the studio (~5s, no build)
|
||
|
||
Before the four-minute build, iterate in the **plugin studio** — the single staging environment, where
|
||
every plugin appears in one place as a real mounted element:
|
||
|
||
```sh
|
||
bun run dev # vite on :5173; `/` mounts <plugin-studio>, one tab per profile
|
||
bun run smoke:studio # ~5s: asserts the studio mounts and every tab is a real subtree
|
||
```
|
||
|
||
Measured 2026-09-22: **PASS in 4.7s — 29 tabs, 0 failed requests**, against a full `bun run build` of
|
||
**43s–4min** on the same box (it runs 19 bundles sequentially and queues behind any other build, so
|
||
the spread is real). An order of magnitude either way: this is the loop to live in while authoring.
|
||
The build is for publishing, not for looking.
|
||
|
||
> **★ The studio catches a class the built tree structurally cannot.** The dev manifest hands
|
||
> **rooted** refs (`/plugin-designs/…`) while the built manifest hands **relative** ones
|
||
> (`./bundle/bundle.js`). A base-join bug is therefore invisible in one surface and fatal in the
|
||
> other — which is why both this step and the next exist, rather than one replacing the other.
|
||
|
||
There is exactly **one** staging environment. If you find yourself looking at a second index of
|
||
plugins, it is the archived gallery at `/__gallery` — kept because the dev server still imports its
|
||
renderer, not because it is a place to review work.
|
||
|
||
## Step 3b — Prove it MOUNTS in a browser (before you publish)
|
||
|
||
A green build is a **static** result: the guards check that files exist, parse, carry a correct SRI hash and
|
||
resolve. None of them opens your element. A bundle can pass every guard above and still render nothing —
|
||
a thrown `setup()` removes the subtree silently, and the build stays green.
|
||
|
||
> **This was measured, not assumed (2026-09-21).** A `throw` was injected into `template-map-a.vue`'s
|
||
> `setup()` and the bundle rebuilt. Result: the build **succeeded** (206 kB emitted, exit 0), `check:parse`
|
||
> **PASS**, `check:drop` **PASS** — and `smoke:embeds` reported **0/2 pages mount cleanly**. One deliberate
|
||
> defect, invisible to every static guard, caught only in a browser. That gap is the entire reason this
|
||
> step exists.
|
||
|
||
```sh
|
||
# A token is required or map profiles cannot mount. Take it from the build config, which is
|
||
# the one source that works from a local gate — see the note below before changing this.
|
||
MAPBOX_TOKEN=$(python3 -c "import json;print(json.load(open('vite.doxa-maps-build-config.json'))['token'])")
|
||
|
||
MAPBOX_TOKEN=$MAPBOX_TOKEN bun run smoke:embeds # every profile in the manifest
|
||
MAPBOX_TOKEN=$MAPBOX_TOKEN node internal/embed-smoke.mjs --grep <your-plugin> # just yours
|
||
```
|
||
|
||
> **Do not fetch the token from `https://doxa.life/api/maps/token` here — measured 2026-09-23, it does
|
||
> not work for this.** That endpoint serves a token URL-restricted to `doxa.life`, so a tile request
|
||
> without that Referer returns **403** and every basemap in your smoke test renders blank while the pins
|
||
> draw fine. That is the exact symptom first reported as *"it's loading the pins, but it's not
|
||
> loading the actual underlying map"*, and it is why `tokenUrl` was emptied in the build config on
|
||
> 2026-09-22. This doc used to hand you that command, so an inheritor's first run reproduced the bug.
|
||
>
|
||
> The `token` field in `vite.doxa-maps-build-config.json` is the one that works: measured, the config
|
||
> token returns **200** for a tile request with no Referer while the endpoint token returns **403**. It
|
||
> is a `pk.` public key, baked deliberately (a project decision, 2026-09-21) so a map works on any host —
|
||
> protected by URL restrictions rather than secrecy. `check:public-hygiene` allows `pk.` and fails the
|
||
> build on `sk.`/`tk.`, so this is safe to read and never a secret to guard.
|
||
|
||
`smoke:embeds` serves `public/` and opens **every bundle × profile × preconfigured instance** from
|
||
`manifest.json`, asserting each custom element defines, attaches a shadow root and fills it. Expect
|
||
`N/N pages mount cleanly`. Measured 2026-09-21: **40/40**.
|
||
|
||
> **The token is not optional, and its absence is loud.** Without `MAPBOX_TOKEN` every *map* profile fails
|
||
> its mount assertion while every *DOM* profile passes — 27/40, which reads like 13 broken plugins and is
|
||
> not. The run prints a two-line warning when the token is missing; believe the warning, not the failures.
|
||
> `?tk=` alone is also not enough for a composite: it reaches only the top-level element's `profile-config`,
|
||
> while **nested children read `window.MAP_TOKEN`**. The smoke sets both.
|
||
|
||
**This step is not in CI.** CI is the single authoritative builder but installs no browser binary (no
|
||
`playwright install` step), so the mount check is a LOCAL gate an author runs deliberately. That is exactly
|
||
why it belongs here in the golden path rather than being left to be remembered.
|
||
|
||
## Step 4 — Publish to the live catalog
|
||
|
||
The platform serves from the **catalog repo** (`platform/mfe-index`), NOT from where you develop. Publishing
|
||
= merging your built bundles into the catalog. **The tested procedure, in one block:**
|
||
|
||
```sh
|
||
set -e # ← FIRST LINE, not a style choice. Explained below; without it this block is unsafe.
|
||
|
||
# 0. PRECONDITION: never publish a tree a build is rewriting. `clean` empties public/doxa-maps at
|
||
# the START of every build, so a concurrent build makes the source momentarily EMPTY.
|
||
bun run check:settled
|
||
|
||
# 1. clone the catalog fresh (fetch-first, never force)
|
||
git clone --depth 1 <catalog-repo-url> /tmp/catalog
|
||
# 2. merge your built bundles into its doxa-maps subtree (--delete mirrors removals)
|
||
rsync -a --delete public/doxa-maps/ /tmp/catalog/app/doxa-maps/
|
||
# 3. VERIFY — these are commands, not a reminder. Under `set -e` a failure aborts before the push.
|
||
bun run check:sri # every leaf's baked integrity matches the bundle it loads
|
||
bun run check:embed-targets # every embed.iframe src in the manifest resolves to a real file
|
||
git -C /tmp/catalog diff --stat -- . ':(exclude)app/doxa-maps' | grep -q . \
|
||
&& { echo 'ABORT: diff touches files outside app/doxa-maps'; exit 1; } || true
|
||
# 4. commit (product-only message), then push with a RAISED BUFFER (see below):
|
||
git -C /tmp/catalog -c http.postBuffer=524288000 push origin HEAD:main
|
||
```
|
||
|
||
> **★ Why `set -e` is the first line, and why step 3 is commands rather than a comment.** Both halves
|
||
> of this once took the live catalog down with a 404. The verify was chained into a compound command,
|
||
> so a *failing* check still fell through to the push; and `rsync --delete` ran from a source a
|
||
> concurrent build had just emptied, which faithfully mirrored that emptiness into the catalog.
|
||
> `--delete` is what makes a mid-churn source catastrophic — it deletes the live tree to match your
|
||
> empty one. So the settled check is a precondition, not a courtesy, and a verify that cannot abort
|
||
> is not a verify.
|
||
|
||
> **★ `http.postBuffer=524288000` is load-bearing, not optional.** The tree is ~39 MB and git's 1 MB
|
||
> default cannot stream it. Without the flag the push fails with `The remote end hung up unexpectedly`
|
||
> — which reads as a SERVER rejection (permissions? branch protection? repo full?) and sends you
|
||
> debugging the wrong machine entirely. It is a local buffer limit. This one line is the difference
|
||
> between step 4 working and step 4 looking like an access problem.
|
||
|
||
|
||
The platform serves the catalog directly, so a pushed change is live at once — verify at the URL in Step 5.
|
||
(The fuller runbook with every gotcha lives in the project's deploy-cycle notes; the block above is the
|
||
load-bearing sequence.)
|
||
|
||
(The end-state is the Forgejo CI build-gate — push source, CI builds + publishes automatically. Until a runner
|
||
is registered, the runbook is the manual path. Same `build:unlocked` seam.)
|
||
|
||
## Step 5 — See it live (the 5 deployment methods)
|
||
|
||
Your one element deploys five ways (only an element supports all five — a page supports iframe only):
|
||
|
||
| # | Method | How | Status today |
|
||
|---|---|---|---|
|
||
| 1 | **iframe** | point an `<iframe>` at the plugin's leaf page | ✅ proven |
|
||
| 3 | **JavaScript embed** | `<script src=".../<plugin>.js">` + `<your-tag>` (SRI-hashed) | ✅ proven |
|
||
| 2 | **mobile** | `bun run phone` (Capacitor) — build step, no new code | 🔶 built, not shot |
|
||
| 4 | **WordPress plugin** | the `deployment/wordpress/doxa-maps-embed/` PHP wrapper emits the embed snippet | 🔶 built, not shot |
|
||
| 5 | **desktop (Tauri)** | `bun run tauri:build` → a real binary + `.deb` wrapping the same `public/` tree | 🔶 built, not shot |
|
||
|
||
Say "proven" ONLY for 1 & 3. For 2, 4 and 5 say "built, not shot" — each is scaffolded and
|
||
contract-checked, and what is missing differs per method: 2 has no APK (no Android SDK here), 4 has
|
||
no capture of a live WordPress install, and 5 **does** emit artifacts — a 67 MB binary and a 60 MB
|
||
`.deb` are on disk — but no window has been opened, because this box is headless. "Not shot" means
|
||
nobody has watched it run, which is not the same as "nothing was produced". Verify at
|
||
`https://<platform>/plugin-assets/doxa-maps/<plugin>/<plugin>/index.html`.
|
||
**Done = the live site shows it, not a green build.**
|
||
|
||
> **Check the path itself, not just your plugin.** The catalog ships a one-element self-test of
|
||
> everything above — open
|
||
> `https://<platform>/plugin-assets/doxa-maps/golden-path-check/golden-path-check/index.html`.
|
||
> It reads the manifest it was published beside and reports four things: how many bundles and
|
||
> profiles the catalog holds, how many profiles describe themselves, and whether it can find
|
||
> *itself* listed. If it answers, authoring → build → publish → serve all worked on this platform,
|
||
> which is a different question from whether *your* plugin did. Verified on the live catalog
|
||
> 2026-09-23: 19 bundles, 28 profiles, 28 of 28 self-describing, listed = yes, zero console errors.
|
||
>
|
||
> Read what it does **not** claim. Those are questions about the catalog's SHAPE, so it stays green
|
||
> even if a bundle's CONTENT is stale — freshness belongs to `bun run check:catalog-currency`, which
|
||
> compares every live bundle's bytes against your build. Run both: the self-test says the pipe
|
||
> works, the currency gate says what came through it is current.
|
||
|
||
## Step 6 — Surface it (make it discoverable)
|
||
|
||
A plugin reachable only by direct URL isn't really "seen live." Good news: **you usually don't have to do
|
||
anything.**
|
||
|
||
**It is already surfaced — automatically.** The cap-5 SPA reads the built `manifest.json` at runtime and
|
||
auto-discovers every bundle that is neither already placed in a curated section nor on the small
|
||
`NOT_EXPERIMENTAL` exclusion list (the two shells, the template, and bundles that are nested *inside* another
|
||
tab). Your plugin appears in the collapsible **Experimental** folder at the bottom of the rail, marked 🧪, with
|
||
its label and description read from the manifest. No `TAB_DEFS` entry, no `NAV_MODEL` row, no code edit.
|
||
|
||
> Verified 2026-09-21 in a browser: the rail rendered **11 rows — 6 curated, then 5 auto-discovered**
|
||
> (`all-people-groups`, `country-posters`, `demo-map`, `doxa-vision-card`, `stack-demo`), each carrying the
|
||
> 🧪 marker. Discovery is fail-soft: if the manifest is unreachable the folder stays empty and the curated
|
||
> rail is unaffected — a discovery failure never takes down the app.
|
||
|
||
**Promote it to a curated tab (optional).** The curated sections encode a deliberate teaching order, so a
|
||
plugin graduates out of Experimental only when it earns a place in that order. That is the edit Step 6 used to
|
||
describe as mandatory: add a `TAB_DEFS` entry plus a `NAV_MODEL` row in `plugin-designs/doxa-maps-spa/`, then
|
||
rebuild + republish.
|
||
|
||
**One rail row per BUNDLE, not per profile.** A bundle's profiles (gospel-resources ships four) are alternate
|
||
views reached *inside* the pane. The rail is an onboarding ramp, not an inventory.
|
||
|
||
---
|
||
|
||
**That's the whole path: author in `plugin-designs/` → `bun run build` → prove it MOUNTS (`smoke:embeds`) →
|
||
publish via the runbook → see it live → and it surfaces itself in the SPA's Experimental folder.** Only two
|
||
of those six steps are edits you write: the plugin, and the optional promotion out of Experimental. Deep
|
||
dives: [tutorial.md](tutorial.md) · [how-to.md](how-to.md) · [reference.md](reference.md) · [why.md](why.md).
|