How to publish JavaScript Libraries

Javi Carrasco
Dec 11, 2019 · 11 min read
Photo by James Pond on Unsplash

You wrote some code, and it’s useful. Of course, you want to use it in different projects or you may even want to publish it for more people to use it. You can upload the source somewhere and write instructions about how to use it. But there is a more standard and simple way: publish it as a library.

When you publish your library, you or other people can get it very easily by typing:

npm install your-super-library

And use it with:

import { somethingUseful } from “your-super-library”;

This enables efficient code reuse and creation of projects based on smaller, reusable pieces.

Where to publish

Currently npmjs is the de facto standard, it allows public and private packages and every OS JavaScript library is published there. Yarn repository is basically a mirror of npmjs.

There are two major clients to access the repository (also called package managers): npm and yarn. Both are great and both allow developers to easily:

  • Get libraries from the repository
  • Handle transitive dependencies
  • Update dependencies as new versions are released
  • Publish your own libraries
  • Replicate the dependency tree between machines (for example dev and CI)

There are also some alternatives to host your own repo like Artifactory or Nexus. In this guide I’ll suppose you will use npmjs but publishing to Artifactory or Nexus should be mostly the same.

package.json

package.json is the file that describes your package (in this guide I’ll use library and package as synonyms).

This is an example package.json

As you can see, it includes some descriptive data like the Name, Version, Description, Author, License, Repository or Homepage.

Name and version are mandatory. Additional properties in the package.json are legal, so projects usually add properties to the package.json to store some configuration used by the CI server, testing libraries, etc.

When choosing the name you should consider using scoped packages not only because it’s easier to find a free name, but also because it will allow you to change the repository or authentication data for that specific scope, which is really helpful when using private packages.

For version you must follow semver. Semver says that when updating your package dependencies you can decide if the change is a patch or a minor, but because this can make a big difference in the projects that use your library (bundle size for example) I recommend to release these changes as minors.

There is also data that affects how other projects will use our package:

  • main — it points to the main js file of our library
  • module — points to the main ES6 module of our library
  • jsnext:main — like module but obsolete
  • types — points to the typescript main d.ts file
  • dependencies — other packages that are needed to use this library
  • sideEffects — hint for nodejs compiler to optimise ES6 modules

Module

The main reason to publish our library is to facilitate its reuse. The package repository makes it easy to download the library but we also want the code to be easy to use in projects.

This is where the module concept kicks in. A module may be seen as a lego piece, it has some “tubes” on the bottom and some “studs” at the top and this simple contract makes it easy to connect some modules together. Inside the module may be anything and the internals of the module don’t affect the internals of other modules.

This contract that makes all the modules connectable to others is called the module definition. Currently we have lots of module definitions, so we need to understand them in order to choose one (or more) for our library.

To understand the different module definitions I recommend you to read my previous article JavaScript Module Definitions

The best module definition for your library depends on how your library will be used:

  • If your library can only be used in node (because it uses filesystem for example) you should use CJS
  • If your library is going to be used in new web applications you should use ESM
  • If you want your library to be used in legacy web applications you probably want AMD or IIFE
  • If you are creating a component for a specific framework and it recommends a specific module definition, then go for it

But what could I do if my library could be used in both browsers and nodejs and I want to open it to everybody?

If you remember, we had the attributes main and module in the package.json, these point to the main js file of the library and the main js of the library using ES modules.

This allows us to code with ESM and bundle/transpile twice our library, and generate for example dist/index.js and dist/index.es.js files. Then we can point main and module to these 2 files.

When a project depends on our library, its bundler will chose the file that best suits its needs. This way newer applications will chose the ESM file.

But what happens with older applications? If we choose UMD as the main file, nodejs applications, apps using requireJS or even web apps that don’t use a bundler or a loader will be able to use it.

So main=UMD and module=ES6 support almost every user of our library.

