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

A modern, flexible ESLint configuration for enforcing best practices and maintaining a consistent coding style.
- Performant: Powered by OXLint (OXC Linter) for rapid linting
- Flat Config: Type-safe ESLint Flat Config with
extendsandoverridessupport - Comprehensive: Dependency detection with support for TypeScript, Astro, 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.
Table of Contents
- Installation and Configuration
- Automatic Dependency Detection
- Formatting
- VS Code Integration
- Customization
- API Reference
- Versioning Policy
- Roadmap to v1.0.0
- Contribution Guide
- Credits
- License
Installation and Configuration
- Install the package:
npm i -D @shayanthenerd/eslint-config
# or
bun i -d @shayanthenerd/eslint-config
# or
pnpm i -D @shayanthenerd/eslint-config oxlintThis will install OXLint along with all the necessary ESLint plugins and parsers. However, if you're using PNPM, you must install oxlint separately.
- 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/eslintThen, 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);[!NOTE] The Nuxt config relies on the Vue config, so make sure it's enabled (either automatically or manually).
- If you're not using OXLint, set
configs.oxlinttofalsein your ESLint config and skip this step. Otherwise, create an OXLint config file (.oxlintrc.json) in the root of your project:
{
"$schema": "./node_modules/oxlint/configuration_schema.json", // Optional
"extends": ["./node_modules/@shayanthenerd/eslint-config/dist/oxlint.config.jsonc"],
/* Customize based on your development environment. */
"env": {
"worker": false,
"commonjs": false,
"bun": false,
"deno": false,
"node": true,
"nodeBuiltin": true,
"browser": true,
"serviceworker": false,
"sharedWorker": false,
"webextension": false,
"audioWorklet": false,
"vitest": true,
"vue": true,
"astro": true
},
"categories": {
"correctness": "error",
"suspicious": "error",
"restriction": "error",
"pedantic": "error",
"perf": "warn",
"style": "warn",
"nursery": "error"
}
}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 lintTo get a visual breakdown of your configuration, run:
npm run lint:inspectAutomatic Dependency Detection
This package automatically detects dependencies in your project and enables the corresponding ESLint configurations for them. This is powered by local-pkg, which scans your node_modules directory instead of package.json.
[!IMPORTANT] This behavior is particularly noticeable with package managers that use a flat node_modules structure, such as NPM or Bun. A concrete example is
eslint-plugin-storybook, which is a dependency of this package. Since the plugin transitively depends onstorybook, NPM and Bun hoiststorybookto the root of your node_modules. As a result, the ESLint configuration forstorybookwill be automatically enabled, even if you haven't explicitly installed it. Using a package manager with strict dependency resolution, such as PNPM, prevents this issue by hiding transitive dependencies from the root of your node_modules.
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 configuration for Tailwind isn't automatically enabled upon dependency detection, because you must explicitly provide the path to your Tailwind entry point (config file), or the ESLint configuration won't work as expected.
Stylistic, Perfectionist, ImportX, and core (JavaScript) rules are enabled by default.
Formatting
This config uses ESLint Stylistic to format JavaScript and TypeScript files (?([mc])[jt]s?(x)) as well as Astro (similar to JSX/TSX) and 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 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;- To prevent conflicts with ESLint, Prettier should be configured to only format files other than JavaScript, TypeScript, Astro, 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,astro}' --cache"
}
}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": 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 OXLint and ESLint automatic fixes on file save. */
"source.fixAll.oxc": "explicit",
"source.fixAll.eslint": "explicit"
},
"oxc.lint.run": "onSave",
"eslint.run": "onSave",
"editor.formatOnSave": true,
"eslint.format.enable": 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][astro]": {
"editor.defaultFormatter": "dbaeumer.vscode-eslint"
},
"eslint.validate": [
"javascript",
"typescript",
"javascriptreact",
"typescriptreact",
"html",
"css",
"tailwindcss",
"vue",
"astro"
],
/* 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
"tailwindCSS.lint.recommendedVariantOrder": "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 }
]
}Customization
OXLint
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.*"]
}ESLint
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'],
);API Reference
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?: {
worker?: boolean,
commonjs?: boolean,
bun?: boolean,
deno?: boolean,
node?: boolean,
nodeBuiltin?: boolean,
browser?: boolean,
serviceworker?: boolean,
sharedWorker?: boolean,
webextension?: boolean,
audioWorklet?: boolean,
vitest?: boolean,
vue?: boolean,
astro?: 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',
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,
icon?: boolean | {
component?: string,
}
ui?: boolean | {
prefix?: string,
}
},
test?: {
storybook?: boolean | {
overrides?: {},
},
vitest?: boolean | {
overrides?: {},
},
playwright?: boolean | {
overrides?: {},
},
cypress?: boolean | {
overrides?: {},
},
testFunction?: 'test' | 'it',
maxNestedDescribe?: number,
},
},
}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:
- 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.
Roadmap to v1.0.0
- [ ] Integrate additional ESLint plugins such as eslint-plugin-unicorn, eslint-plugin-n, eslint-plugin-jsdoc, etc.
- [ ] Add support for other frameworks and file types, including Astro, React, Next.js, MDX, Markdown, JSON, etc.
- [ ] Develop a starter wizard to automate the setup of OXLint, ESLint, Prettier, and other configurations.
Contribution Guide
Any form of contribution is always appreciated! Please chekc 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
