UNPKG

@tpzdsp/eslint-config-dsp

Version:

re-usable config for ESLint, Prettier and semantic-release

299 lines (287 loc) 8.84 kB
import eslintConfigPrettier from 'eslint-config-prettier/flat'; import { createTypeScriptImportResolver } from 'eslint-import-resolver-typescript'; import importX from 'eslint-plugin-import-x'; import jestDom from 'eslint-plugin-jest-dom'; import jsxA11y from 'eslint-plugin-jsx-a11y'; import prettier from 'eslint-plugin-prettier'; import react from 'eslint-plugin-react'; import reactHooks from 'eslint-plugin-react-hooks'; import { reactRefresh } from 'eslint-plugin-react-refresh'; import sonarjs from 'eslint-plugin-sonarjs'; import js from '@eslint/js'; import tsEslint from '@typescript-eslint/eslint-plugin'; import tsEslintParser from '@typescript-eslint/parser'; import vitest from '@vitest/eslint-plugin'; import paddingBeforeBlockStatement from './rules/padding-before-block-statement.js'; import paddingBetweenJsxElements from './rules/padding-between-jsx-elements.js'; export const NO_RESTRICTED_STYNAX_CONFIG = [ { selector: 'FunctionExpression', message: 'Use ES6 arrow functions instead.', }, { selector: 'FunctionDeclaration', message: 'Use arrow functions instead of function declarations.', }, { selector: 'ForStatement', message: 'Use functional iterators like .map(), .forEach(), or for...of instead of a manual for-loop.', }, { selector: 'ForInStatement', message: 'Avoid for...in loops as they iterate over inherited properties. Use Object.keys() or Object.entries() instead.', }, { selector: "ImportDeclaration[source.value='react'] > ImportDefaultSpecifier[local.name='React']", message: 'Default import of React is no longer necessary, and should not be used.', }, ]; const Types = { directive: 'directive', import: 'import', return: 'return', blockLike: 'block-like', variableDeclaration: ['const', 'let', 'var'], expression: 'expression', if: 'if', try: 'try', always: 'always', any: 'any', all: '*', const: 'const', let: 'let', var: 'var', }; const LINE_PADDING_RULES = [ // Always require blank line after directives like 'use strict' { blankLine: Types.always, prev: Types.directive, next: Types.all }, { blankLine: Types.any, prev: Types.directive, next: Types.directive }, // Always require blank line after import statements { blankLine: Types.always, prev: Types.import, next: Types.all }, { blankLine: Types.any, prev: Types.import, next: Types.import }, // Always before return { blankLine: Types.always, prev: Types.all, next: Types.return }, // Always before and after block-like statements (e.g. if, for, while, function declarations) { blankLine: Types.always, prev: Types.all, next: Types.blockLike }, { blankLine: Types.always, prev: Types.blockLike, next: Types.all }, // Always between variable declarations and other code { blankLine: Types.always, prev: [Types.const, Types.let, Types.var], next: Types.all }, { blankLine: Types.any, prev: [Types.const, Types.let, Types.var], next: [Types.const, Types.let, Types.var], }, // Optional: Blank lines before and after expressions like `expression`, `if`, etc. { blankLine: Types.always, prev: Types.all, next: Types.if }, { blankLine: Types.always, prev: Types.if, next: Types.all }, { blankLine: Types.always, prev: Types.all, next: Types.try }, { blankLine: Types.always, prev: Types.try, next: Types.all }, ]; export default [ { name: 'eslint', rules: { ...js.configs.recommended.rules, camelcase: 'off', 'no-var': 'error', 'no-restricted-syntax': ['error', ...NO_RESTRICTED_STYNAX_CONFIG], 'no-restricted-imports': [ 'error', { patterns: ['../../*'], // only allow `./` and `../` }, ], 'no-else-return': ['error', { allowElseIf: false }], 'no-lonely-if': 'error', 'padding-line-between-statements': ['error', ...LINE_PADDING_RULES], }, }, { name: 'custom', plugins: { custom: { rules: { 'padding-before-block-statement': paddingBeforeBlockStatement, 'padding-between-jsx-elements': paddingBetweenJsxElements, }, }, }, rules: { 'custom/padding-before-block-statement': 'error', 'custom/padding-between-jsx-elements': 'error', }, }, { name: 'sonarjs', plugins: { sonarjs, }, rules: { ...sonarjs.configs.recommended.rules, 'sonarjs/no-implicit-dependencies': 'error', 'sonarjs/no-duplicate-string': 'error', 'sonarjs/no-collapsible-if': 'error', 'sonarjs/prefer-immediate-return': 'error', }, }, { name: 'typescript', files: ['**/*.ts', '**/*.tsx'], languageOptions: { parser: tsEslintParser, parserOptions: { project: true, }, }, plugins: { '@typescript-eslint': tsEslint, }, rules: { ...tsEslint.configs.recommended.rules, ...tsEslint.configs['stylistic-type-checked'].rules, '@typescript-eslint/no-unused-vars': 'error', '@typescript-eslint/consistent-type-definitions': ['error', 'type'], '@typescript-eslint/naming-convention': [ 'error', { selector: 'default', format: ['camelCase'], }, { selector: 'import', format: ['camelCase', 'PascalCase'], }, { selector: 'function', format: ['camelCase', 'PascalCase'], }, { selector: 'typeLike', format: ['PascalCase'] }, { selector: 'typeProperty', format: null }, // allow any case { selector: 'enumMember', format: ['PascalCase'] }, { selector: 'variable', format: ['camelCase', 'PascalCase', 'UPPER_CASE'], }, { selector: 'objectLiteralProperty', format: null, // allow any case }, { selector: 'objectLiteralMethod', format: ['camelCase', 'PascalCase'], }, ], }, }, { name: 'react-and-a11y', files: ['**/*.js', '**/*.jsx', '**/*.ts', '**/*.tsx'], plugins: { react, 'jsx-a11y': jsxA11y, import: importX, prettier, 'react-hooks': reactHooks, 'react-refresh': reactRefresh.plugin, }, rules: { ...react.configs.recommended.rules, ...jsxA11y.configs.strict.rules, 'react-hooks/rules-of-hooks': 'error', 'react-hooks/exhaustive-deps': 'warn', 'import/order': [ 'error', { groups: [ 'builtin', // Split external into unscoped and scoped manually 'external', 'internal', // Absolute imports from the same project ['parent', 'sibling', 'index'], // Relative imports 'object', // TypeScript Type imports ], 'newlines-between': 'always', alphabetize: { order: 'asc', caseInsensitive: true }, pathGroups: [ { // Match scoped packages and move them after unscoped pattern: '@?*/**', group: 'external', position: 'after', }, { pattern: 'react', group: 'builtin', position: 'before', }, { pattern: '@/**', // Project root alias group: 'internal', position: 'before', }, ], pathGroupsExcludedImportTypes: ['builtin'], }, ], 'prettier/prettier': [ 'error', { endOfLine: 'auto', }, ], 'react/react-in-jsx-scope': 'off', 'react/jsx-pascal-case': ['error', { allowNamespace: true, allowAllCaps: true }], }, settings: { react: { version: 'detect', }, 'import-x/resolver-next': [ createTypeScriptImportResolver({ alwaysTryTypes: true, }), ], 'import-x/internal-regex': '^@/', }, }, { name: 'testing', files: [ '**/*.test.js', '**/*.test.jsx', '**/*.test.ts', '**/*.test.tsx', '**/__tests__/**/*.js', '**/__tests__/**/*.jsx', '**/__tests__/**/*.ts', '**/__tests__/**/*.tsx', ], plugins: { vitest, 'jest-dom': jestDom, }, rules: { ...vitest.configs.recommended.rules, 'vitest/padding-around-describe-blocks': 'error', 'vitest/padding-around-test-blocks': 'error', 'jest-dom/prefer-checked': 'warn', 'jest-dom/prefer-enabled-disabled': 'warn', 'jest-dom/prefer-required': 'warn', }, }, { name: 'ignores', files: ['**/*.ts', '**/*.tsx'], ignores: ['node_modules/', 'dist/', 'build/'], }, { name: 'prettier', ...eslintConfigPrettier, rules: { curly: 'error', }, }, ];