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.
82 lines
4 KiB
TypeScript
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`)
|