doxa-maps-cdn/scripts/check-runtime-origins.ts
doxa-maps 9e6e9a4bff Plugin Studio branch: lint the new code to the site's style
The map slot's template puts one attribute per line. The runtime loader and the
map composable put one statement per line. The origins check drops a redundant
pair of parentheses. No behaviour changes. Every file this branch touched now
has no more lint findings than it had before.
2026-09-29 05:12:51 +00:00

82 lines
4 KiB
TypeScript

#!/usr/bin/env bun
// check-runtime-origins: no deployment's address is baked into this site.
//
// The plugin-studio branch sends readers to the Research Gallery and loads its maps from the gallery's
// /cdn. 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`)