import { existsSync, readdirSync, readFileSync, statSync } from "node:fs"; import { extname, join, normalize } from "node:path"; const root = new URL("../dist/", import.meta.url); const rootPath = root.pathname; function walk(directory) { return readdirSync(directory).flatMap((name) => { const path = join(directory, name); return statSync(path).isDirectory() ? walk(path) : [path]; }); } function routeFile(pathname) { const clean = decodeURIComponent(pathname).replace(/^\/+/, ""); if (!clean) return join(rootPath, "index.html"); const local = normalize(join(rootPath, clean)); if (!local.startsWith(normalize(rootPath))) return null; if (extname(local)) return local; return join(local, "index.html"); } if (!existsSync(rootPath)) { console.error("dist/ 不存在;请先运行 npm run build。"); process.exit(1); } const htmlFiles = walk(rootPath).filter((file) => file.endsWith(".html")); const anchors = new Map(); for (const file of htmlFiles) { const html = readFileSync(file, "utf8"); anchors.set(file, new Set([...html.matchAll(/\sid=["']([^"']+)["']/g)].map((match) => match[1]))); } let references = 0; let anchorReferences = 0; const failures = []; for (const source of htmlFiles) { const html = readFileSync(source, "utf8"); const hrefs = [...html.matchAll(/\shref=["']([^"']+)["']/g)].map((match) => match[1]); for (const href of hrefs) { if (!href.startsWith("/") || href.startsWith("//")) continue; references += 1; const url = new URL(href, "https://llm-atlas.local"); const target = routeFile(url.pathname); if (!target || !existsSync(target)) { failures.push(`${source.replace(rootPath, "/")} → ${href}(目标不存在)`); continue; } if (url.hash) { anchorReferences += 1; const id = decodeURIComponent(url.hash.slice(1)); if (!anchors.get(target)?.has(id)) { failures.push(`${source.replace(rootPath, "/")} → ${href}(锚点不存在)`); } } } } console.log( `${htmlFiles.length} 个页面,${references} 个站内引用,${anchorReferences} 个跨页锚点,${failures.length} 个失败。`, ); if (failures.length) { failures.forEach((failure) => console.error(`- ${failure}`)); process.exit(1); }