Skip to content

Commit 06b77c8

Browse files
authored
Merge pull request #9 from ovos/eslint-v9
eslint v9
2 parents 4996106 + 1e94414 commit 06b77c8

File tree

3 files changed

+88
-86
lines changed

3 files changed

+88
-86
lines changed

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,8 @@ The configuration is based on the recommended rulesets from [ESLint](https://esl
2222

2323
It also includes [ESLint Stylistic](https://eslint.style/) which replaces deprecated rules from eslint and typescript-eslint.
2424

25-
Currently, it uses ESLint v8 and typescript-eslint v6. (nodejs v16.10+ required)
26-
The upgrade to ESLint v9 and typescript-eslint v8 is planned for the next major release.
25+
Currently, it uses ESLint v9 and typescript-eslint v8. (nodejs v18.18+ required)
26+
If you need to use this package on older nodejs, you can try the v2.x version, which is based on ESLint v8 (nodejs v16.10+ required)
2727

2828

2929
`@ovos-media/coding-standard/eslint` exports a function that accepts an object with the following options:

eslint.ts

Lines changed: 62 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import * as tsParser from '@typescript-eslint/parser';
55
import { Linter } from 'eslint';
66
import checkFilePlugin from 'eslint-plugin-check-file';
77
import * as importPlugin from 'eslint-plugin-import'; // aliased to eslint-plugin-import-x https://github.com/un-ts/eslint-plugin-import-x
8+
import perfectionist from 'eslint-plugin-perfectionist';
89
import globals from 'globals';
910

1011
type CustomizeOptions = {
@@ -34,7 +35,8 @@ type CustomizeOptions = {
3435

3536
// shared settings - for js + ts equivalent rules
3637
const shared: Linter.RulesRecord = {
37-
'no-unused-vars': ['error', { varsIgnorePattern: '^_', args: 'none' }],
38+
'no-unused-expressions': ['error', { allowShortCircuit: true, allowTernary: true }],
39+
'no-unused-vars': ['error', { varsIgnorePattern: '^_', args: 'none', caughtErrors: 'none' }],
3840
};
3941

4042
/**
@@ -50,7 +52,7 @@ const shared: Linter.RulesRecord = {
5052
* @param {boolean} [options.mocha=false] - Whether to enable Mocha-specific rules.
5153
* @param {boolean} [options.react=false] - Whether to enable React-specific rules.
5254
* @param {boolean} [options.vitest=false] - Whether to enable Vitest-specific rules.
53-
* @returns {import('eslint').Linter.FlatConfig[]} requires @types/eslint to be installed for FlatConfig type to appear on Linter - https://stackoverflow.com/a/75684357/3729316
55+
* @returns {import('eslint').Linter.Config[]}
5456
*/
5557
function customize(options: CustomizeOptions = {}) {
5658
const {
@@ -65,7 +67,7 @@ function customize(options: CustomizeOptions = {}) {
6567
} = options;
6668
const consoleUsage = options.console ?? (react ? 'ban-log' : 'allow');
6769

68-
const config: Linter.FlatConfig[] = [
70+
const config: Linter.Config[] = [
6971
// common settings for all files
7072
{
7173
ignores: ['node_modules', 'build', 'coverage', '.yalc', 'vite.config.ts.*'],
@@ -81,6 +83,21 @@ function customize(options: CustomizeOptions = {}) {
8183
plugins: {
8284
// https://eslint.style/ providing replacement for formatting rules, which are now deprecated in eslint and @typescript-eslint
8385
'@stylistic': { rules: stylistic.rules },
86+
perfectionist,
87+
},
88+
settings: {
89+
// https://perfectionist.dev/guide/getting-started#settings
90+
perfectionist: {
91+
type: 'custom',
92+
ignoreCase: false,
93+
// 'perfectionist' uses .localeCompare() which by default which sorts '123..AaBbCc..'
94+
// we want to put uppercase before lowercase, the rest stays the same (esp. symbols)
95+
// Alphabet from 'perfectionist' could be used, such as
96+
// `Alphabet.generateRecommendedAlphabet().sortByNaturalSort('en-US').placeAllWithCaseBeforeAllWithOtherCase('uppercase').getCharacters()`
97+
// but that contains 128k chars, which is unnecessarily large
98+
// so recreated only the needed part of the alphabet, with uppercase before lowercase
99+
alphabet: '_-.@/#~$0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz',
100+
},
84101
},
85102
},
86103
// common settings for typescript files
@@ -91,13 +108,13 @@ function customize(options: CustomizeOptions = {}) {
91108
parser: tsParser,
92109
parserOptions: {
93110
// https://typescript-eslint.io/packages/parser/
94-
project: true,
111+
projectService: true,
95112
},
96113
},
97114
plugins: {
98115
'@typescript-eslint': { rules: tsPlugin.rules as any },
99116
import: importPlugin,
100-
} satisfies Linter.FlatConfig['plugins'],
117+
} satisfies Linter.Config['plugins'],
101118
settings: {
102119
// `import-x` prefix is hardcoded in `eslint-plugin-import-x` to read its settings, ignores the alias defined in `plugins`
103120
'import-x/resolver': { typescript: { alwaysTryTypes: true } },
@@ -121,7 +138,7 @@ function customize(options: CustomizeOptions = {}) {
121138
'*.setup.ts',
122139
...disableTypeChecked,
123140
],
124-
languageOptions: { parserOptions: { project: null } }, // this is what basically the 'disable-type-checked' config does, when 'recommended-type-checked' is not used
141+
languageOptions: { parserOptions: { projectService: null } }, // this is what basically the 'disable-type-checked' config does, when 'recommended-type-checked' is not used
125142
},
126143
];
127144

@@ -132,6 +149,7 @@ function customize(options: CustomizeOptions = {}) {
132149
files: ['**/*.?(m|c)js?(x)'],
133150
rules: {
134151
// ** eslint:recommended overrides:
152+
'no-unused-expressions': shared['no-unused-expressions'],
135153
'no-unused-vars': shared['no-unused-vars'],
136154
// ** end eslint:recommended overrides
137155

@@ -158,10 +176,17 @@ function customize(options: CustomizeOptions = {}) {
158176
'@typescript-eslint/no-explicit-any': 'off',
159177
// allow @ts-ignore
160178
'@typescript-eslint/ban-ts-comment': 'off',
161-
// even though `no-unused-vars` is already reconfigured, this needs to be reconfigured again for typescript files, with the same options repeated
162-
'@typescript-eslint/no-unused-vars': shared['no-unused-vars'],
163179
// we still sometimes want to use dynamic, sync `require()` instead of `await import()`
164-
'@typescript-eslint/no-var-requires': 'off',
180+
'@typescript-eslint/no-require-imports': 'off',
181+
// allow `interface I extends Base<Param> {}` syntax
182+
'@typescript-eslint/no-empty-object-type': [
183+
'error',
184+
{ allowInterfaces: 'with-single-extends' },
185+
],
186+
// even though the rules blow are already reconfigured for eslint:recommended,
187+
// they need to be reconfigured again for typescript files, with the same options repeated
188+
'@typescript-eslint/no-unused-expressions': shared['no-unused-expressions'],
189+
'@typescript-eslint/no-unused-vars': shared['no-unused-vars'],
165190
// ** end typescript-eslint:recommended overrides
166191

167192
// additional rules
@@ -248,50 +273,30 @@ function customize(options: CustomizeOptions = {}) {
248273
indent,
249274
{
250275
SwitchCase: 1,
251-
// indenting parameters on multiline function calls is sometimes broken
252-
CallExpression: { arguments: 'off' },
253-
// @typescript-eslint/indent is broken and unmaintained,
254-
// but there is no other, better option available at the moment. (except the prettier itself?)
255-
// Eslint cuts ties with stylistic lints while leaving them in broken state
256-
// https://github.com/eslint/eslint/issues/17522
257-
// Maybe it'll be fixed one day at https://github.com/eslint-stylistic/eslint-stylistic/ 🙄
258-
//
259-
// Apply workarounds posted in https://github.com/typescript-eslint/typescript-eslint/issues/1824 et al.
276+
// only enable when 'indent' is 2 spaces, as it's broken otherwise -> https://github.com/eslint-stylistic/eslint-stylistic/issues/514
277+
offsetTernaryExpressions: indent === 2,
260278
ignoredNodes: [
261-
// https://github.com/typescript-eslint/typescript-eslint/issues/1824#issuecomment-1378327382
262-
'PropertyDefinition[decorators]',
263-
'FunctionExpression[params]:has(Identifier[decorators])',
279+
// copied list of ignoredNodes from https://github.com/eslint-stylistic/eslint-stylistic/blob/main/packages/eslint-plugin/configs/customize.ts
280+
// which just disables indent rules for cases not properly supported by the plugin
281+
// (issues have been carried over from the original indent and @typescript-eslint/indent rules
282+
// and now are being addressed occasionally, one by one, in eslint-stylistic)
264283
'TSUnionType',
265284
'TSIntersectionType',
266-
// https://github.com/typescript-eslint/typescript-eslint/issues/1824#issuecomment-943783564
267-
// Generics are not properly indented
268285
'TSTypeParameterInstantiation',
286+
'FunctionExpression > .params[decorators.length > 0]',
287+
'FunctionExpression > .params > :matches(Decorator, :not(:first-child))',
288+
// some more exclusions are needed:
289+
// does not indent multiline interface extends (conflicts with prettier)
269290
'TSInterfaceHeritage',
270-
// checking indentation of multiline ternary expression is broken
271-
// https://github.com/eslint/eslint/issues/14058
291+
'.superTypeArguments',
292+
// multiline generic type parameters in function calls
293+
'CallExpression > .typeArguments',
294+
// checking indentation of multiline ternary expression is broken in some cases (i.a. nested function calls)
272295
'ConditionalExpression *',
273-
// breaking on nested arrow functions
296+
// still breaking on nested (chained) arrow functions () => () => {}
274297
'ArrowFunctionExpression',
275298
// https://stackoverflow.com/questions/52178093/ignore-the-indentation-in-a-template-literal-with-the-eslint-indent-rule
276-
'TemplateLiteral *',
277-
// ignore jsx indentation - copied from https://github.com/eslint-stylistic/eslint-stylistic/blob/main/packages/eslint-plugin/configs/customize.ts
278-
// use jsx-indent rule instead
279-
'JSXElement',
280-
'JSXElement > *',
281-
'JSXAttribute',
282-
'JSXIdentifier',
283-
'JSXNamespacedName',
284-
'JSXMemberExpression',
285-
'JSXSpreadAttribute',
286-
'JSXExpressionContainer',
287-
'JSXOpeningElement',
288-
'JSXClosingElement',
289-
'JSXFragment',
290-
'JSXOpeningFragment',
291-
'JSXClosingFragment',
292-
'JSXText',
293-
'JSXEmptyExpression',
294-
'JSXSpreadChild',
299+
'TemplateLiteral *', // even after some fixes in @stylistic, still not handling multiline expressions in template literals properly
295300
],
296301
},
297302
],
@@ -369,6 +374,11 @@ function customize(options: CustomizeOptions = {}) {
369374
'no-param-reassign': 'error',
370375
'object-shorthand': 'error',
371376
'one-var': ['error', 'never'],
377+
'perfectionist/sort-named-exports': ['error', { groupKind: 'types-first' }],
378+
'perfectionist/sort-named-imports': [
379+
'error',
380+
{ ignoreAlias: true, groupKind: 'types-first' },
381+
],
372382
'prefer-arrow-callback': ['error', { allowNamedFunctions: true }],
373383
...(consoleUsage !== 'allow' && {
374384
'no-console':
@@ -390,9 +400,9 @@ function customize(options: CustomizeOptions = {}) {
390400
rules: {
391401
// allow i.a. `type Props = {}` in react components
392402
// https://github.com/typescript-eslint/typescript-eslint/issues/2063#issuecomment-675156492
393-
'@typescript-eslint/ban-types': [
403+
'@typescript-eslint/no-empty-object-type': [
394404
'error',
395-
{ extendDefaults: true, types: { '{}': false } },
405+
{ allowInterfaces: 'with-single-extends', allowWithName: 'Props$' },
396406
],
397407
},
398408
},
@@ -429,7 +439,6 @@ function customize(options: CustomizeOptions = {}) {
429439
'@stylistic/jsx-equals-spacing': 'error',
430440
'@stylistic/jsx-first-prop-new-line': 'error',
431441
'@stylistic/jsx-function-call-newline': 'error',
432-
'@stylistic/jsx-indent': ['error', indent],
433442
'@stylistic/jsx-indent-props': ['error', indent],
434443
'@stylistic/jsx-props-no-multi-spaces': 'error',
435444
'@stylistic/jsx-quotes': 'error',
@@ -492,7 +501,7 @@ function customize(options: CustomizeOptions = {}) {
492501
],
493502
languageOptions: {
494503
globals: {
495-
...globals.jest,
504+
...jestPlugin.environments.globals.globals,
496505
DB: 'readonly',
497506
GQL: 'readonly',
498507
Setup: 'readonly',
@@ -503,7 +512,6 @@ function customize(options: CustomizeOptions = {}) {
503512
jest: jestPlugin,
504513
},
505514
rules: {
506-
// flat config is not supported by eslint-plugin-jest yet - https://github.com/jest-community/eslint-plugin-jest/issues/1408
507515
...jestPlugin.configs.recommended.rules,
508516
// the recommended set is too strict for us. Disable rules which we do not want.
509517
// https://github.com/jest-community/eslint-plugin-jest#rules
@@ -540,7 +548,7 @@ function customize(options: CustomizeOptions = {}) {
540548
}
541549

542550
if (vitest) {
543-
const vitestPlugin = require('eslint-plugin-vitest');
551+
const vitestPlugin = require('@vitest/eslint-plugin');
544552
config.push({
545553
name: 'vitest',
546554
files: [
@@ -572,18 +580,13 @@ function customize(options: CustomizeOptions = {}) {
572580
const mochaPlugin = require('eslint-plugin-mocha');
573581
config.push(
574582
{
583+
...mochaPlugin.configs.flat.recommended,
575584
name: 'mocha',
576585
files: [
577586
`${testsDir}/**/*.?(c|m)[jt]s`,
578587
'**/__tests__/**/*.?(c|m)[jt]s',
579588
'**/*.{spec,test}.?(c|m)[jt]s',
580589
],
581-
languageOptions: {
582-
globals: globals.mocha,
583-
},
584-
plugins: {
585-
mocha: mochaPlugin,
586-
},
587590
rules: {
588591
// https://github.com/lo1tuma/eslint-plugin-mocha#rules
589592
...mochaPlugin.configs.flat.recommended.rules,
@@ -607,20 +610,20 @@ function customize(options: CustomizeOptions = {}) {
607610
rules: {
608611
'mocha/no-exports': 'off',
609612
},
610-
},
613+
}
611614
);
612615
}
613616

614617
if (cypress) {
615618
const cypressPlugin = require('eslint-plugin-cypress/flat');
616619
config.push({
620+
...cypressPlugin.configs.recommended,
617621
name: 'cypress',
618622
files: [
619623
`${testsDir}/**/*.?(c|m)[jt]s`,
620624
'**/__tests__/**/*.?(c|m)[jt]s',
621625
'**/*.{spec,test}.?(c|m)[jt]s',
622626
],
623-
...cypressPlugin.configs.recommended,
624627
rules: {
625628
...cypressPlugin.configs.recommended.rules,
626629
// Even though cypress is based on mocha, and uses `this` in regular functions to access the test context,

package.json

Lines changed: 24 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@ovos-media/coding-standard",
3-
"version": "2.0.2",
3+
"version": "3.0.0-rc.3",
44
"description": "ovos-media coding standard",
55
"main": "index.js",
66
"types": "index.d.ts",
@@ -9,38 +9,37 @@
99
"repository": "https://github.com/ovos/coding-standard",
1010
"homepage": "https://github.com/ovos/coding-standard",
1111
"scripts": {
12-
"prepublish": "yarn run build",
13-
"lint": "yarn run eslint .",
14-
"build": "tsc -p ./tsconfig.json",
15-
"release": "standard-version"
12+
"prepublish": "npm run build",
13+
"lint": "eslint .",
14+
"build": "tsc -p ./tsconfig.json"
1615
},
1716
"dependencies": {
18-
"@stylistic/eslint-plugin": "^1.8.1",
19-
"@types/eslint": "8.56.10",
20-
"@typescript-eslint/eslint-plugin": "^6.21.0",
21-
"@typescript-eslint/parser": "^6.21.0",
22-
"eslint": "^8.57.0",
23-
"eslint-import-resolver-typescript": "^3.6.1",
24-
"eslint-plugin-check-file": "2.6.2",
25-
"eslint-plugin-cypress": "^3.3.0",
26-
"eslint-plugin-import": "npm:eslint-plugin-import-x@0.2.0",
27-
"eslint-plugin-jest": "28.4.0",
28-
"eslint-plugin-mocha": "^10.4.3",
29-
"eslint-plugin-react": "^7.34.2",
30-
"eslint-plugin-react-hooks": "^4.6.2",
31-
"eslint-plugin-vitest": "0.3.10",
32-
"globals": "^13.24.0"
17+
"@stylistic/eslint-plugin": "^2.12.1",
18+
"@typescript-eslint/eslint-plugin": "^8.19.1",
19+
"@typescript-eslint/parser": "^8.19.1",
20+
"@typescript-eslint/utils": "^8.19.1",
21+
"@vitest/eslint-plugin": "^1.1.24",
22+
"eslint": "^9.17.0",
23+
"eslint-import-resolver-typescript": "^3.7.0",
24+
"eslint-plugin-check-file": "^2.8.0",
25+
"eslint-plugin-cypress": "^4.1.0",
26+
"eslint-plugin-import": "npm:eslint-plugin-import-x@^4.6.1",
27+
"eslint-plugin-jest": "^28.10.0",
28+
"eslint-plugin-mocha": "^10.5.0",
29+
"eslint-plugin-perfectionist": "^4.6.0",
30+
"eslint-plugin-react": "^7.37.3",
31+
"eslint-plugin-react-hooks": "^5.1.0",
32+
"globals": "^15.14.0"
3333
},
3434
"devDependencies": {
3535
"@types/eslint-plugin-mocha": "10.4.0",
36-
"@types/eslint__js": "8.42.3",
37-
"@types/node": "20.14.1",
36+
"@types/node": "22.10.5",
3837
"@types/prettier": "3.0.0",
39-
"prettier": "3.3.0",
40-
"typescript": "5.4.5"
38+
"prettier": "3.4.2",
39+
"typescript": "5.7.2"
4140
},
4241
"engines": {
43-
"node": ">=16.10"
42+
"node": ">=18.18"
4443
},
4544
"files": [
4645
"/eslint.js",

0 commit comments

Comments
 (0)