Migrating Your NPM Packages to Deno

Understanding the Deno Module System

Deno uses a decentralized module system using the newer ECMAScript Modules (ESM) standard, which differs from the CommonJS system that NodeJS uses by default. Currently some packages on NPM use this syntax and some don't. However, many CommonJS modules can be wrapped into ESM syntax, so many NPM packages can still be used with Deno without having to make any changes to the packages by the users of Deno. But, some modules on NPM will still use Node APIs that are not available in Deno, so even if those packages use ESM syntax, they are unlikely to currently work with Deno.

To summarize, below is a general hierarchy to determine if an NPM package will be compatible with Deno:

Libraries that can be run in the browser and use ESM syntax will very likely be compatible with Deno. d3 is an example of one of these packages. Libraries that can be run in the browser and use CommonJS syntax will likely be compatible with Deno after wrapping the module in ESM syntax. voca.js is an example of one of these packages. Libraries that don't run in the browser but don't have dependencies on NodeJS APIs will likely be compatible with Deno after wrapping the module in ESM syntax. Libraries that rely on NodeJS APIs heavily likely will not be compatible with Deno. data-forge-fs is an example of one of these packages. However there is a compatibility layer being developed that may allow some packages that use NodeJS APIs to run using Deno.

End Goal of the Migration

Before starting the full tutorial, here is the gist of the desired result from the migration process:

Creating an importmap.json file with NPM packages and Deno standard library modules in it:

importmap.json { "imports": { "d3": "https://cdn.pika.dev/d3@5.15.0", "lodash": "https://dev.jspm.io/lodash@4.17.15", "moment": "https://dev.jspm.io/moment@2.24.0", "uuid": "https://deno.land/std/uuid/mod.ts" } }

Using those libraries in our deno code:

index.js import * as d3 from "d3"; import _ from "lodash"; import moment from "moment"; import * as uuid from "uuid"; console.log("-".repeat(80)); // Testing D3 console.log("Max Number in Array: %s", d3.max([1, 2, 3])); console.log("-".repeat(80)); // Testing Lodash console.log("Random Odd Number Between 1 and 7: %s", _.sample([1, 3, 5, 7])); console.log("-".repeat(80)); // Testing Moment console.log("Current Time (in seconds since epoch): %s", moment.now()); console.log("-".repeat(80)); // Testing Deno's uuid standard library console.log("UUID/GUID v4: %s", uuid.v4.generate()); console.log("-".repeat(80));

Running our program using the created importmap.json:

Command Line Shell deno run --importmap=importmap.json index.js



Using NPM ESM Modules

Lets try to fully migrate all of the Node modules I use to Deno. The following is a snippet of the dependencies from a package.json with all the NPM modules I commonly use in NodeJS:

package.json

"dependencies": { "body-parser": "^1.19.0", "bson": "^4.0.2", "buckets-js": "^1.98.2", "chalk": "^3.0.0", "cheerio": "^1.0.0-rc.3", "chroma-js": "^2.1.0", "claudia": "^5.12.0", "clipboardy": "^2.1.0", "cookie-session": "^1.3.3", "d3": "^5.15.0", "data-forge": "^1.7.7", "data-forge-fs": "0.0.7", "express": "^4.17.1", "express-session": "^1.17.0", "gnuplot": "^0.3.1", "helmet": "^3.21.2", "jsdom": "^15.2.1", "jstat": "^1.9.2", "knex": "^0.20.4", "lodash": "^4.17.15", "marked": "^0.8.0", "ml-regression": "^5.0.0", "moment": "^2.24.0", "morgan": "^1.9.1", "natural": "^0.6.3", "nexe": "^3.3.2", "nodemon": "^2.0.2", "numeral": "^2.0.6", "nunjucks": "^3.2.0", "objection": "^2.0.8", "papaparse": "^5.1.0", "pdf-parse": "^1.1.1", "request-promise": "^4.2.5", "sails": "^1.2.3", "sequelize": "^5.21.3", "sqlite3": "^4.1.1", "tempy": "^0.3.0", "tesseract.js": "^2.0.0-beta.2", "three": "^0.114.0", "uuid": "^3.4.0", "voca": "^1.4.0", "ws": "^7.2.1", "xlsx-populate": "^1.20.1" },

First create the following directory structure:

deno_example folder

|--deno_program |--index.js |--importmap.json |--modules.js |--test.js

