25 April 2016

Written by Jon Bevan

ES6 is the new DHTML

Anyone remember DHTML? I remember it being awesome. Suddenly we were able to do all kinds of exciting things like displaying fireworks when our mouse is clicked, creating our own tooltips with funky styling and scrolling stuff automatically. And all in the global scope! Those were the good old days ☺

Now that a lot of ES6 is supported either by browsers themselves, or Babel/Traceur, we at Adaptavist are eager to embrace the cool new features that the ES6 standard provides, which are somewhat more nerdy than the examples above, but no less exciting!

Our ES6 stack

At the moment our typical technology stack for including ES6 in an Atlassian add-on looks a bit like this: the frontend-maven-plugin allows us to download/install Node and NPM and run Grunt to compile/convert/batch our ES6 files into JS using Babel, Uglify and any other dependencies we need. We're currently using Karma to run unit tests.

Both JIRA and Confluence use almond.js as their JS module loader, so we use the following Babel config (or something similar) in our Gruntfile.js to turn our ES6 files into well named JS modules.

grunt.initConfig({

    // Convert ES6 to ES5
    babel: {
        options: {
            sourceMap: false,  // or inline
            moduleIds: true,
            presets: ["es2015"],
            getModuleId: function (name) {
                return name.replace(/^.*src\/main\/frontend\/web-resource\/(.+)$/, '$1')
            },
            // If I remember right, this is mainly to allow us to put a "/" at the start of
            // module names so our IDE resolves the module names to the correct files
            resolveModuleSource: function(source) {
                return source.replace(/^\//, ""); // Strip the leading / from module names
            }
        },

        main: {
            options: {
                sourceRoot: 'src/main/frontend/web-resource'
            },
            files: [
                {
                    expand: true,
                    cwd: 'src/main/frontend',
                    src: ['**/*.es6'],
                    dest: 'target/generated-resources',
                    ext: '.js',
                    extDot: 'first'
                }
            ]
        }
    }

});

Module names

The configuration above generates module names based on the relative file path of our ES6 files. So if we have a file at src/main/frontend/web-resource/example-app/lib/utils.es6 the contents of that file will be converted into a file in target/generated-resources/web-resource/example-app/lib/utils.js with a module name of example-app/lib/utils.

Before:

import $ from "jquery";
import flag from "aui/flag";    // Check me out, using a module provided by AUI! 

export function somethingGreat(){};

After:

define("example-app/lib/utils", ["exports", "jquery", "aui/flag"], function(exports, _jquery, _auiFlag) {
    "use strict";

    Object.defineProperty(exports, "__esModule", {
        value: true
    });
    exports.somethingGreat = somethingGreat;

    //... removed for brevity
});

Build lifecycle

To speed up development we use grunt-contrib-watch to automagically recompile our ES6 files when they change.

With the following Gruntfile.js configuration all we need to do when we make changes to our source files is to refresh the page in the browser!

grunt.initConfig({
    watch: {
        modules: {
            files: [
                'src/main/frontend/**/*.es6'
            ],
            tasks: ['build']
        }
    },

    // the babel config from above goes here...
});

// This creates a new task called 'build' that will run the 'babel' task and
// the 'concat' task defined elsewhere.
grunt.registerTask('build', [
    'babel',
    'concat',
    //'uglify'
]);

Typically, when I kick off the atlas-debug command for a project in a terminal I'll open up a new terminal tab/session and run grunt build watch as well.

We also have a useful grunt clean command for removing only the generated Javascript files:

grunt.registerTask('clean', function() {
    grunt.file.delete("target/generated-resources");
    grunt.file.delete("target/generated-test-resources");
});

Source structure

When we're developing JS-heavy Atlassian add-ons it can take a lot of time having to maintain an up-to-date web-resource configuration in the atlassian-plugin.xml.

To reduce the manual XML editing overhead we developed a dynamic web modules add-on that will automatically add web-resources to your plugin without you needed to specify each one.

Things to note

A couple of the things that took me a little while to wrap my head around are:

  1. Modules don't auto-run. Module code won't execute unless we want it to. They are not IIFEs. We need to a) load the modules we want into our application, and b) call an exported function of those modules to start the execution. Often we have a vanila JS file that uses the almond.js library to load the relevant modules for our application, and then calls some of the exported functions of those modules to 'start' them.

    // setup.js
    require(['example-app/setup', 'adaptavist/analytics'], function(setup, analytics){
        setup.bindEvents();
        setup.renderApp();
        analytics.register();
    });
    
  2. Modules are singletons, so any state within a module is shared whenever that module is imported. If we want to create different 'instances' of whatever our module represents, we need to ensure our module exports some kind of constructor which encapsulates the state.



blog comments powered by Disqus