ESLint 9's new config file structure

ESLint logo

Development moves pretty fast. If you stop to look around too much, you could lose it.

One stalwart of many people’s stack doing some moving is ESLint. With version 9, ESLint is bidding adieu to its “rc” style configuration files and defaulting to a new “flat” config format.

What is “flat” configuration?

In previous versions of ESLint, your config file would declare configs, environments and plugins with text strings, and ESLint would then rummage amongst its registered items for something that matched before then including its rules/functionality in your linting config.

A common example of this was including plugins:

1module.exports = {
2 // ...other config
3 plugins: ["jsdoc"],
4 // ...other config
5};

ESLint, when confronted with this config, would then search your node_modules tree for the eslint-plugin-jsdoc package before automatically importing it into your config. Very magical, but on the downside, what if you want to tweak some settings with JSDoc for your project?

The concept of “flat” configs is that everything about your linting config is available inside your eslint.config.js file. Every setting can be tweaked, extended, removed or replaced before it is sent to the linter as an array of JavaScript objects. So the previous example might be written as:

1import jsdoc from "eslint-plugin-jsdoc";
2
3export default [
4 {
5 plugins: {
6 jsdoc: jsdoc
7 },
8 }
9];

Now, the entire JSDoc linting plugin is available inside my config file. Some will undoubtedly miss the cleanliness of the “rc” format since the average eslint.config.js file is now going to be importing a fair few things and adding them to the config in a few different ways (some will be arrays of objects, some will be single objects, others might be plugins as above).

Real-world migration example

A simple real-world example might be something like the following:

module.exports = {
env: {
node: true,
},
extends: [
"./.eslintrc-auto-import.json",
"eslint:recommended",
"plugin:vue/vue3-recommended",
"prettier"
],
rules: {
"vue/multi-word-component-names": "off",
"vue/valid-v-slot": "off",
},
};

It’s a small project that uses a generated list of globals imported via Unplugin Auto Import and then applies the recommended ESLint and Vue3 rules before using the Prettier compatibility plugin. We then manually override a few rules which aren’t needed in the project.

In previous versions, ESLint would automatically go off and import our JSON file for us, then search for its recommended rule set. Then it would search node_modules for both eslint-plugin-vue and eslint-config-prettier before bringing them into the config too.

Now, there’s no magic, so we must exercise full control over what’s imported and where it’s imported from. In “flat” config, the above might look like the following:

1import AutoImportJson from "./.eslint-auto-import.json" with { type: "json" };
2import configPrettier from "eslint-config-prettier";
3import pluginVue from "eslint-plugin-vue";
4import globals from "globals";
5import js from "@eslint/js";
6
7export default [
8 {
9 languageOptions: {
10 ...AutoImportJson,
11 },
12 },
13 js.configs.recommended,
14 ...pluginVue.configs["flat/recommended"],
15 configPrettier,
16 {
17 languageOptions: {
18 globals: {
19 ...globals.node,
20 },
21 },
22 rules: {
23 "vue/multi-word-component-names": "off",
24 "vue/valid-v-slot": "off",
25 },
26 },
27];
28

Notice that we’re exporting a number of objects inside an array. ESLint will iterate through these, building up our flat array configuration. Note the following changes:

  • Globals should be placed inside the languageOptions nested object.
  • Env node: true is no longer available and we must install the globals package and import from there instead
  • Some imports (like pluginVue) export an array of objects, and so we must “spread” them into our flat array.
  • Others (like configPrettier) only export a single object, and so can be passed directly to our config.
  • Even the basic, recommended ESLint rules now have to be installed as a devDependency and then imported

Why did they do this?

The change has been a long time coming. The original RFC for flat configs was written back in 2019. The reasons that the “rc” format was problematic was complications with:

  1. Resolution behavior of modules (plugins, parser, extends)
  2. Merging of cascading config files in a directory structure
  3. Merging of config files via extends
  4. Overriding of configuration via overrides

All of these complexities arise from .eslintrc format of describing what should happen rather than how it should happen.

Nicholas C. Zakas (ESLint)

Like most projects, as the userbase grew, more people wanted more things out of ESLint and “rc” was just the wrong tool for that job.

Do I have to change now?

As with most semantically versioned projects, ESLint will go from using “rc” configs in version 8, to deprecating them in version 9, to removing them in version 10. So while version 9 is around, you’ll be able to use either format. Setting the environment flag ESLINT_USE_FLAT_CONFIG to false will unlock all the previous “rc” config functionality within version 9.