The way I see it, the primary goal of Deno is to simplify development of software, relative to the status quo.
Simplifying means removing the accidental complexity.
To me, a big source of accidental complexity in today’s software are implicit dependencies.
Software is built of many components, and while some components are relatively well-defined (Linux syscall interface, amd64 ISA), others are much less so.
Example: upgrading OpenSSL for your Rust project from 1.1.1 to 3.0.0 works on your machine, but breaks on CI, because 3.0.0 now needs some new perl module, which is expected to usually be there together with the perl installation, but that is not universally so.
One way to solve these kinds of problems is by putting
an abstraction boundary a docker container around them.
But a different approach is to very carefully avoid creating the issues.
Deno, in the general sense, picks this second noble hard path.
One of the first problems in this area is bootstrapping. In general, you can paper over quite a bit of complexity by writing some custom script to do all the grunt work. But how do you run it?
One answer is to use a shell script, as the shell is already installed.
Which shell? Bash, sh, powershell?
Probably POSIX sh is a sane choice, Windows users can just run
a docker container a Linux in their subsystem.
You’ll also want to install shellcheck to make sure you don’t accidentally use bashisms.
At some point your script grows too large, and you rewrite it in Python.
You now have to install Python, I’ve heard it’s much easier these days on Windows.
Of course, you’ll run that inside a docker container a virtual environment.
And you would be careful to use
python3 -m pip rather than
pip3 to make sure you use the right thing.
Although scripting and plumbing should be a way to combat complexity, just getting to the point where every contributor to your software can run scripts requires
a docker container a great deal of futzing with the environment!
Deno doesn’t solve the problem of just being already there on every imaginable machine.
However, it strives very hard to not create additional problems once you get the
deno binary onto the machine.
Some manifestations of that:
Deno comes with a code formatter (
deno fmt) and an LSP server (
deno lsp) out of the box.
The high order bit here is not that these are high-value features which drive productivity (though that is so), but that you don’t need to pull extra deps to get these features.
Similarly, Deno is a TypeScript runtime — there’s no transpilation step involved, you just
Deno does not rely on system’s shell. Most scripting environments, including node, python, and ruby, make a grave mistake of adding an API to spawn a process intermediated by the shell. This is slow, insecure, and brittle (which shell was that, again?). I have a longer post about the issue. Deno doesn’t have this vulnerable API. Not that “not having an API” is a particularly challenging technical achievement, but it is better than the current default.
Deno has a correctly designed tasks system.
Whenever you do a non-trivial software project, there inevitably comes a point where you need to write some software to orchestrate your software.
Accidental complexity creeps in the form of a
make is that?) or a
Node (as far as I know) pioneered a great idea to treat these as a first-class concern of the project, by including a
scripts field in the
It then botched the execution by running the scripts through system’s shell, which downgrades it to
./scripts directory with more indirection.
In contrast, Deno runs the scripts in
deno_task_shell — a purpose-built small cross-platform shell.
You no longer need to worry that
rm might behave differently depending on
which rm it is, because it’s a shell’s built-in now.
These are all engineering nice-to-haves. They don’t necessary matter as much in isolation, but together they point at project values which align very well with my own ones. But there are a couple of innovative, bigger features as well.
The first big feature is the permissions system.
When you run a Deno program, you need to specify explicitly which OS resources it can access.
google.com would require an explicit opt-in.
You can safely run
and be sure that this won’t steal your secrets.
Of course, it can still burn the CPU indefinitely or fill
out.txt with garbage, but it won’t be able to read anything beyond explicitly passed input.
For many, if not most, scripting tasks this is a nice extra protection from supply chain attacks.
The second big feature is Deno’s interesting, minimal, while still practical, take on dependency management. First, it goes without saying that there are no global dependencies. Everything is scoped to the current project. Naturally, there are also lockfiles with checksums.
However, there’s no package registry or even a separate package manager.
In Deno, a dependency is always a URL.
Surprisingly, it feels like this is enough to express various dependency patterns.
For example, if you need a centralized registry, like https://deno.land/x, you can use URLs pointing to that!
URLs can also express semver, with
foo@1 redirecting to
Import maps are a standard, flexible way to remap dependencies, for when you need to tweak something deep in the tree.
Crucially, in addition to lockfiles Deno comes with a built in
deno vendor command, which fetches all of the dependencies of the current project and puts them into a subfolder, making production deployments immune to dependencies’ hosting failures.
Deno’s approach to built-in APIs beautifully bootstraps from its url-based dependency management.
First, Deno provides a set of runtime APIs.
These APIs are absolutely stable, follow existing standards (eg,
fetch for doing networking), and play the role of providing cross-platform interface for the underlying OS.
Then there’s the standard library.
There’s an ambition to provide a comprehensive batteries included standard library, which is vetted by core developers, a-la Go.
At the same time, huge stdlib requires a lot of work over many years.
So, as a companion to a stable 1.30.3 runtime APIs, which is a part of
deno binary, there’s 0.177.0 version of stdlib, which is downloaded just like any other dependency.
I am fairly certain that in time this will culminate in actually stable, comprehensive, and high quality stdlib.
All these together mean that you can be sure that, if you got
deno --version working, then
deno run your-script.ts will always work, as the surface area for things to go wrong due to differences in the environment is drastically cut.
To sum up, historically the domain of “scripting” and “glue code” was plagued by the problem of accidentally supergluing oneself to a particular UNIX flavor at hand.
Deno finally seems like a technology that tries to solve this issue of implicit dependencies by not having the said dependencies
instead of putting everything in a docker container.