Rollup Library Starter
Tags: rollup, javascript, bundle, library
Published: 2022-10-05 16:47:31

---
title: Rollup Library Starter
published: true
description: Learn how to package your JavaScript library into a production-ready bundle using Rollup module bundler
tags: Rollup, JavaScript, Bundle, Library
canonical_url: https://misha.wtf/blog/rollup-library-starter
cover_image: https://www.misha.wtf/_next/image?url=%2Fblog%2Frollup-library-starter%2Fcover.png&w=1200&q=100
---

## Intro

In this post we'll take a look at how to package your JavaScript library code into a production-ready bundle using [Rollup](https://rollupjs.org) 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](/blog/javascript-modules)!

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](https://tsdx.io/) 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:

```json
// package.json
{
  "name": "rollup-library-starter",
  "version": "1.0.0",
  "description": "Starter template for building JS libraries using Rollup",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "Mykhaylo Ryechkin",
  "license": "MIT"
}
```

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

```
.DS_Store
dist
node_modules
```

### Folder Structure

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

```
src/
├── components/
│   ├── Button/
│   │   ├── Button.js
│   │   └── index.js
│   ├── Text/
│   │   ├── Text.js
│   │   └── index.js
│   └── index.js
├── utils/
│   └── index.js
└── index.js
```

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`:

```js
// src/index.js
export * from './components';
export * from './utils';
```

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:

```js
import { Button, Text } from 'rollup-library-starter';
```

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:

```js
// src/components/index.js
export { default as Button } from './Button';
export { default as Text } from './Text';
```

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`):

```jsx
// src/components/Button/Button.js
const Button = (props) => {
  const { children, onClick } = props;

  function handleOnClick() {
    console.log('Button onClick');
    return onClick;
  }

  return (
    <button type="button" onClick={handleOnClick} {...props}>
      {children}
    </button>
  );
};

export const VARIANT = {
  PRIMARY: 'primary',
  SECONDARY: 'secondary',
};

export default Button;
```

<p></p>

```jsx
// src/components/Button/index.js
import Button, { VARIANT } from './Button';

export default Object.assign(Button, {
  VARIANT,
});
```

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

```js
export default Object.assign(Button, {
  VARIANT,
});
```

Here we're assigning `VARIANT` as a property on the main `Button` component using [Object.assign()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/assign), which will let us use `VARIANT` without having to import it explicitly:

```jsx
import { Button } from 'rollup-library-starter';
// ...
<Button variant={Button.VARIANT.PRIMARY}>
```

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`:

```jsx
// src/components/Text/Text.js
export default function Text({ children }) {
  return <div style={{ fontSize: '2.5rem', fontFamily: 'monospace' }}>{children}</div>;
}
```

<p></p>

```js
// src/components/Text/index.js
import Text from './Text';

export default Text;
```

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

```js
// src/utils/index.js
export const Greet = (text) => console.log(`Hello ${text}.`);
```

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:

```js
// src/index.js
export * from './components';
export * from './utils';
```

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](https://rollupjs.org) 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](https://github.com/rollup/awesome).

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](https://babeljs.io/) 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:

```sh
npm i @babel/runtime
```

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

- [`@babel/plugin-transform-runtime`](https://babeljs.io/docs/babel-plugin-transform-runtime/)
- [`@babel/preset-env`](https://babeljs.io/docs/babel-preset-env.html)
- [`@babel/preset-react`](https://babeljs.io/docs/babel-preset-react)
- [`@rollup/plugin-alias`](https://www.npmjs.com/package/@rollup/plugin-alias)
- [`@rollup/plugin-babel`](https://github.com/rollup/plugins/tree/master/packages/babel)
- [`@rollup/plugin-commonjs`](https://www.npmjs.com/package/@rollup/plugin-commonjs)
- [`@rollup/plugin-node-resolve`](https://www.npmjs.com/package/@rollup/plugin-node-resolve)
- [`@rollup/plugin-terser`](https://www.npmjs.com/package/@rollup/plugin-terser)
- [`rollup-plugin-analyzer`](https://www.npmjs.com/package/rollup-plugin-analyzer)

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

```bash
npm i -D rollup rollup-plugin-analyzer @rollup/plugin-alias @rollup/plugin-babel @rollup/plugin-commonjs @rollup/plugin-node-resolve @rollup/plugin-terser @babel/plugin-transform-runtime @babel/preset-env @babel/preset-react
```

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`:

