Rollup's intro and outro - Two Amazing Features You Must Know!

Stay updated with our latest articles:

What is Rollup?

If you're not aware of it, Rollup is a JavaScript module bundler.

To further elaborate upon the latter, a JavaScript module bundler is a program that takes in a bunch of JavaScript modules (usually via one single entry point file) and then bundles them all together into one single file.

In the frontend world, bundling is more than just common — it's more or less a necessity given the fact that it allows us to deliver just one single file to the client along with numerous optimizations performed on that file, such as code splitting and minification.

While the de-facto out there for bundling JavaScript files would probably be the ultimate beast Webpack, by no means is it the only bundler. For simple projects, Webpack might be quite cumbersome to set up. As a matter of fact, there are complete books written on just configuring and navigating Webpack, so you can just imagine the level of complexity and power it comes equipped with. It's all good but too much for basic needs.

Rollup is the perfect choice to use in simpler cases. But don't get us wrong; we don't mean to say that Rollup is a simple module bundler program. A big NO!

Rollup is quite mature, powerful, and capable in handling the bundling concerns of a complex project in numerous ways. There is a host full of configurations that can be done in order to tweak it according to our requirements.

In this article, we aim to explore just that.

In particular, following we get to see two configurative properties of Rollup — intro and outro — which are obscurely hidden down within the advanced section of the documentation but that would be very useful in certain cases.

If you're an avid Rollup user, these configurations might even get you to say: "Oh yeah, I was looking for just that!"

Let's begin the exploration. Let's roll up!

The very basic setup

Before we dive into exploring intro and outro, let's quickly spare 5 minutes or even less in getting a quick recap of how to set up Rollup to be able to bundle a project directory.

The rollup command

First off, we need to make sure that the rollup command is available for us to call within the project's directory.

The best way to do so, given that the directory is an npm project with a package.json file, is to use an npm script. A good name for the script could be build.

Here's how the package.json would look with this npm script:

{
  ...
  "scripts": {
    "build": "rollup -c -w"
  },
  ...
  "dependencies": {
    "rollup": "^4.9.6"
  }
}

The rollup.config.mjs file

Rollup could be provided a lot of configurations right when we call it in the terminal. However, due to the verbosity of the terminal, there's a high chance that this is used sparingly.

Instead, the most common approach of configuring Rollup is to use a rollup.config.mjs file.

The .mjs extension here is used in order to instruct the Node.js runtime that runs the rollup program to treat the configuration file as an ECMAScript module. Without this, the package.json file would need type:"module" in order to not squawk an error. Otherwise, the runtime would throw an error as soon as it encounters the import keyword.

Here's the general structure of the file:

export default {
   input: inputFilePath,
   output: {
      file: outputFilePath,
      format: format
   },
   plugins: plugins
}

A default-exported object defines all of the settings for the current build process of Rollup. Three typical properties are as follows:

  • input: The path to the input file
  • output: An object containing the necessary details for the bundled, output file.
  • plugins: An array containing a list of plugins to apply to the bundling.

For example, let's say we have the following directory structure of a project:

A simple project using Rollup
A simple project using Rollup

All the scripts reside in the Scripts directory while all the static files (including images and CSS files) reside in the static/scripts directory.

(Another common structure might be the public and src directories, with public being served over HTTP for the public and src kept for the development team only.)

The index.js file in Scripts is the entry point of the project that calls upon all the rest of the files in Script. And this index.js file is exactly the file that Rollup is concerned with.

Here's how we'd set up Rollup to bundle index.js (and the entire tree of files that are imported into it) into a bundle.js file inside static/scripts:

export default {
   input: 'Scripts/index.js',
   output: {
      file: 'static/scripts/bundle.js',
      format: 'iife'
   },
}

Now, as you can see, this example doesn't utilize any of the potent plugin power of Rollup. However, usually projects using Rollup do. Let's do that up next.

Plugins

The great thing about Rollup is that it is easily extendible via external plugins that can be used to modify how the bundling process goes and what happens therein.

Let's use the terse plugin in our example. terse is used to make a bundle terse, i.e. compact. It minifies all the code in the bundle and saves on a significant number of bytes.

Here's the code with the plugin:

import terse from '@rollup/plugins-terse';

export default {
   input: 'Scripts/index.js',
   output: {
      file: 'static/scripts/bundle.js',
      format: 'iife'
   },
   plugins: [
      terse()
   ]
}

Now, in order to perform the bundling, we just ought to call npm run build:

> npm run build

And that's essentially it for the quick review of Rollup.

It's now time to get to the very crux of this article — Rollup's intro and outro output options.

The intro and outro properties

The optional intro and outro properties go inside the output object that's part of the default export of Rollup's configuration file. They specify additional code to prepend or append to the wrapper of a code bundle.

Let's see how they both work.

intro specifies a piece of text to add right at the beginning of the entire bundled code before it is converted to the desired output format.

On the same lines:

outro specifies a piece of text to add right at the end of the entire bundled code before it is converted to the desired output format.

