Files
HiveGrid-Wireframes/hivegrid-cloud/scripts/verify-porting-guide.mjs
T

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.`);