question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

Declaring shareable configs and plugins in package.json is unreliable

See original GitHub issue

As of ESLint v4.19.1, all shareable configs and plugins are loaded from the location of the eslint package. This behavior is clearly documented, and we advise users that if they want their config/plugin packages to be loaded, they should ensure that the packages are placed where eslint can find them.

However, there is a problem with this approach: package managers provide no way for users to ensure that the packages will be placed in a valid way. There are a few common practices that people use to load their configs/plugins, but all of them are actually relying on an implementation detail of a package manager rather than a well-defined behavior.

For example, in order to use a config that depends on “eslint-config-foo” and “eslint-plugin-bar”, we recommend putting this in a package.json file:

{
  "devDependencies": {
    "eslint": "^4.19.2",
    "eslint-config-foo": "1.0.0",
    "eslint-plugin-bar": "1.0.0"
  }
}

This package.json file imposes a requirement on a package manager: the eslint, eslint-config-foo, and eslint-config-bar packages must be accessible from the root package. However, it does not impose any requirement that the packages are accessible from each other, which is actually what the user needs.

It happens to be the case that with the most common package management strategy, the packages are accessible from each other anyway, because the packages are laid out like this:

(my project root)/
├── .eslintrc.json
└── node_modules/
    ├── eslint/
    └── eslint-config-foo/
    └── eslint-plugin-bar/

But I want to stress that this is an implementation detail of package managers, and not a required behavior. As a result, some package management strategies arrange packages in a way that breaks ESLint. For example, lerna would sometimes arrange packages like this:

(monorepo root)/
├── node_modules/
|   ├── eslint/
|   └── eslint-plugin-bar/
└── packages/
    ├── eslint-config-foo/
    |   └── index.js
    └── (my project root)/
        ├── .eslintrc.json
        ├── package.json
        └── node_modules/
            └── eslint-config-foo (symlink to (monorepo root)/packages/eslint-config-foo)

In this example, lerna is fulfilling all of the requirements in package.json: eslint, eslint-config-foo, and eslint-plugin-bar are all reachable from (my project root)/. However, ESLint fails to run because eslint-config-foo is not reachable from eslint.

It should be noted that adding a peerDependency of eslint in a config/plugin doesn’t help in this case. If eslint-config-foo has a peerDependency of eslint, this imposes a requirement that the same version of eslint is reachable from both the project root and from eslint-config-foo. However, there is no requrement that eslint-config-foo is reachable from eslint. In the package layout above, all peerDependencies would be satisfied, but eslint would still be broken.

Similarly, we recommend that shareable configs include all of their plugins as peerDependencies, but this is also unreliable, because it only imposes a requirement that the plugin is reachable from the config and the project root. It doesn’t impose a requirement that the plugin is reachable from the eslint package.


To summarize the problem, config file loading is generally fragile when using a package manager, because eslint has an unusual requirement about package layout and the user has no way to ensure that the requirement is satisfied. As a result, a lot of people end up getting confused about why eslint can’t find a package, and while there are solutions that appear to work in most cases, the solutions break down when the user has an uncommon-but-valid package management workflow.

I think the current plugin/config-loading system was designed when npm (specifically npm<=2) was the only prevalent package manager that people used for Node packages. As a result, the current system encourages a pattern with peerDependencies that happened to work well when using npm2, but actually relied on implementation details rather than a well-defined behavior. Now that it’s common for people to use other package management strategies (such as lerna and yarn workspaces), I think it’s important that we upgrade to be compliant with how packages are supposed to work, so that users can declare their dependencies robustly.

When discussing this before, we’ve described it as a distinction between “local” and “global” installations, and we encourage users to install all packages “locally”. However, I think this oversimplifies the issue because it assumes that everything will work fine if packages are installed locally. In fact, all of the packages in the examples above have packages installed “locally”, but they still can cause ESLint to break.

After going back and forth a few times, this problem has convinced me that we should fix the “global versus local” issue by resolving all config and plugin names from the location of the config file where they appear. This would make ESLint’s behavior consistent with how modules generally work in Node when calling require – the package that contains the module name is in charge of providing it as a dependency. This behavior also has other benefits of a well-designed module system (e.g. it’s possible to have nested dependencies, so modules are encapsulated and there isn’t a global namespace of module names).


