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.
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.tsDownload https://deno-greeter.netlify.app/greetings.tsDeno 1.24.3exit 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.
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 versionfeat:
will trigger a minor versionfeat!:
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! ∎