eslint / eslint

Find and fix problems in your JavaScript code.
https://eslint.org
MIT License
24.39k stars 4.4k forks source link

Change Request: Make it easier to inherit flat configs from the repo root #18385

Open jakebailey opened 3 weeks ago

jakebailey commented 3 weeks ago

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:

import rootConfig from "../../../eslint.config.js";

export default [
    ...rootConfig,
    // Some more stuff
]

However, this doesn't acutally work!

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):

import { globSync } from "glob";

const allConfigs = globSync("**/eslint.config.json");

export default [
    // ...
    allConfigs.map((f) => {
        // Somehow read in the config, then remap all of its internal `files` globs to match a subdir?
    })
];

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:

$ find . -iname 'package.json' | wc -l
9244
$ find . -iname '.eslintrc.json' | wc -l
2247

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:

.
├── eslint.base.config.js - contains the actual shared config
├── eslint.config.js      - imports shared config, all child dir configs
└── packages/
    ├── foo/
    │   └── eslint.config.js  - imports only shared config
    └── bar/
        └── eslint.config.js  - imports only shared config

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.

nzakas commented 3 weeks ago

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.

jakebailey commented 3 weeks ago

Answering directly first:

  • How do you usually run ESLint inside the repo? Is it one npx eslint . that lints the entire repo? Or are you always linting inside of one package directory at a time?

We run eslint programmatically via new ESLint, or in the editor with vscode-eslint.

  • Are you passing individual filenames or directory names?

Individual file names, but this is an artifact of DT's old layout where the only source of config were tsconfig.json files.


Getting into more details:

DT is sort of weird; we have a wrapper dtslint which operates more-or-less per package, doing new ESLint and calling it via the API. Then a higher-level tool dtslint-runner runs all modified packages in parallel. A large number of our checks are in ESLint, but not all.

It's extra weird becuase dtslint only runs ESLint on a subset of the files (those that are reachable from a tsconfig.json), so some are unlinted, and some have separate overrides applied on top (as our lint rules can actually use more than one TS version at a time and at the moment only dtslint knows that info).

So, no, we aren't actualy linting all in one go. However, I would like to be able to modify things such that eslint . would "just work" without that preprocessing, moving that logic down. This would make the editor experience more consistent with running dtslint.

That being said, downstream tooling that uses ESLint has to manually create new ESLint instances based on where configs are. For example, vscode-eslint has to figure out which working directories would be needed, then spin up multiple instances of ESLint to handle that case: https://github.com/microsoft/vscode-eslint/issues/1644#issuecomment-2022836056


However, DT is itself very odd in that we can get away with never running npx eslint .; my impression from talking to others is that they have similar issues trying to configure flat ESLint in a monorepo that would benefit from some improvement here.

jakebailey commented 3 weeks ago

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.

nzakas commented 3 weeks ago

I think that figuring out how to have configs in the subdirectories is our main goal here. :+1:

DT is sort of weird; we have a wrapper dtslint which operates more-or-less per package, doing new ESLint and calling it via the API. Then a higher-level tool dtslint-runner runs all modified packages in parallel. A large number of our checks are in ESLint, but not all.

How is this initiated? Are people already in the package directory and then run npm run lint? Or are they running it from some other location?

Some possible hypothetical approaches going through my head (none of which have been thoroughly thought through):

  1. eslint --config-lookup-from=path/to/package - instead of looking up from the cwd, look up from this other directory regardless of where ESLint is executed.
  2. eslint --config-lookup-mode=file - instead of looking up from the cwd, look up from the directory of each linted file. This is what eslintrc did (and is what we were trying to avoid with flat config due to the amount of work this entails).

(Of course, both flags would need to be represented in the ESLint class as well, but I think of things in terms of the CLI.)

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.

jakebailey commented 3 weeks ago

How is this initiated? Are people already in the package directory and then run npm run lint? Or are they running it from some other location?

At the moment, they run pnpm test <their package name> from the repo root. Ideally, they could run pnpm test inside their package and that work, but the package.json monorepo layout is fairly new and we haven't improved that tooling. But this difference doesn't much matter because dtslint invokes ESLint via its API.

Do either of those seem like they'd solve your use case?

Yes and no; right now, we always invoke ESLint ourselves via the API, we can already provide it a cwd, and therefore it would "just work" already so long as we don't have any other eslint configs that aren't at the package root. This turns out to not be true, as there are .eslintrc.json files in nested dirs which would not behave properly with flat configs.

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.

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.

I'm not sure I follow this; no matter who implements an editor integration, the fact that eslint packages/foo/index.js and cd packages/foo; eslint index.js differ in behavior makes getting things right a challenge. Before flat config, this distinction didn't matter.