I think the one major blocker for this change is that ESLint can’t currently handle loading multiple plugins with the same name, because users need to be able to uniquely refer to a plugin when they configure its rules. With the current system, this can’t occur because plugins need to be reachable from eslint in node_modules, which effectively guarantees a namespace without conflicts. (Unfortunately, this method of getting unique plugin names has significant downsides, e.g. it’s not possible to load a plugin from a path because it could conflict with a name from node_modules.) This issue has also blocked #3458 and #6237.

To resolve it, I think we should investigate a solution involving plugin namespaces, (along the lines of https://github.com/eslint/eslint/issues/3458#issuecomment-266594020), where users can more precisely define a rule in a config if necessary, in order to avoid naming conflicts. In my opinion, this would be a much better solution than dumping plugins into a global namespace and making the user avoid conflicts at installation time.

Issue Analytics

  • State:closed
  • Created 5 years ago
  • Reactions:27
  • Comments:16 (9 by maintainers)

github_iconTop GitHub Comments

7reactions
not-an-aardvarkcommented, Sep 28, 2018

This issue was discussed in today’s TSC meeting. The resolution was that we plan to solve this issue with option (1) from https://github.com/eslint/eslint/issues/10125#issuecomment-425169945 in a major release. (The solution would probably be https://github.com/eslint/eslint/issues/10643, although it’s still pending an implementation.) We decided not to add a stopgap solution like option (3) in the meantime, because it might have the potential to create confusion after (1) is implemented, and it might interfere with the solution for (1) if the proposal changes.

6reactions
not-an-aardvarkcommented, Sep 27, 2018

TSC Summary: ESLint’s plugin- and config-loading mechanism currently depends on implementation details of package managers like npm and yarn, rather than intended behavior. (In short, eslint needs to be able to load plugins like eslint-plugin-react relative to its own location, even though eslint never specifies eslint-plugin-react in its own dependencies.) This causes ESLint to unexpectedly break when using package management tools like Lerna or Yarn Plug N’ Play, which is creating problems for downstream tools.

It appears that any solution to this problem would require shareable configs to be able to load their plugins from their own dependencies (or from a config-provided absolute path). As discussed in https://github.com/eslint/eslint/issues/3458 and https://github.com/eslint/eslint/issues/10643, this would introduce a problem if two loaded plugins have the same name, because the user wouldn’t be able to independently configure the rules from the two plugins.

It seems like we have several ways we could proceed:

  1. Load plugins from the dependencies of the config that specifies them, and introduce a mechanism for users to disambiguate plugins with the same name (e.g. as proposed in https://github.com/eslint/eslint/issues/10643). Note that https://github.com/eslint/eslint/issues/10643 is semver-major.
  2. Load plugins from the dependencies of the config that specifies them, and throw a fatal error if two loaded plugins have the same name. (This would also be semver-major, although it would require significantly less implementation effort than (1).)
  3. Continue loading plugins from the location of the eslint package, but allow configs to specify absolute paths to plugins. Throw a fatal error if two loaded plugins have the same name. (This would not be semver-major, although it would introduce the same potential user confusion (2).
  4. Don’t change anything, and decide that we don’t support using Lerna or Yarn Plug N’ Play. I would not recommend this option; ostensibly a linter shouldn’t care what package management strategy is being used.

Solution (3) seems to be the only option that fixes the issue without a breaking change. If we adopt solution (3), I think we should commit to also adopting solution (1) or (2) in the next major release, otherwise the situation will end up very messy. In past discussions, the idea of throwing a fatal error when encountering duplicate plugin names (i.e. implementing (2) or (3)) has been very contentious.

I think solution (1) would be the best solution in the long term, but based on past discussions I suspect this opinion might also be contentious.

TSC Question: How should we address this issue?

Read more comments on GitHub >

github_iconTop Results From Across the Web

Shareable Configs - ESLint - Pluggable JavaScript Linter
A pluggable and configurable linter tool for identifying and reporting on patterns in JavaScript. Maintain your code quality with ease.
Read more >
package.json - npm Docs
This document is all you need to know about what's required in your package.json file. It must be actual JSON, not just a...
Read more >
Failed to load plugin @typescript-eslint: Cannot find module ...
I fixed this error by replacing all the ^ to empty in the package.json , and then npm i . enter image description...
Read more >
A Bit On ESLint Configuration In A React Project - Medium
There is another reason to place your config outside of package.json . By using .js or . yml formats, you can add comments...
Read more >
Specifying dependencies in Node.js - Cloud Functions
You can achieve this by declaring your module in package.json using the file: ... You can use a private npm module by providing...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found