A modern, flexible ESLint configuration for enforcing best practices and maintaining a consistent coding style.
- Performant: Powered by OXLint for rapid linting
- Flat Config: Type-safe ESLint Flat Config with
extends
andoverrides
support - Comprehensive: Dependency detection with support for TypeScript, Vue & Nuxt, Tailwind, Storybook, Vitest & Playwright, and more
- Automatic Formatting: Fine-grained control over formatting with ESLint Stylistic, eliminating the need for Prettier
- Smart Defaults: Respects your .gitignore file and provides reasonable, opinionated, yet highly customizable defaults
- Developer-friendly: Easy to use and well-documented with JSDoc
- Modern: Requires Node.js v20.12.0+ and ESLint v9.28.0+ (ESM-only)
Note
This configuration is designed with a flexible API for easy customization. However, it remains a personal config. While its primary goal is to enforce best practices and maintain code consistency, some rules—particularly stylistic ones—are rather opinionated.
If the available customization and override options still don't meet your requirements, feel free to fork the project and tailor it to your needs.
- Installation and Configuration
- Automatic Dependency Detection
- Formatting
- VS Code Integration
- Customization
- API Reference
- Versioning Policy
- Roadmap to v1.0.0
- Contribution Guide
- Credits
- License
- Install the package:
npm i -D @shayanthenerd/eslint-config
OXLint and all necessary ESLint plugins and parsers will be installed automatically.
- Create an ESLint config file (eslint.config.js) at the root of your project:
import { defineConfig } from '@shayanthenerd/eslint-config';
export default defineConfig();
You can also use a TypeScript file (eslint.config.ts). Depending on your Node.js version, additional setup may be required.
If you're using Nuxt, install @nuxt/eslint as a dev dependency:
npm i -D @nuxt/eslint
Then, update your ESLint config file:
import { defineConfig } from '@shayanthenerd/eslint-config';
import eslintConfigNuxt from './.nuxt/eslint.config.mjs';
const eslintConfig = defineConfig();
export default eslintConfigNuxt(eslintConfig);
- Create an OXLint config file (.oxlintrc.json) in the root of your project:
Due to the limitation of OXLint, only rules
, plugins
, and overrides
can be extended. Check out OXLint config reference for more details.
- Add the following scripts to your package.json file:
{
"scripts": {
"lint:inspect": "npx @eslint/config-inspector",
"lint:oxlint": "oxlint --fix",
"lint:eslint": "eslint --fix --cache --cache-location='node_modules/.cache/.eslintcache'",
"lint": "npm run lint:oxlint && npm run lint:eslint"
}
}
That's it! You can now run OXLint and ESLint in your project:
npm run lint
To get a visual breakdown of your configuration, run:
npm run lint:inspect
By default, this config automatically detects dependencies in your project, and enables the appropriate ESLint configurations. This is powered by local-pkg, which scans your node_modules directory instead of package.json.
For instance, if you install a package "A" that depends on another package "B", your package manager will install both. Even if "B" isn't listed in your package.json file, it will be present in node_modules. Consequently, this config will detect both "A" and "B" and enable the appropriate ESLint configurations for them.
To opt out of this behavior, you can either set autoDetectDeps: false
in the options object or explicitly disable any unwanted configurations that were automatically enabled.
Unlike other plugins, the configurations for Tailwind aren't automatically enabled upon dependency detection. This is because you must explicitly provide the path to your Tailwind config file or entry point.
Stylistic, Perfectionist, ImportX, and core (JavaScript) rules are enabled by default.
This config uses ESLint Stylistic to format JavaScript and TypeScript files (?([mc])[jt]s?(x)
) as well as the <script>
blocks in Vue components. HTML and the <template>
blocks in Vue components are formatted with html-eslint and eslint-plugin-vue, respectively. For other files such as CSS, JSON, and Markdown, you'll need Prettier. To make this easier, a customizable shared Prettier configuration is provided. Here's how to set it up:
- Install Prettier:
npm i -D prettier
- Create a Prettier config file (prettier.config.js) in the root of your project:
import prettierConfig from '@shayanthenerd/eslint-config/prettier';
/** @type {import('prettier').Config} */
export default {
...prettierConfig,
semi: false, // Override the `semi` option from the shared config
};
- To prevent conflicts with ESLint, Prettier should be configured to only format files other than JavaScript, TypeScript, HTML, and Vue. Hence, add the following script to your package.json file:
{
"scripts": {
"format": "prettier --write . '!**/*.{js,cjs,mjs,jsx,ts,cts,mts,tsx,html,vue}' --cache"
}
}
For IDE setup guidance, refer to VS Code Integration.
Install VS Code extensions for ESLint, OXLint, and Prettier. Then, add the following in the .vscode/settings.json file of your project:
{
/* Enforce Unix-like line endings (LF). */
"files.eol": "\n",
/* Enforce either tabs or spaces for indentation. */
"editor.tabSize": 2,
"editor.insertSpaces": false,
"editor.detectIndentation": false,
"editor.codeActionsOnSave": {
/* Imports are sorted and organized with eslint-plugin-perfectionist. */
"source.sortImports": "never",
"source.organizeImports": "never",
"source.removeUnusedImports": "never",
/* Apply OXLint and ESLint automatic fixes on file save. */
"source.fixAll.oxc": "explicit",
"source.fixAll.eslint": "explicit"
},
"eslint.run": "onSave",
"oxc.lint.run": "onSave",
"editor.formatOnSave": true,
/* Format and lint JavaScript, TypeScript, HTML, and Vue files with ESLint, while everything else is formatted with Prettier. */
"editor.defaultFormatter": "esbenp.prettier-vscode",
"[javascript][typescript][javascriptreact][typescriptreact][html][vue]": {
"editor.defaultFormatter": "dbaeumer.vscode-eslint"
},
"eslint.validate": [
"javascript",
"typescript",
"javascriptreact",
"typescriptreact",
"html",
"css",
"tailwindcss",
"vue"
],
/* Adjust these based on the features you're using to silently auto-fix the stylistic rules in your IDE. */
"tailwindCSS.lint.cssConflict": "ignore", // Only if you're using the Tailwind config
"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": "vue/max-len", "severity": "off", "fixable": true },
{ "rule": "vue/comma-dangle", "severity": "off", "fixable": true },
{ "rule": "vue/space-in-parens", "severity": "off", "fixable": true },
{ "rule": "better-tailwindcss/*", "severity": "off", "fixable": true },
{ "rule": "better-tailwindcss/no-restricted-classes", "severity": "error", "fixable": true },
{ "rule": "better-tailwindcss/no-conflicting-classes", "severity": "error", "fixable": false },
{ "rule": "better-tailwindcss/no-unregistered-classes", "severity": "error", "fixable": false }
]
}
Since OXLint and ESLint use separate config files, customizations made in your ESLint config will not apply to OXLint. However, you can still customize OXLint rules in your .oxlintrc.json file. Here's an example:
{
/* Base configuration */
"rules": {
/* Globally override rules. */
"oxlint/no-named-as-default-member": "warn"
},
"overrides": [
/* Override rules for specific files. */
{
"files": ["app/**/*.tsx"],
"ignores": ["app/app.tsx"],
"rules": {
"oxlint/max-depth": ["error", { "max": 5 }],
"oxlint/explicit-function-return-type": "off"
}
}
],
/* OXLint respects the ignore patterns defined in `.gitignore` and `.eslintignore` files by default. */
"ignorePatterns": ["**/*.min.*"]
}
defineConfig
takes the options
object as the first argument. options
is thoroughly documented with JSDoc, and provides many options for rule customizations. In addition, each config object in options.configs
accepts an overrides
option:
interface overrides {
name: '',
files: [],
ignores: [],
plugins: {},
settings: {},
languageOptions: {
parser: {},
globals: {},
},
rules: {},
}
overrides
is merged with the default config, taking precedence over its properties. However, there is no guarantee that the resulting configuration works correctly — it depends on the options you provide.
defineConfig
also accepts any number of custom ESLint Flat Configs (eslint.config.js):
import eslintPluginYaml from 'eslint-plugin-yaml';
import * as eslintPluginRegexp from 'eslint-plugin-regexp';
import { defineConfig } from '@shayanthenerd/eslint-config';
export default defineConfig(
/* The options object: */
{
env: 'bun',
configs: {
typescript: {
typeDefinitionStyle: 'type',
overrides: {
rules: {
'@typescript-eslint/no-unsafe-type-assertion': 'off',
},
},
},
},
},
/* ESLint Flat Configs: */
{
files: ['**/*.yaml', '**/*.yml'],
ignores: ['**/*.schema.yaml', '**/*.schema.yml'],
extends: [pluginYaml.configs.recommended],
},
regexpPlugin.configs['flat/recommended'],
);
The API reference
Some types are omitted or aliased for brevity.interface options {
autoDetectDeps?: boolean | 'verbose',
gitignore?: false | string,
packageDir?: string,
env?: 'bun' | 'node',
tsConfig?: {
rootDir: string,
filename?: string,
},
global?: {
name?: string,
basePath?: string,
ignores?: string[],
globals?: {
node?: boolean,
commonjs?: boolean,
browser?: boolean,
worker?: boolean,
serviceworker?: boolean,
webextension?: boolean,
custom?: {
[key: string]: boolean | 'off' | 'readonly' | 'readable' | 'writable' | 'writeable',
},
}
linterOptions?: {
noInlineConfig?: boolean,
reportUnusedInlineConfigs?: 'error' | 'off' | 'warn',
reportUnusedDisableDirectives?: 'error' | 'off' | 'warn',
},
settings?: {
[name: string]: unknown,
}
rules?: Linter.RulesRecord,
},
configs?: {
oxlint?: false | string,
base?: {
maxDepth?: number,
maxNestedCallbacks?: number,
preferNamedExports?: boolean,
functionStyle?: 'declaration' | 'expression',
overrides?: {},
},
stylistic?: boolean | {
semi?: 'always' | 'never',
trailingComma?: 'always' | 'never' | 'always-multiline' | 'only-multiline',
memberDelimiterStyle?: 'semi' | 'comma',
quotes?: 'single' | 'double' | 'backtick',
jsxQuotes?: 'prefer-single' | 'prefer-double',
arrowParens?: 'always' | 'as-needed',
useTabs?: boolean,
indent?: number,
maxConsecutiveEmptyLines?: number,
maxLineLength?: number,
maxAttributesPerLine?: number,
selfCloseVoidHTMLElements?: 'never' | 'always',
overrides?: {},
},
html?: boolean | {
useBaseline?: number | false | 'widely' | 'newly',
idNamingConvention?: 'camelCase' | 'snake_case' | 'PascalCase' | 'kebab-case',
overrides?: {},
},
css?: boolean | {
useBaseline?: number | false | 'widely' | 'newly',
allowedRelativeFontUnits?: ('%' | 'cap' | 'ch' | 'em' | 'ex' | 'ic' | 'lh' | 'rcap' | 'rch' | 'rem' | 'rex' | 'ric' | 'rlh')[],
overrides?: {},
},
tailwind?: false | {
config: string,
entryPoint?: string,
multilineSort?: boolean,
ignoredUnregisteredClasses?: string[],
overrides?: {},
} | {
config?: string,
entryPoint: string,
multilineSort?: boolean,
ignoredUnregisteredClasses?: string[],
overrides?: {},
},
typescript?: boolean | {
allowedDefaultProjects?: string[],
methodSignatureStyle?: 'property' | 'method',
typeDefinitionStyle?: 'interface' | 'type',
overrides?: {},
},
importX?: boolean | {
removeUnusedImports?: boolean,
overrides?: {},
},
perfectionist?: boolean | {
sortType?: 'custom' | 'natural' | 'alphabetical' | 'line-length' | 'unsorted',
overrides?: {},
},
vue?: boolean | {
accessibility?: boolean | {
anchorComponents?: string[],
imageComponents?: string[],
accessibleChildComponents?: string[],
},
blockOrder?: (
| 'docs'
| 'template'
| 'script[setup]'
| 'style[scoped]'
| 'i18n[locale=en]'
| 'script:not([setup])'
| 'style:not([scoped])'
| 'i18n:not([locale=en])'
)[],
macrosOrder?: (
| 'definePage'
| 'defineModel'
| 'defineProps'
| 'defineEmits'
| 'defineSlots'
| 'defineCustom'
| 'defineExpose'
| 'defineOptions'
)[],
attributesOrder?: RuleOptions<'vue/attributes-order'>['order'],
attributeHyphenation?: 'never' | 'always',
preferVBindSameNameShorthand?: 'never' | 'always',
preferVBindTrueShorthand?: 'never' | 'always',
allowedStyleAttributes?: ['module' | 'plain' | 'scoped', 'module' | 'plain' | 'scoped'],
blockLang?: {
style?: 'css' | 'implicit' | 'scss' | 'postcss',
script?: 'js' | 'ts' | 'jsx' | 'tsx' | 'implicit',
},
destructureProps?: 'never' | 'always',
componentNameCaseInTemplate?: 'PascalCase' | 'kebab-case',
vForDelimiterStyle?: 'in' | 'of',
vOnHandlerStyle?: 'inline' | 'inline-function' | ['method', 'inline' | 'inline-function'],
restrictedElements?: (string | {
element: string | string[],
message: string,
})[],
restrictedStaticAttributes?: (string | {
key: string,
value?: string | true,
element: string,
message: string,
})[],
ignoredUndefinedComponents?: string[],
overrides?: {},
},
nuxt?: boolean | {
image?: boolean,
ui?: boolean | {
prefix?: string,
}
},
test?: {
storybook?: boolean | {
overrides?: {},
},
vitest?: boolean | {
overrides?: {},
},
playwright?: boolean | {
overrides?: {},
},
cypress?: boolean | {
overrides?: {},
},
testFunction?: 'test' | 'it',
maxNestedDescribe?: number,
},
},
}
This project adheres to The Semantic Versioning Standard. However, to facilitate rapid development and fast iteration, the following changes are considered non-breaking:
- Upgrades 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.
- Integrate additional ESLint plugins, such as eslint-plugin-unicorn, eslint-plugin-n, and eslint-plugin-jsdoc.
- Add support for other frameworks and file types, including Astro, React, Next.js, MDX, Markdown, and JSON.
- Reduce bundle size by dynamically (programmatically) installing dependencies as needed.
- Develop a starter wizard to automate the setup of OXLint, ESLint, Prettier, and other configurations.
Any form of contribution is always appreciated! Please refer to the CONTRIBUTING.md file.
This project was inspired by the work of Anthony Fu, whose generous contributions to the JavaScript and ESLint ecosystem were instrumental in making it possible.
MIT License © 2025-PRESENT — Shayan Zamani