nzakas commented 3 weeks ago

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.

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 @eslint/eslintrc.

I can see determining where to start the search as an issue, but as you mentioned, you can always set cwd on the ESLint instance to handle that. As I've been exploring more, I can see how this would useful.

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?

jakebailey commented 3 weeks ago

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.

An example I can think of is those using lint-staged or similar to lint only changed files; those tools give path paths, so the easiest method to lint them would be to pass theminto the eslint CLI like eslint packages/foo/index.js packages/bar/index.js. But if they have different configs in each directory, you can't lint like that, you need to know that they have different configs and go into their specific directories and then lint them there.

The other case is people opening subdirs, where the behavior should match if you opened the entire repo.

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?

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: https://github.com/microsoft/TypeScript/pull/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.

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?

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 eslint.config.mjs files at all, and just invent or own config (not called .eslintrc.json) and force the repo root eslint.config.mjs file to load everything and eat the perf cost of doing so for packages that aren't referenced. Something like mergeESlintConfigs in https://github.com/eslint/eslint/discussions/18353#discussioncomment-9209819 but not loading real configs.

nzakas commented 2 weeks ago

Thanks, that's helpful. I think there are two things the TSC needs to consider:

  1. Should we always search for a config file from the files passed to ESLint? This would be a breaking change. We could ease into this by creating a CLI flag/ESLint constructor option that lets people opt-in to this behavior in v9 and then switch to make it the default in v10. I'm not opposed to doing a v10 soonish (later this year) to address this. I am currently in favor of this approach, as I think this helps with both the monorepo and IDE use cases. We will lose some of the perf gains we currently have by only searching for a config file once, but this seems like the right thing to do.
  2. Should we crawl up the directory structure merging config files like in eslintrc? If we implement item 1, then this might be best implemented by creating a standalone utility that people could use in their config files if they so choose rather than adding it back into the core. I don't think folks should pay the cost of always doing this search if they aren't going to use.

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 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.

nzakas commented 1 week ago

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:

  1. We define a eslint.project.js file that contains project-related configuration. The first such configuration would be a config loader. Each project has exactly one eslint.project.js file, which is found by searching up the directory structure from the first file ESLint finds to lint, either by what is passed on the command line or the open file in an editor. After that, there is no further searching for eslint.project.js.
  2. If there is no eslint.project.js, then ESLint behaves as it does today. It still looks for eslint.config.js in the cwd (in v9.x).
  3. ESLint uses the defined config loader from eslint.project.js to identify the correct config array for a given file. So for each file ESLint is linting, it will call the config loader to ask for the correct configuration.
// 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.

jakebailey commented 1 week ago

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).

  1. We define a eslint.project.js file that contains project-related configuration. The first such configuration would be a config loader. Each project has exactly one eslint.project.js file, which is found by searching up the directory structure from the first file ESLint finds to lint, either by what is passed on the command line or the open file in an editor. After that, there is no further searching for eslint.project.js.

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 eslint.project.js files to load the monorepo, so the behavior of eslint would then depend on which file happened to be processed first.

In non-flat config, this repo layout works pretty well as both monorepo roots can be marked as root: true, and ESLint would correctly load the config for any file no matter how it was run or on which file.

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.

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?

nzakas commented 1 week ago

In non-flat config, this repo layout works pretty well as both monorepo roots can be marked as root: true, and ESLint would correctly load the config for any file no matter how it was run or on which file.

If we did this for flat config, without doing any of the subdirectory merging, would that work for you? (Meaning, we search for eslint.config.js starting from each file being linted.)

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?

I'm not quite sure what you're saying here. What "sort of thing" are you saying should be built into ESLint?

jakebailey commented 1 week ago

If we did this for flat config, without doing any of the subdirectory merging, would that work for you? (Meaning, we search for eslint.config.js starting from each file being linted.)

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 eslint.config.js files themselves (for previously mentioned reasons), so I'm still hoping to find time in the near future to see how eslint performs when we do the 2000 JSON loads. (That's technically the area your "config provider" idea would work well for, given a root config could export a function that can return a config for any path and avoid up-front work.)

