Carles Andrés

Passionate Web Developer

Some Grunt Tasks for JS Libraries

While recently working on the development of a Javascript library, I had the goal of automating the release process as much as possible. The main tasks that I wanted to automate were:

  • Adding a copyright banner at the top of our compiled JS library files
  • Tagging our git repo with our library version
  • Deploying our library to an AWS bucket

The team was already using Grunt to automate the build process through a custom grunt build task, so we were ready to build our grunt release task on top of it.

Adding a copyright banner to the top of a file

The goal of this task was two-fold. First, we wanted to add a non-minified text notice at the beginning of our already-minified library with the usual authorship, copyright and license information.

Second, we wanted to include in the banner the updated version number, which needed to be extracted from the project’s package.json and only added to the library after the library had been compiled.

It turns out this simple operation of adding a string to the top of a file can be performed both from the concat task and from the uglify task, which, in our case, was useful because we were publishing both a non-minified and a minified version of the library a we were already using both tasks.

Before adding the banner we had to load the contents of package.json into a Javascript object, which can be simply achieved by making a call to grunt.file.readJSON which is one of the built-in methods available in Grunt.

1
2
3
4
...
grunt.initConfig({
  pkg: grunt.file.readJSON('package.json'),
...

From that moment on, the contents of package.json file were available from the pkg variable and the addition of the banner was as easy as adding the corresponding banner option to the concat and uglify tasks:

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
29
30
31
32
33
concat: {
    options: {
        separator: ';',
        banner: '/*!\n' +
                ' * Client-side Javascript library to access myproject API v<%= pkg.version %>\n' +
                ' * https://github.com/myproject/myproject\n' +
                ' *\n' +
                ' * Copyright [<%= grunt.template.today("yyyy") %>] [myproject Ltd. Cambridge]\n' +
                ' *\n' +
                ' * Released under the http://www.apache.org/licenses/LICENSE-2.0\n' +
                ' * https://github.com/myproject/myproject/blob/master/LICENSE.txt\n' +
                ' */\n' +
                '\n'
    },
    dist: {
        src: ['<%= yeoman.app %>/{,*/}*.js'],
        dest: '<%= yeoman.dist %>/concatenated/myproject-<%= pkg.version %>.js'
    }
},

uglify: {
    options: {
        banner: '/*!\n' +
                ' * Client-side Javascript library to access myproject API v<%= pkg.version %>\n' +
                ' * https://github.com/myproject/myproject\n' +
                ' *\n' +
                ' * Copyright [<%= grunt.template.today("yyyy") %>] [myproject Ltd. Cambridge]\n' +
                ' *\n' +
                ' * Released under the http://www.apache.org/licenses/LICENSE-2.0\n' +
                ' * https://github.com/myproject/myproject/blob/master/LICENSE.txt\n' +
                ' */\n' +
                '\n'
    },

Notice that this configuration provides a means to keep the copyright notice updated to the year of the build, by invoking another one of Grunt’s built-in methods: grunt.template.today.

Tagging the remote Git repository

We also wanted to tag our GitHub repo every time we released a new version of our library. I found out there is a nice Grunt plugin to run Git commands as Grunt tasks: grunt-git.

At the moment of this writing the grunt-git plugin supports the following Git commands (with maybe some limitations): add, commit, rebase, tag, checkout, stash, clone, reset, rm, clean, push, pull, fetch, merge, archive and log.

So we configured a Grunt task that tagged the repository with a label based on the contents of the package.json file at the time of its execution.

1
2
3
4
5
6
7
gittag: {
    task: {
        options: {
            tag: 'v<%= pkg.version %>'
        }
    }
},

That was pretty easy. Wasn’t it? Well, not really. We also wanted to ensure that our local repo had no unstaged changes before tagging (i.e. it was “clean”), because if it wasn’t that would be a sympton that the repository contents were probably not in-sync with the remote.

Fortunately, another Grunt plugin came to the rescue grunt-checkrepo. which can do exactly that, i.e.: check whether there are unstaged files in local repository, as well as other tag-related checks which turned out to be useful later on.

For the moment, we only created the subtask to check if the repo was clean:

1
2
3
4
5
checkrepo: {
    // Check repo is clean before tagging
    tag: {
        clean: true,        // Check repo is clean
    },

And then we registered a task to perform the tag operation only after succesfully checking that the local repository was clean:

1
2
3
4
grunt.registerTask('tag', [
    'checkrepo:tag',
    'gittag'
]);

Deploying our library to an AWS S3 bucket

Once our library was tested and compiled (not part of this article) and our repo was clean and tagged, we were ready to deploy to our CDN, which in our case was the AWS S3 service.

To perform this task, another convenient Grunt plugin, grunt-aws-s3, came handy.

Naturally, part of this plugin configuration involves (who would have thought?) providing the user’s credentials to the AWS account that is intented to be used. We were concerned that if our AWS keys were stored in a file, they might end up being exposed by mistake, despite being .gitignored. For that reason, we decided to store the credentials only in system (environment) variables.

It turns out Grunt exposes, by default, all environment variables to Gruntfile.js through the process.env variable. So just, we created our custom task to read them:

1
2
3
4
5
6
7
// Read AWS environment variables (if available) into an object
aws: {
    AccessKeyId : process.env.AWS_ACCESS_KEY_ID,
    SecretKey : process.env.AWS_SECRET_KEY,
    ProductionBucket: process.env.AWS_MYPROJ_PROD_BUCKET,
    DemoBucket: process.env.AWS_MYPROJ_DEMO_BUCKET
},

We also wanted to do some additional checks on the state of our local repo before uploading, like ensuring that the repo was clean (again), that the last commit had been properly tagged, that the tag that had been used matched the current version specified in package.json and also that the version

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
checkrepo: {
    // Check repo is clean before tagging
    tag: {
        clean: true,        // Check repo is clean
    },
    // Check repo is tagged and tag matches package
    // version number before deploying
    deploy: {
        clean: true,        // Check repo is clean
        tagged: true,       // Checks whether the last commit (HEAD) is tagged.
        tag: {
            eq: '<%= pkg.version %>',    // Check if highest repo tag is equal to pkg.version
            valid: '<%= pkg.version %>', // Check if pkg.version is valid semantic version
        }
    }
},

Some of this validation might seem redudant considering that the tagging of the repo and the push of the tag to the remote was suposed to be done exclusively via the aforementioned Grunt workflow. However, we want our Grunt tasks to follow the usual best practices in development and, therefore, be robust and modular so that they can be reused and combined and not depend on any of the other tasks. This approach allows the deploy task to be executed independently of the build task with some guarantee that we are deploying the latest code in our branch.

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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
// Deploy to AWS bucket
aws_s3: {
    options: {
        accessKeyId: '<%= aws.AccessKeyId %>',
        secretAccessKey: '<%= aws.SecretKey %>'
    },
    demo: {
        options: {
            bucket: '<%= aws.DemoBucket %>',
            // debug: true
        },
        files: [
            {
                src: '<%= yeoman.dist %>/concatenated/myproject-<%= pkg.version %>.js',
                dest: 'myproject-<%= pkg.version %>-<%= timestamp %>.js'
            },
            {
                src: '<%= yeoman.dist %>/minified/myproject-<%= pkg.version %>.min.js',
                dest: 'myproject-<%= pkg.version %>-<%= timestamp %>.min.js'
            }
        ]
    },
    production: {
        options: {
            bucket: '<%= aws.AWSProductionBucket %>',
            // Debug option is for testing purposes
            // debug: true
        },
        files: [
            {
                expand: true,
                cwd: '<%= yeoman.dist %>/concatenated',
                src: ['**'],
                dest: 'subfolder/myproject-js-wrapper',
                filter: 'isFile'
            },
            {
                expand: true,
                cwd: '<%= yeoman.dist %>/minified',
                src: ['**'],
                dest: 'subfolder/myproject-js-wrapper',
                filter: 'isFile'
            }
        ]
    }
},

One of the nicest bits about the grunt-aws-s3 plugin, is that it it allows to perform a task in debug mode, which would run a safe version of the task in which the credentials and the availability of both the AWS S3 bucket and the file to upload are checked, but no actual uploading takes place. This behaviour turns out to be very useful while creating or modifying the task itself and can easily be forced by adding debug: true to the subtask configuration.

Finally, publishing the library made the release “official”, which made it necessary to ensure we were updating the remote with the current tag in the local repo. Another grunt-git

1
2
3
4
5
6
7
8
9
// Creates the gitpush task and ensures that the --tags flag is included
// so that any tag is also pushed to the remote
gitpush: {
    task: {
        options: {
            tags: true
        }
    }
},

Conclusion

That was all. We added a couple of minor tasks after that, like a task to automatically rebuild our library documentation based on its own comments grunt-docco but…you get the point.

There is a lot of automation that can be achieved by doing some research. I believe that all this automation very quickly compensates the time spent on that research.

There are many, many plugins out there. Like the ones to help deploying to other CDNs (e.g. to Heroku).

I believe there is also room for even safer and more useful processes to be designed by using git hooks in combination with grunt tasks.

I’d like to know about other people’s experiences with front-end automation. If you have something to share, please, post a comment.

Comments