| | |

ESLint 13 🛡️ Plugin Rules — The plugin/rule namespacing convention

⏱️ Estimated read: 9 min | |

A plugin’s rules are referenced with a namespace prefix. The prefix is the property name that the plugin is assigned to in the plugins object, and the rule is referenced as namespace/rule-name. The convention exists because a plugin’s rule names are not guaranteed to be unique: two plugins can both define a rule called no-unused-vars, and the namespace is what distinguishes them. The flat config made the namespace explicit: it is the key in the plugins object, and it can be renamed, which is the mechanism that resolves a collision between two plugins that use the same namespace .

This chapter covers the namespace convention, the scoped plugin naming pattern, the collision problem and its rename fix, and the difference between the plugin’s namespace and the rule name.

Key point: The namespace is the plugins object key, not the plugin package name. The default is the package name without the eslint-plugin- prefix, but the project can rename it. The rule reference is namespace/rule-name, and the namespace must match the key in the plugins object exactly.


Why the namespace convention exists

The uniqueness problem. ESLint’s core rules have no namespace: no-unused-vars, eqeqeq, no-console. A plugin’s rules need a prefix because the plugin’s authors cannot know which other plugins a project will install. The plugin/rule convention is the mechanism that prevents two plugins from colliding on the same rule name. The flat config’s plugins object makes the namespace explicit, which is what allows the project to resolve a collision by renaming one of the plugins .

The scoped package problem. A plugin published under a scoped package, such as @typescript-eslint/eslint-plugin, has a scoped namespace: @typescript-eslint/no-unused-vars. The namespace is the scope plus the package name without the eslint-plugin- prefix. The rule reference includes the @ and the slash, which is why the rule ID contains two slashes: @typescript-eslint/no-unused-vars .

The flat config problem. The legacy .eslintrc format resolved the namespace from the plugin package name by convention. The flat config requires the namespace to be specified as the plugins object key. The key is the namespace, and the rule reference must match it exactly. A plugin assigned as plugins: { ts: typescript } has the rule prefix ts/, not @typescript-eslint/ .


a. The namespace and the rule reference

The plugin is imported and assigned to a namespace. The rule is referenced with the namespace prefix.

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

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

The plugins: { jsdoc } assignment makes the namespace jsdoc. The rule reference jsdoc/require-description uses the same prefix . The two strings must match exactly: a rule reference with a namespace that does not appear in the plugins object produces the error “Could not find plugin” .

The rule ID in the rules object is the namespace, a slash, and the rule name. The namespace can be a single word (jsdoc), a hyphenated name (react-hooks), or a scoped name (@typescript-eslint). The rule name is the key in the plugin’s rules object .


b. The scoped plugin namespace

A scoped plugin uses the scope as part of the namespace. The @typescript-eslint/eslint-plugin package is the scoped form of the TypeScript-ESLint plugin, and its namespace is @typescript-eslint.

import tseslint from "@typescript-eslint/eslint-plugin"

export default [
  {
    plugins: { "@typescript-eslint": tseslint },
    rules: {
      "@typescript-eslint/no-unused-vars": "error",
    },
  },
]

The plugins key is the string "@typescript-eslint", which is a quoted property name because it contains a slash and an @. The rule reference is @typescript-eslint/no-unused-vars, which contains two slashes: one after the scope and one before the rule name .

The TypeScript-ESLint project also exports a configs object that includes the recommended config with the namespace already applied. The spread of tseslint.configs.recommended registers the plugin and enables the rules without the project needing to write the namespace manually .


c. The collision problem and the rename fix

Two plugins can use the same namespace. The eslint-plugin-import package and a hypothetical local plugin both assigned to import would collide. The flat config resolves the collision by renaming one of the plugins in the plugins object.

import importPlugin from "eslint-plugin-import"
import importLocal from "./local-import-plugin.js"

export default [
  {
    plugins: {
      import: importPlugin,
      "import-local": importLocal,
    },
    rules: {
      "import/no-unresolved": "error",
      "import-local/no-unresolved": "error",
    },
  },
]

The import namespace is assigned to the npm plugin, and the import-local namespace is assigned to the local plugin. The rule references use the corresponding prefix. The rename is the mechanism that resolves the collision without disabling either plugin .

The rename also applies to a plugin whose default namespace is inconvenient. The @typescript-eslint namespace contains a slash and an @, which makes the rule references awkward. A project can rename it to ts and use ts/no-unused-vars instead.


Complete Example Session

// ============================================
// PART 1: THE SIMPLE NAMESPACE
// ============================================
import jsdoc from "eslint-plugin-jsdoc"

