Files
2026-07-16 00:12:26 -07:00

57 lines
2.0 KiB
JavaScript

/**
* Verify repository-local Markdown links without making network requests.
*
* External URLs are intentionally skipped: CI should not fail because an
* upstream website is temporarily unavailable. Local documentation links are
* deterministic and catch renamed component guides or incorrect relative paths.
*/
import { existsSync, readdirSync, readFileSync } from 'node:fs'
import { dirname, extname, resolve } from 'node:path'
const root = resolve(process.argv[2] ?? new URL('../../..', import.meta.url).pathname)
const ignoredDirectories = new Set([
'.agents',
'.codex',
'.git',
'dist',
'node_modules',
'target',
'vendor',
])
const markdownFiles = []
function collect(directory) {
for (const entry of readdirSync(directory, { withFileTypes: true })) {
if (entry.isDirectory() && ignoredDirectories.has(entry.name)) continue
const path = resolve(directory, entry.name)
if (entry.isDirectory()) collect(path)
else if (extname(entry.name).toLowerCase() === '.md') markdownFiles.push(path)
}
}
collect(root)
const failures = []
const markdownLink = /\[[^\]]*\]\(([^)]+)\)/g
for (const file of markdownFiles) {
const source = readFileSync(file, 'utf8')
for (const match of source.matchAll(markdownLink)) {
const rawTarget = match[1].trim().replace(/^<|>$/g, '')
if (!rawTarget || rawTarget.startsWith('#') || /^[a-z][a-z+.-]*:/i.test(rawTarget)) continue
// The project does not currently use titled local links. Splitting here
// still handles the conventional `(path "title")` form if one is added.
const target = decodeURI(rawTarget.split(/\s+["']/)[0].split('#')[0])
if (!target) continue
const destination = resolve(dirname(file), target)
if (!existsSync(destination)) failures.push(`${file}: ${rawTarget}`)
}
}
if (failures.length > 0) {
console.error(`Broken local documentation links:\n${failures.join('\n')}`)
process.exitCode = 1
} else {
console.log(`Checked ${markdownFiles.length} Markdown files; local links are valid.`)
}