Publishing monolithic bundles

For now, we will focus on the first four publishing targets, i.e. CJS, script tag, AMD and ESM. To that end, let us prepare our project for publishing:

mkdir fancy-case cd fancy-case npm init --yes git clone https://gist.github.com/lukastaegert/e9c6c04b8f96adc562a70c096c3e7705 src npm install --save-dev rollup

This will create a package.json file for our project, put our sample files into a src folder and install Rollup. Rollup supports a special output format called a “Universal Module Definition”, which simultaneously supports the CJS, script tag, and ESM use cases. To create it, add a new file called rollup.config.js to the root of your project:

export default {

input: 'src/main.js',

output: {

file: 'umd/fancy-case.js',

format: 'umd',

name: 'fancyCase'

}

};

This instructs Rollup to start with src/main.js and bundle it together with all its dependencies into a UMD bundle in umd/fancy-case.js . The name option tells Rollup which global variable to create when the bundle is used in a script tag, in this case fancyCase . This variable will only be created if this bundle is not consumed in a Node or AMD context.

Now if you run

npx rollup --config

from your project’s root, this will pick up our config file and create a new folder named “umd” that contains our UMD bundle. You can check out the result on Rollup’s website: https://rollupjs.org/repl?gist=e9c6c04b8f96adc562a70c096c3e7705

A monolithic bundle merges all modules together

If you switch to the UMD tab in the output section of the website and enter the correct global variable name, you will see all your files compressed together, surrounded by a wrapper like this

(function (global, factory) {

typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) :

typeof define === 'function' && define.amd ? define(['exports'], factory) :

(global = global || self, factory(global.fancyCase = {}));

}(this, function (exports) { 'use strict';



// ... all your bundled code Object.defineProperty(exports, '__esModule', { value: true });

}));

This wrapper will analyze the current runtime environment and provide the exports of your module in a convenient way. Note this line:

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

When attempting to import default our UMD bundle in an ESM context, modern bundlers add interoperability code that checks for the presence of the __esModule property. If it is present, then the default import will not provide the whole exports object but just the default property of that object.

As we are going to create a dedicated ESM bundle anyway which should be used in these cases, we can consider skipping this line by adding esModule: false to the output section of the config file. You can also get a more optimized wrapper by creating dedicated builds for the formats “cjs” (Node), “amd” or “iife” (script tag), check out the corresponding tabs on the website.

Note that apart from the wrapper code in contrast to most other bundlers, no dedicated runtime environment to resolve imports is added. Apart from its configurability, this is another reason why Rollup is quite popular with libraries creators that strive to create efficient bundles with minimal overhead.

UMD bundles should be minified

As especially for the AMD and script tag use, this bundle is meant to be run in the browser unmodified, we should go the whole way and minify it. To do that, I recommend using TerserJS, which is a fork of the more well-known UglifyJS that supports modern ES2015+ JavaScript code. After installing the necessary dependency

npm install --save-dev rollup-plugin-terser

you should modify your rollup.config.js like this:

import {terser} from 'rollup-plugin-terser'; export default {

input: 'src/main.js',

plugins: [terser()],

output: {

file: 'umd/fancy-case.js',

format: 'umd',

name: 'fancyCase',

esModule: false

}

};

As mentioned above, we also want to provide a dedicated ESM bundle. This could be done by adding a second output to our configuration but as this bundle is meant to be consumed by other bundlers anyway and does not profit from minification (in fact this will make it harder to hunt for bugs), I rather recommend to forgo this and export two separate configurations:

import {terser} from 'rollup-plugin-terser'; export default [

{

input: 'src/main.js',

plugins: [terser()],

output: {

file: 'umd/fancy-case.js',

format: 'umd',

name: 'fancyCase',

esModule: false

}

},

{

input: 'src/main.js',

output: {

file: 'esm/index.js',

format: 'esm'

}

}

];

To publish our module, we need to make sure that importers of our library receive the right file and that this file is built from the latest sources upon publishing. To do that, modify our packjage.json file like this:

{

"name": "fancy-case",

"version": "1.0.0",

"main": "umd/fancy-case.js",

"module": "esm/index.js",

"scripts": {

"prepare": "rollup --config"

},

"files": [

"esm/*",

"umd/*"

],

"devDependencies": {

"rollup": "^1.1.0",

"rollup-plugin-terser": "^4.0.2"

}

}

Add both “main” and “module” fields

The main field makes sure that Node users using require will be served the UMD version. The module field is not an official npm feature but a common convention among bundlers to designate how to import an ESM version of our library.

Use “files” to include your bundles

The files field makes sure that besides some default files, only our designated bundles are distributed via npm excluding sources, test files etc. This will keep the node_modules folder of your users small and make npm install faster. You could also create an .npmignore file for a similar effect but with a “deny-listing” instead of an “allow-listing” approach. To my experience, “files” is easier to maintain, though.

Create a “prepare” script

The prepare script is a special script that will be run by npm each time we run npm install or npm publish . It also makes it possible to directly install branches from Github for testing purposes via

npm install <user>/<repository>#<branch>

Now we can just run npm publish and the first version of our library will be available for everyone via npm install fancy-case !

Do a publishing dry-run with “npm pack”