Rollup Library Starter

RollupJavaScriptNPMpackage.json
·
16 min read
Rollup Library Starter

Intro

In this post we'll take a look at how to package your JavaScript library code into a production-ready bundle using Rollup module bundler. By the end of this tutorial we will have a dual-module format bundle that is ready to be published to NPM, can be consumed in either server or browser environments, and is available in both ESM and CommonJS formats.

Want to learn more about the different module formats? Check out this post!

The base recipe assumes use of regular JavaScript and is meant for use with React projects, however the basic principles and patterns we'll cover here are not restricted to any one framework, and can be extended to a variety of different projects.

NOTE: If your project uses TypeScript, I would suggest using tsdx instead.

Let's get started!

Project Setup

First, init your project using npm init. You should have a package.json now that looks something like this:

./package.json

You'll also want to add a .gitignore file:

Folder Structure

Our project files will live inside the src folder, and the example folder structure will look something like this:

This is just an example convention. Ultimately, you can structure it however you'd like. The main thing to remember is every folder will need an index.js file, including the top-level src:

./src/index.js

Here we're simultaneously importing and re-exporting all modules from the components and utils folders (each having their own index.js file). This is where we control what's available from our library for external consumption - anything that's exported will be available to the library consumer.

Modules

Our library will expose each module as a named module, ie:

To make things easier to maintain, we're putting each module and the related files in its own folder, using the same name as the module (ie. src/components/Button and src/components/Text).

We can then further group these modules by category and put all related modules in the same folder (ie. src/components). Each folder also needs to have its own index.js file as well:

./src/components/index.js

Here we're importing the default exports of the Button and Text modules, and re-exporting them as Button and Text.

Inside each of our Button and Text folders, we have the main index.js files, as well as separate component files (Button.js and Text.js):

./src/components/Button/Button.js

./src/components/Button/index.js

Notice the little trick we're doing inside the component index.js file above:

Here we're assigning VARIANT as a property on the main Button component using Object.assign(), which will let us use VARIANT without having to import it explicitly:

This pattern allows us to group related code that normally would be imported together anyway, reducing some verbosity in the import statements.

The Text component is slightly simpler, with just one main default export, which is then re-exported inside index.js:

./src/components/Text/Text.js
./src/components/Text/index.js

And finally, in our utils folder, we have a single index.js file, with a sample function Greet exported as a named export:

./src/utils/index.js

To export all these modules at the root, make sure they're included in the index.js file at the top of the src folder, as we saw earlier:

./src/index.js

The idea here is that our main index.js file will contain all the exported modules within our library, as named exports.

With that done, time to install our dependencies.

Tooling & Dependencies

Now that the base library setup, let's install all the necessary tooling. As mentioned earlier, we'll be using Rollup as our module bundler (v2 at the time of writing this post):

Rollup is a module bundler for JavaScript which compiles small pieces of code into something larger and more complex, such as a library or application. It uses the new standardized format for code modules included in the ES6 revision of JavaScript, instead of previous idiosyncratic solutions such as CommonJS and AMD. ES modules let you freely and seamlessly combine the most useful individual functions from your favorite libraries. This will eventually be possible natively everywhere, but Rollup lets you do it today.

Rollup has an extensive plugin ecosystem, which provides a lot of flexibility in adding specific functionality for a variety of use cases. Check out the awesome curated list here.

Now, it can be daunting to figure out which exact plugins to use - and the goal of this article is to provide guidance on what's needed for a majority of bundles that you'll be building. First, we will install all the required dependencies, and then go through what each of them does as we start to configure the steps.

We'll be using Babel to transpile our code into JavaScript that all current and older browsers and environments will understand. For this, install @babel/runtime as a dependency. This will be our only production dependency, and will let Rollup bundle some of the helper functions required for backwards compatibility:

