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