One server for a site and its maps: bun run dropin, and the drop-in goes straight into the site's public/

The drop-in was proven on a laptop with one server: the three DX Life branches rendered their maps,
7 of 7 checks passed, with internal/sync-dropin.mjs --from-config --dest <doxa-life>/public. This
makes that the default path:

- dropin.dest is ../doxa-life/public. The old ../doxa-life/public/embed/doxa-maps-build was stale.
- bun run dropin = sync-dropin --from-config; deploy:dev (build + drop-in) now uses it.
- The README gains "The easiest path": the studio for the work, dropin.dest at your site's public/,
  then build and drop in.

One hazard came with the new destination, and it is closed here. --prune deletes every file in the
destination outside the drop-in's allowlist, which was right while that was a folder of its own. Into
a site's public/, it would delete the site. deploy:dev passed --prune, so it no longer does, and
sync-dropin refuses --prune into any folder named public/. Measured in a scratch site: --prune into
site/public exits 1 with the reason; a plain drop-in copies dx-maps/ and leaves the site's own
file in place.
This commit is contained in:
Doxa Maps 2026-09-29 02:40:02 +00:00
parent a841dbd322
commit 867c559095
5 changed files with 28 additions and 6 deletions

View file

@ -688,6 +688,16 @@ bun run dev # → open the URL Vite prints: that page is the
No build needed first — the built maps are already in the repo. To start a plugin, copy
`plugin-designs/template-bundle/` to `plugin-designs/<your-map>/`; the studio finds it automatically.
### The easiest path: one server for your site and its maps
1. **Work in the studio.** `bun run dev` opens the Plugin Studio (with voice, if you set up Rudder above):
build and try maps there; they update as you save.
2. **Point the drop-in at your site.** In `vite.doxa-maps-build-config.json`, set `dropin.dest` to your
site's `public/` folder (the default is `../doxa-life/public`).
3. **Build and drop in.** `bun run build && bun run dropin` copies the maps into your site's
`public/dx-maps/`, and your site's own server serves them: one server, nothing else to deploy.
The drop-in touches only `dx-maps/`; the rest of your site is left alone.
<details>
<summary><b>Make changes to the studio itself</b></summary>

View file

@ -145,6 +145,7 @@ i18n, under a **distinct** tag, since `customElements.define()` is one-shot per
| `bun run clean` | wipes and recreates the `public/<name>/` output tree |
| `bun run tree` | writes `manifest.json` + the leaf pages, then copies the tree to every build-config `paths` destination |
| `bun run verify` · `bun run verify:full` | data-integrity, shipped-bundle and network-policy guards (fast, no browser) · plus rendered posters and live apps in a browser |
| `bun run dropin` | the one-server drop-in: copies the built tree into your site's `public/` (`dropin.dest`, default `../doxa-life/public`), so the site's own server serves every map. Never prunes; `--prune` into a `public/` folder is refused |
| `bun run sync:dropin <dest> [--from-config] [--prune]` | mirrors the built `public/` into a host checkout. The allowlist lives in ONE place, `vite.doxa-maps-build-config.json` → `dropin.include`; `dropin.exclude` lists built files no page fetches. Never copies credentials, docs or staging pages; `--dry-run` first |
| `bun run check:public-hygiene [<dir>] [--strict]` | proves a served folder is fit to publish: no local paths, internal names, dev URLs, token-shaped secrets, source maps or staging pages. Run it on a drop-in before any public push |

View file

