Skip to content

Enhancement: Default tsconfigRootDir to the running flat config's directory if available #10841

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

Open
4 tasks done
JoshuaKGoldberg opened this issue Feb 17, 2025 · 14 comments
Open
4 tasks done
Labels
enhancement New feature or request triage Waiting for team members to take a look

Comments

@JoshuaKGoldberg
Copy link
Member

JoshuaKGoldberg commented Feb 17, 2025

Before You File a Proposal Please Confirm You Have Done The Following...

Relevant Package

parser

My proposal is suitable for this project

  • I believe my proposal would be useful to the broader TypeScript community (meaning it is not a niche proposal).

Description

Re-filing #251 now that ESLint's flat config is stable: asking users to manually set tsconfigRootDir is a mild config pain:

  • It's an extra line of code plumbing to memorize a need for
  • Its default value is >99% of the time just the current file's directory...
  • ...which has some nuance in how to retrieve (CJS/ESM; import.meta.dirname Node.js support)

#251 was closed as noted in #251 (comment) because ESLint doesn't tell parsers the eslintrc path(s). But, now that we're on flat config, maybe there's a way to get the single flat config file / directory. That would be ideal IMO.

If we could infer tsconfigRootDir then https://typescript-eslint.io/getting-started/typed-linting would be simplified by a line:

 import eslint from '@eslint/js';
 import tseslint from 'typescript-eslint';
 
 export default tseslint.config(
   eslint.configs.recommended,
   tseslint.configs.recommendedTypeChecked,
   {
     languageOptions: {
       parserOptions: {
         projectService: true,
-        tsconfigRootDir: import.meta.dirname,
       },
     },
   },
 );

...and #10507 would be unblocked, which would allow simplifying that config in the next major version to the much cleaner:

 import eslint from '@eslint/js';
 import tseslint from 'typescript-eslint';
 
 export default tseslint.config(
   eslint.configs.recommended,
   tseslint.configs.recommendedTypeChecked,
-  {
-    languageOptions: {
-      parserOptions: {
-        projectService: true,
-      },
-    },
-  },
 );

✨.

Additional Info

Investigation needed:

  1. Is there an ESLint API / something provided to parsers that would help with this?
  2. If not, can we file an issue on ESLint core asking for it?

💖

@JoshuaKGoldberg JoshuaKGoldberg added enhancement New feature or request triage Waiting for team members to take a look labels Feb 17, 2025
@kirkwaiblinger
Copy link
Member

Curious how this would interact with experimental-config-lookup. Guess we'd default it to the resolved, inner config? Even if the inner config imports the root config or something?

@bradzacher
Copy link
Member

Copying my comment #10507 (comment)

With the advent of flat configs we could probably request that ESLint passes the config path as configPath: string | undefined to parsers. Each file has 0..1 flat config files now so that would be a way to do-away with tsconfigRootDir for 99.99% of cases.

@bradzacher
Copy link
Member

Curious how this would interact with experimental-config-lookup

The config lookup feature doesn't change the 0..1 relationship -- AIUI the feature just means like "If I run cd root && eslint it will use root/package/eslint.config.js" instead of "If I run cd root && eslint it will use root/eslint.config.js". Same arity to the file:config relationship -- just means there can be 0..n config files in a repo now (we don't care about that though).

@JoshuaKGoldberg
Copy link
Member Author

👍 filed eslint/eslint#19438. Marking this blocked on that.

@JoshuaKGoldberg JoshuaKGoldberg added blocked by external API Blocked by a tool we depend on exposing an API, such as TypeScript's Type Relationship API and removed triage Waiting for team members to take a look labels Feb 19, 2025
@JoshuaKGoldberg
Copy link
Member Author

JoshuaKGoldberg commented Apr 7, 2025

Welp, eslint/eslint#19438 was rejected. But I really want us to be able to do this for users - not having to explicitly provide a tsconfigRootDir unlocks a nice simplification of configs.

Proposal: how about as a breaking change in the next major version with behavioral changes, if the user has enabled project or projectService:

  • Have tsconfigRootDir default to the nearest eslint.config.* found in the call stack
  • Throw an error if that's not found