index.js: This file will contain the program we want to make.

This file will contain the program we want to make. importmap.json: This is a JSON file that contains a mapping between the name of a module and the URL of the module. This makes it much easier to import modules, as you don't need to remember the full URL path when importing that same module over multiple programs. You can simply make a copy of importmap.json and use it in other programs.

This is a JSON file that contains a mapping between the name of a module and the URL of the module. This makes it much easier to import modules, as you don't need to remember the full URL path when importing that same module over multiple programs. You can simply make a copy of importmap.json and use it in other programs. modules.js: This is just a helper file we will use to test out URLs and the modules we import. It can be used to recreate our module cache.

This is just a helper file we will use to test out URLs and the modules we import. It can be used to recreate our module cache. test.js: This is a testing file that we will use to test some Deno code, and can be deleted later.

The contents of these files will be blank for now, except for importmap.json:

importmap.json

{ "imports": { } }

A map between module names and URLs will be added in the imports object of the import map.

Lets start going through the dependencies from my package.json and start trying to import libraries into Deno. The first place we should check is the Deno standard library. Deno provides a series of official modules that can be imported from their website, and many of these modules have very similar APIs to common NPM modules, with the added benefit that they are written in ESM syntax and in TypeScript, so you can use them for both JavaScript and TypeScript. Here are some modules that can be pulled directly from the standard library:

package.json

... "chalk": "^3.0.0", "uuid": "^3.4.0", "ws": "^7.2.1", ...

modules.js

import * as colors from "https://deno.land/std/fmt/colors.ts"; import * as uuid from "https://deno.land/std/uuid/mod.ts"; import * as ws from "https://deno.land/std/ws/mod.ts"

The chalk package in Node is a popular library that allows you to print in colors in the terminal, and the uuid module allows you to generate various UUIDs/GUIDs. Deno's standard library has 2 modules that have very similar APIs, colors and uuid, so those should be used instead. Additionally, the Deno standard library has a web socket module, ws, so we can use that instead of the NPM ws package.

Now run this command on the command line to download and cache the packages:

Command Line Shell

deno run modules.js

The output from the console should look like this:

Command Line Shell

Download https://deno.land/std/fmt/colors.ts Download https://deno.land/std/uuid/mod.ts Download https://deno.land/std/ws/mod.ts Compile https://deno.land/std/fmt/colors.ts Compile https://deno.land/std/ws/mod.ts Download https://deno.land/std/strings/mod.ts Download https://deno.land/std/util/has_own_property.ts ...

Deno will now fetch all the modules from the different URLs and cache them. The locations of the cache on various platforms are:

Windows: %LOCALAPPDATA%/deno

MacOS: $HOME/Library/Caches/deno

Linux: $HOME/.cache/deno

You can delete all these files in the cache manually if you want to re-download the packages. This can be useful when linking to URLs that return the latest packages.

Since the modules downloaded successfully, lets add them to the import map:

importmap.json

{ "imports": { "colors": "https://deno.land/std/fmt/colors.ts", "uuid": "https://deno.land/std/uuid/mod.ts", "ws": "https://deno.land/std/ws/mod.ts" } }

Next, lets try to convert packages that can be used in the browser as well as in Node. Here are some packages that work in both environments:

package.json

... "bson": "^4.0.2", "buckets-js": "^1.98.2", "chroma-js": "^2.1.0", "d3": "^5.14.2", "data-forge": "^1.7.7", "jstat": "^1.9.2", "lodash": "^4.17.15", "marked": "^0.8.0", "moment": "^2.24.0", "natural": "^0.6.3", "numeral": "^2.0.6", "nunjucks": "^3.2.0", "papaparse": "^5.1.0", "three": "^0.114.0", "voca": "^1.4.0", ...

To give a brief overview of these packages:

bson: used to converting data to Binary JSON format

used to converting data to Binary JSON format buckets-js: adds collections such as Stacks, Queues, and Bags

adds collections such as Stacks, Queues, and Bags chroma-js: for converting colors and creating color scales

for converting colors and creating color scales d3: data driven design tools for data driven document and SVG manipulation

data driven design tools for data driven document and SVG manipulation data-forge: Series and DataFrame library for easy data manipulation

Series and DataFrame library for easy data manipulation jstat: Basic statistics library

Basic statistics library lodash: Array manipulation library

