October
2014

A browserify workflow, part 2

I wrote an article on working with React JS in a browserify workflow. Well, I got some more experience with it and here is PART2. You can grab this boilerplate at react-app-boilerplate.

There were specifically two things we did not handle very well on the initial workflow:

  1. External dependencies
  2. Testing

External dependencies

When browserify rebundles your project it goes through all the require statements in your application code and figures out what to put into your bundle. Luckily we have watchify that makes sure that it does not rebundle what it already has bundled, BUT browserify still goes through the files and figures out if something should be done. You might not notice this when only React JS is part of the bundle, but as you load up more dependencies it can take several seconds on each save and rebundle of your project.

What we want to do is only touch these external dependencies once and never touch them again during the workflow session. You will not do any changes to React JS or underscore I suppose?

Testing

The initial version of this workflow used Karma for testing. Now Karma is great, but by including browserify as part of the testing it got very slow. By letting our current workflow bundle our tests with external dependencies and watchify we can make this a lot faster! It would also be nice that our tests run automatically when we do changes to our project.

Our Gulpfile.js


// We need a bunch of dependencies, but they all do an important
// part of this workflow
var gulp = require('gulp');
var source = require('vinyl-source-stream');
var browserify = require('browserify');
var watchify = require('watchify');
var reactify = require('reactify'); 
var gulpif = require('gulp-if');
var uglify = require('gulp-uglify');
var streamify = require('gulp-streamify');
var notify = require('gulp-notify');
var concat = require('gulp-concat');
var cssmin = require('gulp-cssmin');
var gutil = require('gulp-util');
var shell = require('gulp-shell');
var glob = require('glob');
var livereload = require('gulp-livereload');
var jasminePhantomJs = require('gulp-jasmine2-phantomjs');

// We create an array of dependencies. These are NPM modules you have
// installed in node_modules. Think: "require('react')" or "require('underscore')"
var dependencies = [
  'react'
];

// Now this task both runs your workflow and deploys the code,
// so you will see "options.development" being used to differenciate
// what to do
var browserifyTask = function (options) {

  // First we define our application bundler. This bundle is the
  // files you create in the "app" folder 
  var appBundler = browserify({
    entries: [options.src], // The entry file, normally "main.js"
    transform: [reactify], // Convert JSX style
    debug: options.development, // Sourcemapping
    cache: {}, packageCache: {}, fullPaths: true // Requirement of watchify
  });

  // We set our dependencies as externals of our app bundler.
  // For some reason it does not work to set these in the options above 
  appBundler.external(options.development ? dependencies : []);

  // This is the actual rebundle process of our application bundle. It produces
  // a "main.js" file in our "build" folder. */
  var rebundle = function () {
    var start = Date.now();
    console.log('Building APP bundle');
    appBundler.bundle()
      .on('error', gutil.log)
      .pipe(source('main.js'))
      .pipe(gulpif(!options.development, streamify(uglify())))
      .pipe(gulp.dest(options.dest))

      // It notifies livereload about a change if you use it
      .pipe(gulpif(options.development, livereload())) 
      .pipe(notify(function () {
        console.log('APP bundle built in ' + (Date.now() - start) + 'ms');
      }));
  };

  // When we are developing we want to watch for changes and
  // trigger a rebundle
  if (option.development) {
    appBundler = watchify(appBundler);
    appBundler.on('update', rebundle);
  }

  // And trigger the initial bundling
  rebundle();

  if (options.development) {

    // We need to find all our test files to pass to our test bundler
    var testFiles = glob.sync('./specs/**/*-spec.js');

    // This bundle will include all the test files and whatever modules
    // they require from the application
    var testBundler = browserify({
      entries: testFiles,
      debug: true,
      transform: [reactify],
      cache: {}, packageCache: {}, fullPaths: true // Requirement of watchify
    });

    // Again we tell this bundle about our external dependencies
    testBundler.external(dependencies);

    // Now this is the actual bundle process that ends up in a "specs.js" file
    // in our "build" folder
    var rebundleTests = function () {
      var start = Date.now();
      console.log('Building TEST bundle');
      testBundler.bundle()
        .on('error', gutil.log)
        .pipe(source('specs.js'))
        .pipe(gulp.dest(options.dest))
        .pipe(livereload()) // Every time it rebundles it triggers livereload
        .pipe(notify(function () {
          console.log('TEST bundle built in ' + (Date.now() - start) + 'ms');
        }));
    };

    // We watch our test bundle
    testBundler = watchify(testBundler);

    // We make sure it rebundles on file change
    testBundler.on('update', rebundleTests);

    // Then we create the first bundle
    rebundleTests();

    // And now we have to create our third bundle, which are our external 
    // dependencies, or vendors. This is React JS, underscore, jQuery etc. 
    // We only do this when developing as our deployed code will be one 
    // file with all application files and vendors
    var vendorsBundler = browserify({
      debug: true, // It is nice to have sourcemapping when developing
      require: dependencies
    });

    // We only run the vendor bundler once, as we do not care about changes here,
    // as there are none
    var start = new Date();
    console.log('Building VENDORS bundle');
    vendorsBundler.bundle()
      .on('error', gutil.log)
      .pipe(source('vendors.js'))
      .pipe(gulpif(!options.development, streamify(uglify())))
      .pipe(gulp.dest(options.dest))
      .pipe(notify(function () {
        console.log('VENDORS bundle built in ' + (Date.now() - start) + 'ms');
      }));
  }
}

// We also have a simple css task here that you can replace with
// SaSS, Less or whatever
var cssTask = function (options) {
    if (options.development) {
      var run = function () {
        gulp.src(options.src)
          .pipe(concat('main.css'))
          .pipe(gulp.dest(options.dest));
      };
      run();
      gulp.watch(options.src, run);
    } else {
      gulp.src(options.src)
        .pipe(concat('main.css'))
        .pipe(cssmin())
        .pipe(gulp.dest(options.dest));   
    }
}

// Starts our development workflow
gulp.task('default', function () {

  browserifyTask({
    development: true,
    src: './app/main.js',
    dest: './build'
  });

  cssTask({
    development: true,
    src: './styles/**/*.css',
    dest: './build'
  });

});

// Deploys code to our "dist" folder
gulp.task('deploy', function () {

  browserifyTask({
    development: false,
    src: './app/main.js',
    dest: './dist'
  });

  cssTask({
    development: false,
    src: './styles/**/*.css',
    dest: './dist'
  });

});

// Runs the test with phantomJS and produces XML files
// that can be used with f.ex. jenkins
gulp.task('test', function () {
    return gulp.src('./build/testrunner-phantomjs.html').pipe(jasminePhantomJs());
});

Getting in the flow

  1. Run gulp
  2. Start up a webservice in the “build” folder, f.ex. python -m SimpleHTTPServer 3000
  3. Go to “localhost:3000”, here you will find your app
  4. Go to “localhost:3000/testrunner.html”, here you will find your tests

To make LiveReload work you need to install an extension to Chrome, LiveReload. This puts a small button up in the right corner of your browser. When on you app and/or your tests, hit that button and you will see the little circle in the center will become a black dot. Now these pages will refresh on file changes either in your tests or application files… blazingly fast!

blog comments powered by Disqus