ES6 Harmony Quick Development Setup

ECMAScript 6, also called “harmony” is the next version of JavaScript and it’s packed with all kinds of awesome improvements and fancy features. (GitHub page for ES6 features).

Unfortunately, harmony is not out yet and is in fact still a work in progress. There are some features already implemented in the newest versions of Chrome and Firefox, but it will be some time before we will be able to enjoy the full range of features available within the proposal. There is, howevery, a way to test harmony right now using the magical technique of transpiling, i.e. compiling JS files written in ES6 back to ES5 so they can be executed in all of today’s browsers as well as in NodeJS.

This post will show an easy way to get a development setup running for ES6 with tests and all using Google’s traceur compiler (traceur-compiler). There is also a grunt-task for this compiler, which makes it easier to use (grunt-traceur).

I expect you have npm, grunt and grunt-cli already installed, there are lots of tutorials on the web on how to do this.

The first step is to install grunt-traceur using

npm install grunt-traceur

Then, create a Gruntfile.js and fill it with the following contents:


module.exports = function(grunt) {
    grunt.initConfig({
        traceur: {
            options: {
                'blockBinding': true 
            },
            custom: {
                files: {
                    'build/all.js': ['js/**/*.js'] 
                } 
            }
        }
    });

    grunt.loadNpmTasks('grunt-traceur');
};

The ‘blockBinding’ flag enables the use of ‘let’ and ‘const’, the new block-scoped variable definition mechanisms.

Now, create a ‘js’ folder where we will put all of our JavaScript files. If you execute

grunt traceur

on your shell will take all .js files within the ‘js’ folder and compile them into the ‘all.js’ file within the build folder.

Important Note: If you want to use the compiled ‘all.js’ file, you need to include the traceur-runtime.js before including the compiled sources, because some of the harmony features need some workarounds implemented in this. The file can be found in ‘node_modules/traceur/bin/‘ within the ‘grunt-traceur’ npm folder. You can also find it in the officlal GitHub repo mentioned above.

Important Note2: If you are using JSHint, you need to add the “esnext” flag to your .jshintrc to make it ES6-aware.

Now, we will write the Conway’s Game of Life implementation from (Conway’s Game of Life implementation) in ES6:

index.js


var getCellRepresentation = function(x, y) {
    return "x" + x + "y" + y; 
};
 
class Cell {
    constructor(x, y, alive) {
        this.x = x;
        this.y = y;
        this.alive = alive;
    }

    isAlive() {
        return this.alive; 
    }
}
 
class Board {
    constructor() {
        this.cells = {}; 
    }

    addCell(cell) {
        this.cells[getCellRepresentation(cell.x, cell.y)] = cell; 
    }

    getCellAt(x, y) {
        return this.cells[getCellRepresentation(x, y)]; 
    }

    getAliveNeighbors(cell) {
        let x = cell.x,
            y = cell.y,
            aliveCells = 0;

        for (let i = -1; i < 2; i++) {
            for(let j = -1; j < 2; j++) {
                if(i === 0 && i === j) {
                    continue;
                }
                let currentCell = this.getCellAt(x + i, y + j);

                if(currentCell && currentCell.isAlive()) {
                    aliveCells++;
                }
            }
        }
        return aliveCells;
    }

    calculateNextState(cell) {
        let tempCell = new Cell(cell.x, cell.y, cell.alive),
            livingNeighbors = this.getAliveNeighbors(cell);

        if(cell.isAlive()) {
            if(livingNeighbors === 2 || livingNeighbors === 3) {
                tempCell.alive = true;
            } else {
                tempCell.alive = false;
            }
        } else {
            if(livingNeighbors === 3) {
                tempCell.alive = true;
            }
        }
        return tempCell;
    }

    step() {
        let cells = this.cells,
            tempBoard = {},
            keys = Object.keys(cells);

        keys.forEach((c) => {
            let cell = this.cells[c],
                newCell = this.calculateNextState(cell);
            tempBoard[c] = newCell;
        });

        this.cells = tempBoard;
    }
}

This implementation actually doesn’t even use many of the new fancy things, just classes, the fat-arrow syntax for nested closures (=>) and let instead of var. But especially the new classes show you how well structured your future modules will be and that you don’t have to fight for nice code-structure anymore 😉

We also want to rewrite our Jasmine tests, we have lots of closures there, but not much else, so again, not many features used (code at the bottom of this post):

The two files we created are our index.js and spec.js in the ‘js’ folder. Now we use grunt-traceur to compile them to ES5 and run the tests using karma (karma).

Important to note here, is that this won’t work in PhantomJS, because it’s ES5 and PhantomJS’s JS engine is really old. We will have to use Chrome or something else as our test-browser in the karma.conf.js.


module.exports = function(config) {
  config.set({
    basePath: '',
    frameworks: ['jasmine'],
    files: [
      'node_modules/grunt-traceur/node_modules/traceur/bin/traceur-runtime.js',
      'build/*.js'
    ],
    reporters: ['progress'],
    port: 9876,
    colors: true,
    logLevel: config.LOG_INFO,
    autoWatch:false, 
    browsers: ['Chrome'],
    captureTimeout: 60000,
    singleRun: false
  });
};

As mentioned above, we have to include the traceur-runtime here as well. Now we can go ‘karma start’ and ‘karma run’ and see our green bars and be happy!

