mastering grunt

74
MASTERING GRUNT Spencer Handley

Upload: spencer-handley

Post on 17-Jan-2017

521 views

Category:

Engineering


3 download

TRANSCRIPT

Page 1: Mastering Grunt

MASTERING GRUNTS p e n c e r H a n d l e y

Page 2: Mastering Grunt

ABOUT MES P E N C E R H A N D L E Y

Mastering Grunt VIDEO SERIES

@spencer414

www.spencerhand.ly

Page 3: Mastering Grunt

GRUNT BASICS

Minifying

Uglifying

Concatenating

Linting

CDNifying

Image Optimization

Replacing Compiling StylusWatch/Live Reload

much more…

Page 4: Mastering Grunt

USED BY

*shameless plug

Page 5: Mastering Grunt

REASONS TO USE IT

• Huge Community

• Strong Adoption

• Valuable Resume Boost

• Highly in Demand

• Easy and, dare I say fun to use

• All the cool kids use it

Page 6: Mastering Grunt

PLUGINS

4000+ today

Page 7: Mastering Grunt

GRUNTFILE

'use strict';

module.exports = function (grunt) {

require('jit-grunt')(grunt, { }); var appConfig = { app: require('./bower.json').appPath || 'app', dist: 'dist' };

grunt.initConfig({ });

grunt.registerTask('default', [

]);};

Page 8: Mastering Grunt

INIT CONFIG

grunt.initConfig({ concat: { foo: { // concat task "foo" target options and files go here. }, bar: { // concat task "bar" target options and files go here. }, }, uglify: { bar: { // uglify task "bar" target options and files go here. }, },

});

When you run a task, Grunt looks here for it’s configuration.

Page 9: Mastering Grunt

INIT CONFIG

Multi-tasks can have multiple configurations, defined using

arbitrarily named "targets."gruntjs.com

concat: { foo: { // You could run this with concat:foo }, bar: { // You could run this with concat:bar }, }

Page 10: Mastering Grunt

INIT CONFIG

Multi-tasks can have multiple configurations, defined using

arbitrarily named "targets."gruntjs.com concat: {

foo: { // You could run this with concat:foo }, bar: { // You could run this with concat:bar }, }

Simply running concat will integrate through all targets

Page 11: Mastering Grunt

TASK CONFIGURATION:

OPTIONS

Inside a task configuration, an options property may be specified to override built-in defaults.

You can also pass options to each target.

concat: { options: { // Task-level options may go here, overriding task defaults. }, foo: { options: { // "foo" target options may go here, overriding task-level options. }, }, bar: { // No options specified; this target will use task-level options. }, }

Page 12: Mastering Grunt

DEALING WITH FILES

Page 13: Mastering Grunt

COMPACT FORMAT

grunt.initConfig({ jshint: { foo: { src: ['src/aa.js', 'src/aaa.js'] }, }, concat: { bar: { src: ['src/bb.js', 'src/bbb.js'], dest: 'dest/b.js', }, },});

Typically for read-only tasks where the dest is not needed. Like JShint

Page 14: Mastering Grunt

FILE OBJECT FORMATgrunt.initConfig({ concat: { foo: { files: { 'dest/a.js': ['src/aa.js', 'src/aaa.js'], 'dest/a1.js': ['src/aa1.js', 'src/aaa1.js'], }, }, bar: { files: { 'dest/b.js': ['src/bb.js', 'src/bbb.js'], 'dest/b1.js': ['src/bb1.js', 'src/bbb1.js'], }, }, },});

destination: [source files]

multiple src-dest mappings per-target

Page 15: Mastering Grunt

FILE ARRAY FORMAT

grunt.initConfig({ concat: { foo: { files: [ {src: ['src/aa.js', 'src/aaa.js'], dest: 'dest/a.js'}, {src: ['src/aa1.js', 'src/aaa1.js'], dest: 'dest/a1.js'}, ], }, bar: { files: [ {src: ['src/bb.js', 'src/bbb.js'], dest: 'dest/b/', nonull: true}, {src: ['src/bb1.js', 'src/bbb1.js'], dest: 'dest/b1/', filter: 'isFile'}, ], }, },}); supports multiple src-dest file mappings per-target

while also allowing additional properties per mapping.

Page 16: Mastering Grunt

FILTER FUNCTION

grunt.initConfig({ clean: { foo: { src: ['tmp/**/*'], filter: 'isFile', }, },});

Will clean only if the pattern matches an actual file:

Uses nodes valid fs.Stats method names

Page 17: Mastering Grunt

CUSTOM FILTER FUNCTION

grunt.initConfig({ clean: { foo: { src: ['tmp/**/*'], filter: function(filepath) { return (grunt.file.isDir(filepath) && require('fs').readdirSync(filepath).length === 0); }, }, },});

The following will only clean folders that are empty

You can create custom filters for specifying files

Page 18: Mastering Grunt

CUSTOM FILTER FUNCTION

grunt.initConfig({ clean: { foo: { src: ['tmp/**/*'], filter: function(filepath) { return (grunt.file.isDir(filepath) && require('fs').readdirSync(filepath).length === 0); }, }, },});

The following will only clean folders that are empty

You can create custom filters for specifying files

Page 19: Mastering Grunt

TANGENT TIME

Page 20: Mastering Grunt

GLOBBING BASICS

* ?**

{}!

matches any number of characters, but not / matches a single character, but not / matches any number of characters, including /, as long as it's the only thing in a path part

allows for a comma-separated list of "or" expressions at the beginning of a pattern will negate the match

Page 21: Mastering Grunt

GLOBBING EXAMPLES

// You can specify single files:{src: 'foo/this.js', dest: ...}// Or arrays of files:{src: ['foo/this.js', 'foo/that.js', 'foo/the-other.js'], dest: ...}// Or you can generalize with a glob pattern:{src: 'foo/th*.js', dest: ...}// All .js files, in foo/, in alpha order:{src: ['foo/*.js'], dest: ...}// Here, bar.js is first, followed by the remaining files, in alpha order:{src: ['foo/bar.js', 'foo/*.js'], dest: ...}

Page 22: Mastering Grunt

MORE EXAMPLES

// This single node-glob pattern:{src: 'foo/{a,b}*.js', dest: ...}// Could also be written like this:{src: ['foo/a*.js', 'foo/b*.js'], dest: ...}

// All .js files, in foo/, in alpha order:{src: ['foo/*.js'], dest: …}

// Here, bar.js is first, followed by the remaining files, in alpha order:{src: ['foo/bar.js', 'foo/*.js'], dest: ...}

Page 23: Mastering Grunt

MORE EXAMPLES

// All files except for bar.js, in alpha order:{src: ['foo/*.js', '!foo/bar.js'], dest: ...}// All files in alpha order, but with bar.js at the end.{src: ['foo/*.js', '!foo/bar.js', 'foo/bar.js'], dest: ...}

// Templates may be used in filepaths or glob patterns:{src: ['src/<%= basename %>.js'], dest: 'build/<%= basename %>.min.js'}// But they may also reference file lists defined elsewhere in the config:{src: ['foo/*.js', '<%= jshint.all.src %>'], dest: ...}

Page 24: Mastering Grunt

TEMPLATES IN GLOBS

// Templates may be used in filepaths or glob patterns:{src: ['src/<%= basename %>.js'], dest: 'build/<%= basename %>.min.js'}// But they may also reference file lists defined elsewhere in the config:{src: ['foo/*.js', '<%= jshint.all.src %>'], dest: ...}

<% %> are delimiters to specify templates

Additionally, grunt and its methods are available inside templates, eg. <%= grunt.template.today('yyyy-mm-dd') %>.

Page 25: Mastering Grunt

grunt.initConfig({ uglify: { static_mappings: { // Because these src-dest file mappings are manually specified, every // time a new file is added or removed, the Gruntfile has to be updated. files: [ {src: 'lib/a.js', dest: 'build/a.min.js'}, {src: 'lib/b.js', dest: 'build/b.min.js'}, {src: 'lib/subdir/c.js', dest: 'build/subdir/c.min.js'}, {src: 'lib/subdir/d.js', dest: 'build/subdir/d.min.js'}, ], }, dynamic_mappings: { // Grunt will search for "**/*.js" under "lib/" when the "uglify" task // runs and build the appropriate src-dest file mappings then, so you // don't need to update the Gruntfile when files are added or removed. files: [ { expand: true, // Enable dynamic expansion. Must be set to enable these options cwd: 'lib/', // Src matches are relative to this path. src: ['**/*.js'], // Actual pattern(s) to match. dest: 'build/', // Destination path prefix. ext: '.min.js', // Dest filepaths will have this extension. extDot: 'first' // Extensions in filenames begin after the first dot }, ], }, },});

DYNAMIC MAPPINGEnable dynamic expansion.

Page 26: Mastering Grunt

TEMPLATES EXAMPLE

grunt.initConfig({ concat: { sample: { options: { banner: '/* <%= baz %> */\n', // '/* abcde */\n' }, src: ['<%= qux %>', 'baz/*.js'], // [['foo/*.js', 'bar/*.js'], 'baz/*.js'] dest: 'build/<%= baz %>.js', // 'build/abcde.js' }, }, // Arbitrary properties used in task configuration templates. foo: 'c', bar: 'b<%= foo %>d', // 'bcd' baz: 'a<%= bar %>e', // 'abcde' qux: ['foo/*.js', 'bar/*.js'],});

Page 27: Mastering Grunt

IMPORTING EXTERNAL DATA

grunt.initConfig({ pkg: grunt.file.readJSON('package.json'), uglify: { options: { banner: '/*! <%= pkg.name %> <%= grunt.template.today("yyyy-mm-dd") %> */\n' }, dist: { src: 'src/<%= pkg.name %>.js', dest: 'dist/<%= pkg.name %>.min.js' } }});

Here project metadata is imported into the Grunt config from a package.json file,

Page 28: Mastering Grunt

CREATING TASKS

grunt.registerTask(taskName, [description, ] taskList)

Page 29: Mastering Grunt

SIMPLE EXAMPLE

grunt.registerTask('default', ['jshint', 'qunit', 'concat']);

Page 30: Mastering Grunt

Task Arguments

grunt.registerTask('dist', ['concat:dist', 'uglify:dist']);

Here we create a task called dist passing dist as our target properties on each task.

Page 31: Mastering Grunt

Task Arguments

grunt.registerTask('dist', ['concat:dist', 'uglify:dist']);

Here we create a task called dist passing dist as our target properties on each task.

Page 32: Mastering Grunt

MULTI TASKS

grunt.registerMultiTask(taskName, [description, ] taskFunction)

Page 33: Mastering Grunt

MULTI TASKS

grunt.initConfig({ log: { foo: [1, 2, 3], bar: 'hello world', baz: false }});

grunt.registerMultiTask('log', 'Log stuff.', function() { grunt.log.writeln(this.target + ': ' + this.data);});

What would happen if we ran grunt log?

Page 34: Mastering Grunt

foo: [1, 2, 3]

bar: 'hello world’

baz: false

What would happen if we ran grunt log?

Page 35: Mastering Grunt

foo: [1, 2, 3]

bar: 'hello world’

baz: false

What would happen if we ran grunt log?

Page 36: Mastering Grunt

When a basic task is run, Grunt doesn't look at the configuration or environment—it just runs the specified task function, passing any specified colon-separated arguments in as function arguments.

gruntjs.com

Page 37: Mastering Grunt

grunt.registerTask('foo', 'A sample task that logs stuff.', function(arg1, arg2) { if (arguments.length === 0) { grunt.log.writeln(this.name + ", no args"); } else { grunt.log.writeln(this.name + ", " + arg1 + " " + arg2); }});

This example task logs foo, testing 123 if Grunt is run via grunt foo:testing:123. If the task is run without arguments as

grunt foo the task logs foo, no args.

Page 38: Mastering Grunt

grunt.registerTask('foo', 'My "foo" task.', function() { // Enqueue "bar" and "baz" tasks, to run after "foo" finishes, in-order. grunt.task.run('bar', 'baz'); // Or: grunt.task.run(['bar', 'baz']);});

CUSTOM TASKS

If your tasks don't follow the "multi task" structure, use a custom task.

Page 39: Mastering Grunt

grunt.registerTask('asyncfoo', 'My "asyncfoo" task.', function() { // Force task into async mode and grab a handle to the "done" function. var done = this.async(); // Run some sync stuff. grunt.log.writeln('Processing task...'); // And some async stuff. setTimeout(function() { grunt.log.writeln('All done!'); done(); }, 1000);});

CUSTOM TASKSExample of an asynchronous Task

Page 40: Mastering Grunt

grunt.registerTask('asyncfoo', 'My "asyncfoo" task.', function() { // Force task into async mode and grab a handle to the "done" function. var done = this.async(); // Run some sync stuff. grunt.log.writeln('Processing task...'); // And some async stuff. setTimeout(function() { grunt.log.writeln('All done!'); done(); }, 1000);});

CUSTOM TASKSExample of an asynchronous Task

Page 41: Mastering Grunt

Cool parts of tasks pt 1

Can reference their own name with this.name

Can fail if any errors were logged // Fail by returning false if this task had errors if (ifErrors) { return false; }

console.log(this.name)

Page 42: Mastering Grunt

Cool parts of tasks pt 2

Tasks can be dependent on the successful execution of other tasks.

grunt.registerTask('foo', 'My "foo" task.', function() { return false;});

grunt.registerTask('bar', 'My "bar" task.', function() { // Fail task if "foo" task failed or never ran. grunt.task.requires('foo'); // This code executes if the "foo" task ran successfully. grunt.log.writeln('Hello, world.');});

Page 43: Mastering Grunt

Tasks can access configuration properties.

grunt.registerTask('foo', 'My "foo" task.', function() { // Log the property value. Returns null if the property is undefined. grunt.log.writeln('The meta.name property is: ' + grunt.config('meta.name')); // Also logs the property value. Returns null if the property is undefined. grunt.log.writeln('The meta.name property is: ' + grunt.config(['meta', 'name']));});

Cool parts of tasks pt 3

Page 44: Mastering Grunt

LET’S DIG INTO AN EXAMPLE

Page 45: Mastering Grunt

SHARED CHAT

https://tlk.io/gruntdemo

Page 46: Mastering Grunt

SETUP

npm install -g grunt-cli npm install -g bower

https://nodejs.org/download/

Install Node

Install Grunt CLI and Bower

Page 47: Mastering Grunt

DEMO APP

https://github.com/spencer48/Grunt-Demo

Fork this on Git Hub then…

git clone https://github.com/YOURUSERNAME/Grunt-Demo

Page 48: Mastering Grunt

INSTALL

npm install bower install

Page 49: Mastering Grunt

ARCHITECTURE

app/images/scripts/controllers/services/app.js

styles/views/index.html

bower_components/node_modules/test/.tmp/.saas-cache/.bowerrcbower.jsonGruntfile.jspackage.jsonREADME.md

Page 50: Mastering Grunt

GRUNTFILE

'use strict';

module.exports = function (grunt) {

require('jit-grunt')(grunt, { }); var appConfig = { app: require('./bower.json').appPath || 'app', dist: 'dist' };

grunt.initConfig({ });

grunt.registerTask('default', [

]);};

Page 51: Mastering Grunt

INIT CONFIG

grunt.initConfig({ concat: { foo: { // concat task "foo" target options and files go here. }, bar: { // concat task "bar" target options and files go here. }, }, uglify: { bar: { // uglify task "bar" target options and files go here. }, },

});

When you run a task, Grunt looks here for it’s configuration.

Page 52: Mastering Grunt

ADDING DEPENDENCIES

{ "name": "gruntdemo", "devDependencies": { "grunt": "^0.4.5", "grunt-concurrent": "^1.0.0", "grunt-contrib-connect": "^0.9.0", "grunt-contrib-watch": "^0.6.1", "grunt-newer": "^1.1.0", "jit-grunt": "^0.9.1", "jshint-stylish": "^1.0.0", "time-grunt": "^1.0.0" }, "engines": { "node": ">=0.10.0" }}

…then npm install

package.json

Page 53: Mastering Grunt

SETTING UP CONNECT connect: { options: { port: 9000, hostname: 'localhost', livereload: 35729 }, livereload: { options: { open: true, middleware: function (connect) { return [ connect.static('.tmp'), connect().use( '/bower_components', connect.static('./bower_components') ), connect().use( '/app/styles', connect.static('./app/styles') ), connect.static(appConfig.app) ]; } } }, }

Page 54: Mastering Grunt

SETTING UP WATCHwatch: { bower: { files: ['bower.json'], tasks: ['wiredep'] }, js: { files: ['<%= yeoman.app %>/scripts/{,*/}*.js'], tasks: ['newer:jshint:all'], options: { livereload: '<%= connect.options.livereload %>' } }, compass: { files: ['<%= yeoman.app %>/styles/{,*/}*.{scss,sass}'], tasks: ['compass:server', 'autoprefixer:server'] }, gruntfile: { files: ['Gruntfile.js'] }, livereload: { options: { livereload: '<%= connect.options.livereload %>' }, files: [ '<%= yeoman.app %>/{,*/}*.html', '.tmp/styles/{,*/}*.css', '<%= yeoman.app %>/images/{,*/}*.{png,jpg,jpeg,gif,webp,svg}' ] } }

Page 55: Mastering Grunt

CONFIGURING JSHINT

jshint: { options: { jshintrc: '.jshintrc', reporter: require('jshint-stylish') }, all: { src: [ 'Gruntfile.js', '<%= yeoman.app %>/scripts/{,*/}*.js' ] }, test: { options: { jshintrc: 'test/.jshintrc' }, src: ['test/spec/{,*/}*.js'] } }

{ "bitwise": true, "browser": true, "curly": true, "eqeqeq": true, "esnext": true, "latedef": true, "noarg": true, "node": true, "strict": true, "undef": true, "unused": true, "globals": { "angular": false }}

.jshintrcGruntFile.js

Page 56: Mastering Grunt

CONFIGURING CLEAN

clean: { dist: { files: [{ dot: true, src: [ '.tmp', '<%= yeoman.dist %>/{,*/}*', '!<%= yeoman.dist %>/.git{,*/}*' ] }] }, server: '.tmp' }

GruntFile.js

Page 57: Mastering Grunt

CONFIGURING WIREDEP

wiredep: { app: { src: ['<%= yeoman.app %>/index.html'], ignorePath: /\.\.\// }, test: { devDependencies: true, src: '<%= karma.unit.configFile %>', ignorePath: /\.\.\//, fileTypes:{ js: { block: /(([\s\t]*)\/{2}\s*?bower:\s*?(\S*))(\n|\r|.)*?(\/{2}\s*endbower)/gi, detect: { js: /'(.*\.js)'/gi }, replace: { js: '\'{{filePath}}\',' } } } }, sass: { src: ['<%= yeoman.app %>/styles/{,*/}*.{scss,sass}'], ignorePath: /(\.\.\/){1,2}bower_components\// } },

GruntFile.js

Page 58: Mastering Grunt

CONFIGURING COMPASS

compass: { options: { sassDir: '<%= yeoman.app %>/styles', cssDir: '.tmp/styles', generatedImagesDir: '.tmp/images/generated', imagesDir: '<%= yeoman.app %>/images', javascriptsDir: '<%= yeoman.app %>/scripts', fontsDir: '<%= yeoman.app %>/styles/fonts', importPath: './bower_components', httpImagesPath: '/images', httpGeneratedImagesPath: '/images/generated', httpFontsPath: '/styles/fonts', relativeAssets: false, assetCacheBuster: false, raw: 'Sass::Script::Number.precision = 10\n' }, dist: { options: { generatedImagesDir: '<%= yeoman.dist %>/images/generated' } }, server: { options: { sourcemap: true } } }

GruntFile.js

Page 59: Mastering Grunt

CONFIGURING CLEAN

clean: { dist: { files: [{ dot: true, src: [ '.tmp', '<%= yeoman.dist %>/{,*/}*', '!<%= yeoman.dist %>/.git{,*/}*' ] }] }, server: '.tmp' }

GruntFile.js

Page 60: Mastering Grunt

CONFIGURING AUTOPREFIXER

// Add vendor prefixed styles autoprefixer: { options: { browsers: ['last 1 version'] }, server: { options: { map: true, }, files: [{ expand: true, cwd: '.tmp/styles/', src: '{,*/}*.css', dest: '.tmp/styles/' }] }, dist: { files: [{ expand: true, cwd: '.tmp/styles/', src: '{,*/}*.css', dest: '.tmp/styles/' }] } }

GruntFile.js

Page 61: Mastering Grunt

CONFIGURING FILE REV

// Renames files for browser caching purposes filerev: { dist: { src: [ '<%= yeoman.dist %>/scripts/{,*/}*.js', '<%= yeoman.dist %>/styles/{,*/}*.css', '<%= yeoman.dist %>/images/{,*/}*.{png,jpg,jpeg,gif,webp,svg}', '<%= yeoman.dist %>/styles/fonts/*' ] } },

GruntFile.js

Page 62: Mastering Grunt

CONFIGURING USEMIN

useminPrepare: { html: '<%= yeoman.app %>/index.html', options: { dest: '<%= yeoman.dist %>', flow: { html: { steps: { js: ['concat', 'uglifyjs'], css: ['cssmin'] }, post: {} } } } },

GruntFile.js

Page 63: Mastering Grunt

CONFIGURING USEMIN

// Performs rewrites based on filerev and the useminPrepare configuration usemin: { html: ['<%= yeoman.dist %>/{,*/}*.html'], css: ['<%= yeoman.dist %>/styles/{,*/}*.css'], js: ['<%= yeoman.dist %>/scripts/{,*/}*.js'], options: { assetsDirs: [ '<%= yeoman.dist %>', '<%= yeoman.dist %>/images', '<%= yeoman.dist %>/styles' ], patterns: { js: [[/(images\/[^''""]*\.(png|jpg|jpeg|gif|webp|svg))/g, 'Replacing references to images']] } } },

GruntFile.js

Page 64: Mastering Grunt

CONFIGURING IMG/SVG MIN

imagemin: { dist: { files: [{ expand: true, cwd: '<%= yeoman.app %>/images', src: '{,*/}*.{png,jpg,jpeg,gif}', dest: '<%= yeoman.dist %>/images' }] } },

svgmin: { dist: { files: [{ expand: true, cwd: '<%= yeoman.app %>/images', src: '{,*/}*.svg', dest: '<%= yeoman.dist %>/images' }] } },

GruntFile.js

Page 65: Mastering Grunt

CONFIGURING HTML MIN

htmlmin: { dist: { options: { collapseWhitespace: true, conservativeCollapse: true, collapseBooleanAttributes: true, removeCommentsFromCDATA: true }, files: [{ expand: true, cwd: '<%= yeoman.dist %>', src: ['*.html'], dest: '<%= yeoman.dist %>' }] } },

GruntFile.js

Page 66: Mastering Grunt

CONFIGURING NGTEMPATES

ngtemplates: { dist: { options: { module: 'gruntdemoApp', htmlmin: '<%= htmlmin.dist.options %>', usemin: 'scripts/scripts.js' }, cwd: '<%= yeoman.app %>', src: 'views/{,*/}*.html', dest: '.tmp/templateCache.js' } },

GruntFile.js

Page 67: Mastering Grunt

CONFIGURING NGANNOTATE

ngAnnotate: { dist: { files: [{ expand: true, cwd: '.tmp/concat/scripts', src: '*.js', dest: '.tmp/concat/scripts' }] } }

GruntFile.js

Page 68: Mastering Grunt

CONFIGURING NGANNOTATE

copy: { dist: { files: [{ expand: true, dot: true, cwd: '<%= yeoman.app %>', dest: '<%= yeoman.dist %>', src: [ '*.{ico,png,txt}', '.htaccess', '*.html', 'images/{,*/}*.{webp}', 'styles/fonts/{,*/}*.*' ] }, { expand: true, cwd: '.tmp/images', dest: '<%= yeoman.dist %>/images', src: ['generated/*'] }, { expand: true, cwd: '.', src: 'bower_components/bootstrap-sass-official/assets/fonts/bootstrap/*', dest: '<%= yeoman.dist %>' }] }, styles: { expand: true, cwd: '<%= yeoman.app %>/styles', dest: '.tmp/styles/', src: '{,*/}*.css' } },

GruntFile.js

Sorry for the tiny font :(

Page 69: Mastering Grunt

CONFIGURING KARMA

karma: { unit: { configFile: 'test/karma.conf.js', singleRun: true } }

GruntFile.js

Page 70: Mastering Grunt

CONFIGURING CONCURRENT

// Run some tasks in parallel to speed up the build process concurrent: { server: [ 'compass:server' ], test: [ 'compass' ], dist: [ 'compass:dist', 'imagemin', 'svgmin' ] },

GruntFile.js

Page 71: Mastering Grunt

TASK EXAMPLES

grunt.registerTask('build', [ 'clean:dist', 'wiredep', 'useminPrepare', 'concurrent:dist', 'autoprefixer', 'ngtemplates', 'concat', 'ngAnnotate', 'copy:dist', 'cdnify', 'cssmin', 'uglify', 'filerev', 'usemin', 'htmlmin' ]);

BUILD grunt.registerTask('test', [ 'clean:server', 'wiredep', 'concurrent:test', 'autoprefixer', 'connect:test', 'karma' ]);

TEST

Page 72: Mastering Grunt

TASK EXAMPLES

grunt.registerTask('serve', 'Compile then start a connect web server', function (target) { if (target === 'dist') { return grunt.task.run([‘build', 'connect:dist:keepalive']); }

grunt.task.run([ 'clean:server', 'wiredep', 'concurrent:server', 'autoprefixer:server', 'connect:livereload', 'watch' ]); });

SERVE

Page 73: Mastering Grunt

THANKS

Page 74: Mastering Grunt

QUESTIONS?S P E N C E R H A N D L E Y

Mastering Grunt VIDEO SERIES

@spencer414

www.spencerhand.lywww.podclear.com