diff --git a/README.md b/README.md index 0674132..2fbae62 100644 --- a/README.md +++ b/README.md @@ -67,7 +67,8 @@ The default options are: "userAgent": "textlint-rule-no-dead-link/1.0", "maxRetryTime": 10, "maxRetryAfterTime": 90, - "linkMaxAge": 30000 + "linkMaxAge": 30000, + "httpOnly": false } } } @@ -183,6 +184,12 @@ The time (in milliseconds) that the cache for the result indicating whether a li Default: `30000` +### httpOnly + +Controls whether all links are treated as HTTP/HTTPS links. If `true`, links like `/path/to` are checked for existence via HTTP/HTTPS communication rather than the filesystem. The Base URI is the base of the redirect source URL for redirects, and `baseURI` otherwise. + +Default: `false` + ## CI Integration Probably, Link Checking take long times. diff --git a/src/no-dead-link.ts b/src/no-dead-link.ts index 7c20b4e..7a37e1b 100644 --- a/src/no-dead-link.ts +++ b/src/no-dead-link.ts @@ -23,6 +23,7 @@ export type Options = { maxRetryTime: number; // (number) The max of waiting seconds for retry. It is related to `retry` option. It does affect to `Retry-After` header. maxRetryAfterTime: number; // (number) The max of waiting seconds for `Retry-After` header. linkMaxAge: number; // (number) The max age in milliseconds for caching link check results. + httpOnly: boolean; // (boolean) `true` treats all links as http/https links }; const DEFAULT_OPTIONS: Options = { checkRelative: true, // {boolean} `false` disables the checks for relative URIs. @@ -38,7 +39,8 @@ const DEFAULT_OPTIONS: Options = { userAgent: "textlint-rule-no-dead-link/1.0", // {String} a UserAgent, maxRetryTime: 10, // (number) The max of waiting seconds for retry. It is related to `retry` option. It does affect to `Retry-After` header. maxRetryAfterTime: 10, // (number) The max of waiting seconds for `Retry-After` header. - linkMaxAge: 30 * 1000 // (number) The max age in milliseconds for caching link check results. + linkMaxAge: 30 * 1000, // (number) The max age in milliseconds for caching link check results. + httpOnly: false // (boolean) `true` treats all links as http/https links }; // Adopted from http://stackoverflow.com/a/3809435/951517 @@ -149,9 +151,10 @@ type AliveFunctionReturn = { /** * Create isAliveURI function with ruleOptions * @param {object} ruleOptions + * @param {function} resolvePath * @returns {isAliveURI} */ -const createCheckAliveURL = (ruleOptions: Options) => { +const createCheckAliveURL = (ruleOptions: Options, resolvePath: (path: string, base: string) => string | undefined) => { // Create fetch function for this rule const fetchWithDefaults = createFetchWithRuleDefaults(ruleOptions); /** @@ -183,29 +186,29 @@ const createCheckAliveURL = (ruleOptions: Options) => { }; try { const res = await fetchWithDefaults(uri, opts); + const errorResult = { + ok: false, + redirected: true, + redirectTo: null, + message: `${res.status} ${res.statusText}` + }; + // redirected if (isRedirect(res.status)) { const location = res.headers.get("Location"); // Status code is 301 or 302, but Location header is not set if (location === null) { - return { - ok: false, - redirected: true, - redirectTo: null, - message: `${res.status} ${res.statusText}` - }; + return errorResult; + } + + const base = URL.parse(uri)?.origin; + if (!base) { + return errorResult; } - const redirectedUrl = isRelative(location) - ? URL.parse(location, URL.parse(uri)?.origin)?.href - : location; + const redirectedUrl = resolvePath(location, base); if (!redirectedUrl) { - return { - ok: false, - redirected: true, - redirectTo: null, - message: `${res.status} ${res.statusText}` - }; + return errorResult; } const finalRes = await fetchWithDefaults(redirectedUrl, { ...opts, redirect: "follow" }); @@ -290,7 +293,21 @@ const reporter: TextlintRuleReporter = (context, options) => { const { Syntax, getSource, report, RuleError, fixer, getFilePath, locator } = context; const helper = new RuleHelper(context); const ruleOptions = { ...DEFAULT_OPTIONS, ...options }; - const isAliveURI = createCheckAliveURL(ruleOptions); + const resolvePath = (path: string, base: string): string | undefined => { + if (ruleOptions.httpOnly) { + return URL.parse(path, base)?.href; + } else { + if (isRelative(path)) { + // eslint-disable-next-line no-param-reassign + // Convert file path to file:// URL if needed + const baseURL = base.startsWith("http") || base.startsWith("file://") ? base : `file://${base}`; + return URL.parse(path, baseURL)?.href; + } else { + return path; + } + } + }; + const isAliveURI = createCheckAliveURL(ruleOptions, resolvePath); const memorizedIsAliveURI = pMemoize(isAliveURI, { maxAge: ruleOptions.linkMaxAge }); @@ -306,29 +323,17 @@ const reporter: TextlintRuleReporter = (context, options) => { return; } - if (isRelative(uri)) { - if (!ruleOptions.checkRelative) { - return; - } - - const filePath = getFilePath(); - const base = ruleOptions.baseURI || filePath; - if (!base) { - const message = - "Unable to resolve the relative URI. Please check if the base URI is correctly specified."; - - report(node, new RuleError(message, { padding: locator.range([index, index + uri.length]) })); - return; - } + if (isRelative(uri) && !ruleOptions.checkRelative) { + return; + } + const base = ruleOptions.baseURI || getFilePath(); + if (isRelative(uri) && !base) { + const message = "Unable to resolve the relative URI. Please check if the base URI is correctly specified."; - // eslint-disable-next-line no-param-reassign - // Convert file path to file:// URL if needed - const baseURL = base.startsWith("http") || base.startsWith("file://") ? base : `file://${base}`; - const resolved = URL.parse(uri, baseURL); - if (resolved) { - uri = resolved.href; - } + report(node, new RuleError(message, { padding: locator.range([index, index + uri.length]) })); + return; } + uri = resolvePath(uri, base || "") || uri; // Ignore non http external link // https://github.com/textlint-rule/textlint-rule-no-dead-link/issues/112 diff --git a/test/no-dead-link.ts b/test/no-dead-link.ts index 0ffd9ee..9f06d78 100644 --- a/test/no-dead-link.ts +++ b/test/no-dead-link.ts @@ -135,6 +135,14 @@ tester.run("no-dead-link", rule, { userAgent: "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.106 Safari/537.36" } + }, + // Test whether an absolute path can be resolved for `httpOnly` option + { + text: "should be able to check absolute path when httpOnly is true: [example](/200)", + options: { + httpOnly: true, + baseURI: TEST_SERVER_URL + } } // https://github.com/textlint-rule/textlint-rule-no-dead-link/issues/125 // SKIP: External service test (consul.io redirects too many times) @@ -278,6 +286,21 @@ tester.run("no-dead-link", rule, { } } ] + }, + // If not `httpOnly`, test whether the absolute path can be resolved + { + text: "should be able to check absolute path when httpOnly is false: [example](/200)", + options: { + httpOnly: false, + baseURI: TEST_SERVER_URL + }, + errors: [ + { + message: `/200 is dead. (ENOENT: no such file or directory, access '${path.resolve("/200")}')`, + line: 1, + column: 73 + } + ] } ] });