Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

New: Config File Simplification #9

Open
wants to merge 51 commits into
base: master
from

Conversation

@nzakas
Copy link
Member

nzakas commented Jan 22, 2019

Summary

This proposal provides a way to simplify configuration of ESLint through a new configuration file format. The configuration file format is written in JavaScript and removes several existing configuration keys in favor of allowing the user to manually create them.

Related Issues

@eslint eslint bot added the triage label Jan 22, 2019

@nzakas nzakas added the feature label Jan 22, 2019

@platinumazure
Copy link
Member

platinumazure left a comment

Nice work! This looks very promising!

Identified a few minor typos (the first one is unfortunately breaking the formatting in the PR diff view). Wasn't too sure about one of them, so please check before applying suggestions. Thanks!

Show resolved Hide resolved designs/2019-config-simplification/README.md Outdated
Show resolved Hide resolved designs/2019-config-simplification/README.md Outdated
Show resolved Hide resolved designs/2019-config-simplification/README.md Outdated
Show resolved Hide resolved designs/2019-config-simplification/README.md Outdated
@gyandeeps
Copy link
Member

gyandeeps left a comment

Gonna review more once all the red text is fixed.... it was little hard to read. 😸

Show resolved Hide resolved designs/2019-config-simplification/README.md Outdated
Show resolved Hide resolved designs/2019-config-simplification/README.md
@not-an-aardvark
Copy link
Member

not-an-aardvark left a comment

Thanks for working on this proposal. I think this proposal has some very good ideas, and I'm supportive of the direction overall, but I think it might be worth rethinking some aspects of it.

My main concern is that that this proposal would substantially increase the amount of ESLint-specific knowledge required for end users to perform some common tasks, such as extending a shareable config. In general, this proposal seems like it's taking a lot of the complexity of ESLint's shareable config resolution and throwing it on to the end user. This has some advantages (for example, configs are more expressive and composable with less "magic behavior" from ESLint), but I think it will also make it substantially more difficult for users to build configs. It seems like we agree that ESLint has some abstractions that are overcomplicated and cause problems, but in some cases I think this proposal is throwing the baby out with the bathwater by removing the abstractions completely.