Array manipulation library marked: Markdown to HTML converter

Markdown to HTML converter moment: Date manipulation library

Date manipulation library natural: Adds natural language processing (NLP) algorithms

Adds natural language processing (NLP) algorithms numeral: Number formatting library

Number formatting library nunjucks: A templating library similar to Python's Jinja2 library

A templating library similar to Python's Jinja2 library papaparse: A fast CSV parsing library

A fast CSV parsing library three: Primarily used in the browser, this library allows creating 3D web graphics using WebGL

Primarily used in the browser, this library allows creating 3D web graphics using WebGL voca: A basic string manipulation library

If these libraries are written using ESM syntax, they should by default work with Deno. How can we check if this is the case? We can use Pika CDN which is a 100% ESM CDN. You can type in the name of an NPM package on Pika:

Pika found the package on NPM, but notice how its highlighted in red and there is a warning, indicating that the package does not use ESM syntax, which is not too surprising given the package hasn't been updated in a few years. I will come back to this one later, but lets keep going down the list:

Same issue with chroma-js. Notice how chroma though has been last updated only a few months ago. ESM syntax is a newer standard, so library writers are still in the process of moving libraries over to the new syntax.

Lets keep going down the list until we can find an NPM package written in ESM syntax:

Notice no errors and warnings for the d3 package. If we click the link to the next page and click from npm to import:

Pika CDN gives us an import statement that we can use in Deno. Lets add it to our modules.js file, and change pkg to d3 in the import statement:

modules.js

import * as colors from "https://deno.land/std/fmt/colors.ts"; import * as uuid from "https://deno.land/std/uuid/mod.ts"; import * as ws from "https://deno.land/std/ws/mod.ts" import * as d3 from "https://cdn.pika.dev/d3@5.15.0";

A Note on Versioning:

To prevent breaking changes when downloading the modules again on different machines or when reloading your module cache, best practice would be to chose specific version numbers for your libraries and to keep track of change logs in these packages to determine when it is time to upgrade to a newer version. Node and the NPM ecosystem defaults to semantic versioning in the package.json file when downloading a new package, which can result in different libraries being downloaded on different machines despite the same package.json file.



As an example, the version for my D3 package:



^5.15.0



The ^ character tells NPM to find any version of D3 that has a Minor version number >= 15 and a Patch number >= 0. This type of versioning can cause problems. For example, the interpretation of Minor and Patch versions may differ among package developers, and some developers may implement breaking changes in a Minor or Patch update. Additionally, there is a chance that a malicious actor may hijack the account of a package developer, and release a Patch update that adds malicious code to the library, knowing that many users will erroneously download the new package due to semantic versioning. Finally, even if a package developer does not intentionally make any breaking changes, there is always the possibility of a bug being introduced that breaks your program given your use case, but that was difficult to detect in development as the developer may not have thought about your specific use case.



As a result, best practice is to chose a specific version, and keep track of change logs to determine when an upgrade should be undertaken (for example if the new version fixes a vulnerability).

Now run this deno command again:

Command Line Shell

deno modules.js

Notice how that due to the caching of prior downloaded modules, only the d3 module will be downloaded.

Now lets try using the downloaded module to ensure d3 works as expected with Deno. In the test.js file add the following code:

test.js

import * as d3 from 'https://cdn.pika.dev/d3@5.15.0'; console.log(d3.max([1, 2, 3]) === 3);

Then run this file using deno:

Command Line Shell

deno run test.js

The d3.max() function will return the max of the array, and so this code will print true to the console if the package works as expected.

To summarize the remaining modules, the following NPM packages were found on Pika CDN and use ESM syntax, so our modules.js file should now look like this:

modules.js

import * as colors from "https://deno.land/std/fmt/colors.ts"; import * as uuid from "https://deno.land/std/uuid/mod.ts"; import * as ws from "https://deno.land/std/ws/mod.ts" import * as d3 from 'https://cdn.pika.dev/d3@5.15.0'; import * as dataforge from 'https://cdn.pika.dev/data-forge@1.7.7'; import * as three from 'https://cdn.pika.dev/three@0.114.0';

Unfortunately, only a few of the packages I was searching for use ESM syntax. Additionally when I run the deno command again to download and cache them, one of them fails. After a bit of trial and error, the one that failed was data-forge. My recommendation is to download modules one at a time by adding a module and then running the deno modules.js for each new module added to ensure there isn't any errors when downloading modules. Fortunately, since modules are cached, once the cache is built you won't need to do this iterative process again.

