-
-
Notifications
You must be signed in to change notification settings - Fork 4.5k
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
Change Request: Make it easier to inherit flat configs from the repo root #18385
Comments
Thanks for writing this all up, it's very helpful. Some followup questions:
Anything else about how you're using ESLint would be helpful in thinking this through. |
Answering directly first:
We run eslint programmatically via
Individual file names, but this is an artifact of DT's old layout where the only source of config were Getting into more details: DT is sort of weird; we have a wrapper It's extra weird becuase So, no, we aren't actualy linting all in one go. However, I would like to be able to modify things such that That being said, downstream tooling that uses ESLint has to manually create new ESLint instances based on where configs are. For example, However, DT is itself very odd in that we can get away with never running |
I also forgot to mention again that one of DT's main features is "self service", where a given package can have owners that are allowed to approve PRs to their code, and authors/owners can merge their own code. Moving all config to the top level breaks that model, as any lint rule changes that need to be made will require DT maintainer (so, TS team) approval, as the repo root is considered "infrastructure" and cannot be touched by anyone. So, we need to have configs in the subdirs, somehow. |
I think that figuring out how to have configs in the subdirectories is our main goal here. 👍
How is this initiated? Are people already in the package directory and then run Some possible hypothetical approaches going through my head (none of which have been thoroughly thought through):
(Of course, both flags would need to be represented in the Do either of those seem like they'd solve your use case? re: VS Code - this continues to be a problem, primarily because we don't maintain that extension and therefore have very little control over how it does what it does. We can request changes, but ultimately have zero control. This is something we need to dig into separately on a more holistic level. |
At the moment, they run
Yes and no; right now, we always invoke ESLint ourselves via the API, we can already provide it a The first listed idea is no different than passing in a cwd. The second is what I was suggesting always be on in my original idea, but doesn't handle the more general problem of inheriting parent configs being a challenge due to paths being rooted in the wrong place. But, to be clear, I'm not trying to frame this issue in terms of DefinitelyTyped only, but as a more general "flat config is hard in monorepo" issue. The only saving grace for us is that we programmatically invoke ESLint and don't yet support running ESLint on its own, except in the editor, which now works at least in VS Code because it reimplemented the config file search algorithm to know when to invoke ESLint more than once. So, I still don't have a good idea, because in addition to the cwd thing, making flat configs work nicely in monorepos when inhereting a parent config is challenging, because:
Not sure how to distinguish those, and I apologize for the lack of clarity. It's just a bit hard to figure out what the "right thing" is, compared to the strictly hierarchical old config system.
I'm not sure I follow this; no matter who implements an editor integration, the fact that |
See, I'm not sure that's true. So far, aside from DT, when maintainers have contacted us about monorepo setup, they almost always end up finding a way to make it work within the confines of flat config. That's part of the benefit of flat config -- you can define your own loading mechanism right in the config file. That's why we were able to externalize the eslintrc loading mechanism in I can see determining where to start the search as an issue, but as you mentioned, you can always set However, I'm unclear on how many monorepos prefer to lint the entire repo, in which case one config file at the root makes a lot of sense, as opposed to monorepos where they prefer to lint just an individual package. That's the first question. The second question is, for those who prefer to lint inside of individual packages only, do they use the cascade to apply shared configs? And is there any reason that the shared configuration can't be moved to a file that is just imported into the package-level config file? That said, it would help to have a concrete example of how you're currently using cascade in DT. Can you point to a package and the config files it relies on? |
An example I can think of is those using The other case is people opening subdirs, where the behavior should match if you opened the entire repo.
I haven't worked in a repo that didn't use cascades. TS itself isn't a monorepo per se, but did use nested cascading configs, which I had to undo for consistent behavior in my WIP flat config PR: microsoft/TypeScript#57684 But nobody else is chiming in here about the problems they're having, so it's not super easy for me to made wider claims.
Sure, for DT:
There's a root config, then packages or subdirs within those packages override the settings, but within JSON (statically analyzable using our bot infra without executing code). I have not yet figured out what we're going to do on DT at all for flat config on the loading front; the fact that the code is exectuable is a security and infra processing gotcha for us (we can't really statically analyze a flat config...), so it's not super clear what to do. It may be the case that we give up on letting anyone write |
Thanks, that's helpful. I think there are two things the TSC needs to consider:
It seems like in order to work for your setup, ESLint would need to implement both items 1 and 2 in the core and also allow JSON config files, that is undoing a lot of the simplicity we got with flat config. So yeah, you may need to come up with a custom solution for that. @eslint/eslint-tsc would like some other opinions here. |
I was thinking about this over the weekend and had the following idea: what if config loading was pluggable? Here's a rough outline of what I'm thinking:
// eslint.project.js
export default {
async loadConfig(filePath) {
return [
// config objects
];
}
}; I think that this approach would solve both the problem of wanting to merge configs from ancestor directories and the problem of wanting static configuration files for overrides in specific directories. Anyone could create their own config loader to generate exactly the configuration they wanted in whatever way they wanted. As a bonus, we could implement this as non-breaking in v9.x. This would obviously require an RFC, so just looking for some initial thoughts on that to see if it's worth putting in the work on an RFC. |
I think that's a prety compelling idea, though I'm not 100% certain on having another config file (but I don't know how one could avoid that).
On my last team, we had a monorepo with another monorepo nested within it. The sub-repo was a public repo, while the top-level repo was a closed-source project which built on that public component. This sort of layout would mean that there would actually be multiple In non-flat config, this repo layout works pretty well as both monorepo roots can be marked as
I definitely agree that this could be used to load static configs from subdirs, though I think one can already achieve that today with some manual globbing. But building this sort of thing into ESLint has the benefit of preventing needless config loading for files that don't matter. And I assume ESLint would be aware of that path and apply globs, right? |
If we did this for flat config, without doing any of the subdirectory merging, would that work for you? (Meaning, we search for
I'm not quite sure what you're saying here. What "sort of thing" are you saying should be built into ESLint? |
That alone would be a net positive, I think, in that it preserves the "doesn't matter how you invoke ESLint, it behaves the same". Separately, we could come up with a better way to inherit a parent dir's configuration. Everything else can already be worked around by using helpers to rewrite globs or filter overrides (e.g. that other thread's helpers), it's just that the config file searching is not customizable in the current iteration. For DT, the challenge will continue to be that we don't actually want to let people write
Sorry, I have a problem with overusing the word "thing" as what I say makes sense in my head but maybe not once it leaves 😅 I was referring to your "config provider" idea. With flat config, one of the biggest footguns with loading "someone else's" config is the globs that they have chosen will be relative to where they were. For shared configs imported from an npm package or something, that's usually not a big deal because they seem to all write like But, if I'm actually wanting to pull in a child configuration to the root config, all of the paths are going to be relative to the wrong directory. So, "this sort of thing" meant the provider idea, and I was looking ahead to what that might mean inside ESLint for rewriting that config to be relative to the right place. But, this is somewhat moot if ESLint will just look at every file and choose the closest config. Unfortunately, the two ideas we're discussing are sort of the opposite, since once is saying "it's acceptable to have more than one |
Okay, so I think we have three things we need to consider adding into the core (no promises, just summarizing for the rest of the team):
So I think overall my opinion is that while eslintrc had a lot of problems, it also enabled a lot more control of the linting process, and as such, some of these are worth revisiting for flat config. @eslint/eslint-tsc I need some feedback here. |
Thanks; I think these are compelling changes.
If it's any consolation, if merging is reintroduced, I think that it's going to be much easier to implement and explain to users than If breakage is a concern, a nested config could also do something like: export default [
{ mergeParent: true },
// ...
] To explicitly opt into this. Which is to say, the opposite of
This is all DT needs; we just want to provide some way for package authors to disable lints. I still intend on trying out the "load all of the configs" idea, but I'm just continually bogged down... I'd also like to say that flat config will actually simplify some aspect of DT; right now we have tools repo that provides the infrastructure/tooling for DT, and that's who's defining the preset/plugin. But since we also use third-party plugins, we have to duplicitively install them into DT as well as mention them in the preset in the other repo, so they just always end up drifting. Also, we need to be able to call APIs |
@jakebailey It sounds like you could put a custom .json file in package subdirs to define specific rules to disable. ESLint will always ever load the top-level eslint.config.js file (if there are no other config files), and the top-level config would take care of loading the .json files from the subdirs and applying the rule definitions to the subpaths. I guess the drawback with this approach is that you'd need to load .json files from all subdirs even if you are only linting just one package? I'm asking because this looks like a much simpler problem than merging configs automatically or adding support for .json config files, despite the other advantages these solutions may have. |
@fasttime that was covered further up in this thread. |
Yeah, true. I was actually pointing out that the problem described in #18385 (comment) seems by far more specific than what the envisioned solution would cover in terms of use cases. I think that for DT it shouldn't even be necessary to map any glob patterns as these could be autogenerated by the config from package names. Current package-specific config files don't use any globs either. |
Yes, I was trying to keep my commentary more general to avoid overfitting for DT and potentially not cover / miscover other monorepo use cases. I've personally heard feedback from others that they're also having the sorts of issues I've described above in their monorepos (which are more complicated in configuration that DT by nature of not "just" wanting overrides). I've had a chance to do a little perf testing. In the current DT, I'm able to glob and parse all 2247 So, ignoring any other asks / nicities, I can right now just unblock myself by moving our config to another file ( I may have some time to prototype that in a couple weeks (just have some other more pressing tasks to deal with in the short term). |
This is basically how eslintrc works, so if eslintrc worked for this use case, this would work too. But this adds a lot of complexity that flat config design aimed to avoid as unnecessary because technically all configurations can be defined and maintained in a single top-level configuration file, so I'm wondering how common the need to have a top-level config file + config files in nested directories is. DT seems like a specific use case where there are thousands of packages but only a small number of users have permission to change the top-level config. If the solution with globbing and loading all configs @jakebailey suggested works for DT, maybe we don't need to change anything. |
I'm most interested in implementing option 1 as the others may be added any time without a breaking change. It seems like this might also resolve some issues we've seen with people trying to run ESLint on files outside of the cwd. I think @jakebailey's initial point that running ESLint on the same file should produce the same results regardless of where ESLint is run from is a fair one. |
Implementing option 1 sounds good to me. |
I also agree with implementing option 1 for a start. |
Just so I'm clear on what option 1 means, does this also imply multi-config loading such that linting two files with two different configs works, or is it still one config, just informed by a single file passed in? Presumably the former, otherwise it wouldn't be quite as useful, especially for linting a dir which has other configs within it. But it was also described as "easy" thuogh with per-dir config files (i.e. walking up a tree and remembering what configs go where) where I'd probably think of it as slightly harder 😅). |
At canva we have a monorepo that's structured something like this:
It's a weird structure, but it's what grew over time. I've been trying to figure out how we might be able to migrate this to flat configs (once VSCode ships support without a flag - cos trying to tell >1k engineers to turn on a flag in vscode is a non-starter - but that's a separate thing). The complicated thing for us is that backend engineers will often "scope" their workspace to their backend service folder and frontend engineers will often "scope" their workspace to This complicates flat configs for us because not everyone scopes their workspace - it's a pretty even split. So if we want to migrate to a top-level flat config file then we need to figure out how to solve the problem of supporting both usecases. It's not really feasible to tell >500 engineers "stop doing this pretty please" - it'll need to work both ways. Option 1 to always search from the file would work well because it would mean that we can essentially do an in-place migration for each Otherwise I don't know how to make it work... I would probably need to hoist it all to the root AND include a Either way - option 1 would be aces for our monorepo! |
Option 1:
ESLint would search up from every file being linted for a config file. We'd obviously cache config files as we find them so we don't need to keep reading from disk each time. So yes, two files with two different configs in their ancestry will cause ESLint to load two different configs and lint those files using the most appropriate one. FWIW, this would be a good first run for a feature flag to avoid creating a new major release just to let people start using this feature. |
Thanks, just confirming my understanding! |
I'm going to spend some time investigating this and put together an RFC. May take a while based on current workload, but just wanted folks to know I'm working on it. |
While investigating this, I came across a sticky issue: searching for files. Right now, we do this in In order to implement this change, the equivalent of I'll investigate whether there's a suitable replacement for |
RFC is up: eslint/rfcs#120 |
PR for the RFC: #18742 Haven't checked the details, but will it fulfill the requirements of definitely-typed? |
Theoretically yes, but given that it's considered a breaking change (so behind a flag until another major bump), I'm most likely to try and convert all of our configs to a custom JSON format and then just load all of them from the project root so we can at least move to v9. |
ESLint version
v8.57.0
What problem do you want to solve?
When using a flat config, the CLI only ever loads one config; this is resolved depending on where you invoke it. In a monorepo, you may want to have multiple
eslint.config.js
files; this is common for non-flat configs, where child.eslintrc.json
files merge with their parents.It's a design choice that flat configs no longer merge with parent directories; this is fine, and it seems reasonable that one could write a config like:
However, this doesn't acutally work!
eslint .
at the root of the monorepo, eslint only loads the root config. All other configs are ignored, so any of the overrides done by other configs are not reflected.eslint .
inside of the nested dir, now the other config is loaded, so it sometimes works, depending on the current working directory.scripts/**
in the root will also ignorescript/**
in the local dir, even if the intent was that only the top level dir was ignored. Or, maybe a rule applied to a completely different location and shouldn't be included at all.The only way to do this is to forego multiple configs entirely. The root
eslint.config.js
could include the overrides for everything, or invent a custom config loading mechanism, like (in psuedocode):But, now you're loading more configs than may be needed for a given file. This is especially a problem for DefinitelyTyped; the 9244 package monorepo currently has 2247
.eslintrc.json
files:Normally, this would make me quite worried because if you run eslint in parallel per package (like DT does), that could mean
9,244 * 2,247 = 2,0771,268
config loads. But, under flat config, each config is a module, so the runtime should theoretically be reusing that module, so the reality would likely be closer to just "load 2000 JSON files once" so long as the config is not invalidated.That doesn't solve the problem of "what does this config file look like", however; not everything can be represented in JSON anymore with flat config.
What do you think is the correct solution?
ESLint could look for the closest
eslint.config.js
file. This is similar to old configs, but explicitly without the merging semantics. This would make ESLint working directory invariant; no matter where you run things, you get the same result.I understand that there is some concern about the performance of this, but I don't actually think it will cost too much; you only need to walk a given dir once and remember which config applied to it. All in all, that's no more accesses to the disk than you already need to actually be able to list the files you're going to lint.
However, this does not fix the other flaw; inheriting another directory's config does not imply any modifications of file globs. If I spread in a parent dir's eslint config, none of the paths will line up, and I have no solution to that.
Another thing I thought of was to have/suggest a layout like:
This would also be cwd-invariant, but is pretty complicated, and also does not solve the problem of re-rooting paths from other configs / removing ones that don't apply to the subdir.
Participation
Additional comments
I have yet to do perf testing on the "load 2247 config files" method or the "base config" idea; I've been really busy and have not had time to work on DT related stuff like this.
The text was updated successfully, but these errors were encountered: