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, such as a broken import, an unused variable or a console.log() call. Vortex comes with a pre-configured ESLint ruleset for Drupal projects, including Prettier, 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 only the custom modules in web/modules/custom. The custom theme has its own eslint.config.mjs and package.json, so it can move to a separate repository with its tooling. ahoy lint-fe lints both.

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 $? prints 0 right after the command.

Fix violations​

ESLint's --fix flag repairs many violations automatically. 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 mirrors the one Drupal core uses for its own JavaScript, so the 2 configurations stay comparable as core changes.

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. It 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 and skip the jsdoc/check-tag-names rule. Their callbacks can nest 5 levels deep instead of 3.

The --max-warnings=0 flag in the root lint-js script 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 the root 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. Its presets, rules and globals match the root configuration. The theme setup differs only in the following 3 ways:

  • The theme configuration ignores only node_modules, build and minified files.
  • The theme configuration has no .test.js override.
  • The theme's lint-js script checks js/**/*.js without the minified files and omits --max-warnings=0, so warnings don't fail the theme check.

Prettier integration​

eslint-plugin-prettier runs Prettier inside ESLint, so formatting problems fail the same check as any other violation.

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 it
  • 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 as written
  • 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, up from the Prettier default of 80, so a statement stays on 1 line until it's longer than 160 characters. The jsdocPrintWidth option of the @homer0/prettier-plugin-jsdoc plugin holds documentation blocks to 80 columns, the comment width of the Drupal PHP coding standards.

CSS has its own override: a printWidth of 10000 and double quotes, so Prettier never wraps a declaration. Prettier formats CSS only when something runs it directly, such as an editor on save. Within Vortex, Stylelint lints the stylesheets.

The custom theme ships a second .prettierrc.json with the same settings, so the theme can move to a separate repository with its tooling. The root and the theme each ship a .prettierignore that excludes node_modules and *.yml files from Prettier.

Ignore violations​

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. It skips the theme step when VORTEX_FRONTEND_BUILD_SKIP is set to 1. On GitHub Actions, the theme step reads VORTEX_FRONTEND_BUILD_SKIP from a repository variable and ignores .env.

Each step runs the lint script, which runs ESLint and then Stylelint and stops at the first failure, so an ESLint failure stops Stylelint from running in that step.

Ignore 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