For now lets remove data-forge and come back to that one later:

modules.js

import * as colors from "https://deno.land/std/fmt/colors.ts"; import * as uuid from "https://deno.land/std/uuid/mod.ts"; import * as ws from "https://deno.land/std/ws/mod.ts" import * as d3 from 'https://cdn.pika.dev/d3@5.15.0'; import * as three from 'https://cdn.pika.dev/three@0.114.0';

Wrapping Non-ESM Modules into ESM Modules

So how can we use the packages that not available from Pika CDN? We can use another service: jspm.io. With this website, you can type in the name of an NPM module, and the API of the site will automatically wrap the module into ESM syntax.

To use the site, add the name of an npm package to the end of this URL: "https://dev.jspm.io/". Let's try wrapping the lodash package (one of the packages not available on Pika CDN) into an ESM module. The Deno import statement used will be:

import _ from "https://dev.jspm.io/lodash@4.17.15";

Add it to modules.js:

modules.js

import * as colors from "https://deno.land/std/fmt/colors.ts"; import * as uuid from "https://deno.land/std/uuid/mod.ts"; import * as ws from "https://deno.land/std/ws/mod.ts" import * as d3 from 'https://cdn.pika.dev/d3@5.15.0'; import * as three from 'https://cdn.pika.dev/three@^0.114.0'; import _ from "https://dev.jspm.io/lodash@4.17.15";

Run deno again to cache the module:

Command Line Shell

deno run modules.js

And it ran successfully! Let's test that the module actually works in code by modifying test.js:

test.js

import _ from "https://dev.jspm.io/lodash@4.17.15"; console.log(_.sample([1,2,3]));

And after running deno again, you should see either the number 1, 2, or 3 in the console, as lodash pulls a random sample number from the array. Note though that the import statement imports lodash directly instead of importing everything from the module as lodash (this import uses import _ from "URL" instead of import * as _ from "URL")

And that's it! Now we have a package from NPM that does not use ESM syntax, but that has been wrapped into a module that does. Let's iteratively add and download the modules that weren't on Pika CDN to our cache using jspm.io. Our modules.js file should look like this now:

modules.js

// Deno Standard Library import * as colors from "https://deno.land/std/fmt/colors.ts"; import * as uuid from "https://deno.land/std/uuid/mod.ts"; import * as ws from "https://deno.land/std/ws/mod.ts" // Pika CDN import * as d3 from 'https://cdn.pika.dev/d3@5.15.0'; import * as three from 'https://cdn.pika.dev/three@0.114.0'; // jspm.io import aws from "https://dev.jspm.io/aws-sdk@2.625.0"; import bson from "https://dev.jspm.io/bson@4.0.2"; import bucket from "https://dev.jspm.io/buckets-js@1.98.2"; import cheerio from "https://dev.jspm.io/cheerio@1.0.0-rc.3"; import chroma from "https://dev.jspm.io/chroma-js@2.1.0"; import jsdom from "https://dev.jspm.io/jsdom@15.2.1"; import jstat from "https://dev.jspm.io/jstat@1.9.2"; import _ from "https://dev.jspm.io/lodash@4.17.15"; import ml from "https://dev.jspm.io/ml-regression@5.0.0"; import moment from "https://dev.jspm.io/moment@2.24.0"; import natural from "https://dev.jspm.io/natural@0.6.3"; import numeral from "https://dev.jspm.io/numeral@2.0.6"; import nunjucks from "https://dev.jspm.io/nunjucks@3.2.0"; import papaparse from "https://dev.jspm.io/papaparse@5.1.0"; import voca from "https://dev.jspm.io/voca@1.4.0"; import xlsx from "https://dev.jspm.io/xlsx-populate@1.20.1";

And our importmap.json should look like:

importmap.json

