|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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); |
|
|
} |
|
|
|