Skip to content

Commit

Permalink
add ability to have multiple tsc jobs running / watching for mono rep… (
Browse files Browse the repository at this point in the history
#46)

* add ability to have multiple tsc jobs running / watching for mono repo setups

Author:    Ben Feldberg Collins <[email protected]>

* Move already running warning to only trigger on manual run TSC

* add support for mono-repos of any size, put mono repo behavior behind a flag

* amend README

* amend README

* amend README

* fix find searching node_modules
  • Loading branch information
benfc1993 authored Apr 9, 2024
1 parent 02856f0 commit 2576637
Show file tree
Hide file tree
Showing 3 changed files with 169 additions and 47 deletions.
9 changes: 5 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,8 @@ To run TypeScript type-checking, execute the `:TSC` command in Neovim. The plugi

If `watch` mode is enabled, tsc.nvim will automatically run in the background every time you save in a typescript or tsx file and report the results back to you. In addition, if `auto_start_watch_mode` is enabled, the `:TSC` command will be executed on your behalf when you enter a typescript or tsx files.

To stop any running `:TSC` command, use the `:TSCStop` command in Neovim.

## Configuration

By default, the plugin uses the default `tsc` command with the `--noEmit` flag to avoid generating output files during type-checking. It also emulates the default tsc behavior of performing a backward search from the current directory for a `tsconfig` file. The flags option can accept both a string and a table. Here's the default configuration:
Expand All @@ -79,6 +81,7 @@ By default, the plugin uses the default `tsc` command with the `--noEmit` flag t
auto_focus_qflist = false,
auto_start_watch_mode = false,
use_trouble_qflist = false,
run_as_monorepo = false,
bin_path = utils.find_tsc_bin(),
enable_progress_notifications = true,
flags = {
Expand Down Expand Up @@ -137,13 +140,11 @@ end

### Why doesn't tsc.nvim typecheck my entire monorepo?

In a monorepo setup, tsc.nvim only typechecks the project associated with the nearest `tsconfig.json` by default. If you need to typecheck across all projects in the monorepo, you must change the flags configuration option in the setup function to include `--build`. The `--build` flag instructs TypeScript to typecheck all referenced projects, taking into account project references and incremental builds for better management of dependencies and build performance. Your adjusted setup function should look like this:
By default, tsc.nvim will check only the nearest `tsconfig` file. If you would like it to use all `tsconfig` files in the current working directory, set `run_as_monorepo = true`. All other options will work as usual such as `auto_start_watch_mode`, `flags.watch`, etc.

```lua
require('tsc').setup({
flags = {
build = true,
},
run_as_monorepo = true,
})
```

Expand Down
165 changes: 129 additions & 36 deletions lua/tsc/init.lua
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,20 @@ if success then
nvim_notify = pcall_result
end

--- @class Opts
--- @field auto_open_qflist boolean - (false) When true the quick fix list will automatically open when errors are found
--- @field auto_close_qflist boolean - (false) When true the quick fix list will automatically close when no errors are found
--- @field auto_focus_qflist boolean - (false) When true the quick fix list will automatically focus when errors are found
--- @field auto_start_watch_mode boolean - (false) When true the `tsc` process will be started in watch mode when a typescript buffer is opened
--- @field use_trouble_qflist boolean - (false) When true the quick fix list will be opened in Trouble if it is installed
--- @field run_as_monorepo boolean - (false) When true the `tsc` process will be started mode for each tsconfig in the current working directory
--- @field bin_path string - Path to the tsc binary if it is not in the projects node_modules or globally
--- @field enable_progress_notifications boolean - (true) When false progress notifications will not be shown
--- @field hide_progress_notifications_from_history boolean - (true) When true progress notifications will be hidden from history
--- @field spinner string[] - ({"", "", "", "", "", "", "", ""}) - The spinner characters to use
--- @field pretty_errors boolean - (true) When true errors will be formatted with `pretty`
--- @field flags { [string]: boolean }

local DEFAULT_CONFIG = {
auto_open_qflist = true,
auto_close_qflist = false,
Expand All @@ -17,11 +31,10 @@ local DEFAULT_CONFIG = {
use_trouble_qflist = false,
bin_path = utils.find_tsc_bin(),
enable_progress_notifications = true,
run_as_monorepo = false,
flags = {
noEmit = true,
project = function()
return utils.find_nearest_tsconfig()
end,
project = nil,
watch = false,
},
hide_progress_notifications_from_history = true,
Expand All @@ -34,8 +47,12 @@ local DEFAULT_NOTIFY_OPTIONS = {
hide_from_history = false,
}

local config = {}
local is_running = false
local config = {} ---@type Opts

--- Storage for each running tsc process
--- @type {[string]:{pid: number, errors: table }}
local running_processes = {}
local running_count = 0

local function get_notify_options(...)
local overrides = {}
Expand Down Expand Up @@ -78,20 +95,43 @@ M.run = function()
return
end

if is_running then
local configs_to_run = utils.find_tsconfigs(config.run_as_monorepo)

if #configs_to_run > 0 and not config.run_as_monorepo then
M.stop()
end

for i, k in pairs(configs_to_run) do
if running_processes[k] ~= nil then
configs_to_run[i] = nil
end
end

if #configs_to_run > 20 then
vim.notify_once("Too many tsconfigs found: " .. #configs_to_run, vim.log.levels.ERROR, get_notify_options())
return
end

if not config.flags.watch and #configs_to_run == 0 then
vim.notify(format_notification_msg("Type-checking already in progress"), vim.log.levels.WARN, get_notify_options())
return
end

is_running = true
running_count = #configs_to_run

local function notify()
if not is_running then
if running_count == 0 then
return
end

notify_record = vim.notify(
format_notification_msg("Type-checking your project, kick back and relax 🚀", spinner_idx),
format_notification_msg(
(
config.flags.watch and "👀 Watching your project for changes"
or "Type-checking your project" .. (running_count > 0 and "s" or "")
) .. ", kick back and relax 🚀",
spinner_idx
),
nil,
get_notify_options(
(notify_record and { replace = notify_record.id }),
Expand All @@ -110,23 +150,25 @@ M.run = function()
vim.defer_fn(notify, 125)
end

local function notify_watch_mode()
vim.notify("👀 Watching your project for changes, kick back and relax 🚀", nil, get_notify_options())
if config.enable_progress_notifications and not notify_called then
notify()
end

if config.enable_progress_notifications then
if config.flags.watch then
notify_watch_mode()
else
notify()
local function create_output()
running_count = running_count - 1
if running_count > 0 then
return
end
end

local function on_stdout(_, output)
local result = utils.parse_tsc_output(output, config)
running_count = 0
notify_called = false
errors = {}

errors = result.errors
files_with_errors = result.files
for _, process in pairs(running_processes) do
for _, error in ipairs(process.errors) do
table.insert(errors, error)
end
end

utils.set_qflist(errors, {
auto_open = config.auto_open_qflist,
Expand Down Expand Up @@ -158,54 +200,105 @@ M.run = function()
string.format("Type-checking complete. Found %s errors across %s files 💥", #errors, #files_with_errors)
),
vim.log.levels.ERROR,
get_notify_options()
get_notify_options((notify_record and { overwrite = notify_record.id }))
)
end

local function on_stdout(output, project)
local result = utils.parse_tsc_output(output, config)

running_processes[project].errors = result.errors

for _, v in ipairs(result.files) do
table.insert(files_with_errors, v)
end
end

local total_output = {}

local function watch_on_stdout(_, output)
local function watch_on_stdout(output, project)
for _, v in ipairs(output) do
table.insert(total_output, v)
end

for _, value in pairs(output) do
if string.find(value, "Watching for file changes") then
on_stdout(_, total_output)
on_stdout(total_output, project)
total_output = {}
create_output()
end
end
end

local on_exit = function()
is_running = false
if config.flags.watch then
return
end

create_output()
if running_count == 0 then
running_processes = {}
end
end

local opts = {
on_stdout = on_stdout,
on_exit = on_exit,
stdout_buffered = true,
}
local opts = function(project)
return {
on_stdout = function(_, output)
on_stdout(output, project)
end,
on_exit = function()
on_exit()
end,
stdout_buffered = true,
}
end

if config.flags.watch then
opts.stdout_buffered = false
opts.on_stdout = watch_on_stdout
for _, project in ipairs(configs_to_run) do
local project_opts = opts(project)

if config.flags.watch then
project_opts.stdout_buffered = false
project_opts.on_stdout = function(_, output)
watch_on_stdout(output, project)
end
end

vim.schedule(function()
running_processes[project] = {
pid = vim.fn.jobstart(
tsc .. " " .. utils.parse_flags(vim.tbl_extend("force", config.flags, { project = project })),
project_opts
),
errors = {},
}
end)
end
end

vim.fn.jobstart(tsc .. " " .. utils.parse_flags(config.flags), opts)
function M.is_running(project)
return running_processes[project] ~= nil
end

function M.is_running()
return is_running
M.stop = function()
for _, process in pairs(running_processes) do
vim.fn.jobstop(process.pid)
running_processes = {}
end
end

--- @param opts Opts
function M.setup(opts)
config = vim.tbl_deep_extend("force", config, DEFAULT_CONFIG, opts or {})

vim.api.nvim_create_user_command("TSC", function()
M.run()
end, { desc = "Run `tsc` asynchronously and load the results into a qflist", force = true })

vim.api.nvim_create_user_command("TSCStop", function()
M.stop()
vim.notify_once(format_notification_msg("TSC stopped"), nil, get_notify_options())
end, { desc = "stop running `tsc`", force = true })

vim.api.nvim_create_user_command("TSCOpen", function()
utils.open_qflist(config.use_trouble_qflist, config.auto_focus_qflist)
end, { desc = "Open the results in a qflist", force = true })
Expand Down
42 changes: 35 additions & 7 deletions lua/tsc/utils.lua
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,42 @@ M.find_tsc_bin = function()
return "tsc"
end

--- @param run_mono_repo boolean
--- @return table<string>
M.find_tsconfigs = function(run_mono_repo)
if not run_mono_repo then
return M.find_nearest_tsconfig()
end

local tsconfigs = {}

local found_configs = nil
if M.is_executable("rg") then
found_configs = vim.fn.system("rg -g '!node_modules' --files | rg 'tsconfig.*.json'")
else
found_configs = vim.fn.system('find . -not -path "*/node_modules/*" -name "tsconfig.*.json" -type f')
end

if found_configs == nil then
return {}
end

for s in found_configs:gmatch("[^\r\n]+") do
table.insert(tsconfigs, s)
end

assert(tsconfigs)
return tsconfigs
end

M.find_nearest_tsconfig = function()
local tsconfig = vim.fn.findfile("tsconfig.json", ".;")

if tsconfig ~= "" then
return tsconfig
return { tsconfig }
end

return nil
return {}
end

M.parse_flags = function(flags)
Expand Down Expand Up @@ -107,12 +135,12 @@ M.set_qflist = function(errors, opts)
M.open_qflist(final_opts.use_trouble, final_opts.auto_focus)
end

if #errors == 0 then
-- trouble needs to be refreshed when list is empty.
if final_opts.use_trouble and trouble ~= nil then
trouble.refresh()
end
-- trouble needs to be refreshed when list is empty.
if final_opts.use_trouble and trouble ~= nil then
trouble.refresh()
end

if #errors == 0 then
if final_opts.auto_close then
M.close_qflist(final_opts.use_trouble)
end
Expand Down

0 comments on commit 2576637

Please sign in to comment.