Reasons and Solutions for eslint-config-next Failing in Next.js Projects Using pnpm

  • blog
  • front
  • study
  • eslint
Table of Contents

Introduction

Next.js, pnpm, and eslint are popular technologies widely used together. Many users combine these technologies, especially when using the --use-pnpm flag with create-next-app.

npx create-next-app@latest --use-pnpm

This blog started with a project created using a similar command (to be exact, it was pnpm create next-app).

However, when using pnpm version 10 or higher, after creating a project and running it in VSCode, eslint does not work properly. Upon inspection, the Output panel shows an error message like the one below.

eslint error

There may be an issue with the eslint settings in VSCode. However, even if the settings are correct, the above error can still occur. I discovered that eslint suddenly stopped working in my blog project and have documented the cause and solution.

This article does not cover all aspects of eslint settings. It specifically addresses cases where eslint-config-next produces errors after the eslint setup is complete, especially errors similar to the screenshot above. For more details on eslint settings, you can refer to my blog post Applying ESLint 9 to My Blog, A Struggle and Configuration Records.

Environment Used

To help others facing similar issues, I will describe the environment I used while writing the article in detail.

The project is based on the npx create-next-app@latest --use-pnpm command as of April 15, 2025, with the following major library versions:

  • MacOS Sonoma 14.4.1
  • Node.js 22.11.0
  • npm 10.9.0
  • pnpm 10.8.0
  • Next.js 15.3.0
  • eslint 9.24.0
  • @eslint/eslintrc 3.3.1
  • eslint-config-next: 15.3.0
  • react: 19.1.0
  • react-dom: 19.1.0
  • typescript: 5.8.3

Patchwork Solution

Installing Missing Libraries

Error messages often indicate their own causes. Let's take a closer look at the error message.

Error: Failed to load plugin 'react-hooks' declared in ' » eslint-config-next/core-web-vitals » /Desktop/projects/eslint-next-test/node_modules/.pnpm/[email protected][email protected][email protected]/node_modules/eslint-config-next/index.js': Cannot find module 'eslint-plugin-react-hooks'

The error states that it could not find eslint-plugin-react-hooks, which is used by eslint-config-next. Let's install this plugin.

pnpm add -D eslint-plugin-react-hooks

After installation, let's try running VSCode again. This time, we encounter a new error.

Error: Failed to load plugin '@next/next' declared in ' » eslint-config-next/core-web-vitals » /Desktop/projects/eslint-next-test/node_modules/.pnpm/[email protected][email protected][email protected]/node_modules/eslint-config-next/index.js': Cannot find module '@next/eslint-plugin-next'

Similar to before, it indicates that the @next/eslint-plugin-next plugin could not be found. Let's install that as well.

pnpm add -D @next/eslint-plugin-next

After running VSCode again, we can see a log confirming that eslint is now working correctly.

ESLint library loaded from: /Desktop/projects/eslint-next-test/node_modules/.pnpm/[email protected]/node_modules/eslint/lib/api.js

Question

However, this raises a question. Why do I need to install these plugins myself? This question arises because the libraries we just installed are explicitly defined as dependencies of eslint-config-next.

The ESLint Plugin official documentation for Next.js mentions this, and if we check the package.json of the eslint-config-next package, we find that the plugins I installed are listed under dependencies.

Similarly, when I installed the project dependencies, I could find these packages in the pnpm-lock.yaml file and in the node_modules/.pnpm directory.

# Part of pnpm-lock.yaml
[email protected]([email protected])([email protected]):
    dependencies:
      '@next/eslint-plugin-next': 15.3.0
      # omitted for brevity
      eslint-plugin-react-hooks: 5.2.0([email protected])

When I create a Next.js project using npm or yarn, such issues do not occur. So, why does this happen specifically with pnpm? What is the root cause, and how can we resolve this issue cleanly?

Root Cause

This section explores the fundamental reason for this issue. The explanation is simpler than the solution I will describe in the next section.

