@@ -5,6 +5,7 @@ import * as tsParser from '@typescript-eslint/parser';
55import { Linter } from 'eslint' ;
66import checkFilePlugin from 'eslint-plugin-check-file' ;
77import * 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' ;
89import globals from 'globals' ;
910
1011type CustomizeOptions = {
@@ -34,7 +35,8 @@ type CustomizeOptions = {
3435
3536// shared settings - for js + ts equivalent rules
3637const 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 */
5557function 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,
0 commit comments