Merge remote-tracking branch 'origin/main' into rega/research-gallery-embed
# Conflicts: # public/doxa-maps/country-posters/countries-map/index.html # public/doxa-maps/country-posters/country-poster-wall/index.html # public/doxa-maps/country-posters/country-posters-mfe/index.html # public/doxa-maps/country-posters/country-posters.js # public/doxa-maps/doxa-maps-spa/manifest.json # public/doxa-maps/doxa-research-map/doxa-research-map.js # public/doxa-maps/doxa-research-map/research-map/index.html # public/doxa-maps/doxa-research-map/research-map/maplibre/index.html # public/doxa-maps/index.html # public/doxa-maps/manifest.json
This commit is contained in:
commit
87cc608b65
139 changed files with 1265 additions and 543 deletions
|
|
@ -49,7 +49,7 @@ The scripts these commands are grounded in (from this branch's `package.json`):
|
|||
"build": "bash internal/build-lock.sh",
|
||||
"build:unlocked": "bun run gates:pre && bun run library-index && bun run check:library-index && bun run clean && bun run token && bun run build:bundles && bun run tree && bun run gates:post && bun run gallery",
|
||||
"gates:pre": "node internal/run-parallel.mjs --label=pre -- check:template-literal check:mjs-parse check:sfc-parse check:tab-radius check:synced-css check:wp-embed check:doc-scripts",
|
||||
"gates:post": "node internal/run-parallel.mjs --label=post -- check:drop check:parse verify check:sri check:public-hygiene check:label-joins check:facet-cascade check:facet-one-field check:heartbeat check:kpi-unit check:composite-reg check:descriptions check:no-abs-paths check:no-codename check:dep-names check:composite-srcs check:embed-targets check:nested-token check:i18n-parity check:tabs-localized check:packets-all check:packet-strings-parity check:zone-census check:share-policy check:library-index check:csv-shape check:capability-citations check:catalog-identity check:no-orphan-gates check:quoted-scripts check:done-evidence check:script-flags check:backlog-status check:capability-exemplars check:packet-fixture check:packet-browser check:provider-switch check:locale-reachable check:url-resolver check:manifest-covers check:one-palette check:legend-source check:backlog-refs check:composite-embed-knobs check:composite-locale-leaks check:linked-vocabulary",
|
||||
"gates:post": "node internal/run-parallel.mjs --label=post -- check:drop check:parse verify check:sri check:public-hygiene check:label-joins check:facet-cascade check:facet-one-field check:heartbeat check:kpi-unit check:composite-reg check:descriptions check:no-abs-paths check:no-codename check:dep-names check:composite-srcs check:embed-targets check:nested-token check:i18n-parity check:tabs-localized check:packets-all check:packet-strings-parity check:zone-census check:share-policy check:library-index check:csv-shape check:capability-citations check:catalog-identity check:no-orphan-gates check:quoted-scripts check:done-evidence check:script-flags check:backlog-status check:capability-exemplars check:packet-fixture check:packet-browser check:provider-switch check:locale-reachable check:url-resolver check:manifest-covers check:one-palette check:legend-source check:backlog-refs check:clustering-modes check:composite-embed-knobs check:composite-locale-leaks check:linked-vocabulary",
|
||||
"build:bundles": "node internal/run-parallel.mjs --bundles", // the 22 vite builds, 4 at a time
|
||||
"preview": "bun x vite preview",
|
||||
"serve": "node internal/serve-strict.mjs", // 404s a directory, like production
|
||||
|
|
|
|||
|
|
@ -22,8 +22,9 @@
|
|||
import { readFileSync, existsSync, realpathSync } from 'node:fs'
|
||||
import { createHash } from 'node:crypto'
|
||||
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)
|
||||
|
|
@ -37,7 +38,7 @@ export async function servesThisTree(base, repo) {
|
|||
} catch {
|
||||
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)
|
||||
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')
|
||||
|
|
|
|||
45
internal/_tree-name.mjs
Normal file
45
internal/_tree-name.mjs
Normal 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')
|
||||
|
|
@ -68,6 +68,7 @@ import { setTimeout as sleep } from 'node:timers/promises'
|
|||
import { fileURLToPath } from 'node:url'
|
||||
import { dirname, resolve } from 'node:path'
|
||||
import { execFileSync } from 'node:child_process'
|
||||
import { TREE_NAME } from '../_tree-name.mjs'
|
||||
|
||||
const __dirname = fileURLToPath(new URL('.', import.meta.url))
|
||||
const ROOT = resolve(__dirname, '..', '..')
|
||||
|
|
@ -178,7 +179,7 @@ async function readGrid(page) {
|
|||
async function runDesktop(browser) {
|
||||
const PAIR = 'desktop dashboard-plus-grid'
|
||||
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} ===`)
|
||||
try {
|
||||
await page.goto(url, { waitUntil: 'load', timeout: 60000 })
|
||||
|
|
@ -344,7 +345,7 @@ async function runDesktop(browser) {
|
|||
async function runMobile(browser) {
|
||||
const PAIR = 'mobile research-map-plus-grid'
|
||||
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} ===`)
|
||||
try {
|
||||
await page.goto(url, { waitUntil: 'load', timeout: 60000 })
|
||||
|
|
|
|||
|
|
@ -34,11 +34,16 @@ DRY_RUN="${DRY_RUN:-0}"
|
|||
|
||||
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 "$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"
|
||||
|
||||
# --- 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")
|
||||
[ "$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
|
||||
echo "DRY_RUN — plan (no changes):"
|
||||
echo " 1. mv $PUBLIC_DIR/doxa-maps -> $OLD_TREE"
|
||||
echo " 2. mv $STAGE_TREE -> $PUBLIC_DIR/doxa-maps"
|
||||
echo " 1. mv $PUBLIC_DIR/$TREE -> $OLD_TREE"
|
||||
echo " 2. mv $STAGE_TREE -> $PUBLIC_DIR/$TREE"
|
||||
echo " 3. cp $STAGE_INDEX -> $PUBLIC_DIR/index.html"
|
||||
echo " 4. rm -rf $OLD_TREE"
|
||||
echo " (NEVER touched: $PUBLIC_DIR/assets/ $PUBLIC_DIR/.gitignore $PUBLIC_DIR/.build.lock)"
|
||||
|
|
@ -94,11 +99,11 @@ if [ "$DRY_RUN" = "1" ]; then
|
|||
fi
|
||||
|
||||
# --- THE SWAP (scoped; rename-based; public/ itself and the lock never move) ---------------------------------
|
||||
if [ -d "$PUBLIC_DIR/doxa-maps" ]; then
|
||||
mv "$PUBLIC_DIR/doxa-maps" "$OLD_TREE" # 1. instant; old tree still fully valid under new name
|
||||
if [ -d "$PUBLIC_DIR/$TREE" ]; then
|
||||
mv "$PUBLIC_DIR/$TREE" "$OLD_TREE" # 1. instant; old tree still fully valid under new name
|
||||
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
|
||||
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."
|
||||
|
|
|
|||
|
|
@ -34,6 +34,25 @@ BUSY=75
|
|||
MAX_HOLD="${BROWSER_MAX_HOLD:-900}"
|
||||
KILL_AFTER="${BROWSER_KILL_AFTER:-15}"
|
||||
|
||||
# ★ A GATE THAT IS LEGITIMATELY LONG DECLARES ITS OWN LIMIT, in its header: @browser-slot-max-hold 2700
|
||||
# check:host-locale-contract loads every leaf twice and hit the 900 s default three times on full
|
||||
# runs (NOT MEASURED, not a verdict). An env prefix in its package.json script could not fix that —
|
||||
# this wrapper reads its limit before the script's own environment exists — so the gate carries the
|
||||
# number itself and it holds however the gate is invoked (node <file>, or bun run <script>).
|
||||
# The caller's BROWSER_MAX_HOLD still wins; a declaration is capped at 3600 s so no file can bring
|
||||
# back the unbounded hold this limit exists to prevent.
|
||||
if [ -z "${BROWSER_MAX_HOLD:-}" ]; then
|
||||
gate_file=""
|
||||
for a in "$@"; do case "$a" in *.mjs|*.js) [ -f "$a" ] && gate_file="$a"; break;; esac; done
|
||||
if [ -z "$gate_file" ] && [ "${2:-}" = "run" ] && [ -f package.json ]; then
|
||||
gate_file=$(node -e 'const s=(require("./package.json").scripts||{})[process.argv[1]]||"";const m=s.match(/\S+\.m?js/);process.stdout.write(m?m[0]:"")' "${3:-}" 2>/dev/null)
|
||||
fi
|
||||
if [ -n "$gate_file" ] && [ -f "$gate_file" ]; then
|
||||
declared=$(head -c 16384 "$gate_file" | grep -oE '@browser-slot-max-hold[[:space:]]+[0-9]+' | head -1 | grep -oE '[0-9]+$')
|
||||
if [ -n "$declared" ]; then MAX_HOLD=$declared; [ "$MAX_HOLD" -gt 3600 ] && MAX_HOLD=3600; fi
|
||||
fi
|
||||
fi
|
||||
|
||||
# ★ A SLOT IS HELD FOR AT MOST MAX_HOLD SECONDS, whatever the command does. Measured 2026-09-25: a
|
||||
# probe awaited map.once('load') with no timeout and sat on slot 1 for 89 minutes on 3 s of CPU while
|
||||
# every other agent's browser gate queued behind it and timed out as NOT MEASURED. A wrapper that
|
||||
|
|
|
|||
|
|
@ -51,7 +51,7 @@ mkdir -p "$(dirname "$LOCK_FILE")"
|
|||
# that distinguishes coordinator from subagent; anything claiming to is an untested guess.
|
||||
if [ "${BUILD_ALLOW:-}" != "1" ]; then
|
||||
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 " 461-file phantom deletion and several false 'element did not mount' diagnoses)." >&2
|
||||
echo "" >&2
|
||||
|
|
|
|||
|
|
@ -35,10 +35,11 @@ import { resolve, dirname, join } from 'node:path'
|
|||
import { fileURLToPath } from 'node:url'
|
||||
import { chromium } from 'playwright'
|
||||
import { servesThisTree } from './_serves-this-tree.mjs'
|
||||
import { TREE_NAME } from './_tree-name.mjs'
|
||||
|
||||
const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..')
|
||||
const MANIFEST = join(ROOT, 'public', 'doxa-maps', 'manifest.json')
|
||||
const BASE = process.env.BASE || 'http://127.0.0.1:4173/doxa-maps/'
|
||||
const MANIFEST = join(ROOT, 'public', TREE_NAME, 'manifest.json')
|
||||
const BASE = process.env.BASE || `http://127.0.0.1:4173/${TREE_NAME}/`
|
||||
const ALL = process.env.ALL === '1'
|
||||
|
||||
if (!existsSync(MANIFEST)) {
|
||||
|
|
@ -55,7 +56,7 @@ if (!existsSync(MANIFEST)) {
|
|||
if (!tree.ok) {
|
||||
console.error('A11Y-BUTTON-NAMES: NOT MEASURED — the server is not serving this checkout.')
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@ import { chromium } from 'playwright'
|
|||
import { requireThisTree } from './_serves-this-tree.mjs'
|
||||
import { resolve as _resolvePath, dirname as _dirnamePath } from 'node:path'
|
||||
import { fileURLToPath as _fileURLToPath } from 'node:url'
|
||||
import { TREE_NAME } from './_tree-name.mjs'
|
||||
|
||||
const URL_ = process.argv[2] || process.env.ARIA_URL
|
||||
const ROOT = _resolvePath(_dirnamePath(_fileURLToPath(import.meta.url)), '..')
|
||||
|
|
@ -43,7 +44,7 @@ await requireThisTree(URL_, ROOT, 'ARIA-LABELS-LOCALIZED')
|
|||
if (!URL_) {
|
||||
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(' 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)
|
||||
}
|
||||
const UNDER_TEST = (process.argv[3] || 'es,ar,zh').split(',').map((s) => s.trim()).filter(Boolean)
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@ import { spawn } from 'node:child_process'
|
|||
import { createServer } from 'node:http'
|
||||
import { resolve, dirname } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { TREE_NAME } from './_tree-name.mjs'
|
||||
|
||||
const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..')
|
||||
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 srv = spawn(process.execPath, [resolve(ROOT, 'internal/serve-strict.mjs'), '--port', String(port), '--directory', DIR], { stdio: 'ignore' })
|
||||
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) }
|
||||
|
||||
const browser = await chromium.launch({ args: ['--use-gl=swiftshader', '--enable-webgl', '--ignore-gpu-blocklist'] })
|
||||
async function labels(provider, lang) {
|
||||
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) })()`
|
||||
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) }
|
||||
|
|
|
|||
|
|
@ -34,10 +34,11 @@
|
|||
import { readFileSync, existsSync, readdirSync } from 'node:fs'
|
||||
import { resolve, dirname, join } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { TREE_NAME } from './_tree-name.mjs'
|
||||
|
||||
const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..')
|
||||
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.
|
||||
//
|
||||
|
|
|
|||
|
|
@ -22,11 +22,12 @@ import { readFileSync, existsSync } from 'node:fs'
|
|||
import { execFileSync } from 'node:child_process'
|
||||
import { resolve, dirname, join } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { TREE_NAME } from './_tree-name.mjs'
|
||||
|
||||
const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..')
|
||||
const OUT = join(ROOT, 'documentation', 'reference-capability-exemplars.md')
|
||||
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)) {
|
||||
console.log('CAPABILITY-EXEMPLARS: SKIP — no manifest.json (nothing built yet).')
|
||||
|
|
|
|||
|
|
@ -33,18 +33,19 @@ import { readFileSync, existsSync } from 'node:fs'
|
|||
import { createHash } from 'node:crypto'
|
||||
import { resolve, dirname, join } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { TREE_NAME } from './_tree-name.mjs'
|
||||
|
||||
const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..')
|
||||
const args = process.argv.slice(2)
|
||||
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 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)) {
|
||||
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)
|
||||
}
|
||||
const local = JSON.parse(readFileSync(localManifestPath, 'utf8'))
|
||||
|
|
@ -125,7 +126,7 @@ for (const b of local.bundles || []) {
|
|||
const profile = b.profiles?.[0]?.profile
|
||||
if (!profile) continue
|
||||
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
|
||||
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
|
||||
|
|
@ -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' +
|
||||
' 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' +
|
||||
' 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.'
|
||||
)
|
||||
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}…`)
|
||||
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(' (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)
|
||||
}
|
||||
console.log('CATALOG-CURRENCY: PASS — every local bundle is present live and matches this build.')
|
||||
|
|
|
|||
|
|
@ -34,8 +34,9 @@
|
|||
import { readFileSync, existsSync } from 'node:fs'
|
||||
import { createHash } from 'node:crypto'
|
||||
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 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(` ${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(' 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)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -49,9 +49,10 @@ import { spawn } from 'node:child_process'
|
|||
import { existsSync, readFileSync } from 'node:fs'
|
||||
import { resolve, dirname, join } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { TREE_NAME } from './_tree-name.mjs'
|
||||
|
||||
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)) {
|
||||
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 p of b.profiles || []) {
|
||||
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
|
||||
const m = readFileSync(leaf, 'utf8').match(/BAKED_TOKEN\s*=\s*"(pk\.[^"]+)"/)
|
||||
if (m) return m[1]
|
||||
|
|
@ -109,7 +110,7 @@ const freePort = () => new Promise((ok) => {
|
|||
})
|
||||
const TREE_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>'
|
||||
const tree = spawn(process.execPath, ['internal/serve-strict.mjs', '--port', String(TREE_PORT)], { cwd: ROOT, stdio: 'ignore' })
|
||||
|
|
|
|||
|
|
@ -41,16 +41,17 @@ import { spawnSync } from 'node:child_process'
|
|||
import { tmpdir } from 'node:os'
|
||||
import { resolve, dirname, join } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { TREE_NAME } from './_tree-name.mjs'
|
||||
|
||||
const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..')
|
||||
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 oi = argv.indexOf('--only')
|
||||
const ONLY = oi >= 0 && argv[oi + 1] ? new Set(argv[oi + 1].split(',')) : null
|
||||
|
||||
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)
|
||||
}
|
||||
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`)
|
||||
composites++
|
||||
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 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`)
|
||||
|
|
|
|||
|
|
@ -33,6 +33,7 @@
|
|||
import { readFileSync, existsSync, readdirSync } from 'node:fs'
|
||||
import { resolve, dirname, join } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { TREE_NAME } from './_tree-name.mjs'
|
||||
|
||||
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
|
||||
// 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.
|
||||
let treeName = 'doxa-maps'
|
||||
let treeName = TREE_NAME
|
||||
let treeNameSource = 'built-in default (build config unreadable)'
|
||||
try {
|
||||
const cfg = JSON.parse(readFileSync(join(ROOT, 'vite.doxa-maps-build-config.json'), 'utf8'))
|
||||
|
|
|
|||
|
|
@ -43,6 +43,7 @@ import { createServer } from 'node:http'
|
|||
import { readFileSync, readdirSync, existsSync } from 'node:fs'
|
||||
import { resolve, dirname, join, basename } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { TREE_NAME } from './_tree-name.mjs'
|
||||
|
||||
const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..')
|
||||
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 stopSrv = () => { try { srv.kill() } catch { /* already gone */ } }
|
||||
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) }
|
||||
|
||||
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()
|
||||
let negSeenInOursOnly = null
|
||||
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)
|
||||
const theirs = await read(pg.live)
|
||||
const needH1 = p.requireH1 !== false
|
||||
|
|
|
|||
|
|
@ -36,6 +36,7 @@
|
|||
*/
|
||||
import { resolve, dirname } from 'node:path'
|
||||
import { fileURLToPath, pathToFileURL } from 'node:url'
|
||||
import { TREE_NAME } from './_tree-name.mjs'
|
||||
|
||||
const HERE = dirname(fileURLToPath(import.meta.url))
|
||||
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
|
||||
// expectation cannot drift from what the generated HTML actually means.
|
||||
const TREES = [
|
||||
['live catalog', 'https://nuxtbase.app/plugin-assets/doxa-maps'],
|
||||
['dev tree', 'http://127.0.0.1:4173/doxa-maps'],
|
||||
['doxa.life drop-in', 'https://doxa.life/js/doxa-maps-build/doxa-maps'],
|
||||
['deep subpath', 'https://cdn.example.org/a/b/c/doxa-maps'],
|
||||
['live catalog', `https://nuxtbase.app/plugin-assets/${TREE_NAME}`],
|
||||
['dev tree', `http://127.0.0.1:4173/${TREE_NAME}`],
|
||||
['doxa.life drop-in', `https://doxa.life/js/doxa-maps-build/${TREE_NAME}`],
|
||||
['deep subpath', `https://cdn.example.org/a/b/c/${TREE_NAME}`],
|
||||
]
|
||||
|
||||
const fails = []
|
||||
|
|
|
|||
|
|
@ -33,12 +33,13 @@ import { writeFileSync, mkdtempSync, readFileSync } from 'node:fs'
|
|||
import { resolve, dirname } from 'node:path'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { TREE_NAME } from './_tree-name.mjs'
|
||||
|
||||
const HERE = dirname(fileURLToPath(import.meta.url))
|
||||
const REPO = resolve(HERE, '..')
|
||||
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
|
||||
// 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))
|
||||
|
|
|
|||
|
|
@ -25,13 +25,14 @@ import { createServer } from 'node:http'
|
|||
import { resolve, dirname } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { servesThisTree } from './_serves-this-tree.mjs'
|
||||
import { TREE_NAME } from './_tree-name.mjs'
|
||||
|
||||
const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..')
|
||||
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)
|
||||
|
||||
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) }
|
||||
const treeCheck = await servesThisTree(BASE, ROOT)
|
||||
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
|
||||
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 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>
|
||||
var el = document.createElement(${JSON.stringify(c.tag)});
|
||||
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';
|
||||
document.getElementById('w').appendChild(el);
|
||||
<\/script></body></html>`
|
||||
|
|
|
|||
|
|
@ -7,8 +7,18 @@
|
|||
* Both sets are read from plugin-designs/doxa-maps-spa/doxa-maps-spa.vue, so a bundle that joins or
|
||||
* leaves the shelf changes what this gate measures without an edit here.
|
||||
*
|
||||
* PER BUNDLE, PER ENGINE (mapbox, maplibre), on TWO surfaces: its own leaf page (A) and its tab
|
||||
* inside the Research Gallery (D). A surface passes when:
|
||||
* PER BUNDLE, PER ENGINE (mapbox, maplibre), on FOUR surfaces:
|
||||
* A its own leaf page
|
||||
* B a host page on a DIFFERENT ORIGIN that iframes the leaf, built from the manifest's own
|
||||
* embed.iframe snippet; the host's title, URL and scroll must not change
|
||||
* C a host page on a different origin that pastes the manifest's own embed.script snippet with
|
||||
* the HOST's token and provider filled in. Under maplibre it must make 0 api.mapbox.com
|
||||
* requests; under mapbox, where the plugin draws a Mapbox map, a second run with a fake host
|
||||
* token must show that token in its tile requests and no other (the host's token is used,
|
||||
* not one baked into the bundle). A MapLibre-only plugin takes no token: reported n/a
|
||||
* D its tab inside the Research Gallery
|
||||
* B and C test exactly what a partner pastes, so a broken snippet fails here too.
|
||||
* A surface passes when:
|
||||
* - the element mounted with content (its shadow tree has real nodes),
|
||||
* - 0 page errors and 0 failed requests (4xx/5xx; Mapbox telemetry and favicons excluded),
|
||||
* - no loading text is left on screen (Loading / Adding / Computing),
|
||||
|
|
@ -36,17 +46,22 @@ import { resolve, dirname } from 'node:path'
|
|||
import { fileURLToPath } from 'node:url'
|
||||
import { spawn } from 'node:child_process'
|
||||
import { createServer } from 'node:net'
|
||||
import { createServer as createHttpServer } from 'node:http'
|
||||
import { TREE_NAME } from './_tree-name.mjs'
|
||||
|
||||
const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..')
|
||||
const argv = process.argv.slice(2)
|
||||
const arg = (n, d) => { const i = argv.indexOf(n); return i >= 0 && argv[i + 1] ? argv[i + 1] : d }
|
||||
const ENGINES = arg('--engines', 'mapbox,maplibre').split(',')
|
||||
const ONLY = arg('--only', '') ? arg('--only', '').split(',') : null
|
||||
// --surfaces A,B,C,D (default all). The full shelf is ~60 page loads, ~20 min under SwiftShader:
|
||||
// run it with BROWSER_MAX_HOLD raised, or split it by --only / --surfaces.
|
||||
const SURFACES = arg('--surfaces', 'A,B,C,D').split(',')
|
||||
const SELF_TEST = !argv.includes('--no-self-test')
|
||||
const WAIT_MS = Number(process.env.SHELF_WAIT_MS || 75000)
|
||||
|
||||
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 cfg = JSON.parse(readFileSync(resolve(ROOT, 'vite.doxa-maps-build-config.json'), 'utf8'))
|
||||
const TK = process.env.MAPBOX_TOKEN || cfg.token || ''
|
||||
|
|
@ -81,9 +96,42 @@ 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 reap = () => { try { srv.kill() } catch (_) {} }
|
||||
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)) }
|
||||
|
||||
// ── the PARTNER: a host site on a different origin (localhost vs 127.0.0.1) ────────────────
|
||||
// Its pages are built from the manifest's own embed snippets, placeholders filled the way the
|
||||
// snippet's comments tell a partner to.
|
||||
const byBundle = Object.fromEntries(manifest.bundles.map(x => [x.bundle, x]))
|
||||
const snippet = (bundle, profile, kind) => ((byBundle[bundle].profiles.find(x => x.profile === profile) || {}).embed || {})[kind] || ''
|
||||
function hostPage(kind, s, eng, token) {
|
||||
let body
|
||||
if (kind === 'B') {
|
||||
const leafUrl = `${BASE}/${s.bundle}/${s.profile}/index.html?provider=${eng}` + (eng === 'mapbox' ? `&tk=${token}` : '')
|
||||
body = snippet(s.bundle, s.profile, 'iframe').replace(/src="[^"]*"/, `src="${leafUrl}&utm_source=shelf"`).replace(/height="\d+"/, 'height="720"')
|
||||
} else {
|
||||
body = snippet(s.bundle, s.profile, 'script')
|
||||
.replaceAll('src="./', `src="${BASE}/`).replaceAll('href="./', `href="${BASE}/`)
|
||||
.replaceAll('YOUR_MAPBOX_TOKEN', token).replaceAll('YOUR_SOURCE', 'shelf').replaceAll('YOUR_TREE_URL', BASE)
|
||||
if (eng === 'maplibre') body = body.replace(/<!--[^>]*remove this line[^>]*-->\s*<link[^>]*>\s*/i, '')
|
||||
.replace(/profile-config='\{/, `profile-config='{"provider":"maplibre",`)
|
||||
}
|
||||
return `<!DOCTYPE html><html><head><meta charset="utf-8"><title>PARTNER ${kind}</title>
|
||||
<style>body{margin:0;font:14px sans-serif}header{height:64px;background:#eee;padding:8px}main>*{display:block;width:100%;height:720px;position:relative}</style></head>
|
||||
<body><header>A partner's own site</header><main>${body}</main></body></html>`
|
||||
}
|
||||
const FAKE_TK = 'pk.SHELFHOSTTOKEN' + 'x'.repeat(20)
|
||||
const hostSrv = createHttpServer((req, res) => {
|
||||
const m = req.url.match(/^\/(B|C)\/([a-z0-9-]+)\/(mapbox|maplibre)(\/fake)?/)
|
||||
const s = m && shelf.find(x => x.bundle === m[2])
|
||||
if (!s) { res.writeHead(404); return res.end('no') }
|
||||
res.writeHead(200, { 'content-type': 'text/html; charset=utf-8' })
|
||||
res.end(hostPage(m[1], s, m[3], m[4] ? FAKE_TK : TK))
|
||||
})
|
||||
const hostPort = await new Promise(ok => hostSrv.listen(0, '127.0.0.1', () => ok(hostSrv.address().port)))
|
||||
const HOST = `http://localhost:${hostPort}`
|
||||
process.on('exit', () => { try { hostSrv.close() } catch (_) {} })
|
||||
|
||||
// ── every Map the page builds, recorded without touching the bundle ───────────
|
||||
const RECORD_MAPS = () => {
|
||||
window.__shelfMaps = []
|
||||
|
|
@ -121,14 +169,14 @@ const PROBE = (tag) => `(() => {
|
|||
const layers = (st.layers || []).filter(l => geoIds.includes(l.source)).map(l => l.id)
|
||||
if (layers.length) drawn = m.queryRenderedFeatures({ layers }).length
|
||||
} catch (_) {}
|
||||
return { loaded: !!(m.__shelfLoaded || (m.isStyleLoaded && m.isStyleLoaded())), geo, drawn }
|
||||
return { loaded: !!(m.__shelfLoaded || (m.isStyleLoaded && m.isStyleLoaded())), geo, drawn, mapbox: !!(window.mapboxgl && m instanceof window.mapboxgl.Map) }
|
||||
})
|
||||
return { own, sized, host: !!host, busy, maps }
|
||||
})()`.replace('__TAG__', tag)
|
||||
|
||||
const b = await chromium.launch({ headless: true, args: ['--use-gl=swiftshader', '--enable-unsafe-swiftshader', '--ignore-gpu-blocklist', '--enable-webgl', '--no-sandbox'] })
|
||||
|
||||
async function measure(url, s, poisonBundle) {
|
||||
async function measure(url, s, poisonBundle, opts = {}) {
|
||||
const p = await b.newPage({ viewport: { width: 1280, height: 800 } })
|
||||
await p.addInitScript(RECORD_MAPS)
|
||||
const seed = SEED[s.bundle]
|
||||
|
|
@ -137,27 +185,41 @@ async function measure(url, s, poisonBundle) {
|
|||
Storage.prototype.getItem = function (k) { const v = get.call(this, k); return (v == null && String(k).startsWith(keyPrefix)) ? value : v }
|
||||
}, seed)
|
||||
if (poisonBundle) await p.route(new RegExp(`/${poisonBundle}/${poisonBundle}\\.js(\\?|$)`), r => r.fulfill({ status: 200, contentType: 'text/javascript', body: 'throw new Error("shelf self-test: deliberately broken bundle")' }))
|
||||
const errs = [], fails = []
|
||||
const errs = [], fails = [], mapboxReqs = [], tokens = new Set()
|
||||
p.on('request', r => { const u = r.url(); if (/api\.mapbox\.com/.test(u)) mapboxReqs.push(u.replace(/\?.*/, '').slice(-60)); const t = u.match(/access_token=([^&]+)/); if (t) tokens.add(t[1]) })
|
||||
p.on('pageerror', e => errs.push(e.message.slice(0, 140)))
|
||||
p.on('response', r => { if (r.status() >= 400 && !/events\.mapbox\.com|favicon/.test(r.url())) fails.push(r.status() + ' ' + r.url().replace(/\?.*/, '').slice(-80)) })
|
||||
await p.goto(url)
|
||||
const host0 = opts.frame ? await p.evaluate(() => ({ title: document.title, href: location.href, y: scrollY })) : null
|
||||
// B: the plugin lives in the iframe whose document comes from the tree
|
||||
const ctx = () => (opts.frame ? p.frames().find(f => f.url().startsWith(BASE)) : p.mainFrame()) || p.mainFrame()
|
||||
let st = null
|
||||
const t0 = Date.now()
|
||||
while (Date.now() - t0 < WAIT_MS) {
|
||||
// provenance needs one tile request, not a drawn map: a fake token cannot draw anything
|
||||
while (Date.now() - t0 < (opts.provenanceOnly ? Math.min(WAIT_MS, 25000) : WAIT_MS)) {
|
||||
await p.waitForTimeout(2500)
|
||||
st = await p.evaluate(PROBE(s.tag))
|
||||
try { st = await ctx().evaluate(PROBE(s.tag)) } catch (_) { st = null; continue }
|
||||
const mapsReady = st.maps.every(m => m.loaded && m.drawn > 0)
|
||||
if (st.own > 8 && st.sized && st.busy === 0 && mapsReady && Date.now() - t0 > 6000) break
|
||||
if (opts.provenanceOnly ? tokens.has(FAKE_TK) : (st.own > 8 && st.sized && st.busy === 0 && mapsReady && Date.now() - t0 > 6000)) break
|
||||
}
|
||||
const host1 = opts.frame ? await p.evaluate(() => ({ title: document.title, href: location.href, y: scrollY })) : null
|
||||
await p.close()
|
||||
if (opts.provenanceOnly) {
|
||||
// C provenance: with a FAKE host token the tiles cannot load, so only one thing is asserted:
|
||||
// the requests carry the HOST's token, not a token baked into the bundle.
|
||||
const used = tokens.has(FAKE_TK), baked = [...tokens].filter(t => t !== FAKE_TK)
|
||||
return { pass: used && !baked.length, why: used ? (baked.length ? [`a token other than the host's was also used (${baked[0].slice(0, 14)}…)`] : []) : ['the host token never reached a tile request'], maps: 0 }
|
||||
}
|
||||
const why = []
|
||||
if (!st || !st.host) why.push(`<${s.tag}> never appeared`)
|
||||
else if (st.own <= 8 || !st.sized) why.push(`<${s.tag}> mounted empty (${st.own} node(s)${st.sized ? '' : ', not sized'})`)
|
||||
if (st && st.busy) why.push(`${st.busy} loading label(s) left`)
|
||||
if (st) st.maps.forEach((m, i) => { if (!m.loaded) why.push(`map ${i + 1}: 'load' never fired`); else if (!m.drawn) why.push(`map ${i + 1}: no own data drawn (${m.geo} geojson source(s))`) })
|
||||
if (host0 && host1 && (host0.title !== host1.title || host0.href !== host1.href || host0.y !== host1.y)) why.push(`the host page changed (title/URL/scroll): ${JSON.stringify(host0)} -> ${JSON.stringify(host1)}`)
|
||||
if (opts.noMapbox && mapboxReqs.length) why.push(`${mapboxReqs.length} api.mapbox.com request(s) under maplibre, e.g. ${mapboxReqs[0]}`)
|
||||
if (errs.length) why.push(`page error: ${errs[0]}`)
|
||||
if (fails.length) why.push(`failed request: ${fails[0]}`)
|
||||
return { pass: !why.length, why, maps: st ? st.maps.length : 0 }
|
||||
return { pass: !why.length, why, maps: st ? st.maps.length : 0, mapboxMaps: st ? st.maps.filter(m => m.mapbox).length : 0 }
|
||||
}
|
||||
|
||||
const leaf = (s, eng) => `${BASE}/${s.bundle}/${s.profile}/index.html?provider=${eng}` + (eng === 'mapbox' ? `&tk=${TK}` : '')
|
||||
|
|
@ -168,18 +230,38 @@ try {
|
|||
if (SELF_TEST) {
|
||||
const s = shelf[0]
|
||||
const a = await measure(leaf(s, 'maplibre'), s, s.bundle), d = await measure(gallery(s, 'maplibre'), s, s.bundle)
|
||||
if (a.pass || d.pass) { console.error(`SELF-TEST FAILED: a deliberately broken ${s.bundle} still passed (${a.pass ? 'leaf' : ''} ${d.pass ? 'gallery' : ''}). This gate cannot see a broken bundle.`); exit = 3 }
|
||||
else console.log(`self-test: a deliberately broken ${s.bundle} fails as it must (leaf: ${a.why[0]}; gallery: ${d.why[0]})`)
|
||||
const c = await measure(`${HOST}/C/${s.bundle}/maplibre`, s, s.bundle, { noMapbox: true })
|
||||
if (a.pass || d.pass || c.pass) { console.error(`SELF-TEST FAILED: a deliberately broken ${s.bundle} still passed (${a.pass ? 'leaf ' : ''}${d.pass ? 'gallery ' : ''}${c.pass ? 'script-embed' : ''}). This gate cannot see a broken bundle.`); exit = 3 }
|
||||
else console.log(`self-test: a deliberately broken ${s.bundle} fails as it must (leaf: ${a.why[0]}; script-embed: ${c.why[0]}; gallery: ${d.why[0]})`)
|
||||
}
|
||||
if (exit !== 3) {
|
||||
for (const [k, v] of Object.entries(SEED)) if (shelf.some(s => s.bundle === k)) console.log(`SEEDED ${k}: ${v.why}`)
|
||||
console.log(`EXPERIMENTAL SHELF — ${shelf.length} bundle(s): ${shelf.map(s => s.bundle).join(', ')} · engines: ${ENGINES.join(', ')}`)
|
||||
for (const s of shelf) for (const eng of ENGINES) for (const [surf, url] of [['A leaf', leaf(s, eng)], ['D gallery', gallery(s, eng)]]) {
|
||||
const r = await measure(url, s)
|
||||
// B does NOT assert 0 api.mapbox.com: the iframe loads the LEAF page, and every generated leaf
|
||||
// <head> links Mapbox's JS/CSS statically whatever the provider (generate-tree's template).
|
||||
// That is a fleet-wide leaf property, reported separately; C is where a partner chooses.
|
||||
const row = (r, s, eng, surf, t0) => {
|
||||
if (!r.pass) exit = 1
|
||||
console.log(`${r.pass ? 'PASS' : 'FAIL'} ${s.bundle.padEnd(24)} ${eng.padEnd(9)} ${surf.padEnd(10)} ${r.maps ? r.maps + ' map(s)' : 'no map'}${r.pass ? '' : ' — ' + r.why.join('; ')}`)
|
||||
console.log(`${r.pass ? 'PASS' : 'FAIL'} ${s.bundle.padEnd(24)} ${eng.padEnd(9)} ${surf.padEnd(10)} ${(r.maps ? r.maps + ' map(s)' : 'no map').padEnd(9)} ${String(Math.round((Date.now() - t0) / 1000)).padStart(3)}s${r.pass ? '' : ' — ' + r.why.join('; ')}`)
|
||||
}
|
||||
console.log(exit ? 'EXPERIMENTAL-SHELF: FAIL' : `EXPERIMENTAL-SHELF: PASS — every experimental plugin loads on its own page and in the Gallery, under ${ENGINES.join(' and ')}`)
|
||||
for (const s of shelf) for (const eng of ENGINES) for (const [surf, url, opts] of [
|
||||
['A leaf', leaf(s, eng), {}],
|
||||
['B iframe', `${HOST}/B/${s.bundle}/${eng}`, { frame: true }],
|
||||
['C script', `${HOST}/C/${s.bundle}/${eng}`, { noMapbox: eng === 'maplibre' }],
|
||||
['D gallery', gallery(s, eng), {}],
|
||||
].filter(x => SURFACES.includes(x[0][0]))) {
|
||||
let t0 = Date.now()
|
||||
const r = await measure(url, s, null, opts)
|
||||
row(r, s, eng, surf, t0)
|
||||
// C host-tk: only where the script embed really made a MAPBOX map. A MapLibre-only plugin
|
||||
// (prayer-walk-planner) takes no token at all, so there is nothing to prove.
|
||||
if (surf === 'C script' && eng === 'mapbox' && r.pass) {
|
||||
if (!r.mapboxMaps) { console.log(`n/a ${s.bundle.padEnd(24)} ${eng.padEnd(9)} C host-tk — no Mapbox map on this plugin, it takes no token`); continue }
|
||||
t0 = Date.now()
|
||||
row(await measure(`${HOST}/C/${s.bundle}/mapbox/fake`, s, null, { provenanceOnly: true }), s, eng, 'C host-tk', t0)
|
||||
}
|
||||
}
|
||||
console.log(exit ? 'EXPERIMENTAL-SHELF: FAIL' : `EXPERIMENTAL-SHELF: PASS — every experimental plugin works on its own page, iframed and script-embedded on another site, and in the Gallery, under ${ENGINES.join(' and ')}`)
|
||||
}
|
||||
} finally { await b.close(); reap() }
|
||||
process.exit(exit)
|
||||
|
|
|
|||
|
|
@ -40,9 +40,10 @@ import { dirname, resolve } from 'node:path'
|
|||
import { fileURLToPath } from 'node:url'
|
||||
import { chromium } from 'playwright'
|
||||
import { servesThisTree } from './_serves-this-tree.mjs'
|
||||
import { TREE_NAME } from './_tree-name.mjs'
|
||||
|
||||
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
|
||||
// the default port belongs to whichever server started first — measured 2026-09-24: :4173 was
|
||||
|
|
|
|||
|
|
@ -63,6 +63,7 @@ import { spawn, execFileSync } from 'node:child_process'
|
|||
import { fileURLToPath } from 'node:url'
|
||||
import { dirname, resolve } from 'node:path'
|
||||
import { readFileSync, existsSync } from 'node:fs'
|
||||
import { TREE_NAME } from './_tree-name.mjs'
|
||||
|
||||
const __dirname = fileURLToPath(new URL('.', import.meta.url))
|
||||
const ROOT = resolve(__dirname, '..')
|
||||
|
|
@ -109,7 +110,7 @@ if (!apiReachable) {
|
|||
}
|
||||
|
||||
// ── 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)) {
|
||||
console.log('FITS-THE-SCREEN: SKIP — no manifest.json (nothing built).')
|
||||
process.exit(0)
|
||||
|
|
@ -183,7 +184,7 @@ const browser = await chromium.launch({ args: GL_ARGS })
|
|||
async function runComposite() {
|
||||
const TARGET = 'dashboard-plus-grid'
|
||||
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.waitForTimeout(6000)
|
||||
await reinject(page)
|
||||
|
|
@ -613,7 +614,7 @@ async function runShellBestEffort() {
|
|||
return
|
||||
}
|
||||
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
|
||||
const page = await browser.newPage({ viewport: { width: 1440, height: 900 } })
|
||||
try {
|
||||
|
|
@ -651,8 +652,8 @@ async function runShellBestEffort() {
|
|||
let runError = null
|
||||
try {
|
||||
await runComposite()
|
||||
await runResponsiveSweep(`${BASE}/doxa-maps/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}/dashboard-plus-grid/dashboard-plus-grid/index.html`, 'dashboard-plus-grid')
|
||||
await runResponsiveSweep(`${BASE}/${TREE_NAME}/research-dashboard/research-dashboard/index.html`, 'research-dashboard')
|
||||
await runShellBestEffort()
|
||||
} catch (e) {
|
||||
runError = e
|
||||
|
|
|
|||
|
|
@ -37,8 +37,9 @@ import { chromium } from 'playwright'
|
|||
import { requireThisTree } from './_serves-this-tree.mjs'
|
||||
import { resolve as _resolvePath, dirname as _dirnamePath } from 'node:path'
|
||||
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)), '..')
|
||||
|
||||
// ★ MEASURE THIS BUILD, OR MEASURE NOTHING. Several worktrees run on one machine and a default port
|
||||
|
|
|
|||
|
|
@ -41,6 +41,8 @@ const BROWSER = [
|
|||
['check:leaves-mount', 'all five leaves actually mount'],
|
||||
['check:leaves-mount:poison', '...and that check can fail'],
|
||||
['check:composed-filters', 'the composite filters instead of navigating'],
|
||||
['check:map-paints', 'the map puts pixels on the screen, not just features'],
|
||||
['check:map-paints:poison', '...and that check can fail'],
|
||||
]
|
||||
|
||||
const run = (script, viaSlot) => {
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@ import { serveReaped } from './_serve-reaped.mjs'
|
|||
import { resolve, dirname } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { requireThisTree } from './_serves-this-tree.mjs'
|
||||
import { TREE_NAME } from './_tree-name.mjs'
|
||||
|
||||
const HERE = dirname(fileURLToPath(import.meta.url))
|
||||
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 ✗.
|
||||
// A file:// target passes straight through — there is no server there to be wrong about.
|
||||
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
|
||||
if (!process.env.BASE_URL) srv = serveReaped(PUBLIC, PORT)
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ import { readFileSync } from 'node:fs'
|
|||
import { resolve, dirname } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { serveReaped } from './_serve-reaped.mjs'
|
||||
import { TREE_NAME } from './_tree-name.mjs'
|
||||
|
||||
const HERE = dirname(fileURLToPath(import.meta.url))
|
||||
const REPO = resolve(HERE, '..')
|
||||
|
|
@ -53,7 +54,7 @@ try {
|
|||
for (const lc of LOCALES) {
|
||||
const loc = Object.fromEntries(flat(cat(lc)))
|
||||
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 })
|
||||
await page.waitForTimeout(8000)
|
||||
/* ═══ READ ONLY THE CHROME, NOT THE DECLARED-ENGLISH PROSE ═══════════════════════
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@ import { chromium } from 'playwright'
|
|||
import { serveReaped } from './_serve-reaped.mjs'
|
||||
import { resolve, dirname } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { TREE_NAME } from './_tree-name.mjs'
|
||||
|
||||
const HERE = dirname(fileURLToPath(import.meta.url))
|
||||
const PUBLIC = resolve(HERE, '../public')
|
||||
|
|
@ -80,7 +81,7 @@ try {
|
|||
|
||||
if (POISON) {
|
||||
/* 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)
|
||||
if (!f.length) {
|
||||
console.error('LEAVES-MOUNT: SELF-TEST FAIL — a page with no gospel element passed as mounted.')
|
||||
|
|
@ -93,7 +94,7 @@ try {
|
|||
|
||||
const bad = []
|
||||
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)
|
||||
bad.push(...f)
|
||||
console.log(` ${f.length ? 'FAIL' : 'ok '} ${leaf.padEnd(18)} defined=${r.defined} shadow=${r.hasShadow} children=${r.shadowChildren} text=${r.visibleTextLength}`)
|
||||
|
|
|
|||
147
internal/check-gospel-map-paints.mjs
Normal file
147
internal/check-gospel-map-paints.mjs
Normal file
|
|
@ -0,0 +1,147 @@
|
|||
/**
|
||||
* check-gospel-map-paints.mjs — the map must put PIXELS on the screen, not just features.
|
||||
*
|
||||
* THE FAILURE THIS EXISTS FOR, which I shipped into my own tree and nearly landed. Injecting
|
||||
* mapbox-gl.css into this profile's shadow root was correct, and it collapsed the canvas from
|
||||
* 1440x900 to 1440x300 because .mapboxgl-map carries position:relative and cancelled the
|
||||
* container's absolute box. The result was a WHITE RECTANGLE. And every assertion I had went
|
||||
* green over it:
|
||||
* - verify-gospel-map : 2,107 of 2,107 pins "rendered"
|
||||
* - check:leaves-mount : element defined, shadow root full of text
|
||||
* - the i18n gates : every label translated, nothing clipped
|
||||
* queryRenderedFeatures does not care whether anything is visible, and neither does text. I
|
||||
* caught it by opening the screenshot, which is not a system.
|
||||
*
|
||||
* SO THIS ASKS THE ONE QUESTION NONE OF THEM ASK: is there a map there? It screenshots the
|
||||
* map element itself and counts distinct colours, the same technique check-embed-basemap
|
||||
* uses in an iframe — a basemap with land, water, borders and pins has hundreds; a blank
|
||||
* rectangle has one or two. It also checks the canvas fills its container, because a
|
||||
* correctly-coloured 300px strip in a 900px box is the exact defect that started this.
|
||||
*
|
||||
* Run with --poison to prove it can fail: it hides the canvas and requires a red verdict.
|
||||
*/
|
||||
import { chromium } from 'playwright'
|
||||
import { serveReaped } from './_serve-reaped.mjs'
|
||||
import { resolve, dirname } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { TREE_NAME } from './_tree-name.mjs'
|
||||
|
||||
const HERE = dirname(fileURLToPath(import.meta.url))
|
||||
const REPO = resolve(HERE, '..')
|
||||
const PORT = Number(process.env.PORT || 5399)
|
||||
const BASE = process.env.BASE_URL || ('http://127.0.0.1:' + PORT)
|
||||
const POISON = process.argv.includes('--poison')
|
||||
const MIN_COLOURS = Number(process.env.MIN_MAP_COLOURS || 40)
|
||||
const MIN_FILL = 0.7
|
||||
|
||||
let srv = null
|
||||
if (!process.env.BASE_URL) srv = serveReaped(resolve(REPO, 'public'), PORT)
|
||||
|
||||
const countColours = async (page, b64) => page.evaluate(async (data) => {
|
||||
const img = new Image()
|
||||
img.src = 'data:image/png;base64,' + data
|
||||
await img.decode()
|
||||
const c = document.createElement('canvas')
|
||||
c.width = img.width; c.height = img.height
|
||||
const ctx = c.getContext('2d')
|
||||
ctx.drawImage(img, 0, 0)
|
||||
const d = ctx.getImageData(0, 0, c.width, c.height).data
|
||||
const seen = new Set()
|
||||
for (let i = 0; i < d.length; i += 4) seen.add(((d[i] >> 3) << 10) | ((d[i+1] >> 3) << 5) | (d[i+2] >> 3))
|
||||
return seen.size
|
||||
}, b64)
|
||||
|
||||
let browser
|
||||
try {
|
||||
browser = await chromium.launch()
|
||||
const page = await browser.newPage({ viewport: { width: 1440, height: 900 } })
|
||||
await page.goto(BASE + `/${TREE_NAME}/gospel-resources/resources-map/index.html`,
|
||||
{ waitUntil: 'networkidle', timeout: 90000 })
|
||||
await page.waitForFunction(() => window.__grState && window.__grState().pins > 0, null, { timeout: 60000 })
|
||||
await page.waitForTimeout(9000)
|
||||
|
||||
if (POISON) {
|
||||
/* Reproduce the REAL defect rather than an approximation of it. Hiding the canvas was
|
||||
the first attempt and it did not work: the region still showed 316 colours, because a
|
||||
hidden GL canvas is not the same as an empty rectangle and other chrome sits in the
|
||||
frame. What actually happened was a stylesheet collapsing the container to 300px in a
|
||||
900px box, so that is what this injects — and the fill assertion is the arm that
|
||||
should catch it. A poison that does not mirror the incident tests the wrong thing. */
|
||||
await page.evaluate(() => {
|
||||
const sr = document.querySelector('gospel-resources-map').shadowRoot
|
||||
const map = sr.querySelector('.gm-map')
|
||||
if (map) { map.style.position = 'relative'; map.style.height = '300px'; map.style.inset = 'auto' }
|
||||
const cv = sr.querySelector('canvas')
|
||||
if (cv) { cv.style.height = '300px' }
|
||||
})
|
||||
await page.waitForTimeout(800)
|
||||
}
|
||||
|
||||
/* THE RATIO THAT MATTERS IS MAP-TO-HOST, NOT CANVAS-TO-MAP. The first version compared the
|
||||
canvas to its own container, and the poison proved that useless: in the real incident the
|
||||
stylesheet collapsed the CONTAINER and the canvas dutifully followed, so canvas/container
|
||||
stayed 1.0 and the check would have passed the very defect it was written for. What
|
||||
actually went wrong is that the map stopped filling the ELEMENT, so that is the
|
||||
measurement. */
|
||||
const geom = await page.evaluate(() => {
|
||||
const el = document.querySelector('gospel-resources-map')
|
||||
const sr = el.shadowRoot
|
||||
const map = sr.querySelector('.gm-map')
|
||||
const cv = sr.querySelector('canvas')
|
||||
const eb = el.getBoundingClientRect()
|
||||
const mb = map ? map.getBoundingClientRect() : null
|
||||
const cb = cv ? cv.getBoundingClientRect() : null
|
||||
return {
|
||||
host: Math.round(eb.width) + 'x' + Math.round(eb.height),
|
||||
container: mb ? Math.round(mb.width) + 'x' + Math.round(mb.height) : 'none',
|
||||
canvas: cb ? Math.round(cb.width) + 'x' + Math.round(cb.height) : 'none',
|
||||
fill: mb && eb.height ? mb.height / eb.height : 0,
|
||||
}
|
||||
})
|
||||
|
||||
/* ═══ SAMPLE THE MAP, NOT THE PANEL OVER IT ═════════════════════════════════════════
|
||||
* The first version screenshotted the whole custom element and counted 427 colours with
|
||||
* the canvas HIDDEN — because the legend card is itself full of colour and sits on top.
|
||||
* The poison arm caught that, which is the entire reason a gate carries one: a check that
|
||||
* measures the wrong rectangle passes for the wrong reason. The legend occupies the left
|
||||
* edge, so the sample is taken from the right of it and looks at map alone. */
|
||||
const clip = await page.evaluate(() => {
|
||||
const sr = document.querySelector('gospel-resources-map').shadowRoot
|
||||
const map = sr.querySelector('.gm-map') || sr.host
|
||||
const legend = sr.querySelector('.gm-legend')
|
||||
const m = map.getBoundingClientRect()
|
||||
const left = legend ? legend.getBoundingClientRect().right + 12 : m.left
|
||||
return { x: Math.round(left), y: Math.round(m.top), width: Math.max(8, Math.round(m.right - left)), height: Math.max(8, Math.round(m.height)) }
|
||||
})
|
||||
const shot = await page.screenshot({ clip })
|
||||
const colours = await countColours(page, shot.toString('base64'))
|
||||
|
||||
console.log(' host ' + geom.host + ' · map ' + geom.container + ' · canvas ' + geom.canvas +
|
||||
' · map fills ' + Math.round(geom.fill * 100) + '% of the element')
|
||||
console.log(' distinct colours on screen: ' + colours + ' (floor ' + MIN_COLOURS + ')')
|
||||
|
||||
const problems = []
|
||||
if (colours < MIN_COLOURS) problems.push('only ' + colours + ' distinct colours — this is a blank rectangle, not a map')
|
||||
if (geom.fill < MIN_FILL) problems.push('the map fills only ' + Math.round(geom.fill * 100) + '% of the element height; a stylesheet has collapsed it')
|
||||
|
||||
if (POISON) {
|
||||
if (!problems.length) {
|
||||
console.error('MAP-PAINTS: SELF-TEST FAIL — a collapsed map passed as a painted one.')
|
||||
process.exitCode = 1
|
||||
} else {
|
||||
console.log('MAP-PAINTS --poison: caught it — ' + problems[0])
|
||||
console.log('MAP-PAINTS: SELF-TEST PASS — the check fails as designed.')
|
||||
}
|
||||
} else if (problems.length) {
|
||||
console.error('MAP-PAINTS: FAIL — ' + problems.length + ' problem(s):')
|
||||
for (const p of problems) console.error(' ' + p)
|
||||
console.error(' Pin counts and rendered text cannot see this: queryRenderedFeatures')
|
||||
console.error(' answers happily over a white rectangle. Open the screenshot.')
|
||||
process.exitCode = 1
|
||||
} else {
|
||||
console.log('MAP-PAINTS: PASS — the map paints, and the canvas fills its container.')
|
||||
}
|
||||
} finally {
|
||||
await browser?.close()
|
||||
srv?.stop()
|
||||
}
|
||||
|
|
@ -28,8 +28,10 @@
|
|||
*
|
||||
* USAGE node internal/check-host-locale-contract.mjs [--dir <tree root>] [--only bundle/profile,…]
|
||||
* Serves THIS checkout's public/ on an OS-assigned port, or --dir (another commit's tree, for the
|
||||
* before/after). Run through internal/browser-slot.sh; the FULL run (35 leaves x 2 loads) can pass
|
||||
* the slot's 900 s default hold, so give it BROWSER_MAX_HOLD=1800. --only runs fit easily.
|
||||
* before/after). Run through internal/browser-slot.sh. A FULL run loads every leaf twice and
|
||||
* passed the slot's 900 s default three times, so this gate declares its own limit, which the
|
||||
* wrapper reads from this header:
|
||||
* @browser-slot-max-hold 2700
|
||||
* EXIT 0 every judged leaf enters the host's locale · 1 one does not · 75 not measured.
|
||||
*/
|
||||
import { chromium } from 'playwright'
|
||||
|
|
@ -38,6 +40,7 @@ import { createServer } from 'node:http'
|
|||
import { readFileSync, existsSync } from 'node:fs'
|
||||
import { resolve, dirname, join } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { TREE_NAME } from './_tree-name.mjs'
|
||||
|
||||
const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..')
|
||||
const argv = process.argv.slice(2)
|
||||
|
|
@ -48,14 +51,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 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) }
|
||||
const manifest = JSON.parse(readFileSync(manifestPath, 'utf8'))
|
||||
const leaves = []
|
||||
for (const b of manifest.bundles || []) for (const p of b.profiles || []) {
|
||||
const id = `${b.bundle}/${p.profile}`
|
||||
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) }
|
||||
|
||||
|
|
@ -63,7 +66,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 stop = () => { try { srv.kill() } catch { /* already gone */ } }
|
||||
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) }
|
||||
|
||||
const COUNT = `(() => {
|
||||
|
|
@ -85,7 +88,7 @@ const browser = await chromium.launch({ args: ['--use-gl=swiftshader', '--enable
|
|||
async function measure(id, mode) {
|
||||
const ctx = await browser.newContext({ viewport: { width: 1280, height: 900 } })
|
||||
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'
|
||||
if (mode === 'host') {
|
||||
await page.route(url, async (route) => {
|
||||
|
|
|
|||
|
|
@ -48,6 +48,32 @@ const BASE = resolve(REPO, 'library/i18n/locales')
|
|||
const ALLOWED = new Map([
|
||||
['legend.json:progress.segment',
|
||||
'pure interpolation — "{label} · {count} · {pct}%" contains no prose to translate'],
|
||||
// ── PROPER NOUNS ────────────────────────────────────────────────────────────
|
||||
// Names of ORGANISATIONS that publish these resources, not descriptions of them. A reader
|
||||
// following the link meets the English name on the destination site, so translating it would
|
||||
// hide the thing it identifies. They sit in the same res.* block as `jesusFilm: 'JESUS Film'`,
|
||||
// which the product already treats as a brand, and resources-list.vue:234 maps the literal
|
||||
// string 'Scripture Earth' back to this key — it is an identity, not a label.
|
||||
['gospel.json:res.bibleStories',
|
||||
'proper noun — Open Bible Stories is the publication\'s name (unfoldingWord), not a description'],
|
||||
['gospel.json:res.scriptureEarth',
|
||||
'proper noun — Scripture Earth is the organisation\'s name; the destination site is English'],
|
||||
// ── DRIVER-GATED, NOT RESOLVED ──────────────────────────────────────────────
|
||||
// ⚠ THESE ARE ALLOWED SO THE GATE CAN GO GREEN ON THE THINGS IT HAS ACTUALLY SETTLED, NOT
|
||||
// because the defect is fixed. "UUPGs" really is an English acronym wedged into an otherwise
|
||||
// localized row, in Arabic and Chinese included, while `population` beside it translates
|
||||
// normally and the packet renders the same concept in prose in every locale.
|
||||
// It is gated on TERMINOLOGY ONLY — measured: the column's own tooltip (legend.sortByUpgs) is
|
||||
// already translated in all 11 with just the acronym left English, and the widest candidate
|
||||
// phrase is 28% of the 380px panel, so neither engineering nor space is the blocker. Drafting
|
||||
// an acronym produced invented initialisms (PNA, UEVG, ННГ, GNA) with no source, and a wrong
|
||||
// term looks AUTHORITATIVE to a reader who cannot check it.
|
||||
// See DECIDE-UUPGS-COLUMN-HEADER-IS-ENGLISH-IN-ALL-ELEVEN-LOCALES, which carries the priced
|
||||
// options. REMOVE THESE TWO LINES when the Driver rules — they are a bookmark, not a verdict.
|
||||
['legend.json:columns.upgs',
|
||||
'DRIVER-GATED: needs the field\'s term per locale, not an invented acronym — see DECIDE-UUPGS-COLUMN-HEADER'],
|
||||
['prayerLaps.json:upgs',
|
||||
'DRIVER-GATED: same term as legend.json:columns.upgs — see DECIDE-UUPGS-COLUMN-HEADER'],
|
||||
])
|
||||
|
||||
// Below this many locales the comparison is not meaningful — a key present in two files being the
|
||||
|
|
@ -129,5 +155,14 @@ if (findings.length) {
|
|||
console.error(' cannot check it, which is worse than an honest English acronym.')
|
||||
process.exit(1)
|
||||
}
|
||||
// ★ "ALLOWED" IS NOT ONE THING, and collapsing the two would make this gate lie. A proper noun
|
||||
// is SETTLED — it is correct English forever. A DRIVER-GATED key is an OPEN DEFECT parked so the
|
||||
// gate can still catch the next regression; calling it "identical on purpose" would turn a
|
||||
// deferral into a verdict, which is exactly how an open item stops being visible.
|
||||
const deferred = allowedHits.filter((a) => /DRIVER-GATED/.test(a.why))
|
||||
if (deferred.length) {
|
||||
console.log(`\n ⚠ ${deferred.length} DEFERRED, NOT SETTLED — these ARE untranslated and are waiting on a ruling:`)
|
||||
for (const a of deferred) console.log(` ${a.id} = "${a.value.slice(0, 40)}"`)
|
||||
}
|
||||
console.log('\nI18N-NEVER-TRANSLATED: PASS — every compared key differs across locales, or is '
|
||||
+ 'declared identical on purpose.')
|
||||
+ `declared identical on purpose${deferred.length ? `; ${deferred.length} deferred pending a ruling` : ''}.`)
|
||||
|
|
|
|||
|
|
@ -31,9 +31,10 @@ import { chromium } from 'playwright'
|
|||
import { dirname, resolve } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
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')
|
||||
+ '/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
|
||||
|
||||
let reachable = false
|
||||
|
|
|
|||
|
|
@ -34,6 +34,7 @@ import { join, dirname } from 'node:path'
|
|||
import { fileURLToPath } from 'node:url'
|
||||
import { requireThisTree } from './_serves-this-tree.mjs'
|
||||
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 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')
|
||||
if (!URL_) {
|
||||
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(' my-upg-100-list/upg-100-list/index.html')
|
||||
process.exit(0)
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@
|
|||
import { readFileSync, readdirSync, existsSync } from 'node:fs'
|
||||
import { resolve, join, dirname } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { TREE_NAME } from './_tree-name.mjs'
|
||||
|
||||
const REPO = process.env.DOXA_REPO
|
||||
// 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.
|
||||
const leafRoot = join(REPO, 'public/doxa-maps')
|
||||
const leafRoot = join(REPO, `public/${TREE_NAME}`)
|
||||
let leaves = 0
|
||||
let withLang = 0
|
||||
if (existsSync(leafRoot)) {
|
||||
|
|
|
|||
|
|
@ -35,13 +35,14 @@ import { readFileSync, existsSync } from 'node:fs'
|
|||
import { resolve, dirname, join } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { discoverSpaRegistry } from './plugin-gallery/discover.mjs'
|
||||
import { TREE_NAME } from './_tree-name.mjs'
|
||||
|
||||
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)
|
||||
|
||||
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.')
|
||||
process.exit(1)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ import { createServer } from 'node:http'
|
|||
import { readFileSync, existsSync } from 'node:fs'
|
||||
import { join, dirname, resolve } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { TREE_NAME } from './_tree-name.mjs'
|
||||
|
||||
const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..')
|
||||
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 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) }
|
||||
const manifest = JSON.parse(readFileSync(MANIFEST, 'utf8'))
|
||||
|
||||
const bakedToken = (() => {
|
||||
for (const b of manifest.bundles || []) for (const p of b.profiles || []) {
|
||||
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
|
||||
const m = readFileSync(leaf, 'utf8').match(/BAKED_TOKEN\s*=\s*"(pk\.[^"]+)"/)
|
||||
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 TREE_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>'
|
||||
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')
|
||||
|
|
|
|||
|
|
@ -41,11 +41,12 @@ import { readFileSync, existsSync } from 'node:fs'
|
|||
import { resolve, join, dirname } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
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.
|
||||
const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..')
|
||||
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
|
||||
|
||||
|
|
@ -118,7 +119,7 @@ let unreachable = 0
|
|||
for (const [bundle, profile] of leaves) {
|
||||
const page = await browser.newPage({ viewport: { width: 1100, height: 760 } })
|
||||
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 {
|
||||
await page.goto(url, { waitUntil: 'networkidle', timeout: 60000 })
|
||||
await page.waitForTimeout(9000)
|
||||
|
|
|
|||
|
|
@ -38,10 +38,11 @@ import { resolve, dirname, join } from 'node:path'
|
|||
import { fileURLToPath } from 'node:url'
|
||||
import { chromium } from 'playwright'
|
||||
import { requireThisTree } from './_serves-this-tree.mjs'
|
||||
import { TREE_NAME } from './_tree-name.mjs'
|
||||
|
||||
const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..')
|
||||
const MANIFEST = join(ROOT, 'public', 'doxa-maps', 'manifest.json')
|
||||
const BASE = process.env.BASE || 'http://127.0.0.1:4173/doxa-maps/'
|
||||
const MANIFEST = join(ROOT, 'public', TREE_NAME, 'manifest.json')
|
||||
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
|
||||
// a default port belongs to whichever server started first — measured 2026-09-24, :4173 was another
|
||||
|
|
|
|||
|
|
@ -37,6 +37,7 @@
|
|||
import { readFileSync, existsSync } from 'node:fs'
|
||||
import { resolve, dirname, join } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { TREE_NAME } from './_tree-name.mjs'
|
||||
|
||||
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
|
||||
// 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.
|
||||
let treeName = 'doxa-maps'
|
||||
let treeName = TREE_NAME
|
||||
let treeNameSource = 'built-in default (build config unreadable)'
|
||||
try {
|
||||
const cfg = JSON.parse(readFileSync(join(ROOT, 'vite.doxa-maps-build-config.json'), 'utf8'))
|
||||
|
|
|
|||
|
|
@ -46,6 +46,7 @@ import { execSync } from 'node:child_process'
|
|||
import { readFileSync, existsSync } from 'node:fs'
|
||||
import { resolve, dirname, join } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { TREE_NAME } from './_tree-name.mjs'
|
||||
|
||||
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}`)
|
||||
console.error(
|
||||
'\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' +
|
||||
' repo-relative path instead and check:no-abs-paths will stop reporting it too.'
|
||||
)
|
||||
|
|
|
|||
|
|
@ -50,13 +50,14 @@
|
|||
import { readFileSync, existsSync, readdirSync } from 'node:fs'
|
||||
import { resolve, dirname, join } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { TREE_NAME } from './_tree-name.mjs'
|
||||
|
||||
const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..')
|
||||
const COLORS = join(ROOT, 'library/colors')
|
||||
const BUILT = join(ROOT, 'public/doxa-maps')
|
||||
const BUILT = join(ROOT, `public/${TREE_NAME}`)
|
||||
|
||||
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(' reports success when it never ran is worse than no gate.')
|
||||
process.exit(0)
|
||||
|
|
|
|||
|
|
@ -24,10 +24,11 @@ import { chromium } from 'playwright'
|
|||
import { resolve, dirname } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { servesThisTree } from './_serves-this-tree.mjs'
|
||||
import { TREE_NAME } from './_tree-name.mjs'
|
||||
|
||||
const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..')
|
||||
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
|
||||
// ⚠ 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
|
||||
|
|
@ -79,7 +80,7 @@ if (!reachable) {
|
|||
// 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.
|
||||
// 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)
|
||||
if (!treeCheck.ok) {
|
||||
console.error('POSTER-CARD-FITS: NOT MEASURED — the server is not serving this checkout.')
|
||||
|
|
|
|||
|
|
@ -44,6 +44,7 @@ import { readFileSync } from 'node:fs'
|
|||
import { resolve, dirname, join } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { servesThisTree } from './_serves-this-tree.mjs'
|
||||
import { TREE_NAME } from './_tree-name.mjs'
|
||||
|
||||
const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..')
|
||||
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
|
||||
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(`POSTER-WALL-LOCALISED: SKIP — nothing served at ${BASE}.`)
|
||||
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)))
|
||||
let r = null
|
||||
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 })
|
||||
if (!res || !res.ok()) { rows.push({ leaf, lang, skipped: `HTTP ${res ? res.status() : '?'}` }); await page.close(); continue }
|
||||
await page.waitForTimeout(13000)
|
||||
|
|
|
|||
|
|
@ -26,13 +26,14 @@ import { writeFileSync } from 'node:fs'
|
|||
import { existsSync } from 'node:fs'
|
||||
import { resolve, dirname } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { TREE_NAME } from './_tree-name.mjs'
|
||||
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
|
||||
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 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 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))
|
||||
|
|
@ -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".
|
||||
{
|
||||
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 = ''
|
||||
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 !== disk) {
|
||||
|
|
|
|||
|
|
@ -34,9 +34,10 @@
|
|||
import { readFileSync, existsSync } from 'node:fs'
|
||||
import { resolve, dirname, join } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { TREE_NAME } from './_tree-name.mjs'
|
||||
|
||||
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
|
||||
// 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
|
||||
|
||||
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(' Run `bun run build` first; an absent manifest is a failure, not a pass.')
|
||||
process.exit(1)
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ import { dirname, resolve } from 'node:path'
|
|||
import { fileURLToPath } from 'node:url'
|
||||
import { chromium } from 'playwright'
|
||||
import { servesThisTree, serveThisTree } from './_serves-this-tree.mjs'
|
||||
import { TREE_NAME } from './_tree-name.mjs'
|
||||
|
||||
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
|
||||
// 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')
|
||||
+ '/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 OWN_SERVER = null
|
||||
{
|
||||
|
|
|
|||
|
|
@ -37,12 +37,13 @@ import { readFileSync, existsSync, statSync } from 'node:fs'
|
|||
import { createHash } from 'node:crypto'
|
||||
import { resolve, dirname, join } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { TREE_NAME } from './_tree-name.mjs'
|
||||
|
||||
const HERE = dirname(fileURLToPath(import.meta.url))
|
||||
const REPO = process.env.DOXA_REPO || resolve(HERE, '..')
|
||||
const PUBLISHED_BASE = (process.env.PUBLISHED_BASE
|
||||
|| 'https://nuxtbase.app/plugin-assets/doxa-maps').replace(/\/$/, '')
|
||||
const TREE = resolve(REPO, 'public/doxa-maps')
|
||||
|| `https://nuxtbase.app/plugin-assets/${TREE_NAME}`).replace(/\/$/, '')
|
||||
const TREE = resolve(REPO, `public/${TREE_NAME}`)
|
||||
const MANIFEST = join(TREE, 'manifest.json')
|
||||
|
||||
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(' 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(' 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.')
|
||||
process.exit(1)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -40,6 +40,7 @@ import { spawn } from 'node:child_process'
|
|||
import { existsSync } from 'node:fs'
|
||||
import { resolve, dirname } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { TREE_NAME } from './_tree-name.mjs'
|
||||
|
||||
const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..')
|
||||
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 TREE_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'))) {
|
||||
console.log('SKIP check-research-gallery-embed: no built gallery at public/doxa-maps/doxa-maps-spa/')
|
||||
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/${TREE_NAME}/doxa-maps-spa/`)
|
||||
process.exit(2)
|
||||
}
|
||||
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".
|
||||
{
|
||||
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 = ''
|
||||
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 !== disk) {
|
||||
|
|
|
|||
|
|
@ -43,6 +43,7 @@
|
|||
import { readFileSync, existsSync, readdirSync } from 'node:fs'
|
||||
import { resolve, join, dirname } from 'node:path'
|
||||
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
|
||||
// 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 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 fails = []
|
||||
|
|
|
|||
|
|
@ -35,10 +35,11 @@
|
|||
import { readFileSync, existsSync, readdirSync, statSync } from 'node:fs'
|
||||
import { resolve, dirname, join } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { TREE_NAME } from './_tree-name.mjs'
|
||||
|
||||
const HERE = dirname(fileURLToPath(import.meta.url))
|
||||
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
|
||||
// contains it. `source` is where the marker must also still exist — if it does not, the marker is
|
||||
|
|
|
|||
|
|
@ -46,11 +46,12 @@ import { execFileSync } from 'node:child_process'
|
|||
import { createHash } from 'node:crypto'
|
||||
import { resolve, dirname, join } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { TREE_NAME, TREE_NAME_RE } from './_tree-name.mjs'
|
||||
|
||||
const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..')
|
||||
const SHOTS_SRC = join(ROOT, 'internal', 'readme-shots.mjs')
|
||||
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)) {
|
||||
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'] })
|
||||
let dirty = null
|
||||
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, ''))))
|
||||
} catch { /* not a git checkout */ }
|
||||
function stamp(file) {
|
||||
|
|
@ -88,8 +89,8 @@ for (const { name, path } of shots) {
|
|||
const png = join(OUT, name + '.png')
|
||||
if (!existsSync(png)) { missing.push(name); continue }
|
||||
|
||||
const m = path.match(/^\/doxa-maps\/([^/]+)\//)
|
||||
if (!m) { skipped.push(`${name} (path ${path} is not a /doxa-maps/ leaf)`); continue }
|
||||
const m = path.match(new RegExp(`^/${TREE_NAME_RE}/([^/]+)/`))
|
||||
if (!m) { skipped.push(`${name} (path ${path} is not a /${TREE_NAME}/ leaf)`); continue }
|
||||
|
||||
const bundle = join(BUILT, m[1], `${m[1]}.js`)
|
||||
if (!existsSync(bundle)) { skipped.push(`${name} (bundle ${m[1]} not built)`); continue }
|
||||
|
|
|
|||
|
|
@ -35,9 +35,10 @@ import { chromium } from 'playwright'
|
|||
import { requireDevServesThisCheckout as __devGuard, requireThisTree as __treeGuard } from './_serves-this-tree.mjs'
|
||||
import { fileURLToPath as __f2p } from 'node:url'
|
||||
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 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).
|
||||
await __treeGuard(BASE, __ROOT, 'SPA-TAB-ORDER')
|
||||
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ import { createServer } from 'node:http'
|
|||
import { readFileSync } from 'node:fs'
|
||||
import { resolve, dirname } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { TREE_NAME } from './_tree-name.mjs'
|
||||
|
||||
const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..')
|
||||
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 stop = () => { try { srv.kill() } catch { /* already gone */ } }
|
||||
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) }
|
||||
|
||||
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 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}"]`)
|
||||
if (!(await node.count())) await done(75, `STAGING-LANG: NOT MEASURED — no ${KEY} node on the staging page`)
|
||||
await node.first().click()
|
||||
|
|
|
|||
|
|
@ -35,10 +35,11 @@ import { chromium } from 'playwright'
|
|||
import { resolve, dirname } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { servesThisTree } from './_serves-this-tree.mjs'
|
||||
import { TREE_NAME } from './_tree-name.mjs'
|
||||
|
||||
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 = [
|
||||
'my-upg-100-list/upg-100-list/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)
|
||||
// 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(' 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)
|
||||
}
|
||||
|
||||
|
|
@ -76,7 +77,7 @@ if (!reachable) {
|
|||
// ⚠ 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.
|
||||
// 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)
|
||||
if (!treeCheck.ok) {
|
||||
console.error('UPG-MAP-DATA: NOT MEASURED — the server is not serving this checkout.')
|
||||
|
|
@ -125,7 +126,7 @@ try {
|
|||
console.log('UPG-MAP-DATA')
|
||||
const fails = []
|
||||
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.text) {
|
||||
// A MISSING STATUS LINE IS A FAILURE, NOT A SKIP. If the element never rendered its own
|
||||
|
|
|
|||
|
|
@ -10,16 +10,17 @@
|
|||
import { readFileSync, rmSync, mkdirSync } from 'node:fs'
|
||||
import { dirname, resolve } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { TREE_NAME } from './_tree-name.mjs'
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url))
|
||||
|
||||
function config() {
|
||||
try {
|
||||
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()) : []
|
||||
return { name, paths }
|
||||
} catch { return { name: 'doxa-maps', paths: [] } }
|
||||
} catch { return { name: TREE_NAME, paths: [] } }
|
||||
}
|
||||
|
||||
const { name, paths } = config()
|
||||
|
|
|
|||
|
|
@ -18,13 +18,14 @@
|
|||
import { readFileSync, readdirSync, statSync, existsSync } from 'node:fs';
|
||||
import { join, relative, dirname, resolve } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { TREE_NAME } from './_tree-name.mjs'
|
||||
|
||||
const here = dirname(fileURLToPath(import.meta.url));
|
||||
const bundlerRoot = resolve(here, '..');
|
||||
|
||||
let treeDir = process.argv[2];
|
||||
if (!treeDir) {
|
||||
let name = 'doxa-maps';
|
||||
let name = TREE_NAME;
|
||||
const cfgPath = join(bundlerRoot, 'vite.doxa-maps-build-config.json');
|
||||
if (existsSync(cfgPath)) {
|
||||
try { name = JSON.parse(readFileSync(cfgPath, 'utf8')).name || name; } catch {}
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ import { readFileSync, existsSync, statSync, createReadStream } from 'node:fs'
|
|||
import { resolve, join, extname } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { chromium } from 'playwright'
|
||||
import { TREE_NAME } from './_tree-name.mjs'
|
||||
|
||||
const __dirname = fileURLToPath(new URL('.', import.meta.url))
|
||||
const PUB = resolve(__dirname, '..', 'public')
|
||||
|
|
@ -46,16 +47,16 @@ const server = createServer((req, res) => {
|
|||
})
|
||||
|
||||
// ── 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 = []
|
||||
for (const b of manifest.bundles) for (const p of b.profiles) {
|
||||
// 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 })
|
||||
// Preconfigured instances are embeddable pages too — verify them the same way.
|
||||
for (const inst of (p.instances || [])) {
|
||||
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
|
||||
|
|
|
|||
|
|
@ -76,6 +76,7 @@ import { createHash } from 'node:crypto'
|
|||
import { tmpdir } from 'node:os'
|
||||
import { spawnSync } from 'node:child_process'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { TREE_NAME } from './_tree-name.mjs'
|
||||
|
||||
const HERE = dirname(fileURLToPath(import.meta.url))
|
||||
const CHECKER = join(HERE, 'sri-consistency-check.mjs')
|
||||
|
|
@ -269,7 +270,7 @@ if (SELF_TEST) {
|
|||
selfTest()
|
||||
} else {
|
||||
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
|
||||
}
|
||||
process.exit(code)
|
||||
|
|
|
|||
|
|
@ -28,9 +28,10 @@
|
|||
import { readFileSync, writeFileSync, existsSync } from 'node:fs'
|
||||
import { resolve, dirname, join } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { TREE_NAME } from './_tree-name.mjs'
|
||||
|
||||
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')
|
||||
|
||||
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('>')
|
||||
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('>')
|
||||
lines.push('> **Cite this file rather than remembering.** The same citation has been wrong in three')
|
||||
|
|
|
|||
|
|
@ -50,6 +50,7 @@ import { readFileSync, writeFileSync, existsSync, readdirSync, mkdirSync } from
|
|||
import { join, resolve, dirname } from 'node:path'
|
||||
import { fileURLToPath, pathToFileURL } from 'node:url'
|
||||
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)), '..')
|
||||
|
||||
|
|
@ -99,8 +100,8 @@ export function readBuildConfig() {
|
|||
} catch { /* fall through */ }
|
||||
}
|
||||
return {
|
||||
name: str('name') || 'doxa-maps',
|
||||
title: str('displayTitle') || 'doxa-maps',
|
||||
name: str('name') || TREE_NAME,
|
||||
title: str('displayTitle') || TREE_NAME,
|
||||
tokenUrl: tokenUrl || 'window.MAP_TOKEN_URL',
|
||||
token: str('token'),
|
||||
apiBaseUrl: str('apiBaseUrl').replace(/\/+$/, ''),
|
||||
|
|
@ -185,7 +186,7 @@ const LADDER_JS = String.raw`<script>
|
|||
ex: '<doxa-map profile-config='{"profile":"doxa-simple-map"}'></doxa-map>' },
|
||||
{ title: 'App profile (mounting file)',
|
||||
def: 'One flat .vue file in plugin-designs/<bundle>/ 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' }
|
||||
];
|
||||
var LADDER = [
|
||||
|
|
@ -246,7 +247,7 @@ const LADDER_JS = String.raw`<script>
|
|||
* config fields every leaf page bakes (readBuildConfig)
|
||||
*/
|
||||
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 baked = compact(model, true, tierMap)
|
||||
const json = (v) => JSON.stringify(v).replace(/</g, '\\u003c')
|
||||
|
|
@ -301,7 +302,7 @@ code{font-family:ui-monospace,Menlo,monospace}
|
|||
${dev ? LADDER_CSS : ''}</style>
|
||||
</head>
|
||||
<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),
|
||||
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).
|
||||
|
|
|
|||
|
|
@ -42,6 +42,7 @@ import { createHash } from 'node:crypto'
|
|||
// 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.
|
||||
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 APP_PROFILES = resolve(__dirname, '..', 'plugin-designs')
|
||||
|
|
@ -56,7 +57,7 @@ const SOURCES = resolve(__dirname, '..', 'library/api/sources.json')
|
|||
function readBuildConfig() {
|
||||
try {
|
||||
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()) : []
|
||||
// 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
|
||||
|
|
@ -103,7 +104,7 @@ function readBuildConfig() {
|
|||
const posterSources = cfg.posterSources === true || bundlePosters
|
||||
return { name, paths, embedBaseUrl, tokenUrl, token, assetsBaseUrl, apiBaseUrl, gatedProfiles, qaWidget, bundlePosters, posterSources }
|
||||
} 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()
|
||||
|
|
@ -138,7 +139,7 @@ function assetsBaseFor(depth) {
|
|||
// 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).
|
||||
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.
|
||||
// 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
|
||||
|
|
|
|||
|
|
@ -17,12 +17,13 @@ import { join, relative, dirname, resolve } from 'node:path'
|
|||
import { fileURLToPath } from 'node:url'
|
||||
import { execFileSync } from 'node:child_process'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { TREE_NAME } from './_tree-name.mjs'
|
||||
|
||||
const here = dirname(fileURLToPath(import.meta.url))
|
||||
const bundlerRoot = resolve(here, '..')
|
||||
let treeDir = process.argv[2]
|
||||
if (!treeDir) {
|
||||
let name = 'doxa-maps'
|
||||
let name = TREE_NAME
|
||||
const cfg = join(bundlerRoot, 'vite.doxa-maps-build-config.json')
|
||||
if (existsSync(cfg)) { try { name = JSON.parse(readFileSync(cfg, 'utf8')).name || name } catch {} }
|
||||
treeDir = join(bundlerRoot, 'public', name)
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ import { chromium } from 'playwright'
|
|||
import { spawn } from 'node:child_process'
|
||||
import { createServer } from 'node:http'
|
||||
import { readFileSync } from 'node:fs'
|
||||
import { TREE_NAME, TREE_NAME_RE } from './_tree-name.mjs'
|
||||
|
||||
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 }
|
||||
|
|
@ -25,7 +26,7 @@ const RUNS = Number(arg('--runs', 3)), MBPS = Number(arg('--mbps', 10)), RTT = N
|
|||
const EMBED = argv.includes('--embed')
|
||||
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 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 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 = ''
|
||||
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))
|
||||
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'
|
||||
: /\/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'
|
||||
|
||||
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 }
|
||||
let url = `${T}/${String(p.iframeUrl).replace(/^\.\//, '')}`
|
||||
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)
|
||||
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`
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ import { resolve, extname } from 'node:path'
|
|||
import { readdirSync, statSync, existsSync, readFileSync } from 'node:fs'
|
||||
import { REPO_ROOT, discoverSpaRegistry } from './discover.mjs'
|
||||
import { readBuildConfig, renderGallery } from '../generate-gallery.mjs'
|
||||
import { TREE_NAME } from '../_tree-name.mjs'
|
||||
|
||||
// ─── DEV plugin gallery ──────────────────────────────────────────────────────
|
||||
// `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
|
||||
// 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 ''
|
||||
let segs = path.slice(0, -'/index.html'.length).split('/').filter(Boolean)
|
||||
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
|
||||
// downstream sees a consistent request); never via server.middlewares.handle(), which
|
||||
// replays this same middleware and would loop.
|
||||
let treeName = 'doxa-maps'
|
||||
let treeName = TREE_NAME
|
||||
try { treeName = readBuildConfig()?.name || treeName } catch { /* keep the default */ }
|
||||
const rewritten = leafRewrite(rawPath, treeName)
|
||||
const path = rewritten || rawPath
|
||||
|
|
@ -405,7 +406,7 @@ export function autoSpaDevPlugin(token = '') {
|
|||
'<!DOCTYPE html>',
|
||||
'<html lang="en"><head><meta charset="UTF-8" />',
|
||||
'<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:,">',
|
||||
// The token ladder's baked rung, exactly as the built leaves receive it, so maps paint
|
||||
// in dev without anyone appending ?tk=.
|
||||
|
|
|
|||
|
|
@ -36,13 +36,14 @@
|
|||
import { readdirSync, statSync, readFileSync, existsSync, writeFileSync } from 'node:fs'
|
||||
import { join, relative, resolve, dirname, extname, sep } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { TREE_NAME } from './_tree-name.mjs'
|
||||
|
||||
const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..')
|
||||
const args = process.argv.slice(2)
|
||||
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 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 JSON_OUT = argOf('--json', '')
|
||||
if (!existsSync(DIR) || !statSync(DIR).isDirectory()) { console.error(`[hygiene] not a directory: ${DIR}`); process.exit(2) }
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import { createServer } from 'node:http'
|
|||
import { readFile, stat } from 'node:fs/promises'
|
||||
import { join, extname, resolve, dirname } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { TREE_NAME } from './_tree-name.mjs'
|
||||
|
||||
const HERE = dirname(fileURLToPath(import.meta.url))
|
||||
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).
|
||||
// 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 })
|
||||
|
||||
// cap5: SPA deep-linked to the prayer map instance so a real painted map shows in the right pane.
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ import { createHash } from 'node:crypto'
|
|||
import { existsSync, readFileSync } from 'node:fs'
|
||||
import { join, extname, resolve, dirname } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { TREE_NAME, TREE_NAME_RE } from './_tree-name.mjs'
|
||||
|
||||
const HERE = dirname(fileURLToPath(import.meta.url))
|
||||
const ROOT = resolve(HERE, '..', 'public')
|
||||
|
|
@ -205,8 +206,8 @@ async function shot(name, path, {
|
|||
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
|
||||
// picture from a stale one without trusting any clock (see SHOT_BUNDLES below).
|
||||
const lm = path.match(/^\/doxa-maps\/([^/]+)\//)
|
||||
const bundleJs = lm && join(ROOT, 'doxa-maps', lm[1], `${lm[1]}.js`)
|
||||
const lm = path.match(new RegExp(`^/${TREE_NAME_RE}/([^/]+)/`))
|
||||
const bundleJs = lm && join(ROOT, TREE_NAME, lm[1], `${lm[1]}.js`)
|
||||
if (bundleJs && existsSync(bundleJs)) {
|
||||
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".
|
||||
// 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 })
|
||||
|
||||
// ---- 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-engagement', '/doxa-maps/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-prayer', `/${TREE_NAME}/doxa-simple-map/doxa-simple-map/prayer/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', `/${TREE_NAME}/doxa-simple-map/doxa-simple-map/adoption/index.html`, { waitFor: CANVAS, wait: 8000 })
|
||||
|
||||
// ---- 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:
|
||||
// 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. ----
|
||||
await shot('cap4-upg100-mfe', '/doxa-maps/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-mfe', `/${TREE_NAME}/my-upg-100-list/upg-100-mfe/index.html`, { wait: 8000 })
|
||||
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 ----
|
||||
// ★ 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
|
||||
// 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.
|
||||
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 ----
|
||||
// ★ 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
|
||||
// (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.
|
||||
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 })
|
||||
|
||||
// ---- 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:
|
||||
// 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.
|
||||
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.
|
||||
mapboxReferer: false,
|
||||
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.
|
||||
// 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.
|
||||
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)
|
||||
virtualPages.set('/__readme-iframe-host.html', `<!doctype html>
|
||||
<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 ----
|
||||
// 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.)
|
||||
await shot('prod-big-picture', '/doxa-maps/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-regions', '/doxa-maps/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-gospel-resources', '/doxa-maps/gospel-resources/resources-map/index.html', { wait: 10000, minElements: 100 })
|
||||
await shot('prod-big-picture', `/${TREE_NAME}/dashboard-plus-grid/dashboard-plus-grid/index.html`, { wait: 10000, minElements: 300 })
|
||||
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', `/${TREE_NAME}/regions-dashboard/regions-mfe/index.html`, { wait: 10000, minElements: 100 })
|
||||
await shot('prod-cities-internet', `/${TREE_NAME}/cities-internet/connectivity-map/index.html`, { wait: 10000, minElements: 60 })
|
||||
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 ----
|
||||
// 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.
|
||||
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-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-regions-phone', '/doxa-maps/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-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-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-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', `/${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', `/${TREE_NAME}/regions-dashboard/regions-mfe/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', `/${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', `/${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 ----
|
||||
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-prayer-walk-planner', '/doxa-maps/prayer-walk-planner/prayer-walk-planner/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', `/${TREE_NAME}/prayer-walk-planner/prayer-walk-planner/index.html`, { wait: 12000, minElements: 40 })
|
||||
|
||||
// ---- 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
|
||||
// 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.
|
||||
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 })
|
||||
|
||||
// ---- DEPLOY3: JS embed — the Research Gallery on a real host site ----
|
||||
|
|
|
|||
|
|
@ -40,6 +40,7 @@ import { createReadStream, statSync } from 'node:fs'
|
|||
import { resolve, join, extname, normalize } from 'node:path'
|
||||
import { dirname } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { TREE_NAME, TREE_NAME_RE } from './_tree-name.mjs'
|
||||
|
||||
const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..')
|
||||
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
|
||||
// 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.
|
||||
const TREE = join(DIR, 'doxa-maps')
|
||||
const TREE = join(DIR, TREE_NAME)
|
||||
const MANIFEST = join(TREE, 'manifest.json')
|
||||
function treeIsTorn() {
|
||||
try { return !statSync(MANIFEST).isFile() } catch { return true }
|
||||
|
|
@ -89,8 +90,8 @@ createServer((req, res) => {
|
|||
let pathname
|
||||
try { pathname = decodeURIComponent(new URL(req.url, 'http://x').pathname) } catch { return send(res, 400, 'bad request\n') }
|
||||
|
||||
if (pathname.startsWith('/doxa-maps/') && treeIsTorn()) {
|
||||
return send(res, 503, 'tree is mid-build — public/doxa-maps/manifest.json is absent.\n' +
|
||||
if (pathname.startsWith(`/${TREE_NAME}/`) && treeIsTorn()) {
|
||||
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' +
|
||||
'Wait for the build to finish and retry; do NOT record this as a product finding.\n',
|
||||
{ '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
|
||||
// default; check:gallery-embed --live-cors uses it to prove the gallery still boots cross-origin
|
||||
// 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)
|
||||
}).listen(PORT, HOST, () => {
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@
|
|||
import { readFileSync, writeFileSync, existsSync, copyFileSync } from 'node:fs'
|
||||
import { resolve, dirname, join } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { TREE_NAME } from './_tree-name.mjs'
|
||||
|
||||
const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..')
|
||||
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_ROOT = join(APP_PUBLIC, 'index.html')
|
||||
// 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 = join(APP_PUBLIC, LEAF_REL)
|
||||
|
||||
// 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)) {
|
||||
console.log('APP-START-PAGE: SKIP — no synced assets tree.')
|
||||
|
|
|
|||
|
|
@ -40,8 +40,9 @@
|
|||
import { readdirSync, statSync, existsSync } from 'node:fs'
|
||||
import { join } from 'node:path'
|
||||
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)
|
||||
// ★ 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
|
||||
|
|
|
|||
|
|
@ -43,6 +43,7 @@ import { createServer } from 'node:http'
|
|||
import { readFile, stat } from 'node:fs/promises'
|
||||
import { join, extname, resolve, dirname } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { TREE_NAME } from './_tree-name.mjs'
|
||||
|
||||
const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..', 'public')
|
||||
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
|
||||
// manifest rather than hardcoded, so renaming or removing a bundle cannot leave
|
||||
// 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)
|
||||
if (!first) fail.push('manifest declares no bundle with a profile — nothing to iframe')
|
||||
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 err2 = [], failed2 = []
|
||||
p2.on('pageerror', e => err2.push(e.message.split('\n')[0]))
|
||||
|
|
|
|||
|
|
@ -52,8 +52,9 @@
|
|||
import { readFileSync, existsSync, readdirSync } from 'node:fs'
|
||||
import { join, dirname, resolve } from 'node:path'
|
||||
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)) {
|
||||
console.error('SRI-CONSISTENCY: tree not found: ' + TREE)
|
||||
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('')
|
||||
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(' 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 —')
|
||||
|
|
|
|||
|
|
@ -51,6 +51,7 @@ import { readdirSync, statSync, lstatSync, mkdirSync, copyFileSync, rmSync, exis
|
|||
import { join, relative, resolve, dirname, sep } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { createHash } from 'node:crypto'
|
||||
import { TREE_NAME } from './_tree-name.mjs'
|
||||
|
||||
const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..')
|
||||
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
|
||||
// (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.
|
||||
const TREE = String(argOf('--tree', 'doxa-maps')).replace(/\/+$/, '')
|
||||
const TREE = String(argOf('--tree', TREE_NAME)).replace(/\/+$/, '')
|
||||
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))
|
||||
// Built data nothing shipped fetches: "dropin.exclude" in the build-config (read with --from-config,
|
||||
|
|
|
|||
|
|
@ -36,9 +36,10 @@ import { chromium } from 'playwright'
|
|||
import { requireThisTree } from './_serves-this-tree.mjs'
|
||||
import { resolve as _resolvePath, dirname as _dirnamePath } from 'node:path'
|
||||
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 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)), '..')
|
||||
|
||||
// ★ MEASURE THIS BUILD, OR MEASURE NOTHING. Several worktrees run on one machine and a default port
|
||||
|
|
|
|||
|
|
@ -57,6 +57,7 @@ import { servesThisTree } from './_serves-this-tree.mjs'
|
|||
import { existsSync, readFileSync } from 'node:fs'
|
||||
import { resolve, dirname, join } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { TREE_NAME } from './_tree-name.mjs'
|
||||
|
||||
const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..')
|
||||
const BASE = process.env.BASE_URL || 'http://127.0.0.1:4173'
|
||||
|
|
@ -91,9 +92,9 @@ const GATES = [
|
|||
]
|
||||
|
||||
// ── 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)) {
|
||||
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)
|
||||
}
|
||||
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
|
||||
// timeouts and looks like six broken gates.
|
||||
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
|
||||
// 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
|
||||
|
|
@ -117,7 +118,7 @@ try {
|
|||
// server, sending the reader to start one that was already running.
|
||||
if (r.status === 503) {
|
||||
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(' 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')
|
||||
|
|
|
|||
|
|
@ -47,6 +47,7 @@ import { spawn } from 'node:child_process'
|
|||
import { existsSync, readFileSync } from 'node:fs'
|
||||
import { resolve, dirname } from 'node:path'
|
||||
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 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
|
||||
// disk, it is a different tree and must not be used.
|
||||
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
|
||||
const tree = await servesThisTree(b, REPO)
|
||||
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).`)
|
||||
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.')
|
||||
process.exit(0)
|
||||
}
|
||||
|
|
@ -98,7 +99,7 @@ if (!(await alive(base))) {
|
|||
}
|
||||
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 = []
|
||||
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
|
||||
// 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.
|
||||
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})`)
|
||||
else {
|
||||
const idx = await idxRes.json()
|
||||
|
|
|
|||
|
|
@ -33,6 +33,7 @@ import { chromium } from 'playwright'
|
|||
import { requireThisTree } from './_serves-this-tree.mjs'
|
||||
import { resolve as _resolvePath, dirname as _dirnamePath } from 'node:path'
|
||||
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.
|
||||
const GL_ARGS = [
|
||||
|
|
@ -43,7 +44,7 @@ const GL_ARGS = [
|
|||
'--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)), '..')
|
||||
|
||||
// ★ 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
|
||||
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 errors = []
|
||||
// ⚠ THE STYLESHEET COMPLAINT IS A WARNING, NOT AN ERROR, so filtering to type()==='error' above
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ import { chromium } from 'playwright'
|
|||
import { dirname, resolve } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
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.
|
||||
// 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
|
||||
// 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.
|
||||
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
|
||||
// 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')
|
||||
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
|
||||
// lookup here is a recursive walk with a seen-Set, re-injected after each interaction (a page
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import { firefox } from 'playwright';
|
|||
import { requireThisTree } from '../../../internal/_serves-this-tree.mjs'
|
||||
import { resolve as _resolvePath, dirname as _dirnamePath } from 'node:path'
|
||||
import { fileURLToPath as _fileURLToPath } from 'node:url'
|
||||
import { TREE_NAME } from '../../../internal/_tree-name.mjs'
|
||||
|
||||
// MUST be served over HTTP from THIS repo's public/. Two traps, both hit for real:
|
||||
// * file:// does not mount the bundles at all — zero custom elements, no error.
|
||||
|
|
@ -43,9 +44,9 @@ await requireThisTree(BASE, ROOT, 'APP-TABLES')
|
|||
// tables. key-city-posters is gone entirely (no such profile ships), so it is dropped rather than
|
||||
// renamed to something that merely looks similar.
|
||||
const APPS = [
|
||||
'doxa-maps/cities-internet/data-tables/index.html',
|
||||
'doxa-maps/gospel-resources/data-tables/index.html',
|
||||
'doxa-maps/gospel-resources/resources-list/index.html',
|
||||
`${TREE_NAME}/cities-internet/data-tables/index.html`,
|
||||
`${TREE_NAME}/gospel-resources/data-tables/index.html`,
|
||||
`${TREE_NAME}/gospel-resources/resources-list/index.html`,
|
||||
];
|
||||
|
||||
const browser = await firefox.launch();
|
||||
|
|
@ -92,7 +93,7 @@ for (const app of APPS) {
|
|||
const ours = await page.evaluate(() =>
|
||||
[...document.querySelectorAll('script[src]')].some(s => (s.getAttribute('integrity') || '').startsWith('sha384-')));
|
||||
if (!ours) {
|
||||
console.log(` ✗ ${app} — served page loads no doxa-maps bundle script. Wrong port or wrong tree?`);
|
||||
console.log(` ✗ ${app} — served page loads no ${TREE_NAME} bundle script. Wrong port or wrong tree?`);
|
||||
failed++; await page.close(); continue;
|
||||
}
|
||||
const mounted = await page.evaluate(() =>
|
||||
|
|
|
|||
|
|
@ -24,10 +24,11 @@
|
|||
import { readFileSync, existsSync } from 'node:fs';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { dirname, join, resolve } from 'node:path';
|
||||
import { TREE_NAME } from '../../../internal/_tree-name.mjs'
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
const PUBLIC = resolve(__dirname, '..', '..', '..', 'public');
|
||||
const MF = join(PUBLIC, 'doxa-maps', 'manifest.json');
|
||||
const MF = join(PUBLIC, TREE_NAME, 'manifest.json');
|
||||
|
||||
let failed = 0;
|
||||
const fail = (m) => { console.error(` ✗ ${m}`); failed++; };
|
||||
|
|
@ -35,7 +36,7 @@ const ok = (m) => console.log(` ✓ ${m}`);
|
|||
|
||||
console.log('SHIPPED BUNDLES — does the build match its own manifest?');
|
||||
|
||||
if (!existsSync(MF)) { console.error('FATAL: public/doxa-maps/manifest.json missing — run the build'); process.exit(1); }
|
||||
if (!existsSync(MF)) { console.error(`FATAL: public/${TREE_NAME}/manifest.json missing — run the build`); process.exit(1); }
|
||||
const m = JSON.parse(readFileSync(MF, 'utf8'));
|
||||
const bundles = m.bundles || [];
|
||||
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@
|
|||
import { readFileSync, existsSync } from 'node:fs';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { dirname, join, resolve } from 'node:path';
|
||||
import { TREE_NAME } from '../../../internal/_tree-name.mjs'
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
const ASSETS = resolve(__dirname, '..', '..', '..', 'public', 'assets');
|
||||
|
|
@ -89,7 +90,7 @@ const GATE_CAVEATS = [
|
|||
'A FLOOR IS NOT A FIDELITY PROOF. It catches WHOLESALE coarsening (a 3x point collapse), not a subtle reshape — geometry can be moved, smoothed, or distorted at a constant point count and this passes.',
|
||||
'IT DOES NOT CHECK SHAPE. Points are counted, never compared to a reference outline. A garbled coastline with enough vertices is GREEN.',
|
||||
'gb-CHN-ADM1.geojson HAS NO FLOOR. Shipping 3,264 points vs 2,000 under the same coarsening knobs = 1.63x — too thin to separate honestly. That file is UNGUARDED here; treat it as device-gated.',
|
||||
'THIS REPO HAS 83 FILES NAMED world-adm0.geojson (72 under app/, 10 under public/, 1 under plugin-designs/; none in node_modules). A GREEN here covers ONLY the path printed above — the other 82 are GENERATED BUNDLE ARTIFACTS and this floor says NOTHING about their freshness or even their validity. Do NOT infer a second floor is needed: a 2026-08-02 snapshot found the app/ copies were mostly PRE-INDIA BUILDS (161 features / 111,013 points, differing from the guarded file by IND alone — stale, not coarsened, and above this floor). But that snapshot is NOT a durable fact: the copies are rewritten by bundle builds, one was mid-sweep TRUNCATED and would not even parse, and any census of them is a moving target. The durable question is whether bundles are REBUILT AND VALIDATED after a boundary change — a build-pipeline property, not something a point floor can hold. NOTE THE STAKES: measured 2026-08-02, ALL NINE per-app copies under public/doxa-maps/ were missing IND (161 features, built 22:05 the previous day, before India landed). "Stale by one country" IS the India-paints-nothing regression this guard was written for — and the "India paints" assertion above only covers the ONE canonical path, not the generated per-app trees.',
|
||||
`THIS REPO HAS 83 FILES NAMED world-adm0.geojson (72 under app/, 10 under public/, 1 under plugin-designs/; none in node_modules). A GREEN here covers ONLY the path printed above — the other 82 are GENERATED BUNDLE ARTIFACTS and this floor says NOTHING about their freshness or even their validity. Do NOT infer a second floor is needed: a 2026-08-02 snapshot found the app/ copies were mostly PRE-INDIA BUILDS (161 features / 111,013 points, differing from the guarded file by IND alone — stale, not coarsened, and above this floor). But that snapshot is NOT a durable fact: the copies are rewritten by bundle builds, one was mid-sweep TRUNCATED and would not even parse, and any census of them is a moving target. The durable question is whether bundles are REBUILT AND VALIDATED after a boundary change — a build-pipeline property, not something a point floor can hold. NOTE THE STAKES: measured 2026-08-02, ALL NINE per-app copies under public/${TREE_NAME}/ were missing IND (161 features, built 22:05 the previous day, before India landed). "Stale by one country" IS the India-paints-nothing regression this guard was written for — and the "India paints" assertion above only covers the ONE canonical path, not the generated per-app trees.`,
|
||||
'THE ADM1 LAZY FIGURE IS MODELED, NOT OBSERVED. resimplify() only ever rewrites world-adm0.geojson, so the ADM1 files have no lazy counterpart that the shipping pipeline actually produces. Their floors were set by applying the same knobs offline.',
|
||||
'STILL DEVICE-GATED: whether the coastlines READ at print size. This converts a wholesale-coarsening regression into a CI failure; it does not replace a human looking at a printed poster.',
|
||||
];
|
||||
|
|
|
|||
|
|
@ -153,6 +153,31 @@ export const useMapStore = defineStore('map', {
|
|||
*/
|
||||
clusterMode: 'region',
|
||||
|
||||
/**
|
||||
* ★ WHICH CLUSTERING ALGORITHM IS DRAWN — and NOT the same thing as `clusterMode` above,
|
||||
* which is a grouping AXIS ('region' | 'language-family' | …) and is read by nothing today.
|
||||
* The two names are one letter apart and mean different things, which is exactly why this
|
||||
* one needs saying out loud rather than being folded into its neighbour.
|
||||
*
|
||||
* 'off' | 'mapbox' (native circle clusters) | 'mst' | 'network'. useMapClustering reads it
|
||||
* (`mode` computed) and ResearchMapSideMenu writes it — but it was never declared HERE, so
|
||||
* the side menu's four-way picker wrote to a property no reactive system was watching and
|
||||
* choosing "Native" drew nothing. A control that does nothing is worse than an absent one:
|
||||
* it tells the reader the map cannot do what it can.
|
||||
*
|
||||
* 'off' IS the disabled state, so this single field answers both "is clustering on" and
|
||||
* "which one" — the legend's Clusters checkbox and this picker are two views of it rather
|
||||
* than two sources of truth that can disagree.
|
||||
*/
|
||||
clusteringMode: 'off',
|
||||
|
||||
/**
|
||||
* Opacity of individual pins while clustering is on (the side menu's dim slider).
|
||||
* Declared here for the same reason as the mode: the control wrote to a property that did
|
||||
* not exist, so dragging it changed a number nobody read.
|
||||
*/
|
||||
clusterPinOpacity: 0.6,
|
||||
|
||||
/**
|
||||
* Per-selection MST cluster filter (null when no filter active)
|
||||
* clusterFilterProperty: 'languageFamily' | 'doxaRegion' | 'language' | null
|
||||
|
|
@ -596,6 +621,19 @@ export const useMapStore = defineStore('map', {
|
|||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Set the clustering ALGORITHM (see the clusteringMode note in state). useMapClustering
|
||||
* prefers this action over a direct assignment when it exists, so declaring it keeps the
|
||||
* composable and the store agreeing about who owns the transition.
|
||||
* @param {string} mode - 'off' | 'mapbox' | 'mst' | 'network'
|
||||
*/
|
||||
setClusteringMode(mode) {
|
||||
if (['off', 'mapbox', 'mst', 'network'].includes(mode)) {
|
||||
this.clusteringMode = mode;
|
||||
this.showClusters = mode !== 'off';
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Set per-selection MST cluster filter
|
||||
* @param {string|null} property - 'languageFamily' | 'doxaRegion' | 'language' | null
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@
|
|||
"check:click-sheet": "node plugin-designs/gospel-resources/scripts/verify-gospel-click-sheet.mjs",
|
||||
"check:composed-filters": "node internal/check-gospel-composed-filters.mjs",
|
||||
"check:composite": "node internal/check-composite-freshness.mjs",
|
||||
"check:clustering-modes": "node plugin-designs/doxa-research-map/scripts/check-clustering-modes.mjs",
|
||||
"check:composite-embed-knobs": "node internal/check-composite-embed-knobs.mjs",
|
||||
"check:composite-locale-leaks": "node plugin-designs/research-map-plus-grid/scripts/check-composite-locale-leaks.mjs",
|
||||
"check:composite-reg": "node internal/check-composite-registration.mjs",
|
||||
|
|
@ -95,6 +96,8 @@
|
|||
"check:locale-reachable": "node internal/check-locale-reachable.mjs",
|
||||
"check:manifest-covers": "node internal/check-manifest-covers-discovery.mjs",
|
||||
"check:map-embed-cells": "node internal/check-map-embed-cells.mjs",
|
||||
"check:map-paints": "node internal/check-gospel-map-paints.mjs",
|
||||
"check:map-paints:poison": "node internal/check-gospel-map-paints.mjs --poison",
|
||||
"check:map-unmount-mid-init": "node internal/check-map-unmount-mid-init.mjs",
|
||||
"check:map-unmount-race": "node internal/check-map-unmount-guard.mjs",
|
||||
"check:maplibre-drops": "node internal/check-maplibre-layer-drops.mjs",
|
||||
|
|
@ -169,7 +172,7 @@
|
|||
"dev": "bun x vite",
|
||||
"fix:sri": "node internal/fix-sri.mjs",
|
||||
"gallery": "node internal/generate-gallery.mjs",
|
||||
"gates:post": "node internal/run-parallel.mjs --label=post -- check:drop check:parse verify check:sri check:public-hygiene check:label-joins check:facet-cascade check:facet-one-field check:heartbeat check:kpi-unit check:composite-reg check:descriptions check:no-abs-paths check:no-codename check:dep-names check:composite-srcs check:embed-targets check:nested-token check:i18n-parity check:tabs-localized check:packets-all check:packet-strings-parity check:zone-census check:share-policy check:library-index check:csv-shape check:capability-citations check:catalog-identity check:no-orphan-gates check:quoted-scripts check:done-evidence check:script-flags check:backlog-status check:capability-exemplars check:packet-fixture check:packet-browser check:provider-switch check:locale-reachable check:url-resolver check:manifest-covers check:one-palette check:legend-source check:backlog-refs check:composite-embed-knobs check:linked-vocabulary",
|
||||
"gates:post": "node internal/run-parallel.mjs --label=post -- check:drop check:parse verify check:sri check:public-hygiene check:label-joins check:facet-cascade check:facet-one-field check:heartbeat check:kpi-unit check:composite-reg check:descriptions check:no-abs-paths check:no-codename check:dep-names check:composite-srcs check:embed-targets check:nested-token check:i18n-parity check:tabs-localized check:packets-all check:packet-strings-parity check:zone-census check:share-policy check:library-index check:csv-shape check:capability-citations check:catalog-identity check:no-orphan-gates check:quoted-scripts check:done-evidence check:script-flags check:backlog-status check:capability-exemplars check:packet-fixture check:packet-browser check:provider-switch check:locale-reachable check:url-resolver check:manifest-covers check:one-palette check:legend-source check:backlog-refs check:clustering-modes check:composite-embed-knobs check:composite-locale-leaks check:linked-vocabulary",
|
||||
"gates:pre": "node internal/run-parallel.mjs --label=pre -- check:template-literal check:mjs-parse check:sfc-parse check:tab-radius check:synced-css check:wp-embed check:doc-scripts",
|
||||
"library-index": "node internal/generate-library-index.mjs",
|
||||
"packet-fixture": "node internal/generate-packet-fixture.mjs",
|
||||
|
|
@ -228,6 +231,7 @@
|
|||
"check:composed-filters": "CELL C — the gospel map composed above its own tables. Asserts the composite keeps the reader INSIDE it: \"Every resource in <language>\" must be a <button> that filters in place, not an <a> that navigates away and throws out the map the reader just positioned. Checks the tag, that the URL did not change, that the table NARROWED (row count down but not to zero), that the pin count survived, and that nothing 4xx'd. Picks a tier-2 pin from the live source — the first version opened pin 0, a group with nothing found, and failed a working composite on an empty table. Browser gate: run under both rendezvous locks, product lock first.",
|
||||
"check:composite": "a COMPOSITE is a COPY taken at its own build time: fails if a nested child's source is newer than the parent's bundle. NOT in build:unlocked on purpose — there everything was just built, so it could never catch the real case (a PARTIAL build). Run it before PUBLISH.",
|
||||
"check:composite-embed-knobs": "a composite must pass the HOST's tk and provider down to the map it nests, proven on a genuinely foreign origin (tree served on one port, partner page on another). Our own leaves bake a token and assign window.MAP_TOKEN, so this only ever breaks where there is no leaf - the partner page the embed snippet is written for, and the case nothing else tested. Keyless maplibre asserts ZERO requests to api.mapbox.com, because 'it rendered' would pass on a build that quietly fell back with a baked key.",
|
||||
"check:clustering-modes": "every option in the research map's clustering picker must DRAW something different. Choosing 'Native' changed nothing: mapStore never declared clusteringMode, so the picker wrote to a property nothing watched, AND the Clusters checkbox force-set 'mst' regardless of the store, so even a fixed picker would have snapped back on the next tick. Asserts on what the GL style RENDERS (circles, count labels, lines), not on state - 'clusteringMode === mapbox' would pass on a build still drawing MST. Also asserts no orphan layers between modes, and that MST and Network differ, since two labels for one behaviour is the same defect as a dead option.",
|
||||
"check:composite-locale-leaks": "a composite rendered in a non-English locale must not show a string the catalogue already translates. The criterion is a LOOKUP, not a category: English catalogue holds the string as a value, this locale holds a different value for that key, the page still renders English. The previous sweep split strings into chrome/data/third-party and scored single words like 'Region' and 'Prayer' as data, so 28 leaks on Big Picture and 41 on Simple Research counted as zero. Surfaces that a lookup cannot judge - vendor map chrome, table DATA cells, facet-rail values - are suppressed by NAME with a stated reason, never by word.",
|
||||
"check:coverage-consistent": "the gospel-resources surfaces must agree: no resource on an ISO RESERVED code (und/mul/mis/zxx), every resourced code NAMED, table reach recounted off the pins, no invented URL, tiers reconcile. Static, sub-second. Self-test: `bun run check:coverage-consistent:poison`, all 12 clauses (A-L) must fail. Clause L is the one that is not about consistency: it fails when the link-verification record goes stale, because 'link checked' is a claim about the present tense. Written after the same drift defect was found by hand three times.",
|
||||
"check:csv-shape": "the export must OPEN as a table: header on line 0, every row matching. Run the `check:csv-shape:self-test` script for the NEGATIVE test (forces the provenance comment back to line 0 IN MEMORY and requires this gate to REJECT it) — the header used to claim 'NEGATIVE-TESTED' with no way to re-run it.",
|
||||
|
|
@ -269,6 +273,7 @@
|
|||
"check:linked-vocabulary": "the two halves of research-map-plus-grid must mean the SAME THING by a religion family. The map filters by the legend's FIRST-LETTER key ('B'), the grid by a DERIVED family name ('Buddhism'), and both read ONE shared table (library/colors/religion.js) in opposite directions - so a wrong link stays invisible: both halves look filtered and show different peoples. Compares the total reached over the composite's wire against the total the grid gives for the same facet on its own, and runs the MUTANT first (the legend key instead of the name), failing as NO-TEETH if the wrong mapping scores the same as the right one.",
|
||||
"check:manifest-covers": "every DISCOVERED (non-gated) bundle must be in the built manifest",
|
||||
"check:map-embed-cells": "PRODUCTION-MATRIX cells B (iframe) and C (script embed) for the seven Research Gallery maps on a genuinely foreign origin (tree on 127.0.0.1, partner page on localhost). C passes the HOST a distinct token and fails if any Mapbox request uses another. --only bundle/profile,… --cell B|C.",
|
||||
"check:map-paints": "The map must put PIXELS on screen, not just features. Exists because injecting mapbox-gl.css collapsed this profile's canvas from 1440x900 to 1440x300 - a white rectangle - and EVERY assertion went green over it: verify-gospel-map counted 2,107 of 2,107 pins rendered, leaves-mount found a full shadow root, the i18n gates found every label translated and nothing clipped. queryRenderedFeatures does not care whether anything is visible. This screenshots the element and counts distinct colours (a basemap has hundreds, a blank rectangle one or two) and checks the canvas fills its container. --poison hides the canvas and requires a red verdict.",
|
||||
"check:map-unmount-mid-init": "end-to-end, deterministic: holds mapbox-gl.js, opens a map plugin in the studio, switches away (destroying the pane) mid-initialise, then releases the script. Fails on any page error. REQUIRES --url. Exit 2 if the race was not staged.",
|
||||
"check:map-unmount-race": "browser-free, sub-second: runs the shipped ensureEngineCSS with a null element (what an unmounted pane hands it) against a copy with its guard stripped; the guarded arm must resolve and the stripped arm must throw \"reading getRootNode\". By gospel-plugin, adopted by studio.",
|
||||
"check:mobile": "deploy-2 CONTRACT check: needs NO JDK/SDK/device. cap sync is a file copy + plugin wiring, so it runs anywhere; cap:verify then asserts assets/public/index.html exists. Guards the SOFT FAILURE capacitor.config.json warns about: a wrong webDir yields an EMPTY assets/public and a BLANK WHITE SCREEN at launch, not an error.",
|
||||
|
|
|
|||
|
|
@ -122,6 +122,33 @@ function paintGroups(map) {
|
|||
dotCount.value = fc.features.length
|
||||
// Named 'language-families' because the shared selected-pin highlight looks for it.
|
||||
map.addSource('language-families', { type: 'geojson', data: fc, promoteId: 'uniqueId' })
|
||||
// OPENING VIEW = the dots' own extent (gospel found this on its clone). A hard-coded zoom frames
|
||||
// the world only at one canvas size: MEASURED at 390 px, zoom 1.3 showed 1,579 of the 2,107
|
||||
// groups and cut off the Americas and the Pacific. Fitting the data means the first view cannot
|
||||
// depend on the screen. Once, on first paint; the user owns the camera after that.
|
||||
if (fc.features.length) {
|
||||
let w = 180, s = 90, e = -180, n = -90
|
||||
for (const f of fc.features) { const [x, y] = f.geometry.coordinates; if (x < w) w = x; if (x > e) e = x; if (y < s) s = y; if (y > n) n = y }
|
||||
const pad = Math.round(Math.min(map.getContainer().clientWidth, map.getContainer().clientHeight) * 0.06) + 8
|
||||
try {
|
||||
const want = map.cameraForBounds([[w, s], [e, n]], { padding: pad })
|
||||
map.fitBounds([[w, s], [e, n]], { padding: pad, duration: 0 })
|
||||
// A PORTRAIT PHONE CANNOT FRAME THE WORLD IN MERCATOR: the engine will not zoom out past the
|
||||
// point where the world stops filling the viewport vertically (MEASURED at 390x844: floor
|
||||
// 0.72 while the fit wanted -0.62). When the fit is clamped like that, some dots must be off
|
||||
// screen; put the visible window where the MOST dots are rather than at the bbox centre.
|
||||
if (want && map.getZoom() > want.zoom + 0.05) {
|
||||
const b = map.getBounds(); const span = Math.min(360, b.getEast() - b.getWest())
|
||||
let bestLng = map.getCenter().lng, best = -1
|
||||
for (let c = -180; c < 180; c += 5) {
|
||||
let k = 0
|
||||
for (const f of fc.features) { let d = f.geometry.coordinates[0] - c; d = ((d + 540) % 360) - 180; if (Math.abs(d) <= span / 2) k++ }
|
||||
if (k > best) { best = k; bestLng = c }
|
||||
}
|
||||
map.jumpTo({ center: [bestLng, (s + n) / 2] })
|
||||
}
|
||||
} catch (_) {}
|
||||
}
|
||||
map.addLayer({
|
||||
id: 'ci-pg-dots', type: 'circle', source: 'language-families',
|
||||
paint: {
|
||||
|
|
|
|||
|
|
@ -2063,6 +2063,27 @@ if (typeof window !== 'undefined') {
|
|||
// RELIGION_FAMILIES — the same table, run the same direction, as the grid's `__religion_family`
|
||||
// accessor. That is what makes the two vocabularies comparable, and comparing them is how the
|
||||
// linking is checked rather than assumed (see scripts/check-linked-vocabulary.mjs).
|
||||
// WHAT THIS MAP IS ACTUALLY DRAWING for clustering. The output is painted on the CANVAS, so no
|
||||
// DOM assertion can tell a native circle cluster from an MST web from nothing at all — and "the
|
||||
// state changed" is exactly the kind of evidence that let an inert control ship in the first
|
||||
// place. So this reports, per clustering layer, whether it EXISTS, whether it is VISIBLE, and how
|
||||
// many features are actually RENDERED right now. Layers are found by prefix rather than from a
|
||||
// list, so a clustering layer added later shows up here without anyone remembering to add it.
|
||||
// Same shape and purpose as the two handles beside it.
|
||||
window.__researchMapClusterDraw = () => {
|
||||
const m = map.value
|
||||
const out = {}
|
||||
try {
|
||||
for (const l of (m?.getStyle?.()?.layers || [])) {
|
||||
if (!/^(mapbox-clusters|cluster-lines)/.test(l.id)) continue
|
||||
let rendered = 0
|
||||
try { rendered = m.queryRenderedFeatures({ layers: [l.id] }).length } catch (_) { rendered = -1 }
|
||||
out[l.id] = { visibility: (l.layout && l.layout.visibility) || 'visible', rendered }
|
||||
}
|
||||
} catch (_) { /* style not ready — an empty report is the honest answer */ }
|
||||
return out
|
||||
}
|
||||
|
||||
window.__researchMapVocabulary = (field) => {
|
||||
const rows = mapData?.normalizedPeopleGroups?.value || []
|
||||
const seen = new Set()
|
||||
|
|
@ -2148,23 +2169,60 @@ watch(() => uiStore.religionFilter, (key) => {
|
|||
else applyDimFilter({ kind: null })
|
||||
})
|
||||
|
||||
// ─── Clusters toolbar wiring ─────────────────────────────────────────────────
|
||||
// LegendDesktop's Clusters checkbox writes to mapStore.showClusters. Research-mfe
|
||||
// only ever uses the MST line-clustering algorithm (lines between pins), so we
|
||||
// force-set the mode to 'mst' BEFORE enabling, regardless of what's in the
|
||||
// store. The clusterMode watcher is kept as a no-op for forwards compat.
|
||||
watch(() => mapStore.showClusters, async (on) => {
|
||||
if (on) {
|
||||
// Mode must be set before setEnabled() so the composable's _applyLineClustering
|
||||
// path is selected; otherwise it falls back to its internal default ('mapbox').
|
||||
await clustering.setMode?.('mst')
|
||||
// Replay any legend-row selection that was made *before* clustering was
|
||||
// enabled — keeps the first MST pass scoped to that slice.
|
||||
// ─── Clusters wiring: TWO CONTROLS, ONE STATE ────────────────────────────────
|
||||
// This map has two ways to ask for clustering and they used to disagree:
|
||||
//
|
||||
// LegendDesktop's "Clusters" CHECKBOX -> mapStore.showClusters
|
||||
// the side menu's four-way PICKER -> mapStore.clusteringMode (Off/Native/MST/Network)
|
||||
//
|
||||
// The checkbox force-set 'mst' "regardless of what's in the store", and the picker wrote to a
|
||||
// property mapStore never declared — so choosing Native drew nothing, and had it worked, one tick
|
||||
// of the checkbox would have snapped it back to MST anyway. MEASURED as inert by exp-shepherd; the
|
||||
// second half is what makes fixing only the picker the wrong fix, because the control would have
|
||||
// worked until the reader touched the other one.
|
||||
//
|
||||
// Now clusteringMode IS the state ('off' means disabled) and the checkbox is a view of it: ticking
|
||||
// it keeps whatever the reader picked, promoting only 'off' to 'mst' so the checkbox alone still
|
||||
// behaves exactly as it documented. The two can no longer show different things.
|
||||
//
|
||||
// The guard is not ceremony: each watcher writes the other's field, so without it one click runs
|
||||
// the whole teardown-and-redraw twice — visible as a flash, and twice the work on a 2,100-pin map.
|
||||
let _applyingCluster = false
|
||||
async function applyClusterState() {
|
||||
if (_applyingCluster) return
|
||||
_applyingCluster = true
|
||||
try {
|
||||
const mode = mapStore.clusteringMode || 'off'
|
||||
if (mode === 'off') {
|
||||
if (mapStore.showClusters) mapStore.showClusters = false
|
||||
await clustering.setEnabled?.(false)
|
||||
return
|
||||
}
|
||||
if (!mapStore.showClusters) mapStore.showClusters = true
|
||||
// Mode must be set before setEnabled() so the composable selects the right path; otherwise it
|
||||
// falls back to its own internal default ('mapbox') and the reader's choice is lost.
|
||||
await clustering.setMode?.(mode)
|
||||
// Replay any legend-row selection made BEFORE clustering was enabled — keeps the first pass
|
||||
// scoped to that slice.
|
||||
clustering.flushSelectionFilter?.()
|
||||
await clustering.setEnabled?.(true)
|
||||
} else {
|
||||
await clustering.setEnabled?.(false)
|
||||
} finally {
|
||||
_applyingCluster = false
|
||||
}
|
||||
}
|
||||
|
||||
watch(() => mapStore.showClusters, async (on) => {
|
||||
if (_applyingCluster) return
|
||||
// The checkbox only decides ON or OFF; WHICH clustering is the picker's business, and 'mst' is
|
||||
// the default this map has always used when nobody has picked anything.
|
||||
if (on && (!mapStore.clusteringMode || mapStore.clusteringMode === 'off')) mapStore.clusteringMode = 'mst'
|
||||
else if (!on && mapStore.clusteringMode !== 'off') mapStore.clusteringMode = 'off'
|
||||
await applyClusterState()
|
||||
})
|
||||
|
||||
watch(() => mapStore.clusteringMode, async () => {
|
||||
if (_applyingCluster) return
|
||||
await applyClusterState()
|
||||
})
|
||||
|
||||
// ─── Lifecycle ───────────────────────────────────────────────────────────────
|
||||
|
|
|
|||
|
|
@ -0,0 +1,187 @@
|
|||
#!/usr/bin/env node
|
||||
/**
|
||||
* check-clustering-modes.mjs — every option in the map's clustering picker must DRAW something
|
||||
* different. A control that does nothing is worse than a control that is absent: it tells the
|
||||
* reader the map cannot do what it can.
|
||||
*
|
||||
* THE DEFECT, MEASURED BY exp-shepherd AND CONFIRMED HERE. The side menu offers four clustering
|
||||
* options; choosing "Native" changed nothing on the map. Two separate reasons, and only fixing both
|
||||
* makes the control real:
|
||||
*
|
||||
* 1. mapStore never declared `clusteringMode`, so the picker wrote to a property no reactive
|
||||
* system watched, and useMapClustering's `mode` computed kept its own default.
|
||||
* 2. research-map.vue's Clusters-checkbox watcher force-set 'mst' "regardless of what's in the
|
||||
* store". Even with (1) fixed, one tick of the checkbox would have snapped Native back to MST
|
||||
* — a control that works until you touch the other one.
|
||||
*
|
||||
* ★ WHY THIS ASSERTS ON PIXELS-WORTH-OF-STATE AND NOT ON STATE. "mapStore.clusteringMode === 'mapbox'"
|
||||
* would have passed on the broken build the moment (1) was fixed, while the map still drew MST
|
||||
* lines. Clustering output is painted on the canvas, so the only honest question is what the GL
|
||||
* style is actually rendering. The map exposes `__researchMapClusterDraw()` for exactly this: per
|
||||
* clustering layer, whether it exists, whether it is visible, and how many features are rendered
|
||||
* NOW.
|
||||
*
|
||||
* ASSERTIONS, per mode, on the built tree under the default provider:
|
||||
* off — nothing rendered by any clustering layer.
|
||||
* mapbox — the native circle layer AND its count-label layer render features; no cluster lines.
|
||||
* mst — the cluster-lines layer renders features; no native circles.
|
||||
* network — same as mst (a different algorithm feeding the same line layer), and it must produce
|
||||
* a DIFFERENT number of lines than mst on the same data, or the two options are one
|
||||
* option wearing two labels.
|
||||
* and between modes: no orphans — a layer left rendering after switching away is the bug this
|
||||
* composable's own teardown comments warn about.
|
||||
*
|
||||
* Exit: 0 pass or SKIP (no built tree, no browser, no data) · 1 fail.
|
||||
*/
|
||||
import { createServer } from 'node:http'
|
||||
import { spawn } from 'node:child_process'
|
||||
import { existsSync, readFileSync } from 'node:fs'
|
||||
import { resolve, dirname, join } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
|
||||
const TAG = 'CLUSTERING-MODES'
|
||||
const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..', '..', '..')
|
||||
const MANIFEST = join(ROOT, 'public', 'doxa-maps', 'manifest.json')
|
||||
const skip = (why) => { console.log(`${TAG}: SKIP — ${why}`); process.exit(0) }
|
||||
|
||||
if (!existsSync(MANIFEST)) skip('no built manifest (run `bun run tree` first).')
|
||||
let chromium
|
||||
try { ({ chromium } = await import('playwright')) } catch { skip('playwright is not installed.') }
|
||||
|
||||
const manifest = JSON.parse(readFileSync(MANIFEST, 'utf8'))
|
||||
const bundle = (manifest.bundles || []).find((b) => b.bundle === 'doxa-research-map')
|
||||
const profile = bundle && (bundle.profiles || []).find((p) => p.iframeUrl && /research-map/.test(p.iframeUrl))
|
||||
if (!profile) skip('the manifest declares no research-map leaf to load.')
|
||||
|
||||
const freePort = () => new Promise((ok) => {
|
||||
const s = createServer().listen(0, '127.0.0.1', () => { const p = s.address().port; s.close(() => ok(p)) })
|
||||
})
|
||||
const PORT = await freePort()
|
||||
const TREE = `http://127.0.0.1:${PORT}/doxa-maps`
|
||||
const LEAF = `${TREE}/${String(profile.iframeUrl).replace(/^\.\//, '')}`
|
||||
|
||||
const tree = spawn(process.execPath, ['internal/serve-strict.mjs', '--port', String(PORT)], { cwd: ROOT, stdio: 'ignore' })
|
||||
const cleanup = () => { try { tree.kill() } catch { /* already gone */ } }
|
||||
await new Promise((r) => setTimeout(r, 900))
|
||||
if (!await fetch(`${TREE}/manifest.json`, { signal: AbortSignal.timeout(4000) }).then((r) => r.ok).catch(() => false)) {
|
||||
cleanup(); skip('could not serve the built tree locally.')
|
||||
}
|
||||
const { requireThisTree } = await import(join(ROOT, 'internal', '_serves-this-tree.mjs'))
|
||||
await requireThisTree(TREE, ROOT, TAG)
|
||||
|
||||
const browser = await chromium.launch({ args: ['--enable-unsafe-swiftshader', '--use-gl=angle', '--use-angle=swiftshader'] })
|
||||
const fails = []
|
||||
const lines = []
|
||||
try {
|
||||
const page = await browser.newPage({ viewport: { width: 1440, height: 1000 } })
|
||||
await page.goto(LEAF, { waitUntil: 'domcontentloaded', timeout: 60000 })
|
||||
|
||||
const report = await page.evaluate(async () => {
|
||||
const sleep = (ms) => new Promise((r) => setTimeout(r, ms))
|
||||
// Wait for the map AND its data: clustering has nothing to draw until the pins exist.
|
||||
let draw = null
|
||||
for (let i = 0; i < 120; i++) {
|
||||
if (typeof window.__researchMapClusterDraw === 'function') { draw = window.__researchMapClusterDraw; break }
|
||||
await sleep(500)
|
||||
}
|
||||
if (!draw) return { skip: 'the map never exposed its clustering report (it did not finish mounting)' }
|
||||
const vocab = window.__researchMapVocabulary
|
||||
for (let i = 0; i < 120; i++) {
|
||||
if (vocab && (vocab('religionFamily') || []).length) break
|
||||
await sleep(500)
|
||||
}
|
||||
|
||||
// ★ CLICK THE ACTUAL BUTTONS. Setting the store would prove the state path and nothing about the
|
||||
// control, and a control that does not reach its state is precisely the defect here. The drawer
|
||||
// is off-screen until opened, which does not matter: a dispatched click reaches a Vue @click
|
||||
// handler wherever the element sits, and the drawer's own open/close is a different question.
|
||||
const seen = new Set()
|
||||
const findAll = (sel) => {
|
||||
const hits = []
|
||||
const walk = (root) => {
|
||||
if (!root || seen.has(root)) return
|
||||
seen.add(root)
|
||||
for (const el of root.querySelectorAll('*')) {
|
||||
if (el.matches && el.matches(sel)) hits.push(el)
|
||||
if (el.shadowRoot) walk(el.shadowRoot)
|
||||
}
|
||||
}
|
||||
walk(document)
|
||||
return hits
|
||||
}
|
||||
const buttons = findAll('.rm-cluster-btn')
|
||||
// Order is the component's CLUSTER_MODES array: off, mapbox (Native), mst, network. Index and
|
||||
// not label, because the labels are localized and this gate must pass in every locale.
|
||||
if (buttons.length !== 4) return { skip: `expected 4 clustering buttons in the side menu, found ${buttons.length}` }
|
||||
|
||||
// ★ WAIT FOR THE DRAWING TO SETTLE, DO NOT GUESS A DURATION. A fixed 6s pause reported MST as
|
||||
// drawing nothing while Network drew 2,681 lines — not a bug in MST but in the measurement: a
|
||||
// minimum spanning tree over ~2,100 pins is quadratic and simply had not finished. A gate that
|
||||
// calls a slow feature broken is worse than no gate. So poll until two consecutive reads agree,
|
||||
// which ends in a second or two for a mode that draws nothing and waits as long as the work
|
||||
// actually takes for one that does.
|
||||
const settle = async (maxMs) => {
|
||||
const t0 = Date.now()
|
||||
let prev = JSON.stringify(draw())
|
||||
let stableSince = Date.now()
|
||||
while (Date.now() - t0 < maxMs) {
|
||||
await sleep(1500)
|
||||
const now = JSON.stringify(draw())
|
||||
if (now === prev) { if (Date.now() - stableSince > 4000) return JSON.parse(now) }
|
||||
else { prev = now; stableSince = Date.now() }
|
||||
}
|
||||
return JSON.parse(prev)
|
||||
}
|
||||
|
||||
const out = {}
|
||||
const MODES = ['off', 'mapbox', 'mst', 'network']
|
||||
for (let i = 0; i < MODES.length; i++) {
|
||||
buttons[i].click()
|
||||
out[MODES[i]] = await settle(90000)
|
||||
}
|
||||
return out
|
||||
})
|
||||
|
||||
if (report.skip) { await browser.close(); cleanup(); skip(report.skip) }
|
||||
|
||||
const renderedOf = (snap, re) => Object.entries(snap || {})
|
||||
.filter(([id, v]) => re.test(id) && (v.visibility !== 'none'))
|
||||
.reduce((a, [, v]) => a + Math.max(0, v.rendered), 0)
|
||||
|
||||
const circles = (m) => renderedOf(report[m], /^mapbox-clusters-circle/)
|
||||
const counts = (m) => renderedOf(report[m], /^mapbox-clusters-count/)
|
||||
const linesN = (m) => renderedOf(report[m], /^cluster-lines/)
|
||||
|
||||
for (const m of ['off', 'mapbox', 'mst', 'network']) {
|
||||
lines.push(` · ${m.padEnd(8)} circles=${circles(m)} counts=${counts(m)} lines=${linesN(m)}`)
|
||||
}
|
||||
|
||||
if (circles('off') || counts('off') || linesN('off')) {
|
||||
fails.push(`"Off" still draws clustering: circles=${circles('off')} counts=${counts('off')} lines=${linesN('off')}.`)
|
||||
}
|
||||
if (!circles('mapbox')) fails.push('"Native" drew no cluster circles — the option does nothing, which is the defect this gate exists for.')
|
||||
// Only meaningful once circles exist: reported beside "drew no circles" it reads as a second,
|
||||
// separate defect when it is the same one.
|
||||
if (circles('mapbox') && !counts('mapbox')) fails.push('"Native" drew circles but no count labels — the numbers on the bubbles are the whole point of native clustering.')
|
||||
if (linesN('mapbox')) fails.push(`"Native" also drew ${linesN('mapbox')} cluster LINES — the previous mode was not torn down.`)
|
||||
if (!linesN('mst')) fails.push('"MST" drew no cluster lines.')
|
||||
if (circles('mst')) fails.push(`"MST" still shows ${circles('mst')} native cluster circles — the previous mode was not torn down.`)
|
||||
if (!linesN('network')) fails.push('"Network" drew no cluster lines.')
|
||||
if (circles('network')) fails.push(`"Network" still shows ${circles('network')} native cluster circles — the previous mode was not torn down.`)
|
||||
if (linesN('mst') && linesN('network') && linesN('mst') === linesN('network')) {
|
||||
fails.push(`"MST" and "Network" both drew exactly ${linesN('mst')} lines on the same data — two labels for one behaviour is the same defect as a dead option.`)
|
||||
}
|
||||
} catch (err) {
|
||||
fails.push(`the measurement itself failed: ${err && err.message}`)
|
||||
} finally {
|
||||
await browser.close().catch(() => {})
|
||||
cleanup()
|
||||
}
|
||||
|
||||
for (const l of lines) console.log(l)
|
||||
if (fails.length) {
|
||||
console.log(`\n${TAG}: FAIL`)
|
||||
for (const f of fails) console.log(` ✗ ${f}`)
|
||||
process.exit(1)
|
||||
}
|
||||
console.log(`${TAG}: PASS — each clustering option draws its own thing, and switching leaves no orphans.`)
|
||||
|
|
@ -36,10 +36,11 @@ import { spawn } from 'node:child_process'
|
|||
import { existsSync, readFileSync, readdirSync } from 'node:fs'
|
||||
import { resolve, dirname, join } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { TREE_NAME } from '../../../internal/_tree-name.mjs'
|
||||
|
||||
const TAG = 'COMPOSITE-LOCALE-LEAKS'
|
||||
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 LOCALES_DIR = join(ROOT, 'library', 'i18n', 'locales')
|
||||
const skip = (why) => { console.log(`${TAG}: SKIP — ${why}`); process.exit(0) }
|
||||
|
||||
|
|
@ -106,7 +107,7 @@ const freePort = () => new Promise((ok) => {
|
|||
const s = createServer().listen(0, '127.0.0.1', () => { const p = s.address().port; s.close(() => ok(p)) })
|
||||
})
|
||||
const PORT = await freePort()
|
||||
const TREE = `http://127.0.0.1:${PORT}/doxa-maps`
|
||||
const TREE = `http://127.0.0.1:${PORT}/${TREE_NAME}`
|
||||
const tree = spawn(process.execPath, ['internal/serve-strict.mjs', '--port', String(PORT)], { cwd: ROOT, stdio: 'ignore' })
|
||||
const cleanup = () => { try { tree.kill() } catch { /* already gone */ } }
|
||||
await new Promise((r) => setTimeout(r, 900))
|
||||
|
|
|
|||
|
|
@ -46,10 +46,11 @@ import { spawn } from 'node:child_process'
|
|||
import { existsSync, readFileSync } from 'node:fs'
|
||||
import { resolve, dirname, join } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { TREE_NAME } from '../../../internal/_tree-name.mjs'
|
||||
|
||||
const TAG = 'LINKED-VOCABULARY'
|
||||
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 skip = (why) => { console.log(`${TAG}: SKIP — ${why}`); process.exit(0) }
|
||||
|
||||
if (!existsSync(MANIFEST)) skip('no built manifest (run `bun run tree` first).')
|
||||
|
|
@ -65,7 +66,7 @@ const freePort = () => new Promise((ok) => {
|
|||
const s = createServer().listen(0, '127.0.0.1', () => { const p = s.address().port; s.close(() => ok(p)) })
|
||||
})
|
||||
const PORT = await freePort()
|
||||
const TREE = `http://127.0.0.1:${PORT}/doxa-maps`
|
||||
const TREE = `http://127.0.0.1:${PORT}/${TREE_NAME}`
|
||||
const LEAF = `${TREE}/${String(profile.iframeUrl).replace(/^\.\//, '')}`
|
||||
|
||||
const tree = spawn(process.execPath, ['internal/serve-strict.mjs', '--port', String(PORT)], { cwd: ROOT, stdio: 'ignore' })
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
|
@ -29,7 +29,7 @@
|
|||
|
||||
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
|
||||
<!-- Bundle IIFE — registers the <all-people-groups> custom element. -->
|
||||
<script src="../all-people-groups.js" integrity="sha384-RmScC/lIkolm1Uf5WbPgFbjcZBfbjHOadsDnRbrtdW/6lM4cqSoi6bEL9VFYQJfT" crossorigin="anonymous"></script>
|
||||
<script src="../all-people-groups.js" integrity="sha384-iaWALwDn+RT8rjMuD8GwyKa93ZvomqAvmBf6XMuDyqrbq/VGAuPTtT+6DMxXtFjG" crossorigin="anonymous"></script>
|
||||
|
||||
<script>
|
||||
// Mapbox token resolution — the A3 ladder, in precedence order:
|
||||
|
|
|
|||
48
public/doxa-maps/cities-internet/cities-internet.js
generated
48
public/doxa-maps/cities-internet/cities-internet.js
generated
File diff suppressed because one or more lines are too long
|
|
@ -26,7 +26,7 @@
|
|||
|
||||
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
|
||||
<!-- Bundle IIFE — registers the <cities-internet-map> custom element. -->
|
||||
<script src="../cities-internet.js" integrity="sha384-b+LM9iwu9nc2PjVLOdnkhEyCzVekbagHbsbhifn8s6e6TlPmCIiaMlVrs3CUCtp2" crossorigin="anonymous"></script>
|
||||
<script src="../cities-internet.js" integrity="sha384-4Vw0NTwm8CYuoQ4GZ72er51cbjuljo1e6szPVAxVUZGXjH9KZneXoLqcRopGmYPF" crossorigin="anonymous"></script>
|
||||
|
||||
<script>
|
||||
// Mapbox token resolution — the A3 ladder, in precedence order:
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@
|
|||
|
||||
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
|
||||
<!-- Bundle IIFE — registers the <cities-internet-map> custom element. -->
|
||||
<script src="../cities-internet.js" integrity="sha384-b+LM9iwu9nc2PjVLOdnkhEyCzVekbagHbsbhifn8s6e6TlPmCIiaMlVrs3CUCtp2" crossorigin="anonymous"></script>
|
||||
<script src="../cities-internet.js" integrity="sha384-4Vw0NTwm8CYuoQ4GZ72er51cbjuljo1e6szPVAxVUZGXjH9KZneXoLqcRopGmYPF" crossorigin="anonymous"></script>
|
||||
|
||||
<script>
|
||||
// Mapbox token resolution — the A3 ladder, in precedence order:
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@
|
|||
|
||||
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
|
||||
<!-- Bundle IIFE — registers the <cities-internet-map> custom element. -->
|
||||
<script src="../cities-internet.js" integrity="sha384-b+LM9iwu9nc2PjVLOdnkhEyCzVekbagHbsbhifn8s6e6TlPmCIiaMlVrs3CUCtp2" crossorigin="anonymous"></script>
|
||||
<script src="../cities-internet.js" integrity="sha384-4Vw0NTwm8CYuoQ4GZ72er51cbjuljo1e6szPVAxVUZGXjH9KZneXoLqcRopGmYPF" crossorigin="anonymous"></script>
|
||||
|
||||
<script>
|
||||
// Mapbox token resolution — the A3 ladder, in precedence order:
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@
|
|||
|
||||
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
|
||||
<!-- Bundle IIFE — registers the <country-posters-map> custom element. -->
|
||||
<script src="../country-posters.js" integrity="sha384-8mq4BeDqzXgVRQdfa4DeKpAcQLBJhh5zu3c98dK9lgEzZ5tI27NxofDMXCVgk8Ed" crossorigin="anonymous"></script>
|
||||
<script src="../country-posters.js" integrity="sha384-pHQsGBTvyKxe4ZxxS5IuoCo/qgYD0LFvXBayl4kmbpO02IMfxuO7DDJ2FPD+D397" crossorigin="anonymous"></script>
|
||||
|
||||
<script>
|
||||
// Mapbox token resolution — the A3 ladder, in precedence order:
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@
|
|||
|
||||
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
|
||||
<!-- Bundle IIFE — registers the <country-posters-map> custom element. -->
|
||||
<script src="../country-posters.js" integrity="sha384-8mq4BeDqzXgVRQdfa4DeKpAcQLBJhh5zu3c98dK9lgEzZ5tI27NxofDMXCVgk8Ed" crossorigin="anonymous"></script>
|
||||
<script src="../country-posters.js" integrity="sha384-pHQsGBTvyKxe4ZxxS5IuoCo/qgYD0LFvXBayl4kmbpO02IMfxuO7DDJ2FPD+D397" crossorigin="anonymous"></script>
|
||||
|
||||
<script>
|
||||
// Mapbox token resolution — the A3 ladder, in precedence order:
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@
|
|||
|
||||
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
|
||||
<!-- Bundle IIFE — registers the <country-posters-map> custom element. -->
|
||||
<script src="../country-posters.js" integrity="sha384-8mq4BeDqzXgVRQdfa4DeKpAcQLBJhh5zu3c98dK9lgEzZ5tI27NxofDMXCVgk8Ed" crossorigin="anonymous"></script>
|
||||
<script src="../country-posters.js" integrity="sha384-pHQsGBTvyKxe4ZxxS5IuoCo/qgYD0LFvXBayl4kmbpO02IMfxuO7DDJ2FPD+D397" crossorigin="anonymous"></script>
|
||||
|
||||
<script>
|
||||
// Mapbox token resolution — the A3 ladder, in precedence order:
|
||||
|
|
|
|||
186
public/doxa-maps/country-posters/country-posters.js
generated
186
public/doxa-maps/country-posters/country-posters.js
generated
File diff suppressed because one or more lines are too long
2
public/doxa-maps/demo-map/demo-map.js
generated
2
public/doxa-maps/demo-map/demo-map.js
generated
File diff suppressed because one or more lines are too long
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue