|
| 1 | +// @ts-check |
| 2 | +// Loops through all the sample code and ensures that twoslash doesn't raise |
| 3 | + |
| 4 | +// All |
| 5 | +// yarn validate-twoslash |
| 6 | + |
| 7 | +// Watcher |
| 8 | +// yarn validate-twoslash --watch |
| 9 | + |
| 10 | +// Just italian |
| 11 | +// yarn validate-twoslash it/ |
| 12 | + |
| 13 | +const chalk = require("chalk"); |
| 14 | +const { readFileSync, watch } = require("fs"); |
| 15 | +const { join, basename, sep } = require("path"); |
| 16 | +const readline = require('readline') |
| 17 | + |
| 18 | +const ts = require("typescript"); |
| 19 | +const remark = require("remark"); |
| 20 | +const remarkTwoSlash = require("gatsby-remark-shiki-twoslash"); |
| 21 | +const { read } = require("gray-matter"); |
| 22 | +const { recursiveReadDirSync } = require("./recursiveReadDirSync"); |
| 23 | + |
| 24 | +const docsPath = join(__dirname, "..", "docs"); |
| 25 | +const docs = recursiveReadDirSync(docsPath); |
| 26 | + |
| 27 | +const tick = chalk.bold.greenBright("✓"); |
| 28 | +const cross = chalk.bold.redBright("⤫"); |
| 29 | + |
| 30 | +// Pass in a 2nd arg which either triggers watch mode, or to filter which markdowns to run |
| 31 | +const filterString = process.argv[2] ? process.argv[2] : ""; |
| 32 | + |
| 33 | +if (filterString === "--watch") { |
| 34 | + const clear = () => { |
| 35 | + const blank = '\n'.repeat(process.stdout.rows) |
| 36 | + console.log(blank) |
| 37 | + readline.cursorTo(process.stdout, 0, 0) |
| 38 | + readline.clearScreenDown(process.stdout) |
| 39 | + } |
| 40 | + |
| 41 | + if (process.platform === "linux") throw new Error("Sorry linux peeps, the node watcher doesn't support linux."); |
| 42 | + watch(join(__dirname, "..", "docs"), { recursive: true }, (_, filename) => { |
| 43 | + clear() |
| 44 | + process.stdout.write("♲ ") |
| 45 | + validateAtPaths([join(docsPath, filename)]); |
| 46 | + }); |
| 47 | + clear() |
| 48 | + console.log(`${chalk.bold("Started the watcher")}, pressing save on a file in ./docs will lint that file.`); |
| 49 | +} else { |
| 50 | + const toValidate = docs |
| 51 | + .filter((f) => !f.includes("/en/")) |
| 52 | + .filter((f) => (filterString.length > 0 ? f.toLowerCase().includes(filterString.toLowerCase()) : true)); |
| 53 | + |
| 54 | + validateAtPaths(toValidate); |
| 55 | +} |
| 56 | + |
| 57 | +/** @param {string[]} mdDocs */ |
| 58 | +function validateAtPaths(mdDocs) { |
| 59 | + let errorReports = []; |
| 60 | + |
| 61 | + mdDocs.forEach((docAbsPath, i) => { |
| 62 | + const docPath = docAbsPath; |
| 63 | + const filename = basename(docPath); |
| 64 | + |
| 65 | + let lintFunc = undefined; |
| 66 | + |
| 67 | + if (docAbsPath.endsWith(".ts")) { |
| 68 | + lintFunc = lintTSLanguageFile; |
| 69 | + } else if (docAbsPath.endsWith(".md")) { |
| 70 | + lintFunc = lintMarkdownFile; |
| 71 | + } |
| 72 | + |
| 73 | + const isLast = i === mdDocs.length - 1; |
| 74 | + const suffix = isLast ? "" : ", "; |
| 75 | + |
| 76 | + if (!lintFunc) { |
| 77 | + process.stdout.write(chalk.gray(filename + " skipped" + suffix)); |
| 78 | + return; |
| 79 | + } |
| 80 | + |
| 81 | + const errors = lintFunc(docPath); |
| 82 | + errorReports = errorReports.concat(errors); |
| 83 | + |
| 84 | + const sigil = errors.length ? cross : tick; |
| 85 | + const name = errors.length ? chalk.red(filename) : filename; |
| 86 | + |
| 87 | + process.stdout.write(name + " " + sigil + suffix); |
| 88 | + }); |
| 89 | + |
| 90 | + if (errorReports.length) { |
| 91 | + process.exitCode = 1; |
| 92 | + console.log(""); |
| 93 | + |
| 94 | + errorReports.forEach((err) => { |
| 95 | + console.log(`\n> ${chalk.bold.red(err.path)}\n`); |
| 96 | + err.error.stack = undefined; |
| 97 | + console.log(err.error.message); |
| 98 | + if (err.error.stack) { |
| 99 | + console.log(err.error.stack); |
| 100 | + } |
| 101 | + }); |
| 102 | + console.log("\n"); |
| 103 | + |
| 104 | + if (!filterString) { |
| 105 | + console.log( |
| 106 | + "Note: you can add an extra argument to the lint script ( yarn workspace glossary lint [opt] ) to just run one lint." |
| 107 | + ); |
| 108 | + } |
| 109 | + } else { |
| 110 | + console.log(chalk.green("\n\nAll good")); |
| 111 | + } |
| 112 | +} |
| 113 | + |
| 114 | +/** @param {string} docPath */ |
| 115 | +function lintMarkdownFile(docPath) { |
| 116 | + /** @type { Error[] } */ |
| 117 | + const errors = []; |
| 118 | + const markdown = readFileSync(docPath, "utf8"); |
| 119 | + const markdownAST = remark().parse(markdown); |
| 120 | + const greyMD = read(docPath); |
| 121 | + |
| 122 | + try { |
| 123 | + remarkTwoSlash.runTwoSlashAcrossDocument({ markdownAST }, {}); |
| 124 | + } catch (error) { |
| 125 | + errors.push(error); |
| 126 | + } |
| 127 | + |
| 128 | + const relativePath = docPath.replace(docsPath, ""); |
| 129 | + const docType = relativePath.split(sep)[1]; |
| 130 | + const lang = relativePath.split(sep)[2]; |
| 131 | + |
| 132 | + if (docType === "documentation") { |
| 133 | + if (!greyMD.data.display) { |
| 134 | + errors.push(new Error("Did not have a 'display' property in the YML header")); |
| 135 | + } |
| 136 | + |
| 137 | + if (greyMD.data.layout !== "docs") { |
| 138 | + errors.push(new Error("Expected 'layout: docs' in the YML header")); |
| 139 | + } |
| 140 | + |
| 141 | + if (greyMD.data.permalink.startsWith("/" + lang)) { |
| 142 | + errors.push(new Error(`Expected 'permalink:' in the YML header to start with '/${lang}'`)); |
| 143 | + } |
| 144 | + } else if (docType === "tsconfig") { |
| 145 | + if (relativePath.includes("options")) { |
| 146 | + if (!greyMD.data.display) { |
| 147 | + errors.push(new Error("Did not have a 'display' property in the YML header")); |
| 148 | + } |
| 149 | + |
| 150 | + if (!greyMD.data.display) { |
| 151 | + errors.push(new Error("Did not have a 'oneline' property in the YML header")); |
| 152 | + } |
| 153 | + } |
| 154 | + } |
| 155 | + |
| 156 | + return errors.map((e) => ({ path: docPath, error: e })); |
| 157 | +} |
| 158 | + |
| 159 | +/** @param {string} file */ |
| 160 | +function lintTSLanguageFile(file) { |
| 161 | + /** @type {{ path: string, error: Error }[]} */ |
| 162 | + const errors = []; |
| 163 | + |
| 164 | + const content = readFileSync(file, "utf8"); |
| 165 | + const sourceFile = ts.createSourceFile( |
| 166 | + file, |
| 167 | + content, |
| 168 | + ts.ScriptTarget.Latest, |
| 169 | + /*setParentNodes*/ false, |
| 170 | + ts.ScriptKind.TS |
| 171 | + ); |
| 172 | + |
| 173 | + const tooManyStatements = sourceFile.statements.length > 1; |
| 174 | + const notDeclarationList = sourceFile.statements[0].kind !== 232; |
| 175 | + |
| 176 | + if (tooManyStatements) { |
| 177 | + errors.push({ |
| 178 | + path: file, |
| 179 | + error: new Error("TS files had more than one statement (e.g. more than `export const somethingCopy = { ... }` "), |
| 180 | + }); |
| 181 | + } |
| 182 | + |
| 183 | + if (notDeclarationList) { |
| 184 | + errors.push({ |
| 185 | + path: file, |
| 186 | + error: new Error("TS files should only look like: `export const somethingCopy = { ... }` "), |
| 187 | + }); |
| 188 | + } |
| 189 | + |
| 190 | + return errors; |
| 191 | +} |
0 commit comments