Build a JavaScript Util Library for NPM with ES6 Modules and CommonJS
Distribute Your JavaScript Utils Across Multiple Projects
There’s no shortage of awesome JavaScript util libraries out there. They’re incredibly helpful but, if you’re like me, you often wish for certain functions or features that don’t exist.
We could definitely write our own and include them in our project, but what happens when we work in multiple repositories or other teams want to make use of our cool world-saving utils?
We need our own JavaScript utility library that we can distribute via NPM.
TL;DR
Here’s an example GitHub repo with my personal util functions after which you can pattern your architecture:
Goals
- Have one code source that transpiles into both ES6 and CommonJS (ES5)
- Tree-shakable for ES6
- Implement unit testing with Jest
- Auto-generate the README.md for documentation
Let’s Get Started!
First things first.
Boring Disclaimer
I’ve chosen to use TypeScript for this project, but you don’t have to. It’s perfectly fine if you want to use regular ES6 with Babel. Everything will still pertain to you with the exception of the The tsconfig(s) section.
And now that we’ve gotten that out of the way…
The File Structure
Here is a diagram of the core file structure that we want to setup:
/dist
└───/lib
└───/es5
└───/es6
└───CHANGELOG.md
└───LICENSE
└───README.md
└───package.json
/src
package.json
tsconfig.es5.json
tsconfig.json
The dist
directory will contain everything that we wish to publish as part of our NPM package. We’ll generate everything in lib
as well as the README.md.
The tsconfig(s)
As noted in the disclaimer, if you’re not planning to use TypeScript and instead will use ES6 with Babel, you can skip this step and replace it with your own Babel setup.
We are going to have two tsconfig.json files at the root of our project. One will be for ES6 and the other CommonJS:
The bin Directory
The bin is where we hold our files for generating the README.md. I’m not going to go over the code of these files because the implementation is outside the scope of this article, but I will explain at a high-level how they are used.
The generateReadme.js file will:
- Crawl through our
src
directory to find all of our utils - Parse the JSDoc info in each to determine what to output for the README.md.
- Use the readme.ejs file as the template for the markdown for the final README.md
- Place the main README.md in the
dist
directory and the root. - Create a README.md for each util in the
src/[util]
directory.
Note that you may want to change the behavior of this code for item #5 for your project where you have all of the documentation included in one single README.md file.
I have my utils repo setup the way it is because it’s easier for me and others to find what they’re looking for and isolate what they’re looking at.
If you want to distribute one large README.md file, there’s nothing wrong with doing that and you would have to make only a few minor tweaks to the code in the generateReadme.js and readme.ejs files.
The package.json(s)
We are going to have two package.json files: one at the root and one in the dist
directory.
package.json (root):
Notice the compile
script makes use of the two tsconfig files and executes the readme
script at the end.
package.json (dist):
The points of interest here are:
- Change
name
to what you want your npm package to be. - Change the
repository
,bugs
, andhomepage
urls to match your repository. - Change
author
to your name. - Increment the
version
as you publish. "sideEffects": false,
is critical for tree shaking to occur. It tells Webpack (and the like) that your individual modules are self-contained without any external side effects.main
is the entry point for our CommonJS code.module
is the entry point for our ES6 code. If the code that is utilizing the library understandsmodule
, it will use it. Otherwise, it will use our CommonJS code inmain
.
How does it know to use ES6 or CommonJS?
When the app consuming our library is transpiled to CommonJS (ES5), it won’t recognize module
in our package.json as a property it knows how to use so it will ignore it and enter through main
. When the app is run as ES6, it will recognize module
, use that and ignore main
.
Let’s Create Some Utils!
We should organize our utils by category to help with maintainability as our library scales in the future. I find that this structure works really well:
/src
└───/myCategory
└───/myUtil
└───/index.ts
└───/index.test.js
└───/index.ts
You’ll see how this plays out as we create our first util.
Our First Util
Let’s create a simple value util that checks to see if a value is null
or undefined
.
We’ll create the following tree:
/src
└───/value
└───/isNil
└───/index.ts
└───/index.test.js
└───/index.ts
The two key things to note are:
- Export your function as the name of the util, in this case
isNil
- Structure the JSDoc like the above with a description, since, category, the parameters, returns, and an example(s).
Including and structuring the JSDoc properly will become important to autogenerate the README.md so don’t skip this step!
The Unit Test
We need to make sure we test our functions before we send them out. Here’s a sample test for isNil
as the index.test.js file:
Define the Exports
The next thing we need to do is define the exports to make it easy for our consumers to access our utils. To do that, we’ll create an index.ts file at the root of our source and include the following:
export { isNil } from './value/isNil';
Follow this same pattern in the same file to add any other utils you might have in the future. For example:
Tree Shaking
It’s very important that we use the export syntax outlined above in order to get the tree shaking benefits in ES6. If we were to do this or any variation of this instead, we would lose all tree shaking benefits:
// Bad! Do NOT do anything like this!
import { isNil } from './value/isNil';
export { isNil };
Once something is imported, Webpack will consider it necessary so nothing will be shaken out, even if our consumer uses only one function in our list.
Compile It
Let’s bring our new library to life! From the root of the project, run one of these commands:
npm run compile
Check the README.md
You’ll recall that we included our script for generating the README.md as part of the compile script in our root package.json file. If our compile has run without error, we should see a brand new autogenerated README.md file in our dist
directory.
Test It
From the root of the project, run one of these commands:
npm test
Or if you want code coverage stats:
npm run test:coverage
If you’ve never done testing with Jest before, you should see your tests listed out in the terminal and, hopefully, a lot of green PASS
tags:
Publish It
Navigate to the dist
directory to execute our publish command.
npm publish
For all of the ins and outs of publishing to NPM, you’ll want to check out the NPM docs page for publishing.
Consume It
Once published, we should be able to go to any of our other projects to install it:
npm i @username/utils
Since all of our functions are named exports, you can import them like this:
import { isNil, myOtherUtil } from '@username/utils'
Profit!
We can now distribute our utils package across multiple projects and multiple teams, we can be confident that it will work in an ES6 or CommonJS (ES5) application, we’ve tested each function, and we’ve generated our documentation for each function.
Go forth and make the JavaScript world more convenient for you and your teams!