89 lines
2.5 KiB
JavaScript
89 lines
2.5 KiB
JavaScript
import { readdir, readFile } from "node:fs/promises"
|
|||
|
|
import path from "node:path"
|
||
|
|
|
||
|
|
const repoRoot = path.resolve(import.meta.dirname, "..", "..")
|
||
|
|
const docsRoot = path.join(repoRoot, "packages", "docs")
|
||
|
|
const contentRoot = path.join(docsRoot, "content", "docs")
|
||
|
|
const sidebarPath = path.join(docsRoot, "sidebar.config.json")
|
||
|
|
|
||
|
|
async function walk(dir) {
|
||
|
|
const entries = await readdir(dir, { withFileTypes: true })
|
||
|
|
const files = await Promise.all(
|
||
|
|
entries.map(async (entry) => {
|
||
|
|
const target = path.join(dir, entry.name)
|
||
|
|
if (entry.isDirectory()) return walk(target)
|
||
|
|
return [target]
|
||
|
|
}),
|
||
|
|
)
|
||
|
|
return files.flat()
|
||
|
|
}
|
||
|
|
|
||
|
|
function toPosix(value) {
|
||
|
|
return value.split(path.sep).join("/")
|
||
|
|
}
|
||
|
|
|
||
|
|
function routeFromFile(filePath) {
|
||
|
|
const relative = toPosix(path.relative(contentRoot, filePath))
|
||
|
|
const withoutExt = relative.replace(/\.mdx$/, "")
|
||
|
|
|
||
|
|
if (withoutExt === "index") return "/"
|
||
|
|
if (withoutExt.endsWith("/index")) {
|
||
|
|
return `/${withoutExt.slice(0, -"/index".length)}/`
|
||
|
|
}
|
||
|
|
|
||
|
|
return `/${withoutExt}/`
|
||
|
|
}
|
||
|
|
|
||
|
|
function hasFrontmatterKey(content, key) {
|
||
|
|
const hit = /^---\n([\s\S]*?)\n---\n/m.exec(content)
|
||
|
|
if (!hit) return false
|
||
|
|
return new RegExp(`^${key}:\\s*.+$`, "m").test(hit[1])
|
||
|
|
}
|
||
|
|
|
||
|
|
async function run() {
|
||
|
|
const filePaths = (await walk(contentRoot)).filter((p) => p.endsWith(".mdx"))
|
||
|
|
const routeSet = new Set()
|
||
|
|
const errors = []
|
||
|
|
|
||
|
|
for (const filePath of filePaths) {
|
||
|
|
const body = await readFile(filePath, "utf8")
|
||
|
|
const relative = toPosix(path.relative(repoRoot, filePath))
|
||
|
|
const route = routeFromFile(filePath)
|
||
|
|
routeSet.add(route)
|
||
|
|
|
||
|
|
if (!hasFrontmatterKey(body, "title")) {
|
||
|
|
errors.push(`${relative}: missing frontmatter key 'title'`)
|
||
|
|
}
|
||
|
|
if (!hasFrontmatterKey(body, "description")) {
|
||
|
|
errors.push(`${relative}: missing frontmatter key 'description'`)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
const sidebarRaw = await readFile(sidebarPath, "utf8")
|
||
|
|
const sidebar = JSON.parse(sidebarRaw)
|
||
|
|
const links = (sidebar.sections ?? [])
|
||
|
|
.flatMap((section) => section.items ?? [])
|
||
|
|
.map((item) => item.link)
|
||
|
|
|
||
|
|
for (const link of links) {
|
||
|
|
if (!routeSet.has(link)) {
|
||
|
|
errors.push(`sidebar link has no page: ${link}`)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
if (errors.length > 0) {
|
||
|
|
console.error("Docs validation failed:")
|
||
|
|
for (const error of errors) {
|
||
|
|
console.error(`- ${error}`)
|
||
|
|
}
|
||
|
|
process.exit(1)
|
||
|
|
}
|
||
|
|
|
||
|
|
console.log(`Docs validation passed: ${filePaths.length} pages, ${links.length} sidebar links.`)
|
||
|
|
}
|
||
|
|
|
||
|
|
run().catch((error) => {
|
||
|
|
console.error(error)
|
||
|
|
process.exit(1)
|
||
|
|
})
|