-
Notifications
You must be signed in to change notification settings - Fork 2.3k
feat: improve monorepo support for Typescript and ESLint LSP #3955
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
feat: improve monorepo support for Typescript and ESLint LSP #3955
Conversation
5895e6c
to
60b65ec
Compare
60b65ec
to
7982f83
Compare
Hello,
If this solution is desired, it probably makes sense to also follow this rule for other LSPs in JS ecosystem (e.g., vtsls) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This looks like a nice change, thanks.
before_init
is usually sus but I didn't study the whole config so can't say for sure.
Thanks! I'm gonna take your feedbacks and also apply this on the |
I mentioned |
The biome [documentation](https://biomejs.dev/guides/big-projects/#use-multiple-configuration-files) mentions that > When you use Biome’s features - either with the CLI or LSP - the tool looks for the nearest configuration file using the current working directory. > > If Biome doesn’t find the configuration file there, it starts traversing upwards the directories of the file system, until it finds one. I think it makes sense to follow this recommendation, so we can have proper monorepo support. Currently, in a monorepo, a new client is started for each `biome.json` (similar situation as #3910), which is unnecessary, since biome supports monorepos [natively](https://biomejs.dev/guides/big-projects/#monorepo) (as of v2). The alternatives I have considered aren't as robust. The first concern is searching for a `biome.json` to be set as the root. Unfortunately, that's cumbersome: the documentation recommends flagging the files that _aren't_ the root. That means we _can't_ use `root_markers_with_field` to search for a file that **is** the root. The proposed solution solves this issue by behaving as the CLI would. Secondly, we have to check if a project uses biome, via `package.json` and friends, which is the fallback behavior. I've considered using #3955's approach of lock files, but since it would require searching for the field 'biome' within lock files (which are often thousands of lines long), I was worried about causing a performance hiccup.
I agree that picking workplace root is a good call. I think we should still check if an eslintrc file or its variation is present before enabling the language server. The current implementation can potentially introduce an issue in a project where you use a particular linter/formatter. If in your config you have two linters/formatters enabled by locating a lock file, both of them both will get attached to a buffer and the user may see potentially conflicting diagnostics or formatting suggestions To clarify we already do this for this language server in on_init, just highlighting that if we decide to do this for others we should still check for the presence of their config files |
Ok, waiting on that then. And my other comments. @fuegoio do you plan to finish this |
This technique only works for LSPs that are supporting "monorepos", ie that are supporting multiple configurations of the library (like multiple For Svelte I added it as well, but I'm not 100% sure it works, could you try it? I'm not sure how it behaves if there are multiple "configurations" or multiple versions? I tried to add that to each documentation of LSP to warn users to not use multiple versions if it's not well supported. |
It's working fine, from what I could test. In both mono and regular repos (haven't tested different versions, though). However, I couldn't find any mentions in the documentation stating that the server has full support for monorepos. |
Thanks a lot for this comment, I totally agree that I forgot about this. To avoid this, I propose to go in the
I uniformized this technique in those LSPs with success:
This is needed in fact for every LSP that interacts on files that are not specific to this LSP, like |
Is the PR description still an accurate Problem/Solution explanation? |
Yes clearly! I just added a small part on the fact that we keep the configuration files as important In fact, from my first proposal, this is the only logic that changes, and it's clearly for the best as removing it was more a bug. |
Hello 👋
Monorepos (or "workspaces") in Typescript are more and more popular and the associated tooling is evolving to improve the developer experience in such setup. Especially, the
typescript-language-server
and thevscode-eslint-language-server
now supports monorepos, removing the need to spawn a different server for each package of a workspace.This was my main issue and why I iterated on this: with a few packages as the servers need to load every other package to work (the
typescript-language-server
, even if spawned multiple times with differentroot_dir
, will load in memory other packages to resolve the types), the amount of memory used grows exponentially.But in fact, those servers support monorepos: they support multiple configurations in subpackages and will load the correct one to process a buffer. The ESLint server even supports loading multiple ESLint binaries (and therefore versions), while keeping one instance of the server.
Solution
To enable this, instead of only relying on the configuration files as
root_markers
, we need to instead look at the root of the package (in case of a single project) / monorepo. To me, the best way to get this is to look at the Lock files created by node package managers:package-lock.json
: Npmyarn.lock
: Yarnpnpm-lock.yaml
: Pnpmbun.lockb
: BunWe still need to look at configuration files to enable the conditionnaly attachment of the LSP for a buffer (for ESLint, we want to attach the LSP only if there are ESLint configuration files) in case of LSP that operates on files that are "generic" (like
typescript
orjavascript
).To do that, I replace the
root_markers
that were the configuration files by aroot_dir
function that superseds them. It will both:root_dir
of the LSPPrior experimentations
I've tried to play with the
reuse_client
quite a lot, trying to understand if we need to spawn a new server or not looking at the Typescript / ESLint binary that was loaded, but in fact it's way easier to just have a betterroot_dir
that is the true root of the project for the LSP server: in case of those two servers, the root of the package / monorepo.I also tried to use the current directory opened as the
root_dir
, but it's less powerful on nvim compared to VSCode as we navigate more inside folders using terminal commands and then open vim.I think this method also removes the need from a project-local config (which could be quite useful anyway for ESLint flat config setting which auto-detection is a bit unreliable / compute heavy) as this should work normally accross all different setups.
Fixes #3910.