@ -2,8 +2,9 @@
/**
* sync-dropin.mjs — mirror the built web-root (public/) into a HOST checkout as a "drop-in".
*
* WHY: the website (doxa-life) consumes this bundler's output as generated files under
* public/embed/doxa-maps-build/. Hand-editing there is forbidden — the ONLY way a map changes
* WHY: the website (doxa-life) consumes this bundler's output as generated files: the tree folder
* (dx-maps/) dropped straight into its public/, so one server serves the site and the maps. Hand-
* editing there is forbidden — the ONLY way a map changes
* on the site is: change the bundler → `bun run build` → sync. This is that one honest step,
* so it is a script and not a remembered rsync incantation.
*
@ -40,7 +41,7 @@
*
* Usage: node internal/sync-dropin.mjs <dest> [--from public] [--tree doxa-maps] [--keep-staging]
* [--only a,b | --from-config] [--prune] [--protect a.txt,other] [--dry-run]
* e.g. node internal/sync-dropin.mjs ../doxa-life/public/embed/doxa-maps-build --from-config --prune
* e.g. bun run dropin (= --from-config, into dropin.dest: ../doxa-life/public)
* Prints: copied · unchanged · deleted (stale inside the allowlist) · pruned (outside it) ·
* excluded (credentials/docs/staging) · excluded (not shipped: dropin.exclude), then the
* destination's top-level tree after the sync.
@ -48,7 +49,7 @@
* Review the result with `git -C <host> status` — the host repo is the restore point.
*/
import { readdirSync, statSync, lstatSync, mkdirSync, copyFileSync, rmSync, existsSync, readFileSync, realpathSync } from 'node:fs'
import { join, relative, resolve, dirname, sep } from 'node:path'
import { join, relative, resolve, dirname, sep, basename } from 'node:path'
import { fileURLToPath } from 'node:url'
import { createHash } from 'node:crypto'
import { TREE_NAME } from './_tree-name.mjs'
@ -87,6 +88,14 @@ const excluded = (f) => excludedInternal(f) || isNotShipped(f)
const usage = 'usage: node internal/sync-dropin.mjs [<dest>] [--from public] [--only a,b | --from-config] [--prune] [--protect a,b] [--dry-run] (<dest> defaults to $DOXA_DROPIN_DEST, then dropin.dest in vite.doxa-maps-build-config.json)'
if (!DEST) { console.error(`[sync-dropin] ${usage}`); process.exit(1) }
const dest = resolve(DEST)
// ★ --prune INTO A SITE'S WEB-ROOT WOULD DELETE THE SITE. --prune removes every file in <dest> outside the
// allowlist, which was right while <dest> was a folder of its own (public/embed/doxa-maps-build). The
// one-server drop-in goes straight into the site's public/, so <dest> IS the site and everything outside
// dx-maps/ would go. Refuse it outright rather than trust that nobody types it.
if (PRUNE && basename(dest) === 'public') {
console.error(`[sync-dropin] refusing --prune into ${dest}: it is a site's web-root, and --prune would delete every file of the site outside the drop-in. Sync without --prune (bun run dropin); stale files INSIDE the drop-in are still removed.`)
process.exit(1)
}
if (!existsSync(dest) || !statSync(dest).isDirectory()) { console.error(`[sync-dropin] dest does not exist or is not a directory: ${dest} — create the drop-in folder in the host first`); process.exit(1) }
if (!existsSync(FROM)) { console.error(`[sync-dropin] source missing: ${FROM} — run \`bun run build\` first`); process.exit(1) }

View file

