@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 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
@@ -6,6 +6,10 @@
6
6
 
7
7
  ESLint config for Node.js projects.
8
8
 
9
+ ## Migrations
10
+
11
+ See the [migrations/](./migrations/) directory for version-specific upgrade guides. Each file is structured for both human and AI-assisted migration.
12
+
9
13
  ## Install
10
14
 
11
15
  ```bash
package/index.js CHANGED
@@ -1,132 +1,141 @@
1
- // This is a workaround for https://github.com/eslint/eslint/issues/3458
2
- require('@rushstack/eslint-patch/modern-module-resolution');
1
+ const fs = require('fs');
2
+ const path = require('path');
3
3
 
4
- const { statusCode, allSupportedExtensions } = require('./helpers/eslint');
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
- parserOptions: {
15
- sourceType: 'module',
16
- requireConfigFile: false,
17
- /**
18
- * @type {import('@babel/core').TransformOptions}
19
- */
20
- babelOptions: {
21
- presets: [['@babel/preset-env', { targets: { node: 'current' } }]],
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
- env: {
26
- browser: false,
27
- commonjs: true,
28
- es6: true,
29
- jest: true,
30
- node: true,
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
- extends: [
34
- 'eslint:recommended',
35
- // https://github.com/mysticatea/eslint-plugin-n
36
- 'plugin:n/recommended',
37
- // https://github.com/benmosher/eslint-plugin-import
38
- 'plugin:import/errors',
39
- 'plugin:import/warnings',
40
- // https://github.com/jest-community/eslint-plugin-jest
41
- 'plugin:jest/recommended',
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
- plugins: [
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
- settings: {
56
- 'import/resolver': {
57
- node: {
58
- extensions: allSupportedExtensions,
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
- rules: {
64
- // Nodejs
65
- 'n/no-missing-import': statusCode.off,
66
- 'n/no-unsupported-features/es-syntax': statusCode.off,
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
- // NOTE: The regular rule does not support do-expressions. The equivalent rule of babel does.
69
- 'no-unused-expressions': statusCode.off,
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
- // Imports
72
- 'import/extensions': [
73
- statusCode.error,
74
- {
75
- js: 'never',
76
- jsx: 'never',
77
- ts: 'never',
78
- tsx: 'never',
79
- mjs: 'never',
80
- json: 'always',
81
- svg: 'always',
82
- graphql: 'always',
83
- },
84
- ],
85
- 'import/default': statusCode.off,
86
- 'import/first': statusCode.error,
87
- // TODO: enable this once there is support for `import type`
88
- // 'import/order': statusCode.error,
89
- 'import/named': statusCode.off,
90
- 'import/namespace': statusCode.off,
91
- 'import/no-extraneous-dependencies': statusCode.off,
92
- 'import/no-named-as-default': statusCode.off,
93
- 'import/no-named-as-default-member': statusCode.off,
94
- 'import/no-unresolved': statusCode.error,
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
- // Jest
97
- 'jest/expect-expect': statusCode.off,
98
- 'jest/no-identical-title': statusCode.warn,
99
- 'jest/no-focused-tests': statusCode.error,
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
- overrides: [
103
- {
104
- files: ['*.{spec,test}.*'],
105
- env: {
106
- 'jest/globals': true,
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
- plugins: ['@typescript-eslint'],
146
- rules: {
147
- // TypeScript's `noFallthroughCasesInSwitch` option is more robust (#6906)
148
- 'default-case': statusCode.off,
149
- // 'tsc' already handles this (https://github.com/typescript-eslint/typescript-eslint/issues/291)
150
- 'no-dupe-class-members': statusCode.off,
151
- // 'tsc' already handles this (https://github.com/typescript-eslint/typescript-eslint/issues/477)
152
- 'no-undef': statusCode.off,
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
- // Add TypeScript specific rules (and turn off ESLint equivalents)
155
- '@typescript-eslint/consistent-type-assertions': statusCode.warn,
156
- 'no-array-constructor': statusCode.off,
157
- '@typescript-eslint/no-array-constructor': statusCode.warn,
158
- 'no-redeclare': statusCode.off,
159
- '@typescript-eslint/no-redeclare': statusCode.warn,
160
- 'no-use-before-define': statusCode.off,
161
- '@typescript-eslint/no-use-before-define': [
162
- statusCode.error,
163
- {
164
- functions: false,
165
- classes: false,
166
- variables: false,
167
- typedefs: false,
168
- },
169
- ],
170
- 'no-unused-expressions': statusCode.off,
171
- '@typescript-eslint/no-unused-expressions': [
172
- statusCode.error,
173
- {
174
- allowShortCircuit: true,
175
- allowTernary: true,
176
- allowTaggedTemplates: true,
177
- },
178
- ],
179
- 'no-unused-vars': statusCode.off,
180
- '@typescript-eslint/no-unused-vars': [
181
- statusCode.warn,
182
- {
183
- args: 'none',
184
- ignoreRestSiblings: true,
185
- },
186
- ],
187
- 'no-useless-constructor': statusCode.off,
188
- '@typescript-eslint/no-useless-constructor': statusCode.warn,
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
- // TypeScript
191
- '@typescript-eslint/ban-types': statusCode.off,
192
- '@typescript-eslint/naming-convention': statusCode.off,
193
- '@typescript-eslint/consistent-type-definitions': statusCode.off,
194
- '@typescript-eslint/no-explicit-any': statusCode.error,
195
- '@typescript-eslint/no-var-requires': statusCode.off,
196
- '@typescript-eslint/unbound-method': statusCode.off,
197
- '@typescript-eslint/ban-ts-comment': statusCode.off,
198
- '@typescript-eslint/explicit-function-return-type': statusCode.off,
199
- '@typescript-eslint/explicit-member-accessibility': [
200
- statusCode.error,
201
- { accessibility: 'no-public' },
202
- ],
203
- '@typescript-eslint/no-require-imports': statusCode.off,
204
- '@typescript-eslint/promise-function-async': statusCode.off,
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
- settings: {
207
- 'import/parsers': {
208
- '@typescript-eslint/parser': allSupportedExtensions,
209
- },
210
- 'import/resolver': {
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": "26.0.2",
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
- "@rushstack/eslint-patch": "^1.3.3",
28
- "@typescript-eslint/eslint-plugin": "^5.62.0",
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": "^27.2.3",
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": "8.x"
40
+ "eslint": "^9.0.0"
41
41
  },
42
42
  "devDependencies": {
43
43
  "@tsconfig/node22": "^22.0.0",
44
- "eslint": "8.57.1"
44
+ "eslint": "^9.0.0"
45
45
  },
46
46
  "engines": {
47
47
  "node": "18.x || 20.x || >=22.0.0"