So, looking at the different use cases:

  • Users not using typed linting: no change; no new logic
  • Users on typed linting with a provided tsconfigRootDir: no change; no new logic
  • Users on typed linting without a provided tsconfigRootDir;
    • The common case of eslint.config.* being found: tsconfigRootDir is set to that ESLint config's
    • The uncommon case of the user using a custom config file: an error will now be thrown

What do we think @typescript-eslint/triage-team?

@JoshuaKGoldberg JoshuaKGoldberg added triage Waiting for team members to take a look and removed blocked by external API Blocked by a tool we depend on exposing an API, such as TypeScript's Type Relationship API labels Apr 7, 2025
@bradzacher
Copy link
Member

default to the nearest eslint.config.* found in the call stack

The ESLint config won't show up in the call stack -- it's ESLint itself that calls the parser, not the ESLint config. So we'd need to do disk traversal to find it.

@JoshuaKGoldberg
Copy link
Member Author

But if they're calling to tseslint.config< we could hijack things ...

@bradzacher
Copy link
Member

typescript-eslint - ~9.6m weekly DLs
@typescript-eslint/eslint-plugin - ~46.2m weekly DLs

Really depends on who you're targeting.
We also have considered deprecating our config function in favour of the new core function.

@JoshuaKGoldberg
Copy link
Member Author

I'm glad we haven't committed to anything or triaged #10935 yet then 😄

