Creating Plugins
Plugins are the primary way to extend ESLint with custom functionality. A plugin can bundle rules, processors, parsers, and configurations into a single, reusable package.
What is a Plugin?
An ESLint plugin is a JavaScript object that exports:
Rules - Custom linting rules
Processors - Transform non-JavaScript files
Configs - Shareable configurations
Parsers - Custom syntax support
Popular Plugins:
@typescript-eslint/eslint-plugin - TypeScript rules
eslint-plugin-react - React-specific rules
eslint-plugin-vue - Vue.js support
eslint-plugin-import - Import/export validation
Plugin Structure
A plugin is an object with specific properties:
const plugin = {
meta: {
name: "eslint-plugin-example" ,
version: "1.0.0" ,
namespace: "example"
},
configs: {},
rules: {},
processors: {},
};
// ES Module
export default plugin ;
// CommonJS
module . exports = plugin ;
Provide metadata for better debugging and caching:
Plugin name (should match npm package name) meta : {
name : "eslint-plugin-myproject"
}
Plugin version (should match npm package version) meta : {
version : "1.0.0"
}
Default namespace for accessing plugin features meta : {
namespace : "myproject"
}
Read metadata from package.json: import fs from "fs" ;
const pkg = JSON . parse (
fs . readFileSync ( new URL ( "./package.json" , import . meta . url ), "utf8" )
);
const plugin = {
meta: {
name: pkg . name ,
version: pkg . version ,
namespace: "myproject"
}
};
Adding Rules
Export rules in the rules object:
const plugin = {
meta: {
name: "eslint-plugin-example" ,
version: "1.0.0"
},
rules: {
// Rule ID: rule object
"no-foo" : {
meta: {
type: "suggestion" ,
docs: {
description: "Disallow foo"
},
messages: {
avoid: "Avoid using 'foo'."
}
},
create ( context ) {
return {
Identifier ( node ) {
if ( node . name === "foo" ) {
context . report ({
node ,
messageId: "avoid"
});
}
}
};
}
},
"no-bar" : {
meta: { /* ... */ },
create ( context ) { /* ... */ }
}
}
};
export default plugin ;
Rule ID Naming:
Rule IDs should not contain / characters. Use kebab-case: no-foo, enforce-pattern.
Using Plugin Rules
import example from "eslint-plugin-example" ;
export default [
{
plugins: {
example
} ,
rules: {
"example/no-foo" : "error" ,
"example/no-bar" : "warn"
}
}
] ;
Adding Processors
Export processors for non-JavaScript files:
const plugin = {
meta: {
name: "eslint-plugin-markdown" ,
version: "1.0.0"
},
processors: {
// Processor for .md files
"markdown" : {
meta: {
name: "markdown-processor" ,
version: "1.0.0"
},
preprocess ( text , filename ) {
// Extract code blocks
const blocks = extractCodeBlocks ( text );
return blocks . map (( block , i ) => ({
text: block . code ,
filename: ` ${ i } .js`
}));
},
postprocess ( messages , filename ) {
// Adjust message locations
return messages . flat ();
},
supportsAutofix: true
}
}
};
Using Plugin Processors
import markdown from "eslint-plugin-markdown" ;
export default [
{
files: [ "**/*.md" ] ,
plugins: { markdown } ,
processor: "markdown/markdown"
}
] ;
Adding Configurations
Bundle recommended configurations:
const plugin = {
meta: {
name: "eslint-plugin-example" ,
version: "1.0.0"
},
rules: {
"no-foo" : { /* ... */ },
"no-bar" : { /* ... */ }
},
configs: {}
};
// Add configs after defining rules
Object . assign ( plugin . configs , {
recommended: [
{
plugins: {
example: plugin
},
rules: {
"example/no-foo" : "error" ,
"example/no-bar" : "warn"
}
}
],
strict: [
{
plugins: {
example: plugin
},
rules: {
"example/no-foo" : "error" ,
"example/no-bar" : "error"
}
}
]
});
export default plugin ;
Config Format:
Configs should be arrays of configuration objects (flat config format). You can also export a single object if there’s only one configuration.
Using Plugin Configs
import example from "eslint-plugin-example" ;
export default [
{
files: [ "**/*.js" ] ,
plugins: { example } ,
extends: [ "example/recommended" ]
}
] ;
Complete Plugin Example
Here’s a full plugin with rules, configs, and metadata:
import fs from "fs" ;
import path from "path" ;
import { fileURLToPath } from "url" ;
const __dirname = path . dirname ( fileURLToPath ( import . meta . url ));
const pkg = JSON . parse (
fs . readFileSync ( path . join ( __dirname , "package.json" ), "utf8" )
);
const plugin = {
meta: {
name: pkg . name ,
version: pkg . version ,
namespace: "myproject"
},
rules: {
"no-dollar-sign" : {
meta: {
type: "suggestion" ,
docs: {
description: "Disallow identifiers starting with $" ,
url: "https://example.com/rules/no-dollar-sign"
},
messages: {
noDollar: "Identifier '{{name}}' should not start with '$'."
},
schema: []
},
create ( context ) {
return {
Identifier ( node ) {
if ( node . name . startsWith ( "$" )) {
context . report ({
node ,
messageId: "noDollar" ,
data: { name: node . name }
});
}
}
};
}
},
"require-copyright" : {
meta: {
type: "suggestion" ,
docs: {
description: "Require copyright header in files"
},
messages: {
missing: "Missing copyright header."
}
},
create ( context ) {
return {
Program ( node ) {
const sourceCode = context . sourceCode ;
const comments = sourceCode . getAllComments ();
const hasCopyright = comments . some ( comment =>
/copyright/ i . test ( comment . value )
);
if ( ! hasCopyright ) {
context . report ({
node ,
messageId: "missing"
});
}
}
};
}
}
},
configs: {}
};
// Add configs
Object . assign ( plugin . configs , {
recommended: [
{
plugins: {
myproject: plugin
},
rules: {
"myproject/no-dollar-sign" : "error" ,
"myproject/require-copyright" : "warn"
}
}
]
});
export default plugin ;
Plugin Naming Conventions
Follow these naming conventions for better discoverability: Unscoped packages:
Start with eslint-plugin-
Example: eslint-plugin-myproject
Scoped packages:
Format: @scope/eslint-plugin or @scope/eslint-plugin-name
Examples: @company/eslint-plugin, @company/eslint-plugin-custom
Project Structure
Organize your plugin project:
eslint-plugin-myproject/
├── package.json
├── README.md
├── index.js # Plugin entry point
├── lib/
│ ├── rules/
│ │ ├── no-foo.js
│ │ └── no-bar.js
│ ├── processors/
│ │ └── markdown.js
│ └── configs/
│ ├── recommended.js
│ └── strict.js
└── tests/
├── rules/
│ ├── no-foo.test.js
│ └── no-bar.test.js
└── processors/
└── markdown.test.js
Organizing Rules
export default {
meta: {
type: "suggestion" ,
docs: {
description: "Disallow foo"
},
messages: {
avoid: "Avoid foo."
}
} ,
create ( context ) {
// Rule implementation
}
} ;
import noFoo from "./lib/rules/no-foo.js" ;
import noBar from "./lib/rules/no-bar.js" ;
const plugin = {
meta: { /* ... */ },
rules: {
"no-foo" : noFoo ,
"no-bar" : noBar
}
};
export default plugin ;
Creating Your Plugin
Initialize Project
mkdir eslint-plugin-myproject
cd eslint-plugin-myproject
npm init -y
Install Dependencies
npm install --save-dev eslint
Create Plugin File
const plugin = {
meta: {
name: "eslint-plugin-myproject" ,
version: "1.0.0" ,
namespace: "myproject"
},
rules: {
// Add your rules
},
configs: {}
};
module . exports = plugin ;
Test Locally
Link plugin for local testing: In test project: npm link eslint-plugin-myproject
Testing Plugins
Test rules using RuleTester:
tests/rules/no-foo.test.js
import { RuleTester } from "eslint" ;
import rule from "../../lib/rules/no-foo.js" ;
const ruleTester = new RuleTester ();
ruleTester . run ( "no-foo" , rule , {
valid: [
"const bar = 1;" ,
"function baz() {}"
],
invalid: [
{
code: "const foo = 1;" ,
errors: [{ messageId: "avoid" }]
},
{
code: "function foo() {}" ,
errors: [{ messageId: "avoid" }]
}
]
});
Integration Tests
Test the complete plugin:
import { ESLint } from "eslint" ;
import plugin from "../index.js" ;
const eslint = new ESLint ({
overrideConfigFile: true ,
overrideConfig: [
{
plugins: {
myproject: plugin
},
rules: {
"myproject/no-foo" : "error"
}
}
]
});
const results = await eslint . lintText ( "const foo = 1;" );
console . log ( results [ 0 ]. messages ); // Should have errors
Publishing Your Plugin
Update package.json
{
"name" : "eslint-plugin-myproject" ,
"version" : "1.0.0" ,
"description" : "Custom ESLint rules for MyProject" ,
"main" : "index.js" ,
"keywords" : [
"eslint" ,
"eslintplugin" ,
"eslint-plugin"
],
"peerDependencies" : {
"eslint" : ">=9.0.0"
},
"author" : "Your Name" ,
"license" : "MIT"
}
Add README
Document:
Installation instructions
Available rules
Configuration examples
Rule options
Install in Projects
npm install --save-dev eslint-plugin-myproject
Configure: import myproject from "eslint-plugin-myproject" ;
export default [
{
plugins: { myproject } ,
extends: [ "myproject/recommended" ]
}
] ;
Legacy Config Support
Support both flat and legacy configs:
const plugin = {
meta: {
name: "eslint-plugin-example" ,
version: "1.0.0"
},
rules: { /* ... */ },
configs: {}
};
Object . assign ( plugin . configs , {
// Flat config format
"flat/recommended" : [
{
plugins: { example: plugin },
rules: {
"example/no-foo" : "error"
}
}
],
// Legacy format
recommended: {
plugins: [ "example" ],
rules: {
"example/no-foo" : "error"
}
}
});
Linting Your Plugin
Lint plugin code with recommended configs:
import js from "@eslint/js" ;
import eslintPlugin from "eslint-plugin-eslint-plugin" ;
export default [
js . configs . recommended ,
eslintPlugin . configs . recommended ,
{
files: [ "lib/rules/**/*.js" ] ,
rules: {
"eslint-plugin/require-meta-docs-url" : "error"
}
}
] ;
Recommended Linting:
eslint - Core linting
eslint-plugin-eslint-plugin - Plugin-specific rules
eslint-plugin-n - Node.js best practices
Distribution Checklist
Best Practices
Follow these guidelines:
Use clear rule names - Descriptive, kebab-case IDs
Provide good error messages - Help users understand issues
Document thoroughly - Examples for valid/invalid code
Test comprehensively - Cover edge cases
Version carefully - Follow semver strictly
Maintain backwards compatibility - Deprecate before removing
Rule Documentation Template
# no-foo (eslint-plugin-myproject)
Disallow the use of `foo` identifiers.
## Rule Details
This rule disallows using `foo` as an identifier name.
## Examples
### Invalid
\`\`\` javascript
const foo = 1;
function foo() {}
\`\`\`
### Valid
\`\`\` javascript
const bar = 1;
function baz() {}
\`\`\`
## When Not To Use It
If your codebase allows `foo` identifiers.
Real-World Examples
@typescript-eslint/eslint-plugin Comprehensive TypeScript plugin
eslint-plugin-react React-specific rules and configs
eslint-plugin-vue Vue.js plugin with processor
eslint-plugin-import Import/export validation
Next Steps
Custom Rules Learn to create powerful rules
Custom Processors Add processor support
Shareable Configs Create standalone config packages