ES6 + browserify + babel + gulp + jasmine

During Socrates Conference 2015 we decided that it's the right time to jump in ES6 to developer a green field project that our customer is starting. Given that ES6 is already the stable and latest version of JavaScript, it does not make sense to start a new project with ES5, an already old version of the language. With the kind of functional style that we use when coding in JavaScript, the changes are not too big anyway as we are not going to use classes anyway. But we can leverage from block scope with "let" and "const" avoiding the use of "var" from now on. Configuring the tools has taken more time than I thought as the tooling and the ecosystem is changing really fast. You read a recipe from a blog post which is 6 months old and it turns out that most of the stuff described is no longer working. Some npm packages don't work anymore or the behavior at some point is very different from a previous version... apparently things are even more obscure on Windows which is the platform we have to use for this project.

As an example, I installed karma via npm the latest version. But when running gulp from the command line it stopped working with no error message at all, just a line break and back to the prompt. I commented all the lines in the gulpfile.js and then uncommented lines one by one executing gulp each time to discover that "require('karma')" was the reason. So I got into the node repl and type this myself:

var k = require('karma')

The result was the same, node repl exited silently getting me back to the command line prompt. I couldn't find a single way to catch the error although I tried try-catch, signal capturing, domains... and none of that worked. Then I started downgrading the version of the karma package until it worked for me. Version 0.13.3 works but 0.13.4 doesn't. It must be a very specific problem on my machine but I couldn't find any other solution. Eventually we are not using karma for now, we are using jasmine stand alone version and mocha.

This is the simplest gulpfile I was able to get working:

  1. var gulp = require('gulp');
  2. var sourcemaps = require("gulp-sourcemaps");
  3. var mainBowerFiles = require('main-bower-files');
  4. var browserify = require('browserify');
  5. var vinylSource = require('vinyl-source-stream');
  6. var glob = require('glob');
  7. var vinylBuffer = require('vinyl-buffer');
  8. var babel = require('babelify');
  9.  
  10. var globalNamespace = 'mynamespace';
  11. var launcher = './Scripts/src/main.js';
  12. var sourceFiles = 'Scripts/src/*.js';
  13. var specFiles = 'Scripts/spec/*.js';
  14. var libFiles = 'Scripts/dist/vendor/*.js';
  15. var distFolder = "Scripts/dist";
  16. var allFiles = [libFiles, sourceFiles, specFiles];
  17.  
  18. gulp.task('package-src', function () {
  19. var filenames = glob.sync(sourceFiles); // generate array of filenames
  20. return browserify({
  21. entries: filenames,
  22. debug: true
  23. })
  24. .require(launcher, {expose: globalNamespace}) // publish
  25. .on('error', function (err) {
  26. console.error(err); this.emit('end');
  27. })
  28. .transform(babelify)
  29. .bundle()
  30. .pipe(vinylSource('all.js')) // generated output file
  31. .pipe(vinylBuffer()) // required for sourcemaps
  32. .pipe(sourcemaps.init())
  33. .pipe(sourcemaps.write("."))
  34. .pipe(gulp.dest(distFolder));
  35. });
  36.  
  37. gulp.task('package-tests', function (done) {
  38. var filenames = glob.sync(specFiles);
  39. return browserify({
  40. entries: filenames,
  41. debug: true
  42. })
  43. .on('error', function (err) {
  44. console.error(err);
  45. this.emit('end');
  46. })
  47. .transform(babelify)
  48. .bundle()
  49. .pipe(vinylSource('specs.js'))
  50. .pipe(vinylBuffer())
  51. .pipe(sourcemaps.init())
  52. .pipe(sourcemaps.write("."))
  53. .pipe(gulp.dest(distFolder));
  54. });
  55.  
  56. gulp.task('package-vendor', function () {
  57. return gulp.src(mainBowerFiles({filter: '**/*.js'}))
  58. .pipe(gulp.dest(distFolder + '/vendor/'));
  59. });
  60.  
  61. gulp.task('default', [
  62. 'package-src',
  63. 'package-vendor',
  64. 'package-tests'
  65. ]);

The generated package is "all.js" which I include in the html page. The application's entry point is on main.js with exposes a function called startApp.

App starts up at the bottom of the html page:

  1. <script>
  2. var mynamespace = require('mynamespace'); // require function is provided by browserify, no need to include require.js
  3. mynamespace.startApp();
  4. </script>

main.js:

  1. import viewModel from "./viewModel";
  2. import restClient from "./restClient";
  3.  
  4. export function startApp() {
  5. let rc = restClient();
  6. let vm = viewModel(rc);
  7. ko.applyBindings(vm);
  8. };
  9.  

In order to run the tests the most simple choice was Jasmine stand alone, including the generated "specs.js" file in the SpecRunner.html page. As the tests include the production code, the generated file "specs.js" already include all the production code.

