Mastodon Icon RSS Icon GitHub Icon LinkedIn Icon RSS Icon

My First Deno Experiment

I know. This will probably be the 560th article on Deno you have seen this week. Unfortunately, I cannot help myself: I work on Node.js and TypeScript all day long and, when I read “node replacement,” “TypeScript,” and “Rust,” I lose any inhibition.

So I downloaded Deno, and I did the only reasonable thing: I adapted my first Node.js (movingai2json) package to Deno. But let’s start from the beginning.

What is Deno

Using the words of Deno developers, Deno is:

A Web Browser for Command-Line Scripts […] Deno attempts to provide a standalone tool for quickly scripting complex functionality. Deno is (and always will be) a single executable file. Like a web browser, it knows how to fetch external code. In Deno, a single file can define arbitrarily complex behavior without any other tooling.

But in more practical words, Deno is a runtime for TypeScript. Do you know Node.js? Deno is the same thing (it also has the same father, Ryan Dahl) but runs TypeScript code natively.1

If you have worked with Node.js and npm extensively like me, you know what a flaming pile of garbage it is.2 Anything that can be a possible replacement for Node.js is designed to catch my attention.

But it has any chance tough? To answer this question, I needed to get my hands dirty a bit.

Porting the movingai2json package

I have an old, mostly unmaintained, npm package written in TypeScript called movingai2json. It is a small utility to convert MovingAI maps and scene files (a popular format in academia for benchmarking pathfinding algorithms) into JSON files.

NOTE: The original moved into a Rust package some years ago. I talked about it here.

As an experiment, I decided to port this npm package into Deno. It was the perfect candidate: the package was small enough and with very few dependencies but complex enough to let me understand Deno better. You can find the end result in this repository.

How to import dependencies

For the original project, I used a promisified filesystem package for Node.js. I do not need it anymore: Deno’s standard library already contains a file system module, and Deno natively supports the async/await way of life.

But I still needed to import this module. And here we come to the first big difference between Node and Deno: Deno has no package manager. Instead, Deno knows how to fetch dependencies directly from URLs.

1
2
3
4
5
import {
  readFileStr,
  writeFileStr,
  walk,
} from "https://deno.land/std/fs/mod.ts";

You can just import modules from anywhere you like: URLS, GitHub repositories, CDN, and more.

This is probably Deno’s most polarizing feature. On the internet, you can find people fiercely discussing the several pros and cons of this approach. Most of the pros are related to the post-traumatic nightmares we all have when we think about npm. npm is probably history’s worst package manager, and the npm registry is a bloated joke (I am talking to you is-number). So, it is clear to me why so Node.js developers may prefer no package manager at all. They are traumatized.

The cons faction put on the table a good chunk of objections. However, I think many on the people making objections do not have spent a minute looking in the official documentation, in particular here. The Deno documentation contains a quick answer to many common objections:

  • I can I be sure that nobody maliciously changes the code linked by the URL? -> You can generate an integrity check file.
  • I really need to manage dependencies all over my codebase? -> You can create a deps.ts
  • What if the dependency URL goes down? -> Deno encourages vendoring dependencies.
  • What if I need to distribute my application/library? -> You can bundle everything (even dependencies) in a single .js file.

So, all the objections are incorrect? Well, not entirely. There is a certain level of truth. I’ll give you my opinion in the next section. For now, let’s go back to work.

Unit Testing is available by default

After importing the standard library, porting the code was straightforward. In 10 minutes, I was able to convert all the Node.js fs module calls into the equivalent Deno API fs module.

It was time to update the unit testing suite. This is a piece of excellent news: Deno comes with a default unit testing runner.

Node.js test runners are powerful but are a gigantic mess. There are too many options, they are all incredibly big, and they come with configuration files and dumb dependencies (I am still talking to you, is-number).

Therefore, with extreme satisfaction, I nuked all the testing dependencies and used the default test runner. I do not know how many advanced features it has, but for my use case, it was more beautiful, cleaner, less verbose, and – most importantly – enabled by default.

1
2
3
4
5
6
7
Deno.test("Parsing String", async () => {
  const mapStr = await readFileStr("./test/keydoor.map");
  let parsedMap = parseMapString(mapStr);

  assertEquals(parsedMap.width, 20);
  assertEquals(parsedMap.height, 20);
});

Look how simple it is. The only think you need do now is to run the test with

1
deno test --allow-read=./test --allow-write=./test ./test/tests.ts

and…

