@commercetools-frontend/eslint-config-mc-app 26.1.0 → 27.1.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,71 @@
1
1
  # @commercetools-frontend/eslint-config-mc-app
2
2
 
3
+ ## 27.1.0
4
+
5
+ ### Patch Changes
6
+
7
+ - Updated dependencies []:
8
+ - @commercetools-frontend/babel-preset-mc-app@27.1.0
9
+
10
+ ## 27.0.0
11
+
12
+ ### Major Changes
13
+
14
+ - [#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.
15
+
16
+ **Peer dependency:** `eslint@^9.0.0` is now required.
17
+
18
+ ## How to update
19
+
20
+ Both packages ship a `migrations/v27.md` with full step-by-step instructions, structured for both human and AI-assisted migration:
21
+
22
+ ```
23
+ node_modules/@commercetools-frontend/eslint-config-mc-app/migrations/v27.md
24
+ node_modules/@commercetools-backend/eslint-config-node/migrations/v27.md
25
+ ```
26
+
27
+ > "Migrate my eslint config following `node_modules/@commercetools-frontend/eslint-config-mc-app/migrations/v27.md`"
28
+
29
+ Quick summary:
30
+
31
+ 1. Upgrade ESLint to v9: `eslint@^9.0.0`
32
+ 2. Replace `.eslintrc.js` with `eslint.config.js`:
33
+
34
+ ```js
35
+ const mcAppConfig = require('@commercetools-frontend/eslint-config-mc-app');
36
+
37
+ module.exports = [
38
+ ...mcAppConfig,
39
+ // your overrides here
40
+ ];
41
+ ```
42
+
43
+ 3. Delete `.eslintignore` and inline ignore patterns as `{ ignores: ['dist/', 'build/'] }` in `eslint.config.js`
44
+ 4. Remove `@rushstack/eslint-patch` if present
45
+
46
+ ## What changed
47
+
48
+ Both packages now export a flat config array instead of a legacy `.eslintrc` object:
49
+
50
+ - Config is now an array of objects, each targeting its own file patterns (replaces `overrides`)
51
+ - Plugins must be imported as objects, not referenced as strings
52
+ - Parsers moved into `languageOptions.parser`
53
+ - `env` replaced with explicit globals via the `globals` package
54
+ - `extends` removed — plugin rules are configured directly
55
+ - `@rushstack/eslint-patch` removed — no longer needed in ESLint 9
56
+ - `react-hooks` rules now explicitly applied to `**/*.ts` files (custom hooks without JSX)
57
+
58
+ Dependency upgrades: `@typescript-eslint/*` v5→v8, `eslint-plugin-jest` v27→v28, `eslint-plugin-react-hooks` v4→v5, `eslint-plugin-testing-library` v5→v7.
59
+
60
+ ## Why
61
+
62
+ 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.
63
+
64
+ ### Patch Changes
65
+
66
+ - Updated dependencies []:
67
+ - @commercetools-frontend/babel-preset-mc-app@27.0.0
68
+
3
69
  ## 26.1.0
4
70
 
5
71
  ### Patch Changes
package/README.md CHANGED
@@ -4,22 +4,35 @@
4
4
  <a href="https://www.npmjs.com/package/@commercetools-frontend/eslint-config-mc-app"><img src="https://badgen.net/npm/v/@commercetools-frontend/eslint-config-mc-app" alt="Latest release (latest dist-tag)" /></a> <a href="https://www.npmjs.com/package/@commercetools-frontend/eslint-config-mc-app"><img src="https://badgen.net/npm/v/@commercetools-frontend/eslint-config-mc-app/next" alt="Latest release (next dist-tag)" /></a> <a href="https://bundlephobia.com/result?p=@commercetools-frontend/eslint-config-mc-app"><img src="https://badgen.net/bundlephobia/minzip/@commercetools-frontend/eslint-config-mc-app" alt="Minified + GZipped size" /></a> <a href="https://github.com/commercetools/merchant-center-application-kit/blob/main/LICENSE"><img src="https://badgen.net/github/license/commercetools/merchant-center-application-kit" alt="GitHub license" /></a>
5
5
  </p>
6
6
 
7
- ESLint config used by Merchant Center customizations.
7
+ ESLint flat config (v9+) for Merchant Center customizations.
8
+
9
+ ## Requirements
10
+
11
+ - ESLint 9.x or higher
12
+ - Node.js 18.x, 20.x, or >=22.0.0
13
+
14
+ ## Migrations
15
+
16
+ See the [migrations/](./migrations/) directory for version-specific upgrade guides. Each file is structured for both human and AI-assisted migration.
8
17
 
9
18
  ## Install
10
19
 
11
20
  ```bash
12
- $ npm install --save @commercetools-frontend/eslint-config-mc-app
13
-
14
- $ npx install-peerdeps --dev @commercetools-frontend/eslint-config-mc-app
21
+ pnpm add -D eslint @commercetools-frontend/eslint-config-mc-app
15
22
  ```
16
23
 
17
24
  ## Usage
18
25
 
26
+ Create an `eslint.config.js` file in your project root:
27
+
19
28
  ```js
20
- // .eslintrc.js
29
+ // eslint.config.js
30
+ const mcAppConfig = require('@commercetools-frontend/eslint-config-mc-app');
21
31
 
22
- module.exports = {
23
- extends: ['@commercetools-frontend/eslint-config-mc-app'],
24
- };
32
+ module.exports = [
33
+ ...mcAppConfig,
34
+ // Add your custom config overrides here
35
+ ];
25
36
  ```
37
+
38
+ > **Note**: This package uses ESLint's flat config format introduced in ESLint 9. If you're migrating from ESLint 8, you'll need to replace your `.eslintrc.js` file with `eslint.config.js`.
@@ -267,23 +267,21 @@ const craRules = {
267
267
  'jest/valid-title': statusCode.warn,
268
268
 
269
269
  // https://github.com/testing-library/eslint-plugin-testing-library
270
- 'testing-library/await-async-query': statusCode.error,
270
+ // Note: v6 renamed rules to plural forms and removed some rules
271
+ 'testing-library/await-async-queries': statusCode.error,
271
272
  'testing-library/await-async-utils': statusCode.error,
272
- 'testing-library/no-await-sync-query': statusCode.error,
273
+ 'testing-library/no-await-sync-queries': statusCode.error,
273
274
  'testing-library/no-container': statusCode.error,
274
275
  'testing-library/no-debugging-utils': statusCode.error,
275
276
  'testing-library/no-dom-import': [statusCode.error, 'react'],
276
277
  'testing-library/no-node-access': statusCode.error,
277
278
  'testing-library/no-promise-in-fire-event': statusCode.error,
278
- 'testing-library/no-render-in-setup': statusCode.error,
279
279
  'testing-library/no-unnecessary-act': statusCode.error,
280
- 'testing-library/no-wait-for-empty-callback': statusCode.error,
281
280
  'testing-library/no-wait-for-multiple-assertions': statusCode.error,
282
281
  'testing-library/no-wait-for-side-effects': statusCode.error,
283
282
  'testing-library/no-wait-for-snapshot': statusCode.error,
284
283
  'testing-library/prefer-find-by': statusCode.error,
285
284
  'testing-library/prefer-presence-queries': statusCode.error,
286
- 'testing-library/prefer-query-by-disappearance': statusCode.error,
287
285
  'testing-library/prefer-screen-queries': statusCode.error,
288
286
  'testing-library/render-result-naming-convention': statusCode.error,
289
287
  },
package/index.js CHANGED
@@ -1,264 +1,417 @@
1
- process.env.BABEL_ENV = 'production';
2
-
3
- // This is a workaround for https://github.com/eslint/eslint/issues/3458
4
- require('@rushstack/eslint-patch/modern-module-resolution');
5
-
6
- const { statusCode, allSupportedExtensions } = require('./helpers/eslint');
7
- const hasJsxRuntime = require('./helpers/has-jsx-runtime');
8
- const { craRules } = require('./helpers/rules-presets');
9
-
10
1
  /**
11
- * @type {import("eslint").Linter.Config}
2
+ * ESLint flat config (v9+) for @commercetools-frontend/eslint-config-mc-app
3
+ *
4
+ * This package provides ESLint configuration using the new flat config format
5
+ * introduced in ESLint v9. It includes support for:
6
+ * - TypeScript
7
+ * - React and JSX
8
+ * - Jest and Testing Library
9
+ * - Cypress
10
+ * - Import ordering and validation
11
+ * - Prettier integration
12
+ * - Accessibility rules (jsx-a11y)
12
13
  */
13
- module.exports = {
14
- root: true,
15
14
 
16
- parser: '@babel/eslint-parser',
15
+ /**
16
+ * ESLint flat config (v9+) for @commercetools-frontend/eslint-config-mc-app
17
+ *
18
+ * This package provides ESLint configuration using the new flat config format
19
+ * introduced in ESLint v9. It includes support for:
20
+ * - TypeScript
21
+ * - React and JSX
22
+ * - Jest and Testing Library
23
+ * - Cypress
24
+ * - Import ordering and validation
25
+ * - Prettier integration
26
+ * - Accessibility rules (jsx-a11y)
27
+ */
17
28
 
18
- parserOptions: {
19
- sourceType: 'module',
20
- requireConfigFile: false,
21
- babelOptions: {
22
- presets: [
23
- require.resolve(
24
- '@commercetools-frontend/babel-preset-mc-app/production'
25
- ),
26
- ],
27
- },
28
- },
29
+ process.env.BABEL_ENV = 'production';
29
30
 
30
- env: {
31
- browser: true,
32
- commonjs: true,
33
- es6: true,
34
- jest: true,
35
- node: true,
36
- },
31
+ const fs = require('fs');
32
+ const path = require('path');
37
33
 
38
- extends: [
39
- // https://github.com/cypress-io/eslint-plugin-cypress
40
- 'plugin:cypress/recommended',
41
- // https://github.com/import-js/eslint-plugin-import
42
- 'plugin:import/errors',
43
- 'plugin:import/warnings',
44
- 'plugin:import/typescript',
45
- // https://github.com/prettier/prettier-eslint
46
- // NOTE: this should go last.
47
- 'prettier',
48
- ],
34
+ const migrationGuide =
35
+ 'node_modules/@commercetools-frontend/eslint-config-mc-app/migrations/v27.md';
49
36
 
50
- plugins: [
51
- // https://github.com/import-js/eslint-plugin-import
52
- 'import',
53
- // https://github.com/jsx-eslint/eslint-plugin-jsx-a11y
54
- 'jsx-a11y',
55
- // https://github.com/prettier/prettier-eslint
56
- 'prettier',
57
- ],
37
+ // Detect if loaded from a legacy .eslintrc file (the array export won't work there)
38
+ const caller = module.parent?.filename || '';
39
+ if (/\.eslintrc/.test(caller)) {
40
+ console.error(
41
+ '\n\u274c @commercetools-frontend/eslint-config-mc-app v27 exports a flat config array.\n' +
42
+ ' It cannot be used in .eslintrc files. Migrate to eslint.config.js.\n' +
43
+ ' Guide: ' +
44
+ migrationGuide +
45
+ '\n'
46
+ );
47
+ }
58
48
 
59
- settings: {
60
- 'import/resolver': {
61
- node: {
62
- extensions: allSupportedExtensions,
63
- },
64
- },
65
- },
49
+ // Detect leftover legacy config files and warn
50
+ const projectRoot = process.cwd();
51
+ const legacyPatterns = [
52
+ '.eslintrc',
53
+ '.eslintrc.js',
54
+ '.eslintrc.cjs',
55
+ '.eslintrc.json',
56
+ '.eslintrc.yml',
57
+ '.eslintrc.yaml',
58
+ ];
59
+ const foundLegacy = legacyPatterns.filter((name) =>
60
+ fs.existsSync(path.join(projectRoot, name))
61
+ );
62
+ if (foundLegacy.length > 0) {
63
+ console.warn(
64
+ '\n\u26a0\ufe0f @commercetools-frontend/eslint-config-mc-app v27 uses ESLint 9 flat config.\n' +
65
+ ` Found legacy config file(s): ${foundLegacy.join(', ')}\n` +
66
+ ' These files are ignored by ESLint 9 and your subdirectory rules will not be applied.\n' +
67
+ ' Guide: ' +
68
+ migrationGuide +
69
+ '\n'
70
+ );
71
+ }
66
72
 
67
- rules: {
68
- ...craRules.base,
73
+ const babelParser = require('@babel/eslint-parser');
74
+ const typescriptPlugin = require('@typescript-eslint/eslint-plugin');
75
+ const typescriptParser = require('@typescript-eslint/parser');
76
+ const prettierConfig = require('eslint-config-prettier');
77
+ const cypressPlugin = require('eslint-plugin-cypress');
78
+ const importPlugin = require('eslint-plugin-import');
79
+ const jestPlugin = require('eslint-plugin-jest');
80
+ const jestDomPlugin = require('eslint-plugin-jest-dom');
81
+ const jsxA11yPlugin = require('eslint-plugin-jsx-a11y');
82
+ const prettierPlugin = require('eslint-plugin-prettier');
83
+ const reactPlugin = require('eslint-plugin-react');
84
+ const reactHooksPlugin = require('eslint-plugin-react-hooks');
85
+ const testingLibraryPlugin = require('eslint-plugin-testing-library');
86
+ const globals = require('globals');
69
87
 
70
- // NOTE: The regular rule does not support do-expressions. The equivalent rule of babel does.
71
- 'no-unused-expressions': statusCode.off,
88
+ // Reuse existing helpers
89
+ const { statusCode, allSupportedExtensions } = require('./helpers/eslint');
90
+ const hasJsxRuntime = require('./helpers/has-jsx-runtime');
91
+ const { craRules } = require('./helpers/rules-presets');
72
92
 
73
- // Enforce direct lodash imports for tree-shaking (e.g., 'lodash/omit' not 'lodash')
74
- 'no-restricted-imports': [
75
- statusCode.warn,
76
- {
77
- paths: [
78
- {
79
- name: 'lodash',
80
- message:
81
- "Import from 'lodash/<function>' directly for tree-shaking (e.g., import omit from 'lodash/omit').",
82
- },
83
- ],
93
+ /**
94
+ * ESLint flat config format for @commercetools-frontend/eslint-config-mc-app
95
+ * @type {import("eslint").Linter.FlatConfig[]}
96
+ */
97
+ module.exports = [
98
+ // Base config for all JavaScript/TypeScript files
99
+ {
100
+ files: ['**/*.{js,jsx,ts,tsx,cjs,mjs}'],
101
+ languageOptions: {
102
+ parser: babelParser,
103
+ parserOptions: {
104
+ sourceType: 'module',
105
+ requireConfigFile: false,
106
+ babelOptions: {
107
+ presets: [
108
+ require.resolve(
109
+ '@commercetools-frontend/babel-preset-mc-app/production'
110
+ ),
111
+ ],
112
+ },
84
113
  },
85
- ],
86
-
87
- // Imports
88
- 'import/extensions': [
89
- statusCode.error,
90
- {
91
- js: 'never',
92
- jsx: 'never',
93
- ts: 'never',
94
- tsx: 'never',
95
- mjs: 'never',
96
- json: 'always',
97
- svg: 'always',
98
- graphql: 'always',
114
+ ecmaVersion: 2020,
115
+ globals: {
116
+ ...globals.browser,
117
+ ...globals.commonjs,
118
+ ...globals.es2015,
119
+ ...globals.node,
99
120
  },
100
- ],
101
- 'import/default': statusCode.off,
102
- 'import/first': statusCode.error,
103
- // TODO: enable this once there is support for `import type`
104
- // 'import/order': statusCode.error,
105
- 'import/namespace': statusCode.off,
106
- 'import/no-extraneous-dependencies': statusCode.off,
107
- 'import/no-named-as-default': statusCode.off,
108
- 'import/no-named-as-default-member': statusCode.off,
109
- 'import/no-unresolved': statusCode.error,
110
- },
111
-
112
- overrides: [
113
- {
114
- files: ['*.{spec,test}.*'],
115
- env: {
116
- 'jest/globals': true,
121
+ },
122
+ plugins: {
123
+ import: importPlugin,
124
+ 'jsx-a11y': jsxA11yPlugin,
125
+ prettier: prettierPlugin,
126
+ cypress: cypressPlugin,
127
+ },
128
+ settings: {
129
+ 'import/resolver': {
130
+ node: {
131
+ extensions: allSupportedExtensions,
132
+ },
117
133
  },
118
- extends: [
119
- // https://github.com/jest-community/eslint-plugin-jest
120
- 'plugin:jest/recommended',
121
- // https://github.com/testing-library/eslint-plugin-testing-library
122
- 'plugin:testing-library/react',
123
- // https://github.com/prettier/prettier-eslint
124
- // NOTE: this should go last.
125
- 'prettier',
134
+ },
135
+ rules: {
136
+ ...craRules.base,
137
+ // Disable React rules in base config - they'll be enabled in React file configs
138
+ 'react/forbid-foreign-prop-types': statusCode.off,
139
+ 'react/jsx-no-comment-textnodes': statusCode.off,
140
+ 'react/jsx-no-duplicate-props': statusCode.off,
141
+ 'react/jsx-no-target-blank': statusCode.off,
142
+ 'react/jsx-no-undef': statusCode.off,
143
+ 'react/jsx-pascal-case': statusCode.off,
144
+ 'react/no-danger-with-children': statusCode.off,
145
+ 'react/no-direct-mutation-state': statusCode.off,
146
+ 'react/no-is-mounted': statusCode.off,
147
+ 'react/no-typos': statusCode.off,
148
+ 'react/require-render-return': statusCode.off,
149
+ 'react/style-prop-object': statusCode.off,
150
+ 'react-hooks/exhaustive-deps': statusCode.off,
151
+ 'react-hooks/rules-of-hooks': statusCode.off,
152
+ 'no-unused-expressions': statusCode.off,
153
+ // Enforce direct lodash imports for tree-shaking (e.g., 'lodash/<function>' not 'lodash')
154
+ 'no-restricted-imports': [
155
+ statusCode.warn,
156
+ {
157
+ paths: [
158
+ {
159
+ name: 'lodash',
160
+ message:
161
+ "Import from 'lodash/<function>' directly for tree-shaking (e.g., import omit from 'lodash/omit').",
162
+ },
163
+ ],
164
+ },
126
165
  ],
127
- plugins: [
128
- 'testing-library',
129
- // https://github.com/jest-community/eslint-plugin-jest
130
- 'jest',
131
- // https://github.com/testing-library/eslint-plugin-jest-dom
132
- 'jest-dom',
133
- // https://github.com/prettier/prettier-eslint
134
- 'prettier',
166
+ 'import/extensions': [
167
+ statusCode.error,
168
+ {
169
+ js: 'never',
170
+ jsx: 'never',
171
+ ts: 'never',
172
+ tsx: 'never',
173
+ mjs: 'never',
174
+ json: 'always',
175
+ svg: 'always',
176
+ graphql: 'always',
177
+ },
135
178
  ],
136
- rules: {
137
- ...craRules.jest,
138
-
139
- // React
140
- 'react/display-name': statusCode.off,
141
-
142
- // Jest
143
- 'jest/expect-expect': statusCode.off,
144
- 'jest/no-identical-title': statusCode.warn,
145
- 'jest/no-focused-tests': statusCode.error,
146
-
147
- // RTL
148
- 'testing-library/prefer-presence-queries': statusCode.error,
149
- 'testing-library/await-async-query': statusCode.error,
150
- // Enabling these would be a breaking change to the config
151
- 'testing-library/render-result-naming-convention': statusCode.off,
152
- 'testing-library/prefer-screen-queries': statusCode.off,
153
- 'testing-library/no-container': statusCode.warn,
154
- },
179
+ 'import/default': statusCode.off,
180
+ 'import/first': statusCode.error,
181
+ 'import/namespace': statusCode.off,
182
+ 'import/no-extraneous-dependencies': statusCode.off,
183
+ 'import/no-named-as-default': statusCode.off,
184
+ 'import/no-named-as-default-member': statusCode.off,
185
+ 'import/no-unresolved': statusCode.error,
186
+ 'prettier/prettier': 'error',
155
187
  },
156
- {
157
- files: ['**/*.{ts,tsx}'],
158
- parser: '@typescript-eslint/parser',
188
+ },
189
+
190
+ // TypeScript files (non-React)
191
+ {
192
+ files: ['**/*.ts'],
193
+ languageOptions: {
194
+ parser: typescriptParser,
159
195
  parserOptions: {
160
196
  ecmaVersion: 2018,
161
197
  sourceType: 'module',
162
- // typescript-eslint specific options
163
198
  warnOnUnsupportedTypeScriptVersion: true,
164
199
  },
165
- plugins: ['@typescript-eslint'],
166
- extends: ['prettier'],
167
- rules: {
168
- ...craRules.typescript,
169
-
170
- // TypeScript
171
- '@typescript-eslint/ban-types': statusCode.off,
172
- '@typescript-eslint/naming-convention': statusCode.off,
173
- '@typescript-eslint/consistent-type-definitions': statusCode.off,
174
- '@typescript-eslint/no-explicit-any': statusCode.error,
175
- '@typescript-eslint/no-use-before-define': [
176
- statusCode.error,
177
- { functions: false },
178
- ],
179
- '@typescript-eslint/no-var-requires': statusCode.off,
180
- '@typescript-eslint/unbound-method': statusCode.off,
181
- '@typescript-eslint/ban-ts-comment': statusCode.off,
182
- '@typescript-eslint/explicit-function-return-type': statusCode.off,
183
- '@typescript-eslint/explicit-member-accessibility': [
184
- statusCode.error,
185
- { accessibility: 'no-public' },
186
- ],
187
- '@typescript-eslint/no-require-imports': statusCode.off,
188
- '@typescript-eslint/promise-function-async': statusCode.off,
200
+ },
201
+ plugins: {
202
+ '@typescript-eslint': typescriptPlugin,
203
+ 'react-hooks': reactHooksPlugin,
204
+ },
205
+ settings: {
206
+ 'import/parsers': {
207
+ '@typescript-eslint/parser': ['.ts', '.tsx'],
189
208
  },
190
- settings: {
191
- 'import/parsers': {
192
- '@typescript-eslint/parser': ['.js', '.jsx', '.ts', '.tsx'],
209
+ 'import/resolver': {
210
+ typescript: {
211
+ alwaysTryTypes: true,
193
212
  },
194
- 'import/resolver': {
195
- 'eslint-import-resolver-typescript': true,
196
- typescript: {},
197
- node: {
198
- extensions: allSupportedExtensions,
199
- },
213
+ node: {
214
+ extensions: allSupportedExtensions,
200
215
  },
201
216
  },
202
217
  },
203
- {
204
- files: ['**/*.{js,jsx,tsx}'],
205
- extends: [
206
- // https://github.com/yannickcr/eslint-plugin-react
207
- 'plugin:react/recommended',
218
+ rules: {
219
+ ...craRules.typescript,
220
+ '@typescript-eslint/ban-types': statusCode.off,
221
+ '@typescript-eslint/naming-convention': statusCode.off,
222
+ '@typescript-eslint/consistent-type-definitions': statusCode.off,
223
+ '@typescript-eslint/no-explicit-any': statusCode.error,
224
+ '@typescript-eslint/no-use-before-define': [
225
+ statusCode.error,
226
+ { functions: false },
208
227
  ],
209
- plugins: [
210
- // https://github.com/yannickcr/eslint-plugin-react
211
- 'react',
212
- // https://github.com/facebook/react/tree/main/packages/eslint-plugin-react-hooks
213
- 'react-hooks',
228
+ '@typescript-eslint/no-var-requires': statusCode.off,
229
+ '@typescript-eslint/unbound-method': statusCode.off,
230
+ '@typescript-eslint/ban-ts-comment': statusCode.off,
231
+ '@typescript-eslint/explicit-function-return-type': statusCode.off,
232
+ '@typescript-eslint/explicit-member-accessibility': [
233
+ statusCode.error,
234
+ { accessibility: 'no-public' },
214
235
  ],
215
- rules: {
216
- // React
217
- 'react/jsx-uses-vars': statusCode.error,
218
- 'react/no-deprecated': statusCode.error,
219
- 'react/no-unused-prop-types': statusCode.error,
220
- ...(hasJsxRuntime() && {
221
- 'react/jsx-uses-react': statusCode.off,
222
- 'react/react-in-jsx-scope': statusCode.off,
223
- }),
224
- 'react/no-unknown-property': [
225
- statusCode.error,
226
- {
227
- ignore: [
228
- // To allow using Emotion's `css` prop: https://emotion.sh/docs/css-prop
229
- 'css',
230
- ],
231
- },
232
- ],
236
+ '@typescript-eslint/no-require-imports': statusCode.off,
237
+ '@typescript-eslint/promise-function-async': statusCode.off,
238
+ 'react-hooks/rules-of-hooks': 'error',
239
+ 'react-hooks/exhaustive-deps': 'warn',
240
+ },
241
+ },
242
+
243
+ // React/JSX files (JavaScript)
244
+ {
245
+ files: ['**/*.{js,jsx}'],
246
+ plugins: {
247
+ react: reactPlugin,
248
+ 'react-hooks': reactHooksPlugin,
249
+ },
250
+ settings: {
251
+ react: {
252
+ version: 'detect',
233
253
  },
234
- settings: {
235
- react: {
236
- version: 'detect',
254
+ },
255
+ rules: {
256
+ 'react/jsx-uses-vars': statusCode.error,
257
+ 'react/no-deprecated': statusCode.error,
258
+ 'react/no-unused-prop-types': statusCode.error,
259
+ ...(hasJsxRuntime() && {
260
+ 'react/jsx-uses-react': statusCode.off,
261
+ 'react/react-in-jsx-scope': statusCode.off,
262
+ }),
263
+ 'react/no-unknown-property': [
264
+ statusCode.error,
265
+ {
266
+ ignore: ['css'], // Emotion's css prop
237
267
  },
238
- },
268
+ ],
269
+ 'react-hooks/rules-of-hooks': 'error',
270
+ 'react-hooks/exhaustive-deps': 'warn',
239
271
  },
240
- {
241
- files: ['**/*.tsx'],
242
- parser: '@typescript-eslint/parser',
272
+ },
273
+
274
+ // TypeScript + React files (TSX)
275
+ {
276
+ files: ['**/*.tsx'],
277
+ languageOptions: {
278
+ parser: typescriptParser,
243
279
  parserOptions: {
280
+ ecmaVersion: 2018,
281
+ sourceType: 'module',
282
+ warnOnUnsupportedTypeScriptVersion: true,
244
283
  ecmaFeatures: {
245
284
  jsx: true,
246
285
  },
247
286
  },
248
- rules: {
249
- // React
250
- 'react/no-unused-prop-types': statusCode.off,
251
- 'react/prop-types': statusCode.off,
287
+ },
288
+ plugins: {
289
+ '@typescript-eslint': typescriptPlugin,
290
+ react: reactPlugin,
291
+ 'react-hooks': reactHooksPlugin,
292
+ },
293
+ settings: {
294
+ react: {
295
+ version: 'detect',
296
+ },
297
+ 'import/parsers': {
298
+ '@typescript-eslint/parser': ['.ts', '.tsx'],
299
+ },
300
+ 'import/resolver': {
301
+ typescript: {
302
+ alwaysTryTypes: true,
303
+ },
304
+ node: {
305
+ extensions: allSupportedExtensions,
306
+ },
252
307
  },
253
308
  },
254
- {
255
- files: [
256
- '.custom-application-config.{js,cjs,mjs,ts}',
257
- 'custom-application-config.{js,cjs,mjs,ts}',
309
+ rules: {
310
+ ...craRules.typescript,
311
+ '@typescript-eslint/ban-types': statusCode.off,
312
+ '@typescript-eslint/naming-convention': statusCode.off,
313
+ '@typescript-eslint/consistent-type-definitions': statusCode.off,
314
+ '@typescript-eslint/no-explicit-any': statusCode.error,
315
+ '@typescript-eslint/no-use-before-define': [
316
+ statusCode.error,
317
+ { functions: false },
258
318
  ],
259
- rules: {
260
- 'no-template-curly-in-string': statusCode.off,
319
+ '@typescript-eslint/no-var-requires': statusCode.off,
320
+ '@typescript-eslint/unbound-method': statusCode.off,
321
+ '@typescript-eslint/ban-ts-comment': statusCode.off,
322
+ '@typescript-eslint/explicit-function-return-type': statusCode.off,
323
+ '@typescript-eslint/explicit-member-accessibility': [
324
+ statusCode.error,
325
+ { accessibility: 'no-public' },
326
+ ],
327
+ '@typescript-eslint/no-require-imports': statusCode.off,
328
+ '@typescript-eslint/promise-function-async': statusCode.off,
329
+ 'react/jsx-uses-vars': statusCode.error,
330
+ 'react/no-deprecated': statusCode.error,
331
+ 'react/no-unused-prop-types': statusCode.off,
332
+ 'react/prop-types': statusCode.off,
333
+ ...(hasJsxRuntime() && {
334
+ 'react/jsx-uses-react': statusCode.off,
335
+ 'react/react-in-jsx-scope': statusCode.off,
336
+ }),
337
+ 'react/no-unknown-property': [
338
+ statusCode.error,
339
+ {
340
+ ignore: ['css'],
341
+ },
342
+ ],
343
+ 'react-hooks/rules-of-hooks': 'error',
344
+ 'react-hooks/exhaustive-deps': 'warn',
345
+ },
346
+ },
347
+
348
+ // Test files (all)
349
+ {
350
+ files: ['**/*.{spec,test}.{js,jsx,ts,tsx}'],
351
+ languageOptions: {
352
+ globals: {
353
+ ...globals.jest,
261
354
  },
262
355
  },
263
- ],
264
- };
356
+ plugins: {
357
+ jest: jestPlugin,
358
+ 'jest-dom': jestDomPlugin,
359
+ 'testing-library': testingLibraryPlugin,
360
+ },
361
+ rules: {
362
+ ...craRules.jest,
363
+ 'react/display-name': statusCode.off,
364
+ 'jest/expect-expect': statusCode.off,
365
+ 'jest/no-identical-title': statusCode.warn,
366
+ 'jest/no-focused-tests': statusCode.error,
367
+ 'testing-library/prefer-presence-queries': statusCode.error,
368
+ 'testing-library/await-async-queries': statusCode.error,
369
+ 'testing-library/render-result-naming-convention': statusCode.off,
370
+ 'testing-library/prefer-screen-queries': statusCode.off,
371
+ 'testing-library/no-container': statusCode.warn,
372
+ },
373
+ },
374
+
375
+ // Test utility files (e.g., test-utils.tsx, test-helpers.tsx)
376
+ // These files aren't named *.spec.* or *.test.* but contain testing code
377
+ // and use inline eslint-disable comments for testing-library rules
378
+ {
379
+ files: [
380
+ '**/test-utils/**/*.{jsx,tsx}',
381
+ '**/*test-utils.{jsx,tsx}',
382
+ '**/*test-helpers.{jsx,tsx}',
383
+ ],
384
+ languageOptions: {
385
+ globals: {
386
+ ...globals.jest,
387
+ },
388
+ },
389
+ plugins: {
390
+ jest: jestPlugin,
391
+ 'jest-dom': jestDomPlugin,
392
+ 'testing-library': testingLibraryPlugin,
393
+ react: reactPlugin,
394
+ 'react-hooks': reactHooksPlugin,
395
+ },
396
+ rules: {
397
+ 'react/display-name': statusCode.off,
398
+ 'testing-library/render-result-naming-convention': statusCode.off,
399
+ 'testing-library/no-node-access': statusCode.warn,
400
+ 'react-hooks/exhaustive-deps': 'warn',
401
+ },
402
+ },
403
+
404
+ // Custom application config files
405
+ {
406
+ files: [
407
+ '.custom-application-config.{js,cjs,mjs,ts}',
408
+ 'custom-application-config.{js,cjs,mjs,ts}',
409
+ ],
410
+ rules: {
411
+ 'no-template-curly-in-string': statusCode.off,
412
+ },
413
+ },
414
+
415
+ // Prettier must be last to override formatting rules
416
+ prettierConfig,
417
+ ];
@@ -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-frontend/eslint-config-mc-app",
3
- "version": "26.1.0",
3
+ "version": "27.1.0",
4
4
  "description": "ESLint config used by Merchant Center customizations.",
5
5
  "bugs": "https://github.com/commercetools/merchant-center-application-kit/issues",
6
6
  "repository": {
@@ -23,30 +23,30 @@
23
23
  "dependencies": {
24
24
  "@babel/core": "^7.22.17",
25
25
  "@babel/eslint-parser": "^7.22.15",
26
- "@commercetools-frontend/babel-preset-mc-app": "^26.1.0",
27
- "@rushstack/eslint-patch": "^1.3.3",
28
- "@typescript-eslint/eslint-plugin": "^5.62.0",
29
- "@typescript-eslint/parser": "^5.62.0",
26
+ "@commercetools-frontend/babel-preset-mc-app": "^27.1.0",
27
+ "@typescript-eslint/eslint-plugin": "^8.55.0",
28
+ "@typescript-eslint/parser": "^8.55.0",
30
29
  "confusing-browser-globals": "^1.0.11",
31
30
  "eslint-config-prettier": "^8.10.0",
32
31
  "eslint-import-resolver-typescript": "^3.6.0",
33
32
  "eslint-plugin-cypress": "^2.14.0",
34
33
  "eslint-plugin-import": "^2.28.1",
35
- "eslint-plugin-jest": "^27.2.3",
34
+ "eslint-plugin-jest": "^28.0.0",
36
35
  "eslint-plugin-jest-dom": "^4.0.3",
37
36
  "eslint-plugin-jsx-a11y": "^6.7.1",
38
37
  "eslint-plugin-prettier": "^4.2.1",
39
38
  "eslint-plugin-react": "^7.33.2",
40
- "eslint-plugin-react-hooks": "^4.6.0",
41
- "eslint-plugin-testing-library": "^5.11.1",
39
+ "eslint-plugin-react-hooks": "^5.0.0",
40
+ "eslint-plugin-testing-library": "^7.15.4",
41
+ "globals": "^15.15.0",
42
42
  "prettier": "^2.8.4",
43
43
  "typescript": "^5.2.2"
44
44
  },
45
45
  "peerDependencies": {
46
- "eslint": "8.x"
46
+ "eslint": "^9.0.0"
47
47
  },
48
48
  "devDependencies": {
49
- "eslint": "8.57.1"
49
+ "eslint": "^9.0.0"
50
50
  },
51
51
  "engines": {
52
52
  "node": "18.x || 20.x || >=22.0.0"