DEV Community

loading...
Cover image for Node.js compatibility: Using npm packages in Deno

Node.js compatibility: Using npm packages in Deno

mxfellner profile image Maximilian Fellner ・3 min read

Ever since Deno 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" function1. 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 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:

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");
Enter fullscreen mode Exit fullscreen mode

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

deno run --unstable --allow-read --allow-env main.ts
Enter fullscreen mode Exit fullscreen mode

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 to prevent any confusion about what we're importing (there is no runtime code for Deno to load here).

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;
Enter fullscreen mode Exit fullscreen mode

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.

import { Options } from "pascal-case";
Enter fullscreen mode Exit fullscreen mode

Deno needs an import map 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:

{
  "imports": {
    "change-case": "./node_modules/camel-case/dist/index.d.ts",
    "pascal-case": "./node_modules/pascal-case/dist/index.d.ts"
  }
}
Enter fullscreen mode Exit fullscreen mode

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:

 deno run --import-map import_map.json \
  --unstable \
  --allow-read \
  --allow-env \
  main.ts
Enter fullscreen mode Exit fullscreen mode

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:

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 since version 13 but they're not widely used yet. 

Discussion (0)

pic
Editor guide