{ "imports": { "uuid": "https://deno.land/std/uuid/mod.ts", "colors": "https://deno.land/std/fmt/colors.ts", "ws": "https://deno.land/std/ws/mod.ts", "aws": "https://dev.jspm.io/aws-sdk@2.625.0", "bson": "https://dev.jspm.io/bson@4.0.2", "bucket": "https://dev.jspm.io/buckets-js@1.98.2", "cheerio": "https://dev.jspm.io/cheerio@1.0.0-rc.3", "chroma": "https://dev.jspm.io/chroma-js@2.1.0", "d3": "https://cdn.pika.dev/d3@5.15.0", "jsdom": "https://dev.jspm.io/jsdom@15.2.1", "jstat": "https://dev.jspm.io/jstat@1.9.2", "lodash": "https://dev.jspm.io/lodash@4.17.15", "ml": "https://dev.jspm.io/ml-regression@5.0.0", "moment": "https://dev.jspm.io/moment@2.24.0", "natural": "https://dev.jspm.io/natural@0.6.3", "numeral": "https://dev.jspm.io/numeral@2.0.6", "nunjucks": "https://dev.jspm.io/nunjucks@3.2.0", "papaparse": "https://dev.jspm.io/papaparse@5.1.0", "three": "https://cdn.pika.dev/three@0.114.0", "voca": "https://dev.jspm.io/voca@1.4.0", "xlsx": "https://dev.jspm.io/xlsx-populate@1.20.1" } }

Finally, lets test the functionality of the modules. Modifying test.js:

test.js

// Deno Standard Library import * as colors from "https://deno.land/std/fmt/colors.ts"; console.log(colors.yellow("hello") === "\u001b[33mhello\u001b[39m"); import * as uuid from "https://deno.land/std/uuid/mod.ts"; console.log(uuid.v4.validate(uuid.v4.generate())); import * as ws from "https://deno.land/std/ws/mod.ts" console.log(ws.createWebSocket instanceof Function); // Pika CDN import * as d3 from 'https://cdn.pika.dev/d3@5.15.0'; console.log(d3.max([1, 2, 3]) === 3); import * as three from 'https://cdn.pika.dev/three@0.114.0'; console.log(three.Audio instanceof Function); // jspm.io import aws from "https://dev.jspm.io/aws-sdk@2.625.0"; console.log(aws.VERSION.includes("2.")); import bson from "https://dev.jspm.io/bson@4.0.2"; console.log(typeof(bson.serialize("hello").toString()) === "string"); import bucket from "https://dev.jspm.io/buckets-js@1.98.2"; let bucketStack = new bucket.Stack(); bucketStack.push("hello"); console.log(bucketStack.pop() === "hello"); import cheerio from "https://dev.jspm.io/cheerio@1.0.0-rc.3"; let $ = cheerio.load("<html><body><p id='myid'>hello</p></body></html>"); console.log($("#myid").text() === "hello"); import chroma from "https://dev.jspm.io/chroma-js@2.1.0"; console.log(chroma('#D4F880').darken().hex() === "#a1c550"); import jsdom from "https://dev.jspm.io/jsdom@15.2.1"; let dom = new jsdom.JSDOM("<html><body><p id='myid'>hello</p></body></html>"); console.log(dom.window.document.getElementById("myid").innerHTML === "hello"); import jstat from "https://dev.jspm.io/jstat@1.9.2"; console.log(jstat.median([1, 2, 3]) === 2); import _ from "https://dev.jspm.io/lodash@4.17.15"; console.log(_.sample([2]) === 2); import ml from "https://dev.jspm.io/ml-regression@5.0.0"; let regressionInputs = [80, 60, 10, 20, 30]; let regressionOutputs = [20, 40, 30, 50, 60]; let regressionEquation = new ml.SLR(regressionInputs, regressionOutputs); console.log(regressionEquation.toString() === "f(x) = - 0.2647058823529412 * x + 50.58823529411765"); import moment from "https://dev.jspm.io/moment@2.24.0"; console.log(moment.now() <= moment.now()); import natural from "https://dev.jspm.io/natural@0.6.3"; console.log(natural.HammingDistance("cat", "car", false) === 1); import numeral from "https://dev.jspm.io/numeral@2.0.6"; console.log(numeral(1000).format("0,0") === "1,000"); import nunjucks from "https://dev.jspm.io/nunjucks@3.2.0"; // Note render uses a file path, so that wont work console.log(nunjucks.renderString("", {myvar: "hello"}) === "hello"); import papaparse from "https://dev.jspm.io/papaparse@5.1.0"; console.log(papaparse.parse("hello,world").data[0][0] === "hello"); import voca from "https://dev.jspm.io/voca@1.4.0"; console.log(voca.camelCase("hello world") === "helloWorld"); import xlsx from "https://dev.jspm.io/xlsx-populate@1.20.1"; console.log((new xlsx()).toString() === "[object Object]");

