// cspell:ignore githubusercontent streetsidesoftware forgejo import { readFile, stat } from 'node:fs/promises'; import path from 'node:path'; import { walk } from './lib/walk.mjs'; const root = process.cwd(); // GitHub is no longer linked from anywhere on the site: project source links, // the social links, and the repo metadata all point at the self-hosted git // host instead. Matching the host substrings (rather than the bare word // "github") keeps the Shiki `github-light`/`github-dark` themes and the // Forgejo Actions `github.*` context variables from tripping the check. const bannedHosts = [/github\.com/i, /github\.io/i, /githubusercontent\.com/i]; // Upstream tooling URLs that genuinely live on GitHub and have no self-hosted // equivalent (e.g. the cspell config JSON Schema). A flagged URL is permitted // only when it exactly equals one of these. const permitted = new Set([ 'https://raw.githubusercontent.com/streetsidesoftware/cspell/main/cspell.schema.json', ]); const textExtensions = new Set([ '.astro', '.css', '.html', '.js', '.json', '.md', '.mjs', '.ts', '.txt', '.webmanifest', '.xml', ]); const roots = [ 'src', 'public', 'scripts', 'README.md', 'package.json', 'astro.config.mjs', 'cspell.json', ].map((entry) => path.resolve(root, entry)); async function exists(filePath) { try { await stat(filePath); return true; } catch { return false; } } // Grows a match outward to the whole whitespace/quote-delimited URL token so it // can be compared against the permitted set. function urlToken(line, index, length) { const boundary = /[\s"'`<>()[\]{}]/; let start = index; while (start > 0 && !boundary.test(line[start - 1])) start -= 1; let end = index + length; while (end < line.length && !boundary.test(line[end])) end += 1; return line.slice(start, end); } const files = []; for (const entry of roots) { if (!(await exists(entry))) continue; files.push(...(await walk(entry))); } const textFiles = files.filter((file) => textExtensions.has(path.extname(file))); const failures = []; for (const file of textFiles) { const lines = (await readFile(file, 'utf8')).split('\n'); lines.forEach((line, lineIndex) => { for (const pattern of bannedHosts) { for (const match of line.matchAll(new RegExp(pattern, 'gi'))) { const token = urlToken(line, match.index, match[0].length); if (permitted.has(token)) continue; const location = `${path.relative(root, file)}:${lineIndex + 1}:${match.index + 1}`; failures.push(location); } } }); } if (failures.length > 0) { console.error( `GitHub links are not allowed; link the self-hosted git host instead:\n${failures.join('\n')}` ); process.exit(1); } console.log('No GitHub links found.');