86 lines
2.8 KiB
JavaScript
86 lines
2.8 KiB
JavaScript
#!/usr/bin/env node
|
|
/* eslint-disable no-console */
|
|
import { readFile } from "node:fs/promises";
|
|
import path from "node:path";
|
|
import { fileURLToPath } from "node:url";
|
|
|
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
const repoRoot = path.resolve(__dirname, "..");
|
|
const guideRelativePath = "docs/porting-guide.md";
|
|
const guidePath = path.join(repoRoot, guideRelativePath);
|
|
|
|
const requiredHeadings = [
|
|
"# Porting Guide",
|
|
"## Rename/Replace Map",
|
|
"## Do-Not-Replace Rules",
|
|
"## Verification",
|
|
];
|
|
const unresolvedMarkerPattern = /^(?!.*(?:inline TODO markers|node -e )).*\b(?:TODO|TBD|FIXME)\b.*$/m;
|
|
const minimumMapRows = 12;
|
|
|
|
function fail(message) {
|
|
console.error(`Porting Docs porting guide verification failed: ${message}`);
|
|
process.exit(1);
|
|
}
|
|
|
|
function headingPattern(heading) {
|
|
const escapedHeading = heading.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
if (heading === "# Porting Guide") return /^# .*Porting Guide\s*$/m;
|
|
return new RegExp(`^${escapedHeading}\\s*$`, "m");
|
|
}
|
|
|
|
function hasHeading(text, heading) {
|
|
return headingPattern(heading).test(text);
|
|
}
|
|
|
|
function sectionBody(text, heading) {
|
|
const lines = text.split(/\r?\n/);
|
|
const startIndex = lines.findIndex((line) => line.trim() === heading);
|
|
if (startIndex === -1) return "";
|
|
const currentLevel = heading.match(/^#+/)?.[0].length ?? 1;
|
|
const endIndex = lines.findIndex((line, index) => {
|
|
if (index <= startIndex) return false;
|
|
const match = line.match(/^(#+)\s+/);
|
|
return Boolean(match && match[1].length <= currentLevel);
|
|
});
|
|
return lines.slice(startIndex + 1, endIndex === -1 ? undefined : endIndex).join("\n");
|
|
}
|
|
|
|
function countUsefulTableRows(section) {
|
|
return section
|
|
.split(/\r?\n/)
|
|
.map((line) => line.trim())
|
|
.filter((line) => {
|
|
if (!line.startsWith("|") || !line.endsWith("|")) return false;
|
|
const cells = line
|
|
.split("|")
|
|
.slice(1, -1)
|
|
.map((cell) => cell.trim());
|
|
if (cells.length < 4) return false;
|
|
if (cells.every((cell) => /^:?-{3,}:?$/.test(cell))) return false;
|
|
return cells.every((cell) => cell.length > 0);
|
|
}).length;
|
|
}
|
|
|
|
let text;
|
|
try {
|
|
text = await readFile(guidePath, "utf8");
|
|
} catch (error) {
|
|
if (error?.code === "ENOENT") fail(`missing ${guideRelativePath}`);
|
|
throw error;
|
|
}
|
|
|
|
if (!text.trim()) fail(`${guideRelativePath} is empty`);
|
|
if (unresolvedMarkerPattern.test(text)) fail(`${guideRelativePath} contains unresolved TODO/TBD/FIXME markers`);
|
|
|
|
for (const heading of requiredHeadings) {
|
|
if (!hasHeading(text, heading)) fail(`missing required heading \`${heading}\``);
|
|
}
|
|
|
|
const mapRows = countUsefulTableRows(sectionBody(text, "## Rename/Replace Map"));
|
|
if (mapRows < minimumMapRows) {
|
|
fail(`Rename/Replace Map has ${mapRows} useful table rows; expected at least ${minimumMapRows}`);
|
|
}
|
|
|
|
console.log(`Verified ${guideRelativePath}: ${mapRows} Rename/Replace Map table rows.`);
|