@slhs/eslint-config
Version:
373 lines (356 loc) • 11.3 kB
JavaScript
//
// Forked from https://github.com/epicweb-dev/config
//
import globals from 'globals';
import eslint from '@eslint/js';
import tseslint from 'typescript-eslint';
import reactPlugin from 'eslint-plugin-react';
import jsxA11yPlugin from 'eslint-plugin-jsx-a11y';
import playwright from 'eslint-plugin-playwright';
const ERROR = 'error';
const WARN = 'warn';
const OFF = 'off';
function has(pkgName) {
try {
import.meta.resolve(pkgName, import.meta.url);
return true;
} catch {
return false;
}
}
const hasTypeScript = has('typescript');
const hasReact = has('react');
const hasTestingLibrary = has('@testing-library/dom');
const hasJestDom = has('@testing-library/jest-dom');
const hasVitest = has('vitest');
const hasPlaywright = has('@playwright/test');
const vitestFiles = ['**/__tests__/**/*', '**/*.test.*'];
const testFiles = ['**/tests/**', '**/#tests/**', ...vitestFiles];
const allPlaywrightFiles = ['**/playwright/**'];
const playwrightTestFiles = ['**/playwright/**/*.spec.*'];
export const config = [
{
ignores: [
'**/.cache/**',
'**/node_modules/**',
'**/build/**',
'**/public/build/**',
'**/playwright-report/**',
'**/playwright-results/**',
'**/playwright/report/**',
'**/playwright/results/**',
'**/server-build/**',
'**/dist/**',
'**/coverage/**',
'**/.react-router/**',
'**/prisma-client/**',
'**/prisma-generated/**',
],
},
// all files
eslint.configs.recommended,
{
plugins: {
import: (await import('eslint-plugin-import-x')).default,
},
languageOptions: {
globals: {
...globals.browser,
...globals.node,
},
},
rules: {
'import/no-duplicates': [WARN, { 'prefer-inline': true }],
'import/order': [
WARN,
{
pathGroups: [
{ pattern: '#*/**', group: 'internal' },
{ pattern: '~/**', group: 'internal' },
],
groups: ['builtin', 'external', 'internal', 'parent', 'sibling', 'index'],
},
],
'no-unneeded-ternary': WARN,
},
},
// JSX/TSX files
hasReact
? {
files: ['**/*.tsx', '**/*.jsx'],
plugins: {
react: reactPlugin,
'jsx-a11y': jsxA11yPlugin,
},
languageOptions: {
parserOptions: {
ecmaFeatures: {
jsx: true,
},
},
},
rules: {
...reactPlugin.configs.recommended.rules,
...jsxA11yPlugin.configs.recommended.rules,
'react/react-in-jsx-scope': OFF,
'react/prop-types': OFF,
'jsx-a11y/label-has-associated-control': [
ERROR,
{
assert: 'either',
},
],
'jsx-a11y/no-autofocus': OFF,
},
settings: {
react: {
version: 'detect',
},
},
}
: null,
// react-hook rules are applicable in ts/js/tsx/jsx, but only with React as a dep
hasReact
? {
files: ['**/*.ts?(x)', '**/*.js?(x)'],
ignores: [...allPlaywrightFiles],
plugins: {
'react-hooks': (await import('eslint-plugin-react-hooks')).default,
},
rules: {
'react-hooks/rules-of-hooks': ERROR,
'react-hooks/exhaustive-deps': ERROR,
},
}
: null,
// JS, JSX, and CJS files
{
files: ['**/*.{js,jsx,cjs}'],
// most of these rules are useful for JS but not TS because TS handles these better
rules: {
// Blocked by https://github.com/import-js/eslint-plugin-import/issues/2132
// 'import/no-unresolved': [
// ERROR,
// {
// ignore: [
// '^~/styles/bootstrap.css',
// '^#icons/icon',
// '^~/icons/icon',
// './icons-sprite.svg',
// ],
// },
// ],
'no-unused-vars': [
WARN,
{
args: 'after-used',
argsIgnorePattern: '^_',
ignoreRestSiblings: true,
varsIgnorePattern: '^ignored',
},
],
},
},
// TS and TSX files
...(hasTypeScript
? [
// TODO: figure out how to switch to type-checked configs
...tseslint.configs.recommended,
...tseslint.configs.stylistic,
{
files: ['**/*.ts?(x)'],
languageOptions: {
parserOptions: {
parser: tseslint.parser,
projectService: true,
},
},
plugins: {
'@typescript-eslint': tseslint.plugin,
},
rules: {
'@typescript-eslint/ban-ts-comment': OFF,
'@typescript-eslint/consistent-type-assertions': [
ERROR,
{
assertionStyle: 'as',
objectLiteralTypeAssertions: 'allow-as-parameter',
},
],
'@typescript-eslint/consistent-type-definitions': OFF,
// Note: use this rule _OR_ verbatimModuleSyntax in tsconfig.json - not both
// '@typescript-eslint/consistent-type-imports': [
// ERROR,
// {
// prefer: 'type-imports',
// disallowTypeAnnotations: true,
// fixStyle: 'inline-type-imports',
// },
// ],
'@typescript-eslint/explicit-module-boundary-types': OFF,
'@typescript-eslint/naming-convention': [
ERROR,
{
selector: 'typeLike',
format: ['PascalCase'],
custom: { regex: '^I[A-Z]', match: false },
},
],
'@typescript-eslint/no-confusing-void-expression': [
ERROR,
{
ignoreArrowShorthand: true,
},
],
'@typescript-eslint/no-explicit-any': OFF,
'@typescript-eslint/no-floating-promises': [
ERROR,
{
ignoreIIFE: true,
},
],
'@typescript-eslint/no-import-type-side-effects': ERROR,
'no-invalid-this': OFF,
'@typescript-eslint/no-invalid-this': ERROR,
'no-redeclare': OFF,
'@typescript-eslint/no-non-null-assertion': OFF,
'@typescript-eslint/no-redeclare': ERROR,
'no-shadow': OFF,
'@typescript-eslint/no-shadow': ERROR,
'@typescript-eslint/no-unnecessary-type-constraint': 'warn',
'@typescript-eslint/no-unused-vars': [
ERROR,
{
vars: 'all',
args: 'after-used',
argsIgnorePattern: '^_',
ignoreRestSiblings: true,
},
],
'no-use-before-define': OFF,
'@typescript-eslint/no-use-before-define': [
ERROR,
{
functions: false,
classes: true,
variables: false,
ignoreTypeReferences: true,
},
],
'@typescript-eslint/prefer-nullish-coalescing': OFF,
'@typescript-eslint/restrict-template-expressions': OFF,
'@typescript-eslint/require-await': OFF,
'@typescript-eslint/unified-signatures': 'warn',
},
settings: {
'import/resolver': {
typescript: {
alwaysTryTypes: true,
},
},
},
},
]
: []),
// This assumes test files are those which are in the test directory or have
// *.test.* in the filename. If a file doesn't match this assumption, then it
// will not be allowed to import test files.
{
files: ['**/*.ts?(x)', '**/*.js?(x)'],
ignores: testFiles,
rules: {
'no-restricted-imports': [
ERROR,
{
patterns: [
{
group: testFiles,
message: 'Do not import test files in source files',
},
],
},
],
},
},
hasTestingLibrary
? {
files: testFiles,
ignores: [...playwrightTestFiles],
plugins: {
'testing-library': (await import('eslint-plugin-testing-library')).default,
},
rules: {
'testing-library/await-async-events': ERROR,
'testing-library/await-async-queries': ERROR,
'testing-library/await-async-utils': ERROR,
'testing-library/consistent-data-testid': OFF,
'testing-library/no-await-sync-events': ERROR,
'testing-library/no-await-sync-queries': ERROR,
'testing-library/no-container': ERROR,
'testing-library/no-debugging-utils': OFF,
'testing-library/no-dom-import': [ERROR, 'react'],
'testing-library/no-global-regexp-flag-in-query': ERROR,
'testing-library/no-manual-cleanup': ERROR,
'testing-library/no-node-access': ERROR,
'testing-library/no-promise-in-fire-event': ERROR,
'testing-library/no-render-in-lifecycle': ERROR,
'testing-library/no-unnecessary-act': ERROR,
'testing-library/no-wait-for-multiple-assertions': ERROR,
'testing-library/no-wait-for-side-effects': ERROR,
'testing-library/no-wait-for-snapshot': ERROR,
'testing-library/prefer-explicit-assert': ERROR,
'testing-library/prefer-find-by': ERROR,
'testing-library/prefer-presence-queries': ERROR,
'testing-library/prefer-query-by-disappearance': ERROR,
'testing-library/prefer-query-matchers': OFF,
'testing-library/prefer-screen-queries': ERROR,
'testing-library/prefer-user-event': ERROR,
'testing-library/render-result-naming-convention': ERROR,
},
}
: null,
hasJestDom
? {
files: testFiles,
ignores: [...playwrightTestFiles],
plugins: {
'jest-dom': (await import('eslint-plugin-jest-dom')).default,
},
rules: {
'jest-dom/prefer-checked': ERROR,
'jest-dom/prefer-empty': ERROR,
'jest-dom/prefer-enabled-disabled': ERROR,
'jest-dom/prefer-focus': ERROR,
'jest-dom/prefer-in-document': ERROR,
'jest-dom/prefer-required': ERROR,
'jest-dom/prefer-to-have-attribute': ERROR,
'jest-dom/prefer-to-have-class': ERROR,
'jest-dom/prefer-to-have-style': ERROR,
'jest-dom/prefer-to-have-text-content': ERROR,
'jest-dom/prefer-to-have-value': ERROR,
},
}
: null,
hasVitest
? {
files: testFiles,
ignores: [...playwrightTestFiles],
plugins: {
vitest: (await import('@vitest/eslint-plugin')).default,
},
rules: {
// you don't want the editor to autofix this, but we do want to be
// made aware of it
'vitest/no-focused-tests': [WARN, { fixable: false }],
},
}
: null,
hasPlaywright
? {
files: playwrightTestFiles,
...playwright.configs['flat/recommended'],
}
: null,
].filter(Boolean);
// this is for backward compatibility
export default config;