Back to Turborepo

Architecture

packages/turbo-vsc/ARCHITECTURE.md

2.9.97.2 KB
Original Source

Architecture

This document attempts to give a high level overview to the extension / LSP, which APIs it uses to achieve its feature set, and how it is structured.

Client

You're here! The client is the side that runs in VSCode. It is essentially an entry point into the LSP but there are a few other things it manages mostly for convenience sake.

  • basic syntax highlighting for the pipeline gradient
  • discovery and installation of global / local turbo
  • toolbar item to enable / disable the daemon
  • some editor commands
    • start daemon
    • stop daemon
    • restart daemon
    • run turbo command
    • run turbo lint

Otherwise it simply selects the correct LSP binary and runs it using vscode's built-in LSP library, and the LSP in turn interacts with the turbo daemon to get the information it needs to fulfil client requests.

Daemon

The daemon plays a minor role in relaying important metadata about the workspace itself back to the LSP. The general rule is the LSP can know about packages, turbo jsons, and how to parse them, but shouldn't need to do any inference, package manager work, etc etc. Any heavy lifting should be kept on the daemon.

Server - turborepo_lsp

This is the rust side. It imports some parts from the rest of the turbo codebase and utilizes the daemon to query data about the repository. When the LSP is initialized, the client sends a list of open workspaces and the LSP opens a connection to the (hopefully running) daemon, or starts one.

Note that we use the jsonc_parser crate rather than turbo's own TurboJSON parsing logic for maximum flexibility. we don't care if parts of it are malformed, as long as we can parse the parts we need to perform the client's request. See the tech debt section for more.

Language Server Protocol

Using the tower_lsp crate makes LSPs quite easy. It includes a server trait and all the message types required. We implement the LanguageServer trait and the library is responsible for all IO. All that remains is adding the relevant LSP handlers to support the features we need, which are broken down below, and hooking it up to the default communication mechanism: stdio.

To begin a session, the client sends an initialize request. The server must respond with the capabilities it supports, such that the client knows what type of questions it can ask. The capacilities we support are covered in the next sections.

LanguageServer::did_open - textDocument/didOpen

We need to respond to updates to the turbo.json file live as the user is writing in them. Watching the FS does not cut it, as we need to give context to the state of the buffer not the file. The LSP has support for this. By advertising to the client the 'text document sync' capability, the client knows that it can send us updates about file state. We send this during initialization and as a result the client pushes events (open and change) when turbo json files are loaded into the buffer.

We need to store these locally since later requests will only send the URI of the resource they are querying. It is up to the LSP to store the current state which we do through ropes.

All file changes (opening or changing) trigger handle_file_update, which is detailed in the next part.

LanguageServer::did_change - textDocument/didChange

Updates are treated the same. Any file changes require flushing new diagnostics which is done in handle_file_update. It is in charge of a few things:

  • fetch a fresh list of packages and workspaces from the daemon (cheap)
  • traverse the workspaces and parse the package name + scripts
  • parse the turbo json to ensure
    • all globs are valid (global and pipeline specific ones)
    • all pipeline key names refer to valid tasks
    • all dependOn fields are sound

These are reported back to the client along with their line and column ranges via the textDocument/publishDiagnostics LSP command and displayed on the document.

LanguageServer::completion - textDocument/completion

If an IDE requests completion information at a particular position in the document, this call kicks in. In VSCode, knowing that both the json LSP and turbo LSP apply, will fire off a request to each and resolve JsonSchema items, as well as turbo tasks. The logic for turbo is handled here. To resolve, we get all referenced scripts in any package in the workspace, as well as all valid <package>#<script> combinations, using the daemon package discovery to do this. The FIELD completion kind ensures that the task ids will only be recommended as keys.

Ordering / filtering is handled client side.

LanguageServer::code_lens - textDocument/codeLens

Code lenses are those handy little inline buttons you can find decorating tests or main functions. Clicking a code lens triggers some operation defined by the LSP. The only code lens that is useful to use is running turbo commands so we provide a nice way to do that. The LSP informs the client that upon clicking the lens it should invoke some command (turbo run) with some particular arguments (the task that was clicked on).

LanguageServer::code_actions - textDocument/codeActions

Code actions allow editors to surface automatic fixes for diagnostics. The client submits to the server the list of diagnostics it is interested in providing actions for (likely the ones on screen) and we can use the diagnostic code we issued earlier to identify an action, such as running a particular codemod to fix a deprecated:env-var error.

LanguageServer::references - textDocument/references

Finally, we support the references capability. References allow clients to request information about where a particular variable is used elsewhere in the code. Translated to turborepo, it is 'what scripts and packages does this pipeline entry refer to?'. This also helps with discoverability. General method is as follows:

  • parse the turbo json to find which pipeline item we requested data on
  • search through all the workspaces
    • parse the package jsons
    • find scripts that match the pipeline item
  • yield matching workspaces

Tech Debt Notes

  • we could consider moving the client side commands into the LSP to help with cross platform (editor) support.
  • rather than watch package.json buffers, we simply read them from disk. this means that references may be incorrect for json files that the daemon does not know about. we can mitigate this slightly but until the daemon supports the LSP there is not a great workaround so the LSP is 'as bad as' the daemon.
  • similar to above, we only cache file contents for turbo.json files and not any package.json files. This means we parse these fresh each time when a turbo.json changes, or when lenses and completions are requested. Parsing these is fast and keeps the LSP stateless, but we could consider a cache
  • we should probably factor out and re-use logic and types regarding tasks rather than parsing and using an AST. it was done this way since at the time of writing, parsing a TurboJson using the rust code made tracing fields back to line numbers rough and more importantly fallible. in the case of the LSP we want to be as fault tolerant as possible. this may have changed with the new error handling work
  • we can probably evict files from our rope store once they are closed