permalink

8

Environment-specific Builds With Grunt, Gulp or Broccoli

Image credit: http://www.flickr.com/photos/brianneudorff/9109932159/

Image credit: http://www.flickr.com/photos/brianneudorff/9109932159/

The dev, staging and production versions of our projects can vastly vary, which is one reason we may need to change paths to resources (scripts/styles/templates), generated markup or other content based on environment and target-specific information. Luckily there exist a number of build tasks in the Grunt, Gulp and Broccolli eco-systems that can help us here.

Today I’ll cover three approaches to this problem – string replacement, conditional comments and template variables. The option you end up choosing will likely depend on where you’re happy for the bulk of your conditional logic to exist.

Simplest option – string/regex replacement

The simplest option for environment-specific output is to use string replacement with either a simple string or regex pattern to find and remove/replace blocks in your HTML files. Your build-file can have a number of targets setup, each of which can replace strings with arbitrary content.

For example, in Grunt, a very basic example using grunt-string-replace might aim to replace a string source.js with build.js in prod builds as follows:

module.exports = function(grunt) {
  grunt.initConfig({
    'string-replace': {
      prod: {
        src: './app/**/*.html',
        dest: './dist/',
        options: {
          replacements: [{
            pattern: 'source.js',
            replacement: 'build.js'
          }]
        }
      }
    }
  });
  grunt.loadNpmTasks('grunt-string-replace');
  grunt.registerTask('default', ['string-replace']);
};

(grunt-replace also does a fine job at this)

In general, the text replacement approach works, requires minor configuration and is used by developers like Stephen Sawchuk (a contributor to Yeoman and Bower) who found some of the alternatives overly complex for his setup. You can find an example of how he uses string replacement for projects in this gist.

Equivalents if you aren’t using Grunt are gulp-replace and broccoli-replace.

String replacement works fine for simple scenarios where you need to update or remove a set of strings across multiple files. Where this approach becomes less maintainable is when you have a large number of replacements to make across files and end up jumping back and forth between your source and build-files to figure out what exactly is going to be replaced.

A better solution might be to define your replacements in-place within your source files. This brings us to our next option..

Conditional comments

The basic idea with environment conditional comments is that you define the logic and information needed (e.g paths) for replacing a string with another (for a specific target) in the form of HTML comments backed up with minimal config inside your build-file for the tasks. This makes for a readable solution that can be easily authored on a per-file basis. Some options:

Grunt:

Gulp:

The comment-syntax for the first three options on the list vary in readability and verbosity, but they effectively achieve the same end result. All options output target/env specific blocks without much effort, but let’s first compare targethtml and preprocess:

A grunt-targethtml example (Dev vs. Production)

Dev:

<!--(if target dev)><!-->
 <link rel="stylesheet" href="dev.css" />
<!--<!(endif)-->

<!--(if target dev)><!-->
  <script src="dev.js"></script>
  <script>
    var less = { env:'development' };
  </script>
<!--<!(endif)-->

Production:

<!--(if target prod)><!-->
  <link rel="stylesheet" href="release.css">
<!--<!(endif)-->

<!--(if target prod)><!-->
   <script src="release.js"></script>
<!--<!(endif)-->

and the same with grunt-processhtml

Dev:

<!-- @if NODE_ENV='production' -->
 <link rel="stylesheet" href="dev.css">
<!-- @endif -->

<!-- @if NODE_ENV='production' -->
<script src="dev.js"></script>
<script>
  var less = { env:'development' };
</script>
<!-- @endif -->

Production:

<!-- @if NODE_ENV='production' -->
<link rel="stylesheet" href="release.css">
<!-- @endif -->

<!-- @if NODE_ENV='production' -->
<script src="release.js"></script>
<!-- @endif -->

As we can see, specifying our logic in comment-form is fairly straight-forward and I’ve heavily used this approach without too many complaints (I personally prefer grunt-targethtml based on syntax).

One of the main downsides to the conditional comment approach however is that you can end up with a lot of build-specific logic inside your markup files. Some developers may dislike this, but I personally enjoy the readability flow these tasks offer. It also means I don’t have to jump back into my Gruntfile to figure out what is going on.

I mentioned three Grunt tasks earlier, so what about grunt-processhtml? Well, it’s a little more specialist in that you can include completely different files inline, use templated variables and a few other nice tricks.

For example, here you can change the class to ‘production’ only when the ‘dist’ target is run:

