Tooling

ESLint

We use ESLint to encapsulate and enforce frontend code standards. Our configuration may be found in the gitlab-eslint-config project.

You can set the environment variable REVEAL_ESLINT_TODO to 1 in your environment (and your IDE’s) to see pending eslint todo’s that were excluded by .eslint_todo/*.mjs. This allows you to reveal existing eslint exceptions to fix them along your daily work.

Yarn Script

This section describes yarn scripts that are available to validate and apply automatic fixes to files using ESLint.

To check all staged files (based on git diff) with ESLint, run the following script:

shell
yarn run lint:eslint:staged

A list of problems found are logged to the console.

To apply automatic ESLint fixes to all staged files (based on git diff), run the following script:

shell
yarn run lint:eslint:staged:fix

If manual changes are required, a list of changes are sent to the console.

To check a specific file in the repository with ESLINT, run the following script (replacing $PATH_TO_FILE):

shell
yarn run lint:eslint $PATH_TO_FILE

To check all files in the repository with ESLint, run the following script:

shell
yarn run lint:eslint:all

A list of problems found are logged to the console.

To apply automatic ESLint fixes to all files in the repository, run the following script:

shell
yarn run lint:eslint:all:fix

If manual changes are required, a list of changes are sent to the console.

Limit use to global rule updates. Otherwise, the changes can lead to huge Merge Requests.

Disabling ESLint in new files

Do not disable ESLint when creating new files. Existing files may have existing rules disabled due to legacy compatibility reasons but they are in the process of being refactored.

Do not disable specific ESLint rules. To avoid introducing technical debt, you may disable the following rules only if you are invoking/instantiating existing code modules.

Disable these rules on a per-line basis. This makes it easier to refactor in the future. For example, use eslint-disable-next-line or eslint-disable-line.

Disabling ESLint for a single violation

If you do need to disable a rule for a single violation, disable it for the smallest amount of code necessary:

javascript
// bad
/* eslint-disable no-new */

import Foo from 'foo';

new Foo();

// better
import Foo from 'foo';

// eslint-disable-next-line no-new
new Foo();

Generating todo files

When enabling a new ESLint rule that uncovers many offenses across the codebase, it might be easier to generate a todo file to temporarily ignore those offenses. This approach has some pros and cons:

Pros:

  • A single source of truth for all the files that violate a specific rule. This can make it easier to track the work necessary to pay the incurred technical debt.
  • A smaller changeset when initially enabling the rule as you don’t need to modify every offending file.

Cons:

  • Disabling the rule for entire files means that more offenses of the same type can be introduced in those files.
  • When fixing offenses over multiple concurrent merge requests, conflicts can often arise in the todo files, requiring MR authors to rebase their branches.

To generate a todo file, run the scripts/frontend/generate_eslint_todo_list.mjs script:

shell
node scripts/frontend/generate_eslint_todo_list.mjs <rule_name>

For example, generating a todo file for the vue/no-unused-properties rule:

shell
node scripts/frontend/generate_eslint_todo_list.mjs vue/no-unused-properties

This creates an ESLint configuration in .eslint_todo/vue-no-unused-properties.mjs which gets automatically added to the global configuration.

Once a todo file has been created for a given rule, make sure to plan for the work necessary to address those violations. Todo files should be as short lived as possible. If some offenses cannot be addressed, switch to inline ignores by disabling ESLint for a single violation.

When all offending files have been fixed, the todo file should be removed along with the export statement in .eslint_todo/index.mjs.

Enable a rule with a grace period

Enabling a rule as error can break the default branch, even with an up-to-date todo file. Merge trains do not run ESLint. They only check that the MR pipeline is less than 16 hours old. An MR whose pipeline ran before the rule was enabled can still merge a new offense in a file that is not in the todo file. The full eslint job then fails on the default branch and in every tier-2 and tier-3 MR pipeline.

To reduce this risk, enable the rule in two merge requests:

  1. In the first merge request, enable the rule as error, generate its todo file, and add the rule to .eslint_todo/grace_period_rules.txt.

    The full eslint CI job passes the rules in this file to --suppress-rule, so their offenses do not fail it. In merge request pipelines, the job then lints the changed files without suppressions, so new offenses still fail. Inline eslint-disable comments for the rule keep working, and local runs always enforce the rule.

  2. In a second merge request, remove the rule from .eslint_todo/grace_period_rules.txt. Merge it after the first merge request has been on the default branch for at least 16 hours. Before you merge, rebase, regenerate the todo file, and review any changes to it. Then run a fresh pipeline.

