Skip to content

Commit 2f1b5b1

Browse files
authoredJun 28, 2021
feat: add support for url rewrites (#317)
1 parent 999fca3 commit 2f1b5b1

File tree

9 files changed

+121
-0
lines changed

9 files changed

+121
-0
lines changed
 

‎README.md

+7
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,12 @@ $ linkinator LOCATIONS [ --arguments ]
8080
--timeout
8181
Request timeout in ms. Defaults to 0 (no timeout).
8282
83+
--url-rewrite-search
84+
Pattern to search for in urls. Must be used with --url-rewrite-replace.
85+
86+
--url-rewrite-replace
87+
Expression used to replace search content. Must be used with --url-rewrite-search.
88+
8389
--verbosity
8490
Override the default verbosity for this command. Available options are
8591
'debug', 'info', 'warning', 'error', and 'none'. Defaults to 'warning'.
@@ -200,6 +206,7 @@ where the server is started. Defaults to the path passed in `path`.
200206
- `markdown` (boolean) - Automatically parse and scan markdown if scanning from a location on disk.
201207
- `linksToSkip` (array | function) - An array of regular expression strings that should be skipped, OR an async function that's called for each link with the link URL as its only argument. Return a Promise that resolves to `true` to skip the link or `false` to check it.
202208
- `directoryListing` (boolean) - Automatically serve a static file listing page when serving a directory. Defaults to `false`.
209+
- `urlRewriteExpressions` (array) - Collection of objects that contain a search pattern, and replacement.
203210

204211
### linkinator.LinkChecker()
205212

‎src/cli.ts

+24
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,12 @@ const cli = meow(
7171
--timeout
7272
Request timeout in ms. Defaults to 0 (no timeout).
7373
74+
--url-rewrite-search
75+
Pattern to search for in urls. Must be used with --url-rewrite-replace.
76+
77+
--url-rewrite-replace
78+
Expression used to replace search content. Must be used with --url-rewrite-search.
79+
7480
--verbosity
7581
Override the default verbosity for this command. Available options are
7682
'debug', 'info', 'warning', 'error', and 'none'. Defaults to 'warning'.
@@ -96,6 +102,8 @@ const cli = meow(
96102
verbosity: {type: 'string'},
97103
directoryListing: {type: 'boolean'},
98104
retry: {type: 'boolean'},
105+
urlRewriteSearch: {type: 'string'},
106+
urlReWriteReplace: {type: 'string'},
99107
},
100108
booleanDefault: undefined,
101109
}
@@ -109,6 +117,14 @@ async function main() {
109117
return;
110118
}
111119
flags = await getConfig(cli.flags);
120+
if (
121+
(flags.urlRewriteReplace && !flags.urlRewriteSearch) ||
122+
(flags.urlRewriteSearch && !flags.urlRewriteReplace)
123+
) {
124+
throw new Error(
125+
'The url-rewrite-replace flag must be used with the url-rewrite-search flag.'
126+
);
127+
}
112128

113129
const start = Date.now();
114130
const verbosity = parseVerbosity(flags);
@@ -155,6 +171,14 @@ async function main() {
155171
opts.linksToSkip = flags.skip;
156172
}
157173
}
174+
if (flags.urlRewriteSearch && flags.urlRewriteReplace) {
175+
opts.urlRewriteExpressions = [
176+
{
177+
pattern: new RegExp(flags.urlRewriteSearch),
178+
replacement: flags.urlRewriteReplace,
179+
},
180+
];
181+
}
158182
const result = await checker.check(opts);
159183
const filteredResults = result.links.filter(link => {
160184
switch (link.state) {

‎src/config.ts

+2
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ export interface Flags {
1616
serverRoot?: string;
1717
directoryListing?: boolean;
1818
retry?: boolean;
19+
urlRewriteSearch?: string;
20+
urlRewriteReplace?: string;
1921
}
2022

2123
export async function getConfig(flags: Flags) {

‎src/index.ts

+10
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,16 @@ export class LinkChecker extends EventEmitter {
144144
* @returns A list of crawl results consisting of urls and status codes
145145
*/
146146
async crawl(opts: CrawlOptions): Promise<void> {
147+
// apply any regex url replacements
148+
if (opts.checkOptions.urlRewriteExpressions) {
149+
for (const exp of opts.checkOptions.urlRewriteExpressions) {
150+
const newUrl = opts.url.href.replace(exp.pattern, exp.replacement);
151+
if (opts.url.href !== newUrl) {
152+
opts.url.href = newUrl;
153+
}
154+
}
155+
}
156+
147157
// explicitly skip non-http[s] links before making the request
148158
const proto = opts.url.protocol;
149159
if (proto !== 'http:' && proto !== 'https:') {

‎src/options.ts

+6
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,11 @@ import * as globby from 'glob';
66
const stat = util.promisify(fs.stat);
77
const glob = util.promisify(globby);
88

9+
export interface UrlRewriteExpression {
10+
pattern: RegExp;
11+
replacement: string;
12+
}
13+
914
export interface CheckOptions {
1015
concurrency?: number;
1116
port?: number;
@@ -17,6 +22,7 @@ export interface CheckOptions {
1722
serverRoot?: string;
1823
directoryListing?: boolean;
1924
retry?: boolean;
25+
urlRewriteExpressions?: UrlRewriteExpression[];
2026
}
2127

2228
export interface InternalCheckOptions extends CheckOptions {

‎test/fixtures/rewrite/LICENSE.md

+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
The MIT License (MIT)
2+
3+
Copyright (c) Justin Beckwith <justin.beckwith@gmail.com> (jbeckwith.com)
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in
13+
all copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21+
THE SOFTWARE.

‎test/fixtures/rewrite/README.md

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
# Say hello to my README
2+
This has [a link](NOTLICENSE.md) to something.

‎test/test.cli.ts

+36
Original file line numberDiff line numberDiff line change
@@ -202,6 +202,42 @@ describe('cli', function () {
202202
assert.strictEqual(res.exitCode, 0);
203203
});
204204

205+
it('should fail if a url search is provided without a replacement', async () => {
206+
const res = await execa(
207+
node,
208+
[linkinator, '--url-rewrite-search', 'boop', 'test/fixtures/basic'],
209+
{
210+
reject: false,
211+
}
212+
);
213+
assert.strictEqual(res.exitCode, 1);
214+
assert.match(res.stderr, /flag must be used/);
215+
});
216+
217+
it('should fail if a url replacement is provided without a search', async () => {
218+
const res = await execa(
219+
node,
220+
[linkinator, '--url-rewrite-replace', 'beep', 'test/fixtures/basic'],
221+
{
222+
reject: false,
223+
}
224+
);
225+
assert.strictEqual(res.exitCode, 1);
226+
assert.match(res.stderr, /flag must be used/);
227+
});
228+
229+
it('should respect url rewrites', async () => {
230+
const res = await execa(node, [
231+
linkinator,
232+
'--url-rewrite-search',
233+
'NOTLICENSE.md',
234+
'--url-rewrite-replace',
235+
'LICENSE.md',
236+
'test/fixtures/rewrite/README.md',
237+
]);
238+
assert.match(res.stderr, /Successfully scanned/);
239+
});
240+
205241
it('should warn on retries', async () => {
206242
// start a web server to return the 429
207243
let requestCount = 0;

‎test/test.index.ts

+13
Original file line numberDiff line numberDiff line change
@@ -524,4 +524,17 @@ describe('linkinator', () => {
524524
assert.strictEqual(fakeLink.url, 'http://fake.local/');
525525
scope.done();
526526
});
527+
528+
it('should rewrite urls', async () => {
529+
const results = await check({
530+
path: 'test/fixtures/rewrite/README.md',
531+
urlRewriteExpressions: [
532+
{
533+
pattern: /NOTLICENSE\.[a-z]+/,
534+
replacement: 'LICENSE.md',
535+
},
536+
],
537+
});
538+
assert.ok(results.passed);
539+
});
527540
});

0 commit comments

Comments
 (0)
Please sign in to comment.