Skip to content

Commit 39cf9d2

Browse files
authoredDec 29, 2020
feat: support directory listings (#225)
In addition to providing the directory listing flag, this swaps the underlying HTTP server from `serve-static` to `serve-handler`. There should be no user facing changes for that swap.
1 parent 6f8d65a commit 39cf9d2

File tree

11 files changed

+67
-24
lines changed

11 files changed

+67
-24
lines changed
 

‎README.md

+6
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,10 @@ $ linkinator LOCATIONS [ --arguments ]
4545
--config
4646
Path to the config file to use. Looks for `linkinator.config.json` by default.
4747
48+
--directory-listing
49+
Include an automatic directory index file when linking to a directory.
50+
Defaults to 'false'.
51+
4852
--format, -f
4953
Return the data in CSV or JSON format.
5054
@@ -138,6 +142,7 @@ All options are optional. It should look like this:
138142
"concurrency": 100,
139143
"timeout": 0,
140144
"markdown": true,
145+
"directoryListing": true,
141146
"skip": "www.googleapis.com"
142147
}
143148
```
@@ -161,6 +166,7 @@ where the server is started. Defaults to the path passed in `path`.
161166
- `timeout` (number) - By default, requests made by linkinator do not time out (or follow the settings of the OS). This option (in milliseconds) will fail requests after the configured amount of time.
162167
- `markdown` (boolean) - Automatically parse and scan markdown if scanning from a location on disk.
163168
- `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.
169+
- `directoryListing` (boolean) - Automatically serve a static file listing page when serving a directory. Defaults to `false`.
164170

165171
#### linkinator.LinkChecker()
166172
Constructor method that can be used to create a new `LinkChecker` instance. This is particularly useful if you want to receive events as the crawler crawls. Exposes the following events:

‎package.json

