Develop with Deno and Visual Studio Code

I have been contributing to Deno for a while now, and have started authoring slightly more complex applications on top of it.

For those who might not know about Deno, Ryan Dahl (the original author of Node.js) and Bert Bedler (a Node.js core contributor and co-founder of Strong Loop) started up Deno. Ryan gave a talk at JSConf EU highlighting the 10 things he felt he got wrong with Node.js. The decision to integrate the TypeScript compiler directly in the runtime made me very curious, and so I rolled up my sleeves and started to contribute. There has been a lot of progress, though it does feel slow at times, but it is becoming something.

My editor for choice for a while now has been Visual Studio Code. It was because of its strong support for TypeScript (being built with TypeScript as well) that initially drew me to it, but it rapid evolution around the experience has really made me more than comfortable. So having full support for Deno in the IDE was a major need for me, and likely a lack of an integrated experience would be a detractor for adoption.

Deno gives Visual Studio Code (and the TypeScript language services the power the intellisense) a couple problems. First, Deno has a different runtime type library than a browser (which is what comes out of the box with TypeScript in lib.dom.d.ts) and of course Node.js’ (which is available on npm as @types/node). The second problem is that Deno wants to be wholly explicit in the module identifier, which means modules IDs require an extension. With Deno preferring TypeScript as the authoring language, the vast majority of modules end in .ts, which for various reasons, TypeScript has chosen not to currently support (mostly because they never envisioned a runtime that supported TypeScript directly, and so a .ts extension on a file would be a developer mistake, because at runtime it would likely be .js). Third, Deno avoids any sort of external package manager or package meta data. So remote modules are automatically fetched and cached locally for you. This means, out of the box, Visual Studio Code has no idea where to look for those remote modules.

We are going to discuss how to integrate Deno into VSCode below…

Runtime types

The Deno binary knows the runtime type library it will evaluate code against. This type library is embedded and can be output to standard out via the --types flag. To have Visual Studio Code to access it (currently) you will need to place it somewhere where the language services can find it:

$ deno --types > lib/lib.deno_runtime.d.ts

You will also need a tsconfig.json to inform the language services in VSCode how find this. We are also going to take the opportunity to setup a few more defaults so that Visual Studio Code interprets things more inline with how Deno will interpret them. Currently Deno does not support changing the compiler options via a tsconfig.json but it hopefully will in the near term. Here is an example that can be placed in the root of your project and will generally work and mimics the settings internally in Deno:

{
"compilerOptions": {
"allowJs": true,
"checkJs": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "node",
"noEmit": true,
"noLib": true,
"pretty": true,
"resolveJsonModule": true,
"target": "esnext"
},
"include": [
"./**/*.ts"
]
}

By default now, Visual Studio Code will be able type check your modules against the Deno runtime library in the same way Deno will.

Allowing modules with .ts extensions

The TypeScript compiler and language services do not allow module specifiers to be specified ending in .ts. As mentioned above there are various reasons for this, but most of it relates to the TypeScript team not wanting to have to re-write module specifiers at compile time. It expects whatever is loading the modules to figure this out. Also, because TypeScript assumes that a runtime wouldn’t support TypeScript natively, that anything ending in a .ts wouldn’t actually end with that at runtime. So because it won’t rewrite the module specifier, it assumes that anything ending in .ts wouldn’t work at runtime.

Deno has taken a firm stance on being explicit with module specifiers. It does not support any sort of magical module resolution, the specifier is the specifier. If the resources ends in .ts then the module specifier needs to end in .ts. Because Deno provides the embedded TypeScript with the services required to resolve modules, the internal TypeScript never complains about these resources, it just assumes if Deno can resolve it, it is fine.

So these two concepts conflict with each other when using the TypeScript language services in an editor like Visual Studio Code. TypeScript will simply complain that any module ending with .ts is “wrong”, but Deno will not resolve the module unless you specify its extension. The good news is that TypeScript has supported language service plugins for a while now. So I wrote a plugin that replaces the module resolution logic for the language services in a similar fashion to the way Deno works. It is called deno_ls_plugin.

To get it to work for you in Visual Studio Code, you need to do a couple of things. First, you need to install the plugin in a place where it can be found by the TypeScript language services. Also the built in version of TypeScript in Visual Studio Code won’t be able to resolve the plugin, so you will need to install TypeScript and tell VSCode to use the local version. Third, you will need to tell VSCode to use the plugin.

Deno has no centralised package manager nor package meta data/configuration, but TypeScript resolves the plugin using Node.js, so while we don’t need it for Deno, we will need to use npm or yarn to install the plugin and TypeScript:

$ npm install typescript deno_ls_plugin

Or:

$ yarn add typescript deno_ls_plugin

It doesn’t matter to Deno (or anyone) if you have a package.json or not. By default npm won’t create one, but yarn will.

Next, you will have to tell Visual Studio Code to use the locally installed version of TypeScript:

Setting the version of TypeScript

Finally, you need to inform TypeScript to use the plugin. This is done by adding the plugins to the tsconfig.json file:

{
"compilerOptions": {
"allowJs": true,
"checkJs": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "node",
"noEmit": true,
"noLib": true,
"plugins": [ { "name": "deno_ls_plugin" } ],
"pretty": true,
"resolveJsonModule": true,
"target": "esnext"
},
"include": [
"./**/*.ts"
]
}

If you have Visual Studio Code running, you likely will have to restart for the changes to take effect, but now when editing in VSCode, you will be able to get intellisense on your imports and the editor will reflect how your code will be interpreted at runtime.

It is important to note that import * as foo from "./foo" will still be valid in Visual Studio Code, but Deno will throw an error. You will have to be explicit and do import * as foo from "./foo.ts".

Intellisense for remote modules

The final piece of the puzzle is getting intellisense for remote modules. TypeScript language services will not go off and fetch remote modules for you, while Deno supports import * as foo from "https://example.com/foo.ts" perfectly well. Again, the good news is that we can tell TypeScript language services to look somewhere on the local file system for resources. While it won’t go fetch them for you, after you run your application in deno and the remote modules are fetched, they will be cached locally, and you can point TypeScript at that cache. The way this is accomplished is by using the paths compiler option. Deno defaults the local cache to the user’s root path in the .deno directory. On Linux and MacOS systems, that is ~/.deno and on Windows (I believe) it is C:\Users\<username>\.deno. The challenge is that TypeScript can only resolve the paths relative to the baseUrl compiler option. You will need to adjust your tsconfig.json again, adding paths and baseUrl:

{
"compilerOptions": {
"allowJs": true,
"baseUrl": ".",
"checkJs": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "node",
"noEmit": true,
"noLib": true,
"paths": {
"http://*": ["../../.deno/deps/http/*"],
"https://*": ["../../.deno/deps/https/*"],
},
"plugins": [ { "name": "deno_ls_plugin" } ],
"pretty": true,
"resolveJsonModule": true,
"target": "esnext"
},
"include": [
"./**/*.ts"
]
}

You will likely need to restart Visual Studio Code again if you have it running.

Finally…

When it is all done, you should be able to get a full development experience in Visual Studio Code for Deno, taking advantage of all the intellisense of both the built in runtime for Deno, but also any remote modules you are including in your package.

I try to keep all my learnings and follow established conventions and patterns for Deno in an example project on GitHub called deno_example.