The main blocker for eslint/eslint#3458 (allowing shareable configs to manage their own plugin dependencies) has been a concern that two shareable configs could depend on two different versions of a plugin. Currently, ESLint's mechanism for referring to a rule from a config (with pluginName/ruleName
) implicitly assumes plugin names are globally unique. This has led to proposals like eslint/rfcs#5 that attempt to remove the assumption that plugin names are globally unique, by giving config authors a way to disambiguate plugins with the same name.
The case where two plugins have the same name seems like it would be somewhat rare. As an alternative to a disambiguation mechanism, a few people have suggested simply raising an error when a name conflict happens (i.e. declaring that we don't support that case), which would avoid the complexity of proposals like eslint/rfcs#5. In this overview, I'll explain why "simply raising an error" here still creates some nontrivial design questions, and summarize some approaches that have been suggested/discussed for how to resolve those questions.
(Note: The examples below use .eslintrc
syntax and terminology, but all of the design decisions would also apply to the proposed new config format in eslint/rfcs#7.)
Overall, I think it's completely fine for us to decide not to support rare cases like this. The important thing is that we continue to provide the following "robustness guarantee":
A user should be able to reliably avoid unsupported cases when creating a config.
In other words, we shouldn't have a situation where a user's config is initially valid, then it suddenly moves into unsupported territory later due to events outside of the user's control (e.g. a new patch release of a dependency). This is the difference between "ESLint doesn't support some cases" and "ESLint randomly breaks sometimes" -- I think the former is acceptable, but the latter is unacceptable for a stable tool even if the random breakage only happens rarely. In this overview, I'm assuming that the robustness guarantee is uncontroversial.
If we decide to throw an error when plugin conflicts happen, the robustness guarantee is relevant to how we define a "plugin conflict". There are a few options for defining plugin conflicts:
-
Raise an error whenever two different
extends
clauses result in loading plugins with the same name.This strategy appears to be overly strict in the case of plugin-provided configs. Sometimes, the two
extends
clauses are actually intending to load the same plugin, rather than potentially-different versions of a plugin. For example, this strategy would result in a fatal error given the following config, because a plugin calledfoo
is getting loaded twice:{ "extends": [ "plugin:foo/ruleset-a", "plugin:foo/ruleset-b" ] }
-
Raise an error whenever two different
extends
clauses result in loading distinct plugins with the same name. (In other words, tolerate multipleplugins
references to with same name, as long as they resolve to the same plugin.)This strategy is an attempt to resolve the issue from strategy (1), and is the approach used by eslint/rfcs#7 at the time of writing. Unfortunately, this strategy doesn't provide the robustness guarantee. Consider the following case:
// .eslintrc.js module.exports = { extends: ['configA', 'configB'] };
// eslint-config-configA/package.json { "dependencies": { "eslint-plugin-foo": "^1.0.0" } }
// eslint-config-configA/index.js module.exports = { plugins: ['foo'], rules: { /* some rules from eslint-plugin-foo */ } };
// eslint-config-configB/package.json { "dependencies": { "eslint-plugin-foo": "=1.0.0" } }
// eslint-config-configB/index.js module.exports = { plugins: ['foo'], rules: { /* some other rules from eslint-plugin-foo */ } };
If
1.0.0
is the latest version ofeslint-config-foo
, and the user's package manager flattens duplicate packages, then the end user's config will work correctly because botheslint-config-configA
andeslint-config-configB
load the same version ofeslint-plugin-foo
. However, if[email protected]
is released later to fix a bug, theneslint-config-configA
andeslint-config-configB
will start loading different versions ofeslint-plugin-foo
, causing the user's config to suddenly break. This violates the robustness guarantee. (Depending on the implementation details of the user's package manager, it might also be necessary foreslint-config-configA
to switch toeslint-plugin-foo@^1.0.1
to trigger this issue.) -
Raise an error whenever two different
extends
clauses in different configs result in loading plugins with the same nameIn other words, the example from (1) would be allowed because it's obvious that both
plugin:foo/ruleset-a
andplugin:foo/ruleset-b
are intended to refer to the same version ofeslint-plugin-foo
, given that they appear in the same config. But if the user's shareable config hasextends: plugin:foo/ruleset-a
, the user wouldn't be able to useextends: plugin:foo/ruleset-b
in their own config. -
Raise an error whenever two different
extends
clauses in configs which are not ancestors of each other result in loading plugins with the same nameIn other words, this would allow everything from (3), and also the case where a user extends a plugin config, and the user's shareable config also extends a config from that plugin. This seems to support most of the necessary use cases, and also the robustness guarantee. However, it could lead to some additional details to work out (for example, could a sharable config reconfigure plugin rules from its sibling if it didn't introduce that plugin? What happens if a user's config tries to reintroduce a plugin that was already introduced by one of its descendants?)
-
...Other ideas?
I don't think we've fully explored this design space yet. It's possible that there are other mechanisms for identifying conflicts that still obey the robustness guarantee and would create fewer perceived false positives.