Custom Processors
Processors enable ESLint to extract and lint JavaScript from non-JavaScript files. They’re perfect for Markdown code blocks, HTML script tags, or framework-specific file formats.
When to Use Processors
Create a processor when you need to:
Lint JavaScript in Markdown code blocks
Extract scripts from HTML files
Process framework-specific files (Vue, Svelte)
Handle template languages
Transform files before linting
Popular Processors:
@eslint/markdown - Lint JavaScript in Markdown
eslint-plugin-vue - Vue single-file components
eslint-plugin-html - HTML script tags
Processor Structure
A processor is an object with preprocess() and postprocess() methods:
const plugin = {
meta: {
name: "eslint-plugin-example" ,
version: "1.2.3"
},
processors: {
"processor-name" : {
meta: {
name: "eslint-processor-name" ,
version: "1.2.3"
},
// Extract JavaScript code
preprocess ( text , filename ) {
return [
{ text: code1 , filename: "0.js" },
{ text: code2 , filename: "1.js" }
];
},
// Adjust and aggregate messages
postprocess ( messages , filename ) {
return []. concat ( ... messages );
},
supportsAutofix: true
}
}
};
export default plugin ;
Processor Methods
preprocess(text, filename)
Extracts JavaScript code blocks from the file Parameters:
text (string): File contents
filename (string): File path
Returns: Array of code blocks, each with:
text (string): JavaScript code to lint
filename (string): Virtual filename (should include extension)
preprocess ( text , filename ) {
const blocks = extractCodeBlocks ( text );
return blocks . map (( code , index ) => ({
text: code ,
filename: ` ${ index } .js`
}));
}
postprocess(messages, filename)
Adjusts lint message locations and aggregates results Parameters:
messages (Message[][]): Two-dimensional array of messages
filename (string): File path
Returns: Flat array of adjusted messagespostprocess ( messages , filename ) {
// messages[0] = messages for first code block
// messages[1] = messages for second code block
return messages
. flat ()
. map ( adjustMessageLocation );
}
Set to true to enable automatic fixes When supportsAutofix is true, postprocess() must also transform the fix property of messages to reference the original file locations.
Message Structure
Lint messages have this structure:
type LintMessage = {
line ?: number ; // 1-based line number
column ?: number ; // 1-based column number
endLine ?: number ; // 1-based end line
endColumn ?: number ; // 1-based end column
ruleId : string | null ; // Rule ID
message : string ; // Error message
severity : 0 | 1 | 2 ; // off, warn, error
fix ?: {
range : [ number , number ];
text : string ;
};
suggestions ?: Array <{
desc ?: string ;
messageId ?: string ;
fix : {
range : [ number , number ];
text : string ;
};
}>;
};
Example: Markdown Processor
Extract JavaScript from Markdown code blocks:
const plugin = {
meta: {
name: "eslint-plugin-markdown" ,
version: "1.0.0"
},
processors: {
markdown: {
meta: {
name: "markdown-processor" ,
version: "1.0.0"
},
preprocess ( text , filename ) {
const codeBlocks = [];
const regex = /``` (?: javascript | js ) \n ( [ \s\S ] *? ) ```/ g ;
let match ;
while (( match = regex . exec ( text )) !== null ) {
const code = match [ 1 ];
const offset = match . index + match [ 0 ]. indexOf ( code );
codeBlocks . push ({
text: code ,
filename: ` ${ codeBlocks . length } .js` ,
offset // Store for postprocess
});
}
// Store offsets for postprocessing
this . offsets = codeBlocks . map ( block => block . offset );
return codeBlocks ;
},
postprocess ( messages , filename ) {
// Flatten messages from all code blocks
return messages . flatMap (( blockMessages , index ) => {
const offset = this . offsets [ index ];
const offsetLines = text . slice ( 0 , offset )
. split ( ' \n ' ). length - 1 ;
return blockMessages . map ( message => ({
... message ,
// Adjust line numbers to original file
line: message . line + offsetLines ,
endLine: message . endLine
? message . endLine + offsetLines
: undefined
}));
});
},
supportsAutofix: false
}
}
};
export default plugin ;
Example: HTML Processor
Extract JavaScript from HTML script tags:
const plugin = {
processors: {
".html" : {
preprocess ( text , filename ) {
const scripts = [];
const scriptRegex = /<script [ ^ > ] * > ( [ \s\S ] *? ) < \/ script>/ gi ;
let match ;
while (( match = scriptRegex . exec ( text )) !== null ) {
const code = match [ 1 ];
const offset = match . index + match [ 0 ]. indexOf ( code );
// Calculate line offset
const lineOffset = text . slice ( 0 , offset )
. split ( ' \n ' ). length - 1 ;
scripts . push ({
text: code ,
filename: ` ${ scripts . length } .js` ,
lineOffset
});
}
this . lineOffsets = scripts . map ( s => s . lineOffset );
return scripts ;
},
postprocess ( messages , filename ) {
return messages . flatMap (( scriptMessages , index ) => {
const lineOffset = this . lineOffsets [ index ];
return scriptMessages . map ( msg => ({
... msg ,
line: ( msg . line || 0 ) + lineOffset ,
endLine: msg . endLine
? msg . endLine + lineOffset
: undefined ,
column: msg . column ,
endColumn: msg . endColumn
}));
});
}
}
}
};
Supporting Autofixes
To support automatic fixes, transform fix ranges to original file positions:
processors : {
"processor-name" : {
preprocess ( text , filename ) {
const blocks = extractBlocks ( text );
// Store mapping info
this . blocks = blocks . map ( block => ({
originalOffset: block . offset ,
text: block . text
}));
return blocks . map (( block , i ) => ({
text: block . text ,
filename: ` ${ i } .js`
}));
},
postprocess ( messages , filename ) {
return messages . flatMap (( blockMessages , blockIndex ) => {
const block = this . blocks [ blockIndex ];
const originalOffset = block . originalOffset ;
return blockMessages . map ( message => {
const adjustedMessage = {
... message ,
line: calculateOriginalLine ( message . line , originalOffset ),
column: message . column
};
// Adjust fix ranges
if ( message . fix ) {
adjustedMessage . fix = {
range: [
message . fix . range [ 0 ] + originalOffset ,
message . fix . range [ 1 ] + originalOffset
],
text: message . fix . text
};
}
// Adjust suggestion fix ranges
if ( message . suggestions ) {
adjustedMessage . suggestions = message . suggestions . map ( sugg => ({
... sugg ,
fix: {
range: [
sugg . fix . range [ 0 ] + originalOffset ,
sugg . fix . range [ 1 ] + originalOffset
],
text: sugg . fix . text
}
}));
}
return adjustedMessage ;
});
});
},
supportsAutofix: true
}
}
Define at plugin level:
const plugin = {
meta: {
name: "eslint-plugin-example" ,
version: "1.2.3" ,
namespace: "example"
},
processors: { /* ... */ }
};
Define for each processor:
processors : {
"processor-name" : {
meta: {
name: "eslint-processor-name" ,
version: "1.2.3"
},
preprocess () { /* ... */ },
postprocess () { /* ... */ }
}
}
Why Both Meta Objects? The plugin meta is used when the processor is referenced by string (e.g., "example/processor-name"). The processor meta is used when the processor object is passed directly in configuration.
Creating Your Processor
Identify Code Blocks
Determine how to extract JavaScript from your file format: function extractCodeBlocks ( text ) {
// For Markdown
const blocks = [];
const regex = /```js \n ( [ \s\S ] *? ) ```/ g ;
let match ;
while (( match = regex . exec ( text ))) {
blocks . push ({
code: match [ 1 ],
offset: match . index ,
line: text . slice ( 0 , match . index )
. split ( ' \n ' ). length
});
}
return blocks ;
}
Implement Preprocess
Extract and return code blocks: preprocess ( text , filename ) {
const blocks = extractCodeBlocks ( text );
// Store metadata for postprocess
this . blockMetadata = blocks . map ( b => ({
offset: b . offset ,
line: b . line
}));
return blocks . map (( block , i ) => ({
text: block . code ,
filename: ` ${ i } .js`
}));
}
Implement Postprocess
Adjust message locations: postprocess ( messages , filename ) {
return messages . flatMap (( blockMsgs , i ) => {
const metadata = this . blockMetadata [ i ];
return blockMsgs . map ( msg => ({
... msg ,
line: msg . line + metadata . line - 1 ,
endLine: msg . endLine
? msg . endLine + metadata . line - 1
: undefined
}));
});
}
Test Thoroughly
Test with various code block configurations: const processor = plugin . processors . markdown ;
const input = `
# Title
\`\`\` javascript
const x = 1;
\`\`\`
Text
\`\`\` js
const y = 2;
\`\`\`
` ;
const blocks = processor . preprocess ( input , "test.md" );
console . log ( blocks );
// [
// { text: "const x = 1;\n", filename: "0.js" },
// { text: "const y = 2;\n", filename: "1.js" }
// ]
Using Processors in Configuration
Configure ESLint to use your processor:
import example from "eslint-plugin-example" ;
export default [
{
files: [ "**/*.md" ] ,
plugins: {
example
} ,
processor: "example/markdown"
},
{
// Rules for extracted JavaScript
files: [ "**/*.md/*.js" ] ,
rules: {
"no-console" : "off" ,
"no-unused-vars" : "warn"
}
}
] ;
Virtual Filenames ESLint uses the filename from preprocessed blocks to match configuration. Use patterns like **/*.md/*.js to target extracted code.
Advanced Patterns
Multiple File Types
Handle different file types with one plugin:
const plugin = {
processors: {
".md" : markdownProcessor ,
".html" : htmlProcessor ,
".vue" : vueProcessor
}
};
Conditional Processing
Process only certain code blocks:
preprocess ( text , filename ) {
const blocks = [];
const regex = /``` ( \w + ) \n ( [ \s\S ] *? ) ```/ g ;
let match ;
while (( match = regex . exec ( text ))) {
const lang = match [ 1 ];
const code = match [ 2 ];
// Only process JavaScript and TypeScript
if ( lang === 'javascript' || lang === 'js' || lang === 'typescript' || lang === 'ts' ) {
blocks . push ({
text: code ,
filename: ` ${ blocks . length } . ${ lang === 'typescript' || lang === 'ts' ? 'ts' : 'js' } `
});
}
}
return blocks ;
}
Preserving Indentation
Maintain original indentation in extracted code:
preprocess ( text , filename ) {
const blocks = extractBlocks ( text );
return blocks . map (( block , i ) => {
// Calculate base indentation
const lines = block . text . split ( ' \n ' );
const firstLine = lines . find ( line => line . trim ());
const baseIndent = firstLine ? firstLine . match ( / ^ \s * / )[ 0 ]. length : 0 ;
// Remove base indentation
const dedented = lines
. map ( line => line . slice ( baseIndent ))
. join ( ' \n ' );
return {
text: dedented ,
filename: ` ${ i } .js`
};
});
}
Testing Processors
Unit Tests
const assert = require ( "assert" );
const plugin = require ( "./index" );
const processor = plugin . processors . markdown ;
describe ( "Markdown Processor" , () => {
describe ( "preprocess" , () => {
it ( "should extract code blocks" , () => {
const input = `
# Title
\`\`\` javascript
const x = 1;
\`\`\`
` ;
const blocks = processor . preprocess ( input , "test.md" );
assert . strictEqual ( blocks . length , 1 );
assert . strictEqual ( blocks [ 0 ]. text . trim (), "const x = 1;" );
});
it ( "should handle multiple blocks" , () => {
const input = `
\`\`\` js
const a = 1;
\`\`\`
\`\`\` js
const b = 2;
\`\`\`
` ;
const blocks = processor . preprocess ( input , "test.md" );
assert . strictEqual ( blocks . length , 2 );
});
});
describe ( "postprocess" , () => {
it ( "should adjust line numbers" , () => {
const messages = [
[{
line: 1 ,
column: 7 ,
message: "Error" ,
ruleId: "no-unused-vars" ,
severity: 2
}]
];
// Simulate processor state
processor . offsets = [ 50 ]; // Code block starts at char 50
const result = processor . postprocess ( messages , "test.md" );
assert ( result [ 0 ]. line > 1 ); // Line adjusted
});
});
});
Integration Tests
const { ESLint } = require ( "eslint" );
const plugin = require ( "./index" );
const eslint = new ESLint ({
overrideConfigFile: true ,
overrideConfig: [
{
files: [ "**/*.md" ],
plugins: { example: plugin },
processor: "example/markdown"
},
{
files: [ "**/*.md/*.js" ],
rules: {
"no-unused-vars" : "error"
}
}
]
});
const results = await eslint . lintText (
`
# Test
\`\`\` javascript
const unused = 1;
\`\`\`
` ,
{ filePath: "test.md" }
);
console . log ( results [ 0 ]. messages );
Processors run on every file. Optimize performance:
Cache regex patterns - Compile once, use many times
Minimize string operations - Avoid unnecessary slicing/splitting
Use efficient parsing - Consider parser libraries for complex formats
Limit regex backtracking - Use possessive quantifiers
// Good: Compile regex once
const CODE_BLOCK_REGEX = /``` (?: javascript | js ) \n ( [ \s\S ] *? ) ```/ g ;
preprocess ( text , filename ) {
const blocks = [];
let match ;
// Reuse compiled regex
CODE_BLOCK_REGEX . lastIndex = 0 ;
while (( match = CODE_BLOCK_REGEX . exec ( text ))) {
blocks . push ({
text: match [ 1 ],
filename: ` ${ blocks . length } .js`
});
}
return blocks ;
}
Common Pitfalls
Incorrect Line Number Adjustments
Always calculate line offsets based on the original file: // Calculate line offset
const lineOffset = text . slice ( 0 , blockOffset )
. split ( ' \n ' ). length - 1 ;
// Adjust message
message . line += lineOffset ;
Forgetting to Flatten Messages
postprocess() receives a 2D array but must return a 1D array:// Wrong
return messages ;
// Right
return messages . flat ();
// or
return []. concat ( ... messages );
Not Setting supportsAutofix
If you want fixes to work, set supportsAutofix: true and adjust fix ranges in postprocess().
Losing Message Properties
Preserve all message properties when adjusting: return blockMessages . map ( msg => ({
... msg , // Preserve all properties
line: adjustedLine ,
endLine: adjustedEndLine
}));
Real-World Examples
@eslint/markdown Official Markdown processor
eslint-plugin-vue Vue single-file component processor
eslint-plugin-html HTML script tag processor
eslint-plugin-svelte Svelte component processor
Next Steps
Create a Plugin Package your processor for distribution
Configure Processors Learn how users configure processors