Will deno be the new node ?

9 minute read

During a JavaScript event (JSConf EU) in June 2018, Ryan Dahl, creator of Node.js, made a 26 min long mea culpa about this project.

Ryan Dahl

“Node is like nails on chalkboard to me”.

At the end of his talk he confessed that preparing it was a bit sad, because he explained problems without introducing solutions. So he made a small Proof of Concept, “deno”, a tool which by design fixes all the problems of node. At the time I write this post, deno has evolved into a serious project, which 1.0 release is officialy announced for May 2020. Let’s see if it is interesting !

First of all, we have to introduce the problems of Node.js to understand how deno is a game changer.

The Node.js mess

Node.js

Node was introduced in 2009 and allowed developers to make Server-Side Javascript applications. This was allowed by the implementation of and event queue wich overcomes the difficulties to make JavaScript server-side because of its single-thread design.

It was really innovative: Node.js took Google’s V8 JS engine outside browsers and made it server side.

Node quickly grew to be one of the most widely used Server Side Technology. The issue now, is to deal with it.

According to its creator, Node.js has a lot of design issues which make it unecessary complex and unsecure.

Node.js doesn’t know about Security

There is absolutly no permissions management for third parties code. I will cite Node.js’s creator here: “your linter shouldn’t get complete access to your computer and network”.

As JavaScript is a dynamic language, Monkey Patch is possible. It allows you to replace attributes at runtime (a method for instance). Now imagine what could happen if a malicious module was trying to monkey patch some ExpressJS method to add a custom middleware ! Your application would be comprised by this module.

Node.js is insecure, and it cannot be patched. The only solution is to rigorously analyze each one of your dependencies (and their dependencies) to be sure not to be vulnerable.

There are also some modules which can help you find malicious dependencies. But they should not exist, security should be handled by Node.js design.

package.json is everything

package.json is necessary in all node projects. It killed simplicity for two reasons: 1. To use Node.js in a project, you need to make some project initialization steps (npm init, installation of dependencies etc.) and then code but a better approach would be to code right away. 2. package.json contains absolutly everything: name and description of your projects, your dependencies and their versions, the license of your project, SEO keywords, your git repository etc. I will cite Ryan Dahl who deeply regret this feature: “It’s boilerplate noise”.

    $ npm init

    {
        "name": "example",
        "version": "1.0.0",
        "description": "description of the package",
        "main": "index.js",
        "scripts": {
            "test": "run tests"
        },
        "repository": {
            "type": "git",
            "url": "git@git.package.com"
        },
        "keywords": [
            "stack",
            "npm",
            "yarn"
        ],
        "author": "pwnh4",
        "license": "MIT"
    }

An overcomplicated module management

meme node_modules

This is maybe the most popular meme about node_modules. This directory contains all your dependencies and is managed with npm. This directory is always too heavy and basically adds complexity to your project codebase. A lot of projects have node_modules with a size greater than a GB !

Ideally, your project should only contain your code, not gigabytes of dependencies.

Handling all the modules is also quite exhausting: when you are developing a feature and you realize that it needs a third party module, you have to stop coding and go play with npm to find the module and download it.

node_modules and its management are distractions, you should only have to focus on your code.

Incompatibilities

The first compatibility problem comes from the build of native addon modules. Initially, Node.js and Chrome both used GYP to build those modules. But Chrome team decided to drop it for GN, a better alternative. Node.js did not make this change, now it uses node-gyp to build this dependencies: it is a fork from the original Chrome GYP. It adds a lot of levels of complexity on the Node.js project and reduces the compatibility with Chrome team’s projects.

The other main issue is that Node.js is not compatible with browsers. Even if it is thought for server side, it is really sad that one cannot use node features to build browser apps.

deno: Simplicity and Security

deno

Now let’s see if the solution proposed by Ryan Dahl is a good way to get out of this Node.js mess.

Simplicity

When he listed all his regrets about Node.js, it was always the same conclusion that came up: too much complexity.

It’s funny to see that everything in deno is strictly following the KISS (Keep It Simple, Stupid) principle. We can really feel that Ryan Dahl wants to make things differently from Node.

As he said in his conference, a feature should be added to a project only if it is necessary.

Installation