The grace period does not cover every case:

  • Use it only for rules that check each file on its own. Linting changed files cannot catch offenses in unchanged files, such as an import that breaks when another file is deleted.
  • Merge requests with the pipeline::expedited label skip ESLint.

The no-undef rule and declaring globals

Never disable the no-undef rule. Declare globals with /* global Foo */ instead.

When declaring multiple globals, always use one /* global [name] */ line per variable.

javascript
// bad
/* globals Flash, Cookies, jQuery */

// good
/* global Flash */
/* global Cookies */
/* global jQuery */

Deprecating modules and functions

To deprecate a module, or to mark code for migration, add an entry to the no-restricted-imports or no-restricted-properties rule in eslint.config.mjs. Give each entry a message that names the replacement and links to the issue that tracks the migration. For examples, see the vuex and axios entries.

We do not use the import-x/no-deprecated rule. It is turned off because it is slow, and it does not report all uses. A @deprecated JSDoc tag still shows a warning in editors, but ESLint does not report it.

vue/multi-word-component-names is disabled in my file

Single name components are discouraged by the Vue style guide.

They are problematic because they can be confused with other HTML components: We could name a component <table> and it would stop rendering an HTML <table>.

To solve this, you should rename the .vue file and its references to use at least two words, for example:

  • user/table.vue could be renamed to user/users_table.vue and be imported as UsersTable and used with <users-table />.

GraphQL schema and operations validation

We use @graphql-eslint/eslint-plugin to lint GraphQL schema and operations. This plugin requires the entire schema to function properly. It is thus recommended to generate an up-to-date dump of the schema when running ESLint locally. You can do this by running the ./scripts/dump_graphql_schema script.

Formatting with Prettier

Our code is automatically formatted with Prettier to follow our style guides. Prettier is taking care of formatting .js, .vue, .graphql, and .scss files based on the standard prettier rules. You can find all settings for Prettier in .prettierrc.

Editor

The recommended method to include Prettier in your workflow is to set up your preferred editor (all major editors are supported) accordingly. We suggest setting up Prettier to run when each file is saved. For instructions about using Prettier in your preferred editor, see the Prettier documentation.

Take care that you only let Prettier format the same file types as the global Yarn script does (.js, .vue, .graphql, and .scss). For example, you can exclude file formats in your Visual Studio Code settings file:

json
  "prettier.disableLanguages": [
      "json",
      "markdown"
  ]

Yarn Script

The following yarn scripts are available to do global formatting:

shell
yarn run lint:prettier:staged:fix

Updates all staged files (based on git diff) with Prettier and saves the needed changes.

shell
yarn run lint:prettier:staged

Checks all staged files (based on git diff) with Prettier and log which files would need manual updating to the console.

shell
yarn run lint:prettier

Checks all files with Prettier and logs which files need manual updating to the console.

shell
yarn run lint:prettier:fix

Formats all files in the repository with Prettier.

VS Code Settings

Select Prettier as default formatter

To select Prettier as a formatter, add the following properties to your User or Workspace Settings:

javascript
{
  "[html]": {
    "editor.defaultFormatter": "esbenp.prettier-vscode"
  },
  "[javascript]": {
    "editor.defaultFormatter": "esbenp.prettier-vscode"
  },
  "[vue]": {
    "editor.defaultFormatter": "esbenp.prettier-vscode"
  },
  "[graphql]": {
    "editor.defaultFormatter": "esbenp.prettier-vscode"
  }
}

Format on Save

To automatically format your files with Prettier, add the following properties to your User or Workspace Settings:

javascript
{
  "[html]": {
    "editor.formatOnSave": true
  },
  "[javascript]": {
    "editor.formatOnSave": true
  },
  "[vue]": {
    "editor.formatOnSave": true
  },
  "[graphql]": {
    "editor.formatOnSave": true
  },
}

Trace unused Vue provides

Vue dependency injection decays over time. When a component that uses inject is refactored or removed, the matching provide entry often stays behind, along with the el.dataset read, the HAML data: attribute, and the Ruby helper that feed it. The scripts/frontend/trace_provide_inject_usage.mjs script finds the provided keys that no descendant injects, so you can remove the dead plumbing. For examples of this cleanup, see merge requests !242664 and !242662.

To trace a provider file or a set of files, run the script with one or more file paths or globs. The script requires ripgrep (rg) on your PATH and analyzes only .vue and .js files:

shell
node scripts/frontend/trace_provide_inject_usage.mjs <file|glob> [<file|glob>...]

