Publishing Deno modules on Netlify

Runtimes like Node.js rely on a package manager and a registry to install and distribute modules, but Deno has a different spin. It allows developers to import modules directly from a URL, which can be hosted on a CDN, your own server, or really anywhere on the web.

This level of flexibility brings infinite options, so I started looking for the best workflow to release modules in this new paradigm. This article describes the solution I landed on — one that works for solo open-source developers or large development teams in the enterprise world.

Here's the gist of it:

  • The source code lives in a GitHub repository
  • Releases are automated using a combination of Conventional Commits and Release Please
  • The repository is connected to a Netlify site, which is responsible for serving the modules in version-locked URLs, protecting private modules, serving documentation pages, and a few other niceties

Let's get started.

Setting up

The first step is to create a GitHub repository with a few things:

  • A JavaScript or TypeScript entry point for the module. This is the file that people will import into their applications.
  • A workflow file for Release Please, which will automate the release process using GitHub Actions.
  • A Netlify Build Plugin for generating redirects to version-locked URLs.
  • A Netlify configuration file, setting the right directories, build command, and response headers.

Next, we connect the repository to Netlify.

Alternatively, you can use the button below to create everything with one click.

Deploy to Netlify

Finally, we can change the site name to something a bit more memorable. You can do this by going to Site settings > General > Change site_name. I picked deno-greeter.

We're now ready to use the module in a Deno program.

$ deno repl --eval "import { greet } from 'https://deno-greeter.netlify.app/mod.ts'"
Download https://deno-greeter.netlify.app/mod.ts
Download https://deno-greeter.netlify.app/greetings.ts
Deno 1.24.3
exit using ctrl+d or close()
> greet("Jane")
"Good morning, Jane!"
>

Versioning

We could start using our module as is, but we're missing an important part: versioning.

If we want to release a new version that introduces breaking changes, consumers should be able to decide whether and when to update their code.

Unlike Node.js, Deno does not use a package.json file to pin dependencies to specific versions. Instead, the import URLs themselves are expected to point to an immutable version of the module. When the code changes, the URL must also change.

We can achieve this pretty easily with Netlify, as you can configure your site to create a new deploy when a new Git tag is published. If we then configure Release Please to create a new tag for every release, we'll get distinct URLs for each new version of the module.

To do this, open the Netlify dashboard and navigate to Site settings > Build & deploy > Branches and select All.

Configuration of branch deploys in Netlify
Configuration of branch deploys in Netlify

To test the release flow, make some changes to the module code, push a commit using the feat: prefix, and open a pull request. Once you merge it, Release Please will create a release pull request automatically. Merging it will complete the release.

If you go to the Deploys page of the Netlify dashboard, you'll see a new deploy in progress. Once it finishes, you'll see that deno-greeter is available at https://deno-greeter.netlify.app/1.0.0/mod.ts — this is an immutable URL that points to version 1.0.0 of the module, and will be unaffected by future versions.

This process will happen automatically for every new pull request that you merge. New versions of the module will respect Semantic Versioning and will be inferred automatically from the Conventional Commits convention prefixes used in your commits:

  • fix: will generate a patch version
  • feat: will trigger a minor version
  • feat!: signals a major version with breaking changes

Private modules

The current setup works great for public modules, but you might want to restrict access to our module to people with the right credentials. This is a very common scenario in an enterprise setting, where teams of developers want to share common pieces of proprietary code without making it accessible to the outside world.

To do this, we can leverage Netlify's password protection feature. We start by creating a _headers file in the src directory with the following contents.

/*
Basic-Auth: janedoe:supersecret123

This protects your site with a username and password combination, leveraing basic HTTP authentication.

To use the module in their applications, consumers must set a DENO_AUTH_TOKENS environment variable with the right credentials when running Deno CLI commands.

DENO_AUTH_TOKENS=janedoe:supersecret123@deno-greeter.netlify.app

You can read more about private modules in Deno and explore more advanced authentication mechanisms offered by Netlify.

Documentation site

Using a full-fledged website deployment platform to host your modules comes with a few extra perks. For example, if you want to create a documentation site for your project, you don't need any additional configuration or tooling. You can place the HTML files in the src directory and Netlify will serve them on the same URL.

If you want to something like Docusaurus to build your docs, or even a full-fledged web framework like Gatsby or Next.js, you totally can.

And because the site is deployed alongside the module's code, they will be versioned together. So if someone is using version 1.0.0 of your module, they can find the documentation for that specific version at https://deno-greeter.netlify.app/1.0.0.

Hosted version

Because we're using a Netlify site, we have a series of primitives at our disposal, including Edge Functions.

Edge functions are themselves based on Deno, so we can easily write one that imports our module and uses it to produce a response to an HTTP call. This lets us create a hosted version of our module, which applications can interact with by issuing an HTTP request rather than an in-memory import.

This can work as an alternative interface for applications that are using another JavaScript runtime or a different programming language entirely.

import { greet } from "../../src/mod.ts";
export default async (req: Request) => {
const url = new URL(req.url);
const name = url.searchParams.get("name");
const greeting = greet(name);
return new Response(greeting);
}

By deploying this edge function, deno-greeter can be accessed at https://deno-greeter.netlify.app/api?name=Jane.

Parting thoughts

This is admittedly not be the most impartial technical piece ever, since I work at Netlify. I did honestly start from the premise of finding the best workflow for me, which I decided to share. So while there are many options out there, I struggled to find one that packs this much functionality with such simplicity.

With the «this is not a Netlify marketing piece» caveat out of the way, I would love to hear about the workflow that works for you. If you end up using the same as mine, definitely hit me up! ∎