11import { RuleHelper } from "textlint-rule-helper" ;
2- import fetch from "node-fetch" ;
2+ import fetch , { RequestInit } from "node-fetch" ;
33import URL from "url" ;
4- import fs from "fs-extra " ;
4+ import fs from "fs/promises " ;
55import minimatch from "minimatch" ;
66import { isAbsolute } from "path" ;
77import { getURLOrigin } from "get-url-origin" ;
88import pMemoize from "p-memoize" ;
99import PQueue from "p-queue" ;
1010import * as http from "http" ;
1111import * as https from "https" ;
12-
13- const DEFAULT_OPTIONS = {
12+ import { TextlintRuleReporter } from "@textlint/types" ;
13+ import { TxtNode } from "@textlint/ast-node-types" ;
14+
15+ export type Options = {
16+ checkRelative : boolean ; // {boolean} `false` disables the checks for relative URIs.
17+ baseURI : null | string ; // {String|null} a base URI to resolve relative URIs.
18+ ignore : string [ ] ; // {Array<String>} URIs to be skipped from availability checks.
19+ ignoreRedirects : boolean ; // {boolean} `false` ignores redirect status codes.
20+ preferGET : string [ ] ; // {Array<String>} origins to prefer GET over HEAD.
21+ retry : number ; // {number} Max retry count
22+ concurrency : number ; // {number} Concurrency count of linting link [Experimental]
23+ interval : number ; // The length of time in milliseconds before the interval count resets. Must be finite. [Experimental]
24+ intervalCap : number ; // The max number of runs in the given interval of time. [Experimental]
25+ keepAlive : boolean ; // {boolean} if it is true, use keepAlive for checking request [Experimental]
26+ userAgent : string ; // {String} a UserAgent,
27+ maxRetryTime : number ; // (number) The max of waiting seconds for retry, if response returns `After-Retry` header.
28+ } ;
29+ const DEFAULT_OPTIONS : Options = {
1430 checkRelative : true , // {boolean} `false` disables the checks for relative URIs.
1531 baseURI : null , // {String|null} a base URI to resolve relative URIs.
1632 ignore : [ ] , // {Array<String>} URIs to be skipped from availability checks.
33+ ignoreRedirects : false , // {boolean} `false` ignores redirect status codes.
1734 preferGET : [ ] , // {Array<String>} origins to prefer GET over HEAD.
1835 retry : 3 , // {number} Max retry count
1936 concurrency : 8 , // {number} Concurrency count of linting link [Experimental]
@@ -33,7 +50,7 @@ const URI_REGEXP =
3350 * @param {string } uri
3451 * @return {boolean }
3552 */
36- function isHttp ( uri ) {
53+ function isHttp ( uri : string ) {
3754 const { protocol } = URL . parse ( uri ) ;
3855 return protocol === "http:" || protocol === "https:" ;
3956}
@@ -44,7 +61,7 @@ function isHttp(uri) {
4461 * @return {boolean }
4562 * @see https://github.com/panosoft/is-local-path
4663 */
47- function isRelative ( uri ) {
64+ function isRelative ( uri : string ) {
4865 const { host } = URL . parse ( uri ) ;
4966 return host === null || host === "" ;
5067}
@@ -55,7 +72,7 @@ function isRelative(uri) {
5572 * @return {boolean }
5673 * @see https://nodejs.org/api/path.html#path_path_isabsolute_path
5774 */
58- function isLocal ( uri ) {
75+ function isLocal ( uri : string ) {
5976 if ( isAbsolute ( uri ) ) {
6077 return true ;
6178 }
@@ -68,11 +85,11 @@ function isLocal(uri) {
6885 * @param {number } code
6986 * @returns {boolean }
7087 */
71- function isRedirect ( code ) {
88+ function isRedirect ( code : number ) {
7289 return code === 301 || code === 302 || code === 303 || code === 307 || code === 308 ;
7390}
7491
75- function isIgnored ( uri , ignore = [ ] ) {
92+ function isIgnored ( uri : string , ignore : string [ ] = [ ] ) {
7693 return ignore . some ( ( pattern ) => minimatch ( uri , pattern ) ) ;
7794}
7895
@@ -81,7 +98,7 @@ function isIgnored(uri, ignore = []) {
8198 * @param ms
8299 * @returns {Promise<any> }
83100 */
84- function waitTimeMs ( ms ) {
101+ function waitTimeMs ( ms : number ) {
85102 return new Promise ( ( resolve ) => {
86103 setTimeout ( resolve , ms ) ;
87104 } ) ;
@@ -92,24 +109,24 @@ const keepAliveAgents = {
92109 https : new https . Agent ( { keepAlive : true } )
93110} ;
94111
95- const createFetchWithRuleDefaults = ( ruleOptions ) => {
112+ const createFetchWithRuleDefaults = ( ruleOptions : Options ) => {
96113 /**
97114 * Use library agent, avoid to use global.http(s)Agent
98115 * Want to avoid Socket hang up
99116 * @param parsedURL
100117 * @returns {module:http.Agent|null|module:https.Agent }
101118 */
102- const getAgent = ( parsedURL ) => {
119+ const getAgent = ( parsedURL : URL ) => {
103120 if ( ! ruleOptions . keepAlive ) {
104- return null ;
121+ return ;
105122 }
106123 if ( parsedURL . protocol === "http:" ) {
107124 return keepAliveAgents . http ;
108125 }
109126 return keepAliveAgents . https ;
110127 } ;
111128
112- return ( uri , fetchOptions ) => {
129+ return ( uri : string , fetchOptions : RequestInit ) => {
113130 const { host } = URL . parse ( uri ) ;
114131 return fetch ( uri , {
115132 ...fetchOptions ,
@@ -123,21 +140,34 @@ const createFetchWithRuleDefaults = (ruleOptions) => {
123140 headers : {
124141 "User-Agent" : ruleOptions . userAgent ,
125142 Accept : "*/*" ,
126- // Same host for target url
127- // https://github.com/textlint-rule/textlint-rule-no-dead-link/issues/111
128- Host : host
143+ // avoid assign null to Host
144+ ...( host
145+ ? {
146+ // Same host for target url
147+ // https://github.com/textlint-rule/textlint-rule-no-dead-link/issues/111
148+ Host : host
149+ }
150+ : { } )
129151 } ,
130152 // custom http(s).agent
131153 agent : getAgent
132154 } ) ;
133155 } ;
134156} ;
157+
158+ type AliveFunctionReturn = {
159+ ok : boolean ;
160+ message : string ;
161+ redirected ?: boolean ;
162+ redirectTo ?: string | null ;
163+ } ;
164+
135165/**
136166 * Create isAliveURI function with ruleOptions
137167 * @param {object } ruleOptions
138168 * @returns {isAliveURI }
139169 */
140- const createCheckAliveURL = ( ruleOptions ) => {
170+ const createCheckAliveURL = ( ruleOptions : Options ) => {
141171 // Create fetch function for this rule
142172 const fetchWithDefaults = createFetchWithRuleDefaults ( ruleOptions ) ;
143173 /**
@@ -155,8 +185,13 @@ const createCheckAliveURL = (ruleOptions) => {
155185 * @param {number } currentRetryCount
156186 * @return {{ ok: boolean, redirect?: string, message: string } }
157187 */
158- return async function isAliveURI ( uri , method = "HEAD" , maxRetryCount = 3 , currentRetryCount = 0 ) {
159- const opts = {
188+ return async function isAliveURI (
189+ uri : string ,
190+ method : string = "HEAD" ,
191+ maxRetryCount : number = 3 ,
192+ currentRetryCount : number = 0
193+ ) : Promise < AliveFunctionReturn > {
194+ const opts : RequestInit = {
160195 method,
161196 // Use `manual` redirect behaviour to get HTTP redirect status code
162197 // and see what kind of redirect is occurring
@@ -167,6 +202,15 @@ const createCheckAliveURL = (ruleOptions) => {
167202 // redirected
168203 if ( isRedirect ( res . status ) ) {
169204 const redirectedUrl = res . headers . get ( "Location" ) ;
205+ // Status code is 301 or 302, but Location header is not set
206+ if ( redirectedUrl === null ) {
207+ return {
208+ ok : false ,
209+ redirected : true ,
210+ redirectTo : null ,
211+ message : `${ res . status } ${ res . statusText } `
212+ } ;
213+ }
170214 const finalRes = await fetchWithDefaults ( redirectedUrl , { ...opts , redirect : "follow" } ) ;
171215 const { hash } = URL . parse ( uri ) ;
172216 return {
@@ -186,7 +230,8 @@ const createCheckAliveURL = (ruleOptions) => {
186230 const retrySeconds = res . headers . get ( "Retry-After" ) ;
187231 // If the response has `Retry-After` header, prefer it
188232 // else exponential retry: 0ms -> 100ms -> 200ms -> 400ms -> 800ms ...
189- const retryWaitTimeMs = retrySeconds !== null ? retrySeconds * 1000 : currentRetryCount ** 2 * 100 ;
233+ const retryWaitTimeMs =
234+ retrySeconds !== null ? Number ( retrySeconds ) * 1000 : currentRetryCount ** 2 * 100 ;
190235 const maxRetryTimeMs = ruleOptions . maxRetryTime * 1000 ;
191236 if ( retryWaitTimeMs <= maxRetryTimeMs ) {
192237 await waitTimeMs ( retryWaitTimeMs ) ;
@@ -198,7 +243,7 @@ const createCheckAliveURL = (ruleOptions) => {
198243 ok : res . ok ,
199244 message : `${ res . status } ${ res . statusText } `
200245 } ;
201- } catch ( ex ) {
246+ } catch ( ex : any ) {
202247 // Retry with `GET` method if the request failed
203248 // as some servers don't accept `HEAD` requests but are OK with `GET` requests.
204249 // https://github.com/textlint-rule/textlint-rule-no-dead-link/pull/86
@@ -217,22 +262,22 @@ const createCheckAliveURL = (ruleOptions) => {
217262/**
218263 * Check if a given file exists
219264 */
220- async function isAliveLocalFile ( filePath ) {
265+ async function isAliveLocalFile ( filePath : string ) : Promise < AliveFunctionReturn > {
221266 try {
222267 await fs . access ( filePath . replace ( / [ ? # ] .* ?$ / , "" ) ) ;
223-
224268 return {
225- ok : true
269+ ok : true ,
270+ message : "OK"
226271 } ;
227- } catch ( ex ) {
272+ } catch ( ex : any ) {
228273 return {
229274 ok : false ,
230275 message : ex . message
231276 } ;
232277 }
233278}
234279
235- function reporter ( context , options = { } ) {
280+ const reporter : TextlintRuleReporter < Options > = ( context , options ) => {
236281 const { Syntax, getSource, report, RuleError, fixer, getFilePath } = context ;
237282 const helper = new RuleHelper ( context ) ;
238283 const ruleOptions = { ...DEFAULT_OPTIONS , ...options } ;
@@ -248,7 +293,7 @@ function reporter(context, options = {}) {
248293 * @param {number } index column number the URI is located at.
249294 * @param {number } maxRetryCount retry count of linting
250295 */
251- const lint = async ( { node, uri, index } , maxRetryCount ) => {
296+ const lint = async ( { node, uri, index } : { node : TxtNode ; uri : string ; index : number } , maxRetryCount : number ) => {
252297 if ( isIgnored ( uri , ruleOptions . ignore ) ) {
253298 return ;
254299 }
@@ -296,16 +341,15 @@ function reporter(context, options = {}) {
296341 report ( node , new RuleError ( lintMessage , { index } ) ) ;
297342 } else if ( redirected ) {
298343 const lintMessage = `${ uri } is redirected to ${ redirectTo } . (${ message } )` ;
299- const fix = fixer . replaceTextRange ( [ index , index + uri . length ] , redirectTo ) ;
344+ const fix = redirectTo ? fixer . replaceTextRange ( [ index , index + uri . length ] , redirectTo ) : undefined ;
300345 report ( node , new RuleError ( lintMessage , { fix, index } ) ) ;
301346 }
302347 } ;
303348
304349 /**
305350 * URIs to be checked.
306- * @type {Array<{ node: TextLintNode, uri: string, index: number }> }
307351 */
308- const URIs = [ ] ;
352+ const URIs : { node : TxtNode ; uri : string ; index : number } [ ] = [ ] ;
309353
310354 return {
311355 [ Syntax . Str ] ( node ) {
@@ -322,8 +366,12 @@ function reporter(context, options = {}) {
322366
323367 // Use `String#replace` instead of `RegExp#exec` to allow us
324368 // perform RegExp matches in an iterate and immutable manner
325- text . replace ( URI_REGEXP , ( uri , index ) => {
326- URIs . push ( { node, uri, index } ) ;
369+ const matches = text . matchAll ( URI_REGEXP ) ;
370+ Array . from ( matches ) . forEach ( ( match ) => {
371+ const url = match [ 0 ] ;
372+ if ( url && match . input !== undefined && match . index !== undefined ) {
373+ URIs . push ( { node, uri : url , index : match . index } ) ;
374+ }
327375 } ) ;
328376 } ,
329377
@@ -378,8 +426,7 @@ function reporter(context, options = {}) {
378426 return queue . addAll ( linkTasks ) ;
379427 }
380428 } ;
381- }
382-
429+ } ;
383430export default {
384431 linter : reporter ,
385432 fixer : reporter
0 commit comments