In summary, the previous eslint configuration method allowed eslint to locate plugins by specifying them as strings, leading to loading them directly from node_modules. There was a setting ensuring that eslint plugins existed at the top level of node_modules. However, starting from pnpm v10, this setting was removed from the default configuration. As a result, eslint could not find the plugins, leading to this bug. (pnpm issue #8878)

This explanation might be difficult to understand at first. So, I will elaborate a bit more. Some background knowledge about JavaScript package managers and module systems is helpful here.

eslint Plugin Loading Method and Requirements

The issue arises because eslint failed to load the plugins. But why did that happen? To understand this, let's examine how eslint loads plugins.

In the JavaScript ecosystem, libraries are typically imported using import or require. While there are many discussions around the differences and history of the two, it is not relevant to the main topic, so I will skip it. If needed, refer to the article JS Exploration Life - require and import and the JS Module System.

The important point is not whether one uses import or require, but that the user "specifies the path of the library module directly." Even though it goes through node_modules, the module's location is explicitly stated.

This method also applies to eslint's latest flat config format, which imports plugins using import. For example, in my blog's eslint configuration file, the plugins are explicitly stated where they are located.

import eslint from '@eslint/js';
import stylisticJs from '@stylistic/eslint-plugin';
import unusedImports from 'eslint-plugin-unused-imports';
import tseslint from 'typescript-eslint';

export default tseslint.config({
  eslint.configs.recommended,
  tseslint.configs.strictTypeChecked,
  tseslint.configs.stylisticTypeChecked,
  {
    files: ['src/**/*.{ts,tsx}'],
    plugins: {
      '@stylistic': stylisticJs,
      'unused-imports': unusedImports,
    },
    rules: {
      '@typescript-eslint/consistent-type-definitions': ['error', 'type'],
      // ...
    },
  },
})

In contrast, before flat config was introduced, the configuration could be done using multiple formats, including js, json, or yaml, through what is called the "eslintrc" method. When using a .eslintrc.json file, it looked something like this:

// Source: eslint's Configuration Files (Deprecated), Using a configuration from a plugin section
// https://eslint.org/docs/latest/use/configure/configuration-files-deprecated
{
	"plugins": ["react"],
	"extends": ["eslint:recommended", "plugin:react/recommended"],
	"rules": {
		"react/no-set-state": "off"
	}
}

When specifying plugins as strings in this way, how did eslint load them? Since these aren't js files, it wouldn't have used import.

Instead, eslint performed this task. It would take the strings specified for plugins and load them by adding eslint-plugin- as a prefix before calling require.

For example, if the plugins array contained "react", eslint would load the package eslint-plugin-react, behaving as if it had written require('eslint-plugin-react').

Furthermore, plugins could be loaded through extends, which could bring in additional configurations (eslint-config...). It’s important to note that plugins required by configurations loaded through extends are also sourced based on the location of the eslint configuration file.

To clarify, let’s consider an example. Let’s say our /.eslintrc.json contains extends: ['next']. This indicates that we're using the Next setting. If eslint-config-next utilizes eslint-plugin-react, it would seem logical for eslint to look for eslint-plugin-react in the node_modules directory within eslint-config-next.

However, this isn't the case! Instead of looking for eslint-plugin-react in the node_modules of eslint-config-next, eslint looks for it in the node_modules directory at the project root where the eslint configuration file is located.

Plugin loading diagram

This means that for eslint to function properly, all the plugin packages used in the configuration file must exist at the top level of the project’s node_modules. The eslint loading relies solely on packages at the top of the node_modules directory.

pnpm's Issue

Both npm and yarn install packages such that all packages, including their dependencies, appear at the top level of node_modules. This is known as hoisting, and while it has historical context, I will not elaborate on it here. (Performant NPM - PNPM offers more insights.) The key takeaway is that when using npm or yarn, all packages exist at the top level of the node_modules, allowing eslint to consistently find plugins.

In contrast, pnpm does not install packages at the top level of node_modules. Instead, it stores them in the node_modules/.pnpm directory and creates symlinks to only the necessary items for each package. This approach is for performance and memory efficiency. In this structure, the plugin packages typically do not reside at the top level of node_modules.

Specifically, the plugins that eslint-config-next uses as dependencies exist in node_modules/.pnpm. Therefore, if the configuration is written using the eslintrc method, eslint would fail to find the required plugins.

Due to the widespread use of eslint, pnpm was aware of this issue. As a result, until pnpm v9, the default configuration made sure that eslint and prettier (commonly used with eslint) related packages existed at the top level of node_modules. This was achieved through the public-hoist-pattern configuration setting.

This can be roughly visualized as the default setting in the .npmrc. (Of course, similar configurations can be done with the pnpm-workspace.yaml file. For more about that, see the publicHoistPattern documentation.)

public-hoist-pattern[]=*eslint*
public-hoist-pattern[]=*prettier*

However, with the introduction of flat config in eslint, the need for eslint plugins to exist at the top level of node_modules was negated. Thus, pnpm v10 removed the *eslint* and *prettier* settings from its default public-hoist-pattern.

Now, by default, eslint-related packages do not exist at the top level of node_modules with pnpm.

From a user's perspective employing flat config for eslint, there would be no issue. But it’s not solely reliant on user actions. The eslint-config-next still employs the eslintrc method for its settings (as of April 15, 2025). This means that eslint continues to look for plugins at the top level of node_modules.

These factors combined mean that using eslint-config-next in a pnpm v10 environment triggers this issue. The internal eslint configuration files used by eslint-config-next rely on the eslintrc method to load plugins, which is not supported by the configuration changes in pnpm v10.

Examining the eslint-config-next Package

To be sure, let's examine the code of the eslint-config-next package. When you create a Next.js project and set up eslint, this package will be installed. The generated eslint configuration file looks like this:

// eslint.config.mjs
import { dirname } from "path";
import { fileURLToPath } from "url";
import { FlatCompat } from "@eslint/eslintrc";

const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);

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

