Neovim, Deno, and TypeScript in a monorepo
November 9, 2023
If you’ve ever tried to get both Deno and TypeScript LSP’s set up in Neovim you’ve likely run into configuration issues. Both Deno and TypeScript operate on .ts
files, so by default both LSP’s will try to attach when you open a TypeScript file.
The Deno docs recommend using the root file pattern to avoid conflicts.
local nvim_lsp = require('lspconfig')
nvim_lsp.denols.setup {
on_attach = on_attach,
root_dir = nvim_lsp.util.root_pattern("deno.json", "deno.jsonc"),
}
nvim_lsp.tsserver.setup {
on_attach = on_attach,
root_dir = nvim_lsp.util.root_pattern("package.json"),
single_file_support = false
}
This tells the Deno language server to only start if it finds a deno.json
or deno.jsonc
file at the root of your project. Similarly, the TypeScript language server will start if it finds a package.json
file. The single_file_support = false
part is needed to avoid the TypeScript LSP starting up in “single file mode” for any .ts
file.
This mostly works, but if you’re in a monorepo set up with something like pnpm, this won’t quite work because there is likely a package.json
file setup at your workspace root:
my-monorepo/
apps/
deno-api/
main.ts
deno.jsonc
typescript-frontend/
package.json
package.json
Opening main.ts
will still attach the TypeScript language server because it finds the package.json
at the workspace root. You’ll end up with both the TypeScript and Deno LSP’s attached.
The root_dir
field is a function takes the filename as a parameter and is expected to return either a string or nil
. If it returns nil
, then the LSP won’t attach to the current buffer. We can use this idea to make a specialized root_pattern_exclude
function that takes an exclusion value so that we only attach the TypeScript LSP if we don’t find a Deno root file. Here’s what that might look like:
---Specialized root pattern that allows for an exclusion
---@param opt { root: string[], exclude: string[] }
---@return fun(file_name: string): string | nil
local function root_pattern_exclude(opt)
local lsputil = require('lspconfig.util')
return function(fname)
local excluded_root = lsputil.root_pattern(opt.exclude)(fname)
local included_root = lsputil.root_pattern(opt.root)(fname)
if excluded_root then
return nil
else
return included_root
end
end
end
lspconfig.tsserver.setup({
on_attach = on_attach,
root_dir = root_pattern_exclude({
root = { "package.json" },
exclude = { "deno.json", "deno.jsonc" }
}),
single_file_support = false
})
lspconfig.denols.setup({
on_attach = on_attach,
root_dir = lspconfig.util.root_pattern("deno.json", "deno.jsonc", "deno.lock"),
init_options = {
lint = true,
suggest = {
imports = {
hosts = {
["https://deno.land"] = true,
},
},
},
},
})
Now, we can pull up a TypeScript file and, depending on which application we’re in within the monorepo, either the Deno LSP or the TypeScript LSP will start.