@hanzo/cms-eslint-config 3.28.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/LICENSE.md +22 -0
- package/configs/react/index.mjs +50 -0
- package/configs/react/rules/react-a11y.mjs +246 -0
- package/configs/react/rules/react.mjs +18 -0
- package/deepMerge.js +58 -0
- package/index.mjs +262 -0
- package/package.json +45 -0
package/LICENSE.md
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2018-2025 Payload CMS, Inc. <info@payloadcms.com>
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
|
6
|
+
a copy of this software and associated documentation files (the
|
|
7
|
+
'Software'), to deal in the Software without restriction, including
|
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
|
11
|
+
the following conditions:
|
|
12
|
+
|
|
13
|
+
The above copyright notice and this permission notice shall be
|
|
14
|
+
included in all copies or substantial portions of the Software.
|
|
15
|
+
|
|
16
|
+
THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
|
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
|
19
|
+
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
|
20
|
+
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
|
21
|
+
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
|
22
|
+
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import reactRules from './rules/react.mjs'
|
|
2
|
+
import reactA11yRules from './rules/react-a11y.mjs'
|
|
3
|
+
import jsxA11y from 'eslint-plugin-jsx-a11y'
|
|
4
|
+
import react from '@eslint-react/eslint-plugin'
|
|
5
|
+
import reactHooks from 'eslint-plugin-react-hooks'
|
|
6
|
+
import globals from 'globals'
|
|
7
|
+
import { deepMerge } from '../../deepMerge.js'
|
|
8
|
+
|
|
9
|
+
/** @type {import('eslint').Linter.Config} */
|
|
10
|
+
export const index = deepMerge(
|
|
11
|
+
react.configs['recommended-type-checked'],
|
|
12
|
+
{
|
|
13
|
+
rules: {
|
|
14
|
+
...reactHooks.configs.recommended.rules,
|
|
15
|
+
'@eslint-react/hooks-extra/no-direct-set-state-in-use-effect': 'off',
|
|
16
|
+
'@eslint-react/naming-convention/use-state': 'off',
|
|
17
|
+
},
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
rules: reactRules,
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
rules: reactA11yRules,
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
languageOptions: {
|
|
27
|
+
globals: {
|
|
28
|
+
...globals.browser,
|
|
29
|
+
},
|
|
30
|
+
parserOptions: {
|
|
31
|
+
ecmaFeatures: {
|
|
32
|
+
jsx: true,
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
plugins: {
|
|
37
|
+
'jsx-a11y': jsxA11y,
|
|
38
|
+
'react-hooks': reactHooks,
|
|
39
|
+
},
|
|
40
|
+
settings: {
|
|
41
|
+
react: {
|
|
42
|
+
version: 'detect',
|
|
43
|
+
},
|
|
44
|
+
},
|
|
45
|
+
rules: {
|
|
46
|
+
'react-hooks/rules-of-hooks': 'error',
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
)
|
|
50
|
+
export default index
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
// Sourced from https://github.com/airbnb/javascript/blob/master/packages/eslint-config-airbnb/rules/react-a11y.js
|
|
2
|
+
|
|
3
|
+
/** @type {import('eslint').Linter.Config} */
|
|
4
|
+
export const index = {
|
|
5
|
+
// Enforce that anchors have content
|
|
6
|
+
// https://github.com/evcohen/eslint-plugin-jsx-a11y/blob/master/docs/rules/anchor-has-content.md
|
|
7
|
+
'jsx-a11y/anchor-has-content': ['error', { components: [] }],
|
|
8
|
+
|
|
9
|
+
// Require ARIA roles to be valid and non-abstract
|
|
10
|
+
// https://github.com/evcohen/eslint-plugin-jsx-a11y/blob/master/docs/rules/aria-role.md
|
|
11
|
+
'jsx-a11y/aria-role': ['error', { ignoreNonDom: false }],
|
|
12
|
+
|
|
13
|
+
// Enforce all aria-* props are valid.
|
|
14
|
+
// https://github.com/evcohen/eslint-plugin-jsx-a11y/blob/master/docs/rules/aria-props.md
|
|
15
|
+
'jsx-a11y/aria-props': 'error',
|
|
16
|
+
|
|
17
|
+
// Enforce ARIA state and property values are valid.
|
|
18
|
+
// https://github.com/evcohen/eslint-plugin-jsx-a11y/blob/master/docs/rules/aria-proptypes.md
|
|
19
|
+
'jsx-a11y/aria-proptypes': 'error',
|
|
20
|
+
|
|
21
|
+
// Enforce that elements that do not support ARIA roles, states, and
|
|
22
|
+
// properties do not have those attributes.
|
|
23
|
+
// https://github.com/evcohen/eslint-plugin-jsx-a11y/blob/master/docs/rules/aria-unsupported-elements.md
|
|
24
|
+
'jsx-a11y/aria-unsupported-elements': 'error',
|
|
25
|
+
|
|
26
|
+
// Enforce that all elements that require alternative text have meaningful information
|
|
27
|
+
// https://github.com/evcohen/eslint-plugin-jsx-a11y/blob/master/docs/rules/alt-text.md
|
|
28
|
+
'jsx-a11y/alt-text': [
|
|
29
|
+
'error',
|
|
30
|
+
{
|
|
31
|
+
elements: ['img', 'object', 'area', 'input[type="image"]'],
|
|
32
|
+
img: [],
|
|
33
|
+
object: [],
|
|
34
|
+
area: [],
|
|
35
|
+
'input[type="image"]': [],
|
|
36
|
+
},
|
|
37
|
+
],
|
|
38
|
+
|
|
39
|
+
// Prevent img alt text from containing redundant words like "image", "picture", or "photo"
|
|
40
|
+
// https://github.com/evcohen/eslint-plugin-jsx-a11y/blob/master/docs/rules/img-redundant-alt.md
|
|
41
|
+
'jsx-a11y/img-redundant-alt': 'error',
|
|
42
|
+
|
|
43
|
+
// require that JSX labels use "htmlFor"
|
|
44
|
+
// https://github.com/evcohen/eslint-plugin-jsx-a11y/blob/master/docs/rules/label-has-for.md
|
|
45
|
+
// deprecated: replaced by `label-has-associated-control` rule
|
|
46
|
+
'jsx-a11y/label-has-for': [
|
|
47
|
+
'off',
|
|
48
|
+
{
|
|
49
|
+
components: [],
|
|
50
|
+
required: {
|
|
51
|
+
every: ['nesting', 'id'],
|
|
52
|
+
},
|
|
53
|
+
allowChildren: false,
|
|
54
|
+
},
|
|
55
|
+
],
|
|
56
|
+
|
|
57
|
+
// Enforce that a label tag has a text label and an associated control.
|
|
58
|
+
// https://github.com/evcohen/eslint-plugin-jsx-a11y/blob/b800f40a2a69ad48015ae9226fbe879f946757ed/docs/rules/label-has-associated-control.md
|
|
59
|
+
'jsx-a11y/label-has-associated-control': [
|
|
60
|
+
'error',
|
|
61
|
+
{
|
|
62
|
+
labelComponents: [],
|
|
63
|
+
labelAttributes: [],
|
|
64
|
+
controlComponents: [],
|
|
65
|
+
assert: 'both',
|
|
66
|
+
depth: 25,
|
|
67
|
+
},
|
|
68
|
+
],
|
|
69
|
+
|
|
70
|
+
// Enforce that a control (an interactive element) has a text label.
|
|
71
|
+
// https://github.com/evcohen/eslint-plugin-jsx-a11y/blob/master/docs/rules/control-has-associated-label.md
|
|
72
|
+
'jsx-a11y/control-has-associated-label': [
|
|
73
|
+
'error',
|
|
74
|
+
{
|
|
75
|
+
labelAttributes: ['label'],
|
|
76
|
+
controlComponents: [],
|
|
77
|
+
ignoreElements: ['audio', 'canvas', 'embed', 'input', 'textarea', 'tr', 'video'],
|
|
78
|
+
ignoreRoles: [
|
|
79
|
+
'grid',
|
|
80
|
+
'listbox',
|
|
81
|
+
'menu',
|
|
82
|
+
'menubar',
|
|
83
|
+
'radiogroup',
|
|
84
|
+
'row',
|
|
85
|
+
'tablist',
|
|
86
|
+
'toolbar',
|
|
87
|
+
'tree',
|
|
88
|
+
'treegrid',
|
|
89
|
+
],
|
|
90
|
+
depth: 5,
|
|
91
|
+
},
|
|
92
|
+
],
|
|
93
|
+
|
|
94
|
+
// require that mouseover/out come with focus/blur, for keyboard-only users
|
|
95
|
+
// https://github.com/evcohen/eslint-plugin-jsx-a11y/blob/master/docs/rules/mouse-events-have-key-events.md
|
|
96
|
+
'jsx-a11y/mouse-events-have-key-events': 'error',
|
|
97
|
+
|
|
98
|
+
// Prevent use of `accessKey`
|
|
99
|
+
// https://github.com/evcohen/eslint-plugin-jsx-a11y/blob/master/docs/rules/no-access-key.md
|
|
100
|
+
'jsx-a11y/no-access-key': 'error',
|
|
101
|
+
|
|
102
|
+
// require onBlur instead of onChange
|
|
103
|
+
// https://github.com/evcohen/eslint-plugin-jsx-a11y/blob/master/docs/rules/no-onchange.md
|
|
104
|
+
'jsx-a11y/no-onchange': 'off',
|
|
105
|
+
|
|
106
|
+
// Elements with an interactive role and interaction handlers must be focusable
|
|
107
|
+
// https://github.com/evcohen/eslint-plugin-jsx-a11y/blob/master/docs/rules/interactive-supports-focus.md
|
|
108
|
+
'jsx-a11y/interactive-supports-focus': 'error',
|
|
109
|
+
|
|
110
|
+
// Enforce that elements with ARIA roles must have all required attributes
|
|
111
|
+
// for that role.
|
|
112
|
+
// https://github.com/evcohen/eslint-plugin-jsx-a11y/blob/master/docs/rules/role-has-required-aria-props.md
|
|
113
|
+
'jsx-a11y/role-has-required-aria-props': 'error',
|
|
114
|
+
|
|
115
|
+
// Enforce that elements with explicit or implicit roles defined contain
|
|
116
|
+
// only aria-* properties supported by that role.
|
|
117
|
+
// https://github.com/evcohen/eslint-plugin-jsx-a11y/blob/master/docs/rules/role-supports-aria-props.md
|
|
118
|
+
'jsx-a11y/role-supports-aria-props': 'error',
|
|
119
|
+
|
|
120
|
+
// Enforce tabIndex value is not greater than zero.
|
|
121
|
+
// https://github.com/evcohen/eslint-plugin-jsx-a11y/blob/master/docs/rules/tabindex-no-positive.md
|
|
122
|
+
'jsx-a11y/tabindex-no-positive': 'error',
|
|
123
|
+
|
|
124
|
+
// ensure <hX> tags have content and are not aria-hidden
|
|
125
|
+
// https://github.com/evcohen/eslint-plugin-jsx-a11y/blob/master/docs/rules/heading-has-content.md
|
|
126
|
+
'jsx-a11y/heading-has-content': ['error', { components: [''] }],
|
|
127
|
+
|
|
128
|
+
// require HTML elements to have a "lang" prop
|
|
129
|
+
// https://github.com/evcohen/eslint-plugin-jsx-a11y/blob/master/docs/rules/html-has-lang.md
|
|
130
|
+
'jsx-a11y/html-has-lang': 'error',
|
|
131
|
+
|
|
132
|
+
// require HTML element's lang prop to be valid
|
|
133
|
+
// https://github.com/evcohen/eslint-plugin-jsx-a11y/blob/master/docs/rules/lang.md
|
|
134
|
+
'jsx-a11y/lang': 'error',
|
|
135
|
+
|
|
136
|
+
// prevent distracting elements, like <marquee> and <blink>
|
|
137
|
+
// https://github.com/evcohen/eslint-plugin-jsx-a11y/blob/master/docs/rules/no-distracting-elements.md
|
|
138
|
+
'jsx-a11y/no-distracting-elements': [
|
|
139
|
+
'error',
|
|
140
|
+
{
|
|
141
|
+
elements: ['marquee', 'blink'],
|
|
142
|
+
},
|
|
143
|
+
],
|
|
144
|
+
|
|
145
|
+
// only allow <th> to have the "scope" attr
|
|
146
|
+
// https://github.com/evcohen/eslint-plugin-jsx-a11y/blob/master/docs/rules/scope.md
|
|
147
|
+
'jsx-a11y/scope': 'error',
|
|
148
|
+
|
|
149
|
+
// require onClick be accompanied by onKeyUp/onKeyDown/onKeyPress
|
|
150
|
+
// https://github.com/evcohen/eslint-plugin-jsx-a11y/blob/master/docs/rules/click-events-have-key-events.md
|
|
151
|
+
'jsx-a11y/click-events-have-key-events': 'error',
|
|
152
|
+
|
|
153
|
+
// Enforce that DOM elements without semantic behavior not have interaction handlers
|
|
154
|
+
// https://github.com/evcohen/eslint-plugin-jsx-a11y/blob/master/docs/rules/no-static-element-interactions.md
|
|
155
|
+
'jsx-a11y/no-static-element-interactions': [
|
|
156
|
+
'error',
|
|
157
|
+
{
|
|
158
|
+
handlers: ['onClick', 'onMouseDown', 'onMouseUp', 'onKeyPress', 'onKeyDown', 'onKeyUp'],
|
|
159
|
+
},
|
|
160
|
+
],
|
|
161
|
+
|
|
162
|
+
// A non-interactive element does not support event handlers (mouse and key handlers)
|
|
163
|
+
// https://github.com/evcohen/eslint-plugin-jsx-a11y/blob/master/docs/rules/no-noninteractive-element-interactions.md
|
|
164
|
+
'jsx-a11y/no-noninteractive-element-interactions': [
|
|
165
|
+
'error',
|
|
166
|
+
{
|
|
167
|
+
handlers: ['onClick', 'onMouseDown', 'onMouseUp', 'onKeyPress', 'onKeyDown', 'onKeyUp'],
|
|
168
|
+
},
|
|
169
|
+
],
|
|
170
|
+
|
|
171
|
+
// ensure emoji are accessible
|
|
172
|
+
// https://github.com/evcohen/eslint-plugin-jsx-a11y/blob/master/docs/rules/accessible-emoji.md
|
|
173
|
+
'jsx-a11y/accessible-emoji': 'error',
|
|
174
|
+
|
|
175
|
+
// elements with aria-activedescendant must be tabbable
|
|
176
|
+
// https://github.com/evcohen/eslint-plugin-jsx-a11y/blob/master/docs/rules/aria-activedescendant-has-tabindex.md
|
|
177
|
+
'jsx-a11y/aria-activedescendant-has-tabindex': 'error',
|
|
178
|
+
|
|
179
|
+
// ensure iframe elements have a unique title
|
|
180
|
+
// https://github.com/evcohen/eslint-plugin-jsx-a11y/blob/master/docs/rules/iframe-has-title.md
|
|
181
|
+
'jsx-a11y/iframe-has-title': 'error',
|
|
182
|
+
|
|
183
|
+
// prohibit autoFocus prop
|
|
184
|
+
// https://github.com/evcohen/eslint-plugin-jsx-a11y/blob/master/docs/rules/no-autofocus.md
|
|
185
|
+
'jsx-a11y/no-autofocus': ['error', { ignoreNonDOM: true }],
|
|
186
|
+
|
|
187
|
+
// ensure HTML elements do not specify redundant ARIA roles
|
|
188
|
+
// https://github.com/evcohen/eslint-plugin-jsx-a11y/blob/master/docs/rules/no-redundant-roles.md
|
|
189
|
+
'jsx-a11y/no-redundant-roles': 'error',
|
|
190
|
+
|
|
191
|
+
// media elements must have captions
|
|
192
|
+
// https://github.com/evcohen/eslint-plugin-jsx-a11y/blob/master/docs/rules/media-has-caption.md
|
|
193
|
+
'jsx-a11y/media-has-caption': [
|
|
194
|
+
'error',
|
|
195
|
+
{
|
|
196
|
+
audio: [],
|
|
197
|
+
video: [],
|
|
198
|
+
track: [],
|
|
199
|
+
},
|
|
200
|
+
],
|
|
201
|
+
|
|
202
|
+
// WAI-ARIA roles should not be used to convert an interactive element to non-interactive
|
|
203
|
+
// https://github.com/evcohen/eslint-plugin-jsx-a11y/blob/master/docs/rules/no-interactive-element-to-noninteractive-role.md
|
|
204
|
+
'jsx-a11y/no-interactive-element-to-noninteractive-role': [
|
|
205
|
+
'error',
|
|
206
|
+
{
|
|
207
|
+
tr: ['none', 'presentation'],
|
|
208
|
+
},
|
|
209
|
+
],
|
|
210
|
+
|
|
211
|
+
// WAI-ARIA roles should not be used to convert a non-interactive element to interactive
|
|
212
|
+
// https://github.com/evcohen/eslint-plugin-jsx-a11y/blob/master/docs/rules/no-noninteractive-element-to-interactive-role.md
|
|
213
|
+
'jsx-a11y/no-noninteractive-element-to-interactive-role': [
|
|
214
|
+
'error',
|
|
215
|
+
{
|
|
216
|
+
ul: ['listbox', 'menu', 'menubar', 'radiogroup', 'tablist', 'tree', 'treegrid'],
|
|
217
|
+
ol: ['listbox', 'menu', 'menubar', 'radiogroup', 'tablist', 'tree', 'treegrid'],
|
|
218
|
+
li: ['menuitem', 'option', 'row', 'tab', 'treeitem'],
|
|
219
|
+
table: ['grid'],
|
|
220
|
+
td: ['gridcell'],
|
|
221
|
+
},
|
|
222
|
+
],
|
|
223
|
+
|
|
224
|
+
// Tab key navigation should be limited to elements on the page that can be interacted with.
|
|
225
|
+
// https://github.com/evcohen/eslint-plugin-jsx-a11y/blob/master/docs/rules/no-noninteractive-tabindex.md
|
|
226
|
+
'jsx-a11y/no-noninteractive-tabindex': [
|
|
227
|
+
'error',
|
|
228
|
+
{
|
|
229
|
+
tags: [],
|
|
230
|
+
roles: ['tabpanel'],
|
|
231
|
+
},
|
|
232
|
+
],
|
|
233
|
+
|
|
234
|
+
// ensure <a> tags are valid
|
|
235
|
+
// https://github.com/evcohen/eslint-plugin-jsx-a11y/blob/0745af376cdc8686d85a361ce36952b1fb1ccf6e/docs/rules/anchor-is-valid.md
|
|
236
|
+
'jsx-a11y/anchor-is-valid': [
|
|
237
|
+
'error',
|
|
238
|
+
{
|
|
239
|
+
components: ['Link'],
|
|
240
|
+
specialLink: ['to'],
|
|
241
|
+
aspects: ['noHref', 'invalidHref', 'preferButton'],
|
|
242
|
+
},
|
|
243
|
+
],
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
export default index
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/** @type {import('eslint').Linter.Config} */
|
|
2
|
+
export const index = {
|
|
3
|
+
'@eslint-react/dom/no-dangerously-set-innerhtml': 'off',
|
|
4
|
+
'@eslint-react/dom/no-dangerously-set-innerhtml-with-children': 'off',
|
|
5
|
+
'@eslint-react/no-unsafe-component-will-mount': 'off',
|
|
6
|
+
'@eslint-react/no-unsafe-component-will-receive-props': 'off',
|
|
7
|
+
'@eslint-react/no-unsafe-component-will-update': 'off',
|
|
8
|
+
'@eslint-react/no-set-state-in-component-did-mount': 'off',
|
|
9
|
+
'@eslint-react/no-set-state-in-component-did-update': 'off',
|
|
10
|
+
'@eslint-react/no-set-state-in-component-will-update': 'off',
|
|
11
|
+
'@eslint-react/no-missing-component-display-name': 'off',
|
|
12
|
+
'@eslint-react/no-direct-mutation-state': 'off',
|
|
13
|
+
'@eslint-react/no-array-index-key': 'off',
|
|
14
|
+
'@eslint-react/no-unstable-default-props': 'off', // TODO: Evaluate enabling this
|
|
15
|
+
'@eslint-react/no-unstable-context-value': 'off', // TODO: Evaluate enabling this
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export default index
|
package/deepMerge.js
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* obj2 has priority over obj1
|
|
3
|
+
*
|
|
4
|
+
* Merges obj2 into obj1
|
|
5
|
+
*/
|
|
6
|
+
export function _deepMerge(obj1, obj2, doNotMergeInNulls = true) {
|
|
7
|
+
const output = { ...obj1 }
|
|
8
|
+
|
|
9
|
+
for (const key in obj2) {
|
|
10
|
+
if (Object.prototype.hasOwnProperty.call(obj2, key)) {
|
|
11
|
+
if (doNotMergeInNulls) {
|
|
12
|
+
if (
|
|
13
|
+
(obj2[key] === null || obj2[key] === undefined) &&
|
|
14
|
+
obj1[key] !== null &&
|
|
15
|
+
obj1[key] !== undefined
|
|
16
|
+
) {
|
|
17
|
+
continue
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Check if both are arrays
|
|
22
|
+
if (Array.isArray(obj1[key]) && Array.isArray(obj2[key])) {
|
|
23
|
+
// Merge each element in the arrays
|
|
24
|
+
|
|
25
|
+
// We need the Array.from, map rather than a normal map because this handles holes in arrays properly. A simple .map would skip holes.
|
|
26
|
+
output[key] = Array.from(obj2[key], (item, index) => {
|
|
27
|
+
if (doNotMergeInNulls) {
|
|
28
|
+
if (
|
|
29
|
+
(item === undefined || item === null) &&
|
|
30
|
+
obj1[key][index] !== null &&
|
|
31
|
+
obj1[key][index] !== undefined
|
|
32
|
+
) {
|
|
33
|
+
return obj1[key][index]
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (typeof item === 'object' && !Array.isArray(item) && obj1[key][index]) {
|
|
38
|
+
// Deep merge for objects in arrays
|
|
39
|
+
return deepMerge(obj1[key][index], item, doNotMergeInNulls)
|
|
40
|
+
}
|
|
41
|
+
return item
|
|
42
|
+
})
|
|
43
|
+
} else if (typeof obj2[key] === 'object' && !Array.isArray(obj2[key]) && obj1[key]) {
|
|
44
|
+
// Existing behavior for objects
|
|
45
|
+
output[key] = deepMerge(obj1[key], obj2[key], doNotMergeInNulls)
|
|
46
|
+
} else {
|
|
47
|
+
// Direct assignment for values
|
|
48
|
+
output[key] = obj2[key]
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return output
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function deepMerge(...objs) {
|
|
57
|
+
return objs.reduce((acc, obj) => _deepMerge(acc, obj), {})
|
|
58
|
+
}
|
package/index.mjs
ADDED
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
import js from '@eslint/js'
|
|
2
|
+
import tseslint from 'typescript-eslint'
|
|
3
|
+
import perfectionist from 'eslint-plugin-perfectionist'
|
|
4
|
+
import { configs as regexpPluginConfigs } from 'eslint-plugin-regexp'
|
|
5
|
+
import eslintConfigPrettier from 'eslint-config-prettier/flat'
|
|
6
|
+
import cmsPlugin from '@hanzo/cms-eslint-plugin'
|
|
7
|
+
import reactExtends from './configs/react/index.mjs'
|
|
8
|
+
import globals from 'globals'
|
|
9
|
+
import importX from 'eslint-plugin-import-x'
|
|
10
|
+
import typescriptParser from '@typescript-eslint/parser'
|
|
11
|
+
import { deepMerge } from './deepMerge.js'
|
|
12
|
+
import reactCompiler from 'eslint-plugin-react-compiler'
|
|
13
|
+
import vitest from '@vitest/eslint-plugin'
|
|
14
|
+
|
|
15
|
+
const baseRules = {
|
|
16
|
+
// This rule makes no sense when overriding class methods. This is used a lot in richtext-lexical.
|
|
17
|
+
'class-methods-use-this': 'off',
|
|
18
|
+
curly: ['warn', 'all'],
|
|
19
|
+
'arrow-body-style': 0,
|
|
20
|
+
'import-x/prefer-default-export': 'off',
|
|
21
|
+
'no-restricted-exports': ['warn', { restrictDefaultExports: { direct: true } }],
|
|
22
|
+
'no-console': 'warn',
|
|
23
|
+
'no-sparse-arrays': 'off',
|
|
24
|
+
'no-underscore-dangle': 'off',
|
|
25
|
+
'no-use-before-define': 'off',
|
|
26
|
+
'object-shorthand': 'warn',
|
|
27
|
+
'no-useless-escape': 'warn',
|
|
28
|
+
'import-x/no-duplicates': 'warn',
|
|
29
|
+
'perfectionist/sort-objects': [
|
|
30
|
+
'error',
|
|
31
|
+
{
|
|
32
|
+
type: 'natural',
|
|
33
|
+
order: 'asc',
|
|
34
|
+
partitionByComment: true,
|
|
35
|
+
partitionByNewLine: true,
|
|
36
|
+
groups: ['top', 'unknown'],
|
|
37
|
+
customGroups: {
|
|
38
|
+
top: ['_id', 'id', 'name', 'slug', 'type'],
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
],
|
|
42
|
+
/*'perfectionist/sort-object-types': [
|
|
43
|
+
'error',
|
|
44
|
+
{
|
|
45
|
+
partitionByNewLine: true,
|
|
46
|
+
},
|
|
47
|
+
],
|
|
48
|
+
'perfectionist/sort-interfaces': [
|
|
49
|
+
'error',
|
|
50
|
+
{
|
|
51
|
+
partitionByNewLine': true,
|
|
52
|
+
},
|
|
53
|
+
],*/
|
|
54
|
+
'cms/no-jsx-import-statements': 'error',
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const reactA11yRules = {
|
|
58
|
+
'jsx-a11y/anchor-is-valid': 'warn',
|
|
59
|
+
'jsx-a11y/control-has-associated-label': 'warn',
|
|
60
|
+
'jsx-a11y/no-static-element-interactions': 'warn',
|
|
61
|
+
'jsx-a11y/label-has-associated-control': 'warn',
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const typescriptRules = {
|
|
65
|
+
'@typescript-eslint/no-use-before-define': 'off',
|
|
66
|
+
|
|
67
|
+
// Type-aware any rules:
|
|
68
|
+
'@typescript-eslint/no-unsafe-assignment': 'off',
|
|
69
|
+
'@typescript-eslint/no-unsafe-member-access': 'off',
|
|
70
|
+
'@typescript-eslint/no-unsafe-call': 'off',
|
|
71
|
+
'@typescript-eslint/no-unsafe-argument': 'off',
|
|
72
|
+
'@typescript-eslint/no-unsafe-return': 'off',
|
|
73
|
+
'@typescript-eslint/unbound-method': 'warn',
|
|
74
|
+
'@typescript-eslint/consistent-type-imports': 'warn',
|
|
75
|
+
'@typescript-eslint/no-explicit-any': 'warn',
|
|
76
|
+
// Type-aware any rules end
|
|
77
|
+
|
|
78
|
+
// ts-expect preferred over ts-ignore. It will error if the expected error is no longer present.
|
|
79
|
+
'@typescript-eslint/ban-ts-comment': 'warn', // Recommended over deprecated @typescript-eslint/prefer-ts-expect-error: https://github.com/typescript-eslint/typescript-eslint/issues/8333. Set to warn to ease migration.
|
|
80
|
+
// By default, it errors for unused variables. This is annoying, warnings are enough.
|
|
81
|
+
'@typescript-eslint/no-unused-vars': [
|
|
82
|
+
'warn',
|
|
83
|
+
{
|
|
84
|
+
vars: 'all',
|
|
85
|
+
args: 'after-used',
|
|
86
|
+
ignoreRestSiblings: false,
|
|
87
|
+
argsIgnorePattern: '^_',
|
|
88
|
+
varsIgnorePattern: '^_',
|
|
89
|
+
destructuredArrayIgnorePattern: '^_',
|
|
90
|
+
caughtErrorsIgnorePattern: '^(_|ignore)',
|
|
91
|
+
},
|
|
92
|
+
],
|
|
93
|
+
'@typescript-eslint/no-base-to-string': 'warn',
|
|
94
|
+
'@typescript-eslint/restrict-template-expressions': 'warn',
|
|
95
|
+
'@typescript-eslint/no-redundant-type-constituents': 'warn',
|
|
96
|
+
'@typescript-eslint/no-unnecessary-type-constraint': 'warn',
|
|
97
|
+
'@typescript-eslint/no-misused-promises': [
|
|
98
|
+
'error',
|
|
99
|
+
{
|
|
100
|
+
// See https://github.com/typescript-eslint/typescript-eslint/issues/4619 and https://github.com/typescript-eslint/typescript-eslint/pull/4623
|
|
101
|
+
// Don't want something like <button onClick={someAsyncFunction}> to error
|
|
102
|
+
checksVoidReturn: {
|
|
103
|
+
attributes: false,
|
|
104
|
+
arguments: false,
|
|
105
|
+
},
|
|
106
|
+
},
|
|
107
|
+
],
|
|
108
|
+
'@typescript-eslint/no-empty-object-type': 'warn',
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** @typedef {import('eslint').Linter.Config} Config */
|
|
112
|
+
|
|
113
|
+
/** @type {FlatConfig} */
|
|
114
|
+
const baseExtends = deepMerge(
|
|
115
|
+
js.configs.recommended,
|
|
116
|
+
perfectionist.configs['recommended-natural'],
|
|
117
|
+
regexpPluginConfigs['flat/recommended'],
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
/** @type {Config[]} */
|
|
121
|
+
export const rootEslintConfig = [
|
|
122
|
+
{
|
|
123
|
+
name: 'Settings',
|
|
124
|
+
languageOptions: {
|
|
125
|
+
parserOptions: {
|
|
126
|
+
ecmaFeatures: {
|
|
127
|
+
jsx: true,
|
|
128
|
+
},
|
|
129
|
+
projectService: {
|
|
130
|
+
// This is necessary because `tsconfig.base.json` defines `"rootDir": "${configDir}/src"`,
|
|
131
|
+
// And the following files aren't in src because they aren't transpiled.
|
|
132
|
+
// This is typescript-eslint's way of adding files that aren't included in tsconfig.
|
|
133
|
+
// See: https://typescript-eslint.io/troubleshooting/typed-linting/#i-get-errors-telling-me--was-not-found-by-the-project-service-consider-either-including-it-in-the-tsconfigjson-or-including-it-in-allowdefaultproject
|
|
134
|
+
// The best practice is to have a tsconfig.json that covers ALL files and is used for
|
|
135
|
+
// typechecking (with noEmit), and a `tsconfig.build.json` that is used for the build
|
|
136
|
+
// (or alternatively, swc, tsup or tsdown). That's what we should ideally do, in which case
|
|
137
|
+
// this hardcoded list wouldn't be necessary. Note that these files don't currently go
|
|
138
|
+
// through ts, only through eslint.
|
|
139
|
+
allowDefaultProject: [
|
|
140
|
+
'../payload/bin.js',
|
|
141
|
+
'../payload/bundle.js',
|
|
142
|
+
'../next/babel.config.cjs',
|
|
143
|
+
'../next/bundleScss.js',
|
|
144
|
+
'../ui/babel.config.cjs',
|
|
145
|
+
'../ui/bundle.js',
|
|
146
|
+
'../graphql/bin.js',
|
|
147
|
+
'../richtext-lexical/babel.config.cjs',
|
|
148
|
+
'../richtext-lexical/bundle.js',
|
|
149
|
+
'../richtext-lexical/scripts/translateNewKeys.ts',
|
|
150
|
+
'../db-postgres/bundle.js',
|
|
151
|
+
'../db-postgres/relationships-v2-v3.mjs',
|
|
152
|
+
'../db-postgres/scripts/renamePredefinedMigrations.ts',
|
|
153
|
+
'../db-sqlite/bundle.js',
|
|
154
|
+
'../db-d1-sqlite/bundle.js',
|
|
155
|
+
'../db-vercel-postgres/relationships-v2-v3.mjs',
|
|
156
|
+
'../db-vercel-postgres/scripts/renamePredefinedMigrations.ts',
|
|
157
|
+
'../plugin-cloud-storage/azure.d.ts',
|
|
158
|
+
'../plugin-cloud-storage/azure.js',
|
|
159
|
+
'../plugin-cloud-storage/gcs.d.ts',
|
|
160
|
+
'../plugin-cloud-storage/gcs.js',
|
|
161
|
+
'../plugin-cloud-storage/s3.d.ts',
|
|
162
|
+
'../plugin-cloud-storage/s3.js',
|
|
163
|
+
'../plugin-redirects/types.d.ts',
|
|
164
|
+
'../plugin-redirects/types.js',
|
|
165
|
+
'../translations/scripts/translateNewKeys/applyEslintFixes.ts',
|
|
166
|
+
'../translations/scripts/translateNewKeys/findMissingKeys.ts',
|
|
167
|
+
'../translations/scripts/translateNewKeys/generateTsObjectLiteral.ts',
|
|
168
|
+
'../translations/scripts/translateNewKeys/index.ts',
|
|
169
|
+
'../translations/scripts/translateNewKeys/run.ts',
|
|
170
|
+
'../translations/scripts/translateNewKeys/sortKeys.ts',
|
|
171
|
+
'../translations/scripts/translateNewKeys/translateText.ts',
|
|
172
|
+
'../@hanzo/create-cms-app/bin/cli.js',
|
|
173
|
+
],
|
|
174
|
+
},
|
|
175
|
+
tsconfigRootDir: import.meta.dirname,
|
|
176
|
+
},
|
|
177
|
+
ecmaVersion: 'latest',
|
|
178
|
+
sourceType: 'module',
|
|
179
|
+
globals: {
|
|
180
|
+
...globals.node,
|
|
181
|
+
},
|
|
182
|
+
parser: typescriptParser,
|
|
183
|
+
},
|
|
184
|
+
linterOptions: {
|
|
185
|
+
reportUnusedDisableDirectives: 'error',
|
|
186
|
+
},
|
|
187
|
+
plugins: {
|
|
188
|
+
'import-x': importX,
|
|
189
|
+
},
|
|
190
|
+
},
|
|
191
|
+
{
|
|
192
|
+
name: 'TypeScript',
|
|
193
|
+
// has 3 entries: https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/typescript-eslint/src/configs/recommended-type-checked.ts
|
|
194
|
+
...deepMerge(
|
|
195
|
+
baseExtends,
|
|
196
|
+
tseslint.configs.recommendedTypeChecked[0],
|
|
197
|
+
tseslint.configs.recommendedTypeChecked[1],
|
|
198
|
+
tseslint.configs.recommendedTypeChecked[2],
|
|
199
|
+
eslintConfigPrettier,
|
|
200
|
+
{
|
|
201
|
+
plugins: {
|
|
202
|
+
cms: cmsPlugin,
|
|
203
|
+
},
|
|
204
|
+
rules: {
|
|
205
|
+
...baseRules,
|
|
206
|
+
...typescriptRules,
|
|
207
|
+
},
|
|
208
|
+
},
|
|
209
|
+
),
|
|
210
|
+
files: ['**/*.ts'],
|
|
211
|
+
},
|
|
212
|
+
{
|
|
213
|
+
name: 'TypeScript-React',
|
|
214
|
+
...deepMerge(
|
|
215
|
+
baseExtends,
|
|
216
|
+
tseslint.configs.recommendedTypeChecked[0],
|
|
217
|
+
tseslint.configs.recommendedTypeChecked[1],
|
|
218
|
+
tseslint.configs.recommendedTypeChecked[2],
|
|
219
|
+
reactExtends,
|
|
220
|
+
eslintConfigPrettier,
|
|
221
|
+
{
|
|
222
|
+
plugins: {
|
|
223
|
+
cms: cmsPlugin,
|
|
224
|
+
},
|
|
225
|
+
rules: {
|
|
226
|
+
...baseRules,
|
|
227
|
+
...typescriptRules,
|
|
228
|
+
...reactA11yRules,
|
|
229
|
+
},
|
|
230
|
+
},
|
|
231
|
+
),
|
|
232
|
+
files: ['**/*.tsx'],
|
|
233
|
+
},
|
|
234
|
+
{
|
|
235
|
+
name: 'Unit and Integration Tests',
|
|
236
|
+
plugins: {
|
|
237
|
+
vitest,
|
|
238
|
+
},
|
|
239
|
+
rules: {
|
|
240
|
+
...vitest.configs.recommended.rules,
|
|
241
|
+
},
|
|
242
|
+
files: ['**/*.spec.ts'],
|
|
243
|
+
ignores: ['**/*.e2e.spec.ts'],
|
|
244
|
+
},
|
|
245
|
+
{
|
|
246
|
+
name: 'CMS Config',
|
|
247
|
+
plugins: {
|
|
248
|
+
cms: cmsPlugin,
|
|
249
|
+
},
|
|
250
|
+
rules: {
|
|
251
|
+
'no-restricted-exports': 'off',
|
|
252
|
+
},
|
|
253
|
+
files: ['*.config.ts', 'config.ts'],
|
|
254
|
+
},
|
|
255
|
+
{
|
|
256
|
+
name: 'React Compiler',
|
|
257
|
+
...reactCompiler.configs.recommended,
|
|
258
|
+
files: ['**/*.tsx'],
|
|
259
|
+
},
|
|
260
|
+
]
|
|
261
|
+
|
|
262
|
+
export default rootEslintConfig
|
package/package.json
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@hanzo/cms-eslint-config",
|
|
3
|
+
"version": "3.28.0",
|
|
4
|
+
"description": "CMS styles for ESLint and Prettier",
|
|
5
|
+
"keywords": [],
|
|
6
|
+
"homepage": "https://payloadcms.com",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "https://github.com/payloadcms/payload.git",
|
|
10
|
+
"directory": "packages/eslint-config"
|
|
11
|
+
},
|
|
12
|
+
"license": "MIT",
|
|
13
|
+
"author": "Payload <dev@payloadcms.com> (https://payloadcms.com)",
|
|
14
|
+
"maintainers": [
|
|
15
|
+
{
|
|
16
|
+
"name": "Payload",
|
|
17
|
+
"email": "info@payloadcms.com",
|
|
18
|
+
"url": "https://payloadcms.com"
|
|
19
|
+
}
|
|
20
|
+
],
|
|
21
|
+
"type": "module",
|
|
22
|
+
"main": "index.mjs",
|
|
23
|
+
"dependencies": {
|
|
24
|
+
"@eslint-react/eslint-plugin": "1.53.1",
|
|
25
|
+
"@eslint/js": "9.39.2",
|
|
26
|
+
"@types/eslint": "9.6.1",
|
|
27
|
+
"@typescript-eslint/parser": "8.26.1",
|
|
28
|
+
"@vitest/eslint-plugin": "1.5.4",
|
|
29
|
+
"eslint": "9.39.2",
|
|
30
|
+
"eslint-config-prettier": "10.1.1",
|
|
31
|
+
"eslint-plugin-import-x": "4.6.1",
|
|
32
|
+
"eslint-plugin-jsx-a11y": "6.10.2",
|
|
33
|
+
"eslint-plugin-perfectionist": "3.9.1",
|
|
34
|
+
"eslint-plugin-react-compiler": "19.1.0-rc.2",
|
|
35
|
+
"eslint-plugin-react-hooks": "0.0.0-experimental-d331ba04-20250307",
|
|
36
|
+
"eslint-plugin-regexp": "2.7.0",
|
|
37
|
+
"globals": "16.0.0",
|
|
38
|
+
"typescript": "5.7.3",
|
|
39
|
+
"typescript-eslint": "8.26.1",
|
|
40
|
+
"@hanzo/cms-eslint-plugin": "3.28.0"
|
|
41
|
+
},
|
|
42
|
+
"scripts": {
|
|
43
|
+
"test": "echo \"Error: no test specified\" && exit 1"
|
|
44
|
+
}
|
|
45
|
+
}
|