npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@shayanthenerd/eslint-config

v0.16.0

Published

A modern, flexible ESLint configuration for enforcing best practices and maintaining a consistent coding style

Downloads

361

Readme

@shayanthenerd/eslint-config    NPM Version License MIT Netlify Status

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 extends and overrides support
  • 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

  1. Install the package:
npm i -D @shayanthenerd/eslint-config
# or
bun i -d @shayanthenerd/eslint-config
# or
pnpm i -D @shayanthenerd/eslint-config oxlint

This will install OXLint along with all the necessary ESLint plugins and parsers. However, if you're using PNPM, you must install oxlint separately.

  1. 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);

[!NOTE] The Nuxt config relies on the Vue config, so make sure it's enabled (either automatically or manually).

  1. If you're not using OXLint, set configs.oxlint to false in 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.

  1. 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

Automatic 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 on storybook, NPM and Bun hoist storybook to the root of your node_modules. As a result, the ESLint configuration for storybook will 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:

  1. Install Prettier:
npm i -D prettier
  1. 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;
  1. 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