(By the way, 'outro' is actually a word in English. It means: "a closing section." Good nomenclature for the property, isn't it?)

It's extremely important to note an important point in both these definitions. That is, intro and outro get applied before the code is converted into a given format by Rollup.

In contrast, the banner and footer properties (which may seem identical to intro and outro) get applied after the code is converted into a given format by Rollup.

It's easier seen than said, so if you're confused by this distinction, don't worry, for we'll demonstrate intro and outro up next using a concrete example.

An intro/outro example

Going with the same setup as shown above (with Scripts and static/scripts), suppose that the index.js file has the following code in it:

function add(a, b) {
   return a + b;
}

console.log(add(2, 3));

A simple function adding two numbers together followed by a console log statement.

If we bundle this file (into bundle.js inside static/scripts), here's the code we get (without the terse plugin):

(function () {
   'use strict';

   function add(a, b) {
      return a + b;
   }

   console.log(add(2, 3));

})();

Notice that the code is wrapped inside an IIFE and that's because we specified so in the rollup.config.mjs file.

IIFE stands for Immediately Invoked Function Expression. If you wish to explore more details about an IIFE in JavaScript and why it's often used to wrap code, consider reading JavaScript Functions - Basics: IIFEs.

If we wish to add some code before and after the code wrapped up inside the IIFE, we seek the intro and outro properties. In other words, intro and outro go inside the IIFE (or any other format-specific wrapper function).

This is exactly what Rollup's official documentation for intro/outro states:

[They are] similar to output.banner/output.footer, except that the code goes inside any format-specific wrapper.

Let's say we want to add a console log before the code begins executing and after it all finishes executing. In this case, we can leverage intro and outro as shown below:

export default {
   input: 'Scripts/index.js',
   output: {
      file: 'static/scripts/bundle.js',
      format: 'iife',
intro: "console.log('Starting');",
outro: "console.log('Ending');" } }

When the bundling completes, here's what the bundled file contains:

(function () {
   'use strict';

console.log('Starting'); function add(a, b) { return a + b; } console.log(add(2, 3));;
console.log('Ending') })();

Notice how the intro text comes at the start inside the IIFE while the outro text comes at the end, again inside the IIFE.

And this is essentially everything about intro and outro.

At this point you might be wondering "Yeah well, intro and outro are good, but what's so exciting about them?"

Well, sometimes it's not the excitement around a given utility but rather around the problem that it can solve. And one such problem that can be solved easily using intro/outro while working with Rollup is discussed next.

A more practical example

In browser's, to keep from delaying the occurrence of the very important DOMContentLoaded event and load as well, developers usually defer the execution of certain pieces of code until one or the other happens.

For instance, if we have a CSS stylesheet to import a font — the ones that are typically used to import the cool stuff from Google Fonts — we could do the following:

<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Figtree:ital,wght@0,500;0,700&display=swap">

There's nothing wrong with this per se. However, on a high-latency network, it will keep the page loading and loading until the CSS file and its imported fonts aren't completely downloaded. This can be problematic.

As web technology has evolved, so has the expectation of every user using it. Now, users don't expect websites to stay unresponsive for more than a handful of seconds (yes that's right — seconds!!). In this regard, optimizing websites to make sure that they load faster than the blink of an eye, or at least close to it, is a paramount craft to exercise.

There are numerous things we could do to improve the loading times of a webpage. One of them is to render fonts as soon as the document is loaded completely, i.e. upon the occurrence of its load event.

In order to render the stylesheet shown above in this way, here's the JavaScript code we'll need:

function loadStylesheet(href) {
   var linkElement = document.createElement('link');
   linkElement.rel = 'stylesheet';
   linkElement.href = href;
   document.head.appendChild(linkElement);
}

window.addEventListener('load', function() {
   loadStylesheet('https://fonts.googleapis.com/css2?family=Figtree:ital,wght@0,500;0,700&display=swap');
});

It's fairly simple enough to understand.

Sometimes, while developing complex apps, it won't just be external font stylesheet imports that ought to be put inside a load handler; we might have whole scripts waiting to get the same treatment. Now this comes with an issue.

How can we bundle such programs?

Let's consider an example to help clarify this.

Suppose we have two different JavaScript files, one loading a couple of external fonts and one loading a couple of images, both upon the occurrence of the load event:

fonts.js
window.addEventListener('load', function() {
   function loadStylesheet(href) {
      var linkElement = document.createElement('link');
      linkElement.rel = 'stylesheet';
      linkElement.href = href;
      document.head.appendChild(linkElement);
   }

   // Load a couple of fonts.
   loadStylesheet('https://fonts.googleapis.com/css2?family=Figtree:ital,wght@0,500;0,700&display=swap');
   loadStylesheet('https://fonts.googleapis.com/css2?family=Commissioner:ital,wght@0,500;0,700&display=swap');
});
images.js
window.addEventListener('load', function() {
   document.querySelectorAll('.lazy-img').forEach(function(lazyImageElement) {
      // Load the image by assigning it a src attribute.
      lazyImageElement.src = lazyImageElement.getAttribute('data-src');
   });
});