Depending on the packages and versions, results of the test will vary, but many of these tests should return true (as of this writing all these tests returned true on my machine).

Handling Modules that use Node APIs

While we were able to add and cache a lot of our modules from NPM and Node to Deno, there are still many modules missing that we will likely want to start using Deno frequently. Let's take a look at some modules built into Node.

File System Module

The Deno standard library contains an fs module, with very similar APIs to the Node fs module, but Deno also comes with the built-in Deno global object that has access to file system APIs. Here's an example of using some of the file system APIs in the Deno global:

test.js

/* * Asynchronous making directory, writing file, renaming file, reading file, * deleting file, and then deleting the directory */ // Making Directory await Deno.mkdir("tempdir"); // Writing File const encoder = new TextEncoder(); const data = encoder.encode("Hello world"); await Deno.writeFile("tempdir/hello.txt", data); // Renaming File await Deno.rename("tempdir/hello.txt", "tempdir/world.txt"); // Reading File const decoder = new TextDecoder("utf-8"); let text = decoder.decode(await Deno.readFile("tempdir/world.txt")); console.log(text); // Deleting File await Deno.remove("tempdir/world.txt"); // Deleting Directory await Deno.remove("tempdir");

The documentation for all functions in the Deno global can be found here.

Notice the extensive use of the async/await syntax. Deno, unlike Node, is Promised-based (Node is callback-based), and thus newer Promise syntax can be used, making the code much more readable. Also note the ability to use await at the top level of the module.

Now try to run the program:

Command Line Shell

deno run test.js

Notice that the program won't execute. This is due to the sandbox and security features of Deno that prevent read and write access by default. These security features make Deno a very secure runtime, similar to the browser, which uses a sandboxed model to prevent websites from arbitrarily reading and writing to the disk.

To be able to execute this program, add the --allow-write and --allow-read flags to the deno command:

Command Line Shell

deno run --allow-write --allow-read test.js

Since this is built-in, we don't need to make any changes to our modules.js and our importmap.json

The Path Module

