Build and Publish Your Own Grunt Plugin

    Stephan Max
    Share

    Grunt is a widespread and popular task runner for JavaScript. Its architecture is based on plugins that you can combine and configure to create a powerful build system for your web applications. The Grunt ecosystem is huge and offers hundreds of plugins to help you with tedious and repetitive tasks, such as linting, testing, minification, image processing, and so on.

    I had a blast building and publishing my Grunt plugin and I’m keen to share with you the experience I’ve gained along the way. I’ll show you how to build your own little Grunt plugin and publish it via the npm package manager.

    The plugin we will build in this article will serve as a remedy for so-called typographic orphans — single words on the last line of a paragraph or block element — by replacing the last space with a non-breakable space. This is a quite easy task, but while implementing it, we’ll touch all the relevant subjects, such as setup, best practices, configuration, testing, and publishing.

    A typographic orphan in a text paragraph

    If you want to get an in-depth knowledge of Grunt’s mechanics or wish to contribute to an existing plugin this article is for you. Before starting, I suggest you to take some time to have a look at the official Getting Started guide and at Etienne Margraff’s article titled How to Grunt and Gulp Your Way to Workflow Automation.

    The plugin that we’ll build in this article is available on GitHub. For your benefit, I added tags (called step01step04) to the repository. If you want to follow along with the code at hand just check out the respective tag. For example the command git checkout tags/step02 mirrors the state of the code after section 2.

    Setting up Your Playground

    Assuming you have Node.js installed on your machine, we can immediately get started to set up our plugin skeleton. Luckily, the Grunt team provides a nice tool called grunt-init to make plugin development easy. We’ll install this tool globally with npm and clone the Grunt plugin template from Git:

    npm install -g grunt-init
    git clone git://github.com/gruntjs/grunt-init-gruntplugin.git .grunt-init/gruntplugin

    Now we’re ready to create a new directory for our plugin and run the command grunt-init:

    mkdir grunt-typographic-adoption
    cd grunt-typographic-adoption
    grunt-init gruntplugin

    We’ll be prompted with a couple of questions regarding the meta data of our plugin. When naming your Grunt plugin, remember that the grunt-contrib namespace is reserved for tasks that are maintained by the Grunt team. So, your first job is to find a meaningful name that respects that rule. Since we’re dealing with typographic orphans, I thought that a name as grunt-typographic-adoption could be appropriate.

    If you put your new plugin folder under version control and set a remote to GitHub before running grunt-init, you’re lucky. The scaffolding script will use the information provided by Git and GitHub to populate a lot of the points you have to tick off. Stick to the default values for Grunt’s and Node.js’ version unless some of your dependencies demand a specific one. With regard to versioning your own plugin, you should get familiar with Semantic Versioning. I suggest you to take a look at the official documentation for project scaffolding where you can read more about other available templates for grunt-init and ways to specify default prompt answers.

    Now, let’s have a look at the directory and file structure we have in place now:

    .gitignore
    .jshintrc
    Gruntfile.js
    LICENSE
    README.md
    package.json
    - tasks
      | - typographic_adoption.js
    - test
      | - expected
      | - custom_options
        | - default_options
      | - fixtures
        | - 123
        | - testing
      | - typographic_adoption_test.js

    The .gitignore file comes in handy once you put your plugin under version control (operation that you should do and hopefully you already performed!). The Gruntfile.js specifies what needs to be done to build our plugin and luckily it comes with some pre-defined tasks, namely JavaScript linting (configured in .jshintrc) and a simple test suite (we’ll examine in detail the corresponding test folder in a minute). LICENSE and README.md are self-explanatory, pre-filled with standard content and important once you decide to publish your plugin.

    Finally, package.json contains all the information about our plugin including all its dependencies. Let’s install and run it:

    npm install
    grunt

    If all went smoothly, we are rewarded with our typographic_adoption task in action and the output Done, without errors.. Give yourself a pat on the back since we have a fully functional Grunt plugin. It’s not doing anything particularly useful yet, but we’ll get there. The whole magic happens in tasks/typographic_adoption.js where we’ll implement our anti-widow code. But first of all, we’ll write some tests.

    Test-Driven Development

    It’s always a good idea to implement the tests first and thus specify what you want your task to accomplish. We’ll make the tests pass again, which gives us a good hint that we implemented everything correctly. Test-driven development is amazing and the users of your plugin will thank you!

    So what do we want to accomplish? I already told you that we want to tackle typographic orphans, that is single words on the last line of a paragraph or other block element. We’ll do this by scanning files for HTML block elements, extracting the inner text, and replacing the last space with a non-breakable space.

    In other words, we feed our plugin with this:

    <p>
      Lorem ipsum dolor sit amet, consetetur sadipscing elitr,
      sed diam nonumy eirmod tempor invidunt ut labore et dolore
      magna aliquyam erat, sed diam voluptua.
    </p>

    And we expect it to transform it into this:

    <p>
      Lorem ipsum dolor sit amet, consetetur sadipscing elitr,
      sed diam nonumy eirmod tempor invidunt ut labore et dolore
      magna aliquyam erat, sed diam&nbsp;voluptua.
    </p>

    Since our plugin scaffold comes with the nodeunit testing task, we can implement this kind of tests easily.

    The mechanism is simple:

    1. Grunt executes our typographic adoption task on all files specified in Gruntfile.js (best practice is to put them into test/fixtures).
    2. The transformed files are then stored in tmp (the .gitignore file makes sure that this folder is never going into your code repository).
    3. The nodeunit task looks for test files in test and finds typographic_adoption_test.js. This file specifies any number of tests, that is checking whether a file in tmp equals its counterpart in test/expected.
    4. nodeunit informs us on the command line if and which tests failed or if the whole test suite passed.

    Usually, you build one test per configuration to make sure your task can handle all kind of scenarios and edge cases. Let’s take some time and think about possible configurations for our Grunt plugin. We basically want the user to be able to configure in which HTML elements our task is run. The default option could be every text-containing HTML block element (h1, p, blockquote, th, and many others), while we let the user customize this with an option to set arbitrary CSS selectors. That helps to widen or narrow down the scope of our task.

    Now it’s time to get our hands dirty. Firstly, navigate to test/fixtures, remove the 123 file, and edit testing into a simple HTML file with some block elements that you want to test your plugin against. I decided to use a short article about Marvel’s Black Widow since typographic orphans are also sometimes called widows.

    Now, copy the content of test/fixtures/testing and override the two files in test/expected with it. Edit them according to what you expect as an outcome after your plugin processed the testing file. For the case with custom options I chose the scenario where the user only wants <p> elements to get de-orphanized.

    Lastly, edit Gruntfile.js to target only your testing file (that means remove the 123 bit from the files arrays) and give your tests a meaningful description in test/typographic_adoption_test.js.

    The moment of truth has arrived. Run grunt in your project’s root:

    grunt
    ...
    Warning: 2/2 assertions failed

    Brilliant! All our tests fail, let’s fix this.

    Implementing the Task

    Before we start with implementation, we should think to the helpers we might need. Since we want to search HTML files for certain elements and alter their text portion, we need a DOM traversing engine with jQuery-like capabilities. I found cheerio very helpful and lightweight, but feel free to use whatever you are comfortable with.

    Let’s plug in cheerio as a dependency:

    npm install cheerio --save

    This installs the cheerio package into your node_modules directory and also, thanks to --save, saves it under dependencies in your package.json. The only thing left to do is open up tasks/typographic_adoption.js and load the cheerio module:

    module.exports = function(grunt) {
      var cheerio = require('cheerio');
      ...

    Now, let’s fix our available options. There is just one thing the users can configure at this stage: the elements they want to de-orphanize. Look for the options object inside the grunt.registerMultiTask function and change it accordingly:

    var options = this.options({
      selectors: 'h1.h2.h3.h4.h5.h6.p.blockquote.th.td.dt.dd.li'.split('.')
    });

    The options object gives us all the customized settings the plugin users put into their Gruntfile.js but also the possibility to set default options. Go ahead and change the custom_options target in your own Gruntfile.js to reflect whatever your tests from chapter 2 are testing. Since I just want paragraphs to get processed, it looks like this:

    custom_options: {
      options: {
        selectors: ['p']
      },
      files: {
        'tmp/custom_options': ['test/fixtures/testing']
      }
    }

    Make sure to consult the Grunt API docs for more information.

    Now that we have cheerio and our options in place, we can go ahead and implement the core of the plugin. Go back to tasks/typographic_adoption.js and right under the line where you are building the options object replace the scaffolding code with this:

    this.files.forEach(function(f) {
      var filepath = f.src, content, $;
    
      content = grunt.file.read(filepath);
      $ = cheerio.load(content, { decodeEntities: false });
    
      $(options.selectors.join(',')).each(function() {
        var text = $(this).html();
        text = text.replace(/ ([^ ]*)$/, ' $1');
        $(this).html(text);
      });
    
      grunt.file.write(f.dest, $.html());
      grunt.log.writeln('File "' + f.dest + '" created.');
    });

    We are looping over all the files that we have specified in Gruntfile.js. The function we call for each file loads the file’s content with the grunt.file API, feeds it into cheerio, and searches for all the HTML elements we have selected in the options. Once found, we replace the last space inside the text of each element with a non-breakable space and write that back to a temporary file. Our test suite can now compare those temporary files with our expected ones and hopefully it shows you something like this:

    grunt
    ...
    Running "nodeunit:tests" (nodeunit) task
    Testing typographic_adoption_test.js..OK
    >> 2 assertions passed (59ms)
    
    Done, without errors.

    Awesome! We have just implemented our own little Grunt plugin and it works like a charm!

    A typographic orphan in a text paragraph

    If you want you can further improve, extend, and polish it until you are happy with the result and feel like sharing it with other developers.

    Publish Your Plugin

    Publishing our plugin is easy and it takes only a few minutes. Before we push our code, we have to make sure that everything is set up correctly.

    Let’s take a look at the package.json file where all the information that npm uses in their registry reside. Our initial template already took care of adding gruntplugin to the keywords list, which is essential for our plugin to be found as a Grunt plugin. This is the moment to take some time and add more keywords so people can find our plugin easily.

    We also take care of our README.md file and provide our future users with documentation on our task’s general usage, use cases, and options. Thanks to grunt-init we already got a nice first draft to work with and can polish it from there.

    Once these preparations are done, we are good to publish our plugin. If you don’t have a npm account yet, you can create one on their website, or fire up npm on the command line and set up everything there. The following command will ask you for a username and password and either create a new user on npm and save your credentials at .npmrc or log you in:

    npm adduser

    Once you are registered and logged in, you can go ahead and upload your plugin to the npm:

    npm publish

    That’is it! All the information needed are automatically retrieved from thepackage.json file. Take a look at the Grunt plugin we just created here.

    Conclusions

    Thanks to this tutorial, you have learned how to create a Grunt plugin from scratch. Moreover, if you’ve published it, you’re now the proud owner of a Grunt plugin that is available on the Web, ready to be used by other web developers. Keep it up, keep maintaining your plugin, and stick to test-driven development.

    If you are in the process of building a Grunt plugin or already built one and want to share something around the process, please comment in the section below. Once again, I want to highlight that the plugin we’ve build in this article is available on GitHub.