Files
proxmox/scripts/check-doc-links.mjs
defiQUG bea1903ac9
Some checks failed
Deploy to Phoenix / deploy (push) Has been cancelled
Sync all local changes: docs, config, scripts, submodule refs, verification evidence
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-21 15:46:06 -08:00

79 lines
2.6 KiB
JavaScript

#!/usr/bin/env node
/**
* Check internal doc links: resolve relative paths from each source file
* and report only targets that are missing (file or directory).
* Usage: node scripts/check-doc-links.mjs
*/
import { readdirSync, readFileSync, existsSync } from 'fs';
import { join, resolve, dirname, normalize } from 'path';
import { fileURLToPath } from 'url';
const __dirname = dirname(fileURLToPath(import.meta.url));
const REPO_ROOT = resolve(__dirname, '..');
const DOCS = join(REPO_ROOT, 'docs');
function* walkMd(dir, prefix = '') {
const entries = readdirSync(dir, { withFileTypes: true });
for (const e of entries) {
const rel = prefix ? `${prefix}/${e.name}` : e.name;
if (e.isDirectory()) {
if (e.name === 'node_modules' || e.name === '.git') continue;
yield* walkMd(join(dir, e.name), rel);
} else if (e.name.endsWith('.md')) {
yield { path: join(dir, e.name), rel: join(prefix, e.name) };
}
}
}
const linkRe = /\]\(([^)]+)\)/g;
function resolveTarget(fromDir, href) {
const pathOnly = href.replace(/#.*$/, '').trim();
if (!pathOnly || pathOnly.startsWith('http://') || pathOnly.startsWith('https://') || pathOnly.startsWith('mailto:')) return null;
if (pathOnly.startsWith('#')) return null;
if (pathOnly.startsWith('~/')) return null; // skip home-relative
let resolved;
if (pathOnly.startsWith('/')) {
// repo-root-relative: /docs/... or /reports/...
resolved = normalize(join(REPO_ROOT, pathOnly.slice(1)));
} else {
resolved = normalize(join(fromDir, pathOnly));
}
return resolved.startsWith(REPO_ROOT) ? resolved : null;
}
const broken = [];
const seen = new Set();
for (const { path: filePath, rel } of walkMd(DOCS)) {
const fromDir = dirname(filePath);
const content = readFileSync(filePath, 'utf8');
let m;
linkRe.lastIndex = 0;
while ((m = linkRe.exec(content)) !== null) {
const href = m[1];
const targetPath = resolveTarget(fromDir, href);
if (!targetPath) continue;
const key = `${rel} -> ${href}`;
if (seen.has(key)) continue;
seen.add(key);
if (!existsSync(targetPath)) {
broken.push({ source: rel, link: href, resolved: targetPath.replace(REPO_ROOT + '/', '') });
}
}
}
console.log('=== Doc link check (docs/ only, relative links resolved from source file) ===\n');
if (broken.length === 0) {
console.log('No broken internal links found.');
process.exit(0);
}
console.log(`Found ${broken.length} broken link(s):\n`);
broken.forEach(({ source, link, resolved }) => {
console.log(` ${source}`);
console.log(` -> ${link}`);
console.log(` resolved: ${resolved}`);
console.log('');
});
process.exit(1);