Article

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

gulp-header-scaled

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:

[code]var env = gutil.env.env || ‘development’;][/code]

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:

[code]var reqDir = require(‘require-dir’), tasks = reqDir(‘tasks/’); [/code]

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:

[code]
var paths = {
src: {
js: ‘app.js’,
constants: ‘constants.js’,
html: ‘index.html’,
css: ‘style.css’
},
dest: {
js: ‘dist/’,
constants: ‘dist/’,
html: ‘dist/’,
css: ‘dist/’
}
};
[/code]

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:
[code]
var constants = {
default: {
apiHost: ”
},
development: {
apiHost: ‘https://localhost:9050’
},
staging: {
apiHost: ‘https://staging.example.com/api/’
},
production: {
apiHost: ‘https://example.com/api/’
}
};
[/code]

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:
[code]
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
}
}
};
[/code]

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:
[code]
var plugin = {
default: {
js: {
uglify: {
mangle: true
}
}
},
development: {
js: {
uglify: {
mangle: false
}
}
},
staging: {
js: {
uglify: {
mangle: true
}
}
},
production: {
js: {
uglify: {
mangle: true
}
}
}
};
[/code]

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:
[code]
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;
[/code]

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:
[code]
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));
});
[/code]

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:
[code]
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));
});
[/code]

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:
[code]gulp build –env=production[/code]

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.

Travis-Luong-Default-BW_optimize.jpg

Travis Luong

Full-Stack Developer

Travis is a Full-Stack Developer with 5 years of professional programming experience. Having worked as a freelance consultant, web contractor, and full-time engineer prior to joining Fresh, he excels at both server-side and client-side web development. While his expertise is in Ruby on Rails and Javascript, Travis is familiar with a variety of tools, languages, and frameworks. Most recently, he has worked on web and mobile apps for CBRE and Fornetix.