Eslint flat config and new system an ultimate deep dive 2023

Mohamed Lamine Allal
33 min readNov 25, 2023

--

Eslint flat config, deep dive, full guide. With examples and practice and a large covering.

  • this article intends to cover all that can be covered about the new system. Too long. The ultimate guide. If no time. Consume with skimming (TLDS)
Eslint, FlatConfig, Full HD

Was AI used in the writing of this article?

  • In most of my articles, I don’t use AI. In case I would do. I would write and mention notices and show where at every place.
  • For this one, Absolute zero (not at all). First, I wrote in my knowledge base docs (Months ago). I decided to give time to share it. If you love follow for more deep articles.

Navigation of this article

The article goes in this structure

  • Introduce you to the new system Flat Config and compare it with eslintrc, and what changed. As well as fundamentals. And it’s very long.
    ◌ Many parts are a repetition over what the eslint team shared through their blog (refs included below). But I go into more details in every place where there is what to show. + formatting …
  • After that. We go with important examples
  • And, in the last section, I do go into FlatCompat and that would help further empower understanding and solidify-ing how to work with the new system. And also gain confidence. I covered clearing any possible guessing. And covered everything well. By going deep through the code source and explaining going through it.

You’ll find those section separated by section separator ( . . . ) in the middle
- I believe also it’s critical to cover a good insight about the fundamentals and this whole transition to the new system base. After it you’ll be in solid foot.

Overview and What You Learn

  • Eslint came a long way. A new system is taking place. And things changed and we are moving to a better place
  • Old config: eslintrc.
    New one: flat config
  • The goals and design choices when making flat config (Eslint team)
  • What changed between the old and the new and what to look-up for
  • New eslintbetter default behavior and config
  • Config file => only one format now, js only
  • Flat config env variable
  • config format
  • Esm support is here and is the default (No esm support before)
  • Files matching in the new flat conf, and ignores
  • flat cascade and how config objects are merged
  • Language options are better now
  • Things that were removed (extends, overrides, env)
  • Things that were moved and structured better
  • Things that remained the same
  • Globals handed back to us
    ◌ (Goodbye environments , hello globals)
  • Use of globals package
  • Setting ecmaVersion and sourceType
  • Packcages no more strings. Now we import all. And we can add things better
  • Custom parsers and parser options (mostly the same, but better) [no you import, object config]
  • Plugins and their loading and their config, and the naming and the rules
  • The naming convention when you import plugins the strong guide
  • processors
  • linter options
  • predefined configs
    string way
    object way
  • How to extends and configure old plugins and packages, the new way and using a helper that translates old configs and backward compatibility
  • Deep dive into FlatCompat eslintrc translation tool
  • Vscode extension setup (activate flat config)
  • Productivity: Be productive setting up eslint
  • Some critical one [TODO]

— — — — — — — — — — — — — — — —

  • understand eslint naming convention, and string resolution
  • configs of disabling rules and of enabling rules, order of insertion
    plugin, config name to package name (not scoped, and scoped)
  • eslint with prettier and JSON support
    new way and FlatCompat old way
    ◌ How to name the plugins
    ◌ The different ways for prettier
    eslint-plugin-prettier
    eslint-plugin-prettier vs eslint-config-prettier
    ◌ How does eslint-config-prettier
    ◌ Well-set templates to use

— — — — — — — — —

  • Deep deep dive into how FlatCompat, Eslintrc translation tool works (resume, and Link to a second article in the Eslint series)
    ◌ Deep from code detailing how the old config translates to the new. And a lot of clearing, coming from the code directly
    ◌ how to use Vscode debugger to analyze projects and code like a pro
    ◌ Awesome patterns used in such type of work

Doc and references

Migration guide

Let’s show you an example to get you fired up (long, brain, memory, image, hook)