Next, we need to install a few Babel plugins, as well as Rollup itself and a number of Rollup plugins:

  • rollup-plugin-analyzer
  • rollup-plugin-terser
  • @rollup/plugin-alias
  • @rollup/plugin-babel
  • @rollup/plugin-commonjs
  • @rollup/plugin-image
  • @rollup/plugin-node-resolve
  • @babel/plugin-transform-runtime
  • @babel/preset-env
  • @babel/preset-react

These should all be devDependencies, so make sure to add the -D flag:

And lastly, since we're using React in our library, we'll also need to specify react and react-dom as peer dependencies in package.json:

./package.json

This will ensure that our library doesn't try to include its own copy of React when it's installed, as that can cause all sorts of issues.

Rollup Configuration

To get started with Rollup, we'll need to create a configuration file at the root of our project, where we specify all the options for bundling, plugins, presets, etc. This file will be the bulk of our work.

Our library is written using the modern ESM module syntax, however we want our code to be executable in all JavaScript environments. In order to support this, we will ship two distinctly separate bundles as part of our library package: ESM and CommonJS (or CJS).

Rollup will handle all the transpilation of our code from ESM to CJS, and generate two separate bundles for each format. This will make our library package consumable in both server (Node) and browser environments. We will also need to modify our package.json to support this.

NOTE: it's worthwhile to keep in mind the dual package hazard, which is explained in the official Node docs.

With that said, this pattern has been working great for our team across multiple libraries, and has proven itself in many production applications.

Initial Config

First, let's create a file called rollup.config.js at the root of our project. We will import all the packages and plugins, and then break down what each of them does:

./rollup.config.js

The config is where we'll be setting all the different options, as well as plugins and Babel presets.

Output Options

We will be generating two bundles, and there are some common output options shared between the two. Let's put these in a variable:

./rollup.config.js

Here, we're setting the exports option to "named", because we're exporting everything as named exports. This is intentional, as there can be issues with mixing both default and named patterns of exports at the root of your package, when exporting to CommonJS format. Rollup will also throw a warning when you do that.

The preserveModules option will tell Rollup to create separate chunks for all modules, using the original module names as file names.

This is needed in order for tools like Webpack 5 to be able to successfully tree-shake unused imports when consuming our library. We ran into this issue with our component library when we originally moved to Rollup. Enabling this option eliminated issues with tree-shaking for our users, and this pattern has worked well for us since.

Lastly, and completely optional, we can add a banner to each of the generated files. This is a good way to provide some info about our library, and add author attribution.

For all other available options, check out the big list of options here.

With these options defined, let's now provide the entry point for our package. This will be the root-level index.js file, which if you recall containts all of our available modules exported as "named". We will also set our output options as well:

./rollup.config.js

Our output option is an array of objects, one for each of the bundles we'd like to build. Here we specify both esm and cjs formats, which output to dist/esm and dist/cjs folders respectively. All other shared outputOptions are provided for both.

External Modules

Next, we need to tell Rollup which of the modules used in our code are external to our library. Together with @rollup/plugin-node-resolve, this ensures that Rollup doesn't bundle those dependencies into our final bundle. The function makeExternalPredicate() generates the list of package names specified in dependencies and peerDependencies in package.json. All credit for this and a big thank you goes out to Mateusz Burzyński for providing it in this issue:

./rollup.config.js

Plugins

Next up, we have the plugins array, and it specifies which Rollup plugins to run. The order in which plugins are specified is very important, as the input for each plugin is the previous one's output, so make sure to follow the order they're listed in here.

Plugin: Alias

The first plugin we're using is @rollup/plugin-alias, which enables us to use absolute import paths for src (or any other path you want to configure):

./rollup.config.js

Plugin: Resolve Node Modules

Next, we have @rollup/plugin-node-resolve, which allows Rollup to resolve external modules from node_modules:

./rollup.config.js

Plugin: CommonsJS

The @rollup/plugin-commonjs plugin converts 3rd-party CommonJS modules into ES6 code, so that they can be included in our Rollup bundle:

./rollup.config.js

Plugin: Babel

