@tpzdsp/eslint-config-dsp
Version:
re-usable config for ESLint, Prettier and semantic-release
299 lines (287 loc) • 8.84 kB
JavaScript
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',
},
},
];