Let’s see how to write a JavaScript library using ES2015 transpiled with Babel, featuring ES modules packed up using Rollup via Gulp, and Jest to test your code.

Here I’m sharing the experience I had while developing my vanilla-lazyload script, since at the time I didn’t find anything that explained how to use those libraries together, especially Rollup and Jest.

Installing Gulp

Gulp is a task runner similar to Grunt for automating painful or time-consuming tasks in your development workflow. I chose to migrate from Grunt to Gulp because the latter is blazing fast, being it based on Node’s streams instead of the file system.

In order to use Gulp, you need to install Gulp locally to your project, to do so:

npm install --save-dev gulp 

A good option to use Gulp is to install its command line executable with:

npm install -g gulp-cli

For more information, I would suggest to read Gulp for beginners on CSS tricks.

Configuring gulp

Gulp execution requires a configuration file named gulpfile.js. Create an empty one for now, I’m going to show you what to put in it, step by step.

1. Linting source files

Being JavaScript a dynamic and loosely-typed language, is especially prone to developer errors. Linting tools like ESLint allow developers to discover problems with their JavaScript code without executing it.

To install ESLint for Gulp, just run:

npm install --save-dev gulp-eslint 

Then in your empty gulpfile.js add:

var gulp = require("gulp");
var eslint = require("gulp-eslint");

gulp.task("default", function () {
    process.env.NODE_ENV = "release";
    return gulp.src("./src/**/*.js")
        // ----------- linting --------------
        .pipe(eslint())
        .pipe(eslint.format())
        .pipe(eslint.failAfterError()) // --> fails if errors
        // --> pipe more stuff here 
});

NOTE: Setting the process.env.NODE_ENV variable is necessary to match the task we’re running in the Babel configuration. I’ll explain this in a while.

2. Bundling modules with Rollup

Rollup is a module bundler for JavaScript which compiles small pieces of code into something larger, such as a library. It uses the new standardized format for code modules included in the ES2015 revision of JavaScript, today.

Long story short, Rollup reads your main JavaScript file and generates a bigger file with all the modules included, automatically.

Install rollup with:

npm install --save-dev gulp-rollup gulp-rename 

Then in your gulpfile, just add a pipe command:

// ----------- rolling up --------------
.pipe(rollup({
    format: "umd",
    moduleName: "LazyLoad",
    entry: "./src/lazyload.js"
}))

This means that Rollup has to produce a umd type of module, the produced module name will be LazyLoad, and the entry point to start looking for dependencies is ./src/lazyload.js.

Since I want to distribute a version of the script which is not transpiled in ES5, I save it to lazyload.es2015.js piping the rename command and the dest one.

.pipe(rename("lazyload.es2015.js"))
.pipe(gulp.dest(destFolder)) // --> writing rolledup

destFolder is a JavaScript variable and it’s just set to ./dist inside our gulpfile.js.

NOTE: Webpack is a similar bundling script which might be preferrable to bundle complex applications, but I noticed that the final code generated by Webpack would be much heavier, so Rollup worked better for me.

3. Transpiling to ES5 with Babel

Until now, we have bundled all our modules in a single file which will work in modern browsers only. Of course we also want to produce a file which is readable by not so modern browsers.

To install babel and its ES2015 preset, plus a plugin to transform Object.assign() to ES5, do:

npm install --save-dev babel-core gulp-babel babel-preset-es2015 babel-plugin-transform-object-assign

Then, to babelize our previously rolled-up JavaScript and save it to lazyload.js, let’s require gulp-babel and add the following pipe to our gruntfile.js:

var babel = require("gulp-babel");
// ----------- babelizing --------------
.pipe(babel())
.pipe(rename("lazyload.js"))
.pipe(gulp.dest(destFolder)) // --> writing babelized es5 js

The babel configuration has to be stored in an external .babelrc file, that in my case looks like that:

{
    "ignore": [
        "node_modules/**"
    ],
    "env": {
        "test": {
            "presets": [
                "es2015"
            ],
            "plugins": ["transform-object-assign"]
        },
        "release": {
            "presets": [
                ["es2015", {
                    "modules": false
                }]
            ],
            "sourceMap": false,
            "plugins": ["transform-object-assign"]
        }
    }
}

You may have noticed that I configured two envs differently:

  • release is the one used by Rollup, in Gulp. Remember? We set it in the gulpfile.js using process.env.NODE_ENV = "release"
  • test is the one used by Jest (explanation below)

4. Minifying with Uglify

Uglify JS is a JavaScript compressor toolkit commonly used to minify the distribution version of JavaScript libraries.

npm install --save-dev gulp-uglify

Then, add the following require and pipe to our gruntfile.js:

var uglify = require("gulp-uglify");
// ----------- minifying --------------
.pipe(uglify())
.pipe(rename("lazyload.min.js"))
.pipe(gulp.dest(destFolder)); // --> writing uglified

All together now

This is how your gulpfile.js should look like at the end:

var gulp = require("gulp");
var eslint = require("gulp-eslint");
var rollup = require("gulp-rollup");
var rename = require("gulp-rename");
var babel = require("gulp-babel");
var uglify = require("gulp-uglify");

var destFolder = "./dist";

gulp.task("default", function () {
    process.env.NODE_ENV = "release";
    return gulp.src("./src/**/*.js")
        // ----------- linting --------------
        .pipe(eslint())
        .pipe(eslint.format())
        .pipe(eslint.failAfterError()) // --> failing if errors
        // ----------- rolling up --------------
        .pipe(rollup({
            format: "umd",
            moduleName: "LazyLoad",
            entry: "./src/lazyload.js"
        }))
        .pipe(rename("lazyload.es2015.js"))
        .pipe(gulp.dest(destFolder)) // --> writing rolledup
        // ----------- babelizing --------------
        .pipe(babel())
        .pipe(rename("lazyload.js"))
        .pipe(gulp.dest(destFolder)) // --> writing babelized ES5
        // ----------- minifying --------------
        .pipe(uglify())
        .pipe(rename("lazyload.min.js"))
        .pipe(gulp.dest(destFolder)); // --> writing uglified
});

gulp.task("watch", function () {
    gulp.watch("./src/**/*.js", ["default"]);
    // Other watchers
});

The "watch" part is so that if we run the gulp watch command, gulp watches the src folder for changes and re-creates the dist files automatically. Sweet.

Running Gulp

You can now build your library executing Gulp in single run:

gulp

Or make it watch for changes in your src files if you’re still developing:

gulp watch

Making Jest work along with Rollup

I’ll write another in-depth post about how I used Jest to test modules which would become private uglified variables and functions, but the point here is how to make Jest work together with Rollup, since the two require different Babel configuration.

As we saw, the Babel configuration is split in two envs and the one set and used by Jest is test. This is because Jest just needs the presets object to contain ["es2015"], whereas rollup requires ["es2015", {"modules": false}].

If you did what I explained correctly, Jest should work out of the box.

npm install --save-dev jest
npm install -g jest-cli

Then just launch jest like that:

jest

More information on Jest on the Get Started page.

Further steps

A more advanced configuration would be to configure npm to be the command line interface for the build and the tests, so that we could run npm run build and npm run test to launch either the build or the tests, or better include the testing process in the build so whenever our tests fail, the build fails too. I will do that as next steps.