This post is half rant, half guide. Each of these 13 questions reveals tradeoffs that take time and mental energy to research. In case you don’t already have enough to decide today, here’s what you must consider when creating and publishing a new JavaScript package.
1. Do you want to write it in TypeScript?
If so, you’ll need to compile it before publishing. Bookmark the the tsconfig.json reference.
2. Do you want to compile using tsc, swc, or esbuild?
By default, TypeScript uses tsc to type check and transpile your source .ts files into regular JavaScript. It’s slower because it’s written in JavaScript. Other tools like swc (Rust) or esbuild (Go) transpile much faster, but do not type check. Nobody likes waiting around for a build, but maybe you’d rather have one tool.
3. Do you want it to run in Node?
Now you need to decide your module format.
4. Do you want to output your package as ESM or CJS?
CommonJS modules (CJS) was the original way to include code from another file in Node. ECMAScript modules (ESM) is now the standard way to include JS code and Node just recently implemented support for it. The community wants us to move to pure ESM, but many things are CJS and they don’t exactly interop seamlessly.
5. Are any of your dependencies pure ESM?
Using them in your CJS project is not going to work out of the box.
6. Are any of your dependents CJS?
They won’t be able to use your pure ESM library out of the box.
7. Since Node can run ESM and CJS, do you want to support both?
Then you need to avoid the Dual Package Hazard when both versions of your library might end up loaded at the same time.
8. Do you want it to run in browsers?
Since all browsers can run ESM, you’ll probably choose that as your module format, as opposed to AMD or an IIFE.
9. Will you bundle all your code into a single JS file?
This will make it download faster. You probably want it minified too.
10. Will you inline your dependencies in the bundle?
This will make it download slower, but it will be more portable.
11. Will you use a CDN to get it into the browser?
You’ll need to know about services like jsDelivr or Skypack which automatically serve packages in the npm registry. Some services let you leave the npm dependencies out of the bundle while their CDNs serve them for you. Otherwise your users will need to download the bundle like the old days or npm install it and link to it in node_modules from their HTML page. ES modules must have an explicit path to a real file, no index.js or package.json magic. Or you can use import maps.
12. Do you want to bundle it for node?
Esbuild talks about doing this in their docs. It can be faster to read from the disc.
13. Where will you specify your entry point in package.json?
The top choices for your entry point are: “exports”, “main”, and “browser”. Each runtime and build tool will use this information to run or compile your code.
What. A. Nightmare.
To simply share some JavaScript you’ve got to consider JS runtimes, module formats, and build tools with no one true way. Was it easier when cross-browser compatibility was the big headache?
My Answers
I had to spend the time making these decisions for my own package @brimdata/zed-js. A JavaScript client package for the Zed data platform. Here’s what I came up with.
I will write TypeScript.
I will use nx to generate the project because their developers have many sane decisions for me. Thank you!
I will build a sole CJS version for Node since I plan to consume this package in an Electron app and Electron doesn’t yet support ESM. This was a hard decision because respected community members are pushing valiantly for pure ESM packages. I agree philosophically 100%. If I wasn’t the one consuming this package, I’d go pure ESM. However, I don’t feel like hacking around Electron to get my own code to run, so I’m compromising on my values for today.
I will use tsc to build the CJS version to avoid another tool. If my project were huge, I’d use to swc to build and tsc to lint.
I will leave the CJS version unbundled because…what happens in node_modules, stays in node_modules.
I will use the “exports” field in package.json to point to the Node entry point. It has some advantages over “main” and will make it easier to switch to pure ESM in the future.
I will use esbuild to bundle an ESM version for the web.
I will inline my dependencies because I don’t have many and they are not big. If they were big, I’d consider using a CDN like Skypack.
I will use the “browser” field in package.json to point to the bundle.
Hope For Brighter Days
My introduction to programming came from Ruby on Rails. It championed the principle “convention over configuration” when the back-end landscape was overgrown with XML config files for Java VMs.
Today we are in having a “configuration-heavy” moment in the front-end world, but I have hope that good conventions will emerge. Then we can get back to doing the real work.