It seems to me that we and our users have two options for this area of configuring typed linting:

  • "Core" defineConfig: less work on tseslint's side, less conceptual difference for users, less migration work in onboarding
  • "Enhanced" tseslint.config: allows for implicit tsconfigRootDir and potentially other optimizations (feat: parsing session objects eslint/rfcs#102)

We would of course need to make both cases work out-of-the-box; using some tseslint.config thing should never be a prerequisite for us to work.

I'm in favor of going with an "enhanced" config, so that we can give users the features they need that are blocked on ESLint core.

@kirkwaiblinger
Copy link
Member

I'm firmly 👎 on adding functionality into the tseslint.config() function especially now that defineConfig() is around. In my mind it should be feature frozen ASAP and deprecated/removed once our minimum eslint support is eslint v9.


An alternative idea for supporting this in the config would be to have something like tsconfig.configs.enableProjectService(), which would require a file input (and throw if it didn't receive one!). So something like

 import eslint from '@eslint/js';
 import tseslint from 'typescript-eslint';
 
 export default tseslint.config(
   eslint.configs.recommended,
   tseslint.configs.recommendedTypeChecked,
   tseslint.configs.enableProjectService({ tsconfigRootDir: import.meta.dirname })
 );

@JoshuaKGoldberg
Copy link
Member Author

I'm firmly 👎 on adding functionality into the tseslint.config() function especially now that defineConfig() is around. In my mind it should be feature frozen ASAP and deprecated/removed once our minimum eslint support is eslint v9.

Hypothetically, if we had to choose between:

  • Being able to infer tsconfigRootDir without adding any arguments to the *.config(...) function
  • Deprecating the tseslint.config() function

...which would you choose? I think that's a false dichotomy and that we can reach a workaround, but just for the sake of discussion. 😄

something like tsconfig.configs.enableProjectService(), which would require a file input

I think the ideal solution would be to not add an explicit line about tsconfigRootDir to the ESLint config at all. Regardless of how it's surfaced, if we ask users to do that config line, we make it the thing more conceptually complicated. I think we'd be best off not letting users know about that concept unless it's actually relevant to them.

Spitballing some alternatives:

  • 🍏 Put a dynamic get()ter on tseslint.configs.recommendedTypeChecked & co. to populate some global-ish variable?
  • 🍌 Turn tseslint.configs.recommendedTypeChecked & co. into functions that can be called?
  • 🍒 Make tseslint.(configs.)enableProjectService() but without an explicitly required tsconfigRootDir?

I think 🍏 I normally would hate to add a dynamic getter and also hate to add yet another weird implicit global state shenanigan. But it's the only one that doesn't add work for users.

@kirkwaiblinger
Copy link
Member

I'm firmly 👎 on adding functionality into the tseslint.config() function especially now that defineConfig() is around. In my mind it should be feature frozen ASAP and deprecated/removed once our minimum eslint support is eslint v9.

Hypothetically, if we had to choose between:

  • Being able to infer tsconfigRootDir without adding any arguments to the *.config(...) function
  • Deprecating the tseslint.config() function

...which would you choose? I think that's a false dichotomy and that we can reach a workaround, but just for the sake of discussion. 😄

button sweat

something like tsconfig.configs.enableProjectService(), which would require a file input

I think the ideal solution would be to not add an explicit line about tsconfigRootDir to the ESLint config at all. Regardless of how it's surfaced, if we ask users to do that config line, we make it the thing more conceptually complicated. I think we'd be best off not letting users know about that concept unless it's actually relevant to them.

Spitballing some alternatives:

  • 🍏 Put a dynamic get()ter on tseslint.configs.recommendedTypeChecked & co. to populate some global-ish variable?
  • 🍌 Turn tseslint.configs.recommendedTypeChecked & co. into functions that can be called?
  • 🍒 Make tseslint.(configs.)enableProjectService() but without an explicitly required tsconfigRootDir?

I think 🍏 I normally would hate to add a dynamic getter and also hate to add yet another weird implicit global state shenanigan. But it's the only one that doesn't add work for users.

In all of these we're forced to choose between forcing users to pass import.meta.dirname explicitly, which is yucky yucky for the user, or guessing the config directory.

Technical ideas as to how we'd guess the config directory:

  • process.cwd() + look for eslint.config.(c|m)?(j|t)s in cwd - yucky to have to do, but probably works in most setups.
  • process.cwd() + process.argv() to try and catch -c and such (and if so error? we don't want to be in the business of reimplementing eslint's CLI) - yuckier, but may let us get more scenarios correct.
  • call stack analysis for current file - abhorrent but also probably works in most setups.

I think if we can reasonably implement a solution where we get process.cwd() there's an eslint config in that directory, that's probably a gross-but-"good enough" way to hack in the functionality for straightforward setups. Does that actually need to be done during the eslint config execution or can it be done when the project service initializes?

@JoshuaKGoldberg
Copy link
Member Author

JoshuaKGoldberg commented Apr 15, 2025

Does that actually need to be done during the eslint config execution or can it be done when the project service initializes?

In theory either. I think there are roughly three places we can look for information (since ESLint doesn't give us any), in descending order of how reliable they are:

  1. 🐉 new Error().stack from some code run within the ESLint config file
  2. 🥚 process.cwd() -> disk traversal
  3. 🐟 context.physicalFilename -> disk traversal

We can only trigger 🐉 from code actually run in the ESLint config (#10841 (comment)). But I love the being able to definitively see that some file named eslint.config.* is being used.

I think the problem with 🥚 and 🐟 disk traversal is edge cases that cause it to search up the directory chain:

  • If the user is running from a different directory (npx eslint repos/my-repo)
  • If the user's ESLint config is some custom name (npx eslint -c gotcha.js)

...then again, -c gotcha.js is also a problem with 🐉. 😩.

Anyway, my current tentative proposal is still 🐉 for the current major version.

@kirkwaiblinger
Copy link
Member

I think my order of preference is

  1. Be painfully explicit pending technical alternative from upstream.

    • Hard error when parsing with type information if tsconfigRootDir isn't provided in any way and we're parsing with projectService: true (and project: true?).
    • Add an explanatory error message with what info we want from the user
    • Optionally, add a tseslint.(configs.)?configureProjectService({ tsconfigRootDir: string }) API with required tsconfigRootDir.
  2. 🐉 - Use new Error().stack on tseslint.configs.* getters to guide inference.

    • is it possible to avoid global state and put the tsconfigRootDir: import.meta.dirname into the resulting config?
    • What about project: true scenarios?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request triage Waiting for team members to take a look
Projects
None yet
Development

No branches or pull requests

3 participants