@ -113,6 +113,7 @@
"check:embed-contract": "node internal/check-gospel-embed-contract.mjs",
"check:engine-contract": "node internal/check-gospel-engine-contract.mjs",
"check:link-coverage": "node internal/check-link-verification-coverage.mjs",
"dropin": "node internal/sync-dropin.mjs --from-config",
"gospel:bridge-reach": "node plugin-designs/gospel-resources/scripts/build-bridge-reach-table.mjs",
"gospel:field-cards": "node plugin-designs/gospel-resources/scripts/build-field-cards.mjs",
"check:link-coverage:poison": "node internal/check-link-verification-coverage.mjs --poison",
@ -309,7 +310,7 @@
"check:zoning-declared": "node internal/check-zoning-declared.mjs",
"check:zoning-declared:self-test": "node internal/check-zoning-declared.mjs --self-test",
"clean": "node internal/clean.mjs",
"deploy:dev": "bun run build && node internal/sync-dropin.mjs --from-config --prune",
"deploy:dev": "bun run build && bun run dropin",
"desktop:build": "node internal/desktop-build.mjs",
"dev": "bun x vite",
"fix:sri": "node internal/fix-sri.mjs",
@ -558,6 +559,7 @@
"check:zoning-declared": "A project list's ZONING matches what its author DECLARED (data/zoning-declarations.json). Zoning is human judgement — geography first, balanced with language — so a gate cannot decide whether a zoning is GOOD; it can decide whether it still matches what was stated, which is what makes UPG-100 reusable as a PATTERN for any project list (7000/4700/3000/2000/700) zoned by an agent who did not write this code. ★ EVERY OBVIOUS RULE FAILED THE REFERENCE IMPLEMENTATION when measured on the live 28 zones / 2107 records: 'every group in exactly one zone' is wrong (112 double-claims, 110 of them the design-approved deaf bloc double-count, legend-tree.js:151) and 'no group unzoned' is wrong (11 JPN/CHN/MNG groups, plausibly deliberate). So it asserts A1 no zone resolves to zero groups, A2 at most one GEOGRAPHIC zone per group with bloc zones declared as a second partition, A3 the orphan set EQUALS the declared set (a RESOLVED orphan fails too — a stale declaration stops being evidence), A4 every selector type is one selector-evaluator supports, A5 no zone over the declared page budget. A1 and A4 would both have caught this cycle's real defects (deaf + 5 wagf zones resolving to nothing) before a browser did, and A2 caught a third overlap my own hand analysis had missed. Exit 75 without records or declarations. Run :self-test to prove the five still fire.",
"deploy:dev": "destination + allowlist come from vite.doxa-maps-build-config.json (dropin.dest / dropin.include); DOXA_DROPIN_DEST=<dir> overrides the destination",
"desktop:build": "bun run desktop:build <plugin>: packages ONE plugin whose meta targets include desktop as a desktop app (Tauri): builds it into .desktop/<plugin>/, builds its sidecar engine for this machine, and runs tauri build with an override pointing the window at it. See documentation/HOWTO-build-the-desktop-app.md.",
"dropin": "copy the built tree (dx-maps/) into your site's public/ (vite.doxa-maps-build-config.json dropin.dest, default ../doxa-life/public), so ONE server serves the site and every map. Only the drop-in is touched: the site's own files stay, and stale files inside dx-maps/ are removed. It never prunes: --prune into a folder named public/ is refused, because it would delete the site.",
"gallery": "writes public/__gallery/index.html — the ARCHIVED bundle-index gallery, from public/doxa-maps/manifest.json. It is NO LONGER the root page: as of 2026-09-22 `/` mounts <plugin-studio> in BOTH trees (the built root page is written by `bun run tree`), and the gallery is the opt-in index at /__gallery. Archived, not deleted — the dev server imports renderGallery()/readBuildConfig() from this module.",
"gen:iso3-to-alpha2": "derive library/data/iso3-to-alpha2.json from vendored CLDR (48.2.2, Unicode-3.0) joined against the 242 codes in library/data/countries.json, so Intl.DisplayNames can produce a LOCALIZED country name — it takes alpha-2 and our data speaks alpha-3. Offline, no dependency. GRADES ITS OWN OUTPUT and writes nothing on failure: 242 coverage, injectivity, and KOS -> XK as the one documented exception (Kosovo has no ISO assignment). Cross-checks the 176-country research spine map and REPORTS disagreements without rewriting it — that file drives fetches and is not this generator's to edit.",
"internet:adm1": "regenerate data/tables/internet-by-adm1.json (internet use by first-level area; India from NFHS-5 2019-21, CC BY 4.0) from data/internet/ through a strict name crosswalk to the shipped gb-IND-ADM1 boundaries. Fails on an unmatched name, never guesses. Generic rows (iso3, adm1, measure, sex, area, value, source, year) so other plugins and go-int can reuse it.",

View file

@ -7,7 +7,7 @@
"tokenUrl": "",
"token": "",
"dropin": {
"dest": "../doxa-life/public/embed/doxa-maps-build",
"dest": "../doxa-life/public",
"include": [
"README.md"
],