Very few people build websites without a bundler today, so if you are not interested on supporting legacy websites you may prefer main=CJS and module=ES6.

Should we Bundle?

Bundling means concatenating all the files together into a single js file. It’s a technique very useful for web applications. But our library can be published bundled or unbundled.

If your library is used from a node application it shouldn’t matter if it’s bundled or not. The same happens for web applications that use a bundler. Except in the cases when your library uses aliases (or other methods) to ease “relative path hell” in imports. In that case you can’t rely on the application using your library to be able to locate the files, and it would be better to bundle.

For web applications that don’t use a bundler it’s usually better to bundle, because it could be really difficult for the browser to find the imported files through HTTP. And even if that worked, the cycle of download > parse > find imports > download > parse > find imports… would be slower than downloading the whole library at once.

So yes, it’s usually safer to bundle your library.

Should we include dependencies in the bundle?

Our bundle must not include the dependent code. We already list the dependencies in the package.json so the projects that use our library will download the dependencies themselves.

Some developers (including me) import the package.json in the bundler configuration file to use the package.json dependencies as externals. This way we ensure no dependency is included in the bundle.

For legacy web applications that don’t use a loader or bundler it may be useful to have a bundle with the dependencies included, but you should never use it as the main file.

Should we transpile to older EcmaScript?

Of course, we have to do it when we are generating UMD, AMD, or IIFE bundles because our library might be used in the browser as is.

But we also need to transpile to ES5 the ESM bundle. This is funny because ESM was defined in ES6. This is needed because modern bundlers understand ES modules but don’t transpile the dependencies by default. This would produce application bundles with code some browsers can’t run.

What happens to Polyfills?

If we transpiled our code to older ES it’s also probable that the browser needs some polyfills to run our library.

But the responsibility of providing polyfills is always owned by the application and never by its dependencies because only the application creators know the target browsers and it’s the only way to prevent several dependencies to provide several equivalent polyfills.

So, our library has to check for the existence of the needed feature on runtime, and in case it’s not there it will throw an error to help the application developer to add the polyfill.

Also it would be good to mention the required polyfills in the library documentation.

What happens with Typescript?

As library developers, we want other devs to use our library with ease, so we will provide typescript definition files (.d.ts). These files explain the types our modules expect and export. So applications written with typescript (quite popular nowadays) can use our library without the need to write the .d.ts files themselves.

If we write our code with typescript then we won’t need to code the .d.ts ourselves because typescript will generate it automatically. Typescript also helps us to treat types correctly inside our modules, so it will improve our code quality.

In a modular architecture, some pieces might be created by different teams and with different technologies so it’s really important that the pieces work as expected and have as less bugs as possible.

Just as we can configure the main file of our library, in the package.json we can also inform where is located the main .d.ts of our package. To do it we will use the attribute types.

Note: If our library depends on other packages and we need to import additional types to use them, these type dependencies are NOT dev-dependencies. Because applications that use our library will need to transitively use them.

package-lock.json, yarn.lock or npm-shrinkwrap.json?

The lock files let us replicate the same exact dependency tree instance, both for direct dependencies and transitive dependencies. This is really useful to have the same environment in different developer’s machines, CI or even to get back to the dev environment that generated a build in the past.

Now that we are building a library to be used in other applications, we have a different requirement:

If our library has a dependency and an application that uses our library has the same dependency, we want the application bundle to include the dependency only once.

This is why we define dependencies in our package.json with wildcards (^, ~, *, >, …), if there’s a version of the dependency compatible both with our library and the application, it should be included only once.

Both the package.lock and yarn.lock from our library will be ignored when an application that depends on it was built. But the npm-shrinkwrap.json is transitive so the application will be forced to use the same exact version of the common dependencies and they will be probably added twice.

For libraries, it’s recommended NOT to add an npm-shrinkwrap.json.

For server side applications or CLI applications it’s ok to add it, but probably package.lock would be ok too.

Files that will be published

By default all the files in the project folder will be published except those listed in a file called .npmignore. This file is very similar to the .gitignore, in fact, if there’s no .npmignore the .gitignore file will be used instead.

In the package.json we can also configure the files attribute. It’s an array of file patterns that describes the entries to be uploaded to the repository when your library is published. If your package.json has a files attribute, only the files that match will be included in the package.

There are also some files that are always included, no matter your configuration: package.json, README, CHANGELOG, LICENSE and the file in the main attribute.

You can check which files will be included in your package by running

npm pack

Publishing process

We are finally ready to publish our library. To create our library we should have done:

  • Configure our software version control (git init)
  • Create our package.json and configure all the attributes needed (npm init)
  • Write code and tests
  • Configure a bundler and generate the bundles
  • Create a readme explaining at least what’s the library for and how to use it

As we are generating a library, we want to test it as a dependency of an actual application.

I recommend you to create a directory named example inside your library with a demo application that uses the library.

The following package.json inside the example folder would let you depend on your library bundles and then use it as if it was an external dependency, without the need of publishing it:

{    “name”: “@jacarma/my-awesome-library-example”,    “version”: “0.0.0”,    “license”: “MIT”,    “private”: true,    “dependencies”: {        “@jacarma/my-awesome-library”: “file:..”    },    “scripts”: {        “start”: “dev-server”    }}

Alternatively you can npm link inside your library folder and then, in another folder’s directory, npm link your-library-name. That will create a link in the node_modules that will let you use it as a dependency.

Before publishing we want to ensure we will do it right, these are some checks that make sense to me:

  • We are on master or on the release branch
  • Our working directory matches exactly the code in the repo
  • Our dependencies are updated
  • Bundle content is ok. Dependencies are not included in the bundles, the generated bundles use the expected module definitions…
  • We are not including private or unneeded files (npm pack)

First, if you don’t have an npm account you can create one at npmjs.com/signup

  • Login at npmjs from your terminal, it will ask for your user and password
npm login
  • Bump your library version and create a new git tag
npm version
  • Push the bumped code and new tag
git push --follow-tags
  • Publish the library to npmjs
npm publish --access public
  • Celebrate! Your library is ready to be used, you can create a new application and add the library as a dependency to prove it
Photo by Jason Dent on Unsplash

There is a tool that eases the publishing process, it’s called np.

Duties as an OS library maintainer

In case you want to publish the library as open source, you will acquire some responsibilities.

  • Answer questions, bugs and PR
  • Keep dependencies up to date
  • Keep your library secure
  • Have an eye on security alerts
  • Warn when not maintained

There’s nothing mandatory about it but if you don’t fulfill them, someone will fork your code, and devs prefer well maintained code.

Conclusions

There’s nothing difficult in the publishing process: npm version and npm publish should be enough.

All the “complications” come from generating bundles that can be easily used in applications:

  • Some applications run on the browser, some run on node
  • Some applications use bundlers, some not
  • Some applications use typescript, some not
  • Browsers support different versions of ES and the way to alleviate that (transpile + polyfill) is complex or unclear in a modular architecture
  • There are too many module definitions
  • Developers don’t fully understand how transitive dependencies are resolved (peer dependencies, .lock files, wildcards)
  • Developers don’t fully differentiate the responsibilities of package manager, bundler, transpiler and module loader because often several of them are included in a single tool

I hope this article helps you to clarify how to publish JS libraries. Remember that this ecosystem is evolving everyday, this article was first published in December 2019 and some tips may be invalid in the future so use with caution.

I would love to know if this article helped you to publish a library, you can reach me at @jacarma

Follow up

JavaScript in Plain English

Learn the web's most important programming language.

Thanks to Jorge Sanz

Javi Carrasco

Written by

Frontend team lead at edorasware

JavaScript in Plain English

Learn the web's most important programming language.

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade