Source mirror · commit 369512f · 2026-10-26 · MIT · canonical repo: gitlab.com/jiapunk/upgrade-airlock
git clone https://upgrade-airlock.pages.dev/source/upgrade-airlock.git
upgrade-airlock / web / test / scan.ts
web/test/scan.ts54 lines · 2740 bytes
1// Shared by both self-tests: the identity scan and the files it covers.
2// The blocked names are not in the repo. They are read at test time from prep/blocked-names.txt (gitignored; one
3// name per line, '#' starts a comment), or from the file named by BLOCKED_NAMES_FILE. Without that file the name
4// check is skipped with a note, so a fresh clone and CI still pass; the local-path check always runs.
5import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs'
6import { join } from 'node:path'
7
8/** The self-tests run from web/ (npm run selftest), so the repo root is one level up */
9const NAMES_FILE = process.env.BLOCKED_NAMES_FILE || join(process.cwd(), '..', 'prep', 'blocked-names.txt')
10
11function loadNames(): string[] {
12 if (!existsSync(NAMES_FILE)) return []
13 return readFileSync(NAMES_FILE, 'utf8')
14 .split('\n')
15 .map((l) => l.replace(/#.*$/, '').trim())
16 .filter(Boolean)
17}
18export const BLOCKED: readonly string[] = loadNames()
19/** Says which name list the scan used; the self-tests print it next to their identity checks */
20export const NAMES_NOTE = BLOCKED.length
21 ? `${BLOCKED.length} ${BLOCKED.length === 1 ? 'name' : 'names'} from the local list`
22 : 'name check skipped, no prep/blocked-names.txt; local paths still checked'
23
24/** Local paths: a home directory on macOS or Linux, or a macOS temp dir (split so this file does not match itself) */
25export const LOCAL_PATH = new RegExp(['/Us', 'ers/'].join('') + '|' + ['/ho', 'me/'].join('') + '|' + ['/private/', 'var/'].join('') + '|' + ['/var/', 'folders/'].join(''))
26
27export function findBlocked(text: string): string[] {
28 const low = text.toLowerCase()
29 // a hit names the entry's position in the list, never the name itself, so test output stays clean too
30 const hits = BLOCKED.flatMap((b, i) => (low.includes(b.toLowerCase()) ? [`blocked name #${i + 1}`] : []))
31 if (LOCAL_PATH.test(text)) hits.push('local path')
32 return hits
33}
34
35/** Every source file of web/ that ships or builds (not node_modules, not build output) */
36export function webFiles(root: string): string[] {
37 const out: string[] = []
38 const walk = (dir: string) => {
39 for (const name of readdirSync(dir)) {
40 if (name === 'node_modules' || name.startsWith('dist') || name.startsWith('.')) continue
41 const p = join(dir, name)
42 if (statSync(p).isDirectory()) walk(p)
43 else if (/\.(ts|mjs|js|json|css|html|svg)$/.test(name) && name !== 'package-lock.json') out.push(p)
44 }
45 }
46 walk(root)
47 return out
48}
49
50export function scanWeb(root: string): { file: string; hits: string[] }[] {
51 return webFiles(root)
52 .map((file) => ({ file: file.slice(root.length + 1), hits: findBlocked(readFileSync(file, 'utf8')) }))
53 .filter((x) => x.hits.length)
54}