@@ -6,6 +6,7 @@ import chalk = require('chalk');
6
6
import { LinkChecker , LinkState , LinkResult , CheckOptions } from './index' ;
7
7
import { promisify } from 'util' ;
8
8
import { Flags , getConfig } from './config' ;
9
+ import { Format , Logger , LogLevel } from './logger' ;
9
10
10
11
// eslint-disable-next-line @typescript-eslint/no-var-requires
11
12
const toCSV = promisify ( require ( 'jsonexport' ) ) ;
@@ -14,6 +15,8 @@ const toCSV = promisify(require('jsonexport'));
14
15
const pkg = require ( '../../package.json' ) ;
15
16
updateNotifier ( { pkg} ) . notify ( ) ;
16
17
18
+ /* eslint-disable no-process-exit */
19
+
17
20
const cli = meow (
18
21
`
19
22
Usage
@@ -45,18 +48,19 @@ const cli = meow(
45
48
Recursively follow links on the same root domain.
46
49
47
50
--server-root
48
- When scanning a locally directory, customize the location on disk
51
+ When scanning a locally directory, customize the location on disk
49
52
where the server is started. Defaults to the path passed in [LOCATION].
50
53
51
- --silent
52
- Only output broken links
53
-
54
54
--skip, -s
55
- List of urls in regexy form to not include in the check.
55
+ List of urls in regexy form to not include in the check.
56
56
57
57
--timeout
58
58
Request timeout in ms. Defaults to 0 (no timeout).
59
59
60
+ --verbosity
61
+ Override the default verbosity for this command. Available options are
62
+ 'debug', 'info', 'warning', 'error', and 'none'. Defaults to 'warning'.
63
+
60
64
Examples
61
65
$ linkinator docs/
62
66
$ linkinator https://www.google.com
@@ -75,6 +79,7 @@ const cli = meow(
75
79
timeout : { type : 'number' } ,
76
80
markdown : { type : 'boolean' } ,
77
81
serverRoot : { type : 'string' } ,
82
+ verbosity : { type : 'string' } ,
78
83
} ,
79
84
booleanDefault : undefined ,
80
85
}
@@ -90,34 +95,29 @@ async function main() {
90
95
flags = await getConfig ( cli . flags ) ;
91
96
92
97
const start = Date . now ( ) ;
98
+ const verbosity = parseVerbosity ( cli . flags ) ;
99
+ const format = parseFormat ( cli . flags ) ;
100
+ const logger = new Logger ( verbosity , format ) ;
101
+
102
+ logger . error ( `🏊♂️ crawling ${ cli . input } ` ) ;
93
103
94
- if ( ! flags . silent ) {
95
- log ( `🏊♂️ crawling ${ cli . input } ` ) ;
96
- }
97
104
const checker = new LinkChecker ( ) ;
98
- // checker.on('pagestart', url => {
99
- // if (!flags.silent) {
100
- // log(`\n Scanning ${chalk.grey(url)}`);
101
- // }
102
- // });
103
105
checker . on ( 'link' , ( link : LinkResult ) => {
104
- if ( flags . silent && link . state !== LinkState . BROKEN ) {
105
- return ;
106
- }
107
-
108
106
let state = '' ;
109
107
switch ( link . state ) {
110
108
case LinkState . BROKEN :
111
109
state = `[${ chalk . red ( link . status ! . toString ( ) ) } ]` ;
110
+ logger . error ( `${ state } ${ chalk . gray ( link . url ) } ` ) ;
112
111
break ;
113
112
case LinkState . OK :
114
113
state = `[${ chalk . green ( link . status ! . toString ( ) ) } ]` ;
114
+ logger . warn ( `${ state } ${ chalk . gray ( link . url ) } ` ) ;
115
115
break ;
116
116
case LinkState . SKIPPED :
117
117
state = `[${ chalk . grey ( 'SKP' ) } ]` ;
118
+ logger . info ( `${ state } ${ chalk . gray ( link . url ) } ` ) ;
118
119
break ;
119
120
}
120
- log ( `${ state } ${ chalk . gray ( link . url ) } ` ) ;
121
121
} ) ;
122
122
const opts : CheckOptions = {
123
123
path : cli . input ,
@@ -128,55 +128,78 @@ async function main() {
128
128
serverRoot : flags . serverRoot ,
129
129
} ;
130
130
if ( flags . skip ) {
131
- if ( typeof flags . skip === 'string' ) {
132
- opts . linksToSkip = flags . skip . split ( ' ' ) . filter ( x => ! ! x ) ;
133
- } else if ( Array . isArray ( flags . skip ) ) {
134
- opts . linksToSkip = flags . skip ;
135
- }
131
+ opts . linksToSkip = flags . skip . split ( ' ' ) . filter ( x => ! ! x ) ;
136
132
}
137
133
const result = await checker . check ( opts ) ;
138
- log ( ) ;
139
-
140
- const format = flags . format ? flags . format . toLowerCase ( ) : null ;
141
- if ( format === 'json' ) {
134
+ if ( format === Format . JSON ) {
142
135
console . log ( JSON . stringify ( result , null , 2 ) ) ;
143
136
return ;
144
- } else if ( format === 'csv' ) {
137
+ } else if ( format === Format . CSV ) {
145
138
const csv = await toCSV ( result . links ) ;
146
139
console . log ( csv ) ;
147
140
return ;
148
141
} else {
142
+ // Build a collection scanned links, collated by the parent link used in
143
+ // the scan. For example:
144
+ // {
145
+ // "./README.md": [
146
+ // {
147
+ // url: "https://img.shields.io/npm/v/linkinator.svg",
148
+ // status: 200
149
+ // ....
150
+ // }
151
+ // ],
152
+ // }
149
153
const parents = result . links . reduce ( ( acc , curr ) => {
150
- if ( ! flags . silent || curr . state === LinkState . BROKEN ) {
151
- const parent = curr . parent || '' ;
152
- if ( ! acc [ parent ] ) {
153
- acc [ parent ] = [ ] ;
154
- }
155
- acc [ parent ] . push ( curr ) ;
154
+ const parent = curr . parent || '' ;
155
+ if ( ! acc [ parent ] ) {
156
+ acc [ parent ] = [ ] ;
156
157
}
158
+ acc [ parent ] . push ( curr ) ;
157
159
return acc ;
158
160
} , { } as { [ index : string ] : LinkResult [ ] } ) ;
159
161
160
162
Object . keys ( parents ) . forEach ( parent => {
161
- const links = parents [ parent ] ;
162
- log ( chalk . blue ( parent ) ) ;
163
- links . forEach ( link => {
164
- if ( flags . silent && link . state !== LinkState . BROKEN ) {
165
- return ;
163
+ // prune links based on verbosity
164
+ const links = parents [ parent ] . filter ( link => {
165
+ if ( verbosity === LogLevel . NONE ) {
166
+ return false ;
167
+ }
168
+ if ( link . state === LinkState . BROKEN ) {
169
+ return true ;
170
+ }
171
+ if ( link . state === LinkState . OK ) {
172
+ if ( verbosity <= LogLevel . WARNING ) {
173
+ return true ;
174
+ }
166
175
}
176
+ if ( link . state === LinkState . SKIPPED ) {
177
+ if ( verbosity <= LogLevel . INFO ) {
178
+ return true ;
179
+ }
180
+ }
181
+ return false ;
182
+ } ) ;
183
+ if ( links . length === 0 ) {
184
+ return ;
185
+ }
186
+ logger . error ( chalk . blue ( parent ) ) ;
187
+ links . forEach ( link => {
167
188
let state = '' ;
168
189
switch ( link . state ) {
169
190
case LinkState . BROKEN :
170
191
state = `[${ chalk . red ( link . status ! . toString ( ) ) } ]` ;
192
+ logger . error ( ` ${ state } ${ chalk . gray ( link . url ) } ` ) ;
171
193
break ;
172
194
case LinkState . OK :
173
195
state = `[${ chalk . green ( link . status ! . toString ( ) ) } ]` ;
196
+ logger . warn ( ` ${ state } ${ chalk . gray ( link . url ) } ` ) ;
174
197
break ;
175
198
case LinkState . SKIPPED :
176
199
state = `[${ chalk . grey ( 'SKP' ) } ]` ;
200
+ logger . info ( ` ${ state } ${ chalk . gray ( link . url ) } ` ) ;
177
201
break ;
178
202
}
179
- log ( ` ${ state } ${ chalk . gray ( link . url ) } ` ) ;
180
203
} ) ;
181
204
} ) ;
182
205
}
@@ -185,7 +208,7 @@ async function main() {
185
208
186
209
if ( ! result . passed ) {
187
210
const borked = result . links . filter ( x => x . state === LinkState . BROKEN ) ;
188
- console . error (
211
+ logger . error (
189
212
chalk . bold (
190
213
`${ chalk . red ( 'ERROR' ) } : Detected ${
191
214
borked . length
@@ -194,11 +217,10 @@ async function main() {
194
217
) } links in ${ chalk . cyan ( total . toString ( ) ) } seconds.`
195
218
)
196
219
) ;
197
- // eslint-disable-next-line no-process-exit
198
220
process . exit ( 1 ) ;
199
221
}
200
222
201
- log (
223
+ logger . error (
202
224
chalk . bold (
203
225
`🤖 Successfully scanned ${ chalk . green (
204
226
result . links . length . toString ( )
@@ -207,10 +229,38 @@ async function main() {
207
229
) ;
208
230
}
209
231
210
- function log ( message = '\n' ) {
232
+ function parseVerbosity ( flags : typeof cli . flags ) : LogLevel {
233
+ if ( flags . silent && flags . verbosity ) {
234
+ throw new Error (
235
+ 'The SILENT and VERBOSITY flags cannot both be defined. Please consider using VERBOSITY only.'
236
+ ) ;
237
+ }
238
+ if ( flags . silent ) {
239
+ return LogLevel . ERROR ;
240
+ }
241
+ if ( ! flags . verbosity ) {
242
+ return LogLevel . WARNING ;
243
+ }
244
+ const verbosity = flags . verbosity . toUpperCase ( ) ;
245
+ const options = Object . values ( LogLevel ) ;
246
+ if ( ! options . includes ( verbosity ) ) {
247
+ throw new Error (
248
+ `Invalid flag: VERBOSITY must be one of [${ options . join ( ',' ) } ]`
249
+ ) ;
250
+ }
251
+ return LogLevel [ verbosity as keyof typeof LogLevel ] ;
252
+ }
253
+
254
+ function parseFormat ( flags : typeof cli . flags ) : Format {
211
255
if ( ! flags . format ) {
212
- console . log ( message ) ;
256
+ return Format . TEXT ;
257
+ }
258
+ flags . format = flags . format . toUpperCase ( ) ;
259
+ const options = Object . values ( Format ) ;
260
+ if ( ! options . includes ( flags . format ) ) {
261
+ throw new Error ( "Invalid flag: FORMAT must be 'TEXT', 'JSON', or 'CSV'." ) ;
213
262
}
263
+ return Format [ flags . format as keyof typeof Format ] ;
214
264
}
215
265
216
266
main ( ) ;
0 commit comments