Files
OmniRoute/scripts/check/check-docs-frontmatter.mjs
Diego Rodrigues de Sa e Souza 9ddb8e0a93 fix(docs): restore the Next build — REMOVED_PROVIDERS.md had no frontmatter (base-red #12581) (#12610)
`source.config.ts` feeds `docs/reference/**/*.md` to fumadocs-mdx, whose default
schema requires a `title`. #12478 added `docs/reference/REMOVED_PROVIDERS.md`
with no frontmatter block at all, so every production build died with:

    [MDX] invalid frontmatter in docs/reference/REMOVED_PROVIDERS.md:
    - title: Invalid input: expected string, received undefined

That single missing block is what turns three release-green gates red at once —
`Package artifact (npm pack policy)` fails on the build, and both
`Tarball boot-smoke` and the packaged CLI checks are skipped for lack of a
valid `dist/`.

Fixes:
- add the frontmatter block, matching the convention of its sibling reference
  docs (`title` / `version` / `lastUpdated`).
- add `check:docs-frontmatter`, wired into `check:docs-all`, so the next doc
  added without a title fails in milliseconds instead of costing a full Next
  build and a red release branch. The gate reads its globs from
  `source.config.ts` rather than duplicating them, so a new docs directory
  cannot silently escape the check.

Verified: the gate reports OK across all 122 compiled docs, fails (exit 1) when
the frontmatter is removed, and `npm run check:docs-all` passes.
2026-09-03 11:33:07 -03:00

104 lines
3.7 KiB
JavaScript

#!/usr/bin/env node
/**
* Validates the frontmatter of every Markdown file that fumadocs-mdx compiles.
*
* Why this gate exists: `source.config.ts` feeds `docs/**` globs to
* `defineDocs()`, and fumadocs' default frontmatter schema REQUIRES a `title`
* string. A doc added without frontmatter does not fail any docs gate — it
* fails the **production build** with a generic Turbopack error
* (`[MDX] invalid frontmatter … title: Invalid input: expected string,
* received undefined`), which then cascades into `check:pack-artifact` and the
* tarball boot-smoke. That is exactly how #12478 turned the release branch red
* (base-red #12581): one new reference doc, no frontmatter, three failing
* gates and an unbuildable branch.
*
* Catching it here costs milliseconds instead of a full Next build.
*
* The globs are read from `source.config.ts` rather than duplicated, so adding
* a new docs directory there cannot silently escape this check.
*/
import fs from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..", "..");
const CONFIG_PATH = path.join(ROOT, "source.config.ts");
/** Extract the `files: [...]` globs declared in source.config.ts. */
function readConfiguredGlobs() {
const src = fs.readFileSync(CONFIG_PATH, "utf-8");
const block = src.match(/files\s*:\s*\[([\s\S]*?)\]/);
if (!block) {
console.error(
"[docs-frontmatter] FAIL — could not locate the `files:` globs in source.config.ts"
);
process.exit(1);
}
const globs = [...block[1].matchAll(/["'`]([^"'`]+)["'`]/g)].map((m) => m[1]);
if (globs.length === 0) {
console.error("[docs-frontmatter] FAIL — source.config.ts declares no doc globs");
process.exit(1);
}
return globs;
}
/** "./reference/**\/*.md" -> the directory under docs/ it covers. */
function globToDir(glob) {
const cleaned = glob.replace(/^\.\//, "");
const dir = cleaned.split("/**")[0];
return path.join(ROOT, "docs", dir);
}
function walkMarkdown(dir) {
if (!fs.existsSync(dir)) return [];
const out = [];
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
const full = path.join(dir, entry.name);
if (entry.isDirectory()) out.push(...walkMarkdown(full));
else if (entry.isFile() && entry.name.endsWith(".md")) out.push(full);
}
return out;
}
const violations = [];
const files = [...new Set(readConfiguredGlobs().flatMap((g) => walkMarkdown(globToDir(g))))];
for (const file of files) {
const rel = path.relative(ROOT, file);
const text = fs.readFileSync(file, "utf-8");
if (!text.startsWith("---")) {
violations.push(`${rel}: no frontmatter block (fumadocs requires a \`title\`)`);
continue;
}
const end = text.indexOf("\n---", 3);
if (end === -1) {
violations.push(`${rel}: frontmatter block is never closed`);
continue;
}
const frontmatter = text.slice(3, end);
const title = frontmatter.match(/^\s*title\s*:\s*(.+)$/m);
if (!title) {
violations.push(`${rel}: frontmatter has no \`title\``);
} else if (title[1].trim().replace(/^["']|["']$/g, "") === "") {
violations.push(`${rel}: \`title\` is empty`);
}
}
if (violations.length > 0) {
console.error(
`[docs-frontmatter] FAIL — ${violations.length} doc(s) would break the Next build:`
);
for (const v of violations) console.error(` - ${v}`);
console.error(
"\nEvery Markdown file matched by source.config.ts is compiled by fumadocs-mdx and needs a\n" +
'frontmatter block with a title, e.g.:\n\n---\ntitle: "Removed Providers"\nversion: 3.8.51\nlastUpdated: 2026-09-03\n---\n'
);
process.exit(1);
}
console.log(
`[docs-frontmatter] OK — ${files.length} compiled doc(s) carry a valid frontmatter title.`
);