developer-tools
EditorConfig vs Prettier vs ESLint: Who Does What
EditorConfig vs Prettier vs ESLint: three layers, not three competitors. What only .editorconfig can do, how Prettier reads it, and why yours isn't working.
Someone on the team opens a pull request where the only change is that every line ending flipped, the Makefile’s recipe lines turned into spaces and stopped working, and a .env file lost its final newline. You already have Prettier. You already have ESLint. Neither of them ever looked at those files — Prettier only rewrites the languages it can parse, ESLint only reads JavaScript, and the thing that governs what your editor types into everything else is a layer below both of them. Here’s the division of labor, the one place all three overlap, and the checklist for when .editorconfig appears to do nothing at all.
Three layers, not three competitors
| Runs when | Sees | Decides | |
|---|---|---|---|
| EditorConfig | You type and save, inside the editor | Every file, regardless of language | Tabs vs spaces, width, line endings, charset, trailing whitespace, final newline |
| Prettier | On save, pre-commit, or in CI | Files it has a parser for | The entire printed shape of the file — it discards your formatting and reprints from the AST |
| ESLint | On save, pre-commit, or in CI | JavaScript/TypeScript, as a program | Whether the code is wrong: unused vars, bad await, unreachable branches |
The ordering is real, not rhetorical. EditorConfig acts while the file is being written; Prettier acts on a file that already exists; ESLint reads the result and has opinions about the logic. They can all be wrong about the same file at once, but about different properties of it.
If you want the short version, the .editorconfig generator emits a valid block in about fifteen seconds and you can stop reading. The rest of this is for when it doesn’t take effect.
What only EditorConfig can do
Prettier’s built-in language list is JavaScript, JSX, Angular, Vue, Flow, TypeScript, CSS/Less/SCSS, HTML, Ember/Handlebars, JSON, GraphQL, Markdown (including GFM and MDX v1), YAML, LWC, and MJML. That is a long list — YAML and Markdown being on it surprises people — but look at what a normal repo contains that isn’t. Makefile, whose recipe lines must begin with a literal tab or the build dies with missing separator. Dockerfile and the .dockerignore next to it. .env, where a stray trailing space can become part of the secret depending on which parser reads it (there is no .env spec). Then .sql, .py, .go, .rs, .sh, .tf, .toml, .gitignore, .csv.
For every one of those, .editorconfig is the only config in the repo with an opinion. It also owns three properties for all files, including the ones Prettier does handle:
end_of_line—lf,crlf, orcr.insert_final_newline— the POSIX “text file ends with a newline” convention that shows up as\ No newline at end of filein diffs.charset—latin1,utf-8,utf-8-bom,utf-16be,utf-16le.
And the practical argument that wins in mixed-language teams: EditorConfig needs no toolchain. A data scientist opening a .py file in PyCharm and a Go developer in Neovim both get your indentation without installing Node, without npm install, and without a formatter running on files their team doesn’t format. Per editorconfig.org, JetBrains IDEs, Visual Studio, Xcode, Vim, Neovim, Emacs, Helix, and even GitHub’s and GitLab’s web editors support it with no plugin at all.
Where they overlap: Prettier reads your .editorconfig
This is the part people get wrong in both directions. Prettier does not ignore .editorconfig — it converts four properties into its own options:
.editorconfig | Prettier option |
|---|---|
indent_style | useTabs |
indent_size (or tab_width) | tabWidth |
max_line_length | printWidth |
end_of_line | endOfLine |
Two more are honored by being non-negotiable: Prettier always writes UTF-8 and always inserts a final newline, so charset = utf-8 and insert_final_newline = true hold whether you write them or not.
Prettier’s own config wins. The docs are explicit that an .editorconfig-derived configuration “will be overridden by .prettierrc, etc.” So tabWidth: 4 in .prettierrc beats indent_size = 2 — the correct precedence, and the first thing to check when the two disagree.
The default differs by entry point, which catches people. The CLI reads .editorconfig by default — --no-editorconfig exists specifically to turn it off. The Node API does not: resolveConfig only parses it “if options.editorconfig is true,” so prettier.resolveConfig(filePath) in a custom script ignores your indent settings until you pass { editorconfig: true }. Editor integrations sit in between and choose for you; the official Prettier VS Code extension exposes prettier.useEditorConfig, defaulting to true.
One asymmetry worth knowing: max_line_length is not in the current EditorConfig specification’s property list. It’s a de-facto extension that Prettier and most IDEs implement anyway — it just isn’t something every conforming tool is required to understand.
The ESLint half of the argument is over
“Prettier vs ESLint” was a real debate for about six years, and the ESLint team settled it. In ESLint v8.53.0, released November 3, 2023, the core formatting rules — indent, semi, quotes, comma-dangle, space-before-function-paren, and the rest — were deprecated in one batch, on the reasoning that whitespace “is better handled holistically by source code formatters.”
They are still in ESLint. Open the current docs for indent and you get:
This rule was deprecated in ESLint v8.53.0. It will be removed in v11.0.0. Please use the corresponding rule in
@stylistic/eslint-plugin.
Deprecated since v8.53.0, still functional in v9 and v10, scheduled for removal in v11.0.0, with @stylistic/eslint-plugin as the maintained home for anyone who genuinely wants lint-based formatting. Nothing in your config breaks today — but eslint-config-prettier, which exists to disable the conflicting rules, is now solving a problem that is deprecated upstream. The intended shape of a modern setup: ESLint for correctness, Prettier for shape, EditorConfig for everything Prettier can’t parse.
A real .editorconfig for a mixed repo
root = true
[*]
indent_style = space
indent_size = 2
charset = utf-8
end_of_line = lf
trim_trailing_whitespace = true
insert_final_newline = true
[*.md]
trim_trailing_whitespace = false
[Makefile]
indent_style = tab
[*.{yml,yaml}]
indent_size = 2
[*.py]
indent_size = 4
[*.{bat,cmd,ps1}]
end_of_line = crlf
Line by line, because three of those exist for non-obvious reasons:
root = truestops the upward search. Without it, EditorConfig keeps walking up past your repo — into~/and beyond — and a stale.editorconfigin your home directory silently governs every project you own. The spec: “the search shall stop if an EditorConfig file is found with therootkey set totruein the preamble or when reaching the root filesystem directory.”[*.md]turns trimming off because two trailing spaces are a hard line break in Markdown. Trim them and the rendered output changes.[*.{bat,cmd,ps1}]iscrlfbecause some Windows shells genuinely misbehave on LF-terminated scripts. Everything else stayslf.
Our .editorconfig generator builds one section at a time: pick the glob, indent style and size, charset, and line ending, and it emits root = true plus a single [glob] block with indent_style, indent_size (or tab_width when you choose tabs), charset, end_of_line, trim_trailing_whitespace, and insert_final_newline. For a multi-section file like the one above, generate the [*] block with root checked, then generate each extra block with it unchecked and paste them underneath. It does not emit max_line_length — add that by hand if you want Prettier to take its print width from here.
The precedence rules that surprise people
Two rules, running in opposite directions. Within one file, later sections win — if [*] sets indent_size = 2 and a later [*.py] sets 4, Python files get 4; reverse the order and they get 2, because [*] is now the more recent match. Across files, the nearest file wins — a .editorconfig in packages/legacy/ overrides the repo root for everything under it, which is the supported way to keep one vendored subdirectory on tabs.
Then the glob rule that generates the most “my section doesn’t match” reports: a pattern containing a / is anchored to the .editorconfig’s own directory; a pattern without one matches at any depth below it. So [*.py] matches src/api/handlers.py and [Makefile] matches a Makefile in any subdirectory — but [src/*.py] matches only files directly in src/, not in src/api/. Use ** when you mean “cross directories”: [src/**/*.py].
”EditorConfig isn’t working” — the checklist
Run through these in order.
- Your editor needs the plugin. VS Code is on editorconfig.org’s Download a Plugin list, not the native one — install EditorConfig for VS Code. Same for Sublime Text, Notepad++, Eclipse, NetBeans, and VSCodium. JetBrains, Visual Studio, Xcode, Vim, Neovim, Emacs, and Helix need nothing.
- The file was already wrong before the config existed. EditorConfig governs what the editor writes from now on; it never reformats on open. Do one deliberate normalization pass and commit it separately so the diff stays reviewable.
- The glob doesn’t match.
/anchors, no/floats — and[*.yml]does not match.yaml. - A later section, or a nearer file, is overriding you.
root = trueis missing and a.editorconfigfurther up the tree is still in play.- Prettier is running after save with a conflicting
.prettierrc. Prettier’s config beats the.editorconfig-derived one, so the last writer wins and it isn’t EditorConfig. - Git is converting line endings underneath you. This is the big one, and it looks exactly like EditorConfig failing.
end_of_line vs core.autocrlf
end_of_line = lf tells the editor what to write to disk. Git’s core.autocrlf tells Git what to write during checkout. Both can be on, pointing in opposite directions, and the result is a file that reverts to CRLF every time you pull.
From git config’s own documentation:
Setting this variable to “true” is the same as setting the
textattribute to “auto” on all files andcore.eolto “crlf”. Set to true if you want to have CRLF line endings in your working directory and the repository has LF line endings. This variable can be set toinput, in which case no output conversion is performed.
autocrlf = true is what the Windows Git installer offers by default, so on a mixed-OS team it is probably already on for half of you — and because it is per-machine and uncommitted, the symptom is always “it only happens to Priya.” Set it to input (or leave it false) on macOS and Linux, and commit a .gitattributes with * text=auto plus an explicit eol=lf for scripts: the text attribute normalizes to LF in the index on check-in regardless of anyone’s local config.
Same neighborhood, same shape of bug: why your .gitignore isn’t working and your .dockerignore is not .gitignore.
What to do
- Add
.editorconfigeven if you have Prettier. It covers the Makefile, the Dockerfile, the.env, the SQL, and the line endings of all of them — plus every contributor whose editor has no Node toolchain behind it. - Keep
[*]boring, and add sections only where a tool genuinely requires it: Makefile, Windows scripts, Markdown, Python. Always writeroot = trueon the first line. - Decide once where indentation is defined. If both files set it, Prettier’s wins and the
.editorconfigvalue is a lie for every file Prettier touches. Pick.editorconfigand deletetabWidth/useTabsfrom.prettierrc. - Stop fighting ESLint about formatting. Its formatting rules have been deprecated since v8.53.0 and go away in v11.0.0 — let it lint, and let Prettier print.
- Check
core.autocrlfbefore blamingend_of_line.
Build the file with the .editorconfig generator — it runs entirely in your browser and nothing is uploaded. It sits next to the .gitignore generator and the .dockerignore generator for the other two files every repo needs on day one, and the package.json explorer if you want to see which of your scripts are actually invoking a formatter in the first place.