const eslintConfig = [
  // If the project was created with js settings, "next/typescript" is not present
  ...compat.extends("next/core-web-vitals", "next/typescript"),
];

export default eslintConfig;

Here, FlatCompat is specifically a library function designed to integrate eslintrc settings into flat config. This indicates the use of eslintrc settings. Now, let's check how next/core-web-vitals is defined in the eslint-config-next package's core-web-vitals.js.

// next.js/packages/eslint-config-next/core-web-vitals.js
module.exports = {
  extends: [require.resolve('.'), 'plugin:@next/next/core-web-vitals'],
}

Once again, it uses eslintrc formatting. Since @next/next/core-web-vitals references additional eslint-plugin configurations, an error will emerge starting with pnpm v10. Lastly, let’s look into the index.js file of eslint-config-next, which uses similar eslintrc settings.

// next.js/packages/eslint-config-next/index.js
// Complex code for default settings is omitted
module.exports = {
  extends: [
    'plugin:react/recommended',
    'plugin:react-hooks/recommended',
    'plugin:@next/next/recommended',
  ],
  plugins: ['import', 'react', 'jsx-a11y'],
  rules: {
    'import/no-anonymous-default-export': 'warn',
    // ...
  },
  // ...
}

Therefore, it has been confirmed that the configuration files used by eslint-config-next are still based on the eslintrc format rather than the flat config. Although I only examined next/core-web-vitals, next/typescript follows the same pattern.

When you use eslint-config-next, eslint will look for the necessary plugins at the top level of node_modules. However, since pnpm v10 does not place eslint-related packages at the top level, eslint will fail to locate them, resulting in errors as discussed.

Solution

While I detailed the problem's root causes, the solution is quite simple. The issue arises due to the absence of the public-hoist-pattern setting in pnpm v10, so we can reintroduce this setting. Create a .npmrc file at the project root and include the following lines.

