This talk will walk you through writing a module in TypeScript that can be consumed by users of Deno, Node, and browsers. I will walk through how to set up formatting, linting, and testing in Deno, and then how to publish your module to deno.land/x and npm. I will also start out with a quick introduction on what Deno is.
Writing universal modules for Deno, Node, and the browser
AI Generated Video Summary
1. Introduction to Deno and TypeScript
Hey folks, I'm Luka from the Deno team and I'm going to be talking to you today about writing TypeScript code for Deno, Node, and the browser. Quickly about me, who am I? I am Luka. I work on the Deno team at the Deno company on Deno, the Deno CLI, so that's the open source tool that you can download and run on your own computer. And I also work on Deno Deploy, which is our hosted cloud offering that lets you run Deno projects all across the world close to users at the edge. You can learn more about that at deno.com.
2. Introduction to Deno and its Features
Something else that Deno provides is that it's secure by default. So just like the browser, you can't just do anything without the users consenting. So you can't read files from disk, you can't read environment variables, you can't talk the network without the user explicitly allowing that. So that can either happen by... Yeah, like if you're in the browser the site can't just send you notifications but you have to explicitly opt-in. Deno is also single executable that you download. So unlike Node or some other project it's not a whole zip file of different files that you need to put somewhere, but it's just a single executable that you place somewhere on your path and it runs. There's no need to install OpenSSL. Deno also has a big standard library of modules which are very useful for day-to-day development. Things like YAML encoder, Base64 encoder, cryptography things, HTTP servers, you name it, it's in there. And we aim to encapsulate the top 100 or something of npm modules. We also try to very closely follow web standards where possible. So Deno does not have a custom HTTP API. Instead we just use the Fetch API just like you would in the browser. We also use import maps for dependency remapping. Just like in the browser, we use ECMAScript modules, we use WebWorkers for multithreading, we use web streams for anything that's streaming and promises for anything that's asynchronous. So there's no callbacks, there's no custom streams implementation, it's all very standard.
3. Building a Greeting Message Library with Deno
Today, we'll explore how easy it is to build something with Deno. We'll write a library that creates greeting messages, add unit tests, format and lint the code, generate documentation, and test it in Node before publishing to MPM.
So yeah, that's Deno. But what are we actually going to do today? So, what we're going to do today is we're going to explore how easy it is to actually build something with Deno. So we're going to write a little library, and this library, I'll explain exactly what it does later, but it creates greeting messages, essentially. We're going to add some unit tests for this library, we're going to then format and lint it with our built-in tooling, we're going to write some doc comments and view those via our documentation generator, and then we're going to test that code in Node and publish it to MPM.
4. Compiling Deno Libraries for Node Consumers
Deno provides a way to compile your libraries for Node consumers, allowing you to switch to Deno while still enabling Node consumers to import your library. TypeScript in Deno is incredibly boring to write because it runs out of the box with no configuration. This is a paradigm shift, and if there are any issues, they will be fixed. The library we're going to write is simple, with a single greet function that generates a greeting message based on the name and greeting parameters. Let's get started with coding!
And the last part is probably the most interesting to many of you because Deno might be as awesome as it is, but if some of the consumers of your library are Node consumers, then you can't really switch over to Deno completely until they also switch over, which is annoying. You don't really want that. So what we do is we provide you with a way to compile your Deno libraries to something which Node consumers can still use so you can switch over to using Deno for everything And they can still import your library from Node. And that's really cool. I'll show you exactly how that works later.
You can see that there's not specifically a lot about TypeScript here, which is kind of weird for a talk at TypeScript Congress, right? But what I really want you to pay attention to is how incredibly boring it is to write TypeScript for Deno. Like, that is what I want you to take away from this talk is that Deno runs TypeScript out of the box with no configuration, and it is incredibly boring to write TypeScript in Deno. Like, there's nothing to it. There's nothing for you to set up. Nothing. And that is a paradigm shift. This is not something that we have seen a lot before. Vite sort of provides this for browser development, but this is not something that we've seen a lot for in the Node world. Yeah, so take away from this that using TypeScript in Deno is really boring, and TypeScript works everywhere in Deno, out of the box, with a zero configuration. And if you find anything where it doesn't work, then that's a bug, and it will be fixed. But, yeah, let's hope you don't find any bugs in Deno. Anyway.
Okay, so the library that we're actually going to write is going to be relatively simple. It has a single function that it exports, the greet function. It takes a name and a greeting, and it generates a greeting message. So for example, it takes the name Luca and the greeting Hello, and then it generates the greeting Hello, Luca. And you can choose exactly which greeting should be generated through the second parameter to this function, which is of type greeting, which is an enum, a TypeScript enum. You can specify like, I want a hello greeting or I want a hi greeting or I want a good morning greeting or whatever you want. So that's fine. So let's actually get started with coding on the library here. You'll see that I'm going to be copy pasting some code because we're this is a really short talk and I don't have much time. But yeah, I'll explain everything as we go through. So you'll see that I'm using VS code. I like this code, because I don't know, it's a great editor. And it works really well with you know, so the first thing you want to do is if you're using Dino with VS code, you want to go to the extensions, type in Dino and press install.
5. Setting Up Editor and Writing Code
Press F1 to open the command palette and initialize the Dino workspace configuration. Create a new file called mod.ts as the entry point for the library. Add a greeting enum with three different members: hello, hi, and good evening. Define a greet function that takes a name and greeting, with a default greeting of hello. The function returns a string containing the greeting, name, and exclamation point. Try it out using the Dino repo and write your code in the Dino redevelopment loop.
Press that. And then every time you open a new project that you haven't used it before, you want to use Dino in it in VS code, you press F one to open the command palette and then click on this Dino initialize workspace configuration. And if it's not here, you search for it. And then it'll ask you do you want to enable linting? We'll say yes. Do you want to enable unstable APIs? I'll say no to that. And it'll generate this VS code folder with the settings for JSON file.
6. Writing Tests and Running Them with Dnotest
We import the greet and greeting functions from mod.ts and call them. Our library works. We write tests using the built-in testing framework called dnotest. We import assertions from the dnostandard library and write tests using dno.test. Multiple ways to run tests: using the built-in testing in VS Code, clicking the testing button, or running dnotest in the shell. We can add more tests with different greetings.
So we can do import greet greeting from mod.ts. And then we have the greet function and the greeting enum, and we can call them. So greet Luca, and we'll say greeting.hi, and it returns hi Luca as a message, and we can also leave out the greeting, and then it'll say hello Luca. Cool. Our library works.
Let's go on to actually writing tests. So writing tests in dno is really easy because we have a built-in testing framework called dnotest. What it does is it looks for specific files in your repository or in your project, which end in either underscore test or dot test, and inside of those files you can register tests, and then dnotest will run those tests and it'll report on if the test succeeded or failed. It's a very simple interface, but it allows you to do very advanced things, like you can do before and after hooks, you can do sub-steps, there's a whole section in the dno documentation, and the manual for how it works.
So let's get to writing code. Again, we're going to create a file mod underscore test dot ts, this is what we write our tests in. First thing we're going to do is we're going to import the greet and the greeting function from mod dot ts, those are the functions that we're testing, so we actually, yeah, we need to import them. Next thing we're going to do is we're going to import some assertions from the dnostandard library from the assertion module. This assertion equal or assert equals, takes two parameters, and it just checks that they are the same, and if they are not the same it throws an error. And then we can start writing our tests. So to register tests you call dno dot test, with the first argument being the name of the test, and the second argument being the function which defines the actual test. So what this test is going to do is it's going to call greet with our name, TypeScript congress, and then it asserts that the greeting is equal to, hello, TypeScript congress exclamation point. There's now multiple ways to actually run this test. You can either use the built in testing in VS Code, you can hit this testing button on the left here, and then execute the tests, it'll run them. You can also click this little button on the side here. Or what you can do is if you're writing, running tests in CI, or you're not using dnotest is you can use, you can type in dnotest in your shell, and it'll find all the test files and run them as well. And this has passed, I can also make it fail, let's change the message to compare to something which it is not. If I run the test now, it says assertion error values are not equal, which is good. If I change this back, rendino test, oh, I don't think I saved. Let's do that again, okay, there we go. Yeah. So that works. I can add some more tests. Let's add some more tests which use different greetings, so this greeting hi, and greeting good evening, which have slightly different greeting messages. I can also run these in VS Code, or all at once, or using dino test.
7. Code Formatting, Linting, and Publishing
So that's writing tests in Dino, really not much to it. Yeah, so now we have tests, let's do some of the things which you probably want to do for your library project. You want to check that formatting's correct, ensure that there's a consistent formatting across the project, and also across all of your projects. For that, you can use dino fmt. dino fmt is another subcommand that you can run, and it formats all of your files. So if I mess up the styling here, let's add some spaces, and like, this is very ugly, right? Run dino fmt, it all snaps it back into place to make it look good.
Dino also has a built-in linter. So formatting is for styling, and linting is for logic errors. So if you have logic errors in your code, we can also find those. So sometimes, for example, you might accidentally write, if false console.log hello, this console.log hello can never happen, because false, like if false, can never happen, right? False, you're like, you're comparing to a constant expression here. False. And, you know, lint will catch this, and it'll say, use for constant expression as a condition is not allowed, remove this constant expression. And I can also check this in CI or from my shell with the dno lint sub command, and it'll tell me the same thing, and it'll give me more information to where I can, or it'll give me a link to where I can find more information. So that's formatting and linting.
Then you probably want to publish your code for dno users, not just node users, which we're going to look into later, but also dno users. To do that, you can actually publish it to just any web server. So dno imports its code, as you saw here, just from URLs, so you can host code anywhere. We do provide a module registry called dno.land.x, which has some nice guarantees, like it's immutable, people can't change the code after they've uploaded it, and it hooks right into your existing GitHub flow. But yeah, you can host your code wherever you want. If you want to learn more about dno.land.x, you can hit go to dno.land.x. But I've already published this module, so let's look at that real quick. dno.land.x.greeter. You can see our mod.ts, our readme, our tests. And yeah, so it's published. I told you about this documentation generation, so that's also directly integrated into dno.land.x or any website you want. You can go to dnoc.dno.land, enter a URL, and get documentation for it. Or if you're in dno.land.x, you can just hit the documentation button on the right hand side of any module, and then it'll show you the documentation.
8. Publishing to NPM and Running Tests
9. Building and Publishing the NPM Package
First, we define the entry point and output directory for transpilation. We can inject shims or polyfills for unavailable APIs. In dev mode, we shim the Deno namespace and specify package.json properties. We copy the readme file and run the build script, transforming the code, bundling the project, and emitting the necessary files. The tests execute on compiled code in Node, and the NPM folder contains the package files. Finally, we publish the package to NPM and import it in package JSON.
First one being entry points. This tells dnt what files it should transpile. So mod.ts is the entry point. It tells it what directory to output into dot slash NPM. And I told you earlier, it can automatically inject shims or poly fills for API's that aren't available in node.
So for example, the Deno namespace is not available in node, right? But to get our tests to run, we need to inject the deno.test API, because it doesn't exist in node. So what we'll do is we'll shim out the Deno namespace in dev mode. So dev is when you compile with tests. Yeah, so we'll shim that out. So the tests run, and then we can also specify our package.json properties here. So the name of the package, the version that we're going to publish, we'll grab that from the command line arguments, the description of the package and the license.
And then last thing we're going to do is we're going to copy the readme file from the root here into the npm directory. So now we have our build script written, we can run deno run .a build script or build.ts. And this will transform the code. It'll run NPM install, it will bundle the project, it'll type check it, it'll emit the TypeScript declaration files, it'll emit the ESM package, the package using require. So it still works in old versions of Node even because it supports require as well. And then it runs the tests. So these are all the tests we wrote earlier. These tests are executing on the compiled code in Node. So there's no deno involved here, this runs in Node. You can see the tests pass, it prints out complete, and it created this NPM folder here. So this NPM folder has our package.json file, it has our type declarations, it has the require version of our module, it has the ESM version of our module all in here. So now we want to publish this NPM. We're gonna cd NPM. And then we're gonna run NPM publish. And then what I will need to do is I will need to grab myself a one-time password from NPM, which I have now. And boom, package published. Let's go back to Chrome and go to the NPM registry, refresh here, and you can see version 0.1.1 has been published a few seconds ago. Yeah, published NPM now. You can import this in your package JSON and use it.
10. Summary of Building and Publishing
We've built a library in TypeScript that runs in Dino, Node, and browsers. We added and ran tests in Dino and Node. We set up linting and formatting. We can publish to Dino Linux and NPM. We used the Dino CLI, VS Code extension, and DNT, all provided by Dino. It's a fully integrated tooling system.
Cool. So what have we done today? We've built a library written in TypeScript that runs in Dino, Node, and the browsers. We added and ran tests, both in Dino and in Node. We set up linting and formatting. We can publish to Dino Linux and NPM, our package, so that it can be used by both Dino users and Node users. Most importantly, we did not use any tooling which was not provided by the Dino project. We used the Dino CLI, we used the VS Code extension for Dino, and we used DNT. All of those are provided by the Dino project. So it's like this fully integrated tooling system.