All articles
Engineering/2 minutes read

Building an LSP for your docs

October 23, 2025

RN

Ricardo Nunez

Engineering

Share this article


Building an LSP for your docs

I work at Mintlify, where we provide a platform that helps companies create product documentation that developers love. Part of doing our job well is keeping up with the latest and greatest industry trends and one of those is a new library called Twoslash.

It has recently exploded in popularity, going from 47k weekly downloads in January of this year to 300k as of this month, and it's easy to see why. It provides an IDE-like experience in documentation code examples by showing type information on hover. Hover over the code below to see it in action.

;

How Twoslash works

Twoslash creates an in-memory TypeScript project using @typescript/vfs, which provides the same language services that power your IDE. When you start a codeblock with ```lang twoslash, its Shiki plugin kicks in and adds type information above the ---cut--- marker into the virtual file system or VFS.

```ts twoslash
const greeting = 'Hello';
// ---cut---
greeting;
```

After collecting all type information into the VFS, the tool uses the typescript package to compile the code just like a normal Typescript project.

With the compiled result, it can go through each identifier in the abstract syntax tree to add type information around the resulting tags in HTML, which we can then target with CSS to create a popup on hover.

<pre class="twoslash">
  <code>
    <span class="line">
      <span style="color:#C678DD">const</span>
      <span style="color:#E06C75"> </span>
      <span class="twoslash-hover">
        greeting
        <div class="twoslash-popup-container mint-twoslash-popover">
          <div class="twoslash-popup-code">
            <code class="shiki">const greeting: "Hello"</code>
          </div>
        </div>
      </span>
      <span style="color:#56B6C2"> = </span>
      <span style="color:#98C379">"Hello"</span>
    </span>
  </code>
</pre>

This is all done at build time, so the end user doesn't have to include any extra JavaScript in the final bundle to get the hover experience.

But what about built-in types?

Twoslash explicitly includes types defined above ---cut--- into the VFS, but built-in types like Promise, Array, and AbortSignal are globally available, so we don't need to annotate them ourselves.

Most of the time, this works great since the happy path is to use Twoslash during your build step when your local environment has access to the TypeScript installation.

However, at Mintlify, we power all our tenants from a single NextJS app that has no content available at build time, so Twoslash compilation is done on-the-fly using Vercel serverless compute.

Locally, running next build and next start worked perfectly because our machines have the full TypeScript environment available, along with the necessary node_modules it needs to compile code. But when deployed to Vercel, their serverless functions only include the result of building your project (a small subset of the total files in your project) to optimize cold start times.

```ts twoslash
type StringArray = Array<string>;

const names: StringArray = ["Squilliam", "Squidward"];
```

Locally, Twoslash properly included the type information for Array<string> into @typescript/vfs, but when deployed to Vercel, we got the following error when the MDX was being compiled to React:

TS2304: Cannot find name 'Array'

The black box problem

What made this particularly challenging is that we discovered that next build behaves differently locally versus on Vercel. This is a problem when deployed, because Vercel only bundles the compiled output from the /dist directory in their serverless functions, effectively excluding all typescript files since they're useless at runtime since browser only cares about the outputted JavaScript.

On a lower level, we were specifically missing the *.d.ts files that ship with TypeScript, which include all of the built-in type definitions for JavaScript and Node.

There was no way to catch this as next build worked without errors locally, but the same code failed when deployed to Vercel.

We found ourselves in a cycle of deploy driven development. Without a way to reproduce Vercel's environment on our machines, we had to guess, deploy, and guess again when things didn't work.

To illustrate the difference, we deployed the same Next.js app to two different environments: Vercel and a VPS that replicated our local setup. The VPS deployment worked perfectly out of the box, while the Vercel deployment failed with missing type errors.

VPS Deployment - twoslash-vps.mintlify.com

Vercel Deployment - twoslash-vercel.mintlify.com

Going down the rabbit hole

We had a few theories we wanted to test, most of which were hacky solutions we thought might work.

Including an explicit import

First, we tried just importing all of TypeScript:

import * as ts from 'typescript';

ts;

This just bloated the bundle size without any type files actually being included.

Next, we tried to import directly from the type files hoping that because they were required by a file used in the final build, Vercel would include the types:

import 'typescript/lib/lib.dom.d.ts';
import 'typescript/lib/lib.esnext.d.ts';

Intuitively, this feels like if it fails, it's because the compiler would exclude the imports from the build output, because these files have no side effects and we're not using any of their exports.

Actually, it was far simpler. The build just errored:

Failed to load chunk server/chunks/ssr/node_modules_typescript_lib_lib_dom_d_ts_6ab6a62a._.js

So trying to import the types doesn't work in any way.

We were close to giving up and trying something really hacky (creating a huge string that just concatenated all of the TypeScript type files together), but we decided to focus on what we could do in Next.js to help us fix this problem.

Into the configuration mines of next.config.ts

While we were looking through the Next.js docs, we found an interesting setting.

The outputFileTracingIncludes config is a way to guarantee that a file gets included in the build output and therefore in the Vercel serverless function. Sounds like exactly what we were looking for!

After some experimentation and some help from the Vercel team, we found the following solution:

// next.config.ts

export default config = {
  outputFileTracingIncludes: {
    '/*': [
      path.relative(
        process.cwd(),
        path.resolve(require.resolve('typescript/package.json'), '..', 'lib', 'lib.*.d.ts')
      ),
      './node_modules/@types/node/**',
    ],
  },
};

We set it up such that for all routes ("/*"), any files that matched the lib.*.d.ts pattern from the typescript package and any files from the @types/node package were going to be included in the output.

We verified this by running it with a simple example from Claude's docs:

type  = (
  : string,
  : ,
  : {
    : AbortSignal;
    ?: [];
  }
) => <>;

And just like that, we were able to ensure that for built in types, Twoslash is able to properly provide type information!

You can use this feature today in your docs by simply adding twoslash to your code block meta line:

```ts twoslash
if (squid.name === 'Squidward') {
  playClarinet(squid);
}
```

Because Twoslash uses the TypeScript compiler to work, this feature is only available for TypeScript and JavaScript code blocks.

You can find the docs on this feature and other code block features here.