As announced, the project is kept in one binary with a minimal amount of linkages.

    $ curl -fsSL https://deno.land/x/install/install.sh | sh -s v0.38.0
        inflating: deno

    $ /bin/deno
        linux-vdso.so.1 (0x00007ffc77cea000)
        libdl.so.2 => /lib64/libdl.so.2 (0x00007f4f6439e000)
        librt.so.1 => /lib64/librt.so.1 (0x00007f4f64393000)
        libpthread.so.0 => /lib64/libpthread.so.0 (0x00007f4f64371000)
        libgcc_s.so.1 => /lib64/libgcc_s.so.1 (0x00007f4f64357000)
        libc.so.6 => /lib64/libc.so.6 (0x00007f4f6418e000)
        /lib64/ld-linux-x86-64.so.2 (0x00007f4f6692b000)
        libm.so.6 => /lib64/libm.so.6 (0x00007f4f64048000)

Features

Deno defines itself as a “secure runtime for JavaScript and TypeScript”. Let’s see the main features it proposes.

TypeScript support out of the box

TypeScript is natively supported by deno. When you run a project, you will almost see no difference between TypeScript or JavaScript using deno.

    $ deno run hello.ts
        Compile file:///tmp/hello.ts
        Hello deno !

Promise API

When you develop a Deno application, you can use the Deno API to interact with your filesystem, program arguments etc. What is cool about this API is that it always return promesses. This allow us to use await more often and avoid callback hell. Promises are also great to perform modern JS/TS async development.

Here is a program which reproduces the behaviour of the cat command in TypeScript with the Deno API:

for (let i = 0; i < Deno.args.length; i++) {
  let filename = Deno.args[i];
  let file = await Deno.open(filename);
  await Deno.copy(Deno.stdout, file);
  file.close();
}

In Node.js, we should have written copy as a callback of open. Here the syntax is clearer and more respectful of current web development standards.

Security

A huge feature of Deno is Security. All your code and its dependencies run in a sandbox which does not have any rights (on the filesystem, network etc.) by default.

For example if we execute like this the above cat program:

    $ deno run cat.ts CATME
        error: Uncaught PermissionDenied: read access to "/home/pwnh4/Documents/WIP/deno/security/CATME", run again with
        the --allow-read flag
        â–º $deno$/ops/dispatch_json.ts:43:11
            at PermissionDenied ($deno$/errors.ts:81:5)
            at unwrapResponse ($deno$/ops/dispatch_json.ts:43:11)
            at sendAsync ($deno$/ops/dispatch_json.ts:98:10)

We have to explictly give each permissions to our Deno application :

    $ security deno run --allow-read cat.ts CATME
    HELLO WORLD !!!!!

We can also interact with the Deno API to get the rights set on the program sandbox and revoke them if we no longer need them !

const status = await Deno.permissions.query({ name: "write" });
if (status.state !== "granted") {
  throw new Error("need write permission");
}

const log = await Deno.open("request.log", "a+");

// revoke some permissions
await Deno.permissions.revoke({ name: "read" });
await Deno.permissions.revoke({ name: "write" });

// use the log file
const encoder = new TextEncoder();
await log.write(encoder.encode("hello\n"));

// this will fail.
await Deno.remove("request.log");

Remote execution

Not a necessary feature, but it can be quite nice to test Deno. Here is an example with the cat program we saw earlier

    $ deno --allow-read https://deno.land/std/examples/cat.ts ~/.bashrc

Module management

In deno, there is absolutly no node_modules or package.json. All you have in your project is your code. So how does deno manage our dependencies ?

When you want to include a third party module, you simply have to add the url to fetch it. Here is the exampe of a simple API:

import { Application, Router } from "https://deno.land/x/oak/mod.ts";

import add from "./utils/add.ts";

const router = new Router();

/*
 * @brief I add some stuff
 *
 * @return the addition of 12 and 30
 */
router.get("/", (context) => {
  context.response.body = add(12, 30);
});

const app = new Application();
const port = 5000;

app.use(router.routes());
app.use(router.allowedMethods());

const server = app.listen({ port });

console.log(`Listening on port ${port}`);

At the first execution of this code, deno will fetch all modules imported via url and store them in cache. This way in the following executions, it won’t need to fetch it anymore.

Note that your modules will never be updated automatically. You have to explicitly tell deno with the --reload to update dependencies.

    deno --reload server.ts     # fetches again all dependencies

