Nuxt mid-level certification launch date announced!
Article·  

Refreshed Nuxt ESLint Integrations

We revamped our ESLint integrations to support ESLint v9 with the flat config, as well as a new module with many more capabilities.

TL;DR

We revamped our ESLint integrations to support ESLint v9 with the new flat config. Along the way, we have explored many new possibilities to make it more personalized, powerful, and with better developer experience.

You can run the following command to install the new @nuxt/eslint module:

Terminal
npx nuxi module add eslint

Continue reading the story or learn more with the documentation.

Background

ESLint has become an essential tool for today's web development. It helps you to catch errors and enforce a consistent coding style in your project. At Nuxt, we do our best to provide an out-of-the-box experience for ESLint, making it easy to use, configure and follow the best practices we recommend.

Since, both Nuxt and ESLint have evolved a lot. Historically, we ended up with a few different packages and integrations for ESLint in Nuxt, and it wasn't always obvious which one to use for what purpose. We have received a lot of feedback from our community.

To improve the situation and also make it future-proof, we recently refreshed our ESLint integrations to support ESLint v9 with the flat config. It opens up many more capabilities for customizing your ESLint setup, providing a more straightforward and unified experience.

Nuxt ESLint Monorepo

We moved ESLint-related packages scattered across different repositories into a single monorepo: nuxt/eslint, with a dedicated new documentation site: eslint.nuxt.com.

To help understand the differences between each package and what to use, we also have a FAQ page comparing them and explaining their scopes.

This monorepo now includes:

  • @nuxt/eslint - The new, all-in-one ESLint module for Nuxt 3, supporting project-aware ESLint flat config and more.
  • @nuxt/eslint-config - The unopinionated but customizable shared ESLint config for Nuxt 3. Supports both the flat config format and the legacy format.
  • @nuxt/eslint-plugin - The ESLint plugin for Nuxt 3 provides Nuxt-specific rules and configurations.
  • Two packages for Nuxt 2 in maintenance mode.

ESLint Flat Config

Before diving into new Nuxt integrations, let me introduce you to the concept of ESLint flat config.

Flat config is a configuration format introduced in ESLint v8.21.0 as experimental, and became the default format in ESLint v9.

A quick reference to differentiate:

  • Flat config: eslint.config.js eslint.config.mjs etc.
  • Legacy config: .eslintrc .eslintrc.json .eslintrc.js etc.

Why Flat Config?

This blog post from ESLint explains the motivation behind the flat config system in detail. In short, the legacy eslintrc format was designed in the early time of JavaScript, when ES modules and modern JavaScript features were not yet standardized. Many implicit conventions involved, and the extends feature makes the final config result hard to understand and predict. Which also makes shared configs hard to maintain and debug.

.eslintrc
{
  "extends": [
    // Solve from `import("@nuxtjs/eslint-config").then(mod => mod.default)`
    "@nuxtjs",
    // Solve from `import("eslint-config-vue").then(mod => mod.default.configs["vue3-recommended"])`
    "plugin:vue/vue3-recommended",
  ],
  "rules": {
    // ...
  }
}

The new flat config moves the plugins and configs resolution from ESLint's internal convention to the native ES module resolution. This in turn makes it more explicit and transparent, allowing you to even import it from other modules. Since the flat config is just a JavaScript module, it also opens the door for much more customization.

Nuxt Presets for Flat Config

In the latest @nuxt/eslint-config package, we leverage the flexibility we have to provide a factory function that allows you to customize the config presets easily in a more high-level way. Here is an example of how you can use it:

eslint.config.js
import { createConfigForNuxt } from '@nuxt/eslint-config/flat'

export default createConfigForNuxt()

@nuxt/eslint-config starts with an unopinionated base config, which means we only include rules for best practices of TypeScript, Vue and Nuxt, and leave the rest like code style, formatting, etc for you to decide. You can also run Prettier alongside for formatting with the defaults.

The config also allows you to opt-in to more opinionated features as needed. For example, if you want ESLint to take care of the formatting as well, you can turn it on by passing features.stylistic to the factory function (powered by ESLint Stylistic):