I'm not quite sure what you're saying here. What "sort of thing" are you saying should be built into ESLint?

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 **/*.{ts,mts,cts} to match without ever mentioning a root.

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 eslint.config.js in a repo, the tool with handle it", while the other says "there's a function/loader that the root config needs to provide if it wants to correctly handle any nested eslint.config.js files".

nzakas commented 1 week ago

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):

  1. Search for eslint.config.js from the file being linted. I'm fairly convinced that this is the correct move based on this discussion. While technically a breaking change, I think most non-monorepo users won't see any difference. I think this could be done relatively easily.
  2. Merge ancestor directory configs from the file being linted. This one hurts my heart a bit but I understand what you're saying. Merging and calculating relative directories for each config would be a pain for anyone who wants to do it, so it may make sense to add back into the core. This would be complicated because flat config's design was expressly to avoid doing this, so there will need to be some rethinking.
  3. Load JSON config files. We could potentially support a minimal JSON config file format specifically for overrides. This would depend on having the ancestor directory merge in place, so you'd still need an eslint.config.js file in the root but you could have eslint.config.json that could contain anything that can be defined using arrays/objects/strings/numbers/booleans. This necessarily means that static config files would not be able to specify additional plugins.

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.

jakebailey commented 1 week ago

Thanks; I think these are compelling changes.

Merge ancestor directory configs from the file being linted. This one hurts my heart a bit but I understand what you're saying.

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 .eslintrc was. For any nested config, it's just "my parent's config concatenated with my config" (plus or minus the whole path fixup).

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 root: true in the old config system!

We could potentially support a minimal JSON config file format specifically for overrides.

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 typescript-eslint (but the same one as in the linted repo), so we had to do strange things to try everywhere that things could live in order to access them. With flat config, I expect to be able to provide all linting configuration from our preset in dt-tools, and then drop all mentions of plugins from DT itself (no more package duplication).

fasttime commented 5 days ago

@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.

nzakas commented 5 days ago

@fasttime that was covered further up in this thread.

fasttime commented 4 days ago

@fasttime that was covered further up in this thread.

Yeah, true. I was actually pointing out that the problem described in https://github.com/eslint/eslint/issues/18385#issuecomment-2104813865 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.

jakebailey commented 4 days ago

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 .eslintrc.json files in about 600ms (at least on my pretty fast machine). Given ESLint imports its configs, it should be the case that this happens just once during the lifetime of a process, though I have not had the time to invest in porting our tooling in full to flat config.

So, ignoring any other asks / nicities, I can right now just unblock myself by moving our config to another file (eslint.overrides.json maybe) and unconditionally load all of them without too bad of a penalty. The config loading stuff has to live within DT (and not its shared config in dt-tools), but that's not so bad, I guess.

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).

mdjermanovic commented 2 days ago
  1. Search for eslint.config.js from the file being linted. I'm fairly convinced that this is the correct move based on this discussion. While technically a breaking change, I think most non-monorepo users won't see any difference. I think this could be done relatively easily.
  2. Merge ancestor directory configs from the file being linted. This one hurts my heart a bit but I understand what you're saying. Merging and calculating relative directories for each config would be a pain for anyone who wants to do it, so it may make sense to add back into the core. This would be complicated because flat config's design was expressly to avoid doing this, so there will need to be some rethinking.
  3. Load JSON config files. We could potentially support a minimal JSON config file format specifically for overrides. This would depend on having the ancestor directory merge in place, so you'd still need an eslint.config.js file in the root but you could have eslint.config.json that could contain anything that can be defined using arrays/objects/strings/numbers/booleans. This necessarily means that static config files would not be able to specify additional plugins.

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.

nzakas commented 2 days ago

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.

mdjermanovic commented 2 days ago

Implementing option 1 sounds good to me.

fasttime commented 2 days ago

I also agree with implementing option 1 for a start.

jakebailey commented 2 days ago

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 😅).

bradzacher commented 2 days ago

At canva we have a monorepo that's structured something like this:

canva/
├─ backend_service_a/
│  ├─ service_specific_js_tool/
│  │  ├─ .eslintrc.js
├─ backend_service_b/
│  ├─ service_specific_js_tool/
│  │  ├─ .eslintrc.js
├─ .../
├─ tools/
│  ├─ js_tool_a/
│  │  ├─ .eslintrc.js
│  ├─ js_tool_b/
│  │  ├─ .eslintrc.js
│  ├─ .../
├─ web/
│  ├─ .eslintrc.js

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 /web. By scope I mean they open that folder as the workspace root, rather than opening the monorepo root as the root.

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 .eslintrc.js and have it "just work".

Otherwise I don't know how to make it work... I would probably need to hoist it all to the root AND include a eslint.config.js per folder whilst translating all of the files/ignores paths based on where the nested config is in relation to the root? Oof I'm not sure.

Either way - option 1 would be aces for our monorepo!

nzakas commented 1 day ago

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?

@jakebailey

Option 1:

Search for eslint.config.js from the file being linted. I'm fairly convinced that this is the correct move based on this discussion. While technically a breaking change, I think most non-monorepo users won't see any difference. I think this could be done relatively easily.

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.

jakebailey commented 1 day ago

Thanks, just confirming my understanding!