tests:

  1. import restClient from "../src/restClient";
  2. import viewModel from "../src/viewModel";
  3.  
  4. describe("make an vehicle order", ()=> {
  5. ...
  6.  

The next step was to include "watchify" in order to rebundle everytime a file is saved.

gulpfile.js:

  1. var gulp = require('gulp');
  2. var sourcemaps = require("gulp-sourcemaps");
  3. var mainBowerFiles = require('main-bower-files');
  4. var browserify = require('browserify');
  5. var vinylSource = require('vinyl-source-stream');
  6. var glob = require('glob');
  7. var vinylBuffer = require('vinyl-buffer');
  8. var watchify = require('watchify');
  9. var babelify = require('babelify');
  10.  
  11. var launcher = './Scripts/src/main.js';
  12. var globalNamespace = 'mynamespace';
  13. var sourceFiles = 'Scripts/src/**/*.js';
  14. var sourceBundle = 'all.js';
  15. var specFiles = 'Scripts/spec/**/*.js';
  16. var specBundle = 'specs.js';
  17. var libFiles = 'Scripts/dist/vendor/*.js';
  18. var distFolder = 'Scripts/dist';
  19. var allFiles = [libFiles, sourceFiles, specFiles];
  20.  
  21. gulp.task('package-src-dev', function() {
  22. bundleWatchify(sourceFiles, sourceBundle);
  23. });
  24.  
  25. gulp.task('package-src', function() {
  26. bundle(sourceFiles, sourceBundle);
  27. });
  28.  
  29. gulp.task('test-dev', function () {
  30. bundleWatchify(specFiles, specBundle);
  31. });
  32.  
  33. gulp.task('package-vendor', function () {
  34. return gulp.src(mainBowerFiles({filter: '**/*.js'}))
  35. .pipe(gulp.dest(distFolder + '/vendor/'));
  36. });
  37.  
  38. gulp.task('default', ['package-src', 'package-vendor', 'test-dev']);
  39. gulp.task('package-dist', ['package-src', 'package-vendor']);
  40.  
  41. function bundleWatchify(sources, output) {
  42. var watchified = watchify(doBrowserify(sources))
  43. .on('update', function (filenames) {
  44. console.log('rebuilding -> ', filenames[0]);
  45. rebundle(watchified, output);
  46. });
  47. return rebundle(watchified, output);
  48. }
  49.  
  50.  
  51. function bundle(sources, output) {
  52. return rebundle(doBrowserify(sources), output);
  53. }
  54.  
  55. function doBrowserify(sources) {
  56. var filenames = glob.sync(sources);
  57. var browserified = browserify({
  58. entries: filenames,
  59. debug: true
  60. });
  61. return browserified;
  62. }
  63.  
  64. function rebundle(b, output) {
  65. return b
  66. .require(launcher, { expose: globalNamespace })
  67. .on('error', function (err) {
  68. console.error(err);
  69. this.emit('end');
  70. })
  71. .transform(babelify)
  72. .bundle()
  73. .pipe(vinylSource(output))
  74. .pipe(vinylBuffer()) // required for sourcemaps
  75. .pipe(sourcemaps.init())
  76. .pipe(sourcemaps.write("."))
  77. .pipe(gulp.dest(distFolder));
  78. }
  79.  

This post has been written on September 14th 2015, if you try to use any of the snippets posted a few months later they probably won't work for you. Versions used:

package.json:

  1. {
  2. "name": "VehicleOrders",
  3. "version": "1.0.0",
  4. "description": "",
  5. "main": "gulpfile.js",
  6. "devDependencies": {
  7. "babel": "5.8.23",
  8. "babelify": "6.3.0",
  9. "browserify": "11.0.1",
  10. "glob": "5.0.14",
  11. "gulp": "3.9.0",
  12. "gulp-babel": "5.2.1",
  13. "gulp-concat": "2.4.1",
  14. "gulp-sourcemaps": "1.5.2",
  15. "main-bower-files": "2.8.0",
  16. "vinyl-buffer": "1.0.0",
  17. "vinyl-source-stream": "1.1.0",
  18. "vinyl-transform": "1.0.0",
  19. "watchify": "3.4.0"
  20. },
  21. "scripts": {
  22. "test": "echo \"Error: no test specified\" && exit 1"
  23. },
  24. "author": "AIDA",
  25. "license": "ISC"
  26. }
Enjoyed reading this post?
Subscribe to the RSS feed and have all new posts delivered straight to you.
  • Steve Purves

    Hi Carlos,

    Thank you for the post and outline gulpfile 🙂
    I have some questions especially as I have not touched ES6 yet.

    – You are packaging your source and tests in gulp but I don’t see the tests being invoked, are you using a different runner for that outside of gulp or are you invoking manually at the moment?
    – You are packing tests so that you can test built source with the built tests, great. but are you able to the mocha tests on the unbuilt source using unpackaged specs? or are you only able to run tests on the packaged code that has been through babel?

  • http://www.carlosble.com/ Carlos Ble

    Hi Steve! We are currently running tests with Jasmine standalone, the html you download in the .zip file. The reason is that we are using the DOM, otherwise we would just use mocha. Currently we see the test failures by looking at the compiled ES5 code but it’s very easy to recognise the problem in our ES6 codebase. However this is something we would work on later on as we really need it. Our ES6 style is not much different from ES5, we don’t use classes but more of a functional style. Mocha is able to run ES6 directly.

  • blindfish

    Running Node on Windows is fiddly: when I first tried (a year or so ago) I ran into problems with the Windows path character limit; but that eventually got fixed in npm. Tried again recently and hit problems with node-gyp dependencies (you may find these were responsible for your silent error): didn’t fancy installing 7GB of Visual Studio just to get C++ compilation… So far the only sensible solution I’ve found is to install it on a VM managed with Vagrant. In principle that also makes it easier to reproduce/rebuild your dev environment across different OSes…

  • http://www.vernonkesner.com Vernon Kesner

    Anyone that’s on Windows and dealing with node-gyp issues, this is a great reference issue that helped me out a ton. https://github.com/nodejs/node-gyp/issues/629