106 lines
3.1 KiB
JavaScript
106 lines
3.1 KiB
JavaScript
#!/usr/bin/env node
|
|
/**
|
|
* CI: spot-check markdown links to repo-relative paths (../ or ./ from each file).
|
|
*
|
|
* Skips `packages/bare-os-seeder/kernel/**`: it is a byte-identical copy of `kernel/`
|
|
* (see verify-kernel-seeder-parity.mjs). Links in those files are validated from the
|
|
* canonical `kernel/**` paths only.
|
|
*/
|
|
import fs from 'node:fs'
|
|
import path from 'node:path'
|
|
import process from 'node:process'
|
|
import { fileURLToPath } from 'node:url'
|
|
|
|
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..')
|
|
|
|
const GLOBS = [
|
|
'docs',
|
|
'handbook',
|
|
'developer-guide',
|
|
'scripts',
|
|
'users-manual',
|
|
'packages',
|
|
'kernel',
|
|
]
|
|
|
|
const ROOT_MD_FILES = ['README.md', 'DOCUMENTATION.md', 'PEAR-RUN.md']
|
|
|
|
/** Copied into `kernel/lib/bare/README.md`; links are relative to that path. */
|
|
const SKIP_FILES = new Set(['packages/bare-os-bare-libs/README.kernel-lib-bare.md'])
|
|
|
|
function isSkippedPath(relFromRoot) {
|
|
const norm = relFromRoot.split(path.sep).join('/')
|
|
return norm === 'packages/bare-os-seeder/kernel' || norm.startsWith('packages/bare-os-seeder/kernel/')
|
|
}
|
|
|
|
/**
|
|
* @param {string} dir
|
|
* @returns {string[]}
|
|
*/
|
|
function walkMd(dir) {
|
|
const abs = path.join(root, dir)
|
|
if (!fs.existsSync(abs)) return []
|
|
/** @type {string[]} */
|
|
const out = []
|
|
const st = fs.statSync(abs)
|
|
if (st.isFile() && abs.endsWith('.md')) {
|
|
out.push(abs)
|
|
return out
|
|
}
|
|
if (!st.isDirectory()) return out
|
|
for (const ent of fs.readdirSync(abs, { withFileTypes: true })) {
|
|
if (ent.name === 'node_modules' || ent.name === '.git') continue
|
|
const p = path.join(abs, ent.name)
|
|
const relFromRoot = p.slice(root.length + 1)
|
|
if (isSkippedPath(relFromRoot)) continue
|
|
if (ent.isDirectory()) out.push(...walkMd(relFromRoot))
|
|
else if (ent.isFile() && ent.name.endsWith('.md')) out.push(p)
|
|
}
|
|
return out
|
|
}
|
|
|
|
/**
|
|
* @param {string} file
|
|
* @param {string[]} errors
|
|
*/
|
|
function collectMdLinkErrors(file, errors) {
|
|
const rel = path.relative(root, file)
|
|
if (SKIP_FILES.has(rel.split(path.sep).join('/')) || isSkippedPath(rel)) return
|
|
const text = fs.readFileSync(file, 'utf8')
|
|
const re = /\]\(([^)#\s]+\.md)(#[^)]*)?\)/g
|
|
let m
|
|
while ((m = re.exec(text))) {
|
|
const target = m[1]
|
|
if (target.startsWith('http')) continue
|
|
const resolved = path.normalize(path.join(path.dirname(file), target))
|
|
if (!fs.existsSync(resolved)) {
|
|
errors.push(
|
|
`${path.relative(root, file)} → missing ${path.relative(root, resolved)}`
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
function main() {
|
|
/** @type {string[]} */
|
|
const errors = []
|
|
for (const g of GLOBS) {
|
|
for (const file of walkMd(g)) {
|
|
collectMdLinkErrors(file, errors)
|
|
}
|
|
}
|
|
for (const name of ROOT_MD_FILES) {
|
|
const file = path.join(root, name)
|
|
if (fs.existsSync(file)) collectMdLinkErrors(file, errors)
|
|
}
|
|
if (errors.length) {
|
|
console.error('verify-doc-links: broken relative .md links:')
|
|
for (const e of errors.slice(0, 80)) console.error(' ', e)
|
|
if (errors.length > 80) console.error(` … and ${errors.length - 80} more`)
|
|
process.exit(1)
|
|
}
|
|
console.log('verify-doc-links: OK')
|
|
}
|
|
|
|
main()
|