Supporting Deno with import maps and Node compat
I wrote a static site generator called PRPL with an explicit goal of longevity. As a library of modules compiled to JavaScript, that's pretty much a moving target.
The JavaScript ecosystem is undergoing a runtime renaissance while also dealing with incompatible module definitions. Module authors wrestle with how to support:
- CommonJS and/or ECMAScript modules (ESM)
- Node and/or Deno
- Edge runtimes like Cloudflare Workers
- Browser usage
Any approach for one of these targets has implications on the other targets. It's not a straightforward situation, which is fair for an ecosystem with over a decade of history.
With PRPL I achieved CommonJS and ESM support, but it has always been a goal to also support Deno. Today I managed to do that partially via import maps and Deno's Node compatibility mode.
Prior art and first approach
I wrote most of PRPL prior to Deno v1.15, which introduced Node compatibility mode. There were discussions on the topic around GitHub back then, but it was impossible to know whether Deno would be able to achieve something like a full fledged polyfill for Node.
At the time, one of the best examples I referred to when thinking about solutions was yargs
. yargs
was able to support CommonJS, ESM, Deno and the browser by using the factory pattern. It provided unique entry points for each target and passed platform shims to a common yargs-factory.
Initially, I took a similar approach with the higher order function pattern.
Here's yargs
' Deno entry point:
;
;
;
;
Here's PRPL's initial Deno entry point (truncated to show just the interpolate
function):
;
;
;
;
;
;
;
;
;
The problem with this approach is that it is very intrusive. It required changes to nearly all files in the PRPL project, and also made it much less legible to potential contributors. Unsatisfied with this tradeoff, I dug deeper.
Import maps for Node APIs
I was aware of Deno's support for import maps and thought initially that I might be able to give Deno an import map for the short list of Node APIs that PRPL uses. Given how strict I was driving toward (near) zero dependencies and how careful I was to select APIs Deno had standard library implementations of, the scope of what I needed to shim was contained to path
and fs/promises
.
Deno has a 1:1 path
implementation with Node, so that worked fine. The problems were with fs/promises
:
// Node
;
// Deno
;
;
These functions in node are all imported from fs/promises
and can't be imported from individual paths (e.g. fs/promises/copy-file.js
) as far as I can tell. There's no way to differentiate between them in an import map. I briefly explored re-exporting these imports locally in different files to provide different target paths, but because I'm compiling to a single file the imports get changed back to fs/promises
anyway.
Moving on, digging deeper.
Import maps and Deno-Node compatibility
As it turns out, the Node compatibility layer I heard rumblings about earlier this year had since covered a lot of ground. The limited set of Node APIs I was using had support, so I decided to try this approach.
I navigated to the PRPL example project and executed deno run --unstable --compat scripts/build.js
, where the build script is the basic one:
// scripts/build.js
;
;
And drum roll, Deno said:
Ah yes, so I executed deno run --unstable --compat --allow-net scripts/build.js
, and Deno said:
;
Indeed, marked
is the single dependency that PRPL core has. I rummaged around to find a CDN url that exports a default
import Deno can accept. Now was the time for an import map. I created it locally as scripts/import-map.json
to test first:
And when I executed deno run --unstable --compat --allow-net --import-map=scripts/import-map.json scripts/build.js
, Deno said:
)
Alright then, I added the argument, executed deno run --unstable --compat --allow-net --allow-read --import-map=scripts/import-map.json scripts/build.js
, and Deno said:
)
Quite a lot of arguments, but at least the error messages are helpful. Okay, one more try. I executed deno run --unstable --compat --allow-net --allow-read --allow-write --import-map=scripts/import-map.json scripts/build.js
This time, Deno had nothing to say. Instead, my terminal said in a familiar, cheerful pink-purple text:
[PRPL] Build complete
Pumps fist.
Shipping Deno support in PRPL core
Happily, import maps and the Deno-Node compatibility layer helped me achieve Deno support in PRPL core. To make the experience a bit better for users, I added the import map to the PRPL core module and included the file in core's package.json
:
As of @prpl/core
v0.3, users can run this command to build PRPL sites with Deno:
Although only the core module works this way for the time being, it is useful to know that this strategy can be employed for all other PRPL library modules without significant changes.
While it's unclear how much adoption Deno will have in the JavaScript ecosystem 2 or 3 years from now, I'm satisfied that I was able to extend support for Deno in PRPL without making uncomfortable trade-offs.
Thanks for reading! Go home for more notes.