For example, to trace a single entry point and then a whole directory (quote globs so the shell does not expand them):

shell
node scripts/frontend/trace_provide_inject_usage.mjs app/assets/javascripts/ci/pipeline_details/pipeline_header.js

node scripts/frontend/trace_provide_inject_usage.mjs 'app/assets/javascripts/ci/**/*.js'

The script prints a breakdown for each traced file and a final list of removal candidates. Each provided key gets one of the following verdicts:

VerdictActionDescription
REMOVABLERemove itNo component injects the key anywhere, so the provide is dead.
LIKELY-REMOVABLERemove it, then confirmInjectors exist, but none are reachable from this provider in the module import graph. Reachability is a heuristic that misses some dynamic imports, so confirm with the component specs or by loading the page.
IN USEKeep itAn injector is reachable from this provider. This result is a possibility of use, not proof, because shared modules can link unrelated components.
INCONCLUSIVEInvestigate manuallySomething prevents a reliable verdict: a dynamic boundary (a Vue.component() global registration or an unresolved dynamic import), an injector that only exists in a vendored npm package (reachability can’t be assessed across node_modules), a provider whose render() only forwards its default slot (real descendants come from the call site, not this file’s imports), or an unreachable injector that shares a directory with the provider (a strong sign the reachability heuristic missed a real relationship).

To remove a key the script reports as REMOVABLE or LIKELY-REMOVABLE, work through the layers that feed it, and stop at any layer that another consumer still uses:

  1. Remove the provide: entry.
  2. Remove the el.dataset read and any derived constants or imports, but only when they are unused elsewhere in the entry point. Keep them when the value also feeds a store, a router, props, or a sibling application.
  3. Remove the HAML data: attribute, after you confirm that no other entry point reads the same mount element.
  4. Remove the Ruby helper that builds the attribute value, after you check for other callers in both app/ and ee/app/.
  5. Update the RSpec and Jest specs that assert the removed key.

A removed provide that a descendant still injects throws an injection not found error at runtime, so load the affected page or run its feature spec to confirm the removal is safe. Scope each change to a single provider file.

Find unused production exports

Shared modules collect exports that nothing uses anymore. When the last caller of a helper is removed, the helper and its tests stay behind. The scripts/frontend/check_unused_production_code.mjs script uses knip to find the exports of a module that no production code uses. It also lists the specs that import each export, so you can clean up the tests in the same change.

To check a module or a set of modules, run the script with one or more file paths or globs:

shell
node scripts/frontend/check_unused_production_code.mjs <file|glob> [<file|glob>...]

For example, to check a single module and then a whole directory (quote globs so the shell does not expand them):

shell
node scripts/frontend/check_unused_production_code.mjs app/assets/javascripts/lib/utils/common_utils.js

node scripts/frontend/check_unused_production_code.mjs 'app/assets/javascripts/lib/utils/**/*.{js,vue}'

The script prints the exports that need an action, and the specs that import them. Add --verbose to also print the exports to keep and their production importers, or --json for machine-readable output. Each export gets one of the following verdicts:

VerdictActionDescription
removeDelete it and its testsNo production code uses the export, and the module itself does not use it either.
unexportRemove the export keywordOnly the module itself uses the export.
keepNoneProduction code uses the export, possibly through a dynamic import or a namespace import.

After you remove an export keyword, update the specs that imported the export. They should test the behavior through the exported function that uses it.

Spec usage does not change the verdict: an export that only specs use is still a removal candidate. The exception is a test seam, an export whose name ends in ForTests. A test seam exists for specs, so the script keeps it while a spec uses it.

The script treats the following files as production entry points:

  • Every pages/**/index.js and entrypoints/** file, in both CE and EE.
  • The base entry points in config/helpers/entry_points.js.
  • The modules that a webpack alias substitutes for a package, such as lib/utils/vue3compat/vue.js for vue. The script reports these modules as webpack alias targets and keeps all their exports.

Some exports are used in ways that the script cannot see:

  • Code that loads a module through a path in a string.
  • Vuex actions and getters that are dispatched by name.

A finding is a candidate to review, not an error. Application code can also land before the feature that uses it, behind a feature flag. Before you remove an export, check its history with git log and search for open merge requests that use it.

The first run takes about 30 seconds. Later runs reuse the knip cache in node_modules/.cache/knip-unused-production-code and take about 10 seconds. The script removes the caches of older configurations, and you can delete the directory at any time. A CE module that has an EE override, such as a file that ee_else_ce resolves, takes longer, because the script also analyzes the FOSS build.