My custom configurations for ESLint
Let's start with... I LOVE ESLint and I use it on every project.
Together with ESLint, I use the Airbnb's plugin, eslint-plugin-import
and eslint-plugin-node
.
Everything is great, but...I'm always overwriting the same rules, and every time one the packages mentioned above is released, I have to go and update every single project.
So, I decided to make my own plugin, with the configurations I use.
Just include the plugin and extend
from the configuration you want to use:
{
"root": true,
"plugins": ["homer0"],
"extends": ["plugin:homer0/node"]
}
There are a few different configurations that can be used, dependending on the project and/or the scope:
A base configuration that uses @typescript-eslint/parser
and @typescript-eslint/eslint-plugin
. It extends plugin:@typescript-eslint/recommended
, and it's prepared to work with a .eslintrc.js
config file.
Name: plugin:homer0/typescript
This is a basic configuration for Node that extends from plugin:node/recommended
.
Name: plugin:homer0/node
It's a merge of the TypeScript
and Node
configurations, with a few extra overrides specifics to Node.
Name: plugin:homer0/node-typescript
This extends the base rules and disables a few ones from the import
plugin, as most of the browser code I wrote goes through transpilation, so there's no need to validate production/development dependencies.
Name: plugin:homer0/browser
Jest is my favorite tool for testing, so all my projects (with test) use it. This configuration not only changes the environment to jest
but it also disables a few rules from the import
plugin.
Name: plugin:homer0/jest
Instead of using the base configuration like the Jest configuration, this one uses Node with TypeScript
as a base, since it requires a few changes in the root level.
Name: plugin:homer0/jest-node-typescript
I'm a big fan of documenting EVERY class, method/function, property, etc; so this configuration helps me validate that all my comments are valid JSDoc.
Name: plugin:homer0/jsdoc
A configuration for React projects, based on the react
and react-hooks
plugins. It extends from the browser
configuration as a base, then adds plugin:react/recommended
and plugin:react-hooks/recommended
, and a few extra rules based on my experience.
Specific for Svelte (v3 (for now)) projects, uses svelte
plugin, the browser
configuration as a base, and extends plugin:svelte/recommended
, with a few extra rules based on my experience.
A base configuration to use with Next.js projects with TypeScript. This is an internal configuration because it's meant to be used with the Next.js preset.
Name: plugin:homer0/next-base
Most of the configuartions have a -with-prettier
version that includes the prettier
plugin, turning off the rules that conflict with it.
Configuration | Prettier version |
---|---|
browser |
browser-with-prettier |
jest |
jest-with-prettier |
node |
node-with-prettier |
node-typescript |
node-typescript-with-prettier |
jest-node-typescript |
jest-node-typescript-with-prettier |
react |
react-with-prettier |
svelte |
svelte-with-prettier |
Presets are a way to generate ESLint configurations that use the plugin by passing a few options.
Allows you to generate a configuration for a Next.js project with TypeScript:
// .eslintrc.js in the root of the project
const preset = require('@homer0/eslint-plugin/presets/next');
module.exports = preset({
// The directory where the `tsconfig` is located.
rootDir: __dirname,
// A list of extra configurations to extend. They'll be added at the end of the list
// (after the one from next and the one from this plugin).
configs: [],
// The name of the `tsconfig`.
tsConfig: 'tsconfig.json',
// The source type for the `tsconfig`.
sourceType: 'module',
// The name of the Next.js config to extend.
nextConfig: 'next/core-web-vitals',
// The filter for JS files, like `next.config.js` that should be linted with `espree`.
jsFilter: ['*.js'],
// Whether or not the config is root.
root: true,
// Whether or not to use the prettier variant of this plugin base config.
prettier: true,
});
Generates a base configuration for TypeScript projects:
// .eslintrc.js in the root of the project
const preset = require('@homer0/eslint-plugin/presets/typescript');
module.exports = preset({
// The directory where the `tsconfig` is located.
rootDir: __dirname,
// A list of extra configurations to extend. They'll be added at the end of the list
// (after the one from next and the one from this plugin).
configs: [],
// The name of the `tsconfig`.
tsConfig: 'tsconfig.json',
// The source type for the `tsconfig`.
sourceType: 'module',
// Whether or not the config is root.
root: true,
});
Here's a list of the rules I modified and the reason I did it:
off
->warn
.
I want the linter to warn me if I'm writing big function/methods.
error
->off
I write almost everything using OOP, and some times I want to create a method to extract some logic and I don't want to be restricted by this rule.
off
->['error', { ignore: [0, 1, -1, 60, 1000]}]
I like self explanatory code, and magic numbers get in the way of that. The reason I left those 4 "ignore cases" is because...
-
0
and1
: To work with arrays. Examples: To check if an array is empty or not, and to check if an array has a single element or more. -
-1
: To work withindexOf
. -
60
and1000
: To work with timestamps.
Now, this rule is disabled on the jest
config, as it's not uncommon to use random numbers for "fake data" or to check calls with .toHaveBeenCalledTimes()
.
['error', {...}]
->error
I don't believe this is a good practice (as explained by the rule) and I prefer having to add an exception in the cases I need to use instead of having a lot of exceptions by default.
off
->error
No idea why do they disabled this rule, it's a great help to avoid infinite loops.
off
->error
I haven't use call
or apply
for years now, as I believe that with ES6 you can cover 99% of the cases in which they were useful. Now, I enabled this rule because if I can't prevent their use, I want to make it as restricted as possible.
error
->off
Not all the time, but I use regular expressions quite a lot, and I have found cases in which this rule is unnecessarily triggered.
off
->['error', '^(err|error|\\w+Error)$']
I consider error handling a "required good practice" on every project.
error
->off
You can't tell ESLint which part of the project is tooling for development and which part is for production, and I don't want to have to create different directories with other .eslintrc
for this... at least for now.
off
->error
It helps to organize the code a little bit.
off
->error
No, I'm not crazy and is not that I don't use process.env
. The reason I enabled this rule is because I think environment variables should be consumed on a single place, instead of reading them all over the project.
Instead of having the rule disable for all files, I prefer writing the "disable comment" on a single file.
error
->off
I don't get how this rule works...it gets triggered for some packages that comply with all the requirements a "published package" must have.
error
->off
This is because is not unusual for me to write .sh
or extension-less files with the Node shebang...which triggers the rule.
off
->['error', 'consistent']
Consistency.
['error', 2, {...}]
->['error', 2, {..., MemberExpression: 0 }]
I don't like to indent when there's really no need for it:
// Bad
something()
.then(() => ...);
// Good
something()
.then(() => ...);
['error', {...}]
->off
Most of the times, I write JSDoc block comments between methods, and I consider that is enough "separation".
off
->['error', { max: 1 }]
Readability
error
->off
Yes, the reason for having this rule enabled is valid, but I don't believe that's a very common case.
['error', { allow: [], allowAfterThis: false, allowAfterSuper: false, enforceInMethodNames: true, }]
->
['error', { allowAfterThis: true, allowAfterSuper: true, enforceInMethodNames: false, }]
Since I don't use TypeScript, I follow the convention of prefixing protected/private property/methods with underscore.
['error', 'before', {...}]
->['error', 'after', {...}]
I know that having the operators at the beginning of the line may better for readability for some people, but in my case, I find it easier to read if they are at the end. Yes, I may be that I've been using it like this for years.
These are not overwrites, the plugin I use doesn't have a default preset, so I'm just going to explain why did enabled each rule and what values they have.
'error'
Since most of the things I write are classes, I use the @access
tag a lot, and this rule is more about protecting you from typos and invalid values than a restriction.
'error'
It's not uncommon for IDEs/editors to "auto align" JSDoc blocks when you copy them and mess the alignment of the asterisks.
['error', { allowExtraTrailingParamDocs: true }]
This is a big helper for when you update a function/method signature, you won't forget to update the JSDoc block.
The allowExtraTrailingParamDocs
option is enabled so you won't get errors when documenting parameters for abstract methods.
'error'
It helps you prevent duplicated properties and avoid incomplete declarations.
'error'
Helps avoid invalid syntax.
['error', { definedTags: ['parent'], }]
This rule prevents the use of invalid JSDoc tags; the definedTags
is used to add the following exceptions:
-
parent
: I use it as an alias ofmemberof
to be able usemodule:
and avoid issues with the plugin. I transform it tomemberof
using thejsdoc-ts-utils
when generating the JSDoc site.
'error'
Helps with consistency as it forces you to always use the same casing for native types.
'error'
The @implements
tag should only be used on classes.
'error'
Consistency.
['error', { checkConstructors: false }]
Everything should have a description.
The checkConstructors
option is to avoid comments like "class constructor".
'error'
All descriptions should formatted as sentences.
['error', 'never']
I never used, and I don't like, hyphens as separators on JSDoc blocks.
['error', { require: { allowAfterThis: true, ArrowFunctionExpression: true, ClassDeclaration: true, ClassExpression: true, FunctionDeclaration: true, MethodDefinition: true, }, exemptEmptyConstructors: true, }]
EVERYTHING should be documented!
The require
option is so the rule will be applied to all available contexts, and exemptEmptyConstructors
is so it will skip constructors with no pameters.
'error'
Yes, every parameter should be documented.
'error'
Yes, every parameter should have a human-readable description.
'error'
I'm still not sure why this rule even exists, it should be part of jsdoc/require-param
.
'error'
Of course every parameter should have its type documented.
'error'
The type Object
is like the any
on TypeScript, it doesn't say anything; if you are going to use it, you should documented the properties too.
'error'
Yes, every property should have a human-readable description.
'error'
Like jsdoc/require-param-name
, this shouldn't exist.
'error'
Yes, the types should be documented.
'error'
Yes, the @returns
tag is as importan as @param
.
'error'
I like this rule because is not that it validates the comments based on the code, but it's the other way around: if you use @returns
, the function/method should have a return
(it's only logical).
'error'
Yes, if there's a @returns
, it should have a type
.
'error'
This helps a lot when writing error handling code.
'error'
Prevents invalid definitions.
['error', { ignores: ['modules'] }]
Because TypeScript uses ES syntax.
off
Like the version for require
, not sure how the rule works.
off
It's an overkill.
off
Because TypeScript uses ES syntax, and the only case in which I would consider to enforce this rule would be CommonJS, where it's actual a destructuring assignment.
off
I enforce this rule when using JSDoc, as the blocks work as separators between the methods. When working with TypeScript, I don't use that much JSDoc, so I want to be able to leave an empty line between the methods.
off
Empty function are a common resource for optional paramters/properties.
off
There are some cases, mostly on frontend, where you know the state is non-null, and I don't want to have to use optional chaining for EVERYTHING.
['error', { require: { 'ts-expect-error': 'allow-with-description', 'ts-ignore': 'allow-with-description', 'ts-nocheck': 'allow-with-description', 'ts-check': 'allow-with-description', }, exemptEmptyConstructors: true, }]
I'm allowed to ignore TypeScript... with a valid excuse.
Task | Description |
---|---|
lint |
Lint the modified files. |
lint:all |
Lint the project code. |
todo |
List all the pending to-do's. |
I use husky
to automatically install the repository hooks so the code will be tested and linted before any commit and the dependencies updated after every merge.
The configuration is on the husky
property of the package.json
and the hooks' files are on ./utils/hooks
.
I use conventional commits with commitizen
in order to support semantic releases. The one that sets it up is actually husky, that installs a script that runs commitizen on the git commit
command.
The hook for this is on ./utils/hooks/prepare-commit-msg
and the configuration for comitizen is on the config.commitizen
property of the package.json
.