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.
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 fileoutput
: 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:
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
:
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.
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 tooutput.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:
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');
});
});
Now we wish to bundle both these files together.
Clearly, there's nothing difficult about that. We'd have the following in index.js:
import './fonts';
import './images';
Upon bundling, here's what we'd get:
(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:
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:
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 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:
(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
.