DEV Community

Cover image for How to bundle your library and why
Tobias Barth
Tobias Barth

Posted on • Edited on • Originally published at tobias-barth.net

How to bundle your library and why

Preface

This article is part 6 of the series "Publish a modern JavaScript (or TypeScript) library". Check out the motivation and links to other parts in the introduction.

Publishing formats – do you even need a bundle?

At this point in our setup we deliver our library as separate modules. ES Modules to be exact. Let's discuss what we achieve with that and what could be missing.

Remember, we are publishing a library that is to be used within other applications. Depending on your concrete use case the library will be used in web applications in browsers or in NodeJS applications on servers or locally.

Web applications (I)

In the case of web applications we can assume that they will get bundled with any of the current solutions, Webpack for example. These bundlers can understand ES Module syntax and since we deliver our code in several modules, the bundler can optimize what code needs to be included and which code doesn't (tree-shaking). In other words, for this use case we already have everything we need. In fact, bundling our modules together into one blob could defeat our goal to enable end-users to end up with only the code they need. The final application bundlers could maybe no longer differentiate which parts of the library code is being used.

Conclusion: No bundle needed.

NodeJS applications

What about NodeJS? It is standard for Node applications to consist of several independent files; source files and their dependencies (node_modules). The modules will get imported during runtime when they are needed. But does it work with ES Modules? Sort of.

NodeJS v12 has experimental support for ES Modules. "Experimental" means we must "expect major changes in the implementation including interoperability support, specifier resolution, and default behavior." But yes, it works and it will work even better and smoother in future versions.

Since Node has to support CommonJS modules for the time being and since the two module types are not 100% compatible, there are a few things we have to respect if we want to support both ways of usage. First of all, things will change. The NodeJS team even warns to "publish any ES module packages intended for use by Node.js until [handling of packages that support CJS and ESM] is resolved."

That means, if your library is intended only for Node (so no browser optimization necessary), you don't want to rely on your library's users reading your installation notes (they will have to know what to import/require) or you are not interested in investing in features that are only almost there: Please just don't publish ES Modules. Change the configuration of Babel's env preset to { modules: 'commonjs' } and ship only CommonJS modules.

But with a bit of work we can make sure everthing will be fine. For now the ESM support is behind a flag (--experimental-modules). When the implementation changes, I will hopefully update this post as soon as possible. As of Nov. 21 2019 the feature is unflagged: Changelog. So your users do not have to start their app with the flag with Node version 13.2.0 upwards.

NodeJS uses a combination of a declaration of module type inside of package.json and filename extensions. I won't lay out every detail and combination of these variants but rather show the (in my opinion) most future-proof and easiest approach.

Right now we have created .js files that are in ES Module syntax. Therefore, we will add the type key to our package.json and set it to "module". This is the signal to NodeJS that it should parse every .js file in this package scope as ES Module:

{
  // ...
  "type": "module",
  // ...
}
Enter fullscreen mode Exit fullscreen mode

Note that you oftentimes will come across the advice to use *.mjs file extensions. Don't do that. *.js is the extension for JavaScript files and will probably always be. Let's use the default naming for the current standards like ESM syntax. If you have for whatever reason files inside your package that must use CommonJS syntax, give them another extension: *.cjs. Node will know what to do with it.

There are a few caveats:

  1. Using third party dependencies
    1. If the external module is (only) in CommonJS syntax, you can import it only as default import. Node says that will hopefully change in the future but for now you can't have named imports on a CommonJS module.
    2. If the external module is published in ESM syntax, check if it follows Node's rules: If there is ESM syntax in a *.js file and there is no "type": "module" in the package.json, the package is broken and you can not use it with ES Modules. (Example: react-lifecycles-compat). Webpack would make it work but not Node. An example for a properly configured package is graphql-js. It uses the *.mjs extension for ESM files.
  2. Imports need file extensions. You can import from a package name (import _ from 'lodash') like before but you can not import from a file (or a folder containing an index.(m)js) without the complete path: import x from './otherfile.js' will work but import x from './otherfile' won't. import y from './that-folder/index.js' will work but import y from './that-folder' won't.
  3. There is a way around the file extension rule but you have to force your users do it: They must run their program with a second flag: --es-module-specifier-resolution=node. That will restore the resolution pattern Node users know from CommonJS. Unfortunately that is also necessary if you have Babel runtime helpers included by Babel. Babel will inject default imports which is good, but it omits the file extensions. So if your library depends on Babel transforms, you have to tell your users that they will have to use that flag. (Not too bad because they already know how to pass ESM related flags when they want to opt into ESM.)

For all other users that are not so into experimental features we also publish in CommonJS. To support CommonJS we do something, let's say, non-canonical in the NodeJS world: we deliver a single-file bundle. Normally, people don't bundle for Node because it's not necessary. But because we need a second compile one way or the other, it's the easiest path. Also note that other than in the web we don't have to care to much for size as everything executes locally and is installed beforehand.

Conclusion: Bundle needed if we want to ship both, CommonJS and ESM.

Web applications (II)

There is another use case regarding web applications. Sometimes people want to be able to include a library by dropping a <script> tag into their HTML and refer to the library via a global variable. (There are also other scenarios that may need such a kind of package.) To make that possible without additional setup by the user, all of your library's code must be bundled together in one file.

Conclusion: Bundle needed to make usage as easy as possible.

Special "imports"

There is a class of use cases that came up mainly with the rise of Webpack and its rich "loader" landscape. And that is: importing every file type that you can imagine into your JavaScript. It probably started with requiring accompanying CSS files into JS components and went over images and what not. If you do something like that in your library, you have to use a bundler. Because otherwise the consumers of your library would have to use a bundler themselves that is at least configured exactly in a way that handles all strange (read: not JS-) imports in your library. Nobody wants to do that.

If you deliver stylings alongside with your JS code, you should do it with a separate CSS file that comes with the rest of the code. And if you write a whole component library like Bootstrap then you probably don't want to ask your users for importing hundreds of CSS files but one compiled file. And the same goes for other non-JS file types.

Conclusion: Bundle needed

Ok, ok, now tell me how to do it!

Alright. Now you can decide if you really need to bundle your library. Also, you have an idea of what the bundle should "look" like from outside: For classic usage with Node.js it should be a big CommonJS module, consumable with require(). For further bundling in web applications it may be better to have a big ES module that is tree-shakable.

And here is the cliffhanger: Each of the common bundling tools will get their own article in this series. This post is already long enough.

Next up: Use Webpack for bundling your library.


As always many thanks to my friend Tim Kraut for proof-reading this article!

Top comments (2)

Collapse
 
fkrautwald profile image
Frederik Krautwald

Nice picture of Harpa in Reykjavik, Iceland.

Collapse
 
4nduril profile image
Tobias Barth

Thank you!