Tuesday, 18 September 2018

Introduction to Rollup.js

rollup.js is a module bundler which compiles JavaScript  modules into a library or application.


In this article I want to go through Rollup Tutorial and:

  • install Rolllup.js
  • create an application which consists of modules 
  • demonstrate how Rollup bundles multiple files into a single one


Rollup Installation 



Instead of installing Rollup.js globally as in the tutorial, I'll install it locally as I want to make it portable - all clones of this project will use the same Rollup version.

Let's see how package.json looks after executing npm init:

C:\wherever\rollup-js-demo>type package.json
{
  "name": "rollup-js-demo",
  "version": "1.0.0",
  "description": "Application which demoes Rollup.JS basic features.",
  "main": "main.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "repository": {
    "type": "git",
    "url": "git+https://github.com/BojanKomazec/rollup-js-demo.git"
  },
  "keywords": [
    "rollup.js",
    "demo"
  ],
  "author": "Bojan Komazec",
  "license": "ISC",
  "bugs": {
    "url": "https://github.com/BojanKomazec/rollup-js-demo/issues"
  },
  "homepage": "https://github.com/BojanKomazec/rollup-js-demo#readme"
}

To install Rollout.js locally (per project) we'll omit -g in npm install. By default npm install rollout would add Rollout to dependencies but as it is a dependency used in development we'll use --save-dev option:

C:\wherever\rollup-js-demo>npm install --save-dev rollup
+ rollup@0.66.0
added 3 packages from 33 contributors and audited 3 packages in 0.903s
found 0 vulnerabilities

If we now check package.json, Rollout will be listed in devDependencies:

C:\wherever\rollup-js-demo>type package.json
{
  "name": "rollup-js-demo",
  "version": "1.0.0",
  "description": "Application which demoes Rollup.JS basic features.",
  "main": "main.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "repository": {
    "type": "git",
    "url": "git+https://github.com/BojanKomazec/rollup-js-demo.git"
  },
  "keywords": [
    "rollup.js",
    "demo"
  ],
  "author": "Bojan Komazec",
  "license": "ISC",
  "bugs": {
    "url": "https://github.com/BojanKomazec/rollup-js-demo/issues"
  },
  "homepage": "https://github.com/BojanKomazec/rollup-js-demo#readme",
  "devDependencies": {
    "rollup": "^0.66.0"
  }
}

As this package is installed locally we won't be able to access its executable from anywhere - we have to go to its directory. We can execute it and for example check Rollout.js version first:

C:\wherever\rollup-js-demo\node_modules\.bin>rollup --version
rollup v0.66.0

Or, from the root directory:

C:\wherever\rollup-js-demo>npx rollup --version
rollup v0.66.0

Let's check command line arguments of Rollup CLI:

C:\wherever\rollup-js-demo>npx rollup

rollup version 0.66.0
=====================================

Usage: rollup [options] <entry file>

Basic options:

-v, --version               Show version number
-h, --help                  Show this help message
-c, --config                Use this config file (if argument is used but value
                              is unspecified, defaults to rollup.config.js)
-w, --watch                 Watch files in bundle and rebuild on changes
-i, --input                 Input (alternative to <entry file>)
-o, --file <output>         Output (if absent, prints to stdout)
-f, --format [es]           Type of output (amd, cjs, es, iife, umd)
-e, --external              Comma-separate list of module IDs to exclude
-g, --globals               Comma-separate list of `module ID:Global` pairs
                              Any module IDs defined here are added to external
-n, --name                  Name for UMD export
-m, --sourcemap             Generate sourcemap (`-m inline` for inline map)
--amd.id                    ID for AMD module (default is anonymous)
--amd.define                Function to use in place of `define`
--no-strict                 Don't emit a `"use strict";` in the generated modules.
--no-indent                 Don't indent result
--environment <values>      Settings passed to config file (see example)
--no-conflict               Generate a noConflict method for UMD globals
--no-treeshake              Disable tree-shaking
--silent                    Don't print warnings
--intro                     Content to insert at top of bundle (inside wrapper)
--outro                     Content to insert at end of bundle (inside wrapper)
--banner                    Content to insert at top of bundle (outside wrapper)
--footer                    Content to insert at end of bundle (outside wrapper)
--no-interop                Do not include interop block

Examples:

# use settings in config file
rollup -c

# in config file, process.env.INCLUDE_DEPS === 'true'
# and process.env.BUILD === 'production'
rollup -c --environment INCLUDE_DEPS,BUILD:production

# create CommonJS bundle.js from src/main.js
rollup --format=cjs --file=bundle.js -- src/main.js

# create self-executing IIFE using `window.jQuery`
# and `window._` as external globals
rollup -f iife --globals jquery:jQuery,lodash:_ \
  -i src/app.js -o build/app.js -m build/app.js.map

Notes:

* When piping to stdout, only inline sourcemaps are permitted

For more information visit https://rollupjs.org



Writing an Application in a Modular Way



Now when we have Rollup installed, we can create our project which consist of an entry point script which imports a module:

C:\wherever\rollup-js-demo\src\scripts\modules\foo.js:

export default 'hello world!';

C:\wherever\rollup-js-demo\src\scripts\main.js:

// application entry point
import foo from "./modules/foo.js";
export default function() {
    console.log(foo);
}


Bundling Modules With Rollup



Let's create a bundle and push it onto stdout:

C:\wherever\rollup-js-demo\node_modules\.bin>rollup ../../src/scripts/main.js -f cjs

../../src/scripts/main.js → stdout...
'use strict';

// a module

// export a primitive (string in this case)
var foo = 'hello world!';

// application entry point
function main() {
    console.log(foo);
}

module.exports = main;
created stdout in 39ms

Rollup took couple of parameters:
  • path to the JS script which is application's entry point
  • -f cjs - bundle format specification; in this case cjs - CommonJS runnable by Node.js


Running the Bundle in Node Console



To run this bundle with Node.js we have to save it to the file:

C:\wherever\rollup-js-demo\node_modules\.bin>rollup ../../src/scripts/main.js -f cjs -o ../../build/js/bundle.js

../../src/scripts/main.js → ../../build/js/bundle.js...
created ../../build/js/bundle.js in 40ms

Let's run the bundle now with Node.js:

C:\wherever\rollup-js-demo\node_modules\.bin>node
> let bundle = require('../../build/js/bundle.js');
undefined
> bundle()
hello world!
undefined

For each function Node outputs its return value. If function has only side effects (like printing something on the stdout) Node will output undefined as its return value. [source]


Using Configuration File



Instead of passing a (potentially long) list of parameters and options to rollup executable, we can place them into a configuration file (which has to be added manually, there is no some Rollup option which would create it (like --init or something similar...)):

C:\wherever\rollup-js-demo\rollup.config.js:

export default {
    input: '../../src/scripts/main.js',
    output: {
        file: '../../build/js/bundle.js',
        format: 'cjs'
    }
}

input: entry point - a file that imports other modules. Only single file is allowed by default. rollup-plugin-multi-entry can be used to use multiple entry points in the rollup bundle.
output: a bundle file; defaults to stdout if not specified
format: specifies what kind of bundle is to be created:
  • cjsCommonJS (runs in Node.js)
  • iife - Immediately-Invoked Function Expression (runs in the browser)
sourceMap: adds a sourcemap inside the generated file (helpful for debugging)



    Paths in configuration file have to be relative to the path of the Rollup executable if we call it directly.

    Rollup now only needs to be passed the path to configuration file:

    C:\wherever\rollup-js-demo\node_modules\.bin>rollup -c ../../rollup.config.js

    ../../src/scripts/main.js → ../../build/js/bundle.js...
    created ../../build/js/bundle.js in 18ms

    Rollup creates all directories in specified path to the output file. In this case that is: build/js.

    If we use npx rollup paths in the config file can be relative to the project root directory (where npx is usually called from):

    C:\wherever\rollup-js-demo\rollup.config.js:

    export default {
        input: 'src/scripts/main.js',
        output: {
            file: 'build/js/bundle.js',
            format: 'cjs'
        }
    }

    We can now call it like this:

    C:\wherever\rollup-js-demo>npx rollup -c

    If we try to call rollup -c when rollup.config.js does not exist, we'll get an error:

    C:\wherever\rollup-js-demo>npx rollup -c
    ENOENT: no such file or directory, lstat 'C:\wherever\rollup-js-demo\rollup.config.js'

    In the real life web apps are usually created as Node projects so we'd have package.json with the script which runs rollup e.g.:

      "scripts": {
        ...
        "build": "rollup -c --environment BUILD:production"
      }

    In this case we'd run npm run build and npm knows how to call locally installed dependencies (it's probably using npx under the bonnet).

    Plugins configuration


    If we install some of the Rollup's plugins, their configuration can be embedded into rollup.config.js. E.g. if we install rollup-plugin-node-resolve, it and its configuration will be listed under plugins section:

    import resolve from 'rollup-plugin-node-resolve';

    export default {
      input: 'main.js',
      output: {
        file: 'bundle.js',
        format: 'iife'
      },
      name: 'MyModule',
      plugins: [
        resolve({
           ...
           extensions: ['.js', '.json'],
           browser: true,
           ...
        })
      ]
    };

    Creating a Bundle Loadable Into Web Page



    Let's now create a bundle script that can be loaded into an HTML file. Let's say that now instead of printing foo value into console we want to print it in HTML page. Let's define first HTML page.

    C:\wherever\rollup-js-demo\test\index.html:

    <!DOCTYPE html>
      <head>
        <meta charset="utf-8">
        <title>Rollup.js Demo</title>
      </head>
      <body>
        <p id="log"></p>
        <script src="../build/js/bundle-browser.js" async defer></script>
      </body>
    </html>

    bundle-browser.js is a bundle output that will be created for this test.

    Let's create another entry point JS file which is adjusted to the web page it will be loaded into.

    C:\wherever\rollup-js-demo\src\scripts\main-browser.js:

    // application entry point
    import foo from "./modules/foo.js";

    const log = document.getElementById('log');
    log.innerText = 'foo value (loaded from a module) is: \n';
    log.innerText += foo;

    As we now have different input and output files, we can create another Rollup config file.

    C:\wherever\rollup-js-demo\rollup.config.browser.js:

    export default {
        input: '../../src/scripts/main-browser.js',
        output: {
            file: '../../build/js/bundle-browser.js',
            format: 'iife',
            name: 'main'
        }
    }

    The content of the JS file aimed to be loaded into web page should be wrapped in the auto-executed function - therefore we have to use iife output format.

    If output.name is not specified Rollup gives the following error:
    Error: You must supply output.name for IIFE bundles

    Let's now create a bundle:

    C:\wherever\rollup-js-demo\node_modules\.bin>rollup -c ../../rollup.config.browser.js

    ../../src/scripts/main-browser.js → ../../build/js/bundle-browser.js...
    created ../../build/js/bundle-browser.js in 25ms

    C:\wherever\rollup-js-demo\build\js\bundle-browser.js:

    (function () {
    'use strict';

    // a module

    // export a primitive (string in this case)
    var foo = 'hello world!';

    // application entry point

    const log = document.getElementById('log');
    log.innerText = `Demo app: \n\n`;
    log.innerText += foo;

    }());

    If we open index.html in browser, we can see:


    Check out the source code for this demo (on my GitHub).

    What about TypeScript source files?


    We can have TypeScript file specified as an entry point. To learn how to achieve that read in this article on this blog.


    Further reading:

    Rollup.js Tutorial, Part 1: How to Set Up Smaller, More Efficient JavaScript Bundling Using Rollup

    Article ToDo:


    • introduce using npx (so can be called as npx rollup) or at least adding rollup : rollup within scripts section of package.json so it can be run as npm rollup - all this is to avoid calling a locally installed package from .bin directory and passing relative paths which is very messy...

    No comments: