How to Create a JavaScript Package in 2019?

Konrad Stobiecki
8 min readAug 6, 2019

--

Konrad Stobiecki

It may seem obvious but nobody actually knows how to make them perfectly, especially when it comes to creating both Node.js- and browser-friendly packages. Do not expect me to solve all the problems, so feel free to leave comments!

A Little Bit of a Background Story

Development of ECMA-262 Standard (ECMAScript®) 6th release (ES6) started as early as in 2009 and the final version was released in June 2015 with modules, along some other improvements like lexical block scope, classes, promises, etc.

According to ECMA-262, large applications should be divided into modules that contain explicit statements abouts parts of the module declared in other modules and, analogically, declarations used by other modules. It also distinguishes between Script and Module, and in most cases uses these terms in conjunction.

💡 As a curiosity, ECMAScript defines global, module, function and block (block, try, catch etc.) environments. Global environment has no outer environment, it’s built-in global object value is null. Actually, it is not that much built-in in Node.js, as you can provide it when running Script with vm.runInContext, vm.runInThisContext, vm.runInNewContext methods of VM module API. Now, when you eval a Script with both let- and var- declared variables, the parent’s global will be polluted by the var-declared variable and its defined value while the let one will live only in the called Script’s environment (although it is discouraged to define global variables in scripts and it is even more discouraged to use eval). Now try to run the same script with VM module and see what happens to the passed context and the caller’s environment.

What is more important for this article, Module chapter of ECMA-262 of the specification declares module loading syntax with imports and exports.

💡 It also says that the similarities to Java are intentional, so next time when you are asked “is JavaScript a Script version of Java?”, you know what to say…

CommonJS Modules

Both Node.js and npm (Never Push to Master? Nineties Party Mix?) initial releases date way back to 2009 (not a coincidence). This means long before 2015 and before the official release of ES6. Therefore, it had to handle modules anyways. Even though ECMAScript Module Standard (ESM) is now oficially released, Node.js still uses CommonJS Standard (CJS) and seems to respect both standards equally.

CJS reserves global exports and require words. Historically, it is synchronous although there are proposals for asynchronous require. The exports and require implamentations are objects not keywords. CJS exports is mutable and keeps the exported variables per module environment.

New Strategy

ECMAScript Modules

Regarding ESM, Node.js 10 (2018) allowed for using them under condition that ESM modules are suffixed with .mjs instead .js which caused some unease.

With Node.js 12 (2019) it is no longer a problem. Just the nearest package.json has to contain the following entry:

{ “type”: “module” }

and .js or unsuffixed files will be treated as ECMAScript modules by default.

Unfortunately, as for Node.js 12, ECMAScript modules still require

--experimental-modules

flag to be enabled even with the package.json entry in place.

💡 ESM import also imports JSON modules. There is also the

--experimental-wasm-modules

flag that enables import of WebAssembly modules.

TypeScript Modules

With all the hype around TypeScript in Angular, React, Vue or Svelte, it becomes obligatory to learn the ultimate superset of JavaScript. While popular old packages get their typings, I think new ones should be written in TypeScript from scratch.

💡 TypeScript module syntax is exactly what is described in ECMA-262 (and has always been). And TypeScript’s reserved words like interface, protected, private, etc. are also reserved words in ECMA-262's future. No awkward # to declare a private field. What is the etymology of the word field, anyways? It’s a class’ property AFAIK being an instance variable in Pascal… then what is static #someVar ? For an instance?!

You can always build TypeScript to JavaScript but good luck going in the opposite direction. Have a look at the example portability of one of my libs:

Example (ts module)
import { snap } from ‘snap-to-grid-clustering’;
console.log(snap([[-1, 1], [2, 3], [10, 10], [11, 11]], 5))
// => { ‘0,0’: [ 0 ], ‘0,1’: [ 1 ], ‘2,2’: [ 2, 3 ] }
Example (es module)
import { snap } from ‘snap-to-grid-clustering’
console.log(snap([[-1, 1], [2, 3], [10, 10], [11, 11]], 5))
// => { ‘0,0’: [ 0 ], ‘0,1’: [ 1 ], ‘2,2’: [ 2, 3 ] }
Example (commonjs)
var snap = require(‘snap-to-grid-clustering’).snap;
console.log(snap([[-1, 1], [2, 3], [10, 10], [11, 11]], 5))
// => { ‘0,0’: [ 0 ], ‘0,1’: [ 1 ], ‘2,2’: [ 2, 3 ] }

It works as TypeScript, ECMAScript and CommonJS modules out of the box. In my humble opinion it would be enough with only ESM and TS.

