Will deno be the new node ?
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.
“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 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
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
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 !