Writing a Grunt Plugin that Uses other Plugins

I’ve been using Grunt for a while now, and wanted to package some reusable functionality from one project into a plugin, so that it could easily be used in other projects. For some reason, this turned out to be a lot less intuitive than I was expecting. I read the official page on Creating Plugins, but it doesn’t mention a variety of details you need to pay attention to if you want to encapsulate other plugins within your own plugin.

There are several gotchas. First, if you simply port the 3rd-party tasks’ configurations to the plugin’s Gruntfile.js, your plugin will work when you test it (from the plugin source), but it will fail when you use your plugin, since the plugin’s Gruntfile doesn’t get processed when it is run as a plugin. You also have to explicitly load tasks you want to use, and be mindful of how you use paths to reference them.

Here are my tips on writing plugins:

  1. Clone the grunt-init gruntplugin repository and run grunt-init gruntplugin to setup the plugin template, as described in the Grunt documentation on creating plugins. This will be your plugin source. You’ll be able to run Grunt in there to test it.
  2. Right away, add your plugin to the project you wish to use it in, so that you can test it as a plugin while you develop. You don’t need to publish it, instead, in your project package.json, add it as a devDependency with a URL to its GIT repository:
    myproject/package.json
      "devDependencies": {
        "grunt-myplugin": "git@bitbucket.org:antun/grunt-myplugin.git"
      }

    As you make updates, you’ll need to commit/push, then uninstall/reinstall the plugin from your project as follows:

    $ npm rm grunt-myplugin
    $ npm install
  3. Add your 3rd-party plugins to your plugin source’s package file as dependencies, rather than devDependencies. This will ensure they’re installed under your project when your plugin is installed.
    package.json
      "dependencies": {
        "grunt-shell" : "1.1.1"
      },
  4. Explicitly load the 3rd party task you want to use, from your plugin source’s task file. This is necessary since your plugin won’t automatically have access to its own dependencies, when it’s used as a plugin in another project. Do this near the top of the task file. e.g. If you rely on the grunt-shell module within your plugin, would do:
    tasks/buttonimage.js
    module.exports = function(grunt) {
      var path = require('path');
      grunt.task.loadTasks(path.resolve(__dirname, '../node_modules/grunt-shell/tasks'));
      // load any other tasks, and task registration
    }

    Note also that I used the Node.js path.resolve() method to handle loading the URL relative to the plugin, no matter where it’s run from. Since path is a core Node.js module, you can require it without any special dependency handling.

  5. Define your plugin’s custom task(s) in its source Gruntfile. Organize the options as you would like to see them used when it is used as a plugin. Remember, this configuration will only be used when you run your plugin’s tasks from within the source.
    Gruntfile.js
      grunt.initConfig({
        buttonimage: {
          testButton: { 
              output: 'dist/testButton.gif'
          }
        }
        /* Other configuration, test, stuff */
      }

    You can use this.options() inside of your

  6. Don’t bother adding configuration for your 3rd-party tasks in the plugin source Gruntfile. The source Gruntfile doesn’t get processed when you run your plugin in another project. Instead, set the configuration procedurally from within your task file using grunt.config.set() before running the task. e.g.
    tasks/buttonimage.js
      grunt.registerMultiTask('buttonimage', 'Generate localized buttons with text labels.', function() {
        var cmd = 'some shell command you built up';
        grunt.config.set('shell', {
            buttonImage: {
                command: cmd
            }
          }
        );
        grunt.task.run(['shell:buttonImage']);
      });
  7. Don’t change the working directory in your task, so work with absolute paths instead. Use path.resolve() to get an absolute path relative to the top-level Gruntfile for files that you will want to operate on. When you define file paths in the Gruntfile, whether that’s the one in your plugin source config or eventually the one in the project where you will use your plugin, you will want to convert them to absolute paths. To do this, use the Node.js path.resolve() method to convert the input path – which is relative to the root Gruntfile – to an absolute path:
    tasks/buttonimage.js
      grunt.registerMultiTask('buttonimage', 'Generate localized buttons with text labels.', function() {
        var outputPath = path.resolve(this.options().output);
        var cmd = "convert -some arguments " + outputPath
        /* Set the config and run your task */
      }

    If you need to get an absolute path relative to your task definition (e.g. to run a Node.js script that you have bundled inside your plugin), you can use path.resolve(__dirname, ‘path/relative/to/task/file.js’) to get that.

Hopefully these will save you some time!