This ensures stability for your codebase as your modules will never be changed if you don’t explicitly ask to update them.

Deno standards recommend to have a modules.ts in which you centralize all your URL imports. When you will need a third party imported in modules.ts in another file, you will just have to make a relative import to this file !

It is also recommended to use git on your cache to ensure the saving of modules’ version.

Finally note that deno only works with ES modules import (better than Node.js with the outdated require) and that the module file extension must be specified. Explicit is better than implicit !

Standard Modules

Deno provide nice standard modules. As the project is not in 1.0 yet, there is only the strict minimum to make correct applications :

dir	0	archive/
dir	0	bytes/
dir	0	datetime/
dir	0	encoding/
dir	0	examples/
dir	0	flags/
dir	0	fmt/
dir	0	fs/
dir	0	http/
dir	0	io/
dir	0	log/
file	49993	manual.md
dir	0	media_types/
dir	0	mime/
dir	0	node/
dir	0	path/
dir	0	permissions/
dir	0	signal/
file	8133	style_guide.md
dir	0	testing/
dir	0	textproto/
dir	0	types/
dir	0	util/
dir	0	uuid/
dir	0	ws/

This is still quite enough for basic prototyping, and we should hope that many libs will be released by the community by the time first serious releases come out.

Tooling

deno provides a nice integrated toolbox to manage your codebase.

    $ deno fmt server.ts        # formats your code

    $ cat tests/tests.ts        # use deno API to unit test your code
        import { assertEquals } from "https://deno.land/std/testing/asserts.ts";
        import add from '../utils/add.ts';

        Deno.test(function test1And1() {
            assertEquals(add(1, 1), 2);
        });

        Deno.test(function test2And2() {
            assertEquals(add(2, 2), 4);
        });

    $ deno test tests/tests.ts  # run tests
        Compile file:///home/pwnh4/Documents/WIP/deno/modules/tests/test_add.ts
        running 2 tests
        test test1And1 ... ok (2ms)
        test test2And2 ... ok (1ms)

        test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out (2ms)


    $ deno bundle server.ts     # bundle server.ts and its dependencies to a single JS file
        ...

    $ deno info server.ts       # displays all dependencies of server.ts
        local: /home/pwnh4/Documents/WIP/deno/modules/server.ts
        type: TypeScript
        compiled: /home/pwnh4/.cache/deno/gen/file/home/pwnh4/Documents/WIP/deno/modules/server.ts.js
        map: /home/pwnh4/.cache/deno/gen/file/home/pwnh4/Documents/WIP/deno/modules/server.ts.js.map
        deps:
        file:///home/pwnh4/Documents/WIP/deno/modules/server.ts
            ├─┬ https://deno.land/x/oak/mod.ts
            │ ├─┬ https://deno.land/x/oak/application.ts
            │ │ ├─┬ https://deno.land/x/oak/context.ts
            │ │ │ ├─┬ https://deno.land/x/oak/httpError.ts
            │ │ │ │ ├─┬ https://deno.land/x/oak/deps.ts
            ...

At this moment two other tools are being developed and not released yet: * a linter deno lint * a debug mode deno --debug

Browser compatibility

Deno code can be used in browsers. The only two conditions are : * it should not use the Deno API * if code is in TypeScript it should be bundled with deno bundle

WebASM Support

Deno has an integrated web asm support. Here is an example with a basic add() function in C:

    $ ls  # module.asm is the webasm compiled add.c
        add.c  module.wasm  use_module.ts

    $ cat add.c
        int add(int a, int b)
        {
            return a + b;
        }

    $ cat use_module.ts
        const mod = new WebAssembly.Module(await Deno.readFile("module.wasm"));
        const {
            exports: { add }
        } = new WebAssembly.Instance(mod);
        console.log(`add(1, 5) = ${add(1, 5)}`);
        console.log(`add(100, 10) = ${add(100, 10)}`);

    $ deno --allow-read use_module.ts
        add(1, 5) = 6
        add(100, 10) = 110

Conclusion

Deno is a nice project that fixes a lot of Node.js design issues. I am really looking forward to test the V1.0 of the project in May 2020. I will wait concrete feedbacks of production-ready applications made with deno before making concrete web server with it.

A nice project to follow !