Roman Zaynetdinov (zaynetro)

Deno is an alternative JavaScript runtime that uses V8 and Rust. Think of Node.js but with some different design decisions. Deno comes with built-in support for TypeScript, ES modules, test runner, code formatter, LSP server and sandboxed execution.

Deno logo") 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

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. The server is open-source and you can see how sandbox works on GitHub.

Test runner

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

import { assertEquals } from "https://deno.land/std@0.178.0/testing/asserts.ts";

Deno.test("url test", () => {

const url = new URL("./foo.js", "https://deno.land/");

assertEquals(url.href, "https://deno.land/foo.js");

});

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

import { useState } from "preact/hooks";

interface Props {

initialCount: number;

}

export default function Counter({ initialCount }: Props) {

const [count, setCount] = useState(initialCount);

const increment = () => setCount(count + 1);

return (

<div>

Current value: {count}

<button onClick={increment}>Increment</button>

</div>

);

}

Now comes the interesting part: our test file.

// Counter_test.tsx

import { fireEvent, render, waitFor, cleanup } from "@testing-library/preact";

import { assertEquals, assertExists } from "std/testing/asserts.ts";

import { JSDOM } from "jsdom";

import Counter from "./Counter.tsx";

// Setup JSDOM

const doc = new JSDOM("");

globalThis.document = doc.window.document;

Deno.test("Counter", async (t) => {

await t.step("should display initial count", () => {

cleanup();

const { container } = render(<Counter initialCount={5} />);

assertEquals(container.textContent, "Current value: 5Increment");

});

await t.step(

'should increment after "Increment" button is clicked',

async () => {

cleanup();

const { getByText } = render(<Counter initialCount={5} />);

fireEvent.click(getByText("Increment"));

await waitFor(() => {

assertExists(getByText("Current value: 6"));

});

},

);

});

// Instead of manual clean up before each test we can use Deno's BDD testing methods:

// describe, it, beforeEach from "https://deno.land/std@0.178.0/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.

// deno.jsonc

{

// Specify what JSX should be transpiled to.

"compilerOptions": {

"jsx": "react-jsx",

"jsxImportSource": "preact"

},

// Our import map

"imports": {

"preact": "https://cdn.skypack.dev/preact@10.13.0",

"preact/": "https://cdn.skypack.dev/preact@10.13.0/",

"@testing-library/preact": "https://cdn.skypack.dev/@testing-library/preact@3.2.3",

// jsdom from skypack didn't work

"jsdom": "https://esm.sh/v102/jsdom@21.1.0",

"std/": "https://deno.land/std@0.178.0/"

},

// Ref: https://deno.land/manual@v1.31.1/basics/import_maps#overriding-imports

"scopes": {

"https://esm.sh/v102/jsdom@21.1.0/": {

"https://deno.land/std@0.175.0/node/vm.ts": "./vm.ts"

}

}

}

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

export default {

isContext: () => false,

};

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://deno.land/std@0.170.0/node/_core.ts:22:13)

at processTicksAndRejections (https://deno.land/std@0.170.0/node/_next_tick.ts:62:10)

at https://deno.land/std@0.170.0/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!

Summary

For UI tests I would recommend to stick with Vitest, Cypress or Playwright.

Other runtimes

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 about benchmarks.

Read the original on zaynetro.com ↗