PROFILE

Gulp Browserify ESLint

Gulp Multi-Bundles of Browserify

Browserify brings the beauty of CommonJS to the browser. It bundles up all your dependencies into one file.

This works well with small projects or with Single Page Applications(SPA), where having just one JS file works well. But what happens if you're building a website with lots of views? If you're using browserify, you'll end up with a huge bundle file.

What if we could split the bundle in a main bundle with all the global scripts and libs, and then have a bundle for different views? After a while of research, I could finally come up with a gulp recipe that does just that! We can have multiple bundles(which will be dynamically loaded following a pattern), each watching their dependencies, linting them before bundling and compiled individually. Did I mentioned it is blazing fast? (Well...the first load still takes bit tho...)

I know Webpack can handle this as well, but Webpack and me have a long love/hate relationship...kinda makes me keep coming back to Gulp.

Here is the directory structure I'll use (just includes the JS part):

./Project
    |_ .babelrc
    |_ .eslintrc
    |_ package.json
    |_ gulpfile.babel.js
    |_ index.html
    |_ /static
        |_ /js  # Gulp scripts output 
        |_ /src # Source files
            |_ /js
                |_ app.js
                |_ /modules
                    |_module-1.js
                |_ /views
                    |_ view-1.js

With this structure in mind, I'm gonna create the gulpfile. My main bundle will live at /static/src/js/app.js(I can add more, if necesary) and views will be provided using the following pattern: /static/src/js/views/[section]/[view].js. Every [view].js will create a new bundle.

I'm so used to using ES6 by now that I'll write the gulpfile with it, which needs babel-register & babel-preset-2015 modules to run and you need to add the .babelrc file with -at least- the es2015 preset available.

Here is the command to install the necessary modules for this exercise to work:

yarn add --dev babel-preset-es2015 babel-register babelify browserify glob gulp gulp-eslint gulp-sourcemaps gulp-uglify gulp-util path vinyl-buffer vinyl-source-stream watchify  

I'm using yarn now, which is basically the same as NPM, but the inner works are different and we get a better version handling for our dependencies. If you don't like yarn, just replace yarn add --dev with npm i -D.

Now for the gulpfile, I'm leaving a link to the whole file in github, I'll just concentrate in the JS part, to keep this short. I'll divide it in 3 parts:

  • The gulp task
  • The createBundle function
  • The linter task (I like my code clean)

The gulp task

gulp.task('scripts', (callback) => {

    const mainFiles = [`${[static path]}/js/app.js`];
    glob(`${[static path]}/src/js/views/*/*.js`, (err, files) => {

        if (err) {
            done(err);
        }

        files = [...files, ...mainFiles];

        const tasks = files.map(function (entry, index) {

            createBundle({
                entries: entry,
                output: path.basename(entry),
                destination: `${[static path]}/js`
            });
        });
    });
    return callback();
});

Replace [static path] with the path to your "static" assets or source path.

I'm using glob which is a module that match files using the patterns the shell uses, like stars and stuff. I'm using the [section]/[view].js pattern I mentioned earlier, so every view, will be a separate bundle. The main(or global) file is passed as an element in the array mainFiles, this is where you can add more "main" bundles, if you need to manually add more bundles.

glob will give us the files matching the pattern provided and then we merge those results with the mainFiles array, to get the list of bundles to compile.

Lastly, we're gonna create a task per bundle, at this point, we'll be calling the createBundle() function passing the "entry path", the "output" file(which will be the name of the file to bundle, and where the "destination" path will be.

createBundle function

const internals = {  
    isWatchify: false,
    deps: [] // Here will go global modules. E.G.: ['react', 'react-dom', 'jquery']
};

const createBundle = (options, callback) => {

    const opts = Object.assign({}, watchify.args, options, {
        debug: true
    });

    let b = browserify(opts);
    b.transform(babelify, {presets: ["es2015"]});

    if (path.basename(options.entries) === 'app.js') {
        b.require(internals.deps)
    } else {
        b.external(internals.deps);
    }

    const rebundle = () => {

        return b.bundle()
            // log errors if they happen
            .on('error', gutil.log.bind(gutil, 'Browserify Error'))
            .pipe(source(options.output))
            .pipe(buffer())
            .pipe(sourcemaps.init({ loadMaps: true }))
            .pipe(sourcemaps.write('./maps'))
            .pipe(gulp.dest(options.destination));
    };

    if (internals.isWatchify) {
        b = watchify(b);
        b.on('update', (id) => {

            lint(callback, id);
            rebundle();
        });
        b.on('log', gutil.log);
    }

    return rebundle();
};

This will be the one who creates the bundles with browserify.

First, we create an "options" variable, which gets passed to browserify. Since watchify requires a set of parameters, will merge it with browserify options.

We then pass these options to browserify. Since I'm using ES6, I need the babelify module, for it to be transpiled to pure JS, so I pass this plugin to browserify with the "es2015" preset.

Then I check if the bundle is for the main file, since I'm loading ReactJS everywhere, I'll put it in the main file, so here is where I'm telling browserify that these dependencies will be global.

Gulp requires to return a closure, so I'm creating the rebundle function and return it at the end. Next is the actual bundle task, we tell it to report any error to the console, to prevent the task from halting on error. source will create a conventional text streams, then buffer convert the text streams from b.bundle() to vinyl streams so you can pipe with other gulp plugins which support streaming.

Next we can do things like uglifying our code, sourcemaps and more.

The we save the newly created file to the destination we specified.

Lastly, if we want gulp to automatically watch our files for re-bundling whenever we save a dependency or the actual file, we use watchify to wrap the browserify's variable b. Now we append the "update" listener which will be triggered by watchify.

It will contain the file which was changed, the perfect opportunity to lint our files if we wanted. I'll add that here, passing the task "callback" or "done" callback so if the lint fails, it can exit the task without rebundling.

The linter task

const lint = (callback, src) => {

    return gulp
        .src(src)
        .pipe(eslint({ useEslintrc: true }))
        .pipe(eslint.format());
};

I'm a freak for clean and consistent code, so a linting task is a must for me. It'll be straightforward, get the file to lint, add the rules for our codestyle and lint the file.

I know it's a lot to explain(which I suck at), and if you are more of a try and test something, you can check the whole example in my github repo.

If anyone think I can improve this task or have a question about it, write a comment below 🙂

comments powered by Disqus