DEV Community

Cover image for How To Bundle Code For Deno Web Applications
Craig Morten
Craig Morten

Posted on • Edited on

How To Bundle Code For Deno Web Applications

My a previous post on writing a React SSR app in Deno I covered how you could write a server-side rendered React app making use of JSPM and by serving client bundles using template literals.

Since the post was written over a month ago, the Deno team have released a fleet of features and bug fixes which means we can now do client-side script bundling in a far more elegant way, and is likely far closer to your current workflows in Node projects.

In this post I'll cover the deno bundle command and the Deno Compiler API, and guide you through how we can use these features to create a working React SSR application, complete with a bundled client-side script.

The Deno bundle command

Deno comes with it's own bundling capability built into the CLI.

$ deno bundle [OPTIONS] <source_file> [out_file]
Enter fullscreen mode Exit fullscreen mode

This can be used to output a single JavaScript file, which includes all dependencies of the specified input.

That is, this command will include your module as well all of the sub-modules that your code imports, including remote modules importing using a URL.

For example, let's create a simple Deno script helloDeno.ts:

import { bgBlue, red, bold, italic } from "https://deno.land/x/std@0.65.0/fmt/colors.ts";

console.log(bgBlue(italic(red(bold("Hello Deno!")))));
Enter fullscreen mode Exit fullscreen mode

We can run this normally using deno run helloDeno.ts:

$ deno run ./helloDeno.ts 

Hello Deno!
Enter fullscreen mode Exit fullscreen mode

Where we should see a horribly unreadable Hello Deno! written in red on a blue background 😂.

Now let's check out what the deno bundle command does! We can call it with our helloDeno.ts file and provide a target output helloDeno.bundle.js. If you don't provide an output it will print to stdout on your console.

$ deno bundle ./helloDeno.ts helloDeno.bundle.js
Bundle ~/helloDeno.ts
Emit "helloDeno.bundle.js" (9.37 KB)
Enter fullscreen mode Exit fullscreen mode

You should now have another file in your directory called helloDeno.bundle.js 🎉. I encourage you to open it and have a quick read through - it's complicated, but within the system registering code you should be able to find what you wrote! It will look something like:

// ... rest of the code

execute: function () {
    console.log(colors_ts_1.bgBlue(colors_ts_1.italic(colors_ts_1.red(colors_ts_1.bold("Hello Deno!")))));
}

// ... rest of the code
Enter fullscreen mode Exit fullscreen mode

If you look closely you should also be able to find all of the code from the https://deno.land/std/fmt/colors.ts module we imported - as promised it has bundled all of our code, including sub-modules.

We can check that it runs by using the Deno CLI again:

$ deno run ./helloDeno.bundle.js                
Hello Deno!
Enter fullscreen mode Exit fullscreen mode

This time you should notice that the execution is nearly instant! Because we have already bundled the Deno code down to a single JavaScript file we no longer have the overhead of having to run the TypeScript compiler and fetch remote modules etc. Deno can just get on with running the code!

You could now use this command to create bundled code as part of your CI / CD pipeline for client-side assets.

The Deno Compiler API

Deno also offers bundling methods as part of it's core runtime Compiler API.

Please note that this API is currently unstable. To learn more about using unstable APIs and how to use them, check out the Deno stability documentation.

This API supports three different methods built into the Deno namespace that provide access to the built-in TypeScript compiler. These are:

  • Deno.compile() - Similar to deno cache. It can fetch and cache the code, compile it, but does not run it. Returns diagnostics and a map of compiled filenames to code, but does not create any files - you need to perform this yourself.
  • Deno.bundle() - This works a lot like deno bundle. It is also very close to Deno.compile(), but instead of returning a map of files to code, it returns a single string which is a self-contained ES module.
  • Deno.transpileOnly() - Based off of the TypeScript function transpileModule() and simply converts the code from TypeScript to JavaScript and returns the source and a source-map.

Let's see how the first two could work with our simple helloDeno.ts script! (We don't cover Deno.transpileOnly(), but it is very similar to the other two).

Deno.compile()

Create a file called compile.ts and add the following:

const [diagnostics, emitMap] = await Deno.compile(
  "./helloDeno.ts",
);

console.log(emitMap);
Enter fullscreen mode Exit fullscreen mode

You can then run the compile script using the following command:

$ deno run --unstable --allow-read --allow-net ./compile.ts
Enter fullscreen mode Exit fullscreen mode

