Node.js compatibility: Using npm packages in Deno 
Tags: node, deno, npm, typescript
Published: 2020-12-26 22:22:22

Ever since [Deno](http://deno.land) was released, developers have been busy writing hundreds of native TypeScript modules for it. At least to a certain extent, this effort is an attempt to recreate some of the rich diversity that **Node.js** and the **npm** ecosystem have to offer.

That's not only because Deno's philosophy is different from that of Node.js, but the two JavaScript runtimes are also technically incompatible. Although they both support modern JavaScript and can in principle run the same code, their module loading mechanisms and core APIs are different.

Node.js uses CommonJS modules and looks up installed packages in the `node_modules` directory with the "require" function[^1]. Deno on the other hand uses ES modules and absolute URLs to download code directly from the web, much like a browser. Reading or writing files and handling HTTP requests also work differently, making it virtually impossible to use an npm package in Deno.

Thankfully the Deno project has begun to address this limitation, opening up exciting possibilities for code reuse!

# Deno Node compatibility

The standard library module [deno.land/std/node](https://deno.land/std@0.82.0/node) offers two important features:

- An implementation of the **"require"** function to load CommonJS modules.
- **Polyfills** for the Node.js core APIs (still incomplete).

Here's how it works:

```ts
import { createRequire } from "https://deno.land/std@0.82.0/node/module.ts";

const require = createRequire(import.meta.url);

// Require a Node.js polyfill.
const { EventEmitter } = require("events");
// Require an npm module from `node_modules`.
const { constantCase } = require("change-case");
// Require a local CommonJS module.
const myFunction = require("./my-module");
```

To use the Node compatibility library, a few flags must be set:

```sh
deno run --unstable --allow-read --allow-env main.ts
```

That's it! One downside is that the results of the "require" function are typed as `any`. Unfortunately, the only way to get TypeScript types is to add them manually.

## Adding types

The solution is quite straightforward: import the types and cast the result of the "require" function. Note that we use [`import type`](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-8.html#type-only-imports-and-export) to prevent any confusion about what we're importing (there is no runtime code for Deno to load here).

```ts
import { createRequire } from "https://deno.land/std@0.82.0/node/module.ts";
import type ChangeCase from "./node_modules/camel-case/dist/index.d.ts";

const require = createRequire(import.meta.url);

const { constantCase } = require("change-case") as typeof ChangeCase;
```

This is not the whole story, however. The `.d.ts` type declaration files of 3rd party modules will typically make use of bare import specifiers, e.g.

```ts
import { Options } from "pascal-case";
```

Deno needs an [import map](https://deno.land/manual/linking_to_external_code/import_maps) to resolve such specifiers. In a file `import_map.json` we can simply declare all the imports and their respective type declaration files, for instance:

```json
{
  "imports": {
    "change-case": "./node_modules/camel-case/dist/index.d.ts",
    "pascal-case": "./node_modules/pascal-case/dist/index.d.ts"
  }
}
```

It's not actually necessary to do this for all the imports in the type declaration files of a 3rd party library, only the ones that expose types used in your own code. Undeclared bare specifiers will simply be typed as `any`.

When running Deno the import map file must be provided as a flag:

```sh
 deno run --import-map import_map.json \
  --unstable \
  --allow-read \
  --allow-env \
  main.ts
```

By the way, with the import map in place, we can also shorten the original type import to just `import type ChangeCase from "change-case"`.

You can check out a complete example in this repository:

{% github mfellner/deno-node-compatibility-example no-readme %}

## The future

So far, only a small subset of Node.js core API polyfills have been implemented in Deno and it is only possible to reuse npm packages with no or very few dependencies on Node.js itself. 

For instance, `http` and `https` don't exist yet so one could not use Express.js or any of the other popular web frameworks in Deno.

Given how much effort and polish has gone into many of the most popular Node.js libraries, it is safe to say that it would be a big win for Deno if it was possible to take advantage of this great collection of code.

[^1]: Node.js [can actually load ES modules](https://nodejs.org/api/esm.html) since version 13 but they're not widely used yet.