@rpcbase/eslint-config 0.18.0 → 0.20.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/eslint.config.js +18 -3
- package/package.json +3 -2
- package/src/rules/no-lone-text-node.ts +110 -22
package/eslint.config.js
CHANGED
|
@@ -6,6 +6,8 @@ import pluginReact from "eslint-plugin-react"
|
|
|
6
6
|
import tseslint from "typescript-eslint"
|
|
7
7
|
import reactRefresh from "eslint-plugin-react-refresh"
|
|
8
8
|
|
|
9
|
+
import noLoneTextNode from "./src/rules/no-lone-text-node"
|
|
10
|
+
|
|
9
11
|
|
|
10
12
|
/** @type {import('eslint').Linter.Config[]} */
|
|
11
13
|
export const config = [
|
|
@@ -48,6 +50,7 @@ export const config = [
|
|
|
48
50
|
caughtErrors: "none",
|
|
49
51
|
},
|
|
50
52
|
],
|
|
53
|
+
"@typescript-eslint/no-explicit-any": "warn",
|
|
51
54
|
"import/newline-after-import": ["error", { count: 2 }],
|
|
52
55
|
// https://github.com/import-js/eslint-plugin-import/blob/HEAD/docs/rules/order.md
|
|
53
56
|
"import/order": [
|
|
@@ -83,14 +86,26 @@ export const config = [
|
|
|
83
86
|
],
|
|
84
87
|
"max-lines": [
|
|
85
88
|
"warn",
|
|
86
|
-
{ max:
|
|
89
|
+
{ max: 300, skipBlankLines: true, skipComments: false },
|
|
87
90
|
],
|
|
88
|
-
quotes: ["error", "double"],
|
|
91
|
+
"quotes": ["error", "double"],
|
|
89
92
|
"react/display-name": "off",
|
|
90
93
|
"react/no-unescaped-entities": "off",
|
|
91
94
|
"react/prop-types": "off",
|
|
92
95
|
"react/react-in-jsx-scope": "off",
|
|
93
|
-
semi: ["error", "never"],
|
|
96
|
+
"semi": ["error", "never"],
|
|
97
|
+
},
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
plugins: {
|
|
101
|
+
rb: {
|
|
102
|
+
rules: {
|
|
103
|
+
...noLoneTextNode,
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
},
|
|
107
|
+
rules: {
|
|
108
|
+
"rb/no-lone-text-node": "error",
|
|
94
109
|
},
|
|
95
110
|
},
|
|
96
111
|
];
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rpcbase/eslint-config",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.20.0",
|
|
4
4
|
"description": "",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -18,7 +18,8 @@
|
|
|
18
18
|
"dependencies": [],
|
|
19
19
|
"files": [
|
|
20
20
|
"package.json",
|
|
21
|
-
"eslint.config.js"
|
|
21
|
+
"eslint.config.js",
|
|
22
|
+
"src/"
|
|
22
23
|
],
|
|
23
24
|
"output": [],
|
|
24
25
|
"env": {
|
|
@@ -2,12 +2,53 @@ import { TSESTree, ESLintUtils } from "@typescript-eslint/experimental-utils"
|
|
|
2
2
|
|
|
3
3
|
type MessageIds = "loneText"
|
|
4
4
|
|
|
5
|
+
/**
|
|
6
|
+
* Options accepted by the rule.
|
|
7
|
+
* Currently only `ignoredChars` is supported.
|
|
8
|
+
*/
|
|
9
|
+
type RuleOptions = {
|
|
10
|
+
/**
|
|
11
|
+
* Additional characters that should be treated as "ignorable"
|
|
12
|
+
* when they appear as the only non-whitespace content of a text node.
|
|
13
|
+
*/
|
|
14
|
+
ignoredChars?: string[]
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
type Options = [RuleOptions]
|
|
18
|
+
|
|
5
19
|
const createRule = ESLintUtils.RuleCreator(
|
|
6
20
|
name => `@rpcbase/eslint-config/rules/${name}`
|
|
7
21
|
)
|
|
8
22
|
|
|
9
|
-
|
|
10
|
-
|
|
23
|
+
const defaultIgnoredChars = ["*", ":", "·", "•", "—", "–", "…", "-", "+", "(", ")", "\"", "\'"]
|
|
24
|
+
|
|
25
|
+
function isIgnoredText(text: string, ignored: string[]) {
|
|
26
|
+
// ignore if every non-whitespace char is in the list
|
|
27
|
+
return (
|
|
28
|
+
text.length > 0 &&
|
|
29
|
+
text.split("").every(ch => ch.trim().length === 0 || ignored.includes(ch))
|
|
30
|
+
)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function isMeaningful(child: TSESTree.JSXChild, ignored: string[]): boolean {
|
|
34
|
+
if (child.type === "JSXText") {
|
|
35
|
+
return !isIgnoredText(child.value.trim(), ignored) && child.value.trim().length > 0
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (
|
|
39
|
+
child.type === "JSXExpressionContainer" &&
|
|
40
|
+
child.expression.type === "Literal" &&
|
|
41
|
+
typeof child.expression.value === "string"
|
|
42
|
+
) {
|
|
43
|
+
return child.expression.value.trim().length > 0
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Any non-text node counts as meaningful
|
|
47
|
+
return true
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export default {
|
|
51
|
+
"no-lone-text-node": createRule<Options, MessageIds>({
|
|
11
52
|
name: "no-lone-text-node",
|
|
12
53
|
meta: {
|
|
13
54
|
type: "problem",
|
|
@@ -16,41 +57,88 @@ export const rules = {
|
|
|
16
57
|
recommended: "error"
|
|
17
58
|
},
|
|
18
59
|
messages: {
|
|
19
|
-
loneText:
|
|
60
|
+
loneText:
|
|
61
|
+
"Wrap text '{{text}}' in an element (e.g. <span>) to avoid Google Translate crashes."
|
|
20
62
|
},
|
|
21
|
-
schema: [
|
|
63
|
+
schema: [
|
|
64
|
+
{
|
|
65
|
+
type: "object",
|
|
66
|
+
properties: {
|
|
67
|
+
ignoredChars: {
|
|
68
|
+
type: "array",
|
|
69
|
+
items: { type: "string" }
|
|
70
|
+
}
|
|
71
|
+
},
|
|
72
|
+
additionalProperties: false
|
|
73
|
+
}
|
|
74
|
+
]
|
|
22
75
|
},
|
|
23
|
-
defaultOptions: [],
|
|
76
|
+
defaultOptions: [{}],
|
|
24
77
|
create(context) {
|
|
25
|
-
|
|
78
|
+
//--------------------------------------------------------------------
|
|
26
79
|
// helpers
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
function isAllowedWrapper(node: TSESTree.JSXElement) {
|
|
30
|
-
const name = node.openingElement.name
|
|
31
|
-
return (
|
|
32
|
-
name.type === "JSXIdentifier" &&
|
|
33
|
-
["span", "p", "label", "div", "strong", "em", "Trans", "button"].includes(
|
|
34
|
-
name.name
|
|
35
|
-
)
|
|
36
|
-
)
|
|
37
|
-
}
|
|
38
|
-
|
|
80
|
+
//--------------------------------------------------------------------
|
|
39
81
|
function report(node: TSESTree.Node, value: string) {
|
|
40
82
|
context.report({ node, messageId: "loneText", data: { text: value } })
|
|
41
83
|
}
|
|
42
84
|
|
|
43
|
-
|
|
85
|
+
//--------------------------------------------------------------------
|
|
44
86
|
// visitors
|
|
45
|
-
|
|
46
|
-
|
|
87
|
+
//--------------------------------------------------------------------
|
|
47
88
|
return {
|
|
48
89
|
JSXElement(node) {
|
|
49
|
-
|
|
90
|
+
const options = context.options[0] ?? {}
|
|
91
|
+
const ignored = options.ignoredChars ?? defaultIgnoredChars
|
|
92
|
+
|
|
93
|
+
// all meaningful children (ignoring whitespace & ignored chars)
|
|
94
|
+
const meaningfulChildren = node.children.filter(child =>
|
|
95
|
+
isMeaningful(child, ignored)
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
const singleMeaningfulTextChild =
|
|
99
|
+
meaningfulChildren.length === 1 &&
|
|
100
|
+
(meaningfulChildren[0].type === "JSXText" ||
|
|
101
|
+
(meaningfulChildren[0].type === "JSXExpressionContainer" &&
|
|
102
|
+
meaningfulChildren[0].expression.type === "Literal" &&
|
|
103
|
+
typeof meaningfulChildren[0].expression.value === "string"))
|
|
104
|
+
|
|
105
|
+
node.children.forEach(child => {
|
|
106
|
+
// --------------------------------------------------------------
|
|
107
|
+
// raw JSX text
|
|
108
|
+
// --------------------------------------------------------------
|
|
109
|
+
if (
|
|
110
|
+
child.type === "JSXText" &&
|
|
111
|
+
!isIgnoredText(child.value.trim(), ignored) &&
|
|
112
|
+
child.value.trim().length > 0
|
|
113
|
+
) {
|
|
114
|
+
if (singleMeaningfulTextChild) return
|
|
115
|
+
report(child, child.value.trim())
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// --------------------------------------------------------------
|
|
119
|
+
// {"string literal"} expression
|
|
120
|
+
// --------------------------------------------------------------
|
|
121
|
+
if (
|
|
122
|
+
child.type === "JSXExpressionContainer" &&
|
|
123
|
+
child.expression.type === "Literal" &&
|
|
124
|
+
typeof child.expression.value === "string" &&
|
|
125
|
+
child.expression.value.trim().length > 0
|
|
126
|
+
) {
|
|
127
|
+
if (singleMeaningfulTextChild) return
|
|
128
|
+
report(child, child.expression.value.trim())
|
|
129
|
+
}
|
|
130
|
+
})
|
|
131
|
+
},
|
|
132
|
+
|
|
133
|
+
// Fragments: always report raw text, even if it's the only child
|
|
134
|
+
JSXFragment(node) {
|
|
135
|
+
const options = context.options[0] ?? {}
|
|
136
|
+
const ignored = options.ignoredChars ?? defaultIgnoredChars
|
|
50
137
|
|
|
51
138
|
node.children.forEach(child => {
|
|
52
139
|
if (
|
|
53
140
|
child.type === "JSXText" &&
|
|
141
|
+
!isIgnoredText(child.value.trim(), ignored) &&
|
|
54
142
|
child.value.trim().length > 0
|
|
55
143
|
) {
|
|
56
144
|
report(child, child.value.trim())
|