Skip to content

Repository files navigation

@shayanthenerd/eslint-config   license-badge npm-version-badge

ESLint configuration for enforcing best practices and maintaining a consistent coding style. Inspect configurations!

Table of Contents

Plugin Support

Legend:

  • ✅ — Enabled by default
  • ⚙️ — Opt-in (requires manual configuration)
  • 🔍 — Automatically detected (based on project dependencies)
Category Activation
Languages
JavaScript ✅ 
TypeScript 🔍 
Markdown ✅ 
HTML ⚙️ 
CSS ⚙️ 
Formatting
Stylistic ✅ 
Perfectionist ✅ 
Frameworks & Libraries
Astro (jsx-accessibility) 🔍 
React (jsx-accessibility, @html-eslint/react) 🔍 
Next 🔍 
Vue & Nuxt (vue-accessibility) 🔍 
Tailwind ⚙️ 
Zod & Zod Mini 🔍 
Testing Tools
Storybook 🔍 
Vitest 🔍 
Cypress 🔍 
Playwright 🔍 
Miscellaneous
package.json ✅ 
Node ✅ 
Promises ✅ 
Imports ✅ 
Unicorn ✅ 
Baseline 🔍 

Installation and Configuration

  1. Install the package and ESLint as dev dependencies:

    npm i -D @shayanthenerd/eslint-config eslint
  2. Create an ESLint configuration file (eslint.config.js) at the root of your project:

    import { defineConfig } from '@shayanthenerd/eslint-config';
    
    export default defineConfig();

    Note: TypeScript configuration files (eslint.config.ts) are also supported. Node.js versions below v22.18.0 may require additional setup.

  3. Add the following scripts to your package.json:

    {
      "scripts": {
        "lint:inspect": "npx @eslint/config-inspector",
        "lint": "eslint --fix --max-warnings=0 --cache --cache-location='node_modules/.cache/.eslintcache'"
      }
    }

After installation:

  • Use npm run lint to lint and fix files.
  • Use npm run lint:inspect to see a visual breakdown of your configuration.
  • See IDE Support for editor integration.
  • See Customization for advanced configuration.
  • See Formatting for formatting options and Prettier integration.

Customization

defineConfig() supports both simple and advanced use cases:

import eslintPluginYaml from 'eslint-plugin-yaml';
import * as eslintPluginRegexp from 'eslint-plugin-regexp';

import { defineConfig } from '@shayanthenerd/eslint-config';

// Use the default configuration:
export default defineConfig();

// Customize the built-in configurations:
export default defineConfig({
  autoDetectDeps: false,
  configs: {
    stylistic: false,
    markdown: {
      language: 'commonmark',
    },
  },
});

// Use custom configuration objects:
export default defineConfig([
  {
    files: ['**/*.yaml', '**/*.yml'],
    ignores: ['**/*.schema.yaml', '**/*.schema.yml'],
    extends: [eslintPluginYaml.configs.recommended],
  },
  eslintPluginRegexp.configs['flat/recommended'],
]);

// Customize the built-in configurations and use custom configuration objects:
export default defineConfig(
  {
    autoDetectDeps: 'verbose',
    configs: {
      typescript: {
        typeDefinitionStyle: 'type',
        overrides: {
          rules: {
            '@typescript-eslint/explicit-module-boundary-types': 'off',
          },
        },
      },
    },
  },
  [
    {
      files: ['**/*.yaml', '**/*.yml'],
      ignores: ['**/*.schema.yaml', '**/*.schema.yml'],
      extends: [eslintPluginYaml.configs.recommended],
    },
    eslintPluginRegexp.configs['flat/recommended'],
  ],
);

Every built-in configuration accepts an overrides option. These values are merged into the generated configuration and take precedence over the defaults.

import type { ESLint, Linter } from 'eslint';

interface Overrides {
  name?: string,
  files?: (string | string[])[],
  ignores?: string[],
  plugins?: Record<string, ESLint.Plugin>,
  languageOptions?: Linter.Config['languageOptions'],
  settings?: Record<string, unknown>,
  rules?: ConfigRules, // The available rules in the current configuration object
}

Automatic Dependency Detection

local-pkg is used to detect installed dependencies and automatically enable the relevant integrations. Package managers that hoist transitive dependencies (such as NPM and Bun) may sometimes cause an integration to get enabled unexpectedly. This is because local-pkg scans node_modules instead of package.json.