+2-4
Original file line numberDiff line numberDiff line change
@@ -23,27 +23,25 @@
2323
"dependencies": {
2424
"chalk": "^4.0.0",
2525
"cheerio": "^1.0.0-rc.2",
26-
"finalhandler": "^1.1.2",
2726
"gaxios": "^4.0.0",
2827
"glob": "^7.1.6",
2928
"jsonexport": "^3.0.0",
3029
"marked": "^1.2.5",
3130
"meow": "^8.0.0",
3231
"p-queue": "^6.2.1",
33-
"serve-static": "^1.14.1",
32+
"serve-handler": "^6.1.3",
3433
"server-destroy": "^1.0.1",
3534
"update-notifier": "^5.0.0"
3635
},
3736
"devDependencies": {
3837
"@types/chai": "^4.2.7",
3938
"@types/cheerio": "0.22.22",
40-
"@types/finalhandler": "^1.1.0",
4139
"@types/glob": "^7.1.3",
4240
"@types/marked": "^1.2.0",
4341
"@types/meow": "^5.0.0",
4442
"@types/mocha": "^8.0.0",
4543
"@types/node": "^12.7.12",
46-
"@types/serve-static": "^1.13.8",
44+
"@types/serve-handler": "^6.1.0",
4745
"@types/server-destroy": "^1.0.0",
4846
"@types/sinon": "^9.0.0",
4947
"@types/update-notifier": "^5.0.0",

‎src/cli.ts

+6
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,10 @@ const cli = meow(
3535
--config
3636
Path to the config file to use. Looks for \`linkinator.config.json\` by default.
3737
38+
--directory-listing
39+
Include an automatic directory index file when linking to a directory.
40+
Defaults to 'false'.
41+
3842
--format, -f
3943
Return the data in CSV or JSON format.
4044
@@ -80,6 +84,7 @@ const cli = meow(
8084
markdown: {type: 'boolean'},
8185
serverRoot: {type: 'string'},
8286
verbosity: {type: 'string'},
87+
directoryListing: {type: 'boolean'},
8388
},
8489
booleanDefault: undefined,
8590
}
@@ -126,6 +131,7 @@ async function main() {
126131
markdown: flags.markdown,
127132
concurrency: Number(flags.concurrency),
128133
serverRoot: flags.serverRoot,
134+
directoryListing: flags.directoryListing,
129135
};
130136
if (flags.skip) {
131137
if (typeof flags.skip === 'string') {

‎src/config.ts

+1
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ export interface Flags {
1313
timeout?: number;
1414
markdown?: boolean;
1515
serverRoot?: string;
16+
directoryListing?: boolean;
1617
}
1718

1819
export async function getConfig(flags: Flags) {

‎src/index.ts

+11-4
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ export interface CheckOptions {
2525
markdown?: boolean;
2626
linksToSkip?: string[] | ((link: string) => Promise<boolean>);
2727
serverRoot?: string;
28+
directoryListing?: boolean;
2829
}
2930

3031
export enum LinkState {
@@ -82,11 +83,12 @@ export class LinkChecker extends EventEmitter {
8283
const hasHttpPaths = options.path.find(x => x.startsWith('http'));
8384
if (!hasHttpPaths) {
8485
const port = options.port || 5000 + Math.round(Math.random() * 1000);
85-
server = await startWebServer(
86-
options.serverRoot!,
86+
server = await startWebServer({
87+
root: options.serverRoot!,
8788
port,
88-
options.markdown
89-
);
89+
markdown: options.markdown,
90+
directoryListing: options.directoryListing,
91+
});
9092
for (let i = 0; i < options.path.length; i++) {
9193
if (options.path[i].startsWith('/')) {
9294
options.path[i] = options.path[i].slice(1);
@@ -150,6 +152,11 @@ export class LinkChecker extends EventEmitter {
150152
options.path = [options.path];
151153
}
152154

155+
// disable directory listings by default
156+
if (options.directoryListing === undefined) {
157+
options.directoryListing = false;
158+
}
159+
153160
// Ensure we do not mix http:// and file system paths. The paths passed in
154161
// must all be filesystem paths, or HTTP paths.
155162
let isUrlType: boolean | undefined = undefined;

‎src/server.ts

+20-15
Original file line numberDiff line numberDiff line change
@@ -3,34 +3,36 @@ import * as path from 'path';
33
import * as util from 'util';
44
import * as fs from 'fs';
55
import * as marked from 'marked';
6-
import finalhandler = require('finalhandler');
7-
import serveStatic = require('serve-static');
6+
import serve = require('serve-handler');
87
import enableDestroy = require('server-destroy');
98

109
const readFile = util.promisify(fs.readFile);
1110

11+
export interface WebServerOptions {
12+
// The local path that should be mounted as a static web server
13+
root: string;
14+
// The port on which to start the local web server
15+
port: number;
16+
// If markdown should be automatically compiled and served
17+
markdown?: boolean;
18+
// Should directories automatically serve an inde page
19+
directoryListing?: boolean;
20+
}
21+
1222
/**
1323
* Spin up a local HTTP server to serve static requests from disk
14-
* @param root The local path that should be mounted as a static web server
15-
* @param port The port on which to start the local web server
16-
* @param markdown If markdown should be automatically compiled and served
1724
* @private
1825
* @returns Promise that resolves with the instance of the HTTP server
1926
*/
20-
export async function startWebServer(
21-
root: string,
22-
port: number,
23-
markdown?: boolean
24-
) {
27+
export async function startWebServer(options: WebServerOptions) {
2528
return new Promise<http.Server>((resolve, reject) => {
26-
const serve = serveStatic(root);
2729
const server = http
2830
.createServer(async (req, res) => {
2931
const pathParts = req.url!.split('/').filter(x => !!x);
3032
if (pathParts.length > 0) {
3133
const ext = path.extname(pathParts[pathParts.length - 1]);
32-
if (markdown && ext.toLowerCase() === '.md') {
33-
const filePath = path.join(path.resolve(root), req.url!);
34+
if (options.markdown && ext.toLowerCase() === '.md') {
35+
const filePath = path.join(path.resolve(options.root), req.url!);
3436
const data = await readFile(filePath, {encoding: 'utf-8'});
3537
const result = marked(data, {gfm: true});
3638
res.writeHead(200, {
@@ -40,9 +42,12 @@ export async function startWebServer(
4042
return;
4143
}
4244
}
43-
return serve(req, res, finalhandler(req, res) as () => void);
45+
return serve(req, res, {
46+
public: options.root,
47+
directoryListing: options.directoryListing,
48+
});
4449
})
45-
.listen(port, () => resolve(server))
50+
.listen(options.port, () => resolve(server))
4651
.on('error', reject);
4752
enableDestroy(server);
4853
});

‎test/fixtures/config/linkinator.config.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -3,5 +3,6 @@
33
"recurse": true,
44
"silent": true,
55
"concurrency": 17,
6-
"skip": "🌳"
6+
"skip": "🌳",
7+
"directoryListing": false
78
}
+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
This has links to a [directory with files](dir1/) and an [empty directory](dir2/).
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
👋

‎test/fixtures/directoryIndex/dir2/dir2.md

Whitespace-only changes.

‎test/test.ts

+17
Original file line numberDiff line numberDiff line change
@@ -480,4 +480,21 @@ describe('linkinator', () => {
480480
scope.done();
481481
assert.strictEqual(results.links.length, 2);
482482
});
483+
484+
it('should support directory index', async () => {
485+
const results = await check({
486+
path: 'test/fixtures/directoryIndex/README.md',
487+
directoryListing: true,
488+
});
489+
assert.ok(results.passed);
490+
assert.strictEqual(results.links.length, 3);
491+
});
492+
493+
it('should disabling directory index by default', async () => {
494+
const results = await check({
495+
path: 'test/fixtures/directoryIndex/README.md',
496+
});
497+
assert.ok(!results.passed);
498+
assert.strictEqual(results.links.length, 3);
499+
});
483500
});

0 commit comments

Comments
 (0)
Please sign in to comment.