Introduction
To ensure a consistent code style for JavaScript code, we use ESLint Show archive.org snapshot . The workflow is similar to integrating rubocop for Ruby code.
1. Adding the gem to an existing code base
You can add the following lines to your package.json
under devDependencies
:
"devDependencies": {
"@eslint/js": "x",
"@stylistic/eslint-plugin": "x",
"eslint": "x",
"eslint-plugin-import": "x",
"globals": "x",
}
Then run yarn install
and add a eslint.config.mjs
file:
import js from '@eslint/js'
import globals from 'globals'
import stylistic from '@stylistic/eslint-plugin'
import { defineConfig } from 'eslint/config'
export default defineConfig([
{
files: ['**/*.{js,mjs,cjs}'],
plugins: {
js,
'@stylistic': stylistic,
},
extends: [
'js/recommended',
],
languageOptions: {
globals: {
...globals.browser,
up: 'readonly',
process: 'readonly',
require: 'readonly',
},
},
rules: {
'no-unused-vars': ['error', {
'varsIgnorePattern': '^_',
'argsIgnorePattern': '^_',
'caughtErrorsIgnorePattern': '^_',
}],
'@stylistic/arrow-spacing': ['error', { before: true, after: true }],
'@stylistic/block-spacing': ['error', 'always'],
'@stylistic/brace-style': ['error', '1tbs', { allowSingleLine: true }],
'@stylistic/comma-dangle': ['error', {
'arrays': 'always-multiline',
'objects': 'always-multiline',
'imports': 'always-multiline',
'exports': 'always-multiline',
'functions': 'always-multiline',
'importAttributes': 'always-multiline',
'dynamicImports': 'always-multiline',
'enums': 'always-multiline',
'generics': 'always-multiline',
'tuples': 'always-multiline'
}],
'@stylistic/comma-spacing': ['error', { before: false, after: true }],
'@stylistic/comma-style': ['error', 'last'],
'@stylistic/computed-property-spacing': ['error', 'never', { enforceForClassMembers: true }],
'@stylistic/dot-location': ['error', 'property'],
'@stylistic/eol-last': 'error',
'@stylistic/generator-star-spacing': ['error', { before: true, after: true }],
'@stylistic/indent': ['error', 2, {
SwitchCase: 1,
VariableDeclarator: 1,
outerIIFEBody: 1,
MemberExpression: 1,
FunctionDeclaration: { parameters: 1, body: 1 },
FunctionExpression: { parameters: 1, body: 1 },
CallExpression: { arguments: 1 },
ArrayExpression: 1,
ObjectExpression: 1,
ImportDeclaration: 1,
flatTernaryExpressions: false,
ignoreComments: false,
ignoredNodes: [
'TemplateLiteral *',
'JSXElement',
'JSXElement > *',
'JSXAttribute',
'JSXIdentifier',
'JSXNamespacedName',
'JSXMemberExpression',
'JSXSpreadAttribute',
'JSXExpressionContainer',
'JSXOpeningElement',
'JSXClosingElement',
'JSXFragment',
'JSXOpeningFragment',
'JSXClosingFragment',
'JSXText',
'JSXEmptyExpression',
'JSXSpreadChild'
],
offsetTernaryExpressions: true
}],
'@stylistic/key-spacing': ['error', { beforeColon: false, afterColon: true }],
'@stylistic/keyword-spacing': ['error', { before: true, after: true }],
'@stylistic/lines-between-class-members': ['error', 'always', { exceptAfterSingleLine: true }],
'@stylistic/multiline-ternary': ['error', 'always-multiline'],
'@stylistic/new-parens': 'error',
'@stylistic/no-extra-parens': ['error', 'functions'],
'@stylistic/no-floating-decimal': 'error',
'@stylistic/no-mixed-operators': ['error', {
groups: [
['==', '!=', '===', '!==', '>', '>=', '<', '<='],
['&&', '||'],
['in', 'instanceof']
],
allowSamePrecedence: true
}],
'@stylistic/no-mixed-spaces-and-tabs': 'error',
'@stylistic/no-multi-spaces': 'error',
'@stylistic/no-multiple-empty-lines': ['error', { max: 1, maxBOF: 0, maxEOF: 0 }],
'@stylistic/no-tabs': 'error',
'@stylistic/no-trailing-spaces': 'error',
'@stylistic/no-whitespace-before-property': 'error',
'@stylistic/object-curly-newline': ['error', { multiline: true, consistent: true }],
'@stylistic/object-curly-spacing': ['error', 'always'],
'@stylistic/operator-linebreak': ['error', 'after', { overrides: { '?': 'before', ':': 'before', '|>': 'before' } }],
'@stylistic/quote-props': ['error', 'as-needed'],
'@stylistic/quotes': ['error', 'single', { avoidEscape: true, allowTemplateLiterals: 'never' }],
'@stylistic/rest-spread-spacing': ['error', 'never'],
'@stylistic/semi': ['error', 'never'],
'@stylistic/semi-spacing': ['error', { before: false, after: true }],
'@stylistic/space-before-blocks': ['error', 'always'],
'@stylistic/space-before-function-paren': ['error', {
'anonymous': 'never',
'named': 'never',
'asyncArrow': 'always'
}],
'@stylistic/space-in-parens': ['error', 'never'],
'@stylistic/space-infix-ops': 'error',
'@stylistic/space-unary-ops': ['error', { words: true, nonwords: false }],
'@stylistic/spaced-comment': ['error', 'always', {
line: { markers: ['*package', '!', '/', ',', '='] },
block: { balanced: true, markers: ['*package', '!', ',', ':', '::', 'flow-include'], exceptions: ['*'] }
}],
'@stylistic/template-curly-spacing': ['error', 'never'],
'@stylistic/template-tag-spacing': ['error', 'never'],
'@stylistic/wrap-iife': ['error', 'any', { functionPrototypeMethods: true }],
'@stylistic/yield-star-spacing': ['error', 'both'],
},
},
{
files: ['app/webpack/specs/**/*.{js,mjs,cjs}'],
rules: {
'no-undef': 'off',
},
},
{
ignores: [
'app/assets/builds/',
'coverage/',
'node_modules/',
'public/assets/',
'vendor/ruby',
'vendor/npm',
'vendor/asset-libs/',
'tmp/',
]
},
])
Notes:
- Stylistic rules for ESLint are imported from the dedicated package @stylistic/eslint-plugin Show archive.org snapshot .
- In previous setups we used
eslint-config-standard
Show archive.org snapshot
package. This package is not maintained anymore, therefore we extracted the
most important rules
Show archive.org snapshot
directly into the
eslint.config.mjs
. - The
globals
Show archive.org snapshot
package gives you a predefined list of global variables in a browser. Other global variables like
require
(from webpack) need to be added manually. - The
ignores
settings differs on your project setup. See the templates below for each common setups.
webpack & propshaft:
{
ignores: [
'app/assets/builds/',
'coverage/',
'node_modules/',
'public/assets/',
'vendor/ruby',
'vendor/npm',
'vendor/asset-libs/',
'tmp/',
]
}
webpack & precompiled_assets:
{
ignores: [
'/coverage/',
'/node_modules/',
'/public/packs/',
'/public/packs-test/',
'/tmp/',
]
}
esbuild & precompiled_assets:
{
ignores: [
'/app/assets/builds/',
'/coverage/',
'/node_modules/',
'/public/assets/',
'/public/assets-test/',
'/tmp/',
]
}
2. Report all existing offenses and exclude them initially
Add an RSpec test, which runs ESLint (also during a full RSpec test run), e.g. in spec/eslint_spec.rb
:
require 'open3'
RSpec.describe 'eslint' do
it 'is not offended' do
stdout, stderr, status = Open3.capture3('yarn', 'run', 'eslint', '.')
failure_message = [stderr, stdout].reject(&:empty?).join("\n\n")
expect(status.success?).to eq(true), failure_message
end
end
Run the test or simply run yarn run eslint .
from your console to check all files. Now you should get a list of all existing offenses, for which you should disable the rules in your .eslint.rc.js
at first:
rules: {
'@stylistic/comma-dangle': ['error', 'never'],
'@stylistic/space-before-function-paren': ['error', 'never'],
...
},
This is only a temporary measure. We fix each offense in the next step.
3. Fixing each offense per commit
Now you should fix each offense in an own commit.
- Remove one of the disabled rules
- Run your spec or
yarn run eslint .
- Fix all offenses, either manually or with the auto-correct
yarn run eslint . --fix
- Check if tests are still green and everything works as expected
- Commit the changes with a message like
[#180343876] Fix ESLint offense for rule "comma-dangle"
To run a single file, use yarn run eslint path/to/some_file.js
.
3.1 Disable and configure specific rules
You can use comments to disable specific rules in your JavaScript code. Show archive.org snapshot .
Or you could use your own configuration for specific rules, like:
rules: {
'@stylistic/comma-dangle': ['error', 'never'],
'@stylistic/space-before-function-paren': ['error', {
'anonymous': 'never',
'named': 'never',
'asyncArrow': 'always'
}],
},
3.2 Disable specific rules for a lot of files
If you have a lot of files for which you want to disable a specific rule, e.g. Jasmine specs, you could use overrides
in your eslint.config.mjs
:
overrides: [
{
files: ['app/webpack/backend/index.js', 'app/webpack/frontend/index.js'],
rules: {
'import/first': ['error', 'never'],
},
},
{
files: ['app/webpack/specs/**/*.js'],
rules: {
'no-undef': ['error', 'never'],
},
},
],
With that you don't need to add a disabling comment to each file.
4. Finish the integration
In the end you have a long commit list, first for integrating ESLint and then for fixing each offense, e.g.:
...
[#180343876] Fix ESLint offense for rule "camelcase"
[#180343876] Fix ESLint offense for rule "space-before-blocks"
[#180343876] Fix ESLint offense for rule "no-useless-return"
[#180343876] Add ESLint without any offenses