PNPM's strict dependency resolution avoids this issue.

Note

For example, eslint-plugin-storybook depends on storybook, which is hoisted by NPM and Bun. As a result, the integration for storybook will be enabled automatically, even if you haven't explicitly installed it.

configs.useBaseline is automatically enabled when autoDetectDeps: true and env: 'browser' (both are the defaults).

To opt out of this behavior, either globally disable automatic dependency detection or manually disable the unwanted integrations that were enabled automatically.

Framework and Tool Integrations

Tailwind

To enable the Tailwind integration, provide the location of your Tailwind configuration or CSS entry point.

import { defineConfig } from '@shayanthenerd/eslint-config';

export default defineConfig({
  configs: {
    tailwind: {
      /* Either option is sufficient, but both can be provided. */
      config: './tailwind.config.js',
      entryPoint: './app/assets/styles/app.css',
    },
  },
});

For editor integration, to avoid inconsistent diagnostics from the Tailwind CSS IntelliSense VS Code extension, add the following settings to .vscode/settings.json:

{
  "tailwindCSS.lint.cssConflict": "ignore",
  "tailwindCSS.lint.recommendedVariantOrder": "ignore",
  "tailwindCSS.lint.suggestCanonicalClasses": "ignore",

  "eslint.rules.customizations": [
    { "rule": "better-tailwindcss/*", "severity": "off", "fixable": true },
    { "rule": "better-tailwindcss/no-restricted-classes", "severity": "warn", "fixable": true },
    { "rule": "better-tailwindcss/no-conflicting-classes", "severity": "error", "fixable": false },
    { "rule": "better-tailwindcss/no-unknown-classes", "severity": "warn", "fixable": false }
  ]
}

Nuxt

The required Nuxt configurations and rules are already included, so there's no need to install or configure the @nuxt/eslint module.

Markdown

Markdown linting is powered by @eslint/markdown.

By default, the plugin uses GitHub Flavored Markdown (GFM). You can switch to CommonMark if you prefer, but rules related to tables, label references, and other GFM syntax will be disabled because CommonMark doesn't support them.

Note

Fenced code blocks inside Markdown files are not linted by @eslint/markdown.

Node.js

Some rules depend on the specified Node.js version. Visit the documentation for version-resolution options and project-specific configurations.

IDE Support

Install the VS Code extensions for ESLint and Prettier.

Tip

In case you're using PNPM without shamefullyHoist: true and ESLint's VS Code extension isn't working as expected, add the following to your pnpm-workspace.yaml and run pnpm install --yes:

publicHoistPattern:
  - '*eslint*'

You can also add the following to your .vscode/settings.json:

{
  /* Enforce Unix-like line endings (LF). */
  "files.eol": "\n",

  /* Enforce 2 spaces for indentation. */
  "editor.tabSize": 2,
  "editor.insertSpaces": true,
  "editor.detectIndentation": false,

  "editor.codeActionsOnSave": {
    /* Imports are sorted and organized with eslint-plugin-perfectionist. */
    "source.sortImports": "never",
    "source.organizeImports": "never",
    "source.removeUnusedImports": "never",

    /* Apply ESLint fixes when saving files. */
    "source.fixAll.eslint": "explicit"
  },

  "eslint.run": "onSave",
  "eslint.format.enable": true,
  "eslint.validate": [
    "javascript",
    "typescript",
    "javascriptreact",
    "typescriptreact",
    "json",
    "markdown",
    "html",
    "css",
    "tailwindcss",
    "astro",
    "vue"
  ],

  /* Adjust these based on the features you're using to silently auto-fix the stylistic rules. */
  "eslint.rules.customizations": [
    { "rule": "*styl*", "severity": "off", "fixable": true },
    { "rule": "*sort*", "severity": "off", "fixable": true },
    { "rule": "*indent", "severity": "off", "fixable": true },
    { "rule": "*quotes", "severity": "off", "fixable": true },
    { "rule": "import*", "severity": "off", "fixable": true },
    { "rule": "*-spac*", "severity": "off", "fixable": true },
    { "rule": "*order-*", "severity": "off", "fixable": true },
    { "rule": "*newline*", "severity": "off", "fixable": true },
    { "rule": "*attribute*", "severity": "off", "fixable": true },
    { "rule": "package-json/order-properties", "severity": "off", "fixable": true },
    { "rule": "vue/max-len", "severity": "off", "fixable": true },
    { "rule": "vue/comma-dangle", "severity": "off", "fixable": true },
    { "rule": "vue/space-in-parens", "severity": "off", "fixable": true }
  ]
}

