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 ignoressettings 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