| #!/usr/bin/env node |
| /** |
| * Used to check whether there are broken links in the modified files in the pipeline |
| */ |
| |
| |
| const { execSync } = require("child_process"); |
| const fs = require("fs"); |
| const path = require("path"); |
| |
| const commitHash = process.argv[2]; |
| |
| if (!commitHash) { |
| console.error("❌ Please provide the commit hash, such as: node check-dead-links.js <commit-hash>"); |
| process.exit(1); |
| } |
| |
| const linkRegex = /\[.*?\]\((.*?)\)/g; |
| let hasBrokenLinks = false; |
| |
| // Get the modified or newly added .md/.mdx files in the commit |
| function getModifiedMarkdownFiles(commit) { |
| const output = execSync( |
| `git show --name-status --diff-filter=AM ${commit} -- '*.md' '*.mdx'`, |
| { encoding: "utf-8", maxBuffer: 100 * 1024 * 1024 } |
| ); |
| const lines = output.split("\n"); |
| console.log('lines',lines); |
| |
| const files = []; |
| |
| for (const line of lines) { |
| const parts = line.trim().split(/\s+/); |
| if (parts.length === 2) { |
| const [status, filePath] = parts; |
| if ((status === "A" || status === "M") && (filePath.endsWith(".md") || filePath.endsWith(".mdx"))) { |
| files.push(filePath); |
| } |
| } |
| } |
| |
| return files; |
| } |
| |
| // Checks if the link points to an existing local file |
| function isLocalLink(link) { |
| return !link.startsWith("http://") && |
| !link.startsWith("https://") && |
| !link.startsWith("mailto:") && |
| !link.startsWith("#") && |
| !path.isAbsolute(link)&& |
| !/.*@.*\..*/.test(link); |
| } |
| |
| // Strip Markdown code regions so the link scanner ignores their contents. |
| // |
| // The previous implementation used `/```[\s\S]*?```/g` followed by `` /`[^`]*`/g ``, |
| // which mishandles docs whose inline code itself contains triple backticks |
| // (e.g. a table cell that reads "` ```shell `"). In that case the global |
| // triple-backtick regex paired ``` across rows and across real fenced blocks, |
| // shifting the open/close boundaries and leaving real code regions visible |
| // to the link scanner — which then reported their contents as broken links. |
| // |
| // Correct rules (a subset of CommonMark sufficient for our docs): |
| // 1. A fenced code block opens with 3+ backticks at the start of a line |
| // (up to 3 leading spaces). It closes with another run of >= that many |
| // backticks at the start of a line. Anything else (e.g. ``` inside a |
| // table cell) is NOT a fence. |
| // 2. An inline code span opens with N backticks and closes with the next |
| // run of EXACTLY N backticks. The content may contain shorter runs. |
| function removeCodeBlocks(content) { |
| const lines = content.split("\n"); |
| const stripped = []; |
| let openFenceLen = 0; |
| for (const line of lines) { |
| if (openFenceLen === 0) { |
| const m = line.match(/^ {0,3}(`{3,})/); |
| if (m) { |
| openFenceLen = m[1].length; |
| stripped.push(""); |
| continue; |
| } |
| stripped.push(line); |
| } else { |
| const m = line.match(/^ {0,3}(`{3,})\s*$/); |
| if (m && m[1].length >= openFenceLen) { |
| openFenceLen = 0; |
| } |
| stripped.push(""); |
| } |
| } |
| |
| let result = stripped.join("\n"); |
| result = result.replace(/(`+)(?:(?!\1)[\s\S])*?\1/g, ""); |
| return result; |
| } |
| |
| |
| // Check links in files |
| function checkFileLinks(filePath) { |
| const content = fs.readFileSync(filePath, "utf-8"); |
| const dir = path.dirname(filePath); |
| |
| const cleanedContent = removeCodeBlocks(content); |
| const matches = [...cleanedContent.matchAll(linkRegex)]; |
| for (const match of matches) { |
| const rawLink = match[1].split("#")[0]; // Remove anchor point |
| if (!isLocalLink(rawLink)) continue; |
| console.log('rawLink',rawLink); |
| |
| let fullPath = path.resolve(dir, rawLink); |
| if (!fs.existsSync(fullPath)) { |
| // Try adding a .md/.mdx suffix and try again |
| if (fs.existsSync(fullPath + ".md")) continue; |
| if (fs.existsSync(fullPath + ".mdx")) continue; |
| |
| console.error(`❌ ${filePath}: Broken link -> ${rawLink}`); |
| hasBrokenLinks = true; |
| } |
| } |
| } |
| |
| // Main function |
| function main() { |
| const files = getModifiedMarkdownFiles(commitHash); |
| if (files.length === 0) { |
| console.log("✅ Unmodified Markdown files"); |
| return; |
| } |
| |
| for (const file of files) { |
| if (fs.existsSync(file)) { |
| checkFileLinks(file); |
| } |
| } |
| |
| |
| if (hasBrokenLinks) { |
| console.error("❗ A broken link was detected. Please fix it and submit."); |
| process.exit(1); |
| } else { |
| console.log("✅ All links are OK"); |
| } |
| } |
| |
| main(); |