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:
- Ahoy
- Docker Compose
# Lint all front-end code: Twig, JavaScript and CSS.
ahoy lint-fe
# Lint only JavaScript in custom modules.
ahoy cli "npm run lint-js"
docker compose exec 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:
- Ahoy
- Docker Compose
# Fix all front-end lint issues.
ahoy lint-fe-fix
# Fix only JavaScript issues in custom modules.
ahoy cli "npm run lint-fix-js"
docker compose exec 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/jsrecommended - the rules ESLint itself shipseslint-plugin-import-xrecommended - import correctness: modules that resolve, names the module exports, and no duplicate importseslint-plugin-jsdocrecommended - documentation block correctnesseslint-plugin-prettierrecommended - Prettier integration for code formattingeslint-plugin-ymlrecommended - linting for the YAML files in custom moduleseslint-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:
| Rule | Level | Effect |
|---|---|---|
prettier/prettier | Error | Reports every difference from the Prettier formatting. |
no-console | Error | Reports console calls, because they print to every visitor's browser. |
operator-linebreak | Error | Requires an operator other than ? or : at the end of a wrapped line. |
yml/indent | Error | Requires a 2-space indent in YAML files. |
max-nested-callbacks | Warning | Reports callbacks nested more than 3 levels deep. |
no-unused-vars | Warning | Reports unused variables. |
no-plusplus | Warning | Reports ++ and --, except in the final expression of a for loop. |
import-x/no-mutable-exports | Warning | Reports exports declared with let or var. |
jsdoc/tag-lines | Warning | Requires 1 blank line between a block's description and its first tag. |
jsdoc/require-jsdoc | Off | Stops requiring a documentation block on every function. |
consistent-return | Off | Not checked. |
no-param-reassign | Off | Not checked. |
no-prototype-builtins | Off | Not checked. |
no-underscore-dangle | Off | Not checked. |
no-unexpected-multiline | Off | Not 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:
singleQuoteset totrue, so strings use single quotessemiset totrue, so statements end with a semicolontrailingCommaset toall, so a list that wraps across lines ends with a comma wherever the syntax allows onepluginsset to@homer0/prettier-plugin-jsdoc, which formats documentation blocksjsdocReplaceTagsSynonymsset tofalse, so the plugin keeps a tag synonym such as@arginstead of replacing it with the official tagjsdocUseTypeScriptTypesCasingset tofalse, so the plugin keeps the casing of basic types such asStringandObjectas 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.