Formatting

This configuration uses ESLint Stylistic to format:

  • JavaScript and TypeScript (js, cjs, mjs, jsx, ts, cts, mts, tsx),
  • Astro (similar to jsx/tsx), and
  • Vue's <script> blocks.

HTML and Vue's <template> blocks are formatted with @html-eslint/eslint-plugin and eslint-plugin-vue, respectively.

For file types not handled by ESLint Stylistic—such as CSS, JSON, Yaml, and Markdown—you can use Prettier. To simplify the setup, this package also provides a customizable shared Prettier configuration.

  1. Install Prettier as a dev dependency:

    npm i -D prettier
  2. Create a Prettier config file in the root of your project (prettier.config.js):

    import prettierConfig from '@shayanthenerd/eslint-config/prettier';
    
    /** @type {import('prettier').Config} */
    export default {
      ...prettierConfig,
      semi: false, // Override `semi` from the shared config
    };

    Or if you prefer using TypeScript (prettier.config.ts):

    import type { Config } from 'prettier';
    
    import prettierConfig from '@shayanthenerd/eslint-config/prettier';
    
    export default {
      ...prettierConfig,
      semi: true, // Override `semi` from the shared config
    } satisfies Config;

Using ESLint Stylistic with Prettier

In this setup, Prettier formats everything that ESLint Stylistic doesn't. To prevent overlaps and potential race conditions, Prettier should only target files that ESLint Stylistic doesn't format.

package.json:

{
  "scripts": {
    "format": "prettier --write . '!**/*.{js,cjs,mjs,jsx,ts,cts,mts,tsx,html,vue,astro}' --cache"
  }
}

.vscode/settings.json:

{
  "editor.formatOnSave": true,
  "editor.defaultFormatter": "esbenp.prettier-vscode",
  "[javascript][typescript][javascriptreact][typescriptreact][html][vue][astro][{**/package.json}]": {
  "editor.defaultFormatter": "dbaeumer.vscode-eslint"
  },
}

Using Prettier Alone

If you prefer to use Prettier as the only formatter, disable the stylistic configuration and let Prettier handle all formatting. Just make sure to avoid running lint and format scripts on the same files simultaneously.

package.json:

{
  "scripts": {
    "format": "prettier --write . --cache",
    "lint": "eslint --fix --max-warnings=0 --cache --cache-location='node_modules/.cache/.eslintcache'"
  }
}

.vscode/settings.json:

{
  "editor.formatOnSave": true,
  "editor.defaultFormatter": "esbenp.prettier-vscode",

  /* On file save, code actions are run before format, so the following doesn't cause conflicts. */
  "editor.codeActionsOnSave": {
    "source.fixAll.eslint": "explicit"
  }
}

API Reference

The API reference of the options object passed to defineConfig

Some types are omitted or aliased for brevity.

import type { ESLint, Linter } from 'eslint';

type VueAttributeCategory =
  | 'SLOT'
  | 'EVENTS'
  | 'GLOBAL'
  | 'UNIQUE'
  | 'CONTENT'
  | 'DEFINITION'
  | 'OTHER_ATTR'
  | 'ATTR_STATIC'
  | 'ATTR_DYNAMIC'
  | 'CONDITIONALS'
  | 'LIST_RENDERING'
  | 'TWO_WAY_BINDING'
  | 'OTHER_DIRECTIVES'
  | 'RENDER_MODIFIERS'
  | 'ATTR_SHORTHAND_BOOL';

interface Overrides {
  name?: string,
  files?: (string | string[])[],
  ignores?: string[],
  plugins?: Record<string, ESLint.Plugin>,
  languageOptions?: Linter.Config['languageOptions'],
  settings?: Record<string, unknown>,
  rules?: ConfigRules, // The available rules in the current configuration object
}

interface Options {
  autoDetectDeps?: boolean | 'verbose',
  env?: 'browser' | 'bun' | 'deno' | 'node',
  gitignore?: false | string,
  packageDir?: string,
  tsConfig?: false | {
    filename?: string,
    rootDir?: string,
  },

