The journey of building a tiny JS library
Excited about what does it take to conceptualize, and publish a library? Learn about project setup, test cases, different JS Modules, Tree shaking, an
Table of Content
- GitHub repository
- Folder structure
- Installing dependencies
- Dismantling package.json
- Types of JS Module
- Rollup configuration
- Other configurations
- The human side
- Tree Shaking
- The healthy library
- Pre-publishing hygiene
- Release
I was curious to learn about the Rollup bundler and building a library using it. I thought I would build a pluralization library that pluralizes any given word.
The idea came when I stumbled upon Grammarly's article on plural rules. I thought it should be pretty straightforward and started my journey of learning and building PluralJS.
I already read Kamlesh Chandani’s article on building a library and I was pretty energetic to build one. I decided to explore his demo project along with Rollup's documentation and sample project
Even before I setup the project, I need to have a name for my library. I thought "Pluralize" would be good. I search npm directory and it was already taken and had .
Finally, I settled with PluralJS.
GitHub repository
I created a private repository on GitHub and cloned it on my local machine. I installed a few dev dependencies (no direct dependencies in the project). In the later sections, I will explain about each package.json
field and dependencies.
Folder structure
The folder structure was pretty straightforward. You keep your all configuration files such as rollup.config.js
, .babelrc
, .huskyrc
, .npmrc
, .gitignore
along with LICENSE and README files at root level.
You have src
and dist
folder. The src
folder has your source files and dist
(short for distribution) folder has bundles generated by Rollup.
We have a few more folders in src
. We have assets, test and helpers. We keep our main file as main.js
at root of src
folder. I have index.html
there to play with changes in library.
The assets folder, as name suggest has images. This is the location to keep font files, sound files, icons, etc.
(Click on screenshot to zoom Folder structure)
The test folder has a test file and can further be organized as per developer’s convenience. I had only single file so I just dumped it there. In case you have quite handful test cases then you can have different sub-folders and convention for test file names.
Helpers folder usually has utility functions that your main file needs. It can be an array utility, data conversion utility, DOM manipulation utility, etc. I have kept grammar rules and dictionary in the helper
folder to avoid polluting core logic file.
Installing dependencies
I initialized the npm package using the npm init
command. It asks a bunch of questions before generating the package.json
at the root of your project. You can skip the questionnaire and update it later in the package.json
. You can use npm -y
or npm -yes
to skip the questionnaire altogether. These questions are usually meta information such as license, package name, version, keywords, etc.
Once we have a package.json
file, we are good to install dependencies. We will briefly talk about each dependency in the next section.
Dismantling package.json
I have divided the dev dependencies into 3 categories viz. Rollup related, transcompiling, and developer experience related.
1. Rollup and its plugins
I'm using Rollup as a build tool so we have Rollup 2.8.x
as a dev dependency. Apart from that, I have used 4 Rollup plugins.
- rollup-plugin-filesize – A plugin to display filesizes of your build files. It helps to know if we are increasing file size while developing the library.
- rollup-plugin-terser – A plugin to minify bundle. We use this to minify our UMD bundle.
- @rollup/plugin-babel – An official Rollup plugin to compile code in Babel. In our modules, we want to compile our code to ES5.
- @rollup/plugin-json – I'm importing
package.json
in ourmain.js
file to export library version in the build files. This plugin helps to convert JSON files to ES6 modules.
2. Transcompiling
- @babel/preset-env – The Babel provides you to micro-control which syntax to transform. It could be time-consuming and verbose to add each syntax in configuration to hint Babel to transform. We can use preset that supports almost everything from next-generation JavaScript without explicitly adding each syntax.
3. Developer experience
commitizen- This helps to write a well-formatted commit message. This will make all the commit messages consistent when more people start contributing to it.
cz-conventional-changelog – It enforces a standard convention for the commit message. You can plug and play with different conventions from commitizen.
jest- It is used for writing test cases for the main logic.
prettier- This helps to format code. It will enforce a formatting style to make code consistent.
husky- This helps to easily configure Git Hooks. I use Husky to run pre-commit hooks such as prettier formatting and test cases.
Types of JS Module
There are 4 different JS module formats viz. AMD (Asynchronous Module Definition), ESM (ECMAScript module), CJS (CommonJS), and UMD (Universal Module Definition).
The UMD supports CJS, AMD, and even runs in the browser. It implies that we can bundle our library in UMD and it will work in the NodeJS environment (with require()
) as well as in browser with direct <script>
tag.
The PluralJS can be used in the NodeJS environment and in the browser (with ESM or UMD). We need to define 3 fields viz. "main", "module" and "browser" in package.json
. Officially, npm only mention about main and browser.
The main field indicates that it is used in the CJS manner. If someone uses require(‘pluraljs’)
then the main module’s export object will be returned.
The browser field indicates to the user that the output file might have window or similar primitives that can’t be used NodeJS environment. It is usually a UMD module that can be directly run in the browser with <script>
tag.
The "module" field is unofficial and it is proposed to support in npm. It is used to include ESM modules. It can directly be used in the browser with the following code though IE 11 and Edge 12-15 doesn't support it.
<script type="module">
import pluralJs from '../dist/pluraljs.esm.js';
</script>
Nowadays, We develop applications with build tool so we use import plural from 'pluralJs';
in our application. Let's consider the ReactJS example where Webpack does heavy work for us to transform it into ES5.
Read more about JS modules –
- Learn the basics of the JavaScript module system and build your own library
- Setting up multi-platform npm packages
- What is the “module” package.json field for?
Rollup configuration
I use Rollup to build PluralJS and the configuration is pretty straight forward. There are a few ways to run Rollup. I used the CLI option with the configuration file. I created rollup.config.js
and kept it in the root folder.
Let's dissect the Rollup configuration for PluralJS.
import pkg from './package.json';
/** Plugins **/
import babel from '@rollup/plugin-babel';
import {
terser
} from 'rollup-plugin-terser';
import filesize from 'rollup-plugin-filesize';
import json from '@rollup/plugin-json';
export default {
input: 'src/main.js',
output: [{
file: pkg.main,
format: 'cjs'
},
{
file: pkg.module,
format: 'es'
},
{
file: pkg.browser,
format: 'umd',
name: 'pluralJs'
},
],
plugins: [
json(),
babel({
exclude: 'node_modules/**'
}),
terser({
include: [/^.+\.min\.js$/]
}),
filesize(),
],
};
We have imported package.json
and a few Rollup plugins. The package.json
to extract values used in Rollup configuration.
We tell Rollup to treat main.js
as input file and compile it into different output formats. We have 3 different module formats.
A slight difference in UMD output, we need to pass a name for UMD export. That’s become a global variable when you use the UMD module.
<script src="../dist/pluraljs.umd.min.js"></script>
<script>
console.log(pluralJs);
</script>
Also, I minify only the UMD module as it can be directly used in the browser whereas ESM will probably get processed in consumer apps (if not then you can minify it as well). The CJS format doesn’t really require minification as NodeJS is server-side and bundle size doesn’t really matter.
Let’s talk about plugins we used in PluralJS.
- json() – It converts
.json
files to ES6 modules. I importpackage.json
inmain.js
to get version so that I can export version constant with the library.pluralJs.VERSION
in the console will give the current version. - babel() – It compiles code in ES5.
- terser() – It minifies output code to reduce bundle size. At the time of writing, PluralJS (UMD format) has the following sizes -
Minified Size | Gzipped Size |
3.04 KiB | 1.51 KiB |
- filesize() – It shows filesize in the CLI. The above table is generated using the help of the filesize plugin.
Feel easy and straightforward, right? You can checkout the Rollup configuration of PluralJS. Additionally, You can add Rollup commands in package.json
'script' to quickly build project.
"build": "rollup -c" – This will build the distribution files.
"dev": "rollup -c -w" – It will continuously build files while you develop. Pretty helpful when you are verifying the build changes in browser or NodeJS environment.
Other configurations
We need to take care of other consistency and formatting in the codebase. As the codebase grows and the number of contributors grows, we should make sure that even after contribution from multiple developers, we maintain our consistency in the codebase. The basic consistency ranges from code formatting before commit to writing a well-formatted commit message.
We already talked about configurations in "Developer Experience". You can check .rc
configurations of PluralJS to understand it better.
The human side
The human side involves interaction with other developers before and after building a library. You ask your friends to give a spin to your baked library or something which I mentioned below.
1. Request to borrow
If you are borrowing the logic, documentation, test cases, or any other material from other developers then you should write them an email and request them to allow you to use it. You should ask even their work has an MIT license. Do provide them the necessary attribute to respect their hard work (which you got free).
2. Check how other people write
If you think that your idea is already floating on npm then you better check how other people have written their libraries, what are the GitHub issues filed, how frequent is release and fixes, etc. Connect with these developers and understand their rationale behind any piece of code or creativity.
You can address existing issues filed in their libraries and provide a new improved library or you can request to become the maintainer of the library and solve existing problems.
Tree Shaking
The explicit import and export statements are enough for build tools to statically analyze the code and exclude everything that is not required. A classic case I encounter where I imported package.json
in main logic to get package version. I realized that suddenly 1.5 KiB is added extra in the bundle. This is fixed using named import.
Tree shaking relies on import and export statements. The name and concept are popularized by Rollup and works with ES6 imports/exports and doesn’t work with CommonJS. A common thumb rule is avoiding default exports and use named exports.
❌ Default import | ✅ Named import |
It imported the entire package file and added 1.5 KiB in the bundle size. | It imported only version field from the package and I avoided shipping an extra 1.5 KiB just for version. |
import pkg from '../package.json'; | import { version } from '../package.json'; |
The healthy library
TDD development has helped me to achieve confidence with PluralJS. The library has delicate rules and adding new functionality might break them. At the time of writing this article, 1,145 tests are helping me to be confident about the health of the library.
Setup
I have used the Jest framework to write unit test cases. A test folder is created with a test file and I have all my test cases in it. A script is referenced in package.json
to run the test
command to execute test cases. I’m using WebStorm and using Jest configuration to run test cases with a single click rather than the command.
Need details on Jest configuration and integration in WebStorm? Do reach out on Twitter and I will update the article.
(Click on screenshot to zoom Jest configuration in WebStorm)
Playground
How do you test different module formats? The test cases help you to gain confidence in output. What about consumptions in the actual project?
I have created a playground.html
file where I test my UMD and ES6 module integration. I refer /dist/
folder files in the HTML to mimic imports. The HTML file has UI elements to take input and I pass them to PluralJS function to produce output. This way I ensure that UMD and ES6 are functional.
A playground.js
file is created to test CJS and I run it on CLI to see its functionality.
Note: I once tested with
npm link
in another project and now quite confident that I don’t need to test using thenpm link
again.
Pre-publishing hygiene
I take care of code formatting, running test cases, updating the package version, and documentation before I publish. The code formatting and unit test cases are part of the pre-commit hook using Husky. The documentation change and package version update are still manual.
💡 Do share your ideas around automating Changelog and bumping up the library version.
I reviewed a few README files of different open-source projects and decided to add the following in my README file. A well-documented library helps other developers to understand and check the library quickly for their requirements.
The PluralJS has few sections in its README file.
- Logo
- Badges
<img src="https://img.shields.io/npm/dm/pluraljs.svg?style=flat" alt="npm downloads for PluralJS">
- Single line description
- Motive
- Installation
- Using different package managers (npm and yarn)
- Direct downloads (jsdelivr and unpkg CDNs)
- Usage
- API table
- Usage example in each module format
- A usage example in the real world
- Changelog
- Contact
- Credits
- License
Release
The release is the last part of the library and that takes a whole lot of effort and energy to reach this point. I want to share a couple of observations from my side for release. It includes package publishing, Git tag, and CDN hosting.
Package release
You can release alpha, beta, or release candidate or any version of your library. It is actually pretty straightforward to tag your package and publish it. You just need to use --tag
flag in publish command.
npm publish --tag gamma
The consumer app developer will install using -
npm install --save pluraljs@gamma
Kevin has written a blog post on releasing alpha and beta versions on npm. I would encourage you to read more about the release in his article.
Git tag
Git tags are essentially a reference to your commit. It is typically used when a new version is released or to mark any important commit. I used the Git tag when I released PluralJS 1.1.0. It is similar to branch but there is no further commit in Git tag. You can checkout a Git tag and see the specific application/library state from that tag. I watched a YouTube video by Jacek on Git tags to understand it.
CDN hosting
The good part of hosting the library on CDN is that you don’t need to host it. Once your library is published on npm and open for the world, popular CDN will automatically make it available for your users. You just need to put a link in your README file for your users to download.
I’m currently using two popular CDN ( jsdelivr and unpkg) links for PluralJS in the README file.
That’s all folks. I hope you enjoyed (and learned) from this journey. 🏁
Spotted a typo or technical mistake? Tweet your feedback on Twitter. Your feedback will help me to revise this article and encourage me to learn and share more such concepts with you.
Thank you Siwalik for proofreading it.