Grunt Cheatsheet¶
Grunt - The JavaScript Task Runner
Grunt is a JavaScript task runner that automates repetitive tasks like minification, compilation, unit testing, and linting. It uses a configuration-over-code approach with a vast ecosystem of plugins to handle virtually any task.
Table of Contents¶
- Installation
- Getting Started
- Gruntfile Configuration
- Core Tasks
- File Processing
- CSS Tasks
- JavaScript Tasks
- Image Processing
- HTML Tasks
- Development Server
- Watch Tasks
- Build Pipeline
- Plugin Ecosystem
- Custom Tasks
- Multi-target Tasks
- Template Processing
- Testing Integration
- Deployment
- Performance Optimization
- Best Practices
Installation¶
Global Installation¶
Local Installation¶
# Initialize npm project
npm init -y
# Install Grunt locally
npm install --save-dev grunt
# Install common plugins
npm install --save-dev grunt-contrib-uglify grunt-contrib-cssmin grunt-contrib-concat grunt-contrib-watch
Project Setup¶
# Create project structure
mkdir my-grunt-project
cd my-grunt-project
# Initialize package.json
npm init -y
# Install Grunt and essential plugins
npm install --save-dev grunt grunt-contrib-uglify grunt-contrib-cssmin grunt-contrib-concat grunt-contrib-copy grunt-contrib-clean grunt-contrib-watch grunt-contrib-connect
# Create Gruntfile
touch Gruntfile.js
# Create source directories
mkdir -p src/{css,js,images,html}
mkdir dist
Package.json Configuration¶
{
"name": "my-grunt-project",
"version": "1.0.0",
"description": "A Grunt-powered project",
"scripts": {
"build": "grunt build",
"dev": "grunt dev",
"watch": "grunt watch",
"serve": "grunt serve",
"test": "grunt test"
},
"devDependencies": {
"grunt": "^1.6.1",
"grunt-contrib-uglify": "^5.2.2",
"grunt-contrib-cssmin": "^4.0.0",
"grunt-contrib-concat": "^2.1.0",
"grunt-contrib-copy": "^1.0.0",
"grunt-contrib-clean": "^2.0.1",
"grunt-contrib-watch": "^1.1.0",
"grunt-contrib-connect": "^3.0.0",
"grunt-contrib-jshint": "^3.2.0",
"grunt-sass": "^3.1.0"
}
}
Getting Started¶
Basic Gruntfile¶
// Gruntfile.js
module.exports = function(grunt) {
// Project configuration
grunt.initConfig({
pkg: grunt.file.readJSON('package.json'),
// Concatenate files
concat: {
js: {
src: ['src/js/**/*.js'],
dest: 'dist/js/bundle.js'
},
css: {
src: ['src/css/**/*.css'],
dest: 'dist/css/styles.css'
}
},
// Minify JavaScript
uglify: {
options: {
banner: '/*! <%= pkg.name %> <%= grunt.template.today("yyyy-mm-dd") %> */\n'
},
build: {
src: 'dist/js/bundle.js',
dest: 'dist/js/bundle.min.js'
}
},
// Minify CSS
cssmin: {
target: {
files: {
'dist/css/styles.min.css': ['dist/css/styles.css']
}
}
},
// Watch for changes
watch: {
js: {
files: ['src/js/**/*.js'],
tasks: ['concat:js', 'uglify']
},
css: {
files: ['src/css/**/*.css'],
tasks: ['concat:css', 'cssmin']
}
}
});
// Load plugins
grunt.loadNpmTasks('grunt-contrib-concat');
grunt.loadNpmTasks('grunt-contrib-uglify');
grunt.loadNpmTasks('grunt-contrib-cssmin');
grunt.loadNpmTasks('grunt-contrib-watch');
// Register tasks
grunt.registerTask('default', ['concat', 'uglify', 'cssmin']);
grunt.registerTask('build', ['concat', 'uglify', 'cssmin']);
grunt.registerTask('dev', ['build', 'watch']);
};
Running Tasks¶
# Run default task
grunt
# Run specific task
grunt uglify
grunt cssmin
# Run multiple tasks
grunt concat uglify
# Run with options
grunt build --force
grunt watch --verbose
# List available tasks
grunt --help
Project Structure¶
my-grunt-project/
├── src/
│ ├── css/
│ │ ├── main.css
│ │ └── components/
│ ├── js/
│ │ ├── main.js
│ │ └── modules/
│ ├── images/
│ └── html/
├── dist/
│ ├── css/
│ ├── js/
│ ├── images/
│ └── index.html
├── Gruntfile.js
└── package.json
Gruntfile Configuration¶
Configuration Structure¶
module.exports = function(grunt) {
grunt.initConfig({
// Package information
pkg: grunt.file.readJSON('package.json'),
// Global options
globalConfig: {
src: 'src',
dist: 'dist'
},
// Task configurations
taskName: {
options: {
// Global options for this task
},
target1: {
options: {
// Target-specific options
},
src: 'source/files',
dest: 'destination/file'
},
target2: {
// Another target configuration
}
}
});
};
Advanced Configuration¶
module.exports = function(grunt) {
// Configuration
grunt.initConfig({
pkg: grunt.file.readJSON('package.json'),
// Global configuration
config: {
src: 'src',
dist: 'dist',
tmp: '.tmp'
},
// Clean task
clean: {
dist: ['<%= config.dist %>'],
tmp: ['<%= config.tmp %>']
},
// Copy task
copy: {
html: {
expand: true,
cwd: '<%= config.src %>',
src: '**/*.html',
dest: '<%= config.dist %>'
},
images: {
expand: true,
cwd: '<%= config.src %>/images',
src: '**/*',
dest: '<%= config.dist %>/images'
}
},
// Sass compilation
sass: {
options: {
implementation: require('sass'),
sourceMap: true
},
dist: {
files: {
'<%= config.dist %>/css/main.css': '<%= config.src %>/scss/main.scss'
}
}
},
// PostCSS processing
postcss: {
options: {
processors: [
require('autoprefixer')({browsers: 'last 2 versions'}),
require('cssnano')()
]
},
dist: {
src: '<%= config.dist %>/css/*.css'
}
}
});
// Load plugins
require('load-grunt-tasks')(grunt);
// Custom tasks
grunt.registerTask('build', [
'clean:dist',
'copy',
'sass',
'postcss',
'uglify',
'cssmin'
]);
grunt.registerTask('default', ['build']);
};
Template Processing¶
grunt.initConfig({
pkg: grunt.file.readJSON('package.json'),
// Using templates
concat: {
options: {
banner: '/*! <%= pkg.name %> - v<%= pkg.version %> - ' +
'<%= grunt.template.today("yyyy-mm-dd") %> */\n'
},
dist: {
src: ['src/js/**/*.js'],
dest: 'dist/js/<%= pkg.name %>.js'
}
},
// Dynamic file mapping
uglify: {
dynamic: {
files: [{
expand: true,
cwd: 'src/js/',
src: '**/*.js',
dest: 'dist/js/',
ext: '.min.js'
}]
}
}
});
Core Tasks¶
File Operations¶
// Clean files and directories
clean: {
build: ['dist/'],
tmp: ['.tmp/'],
css: ['dist/css/**/*.css'],
js: ['dist/js/**/*.js']
}
// Copy files
copy: {
main: {
files: [
// Copy all files from src to dist
{
expand: true,
cwd: 'src/',
src: ['**'],
dest: 'dist/'
},
// Copy specific files
{
expand: true,
cwd: 'src/assets/',
src: ['fonts/**', 'images/**'],
dest: 'dist/assets/'
}
]
},
// Copy with renaming
rename: {
files: [{
expand: true,
cwd: 'src/',
src: ['**/*.txt'],
dest: 'dist/',
ext: '.bak'
}]
}
}
// Concatenate files
concat: {
options: {
separator: ';',
stripBanners: true,
banner: '/*! <%= pkg.name %> - v<%= pkg.version %> */\n'
},
js: {
src: [
'src/js/vendor/*.js',
'src/js/modules/*.js',
'src/js/main.js'
],
dest: 'dist/js/bundle.js'
},
css: {
src: ['src/css/**/*.css'],
dest: 'dist/css/styles.css'
}
}
File Filtering¶
// File filtering and processing
copy: {
filterFiles: {
files: [{
expand: true,
cwd: 'src/',
src: ['**/*'],
dest: 'dist/',
filter: function(filepath) {
// Only copy files modified in last 24 hours
return (Date.now() - fs.statSync(filepath).mtime) < 24*60*60*1000;
}
}]
}
}
// Conditional file processing
uglify: {
conditional: {
files: [{
expand: true,
cwd: 'src/js/',
src: ['**/*.js'],
dest: 'dist/js/',
ext: '.min.js',
filter: function(filepath) {
// Only minify files larger than 1KB
return fs.statSync(filepath).size > 1024;
}
}]
}
}
CSS Tasks¶
Sass Compilation¶
// Sass compilation
sass: {
options: {
implementation: require('sass'),
sourceMap: true,
outputStyle: 'expanded'
},
// Development build
dev: {
options: {
sourceMap: true,
outputStyle: 'expanded'
},
files: {
'dist/css/main.css': 'src/scss/main.scss'
}
},
// Production build
prod: {
options: {
sourceMap: false,
outputStyle: 'compressed'
},
files: {
'dist/css/main.min.css': 'src/scss/main.scss'
}
},
// Multiple files
multiple: {
files: [{
expand: true,
cwd: 'src/scss/',
src: ['*.scss'],
dest: 'dist/css/',
ext: '.css'
}]
}
}
// Less compilation
less: {
development: {
options: {
paths: ['src/less/includes']
},
files: {
'dist/css/main.css': 'src/less/main.less'
}
},
production: {
options: {
paths: ['src/less/includes'],
cleancss: true,
modifyVars: {
imgPath: '"http://mycdn.com/path/to/images"',
bgColor: 'red'
}
},
files: {
'dist/css/main.min.css': 'src/less/main.less'
}
}
}
CSS Processing¶
// CSS minification
cssmin: {
options: {
mergeIntoShorthands: false,
roundingPrecision: -1
},
target: {
files: {
'dist/css/main.min.css': ['src/css/**/*.css']
}
},
// Multiple targets
multiple: {
files: [{
expand: true,
cwd: 'src/css/',
src: ['*.css', '!*.min.css'],
dest: 'dist/css/',
ext: '.min.css'
}]
}
}
// Autoprefixer
autoprefixer: {
options: {
browsers: ['last 2 versions', 'ie 8', 'ie 9']
},
single_file: {
src: 'dist/css/main.css',
dest: 'dist/css/main.prefixed.css'
},
multiple_files: {
expand: true,
flatten: true,
src: 'dist/css/*.css',
dest: 'dist/css/prefixed/'
}
}
// PostCSS
postcss: {
options: {
map: true,
processors: [
require('pixrem')(),
require('autoprefixer')({browsers: 'last 2 versions'}),
require('cssnano')()
]
},
dist: {
src: 'dist/css/*.css'
}
}
CSS Linting¶
// CSS Lint
csslint: {
options: {
csslintrc: '.csslintrc'
},
strict: {
options: {
import: 2
},
src: ['src/css/**/*.css']
},
lax: {
options: {
import: false
},
src: ['src/css/**/*.css']
}
}
// Stylelint
stylelint: {
all: ['src/css/**/*.css'],
options: {
configFile: '.stylelintrc',
formatter: 'string',
ignoreDisables: false,
failOnError: true,
outputFile: '',
reportNeedlessDisables: false,
syntax: ''
}
}
JavaScript Tasks¶
JavaScript Minification¶
// UglifyJS
uglify: {
options: {
banner: '/*! <%= pkg.name %> <%= grunt.template.today("yyyy-mm-dd") %> */\n',
mangle: {
reserved: ['jQuery', '$']
},
compress: {
drop_console: true
}
},
// Single file
single: {
src: 'src/js/main.js',
dest: 'dist/js/main.min.js'
},
// Multiple files
multiple: {
files: [{
expand: true,
cwd: 'src/js/',
src: '**/*.js',
dest: 'dist/js/',
ext: '.min.js'
}]
},
// With source maps
sourcemap: {
options: {
sourceMap: true,
sourceMapName: 'dist/js/main.min.js.map'
},
src: 'src/js/main.js',
dest: 'dist/js/main.min.js'
}
}
// Terser (ES6+ minification)
terser: {
options: {
ecma: 2015,
compress: {
drop_console: true
},
output: {
comments: false
}
},
main: {
files: {
'dist/js/main.min.js': ['src/js/main.js']
}
}
}
JavaScript Transpilation¶
// Babel transpilation
babel: {
options: {
sourceMap: true,
presets: ['@babel/preset-env']
},
dist: {
files: [{
expand: true,
cwd: 'src/js/',
src: ['**/*.js'],
dest: 'dist/js/',
ext: '.js'
}]
}
}
// TypeScript compilation
typescript: {
base: {
src: ['src/ts/**/*.ts'],
dest: 'dist/js/app.js',
options: {
module: 'amd',
target: 'es5',
sourceMap: true,
declaration: true
}
}
}
// CoffeeScript compilation
coffee: {
compile: {
files: {
'dist/js/app.js': 'src/coffee/app.coffee'
}
},
multiple: {
expand: true,
flatten: true,
cwd: 'src/coffee/',
src: ['*.coffee'],
dest: 'dist/js/',
ext: '.js'
}
}
JavaScript Linting¶
// JSHint
jshint: {
options: {
curly: true,
eqeqeq: true,
eqnull: true,
browser: true,
globals: {
jQuery: true
}
},
uses_defaults: ['src/js/**/*.js'],
with_overrides: {
options: {
curly: false,
undef: true
},
files: {
src: ['src/js/specific.js']
}
}
}
// ESLint
eslint: {
options: {
configFile: '.eslintrc.js',
rulePaths: ['conf/rules']
},
target: ['src/js/**/*.js']
}
// JSLint
jslint: {
client: {
src: ['src/js/**/*.js'],
directives: {
browser: true,
predef: ['jQuery']
}
}
}
Browserify Integration¶
browserify: {
dist: {
files: {
'dist/js/bundle.js': ['src/js/main.js']
},
options: {
transform: [['babelify', {presets: ['@babel/preset-env']}]]
}
},
// With external libraries
vendor: {
src: [],
dest: 'dist/js/vendor.js',
options: {
require: ['jquery', 'underscore']
}
},
app: {
src: ['src/js/main.js'],
dest: 'dist/js/app.js',
options: {
external: ['jquery', 'underscore']
}
}
}
// Webpack integration
webpack: {
options: {
entry: './src/js/main.js',
output: {
path: 'dist/js/',
filename: 'bundle.js'
},
module: {
rules: [{
test: /\.js$/,
use: {
loader: 'babel-loader',
options: {
presets: ['@babel/preset-env']
}
}
}]
}
},
dev: {
mode: 'development',
devtool: 'source-map'
},
prod: {
mode: 'production'
}
}
Image Processing¶
Image Optimization¶
// Image minification
imagemin: {
dynamic: {
files: [{
expand: true,
cwd: 'src/images/',
src: ['**/*.{png,jpg,gif,svg}'],
dest: 'dist/images/'
}]
},
// With options
options: {
optimizationLevel: 3,
svgoPlugins: [{removeViewBox: false}],
use: [mozjpeg({quality: 80})]
}
}
// Responsive images
responsive_images: {
dev: {
options: {
engine: 'im',
sizes: [{
width: 320,
suffix: '_small',
quality: 80
}, {
width: 640,
suffix: '_medium',
quality: 80
}, {
width: 1024,
suffix: '_large',
quality: 80
}]
},
files: [{
expand: true,
src: ['src/images/**/*.{jpg,gif,png}'],
cwd: 'src/',
dest: 'dist/'
}]
}
}
// WebP conversion
webp: {
files: {
expand: true,
cwd: 'src/images/',
src: ['**/*.{png,jpg,jpeg}'],
dest: 'dist/images/webp/',
ext: '.webp'
},
options: {
binpath: require('webp-bin'),
preset: 'photo',
verbose: true,
quality: 80,
alphaQuality: 80,
compressionMethod: 6,
segments: 4,
psnr: 42,
sns: 50,
filterStrength: 40,
filterSharpness: 3,
simpleFilter: true,
partitionLimit: 50,
analysisPass: 6,
multiThreading: true,
lowMemory: false,
alphaMethod: 0,
alphaFilter: 'best',
alphaCleanup: true,
noAlpha: false,
lossless: false
}
}
Sprite Generation¶
// CSS Sprites
sprite: {
all: {
src: 'src/images/icons/*.png',
dest: 'dist/images/spritesheet.png',
destCss: 'dist/css/sprites.css',
cssFormat: 'css',
algorithm: 'binary-tree',
padding: 2
}
}
// SVG Sprites
svgstore: {
options: {
prefix: 'icon-',
svg: {
viewBox: '0 0 100 100',
xmlns: 'http://www.w3.org/2000/svg'
}
},
default: {
files: {
'dist/images/icons.svg': ['src/images/icons/*.svg']
}
}
}
// Icon fonts
webfont: {
icons: {
src: 'src/images/icons/*.svg',
dest: 'dist/fonts/',
destCss: 'dist/css/',
options: {
font: 'icons',
types: 'eot,woff,woff2,ttf,svg',
syntax: 'bem',
engine: 'node',
autoHint: false,
execMaxBuffer: 1024 * 200,
htmlDemo: true,
destHtml: 'dist/'
}
}
}
HTML Tasks¶
HTML Processing¶
// HTML minification
htmlmin: {
dist: {
options: {
removeComments: true,
collapseWhitespace: true,
removeRedundantAttributes: true,
removeScriptTypeAttributes: true,
removeStyleLinkTypeAttributes: true,
minifyJS: true,
minifyCSS: true
},
files: [{
expand: true,
cwd: 'src/',
src: ['**/*.html'],
dest: 'dist/'
}]
}
}
// HTML includes
includes: {
files: {
src: ['src/pages/*.html'],
dest: 'dist/',
flatten: true,
cwd: '.',
options: {
silent: true,
includePath: 'src/includes/'
}
}
}
// Template processing
template: {
dev: {
options: {
data: {
title: 'Development Site',
version: '<%= pkg.version %>',
timestamp: '<%= grunt.template.today() %>'
}
},
files: [{
expand: true,
cwd: 'src/templates/',
src: ['**/*.html'],
dest: 'dist/'
}]
}
}
HTML Validation¶
// HTML validation
htmlhint: {
build: {
options: {
'tag-pair': true,
'tagname-lowercase': true,
'attr-lowercase': true,
'attr-value-double-quotes': true,
'doctype-first': true,
'spec-char-escape': true,
'id-unique': true,
'head-script-disabled': true,
'style-disabled': true
},
src: ['dist/**/*.html']
}
}
// W3C validation
validation: {
options: {
reset: grunt.option('reset') || false,
stoponerror: false,
remotePath: 'http://localhost:9001/',
remoteFiles: ['index.html', 'about.html'],
relaxerror: ['Bad value X-UA-Compatible for attribute http-equiv on element meta.']
},
files: {
src: ['dist/**/*.html']
}
}
Development Server¶
Connect Server¶
// Basic server
connect: {
server: {
options: {
port: 9000,
hostname: 'localhost',
base: 'dist',
open: true
}
}
}
// Advanced server configuration
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.static('src')
];
}
}
},
test: {
options: {
port: 9001,
middleware: function(connect) {
return [
connect.static('.tmp'),
connect.static('test'),
connect().use('/bower_components', connect.static('./bower_components')),
connect.static('src')
];
}
}
},
dist: {
options: {
open: true,
base: 'dist'
}
}
}
// Proxy server
connect: {
proxies: [{
context: '/api',
host: 'localhost',
port: 3000,
https: false,
xforward: false,
headers: {
'x-custom-added-header': 'value'
},
hideHeaders: ['x-removed-header']
}],
server: {
options: {
port: 9000,
middleware: function(connect, options) {
var middlewares = [];
var directory = options.directory || options.base[options.base.length - 1];
// Setup the proxy
middlewares.push(require('grunt-connect-proxy/lib/utils').proxyRequest);
// Serve static files
if (!Array.isArray(options.base)) {
options.base = [options.base];
}
options.base.forEach(function(base) {
middlewares.push(connect.static(base));
});
// Make directory browse-able
middlewares.push(connect.directory(directory));
return middlewares;
}
}
}
}
Live Reload¶
// Live reload configuration
watch: {
options: {
livereload: true
},
html: {
files: ['src/**/*.html'],
tasks: ['includes', 'htmlmin']
},
css: {
files: ['src/css/**/*.css'],
tasks: ['concat:css', 'cssmin']
},
js: {
files: ['src/js/**/*.js'],
tasks: ['jshint', 'concat:js', 'uglify']
},
livereload: {
options: {
livereload: '<%= connect.options.livereload %>'
},
files: [
'dist/**/*.html',
'dist/css/**/*.css',
'dist/js/**/*.js',
'dist/images/**/*.{png,jpg,jpeg,gif,webp,svg}'
]
}
}
// Browser Sync integration
browserSync: {
dev: {
bsFiles: {
src: [
'dist/css/*.css',
'dist/js/*.js',
'dist/*.html'
]
},
options: {
watchTask: true,
server: './dist'
}
}
}
Watch Tasks¶
Basic Watch Configuration¶
watch: {
// Watch CSS files
css: {
files: ['src/css/**/*.css'],
tasks: ['concat:css', 'cssmin'],
options: {
spawn: false
}
},
// Watch JavaScript files
js: {
files: ['src/js/**/*.js'],
tasks: ['jshint', 'concat:js', 'uglify'],
options: {
spawn: false
}
},
// Watch Sass files
sass: {
files: ['src/scss/**/*.scss'],
tasks: ['sass', 'autoprefixer', 'cssmin']
},
// Watch HTML files
html: {
files: ['src/**/*.html'],
tasks: ['includes', 'htmlmin']
},
// Watch images
images: {
files: ['src/images/**/*.{png,jpg,gif,svg}'],
tasks: ['imagemin']
}
}
Advanced Watch Patterns¶
watch: {
options: {
spawn: false,
livereload: true
},
// Conditional tasks based on file changes
configFiles: {
files: ['Gruntfile.js', 'package.json'],
options: {
reload: true
}
},
// Watch with different intervals
slow: {
files: ['src/large-files/**/*'],
tasks: ['process-large-files'],
options: {
interval: 5000
}
},
// Watch with custom event handling
custom: {
files: ['src/special/**/*'],
tasks: [],
options: {
event: ['added', 'deleted']
}
}
}
// Dynamic watch configuration
grunt.event.on('watch', function(action, filepath, target) {
if (target === 'js') {
// Only process the changed file
grunt.config('jshint.single.src', filepath);
grunt.config('uglify.single.src', filepath);
grunt.config('uglify.single.dest', filepath.replace('src/', 'dist/').replace('.js', '.min.js'));
}
});
Watch with Error Handling¶
watch: {
options: {
spawn: false,
interrupt: true
},
sass: {
files: ['src/scss/**/*.scss'],
tasks: ['sass'],
options: {
spawn: false,
interrupt: true,
event: ['added', 'deleted', 'changed']
}
}
}
// Error handling in tasks
grunt.registerTask('safe-sass', function() {
try {
grunt.task.run('sass');
} catch (e) {
grunt.log.error('Sass compilation failed: ' + e.message);
grunt.fail.warn('Sass task failed, but continuing...');
}
});
Build Pipeline¶
Development Build¶
// Development build pipeline
grunt.registerTask('dev', [
'clean:dist',
'jshint',
'sass:dev',
'autoprefixer',
'concat',
'copy:dev',
'includes',
'connect:livereload',
'watch'
]);
// Development build without server
grunt.registerTask('build-dev', [
'clean:dist',
'jshint',
'sass:dev',
'autoprefixer',
'concat',
'copy:dev',
'includes'
]);
Production Build¶
// Production build pipeline
grunt.registerTask('build', [
'clean:dist',
'jshint',
'sass:prod',
'autoprefixer',
'cssmin',
'concat',
'uglify',
'imagemin',
'copy:prod',
'includes',
'htmlmin',
'rev',
'usemin'
]);
// Build with testing
grunt.registerTask('test-build', [
'build',
'connect:test',
'qunit'
]);
// Complete production pipeline
grunt.registerTask('production', [
'build',
'test-build',
'compress',
'deploy'
]);
Multi-Environment Builds¶
// Environment-specific configurations
grunt.initConfig({
env: {
dev: {
NODE_ENV: 'development'
},
prod: {
NODE_ENV: 'production'
}
},
// Conditional configuration
uglify: {
options: {
compress: {
drop_console: '<%= process.env.NODE_ENV === "production" %>'
}
},
build: {
src: 'dist/js/bundle.js',
dest: 'dist/js/bundle.min.js'
}
}
});
// Environment-specific tasks
grunt.registerTask('build-dev', ['env:dev', 'build-common']);
grunt.registerTask('build-prod', ['env:prod', 'build-common']);
grunt.registerTask('build-common', [
'clean',
'sass',
'concat',
'uglify',
'copy'
]);
Plugin Ecosystem¶
Essential Plugins¶
# File operations
npm install --save-dev grunt-contrib-clean grunt-contrib-copy grunt-contrib-concat
# CSS processing
npm install --save-dev grunt-sass grunt-contrib-cssmin grunt-autoprefixer
# JavaScript processing
npm install --save-dev grunt-contrib-uglify grunt-contrib-jshint grunt-babel
# HTML processing
npm install --save-dev grunt-contrib-htmlmin grunt-includes
# Image processing
npm install --save-dev grunt-contrib-imagemin grunt-responsive-images
# Development
npm install --save-dev grunt-contrib-watch grunt-contrib-connect grunt-browser-sync
# Build tools
npm install --save-dev grunt-usemin grunt-rev grunt-filerev
Advanced Plugins¶
// File revisioning
rev: {
files: {
src: ['dist/js/**/*.js', 'dist/css/**/*.css']
}
}
// Asset injection
usemin: {
html: ['dist/**/*.html'],
options: {
assetsDirs: ['dist', 'dist/images']
}
}
// Compression
compress: {
main: {
options: {
archive: 'dist.zip'
},
files: [{
expand: true,
cwd: 'dist/',
src: ['**/*'],
dest: '.'
}]
}
}
// FTP deployment
'ftp-deploy': {
build: {
auth: {
host: 'server.com',
port: 21,
authKey: 'key1'
},
src: 'dist',
dest: '/public_html',
exclusions: ['dist/**/.DS_Store', 'dist/**/Thumbs.db']
}
}
Plugin Loading¶
// Load all grunt plugins
require('load-grunt-tasks')(grunt);
// Load specific plugins
grunt.loadNpmTasks('grunt-contrib-uglify');
grunt.loadNpmTasks('grunt-contrib-cssmin');
// Load plugins with pattern
require('load-grunt-tasks')(grunt, {
pattern: ['grunt-*', '!grunt-legacy-util']
});
// Load plugins from different locations
grunt.loadTasks('custom-tasks');
Custom Tasks¶
Basic Custom Tasks¶
// Simple custom task
grunt.registerTask('hello', 'Say hello', function() {
grunt.log.writeln('Hello, World!');
});
// Task with parameters
grunt.registerTask('greet', 'Greet someone', function(name) {
if (!name) {
grunt.log.writeln('Hello, stranger!');
} else {
grunt.log.writeln('Hello, ' + name + '!');
}
});
// Multi-task
grunt.registerMultiTask('log', 'Log stuff', function() {
grunt.log.writeln(this.target + ': ' + this.data);
});
grunt.initConfig({
log: {
foo: 'Hello',
bar: 'World'
}
});
Advanced Custom Tasks¶
// Async task
grunt.registerTask('async-task', 'An async task', function() {
var done = this.async();
setTimeout(function() {
grunt.log.writeln('Async task completed!');
done();
}, 1000);
});
// Task with options
grunt.registerTask('custom-build', function() {
var options = this.options({
minify: true,
sourcemap: false
});
if (options.minify) {
grunt.task.run('uglify');
}
if (options.sourcemap) {
grunt.task.run('sourcemap');
}
});
// File processing task
grunt.registerMultiTask('process-files', function() {
var options = this.options({
separator: '\n'
});
this.files.forEach(function(file) {
var src = file.src.filter(function(filepath) {
if (!grunt.file.exists(filepath)) {
grunt.log.warn('Source file "' + filepath + '" not found.');
return false;
} else {
return true;
}
}).map(function(filepath) {
return grunt.file.read(filepath);
}).join(options.separator);
grunt.file.write(file.dest, src);
grunt.log.writeln('File "' + file.dest + '" created.');
});
});
Task Dependencies¶
// Task with dependencies
grunt.registerTask('build-all', [
'clean',
'jshint',
'sass',
'concat',
'uglify',
'cssmin'
]);
// Conditional task execution
grunt.registerTask('conditional-build', function() {
if (grunt.option('production')) {
grunt.task.run(['build-prod']);
} else {
grunt.task.run(['build-dev']);
}
});
// Dynamic task creation
grunt.registerTask('create-tasks', function() {
var files = grunt.file.expand('src/modules/*.js');
files.forEach(function(file) {
var taskName = 'process-' + path.basename(file, '.js');
grunt.registerTask(taskName, function() {
// Process individual file
grunt.log.writeln('Processing: ' + file);
});
});
});
Multi-target Tasks¶
Configuration¶
grunt.initConfig({
// Multi-target task configuration
uglify: {
options: {
banner: '/*! Global banner */\n'
},
// Target 1: Main application
app: {
options: {
banner: '/*! App banner */\n'
},
src: 'src/js/app.js',
dest: 'dist/js/app.min.js'
},
// Target 2: Vendor libraries
vendor: {
src: ['src/js/vendor/*.js'],
dest: 'dist/js/vendor.min.js'
},
// Target 3: Dynamic files
dynamic: {
files: [{
expand: true,
cwd: 'src/js/',
src: '**/*.js',
dest: 'dist/js/',
ext: '.min.js'
}]
}
}
});
// Run specific targets
// grunt uglify:app
// grunt uglify:vendor
// grunt uglify (runs all targets)
Custom Multi-target Tasks¶
grunt.registerMultiTask('custom-process', 'Process files', function() {
var options = this.options({
prefix: '',
suffix: ''
});
grunt.log.writeln('Processing target: ' + this.target);
this.files.forEach(function(file) {
var contents = file.src.map(function(src) {
return grunt.file.read(src);
}).join('\n');
var processed = options.prefix + contents + options.suffix;
grunt.file.write(file.dest, processed);
grunt.log.writeln('Created: ' + file.dest);
});
});
grunt.initConfig({
'custom-process': {
options: {
prefix: '/* Global prefix */\n'
},
css: {
options: {
suffix: '\n/* CSS suffix */'
},
files: {
'dist/css/processed.css': ['src/css/**/*.css']
}
},
js: {
options: {
suffix: '\n/* JS suffix */'
},
files: {
'dist/js/processed.js': ['src/js/**/*.js']
}
}
}
});
Template Processing¶
Built-in Templates¶
grunt.initConfig({
pkg: grunt.file.readJSON('package.json'),
// Using package.json data
concat: {
options: {
banner: '/*! <%= pkg.name %> - v<%= pkg.version %> - ' +
'<%= grunt.template.today("yyyy-mm-dd") %> */\n'
},
dist: {
src: ['src/js/**/*.js'],
dest: 'dist/js/<%= pkg.name %>.js'
}
},
// Date templates
copy: {
timestamped: {
src: 'src/config.js',
dest: 'dist/config-<%= grunt.template.today("yyyymmdd") %>.js'
}
}
});
Custom Templates¶
// Custom template data
grunt.initConfig({
config: {
app: {
name: 'MyApp',
version: '1.0.0',
author: 'John Doe'
},
build: {
timestamp: '<%= grunt.template.today("isoDateTime") %>',
environment: process.env.NODE_ENV || 'development'
}
},
// Using custom data
replace: {
config: {
src: ['src/js/config.js'],
dest: 'dist/js/config.js',
replacements: [{
from: '{{APP_NAME}}',
to: '<%= config.app.name %>'
}, {
from: '{{VERSION}}',
to: '<%= config.app.version %>'
}, {
from: '{{BUILD_TIME}}',
to: '<%= config.build.timestamp %>'
}]
}
}
});
// Template processing task
grunt.registerTask('process-templates', function() {
var config = grunt.config('config');
grunt.file.expand('src/templates/**/*.html').forEach(function(file) {
var content = grunt.file.read(file);
var processed = grunt.template.process(content, {
data: config
});
var dest = file.replace('src/templates/', 'dist/');
grunt.file.write(dest, processed);
});
});
Testing Integration¶
Unit Testing¶
// QUnit
qunit: {
files: ['test/**/*.html']
}
// Mocha
mocha: {
test: {
src: ['test/**/*.html'],
options: {
run: true,
reporter: 'Spec'
}
}
}
// Jasmine
jasmine: {
src: 'src/js/**/*.js',
options: {
specs: 'test/spec/**/*.js',
helpers: 'test/helpers/**/*.js'
}
}
// Karma
karma: {
unit: {
configFile: 'karma.conf.js'
},
continuous: {
configFile: 'karma.conf.js',
singleRun: true,
browsers: ['PhantomJS']
}
}
Code Coverage¶
// Istanbul coverage
instrument: {
files: 'src/js/**/*.js',
options: {
lazy: true,
basePath: 'instrumented/'
}
}
storeCoverage: {
options: {
dir: 'coverage/'
}
}
makeReport: {
src: 'coverage/**/*.json',
options: {
type: 'lcov',
dir: 'coverage/',
print: 'detail'
}
}
// Coverage task
grunt.registerTask('coverage', [
'clean:coverage',
'instrument',
'qunit',
'storeCoverage',
'makeReport'
]);
End-to-End Testing¶
// Protractor
protractor: {
options: {
configFile: 'protractor.conf.js',
keepAlive: true,
noColor: false
},
e2e: {
options: {
args: {
suite: 'e2e'
}
}
}
}
// WebDriver
webdriver: {
chrome: {
tests: ['test/e2e/**/*.js'],
options: {
timeout: 10000,
reporter: 'spec',
browser: 'chrome'
}
}
}
// Nightwatch
nightwatch: {
options: {
standalone: true,
jar_version: '2.53.0',
jar_url: 'http://selenium-release.storage.googleapis.com/2.53/selenium-server-standalone-2.53.0.jar'
}
}
Deployment¶
Build and Deploy¶
// Build for deployment
grunt.registerTask('deploy-build', [
'clean',
'jshint',
'test',
'sass:prod',
'autoprefixer',
'cssmin',
'concat',
'uglify',
'imagemin',
'htmlmin',
'rev',
'usemin'
]);
// FTP deployment
'ftp-deploy': {
production: {
auth: {
host: 'ftp.example.com',
port: 21,
authKey: 'production'
},
src: 'dist/',
dest: '/public_html/',
exclusions: [
'dist/**/.DS_Store',
'dist/**/Thumbs.db',
'dist/tmp'
]
}
}
// SFTP deployment
sftp: {
production: {
files: {
'./': 'dist/**'
},
options: {
path: '/var/www/html/',
host: 'example.com',
username: 'deploy',
privateKey: grunt.file.read('deploy_key'),
showProgress: true
}
}
}
Git Deployment¶
// Git deployment
'gh-pages': {
options: {
base: 'dist'
},
src: ['**']
}
// Git push
gitpush: {
production: {
options: {
remote: 'production',
branch: 'master'
}
}
}
// Complete deployment pipeline
grunt.registerTask('deploy', [
'deploy-build',
'ftp-deploy:production'
]);
grunt.registerTask('deploy-github', [
'deploy-build',
'gh-pages'
]);
Docker Deployment¶
// Docker build
shell: {
docker_build: {
command: 'docker build -t myapp:latest .'
},
docker_push: {
command: 'docker push myapp:latest'
},
docker_deploy: {
command: 'docker run -d -p 80:80 myapp:latest'
}
}
// Docker deployment task
grunt.registerTask('deploy-docker', [
'deploy-build',
'shell:docker_build',
'shell:docker_push',
'shell:docker_deploy'
]);
Performance Optimization¶
Parallel Processing¶
// Concurrent task execution
concurrent: {
dev: ['sass', 'coffee', 'jshint'],
prod: ['cssmin', 'uglify', 'imagemin'],
options: {
logConcurrentOutput: true
}
}
// Use in build pipeline
grunt.registerTask('build-fast', [
'clean',
'concurrent:dev',
'concat',
'concurrent:prod'
]);
Incremental Builds¶
// Newer files only
newer: {
options: {
cache: '.grunt/newer'
}
}
// Use with tasks
grunt.registerTask('build-incremental', [
'newer:jshint',
'newer:sass',
'newer:imagemin'
]);
// Watch with newer
watch: {
js: {
files: ['src/js/**/*.js'],
tasks: ['newer:jshint', 'newer:uglify']
}
}
Caching¶
// Cache configuration
cache: {
options: {
cache: '.grunt/cache'
}
}
// Cached image processing
imagemin: {
dynamic: {
files: [{
expand: true,
cwd: 'src/images/',
src: ['**/*.{png,jpg,gif}'],
dest: 'dist/images/'
}]
},
options: {
cache: false // Disable cache for this task
}
}
Best Practices¶
Project Organization¶
// Organized Gruntfile structure
module.exports = function(grunt) {
// Time how long tasks take
require('time-grunt')(grunt);
// Load all grunt tasks matching the ['grunt-*', '@*/grunt-*'] patterns
require('load-grunt-tasks')(grunt);
// Configurable paths
var config = {
src: 'src',
dist: 'dist',
tmp: '.tmp'
};
grunt.initConfig({
// Reference the config
config: config,
// Load package.json
pkg: grunt.file.readJSON('package.json'),
// Task configurations
// ... (organized by type)
});
// Custom tasks
grunt.registerTask('serve', function(target) {
if (target === 'dist') {
return grunt.task.run(['build', 'connect:dist:keepalive']);
}
grunt.task.run([
'clean:server',
'concurrent:server',
'autoprefixer',
'connect:livereload',
'watch'
]);
});
grunt.registerTask('build', [
'clean:dist',
'useminPrepare',
'concurrent:dist',
'autoprefixer',
'concat',
'cssmin',
'uglify',
'copy:dist',
'rev',
'usemin',
'htmlmin'
]);
grunt.registerTask('default', [
'newer:jshint',
'test',
'build'
]);
};
Configuration Management¶
// External configuration
var config = require('./grunt.config.js');
grunt.initConfig(config);
// Environment-specific configs
var env = process.env.NODE_ENV || 'development';
var config = require('./config/' + env + '.js');
// Modular configuration
grunt.initConfig({
// Load task configs
sass: require('./grunt/sass.js'),
uglify: require('./grunt/uglify.js'),
watch: require('./grunt/watch.js')
});
Error Handling¶
// Graceful error handling
grunt.option('force', true); // Continue on errors
// Custom error handling
grunt.registerTask('safe-build', function() {
try {
grunt.task.run(['jshint', 'sass', 'uglify']);
} catch (e) {
grunt.log.error('Build failed: ' + e.message);
grunt.fail.warn('Build task failed, but continuing...');
}
});
// Conditional error handling
grunt.registerTask('conditional-fail', function() {
if (grunt.option('strict')) {
grunt.fail.fatal('Strict mode enabled, failing on any error');
}
});
Performance Best Practices¶
- Use concurrent tasks for independent operations
- Implement incremental builds with
grunt-newer
- Cache expensive operations when possible
- Optimize watch patterns to avoid unnecessary rebuilds
- Use
spawn: false
in watch tasks for faster rebuilds
Maintenance Best Practices¶
- Keep plugins updated regularly
- Use semantic versioning for your builds
- Document your tasks and configuration
- Implement proper error handling
- Use environment variables for sensitive data
- Create modular configuration files
- Monitor build performance with
time-grunt
Summary¶
Grunt is a powerful JavaScript task runner that excels at automating repetitive development tasks through configuration. Key features include:
- Configuration-driven: JSON-based configuration for easy setup
- Extensive Plugin Ecosystem: Thousands of plugins for every task
- Multi-target Tasks: Run different configurations for the same task
- Template System: Dynamic configuration with templates
- File Processing: Powerful file globbing and processing capabilities
- Watch and Live Reload: Automatic task execution on file changes
- Custom Tasks: Easy creation of custom tasks and workflows
- Mature Ecosystem: Well-established with extensive documentation
By leveraging Grunt's configuration-driven approach and vast plugin ecosystem, you can create robust, maintainable build processes that handle everything from simple file concatenation to complex deployment pipelines.