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