4
4
5
5
let chalk ;
6
6
const fs = require ( 'fs' ) ;
7
- const markdownLinkCheck = require ( './' ) ;
7
+ const { promisify } = require ( 'util' ) ;
8
+ const markdownLinkCheck = promisify ( require ( './' ) ) ;
8
9
const needle = require ( 'needle' ) ;
9
10
const path = require ( 'path' ) ;
10
11
const pkg = require ( './package.json' ) ;
11
12
const { Command } = require ( 'commander' ) ;
12
13
const program = new Command ( ) ;
13
- const url = require ( 'url' ) ;
14
14
const { ProxyAgent } = require ( 'proxy-agent' ) ;
15
15
16
16
class Input {
@@ -31,6 +31,26 @@ function commaSeparatedCodesList(value, dummyPrevious) {
31
31
} ) ;
32
32
}
33
33
34
+ /**
35
+ * Load all files in the rootFolder and all subfolders that end with .md
36
+ */
37
+ function loadAllMarkdownFiles ( rootFolder = '.' ) {
38
+ const files = [ ] ;
39
+ fs . readdirSync ( rootFolder ) . forEach ( file => {
40
+ const fullPath = path . join ( rootFolder , file ) ;
41
+ if ( fs . lstatSync ( fullPath ) . isDirectory ( ) ) {
42
+ files . push ( ...loadAllMarkdownFiles ( fullPath ) ) ;
43
+ } else if ( fullPath . endsWith ( '.md' ) ) {
44
+ files . push ( fullPath ) ;
45
+ }
46
+ } ) ;
47
+ return files ;
48
+ }
49
+
50
+ function commaSeparatedReportersList ( value ) {
51
+ return value . split ( ',' ) . map ( ( reporter ) => require ( path . resolve ( 'reporters' , reporter ) ) ) ;
52
+ }
53
+
34
54
function getInputs ( ) {
35
55
const inputs = [ ] ;
36
56
@@ -40,11 +60,12 @@ function getInputs() {
40
60
. option ( '-c, --config [config]' , 'apply a config file (JSON), holding e.g. url specific header configuration' )
41
61
. option ( '-q, --quiet' , 'displays errors only' )
42
62
. option ( '-v, --verbose' , 'displays detailed error information' )
43
- . option ( '-i --ignore <paths>' , 'ignore input paths including an ignore path' , commaSeparatedPathsList )
63
+ . option ( '-i, --ignore <paths>' , 'ignore input paths including an ignore path' , commaSeparatedPathsList )
44
64
. option ( '-a, --alive <code>' , 'comma separated list of HTTP codes to be considered as alive' , commaSeparatedCodesList )
45
65
. option ( '-r, --retry' , 'retry after the duration indicated in \'retry-after\' header when HTTP code is 429' )
66
+ . option ( '--reporters <names>' , 'specify reporters to use' , commaSeparatedReportersList )
46
67
. option ( '--projectBaseUrl <url>' , 'the URL to use for {{BASEURL}} replacement' )
47
- . arguments ( '[filenamesOrUrls ...]' )
68
+ . arguments ( '[filenamesOrDirectorynamesOrUrls ...]' )
48
69
. action ( function ( filenamesOrUrls ) {
49
70
let filenameForOutput ;
50
71
let stream ;
@@ -70,6 +91,7 @@ function getInputs() {
70
91
for ( const filenameOrUrl of filenamesOrUrls ) {
71
92
filenameForOutput = filenameOrUrl ;
72
93
let baseUrl = '' ;
94
+ // remote file
73
95
if ( / h t t p s ? : / . test ( filenameOrUrl ) ) {
74
96
stream = needle . get (
75
97
filenameOrUrl , { agent : new ProxyAgent ( ) , use_proxy_from_env_var : false }
@@ -81,37 +103,44 @@ function getInputs() {
81
103
parsed . search = '' ;
82
104
parsed . hash = '' ;
83
105
if ( parsed . pathname . lastIndexOf ( '/' ) !== - 1 ) {
84
- parsed . pathname = parsed . pathname . substr ( 0 , parsed . pathname . lastIndexOf ( '/' ) + 1 ) ;
106
+ parsed . pathname = parsed . pathname . substring ( 0 , parsed . pathname . lastIndexOf ( '/' ) + 1 ) ;
85
107
}
86
108
baseUrl = parsed . toString ( ) ;
87
- } catch ( err ) { /* ignore error */
88
- }
109
+ inputs . push ( new Input ( filenameForOutput , stream , { baseUrl : baseUrl } ) ) ;
110
+ } catch ( err ) {
111
+ /* ignore error */
112
+ }
89
113
} else {
90
- const stats = fs . statSync ( filenameOrUrl ) ;
91
- if ( stats . isDirectory ( ) ) {
92
- console . error ( chalk . red ( '\nERROR: ' + filenameOrUrl + ' is a directory! Please provide a valid filename as an argument.' ) ) ;
93
- process . exit ( 1 ) ;
114
+ // local file or directory
115
+ let files = [ ] ;
116
+
117
+ if ( fs . statSync ( filenameOrUrl ) . isDirectory ( ) ) {
118
+ files = loadAllMarkdownFiles ( filenameOrUrl )
119
+ } else {
120
+ files = [ filenameOrUrl ]
94
121
}
95
122
96
- const resolved = path . resolve ( filenameOrUrl ) ;
123
+ for ( let file of files ) {
124
+ filenameForOutput = file ;
125
+ const resolved = path . resolve ( filenameForOutput ) ;
97
126
98
- // skip paths given if it includes a path to ignore.
99
- // todo: allow ignore paths to be glob or regex instead of just includes?
100
- if ( ignore && ignore . some ( ( ignorePath ) => resolved . includes ( ignorePath ) ) ) {
101
- continue ;
102
- }
127
+ // skip paths given if it includes a path to ignore.
128
+ // todo: allow ignore paths to be glob or regex instead of just includes?
129
+ if ( ignore && ignore . some ( ( ignorePath ) => resolved . includes ( ignorePath ) ) ) {
130
+ continue ;
131
+ }
103
132
104
- if ( process . platform === 'win32' ) {
105
- baseUrl = 'file://' + path . dirname ( resolved ) . replace ( / \\ / g, '/' ) ;
106
- }
107
- else {
108
- baseUrl = 'file://' + path . dirname ( resolved ) ;
109
- }
133
+ if ( process . platform === 'win32' ) {
134
+ baseUrl = 'file://' + path . dirname ( resolved ) . replace ( / \\ / g, '/' ) ;
135
+ }
136
+ else {
137
+ baseUrl = 'file://' + path . dirname ( resolved ) ;
138
+ }
110
139
111
- stream = fs . createReadStream ( filenameOrUrl ) ;
140
+ stream = fs . createReadStream ( filenameForOutput ) ;
141
+ inputs . push ( new Input ( filenameForOutput , stream , { baseUrl : baseUrl } ) ) ;
142
+ }
112
143
}
113
-
114
- inputs . push ( new Input ( filenameForOutput , stream , { baseUrl : baseUrl } ) ) ;
115
144
}
116
145
}
117
146
) . parse ( process . argv ) ;
@@ -122,6 +151,7 @@ function getInputs() {
122
151
input . opts . verbose = ( program . opts ( ) . verbose === true ) ;
123
152
input . opts . retryOn429 = ( program . opts ( ) . retry === true ) ;
124
153
input . opts . aliveStatusCodes = program . opts ( ) . alive ;
154
+ input . opts . reporters = program . opts ( ) . reporters ?? [ require ( path . resolve ( 'reporters' , 'default.js' ) ) ] ;
125
155
const config = program . opts ( ) . config ;
126
156
if ( config ) {
127
157
input . opts . config = config . trim ( ) ;
@@ -196,68 +226,23 @@ async function processInput(filenameForOutput, stream, opts) {
196
226
opts . retryCount = config . retryCount ;
197
227
opts . fallbackRetryDelay = config . fallbackRetryDelay ;
198
228
opts . aliveStatusCodes = config . aliveStatusCodes ;
229
+ opts . reporters = config . reporters ;
199
230
}
200
231
201
232
await runMarkdownLinkCheck ( filenameForOutput , markdown , opts ) ;
202
233
}
203
234
204
235
async function runMarkdownLinkCheck ( filenameForOutput , markdown , opts ) {
205
- const statusLabels = {
206
- alive : chalk . green ( '✓' ) ,
207
- dead : chalk . red ( '✖' ) ,
208
- ignored : chalk . gray ( '/' ) ,
209
- error : chalk . yellow ( '⚠' ) ,
210
- } ;
236
+ const [ err , results ] = await markdownLinkCheck ( markdown , opts )
237
+ . then ( res => [ null , res ] ) . catch ( err => [ err ] ) ;
211
238
212
- return new Promise ( ( resolve , reject ) => {
213
- markdownLinkCheck ( markdown , opts , function ( err , results ) {
214
- if ( err ) {
215
- console . error ( chalk . red ( '\n ERROR: something went wrong!' ) ) ;
216
- console . error ( err . stack ) ;
217
- reject ( ) ;
218
- }
219
-
220
- if ( results . length === 0 && ! opts . quiet ) {
221
- console . log ( chalk . yellow ( ' No hyperlinks found!' ) ) ;
222
- }
223
- results . forEach ( function ( result ) {
224
- // Skip messages for non-deadlinks in quiet mode.
225
- if ( opts . quiet && result . status !== 'dead' ) {
226
- return ;
227
- }
228
-
229
- if ( opts . verbose ) {
230
- if ( result . err ) {
231
- console . log ( ' [%s] %s → Status: %s %s' , statusLabels [ result . status ] , result . link , result . statusCode , result . err ) ;
232
- } else {
233
- console . log ( ' [%s] %s → Status: %s' , statusLabels [ result . status ] , result . link , result . statusCode ) ;
234
- }
235
- }
236
- else if ( ! opts . quiet ) {
237
- console . log ( ' [%s] %s' , statusLabels [ result . status ] , result . link ) ;
238
- }
239
- } ) ;
239
+ await Promise . allSettled (
240
+ opts . reporters . map ( reporter => reporter ( err , results , opts , filenameForOutput )
241
+ ) ) ;
240
242
241
- if ( ! opts . quiet ) {
242
- console . log ( '\n %s links checked.' , results . length ) ;
243
- }
244
-
245
- if ( results . some ( ( result ) => result . status === 'dead' ) ) {
246
- let deadLinks = results . filter ( result => { return result . status === 'dead' ; } ) ;
247
- if ( ! opts . quiet ) {
248
- console . error ( chalk . red ( '\n ERROR: %s dead links found!' ) , deadLinks . length ) ;
249
- } else {
250
- console . error ( chalk . red ( '\n ERROR: %s dead links found in %s !' ) , deadLinks . length , filenameForOutput ) ;
251
- }
252
- deadLinks . forEach ( function ( result ) {
253
- console . log ( ' [%s] %s → Status: %s' , statusLabels [ result . status ] , result . link , result . statusCode ) ;
254
- } ) ;
255
- reject ( ) ;
256
- }
257
-
258
- resolve ( ) ;
259
- } ) ;
260
- } ) ;
243
+ if ( err ) throw null ;
244
+ else if ( results . some ( ( result ) => result . status === 'dead' ) ) return ;
245
+ else return ;
261
246
}
262
247
263
248
async function main ( ) {
0 commit comments