Skip to content

Commit ac28ae5

Browse files
committed
Adds a linter
1 parent 8707f4c commit ac28ae5

File tree

5 files changed

+460
-8
lines changed

5 files changed

+460
-8
lines changed

README.md

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,9 @@ yarn
1515
yarn pull-en
1616

1717
# Optional: Verify your changes will correctly replace the english files
18-
yarn validate-paths
18+
yarn lint
19+
# Alternative: Run the lint watcher
20+
yarn lint --watch
1921
```
2022

2123
That's it, you've got a copy of all the documentation and now can write documentation which follows the existing patterns.
@@ -46,7 +48,8 @@ For example if you wanted to translate a new handbook page you would:
4648
- Pull in the English files `yarn pull-en` (these will be gitignored)
4749
- Find your english file: `docs/documentation/en/handbook-v2/Basics.md`
4850
- Recreate the same folder path in your language: `docs/documentation/it/handbook-v2/Basics.md`
49-
- Translate the file!
51+
- Translate the file!
52+
- Validate your changes: `yarn lint` (or `yarn lint docs/documentation/it/handbook-v2/Basics.md`)
5053
- Create a pull request to this repo
5154
- Once merged, the translation will appear on the next website deploy
5255

@@ -56,12 +59,14 @@ When a new language is created, we ask for a few people to become language owner
5659

5760
The TypeScript team generally only know English, and can answer clarifying questions if needed! For quick questions, you can use the `ts-website-translation` channel in the [TypeScript Discord](https://discord.gg/typescript).
5861

59-
#### Well tested
62+
#### Secure
6063

6164
This repo has extensive CI to ensure that you can't accidentally break the TypeScript website.
6265

6366
There are local, fast checks that it won't break via `yarn test` and then the full TypeScript website test suite runs with the changes, and a website build is generated to ensure that nothing breaks.
6467

68+
The checks may not seem obvious to an outsider, because the website is complex, so there is a watch mode which you can run via `yarn link --watch` to get instant feedback.
69+
6570
# Contributing
6671

6772
This project welcomes contributions and suggestions. Most contributions require you to agree to a

package.json

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,20 @@
88
"pull-en": "docs-sync get-en microsoft/TypeScript-Website#v2",
99
"pull-others": "docs-sync yarn get-en microsoft/TypeScript-Website#v2 --all",
1010
"validate-paths": "docs-sync validate-against-en",
11-
"test": "yarn validate-paths"
11+
"lint": "node scripts/lint.js",
12+
"test": "yarn validate-paths && yarn lint"
1213
},
1314
"dependencies": {
1415
"@orta/markdown-translator": "^0.4.2",
1516
"@oss-docs/sync": "*",
1617
"danger": "^10.6.0"
18+
},
19+
"devDependencies": {
20+
"@typescript/twoslash": "^1.1.3",
21+
"chalk": "^4.1.0",
22+
"gatsby-remark-shiki-twoslash": "^0.7.0",
23+
"gray-matter": "^4.0.2",
24+
"remark": "^13.0.0",
25+
"typescript": "^4.1.3"
1726
}
1827
}

scripts/lint.js

Lines changed: 191 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,191 @@
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+
}

scripts/recursiveReadDirSync.js

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
const fs = require("fs")
2+
const path = require("path")
3+
4+
/** Recursively retrieve file paths from a given folder and its subfolders. */
5+
// https://gist.github.com/kethinov/6658166#gistcomment-2936675
6+
/** @returns {string[]} */
7+
const recursiveReadDirSync = folderPath => {
8+
if (!fs.existsSync(folderPath)) return []
9+
10+
const entryPaths = fs
11+
.readdirSync(folderPath)
12+
.map(entry => path.join(folderPath, entry))
13+
const filePaths = entryPaths.filter(entryPath =>
14+
fs.statSync(entryPath).isFile()
15+
)
16+
const dirPaths = entryPaths.filter(
17+
entryPath => !filePaths.includes(entryPath)
18+
)
19+
const dirFiles = dirPaths.reduce(
20+
(prev, curr) => prev.concat(recursiveReadDirSync(curr)),
21+
[]
22+
)
23+
24+
return [...filePaths, ...dirFiles]
25+
.filter(f => !f.endsWith(".DS_Store") && !f.endsWith("README.md"))
26+
}
27+
28+
module.exports = {
29+
recursiveReadDirSync,
30+
}

0 commit comments

Comments
 (0)