Updated: Integrating ESLint

Posted . Visible to the public. Auto-destruct in 60 days

Updated the instructions for ESLint to work with version 5.x.

Changes

  • ## Introduction
  • To ensure a consistent code style for JavaScript code, we use [ESLint](https://eslint.org/). The workflow is similar to [integrating rubocop](https://makandracards.com/makandra/400093-integrating-or-upgrading-makandra-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`:
  • +
  • ```json
  • -"devDependencies": {
  • - "eslint": "^8.7.0",
  • - "eslint-config-standard": "^16.0.3",
  • -"eslint-plugin-import": "^2.25.4",
  • -"eslint-plugin-node": "^11.1.0",
  • -"eslint-plugin-promise": "^6.0.0"
  • - }
  • -```
  • -This should work for node 12+.
  • -Then run `yarn install`.
  • -
  • -Add an `.eslintrc.js` file, for example:
  • -```js
  • -module.exports = {
  • - env: {
  • -browser: true,
  • -es2021: true,
  • +"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:
  • +
  • +```js
  • +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'],
  • + },
  • },
  • -parserOptions: {
  • -ecmaVersion: 12,
  • -sourceType: 'module',
  • -
  • +{
  • + files: ['app/webpack/specs/**/*.{js,mjs,cjs}'],
  • +rules: {
  • + 'no-undef': 'off',
  • + },
  • +
  • },
  • - ignorePatterns: [
  • -'/coverage/',
  • -'/node_modules/',
  • -'/tmp/',
  • -...
  • - ],
  • -globals: {
  • -up: 'readonly',
  • -
  • +{
  • +ignores: [
  • + 'app/assets/builds/',
  • + 'coverage/',
  • + 'node_modules/',
  • + 'public/assets/',
  • + 'vendor/ruby',
  • + 'vendor/npm',
  • + 'vendor/asset-libs/',
  • + 'tmp/',
  • + ]
  • +
  • },
  • - extends: [
  • - 'standard',
  • -],
  • -
  • +])
  • +```
  • +
  • +
  • +Notes:
  • +
  • +- Stylistic rulesfor ESLint are imported from the dedicated package [@stylistic/eslint-plugin](https://eslint.style/rules).
  • +- In previous setups we used [eslint-config-standard](https://github.com/standard/eslint-config-standard) package. This package is not maintained anymore, therefore we extracted the [most important rules](https://github.com/standard/eslint-config-standard/blob/60d408b04723fb7cd743f6be7c5c01bb3f729139/src/index.ts) directly into the `eslint.config.mjs`.
  • +- The [globals](https://github.com/sindresorhus/globals) 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**:
  • +
  • +```js
  • +{
  • + ignores: [
  • + 'app/assets/builds/',
  • + 'coverage/',
  • + 'node_modules/',
  • + 'public/assets/',
  • + 'vendor/ruby',
  • + 'vendor/npm',
  • + 'vendor/asset-libs/',
  • + 'tmp/',
  • + ]
  • +
  • }
  • ```
  • -This config is based on the [standardjs.com ruleset](https://github.com/standard/eslint-config-standard) and has been adjusted slightly.
  • -> [NOTE]
  • -> Things you define on `window` to make them accessible from anywhere like `up` or `_` might lead to `no-undef` errors. Add them to `globals` so they won't trigger a warning.
  • ----
  • -Example `ignorePatterns` for a project with **webpack**:
  • +**webpack & precompiled_assets**:
  • +
  • +
  • ```js
  • -ignorePatterns: [
  • - '/coverage/',
  • - '/node_modules/',
  • - '/public/packs/',
  • - '/public/packs-test/',
  • - '/tmp/',
  • -],
  • +{
  • +ignores: [
  • +'/coverage/',
  • +'/node_modules/',
  • +'/public/packs/',
  • +'/public/packs-test/',
  • +'/tmp/',
  • + ]
  • +}
  • ```
  • ----
  • -Example `ignorePatterns` for a project with **esbuild**:
  • +**esbuild & precompiled_assets**:
  • +
  • +
  • ```js
  • -ignorePatterns: [
  • - '/app/assets/builds/',
  • - '/coverage/',
  • - '/node_modules/',
  • - '/public/assets/',
  • - '/public/assets-test/',
  • - '/tmp/',
  • -],
  • +{
  • +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`:
  • ```ruby
  • 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:
  • +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:
  • +
  • +
  • +
  • ```js
  • rules: {
  • - 'comma-dangle': 'off',
  • - 'padded-blocks': 'off',
  • - 'space-before-function-paren': 'off',
  • - 'prefer-const': 'off',
  • + '@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.
  • 1. Remove one of the disabled rules
  • 2. Run your spec or `yarn run eslint .`
  • 3. Fix all offenses, either manually or with the auto-correct `yarn run eslint . --fix`
  • 4. Check if tests are still green and everything works as expected
  • 5. 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.](https://eslint.org/docs/user-guide/configuring/rules#disabling-rules).
  • +
  • Or you could use your own configuration for specific rules, like:
  • +
  • ```js
  • rules: {
  • - 'padded-blocks': 'off',
  • - 'space-before-function-paren': [
  • -'error', {
  • -anonymous: 'never',
  • -named: 'never',
  • -asyncArrow: 'always',
  • - },
  • -],
  • + '@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 `.eslintrc.js`:
  • +
  • +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`:
  • +
  • +
  • ```js
  • overrides: [
  • {
  • files: ['app/webpack/backend/index.js', 'app/webpack/frontend/index.js'],
  • rules: {
  • - 'import/first': 'off',
  • + 'import/first': ['error', 'never'],
  • },
  • },
  • {
  • files: ['app/webpack/specs/**/*.js'],
  • rules: {
  • - 'no-undef': 'off',
  • + '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
  • ```
License
Source code in this card is licensed under the MIT License.
Posted by Emanuel to makandra dev (2025-08-25 14:48)