Note the various permissions we need to add. We are reading a local file helloDeno.ts which requires --allow-read, fetching a remote file for colours which requires --allow-net and using an unstable API which needs --unstable.

You should then see in your console something that looks like:

{
  https://deno.land/std/fmt/colors.js.map: '{"version":3,"file":"colors.js","sourceRoot":"","sources":["colors.ts"],"names":[],"mappings":"AAAA,...',
  https://deno.land/std/fmt/colors.js: "// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license.
/** A module to print ANS...",
  ~/helloDeno.js.map: '{"version":3,"file":"helloDeno.js","sourceRoot":"","sources":["helloDeno.ts"],"names":[],"mappings":...',
  ~/helloDeno.js: 'import { bgBlue, red, bold, italic } from "https://deno.land/std/fmt/colors.ts";
console.log(bgBlue(...'
}
Enter fullscreen mode Exit fullscreen mode

It has successfully compiled our code and generated a map of filenames to JavaScript code and source-maps.

Deno.bundle()

Let's now create a bundle.ts file and add the following:

const [diagnostics, emit] = await Deno.bundle(
  "./helloDeno.ts",
);

console.log(emit);
Enter fullscreen mode Exit fullscreen mode

This should look very similar to our compile.ts script! But if we now run it, we should see something very different in our console:

$ deno run --unstable --allow-read --allow-net ./bundle.ts

// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license.

// This is a specialized implementation of a System module loader.

"use strict";

// @ts-nocheck
/* eslint-disable */
let System, __instantiate;
(() => {
  const r = new Map();

// ... rest of the code
Enter fullscreen mode Exit fullscreen mode

It has printed a single string of the bundled code to stdout which matches the exact output of running the deno bundle command earlier. In fact, we can recreate the deno bundle command by writing the value of emit to a file, e.g.:

const [diagnostics, emit] = await Deno.bundle(
  "./helloDeno.ts",
);

await Deno.writeTextFile("./helloDeno.bundle.v2.js", emit);
Enter fullscreen mode Exit fullscreen mode

Executing the script again, but this time with the --allow-write permission will result in a file called helloDeno.bundle.v2.js being created with all the bundled code. 🎉

$ deno run --unstable --allow-read --allow-net --allow-write ./bundle.ts
Enter fullscreen mode Exit fullscreen mode

Writing a React SSR app with client JS bundling

Let's now look at how we could use these compiler APIs within our application by reviewing this Opine React example.

If we look at client.tsx we can see that it is the entrypoint to the client-side JavaScript, and is responsible for hydrating the React application into an element with id set to root.

import React from "https://dev.jspm.io/react@16.13.1";
import ReactDOM from "https://dev.jspm.io/react-dom@16.13.1";
import { App } from "./components/App.tsx";

(ReactDOM as any).hydrate(
  <App />,
  // @ts-ignore
  document.getElementById("root"),
);
Enter fullscreen mode Exit fullscreen mode

The App that is referenced is found in a components folder, and this creates a simple React application with some sub-components rendered using React Suspense.

// @deno-types="https://raw.githubusercontent.com/Soremwar/deno_types/4a50660/react/v16.13.1/react.d.ts"
import React from "https://dev.jspm.io/react@16.13.1";
import { Title } from "./Title.tsx";
import { List } from "./List.tsx";

export const App = ({ isServer = false }) => {
  if (isServer) {
    return (<>
      <Title />
      <p className="app_loading">Loading Doggos...</p>
    </>);
  }

  return (<>
    <Title />
    <React.Suspense fallback={<p className="app_loading">Loading Doggos...</p>}>
      <List />
    </React.Suspense>
  </>);
};
Enter fullscreen mode Exit fullscreen mode

By making use of the // @deno-types ... compiler hint we can also make use of community written types for popular modules such as React.

If we now move to the server.tsx file, we can see that this is intended to be the main entrypoint of the application. If you look at the top of the file you might see some code that looks very familiar!

import { opine, serveStatic } from "../../mod.ts";
import { join, dirname } from "../../deps.ts";
import { renderFileToString } from "https://deno.land/x/dejs@0.7.0/mod.ts";
import React from "https://dev.jspm.io/react@16.13.1";
import ReactDOMServer from "https://dev.jspm.io/react-dom@16.13.1/server";
import { App } from "./components/App.tsx";

/**
 * Create our client bundle - you could split this out into
 * a preprocessing step.
 */
const [diagnostics, js] = await Deno.bundle(
  "./examples/react/client.tsx",
  undefined,
  { lib: ["dom", "dom.iterable", "esnext"] },
);

if (diagnostics) {
  console.log(diagnostics);
}

/**
 * Create our Opine server.
 */
const app = opine();
const __dirname = dirname(import.meta.url);

// ... rest of the code
Enter fullscreen mode Exit fullscreen mode

The first thing the server code does is use the Deno.bundle() method to create a single js bundle using the client.tsx file as the entrypoint. You can then see further down in the script that this JavaScript is then served on a /scripts/client.js path:

// ... rest of the code

/**
 * Serve our client JS bundle.
 */
app.get("/scripts/client.js", async (req, res) => {
  res.type("application/javascript").send(js);
});

// ... rest of the code
Enter fullscreen mode Exit fullscreen mode

If you were looking closely, you may have noticed that the code has also passed some extra parameters to the Deno.bundle() method which we haven't covered yet! It turns out there are some additional optional parameters which you can make use of.

Deno.bundle(rootName [, sources] [, options])
Enter fullscreen mode Exit fullscreen mode

This code example doesn't make use of the sources option, but you can check out how it works in the Deno documentation.

What is provided is the last options argument. This is a set of options of type Deno.CompilerOptions, which is a subset of the TypeScript compiler options containing the ones which are supported by Deno.

This application makes use of the lib option which allows you to define a list of library files to be included in the compilation. This means you can define the libraries required for the specific destination(s) of your code, for example the browser where you would usually define something like:

const [diagnostics, emit] = await Deno.bundle(
  "main.ts",
  {
    "main.ts": `document.getElementById("foo");\n`,
  },
  {
    lib: ["dom", "esnext"],
  }
);
Enter fullscreen mode Exit fullscreen mode

In the above snippet we tell Deno to bundle a script called main.ts, which in this instance is defined using the sources option opposed to using an existing file, and some additional compiler options which tell the compiler that the intended target requires DOM library and ESNext support.

If you want to learn more about compiler options you can find more details in the TypeScript compile options documentation.

Running the React application

So having covered the main aspects of the code (I encourage you to read through the rest to see how it all works!), let's run the example and see the results!

First we need to clone the Opine repo locally, for example:

# Using SSH:
git clone git@github.com:asos-craigmorten/opine.git

# Using HTTPS:
git clone https://github.com/asos-craigmorten/opine.git
Enter fullscreen mode Exit fullscreen mode

If then make the repository our currently working directory (e.g. cd opine) we can then run the command provided in the example's Readme:

$ deno run --allow-net --allow-read --unstable ./examples/react/server.tsx

Check ~/opine/examples/react/server.tsx
Opine started on port 3000
Enter fullscreen mode Exit fullscreen mode

If we open our browser to http://localhost:3000 we can see that our application has started and running successfully! 🎉

Deno React example running in the browser

And if we open our developer tools, we can see in the Network tab that the application is successfully fetching the bundled client.js file from the server and using it to run React client-side.

Developer tools Network tab with successful client.js request

Congratulations, you've managed to run a relatively complicated application (it's using Suspense for data-fetching!) using Deno and it's bundling capabilities! 🎉 🎉

This particular example has opted to run the bundler when the server starts. In production it is more likely you will run the compilation / bundling as a pre-requisite step within your CI / CD, but all the concepts are the same as we have covered!


I hope this was useful!

One thing to note is that these APIs are still marked as unstable, so you may find they change and / or break from one release to the next, however they are now in a reasonably good state so that is unlikely! If you want to be cautious, the CLI commands are stable so you can always fall back to using deno bundle instead of writing JavaScript code for the compilation.

Let me how you're managing your client-side code and bundling in Deno! Doing something different, or found a third party bundler that works really well? I'd love to hear about it in the comments below!

Till next time! 🦕

Top comments (18)

Collapse
 
svdoever profile image
Serge van den Oever • Edited

@craigmorton, this looks really promising! I cloned the opine repo and executed:

deno run --allow-net --allow-read --unstable ./examples/react/server.tsx`

But I'm getting the error:

error: Import 'deno.land/x/types/react/v16.13.1/r...' failed: 404 Not Found
Imported from "file:///C:/p/opine/examples/react/components/App.tsx:2"

I'm new to Deno, and have googled, searched on deno.land, but have no clue how to solve this... Any help would be appreciated!

I'm running on version 1.3 of Deno...

Collapse
 
craigmorten profile image
Craig Morten

Hey Serge 👋

Sorry it's not working for you! I'll take a look now and see if can reproduce or suggest a fix 😄

Collapse
 
craigmorten profile image
Craig Morten

Ah it seems React types were removed from the deno.land/x registry and I didn't notice -_- (see discord.com/channels/6848986651432...), I'll have a look at pushing a fix using denopkg.com/Soremwar/deno_types/re... instead 😄

Thread Thread
 
svdoever profile image
Serge van den Oever

Hi Craig, thank for looking into the issue. Would be great if it could be fixed. I wrote a solution for server-side rect rendering with async calls for data a few years a go based on Hypernova that is used in production on anwbcamping.nl and available in github.com/macaw-interactive/react... (latest version available in the features/serveroute branch). This morning we were discussing the possibility to create a version based on react-suspence, running in an Azure function based on Deno, and then I found your work! So looking forward to investigate possibilities and try to adapt it to our use-case!

Thread Thread
 
craigmorten profile image
Craig Morten • Edited

Heya, so think I've got something working again and have updated the article and the Opine React SSR example.

The Deno types directive didn't seem to play nicely with the denopkg.com proxy so in the end directly referencing the types via the GitHub raw URL did the trick.

Give pulling the latest down and let me know how get on! 😄

Thread Thread
 
svdoever profile image
Serge van den Oever

Application starts up, but on localhost:3000 I get the following error:

TypeError: Cannot read property 'req' of undefined
at redirect (serveStatic.ts:220:39)
at serveStatic (serveStatic.ts:124:14)
at async Layer.handle as handle_request

Thread Thread
 
craigmorten profile image
Craig Morten

Oh dear, sounds similar to github.com/asos-craigmorten/opine/... which has cropped up for someone else today :/ haven’t been able to reproduce just yet so not sure what is causing it!

Thread Thread
 
svdoever profile image
Serge van den Oever

That looks very similar;-)

Thread Thread
 
craigmorten profile image
Craig Morten

Hey Serge, can you try with the --reload flag to see if it's a cached issue that has been resolved? Opine 0.21.0 has just been released for Deno 1.3.0 so might be good to check with that as well 😄

Thread Thread
 
svdoever profile image
Serge van den Oever

I run from the cloned version of the Opine...is that version 0.21.0? The --reload flag didn't work. Can you reproduce the issue? Or does it work on your side? What version of Deno are you running? 1.3.0 as well?

Thread Thread
 
craigmorten profile image
Craig Morten

Unfortunately I’m still unable to reproduce - I am on Deno 1.3.0, see in github.com/asos-craigmorten/opine/...

Can you confirm whether you are using windows? Wondering if is an OS based issue

Thread Thread
 
svdoever profile image
Serge van den Oever

Yes I'm running on Windows 64 bit version 1909... I will ask a colleague tomorrow to try it out as well...

Thread Thread
 
craigmorten profile image
Craig Morten

Cool, will get a VM booted up and see if it’s a windows issue 😊

Thread Thread
 
craigmorten profile image
Craig Morten

We've managed to confirm that this is definitely an issue with Opine that arises for Windows only users 🤦

I think I might have solved it in opine@0.21.2 (see github.com/asos-craigmorten/opine/...) but it would be super awesome if you (or anyone!) can confirm whether it is working as expected? 😄

Collapse
 
michaelcurrin profile image
Michael Currin • Edited

Some corrections

-make use community 
+make use of community
-:tada 
+:tada:
Enter fullscreen mode Exit fullscreen mode

And use sh for shell formatting markdown.

```sh
deno run ...
```
Enter fullscreen mode Exit fullscreen mode
Collapse
 
michaelcurrin profile image
Michael Currin • Edited

Thanks. I was looking for React client code. Unfortunately with and without hydrate I still get errors that # private symbol and Deno is not valid, when I load my bundle in the browser.

Collapse
 
michaelcurrin profile image
Michael Currin

It works for me to use .hydrate or .render

I fixed my errors by the way. Importing from CDNs alone was fine. Importing from Deno modules caused my bundle to include things not compatible in the browser.

Collapse
 
michaelcurrin profile image
Michael Currin

I have some guides for using bundle and compile on the CLI

michaelcurrin.github.io/dev-cheats...