Scato Eggen
5 januari 2016

I can still remember seeing jQuery for the first time. In those days, JavaScript was hard to write. Single Page Applications weren’t a thing then. Too many memory leaks, for one thing, made them impractical. Then came the whole MV* roller coaster.

But that’s the past. Today, we live in a world in which Internet Explorer implements quite a lot of CSS3 (some say Safari is the new IE) and you don’t need jQuery to be cross-browser compatible (look at the Vanilla JS movement). And we have Node JS!

For a moment, I was worried, because running client-side code on Node wasn’t always possible, and ECMAScript 6 was great in theory, but how long would we have to wait until all browsers were updated? Much to my surprise, these problems are gone. Today’s workflow is totally different from what it used to be. With the right tools, we can start writing ECMAScript 6 today, and run the code anywhere.

In this blog post, I will describe a JavaScript workflow that includes:

  • bundling JavaScript with Webpack
  • running ECMAScript 6 with Babel
  • checking style with ESLint
  • running unit tests on Node with Mocha

There are other tools that do the job as well, but these are my favorite.

Installation

If you want to type along with this blog post, you will need to install some dependencies. First of all, you need Node and npm. Once you’ve set that up, create an empty directory and install the packages as follows:

npm install webpack babel-core babel-preset-es2015 babel-loader eslint eslint-config-airbnb eslint-plugin-react eslint-loader mocha chai sinon npm install -g bower bower install angular

Also, you can read all the example code on GitHub.

Bundling JavaScript with Webpack

After working with RequireJS (and feeling increasingly unhappy in the process) I was very excited about Webpack. Webpack takes your JavaScript code and all its dependencies and puts it into one big file.

Webpack lets you define entry points, which is typically a bootstrap file that initializes your app. For example, an Angular bootstrap file looks as follows:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// src/articleOverview/bootstrap.js
var angular = require('angular');
var app = require('./app');

app.init(articles);
angular.bootstrap(document, ['ArticleOverview']);

You just add the entry point to your Webpack configuration:

// webpack.config.js
module.exports = {
  entry: {
    articleOverview: './src/articleOverview/bootstrap.js',
  },
  output: {
    path: 'build',
    filename: '[name].js',
  },
  resolve: {
    root: [
      __dirname + '/bower_components',
    ],
    extensions: ['', '.js'],
 },
};

When you run Webpack, you will see that the build directory now contains one big JavaScript file that contains your Angular app, together with all its dependencies!

Pro tip: if you add “node_modules/.bin” to your shell path, you can just type “webpack” and hit enter!

Also, if you are typing along, you will notice that the app dependency is missing. You can just create an empty file “src/articleOverview/app.js”, since we are not running the code anyway.

If you have a web application that has several pages with Angular apps, you can add an entry point for each of them, and Webpack will create separate bundles. The configuration above places these bundles in the directory “build”, but you can place them in “web/assets” directly if you use Symfony, for example.

Running ECMAScript 6 with Babel

One of the things that has been bothering me in JavaScript is the lack of a standard for defining modules. But no more! ECMAScript 6 has module syntax, one that’s compatible with Node even! And with transpilers like Babel, we don’t need to wait for browser vendors to catch up, we can use ECMAScript 6 today!

A transpiler takes code in one language and transforms it into another language. Another example is CoffeeScript, which also compiles to JavaScript (ECMAScript 5). Babel is a transpiler that transforms code that is written in future JavaScript versions (ECMAScript 6, 7, etc.) and transforms it into JavaScript that runs in contemporary browsers.

For example, if we change our bootstrap to:

1
2
3
4
5
6
// src/articleOverview/bootstrap.js
import angular from 'angular';
import app from './app';

app.init(articles);
angular.bootstrap(document, ['ArticleOverview']);

Webpack will report an error, because it can’t parse ECMAScript 6.

You can use Babel in a number of ways. You can transpile your code one file at the time, register a require hook that runs code through Babel before executing it or use a runtime that wraps around Node that performs the same trick.

But there’s also a loader for Webpack. In Webpack, you can specify which loaders should be applied to which files. If we want to apply the Babel loader to all .js files, we add the following to our Webpack config:

1
2
3
4
5
6
7
8
9
// webpack.config.js
module.exports = {
  // ...
  module: {
    loaders: [
      { test: /.js$/, loader: 'babel-loader', exclude: /bower_components|node_modules/ },
    ],
  },
};

Note that Babel used to translate to ECMAScript 6 (also known as ES2015) by default, but since version 6, you have to add the ES2015 preset. Create the following .babelrc:

{ “presets”: [“es2015”] }

Now you can run Webpack and look: no more error!

Checking style with ESLint

Using the new import syntax makes our code much more readable, but we can do more! A linter is a program that looks for suspicious code constructs that might lead to bugs. ESLint also checks code style, which means your code will be more uniform and thus more readable. There are many JavaScript style guides, but Airbnb has a style guide that includes guidelines on the new ECMAScript 6 syntax. It has a preset that you can add to your .eslintrc as follows:

{ “extends”: “airbnb” }

All we have to do now is add the ESLint loader to the Webpack config:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
// webpack.config.js
module.exports = {
  // ...
  module: {
    loaders: [
      { test: /.js$/, loader: 'babel-loader', exclude: /bower_components|node_modules/ },
      { test: /.js$/, loader: 'eslint-loader', exclude: /bower_components|node_modules/ },
    ],
  },
};

