myth: You need to use
pnpm , et al. to
publish packages to https://npmjs.org
Fact: Deno is perfectly capable of maintaining NPM packages. Studies conclusively show that maintaining NPM packages with Deno instead of Node is up to 900% more stress free.1
But given this, how will we swap over to use Deno without losing what we have in terms of our existing install base of NPM packages and Node applications? Simple intuition tells us we should use Node to develop Node packages and Deno to develop Deno modules:
But as it turns out, Deno has excellent capabilities to build NPM packages that we use every day, allowing us to both have our cake and eat it too. With a slight tweak to how we think about versioning, we can get all the DX goodness of Deno, but our code is accessible to the widest possible audience of developers on both deno.land and NPM.
The key change in thinking that enables this transition is to move away from package.json as the final source of truth regarding the package version and to replace it instead with a humble git tag. Each released version will have a single tag to represent it, with the result that package.json goes from being authoritative to just another build artifact. In short, moving forward we’re following this mantra: If it doesn’t have a tag, it ain’t a release.
Tag once. Publish twice.
With that mental shift in place, we can implement the following scheme:
- Wait until a release tag is pushed
- Then use it to build and publish to both deno.land/x and npmjs.com
We’ll spend the rest of this blog post showing how we will do this for both platforms.
Stage 1: The release tag
For all projects, a release tag is a combination of a “version prefix”
followed by a semantic version number. The most common version prefix
is the letter ‘"v"’, so for example, the semantic version
1.3.5 would be
"v1.3.5" . The beta version
1.6.0-beta.3 would be tagged
"v"is the most common version prefix for a single module, monorepos that contain multiple distributed modules will need to use a more specific version prefix for tags like
“package-a-v,”which will result in release tags like
Stage 2: Build and Publish
2.1: Publish to deno.land
The deno.land build/publish is almost a gimme since it uses git tags natively. All we have to do is create and register the webhook for our module and we’re done! Moving forward, every time deno.land sees a release tag, it will snapshot our source at that tag and make that version available to users – in perpetuity.
2.2: Publish to NPM
There’s a little more work to be done getting our package onto NPM, but with a helping hand from Deno, we can do it in just two steps:
- Write a script to take a version number and build an NPM package corresponding to it.
- Implement a GitHub workflow to listen for a release tag and then invoke the script from (1) to build the package and publish it to https://npmjs.org
You might be asking yourself, “How are we going to take a bare ESM module implemented in TypeScript and just whip up a script that does all the things needed to end up with a valid NPM package?”
The short answer is that the script practically writes itself! That's because Deno has our backs with a brilliant little tool called dnt or Deno to Node Transform. dnt is downright amazing, and its capabilities are way undersold. Whether we're creating a new package that we want to publish everywhere or we are just migrating repositories from Node to Deno, dnt is the secret sauce we use that does all the heavy lifting for us.
dnt works by taking a single ES module entry point as the input and generating a feature-complete NPM package containing all its exports as the output. Along the way, it does all the terrible, tedious, un-fun, and error-prone things you hate about NPM-ing, such as maintaining the list of dependencies, compiling typescript, generating source maps and typings files, creating both commonjs as well as esm builds, and optionally vendoring any dependencies. The results work for users consuming the package from TS, JS, on the browser or the node – including every possible combination of those – and you didn't have to think about any of it. Magic!
I won’t go into all of the details because the dnt setup
instructions are very thorough, but as an example, look at
our build script at
in the GraphGen repository. It can build any version of
our package, but here’s how we would invoke it to build version
$ deno run -A tasks/build-npm.ts 1.0.0 [dnt] Transforming... [dnt] Running npm install... added 6 packages, and audited 7 packages in 1s found 0 vulnerabilities [dnt] Building project... [dnt] Emitting declaration files... [dnt] Emitting ESM package... [dnt] Emitting script package... [dnt] Complete!
Now that we have the script to build an NPM package from source, the only remaining step is to add a GitHub workflow to be able to call it. Here is an example from the GraphGen repository. Each step below is linked to the relevant section in it, but the overall sequence is:
- Listen for any tag that matches our release pattern
- Capture the target version number based off the tag that triggered the workflow
- Invoke our dnt script to build an NPM package corresponding to the captured version number
- Publish the built package to https://npmjs.org
It’s about as straightforward as a GitHub workflow can get, and with it in place, we can now publish to two locations – just by creating a tag. (Trust me, it feels even easier when you try it yourself.)
So what are you waiting for?
- Results obtained by conducting a survey of me.↩