ESLint 12 🛡️ Plugins — Importing rules from external packages
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
| Property | Value |
|---|---|
| Type | Object |
| Key | Namespace (rule prefix) |
| Value | Imported plugin object |
| Rule reference | namespace/rule-name |
The plugin object
| Property | Purpose |
|---|---|
rules | Custom rule implementations |
configs | Shareable configs |
processors | Code extractors |
meta | Name and version for caching |
The processor key
| Value | Effect |
|---|---|
namespace/processor-name | References the processor |
plugin.processors.name | Direct reference, no plugins key needed |
The configs
| Access | Effect |
|---|---|
plugin.configs.recommended | The plugin’s recommended rules |
...plugin.configs.recommended | Spread 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
| Pitfall | Why It Happens | Fix |
|---|---|---|
| Rule is not found | Namespace does not match the plugins key | Check the prefix |
| Plugin is not registered | Imported but not in the plugins key | Add plugins: { name: plugin } |
| Plugin config does not apply | Used plugins instead of extends | Use extends: [config] |
| Processor does not run | Missing files pattern | Add the pattern to the object |
| Named code block is not linted | No config for the child pattern | Add 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
| Item | Value |
|---|---|
| Plugin | npm package exporting rules, configs, processors |
| Import | import name from "eslint-plugin-name" |
| Registration | plugins: { namespace: plugin } |
| Rule reference | namespace/rule-name |
| Config application | extends: [plugin.configs.config] |
| Processor | processor: "namespace/processor-name" |
| Local plugin | import local from "./file.js" |
| Named code block | files: ["**/*.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
pluginskey. The namespace is the property name, and it determines the rule prefix . - The
pluginskey registers the plugin; it does not enable its rules. The rules are enabled in theruleskey 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 throughplugins. 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
pluginsobject 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
processorkey references the processor by namespace and name. The named code blocks that the processor creates can be targeted with afilespattern . - A plugin can be a local file. The import is a relative path, and the namespace is the property name in the
pluginsobject. 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.
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!