Val Town makes it easy to ship TypeScript automations and applications to the internet via an integrated web editor experience. We strive to offer a magical tight feedback loop, with 100ms deploys on save.
That online editor experience should be great: we should support high-quality highlighting, autocompletion, information for when you hover over bits of code. But unfortunately it hasn't been so: our previous editor has been buggy and slow to give useful TypeScript feedback.
But now, we've rewritten our editor's TypeScript integration from scratch. It's available to all Val Town users, is fast and accurate, and the code is open source.
Our old system: running TypeScript in a Web Worker
Our previous language integration was entirely client-side. We ran a TypeScript Language Service Host in a Web Worker, to isolate it from the top frame's thread, and communicated between the Web Worker and top frame using Comlink.
The system looked like this:
And we bundled it into codemirror-ts, a CodeMirror extension, and Deno-ATA, an incomplete implementation of Deno's import resolution logic grafted onto TypeScript's capabilities.
This solution worked great in the simplest cases, but stumbled when importing certain NPM packages, and required more and more workarounds. The main two issues we were facing were these:
TypeScript isn't written for Deno. At Val Town, we run Deno, a modern JavaScript runtime that differs from standard TypeScript. Deno supports URL imports, provides server-side APIs through the Deno global (like environment variables), and introduces its own quirks. Sometimes we’ve been able to work around these differences. For example, we could use Deno type definitions. But in other cases, like handling URL imports, it requires us to interpret files differently. Deno is distinct enough that it ships its own language server, built in Rust and wrapping tsserver. NPM modules can be gigantic and installing dependencies is no joke. Huge import trees for NPM modules are nothing new, but at least when you're installing NPM modules locally, you have the brilliant minds of the package manager implementers to do module resolution: to install the minimal number of packages by comparing semver ranges. We didn't have that luxury, and often referencing an NPM module would trigger an avalanche of HTTP requests and bytes downloaded, which would overload the Web Worker and make the editor's language tools unresponsive.
Bringing DenoLS to Val Town
... continue reading