| | |

ESLint 12 🛡️ Plugins — Importing rules from external packages

⏱️ Estimated read: 9 min | |

A plugin is an npm package that exports an object conforming to ESLint’s plugin interface. The object contains rules, configs, and processors. Importing a plugin into the flat config is the mechanism that makes its rules available to the rules object, its configs available to extends, and its processors available to the processor key. The import is explicit JavaScript: import example from "eslint-plugin-example" and then plugins: { example } in the config object . The plugin’s rules are referenced with the plugin’s namespace as a prefix, such as "example/rule1".

This chapter covers the plugin object, the namespace convention that determines the rule prefix, the difference between a plugin’s rules and its configs, and the processor key that extracts JavaScript from non-JavaScript files.

Key point: The plugins key is an object, not an array. The property name is the namespace, and the value is the imported plugin object. The namespace is the prefix for the plugin’s rules in the rules key. The plugin’s configs are applied through extends, not through the plugins key.


Why plugins are objects, not strings

The resolution problem. In the legacy .eslintrc format, plugins were referenced by string name: plugins: ["react"]. ESLint resolved eslint-plugin-react from node_modules by convention. In the flat config, the plugin is imported as a JavaScript module: import react from "eslint-plugin-react". The import is resolved by Node.js, not by ESLint’s convention. The plugin object is then assigned to a namespace in the plugins key .

The namespace problem. The namespace is the property name in the plugins object. It determines the rule prefix. If the plugin is assigned as plugins: { react }, the rules are referenced as react/jsx-uses-react. If the plugin is assigned as plugins: { r: react }, the rules are referenced as r/jsx-uses-react. The namespace does not need to match the plugin’s package name, though it usually does .

The config problem. A plugin can export shareable configs under its configs property. A plugin’s recommended config is applied through the extends key, not through the plugins key. The plugins key makes the rules available; the extends key applies a config that uses those rules .


a. The plugin object and the plugins key

The plugin is imported and assigned to a namespace. The namespace is the prefix for the plugin’s rules.

// eslint.config.js
import jsdoc from "eslint-plugin-jsdoc"

export default [
  {
    plugins: {
      jsdoc: jsdoc,
    },
    rules: {
      "jsdoc/require-description": "error",
      "jsdoc/check-values": "warn",
    },
  },
]

The plugins key is an object. The property name jsdoc is the namespace. The value is the imported plugin object. The rules are referenced as jsdoc/require-description and jsdoc/check-values . The namespace can be shortened to the same name as the import for the common case: plugins: { jsdoc } is equivalent to plugins: { jsdoc: jsdoc }.

A plugin can be a local file, not just an npm package. The import is a relative path, and the namespace is the property name in the plugins object .

import local from "./my-local-plugin.js"

export default [
  {
    plugins: { local },
    rules: { "local/rule1": "error" },
  },
]

b. The plugin’s configs and the extends key

A plugin’s exported configs are applied through the extends key. The TypeScript-ESLint plugin, for example, exports its recommended config as tseslint.configs.recommended. The config is an object that includes the plugin registration and the rule settings.

import tseslint from "typescript-eslint"

export default [
  ...tseslint.configs.recommended,
]

The spread operator applies the array of config objects. The plugin’s config already includes the plugins key with the @typescript-eslint namespace and the rules key with the recommended rules . The project does not need to import the plugin separately when the config includes it.

The distinction matters for the project that wants the plugin’s rules without the plugin’s recommended config. The plugins key registers the plugin, and the rules key enables the specific rules the project wants. The extends key applies a pre-built config that registers the plugin and enables a set of rules at once .


c. The processor key

A processor extracts JavaScript code from a non-JavaScript file and lets ESLint lint the extracted code. The processor key references the processor by its namespace and processor name.

import markdown from "@eslint/markdown"

export default [
  {
    files: ["**/*.md"],
    plugins: { markdown },
    processor: "markdown/markdown",
  },
]

