I was working on dailyjs-backbone-tutorial and I noticed issue #5 where "fiture" was unable to run the build script. That tutorial uses Grunt to invoke
r.j from RequireJS, and it turned out I forgot to specify the version of Grunt in the project's
package.json file, which meant newcomers were getting an incompatible version of Grunt.
I changed the project to first specify the version of Grunt, and then renamed the grunt file to
Gruntfile.js, and it pretty much worked. You can see these changes in commit 0f98f7.
So, what's the big deal? Why is Grunt breaking projects and how can this be avoided in the future?
Global vs. Local
If you're a client-side developer, npm is probably just part of your toolkit and you don't really care about how it works. It gets things like Grunt for you so you can work more efficiently. However, us server-side developers like to obsess about things like dependency management, and to us it's important to be careful about specifying the version of a given module.
Previous versions of Grunt kind of broke this whole idea, because Grunt's documentation assumed you wanted to install Grunt "globally". I've never liked doing that, as I've experienced why this is bad first-hand with the Ruby side projects I've been involved with. What I've always preferred to do with Node is write a
package.json for every project, and specify the version of each dependency. I either specify the exact version, or the minor version if the project uses semantic versioning.
For example, with Grunt I might write this:
, "grunt": "0.3.x"
This causes the
grunt command-line tool to appear in
./node_modules/.bin/grunt, which probably isn't in your
$PATH. Therefore, when you're ready to build the project and you type
grunt, the command won't be found.
Knowing this, I usually add
node_modules/.bin/grunt as a "script" to
package.json which allows
grunt to be invoked through the
npm command. This works in Unix and Windows, which was partly the reason I used Grunt instead of
There were problems with this approach, however. Grunt comes with a load of built-in tasks, so when the developers updated one of these smaller sub-modules they had to release a whole new version of Grunt. This is dangerous when a module is installed globally -- what happens if an updated task has an API breaking change? Now all of your projects that use it need to be updated.
To fix this, the Grunt developers have pulled out the command-line part of Grunt from the base package, and they've also removed the tasks and released those as plugins. That means you can now write this:
, "grunt": "0.4.x"
And install the command-line tool globally:
npm install -g grunt-cli
grunt-cli is a very simple module it's safer to install it globally, while the part that we want to manage more carefully is locked down to a version range that shouldn't break our project.
Built-in Tasks: Gone
The built-in tasks have been removed in Grunt 0.4. I prefer this approach because Grunt was getting extremely large, so it seems natural to move them out into plugins. You'll need to add them back as
devDependencies to your
If you're having trouble finding the old plugins, they've been flagged on the Grunt plugin site with stars.
Before switching to the latest version of Grunt, be sure to uninstall the old one if you installed it globally.
There are other changes in 0.4 that you may run into that didn't affect my little Backbone project. Fortunately, the Grunt developers have written up a migration guide which explains everything in detail.
Also worth reading is Tearing Grunt Apart in which Tyler Kellen and Ben Alman explain why Grunt has been changed, and what to look forward to in 0.5.
If you write Grunt plugins, then I recommend reading Peer Dependencies on the Node blog by Domenic Denicola. As a plugin author, you can now take advantage of the
peerDependencies property in
package.json for defining the version of Grunt that your plugin is compatible with.
Take a look at grunt-contrib-requirejs/package.json to see how this is used in practice. The authors have locked the plugin to Grunt 0.4.x.