From 8ad2d8d8991d868ff8e7c21a7db624f32b882531 Mon Sep 17 00:00:00 2001 From: Alexis Tacnet Date: Sun, 17 Aug 2025 23:12:42 +0200 Subject: feat(ts/js): improve monorepo support for Typescript, ESLint #3955 PROBLEM: 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 the `vscode-eslint-language-server` now supports monorepos, **removing the need to spawn a different server for each package of a workspace**. Example: 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 different `root_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: Instead of only relying on the configuration files as `root_markers`, discover the root of the package / monorepo by finding the Lock files created by node package managers: * `package-lock.json`: Npm * `yarn.lock`: Yarn * `pnpm-lock.yaml`: Pnpm * `bun.lockb`: Bun We 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` or `javascript`). To do that, I replace the `root_markers` that were the configuration files by a `root_dir` function that superseds them. It will both: * look for a configuration file upward to check if the LSP needs to be attached * look for the root of the "project" via the lock files to specify the `root_dir` of the LSP PRIOR 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 better `root_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 --- lsp/ts_ls.lua | 36 +++++++++++++++++++++++++++++++++++- 1 file changed, 35 insertions(+), 1 deletion(-) (limited to 'lsp/ts_ls.lua') diff --git a/lsp/ts_ls.lua b/lsp/ts_ls.lua index e3cc99bc..4626adf0 100644 --- a/lsp/ts_ls.lua +++ b/lsp/ts_ls.lua @@ -32,6 +32,14 @@ --- Use the `:LspTypescriptSourceAction` command to see "whole file" ("source") code-actions such as: --- - organize imports --- - remove unused code +--- +--- ### Monorepo support +--- +--- `ts_ls` supports monorepos by default. It will automatically find the `tsconfig.json` or `jsconfig.json` corresponding to the package you are working on. +--- This works without the need of spawning multiple instances of `ts_ls`, saving memory. +--- +--- It is recommended to use the same version of TypeScript in all packages, and therefore have it available in your workspace root. The location of the TypeScript binary will be determined automatically, but only once. +--- return { init_options = { hostInfo = 'neovim' }, @@ -44,7 +52,33 @@ return { 'typescriptreact', 'typescript.tsx', }, - root_markers = { 'tsconfig.json', 'jsconfig.json', 'package.json', '.git' }, + root_dir = function(bufnr, on_dir) + -- The project root is where the LSP can be started from + -- As stated in the documentation above, this LSP supports monorepos and simple projects. + -- We select then from the project root, which is identified by the presence of a package + -- manager lock file. + local project_root_markers = { 'package-lock.json', 'yarn.lock', 'pnpm-lock.yaml', 'bun.lockb' } + local project_root = vim.fs.root(bufnr, project_root_markers) + if not project_root then + return nil + end + + -- We know that the buffer is using Typescript if it has a config file + -- in its directory tree. + local ts_config_files = { 'tsconfig.json', 'jsconfig.json' } + local is_buffer_using_typescript = vim.fs.find(ts_config_files, { + path = vim.api.nvim_buf_get_name(bufnr), + type = 'file', + limit = 1, + upward = true, + stop = vim.fs.dirname(project_root), + })[1] + if not is_buffer_using_typescript then + return nil + end + + on_dir(project_root) + end, handlers = { -- handle rename request for certain code actions like extracting functions / types ['_typescript.rename'] = function(_, result, ctx) -- cgit v1.2.3-70-g09d2