Note that eslint-loader is lower in the list, but it is executed first. This is exactly what we want, because we don’t want to run ESLint on the output of Babel.

If we run Webpack, ESLint tells us that: “articles” is not defined. This is a global that we plan to set in the HTML. We can go and fix this by adding a /* global */ block to the bootstrap:

1
2
3
4
5
6
7
// src/articleOverview/bootstrap.js
import angular from 'angular';
import app from './app';

/* global articles */
app.init(articles);
angular.bootstrap(document, ['ArticleOverview']);

If we run Webpack one more time, the error is gone.

Running unit tests on Node with Mocha

One of the things that got me most excited when I first heard about Node was the idea that you could write code once and run it both server-side and client-side. This illusion was soon shattered. The Node module system was unfit for the browser, and running unit tests with AMD on Node was only possible in theory. But with Webpack, we can not only run Node modules in the browser, we can also run unit tests for client-side code on Node!

Unit tests should be fast, so I can run them in my pre-commit hook. That’s why I don’t use Karma. My unit tests shouldn’t depend on the execution environment, so I it should be safe to run them on Node. I also want to write tests that use the ECMAScript 6 module syntax, so I need to run my tests through Babel. The fastest way to do this is by combining all your tests in one bundle and letting Mocha run that.

The first problem is that I have to define one entry point, but I don’t want to create an entry point that has a hardcoded list of tests. Here’s a little trick I learned, a sort of magic entry point:

1
2
3
4
// test/index.js
const context = require.context('./', true, /.js$/);

context.keys().forEach(context);

This piece of code uses the Webpack context module. When Webpack parses this file to determine its dependencies, it processes the call to require.context and includes every file that matches the regular expression in the bundle.

We then add the entry point to the Webpack config:

1
2
3
4
5
6
7
8
// webpack.config.js
module.exports = {
  entry: {
    articleOverview: './src/articleOverview/bootstrap.js',
    test: './test',
  },
  // ...
};

In case you don’t have any tests, let’s create a simple one:

1
2
3
4
5
6
7
8
// test/example.js
import { expect } from 'chai';

describe('example', () => {
  it('works', () => {
    expect('foo').to.be.a('string');
  });
});

We run Webpack, but we get an error saying: “describe” is not defined. ESLint doesn’t know we are using globals from Mocha, so we have to tell it in .eslintrc:

1
2
3
4
5
6
{
  "extends": "airbnb",
  "env": {
    "mocha": true
  }
}

It also complains that “a dependency to an entry point is not allowed”. We can fix that by replacing the regular expression in “test/index.js” with the slightly uglier:

/^(?!.\/index\.js$).*\.js$/

This excludes “./index.js”, which is the entry point itself.

Now, Webpack succeeds and we can run the tests using Mocha:

mocha build/test.js

I like to use Chai for assertions, which is compatible with Webpack. I also want to create test doubles with Sinon, which is not compatible with Webpack. I get a lot of warnings saying: “require function is used in a way in which dependencies cannot be statically extracted.” When I try to run the tests, they are indeed broken. But why?

By the way, if you are typing along, you can reproduce the errors by adding the following import to the test:

1
2
// test/example.js
import { mock, spy } from 'sinon';

The problem is that Webpack can only detect dependencies that you pass into the require function itself. Sinon passes the require function as an argument to another function called loadDependencies. Webpack compensates for the lack of knowledge by including the entire directory in a context, which would work if not for those few dependencies that start with “../“.

The workaround is to use the packaged version of Sinon. Only the npm version of Sinon contains this packaged version, so we have to add node_modules to the root paths. To force Webpack to use the packaged version, we have to make an alias in the Webpack config:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
// webpack.config.js
module.exports = {
  // ...
  resolve: {
    root: [
      __dirname + '/bower_components',
      __dirname + '/node_modules',
    ]
    extensions: ['', '.js'],
    alias: {
      'sinon': 'sinon/pkg/sinon'
    },
  },
  // ...
};

But Sinon still tries to include modules it cannot find! To fix this, we have to make Sinon believe there is no define and there is no require:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
// webpack.config.js
module.exports = {
  // ...
  module: {
    loaders: [
      { test: /.js$/, loader: 'babel-loader', exclude: /bower_components|node_modules/ },
      { test: /.js$/, loader: 'eslint-loader', exclude: /bower_components|node_modules/ },
      { test: /sinon/, loader: "imports?define=>false,require=>false" },
    ],
  },
};

Finally, we can use Sinon.

Conclusion

Our final Webpack configuration file looks as follows:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// webpack.config.js
module.exports = {
  entry: {
    articleOverview: './src/articleOverview/bootstrap.js',
    test: './test',
  },
  output: {
    path: 'build',
    filename: '[name].js',
  },
  resolve: {
    root: [
      __dirname + '/bower_components',
      __dirname + '/node_modules',
    ],
    extensions: ['', '.js'],
    alias: {
      'sinon': 'sinon/pkg/sinon',
    },
  },
  module: {
    loaders: [
      { test: /.js$/, loader: 'babel-loader', exclude: /bower_components|node_modules/ },
      { test: /.js$/, loader: 'eslint-loader', exclude: /bower_components|node_modules/ },
      { test: /sinon/, loader: "imports?define=>false,require=>false" },
    ],
  },
};

This setup lets us write Angular applications:

  • that are bundled for the browser
  • in ECMAScript 6
  • guided by tests (including mocks)
  • with tests running on the command line.