mirror of
https://github.com/netbirdio/docs.git
synced 2026-05-22 17:07:57 -07:00
b968695737
* ci: validate PRs with build and MDX heading linter Adds a pull_request workflow running npm run lint:mdx, npm run build, and npm run lint so heading-hierarchy bugs and broken builds get caught before merge rather than after. The new linter (scripts/lint-mdx-headings.mjs) enforces that the first heading is h1 and that heading levels never jump by more than one. Also fixes three existing pages that had no h1 title — two were using a legacy export const title pattern, one was missing a title entirely. * ci: use npm install since lockfile is gitignored package-lock.json is in .gitignore, so npm ci and setup-node's npm cache both fail on a fresh CI checkout. Match the Dockerfile pattern (npm install, no cache) instead. * ci: drop ESLint step; project config is broken `npm run lint` fails with 'Converting circular structure to JSON' under ESLint 9.x — the repo has no .eslintrc or eslint.config file, so the legacy resolver hits the React plugin's circular reference. This is pre-existing (build_n_push.yml never ran lint, so it stayed hidden); fixing it needs flat-config migration and is out of scope. Drop the step until that lands.
98 lines
2.7 KiB
JavaScript
98 lines
2.7 KiB
JavaScript
#!/usr/bin/env node
|
|
/**
|
|
* Lints MDX files for heading-hierarchy violations:
|
|
* - the first heading on a page must be h1 (the page title)
|
|
* - heading levels may not jump by more than 1 going down (h1 -> h3 is invalid; h3 -> h1 is fine)
|
|
*
|
|
* Skips src/pages/ipa/resources/ — those are auto-generated from the OpenAPI spec.
|
|
* Headings inside fenced code blocks are ignored.
|
|
*/
|
|
|
|
import fs from 'fs'
|
|
import path from 'path'
|
|
import { fileURLToPath } from 'url'
|
|
|
|
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
|
const ROOT = path.join(__dirname, '..')
|
|
const PAGES_DIR = path.join(ROOT, 'src/pages')
|
|
const SKIP_PREFIX = 'ipa/resources'
|
|
|
|
function findMdxFiles(dir, basePath = '') {
|
|
const entries = fs.readdirSync(dir, { withFileTypes: true })
|
|
const results = []
|
|
for (const e of entries) {
|
|
const rel = basePath ? `${basePath}/${e.name}` : e.name
|
|
if (e.isDirectory()) {
|
|
if (rel === SKIP_PREFIX || rel.startsWith(`${SKIP_PREFIX}/`)) continue
|
|
results.push(...findMdxFiles(path.join(dir, e.name), rel))
|
|
} else if (e.name.endsWith('.mdx')) {
|
|
results.push({ relPath: rel, filePath: path.join(dir, e.name) })
|
|
}
|
|
}
|
|
return results
|
|
}
|
|
|
|
function checkHeadings(content) {
|
|
const lines = content.split('\n')
|
|
const violations = []
|
|
let inCodeBlock = false
|
|
let prevLevel = 0
|
|
let prevLine = 0
|
|
|
|
for (let i = 0; i < lines.length; i++) {
|
|
const line = lines[i]
|
|
if (/^```/.test(line)) {
|
|
inCodeBlock = !inCodeBlock
|
|
continue
|
|
}
|
|
if (inCodeBlock) continue
|
|
|
|
const match = /^(#{1,6})\s+(.+?)\s*$/.exec(line)
|
|
if (!match) continue
|
|
|
|
const level = match[1].length
|
|
const text = match[2]
|
|
|
|
if (prevLevel === 0) {
|
|
if (level !== 1) {
|
|
violations.push({
|
|
line: i + 1,
|
|
message: `first heading is h${level} ("${text}"); expected h1`,
|
|
})
|
|
}
|
|
} else if (level > prevLevel + 1) {
|
|
violations.push({
|
|
line: i + 1,
|
|
message: `h${level} ("${text}") skips a level (previous was h${prevLevel} on line ${prevLine})`,
|
|
})
|
|
}
|
|
|
|
prevLevel = level
|
|
prevLine = i + 1
|
|
}
|
|
|
|
return violations
|
|
}
|
|
|
|
const files = findMdxFiles(PAGES_DIR)
|
|
let totalViolations = 0
|
|
|
|
for (const { relPath, filePath } of files) {
|
|
const content = fs.readFileSync(filePath, 'utf-8')
|
|
const violations = checkHeadings(content)
|
|
if (violations.length > 0) {
|
|
console.error(`\nsrc/pages/${relPath}`)
|
|
for (const v of violations) {
|
|
console.error(` line ${v.line}: ${v.message}`)
|
|
}
|
|
totalViolations += violations.length
|
|
}
|
|
}
|
|
|
|
if (totalViolations > 0) {
|
|
console.error(`\n${totalViolations} heading hierarchy violation(s) found.`)
|
|
process.exit(1)
|
|
}
|
|
|
|
console.log(`Checked ${files.length} MDX files — no heading hierarchy violations.`)
|