UNPKG

@epic-web/config

Version:

Reasonable ESLint configs for epic web devs

344 lines (311 loc) 9.96 kB
import globals from 'globals' import { has } from './utils.js' const ERROR = 'error' const WARN = 'warn' 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') const vitestFiles = ['**/__tests__/**/*', '**/*.test.*', '**/*.spec.*'] const testFiles = ['**/tests/**', '**/#tests/**', ...vitestFiles] const playwrightFiles = ['**/tests/e2e/**'] export const config = [ { ignores: [ '**/.cache/**', '**/node_modules/**', '**/build/**', '**/public/**', '**/*.json', '**/playwright-report/**', '**/server-build/**', '**/dist/**', '**/coverage/**', '**/*.tsbuildinfo', '**/.react-router/**', '**/.wrangler/**', '**/worker-configuration.d.ts', ], }, // all files { plugins: { import: (await import('eslint-plugin-import-x')).default, }, languageOptions: { globals: { ...globals.browser, ...globals.node, }, }, rules: { 'no-unexpected-multiline': ERROR, 'no-warning-comments': [ ERROR, { terms: ['FIXME'], location: 'anywhere' }, ], 'import/no-duplicates': [WARN, { 'prefer-inline': true }], 'import/order': [ WARN, { alphabetize: { order: 'asc', caseInsensitive: true }, pathGroups: [{ pattern: '#*/**', group: 'internal' }], groups: [ 'builtin', 'external', 'internal', 'parent', 'sibling', 'index', ], }, ], }, }, // JSX/TSX files hasReact ? { files: ['**/*.tsx', '**/*.jsx'], plugins: { react: (await import('eslint-plugin-react')).default, }, languageOptions: { parser: hasTypeScript ? (await import('typescript-eslint')).parser : undefined, parserOptions: { jsx: true, }, }, rules: { 'react/jsx-key': WARN, }, } : null, // react-hook rules are applicable in ts/js/tsx/jsx, but only with React as a // dep hasReact ? { files: ['**/*.ts?(x)', '**/*.js?(x)'], plugins: { 'react-hooks': (await import('eslint-plugin-react-hooks')).default, }, rules: { 'react-hooks/rules-of-hooks': ERROR, 'react-hooks/exhaustive-deps': WARN, }, } : null, // JS and JSX files { files: ['**/*.js?(x)'], rules: { 'no-undef': ERROR, // most of these rules are useful for JS but not TS because TS handles these better // if it weren't for https://github.com/import-js/eslint-plugin-import/issues/2132 // we could enable this :( // 'import/no-unresolved': ERROR, 'no-unused-vars': [ WARN, { args: 'after-used', argsIgnorePattern: '^_', ignoreRestSiblings: true, varsIgnorePattern: '^ignored', }, ], }, }, // TS and TSX files hasTypeScript ? { files: ['**/*.ts?(x)'], languageOptions: { parser: (await import('typescript-eslint')).parser, parserOptions: { projectService: true, }, }, plugins: { '@typescript-eslint': (await import('typescript-eslint')).plugin, }, rules: { '@typescript-eslint/no-unused-vars': [ WARN, { args: 'after-used', argsIgnorePattern: '^_', ignoreRestSiblings: true, varsIgnorePattern: '^ignored', }, ], 'import/consistent-type-specifier-style': [WARN, 'prefer-inline'], '@typescript-eslint/consistent-type-imports': [ WARN, { prefer: 'type-imports', disallowTypeAnnotations: true, fixStyle: 'inline-type-imports', }, ], '@typescript-eslint/no-misused-promises': [ 'error', { checksVoidReturn: false }, ], '@typescript-eslint/no-floating-promises': 'error', // here are rules we've decided to not enable. Commented out rather // than setting them to disabled to avoid them being referenced at all // when config resolution happens. // @typescript-eslint/require-await - sometimes you really do want // async without await to make a function async. TypeScript will ensure // it's treated as an async function by consumers and that's enough for me. // @typescript-eslint/prefer-promise-reject-errors - sometimes you // aren't the one creating the error, and you just want to propagate an // error object with an unknown type. // @typescript-eslint/only-throw-error - same reason as above. // However, this rule supports options to allow you to throw `any` and // `unknown`. Unfortunately, in Remix you can throw Response objects, // and we don't want to enable this rule for those cases. // @typescript-eslint/no-unsafe-declaration-merging - this is a rare // enough problem (especially if you focus on types over interfaces) // that it's not worth enabling. // @typescript-eslint/no-unsafe-enum-comparison - enums are not // recommended or used in epic projects, so it's not worth enabling. // @typescript-eslint/no-unsafe-unary-minus - this is a rare enough // problem that it's not worth enabling. // @typescript-eslint/no-base-to-string - this doesn't handle when // your object actually does implement toString unless you do so with // a class which is not 100% of the time. For example, the timings // object in the epic stack uses defineProperty to implement toString. // It's not high enough risk/impact to enable. // @typescript-eslint/no-non-null-assertion - normally you should not // use ! to tell TS to ignore the null case, but you're a responsible // adult and if you're going to do that, the linter shouldn't yell at // you about it. // @typescript-eslint/restrict-template-expressions - toString is a // feature of many built-in objects and custom ones. It's not worth // enabling. // @typescript-eslint/no-confusing-void-expression - what's confusing // to one person isn't necessarily confusing to others. Arrow // functions that call something that returns void is not confusing // and the types will make sure you don't mess something up. // these each protect you from `any` and while it's best to avoid // using `any`, it's not worth having a lint rule yell at you when you // do: // - @typescript-eslint/no-unsafe-argument // - @typescript-eslint/no-unsafe-call // - @typescript-eslint/no-unsafe-member-access // - @typescript-eslint/no-unsafe-return // - @typescript-eslint/no-unsafe-assignment }, } : null, // This assumes test files are those which are in the test directory or have // *.test.* or *.spec.* 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: [...playwrightFiles], plugins: { 'testing-library': (await import('eslint-plugin-testing-library')) .default, }, rules: { 'testing-library/no-unnecessary-act': [ERROR, { isStrict: false }], 'testing-library/no-wait-for-side-effects': ERROR, 'testing-library/prefer-find-by': ERROR, }, } : null, hasJestDom ? { files: testFiles, ignores: [...playwrightFiles], plugins: { 'jest-dom': (await import('eslint-plugin-jest-dom')).default, }, rules: { 'jest-dom/prefer-checked': ERROR, 'jest-dom/prefer-enabled-disabled': ERROR, 'jest-dom/prefer-focus': ERROR, 'jest-dom/prefer-required': ERROR, }, } : null, hasVitest ? { files: testFiles, ignores: [...playwrightFiles], 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 }], 'vitest/no-import-node-test': ERROR, 'vitest/prefer-comparison-matcher': ERROR, 'vitest/prefer-equality-matcher': ERROR, 'vitest/prefer-to-be': ERROR, 'vitest/prefer-to-contain': ERROR, 'vitest/prefer-to-have-length': ERROR, 'vitest/valid-expect-in-promise': ERROR, 'vitest/valid-expect': ERROR, // vitest/expect-expect - we don't enable this because it's fine to // rely on testing-library to throw errors if elements aren't found. }, } : null, hasPlaywright ? { files: [...playwrightFiles], plugins: { playwright: (await import('eslint-plugin-playwright')).default, }, rules: { 'playwright/max-nested-describe': ERROR, 'playwright/missing-playwright-await': ERROR, 'playwright/no-focused-test': WARN, 'playwright/no-page-pause': ERROR, 'playwright/no-raw-locators': [WARN, { allowed: ['iframe'] }], 'playwright/no-slowed-test': ERROR, 'playwright/no-standalone-expect': ERROR, 'playwright/no-unsafe-references': ERROR, 'playwright/prefer-comparison-matcher': ERROR, 'playwright/prefer-equality-matcher': ERROR, 'playwright/prefer-native-locators': ERROR, 'playwright/prefer-to-be': ERROR, 'playwright/prefer-to-contain': ERROR, 'playwright/prefer-to-have-count': ERROR, 'playwright/prefer-to-have-length': ERROR, 'playwright/prefer-web-first-assertions': ERROR, 'playwright/valid-expect-in-promise': ERROR, 'playwright/valid-expect': ERROR, // playwright/expect-expect - we don't enable this because it's fine to // rely on thrown errors if elements aren't found. }, } : null, ].filter(Boolean) // this is for backward compatibility export default config