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
Transcription
Hey folks, I'm Luca from the deno team and I'm going to be talking to you today about writing typescript code for deno, Node, and the browser. Yeah, quickly about me, who am I? I am Luca. 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 which lets you run deno projects all across the world, close to users at the edge. You can learn more about that at deno.com. And the other thing I do is do a lot of standards/talks">web standards work. So I sit on tc39 as a delegate. tc39 is the standards committee that develops javascript, the language, and I also contribute to some W3C and Whatwig specifications. So things like fetch, the streams api, things like that is sort of the things I open issues on or write PRs to or write tests for. Yeah, so that's me. Then for all of you who aren't familiar with what deno is, let me give you a quick rundown. deno is a modern runtime for typescript and javascript. Usually we say the other way around, javascript and typescript, but this is typescript Congress, so it's for typescript and javascript. And yeah, so what you're probably most interested in is that deno runs typescript out of the box. It can run.ts files and.tsx files out of the box, no transpilation needed. You just import them and they run. And it has a bunch of built-in utilities that work great with typescript and javascript. So deno lint does some linting, deno formatting to format your code just like it would with Prettier. We have a test framework built in. We have great editor integrations, which I'll showcase later. documentation generation, there's a bunch more. Something else that deno provide 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 to 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 a 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 standards/talks">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 WebStreams 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. 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 npm. 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. 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. 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 which we've seen a lot for in the Node world. So take away from this that using typescript in deno is really boring and typescript works everywhere in deno out of the box with 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. Okay, so the library that we're actually going to write, it's 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. Yeah, and you can specify like I want a hello greeting or I want a high greeting or I want a good morning greeting or whatever you want. So that's fine. So let's actually get started with coding the library here. You'll see that I'm going to be copy pasting some code because 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 VS Code because I don't know, it's a great editor and it works really well with deno. So the first thing you want to do is if you're using deno with VS Code, you want to go to the extensions, type in deno and press install. That's that. And then every time you open a new project that you haven't used deno in before and you want to use deno in it in VS Code, you press F1 to open the command palette and then click on this deno 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? We'll say no to that. And it'll generate this VS Code folder with a settings.json file. Okay. So now that we have our editor set up, let's actually start writing code. deno projects do not require any configuration files. So the first thing that you can do when you start a new library is to start writing your code. We're going to create a new file called mod.ts. That's the entry point for our library. There's nothing special about the name mod.ts, but it's sort of like what people do with deno is they call it their library entry points mod.ts. It comes from the rust ecosystem. And then we can start writing our code. So first thing we're going to do is add this greeting enum. So this is an enum with three different enum members in it. Hello, hi, and good evening. So these are all the different greetings that you can use. Nothing really to it. And then the next thing we're going to do is we're going to add our greet function. So our greet function takes the name of the person or the thing that we're greeting and the greeting itself. And if you don't specify greeting, we'll default it to greeting.hello. And it returns a string. And this string is just the greeting space, the name, exclamation point. And then because we want nice documentation for this, we're also going to add a js.comment, which explains what the function actually does. So that's the entire extent of our library. Yeah, 16 lines, really not very much. Let's try it out. To try it out, what we're going to use is the denorepo. That's the redevelopment loop. You can open it by just calling deno on your command line, assuming you have deno installed. And you can just write your javascript or typescript code in here. 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 deno is really easy because we have a built-in testing framework called denotest. 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 denotest 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. Yeah, there's a whole section in the deno documentation, the manual, for how it works. So let's get to writing code. And 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 deno standard 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're not the same, it throws an error. And then we can start writing our tests. So to register tests, you call deno 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. And 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 deno test, is you can use, you can type in deno test in your shell, and it'll find all the test files and run them as well. And this test 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, run deno test. 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 deno test. So that's writing tests in deno, really not much to it. Yeah, so now we have tests. Let's do some other things which you probably want to do for your library project. You want to check that formatting is correct, ensure that there's a consistent formatting across the project and also across all of your projects. For that, you can use deno FMT. You know, FMT is another sub command 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 deno FMT, it all snaps it back into place to make it look good. And this is also integrated right into VS code. So if I mess this up again, right? Oh, and hello. And then right click and click format document. It'll fix this all up. And I also have format on save enabled. So I can also just press control S and it'll also format. And the formatter works for not just typescript, but it also works for javascript, Markdown, JSON and various other things. You know, also it's 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, this constant expression. And I can also check this in CI or from my shell with the dino 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 Dino users, not just node users, which we're going to look into later, but also Dino users to do that. You can actually publish it to just any web server. So Dino imports its code, as you saw here, just from URLs. So you can host code anywhere. We do provide a first, like a module registry called Dino 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 Dino land X, you can hit go to Dino land slash X. But I've already published this module. So let's look at that real quick. Dino land X slash 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 Dino land X or any website you want. You can go to Dino or doc.dinoland, enter a URL and get documentation for it. Or if you're in Dino 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. So it exports greeting enum and the greet function. The greet function has the JS doc. You can click on it to get more information and also get a link to import it. And yeah, you can do this for not just typescript code, but actually any javascript or typescript code which is available at a URL. And this is also not just usable in the browser, but it's also available on the CLI. So you can type Dino doc with a file and it will generate documentation. So the final thing before we're done here is actually publishing this to npm and running tests in Node, compiling to Node and publishing to npm. Sorry. So why do we even need to do this in the first place? So Dino supports executing typescript out of the box. Node does not. So before you can actually run this in Node, you'll need to compile it to plain javascript. And if you want to use this with typescript in Node, you will need to also emit D.ts files. So typescript declaration files. The other thing is that Node cannot import packages directly from remote URLs. Instead, you need to import them from npm. So we need to actually publish to npm. And if your library were to use some web APIs which aren't available directly in let's say, readable stream, which isn't available as a global, but you have to import this from stream slash web in Node, you'll need some polyfills. So that sounds kind of cumbersome, right? Like, how do I actually transpile all this code now? How do I get it to work? It's actually really easy. So we have this build tool also made by the Dino project called DNT, the Dino Node Transform. And it does transpilation from typescript code, from Dino typescript code to common JS and pure javascript ECMAScript modules. It can automatically replace globals which aren't available in Node with polyfills or import from Node internals. It can also automatically transpile your tests and run them in Node. And that one is really cool because it allows you to ensure that your library, even after transpilation, still works correctly. So you'll transpile your library and then you run your tests on that transpiled code inside of Node to make sure that the transpilation didn't mess up your library logic or something, that everything is still completely functional. It gives you the best of both worlds. You can use all of the built-in tooling in Dino, all of the infrastructure we provide, all of the nice editor integrations, but you can still make your modules available for users who are using Node. So how do we do that? We create a build script, underscore build.js. This is another convention. There's nothing special about this name. And in there, we will do some, well, first of all, import dnt. So dnlnx slash dnt is where dnt lives. And we'll import two functions from there. The build script is going to emit the output into a folder called npm inside of a repository or inside of our project here. So first thing we're going to do is we're going to make sure that directory is empty. And the next thing we're going to actually perform the build. So this has some options that we can specify. 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,.slash npm. And I told you earlier, it can automatically inject shims or polyfills for APIs that aren't available in Node. So for example, the dno namespace is not available in Node, right? But to get our tests to run, we need to inject the dno.test api because it doesn't exist in Node. So what we'll do is we'll shim out the dno 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 dno run 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 dno involved here, this runs in Node. And 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 to npm. We're going to cd npm. And then we're going to 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. Cool. So what have we done today, we built a library written in typescript that runs in dno, Node and the browsers. We added and ran tests both in dno and in Node. We set up linting and formatting. We can publish to dnolandx and npm our package so that it can be used by both dno users and Node users. And most importantly, we did not use any tooling, which was not provided by the dno project, right? We used the dno-cli, we used the VS Code extension for dno, and we used dnt and all those are provided by the dno project. So it's like this fully integrated tooling system. And the other thing that I hope you saw today is that using typescript in dno is really boring. There's nothing to it. It just works. So yeah, I hope that's a takeaway you can take away from this talk. If you want to get started yourself, you can install dno, go to dno.land to install. You can learn more about dno in the manual, dnoland manual. There's also a whole bunch of examples at examples.dnoland if you like learning by just reading code. You can use dnt to transpile your code for Node. If you want to learn more about that, you can go to dno.land.x.dnt. And the code for this repo is available on my GitHub at github.com.com. You can also find these slides on my website. And if you have any more questions, feel free to ask me on Twitter at lkasdev on Twitter. Awesome. I hope you enjoyed this talk, and I hope I'll see you all soon. Bye bye.