Next, we need to enable Babel for code transpilation. We do that by passing @rollup/plugin-babel as a plugin, and then specifying the @babel/plugin-transform-runtime Babel plugin:

./rollup.config.js

The @babel/plugin-transform-runtime plugin enables re-use of Babel's injected helper code, to help reduce the final bundle size. To quote the plugin's docs:

Babel uses very small helpers for common functions such as _extend. By default this will be added to every file that requires it. This duplication is sometimes unnecessary, especially when your application is spread out over multiple files.

The version of Babel runtime is pulled from package.json by reading the dependencies:

./rollup.config.js

We're also telling the Babel plugin how to handle Babel helper code via babelHelpers (it is recommended to use the "runtime" option for bundling libraries with Rollup), as well as not to touch anything imported from node_modules by setting the exclude option:

./rollup.config.js

We also need to specify a few presets so that we can use latest JavaScript features, as well as enable React support. These are @babel/preset-env and @babel/preset-react, respectively:

./rollup.config.js

Plugin: Terser

With Babel configuration out of the way, we only have a few plugins left.

This next one will help us reduce final bundle size by minifying the generated code. It's called rollup-plugin-terser and uses terser under the hood to minify the code.

We'll be sticking with the defaults it provides, so no need to specify any options:

./rollup.config.js

NOTE: You may want to exclude this plugin if you're trying to debug your generated code, as it does obfuscate quite a bit. But for the final bundle you'll definitely want to have it enabled.

Plugin: Bundle Analyzer

Lastly, we have rollup-plugin-analyzer. This plugin will print out some useful info about our generated bundle upon sucessfull builds:

./rollup.config.js

Config Summary

And with that, we're done configuring Rollup! The full configuration file can be found here for your reference.

Keep in mind, this configuration is meant to be the foundation for your library, but not necessarily its final form. It serves as a starting point, but there's lots more you can do here with all the plugins available in the Rollup ecosystem. Make sure to explore the awesome curated selection, and see if there's anything else that is applicable to your project.

Package Setup

With Rollup configuration done, let's now configure our package file package.json so that it can be properly packaged and published to NPM.

Scripts

First, let's add a build script, so that we can actually run Rollup:

./package.json

This will run Rollup CLI using the configuration we defined in rollup.config.js. Since this file is at the root level, we don't need to specify it explicitly (the -c flag takes care of that).

Entry Points

We've configured Rollup to output the bundle to the dist folder. And since we've configured our library to have a root-level index.js, we can use this file as the entry point of our package. Since we're publishing a dual-module package, we will need to specify these entry points separately for each of the formats (ESM and CJS).

For CommonJS, set the main field to point to the cjs bundle:

./package.json

Then do the same for ES Modules, by pointing the module field to the esm bundle:

./package.json

Exports

In order to ensure maximum interoperability with tools like Webpack (and others), we should also specify the subpath exports separtely for each module format.

To do this, add an exports field and specify the main entry point's import and require to point to the esm and cjs bundles, respectively:

./package.json

This is to ensure that we can do both:

Package Files

Lastly, we need to tell NPM which files and folders to include in our package. For this we'll use the files field, which describes the entries to be included when the package is installed as a dependency. In addition to the default includes (package.json itself, README, LICENSE, etc.) we need to specify the folder with output from Rollup.

Add the dist folder to the files array:

./package.json

The full list of package.json configuration options is available here

Running the Build

With package.json setup, let's now run our build and check it out. In the terminal at the root of our project type in npm run build. This will run the Rollup CLI and generate its output in the dist folder. You'll notice that there are also two folders in there: esm and cjs, each containing code in the corresponding module format.

Thanks to the rollup-plugin-analyzer plugin, we also get a summary of our package build:

Rollup bundle analyzer plugin output

Wrap Up

And that about does it! Hopefully this recipe gives you a good starting point for your next JavaScript library, and remember to experiment and adjust it as needed.

The full example project is available in GitHub for your reference:

If you've made it this far - thanks for reading, and until next time!

00000