This reminds me of Webpack config files. The Webpack config file format is very expressive and (as far as I'm aware) logically consistent, but in my experience it's also very difficult to understand what's going on in a Webpack config file without substantial background knowledge. I think it would be worth considering whether we could mitigate that type of effect through better API design.

The proposal does a lot of things, so I'll address them individually:

Removing extends

extends actually does two separate things at the moment: it loads another config from the filesystem, and it merges that config with the parent config. This proposal removes both functionalities, but I think it's important to distinguish between them, because they have very different tradeoffs.

  • Removing the behavior where ESLint loads another config from the filesystem is a good choice; in fact, it's I think it's the main benefit of this proposal. The advantage is that the user is providing a single config object to ESLint, which is assumed to contain all necessary information. This prevents ESLint from needing to search through the filesystem for the user's dependencies, and makes it much easier for users to debug or programmatically modify their configs.

  • I don't think it's a good idea to remove the functionality where third-party configs are merged with parent configs as a core part of the config format. According to a recent study, 67.2% of projects use a shareable config (including eslint:recommended); this is an extremely common use case. While it's theoretically possible for users to merge shareable configs on their own, doing so seems very error-prone. (Case in point: the example given in this RFC has a bug in that it mutates the global state of a shareable config module, potentially causing confusing problems with integrations.) As a result, it seems like most users would either need to add the @eslint/config package as an additional dependency (and understand how to use it), or implement their own buggy version. Even when using the @eslint/config package, configs would still be flattened before ESLint sees them, resulting in a suboptimal debugging experience.

    Additionally, it's not clear what advantage is gained by removing the core config-merging functionality. We could remove the "loading configs from the filesystem" behavior without removing the "merging configs" behavior, by having configs contain something like this instead:

    extends: [
        require('eslint-config-airbnb')
    ]

    This would effectively result in configs having a tree structure, rather than a flat structure. The tree structure would be flattened by ESLint core at runtime to produce a final resolved config. I think this would improve the experience for users, because it would allow for a much simpler way to carry out a common use case, while also allowing the original config tree to be used for debugging (e.g. we could add debugging functionality to pinpoint where a particular rule is being configured).

    (We would still need to handle the issue of ruledefs with the same name, but that's a problem regardless of whether people are using @eslint/config to merge configs.)

Removing envs

I'm fine with this. Envs were already redundant with shareable configs anyway.

Removing plugins

In effect, this proposal seems like it would completely remove the plugin API. ESLint core would no longer be touching plugin objects directly. Instead, users would load plugin modules on their own and extract the relevant components. Users might continue to create packages that export objects with rules and processors keys, but only by convention.

I have mixed feelings about this because it seems like it would make it difficult to add features to plugins in the future. With this change, there would be no way for us to add a new plugin API without explicit opt-in from end users.

For an example of a case where this would be a problem, suppose we hypothetically wanted to add a "prelint" hook where a plugin has access to all of the filepaths that will be linted (for the benefit of tools like typecheckers that operate on multiple files at once) and provides some services to rules as a result. End user opt-in seems unnecessary for this change because the new services would only be visible to rules, but if we implemented this RFC there would be no way for a plugin to opt into the feature without making the end user change their config.

One option would be to replace ruledefs with something like pluginDefs, where the values of the object would be plugins themselves (and envs/configs keys would be ignored). On the other hand, this would require processor to be a string again, which doesn't seem ideal.

Removing overrides

This RFC removes overrides in favor of supporting an array of configs with explicit files arguments. I don't have a strong objection to this, although in my experience, most overrides make very small modifications to a larger config. Repeating the same config for multiple globs seems tedious (even if the config is assigned to a variable, it still adds some boilerplate), and it's not clear to me what the value-add is.

Replacing .eslintignore

This is fine by me, although it might be confusing if ignore patterns are inherited from third-party configs.

Show resolved Hide resolved designs/2019-config-simplification/README.md Outdated
Show resolved Hide resolved designs/2019-config-simplification/README.md Outdated
Show resolved Hide resolved designs/2019-config-simplification/README.md Outdated
Show resolved Hide resolved designs/2019-config-simplification/README.md
Show resolved Hide resolved designs/2019-config-simplification/README.md
Show resolved Hide resolved designs/2019-config-simplification/README.md Outdated
Show resolved Hide resolved designs/2019-config-simplification/README.md Outdated
Show resolved Hide resolved designs/2019-config-simplification/README.md Outdated
Show resolved Hide resolved designs/2019-config-simplification/README.md Outdated
Show resolved Hide resolved designs/2019-config-simplification/README.md Outdated
Show resolved Hide resolved designs/2019-config-simplification/README.md Outdated
Show resolved Hide resolved designs/2019-config-simplification/README.md
Show resolved Hide resolved designs/2019-config-simplification/README.md Outdated
exports.config = {
ruledefs: {
react: reactPlugin.rules

This comment has been minimized.

Copy link
@ilyavolodin

ilyavolodin Jan 22, 2019

Member

One thing to consider here is how the users will be able to search the internet for specific error they receive if they use shareable config and config author decided to rename the plugin. I.e.:

const reactPlugin = require("eslint-plugin-react");

exports.config = {
  ruledefs: {
    "not-angular": reactPlugin.rules
  },
  rules: {
    "not-angular/jsx-uses-react": "error"
  }
}

As far as I understand, with configuration like that, error on the console will be reported as not-angular/jsx-uses-react which will make it hard to find which plugin error came from. Also, for overriding rules, you have to know what the author of shareable config decided to name plugins as well, which means reading shareable config source code.

This comment has been minimized.

Copy link
@nzakas

nzakas Jan 23, 2019

Author Member

Yes, there is no more canonical name for a rule in this proposal. I'm not entirely sure that matters, though. I anticipate a lot of users would just use the plugin name as their namespace. We could encourage plugin authors to export a namespace property so there is a canonical name to refer to:

const reactPlugin = require("eslint-plugin-react");

exports.config = {
  ruledefs: {
    [reactPlugin.namespace]: reactPlugin.rules
  },
  rules: {
    "not-angular/jsx-uses-react": "error"
  }
}

Would that make sense?

This comment has been minimized.

Copy link
@ilyavolodin

ilyavolodin Jan 24, 2019

Member

I guess we could try that. But it requires person who creates a config to not only know about this property being exposed by plugin, but also know about computed property name syntax.

This comment has been minimized.

Copy link
@ljharb

ljharb Jan 25, 2019

could maybe we just pass in reactPlugin, only, and let the schema of that object be a contract maintained between plugins and eslint (ie, not involving end users)?

This comment has been minimized.

Copy link
@nzakas

nzakas Jan 25, 2019

Author Member

@ilyavolodin I think that if we help them with tools like --init then this becomes clearer. Eventually everyone will understand computed properties, so I'm not too worried about that.

@ljharb what type of contract are you envisioning? In this proposal, the concept for a formal plugin really goes away as you can pass any object that contains rules into ruledefs, so requiring plugins to look a certain way could restrict what otherwise is a wide-open playing field.

This comment has been minimized.

Copy link
@ljharb

ljharb Jan 29, 2019

@nzakas You could start with "an object with a rules property", but then that allows for future backwards-compatible expansion, without requiring any config changes for users (only core changes, and then corresponding plugin changes).

This comment has been minimized.

Copy link
@nzakas

nzakas Jan 30, 2019

Author Member

Please see the FAQ I added about this.

This comment has been minimized.

Copy link
@nzakas

nzakas Feb 1, 2019

Author Member

I'm adding a new section explaining how we can retain canonical rule names without making changes to this design.

Show resolved Hide resolved designs/2019-config-simplification/README.md Outdated
Show resolved Hide resolved designs/2019-config-simplification/README.md Outdated
Show resolved Hide resolved designs/2019-config-simplification/README.md

platinumazure and others added some commits Jan 23, 2019

Fix typo
Co-Authored-By: nzakas <nicholas@nczconsulting.com>
Fix typo
Co-Authored-By: nzakas <nicholas@nczconsulting.com>
Fix typo
Co-Authored-By: nzakas <nicholas@nczconsulting.com>
@nzakas

This comment has been minimized.

Copy link
Member Author

nzakas commented Jan 23, 2019

@not-an-aardvark Thanks for the thoughtful feedback. Here are some responses to your questions and concerns:

My main concern is that that this proposal would substantially increase the amount of ESLint-specific knowledge required for end users to perform some common tasks, such as extending a shareable config. In general, this proposal seems like it's taking a lot of the complexity of ESLint's shareable config resolution and throwing it on to the end user. This has some advantages (for example, configs are more expressive and composable with less "magic behavior" from ESLint), but I think it will also make it substantially more difficult for users to build configs. It seems like we agree that ESLint has some abstractions that are overcomplicated and cause problems, but in some cases I think this proposal is throwing the baby out with the bathwater by removing the abstractions completely.

My thoughts around this fall into a couple of categories:

  1. Config files are already quite complicated. Trying to figure out how configs are merged with extends, overrides, and cascading is difficult for people to understand. This is why we ended up creating the --print-config method, to give people a better idea of the magic they don't see.
  2. I think by removing all abstractions we can make a better decision about which ones are helpful and necessary and which aren't. Undoubtedly some things in this initial proposal won't work, but I didn't think we could keep most things and still make progress towards solving the problems we have.
  3. People don't modify config files frequently, so the pain of setting one up is front-loaded. We can mitigate that with --init as well.

This reminds me of Webpack config files. The Webpack config file format is very expressive and (as far as I'm aware) logically consistent, but in my experience it's also very difficult to understand what's going on in a Webpack config file without substantial background knowledge. I think it would be worth considering whether we could mitigate that type of effect through better API design.

I totally agree. That's part of why I think the addition of a helper package that makes things more explicit is key to this proposal.

The proposal does a lot of things, so I'll address them individually:

Removing extends

I don't think it's a good idea to remove the functionality where third-party configs are merged with parent configs as a core part of the config format.

Additionally, it's not clear what advantage is gained by removing the core config-merging functionality. We could remove the "loading configs from the filesystem" behavior without removing the "merging configs" behavior, by having configs contain something like this instead:

extends: [
     require('eslint-config-airbnb')
 ]

That's an interesting idea. Let me look at this proposal again to make sure it fits (I think it does) and if so, would eliminate a lot of complexity. (Just running out of energy now.)

Removing plugins

In effect, this proposal seems like it would completely remove the plugin API. ESLint core would no longer be touching plugin objects directly. Instead, users would load plugin modules on their own and extract the relevant components. Users might continue to create packages that export objects with rules and processors keys, but only by convention.

Keep in mind that we don't actually have a plugin API. What we have is a plugin format. ESLint has always treated plugins as just a collection of named things, and that doesn't change with this proposal. People could continue creating plugins in the exact same way, they'd just pluck out the configs, processors, and rules as necessary.

For an example of a case where this would be a problem, suppose we hypothetically wanted to add a "prelint" hook where a plugin has access to all of the filepaths that will be linted (for the benefit of tools like typecheckers that operate on multiple files at once) and provides some services to rules as a result. End user opt-in seems unnecessary for this change because the new services would only be visible to rules, but if we implemented this RFC there would be no way for a plugin to opt into the feature without making the end user change their config.

Our plugins don't do anything automatically. That's been a design principle from the start and I wouldn't expect that to change. Plugins could expose whatever functionality they wanted, including hook information, and people could decide whether or not to use it in their config. I'm not a fan of magic plugins that exert their will on users without explicitly opting-in.

One option would be to replace ruledefs with something like pluginDefs, where the values of the object would be plugins themselves (and envs/configs keys would be ignored). On the other hand, this would require processor to be a string again, which doesn't seem ideal.

I actually just used plugins earlier, but then realized this definition was only helpful for rules because parsers, processors, etc., could be passed objects. Then I changed it to ruledefs. I think in the case of your hypothetical, we could do the same thing with hooks. (Though I'd prefer not to go down this hypothetical road any further so we can focus on the current problems we have.)

Removing overrides

This RFC removes overrides in favor of supporting an array of configs with explicit files arguments. I don't have a strong objection to this, although in my experience, most overrides make very small modifications to a larger config. Repeating the same config for multiple globs seems tedious (even if the config is assigned to a variable, it still adds some boilerplate), and it's not clear to me what the value-add is.

In this case, you can create a base config that has everything you want and merge that into override configs so you still only modify a small number of things.

Replacing .eslintignore

This is fine by me, although it might be confusing if ignore patterns are inherited from third-party configs.

Agreed. That's a bit of a problem and hopefully we can figure out some easy way to deal with this going forward.

@nzakas

This comment has been minimized.

Copy link
Member Author

nzakas commented Jan 23, 2019

Thanks for the awesome feedback everyone. I'm out of energy for today, but I'll take a look at incorporating more feedback tomorrow.

I think the overall negative feedback I heard was that extends needs to be easier. I think @not-an-aardvark's suggestion for continuing to support extends with objects instead of strings makes sense, and my priority will be to incorporate that into this proposal next.

And sorry for the poor formatting and all-red text. :(

@ilyavolodin

This comment has been minimized.

Copy link
Member

ilyavolodin commented Jan 23, 2019

I share @not-an-aardvark concerns about shifting complexities from us to end user, but for a different reasons, I think. There are so many people who are using ESLint, and, while some of them are developers with a lot of experience and understanding of the language, a large portion of the user base probably either just starting out with the language, or not up to date on the latest format. Merging configs manually is reasonably straight-forward if you know and understand ES6+ syntax changes like spread and rest, but if you've been stuck doing ES3/5 and don't know anything about spread, it's a lot of complex code that you have to write to merge multiple configs together.
One of the most common issues that we currently have is users not understanding the difference between global and local npm installation, which is reasonably trivial thing to figure out (at least, in my opinion it's a lot easier to understand that spread/rest operators in ES6+). So my concern is that a lot of users will not be able to create good enough configs for their projects.
And while @nzakas is correct, that the current configs are complicated as well - they are complicated for complex scenarios, for simple things like using shareable configs and changing couple of rules, they are actually pretty simple and straight-forward.
P.S. That's not to say that I don't like the proposal. I just think that the focus is to make more complicated configs simpler, at the expense of making simple configs more complicated.

Concerns have been addressed but more iteration is needed

@mysticatea

This comment has been minimized.

Copy link
Member

mysticatea commented Jan 23, 2019

@nzakas @not-an-aardvark how about my proposal in #9 (comment) ? It will simplify the config system with merging extends and overrides functionality. I think linear overriding is more understandable than combination of the two.

@nzakas

This comment has been minimized.

Copy link
Member Author

nzakas commented Jan 24, 2019

I've updated the proposal to add back extends, per @not-an-aardvark's feedback. I do think that makes this proposal a lot more appealing that the original and reduces a lot of complexity for end users.

nzakas added some commits Jan 24, 2019

@ljharb
Copy link

ljharb left a comment

Can you add an example (or point to it if I missed it) about where in the config we'd define --ext?

Show resolved Hide resolved designs/2019-config-simplification/README.md Outdated
exports.config = {
ruledefs: {
react: reactPlugin.rules

This comment has been minimized.

Copy link
@ljharb

ljharb Jan 25, 2019

could maybe we just pass in reactPlugin, only, and let the schema of that object be a contract maintained between plugins and eslint (ie, not involving end users)?

@nzakas

This comment has been minimized.

Copy link
Member Author

nzakas commented Jan 25, 2019

Pushed more updates:

  • Removed Config.create() (not necessary now that we have extends back)
  • Added example for replacing --ext
@platinumazure

This comment has been minimized.

Copy link
Member

platinumazure commented Feb 24, 2019

I think I'm on the fence again about this proposal. I'm not sure whether shareable config and plugin authors will benefit from the simplified dependency management as much as they would be hindered by potentially having to support two config formats.

@mysticatea

This comment has been minimized.

Copy link
Member

mysticatea commented Feb 26, 2019

I have written a formal RFC about evolving .eslintrc. Please check #13.

@nzakas

This comment has been minimized.

Copy link
Member Author

nzakas commented Mar 7, 2019

Thanks everyone for the thoughtful responses. To summarize so far, there are two remaining technical issues identified:

  1. Uncertainty over whether or not an error should be thrown for duplicate plugin definitions.
  2. The way we calculate caching lint results needs to change.
  3. From me: Upon more research, I think module.exports is a better choice than exports.config as it seems to be what most users expect, and there's no need to introduce friction at this level.

From a technical perspective, it seems to me that these are both problems with solutions that we could work through.


To address some of the other concerns:

I have an assumption; we don't remove .eslintrc for backward compatibility.
It means that there are two same functionalities in parallel for a long time.

This is a danger, yes, however I think we can decide how long it makes sense to do this. For instance, if we see a dramatic uptick in the number of new-style configs being used, we could always move up the timeline for removing the old-style config.

This is also why we would need to go "all-in" on the new format and tell people that we will be eventually removing the old-style config, to give them incentive to move forward. Labeling the new-style config as "experimental" would only lengthen the amount of time that we'd have two configs because there would be little incentive for most people to make the change.

as much as they would be hindered by potentially having to support two config formats.

Shareable config owners and plugin owners will consider supporting both config styles similarly. For example: module.exports = [require("eslint-plugin-foo/recommended")] and { "extends": ["plugin:foo/recommended"]. This increases maintenance cost for maintainers.

For a bit more context, plugins (for rules, processors, and environments) and parsers would not need to change in any way to be used in the new config format. So I'm assuming the primary concern here is about shareable configs and plugins exporting configs needing to support two different formats, one for legacy configs and one for new.

To me, this is fairly straightforward to solve by shipping a compat utility that can transform an old-style config into a new-style config (I believe @not-an-aardvark suggested this somewhere in this sea of comments). We can probably extract the current config functionality into a separate module so end users defining their eslint.config.js need to do something like this:

const configCompat = require("@eslint/config").compat;
const newStyleConfig = require("eslint-config-new");
const oldStyleConfig = require("eslint-config-old");

exports.config = [
    newStyleConfig,
    configCompat(oldStyleConfig)
];

Of course, this can be partially solved, too, by npm versioning the config/plugin so that it requires a minimum version of ESLint (this already happens whenever we add a new core rule that shareable configs want to include).

#7 has resolved the important pain for shareable config maintainers.

This still requires shareable configs to specify peer dependencies, though, which still means either forcing users to manually install or having shareable configs do a postinstall script. This runs counter to how most plugin systems operate, that's what I'd like to get to.

A part of configs/plugins may decide to support only eslint.config.js. Another part of configs/plugins may not follow the change for some reason. Users have to know the structures behind configs/plugins to use both together. This increases leaning cost for users.

Again, I think we are just talking about shareable configs here?

Users are going to get confused. They may use a config for eslint.config.js in .eslintrc. They may use a config for .eslintrc in eslint.config.js. This increases support cost for us and config/plugin maintainers.

This is why I think that switching the format dramatically (eliminating JSON and YAML, using an array) is a good idea. These are key differences that will make it more obvious what is expected. And of course, we can also validation on new config to see if they are using any old-config keys (like a string for parser) and print out a friendly error message to inform them (like we do/did with missing plugins for global installations).

Users might give up to use. We cannot remove the articles about .eslintrc from the Web. Especially, many non-English people prefer resources in their language rather than the official resource. At least, most of Japanese is so.

This is an important point that I'm glad you brought up, because I hadn't considered this. We do need to consider the impact to non-English speaking ESLint users. The good news is that because we now have funding through Open Collective, once we have documented what we need, we can hire a translator to produce translations into at least Chinese and Japanese (I think this is an excellent use of some of the money).

I have written a formal RFC about evolving .eslintrc. Please check #13.

I will, thanks for writing that up.

@ljharb

This comment has been minimized.

Copy link

ljharb commented Mar 7, 2019

@nzakas one way you could really accelerate adoption is if you backported this semver-minor change to eslint 3 and 4 - that way, there'd be no reason for basically any plugin to ever use the old format, since it's completely trivial to expect people to use the latest of their current major, but not as reasonable to force them to upgrade a full major.

@mysticatea

This comment has been minimized.

Copy link
Member

mysticatea commented Mar 8, 2019

Thank you for your reply.

I have an assumption; we don't remove .eslintrc for backward compatibility.
It means that there are two same functionalities in parallel for a long time.

This is a danger, yes, however I think we can decide how long it makes sense to do this. For instance, if we see a dramatic uptick in the number of new-style configs being used, we could always move up the timeline for removing the old-style config.

This is also why we would need to go "all-in" on the new format and tell people that we will be eventually removing the old-style config, to give them incentive to move forward. Labeling the new-style config as "experimental" would only lengthen the amount of time that we'd have two configs because there would be little incentive for most people to make the change.

Good to hear.
Introducing the new style config is fine only in assuming to remove the old style config.

as much as they would be hindered by potentially having to support two config formats.

Shareable config owners and plugin owners will consider supporting both config styles similarly. For example: module.exports = [require("eslint-plugin-foo/recommended")] and { "extends": ["plugin:foo/recommended"]. This increases maintenance cost for maintainers.

For a bit more context, plugins (for rules, processors, and environments) and parsers would not need to change in any way to be used in the new config format. So I'm assuming the primary concern here is about shareable configs and plugins exporting configs needing to support two different formats, one for legacy configs and one for new.

To me, this is fairly straightforward to solve by shipping a compat utility that can transform an old-style config into a new-style config (I believe @not-an-aardvark suggested this somewhere in this sea of comments). We can probably extract the current config functionality into a separate module so end users defining their eslint.config.js need to do something like this:

const configCompat = require("@eslint/config").compat;
const newStyleConfig = require("eslint-config-new");
const oldStyleConfig = require("eslint-config-old");

exports.config = [
    newStyleConfig,
    configCompat(oldStyleConfig)
];

Of course, this can be partially solved, too, by npm versioning the config/plugin so that it requires a minimum version of ESLint (this already happens whenever we add a new core rule that shareable configs want to include).

My main concern was about the recommended configs of plugins.
For example, eslint-plugin-vue will need the conbination of parser, processor, ruledefs, and rules to work well in the new config. They will need to provide two recommended configs for a while.

#7 has resolved the important pain for shareable config maintainers.

This still requires shareable configs to specify peer dependencies, though, which still means either forcing users to manually install or having shareable configs do a postinstall script. This runs counter to how most plugin systems operate, that's what I'd like to get to.

Yes, I was wrong. I have realized my misunderstood when #14 opened.

A part of configs/plugins may decide to support only eslint.config.js. Another part of configs/plugins may not follow the change for some reason. Users have to know the structures behind configs/plugins to use both together. This increases leaning cost for users.

Again, I think we are just talking about shareable configs here?

My main concern was about the recommended configs of plugins.

Users are going to get confused. They may use a config for eslint.config.js in .eslintrc. They may use a config for .eslintrc in eslint.config.js. This increases support cost for us and config/plugin maintainers.

This is why I think that switching the format dramatically (eliminating JSON and YAML, using an array) is a good idea. These are key differences that will make it more obvious what is expected. And of course, we can also validation on new config to see if they are using any old-config keys (like a string for parser) and print out a friendly error message to inform them (like we do/did with missing plugins for global installations).

We have .eslintrc.js file :)
Couldn't we grow the proposal on .eslintrc.js file with keeping backward compatibility?
Because #13 makes our codebase much simpler, this is not hard, and I think the burden to ecosystem is less.

Users might give up to use. We cannot remove the articles about .eslintrc from the Web. Especially, many non-English people prefer resources in their language rather than the official resource. At least, most of Japanese is so.

This is an important point that I'm glad you brought up, because I hadn't considered this. We do need to consider the impact to non-English speaking ESLint users. The good news is that because we now have funding through Open Collective, once we have documented what we need, we can hire a translator to produce translations into at least Chinese and Japanese (I think this is an excellent use of some of the money).

That's excellent.

I have written a formal RFC about evolving .eslintrc. Please check #13.

I will, thanks for writing that up.

Thank you.

@yacinehmito yacinehmito referenced this pull request Mar 9, 2019

Closed

Extend config #3146

@davidtheclark davidtheclark referenced this pull request Mar 10, 2019

Closed

Allow extends #1

@nzakas nzakas referenced this pull request Mar 11, 2019

Closed

TSC meeting 14-March-2019 #122

@nzakas

This comment has been minimized.

Copy link
Member Author

nzakas commented Mar 11, 2019

@ljharb that's a good point that I hadn't considered. It would take a bunch of work but it might be worth it to look into.

@mysticatea It sounds like your primary concern at this point is ensuring that existing shareable configs in plugins will continue to work. I think that's a fair concern and one I should look into.

Here's what I'd like to propose: how about I prototype how this proposal would work? That way, people could play with it, and we could try it out on a bunch of different projects with different shareable configs to see if it is possible to make them compatible without changes?

@nzakas

This comment has been minimized.

Copy link
Member Author

nzakas commented Mar 21, 2019

Quick update: I'm working on the prototype and anticipate I'll have it ready for people to try next week. It turns out that the toughest part isn't importing existing configs, it's merging configs inside of ConfigArray that is taking the most time (our existing merge() function is pretty specific to the current config format, so I'm needing to pull out the pieces I need and swap out others for simple configs).

In any event, I'll have another update soon!

@nzakas

This comment has been minimized.

Copy link
Member Author

nzakas commented Mar 25, 2019

Okay, so I have a prototype ready for people to try out! A few notes about this prototype:

  1. It is a prototype! That means the code is messy, unoptimized and a bit disorganized.
  2. The goal was to address the two outstanding technical questions about a) whether or not existing configs could be made to work and b) how merging of plugin namespaces should work. Both of these paths should be working in this prototype.
  3. I made a slight change in that ruledefs is now just defs (in anticipation of needing to define other things in the future) and it has a key called ruleNamespaces that acts like the old ruledefs. This has been updated in the RFC.
  4. You can create a eslint.config.js file in the project and root and it will be used instead of any .eslintrc files found in the project.
  5. Most of the relevant source code is in lib/simpleconfig.

Not Implemented

These things are not implemented in this prototype:

  • Turning off or warn on unsupported CLI options. Some CLI options just won't work when simple config is used.
  • .eslintignore is still honored
  • /*eslint-env*/ is still honored in code
  • The replacement for --ext (you still need to use --ext to get non .js files)
  • Function configs are not implemented.

Getting the Source

The source is found in the simpleconfig branch of this repo. You can view the eslint.config.js file that is equivalent to our current .eslintrc.js here. You can run npm run lint and it will use the eslint.config.js file to lint the project. I left in some linting errors for fun.

Note: The proposed @eslint/config utility is implemented in this prototype as lib/simpleconfig/eslintconfig.js for simpicity sake. The idea is still to have a separate utility.

Installing Into a Project

You can install locally using:

$ npm i eslint/eslint#simpleconfig

I've also taken the liberty of installing this into Vue to give it a test drive. Vue uses babel-eslint so it seemed like a good way to test this out. You can see my local repo here: https://github.com/nzakas/vue

Using Existing Plugins and Configs

In general, you can include an existing plugin like this:

module.exports = [
    importPlugin("example", __dirname)
];

You can include an existing shareable config (anything that would be in extends), like this:

module.exports = [
    importESLintRC("example", __dirname)
];

Take Some Time!

Feel free to take some time to play with the prototype and leave your feedback here. I think that this shows we wouldn't have a problem supporting the existing ecosystem. I'd especially like to hear from @mysticatea and @platinumazure as to whether this addresses the concern about using existing plugins and shareable configs.

@mysticatea

This comment has been minimized.

Copy link
Member

mysticatea commented Mar 26, 2019

Thank you for the prototype.

How do you think migration paths for configs/plugins owners?

In the perspective of a plugin maintainer, I'm confused because I could not find incentives to migrate my plugins to the new style (e.g. using defs).

  • Old style configs cannot use the new style's.
  • New style configs can use the old style's.
  • New style configs are fragile. For example, if two configs (e.g. your config and a shareable config, or two shareable configs) contain the same namespace such as react, it throws a fatal error.
  • New style configs are complex to users though it's simple to us.
@nzakas

This comment has been minimized.

Copy link
Member Author

nzakas commented Mar 26, 2019

How do you think migration paths for configs/plugins owners?

The migration path is laid out in the RFC: we introduce simple configs alongside the eslintrc so people can start migrating over, and then we eventually remove support for eslintrc.

In the perspective of a plugin maintainer, I'm confused because I could not find incentives to migrate my plugins to the new style (e.g. using defs).

There really isn't a required new style for plugins. We could allow the use of importPlugin() forever if we want. I'm less concerned about plugins than I am about shareable configs.

  • Old style configs cannot use the new style's.
  • New style configs can use the old style's.

I think that this is a benefit. By having people switch their projects to the simple config, they can continue to use whatever is already available in the ecosystem pretty much forever. We are actually providing a more flexible solution that has good backwards compatibility.

  • New style configs are fragile. For example, if two configs (e.g. your config and a shareable config, or two shareable configs) contain the same namespace such as react, it throws a fatal error.

It only throws an error right now because that's the feedback provided on this RFC. It could also not throw an error, or provide a warning, or handle it in any other way. For instance, this could be made smarter by only throwing an error if the namespace object is different, so if two configs do react: require("eslint-plugin-react").rules, that doesn't have to throw an error if they are both pulling from the same eslint-plugin-react because the rules object will be the same and can easily be compared.

Keep in mind that there is already an analogous problem today, where two shareable configs depend on different versions of the same plugin. There is really no way to solve this problem today, but there is with this RFC.

  • New style configs are complex to users though it's simple to us.

Can you explain why you think this is complex to users? To me, this is actually a much simpler way of configuring ESLint because it dramatically reduces the number of concepts people have to understand overridden (extends, overrides, cascades) and replaces them with a single way (one array, the last one wins).

@mysticatea

This comment has been minimized.

Copy link
Member

mysticatea commented Mar 27, 2019

I'm worried that the recommended configs of plugins are forgotten.

I think that the goal of this RFC is the removal of the current config system because this RFC started with "we cannot maintain the current config system." Therefore, I think the incentive for migration is the most important thing. The importESLintRC() is good parts for end users. However, it removes the incentive for migration for plugin owners. This RFC gives no features for the recommended presets of plugins, but migration breaks the existing configs. It may force end users to migrate configs, but it's not the incentive for plugins owners. Breaking the existing configs is a barrier to migration.

About "New style configs are fragile", this is a new matter in the new config system. People can write plugins key freely in their config files and shareable configs without any conflict concerns currently. On the other hand, people need to be worried about conflict errors to write defs key in the new config system. @not-an-aardvark summarized about this problem and an existing discussion in #14: #14 (comment). Of course, I know this is a trade-off. But I like localPlugins solution rather than throwing anyway. The new config system has less information about configs that ESLint can know than the current config system. For example, ESLint doesn't know where dependencies came from. So it may reduce what ESLint can do something to convenience for users.

About "New style configs are complex to users though it's simple to us", this may be wrong feeling that caused by interop utilities. But, at least, users have to know which plugins/configs are migrated or not and choose the way to import.

Honestly, I believe the start point of this RFC is a wrong assumption. We can solve existing problems on the current system with minor changes. Yes, we can solve the problems with the new config system as well. Therefore this is the matter of like or dislike of notations; I believe that it's not enough value for adding a drastic feature into the core with enforcing a load of migration to the ecosystem. I have felt a smell like Windows 8 (though I liked that).

@ExE-Boss

This comment has been minimized.

Copy link

ExE-Boss commented Mar 27, 2019

We could do what Babel 7 does and use both formats for different purposes, with eslint.config.js being used to configure file extensions, while .eslintrc would be kept effectively unchanged.

@nzakas

This comment has been minimized.

Copy link
Member Author

nzakas commented Mar 28, 2019

@mysticatea I'm confused by some of your comments because I'm not seeing the same things you're seeing.

This RFC gives no features for the recommended presets of plugins, but migration breaks the existing configs. It may force end users to migrate configs, but it's not the incentive for plugins owners. Breaking the existing configs is a barrier to migration.

I'm not sure I see how this proposal breaks any existing configs. In the example config I wrote in the prototype, there is an importESLintRC() function that works equally well for plugins. You can write importESLintRC("plugin:vue/recommended") and the config will work. I agree that breaking existing configs is a barrier for migration, and that's why I agreed to create this prototype to show that all existing configs will still work. (I will work to update the RFC with this info, too.)

Is there a specific config you're thinking of, or a specific project? I'd be happy to take a look and use the prototype to see if there are any issues.

The new config system has less information about configs that ESLint can know than the current config system. For example, ESLint doesn't know where dependencies came from. So it may reduce what ESLint can do something to convenience for users.

This is a feature of this proposal, not a bug. My assertion is that ESLint shouldn't know this information if it doesn't have to.

...users have to know which plugins/configs are migrated or not and choose the way to import.

This is true. However, this is a one-time cost that is solved quickly by either the plugin's documentation or by providing a good error message when an invalid key is found. This is the same as with knowing to use <script type=module> vs. just <script> to load a JavaScript file. If one doesn't work, you just use the other.

Honestly, I believe the start point of this RFC is a wrong assumption. We can solve existing problems on the current system with minor changes. Yes, we can solve the problems with the new config system as well.

It's okay, I can see that we disagree on the starting point of this RFC. I strongly believe that the eslintrc system is too complex and fragile right now, and the only way to solve some of the long-standing problems we have is start over from scratch on a clean slate. The problem with continuing to evolve eslintrc is that our only option is to add more complexity - more features, more concepts for users to understand, while still maintaining backwards compatibility. And even if we decide to start removing features, say, removing env, then we are stuck breaking a lot of configs immediately. Every time we try to remove something from eslintrc, we will end up causing more pain for all users, or else we end up supporting multiple different versions of eslintrc. I don't want to put our users through that.

And you know, we might never fully agree with each other on this, but the thing I want to be clear about is that there doesn't appear to be any technical reason that this proposal won't work. Backwards compatibility is still there for everything, and we can agree or disagree on how to deal with plugin namespace collisions, but all we really need to do is make a decision on the right approach. I think the prototype has shown that ESLint can run on this proposal and make use of the existing ecosystem.

Otherwise, you can say you just don't like this proposal, and I respect that.

@platinumazure
Copy link
Member

platinumazure left a comment

Left a note about one typo. Otherwise LGTM.

I've gone back and forth on this in my head and I'm back to being in favor of at least trying this (even if we decide to spend a while prototyping it). Although there are definite migration costs and new complexities for end users, the overall number of concepts end users must be aware of in configs is significantly reduced, and the entire design is very cohesive and extensible. I still need to play with the prototype at some point, though 😄

I assume that the decision to remove support for .eslintrc will not be taken lightly, will be discussed separately, and ideally won't happen for at least 2 major releases after we add support for the new format.

// include in config
exports.config = [
require("eslint-config-first");

This comment has been minimized.

Copy link
@platinumazure

platinumazure Mar 28, 2019

Member
Suggested change
require("eslint-config-first");
require("eslint-config-first"),
@mysticatea

This comment has been minimized.

Copy link
Member

mysticatea commented Mar 28, 2019

EDITED: 2019/04/02


I'm not sure I see how this proposal breaks any existing configs.

This is simple. If I added defs property to existing shareable config (including recommended configs) to define rules, ESLint throws a fatal error on the unknown property in configs. Or if I change parser property to an object from a module name, ESLint throws a fatal error.

This is a feature of this proposal, not a bug. My assertion is that ESLint shouldn't know this information if it doesn't have to.

...users have to know which plugins/configs are migrated or not and choose the way to import.

This is true. However, this is a one-time cost that is solved quickly by either the plugin's documentation or by providing a good error message when an invalid key is found.

I'm not sure if I can agree with this statement. How do you provide "a good error message"? The new config system introduces new conflict errors for rule's namespaces. It's a new concept to users so we should provide a good error message. However, ESLint doesn't know where the happened conflicts came from. The current config system could make a good message such as "The rule namespace 'foo' is conflicted between '.eslintrc.js' and '.eslintrc.js » eslint-config-xxx » plugin:yyy/recommended'." or something like. But the new config system cannot do it. This RFC has adding name property, but user will be confused if ESLint showed a package name the user doesn't use (indirect dependency).

I strongly believe that the eslintrc system is too complex and fragile right now, and the only way to solve some of the long-standing problems we have is start over from scratch on a clean slate.

I can't agree with this assumption. So the difference in our position looks to be on the essential point. It's a thing before the technical possibility; why we do it.

I know the fact that there are several long-living issues. However, various good ideas have been born in the discussion in this RFC. We can applicate those ideas to our codebase to fix the issues.

And you know, we might never fully agree with each other on this, but the thing I want to be clear about is that there doesn't appear to be any technical reason that this proposal won't work.

Even if the design isn't wrong technically, I should object it if I think it's going to a wrong goal based on an invalid assumption. I don't think reasonable if we force a load of migration even if there are ways which don't need the load. Indeed, the load may be one time for each person. But we can't delete existing articles on the Web, so possibly the ecosystem will be confused while a long time.

And even if we decide to start removing features, say, removing env, then we are stuck breaking a lot of configs immediately.

Even if we decided to remove env, it has less impact than removing eslintrc itself because many users are using shareable configs for personal or company. They may fix only a few shareable configs instead of config files on hundred projects.


I'd like to make common ground. My hope is more seamless interop. The interop utility functions don't appear on the user-land so users don't need to check shareable configs and choose the importing way. The defs property doesn't break existing user's configs so shareable config (including plugin configs) owners can migrate without breaking changes if they didn't change what the config does.


One additional technical concern:

  • We provide context.parserPath property to rules as a public API. How this RFC provides it?
@btmills

This comment has been minimized.

Copy link
Member

btmills commented Mar 30, 2019

I took some time to convert our linting setup at work to use @nzakas' simpleconfig branch. This is an overview of my experience and thoughts.

Situation

We have a client-side app using React, static typing with Flow, compiled by Babel, bundled by Webpack, and tested with Jest. All of our source code lives in src/. Type declarations for npm packages that we've installed with flow-typed live in flow-typed/npm/, and hand-written declarations go in flow-typed/. Babel, ESLint, and Webpack all have *.config.js files in the project root directory. Jest looks for tests in files matching src/**/*.test.js.

.
├── flow-typed
│   ├── npm
│   │   ├── pre-defined-typedef.js
│   │   └── ...
│   ├── custom-typedef.js
│   └── ...
├── node_modules
│   └── ...
├── src
│   ├── foo.js
│   ├── foo.test.js
│   └── ...
├── babel.config.js
├── eslint.config.js
├── package.json
└── webpack.config.js

My goal was to convert our existing linting setup with .eslintrc.js to the proposed eslint.config.js format. We use our own shareable configs (e.g. @company/eslint-config, @company/eslint-config/react), and I wanted to use those without modification to more closely mimic what migration might be like for our users.

As things are configured in .eslintrc.js today:

  1. All files that aren't ignored use eslint-plugin-prettier via plugin:prettier/recommended.
  2. .js scripts and config files in the project root directory are linted as ES2015 script code with the node environment.
  3. .js files in flow-typed/ and src/ are linted using our @company/eslint-config/react and @company/eslint-config/flow configs, which set babel-eslint as the parser for module code and include rules from eslint-plugin-react and eslint-plugin-flowtype. It also includes prettier/react and prettier/flowtype.
  4. .js files in flow-typed/npm/ are ignored by .eslintignore because they're written by third parties.
  5. Files ending in .test.js include the jest environment.

What went well

Switching from env to spreading globals was no big deal. In fact, I appreciated the unification of concepts.

I started off by linting just src/ without worrying about the declarations in flow-typed/ or scripts and config files in the root directory. Using the importESLintRC helper, I was able to include our existing shareable configs without modification. Setting parser: require('babel-eslint') felt like the natural way to do it. We also had a few settings in rules that were distinct from the shareable configs, and I was able to copy those directly.

Adding the jest globals just for *.test.js files was straightforward and showed me the potential power of this configuration model. I always had to look up the syntax for overrides, but this time it was intuitive.

Where I struggled

Once I tried to set up a separate config for scripts and config files in the project root directory ./*.js, things got more difficult. I didn't want the @company/eslint-cofig/react and @company/eslint-config/flow configs to apply to those. I tried creating a scope function to spread those into an object containing files: ['src/**/*.js'] to see if I could isolate them. I wanted to start with just eslint:recommended for the scripts and configs, but since that's limited to a string, I didn't see a way to apply that to those without also affecting files in src/.

I also ran into trouble trying to ignore flow-typed/npm/*.js. I had to add ignores: ['flow-typed/npm/*.js'] to every chunk that could possibly apply to it, and I didn't find a good way to ignore those files from eslint:recommended.

Summary

Composing a config for a source directory out of reusable chunks was a great experience. I didn't have to worry about the cascade or remembering how to do overrides. Once I tried to configure a whole project, where not everything was source code, I hit difficulties. This is a case where the cascade and root: true was serving me well to give logically isolated configs. Maybe I'm missing an obvious solution here?

I hope this gave some helpful insight, and let me know if there's anything I need to clarify!

@ExE-Boss

This comment has been minimized.

Copy link

ExE-Boss commented Mar 31, 2019

@btmills Well, you could always define configs in each subdirectory and then just require(…) them into the base config.


A config array is always flattened before being evaluated, so even though this example is a two-dimensional config array, it will be evaluated as if it were a one-dimensional config array.

When using a config array, only one config object must have a `files` key (config arrays where no objects contain `files` will result in an error). If a config in the config array does not contain `files` or `ignores`, then that config is merged into every config with a `files` pattern. For example:

This comment has been minimized.

Copy link
@ExE-Boss

ExE-Boss Mar 31, 2019

The current wording of this sentence contradicts the rest of the document.

Suggested change
When using a config array, only one config object must have a `files` key (config arrays where no objects contain `files` will result in an error). If a config in the config array does not contain `files` or `ignores`, then that config is merged into every config with a `files` pattern. For example:
When using a config array, at least one config object must have a `files` key (config arrays where no objects contain `files` will result in an error). If a config in the config array does not contain `files` or `ignores`, then that config is merged into every config with a `files` pattern. For example:
];
```

A config array is always flattened before being evaluated, so even though this example is a two-dimensional config array, it will be evaluated as if it were a one-dimensional config array.

This comment has been minimized.

Copy link
@ExE-Boss

ExE-Boss Mar 31, 2019

Out of curiosity, how deeply can config arrays be nested, or is there no hard limit?

@boneskull

This comment has been minimized.

Copy link

boneskull commented Apr 19, 2019

I'd just like to drop in and give my 👍 here. This RFC looks like a great solution to distributable config files. I have a tool similar to ESLint that I'm developing (with distributable configs, rules, & reporting), and have adopted the strategy outlined by this RFC.

When things come together, I'd love to look more closely at our respective implementations and see if we can create a generalized solution & publish a module.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.