The files key scopes the processor to Markdown files. The processor value is the namespace and the processor name separated by a slash . The processor can create named code blocks, such as 0.js and 1.js, which ESLint treats as child files of the original. Additional config objects can target the named blocks with the **/*.md/*.js pattern .

A processor can also be embedded directly in the config without the plugins key. The documentation shows the pattern: processor: markdown.processors.markdown . The plugin does not need to be registered if the processor object is referenced directly.


Complete Example Session

// ============================================
// PART 1: THE PLUGIN IMPORT
// ============================================
import jsdoc from "eslint-plugin-jsdoc"

export default [
  {
    plugins: { jsdoc },
  },
]
// ============================================
// PART 2: THE RULES
// ============================================
import jsdoc from "eslint-plugin-jsdoc"

export default [
  {
    plugins: { jsdoc },
    rules: {
      "jsdoc/require-description": "error",
      "jsdoc/check-values": "warn",
    },
  },
]
// ============================================
// PART 3: THE NAMESPACE RENAME
// ============================================
import jsdoc from "eslint-plugin-jsdoc"

export default [
  {
    plugins: { jsd: jsdoc },
    rules: {
      "jsd/require-description": "error",
    },
  },
]
// ============================================
// PART 4: THE LOCAL PLUGIN
// ============================================
import local from "./my-local-plugin.js"

export default [
  {
    plugins: { local },
    rules: { "local/rule1": "error" },
  },
]
// ============================================
// PART 5: THE PLUGIN CONFIG
// ============================================
import tseslint from "typescript-eslint"

export default [
  ...tseslint.configs.recommended,
]
// ============================================
// PART 6: THE MANUAL RULES
// ============================================
import tseslint from "@typescript-eslint/eslint-plugin"

export default [
  {
    plugins: { "@typescript-eslint": tseslint },
    rules: {
      "@typescript-eslint/no-unused-vars": "error",
    },
  },
]
// ============================================
// PART 7: THE PROCESSOR
// ============================================
import markdown from "@eslint/markdown"

export default [
  {
    files: ["**/*.md"],
    plugins: { markdown },
    processor: "markdown/markdown",
  },
]
// ============================================
// PART 8: THE NAMED CODE BLOCK
// ============================================
export default [
  {
    files: ["**/*.md"],
    plugins: { markdown },
    processor: "markdown/markdown",
  },
  {
    files: ["**/*.md/*.js"],
    rules: { "no-console": "off" },
  },
]
// ============================================
// PART 9: THE DIRECT PROCESSOR
// ============================================
import markdown from "@eslint/markdown"

export default [
  {
    files: ["**/*.md"],
    processor: markdown.processors.markdown,
  },
]
// ============================================
// PART 10: THE DECISION
// ============================================
// Plugin rules only → plugins + rules
// Plugin config → extends
// Non-JS file → processor

The ten parts covered the plugin import, the rules, the namespace rename, the local plugin, the plugin config, the manual rules, the processor, the named code block, the direct processor, and the decision.


Quick Reference

The plugins key

PropertyValue
TypeObject
KeyNamespace (rule prefix)
ValueImported plugin object
Rule referencenamespace/rule-name

The plugin object

PropertyPurpose
rulesCustom rule implementations
configsShareable configs
processorsCode extractors
metaName and version for caching

The processor key

ValueEffect
namespace/processor-nameReferences the processor
plugin.processors.nameDirect reference, no plugins key needed

The configs

AccessEffect
plugin.configs.recommendedThe plugin’s recommended rules
...plugin.configs.recommendedSpread into the array

Best Practices

✅ Do This:

// Import the plugin and assign a namespace
import jsdoc from "eslint-plugin-jsdoc"
plugins: { jsdoc }                                           // ✅
// Reference the rules with the namespace prefix
"jsdoc/require-description": "error"                         // ✅
// Use extends for the plugin's config
...tseslint.configs.recommended                              // ✅
// Use the processor key for non-JS files
processor: "markdown/markdown"                               // ✅

❌ Don’t Do This:

// Use an array for the plugins key
plugins: ["jsdoc"]                                           // ❌
// Reference the rule without the namespace
"require-description": "error"                               // ❌
// Expect the plugins key to apply the plugin's config
plugins: { jsdoc }  // rules not enabled                      // ❌

Common Pitfalls

PitfallWhy It HappensFix
Rule is not foundNamespace does not match the plugins keyCheck the prefix
Plugin is not registeredImported but not in the plugins keyAdd plugins: { name: plugin }
Plugin config does not applyUsed plugins instead of extendsUse extends: [config]
Processor does not runMissing files patternAdd the pattern to the object
Named code block is not lintedNo config for the child patternAdd files: ["**/*.md/*.js"]

