@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 +66 -0
- package/README.md +21 -8
- package/helpers/rules-presets.js +3 -5
- package/index.js +375 -222
- package/migrations/v27.md +328 -0
- package/package.json +10 -10
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
|
|
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
|
-
|
|
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
|
-
// .
|
|
29
|
+
// eslint.config.js
|
|
30
|
+
const mcAppConfig = require('@commercetools-frontend/eslint-config-mc-app');
|
|
21
31
|
|
|
22
|
-
module.exports =
|
|
23
|
-
|
|
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`.
|
package/helpers/rules-presets.js
CHANGED
|
@@ -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
|
-
|
|
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-
|
|
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
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
31
|
-
|
|
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
|
-
|
|
39
|
-
|
|
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
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
'
|
|
55
|
-
|
|
56
|
-
|
|
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
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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
|
-
|
|
68
|
-
|
|
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
|
-
|
|
71
|
-
|
|
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
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
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
|
-
|
|
88
|
-
|
|
89
|
-
|
|
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
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
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
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
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
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
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
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
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
|
-
|
|
158
|
-
|
|
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
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
'@typescript-eslint/
|
|
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
|
-
|
|
191
|
-
|
|
192
|
-
|
|
209
|
+
'import/resolver': {
|
|
210
|
+
typescript: {
|
|
211
|
+
alwaysTryTypes: true,
|
|
193
212
|
},
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
typescript: {},
|
|
197
|
-
node: {
|
|
198
|
-
extensions: allSupportedExtensions,
|
|
199
|
-
},
|
|
213
|
+
node: {
|
|
214
|
+
extensions: allSupportedExtensions,
|
|
200
215
|
},
|
|
201
216
|
},
|
|
202
217
|
},
|
|
203
|
-
{
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
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
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
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
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
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
|
-
|
|
235
|
-
|
|
236
|
-
|
|
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
|
-
|
|
242
|
-
|
|
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
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
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
|
-
|
|
256
|
-
|
|
257
|
-
|
|
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
|
-
|
|
260
|
-
|
|
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": "
|
|
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": "^
|
|
27
|
-
"@
|
|
28
|
-
"@typescript-eslint/
|
|
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": "^
|
|
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": "^
|
|
41
|
-
"eslint-plugin-testing-library": "^
|
|
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": "
|
|
46
|
+
"eslint": "^9.0.0"
|
|
47
47
|
},
|
|
48
48
|
"devDependencies": {
|
|
49
|
-
"eslint": "
|
|
49
|
+
"eslint": "^9.0.0"
|
|
50
50
|
},
|
|
51
51
|
"engines": {
|
|
52
52
|
"node": "18.x || 20.x || >=22.0.0"
|