@rpcbase/eslint-config 0.17.0 → 0.19.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/package.json +6 -4
- package/src/rules/no-lone-text-node.ts +160 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rpcbase/eslint-config",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.19.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": {
|
|
@@ -33,6 +34,7 @@
|
|
|
33
34
|
},
|
|
34
35
|
"dependencies": {
|
|
35
36
|
"@eslint/js": "9.26.0",
|
|
37
|
+
"@typescript-eslint/experimental-utils": "5.62.0",
|
|
36
38
|
"eslint-config-flat-gitignore": "2.1.0",
|
|
37
39
|
"eslint-import-resolver-typescript": "4.3.4",
|
|
38
40
|
"eslint-plugin-import": "2.31.0",
|
|
@@ -43,7 +45,7 @@
|
|
|
43
45
|
},
|
|
44
46
|
"devDependencies": {
|
|
45
47
|
"@types/eslint": "9.6.1",
|
|
46
|
-
"
|
|
47
|
-
"
|
|
48
|
+
"eslint": "^9.x",
|
|
49
|
+
"typescript": "^5.x"
|
|
48
50
|
}
|
|
49
51
|
}
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import { TSESTree, ESLintUtils } from "@typescript-eslint/experimental-utils"
|
|
2
|
+
|
|
3
|
+
type MessageIds = "loneText"
|
|
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
|
+
|
|
19
|
+
const createRule = ESLintUtils.RuleCreator(
|
|
20
|
+
name => `@rpcbase/eslint-config/rules/${name}`
|
|
21
|
+
)
|
|
22
|
+
|
|
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 const rules = {
|
|
51
|
+
"no-lone-text-node": createRule<Options, MessageIds>({
|
|
52
|
+
name: "no-lone-text-node",
|
|
53
|
+
meta: {
|
|
54
|
+
type: "problem",
|
|
55
|
+
docs: {
|
|
56
|
+
description: "disallow raw text nodes in JSX",
|
|
57
|
+
recommended: "error"
|
|
58
|
+
},
|
|
59
|
+
messages: {
|
|
60
|
+
loneText:
|
|
61
|
+
"Wrap text '{{text}}' in an element (e.g. <span>) to avoid Google Translate crashes."
|
|
62
|
+
},
|
|
63
|
+
schema: [
|
|
64
|
+
{
|
|
65
|
+
type: "object",
|
|
66
|
+
properties: {
|
|
67
|
+
ignoredChars: {
|
|
68
|
+
type: "array",
|
|
69
|
+
items: { type: "string" }
|
|
70
|
+
}
|
|
71
|
+
},
|
|
72
|
+
additionalProperties: false
|
|
73
|
+
}
|
|
74
|
+
]
|
|
75
|
+
},
|
|
76
|
+
defaultOptions: [{}],
|
|
77
|
+
create(context) {
|
|
78
|
+
//--------------------------------------------------------------------
|
|
79
|
+
// helpers
|
|
80
|
+
//--------------------------------------------------------------------
|
|
81
|
+
function report(node: TSESTree.Node, value: string) {
|
|
82
|
+
context.report({ node, messageId: "loneText", data: { text: value } })
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
//--------------------------------------------------------------------
|
|
86
|
+
// visitors
|
|
87
|
+
//--------------------------------------------------------------------
|
|
88
|
+
return {
|
|
89
|
+
JSXElement(node) {
|
|
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
|
|
137
|
+
|
|
138
|
+
node.children.forEach(child => {
|
|
139
|
+
if (
|
|
140
|
+
child.type === "JSXText" &&
|
|
141
|
+
!isIgnoredText(child.value.trim(), ignored) &&
|
|
142
|
+
child.value.trim().length > 0
|
|
143
|
+
) {
|
|
144
|
+
report(child, child.value.trim())
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (
|
|
148
|
+
child.type === "JSXExpressionContainer" &&
|
|
149
|
+
child.expression.type === "Literal" &&
|
|
150
|
+
typeof child.expression.value === "string" &&
|
|
151
|
+
child.expression.value.trim().length > 0
|
|
152
|
+
) {
|
|
153
|
+
report(child, child.expression.value.trim())
|
|
154
|
+
}
|
|
155
|
+
})
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
})
|
|
160
|
+
}
|