Skip to content

Commit 19d8fc0

Browse files
Merge pull request #2106 from remarkablemark/refactor/eslint
refactor(eslint): add more ESLint rules and fix lint errors
2 parents 1e6c401 + 74dd707 commit 19d8fc0

File tree

12 files changed

+79
-27
lines changed

12 files changed

+79
-27
lines changed

__tests__/dom-to-react.test.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ describe('domToReact', () => {
5252

5353
it('does not have `children` for void elements', () => {
5454
const reactElement = domToReact(htmlToDOM(html.img)) as React.JSX.Element;
55+
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
5556
expect(reactElement.props.children).toBe(undefined);
5657
});
5758

@@ -87,6 +88,7 @@ describe('domToReact', () => {
8788
});
8889
});
8990

91+
/* eslint-disable @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access */
9092
describe('library option', () => {
9193
const React = require('react');
9294
const Preact = require('preact');
@@ -112,6 +114,7 @@ describe('library option', () => {
112114
expect(parsedElement).toEqual(preactElement);
113115
});
114116
});
117+
/* eslint-enable @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access */
115118

116119
describe('replace option', () => {
117120
it.each([undefined, null, 0, 1, true, false, {}])(
@@ -191,7 +194,9 @@ describe('transform option', () => {
191194
}) as React.JSX.Element;
192195

193196
expect(reactElement.key).toBe('0');
197+
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
194198
expect(reactElement.props.children.props.children[0].key).toBe('0');
199+
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
195200
expect(reactElement.props.children.props.children[1].key).toBe('1');
196201
expect(reactElement).toMatchSnapshot();
197202
});

__tests__/esm/index.test.mjs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ describe('index', () => {
1919
});
2020

2121
it('parses HTML to React element', () => {
22+
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
2223
assert.ok(isValidElement(parse('<p>text</p>')));
2324
});
2425

__tests__/index.test.tsx

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import * as domhandler from 'domhandler';
22
import type { Element } from 'html-dom-parser';
3-
import type { JSX } from 'react';
43

54
import * as HTMLReactParser from '../src';
65
import parse from '../src';
@@ -31,6 +30,7 @@ describe('module', () => {
3130
});
3231

3332
describe('HTMLReactParser', () => {
33+
// eslint-disable-next-line @typescript-eslint/no-empty-function
3434
it.each([undefined, null, {}, [], true, false, 0, 1, () => {}, new Date()])(
3535
'throws error for value: %p',
3636
(value) => {
@@ -110,7 +110,10 @@ describe('HTMLReactParser', () => {
110110
it('decodes HTML entities', () => {
111111
const encodedEntities = 'asdf &amp; &yuml; &uuml; &apos;';
112112
const decodedEntities = "asdf & ÿ ü '";
113-
const reactElement = parse('<i>' + encodedEntities + '</i>') as JSX.Element;
113+
const reactElement = parse(
114+
'<i>' + encodedEntities + '</i>',
115+
) as React.JSX.Element;
116+
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
114117
expect(reactElement.props.children).toBe(decodedEntities);
115118
});
116119

@@ -136,6 +139,7 @@ describe('replace option', () => {
136139
expect(
137140
parse(html.complex, {
138141
replace(domNode) {
142+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
139143
if ((domNode as Element).attribs?.id === 'header') {
140144
return {
141145
type: 'h1',
@@ -149,6 +153,7 @@ describe('replace option', () => {
149153
});
150154

151155
describe('library option', () => {
156+
/* eslint-disable @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access */
152157
const Preact = require('preact');
153158
const React = require('react');
154159

@@ -162,6 +167,7 @@ describe('library option', () => {
162167
delete parsedElement.__v;
163168
delete preactElement.__v;
164169
expect(parsedElement).toEqual(preactElement);
170+
/* eslint-enable @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access */
165171
});
166172
});
167173

@@ -182,7 +188,7 @@ describe('trim option', () => {
182188
<tr><td>hello</td><td>\n</td><td>&nbsp;</td>\t</tr>\r
183189
</tbody>
184190
</table>`;
185-
const reactElement = parse(html) as JSX.Element;
191+
const reactElement = parse(html) as React.JSX.Element;
186192
expect(render(reactElement)).toBe(
187193
'<table><tbody><tr><td>hello</td><td>\n</td><td>\u00a0</td></tr></tbody></table>',
188194
);
@@ -193,7 +199,7 @@ describe('trim option', () => {
193199
const html = `<table>
194200
<tbody><tr><td> text </td><td> </td>\t</tr>\r</tbody>\n</table>`;
195201
const options = { trim: true };
196-
const reactElement = parse(html, options) as JSX.Element;
202+
const reactElement = parse(html, options) as React.JSX.Element;
197203
expect(render(reactElement)).toBe(
198204
'<table><tbody><tr><td> text </td><td></td></tr></tbody></table>',
199205
);
@@ -203,7 +209,7 @@ describe('trim option', () => {
203209
describe('invalid styles', () => {
204210
it('copes with invalid styles', () => {
205211
const html = '<p style="font - size: 1em">X</p>';
206-
const reactElement = parse(html) as JSX.Element;
212+
const reactElement = parse(html) as React.JSX.Element;
207213
expect(render(reactElement)).toBe('<p>X</p>');
208214
});
209215
});

__tests__/integration/index.test.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
/* eslint-disable */
2+
13
const paths = ['dist/html-react-parser.min.js', 'dist/html-react-parser.js'];
24

35
describe.each(paths)('UMD %s', (type) => {

__tests__/utilities.test.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
/* eslint-disable @typescript-eslint/no-confusing-void-expression */
2+
13
import type { Element } from 'html-dom-parser';
24
import * as React from 'react';
35

@@ -29,9 +31,10 @@ describe('isCustomComponent', () => {
2931

3032
describe('PRESERVE_CUSTOM_ATTRIBUTES', () => {
3133
const isReactGreaterThan15 =
32-
parseInt(React.version.match(/^\d./)![0], 10) >= 16;
34+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
35+
parseInt(/^\d./.exec(React.version)![0], 10) >= 16;
3336

34-
it(`is ${isReactGreaterThan15} when React.version="${React.version}"`, () => {
37+
it(`is ${String(isReactGreaterThan15)} when React.version="${React.version}"`, () => {
3538
expect(PRESERVE_CUSTOM_ATTRIBUTES).toBe(isReactGreaterThan15);
3639
});
3740
});

benchmark/index.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,8 @@ suite
1616
parse(html.complex);
1717
})
1818
.on('cycle', (event: Benchmark.Event) => {
19-
process.stdout.write(String(event.target) + '\n');
19+
// eslint-disable-next-line no-console, @typescript-eslint/no-base-to-string
20+
console.log(String(event.target));
2021
})
2122
.run({
2223
minSamples: 100,

eslint.config.mjs

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import eslint from '@eslint/js';
55
import { defineConfig } from 'eslint/config';
66
import prettier from 'eslint-plugin-prettier';
77
import simpleImportSort from 'eslint-plugin-simple-import-sort';
8+
import tsdoc from 'eslint-plugin-tsdoc';
89
import globals from 'globals';
910
import tseslint from 'typescript-eslint';
1011

@@ -14,31 +15,40 @@ export default defineConfig([
1415
includeIgnoreFile(gitignorePath),
1516

1617
{
17-
ignores: ['examples/*'],
18+
ignores: ['esm', 'examples', 'umd'],
1819
},
1920

2021
{
21-
files: ['**/*.{js,jsx,mjs,cjs,ts,tsx,mts,cts}'],
22+
files: ['**/*.{cjs,cts,js,jsx,mjs,mts,ts,tsx}'],
2223

2324
plugins: {
2425
'simple-import-sort': simpleImportSort,
2526
eslint,
2627
prettier,
28+
tsdoc,
2729
},
2830

29-
extends: ['eslint/recommended'],
31+
extends: [
32+
eslint.configs.recommended,
33+
tseslint.configs.recommended,
34+
tseslint.configs.recommendedTypeChecked,
35+
tseslint.configs.strictTypeChecked,
36+
tseslint.configs.stylisticTypeChecked,
37+
],
3038

3139
languageOptions: {
3240
globals: {
3341
...globals.browser,
3442
...globals.jest,
3543
...globals.node,
3644
},
45+
parserOptions: {
46+
project: ['tsconfig.build.json', 'tsconfig.test.json'],
47+
tsconfigRootDir: import.meta.dirname,
48+
},
3749
},
3850

3951
rules: {
40-
'@typescript-eslint/no-explicit-any': 'off',
41-
'@typescript-eslint/no-var-requires': 'off',
4252
'no-console': 'error',
4353
'no-debugger': 'error',
4454
'prettier/prettier': 'error',
@@ -54,6 +64,15 @@ export default defineConfig([
5464

5565
rules: {
5666
'@typescript-eslint/no-require-imports': 'off',
67+
// https://github.com/nodejs/node/issues/51292#issuecomment-3151271587
68+
'@typescript-eslint/no-floating-promises': [
69+
'error',
70+
{
71+
allowForKnownSafeCalls: [
72+
{ from: 'package', name: ['suite', 'test'], package: 'node:test' },
73+
],
74+
},
75+
],
5776
},
5877
},
5978
]);

src/attributes-to-props.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -66,15 +66,17 @@ export default function attributesToProps(
6666
let propName = getPropName(attributeNameLowerCased);
6767

6868
if (propName) {
69-
const propertyInfo = getPropertyInfo(propName);
69+
const propertyInfo = getPropertyInfo(propName) as
70+
| { type: number }
71+
| undefined;
7072

7173
// convert attribute to uncontrolled component prop (e.g., `value` to `defaultValue`)
7274
if (
7375
UNCONTROLLED_COMPONENT_ATTRIBUTES.includes(
7476
propName as UncontrolledComponentAttributes,
7577
) &&
7678
UNCONTROLLED_COMPONENT_NAMES.includes(
77-
nodeName! as UncontrolledComponentNames,
79+
nodeName as UncontrolledComponentNames,
7880
) &&
7981
!isInputValueOnly
8082
) {
@@ -83,7 +85,7 @@ export default function attributesToProps(
8385

8486
props[propName] = attributeValue;
8587

86-
switch (propertyInfo && propertyInfo.type) {
88+
switch (propertyInfo?.type) {
8789
case BOOLEAN:
8890
props[propName] = true;
8991
break;

src/dom-to-react.ts

Lines changed: 20 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
/* eslint-disable @typescript-eslint/no-unsafe-enum-comparison */
2+
13
import type { DOMNode, Element, Text } from 'html-dom-parser';
24
import type { JSX } from 'react';
35
import { cloneElement, createElement, isValidElement } from 'react';
@@ -30,12 +32,12 @@ export default function domToReact(
3032
nodes: DOMNode[],
3133
options: HTMLReactParserOptions = {},
3234
): string | JSX.Element | JSX.Element[] {
33-
const reactElements = [];
35+
const reactElements: JSX.Element[] = [];
3436

3537
const hasReplace = typeof options.replace === 'function';
36-
const transform = options.transform || returnFirstArg;
38+
const transform = options.transform ?? returnFirstArg;
3739
const { cloneElement, createElement, isValidElement } =
38-
options.library || React;
40+
options.library ?? React;
3941

4042
const nodesLength = nodes.length;
4143

@@ -44,18 +46,21 @@ export default function domToReact(
4446

4547
// replace with custom React element (if present)
4648
if (hasReplace) {
47-
let replaceElement = options.replace!(node, index) as JSX.Element;
49+
let replaceElement = options.replace?.(node, index) as JSX.Element;
4850

4951
if (isValidElement(replaceElement)) {
5052
// set "key" prop for sibling elements
5153
// https://react.dev/learn/rendering-lists#rules-of-keys
5254
if (nodesLength > 1) {
5355
replaceElement = cloneElement(replaceElement, {
54-
key: replaceElement.key || index,
56+
key: replaceElement.key ?? index,
5557
});
5658
}
5759

58-
reactElements.push(transform(replaceElement, node, index));
60+
reactElements.push(
61+
transform(replaceElement, node, index) as JSX.Element,
62+
);
63+
5964
continue;
6065
}
6166
}
@@ -81,7 +86,7 @@ export default function domToReact(
8186

8287
// We have a text node that's not whitespace and it can be nested
8388
// in its parent so add it to the results
84-
reactElements.push(transform(node.data, node, index));
89+
reactElements.push(transform(node.data, node, index) as JSX.Element);
8590
continue;
8691
}
8792

@@ -91,6 +96,7 @@ export default function domToReact(
9196
if (skipAttributesToProps(element)) {
9297
setStyleProp(element.attribs.style, element.attribs);
9398
props = element.attribs;
99+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
94100
} else if (element.attribs) {
95101
props = attributesToProps(element.attribs, element.name);
96102
}
@@ -114,7 +120,8 @@ export default function domToReact(
114120
// https://react.dev/reference/react-dom/components/textarea#caveats
115121
if (node.name === 'textarea' && node.children[0]) {
116122
props.defaultValue = (node.children[0] as Text).data;
117-
} else if (node.children && node.children.length) {
123+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
124+
} else if (node.children?.length) {
118125
// continue recursion of creating React elements (if applicable)
119126
children = domToReact(node.children as Text[], options);
120127
}
@@ -132,7 +139,11 @@ export default function domToReact(
132139
}
133140

134141
reactElements.push(
135-
transform(createElement(node.name, props, children), node, index),
142+
transform(
143+
createElement(node.name, props, children),
144+
node,
145+
index,
146+
) as JSX.Element,
136147
);
137148
}
138149

src/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ export default function HTMLReactParser(
3232
}
3333

3434
return domToReact(
35-
htmlToDOM(html, options?.htmlparser2 || domParserOptions),
35+
htmlToDOM(html, options?.htmlparser2 ?? domParserOptions),
3636
options,
3737
);
3838
}

0 commit comments

Comments
 (0)