Technology

Tips

How to Organize Your Gulp.js Development Builds for Multiple Environments

March 18, 2016

Travis Luong

By Travis Luong

You are developing a growing front-end application with gulp.js. You notice that your scripts are starting to look like incomprehensible spaghetti.

You have many environments including test, development, staging, production, and continuous integration. They all depend on your build scripts to generate the correct build per environment. Each environment may have an entirely or slightly different build process than the other. 

How do you manage all of your environment-specific builds with gulp? How do you minimize duplicate code and keep the scripts simple to understand? How do you add new environments with minimal effort? Here are several techniques we’ve found for organizing gulp scripts.

Use an Environment Variable and Reduce Flags

The first step is to use gulp-util to read in an environmental variable. We also do our best to reduce other “flag” environmental variables passed in. Flag variables make it increasingly confusing to follow all the conditionals in the script and will inevitably begin to clog the gulp pipes if you are not careful. We recommend minimizing the number of flag variables that you pass into your gulp tasks. We’ve been able to get by with just passing in the single environment flag. Later in this post, we’ll go over how we can use this variable:

Copy
var env = gutil.env.env || 'development';]

Create Separate Files for Your Gulp Tasks

As your build scripts increase in size, you may want to separate your tasks into multiple files. Doing this is relatively simple – just drop all your tasks into a separate folder, and add this line into your gulp file. You will have to require gulp as well as any other plugins needed for each separate task. Use the “require-dir” package to do this:

Copy
var reqDir = require('require-dir'), tasks = reqDir('tasks/'); 

Create a Separate Config File and Include It in Your Tasks

Create a configuration file with four main sections to it. This file will hold information about the following:

  1. the source and destination paths for your build
  2. any environmental constants or variables used within your application (API hosts, etc.)
  3. a run list to toggle which gulp plugins run in each environment
  4. an object containing all the options for each gulp plugin

Each of your tasks will require this file so we can pass in the options to their corresponding plugin. For each of these sections, we will have a default object, plus an object for each environment. At the end of the script we’ll override the default settings with the environment-specific settings. This will give us the final configuration object that we will use in each of our tasks.

Remove Hard Coded Paths and Put them into a Separate Object

The first part of the config file is to put all your path information into a single object. Why? So you can see at a glance where all your source and destination files and folders are located. Also, it makes it easy to change the build configuration later if you decide to move files around:

Copy

var paths = {
  src: {
    js: 'app.js',
    constants: 'constants.js',
    html: 'index.html',
    css: 'style.css'
  },
  dest: {
    js: 'dist/',
    constants: 'dist/',
    html: 'dist/',
    css: 'dist/'
  }
};

Store Application Constants for Each Environment in Their Own Objects

The constants are static variables injected into your application. This may be a global variable that gets added to your compiled javascript, or, in the case of a project Fresh recently worked on, an Angular constant. These constants will typically hold variables set during the build process, such as the API host. We’ll have an object containing the constants we want for each environment:

Copy

var constants = {
  default: {
    apiHost: ''
  },
  development: {
    apiHost: 'http://localhost:9050'
  },
  staging: {
    apiHost: 'http://staging.example.com/api/'
  },
  production: {
    apiHost: 'http://example.com/api/'
  }
};

Create an Object for Toggling Plugins

Next, create an object to specify which plugins to toggle on or off for each environment build. In our experience, it’s easier to just leave the plugins off by default. We can then opt-in to specific plugins based on environment. This is used in conjunction with the gulp-if plugin to check each plugin’s run value in each task. Alternatively, we can pipe through to gulp-util noop function:

Copy

var run = {
  default: {
    js: {
      uglify: false
    },
    css: {
      cssnano: false
    }
  },
  development: {
    js: {
      uglify: false
    },
    css: {
      cssnano: false
    }
  },
  staging: {
    js: {
      uglify: true
    },
    css: {
      cssnano: true
    }
  },
  production: {
    js: {
      uglify: true
    },
    css: {
      cssnano: true
    }
  }
};

Store Plugin Options Into Its Own Object

We extract our plugin configuration options out of the gulp pipes and into a separate object. Why? It makes our gulp task dynamic by allowing us to pass in different options to the plugins, allowing different options per environment, per plugin. The result: our code stays DRY because we don’t need to create duplicate tasks per environment. We simply alter the config objects that get passed into the plugins:

Copy

