Tooling reads the tree's name from the build config, in one place

The output tree's name has always been configurable (vite.doxa-maps-build-config.json "name"),
but 61 tooling files ignored it and hardcoded 'doxa-maps' (181 literals), so a rename would have
moved the build while the gates kept reading the old folder.

- internal/_tree-name.mjs reads it once (validated as one URL-safe path segment; falls back to the
  historical name) and exports TREE_NAME / TREE_NAME_RE; `node internal/_tree-name.mjs` prints it
  for shell scripts.
- A codemod rewrote 171 literals in internal/ to TREE_NAME; the regex literals, messages with
  backticks, atomic-public-swap.sh (fails loudly if the name cannot be read), build-lock.sh and
  vite.config.js (its duplicate readMfeName is gone) were done by hand.
- The name is unchanged: old and new tooling produce all 1408 files under public/ byte for byte.
  Code lines still naming the tree in internal/ + vite.config.js: 2, both intended (the helper's
  fallback, the private-repo-name pattern in public-hygiene-check).

This renames nothing visible; the eventual rename becomes a one-line config change.
This commit is contained in:
doxa-maps-88655c70 2026-09-25 15:42:10 +00:00
parent 577149afb4
commit 44ef31b2c6
75 changed files with 320 additions and 204 deletions

View file

@ -22,8 +22,9 @@
import { readFileSync, existsSync, realpathSync } from 'node:fs' import { readFileSync, existsSync, realpathSync } from 'node:fs'
import { createHash } from 'node:crypto' import { createHash } from 'node:crypto'
import { resolve, dirname, join } from 'node:path' import { resolve, dirname, join } from 'node:path'
import { TREE_NAME, TREE_NAME_RE } from './_tree-name.mjs'
const MANIFEST = 'doxa-maps/manifest.json' const MANIFEST = `${TREE_NAME}/manifest.json`
/** /**
* @param {string} base e.g. http://127.0.0.1:4173 (a trailing /doxa-maps/ is tolerated) * @param {string} base e.g. http://127.0.0.1:4173 (a trailing /doxa-maps/ is tolerated)
@ -37,7 +38,7 @@ export async function servesThisTree(base, repo) {
} catch { } catch {
return { ok: false, why: `no local public/${MANIFEST} to compare against — build the tree first` } return { ok: false, why: `no local public/${MANIFEST} to compare against — build the tree first` }
} }
const root = String(base).replace(/\/+$/, '').replace(/\/doxa-maps$/, '') const root = String(base).replace(/\/+$/, '').replace(new RegExp(`/${TREE_NAME_RE}$`), '')
const r = await fetch(`${root}/${MANIFEST}`).catch(() => null) const r = await fetch(`${root}/${MANIFEST}`).catch(() => null)
if (!r || !r.ok) return { ok: false, why: `nothing serving ${MANIFEST} at ${root}` } if (!r || !r.ok) return { ok: false, why: `nothing serving ${MANIFEST} at ${root}` }
const served = createHash('sha256').update(Buffer.from(await r.arrayBuffer())).digest('hex') const served = createHash('sha256').update(Buffer.from(await r.arrayBuffer())).digest('hex')

45
internal/_tree-name.mjs Normal file
View file

@ -0,0 +1,45 @@
/**
* _tree-name.mjs — the ONE place the output tree's name is read.
*
* The built tree lives at public/<TREE_NAME>/ and every embeddable URL carries it
* (…/<TREE_NAME>/<bundle>/<profile>/index.html). Its name has always been configurable —
* vite.doxa-maps-build-config.json `name` — but a census on 2026-09-25 found 61 tooling files
* that ignored the knob and hardcoded 'doxa-maps' (181 literals), so changing the config renamed
* the build while those gates kept reading the old folder. Every tool now imports TREE_NAME from
* here, which makes renaming the tree a one-line config change (see
* _qa-cycle/03-deployment-cycle/qa-loops/RENAME-IMPACT-dx.md, mechanism A).
*
* import { TREE_NAME, BUILD_CONFIG_FILE } from './_tree-name.mjs'
* TREE=$(node internal/_tree-name.mjs) # from a shell script
*
* The fallback, used only when the config is missing or has no name, is the historical name,
* so a checkout without the config behaves exactly as before.
*/
import { readFileSync } from 'node:fs'
import { resolve, dirname } from 'node:path'
import { fileURLToPath } from 'node:url'
const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..')
export const BUILD_CONFIG_FILE = resolve(ROOT, 'vite.doxa-maps-build-config.json')
const FALLBACK = 'doxa-maps'
function read() {
let name = ''
try {
const cfg = JSON.parse(readFileSync(BUILD_CONFIG_FILE, 'utf8'))
if (cfg && typeof cfg.name === 'string') name = cfg.name.trim()
} catch { /* no config: the fallback */ }
if (!name) return FALLBACK
// A tree name is ONE path segment that also appears in public URLs: refuse anything that could
// escape public/ or need escaping in a URL, rather than build into a surprising place.
if (!/^[a-z0-9][a-z0-9._-]*$/i.test(name) || name.includes('..')) {
throw new Error(`[_tree-name] build config "name" must be a single URL-safe path segment, got ${JSON.stringify(name)}`)
}
return name
}
export const TREE_NAME = read()
// The name escaped for use inside a RegExp source (a '.' in a future name must not match any char).
export const TREE_NAME_RE = TREE_NAME.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) process.stdout.write(TREE_NAME + '\n')

View file

