mithril-vndb/scripts/lint-docs.js
Isiah Meadows 22e6d37a26 Refactor the scripts to work as advertised
- Correct docs generation to always fetch its dependency
- Don't try to close a handle that's already been closed by other methods
- Allow the release script to actually be testable.
2019-08-17 21:58:57 -04:00

209 lines
4.9 KiB
JavaScript

#!/usr/bin/env node
"use strict"
const {promises: fs} = require("fs")
const path = require("path")
const {Glob} = require("glob")
const marked = require("marked")
// Accept just about anything
const babelParser = require("@babel/parser")
// Peer dependency on `request`
const request = require("request-promise-native")
// lint rules
class LintRenderer extends marked.Renderer {
constructor(file) {
super()
this._file = file
this._dir = path.dirname(file)
this._context = undefined
this._code = undefined
this._lang = undefined
this._error = undefined
this._awaiting = []
}
_emitTolerate(...data) {
let str = data.join("\n")
if (str.endsWith("\n")) str = str.slice(0, -1)
console.log(`${this._file} - ${str}\n${"-".repeat(60)}`)
}
_emit(...data) {
this._emitTolerate(...data)
process.exitCode = 1
}
_block() {
return `\`\`\`${this._lang || ""}\n${this._code}\n\`\`\``
}
link(href) {
// Don't fail if something byzantine shows up - it's the freaking
// internet. Just log it and move on.
const httpError = (e) =>
this._emitTolerate(`http error for ${href}`, e.message)
// Prefer https: > http: where possible, but allow http: when https: is
// inaccessible.
if ((/^https?:\/\//).test(href)) {
const url = href.replace(/#.*$/, "")
this._awaiting.push(request.head(url).then(() => {
const isHTTPS = href.startsWith("https:")
if (!isHTTPS) {
return request.head(`https:${url.slice(7)}`).then(
() => this._emit("change http: to https:"),
() => { /* ignore inner errors */ }
)
}
}, (e) => {
if (e.statusCode === 404) {
this._emit(`broken external link: ${href}`)
}
else {
if (
e.error.code === "ERR_TLS_CERT_ALTNAME_INVALID" &&
href.startsWith("https://")
) {
return request.head(`http:${url.slice(6)}`).then(
() => this._emit(`change ${href} to use http:`),
// ignore inner errors
() => httpError(e)
)
}
httpError(e)
}
}))
}
else {
const exec = (/^([^#?]*\.md)(?:$|\?|#)/).exec(href)
if (exec != null) {
const resolved = path.resolve(this._dir, exec[1])
this._awaiting.push(fs.access(resolved).catch(() => {
this._emit(`broken internal link: ${href}`)
}))
}
}
}
code(code, lang) {
this._code = code
this._lang = lang
if (!lang || lang === "js" || lang === "javascript") {
try {
// Could be within any production.
babelParser.parse(code, {
sourceType: "unambiguous",
allowReturnOutsideFunction: true,
allowAwaitOutsideFunction: true,
allowSuperOutsideMethod: true,
allowUndeclaredExports: true,
plugins: ["dynamicImport"],
})
}
catch (e) {
this._error = e
}
}
this._ensureCodeIsHighlightable()
this._ensureCodeHasConsistentTag()
this._ensureCodeIsSyntaticallyValid()
this._ensureCommentStyle()
}
_ensureCodeIsHighlightable() {
// We only care about what's not tagged here.
if (!this._lang) {
// TODO: ensure all code blocks have tags, and check this in CI.
if (this._error == null) {
this._emit(
"Code block possibly missing `javascript` language tag",
this._block(),
)
}
try {
JSON.parse(this._code)
this._emit(
"Code block possibly missing `json` language tag",
this._block(),
)
}
catch (_) {
// ignore
}
}
}
_ensureCodeHasConsistentTag() {
if (this._lang === "js") {
this._emit("JS code block has wrong language tag", this._block())
}
}
_ensureCodeIsSyntaticallyValid() {
if (!this.lang || !(/^js$|^javascript$/).test(this._lang)) return
if (this._error != null) {
this._emit(
"JS code block has invalid syntax", this._error.message,
this._block()
)
}
}
_ensureCommentStyle() {
if (!this.lang || !(/^js$|^javascript$/).test(this._lang)) return
if ((/(^|\s)\/\/[\S]/).test(this._code)) {
this._emit("Comment is missing a preceding space", this._block())
}
}
}
exports.lintOne = lintOne
async function lintOne(file) {
const contents = await fs.readFile(file, "utf-8")
const renderer = new LintRenderer(file)
marked(contents, {renderer})
return Promise.all(renderer._awaiting)
}
exports.lintAll = lintAll
function lintAll() {
return new Promise((resolve, reject) => {
const glob = new Glob(path.resolve(__dirname, "../docs/**/*.md"), {
ignore: [
"**/change-log.md",
"**/migration-*.md",
"**/node_modules/**",
],
nodir: true,
})
const awaiting = []
glob.on("match", (file) => {
awaiting.push(lintOne(file))
})
glob.on("error", reject)
glob.on("end", () => resolve(Promise.all(awaiting)))
})
}
/* eslint-disable global-require */
if (require.main === module) {
require("./_command")({
exec: lintAll,
watch() {
require("chokidar")
.watch(path.resolve(__dirname, "../docs/**/*.md"), {
ignore: [
"**/change-log.md",
"**/migration-*.md",
"**/node_modules/**",
],
})
.on("add", lintOne)
.on("change", lintOne)
},
})
}