export default [
  {
    plugins: { jsdoc },
    rules: { "jsdoc/require-description": "error" },
  },
]
// ============================================
// PART 2: THE HYPHENATED NAMESPACE
// ============================================
import reactHooks from "eslint-plugin-react-hooks"

export default [
  {
    plugins: { "react-hooks": reactHooks },
    rules: { "react-hooks/exhaustive-deps": "warn" },
  },
]
// ============================================
// PART 3: THE SCOPED NAMESPACE
// ============================================
import tseslint from "@typescript-eslint/eslint-plugin"

export default [
  {
    plugins: { "@typescript-eslint": tseslint },
    rules: { "@typescript-eslint/no-unused-vars": "error" },
  },
]
// ============================================
// PART 4: THE RENAMED NAMESPACE
// ============================================
import tseslint from "@typescript-eslint/eslint-plugin"

export default [
  {
    plugins: { ts: tseslint },
    rules: { "ts/no-unused-vars": "error" },
  },
]
// ============================================
// PART 5: THE COLLISION
// ============================================
import importPlugin from "eslint-plugin-import"
import importLocal from "./local-import-plugin.js"

export default [
  {
    plugins: {
      import: importPlugin,
      "import-local": importLocal,
    },
    rules: {
      "import/no-unresolved": "error",
      "import-local/no-unresolved": "error",
    },
  },
]
// ============================================
// PART 6: THE MISMATCH ERROR
// ============================================
import jsdoc from "eslint-plugin-jsdoc"

export default [
  {
    plugins: { jsdoc },
    rules: { "jsd/require-description": "error" },
  },
]
// Error: Could not find plugin "jsd".
// ============================================
// PART 7: THE PLUGIN CONFIG
// ============================================
import tseslint from "typescript-eslint"

export default [
  ...tseslint.configs.recommended,
]
// The config registers the plugin and the rules.
// ============================================
// PART 8: THE RULE OBJECT
// ============================================
// A plugin's rules object:
{
  rules: {
    "require-description": { /* rule implementation */ },
  },
}

// The rule name is "require-description".
// The full ID is "jsdoc/require-description".
// ============================================
// PART 9: THE NAMESPACE IN A CONFIG
// ============================================
// The plugin's config includes:
{
  plugins: { jsdoc },
  rules: { "jsdoc/require-description": "error" },
}
// ============================================
// PART 10: THE DECISION
// ============================================
// Default namespace → package name without prefix
// Collision → rename in the plugins object
// Scoped plugin → scope as namespace

The ten parts covered the simple namespace, the hyphenated namespace, the scoped namespace, the renamed namespace, the collision, the mismatch error, the plugin config, the rule object, the namespace in a config, and the decision.


Quick Reference

The namespace

Plugin packageDefault namespaceRule reference
eslint-plugin-jsdocjsdocjsdoc/rule-name
eslint-plugin-react-hooksreact-hooksreact-hooks/rule-name
@typescript-eslint/eslint-plugin@typescript-eslint@typescript-eslint/rule-name
Local pluginThe plugins keylocal/rule-name

The plugins key

TypeExample
Shorthandplugins: { jsdoc }
Explicitplugins: { jsdoc: jsdoc }
Renamedplugins: { ts: tseslint }
Scopedplugins: { "@typescript-eslint": tseslint }

The rule ID

PartExample
Namespacejsdoc
Separator/
Rule namerequire-description
Full IDjsdoc/require-description

The errors

ErrorCauseFix
Could not find pluginNamespace not in the plugins keyMatch the prefix
Definition for rule not foundRule name misspelledCheck the plugin’s rules
Plugin config not appliedUsed plugins instead of extendsUse extends

Best Practices

✅ Do This:

// Use the namespace prefix in the rule ID
"jsdoc/require-description": "error"                         // ✅
// Match the plugins key to the rule prefix
plugins: { jsdoc }, rules: { "jsdoc/rule": "error" }         // ✅
// Rename the namespace to resolve a collision
plugins: { "import-local": importLocal }                     // ✅
// Use the plugin config for the recommended set
...tseslint.configs.recommended                              // ✅

❌ Don’t Do This:

// Reference the rule without the namespace
"require-description": "error"                               // ❌
// Use a namespace that is not in the plugins key
plugins: { jsdoc }, rules: { "jsd/rule": "error" }           // ❌
// Assume the namespace is the package name
plugins: { "eslint-plugin-jsdoc": jsdoc }                    // ❌ (prefix is the key)

Common Pitfalls

PitfallWhy It HappensFix
Rule is not foundNamespace mismatchMatch the rule prefix to the plugins key
Plugin is not registeredImported but not in the plugins keyAdd the plugin to the key
Scoped rule ID failsThe @ and slash are not quotedQuote the property name
Collision between pluginsSame namespaceRename one in the plugins key
Config does not apply the rulesUsed plugins instead of extendsUse extends: [config]

