@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 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: 160, skipBlankLines: true, skipComments: false },
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.18.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
- export const rules = {
10
- "no-lone-text-node": createRule<[], MessageIds>({
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: "Wrap text '{{text}}' in an element (e.g. <span>) to avoid Google Translate crashes."
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
- if (isAllowedWrapper(node)) return
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())