1
2
3
4
5
6
error: TS2339 [ERROR]: Property 'utime' does not exist on type 'typeof Deno'.
    await Deno.utime(dest, statInfo.atime, statInfo.mtime);
               ~~~~~
    at https://deno.land/std/fs/copy.ts:92:16
[...]
// More errors like this.

What the hell is that?

The unstable flag and the weird errors

Apparently, if you follow the standard documented way, you may encounter a big chunk of errors like the ones above. Of course, the errors say absolutely nothing on what to do.

It turned out that some standard library functions are gated behind an --unstable flags (and some of them are mistakenly flagged as unstable even if they are in the stable build).

The error could be better. Fortunately, the solution is easy. Just add the --unstable flag:

1
deno test --unstable --allow-read=./test --allow-write=./test ./test/tests.ts

This solution is offered to you, so you do not have to look at Deno’s issue board for this problem! Enjoy.

Anyway, run the command and look at the happy results.

1
2
3
4
5
6
running 3 tests
test Parsing String ... ok (22ms)
test Parsing File ... ok (4ms)
test Parsing Scen String ... ok (4ms)

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

All this with ZERO external dependencies.

Granular Security

Have you noted the --allow-read=./test --allow-write=./test? These are flags to enable the application to write/read in a specific folder. I have not a strong opinion on this, but as a first impression, it seems a very good thing. The fact that allows granularity at the folder-level is very appreciated.

More info here.

What I liked and about what I am dubious

At that point, this simple toy project was done. It was a small experience but sufficient to have a first “non Hello World” experience with Deno.

There are many things I liked/loved:

  • I do not have a node_modules monstrosity in my folder. The project is just 68kb, and I can bundle the application with all the dependencies in 169kb. Meanwhile, the Node version and all its dependencies use 78Mb (megabytes!) in my hard drive. S E V E N T Y E I G H T.
  • The coding experience is exceptionally streamlined, especially for small applications. I do not need to set up anything. I can just create a script.ts somewhere, and that script is the only thing I need to write a TypeScript software. This is somehow liberating.
  • Deno comes with a standard testing suite. And a built-in formatter. And a built-in bundler. And a built-in documentation generator. And a built-in linter (in the future). I cannot stress enough on how this should be the standard.
  • The granular security permission system stays on the pro even if I am still not 100% sure how much it is worth.

However, on the other features, I am more cautious.

For instance, I am not a huge fan of the distributed and ethereal dependency system. I know that npm is scary, and we all hate it, but there are good package managers out there.

The documentation answer many of the main concerns. However, if I have to manage a deps.ts, an import map, a lock file for integrity and vendoring all my dependencies… Well, I feel like I am reimplementing a dependency manager myself. If the problem is “to have a centralized registry” (and even this is debatable), then the problem is not the package manager.3

Of course, this is not important for small-medium applications (that I think is where Deno shines), and the project is barely v1.0. I believe we will quickly see some third-party “Deno package manager” at some point.

For now, I also do not see a lot of performance advantages over Node.js The TypeScript compilation is infamously slow, and it is a burden on Deno startup (at least, the bundled version designed for distribution, is already compiled). Even in this case, this issue is supposed to improve in the future (especially if they ever achieve the ambitious task of rewriting the TSC in Rust).

Lastly, Deno “executables” need to pass through deno run and managing permissions. The overall experience seems less nice than npm install-ing a tool and having it available in a single command. Even for this, I see the space of a “Deno runner” of some kind.

Conclusions

In the end, I think I will keep following Deno. It already fills the role of simplifying Node for not-big projects, and for every cons there is a large room for improvement.

Will Deno be another viable platform? It will be succesful? It mostly depends on how much community the project will attract. If the “hype” will remain strong enough for the following 2-3 years, and if Deno will succesfully deliver an ecosystem of tools, then I think there is a solid chance.

And you? What do you think? Leave a comment here or on Twitter. See you next time!


  1. To be honest, Deno compiles on the fly TypeScript code into JavaScript using the Microsoft’s TypeScript Compiler. So this is just a performance penalty on the application startup with the only advantage of hiding the compilation step from the user. At least for now. ↩︎

  2. Things are now marginally better, mostly because the community threw at it an insane amount of tooling. Node.js is still in its essence a flaming pile of garbage, but at least now it is encapsulated in a pretty crystal case. I really do not want to start a rant on Node, though. ↩︎

  3. Go had the same brilliant idea, and they tried to die on that hill for as long as possible even if all the empiric experience was against it. We all know how it ended↩︎