mithril-vndb/scripts/_lint-docs/do-lint.js
Claudia Meadows 0d095d1373
Rewrite docs linter
1. I want to set the stage to deal with #2898 properly.
2. `request` was deprecated years ago. Decided that it's better to just
   move to native Node.js APIs in its place.
3. `glob` was outdated, and it's easier to just toss it than to upgrade
   across a major version.
4. I switched to using Marked's "lexer" directly so I'm not fussing
   with the complexity of renderers. This of course necessitated a more
   complex file processor as its "lexer" is really an AST parser.

I also decided to go a few steps further:
- Drop the cache to simplify everything. I might reverse this later,
  but just caching URLs per-page should be enough to prevent the world
  from crashing down.
- Drop some more dependencies, so I don't have to come back to this
  later nearly as quickly.
- Upgrade to a more modern language version in the scripts.
- Update Marked. It was super outdated.
- Add line and column numbers to the warnings. That took quite a bit of
  work, thanks to a missing Marked feature plus a bug in Marked.
2024-09-23 04:54:17 -07:00

162 lines
3.3 KiB
JavaScript

"use strict"
const fs = require("fs")
const path = require("path")
const {submitTask} = require("./task-queue.js")
const {processOne} = require("./process-file.js")
const {root, rel, p, warnError} = require("../_utils.js")
const doNotVisit = /[\\/]node_modules(?:$|[\\/])|[\\/]docs[\\/](?:changelog|recent-changes|migration-[^\\/]*)\.md$/
function lintOne(file, callback) {
let warnings = 0
let errors = 0
let files = 0
let pending = 1
function settle() {
if (--pending === 0) {
callback(warnings, errors, files)
}
}
function visitNext(file, contents) {
if (contents !== undefined) {
files++
pending++
processOne(file, contents, (w, e) => {
warnings += w
errors += e
settle()
})
} else {
pending++
submitTask(fs.readdir.bind(null, file), (err, files) => {
if (err) {
if (err.code !== "ENOTDIR") {
warnError(err)
}
} else {
for (const child of files) {
const joined = path.join(file, child)
if (!doNotVisit.test(joined)) {
visit(joined)
}
}
}
settle()
})
}
}
function visit(file) {
if (file.endsWith(".md")) {
pending++
submitTask(fs.readFile.bind(null, file, "utf8"), (err, contents) => {
// Not found is fine. Just ignore it.
if (!err || err.code === "EISDIR") {
visitNext(file, err ? undefined : contents)
} else if (err.code !== "ENOENT") {
warnError(err)
}
settle()
})
} else {
visitNext(file, undefined)
}
}
visit(file)
}
function lintAll() {
lintOne(root, (warnings, errors, files) => {
let problems = ""
if (errors !== 0) {
process.exitCode = 1
problems = `${problems}\n${errors} error${errors === 1 ? "" : "s"}`
}
if (warnings !== 0) {
problems = `${problems}\n${warnings} warning${warnings === 1 ? "" : "s"}`
}
if (problems !== "") {
console.error(`${problems} found in the docs\n`)
console.error(`Scanned ${files} file${files === 1 ? "" : "s"}\n`)
} else {
console.log("The docs are in good shape!\n")
console.log(`Scanned ${files} file${files === 1 ? "" : "s"}\n`)
}
})
}
function lintFile(file, callback) {
if (doNotVisit.test(file)) {
if (typeof callback === "function") process.nextTick(callback)
} else {
lintOne(file, (warnings, errors) => {
const relativePath = rel(file)
let problems = ""
if (errors !== 0) {
problems = `${problems}\n${errors} error${errors === 1 ? "" : "s"}`
}
if (warnings !== 0) {
problems = `${problems}\n${warnings} warning${warnings === 1 ? "" : "s"}`
}
if (problems !== "") {
console.error(`${problems} found in ${relativePath}\n`)
}
callback?.()
})
}
}
function lintWatch() {
const timers = new Map()
const handleFileInner = (filename) => {
timers.delete(filename)
lintFile(p(filename))
}
const handleFile = (filename) => {
const timer = timers.get(filename)
if (timer !== undefined) {
timer.refresh()
} else {
timers.set(filename, setTimeout(handleFileInner, 400, filename))
}
}
let fileBuffer = []
lintFile(root, () => {
for (const file of fileBuffer) {
handleFile(file)
}
fileBuffer = undefined
})
fs.watch(root, {recursive: true}, (_, filename) => {
if (fileBuffer !== undefined) {
fileBuffer.push(filename)
} else {
handleFile(filename)
}
})
}
module.exports = {
lintAll,
lintWatch,
}