Embed a Vite SPA in a binary with Deno


A photo of myself sat on the green cliffs of Cornwall in the UK with the ocean & sky in the background.

I have always kept a close eye on Deno as they really seem to be going for the “Let’s redo Node.js & fix all of its shit” mentality and I love it.

But in keeping an eye on the progress of the project, one key feature stood out to me that intrigued me: deno compile.

What is deno compile?

In short, it allows you to take any JavaScript or TypeScript you have written & create a single executable binary that you can run anywhere to run your code.

It does this by embedding a slimmed down version of the Deno runtime along with your JavaScript or TypeScript code.

This is perfect for scenarios where you want to ship code to the end user but don’t want to have to ask them to install Deno, etc. It’s one executable binary that does everything.

If you want a more in depth look into what deno compile is or how it works, I highly recommend reading either the documentation on it, or this blog post breaking down a full example of how to use it.

But for now, let’s get to actually building something with it.

Installing Deno

If you haven’t already you’ll need to go ahead and install Deno verison 2.1 or newer.

To do this I would recommend following the official “Getting Started” documentation for the most up to date information. Or if you’re too lazy, here is the short version:

Windows PowerShell
irm https://deno.land/install.ps1 | iex
macOS
brew install deno
Linux
curl -fsSL https://deno.land/install.sh | sh

And to check it’s all working and installed, run the following to get the version of Deno you have installed:

deno --version

Great, now let’s set up the project to house all of our code.

Setting up the project

To get started, let’s create a new Vite app with Deno support.

Thankfully there is a helpful package out there to help us do this called create-vite-extra.

deno run -A npm:create-vite-extra --template deno-react-ts

With that done we now have a basic Vite app that can be run with Deno & lets us use React & TypeScript.

To test it all works run the following command to start the local development server:

deno task dev

Once the server is running you should be able to visit http://localhost:5173/ to see the following:

A of a browser showing the Vite + React + TS page

With that done, we can make a few tweaks to our TypeScript setup & then get on with building our application to run this SPA.

Configure TypeScript

Now, the template we’re using doesn’t enable Deno’s namespace types by default. Without this we will get some errors later on when we try to compile the binary.

To fix this we will need to add "deno.ns" to the compilerOptions.lib array in the deno.json file.

deno.json
{ "compilerOptions": { "lib": ["dom", "dom.iterable", "esnext", "deno.ns"] // ... } }

Create a static file server

Next up, we need to create a basic static file server to serve the files to the user.

For this example I’ll be using Hono as it’s both easy to use and works well with most runtimes, including Deno.

To install Hono, run the following command:

deno add jsr:@hono/hono

This will add the JSR @hono/hono package to your import map inside deno.json.

From there let’s create a main.ts file in the root of the project with a basic Hono application.

main.ts
import { Hono } from '@hono/hono'; import { serveStatic } from '@hono/hono/deno';   const app = new Hono();

With a Hono app created, we can add the routes to it needed in order to serve the static files.

main.ts
// Serve static files from the Vite build directory app.use('/*', serveStatic({ root: `${import.meta.dirname}/dist` }));   // Fallback for SPA routing app.get('*', (c) => c.html(`${import.meta.dirname}/dist/index.html`));

And lastly we need to tell Deno to actually start a server and listen on a port.

main.ts
Deno.serve(app.fetch);

Running the server

With the static file server set up, we can now check to see if it actually works.

To do this we will first need to build the Vite app so that we have the static files to serve.

deno task build

This will output the static files into the dist directory.

Then we can then finally start the server by running the following command:

deno run -A main.ts

When it starts you should be able to visit: http://0.0.0.0:8000/ & see the same page as before.

Compiling the binary

Now we get to the fun part of embedding all of this inside a single executable binary.

To do this we can use the deno compile command with a few extra flags.

deno compile \ --allow-net \ --allow-read \ --include index.html \ --include dist/ \ --output main \ main.ts

Let’s break this down a bit:

  • --allow-net - This flag allows the binary to make network requests.
  • --allow-read - This flag allows the binary to read files from the filesystem.
  • --include index.html - This flag tells Deno to embed the index.html file in the binary.
  • --include dist/ - This flag tells Deno to embed the dist directory in the binary.
  • --output main - This flag tells Deno to name the output binary main.

Now that you know a bit more of what is being run, let’s run it!

Once finished, Deno will show you a file tree breaking down all of the files that have been embedded inside the binary. Which in this case will be a lot as we’re including everything in the dist directory.

To start the server, you can run the following command:

./main

With the server running you should be able to visit http://0.0.0.0:8000/ & see the same page as we did when running the development server.

Conclusion

And that’s it! With that you now have a single executable binary that contains all embedded inside it.

Feel free to move the binary somewhere else on your machine and run it there. It should work just the same as it did when you ran it on your machine.

Or if you want to run it on a different system, I recommend looking into cross-compilation.