@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 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
+ }