Hey there, TypeScript developers! If you’re like me, you crave the secure and sandboxed environment that Deno provides. Deno also has fantastic out-of-the-box TypeScript, linting, formatting, testing, benchmarking support that makes development a breeze and frees you and your team from decision overload and multiple tool configuration paralysis. But what if you’re working on a library that needs to also support Node.js users? Fear not, because in this blog post, I’ll show you how to seamlessly maintain a TypeScript library using Deno as your primary toolchain while still ensuring compatibility with Node.js consumers.
Who Is This For?
This article is specifically designed for developers who build libraries for Node.js users but prefer the development experience offered by Deno. You might be wondering, “Can Deno handle my Node-isms?” Well, fear not! Deno has got your back. Head over to this page for more specifics on Deno’s compatibility with Node.js.
Before We Dive In (Prerequisites)
First, let’s make sure you have the necessary tools available in our environment. Here’s what I’ll use:
- Node.js (Let’s say version 18.x, representative of your user base during local testing)
- Deno (version 1.34+)
- Esbuild (the shiny new version) - Please note that the old
deno bundle
command has been deprecated.
In the next section I will talk about setting this up using the Nix package manager . If you aren’t interested in that you can skip and set the above up in your choice of package manager.
Nix Setup (optional)
If you’re eager to get started right away and want to set up a quick ephemeral development environment using the Nix shell, try this command:
nix-shell -I nixpkgs=channel:nixos-23.05 -p nodejs-18_x deno esbuild
The versions you’ll get from this command (or similar) should be something like:
[nix-shell]$ node --version
v18.16.0
[nix-shell]$ deno --version
deno 1.33.3 (release, x86_64-unknown-linux-gnu)
v8 11.4.183.2
typescript 5.0.4
[nix-shell]$ esbuild --version
0.17.19
These are the versions I used for our uber simple demo, but feel free to adapt to your preferred setup.
If you want a Nix flake for your project after trying this out, try using the following flake.nix
:
{
description = "TypeScript library dev, Deno/Node workflow";
nixConfig.bash-prompt = "\[ts-deno-node\]> ";
inputs = {
nixpkgs.url = "github:nixos/nixpkgs/release-23.05";
flake-utils.url = "github:numtide/flake-utils";
};
outputs = { self, nixpkgs, flake-utils }:
flake-utils.lib.eachDefaultSystem (system:
let
pkgs = nixpkgs.legacyPackages."${system}";
inherit (pkgs) mkShell;
in
{
overlay = final: prev: {
};
devShell = pkgs.mkShell {
buildInputs = with pkgs; [ deno nodejs-18_x esbuild ];
};
}
);
}
Once you have the flake.nix
in the root of your project repo and added to your git repository you can run nix develop
in the root dir. If you have direnv
installed add a .envrc
with the contents use flake
in it then direnv allow path/to/project/root/here
. This will automatically always load the flake when you enter the project directory.
The purpose of the Nix flake it to make the core tool dependencies reproducibly setup each time you work in the library. When you bootstrap the Nix environment the firs time, Nix will create a lock file for your flake so until you update the lock file or Nix flake file, you will be using the same versions!
Setting Up Environment Variables
Let’s get your environment ready by setting the NPM_CONFIG_REGISTRY
environment variable. This variable allows you to customize the URL for the npm registry. Just run the following command:
export NPM_CONFIG_REGISTRY=https://registry.npmjs.org
Let’s Build a Test Library
To illustrate our workflow, we need a small “library” that we can test and bundle for Node.js. Here’s a TypeScript library of exported validation functions to help us demonstrate the validity of the workflow on a small surface area:
import url from 'node:url';
export function isValidSlug(slug: string): boolean {
return slug.length > 0;
}
export function isValidId(id: number): boolean {
return id > 0;
}
export function isValidTitle(title: string): boolean {
return title.length > 0;
}
export function isValidUrl(url: string): boolean {
return url.parse(url).ok;
}
Don’t worry too much about the code logic here; it’s just a simple example to demonstrate the process. We have a few functions that perform basic validations that are exported using ES Modules.
Some quick notes:
- we are leveraging ECMAScript Modules in the source code which works in Deno out of the box
- we are importing the
node:url
API and it will run on Deno and in most cases the behavior will be exactly as it is on Node.js for Node-specific APIs - To import NPM dependencies, we can use the following import form:
import { option, either } from "npm:fp-ts@^2.16";
. Read more here about npm: import specifiers . - if you want to manage versions centrally across all source files, Deno supports import maps, again out-of-the-box. You would then define an import map key for the dependency (
fp-ts
in our example) and use an import like the following in relevant source files:import { option, either } from "fp-ts"
then we add the entry"fp-ts": "npm:fp-ts@^2.16"
to theimports
JSON map in your project’s import map file. I usually keep mine atdeno.json
in the root of my project/package. Read more here about import map support in Deno .
Testing the Library on Node.js
Now that we have our library, it’s time to test it on Node.js. We’ll create a test suite using the built-in assert module. Here’s an example:
const assert = require("node:assert");
const lib = require("./lib");
assert.equal(true, lib.isValidId(1));
assert.equal(false, lib.isValidId(-1));
assert.equal(false, lib.isValidSlug(""));
assert.equal(true, lib.isValidSlug("hello"));
console.log("All library tests passed!");
We can save this code in a file called libtest.js. When we run it with Node.js, we should see the following output:
$ node libtest.js
All library tests passed!
Perfect! Our library is working as expected in the Node.js environment.
Bundling with Esbuild
Before we continue running tests on Node.js, we need to generate a bundled version of our library specifically for the Node.js platform. To achieve this, we’ll use esbuild , a fast and efficient bundler. Execute the following command:
esbuild --bundle ./lib.ts --platform=node > lib.js
Now, when we run the tests again, we’ll get the expected output:
$ node libtest.js
All library tests passed!
Fantastic! Our bundled library is fully compatible with Node.js.
Why Bother with Deno?
You might be wondering, “Why go through all this trouble when I can stick with Node.js for library development?” Well, my friend, there are several advantages to embracing Deno alongside Node.js:
- Sandboxed Development Environment
- Deno provides a secure sandboxed environment, which is excellent for local development. By delegating Node.js testing to your CI/CD pipeline, you can keep your development environment contained and avoid any unforeseen issues.
- ES Modules Out of the Box
- Deno supports ECMAScript Modules (ESM) at the source level, without any additional configuration. This means you can utilize modern module syntax without tinkering with package.json or webpack configurations.
- Seamless NPM Package Imports
- Deno makes importing NPM packages a breeze. You can simply use a syntax like this: import { option, either } from “npm:fp-ts@^2.16”; Deno takes care of handling the package resolution for you.
- Effortless Version Management
- Deno introduces import maps, which allow you to manage versions across multiple source files effortlessly. By creating an import map JSON file, such as imports.json, in your project/package root directory and adding an key-value entry like
"fp-ts": "npm:fp-ts@^2.16"
to the imports map, you can ensure consistent versioning. When running your program, you can refer to the import map file using the--import-map ./import.json
option or embed your import map in the project’sdeno.json
and simplify with good defaults. - Enhanced Developer Ergonomics
- Deno offers a superior developer experience compared to Node.js. It provides TypeScript support out of the box, eliminating the need for extensive tsconfig.json configurations. Deno’s LSP (Language Server Protocol) enables powerful code editing features, and you can format and check your codebase using Deno without relying on additional tools. Deno even includes a benchmarking and testing library in its stdlib, saving you from adding extra dependencies. Deno’s default settings cover most scenarios, and if you need to override them, simply create a
deno.jsonc
file with the desired settings. Plus, it supports comments! Some keyfmt
config options I override arelineWidth
(120, hey we have bigger screens now than the 1970s),singleQuote
(true). Consult the configuration reference docs . - Cross-Platform Binary Building
- Deno allows you to build cross-platform command-line tools directly from your codebase using the deno compile command. Unlike Node.js, where this process can be quite complex, Deno simplifies it, saving you time and effort.
- Testing Fixes on Node.js
- Occasionally, you may need to test a fix specifically on Node.js. With esbuild’s watch capabilities, you can easily iterate and test changes in a tight loop on the Node.js platform.
With all these benefits, Deno empowers you to make informed decisions, reduces unnecessary extran dependencies or additional configuration maintenance, and streamlines your project setup.
Conclusion
Cheers for making it to the end of the blog post, I am hoping you found it valuable! You’ve learned how to leverage Deno’s power while ensuring compatibility with Node.js. By embracing Deno’s sandboxed environment, seamless NPM package imports, and enhanced developer ergonomics, you can build libraries that cater to both Deno and Node.js users. So go ahead, start your deno.json
(or better yet deno.jsonc
) file (remember you can just rename imports.json
), and enjoy the freedom of a streamlined and forward-looking development workflow without the overhead of decision overload at each step of the way. Say goodbye to unnecessary decisions, tooling and twenty different configuration files at your project root directory, and let Deno do the heavy lifting for you. Happy coding!
If you enjoyed this content, please consider sharing this link with a friend, following my GitHub, Twitter/X or LinkedIn accounts, or subscribing to my RSS feed.