eslint.config.js
import { createConfigForNuxt } from '@nuxt/eslint-config/flat'

export default createConfigForNuxt({
  features: {
    stylistic: true
  }
})

Or tweak your preferences with an options object (learn more with the options here):

eslint.config.js
import { createConfigForNuxt } from '@nuxt/eslint-config/flat'

export default createConfigForNuxt({
  features: {
    stylistic: {
      semi: false,
      indent: 2, // 4 or 'tab'
      quotes: 'single',
      // ... and more
    }
  }
})

And if you are authoring a Nuxt Module, you can turn on features.tooling to enable the rules for the Nuxt module development:

eslint.config.js
import { createConfigForNuxt } from '@nuxt/eslint-config/flat'

export default createConfigForNuxt({
  features: {
    tooling: true
  }
})

...and so on. The factory function in flat config allows the presets to cover the complexity of the underlying ESLint configuration, and provide high-level and user-friendly abstractions for end users to customize. All this without requiring users to worry about the internal details.

While this approach offers you a Prettier-like experience with minimal configurations (because it's powered by ESLint), you still get the full flexibility to customize and override fine-grained rules and plugins as needed.

We also made a FlatConfigComposer utility from eslint-flat-config-utils to make it even easier to override and extend the flat config. The factory function in @nuxt/eslint-config/flat returns a FlatConfigComposer instance:

eslint.config.js
import { createConfigForNuxt } from '@nuxt/eslint-config/flat'

export default createConfigForNuxt({
  // ...options for Nuxt integration
})
  .append(
    // ...append other flat config items
  )
  .prepend(
    // ...prepend other flat config items before the base config
  )
  // override a specific config item based on their name
  .override(
    'nuxt/typescript', // specify the name of the target config, or index
    {
      rules: {
        // ...override the rules
        '@typescript-eslint/no-unsafe-assignment': 'off'
      }
    }
  )
  // an so on, operations are chainable

With this approach, we get the best of both worlds: the simplicity and high-level abstraction for easy to use, and the power for customization and fine-tuning.

Nuxt ESLint Module

Taking this even further, we made the new, all-in-one @nuxt/eslint module for Nuxt 3. It leverages Nuxt's context to generate project-aware and type-safe ESLint configurations for your project.

Project-aware Rules

We know that Vue's Style Guide suggests the use of multi-word names for components to avoid conflicts with existing and future HTML elements. Thus, in eslint-plugin-vue, we have the rule vue/multi-word-component-names enabled by default. It's a good practice to follow, but we know that in a Nuxt project, not all .vue files are registered as components. Files like app.vue, pages/index.vue, layouts/default.vue, etc. are not available as components in other Vue files, and the rule will be irrelevant to them.

Usually, we could turn off the rule for those files like:

eslint.config.js
export default [
  {
    files: ['*.vue'],
    rules: {
      'vue/multi-word-component-names': 'error'
    }
  },
  {
    files: ['app.vue', 'error.vue', 'pages/**/*.vue', 'layouts/**/*.vue'],
    rules: {
      // disable the rule for these files
      'vue/multi-word-component-names': 'off'
    }
  }
]

It should work for the majority of the cases. However, we know that in Nuxt you can customize the path for each directory, and layers allow you to have multiple sources for each directory. This means the linter rules will be less accurate and potentially cause users extra effort to keep them aligned manually.

Similarly, we want to have vue/no-multiple-template-root enabled only for pages and layouts, etc. As the cases grow, it becomes unrealistic to ask users to maintain the rules manually.

That's where the magic of @nuxt/eslint comes in! It leverages Nuxt's context to generate the configurations and rules specific to your project structure. Very similar to the .nuxt/tsconfig.json Nuxt provides, you now also have the project-aware .nuxt/eslint.config.mjs to extend from.

To use it, you can add the module to your Nuxt project:

Terminal
npx nuxi module add eslint

Config Inspector DevTools Integrations

During the migrations and research for the new flat config, I came up with the idea to make an interactive UI inspector for the flat config and make the configurations more transparent and easier to understand. We have integrated it into Nuxt DevTools when you have the @nuxt/eslint module installed so you easily access it whenever you need it.

Screenshot of ESLint Config Inspector running as a tab in Nuxt DevTools

The inspector allows you to see the final resolved configurations, rules and plugins that have been enabled, and do quick matches to see how rules and configurations have applied to specific files. It's great for debugging and learning how ESLint works in your project.

We are delighted that the ESLint team also finds it useful and is interested in having it for the broader ESLint community. We later joined the effort and made it the official ESLint Config Inspector (it's built with Nuxt, by the way). You can read this announcement post for more details.

Type Generation for Rules

One of the main pain points of configuring ESLint was the leak of type information for the rules and configurations. It's hard to know what options are available for a specific rule, and it would require you to jump around the documentation for every rule to figure that out.

Thanks again for the new flat config being dynamic with so many possibilities. We figured out a new tool, eslint-typegen, that we could generate the corresponding types from rules configuration schema for each rule based on the actual plugins you are using. This means it's a universal solution that works for any ESLint plugins, and the types are always accurate and up-to-date.

In the @nuxt/eslint module, this feature is integrated out-of-box, so that you will get this awesome experience right away:

Screenshot of VS Code that showcases the type check and autocomplete with ESLint rules config

Dev Server Checker

With the new module, we took the chance to merge the @nuxtjs/eslint-module and the dev server checker for ESLint into the new @nuxt/eslint module as an opt-in feature.

You might not need this feature most of the time, as your editor integration should already provide ESLint diagnostics right in your editor. However, for some teams that work with different editors and want to ensure ESLint is always running, being able to run ESLint within the dev server might be helpful in some cases.

To enable it, you can set the checker option to true in the module options:

nuxt.config.ts
export default defineNuxtConfig({
  modules: [
    '@nuxt/eslint'
  ],
  eslint: {
    checker: true // <---
  }
})

Whenever you get some ESLint errors, you will see a warning in the console and the browser. To learn more about this feature, you can check the documentation.

Module Hooks

Since we are now in the Nuxt module with the codegen capabilities and the project-aware configurations, we can actually do a lot more interesting things with this. One is that we can allow modules to contribute to the ESLint configurations as well. Imagine that in the future, when you install a Nuxt module like @nuxtjs/i18n it can automatically enable specific ESLint rules for i18n-related files, or when you install something like @pinia/nuxt it can install the Pinia ESLint plugin to enforce the best practices for Pinia, etc.

As an experiment, we made a module nuxt-eslint-auto-explicit-import that can do auto inserts for the auto-imports registered in your Nuxt project with a preconfigured ESLint preset. So that you can get the same nice developer experience with auto-imports on using APIs, but still have the auto-inserted explicit imports in your codebase.

This is still in the early stages, and we are still exploring the possibilities and best practices. But we are very excited about the potential and the opportunities it opens up. We will collaborate with the community to see how we can make the most out of it. If you have any ideas or feedback, please do not hesitate to share them with us!

Ecosystem

At Nuxt, we care a lot about the ecosystem and the community as always. During our explorations to adopt the new flat config and improve the developer experience, we made quite a few tools to reach that goal. All of them are general-purposed and can be used outside of Nuxt:

We are committed to supporting the broader community and collaborating with developers to improve these tools and expand their possibilities. We are excited to see how these tools can benefit the ESLint ecosystem and contribute to the overall developer experience.

The Future

The flat config format is still fairly new, and ESLint v9 was just released a couple of weeks ago. Plugins and the community are gradually catching up to the new format. It's still in the phase of exploration and experimentation.

Looking ahead, we are eager to see how the ESLint ecosystem will continue to evolve and how we can leverage new capabilities and possibilities to further enhance Nuxt's developer experience. We are dedicated to providing a seamless and powerful development environment for Nuxt users, and we will continue to explore new ideas and collaborate with the community to achieve this goal.