Making Emacs Work with Project's ESLint

Cover Image for Making Emacs Work with Project's ESLint
Rahul M. Juliato
Rahul M. Juliato
#emacs#eslint#flymake#programming

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:

  1. 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)
  1. 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 with lemacs, 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))))
  1. Configure ESLint with Flymake

    You can configure Flymake to use the local ESLint for specific major modes like tsx-ts-mode, typescript-ts-mode, and typescriptreact-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)))
  1. 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):

eslint_01

With the Hack (both typescript server and eslint errors are shown):

eslint_02

Case B:

Project with only ESLint in an ancient version.

Without the Hack (no linting):

eslint_03

With the Hack (the right 'age compatible' linting):

eslint_04

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.