Rolled libraries

How to distribute a tree-shaken library for your tree-shaken apps using Rollup.

Common problem: you are writing a library that you want to reuse in Node and maybe in a browser. You want the client apps to be able to include only the used parts, and not your entire code. How do you do this?

A great tool for this is Rollup. If your library uses ES2015 modules (think import and export keywords), then Rollup can perform static analysis and determine which pieces are reachable and can drop everything else, making the tiny output bundle.

Write the library

First let us write a sample library, for example named try-rollup with two functions add and mul. Our single index.js will export both

1
2
3
// try-rollup index.js
export const add = (a, b) => a + b
export const mul = (a, b) => a * b

We can bundle this module so it is usable as a plain Node module for distribution. This is simple enough to use "rollup" from the command line. In addition, I will set the main file in my package.json to point at the created bundle.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
{
"name": "try-rollup",
"version": "1.0.0",
"main": "dist/index.js",
"module": "index.js",
"files": [
"index.js",
"dist"
],

"scripts": {
"build": "rollup index.js -f cjs -o dist/index.js"
}
,

"devDependencies": {
"rollup": "0.41.5"
}

}

Command npm run build creates an output file that looks almost like the input file index.js, but is compatible with Node CommonJS require call

1
2
3
4
5
6
7
8
9
10
// dist/index.js
'use strict';

Object.defineProperty(exports, '__esModule', { value: true });

const add = (a, b) => a + b;
const mul = (a, b) => a * b;

exports.add = add;
exports.mul = mul;

The most important field in package.json is module that points back at our original ES6 source file index.js. When we distribute our library we include the CommonJS bundle and the entry file index.js (by listing them both in files list). The users of our library try-rollup can use Rollup or Webpack v2 to bundle their code and the tools can go back to the original ES6 code and do the application tree-shaking again, see Rollup module doc.

Use the library

Let us write a small Node client application called try-rollup-user that uses try-rollup. For simplicity I will install try-rollup from the file system folder, without publishing to the public NPM registry. I also need Rollup again. Because the module try-rollup goes into node_modules folder, I will need a Rollup plugin to actually find it (since Rollup supports many different module definitions). In our case it is node-resolve plugin

1
npm i -D rollup rollup-plugin-node-resolve ../try-rollup

The package.json shows the build command - now I need to use the [Rollup config file][https://rollupjs.org/#using-config-files] to configure the build step

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
{
"name": "try-rollup-user",
"version": "1.0.0",
"main": "build/index.js",
"scripts": {
"test": "node build/index.js",
"build": "rollup -c"
}
,

"devDependencies": {
"rollup": "0.41.5",
"rollup-plugin-node-resolve": "3.0.0"
}
,

"dependencies": {
"try-rollup": "file:///Users/gleb/try-rollup"
}

}

To execute the result we point Node at the output file build/index.js.

The Rollup config file just reads the entry file and uses the Node resolve plugin for any module installed using npm install command.

1
2
3
4
5
6
7
8
9
10
// rollup.config.js
import resolve from 'rollup-plugin-node-resolve'
export default {
entry: 'index.js',
dest: 'build/index.js',
format: 'cjs',
plugins: [
resolve({})
]
}

Bundle and run

Let us say our app is very simple and only uses mul function from try-rollup and nothing else.

1
2
3
// index.js
import {mul} from 'try-rollup'
console.log('mul 2 * 3 =', mul(2, 3))

The build step generates tree-shaken file in CommonJS format

1
2
3
4
5
// "npm run build" which runs "rollup -c"
// build/index.js
'use strict';
const mul = (a, b) => a * b;
console.log('mul 2 * 3 =', mul(2, 3));

That is it - Rollup finds node_modules/try-rollup, loads package.json, finds the module property and looks at the original ES6 source files, which can be analyzed and tree-shaken. This is how we get mul from try-rollup and nothing else in the output file.

Bonus - Bundle ES6

You can roll and distribute 2 bundles: one with CommonJS code and another one with ES6 modules without distributing your entire source. This is especially helpful if your library depends on other ES6 libraries and can benefit from tree-shaking itself. Make sure to save your dependencies as devDependencies because we will include them in the generated bundle.

Because Rollup does not support multiple bundles right away (but you can write separate Rollup commands of course), I will use my tool rollem. Here is the rollem.config.js file that defines two output bundles: one in CommonJS format, another in ES6 format.

1
2
3
4
5
6
7
8
9
10
// rollem.config.js
export default [{
entry: 'index.js',
dest: 'dist/index.js',
format: 'cjs'
}, {
entry: 'index.js',
dest: 'dist/index.es6.js',
format: 'es'
}]

We are only going to distribute these bundles, and not the original files (see files list)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
{
"name": "try-rollup",
"version": "1.0.0",
"main": "dist/index.js",
"module": "dist/index.es6.js",
"scripts": {
"build": "rollem"
}
,

"files": [
"dist"
],

"devDependencies": {
"rollem": "1.11.0",
"rollup": "0.41.5"
}

}

1
2
3
4
5
$ npm run build

> try-rollup@1.0.0 build /Users/gleb/try-rollup
> rollem
[11:14:57 GMT-0400 (EDT)] built 2 bundles

We can install and use the new try-rollup just like before - nothing changes in the client code except the install gets just three files:

1
2
3
4
5
6
7
8
9
$ ls -lR node_modules/try-rollup
total 8
drwxr-xr-x 4 gleb staff 136 Apr 10 11:10 dist
-rw-r--r-- 1 gleb staff 1428 Apr 10 11:10 package.json

node_modules/try-rollup/dist:
total 16
-rw-r--r-- 1 gleb staff 80 Apr 10 11:09 index.es6.js
-rw-r--r-- 1 gleb staff 176 Apr 10 11:09 index.js

Tree-shaking all the way!

Double bonus - Multiple targets using Rollup

Rich Harris, the author of Rollup, pointed that you can output multiple formats from same entry point using rollup.config.js, even reusing the filenames defined in the package file.

1
2
3
4
5
6
7
8
9
// rollup.config.js
const pkg = require('./package.json')
export default {
entry: 'index.js',
targets: [
{dest: pkg.main, format: 'cjs'},
{dest: pkg.module, format: 'es'}
]
}

This is my favorite syntax :)