Now we wish to bundle both these files together.

Clearly, there's nothing difficult about that. We'd have the following in index.js:

index.js
import './fonts';
import './images';

Upon bundling, here's what we'd get:

bundle.js
(function () {
   'use strict';

   window.addEventListener('load', function() {
      function loadStylesheet(href) {
         var linkElement = document.createElement('link');
         linkElement.rel = 'stylesheet';
         linkElement.href = href;
         document.head.appendChild(linkElement);
      }

      // Load a couple of fonts.
      loadStylesheet('https://fonts.googleapis.com/css2?family=Figtree:ital,wght@0,500;0,700&display=swap');
      loadStylesheet('https://fonts.googleapis.com/css2?family=Commissioner:ital,wght@0,500;0,700&display=swap');
   });

   window.addEventListener('load', function() {
      document.querySelectorAll('.lazy-img').forEach(function(lazyImageElement) {
         // Load the image by assigning it a src attribute.
         lazyImageElement.src = lazyImageElement.getAttribute('data-src');
      });
   });

})();

However, if we suppose that there are 10 - 20 such files, we might start to question our decision of wrapping up every single file's main logic with a load event handler.

Is that the right thing to do? Well, probably or probably not.

If the answer is the latter, how could we improve this in Rollup?

The answer is to use intro/outro.

If you notice the bundled code above, the entire part before and including the load handler's left brace ({) could be made the intro while the entire part after and including the handler's right brace (}) could be made the outro.

In this way, we could let go off the handler from both the files and have their code written in the top level individually, while the bundled code generated by Rollup automatically gets intro and outro injected before and after it, respectively, so to have the entire code inside one single load handler.

Simple.

In order to achieve this, we can either manually extract the part before and including the left brace ({) and assign it to intro, and do the same for outro. Or, we could be slightly lazier — in a programmatic way — and get this to be done by code itself.

We are quite lazy (in a good way), and so we'll go with the latter.

We start by creating a template JavaScript file that defines the code where our bundled code ought be injected:

template.js
window.addEventListener('load', function() {
   /* ... */
});

In this file, the comment /* ... */ holds a special meaning — it's where the bundled code will get injected by Rollup.

So far, so good.

Now, let's come to rollup.config.mjs and extract the text of intro and outro from this template file. We'll use Node's readFileSync() function for this task:

import { readFileSync } from 'fs';
 
const PLACEHOLDER = '/* ... */';

const fileStr = readFileSync('./template.js').toString();
const index = fileStr.indexOf(PLACEHOLDER);
const intro = fileStr.slice(0, index);
const outro = fileStr.slice(index + PLACEHOLDER.length);

export default {
   input: 'Scripts/index.js',
   output: {
      file: 'static/scripts/bundle.js',
      format: 'iife',
      intro,
      outro
   }
}

Both the files, fonts.js and images.js will get modified as well:

fonts.js
function loadStylesheet(href) {
   var linkElement = document.createElement('link');
   linkElement.rel = 'stylesheet';
   linkElement.href = href;
   document.head.appendChild(linkElement);
}

// Load a couple of fonts.
loadStylesheet('https://fonts.googleapis.com/css2?family=Figtree:ital,wght@0,500;0,700&display=swap');
loadStylesheet('https://fonts.googleapis.com/css2?family=Commissioner:ital,wght@0,500;0,700&display=swap');
images.js
document.querySelectorAll('.lazy-img').forEach(function(lazyImageElement) {
   // Load the image by assigning it a src attribute.
   lazyImageElement.src = lazyImageElement.getAttribute('data-src');
});

See how we've thrown away the load handler from both of these files, having put all our trust in Rollup's intro and outro.

With the code written, let's see how the bundling goes.

This time, when we bundle the application code, here's what we get:

bundle.js
(function () {
   'use strict';

   window.addEventListener('load', function() {


   function loadStylesheet(href) {
      var linkElement = document.createElement('link');
      linkElement.rel = 'stylesheet';
      linkElement.href = href;
      document.head.appendChild(linkElement);
   }

   // Load a couple of fonts.
   loadStylesheet('https://fonts.googleapis.com/css2?family=Figtree:ital,wght@0,500;0,700&display=swap');
   loadStylesheet('https://fonts.googleapis.com/css2?family=Commissioner:ital,wght@0,500;0,700&display=swap');

   document.querySelectorAll('.lazy-img').forEach(function(lazyImageElement) {
      // Load the image by assigning it a src attribute.
      lazyImageElement.src = lazyImageElement.getAttribute('data-src');
   });


   });

})();

See? Both the code files get amalgamated into one single block of code, wrapped up inside just one load handler, all thanks to intro and outro.

Never miss an article

Follow Codeguage on either of the following channels and stay updated with every latest blog article that we publish.

Improve your web development knowledge, today!