Real-World Examples

1. The plugin import

import jsdoc from "eslint-plugin-jsdoc"

2. The plugins key

plugins: { jsdoc }

3. The rule reference

"jsdoc/require-description": "error"

4. The namespace rename

plugins: { jsd: jsdoc }

5. The local plugin

import local from "./my-local-plugin.js"
plugins: { local }

6. The plugin config

...tseslint.configs.recommended

7. The processor

processor: "markdown/markdown"

8. The named block

files: ["**/*.md/*.js"]

9. The direct processor

processor: markdown.processors.markdown

10. The decision

Rules only → plugins + rules; Config → extends; Non-JS → processor

Visual

The plugin flow

┌─────────────────────────────────────────────────────────────┐
│  import jsdoc from "eslint-plugin-jsdoc"                     │
│       │                                                      │
│       ▼                                                      │
│  plugins: { jsdoc }                                          │
│       │                                                      │
│       ▼                                                      │
│  rules: { "jsdoc/require-description": "error" }             │
│                                                              │
│  The namespace is the rule prefix.                           │
└─────────────────────────────────────────────────────────────┘

The config vs the plugin

┌─────────────────────────────────────────────────────────────┐
│  plugins: { jsdoc }                                          │
│    → registers the plugin, enables nothing                    │
│                                                              │
│  extends: [jsdoc.configs.recommended]                        │
│    → applies the config, registers the plugin and rules      │
└─────────────────────────────────────────────────────────────┘

The processor

┌─────────────────────────────────────────────────────────────┐
│  files: ["**/*.md"]                                          │
│  plugins: { markdown }                                       │
│  processor: "markdown/markdown"                              │
│       │                                                      │
│       ▼                                                      │
│  The processor extracts JS blocks from the Markdown.         │
│  ESLint lints the extracted blocks.                          │
└─────────────────────────────────────────────────────────────┘

The namespace

┌─────────────────────────────────────────────────────────────┐
│  plugins: { jsdoc: plugin }                                  │
│    → rule prefix: jsdoc/                                     │
│                                                              │
│  plugins: { jsd: plugin }                                    │
│    → rule prefix: jsd/                                       │
│                                                              │
│  The namespace is whatever the property name is.             │
└─────────────────────────────────────────────────────────────┘

Summary

ItemValue
Pluginnpm package exporting rules, configs, processors
Importimport name from "eslint-plugin-name"
Registrationplugins: { namespace: plugin }
Rule referencenamespace/rule-name
Config applicationextends: [plugin.configs.config]
Processorprocessor: "namespace/processor-name"
Local pluginimport local from "./file.js"
Named code blockfiles: ["**/*.md/*.js"]

Key takeaways:

  • A plugin is an object, not a string. The flat config imports the plugin as a JavaScript module and assigns it to a namespace in the plugins key. The namespace is the property name, and it determines the rule prefix .
  • The plugins key registers the plugin; it does not enable its rules. The rules are enabled in the rules key with the namespace prefix. A plugin that is registered but has no rules configured does nothing.
  • A plugin’s config is applied through extends, not through plugins. The config is a pre-built object that registers the plugin and enables a set of rules. The spread operator applies an array of configs .
  • The namespace can be renamed. The property name in the plugins object is the namespace, and it does not need to match the plugin’s package name. A renamed namespace changes the rule prefix .
  • A processor extracts JavaScript from non-JavaScript files. The processor key references the processor by namespace and name. The named code blocks that the processor creates can be targeted with a files pattern .
  • A plugin can be a local file. The import is a relative path, and the namespace is the property name in the plugins object. The plugin does not need to be published to npm .

Remember: The plugins key is an object that maps a namespace to an imported plugin object. The namespace is the rule prefix. The rules are enabled in the rules key. The plugin’s configs are applied through extends. The processors are referenced through the processor key. The import is explicit JavaScript, not a string convention. And the namespace can be anything the project chooses, as long as the rule references use the same prefix.

Was this helpful?


Stop using slow, ad-bloated tool sites! 🤮

🔎 Search “KandZ Tools” on Google to use many professional utilities for free.

KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)

⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free

🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!