chore: use vscode-markdown-languageservice for link linting (#36901)
* chore: use vscode-markdown-languageservice for docs link linting * docs: make links relative
This commit is contained in:
parent
37f5881882
commit
ca3145a547
10 changed files with 700 additions and 176 deletions
177
script/lint-docs-links.ts
Executable file
177
script/lint-docs-links.ts
Executable file
|
@ -0,0 +1,177 @@
|
|||
#!/usr/bin/env ts-node
|
||||
|
||||
import * as path from 'path';
|
||||
|
||||
import {
|
||||
createLanguageService,
|
||||
DiagnosticLevel,
|
||||
DiagnosticOptions,
|
||||
ILogger
|
||||
} from '@dsanders11/vscode-markdown-languageservice';
|
||||
import * as minimist from 'minimist';
|
||||
import fetch from 'node-fetch';
|
||||
import { CancellationTokenSource } from 'vscode-languageserver';
|
||||
import { URI } from 'vscode-uri';
|
||||
|
||||
import {
|
||||
DocsWorkspace,
|
||||
MarkdownLinkComputer,
|
||||
MarkdownParser
|
||||
} from './lib/markdown';
|
||||
|
||||
class NoOpLogger implements ILogger {
|
||||
log (): void {}
|
||||
}
|
||||
|
||||
const diagnosticOptions: DiagnosticOptions = {
|
||||
ignoreLinks: [],
|
||||
validateDuplicateLinkDefinitions: DiagnosticLevel.error,
|
||||
validateFileLinks: DiagnosticLevel.error,
|
||||
validateFragmentLinks: DiagnosticLevel.error,
|
||||
validateMarkdownFileLinkFragments: DiagnosticLevel.error,
|
||||
validateReferences: DiagnosticLevel.error,
|
||||
validateUnusedLinkDefinitions: DiagnosticLevel.error
|
||||
};
|
||||
|
||||
async function fetchExternalLink (link: string, checkRedirects = false) {
|
||||
try {
|
||||
const response = await fetch(link);
|
||||
if (response.status !== 200) {
|
||||
console.log('Broken link', link, response.status, response.statusText);
|
||||
} else {
|
||||
if (checkRedirects && response.redirected) {
|
||||
const wwwUrl = new URL(link);
|
||||
wwwUrl.hostname = `www.${wwwUrl.hostname}`;
|
||||
|
||||
// For now cut down on noise to find meaningful redirects
|
||||
const wwwRedirect = wwwUrl.toString() === response.url;
|
||||
const trailingSlashRedirect = `${link}/` === response.url;
|
||||
|
||||
if (!wwwRedirect && !trailingSlashRedirect) {
|
||||
console.log('Link redirection', link, '->', response.url);
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
} catch {
|
||||
console.log('Broken link', link);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
async function main ({ fetchExternalLinks = false, checkRedirects = false }) {
|
||||
const workspace = new DocsWorkspace(path.resolve(__dirname, '..', 'docs'));
|
||||
const parser = new MarkdownParser();
|
||||
const linkComputer = new MarkdownLinkComputer(workspace);
|
||||
const languageService = createLanguageService({
|
||||
workspace,
|
||||
parser,
|
||||
logger: new NoOpLogger(),
|
||||
linkComputer
|
||||
});
|
||||
|
||||
const cts = new CancellationTokenSource();
|
||||
let errors = false;
|
||||
|
||||
const externalLinks = new Set<string>();
|
||||
|
||||
try {
|
||||
// Collect diagnostics for all documents in the workspace
|
||||
for (const document of await workspace.getAllMarkdownDocuments()) {
|
||||
for (let link of await languageService.getDocumentLinks(
|
||||
document,
|
||||
cts.token
|
||||
)) {
|
||||
if (link.target === undefined) {
|
||||
link =
|
||||
(await languageService.resolveDocumentLink(link, cts.token)) ??
|
||||
link;
|
||||
}
|
||||
|
||||
if (
|
||||
link.target &&
|
||||
link.target.startsWith('http') &&
|
||||
new URL(link.target).hostname !== 'localhost'
|
||||
) {
|
||||
externalLinks.add(link.target);
|
||||
}
|
||||
}
|
||||
const diagnostics = await languageService.computeDiagnostics(
|
||||
document,
|
||||
diagnosticOptions,
|
||||
cts.token
|
||||
);
|
||||
|
||||
if (diagnostics.length) {
|
||||
console.log(
|
||||
'File Location:',
|
||||
path.relative(workspace.root, URI.parse(document.uri).path)
|
||||
);
|
||||
}
|
||||
|
||||
for (const diagnostic of diagnostics) {
|
||||
console.log(
|
||||
`\tBroken link on line ${diagnostic.range.start.line + 1}:`,
|
||||
diagnostic.message
|
||||
);
|
||||
errors = true;
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
cts.dispose();
|
||||
}
|
||||
|
||||
if (fetchExternalLinks) {
|
||||
const externalLinkStates = await Promise.all(
|
||||
Array.from(externalLinks).map((link) =>
|
||||
fetchExternalLink(link, checkRedirects)
|
||||
)
|
||||
);
|
||||
|
||||
errors = errors || !externalLinkStates.every((x) => x);
|
||||
}
|
||||
|
||||
return errors;
|
||||
}
|
||||
|
||||
function parseCommandLine () {
|
||||
const showUsage = (arg?: string): boolean => {
|
||||
if (!arg || arg.startsWith('-')) {
|
||||
console.log(
|
||||
'Usage: script/lint-docs-links.ts [-h|--help] [--fetch-external-links] ' +
|
||||
'[--check-redirects]'
|
||||
);
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
const opts = minimist(process.argv.slice(2), {
|
||||
boolean: ['help', 'fetch-external-links', 'check-redirects'],
|
||||
stopEarly: true,
|
||||
unknown: showUsage
|
||||
});
|
||||
|
||||
if (opts.help) showUsage();
|
||||
|
||||
return opts;
|
||||
}
|
||||
|
||||
if (process.mainModule === module) {
|
||||
const opts = parseCommandLine();
|
||||
|
||||
main({
|
||||
fetchExternalLinks: opts['fetch-external-links'],
|
||||
checkRedirects: opts['check-redirects']
|
||||
})
|
||||
.then((errors) => {
|
||||
if (errors) process.exit(1);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(error);
|
||||
process.exit(1);
|
||||
});
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue