Building our assets
Now that we have a pipeline up and running it’s time to turn our attention to dealing with building our code and assets and figure out how are our app will consume these. We do not want to check in our generated assets, however Heroku deploys using a git mechanism.
Let’s start by creating some tasks to generate our css, then concatenate and ulgyfy our JS and we’ll finish by deploying these assets to Heroku as part of a successful build. We’ll also add some tasks to run these tasks on our local machine and have these re-generated when we save changes to the file.
1 git checkout -c generate-assets
TODO: why do we generate our assets?
Compile our less to css
Let’s tackle with our CSS files. The first thing we want to do is add the destination folder for our css content to the .gitignore list:
1 node_modules
2 .idea
3 bower_components
4 phantomjsdriver.log
5 app/css/
Under our source folder let’s create a less folder and create a main.less file in it. Here’s content of the file:
1 @import '../../bower_components/bootstrap/less/bootstrap';
Our build tasks will take this import directive and create us a nice main.css file that we can then use in our app.
1 npm install grunt-contrib-less --save-dev
And now let’s create a task to generate our css files by adding to our Gruntfile:
1 module.exports = function (grunt) {
2 grunt.initConfig({
3 express: {
4 test: {
5 options: {
6 script: './server.js'
7 }
8 }
9 },
10 cucumberjs: {
11 src: 'tests/e2e/features/',
12 options: {
13 steps: 'tests/e2e/steps/'
14 }
15 },
16 less: {
17 production: {
18 options: {
19 paths: ['app/css/'],
20 cleancss: true
21 },
22 files: {
23 'app/css/main.css': 'src/less/main.less'
24 }
25 }
26 }
27 });
28
29 grunt.loadNpmTasks('grunt-express-server');
30 grunt.loadNpmTasks('grunt-selenium-webdriver');
31 grunt.loadNpmTasks('grunt-cucumber');
32 grunt.loadNpmTasks('grunt-contrib-less');
33
34 grunt.registerTask('generate', ['less:production']);
35
36 grunt.registerTask('e2e', [
37 'selenium_start',
38 'express:test',
39 'cucumberjs',
40 'selenium_stop',
41 'express:test:stop'
42 ]);
43
44 };
When you run grunt generate, you should see the following output:
1 Running "less:production" (less) task
2 File app/css/main.css created: 131.45 kB → 108.43 kB
If you were to start up our server and browse to localhost:3000, our UI should have more of a Bootstrap feel to it!
Now bootstrap also needs some fonts, so let’s move these across as part of the build.
1 npm install grunt-contrib-copy --save-dev
And add a simple task to copy our fonts across as well:
1 module.exports = function (grunt) {
2 grunt.initConfig({
3 express: {
4 test: {
5 options: {
6 script: './server.js'
7 }
8 }
9 },
10 cucumberjs: {
11 src: 'tests/e2e/features/',
12 options: {
13 steps: 'tests/e2e/steps/'
14 }
15 },
16 less: {
17 production: {
18 options: {
19 paths: ['app/css/'],
20 cleancss: true
21 },
22 files: {
23 'app/css/main.css': 'src/less/main.less'
24 }
25 }
26 },
27 copy: {
28 fonts: {
29 expand: true,
30 src: ['bower_components/bootstrap-sass-official/vendor/assets/fo\
31 nts/bootstrap/*'],
32 dest: 'app/css/bootstrap/',
33 filter: 'isFile',
34 flatten: true
35 }
36 }
37 });
38
39 grunt.loadNpmTasks('grunt-express-server');
40 grunt.loadNpmTasks('grunt-selenium-webdriver');
41 grunt.loadNpmTasks('grunt-cucumber');
42 grunt.loadNpmTasks('grunt-contrib-less');
43 grunt.loadNpmTasks('grunt-contrib-copy');
44
45 grunt.registerTask('generate', ['less:production', 'copy:fonts']);
46
47 grunt.registerTask('e2e', [
48 'selenium_start',
49 'express:test',
50 'cucumberjs',
51 'selenium_stop',
52 'express:test:stop'
53 ]);
54
55 };
Running grunt generate task should now also copy our fonts across.

Fonts now included
This is great, but how do we get this to run as part of our successful build?
Heroku build packs
Heroku has a way to run commands after a build, these come in the form of build packs. Luckily for us someone has already gone through the effort of creating one to run Grunt after an install.
I have to say it’s not ideal, however given Heroku’s git based deployment approach, we have little choice but to generate these as part of the deployement. Typicall you would rely on the build to generate a package with all of the generated assets ready for consumption. This works though and does not force us to commit our generated assets into our repository.
So here’s how you go about installing our Build Pack for Grunt (be sure to replace --app lit-meadow-5649 with your actual heroku app name):
1 heroku login
2
3 heroku config:add BUILDPACK_URL=https://github.com/mbuchetics/heroku-buildpack-n\
4 odejs-grunt.git --app lit-meadow-5649
5
6 heroku config:set NODE_ENV=production --app lit-meadow-5649
Then we modify our Gruntfile to include a new heroku:production task, which basically references our build task:
1 module.exports = function (grunt) {
2 grunt.initConfig({
3 express: {
4 test: {
5 options: {
6 script: './server.js'
7 }
8 }
9 },
10 cucumberjs: {
11 src: 'tests/e2e/features/',
12 options: {
13 steps: 'tests/e2e/steps/'
14 }
15 },
16 less: {
17 production: {
18 options: {
19 paths: ['app/css/'],
20 cleancss: true
21 },
22 files: {
23 'app/css/main.css': 'src/less/main.less'
24 }
25 }
26 },
27 copy: {
28 fonts: {
29 expand: true,
30 src: ['bower_components/bootstrap-sass-official/vendor/assets/fo\
31 nts/bootstrap/*'],
32 dest: 'app/css/bootstrap/',
33 filter: 'isFile',
34 flatten: true
35 }
36 }
37 });
38
39 grunt.loadNpmTasks('grunt-express-server');
40 grunt.loadNpmTasks('grunt-selenium-webdriver');
41 grunt.loadNpmTasks('grunt-cucumber');
42 grunt.loadNpmTasks('grunt-contrib-less');
43 grunt.loadNpmTasks('grunt-contrib-copy');
44
45 grunt.registerTask('generate', ['less:production', 'copy:fonts']);
46
47 grunt.registerTask('e2e', [
48 'selenium_start',
49 'express:test',
50 'cucumberjs',
51 'selenium_stop',
52 'express:test:stop'
53 ]);
54
55 grunt.registerTask('unit', [
56 'karma:unit'
57 ]);
58
59 grunt.registerTask('heroku:production', 'generate');
60 };
The final step involves re-jigging package.json to include those newly added grunt tasks as a general dependency:
1 {
2 "name": "weatherly",
3 "version": "0.0.0",
4 "description": "Building a web app guided by tests",
5 "main": "index.js",
6 "engines": {
7 "node": "~0.10.28"
8 },
9 "scripts": {
10 "test": "grunt test"
11 },
12 "repository": {
13 "type": "git",
14 "url": "https://github.com/gregstewart/weatherly.git"
15 },
16 "author": "Greg Stewart",
17 "license": "MIT",
18 "bugs": {
19 "url": "https://github.com/gregstewart/weatherly/issues"
20 },
21 "homepage": "https://github.com/gregstewart/weatherly",
22 "dependencies": {
23 "express": "^4.4.5",
24 "grunt-contrib-copy": "^0.5.0",
25 "grunt-contrib-less": "^0.11.3"
26 },
27 "devDependencies": {
28 "chai": "^1.9.1",
29 "cucumber": "^0.4.0",
30 "grunt": "^0.4.5",
31 "grunt-contrib-copy": "^0.5.0",
32 "grunt-contrib-less": "^0.11.3",
33 "grunt-cucumber": "^0.2.3",
34 "grunt-express-server": "^0.4.17",
35 "grunt-selenium-webdriver": "^0.2.420",
36 "webdriverjs": "^1.7.1"
37 }
38 }
This is another thing about this approach that I am not a fan of, having to move what are essentially development dependencies into our production dependencies.
Now we are nearly ready to test this out, however there is one more task we need to add. Since we are using Bower for some of our front end components and we haven’t checked these into our repository, we’ll need to restore them from our bower.json file. Let’s first install a new grunt package to assist us:
1 npm install grunt-bower-task --save
And the edit our Gruntfile.js:
1 module.exports = function (grunt) {
2 grunt.initConfig({
3 express: {
4 test: {
5 options: {
6 script: './server.js'
7 }
8 }
9 },
10 cucumberjs: {
11 src: 'tests/e2e/features/',
12 options: {
13 steps: 'tests/e2e/steps/'
14 }
15 },
16 less: {
17 production: {
18 options: {
19 paths: ['app/css/'],
20 cleancss: true
21 },
22 files: {
23 'app/css/main.css': 'src/less/main.less'
24 }
25 }
26 },
27 copy: {
28 fonts: {
29 expand: true,
30 src: ['bower_components/bootstrap-sass-official/vendor/assets/fo\
31 nts/bootstrap/*'],
32 dest: 'app/css/bootstrap/',
33 filter: 'isFile',
34 flatten: true
35 }
36 },
37 bower: {
38 install: {
39 options: {
40 cleanTargetDir:false,
41 targetDir: './bower_components'
42 }
43 }
44 }
45 });
46
47 grunt.loadNpmTasks('grunt-express-server');
48 grunt.loadNpmTasks('grunt-selenium-webdriver');
49 grunt.loadNpmTasks('grunt-cucumber');
50 grunt.loadNpmTasks('grunt-contrib-less');
51 grunt.loadNpmTasks('grunt-contrib-copy');
52 grunt.loadNpmTasks('grunt-bower-task');
53
54 grunt.registerTask('generate', ['less:production', 'copy:fonts']);
55 grunt.registerTask('build', ['bower:install', 'generate']);
56
57 grunt.registerTask('e2e', [
58 'selenium_start',
59 'express:test',
60 'cucumberjs',
61 'selenium_stop',
62 'express:test:stop'
63 ]);
64
65 grunt.registerTask('heroku:production', 'build');
66 };
With that let’s push these changes and see if we can’t have a more nicely styled page appear on our Heroku app!
1 git add .
2 git commit -m "Generate less as part of the build and copy fonts to app folder"
3 git checkout master
4 git merge code-build
5 git push origin master
Concatenate and minify our JavaScript
Having generated our CSS at build time, it’s time to turn our attention to concatenating and minifying our JavaScript.
If you recall in our getting started section we set up our project and used Bower to manage our front end dependencies. For our code we will be Browserify and adopting a CommonJS approach to dealing with modules and dependencies.
TODO: CommonJS vs AMD and why
To get started first create a our source directory for our JavaScript, we’ll store our source under:
1 > mkdir node_modules/weatherly/js
Storing them under node_modules has a great benefit when using browserify, you no longer need require your files using relative paths such as this little beauty:
1 var model = require('../../src/js/model/TodaysWeather');
Typing all this out will get tedius very quickly. Time for a file to test our build process, call it TodaysWeather.js and let’s save it under a sub folder called models:
1 > mkdir node_modules/weatherly/js/models
2 > touch node_modules/weatherly/js/models/TodaysWeather.js
And add the following to that file:
1 var TodaysWeather = function () {
2 console.log('test');
3 };
4
5 module.exports = TodaysWeather;
At this point you may be wondering how we are going to commit the code under node_modules/weatherly/js, given how we have set up our .gitignore file. I will come to that shortly.
With that done let’s install a grunt task for Browserify
1 npm install grunt-browserify --save
The reason we have chosen a grunt task is that we will use this to export our source so that browsers can understand module.exports and use it to concatenate our code.
We’ll skip through a few steps below and edit our Gruntfile.js to include the task we just installed, define the steps to build our JavaScript and include it into our build task:
1 module.exports = function (grunt) {
2 grunt.initConfig({
3 express: {
4 test: {
5 options: {
6 script: './server.js'
7 }
8 }
9 },
10 cucumberjs: {
11 src: 'tests/e2e/features/',
12 options: {
13 steps: 'tests/e2e/steps/'
14 }
15 },
16 less: {
17 production: {
18 options: {
19 paths: ['app/css/'],
20 cleancss: true
21 },
22 files: {
23 'app/css/main.css': 'src/less/main.less'
24 }
25 }
26 },
27 copy: {
28 fonts: {
29 expand: true,
30 src: ['bower_components/bootstrap-sass-official/vendor/assets/fo\
31 nts/bootstrap/*'],
32 dest: 'app/css/bootstrap/',
33 filter: 'isFile',
34 flatten: true
35 }
36 },
37 bower: {
38 install: {
39 options: {
40 cleanTargetDir:false,
41 targetDir: './bower_components'
42 }
43 }
44 },
45 browserify: {
46 code: {
47 dest: 'app/js/main.min.js',
48 src: 'node_modules/weatherly/js/**/*.js' }
49 }
50 });
51
52 grunt.loadNpmTasks('grunt-express-server');
53 grunt.loadNpmTasks('grunt-selenium-webdriver');
54 grunt.loadNpmTasks('grunt-cucumber');
55 grunt.loadNpmTasks('grunt-contrib-less');
56 grunt.loadNpmTasks('grunt-contrib-copy');
57 grunt.loadNpmTasks('grunt-bower-task');
58 grunt.loadNpmTasks('grunt-browserify');
59
60 grunt.registerTask('generate', ['less:production', 'copy:fonts', 'browserify:co\
61 de']);
62 grunt.registerTask('build', ['bower:install', 'generate']);
63 grunt.registerTask('e2e', [
64 'selenium_start',
65 'express:test',
66 'cucumberjs',
67 'selenium_stop',
68 'express:test:stop'
69 ]);
70
71 grunt.registerTask('heroku:production', 'build');
72 };
If we now run our generate task you should find a main.min.js file under app/js, which contains a bunch of Browserify and our test file. However you will notice that while it’s concatenated it’s not minified. Let’s fix this.
I chose to go with Uglifyify, as always let’s just install it:
1 npm install uglifyify --save
And then edit our Gruntfile.js by configuring our browserify task to use it is a transform:
1 module.exports = function (grunt) {
2 grunt.initConfig({
3 express: {
4 test: {
5 options: {
6 script: './server.js'
7 }
8 }
9 },
10 cucumberjs: {
11 src: 'tests/e2e/features/',
12 options: {
13 steps: 'tests/e2e/steps/'
14 }
15 },
16 less: {
17 production: {
18 options: {
19 paths: ['app/css/'],
20 cleancss: true
21 },
22 files: {
23 'app/css/main.css': 'src/less/main.less'
24 }
25 }
26 },
27 copy: {
28 fonts: {
29 expand: true,
30 src: ['bower_components/bootstrap-sass-official/vendor/assets/fo\
31 nts/bootstrap/*'],
32 dest: 'app/css/bootstrap/',
33 filter: 'isFile',
34 flatten: true
35 }
36 },
37 bower: {
38 install: {
39 options: {
40 cleanTargetDir:false,
41 targetDir: './bower_components'
42 }
43 }
44 },
45 browserify: {
46 code: {
47 dest: 'app/js/main.min.js',
48 src: 'node_modules/weatherly/js/**/*.js',
49 options: {
50 transform: ['uglifyify']
51 }
52 }
53 }
54 });
55
56 grunt.loadNpmTasks('grunt-express-server');
57 grunt.loadNpmTasks('grunt-selenium-webdriver');
58 grunt.loadNpmTasks('grunt-cucumber');
59 grunt.loadNpmTasks('grunt-contrib-less');
60 grunt.loadNpmTasks('grunt-contrib-copy');
61 grunt.loadNpmTasks('grunt-bower-task');
62 grunt.loadNpmTasks('grunt-browserify');
63
64 grunt.registerTask('generate', ['less:production', 'copy:fonts', 'browserify\
65 ']);
66 grunt.registerTask('build', ['bower:install', 'generate']);
67
68 grunt.registerTask('e2e', [
69 'selenium_start',
70 'express:test',
71 'cucumberjs',
72 'selenium_stop',
73 'express:test:stop'
74 ]);
75
76 grunt.registerTask('heroku:production', 'build');
77 };
If you now run our generate task, the contents of main should be nicely minified. Now all that’s left to do is edit our index.html file and add our generated JavaScript file:
1 <!DOCTYPE html>
2 <!--[if lt IE 7]> <html class="no-js lt-ie9 lt-ie8 lt-ie7"> <![endif]-->
3 <!--[if IE 7]> <html class="no-js lt-ie9 lt-ie8"> <![endif]-->
4 <!--[if IE 8]> <html class="no-js lt-ie9"> <![endif]-->
5 <!--[if gt IE 8]><!--> <html class="no-js"> <!--<![endif]-->
6 <head>
7 <meta charset="utf-8">
8 <meta http-equiv="X-UA-Compatible" content="IE=edge">
9 <title>Weatherly - Forecast for London</title>
10 <meta name="description" content="">
11 <meta name="viewport" content="width=device-width, initial-scale=1">
12
13 <!-- Place favicon.ico and apple-touch-icon.png in the root directory -->
14
15 <link rel="stylesheet" href="css/main.css">
16 </head>
17 <body>
18 <!--[if lt IE 7]>
19 <p class="browsehappy">You are using an <strong>outdated</strong> br\
20 owser. Please <a href="http://browsehappy.com/">upgrade your browser</a> to impr\
21 ove your experience.</p>
22 <![endif]-->
23
24 <!-- Add your site or application content here -->
25 <div class="container">
26 <div class="header">
27 <ul class="nav nav-pills pull-right">
28 <li class="active"><a href="#">Home</a></li>
29 <li><a href="#">About</a></li>
30 <li><a href="#">Contact</a></li>
31 </ul>
32 </div>
33
34 <div class="jumbotron">
35 <h1>London Right Now</h1>
36 <p class="temperature">14 degrees</p>
37 <p>Mostly cloudy - feels like 14 degrees</p>
38 </div>
39
40 <div class="row marketing">
41 <div class="col-lg-6">
42 <h4>NEXT HOUR</h4>
43 <p>Mostly cloudy for the hour.</p>
44
45 <h4>NEXT 24 HOURS</h4>
46 <p>Mostly cloudy until tomorrow afternoon.</p>
47 </div>
48 </div>
49
50 <div class="footer">
51 <p><span class="glyphicon glyphicon-heart"></span> from Weatherl\
52 y</p>
53 </div>
54
55 </div>
56 <p></p>
57
58 <script src="//ajax.googleapis.com/ajax/libs/jquery/1.10.2/jquery.min.js\
59 "></script>
60 <script>window.jQuery || document.write('<script src="js/vendor/jquery-1\
61 .10.2.min.js"><\/script>')</script>
62
63 <!-- Google Analytics: change UA-XXXXX-X to be your site's ID. -->
64 <script>
65 (function(b,o,i,l,e,r){b.GoogleAnalyticsObject=l;b[l]||(b[l]=
66 function(){(b[l].q=b[l].q||[]).push(arguments)});b[l].l=+new\
67 Date;
68 e=o.createElement(i);r=o.getElementsByTagName(i)[0];
69 e.src='//www.google-analytics.com/analytics.js';
70 r.parentNode.insertBefore(e,r)}(window,document,'script','ga'));
71 ga('create','UA-XXXXX-X');ga('send','pageview');
72 </script>
73 <script src="js/main.min.js"></script>
74 </body>
75 </html>
Before we commit our changes let’s edit our .gitignore file one more time and tell it not to include our generated JavaScript:
1 node_modules
2 .idea
3 bower_components
4 phantomjsdriver.log
5 app/css/
6 app/fonts/
7 app/js/
While are here let’s fix our git configuration so that it includes our app code by editing our .gitignore file to look as follows:
1 .idea
2 bower_components
3 phantomjsdriver.log
4 app/css/
5 app/fonts/
6 app/js/
7 node_modules/*
8 !node_modules/weatherly
By changing the blanket exclusion rule for our node_modules to one using exceptions, we can now commit the weatherly code that sits under our node_modules folder. That’s what the last two lines do:
1 node_modules/*
2 !node_modules/weatherly
Now when you type git add . it will include all if your changes:
1 > git status
2 > # On branch generate-assets
3 > # Changes to be committed:
4 > # (use "git reset HEAD <file>..." to unstage)
5 > #
6 > # modified: .gitignore
7 > # modified: Gruntfile.js
8 > # new file: node_modules/weatherly/js/model/TodaysWeather.js
9 > # modified: package.json
10 > # modified: app/index.html
11 > #
12 > # Changes not staged for commit:
13 > # (use "git add/rm <file>..." to update what will be committed)
14 > # (use "git checkout -- <file>..." to discard changes in working directory)
15 > #
Let’s commit, merge and push to our remote repository:
1 git add .
2 git commit -m "JavaScript browserified and uglyfied"
3 git checkout master
4 git merge generate-assets
5 git push
TODO recap for the chapter