/** @type {import('eslint').Linter.FlatConfig[]} */
export default [
// js files
{
files: ['**/*.{mjs,cjs,js}'],
languageOptions: {
globals: {
...globals.es2021,
},
// ecmascriptVersion, and sourceType, default is right
},
// extends: ['plugin:prettier/recommended', 'airbnb-base'],
plugins: {
prettier: prettierPlugin
},
rules: {
...jsRules,
...prettierPlugin.configs.recommended.rules,
...eslintConfigPrettier.rules
},
},
// json files
{
// ...
}
]
/** @type {import('eslint').Linter.FlatConfig[]} */
export default [{},{},{}]
[  
{
//...
},
` // ...
...compat.extends('plugin:prettier/recommended'),
];
  • The first one is a prettier config done fully in flat-config way the best way
    ◌ The details and the certainty and the logic of how to manage will be explained and we will go to more than just this example. Including the full signature.
    ◌ This is an example, for your brain conditioning and image construction. Flat take an array.
  • FlatCompat tool does the same, magic translation — will leave philosophy for later.
    ◌ Through its analysis. You will understand more about how a lot works in the new flat config (all last section) and how to translate and migrate the old config to the new one.

The goals of flat config

(By Eslint team, ref already included in doc above, a good and must read)

To set the stage for the changes in the flat config, we had several goals:

  1. Logical defaults — the way people write JavaScript has changed a lot in the past nine years, and we wanted the new config system to reflect our current reality rather than the one we lived in when ESLint was first released.
  2. One way to define configs — we didn’t want folks to have multiple ways to do the same thing any longer. There should be one way to define configs for any given project.
  3. Rules configs should remain unchanged — we felt like the way rules were configured already worked fine, so to make it easier to transition to flat config, we didn’t want to make any changes to rule configs. The same rules key can be used the same way in flat config.
  4. Use native loading for everything — one of our biggest regrets about eslintrc was recreating the Node.js require resolution in a custom way. This was a significant source of complexity and, in hindsight, unnecessary. Going forward, we wanted to leverage the loading capabilities of the JavaScript runtime directly.
  5. Better organized top-level keys — the number of keys at the top-level of eslintrc had grown dramatically since ESLint was released. We need to look at which keys were necessary and how they related to one another.
  6. Existing plugins should work — the ESLint ecosystem is filled with hundreds of plugins. It was important that these plugins continued to work.
  7. Backwards compatibility should be a priority — even though we are moving to a new config system, we didn’t want to leave all of the existing ecosystem behind. In particular, we wanted to have ways for shareable configs to continue to work as closely as possible. While we knew 100% compatibility was probably unrealistic, we wanted to do our best to ensure existing shareable configs would work.

With these goals in mind, we came up with the new flat config system.

Logical defaults (Awesome) 🔥

✨ ecmaVersion defaulting to "latest"

(ecmaVersion)

  • ecmaVersion: "latest" for all JavaScript files - That’s right, by default all JavaScript files will be set to the latest version of ECMAScript. This mimics how JavaScript runtimes work, in that every upgrade means you are opting-in to the latest and greatest version of JavaScript. This change should mean that you probably won’t have to manually set ecmaVersion in your config unless you want to enforce a previous version due to runtime constraints. You will still be able to set ecmaVersion all the way down to 3 if necessary.
  • Old was:
    ◌ defaults to es5 (Doc Ref)
    ◌ Globals and features were different, to support some globals you had to use { “env”: { “es6”: true } }

sourceType defaulting

(sourceType)

  • Default of all .js and .mjs files
    sourceType: "module"
    ◌ - By default, flat config assumes you are writing ESM.
    ◌ If not, you can always set sourceType back to "script".
  • .cjs files
    sourceType: "commonjs"
    ◌ - We are still in a transition period where a lot of Node.js code is written in CommonJS.
    ◌ To support those users, we added a new sourceType of "commonjs" that configures everything correctly for that environment.
  • OLD
    ◌ default: script
    ◌ no commonjs (only script or module )
    Doc Ref

✨ search for .js .mjs .cjs (old system searched for .js only)

  • With flat config
    ◌ ESLint searches for .js, .mjs, and .cjs files
    ◌ all three of the most common JavaScript filename extensions are automatically searched.
  • With eslintrc (OLD)
    .js files only.
    ◌ and you would need to use the --ext flag to define more.
    ▶︎ — ext jsx,cjs,js

Config file 🔥🔥 (only one)

  • Flat config
    ◌ support only one config file and place to configure
eslint.config.js

◌ in root

◌ No support for other formats

◌ no configuration in package.json

  • Old (eslintrc)
    .eslintrc.js
    .eslintrc.cjs
    .eslintrc.yaml
    .eslintrc.yml
    .eslintrc.json
    package.json

Reasons and goals

  • Unifying configuration
    ◌ Same cross all projects
    ◌ one way and an efficient one (js give all possibilities)
    ◌ No need to think of what file type, format, and limitations
  • Optimizing config file search upward traversal
    ◌ When the ESLint CLI is used,
    ▶︎ By default, it searches for eslint.config.js from the current working directory.
    - ︎and if not found will continue the search up the directory’s ancestors
    - until the file is found or the root directory is hit. 🔥🔥
    ▶ ︎The one eslint.config.js file only, does dramatically reduce the disk access required
    - as compared to eslintrc, which had to check for all the other supported config files formats. At every directory level.

The benefit of configuring with js

  • Additionally, using a JavaScript file allowed us (eslint team)
    ◌ to rely on users to load additional information that their config file might need.
    ◌ Instead of extends and plugins loading things by name, you can now just use import and require as necessary to bring in those additional resources.
    ▶︎ That doesn't make things more flexible. Also js is the most dynamic way. I personally (me Allal), I prefer js, for variables reusability

✨ Specifying the config manually

ESLINT_USE_FLAT_CONFIG=true npx eslint --config eslint.config.js **/*.js
  • setting the ESLINT_USE_FLAT_CONFIG environment variable to true
  • and using the -c or --config option on the command line to specify an alternate configuration file, such as —config config/eslint.config.js

Flat config env variable ESLINT_USE_FLAT_CONFIG

  • ESLINT_USE_FLAT_CONFIG=true: Only use eslint.config.js (flat config).
  • ESLINT_USE_FLAT_CONFIG=false: Only use eslintrc files (the many formats).
  • Unset or any other value: First try eslint.config.js, then eslintrc.

❗Wait. A set back

Eslint usage: CLI & Core library (nodejs) & integrations

(Usage, CLI, Core library, integration)

  • Through CLI, or the nodejs library API, or integrations
    ◌ 3 ways

The eslint core is exposed in the eslint package

  • The eslint package is required on all setup
  • Generally, for version purposes, we install eslint per project pnpm add eslint --save-dev
  • CLI does use the core library. And so the Integrations
  • We can programmatically use, and execute eslint and configure it.

✨ CLI

# All default files in lib
# - For Eslintrc => .js only
# - For FlatEslint => .js, .cjs, .mjs
npx eslint lib/
# jsx and js files in lib/
npx eslint --ext .jsx --ext .js lib/
npx eslint --ext .jsx --ext .js lib/ --fix
  • In most cases, we do use the CLI
  • We would use nodejs scripting and core library
    ◌ In advanced cases where we need something granular or event-based. Or part of a build script.
    ◌ Or if we are using eslint part of a plugin, another library, or software we are building.

✨ Core Library (programmatically)

  • Before (Eslintrc )
const { ESLint } = require("eslint");

(async function main() {
// 1. Create an instance.
const eslint = new ESLint();
// 2. Lint files.
const results = await eslint.lintFiles(["lib/**/*.js"]);
// 3. Format the results.
const formatter = await eslint.loadFormatter("stylish");
const resultText = formatter.format(results);
// 4. Output it.
console.log(resultText);
})().catch((error) => {
process.exitCode = 1;
console.error(error);
});
const { ESLint } = require("eslint");

(async function main() {
// 1. Create an instance with the `fix` option.
const eslint = new ESLint({ fix: true });
// 2. Lint files. This doesn't modify target files.
const results = await eslint.lintFiles(["lib/**/*.js"]);
// 3. Modify the files with the fixed code.
await ESLint.outputFixes(results);
// 4. Format the results.
const formatter = await eslint.loadFormatter("stylish");
const resultText = formatter.format(results);
// 5. Output it.
console.log(resultText);
})().catch((error) => {
process.exitCode = 1;
console.error(error);
});
const config:  = [];

const eslint = new ESLint({
useEslintrc: false,
overrideConfig: config,
fix: true
});
// Lint the specified files and return the results
async function lintAndFix(eslint, filePaths) {
const results = await eslint.lintFiles(filePaths);
// Apply automatic fixes and output fixed code
await ESLint.outputFixes(results);
return results;
}

◌ Doc Ref

  • The new Flat config

(Flat config)

— — — — -

✨ Flat config with Linter class

— — — — -
◌ If you are currently using Linter from the eslint package, you can enable flat config by setting configType: "flat" as an option on the constructor.

const linter = new Linter({ configType: "flat" });

const messages = linter.verify("new Map()", {
languageOptions: {
ecmaVersion: 5,
sourceType: "script"
},
rules: {
"no-undef": "error"
}
}, "filename.js");

◌ While this base case works the same regardless of which config system you’re using, there are some important differences:

  • defineRule(), defineRules(), and defineParser() now throw errors. Runtime plugins (discussed in my previous post) make these methods obsolete.
  • getRules() also throws an error. This method would return different data depending on when it was called, so it can’t be used with flat config.

— — — — -

✨ Flat config with Eslint class

— — — — -

◌ Introduction of FlatEslint class

  • Temporal class for transition (in the future it will be renamed back to Eslint)
    Eslint team :

While implementing flat config we discovered that it would be too difficult to create an option to switch between config systems like we did for Linter. Instead, we created a FlatESLint class that encapsulates all of the existing functionality in ESLint but uses flat config instead of eslintrc. The FlatESLint class is intended only as a preview of functionality; once we switch over to flat config permanently, the current ESLint class will be deleted and FlatESLint will be renamed to ESLint.

  • You can access FlatEslint through eslint/use-at-your-own-risk
// ESM
import pkg from "eslint/use-at-your-own-risk";
const { FlatESLint } = pkg;

// CommonJS
const { FlatESLint } = require("eslint/use-at-your-own-risk");

// Usage
const eslint = new FlatESLint({
cwd: originalDir,
overrideConfigFile: "other.config.js"
});
const results = await eslint.lintText("foo");
  • As with Linter, there are a few differences between FlatESLint and ESLint worth pointing out:
    Caching is not yet implemented in FlatESLint, so cache: true throws an error.
    ◌ The useEslintrc option has been removed. If you want to avoid automatic loading of eslint.config.js without specifying an alternate config file, set overrideConfigFile: true. 🔥
    ◌ The envs option has been removed.
    ◌ The resolvePluginsRelativeTo option has been removed.
    ◌ The rulePaths option has been removed. Custom rules must be added directly by config (imported).
  • 🔥 The best demonstration (example) of the usage is in the vscode-eslint extension
    https://github.com/microsoft/vscode-eslint/blob/24d2ac45b2fe1b8cc8639038b724ba48610da8e2/server/src/eslint.ts#L872C102-L872C102
    That’s the line that brings for us the experimental option in the vscode eslint extension.
    ◌ ✨ The whole construct also shows a great example of 🔥 how to make a smooth transition to a whole new system 🔥. We can see the good thinking that was put by the eslint team. And the introduction of an experimental API. And how a depending project uses it. As vscode-eslint did to provide the experimental features. ✨

✨ Integrations

❗let’s resume

Config format

The format is a flat array of FaltConfig objects

/** @type {import('eslint').Linter.FlatConfig[]} */
export default [{},{},{}]
  • That comment will add auto-complete for vscode 🔥

The FlatConfig

interface FlatConfig {
/**
* An array of glob patterns indicating the files that the configuration
* object should apply to. If not specified, the configuration object applies
* to all files
*/
files?: Array<FlatConfigFileSpec | FlatConfigFileSpec[]>;
/**
* An array of glob patterns indicating the files that the configuration
* object should not apply to. If not specified, the configuration object
* applies to all files matched by files
*/
ignores?: FlatConfigFileSpec[];
/**
* An object containing settings related to how JavaScript is configured for
* linting.
*/
languageOptions?: {
/**
* The version of ECMAScript to support. May be any year (i.e., 2022) or
* version (i.e., 5). Set to "latest" for the most recent supported version.
* @default "latest"
*/
ecmaVersion?: ParserOptions["ecmaVersion"],
/**
* The type of JavaScript source code. Possible values are "script" for
* traditional script files, "module" for ECMAScript modules (ESM), and
* "commonjs" for CommonJS files. (default: "module" for .js and .mjs
* files; "commonjs" for .cjs files)
*/
sourceType?: "script" | "module" | "commonjs",
/**
* An object specifying additional objects that should be added to the
* global scope during linting.
*/
globals?: ESLint.Environment["globals"],
/**
* An object containing a parse() or parseForESLint() method.
* If not configured, the default ESLint parser (Espree) will be used.
*/
parser?: ParserModule,
/**
* An object specifying additional options that are passed directly to the
* parser() method on the parser. The available options are parser-dependent
*/
parserOptions?: ESLint.Environment["parserOptions"],
};
/**
* An object containing settings related to the linting process
*/
linterOptions?: {
/**
* A Boolean value indicating if inline configuration is allowed.
*/
noInlineConfig?: boolean,
/**
* A Boolean value indicating if unused disable directives should be
* tracked and reported.
*/
reportUnusedDisableDirectives?: boolean,
};
/**
* Either an object containing preprocess() and postprocess() methods or a
* string indicating the name of a processor inside of a plugin
* (i.e., "pluginName/processorName").
*/
processor?: string | Processor;
/**
* An object containing a name-value mapping of plugin names to plugin objects.
* When files is specified, these plugins are only available to the matching files.
*/
plugins?: Record<string, ESLint.Plugin>;
/**
* An object containing the configured rules. When files or ignores are specified,
* these rule configurations are only available to the matching files.
*/
rules?: RulesRecord;
/**
* An object containing name-value pairs of information that should be
* available to all rules.
*/
settings?: Record<string, unknown>;
}

✨ Finally supporting ESM

  • The format now is esm by default.

Files matching with globs

files and ignores

  • files to match
  • ignores to ignore files
    ◌ filters files matched by files
  • use minimatch-based glob patterns to match files

❗ if no files, then

  • match all at files level
    ignores will filter them after

process

files

• Matches the file

  • if yes
    ◌ Check if should be ignored
    ▶︎ if not
    — matched ✅
    ▶︎ if yes
    — not matched ❌
  • if no
    ◌ good not matched ❌

• Mainly, I guess a glob-based solution is used. Which already support ignore

✨ Ignoring files completely (special behavior)

  • What if you want to ignore files completely?
  • You can do that by specifying a config object that has only an ignores key, like this:
export default [
{
ignores: ["**/*.test.js"]
},
{
files: ["**/*.js"],
rules: {
"semi": "error",
"no-unused-vars": "error"
}
}
];
  • With this config, all JavaScript files ending with .test.js will be ignored. You can think of this as the equivalent of ignorePatterns in eslintrc, albeit with minimatch patterns.

If ignores is used without any other keys in the configuration object, then the patterns act as global ignores.

export default [
{
ignores: [".config/*"]
}
];
export default [
{
ignores: [
"!node_modules/", // unignore `node_modules/` directory
"node_modules/*", // ignore its content
"!node_modules/mylibrary/" // unignore `node_modules/mylibrary` directory
]
}
];

Removed extends

  • extends are now replaced by flat cascade
    overriding nature of flat config

flat cascade and how config objects are merged

(flat cascade, config object merging)

▶︎ Objects that have overlapping patterns

  • are merged from top to down

▶︎ Process

  • Linter => gonna lint a file
  • file when matched, had the array of matched configs
  • eslint will take those configs
    ◌ merge them from top to down (first to last)
    - With deep merge
    ◌ The last config will override the other ones

✨ How to use this to extends and create base configs

(extends, base config)

  • The base config. Should go first
  • You can have multiple ones
  • The one coming after would override the one before 🔥🔥

✨ Examples

While we wanted to get rid of the directory-based config cascade, flat config actually still has a flat cascade defined directly in your eslint.config.js file.

Inside of the array, ESLint finds all config objects that match the file being linted and merges them together in much the same way that eslintrc did. The only real difference is the merge happens from the top of the array down to the bottom instead of using files in a directory structure.

For example:

export default [
{
files: ["**/*.js", "**/*.cjs"],
rules: {
"semi": "error",
"no-unused-vars": "error"
}
},
{
files: ["**/*.js"],
rules: {
"no-undef": "error",
"semi": "warn"
}
}
];

This config has two config objects with overlapping files patterns. The first config object applies to all .js and .cjs files while the second applies only to .js files. When linting a file ending with .js, ESLint combines both config objects to create the final config for the file. Because the second config sets semi to a severity of "warn", that takes precedence over the "error" that was set in the first config. The last matching config always wins when there is a conflict (overlapping).

What this means for shareable configs is that you can insert them directly into the array instead of using extends, such as:

import customConfig from "eslint-config-custom";

export default [
customConfig,
{
files: ["**/*.js", "**/*.cjs"],
rules: {
"semi": "error",
"no-unused-vars": "error"
}
},
{
files: ["**/*.js"],
rules: {
"no-undef": "error",
"semi": "warn"
}
}
];

Here, customConfig is inserted first in the array so that it becomes the base configuration for this file. Each of the following config objects builds upon that base to create the final config for a given JavaScript file.

Language options (restructured well)

(eslint team)

  • 🔥 ESLint has always had a strange mix of options that affected how JavaScript was interpreted. There was the top-level globals key that modified available global variables, and ecmaVersion and sourceType as parserOptions, not to mention env to add more globals. Perhaps the most confusing is that you had to set both ecmaVersion and add an environment like es6 to enable both the syntax you wanted and ensure that the correct global variables would be available.
  • 🔥 In flat config, we moved all keys related to JavaScript evaluation into a new top-level key called languageOptions.
{
languageOptions?: {
/**
* The version of ECMAScript to support. May be any year (i.e., 2022) or
* version (i.e., 5). Set to "latest" for the most recent supported version.
* @default "latest"
*/
ecmaVersion?: ParserOptions["ecmaVersion"],
/**
* The type of JavaScript source code. Possible values are "script" for
* traditional script files, "module" for ECMAScript modules (ESM), and
* "commonjs" for CommonJS files. (default: "module" for .js and .mjs
* files; "commonjs" for .cjs files)
*/
sourceType?: "script" | "module" | "commonjs",
/**
* An object specifying additional objects that should be added to the
* global scope during linting.
*/
globals?: ESLint.Environment["globals"],
/**
* An object containing a parse() or parseForESLint() method.
* If not configured, the default ESLint parser (Espree) will be used.
*/
parser?: ParserModule,
/**
* An object specifying additional options that are passed directly to the
* parser() method on the parser. The available options are parser-dependent
*/
parserOptions?: ESLint.Environment["parserOptions"],
};
]

✨ Env removed

  • No more need to set env and ecmaVersion both 🔥❗
  • only languageOptions
  • no more duplications
  • better structured

▪️ Most of the time now because of new logical defaults (Already mentioned above 👆)

  • ecmaScript is set to latest
    ◌ most of the time you wouldn’t need to change that

Setting ecmaVersion and sourceType

(ecmaVerision, sourceType)

▪️ moved to languageOptions

  • ︎Apply them only when you need to
  • The default already works well in most cases
  • new
    globals in the new system are set by ecmaVersion or sourceType
export default [
{
files: ["**/*.js"],
languageOptions: {
ecmaVersion: 5,
sourceType: "script"
}
}
];

ecmaVersion

New

▪️ Enable both syntax and global variables based on the specified version of ECMAScript.

  • global variables are set
  • In contrast to old system
    ◌ set in env
    ◌ which created the need to set things in two places (env, parserOptions.ecmaVersion) [Ref]

sourceType

  • Similar to ecmaVersion, this key affects not just how a file is parsed, but also how ESLint evaluates its scope structure.
  • We kept the traditional "module" for ESM
  • and "script" for scripts,
  • and also added "commonjs". (enable commonjs globals )
    ◌ which lets ESLint know that it should treat the file as CommonJS
    ◌ which also enables CommonJS-specific globals .🔥🔥
  • If you are using ecmaVersion: 3 or ecmaVersion: 5, be sure to set sourceType: script

Goodbye environments , hello globals 🔥

(environments, globals)

▪️ doc: configuring-global-variables

▪️ env fully removed

▪️ in the old system

  • eslint was managing environments and globals
    ◌ They do change through time
    ◌ did need new releases to update them
  • was using globals package under the hood
    ◌ they worked with the author

▪️ env removing

  • Because it’s no longer needed.
    ◌ All of the custom functionality we hooked onto environments for use with Node.js is now covered by sourceType: "commonjs"
    ◌ so all that was left was for environments to manage global variables.
    ◌ It doesn’t make sense for ESLint to do this in the core, so we are handing this responsibility back to you. (Eslint team)
  • globals package to help you with setting globals 🔥
  • Now 🔥
    ◌ with globals handed to us
    ◌ We can update the globals immediately without needing to wait for new eslint release
export default [
{
files: ["**/*.js"],
languageOptions: {
globals: {
var1: "writable",
var2: "readonly"
}
}
}
];

▪️ Old ( eslintrc ) - to clear things -

  • environments (env)
    ◌ are presets of predefined globals
    - ex: es2020, es…, node, and, jest, mocha, …. there is so many
    - And they are defined in this file (using globals package)
  • globals
    ◌ Defined In top level globals prop in config, or through comments in code files.
    ◌ Allow adding specific custom globals. Or basically, it’s the mean for setting globals manually. And env was a helper (preset).

✨ Use of globals package

(globals)

install globals package

pnpm add globals -D
  • For the moment es is limited at es2021
  • And there isn’t a eslatest or latest something option 🔥
import globals from "globals";

export default [
{
files: ["**/*.js"],
languageOptions: {
globals: {
...globals.browser,
myCustomGlobal: "readonly"
}
}
}
];

✨ Disabling globals

Globals can be disabled with the string "off". For example, in an environment where most ES2015 globals are available but Promise is unavailable, you might use this config:

export default [
{
languageOptions: {
globals: {
Promise: "off"
}
}
}
];
  • For historical reasons, the boolean value false and the string value "readable" are equivalent to "readonly". Similarly, the boolean value true and the string value "writeable" are equivalent to "writable". However, the use of older values is deprecated.

blog saying about env and globals

Environments in eslintrc provided a known set of globals and were a constant source of confusion for users. They need to be kept up to date (especially in the case of browser) and that update needs to wait for ESLint releases. Plus, we had hooked some additional functionality onto environments to make it easier to work with Node.js, and in the end, we made a mess.

For flat config, we decided to remove the env key completely. Why? Because it’s no longer needed. All of the custom functionality we hooked onto environments for use with Node.js is now covered by sourceType: "commonjs", so all that was left was for environments to manage global variables. It doesn’t make sense for ESLint to do this in the core, so we are handing this responsibility back to you.

Years ago, we worked with Sindre Sorhus to create the globals package, which extracted all of the environment information from ESLint so that it would be available to other packages. ESLint then used globals as the source for its environments.

With flat config, you can use the globals package directly, updating it whenever you want, to get all of the same functionality that environments used to provide. For example, here is how you add browser globals into your configuration:

import globals from "globals";

export default [
{
files: ["**/*.js"],
languageOptions: {
globals: {
...globals.browser,
myCustomGlobal: "readonly"
}
}
}
];

The languageOptions.globals key works the same as it did in eslintrc, only now, you can use JavaScript to dynamically insert any global variables that you want.

Custom parsers and parser options (mostly the same, but better)

(Custom parsers, parser options)

The parser and parserOptions keys have now moved into the languageOptions key, but they mostly work the same as in eslintrc with two specific differences:

  1. You can now insert the parser object directly into the config.

2.

  • Parsers can now be bundled with plugins
  • and you can specify a string value for parser to use a parser from a plugin. (Described more in the next section.)

Here’s an example using the Babel ESLint parser:

import babelParser from "@babel/eslint-parser";

export default [
{
files: ["**/*.js", "**/*.mjs"],
languageOptions: {
parser: babelParser
}
}
];

This configuration ensures that the Babel parser, rather than the default, will be used to parse all files ending with .js and .mjs.

You can also pass options directly to the custom parser by using the parserOptions key in the same way as it works in eslintrc:

import babelParser from "@babel/eslint-parser";

export default [
{
files: ["**/*.js", "**/*.mjs"],
languageOptions: {
parser: babelParser,
parserOptions: {
requireConfigFile: false,
babelOptions: {
babelrc: false,
configFile: false,
// your babel options
presets: ["@babel/preset-env"],
}
}
}
}
];

Using plugins

✨ More powerful and configurable plugins

The strength of ESLint is the ecosystem of plugins that individuals and companies maintain to customize their linting strategy. As such, we wanted to be sure that existing plugins continued to work without modification as well as allowing plugins to do things they were never able to do in the past.

On the surface, using a plugin in flat config looks very similar to using a plugin in eslintrc.

▪️ The big difference is

  • that eslintrc used strings whereas flat configs uses objects (imported).
  • In FlatConfig now you can name a plugin whatever you want
    ◌ Instead of using plugin string name. Now you import an object and pass it with a name of your choice
    ▶︎ However there are cases where you should name a plugin in a specific way 🔥. If some of the plugins depend on others and require a specific name (very rare case).
import jsdoc from "eslint-plugin-jsdoc";

export default [
{
files: ["**/*.js"],
plugins: {
jsdoc
}
rules: {
"jsdoc/require-description": "error",
"jsdoc/check-values": "error"
}
}
];

This config uses the eslint-plugin-jsdoc plugin by importing it as a local jsdoc variable and then inserting it into the plugins key in the config. After that, the rules inside the plugin are referenced using the jsdoc namespace.

Note: Because plugins are now imported like any other JavaScript module, there’s no more strict enforcement of plugin package names. You no longer need to include eslint-plugin- as the prefix for your package names…but we would like it if you did. (Eslint team )

✨ Personalized plugin namespaces

Because the name of the plugin in your config is now decoupled from the name of the plugin package, you can choose any name you want, as in this example:

import jsdoc from "eslint-plugin-jsdoc";

export default [
{
files: ["**/*.js"],
plugins: {
jsd: jsdoc
}
rules: {
"jsd/require-description": "error",
"jsd/check-values": "error"
}
}
];

Here, the plugin is named jsd in the config, so the rules also use jsd to indicate which plugin they are coming from.

import jsdoc from "eslint-plugin-jsdoc";

export default [
{
files: ["**/*.js"],
plugins: {
someDomain: jsdoc
}
rules: {
"someDomain/require-description": "error",
"someDomain/check-values": "error"
}
}
];
  • I set the namespace to be someDomain instead. And it would work.
  • However, in general, it’s advised to follow a certain naming convention. Will explain it in the next section.
  • If some modules depend on some plugins. And does set rules of a plugin. Then the naming of that plugin when set in the configuration. is required to be set with the same name that the module defines the rules with. We will show an example of such a case later on.

How to use example

✨ Good practices ✨

✨ How to name plugins convention 🔥

  • Read the doc of the plugin and check for flat config
    CTRL|CMD + F

◌ If found, follow it

▶︎ if not

▪️ If plugin is an old one and doesn’t do anything for new flat-config

  • "eslint-plugin-jsdoc" if named with eslint-plugin-
    ◌ take jsdoc (what comes after)
    ◌ use it as the name
    ◌ to stay consistent with that time naming

▪️ If not following eslint-plugin-

Custom rules

  • Simply load the rule file
  • And either use spread to include
  • or create a custom plugin (object) and add the rules in it
    ◌ example below

✨ From --rulesdir to runtime plugins

( — rulesdir)

( eslint team )

  • With eslintrc, rules needed to be loaded by the CLI directly in order to be available inside of a config file. This means either bundling custom rules in a plugin or using the --rulesdir flag to specify the directory from which ESLint should load custom rules. Both approaches required some extra work to set up and were a frequent cause of frustration for our users.
  • ✨ With flat config, you can load custom rules directly in the config file. Because plugins are now objects directly in the config, you can easily create runtime plugins that exist only in your config file, such as:
import myrule from "./custom-rules/myrule.js";

export default [
{
files: ["**/*.js"],
plugins: {
custom: {
rules: {
myrule
}
}
}
rules: {
"custom/myrule": "error"
}
}
];

Here, a custom rule is imported as myrule and then a runtime plugin is created named custom to provide that rule to the config as custom/myrule.

🔥 As a result, we will be removing --rulesdir once the transition to flat config is complete. 🔥

Processors work in a similar way to eslintrc

(processsors)

▪️ The one addition in flat config is that processor can now also be an object containing both a preprocess() and a postprocess() method.

▪️ Primary use case

  • specify to use a processor defined in a plugin with plugin/processor string: "markdown/markdown"
    markdown plugin (named by us) plugins: { markdown: markdownPlugin, ...}
    ◌ use markdown processor of that plugin
import markdown from "eslint-plugin-markdown";

export default [
{
files: ["**/*.md"],
plugins: {
markdown
},
processor: "markdown/markdown"
}
];
  • use processor markdown that is defined in plugin eslint-plugin-markdown

Organized linter options

In eslintrc, there were a couple of keys that related directly to how the linter operated, namely noInlineConfig and reportUnusedDisableDirectives. These have moved into the new linterOptions key but work exactly the same as in eslintrc. Here’s an example:

export default [
{
files: ["**/*.js"],
linterOptions: {
noInlineConfig: true,
reportUnusedDisableDirectives: true
}
}
];

Shared settings are exactly the same

The top-level settings key behaves the exact same way as in eslintrc. You can define an object with key-value pairs that should be available to all rules. Here’s an example:

export default [
{
settings: {
sharedData: "Hello"
}
}
];

Using predefined configs

  • string
  • objects
    ◌ with imports

✨ String way

ESLint has two predefined configs:

  • eslint:recommended - enables the rules that ESLint recommends everyone to use to avoid potential errors
  • eslint:all - enables all of the rules shipped with ESLint

To include these predefined configs, you can insert the string values into the exported array and then make any modifications to other properties in subsequent configuration objects:

export default [
"eslint:recommended",
{
rules: {
semi: ["warn", "always"]
}
}
];
  • Here, the eslint:recommended predefined configuration is applied first and then another configuration object adds the desired configuration for semi.

✨ Object way 🔥

import js from "@eslint/js";

export default [
js.configs.recommended,
{
rules: {
semi: ["warn", "always"]
}
}
];
  • You’ll find auto-completion this way 🔥

✨ Object way allows specifying to only a subset of files 🔥🔥

import js from "@eslint/js";

export default [
{
files: ["**/src/safe/*.js"],
...js.configs.recommended
}
];
  • We can be precise to what part, and matched files, we are going to apply the rules
  • Granual setting.

Backward compatibility with old config, and using old config

FlatCompat Backwards compatibility utility

( FlatCompat )

As mentioned previously, we felt like there needed to be a good amount of backwards compatibility with eslintrc in order to ease the transition. The @eslint/eslintrc package provides a FlatCompat class that makes it easy to continue using eslintrc-style shared configs and settings within a flat config file. Here’s an example:

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

// mimic CommonJS variables -- not needed if using CommonJS
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);

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

export default [
// mimic ESLintRC-style extends
...compat.extends("standard", "example"),
// mimic environments
...compat.env({
es2020: true,
node: true
}),
// mimic plugins
...compat.plugins("airbnb", "react"),
// translate an entire config
...compat.config({
plugins: ["airbnb", "react"],
extends: "standard",
env: {
es2020: true,
node: true
},
rules: {
semi: "error"
}
})
];

Using the FlatCompat class allows you to continue using all of your existing eslintrc files while optimizing them for use with flat config. We envision this as a necessary transitional step to allow the ecosystem to slowly convert over to flat config. 🔥🔥

✨ Translating a whole old config

// translate an entire config
...compat.config({
plugins: ["airbnb", "react"],
extends: "standard",
env: {
es2020: true,
node: true
},
rules: {
semi: "error"
}
})

▪️ without overrides

▪️ if you have overrides

  • use flat objects for each override
  • set the no override global base config as the first flat object
  • use compat.config() to translate global and each override config

Better use new configuration

  • There is no need for translation 🔥
  • unless you want to save time
  • but the whole ecosystem is moving to flat configs
    ◌ so u would get to it, you would get to it

Vscode extension setup

To have the extension support Flat config. We need to activate that in settings.

▪️ to setup for the project repo only

  • choose Workspace
    Workspace will create the config in .vscode/settings.json
  • User would create it in vscode app directory
    ◌ depending on your system
    - in my case ~/Library/Application Support/Code/User/settings.json
  • that would add the following to .vscode/setting.json
"eslint.experimental.useFlatConfig": true
  • you can add this directly without UI
  • Which is basically managed by the extension through this code 👀using the experiment feature.

Productivity

✨ Your own eslint modular packages

— — -

✨ In old eslintrc you could extend eslint with

— — -

// index.js
module.exports = {

globals: {
MyGlobal: true
},

rules: {
semi: [2, "always"]
}

};

◌ Then publish to npm
◌ And use with

{
"extends": "eslint-config-myconfig"
}

or

{
"extends": "myconfig"
}

◌ As the convention eslint-config- allows that.
◌ Follow this for scoped modules @scope/eslint-config-myconfig
- And you can check Eslint: how to name plugins 👀 article for the naming convention for more details.

— — -

Flat config

— — -

  • All the sharing now is natural. And very flexible. Just publish and make about any module for any piece. And you can include it and use it across all projects flexibly with any part of the config. And merge things easily and gradually
    ◌ So for example you can set different rules
    ◌ You can have one module that contains all the different things and organize them in a good structure (configs, rules, plugins, …)
    ◌ 🔥 One good thing when you set such kinds of modules is you can include the dependencies within them. So no need to remember any package to install 🔥.
    ◌ that also applies for old eslintrc

✨ Vscode or some other tool snippets

  • vscode snippets are critical
    ◌ in vscode to be super fast
  • raycast snippet or others
    works across all editors

By setting the config as snippet, you wouldn’t waste any time

Even when you setup your packages for reusability

  • create a snippet for that particular usage
  • you may like to install some snippet extensions to make snippet creation even better as an experience (and for speed)
  • For details on how you can be really productive with snippets and integrate them in your flow check Be extremely productive with snippets setup, part of your flow article 👀 🔥. I wrote about it. It’s a big deal for those who are missing out on it!
    ◌ For eslint
    => create eslint.config.js
    => type eslint you get your snippet suggestions (IntelliSense)
    => Pick the right one (this can be skipped if you type something that pick you the thing directly ex: eslintjsjson)
    => Your config is ready
    (5–10 secs max)
    - If you tweak it => 1mins, 5 mins …

It makes a huge difference and lowers friction fully. And the need to remember. The article above does go deeply into the details.

✨ Boiler

▪️ manage boiler

  • either through a repo for all boilers locally
  • make a shortcut to open it in vscode fast
    ◌ I use raycast and have a command to do that
    ◌ it takes me a second literally
  • search fast with vscode through file search for ex: eslint js json prettier (depending on what you name
    ◌ you get fast the boiler folder
    copy files
    ◌ and past
  • Boiler is too great when you setup multiple files all at once
    ◌ The tooling setup can make a huge difference

✨ another option is npm packages with create cli

▪️ create script

▪️ that allows you to easily call using npm

▪️ And with a CLI that has interaction

▪️ You can have one project for all your configs

▪️ One CLI to pick up from all

  • can be inspired by tools like
    nextjs
    tauri
    ◌ …
    ◌ reuse their CLI code
    - and do it fast

Personally, at least for now. I’m sticking with the combination of snippets, boiler and modules. And generally i always use snippets as it works faster for me, with no friction.

  • I’m counting on doing better though.

Example: Setting up Eslint and Prettier with eslint-prettier-plugin

▪️ Flat config: Setting up Eslint and Prettier with eslint-prettier-plugin 🔥🔥 👀

  • Three ways to use prettier with Eslint
    eslint-config-prettier + prettier run separately
    - - eslint-config-prettier=> disable eslint conflicting rules with prettier => So you can use prettier separately along.
    ✨ Using Eslint plugin: eslint-plugin-prettier (Full eslint integration)
    - - Provides formatting with Eslint, and lining (seeing red lines)
    ✨ Using prettier-eslint (Not recommended)
    - - Details in article above
  • How to pick
    ◌ I recommend eslint-plugin-prettier
    ◌ Consider eslint-config-prettier with prettier run separately. If you would ever hit something with the first way, like performance (which I think you will never get to encounter.)
    - - Details in the article above

✨ Setting up eslint-config-prettier

Two ways

  • Using FlatCompat util ( from @eslint/eslintrc package )
  • Pure Flat Config without FlatCompat

✨ First FlatCompat way gonna be by simply adding

import { FlatCompat } from '@eslint/eslintrc';
import globals from 'globals';
import * as url from 'url';

const __filename = url.fileURLToPath(import.meta.url);
const __dirname = url.fileURLToPath(new URL('.', import.meta.url));
const compat = new FlatCompat({
baseDirectory: __dirname, // optional; default: process.cwd()
resolvePluginsRelativeTo: __dirname, // optional
});

/** @type {import('eslint').Linter.FlatConfig[]} */
export default [
// js files
{
files: ['**/*.{mjs,cjs,js}'],
languageOptions: {
globals: {
...globals.es2021,
},
// ecmascriptVersion, and sourceType, default is right
},
rules: {
// ...eslintPluginPrettier.rules,
...jsRules,
},
},
// prettier config all above this one are supposed to use prettier
...compat.extends('plugin:prettier/recommended'),
];

✨ And pure Flat Config gonna be with

import prettierPlugin from 'eslint-plugin-prettier';
import eslintConfigPrettier from 'eslint-config-prettier';
import globals from 'globals';

// ...

/** @type {import('eslint').Linter.FlatConfig[]} */
export default [
// js files
{
files: ['**/*.{mjs,cjs,js}'],
languageOptions: {
globals: {
...globals.es2021,
},
// ecmascriptVersion, and sourceType, default is right
},
plugins: {
prettier: prettierPlugin
},
rules: {
...jsRules,
...prettierPlugin.configs.recommended.rules,
...eslintConfigPrettier.rules
},
}
];

🔥❗ How to figure out what to do? details?=> Detailed article above

  • Check as well What about if you want to do some things for just some files title at the end

🔥 Should you pick up pure Flat Config or FlatCompat

  • It’s up to you.
  • I recommend Pure Flat Config as it gives you a precise granular control
  • Use FlatCompat if you want to go faster. But generally, you would set the config once and for all. So I would still recommend in general, to give the little time to check the plugin's source. And their structure. So you know how and what to import. And it doesn’t take long, once you clearly know what you should do. And the article above does detail all of that well.

How FlatCompat utility works and how to translate from eslintrc to Flat Config

Eslint: strings names resolution

  • Eslint: strings names resolution 🔥🔥 ✨
    ◌ Do you know how the string resolution works? And the convention. Here more details, and examples then we covered above.

--

--

Mohamed Lamine Allal

Developper, Entrepreneur, CTO, Writer! A magic chaser! And A passionate thinker! Obsessed with Performance! And magic for life! And deep thinking!