140 lines
4.2 KiB
JavaScript
140 lines
4.2 KiB
JavaScript
#!/usr/bin/env node
|
|
import { execFileSync } from "node:child_process";
|
|
import fs from "node:fs";
|
|
import path from "node:path";
|
|
|
|
const root = process.cwd();
|
|
const bin = process.env.GOG_BIN || path.join(root, "bin", "gog");
|
|
const docsDir = path.join(root, "docs");
|
|
const commandsDir = path.join(docsDir, "commands");
|
|
|
|
const requiredFeatureDocs = [
|
|
"install.md",
|
|
"quickstart.md",
|
|
"auth-clients.md",
|
|
"safety-profiles.md",
|
|
"raw-api.md",
|
|
"raw-audit.md",
|
|
"gmail-workflows.md",
|
|
"watch.md",
|
|
"email-tracking.md",
|
|
"drive-audits.md",
|
|
"contacts-dedupe.md",
|
|
"contacts-json-update.md",
|
|
"docs-editing.md",
|
|
"sheets-tables.md",
|
|
"sheets-formatting.md",
|
|
"slides-markdown.md",
|
|
"slides-template-replacement.md",
|
|
"backup.md",
|
|
"dates.md",
|
|
];
|
|
|
|
const schema = JSON.parse(execFileSync(bin, ["schema", "--json"], { encoding: "utf8", maxBuffer: 16 * 1024 * 1024 }));
|
|
const commands = Array.from(walk(schema.command || {}));
|
|
const seenSlugs = new Set();
|
|
const missingCommandPages = [];
|
|
|
|
for (const command of commands) {
|
|
const base = commandSlug(command);
|
|
let slug = base;
|
|
let suffix = 2;
|
|
while (seenSlugs.has(slug)) {
|
|
slug = `${base}-${suffix}`;
|
|
suffix += 1;
|
|
}
|
|
seenSlugs.add(slug);
|
|
|
|
const page = path.join(commandsDir, `${slug}.md`);
|
|
if (!fs.existsSync(page)) {
|
|
missingCommandPages.push(path.relative(root, page));
|
|
}
|
|
}
|
|
|
|
const navSourcePath = path.join(root, "scripts", "build-docs-site.mjs");
|
|
const navSource = fs.readFileSync(navSourcePath, "utf8");
|
|
const missingFeaturePages = [];
|
|
const unlinkedFeaturePages = [];
|
|
const brokenLinks = checkMarkdownLinks(docsDir);
|
|
|
|
for (const rel of requiredFeatureDocs) {
|
|
const page = path.join(docsDir, rel);
|
|
if (!fs.existsSync(page)) {
|
|
missingFeaturePages.push(`docs/${rel}`);
|
|
continue;
|
|
}
|
|
if (!navSource.includes(`"${rel}"`)) {
|
|
unlinkedFeaturePages.push(`docs/${rel}`);
|
|
}
|
|
}
|
|
|
|
if (missingCommandPages.length || missingFeaturePages.length || unlinkedFeaturePages.length || brokenLinks.length) {
|
|
for (const name of missingCommandPages) console.error(`missing command doc: ${name}`);
|
|
for (const name of missingFeaturePages) console.error(`missing feature doc: ${name}`);
|
|
for (const name of unlinkedFeaturePages) console.error(`feature doc not in scripts/build-docs-site.mjs sidebar: ${name}`);
|
|
for (const item of brokenLinks) console.error(`broken docs link: ${item}`);
|
|
process.exit(1);
|
|
}
|
|
|
|
console.log(`docs coverage ok: ${commands.length} command pages, ${requiredFeatureDocs.length} feature pages`);
|
|
|
|
function* walk(command) {
|
|
yield command;
|
|
for (const child of command.subcommands || []) {
|
|
yield* walk(child);
|
|
}
|
|
}
|
|
|
|
function canonicalTokens(commandPath) {
|
|
return (commandPath || "")
|
|
.split(/\s+/)
|
|
.filter((part) => part && !(part.startsWith("(") && part.endsWith(")")));
|
|
}
|
|
|
|
function canonicalPath(command) {
|
|
return canonicalTokens(command.path || command.name || "").join(" ");
|
|
}
|
|
|
|
function commandSlug(command) {
|
|
const slug = canonicalPath(command)
|
|
.toLowerCase()
|
|
.replace(/[^a-z0-9]+/g, "-")
|
|
.replace(/^-+|-+$/g, "");
|
|
return slug || "gog";
|
|
}
|
|
|
|
function checkMarkdownLinks(dir) {
|
|
const broken = [];
|
|
for (const file of allMarkdown(dir)) {
|
|
const markdown = fs.readFileSync(file, "utf8");
|
|
const linkPattern = /!?\[[^\]]*\]\(([^)]+)\)/g;
|
|
let match;
|
|
while ((match = linkPattern.exec(markdown)) !== null) {
|
|
const rawTarget = match[1].trim().replace(/^<|>$/g, "");
|
|
if (!rawTarget || rawTarget.startsWith("#")) continue;
|
|
if (/^[a-z][a-z0-9+.-]*:/i.test(rawTarget)) continue;
|
|
|
|
const targetWithoutTitle = rawTarget.split(/\s+["'][^"']*["']\s*$/)[0];
|
|
const targetPath = targetWithoutTitle.split("#")[0];
|
|
if (!targetPath) continue;
|
|
if (/^(url|path|file)$/i.test(targetPath)) continue;
|
|
|
|
const resolved = path.resolve(path.dirname(file), targetPath);
|
|
if (!fs.existsSync(resolved)) {
|
|
broken.push(`${path.relative(root, file)} -> ${targetPath}`);
|
|
}
|
|
}
|
|
}
|
|
return broken;
|
|
}
|
|
|
|
function allMarkdown(dir) {
|
|
return fs
|
|
.readdirSync(dir, { withFileTypes: true })
|
|
.flatMap((entry) => {
|
|
const full = path.join(dir, entry.name);
|
|
if (entry.isDirectory()) return allMarkdown(full);
|
|
return entry.name.endsWith(".md") ? [full] : [];
|
|
});
|
|
}
|