-
Notifications
You must be signed in to change notification settings - Fork 3.9k
/
check-links.js
190 lines (177 loc) · 6.37 KB
/
check-links.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
/**
* Copyright 2017 The AMP HTML Authors. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS-IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
'use strict';
var argv = require('minimist')(process.argv.slice(2));
var path = require('path');
var BBPromise = require('bluebird');
var chalk = require('chalk');
var fs = require('fs-extra');
var getStdout = require('../exec.js').getStdout;
var gulp = require('gulp-help')(require('gulp'));
var markdownLinkCheck = BBPromise.promisify(require('markdown-link-check'));
var path = require('path');
var util = require('gulp-util');
/**
* Parses the list of files in argv, or extracts it from the commit log.
*
* @return {!Array<string>}
*/
function getMarkdownFiles() {
if (!!argv.files) {
return argv.files.split(',');
} else {
if (!!process.env.TRAVIS_PULL_REQUEST_SHA) {
// On Travis, derive the list of .md files from the latest commit.
var filesInPr =
getStdout(`git diff --name-only master...HEAD`).trim().split('\n');
return filesInPr.filter(function(file) {
return path.extname(file) == '.md'
});
} else {
// A list of files is required when check-links is run locally.
util.log(util.colors.red(
'Error: A list of markdown files must be specified via --files'));
process.exit(1);
}
}
}
/**
* Parses the list of files in argv and checks for dead links.
*
* @return {Promise} Used to wait until all async link checkers finish.
*/
function checkLinks() {
var markdownFiles = getMarkdownFiles();
var linkCheckers = markdownFiles.map(function(markdownFile) {
return runLinkChecker(markdownFile);
});
return BBPromise.all(linkCheckers)
.then(function(allResults) {
var deadLinksFound = false;
var filesWithDeadLinks = [];
allResults.map(function(results, index) {
// Skip files that were deleted by the PR.
if (!fs.existsSync(markdownFiles[index])) {
return;
}
var deadLinksFoundInFile = false;
results.forEach(function (result) {
// Skip links to files that were introduced by the PR.
if (isLinkToFileIntroducedByPR(result.link)) {
return;
}
if(result.status === 'dead') {
deadLinksFound = true;
deadLinksFoundInFile = true;
util.log('[%s] %s', chalk.red('✖'), result.link);
} else if (!process.env.TRAVIS) {
util.log('[%s] %s', chalk.green('✔'), result.link);
}
});
if(deadLinksFoundInFile) {
filesWithDeadLinks.push(markdownFiles[index]);
util.log(
util.colors.red('ERROR'),
'Possible dead link(s) found in',
util.colors.magenta(markdownFiles[index]));
} else {
util.log(
util.colors.green('SUCCESS'),
'All links in',
util.colors.magenta(markdownFiles[index]), 'are alive.');
}
});
if (deadLinksFound) {
util.log(
util.colors.red('ERROR'),
'Please update dead link(s) in',
util.colors.magenta(filesWithDeadLinks.join(',')),
'or whitelist them in build-system/tasks/check-links.js');
util.log(
util.colors.yellow('NOTE'),
'If the link(s) above are illustrative and aren\'t meant to work,',
'surrounding them with backticks or <code></code> will exempt them',
'from the link checker.');
process.exit(1);
} else {
util.log(
util.colors.green('SUCCESS'),
'All links in all markdown files in this PR are alive.');
}
});
}
/**
* Determines if a link points to a file added, copied, or renamed in the PR.
*
* @param {string} link Link being tested.
* @return {boolean} True if the link points to a file introduced by the PR.
*/
function isLinkToFileIntroducedByPR(link) {
var filesAdded =
getStdout(`git diff --name-only --diff-filter=ARC master...HEAD`)
.trim().split('\n');
return filesAdded.some(function(file) {
return (file.length > 0 && link.includes(path.parse(file).base));
});
}
/**
* Filters out whitelisted links before running the link checker.
*
* @param {string} markdown Original markdown.
* @return {string} Markdown after filtering out whitelisted links.
*/
function filterWhitelistedLinks(markdown) {
var filteredMarkdown = markdown;
// localhost links optionally preceded by ( or [ (not served on Travis)
filteredMarkdown =
filteredMarkdown.replace(/(\(|\[)?http:\/\/localhost:8000/g, '');
// Links in script tags (illustrative, and not always valid)
filteredMarkdown = filteredMarkdown.replace(/src="http.*?"/g, '');
// Links inside a <code> block (illustrative, and not always valid)
filteredMarkdown = filteredMarkdown.replace(/<code>(.*?)<\/code>/g, '');
// After all whitelisting is done, clean up any remaining empty blocks bounded
// by backticks. Otherwise, `` will be treated as the start of a code block
// and confuse the link extractor.
filteredMarkdown = filteredMarkdown.replace(/\ \`\`\ /g, '');
return filteredMarkdown;
}
/**
* Reads the raw contents in the given markdown file, filters out localhost
* links (because they do not resolve on Travis), and checks for dead links.
*
* @param {string} markdownFile Path of markdown file, relative to src root.
* @return {Promise} Used to wait until the async link checker is done.
*/
function runLinkChecker(markdownFile) {
// Skip files that were deleted by the PR.
if (!fs.existsSync(markdownFile)) {
return Promise.resolve();
}
var markdown = fs.readFileSync(markdownFile).toString();
var filteredMarkdown = filterWhitelistedLinks(markdown);
var opts = {
baseUrl : 'file://' + path.dirname(path.resolve((markdownFile)))
};
return markdownLinkCheck(filteredMarkdown, opts);
}
gulp.task(
'check-links',
'Detects dead links in markdown files',
checkLinks, {
options: {
'files': ' CSV list of files in which to check links'
}
});