Making Emacs Work with Project's ESLint
Discover how to configure Emacs to use the project-specific ESLint binary, ensuring compatibility with older projects. Learn about the issue caused by the ESLint v9.0.0 update and how to hack Flymake (via flymake-eslint) for a happy experience.
Making Emacs Work with Project's ESLint
This means: local ESlint from node_modules, the one you installed to your project.
Introduction
After a recent clean reinstall of the asdf
plugin, which manages my
Node.js versions via .tool-versions
, I encountered an issue: ESLint
stopped working in Emacs. My older projects, which were not compatible
with the newer globally installed ESLint, caused Emacs to fail
silently when trying to load ESLint. Debugging revealed that the
incompatibility stemmed from the ESLint v9.0.0 update, which
deprecated the previous configuration format, .eslintrc
, in favor of
the new default configuration format, eslint.config.js
[source].
This change affected both my Emacs and Neovim personal configurations,
as neither editor was set to use the local ESLint binary, but rather
the global one.
The Hack: Using Local ESLint Binary with Flymake
To solve this problem, I created/adapted a hack to make Flymake in
Emacs use the project-local ESLint binary located in
node_modules/.bin/eslint
instead of the globally installed
version. This ensures that each project uses the version of ESLint it
was originally configured with, avoiding compatibility issues.
Here’s how to set it up:
-
Install flymake-eslint
First, make sure you have
flymake-eslint
installed. You can do this by adding the following to your Emacs configuration file:
(use-package flymake-eslint
:ensure t
:config
;; If Emacs is compiled with JSON support
(setq flymake-eslint-prefer-json-diagnostics t)
-
Define Function to Use Local ESLint
Next, define a function that sets the project's
node_modules
binary ESLint as the first priority. Note that my functions are prefixed withlemacs
, which is the name of my Emacs configuration available at LEmacs:
(defun lemacs/use-local-eslint ()
"Set project's `node_modules' binary eslint as first priority.
If nothing is found, keep the default value flymake-eslint set or
your override of `flymake-eslint-executable-name.'"
(interactive)
(let* ((root (locate-dominating-file (buffer-file-name) "node_modules"))
(eslint (and root
(expand-file-name "node_modules/.bin/eslint"
root))))
(when (and eslint (file-executable-p eslint))
(setq-local flymake-eslint-executable-name eslint)
(message (format "Found local ESLINT! Setting: %s" eslint))
(flymake-eslint-enable))))
-
Configure ESLint with Flymake
You can configure Flymake to use the local ESLint for specific major modes like
tsx-ts-mode
,typescript-ts-mode
, andtypescriptreact-mode
:
(defun lemacs/configure-eslint-with-flymake ()
(when (or (eq major-mode 'tsx-ts-mode)
(eq major-mode 'typescript-ts-mode)
(eq major-mode 'typescriptreact-mode))
(lemacs/use-local-eslint)))
-
Add Hooks
Finally, add the function to the
eglot-managed-mode-hook
and other relevant hooks:
(add-hook 'eglot-managed-mode-hook #'lemacs/use-local-eslint)
;; With older projects without LSP or if eglot fails
;; you can call interactivelly M-x lemacs/use-local-eslint RET
;; or add a hook like:
(add-hook 'js-ts-mode-hook #'lemacs/use-local-eslint))
Notice that as lemacs/use-local-eslint
is an interactive function it
might also be called manually with M-x lemacs/use-local-eslint RET
.
Full Code
(use-package flymake-eslint
:ensure t
:config
;; If Emacs is compiled with JSON support
(setq flymake-eslint-prefer-json-diagnostics t)
(defun lemacs/use-local-eslint ()
"Set project's `node_modules' binary eslint as first priority.
If nothing is found, keep the default value flymake-eslint set or
your override of `flymake-eslint-executable-name.'"
(interactive)
(let* ((root (locate-dominating-file (buffer-file-name) "node_modules"))
(eslint (and root
(expand-file-name "node_modules/.bin/eslint"
root))))
(when (and eslint (file-executable-p eslint))
(setq-local flymake-eslint-executable-name eslint)
(message (format "Found local ESLINT! Setting: %s" eslint))
(flymake-eslint-enable))))
(defun lemacs/configure-eslint-with-flymake ()
(when (or (eq major-mode 'tsx-ts-mode)
(eq major-mode 'typescript-ts-mode)
(eq major-mode 'typescriptreact-mode))
(lemacs/use-local-eslint)))
(add-hook 'eglot-managed-mode-hook #'lemacs/use-local-eslint)
;; With older projects without LSP or if eglot fails
;; you can call interactivelly M-x lemacs/use-local-eslint RET
;; or add a hook like:
(add-hook 'js-ts-mode-hook #'lemacs/use-local-eslint))
Demo
Case A:
Project with Typescript and ESLint version 8.x.x.
Without the Hack (only typescript server errors are shown):
With the Hack (both typescript server and eslint errors are shown):
Case B:
Project with only ESLint in an ancient version.
Without the Hack (no linting):
With the Hack (the right 'age compatible' linting):
Conclusion
By hacking flymake-eslint
to use the local ESLint binary, you can
maintain compatibility with older projects without relying on globally
installed versions that may introduce breaking changes. This approach
ensures a smooth development experience in Emacs, keeping ESLint
functional across various project setups.
As a small sidenote, I did noticed some time between the first time the first project buffer opens and flymake starts to work, but it looks like this is already the case even without the hack, just start editing and things will adjust themselves.
Also, if you do not use your projects with the built-in project.el
(as you probably should), I believe the hack is easy enough to be
changed to your needs.