@commercetools-backend/eslint-config-node 26.0.2 → 27.0.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/CHANGELOG.md +56 -0
- package/README.md +4 -0
- package/index.js +228 -186
- package/migrations/v27.md +328 -0
- package/package.json +7 -7
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,61 @@
|
|
|
1
1
|
# @commercetools-backend/eslint-config-node
|
|
2
2
|
|
|
3
|
+
## 27.0.0
|
|
4
|
+
|
|
5
|
+
### Major Changes
|
|
6
|
+
|
|
7
|
+
- [#3936](https://github.com/commercetools/merchant-center-application-kit/pull/3936) [`d890dce`](https://github.com/commercetools/merchant-center-application-kit/commit/d890dce89affdce28220fd3627a1b31244c26640) Thanks [@valoriecarli](https://github.com/valoriecarli)! - Migrate to ESLint 9 flat config format.
|
|
8
|
+
|
|
9
|
+
**Peer dependency:** `eslint@^9.0.0` is now required.
|
|
10
|
+
|
|
11
|
+
## How to update
|
|
12
|
+
|
|
13
|
+
Both packages ship a `migrations/v27.md` with full step-by-step instructions, structured for both human and AI-assisted migration:
|
|
14
|
+
|
|
15
|
+
```
|
|
16
|
+
node_modules/@commercetools-frontend/eslint-config-mc-app/migrations/v27.md
|
|
17
|
+
node_modules/@commercetools-backend/eslint-config-node/migrations/v27.md
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
> "Migrate my eslint config following `node_modules/@commercetools-frontend/eslint-config-mc-app/migrations/v27.md`"
|
|
21
|
+
|
|
22
|
+
Quick summary:
|
|
23
|
+
|
|
24
|
+
1. Upgrade ESLint to v9: `eslint@^9.0.0`
|
|
25
|
+
2. Replace `.eslintrc.js` with `eslint.config.js`:
|
|
26
|
+
|
|
27
|
+
```js
|
|
28
|
+
const mcAppConfig = require('@commercetools-frontend/eslint-config-mc-app');
|
|
29
|
+
|
|
30
|
+
module.exports = [
|
|
31
|
+
...mcAppConfig,
|
|
32
|
+
// your overrides here
|
|
33
|
+
];
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
3. Delete `.eslintignore` and inline ignore patterns as `{ ignores: ['dist/', 'build/'] }` in `eslint.config.js`
|
|
37
|
+
4. Remove `@rushstack/eslint-patch` if present
|
|
38
|
+
|
|
39
|
+
## What changed
|
|
40
|
+
|
|
41
|
+
Both packages now export a flat config array instead of a legacy `.eslintrc` object:
|
|
42
|
+
|
|
43
|
+
- Config is now an array of objects, each targeting its own file patterns (replaces `overrides`)
|
|
44
|
+
- Plugins must be imported as objects, not referenced as strings
|
|
45
|
+
- Parsers moved into `languageOptions.parser`
|
|
46
|
+
- `env` replaced with explicit globals via the `globals` package
|
|
47
|
+
- `extends` removed — plugin rules are configured directly
|
|
48
|
+
- `@rushstack/eslint-patch` removed — no longer needed in ESLint 9
|
|
49
|
+
- `react-hooks` rules now explicitly applied to `**/*.ts` files (custom hooks without JSX)
|
|
50
|
+
|
|
51
|
+
Dependency upgrades: `@typescript-eslint/*` v5→v8, `eslint-plugin-jest` v27→v28, `eslint-plugin-react-hooks` v4→v5, `eslint-plugin-testing-library` v5→v7.
|
|
52
|
+
|
|
53
|
+
## Why
|
|
54
|
+
|
|
55
|
+
ESLint 9 drops support for the legacy `.eslintrc` format. The flat config system provides explicit, predictable scoping — plugins and parsers apply only to the file patterns they are registered for, eliminating the silent global leaking behavior of ESLint 8 overrides.
|
|
56
|
+
|
|
57
|
+
## 26.1.0
|
|
58
|
+
|
|
3
59
|
## 26.0.2
|
|
4
60
|
|
|
5
61
|
## 26.0.1
|
package/README.md
CHANGED
package/index.js
CHANGED
|
@@ -1,132 +1,141 @@
|
|
|
1
|
-
|
|
2
|
-
require('
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
3
|
|
|
4
|
-
const
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* @type {import("eslint").Linter.Config}
|
|
8
|
-
*/
|
|
9
|
-
module.exports = {
|
|
10
|
-
root: true,
|
|
11
|
-
|
|
12
|
-
parser: '@babel/eslint-parser',
|
|
4
|
+
const migrationGuide =
|
|
5
|
+
'node_modules/@commercetools-frontend/eslint-config-node/migrations/v27.md';
|
|
13
6
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
7
|
+
// Detect if loaded from a legacy .eslintrc file (the array export won't work there)
|
|
8
|
+
const caller = module.parent?.filename || '';
|
|
9
|
+
if (/\.eslintrc/.test(caller)) {
|
|
10
|
+
console.error(
|
|
11
|
+
'\n\u274c @commercetools-frontend/eslint-config-node v27 exports a flat config array.\n' +
|
|
12
|
+
' It cannot be used in .eslintrc files. Migrate to eslint.config.js.\n' +
|
|
13
|
+
' Guide: ' +
|
|
14
|
+
migrationGuide +
|
|
15
|
+
'\n'
|
|
16
|
+
);
|
|
17
|
+
}
|
|
24
18
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
19
|
+
// Detect leftover legacy config files and warn
|
|
20
|
+
const projectRoot = process.cwd();
|
|
21
|
+
const legacyPatterns = [
|
|
22
|
+
'.eslintrc',
|
|
23
|
+
'.eslintrc.js',
|
|
24
|
+
'.eslintrc.cjs',
|
|
25
|
+
'.eslintrc.json',
|
|
26
|
+
'.eslintrc.yml',
|
|
27
|
+
'.eslintrc.yaml',
|
|
28
|
+
];
|
|
29
|
+
const foundLegacy = legacyPatterns.filter((name) =>
|
|
30
|
+
fs.existsSync(path.join(projectRoot, name))
|
|
31
|
+
);
|
|
32
|
+
if (foundLegacy.length > 0) {
|
|
33
|
+
console.warn(
|
|
34
|
+
'\n\u26a0\ufe0f @commercetools-frontend/eslint-config-node v27 uses ESLint 9 flat config.\n' +
|
|
35
|
+
` Found legacy config file(s): ${foundLegacy.join(', ')}\n` +
|
|
36
|
+
' These files are ignored by ESLint 9 and your subdirectory rules will not be applied.\n' +
|
|
37
|
+
' Guide: ' +
|
|
38
|
+
migrationGuide +
|
|
39
|
+
'\n'
|
|
40
|
+
);
|
|
41
|
+
}
|
|
32
42
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
// NOTE: this should go last.
|
|
43
|
-
'prettier',
|
|
44
|
-
],
|
|
43
|
+
const babelParser = require('@babel/eslint-parser');
|
|
44
|
+
const typescriptPlugin = require('@typescript-eslint/eslint-plugin');
|
|
45
|
+
const typescriptParser = require('@typescript-eslint/parser');
|
|
46
|
+
const prettierConfig = require('eslint-config-prettier');
|
|
47
|
+
const importPlugin = require('eslint-plugin-import');
|
|
48
|
+
const jestPlugin = require('eslint-plugin-jest');
|
|
49
|
+
const nPlugin = require('eslint-plugin-n');
|
|
50
|
+
const prettierPlugin = require('eslint-plugin-prettier');
|
|
51
|
+
const globals = require('globals');
|
|
45
52
|
|
|
46
|
-
|
|
47
|
-
// https://github.com/import-js/eslint-plugin-import
|
|
48
|
-
'import',
|
|
49
|
-
// https://github.com/jest-community/eslint-plugin-jest
|
|
50
|
-
'jest',
|
|
51
|
-
// https://github.com/prettier/prettier-eslint
|
|
52
|
-
'prettier',
|
|
53
|
-
],
|
|
53
|
+
const { statusCode, allSupportedExtensions } = require('./helpers/eslint');
|
|
54
54
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
55
|
+
/**
|
|
56
|
+
* @type {import("eslint").Linter.Config[]}
|
|
57
|
+
*/
|
|
58
|
+
module.exports = [
|
|
59
|
+
// Base configuration for all JS/TS files
|
|
60
|
+
{
|
|
61
|
+
files: ['**/*.{js,mjs,cjs,ts}'],
|
|
62
|
+
languageOptions: {
|
|
63
|
+
parser: babelParser,
|
|
64
|
+
parserOptions: {
|
|
65
|
+
sourceType: 'module',
|
|
66
|
+
requireConfigFile: false,
|
|
67
|
+
/**
|
|
68
|
+
* @type {import('@babel/core').TransformOptions}
|
|
69
|
+
*/
|
|
70
|
+
babelOptions: {
|
|
71
|
+
presets: [['@babel/preset-env', { targets: { node: 'current' } }]],
|
|
72
|
+
},
|
|
73
|
+
},
|
|
74
|
+
globals: {
|
|
75
|
+
...globals.commonjs,
|
|
76
|
+
...globals.es2015,
|
|
77
|
+
...globals.node,
|
|
78
|
+
...globals.jest,
|
|
59
79
|
},
|
|
60
80
|
},
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
81
|
+
plugins: {
|
|
82
|
+
import: importPlugin,
|
|
83
|
+
jest: jestPlugin,
|
|
84
|
+
n: nPlugin,
|
|
85
|
+
prettier: prettierPlugin,
|
|
86
|
+
},
|
|
87
|
+
settings: {
|
|
88
|
+
'import/resolver': {
|
|
89
|
+
node: {
|
|
90
|
+
extensions: allSupportedExtensions,
|
|
91
|
+
},
|
|
92
|
+
},
|
|
93
|
+
},
|
|
94
|
+
rules: {
|
|
95
|
+
// Nodejs
|
|
96
|
+
'n/no-missing-import': statusCode.off,
|
|
97
|
+
'n/no-unsupported-features/es-syntax': statusCode.off,
|
|
67
98
|
|
|
68
|
-
|
|
69
|
-
|
|
99
|
+
// NOTE: The regular rule does not support do-expressions. The equivalent rule of babel does.
|
|
100
|
+
'no-unused-expressions': statusCode.off,
|
|
70
101
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
102
|
+
// Imports
|
|
103
|
+
'import/extensions': [
|
|
104
|
+
statusCode.error,
|
|
105
|
+
{
|
|
106
|
+
js: 'never',
|
|
107
|
+
jsx: 'never',
|
|
108
|
+
ts: 'never',
|
|
109
|
+
tsx: 'never',
|
|
110
|
+
mjs: 'never',
|
|
111
|
+
json: 'always',
|
|
112
|
+
svg: 'always',
|
|
113
|
+
graphql: 'always',
|
|
114
|
+
},
|
|
115
|
+
],
|
|
116
|
+
'import/default': statusCode.off,
|
|
117
|
+
'import/first': statusCode.error,
|
|
118
|
+
// TODO: enable this once there is support for `import type`
|
|
119
|
+
// 'import/order': statusCode.error,
|
|
120
|
+
'import/named': statusCode.off,
|
|
121
|
+
'import/namespace': statusCode.off,
|
|
122
|
+
'import/no-extraneous-dependencies': statusCode.off,
|
|
123
|
+
'import/no-named-as-default': statusCode.off,
|
|
124
|
+
'import/no-named-as-default-member': statusCode.off,
|
|
125
|
+
'import/no-unresolved': statusCode.error,
|
|
95
126
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
127
|
+
// Jest
|
|
128
|
+
'jest/expect-expect': statusCode.off,
|
|
129
|
+
'jest/no-identical-title': statusCode.warn,
|
|
130
|
+
'jest/no-focused-tests': statusCode.error,
|
|
131
|
+
},
|
|
100
132
|
},
|
|
101
133
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
},
|
|
108
|
-
rules: {
|
|
109
|
-
'n/no-extraneous-require': [
|
|
110
|
-
statusCode.error,
|
|
111
|
-
{
|
|
112
|
-
allowModules: ['jest-each', 'msw'],
|
|
113
|
-
},
|
|
114
|
-
],
|
|
115
|
-
// https://github.com/jest-community/eslint-plugin-jest
|
|
116
|
-
'jest/no-conditional-expect': statusCode.error,
|
|
117
|
-
'jest/no-identical-title': statusCode.error,
|
|
118
|
-
'jest/no-interpolation-in-snapshots': statusCode.error,
|
|
119
|
-
'jest/no-jasmine-globals': statusCode.error,
|
|
120
|
-
'jest/no-mocks-import': statusCode.error,
|
|
121
|
-
'jest/valid-describe-callback': statusCode.error,
|
|
122
|
-
'jest/valid-expect': statusCode.error,
|
|
123
|
-
'jest/valid-expect-in-promise': statusCode.error,
|
|
124
|
-
'jest/valid-title': statusCode.warn,
|
|
125
|
-
},
|
|
126
|
-
},
|
|
127
|
-
{
|
|
128
|
-
files: ['**/*.ts?(x)'],
|
|
129
|
-
parser: '@typescript-eslint/parser',
|
|
134
|
+
// TypeScript-specific configuration
|
|
135
|
+
{
|
|
136
|
+
files: ['**/*.ts'],
|
|
137
|
+
languageOptions: {
|
|
138
|
+
parser: typescriptParser,
|
|
130
139
|
parserOptions: {
|
|
131
140
|
ecmaVersion: 2022,
|
|
132
141
|
sourceType: 'module',
|
|
@@ -142,79 +151,112 @@ module.exports = {
|
|
|
142
151
|
],
|
|
143
152
|
},
|
|
144
153
|
},
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
154
|
+
},
|
|
155
|
+
plugins: {
|
|
156
|
+
'@typescript-eslint': typescriptPlugin,
|
|
157
|
+
},
|
|
158
|
+
rules: {
|
|
159
|
+
// TypeScript's `noFallthroughCasesInSwitch` option is more robust (#6906)
|
|
160
|
+
'default-case': statusCode.off,
|
|
161
|
+
// 'tsc' already handles this (https://github.com/typescript-eslint/typescript-eslint/issues/291)
|
|
162
|
+
'no-dupe-class-members': statusCode.off,
|
|
163
|
+
// 'tsc' already handles this (https://github.com/typescript-eslint/typescript-eslint/issues/477)
|
|
164
|
+
'no-undef': statusCode.off,
|
|
153
165
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
166
|
+
// Add TypeScript specific rules (and turn off ESLint equivalents)
|
|
167
|
+
'@typescript-eslint/consistent-type-assertions': statusCode.warn,
|
|
168
|
+
'no-array-constructor': statusCode.off,
|
|
169
|
+
'@typescript-eslint/no-array-constructor': statusCode.warn,
|
|
170
|
+
'no-redeclare': statusCode.off,
|
|
171
|
+
'@typescript-eslint/no-redeclare': statusCode.warn,
|
|
172
|
+
'no-use-before-define': statusCode.off,
|
|
173
|
+
'@typescript-eslint/no-use-before-define': [
|
|
174
|
+
statusCode.error,
|
|
175
|
+
{
|
|
176
|
+
functions: false,
|
|
177
|
+
classes: false,
|
|
178
|
+
variables: false,
|
|
179
|
+
typedefs: false,
|
|
180
|
+
},
|
|
181
|
+
],
|
|
182
|
+
'no-unused-expressions': statusCode.off,
|
|
183
|
+
'@typescript-eslint/no-unused-expressions': [
|
|
184
|
+
statusCode.error,
|
|
185
|
+
{
|
|
186
|
+
allowShortCircuit: true,
|
|
187
|
+
allowTernary: true,
|
|
188
|
+
allowTaggedTemplates: true,
|
|
189
|
+
},
|
|
190
|
+
],
|
|
191
|
+
'no-unused-vars': statusCode.off,
|
|
192
|
+
'@typescript-eslint/no-unused-vars': [
|
|
193
|
+
statusCode.warn,
|
|
194
|
+
{
|
|
195
|
+
args: 'none',
|
|
196
|
+
ignoreRestSiblings: true,
|
|
197
|
+
},
|
|
198
|
+
],
|
|
199
|
+
'no-useless-constructor': statusCode.off,
|
|
200
|
+
'@typescript-eslint/no-useless-constructor': statusCode.warn,
|
|
189
201
|
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
202
|
+
// TypeScript
|
|
203
|
+
'@typescript-eslint/ban-types': statusCode.off,
|
|
204
|
+
'@typescript-eslint/naming-convention': statusCode.off,
|
|
205
|
+
'@typescript-eslint/consistent-type-definitions': statusCode.off,
|
|
206
|
+
'@typescript-eslint/no-explicit-any': statusCode.error,
|
|
207
|
+
'@typescript-eslint/no-var-requires': statusCode.off,
|
|
208
|
+
'@typescript-eslint/unbound-method': statusCode.off,
|
|
209
|
+
'@typescript-eslint/ban-ts-comment': statusCode.off,
|
|
210
|
+
'@typescript-eslint/explicit-function-return-type': statusCode.off,
|
|
211
|
+
'@typescript-eslint/explicit-member-accessibility': [
|
|
212
|
+
statusCode.error,
|
|
213
|
+
{ accessibility: 'no-public' },
|
|
214
|
+
],
|
|
215
|
+
'@typescript-eslint/no-require-imports': statusCode.off,
|
|
216
|
+
'@typescript-eslint/promise-function-async': statusCode.off,
|
|
217
|
+
},
|
|
218
|
+
settings: {
|
|
219
|
+
'import/parsers': {
|
|
220
|
+
'@typescript-eslint/parser': allSupportedExtensions,
|
|
205
221
|
},
|
|
206
|
-
|
|
207
|
-
'import
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
'eslint-import-resolver-typescript': true,
|
|
212
|
-
typescript: {},
|
|
213
|
-
node: {
|
|
214
|
-
extensions: allSupportedExtensions,
|
|
215
|
-
},
|
|
222
|
+
'import/resolver': {
|
|
223
|
+
'eslint-import-resolver-typescript': true,
|
|
224
|
+
typescript: {},
|
|
225
|
+
node: {
|
|
226
|
+
extensions: allSupportedExtensions,
|
|
216
227
|
},
|
|
217
228
|
},
|
|
218
229
|
},
|
|
219
|
-
|
|
220
|
-
|
|
230
|
+
},
|
|
231
|
+
|
|
232
|
+
// Test files configuration
|
|
233
|
+
{
|
|
234
|
+
files: ['*.{spec,test}.*'],
|
|
235
|
+
languageOptions: {
|
|
236
|
+
globals: {
|
|
237
|
+
...globals.jest,
|
|
238
|
+
},
|
|
239
|
+
},
|
|
240
|
+
rules: {
|
|
241
|
+
'n/no-extraneous-require': [
|
|
242
|
+
statusCode.error,
|
|
243
|
+
{
|
|
244
|
+
allowModules: ['jest-each', 'msw'],
|
|
245
|
+
},
|
|
246
|
+
],
|
|
247
|
+
// https://github.com/jest-community/eslint-plugin-jest
|
|
248
|
+
'jest/no-conditional-expect': statusCode.error,
|
|
249
|
+
'jest/no-identical-title': statusCode.error,
|
|
250
|
+
'jest/no-interpolation-in-snapshots': statusCode.error,
|
|
251
|
+
'jest/no-jasmine-globals': statusCode.error,
|
|
252
|
+
'jest/no-mocks-import': statusCode.error,
|
|
253
|
+
'jest/valid-describe-callback': statusCode.error,
|
|
254
|
+
'jest/valid-expect': statusCode.error,
|
|
255
|
+
'jest/valid-expect-in-promise': statusCode.error,
|
|
256
|
+
'jest/valid-title': statusCode.warn,
|
|
257
|
+
},
|
|
258
|
+
},
|
|
259
|
+
|
|
260
|
+
// Prettier configuration (must be last)
|
|
261
|
+
prettierConfig,
|
|
262
|
+
];
|
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
# Migrating to ESLint 9 Flat Config
|
|
2
|
+
|
|
3
|
+
This guide covers migrating from ESLint 8 (legacy `.eslintrc` format) to ESLint 9 (flat config) for projects using `@commercetools-frontend/eslint-config-mc-app`.
|
|
4
|
+
|
|
5
|
+
> **AI agents**: This document is structured as step-by-step instructions that can be followed automatically. Read the existing config files before generating new ones.
|
|
6
|
+
|
|
7
|
+
## Key concept: no more config cascading
|
|
8
|
+
|
|
9
|
+
In legacy ESLint, `.eslintrc.*` files in subdirectories automatically merged with parent configs. **Flat config does not cascade.** ESLint 9 reads only the root `eslint.config.js`. Any `.eslintrc.*` files in subdirectories are silently ignored.
|
|
10
|
+
|
|
11
|
+
This means every subdirectory `.eslintrc.*` must be explicitly migrated into the root config (or into files imported by the root config). Missing this is the most common migration mistake — your subdirectory rules will silently stop being enforced.
|
|
12
|
+
|
|
13
|
+
## Step 1: Discover and analyze existing config
|
|
14
|
+
|
|
15
|
+
Find **all** ESLint-related files in the project:
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
find . -name '.eslintrc*' -not -path '*/node_modules/*'
|
|
19
|
+
find . -name '.eslintignore' -not -path '*/node_modules/*'
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
For **each** `.eslintrc.*` file found (root and subdirectories), extract: `extends`, `plugins`, `rules`, `overrides` (with their `files`, `excludedFiles`, `parser`, `parserOptions`, `plugins`, `rules`), `env`, `globals`, `settings`, and any `process.env` assignments before `module.exports`. Also collect `.eslintignore` patterns and note `"type": "module"` fields in `package.json`.
|
|
23
|
+
|
|
24
|
+
**Legacy cascade behavior**: A child config completely replaces the parent's value for the same rule key (e.g., if root sets `no-restricted-imports` with `paths` and a subdirectory sets it with `patterns`, the subdirectory version wins entirely). Replicate this override behavior in flat config.
|
|
25
|
+
|
|
26
|
+
## Step 2: Plan subdirectory config strategy
|
|
27
|
+
|
|
28
|
+
If subdirectory `.eslintrc.*` files were found, **ask the user which approach they prefer**:
|
|
29
|
+
|
|
30
|
+
### Option A: Subdirectory files imported by root (recommended for monorepos)
|
|
31
|
+
|
|
32
|
+
Each subdirectory exports a flat config array with **root-relative** `files` patterns. The root imports and spreads them.
|
|
33
|
+
|
|
34
|
+
```js
|
|
35
|
+
// packages/my-app/src/eslint.config.cjs
|
|
36
|
+
module.exports = [
|
|
37
|
+
{
|
|
38
|
+
files: ['packages/my-app/src/**/*.{js,jsx,ts,tsx}'],
|
|
39
|
+
rules: { 'my-rule': 'error' },
|
|
40
|
+
},
|
|
41
|
+
];
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
```js
|
|
45
|
+
// eslint.config.js (root)
|
|
46
|
+
const myAppConfig = require('./packages/my-app/src/eslint.config.cjs');
|
|
47
|
+
module.exports = [...mcAppConfig, ...myAppConfig];
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
> **Use `.cjs` extension** for subdirectory config files. If any package has `"type": "module"` in its `package.json`, a `.js` file will be treated as ESM and `module.exports` will fail.
|
|
51
|
+
|
|
52
|
+
### Option B: Inline in root config
|
|
53
|
+
|
|
54
|
+
All rules defined directly in root `eslint.config.js` with directory-scoped `files` patterns. Simpler for small projects.
|
|
55
|
+
|
|
56
|
+
## Step 3: Create `eslint.config.js`
|
|
57
|
+
|
|
58
|
+
### Base structure
|
|
59
|
+
|
|
60
|
+
```js
|
|
61
|
+
// Preserve any process.env assignments from the old config
|
|
62
|
+
process.env.ENABLE_NEW_JSX_TRANSFORM = 'true';
|
|
63
|
+
|
|
64
|
+
// Plugins are now imported as objects, not strings
|
|
65
|
+
const somePlugin = require('some-eslint-plugin');
|
|
66
|
+
const mcAppConfig = require('@commercetools-frontend/eslint-config-mc-app');
|
|
67
|
+
|
|
68
|
+
module.exports = [
|
|
69
|
+
// Ignores replace .eslintignore (directory patterns end with /)
|
|
70
|
+
{ ignores: ['dist/', 'build/'] },
|
|
71
|
+
|
|
72
|
+
// Spread the base config (replaces "extends")
|
|
73
|
+
...mcAppConfig,
|
|
74
|
+
|
|
75
|
+
// Top-level rule overrides become a config object after the spread
|
|
76
|
+
{
|
|
77
|
+
files: ['**/*.{js,jsx,ts,tsx}'],
|
|
78
|
+
rules: { 'no-console': 'warn' },
|
|
79
|
+
},
|
|
80
|
+
];
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
### Converting `plugins`
|
|
84
|
+
|
|
85
|
+
Plugins registered as strings become imported objects:
|
|
86
|
+
|
|
87
|
+
```js
|
|
88
|
+
// Before: plugins: ['@graphql-eslint']
|
|
89
|
+
// After:
|
|
90
|
+
const graphqlPlugin = require('@graphql-eslint/eslint-plugin');
|
|
91
|
+
// plugins: { '@graphql-eslint': graphqlPlugin }
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
### Converting `overrides`
|
|
95
|
+
|
|
96
|
+
Each `overrides` entry becomes a separate object in the config array:
|
|
97
|
+
|
|
98
|
+
```js
|
|
99
|
+
// Before (legacy):
|
|
100
|
+
overrides: [{ files: ['**/*.graphql'], parser: '@graphql-eslint/eslint-plugin',
|
|
101
|
+
parserOptions: { graphQLConfig: { schema: './schema.json' } },
|
|
102
|
+
rules: { '@graphql-eslint/known-type-names': 'error' } }]
|
|
103
|
+
|
|
104
|
+
// After (flat config):
|
|
105
|
+
{
|
|
106
|
+
files: ['**/*.graphql'],
|
|
107
|
+
plugins: { '@graphql-eslint': graphqlPlugin },
|
|
108
|
+
languageOptions: {
|
|
109
|
+
parser: graphqlPlugin,
|
|
110
|
+
parserOptions: { graphQLConfig: { schema: './schema.json' } },
|
|
111
|
+
},
|
|
112
|
+
rules: { '@graphql-eslint/known-type-names': 'error' },
|
|
113
|
+
}
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
> **Glob patterns must include `**/`for subdirectory matching.** In legacy config,`files: ['*.foo.js']`matched anywhere. In flat config, it only matches the root directory. Always prefix with`\*\*/`.
|
|
117
|
+
|
|
118
|
+
### Key property mappings
|
|
119
|
+
|
|
120
|
+
| Legacy (`.eslintrc`) | Flat config (`eslint.config.js`) |
|
|
121
|
+
| ---------------------------------- | --------------------------------------------------------- |
|
|
122
|
+
| `parser` (string) | `languageOptions.parser` (imported module) |
|
|
123
|
+
| `parserOptions` | `languageOptions.parserOptions` |
|
|
124
|
+
| `env: { browser: true }` | `languageOptions.globals` (use the `globals` npm package) |
|
|
125
|
+
| `globals: { myVar: 'readonly' }` | `languageOptions.globals: { myVar: 'readonly' }` |
|
|
126
|
+
| `plugins: ['name']` (string array) | `plugins: { name: importedPlugin }` (object) |
|
|
127
|
+
| `excludedFiles` | `ignores` (within the same config object) |
|
|
128
|
+
|
|
129
|
+
### Plugin scoping: matching rules to file types
|
|
130
|
+
|
|
131
|
+
**Critical difference from legacy ESLint.** In flat config, plugins are only registered for files matching the config object's `files` pattern. Setting a rule from an unregistered plugin causes an error. Split rule overrides by plugin scope.
|
|
132
|
+
|
|
133
|
+
The base `mcAppConfig` registers plugins for:
|
|
134
|
+
|
|
135
|
+
| Plugin | Registered for |
|
|
136
|
+
| -------------------- | -------------------------------- |
|
|
137
|
+
| `react` | `*.js`, `*.jsx`, `*.tsx` |
|
|
138
|
+
| `react-hooks` | `*.js`, `*.jsx`, `*.ts`, `*.tsx` |
|
|
139
|
+
| `@typescript-eslint` | `*.ts`, `*.tsx` |
|
|
140
|
+
| `jest` | `*.spec.*`, `*.test.*` |
|
|
141
|
+
| `testing-library` | `*.spec.*`, `*.test.*` |
|
|
142
|
+
|
|
143
|
+
If your override mixes rules from different plugins, split into separate config objects matching each plugin's file types (e.g., `react/` rules in `**/*.{js,jsx,tsx}`, `@typescript-eslint/` rules in `**/*.{ts,tsx}`, core rules in `**/*.{js,jsx,ts,tsx}`).
|
|
144
|
+
|
|
145
|
+
#### Common mistake: catch-all file patterns with mixed plugin rules
|
|
146
|
+
|
|
147
|
+
The natural instinct when converting a subdirectory `.eslintrc.cjs` is to put all the rules into a single config object targeting `**/*.{js,jsx,ts,tsx}`. This will fail because not every plugin is registered for every file type.
|
|
148
|
+
|
|
149
|
+
```js
|
|
150
|
+
// WRONG — will error on .ts files because `react` plugin is not registered for them
|
|
151
|
+
{
|
|
152
|
+
files: ['packages/my-app/src/**/*.{js,jsx,ts,tsx}'],
|
|
153
|
+
rules: {
|
|
154
|
+
'no-console': 'warn', // core — works on all
|
|
155
|
+
'react/jsx-sort-props': 'error', // react — NOT on .ts
|
|
156
|
+
'@typescript-eslint/consistent-type-imports': 'error', // ts — NOT on .js/.jsx
|
|
157
|
+
},
|
|
158
|
+
}
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
Split into separate config objects, each matching the plugin's registered file types:
|
|
162
|
+
|
|
163
|
+
```js
|
|
164
|
+
// Core ESLint rules — all source files
|
|
165
|
+
{
|
|
166
|
+
files: ['packages/my-app/src/**/*.{js,jsx,ts,tsx}'],
|
|
167
|
+
rules: { 'no-console': 'warn' },
|
|
168
|
+
},
|
|
169
|
+
// React rules — only file types where the react plugin is registered
|
|
170
|
+
{
|
|
171
|
+
files: ['packages/my-app/src/**/*.{js,jsx,tsx}'],
|
|
172
|
+
rules: { 'react/jsx-sort-props': 'error' },
|
|
173
|
+
},
|
|
174
|
+
// TypeScript rules — only .ts and .tsx
|
|
175
|
+
{
|
|
176
|
+
files: ['packages/my-app/src/**/*.{ts,tsx}'],
|
|
177
|
+
rules: { '@typescript-eslint/consistent-type-imports': 'error' },
|
|
178
|
+
},
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
> **Tip**: When converting an existing `.eslintrc.cjs`, group its rules by which plugin they belong to, then create one config object per plugin group with the correct `files` pattern. Core ESLint rules (no plugin prefix) can target all file types.
|
|
182
|
+
|
|
183
|
+
#### Common mistake: `no-unused-vars` duplication on TypeScript files
|
|
184
|
+
|
|
185
|
+
The base `mcAppConfig` disables core `no-unused-vars` on `.ts`/`.tsx` files and replaces it with `@typescript-eslint/no-unused-vars`. If your subdirectory config re-enables the core `no-unused-vars` for all file types, both rules will fire on TypeScript files, producing duplicate errors.
|
|
186
|
+
|
|
187
|
+
```js
|
|
188
|
+
// WRONG — re-enables core no-unused-vars on .ts/.tsx where @typescript-eslint
|
|
189
|
+
// version is already active from the base config
|
|
190
|
+
{
|
|
191
|
+
files: ['packages/my-app/src/**/*.{js,jsx,ts,tsx}'],
|
|
192
|
+
rules: { 'no-unused-vars': 'error' },
|
|
193
|
+
}
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
Scope core `no-unused-vars` overrides to JavaScript files only:
|
|
197
|
+
|
|
198
|
+
```js
|
|
199
|
+
// CORRECT — only overrides the core rule on files where it applies
|
|
200
|
+
{
|
|
201
|
+
files: ['packages/my-app/src/**/*.{js,jsx}'],
|
|
202
|
+
rules: { 'no-unused-vars': 'error' },
|
|
203
|
+
}
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
### Self-linting the config file
|
|
207
|
+
|
|
208
|
+
The root `eslint.config.js` is itself a `.js` file in your project, so ESLint will lint it. Rules like `import/extensions` may error on `.cjs` requires. Add a self-override to avoid this:
|
|
209
|
+
|
|
210
|
+
```js
|
|
211
|
+
{
|
|
212
|
+
files: ['eslint.config.js'],
|
|
213
|
+
rules: { 'import/extensions': 'off' },
|
|
214
|
+
},
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
## Step 4: Update `package.json`
|
|
218
|
+
|
|
219
|
+
```diff
|
|
220
|
+
- "eslint": "8.57.1",
|
|
221
|
+
+ "eslint": "^9.0.0",
|
|
222
|
+
|
|
223
|
+
- "@commercetools-frontend/eslint-config-mc-app": "^25.0.0",
|
|
224
|
+
+ "@commercetools-frontend/eslint-config-mc-app": "^27.0.0",
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
Remove `@rushstack/eslint-patch` — not needed in flat config.
|
|
228
|
+
|
|
229
|
+
### Update custom ESLint plugins
|
|
230
|
+
|
|
231
|
+
If you maintain custom ESLint rule plugins, update deprecated APIs:
|
|
232
|
+
|
|
233
|
+
| Deprecated (ESLint 8) | Replacement (ESLint 9) |
|
|
234
|
+
| ------------------------- | --------------------------- |
|
|
235
|
+
| `context.getFilename()` | `context.filename` |
|
|
236
|
+
| `context.getSourceCode()` | `context.sourceCode` |
|
|
237
|
+
| `context.getScope()` | `sourceCode.getScope()` |
|
|
238
|
+
| `context.getAncestors()` | `sourceCode.getAncestors()` |
|
|
239
|
+
| `context.getCwd()` | `context.cwd` |
|
|
240
|
+
|
|
241
|
+
Also update the plugin's `peerDependencies` to `"eslint": "9.x"`.
|
|
242
|
+
|
|
243
|
+
### `jest-runner-eslint` compatibility
|
|
244
|
+
|
|
245
|
+
If using `jest-runner-eslint`, note that the `rules` key in `cliOptions` is not supported in flat config mode — move those rule overrides into `eslint.config.js` instead. You may also need a `pnpm.overrides` (or npm `overrides`) entry if the package's peer dependency still specifies ESLint 8:
|
|
246
|
+
|
|
247
|
+
```json
|
|
248
|
+
{
|
|
249
|
+
"pnpm": {
|
|
250
|
+
"overrides": {
|
|
251
|
+
"jest-runner-eslint>eslint": "^9.0.0"
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
If the formatter path uses a bare directory import (e.g., `node_modules/eslint-formatter-pretty`), ESM module resolution will reject it. Change to an explicit file path:
|
|
258
|
+
|
|
259
|
+
```diff
|
|
260
|
+
- format: 'node_modules/eslint-formatter-pretty',
|
|
261
|
+
+ format: 'node_modules/eslint-formatter-pretty/index.js',
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
## Step 5: Clean up
|
|
265
|
+
|
|
266
|
+
- Delete **all** `.eslintrc.*` files (root and subdirectories)
|
|
267
|
+
- Delete `.eslintignore`
|
|
268
|
+
- Verify: `find . -name '.eslintrc*' -not -path '*/node_modules/*'`
|
|
269
|
+
|
|
270
|
+
## Step 6: Verify
|
|
271
|
+
|
|
272
|
+
Run eslint on at least one file from **each directory that had its own config**. Check for:
|
|
273
|
+
|
|
274
|
+
- **Configuration errors** — fix the generated `eslint.config.js`
|
|
275
|
+
- **New lint violations** from dependency upgrades:
|
|
276
|
+
- `@typescript-eslint` v5→v8 (stricter type checking)
|
|
277
|
+
- `eslint-plugin-jest` v27→v28 (new rules)
|
|
278
|
+
- `eslint-plugin-react-hooks` v4→v5 (improved detection; rules now also apply to `*.ts` — custom hooks in `.ts` files may surface new warnings)
|
|
279
|
+
- `eslint-plugin-testing-library` v5→v7 (new best practices)
|
|
280
|
+
|
|
281
|
+
## Troubleshooting
|
|
282
|
+
|
|
283
|
+
**"Definition for rule 'plugin/rule-name' was not found"**: This means a rule is referenced on a file type where its plugin isn't registered. Two common causes:
|
|
284
|
+
|
|
285
|
+
1. **Config rules targeting wrong file types** — your subdirectory config has a catch-all `files` pattern like `**/*.{js,jsx,ts,tsx}` but includes rules from a plugin that isn't registered for all of those types. See "Plugin scoping" above for how to split by plugin.
|
|
286
|
+
2. **Stale inline `eslint-disable` comments** — ESLint 8 silently ignored `eslint-disable` comments referencing rules from unregistered plugins. ESLint 9 treats them as errors. This surfaces pre-existing stale comments that were previously harmless (e.g., `// eslint-disable-next-line testing-library/no-render-in-setup` in a file that isn't matched by the `testing-library` plugin's file pattern, or `@typescript-eslint/...` in a `.js` file). **Fix**: remove the stale disable comment, or if the rule should apply, register the plugin for that file type.
|
|
287
|
+
|
|
288
|
+
**"ReferenceError: module is not defined in ES module scope"**: Config file uses `module.exports` but nearest `package.json` has `"type": "module"`. **Fix**: rename to `.cjs` extension and update imports.
|
|
289
|
+
|
|
290
|
+
**Jest globals (`describe`, `it`, `expect`) not defined**: The base config only injects Jest globals for `**/*.{spec,test}.*`. Other files that use Jest APIs need explicit globals. Common patterns that are easy to miss:
|
|
291
|
+
|
|
292
|
+
- `__mocks__/` directories (at any depth)
|
|
293
|
+
- `test-utils/` directories (helper modules used by tests)
|
|
294
|
+
|
|
295
|
+
Add a config block for these:
|
|
296
|
+
|
|
297
|
+
```js
|
|
298
|
+
{
|
|
299
|
+
files: [
|
|
300
|
+
'**/__mocks__/**/*.{js,jsx,ts,tsx}',
|
|
301
|
+
'**/test-utils/**/*.{js,jsx,ts,tsx}',
|
|
302
|
+
],
|
|
303
|
+
languageOptions: {
|
|
304
|
+
globals: {
|
|
305
|
+
jest: 'readonly',
|
|
306
|
+
expect: 'readonly',
|
|
307
|
+
},
|
|
308
|
+
},
|
|
309
|
+
},
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
> **Note**: Use `**/__mocks__/**` (with leading `**/`), not `__mocks__/**`. The latter only matches a `__mocks__/` directory at the project root. Most projects have `__mocks__/` directories nested inside packages or `src/` directories.
|
|
313
|
+
|
|
314
|
+
**Duplicate `no-unused-vars` errors on `.ts`/`.tsx` files**: The base config sets `@typescript-eslint/no-unused-vars` for TypeScript files and disables the core `no-unused-vars` there. If your subdirectory config re-enables `no-unused-vars` for `**/*.{js,jsx,ts,tsx}`, both rules fire on TypeScript files. **Fix**: scope `no-unused-vars` overrides to `**/*.{js,jsx}` only. See "Common mistake" above.
|
|
315
|
+
|
|
316
|
+
**Lint errors in `eslint.config.js` itself**: The config file is a `.js` file in the project root, so ESLint lints it. Rules like `import/extensions` may error on `.cjs` requires. **Fix**: add a self-override: `{ files: ['eslint.config.js'], rules: { 'import/extensions': 'off' } }`.
|
|
317
|
+
|
|
318
|
+
**"Cannot find module 'some-eslint-plugin'"**: Plugins must be installed as direct dependencies in flat config.
|
|
319
|
+
|
|
320
|
+
**"context.getScope is not a function"**: Incompatible plugin version. Update to the latest major version supporting ESLint 9.
|
|
321
|
+
|
|
322
|
+
**Lint violations in test files**: The testing-library plugin v7 is stricter. Common issues: `testing-library/no-node-access` (use `fireEvent.click()` instead of `.click()`) and `testing-library/no-wait-for-side-effects` (only assertions belong in `waitFor()`).
|
|
323
|
+
|
|
324
|
+
## Additional resources
|
|
325
|
+
|
|
326
|
+
- [ESLint Flat Config Documentation](https://eslint.org/docs/latest/use/configure/configuration-files)
|
|
327
|
+
- [Migration Guide (Official ESLint)](https://eslint.org/docs/latest/use/configure/migration-guide)
|
|
328
|
+
- [TypeScript ESLint v8 Release Notes](https://typescript-eslint.io/blog/announcing-typescript-eslint-v8)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@commercetools-backend/eslint-config-node",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "27.0.0",
|
|
4
4
|
"description": "ESLint config for Node.js projects.",
|
|
5
5
|
"bugs": "https://github.com/commercetools/merchant-center-application-kit/issues",
|
|
6
6
|
"repository": {
|
|
@@ -24,24 +24,24 @@
|
|
|
24
24
|
"@babel/eslint-parser": "^7.22.15",
|
|
25
25
|
"@babel/preset-env": "^7.22.15",
|
|
26
26
|
"@babel/preset-typescript": "^7.22.15",
|
|
27
|
-
"@
|
|
28
|
-
"@typescript-eslint/
|
|
29
|
-
"@typescript-eslint/parser": "^5.62.0",
|
|
27
|
+
"@typescript-eslint/eslint-plugin": "^8.55.0",
|
|
28
|
+
"@typescript-eslint/parser": "^8.55.0",
|
|
30
29
|
"eslint-config-prettier": "^8.10.0",
|
|
31
30
|
"eslint-import-resolver-typescript": "^3.6.0",
|
|
32
31
|
"eslint-plugin-import": "^2.28.1",
|
|
33
|
-
"eslint-plugin-jest": "^
|
|
32
|
+
"eslint-plugin-jest": "^28.14.0",
|
|
34
33
|
"eslint-plugin-n": "^17.1.0",
|
|
35
34
|
"eslint-plugin-prettier": "^4.2.1",
|
|
35
|
+
"globals": "^15.15.0",
|
|
36
36
|
"prettier": "^2.8.4",
|
|
37
37
|
"typescript": "^5.2.2"
|
|
38
38
|
},
|
|
39
39
|
"peerDependencies": {
|
|
40
|
-
"eslint": "
|
|
40
|
+
"eslint": "^9.0.0"
|
|
41
41
|
},
|
|
42
42
|
"devDependencies": {
|
|
43
43
|
"@tsconfig/node22": "^22.0.0",
|
|
44
|
-
"eslint": "
|
|
44
|
+
"eslint": "^9.0.0"
|
|
45
45
|
},
|
|
46
46
|
"engines": {
|
|
47
47
|
"node": "18.x || 20.x || >=22.0.0"
|