Node.JS (New) Package.json Exports Field

Heard about export map ? It’s quite powerful !

Thomas Juster
The Startup

--

TL;DR

The documentation for the exports package.json field is here, it comes from this proposal. I won’t cover everything, so check out the proposal if you need any additional information.
Plus I did not cover the mirror imports field that is very interesting in the ESModule world.

Seriously, how ?!

“Export” what ?

The exports field (or “export map”) provides a way to expose your package modules for different environments and JavaScript flavors WHILE restricting access to its internal parts.

  • Environments like NodeJS, browser, of even node-env-like filters production/development, for instance React could (or does ?) use it for its development build with warnings & stuff, and its production build.
  • Flavors like EcmaScript Module import … from '…' vs. CommonJS const … = require(…)

How to use it

Consider the following architecture:

my-awesome-lib
├── lib/
│ ├── whole-lib.browser.js (iife format)
│ ├── public-module-a.cjs (commonjs format)
│ ├── public-module-a.mjs (esmodule format)
│ ├── public-module-b.cjs
│ ├── public-module-b.mjs
│ └── internals/
│ ├── private-module-c.cjs
│ └── private-module-c.mjs
├── package.json
└── …

We want to export module-a and module-b while restricting access to module-c. We also want our package to provide CommonJS and ESModule outputs.

Here’s an example of how you could use that the benefits of the exports field:

package.json | export field usage

Note: every path should be relative to the package root. Meaning each path must start with ./

By providing the following information about our package my-awesome-lib, we can now use it anywhere (it is supported) like this:

JavaScript usage

In both flavors, notice the path rewrite: there’s no /lib/ in them. The path correspond to what we declared in the exports map.

That’s it !

We’re done here. I didn’t cover how to use production/development exports, but you get the idea, you can experiment it on your own 😉

🦄 🦕

The story of how I met … exports map

Lately, while building a small NPM library, I checked how it looked on skypack (ex pika), just out of curiosity. And I found this:

Skypack package score

My eye was caught by Export Map. So I figured “Cool, I’ll just check that out, see how it would help me build a next-gen package”, I ended following the “learn more” link.

At the end of the road, you’re redirected to the NodeJS documentation, but the doc itself is − paradoxically − too big and too shallow (in my opinion).

A more detailed section of it − which I actually found out while writing this post − is the “Writing dual packages while avoiding or minimizing hazards”. And I gave it a shot. I changed a bit the build process of my lib, added the export map, and published it. Fine.

Some hours later, I check if I can still install my lib… and everything is broken 😱 ! I can’t even import/require it !
(Fortunately, it was a brand-new library, with very few downloads, and no one filed me an issue, so the lib was probably still usable in their use case).

So I’m like “Crap I doomed my build process again, what did I do wrong this time ?”. I started playing where’s Waldo 🧐 … and I finally found what the issue was: the exports map. Which I didn’t expect at all. I was expecting to discover one of my changes to the build process, and absolutely not.

At this point I knew where the issue came from, but not how to fix it. “Not so fast” right ? Obviously, everywhere exports field was mentioned, it was mentioned only as The package.json new "exports" field, but every time I duckduckgo⋅ed it, there’s no obvious result showing how it works. And that’s because there are only extremely common words (for a dev) in that search: npm, package.json, field and exports. I had to go back to the proposal to understand the usage of it… Which I did !

And now you can scroll back to the top 😁.

Here, take this cat GIF as my thanks for your reading 🙏

--

--

Thomas Juster
The Startup

Front-end Developer; I‘m not sure who I am.