Skip to content

Instantly share code, notes, and snippets.

@marcamos
Last active January 16, 2018 19:51
Show Gist options
  • Star 13 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • Save marcamos/7436015 to your computer and use it in GitHub Desktop.
Save marcamos/7436015 to your computer and use it in GitHub Desktop.
Learning Grunt by writing a verbose Gruntfile that replaces (and, goes beyond) what we're used to with CodeKit.
module.exports = function(grunt) {
"use strict";
// -------------------------------------------------------------------------
// #### Load plugins as needed ####
// Using a 'just in time' approach -- meaning: only load plugins when they
// are needed -- this will automatically find, then load, any and all
// plugins that are needed by the task currently being executed. It will
// scan the devDependencies object, in package.json, and match any of the
// following patterns automatically:
// - grunt-contrib-[task name]
// - grunt-[task name]
// - [task name]
// For any [task name] / [plugin name] pair that can't be found by the above
// patterns, you can find them manually by adding them as a second argument
// using the following pattern:
// - task name: 'plugin name'
// We've done that, below, with "replace: 'grunt-text-replace'", as it does
// not conform to the three patterns that are matched automatically.
// https://github.com/shootaroo/jit-grunt
// -------------------------------------------------------------------------
require('jit-grunt')(grunt, {
replace: 'grunt-text-replace'
});
// -------------------------------------------------------------------------
// #### Display task execution time ####
// This module will display elapsed execution time of grunt tasks when they
// are finished, right on the command line. It's useful for keeping tabs on
// the performance of your Grunt configuration.
// https://github.com/sindresorhus/time-grunt
// -------------------------------------------------------------------------
require('time-grunt')(grunt);
// -------------------------------------------------------------------------
// #### Project configuration ####
// -------------------------------------------------------------------------
grunt.initConfig({
// ---------------------------------------------------------------------
// #### Variables ####
// ---------------------------------------------------------------------
sassPath: 'html/lib/sass/',
cssPath: 'html/lib/css/',
jsPath: 'html/lib/js/',
imgPath: 'html/lib/img/',
sassFileTypes: 'sass,scss',
imgFileTypes: 'gif,jpg,jpeg,png',
docFileTypes: '.html,.php',
// ---------------------------------------------------------------------
// #### Get data from package.json ####
// Get data from the package.json file and assign it to a pkg variable.
// ---------------------------------------------------------------------
pkg: grunt.file.readJSON('package.json'),
// ---------------------------------------------------------------------
// #### Build a reusable banner ####
// A banner variable that can be utilized later via "<%= banner %>".
// Property names (defined over in package.json) which are preceded by
// "pkg." will output the corresponding values for those properties.
// ---------------------------------------------------------------------
banner: '/*!\n' +
' * <%= pkg.name %> | version: <%= pkg.version %> | updated: <%= grunt.template.today("yyyy-mm-dd @ h:MM:ss TT") %>\n' +
' * Copyright (c) <%= grunt.template.today("yyyy") %> <%= pkg.owner.name %>\n' +
' */\n',
// ---------------------------------------------------------------------
// #### Task configuration ####
// ---------------------------------------------------------------------
// Task: Sass compiling.
// https://github.com/gruntjs/grunt-contrib-sass
sass: {
// Task-wide options.
options: {},
// Target: development.
dev: {
// Target-specific options.
options: {
style: "expanded",
lineNumbers: true
},
// Source file(s), in the order we want to compile them.
src: '<%= sassPath %>all.{<%= sassFileTypes %>}',
// Destination file.
dest: '<%= cssPath %>all.css'
},
// Target: production.
prod: {
// Target-specific options.
options: {
style: "compressed",
// Utilize the banner variable defined above.
banner: '<%= banner %>'
},
// Source file(s), in the order we want to compile them.
src: '<%= sassPath %>all.{<%= sassFileTypes %>}',
// Destination file.
dest: '<%= cssPath %>all.min.css'
}
},
// Task: JavaScript hinting.
// https://github.com/gruntjs/grunt-contrib-jshint
jshint: {
// Task-wide options.
options: {},
// Target: all.
all: {
// Target-specific options.
options: {},
// Source file(s), in the order we want to hint them.
src: [
'Gruntfile.js',
'<%= jsPath %>*.js',
'!<%= jsPath %>*.min.js', // ...but not minified files.
'!<%= jsPath %>all.js' // ...but not the concatenated file.
]
}
},
// Task: JavaScript file concatenation.
// https://github.com/gruntjs/grunt-contrib-concat
concat: {
// Task-wide options.
options: {},
// Target: all.
all: {
// Target-specific options.
options: {
// Separate each concatenated script with a semicolon.
separator: ';'
},
// Source file(s), in the order we want to concatenate them.
src: [
'<%= jsPath %>vendor/jquery-1.10.2.js',
'<%= jsPath %>plugins/*.js',
'<%= jsPath %>*.js',
'!<%= jsPath %>*.min.js', // ...but not minified files.
'!<%= jsPath %>all.js' // ...but not the concatenated file.
],
// Destination file.
dest: '<%= jsPath %>all.js',
// Warn if a given file is missing or invalid.
nonull: true
}
},
// Task: JavaScript minification.
// https://github.com/gruntjs/grunt-contrib-uglify
uglify: {
// Task-wide options.
options: {},
// Target: all.
all: {
// Target-specific options.
options: {
// Utilize the banner variable defined above.
banner: '<%= banner %>',
// Report the original vs. minified file-size.
report: 'min'
},
// Source file(s), in the order we want to minify them.
src: '<%= jsPath %>all.js',
// Destination file.
dest: '<%= jsPath %>all.min.js'
}
},
// Task: image minification.
// https://github.com/gruntjs/grunt-contrib-imagemin
imagemin: {
// Task-wide options.
options: {},
// Target: all.
all: {
// Target-specific options.
options: {},
// Files to minify.
files: [{
// Allow for a dynamically-built file-list.
expand: true,
// The directory to start this task within.
cwd: '<%= imgPath %>',
// Relative to the path defined in "cwd", the sub-
// directories and file type(s) to work on.
src: ['**/*.{<%= imgFileTypes %>}'],
// Destination location.
dest: '<%= imgPath %>'
}]
}
},
// Task: find and replace things in the project.
// https://github.com/yoniholmes/grunt-text-replace
replace: {
// Target: remove the Livereload script.
removeLivereload: {
// Scan all document file types inside of html/.
src: 'html/**/*{<%= docFileTypes %>}',
// Replacement(s) to make.
replacements: [{
// From: The full script element needed for Livereload to
// function, plus a line-return.
from: /<script src="\/\/localhost:35729\/livereload\.js"><\/script>\n/m,
// To: Nothing.
to: ''
}],
// Overwrite each file.
overwrite: true
},
// Target: add the Livereload script.
addLivereload: {
// Scan all document file types inside of html/.
src: 'html/**/*{<%= docFileTypes %>}',
// Replacement(s) to make.
replacements: [{
// From: a closing body tag.
from: /<\/body>/,
// To: The full script element needed for Livereload to
// function, a line-return, and the closing body tag.
to: '<script src="//localhost:35729/livereload.js"></script>\n</body>'
}],
// Overwrite each file.
overwrite: true
},
// Target: replace production-specific css file name with
// development-specific css file name.
devCssFile: {
// Scan all document file types inside of html/.
src: 'html/**/*{<%= docFileTypes %>}',
// Replacement(s) to make.
replacements: [{
// From: "all.min.css".
from: /all\.min\.css/,
// To: "all.css".
to: 'all.css'
}],
// Overwrite each file.
overwrite: true
},
// Target: replace development-specific css file name with
// production-specific css file name.
prodCssFile: {
// Scan all document file types inside of html/.
src: 'html/**/*{<%= docFileTypes %>}',
// Replacement(s) to make.
replacements: [{
// From: "all.css".
from: /all\.css/,
// To: "all.min.css".
to: 'all.min.css'
}],
// Overwrite each file.
overwrite: true
},
// Target: replace production-specific js file name with
// development-specific js file name.
devJsFile: {
// Scan all document file types inside of html/.
src: 'html/**/*{<%= docFileTypes %>}',
// Replacement(s) to make.
replacements: [{
// From: "all.min.js".
from: /all\.min\.js/,
// To: "all.js".
to: 'all.js'
}],
// Overwrite each file.
overwrite: true
},
// Target: replace development-specific js file name with
// production-specific js file name.
prodJsFile: {
// Scan all document file types inside of html/.
src: 'html/**/*{<%= docFileTypes %>}',
// Replacement(s) to make.
replacements: [{
// From: "all.js".
from: /all\.js/,
// To: "all.min.js".
to: 'all.min.js'
}],
// Overwrite each file.
overwrite: true
},
// Target: bust cache. This target assumes "?cb=[any number here]"
// has been appended to any file-path you'd like to cache-bust. For
// example, css files, js files, image files, etc.:
// <link rel="stylesheet" href="/lib/css/all.css?cb=1" />
// <script src="/lib/js/all.min.js?cb=1"></script>
// <img src="/lib/img/photo.jpg?cb=1" alt="A photo" />
cacheBust: {
// Scan all document file types inside of html/.
src: 'html/**/*{<%= docFileTypes %>}',
// Replacement(s) to make.
replacements: [{
// From: "?cb=[any number here]".
from: /\?cb=[0-9]*/g,
// To: "?cb=[the current Unix epoch time]"
to: function() {
var uid = new Date().valueOf();
return '?cb=' + uid;
}
}],
// Overwrite each file.
overwrite: true
}
},
// Task: aside from creating notifications when something fails (which
// is this task's permanent/default behavior), also create the following
// notifications for these custom, non-failure events. The targets below
// are utilized by requesting them via targets found in the watch task.
// https://github.com/dylang/grunt-notify
notify: {
// Target: Sass.
sass: {
options: {
title: 'CSS',
message: 'Compiled successfully.'
}
},
// Target: JavaScript.
js: {
options: {
title: 'JavaScript',
message: 'Hinted and concatenated successfully.'
}
},
// Target: ready for production.
goProd: {
options: {
title: 'Ready for Production',
message: 'Ship it!'
}
},
// Target: ready for development.
goDev: {
options: {
title: 'Ready for Development',
message: 'Make it better!'
}
}
},
// Task: when something changes, run specific task(s).
// https://github.com/gruntjs/grunt-contrib-watch
watch: {
// Task-wide options.
options: {},
// Target: Sass.
sass: {
// Target-specific options.
options: {},
// Watch all sassFileTypes files inside of sassPath.
files: '<%= sassPath %>**/*.{<%= sassFileTypes %>}',
// Task(s) to run.
tasks: [
'newer:sass:dev',
'notify:sass'
]
},
// Target: css.
css: {
// Target-specific options.
options: {
// Utilize Livereload.
livereload: true
},
// Watch all .css files inside of cssPath.
files: '<%= cssPath %>*.css'
},
// Target: JavaScript.
js: {
// Target-specific options.
options: {
// Utilize Livereload.
livereload: true
},
// Watch all JavaScript files inside of jsPath.
files: [
'<%= jsPath %>*.js',
'!<%= jsPath %>*.min.js', // ...but not minified files.
'!<%= jsPath %>all.js' // ...but not the concatenated file.
],
// Task(s) to run.
tasks: [
'newer:jshint:all',
'concat:all',
'notify:js'
]
},
// Target: documents.
docs: {
// Target-specific options.
options: {
// Utilize Livereload.
livereload: true
},
// Watch all docFileTypes inside of html/.
files: 'html/**/*{<%= docFileTypes %>}'
}
}
});
// -------------------------------------------------------------------------
// #### Define task aliases that run multiple tasks ####
// On the command-line, type "grunt [alias]" without the square brackets or
// double-quotes, and press return. The format is as follows:
// grunt.registerTask('alias', [
// 'task1',
// 'task2:targetName',
// 'task3'
// ]);
// -------------------------------------------------------------------------
// #### Task alias: "default" ####
// Task(s) to run when typing only "grunt" in the console.
grunt.registerTask('default', [
'watch'
]);
// #### Task alias: "goProd" ####
// Tasks(s) to run when it's time to convert things from development mode to
// production mode.
grunt.registerTask('goProd', [
'sass:prod',
'jshint:all',
'concat:all',
'uglify:all',
'imagemin:all',
'replace:removeLivereload',
'replace:prodCssFile',
'replace:prodJsFile',
'replace:cacheBust',
'notify:goProd'
]);
// #### Task alias: "goDev" ####
// Tasks(s) to run when it's time to convert things from production mode to
// development mode.
grunt.registerTask('goDev', [
'replace:addLivereload',
'replace:devCssFile',
'replace:devJsFile',
'notify:goDev'
]);
};
{
"name": "xyz-inc-marketing",
"description": "A marketing website for XYZ, Inc.",
"version": "0.1.0",
"author": {
"name": "The Outfit, Inc.",
"url": "http://fromtheoutfit.com",
"email": "hello@fromtheoutfit.com"
},
"owner": {
"name": "XYZ, Inc.",
"url": "http://xyzinc.com",
"email": "hello@xyzinc.com"
},
"devDependencies": {
"grunt": "~0.4.1",
"grunt-contrib-concat": "~0.3.0",
"grunt-contrib-jshint": "~0.6.5",
"grunt-contrib-sass": "~0.5.1",
"grunt-contrib-uglify": "~0.2.7",
"grunt-contrib-watch": "~0.5.3",
"grunt-contrib-imagemin": "~0.3.0",
"grunt-text-replace": "~0.3.11",
"grunt-notify": "~0.2.17",
"grunt-newer": "~0.6.1",
"jit-grunt": "~0.2.1",
"time-grunt": "~0.2.9"
}
}
@jimmynotjim
Copy link

