doxa-maps-cdn/scripts/check-runtime-origins.ts
doxa-maps fa3591ca9b Subdomain branch: the gallery and maps addresses are read per request, as on the Plugin Studio branch
dx-base's three commits from research-gallery-plugin-studio (the runtime loader, check:origins and
its lint pass) apply here unchanged: the code is byte-identical to that branch. What differs is what
the two settings point at on this route. The Research Gallery is the research site the team's
dx-maps container serves, and the maps load from the container's published tree, which the research
subdomain serves at /dx-maps (the Studio's host serves the same tree at /cdn/dx-maps; both send
CORS). The comments, .env.example and DX-MAPS-GALLERY.md now say so, with this route's laptop
values (research.localhost:4615, as compose.laptop.yaml already sets).

DX-MAPS-GALLERY.md's "Run it" said no .env is needed for /research. That stopped being true when the
default maps host was removed, so it now names the one setting the maps need.

check:origins: 15 problems on research-gallery-subdomain before, PASS (306 files) after.
2026-09-29 05:19:41 +00:00

82 lines
4 KiB
TypeScript

#!/usr/bin/env bun
// check-runtime-origins: no deployment's address is baked into this site.
//
// This site sends readers to the Research Gallery and loads its maps from the tree the gallery's
// subdomain publishes. Both addresses differ per deployment, so the server reads them at REQUEST time
// (server/utils/dx-runtime.ts), and pages ask /api/dx-runtime or link through /research-gallery. About
// 4,000 pages are prerendered, and a prerendered page carries the build's config. An address that
// reaches nuxt.config's runtimeConfig, or a page, is baked into the build, which is how the CDN branch
// failed. This check holds the line:
// 1. nuxt.config.ts declares neither address;
// 2. under app/, only app/utils/dxRuntime.ts touches dxMapsBase, and only it, app/utils/nav.ts and
// app/components/public/TheHeader.vue touch researchGalleryUrl (the menu's key, resolved to
// /research-gallery);
// 3. no source file or static page names a deployment host (nuxtbase.app, sslip.io, a *.localhost
// address or a 46xx laptop port), since those belong in the environment.
// Static: reads files, starts nothing. `bun scripts/check-runtime-origins.ts [root]`. Exit 0 pass, 1 fail.
import { readdirSync, readFileSync, statSync } from 'node:fs'
import { join, relative, resolve } from 'node:path'
const ROOT = resolve(process.argv[2] || join(import.meta.dir, '..'))
const fails: string[] = []
let scanned = 0
function walk(dir: string, out: string[] = []): string[] {
for (const e of readdirSync(dir)) {
if (e === 'node_modules' || e.startsWith('.')) continue
const p = join(dir, e)
if (statSync(p).isDirectory()) walk(p, out)
else if (/\.(vue|ts|js|mjs|html)$/.test(e)) out.push(p)
}
return out
}
const rel = (p: string) => relative(ROOT, p)
const lines = (text: string, re: RegExp) =>
text.split('\n').map((l, i) => [i + 1, l] as const).filter(([, l]) => re.test(l))
// 1. nuxt.config.ts declares neither address.
const config = readFileSync(join(ROOT, 'nuxt.config.ts'), 'utf8')
scanned += 1
for (const [n, l] of lines(config, /\b(dxMapsBase|researchGalleryUrl)\s*:/)) {
fails.push(`nuxt.config.ts:${n} declares a per-deployment address in the build's config: ${l.trim()}`)
}
// 2. Only the runtime loader (and the menu's key) touch the two names under app/.
const ALLOWED: Record<string, string[]> = {
dxMapsBase: ['app/utils/dxRuntime.ts'],
researchGalleryUrl: ['app/utils/dxRuntime.ts', 'app/utils/nav.ts', 'app/components/public/TheHeader.vue']
}
for (const file of walk(join(ROOT, 'app'))) {
scanned += 1
const text = readFileSync(file, 'utf8')
for (const [name, allowed] of Object.entries(ALLOWED)) {
if (allowed.includes(rel(file))) continue
for (const [n, l] of lines(text, new RegExp(`\\b${name}\\b`))) {
fails.push(`${rel(file)}:${n} reads ${name} outside the runtime loader (use loadDxRuntime or /research-gallery): ${l.trim()}`)
}
}
}
// 3. No deployment host in source or static pages (data under public/assets is not scanned).
const HOST = /(\bnuxtbase\.app\b|\bsslip\.io\b|\b[a-z0-9-]+\.localhost\b|localhost:46\d\d)/
const sources = [
...walk(join(ROOT, 'app')), ...walk(join(ROOT, 'server')), ...walk(join(ROOT, 'modules')),
join(ROOT, 'nuxt.config.ts'),
...walk(join(ROOT, 'public')).filter(f => f.endsWith('.html') && !rel(f).startsWith('public/assets/'))
]
for (const file of sources) {
scanned += 1
for (const [n, l] of lines(readFileSync(file, 'utf8'), HOST)) {
fails.push(`${rel(file)}:${n} names a deployment host (it belongs in the environment): ${l.trim().slice(0, 160)}`)
}
}
// Refuse to pass vacuously: the walk must have read the site.
if (scanned < 50) fails.push(`only ${scanned} file(s) scanned under ${ROOT}: the walk is broken or the root is wrong`)
if (fails.length) {
console.error(`RUNTIME-ORIGINS: FAIL — ${fails.length} problem(s)`)
for (const f of fails) console.error(` ✗ ${f}`)
process.exit(1)
}
console.log(`RUNTIME-ORIGINS: PASS — ${scanned} files; no deployment address in the build's config, the pages or the static pages`)