@alexlit/config-eslint 157.0.0 → 158.0.1

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/README.md CHANGED
@@ -5,6 +5,10 @@
5
5
  > [@alexlit/config-eslint@90](https://www.npmjs.com/package/@alexlit/config-eslint?activeTab=versions)
6
6
  > or lower.
7
7
 
8
+ There are no presets: `base` only provides the shared file scope, ignore
9
+ patterns and globals. Every plugin is enabled explicitly, so you pay only for
10
+ what you use.
11
+
8
12
  ## Installation
9
13
 
10
14
  ```sh
@@ -13,33 +17,118 @@ npm i @alexlit/config-eslint -D
13
17
 
14
18
  ## Connection
15
19
 
16
- - With preset
20
+ Always start with `base` (it must be first) and finish with `prettierConfig` (it
21
+ must be last, it disables the formatting rules).
22
+
23
+ ```js
24
+ // eslint.config.js
25
+ import {
26
+ // foundation
27
+ base,
28
+
29
+ // plugins
30
+ deMorgan,
31
+ importX,
32
+ javascript,
33
+ jsdoc,
34
+ json,
35
+ perfectionist,
36
+ prettierConfig,
37
+ regexp,
38
+ sonar,
39
+ stylistic,
40
+ typescript,
41
+ unicorn,
42
+
43
+ // framework plugins
44
+ pinia,
45
+ tailwindcss,
46
+ tanstackQuery,
47
+ unocss,
48
+ vitest,
49
+ vue,
50
+ vueAccessibility,
51
+ vueI18n,
52
+ zod,
53
+ } from '@alexlit/config-eslint';
54
+
55
+ export default [
56
+ ...base, // must be first
57
+
58
+ ...deMorgan,
59
+ ...importX,
60
+ ...javascript,
61
+ ...jsdoc,
62
+ ...json,
63
+ ...perfectionist,
64
+ ...regexp,
65
+ ...sonar,
66
+ ...stylistic,
67
+ ...typescript,
68
+ ...unicorn,
69
+
70
+ ...pinia,
71
+ ...tanstackQuery,
72
+ ...unocss,
73
+ ...vitest,
74
+ ...vue,
75
+ ...vueAccessibility,
76
+ ...vueI18n,
77
+ ...zod,
78
+
79
+ // `tailwindcss` is the only plugin exported as a factory, because it needs a path to your CSS entrypoint
80
+ ...tailwindcss({ cssConfigPath: './src/assets/css/main.css' }),
81
+
82
+ ...prettierConfig, // must be last
17
83
 
18
- ```js
19
- // eslint.config.js
20
- import { defaultPreset } from '@alexlit/config-eslint';
84
+ // Custom rules example
85
+ // { files: ['**/*.js'], rules: { 'no-console': 'off' } },
86
+ ];
87
+ ```
88
+
89
+ Drop what you do not need.
90
+
91
+ ## Type-aware rules (optional)
92
+
93
+ Type-aware rules (`no-floating-promises`, `no-misused-promises`,
94
+ `await-thenable`, `no-unsafe-*`, `switch-exhaustiveness-check`,
95
+ `no-unnecessary-condition`, …) require type information, which makes linting
96
+ slower. They are not enabled by default.
97
+
98
+ To enable them, replace `typescript` with `typescriptTypeChecked`:
99
+
100
+ ```js
101
+ import {
102
+ base,
103
+ typescriptTypeChecked,
104
+ prettierConfig,
105
+ } from '@alexlit/config-eslint';
106
+
107
+ export default [
108
+ ...base,
109
+ // ...other plugins
110
+ ...typescriptTypeChecked, // instead of ...typescript
111
+ ...prettierConfig,
112
+ ];
113
+ ```
21
114
 
22
- export default [...defaultPreset];
23
- ```
115
+ > [!NOTE] `typescriptTypeChecked` needs a `tsconfig.json` in your project root
116
+ > (`projectService` is used, so keep both `tsconfig.json` and
117
+ > `tsconfig.node.json` etc. in a single root if needed). Use it instead of
118
+ > `typescript`, not alongside.
24
119
 
25
- - Custom rules
26
- ([available plugins](https://github.com/alex-lit/lint-kit/blob/master/packages/config-eslint/index.js))
120
+ ## Formatting: `prettier` vs `prettierConfig`
27
121
 
28
- ```js
29
- // eslint.config.js
30
- import { typescript, unicorn, vitest } from '@alexlit/config-eslint';
122
+ Both are included, and they are **not** interchangeable.
31
123
 
32
- // config example:
33
- export default [
34
- ...typescript,
35
- ...unicorn,
36
- ...vitest,
124
+ | Export | From | Effect |
125
+ | ---------------- | ------------------------ | ------------------------------------------------ |
126
+ | `prettierConfig` | `eslint-config-prettier` | only turns off formatting rules that conflict |
127
+ | `prettier` | `eslint-plugin-prettier` | additionally runs Prettier itself as a lint rule |
37
128
 
38
- ...[
39
- // <custom rules>
40
- ],
41
- ];
42
- ```
129
+ Use `prettierConfig` when Prettier runs as a separate step (editor, `prettier`
130
+ CLI, Husky). Use `prettier` if you want ESLint to report and fix formatting in
131
+ one pass — it is slower and duplicates the formatter.
43
132
 
44
133
  ## Usefull links
45
134
 
package/base.js ADDED
@@ -0,0 +1,68 @@
1
+ import { defineConfig } from 'eslint/config';
2
+ import globals from 'globals';
3
+
4
+ /** All source files covered by the shareable config */
5
+ export const FILES = ['**/*.{js,mjs,cjs,jsx,ts,tsx,mts,cts,vue}'];
6
+
7
+ /** TypeScript source files */
8
+ export const TS_FILES = ['**/*.{ts,tsx,mts,cts}'];
9
+
10
+ /** Vue.js single-file components */
11
+ export const VUE_FILES = ['**/*.vue'];
12
+
13
+ /** JSON files */
14
+ export const JSON_FILES = ['**/*.{json,jsonc,json5}'];
15
+
16
+ /** YAML files */
17
+ export const YAML_FILES = ['**/*.{yaml,yml}'];
18
+
19
+ /**
20
+ * Files that hold data rather than code.
21
+ *
22
+ * Plugins that ship configs without a `files` key would otherwise apply their
23
+ * rules to these files, so such blocks have to exclude them explicitly.
24
+ */
25
+ export const DATA_FILES = [...JSON_FILES, ...YAML_FILES];
26
+
27
+ /** Test files */
28
+ export const TEST_FILES = [
29
+ '**/*.{test,spec}.{js,cjs,mjs,jsx,ts,tsx,mts,cts,vue}',
30
+ ];
31
+
32
+ /** Type declaration files */
33
+ export const DTS_FILES = ['**/*.d.{ts,mts,cts}'];
34
+
35
+ /** Never linted */
36
+ export const IGNORES = [
37
+ '**/.*',
38
+ '**/.*/**',
39
+ '**/_deprecated/**',
40
+ '**/build/**',
41
+ '**/dist/**',
42
+ '**/docs/**',
43
+ '**/mockServiceWorker.js',
44
+ '**/storybook-*/**',
45
+ '**/sw.js',
46
+ ];
47
+
48
+ /**
49
+ * Base config.
50
+ *
51
+ * Provides the file scope, the shared ignore patterns and the global variables.
52
+ * Include it first, then the plugins you actually need.
53
+ *
54
+ * @example
55
+ * import { base, javascript, unicorn } from '@alexlit/config-eslint';
56
+ *
57
+ * export default [...base, ...javascript, ...unicorn];
58
+ */
59
+ export const base = defineConfig([
60
+ { files: FILES, languageOptions: { globals: globals.browser } },
61
+
62
+ // Node.js globals for commonjs/esm files
63
+ { files: ['**/*.{cjs,mjs}'], languageOptions: { globals: globals.node } },
64
+
65
+ { linterOptions: { reportUnusedDisableDirectives: 'error' } },
66
+
67
+ { ignores: IGNORES },
68
+ ]);
package/index.js CHANGED
@@ -1,4 +1,18 @@
1
+ export {
2
+ base,
3
+ DATA_FILES,
4
+ DTS_FILES,
5
+ FILES,
6
+ IGNORES,
7
+ JSON_FILES,
8
+ TEST_FILES,
9
+ TS_FILES,
10
+ VUE_FILES,
11
+ YAML_FILES,
12
+ } from './base.js';
13
+
1
14
  export { deMorgan } from './plugins/de-morgan.js';
15
+ export { importX } from './plugins/import-x.js';
2
16
  export { javascript } from './plugins/javascript.js';
3
17
  export { jsdoc } from './plugins/jsdoc.js';
4
18
  export { json } from './plugins/json.js';
@@ -10,6 +24,7 @@ export { sonar } from './plugins/sonar.js';
10
24
  export { stylistic } from './plugins/stylistic.js';
11
25
  export { tailwindcss } from './plugins/tailwindcss.js';
12
26
  export { tanstackQuery } from './plugins/tanstack-query.js';
27
+ export { typescriptTypeChecked } from './plugins/typescript-type-checked.js';
13
28
  export { typescript } from './plugins/typescript.js';
14
29
  export { unicorn } from './plugins/unicorn.js';
15
30
  export { unocss } from './plugins/unocss.js';
@@ -18,5 +33,5 @@ export { vueAccessibility } from './plugins/vue-accessibility.js';
18
33
  export { vueI18n } from './plugins/vue-i18n.js';
19
34
  export { vue } from './plugins/vue.js';
20
35
  export { zod } from './plugins/zod.js';
21
-
22
- export { defaultPreset } from './presets/default.js';
36
+ export { NO_RESTRICTED_SYNTAX } from './rules/restricted-syntax.js';
37
+ export { TS_RULES } from './rules/typescript.js';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@alexlit/config-eslint",
3
- "version": "157.0.0",
3
+ "version": "158.0.1",
4
4
  "private": false,
5
5
  "description": "Sharable ESLint configuration",
6
6
  "keywords": [
@@ -26,8 +26,9 @@
26
26
  "main": "./index.js",
27
27
  "files": [
28
28
  "index.js",
29
+ "base.js",
29
30
  "plugins/",
30
- "presets/",
31
+ "rules/",
31
32
  "README.md"
32
33
  ],
33
34
  "scripts": {
@@ -49,6 +50,7 @@
49
50
  "eslint": "^10.11.0",
50
51
  "eslint-config-prettier": "^10.1.8",
51
52
  "eslint-plugin-de-morgan": "^2.2.0",
53
+ "eslint-plugin-import-x": "^4.17.1",
52
54
  "eslint-plugin-jsdoc": "^65.0.0",
53
55
  "eslint-plugin-perfectionist": "^5.12.1",
54
56
  "eslint-plugin-pinia": "^0.4.2",
@@ -1,5 +1,9 @@
1
1
  import plugin from 'eslint-plugin-de-morgan';
2
2
  import { defineConfig } from 'eslint/config';
3
3
 
4
+ import { FILES } from '../base.js';
5
+
4
6
  /** @see [eslint-plugin-de-morgan](https://github.com/azat-io/eslint-plugin-de-morgan) */
5
- export const deMorgan = defineConfig([plugin.configs.recommended]);
7
+ export const deMorgan = defineConfig([
8
+ { files: FILES, ...plugin.configs.recommended },
9
+ ]);
@@ -0,0 +1,77 @@
1
+ import plugin from 'eslint-plugin-import-x';
2
+ import { createNodeResolver } from 'eslint-plugin-import-x/node-resolver';
3
+ import { defineConfig } from 'eslint/config';
4
+
5
+ import { DATA_FILES, FILES, TEST_FILES, VUE_FILES } from '../base.js';
6
+
7
+ const EXTENSIONS = [
8
+ '.ts',
9
+ '.tsx',
10
+ '.mts',
11
+ '.cts',
12
+ '.js',
13
+ '.jsx',
14
+ '.mjs',
15
+ '.cjs',
16
+ '.vue',
17
+ '.json',
18
+ ];
19
+
20
+ /** @see [eslint-plugin-import-x](https://github.com/un-ts/eslint-plugin-import-x) */
21
+ export const importX = defineConfig([
22
+ // `recommended` comes without a `files` key, so it would also land on data
23
+ // files, which are not modules and have nothing to import from.
24
+ { ...plugin.flatConfigs.recommended, files: FILES, ignores: DATA_FILES },
25
+
26
+ {
27
+ files: FILES,
28
+ languageOptions: { parserOptions: { ecmaFeatures: { jsx: true } } },
29
+ plugins: { 'import-x': plugin },
30
+ rules: {
31
+ 'import-x/consistent-type-specifier-style': ['error', 'prefer-top-level'],
32
+ 'import-x/first': 'error',
33
+ 'import-x/newline-after-import': 'warn',
34
+ 'import-x/no-absolute-path': 'error',
35
+ 'import-x/no-cycle': 'error',
36
+ 'import-x/no-extraneous-dependencies': 'error',
37
+ // Both come from `recommended`, but they are wrong for ESLint plugins:
38
+ // the default export is the plugin object, so `plugin.configs` is a
39
+ // property of it, not a named export of the package. Some packages even
40
+ // export `plugin` as a named export, which makes `no-named-as-default`
41
+ // fire on the conventional `import plugin from '…'` line.
42
+ 'import-x/no-named-as-default': 'off',
43
+ 'import-x/no-named-as-default-member': 'off',
44
+ 'import-x/no-nodejs-modules': 'off',
45
+ 'import-x/no-self-import': 'error',
46
+ 'import-x/no-useless-path-segments': ['error', { noUselessIndex: true }],
47
+ 'import-x/unambiguous': 'error',
48
+ },
49
+ settings: {
50
+ 'import-x/extensions': EXTENSIONS,
51
+ 'import-x/parsers': { '.vue': ['vue-eslint-parser'] },
52
+ // Reads `paths` from the closest `tsconfig.json`, so `~/…`, `@/…` and
53
+ // other aliases are resolved the same way the bundler resolves them.
54
+ 'import-x/resolver-next': [
55
+ createNodeResolver({ extensions: EXTENSIONS, tsconfig: 'auto' }),
56
+ ],
57
+ },
58
+ },
59
+ {
60
+ files: TEST_FILES,
61
+ rules: {
62
+ 'import-x/no-extraneous-dependencies': [
63
+ 'error',
64
+ { devDependencies: true },
65
+ ],
66
+ },
67
+ },
68
+
69
+ {
70
+ files: VUE_FILES,
71
+ rules: {
72
+ // A component without a `<script>` block is a valid component, not an
73
+ // ambiguous module that needs explicit imports.
74
+ 'import-x/unambiguous': 'off',
75
+ },
76
+ },
77
+ ]);
@@ -1,7 +1,9 @@
1
1
  import plugin from '@eslint/js';
2
2
  import { defineConfig } from 'eslint/config';
3
3
 
4
- import { FILES } from '../presets/base.js';
4
+ import { FILES, TEST_FILES } from '../base.js';
5
+ import { NO_RESTRICTED_SYNTAX } from '../rules/restricted-syntax.js';
6
+ import { STYLE_RULES } from '../rules/style.js';
5
7
 
6
8
  /** @see [eslint](https://eslint.org) */
7
9
  export const javascript = defineConfig([
@@ -10,11 +12,14 @@ export const javascript = defineConfig([
10
12
  files: FILES,
11
13
  rules: {
12
14
  'arrow-body-style': ['error', 'as-needed'],
13
- 'curly': 'error',
15
+ ...STYLE_RULES,
16
+ 'default-case-last': 'error',
14
17
  'dot-notation': ['error'],
15
- 'func-style': ['error', 'expression'],
16
18
  'grouped-accessor-pairs': ['error', 'getBeforeSet'],
19
+ 'no-alert': 'error',
20
+ 'no-console': 'error',
17
21
  'no-duplicate-imports': ['error', { includeExports: false }],
22
+ 'no-else-return': 'error',
18
23
  'no-implicit-coercion': 'error',
19
24
  'no-param-reassign': ['error', { props: false }],
20
25
  'no-restricted-exports': [
@@ -24,6 +29,14 @@ export const javascript = defineConfig([
24
29
  restrictedNamedExportsPattern: '^_',
25
30
  },
26
31
  ],
32
+ 'no-restricted-globals': [
33
+ 'error',
34
+ // `event`, `top` and `parent` are global browser properties.
35
+ // Reach for `window.event` / `window.top` / `window.parent` instead.
36
+ { message: 'Use `window.event` instead.', name: 'event' },
37
+ { message: 'Use `window.parent` instead.', name: 'parent' },
38
+ { message: 'Use `window.top` instead.', name: 'top' },
39
+ ],
27
40
  'no-restricted-imports': [
28
41
  'error',
29
42
  {
@@ -58,40 +71,21 @@ export const javascript = defineConfig([
58
71
  ],
59
72
  },
60
73
  ],
61
- 'no-restricted-syntax': [
62
- 'error',
63
- {
64
- message:
65
- "Не используйте префикс 'handle' для обработчиков событий. Переименуйте метод, заменив 'handle' на 'on' (например, 'onClick' вместо 'handleClick').",
66
- selector: 'Identifier[name=/^handle[A-Z]/]',
67
- },
68
- {
69
- message:
70
- "Используйте методы массивов (.forEach, .map и т.д.) вместо цикла 'for'.",
71
- selector: 'ForStatement',
72
- },
73
- {
74
- message:
75
- "Используйте методы объектов (.keys, .values и т.д.) вместо цикла 'for...in'.",
76
- selector: 'ForInStatement',
77
- },
78
- {
79
- message:
80
- "Используйте методы массивов(.forEach, .map и т.д.) вместо цикла 'for...of'.",
81
- selector: 'ForOfStatement',
82
- },
83
- ],
74
+ 'no-restricted-syntax': ['error', ...NO_RESTRICTED_SYNTAX],
75
+ 'no-useless-rename': 'error',
84
76
  'prefer-arrow-callback': [
85
77
  'error',
86
78
  { allowNamedFunctions: false, allowUnboundThis: false },
87
79
  ],
88
80
  'prefer-const': 'warn',
89
81
  'prefer-template': 'warn',
90
- 'quotes': [
91
- 'error',
92
- 'single',
93
- { allowTemplateLiterals: false, avoidEscape: true },
94
- ],
82
+ 'require-await': 'error',
95
83
  },
96
84
  },
85
+
86
+ {
87
+ // Tests are allowed to log and to alert
88
+ files: TEST_FILES,
89
+ rules: { 'no-alert': 'off', 'no-console': 'off' },
90
+ },
97
91
  ]);
package/plugins/jsdoc.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import plugin from 'eslint-plugin-jsdoc';
2
2
  import { defineConfig } from 'eslint/config';
3
3
 
4
- import { FILES } from '../presets/base.js';
4
+ import { FILES } from '../base.js';
5
5
 
6
6
  /** @see [eslint-plugin-jsdoc](https://github.com/gajus/eslint-plugin-jsdoc) */
7
7
  export const jsdoc = defineConfig([
@@ -11,20 +11,7 @@ export const jsdoc = defineConfig([
11
11
  rules: {
12
12
  'jsdoc/lines-before-block': 'off',
13
13
  'jsdoc/no-undefined-types': 'off',
14
- 'jsdoc/require-jsdoc': [
15
- 'warn',
16
- {
17
- enableFixer: false,
18
- require: {
19
- ArrowFunctionExpression: false,
20
- ClassDeclaration: false,
21
- ClassExpression: false,
22
- FunctionDeclaration: false,
23
- FunctionExpression: false,
24
- MethodDefinition: false,
25
- },
26
- },
27
- ],
14
+ 'jsdoc/require-jsdoc': 'off',
28
15
  'jsdoc/require-param-description': 'off',
29
16
  'jsdoc/require-param-type': 'off',
30
17
  'jsdoc/require-returns': 'off',
package/plugins/json.js CHANGED
@@ -1,5 +1,8 @@
1
1
  import plugin from '@eslint/json';
2
2
  import { defineConfig } from 'eslint/config';
3
+ import * as yamlParser from 'yaml-eslint-parser';
4
+
5
+ import { YAML_FILES } from '../base.js';
3
6
 
4
7
  /** @see [ESLint/JSON](https://github.com/eslint/json) */
5
8
  export const json = defineConfig([
@@ -15,6 +18,7 @@ export const json = defineConfig([
15
18
  extends: ['json/recommended'],
16
19
  files: ['**/*.jsonc'],
17
20
  language: 'json/jsonc',
21
+ languageOptions: { allowTrailingCommas: true },
18
22
  plugins: { json: plugin },
19
23
  },
20
24
 
@@ -24,4 +28,6 @@ export const json = defineConfig([
24
28
  language: 'json/json5',
25
29
  plugins: { json: plugin },
26
30
  },
31
+
32
+ { files: YAML_FILES, languageOptions: { parser: yamlParser } },
27
33
  ]);
@@ -1,7 +1,7 @@
1
1
  import plugin from 'eslint-plugin-perfectionist';
2
2
  import { defineConfig } from 'eslint/config';
3
3
 
4
- import { FILES } from '../presets/base.js';
4
+ import { FILES } from '../base.js';
5
5
 
6
6
  /** @see [eslint-plugin-perfectionist](https://perfectionist.dev) */
7
7
  export const perfectionist = defineConfig([
@@ -13,7 +13,7 @@ export const perfectionist = defineConfig([
13
13
  'warn',
14
14
  {
15
15
  customGroups: [
16
- // hash
16
+ // hash (nuxt)
17
17
  { elementNamePattern: ['^#'], groupName: 'hash' },
18
18
 
19
19
  // @@/
@@ -81,8 +81,11 @@ export const perfectionist = defineConfig([
81
81
 
82
82
  'type-internal',
83
83
  'value-internal',
84
- ['type-parent', 'type-sibling', 'type-index'],
85
- ['value-parent', 'value-sibling', 'value-index'],
84
+ // `parent` and `index` groups are omitted on purpose:
85
+ // `no-restricted-imports` in `javascript.js` forbids `..`, `.`
86
+ // and `**/index` imports, so nothing can ever land in them.
87
+ 'type-sibling',
88
+ 'value-sibling',
86
89
  'ts-equals-import',
87
90
  'unknown',
88
91
  ],
package/plugins/pinia.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import plugin from 'eslint-plugin-pinia';
2
2
  import { defineConfig } from 'eslint/config';
3
3
 
4
- import { FILES } from '../presets/base.js';
4
+ import { FILES } from '../base.js';
5
5
 
6
6
  /** @see [eslint-plugin-pinia](https://github.com/lisilinhart/eslint-plugin-pinia) */
7
7
  export const pinia = defineConfig([
@@ -2,7 +2,8 @@ import config from 'eslint-config-prettier';
2
2
  import plugin from 'eslint-plugin-prettier/recommended';
3
3
  import { defineConfig } from 'eslint/config';
4
4
 
5
- import { FILES } from '../presets/base.js';
5
+ import { FILES, VUE_FILES } from '../base.js';
6
+ import { STYLE_RULES } from '../rules/style.js';
6
7
 
7
8
  const fixes = {
8
9
  // fix for prettier
@@ -18,36 +19,12 @@ const fixes = {
18
19
 
19
20
  /** @see [eslint-plugin-prettier](https://github.com/prettier/eslint-plugin-prettier) */
20
21
  export const prettier = defineConfig([
21
- {
22
- files: FILES,
23
- ...plugin,
24
- rules: {
25
- ...plugin.rules,
26
- curly: 'error',
27
- quotes: [
28
- 'error',
29
- 'single',
30
- { allowTemplateLiterals: false, avoidEscape: true },
31
- ],
32
- },
33
- },
34
- { files: ['**/*.vue'], rules: { ...fixes } },
22
+ { files: FILES, ...plugin, rules: { ...plugin.rules, ...STYLE_RULES } },
23
+ { files: VUE_FILES, rules: { ...fixes } },
35
24
  ]);
36
25
 
37
26
  /** @see [eslint-config-prettier](https://github.com/prettier/eslint-config-prettier) */
38
27
  export const prettierConfig = defineConfig([
39
- {
40
- files: FILES,
41
- ...config,
42
- rules: {
43
- ...config.rules,
44
- curly: 'error',
45
- quotes: [
46
- 'error',
47
- 'single',
48
- { allowTemplateLiterals: false, avoidEscape: true },
49
- ],
50
- },
51
- },
52
- { files: ['**/*.vue'], rules: { ...fixes } },
28
+ { files: FILES, ...config, rules: { ...config.rules, ...STYLE_RULES } },
29
+ { files: VUE_FILES, rules: { ...fixes } },
53
30
  ]);
package/plugins/regexp.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import plugin from 'eslint-plugin-regexp';
2
2
  import { defineConfig } from 'eslint/config';
3
3
 
4
- import { FILES } from '../presets/base.js';
4
+ import { FILES } from '../base.js';
5
5
 
6
6
  /** @see [eslint-plugin-regexp](https://github.com/ota-meshi/eslint-plugin-regexp) */
7
7
  export const regexp = defineConfig([
package/plugins/sonar.js CHANGED
@@ -1,14 +1,18 @@
1
1
  import plugin from 'eslint-plugin-sonarjs';
2
2
  import { defineConfig } from 'eslint/config';
3
3
 
4
- import { FILES } from '../presets/base.js';
4
+ import { FILES } from '../base.js';
5
5
 
6
6
  /** @see [eslint-plugin-sonarjs](https://github.com/SonarSource/SonarJS/blob/master/packages/jsts/src/rules/README.md) */
7
7
  export const sonar = defineConfig([
8
- { files: FILES, ...plugin.configs.recommended },
9
8
  {
10
9
  files: FILES,
11
- plugins: { sonar: plugin },
12
- rules: { 'sonarjs/no-commented-code': 'warn', 'sonarjs/todo-tag': 'off' },
10
+ ...plugin.configs.recommended,
11
+ rules: {
12
+ ...plugin.configs.recommended.rules,
13
+ 'sonarjs/no-commented-code': 'warn',
14
+ 'sonarjs/todo-tag': 'off',
15
+ 'sonarjs/unused-import': 'off', // @typescript-eslint/no-unused-vars
16
+ },
13
17
  },
14
18
  ]);
@@ -1,18 +1,13 @@
1
1
  import plugin from '@stylistic/eslint-plugin';
2
2
  import { defineConfig } from 'eslint/config';
3
3
 
4
- import { FILES } from '../presets/base.js';
4
+ import { DATA_FILES, FILES } from '../base.js';
5
5
 
6
6
  /** @see [@stylistic/eslint-plugin](https://eslint.style/) */
7
7
  export const stylistic = defineConfig([
8
8
  {
9
9
  ...plugin.configs.recommended,
10
- ignores: [
11
- ...(plugin.configs.recommended.ignores ?? []),
12
- '**/*.json',
13
- '**/*.jsonc',
14
- '**/*.json5',
15
- ],
10
+ ignores: [...(plugin.configs.recommended.ignores ?? []), ...DATA_FILES],
16
11
  },
17
12
  {
18
13
  files: FILES,
@@ -1,7 +1,7 @@
1
1
  import plugin from 'eslint-plugin-tailwindcss';
2
2
  import { defineConfig } from 'eslint/config';
3
3
 
4
- import { FILES } from '../presets/base.js';
4
+ import { FILES } from '../base.js';
5
5
 
6
6
  /**
7
7
  * @example
@@ -1,8 +1,14 @@
1
1
  import plugin from '@tanstack/eslint-plugin-query';
2
2
  import { defineConfig } from 'eslint/config';
3
3
 
4
+ import { DATA_FILES } from '../base.js';
5
+
4
6
  /** @see [@tanstack/query](https://tanstack.com/query/latest/docs/eslint/eslint-plugin-query) */
5
7
  export const tanstackQuery = defineConfig([
6
- ...plugin.configs['flat/recommended'],
8
+ // The shipped config has no `files` key, so it would also cover data files.
9
+ ...plugin.configs['flat/recommended'].map((config) => ({
10
+ ...config,
11
+ ignores: [...(config.ignores ?? []), ...DATA_FILES],
12
+ })),
7
13
  { rules: { '@tanstack/query/infinite-query-property-order': 'off' } },
8
14
  ]);
@@ -0,0 +1,50 @@
1
+ import { defineConfig } from 'eslint/config';
2
+ import plugin from 'typescript-eslint';
3
+
4
+ import { DATA_FILES, FILES, TEST_FILES, TS_FILES } from '../base.js';
5
+ import { TS_RULES } from '../rules/typescript.js';
6
+
7
+ /**
8
+ * Type-aware TypeScript config.
9
+ *
10
+ * Opt-in: requires a `tsconfig.json` in the project and needs type information,
11
+ * which makes linting slower. Use it instead of `typescript`, not alongside
12
+ * it.
13
+ *
14
+ * @see [typescript-eslint](https://github.com/typescript-eslint/typescript-eslint)
15
+ */
16
+ export const typescriptTypeChecked = defineConfig([
17
+ // The parser is needed for every JS/TS/Vue file, not only for TypeScript ones,
18
+ // otherwise `.js` files fall back to the default parser and modern syntax
19
+ // (`import.meta`, top-level await) breaks.
20
+ { ...plugin.configs.base, files: FILES },
21
+
22
+ // Non type-aware rules — the same scope as the regular `typescript` plugin,
23
+ // so `.js`/`.vue` files keep the TypeScript versions of the rules.
24
+ // `plugin.configs.strict`/`stylistic` contain blocks without a `files` key,
25
+ // so they would also land on data files that are not JavaScript at all.
26
+ ...[...plugin.configs.strict, ...plugin.configs.stylistic].map((config) => ({
27
+ ...config,
28
+ ignores: [...(config.ignores ?? []), ...DATA_FILES],
29
+ })),
30
+
31
+ // Type-aware rules only make sense where the type information is available.
32
+ ...plugin.configs.strictTypeChecked.map((config) => ({
33
+ ...config,
34
+ files: TS_FILES,
35
+ })),
36
+ ...plugin.configs.stylisticTypeChecked.map((config) => ({
37
+ ...config,
38
+ files: TS_FILES,
39
+ })),
40
+
41
+ {
42
+ files: TS_FILES,
43
+ languageOptions: { parserOptions: { projectService: true } },
44
+ rules: TS_RULES,
45
+ },
46
+ {
47
+ files: TEST_FILES,
48
+ rules: { '@typescript-eslint/no-empty-function': 'off' },
49
+ },
50
+ ]);
@@ -1,48 +1,21 @@
1
- /* eslint-disable unicorn/no-null */
2
1
  import { defineConfig } from 'eslint/config';
3
2
  import plugin from 'typescript-eslint';
4
3
 
5
- import { FILES } from '../presets/base.js';
4
+ import { DATA_FILES, FILES, TEST_FILES } from '../base.js';
5
+ import { TS_RULES } from '../rules/typescript.js';
6
6
 
7
7
  /** @see [typescript-eslint](https://github.com/typescript-eslint/typescript-eslint) */
8
8
  export const typescript = defineConfig([
9
- ...plugin.configs.strict,
10
- ...plugin.configs.stylistic,
9
+ // `plugin.configs.strict`/`stylistic` contain blocks without a `files` key,
10
+ // so they would also land on data files that are not JavaScript at all.
11
+ ...[...plugin.configs.strict, ...plugin.configs.stylistic].map((config) => ({
12
+ ...config,
13
+ ignores: [...(config.ignores ?? []), ...DATA_FILES],
14
+ })),
11
15
 
16
+ { files: FILES, rules: TS_RULES },
12
17
  {
13
- files: FILES,
14
- rules: {
15
- '@typescript-eslint/naming-convention': [
16
- 'error',
17
- {
18
- format: ['strictCamelCase'],
19
- leadingUnderscore: 'allow',
20
- selector: 'function',
21
- },
22
- {
23
- format: ['strictCamelCase'],
24
- leadingUnderscore: 'require',
25
- modifiers: ['private'],
26
- selector: 'memberLike',
27
- },
28
- {
29
- format: ['strictCamelCase'],
30
- leadingUnderscore: 'allow',
31
- selector: 'parameter',
32
- },
33
- { format: ['StrictPascalCase'], selector: 'typeLike' },
34
- {
35
- format: ['strictCamelCase', 'PascalCase', 'UPPER_CASE'],
36
- leadingUnderscore: 'allow',
37
- selector: 'variable',
38
- },
39
- { format: null, modifiers: ['destructured'], selector: 'variable' },
40
- ],
41
- '@typescript-eslint/no-explicit-any': 'warn',
42
- },
43
- },
44
- {
45
- files: ['**/*.{fixtures,test,spec}.{js,ts}'],
18
+ files: TEST_FILES,
46
19
  rules: { '@typescript-eslint/no-empty-function': 'off' },
47
20
  },
48
21
  ]);
@@ -1,7 +1,7 @@
1
1
  import plugin from 'eslint-plugin-unicorn';
2
2
  import { defineConfig } from 'eslint/config';
3
3
 
4
- import { FILES } from '../presets/base.js';
4
+ import { DTS_FILES, FILES, VUE_FILES } from '../base.js';
5
5
 
6
6
  /** @see [eslint-plugin-unicorn](https://github.com/sindresorhus/eslint-plugin-unicorn) */
7
7
  export const unicorn = defineConfig([
@@ -18,6 +18,10 @@ export const unicorn = defineConfig([
18
18
  'error',
19
19
  { default: 'arrow-function' },
20
20
  ],
21
+ // Explicit: the default happens to be kebab case, but the intent is
22
+ // worth pinning down. Applies to `.vue` components too, which is why
23
+ // `vue/match-component-file-name` is kept in sync with it in `vue.js`.
24
+ 'unicorn/filename-case': ['error', { case: 'kebabCase' }],
21
25
  'unicorn/iteration-fallback-style': ['error', 'guard'],
22
26
  'unicorn/name-replacements': 'off',
23
27
  'unicorn/no-array-reduce': ['error', { allowSimpleOperations: true }],
@@ -42,8 +46,15 @@ export const unicorn = defineConfig([
42
46
  'unicorn/single-line-block-comment-style': 'off',
43
47
  },
44
48
  },
45
- { files: ['.*'], rules: { 'unicorn/no-null': 'off' } },
46
- { files: ['*.d.ts'], rules: { 'unicorn/prefer-export-from': 'off' } },
49
+ { files: ['**/.*'], rules: { 'unicorn/no-null': 'off' } },
50
+ { files: DTS_FILES, rules: { 'unicorn/prefer-export-from': 'off' } },
51
+ {
52
+ // `null` is part of the type contract in Vue.js SFC:
53
+ // `ref<Type | null>(null)` is the idiomatic way to declare an initially
54
+ // empty reactive value. Everywhere else `null` remains an error.
55
+ files: VUE_FILES,
56
+ rules: { 'unicorn/no-null': 'off' },
57
+ },
47
58
  {
48
59
  files: ['**/*.{api,endpoints,fixtures,schemas,service,spec,test}.{js,ts}'],
49
60
  rules: {
@@ -56,6 +67,6 @@ export const unicorn = defineConfig([
56
67
  },
57
68
  {
58
69
  files: ['**/temp/**/*.vue'],
59
- rules: { 'unicorn/prevent-abbreviations': 'off' },
70
+ rules: { 'unicorn/name-replacements': 'off' },
60
71
  },
61
72
  ]);
package/plugins/unocss.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import plugin from '@unocss/eslint-config/flat';
2
2
  import { defineConfig } from 'eslint/config';
3
3
 
4
- import { FILES } from '../presets/base.js';
4
+ import { FILES } from '../base.js';
5
5
 
6
6
  /** @see [@unocss/eslint-config](https://unocss.dev/integrations/eslint) */
7
7
  export const unocss = defineConfig([
package/plugins/vitest.js CHANGED
@@ -1,10 +1,12 @@
1
1
  import plugin from '@vitest/eslint-plugin';
2
2
  import { defineConfig } from 'eslint/config';
3
3
 
4
+ import { TEST_FILES } from '../base.js';
5
+
4
6
  /** @see [eslint-plugin-vitest](https://github.com/veritem/eslint-plugin-vitest) */
5
7
  export const vitest = defineConfig([
6
8
  {
7
- files: ['*.{test,spec}.{js,cjs,mjs,ts}'],
9
+ files: TEST_FILES,
8
10
  plugins: { vitest: plugin },
9
11
  rules: {
10
12
  ...plugin.configs.recommended.rules,
@@ -1,16 +1,13 @@
1
1
  import plugin from '@intlify/eslint-plugin-vue-i18n';
2
2
  import { defineConfig } from 'eslint/config';
3
3
 
4
+ import { DATA_FILES } from '../base.js';
5
+
4
6
  /** @see [eslint-plugin-vue-i18n](https://eslint-plugin-vue-i18n.intlify.dev/) */
5
7
  export const vueI18n = defineConfig([
6
8
  ...plugin.configs['flat/recommended'].map((config) => ({
7
9
  ...config,
8
- ignores: [
9
- ...(config.ignores ?? []),
10
- '**/*.json',
11
- '**/*.jsonc',
12
- '**/*.json5',
13
- ],
10
+ ignores: [...(config.ignores ?? []), ...DATA_FILES],
14
11
  })),
15
12
 
16
13
  {
package/plugins/vue.js CHANGED
@@ -2,20 +2,18 @@ import plugin from 'eslint-plugin-vue';
2
2
  import { defineConfig } from 'eslint/config';
3
3
  import typescript from 'typescript-eslint';
4
4
 
5
+ import { DATA_FILES, TEST_FILES, VUE_FILES } from '../base.js';
6
+ import { NO_RESTRICTED_SYNTAX } from '../rules/restricted-syntax.js';
7
+
5
8
  /** @see [eslint-plugin-vue](https://eslint.vuejs.org/rules/) */
6
9
  export const vue = defineConfig([
7
10
  ...plugin.configs['flat/recommended'].map((config) => ({
8
11
  ...config,
9
- ignores: [
10
- ...(config.ignores ?? []),
11
- '**/*.json',
12
- '**/*.jsonc',
13
- '**/*.json5',
14
- ],
12
+ ignores: [...(config.ignores ?? []), ...DATA_FILES],
15
13
  })),
16
14
 
17
15
  {
18
- files: ['**/*.vue'],
16
+ files: VUE_FILES,
19
17
  languageOptions: { parserOptions: { parser: typescript.parser } },
20
18
  rules: {
21
19
  '@typescript-eslint/no-empty-object-type': [
@@ -24,6 +22,8 @@ export const vue = defineConfig([
24
22
  ],
25
23
  'no-restricted-syntax': [
26
24
  'error',
25
+ // Merge the shared list, do not replace it
26
+ ...NO_RESTRICTED_SYNTAX,
27
27
  {
28
28
  message:
29
29
  'Запрещено использовать имя "Props". Используйте именованный формат, например: <ИмяКомпонента>Props.',
@@ -109,7 +109,14 @@ export const vue = defineConfig([
109
109
  svg: 'always',
110
110
  },
111
111
  ],
112
- 'vue/match-component-file-name': ['error'],
112
+ // Keep in sync with `unicorn/filename-case` in `unicorn.js`:
113
+ // components live in kebab-case files, so the component name is
114
+ // derived from a kebab-case file name instead of being required to be
115
+ // PascalCase in the file name itself.
116
+ 'vue/match-component-file-name': [
117
+ 'error',
118
+ { extensions: ['vue'], shouldMatchCase: false },
119
+ ],
113
120
  'vue/match-component-import-name': ['error'],
114
121
  'vue/max-attributes-per-line': ['error', { singleline: 3 }],
115
122
  'vue/max-lines-per-block': [
@@ -244,8 +251,5 @@ export const vue = defineConfig([
244
251
  rules: { 'vue/multi-word-component-names': 'off' },
245
252
  },
246
253
 
247
- {
248
- files: ['**/*.{test,spec}.{js,ts}'],
249
- rules: { 'vue/one-component-per-file': 'off' },
250
- },
254
+ { files: TEST_FILES, rules: { 'vue/one-component-per-file': 'off' } },
251
255
  ]);
package/plugins/zod.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import plugin from 'eslint-plugin-zod';
2
2
  import { defineConfig } from 'eslint/config';
3
3
 
4
- import { FILES } from '../presets/base.js';
4
+ import { FILES } from '../base.js';
5
5
 
6
6
  /** @see [eslint-plugin-zod](https://github.com/marcalexiei/eslint-zod/tree/main/plugins/eslint-plugin-zod) */
7
7
  export const zod = defineConfig([
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Shared restricted syntax rules.
3
+ *
4
+ * Keep it in a standalone module so that plugins which extend the list (e.g.
5
+ * `vue.js`) merge it instead of silently replacing it.
6
+ */
7
+ export const NO_RESTRICTED_SYNTAX = [
8
+ {
9
+ message:
10
+ "Не используйте префикс 'handle' для обработчиков событий. Переименуйте метод, заменив 'handle' на 'on' (например, 'onClick' вместо 'handleClick').",
11
+ selector: 'Identifier[name=/^handle[A-Z]/]',
12
+ },
13
+ {
14
+ message:
15
+ "Используйте методы массивов (.forEach, .map и т.д.) вместо цикла 'for'.",
16
+ selector: 'ForStatement',
17
+ },
18
+ {
19
+ message:
20
+ "Используйте методы объектов (.keys, .values и т.д.) вместо цикла 'for...in'.",
21
+ selector: 'ForInStatement',
22
+ },
23
+ {
24
+ message:
25
+ "Используйте методы массивов(.forEach, .map и т.д.) вместо цикла 'for...of'.",
26
+ selector: 'ForOfStatement',
27
+ },
28
+ ];
package/rules/style.js ADDED
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Formatting rules that survive `eslint-config-prettier`.
3
+ *
4
+ * `eslint-config-prettier` turns off every core formatting rule, so these two
5
+ * are declared in a single place and re-applied by `prettier.js` to both
6
+ * `prettier` and `prettierConfig`. Keep them in sync with `prettier.js`.
7
+ */
8
+ export const STYLE_RULES = {
9
+ curly: 'error',
10
+ quotes: [
11
+ 'error',
12
+ 'single',
13
+ { allowTemplateLiterals: false, avoidEscape: true },
14
+ ],
15
+ };
@@ -0,0 +1,62 @@
1
+ /* eslint-disable unicorn/no-null */
2
+
3
+ /**
4
+ * Shared TypeScript rules.
5
+ *
6
+ * `format: null` disables the format check for destructured variables — their
7
+ * names may be arbitrary.
8
+ */
9
+ export const TS_RULES = {
10
+ '@typescript-eslint/consistent-type-imports': [
11
+ 'error',
12
+ { fixStyle: 'separate-type-imports', prefer: 'type-imports' },
13
+ ],
14
+ '@typescript-eslint/naming-convention': [
15
+ 'error',
16
+ {
17
+ format: ['strictCamelCase'],
18
+ leadingUnderscore: 'allow',
19
+ selector: 'function',
20
+ },
21
+ {
22
+ format: ['strictCamelCase'],
23
+ leadingUnderscore: 'require',
24
+ modifiers: ['private'],
25
+ selector: 'memberLike',
26
+ },
27
+ {
28
+ format: ['strictCamelCase'],
29
+ leadingUnderscore: 'allow',
30
+ selector: 'parameter',
31
+ },
32
+ { format: ['StrictPascalCase'], selector: 'typeLike' },
33
+ {
34
+ format: ['strictCamelCase', 'PascalCase', 'UPPER_CASE'],
35
+ leadingUnderscore: 'allow',
36
+ selector: 'variable',
37
+ },
38
+ { format: null, modifiers: ['destructured'], selector: 'variable' },
39
+ {
40
+ format: ['strictCamelCase', 'PascalCase', 'UPPER_CASE'],
41
+ selector: 'enumMember',
42
+ },
43
+ {
44
+ format: ['strictCamelCase', 'UPPER_CASE'],
45
+ leadingUnderscore: 'allow',
46
+ selector: 'classProperty',
47
+ },
48
+ {
49
+ format: ['strictCamelCase', 'PascalCase', 'UPPER_CASE', 'snake_case'],
50
+ leadingUnderscore: 'allow',
51
+ selector: 'typeProperty',
52
+ },
53
+ {
54
+ format: ['strictCamelCase', 'PascalCase', 'UPPER_CASE'],
55
+ leadingUnderscore: 'allow',
56
+ selector: 'import',
57
+ },
58
+ ],
59
+ '@typescript-eslint/no-explicit-any': 'warn',
60
+ '@typescript-eslint/no-import-type-side-effects': 'error',
61
+ };
62
+ /* eslint-enable unicorn/no-null */
package/presets/base.js DELETED
@@ -1,22 +0,0 @@
1
- import { defineConfig } from 'eslint/config';
2
- import globals from 'globals';
3
-
4
- export const FILES = ['**/*.{js,mjs,cjs,jsx,ts,tsx,vue}'];
5
-
6
- /** Base config */
7
- export const base = defineConfig([
8
- { files: FILES, languageOptions: { globals: globals.browser } },
9
- {
10
- ignores: [
11
- '**/.*/',
12
- '**/_deprecated/',
13
- '**/mockServiceWorker.js',
14
- '.*/',
15
- 'build',
16
- 'dist',
17
- 'docs',
18
- 'storybook-*',
19
- 'sw.js',
20
- ],
21
- },
22
- ]);
@@ -1,37 +0,0 @@
1
- import { defineConfig } from 'eslint/config';
2
-
3
- import { deMorgan } from '../plugins/de-morgan.js';
4
- import { javascript } from '../plugins/javascript.js';
5
- import { jsdoc } from '../plugins/jsdoc.js';
6
- import { json } from '../plugins/json.js';
7
- import { perfectionist } from '../plugins/perfectionist.js';
8
- import { prettierConfig } from '../plugins/prettier.js';
9
- import { regexp } from '../plugins/regexp.js';
10
- import { sonar } from '../plugins/sonar.js';
11
- import { stylistic } from '../plugins/stylistic.js';
12
- import { typescript } from '../plugins/typescript.js';
13
- import { unicorn } from '../plugins/unicorn.js';
14
- import { vueAccessibility } from '../plugins/vue-accessibility.js';
15
- import { vueI18n } from '../plugins/vue-i18n.js';
16
- import { vue } from '../plugins/vue.js';
17
- import { base } from './base.js';
18
-
19
- export const defaultPreset = defineConfig([
20
- ...base, // must be first
21
-
22
- ...javascript,
23
- ...deMorgan,
24
- ...jsdoc,
25
- ...json,
26
- ...perfectionist,
27
- ...regexp,
28
- ...sonar,
29
- ...stylistic,
30
- ...typescript,
31
- ...unicorn,
32
- ...vue,
33
- ...vueAccessibility,
34
- ...vueI18n,
35
-
36
- ...prettierConfig, // must be last
37
- ]);