The only weird part to me is that you're naming the concatenated file .min even though it hasn't been minified until uglify runs. Since you'll most likely be running these together it's not a big thing, just something that stood out to me.

@marcamos
Copy link
Author

So I just found this today for loading all the grunt tasks instead of using matchdep. Testing on scrollNav and it's pretty nice https://github.com/sindresorhus/load-grunt-tasks

Cool; the lazy person in me wonders what the advantage is?

The only weird part to me is that you're naming the concatenated file .min even though it hasn't been minified until uglify runs. Since you'll most likely be running these together it's not a big thing, just something that stood out to me.

Yeah, I agree(d). In the past hour or so, I've changed things around quite a bit. With regard to this thing you've pointed out, I ended up mimicking a pattern/process already used by the Sass task, where a dev-only JS file is maintained along-side a production-only JS file, then the replace task is used to swap file-names whenever necessary.

@jimmynotjim
Copy link

Minimal difference, just allows you to specify grunt- vs grunt-contrib- tasks which you can obviously do with matchdep.

@marcamos
Copy link
Author

Ah, cool, thanks.

@jlbruno
Copy link

jlbruno commented Dec 4, 2013

Didn't know about the notify plugin either. Makes watching all your files that much more worthwhile, if you're hinting/linting on a watch task. Are you not using compass with sass?

