James Mugliston

James Mugliston

Using the New ESLint Flat Config

For better or worse, ESLint is a standard linting tool used across JavaScript and TypeScript projects. However, configuring it can be painful — especially when used in monorepos with different linting requirements between packages.

A while ago, ESLint announced a new config system, but from what I've seen, adoption has been quite slow. For example, in the VSCode ESLint extension, it's currently only available as a pre-release feature but will be enabled as default soon!

Let's explore this new flat configuration approach and how it can benefit our projects.

Why?

So why do we need yet another ESLint configuration method? Though powerful, ESLint's traditional hierarchical configuration system often leads to complexities, especially in larger projects. Managing multiple configuration files spread across directories could be a headache, not to mention the potential for conflicting rules and redundant configurations.

Previously, you could use any of the following configuration files:

  • .eslintrc

  • .eslintrc.json

  • .eslintrc.js

  • .eslintrc.yaml

  • etc...

Now, with the new flat config, we have a single file eslint.config.js

The ESLint flat config introduces a single, centralised configuration file for the entire project. This flat structure promises easier management, clearer visibility of rules, and reduced chances of configuration conflicts.

What it Looks Like

Let's examine how this new flat configuration structure differs from the conventional hierarchical setup.

In the traditional setup, we'd typically have multiple .eslintrc files across different directories, each specifying rules for that particular directory and its subdirectories. This led to decentralized and often confusing configuration.

Using the new flat config syntax, we consolidate all our ESLint rules into a single .eslintrc.js file placed at the root of our project. This file encompasses rules for the entire project, ensuring a unified and consistent linting experience across all files and directories.

Monorepo Example

I'll show a simple setup in a monorepo for configuring linting using the new flat config. The flat config is defined as an array where we can specify file glob patterns and their associated rules in a hierarchical way.

const config = [
  {
    files: ['**/*.js'],
    rules: {
      'no-unused-vars': 'error'
    }
  },
  {
    files: ['packages/package-a/**/*.js'],
    rules: {}
  },
  {
    files: ['packages/package-b/**/*.js'],
    rules: {
      'no-unused-vars': 'off'
    }
  }
]

export default config

As you can see, I have enabled the no-unused-var rule across all JavaScript files but specifically disabled it in package b. This means I would expect to see a lint error in package a, which you can see within VSCode and the CLI (run using Lerna).

An example error in VSCode with eslint.
An example error from eslint from the cli.

This is a great start, but I normally want to add something like the eslint rules for the Standard JS code style. Currently, the eslint-config-standard package doesn't support the new config style, but we can still make it work using FlatCompat. The example below shows how we can include the standard js rules as part of our flat config file:

import { FlatCompat } from '@eslint/eslintrc'
import path from 'path'
import { fileURLToPath } from 'url'

const __filename = fileURLToPath(import.meta.url)
const __dirname = path.dirname(__filename)

const compat = new FlatCompat({
  baseDirectory: __dirname
})

const config = [
  ...compat.extends('standard'),
  {
    files: ['**/*.js'],
    rules: {
      'no-unused-vars': 'error'
    }
  },
  {
    files: ['packages/package-a/**/*.js'],
    rules: {}
  },
  {
    files: ['packages/package-b/**/*.js'],
    rules: {
      'no-unused-vars': 'off'
    }
  }
]

export default config

This example uses the FlatCompat#extends() method to insert the "standard" config into the flat config array.

Importing plugins has also changed with the new config file. Previously ESLint would use a string-based import system. The new format uses JavaScript objects, which means you can just use import / require statements to load plugins from external sources. I've added a plugin example below to demonstrate this:

import { FlatCompat } from '@eslint/eslintrc'
import path from 'path'
import { fileURLToPath } from 'url'
import eslintPluginPrettierRecommended from 'eslint-plugin-prettier/recommended'

const __filename = fileURLToPath(import.meta.url)
const __dirname = path.dirname(__filename)

const compat = new FlatCompat({
  baseDirectory: __dirname
})

const config = [
  ...compat.extends('standard'),
  {
    files: ['**/*.js'],
    eslintPluginPrettierRecommended,
    rules: {
      'no-unused-vars': 'error'
    }
  },
  {
    files: ['packages/package-a/**/*.js'],
    rules: {}
  },
  {
    files: ['packages/package-b/**/*.js'],
    rules: {
      'no-unused-vars': 'off'
    }
  }
]

export default config

Using these examples, we can easily build out a fully-fledged flat config for a monorepo with different linting requirements per package!

Summary

The JavaScript ecosystem has changed a lot since ESLint was first created, and this new config approach allows the ESLint project to move forward, bringing in new sensible defaults and a more streamlined approach to configuration. I think this change will be especially well suited to teams that use ESLint across large monorepos, as centralising rules into a single configuration file offers a simpler and more consistent approach to linting.