MASTERING GRUNT
S p e n c e r H a n d l e y
ABOUT ME
S P E N C E R H A N D L E Y
Mastering Grunt
VIDEO SERIES
@spencer414
www.spencerhand.ly
GRUNT BASICS
Minifying
Uglifying
Concatenating
Linting
CDNifying
Image Optimization
Replacing Compiling StylusWatch/Live Reload
much more…
USED BY
*shameless plug
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
PLUGINS
4000+ today
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', [
]);
};
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.
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
},
}
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
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.
},
}
DEALING WITH FILES
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
FILE OBJECT FORMAT
grunt.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
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.
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
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
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
TANGENT TIME
GLOBBING BASICS
*
?
**
{}
!
matches any number of characters, but not /
matches a single character, but not /
matches any number of characters, including /, as long as
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
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: ...}
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: ...}
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: ...}
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') %>.
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 MAPPING
Enable dynamic expansion.
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'],
});
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,
CREATING TASKS
grunt.registerTask(taskName, [description, ] taskList)
SIMPLE EXAMPLE
grunt.registerTask('default', ['jshint', 'qunit', 'concat']);
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.
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.
MULTI TASKS
grunt.registerMultiTask(taskName, [description, ] taskFunction)
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?
foo: [1, 2, 3]
bar: 'hello world’
baz: false
What would happen if we ran grunt log?
foo: [1, 2, 3]
bar: 'hello world’
baz: false
What would happen if we ran grunt log?
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
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.
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.
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 TASKS
Example of an asynchronous Task
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 TASKS
Example of an asynchronous Task
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)
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.');
});
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
LET’S DIG INTO AN EXAMPLE
SHARED CHAT
https://tlk.io/gruntdemo
SETUP
npm install -g grunt-cli
npm install -g bower
https://nodejs.org/download/
Install Node
Install Grunt CLI and Bower
DEMO APP
https://github.com/spencer48/Grunt-Demo
Fork this on Git Hub
then…
git clone https://github.com/YOURUSERNAME/Grunt-Demo
INSTALL
npm install
bower install
ARCHITECTURE
app/
images/
scripts/
controllers/
services/
app.js
styles/
views/
index.html
bower_components/
node_modules/
test/
.tmp/
.saas-cache/
.bowerrc
bower.json
Gruntfile.js
package.json
README.md
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', [
]);
};
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.
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
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)
];
}
}
},
}
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}'
]
}
}
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
CONFIGURING CLEAN
clean: {
dist: {
files: [{
dot: true,
src: [
'.tmp',
'<%= yeoman.dist %>/{,*/}*',
'!<%= yeoman.dist %>/.git{,*/}*'
]
}]
},
server: '.tmp'
}
GruntFile.js
CONFIGURING WIREDEP
wiredep: {
app: {
src: ['<%= yeoman.app %>/index.html'],
ignorePath: /..//
},
test: {
devDependencies: true,
src: '<%= karma.unit.configFile %>',
ignorePath: /..//,
fileTypes:{
js: {
block: /(([st]*)/{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
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 = 10n'
},
dist: {
options: {
generatedImagesDir: '<%= yeoman.dist %>/images/generated'
}
},
server: {
options: {
sourcemap: true
}
}
}
GruntFile.js
CONFIGURING CLEAN
clean: {
dist: {
files: [{
dot: true,
src: [
'.tmp',
'<%= yeoman.dist %>/{,*/}*',
'!<%= yeoman.dist %>/.git{,*/}*'
]
}]
},
server: '.tmp'
}
GruntFile.js
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
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
CONFIGURING USEMIN
useminPrepare: {
html: '<%= yeoman.app %>/index.html',
options: {
dest: '<%= yeoman.dist %>',
flow: {
html: {
steps: {
js: ['concat', 'uglifyjs'],
css: ['cssmin']
},
post: {}
}
}
}
},
GruntFile.js
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
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
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
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
CONFIGURING NGANNOTATE
ngAnnotate: {
dist: {
files: [{
expand: true,
cwd: '.tmp/concat/scripts',
src: '*.js',
dest: '.tmp/concat/scripts'
}]
}
}
GruntFile.js
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 :(
CONFIGURING KARMA
karma: {
unit: {
configFile: 'test/karma.conf.js',
singleRun: true
}
}
GruntFile.js
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
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
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
THANKS
QUESTIONS?
S P E N C E R H A N D L E Y
Mastering Grunt
VIDEO SERIES
@spencer414
www.spencerhand.ly
www.podclear.com

Mastering Grunt

  • 1.
    MASTERING GRUNT S pe n c e r H a n d l e y
  • 2.
    ABOUT ME S PE N C E R H A N D L E Y Mastering Grunt VIDEO SERIES @spencer414 www.spencerhand.ly
  • 3.
  • 4.
  • 5.
    REASONS TO USEIT • Huge Community • Strong Adoption • Valuable Resume Boost • Highly in Demand • Easy and, dare I say fun to use • All the cool kids use it
  • 6.
  • 7.
    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', [ ]); };
  • 8.
    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.
  • 9.
    INIT CONFIG Multi-tasks canhave 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 }, }
  • 10.
    INIT CONFIG Multi-tasks canhave 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
  • 11.
    TASK CONFIGURATION: OPTIONS Inside atask 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. }, }
  • 12.
  • 13.
    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
  • 14.
    FILE OBJECT FORMAT grunt.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
  • 15.
    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.
  • 16.
    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
  • 17.
    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
  • 18.
    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
  • 19.
  • 20.
    GLOBBING BASICS * ? ** {} ! matches anynumber of characters, but not / matches a single character, but not / matches any number of characters, including /, as long as 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
  • 21.
    GLOBBING EXAMPLES // Youcan 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: ...}
  • 22.
    MORE EXAMPLES // Thissingle 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: ...}
  • 23.
    MORE EXAMPLES // Allfiles 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: ...}
  • 24.
    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') %>.
  • 25.
    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 MAPPING Enable dynamic expansion.
  • 26.
    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'], });
  • 27.
    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,
  • 28.
  • 29.
  • 30.
    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.
  • 31.
    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.
  • 32.
  • 33.
    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?
  • 34.
    foo: [1, 2,3] bar: 'hello world’ baz: false What would happen if we ran grunt log?
  • 35.
    foo: [1, 2,3] bar: 'hello world’ baz: false What would happen if we ran grunt log?
  • 36.
    When a basictask 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
  • 37.
    grunt.registerTask('foo', 'A sampletask 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.
  • 38.
    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.
  • 39.
    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 TASKS Example of an asynchronous Task
  • 40.
    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 TASKS Example of an asynchronous Task
  • 41.
    Cool parts oftasks 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)
  • 42.
    Cool parts oftasks 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.'); });
  • 43.
    Tasks can accessconfiguration 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
  • 44.
    LET’S DIG INTOAN EXAMPLE
  • 45.
  • 46.
    SETUP npm install -ggrunt-cli npm install -g bower https://nodejs.org/download/ Install Node Install Grunt CLI and Bower
  • 47.
    DEMO APP https://github.com/spencer48/Grunt-Demo Fork thison Git Hub then… git clone https://github.com/YOURUSERNAME/Grunt-Demo
  • 48.
  • 49.
  • 50.
    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', [ ]); };
  • 51.
    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.
  • 52.
    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
  • 53.
    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) ]; } } }, }
  • 54.
    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}' ] } }
  • 55.
    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
  • 56.
    CONFIGURING CLEAN clean: { dist:{ files: [{ dot: true, src: [ '.tmp', '<%= yeoman.dist %>/{,*/}*', '!<%= yeoman.dist %>/.git{,*/}*' ] }] }, server: '.tmp' } GruntFile.js
  • 57.
    CONFIGURING WIREDEP wiredep: { app:{ src: ['<%= yeoman.app %>/index.html'], ignorePath: /..// }, test: { devDependencies: true, src: '<%= karma.unit.configFile %>', ignorePath: /..//, fileTypes:{ js: { block: /(([st]*)/{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
  • 58.
    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 = 10n' }, dist: { options: { generatedImagesDir: '<%= yeoman.dist %>/images/generated' } }, server: { options: { sourcemap: true } } } GruntFile.js
  • 59.
    CONFIGURING CLEAN clean: { dist:{ files: [{ dot: true, src: [ '.tmp', '<%= yeoman.dist %>/{,*/}*', '!<%= yeoman.dist %>/.git{,*/}*' ] }] }, server: '.tmp' } GruntFile.js
  • 60.
    CONFIGURING AUTOPREFIXER // Addvendor 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
  • 61.
    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
  • 62.
    CONFIGURING USEMIN useminPrepare: { html:'<%= yeoman.app %>/index.html', options: { dest: '<%= yeoman.dist %>', flow: { html: { steps: { js: ['concat', 'uglifyjs'], css: ['cssmin'] }, post: {} } } } }, GruntFile.js
  • 63.
    CONFIGURING USEMIN // Performsrewrites 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
  • 64.
    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
  • 65.
    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
  • 66.
    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
  • 67.
    CONFIGURING NGANNOTATE ngAnnotate: { dist:{ files: [{ expand: true, cwd: '.tmp/concat/scripts', src: '*.js', dest: '.tmp/concat/scripts' }] } } GruntFile.js
  • 68.
    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 :(
  • 69.
    CONFIGURING KARMA karma: { unit:{ configFile: 'test/karma.conf.js', singleRun: true } } GruntFile.js
  • 70.
    CONFIGURING CONCURRENT // Runsome tasks in parallel to speed up the build process concurrent: { server: [ 'compass:server' ], test: [ 'compass' ], dist: [ 'compass:dist', 'imagemin', 'svgmin' ] }, GruntFile.js
  • 71.
  • 72.
    TASK EXAMPLES grunt.registerTask('serve', 'Compilethen 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
  • 73.
  • 74.
    QUESTIONS? S P EN C E R H A N D L E Y Mastering Grunt VIDEO SERIES @spencer414 www.spencerhand.ly www.podclear.com