public-hoist-pattern[]=*eslint*
public-hoist-pattern[]=*prettier*

Since the package manager's settings have changed, delete the node_modules directory and pnpm-lock.yaml file before reinstalling the packages.

rm -rf node_modules
rm -rf pnpm-lock.yaml
pnpm install

Now, eslint-related packages will be located at the top level of node_modules, allowing eslint to successfully find the plugins used in the eslintrc configuration, thus avoiding errors.

This issue would not occur if the eslint-config-next package utilized flat config. As previously mentioned, flat config directly loads plugins via import instead of through eslint.

However, it appears that adopting flat config is still a challenge for the near future. Many of the plugins that eslint-config-next depends on do not yet support flat config. Though related PRs are open, implementing them may breach compatibility.

Even if they were implemented, flat config has not yet seen widespread adoption, making its immediate use in real projects difficult. Therefore, for now, it’s best to apply the above settings. Alternatively, you could experiment with a flat config preset created by someone else.

Conclusion

Originally, eslint operated by loading plugins specified as strings in the configuration file. To achieve this, eslint needed to find plugins in the top level of the node_modules directory. pnpm held this hoisting of eslint plugin packages as its default configuration.

However, the introduction of eslint flat config in pnpm v10 eliminated that default hoisting configuration. Since eslint-config-next continues to use the eslintrc configuration file, this incompatibility caused eslint to be unable to locate plugins.

To resolve this, we can restore the public-hoist-pattern setting so that eslint-related packages appear at the top level of node_modules.

While we could have quickly resolved the issue by installing missing libraries, I wanted to avoid hasty fixes in my blog code. Thus, I pursued a thorough understanding of the problem and documented it herein.

References

Next.js documentation create-next-app

https://nextjs.org/docs/app/api-reference/cli/create-next-app

Next.js documentation ESLint Plugin

https://nextjs.org/docs/app/api-reference/config/eslint

Next.js eslint-config-next package code

https://github.com/vercel/next.js/tree/canary/packages/eslint-config-next

Next.js issue #64114, New ESLint "flat" configuration file does not work with next/core-web-vitals

https://github.com/vercel/next.js/issues/64114

Next.js issue #73968, Failed to load plugin 'react-hooks' declared in ' » eslint-config-next/core-web-vitals » Cannot find module 'eslint-plugin-react-hooks'

https://github.com/vercel/next.js/issues/73968

pnpm documentation, Settings (pnpm-workspace.yaml)

https://pnpm.io/settings#publichoistpattern

pnpm issue #8378, Remove the default option *eslint* and *prettier* from public-hoist-pattern option in next major version

https://github.com/pnpm/pnpm/issues/8378

pnpm PR #8621, feat!: remove prettier and eslint from the default value of public-hoist-pattern

https://github.com/pnpm/pnpm/pull/8621

pnpm issue #8878, Using public-hoist-pattern breaks ESLint extension?!

https://github.com/pnpm/pnpm/issues/8878

eslint Configuration Migration Guide documentation

https://eslint.org/docs/latest/use/configure/migration-guide

eslint Configuration Files (Deprecated) documentation

https://eslint.org/docs/latest/use/configure/configuration-files-deprecated

eslint Configure Plugins (Deprecated)

https://eslint.org/docs/latest/use/configure/plugins-deprecated

Introducing ESLint Compatibility Utilities

https://eslint.org/blog/2024/05/eslint-compatibility-utilities/

@eslint/eslintrc README

https://github.com/eslint/eslintrc

Comparing npm, yarn, pnpm

https://yceffort.kr/2022/05/npm-vs-yarn-vs-pnpm

Performant NPM - PNPM

https://kdydesign.github.io/2023/09/25/pnpm-tutorial/

How does ESLint analyze code?

https://1lsang.vercel.app/posts/eslint-01