  project?: {
    basePath?: string,
    globals?: {
      astro?: boolean,
      audioWorklet?: boolean,
      browser?: boolean,
      bun?: boolean,
      commonjs?: boolean,
      deno?: boolean,
      node?: boolean,
      nodeBuiltin?: boolean,
      serviceworker?: boolean,
      sharedWorker?: boolean,
      vitest?: boolean,
      vue?: boolean,
      webextension?: boolean,
      worker?: boolean,
      custom?: Record<string, 'off' | boolean | 'readable' | 'readonly' | 'writable' | 'writeable'>,
    },
    ignores?: string[],
    linterOptions?: {
      noInlineConfig?: boolean,
      reportUnusedDisableDirectives?: 'off' | 'warn' | 'error',
      reportUnusedInlineConfigs?: 'off' | 'warn' | 'error',
    },
    name?: string,
    rules?: Linter.RulesRecord,
    settings?: Record<string, unknown>,
  },

  configs?: {
    astro?: boolean | {
      overrides?: Overrides,
    },
    base?: {
      maxDepth?: number,
      maxNestedCallbacks?: number,
      overrides?: Overrides,
    },
    css?: boolean | {
      overrides?: Overrides,
    },
    html?: boolean | {
      idNamingConvention?: 'camelCase' | 'kebab-case' | 'PascalCase' | 'snake_case',
      overrides?: Overrides,
    },
    importX?: boolean | {
      overrides?: Overrides,
    },
    markdown?: boolean | {
      allowedHtmlTags?: string[],
      frontmatter?: false | 'json' | 'toml' | 'yaml',
      language?: 'gfm' | 'commonmark',
      overrides?: Overrides,
    },
    next?: boolean | {
      overrides?: Overrides,
    },
    node?: boolean | {
      overrides?: Overrides,
    },
    nuxt?: boolean | {
      icon?: boolean | {
        component?: string,
      },
      image?: boolean,
      ui?: boolean | {
        prefix?: string,
      },
      overrides?: Overrides,
    },
    packageJson?: boolean | {
      overrides?: Overrides,
    },
    perfectionist?: boolean | {
      sortType?: 'custom' | 'natural' | 'unsorted' | 'line-length' | 'alphabetical' | 'subgroup-order',
      overrides?: Overrides,
    },
    promise?: boolean | {
      overrides?: Overrides,
    },
    react?: boolean | {
      accessibility?: boolean | {
        anchorComponents?: string[],
        headingComponents?: string[],
        imageComponents?: string[],
      },
      overrides?: Overrides,
    },
    stylistic?: boolean | {
      arrowParens?: 'always' | 'as-needed',
      indent?: number,
      jsxQuotes?: 'prefer-double' | 'prefer-single',
      maxAttributesPerLine?: number,
      maxConsecutiveEmptyLines?: number,
      maxLineLength?: number,
      memberDelimiterStyle?: 'semi' | 'comma',
      quotes?: 'double' | 'single' | 'backtick',
      selfCloseVoidHtmlElements?: 'never' | 'always',
      semi?: 'never' | 'always',
      trailingComma?: 'never' | 'always' | 'only-multiline' | 'always-multiline',
      overrides?: Overrides,
    },
    tailwind?: false | {
      config: string,
      cwd?: string,
      entryPoint?: string,
      ignoredUnknownClasses?: string[],
      multilineSort?: boolean,
      overrides?: Overrides,
    } | {
      config?: string,
      cwd?: string,
      entryPoint: string,
      ignoredUnknownClasses?: string[],
      multilineSort?: boolean,
      overrides?: Overrides,
    },
    test?: {
      maxNestedDescribe?: number,
      testFunction?: 'it' | 'test',
      cypress?: boolean | {
        overrides?: Overrides,
      },
      playwright?: boolean | {
        overrides?: Overrides,
      },
      storybook?: boolean | {
        overrides?: Overrides,
      },
      vitest?: boolean | {
        overrides?: Overrides,
      },
    },
    typescript?: boolean | {
      allowedDefaultProjects?: string[],
      methodSignatureStyle?: 'method' | 'property',
      removeUnusedImports?: boolean,
      typeDefinitionStyle?: 'type' | 'interface',
      overrides?: Overrides,
    },
    unicorn?: boolean | {
      functionStyle?: {
        callbacks?: 'ignore' | 'arrow-function' | 'function-expression',
        default?: 'ignore' | 'declaration' | 'arrow-function' | 'function-expression',
        defaultExport?: 'ignore' | 'declaration' | 'arrow-function' | 'function-expression',
        namedExports?: 'ignore' | 'declaration' | 'arrow-function' | 'function-expression',
        namedFunctions?: 'ignore' | 'declaration' | 'arrow-function' | 'function-expression',
        objectProperties?: 'ignore' | 'method' | 'arrow-function' | 'function-expression',
        reassignedVariables?: 'ignore' | 'arrow-function' | 'function-expression',
        typedVariables?: 'ignore' | 'arrow-function' | 'function-expression',
      },
      overrides?: Overrides,
    },
    useBaseline?: boolean | {
      baseline?: number | 'newly' | 'widely',
      css?: {
        allowedAtRules?: AllowedAtRules,
        allowedFunctions?: AllowedFunctions,
        allowedMediaConditions?: AllowedMediaConditions,
        allowedProperties?: AllowedProperties,
        allowedPropertyValues?: AllowedPropertyValues,
        allowedSelectors?: AllowedSelectors,
        allowedUnits?: AllowedUnits,
      },
      javascript?: {
        ignoredFeatures?: string[],
        ignoredNodeTypes?: string[],
      },
      overrides?: Overrides,
    },
    vue?: boolean | {
      accessibility?: boolean | {
        accessibleChildComponents?: string[],
        anchorComponents?: string[],
        imageComponents?: string[],
      },
      allowedStyleAttributes?: ['plain' | 'module' | 'scoped', 'plain' | 'module' | 'scoped'],
      attributeHyphenation?: 'never' | 'always',
      attributesOrder?: (VueAttributeCategory | VueAttributeCategory[])[],
      blockLang?: {
        script?: 'js' | 'ts' | 'jsx' | 'tsx' | 'implicit',
        style?: 'css' | 'scss' | 'postcss' | 'implicit',
      },
      blocksOrder?: (
        | 'docs'
        | 'template'
        | 'script[setup]'
        | 'style[scoped]'
        | 'i18n[locale=en]'
        | 'script:not([setup])'
        | 'style:not([scoped])'
        | 'i18n:not([locale=en])'
      )[],
      componentNameCaseInTemplate?: 'kebab-case' | 'PascalCase',
      destructureProps?: 'never' | 'always' | 'only-when-assigned',
      ignoredUndefinedComponents?: string[],
      macrosOrder?: (
        | 'definePage'
        | 'defineEmits'
        | 'defineModel'
        | 'defineProps'
        | 'defineSlots'
        | 'defineCustom'
        | 'defineExpose'
        | 'defineOptions'
      )[],
      preferVBindSameNameShorthand?: 'never' | 'always',
      preferVBindTrueShorthand?: 'never' | 'always',
      restrictedElements?: (string | {
        element: string | string[],
        message: string,
      })[],
      restrictedStaticAttributes?: (string | {
        element: string,
        key: string,
        message: string,
        value: true | string,
      })[],
      templateMaxLineLength?: number,
      vForDelimiterStyle?: 'in' | 'of',
      overrides?: Overrides,
    },
    zod?: boolean | {
      mini?: boolean,
      overrides?: Overrides,
    },
  },
}

Versioning Policy

This project adheres to The Semantic Versioning Standard. However, to facilitate rapid development and fast iteration, the following changes are considered non-breaking:

  • Updates to dependency versions
  • Modifications to rule options
  • Enabling or disabling rules and plugins

Under this policy, minor updates may introduce new linting errors, which could break your project's build pipeline. To prevent this, it's recommended to use an exact version. Alternatively, you can use a tilde (~) version range in your package.json file (e.g., "@shayanthenerd/eslint-config": "~1.2.3"), which will restrict updates to patches only, ensuring your project's build pipeline remains stable.

You can find a list of all available versions and their changelogs on the releases page.

Contribution Guide

Contributions of all kinds are welcome. Please check out the CONTRIBUTING.md file.

Credits

This project was inspired by the work of Anthony Fu, whose generous contributions to the JavaScript and the ESLint ecosystem were instrumental in making it possible.

License

MIT License © 2025-PRESENT — Shayan Zamani

About

ESLint configuration for enforcing best practices and maintaining a consistent coding style.

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

10 stars

Watchers

0 watching

Forks

Releases

Sponsor this project

Used by

Contributors

Languages