Rollup Library Starter
NOTE: Make sure to also check out the new post on working with Client and Server Components with Rollup here.
Want to learn more about the different module formats? Check out this post!
NOTE: If your project uses TypeScript, I would suggest using tsdx instead.
Let's get started!
First, init your project using
npm init. You should have a
package.json now that looks something like this:
You'll also want to add a
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
Here we're simultaneously importing and re-exporting all modules from the
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.
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.
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:
Here we're importing the
default exports of the
Text modules, and re-exporting them as
Inside each of our
Text folders, we have the main
index.js files, as well as separate component files (
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.
Text component is slightly simpler, with just one main default export, which is then re-exported inside
And finally, in our
utils folder, we have a single
index.js file, with a sample function
Greet exported as a named export:
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:
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 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.
@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:
These should all be
devDependencies, so make sure to add the
And lastly, since we're using React in our library, we'll also need to specify
react-dom as peer dependencies in
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.
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.
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.
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 my team across multiple JS libraries for several years now, and has proven itself in many enterprise-grade production applications.
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.
config is where we'll be setting all the different options, as well as plugins and Babel presets.
We will be generating two bundles, and there are some common output options shared between the two. Let's put these in a variable:
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.
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:
output option is an array of objects, one for each of the bundles we'd like to build. Here we specify both
cjs formats, which output to
dist/cjs folders respectively. All other shared
outputOptions are provided for both.
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
package.json. All credit for this and a big thank you goes out to Mateusz Burzyński for providing it in this issue:
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.
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):
Plugin: Resolve Node Modules
Next, we have
@rollup/plugin-node-resolve, which allows Rollup to resolve external modules from
@rollup/plugin-commonjs plugin converts 3rd-party CommonJS modules into ES6 code, so that they can be included in our Rollup bundle:
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:
@babel/plugin-transform-runtime plugin enables re-use of Babel's injected helper code, to help reduce the final bundle size.
From 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
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
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:
NOTE: You may want to comment out this plugin if you're trying to debug your generated code, but you'll definitely want to have it enabled for your production bundle.
Plugin: Bundle Analyzer
Lastly, we have
rollup-plugin-analyzer. This plugin will print out some useful info about our generated bundle upon successful builds:
And with that, we're done configuring Rollup! Here's what your
rollup.config.js should look like in the end:
The example configuration file can be found here for your reference as well.
Keep in mind, this configuration is meant to be the foundation for your library. 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.
With Rollup configuration done, let's now configure our package file
package.json so that it can be properly packaged and published to NPM.
First, let's add a
build script, so that we can actually run Rollup:
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).
As of Rollup v3, we need to ensure that our configuration file is loaded as an ES module by Node.
There are a few ways to do this. One is by using the
.mjs extension, and another is by specifying our library package to be ESM, which is done by setting the
"module" in the package file:
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 CommonJS).
For CommonJS, set the
main field to point to the
Then do the same for ES Modules, by pointing the
module field to the
In order to ensure maximum interoperability with tools like Webpack (and others), we should also specify the subpath exports separately for each module format.
To do this, add an
exports field and specify the main entry point's
require to point to the
cjs bundles, respectively:
This is to ensure that we can do both:
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 (
LICENSE, etc.) we need to specify the folder with output from Rollup.
dist folder to the
The full list of
package.jsonconfiguration options is available here
Running the Build
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:
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:
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!