var plugin = {
  default: {
    js: {
      uglify: {
        mangle: true
      }
    }
  },
  development: {
    js: {
      uglify: {
        mangle: false
      }
    }
  },
  staging: {
    js: {
      uglify: {
        mangle: true
      }
    }
  },
  production: {
    js: {
      uglify: {
        mangle: true
      }
    }
  }
};

Merge Environment-Specific Options with Default Options

Here is where we merge the environment-specific run config with the default run config, and the environment-specific plugin options with our default plugin options. We’ll also merge our environment-specific constants with our default constants. For convenience, we’ll employ the lodash utility library for its merge function. Lodash also allows a deep merge of the environment-specific objects with the default objects. Finally, we’ll attach these objects to module.exports, which will give our tasks access to the config:

Copy

var runOpts = _.merge({}, run.default, run[env]);
var pluginOpts = _.merge({}, plugin.default, plugin[env]);
var constantsOpts = _.merge({}, constants.default, constants[env]
);
module.exports.paths = paths;
module.exports.constants = constantsOpts;
module.exports.run = runOpts;
module.exports.plugin = pluginOpts;

Use the Config Object in Tasks

Now that you have your configuration objects exported, you can use them in tasks. Check the run values to determine if the stream should be piped to the corresponding plugin. If the run value is false, it will be piped through. You can pass in the plugin options to the corresponding plugins:

Copy

var gulp = require('gulp'),
  uglify = require('gulp-uglify'),
  gutil = require('gulp-util'),
  config = require('./config');
gulp.task('js', function () {
  return gulp
    .src(config.paths.src.js)
    .pipe(config.run.js.uglify ? uglify(config.plugin.js.uglify) : gutil.noop())
    .pipe(gulp.dest(config.paths.dest.js));
});

Auto-generate the Constants File

To get the constants into the application, you can generate a file using the gulp-file plugin. Read in the constants object from the configuration and generate the code for the constants file. The benefit of this is that any time you add a new constant, all you have to do is add it to the config and the constants module will automatically be generated:

Copy

var gulp = require('gulp'),
  file = require('gulp-file'),
  config = require('./config');
gulp.task('constants', function () {
  var constantsObjString, codeString, key, value;
  constantsObjString = '{';
  for (key in config.constantsOpts) {
    value = config.constantsOpts[key];
    if (typeof value === 'string') {
      value = '\'' + value + '\'';
    }
    constantsObjString += '\n ' + key + ': ' + value + ',';
  }
  // Remove the last comma
  constantsObjString = constantsObjString.substring(0, constantsObjString.length - 1);
  constantsObjString += '\n }';
  codeString = ';(function(){' +
    '\n angular.module(\'Constants\', [])' +
    '\n .constant(\'Constants\', ' + constantsObjString + ');' +
    '\n})();';
  return file('constants.js', codeString, { src: true })
    .pipe(gulp.dest(config.paths.dest.js));
});

Run Your Build

Now you can pass in the environment for your build. The script will set the configuration for all your tasks based on the environment passed in. If no environment is passed in, it will default to development, or whatever you’ve set as the default:

Copy
gulp build --env=production

Conclusion

Organizing gulp scripts makes it much easier to add new environments and make changes to the build without blowing things up. Even though this method may seem overkill for a small app, the benefits will become clear as you begin to add more to your build process. So far, organizing our gulp scripts in this manner has helped us save time and effort during development. And while this specific approach is designed for gulp, the concepts may be applicable with other tools and technologies.

Want more tips?

Check out our Dev Principles.

Travis Luong

Travis Luong

Unless otherwise specified, source code in this post is licensed under a
Creative Commons Attribution 4.0 International license (CC BY 4.0).

You might also like...

18

Jun.

James Lorentson

Autocomplete: Varieties, Benefits & UX Best Practices

For just about everything besides scenic drives, people prefer shortcuts. And if a shortcut not only gets you there quicker but also takes you to a better destination, now that’s something! This is what autocomplete does. By giving users the option of completing words and forms based on what they’ve typed before, it shortcuts their … Continued

7

Feb.

Jeff Dance

10 Factors for Choosing a CMS

There are dozens of Content Management System (CMS) platforms available to to help you manage the content, marketing, and SEO on your website. But with all the good options out there, how do you know how to choose the right CMS? Consider the following 10 factors when choosing your CMS. #1 Price Some CMS licenses start … Continued

2

Oct.

Michael Wiggins

How Will the Oracle Java Licensing Changes Affect You?

Earlier this year, Oracle announced that beginning January 1, 2019, it will no longer provide support and updates to Java SE 8. Instead, the support and updates that have been included as part of the Java license will now be available only through a separate subscription support service. As we are now well into the … Continued