Going JavaScript first would require declaring additional discouraged TypeScript packages (the ones whose names start with @type) later. And that generates costs. So why not jump start with TypeScript?

... and the Browsers

When all major bundlers like Browserify, Webpack, Parcel, Rollup (have I missed something?) working perfectly well with ESM and TypeScript I think no further comment is necessary.

💡 There is also the native handling of ESM with

<script type=”module”>

that shines a bright future on low build effort of JavaScript.

OOP Principles

Consider two things. First: Your new package will be the foundation of someone else’s app. Second: the most important element of any package
is the public API. Imagine one change in your API when your package gets to 1000 downloads weekly (a geek’s dream). It makes 1000 people work on upgrading their app to comply because of one simple change in your interface. Although they will not send the package owner a 100,000$ bill for those 55 minutes spent on surprise code refactoring (plus five minutes of melissa break), they will definitely regret using the lib that many times. Unless you want to write a developer trolling app, have all those fancy OOPSODRYKISS abbrevs in mind before even naming your app.

Functional Reactive Friendly

Everyone and their grandma uses either Redux or NGRX (NGXS, Akita, Vuex…) nowadays. The main idea behind reactive state management is that the states should be immutable as a result of functional transforms. I think it is worth considering since the recipient of your package may use it carelessly. There will be one less junior dev to be… reduced… when they store your package object that turns out to be mutable (bad practice warning). Are you feeling the power of responsibility now?

There are some lint options that will help you creating proper functional modules.

For TypeScript, it is tslint-functional-preset (or check the TypeScript boilerplate described below). For JavaScript, you might want to try eslint-plugin-fp.

Tactics

Versioning

Creating a changelog with the first published version is always a good idea. When people start downloading your package, they would like to know how your package evolved, which helps debugging.

My first package became version 1.0.5 after a short while and I quickly forgot what I was doing in 1.0.0, so it is quite important to create the changelog before doing the first push to npm. As you can see I made a mistake in my package so I had to call versions 1.0.0–1.0.4 “experimental” just because I forgot what I changed there earlier.

Before you run npm publish make sure you have created a tag in your Git repository.

git tag -a 1.0.4 -m “release 1.0.4”

That way anyone who found your project in Git will know it is well maintained and easy to upgrade. I found many package owners forget about this simple feature.

Remember to push tags along with other stuff each time with:

git push --follow-tags

or add followTags to config and push as usual:

git config --global push.followTags true

Shields, Shields, Shields, CI

I am using CircleCI with GitHub. Of course, you can use any CI you wish. Since I do not play DevOps nor have time for configuring CI (I have done it only to have those shiny shields in package description), I found CircleCI very comfortable with just a few clicks web configuration which requires just signing in and pointing at your repo.

Using CI you are always aware of any build problems even if you forget to run tests or build locally before pushing (worse things happen at sea).

Boilerplate

Unless you are a tinker type loving to configure every project from scratch, use some boilerplate. My favourite is typescript-starter which brings a very nice intro that makes you feel like that main character in a hacker movie.

You can add the description and choose between creating a NodeJS app or
a JS library, npm or yarn first, provide lint options and CI variants with
a multiselect input.

CircleCI, Travis or Jenkins configs are created during install and there is absolutely nothing else to do but provide the URL in the related web console.

One interesting feature of typescript-starter is that you can enable tslint-immutable setup for functional programming, a tricky bit of challenge but very safe when working with state management. In my opinion, stricter checking should also be on by default.

The script is eventually creating a new folder and initializes a git repository. To make that possible with PowerShell, I had to enable Git by installing Posh.
I had Git for Windows already installed.

Bibliography links show how to configure powershell with your typescript starter.

Demo, Test, Examples

E2E testing is as obvious as it is neglected. How silly would it be if someone told you that your lib does not work although the code looks great at first glance?

Use your package in various environments. NPM gives you the opportunity to test CommonJS packages with RunKit with just a single click.

When you use any CI, it will alarm you of any failing tests. It is also wise to have some test apps using your module. These can be used as examples and demos to encourage public.

Remember, more examples and larger readme file increase the likelihood of your package being favored over alternatives.

💡 Please keep in mind that these are only my loose thoughts I tried to make up very hard. I am a heavily “experimental” programmer (good excuse for not being a bookworm), so do not treat this article as the single source of truth about programming, but rather a subject to dialogue.

Now go spam the world with proper-Node.js-and-browser-friendly-packages!

References

Proof of Concept

CommonJS + Async Proposals

ECMAScript®

Node.js (ESM Documentation)

How to Configure GIT for your PowerShell Project

Other Interesting Sites

--

--