Clean & Simple: The Lost Art of Coding
William Drol
William Drol
Home  |  Blog

One Simple Recipe To Make Browserify Even More Awesome

April 7th 2016  |  LinkedIn

Recipes If you've discovered Browserify and love it as much as I do, then you've probably used it already on a variety of projects. It's great to write simple JS components and then let Browserify work its magic automatically. Well, that's not exactly true. It's not quite as automatic or flexible as one might hope, but we can easily correct that with a small bit of custom code.

Ninja Chef

Seriously? It Doesn't Do That Already?

As cool as Browserify is, something obvious seems to be missing. Why can't we just point it at a folder (or folders) and say, "Ok, start there" and let it figure out all the required files, no matter how many nested sub-folders. Well, not so fast. Browserify requires a main application entry point: a single JS file that lists all components in the application, or at least the core components needed to launch the application. This is just how Browserify behaves out of the box.

What About Websites?

Even though websites are more like traditional applications than ever before, they're still a bit ambiguous. They often contain hundreds of components without a well-defined, single point of entry. Since Browserify demands a single point of entry, this would force us to manually construct a Browserify-friendly index file that lists all the dependencies. That's a lot of extra work and ongoing maintenance, so let's do it automatically instead.

The Recipe

This little recipe constructs the Browserify index file for us automatically. It's perfect for websites that contain a large number of ambiguous components, spread out in a variety of places (with or without dependencies on other components). We'll demonstrate using the Gulp task runner. Here are the required Gulp plugins:

var gulp    = require( 'gulp' );
var gufn    = require( 'gulp-fn' );
var gufile  = require( 'gulp-file' );
var bundler = require( 'browserify' );
var vbuffer = require( 'vinyl-buffer' );
var vstream = require( 'vinyl-source-stream' );

Here's the initial Gulp task and some configuration values (adjust these values as needed):

var config =
{
    cwd      : '.' ,
    end      : 'end' ,
    split    : '/Views/' ,
    relPath  : '../Views/' ,
    jsFiles  : '../Views/Components/**/*.js' ,
    jsIndex  : 'index.js' ,
    jsBundle : 'bundle.js'
};

gulp.task( 'scripts' , function()
{
    // gulp task here
});

To create a dynamic list of required Javascript files, we'll need a function to handle each Javascript file that matches the given search path (see config.jsFiles above). Here's the appendText() function:

gulp.task( 'scripts' , function()
{
    var myText = '';

    function appendText()
    {
        return gufn( function( file )
        {
            var absPath = file.path.split( '\\' ).join( '/' );
            var relFile = absPath.split( config.split )[1];

            myText += 'require( "' + config.relPath + relFile + '" );\n';
        });
    };
});

And this is how to call the function (shown in green below):

gulp.task( 'scripts' , function()
{
    var myText = '';

    function appendText()
    {
        return gufn( function( file )
        {
            var absPath = file.path.split( '\\' ).join( '/' );
            var relFile = absPath.split( config.split )[1];

            myText += 'require( "' + config.relPath + relFile + '" );\n';
        });
    };

    // Build string of required javascript files
    return gulp.src( config.jsFiles )
        .pipe( appendText() ).on( config.end , function()
        {
            console.log( myText ); // the final string of files
        });
});

The output as this point (everything stored in the myText variable) should be a list of require() lines. On my computer, the output looks like this:

require( "../Views/Components/Content/Footer/Footer.js" );
require( "../Views/Components/Content/Header/Header.js" );
require( "../Views/Components/Lists/MenuList/MenuList.js" );
require( "../Views/Components/Lists/TreeList/TreeList.js" );
require( "../Views/Components/Layouts/MainLayout/MainLayout.js" );
require( "../Views/Components/Nav/Breadcrumbs/Breadcrumbs.js" );

etc…
etc…
etc…

Now that we have the text, we can write it to an index file (shown in green below):

gulp.task( 'scripts' , function()
{
    var myText = '';

    function appendText()
    {
        return gufn( function( file )
        {
            var absPath = file.path.split( '\\' ).join( '/' );
            var relFile = absPath.split( config.split )[1];

            myText += 'require( "' + config.relPath + relFile + '" );\n';
        });
    };

    // Build string of required javascript files
    return gulp.src( config.jsFiles )
        .pipe( appendText() ).on( config.end , function()
        {
            // Create index file from myText
            gufile( config.jsIndex , myText , { src : true } )
                .pipe( gulp.dest( config.cwd ) ).on( config.end , function()
                {
                    console.log( 'Index file created' );
                });
        });
});

Finally, now that we have a single point of entry (the index file) we can bundle it with Browserify the same as always. The major difference is that we discovered all the required Javascript files dynamically and wrote them into a Browserify-friendly index file. Here's how to bundle the index file (shown in green below).

gulp.task( 'scripts' , function()
{
    var myText = '';
 
    function appendText()
    {
        return gufn( function( file )
        {
            var absPath = file.path.split( '\\' ).join( '/' );
            var relFile = absPath.split( config.split )[1];

            myText += 'require( "' + config.relPath + relFile + '" );\n';
        });
    };

    // Build string of required javascript files
    return gulp.src( config.jsFiles )
        .pipe( appendText() ).on( config.end , function()
        {
            // Create index file from myText
            gufile( config.jsIndex , myText , { src : true } )
                .pipe( gulp.dest( config.cwd ) ).on( config.end , function()
                {
                    // Bundle the index file
                    bundler( config.jsIndex , { debug : true } ).bundle()
                        .pipe( vstream( config.jsBundle ) )
                        .pipe( vbuffer() )
                        .pipe( gulp.dest( config.cwd ) );
                });
        });
});

Here's the final source code listing:

// Gulp plugins
var gulp    = require( 'gulp' );
var gufn    = require( 'gulp-fn' );
var gufile  = require( 'gulp-file' );
var bundler = require( 'browserify' );
var vbuffer = require( 'vinyl-buffer' );
var vstream = require( 'vinyl-source-stream' );


// Configuration values
var config =
{
    cwd      : '.' ,
    end      : 'end' ,
    split    : '/Views/' ,
    relPath  : '../Views/' ,
    jsFiles  : '../Views/Components/**/*.js' ,
    jsIndex  : 'index.js' ,
    jsBundle : 'bundle.js'
};


// Bundler task with dynamic index file
gulp.task( 'scripts' , function()
{
    var myText = '';

    function appendText()
    {
        return gufn( function( file )
        {
            var absPath = file.path.split( '\\' ).join( '/' );
            var relFile = absPath.split( config.split )[1];

            myText += 'require( "' + config.relPath + relFile + '" );\n';
        });
    };

    // Build string of required javascript files
    return gulp.src( config.jsFiles )
        .pipe( appendText() ).on( config.end , function()
        {
            // Create index file from myText
            gufile( config.jsIndex , myText , { src : true } )
                .pipe( gulp.dest( config.cwd ) ).on( config.end , function()
                {
                    // Bundle the index file
                    bundler( config.jsIndex , { debug : true } ).bundle()
                        .pipe( vstream( config.jsBundle ) )
                        .pipe( vbuffer() )
                        .pipe( gulp.dest( config.cwd ) );
                });
        });
});