@shayanthenerd/eslint-config
v0.28.0
Published
A modern, flexible ESLint configuration for enforcing best practices and maintaining a consistent coding style
Readme
@shayanthenerd/eslint-config

ESLint configuration for enforcing best practices and maintaining a consistent coding style. Inspect configurations!
- Flexible: Highly-customizable options and configurations with sensible defaults.
- Smart: Context-aware linting with automatic dependency detection and .gitignore recognition.
- Comprehensive: Supports useful plugins for TypeScript, React & Next, Vue & Nuxt, Astro, Tailwind, and more.
- Type-safe: Fully-typed and well-documented API with
overridessupport for built-in configuration objects. - Modern: Requires ESLint ^10.6.0 and Node.js ^20.19.0 (ESM-only)
Table of Contents
- Plugin Support
- Installation and Configuration
- Customization
- Automatic Dependency Detection
- Framework and Tool Integrations
- IDE Support
- Formatting
- API Reference
- Versioning Policy
- Roadmap to v1.0.0
- Contribution Guide
- Credits
- License
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
Install the package and ESLint as dev dependencies:
npm i -D @shayanthenerd/eslint-config eslintCreate 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.
Add the following scripts to your package.json:
{ "scripts": { "lint:inspect": "npx @eslint/config-inspector", "lint": "eslint --fix --cache --cache-location='node_modules/.cache/.eslintcache'" } }
After installation:
- Use
npm run lintto lint and fix files. - Use
npm run lint:inspectto 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-storybookdepends onstorybook, which is hoisted by NPM and Bun. As a result, the integration forstorybookwill 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
Nuxt support is already included through the Vue integration, so you don’t need to install or configure @nuxt/eslint manually. If you need additional rules tailored to Nuxt, refer to the plugin’s documentation for setup instructions.
[!TIP]
configs.nuxtonly includes options for Nuxt Image, Nuxt UI, and the<Icon>component. Vue's rules andoverridescan be configured viaconfigs.vue.
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: trueand ESLint's VS Code extension isn't working as expected, add the following to your pnpm-workspace.yaml and runpnpm 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.
Install Prettier as a dev dependency:
npm i -D prettierCreate 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 --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
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?: {
functionStyle?: 'expression' | 'declaration',
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,
},
},
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 | {
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,
})[],
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.
Roadmap to v1.0.0
- [x] Add integration for ESLint plugins such as eslint-plugin-n, eslint-plugin-unicorn, and more.
- [x] Add support for React, Next, Astro, and Markdown.
- [ ] Develop an interactive starter wizard to quickly scaffold the configurations for ESLint, Prettier, etc.
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