<!-- build:[class]:dist production -->
<html class="debug_mode">
<!-- /build -->

or replace a block for a target with an arbitrary file:

<!-- build:include:dev dev/content.html -->
This will be replaced by the content of dev/content.html
<!-- /build -->

or remove a block depending on your target:

<!-- build:remove -->
<p>This will be removed when any processhtml target is done</p>
<!-- /build -->

<!-- build:remove:dist -->
<p>But this one only when doing processhtml:dist target</p>
<!-- /build -->

There seem to be a minefield of tasks available to try solving similar problems, but I’ll mention just one more in this section. Should you absolutely need the ability to configure the format of the code-comment blocks being used for conditional inclusion, see grunt-devcode which supports this. Some sample configuration:

Gruntfile:

devcode :
    {
      options :
      {
        html: true,        // html files parsing?
        js: true,          // javascript files parsing?
        css: true,         // css files parsing?
        clean: true,       
        block: {
          open: 'condition', // open code block
          close: 'conditionend' // close code block
        },
        dest: 'dist'       
      },

Markup:

<!-- condition: !production -->
  <li class="right">
    <a href="#settings" data-toggle="tab">
      Settings
    </a>
  </li>
<!-- conditionend -->

So, this approach works but what if you want something with the placeholder benefits that conditional comments have to offer but leverage your build file for the data strings used for replacements? Let’s move onto..

Template variables in HTML

Build-time templating allows you to swap out the values for placeholder strings (e.g <%- title %>) defined in your source files with target-specific data (strings, objects – whatever you need) defined in your build-file (e.g Gruntfile). This contrasts with many approaches to conditional HTML comments in that the data you’re injecting isn’t specified inside the target-file itself. Some options for this:

Grunt:

Gulp:

grunt-template (which I recommend) is basically a wrapper around grunt.template.process(). Whilst it’s primarily targeted at target-based population of template strings, you could change paths or content based on a dev/prod target via the data object. A contrived example can be seen below:

index.html.tpl

<title><%- title %></title>

Gruntfile.js

module.exports = function(grunt) {
    grunt.initConfig({
        'template': {
            'dev': {
                'options': {
                    'data': {
                        'title': 'I <3 JS in dev'
                    }
                },
                'files': {
                    'dev/index.html': ['src/index.html.tpl']
                }
            },
            'prod': {
                'options': {
                    'data': {
                        'title': 'I <3 JS in prod'
                    }
                },
                'files': {
                    'dist/index.html': ['src/index.html.tpl']
                }
            }
        }
    });
    grunt.loadNpmTasks('grunt-template');
    grunt.registerTask('default', [
        'template:prod'
    ]);
    grunt.registerTask('dev', [
        'template:dev'
    ]);
};

grunt-include-replace does something similar with a different format for templated strings (<title>@@title</title>).

Wrapping up

I have yet to come across what I would consider an ideal solution for environment-specific build targeting, however I and some of the developers I work with have found the options mentioned useful in past projects. If you’re aware of more efficient or maintainable tasks for solving this problem, please do feel free to share them. I hope at least some of this is useful to someone out there!

Thanks to Sindre Sorhus, Pascal Hartig and Stephen Sawchuk for their technical reviews

8 Comments

  1. Great article, thanks! Tip for environment specific RequireJS config: grunt-requirejs-transformconfig. If you can manage your requirejs config in your env specific grunt builds, you can still use almondjs and save the size of full blown requirejs.

  2. Thanks for covering this, Mr. Osmani. There’s nothing quite great out there yet. I use `grunt-include-replace` which manages a lot of things. Interesting to think about env specific configurations combined with a build-specific folders like Alex Sexton recommends: https://alexsexton.com/blog/2013/03/deploying-javascript-applications/

    I do this with good success but it’s a hassle to swap the path in everywhere, especially image paths in stylesheets.

    Also it would be great to have a better way to write things like this:
    “`
    angular.module(‘coatue’).constant ‘cfg’, ->
    logLevel: if (‘@@logLevel’.indexOf(‘@logLevel’) isnt -1) then ‘debug’ else ‘@@logLevel’
    “`
    (have to search for @logLevel to see if it’s unparsed, which is useful in some situations)

  3. Great article. Exactly what i was looking for.

    And nicely structured:
    * Explanation of Technique
    * Example of how to integrate it in code
    * Example of how to integrate it in config

    Makes it very easy to understand and follow. I like that.

Leave a Reply

Required fields are marked *.