Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}
}
Expand Down Expand Up @@ -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.
Expand Down
83 changes: 44 additions & 39 deletions src/no-dead-link.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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
Expand Down Expand Up @@ -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);
/**
Expand Down Expand Up @@ -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" });
Expand Down Expand Up @@ -290,7 +293,21 @@ const reporter: TextlintRuleReporter<Options> = (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
});
Expand All @@ -306,29 +323,17 @@ const reporter: TextlintRuleReporter<Options> = (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
Expand Down
23 changes: 23 additions & 0 deletions test/no-dead-link.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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
}
]
}
]
});