@marcamos
Copy link
Author

marcamos commented Dec 4, 2013

Are you not using compass with sass?

More often than not, yes. The test project that's associated with—but not visible here—this gruntfile doesn't use Compass, hence its absence above.

@notasausage
Copy link

I just started using Grunt today and my Gruntfile.js looks sad compared to this. But there's so much great stuff here, I can't wait to put some of it to use. Thanks for putting this together!

@marcamos
Copy link
Author

You're very welcome! I hope it helps, and if you (or anybody) finds issues, please let me know. This is my first one, so it's far from perfect.

@marcamos
Copy link
Author

I switched things up a bit. The imagemin task no longer runs automatically via the watch task; I felt that was overkill. Instead, it now runs via the goProd task alias because -- in my opinion -- image minification is not really needed until the project is ready for production.

@kimili
Copy link

kimili commented Dec 29, 2013

This is pretty awesome, Marc. Lots of great stuff in here.

Being preferential to SCSS syntax myself, I've one small suggestion: to make it a bit more flexible, how about adding a sassFileTypes variable and setting it to .sass,.scss, and replacing all the instances of *.sass In the tasks with *.{<%= sassFileTypes %>}?

@marcamos
Copy link
Author

Not a bad idea! I'll do that very very soon, thanks @kimili.

@marcamos
Copy link
Author

marcamos commented Jan 7, 2014

@kimili: I've updated the Gruntfile to incorporate your suggestion. The only adjustment that was needed was the removal of the dot preceding each file extension. Thanks again for making that suggestion!

@kimili
Copy link

kimili commented Jan 7, 2014

Awesome, Marc, thanks for making that tweak!

@marcamos
Copy link
Author

I've made a few adjustments. One of them introduces reporting on Grunt's execution time(s), and two of them aim to improve Grunt's performance in general:

  • Three plugins added to package.json.
  • Added time-grunt to provide task-execution-time reporting in the console.
  • Swapped matchdep for jit-grunt, which will load plugins as they're needed by the tasks that require them, rather than load all of the plugins every time Grunt is run.
  • Added grunt-newer -- here & here -- which ensures that Grunt only processes files that have changed since the last run, rather than process each and every file regardless of modification date.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment