| | |
| | |
| | |
| | |
| | |
| |
|
| | import fs from "node:fs"; |
| | import { |
| | execGit, |
| | getRootDir, |
| | walkSync, |
| | getLocations, |
| | IMG_RX, |
| | stringToFragment, |
| | } from "./utils.js"; |
| |
|
| | const rootDir = getRootDir(); |
| | const fileCache = new Map(); |
| | const anchorCache = new Map(); |
| | const deletedSlugs = []; |
| | const addedFragmentDetails = []; |
| | let deletedFragmentDetails = []; |
| | let isAllOk = true; |
| |
|
| | function getFileContent(path) { |
| | if (fileCache.has(path)) { |
| | return fileCache.get(path); |
| | } |
| |
|
| | const content = fs.readFileSync(path, "utf-8"); |
| | fileCache.set(path, content); |
| | return content; |
| | } |
| |
|
| | function getFileAnchors(path) { |
| | if (anchorCache.has(path)) { |
| | return anchorCache.get(path); |
| | } |
| | const content = getFileContent(path); |
| | const headerAnchors = [...content.matchAll(/^#+ .*?$/gm)] |
| | .map((match) => match[0].toLowerCase()) |
| | .map((h) => h.replace(/#+ /g, "")) |
| | .map((h) => stringToFragment(h)); |
| | anchorCache.set(path, headerAnchors); |
| | return headerAnchors; |
| | } |
| |
|
| | function getDeletedSlugs(fromStaging = true) { |
| | let result = ""; |
| |
|
| | if (fromStaging) { |
| | |
| | result = execGit(["status", "--short", "--porcelain"], { cwd: "." }); |
| | if (result.trim()) { |
| | deletedSlugs.push( |
| | ...result |
| | .split("\n") |
| | .filter( |
| | (line) => |
| | /^\s*D\s+/gi.test(line) && |
| | line.includes("files/en-us") && |
| | (IMG_RX.test(line) || line.includes("index.md")), |
| | ) |
| | .map((line) => |
| | line.replaceAll(/^\s+|files\/en-us\/|\/index.md/gm, ""), |
| | ) |
| | .map((line) => line.split(/\s+/)[1]), |
| | ); |
| | } |
| | } else { |
| | |
| | result = execGit(["diff", "--summary", "origin/main...HEAD"], { cwd: "." }); |
| | if (result.trim()) { |
| | deletedSlugs.push( |
| | ...result |
| | .split("\n") |
| | .filter( |
| | (line) => |
| | line.includes("delete mode") && |
| | line.includes("files/en-us") && |
| | (IMG_RX.test(line) || line.includes("index.md")), |
| | ) |
| | .map((line) => line.replace(/^\s*delete mode \d+ /gm, "")) |
| | .map((line) => |
| | line.replaceAll(/^\s+|files\/en-us\/|\/index.md/gm, ""), |
| | ), |
| | ); |
| | } |
| | } |
| | } |
| |
|
| | function getFragmentDetails(fromStaging = true) { |
| | let result = ""; |
| |
|
| | if (fromStaging) { |
| | |
| | result = execGit(["diff", "HEAD"], { cwd: "." }); |
| | } else { |
| | |
| | result = execGit(["diff", "origin/main...HEAD"], { cwd: "." }); |
| | } |
| |
|
| | if (result.trim()) { |
| | const segments = [ |
| | ...result.split("diff --git a/").filter((segment) => segment !== ""), |
| | ]; |
| | for (const segment of segments) { |
| | const path = segment |
| | .substring(0, segment.indexOf(" ")) |
| | .replaceAll(/files\/en-us\/|\/index.md/gm, ""); |
| |
|
| | const addedHeaderRx = /^\+#+ .*$/gm; |
| | const addedFragments = [...segment.matchAll(addedHeaderRx)] |
| | .map((match) => match[0].toLowerCase()) |
| | .map((header) => header.replace(/\+#+ /g, "")) |
| | .map((header) => stringToFragment(header)); |
| |
|
| | const removedHeaderRx = /^-#+ .*$/gm; |
| | [...segment.matchAll(removedHeaderRx)] |
| | .map((match) => match[0].toLowerCase()) |
| | .map((header) => header.replace(/-#+ /g, "")) |
| | .map((header) => stringToFragment(header)) |
| | .filter((header) => !addedFragments.includes(header)) |
| | .filter((header) => { |
| | |
| | const absPath = `${rootDir}/files/en-us/${path}/index.md`; |
| | if (!fs.existsSync(absPath)) { |
| | return true; |
| | } |
| | const headerAnchors = getFileAnchors(absPath); |
| | return !headerAnchors.includes(header); |
| | }) |
| | .forEach((header) => { |
| | const fragment = `${path}#${header}`; |
| | if (!deletedFragmentDetails.includes(fragment)) { |
| | deletedFragmentDetails.push(fragment); |
| | } |
| | }); |
| |
|
| | addedFragments.forEach((header) => |
| | addedFragmentDetails.push(`${path}#${header}`), |
| | ); |
| | } |
| | } |
| | } |
| |
|
| | if (process.argv[2] !== "--workflow") { |
| | getDeletedSlugs(); |
| | getFragmentDetails(); |
| | } else { |
| | getDeletedSlugs(false); |
| | getFragmentDetails(false); |
| | } |
| |
|
| | deletedFragmentDetails = deletedFragmentDetails.filter( |
| | (fragment) => !addedFragmentDetails.includes(fragment), |
| | ); |
| |
|
| | if (deletedSlugs.length < 1 && deletedFragmentDetails.length < 1) { |
| | console.log("Nothing to check. 🎉"); |
| | process.exit(0); |
| | } |
| |
|
| | console.log("deletedSlugs", deletedSlugs); |
| | console.log("deletedFragmentDetails", deletedFragmentDetails); |
| |
|
| | for await (const filePath of walkSync(rootDir)) { |
| | if (filePath.endsWith("index.md")) { |
| | try { |
| | const content = |
| | fileCache.get(filePath) ?? fs.readFileSync(filePath, "utf-8"); |
| | const relativePath = filePath.substring(filePath.indexOf("files/en-us")); |
| |
|
| | |
| | for (const slug of deletedSlugs) { |
| | const locations = getLocations( |
| | content, |
| | new RegExp(`/${slug}[)># \"']`, "mig"), |
| | ); |
| | if (locations.length) { |
| | isAllOk = false; |
| | for (const location of locations) { |
| | console.error( |
| | `ERROR:${relativePath}:${location.line}:${location.column}:Slug '${slug}' has been deleted`, |
| | ); |
| | } |
| | } |
| | } |
| |
|
| | |
| | for (const fragment of deletedFragmentDetails) { |
| | const locations = getLocations(content, `${fragment}(?: "|\\))`); |
| | |
| | const urlParts = fragment.split("#"); |
| | if (filePath.includes(`${urlParts[0]}/index.md`)) { |
| | locations.push( |
| | ...getLocations(content, `\\(#${urlParts[1]}(?: "|\\))`), |
| | ); |
| | } |
| | if (locations.length) { |
| | isAllOk = false; |
| | for (const location of locations) { |
| | console.error( |
| | `ERROR:${relativePath}:${location.line}:${location.column}:URL fragment in URL '${fragment}' is broken`, |
| | ); |
| | } |
| | } |
| | } |
| | } catch (e) { |
| | console.error(`Error processing ${filePath}: ${e.message}`); |
| | throw e; |
| | } |
| | } |
| | } |
| |
|
| | if (!isAllOk) { |
| | process.exit(1); |
| | } |
| |
|