Skip to main content
Version: v1

ESLint - JavaScript linter

The ESLint website describes the tool as follows:

ESLint is a tool for identifying and reporting on patterns found in ECMAScript/JavaScript code, with the goal of making code more consistent and avoiding bugs.

ESLint catches JavaScript mistakes before they reach a browser: a broken import, an unused variable, or a console.log() call left in shipped code. Vortex comes with a pre-configured ESLint ruleset for Drupal projects, along with Prettier integration, so formatting problems show up in the same check as any other mistake.

This page is for developers who write JavaScript in custom modules or the custom theme. It shows how to run ESLint and fix what it reports, what the shipped configuration enforces, and how to ignore a rule. For problems with ESLint itself, see the ESLint repository.

In Vortex, the root lint-js script lints custom modules only (web/modules/custom). The custom theme carries its own eslint.config.mjs and package.json in its directory, so it can move to a separate repository with its tooling intact.

Usage​

Check for violations​

To check the code, run 1 of the following commands:

# Lint all front-end code: Twig, JavaScript and CSS.
ahoy lint-fe
# Lint only JavaScript in custom modules.
ahoy cli "npm run lint-js"

Confirm that the check passed: ESLint lists no problems, and echo $?, run straight after the command, prints 0.

Fix violations​

ESLint's --fix flag repairs many violations automatically, and the Prettier integration reformats the code in the same pass. To apply both, run 1 of the following commands:

# Fix all front-end lint issues.
ahoy lint-fe-fix
# Fix only JavaScript issues in custom modules.
ahoy cli "npm run lint-fix-js"

Configuration​

See the ESLint configuration reference.

All global configuration lives in the eslint.config.mjs file.

By default, ESLint checks against the following presets:

  • @eslint/js recommended - the rules ESLint itself ships
  • eslint-plugin-import-x recommended - import correctness: modules that resolve, names the module exports, and no duplicate imports
  • eslint-plugin-jsdoc recommended - documentation block correctness
  • eslint-plugin-prettier recommended - Prettier integration for code formatting
  • eslint-plugin-yml recommended - linting for the YAML files in custom modules
  • eslint-plugin-no-jquery - the full rule set, discouraging jQuery where the browser has an equivalent

The configuration follows the one Drupal core adopts for its own JavaScript, so the 2 configurations stay comparable as core evolves.

The import rules come from eslint-plugin-import-x, a fork of the eslint-plugin-import plugin that Drupal core uses. Their names start with import-x/, for example import-x/no-mutable-exports.

On top of the presets, the configuration sets the following rules:

RuleLevelEffect
prettier/prettierErrorReports every difference from the Prettier formatting.
no-consoleErrorReports console calls, because they print to every visitor's browser.
operator-linebreakErrorRequires an operator other than ? or : at the end of a wrapped line.
yml/indentErrorRequires a 2-space indent in YAML files.
max-nested-callbacksWarningReports callbacks nested more than 3 levels deep.
no-unused-varsWarningReports unused variables.
no-plusplusWarningReports ++ and --, except in the final expression of a for loop.
import-x/no-mutable-exportsWarningReports exports declared with let or var.
jsdoc/tag-linesWarningRequires 1 blank line between a block's description and its first tag.
jsdoc/require-jsdocOffStops requiring a documentation block on every function.
consistent-returnOffNot checked.
no-param-reassignOffNot checked.
no-prototype-builtinsOffNot checked.
no-underscore-dangleOffNot checked.
no-unexpected-multilineOffNot checked.

The jsdoc settings also prefer @return to @returns and @prop to @property, matching the tag names Drupal uses.

The configuration parses code as ECMAScript 2020 and declares the browser and Node.js globals, plus read-only globals for Drupal and the libraries it loads:

{
languageOptions: {
ecmaVersion: 2020,
globals: {
...globals.browser,
...globals.node,

Drupal: 'readonly',
drupalSettings: 'readonly',
drupalTranslations: 'readonly',
jQuery: 'readonly',
_: 'readonly',
Cookies: 'readonly',
Backbone: 'readonly',
htmx: 'readonly',
loadjs: 'readonly',
Shepherd: 'readonly',
Sortable: 'readonly',
once: 'readonly',
CKEditor5: 'readonly',
CKEDITOR: 'readonly',
tabbable: 'readonly',
transliterate: 'readonly',
bodyScrollLock: 'readonly',
FloatingUIDOM: 'readonly',
},
},
}