Real-World Examples

1. Simple namespace

plugins: { jsdoc }, rules: { "jsdoc/require-description": "error" }

2. Hyphenated namespace

plugins: { "react-hooks": reactHooks }, rules: { "react-hooks/exhaustive-deps": "warn" }

3. Scoped namespace

plugins: { "@typescript-eslint": tseslint }, rules: { "@typescript-eslint/no-unused-vars": "error" }

4. Renamed namespace

plugins: { ts: tseslint }, rules: { "ts/no-unused-vars": "error" }

5. Collision fix

plugins: { import: importPlugin, "import-local": importLocal }

6. The mismatch error

plugins: { jsdoc }, rules: { "jsd/rule": "error" } // error

7. The plugin config

...tseslint.configs.recommended

8. The rule object

{ rules: { "require-description": { /* ... */ } } }

9. The namespace in the config

{ plugins: { jsdoc }, rules: { "jsdoc/rule": "error" } }

10. The decision

Default → package name; Collision → rename; Scoped → scope

Visual

The namespace mapping

┌─────────────────────────────────────────────────────────────┐
│  plugins: { jsdoc }                                          │
│       │                                                      │
│       ▼                                                      │
│  namespace = "jsdoc"                                         │
│       │                                                      │
│       ▼                                                      │
│  rules: { "jsdoc/require-description": "error" }             │
│                                                              │
│  The key is the prefix.                                      │
└─────────────────────────────────────────────────────────────┘

The scoped namespace

┌─────────────────────────────────────────────────────────────┐
│  plugins: { "@typescript-eslint": tseslint }                 │
│       │                                                      │
│       ▼                                                      │
│  namespace = "@typescript-eslint"                            │
│       │                                                      │
│       ▼                                                      │
│  rule ID = "@typescript-eslint/no-unused-vars"               │
│             └──────┬──────┘ └──────┬──────┘                  │
│                  scope           rule name                   │
└─────────────────────────────────────────────────────────────┘

The collision fix

┌─────────────────────────────────────────────────────────────┐
│  plugins: {                                                  │
│    import: importPlugin,        ← npm plugin                │
│    "import-local": importLocal, ← local plugin              │
│  }                                                           │
│                                                              │
│  rules: {                                                    │
│    "import/no-unresolved": "error",                         │
│    "import-local/no-unresolved": "error",                   │
│  }                                                           │
│                                                              │
│  The rename resolves the collision.                          │
└─────────────────────────────────────────────────────────────┘

The mismatch error

┌─────────────────────────────────────────────────────────────┐
│  plugins: { jsdoc }                                          │
│  rules: { "jsd/rule": "error" }                              │
│       │                                                      │
│       ▼                                                      │
│  Could not find plugin "jsd".                                │
│                                                              │
│  The prefix must match the plugins key.                      │
└─────────────────────────────────────────────────────────────┘

Summary

ItemValue
Conventionnamespace/rule-name
Namespace sourceThe plugins object key
Default namespacePackage name without eslint-plugin-
Scoped namespaceThe scope (@typescript-eslint)
RenameChange the plugins key
CollisionRename one of the plugins
Mismatch errorCould not find plugin
Plugin configextends: [plugin.configs.config]

Key takeaways:

  • The namespace is the plugins object key, not the package name. The rule reference must use the same prefix as the key. A plugin assigned as plugins: { jsdoc } has the rule prefix jsdoc/, and a rule reference with a different prefix produces the Could not find plugin error .
  • The scoped plugin namespace includes the scope. @typescript-eslint/eslint-plugin uses the namespace @typescript-eslint, which makes the rule ID contain two slashes. The property name in the plugins object must be quoted because it contains an @ and a slash .
  • The namespace can be renamed. The flat config allows the project to assign the plugin to any key. The rename resolves a collision between two plugins that use the same namespace, and it can also shorten an awkward scoped namespace .
  • The rule ID has three parts: namespace, separator, and rule name. The rule name is the key in the plugin’s rules object. The full ID is the namespace, a slash, and the rule name, such as jsdoc/require-description.
  • The plugin’s config applies the rules with the namespace already included. The tseslint.configs.recommended config registers the plugin with the @typescript-eslint namespace and enables the recommended rules. The project does not need to write the namespace manually when it uses the config .

Remember: The plugin/rule convention is the mechanism that keeps a plugin’s rule names from colliding with the core rules and with other plugins. The namespace is the plugins object key, and the rule reference must match it exactly. The key can be renamed, which is how the project resolves a collision or shortens a scoped namespace. The plugin’s config is the shortcut that registers the plugin and enables a set of rules at once. The mismatch between the key and the prefix is the most common error, and the error message names the missing plugin.

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!