This post contains step-by-step instructions to enable development and deployment task automation for angular-seed projects using Grunt.
Step 1 – Preparing to use Grunt
To make sure that no npm errors will propagate during the installation of npm packages it is recommended to clean the npm cache.
“.. clear you cache! It might save you from switching professions.” – Glenn Block
$ npm cache clean
Next, install Grunt's command line interface, grunt-cli.
$ npm install -g grunt-cli
In fact, grunt-cli is the only required globally installed npm module to demo this post.
$ npm list -g -depth=0 C:\Users\Nikos\AppData\Roaming\npm └── grunt-cli@0.1.13
Step 2 - Create a Gruntfile.js in the root directory
'use strict'; module.exports = function (grunt) { require('load-grunt-tasks')(grunt); require('time-grunt')(grunt); grunt.initConfig({ myApp: { app: require('./bower.json').appPath || 'app', dist: 'dist' }, typescript: { base: { src: ['<%= myApp.app %>/js/{,*/}*.ts'], options: { target: 'es5', sourceMap: true } }, test: { src: ['test/unit/{,*/}*.ts'], options: { target: 'es5', sourceMap: true } } }, watch: { ts: { files: ['<%= myApp.app %>/js/{,*/}*.ts'], tasks: ['typescript'] }, tsTest: { files: ['test/unit/{,*/}*.ts'], tasks: ['typescript:test'] }, js: { files: ['<%= myApp.app %>/js/{,*/}*.js'], tasks: ['newer:jshint:all'], options: { livereload: true } }, jsTest: { files: ['test/unit/{,*/}*.js'], tasks: ['newer:jshint:test', 'karma'] }, styles: { files: ['<%= myApp.app %>/css/{,*/}*.css'], tasks: ['newer:copy:styles', 'autoprefixer'] }, gruntfile: { files: ['Gruntfile.js'] }, livereload: { options: { livereload: '<%= connect.options.livereload %>' }, files: [ '<%= myApp.app %>/{,*/}*.html', '.tmp/css/{,*/}*.css', '<%= myApp.app %>/img/{,*/}*.{png,jpg,jpeg,gif,webp,svg}' ] } }, connect: { options: { port: 9000, hostname: 'localhost', livereload: 35729 }, livereload: { options: { open: true, base: [ '.tmp', '<%= myApp.app %>' ] } }, test: { options: { port: 9001, base: [ '.tmp', 'test', '<%= myApp.app %>' ] } }, dist: { options: { base: '<%= myApp.dist %>' } } }, jshint: { options: { jshintrc: '.jshintrc', reporter: require('jshint-stylish') }, all: [ 'Gruntfile.js', '<%= myApp.app %>/js/{,*/}*.js' ], test: { options: { jshintrc: '.jshintrc' }, src: ['test/unit/{,*/}*.js'] } }, clean: { dist: { files: [{ dot: true, src: [ '.tmp', '<%= myApp.dist %>/*', '!<%= myApp.dist %>/.git*' ] }] }, server: '.tmp' }, autoprefixer: { options: { browsers: ['last 1 version'] }, dist: { files: [{ expand: true, cwd: '.tmp/css/', src: '{,*/}*.css', dest: '.tmp/css/' }] } }, 'bower-install': { app: { html: '<%= myApp.app %>/index.html', ignorePath: '<%= myApp.app %>/' } }, rev: { dist: { files: { src: [ '<%= myApp.dist %>/js/{,*/}*.js', '<%= myApp.dist %>/css/{,*/}*.css', '<%= myApp.dist %>/img/{,*/}*.{png,jpg,jpeg,gif,webp,svg}', '<%= myApp.dist %>/css/fonts/*' ] } } }, useminPrepare: { html: '<%= myApp.app %>/index.html', options: { dest: '<%= myApp.dist %>' } }, usemin: { html: ['<%= myApp.dist %>/{,*/}*.html'], css: ['<%= myApp.dist %>/css/{,*/}*.css'], options: { assetsDirs: ['<%= myApp.dist %>'] } }, imagemin: { dist: { files: [{ expand: true, cwd: '<%= myApp.app %>/img', src: '{,*/}*.{png,jpg,jpeg,gif}', dest: '<%= myApp.dist %>/img' }] } }, svgmin: { dist: { files: [{ expand: true, cwd: '<%= myApp.app %>/img', src: '{,*/}*.svg', dest: '<%= myApp.dist %>/img' }] } }, htmlmin: { dist: { options: { collapseWhitespace: true, collapseBooleanAttributes: true, removeCommentsFromCDATA: true, removeOptionalTags: true }, files: [{ expand: true, cwd: '<%= myApp.dist %>', src: ['*.html', 'partials/{,*/}*.html'], dest: '<%= myApp.dist %>' }] } }, ngmin: { dist: { files: [{ expand: true, cwd: '.tmp/concat/js', src: '*.js', dest: '.tmp/concat/js' }] } }, cdnify: { dist: { html: ['<%= myApp.dist %>/*.html'] } }, copy: { dist: { files: [{ expand: true, dot: true, cwd: '<%= myApp.app %>', dest: '<%= myApp.dist %>', src: [ '*.{ico,png,txt}', '.htaccess', '*.html', 'partials/{,*/}*.html', 'bower_components/**/*', 'img/{,*/}*.{webp}', 'fonts/*' ] }, { expand: true, cwd: '.tmp/img', dest: '<%= myApp.dist %>/img', src: ['generated/*'] }] }, styles: { expand: true, cwd: '<%= myApp.app %>/css', dest: '.tmp/css/', src: '{,*/}*.css' } }, concurrent: { server: [ 'copy:styles' ], test: [ 'copy:styles' ], dist: [ 'copy:styles', 'imagemin', 'svgmin' ] }, replace: { development: { options: { patterns: [{ json: grunt.file.readJSON( 'config.development.json') }] }, files: [{ expand: true, flatten: true, src: ['config.js'], dest: '<%= myApp.app %>/js' }] }, azure: { options: { patterns: [{ json: grunt.file.readJSON('config.azure.json') }] }, files: [{ expand: true, flatten: true, src: ['config.js'], dest: '<%= myApp.app %>/js' }] } }, karma: { unit: { configFile: './test/karma.conf.js', singleRun: true } } }); grunt.registerTask('serve', function (target) { if (target === 'dist') { return grunt.task.run(['build', 'connect:dist:keepalive']); } grunt.task.run([ 'clean:server', 'bower-install', 'concurrent:server', 'autoprefixer', 'connect:livereload', 'replace:development', 'watch' ]); }); grunt.registerTask('server', function () { grunt.log.warn( 'The `server` task has been deprecated. Use `grunt serve` to start a server.' ); grunt.task.run(['serve']); }); grunt.registerTask('test', [ 'clean:server', 'concurrent:test', 'autoprefixer', 'connect:test', 'karma' ]); grunt.registerTask('build', [ 'clean:dist', 'bower-install', 'useminPrepare', 'concurrent:dist', 'autoprefixer', 'concat', 'ngmin', 'copy:dist', 'cdnify', 'cssmin', 'uglify', 'rev', 'usemin', 'htmlmin' ]); grunt.registerTask('default', [ 'newer:jshint', 'test', 'build' ]); };
This is quite a sophisticated Gruntfile, with reasonable defaults and best practises, composed by generator-angular, as well as this post, and this post.
Step 3 – Add environment specific configuration files
{ "name": "value" }
Save the above key-value placeholder as config.azure.json and config.development.json in the root directory.
Step 4 – Make angular-mocks a Bower development-dependency
diff --git a/bower.json b/bower.json @@ -9,7 +9,9 @@ "angular": "1.2.x", "angular-route": "1.2.x", "angular-loader": "1.2.x", - "angular-mocks": "~1.2.x", "html5-boilerplate": "~4.3.0" - } + }, + "devDependencies": { + "angular-mocks": "~1.2.x" + } }
(Currently bower.json includes angular-mocks
as an application dependency, although it's really a development/testing dependency.)
Step 5 – Add the npm packages required by Grunt
diff --git a/package.json b/package.json @@ -11,12 +11,52 @@ "http-server": "^0.6.1", "bower": "^1.3.1", "shelljs": "^0.2.6", - "karma-junit-reporter": "^0.2.2" + "karma-junit-reporter": "^0.2.2", + "grunt": "~0.4.1", + "grunt-cli": "~0.1.13", + "grunt-autoprefixer": "~0.4.0", + "grunt-bower-install": "~0.7.0", + "grunt-concurrent": "~0.4.1", + "grunt-contrib-clean": "~0.5.0", + "grunt-contrib-coffee": "~0.7.0", + "grunt-contrib-compass": "~0.6.0", + "grunt-contrib-concat": "~0.3.0", + "grunt-contrib-connect": "~0.5.0", + "grunt-contrib-copy": "~0.4.1", + "grunt-contrib-cssmin": "~0.7.0", + "grunt-contrib-htmlmin": "~0.1.3", + "grunt-contrib-imagemin": "~0.3.0", + "grunt-contrib-jshint": "~0.7.1", + "grunt-contrib-uglify": "~0.2.0", + "grunt-contrib-watch": "~0.5.2", + "grunt-google-cdn": "~0.2.0", + "grunt-karma": "~0.6.2", + "grunt-newer": "~0.5.4", + "grunt-ngmin": "~0.0.2", + "grunt-replace": "~0.7.6", + "grunt-rev": "~0.1.0", + "grunt-svgmin": "~0.2.0", + "grunt-typescript": "*", + "grunt-usemin": "~2.0.0", + "jshint-stylish": "~0.1.3", + "load-grunt-tasks": "~0.2.0", + "requirejs": "~2.1.10", + "time-grunt": "~0.2.1", + "karma-ng-scenario": "~0.1.0", + "karma-html2js-preprocessor": "~0.1.0", + "karma-firefox-launcher": "~0.1.3", + "karma-script-launcher": "~0.1.0", + "karma-chrome-launcher": "~0.1.2", + "karma-jasmine": "~0.1.5", + "karma-coffee-preprocessor": "~0.1.2", + "karma-requirejs": "~0.2.1", + "karma-phantomjs-launcher": "~0.1.1", + "karma-ng-html2js-preprocessor": "~0.1.0" }, "scripts": { "postinstall": "bower install", - "prestart": "npm install", + "prestart": "npm install && grunt bower-install", "start": "http-server -a localhost -p 8000", "pretest": "npm install",
Step 6 – Install locally the npm and Bower packages
npm install
This has been pre-configured by angular-seed to also automatically call bower install
. Two new folders will be created in the project:
This normally takes a while.
Step 7 – Inject some awesomeness to index.html
diff --git a/app/index.html b/app/index.html @@ -9,9 +9,15 @@ <title>My AngularJS App</title> <meta name="description" content=""> <meta name="viewport" content="width=device-width, initial-scale=1"> + <!-- build:css styles/vendor.css --> <link rel="stylesheet" href="bower_components/html5-boilerplate/css/normalize.css"> <link rel="stylesheet" href="bower_components/html5-boilerplate/css/main.css"> + <!-- bower:css --> + <!-- endbower --> + <!-- endbuild --> + <!-- build:css({.tmp,app}) styles/main.css --> <link rel="stylesheet" href="css/app.css"/> + <!-- endbuild --> <script src="bower_components/html5-boilerplate/js/vendor/modernizr-2.6.2.min.js"></script> </head> <body> @@ -31,12 +37,19 @@ <!-- In production use: <script src="//ajax.googleapis.com/ajax/libs/angularjs/x.x.x/angular.min.js"></script> --> + <!-- build:js scripts/vendor.js --> + <!-- bower:js --> <script src="bower_components/angular/angular.js"></script> <script src="bower_components/angular-route/angular-route.js"></script> + <!-- endbower --> + <!-- endbuild --> + + <!-- build:js({.tmp,app}) scripts/scripts.js --> <script src="js/app.js"></script> <script src="js/services.js"></script> <script src="js/controllers.js"></script> <script src="js/filters.js"></script> <script src="js/directives.js"></script> + <!-- endbuild --> </body> </html>
The build-css
tag bundles and minifies CSS while build:js
bundles and minifies the scripts.
Run the angular-seed application
npm start
This is already pre-configured by angular-seed with a simple development web server.
In addition, grunt-bower-install is invoked to look at all bower.json components and determine the best order to inject their scripts in the HTML file.
That is also the reason why we injected
build:js
tags in the index.html.
Run the angular-seed application – using Grunt
grunt serve
Modify the index.html, partial views, TypeScript1, or JavaScript files to see everything in action.
Deploying
grunt
This normally takes a few seconds2.
Done, without errors. Execution Time (2014-06-02 21:57:42 UTC) concurrent:test 2.3s ■■■■■■■■■■■■■■■■■■■■ 14% karma:unit 4.5s ■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■ 27% concurrent:dist 6.5s ■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■ 40% copy:dist 381ms ■■■■ 2% uglify:generated 1.9s ■■■■■■■■■■■■■■■■■ 12% Total 16.3s
Pretty cool!
1To enable *.ts auto-compilation install TypeScript with npm install -g TypeScript
.
2In case of JSHint errors, under the globals
directive in .jshintrc
add the variables below:
diff --git a/.jshintrc b/.jshintrc @@ -1,6 +1,13 @@ { "globalstrict": true, "globals": { - "angular": false + "angular" : false, + "module" : false, + "require" : false, + "describe" : false, + "beforeEach": false, + "it" : false, + "inject" : false, + "expect" : false } }