Files ending in .test.js also get the Jest globals, can nest callbacks 5 levels deep instead of 3, and skip the jsdoc/check-tag-names rule.

Targets include custom modules only. The --max-warnings=0 flag makes warnings fail the check too:

{
"scripts": {
"lint-js": "eslint web/modules/custom --max-warnings=0 --no-error-on-unmatched-pattern"
}
}

To add or remove targets, change the paths in the lint-js and lint-fix-js scripts in package.json. The following lint-js script also lints web/sites/default:

{
"scripts": {
"lint-js": "eslint web/modules/custom web/sites/default --max-warnings=0 --no-error-on-unmatched-pattern"
}
}

The custom theme runs ESLint from its own package.json and eslint.config.mjs. The theme configuration sets the same presets, rules, and globals, has no .test.js override, and ignores only node_modules, build, and minified files. Its lint-js script checks js/**/*.js without the minified files and doesn't pass --max-warnings=0, so warnings don't fail the theme check.

Prettier integration​

eslint-plugin-prettier runs Prettier inside ESLint, and the "prettier/prettier": "error" rule reports every formatting difference as an ESLint error. Formatting problems then fail the same check as any other violation.

ahoy lint-fe-fix applies both ESLint's fixes and Prettier's formatting.

Besides the widths in Line width, the root .prettierrc.json sets the following options:

  • singleQuote set to true, so strings use single quotes
  • semi set to true, so statements end with a semicolon
  • trailingComma set to all, so a list that wraps across lines ends with a comma wherever the syntax allows one
  • plugins set to @homer0/prettier-plugin-jsdoc, which formats documentation blocks
  • jsdocReplaceTagsSynonyms set to false, so the plugin keeps a tag synonym such as @arg instead of replacing it with the official tag
  • jsdocUseTypeScriptTypesCasing set to false, so the plugin keeps the casing of basic types such as String and Object as written

Line width​

Vortex sets printWidth to 160 in .prettierrc.json instead of the Prettier default of 80, so a statement stays on 1 line unless it's longer than 160 characters. The jsdocPrintWidth option of the @homer0/prettier-plugin-jsdoc plugin holds documentation blocks to 80 columns, matching the width the PHP coding standard applies to comments.

CSS carries its own override, a printWidth of 10000 and double quotes, so Prettier never wraps a declaration. Prettier only reaches CSS when it's invoked directly, such as from an editor on save. Within Vortex, Stylelint lints the stylesheets.

The custom theme ships a second .prettierrc.json with the same settings, because a theme carries its own front-end tooling and can be moved into a separate repository. The root and the theme also ship a .prettierignore that excludes node_modules and *.yml files from Prettier.

Ignoring​

Global ignoring​

To ignore paths globally, list them in the ignores entry of the eslint.config.mjs file. The shipped list skips dependencies, core and contributed code, uploaded files, build output, and minified scripts:

{
ignores: [
'node_modules/**/*',
'vendor/**/*',
'web/core/**/*',
'web/libraries/**/*',
'web/modules/contrib/**/*',
'web/profiles/contrib/**/*',
'web/themes/contrib/**/*',
'web/sites/*/files/**/*',
'**/build/**/*',
'**/*.min.js',
],
}

Inline ignoring​

To ignore all ESLint rules in a file, add this comment to the file header:

/* eslint-disable */

To ignore specific rules in a file, add this comment to the file header:

/* eslint-disable no-console, no-unused-vars */

To ignore rules for a block of code, put it between a disable and an enable comment:

/* eslint-disable no-console */
console.log('Debug information');
/* eslint-enable no-console */

To ignore rules for the next line only, add this comment on the line before it:

// eslint-disable-next-line no-console
console.log('Single debug statement');

To ignore rules for a single line, add this comment at the end of it:

console.log('Debug'); // eslint-disable-line no-console

Continuous integration​

ESLint runs in the lint job of the continuous integration pipeline and fails the build on violations. CI lints the custom modules and the custom theme in separate steps, and skips the theme step when VORTEX_FRONTEND_BUILD_SKIP is set to 1. On GitHub Actions, the theme step reads a repository variable of that name, not the value in .env. Each step runs the lint script, which stops at the first linter that fails, so an ESLint failure keeps Stylelint from running in that step.

Ignoring failures​

To let the build pass anyway, set the VORTEX_CI_NODEJS_LINT_IGNORE_FAILURE CI variable to 1. ESLint still runs and prints its violations in the job log. The variable is shared with Stylelint, which runs in the same step.

➡️ See Continuous integration > Ignore tool failures