import path from "node:path"; import fg from "fast-glob"; import type { CategoryConfig, CheckError, SyncCheckResult } from "./shared"; function shouldExclude(fileName: string, exclusions: RegExp[]): boolean { return exclusions.some((pattern) => pattern.test(fileName)); } /** * 1:1 file-based sync check: each source file must have a doc file with the * same basename, and vice versa. */ export async function runSourceSyncCheck( categories: CategoryConfig[], cwd: string, ): Promise { const errors: CheckError[] = []; let filesChecked = 0; for (const category of categories) { const sources = await fg(category.sourcePattern, { cwd }); const docs = await fg(category.docPattern, { cwd }); const sourceBasenames = new Map(); const docBasenames = new Map(); for (const sourcePath of sources) { const fileName = path.basename(sourcePath); if (shouldExclude(fileName, category.exclusions)) continue; let basename = path.basename(sourcePath, path.extname(sourcePath)); if (basename.endsWith(".generated")) { basename = basename.slice(0, -".generated".length); } sourceBasenames.set(basename.toLowerCase(), sourcePath); } for (const docPath of docs) { const basename = path.basename(docPath, path.extname(docPath)); docBasenames.set(basename.toLowerCase(), docPath); } for (const [basename, sourcePath] of sourceBasenames) { if (!docBasenames.has(basename)) { errors.push({ type: "missing-doc", category: category.name, sourcePath, expectedBasename: basename, }); } } for (const [basename, docPath] of docBasenames) { if (!sourceBasenames.has(basename)) { errors.push({ type: "orphaned-doc", category: category.name, docPath, expectedBasename: basename, }); } } filesChecked += sourceBasenames.size + docBasenames.size; } return { errors, filesChecked }; }