ESLint 13 🛡️ Plugin Rules — The plugin/rule namespacing convention
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 package | Default namespace | Rule reference |
|---|---|---|
eslint-plugin-jsdoc | jsdoc | jsdoc/rule-name |
eslint-plugin-react-hooks | react-hooks | react-hooks/rule-name |
@typescript-eslint/eslint-plugin | @typescript-eslint | @typescript-eslint/rule-name |
| Local plugin | The plugins key | local/rule-name |
The plugins key
| Type | Example |
|---|---|
| Shorthand | plugins: { jsdoc } |
| Explicit | plugins: { jsdoc: jsdoc } |
| Renamed | plugins: { ts: tseslint } |
| Scoped | plugins: { "@typescript-eslint": tseslint } |
The rule ID
| Part | Example |
|---|---|
| Namespace | jsdoc |
| Separator | / |
| Rule name | require-description |
| Full ID | jsdoc/require-description |
The errors
| Error | Cause | Fix |
|---|---|---|
Could not find plugin | Namespace not in the plugins key | Match the prefix |
Definition for rule not found | Rule name misspelled | Check the plugin’s rules |
| Plugin config not applied | Used plugins instead of extends | Use 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
| Pitfall | Why It Happens | Fix |
|---|---|---|
| Rule is not found | Namespace mismatch | Match the rule prefix to the plugins key |
| Plugin is not registered | Imported but not in the plugins key | Add the plugin to the key |
| Scoped rule ID fails | The @ and slash are not quoted | Quote the property name |
| Collision between plugins | Same namespace | Rename one in the plugins key |
| Config does not apply the rules | Used plugins instead of extends | Use 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
| Item | Value |
|---|---|
| Convention | namespace/rule-name |
| Namespace source | The plugins object key |
| Default namespace | Package name without eslint-plugin- |
| Scoped namespace | The scope (@typescript-eslint) |
| Rename | Change the plugins key |
| Collision | Rename one of the plugins |
| Mismatch error | Could not find plugin |
| Plugin config | extends: [plugin.configs.config] |
Key takeaways:
- The namespace is the
pluginsobject key, not the package name. The rule reference must use the same prefix as the key. A plugin assigned asplugins: { jsdoc }has the rule prefixjsdoc/, and a rule reference with a different prefix produces theCould not find pluginerror . - The scoped plugin namespace includes the scope.
@typescript-eslint/eslint-pluginuses the namespace@typescript-eslint, which makes the rule ID contain two slashes. The property name in thepluginsobject 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
rulesobject. The full ID is the namespace, a slash, and the rule name, such asjsdoc/require-description. - The plugin’s config applies the rules with the namespace already included. The
tseslint.configs.recommendedconfig registers the plugin with the@typescript-eslintnamespace 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.
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!