UNPKG

@mintlify/link-rot

Version:

Static checking for broken internal links

95 lines (94 loc) 3.69 kB
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; const CONCURRENCY = 5; const TIMEOUT_MS = 10000; const USER_AGENT = 'Mintlify-LinkChecker/1.0'; const isHttpUrl = (url) => { try { const parsed = new URL(url); return parsed.protocol === 'http:' || parsed.protocol === 'https:'; } catch (_a) { return false; } }; const checkExternalUrl = (url) => __awaiter(void 0, void 0, void 0, function* () { const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), TIMEOUT_MS); try { const response = yield fetch(url, { method: 'HEAD', signal: controller.signal, headers: { 'User-Agent': USER_AGENT }, redirect: 'follow', }); clearTimeout(timeoutId); return { ok: response.status !== 404, status: response.status, }; } catch (err) { clearTimeout(timeoutId); return { ok: false, status: null, error: err instanceof Error ? err.message : 'unknown error', }; } }); const pMap = (items, fn, concurrency) => __awaiter(void 0, void 0, void 0, function* () { const results = []; let index = 0; const run = () => __awaiter(void 0, void 0, void 0, function* () { while (index < items.length) { const currentIndex = index++; results[currentIndex] = yield fn(items[currentIndex]); } }); const workers = Array.from({ length: Math.min(concurrency, items.length) }, () => run()); yield Promise.all(workers); return results; }); export const getBrokenExternalLinks = (graph, options) => __awaiter(void 0, void 0, void 0, function* () { const urlSourceMap = new Map(); for (const [nodeLabel, externalPaths] of graph.getExternalPathsByNode()) { if ((options === null || options === void 0 ? void 0 : options.sourceFiles) && !options.sourceFiles.has(nodeLabel)) continue; for (const mdxPath of externalPaths) { if (isHttpUrl(mdxPath.originalPath)) { const url = mdxPath.originalPath; const existing = urlSourceMap.get(url); const source = { file: nodeLabel, originalPath: mdxPath.originalPath }; if (existing) { existing.push(source); } else { urlSourceMap.set(url, [source]); } } } } const uniqueUrls = [...urlSourceMap.keys()]; const results = yield pMap(uniqueUrls, (url) => __awaiter(void 0, void 0, void 0, function* () { const result = yield checkExternalUrl(url); if (result.ok) return null; const entry = { url, status: result.status, sources: urlSourceMap.get(url), }; if (result.error) entry.error = result.error; return entry; }), CONCURRENCY); return results.filter((r) => r !== null); });