Skip to content

Commit a923fce

Browse files
authored
refactor: migrate to TypeScript (#150)
* refactor: migrate to TypeScript * test: add mocharc * fix: use match * chore: remove prettier * fix: to use uri
1 parent ef0d837 commit a923fce

File tree

7 files changed

+363
-62
lines changed

7 files changed

+363
-62
lines changed

.mocharc.json

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,3 @@
11
{
2-
"require": [
3-
"textlint-scripts/register"
4-
],
5-
"timeout": "20000"
2+
"timeout": 20000
63
}

.prettierrc

Lines changed: 0 additions & 5 deletions
This file was deleted.

package.json

Lines changed: 26 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -13,24 +13,32 @@
1313
"repository": "textlint-rule/textlint-rule-no-dead-link",
1414
"license": "MIT",
1515
"author": "nodaguti",
16+
"main": "lib/no-dead-link.js",
17+
"types": "lib/no-dead-link.d.ts",
1618
"files": [
1719
"lib",
1820
"src"
1921
],
20-
"main": "lib/no-dead-link.js",
2122
"scripts": {
2223
"build": "textlint-scripts build",
23-
"prepublish": "yarn run --if-present build",
24-
"test": "textlint-scripts test",
25-
"watch": "textlint-scripts build --watch",
2624
"format": "prettier --write \"**/*.{js,jsx,ts,tsx,css}\"",
27-
"prepare": "git config --local core.hooksPath .githooks"
25+
"prepare": "git config --local core.hooksPath .githooks",
26+
"prepublish": "yarn run --if-present build",
27+
"test": "npm run type-check && textlint-scripts test",
28+
"type-check": "tsc --noEmit",
29+
"watch": "textlint-scripts build --watch"
2830
},
2931
"lint-staged": {
3032
"*.{js,jsx,ts,tsx,css}": [
3133
"prettier --write"
3234
]
3335
},
36+
"prettier": {
37+
"printWidth": 120,
38+
"singleQuote": false,
39+
"tabWidth": 4,
40+
"trailingComma": "none"
41+
},
3442
"dependencies": {
3543
"fs-extra": "^8.1.0",
3644
"get-url-origin": "^1.0.1",
@@ -41,20 +49,25 @@
4149
"textlint-rule-helper": "^2.2.2"
4250
},
4351
"devDependencies": {
52+
"@textlint/ast-node-types": "^12.2.2",
53+
"@textlint/types": "^12.2.2",
54+
"@types/minimatch": "^5.1.2",
55+
"@types/mocha": "^10.0.0",
56+
"@types/node": "^18.11.7",
57+
"@types/node-fetch": "^2.6.2",
58+
"cross-env": "^7.0.3",
4459
"lint-staged": "^13.0.3",
60+
"mocha": "^10.1.0",
4561
"prettier": "^2.7.1",
4662
"textlint": "^12.2.2",
4763
"textlint-scripts": "^12.2.2",
48-
"textlint-tester": "^12.2.2"
64+
"textlint-tester": "^12.2.2",
65+
"ts-node": "^10.9.1",
66+
"ts-node-test-register": "^10.0.0",
67+
"typescript": "^4.8.4"
4968
},
69+
"packageManager": "[email protected]",
5070
"engines": {
5171
"node": ">=4"
52-
},
53-
"packageManager": "[email protected]",
54-
"prettier": {
55-
"singleQuote": false,
56-
"printWidth": 120,
57-
"tabWidth": 4,
58-
"trailingComma": "none"
5972
}
6073
}

src/no-dead-link.js renamed to src/no-dead-link.ts

Lines changed: 82 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,36 @@
11
import { RuleHelper } from "textlint-rule-helper";
2-
import fetch from "node-fetch";
2+
import fetch, { RequestInit } from "node-fetch";
33
import URL from "url";
4-
import fs from "fs-extra";
4+
import fs from "fs/promises";
55
import minimatch from "minimatch";
66
import { isAbsolute } from "path";
77
import { getURLOrigin } from "get-url-origin";
88
import pMemoize from "p-memoize";
99
import PQueue from "p-queue";
1010
import * as http from "http";
1111
import * 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+
};
383430
export default {
384431
linter: reporter,
385432
fixer: reporter

test/no-dead-link.js renamed to test/no-dead-link.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
1-
/* eslint-disable max-len */
21
import TextlintTester from "textlint-tester";
32
import fs from "fs";
43
import path from "path";
54
import rule from "../src/no-dead-link";
65

76
const tester = new TextlintTester();
87

8+
// @ts-expect-error
99
tester.run("no-dead-link", rule, {
1010
valid: [
1111
"should ignore non-http url [email address](mailto:mail.example.com) by default",

0 commit comments

Comments
 (0)