Moving to GruntJS
Published 2013-3-13Watch on YouTube: youtu.be/s5eBIie03ls
Are you still using bash scripts to compile your browser code? Say hello to GruntJS!
A quick thanks to @iammerrick for teaching me everything I know about Grunt
Intro
You can think of Grunt as the connect of build tasks - it allows you to specify how something is built and in what order.
Just like connect, Grunt is, of itself, almost nothing. It's a more of a convention than a real thing, y'know?
The real meat of grunt is in the tasks (just like the real meat of connect is in the middleware).
Conventionally, npm packages that are named with a grunt- prefix (i.e. grunt-pakmanager) are 3rd party grunt tasks, whereas packages prefixed with grunt-contrib- (i.e. grunt-contrib-jade) are part of the grunt core.
Browser Apps
The current market where grunt really shines is in building single-page (or few-page) browser apps.
Everybody has a different religion when it comes to what to use, but we all pretty much agree that html, css, and javascript all suck terribly and we use some other format or program to help us cope.
Pick your poisen. I pick jade, less, and pakmanager.
Standard tasks
You can easily search for all of the tasks available on npm that have
gruntplugin
in the keywords from http://gruntjs.com/plugins.
If you search for jade
, for example, you'll end up at
https://npmjs.org/package/grunt-contrib-jade
which has a nice README.md detailing the options.
All modules can have different options, but fortunately many common conventions have taken hold across them all.
Example: LESS -> CSS
Just because it's extremely simple, let's start with a simple setup that only compiles from LESS to CSS.
Let's assume we already have a project with this file structure
~/
- my-project/
- package.json
- src/
- style.less
- public/
- .gitkeep
Install Grunt
sudo npm install -g grunt-cli
npm install --save-dev grunt
npm install --save-dev grunt-contrib-less
Add Gruntfile
Now we need to create
~/my-project/Gruntfile.js
:
module.exports = function (grunt) {
"use strict";
// Configuration
grunt.initConfig({});
};
And with that we can successfully... do nothing
grunt --help
Add a task to compile LESS to CSS
And now need to go back and add a task
~/my-project/Gruntfile.js
:
module.exports = function (grunt) {
"use strict";
// Configuration
grunt.initConfig({
"less": {
"development": {
files: {
"public/style.css": "src/style.less"
}
}
, "production": {
files: {
"public/style.min.css": "src/style.less"
}
, options: {
yuicompress: true
}
}
}
});
// Task Loading
grunt.loadNpmTasks('grunt-contrib-less');
};
Now with that little bit of config we have some tasks we can run
grunt --help
grunt less:development
grunt less:production
NOTE:
You should know that the targets (development and production) could just have well been called dev and dist or foo and bar. The name has no meaning.
Also note that the name of the files array, the mapping of { "compiled-file.bin": "source-file.src" }, and using an options hash are all common conventions.
Nothing is enforcing those conventions aside from goodwill.
Create a 'build' task
Something about having a bulid process that isn't called build just doesn't feel rigt, so let's change that:
~/my-project/Gruntfile.js
:
module.exports = function (grunt) {
"use strict";
// Configuration...
// ...
// Task Loading
// ...
grunt.registerTask('build', ["less:development", "less:production"]);
};
I think this will satisfy my CDO
grunt build
Ahh, much better
Custom tasks
Of course, Grunt may not have pre-built tasks for all the things that you want to do.
The great news is that's it's dirt-simple to add a custom task in quick-n-dirty fashion and then red-green-refactor your way to publishing it (if you want to).
No pressure.
Registering a function
Snippet of Gruntfile.js
:
// Configuration
grunt.initConfig({
// ...
, "pakmanager": {
"browser": {
"files": { "pakmanaged.js": "lib/browser.js" }
}
, "node": {
"files": { "dist.js": "lib/server.js" }
}
}
});
// Task Loading
// ...
grunt.registerMultiTask('pakmanager', 'Packages CommonJS modules into a single file', function () {
// pakmanager defaults to building package.json.main
grunt.util.spawn({"cmd": "pakmanager", args: ["-e", this.target, "build"]}, this.async());
});
grunt.registerTask('build', ["less:development", "less:production", "pakmanager"]);
Moving that function to a file
mkdir -p grunt-tasks/
touch grunt-tasks/pakmanager.js
In addition to moving the task out to it's own file,
I'm also adding options for handling the files
hash.
grunt-tasks/pakmanager.js
:
module.exports = function (grunt) {
"use strict";
grunt.registerMultiTask('pakmanager', function () {
var files = this.files
;
function pakmanage(f) {
var source = f.src
, destination = f.dest
;
grunt.util.spawn(
{
"cmd": "pakmanager"
, args: [
"-e"
, environment
, "build"
, source
, destination
]
}
, this.async()
);
}
files.forEach(pakmanage, this); // preserving this-ness
});
};
Snippet of Gruntfile.js
:
// Task Loading
// ...
grunt.loadTasks('grunt-tasks/');
grunt.registerTask('build', ["less:development", "less:production", "pakmanager"]);
NOTE: You can have any number of tasks in that folder.
Creating a gruntplugin
npm install -g grunt-init
git clone git://github.com/gruntjs/grunt-init-gruntplugin.git ~/.grunt-init/gruntplugin
mkdir -p ~/grunt-pakmanager
pushd ~/grunt-pakmanager
grunt-init gruntplugin
The questions you will need to ask probably look something like this:
Please answer the following:
[?] Project name (grunt-pakmanager)
[?] Description (The best Grunt plugin ever.)
[?] Version (0.1.0)
[?] Project git repository (git://github.com/aj/node-pakmanager.git)
[?] Project homepage (https://github.com/aj/node-pakmanager)
[?] Project issues tracker (https://github.com/aj/node-pakmanager/issues)
[?] Licenses (MIT) Apache2
[?] Author name (AJ ONeal)
[?] Author email (awesome@coolaj86.com)
[?] Author url (http://coolaj86.com)
[?] What versions of grunt does it require? (~0.4.1)
[?] What versions of node does it run on? (>= 0.8.0)
[?] Do you need to make any changes to the above before continuing? (y/N)
The last part is to add in the core functionality of our gruntplugin to
~/grunt-pakmanager/tasks/pakmanager.js
, which basically entails copying and pasting
from the ealier ~/my-project/grunt-tasks/grunt-pakmanager.js
.
On the lookout (watch files and rebuilding on change)
// Configuration
grunt.initConfig({
// ...
, "watch": {
"all": {
"files": ["**.less", "**.jade", "**.js"]
, "tasks": ["build"]
}
, "less": {
"files": ["style.less", "styles/*.less"]
, "tasks": ["less:development"]
}
, "jade": {
"files": ["index.jade", "includes/*.jade"]
, "tasks": ["jade:development"]
}
, "js": {
"files": ["lib/*.js"]
, "tasks": ["pakmanager:browser"]
}
,
}
});
// Task Loading
// ...
grunt.loadNpmTasks('grunt-contrib-watch');
// ...
Now I can watch everything all at once with grunt watch:all
or I can just watch individual subsets of tasks using screen
and having each of grunt watch:less
, grunt watch:jade
, and
grunt watch:js
in separate screens.
By AJ ONeal
Did I make your day?
Buy me a coffee
(you can learn about the bigger picture I'm working towards on my patreon page )