```json
// package.json
{
  "peerDependencies": {
    "react": "^16.14.0 || ^17.0.0 || ^18.2.0",
    "react-dom": "^16.14.0 || ^17.0.0 || ^18.2.0"
  }
}
```

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](https://rollupjs.org/guide/en/#using-config-files) 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.

> It's worthwhile to keep in mind the [dual package hazard](https://nodejs.org/api/packages.html#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.

### Base 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.

```js
// rollup.config.js
import { fileURLToPath } from 'node:url';
import { createRequire } from 'node:module';

import alias from '@rollup/plugin-alias';
import babel from '@rollup/plugin-babel';
import commonjs from '@rollup/plugin-commonjs';
import nodeResolve from '@rollup/plugin-node-resolve';
import terser from '@rollup/plugin-terser';
import analyze from 'rollup-plugin-analyzer';

const require = createRequire(import.meta.url);
const pkg = require('./package.json');

const config = {
  // Rollup options go here
};

export default config;
```

The `config` is where we'll be setting all the different [options](https://rollupjs.org/guide/en/#big-list-of-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:

```js
// rollup.config.js
// ...
const outputOptions = {
  exports: 'named',
  preserveModules: true,
  banner: `/*
 * Rollup Library Starter
 * {@link https://github.com/mryechkin/rollup-library-starter}
 * @copyright Mykhaylo Ryechkin (@mryechkin)
 * @license MIT
 */`,
};
```

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](https://github.com/rollup/rollup/issues/1961#issuecomment-423037881) 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](https://rollupjs.org/guide/en/#tree-shaking) 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](https://rollupjs.org/guide/en/#big-list-of-options).

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:

```js
// rollup.config.js
// ...
const config = {
  input: 'src/index.js',
  output: [
    {
      dir: 'dist/esm',
      format: 'esm',
      ...outputOptions,
    },
    {
      dir: 'dist/cjs',
      format: 'cjs',
      ...outputOptions,
    },
  ],
};
```

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](https://github.com/rollup/plugins/tree/master/packages/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](https://github.com/Andarist) for providing it in [this issue](https://github.com/rollup/rollup-plugin-babel/issues/148#issuecomment-399696316):

```js
// rollup.config.js
// ...
const makeExternalPredicate = (externalArr) => {
  if (externalArr.length === 0) {
    return () => false;
  }
  const pattern = new RegExp(`^(${externalArr.join('|')})($|/)`);
  return (id) => pattern.test(id);
};

const config = [
  {
    // ...
    external: makeExternalPredicate([
      ...Object.keys(pkg.dependencies || {}),
      ...Object.keys(pkg.peerDependencies || {}),
    ]),
  },
];
```

### 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`](https://www.npmjs.com/package/@rollup/plugin-alias), which enables us to use absolute import paths for `src` (or any other path you want to configure):

```js
// rollup.config.js
import alias from '@rollup/plugin-alias';
// ...
const config = [
  {
    // ...
    plugins: [
      alias({
        entries: {
          src: fileURLToPath(new URL('src', import.meta.url)),
        },
      }),
    ],
  },
];
```

#### Plugin: Resolve Node Modules

Next, we have [`@rollup/plugin-node-resolve`](https://www.npmjs.com/package/@rollup/plugin-node-resolve), which allows Rollup to resolve external modules from `node_modules`:

```js
// rollup.config.js
import nodeResolve from '@rollup/plugin-node-resolve';
// ...
const config = [
  {
    // ...
    plugins: [
      // ...
      nodeResolve(),
    ],
  },
];
```

#### Plugin: CommonsJS

The [`@rollup/plugin-commonjs`](https://www.npmjs.com/package/@rollup/plugin-commonjs) plugin converts 3rd-party CommonJS modules into ES6 code, so that they can be included in our Rollup bundle:

```js
// rollup.config.js
import commonjs from '@rollup/plugin-commonjs';
// ...
const config = [
  {
    // ...
    plugins: [
      // ...
      commonjs({ include: ['node_modules/**'] }),
    ],
  },
];
```

#### Plugin: Babel

Next, we need to enable Babel for code transpilation. We do that by passing [`@rollup/plugin-babel`](https://github.com/rollup/plugins/tree/master/packages/babel) as a plugin, and then specifying the `@babel/plugin-transform-runtime` Babel plugin:

```js
// rollup.config.js
import babel from '@rollup/plugin-babel';
// ...
const babelRuntimeVersion = pkg.dependencies['@babel/runtime'].replace(/^[^0-9]*/, '');

const config = [
  // ...
  plugins: [
    // ...
    babel({
      plugins: [['@babel/plugin-transform-runtime', { version: babelRuntimeVersion }]]
    })
  ],
];
```

The [`@babel/plugin-transform-runtime`](https://babeljs.io/docs/en/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 `dependencies`:

```js
// rollup.config.js
const babelRuntimeVersion = pkg.dependencies['@babel/runtime'].replace(/^[^0-9]*/, '');
```

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:

```js
// rollup.config.js
import babel from '@rollup/plugin-babel';
// ...
const config = [
  plugins: [
    // ...
    babel({
      babelHelpers: 'runtime',
      exclude: /node_modules/,
      plugins: [
        // ...
      ],
    })
  ],
];
```

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`](https://babeljs.io/docs/en/babel-preset-env) and [`@babel/preset-react`](https://babeljs.io/docs/en/babel-preset-react), respectively:

```js
// rollup.config.js
import babel from '@rollup/plugin-babel';
// ...
const config = [
  plugins: [
    // ...
    babel({
      babelHelpers: 'runtime',
      exclude: /node_modules/,
      presets: [
        ['@babel/preset-env', { targets: 'defaults' }],
        ['@babel/preset-react', { runtime: 'automatic' }],
      ],
    })
  ],
];
```

#### 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`](https://www.npmjs.com/package/@rollup/plugin-terser) and uses [terser](https://github.com/terser/terser) under the hood to minify the code.

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

```js
// rollup.config.js
import terser from '@rollup/plugin-terser';
// ...
const config = [
  plugins: [
    // ...
    terser(),
  ],
];
```

> **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`](https://github.com/doesdev/rollup-plugin-analyzer). This plugin will print out some useful info about our generated bundle upon successful builds:

```js
// rollup.config.js
import analyze from 'rollup-plugin-analyzer';
// ...
const config = [
  plugins: [
    // ...
    analyze({
      hideDeps: true,
      limit: 0,
      summaryOnly: true,
    }),
  ],
];

export default config;
```

### Config Summary

And with that, we're done configuring Rollup! Here's what your `rollup.config.js` should look like in the end:

```js
// rollup.config.js
import { fileURLToPath } from 'node:url';
import { createRequire } from 'node:module';

import alias from '@rollup/plugin-alias';
import babel from '@rollup/plugin-babel';
import commonjs from '@rollup/plugin-commonjs';
import nodeResolve from '@rollup/plugin-node-resolve';
import terser from '@rollup/plugin-terser';
import analyze from 'rollup-plugin-analyzer';

const require = createRequire(import.meta.url);
const pkg = require('./package.json');

const makeExternalPredicate = (externalArr) => {
  if (externalArr.length === 0) {
    return () => false;
  }
  const pattern = new RegExp(`^(${externalArr.join('|')})($|/)`);
  return (id) => pattern.test(id);
};

const babelRuntimeVersion = pkg.dependencies['@babel/runtime'].replace(/^[^0-9]*/, '');

const outputOptions = {
  exports: 'named',
  preserveModules: true,
  banner: `/*
 * Rollup Library Starter
 * {@link https://github.com/mryechkin/rollup-library-starter}
 * @copyright Mykhaylo Ryechkin (@mryechkin)
 * @license MIT
 */`,
};

const config = {
  input: 'src/index.js',
  output: [
    {
      dir: 'dist/esm',
      format: 'esm',
      ...outputOptions,
    },
    {
      dir: 'dist/cjs',
      format: 'cjs',
      ...outputOptions,
    },
  ],
  external: makeExternalPredicate([
    ...Object.keys(pkg.dependencies || {}),
    ...Object.keys(pkg.peerDependencies || {}),
  ]),
  plugins: [
    alias({
      entries: {
        src: fileURLToPath(new URL('src', import.meta.url)),
      },
    }),
    nodeResolve(),
    commonjs({ include: ['node_modules/**'] }),
    babel({
      babelHelpers: 'runtime',
      exclude: /node_modules/,
      plugins: [['@babel/plugin-transform-runtime', { version: babelRuntimeVersion }]],
      presets: [
        ['@babel/preset-env', { targets: 'defaults' }],
        ['@babel/preset-react', { runtime: 'automatic' }],
      ],
    }),
    terser(),
    analyze({
      hideDeps: true,
      limit: 0,
      summaryOnly: true,
    }),
  ],
};

export default config;
```

The example configuration file can be found [here](https://github.com/mryechkin/rollup-library-starter/blob/main/rollup.config.js) 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](https://github.com/rollup/awesome), 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:

```json
// package.json
{
  "scripts": {
    "prebuild": "rm -rf dist",
    "build": "rollup -c"
  }
}
```

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).

### ESM

As of Rollup **v3**, we need to ensure that our configuration file is [loaded as an ES module](https://rollupjs.org/migration/#using-configuration-files) 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 `type` to `"module"` in the package file:

```json
// package.json
{
  "type": "module"
}
```

### 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](https://nodejs.org/api/packages.html#package-entry-points) 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 `cjs` bundle:

```json
// package.json
{
  "main": "./dist/cjs/index.js"
}
```

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

```json
// package.json
{
  "module": "./dist/esm/index.js"
}
```

### Exports

In order to ensure maximum interoperability with tools like Webpack (and others), we should also specify the [subpath exports](https://nodejs.org/api/packages.html#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:

```json
// package.json
{
  "exports": {
    ".": {
      "import": "./dist/esm/index.js",
      "require": "./dist/cjs/index.js"
    }
  },
}
```

This is to ensure that we can do both:

```js
// ESM
import { Button } from 'rollup-library-starter';

// CommonJS
const { Button } = require('rollup-library-starter');
```

### 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:

```json
// package.json
{
  "files": ["dist"]
}
```

> The full list of `package.json` configuration options is available [here](https://docs.npmjs.com/cli/v8/configuring-npm)

## 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](https://www.misha.wtf/blog/rollup-library-starter/analyzer-output.png)

## 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:

> [https://github.com/mryechkin/rollup-library-starter](https://github.com/mryechkin/rollup-library-starter)

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