Alright, that’s it! A short glimpse into the world of ES6. I believe that, although it is a little cumbersome to get running and there are some issues still with the tools we are used to in our daily development, starting to learn how to use harmony and getting good at it will pay off big within the next few years for every professional JavaScript developer and…it’s a LOT of fun 😀

Code:
spec.js


/*global describe, it, expect, sinon, stub, assert, before, beforeEach, afterEach, Board, Cell */
/*jshint expr:true */
describe('Conways Game of Life', () => {

    it('is a sanity test', () => {
        expect(true).toBe(true);
    });
    let board, cell; 

    beforeEach(() => {
        board = new Board();
        cell = new Cell(1, 1, true);
        board.addCell(cell);
    });

    describe('addCell', () => {

        it('adds a cell to a board', () => {
            expect(board.cells.x1y1).toEqual(cell);
        });

    });

    describe('getCellAt', () => {

        it('returns the cell at the provided coordinates', () => {
            expect(board.getCellAt(1, 1)).toEqual(cell);
        });

    });

    describe('getAliveNeighbors', () => {

        it('returns 0 if there are no other cells', () => {
            expect(board.getAliveNeighbors(cell)).toEqual(0);
        });

        it('returns 1 if there is one alive cell next to the cell', () => {
            let neighborCell = new Cell(0, 1, true);
            board.addCell(neighborCell);

            expect(board.getAliveNeighbors(cell)).toEqual(1);
        });

        it('returns 8 if there are 8 neighbors available', () => {
            board.addCell(new Cell(0, 1, true));
            board.addCell(new Cell(0, 2, true));
            board.addCell(new Cell(0, 0, true));
            board.addCell(new Cell(2, 1, true));
            board.addCell(new Cell(1, 0, true));
            board.addCell(new Cell(2, 2, true));
            board.addCell(new Cell(1, 2, true));
            board.addCell(new Cell(2, 0, true));

            expect(board.getAliveNeighbors(cell)).toEqual(8);
        });

        it('returns 1 if there are 7 dead cells next available', () => {
            board.addCell(new Cell(0, 1, true));
            board.addCell(new Cell(0, 2, false));
            board.addCell(new Cell(0, 0, false));
            board.addCell(new Cell(2, 1, false));
            board.addCell(new Cell(1, 0, false));
            board.addCell(new Cell(2, 2, false));
            board.addCell(new Cell(1, 2, false));
            board.addCell(new Cell(2, 0, false));

            expect(board.getAliveNeighbors(cell)).toEqual(1);
        });
    });

    describe('calculateNextState', () => {

        it('dies if there are less than 2 living neighbors', () => {
            board.addCell(new Cell(0, 0, true));

            expect(board.calculateNextState(cell).isAlive()).toBe(false);
        });

        it('dies if there are more than 3 living neighbors', () => {
            board.addCell(new Cell(0, 1, true));
            board.addCell(new Cell(0, 2, true));
            board.addCell(new Cell(0, 0, true));
            board.addCell(new Cell(1, 2, true));

            expect(board.calculateNextState(cell).isAlive()).toBe(false);
        });

        it('lives if there are 2 or 3 living neighbors', () => {
            board.addCell(new Cell(0, 0, true));
            board.addCell(new Cell(0, 1, true));

            expect(board.calculateNextState(cell).isAlive()).toBe(true);
        });

        it('comes back to live if there are exactly 3 living neighbors ', () => {
            board.addCell(new Cell(0, 0, true));
            board.addCell(new Cell(0, 1, true));
            board.addCell(new Cell(0, 2, true));
            cell.alive = false; 

            expect(board.calculateNextState(cell).isAlive()).toBe(true);
        });

    });

    describe('step', () => {

        it('calculates the new state for all dying cells', () => {
            board.addCell(new Cell(0, 0, true));

            board.step();

            expect(board.getCellAt(0, 0).isAlive()).toBe(false);
            expect(board.getCellAt(1, 1).isAlive()).toBe(false);
        });

        it('calculates the new state for all living cells', () => {
            board.addCell(new Cell(0, 0, true));
            board.addCell(new Cell(1, 2, true));

            board.step();

            expect(board.getCellAt(0, 0).isAlive()).toBe(false);
            expect(board.getCellAt(1, 1).isAlive()).toBe(true);
        });

        it('calculates the new state correctly for many cells', () => {
            board.addCell(new Cell(0, 1, true));
            board.addCell(new Cell(0, 2, true));
            board.addCell(new Cell(0, 0, false));
            board.addCell(new Cell(2, 1, true));
            board.addCell(new Cell(1, 0, true));
            board.addCell(new Cell(2, 2, true));
            board.addCell(new Cell(1, 2, false));
            board.addCell(new Cell(2, 0, false));
            board.step();

            expect(board.getCellAt(1, 1).isAlive()).toBe(false);
            expect(board.getCellAt(0, 1).isAlive()).toBe(true);
            expect(board.getCellAt(2, 2).isAlive()).toBe(true);
        });
    });

    describe('Cell', () => {

        it('is either alive or dead', () => {
            expect(cell.isAlive()).toEqual(true);
        });
    });
});

~m

One thought on “ES6 Harmony Quick Development Setup

Leave a Reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>