57 lines
2.0 KiB
JavaScript
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.`)
|
|
}
|