@tony.ganchev/eslint-plugin-header
v3.2.4
Published
The native ESLint 9/10 header plugin. A zero-bloat, drop-in replacement for 'eslint-plugin-header' with first-class Flat Config & TypeScript support. Auto-fix Copyright, License, and banner comments in JavaScrip and TypeScript files.
Downloads
91,488
Maintainers
Readme
@tony.ganchev/eslint-plugin-header
The native ESLint 9/10 standard header-validating plugin. A zero-bloat, drop-in replacement for eslint-plugin-header with first-class Flat Config & TypeScript support. Auto-fix copyright, license, and banner comments in JavaScript and TypeScript files.
Table of Contents
Motivation and Acknowledgements
The plugin started as a fork of eslint-plugin-header to address missing ESLint 9 compatibility.
Today it addresses the following issues:
- Support for ESLint 9/10 with a fully-validated configuration schema.
- Continued support for ESLint 7/8.
- Complete Windows support.
- New object-based configuration providing the bases for future enhancements.
- Continued support for eslint-plugin-header array configuration.
- Bugfixes where the original project has not been updated for the three years before the fork.
- Fixes issues with she-bangs and empty lines before the header. See PR history for more details.
- Good error reporting and improved auto-fixes.
- Complete drop-in-replacement compatibility with existing projects using eslint-plugin-header.
Multiple other projects took from where eslint-plugin-header left off. A comparison of the current project to these alternatives is available in a dedicated section.
Compatibility
The plugin supports ESLint 7 / 8 / 9 / 10. Both flat config and legacy, hierarchical config can be used.
The NPM package provides TypeScript type definitions and can be used with
TypeScript-based ESLint flat configuration without the need for @ts-ignore
statements.
Usage
The plugin and its header rule goes through evolution of its configuration in the 3.2.x release. We introduced a new single object-based configuration format that is easier to evolve in the future to add more capabilities.
The legacy configuration format inherited from eslint-plugin-header is still supported and you can learn how to use it in a dedicated document. For information on how to switch from the legacy configuration format to the new style, follow our migration guide. The current document from this point on will cover only the new configuration format.
This header rule takes a single object as configuration, after the severity
level. At the very least, the object should contain a header field describing
the expected header to match in the source files.
For TypesScript-based flat ESLint configuration, two types are provided:
HeaderRuleConfigdefines the overall rule configuration for theheaderrule and includes severity level and supports both the modern object-based configuration and the legacy array-based configuration.HeaderOptionshelper type that defines the structure of the configuration object used in the modern configuration style that is used in this document. It can be used to either simplify auto-completion since this type is not mixed with a large number of named tuple types, or it can be used when the config object is defined outside of the definition of a specific rule.
File-based Configuration
In this configuration mode, the header template is read from a file.
eslint.config.ts:
import header, {
HeaderOptions, HeaderRuleConfig
} from "@tony.ganchev/eslint-plugin-header";
import { defineConfig } from "eslint/config";
export default defineConfig([
{
files: ["**/*.js"],
plugins: {
"@tony.ganchev": header
},
rules: {
"@tony.ganchev/header": [
"error",
{
header: {
file: "config/header.js"
}
} as HeaderOptions
] as HeaderRuleConfig
}
}
]);config/header.js:
// Copyright 2015
// My companyDue to limitations in ESLint plugins, the file is read relative to the working directory that ESLint is executed in. If you run ESLint from elsewhere in your tree then the header file will not be found.
Inline Configuration
In this configuration mode, the matching rules for the header are given inline.
The header field should contain the following nested properties:
commentTypewhich is either"block"or"line"to indicate what style of comment should be used.linewhich defines the lines of the header. It can be either a single multiline string / regular expression with the full contents of the header comment or an array with comment lines or regular expressions matching each line. It can also include template replacement strings to enable ESLint's auto-fix capabilities.
Header Contents Configuration
Suppose we want our header to look like this:
/*
* Copyright (c) 2015
* My Company
*/All of the following configurations will match the header:
Single string:
import header, { HeaderOptions } from "@tony.ganchev/eslint-plugin-header"; import { defineConfig } from "eslint/config"; export default defineConfig([ { files: ["**/*.js"], plugins: { "@tony.ganchev": header }, rules: { "@tony.ganchev/header": [ "error", { header: { commentType: "block", lines: ["\n * Copyright (c) 2015\n * My Company\n "] } } as HeaderOptions ] } } ]);Note that the above would work for both Windows and POSIX systems even though the EOL in the header content was specified as
\n.Also, notice how we have an empty space before each line. This is because the plugin only strips the leading
//characters from a line comment. Similarly, for a block comment, only the opening/*and closing*/will be preserved with all new lines and whitespace preserved. Keep this in mind as this can lead to poorly configured header matching rules that never pass. In a future release error messages would be more detailed and show exactly where header validation failed.Single regular expression:
You can match the whole header with a regular expression. To do it, simply pass a
RegExpobject in place of a string.import header, { HeaderOptions } from "@tony.ganchev/eslint-plugin-header"; import { defineConfig } from "eslint/config"; export default defineConfig([ { files: ["**/*.js"], plugins: { "@tony.ganchev": header }, rules: { "@tony.ganchev/header": [ "error", { header: { commentType: "block", lines: [ /\n \* Copyright \(c\) 2015\n \* Company\n / ] } } as HeaderOptions ] } } ]);If you still use hierarchical configuration, you can define the regular expression as a string.
import header, { HeaderOptions } from "@tony.ganchev/eslint-plugin-header"; import { defineConfig } from "eslint/config"; export default defineConfig([ { files: ["**/*.js"], plugins: { "@tony.ganchev": header }, rules: { "@tony.ganchev/header": [ "error", { header: { commentType: "block", lines: [ { pattern: "\\n \\* Copyright \\(c\\) 2015" + "\\n \\* My Company\\n "} ] } } as HeaderOptions ] } } ]);Notice the double escaping of the braces. Since these pattern strings into
RegExpobjects, the backslashes need to be present in the string instead of disappear as escape characters.You can pass a
RegExpobject to thepatternfield. This is necessary if you want to add an aut-fix for the line as we will explain further in this document.import header, { HeaderOptions } from "@tony.ganchev/eslint-plugin-header"; import { defineConfig } from "eslint/config"; export default defineConfig([ { files: ["**/*.js"], plugins: { "@tony.ganchev": header }, rules: { "@tony.ganchev/header": [ "error", { header: { commentType: "block", lines: [ { pattern: /Copyright \(c\) 20\d{2}/ } ] } } as HeaderOptions ] } } ]);Array of strings:
import header, { HeaderOptions } from "@tony.ganchev/eslint-plugin-header"; import { defineConfig } from "eslint/config"; export default defineConfig([ { files: ["**/*.js"], plugins: { "@tony.ganchev": header }, rules: { "@tony.ganchev/header": [ "error", { header: { commentType: "block", lines: [ "", " * Copyright (c) 2015", " * My Company", " " ] } } as HeaderOptions ] } } ]);Array of strings and/or patterns:
import header, { HeaderOptions } from "@tony.ganchev/eslint-plugin-header"; import { defineConfig } from "eslint/config"; export default defineConfig([ { files: ["**/*.js"], plugins: { "@tony.ganchev": header }, rules: { "@tony.ganchev/header": [ "error", { header: { commentType: "block", lines: [ "", / \* Copyright \(c\) 2015/, " * My Company", " " ] } } as HeaderOptions ] } } ]);
Regular expressions allow for a number of improvements in the maintainability of the headers. Given the example above, what is clear is that new sources may have been created later than 2015 and a comment with a different year should be perfectly valid, such as:
/*
* Copyright 2020
* My company
*/Moreover, suppose your legal department expects that the year of first and last change be added except if all changes happen in the same year, we also need to support:
/*
* Copyright 2017-2022
* My company
*/We can use a regular expression to support all of these cases for your header:
import header, { HeaderOptions } from "@tony.ganchev/eslint-plugin-header";
import { defineConfig } from "eslint/config";
export default defineConfig([
{
files: ["**/*.js"],
plugins: {
"@tony.ganchev": header
},
rules: {
"@tony.ganchev/header": [
"error",
{
header: {
commentType: "block",
lines: [
"",
/ \* Copyright \(c\) (\d{4}-)?\d{4}/,
" * My Company",
" "
]
}
} as HeaderOptions
]
}
}
]);Note on auto-fixes i.e. eslint --fix: whenever strings are used to define the
header - counting in file-based configuration - the same strings would be used
to replace a header comment that did not pass validation. This is not possible
with regular expressions. For regular expression pattern-objects, a second
property template adds a replacement string.
import header, { HeaderOptions } from "@tony.ganchev/eslint-plugin-header";
import { defineConfig } from "eslint/config";
export default defineConfig([
{
files: ["**/*.js"],
plugins: {
"@tony.ganchev": header
},
rules: {
"@tony.ganchev/header": [
"error",
{
header: {
commentType: "line",
lines: [
{
pattern: / Copyright \(c\) (\d{4}-)?\d{4}/,
template: " Copyright 2025",
},
" My Company"
]
}
} as HeaderOptions
]
}
}
]);There are a number of things to consider:
- Templates need to be matched by the regular expression pattern or otherwise an auto-fixed source would again fail linting. This needs to be validated manually today as the plugin does not do it for you.
- Templates are hardcoded strings therefore it may be better to hand-fix a bad header in order not to lose the from- and to-years in the copyright notice.
Providing To-year in Auto-fix
A common request across similar plugins is to provide for {year} variable to
not change the ESLint configuration every year. While such special requests were
relevant to old JSON-based configuration, this can be handled with JavaScript in
the flat configuration format:
import header, { HeaderOptions } from "@tony.ganchev/eslint-plugin-header";
import { defineConfig } from "eslint/config";
export default defineConfig([
{
files: ["**/*.js"],
plugins: {
"@tony.ganchev": header
},
rules: {
"@tony.ganchev/header": [
"error",
{
header: {
commentType: "line",
lines: [
{
pattern: / Copyright \(c\) (\d{4}-)?\d{4}/,
template: ` Copyright ${new Date().getFullYear()}`,
},
" My Company"
]
}
} as HeaderOptions
]
}
}
]);Trailing Empty Lines Configuration
The third argument of the rule configuration which defaults to 1 specifies the number of newlines that are enforced after the header.
Zero newlines:
import header, { HeaderOptions } from "@tony.ganchev/eslint-plugin-header";
import { defineConfig } from "eslint/config";
export default defineConfig([
{
files: ["**/*.js"],
plugins: {
"@tony.ganchev": header
},
rules: {
"@tony.ganchev/header": [
"error",
{
header: {
commentType: "block",
lines: [
" Copyright now",
"My Company "
],
},
trailingEmptyLines: {
minimum: 0
}
} as HeaderOptions
]
}
}
]);/* Copyright now
My Company */ console.log(1)One newline (default):
import header, { HeaderOptions } from "@tony.ganchev/eslint-plugin-header";
import { defineConfig } from "eslint/config";
export default defineConfig([
{
files: ["**/*.js"],
plugins: {
"@tony.ganchev": header
},
rules: {
"@tony.ganchev/header": [
"error",
{
header: {
commentType: "block",
lines: [
" Copyright now",
"My Company "
],
},
trailingEmptyLines: {
minimum: 1
}
} as HeaderOptions
]
}
}
]);/* Copyright now
My Company */
console.log(1)Two newlines:
import header, { HeaderOptions } from "@tony.ganchev/eslint-plugin-header";
import { defineConfig } from "eslint/config";
export default defineConfig([
{
files: ["**/*.js"],
plugins: {
"@tony.ganchev": header
},
rules: {
"@tony.ganchev/header": [
"error",
{
header: {
commentType: "block",
lines: [
" Copyright now",
"My Company "
]
},
trailingEmptyLines: {
minimum: 2
}
} as HeaderOptions
]
}
}
]);/* Copyright now
My Company */
console.log(1)Line Endings
The rule works with both Unix/POSIX and Windows line endings. For ESLint
--fix, the rule will use the line ending format of the current operating
system (via Node's os package). This setting can be overwritten as follows:
import header, { HeaderOptions } from "@tony.ganchev/eslint-plugin-header";
import { defineConfig } from "eslint/config";
export default defineConfig([
{
files: ["**/*.js"],
plugins: {
"@tony.ganchev": header
},
rules: {
"@tony.ganchev/header": [
"error",
{
header: {
commentType: "block",
lines: [
"Copyright 2018",
"My Company"
]
},
lineEndings: "windows"
} as HeaderOptions
]
}
}
]);Possible values are "unix" for \n and "windows" for \r\n line endings.
The default value is "os" which means assume the system-specific line endings.
Examples
The following examples are all valid.
import header, { HeaderOptions } from "@tony.ganchev/eslint-plugin-header";
import { defineConfig } from "eslint/config";
export default defineConfig([
{
files: ["**/*.js"],
plugins: {
"@tony.ganchev": header
},
rules: {
"@tony.ganchev/header": [
"error",
{
header: {
commentType: "block",
lines: ["Copyright 2015, My Company"]
}
} as HeaderOptions
]
}
}
]);/*Copyright 2015, My Company*/
console.log(1);import header, { HeaderOptions } from "@tony.ganchev/eslint-plugin-header";
import { defineConfig } from "eslint/config";
export default defineConfig([
{
files: ["**/*.js"],
plugins: {
"@tony.ganchev": header
},
rules: {
"@tony.ganchev/header": [
"error",
{
header: {
commentType: "line",
lines: [
"Copyright 2015",
"My Company"
]
}
} as HeaderOptions
]
}
}
]);//Copyright 2015
//My Company
console.log(1)import header, { HeaderOptions } from "@tony.ganchev/eslint-plugin-header";
import { defineConfig } from "eslint/config";
export default defineConfig([
{
files: ["**/*.js"],
plugins: {
"@tony.ganchev": header
},
rules: {
"@tony.ganchev/header": [
"error",
{
header: {
commentType: "line",
lines: [
/^Copyright \d{4}$/,
/^My Company$/
]
}
} as HeaderOptions
]
}
}
]);//Copyright 2017
//My Company
console.log(1)With more decoration:
import header, { HeaderOptions } from "@tony.ganchev/eslint-plugin-header";
import { defineConfig } from "eslint/config";
export default defineConfig([
{
files: ["**/*.js"],
plugins: {
"@tony.ganchev": header
},
rules: {
"@tony.ganchev/header": [
"error",
{
header: {
commentType: "block",
lines: [
"************************",
" * Copyright 2015",
" * My Company",
" ************************"
]
}
} as HeaderOptions
]
}
}
]);/*************************
* Copyright 2015
* My Company
*************************/
console.log(1);Comparison to Alternatives
A number of projects have been aiming to solve problems similar to @tony.ganchev/eslint-plugin-header - mainly with respect to providing ESLint 9 support. The section below tries to outline why developers may choose either. The evaluation is based on the versions of each project as of the time of publishing the current version of @tony.ganchev/eslint-plugin-header.
Further iterations of this document would add migration information.
Compared to eslint-plugin-headers
@tony.ganchev/eslint-plugin-header is a drop-in replacement for eslint-plugin-header and all plugins that already use the latter can migrate to the fork right away. At the same time, it provides improved user experience and windows support.
eslint-plugin-headers is not a drop-in replacement. It offers additional
features. Some of them, such as support for Vue templates do not have an
analogue in the current version of @tony.ganchev/eslint-plugin-header while
others such as {year} variable placeholders are redundant in the world of
ESLint 9's flat, JavaScript-based configuration as already pointed out in this
document.
The configuration format philosophy of the two plugin differs. @tony.ganchev/eslint-plugin-header supports both the legacy model inherited from eslint-plugin-header and a new object-based configuration that is easy to adopt and offers both a lot of power to the user as to what the headers should look like, and keeps the configuration compact - just a few lines defining the content inside the comment. At the same time, the configuration is structured in a way that can evolve without breaking compatibility, which is critical for a tool that is not differentiating for the critical delivery of teams.
eslint-plugin-headers also offers an object-based format, but the content is
flat and may need breaking changes to be kept concise as new features come
about. Further, it makes assumption that then need to be corrected such as a
block comment starting with /** instead of /* by default. The correction
needs to happen not by adjusting the header template but through a separate
confusing configuration properties.
Overall, the configuration tends to be noisier nad harder to read than that of
@tony.ganchev/eslint-plugin-header.
eslint-plugin-headers's error reporting is rudimentary - either the header passes or it fails and with complex templates you get no idea what the issue is. Granted this is the case with eslint-plugin-header but we spent many hours improving this and the side by side comparison is telling.
eslint-plugin-headers does not offer TypeScript bindings for its configuration format making it slower to author configuration. @tony.ganchev/eslint-plugin-header as often as possible reports which line is problematic and starting from which character.
eslint-plugin-headers supports some level of partial auto-fixes such as replacing company names but not years. Or keeping JSDoc variables after the copyright notice within the same comment. Some cases can be supported by @tony.ganchev/eslint-plugin-header but in general our design has shied away from touching existing comments to provide "smart" fixes. This is the current state of the feature set yet the team is looking for the right model to bridge the functionality gaps.
We have prepared a detailed migration guide for anyone eager to migrate to @tony.ganchev/eslint-plugin-header.
Health Scans
@tony.ganchev/eslint-plugin-header - snyk.io, socket.dev
eslint-plugin-headers - snyk.io, socket.dev
At the time of the publishing of the current version of @tony.ganchev/eslint-plugin-header, the latter has a slight edge in both scans against eslint-plugin-headers.
Compared to eslint-plugin-license-header
eslint-plugin-license-header per its limited documentation does not have a lot of features including not matching arbitrary from-years in the copyright notice. This on one hand leads to it having a nice, dead-simple configuration, but means no complex multi-year project would be happy with it. Surprisingly, given the limited feature set, the plugin has more peer dependencies than the competition.
We have prepared a detailed migration guide for anyone eager to migrate to @tony.ganchev/eslint-plugin-header.
Versioning
The project follows standard NPM semantic versioning policy.
The following guidelines apply:
- major versions - new functionality that breaks compatibility.
- minor versions - new features that do not break compatibility. For the most part we would aim to continue releasing new versions in the 3.x product line and have opt-in flags for changes in behavior of existing features.
- revisions - bugfixes and minor non-feature improvements that do not break compatibility. Note that bug-fixes are allowed to break compatibility with previous version if the older version regressed previous expected behavior.
Two concepts are important when going over the above guidelines and we will go over them in the next sections.
What is a Feature?
We keep the distinction between a feature and a non-feature improvement / bug fix as simple as possible:
- If configuration changes, it's a feature.
- If it doesn't, then you have two cases:
- If it changes behavior back to what is expected, it is a bug.
- If it changes the expected behavior, it is an improvement.
What is Backward-compatibility?
Backward compatibility in the context of this plugin relates to how the plugin consistently passes or fails one and the same code in between upgrades to newer backward-compatible versions. This guarantees that plugin updates can be done without breaking CI/CD pipeline linting.
Backward-compatibility does not cover the following functional aspects:
- Rule violation messages are not kept stable between backward-compatible versions. This allows us to improve error reporting in addition to bug fixes.
- Auto-fix behavior is not stable between backward-compatible versions. As auto- fixes are not part of CI/CD processes results of them may vary.
- Bugs to released functionality. Bugs are considered regression to expected functionality regardless of whether they went into a release or not. We fix bugs without maintaining bug-compatibility.
License
MIT
