ESLint configuration for Remix projects providing comprehensive linting rules for React, TypeScript, JSX accessibility, and import management
npx @tessl/cli install tessl/npm-remix-run--eslint-config@2.17.0@remix-run/eslint-config provides comprehensive ESLint configurations specifically designed for Remix projects. It offers shareable ESLint configs that enforce best practices for React, TypeScript, JSX accessibility, and import/export patterns.
⚠️ Deprecation Notice: This package is deprecated and will not be included in React Router v7. Users should migrate to streamlined ESLint configurations as provided in Remix templates.
npm install -D eslint @remix-run/eslint-configThis package is used via ESLint configuration files - no JavaScript imports required.
Create a .eslintrc.js file in your project root:
module.exports = {
extends: "@remix-run/eslint-config",
};With Jest and Testing Library:
module.exports = {
extends: [
"@remix-run/eslint-config",
"@remix-run/eslint-config/jest-testing-library",
],
};For Node.js projects:
module.exports = {
extends: [
"@remix-run/eslint-config",
"@remix-run/eslint-config/node",
],
};Note: The package.json references a jest.js file in the files array, but this file does not exist in the actual package. Testing configuration is provided through jest-testing-library.js instead.
@remix-run/eslint-config is built around several key configuration modules:
Main shareable ESLint configuration for Remix projects with comprehensive rules for JavaScript, React, TypeScript, and accessibility.
// Usage in .eslintrc.js
module.exports = {
extends: "@remix-run/eslint-config",
};
// ESLint Configuration Object Structure
interface ESLintConfig {
parser: string;
parserOptions: {
sourceType: "module";
requireConfigFile: false;
ecmaVersion: "latest";
babelOptions: {
presets: string[];
};
};
env: {
browser: boolean;
commonjs: boolean;
es6: boolean;
};
plugins: string[];
settings: Record<string, any>;
rules: Record<string, any>;
overrides: Array<{
files: string[];
extends?: string[];
parser?: string;
parserOptions?: Record<string, any>;
plugins?: string[];
rules?: Record<string, any>;
}>;
}Features:
Optional ESLint configuration for projects using Jest and Testing Library.
// Usage in .eslintrc.js
module.exports = {
extends: [
"@remix-run/eslint-config",
"@remix-run/eslint-config/jest-testing-library",
],
};
// Configuration Structure
interface JestTestingLibraryConfig {
plugins: ["jest", "jest-dom", "testing-library"];
env: {
node: boolean;
};
overrides: Array<{
files: string[];
env: {
"jest/globals": boolean;
};
rules: Record<string, any>;
}>;
}Features:
**/__tests__/**/* and **/*.{spec,test}.* filesRequired Peer Dependencies (if using this configuration):
{
"dependencies": {
"@testing-library/jest-dom": ">=5.16.0",
"@testing-library/react": ">=12.0.0",
"jest": ">=28.0.0"
}
}Basic Node.js environment configuration for server-side projects.
// Usage in .eslintrc.js
module.exports = {
extends: [
"@remix-run/eslint-config",
"@remix-run/eslint-config/node",
],
};
// Configuration Structure
interface NodeConfig {
plugins: ["node"];
env: {
node: boolean;
};
}Features:
Extended ESLint configuration used internally by the Remix team. This configuration is not intended for external use and should not be considered public API regarding semver considerations.
// Usage (internal Remix projects only)
module.exports = {
extends: "@remix-run/eslint-config/internal",
};
// Configuration Structure
interface InternalConfig {
root: true;
extends: [
"./index.js",
"./jest-testing-library.js"
];
env: {
node: boolean;
};
plugins: ["node", "prefer-let"];
rules: {
"@typescript-eslint/consistent-type-imports": 2; // ERROR
"import/order": [2, {
"newlines-between": "always";
groups: [
["builtin", "external"],
"internal",
["parent", "sibling", "index"]
];
}];
"jest/no-disabled-tests": 0; // OFF
"prefer-let/prefer-let": 1; // WARN
};
overrides: Array<{
files: string[];
rules: Record<string, any>;
}>;
}Features:
Internal-Only Rules:
Fundamental ESLint rules for JavaScript/ES6+ code quality and error prevention.
// Core JavaScript Rules (comprehensive)
interface CoreRules {
"array-callback-return": 1; // WARN
"getter-return": 1; // WARN
"new-parens": 1; // WARN
"no-array-constructor": 1; // WARN
"no-caller": 2; // ERROR
"no-cond-assign": [1, "except-parens"]; // WARN
"no-const-assign": 2; // ERROR
"no-control-regex": 1; // WARN
"no-dupe-args": 1; // WARN
"no-dupe-class-members": 1; // WARN
"no-dupe-keys": 1; // WARN
"no-duplicate-case": 1; // WARN
"no-empty-character-class": 1; // WARN
"no-empty-pattern": 1; // WARN
"no-empty": [1, { allowEmptyCatch: true }]; // WARN
"no-eval": 2; // ERROR
"no-ex-assign": 1; // WARN
"no-extend-native": 1; // WARN
"no-extra-bind": 1; // WARN
"no-extra-label": 1; // WARN
"no-extra-boolean-cast": 1; // WARN
"no-func-assign": 2; // ERROR
"no-global-assign": 2; // ERROR
"no-implied-eval": 1; // WARN
"no-invalid-regexp": 1; // WARN
"no-label-var": 1; // WARN
"no-labels": [1, { allowLoop: true, allowSwitch: false }]; // WARN
"no-lone-blocks": 1; // WARN
"no-loop-func": 1; // WARN
"no-mixed-operators": [1, {
groups: [
["&", "|", "^", "~", "<<", ">>", ">>>"],
["==", "!=", "===", "!==", ">", ">=", "<", "<="],
["&&", "||"],
["in", "instanceof"]
],
allowSamePrecedence: false
}]; // WARN
"no-unsafe-negation": 1; // WARN
"no-new-func": 1; // WARN
"no-new-object": 1; // WARN
"no-octal": 1; // WARN
"no-redeclare": 2; // ERROR
"no-script-url": 1; // WARN
"no-self-assign": 1; // WARN
"no-self-compare": 1; // WARN
"no-sequences": 1; // WARN
"no-shadow-restricted-names": 1; // WARN
"no-sparse-arrays": 1; // WARN
"no-template-curly-in-string": 1; // WARN
"no-this-before-super": 1; // WARN
"no-undef": 2; // ERROR
"no-unreachable": 1; // WARN
"no-unused-expressions": [1, {
allowShortCircuit: true,
allowTernary: true,
allowTaggedTemplates: true
}]; // WARN
"no-unused-labels": 1; // WARN
"no-unused-vars": [1, { args: "none", ignoreRestSiblings: true }]; // WARN
"no-use-before-define": [1, { classes: false, functions: false, variables: false }]; // WARN
"no-useless-computed-key": 1; // WARN
"no-useless-concat": 1; // WARN
"no-useless-constructor": 1; // WARN
"no-useless-escape": 1; // WARN
"no-useless-rename": [1, {
ignoreDestructuring: false,
ignoreImport: false,
ignoreExport: false
}]; // WARN
"require-yield": 1; // WARN
"use-isnan": 1; // WARN
"valid-typeof": 1; // WARN
}React-specific rules for component development and JSX syntax.
// React and JSX Rules (comprehensive)
interface ReactRules {
"react/display-name": 1; // WARN
"react/forbid-foreign-prop-types": [1, { allowInPropTypes: true }]; // WARN
"react/jsx-key": 1; // WARN
"react/jsx-no-comment-textnodes": 1; // WARN
"react/jsx-no-target-blank": 1; // WARN
"react/jsx-no-undef": 2; // ERROR
"react/jsx-pascal-case": [1, { allowAllCaps: true, ignore: [] }]; // WARN
"react/jsx-uses-vars": 1; // WARN
"react/jsx-uses-react": 1; // WARN
"react/no-danger-with-children": 1; // WARN
"react/no-direct-mutation-state": 1; // WARN
"react/no-find-dom-node": 1; // WARN
"react/no-is-mounted": 1; // WARN
"react/no-render-return-value": 2; // ERROR
"react/no-string-refs": 1; // WARN
"react/no-typos": 1; // WARN
"react/react-in-jsx-scope": 0; // OFF - Not needed in modern React
"react/require-render-return": 2; // ERROR
"react/style-prop-object": 1; // WARN
// React Hooks Rules
"react-hooks/exhaustive-deps": 1; // WARN
"react-hooks/rules-of-hooks": 2; // ERROR
}TypeScript-specific rules and overrides for type safety.
// TypeScript Rules (comprehensive)
interface TypeScriptRules {
// Disabled TypeScript rules (turned off for flexibility)
"@typescript-eslint/ban-ts-comment": 0; // OFF
"@typescript-eslint/ban-types": 0; // OFF
"@typescript-eslint/no-empty-function": 0; // OFF
"@typescript-eslint/no-empty-interface": 0; // OFF
"@typescript-eslint/no-explicit-any": 0; // OFF
"@typescript-eslint/no-inferrable-types": 0; // OFF
"@typescript-eslint/no-namespace": 0; // OFF
"@typescript-eslint/no-non-null-assertion": 0; // OFF
"@typescript-eslint/no-var-requires": 0; // OFF
"no-var": 0; // OFF
"prefer-rest-params": 0; // OFF
// Configured TypeScript rules
"@typescript-eslint/no-use-before-define": [2, {
functions: false,
classes: false,
variables: false,
typedefs: false
}]; // ERROR
"@typescript-eslint/no-unused-expressions": [2, {
allowShortCircuit: true,
allowTernary: true,
allowTaggedTemplates: true
}]; // ERROR
"@typescript-eslint/no-unused-vars": [2, {
args: "none",
ignoreRestSiblings: true
}]; // ERROR
// JavaScript rules disabled for TypeScript
"no-dupe-class-members": 0; // OFF - Handled by TypeScript
"no-undef": 0; // OFF - Handled by TypeScript
"no-use-before-define": 0; // OFF - Use TypeScript version
"prefer-const": 0; // OFF - Style preference
// Stylistic TypeScript rules
"@typescript-eslint/consistent-type-assertions": 1; // WARN
"@typescript-eslint/consistent-type-imports": 1; // WARN
}Accessibility rules for JSX elements and ARIA compliance.
// JSX Accessibility Rules (comprehensive)
interface JsxA11yRules {
"jsx-a11y/alt-text": 1; // WARN
"jsx-a11y/anchor-has-content": [1, { components: ["Link", "NavLink"] }]; // WARN
"jsx-a11y/anchor-is-valid": [1, { aspects: ["noHref", "invalidHref"] }]; // WARN
"jsx-a11y/aria-activedescendant-has-tabindex": 1; // WARN
"jsx-a11y/aria-props": 1; // WARN
"jsx-a11y/aria-proptypes": 1; // WARN
"jsx-a11y/aria-role": [1, { ignoreNonDOM: true }]; // WARN
"jsx-a11y/aria-unsupported-elements": 1; // WARN
"jsx-a11y/iframe-has-title": 1; // WARN
"jsx-a11y/img-redundant-alt": 1; // WARN
"jsx-a11y/lang": 1; // WARN
"jsx-a11y/no-access-key": 1; // WARN
"jsx-a11y/no-redundant-roles": 1; // WARN
"jsx-a11y/role-has-required-aria-props": 1; // WARN
"jsx-a11y/role-supports-aria-props": 1; // WARN
}ES6 module import/export management and organization.
// Import and Export Rules
interface ImportRules {
"import/first": 2; // ERROR - Imports must be at top
"import/no-amd": 2; // ERROR - No AMD require/define
"import/no-duplicates": 2; // ERROR - No duplicate imports
"import/no-webpack-loader-syntax": 2; // ERROR - No webpack loader syntax
}Jest-specific rules for test structure and best practices when using jest-testing-library configuration.
// Jest Rules (comprehensive)
interface JestRules {
"jest/no-conditional-expect": 1; // WARN
"jest/no-deprecated-functions": 1; // WARN
"jest/no-disabled-tests": 1; // WARN
"jest/no-export": 2; // ERROR - Test files should not export
"jest/no-focused-tests": 1; // WARN - No .only or .skip
"jest/no-identical-title": 1; // WARN
"jest/no-interpolation-in-snapshots": 1; // WARN
"jest/no-jasmine-globals": 2; // ERROR - Use Jest, not Jasmine
"jest/no-jest-import": 1; // WARN
"jest/no-mocks-import": 1; // WARN
"jest/valid-describe-callback": 2; // ERROR
"jest/valid-expect": 2; // ERROR
"jest/valid-expect-in-promise": 2; // ERROR
}Jest DOM assertion preferences and element state checking.
// Jest DOM Rules
interface JestDomRules {
"jest-dom/prefer-checked": 1; // WARN
"jest-dom/prefer-empty": 1; // WARN
"jest-dom/prefer-enabled-disabled": 1; // WARN
"jest-dom/prefer-focus": 1; // WARN
"jest-dom/prefer-in-document": 1; // WARN
"jest-dom/prefer-required": 1; // WARN
"jest-dom/prefer-to-have-attribute": 1; // WARN
"jest-dom/prefer-to-have-class": 1; // WARN
"jest-dom/prefer-to-have-style": 1; // WARN
"jest-dom/prefer-to-have-text-content": 1; // WARN
"jest-dom/prefer-to-have-value": 1; // WARN
}Testing Library best practices for async queries, event handling, and screen queries.
// Testing Library Rules
interface TestingLibraryRules {
"testing-library/await-async-query": 2; // ERROR
"testing-library/await-async-utils": 2; // ERROR
"testing-library/no-await-sync-events": 2; // ERROR
"testing-library/no-await-sync-query": 2; // ERROR
"testing-library/no-debugging-utils": 1; // WARN
"testing-library/no-promise-in-fire-event": 2; // ERROR
"testing-library/no-render-in-setup": 2; // ERROR
"testing-library/no-unnecessary-act": 2; // ERROR
"testing-library/no-wait-for-empty-callback": 2; // ERROR
"testing-library/no-wait-for-multiple-assertions": 2; // ERROR
"testing-library/no-wait-for-side-effects": 2; // ERROR
"testing-library/no-wait-for-snapshot": 2; // ERROR
"testing-library/prefer-find-by": 1; // WARN
"testing-library/prefer-presence-queries": 1; // WARN
"testing-library/prefer-query-by-disappearance": 1; // WARN
"testing-library/prefer-screen-queries": 1; // WARN
"testing-library/prefer-user-event": 1; // WARN
"testing-library/prefer-wait-for": 1; // WARN
"testing-library/render-result-naming-convention": 1; // WARN
}React-specific ESLint settings for Remix components.
interface ReactSettings {
react: {
version: "detect";
formComponents: ["Form"];
linkComponents: Array<{
name: "Link" | "NavLink";
linkAttribute: "to";
}>;
};
}Import parser and resolver configuration for JavaScript and TypeScript.
// Import Resolution Settings (detailed)
interface ImportSettings {
"import/ignore": [
"node_modules",
"\\.(css|md|svg|json)$"
];
"import/parsers": {
"@typescript-eslint/parser": [".ts", ".tsx", ".d.ts"];
};
"import/resolver": {
"eslint-import-resolver-node": {
extensions: [".js", ".jsx", ".ts", ".tsx"];
};
"eslint-import-resolver-typescript": {
alwaysTryTypes: true;
};
};
}Special configuration for .ts and .tsx files.
// Files: ["**/*.ts?(x)"]
interface TypeScriptOverride {
extends: [
"plugin:import/typescript",
"plugin:@typescript-eslint/recommended",
];
parser: "@typescript-eslint/parser";
parserOptions: {
sourceType: "module";
ecmaVersion: 2019;
};
plugins: ["@typescript-eslint"];
rules: Record<string, any>;
}Special handling for Remix route files allowing unnamed default exports.
// Files: ["**/routes/**/*.js?(x)", "**/routes/**/*.tsx", "app/root.js?(x)", "app/root.tsx"]
interface RouteOverride {
rules: {
"react/display-name": 0; // OFF - Routes may use default exports without a name
};
}Configuration applied specifically to test files when using jest-testing-library config.
// Files: ["**/__tests__/**/*", "**/*.{spec,test}.*"]
interface TestOverride {
env: {
"jest/globals": true;
};
rules: {
// Jest, Jest DOM, and Testing Library rules applied here
};
}interface PeerDependencies {
eslint: "^8.0.0";
react: "^18.0.0";
typescript?: "^5.1.0"; // Optional
}
interface Engines {
node: ">=18.0.0";
}Additional dependencies needed when using jest-testing-library configuration:
{
"dependencies": {
"@testing-library/jest-dom": ">=5.16.0",
"@testing-library/react": ">=12.0.0",
"jest": ">=28.0.0"
}
}The package includes proper error handling through:
When using this package, a deprecation warning will be displayed:
⚠️ REMIX FUTURE CHANGE: The `@remix-run/eslint-config` package is deprecated
and will not be included in React Router v7. We recommend moving towards a
streamlined ESLint config such as the ones included in the Remix templates.
See https://github.com/remix-run/remix/blob/main/templates/remix/.eslintrc.cjs.This warning is shown every time the configuration is loaded, helping users transition to alternative ESLint configurations before React Router v7.
// ESLint Rule Severity Levels
type RuleSeverity = 0 | 1 | 2 | "off" | "warn" | "error";
// Rule Configuration
type RuleConfig = RuleSeverity | [RuleSeverity, ...any[]];
// ESLint Configuration Object
interface ESLintConfiguration {
root?: boolean;
extends?: string | string[];
parser?: string;
parserOptions?: Record<string, any>;
env?: Record<string, boolean>;
plugins?: string[];
settings?: Record<string, any>;
rules?: Record<string, RuleConfig>;
overrides?: Array<{
files: string | string[];
excludedFiles?: string | string[];
extends?: string | string[];
parser?: string;
parserOptions?: Record<string, any>;
env?: Record<string, boolean>;
plugins?: string[];
rules?: Record<string, RuleConfig>;
}>;
}