@ -68,6 +68,7 @@ import { setTimeout as sleep } from 'node:timers/promises'
import { fileURLToPath } from 'node:url' import { fileURLToPath } from 'node:url'
import { dirname, resolve } from 'node:path' import { dirname, resolve } from 'node:path'
import { execFileSync } from 'node:child_process' import { execFileSync } from 'node:child_process'
import { TREE_NAME } from '../_tree-name.mjs'
const __dirname = fileURLToPath(new URL('.', import.meta.url)) const __dirname = fileURLToPath(new URL('.', import.meta.url))
const ROOT = resolve(__dirname, '..', '..') const ROOT = resolve(__dirname, '..', '..')
@ -178,7 +179,7 @@ async function readGrid(page) {
async function runDesktop(browser) { async function runDesktop(browser) {
const PAIR = 'desktop dashboard-plus-grid' const PAIR = 'desktop dashboard-plus-grid'
const page = await browser.newPage({ viewport: { width: 1600, height: 1000 } }) const page = await browser.newPage({ viewport: { width: 1600, height: 1000 } })
const url = `${BASE}/doxa-maps/dashboard-plus-grid/dashboard-plus-grid/index.html` const url = `${BASE}/${TREE_NAME}/dashboard-plus-grid/dashboard-plus-grid/index.html`
console.log(`\n=== ${PAIR} — ${url} ===`) console.log(`\n=== ${PAIR} — ${url} ===`)
try { try {
await page.goto(url, { waitUntil: 'load', timeout: 60000 }) await page.goto(url, { waitUntil: 'load', timeout: 60000 })
@ -344,7 +345,7 @@ async function runDesktop(browser) {
async function runMobile(browser) { async function runMobile(browser) {
const PAIR = 'mobile research-map-plus-grid' const PAIR = 'mobile research-map-plus-grid'
const page = await browser.newPage({ viewport: { width: 420, height: 900 } }) const page = await browser.newPage({ viewport: { width: 420, height: 900 } })
const url = `${BASE}/doxa-maps/research-map-plus-grid/research-map-plus-grid/index.html` const url = `${BASE}/${TREE_NAME}/research-map-plus-grid/research-map-plus-grid/index.html`
console.log(`\n=== ${PAIR} — ${url} ===`) console.log(`\n=== ${PAIR} — ${url} ===`)
try { try {
await page.goto(url, { waitUntil: 'load', timeout: 60000 }) await page.goto(url, { waitUntil: 'load', timeout: 60000 })

View file

@ -34,11 +34,16 @@ DRY_RUN="${DRY_RUN:-0}"
die() { echo "❌ atomic-public-swap: $*" >&2; exit 1; } die() { echo "❌ atomic-public-swap: $*" >&2; exit 1; }
[ -n "$STAGE_DIR" ] || die "usage: atomic-public-swap.sh <STAGE_DIR> (dir holding a built doxa-maps/ + index.html)" # The tree's name comes from the build config (internal/_tree-name.mjs), never a literal: this script
# renames live folders, so it must fail rather than swap the wrong one if the name cannot be read.
TREE="$(node "$(dirname "$0")/_tree-name.mjs")" || die "could not read the tree name (internal/_tree-name.mjs)"
[ -n "$TREE" ] || die "empty tree name from internal/_tree-name.mjs"
[ -n "$STAGE_DIR" ] || die "usage: atomic-public-swap.sh <STAGE_DIR> (dir holding a built $TREE/ + index.html)"
[ -d "$STAGE_DIR" ] || die "STAGE_DIR '$STAGE_DIR' is not a directory" [ -d "$STAGE_DIR" ] || die "STAGE_DIR '$STAGE_DIR' is not a directory"
[ -d "$PUBLIC_DIR" ] || die "PUBLIC_DIR '$PUBLIC_DIR' is not a directory (run from the studio root)" [ -d "$PUBLIC_DIR" ] || die "PUBLIC_DIR '$PUBLIC_DIR' is not a directory (run from the studio root)"
STAGE_TREE="$STAGE_DIR/doxa-maps" STAGE_TREE="$STAGE_DIR/$TREE"
STAGE_INDEX="$STAGE_DIR/index.html" STAGE_INDEX="$STAGE_DIR/index.html"
# --- GATE: the stage must be a COMPLETE, verified tree before we touch the live web-root --------------------- # --- GATE: the stage must be a COMPLETE, verified tree before we touch the live web-root ---------------------
@ -81,12 +86,12 @@ stage_dev=$(stat -c '%d' "$STAGE_DIR")
pub_dev=$(stat -c '%d' "$PUBLIC_DIR") pub_dev=$(stat -c '%d' "$PUBLIC_DIR")
[ "$stage_dev" = "$pub_dev" ] || die "STAGE_DIR and PUBLIC_DIR are on different filesystems ($stage_dev != $pub_dev) — rename() would not be atomic. Stage under the same volume as public/." [ "$stage_dev" = "$pub_dev" ] || die "STAGE_DIR and PUBLIC_DIR are on different filesystems ($stage_dev != $pub_dev) — rename() would not be atomic. Stage under the same volume as public/."
OLD_TREE="$PUBLIC_DIR/doxa-maps.old.$$" OLD_TREE="$PUBLIC_DIR/$TREE.old.$$"
if [ "$DRY_RUN" = "1" ]; then if [ "$DRY_RUN" = "1" ]; then
echo "DRY_RUN — plan (no changes):" echo "DRY_RUN — plan (no changes):"
echo " 1. mv $PUBLIC_DIR/doxa-maps -> $OLD_TREE" echo " 1. mv $PUBLIC_DIR/$TREE -> $OLD_TREE"
echo " 2. mv $STAGE_TREE -> $PUBLIC_DIR/doxa-maps" echo " 2. mv $STAGE_TREE -> $PUBLIC_DIR/$TREE"
echo " 3. cp $STAGE_INDEX -> $PUBLIC_DIR/index.html" echo " 3. cp $STAGE_INDEX -> $PUBLIC_DIR/index.html"
echo " 4. rm -rf $OLD_TREE" echo " 4. rm -rf $OLD_TREE"
echo " (NEVER touched: $PUBLIC_DIR/assets/ $PUBLIC_DIR/.gitignore $PUBLIC_DIR/.build.lock)" echo " (NEVER touched: $PUBLIC_DIR/assets/ $PUBLIC_DIR/.gitignore $PUBLIC_DIR/.build.lock)"
@ -94,11 +99,11 @@ if [ "$DRY_RUN" = "1" ]; then
fi fi
# --- THE SWAP (scoped; rename-based; public/ itself and the lock never move) --------------------------------- # --- THE SWAP (scoped; rename-based; public/ itself and the lock never move) ---------------------------------
if [ -d "$PUBLIC_DIR/doxa-maps" ]; then if [ -d "$PUBLIC_DIR/$TREE" ]; then
mv "$PUBLIC_DIR/doxa-maps" "$OLD_TREE" # 1. instant; old tree still fully valid under new name mv "$PUBLIC_DIR/$TREE" "$OLD_TREE" # 1. instant; old tree still fully valid under new name
fi fi
mv "$STAGE_TREE" "$PUBLIC_DIR/doxa-maps" # 2. instant; new tree (built+verified) becomes live mv "$STAGE_TREE" "$PUBLIC_DIR/$TREE" # 2. instant; new tree (built+verified) becomes live
cp "$STAGE_INDEX" "$PUBLIC_DIR/index.html" # 3. fresh root gallery page cp "$STAGE_INDEX" "$PUBLIC_DIR/index.html" # 3. fresh root gallery page
rm -rf "$OLD_TREE" # 4. drop the superseded tree rm -rf "$OLD_TREE" # 4. drop the superseded tree
echo "🔀 atomic swap complete — $PUBLIC_DIR/doxa-maps is the freshly-built tree; assets/ + .build.lock untouched." echo "🔀 atomic swap complete — $PUBLIC_DIR/$TREE is the freshly-built tree; assets/ + .build.lock untouched."

View file

@ -51,7 +51,7 @@ mkdir -p "$(dirname "$LOCK_FILE")"
# that distinguishes coordinator from subagent; anything claiming to is an untested guess. # that distinguishes coordinator from subagent; anything claiming to is an untested guess.
if [ "${BUILD_ALLOW:-}" != "1" ]; then if [ "${BUILD_ALLOW:-}" != "1" ]; then
echo "🚫 FULL BUILD REFUSED — a full build is almost never what you want here, and its FIRST" >&2 echo "🚫 FULL BUILD REFUSED — a full build is almost never what you want here, and its FIRST" >&2
echo " step deletes public/doxa-maps/ entirely, which tears" >&2 echo " step deletes public/$(node "$(dirname "$0")/_tree-name.mjs" 2>/dev/null || echo "<tree>")/ entirely, which tears" >&2
echo " the tree out from under every other agent reading it (measured 3+ times, incl. a" >&2 echo " the tree out from under every other agent reading it (measured 3+ times, incl. a" >&2
echo " 461-file phantom deletion and several false 'element did not mount' diagnoses)." >&2 echo " 461-file phantom deletion and several false 'element did not mount' diagnoses)." >&2
echo "" >&2 echo "" >&2

View file

@ -35,10 +35,11 @@ import { resolve, dirname, join } from 'node:path'
import { fileURLToPath } from 'node:url' import { fileURLToPath } from 'node:url'
import { chromium } from 'playwright' import { chromium } from 'playwright'
import { servesThisTree } from './_serves-this-tree.mjs' import { servesThisTree } from './_serves-this-tree.mjs'
import { TREE_NAME } from './_tree-name.mjs'
const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..') const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..')
const MANIFEST = join(ROOT, 'public', 'doxa-maps', 'manifest.json') const MANIFEST = join(ROOT, 'public', TREE_NAME, 'manifest.json')
const BASE = process.env.BASE || 'http://127.0.0.1:4173/doxa-maps/' const BASE = process.env.BASE || `http://127.0.0.1:4173/${TREE_NAME}/`
const ALL = process.env.ALL === '1' const ALL = process.env.ALL === '1'
if (!existsSync(MANIFEST)) { if (!existsSync(MANIFEST)) {
@ -55,7 +56,7 @@ if (!existsSync(MANIFEST)) {
if (!tree.ok) { if (!tree.ok) {
console.error('A11Y-BUTTON-NAMES: NOT MEASURED — the server is not serving this checkout.') console.error('A11Y-BUTTON-NAMES: NOT MEASURED — the server is not serving this checkout.')
console.error(` ${tree.why}`) console.error(` ${tree.why}`)
console.error(' Point BASE at your own server (BASE=http://127.0.0.1:PORT/doxa-maps/).') console.error(` Point BASE at your own server (BASE=http://127.0.0.1:PORT/${TREE_NAME}/).`)
process.exit(2) process.exit(2)
} }
} }

View file

@ -30,6 +30,7 @@ import { chromium } from 'playwright'
import { requireThisTree } from './_serves-this-tree.mjs' import { requireThisTree } from './_serves-this-tree.mjs'
import { resolve as _resolvePath, dirname as _dirnamePath } from 'node:path' import { resolve as _resolvePath, dirname as _dirnamePath } from 'node:path'
import { fileURLToPath as _fileURLToPath } from 'node:url' import { fileURLToPath as _fileURLToPath } from 'node:url'
import { TREE_NAME } from './_tree-name.mjs'
const URL_ = process.argv[2] || process.env.ARIA_URL const URL_ = process.argv[2] || process.env.ARIA_URL
const ROOT = _resolvePath(_dirnamePath(_fileURLToPath(import.meta.url)), '..') const ROOT = _resolvePath(_dirnamePath(_fileURLToPath(import.meta.url)), '..')
@ -43,7 +44,7 @@ await requireThisTree(URL_, ROOT, 'ARIA-LABELS-LOCALIZED')
if (!URL_) { if (!URL_) {
console.log('ARIA-LABELS-LOCALIZED: SKIP — no leaf url given.') console.log('ARIA-LABELS-LOCALIZED: SKIP — no leaf url given.')
console.log(' Pass one, or set ARIA_URL. Serve the TREE ROOT (public/) and point at a leaf,') console.log(' Pass one, or set ARIA_URL. Serve the TREE ROOT (public/) and point at a leaf,')
console.log(' e.g. doxa-maps/my-upg-100-list/upg-100-list/index.html') console.log(` e.g. ${TREE_NAME}/my-upg-100-list/upg-100-list/index.html`)
process.exit(0) process.exit(0)
} }
const UNDER_TEST = (process.argv[3] || 'es,ar,zh').split(',').map((s) => s.trim()).filter(Boolean) const UNDER_TEST = (process.argv[3] || 'es,ar,zh').split(',').map((s) => s.trim()).filter(Boolean)

View file

@ -30,6 +30,7 @@ import { spawn } from 'node:child_process'
import { createServer } from 'node:http' import { createServer } from 'node:http'
import { resolve, dirname } from 'node:path' import { resolve, dirname } from 'node:path'
import { fileURLToPath } from 'node:url' import { fileURLToPath } from 'node:url'
import { TREE_NAME } from './_tree-name.mjs'
const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..') const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..')
const argv = process.argv.slice(2) const argv = process.argv.slice(2)
@ -41,13 +42,13 @@ const PAIRS = { Germany: 'Германия', Ukraine: 'Украина', Turkey:
const port = await new Promise((ok) => { const s = createServer().listen(0, '127.0.0.1', () => { const p = s.address().port; s.close(() => ok(p)) }) }) const port = await new Promise((ok) => { const s = createServer().listen(0, '127.0.0.1', () => { const p = s.address().port; s.close(() => ok(p)) }) })
const srv = spawn(process.execPath, [resolve(ROOT, 'internal/serve-strict.mjs'), '--port', String(port), '--directory', DIR], { stdio: 'ignore' }) const srv = spawn(process.execPath, [resolve(ROOT, 'internal/serve-strict.mjs'), '--port', String(port), '--directory', DIR], { stdio: 'ignore' })
await new Promise((r) => setTimeout(r, 900)) await new Promise((r) => setTimeout(r, 900))
const up = await fetch(`http://127.0.0.1:${port}/doxa-maps/${LEAF}/index.html`).then((r) => r.ok).catch(() => false) const up = await fetch(`http://127.0.0.1:${port}/${TREE_NAME}/${LEAF}/index.html`).then((r) => r.ok).catch(() => false)
if (!up) { srv.kill(); console.log(`BASEMAP-LABELS: NOT MEASURED — could not serve ${DIR}`); process.exit(2) } if (!up) { srv.kill(); console.log(`BASEMAP-LABELS: NOT MEASURED — could not serve ${DIR}`); process.exit(2) }
const browser = await chromium.launch({ args: ['--use-gl=swiftshader', '--enable-webgl', '--ignore-gpu-blocklist'] }) const browser = await chromium.launch({ args: ['--use-gl=swiftshader', '--enable-webgl', '--ignore-gpu-blocklist'] })
async function labels(provider, lang) { async function labels(provider, lang) {
const page = await browser.newPage({ viewport: { width: 1280, height: 900 } }) const page = await browser.newPage({ viewport: { width: 1280, height: 900 } })
await page.goto(`http://127.0.0.1:${port}/doxa-maps/${LEAF}/index.html?lang=${lang}${provider === 'maplibre' ? '&provider=maplibre' : ''}`, { waitUntil: 'networkidle' }) await page.goto(`http://127.0.0.1:${port}/${TREE_NAME}/${LEAF}/index.html?lang=${lang}${provider === 'maplibre' ? '&provider=maplibre' : ''}`, { waitUntil: 'networkidle' })
const FIND = `(() => { const f = (r) => { for (const e of r.querySelectorAll('*')) { if (e.__doxaMap) return e.__doxaMap; if (e.shadowRoot) { const m = f(e.shadowRoot); if (m) return m } } return null }; return f(document) })()` const FIND = `(() => { const f = (r) => { for (const e of r.querySelectorAll('*')) { if (e.__doxaMap) return e.__doxaMap; if (e.shadowRoot) { const m = f(e.shadowRoot); if (m) return m } } return null }; return f(document) })()`
let ok = false let ok = false
for (let i = 0; i < 60 && !ok; i++) { await page.waitForTimeout(500); ok = await page.evaluate(`(() => { const m = ${FIND}; return !!(m && m.isStyleLoaded && m.isStyleLoaded()) })()`).catch(() => false) } for (let i = 0; i < 60 && !ok; i++) { await page.waitForTimeout(500); ok = await page.evaluate(`(() => { const m = ${FIND}; return !!(m && m.isStyleLoaded && m.isStyleLoaded()) })()`).catch(() => false) }

View file

@ -34,10 +34,11 @@
import { readFileSync, existsSync, readdirSync } from 'node:fs' import { readFileSync, existsSync, readdirSync } from 'node:fs'
import { resolve, dirname, join } from 'node:path' import { resolve, dirname, join } from 'node:path'
import { fileURLToPath } from 'node:url' import { fileURLToPath } from 'node:url'
import { TREE_NAME } from './_tree-name.mjs'
const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..') const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..')
const README = join(ROOT, 'README.md') const README = join(ROOT, 'README.md')
const MANIFEST = join(ROOT, 'public', 'doxa-maps', 'manifest.json') const MANIFEST = join(ROOT, 'public', TREE_NAME, 'manifest.json')
// ★ WIDENED 2026-09-22 TO documentation/ AS WELL AS README.md. // ★ WIDENED 2026-09-22 TO documentation/ AS WELL AS README.md.
// //

View file

@ -22,11 +22,12 @@ import { readFileSync, existsSync } from 'node:fs'
import { execFileSync } from 'node:child_process' import { execFileSync } from 'node:child_process'
import { resolve, dirname, join } from 'node:path' import { resolve, dirname, join } from 'node:path'
import { fileURLToPath } from 'node:url' import { fileURLToPath } from 'node:url'
import { TREE_NAME } from './_tree-name.mjs'
const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..') const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..')
const OUT = join(ROOT, 'documentation', 'reference-capability-exemplars.md') const OUT = join(ROOT, 'documentation', 'reference-capability-exemplars.md')
const GEN = join(ROOT, 'internal', 'generate-capability-exemplars.mjs') const GEN = join(ROOT, 'internal', 'generate-capability-exemplars.mjs')
const MANIFEST = join(ROOT, 'public', 'doxa-maps', 'manifest.json') const MANIFEST = join(ROOT, 'public', TREE_NAME, 'manifest.json')
if (!existsSync(MANIFEST)) { if (!existsSync(MANIFEST)) {
console.log('CAPABILITY-EXEMPLARS: SKIP — no manifest.json (nothing built yet).') console.log('CAPABILITY-EXEMPLARS: SKIP — no manifest.json (nothing built yet).')

View file

@ -33,18 +33,19 @@ import { readFileSync, existsSync } from 'node:fs'
import { createHash } from 'node:crypto' import { createHash } from 'node:crypto'
import { resolve, dirname, join } from 'node:path' import { resolve, dirname, join } from 'node:path'
import { fileURLToPath } from 'node:url' import { fileURLToPath } from 'node:url'
import { TREE_NAME } from './_tree-name.mjs'
const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..') const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..')
const args = process.argv.slice(2) const args = process.argv.slice(2)
const argOf = (k, d) => { const i = args.indexOf(k); return i >= 0 ? args[i + 1] : d } const argOf = (k, d) => { const i = args.indexOf(k); return i >= 0 ? args[i + 1] : d }
const BASE = (argOf('--base', 'https://nuxtbase.app/plugin-assets/doxa-maps')).replace(/\/+$/, '') const BASE = (argOf('--base', `https://nuxtbase.app/plugin-assets/${TREE_NAME}`)).replace(/\/+$/, '')
const TIMEOUT = Number(argOf('--timeout', 25000)) const TIMEOUT = Number(argOf('--timeout', 25000))
const sha384 = (buf) => 'sha384-' + createHash('sha384').update(buf).digest('base64') const sha384 = (buf) => 'sha384-' + createHash('sha384').update(buf).digest('base64')
const localManifestPath = join(ROOT, 'public', 'doxa-maps', 'manifest.json') const localManifestPath = join(ROOT, 'public', TREE_NAME, 'manifest.json')
if (!existsSync(localManifestPath)) { if (!existsSync(localManifestPath)) {
console.log('CATALOG-CURRENCY: SKIP — no local build (public/doxa-maps/manifest.json missing). Run `bun run build`.') console.log(`CATALOG-CURRENCY: SKIP — no local build (public/${TREE_NAME}/manifest.json missing). Run \`bun run build\`.`)
process.exit(0) process.exit(0)
} }
const local = JSON.parse(readFileSync(localManifestPath, 'utf8')) const local = JSON.parse(readFileSync(localManifestPath, 'utf8'))
@ -125,7 +126,7 @@ for (const b of local.bundles || []) {
const profile = b.profiles?.[0]?.profile const profile = b.profiles?.[0]?.profile
if (!profile) continue if (!profile) continue
const rel = `${b.bundle}/${profile}/index.html` const rel = `${b.bundle}/${profile}/index.html`
const localPath = join(ROOT, 'public', 'doxa-maps', rel) const localPath = join(ROOT, 'public', TREE_NAME, rel)
if (!existsSync(localPath)) continue if (!existsSync(localPath)) continue
const { buf, err } = await fetchBuf(`${BASE}/${rel}`) const { buf, err } = await fetchBuf(`${BASE}/${rel}`)
// Same MISSING-vs-UNREACHABLE split as the bundle loop above: a 4xx is the server telling us the // Same MISSING-vs-UNREACHABLE split as the bundle loop above: a 4xx is the server telling us the
@ -157,7 +158,7 @@ if (missing.length) {
'\n Something was LOST between the build and the catalog — the merge dropped it, or the publish\n' + '\n Something was LOST between the build and the catalog — the merge dropped it, or the publish\n' +
' never included it. This is the guarantee Card 1 Step 1 asks for ("merge all bundles so nothing\n' + ' never included it. This is the guarantee Card 1 Step 1 asks for ("merge all bundles so nothing\n' +
' is lost"). Re-run the publish and assert the resulting bundle SET, not just the changed files:\n' + ' is lost"). Re-run the publish and assert the resulting bundle SET, not just the changed files:\n' +
' rsync -a --delete public/doxa-maps/ <catalog>/app/doxa-maps/ # --delete mirrors removals\n' + ` rsync -a --delete public/${TREE_NAME}/ <catalog>/app/${TREE_NAME}/ # --delete mirrors removals\n` +
' then confirm every local bundle name appears live before pushing.' ' then confirm every local bundle name appears live before pushing.'
) )
process.exit(1) process.exit(1)
@ -167,7 +168,7 @@ if (stale.length) {
for (const s of stale) console.error(` ✗ ${s.bundle}\n local ${s.want}…\n live ${s.got}…`) for (const s of stale) console.error(` ✗ ${s.bundle}\n local ${s.want}…\n live ${s.got}…`)
console.error('\n The live catalog is BEHIND this build. Publish per') console.error('\n The live catalog is BEHIND this build. Publish per')
console.error(' _qa-cycle/03-deployment-cycle/delegation/RUNBOOK-publish-bundles-to-catalog.md') console.error(' _qa-cycle/03-deployment-cycle/delegation/RUNBOOK-publish-bundles-to-catalog.md')
console.error(' (rsync public/doxa-maps/ into the catalog, then push with -c http.postBuffer=524288000).') console.error(` (rsync public/${TREE_NAME}/ into the catalog, then push with -c http.postBuffer=524288000).`)
process.exit(1) process.exit(1)
} }
console.log('CATALOG-CURRENCY: PASS — every local bundle is present live and matches this build.') console.log('CATALOG-CURRENCY: PASS — every local bundle is present live and matches this build.')

View file

@ -34,8 +34,9 @@
import { readFileSync, existsSync } from 'node:fs' import { readFileSync, existsSync } from 'node:fs'
import { createHash } from 'node:crypto' import { createHash } from 'node:crypto'
import { join } from 'node:path' import { join } from 'node:path'
import { TREE_NAME } from './_tree-name.mjs'
const BASE = (process.env.BASE || 'https://nuxtbase.app/plugin-assets/doxa-maps/').replace(/\/?$/, '/') const BASE = (process.env.BASE || `https://nuxtbase.app/plugin-assets/${TREE_NAME}/`).replace(/\/?$/, '/')
const CATALOG = process.env.CATALOG || '' const CATALOG = process.env.CATALOG || ''
const sha384 = (buf) => 'sha384-' + createHash('sha384').update(buf).digest('base64') const sha384 = (buf) => 'sha384-' + createHash('sha384').update(buf).digest('base64')
@ -64,7 +65,7 @@ try {
console.error('CATALOG-IDENTITY: FAIL — the manifest URL did not return JSON.') console.error('CATALOG-IDENTITY: FAIL — the manifest URL did not return JSON.')
console.error(` ${BASE}manifest.json served ${liveManifestBuf.length} bytes that do not parse.`) console.error(` ${BASE}manifest.json served ${liveManifestBuf.length} bytes that do not parse.`)
console.error(' This is what the LOGIN WALL looks like: HTTP 200 with an HTML sign-in page.') console.error(' This is what the LOGIN WALL looks like: HTTP 200 with an HTML sign-in page.')
console.error(' The public door is /plugin-assets/doxa-maps/ — NOT /app/doxa-maps/.') console.error(` The public door is /plugin-assets/${TREE_NAME}/ — NOT /app/${TREE_NAME}/.`)
process.exit(1) process.exit(1)
} }

View file

@ -49,9 +49,10 @@ import { spawn } from 'node:child_process'
import { existsSync, readFileSync } from 'node:fs' import { existsSync, readFileSync } from 'node:fs'
import { resolve, dirname, join } from 'node:path' import { resolve, dirname, join } from 'node:path'
import { fileURLToPath } from 'node:url' import { fileURLToPath } from 'node:url'
import { TREE_NAME } from './_tree-name.mjs'
const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..') const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..')
const MANIFEST = join(ROOT, 'public', 'doxa-maps', 'manifest.json') const MANIFEST = join(ROOT, 'public', TREE_NAME, 'manifest.json')
if (!existsSync(MANIFEST)) { if (!existsSync(MANIFEST)) {
console.log('COMPOSITE-EMBED-KNOBS: SKIP — no built manifest (run `bun run tree` first).') console.log('COMPOSITE-EMBED-KNOBS: SKIP — no built manifest (run `bun run tree` first).')
@ -95,7 +96,7 @@ const bakedToken = (() => {
for (const b of manifest.bundles || []) { for (const b of manifest.bundles || []) {
for (const p of b.profiles || []) { for (const p of b.profiles || []) {
if (!p.isMap || !p.iframeUrl) continue if (!p.isMap || !p.iframeUrl) continue
const leaf = join(ROOT, 'public', 'doxa-maps', String(p.iframeUrl).replace(/^\.\//, '')) const leaf = join(ROOT, 'public', TREE_NAME, String(p.iframeUrl).replace(/^\.\//, ''))
if (!existsSync(leaf)) continue if (!existsSync(leaf)) continue
const m = readFileSync(leaf, 'utf8').match(/BAKED_TOKEN\s*=\s*"(pk\.[^"]+)"/) const m = readFileSync(leaf, 'utf8').match(/BAKED_TOKEN\s*=\s*"(pk\.[^"]+)"/)
if (m) return m[1] if (m) return m[1]
@ -109,7 +110,7 @@ const freePort = () => new Promise((ok) => {
}) })
const TREE_PORT = await freePort() const TREE_PORT = await freePort()
const HOST_PORT = await freePort() const HOST_PORT = await freePort()
const TREE = `http://127.0.0.1:${TREE_PORT}/doxa-maps` const TREE = `http://127.0.0.1:${TREE_PORT}/${TREE_NAME}`
let pageHtml = '<!doctype html><html><body>nothing yet</body></html>' let pageHtml = '<!doctype html><html><body>nothing yet</body></html>'
const tree = spawn(process.execPath, ['internal/serve-strict.mjs', '--port', String(TREE_PORT)], { cwd: ROOT, stdio: 'ignore' }) const tree = spawn(process.execPath, ['internal/serve-strict.mjs', '--port', String(TREE_PORT)], { cwd: ROOT, stdio: 'ignore' })

View file

@ -41,16 +41,17 @@ import { spawnSync } from 'node:child_process'
import { tmpdir } from 'node:os' import { tmpdir } from 'node:os'
import { resolve, dirname, join } from 'node:path' import { resolve, dirname, join } from 'node:path'
import { fileURLToPath } from 'node:url' import { fileURLToPath } from 'node:url'
import { TREE_NAME } from './_tree-name.mjs'
const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..') const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..')
const DESIGNS = join(ROOT, 'plugin-designs') const DESIGNS = join(ROOT, 'plugin-designs')
const TREE = join(ROOT, 'public', 'doxa-maps') const TREE = join(ROOT, 'public', TREE_NAME)
const argv = process.argv.slice(2) const argv = process.argv.slice(2)
const oi = argv.indexOf('--only') const oi = argv.indexOf('--only')
const ONLY = oi >= 0 && argv[oi + 1] ? new Set(argv[oi + 1].split(',')) : null const ONLY = oi >= 0 && argv[oi + 1] ? new Set(argv[oi + 1].split(',')) : null
if (!existsSync(join(TREE, 'manifest.json'))) { if (!existsSync(join(TREE, 'manifest.json'))) {
console.log('COMPOSITE-FRESHNESS: SKIP — no built tree (public/doxa-maps/manifest.json).') console.log(`COMPOSITE-FRESHNESS: SKIP — no built tree (public/${TREE_NAME}/manifest.json).`)
process.exit(0) process.exit(0)
} }
const manifest = JSON.parse(readFileSync(join(TREE, 'manifest.json'), 'utf8')) const manifest = JSON.parse(readFileSync(join(TREE, 'manifest.json'), 'utf8'))
@ -81,7 +82,7 @@ try {
for (const e of composes) if (!resolveEntry(e, bundle)) unresolved.push(`${bundle} composes "${e}" — resolves to no bundle`) for (const e of composes) if (!resolveEntry(e, bundle)) unresolved.push(`${bundle} composes "${e}" — resolves to no bundle`)
composites++ composites++
const committed = join(TREE, bundle, `${bundle}.js`) const committed = join(TREE, bundle, `${bundle}.js`)
if (!existsSync(committed)) { failures.push(`${bundle}: no committed bundle at public/doxa-maps/${bundle}/${bundle}.js`); continue } if (!existsSync(committed)) { failures.push(`${bundle}: no committed bundle at public/${TREE_NAME}/${bundle}/${bundle}.js`); continue }
const out = join(tmp, bundle) const out = join(tmp, bundle)
const r = spawnSync('bun', ['x', 'vite', 'build'], { cwd: ROOT, env: { ...process.env, BUNDLE: bundle, DOXA_OUT_ROOT: out }, encoding: 'utf8' }) const r = spawnSync('bun', ['x', 'vite', 'build'], { cwd: ROOT, env: { ...process.env, BUNDLE: bundle, DOXA_OUT_ROOT: out }, encoding: 'utf8' })
const built = join(out, bundle, `${bundle}.js`) const built = join(out, bundle, `${bundle}.js`)

View file

@ -33,6 +33,7 @@
import { readFileSync, existsSync, readdirSync } from 'node:fs' import { readFileSync, existsSync, readdirSync } from 'node:fs'
import { resolve, dirname, join } from 'node:path' import { resolve, dirname, join } from 'node:path'
import { fileURLToPath } from 'node:url' import { fileURLToPath } from 'node:url'
import { TREE_NAME } from './_tree-name.mjs'
const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..') const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..')
@ -51,7 +52,7 @@ const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..')
// printed: "checked against public/doxa-maps/" looked identical whether the name came from the // printed: "checked against public/doxa-maps/" looked identical whether the name came from the
// config or from the fallback. Printing WHICH means a future regression shows up in normal output // config or from the fallback. Printing WHICH means a future regression shows up in normal output
// instead of waiting for a rename to expose it. // instead of waiting for a rename to expose it.
let treeName = 'doxa-maps' let treeName = TREE_NAME
let treeNameSource = 'built-in default (build config unreadable)' let treeNameSource = 'built-in default (build config unreadable)'
try { try {
const cfg = JSON.parse(readFileSync(join(ROOT, 'vite.doxa-maps-build-config.json'), 'utf8')) const cfg = JSON.parse(readFileSync(join(ROOT, 'vite.doxa-maps-build-config.json'), 'utf8'))

View file

@ -43,6 +43,7 @@ import { createServer } from 'node:http'
import { readFileSync, readdirSync, existsSync } from 'node:fs' import { readFileSync, readdirSync, existsSync } from 'node:fs'
import { resolve, dirname, join, basename } from 'node:path' import { resolve, dirname, join, basename } from 'node:path'
import { fileURLToPath } from 'node:url' import { fileURLToPath } from 'node:url'
import { TREE_NAME } from './_tree-name.mjs'
const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..') const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..')
const PRESET_DIR = join(ROOT, 'internal', 'element-census') const PRESET_DIR = join(ROOT, 'internal', 'element-census')
@ -94,7 +95,7 @@ const port = await new Promise((ok) => { const s = createServer().listen(0, '127
const srv = spawn(process.execPath, [resolve(ROOT, 'internal/serve-strict.mjs'), '--port', String(port), '--directory', DIR], { stdio: 'ignore' }) const srv = spawn(process.execPath, [resolve(ROOT, 'internal/serve-strict.mjs'), '--port', String(port), '--directory', DIR], { stdio: 'ignore' })
const stopSrv = () => { try { srv.kill() } catch { /* already gone */ } } const stopSrv = () => { try { srv.kill() } catch { /* already gone */ } }
let up = false let up = false
for (let i = 0; i < 40 && !up; i++) { await new Promise((r) => setTimeout(r, 150)); up = await fetch(`http://127.0.0.1:${port}/doxa-maps/manifest.json`).then((r) => r.ok).catch(() => false) } for (let i = 0; i < 40 && !up; i++) { await new Promise((r) => setTimeout(r, 150)); up = await fetch(`http://127.0.0.1:${port}/${TREE_NAME}/manifest.json`).then((r) => r.ok).catch(() => false) }
if (!up) { stopSrv(); console.log(`ELEMENT-CENSUS: NOT MEASURED — could not serve ${DIR}`); process.exit(NOT_MEASURED) } if (!up) { stopSrv(); console.log(`ELEMENT-CENSUS: NOT MEASURED — could not serve ${DIR}`); process.exit(NOT_MEASURED) }
const THIRD_PARTY = /mapbox|openstreetmap|improve this map|maxar|geoboundaries|©/i const THIRD_PARTY = /mapbox|openstreetmap|improve this map|maxar|geoboundaries|©/i
@ -141,7 +142,7 @@ for (const [name, p] of presets) {
const oursByLeaf = new Map() const oursByLeaf = new Map()
let negSeenInOursOnly = null let negSeenInOursOnly = null
for (const pg of p.pages) { for (const pg of p.pages) {
const ours = await read(`http://127.0.0.1:${port}/doxa-maps/${pg.leaf}`) const ours = await read(`http://127.0.0.1:${port}/${TREE_NAME}/${pg.leaf}`)
oursByLeaf.set(pg.leaf, ours) oursByLeaf.set(pg.leaf, ours)
const theirs = await read(pg.live) const theirs = await read(pg.live)
const needH1 = p.requireH1 !== false const needH1 = p.requireH1 !== false

View file

@ -36,6 +36,7 @@
*/ */
import { resolve, dirname } from 'node:path' import { resolve, dirname } from 'node:path'
import { fileURLToPath, pathToFileURL } from 'node:url' import { fileURLToPath, pathToFileURL } from 'node:url'
import { TREE_NAME } from './_tree-name.mjs'
const HERE = dirname(fileURLToPath(import.meta.url)) const HERE = dirname(fileURLToPath(import.meta.url))
const REPO = process.env.DOXA_REPO || resolve(HERE, '..') const REPO = process.env.DOXA_REPO || resolve(HERE, '..')
@ -53,10 +54,10 @@ try {
// The leaf's own arithmetic is the oracle — computed here rather than hardcoded, so the // The leaf's own arithmetic is the oracle — computed here rather than hardcoded, so the
// expectation cannot drift from what the generated HTML actually means. // expectation cannot drift from what the generated HTML actually means.
const TREES = [ const TREES = [
['live catalog', 'https://nuxtbase.app/plugin-assets/doxa-maps'], ['live catalog', `https://nuxtbase.app/plugin-assets/${TREE_NAME}`],
['dev tree', 'http://127.0.0.1:4173/doxa-maps'], ['dev tree', `http://127.0.0.1:4173/${TREE_NAME}`],
['doxa.life drop-in', 'https://doxa.life/js/doxa-maps-build/doxa-maps'], ['doxa.life drop-in', `https://doxa.life/js/doxa-maps-build/${TREE_NAME}`],
['deep subpath', 'https://cdn.example.org/a/b/c/doxa-maps'], ['deep subpath', `https://cdn.example.org/a/b/c/${TREE_NAME}`],
] ]
const fails = [] const fails = []

View file

@ -33,12 +33,13 @@ import { writeFileSync, mkdtempSync, readFileSync } from 'node:fs'
import { resolve, dirname } from 'node:path' import { resolve, dirname } from 'node:path'
import { tmpdir } from 'node:os' import { tmpdir } from 'node:os'
import { fileURLToPath } from 'node:url' import { fileURLToPath } from 'node:url'
import { TREE_NAME } from './_tree-name.mjs'
const HERE = dirname(fileURLToPath(import.meta.url)) const HERE = dirname(fileURLToPath(import.meta.url))
const REPO = resolve(HERE, '..') const REPO = resolve(HERE, '..')
const PUBLIC = resolve(REPO, 'public') const PUBLIC = resolve(REPO, 'public')
const LEAF = 'doxa-maps/gospel-resources/resources-map/index.html' const LEAF = `${TREE_NAME}/gospel-resources/resources-map/index.html`
// EMBED_BASEMAP_PORT pins the port — used to prove the identity guard below (occupy it with another // EMBED_BASEMAP_PORT pins the port — used to prove the identity guard below (occupy it with another
// tree's server first; the gate must refuse rather than measure that tree). // tree's server first; the gate must refuse rather than measure that tree).
const leafPort = Number(process.env.EMBED_BASEMAP_PORT) || (5810 + Math.floor(Math.random() * 60)) const leafPort = Number(process.env.EMBED_BASEMAP_PORT) || (5810 + Math.floor(Math.random() * 60))

View file

@ -25,13 +25,14 @@ import { createServer } from 'node:http'
import { resolve, dirname } from 'node:path' import { resolve, dirname } from 'node:path'
import { fileURLToPath } from 'node:url' import { fileURLToPath } from 'node:url'
import { servesThisTree } from './_serves-this-tree.mjs' import { servesThisTree } from './_serves-this-tree.mjs'
import { TREE_NAME } from './_tree-name.mjs'
const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..') const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..')
const BASE = process.argv[2] || process.env.BASE_URL || 'http://127.0.0.1:4173' const BASE = process.argv[2] || process.env.BASE_URL || 'http://127.0.0.1:4173'
const HOST_PORT = Number(process.env.RTL_HOST_PORT || 47393) const HOST_PORT = Number(process.env.RTL_HOST_PORT || 47393)
let reachable = false let reachable = false
try { const r = await fetch(`${BASE}/doxa-maps/manifest.json`, { signal: AbortSignal.timeout(4000) }); reachable = r.ok } catch { /* precondition */ } try { const r = await fetch(`${BASE}/${TREE_NAME}/manifest.json`, { signal: AbortSignal.timeout(4000) }); reachable = r.ok } catch { /* precondition */ }
if (!reachable) { console.log(`EMBEDDED-RTL: SKIP — nothing served at ${BASE}.`); process.exit(0) } if (!reachable) { console.log(`EMBEDDED-RTL: SKIP — nothing served at ${BASE}.`); process.exit(0) }
const treeCheck = await servesThisTree(BASE, ROOT) const treeCheck = await servesThisTree(BASE, ROOT)
if (!treeCheck.ok) { console.error(`EMBEDDED-RTL: NOT MEASURED — ${treeCheck.why}`); process.exit(2) } if (!treeCheck.ok) { console.error(`EMBEDDED-RTL: NOT MEASURED — ${treeCheck.why}`); process.exit(2) }
@ -52,11 +53,11 @@ const hostHtml = (c) => `<!DOCTYPE html>
renders zero cards and the locale has nothing to translate. The first run of this gate renders zero cards and the locale has nothing to translate. The first run of this gate
omitted it and reported "never mounted" — a harness gap, not the RTL defect. --> omitted it and reported "never mounted" — a harness gap, not the RTL defect. -->
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";<\/script> <script>window.MAP_APP_API_URL = "https://pray.doxa.life";<\/script>
<script src="${TREE}/doxa-maps/${c.id}/${c.id}.js" crossorigin="anonymous"><\/script> <script src="${TREE}/${TREE_NAME}/${c.id}/${c.id}.js" crossorigin="anonymous"><\/script>
<script> <script>
var el = document.createElement(${JSON.stringify(c.tag)}); var el = document.createElement(${JSON.stringify(c.tag)});
el.setAttribute('profile-config', JSON.stringify({ profile: ${JSON.stringify(c.profile)}, locale: 'ar', el.setAttribute('profile-config', JSON.stringify({ profile: ${JSON.stringify(c.profile)}, locale: 'ar',
assetsBase: ${JSON.stringify(TREE + '/doxa-maps/assets')} })); assetsBase: ${JSON.stringify(TREE + `/${TREE_NAME}/assets`)} }));
el.style.cssText = 'display:block;width:100%;height:900px'; el.style.cssText = 'display:block;width:100%;height:900px';
document.getElementById('w').appendChild(el); document.getElementById('w').appendChild(el);
<\/script></body></html>` <\/script></body></html>`

View file

@ -47,6 +47,7 @@ import { fileURLToPath } from 'node:url'
import { spawn } from 'node:child_process' import { spawn } from 'node:child_process'
import { createServer } from 'node:net' import { createServer } from 'node:net'
import { createServer as createHttpServer } from 'node:http' import { createServer as createHttpServer } from 'node:http'
import { TREE_NAME } from './_tree-name.mjs'
const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..') const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..')
const argv = process.argv.slice(2) const argv = process.argv.slice(2)
@ -60,7 +61,7 @@ const SELF_TEST = !argv.includes('--no-self-test')
const WAIT_MS = Number(process.env.SHELF_WAIT_MS || 75000) const WAIT_MS = Number(process.env.SHELF_WAIT_MS || 75000)
const { chromium } = await import(resolve(ROOT, 'node_modules/playwright/index.mjs')) const { chromium } = await import(resolve(ROOT, 'node_modules/playwright/index.mjs'))
const manifest = JSON.parse(readFileSync(resolve(ROOT, 'public/doxa-maps/manifest.json'), 'utf8')) const manifest = JSON.parse(readFileSync(resolve(ROOT, `public/${TREE_NAME}/manifest.json`), 'utf8'))
const spa = readFileSync(resolve(ROOT, 'plugin-designs/doxa-maps-spa/doxa-maps-spa.vue'), 'utf8') const spa = readFileSync(resolve(ROOT, 'plugin-designs/doxa-maps-spa/doxa-maps-spa.vue'), 'utf8')
const cfg = JSON.parse(readFileSync(resolve(ROOT, 'vite.doxa-maps-build-config.json'), 'utf8')) const cfg = JSON.parse(readFileSync(resolve(ROOT, 'vite.doxa-maps-build-config.json'), 'utf8'))
const TK = process.env.MAPBOX_TOKEN || cfg.token || '' const TK = process.env.MAPBOX_TOKEN || cfg.token || ''
@ -95,7 +96,7 @@ const port = await new Promise(ok => { const s = createServer().listen(0, '127.0
const srv = spawn(process.execPath, [resolve(ROOT, 'internal/serve-strict.mjs'), '--port', String(port), '--directory', 'public'], { cwd: ROOT, stdio: 'ignore' }) const srv = spawn(process.execPath, [resolve(ROOT, 'internal/serve-strict.mjs'), '--port', String(port), '--directory', 'public'], { cwd: ROOT, stdio: 'ignore' })
const reap = () => { try { srv.kill() } catch (_) {} } const reap = () => { try { srv.kill() } catch (_) {} }
process.on('exit', reap); for (const s of ['SIGINT', 'SIGTERM']) process.on(s, () => { reap(); process.exit(130) }) process.on('exit', reap); for (const s of ['SIGINT', 'SIGTERM']) process.on(s, () => { reap(); process.exit(130) })
const BASE = `http://127.0.0.1:${port}/doxa-maps` const BASE = `http://127.0.0.1:${port}/${TREE_NAME}`
for (let i = 0; i < 40; i++) { try { if ((await fetch(BASE + '/manifest.json')).ok) break } catch (_) {} await new Promise(r => setTimeout(r, 250)) } for (let i = 0; i < 40; i++) { try { if ((await fetch(BASE + '/manifest.json')).ok) break } catch (_) {} await new Promise(r => setTimeout(r, 250)) }
// ── the PARTNER: a host site on a different origin (localhost vs 127.0.0.1) ──────────────── // ── the PARTNER: a host site on a different origin (localhost vs 127.0.0.1) ────────────────

View file

@ -40,9 +40,10 @@ import { dirname, resolve } from 'node:path'
import { fileURLToPath } from 'node:url' import { fileURLToPath } from 'node:url'
import { chromium } from 'playwright' import { chromium } from 'playwright'
import { servesThisTree } from './_serves-this-tree.mjs' import { servesThisTree } from './_serves-this-tree.mjs'
import { TREE_NAME } from './_tree-name.mjs'
const REPO = resolve(dirname(fileURLToPath(import.meta.url)), '..') const REPO = resolve(dirname(fileURLToPath(import.meta.url)), '..')
const BASE = process.env.BASE_URL || 'http://127.0.0.1:4173/doxa-maps' const BASE = process.env.BASE_URL || `http://127.0.0.1:4173/${TREE_NAME}`
// ⚠ "ANSWERS 200" IS NOT "IS THIS BUILD". This machine runs several worktrees of one repo and // ⚠ "ANSWERS 200" IS NOT "IS THIS BUILD". This machine runs several worktrees of one repo and
// the default port belongs to whichever server started first — measured 2026-09-24: :4173 was // the default port belongs to whichever server started first — measured 2026-09-24: :4173 was

View file

@ -63,6 +63,7 @@ import { spawn, execFileSync } from 'node:child_process'
import { fileURLToPath } from 'node:url' import { fileURLToPath } from 'node:url'
import { dirname, resolve } from 'node:path' import { dirname, resolve } from 'node:path'
import { readFileSync, existsSync } from 'node:fs' import { readFileSync, existsSync } from 'node:fs'
import { TREE_NAME } from './_tree-name.mjs'
const __dirname = fileURLToPath(new URL('.', import.meta.url)) const __dirname = fileURLToPath(new URL('.', import.meta.url))
const ROOT = resolve(__dirname, '..') const ROOT = resolve(__dirname, '..')
@ -109,7 +110,7 @@ if (!apiReachable) {
} }
// ── 1. MANIFEST-DERIVED TARGETS (never hardcode which bundle is "the" cap-5 shell) ──────────── // ── 1. MANIFEST-DERIVED TARGETS (never hardcode which bundle is "the" cap-5 shell) ────────────
const MANIFEST = resolve(ROOT, 'public', 'doxa-maps', 'manifest.json') const MANIFEST = resolve(ROOT, 'public', TREE_NAME, 'manifest.json')
if (!existsSync(MANIFEST)) { if (!existsSync(MANIFEST)) {
console.log('FITS-THE-SCREEN: SKIP — no manifest.json (nothing built).') console.log('FITS-THE-SCREEN: SKIP — no manifest.json (nothing built).')
process.exit(0) process.exit(0)
@ -183,7 +184,7 @@ const browser = await chromium.launch({ args: GL_ARGS })
async function runComposite() { async function runComposite() {
const TARGET = 'dashboard-plus-grid' const TARGET = 'dashboard-plus-grid'
const page = await browser.newPage({ viewport: { width: 1440, height: 900 } }) const page = await browser.newPage({ viewport: { width: 1440, height: 900 } })
const url = `${BASE}/doxa-maps/dashboard-plus-grid/dashboard-plus-grid/index.html` const url = `${BASE}/${TREE_NAME}/dashboard-plus-grid/dashboard-plus-grid/index.html`
await page.goto(url, { waitUntil: 'load', timeout: 60000 }).catch((e) => { throw new Error(`cannot reach ${TARGET}: ${e.message}`) }) await page.goto(url, { waitUntil: 'load', timeout: 60000 }).catch((e) => { throw new Error(`cannot reach ${TARGET}: ${e.message}`) })
await page.waitForTimeout(6000) await page.waitForTimeout(6000)
await reinject(page) await reinject(page)
@ -613,7 +614,7 @@ async function runShellBestEffort() {
return return
} }
for (const shell of shellLeaves) { for (const shell of shellLeaves) {
const url = shell.iframeUrl ? `${BASE}/doxa-maps/${shell.iframeUrl.replace(/^\.\//, '')}` : null const url = shell.iframeUrl ? `${BASE}/${TREE_NAME}/${shell.iframeUrl.replace(/^\.\//, '')}` : null
if (!url) continue if (!url) continue
const page = await browser.newPage({ viewport: { width: 1440, height: 900 } }) const page = await browser.newPage({ viewport: { width: 1440, height: 900 } })
try { try {
@ -651,8 +652,8 @@ async function runShellBestEffort() {
let runError = null let runError = null
try { try {
await runComposite() await runComposite()
await runResponsiveSweep(`${BASE}/doxa-maps/dashboard-plus-grid/dashboard-plus-grid/index.html`, 'dashboard-plus-grid') await runResponsiveSweep(`${BASE}/${TREE_NAME}/dashboard-plus-grid/dashboard-plus-grid/index.html`, 'dashboard-plus-grid')
await runResponsiveSweep(`${BASE}/doxa-maps/research-dashboard/research-dashboard/index.html`, 'research-dashboard') await runResponsiveSweep(`${BASE}/${TREE_NAME}/research-dashboard/research-dashboard/index.html`, 'research-dashboard')
await runShellBestEffort() await runShellBestEffort()
} catch (e) { } catch (e) {
runError = e runError = e

View file

@ -37,8 +37,9 @@ import { chromium } from 'playwright'
import { requireThisTree } from './_serves-this-tree.mjs' import { requireThisTree } from './_serves-this-tree.mjs'
import { resolve as _resolvePath, dirname as _dirnamePath } from 'node:path' import { resolve as _resolvePath, dirname as _dirnamePath } from 'node:path'
import { fileURLToPath as _fileURLToPath } from 'node:url' import { fileURLToPath as _fileURLToPath } from 'node:url'
import { TREE_NAME } from './_tree-name.mjs'
const BASE = process.env.BASE || 'http://127.0.0.1:4173/doxa-maps' const BASE = process.env.BASE || `http://127.0.0.1:4173/${TREE_NAME}`
const ROOT = _resolvePath(_dirnamePath(_fileURLToPath(import.meta.url)), '..') const ROOT = _resolvePath(_dirnamePath(_fileURLToPath(import.meta.url)), '..')
// ★ MEASURE THIS BUILD, OR MEASURE NOTHING. Several worktrees run on one machine and a default port // ★ MEASURE THIS BUILD, OR MEASURE NOTHING. Several worktrees run on one machine and a default port

View file

@ -30,6 +30,7 @@ import { serveReaped } from './_serve-reaped.mjs'
import { resolve, dirname } from 'node:path' import { resolve, dirname } from 'node:path'
import { fileURLToPath } from 'node:url' import { fileURLToPath } from 'node:url'
import { requireThisTree } from './_serves-this-tree.mjs' import { requireThisTree } from './_serves-this-tree.mjs'
import { TREE_NAME } from './_tree-name.mjs'
const HERE = dirname(fileURLToPath(import.meta.url)) const HERE = dirname(fileURLToPath(import.meta.url))
const PUBLIC = resolve(HERE, '../public') const PUBLIC = resolve(HERE, '../public')
@ -43,7 +44,7 @@ const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..')
// tree is a precondition, not a product defect, and the wave runner reports 75 as ⊘ rather than ✗. // tree is a precondition, not a product defect, and the wave runner reports 75 as ⊘ rather than ✗.
// A file:// target passes straight through — there is no server there to be wrong about. // A file:// target passes straight through — there is no server there to be wrong about.
await requireThisTree(BASE, ROOT, 'GOSPEL-COMPOSED-FILTERS') await requireThisTree(BASE, ROOT, 'GOSPEL-COMPOSED-FILTERS')
const PATHNAME = '/doxa-maps/gospel-resources/gospel-dashboard/index.html' const PATHNAME = `/${TREE_NAME}/gospel-resources/gospel-dashboard/index.html`
let srv = null let srv = null
if (!process.env.BASE_URL) srv = serveReaped(PUBLIC, PORT) if (!process.env.BASE_URL) srv = serveReaped(PUBLIC, PORT)

View file

@ -27,6 +27,7 @@ import { readFileSync } from 'node:fs'
import { resolve, dirname } from 'node:path' import { resolve, dirname } from 'node:path'
import { fileURLToPath } from 'node:url' import { fileURLToPath } from 'node:url'
import { serveReaped } from './_serve-reaped.mjs' import { serveReaped } from './_serve-reaped.mjs'
import { TREE_NAME } from './_tree-name.mjs'
const HERE = dirname(fileURLToPath(import.meta.url)) const HERE = dirname(fileURLToPath(import.meta.url))
const REPO = resolve(HERE, '..') const REPO = resolve(HERE, '..')
@ -53,7 +54,7 @@ try {
for (const lc of LOCALES) { for (const lc of LOCALES) {
const loc = Object.fromEntries(flat(cat(lc))) const loc = Object.fromEntries(flat(cat(lc)))
const page = await browser.newPage({ viewport: { width: 1440, height: 1000 } }) const page = await browser.newPage({ viewport: { width: 1440, height: 1000 } })
await page.goto(BASE + '/doxa-maps/gospel-resources/' + leaf + '/index.html?lang=' + lc, await page.goto(BASE + `/${TREE_NAME}/gospel-resources/` + leaf + '/index.html?lang=' + lc,
{ waitUntil: 'networkidle', timeout: 90000 }) { waitUntil: 'networkidle', timeout: 90000 })
await page.waitForTimeout(8000) await page.waitForTimeout(8000)
/* ═══ READ ONLY THE CHROME, NOT THE DECLARED-ENGLISH PROSE ═══════════════════════ /* ═══ READ ONLY THE CHROME, NOT THE DECLARED-ENGLISH PROSE ═══════════════════════

View file

@ -31,6 +31,7 @@ import { chromium } from 'playwright'
import { serveReaped } from './_serve-reaped.mjs' import { serveReaped } from './_serve-reaped.mjs'
import { resolve, dirname } from 'node:path' import { resolve, dirname } from 'node:path'
import { fileURLToPath } from 'node:url' import { fileURLToPath } from 'node:url'
import { TREE_NAME } from './_tree-name.mjs'
const HERE = dirname(fileURLToPath(import.meta.url)) const HERE = dirname(fileURLToPath(import.meta.url))
const PUBLIC = resolve(HERE, '../public') const PUBLIC = resolve(HERE, '../public')
@ -80,7 +81,7 @@ try {
if (POISON) { if (POISON) {
/* A URL that serves no element at all must fail EVERY leaf-shaped assertion. */ /* A URL that serves no element at all must fail EVERY leaf-shaped assertion. */
const r = await probe(page, `${BASE}/doxa-maps/index.html`) const r = await probe(page, `${BASE}/${TREE_NAME}/index.html`)
const f = failuresFor('poison', r) const f = failuresFor('poison', r)
if (!f.length) { if (!f.length) {
console.error('LEAVES-MOUNT: SELF-TEST FAIL — a page with no gospel element passed as mounted.') console.error('LEAVES-MOUNT: SELF-TEST FAIL — a page with no gospel element passed as mounted.')
@ -93,7 +94,7 @@ try {
const bad = [] const bad = []
for (const leaf of LEAVES) { for (const leaf of LEAVES) {
const r = await probe(page, `${BASE}/doxa-maps/gospel-resources/${leaf}/index.html`) const r = await probe(page, `${BASE}/${TREE_NAME}/gospel-resources/${leaf}/index.html`)
const f = failuresFor(leaf, r) const f = failuresFor(leaf, r)
bad.push(...f) bad.push(...f)
console.log(` ${f.length ? 'FAIL' : 'ok '} ${leaf.padEnd(18)} defined=${r.defined} shadow=${r.hasShadow} children=${r.shadowChildren} text=${r.visibleTextLength}`) console.log(` ${f.length ? 'FAIL' : 'ok '} ${leaf.padEnd(18)} defined=${r.defined} shadow=${r.hasShadow} children=${r.shadowChildren} text=${r.visibleTextLength}`)

View file

@ -38,6 +38,7 @@ import { createServer } from 'node:http'
import { readFileSync, existsSync } from 'node:fs' import { readFileSync, existsSync } from 'node:fs'
import { resolve, dirname, join } from 'node:path' import { resolve, dirname, join } from 'node:path'
import { fileURLToPath } from 'node:url' import { fileURLToPath } from 'node:url'
import { TREE_NAME } from './_tree-name.mjs'
const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..') const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..')
const argv = process.argv.slice(2) const argv = process.argv.slice(2)
@ -48,14 +49,14 @@ const CONTROL = 'regions-dashboard/regions-list'
const MIN = 20 // Arabic characters a page must show under ?lang=ar before it can be judged const MIN = 20 // Arabic characters a page must show under ?lang=ar before it can be judged
const CONCURRENCY = 3 const CONCURRENCY = 3
const manifestPath = join(DIR, 'doxa-maps', 'manifest.json') const manifestPath = join(DIR, TREE_NAME, 'manifest.json')
if (!existsSync(manifestPath)) { console.log(`HOST-LOCALE: NOT MEASURED — no ${manifestPath}`); process.exit(75) } if (!existsSync(manifestPath)) { console.log(`HOST-LOCALE: NOT MEASURED — no ${manifestPath}`); process.exit(75) }
const manifest = JSON.parse(readFileSync(manifestPath, 'utf8')) const manifest = JSON.parse(readFileSync(manifestPath, 'utf8'))
const leaves = [] const leaves = []
for (const b of manifest.bundles || []) for (const p of b.profiles || []) { for (const b of manifest.bundles || []) for (const p of b.profiles || []) {
const id = `${b.bundle}/${p.profile}` const id = `${b.bundle}/${p.profile}`
if (ONLY && !ONLY.has(id) && id !== CONTROL) continue if (ONLY && !ONLY.has(id) && id !== CONTROL) continue
if (existsSync(join(DIR, 'doxa-maps', id, 'index.html'))) leaves.push(id) if (existsSync(join(DIR, TREE_NAME, id, 'index.html'))) leaves.push(id)
} }
if (!leaves.includes(CONTROL)) { console.log(`HOST-LOCALE: NOT MEASURED — the control ${CONTROL} is not in this tree`); process.exit(75) } if (!leaves.includes(CONTROL)) { console.log(`HOST-LOCALE: NOT MEASURED — the control ${CONTROL} is not in this tree`); process.exit(75) }
@ -63,7 +64,7 @@ const port = await new Promise((ok) => { const s = createServer().listen(0, '127
const srv = spawn(process.execPath, [resolve(ROOT, 'internal/serve-strict.mjs'), '--port', String(port), '--directory', DIR], { stdio: 'ignore' }) const srv = spawn(process.execPath, [resolve(ROOT, 'internal/serve-strict.mjs'), '--port', String(port), '--directory', DIR], { stdio: 'ignore' })
const stop = () => { try { srv.kill() } catch { /* already gone */ } } const stop = () => { try { srv.kill() } catch { /* already gone */ } }
let up = false let up = false
for (let i = 0; i < 40 && !up; i++) { await new Promise((r) => setTimeout(r, 150)); up = await fetch(`http://127.0.0.1:${port}/doxa-maps/manifest.json`).then((r) => r.ok).catch(() => false) } for (let i = 0; i < 40 && !up; i++) { await new Promise((r) => setTimeout(r, 150)); up = await fetch(`http://127.0.0.1:${port}/${TREE_NAME}/manifest.json`).then((r) => r.ok).catch(() => false) }
if (!up) { stop(); console.log(`HOST-LOCALE: NOT MEASURED — could not serve ${DIR}`); process.exit(75) } if (!up) { stop(); console.log(`HOST-LOCALE: NOT MEASURED — could not serve ${DIR}`); process.exit(75) }
const COUNT = `(() => { const COUNT = `(() => {
@ -85,7 +86,7 @@ const browser = await chromium.launch({ args: ['--use-gl=swiftshader', '--enable
async function measure(id, mode) { async function measure(id, mode) {
const ctx = await browser.newContext({ viewport: { width: 1280, height: 900 } }) const ctx = await browser.newContext({ viewport: { width: 1280, height: 900 } })
const page = await ctx.newPage() const page = await ctx.newPage()
const url = `http://127.0.0.1:${port}/doxa-maps/${id}/index.html${mode === 'ref' ? '?lang=ar' : ''}` const url = `http://127.0.0.1:${port}/${TREE_NAME}/${id}/index.html${mode === 'ref' ? '?lang=ar' : ''}`
let injected = mode === 'ref' let injected = mode === 'ref'
if (mode === 'host') { if (mode === 'host') {
await page.route(url, async (route) => { await page.route(url, async (route) => {

View file

@ -31,9 +31,10 @@ import { chromium } from 'playwright'
import { dirname, resolve } from 'node:path' import { dirname, resolve } from 'node:path'
import { fileURLToPath } from 'node:url' import { fileURLToPath } from 'node:url'
import { requireThisTree } from './_serves-this-tree.mjs' import { requireThisTree } from './_serves-this-tree.mjs'
import { TREE_NAME } from './_tree-name.mjs'
const DEFAULT_LEAF = (process.env.BASE_URL || 'http://127.0.0.1:4173') const DEFAULT_LEAF = (process.env.BASE_URL || 'http://127.0.0.1:4173')
+ '/doxa-maps/my-upg-100-list/upg-100-list/index.html' + `/${TREE_NAME}/my-upg-100-list/upg-100-list/index.html`
const LEAF = process.argv[2] || DEFAULT_LEAF const LEAF = process.argv[2] || DEFAULT_LEAF
let reachable = false let reachable = false

View file

@ -34,6 +34,7 @@ import { join, dirname } from 'node:path'
import { fileURLToPath } from 'node:url' import { fileURLToPath } from 'node:url'
import { requireThisTree } from './_serves-this-tree.mjs' import { requireThisTree } from './_serves-this-tree.mjs'
import { resolve as _resolvePath, dirname as _dirnamePath } from 'node:path' import { resolve as _resolvePath, dirname as _dirnamePath } from 'node:path'
import { TREE_NAME } from './_tree-name.mjs'
const REPO = join(dirname(fileURLToPath(import.meta.url)), '..') const REPO = join(dirname(fileURLToPath(import.meta.url)), '..')
const LOCALES = join(REPO, 'library/i18n/locales') const LOCALES = join(REPO, 'library/i18n/locales')
@ -49,7 +50,7 @@ const ROOT = _resolvePath(_dirnamePath(_fileURLToPath(import.meta.url)), '..')
await requireThisTree(URL_, ROOT, 'LEGEND-INFO-LOCALIZED') await requireThisTree(URL_, ROOT, 'LEGEND-INFO-LOCALIZED')
if (!URL_) { if (!URL_) {
console.log('LEGEND-INFO-LOCALIZED: SKIP — no leaf url given.') console.log('LEGEND-INFO-LOCALIZED: SKIP — no leaf url given.')
console.log(' Pass one, or set LEGEND_URL. Serve the TREE ROOT (public/doxa-maps) and point') console.log(` Pass one, or set LEGEND_URL. Serve the TREE ROOT (public/${TREE_NAME}) and point`)
console.log(' at a profile whose legend carries per-tab info buttons, e.g.') console.log(' at a profile whose legend carries per-tab info buttons, e.g.')
console.log(' my-upg-100-list/upg-100-list/index.html') console.log(' my-upg-100-list/upg-100-list/index.html')
process.exit(0) process.exit(0)

View file

@ -17,6 +17,7 @@
import { readFileSync, readdirSync, existsSync } from 'node:fs' import { readFileSync, readdirSync, existsSync } from 'node:fs'
import { resolve, join, dirname } from 'node:path' import { resolve, join, dirname } from 'node:path'
import { fileURLToPath } from 'node:url' import { fileURLToPath } from 'node:url'
import { TREE_NAME } from './_tree-name.mjs'
const REPO = process.env.DOXA_REPO const REPO = process.env.DOXA_REPO
// FOURTH instance of this pattern in one flight (check-zone-census, check-share-policy, // FOURTH instance of this pattern in one flight (check-zone-census, check-share-policy,
@ -95,7 +96,7 @@ if (unreachable.length) {
} }
// The host-page path must survive: leaf pages still declare a lang. // The host-page path must survive: leaf pages still declare a lang.
const leafRoot = join(REPO, 'public/doxa-maps') const leafRoot = join(REPO, `public/${TREE_NAME}`)
let leaves = 0 let leaves = 0
let withLang = 0 let withLang = 0
if (existsSync(leafRoot)) { if (existsSync(leafRoot)) {

View file

@ -35,13 +35,14 @@ import { readFileSync, existsSync } from 'node:fs'
import { resolve, dirname, join } from 'node:path' import { resolve, dirname, join } from 'node:path'
import { fileURLToPath } from 'node:url' import { fileURLToPath } from 'node:url'
import { discoverSpaRegistry } from './plugin-gallery/discover.mjs' import { discoverSpaRegistry } from './plugin-gallery/discover.mjs'
import { TREE_NAME } from './_tree-name.mjs'
const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..') const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..')
const MANIFEST = join(ROOT, 'public', 'doxa-maps', 'manifest.json') const MANIFEST = join(ROOT, 'public', TREE_NAME, 'manifest.json')
const MIN_BUNDLES = Number(process.env.MIN_BUNDLES || 10) const MIN_BUNDLES = Number(process.env.MIN_BUNDLES || 10)
if (!existsSync(MANIFEST)) { if (!existsSync(MANIFEST)) {
console.error('MANIFEST-COVERS: FAIL — public/doxa-maps/manifest.json does not exist.') console.error(`MANIFEST-COVERS: FAIL — public/${TREE_NAME}/manifest.json does not exist.`)
console.error(' Run `bun run build` first; there is nothing to compare against.') console.error(' Run `bun run build` first; there is nothing to compare against.')
process.exit(1) process.exit(1)
} }

View file

@ -26,6 +26,7 @@ import { createServer } from 'node:http'
import { readFileSync, existsSync } from 'node:fs' import { readFileSync, existsSync } from 'node:fs'
import { join, dirname, resolve } from 'node:path' import { join, dirname, resolve } from 'node:path'
import { fileURLToPath } from 'node:url' import { fileURLToPath } from 'node:url'
import { TREE_NAME } from './_tree-name.mjs'
const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..') const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..')
const argv = process.argv.slice(2) const argv = process.argv.slice(2)
@ -35,14 +36,14 @@ const DEFAULT = JSON.parse(readFileSync(join(ROOT, 'plugin-designs/doxa-maps-spa
const ONLY = (argOf('--only', '') ? argOf('--only', '').split(',') : DEFAULT).map((s) => s.trim()).filter(Boolean) const ONLY = (argOf('--only', '') ? argOf('--only', '').split(',') : DEFAULT).map((s) => s.trim()).filter(Boolean)
const CELLS = argOf('--cell', '') ? [argOf('--cell', '').toUpperCase()] : ['B', 'C'] const CELLS = argOf('--cell', '') ? [argOf('--cell', '').toUpperCase()] : ['B', 'C']
const MANIFEST = join(ROOT, 'public', 'doxa-maps', 'manifest.json') const MANIFEST = join(ROOT, 'public', TREE_NAME, 'manifest.json')
if (!existsSync(MANIFEST)) { console.log('MAP-EMBED-CELLS: SKIP — no built tree'); process.exit(2) } if (!existsSync(MANIFEST)) { console.log('MAP-EMBED-CELLS: SKIP — no built tree'); process.exit(2) }
const manifest = JSON.parse(readFileSync(MANIFEST, 'utf8')) const manifest = JSON.parse(readFileSync(MANIFEST, 'utf8'))
const bakedToken = (() => { const bakedToken = (() => {
for (const b of manifest.bundles || []) for (const p of b.profiles || []) { for (const b of manifest.bundles || []) for (const p of b.profiles || []) {
if (!p.iframeUrl) continue if (!p.iframeUrl) continue
const leaf = join(ROOT, 'public', 'doxa-maps', String(p.iframeUrl).replace(/^\.\//, '')) const leaf = join(ROOT, 'public', TREE_NAME, String(p.iframeUrl).replace(/^\.\//, ''))
if (!existsSync(leaf)) continue if (!existsSync(leaf)) continue
const m = readFileSync(leaf, 'utf8').match(/BAKED_TOKEN\s*=\s*"(pk\.[^"]+)"/) const m = readFileSync(leaf, 'utf8').match(/BAKED_TOKEN\s*=\s*"(pk\.[^"]+)"/)
if (m) return m[1] if (m) return m[1]
@ -67,7 +68,7 @@ for (const ref of ONLY) {
const freePort = () => new Promise((ok) => { const s = createServer().listen(0, '127.0.0.1', () => { const port = s.address().port; s.close(() => ok(port)) }) }) const freePort = () => new Promise((ok) => { const s = createServer().listen(0, '127.0.0.1', () => { const port = s.address().port; s.close(() => ok(port)) }) })
const TREE_PORT = await freePort() const TREE_PORT = await freePort()
const HOST_PORT = await freePort() const HOST_PORT = await freePort()
const TREE = `http://127.0.0.1:${TREE_PORT}/doxa-maps` const TREE = `http://127.0.0.1:${TREE_PORT}/${TREE_NAME}`
let pageHtml = '<!doctype html><title>host</title><body>nothing</body>' let pageHtml = '<!doctype html><title>host</title><body>nothing</body>'
const tree = spawn(process.execPath, ['internal/serve-strict.mjs', '--port', String(TREE_PORT)], { cwd: ROOT, stdio: 'ignore' }) const tree = spawn(process.execPath, ['internal/serve-strict.mjs', '--port', String(TREE_PORT)], { cwd: ROOT, stdio: 'ignore' })
const host = createServer((_, r) => { r.writeHead(200, { 'content-type': 'text/html; charset=utf-8' }); r.end(pageHtml) }).listen(HOST_PORT, 'localhost') const host = createServer((_, r) => { r.writeHead(200, { 'content-type': 'text/html; charset=utf-8' }); r.end(pageHtml) }).listen(HOST_PORT, 'localhost')

View file

@ -41,11 +41,12 @@ import { readFileSync, existsSync } from 'node:fs'
import { resolve, join, dirname } from 'node:path' import { resolve, join, dirname } from 'node:path'
import { fileURLToPath } from 'node:url' import { fileURLToPath } from 'node:url'
import { servesThisTree } from './_serves-this-tree.mjs' import { servesThisTree } from './_serves-this-tree.mjs'
import { TREE_NAME } from './_tree-name.mjs'
// Portable by construction — see check-no-absolute-paths.mjs. Never hardcode a worktree. // Portable by construction — see check-no-absolute-paths.mjs. Never hardcode a worktree.
const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..') const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..')
const BASE = String(process.env.BASE_URL || 'http://127.0.0.1:4173').replace(/\/+$/, '') const BASE = String(process.env.BASE_URL || 'http://127.0.0.1:4173').replace(/\/+$/, '')
const MANIFEST = join(ROOT, 'public', 'doxa-maps', 'manifest.json') const MANIFEST = join(ROOT, 'public', TREE_NAME, 'manifest.json')
const DROP_RE = /unknown property|does not exist|is required|Expected|invalid/i const DROP_RE = /unknown property|does not exist|is required|Expected|invalid/i
@ -118,7 +119,7 @@ let unreachable = 0
for (const [bundle, profile] of leaves) { for (const [bundle, profile] of leaves) {
const page = await browser.newPage({ viewport: { width: 1100, height: 760 } }) const page = await browser.newPage({ viewport: { width: 1100, height: 760 } })
await page.addInitScript(INIT) await page.addInitScript(INIT)
const url = `${BASE}/doxa-maps/${bundle}/${profile}/index.html?provider=maplibre` const url = `${BASE}/${TREE_NAME}/${bundle}/${profile}/index.html?provider=maplibre`
try { try {
await page.goto(url, { waitUntil: 'networkidle', timeout: 60000 }) await page.goto(url, { waitUntil: 'networkidle', timeout: 60000 })
await page.waitForTimeout(9000) await page.waitForTimeout(9000)

View file

@ -38,10 +38,11 @@ import { resolve, dirname, join } from 'node:path'
import { fileURLToPath } from 'node:url' import { fileURLToPath } from 'node:url'
import { chromium } from 'playwright' import { chromium } from 'playwright'
import { requireThisTree } from './_serves-this-tree.mjs' import { requireThisTree } from './_serves-this-tree.mjs'
import { TREE_NAME } from './_tree-name.mjs'
const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..') const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..')
const MANIFEST = join(ROOT, 'public', 'doxa-maps', 'manifest.json') const MANIFEST = join(ROOT, 'public', TREE_NAME, 'manifest.json')
const BASE = process.env.BASE || 'http://127.0.0.1:4173/doxa-maps/' const BASE = process.env.BASE || `http://127.0.0.1:4173/${TREE_NAME}/`
// ★ MEASURE THIS BUILD, OR MEASURE NOTHING. Several worktrees of this repo run on one machine and // ★ MEASURE THIS BUILD, OR MEASURE NOTHING. Several worktrees of this repo run on one machine and
// a default port belongs to whichever server started first — measured 2026-09-24, :4173 was another // a default port belongs to whichever server started first — measured 2026-09-24, :4173 was another

View file

@ -37,6 +37,7 @@
import { readFileSync, existsSync } from 'node:fs' import { readFileSync, existsSync } from 'node:fs'
import { resolve, dirname, join } from 'node:path' import { resolve, dirname, join } from 'node:path'
import { fileURLToPath } from 'node:url' import { fileURLToPath } from 'node:url'
import { TREE_NAME } from './_tree-name.mjs'
const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..') const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..')
@ -46,7 +47,7 @@ const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..')
// correct only because the default happens to equal the config's current name. A dead read is // correct only because the default happens to equal the config's current name. A dead read is
// INVISIBLE when only the result is printed, so the source is now part of the output: a future // INVISIBLE when only the result is printed, so the source is now part of the output: a future
// regression shows up on every run instead of waiting for a tree rename to expose it. // regression shows up on every run instead of waiting for a tree rename to expose it.
let treeName = 'doxa-maps' let treeName = TREE_NAME
let treeNameSource = 'built-in default (build config unreadable)' let treeNameSource = 'built-in default (build config unreadable)'
try { try {
const cfg = JSON.parse(readFileSync(join(ROOT, 'vite.doxa-maps-build-config.json'), 'utf8')) const cfg = JSON.parse(readFileSync(join(ROOT, 'vite.doxa-maps-build-config.json'), 'utf8'))

View file

@ -46,6 +46,7 @@ import { execSync } from 'node:child_process'
import { readFileSync, existsSync } from 'node:fs' import { readFileSync, existsSync } from 'node:fs'
import { resolve, dirname, join } from 'node:path' import { resolve, dirname, join } from 'node:path'
import { fileURLToPath } from 'node:url' import { fileURLToPath } from 'node:url'
import { TREE_NAME } from './_tree-name.mjs'
const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..') const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..')
@ -128,7 +129,7 @@ if (hits.length) {
for (const h of hits) console.error(` ✗ ${h.rel}:${h.line} ${h.label}\n ${h.text}`) for (const h of hits) console.error(` ✗ ${h.rel}:${h.line} ${h.label}\n ${h.text}`)
console.error( console.error(
'\n This repo is handed to the Doxa team; an internal code name means nothing to them and is a\n' + '\n This repo is handed to the Doxa team; an internal code name means nothing to them and is a\n' +
' hand-off defect. Remove it, or rewrite the sentence with the product name — doxa-maps, or\n' + ` hand-off defect. Remove it, or rewrite the sentence with the product name — ${TREE_NAME}, or\n` +
' "the plugin studio". Do not invent a replacement brand. If the match is a unix path, use a\n' + ' "the plugin studio". Do not invent a replacement brand. If the match is a unix path, use a\n' +
' repo-relative path instead and check:no-abs-paths will stop reporting it too.' ' repo-relative path instead and check:no-abs-paths will stop reporting it too.'
) )

View file

@ -50,13 +50,14 @@
import { readFileSync, existsSync, readdirSync } from 'node:fs' import { readFileSync, existsSync, readdirSync } from 'node:fs'
import { resolve, dirname, join } from 'node:path' import { resolve, dirname, join } from 'node:path'
import { fileURLToPath } from 'node:url' import { fileURLToPath } from 'node:url'
import { TREE_NAME } from './_tree-name.mjs'
const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..') const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..')
const COLORS = join(ROOT, 'library/colors') const COLORS = join(ROOT, 'library/colors')
const BUILT = join(ROOT, 'public/doxa-maps') const BUILT = join(ROOT, `public/${TREE_NAME}`)
if (!existsSync(BUILT)) { if (!existsSync(BUILT)) {
console.error('ONE-PALETTE: SKIP — no built tree at public/doxa-maps. Run `bun run build` first.') console.error(`ONE-PALETTE: SKIP — no built tree at public/${TREE_NAME}. Run \`bun run build\` first.`)
console.error(' Reported as a SKIP with a non-silent message rather than a pass: a gate that') console.error(' Reported as a SKIP with a non-silent message rather than a pass: a gate that')
console.error(' reports success when it never ran is worse than no gate.') console.error(' reports success when it never ran is worse than no gate.')
process.exit(0) process.exit(0)

View file

@ -24,10 +24,11 @@ import { chromium } from 'playwright'
import { resolve, dirname } from 'node:path' import { resolve, dirname } from 'node:path'
import { fileURLToPath } from 'node:url' import { fileURLToPath } from 'node:url'
import { servesThisTree } from './_serves-this-tree.mjs' import { servesThisTree } from './_serves-this-tree.mjs'
import { TREE_NAME } from './_tree-name.mjs'
const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..') const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..')
const DEFAULT_LEAF = (process.env.BASE_URL || 'http://127.0.0.1:4173') const DEFAULT_LEAF = (process.env.BASE_URL || 'http://127.0.0.1:4173')
+ '/doxa-maps/my-upg-100-list/upg-100-mfe/index.html' + `/${TREE_NAME}/my-upg-100-list/upg-100-mfe/index.html`
const LEAF = process.argv[2] || DEFAULT_LEAF const LEAF = process.argv[2] || DEFAULT_LEAF
// ⚠ SWEEP WIDTHS. The report named 1440px, so this first guarded 1440px — but the defect is a // ⚠ SWEEP WIDTHS. The report named 1440px, so this first guarded 1440px — but the defect is a
// FLEX-SHRINK failure, and those are width-dependent: a narrower viewport gives the controls less // FLEX-SHRINK failure, and those are width-dependent: a narrower viewport gives the controls less
@ -79,7 +80,7 @@ if (!reachable) {
// so a caller can tell it apart from both a pass (0) and a real failure (1). // so a caller can tell it apart from both a pass (0) and a real failure (1).
// The helper wants the tree's BASE, not a leaf URL — it fetches <base>/doxa-maps/manifest.json. // The helper wants the tree's BASE, not a leaf URL — it fetches <base>/doxa-maps/manifest.json.
// LEAF is `<base>/doxa-maps/<bundle>/<profile>/index.html`, so cut at the /doxa-maps/ segment. // LEAF is `<base>/doxa-maps/<bundle>/<profile>/index.html`, so cut at the /doxa-maps/ segment.
const TREE_BASE = LEAF.split('/doxa-maps/')[0] const TREE_BASE = LEAF.split(`/${TREE_NAME}/`)[0]
const treeCheck = await servesThisTree(TREE_BASE, ROOT) const treeCheck = await servesThisTree(TREE_BASE, ROOT)
if (!treeCheck.ok) { if (!treeCheck.ok) {
console.error('POSTER-CARD-FITS: NOT MEASURED — the server is not serving this checkout.') console.error('POSTER-CARD-FITS: NOT MEASURED — the server is not serving this checkout.')

View file

@ -44,6 +44,7 @@ import { readFileSync } from 'node:fs'
import { resolve, dirname, join } from 'node:path' import { resolve, dirname, join } from 'node:path'
import { fileURLToPath } from 'node:url' import { fileURLToPath } from 'node:url'
import { servesThisTree } from './_serves-this-tree.mjs' import { servesThisTree } from './_serves-this-tree.mjs'
import { TREE_NAME } from './_tree-name.mjs'
const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..') const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..')
const BASE = process.argv[2] || process.env.BASE_URL || 'http://127.0.0.1:4173' const BASE = process.argv[2] || process.env.BASE_URL || 'http://127.0.0.1:4173'
@ -102,7 +103,7 @@ if (blind.length) {
} }
let reachable = false let reachable = false
try { const r = await fetch(`${BASE}/doxa-maps/manifest.json`, { signal: AbortSignal.timeout(4000) }); reachable = r.ok } catch { /* precondition */ } try { const r = await fetch(`${BASE}/${TREE_NAME}/manifest.json`, { signal: AbortSignal.timeout(4000) }); reachable = r.ok } catch { /* precondition */ }
if (!reachable) { if (!reachable) {
console.log(`POSTER-WALL-LOCALISED: SKIP — nothing served at ${BASE}.`) console.log(`POSTER-WALL-LOCALISED: SKIP — nothing served at ${BASE}.`)
console.log(' Serve the built tree: bun run serve (or set BASE_URL).') console.log(' Serve the built tree: bun run serve (or set BASE_URL).')
@ -127,7 +128,7 @@ for (const leaf of LEAVES) {
page.on('pageerror', (e) => errs.push(String(e).slice(0, 120))) page.on('pageerror', (e) => errs.push(String(e).slice(0, 120)))
let r = null let r = null
try { try {
const res = await page.goto(`${BASE}/doxa-maps/${leaf}/index.html?lang=${lang}`, const res = await page.goto(`${BASE}/${TREE_NAME}/${leaf}/index.html?lang=${lang}`,
{ waitUntil: 'load', timeout: 45000 }) { waitUntil: 'load', timeout: 45000 })
if (!res || !res.ok()) { rows.push({ leaf, lang, skipped: `HTTP ${res ? res.status() : '?'}` }); await page.close(); continue } if (!res || !res.ok()) { rows.push({ leaf, lang, skipped: `HTTP ${res ? res.status() : '?'}` }); await page.close(); continue }
await page.waitForTimeout(13000) await page.waitForTimeout(13000)

View file

@ -26,13 +26,14 @@ import { writeFileSync } from 'node:fs'
import { existsSync } from 'node:fs' import { existsSync } from 'node:fs'
import { resolve, dirname } from 'node:path' import { resolve, dirname } from 'node:path'
import { fileURLToPath } from 'node:url' import { fileURLToPath } from 'node:url'
import { TREE_NAME } from './_tree-name.mjs'
const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..') const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..')
if (!existsSync(resolve(ROOT, 'public/doxa-maps/prayer-walk-planner/prayer-walk-planner.js'))) { console.log('SKIP check-prayer-walk-planner: no built planner'); process.exit(2) } if (!existsSync(resolve(ROOT, `public/${TREE_NAME}/prayer-walk-planner/prayer-walk-planner.js`))) { console.log('SKIP check-prayer-walk-planner: no built planner'); process.exit(2) }
let chromium let chromium
try { ({ chromium } = await import('playwright')) } catch { console.log('SKIP check-prayer-walk-planner: playwright not installed'); process.exit(2) } try { ({ chromium } = await import('playwright')) } catch { console.log('SKIP check-prayer-walk-planner: playwright not installed'); process.exit(2) }
const OUT = process.env.OUT || '/tmp' const OUT = process.env.OUT || '/tmp'
const free = () => new Promise(ok => { const s = createServer().listen(0, '127.0.0.1', () => { const p = s.address().port; s.close(() => ok(p)) }) }) const free = () => new Promise(ok => { const s = createServer().listen(0, '127.0.0.1', () => { const p = s.address().port; s.close(() => ok(p)) }) })
const TP = await free(), HP = await free(), TREE = `http://127.0.0.1:${TP}/doxa-maps` const TP = await free(), HP = await free(), TREE = `http://127.0.0.1:${TP}/${TREE_NAME}`
const srv = spawn('node', ['internal/serve-strict.mjs', '--port', String(TP)], { cwd: ROOT, stdio: 'ignore' }) const srv = spawn('node', ['internal/serve-strict.mjs', '--port', String(TP)], { cwd: ROOT, stdio: 'ignore' })
const host = createServer((q, r) => { const l = new URL(q.url, 'http://x').searchParams.get('l') || 'en'; r.writeHead(200, { 'content-type': 'text/html' }); r.end(`<!doctype html><html lang="${l}"><body style="margin:0"><script src="${TREE}/doxa-maps-spa/doxa-maps-spa.js"></script><doxa-maps-spa tab="prayer-walk-planner" style="height:100vh"></doxa-maps-spa></body></html>`) }).listen(HP, 'localhost') const host = createServer((q, r) => { const l = new URL(q.url, 'http://x').searchParams.get('l') || 'en'; r.writeHead(200, { 'content-type': 'text/html' }); r.end(`<!doctype html><html lang="${l}"><body style="margin:0"><script src="${TREE}/doxa-maps-spa/doxa-maps-spa.js"></script><doxa-maps-spa tab="prayer-walk-planner" style="height:100vh"></doxa-maps-spa></body></html>`) }).listen(HP, 'localhost')
await new Promise(r => setTimeout(r, 800)) await new Promise(r => setTimeout(r, 800))
@ -44,10 +45,10 @@ await new Promise(r => setTimeout(r, 800))
// pass or a fail — the same code the shared browser locks use for "busy, re-run later". // pass or a fail — the same code the shared browser locks use for "busy, re-run later".
{ {
const { readFileSync } = await import('node:fs') const { readFileSync } = await import('node:fs')
const disk = readFileSync(resolve(ROOT, 'public/doxa-maps/manifest.json'), 'utf8') const disk = readFileSync(resolve(ROOT, `public/${TREE_NAME}/manifest.json`), 'utf8')
let served = '' let served = ''
for (let i = 0; i < 20 && !served; i++) { for (let i = 0; i < 20 && !served; i++) {
try { const r = await fetch(`http://127.0.0.1:${TP}/doxa-maps/manifest.json`); if (r.ok) served = await r.text() } catch { /* not up yet */ } try { const r = await fetch(`http://127.0.0.1:${TP}/${TREE_NAME}/manifest.json`); if (r.ok) served = await r.text() } catch { /* not up yet */ }
if (!served) await new Promise(r => setTimeout(r, 300)) if (!served) await new Promise(r => setTimeout(r, 300))
} }
if (served !== disk) { if (served !== disk) {

View file

@ -34,9 +34,10 @@
import { readFileSync, existsSync } from 'node:fs' import { readFileSync, existsSync } from 'node:fs'
import { resolve, dirname, join } from 'node:path' import { resolve, dirname, join } from 'node:path'
import { fileURLToPath } from 'node:url' import { fileURLToPath } from 'node:url'
import { TREE_NAME } from './_tree-name.mjs'
const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..') const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..')
const MANIFEST = join(ROOT, 'public', 'doxa-maps', 'manifest.json') const MANIFEST = join(ROOT, 'public', TREE_NAME, 'manifest.json')
// A description shorter than this is a placeholder, not an explanation. Calibrated against the // A description shorter than this is a placeholder, not an explanation. Calibrated against the
// REAL corpus rather than a guess (measured 2026-09-22): shortest genuine description 62 chars, // REAL corpus rather than a guess (measured 2026-09-22): shortest genuine description 62 chars,
@ -52,7 +53,7 @@ const MIN_CHARS = 25
const MIN_EXPECTED_PROFILES = 20 const MIN_EXPECTED_PROFILES = 20
if (!existsSync(MANIFEST)) { if (!existsSync(MANIFEST)) {
console.error('PROFILE-DESCRIPTIONS: FAIL — no manifest.json at public/doxa-maps/.') console.error(`PROFILE-DESCRIPTIONS: FAIL — no manifest.json at public/${TREE_NAME}/.`)
console.error(' This gate reads the BUILT manifest (the artifact consumers actually read).') console.error(' This gate reads the BUILT manifest (the artifact consumers actually read).')
console.error(' Run `bun run build` first; an absent manifest is a failure, not a pass.') console.error(' Run `bun run build` first; an absent manifest is a failure, not a pass.')
process.exit(1) process.exit(1)

View file

@ -16,6 +16,7 @@ import { dirname, resolve } from 'node:path'
import { fileURLToPath } from 'node:url' import { fileURLToPath } from 'node:url'
import { chromium } from 'playwright' import { chromium } from 'playwright'
import { servesThisTree, serveThisTree } from './_serves-this-tree.mjs' import { servesThisTree, serveThisTree } from './_serves-this-tree.mjs'
import { TREE_NAME } from './_tree-name.mjs'
const REPO = resolve(dirname(fileURLToPath(import.meta.url)), '..') const REPO = resolve(dirname(fileURLToPath(import.meta.url)), '..')
@ -30,7 +31,7 @@ const REPO = resolve(dirname(fileURLToPath(import.meta.url)), '..')
// executed shape check:packets-all had. Default to the upg-100 leaf (the profile this // executed shape check:packets-all had. Default to the upg-100 leaf (the profile this
// gate was written for) and SKIP with a named verdict when nothing is served there. // gate was written for) and SKIP with a named verdict when nothing is served there.
const DEFAULT_LEAF = (process.env.BASE_URL || 'http://127.0.0.1:4173') const DEFAULT_LEAF = (process.env.BASE_URL || 'http://127.0.0.1:4173')
+ '/doxa-maps/my-upg-100-list/upg-100-list/index.html' + `/${TREE_NAME}/my-upg-100-list/upg-100-list/index.html`
let LEAF = process.argv[2] || DEFAULT_LEAF let LEAF = process.argv[2] || DEFAULT_LEAF
let OWN_SERVER = null let OWN_SERVER = null
{ {

View file

@ -37,12 +37,13 @@ import { readFileSync, existsSync, statSync } from 'node:fs'
import { createHash } from 'node:crypto' import { createHash } from 'node:crypto'
import { resolve, dirname, join } from 'node:path' import { resolve, dirname, join } from 'node:path'
import { fileURLToPath } from 'node:url' import { fileURLToPath } from 'node:url'
import { TREE_NAME } from './_tree-name.mjs'
const HERE = dirname(fileURLToPath(import.meta.url)) const HERE = dirname(fileURLToPath(import.meta.url))
const REPO = process.env.DOXA_REPO || resolve(HERE, '..') const REPO = process.env.DOXA_REPO || resolve(HERE, '..')
const PUBLISHED_BASE = (process.env.PUBLISHED_BASE const PUBLISHED_BASE = (process.env.PUBLISHED_BASE
|| 'https://nuxtbase.app/plugin-assets/doxa-maps').replace(/\/$/, '') || `https://nuxtbase.app/plugin-assets/${TREE_NAME}`).replace(/\/$/, '')
const TREE = resolve(REPO, 'public/doxa-maps') const TREE = resolve(REPO, `public/${TREE_NAME}`)
const MANIFEST = join(TREE, 'manifest.json') const MANIFEST = join(TREE, 'manifest.json')
if (!existsSync(MANIFEST)) { if (!existsSync(MANIFEST)) {
@ -133,7 +134,7 @@ if (stale.length) {
console.error(' "red dot" pin hint) were all fixed in main and all still visible live, and each') console.error(' "red dot" pin hint) were all fixed in main and all still visible live, and each')
console.error(' one read as a regression against a card marked done.') console.error(' one read as a regression against a card marked done.')
console.error('\n FIX: publish. That is a COORDINATOR action — deploy:dev runs a full build whose') console.error('\n FIX: publish. That is a COORDINATOR action — deploy:dev runs a full build whose')
console.error(' first step deletes public/doxa-maps/ and tears the tree out from under every') console.error(` first step deletes public/${TREE_NAME}/ and tears the tree out from under every`)
console.error(' other agent, which is why the build guard refuses it without BUILD_ALLOW=1.') console.error(' other agent, which is why the build guard refuses it without BUILD_ALLOW=1.')
process.exit(1) process.exit(1)
} }

View file

@ -40,6 +40,7 @@ import { spawn } from 'node:child_process'
import { existsSync } from 'node:fs' import { existsSync } from 'node:fs'
import { resolve, dirname } from 'node:path' import { resolve, dirname } from 'node:path'
import { fileURLToPath } from 'node:url' import { fileURLToPath } from 'node:url'
import { TREE_NAME } from './_tree-name.mjs'
const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..') const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..')
const argv = process.argv.slice(2) const argv = process.argv.slice(2)
@ -101,10 +102,10 @@ const MAPBOX_KNOWN = {
const freePort = () => new Promise(ok => { const s = createServer().listen(0, '127.0.0.1', () => { const p = s.address().port; s.close(() => ok(p)) }) }) const freePort = () => new Promise(ok => { const s = createServer().listen(0, '127.0.0.1', () => { const p = s.address().port; s.close(() => ok(p)) }) })
const TREE_PORT = await freePort() const TREE_PORT = await freePort()
const HOST_PORT = await freePort() const HOST_PORT = await freePort()
const TREE = `http://127.0.0.1:${TREE_PORT}/doxa-maps` const TREE = `http://127.0.0.1:${TREE_PORT}/${TREE_NAME}`
if (!existsSync(resolve(ROOT, 'public/doxa-maps/doxa-maps-spa/doxa-maps-spa.js'))) { if (!existsSync(resolve(ROOT, `public/${TREE_NAME}/doxa-maps-spa/doxa-maps-spa.js`))) {
console.log('SKIP check-research-gallery-embed: no built gallery at public/doxa-maps/doxa-maps-spa/') console.log(`SKIP check-research-gallery-embed: no built gallery at public/${TREE_NAME}/doxa-maps-spa/`)
process.exit(2) process.exit(2)
} }
let chromium let chromium
@ -139,10 +140,10 @@ await new Promise(r => setTimeout(r, 800))
// pass or a fail — the same code the shared browser locks use for "busy, re-run later". // pass or a fail — the same code the shared browser locks use for "busy, re-run later".
{ {
const { readFileSync } = await import('node:fs') const { readFileSync } = await import('node:fs')
const disk = readFileSync(resolve(ROOT, 'public/doxa-maps/manifest.json'), 'utf8') const disk = readFileSync(resolve(ROOT, `public/${TREE_NAME}/manifest.json`), 'utf8')
let served = '' let served = ''
for (let i = 0; i < 20 && !served; i++) { for (let i = 0; i < 20 && !served; i++) {
try { const r = await fetch(`http://127.0.0.1:${TREE_PORT}/doxa-maps/manifest.json`); if (r.ok) served = await r.text() } catch { /* not up yet */ } try { const r = await fetch(`http://127.0.0.1:${TREE_PORT}/${TREE_NAME}/manifest.json`); if (r.ok) served = await r.text() } catch { /* not up yet */ }
if (!served) await new Promise(r => setTimeout(r, 300)) if (!served) await new Promise(r => setTimeout(r, 300))
} }
if (served !== disk) { if (served !== disk) {

View file

@ -43,6 +43,7 @@
import { readFileSync, existsSync, readdirSync } from 'node:fs' import { readFileSync, existsSync, readdirSync } from 'node:fs'
import { resolve, join, dirname } from 'node:path' import { resolve, join, dirname } from 'node:path'
import { fileURLToPath } from 'node:url' import { fileURLToPath } from 'node:url'
import { TREE_NAME } from './_tree-name.mjs'
// Same fix as check-zone-census.mjs, same reason. DOXA_REPO still wins; only the FALLBACK moved // Same fix as check-zone-census.mjs, same reason. DOXA_REPO still wins; only the FALLBACK moved
// off an absolute path into one author's worktree. check:no-abs-paths rejects those outright — "a // off an absolute path into one author's worktree. check:no-abs-paths rejects those outright — "a
@ -80,7 +81,7 @@ const POLICY = {
} }
const isMap = (src) => /mapboxgl|maplibregl|useMapInstance/.test(src) const isMap = (src) => /mapboxgl|maplibregl|useMapInstance/.test(src)
const manifest = JSON.parse(readFileSync(join(REPO, 'public', 'doxa-maps', 'manifest.json'), 'utf8')) const manifest = JSON.parse(readFileSync(join(REPO, 'public', TREE_NAME, 'manifest.json'), 'utf8'))
const published = new Set(manifest.bundles.map((b) => b.bundle)) const published = new Set(manifest.bundles.map((b) => b.bundle))
const fails = [] const fails = []

View file

@ -35,10 +35,11 @@
import { readFileSync, existsSync, readdirSync, statSync } from 'node:fs' import { readFileSync, existsSync, readdirSync, statSync } from 'node:fs'
import { resolve, dirname, join } from 'node:path' import { resolve, dirname, join } from 'node:path'
import { fileURLToPath } from 'node:url' import { fileURLToPath } from 'node:url'
import { TREE_NAME } from './_tree-name.mjs'
const HERE = dirname(fileURLToPath(import.meta.url)) const HERE = dirname(fileURLToPath(import.meta.url))
const REPO = process.env.DOXA_REPO || resolve(HERE, '..') const REPO = process.env.DOXA_REPO || resolve(HERE, '..')
const TREE = resolve(REPO, 'public/doxa-maps') const TREE = resolve(REPO, `public/${TREE_NAME}`)
// Each entry: a change that landed in a SHARED component, and a string that proves a built bundle // Each entry: a change that landed in a SHARED component, and a string that proves a built bundle
// contains it. `source` is where the marker must also still exist — if it does not, the marker is // contains it. `source` is where the marker must also still exist — if it does not, the marker is

View file

@ -46,11 +46,12 @@ import { execFileSync } from 'node:child_process'
import { createHash } from 'node:crypto' import { createHash } from 'node:crypto'
import { resolve, dirname, join } from 'node:path' import { resolve, dirname, join } from 'node:path'
import { fileURLToPath } from 'node:url' import { fileURLToPath } from 'node:url'
import { TREE_NAME, TREE_NAME_RE } from './_tree-name.mjs'
const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..') const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..')
const SHOTS_SRC = join(ROOT, 'internal', 'readme-shots.mjs') const SHOTS_SRC = join(ROOT, 'internal', 'readme-shots.mjs')
const OUT = join(ROOT, 'documentation', 'readme-images') const OUT = join(ROOT, 'documentation', 'readme-images')
const BUILT = join(ROOT, 'public', 'doxa-maps') const BUILT = join(ROOT, 'public', TREE_NAME)
if (!existsSync(SHOTS_SRC) || !existsSync(OUT)) { if (!existsSync(SHOTS_SRC) || !existsSync(OUT)) {
console.log('SHOT-FRESHNESS: SKIP — readme-shots.mjs or documentation/readme-images/ not present.') console.log('SHOT-FRESHNESS: SKIP — readme-shots.mjs or documentation/readme-images/ not present.')
@ -64,7 +65,7 @@ const recorded = existsSync(SHOT_BUNDLES) ? JSON.parse(readFileSync(SHOT_BUNDLES
const git = (...a) => execFileSync('git', ['-C', ROOT, ...a], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }) const git = (...a) => execFileSync('git', ['-C', ROOT, ...a], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] })
let dirty = null let dirty = null
try { try {
dirty = new Set(git('status', '--porcelain', '--', 'public/doxa-maps', 'documentation/readme-images') dirty = new Set(git('status', '--porcelain', '--', `public/${TREE_NAME}`, 'documentation/readme-images')
.split('\n').filter(Boolean).map((l) => resolve(ROOT, l.slice(3).replace(/^"|"$/g, '')))) .split('\n').filter(Boolean).map((l) => resolve(ROOT, l.slice(3).replace(/^"|"$/g, ''))))
} catch { /* not a git checkout */ } } catch { /* not a git checkout */ }
function stamp(file) { function stamp(file) {
@ -88,8 +89,8 @@ for (const { name, path } of shots) {
const png = join(OUT, name + '.png') const png = join(OUT, name + '.png')
if (!existsSync(png)) { missing.push(name); continue } if (!existsSync(png)) { missing.push(name); continue }
const m = path.match(/^\/doxa-maps\/([^/]+)\//) const m = path.match(new RegExp(`^/${TREE_NAME_RE}/([^/]+)/`))
if (!m) { skipped.push(`${name} (path ${path} is not a /doxa-maps/ leaf)`); continue } if (!m) { skipped.push(`${name} (path ${path} is not a /${TREE_NAME}/ leaf)`); continue }
const bundle = join(BUILT, m[1], `${m[1]}.js`) const bundle = join(BUILT, m[1], `${m[1]}.js`)
if (!existsSync(bundle)) { skipped.push(`${name} (bundle ${m[1]} not built)`); continue } if (!existsSync(bundle)) { skipped.push(`${name} (bundle ${m[1]} not built)`); continue }

View file

@ -35,9 +35,10 @@ import { chromium } from 'playwright'
import { requireDevServesThisCheckout as __devGuard, requireThisTree as __treeGuard } from './_serves-this-tree.mjs' import { requireDevServesThisCheckout as __devGuard, requireThisTree as __treeGuard } from './_serves-this-tree.mjs'
import { fileURLToPath as __f2p } from 'node:url' import { fileURLToPath as __f2p } from 'node:url'
import { dirname as __dn, resolve as __rs } from 'node:path' import { dirname as __dn, resolve as __rs } from 'node:path'
import { TREE_NAME } from './_tree-name.mjs'
const __ROOT = __rs(__dn(__f2p(import.meta.url)), '..') const __ROOT = __rs(__dn(__f2p(import.meta.url)), '..')
const BASE = process.env.BASE_URL || 'http://127.0.0.1:4173/doxa-maps' const BASE = process.env.BASE_URL || `http://127.0.0.1:4173/${TREE_NAME}`
// Measure THIS checkout, or measure nothing (exit 75 = NOT MEASURED; see _serves-this-tree.mjs). // Measure THIS checkout, or measure nothing (exit 75 = NOT MEASURED; see _serves-this-tree.mjs).
await __treeGuard(BASE, __ROOT, 'SPA-TAB-ORDER') await __treeGuard(BASE, __ROOT, 'SPA-TAB-ORDER')

View file

@ -24,6 +24,7 @@ import { createServer } from 'node:http'
import { readFileSync } from 'node:fs' import { readFileSync } from 'node:fs'
import { resolve, dirname } from 'node:path' import { resolve, dirname } from 'node:path'
import { fileURLToPath } from 'node:url' import { fileURLToPath } from 'node:url'
import { TREE_NAME } from './_tree-name.mjs'
const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..') const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..')
const argv = process.argv.slice(2) const argv = process.argv.slice(2)
@ -36,13 +37,13 @@ const port = await new Promise((ok) => { const s = createServer().listen(0, '127
const srv = spawn(process.execPath, [resolve(ROOT, 'internal/serve-strict.mjs'), '--port', String(port), '--directory', DIR], { stdio: 'ignore' }) const srv = spawn(process.execPath, [resolve(ROOT, 'internal/serve-strict.mjs'), '--port', String(port), '--directory', DIR], { stdio: 'ignore' })
const stop = () => { try { srv.kill() } catch { /* already gone */ } } const stop = () => { try { srv.kill() } catch { /* already gone */ } }
let up = false let up = false
for (let i = 0; i < 40 && !up; i++) { await new Promise((r) => setTimeout(r, 150)); up = await fetch(`http://127.0.0.1:${port}/doxa-maps/index.html`).then((r) => r.ok).catch(() => false) } for (let i = 0; i < 40 && !up; i++) { await new Promise((r) => setTimeout(r, 150)); up = await fetch(`http://127.0.0.1:${port}/${TREE_NAME}/index.html`).then((r) => r.ok).catch(() => false) }
if (!up) { stop(); console.log(`STAGING-LANG: NOT MEASURED — could not serve ${DIR}`); process.exit(75) } if (!up) { stop(); console.log(`STAGING-LANG: NOT MEASURED — could not serve ${DIR}`); process.exit(75) }
const browser = await chromium.launch({ args: ['--use-gl=swiftshader', '--enable-webgl', '--ignore-gpu-blocklist'] }) const browser = await chromium.launch({ args: ['--use-gl=swiftshader', '--enable-webgl', '--ignore-gpu-blocklist'] })
const done = async (code, msg) => { console.log(msg); await browser.close(); stop(); process.exit(code) } const done = async (code, msg) => { console.log(msg); await browser.close(); stop(); process.exit(code) }
const page = await browser.newPage({ viewport: { width: 1400, height: 900 } }) const page = await browser.newPage({ viewport: { width: 1400, height: 900 } })
await page.goto(`http://127.0.0.1:${port}/doxa-maps/index.html?lang=ar`, { waitUntil: 'load' }) await page.goto(`http://127.0.0.1:${port}/${TREE_NAME}/index.html?lang=ar`, { waitUntil: 'load' })
const node = page.locator(`.node[data-key="${KEY}"]`) const node = page.locator(`.node[data-key="${KEY}"]`)
if (!(await node.count())) await done(75, `STAGING-LANG: NOT MEASURED — no ${KEY} node on the staging page`) if (!(await node.count())) await done(75, `STAGING-LANG: NOT MEASURED — no ${KEY} node on the staging page`)
await node.first().click() await node.first().click()

View file

@ -35,10 +35,11 @@ import { chromium } from 'playwright'
import { resolve, dirname } from 'node:path' import { resolve, dirname } from 'node:path'
import { fileURLToPath } from 'node:url' import { fileURLToPath } from 'node:url'
import { servesThisTree } from './_serves-this-tree.mjs' import { servesThisTree } from './_serves-this-tree.mjs'
import { TREE_NAME } from './_tree-name.mjs'
const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..') const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..')
const BASE = process.env.BASE || 'http://127.0.0.1:4173/doxa-maps' const BASE = process.env.BASE || `http://127.0.0.1:4173/${TREE_NAME}`
const DEFAULT = [ const DEFAULT = [
'my-upg-100-list/upg-100-list/index.html', 'my-upg-100-list/upg-100-list/index.html',
'my-upg-100-list/upg-100-mfe/index.html', 'my-upg-100-list/upg-100-mfe/index.html',
@ -62,7 +63,7 @@ if (!reachable) {
// "SKIP — nothing served at /tmp/doxa-maps-browser-gate.lock/…" (the slot had overwritten BASE) // "SKIP — nothing served at /tmp/doxa-maps-browser-gate.lock/…" (the slot had overwritten BASE)
// and EXITED 0, so a wave counted a gate that measured nothing as green. // and EXITED 0, so a wave counted a gate that measured nothing as green.
console.log(`UPG-MAP-DATA: NOT MEASURED — nothing served at ${urls[0]}.`) console.log(`UPG-MAP-DATA: NOT MEASURED — nothing served at ${urls[0]}.`)
console.log(' Serve THIS checkout (internal/serve-strict.mjs on your own port) and pass BASE=http://127.0.0.1:<port>/doxa-maps') console.log(` Serve THIS checkout (internal/serve-strict.mjs on your own port) and pass BASE=http://127.0.0.1:<port>/${TREE_NAME}`)
process.exit(75) process.exit(75)
} }
@ -76,7 +77,7 @@ if (!reachable) {
// ⚠ NOT MEASURED, exit 75 — the repo's EX_TEMPFAIL for "not measured" (a9550858), which the wave // ⚠ NOT MEASURED, exit 75 — the repo's EX_TEMPFAIL for "not measured" (a9550858), which the wave
// runner reports as ⊘. It was exit 2 here, a code nothing else in the repo reads that way. // runner reports as ⊘. It was exit 2 here, a code nothing else in the repo reads that way.
// This check is also what makes the :4173 default safe: another agent's tree there is refused. // This check is also what makes the :4173 default safe: another agent's tree there is refused.
const TREE_BASE = String(urls[0]).split('/doxa-maps/')[0] const TREE_BASE = String(urls[0]).split(`/${TREE_NAME}/`)[0]
const treeCheck = await servesThisTree(TREE_BASE, ROOT) const treeCheck = await servesThisTree(TREE_BASE, ROOT)
if (!treeCheck.ok) { if (!treeCheck.ok) {
console.error('UPG-MAP-DATA: NOT MEASURED — the server is not serving this checkout.') console.error('UPG-MAP-DATA: NOT MEASURED — the server is not serving this checkout.')
@ -125,7 +126,7 @@ try {
console.log('UPG-MAP-DATA') console.log('UPG-MAP-DATA')
const fails = [] const fails = []
for (const r of results) { for (const r of results) {
const name = r.url.split('/doxa-maps/')[1] || r.url const name = r.url.split(`/${TREE_NAME}/`)[1] || r.url
if (r.error) { fails.push(`${name}: navigation failed — ${r.error}`); console.log(` ${name} — ERROR`); continue } if (r.error) { fails.push(`${name}: navigation failed — ${r.error}`); console.log(` ${name} — ERROR`); continue }
if (!r.text) { if (!r.text) {
// A MISSING STATUS LINE IS A FAILURE, NOT A SKIP. If the element never rendered its own // A MISSING STATUS LINE IS A FAILURE, NOT A SKIP. If the element never rendered its own

View file

@ -10,16 +10,17 @@
import { readFileSync, rmSync, mkdirSync } from 'node:fs' import { readFileSync, rmSync, mkdirSync } from 'node:fs'
import { dirname, resolve } from 'node:path' import { dirname, resolve } from 'node:path'
import { fileURLToPath } from 'node:url' import { fileURLToPath } from 'node:url'
import { TREE_NAME } from './_tree-name.mjs'
const __dirname = dirname(fileURLToPath(import.meta.url)) const __dirname = dirname(fileURLToPath(import.meta.url))
function config() { function config() {
try { try {
const cfg = JSON.parse(readFileSync(resolve(__dirname, '..', 'vite.doxa-maps-build-config.json'), 'utf8')) const cfg = JSON.parse(readFileSync(resolve(__dirname, '..', 'vite.doxa-maps-build-config.json'), 'utf8'))
const name = (typeof cfg?.name === 'string' && cfg.name.trim()) ? cfg.name.trim() : 'doxa-maps' const name = (typeof cfg?.name === 'string' && cfg.name.trim()) ? cfg.name.trim() : TREE_NAME
const paths = Array.isArray(cfg?.paths) ? cfg.paths.filter(p => typeof p === 'string' && p.trim()) : [] const paths = Array.isArray(cfg?.paths) ? cfg.paths.filter(p => typeof p === 'string' && p.trim()) : []
return { name, paths } return { name, paths }
} catch { return { name: 'doxa-maps', paths: [] } } } catch { return { name: TREE_NAME, paths: [] } }
} }
const { name, paths } = config() const { name, paths } = config()

View file

@ -18,13 +18,14 @@
import { readFileSync, readdirSync, statSync, existsSync } from 'node:fs'; import { readFileSync, readdirSync, statSync, existsSync } from 'node:fs';
import { join, relative, dirname, resolve } from 'node:path'; import { join, relative, dirname, resolve } from 'node:path';
import { fileURLToPath } from 'node:url'; import { fileURLToPath } from 'node:url';
import { TREE_NAME } from './_tree-name.mjs'
const here = dirname(fileURLToPath(import.meta.url)); const here = dirname(fileURLToPath(import.meta.url));
const bundlerRoot = resolve(here, '..'); const bundlerRoot = resolve(here, '..');
let treeDir = process.argv[2]; let treeDir = process.argv[2];
if (!treeDir) { if (!treeDir) {
let name = 'doxa-maps'; let name = TREE_NAME;
const cfgPath = join(bundlerRoot, 'vite.doxa-maps-build-config.json'); const cfgPath = join(bundlerRoot, 'vite.doxa-maps-build-config.json');
if (existsSync(cfgPath)) { if (existsSync(cfgPath)) {
try { name = JSON.parse(readFileSync(cfgPath, 'utf8')).name || name; } catch {} try { name = JSON.parse(readFileSync(cfgPath, 'utf8')).name || name; } catch {}

View file

@ -22,6 +22,7 @@ import { readFileSync, existsSync, statSync, createReadStream } from 'node:fs'
import { resolve, join, extname } from 'node:path' import { resolve, join, extname } from 'node:path'
import { fileURLToPath } from 'node:url' import { fileURLToPath } from 'node:url'
import { chromium } from 'playwright' import { chromium } from 'playwright'
import { TREE_NAME } from './_tree-name.mjs'
const __dirname = fileURLToPath(new URL('.', import.meta.url)) const __dirname = fileURLToPath(new URL('.', import.meta.url))
const PUB = resolve(__dirname, '..', 'public') const PUB = resolve(__dirname, '..', 'public')
@ -46,16 +47,16 @@ const server = createServer((req, res) => {
}) })
// ── the page list: every profile page in the manifest ─────────────────────── // ── the page list: every profile page in the manifest ───────────────────────
const manifest = JSON.parse(readFileSync(join(PUB, 'doxa-maps', 'manifest.json'), 'utf8')) const manifest = JSON.parse(readFileSync(join(PUB, TREE_NAME, 'manifest.json'), 'utf8'))
const targets = [] const targets = []
for (const b of manifest.bundles) for (const p of b.profiles) { for (const b of manifest.bundles) for (const p of b.profiles) {
// iframeUrl is ./-relative to the tree root // iframeUrl is ./-relative to the tree root
const url = p.iframeUrl.replace(/^\.\//, '/doxa-maps/') const url = p.iframeUrl.replace(/^\.\//, `/${TREE_NAME}/`)
targets.push({ bundle: b.bundle, profile: p.profile, element: p.element, url }) targets.push({ bundle: b.bundle, profile: p.profile, element: p.element, url })
// Preconfigured instances are embeddable pages too — verify them the same way. // Preconfigured instances are embeddable pages too — verify them the same way.
for (const inst of (p.instances || [])) { for (const inst of (p.instances || [])) {
targets.push({ bundle: b.bundle, profile: `${p.profile}/${inst.id}`, element: inst.element || p.element, targets.push({ bundle: b.bundle, profile: `${p.profile}/${inst.id}`, element: inst.element || p.element,
url: inst.iframeUrl.replace(/^\.\//, '/doxa-maps/') }) url: inst.iframeUrl.replace(/^\.\//, `/${TREE_NAME}/`) })
} }
} }
const list = GREP ? targets.filter(t => (t.bundle + '/' + t.profile).includes(GREP)) : targets const list = GREP ? targets.filter(t => (t.bundle + '/' + t.profile).includes(GREP)) : targets

View file

@ -76,6 +76,7 @@ import { createHash } from 'node:crypto'
import { tmpdir } from 'node:os' import { tmpdir } from 'node:os'
import { spawnSync } from 'node:child_process' import { spawnSync } from 'node:child_process'
import { fileURLToPath } from 'node:url' import { fileURLToPath } from 'node:url'
import { TREE_NAME } from './_tree-name.mjs'
const HERE = dirname(fileURLToPath(import.meta.url)) const HERE = dirname(fileURLToPath(import.meta.url))
const CHECKER = join(HERE, 'sri-consistency-check.mjs') const CHECKER = join(HERE, 'sri-consistency-check.mjs')
@ -269,7 +270,7 @@ if (SELF_TEST) {
selfTest() selfTest()
} else { } else {
let code = 0 let code = 0
for (const root of (TREES.length ? TREES : ['public/doxa-maps'])) { for (const root of (TREES.length ? TREES : [`public/${TREE_NAME}`])) {
code = report(root, fixTree(root)) || code code = report(root, fixTree(root)) || code
} }
process.exit(code) process.exit(code)

View file

@ -28,9 +28,10 @@
import { readFileSync, writeFileSync, existsSync } from 'node:fs' import { readFileSync, writeFileSync, existsSync } from 'node:fs'
import { resolve, dirname, join } from 'node:path' import { resolve, dirname, join } from 'node:path'
import { fileURLToPath } from 'node:url' import { fileURLToPath } from 'node:url'
import { TREE_NAME } from './_tree-name.mjs'
const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..') const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..')
const MANIFEST = join(ROOT, 'public', 'doxa-maps', 'manifest.json') const MANIFEST = join(ROOT, 'public', TREE_NAME, 'manifest.json')
const OUT = join(ROOT, 'documentation', 'reference-capability-exemplars.md') const OUT = join(ROOT, 'documentation', 'reference-capability-exemplars.md')
if (!existsSync(MANIFEST)) { if (!existsSync(MANIFEST)) {
@ -68,7 +69,7 @@ lines.push('> **Do not hand-edit.** Regenerate with `bun run capability-exemplar
lines.push('> check:capability-exemplars` fails if this file falls behind the manifest.') lines.push('> check:capability-exemplars` fails if this file falls behind the manifest.')
lines.push('>') lines.push('>')
lines.push('> This answers one question authoritatively: **which bundles are the capability-N') lines.push('> This answers one question authoritatively: **which bundles are the capability-N')
lines.push('> examples right now?** It is derived from `public/doxa-maps/manifest.json`, which is what') lines.push(`> examples right now?** It is derived from \`public/${TREE_NAME}/manifest.json\`, which is what`)
lines.push('> the build computed from each folder\'s real shape — so it cannot drift from what ships.') lines.push('> the build computed from each folder\'s real shape — so it cannot drift from what ships.')
lines.push('>') lines.push('>')
lines.push('> **Cite this file rather than remembering.** The same citation has been wrong in three') lines.push('> **Cite this file rather than remembering.** The same citation has been wrong in three')

View file

@ -50,6 +50,7 @@ import { readFileSync, writeFileSync, existsSync, readdirSync, mkdirSync } from
import { join, resolve, dirname } from 'node:path' import { join, resolve, dirname } from 'node:path'
import { fileURLToPath, pathToFileURL } from 'node:url' import { fileURLToPath, pathToFileURL } from 'node:url'
import { PROFILE_TIER, TYPE_DISPLAY, detectProfileType } from './plugin-gallery/discover.mjs' import { PROFILE_TIER, TYPE_DISPLAY, detectProfileType } from './plugin-gallery/discover.mjs'
import { TREE_NAME } from './_tree-name.mjs'
const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..') const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..')
@ -99,8 +100,8 @@ export function readBuildConfig() {
} catch { /* fall through */ } } catch { /* fall through */ }
} }
return { return {
name: str('name') || 'doxa-maps', name: str('name') || TREE_NAME,
title: str('displayTitle') || 'doxa-maps', title: str('displayTitle') || TREE_NAME,
tokenUrl: tokenUrl || 'window.MAP_TOKEN_URL', tokenUrl: tokenUrl || 'window.MAP_TOKEN_URL',
token: str('token'), token: str('token'),
apiBaseUrl: str('apiBaseUrl').replace(/\/+$/, ''), apiBaseUrl: str('apiBaseUrl').replace(/\/+$/, ''),
@ -185,7 +186,7 @@ const LADDER_JS = String.raw`<script>
ex: '&lt;doxa-map profile-config=&#39;{&quot;profile&quot;:&quot;doxa-simple-map&quot;}&#39;&gt;&lt;/doxa-map&gt;' }, ex: '&lt;doxa-map profile-config=&#39;{&quot;profile&quot;:&quot;doxa-simple-map&quot;}&#39;&gt;&lt;/doxa-map&gt;' },
{ title: 'App profile (mounting file)', { title: 'App profile (mounting file)',
def: 'One flat .vue file in plugin-designs/&lt;bundle&gt;/ that defines a whole map app — the main component of that map. The settings written on the tag (profile-config) name which app profile to load, and the shared ProfileLoader picks it. In class-and-object terms: the element class is the class, each tag on a page is an instance, and the app profile is the screen that instance loads.', def: 'One flat .vue file in plugin-designs/&lt;bundle&gt;/ that defines a whole map app — the main component of that map. The settings written on the tag (profile-config) name which app profile to load, and the shared ProfileLoader picks it. In class-and-object terms: the element class is the class, each tag on a page is an instance, and the app profile is the screen that instance loads.',
how: 'Most repos need one such main file per app. doxa-maps builds hundreds of maps from one shared library, so every map gets its own app profile — the folder plugin-designs/ is where they sit. The five kinds on this ladder are the five shapes those files come in.', how: 'Most repos need one such main file per app. ${TREE_NAME} builds hundreds of maps from one shared library, so every map gets its own app profile — the folder plugin-designs/ is where they sit. The five kinds on this ladder are the five shapes those files come in.',
ex: 'plugin-designs/doxa-simple-map/doxa-simple-map.vue' } ex: 'plugin-designs/doxa-simple-map/doxa-simple-map.vue' }
]; ];
var LADDER = [ var LADDER = [
@ -246,7 +247,7 @@ const LADDER_JS = String.raw`<script>
* config fields every leaf page bakes (readBuildConfig) * config fields every leaf page bakes (readBuildConfig)
*/ */
export function renderGallery(model, opts = {}) { export function renderGallery(model, opts = {}) {
const { mode = 'build', title = 'doxa-maps', tree = 'doxa-maps', tokenUrl = 'window.MAP_TOKEN_URL', token = '', apiBaseUrl = '', assetsBaseUrl = '', qaWidget = true, tierMap = {} } = opts const { mode = 'build', title = TREE_NAME, tree = TREE_NAME, tokenUrl = 'window.MAP_TOKEN_URL', token = '', apiBaseUrl = '', assetsBaseUrl = '', qaWidget = true, tierMap = {} } = opts
const dev = mode === 'dev' const dev = mode === 'dev'
const baked = compact(model, true, tierMap) const baked = compact(model, true, tierMap)
const json = (v) => JSON.stringify(v).replace(/</g, '\\u003c') const json = (v) => JSON.stringify(v).replace(/</g, '\\u003c')
@ -301,7 +302,7 @@ code{font-family:ui-monospace,Menlo,monospace}
${dev ? LADDER_CSS : ''}</style> ${dev ? LADDER_CSS : ''}</style>
</head> </head>
<body> <body>
<!-- The doxa-maps nested-mfe-spa (capability 5, the reserved app-shell): a Vue single-page app <!-- The ${TREE_NAME} nested-mfe-spa (capability 5, the reserved app-shell): a Vue single-page app
whose sidebar is the composability ladder rendered as vertical tabs (capability 5 → 1), whose sidebar is the composability ladder rendered as vertical tabs (capability 5 → 1),
each row mounting that plugin's custom element in the one slot. This SAME page is the each row mounting that plugin's custom element in the one slot. This SAME page is the
served studio root (public/index.html) AND the Capacitor mobile-app entry (webDir: public). served studio root (public/index.html) AND the Capacitor mobile-app entry (webDir: public).

View file

@ -42,6 +42,7 @@ import { createHash } from 'node:crypto'
// without a separate tierMap. Previously the manifest carried no `type`, so a manifest-fed // without a separate tierMap. Previously the manifest carried no `type`, so a manifest-fed
// consumer (plugin-studio) mis-tiered every profile as cap-1 (single). Single source of truth. // consumer (plugin-studio) mis-tiered every profile as cap-1 (single). Single source of truth.
import { detectProfileType } from './plugin-gallery/discover.mjs' import { detectProfileType } from './plugin-gallery/discover.mjs'
import { TREE_NAME as CONFIGURED_TREE_NAME } from './_tree-name.mjs'
const __dirname = dirname(fileURLToPath(import.meta.url)) const __dirname = dirname(fileURLToPath(import.meta.url))
const APP_PROFILES = resolve(__dirname, '..', 'plugin-designs') const APP_PROFILES = resolve(__dirname, '..', 'plugin-designs')
@ -56,7 +57,7 @@ const SOURCES = resolve(__dirname, '..', 'library/api/sources.json')
function readBuildConfig() { function readBuildConfig() {
try { try {
const cfg = JSON.parse(readFileSync(BUILD_CONFIG_FILE, 'utf8')) const cfg = JSON.parse(readFileSync(BUILD_CONFIG_FILE, 'utf8'))
const name = (typeof cfg.name === 'string' && cfg.name.trim()) ? cfg.name.trim() : 'doxa-maps' const name = (typeof cfg.name === 'string' && cfg.name.trim()) ? cfg.name.trim() : CONFIGURED_TREE_NAME
const paths = Array.isArray(cfg.paths) ? cfg.paths.filter((p) => typeof p === 'string' && p.trim()) : [] const paths = Array.isArray(cfg.paths) ? cfg.paths.filter((p) => typeof p === 'string' && p.trim()) : []
// Local, NON-committed sync override: `DOXA_SYNC_PATHS=/abs/dir[,/abs/dir2]` ALSO copies the // Local, NON-committed sync override: `DOXA_SYNC_PATHS=/abs/dir[,/abs/dir2]` ALSO copies the
// built tree into extra destinations (e.g. a local doxa-life checkout's public folder) without // built tree into extra destinations (e.g. a local doxa-life checkout's public folder) without
@ -103,7 +104,7 @@ function readBuildConfig() {
const posterSources = cfg.posterSources === true || bundlePosters const posterSources = cfg.posterSources === true || bundlePosters
return { name, paths, embedBaseUrl, tokenUrl, token, assetsBaseUrl, apiBaseUrl, gatedProfiles, qaWidget, bundlePosters, posterSources } return { name, paths, embedBaseUrl, tokenUrl, token, assetsBaseUrl, apiBaseUrl, gatedProfiles, qaWidget, bundlePosters, posterSources }
} catch { } catch {
return { name: 'doxa-maps', paths: [], embedBaseUrl: '', tokenUrl: '', token: '', assetsBaseUrl: '', apiBaseUrl: '', gatedProfiles: [], qaWidget: true, bundlePosters: false, posterSources: false } return { name: CONFIGURED_TREE_NAME, paths: [], embedBaseUrl: '', tokenUrl: '', token: '', assetsBaseUrl: '', apiBaseUrl: '', gatedProfiles: [], qaWidget: true, bundlePosters: false, posterSources: false }
} }
} }
const DEST_CONFIG = readBuildConfig() const DEST_CONFIG = readBuildConfig()
@ -138,7 +139,7 @@ function assetsBaseFor(depth) {
// title is the PRODUCT title (config displayTitle, default org-free), never the deployment TREE_NAME // title is the PRODUCT title (config displayTitle, default org-free), never the deployment TREE_NAME
// (TREE_NAME stays in URLs/paths where it is a folder name, not branding). // (TREE_NAME stays in URLs/paths where it is a folder name, not branding).
const DISPLAY_TITLE = (typeof DEST_CONFIG.displayTitle === 'string' && DEST_CONFIG.displayTitle.trim()) const DISPLAY_TITLE = (typeof DEST_CONFIG.displayTitle === 'string' && DEST_CONFIG.displayTitle.trim())
? DEST_CONFIG.displayTitle.trim() : 'doxa-maps' ? DEST_CONFIG.displayTitle.trim() : TREE_NAME
// Base URL every intra-tree ref (manifest iframeUrl/scriptUrl) is built against. // Base URL every intra-tree ref (manifest iframeUrl/scriptUrl) is built against.
// With embedBaseUrl set (e.g. "https://doxa.life/js"), refs are ABSOLUTE — the // With embedBaseUrl set (e.g. "https://doxa.life/js"), refs are ABSOLUTE — the
// build-time override for known hosts / partner embeds. WITHOUT it, refs are // build-time override for known hosts / partner embeds. WITHOUT it, refs are

View file

@ -17,12 +17,13 @@ import { join, relative, dirname, resolve } from 'node:path'
import { fileURLToPath } from 'node:url' import { fileURLToPath } from 'node:url'
import { execFileSync } from 'node:child_process' import { execFileSync } from 'node:child_process'
import { tmpdir } from 'node:os' import { tmpdir } from 'node:os'
import { TREE_NAME } from './_tree-name.mjs'
const here = dirname(fileURLToPath(import.meta.url)) const here = dirname(fileURLToPath(import.meta.url))
const bundlerRoot = resolve(here, '..') const bundlerRoot = resolve(here, '..')
let treeDir = process.argv[2] let treeDir = process.argv[2]
if (!treeDir) { if (!treeDir) {
let name = 'doxa-maps' let name = TREE_NAME
const cfg = join(bundlerRoot, 'vite.doxa-maps-build-config.json') const cfg = join(bundlerRoot, 'vite.doxa-maps-build-config.json')
if (existsSync(cfg)) { try { name = JSON.parse(readFileSync(cfg, 'utf8')).name || name } catch {} } if (existsSync(cfg)) { try { name = JSON.parse(readFileSync(cfg, 'utf8')).name || name } catch {} }
treeDir = join(bundlerRoot, 'public', name) treeDir = join(bundlerRoot, 'public', name)

View file

@ -16,6 +16,7 @@ import { chromium } from 'playwright'
import { spawn } from 'node:child_process' import { spawn } from 'node:child_process'
import { createServer } from 'node:http' import { createServer } from 'node:http'
import { readFileSync } from 'node:fs' import { readFileSync } from 'node:fs'
import { TREE_NAME, TREE_NAME_RE } from './_tree-name.mjs'
const argv = process.argv.slice(2) const argv = process.argv.slice(2)
const arg = (k, d) => { const i = argv.indexOf(k); return i >= 0 && argv[i + 1] ? argv[i + 1] : d } const arg = (k, d) => { const i = argv.indexOf(k); return i >= 0 && argv[i + 1] ? argv[i + 1] : d }
@ -25,7 +26,7 @@ const RUNS = Number(arg('--runs', 3)), MBPS = Number(arg('--mbps', 10)), RTT = N
const EMBED = argv.includes('--embed') const EMBED = argv.includes('--embed')
const DEFAULT = JSON.parse(readFileSync('plugin-designs/doxa-maps-spa/doxa-maps-spa.meta.json', 'utf8')).composes const DEFAULT = JSON.parse(readFileSync('plugin-designs/doxa-maps-spa/doxa-maps-spa.meta.json', 'utf8')).composes
const ONLY = arg('--only', '') ? arg('--only', '').split(',') : DEFAULT const ONLY = arg('--only', '') ? arg('--only', '').split(',') : DEFAULT
const manifest = JSON.parse(readFileSync('public/doxa-maps/manifest.json', 'utf8')) const manifest = JSON.parse(readFileSync(`public/${TREE_NAME}/manifest.json`, 'utf8'))
const port = await new Promise((ok) => { const s = createServer().listen(0, '127.0.0.1', () => { const p = s.address().port; s.close(() => ok(p)) }) }) const port = await new Promise((ok) => { const s = createServer().listen(0, '127.0.0.1', () => { const p = s.address().port; s.close(() => ok(p)) }) })
const srv = spawn(process.execPath, ['internal/serve-strict.mjs', '--port', String(port)], { stdio: 'ignore' }) const srv = spawn(process.execPath, ['internal/serve-strict.mjs', '--port', String(port)], { stdio: 'ignore' })
@ -33,10 +34,10 @@ const hostPort = await new Promise((ok) => { const s = createServer().listen(0,
let hostHtml = '' let hostHtml = ''
const host = createServer((_, r) => { r.writeHead(200, { 'content-type': 'text/html; charset=utf-8' }); r.end(hostHtml) }).listen(hostPort, 'localhost') const host = createServer((_, r) => { r.writeHead(200, { 'content-type': 'text/html; charset=utf-8' }); r.end(hostHtml) }).listen(hostPort, 'localhost')
await new Promise((r) => setTimeout(r, 900)) await new Promise((r) => setTimeout(r, 900))
const T = `http://127.0.0.1:${port}/doxa-maps` const T = `http://127.0.0.1:${port}/${TREE_NAME}`
const kind = (u) => /mapbox-gl(\.min)?\.js|maplibre-gl\.js/.test(u) ? 'engine' : /mapbox-gl\.css|maplibre-gl\.css/.test(u) ? 'engine-css' const kind = (u) => /mapbox-gl(\.min)?\.js|maplibre-gl\.js/.test(u) ? 'engine' : /mapbox-gl\.css|maplibre-gl\.css/.test(u) ? 'engine-css'
: /\/styles\/v1\/|tiles\.openfreemap\.org\/styles/.test(u) ? 'style' : /\/doxa-maps\/[^/]+\/[^/]+\.js$/.test(u) ? 'bundle' : /\/styles\/v1\/|tiles\.openfreemap\.org\/styles/.test(u) ? 'style' : new RegExp(`/${TREE_NAME_RE}/[^/]+/[^/]+\\.js$`).test(u) ? 'bundle'
: /pray\.doxa\.life\/api/.test(u) ? 'api' : /\/assets\/.*\.(json|geojson)/.test(u) ? 'data' : /\.pbf|\.mvt|vector/.test(u) ? 'tile' : 'other' : /pray\.doxa\.life\/api/.test(u) ? 'api' : /\/assets\/.*\.(json|geojson)/.test(u) ? 'data' : /\.pbf|\.mvt|vector/.test(u) ? 'tile' : 'other'
const browser = await chromium.launch({ args: ['--use-gl=swiftshader', '--enable-webgl', '--ignore-gpu-blocklist'] }) const browser = await chromium.launch({ args: ['--use-gl=swiftshader', '--enable-webgl', '--ignore-gpu-blocklist'] })
@ -48,7 +49,7 @@ for (const ref of ONLY) {
if (!p) { rows.push({ ref, missing: true }); continue } if (!p) { rows.push({ ref, missing: true }); continue }
let url = `${T}/${String(p.iframeUrl).replace(/^\.\//, '')}` let url = `${T}/${String(p.iframeUrl).replace(/^\.\//, '')}`
if (EMBED) { if (EMBED) {
const tok = (readFileSync(`public/doxa-maps/${String(p.iframeUrl).replace(/^\.\//, '')}`, 'utf8').match(/BAKED_TOKEN\s*=\s*"(pk\.[^"]+)"/) || [])[1] || '' const tok = (readFileSync(`public/${TREE_NAME}/${String(p.iframeUrl).replace(/^\.\//, '')}`, 'utf8').match(/BAKED_TOKEN\s*=\s*"(pk\.[^"]+)"/) || [])[1] || ''
const snip = String((p.embed && p.embed.script) || '').replace(/src="\.\//g, `src="${T}/`).replace(/YOUR_MAPBOX_TOKEN/g, tok) const snip = String((p.embed && p.embed.script) || '').replace(/src="\.\//g, `src="${T}/`).replace(/YOUR_MAPBOX_TOKEN/g, tok)
hostHtml = `<!doctype html><html><head><meta charset="utf-8"><title>Partner</title></head><body style="margin:0"><div style="position:relative;height:900px">${snip}</div></body></html>` hostHtml = `<!doctype html><html><head><meta charset="utf-8"><title>Partner</title></head><body style="margin:0"><div style="position:relative;height:900px">${snip}</div></body></html>`
url = `http://localhost:${hostPort}/partner.html` url = `http://localhost:${hostPort}/partner.html`

View file

@ -14,6 +14,7 @@ import { resolve, extname } from 'node:path'
import { readdirSync, statSync, existsSync, readFileSync } from 'node:fs' import { readdirSync, statSync, existsSync, readFileSync } from 'node:fs'
import { REPO_ROOT, discoverSpaRegistry } from './discover.mjs' import { REPO_ROOT, discoverSpaRegistry } from './discover.mjs'
import { readBuildConfig, renderGallery } from '../generate-gallery.mjs' import { readBuildConfig, renderGallery } from '../generate-gallery.mjs'
import { TREE_NAME } from '../_tree-name.mjs'
// ─── DEV plugin gallery ────────────────────────────────────────────────────── // ─── DEV plugin gallery ──────────────────────────────────────────────────────
// `bun x vite` (dev) serves the plugin gallery at `/` — the same page `bun run build` // `bun x vite` (dev) serves the plugin gallery at `/` — the same page `bun run build`
@ -172,7 +173,7 @@ export function galleryModel(reg) {
// //
// Returns the rewritten `/__leaf/…` path, or '' when the URL is not an embed leaf URL (the common // Returns the rewritten `/__leaf/…` path, or '' when the URL is not an embed leaf URL (the common
// case — every other request must fall through untouched). // case — every other request must fall through untouched).
export function leafRewrite(path, treeName = 'doxa-maps') { export function leafRewrite(path, treeName = TREE_NAME) {
if (!path.endsWith('/index.html')) return '' if (!path.endsWith('/index.html')) return ''
let segs = path.slice(0, -'/index.html'.length).split('/').filter(Boolean) let segs = path.slice(0, -'/index.html'.length).split('/').filter(Boolean)
if (segs[0] === treeName) segs = segs.slice(1) // tolerate the tree-prefixed form if (segs[0] === treeName) segs = segs.slice(1) // tolerate the tree-prefixed form
@ -241,7 +242,7 @@ export function autoSpaDevPlugin(token = '') {
// The rewrite is applied to `path` (which every branch reads) and to `req.url` (so anything // The rewrite is applied to `path` (which every branch reads) and to `req.url` (so anything
// downstream sees a consistent request); never via server.middlewares.handle(), which // downstream sees a consistent request); never via server.middlewares.handle(), which
// replays this same middleware and would loop. // replays this same middleware and would loop.
let treeName = 'doxa-maps' let treeName = TREE_NAME
try { treeName = readBuildConfig()?.name || treeName } catch { /* keep the default */ } try { treeName = readBuildConfig()?.name || treeName } catch { /* keep the default */ }
const rewritten = leafRewrite(rawPath, treeName) const rewritten = leafRewrite(rawPath, treeName)
const path = rewritten || rawPath const path = rewritten || rawPath
@ -405,7 +406,7 @@ export function autoSpaDevPlugin(token = '') {
'<!DOCTYPE html>', '<!DOCTYPE html>',
'<html lang="en"><head><meta charset="UTF-8" />', '<html lang="en"><head><meta charset="UTF-8" />',
'<meta name="viewport" content="width=device-width, initial-scale=1.0" />', '<meta name="viewport" content="width=device-width, initial-scale=1.0" />',
'<title>doxa-maps plugin studio</title>', `<title>${TREE_NAME} plugin studio</title>`,
'<link rel="icon" href="data:,">', '<link rel="icon" href="data:,">',
// The token ladder's baked rung, exactly as the built leaves receive it, so maps paint // The token ladder's baked rung, exactly as the built leaves receive it, so maps paint
// in dev without anyone appending ?tk=. // in dev without anyone appending ?tk=.

View file

@ -36,13 +36,14 @@
import { readdirSync, statSync, readFileSync, existsSync, writeFileSync } from 'node:fs' import { readdirSync, statSync, readFileSync, existsSync, writeFileSync } from 'node:fs'
import { join, relative, resolve, dirname, extname, sep } from 'node:path' import { join, relative, resolve, dirname, extname, sep } from 'node:path'
import { fileURLToPath } from 'node:url' import { fileURLToPath } from 'node:url'
import { TREE_NAME } from './_tree-name.mjs'
const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..') const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..')
const args = process.argv.slice(2) const args = process.argv.slice(2)
const argOf = (k, d) => { const i = args.indexOf(k); return i >= 0 ? args[i + 1] : d } const argOf = (k, d) => { const i = args.indexOf(k); return i >= 0 ? args[i + 1] : d }
const VALUE_FLAGS = new Set(['--tree', '--json', '--root']) const VALUE_FLAGS = new Set(['--tree', '--json', '--root'])
const DIR = resolve(argOf('--root', '') || args.find((a, i) => !a.startsWith('--') && !VALUE_FLAGS.has(args[i - 1])) || join(ROOT, 'public')) const DIR = resolve(argOf('--root', '') || args.find((a, i) => !a.startsWith('--') && !VALUE_FLAGS.has(args[i - 1])) || join(ROOT, 'public'))
const TREE = argOf('--tree', 'doxa-maps') const TREE = argOf('--tree', TREE_NAME)
const STRICT = args.includes('--strict') const STRICT = args.includes('--strict')
const JSON_OUT = argOf('--json', '') const JSON_OUT = argOf('--json', '')
if (!existsSync(DIR) || !statSync(DIR).isDirectory()) { console.error(`[hygiene] not a directory: ${DIR}`); process.exit(2) } if (!existsSync(DIR) || !statSync(DIR).isDirectory()) { console.error(`[hygiene] not a directory: ${DIR}`); process.exit(2) }

View file

@ -7,6 +7,7 @@ import { createServer } from 'node:http'
import { readFile, stat } from 'node:fs/promises' import { readFile, stat } from 'node:fs/promises'
import { join, extname, resolve, dirname } from 'node:path' import { join, extname, resolve, dirname } from 'node:path'
import { fileURLToPath } from 'node:url' import { fileURLToPath } from 'node:url'
import { TREE_NAME } from './_tree-name.mjs'
const HERE = dirname(fileURLToPath(import.meta.url)) const HERE = dirname(fileURLToPath(import.meta.url))
const ROOT = resolve(HERE, '..', 'public') const ROOT = resolve(HERE, '..', 'public')
@ -58,7 +59,7 @@ async function tryShot(name, path, { vp = { width: 1440, height: 900 }, wait = 5
// cap1: vision-card DEFAULT instance page (TOKEN_FREE=true -> renders the card instantly, no token error). // cap1: vision-card DEFAULT instance page (TOKEN_FREE=true -> renders the card instantly, no token error).
// Long wait so the aurora backdrop + rotating stat cards fully paint (they animate in). // Long wait so the aurora backdrop + rotating stat cards fully paint (they animate in).
await tryShot('cap1-single-element', '/doxa-maps/doxa-vision-card/doxa-vision-card/default/index.html', await tryShot('cap1-single-element', `/${TREE_NAME}/doxa-vision-card/doxa-vision-card/default/index.html`,
{ wait: 9000 }) { wait: 9000 })
// cap5: SPA deep-linked to the prayer map instance so a real painted map shows in the right pane. // cap5: SPA deep-linked to the prayer map instance so a real painted map shows in the right pane.

View file

@ -17,6 +17,7 @@ import { createHash } from 'node:crypto'
import { existsSync, readFileSync } from 'node:fs' import { existsSync, readFileSync } from 'node:fs'
import { join, extname, resolve, dirname } from 'node:path' import { join, extname, resolve, dirname } from 'node:path'
import { fileURLToPath } from 'node:url' import { fileURLToPath } from 'node:url'
import { TREE_NAME, TREE_NAME_RE } from './_tree-name.mjs'
const HERE = dirname(fileURLToPath(import.meta.url)) const HERE = dirname(fileURLToPath(import.meta.url))
const ROOT = resolve(HERE, '..', 'public') const ROOT = resolve(HERE, '..', 'public')
@ -205,8 +206,8 @@ async function shot(name, path, {
console.log(' OK', name + '.png', `<- ${path} (${ready} elements)`) console.log(' OK', name + '.png', `<- ${path} (${ready} elements)`)
// Record WHICH BUNDLE this picture is of, by content hash, so check:shots can tell a current // Record WHICH BUNDLE this picture is of, by content hash, so check:shots can tell a current
// picture from a stale one without trusting any clock (see SHOT_BUNDLES below). // picture from a stale one without trusting any clock (see SHOT_BUNDLES below).
const lm = path.match(/^\/doxa-maps\/([^/]+)\//) const lm = path.match(new RegExp(`^/${TREE_NAME_RE}/([^/]+)/`))
const bundleJs = lm && join(ROOT, 'doxa-maps', lm[1], `${lm[1]}.js`) const bundleJs = lm && join(ROOT, TREE_NAME, lm[1], `${lm[1]}.js`)
if (bundleJs && existsSync(bundleJs)) { if (bundleJs && existsSync(bundleJs)) {
shotBundles[name] = { bundle: lm[1], sha256: createHash('sha256').update(readFileSync(bundleJs)).digest('hex') } shotBundles[name] = { bundle: lm[1], sha256: createHash('sha256').update(readFileSync(bundleJs)).digest('hex') }
} }
@ -234,22 +235,22 @@ async function shot(name, path, {
// //
// all-people-groups is a genuine cap-1: ONE .vue, NO instances.json, manifest type "single". // all-people-groups is a genuine cap-1: ONE .vue, NO instances.json, manifest type "single".
// It DOES own a map, so it needs a token and longer to settle than the token-free vision card did. // It DOES own a map, so it needs a token and longer to settle than the token-free vision card did.
await shot('cap1-single-element', '/doxa-maps/all-people-groups/all-people-groups/index.html', await shot('cap1-single-element', `/${TREE_NAME}/all-people-groups/all-people-groups/index.html`,
{ wait: 8000, minElements: 25 }) { wait: 8000, minElements: 25 })
// ---- CAP2: prop-table -> 3 shots from ONE root.vue (prayer/engagement/adoption) ---- // ---- CAP2: prop-table -> 3 shots from ONE root.vue (prayer/engagement/adoption) ----
await shot('cap2-prayer', '/doxa-maps/doxa-simple-map/doxa-simple-map/prayer/index.html', { waitFor: CANVAS, wait: 8000 }) await shot('cap2-prayer', `/${TREE_NAME}/doxa-simple-map/doxa-simple-map/prayer/index.html`, { waitFor: CANVAS, wait: 8000 })
await shot('cap2-engagement', '/doxa-maps/doxa-simple-map/doxa-simple-map/engagement/index.html', { waitFor: CANVAS, wait: 8000 }) await shot('cap2-engagement', `/${TREE_NAME}/doxa-simple-map/doxa-simple-map/engagement/index.html`, { waitFor: CANVAS, wait: 8000 })
await shot('cap2-adoption', '/doxa-maps/doxa-simple-map/doxa-simple-map/adoption/index.html', { waitFor: CANVAS, wait: 8000 }) await shot('cap2-adoption', `/${TREE_NAME}/doxa-simple-map/doxa-simple-map/adoption/index.html`, { waitFor: CANVAS, wait: 8000 })
// ---- CAP3: monolithic research map ---- // ---- CAP3: monolithic research map ----
await shot('cap3-monolithic-research', '/doxa-maps/doxa-research-map/research-map/index.html', { waitFor: CANVAS, wait: 9000 }) await shot('cap3-monolithic-research', `/${TREE_NAME}/doxa-research-map/research-map/index.html`, { waitFor: CANVAS, wait: 9000 })
// ---- CAP4: nested-element SPA. The bundle ships THREE profiles, one shot each: // ---- CAP4: nested-element SPA. The bundle ships THREE profiles, one shot each:
// upg-100-mfe (the cap-4 composite) · upg-100-list (the list) · poster-wall. // upg-100-mfe (the cap-4 composite) · upg-100-list (the list) · poster-wall.
// There are no "list pages" — see the note below the second shot. ---- // There are no "list pages" — see the note below the second shot. ----
await shot('cap4-upg100-mfe', '/doxa-maps/my-upg-100-list/upg-100-mfe/index.html', { wait: 8000 }) await shot('cap4-upg100-mfe', `/${TREE_NAME}/my-upg-100-list/upg-100-mfe/index.html`, { wait: 8000 })
await shot('cap4-upg100-list', '/doxa-maps/my-upg-100-list/upg-100-list/index.html', { wait: 7000 }) await shot('cap4-upg100-list', `/${TREE_NAME}/my-upg-100-list/upg-100-list/index.html`, { wait: 7000 })
// ---- CAP4's THIRD profile: the poster wall ---- // ---- CAP4's THIRD profile: the poster wall ----
// ★ REPLACED 'cap4-upg100-list-page2' 2026-09-21. That shot loaded the SAME URL as page 1 and // ★ REPLACED 'cap4-upg100-list-page2' 2026-09-21. That shot loaded the SAME URL as page 1 and
@ -263,7 +264,7 @@ await shot('cap4-upg100-list', '/doxa-maps/my-upg-100-list/upg-100-list/index.ht
// ★ THE SLOT WAS CHASING UI THAT DOES NOT EXIST while a REAL profile went unshot: this bundle // ★ THE SLOT WAS CHASING UI THAT DOES NOT EXIST while a REAL profile went unshot: this bundle
// ships THREE manifest-listed profiles (upg-100-mfe, upg-100-list, poster-wall) and poster-wall // ships THREE manifest-listed profiles (upg-100-mfe, upg-100-list, poster-wall) and poster-wall
// had no screenshot anywhere. Shoot the real third thing rather than fake a fourth. // had no screenshot anywhere. Shoot the real third thing rather than fake a fourth.
await shot('cap4-upg100-poster-wall', '/doxa-maps/my-upg-100-list/poster-wall/index.html', { wait: 8000 }) await shot('cap4-upg100-poster-wall', `/${TREE_NAME}/my-upg-100-list/poster-wall/index.html`, { wait: 8000 })
// ---- CAP5: nested SPA — the doxa-maps-spa ELEMENT, not the gallery page ---- // ---- CAP5: nested SPA — the doxa-maps-spa ELEMENT, not the gallery page ----
// ★ CORRECTED 2026-09-20. This shot previously pointed at /index.html, the AUTO-GENERATED // ★ CORRECTED 2026-09-20. This shot previously pointed at /index.html, the AUTO-GENERATED
@ -274,7 +275,7 @@ await shot('cap4-upg100-poster-wall', '/doxa-maps/my-upg-100-list/poster-wall/in
// mapboxReferer:false — a cap-5 shell nests EVERY tab in an iframe, so the context-wide Referer // mapboxReferer:false — a cap-5 shell nests EVERY tab in an iframe, so the context-wide Referer
// (see shot()) breaks its content exactly as it broke deploy1-iframe. The shell loads no Mapbox // (see shot()) breaks its content exactly as it broke deploy1-iframe. The shell loads no Mapbox
// tiles of its own; each nested leaf is its own document and fetches its own. // tiles of its own; each nested leaf is its own document and fetches its own.
await shot('cap5-doxa-maps-spa', '/doxa-maps/doxa-maps-spa/doxa-maps-spa/index.html', await shot('cap5-doxa-maps-spa', `/${TREE_NAME}/doxa-maps-spa/doxa-maps-spa/index.html`,
{ wait: 10000, mapboxReferer: false }) { wait: 10000, mapboxReferer: false })
// ---- CAP5 (the OTHER one): the plugin-studio ELEMENT — the DEVELOPER gallery ---- // ---- CAP5 (the OTHER one): the plugin-studio ELEMENT — the DEVELOPER gallery ----
@ -300,7 +301,7 @@ await shot('cap5-doxa-maps-spa', '/doxa-maps/doxa-maps-spa/doxa-maps-spa/index.h
// Clicks the first rail row (cap-5 → doxa-maps-spa), so the picture shows what the studio is FOR: // Clicks the first rail row (cap-5 → doxa-maps-spa), so the picture shows what the studio is FOR:
// a nested SPA rendering inside the shell that lists every element by capability. Fails soft — if // a nested SPA rendering inside the shell that lists every element by capability. Fails soft — if
// the row is not found the shot still captures, and the readiness guard below still applies. // the row is not found the shot still captures, and the readiness guard below still applies.
await shot('cap5-plugin-studio', '/doxa-maps/plugin-studio/plugin-studio/index.html', { await shot('cap5-plugin-studio', `/${TREE_NAME}/plugin-studio/plugin-studio/index.html`, {
// Same iframe-nesting reason as the other cap-5 shell above. // Same iframe-nesting reason as the other cap-5 shell above.
mapboxReferer: false, mapboxReferer: false,
wait: 12000, wait: 12000,
@ -337,7 +338,7 @@ await shot('cap5-plugin-studio', '/doxa-maps/plugin-studio/plugin-studio/index.h
// exited 0, so deploy1-iframe.png has been a screenshot of a 404 page since 2026-09-18. // exited 0, so deploy1-iframe.png has been a screenshot of a 404 page since 2026-09-18.
// The host page is SERVED FROM MEMORY rather than written into public/: anything written there // The host page is SERVED FROM MEMORY rather than written into public/: anything written there
// is agentic scratch in the studio tree and would be destroyed by the next build's clean. // is agentic scratch in the studio tree and would be destroyed by the next build's clean.
const IFRAME_SRC = '/doxa-maps/doxa-simple-map/doxa-simple-map/prayer/index.html' const IFRAME_SRC = `/${TREE_NAME}/doxa-simple-map/doxa-simple-map/prayer/index.html`
+ '?tk=' + encodeURIComponent(TK) + '?tk=' + encodeURIComponent(TK)
virtualPages.set('/__readme-iframe-host.html', `<!doctype html> virtualPages.set('/__readme-iframe-host.html', `<!doctype html>
<html><head><meta charset="utf-8"><title>Embedding a Doxa map</title> <html><head><meta charset="utf-8"><title>Embedding a Doxa map</title>
@ -369,31 +370,31 @@ await shot('deploy1-iframe', '/__readme-iframe-host.html',
// ---- THE 7 PRODUCTION MAPS: one shot each, for the README's opening gallery ---- // ---- THE 7 PRODUCTION MAPS: one shot each, for the README's opening gallery ----
// Each is the plugin's own standalone leaf page — the same element the Research Gallery nests. // Each is the plugin's own standalone leaf page — the same element the Research Gallery nests.
// (UPG-100 List and Prayer Progress reuse cap4-upg100-mfe and cap2-prayer.) // (UPG-100 List and Prayer Progress reuse cap4-upg100-mfe and cap2-prayer.)
await shot('prod-big-picture', '/doxa-maps/dashboard-plus-grid/dashboard-plus-grid/index.html', { wait: 10000, minElements: 300 }) await shot('prod-big-picture', `/${TREE_NAME}/dashboard-plus-grid/dashboard-plus-grid/index.html`, { wait: 10000, minElements: 300 })
await shot('prod-simple-research', '/doxa-maps/research-map-plus-grid/research-map-plus-grid/index.html', { wait: 10000, minElements: 200 }) await shot('prod-simple-research', `/${TREE_NAME}/research-map-plus-grid/research-map-plus-grid/index.html`, { wait: 10000, minElements: 200 })
await shot('prod-regions', '/doxa-maps/regions-dashboard/regions-mfe/index.html', { wait: 10000, minElements: 100 }) await shot('prod-regions', `/${TREE_NAME}/regions-dashboard/regions-mfe/index.html`, { wait: 10000, minElements: 100 })
await shot('prod-cities-internet', '/doxa-maps/cities-internet/connectivity-map/index.html', { wait: 10000, minElements: 60 }) await shot('prod-cities-internet', `/${TREE_NAME}/cities-internet/connectivity-map/index.html`, { wait: 10000, minElements: 60 })
await shot('prod-gospel-resources', '/doxa-maps/gospel-resources/resources-map/index.html', { wait: 10000, minElements: 100 }) await shot('prod-gospel-resources', `/${TREE_NAME}/gospel-resources/resources-map/index.html`, { wait: 10000, minElements: 100 })
// ---- THE PRODUCTION MAPS ON A PHONE (390x844) — for the README's phone row ---- // ---- THE PRODUCTION MAPS ON A PHONE (390x844) — for the README's phone row ----
// Big Picture is desktop-only by ruling (on phones the Research Gallery disables it and Simple Research is // Big Picture is desktop-only by ruling (on phones the Research Gallery disables it and Simple Research is
// the dashboard), so it has no phone shot. UPG-100's phone shot is deploy2-mobile-upg100. // the dashboard), so it has no phone shot. UPG-100's phone shot is deploy2-mobile-upg100.
await shot('prod-simple-research-phone', '/doxa-maps/research-map-plus-grid/research-map-plus-grid/index.html', { vp: { width: 390, height: 844 }, dsf: 2, wait: 10000, minElements: 60 }) await shot('prod-simple-research-phone', `/${TREE_NAME}/research-map-plus-grid/research-map-plus-grid/index.html`, { vp: { width: 390, height: 844 }, dsf: 2, wait: 10000, minElements: 60 })
await shot('prod-research-gallery-phone', '/doxa-maps/doxa-maps-spa/doxa-maps-spa/index.html', { vp: { width: 390, height: 844 }, dsf: 2, wait: 10000, minElements: 60 }) await shot('prod-research-gallery-phone', `/${TREE_NAME}/doxa-maps-spa/doxa-maps-spa/index.html`, { vp: { width: 390, height: 844 }, dsf: 2, wait: 10000, minElements: 60 })
await shot('prod-regions-phone', '/doxa-maps/regions-dashboard/regions-mfe/index.html', { vp: { width: 390, height: 844 }, dsf: 2, wait: 10000, minElements: 60 }) await shot('prod-regions-phone', `/${TREE_NAME}/regions-dashboard/regions-mfe/index.html`, { vp: { width: 390, height: 844 }, dsf: 2, wait: 10000, minElements: 60 })
await shot('prod-prayer-phone', '/doxa-maps/doxa-simple-map/doxa-simple-map/prayer/index.html', { vp: { width: 390, height: 844 }, dsf: 2, wait: 10000, minElements: 60 }) await shot('prod-prayer-phone', `/${TREE_NAME}/doxa-simple-map/doxa-simple-map/prayer/index.html`, { vp: { width: 390, height: 844 }, dsf: 2, wait: 10000, minElements: 60 })
await shot('prod-cities-internet-phone', '/doxa-maps/cities-internet/connectivity-map/index.html', { vp: { width: 390, height: 844 }, dsf: 2, wait: 10000, minElements: 60 }) await shot('prod-cities-internet-phone', `/${TREE_NAME}/cities-internet/connectivity-map/index.html`, { vp: { width: 390, height: 844 }, dsf: 2, wait: 10000, minElements: 60 })
await shot('prod-gospel-resources-phone', '/doxa-maps/gospel-resources/resources-map/index.html', { vp: { width: 390, height: 844 }, dsf: 2, wait: 10000, minElements: 60 }) await shot('prod-gospel-resources-phone', `/${TREE_NAME}/gospel-resources/resources-map/index.html`, { vp: { width: 390, height: 844 }, dsf: 2, wait: 10000, minElements: 60 })
// ---- EXPERIMENTAL SHELF: the plugins the README pictures there ---- // ---- EXPERIMENTAL SHELF: the plugins the README pictures there ----
await shot('exp-wall-of-people-groups', '/doxa-maps/wall-of-people-groups/wall-of-people-groups/index.html', { wait: 12000, minElements: 40 }) await shot('exp-wall-of-people-groups', `/${TREE_NAME}/wall-of-people-groups/wall-of-people-groups/index.html`, { wait: 12000, minElements: 40 })
await shot('exp-prayer-walk-planner', '/doxa-maps/prayer-walk-planner/prayer-walk-planner/index.html', { wait: 12000, minElements: 40 }) await shot('exp-prayer-walk-planner', `/${TREE_NAME}/prayer-walk-planner/prayer-walk-planner/index.html`, { wait: 12000, minElements: 40 })
// ---- DEPLOY2: mobile — the UPG-100 list as the phone app ---- // ---- DEPLOY2: mobile — the UPG-100 list as the phone app ----
// The mobile example is UPG-100 (ruled 2026-09-24; Simple Research was retired as the example because // The mobile example is UPG-100 (ruled 2026-09-24; Simple Research was retired as the example because
// it was never built for phones). Shot once its owner marked the phone layout ready at 88a566f: // it was never built for phones). Shot once its owner marked the phone layout ready at 88a566f:
// posters one per row, the legend a bottom sheet flush with the map, print options wrapping. // posters one per row, the legend a bottom sheet flush with the map, print options wrapping.
await shot('deploy2-mobile-upg100', '/doxa-maps/my-upg-100-list/upg-100-mfe/index.html', await shot('deploy2-mobile-upg100', `/${TREE_NAME}/my-upg-100-list/upg-100-mfe/index.html`,
{ vp: { width: 390, height: 844 }, wait: 10000, dsf: 3 }) { vp: { width: 390, height: 844 }, wait: 10000, dsf: 3 })
// ---- DEPLOY3: JS embed — the Research Gallery on a real host site ---- // ---- DEPLOY3: JS embed — the Research Gallery on a real host site ----

View file

@ -40,6 +40,7 @@ import { createReadStream, statSync } from 'node:fs'
import { resolve, join, extname, normalize } from 'node:path' import { resolve, join, extname, normalize } from 'node:path'
import { dirname } from 'node:path' import { dirname } from 'node:path'
import { fileURLToPath } from 'node:url' import { fileURLToPath } from 'node:url'
import { TREE_NAME, TREE_NAME_RE } from './_tree-name.mjs'
const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..') const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..')
const argv = process.argv.slice(2) const argv = process.argv.slice(2)
@ -79,7 +80,7 @@ const send = (res, code, body, headers = {}) => {
// A 503 is deliberate, not a 404: 404 means "this is not here", which a gate may reasonably report // A 503 is deliberate, not a 404: 404 means "this is not here", which a gate may reasonably report
// as a product defect. 503 with a named cause means "ask again later" and cannot be mistaken for a // as a product defect. 503 with a named cause means "ask again later" and cannot be mistaken for a
// finding. Static assets outside the bundle tree (assets/, __gallery/) are unaffected. // finding. Static assets outside the bundle tree (assets/, __gallery/) are unaffected.
const TREE = join(DIR, 'doxa-maps') const TREE = join(DIR, TREE_NAME)
const MANIFEST = join(TREE, 'manifest.json') const MANIFEST = join(TREE, 'manifest.json')
function treeIsTorn() { function treeIsTorn() {
try { return !statSync(MANIFEST).isFile() } catch { return true } try { return !statSync(MANIFEST).isFile() } catch { return true }
@ -89,8 +90,8 @@ createServer((req, res) => {
let pathname let pathname
try { pathname = decodeURIComponent(new URL(req.url, 'http://x').pathname) } catch { return send(res, 400, 'bad request\n') } try { pathname = decodeURIComponent(new URL(req.url, 'http://x').pathname) } catch { return send(res, 400, 'bad request\n') }
if (pathname.startsWith('/doxa-maps/') && treeIsTorn()) { if (pathname.startsWith(`/${TREE_NAME}/`) && treeIsTorn()) {
return send(res, 503, 'tree is mid-build — public/doxa-maps/manifest.json is absent.\n' + return send(res, 503, `tree is mid-build — public/${TREE_NAME}/manifest.json is absent.\n` +
'A build rm -rf\'s this tree at its start, so anything measured now is a phantom.\n' + 'A build rm -rf\'s this tree at its start, so anything measured now is a phantom.\n' +
'Wait for the build to finish and retry; do NOT record this as a product finding.\n', 'Wait for the build to finish and retry; do NOT record this as a product finding.\n',
{ 'retry-after': '30' }) { 'retry-after': '30' })
@ -128,7 +129,7 @@ createServer((req, res) => {
// ACAO on every path INSIDE a folder of the tree, NOT on the tree-root manifest.json. Off by // ACAO on every path INSIDE a folder of the tree, NOT on the tree-root manifest.json. Off by
// default; check:gallery-embed --live-cors uses it to prove the gallery still boots cross-origin // default; check:gallery-embed --live-cors uses it to prove the gallery still boots cross-origin
// under the rule partners actually get. // under the rule partners actually get.
...(process.env.LIVE_CORS === '1' && /^\/doxa-maps\/manifest\.json$/.test(pathname) ? {} : { 'access-control-allow-origin': '*' }), ...(process.env.LIVE_CORS === '1' && new RegExp(`^/${TREE_NAME_RE}/manifest\\.json$`).test(pathname) ? {} : { 'access-control-allow-origin': '*' }),
}) })
createReadStream(file).pipe(res) createReadStream(file).pipe(res)
}).listen(PORT, HOST, () => { }).listen(PORT, HOST, () => {

View file

@ -31,6 +31,7 @@
import { readFileSync, writeFileSync, existsSync, copyFileSync } from 'node:fs' import { readFileSync, writeFileSync, existsSync, copyFileSync } from 'node:fs'
import { resolve, dirname, join } from 'node:path' import { resolve, dirname, join } from 'node:path'
import { fileURLToPath } from 'node:url' import { fileURLToPath } from 'node:url'
import { TREE_NAME } from './_tree-name.mjs'
const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..') const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..')
const CHECK = process.argv.includes('--check') const CHECK = process.argv.includes('--check')
@ -39,12 +40,12 @@ const CHECK = process.argv.includes('--check')
const APP_PUBLIC = join(ROOT, 'deployment/capacitor/android/app/src/main/assets/public') const APP_PUBLIC = join(ROOT, 'deployment/capacitor/android/app/src/main/assets/public')
const APP_ROOT = join(APP_PUBLIC, 'index.html') const APP_ROOT = join(APP_PUBLIC, 'index.html')
// The showcase leaf, inside the served tree. // The showcase leaf, inside the served tree.
const TREE = 'doxa-maps' const TREE = TREE_NAME
const LEAF_REL = `${TREE}/my-upg-100-list/upg-100-mfe/index.html` const LEAF_REL = `${TREE}/my-upg-100-list/upg-100-mfe/index.html`
const LEAF = join(APP_PUBLIC, LEAF_REL) const LEAF = join(APP_PUBLIC, LEAF_REL)
// A marker so --check can tell the app page apart from the studio page without parsing HTML. // A marker so --check can tell the app page apart from the studio page without parsing HTML.
const MARKER = '<!-- doxa-maps: mobile app start page (UPG-100 list) -->' const MARKER = `<!-- ${TREE_NAME}: mobile app start page (UPG-100 list) -->`
if (!existsSync(APP_PUBLIC)) { if (!existsSync(APP_PUBLIC)) {
console.log('APP-START-PAGE: SKIP — no synced assets tree.') console.log('APP-START-PAGE: SKIP — no synced assets tree.')

View file

@ -40,8 +40,9 @@
import { readdirSync, statSync, existsSync } from 'node:fs' import { readdirSync, statSync, existsSync } from 'node:fs'
import { join } from 'node:path' import { join } from 'node:path'
import { execSync } from 'node:child_process' import { execSync } from 'node:child_process'
import { TREE_NAME } from './_tree-name.mjs'
const TREE = process.env.TREE || 'public/doxa-maps' const TREE = process.env.TREE || `public/${TREE_NAME}`
const QUIET_SECONDS = Number(process.env.QUIET_SECONDS || 45) const QUIET_SECONDS = Number(process.env.QUIET_SECONDS || 45)
// ★ RAISED 2026-09-23 from 17 to 21, on this gate's OWN advice. It had been printing a NOTE on // ★ RAISED 2026-09-23 from 17 to 21, on this gate's OWN advice. It had been printing a NOTE on
// every green run — "the tree has outgrown its floors ... they still catch a CATASTROPHICALLY gutted // every green run — "the tree has outgrown its floors ... they still catch a CATASTROPHICALLY gutted

View file

@ -43,6 +43,7 @@ import { createServer } from 'node:http'
import { readFile, stat } from 'node:fs/promises' import { readFile, stat } from 'node:fs/promises'
import { join, extname, resolve, dirname } from 'node:path' import { join, extname, resolve, dirname } from 'node:path'
import { fileURLToPath } from 'node:url' import { fileURLToPath } from 'node:url'
import { TREE_NAME } from './_tree-name.mjs'
const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..', 'public') const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..', 'public')
const TYPES = { '.html':'text/html', '.js':'text/javascript', '.mjs':'text/javascript', const TYPES = { '.html':'text/html', '.js':'text/javascript', '.mjs':'text/javascript',
@ -122,11 +123,11 @@ try {
// A generated profile page — the surface a partner iframes. Picked from the // A generated profile page — the surface a partner iframes. Picked from the
// manifest rather than hardcoded, so renaming or removing a bundle cannot leave // manifest rather than hardcoded, so renaming or removing a bundle cannot leave
// this silently checking a page that no longer exists. // this silently checking a page that no longer exists.
const manifest = JSON.parse(await readFile(join(ROOT, 'doxa-maps', 'manifest.json'), 'utf8')) const manifest = JSON.parse(await readFile(join(ROOT, TREE_NAME, 'manifest.json'), 'utf8'))
const first = manifest.bundles.find(b => b.profiles?.length) const first = manifest.bundles.find(b => b.profiles?.length)
if (!first) fail.push('manifest declares no bundle with a profile — nothing to iframe') if (!first) fail.push('manifest declares no bundle with a profile — nothing to iframe')
else { else {
const path = `/doxa-maps/${first.bundle}/${first.profiles[0].profile}/` const path = `/${TREE_NAME}/${first.bundle}/${first.profiles[0].profile}/`
const p2 = await browser.newPage() const p2 = await browser.newPage()
const err2 = [], failed2 = [] const err2 = [], failed2 = []
p2.on('pageerror', e => err2.push(e.message.split('\n')[0])) p2.on('pageerror', e => err2.push(e.message.split('\n')[0]))

View file

@ -52,8 +52,9 @@
import { readFileSync, existsSync, readdirSync } from 'node:fs' import { readFileSync, existsSync, readdirSync } from 'node:fs'
import { join, dirname, resolve } from 'node:path' import { join, dirname, resolve } from 'node:path'
import { createHash } from 'node:crypto' import { createHash } from 'node:crypto'
import { TREE_NAME } from './_tree-name.mjs'
const TREE = process.argv[2] || 'public/doxa-maps' const TREE = process.argv[2] || `public/${TREE_NAME}`
if (!existsSync(TREE)) { if (!existsSync(TREE)) {
console.error('SRI-CONSISTENCY: tree not found: ' + TREE) console.error('SRI-CONSISTENCY: tree not found: ' + TREE)
process.exit(2) process.exit(2)
@ -119,7 +120,7 @@ if (bad.length) {
console.error(' …then re-run this check. Do NOT publish until it passes.') console.error(' …then re-run this check. Do NOT publish until it passes.')
console.error('') console.error('')
console.error(' ★ DO NOT reach for `bun run build` here. This message used to prescribe exactly') console.error(' ★ DO NOT reach for `bun run build` here. This message used to prescribe exactly')
console.error(' that, and it is the destructive remedy: its first step DELETES public/doxa-maps/,') console.error(` that, and it is the destructive remedy: its first step DELETES public/${TREE_NAME}/,`)
console.error(' tearing the tree out from under every other agent reading it (measured 3+ times,') console.error(' tearing the tree out from under every other agent reading it (measured 3+ times,')
console.error(' incl. a 461-file phantom deletion and several false "element did not mount"') console.error(' incl. a 461-file phantom deletion and several false "element did not mount"')
console.error(' diagnoses). A full rebuild cannot fix anything here that rehashing does not —') console.error(' diagnoses). A full rebuild cannot fix anything here that rehashing does not —')

View file

@ -51,6 +51,7 @@ import { readdirSync, statSync, lstatSync, mkdirSync, copyFileSync, rmSync, exis
import { join, relative, resolve, dirname, sep } from 'node:path' import { join, relative, resolve, dirname, sep } from 'node:path'
import { fileURLToPath } from 'node:url' import { fileURLToPath } from 'node:url'
import { createHash } from 'node:crypto' import { createHash } from 'node:crypto'
import { TREE_NAME } from './_tree-name.mjs'
const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..') const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..')
const CONFIG = join(ROOT, 'vite.doxa-maps-build-config.json') const CONFIG = join(ROOT, 'vite.doxa-maps-build-config.json')
@ -73,7 +74,7 @@ const isDoc = (f) => /\.(md|markdown|txt)$/i.test(f)
// Staging/preview pages are INTERNAL surfaces: <tree>/index.html and <tree>/<bundle>/index.html // Staging/preview pages are INTERNAL surfaces: <tree>/index.html and <tree>/<bundle>/index.html
// (the leaf pages partners embed live one level deeper). A public mirror carries bundles, leaf // (the leaf pages partners embed live one level deeper). A public mirror carries bundles, leaf
// pages and the data they fetch — not previews. --keep-staging opts back in. // pages and the data they fetch — not previews. --keep-staging opts back in.
const TREE = String(argOf('--tree', 'doxa-maps')).replace(/\/+$/, '') const TREE = String(argOf('--tree', TREE_NAME)).replace(/\/+$/, '')
const KEEP_STAGING = args.includes('--keep-staging') const KEEP_STAGING = args.includes('--keep-staging')
const isStaging = (f) => !KEEP_STAGING && (f === `${TREE}/index.html` || new RegExp(`^${TREE.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}/[^/]+/index\\.html$`).test(f)) const isStaging = (f) => !KEEP_STAGING && (f === `${TREE}/index.html` || new RegExp(`^${TREE.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}/[^/]+/index\\.html$`).test(f))
// Built data nothing shipped fetches: "dropin.exclude" in the build-config (read with --from-config, // Built data nothing shipped fetches: "dropin.exclude" in the build-config (read with --from-config,

View file

@ -36,9 +36,10 @@ import { chromium } from 'playwright'
import { requireThisTree } from './_serves-this-tree.mjs' import { requireThisTree } from './_serves-this-tree.mjs'
import { resolve as _resolvePath, dirname as _dirnamePath } from 'node:path' import { resolve as _resolvePath, dirname as _dirnamePath } from 'node:path'
import { fileURLToPath as _fileURLToPath } from 'node:url' import { fileURLToPath as _fileURLToPath } from 'node:url'
import { TREE_NAME } from './_tree-name.mjs'
const GL=['--use-gl=swiftshader','--enable-unsafe-swiftshader','--ignore-gpu-blocklist','--enable-webgl','--no-sandbox'] const GL=['--use-gl=swiftshader','--enable-unsafe-swiftshader','--ignore-gpu-blocklist','--enable-webgl','--no-sandbox']
const B=(process.env.BASE || 'https://nuxtbase.app/plugin-assets/doxa-maps/').replace(/\/?$/,'/') const B=(process.env.BASE || `https://nuxtbase.app/plugin-assets/${TREE_NAME}/`).replace(/\/?$/,'/')
const ROOT = _resolvePath(_dirnamePath(_fileURLToPath(import.meta.url)), '..') const ROOT = _resolvePath(_dirnamePath(_fileURLToPath(import.meta.url)), '..')
// ★ MEASURE THIS BUILD, OR MEASURE NOTHING. Several worktrees run on one machine and a default port // ★ MEASURE THIS BUILD, OR MEASURE NOTHING. Several worktrees run on one machine and a default port

View file

@ -57,6 +57,7 @@ import { servesThisTree } from './_serves-this-tree.mjs'
import { existsSync, readFileSync } from 'node:fs' import { existsSync, readFileSync } from 'node:fs'
import { resolve, dirname, join } from 'node:path' import { resolve, dirname, join } from 'node:path'
import { fileURLToPath } from 'node:url' import { fileURLToPath } from 'node:url'
import { TREE_NAME } from './_tree-name.mjs'
const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..') const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..')
const BASE = process.env.BASE_URL || 'http://127.0.0.1:4173' const BASE = process.env.BASE_URL || 'http://127.0.0.1:4173'
@ -91,9 +92,9 @@ const GATES = [
] ]
// ── PRECONDITION 1: a built tree ──────────────────────────────────────────── // ── PRECONDITION 1: a built tree ────────────────────────────────────────────
const MANIFEST = join(ROOT, 'public', 'doxa-maps', 'manifest.json') const MANIFEST = join(ROOT, 'public', TREE_NAME, 'manifest.json')
if (!existsSync(MANIFEST)) { if (!existsSync(MANIFEST)) {
console.error('BROWSER-ALL: SETUP — no public/doxa-maps/manifest.json. Run `bun run build` first.') console.error(`BROWSER-ALL: SETUP — no public/${TREE_NAME}/manifest.json. Run \`bun run build\` first.`)
process.exit(2) process.exit(2)
} }
let bundleCount = 0 let bundleCount = 0
@ -107,7 +108,7 @@ if (bundleCount < 2) {
// Every gate here fetches over HTTP. Without this check a missing server yields six identical // Every gate here fetches over HTTP. Without this check a missing server yields six identical
// timeouts and looks like six broken gates. // timeouts and looks like six broken gates.
try { try {
const r = await fetch(`${BASE}/doxa-maps/manifest.json`, { signal: AbortSignal.timeout(5000) }) const r = await fetch(`${BASE}/${TREE_NAME}/manifest.json`, { signal: AbortSignal.timeout(5000) })
// ★ 503 IS A DIFFERENT DIAGNOSIS FROM "no server", and saying so is the whole point of the code // ★ 503 IS A DIFFERENT DIAGNOSIS FROM "no server", and saying so is the whole point of the code
// below. serve-strict.mjs returns 503 while public/doxa-maps/manifest.json is absent, because a // below. serve-strict.mjs returns 503 while public/doxa-maps/manifest.json is absent, because a
// build rm -rf's that tree at its START — so a gate running then reads an EMPTY tree and reports // build rm -rf's that tree at its START — so a gate running then reads an EMPTY tree and reports
@ -117,7 +118,7 @@ try {
// server, sending the reader to start one that was already running. // server, sending the reader to start one that was already running.
if (r.status === 503) { if (r.status === 503) {
console.error('BROWSER-ALL: NOT MEASURED — the built tree is mid-build.') console.error('BROWSER-ALL: NOT MEASURED — the built tree is mid-build.')
console.error(` ${BASE} answered 503: public/doxa-maps/manifest.json is absent, which is what a`) console.error(` ${BASE} answered 503: public/${TREE_NAME}/manifest.json is absent, which is what a`)
console.error(' build looks like from the outside (clean rm -rf\'s the tree before rebuilding it).') console.error(' build looks like from the outside (clean rm -rf\'s the tree before rebuilding it).')
console.error(' Anything measured now is a PHANTOM. Wait for the build and re-run; do NOT record') console.error(' Anything measured now is a PHANTOM. Wait for the build and re-run; do NOT record')
console.error(' this as a product finding. Check with: bun run check:settled') console.error(' this as a product finding. Check with: bun run check:settled')

View file

@ -47,6 +47,7 @@ import { spawn } from 'node:child_process'
import { existsSync, readFileSync } from 'node:fs' import { existsSync, readFileSync } from 'node:fs'
import { resolve, dirname } from 'node:path' import { resolve, dirname } from 'node:path'
import { fileURLToPath } from 'node:url' import { fileURLToPath } from 'node:url'
import { TREE_NAME } from './_tree-name.mjs'
const BASE = (process.env.BASE_URL || process.argv[2] || 'http://127.0.0.1:4173').replace(/\/+$/, '') const BASE = (process.env.BASE_URL || process.argv[2] || 'http://127.0.0.1:4173').replace(/\/+$/, '')
const GL = ['--use-gl=swiftshader', '--enable-unsafe-swiftshader', '--ignore-gpu-blocklist', '--enable-webgl', '--no-sandbox'] const GL = ['--use-gl=swiftshader', '--enable-unsafe-swiftshader', '--ignore-gpu-blocklist', '--enable-webgl', '--no-sandbox']
@ -68,7 +69,7 @@ let server = null
// The fingerprint is the built bundle itself: if the bytes served differ from the bytes on // The fingerprint is the built bundle itself: if the bytes served differ from the bytes on
// disk, it is a different tree and must not be used. // disk, it is a different tree and must not be used.
const alive = async (b) => { const alive = async (b) => {
const r = await fetch(`${b}/doxa-maps/gospel-resources/resources-map/index.html`).catch(() => null) const r = await fetch(`${b}/${TREE_NAME}/gospel-resources/resources-map/index.html`).catch(() => null)
if (!(r && r.ok)) return false if (!(r && r.ok)) return false
const tree = await servesThisTree(b, REPO) const tree = await servesThisTree(b, REPO)
if (tree.ok) return true if (tree.ok) return true
@ -81,7 +82,7 @@ if (!(await alive(base))) {
console.log(`GOSPEL-MAP: SKIP — nothing served at ${base} (you named it explicitly, so none was started).`) console.log(`GOSPEL-MAP: SKIP — nothing served at ${base} (you named it explicitly, so none was started).`)
process.exit(0) process.exit(0)
} }
if (!existsSync(resolve(REPO, 'public/doxa-maps/gospel-resources/resources-map/index.html'))) { if (!existsSync(resolve(REPO, `public/${TREE_NAME}/gospel-resources/resources-map/index.html`))) {
console.log('GOSPEL-MAP: SKIP — no built tree. Run `bun run tree` (cheap) or a full build first.') console.log('GOSPEL-MAP: SKIP — no built tree. Run `bun run tree` (cheap) or a full build first.')
process.exit(0) process.exit(0)
} }
@ -98,7 +99,7 @@ if (!(await alive(base))) {
} }
console.log(`GOSPEL-MAP: started serve-strict on ${port} for this run.`) console.log(`GOSPEL-MAP: started serve-strict on ${port} for this run.`)
} }
const PAGE_URL = `${base}/doxa-maps/gospel-resources/resources-map/index.html` const PAGE_URL = `${base}/${TREE_NAME}/gospel-resources/resources-map/index.html`
const fails = [] const fails = []
let linkCount = 0 let linkCount = 0
@ -147,7 +148,7 @@ try {
// jw.org URL planted in the served data did NOT fail this gate until the scan covered // jw.org URL planted in the served data did NOT fail this gate until the scan covered
// the whole index. The doctrine boundary is the one thing here that must never be // the whole index. The doctrine boundary is the one thing here that must never be
// spot-checked: a single off-list link is a doctrinal failure, not a broken link. // spot-checked: a single off-list link is a doctrinal failure, not a broken link.
const idxRes = await fetch(`${base}/doxa-maps/assets/tables/people-group-pins.json`) const idxRes = await fetch(`${base}/${TREE_NAME}/assets/tables/people-group-pins.json`)
if (!idxRes.ok) fails.push(`pin index not served (${idxRes.status})`) if (!idxRes.ok) fails.push(`pin index not served (${idxRes.status})`)
else { else {
const idx = await idxRes.json() const idx = await idxRes.json()

View file

@ -33,6 +33,7 @@ import { chromium } from 'playwright'
import { requireThisTree } from './_serves-this-tree.mjs' import { requireThisTree } from './_serves-this-tree.mjs'
import { resolve as _resolvePath, dirname as _dirnamePath } from 'node:path' import { resolve as _resolvePath, dirname as _dirnamePath } from 'node:path'
import { fileURLToPath as _fileURLToPath } from 'node:url' import { fileURLToPath as _fileURLToPath } from 'node:url'
import { TREE_NAME } from './_tree-name.mjs'
// The SwiftShader launch. Without these a headless Chromium here has NO GL context at all. // The SwiftShader launch. Without these a headless Chromium here has NO GL context at all.
const GL_ARGS = [ const GL_ARGS = [
@ -43,7 +44,7 @@ const GL_ARGS = [
'--no-sandbox', '--no-sandbox',
] ]
const BASE = process.env.BASE || 'https://nuxtbase.app/plugin-assets/doxa-maps' const BASE = process.env.BASE || `https://nuxtbase.app/plugin-assets/${TREE_NAME}`
const ROOT = _resolvePath(_dirnamePath(_fileURLToPath(import.meta.url)), '..') const ROOT = _resolvePath(_dirnamePath(_fileURLToPath(import.meta.url)), '..')
// ★ MEASURE THIS BUILD, OR MEASURE NOTHING. Several worktrees run on one machine and a default port // ★ MEASURE THIS BUILD, OR MEASURE NOTHING. Several worktrees run on one machine and a default port
@ -147,7 +148,7 @@ const splitErrors = (lines) => {
let bad = 0 let bad = 0
for (const url of urls) { for (const url of urls) {
const name = url.split('/doxa-maps/')[1] || url const name = url.split(`/${TREE_NAME}/`)[1] || url
const page = await browser.newPage({ viewport: { width: 1280, height: 800 } }) const page = await browser.newPage({ viewport: { width: 1280, height: 800 } })
const errors = [] const errors = []
// ⚠ THE STYLESHEET COMPLAINT IS A WARNING, NOT AN ERROR, so filtering to type()==='error' above // ⚠ THE STYLESHEET COMPLAINT IS A WARNING, NOT AN ERROR, so filtering to type()==='error' above

View file

@ -26,6 +26,7 @@ import { chromium } from 'playwright'
import { dirname, resolve } from 'node:path' import { dirname, resolve } from 'node:path'
import { fileURLToPath } from 'node:url' import { fileURLToPath } from 'node:url'
import { requireThisTree } from './_serves-this-tree.mjs' import { requireThisTree } from './_serves-this-tree.mjs'
import { TREE_NAME } from './_tree-name.mjs'
// Default matches every other browser gate in this repo: `bun run serve` on loopback 4173. // Default matches every other browser gate in this repo: `bun run serve` on loopback 4173.
// It was `localhost:8899` — one of the ad-hoc ports a previous session happened to have running, // It was `localhost:8899` — one of the ad-hoc ports a previous session happened to have running,
@ -39,11 +40,11 @@ const BASE = process.env.BASE_URL || process.argv[2] || 'http://127.0.0.1:4173'
// gate passed for a whole flight on URLs that do not work in production. `bun run serve` is now // gate passed for a whole flight on URLs that do not work in production. `bun run serve` is now
// internal/serve-strict.mjs, which 404s a directory exactly as production does, and it caught these // internal/serve-strict.mjs, which 404s a directory exactly as production does, and it caught these
// two the first time it ran. The manifest already ships 28/28 iframeUrl values in this form. // two the first time it ran. The manifest already ships 28/28 iframeUrl values in this form.
const SHELL = '/doxa-maps/plugin-studio/plugin-studio/index.html' const SHELL = `/${TREE_NAME}/plugin-studio/plugin-studio/index.html`
// Measure THIS build, or measure nothing: a default port on this machine can belong to another // Measure THIS build, or measure nothing: a default port on this machine can belong to another
// worktree, and a stale studio there would be graded as ours. Exit 75 = NOT MEASURED (see a9550858). // worktree, and a stale studio there would be graded as ours. Exit 75 = NOT MEASURED (see a9550858).
await requireThisTree(BASE, resolve(dirname(fileURLToPath(import.meta.url)), '..'), 'SHELL-SCROLL') await requireThisTree(BASE, resolve(dirname(fileURLToPath(import.meta.url)), '..'), 'SHELL-SCROLL')
const LEAF = '/doxa-maps/dashboard-plus-grid/dashboard-plus-grid/index.html' const LEAF = `/${TREE_NAME}/dashboard-plus-grid/dashboard-plus-grid/index.html`
// Shadow DOM DOES NOT PIERCE. A single-level querySelector lies about every composite, so every // Shadow DOM DOES NOT PIERCE. A single-level querySelector lies about every composite, so every
// lookup here is a recursive walk with a seen-Set, re-injected after each interaction (a page // lookup here is a recursive walk with a seen-Set, re-injected after each interaction (a page

View file

@ -26,22 +26,17 @@ import { autoSpaDevPlugin } from './internal/plugin-gallery/dev-plugin.mjs'
import { stripShadowCssCommentsPlugin } from './internal/vite-plugin-strip-shadow-css-comments.mjs' import { stripShadowCssCommentsPlugin } from './internal/vite-plugin-strip-shadow-css-comments.mjs'
import { fileURLToPath } from 'node:url' import { fileURLToPath } from 'node:url'
import { resolve, dirname } from 'node:path' import { resolve, dirname } from 'node:path'
import { readdirSync, statSync, existsSync, readFileSync } from 'node:fs' import { readdirSync, statSync, existsSync } from 'node:fs'
import { TREE_NAME } from './internal/_tree-name.mjs'
const __dirname = dirname(fileURLToPath(import.meta.url)) const __dirname = dirname(fileURLToPath(import.meta.url))
const profilesDir = resolve(__dirname, 'plugin-designs') const profilesDir = resolve(__dirname, 'plugin-designs')
// The multi-MFE output folder name is configured in vite.doxa-maps-build-config.json (`name`), // The multi-MFE output folder name is configured in vite.doxa-maps-build-config.json (`name`),
// so you can name the output tree (e.g. "doxa-maps") independently of this bundler // so you can name the output tree independently of this bundler folder's own name. It is read in
// folder's own name. Falls back to "doxa-maps" if the file is missing/unset. // ONE place, internal/_tree-name.mjs, which every tool shares — so renaming the tree is a one-line
function readMfeName() { // config change rather than a hunt through the gates.
try { const MFE_NAME = TREE_NAME
const cfg = JSON.parse(readFileSync(resolve(__dirname, 'vite.doxa-maps-build-config.json'), 'utf8'))
if (cfg && typeof cfg.name === 'string' && cfg.name.trim()) return cfg.name.trim()
} catch { /* fall through to default */ }
return 'doxa-maps'
}
const MFE_NAME = readMfeName()
// Default output tree. generate-tree.mjs reads the same name + copies it onward. // Default output tree. generate-tree.mjs reads the same name + copies it onward.
// DOXA_OUT_ROOT redirects a build's output WITHOUT touching public/: check:composite rebuilds each // DOXA_OUT_ROOT redirects a build's output WITHOUT touching public/: check:composite rebuilds each
@ -74,7 +69,7 @@ export default defineConfig(({ command, mode }) => {
if (isBuild && !target) { if (isBuild && !target) {
throw new Error( throw new Error(
`[doxa-maps] Set BUNDLE=<name> for build, or use \`npm run build\` (loops all).\n` + `[${MFE_NAME}] Set BUNDLE=<name> for build, or use \`npm run build\` (loops all).\n` +
`Available: ${allNames.join(', ') || '(none)'}` `Available: ${allNames.join(', ') || '(none)'}`
) )
} }
@ -84,7 +79,7 @@ export default defineConfig(({ command, mode }) => {
// entry pair (e.g. a half-authored map, or a folder still being scaffolded). Fail with // entry pair (e.g. a half-authored map, or a folder still being scaffolded). Fail with
// a clear, actionable message instead of a `Cannot read properties of undefined` stack. // a clear, actionable message instead of a `Cannot read properties of undefined` stack.
throw new Error( throw new Error(
`[doxa-maps] Bundle "${target}" is not buildable: plugin-designs/${target}/ is missing ` + `[${MFE_NAME}] Bundle "${target}" is not buildable: plugin-designs/${target}/ is missing ` +
`index.html and/or index.js (the custom-element entry pair every bundle needs).\n` + `index.html and/or index.js (the custom-element entry pair every bundle needs).\n` +
`Buildable bundles: ${allNames.join(', ') || '(none)'}` `Buildable bundles: ${allNames.join(', ') || '(none)'}`
) )