The prior program will only execute properly on Linux and Mac, but not on Windows due to differing path seperator characters ("/" on Linux and Mac, and "\" on Windows). The Deno standard library has a path module that has a very similar API as the Node path module. Let's modify the the prior program to include and use the path module:

test.js

/* * Asynchronous making directory, writing file, renaming file, reading file, * deleting file, and then deleting the directory */ import * as path from "https://deno.land/std/path/mod.ts"; // Making Directory await Deno.mkdir("tempdir"); // Writing File const encoder = new TextEncoder(); const data = encoder.encode("Hello world"); await Deno.writeFile(path.join("tempdir", "hello.txt"), data); // Renaming File await Deno.rename(path.join("tempdir", "hello.txt"), path.join("tempdir", "world.txt")); // Reading File const decoder = new TextDecoder("utf-8"); let text = decoder.decode(await Deno.readFile(path.join("tempdir", "world.txt"))); console.log(text); // Deleting File await Deno.remove(path.join("tempdir", "world.txt")); // Deleting Directory await Deno.remove("tempdir");

Web Requests Using the Fetch API

One of the dependencies in my package.json file from Node:

package.json

... "request-promise": "^4.2.5", ...

Is a Promised-based library for performing HTTP web requests. Node, by default, doesn't include a Promised-based API for performing HTTP requests. However, Deno has the newer Fetch API function built-in, so this package is no longer needed. Additionally, since the Fetch API is available in most browsers, you can use the same API for deno and the browser for HTTP requests:

test.js

/* * Fetching a UUID from the website https://httpbin.org/, a testing website to * test various web requests. */ fetch("https://httpbin.org/uuid") .then(resp => resp.json()) .then(data => { console.log(data["uuid"]); });

And then run the deno command:

Command Line Shell

deno run test.js

Once again, we get an error. Deno's sandbox blocks network access as well by default, again making the runtime very secure. Run the deno command with the --allow-net flag:

Command Line Shell

deno run --allow-net test.js

Web Frameworks

Let's look at the dependencies in the package.json that are used for web frameworks:

package.json

... "body-parser": "^1.19.0", "cookie-session": "^1.3.3", "express": "^4.17.1", "express-session": "^1.17.0", "helmet": "^3.22.0", "morgan": "^1.9.1", "nodemon": "^2.0.2", "knex": "^0.21.1", "sails": "^1.2.3", "sequelize": "^5.21.3", "sqlite3": "^4.1.1", ...

These packages include Sinatra and Rail-like frameworks, various middleware components, database drivers, and ORM libraries. Unfortunately, many of these are built specifically for Node, and should not be used with Deno. However, the Deno community has already made some frameworks for Deno. The most popular web framework for deno, Oak, is a Koa-based framework that makes it very easy to develop web applications with deno. Oak currently has over 1000 github stars, and has a good amount of middleware developed for it. Below is an example of using the Oak framework:

test.js

import { Application } from "https://deno.land/x/oak/mod.ts"; const app = new Application(); app.use((ctx) => { ctx.response.body = "Hello World!"; }); await app.listen({ port: 8000 });

Oak already includes the ability to parse the body of requests, so the body-parser is no longer needed.

For SQL queries, I've ported over the Knex.js library's query building features and created another library for executing those queries. The query builder port of Knex is the Dex library, and the library that can be used to execute these queries on various databases is the Dexecutor library. Dex allows you to build queries for all of the major SQL databases, and Dexecutor currently supports executing queries on MySQL, MariaDB, Postgres, and Sqlite3 databases. Thus we can use these libraries in place of sqlite3 and sequalize libraries (as well as other database drivers).

For sessions, I've created a sessions library called Session. It currently supports the Oak framework, and allows you to choose either computer memory or a Redis database to store session data.

Finally, for the improved security provided by helmet, I've created a port of helmet called Snelm, which supports multiple deno web frameworks, including Oak, ABC, Alosaur, Pogo, and Aqua.

Remaining Modules

So far we have converted and resolved many packages from my Node package.json file. The remaining modules not yet resolved include:

package.json

... "claudia": "^5.12.0", "clipboardy": "^2.1.0", "data-forge": "^1.7.7", "data-forge-fs": "0.0.7", "gnuplot": "^0.3.1", "nexe": "^3.3.2", "nodemon": "^2.0.2", "pdf-parse": "^1.1.1", "tempy": "^0.3.0", "tesseract.js": "^2.0.0-beta.2", ...

Some modules similar to these that were created specifically for Deno can be found on the Github page Awesome-Deno which is a hand-curated list of Deno resources and modules. Let's try to find compatible modules:

package.json

... "nodemon": "^2.0.2", ...

Nodemon is a command line tool that watches for changes in files and restarts Node when a file changes. This is useful when creating web apps in Express, as the server will be automatically be restarted when changes occur. On Awesome-Deno, a similar module, Denon, can be found. We can install denon using the following command:

Command Line Shell

deno install denon https://deno.land/x/denon/denon.ts --allow-read --allow-run --allow-net

And can use it like this:

Command Line Shell

denon index.js

denon also supports similar command line flags as nodemon

package.json

... "clipboardy": "^2.1.0", ...

Clipboardy is a simple library that allows you to store data in the clipboard. A quick search of Awesome-Deno currently returns no results for a clipboard library. However, we can also check Deno's official website, which contains a section and a repository for 3rd party modules:

A quick search and I found this library: clipboard:

Which has the basic clipboard functionality I'm looking for.

package.json

... "gnuplot": "^0.3.1", "tesseract.js": "^2.0.0-beta.2", ...

The gnuplot and tesseract packages simply are wrappers around command line interfaces for these programs. One workaround is to simply call a subprocess and use the command line interfaces directly. This saves us from having to include an external package. The Deno global object has a function, Deno.run(), that we can use to call a subprocess and interact with these programs directly.

package.json

... "nexe": "^3.3.2", ...

nexe is a command line tool to convert a NodeJS program into a single executable file. Fortunately, Deno, unlike Node, is contained in a single executable file by default, and so there is no longer a need for nexe.

package.json

... "tempy": "^0.3.0", ...

Fortunately, the Deno global has the functions, Deno.makeTempFile and Deno.makeTempDir (as well as the corresponding sync functions), so this package is no longer needed.

package.json

... "data-forge": "^1.7.7", ...

Since I was having issues with using data-forge from Pika CDN, I ported over the webpacked library myself. The deno version of data-forge, DenoForge, can be found here.

Unresolved Modules

Finally, some packages we use in Node won't be compatible with any of these methods, such as these ones in my package.json:

package.json

... "claudia": "^5.12.0", "pdf-parse": "^1.1.1", "data-forge-fs": "0.0.7", ...

These are packages that likely do not use ESM syntax, make use of extensive Node APIs, can't be run in the browser, and have unique features that are unlikely to be replicated using other modules people have created so far for Deno, given that Deno is a newer project and hasn't released in 1.0 yet. Consider reaching out to the maintainers of these modules and seeing if there are any plans to migrate the libraries over to ESM syntax and Deno. But, at least we were able to resolve many common modules!

Using the Import Map

Below are the final contents of our modules.js and importmap.json files:

modules.js

// Deno Standard Library import * as colors from "colors"; import * as uuid from "uuid"; import * as ws from "ws" // Pika CDN import * as d3 from "d3"; import * as three from "three"; // jspm.io import aws from "aws"; import bson from "bson"; import bucket from "bucket"; import cheerio from "cheerio"; import chroma from "chroma"; import jsdom from "jsdom"; import jstat from "jstat"; import _ from "lodash"; import ml from "ml"; import moment from "moment"; import natural from "natural"; import numeral from "numeral"; import nunjucks from "nunjucks"; import papaparse from "papaparse"; import voca from "voca"; import xlsx from "xlsx";

importmap.json

{ "imports": { "uuid": "https://deno.land/std/uuid/mod.ts", "colors": "https://deno.land/std/fmt/colors.ts", "ws": "https://deno.land/std/ws/mod.ts", "aws": "https://dev.jspm.io/aws-sdk@2.625.0", "bson": "https://dev.jspm.io/bson@4.0.2", "bucket": "https://dev.jspm.io/buckets-js@1.98.2", "cheerio": "https://dev.jspm.io/cheerio@1.0.0-rc.3", "chroma": "https://dev.jspm.io/chroma-js@2.1.0", "d3": "https://cdn.pika.dev/d3@5.15.0", "jsdom": "https://dev.jspm.io/jsdom@15.2.1", "jstat": "https://dev.jspm.io/jstat@1.9.2", "lodash": "https://dev.jspm.io/lodash@4.17.15", "ml": "https://dev.jspm.io/ml-regression@5.0.0", "moment": "https://dev.jspm.io/moment@2.24.0", "natural": "https://dev.jspm.io/natural@0.6.3", "numeral": "https://dev.jspm.io/numeral@2.0.6", "nunjucks": "https://dev.jspm.io/nunjucks@3.2.0", "papaparse": "https://dev.jspm.io/papaparse@5.1.0", "three": "https://cdn.pika.dev/three@0.114.0", "voca": "https://dev.jspm.io/voca@1.4.0", "xlsx": "https://dev.jspm.io/xlsx-populate@1.20.1" } }

Now that our import map has been created and all of our modules have been cached, we can now use our import map in other projects and refer to modules using their names in the import map instead of their full URLs.

Create a new project with the following directory structure:

deno_project folder

|--deno_program |--index.js |--importmap.json

Where the importmap.json in the new project is a copy of the one in the prior project.

Write the following code in the index.js file:

index.js

import * as colors from "colors"; import _ from "lodash"; import voca from "voca"; console.log("The random string chosen: %s", colors.yellow(_.sample([ voca.camelCase("hello world"), voca.camelCase("deno land") ])));

Notice I am referring to the modules in this file by their keys in the importmap.json.

Now simply use the deno command to run this program, but use the --importmap flag to associate the modules in importmap.json with the modules in the program:

Command Line Shell

deno --importmap=importmap.json index.js

And now you can import the modules by name instead of by URL. Additionally, no downloading will occur for these modules since they have already been cached. This saves a lot of space on disk since you don't need to have a separate node_modules folder for every project.

Summary

Hopefully this guide gave you a good overview of how to migrate packages over to Deno, and how we can currently use Deno in our programs. Deno is relatively new compared to the mature Node project, but it has a lot of very desirable features, uses modern technologies, and fixes many of the "bugs" and poor design features that Ryan Dahl believes he introduced in Node when first creating, and these changes give Deno the potential to be the successor to Node.

If you have any comments or questions on this article, please feel free to reach out to me at denotutorials@protonmail.com